{
  "config": {
    "configFile": "/var/www/360lm/playwright.config.js",
    "rootDir": "/var/www/360lm/tests",
    "forbidOnly": false,
    "fullyParallel": false,
    "globalSetup": null,
    "globalTeardown": null,
    "globalTimeout": 0,
    "grep": {},
    "grepInvert": null,
    "maxFailures": 0,
    "metadata": {
      "actualWorkers": 1
    },
    "preserveOutput": "always",
    "projects": [
      {
        "outputDir": "/var/www/360lm/test-results",
        "repeatEach": 1,
        "retries": 1,
        "metadata": {
          "actualWorkers": 1
        },
        "id": "android-chrome",
        "name": "android-chrome",
        "testDir": "/var/www/360lm/tests",
        "testIgnore": [],
        "testMatch": [
          "**/*.@(spec|test).?(c|m)[jt]s?(x)"
        ],
        "timeout": 30000
      },
      {
        "outputDir": "/var/www/360lm/test-results",
        "repeatEach": 1,
        "retries": 1,
        "metadata": {
          "actualWorkers": 1
        },
        "id": "desktop-chrome",
        "name": "desktop-chrome",
        "testDir": "/var/www/360lm/tests",
        "testIgnore": [],
        "testMatch": [
          "**/*.@(spec|test).?(c|m)[jt]s?(x)"
        ],
        "timeout": 30000
      }
    ],
    "quiet": false,
    "reporter": [
      [
        "json"
      ]
    ],
    "reportSlowTests": {
      "max": 5,
      "threshold": 300000
    },
    "shard": null,
    "tags": [],
    "updateSnapshots": "missing",
    "updateSourceMethod": "patch",
    "version": "1.60.0",
    "workers": 1,
    "webServer": null
  },
  "suites": [
    {
      "title": "activity.spec.js",
      "file": "activity.spec.js",
      "column": 0,
      "line": 0,
      "specs": [],
      "suites": [
        {
          "title": "Activity — Smoke",
          "file": "activity.spec.js",
          "line": 10,
          "column": 6,
          "specs": [
            {
              "title": "page or hub loads",
              "ok": true,
              "tags": [],
              "tests": [
                {
                  "timeout": 30000,
                  "annotations": [],
                  "expectedStatus": "passed",
                  "projectId": "android-chrome",
                  "projectName": "android-chrome",
                  "results": [
                    {
                      "workerIndex": 0,
                      "parallelIndex": 0,
                      "status": "passed",
                      "duration": 1183,
                      "errors": [],
                      "stdout": [],
                      "stderr": [],
                      "retry": 0,
                      "startTime": "2026-07-12T12:57:21.712Z",
                      "annotations": [],
                      "attachments": []
                    }
                  ],
                  "status": "expected"
                }
              ],
              "id": "df521ef6570605d13829-45bb9da2781273921e33",
              "file": "activity.spec.js",
              "line": 11,
              "column": 3
            },
            {
              "title": "manifest.json returns 200",
              "ok": true,
              "tags": [],
              "tests": [
                {
                  "timeout": 30000,
                  "annotations": [],
                  "expectedStatus": "passed",
                  "projectId": "android-chrome",
                  "projectName": "android-chrome",
                  "results": [
                    {
                      "workerIndex": 0,
                      "parallelIndex": 0,
                      "status": "passed",
                      "duration": 189,
                      "errors": [],
                      "stdout": [],
                      "stderr": [],
                      "retry": 0,
                      "startTime": "2026-07-12T12:57:23.114Z",
                      "annotations": [],
                      "attachments": []
                    }
                  ],
                  "status": "expected"
                }
              ],
              "id": "df521ef6570605d13829-d6284674306e1787a594",
              "file": "activity.spec.js",
              "line": 18,
              "column": 3
            },
            {
              "title": "sw.js contains current version",
              "ok": true,
              "tags": [],
              "tests": [
                {
                  "timeout": 30000,
                  "annotations": [],
                  "expectedStatus": "passed",
                  "projectId": "android-chrome",
                  "projectName": "android-chrome",
                  "results": [
                    {
                      "workerIndex": 0,
                      "parallelIndex": 0,
                      "status": "passed",
                      "duration": 234,
                      "errors": [],
                      "stdout": [],
                      "stderr": [],
                      "retry": 0,
                      "startTime": "2026-07-12T12:57:23.335Z",
                      "annotations": [],
                      "attachments": []
                    }
                  ],
                  "status": "expected"
                }
              ],
              "id": "df521ef6570605d13829-bea7455145b8c2c4d860",
              "file": "activity.spec.js",
              "line": 23,
              "column": 3
            },
            {
              "title": "page or hub loads",
              "ok": true,
              "tags": [],
              "tests": [
                {
                  "timeout": 30000,
                  "annotations": [],
                  "expectedStatus": "passed",
                  "projectId": "desktop-chrome",
                  "projectName": "desktop-chrome",
                  "results": [
                    {
                      "workerIndex": 176,
                      "parallelIndex": 0,
                      "status": "passed",
                      "duration": 1071,
                      "errors": [],
                      "stdout": [],
                      "stderr": [],
                      "retry": 0,
                      "startTime": "2026-07-12T13:17:37.070Z",
                      "annotations": [],
                      "attachments": []
                    }
                  ],
                  "status": "expected"
                }
              ],
              "id": "df521ef6570605d13829-0f05c1db7f936b1770cf",
              "file": "activity.spec.js",
              "line": 11,
              "column": 3
            },
            {
              "title": "manifest.json returns 200",
              "ok": true,
              "tags": [],
              "tests": [
                {
                  "timeout": 30000,
                  "annotations": [],
                  "expectedStatus": "passed",
                  "projectId": "desktop-chrome",
                  "projectName": "desktop-chrome",
                  "results": [
                    {
                      "workerIndex": 176,
                      "parallelIndex": 0,
                      "status": "passed",
                      "duration": 216,
                      "errors": [],
                      "stdout": [],
                      "stderr": [],
                      "retry": 0,
                      "startTime": "2026-07-12T13:17:38.295Z",
                      "annotations": [],
                      "attachments": []
                    }
                  ],
                  "status": "expected"
                }
              ],
              "id": "df521ef6570605d13829-2112e78eca790609920b",
              "file": "activity.spec.js",
              "line": 18,
              "column": 3
            },
            {
              "title": "sw.js contains current version",
              "ok": true,
              "tags": [],
              "tests": [
                {
                  "timeout": 30000,
                  "annotations": [],
                  "expectedStatus": "passed",
                  "projectId": "desktop-chrome",
                  "projectName": "desktop-chrome",
                  "results": [
                    {
                      "workerIndex": 176,
                      "parallelIndex": 0,
                      "status": "passed",
                      "duration": 276,
                      "errors": [],
                      "stdout": [],
                      "stderr": [],
                      "retry": 0,
                      "startTime": "2026-07-12T13:17:38.541Z",
                      "annotations": [],
                      "attachments": []
                    }
                  ],
                  "status": "expected"
                }
              ],
              "id": "df521ef6570605d13829-a830c832290db1660126",
              "file": "activity.spec.js",
              "line": 23,
              "column": 3
            }
          ]
        },
        {
          "title": "Activity — HTML Structure (core)",
          "file": "activity.spec.js",
          "line": 31,
          "column": 6,
          "specs": [
            {
              "title": "capture screen and submit/draft buttons present",
              "ok": true,
              "tags": [],
              "tests": [
                {
                  "timeout": 30000,
                  "annotations": [],
                  "expectedStatus": "passed",
                  "projectId": "android-chrome",
                  "projectName": "android-chrome",
                  "results": [
                    {
                      "workerIndex": 0,
                      "parallelIndex": 0,
                      "status": "passed",
                      "duration": 79,
                      "errors": [],
                      "stdout": [],
                      "stderr": [],
                      "retry": 0,
                      "startTime": "2026-07-12T12:57:23.602Z",
                      "annotations": [],
                      "attachments": []
                    }
                  ],
                  "status": "expected"
                }
              ],
              "id": "df521ef6570605d13829-77f84d04d074f2d70a94",
              "file": "activity.spec.js",
              "line": 32,
              "column": 3
            },
            {
              "title": "response list and presentation screens present",
              "ok": true,
              "tags": [],
              "tests": [
                {
                  "timeout": 30000,
                  "annotations": [],
                  "expectedStatus": "passed",
                  "projectId": "android-chrome",
                  "projectName": "android-chrome",
                  "results": [
                    {
                      "workerIndex": 0,
                      "parallelIndex": 0,
                      "status": "passed",
                      "duration": 24,
                      "errors": [],
                      "stdout": [],
                      "stderr": [],
                      "retry": 0,
                      "startTime": "2026-07-12T12:57:23.701Z",
                      "annotations": [],
                      "attachments": []
                    }
                  ],
                  "status": "expected"
                }
              ],
              "id": "df521ef6570605d13829-3c3440e71d456b4fbe8a",
              "file": "activity.spec.js",
              "line": 47,
              "column": 3
            },
            {
              "title": "capture screen and submit/draft buttons present",
              "ok": true,
              "tags": [],
              "tests": [
                {
                  "timeout": 30000,
                  "annotations": [],
                  "expectedStatus": "passed",
                  "projectId": "desktop-chrome",
                  "projectName": "desktop-chrome",
                  "results": [
                    {
                      "workerIndex": 176,
                      "parallelIndex": 0,
                      "status": "passed",
                      "duration": 78,
                      "errors": [],
                      "stdout": [],
                      "stderr": [],
                      "retry": 0,
                      "startTime": "2026-07-12T13:17:38.845Z",
                      "annotations": [],
                      "attachments": []
                    }
                  ],
                  "status": "expected"
                }
              ],
              "id": "df521ef6570605d13829-7610f56fa1f0287f3249",
              "file": "activity.spec.js",
              "line": 32,
              "column": 3
            },
            {
              "title": "response list and presentation screens present",
              "ok": true,
              "tags": [],
              "tests": [
                {
                  "timeout": 30000,
                  "annotations": [],
                  "expectedStatus": "passed",
                  "projectId": "desktop-chrome",
                  "projectName": "desktop-chrome",
                  "results": [
                    {
                      "workerIndex": 176,
                      "parallelIndex": 0,
                      "status": "passed",
                      "duration": 32,
                      "errors": [],
                      "stdout": [],
                      "stderr": [],
                      "retry": 0,
                      "startTime": "2026-07-12T13:17:38.940Z",
                      "annotations": [],
                      "attachments": []
                    }
                  ],
                  "status": "expected"
                }
              ],
              "id": "df521ef6570605d13829-a28fb01019a5d12e16a6",
              "file": "activity.spec.js",
              "line": 47,
              "column": 3
            }
          ]
        },
        {
          "title": "Activity — HTML Structure (v21 draft datetime)",
          "file": "activity.spec.js",
          "line": 62,
          "column": 6,
          "specs": [
            {
              "title": "submit-datetime modal elements present",
              "ok": true,
              "tags": [],
              "tests": [
                {
                  "timeout": 30000,
                  "annotations": [],
                  "expectedStatus": "passed",
                  "projectId": "android-chrome",
                  "projectName": "android-chrome",
                  "results": [
                    {
                      "workerIndex": 0,
                      "parallelIndex": 0,
                      "status": "passed",
                      "duration": 20,
                      "errors": [],
                      "stdout": [],
                      "stderr": [],
                      "retry": 0,
                      "startTime": "2026-07-12T12:57:23.741Z",
                      "annotations": [],
                      "attachments": []
                    }
                  ],
                  "status": "expected"
                }
              ],
              "id": "df521ef6570605d13829-16370f92dcc0d957eeb0",
              "file": "activity.spec.js",
              "line": 63,
              "column": 3
            },
            {
              "title": "IST datetime helper functions present",
              "ok": true,
              "tags": [],
              "tests": [
                {
                  "timeout": 30000,
                  "annotations": [],
                  "expectedStatus": "passed",
                  "projectId": "android-chrome",
                  "projectName": "android-chrome",
                  "results": [
                    {
                      "workerIndex": 0,
                      "parallelIndex": 0,
                      "status": "passed",
                      "duration": 17,
                      "errors": [],
                      "stdout": [],
                      "stderr": [],
                      "retry": 0,
                      "startTime": "2026-07-12T12:57:23.784Z",
                      "annotations": [],
                      "attachments": []
                    }
                  ],
                  "status": "expected"
                }
              ],
              "id": "df521ef6570605d13829-7fc08d7e1d426a968e27",
              "file": "activity.spec.js",
              "line": 78,
              "column": 3
            },
            {
              "title": "submission datetime picker flow functions present",
              "ok": true,
              "tags": [],
              "tests": [
                {
                  "timeout": 30000,
                  "annotations": [],
                  "expectedStatus": "passed",
                  "projectId": "android-chrome",
                  "projectName": "android-chrome",
                  "results": [
                    {
                      "workerIndex": 0,
                      "parallelIndex": 0,
                      "status": "passed",
                      "duration": 20,
                      "errors": [],
                      "stdout": [],
                      "stderr": [],
                      "retry": 0,
                      "startTime": "2026-07-12T12:57:23.819Z",
                      "annotations": [],
                      "attachments": []
                    }
                  ],
                  "status": "expected"
                }
              ],
              "id": "df521ef6570605d13829-ae0062346cb3397cc1f4",
              "file": "activity.spec.js",
              "line": 91,
              "column": 3
            },
            {
              "title": "audit trail CSS and audit panel in buildPresentation present",
              "ok": true,
              "tags": [],
              "tests": [
                {
                  "timeout": 30000,
                  "annotations": [],
                  "expectedStatus": "passed",
                  "projectId": "android-chrome",
                  "projectName": "android-chrome",
                  "results": [
                    {
                      "workerIndex": 0,
                      "parallelIndex": 0,
                      "status": "passed",
                      "duration": 13,
                      "errors": [],
                      "stdout": [],
                      "stderr": [],
                      "retry": 0,
                      "startTime": "2026-07-12T12:57:23.854Z",
                      "annotations": [],
                      "attachments": []
                    }
                  ],
                  "status": "expected"
                }
              ],
              "id": "df521ef6570605d13829-c3260fe8ff436c5cd715",
              "file": "activity.spec.js",
              "line": 105,
              "column": 3
            },
            {
              "title": "draft PATCH includes submitted_at and error guard",
              "ok": true,
              "tags": [],
              "tests": [
                {
                  "timeout": 30000,
                  "annotations": [],
                  "expectedStatus": "passed",
                  "projectId": "android-chrome",
                  "projectName": "android-chrome",
                  "results": [
                    {
                      "workerIndex": 0,
                      "parallelIndex": 0,
                      "status": "passed",
                      "duration": 9,
                      "errors": [],
                      "stdout": [],
                      "stderr": [],
                      "retry": 0,
                      "startTime": "2026-07-12T12:57:23.877Z",
                      "annotations": [],
                      "attachments": []
                    }
                  ],
                  "status": "expected"
                }
              ],
              "id": "df521ef6570605d13829-c63db10b1de7f63715bf",
              "file": "activity.spec.js",
              "line": 121,
              "column": 3
            },
            {
              "title": "submit-datetime modal elements present",
              "ok": true,
              "tags": [],
              "tests": [
                {
                  "timeout": 30000,
                  "annotations": [],
                  "expectedStatus": "passed",
                  "projectId": "desktop-chrome",
                  "projectName": "desktop-chrome",
                  "results": [
                    {
                      "workerIndex": 176,
                      "parallelIndex": 0,
                      "status": "passed",
                      "duration": 31,
                      "errors": [],
                      "stdout": [],
                      "stderr": [],
                      "retry": 0,
                      "startTime": "2026-07-12T13:17:38.985Z",
                      "annotations": [],
                      "attachments": []
                    }
                  ],
                  "status": "expected"
                }
              ],
              "id": "df521ef6570605d13829-3204473521eed1129c8c",
              "file": "activity.spec.js",
              "line": 63,
              "column": 3
            },
            {
              "title": "IST datetime helper functions present",
              "ok": true,
              "tags": [],
              "tests": [
                {
                  "timeout": 30000,
                  "annotations": [],
                  "expectedStatus": "passed",
                  "projectId": "desktop-chrome",
                  "projectName": "desktop-chrome",
                  "results": [
                    {
                      "workerIndex": 176,
                      "parallelIndex": 0,
                      "status": "passed",
                      "duration": 20,
                      "errors": [],
                      "stdout": [],
                      "stderr": [],
                      "retry": 0,
                      "startTime": "2026-07-12T13:17:39.031Z",
                      "annotations": [],
                      "attachments": []
                    }
                  ],
                  "status": "expected"
                }
              ],
              "id": "df521ef6570605d13829-a7c7cb170591b9ae6d16",
              "file": "activity.spec.js",
              "line": 78,
              "column": 3
            },
            {
              "title": "submission datetime picker flow functions present",
              "ok": true,
              "tags": [],
              "tests": [
                {
                  "timeout": 30000,
                  "annotations": [],
                  "expectedStatus": "passed",
                  "projectId": "desktop-chrome",
                  "projectName": "desktop-chrome",
                  "results": [
                    {
                      "workerIndex": 176,
                      "parallelIndex": 0,
                      "status": "passed",
                      "duration": 29,
                      "errors": [],
                      "stdout": [],
                      "stderr": [],
                      "retry": 0,
                      "startTime": "2026-07-12T13:17:39.066Z",
                      "annotations": [],
                      "attachments": []
                    }
                  ],
                  "status": "expected"
                }
              ],
              "id": "df521ef6570605d13829-124f71d700a952519bbd",
              "file": "activity.spec.js",
              "line": 91,
              "column": 3
            },
            {
              "title": "audit trail CSS and audit panel in buildPresentation present",
              "ok": true,
              "tags": [],
              "tests": [
                {
                  "timeout": 30000,
                  "annotations": [],
                  "expectedStatus": "passed",
                  "projectId": "desktop-chrome",
                  "projectName": "desktop-chrome",
                  "results": [
                    {
                      "workerIndex": 176,
                      "parallelIndex": 0,
                      "status": "passed",
                      "duration": 13,
                      "errors": [],
                      "stdout": [],
                      "stderr": [],
                      "retry": 0,
                      "startTime": "2026-07-12T13:17:39.115Z",
                      "annotations": [],
                      "attachments": []
                    }
                  ],
                  "status": "expected"
                }
              ],
              "id": "df521ef6570605d13829-aadb3c0f20eca04815f5",
              "file": "activity.spec.js",
              "line": 105,
              "column": 3
            },
            {
              "title": "draft PATCH includes submitted_at and error guard",
              "ok": true,
              "tags": [],
              "tests": [
                {
                  "timeout": 30000,
                  "annotations": [],
                  "expectedStatus": "passed",
                  "projectId": "desktop-chrome",
                  "projectName": "desktop-chrome",
                  "results": [
                    {
                      "workerIndex": 176,
                      "parallelIndex": 0,
                      "status": "passed",
                      "duration": 14,
                      "errors": [],
                      "stdout": [],
                      "stderr": [],
                      "retry": 0,
                      "startTime": "2026-07-12T13:17:39.141Z",
                      "annotations": [],
                      "attachments": []
                    }
                  ],
                  "status": "expected"
                }
              ],
              "id": "df521ef6570605d13829-7c8b03df43309d0e3ef7",
              "file": "activity.spec.js",
              "line": 121,
              "column": 3
            }
          ]
        },
        {
          "title": "Activity — HTML Structure (v23 cascading filters)",
          "file": "activity.spec.js",
          "line": 134,
          "column": 6,
          "specs": [
            {
              "title": "audit trail check uses ADMIN_IDS not isAdmin",
              "ok": true,
              "tags": [],
              "tests": [
                {
                  "timeout": 30000,
                  "annotations": [],
                  "expectedStatus": "passed",
                  "projectId": "android-chrome",
                  "projectName": "android-chrome",
                  "results": [
                    {
                      "workerIndex": 0,
                      "parallelIndex": 0,
                      "status": "passed",
                      "duration": 11,
                      "errors": [],
                      "stdout": [],
                      "stderr": [],
                      "retry": 0,
                      "startTime": "2026-07-12T12:57:23.898Z",
                      "annotations": [],
                      "attachments": []
                    }
                  ],
                  "status": "expected"
                }
              ],
              "id": "df521ef6570605d13829-05066d6a9f247b0e561b",
              "file": "activity.spec.js",
              "line": 135,
              "column": 3
            },
            {
              "title": "draft-btn text reset in buildCaptureForm",
              "ok": true,
              "tags": [],
              "tests": [
                {
                  "timeout": 30000,
                  "annotations": [],
                  "expectedStatus": "passed",
                  "projectId": "android-chrome",
                  "projectName": "android-chrome",
                  "results": [
                    {
                      "workerIndex": 0,
                      "parallelIndex": 0,
                      "status": "passed",
                      "duration": 16,
                      "errors": [],
                      "stdout": [],
                      "stderr": [],
                      "retry": 0,
                      "startTime": "2026-07-12T12:57:23.920Z",
                      "annotations": [],
                      "attachments": []
                    }
                  ],
                  "status": "expected"
                }
              ],
              "id": "df521ef6570605d13829-22f8c98b04c7f4d09112",
              "file": "activity.spec.js",
              "line": 146,
              "column": 3
            },
            {
              "title": "client-side min-date validation in confirmSubmitDatetime",
              "ok": true,
              "tags": [],
              "tests": [
                {
                  "timeout": 30000,
                  "annotations": [],
                  "expectedStatus": "passed",
                  "projectId": "android-chrome",
                  "projectName": "android-chrome",
                  "results": [
                    {
                      "workerIndex": 0,
                      "parallelIndex": 0,
                      "status": "passed",
                      "duration": 12,
                      "errors": [],
                      "stdout": [],
                      "stderr": [],
                      "retry": 0,
                      "startTime": "2026-07-12T12:57:23.948Z",
                      "annotations": [],
                      "attachments": []
                    }
                  ],
                  "status": "expected"
                }
              ],
              "id": "df521ef6570605d13829-baba157449c608af4acd",
              "file": "activity.spec.js",
              "line": 156,
              "column": 3
            },
            {
              "title": "server error message extracted from PostgREST JSON",
              "ok": true,
              "tags": [],
              "tests": [
                {
                  "timeout": 30000,
                  "annotations": [],
                  "expectedStatus": "passed",
                  "projectId": "android-chrome",
                  "projectName": "android-chrome",
                  "results": [
                    {
                      "workerIndex": 0,
                      "parallelIndex": 0,
                      "status": "passed",
                      "duration": 13,
                      "errors": [],
                      "stdout": [],
                      "stderr": [],
                      "retry": 0,
                      "startTime": "2026-07-12T12:57:23.970Z",
                      "annotations": [],
                      "attachments": []
                    }
                  ],
                  "status": "expected"
                }
              ],
              "id": "df521ef6570605d13829-83c8b0c6216f28424f1f",
              "file": "activity.spec.js",
              "line": 167,
              "column": 3
            },
            {
              "title": "hint text uses human-readable format",
              "ok": true,
              "tags": [],
              "tests": [
                {
                  "timeout": 30000,
                  "annotations": [],
                  "expectedStatus": "passed",
                  "projectId": "android-chrome",
                  "projectName": "android-chrome",
                  "results": [
                    {
                      "workerIndex": 0,
                      "parallelIndex": 0,
                      "status": "passed",
                      "duration": 16,
                      "errors": [],
                      "stdout": [],
                      "stderr": [],
                      "retry": 0,
                      "startTime": "2026-07-12T12:57:23.994Z",
                      "annotations": [],
                      "attachments": []
                    }
                  ],
                  "status": "expected"
                }
              ],
              "id": "df521ef6570605d13829-5b7638844423a2799628",
              "file": "activity.spec.js",
              "line": 177,
              "column": 3
            },
            {
              "title": "cascading filter — computeAvailable and matchesFiltersExcept present",
              "ok": true,
              "tags": [],
              "tests": [
                {
                  "timeout": 30000,
                  "annotations": [],
                  "expectedStatus": "passed",
                  "projectId": "android-chrome",
                  "projectName": "android-chrome",
                  "results": [
                    {
                      "workerIndex": 0,
                      "parallelIndex": 0,
                      "status": "passed",
                      "duration": 13,
                      "errors": [],
                      "stdout": [],
                      "stderr": [],
                      "retry": 0,
                      "startTime": "2026-07-12T12:57:24.022Z",
                      "annotations": [],
                      "attachments": []
                    }
                  ],
                  "status": "expected"
                }
              ],
              "id": "df521ef6570605d13829-d98e836565a5867d0c7e",
              "file": "activity.spec.js",
              "line": 188,
              "column": 3
            },
            {
              "title": "cascading filter — status split into Lead Type + Submission rows",
              "ok": true,
              "tags": [],
              "tests": [
                {
                  "timeout": 30000,
                  "annotations": [],
                  "expectedStatus": "passed",
                  "projectId": "android-chrome",
                  "projectName": "android-chrome",
                  "results": [
                    {
                      "workerIndex": 0,
                      "parallelIndex": 0,
                      "status": "passed",
                      "duration": 13,
                      "errors": [],
                      "stdout": [],
                      "stderr": [],
                      "retry": 0,
                      "startTime": "2026-07-12T12:57:24.046Z",
                      "annotations": [],
                      "attachments": []
                    }
                  ],
                  "status": "expected"
                }
              ],
              "id": "df521ef6570605d13829-dccccd0142bf3cf29769",
              "file": "activity.spec.js",
              "line": 199,
              "column": 3
            },
            {
              "title": "cascading filter — sequence badge CSS present",
              "ok": true,
              "tags": [],
              "tests": [
                {
                  "timeout": 30000,
                  "annotations": [],
                  "expectedStatus": "passed",
                  "projectId": "android-chrome",
                  "projectName": "android-chrome",
                  "results": [
                    {
                      "workerIndex": 0,
                      "parallelIndex": 0,
                      "status": "passed",
                      "duration": 13,
                      "errors": [],
                      "stdout": [],
                      "stderr": [],
                      "retry": 0,
                      "startTime": "2026-07-12T12:57:24.070Z",
                      "annotations": [],
                      "attachments": []
                    }
                  ],
                  "status": "expected"
                }
              ],
              "id": "df521ef6570605d13829-5c6d3c53b52be256b4c1",
              "file": "activity.spec.js",
              "line": 213,
              "column": 3
            },
            {
              "title": "cascading filter — stats panel is filter-aware",
              "ok": true,
              "tags": [],
              "tests": [
                {
                  "timeout": 30000,
                  "annotations": [],
                  "expectedStatus": "passed",
                  "projectId": "android-chrome",
                  "projectName": "android-chrome",
                  "results": [
                    {
                      "workerIndex": 0,
                      "parallelIndex": 0,
                      "status": "passed",
                      "duration": 10,
                      "errors": [],
                      "stdout": [],
                      "stderr": [],
                      "retry": 0,
                      "startTime": "2026-07-12T12:57:24.093Z",
                      "annotations": [],
                      "attachments": []
                    }
                  ],
                  "status": "expected"
                }
              ],
              "id": "df521ef6570605d13829-fd3da6eb4749870ff2aa",
              "file": "activity.spec.js",
              "line": 225,
              "column": 3
            },
            {
              "title": "audit trail check uses ADMIN_IDS not isAdmin",
              "ok": true,
              "tags": [],
              "tests": [
                {
                  "timeout": 30000,
                  "annotations": [],
                  "expectedStatus": "passed",
                  "projectId": "desktop-chrome",
                  "projectName": "desktop-chrome",
                  "results": [
                    {
                      "workerIndex": 176,
                      "parallelIndex": 0,
                      "status": "passed",
                      "duration": 11,
                      "errors": [],
                      "stdout": [],
                      "stderr": [],
                      "retry": 0,
                      "startTime": "2026-07-12T13:17:39.167Z",
                      "annotations": [],
                      "attachments": []
                    }
                  ],
                  "status": "expected"
                }
              ],
              "id": "df521ef6570605d13829-c8befe1e0a8dda0871e7",
              "file": "activity.spec.js",
              "line": 135,
              "column": 3
            },
            {
              "title": "draft-btn text reset in buildCaptureForm",
              "ok": true,
              "tags": [],
              "tests": [
                {
                  "timeout": 30000,
                  "annotations": [],
                  "expectedStatus": "passed",
                  "projectId": "desktop-chrome",
                  "projectName": "desktop-chrome",
                  "results": [
                    {
                      "workerIndex": 176,
                      "parallelIndex": 0,
                      "status": "passed",
                      "duration": 11,
                      "errors": [],
                      "stdout": [],
                      "stderr": [],
                      "retry": 0,
                      "startTime": "2026-07-12T13:17:39.188Z",
                      "annotations": [],
                      "attachments": []
                    }
                  ],
                  "status": "expected"
                }
              ],
              "id": "df521ef6570605d13829-906574b62a27c3eb7f7a",
              "file": "activity.spec.js",
              "line": 146,
              "column": 3
            },
            {
              "title": "client-side min-date validation in confirmSubmitDatetime",
              "ok": true,
              "tags": [],
              "tests": [
                {
                  "timeout": 30000,
                  "annotations": [],
                  "expectedStatus": "passed",
                  "projectId": "desktop-chrome",
                  "projectName": "desktop-chrome",
                  "results": [
                    {
                      "workerIndex": 176,
                      "parallelIndex": 0,
                      "status": "passed",
                      "duration": 11,
                      "errors": [],
                      "stdout": [],
                      "stderr": [],
                      "retry": 0,
                      "startTime": "2026-07-12T13:17:39.209Z",
                      "annotations": [],
                      "attachments": []
                    }
                  ],
                  "status": "expected"
                }
              ],
              "id": "df521ef6570605d13829-c7b6ed3fd63d74e0b6ae",
              "file": "activity.spec.js",
              "line": 156,
              "column": 3
            },
            {
              "title": "server error message extracted from PostgREST JSON",
              "ok": true,
              "tags": [],
              "tests": [
                {
                  "timeout": 30000,
                  "annotations": [],
                  "expectedStatus": "passed",
                  "projectId": "desktop-chrome",
                  "projectName": "desktop-chrome",
                  "results": [
                    {
                      "workerIndex": 176,
                      "parallelIndex": 0,
                      "status": "passed",
                      "duration": 10,
                      "errors": [],
                      "stdout": [],
                      "stderr": [],
                      "retry": 0,
                      "startTime": "2026-07-12T13:17:39.231Z",
                      "annotations": [],
                      "attachments": []
                    }
                  ],
                  "status": "expected"
                }
              ],
              "id": "df521ef6570605d13829-883777665f5cc6cca6a0",
              "file": "activity.spec.js",
              "line": 167,
              "column": 3
            },
            {
              "title": "hint text uses human-readable format",
              "ok": true,
              "tags": [],
              "tests": [
                {
                  "timeout": 30000,
                  "annotations": [],
                  "expectedStatus": "passed",
                  "projectId": "desktop-chrome",
                  "projectName": "desktop-chrome",
                  "results": [
                    {
                      "workerIndex": 176,
                      "parallelIndex": 0,
                      "status": "passed",
                      "duration": 22,
                      "errors": [],
                      "stdout": [],
                      "stderr": [],
                      "retry": 0,
                      "startTime": "2026-07-12T13:17:39.255Z",
                      "annotations": [],
                      "attachments": []
                    }
                  ],
                  "status": "expected"
                }
              ],
              "id": "df521ef6570605d13829-b3520d121514076d339a",
              "file": "activity.spec.js",
              "line": 177,
              "column": 3
            },
            {
              "title": "cascading filter — computeAvailable and matchesFiltersExcept present",
              "ok": true,
              "tags": [],
              "tests": [
                {
                  "timeout": 30000,
                  "annotations": [],
                  "expectedStatus": "passed",
                  "projectId": "desktop-chrome",
                  "projectName": "desktop-chrome",
                  "results": [
                    {
                      "workerIndex": 176,
                      "parallelIndex": 0,
                      "status": "passed",
                      "duration": 11,
                      "errors": [],
                      "stdout": [],
                      "stderr": [],
                      "retry": 0,
                      "startTime": "2026-07-12T13:17:39.296Z",
                      "annotations": [],
                      "attachments": []
                    }
                  ],
                  "status": "expected"
                }
              ],
              "id": "df521ef6570605d13829-b67c6919a96c2ec3c1a9",
              "file": "activity.spec.js",
              "line": 188,
              "column": 3
            },
            {
              "title": "cascading filter — status split into Lead Type + Submission rows",
              "ok": true,
              "tags": [],
              "tests": [
                {
                  "timeout": 30000,
                  "annotations": [],
                  "expectedStatus": "passed",
                  "projectId": "desktop-chrome",
                  "projectName": "desktop-chrome",
                  "results": [
                    {
                      "workerIndex": 176,
                      "parallelIndex": 0,
                      "status": "passed",
                      "duration": 24,
                      "errors": [],
                      "stdout": [],
                      "stderr": [],
                      "retry": 0,
                      "startTime": "2026-07-12T13:17:39.320Z",
                      "annotations": [],
                      "attachments": []
                    }
                  ],
                  "status": "expected"
                }
              ],
              "id": "df521ef6570605d13829-5064cb61a86a4e40bf45",
              "file": "activity.spec.js",
              "line": 199,
              "column": 3
            },
            {
              "title": "cascading filter — sequence badge CSS present",
              "ok": true,
              "tags": [],
              "tests": [
                {
                  "timeout": 30000,
                  "annotations": [],
                  "expectedStatus": "passed",
                  "projectId": "desktop-chrome",
                  "projectName": "desktop-chrome",
                  "results": [
                    {
                      "workerIndex": 176,
                      "parallelIndex": 0,
                      "status": "passed",
                      "duration": 16,
                      "errors": [],
                      "stdout": [],
                      "stderr": [],
                      "retry": 0,
                      "startTime": "2026-07-12T13:17:39.360Z",
                      "annotations": [],
                      "attachments": []
                    }
                  ],
                  "status": "expected"
                }
              ],
              "id": "df521ef6570605d13829-e6d9f0118ec03ce3639d",
              "file": "activity.spec.js",
              "line": 213,
              "column": 3
            },
            {
              "title": "cascading filter — stats panel is filter-aware",
              "ok": true,
              "tags": [],
              "tests": [
                {
                  "timeout": 30000,
                  "annotations": [],
                  "expectedStatus": "passed",
                  "projectId": "desktop-chrome",
                  "projectName": "desktop-chrome",
                  "results": [
                    {
                      "workerIndex": 176,
                      "parallelIndex": 0,
                      "status": "passed",
                      "duration": 9,
                      "errors": [],
                      "stdout": [],
                      "stderr": [],
                      "retry": 0,
                      "startTime": "2026-07-12T13:17:39.389Z",
                      "annotations": [],
                      "attachments": []
                    }
                  ],
                  "status": "expected"
                }
              ],
              "id": "df521ef6570605d13829-d74f8e534587325d728b",
              "file": "activity.spec.js",
              "line": 225,
              "column": 3
            }
          ]
        },
        {
          "title": "Activity — Hub Navigation",
          "file": "activity.spec.js",
          "line": 238,
          "column": 6,
          "specs": [
            {
              "title": "home topbar has Hub button",
              "ok": true,
              "tags": [],
              "tests": [
                {
                  "timeout": 30000,
                  "annotations": [],
                  "expectedStatus": "passed",
                  "projectId": "android-chrome",
                  "projectName": "android-chrome",
                  "results": [
                    {
                      "workerIndex": 0,
                      "parallelIndex": 0,
                      "status": "passed",
                      "duration": 10,
                      "errors": [],
                      "stdout": [],
                      "stderr": [],
                      "retry": 0,
                      "startTime": "2026-07-12T12:57:24.111Z",
                      "annotations": [],
                      "attachments": []
                    }
                  ],
                  "status": "expected"
                }
              ],
              "id": "df521ef6570605d13829-00d54e0526426c598f17",
              "file": "activity.spec.js",
              "line": 239,
              "column": 3
            },
            {
              "title": "sw.js is v25 or later",
              "ok": true,
              "tags": [],
              "tests": [
                {
                  "timeout": 30000,
                  "annotations": [],
                  "expectedStatus": "passed",
                  "projectId": "android-chrome",
                  "projectName": "android-chrome",
                  "results": [
                    {
                      "workerIndex": 0,
                      "parallelIndex": 0,
                      "status": "passed",
                      "duration": 10,
                      "errors": [],
                      "stdout": [],
                      "stderr": [],
                      "retry": 0,
                      "startTime": "2026-07-12T12:57:24.132Z",
                      "annotations": [],
                      "attachments": []
                    }
                  ],
                  "status": "expected"
                }
              ],
              "id": "df521ef6570605d13829-09bfb43732ec0dd0e518",
              "file": "activity.spec.js",
              "line": 248,
              "column": 3
            },
            {
              "title": "home topbar has Hub button",
              "ok": true,
              "tags": [],
              "tests": [
                {
                  "timeout": 30000,
                  "annotations": [],
                  "expectedStatus": "passed",
                  "projectId": "desktop-chrome",
                  "projectName": "desktop-chrome",
                  "results": [
                    {
                      "workerIndex": 176,
                      "parallelIndex": 0,
                      "status": "passed",
                      "duration": 10,
                      "errors": [],
                      "stdout": [],
                      "stderr": [],
                      "retry": 0,
                      "startTime": "2026-07-12T13:17:39.405Z",
                      "annotations": [],
                      "attachments": []
                    }
                  ],
                  "status": "expected"
                }
              ],
              "id": "df521ef6570605d13829-455a5260a3cc4c799541",
              "file": "activity.spec.js",
              "line": 239,
              "column": 3
            },
            {
              "title": "sw.js is v25 or later",
              "ok": true,
              "tags": [],
              "tests": [
                {
                  "timeout": 30000,
                  "annotations": [],
                  "expectedStatus": "passed",
                  "projectId": "desktop-chrome",
                  "projectName": "desktop-chrome",
                  "results": [
                    {
                      "workerIndex": 176,
                      "parallelIndex": 0,
                      "status": "passed",
                      "duration": 11,
                      "errors": [],
                      "stdout": [],
                      "stderr": [],
                      "retry": 0,
                      "startTime": "2026-07-12T13:17:39.424Z",
                      "annotations": [],
                      "attachments": []
                    }
                  ],
                  "status": "expected"
                }
              ],
              "id": "df521ef6570605d13829-3333643baa7542543de2",
              "file": "activity.spec.js",
              "line": 248,
              "column": 3
            }
          ]
        }
      ]
    },
    {
      "title": "dispatch.spec.js",
      "file": "dispatch.spec.js",
      "column": 0,
      "line": 0,
      "specs": [],
      "suites": [
        {
          "title": "Dispatch — Smoke",
          "file": "dispatch.spec.js",
          "line": 9,
          "column": 6,
          "specs": [
            {
              "title": "page or hub loads",
              "ok": true,
              "tags": [],
              "tests": [
                {
                  "timeout": 30000,
                  "annotations": [],
                  "expectedStatus": "passed",
                  "projectId": "android-chrome",
                  "projectName": "android-chrome",
                  "results": [
                    {
                      "workerIndex": 0,
                      "parallelIndex": 0,
                      "status": "passed",
                      "duration": 1253,
                      "errors": [],
                      "stdout": [],
                      "stderr": [],
                      "retry": 0,
                      "startTime": "2026-07-12T12:57:24.161Z",
                      "annotations": [],
                      "attachments": []
                    }
                  ],
                  "status": "expected"
                }
              ],
              "id": "3510226c3ce1e611dacb-63b703f798c6b89c1844",
              "file": "dispatch.spec.js",
              "line": 10,
              "column": 3
            },
            {
              "title": "manifest.json returns 200",
              "ok": true,
              "tags": [],
              "tests": [
                {
                  "timeout": 30000,
                  "annotations": [],
                  "expectedStatus": "passed",
                  "projectId": "android-chrome",
                  "projectName": "android-chrome",
                  "results": [
                    {
                      "workerIndex": 0,
                      "parallelIndex": 0,
                      "status": "passed",
                      "duration": 170,
                      "errors": [],
                      "stdout": [],
                      "stderr": [],
                      "retry": 0,
                      "startTime": "2026-07-12T12:57:25.429Z",
                      "annotations": [],
                      "attachments": []
                    }
                  ],
                  "status": "expected"
                }
              ],
              "id": "3510226c3ce1e611dacb-0abf5d31404d250a90af",
              "file": "dispatch.spec.js",
              "line": 17,
              "column": 3
            },
            {
              "title": "sw.js is versioned",
              "ok": true,
              "tags": [],
              "tests": [
                {
                  "timeout": 30000,
                  "annotations": [],
                  "expectedStatus": "passed",
                  "projectId": "android-chrome",
                  "projectName": "android-chrome",
                  "results": [
                    {
                      "workerIndex": 0,
                      "parallelIndex": 0,
                      "status": "passed",
                      "duration": 224,
                      "errors": [],
                      "stdout": [],
                      "stderr": [],
                      "retry": 0,
                      "startTime": "2026-07-12T12:57:25.612Z",
                      "annotations": [],
                      "attachments": []
                    }
                  ],
                  "status": "expected"
                }
              ],
              "id": "3510226c3ce1e611dacb-37601325b18bf433a5b0",
              "file": "dispatch.spec.js",
              "line": 22,
              "column": 3
            },
            {
              "title": "page or hub loads",
              "ok": true,
              "tags": [],
              "tests": [
                {
                  "timeout": 30000,
                  "annotations": [],
                  "expectedStatus": "passed",
                  "projectId": "desktop-chrome",
                  "projectName": "desktop-chrome",
                  "results": [
                    {
                      "workerIndex": 176,
                      "parallelIndex": 0,
                      "status": "passed",
                      "duration": 1392,
                      "errors": [],
                      "stdout": [],
                      "stderr": [],
                      "retry": 0,
                      "startTime": "2026-07-12T13:17:39.456Z",
                      "annotations": [],
                      "attachments": []
                    }
                  ],
                  "status": "expected"
                }
              ],
              "id": "3510226c3ce1e611dacb-79724089e7cb2fee990f",
              "file": "dispatch.spec.js",
              "line": 10,
              "column": 3
            },
            {
              "title": "manifest.json returns 200",
              "ok": true,
              "tags": [],
              "tests": [
                {
                  "timeout": 30000,
                  "annotations": [],
                  "expectedStatus": "passed",
                  "projectId": "desktop-chrome",
                  "projectName": "desktop-chrome",
                  "results": [
                    {
                      "workerIndex": 176,
                      "parallelIndex": 0,
                      "status": "passed",
                      "duration": 221,
                      "errors": [],
                      "stdout": [],
                      "stderr": [],
                      "retry": 0,
                      "startTime": "2026-07-12T13:17:40.873Z",
                      "annotations": [],
                      "attachments": []
                    }
                  ],
                  "status": "expected"
                }
              ],
              "id": "3510226c3ce1e611dacb-8c4b10c8bbd0540d96ee",
              "file": "dispatch.spec.js",
              "line": 17,
              "column": 3
            },
            {
              "title": "sw.js is versioned",
              "ok": true,
              "tags": [],
              "tests": [
                {
                  "timeout": 30000,
                  "annotations": [],
                  "expectedStatus": "passed",
                  "projectId": "desktop-chrome",
                  "projectName": "desktop-chrome",
                  "results": [
                    {
                      "workerIndex": 176,
                      "parallelIndex": 0,
                      "status": "passed",
                      "duration": 241,
                      "errors": [],
                      "stdout": [],
                      "stderr": [],
                      "retry": 0,
                      "startTime": "2026-07-12T13:17:41.121Z",
                      "annotations": [],
                      "attachments": []
                    }
                  ],
                  "status": "expected"
                }
              ],
              "id": "3510226c3ce1e611dacb-ada0bd95f6a8d1b606c5",
              "file": "dispatch.spec.js",
              "line": 22,
              "column": 3
            }
          ]
        },
        {
          "title": "Dispatch — HTML Structure",
          "file": "dispatch.spec.js",
          "line": 29,
          "column": 6,
          "specs": [
            {
              "title": "all panes present",
              "ok": true,
              "tags": [],
              "tests": [
                {
                  "timeout": 30000,
                  "annotations": [],
                  "expectedStatus": "passed",
                  "projectId": "android-chrome",
                  "projectName": "android-chrome",
                  "results": [
                    {
                      "workerIndex": 0,
                      "parallelIndex": 0,
                      "status": "passed",
                      "duration": 5,
                      "errors": [],
                      "stdout": [],
                      "stderr": [],
                      "retry": 0,
                      "startTime": "2026-07-12T12:57:25.860Z",
                      "annotations": [],
                      "attachments": []
                    }
                  ],
                  "status": "expected"
                }
              ],
              "id": "3510226c3ce1e611dacb-c8dadba4d4aafc5b2f57",
              "file": "dispatch.spec.js",
              "line": 39,
              "column": 3
            },
            {
              "title": "hub navigation button present",
              "ok": true,
              "tags": [],
              "tests": [
                {
                  "timeout": 30000,
                  "annotations": [],
                  "expectedStatus": "passed",
                  "projectId": "android-chrome",
                  "projectName": "android-chrome",
                  "results": [
                    {
                      "workerIndex": 0,
                      "parallelIndex": 0,
                      "status": "passed",
                      "duration": 5,
                      "errors": [],
                      "stdout": [],
                      "stderr": [],
                      "retry": 0,
                      "startTime": "2026-07-12T12:57:25.919Z",
                      "annotations": [],
                      "attachments": []
                    }
                  ],
                  "status": "expected"
                }
              ],
              "id": "3510226c3ce1e611dacb-e2726a9cf4782c036a25",
              "file": "dispatch.spec.js",
              "line": 47,
              "column": 3
            },
            {
              "title": "hub auth gate present",
              "ok": false,
              "tags": [],
              "tests": [
                {
                  "timeout": 30000,
                  "annotations": [],
                  "expectedStatus": "passed",
                  "projectId": "android-chrome",
                  "projectName": "android-chrome",
                  "results": [
                    {
                      "workerIndex": 0,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 30,
                      "error": {
                        "message": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoContain\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m) // indexOf\u001b[22m\n\nExpected substring: \u001b[32m\"window.location.replace('/hub/')\"\u001b[39m\nReceived string:    \u001b[31m\"<!DOCTYPE html>\u001b[39m\n\u001b[31m<html lang=\\\"en\\\">\u001b[39m\n\u001b[31m<head>\u001b[39m\n\u001b[31m<meta charset=\\\"UTF-8\\\">\u001b[39m\n\u001b[31m<meta name=\\\"viewport\\\" content=\\\"width=device-width,initial-scale=1,maximum-scale=1,user-scalable=no\\\">\u001b[39m\n\u001b[31m<meta name=\\\"theme-color\\\" content=\\\"#080c12\\\">\u001b[39m\n\u001b[31m<meta name=\\\"apple-mobile-web-app-capable\\\" content=\\\"yes\\\">\u001b[39m\n\u001b[31m<meta name=\\\"mobile-web-app-capable\\\" content=\\\"yes\\\">\u001b[39m\n\u001b[31m<title>360LM Dispatch</title>\u001b[39m\n\u001b[31m<link rel=\\\"manifest\\\" href=\\\"manifest.json\\\">\u001b[39m\n\u001b[31m<link href=\\\"https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@300;400;500;600;700&family=Fira+Code:wght@400;500&family=Barlow+Condensed:wght@400;600;700;800&display=swap\\\" rel=\\\"stylesheet\\\">\u001b[39m\n\u001b[31m<style>\u001b[39m\n\u001b[31m/* ════ RESET & VARS ════ */\u001b[39m\n\u001b[31m*{margin:0;padding:0;box-sizing:border-box;-webkit-tap-highlight-color:transparent;}\u001b[39m\n\u001b[31m:root{\u001b[39m\n\u001b[31m  --bg:#080c12; --bg2:#0d1420; --bg3:#111a28;\u001b[39m\n\u001b[31m  --sur:#141f30; --sur2:#1a2840; --sur3:#1f3050;\u001b[39m\n\u001b[31m  --bdr:#1e2e45; --bdr2:#253650;\u001b[39m\n\u001b[31m  --t1:#f0f4f8; --t2:#8ba4bc; --t3:#3d5470; --t4:#243348;\u001b[39m\n\u001b[31m  --acc:#f97316; --acc2:#fb923c; --acc-glow:rgba(249,115,22,0.25);\u001b[39m\n\u001b[31m  --grn:#22c55e; --grn-dim:rgba(34,197,94,0.15);\u001b[39m\n\u001b[31m  --sky:#38bdf8; --sky-dim:rgba(56,189,248,0.15);\u001b[39m\n\u001b[31m  --red:#ef4444; --red-dim:rgba(239,68,68,0.15);\u001b[39m\n\u001b[31m  --ylw:#eab308; --ylw-dim:rgba(234,179,8,0.15);\u001b[39m\n\u001b[31m  --fnt-head:'Barlow Condensed',sans-serif;\u001b[39m\n\u001b[31m  --fnt-body:'Space Grotesk',sans-serif;\u001b[39m\n\u001b[31m  --fnt-mono:'Fira Code',monospace;\u001b[39m\n\u001b[31m  --r:10px; --r-sm:6px; --r-lg:16px;\u001b[39m\n\u001b[31m  --sh:0 4px 24px rgba(0,0,0,0.4);\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31mhtml,body{height:100%;background:var(--bg);color:var(--t1);font-family:var(--fnt-body);overflow:hidden;}·\u001b[39m\n\u001b[31m/* ════ SPLASH ════ */\u001b[39m\n\u001b[31m#splash{\u001b[39m\n\u001b[31m  position:fixed;inset:0;background:var(--bg);z-index:999;\u001b[39m\n\u001b[31m  display:flex;flex-direction:column;align-items:center;justify-content:center;\u001b[39m\n\u001b[31m  transition:opacity .4s .1s;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.splash-logo{font-family:var(--fnt-head);font-size:3rem;font-weight:800;letter-spacing:2px;line-height:1;}\u001b[39m\n\u001b[31m.splash-logo em{color:var(--acc);font-style:normal;}\u001b[39m\n\u001b[31m.splash-sub{color:var(--t3);font-family:var(--fnt-mono);font-size:.7rem;letter-spacing:4px;margin-top:8px;}\u001b[39m\n\u001b[31m.splash-track{width:200px;height:2px;background:var(--bdr);border-radius:2px;margin-top:40px;overflow:hidden;}\u001b[39m\n\u001b[31m.splash-track-fill{height:100%;width:0;background:linear-gradient(90deg,var(--acc),var(--acc2));animation:splashLoad 1.3s cubic-bezier(.4,0,.2,1) forwards;}\u001b[39m\n\u001b[31m@keyframes splashLoad{to{width:100%;}}·\u001b[39m\n\u001b[31m/* ════ LAYOUT ════ */\u001b[39m\n\u001b[31m#app{height:100%;display:flex;flex-direction:column;}\u001b[39m\n\u001b[31m.hdr{\u001b[39m\n\u001b[31m  background:var(--bg2);border-bottom:1px solid var(--bdr);\u001b[39m\n\u001b[31m  padding:10px 14px;display:flex;align-items:center;gap:10px;flex-shrink:0;\u001b[39m\n\u001b[31m  position:relative;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.hdr-brand{font-family:var(--fnt-head);font-size:1.3rem;font-weight:800;letter-spacing:1px;flex:1;}\u001b[39m\n\u001b[31m.hdr-brand em{color:var(--acc);font-style:normal;}\u001b[39m\n\u001b[31m.hdr-dot{width:7px;height:7px;border-radius:50%;background:var(--grn);transition:background .3s;animation:blink 2s infinite;}\u001b[39m\n\u001b[31m@keyframes blink{0%,100%{opacity:1;}50%{opacity:.4;}}\u001b[39m\n\u001b[31m.hdr-btn{\u001b[39m\n\u001b[31m  background:none;border:none;color:var(--t2);cursor:pointer;\u001b[39m\n\u001b[31m  padding:6px 8px;border-radius:var(--r-sm);transition:all .2s;\u001b[39m\n\u001b[31m  font-family:var(--fnt-head);font-size:.85rem;font-weight:700;letter-spacing:.5px;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.hdr-btn:hover{background:var(--bdr);color:var(--t1);}\u001b[39m\n\u001b[31m.hdr-btn.primary{background:var(--acc);color:#fff;padding:6px 14px;}\u001b[39m\n\u001b[31m.hdr-btn.primary:hover{background:var(--acc2);}·\u001b[39m\n\u001b[31m/* ════ TAB BAR ════ */\u001b[39m\n\u001b[31m.tabs{\u001b[39m\n\u001b[31m  display:flex;background:var(--bg2);border-bottom:1px solid var(--bdr);\u001b[39m\n\u001b[31m  overflow-x:auto;flex-shrink:0;padding:0 6px;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.tabs::-webkit-scrollbar{display:none;}\u001b[39m\n\u001b[31m.tab{\u001b[39m\n\u001b[31m  background:none;border:none;color:var(--t3);font-family:var(--fnt-head);\u001b[39m\n\u001b[31m  font-size:.85rem;font-weight:700;letter-spacing:1px;padding:10px 14px;\u001b[39m\n\u001b[31m  cursor:pointer;border-bottom:2px solid transparent;white-space:nowrap;\u001b[39m\n\u001b[31m  transition:all .2s;flex-shrink:0;display:flex;align-items:center;gap:6px;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.tab.active{color:var(--acc);border-bottom-color:var(--acc);}\u001b[39m\n\u001b[31m.tab:hover:not(.active){color:var(--t2);}\u001b[39m\n\u001b[31m.tab-badge{\u001b[39m\n\u001b[31m  background:var(--acc);color:#fff;font-size:.6rem;font-weight:700;\u001b[39m\n\u001b[31m  padding:1px 5px;border-radius:10px;font-family:var(--fnt-mono);\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m/* ════ CONTENT AREAS ════ */\u001b[39m\n\u001b[31m.pane{flex:1;overflow-y:auto;padding:14px;display:none;}\u001b[39m\n\u001b[31m.pane.active{display:block;}\u001b[39m\n\u001b[31m.pane::-webkit-scrollbar{width:3px;}\u001b[39m\n\u001b[31m.pane::-webkit-scrollbar-thumb{background:var(--bdr2);}·\u001b[39m\n\u001b[31m/* ════ CARDS ════ */\u001b[39m\n\u001b[31m.card{\u001b[39m\n\u001b[31m  background:var(--sur);border:1px solid var(--bdr);border-radius:var(--r);\u001b[39m\n\u001b[31m  padding:14px;margin-bottom:12px;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.card.accent-left{border-left:3px solid var(--acc);}\u001b[39m\n\u001b[31m.card-title{\u001b[39m\n\u001b[31m  font-family:var(--fnt-head);font-size:.72rem;font-weight:700;\u001b[39m\n\u001b[31m  color:var(--t3);text-transform:uppercase;letter-spacing:2.5px;\u001b[39m\n\u001b[31m  margin-bottom:12px;display:flex;align-items:center;gap:8px;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.card-title .ct-dot{width:5px;height:5px;border-radius:50%;background:var(--acc);flex-shrink:0;}·\u001b[39m\n\u001b[31m/* ════ FORM ════ */\u001b[39m\n\u001b[31m.f{margin-bottom:12px;}\u001b[39m\n\u001b[31m.f label{\u001b[39m\n\u001b[31m  display:block;font-size:.68rem;color:var(--t3);margin-bottom:5px;\u001b[39m\n\u001b[31m  font-weight:600;text-transform:uppercase;letter-spacing:1.5px;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.f label .req{color:var(--acc);margin-left:2px;}\u001b[39m\n\u001b[31m.f input,.f select,.f textarea{\u001b[39m\n\u001b[31m  width:100%;background:var(--sur2);border:1px solid var(--bdr);\u001b[39m\n\u001b[31m  border-radius:var(--r-sm);color:var(--t1);font-family:var(--fnt-body);\u001b[39m\n\u001b[31m  font-size:.88rem;padding:9px 11px;outline:none;transition:border-color .2s;\u001b[39m\n\u001b[31m  -webkit-appearance:none;appearance:none;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.f input:focus,.f select:focus,.f textarea:focus{border-color:var(--acc);}\u001b[39m\n\u001b[31m.f input::placeholder{color:var(--t4);}\u001b[39m\n\u001b[31m.f textarea{resize:vertical;min-height:65px;}\u001b[39m\n\u001b[31m.f select{\u001b[39m\n\u001b[31m  background-image:url(\\\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='7'%3E%3Cpath d='M1 1l4 4 4-4' stroke='%233d5470' stroke-width='1.5' fill='none' stroke-linecap='round'/%3E%3C/svg%3E\\\");\u001b[39m\n\u001b[31m  background-repeat:no-repeat;background-position:right 10px center;padding-right:28px;cursor:pointer;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.row2{display:grid;grid-template-columns:1fr 1fr;gap:10px;}\u001b[39m\n\u001b[31m.row3{display:grid;grid-template-columns:1fr 1fr 1fr;gap:8px;}·\u001b[39m\n\u001b[31m/* ════ BUTTONS ════ */\u001b[39m\n\u001b[31m.btn{\u001b[39m\n\u001b[31m  width:100%;padding:12px;border:none;border-radius:var(--r);\u001b[39m\n\u001b[31m  font-family:var(--fnt-head);font-size:.9rem;font-weight:700;letter-spacing:.5px;\u001b[39m\n\u001b[31m  cursor:pointer;transition:all .2s;display:flex;align-items:center;justify-content:center;gap:8px;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.btn-orange{background:var(--acc);color:#fff;}\u001b[39m\n\u001b[31m.btn-orange:hover{background:var(--acc2);}\u001b[39m\n\u001b[31m.btn-ghost{background:var(--sur2);border:1px solid var(--bdr);color:var(--t1);}\u001b[39m\n\u001b[31m.btn-ghost:hover{border-color:var(--acc);color:var(--acc);}\u001b[39m\n\u001b[31m.btn-grn{background:var(--grn);color:#000;}\u001b[39m\n\u001b[31m.btn-grn:hover{opacity:.85;}\u001b[39m\n\u001b[31m.btn:active{transform:scale(.98);}\u001b[39m\n\u001b[31m.btn-row{display:grid;grid-template-columns:1fr 1fr;gap:10px;margin-top:6px;}\u001b[39m\n\u001b[31m.btn-sm{\u001b[39m\n\u001b[31m  background:none;border:1px solid var(--bdr);color:var(--t2);\u001b[39m\n\u001b[31m  font-size:.7rem;padding:5px 10px;border-radius:var(--r-sm);cursor:pointer;\u001b[39m\n\u001b[31m  font-family:var(--fnt-body);transition:all .2s;white-space:nowrap;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.btn-sm:hover{border-color:var(--acc);color:var(--acc);}\u001b[39m\n\u001b[31m.btn-sm.primary{background:var(--acc);border-color:var(--acc);color:#fff;}·\u001b[39m\n\u001b[31m/* ════ OCR UPLOAD ZONE ════ */\u001b[39m\n\u001b[31m.upload-zone{\u001b[39m\n\u001b[31m  border:2px dashed var(--bdr2);border-radius:var(--r);\u001b[39m\n\u001b[31m  padding:20px;text-align:center;cursor:pointer;transition:all .25s;\u001b[39m\n\u001b[31m  background:var(--sur2);position:relative;overflow:hidden;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.upload-zone:hover,.upload-zone.drag{border-color:var(--acc);background:rgba(249,115,22,.05);}\u001b[39m\n\u001b[31m.upload-zone .uz-icon{font-size:2rem;margin-bottom:8px;}\u001b[39m\n\u001b[31m.upload-zone .uz-title{font-family:var(--fnt-head);font-size:1rem;font-weight:700;color:var(--t1);}\u001b[39m\n\u001b[31m.upload-zone .uz-sub{font-size:.72rem;color:var(--t3);margin-top:4px;font-family:var(--fnt-mono);}\u001b[39m\n\u001b[31m.upload-zone input{display:none;}\u001b[39m\n\u001b[31m.upload-zone.share-target{border-color:var(--grn);}·\u001b[39m\n\u001b[31m/* ════ OCR RESULTS ════ */\u001b[39m\n\u001b[31m.ocr-results{display:none;}\u001b[39m\n\u001b[31m.ocr-results.visible{display:block;}\u001b[39m\n\u001b[31m.ocr-slip-list{display:flex;flex-direction:column;gap:8px;margin-top:10px;}\u001b[39m\n\u001b[31m.ocr-slip{\u001b[39m\n\u001b[31m  background:var(--sur2);border:1px solid var(--bdr);border-radius:var(--r-sm);\u001b[39m\n\u001b[31m  padding:10px 12px;display:flex;gap:10px;align-items:flex-start;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.ocr-slip img{width:60px;height:60px;object-fit:cover;border-radius:6px;flex-shrink:0;border:1px solid var(--bdr2);}\u001b[39m\n\u001b[31m.ocr-slip-body{flex:1;min-width:0;}\u001b[39m\n\u001b[31m.ocr-slip-name{font-size:.7rem;color:var(--t3);font-family:var(--fnt-mono);margin-bottom:4px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}\u001b[39m\n\u001b[31m.ocr-slip-docket{\u001b[39m\n\u001b[31m  font-family:var(--fnt-mono);font-size:.9rem;font-weight:500;color:var(--acc);\u001b[39m\n\u001b[31m  background:var(--bg);border:1px solid var(--bdr);border-radius:4px;\u001b[39m\n\u001b[31m  padding:4px 8px;width:100%;outline:none;transition:border-color .2s;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.ocr-slip-docket:focus{border-color:var(--acc);}\u001b[39m\n\u001b[31m.ocr-slip-status{font-size:.65rem;margin-top:4px;display:flex;align-items:center;gap:6px;}\u001b[39m\n\u001b[31m.ocr-confidence{padding:2px 6px;border-radius:10px;font-family:var(--fnt-mono);}\u001b[39m\n\u001b[31m.conf-high{background:var(--grn-dim);color:var(--grn);}\u001b[39m\n\u001b[31m.conf-med{background:var(--ylw-dim);color:var(--ylw);}\u001b[39m\n\u001b[31m.conf-low{background:var(--red-dim);color:var(--red);}\u001b[39m\n\u001b[31m.ocr-ai-btn{\u001b[39m\n\u001b[31m  background:rgba(56,189,248,.15);border:1px solid rgba(56,189,248,.3);\u001b[39m\n\u001b[31m  color:var(--sky);font-size:.65rem;padding:3px 8px;border-radius:10px;\u001b[39m\n\u001b[31m  cursor:pointer;font-family:var(--fnt-mono);transition:all .2s;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.ocr-ai-btn:hover{background:rgba(56,189,248,.3);}\u001b[39m\n\u001b[31m.ocr-working{\u001b[39m\n\u001b[31m  background:var(--ylw-dim);border:1px solid rgba(234,179,8,.3);\u001b[39m\n\u001b[31m  border-radius:var(--r-sm);padding:8px 12px;margin-top:8px;\u001b[39m\n\u001b[31m  font-size:.75rem;color:var(--ylw);font-family:var(--fnt-mono);\u001b[39m\n\u001b[31m  display:none;align-items:center;gap:8px;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.ocr-working.visible{display:flex;}\u001b[39m\n\u001b[31m.spin{animation:spin .8s linear infinite;display:inline-block;}\u001b[39m\n\u001b[31m@keyframes spin{to{transform:rotate(360deg);}}·\u001b[39m\n\u001b[31m/* ════ STORE LIST ════ */\u001b[39m\n\u001b[31m.store-upload-zone{\u001b[39m\n\u001b[31m  border:2px dashed var(--bdr2);border-radius:var(--r);padding:16px;\u001b[39m\n\u001b[31m  text-align:center;cursor:pointer;transition:all .25s;background:var(--sur2);\u001b[39m\n\u001b[31m  position:relative;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.store-upload-zone:hover{border-color:var(--sky);background:rgba(56,189,248,.05);}\u001b[39m\n\u001b[31m.store-upload-zone input{display:none;}\u001b[39m\n\u001b[31m.store-table-wrap{max-height:200px;overflow-y:auto;margin-top:10px;border-radius:var(--r-sm);overflow:hidden;}\u001b[39m\n\u001b[31m.store-table{width:100%;border-collapse:collapse;font-size:.75rem;}\u001b[39m\n\u001b[31m.store-table th{\u001b[39m\n\u001b[31m  background:var(--bg);color:var(--t3);font-family:var(--fnt-head);\u001b[39m\n\u001b[31m  font-size:.65rem;letter-spacing:1.5px;text-transform:uppercase;\u001b[39m\n\u001b[31m  padding:7px 8px;text-align:left;border-bottom:1px solid var(--bdr);\u001b[39m\n\u001b[31m  position:sticky;top:0;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.store-table td{\u001b[39m\n\u001b[31m  padding:6px 8px;border-bottom:1px solid var(--bdr);color:var(--t2);\u001b[39m\n\u001b[31m  font-family:var(--fnt-mono);font-size:.72rem;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.store-table tr:hover td{background:var(--sur3);}\u001b[39m\n\u001b[31m.store-table .matched{color:var(--grn);}\u001b[39m\n\u001b[31m.store-table .unmatched{color:var(--ylw);}\u001b[39m\n\u001b[31m.store-map-btn{\u001b[39m\n\u001b[31m  font-size:.65rem;background:none;border:1px solid var(--bdr);color:var(--t3);\u001b[39m\n\u001b[31m  padding:2px 6px;border-radius:4px;cursor:pointer;transition:all .2s;\u001b[39m\n\u001b[31m  font-family:var(--fnt-mono);\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.store-map-btn:hover{border-color:var(--acc);color:var(--acc);}·\u001b[39m\n\u001b[31m/* ════ CONSIGNEE CARDS ════ */\u001b[39m\n\u001b[31m.cons-list{display:flex;flex-direction:column;gap:8px;margin-bottom:10px;}\u001b[39m\n\u001b[31m.cons-card{\u001b[39m\n\u001b[31m  background:var(--sur2);border:1px solid var(--bdr);border-radius:var(--r-sm);\u001b[39m\n\u001b[31m  padding:11px;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.cons-hdr{display:flex;justify-content:space-between;align-items:center;margin-bottom:8px;}\u001b[39m\n\u001b[31m.cons-num{font-family:var(--fnt-mono);font-size:.65rem;color:var(--t3);}\u001b[39m\n\u001b[31m.cons-del{\u001b[39m\n\u001b[31m  background:none;border:none;color:var(--t4);cursor:pointer;\u001b[39m\n\u001b[31m  font-size:.85rem;padding:2px 5px;border-radius:4px;transition:all .2s;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.cons-del:hover{background:var(--red-dim);color:var(--red);}\u001b[39m\n\u001b[31m.cons-docket-linked{\u001b[39m\n\u001b[31m  display:flex;align-items:center;gap:6px;\u001b[39m\n\u001b[31m  background:rgba(249,115,22,.1);border:1px solid rgba(249,115,22,.3);\u001b[39m\n\u001b[31m  border-radius:4px;padding:4px 8px;margin-top:6px;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.cons-docket-linked .dkt{font-family:var(--fnt-mono);font-size:.82rem;color:var(--acc);}\u001b[39m\n\u001b[31m.cons-docket-linked .unlink{background:none;border:none;color:var(--acc);cursor:pointer;font-size:.75rem;}·\u001b[39m\n\u001b[31m/* ════ DISPATCH DASHBOARD ════ */\u001b[39m\n\u001b[31m.dash-stats{\u001b[39m\n\u001b[31m  display:grid;grid-template-columns:repeat(4,1fr);gap:8px;margin-bottom:14px;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.stat-box{\u001b[39m\n\u001b[31m  background:var(--sur);border:1px solid var(--bdr);border-radius:var(--r);\u001b[39m\n\u001b[31m  padding:10px 8px;text-align:center;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.stat-val{font-family:var(--fnt-head);font-size:1.4rem;font-weight:800;}\u001b[39m\n\u001b[31m.stat-lbl{font-size:.6rem;color:var(--t3);font-family:var(--fnt-mono);text-transform:uppercase;letter-spacing:1px;margin-top:2px;}\u001b[39m\n\u001b[31m.stat-box.grn .stat-val{color:var(--grn);}\u001b[39m\n\u001b[31m.stat-box.ylw .stat-val{color:var(--ylw);}\u001b[39m\n\u001b[31m.stat-box.red .stat-val{color:var(--red);}\u001b[39m\n\u001b[31m.stat-box.sky .stat-val{color:var(--sky);}·\u001b[39m\n\u001b[31m/* ════ DOCKET TILES ════ */\u001b[39m\n\u001b[31m.docket-grid{display:flex;flex-direction:column;gap:8px;}\u001b[39m\n\u001b[31m.docket-tile{\u001b[39m\n\u001b[31m  background:var(--sur);border:1px solid var(--bdr);border-radius:var(--r);\u001b[39m\n\u001b[31m  padding:12px;transition:border-color .2s;cursor:pointer;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.docket-tile:hover{border-color:var(--acc);}\u001b[39m\n\u001b[31m.docket-tile-hdr{display:flex;justify-content:space-between;align-items:flex-start;margin-bottom:8px;}\u001b[39m\n\u001b[31m.docket-tile-left{flex:1;}\u001b[39m\n\u001b[31m.docket-num{font-family:var(--fnt-mono);font-size:.95rem;font-weight:500;color:var(--sky);letter-spacing:1px;}\u001b[39m\n\u001b[31m.docket-store{font-size:.82rem;color:var(--t1);font-weight:600;margin-top:2px;}\u001b[39m\n\u001b[31m.docket-courier{font-size:.68rem;color:var(--t3);font-family:var(--fnt-mono);margin-top:2px;}\u001b[39m\n\u001b[31m.status-pill{\u001b[39m\n\u001b[31m  font-size:.62rem;font-weight:700;font-family:var(--fnt-mono);\u001b[39m\n\u001b[31m  padding:3px 8px;border-radius:10px;text-transform:uppercase;letter-spacing:.5px;\u001b[39m\n\u001b[31m  flex-shrink:0;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.sp-delivered{background:var(--grn-dim);color:var(--grn);}\u001b[39m\n\u001b[31m.sp-transit{background:var(--ylw-dim);color:var(--ylw);}\u001b[39m\n\u001b[31m.sp-booked{background:var(--sky-dim);color:var(--sky);}\u001b[39m\n\u001b[31m.sp-pending{background:var(--bdr);color:var(--t3);}\u001b[39m\n\u001b[31m.sp-failed{background:var(--red-dim);color:var(--red);}\u001b[39m\n\u001b[31m.docket-tile-footer{display:flex;align-items:center;gap:8px;flex-wrap:wrap;}\u001b[39m\n\u001b[31m.docket-last-event{font-size:.7rem;color:var(--t3);flex:1;font-family:var(--fnt-mono);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}\u001b[39m\n\u001b[31m.btn-track-live{\u001b[39m\n\u001b[31m  background:none;border:1px solid var(--acc);color:var(--acc);\u001b[39m\n\u001b[31m  font-size:.65rem;padding:4px 10px;border-radius:20px;cursor:pointer;\u001b[39m\n\u001b[31m  font-family:var(--fnt-mono);transition:all .2s;white-space:nowrap;display:flex;align-items:center;gap:4px;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.btn-track-live:hover{background:var(--acc);color:#fff;}\u001b[39m\n\u001b[31m.btn-track-live .live-dot{width:5px;height:5px;border-radius:50%;background:var(--grn);animation:blink 1.5s infinite;flex-shrink:0;}\u001b[39m\n\u001b[31m.docket-progress{height:2px;background:var(--bdr);border-radius:2px;margin-top:8px;overflow:hidden;}\u001b[39m\n\u001b[31m.docket-progress-fill{height:100%;border-radius:2px;transition:width .5s;}·\u001b[39m\n\u001b[31m/* ════ TRACKING DETAIL ════ */\u001b[39m\n\u001b[31m.track-timeline{padding:4px 0;}\u001b[39m\n\u001b[31m.track-evt{display:flex;gap:10px;padding:6px 0;position:relative;padding-left:20px;}\u001b[39m\n\u001b[31m.track-evt::before{\u001b[39m\n\u001b[31m  content:'';position:absolute;left:5px;top:11px;\u001b[39m\n\u001b[31m  width:7px;height:7px;border-radius:50%;background:var(--bdr2);border:1px solid var(--bdr2);\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.track-evt:first-child::before{background:var(--grn);border-color:var(--grn);}\u001b[39m\n\u001b[31m.track-evt::after{\u001b[39m\n\u001b[31m  content:'';position:absolute;left:8px;top:18px;\u001b[39m\n\u001b[31m  width:1px;height:calc(100% - 6px);background:var(--bdr);\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.track-evt:last-child::after{display:none;}\u001b[39m\n\u001b[31m.track-time{font-family:var(--fnt-mono);font-size:.62rem;color:var(--t3);min-width:90px;flex-shrink:0;padding-top:2px;}\u001b[39m\n\u001b[31m.track-desc{font-size:.78rem;color:var(--t2);}\u001b[39m\n\u001b[31m.track-loc{font-size:.67rem;color:var(--t3);font-family:var(--fnt-mono);margin-top:2px;}·\u001b[39m\n\u001b[31m/* ════ SHARE TOAST ════ */\u001b[39m\n\u001b[31m.share-banner{\u001b[39m\n\u001b[31m  position:fixed;top:0;left:0;right:0;z-index:200;\u001b[39m\n\u001b[31m  background:rgba(34,197,94,.15);border-bottom:1px solid rgba(34,197,94,.4);\u001b[39m\n\u001b[31m  padding:10px 16px;font-size:.78rem;color:var(--grn);font-family:var(--fnt-mono);\u001b[39m\n\u001b[31m  display:flex;align-items:center;gap:8px;\u001b[39m\n\u001b[31m  transform:translateY(-100%);transition:transform .3s;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.share-banner.show{transform:translateY(0);}·\u001b[39m\n\u001b[31m/* ════ MODAL ════ */\u001b[39m\n\u001b[31m.overlay{position:fixed;inset:0;background:rgba(0,0,0,.75);z-index:100;display:flex;align-items:flex-end;opacity:0;pointer-events:none;transition:opacity .3s;}\u001b[39m\n\u001b[31m.overlay.open{opacity:1;pointer-events:all;}\u001b[39m\n\u001b[31m.sheet{\u001b[39m\n\u001b[31m  background:var(--sur);border-radius:var(--r-lg) var(--r-lg) 0 0;\u001b[39m\n\u001b[31m  width:100%;max-height:88vh;overflow-y:auto;padding:18px;\u001b[39m\n\u001b[31m  transform:translateY(100%);transition:transform .3s cubic-bezier(.32,.72,0,1);\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.overlay.open .sheet{transform:translateY(0);}\u001b[39m\n\u001b[31m.sheet-handle{width:36px;height:4px;background:var(--bdr2);border-radius:2px;margin:0 auto 18px;}\u001b[39m\n\u001b[31m.sheet-title{font-family:var(--fnt-head);font-size:1.2rem;font-weight:800;letter-spacing:.5px;margin-bottom:14px;}\u001b[39m\n\u001b[31m.sheet::-webkit-scrollbar{width:3px;}\u001b[39m\n\u001b[31m.sheet::-webkit-scrollbar-thumb{background:var(--bdr2);}·\u001b[39m\n\u001b[31m/* ════ TOAST ════ */\u001b[39m\n\u001b[31m.toast{\u001b[39m\n\u001b[31m  position:fixed;bottom:20px;left:50%;transform:translateX(-50%) translateY(70px);\u001b[39m\n\u001b[31m  background:var(--sur2);border:1px solid var(--bdr2);color:var(--t1);\u001b[39m\n\u001b[31m  font-size:.78rem;padding:9px 16px;border-radius:30px;z-index:300;\u001b[39m\n\u001b[31m  transition:transform .3s;white-space:nowrap;font-family:var(--fnt-mono);\u001b[39m\n\u001b[31m  pointer-events:none;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.toast.show{transform:translateX(-50%) translateY(0);}\u001b[39m\n\u001b[31m.toast.ok{border-color:var(--grn);color:var(--grn);}\u001b[39m\n\u001b[31m.toast.err{border-color:var(--red);color:var(--red);}\u001b[39m\n\u001b[31m.toast.info{border-color:var(--sky);color:var(--sky);}·\u001b[39m\n\u001b[31m/* ════ COURIER MASTER ════ */\u001b[39m\n\u001b[31m.courier-master-row{\u001b[39m\n\u001b[31m  display:flex;align-items:center;gap:8px;padding:8px 0;\u001b[39m\n\u001b[31m  border-bottom:1px solid var(--bdr);\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.courier-master-row:last-child{border-bottom:none;}\u001b[39m\n\u001b[31m.cml{flex:1;}\u001b[39m\n\u001b[31m.cml-name{font-weight:600;font-size:.85rem;}\u001b[39m\n\u001b[31m.cml-url{font-family:var(--fnt-mono);font-size:.65rem;color:var(--t3);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:180px;}\u001b[39m\n\u001b[31m.courier-logo{width:24px;height:24px;border-radius:4px;object-fit:contain;background:var(--sur3);padding:2px;}·\u001b[39m\n\u001b[31m/* ════ PROGRESS BAR ════ */\u001b[39m\n\u001b[31m.ai-progress{\u001b[39m\n\u001b[31m  background:var(--sur2);border:1px solid var(--bdr);border-radius:var(--r-sm);\u001b[39m\n\u001b[31m  padding:10px 12px;margin-top:8px;display:none;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.ai-progress.visible{display:block;}\u001b[39m\n\u001b[31m.ai-prog-bar{height:3px;background:var(--bdr);border-radius:3px;margin-top:6px;overflow:hidden;}\u001b[39m\n\u001b[31m.ai-prog-fill{height:100%;background:linear-gradient(90deg,var(--sky),var(--acc));width:0;transition:width .3s;}\u001b[39m\n\u001b[31m.ai-prog-label{font-size:.68rem;color:var(--sky);font-family:var(--fnt-mono);}·\u001b[39m\n\u001b[31m/* ════ EMPTY ════ */\u001b[39m\n\u001b[31m.empty{padding:40px 20px;text-align:center;color:var(--t4);}\u001b[39m\n\u001b[31m.empty .e-icon{font-size:2.5rem;margin-bottom:10px;}\u001b[39m\n\u001b[31m.empty p{font-size:.82rem;}·\u001b[39m\n\u001b[31m/* ════ INFO TAG ════ */\u001b[39m\n\u001b[31m.info-tag{\u001b[39m\n\u001b[31m  background:var(--sur2);border:1px solid var(--bdr);border-radius:var(--r-sm);\u001b[39m\n\u001b[31m  padding:7px 10px;font-size:.75rem;color:var(--t2);margin-bottom:8px;\u001b[39m\n\u001b[31m  display:flex;align-items:center;gap:8px;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.info-tag strong{color:var(--t1);}·\u001b[39m\n\u001b[31m/* ════ JOB PILL ════ */\u001b[39m\n\u001b[31m.job-pill{\u001b[39m\n\u001b[31m  display:inline-flex;align-items:center;gap:6px;\u001b[39m\n\u001b[31m  background:rgba(249,115,22,.1);border:1px solid rgba(249,115,22,.3);\u001b[39m\n\u001b[31m  color:var(--acc);font-family:var(--fnt-mono);font-size:.68rem;\u001b[39m\n\u001b[31m  padding:3px 9px;border-radius:20px;margin-top:4px;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.job-pill button{background:none;border:none;color:var(--acc);cursor:pointer;font-size:.7rem;}·\u001b[39m\n\u001b[31m/* ════ SECTION DIVIDER ════ */\u001b[39m\n\u001b[31m.sec-div{\u001b[39m\n\u001b[31m  display:flex;align-items:center;gap:10px;margin:14px 0 10px;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.sec-div span{font-family:var(--fnt-head);font-size:.7rem;font-weight:700;color:var(--t3);text-transform:uppercase;letter-spacing:2px;white-space:nowrap;}\u001b[39m\n\u001b[31m.sec-div::before,.sec-div::after{content:'';flex:1;height:1px;background:var(--bdr);}·\u001b[39m\n\u001b[31m/* ── MAPPING UI ── */\u001b[39m\n\u001b[31m.map-table{width:100%;border-collapse:collapse;font-size:.78rem;margin:10px 0;}\u001b[39m\n\u001b[31m.map-table th{background:var(--bg);color:var(--t3);font-family:var(--fnt-head);font-size:.65rem;\u001b[39m\n\u001b[31m  letter-spacing:1.5px;text-transform:uppercase;padding:7px 10px;text-align:left;\u001b[39m\n\u001b[31m  border-bottom:1px solid var(--bdr);}\u001b[39m\n\u001b[31m.map-table td{padding:7px 10px;border-bottom:1px solid var(--bdr);vertical-align:middle;}\u001b[39m\n\u001b[31m.map-table tr:last-child td{border-bottom:none;}\u001b[39m\n\u001b[31m.map-table select{background:var(--sur2);border:1px solid var(--bdr);color:var(--t1);\u001b[39m\n\u001b[31m  font-family:var(--fnt-mono);font-size:.75rem;padding:5px 8px;border-radius:var(--r-sm);\u001b[39m\n\u001b[31m  width:100%;outline:none;}\u001b[39m\n\u001b[31m.map-table select:focus{border-color:var(--acc);}\u001b[39m\n\u001b[31m.map-req{color:var(--acc);margin-left:2px;}\u001b[39m\n\u001b[31m.map-saved-badge{font-size:.6rem;background:rgba(57,255,143,.15);color:var(--accent3,#39ff8f);\u001b[39m\n\u001b[31m  border:1px solid rgba(57,255,143,.3);padding:2px 7px;border-radius:10px;\u001b[39m\n\u001b[31m  font-family:var(--fnt-mono);margin-left:6px;}·\u001b[39m\n\u001b[31m/* ── VERIFICATION SCREEN ── */\u001b[39m\n\u001b[31m#verifyPane{display:none;position:fixed;inset:0;background:var(--bg);z-index:50;\u001b[39m\n\u001b[31m  overflow-y:auto;padding:14px;}\u001b[39m\n\u001b[31m.verify-hdr{display:flex;align-items:center;gap:10px;margin-bottom:14px;\u001b[39m\n\u001b[31m  position:sticky;top:0;background:var(--bg);padding-bottom:10px;\u001b[39m\n\u001b[31m  border-bottom:1px solid var(--bdr);z-index:1;}\u001b[39m\n\u001b[31m.verify-title{font-family:var(--fnt-head);font-size:1.1rem;font-weight:800;flex:1;}\u001b[39m\n\u001b[31m.verify-row{background:var(--sur);border:1px solid var(--bdr);border-radius:var(--r-sm);\u001b[39m\n\u001b[31m  padding:10px 12px;margin-bottom:8px;position:relative;}\u001b[39m\n\u001b[31m.verify-row.has-dupe{border-color:var(--red);background:rgba(239,68,68,.04);}\u001b[39m\n\u001b[31m.verify-row.no-docket{border-color:var(--ylw);background:rgba(234,179,8,.04);}\u001b[39m\n\u001b[31m.verify-row.ok{border-left:3px solid var(--grn);}\u001b[39m\n\u001b[31m.verify-row-hdr{display:flex;align-items:center;gap:8px;margin-bottom:8px;}\u001b[39m\n\u001b[31m.verify-num{font-family:var(--fnt-mono);font-size:.65rem;color:var(--t3);}\u001b[39m\n\u001b[31m.verify-store{font-weight:600;font-size:.9rem;flex:1;}\u001b[39m\n\u001b[31m.verify-flag{font-size:.65rem;font-weight:700;font-family:var(--fnt-mono);\u001b[39m\n\u001b[31m  padding:2px 7px;border-radius:10px;text-transform:uppercase;}\u001b[39m\n\u001b[31m.vf-dupe{background:var(--red-dim);color:var(--red);}\u001b[39m\n\u001b[31m.vf-nodocket{background:var(--ylw-dim);color:var(--ylw);}\u001b[39m\n\u001b[31m.vf-ok{background:var(--grn-dim);color:var(--grn);}\u001b[39m\n\u001b[31m.vf-short{background:rgba(56,189,248,.15);color:var(--sky);}\u001b[39m\n\u001b[31m.verify-fields{display:grid;grid-template-columns:1fr 1fr;gap:8px;margin-bottom:8px;}\u001b[39m\n\u001b[31m.verify-field label{font-size:.62rem;color:var(--t3);text-transform:uppercase;\u001b[39m\n\u001b[31m  letter-spacing:1px;display:block;margin-bottom:3px;font-family:var(--fnt-mono);}\u001b[39m\n\u001b[31m.verify-field input{width:100%;background:var(--sur2);border:1px solid var(--bdr);\u001b[39m\n\u001b[31m  color:var(--t1);font-family:var(--fnt-mono);font-size:.82rem;padding:6px 9px;\u001b[39m\n\u001b[31m  border-radius:var(--r-sm);outline:none;}\u001b[39m\n\u001b[31m.verify-field input:focus{border-color:var(--acc);}\u001b[39m\n\u001b[31m.verify-field input.changed{border-color:var(--ylw);background:rgba(234,179,8,.06);}\u001b[39m\n\u001b[31m.verify-actions{display:flex;gap:6px;align-items:center;flex-wrap:wrap;}\u001b[39m\n\u001b[31m.btn-wa{background:rgba(37,211,102,.15);border:1px solid rgba(37,211,102,.3);\u001b[39m\n\u001b[31m  color:#25d366;font-size:.65rem;padding:4px 10px;border-radius:10px;cursor:pointer;\u001b[39m\n\u001b[31m  font-family:var(--fnt-mono);transition:all .2s;}\u001b[39m\n\u001b[31m.btn-wa:hover{background:rgba(37,211,102,.3);}\u001b[39m\n\u001b[31m.verify-thumb{width:48px;height:48px;object-fit:cover;border-radius:6px;\u001b[39m\n\u001b[31m  border:1px solid var(--bdr);cursor:pointer;flex-shrink:0;}\u001b[39m\n\u001b[31m.verify-summary{background:var(--sur);border:1px solid var(--bdr);border-radius:var(--r);\u001b[39m\n\u001b[31m  padding:12px 14px;margin-bottom:14px;display:flex;gap:16px;flex-wrap:wrap;}\u001b[39m\n\u001b[31m.vs-item{text-align:center;}\u001b[39m\n\u001b[31m.vs-val{font-family:var(--fnt-head);font-size:1.3rem;font-weight:800;}\u001b[39m\n\u001b[31m.vs-lbl{font-size:.6rem;color:var(--t3);font-family:var(--fnt-mono);text-transform:uppercase;\u001b[39m\n\u001b[31m  letter-spacing:.5px;margin-top:2px;}\u001b[39m\n\u001b[31m.ocr-correction-note{font-size:.65rem;color:var(--ylw);font-family:var(--fnt-mono);\u001b[39m\n\u001b[31m  margin-top:3px;}\u001b[39m\n\u001b[31m</style>\u001b[39m\n\u001b[31m</head>\u001b[39m\n\u001b[31m<body>·\u001b[39m\n\u001b[31m<!-- SPLASH -->\u001b[39m\n\u001b[31m<div id=\\\"splash\\\">\u001b[39m\n\u001b[31m  <div class=\\\"splash-logo\\\">360LM <em>DISPATCH</em></div>\u001b[39m\n\u001b[31m  <div class=\\\"splash-sub\\\">Courier · Bus · Field Ops</div>\u001b[39m\n\u001b[31m  <div class=\\\"splash-track\\\"><div class=\\\"splash-track-fill\\\"></div></div>\u001b[39m\n\u001b[31m</div>·\u001b[39m\n\u001b[31m<!-- SHARE BANNER -->\u001b[39m\n\u001b[31m<div class=\\\"share-banner\\\" id=\\\"shareBanner\\\">\u001b[39m\n\u001b[31m  📤 Images received via Share — processing for OCR...\u001b[39m\n\u001b[31m  <button style=\\\"background:none;border:none;color:var(--grn);cursor:pointer;margin-left:auto;\\\" onclick=\\\"dismissShare()\\\">✕</button>\u001b[39m\n\u001b[31m</div>·\u001b[39m\n\u001b[31m<!-- APP -->\u001b[39m\n\u001b[31m<div id=\\\"app\\\" style=\\\"display:none;\\\">·\u001b[39m\n\u001b[31m  <div class=\\\"hdr\\\">\u001b[39m\n\u001b[31m    <div class=\\\"hdr-brand\\\">360LM <em>DISPATCH</em></div>\u001b[39m\n\u001b[31m    <span class=\\\"hdr-dot\\\" id=\\\"netDot\\\"></span>\u001b[39m\n\u001b[31m    <button class=\\\"hdr-btn\\\" onclick=\\\"window.location.href='/hub/'\\\" title=\\\"Hub\\\">🏡</button>\u001b[39m\n\u001b[31m    <button class=\\\"hdr-btn\\\" onclick=\\\"showPane('dashboard')\\\">📊</button>\u001b[39m\n\u001b[31m    <button class=\\\"hdr-btn primary\\\" onclick=\\\"newDispatch()\\\">+ NEW</button>\u001b[39m\n\u001b[31m  </div>·\u001b[39m\n\u001b[31m  <div class=\\\"tabs\\\">\u001b[39m\n\u001b[31m    <button class=\\\"tab active\\\" onclick=\\\"showPane('new')\\\" id=\\\"tab-new\\\">NEW</button>\u001b[39m\n\u001b[31m    <button class=\\\"tab\\\" onclick=\\\"showPane('dashboard')\\\" id=\\\"tab-dashboard\\\">DASHBOARD</button>\u001b[39m\n\u001b[31m    <button class=\\\"tab\\\" onclick=\\\"showPane('track')\\\" id=\\\"tab-track\\\">TRACK</button>\u001b[39m\n\u001b[31m    <button class=\\\"tab\\\" onclick=\\\"showPane('history')\\\" id=\\\"tab-history\\\">HISTORY <span class=\\\"tab-badge\\\" id=\\\"histCount\\\">0</span></button>\u001b[39m\n\u001b[31m    <button class=\\\"tab\\\" onclick=\\\"showPane('config')\\\" id=\\\"tab-config\\\">CONFIG</button>\u001b[39m\n\u001b[31m  </div>·\u001b[39m\n\u001b[31m  <!-- ════════════ NEW DISPATCH ════════════ -->\u001b[39m\n\u001b[31m  <div class=\\\"pane active\\\" id=\\\"pane-new\\\">·\u001b[39m\n\u001b[31m    <!-- 1. JOB REFERENCE -->\u001b[39m\n\u001b[31m    <div class=\\\"card accent-left\\\">\u001b[39m\n\u001b[31m      <div class=\\\"card-title\\\"><span class=\\\"ct-dot\\\"></span>Job Reference</div>\u001b[39m\n\u001b[31m      <div class=\\\"row2\\\">\u001b[39m\n\u001b[31m        <div class=\\\"f\\\">\u001b[39m\n\u001b[31m          <label>Client <span class=\\\"req\\\">*</span></label>\u001b[39m\n\u001b[31m          <select id=\\\"n_client\\\" onchange=\\\"onClientChange()\\\">\u001b[39m\n\u001b[31m            <option value=\\\"\\\">Select...</option>\u001b[39m\n\u001b[31m            <option>Lenovo</option><option>HP</option>\u001b[39m\n\u001b[31m            <option>Philips</option><option>Liebherr</option>\u001b[39m\n\u001b[31m            <option value=\\\"__c\\\">+ Custom</option>\u001b[39m\n\u001b[31m          </select>\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m        <div class=\\\"f\\\">\u001b[39m\n\u001b[31m          <label>Job Ref</label>\u001b[39m\n\u001b[31m          <input type=\\\"text\\\" id=\\\"n_jobref\\\" placeholder=\\\"JOB-042\\\" oninput=\\\"updatePill()\\\">\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      <div class=\\\"f\\\" id=\\\"customClientField\\\" style=\\\"display:none;\\\">\u001b[39m\n\u001b[31m        <label>Custom Client</label>\u001b[39m\n\u001b[31m        <input type=\\\"text\\\" id=\\\"n_customClient\\\" placeholder=\\\"Client name\\\" oninput=\\\"updatePill()\\\">\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      <div id=\\\"jobPill\\\" style=\\\"display:none;\\\" class=\\\"job-pill\\\">\u001b[39m\n\u001b[31m        🔗 <span id=\\\"jobPillTxt\\\"></span>\u001b[39m\n\u001b[31m        <button onclick=\\\"clearJob()\\\">✕</button>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      <div class=\\\"row2\\\" style=\\\"margin-top:10px;\\\">\u001b[39m\n\u001b[31m        <div class=\\\"f\\\" style=\\\"margin-bottom:0;\\\">\u001b[39m\n\u001b[31m          <label>Dispatch Date</label>\u001b[39m\n\u001b[31m          <input type=\\\"date\\\" id=\\\"n_date\\\">\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m        <div class=\\\"f\\\" style=\\\"margin-bottom:0;\\\">\u001b[39m\n\u001b[31m          <label>Courier Company <span class=\\\"req\\\">*</span></label>\u001b[39m\n\u001b[31m          <select id=\\\"n_courier\\\">\u001b[39m\n\u001b[31m            <option value=\\\"\\\">Select...</option>\u001b[39m\n\u001b[31m            <option>SkyKing</option><option>DTDC</option>\u001b[39m\n\u001b[31m            <option>Delhivery</option><option>Blue Dart</option>\u001b[39m\n\u001b[31m            <option>FedEx</option><option>Ecom Express</option>\u001b[39m\n\u001b[31m            <option>India Post</option><option value=\\\"__o\\\">Other</option>\u001b[39m\n\u001b[31m          </select>\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m    </div>·\u001b[39m\n\u001b[31m    <!-- 2. STORE LIST UPLOAD -->\u001b[39m\n\u001b[31m    <div class=\\\"card\\\">\u001b[39m\n\u001b[31m      <div class=\\\"card-title\\\"><span class=\\\"ct-dot\\\" style=\\\"background:var(--sky);\\\"></span>Store / Destination List</div>\u001b[39m\n\u001b[31m      <div class=\\\"store-upload-zone\\\" id=\\\"storeZone\\\" onclick=\\\"document.getElementById('storeFile').click();event.stopPropagation();\\\" style=\\\"cursor:pointer;\\\">\u001b[39m\n\u001b[31m        <input type=\\\"file\\\" id=\\\"storeFile\\\" accept=\\\".xlsx,.xls,.csv\\\" onchange=\\\"handleStoreFile(this)\\\" onclick=\\\"event.stopPropagation();\\\" style=\\\"display:none;\\\">\u001b[39m\n\u001b[31m        <div style=\\\"font-size:1.5rem;margin-bottom:6px;\\\">📋</div>\u001b[39m\n\u001b[31m        <div style=\\\"font-family:var(--fnt-head);font-weight:700;font-size:.9rem;\\\">Upload Store List</div>\u001b[39m\n\u001b[31m        <div style=\\\"font-size:.68rem;color:var(--t3);margin-top:3px;font-family:var(--fnt-mono);\\\">Excel / CSV · stores, cities, contacts</div>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      <div id=\\\"storeTableArea\\\" style=\\\"display:none;\\\">\u001b[39m\n\u001b[31m        <!-- Column detection result -->\u001b[39m\n\u001b[31m        <div id=\\\"storeColInfo\\\" style=\\\"margin-top:10px;display:none;\\\"></div>\u001b[39m\n\u001b[31m        <div style=\\\"display:flex;justify-content:space-between;align-items:center;margin-top:10px;margin-bottom:6px;\\\">\u001b[39m\n\u001b[31m          <span style=\\\"font-family:var(--fnt-mono);font-size:.7rem;color:var(--sky);\\\" id=\\\"storeCount\\\">0 stores loaded</span>\u001b[39m\n\u001b[31m          <button class=\\\"btn-sm\\\" onclick=\\\"clearStores()\\\">Clear</button>\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m        <div class=\\\"store-table-wrap\\\">\u001b[39m\n\u001b[31m          <table class=\\\"store-table\\\" id=\\\"storeTable\\\"></table>\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m        <!-- Inline add-as-consignees UI instead of confirm() -->\u001b[39m\n\u001b[31m        <div id=\\\"storeAddPanel\\\" style=\\\"margin-top:10px;background:var(--sur3);border:1px solid var(--bdr2);border-radius:var(--r-sm);padding:10px 12px;display:none;\\\">\u001b[39m\n\u001b[31m          <div style=\\\"font-size:.75rem;color:var(--t1);margin-bottom:8px;font-weight:600;\\\" id=\\\"storeAddTitle\\\">Add stores as consignees?</div>\u001b[39m\n\u001b[31m          <div class=\\\"row2\\\">\u001b[39m\n\u001b[31m            <button class=\\\"btn btn-ghost\\\" style=\\\"font-size:.75rem;padding:8px;\\\" onclick=\\\"document.getElementById('storeAddPanel').style.display='none'\\\">Skip for now</button>\u001b[39m\n\u001b[31m            <button class=\\\"btn btn-orange\\\" style=\\\"font-size:.75rem;padding:8px;\\\" id=\\\"storeAddBtn\\\" onclick=\\\"addStoresToConsignees()\\\">✓ Add All as Consignees</button>\u001b[39m\n\u001b[31m          </div>\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m        <button class=\\\"btn btn-ghost\\\" style=\\\"margin-top:8px;font-size:.78rem;\\\" onclick=\\\"autoMatchDockets()\\\">\u001b[39m\n\u001b[31m          🔗 Auto-match Dockets to Stores\u001b[39m\n\u001b[31m        </button>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m    </div>·\u001b[39m\n\u001b[31m    <!-- 3. DOCKET SLIPS — AI OCR -->\u001b[39m\n\u001b[31m    <div class=\\\"card\\\">\u001b[39m\n\u001b[31m      <div class=\\\"card-title\\\"><span class=\\\"ct-dot\\\" style=\\\"background:var(--grn);\\\"></span>Docket Slips — AI OCR</div>·\u001b[39m\n\u001b[31m      <!-- Share target indicator -->\u001b[39m\n\u001b[31m      <div class=\\\"info-tag\\\" id=\\\"shareTargetInfo\\\" style=\\\"display:none;\\\">\u001b[39m\n\u001b[31m        <span>📤</span><span>Images shared from WhatsApp — <strong id=\\\"sharedCount\\\">0</strong> slips received</span>\u001b[39m\n\u001b[31m      </div>·\u001b[39m\n\u001b[31m      <label class=\\\"upload-zone\\\" id=\\\"slipZone\\\" for=\\\"slipFiles\\\" style=\\\"display:block;cursor:pointer;\\\">\u001b[39m\n\u001b[31m        <input type=\\\"file\\\" id=\\\"slipFiles\\\" accept=\\\"image/*\\\" multiple onchange=\\\"handleSlipFiles(this.files)\\\" style=\\\"display:none;\\\">\u001b[39m\n\u001b[31m        <div class=\\\"uz-icon\\\">🖼️</div>\u001b[39m\n\u001b[31m        <div class=\\\"uz-title\\\">Upload Courier Slips</div>\u001b[39m\n\u001b[31m        <div class=\\\"uz-sub\\\">Multiple images · Tap or drag · Camera capture supported</div>\u001b[39m\n\u001b[31m      </label>·\u001b[39m\n\u001b[31m      <!-- OCR Working indicator -->\u001b[39m\n\u001b[31m      <div class=\\\"ocr-working\\\" id=\\\"ocrWorking\\\">\u001b[39m\n\u001b[31m        <span class=\\\"spin\\\">⟳</span>\u001b[39m\n\u001b[31m        <span id=\\\"ocrWorkingText\\\">Running OCR...</span>\u001b[39m\n\u001b[31m      </div>·\u001b[39m\n\u001b[31m      <!-- AI Progress -->\u001b[39m\n\u001b[31m      <div class=\\\"ai-progress\\\" id=\\\"aiProgress\\\">\u001b[39m\n\u001b[31m        <div class=\\\"ai-prog-label\\\" id=\\\"aiProgLabel\\\">Initializing Transformers.js...</div>\u001b[39m\n\u001b[31m        <div class=\\\"ai-prog-bar\\\"><div class=\\\"ai-prog-fill\\\" id=\\\"aiProgFill\\\"></div></div>\u001b[39m\n\u001b[31m      </div>·\u001b[39m\n\u001b[31m      <!-- OCR Results -->\u001b[39m\n\u001b[31m      <div class=\\\"ocr-results\\\" id=\\\"ocrResults\\\">\u001b[39m\n\u001b[31m        <div class=\\\"sec-div\\\"><span>Detected Dockets</span></div>\u001b[39m\n\u001b[31m        <div id=\\\"ocrSummary\\\" style=\\\"display:none;font-size:.72rem;font-family:var(--fnt-mono);\u001b[39m\n\u001b[31m          padding:4px 8px 8px;gap:8px\\\"></div>\u001b[39m\n\u001b[31m        <div class=\\\"ocr-slip-list\\\" id=\\\"ocrSlipList\\\"></div>\u001b[39m\n\u001b[31m        <div class=\\\"btn-row\\\" style=\\\"margin-top:10px;\\\">\u001b[39m\n\u001b[31m          <button class=\\\"btn btn-ghost\\\" onclick=\\\"clearSlips()\\\">Clear All</button>\u001b[39m\n\u001b[31m          <button class=\\\"btn btn-grn\\\" onclick=\\\"applyAllDockets()\\\">✓ Apply All to Consignees</button>\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m    </div>·\u001b[39m\n\u001b[31m    <!-- 4. CONSIGNEES -->\u001b[39m\n\u001b[31m    <div class=\\\"card accent-left\\\">\u001b[39m\n\u001b[31m      <div class=\\\"card-title\\\"><span class=\\\"ct-dot\\\"></span>Consignees</div>\u001b[39m\n\u001b[31m      <div class=\\\"cons-list\\\" id=\\\"consList\\\"></div>\u001b[39m\n\u001b[31m      <button class=\\\"btn btn-ghost\\\" style=\\\"font-size:.8rem;\\\" onclick=\\\"addConsignee()\\\">+ Add Consignee</button>\u001b[39m\n\u001b[31m    </div>·\u001b[39m\n\u001b[31m    <!-- 5. REMARKS -->\u001b[39m\n\u001b[31m    <div class=\\\"card\\\">\u001b[39m\n\u001b[31m      <div class=\\\"card-title\\\"><span class=\\\"ct-dot\\\" style=\\\"background:var(--t3);\\\"></span>Remarks</div>\u001b[39m\n\u001b[31m      <div class=\\\"f\\\" style=\\\"margin-bottom:0;\\\">\u001b[39m\n\u001b[31m        <textarea id=\\\"n_remarks\\\" placeholder=\\\"Instructions, notes, special handling...\\\"></textarea>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m    </div>·\u001b[39m\n\u001b[31m    <!-- ACTIONS -->\u001b[39m\n\u001b[31m    <div class=\\\"btn-row\\\">\u001b[39m\n\u001b[31m      <button class=\\\"btn btn-ghost\\\" onclick=\\\"saveDraft()\\\">💾 Draft</button>\u001b[39m\n\u001b[31m      <button class=\\\"btn btn-orange\\\" onclick=\\\"finaliseDispatch()\\\">✓ Finalise & Log</button>\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m    <div style=\\\"height:16px;\\\"></div>\u001b[39m\n\u001b[31m  </div>·\u001b[39m\n\u001b[31m  <!-- ════════════ DASHBOARD ════════════ -->\u001b[39m\n\u001b[31m  <div class=\\\"pane\\\" id=\\\"pane-dashboard\\\">\u001b[39m\n\u001b[31m    <div class=\\\"dash-stats\\\">\u001b[39m\n\u001b[31m      <div class=\\\"stat-box grn\\\"><div class=\\\"stat-val\\\" id=\\\"st-delivered\\\">0</div><div class=\\\"stat-lbl\\\">Delivered</div></div>\u001b[39m\n\u001b[31m      <div class=\\\"stat-box ylw\\\"><div class=\\\"stat-val\\\" id=\\\"st-transit\\\">0</div><div class=\\\"stat-lbl\\\">In Transit</div></div>\u001b[39m\n\u001b[31m      <div class=\\\"stat-box sky\\\"><div class=\\\"stat-val\\\" id=\\\"st-booked\\\">0</div><div class=\\\"stat-lbl\\\">Booked</div></div>\u001b[39m\n\u001b[31m      <div class=\\\"stat-box red\\\"><div class=\\\"stat-val\\\" id=\\\"st-pending\\\">0</div><div class=\\\"stat-lbl\\\">Pending</div></div>\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m    <div id=\\\"dashFilters\\\" style=\\\"display:flex;gap:6px;flex-wrap:wrap;margin-bottom:12px;\\\">\u001b[39m\n\u001b[31m      <span class=\\\"btn-sm primary\\\" onclick=\\\"filterDash('all',this)\\\" style=\\\"cursor:pointer;\\\">All</span>\u001b[39m\n\u001b[31m      <span class=\\\"btn-sm\\\" onclick=\\\"filterDash('delivered',this)\\\" style=\\\"cursor:pointer;\\\">Delivered</span>\u001b[39m\n\u001b[31m      <span class=\\\"btn-sm\\\" onclick=\\\"filterDash('transit',this)\\\" style=\\\"cursor:pointer;\\\">In Transit</span>\u001b[39m\n\u001b[31m      <span class=\\\"btn-sm\\\" onclick=\\\"filterDash('booked',this)\\\" style=\\\"cursor:pointer;\\\">Booked</span>\u001b[39m\n\u001b[31m      <span class=\\\"btn-sm\\\" onclick=\\\"filterDash('pending',this)\\\" style=\\\"cursor:pointer;\\\">Pending</span>\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m    <div class=\\\"docket-grid\\\" id=\\\"dashGrid\\\">\u001b[39m\n\u001b[31m      <div class=\\\"empty\\\"><div class=\\\"e-icon\\\">📊</div><p>No dispatches yet.<br>Create one to see the dashboard.</p></div>\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m    <div style=\\\"display:flex;gap:8px;margin-top:10px;flex-wrap:wrap;\\\">\u001b[39m\n\u001b[31m      <button class=\\\"btn btn-ghost\\\" style=\\\"flex:2;font-size:.8rem;\\\" id=\\\"btnRefreshAll\\\" onclick=\\\"refreshAllTracking()\\\">\u001b[39m\n\u001b[31m        🔄 Refresh All Tracking\u001b[39m\n\u001b[31m      </button>\u001b[39m\n\u001b[31m      <button class=\\\"btn btn-ghost\\\" style=\\\"flex:1;font-size:.8rem;border-color:var(--sky);color:var(--sky);\\\"\u001b[39m\n\u001b[31m        onclick=\\\"shareSelectedReport()\\\">\u001b[39m\n\u001b[31m        📊 Share Selected\u001b[39m\n\u001b[31m      </button>\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m  </div>·\u001b[39m\n\u001b[31m  <!-- ════════════ TRACK ════════════ -->\u001b[39m\n\u001b[31m  <div class=\\\"pane\\\" id=\\\"pane-track\\\">·\u001b[39m\n\u001b[31m    <!-- Courier selector -->\u001b[39m\n\u001b[31m    <div class=\\\"card\\\" style=\\\"margin-bottom:10px;\\\">\u001b[39m\n\u001b[31m      <div class=\\\"card-title\\\"><span class=\\\"ct-dot\\\" style=\\\"background:var(--sky);\\\"></span>Manual Tracking</div>\u001b[39m\n\u001b[31m      <div style=\\\"font-size:.72rem;color:var(--t2);margin-bottom:10px;line-height:1.5;\\\">\u001b[39m\n\u001b[31m        Track any docket number — select from already dispatched dockets or enter new ones manually.\u001b[39m\n\u001b[31m        SkyKing supports up to <strong>10 dockets at once</strong>; others are tracked one at a time.\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      <div class=\\\"f\\\">\u001b[39m\n\u001b[31m        <label>Courier Company</label>\u001b[39m\n\u001b[31m        <select id=\\\"t_courier\\\" onchange=\\\"buildTrackDocketChips()\\\">\u001b[39m\n\u001b[31m          <option>SkyKing</option><option>DTDC</option><option>Delhivery</option>\u001b[39m\n\u001b[31m          <option>Blue Dart</option><option>FedEx</option><option>Ecom Express</option><option>India Post</option>\u001b[39m\n\u001b[31m        </select>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m    </div>·\u001b[39m\n\u001b[31m    <!-- Docket selector — from existing dispatches -->\u001b[39m\n\u001b[31m    <div class=\\\"card\\\" style=\\\"margin-bottom:10px;\\\">\u001b[39m\n\u001b[31m      <div class=\\\"card-title\\\"><span class=\\\"ct-dot\\\" style=\\\"background:var(--acc);\\\"></span>\u001b[39m\n\u001b[31m        Select from Dispatched Dockets\u001b[39m\n\u001b[31m        <span id=\\\"trackCourierLimit\\\" style=\\\"margin-left:auto;font-size:.65rem;color:var(--t3);font-weight:400;text-transform:none;letter-spacing:0;font-family:var(--fnt-mono);\\\"></span>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      <div id=\\\"trackDocketChips\\\" style=\\\"display:flex;flex-wrap:wrap;gap:6px;margin-bottom:10px;min-height:28px;\\\">\u001b[39m\n\u001b[31m        <span style=\\\"font-size:.72rem;color:var(--t3);font-family:var(--fnt-mono);\\\">No dispatched dockets yet</span>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      <div style=\\\"font-size:.68rem;color:var(--t3);font-family:var(--fnt-mono);margin-bottom:8px;\\\">\u001b[39m\n\u001b[31m        Tap a docket to add/remove from tracking list. Selected shown in orange.\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m    </div>·\u001b[39m\n\u001b[31m    <!-- Manual entry for new dockets -->\u001b[39m\n\u001b[31m    <div class=\\\"card\\\" style=\\\"margin-bottom:10px;\\\">\u001b[39m\n\u001b[31m      <div class=\\\"card-title\\\"><span class=\\\"ct-dot\\\" style=\\\"background:var(--grn);\\\"></span>Add New Docket Numbers</div>\u001b[39m\n\u001b[31m      <div class=\\\"f\\\" style=\\\"margin-bottom:6px;\\\">\u001b[39m\n\u001b[31m        <label>Enter docket numbers (comma or newline separated)</label>\u001b[39m\n\u001b[31m        <textarea id=\\\"t_manual_dockets\\\" placeholder=\\\"443005527&#10;443005528&#10;or: 443005527, 443005528\\\"\u001b[39m\n\u001b[31m          style=\\\"min-height:56px;font-family:var(--fnt-mono);font-size:.82rem;letter-spacing:.5px;\\\"></textarea>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      <button class=\\\"btn-sm primary\\\" onclick=\\\"addManualDockets()\\\" style=\\\"font-size:.72rem;\\\">+ Add to List</button>\u001b[39m\n\u001b[31m    </div>·\u001b[39m\n\u001b[31m    <!-- Selected dockets summary + track button -->\u001b[39m\n\u001b[31m    <div id=\\\"trackSelectedSummary\\\" style=\\\"display:none;\\\" class=\\\"card\\\" style=\\\"margin-bottom:10px;\\\">\u001b[39m\n\u001b[31m      <div style=\\\"display:flex;align-items:center;justify-content:space-between;margin-bottom:8px;\\\">\u001b[39m\n\u001b[31m        <div>\u001b[39m\n\u001b[31m          <span style=\\\"font-family:var(--fnt-mono);font-size:.72rem;color:var(--sky);\\\" id=\\\"trackSelectedCount\\\">0 selected</span>\u001b[39m\n\u001b[31m          <span style=\\\"font-family:var(--fnt-mono);font-size:.65rem;color:var(--t3);margin-left:8px;\\\" id=\\\"trackSelectedList\\\"></span>\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m        <button class=\\\"btn-sm\\\" onclick=\\\"clearTrackSelection()\\\">Clear</button>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      <div class=\\\"btn-row\\\">\u001b[39m\n\u001b[31m        <button class=\\\"btn btn-ghost\\\" onclick=\\\"openTrackOnCourierSite()\\\">🔗 Open Courier Site</button>\u001b[39m\n\u001b[31m        <button class=\\\"btn btn-orange\\\" onclick=\\\"trackSelected()\\\">🔍 Track All via Proxy</button>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m    </div>·\u001b[39m\n\u001b[31m    <div id=\\\"trackResultsArea\\\"></div>\u001b[39m\n\u001b[31m  </div>·\u001b[39m\n\u001b[31m  <!-- ════════════ HISTORY ════════════ -->\u001b[39m\n\u001b[31m  <div class=\\\"pane\\\" id=\\\"pane-history\\\">\u001b[39m\n\u001b[31m    <div id=\\\"historyList\\\">\u001b[39m\n\u001b[31m      <div class=\\\"empty\\\"><div class=\\\"e-icon\\\">📭</div><p>No dispatch history yet.</p></div>\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m  </div>·\u001b[39m\n\u001b[31m  <!-- ════════════ CONFIG ════════════ -->\u001b[39m\n\u001b[31m  <div class=\\\"pane\\\" id=\\\"pane-config\\\">·\u001b[39m\n\u001b[31m    <!-- SHEETS URL — the only thing you type manually in the PWA -->\u001b[39m\n\u001b[31m    <div class=\\\"card\\\" style=\\\"border-color:var(--acc);border-width:2px;\\\">\u001b[39m\n\u001b[31m      <div class=\\\"card-title\\\"><span class=\\\"ct-dot\\\"></span>Google Sheets — Master Config</div>\u001b[39m\n\u001b[31m      <div class=\\\"info-tag\\\" style=\\\"background:rgba(34,197,94,.07);border-color:rgba(34,197,94,.3);\\\">\u001b[39m\n\u001b[31m        <span>✅</span>\u001b[39m\n\u001b[31m        <span>URL is loaded automatically from <strong>config.json</strong> on the VPS — no manual entry needed. All other settings sync from your <strong>Google Sheet</strong>.</span>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      <div class=\\\"f\\\">\u001b[39m\n\u001b[31m        <label>Apps Script URL <span style=\\\"font-size:.65rem;color:var(--t3);font-weight:400;\\\">(auto-loaded from config.json)</span></label>\u001b[39m\n\u001b[31m        <input type=\\\"url\\\" id=\\\"cfg_sheets_url\\\"\u001b[39m\n\u001b[31m          placeholder=\\\"Auto-loaded from server...\\\"\u001b[39m\n\u001b[31m          oninput=\\\"onSheetsUrlChange()\\\" readonly\u001b[39m\n\u001b[31m          style=\\\"opacity:.7;cursor:default;\\\">\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      <div id=\\\"syncStatus\\\" class=\\\"info-tag\\\" style=\\\"display:none;\\\">\u001b[39m\n\u001b[31m        <span id=\\\"syncIcon\\\">⏳</span>\u001b[39m\n\u001b[31m        <span id=\\\"syncMsg\\\">Syncing from sheet...</span>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      <div class=\\\"btn-row\\\">\u001b[39m\n\u001b[31m        <button class=\\\"btn btn-ghost\\\" onclick=\\\"syncFromSheet()\\\">🔄 Sync Now</button>\u001b[39m\n\u001b[31m        <button class=\\\"btn btn-orange\\\" onclick=\\\"saveSheetsUrl()\\\">💾 Save & Sync</button>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m    </div>·\u001b[39m\n\u001b[31m    <!-- READ-ONLY display of what was loaded from sheet -->\u001b[39m\n\u001b[31m    <div class=\\\"card\\\" id=\\\"sheetConfigDisplay\\\" style=\\\"display:none;\\\">\u001b[39m\n\u001b[31m      <div class=\\\"card-title\\\"><span class=\\\"ct-dot\\\" style=\\\"background:var(--grn);\\\"></span>Config Loaded from Sheet\u001b[39m\n\u001b[31m        <span id=\\\"sheetSyncTime\\\" style=\\\"margin-left:auto;font-size:.62rem;color:var(--t3);font-family:var(--fnt-mono);font-weight:400;text-transform:none;letter-spacing:0;\\\"></span>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      <div id=\\\"sheetConfigRows\\\"></div>\u001b[39m\n\u001b[31m      <div class=\\\"info-tag\\\" style=\\\"margin-top:8px;\\\">\u001b[39m\n\u001b[31m        <span>✏️</span><span>To change any value, edit the <strong>Config sheet</strong> in Google Sheets, then tap Sync Now above.</span>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m    </div>·\u001b[39m\n\u001b[31m    <!-- Courier Master (from sheet) -->\u001b[39m\n\u001b[31m    <div class=\\\"card\\\">\u001b[39m\n\u001b[31m      <div class=\\\"card-title\\\"><span class=\\\"ct-dot\\\" style=\\\"background:var(--ylw);\\\"></span>Courier Master</div>\u001b[39m\n\u001b[31m      <div id=\\\"courierMasterList\\\"></div>\u001b[39m\n\u001b[31m      <button class=\\\"btn btn-ghost\\\" style=\\\"margin-top:8px;font-size:.78rem;\\\" onclick=\\\"openCourierMaster()\\\">✏️ Edit in Sheet / Override</button>\u001b[39m\n\u001b[31m    </div>·\u001b[39m\n\u001b[31m    <button class=\\\"btn\\\" style=\\\"background:var(--red-dim);color:var(--red);border:1px solid var(--red);margin-top:4px;\\\" onclick=\\\"clearAll()\\\">🗑️ Clear All Local Data</button>\u001b[39m\n\u001b[31m    <div style=\\\"height:20px;\\\"></div>\u001b[39m\n\u001b[31m  </div>··\u001b[39m\n\u001b[31m<!-- ════ ADDRESS REVIEW PANE ════ -->\u001b[39m\n\u001b[31m<div id=\\\"addrReviewPane\\\" style=\\\"display:none;position:fixed;inset:0;background:var(--bg);z-index:50;overflow-y:auto;padding:14px;\\\">\u001b[39m\n\u001b[31m  <div class=\\\"verify-hdr\\\">\u001b[39m\n\u001b[31m    <button class=\\\"btn-icon\\\" onclick=\\\"document.getElementById('addrReviewPane').style.display='none'\\\" style=\\\"font-size:1.1rem;\\\">←</button>\u001b[39m\n\u001b[31m    <div class=\\\"verify-title\\\">Review AI-Parsed Addresses</div>\u001b[39m\n\u001b[31m  </div>\u001b[39m\n\u001b[31m  <div style=\\\"background:rgba(56,189,248,.08);border:1px solid rgba(56,189,248,.3);border-radius:var(--r-sm);\u001b[39m\n\u001b[31m    padding:8px 12px;margin-bottom:12px;font-size:.72rem;color:var(--sky);\\\">\u001b[39m\n\u001b[31m    ✏️ Review each row — correct name and city if wrong. Raw address shown below each row for reference.\u001b[39m\n\u001b[31m    Corrections are saved and improve future AI parsing automatically.\u001b[39m\n\u001b[31m  </div>\u001b[39m\n\u001b[31m  <div class=\\\"verify-summary\\\" id=\\\"addrReviewSummary\\\"></div>\u001b[39m\n\u001b[31m  <div id=\\\"addrReviewList\\\"></div>\u001b[39m\n\u001b[31m  <div style=\\\"position:sticky;bottom:0;background:var(--bg);padding-top:10px;\u001b[39m\n\u001b[31m    border-top:1px solid var(--bdr);margin-top:8px;\\\">\u001b[39m\n\u001b[31m    <div style=\\\"display:flex;gap:8px;\\\">\u001b[39m\n\u001b[31m      <button class=\\\"btn btn-ghost\\\" onclick=\\\"backToMapping()\\\" style=\\\"flex:1;\\\">← Back to Mapping</button>\u001b[39m\n\u001b[31m      <button class=\\\"btn btn-orange\\\" onclick=\\\"confirmAddressReview()\\\" style=\\\"flex:2;\\\">✓ Confirm &amp; Use These Addresses</button>\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m    <div style=\\\"font-size:.65rem;color:var(--t3);text-align:center;margin-top:5px;font-family:var(--fnt-mono);\\\">\u001b[39m\n\u001b[31m      Edits you made above are preserved when you go back\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m  </div>\u001b[39m\n\u001b[31m</div>·\u001b[39m\n\u001b[31m<!-- ════ VERIFICATION SCREEN ════ -->\u001b[39m\n\u001b[31m<div id=\\\"verifyPane\\\">\u001b[39m\n\u001b[31m  <div class=\\\"verify-hdr\\\">\u001b[39m\n\u001b[31m    <button class=\\\"btn-icon\\\" onclick=\\\"closeVerify()\\\" style=\\\"font-size:1.1rem;\\\">←</button>\u001b[39m\n\u001b[31m    <div class=\\\"verify-title\\\">Verify Before Finalise</div>\u001b[39m\n\u001b[31m    <button class=\\\"btn-sm\\\" onclick=\\\"approveAll()\\\" style=\\\"white-space:nowrap;\\\">✓ Approve All</button>\u001b[39m\n\u001b[31m  </div>\u001b[39m\n\u001b[31m  <div class=\\\"verify-summary\\\" id=\\\"verifySummary\\\"></div>\u001b[39m\n\u001b[31m  <div id=\\\"verifyList\\\"></div>\u001b[39m\n\u001b[31m  <div style=\\\"position:sticky;bottom:0;background:var(--bg);padding-top:10px;\u001b[39m\n\u001b[31m    border-top:1px solid var(--bdr);margin-top:8px;\\\">\u001b[39m\n\u001b[31m    <div style=\\\"display:flex;gap:8px;flex-wrap:wrap;\\\">\u001b[39m\n\u001b[31m      <button class=\\\"btn btn-ghost\\\" onclick=\\\"closeVerify()\\\" style=\\\"flex:1;\\\">← Back to Edit</button>\u001b[39m\n\u001b[31m      <button class=\\\"btn btn-ghost\\\" onclick=\\\"exportCleanSheet()\\\" style=\\\"flex:1;border-color:var(--sky);color:var(--sky);\\\">\u001b[39m\n\u001b[31m        📥 Export Clean Sheet\u001b[39m\n\u001b[31m      </button>\u001b[39m\n\u001b[31m      <button class=\\\"btn btn-orange\\\" id=\\\"btnConfirmFinalise\\\" onclick=\\\"confirmAndFinalise()\\\" style=\\\"flex:2;\\\">\u001b[39m\n\u001b[31m        ✓ Finalise Dispatch\u001b[39m\n\u001b[31m      </button>\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m  </div>\u001b[39m\n\u001b[31m</div>·\u001b[39m\n\u001b[31m</div><!-- /app -->·\u001b[39m\n\u001b[31m<!-- ════ DISPATCH DETAIL SHEET ════ -->\u001b[39m\n\u001b[31m<div class=\\\"overlay\\\" id=\\\"detailOverlay\\\" onclick=\\\"closeOverlay('detailOverlay')\\\">\u001b[39m\n\u001b[31m  <div class=\\\"sheet\\\" onclick=\\\"event.stopPropagation()\\\">\u001b[39m\n\u001b[31m    <div class=\\\"sheet-handle\\\"></div>\u001b[39m\n\u001b[31m    <div id=\\\"detailContent\\\"></div>\u001b[39m\n\u001b[31m  </div>\u001b[39m\n\u001b[31m</div>·\u001b[39m\n\u001b[31m<!-- ════ COURIER MASTER EDITOR ════ -->\u001b[39m\n\u001b[31m<div class=\\\"overlay\\\" id=\\\"courierMasterOverlay\\\" onclick=\\\"closeOverlay('courierMasterOverlay')\\\">\u001b[39m\n\u001b[31m  <div class=\\\"sheet\\\" onclick=\\\"event.stopPropagation()\\\">\u001b[39m\n\u001b[31m    <div class=\\\"sheet-handle\\\"></div>\u001b[39m\n\u001b[31m    <div class=\\\"sheet-title\\\">✏️ Courier Master</div>\u001b[39m\n\u001b[31m    <div id=\\\"courierMasterEditor\\\"></div>\u001b[39m\n\u001b[31m  </div>\u001b[39m\n\u001b[31m</div>·\u001b[39m\n\u001b[31m<!-- ════ MANUAL TRACKING OVERLAY ════ -->\u001b[39m\n\u001b[31m<div class=\\\"overlay\\\" id=\\\"manualTrackOverlay\\\" onclick=\\\"closeOverlay('manualTrackOverlay')\\\" data-docket=\\\"\\\" data-dispatch-id=\\\"\\\">\u001b[39m\n\u001b[31m  <div class=\\\"sheet\\\" onclick=\\\"event.stopPropagation()\\\">\u001b[39m\n\u001b[31m    <div class=\\\"sheet-handle\\\"></div>\u001b[39m\n\u001b[31m    <div class=\\\"sheet-title\\\">📞 Manual Tracking Log</div>\u001b[39m\n\u001b[31m    <div style=\\\"background:var(--sur2);border:1px solid var(--bdr);border-radius:var(--r-sm);\u001b[39m\n\u001b[31m      padding:8px 12px;margin-bottom:12px;font-family:var(--fnt-mono);font-size:.82rem;color:var(--acc);\\\"\u001b[39m\n\u001b[31m      id=\\\"mtDocketDisplay\\\"></div>\u001b[39m\n\u001b[31m    <div class=\\\"row2\\\">\u001b[39m\n\u001b[31m      <div class=\\\"f\\\">\u001b[39m\n\u001b[31m        <label>Status Reported <span class=\\\"req\\\">*</span></label>\u001b[39m\n\u001b[31m        <select id=\\\"mtStatus\\\">\u001b[39m\n\u001b[31m          <option value=\\\"\\\">Select...</option>\u001b[39m\n\u001b[31m          <option value=\\\"delivered\\\">✅ Delivered</option>\u001b[39m\n\u001b[31m          <option value=\\\"not received\\\">❌ Not Received</option>\u001b[39m\n\u001b[31m          <option value=\\\"in transit\\\">🚚 In Transit / On the way</option>\u001b[39m\n\u001b[31m          <option value=\\\"will deliver tomorrow\\\">📅 Will Deliver Tomorrow</option>\u001b[39m\n\u001b[31m          <option value=\\\"refused\\\">🚫 Refused / Returned</option>\u001b[39m\n\u001b[31m          <option value=\\\"partial\\\">📦 Partial Delivery</option>\u001b[39m\n\u001b[31m        </select>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      <div class=\\\"f\\\">\u001b[39m\n\u001b[31m        <label>Called By (Staff) <span class=\\\"req\\\">*</span></label>\u001b[39m\n\u001b[31m        <input type=\\\"text\\\" id=\\\"mtCalledBy\\\" placeholder=\\\"Your name\\\" list=\\\"staffList\\\">\u001b[39m\n\u001b[31m        <datalist id=\\\"staffList\\\">\u001b[39m\n\u001b[31m          <option>Mukesh</option><option>Sachin</option><option>Shambhu</option>\u001b[39m\n\u001b[31m          <option>Umashankar</option><option>Narender</option><option>Rakesh</option>\u001b[39m\n\u001b[31m          <option>Gurpreet</option><option>Ashok</option>\u001b[39m\n\u001b[31m        </datalist>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m    <div class=\\\"row2\\\">\u001b[39m\n\u001b[31m      <div class=\\\"f\\\">\u001b[39m\n\u001b[31m        <label>Contact Number Used</label>\u001b[39m\n\u001b[31m        <input type=\\\"tel\\\" id=\\\"mtContactUsed\\\" placeholder=\\\"Mobile number called\\\">\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      <div class=\\\"f\\\">\u001b[39m\n\u001b[31m        <label>Person Who Confirmed</label>\u001b[39m\n\u001b[31m        <input type=\\\"text\\\" id=\\\"mtContactPerson\\\" placeholder=\\\"Name of person at store\\\">\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m    <div class=\\\"f\\\">\u001b[39m\n\u001b[31m      <label>Remarks</label>\u001b[39m\n\u001b[31m      <textarea id=\\\"mtRemarks\\\" placeholder=\\\"Any notes, delivery details, issues...\\\" style=\\\"min-height:60px;\\\"></textarea>\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m    <div class=\\\"f\\\">\u001b[39m\n\u001b[31m      <label>Call Date &amp; Time</label>\u001b[39m\n\u001b[31m      <input type=\\\"datetime-local\\\" id=\\\"mtCallTime\\\">\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m    <div class=\\\"btn-row\\\">\u001b[39m\n\u001b[31m      <button class=\\\"btn btn-ghost\\\" onclick=\\\"closeOverlay('manualTrackOverlay')\\\">Cancel</button>\u001b[39m\n\u001b[31m      <button class=\\\"btn btn-orange\\\" onclick=\\\"submitManualTracking()\\\">💾 Log &amp; Save</button>\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m  </div>\u001b[39m\n\u001b[31m</div>·\u001b[39m\n\u001b[31m<!-- ════ POD UPLOAD OVERLAY ════ -->\u001b[39m\n\u001b[31m<div class=\\\"overlay\\\" id=\\\"podOverlay\\\" onclick=\\\"closeOverlay('podOverlay')\\\" data-docket=\\\"\\\" data-dispatch-id=\\\"\\\">\u001b[39m\n\u001b[31m  <div class=\\\"sheet\\\" onclick=\\\"event.stopPropagation()\\\">\u001b[39m\n\u001b[31m    <div class=\\\"sheet-handle\\\"></div>\u001b[39m\n\u001b[31m    <div class=\\\"sheet-title\\\">📸 Upload Proof of Delivery</div>\u001b[39m\n\u001b[31m    <div class=\\\"f\\\">\u001b[39m\n\u001b[31m      <label>POD / Delivery Slip Photo</label>\u001b[39m\n\u001b[31m      <input type=\\\"file\\\" id=\\\"podFileInput\\\" accept=\\\"image/*\\\" capture=\\\"environment\\\"\u001b[39m\n\u001b[31m        onchange=\\\"previewPOD(this,'pod')\\\" style=\\\"font-size:.8rem;color:var(--t2);\\\">\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m    <div class=\\\"f\\\">\u001b[39m\n\u001b[31m      <label>Recipient / Receiver Photo (optional)</label>\u001b[39m\n\u001b[31m      <input type=\\\"file\\\" id=\\\"recipientFileInput\\\" accept=\\\"image/*\\\" capture=\\\"environment\\\"\u001b[39m\n\u001b[31m        onchange=\\\"previewPOD(this,'recip')\\\" style=\\\"font-size:.8rem;color:var(--t2);\\\">\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m    <div id=\\\"podPreview\\\" style=\\\"display:flex;gap:8px;flex-wrap:wrap;margin-bottom:12px;\\\"></div>\u001b[39m\n\u001b[31m    <div class=\\\"btn-row\\\">\u001b[39m\n\u001b[31m      <button class=\\\"btn btn-ghost\\\" onclick=\\\"closeOverlay('podOverlay')\\\">Cancel</button>\u001b[39m\n\u001b[31m      <button class=\\\"btn btn-orange\\\" onclick=\\\"uploadPODFiles()\\\">📤 Upload & Mark Delivered</button>\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m  </div>\u001b[39m\n\u001b[31m</div>·\u001b[39m\n\u001b[31m<!-- ════ ADMIN LOGIN OVERLAY ════ -->\u001b[39m\n\u001b[31m<div class=\\\"overlay\\\" id=\\\"adminLoginOverlay\\\" onclick=\\\"closeOverlay('adminLoginOverlay')\\\">\u001b[39m\n\u001b[31m  <div class=\\\"sheet\\\" onclick=\\\"event.stopPropagation()\\\" style=\\\"max-height:320px;\\\">\u001b[39m\n\u001b[31m    <div class=\\\"sheet-handle\\\"></div>\u001b[39m\n\u001b[31m    <div class=\\\"sheet-title\\\">🔐 Admin Access</div>\u001b[39m\n\u001b[31m    <div style=\\\"font-size:.78rem;color:var(--t2);margin-bottom:14px;\\\">Config settings require admin password.</div>\u001b[39m\n\u001b[31m    <div class=\\\"f\\\">\u001b[39m\n\u001b[31m      <label>Password</label>\u001b[39m\n\u001b[31m      <input type=\\\"password\\\" id=\\\"adminPwdInput\\\" placeholder=\\\"Enter admin password\\\"\u001b[39m\n\u001b[31m        onkeydown=\\\"if(event.key==='Enter')submitAdminLogin()\\\">\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m    <div class=\\\"btn-row\\\" style=\\\"margin-top:4px;\\\">\u001b[39m\n\u001b[31m      <button class=\\\"btn btn-ghost\\\" onclick=\\\"closeOverlay('adminLoginOverlay')\\\">Cancel</button>\u001b[39m\n\u001b[31m      <button class=\\\"btn btn-orange\\\" onclick=\\\"submitAdminLogin()\\\">🔓 Login</button>\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m  </div>\u001b[39m\n\u001b[31m</div>·\u001b[39m\n\u001b[31m<!-- ════ TOAST ════ -->\u001b[39m\n\u001b[31m<div class=\\\"toast\\\" id=\\\"toast\\\"></div>·\u001b[39m\n\u001b[31m<!-- ════ SCRIPTS ════ -->\u001b[39m\n\u001b[31m<!-- SheetJS for Excel/CSV parsing -->\u001b[39m\n\u001b[31m<script src=\\\"https://cdnjs.cloudflare.com/ajax/libs/xlsx/0.18.5/xlsx.full.min.js\\\"></script>·\u001b[39m\n\u001b[31m<script type=\\\"module\\\">\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31m// OCR — Transformers.js\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31mimport { pipeline, env } from 'https://cdn.jsdelivr.net/npm/@huggingface/transformers@3.5.2/dist/transformers.min.js';·\u001b[39m\n\u001b[31m// Cache models in browser\u001b[39m\n\u001b[31menv.allowLocalModels = false;\u001b[39m\n\u001b[31menv.useBrowserCache = true;·\u001b[39m\n\u001b[31mlet ocrPipeline = null;\u001b[39m\n\u001b[31mlet ocrLoading = false;·\u001b[39m\n\u001b[31mwindow.initOCR = async function() {\u001b[39m\n\u001b[31m  if (ocrPipeline || ocrLoading) return;\u001b[39m\n\u001b[31m  ocrLoading = true;\u001b[39m\n\u001b[31m  showAIProgress('Loading OCR model (first time ~120MB)...', 5);\u001b[39m\n\u001b[31m  try {\u001b[39m\n\u001b[31m    ocrPipeline = await pipeline('image-to-text', 'Xenova/trocr-base-printed', {\u001b[39m\n\u001b[31m      progress_callback: (p) => {\u001b[39m\n\u001b[31m        if (p.progress) {\u001b[39m\n\u001b[31m          showAIProgress(`Loading: ${p.file || 'model'} (${Math.round(p.progress)}%)`, p.progress);\u001b[39m\n\u001b[31m        }\u001b[39m\n\u001b[31m      }\u001b[39m\n\u001b[31m    });\u001b[39m\n\u001b[31m    showAIProgress('OCR model ready ✓', 100);\u001b[39m\n\u001b[31m    setTimeout(() => hideAIProgress(), 1500);\u001b[39m\n\u001b[31m    showToast('OCR ready ✓', 'ok');\u001b[39m\n\u001b[31m  } catch(e) {\u001b[39m\n\u001b[31m    console.warn('Transformers.js OCR failed:', e);\u001b[39m\n\u001b[31m    hideAIProgress();\u001b[39m\n\u001b[31m    showToast('OCR model load failed — will use Ollama fallback', 'info');\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m  ocrLoading = false;\u001b[39m\n\u001b[31m};·\u001b[39m\n\u001b[31mwindow.runOCROnImage = async function(imageData, filename) {\u001b[39m\n\u001b[31m  const cfg = window.appConfig;·\u001b[39m\n\u001b[31m  // Strategy 1: Transformers.js\u001b[39m\n\u001b[31m  if ((cfg.ocr_mode === 'transformers' || cfg.ocr_mode === 'both') && ocrPipeline) {\u001b[39m\n\u001b[31m    try {\u001b[39m\n\u001b[31m      const result = await ocrPipeline(imageData);\u001b[39m\n\u001b[31m      const text = result[0]?.generated_text || '';\u001b[39m\n\u001b[31m      return extractDocketFromText(text, 'transformers');\u001b[39m\n\u001b[31m    } catch(e) { /* fall through */ }\u001b[39m\n\u001b[31m  }·\u001b[39m\n\u001b[31m  // Strategy 2: Dispatch AI proxy → Ollama LLaVA (server-side, avoids CORS + port issues)\u001b[39m\n\u001b[31m  if (cfg.ocr_mode === 'ollama' || cfg.ocr_mode === 'both') {\u001b[39m\n\u001b[31m    try {\u001b[39m\n\u001b[31m      const resp = await fetch('/dispatch-ai/ocr', {\u001b[39m\n\u001b[31m        method: 'POST',\u001b[39m\n\u001b[31m        headers: { 'Content-Type': 'application/json' },\u001b[39m\n\u001b[31m        body: JSON.stringify({ imageBase64: imageData }),\u001b[39m\n\u001b[31m        signal: AbortSignal.timeout(180000)\u001b[39m\n\u001b[31m      });\u001b[39m\n\u001b[31m      const data = await resp.json();\u001b[39m\n\u001b[31m      if (data.success && data.docket) {\u001b[39m\n\u001b[31m        return { docket: data.docket, confidence: data.confidence, source: 'ollama' };\u001b[39m\n\u001b[31m      }\u001b[39m\n\u001b[31m    } catch(e) { /* fall through */ }\u001b[39m\n\u001b[31m  }·\u001b[39m\n\u001b[31m  return { docket: '', confidence: 0, source: 'failed' };\u001b[39m\n\u001b[31m};·\u001b[39m\n\u001b[31mfunction extractDocketFromText(text, source) {\u001b[39m\n\u001b[31m  // Pattern: typical courier docket = 8-15 digit numbers, sometimes with letters\u001b[39m\n\u001b[31m  const patterns = [\u001b[39m\n\u001b[31m    /\\\\b([A-Z]{0,3}\\\\d{8,15}[A-Z]{0,3})\\\\b/g,   // alphanumeric tracking\u001b[39m\n\u001b[31m    /\\\\b(\\\\d{9,15})\\\\b/g,                          // pure numeric\u001b[39m\n\u001b[31m    /\\\\b([A-Z]{2,4}\\\\d{6,12})\\\\b/g,               // letter prefix\u001b[39m\n\u001b[31m  ];\u001b[39m\n\u001b[31m  const candidates = [];\u001b[39m\n\u001b[31m  for (const pat of patterns) {\u001b[39m\n\u001b[31m    let m;\u001b[39m\n\u001b[31m    while ((m = pat.exec(text)) !== null) {\u001b[39m\n\u001b[31m      if (!candidates.includes(m[1])) candidates.push(m[1]);\u001b[39m\n\u001b[31m    }\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m  if (candidates.length === 0) return { docket: text.trim().slice(0, 20), confidence: 0.3, source };\u001b[39m\n\u001b[31m  // Pick longest candidate (usually most complete docket)\u001b[39m\n\u001b[31m  const best = candidates.sort((a, b) => b.length - a.length)[0];\u001b[39m\n\u001b[31m  const confidence = candidates.length === 1 ? 0.85 : 0.7;\u001b[39m\n\u001b[31m  return { docket: best, confidence, source };\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// Expose for non-module code\u001b[39m\n\u001b[31mwindow._ocrReady = () => ocrPipeline !== null;\u001b[39m\n\u001b[31m</script>·\u001b[39m\n\u001b[31m<script>\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31m// STATE & STORAGE\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31mlet dispatches = [];\u001b[39m\n\u001b[31mlet stores = [];\u001b[39m\n\u001b[31mlet slipOCRData = [];\u001b[39m\n\u001b[31mlet dashFilter = 'all';·\u001b[39m\n\u001b[31m// Only sheets_url is persisted locally — all other config comes from the sheet\u001b[39m\n\u001b[31mlet _trackingInProgress = false;\u001b[39m\n\u001b[31mwindow.appConfig = {\u001b[39m\n\u001b[31m  sheets_url:      'https://script.google.com/macros/s/AKfycbys8HrToDv2u_dRvSFnNqmO9bnyAO1s8oSprUydpVQnNQutq4tuUY71cVCH24urbvM9/exec',\u001b[39m\n\u001b[31m  webhook_url:     '',\u001b[39m\n\u001b[31m  proxy_url:       '',\u001b[39m\n\u001b[31m  ollama_url:      'http://srv1111289.hstgr.cloud:11434',\u001b[39m\n\u001b[31m  ocr_mode:        'both',\u001b[39m\n\u001b[31m  company:         '360 Degree Logical Marketing',\u001b[39m\n\u001b[31m  city:            'Chandigarh',\u001b[39m\n\u001b[31m  auto_log:        'true',\u001b[39m\n\u001b[31m  refresh_on_load: 'false',\u001b[39m\n\u001b[31m};·\u001b[39m\n\u001b[31mconst COURIER_MASTER = {\u001b[39m\n\u001b[31m  'SkyKing': {\u001b[39m\n\u001b[31m    trackUrl: 'https://skyking.co/track?cno={docket}',\u001b[39m\n\u001b[31m    trackBulkUrl: 'https://skyking.co/track',\u001b[39m\n\u001b[31m    apiProxy: '/track-proxy/skyking?cno={docket}',\u001b[39m\n\u001b[31m    maxBulk: 10\u001b[39m\n\u001b[31m  },\u001b[39m\n\u001b[31m  'DTDC': {\u001b[39m\n\u001b[31m    trackUrl: 'https://www.dtdc.in/tracking.asp',\u001b[39m\n\u001b[31m    queryParam: 'trkId',\u001b[39m\n\u001b[31m    maxBulk: 1\u001b[39m\n\u001b[31m  },\u001b[39m\n\u001b[31m  'Delhivery': {\u001b[39m\n\u001b[31m    trackUrl: 'https://www.delhivery.com/track/package/{docket}',\u001b[39m\n\u001b[31m    maxBulk: 1\u001b[39m\n\u001b[31m  },\u001b[39m\n\u001b[31m  'Blue Dart': {\u001b[39m\n\u001b[31m    trackUrl: 'https://www.bluedart.com/tracking',\u001b[39m\n\u001b[31m    maxBulk: 1\u001b[39m\n\u001b[31m  },\u001b[39m\n\u001b[31m  'FedEx': {\u001b[39m\n\u001b[31m    trackUrl: 'https://www.fedex.com/fedextrack/?trknbr={docket}',\u001b[39m\n\u001b[31m    maxBulk: 10\u001b[39m\n\u001b[31m  },\u001b[39m\n\u001b[31m  'Ecom Express': {\u001b[39m\n\u001b[31m    trackUrl: 'https://ecomexpress.in/tracking/?awb_field={docket}',\u001b[39m\n\u001b[31m    maxBulk: 1\u001b[39m\n\u001b[31m  },\u001b[39m\n\u001b[31m  'India Post': {\u001b[39m\n\u001b[31m    trackUrl: 'https://www.indiapost.gov.in/VAS/Pages/trackconsignment.aspx',\u001b[39m\n\u001b[31m    maxBulk: 1\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m};·\u001b[39m\n\u001b[31mfunction loadData() {\u001b[39m\n\u001b[31m  try {\u001b[39m\n\u001b[31m    dispatches = JSON.parse(localStorage.getItem('360lm_disp_v2') || '[]');\u001b[39m\n\u001b[31m    const saved = JSON.parse(localStorage.getItem('360lm_disp_sheets_url') || '{}');\u001b[39m\n\u001b[31m    if (saved.sheets_url) window.appConfig.sheets_url = saved.sheets_url;\u001b[39m\n\u001b[31m  } catch(e) {}\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31mfunction saveData() {\u001b[39m\n\u001b[31m  try {\u001b[39m\n\u001b[31m    localStorage.setItem('360lm_disp_v2', JSON.stringify(dispatches));\u001b[39m\n\u001b[31m  } catch(e) {\u001b[39m\n\u001b[31m    if (e.name === 'QuotaExceededError' || e.code === 22) {\u001b[39m\n\u001b[31m      showToast('Storage full — old dispatches cleared. Data is safe in Postgres.', 'err');\u001b[39m\n\u001b[31m      // Drop delivered dispatches older than 30 days to free space\u001b[39m\n\u001b[31m      const cutoff = Date.now() - 30 * 24 * 3600 * 1000;\u001b[39m\n\u001b[31m      dispatches = dispatches.filter(d =>\u001b[39m\n\u001b[31m        d.status !== 'draft' && (\u001b[39m\n\u001b[31m          d.status !== 'active' ||\u001b[39m\n\u001b[31m          new Date(d.createdAt || 0).getTime() > cutoff\u001b[39m\n\u001b[31m        ) ? true :\u001b[39m\n\u001b[31m        d.status === 'draft' || new Date(d.createdAt || 0).getTime() > cutoff\u001b[39m\n\u001b[31m      );\u001b[39m\n\u001b[31m      try { localStorage.setItem('360lm_disp_v2', JSON.stringify(dispatches)); } catch(_) {}\u001b[39m\n\u001b[31m    }\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// ── Postgres DB sync ──────────────────────────────────────────────────────\u001b[39m\n\u001b[31mconst _DB        = '/db';\u001b[39m\n\u001b[31m// PostgREST v12: Accept-Profile for reads, Content-Profile for writes\u001b[39m\n\u001b[31mconst _DHDR_READ  = { 'Accept-Profile': 'dispatch' };\u001b[39m\n\u001b[31mconst _DHDR_WRITE = { 'Content-Type': 'application/json', 'Content-Profile': 'dispatch', 'Accept-Profile': 'dispatch' };\u001b[39m\n\u001b[31mconst _DHDR_UPSERT = { ..._DHDR_WRITE, 'Prefer': 'resolution=merge-duplicates,return=minimal' };·\u001b[39m\n\u001b[31mfunction _dispRow(d) {\u001b[39m\n\u001b[31m  return {\u001b[39m\n\u001b[31m    dispatch_id:    d.id,\u001b[39m\n\u001b[31m    client:         d.client    || '',\u001b[39m\n\u001b[31m    job_ref:        d.jobref    || null,\u001b[39m\n\u001b[31m    courier:        d.courier   || '',\u001b[39m\n\u001b[31m    dispatch_date:  d.dispatchDate || null,\u001b[39m\n\u001b[31m    status:         d.status    || 'active',\u001b[39m\n\u001b[31m    total_count:    (d.consignees || []).length,\u001b[39m\n\u001b[31m    docketed_count: (d.consignees || []).filter(c => c.docket).length,\u001b[39m\n\u001b[31m    remarks:        d.remarks   || null,\u001b[39m\n\u001b[31m    created_by:     window._hubUser?.id || 'unknown',\u001b[39m\n\u001b[31m  };\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction _consRow(c, dispatchId, idx) {\u001b[39m\n\u001b[31m  return {\u001b[39m\n\u001b[31m    cons_id:            `${dispatchId}-${idx}`,\u001b[39m\n\u001b[31m    dispatch_id:        dispatchId,\u001b[39m\n\u001b[31m    seq_no:             idx,\u001b[39m\n\u001b[31m    name:               c.name          || '',\u001b[39m\n\u001b[31m    city:               c.city          || null,\u001b[39m\n\u001b[31m    address:            c.address       || null,\u001b[39m\n\u001b[31m    phone:              c.phone         || null,\u001b[39m\n\u001b[31m    pincode:            c.pincode       || null,\u001b[39m\n\u001b[31m    material:           c.material      || null,\u001b[39m\n\u001b[31m    qty:                c.qty           || null,\u001b[39m\n\u001b[31m    docket:             c.docket        || null,\u001b[39m\n\u001b[31m    status:             c.status        || 'pending',\u001b[39m\n\u001b[31m    last_event:         c.lastEvent     || null,\u001b[39m\n\u001b[31m    last_scan_location: c.lastScanLocation || null,\u001b[39m\n\u001b[31m    last_tracked_at:    c.lastTrackedAt || null,\u001b[39m\n\u001b[31m    manual_status:      c.manualStatus  || null,\u001b[39m\n\u001b[31m    manual_delivered:   !!c.manualDelivered,\u001b[39m\n\u001b[31m    manual_called_by:   c.manualCalledBy || null,\u001b[39m\n\u001b[31m    manual_remarks:     c.manualRemarks || null,\u001b[39m\n\u001b[31m    pod_uploaded:       !!c.podUploaded,\u001b[39m\n\u001b[31m  };\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31masync function syncDispatchToDB(d) {\u001b[39m\n\u001b[31m  if (!d || d.status === 'draft') return; // never sync drafts\u001b[39m\n\u001b[31m  try {\u001b[39m\n\u001b[31m    await fetch(`${_DB}/dispatches`, {\u001b[39m\n\u001b[31m      method: 'POST', headers: _DHDR_UPSERT,\u001b[39m\n\u001b[31m      body: JSON.stringify(_dispRow(d)),\u001b[39m\n\u001b[31m    });\u001b[39m\n\u001b[31m    const rows = (d.consignees || []).map((c, i) => _consRow(c, d.id, i));\u001b[39m\n\u001b[31m    if (rows.length) {\u001b[39m\n\u001b[31m      await fetch(`${_DB}/consignees`, {\u001b[39m\n\u001b[31m        method: 'POST', headers: _DHDR_UPSERT,\u001b[39m\n\u001b[31m        body: JSON.stringify(rows),\u001b[39m\n\u001b[31m      });\u001b[39m\n\u001b[31m    }\u001b[39m\n\u001b[31m  } catch(e) { console.warn('[dispatch-db] sync error:', e.message); }\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31masync function syncConsigneeToDB(c, dispatchId, idx) {\u001b[39m\n\u001b[31m  try {\u001b[39m\n\u001b[31m    await fetch(`${_DB}/consignees`, {\u001b[39m\n\u001b[31m      method: 'POST', headers: _DHDR_UPSERT,\u001b[39m\n\u001b[31m      body: JSON.stringify(_consRow(c, dispatchId, idx)),\u001b[39m\n\u001b[31m    });\u001b[39m\n\u001b[31m    // Log tracking event\u001b[39m\n\u001b[31m    if (c.lastEvent && c.status) {\u001b[39m\n\u001b[31m      fetch(`${_DB}/tracking_events`, {\u001b[39m\n\u001b[31m        method: 'POST', headers: _DHDR_WRITE,\u001b[39m\n\u001b[31m        body: JSON.stringify({\u001b[39m\n\u001b[31m          docket: c.docket, dispatch_id: dispatchId,\u001b[39m\n\u001b[31m          cons_id: `${dispatchId}-${idx}`,\u001b[39m\n\u001b[31m          status: c.status, event_text: c.lastEvent,\u001b[39m\n\u001b[31m          scan_location: c.lastScanLocation || null,\u001b[39m\n\u001b[31m        }),\u001b[39m\n\u001b[31m      }).catch(() => {});\u001b[39m\n\u001b[31m    }\u001b[39m\n\u001b[31m  } catch(e) { console.warn('[dispatch-db] consignee sync error:', e.message); }\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31masync function loadFromDB() {\u001b[39m\n\u001b[31m  try {\u001b[39m\n\u001b[31m    const [dResp, cResp] = await Promise.all([\u001b[39m\n\u001b[31m      fetch(`${_DB}/dispatches?order=created_at.desc&limit=100`, { headers: _DHDR_READ }),\u001b[39m\n\u001b[31m      fetch(`${_DB}/consignees?order=dispatch_id,seq_no`, { headers: _DHDR_READ }),\u001b[39m\n\u001b[31m    ]);\u001b[39m\n\u001b[31m    if (!dResp.ok) return;\u001b[39m\n\u001b[31m    const dbDispatches = await dResp.json();\u001b[39m\n\u001b[31m    const dbConsignees = cResp.ok ? await cResp.json() : [];\u001b[39m\n\u001b[31m    if (!dbDispatches.length) return;·\u001b[39m\n\u001b[31m    const dbIds = new Set(dbDispatches.map(d => d.dispatch_id));\u001b[39m\n\u001b[31m    const localOnly = dispatches.filter(d => !dbIds.has(d.id));·\u001b[39m\n\u001b[31m    const fromDB = dbDispatches.map(d => {\u001b[39m\n\u001b[31m      const cons = dbConsignees.filter(c => c.dispatch_id === d.dispatch_id);\u001b[39m\n\u001b[31m      return {\u001b[39m\n\u001b[31m        id:           d.dispatch_id,\u001b[39m\n\u001b[31m        client:       d.client,\u001b[39m\n\u001b[31m        jobref:       d.job_ref,\u001b[39m\n\u001b[31m        courier:      d.courier,\u001b[39m\n\u001b[31m        dispatchDate: d.dispatch_date,\u001b[39m\n\u001b[31m        status:       d.status,\u001b[39m\n\u001b[31m        remarks:      d.remarks,\u001b[39m\n\u001b[31m        createdAt:    d.created_at,\u001b[39m\n\u001b[31m        consignees:   cons.map(c => ({\u001b[39m\n\u001b[31m          name:              c.name,\u001b[39m\n\u001b[31m          city:              c.city,\u001b[39m\n\u001b[31m          address:           c.address,\u001b[39m\n\u001b[31m          phone:             c.phone,\u001b[39m\n\u001b[31m          pincode:           c.pincode,\u001b[39m\n\u001b[31m          material:          c.material,\u001b[39m\n\u001b[31m          qty:               c.qty,\u001b[39m\n\u001b[31m          docket:            c.docket,\u001b[39m\n\u001b[31m          status:            c.status,\u001b[39m\n\u001b[31m          lastEvent:         c.last_event,\u001b[39m\n\u001b[31m          lastScanLocation:  c.last_scan_location,\u001b[39m\n\u001b[31m          lastTrackedAt:     c.last_tracked_at,\u001b[39m\n\u001b[31m          manualStatus:      c.manual_status,\u001b[39m\n\u001b[31m          manualDelivered:   c.manual_delivered,\u001b[39m\n\u001b[31m          manualCalledBy:    c.manual_called_by,\u001b[39m\n\u001b[31m          manualRemarks:     c.manual_remarks,\u001b[39m\n\u001b[31m          podUploaded:       c.pod_uploaded,\u001b[39m\n\u001b[31m          _dbIdx:            c.seq_no,\u001b[39m\n\u001b[31m        })),\u001b[39m\n\u001b[31m      };\u001b[39m\n\u001b[31m    });·\u001b[39m\n\u001b[31m    dispatches = [...fromDB, ...localOnly];\u001b[39m\n\u001b[31m    saveData();\u001b[39m\n\u001b[31m    renderDashboard();\u001b[39m\n\u001b[31m    updateHistCount();\u001b[39m\n\u001b[31m  } catch(e) { console.warn('[dispatch-db] load error:', e.message); }\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31mfunction saveSheetsUrlLocal() {\u001b[39m\n\u001b[31m  localStorage.setItem('360lm_disp_sheets_url', JSON.stringify({ sheets_url: window.appConfig.sheets_url }));\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m/**\u001b[39m\n\u001b[31m * Fetch config.json from the VPS server (same origin).\u001b[39m\n\u001b[31m * This file contains the sheets_url — no manual entry needed.\u001b[39m\n\u001b[31m * Falls back to localStorage cache if fetch fails (offline).\u001b[39m\n\u001b[31m */\u001b[39m\n\u001b[31masync function loadServerConfig() {\u001b[39m\n\u001b[31m  try {\u001b[39m\n\u001b[31m    const resp = await fetch('./config.json', { cache: 'no-store' });\u001b[39m\n\u001b[31m    if (resp.ok) {\u001b[39m\n\u001b[31m      const cfg = await resp.json();\u001b[39m\n\u001b[31m      if (cfg.sheets_url) {\u001b[39m\n\u001b[31m        window.appConfig.sheets_url = cfg.sheets_url;\u001b[39m\n\u001b[31m        saveSheetsUrlLocal(); // cache it for offline use\u001b[39m\n\u001b[31m        document.getElementById('cfg_sheets_url').value = cfg.sheets_url;\u001b[39m\n\u001b[31m        return true;\u001b[39m\n\u001b[31m      }\u001b[39m\n\u001b[31m    }\u001b[39m\n\u001b[31m  } catch(e) {\u001b[39m\n\u001b[31m    console.log('config.json fetch failed, using cached URL:', e.message);\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m  return false;\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31m// INIT\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31m// ── Hub auth gate ─────────────────────────────────────────────────────────\u001b[39m\n\u001b[31mwindow._hubUser = null;\u001b[39m\n\u001b[31m(function() {\u001b[39m\n\u001b[31m  try {\u001b[39m\n\u001b[31m    const hubLogin = '/hub/?next=' + encodeURIComponent(location.pathname + location.search);\u001b[39m\n\u001b[31m    const s = JSON.parse(localStorage.getItem('lm360-session') || 'null');\u001b[39m\n\u001b[31m    if (!s || !s.empId || !s.name) { window.location.replace(hubLogin); return; }\u001b[39m\n\u001b[31m    if (Date.now() - new Date(s.loginAt).getTime() > 12 * 3600 * 1000) {\u001b[39m\n\u001b[31m      window.location.replace(hubLogin); return;\u001b[39m\n\u001b[31m    }\u001b[39m\n\u001b[31m    window._hubUser = { id: s.empId, name: s.name, role: s.role };\u001b[39m\n\u001b[31m  } catch(_) { window.location.replace('/hub/?next=' + encodeURIComponent(location.pathname + location.search)); }\u001b[39m\n\u001b[31m})();·\u001b[39m\n\u001b[31mwindow.addEventListener('load', async () => {\u001b[39m\n\u001b[31m  if (!window._hubUser) return; // should never reach here — gate redirected already\u001b[39m\n\u001b[31m  setTimeout(() => {\u001b[39m\n\u001b[31m    document.getElementById('splash').style.opacity = '0';\u001b[39m\n\u001b[31m    setTimeout(() => {\u001b[39m\n\u001b[31m      document.getElementById('splash').style.display = 'none';\u001b[39m\n\u001b[31m      document.getElementById('app').style.display = 'flex';\u001b[39m\n\u001b[31m    }, 450);\u001b[39m\n\u001b[31m  }, 1400);\u001b[39m\n\u001b[31m  loadData();\u001b[39m\n\u001b[31m  // Auto-authenticate config tab for admin users (no password needed)\u001b[39m\n\u001b[31m  if (['harish', 'pramod'].includes(window._hubUser?.id)) _adminAuthed = true;\u001b[39m\n\u001b[31m  setToday();\u001b[39m\n\u001b[31m  addConsignee();\u001b[39m\n\u001b[31m  updateHistCount();\u001b[39m\n\u001b[31m  setInterval(updateNetDot, 5000);\u001b[39m\n\u001b[31m  updateNetDot();\u001b[39m\n\u001b[31m  checkShareTarget();\u001b[39m\n\u001b[31m  setupDragDrop();·\u001b[39m\n\u001b[31m  // Step 1: fetch sheets_url from VPS config.json (auto, no user input)\u001b[39m\n\u001b[31m  await loadServerConfig();·\u001b[39m\n\u001b[31m  // Step 2: populate config tab field\u001b[39m\n\u001b[31m  document.getElementById('cfg_sheets_url').value = window.appConfig.sheets_url || '';·\u001b[39m\n\u001b[31m  // Step 3: load dispatches from Postgres in background (merge with local)\u001b[39m\n\u001b[31m  loadFromDB(); // non-blocking — updates UI when data arrives·\u001b[39m\n\u001b[31m  // Step 4: sync full config from Google Sheet silently\u001b[39m\n\u001b[31m  if (window.appConfig.sheets_url) {\u001b[39m\n\u001b[31m    syncFromSheet(true); // silent on startup\u001b[39m\n\u001b[31m  } else {\u001b[39m\n\u001b[31m    // Only show config tab if no URL found anywhere\u001b[39m\n\u001b[31m    showPane('config');\u001b[39m\n\u001b[31m    showToast('No config.json found on server', 'err');\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m  // Pre-warm OCR after delay\u001b[39m\n\u001b[31m  setTimeout(() => { if (typeof window.initOCR === 'function') window.initOCR(); }, 4000);\u001b[39m\n\u001b[31m});·\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31m// WEB SHARE TARGET\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31masync function checkShareTarget() {\u001b[39m\n\u001b[31m  // Check URL params for share target\u001b[39m\n\u001b[31m  const params = new URLSearchParams(window.location.search);\u001b[39m\n\u001b[31m  const action = params.get('action');\u001b[39m\n\u001b[31m  if (action !== 'share') return;·\u001b[39m\n\u001b[31m  // Check for shared files via Share Target API\u001b[39m\n\u001b[31m  if ('launchQueue' in window) {\u001b[39m\n\u001b[31m    window.launchQueue.setConsumer(async launchParams => {\u001b[39m\n\u001b[31m      if (!launchParams.files || launchParams.files.length === 0) return;\u001b[39m\n\u001b[31m      const files = await Promise.all(launchParams.files.map(f => f.getFile()));\u001b[39m\n\u001b[31m      const imageFiles = files.filter(f => f.type.startsWith('image/'));\u001b[39m\n\u001b[31m      if (imageFiles.length > 0) {\u001b[39m\n\u001b[31m        document.getElementById('shareTargetInfo').style.display = 'flex';\u001b[39m\n\u001b[31m        document.getElementById('sharedCount').textContent = imageFiles.length;\u001b[39m\n\u001b[31m        document.getElementById('shareBanner').classList.add('show');\u001b[39m\n\u001b[31m        setTimeout(() => document.getElementById('shareBanner').classList.remove('show'), 4000);\u001b[39m\n\u001b[31m        showPane('new');\u001b[39m\n\u001b[31m        await handleSlipFiles(imageFiles);\u001b[39m\n\u001b[31m      }\u001b[39m\n\u001b[31m    });\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction dismissShare() {\u001b[39m\n\u001b[31m  document.getElementById('shareBanner').classList.remove('show');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31m// DRAG & DROP\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31mfunction setupDragDrop() {\u001b[39m\n\u001b[31m  const zone = document.getElementById('slipZone');\u001b[39m\n\u001b[31m  zone.addEventListener('dragover', e => { e.preventDefault(); zone.classList.add('drag'); });\u001b[39m\n\u001b[31m  zone.addEventListener('dragleave', () => zone.classList.remove('drag'));\u001b[39m\n\u001b[31m  zone.addEventListener('drop', e => {\u001b[39m\n\u001b[31m    e.preventDefault();\u001b[39m\n\u001b[31m    zone.classList.remove('drag');\u001b[39m\n\u001b[31m    const files = Array.from(e.dataTransfer.files).filter(f => f.type.startsWith('image/'));\u001b[39m\n\u001b[31m    if (files.length) handleSlipFiles(files);\u001b[39m\n\u001b[31m  });\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31m// SLIP OCR HANDLER\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31masync function handleSlipFiles(files) {\u001b[39m\n\u001b[31m  const fileArr = Array.from(files);\u001b[39m\n\u001b[31m  if (!fileArr.length) return;\u001b[39m\n\u001b[31m  // If dockets already mapped from store list — allow upload but skip OCR extraction\u001b[39m\n\u001b[31m  const skipOCR = shouldSkipOCR();\u001b[39m\n\u001b[31m  if (skipOCR) {\u001b[39m\n\u001b[31m    showToast('Dockets already mapped — slips saved for reference only', 'info');\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m  showOCRWorking(true, `Processing ${fileArr.length} slip(s)...`);·\u001b[39m\n\u001b[31m  for (let i = 0; i < fileArr.length; i++) {\u001b[39m\n\u001b[31m    const file = fileArr[i];\u001b[39m\n\u001b[31m    showOCRWorking(true, `OCR: ${i+1}/${fileArr.length} — ${file.name}`);\u001b[39m\n\u001b[31m    const imageData = await readFileAsDataURL(file);\u001b[39m\n\u001b[31m    let result = { docket: '', confidence: 0, source: 'manual' };\u001b[39m\n\u001b[31m    if (!skipOCR) {\u001b[39m\n\u001b[31m      try {\u001b[39m\n\u001b[31m        result = await window.runOCROnImage(imageData, file.name);\u001b[39m\n\u001b[31m      } catch(e) {}\u001b[39m\n\u001b[31m    } else {\u001b[39m\n\u001b[31m      result = { docket: '', confidence: 1, source: 'skipped — docket pre-mapped' };\u001b[39m\n\u001b[31m    }\u001b[39m\n\u001b[31m    slipOCRData.push({ file, imageData, filename: file.name, ...result });\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m  showOCRWorking(false);\u001b[39m\n\u001b[31m  renderOCRResults();\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction readFileAsDataURL(file) {\u001b[39m\n\u001b[31m  return new Promise((res, rej) => {\u001b[39m\n\u001b[31m    const r = new FileReader();\u001b[39m\n\u001b[31m    r.onload = e => res(e.target.result);\u001b[39m\n\u001b[31m    r.onerror = rej;\u001b[39m\n\u001b[31m    r.readAsDataURL(file);\u001b[39m\n\u001b[31m  });\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction renderOCRResults() {\u001b[39m\n\u001b[31m  if (!slipOCRData.length) return;\u001b[39m\n\u001b[31m  const list = document.getElementById('ocrSlipList');\u001b[39m\n\u001b[31m  document.getElementById('ocrResults').classList.add('visible');·\u001b[39m\n\u001b[31m  const needReview = slipOCRData.filter(s => s.confidence < 0.65 && s.docket).length;\u001b[39m\n\u001b[31m  const autoOk     = slipOCRData.filter(s => s.confidence >= 0.88).length;\u001b[39m\n\u001b[31m  const summary    = document.getElementById('ocrSummary');\u001b[39m\n\u001b[31m  if (summary) {\u001b[39m\n\u001b[31m    summary.style.display = '';\u001b[39m\n\u001b[31m    summary.innerHTML =\u001b[39m\n\u001b[31m      `<span style=\\\"color:var(--grn)\\\">✓ ${autoOk} auto-accepted</span>` +\u001b[39m\n\u001b[31m      (needReview ? `&nbsp;·&nbsp;<span style=\\\"color:var(--ylw)\\\">⚠ ${needReview} need manual check</span>` : '');\u001b[39m\n\u001b[31m  }·\u001b[39m\n\u001b[31m  list.innerHTML = slipOCRData.map((s, i) => {\u001b[39m\n\u001b[31m    const autoAccepted = s.confidence >= 0.88 && s.docket;\u001b[39m\n\u001b[31m    const needsCheck   = s.confidence < 0.65  && s.docket;\u001b[39m\n\u001b[31m    const confClass = autoAccepted ? 'conf-high' : needsCheck ? 'conf-low' : 'conf-med';\u001b[39m\n\u001b[31m    const confLabel = autoAccepted\u001b[39m\n\u001b[31m      ? `✓ ${Math.round(s.confidence * 100)}% — auto`\u001b[39m\n\u001b[31m      : needsCheck\u001b[39m\n\u001b[31m        ? `⚠ ${Math.round(s.confidence * 100)}% — verify`\u001b[39m\n\u001b[31m        : `${Math.round(s.confidence * 100)}%`;\u001b[39m\n\u001b[31m    const borderStyle = needsCheck ? 'border:1px solid var(--ylw);' : '';\u001b[39m\n\u001b[31m    return `\u001b[39m\n\u001b[31m    <div class=\\\"ocr-slip\\\" id=\\\"ocrSlip_${i}\\\" style=\\\"${borderStyle}\\\">\u001b[39m\n\u001b[31m      <img src=\\\"${s.imageData}\\\" alt=\\\"slip ${i+1}\\\">\u001b[39m\n\u001b[31m      <div class=\\\"ocr-slip-body\\\">\u001b[39m\n\u001b[31m        <div class=\\\"ocr-slip-name\\\">${s.filename}</div>\u001b[39m\n\u001b[31m        <input type=\\\"text\\\" class=\\\"ocr-slip-docket\\\" id=\\\"ocrDocket_${i}\\\"\u001b[39m\n\u001b[31m          value=\\\"${s.docket}\\\" placeholder=\\\"Docket number\\\"\u001b[39m\n\u001b[31m          ${autoAccepted ? 'style=\\\"opacity:.6\\\"' : ''}\u001b[39m\n\u001b[31m          oninput=\\\"slipOCRData[${i}].docket=this.value;slipOCRData[${i}].confidence=0.5\\\">\u001b[39m\n\u001b[31m        <div class=\\\"ocr-slip-status\\\">\u001b[39m\n\u001b[31m          <span class=\\\"ocr-confidence ${confClass}\\\">${confLabel}</span>\u001b[39m\n\u001b[31m          <span style=\\\"color:var(--t4);font-family:var(--fnt-mono);font-size:.6rem;\\\">via ${s.source}</span>\u001b[39m\n\u001b[31m          <button class=\\\"ocr-ai-btn\\\" onclick=\\\"reOCR(${i})\\\">🔄 Re-OCR</button>\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m    </div>`;\u001b[39m\n\u001b[31m  }).join('');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31masync function reOCR(i) {\u001b[39m\n\u001b[31m  const s = slipOCRData[i];\u001b[39m\n\u001b[31m  showOCRWorking(true, `Re-running OCR on ${s.filename}...`);\u001b[39m\n\u001b[31m  try {\u001b[39m\n\u001b[31m    const result = await window.runOCROnImage(s.imageData, s.filename);\u001b[39m\n\u001b[31m    slipOCRData[i] = { ...s, ...result };\u001b[39m\n\u001b[31m    renderOCRResults();\u001b[39m\n\u001b[31m    showToast('Re-OCR complete', 'ok');\u001b[39m\n\u001b[31m  } catch(e) {\u001b[39m\n\u001b[31m    showToast('OCR failed', 'err');\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m  showOCRWorking(false);\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction clearSlips() {\u001b[39m\n\u001b[31m  slipOCRData = [];\u001b[39m\n\u001b[31m  document.getElementById('ocrResults').classList.remove('visible');\u001b[39m\n\u001b[31m  document.getElementById('ocrSlipList').innerHTML = '';\u001b[39m\n\u001b[31m  const sum = document.getElementById('ocrSummary');\u001b[39m\n\u001b[31m  if (sum) { sum.style.display = 'none'; sum.innerHTML = ''; }\u001b[39m\n\u001b[31m  document.getElementById('shareTargetInfo').style.display = 'none';\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction applyAllDockets() {\u001b[39m\n\u001b[31m  // Match each OCR result to a consignee by order or by name matching\u001b[39m\n\u001b[31m  const consList = document.getElementById('consList');\u001b[39m\n\u001b[31m  const cards = consList.querySelectorAll('.cons-card');\u001b[39m\n\u001b[31m  slipOCRData.forEach((s, i) => {\u001b[39m\n\u001b[31m    if (!s.docket) return;\u001b[39m\n\u001b[31m    const card = cards[i];\u001b[39m\n\u001b[31m    if (card) {\u001b[39m\n\u001b[31m      const docketInput = card.querySelector('.cons-docket-input');\u001b[39m\n\u001b[31m      if (docketInput) {\u001b[39m\n\u001b[31m        docketInput.value = s.docket;\u001b[39m\n\u001b[31m        showLinkedDocket(card, s.docket);\u001b[39m\n\u001b[31m      }\u001b[39m\n\u001b[31m    }\u001b[39m\n\u001b[31m  });\u001b[39m\n\u001b[31m  showToast(`Applied ${slipOCRData.filter(s=>s.docket).length} dockets ✓`, 'ok');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31m// STORE LIST — with column mapping UI\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31mlet _rawRows = [];      // raw rows from file, kept for re-mapping\u001b[39m\n\u001b[31mlet _rawHeaders = [];   // headers from file·\u001b[39m\n\u001b[31masync function handleStoreFile(input) {\u001b[39m\n\u001b[31m  const file = input.files[0];\u001b[39m\n\u001b[31m  if (!file) return;\u001b[39m\n\u001b[31m  showToast('Reading file...', 'info');·\u001b[39m\n\u001b[31m  const data = await readFileAsDataURL(file);\u001b[39m\n\u001b[31m  const base64 = data.split(',')[1];\u001b[39m\n\u001b[31m  const binary = atob(base64);\u001b[39m\n\u001b[31m  const bytes = new Uint8Array(binary.length);\u001b[39m\n\u001b[31m  for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);·\u001b[39m\n\u001b[31m  let rows = [];\u001b[39m\n\u001b[31m  try {\u001b[39m\n\u001b[31m    if (file.name.endsWith('.csv')) {\u001b[39m\n\u001b[31m      const text = new TextDecoder().decode(bytes);\u001b[39m\n\u001b[31m      rows = text.split('\\\\n').map(r => r.split(',').map(c => c.trim().replace(/^\\\"|\\\"$/g, '')));\u001b[39m\n\u001b[31m    } else {\u001b[39m\n\u001b[31m      const wb = XLSX.read(bytes, { type: 'array' });\u001b[39m\n\u001b[31m      const ws = wb.Sheets[wb.SheetNames[0]];\u001b[39m\n\u001b[31m      rows = XLSX.utils.sheet_to_json(ws, { header: 1 });\u001b[39m\n\u001b[31m    }\u001b[39m\n\u001b[31m  } catch(e) { showToast('File read error: ' + e.message, 'err'); return; }·\u001b[39m\n\u001b[31m  if (!rows.length || !rows[0]) { showToast('File appears empty', 'err'); return; }·\u001b[39m\n\u001b[31m  _rawHeaders = (rows[0] || []).map(h => (h || '').toString().trim());\u001b[39m\n\u001b[31m  _rawRows = rows.slice(1).filter(r => r && r.some(c => c !== '' && c != null));·\u001b[39m\n\u001b[31m  document.getElementById('storeTableArea').style.display = 'block';\u001b[39m\n\u001b[31m  renderMappingUI(file.name);\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// ── COLUMN MAPPING FIELDS ─────────────────────────────\u001b[39m\n\u001b[31mconst MAPPING_FIELDS = [\u001b[39m\n\u001b[31m  { key: 'name',    label: 'Store / Partner Name', required: true,\u001b[39m\n\u001b[31m    keywords: ['store','shop','name','outlet','consignee','destination','partner','dealer','client','retailer'] },\u001b[39m\n\u001b[31m  { key: 'city',    label: 'City / Location',       required: false,\u001b[39m\n\u001b[31m    keywords: ['city','town','district','location','area','region','place'] },\u001b[39m\n\u001b[31m  { key: 'phone',   label: 'Phone / Mobile',        required: false,\u001b[39m\n\u001b[31m    keywords: ['phone','mobile','contact','tel','number','mob'] },\u001b[39m\n\u001b[31m  { key: 'docket',  label: 'Docket / AWB No.',      required: false,\u001b[39m\n\u001b[31m    keywords: ['docket','awb','tracking','courier','dkt','consignment','cno','waybill'] },\u001b[39m\n\u001b[31m  { key: 'address', label: 'Address',               required: false,\u001b[39m\n\u001b[31m    keywords: ['address','addr','street','locality','area'] },\u001b[39m\n\u001b[31m  { key: 'pincode', label: 'Pincode / PIN',          required: false,\u001b[39m\n\u001b[31m    keywords: ['pin','pincode','postal','zip','code'] },\u001b[39m\n\u001b[31m];·\u001b[39m\n\u001b[31m// Saved mapping key based on column fingerprint (so same client file auto-maps)\u001b[39m\n\u001b[31mfunction getMappingKey() {\u001b[39m\n\u001b[31m  return 'colmap_' + _rawHeaders.join('|').toLowerCase().replace(/\\\\s+/g,'');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// ── Detect if a column is a combined/multiline address cell ─────────────\u001b[39m\n\u001b[31mfunction isCombinedColumn(colIdx) {\u001b[39m\n\u001b[31m  if (colIdx < 0) return false;\u001b[39m\n\u001b[31m  // Check first 3 data rows — if any cell contains newline or has 6+ digit number (pincode) = combined\u001b[39m\n\u001b[31m  const samples = _rawRows.slice(0, 3).map(r => (r[colIdx] || '').toString());\u001b[39m\n\u001b[31m  return samples.some(s => s.includes('\\\\n') || /\\\\b\\\\d{6}\\\\b/.test(s) || s.split(',').length >= 3);\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction renderMappingUI(filename) {\u001b[39m\n\u001b[31m  const saved = (() => { try { return JSON.parse(localStorage.getItem(getMappingKey()) || 'null'); } catch(e) { return null; } })();\u001b[39m\n\u001b[31m  const isSaved = !!saved;\u001b[39m\n\u001b[31m  const colInfoEl = document.getElementById('storeColInfo');\u001b[39m\n\u001b[31m  colInfoEl.style.display = 'block';·\u001b[39m\n\u001b[31m  const suggested = {};\u001b[39m\n\u001b[31m  MAPPING_FIELDS.forEach(f => {\u001b[39m\n\u001b[31m    const idx = findCol(_rawHeaders, f.keywords);\u001b[39m\n\u001b[31m    suggested[f.key] = saved ? (saved[f.key] ?? idx) : idx;\u001b[39m\n\u001b[31m  });·\u001b[39m\n\u001b[31m  // Detect if address column is combined\u001b[39m\n\u001b[31m  const addrIdx = suggested.address >= 0 ? suggested.address : findCol(_rawHeaders, ['address','addr']);\u001b[39m\n\u001b[31m  const looksCombo = isCombinedColumn(addrIdx) || isCombinedColumn(suggested.name);\u001b[39m\n\u001b[31m  const comboCol   = looksCombo ? (addrIdx >= 0 ? addrIdx : suggested.name) : -1;\u001b[39m\n\u001b[31m  const comboPreview = comboCol >= 0 && _rawRows[0] ? (_rawRows[0][comboCol] || '').toString().slice(0, 120) : '';·\u001b[39m\n\u001b[31m  colInfoEl.innerHTML = `\u001b[39m\n\u001b[31m    <div style=\\\"background:var(--sur2);border:1px solid var(--bdr);border-radius:var(--r-sm);padding:12px;\\\">\u001b[39m\n\u001b[31m      <div style=\\\"display:flex;align-items:center;justify-content:space-between;margin-bottom:10px;\\\">\u001b[39m\n\u001b[31m        <div style=\\\"font-family:var(--fnt-mono);font-size:.68rem;color:var(--sky);\\\">\u001b[39m\n\u001b[31m          📋 ${filename} &nbsp;·&nbsp; ${_rawRows.length} rows &nbsp;·&nbsp; ${_rawHeaders.length} columns\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m        ${isSaved ? '<span class=\\\"map-saved-badge\\\">✓ mapping remembered</span>' : ''}\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      <div style=\\\"font-size:.68rem;color:var(--t3);font-family:var(--fnt-mono);margin-bottom:10px;word-break:break-all;\\\">\u001b[39m\n\u001b[31m        Columns: <span style=\\\"color:var(--t2);\\\">${_rawHeaders.map((h,i)=>`[${i+1}] ${h||'?'}`).join(' · ')}</span>\u001b[39m\n\u001b[31m      </div>·\u001b[39m\n\u001b[31m      ${looksCombo ? `\u001b[39m\n\u001b[31m      <!-- COMBINED COLUMN DETECTED -->\u001b[39m\n\u001b[31m      <div style=\\\"background:rgba(56,189,248,.08);border:1px solid rgba(56,189,248,.3);\u001b[39m\n\u001b[31m        border-radius:var(--r-sm);padding:10px 12px;margin-bottom:10px;\\\">\u001b[39m\n\u001b[31m        <div style=\\\"font-size:.72rem;color:var(--sky);font-weight:600;margin-bottom:6px;\\\">\u001b[39m\n\u001b[31m          🤖 Combined Address Column Detected\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m        <div style=\\\"font-size:.68rem;color:var(--t2);font-family:var(--fnt-mono);\u001b[39m\n\u001b[31m          white-space:pre-wrap;background:var(--bg);padding:6px 8px;border-radius:4px;\u001b[39m\n\u001b[31m          margin-bottom:8px;max-height:80px;overflow:hidden;\\\">${comboPreview}</div>\u001b[39m\n\u001b[31m        <div style=\\\"font-size:.7rem;color:var(--t3);margin-bottom:8px;\\\">\u001b[39m\n\u001b[31m          AI will extract: Name · Address · City · Pincode · Phone from each cell.\u001b[39m\n\u001b[31m          Runs in browser (Transformers.js) with Ollama fallback.\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m        <div style=\\\"display:flex;align-items:center;gap:8px;\\\">\u001b[39m\n\u001b[31m          <label style=\\\"display:flex;align-items:center;gap:6px;font-size:.72rem;color:var(--t1);cursor:pointer;\\\">\u001b[39m\n\u001b[31m            <input type=\\\"checkbox\\\" id=\\\"useAIParse\\\" checked style=\\\"cursor:pointer;\\\">\u001b[39m\n\u001b[31m            Use AI to parse combined column\u001b[39m\n\u001b[31m          </label>\u001b[39m\n\u001b[31m          <select id=\\\"comboColSel\\\" style=\\\"background:var(--sur2);border:1px solid var(--bdr);\u001b[39m\n\u001b[31m            color:var(--t1);font-family:var(--fnt-mono);font-size:.72rem;padding:4px 8px;\u001b[39m\n\u001b[31m            border-radius:var(--r-sm);outline:none;\\\">\u001b[39m\n\u001b[31m            ${_rawHeaders.map((h,i)=>`<option value=\\\"${i}\\\" ${i===comboCol?'selected':''}>${h||'(col '+(i+1)+')'}</option>`).join('')}\u001b[39m\n\u001b[31m          </select>\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      ` : ''}·\u001b[39m\n\u001b[31m      <div id=\\\"manualMapSection\\\" style=\\\"${looksCombo ? 'display:none;' : ''}\\\">\u001b[39m\n\u001b[31m        <div style=\\\"font-size:.68rem;color:var(--t3);margin-bottom:6px;\\\">\u001b[39m\n\u001b[31m          ${looksCombo ? 'Manual mapping (used when AI parse is off, or for other columns):' : 'Map each field to your column:'}\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m        <table class=\\\"map-table\\\">\u001b[39m\n\u001b[31m          <thead><tr><th>Field</th><th>Your column</th><th>Preview (row 1)</th></tr></thead>\u001b[39m\n\u001b[31m          <tbody>\u001b[39m\n\u001b[31m            ${MAPPING_FIELDS.map(f => {\u001b[39m\n\u001b[31m              const selIdx = suggested[f.key] ?? -1;\u001b[39m\n\u001b[31m              const preview = selIdx >= 0 && _rawRows[0] ? ((_rawRows[0][selIdx]||'').toString().slice(0,40)) : '—';\u001b[39m\n\u001b[31m              return `<tr>\u001b[39m\n\u001b[31m                <td style=\\\"white-space:nowrap;color:var(--t1);font-weight:500;font-size:.75rem;\\\">\u001b[39m\n\u001b[31m                  ${f.label}${f.required ? '<span class=\\\"map-req\\\">*</span>' : ''}\u001b[39m\n\u001b[31m                </td>\u001b[39m\n\u001b[31m                <td>\u001b[39m\n\u001b[31m                  <select id=\\\"mapsel_${f.key}\\\" onchange=\\\"updateMappingPreview('${f.key}')\\\">\u001b[39m\n\u001b[31m                    ${_rawHeaders.map((h,i)=>\u001b[39m\n\u001b[31m                      `<option value=\\\"${i}\\\" ${i===selIdx?'selected':''}>${h||'(col '+(i+1)+')'}</option>`\u001b[39m\n\u001b[31m                    ).join('')}\u001b[39m\n\u001b[31m                    <option value=\\\"-1\\\" ${selIdx===-1?'selected':''}>— not in file —</option>\u001b[39m\n\u001b[31m                  </select>\u001b[39m\n\u001b[31m                </td>\u001b[39m\n\u001b[31m                <td id=\\\"mapprev_${f.key}\\\" style=\\\"font-family:var(--fnt-mono);font-size:.68rem;\u001b[39m\n\u001b[31m                  color:var(--t2);max-width:90px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;\\\">\u001b[39m\n\u001b[31m                  ${preview}\u001b[39m\n\u001b[31m                </td>\u001b[39m\n\u001b[31m              </tr>`;\u001b[39m\n\u001b[31m            }).join('')}\u001b[39m\n\u001b[31m          </tbody>\u001b[39m\n\u001b[31m        </table>\u001b[39m\n\u001b[31m      </div>·\u001b[39m\n\u001b[31m      <!-- Also map docket/courier no column separately — always shown -->\u001b[39m\n\u001b[31m      <div style=\\\"margin-top:10px;padding-top:10px;border-top:1px solid var(--bdr);\\\">\u001b[39m\n\u001b[31m        <div style=\\\"font-size:.68rem;color:var(--t3);margin-bottom:6px;font-family:var(--fnt-mono);\\\">\u001b[39m\n\u001b[31m          DOCKET / COURIER NO. COLUMN (required for tracking):\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m        <div style=\\\"display:flex;gap:8px;align-items:center;\\\">\u001b[39m\n\u001b[31m          <select id=\\\"docketColSel\\\" style=\\\"background:var(--sur2);border:1px solid var(--bdr);\u001b[39m\n\u001b[31m            color:var(--acc);font-family:var(--fnt-mono);font-size:.78rem;padding:6px 10px;\u001b[39m\n\u001b[31m            border-radius:var(--r-sm);outline:none;flex:1;\\\">\u001b[39m\n\u001b[31m            <option value=\\\"-1\\\">— Docket not in file —</option>\u001b[39m\n\u001b[31m            ${_rawHeaders.map((h,i)=>`<option value=\\\"${i}\\\" ${i===(suggested.docket??-1)?'selected':''}>${h||'(col '+(i+1)+')'}</option>`).join('')}\u001b[39m\n\u001b[31m          </select>\u001b[39m\n\u001b[31m          <span id=\\\"docketPrev\\\" style=\\\"font-family:var(--fnt-mono);font-size:.72rem;color:var(--acc);\\\">\u001b[39m\n\u001b[31m            ${suggested.docket >= 0 && _rawRows[0] ? (_rawRows[0][suggested.docket]||'') : ''}\u001b[39m\n\u001b[31m          </span>\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m      </div>·\u001b[39m\n\u001b[31m      <div class=\\\"btn-row\\\" style=\\\"margin-top:12px;\\\">\u001b[39m\n\u001b[31m        <button class=\\\"btn btn-ghost\\\" style=\\\"font-size:.78rem;\\\" onclick=\\\"clearStores()\\\">✕ Cancel</button>\u001b[39m\n\u001b[31m        <button class=\\\"btn btn-orange\\\" style=\\\"font-size:.78rem;\\\" onclick=\\\"applyMapping()\\\">✓ Apply Mapping</button>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m  `;\u001b[39m\n\u001b[31m  document.getElementById('storeTable').innerHTML = '';\u001b[39m\n\u001b[31m  document.getElementById('storeAddPanel').style.display = 'none';·\u001b[39m\n\u001b[31m  // Update docket preview on change\u001b[39m\n\u001b[31m  const docketSel = document.getElementById('docketColSel');\u001b[39m\n\u001b[31m  if (docketSel) docketSel.addEventListener('change', () => {\u001b[39m\n\u001b[31m    const idx = parseInt(docketSel.value);\u001b[39m\n\u001b[31m    const prev = document.getElementById('docketPrev');\u001b[39m\n\u001b[31m    if (prev) prev.textContent = idx >= 0 && _rawRows[0] ? (_rawRows[0][idx]||'') : '';\u001b[39m\n\u001b[31m  });·\u001b[39m\n\u001b[31m  // Toggle manual map opacity when AI checkbox changes\u001b[39m\n\u001b[31m  const aiChk = document.getElementById('useAIParse');\u001b[39m\n\u001b[31m  const manSec = document.getElementById('manualMapSection');\u001b[39m\n\u001b[31m  if (aiChk && manSec) {\u001b[39m\n\u001b[31m    aiChk.addEventListener('change', () => {\u001b[39m\n\u001b[31m      manSec.style.display = aiChk.checked ? 'none' : 'block';\u001b[39m\n\u001b[31m    });\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction updateMappingPreview(key) {\u001b[39m\n\u001b[31m  const sel = document.getElementById('mapsel_' + key);\u001b[39m\n\u001b[31m  if (!sel) return;\u001b[39m\n\u001b[31m  const idx = parseInt(sel.value);\u001b[39m\n\u001b[31m  const prev = document.getElementById('mapprev_' + key);\u001b[39m\n\u001b[31m  if (prev) prev.textContent = (idx >= 0 && _rawRows[0]) ? ((_rawRows[0][idx]||'').toString().slice(0,40)) : '—';\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// ── REGEX-FIRST ADDRESS PARSER ───────────────────────────────────────────\u001b[39m\n\u001b[31m// Improved Indian address parser\u001b[39m\n\u001b[31m// Handles formats seen in real BTL dispatch files\u001b[39m\n\u001b[31mfunction parseAddressRegex(raw) {\u001b[39m\n\u001b[31m  const text = raw.toString().replace(/\\\\r/g, '').trim();\u001b[39m\n\u001b[31m  const lines = text.split('\\\\n').map(l => l.trim()).filter(Boolean);\u001b[39m\n\u001b[31m  const result = { name:'', address:'', city:'', pincode:'', phone:'', confidence:0 };·\u001b[39m\n\u001b[31m  // ── PHONE: Indian mobile 6-9 prefix, 10 digits ──────────────────────────\u001b[39m\n\u001b[31m  // Also handles \\\"Phone : 9876543210\\\", \\\"M.No.-9876543210\\\", \\\"Mobile - 9876543210\\\"\u001b[39m\n\u001b[31m  const phoneMatch = text.match(/(?:Phone|Mobile|Mob|M\\\\.No\\\\.?|Ph\\\\.?)?\\\\s*[:\\\\-]?\\\\s*\\\\b([6-9]\\\\d{9})\\\\b/i);\u001b[39m\n\u001b[31m  if (phoneMatch) result.phone = phoneMatch[1];·\u001b[39m\n\u001b[31m  // ── PINCODE: 6-digit standalone number ──────────────────────────────────\u001b[39m\n\u001b[31m  // Must not be part of a longer number (e.g. not an account number)\u001b[39m\n\u001b[31m  const pinMatch = text.match(/(?<![\\\\d])([1-9]\\\\d{5})(?![\\\\d])/);\u001b[39m\n\u001b[31m  if (pinMatch) result.pincode = pinMatch[1];·\u001b[39m\n\u001b[31m  // ── CITY: the token immediately before the pincode ───────────────────────\u001b[39m\n\u001b[31m  const indianStates = ['rajasthan','gujarat','maharashtra','punjab','haryana',\u001b[39m\n\u001b[31m    'uttar pradesh','madhya pradesh','karnataka','tamil nadu','andhra pradesh',\u001b[39m\n\u001b[31m    'telangana','kerala','west bengal','bihar','odisha','jharkhand','assam',\u001b[39m\n\u001b[31m    'himachal pradesh','uttarakhand','chhattisgarh','goa','delhi','chandigarh',\u001b[39m\n\u001b[31m    'raj.','raj'];\u001b[39m\n\u001b[31m  if (result.pincode) {\u001b[39m\n\u001b[31m    const pinPos = text.indexOf(result.pincode);\u001b[39m\n\u001b[31m    const beforePin = text.slice(0, pinPos);\u001b[39m\n\u001b[31m    // Strategy 1: split by comma/newline, walk backwards skipping state names\u001b[39m\n\u001b[31m    const chunks = beforePin.split(/[,\\\\n]/).map(c => c.trim()).filter(Boolean);\u001b[39m\n\u001b[31m    for (let i = chunks.length - 1; i >= 0; i--) {\u001b[39m\n\u001b[31m      const chunk = chunks[i].replace(/\\\\d+/g,'').replace(/[()]/g,'').replace(/[.-]/g,'').trim();\u001b[39m\n\u001b[31m      const lower = chunk.toLowerCase();\u001b[39m\n\u001b[31m      if (!chunk || chunk.length < 2) continue;\u001b[39m\n\u001b[31m      if (indianStates.includes(lower)) continue;\u001b[39m\n\u001b[31m      // Skip chunks that look like addresses (contain numbers or long strings)\u001b[39m\n\u001b[31m      if (/\\\\d/.test(chunk)) continue;\u001b[39m\n\u001b[31m      result.city = chunk;\u001b[39m\n\u001b[31m      break;\u001b[39m\n\u001b[31m    }\u001b[39m\n\u001b[31m    // Strategy 2: if still no city, look for last capitalised word before pincode\u001b[39m\n\u001b[31m    // handles \\\"compound paota Jodhpur 342006\\\" → Jodhpur\u001b[39m\n\u001b[31m    if (!result.city) {\u001b[39m\n\u001b[31m      const wordsBefore = beforePin.replace(/[,\\\\n]/g,' ').trim().split(/\\\\s+/);\u001b[39m\n\u001b[31m      for (let i = wordsBefore.length - 1; i >= 0; i--) {\u001b[39m\n\u001b[31m        const w = wordsBefore[i].replace(/[^a-zA-Z]/g,'');\u001b[39m\n\u001b[31m        if (w.length < 3) continue;\u001b[39m\n\u001b[31m        if (indianStates.includes(w.toLowerCase())) continue;\u001b[39m\n\u001b[31m        // Prefer words that start with capital letter (likely a proper noun/city)\u001b[39m\n\u001b[31m        if (/^[A-Z]/.test(w)) { result.city = w; break; }\u001b[39m\n\u001b[31m      }\u001b[39m\n\u001b[31m    }\u001b[39m\n\u001b[31m  }·\u001b[39m\n\u001b[31m  // ── NAME: first meaningful line, extract shop name ───────────────────────\u001b[39m\n\u001b[31m  // Patterns:\u001b[39m\n\u001b[31m  //   \\\"Sanwar Mal Saini, C/o R G COMPUTERS\\\" → name = \\\"R G COMPUTERS\\\"\u001b[39m\n\u001b[31m  //   \\\"SANWAR MAL SAINI, C/o ADVANCE TECHNOLOGIES\\\" → name = \\\"ADVANCE TECHNOLOGIES\\\"\u001b[39m\n\u001b[31m  //   \\\"Sunil Vaishnav\\\" → name = \\\"Sunil Vaishnav\\\" (no C/o)\u001b[39m\n\u001b[31m  //   \\\"Baltej Singh, Professional computer care\\\" → name = \\\"Professional computer care\\\"\u001b[39m\n\u001b[31m  if (lines.length > 0) {\u001b[39m\n\u001b[31m    const firstLine = lines[0];\u001b[39m\n\u001b[31m    // Check for C/o or C/O pattern — shop name comes after it\u001b[39m\n\u001b[31m    const coMatch = firstLine.match(/C\\\\/?[Oo]\\\\.?\\\\s+(.+)/);\u001b[39m\n\u001b[31m    if (coMatch) {\u001b[39m\n\u001b[31m      result.name = coMatch[1].trim();\u001b[39m\n\u001b[31m    } else {\u001b[39m\n\u001b[31m      // Check if there is a comma — second part might be the shop name\u001b[39m\n\u001b[31m      const parts = firstLine.split(',').map(p => p.trim());\u001b[39m\n\u001b[31m      if (parts.length >= 2 && parts[1].length > 3) {\u001b[39m\n\u001b[31m        // Heuristic: if second part looks like a business name (has uppercase or >5 chars)\u001b[39m\n\u001b[31m        const hasUpperOrLong = /[A-Z]{2,}/.test(parts[1]) || parts[1].length > 8;\u001b[39m\n\u001b[31m        result.name = hasUpperOrLong ? parts[1] : parts[0];\u001b[39m\n\u001b[31m      } else {\u001b[39m\n\u001b[31m        result.name = parts[0];\u001b[39m\n\u001b[31m      }\u001b[39m\n\u001b[31m    }\u001b[39m\n\u001b[31m  }·\u001b[39m\n\u001b[31m  // ── ADDRESS: lines between name and phone/pincode line ───────────────────\u001b[39m\n\u001b[31m  const phoneLineIdx = lines.findIndex(l => result.phone && l.includes(result.phone));\u001b[39m\n\u001b[31m  const addrLines = lines.filter((l, i) => {\u001b[39m\n\u001b[31m    if (i === 0) return false;\u001b[39m\n\u001b[31m    if (i === phoneLineIdx) return false;\u001b[39m\n\u001b[31m    // Exclude lines that are just the phone number\u001b[39m\n\u001b[31m    if (/^[6-9]\\\\d{9}$/.test(l.replace(/\\\\s/g,''))) return false;\u001b[39m\n\u001b[31m    return true;\u001b[39m\n\u001b[31m  });\u001b[39m\n\u001b[31m  result.address = addrLines.join(', ').replace(/,\\\\s*,/g, ',').trim();·\u001b[39m\n\u001b[31m  // ── CONFIDENCE: simple score based on what we got ────────────────────────\u001b[39m\n\u001b[31m  let conf = 0;\u001b[39m\n\u001b[31m  if (result.name)    conf += 30;\u001b[39m\n\u001b[31m  if (result.city)    conf += 25;\u001b[39m\n\u001b[31m  if (result.pincode) conf += 25;\u001b[39m\n\u001b[31m  if (result.phone)   conf += 20;\u001b[39m\n\u001b[31m  result.confidence = conf;·\u001b[39m\n\u001b[31m  return result;\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// ── AI ADDRESS PARSER (Ollama fallback) ──────────────────────────────────\u001b[39m\n\u001b[31m// Fetches past corrections from localStorage to improve prompt accuracy\u001b[39m\n\u001b[31mfunction getPastCorrections() {\u001b[39m\n\u001b[31m  try {\u001b[39m\n\u001b[31m    const log = JSON.parse(localStorage.getItem('360lm_parse_corrections') || '[]');\u001b[39m\n\u001b[31m    return log.slice(-10); // last 10 corrections as examples\u001b[39m\n\u001b[31m  } catch(e) { return []; }\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction saveCorrectionLocal(entry) {\u001b[39m\n\u001b[31m  try {\u001b[39m\n\u001b[31m    const log = JSON.parse(localStorage.getItem('360lm_parse_corrections') || '[]');\u001b[39m\n\u001b[31m    log.push(entry);\u001b[39m\n\u001b[31m    // Keep only last 50\u001b[39m\n\u001b[31m    localStorage.setItem('360lm_parse_corrections', JSON.stringify(log.slice(-50)));\u001b[39m\n\u001b[31m  } catch(e) {}\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31masync function parseAddressWithOllama(raw) {\u001b[39m\n\u001b[31m  // Calls the server-side /dispatch-ai/address proxy (avoids CORS + port 11434 issues)\u001b[39m\n\u001b[31m  try {\u001b[39m\n\u001b[31m    const resp = await fetch('/dispatch-ai/address', {\u001b[39m\n\u001b[31m      method: 'POST',\u001b[39m\n\u001b[31m      headers: { 'Content-Type': 'application/json' },\u001b[39m\n\u001b[31m      body: JSON.stringify({ text: raw }),\u001b[39m\n\u001b[31m      signal: AbortSignal.timeout(120000)\u001b[39m\n\u001b[31m    });\u001b[39m\n\u001b[31m    const data = await resp.json();\u001b[39m\n\u001b[31m    if (data.success) return data;\u001b[39m\n\u001b[31m  } catch(e) {}\u001b[39m\n\u001b[31m  return null;\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31masync function parseAddressCell(raw) {\u001b[39m\n\u001b[31m  // Step 1: regex — always runs first, no network call\u001b[39m\n\u001b[31m  const regex = parseAddressRegex(raw);\u001b[39m\n\u001b[31m  if (regex.name) return { ...regex, source: 'regex' };·\u001b[39m\n\u001b[31m  // Step 2: Ollama only if regex got nothing AND Ollama is configured\u001b[39m\n\u001b[31m  // Uses a 3-second timeout to prevent browser hang\u001b[39m\n\u001b[31m  if (window.appConfig.ocr_mode !== 'transformers') {\u001b[39m\n\u001b[31m    try {\u001b[39m\n\u001b[31m      const ai = await Promise.race([\u001b[39m\n\u001b[31m        parseAddressWithOllama(raw),\u001b[39m\n\u001b[31m        new Promise((_, reject) => setTimeout(() => reject(new Error('timeout')), 120000))\u001b[39m\n\u001b[31m      ]);\u001b[39m\n\u001b[31m      if (ai && ai.name) return { ...ai, source: 'ollama' };\u001b[39m\n\u001b[31m    } catch(e) { /* timeout or error — continue to fallback */ }\u001b[39m\n\u001b[31m  }·\u001b[39m\n\u001b[31m  // Step 3: fallback — first line as name\u001b[39m\n\u001b[31m  const lines = raw.toString().split('\\\\n').map(l=>l.trim()).filter(Boolean);\u001b[39m\n\u001b[31m  return { name: lines[0]||raw.slice(0,40), address: raw, city:'', pincode:'', phone:'', source:'fallback' };\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31masync function applyMapping() {\u001b[39m\n\u001b[31m  const useAI   = document.getElementById('useAIParse')?.checked ?? false;\u001b[39m\n\u001b[31m  const comboSel = document.getElementById('comboColSel');\u001b[39m\n\u001b[31m  const comboIdx = comboSel ? parseInt(comboSel.value) : -1;\u001b[39m\n\u001b[31m  const docketIdx = parseInt(document.getElementById('docketColSel')?.value ?? -1);·\u001b[39m\n\u001b[31m  // Build manual mapping as fallback\u001b[39m\n\u001b[31m  const mapping = {};\u001b[39m\n\u001b[31m  MAPPING_FIELDS.forEach(f => {\u001b[39m\n\u001b[31m    const sel = document.getElementById('mapsel_' + f.key);\u001b[39m\n\u001b[31m    mapping[f.key] = sel ? parseInt(sel.value) : -1;\u001b[39m\n\u001b[31m  });\u001b[39m\n\u001b[31m  // Override docket from dedicated selector\u001b[39m\n\u001b[31m  mapping.docket = docketIdx;·\u001b[39m\n\u001b[31m  // Determine if we have a valid base for stores\u001b[39m\n\u001b[31m  const nameIdx = useAI && comboIdx >= 0 ? comboIdx : mapping.name;\u001b[39m\n\u001b[31m  if (nameIdx === -1 && !useAI) { showToast('Store Name column is required', 'err'); return; }·\u001b[39m\n\u001b[31m  // Save mapping\u001b[39m\n\u001b[31m  try { localStorage.setItem(getMappingKey(), JSON.stringify({...mapping, _useAI: useAI, _comboIdx: comboIdx})); } catch(e) {}·\u001b[39m\n\u001b[31m  if (useAI && comboIdx >= 0) {\u001b[39m\n\u001b[31m    // AI path — parse each combined cell\u001b[39m\n\u001b[31m    showToast('AI parsing addresses...', 'info');\u001b[39m\n\u001b[31m    showOCRWorking(true, `Parsing 0/${_rawRows.length} addresses...`);\u001b[39m\n\u001b[31m    stores = [];\u001b[39m\n\u001b[31m    for (let i = 0; i < _rawRows.length; i++) {\u001b[39m\n\u001b[31m      const r = _rawRows[i];\u001b[39m\n\u001b[31m      const raw = (r[comboIdx] || '').toString().trim();\u001b[39m\n\u001b[31m      if (!raw) continue;\u001b[39m\n\u001b[31m      showOCRWorking(true, `Parsing ${i+1}/${_rawRows.length} — ${raw.slice(0,30)}...`);\u001b[39m\n\u001b[31m      // Yield to browser every row to prevent UI freeze\u001b[39m\n\u001b[31m      await new Promise(r => setTimeout(r, 0));\u001b[39m\n\u001b[31m      const parsed = await parseAddressCell(raw);\u001b[39m\n\u001b[31m      stores.push({\u001b[39m\n\u001b[31m        id: i,\u001b[39m\n\u001b[31m        name:    parsed.name    || raw.slice(0,40),\u001b[39m\n\u001b[31m        city:    parsed.city    || '',\u001b[39m\n\u001b[31m        phone:   parsed.phone   || '',\u001b[39m\n\u001b[31m        address: parsed.address || raw,\u001b[39m\n\u001b[31m        pincode: parsed.pincode || '',\u001b[39m\n\u001b[31m        docket:  docketIdx >= 0 ? (r[docketIdx]||'').toString().trim() : '',\u001b[39m\n\u001b[31m        _parseSource: parsed.source,\u001b[39m\n\u001b[31m        _confidence:  parsed.confidence || 0,\u001b[39m\n\u001b[31m        _rawCell:     raw,\u001b[39m\n\u001b[31m        _origName:    parsed.name || '',\u001b[39m\n\u001b[31m        _origCity:    parsed.city || '',\u001b[39m\n\u001b[31m        matched: false\u001b[39m\n\u001b[31m      });\u001b[39m\n\u001b[31m    }\u001b[39m\n\u001b[31m    showOCRWorking(false);\u001b[39m\n\u001b[31m  } else {\u001b[39m\n\u001b[31m    // Manual mapping path\u001b[39m\n\u001b[31m    stores = _rawRows\u001b[39m\n\u001b[31m      .filter(r => r[nameIdx] && (r[nameIdx]+'').trim())\u001b[39m\n\u001b[31m      .map((r, i) => ({\u001b[39m\n\u001b[31m        id: i,\u001b[39m\n\u001b[31m        name:    mapping.name    >= 0 ? (r[mapping.name]   ||'').toString().trim() : '',\u001b[39m\n\u001b[31m        city:    mapping.city    >= 0 ? (r[mapping.city]   ||'').toString().trim() : '',\u001b[39m\n\u001b[31m        phone:   mapping.phone   >= 0 ? (r[mapping.phone]  ||'').toString().trim() : '',\u001b[39m\n\u001b[31m        docket:  mapping.docket  >= 0 ? (r[mapping.docket] ||'').toString().trim() : '',\u001b[39m\n\u001b[31m        address: mapping.address >= 0 ? (r[mapping.address]||'').toString().trim() : '',\u001b[39m\n\u001b[31m        pincode: mapping.pincode >= 0 ? (r[mapping.pincode]||'').toString().trim() : '',\u001b[39m\n\u001b[31m        matched: false\u001b[39m\n\u001b[31m      }));\u001b[39m\n\u001b[31m  }·\u001b[39m\n\u001b[31m  document.getElementById('storeCount').textContent = `${stores.length} stores loaded`;·\u001b[39m\n\u001b[31m  if (useAI && comboIdx >= 0) {\u001b[39m\n\u001b[31m    // AI path — show row-by-row review panel before adding consignees\u001b[39m\n\u001b[31m    showAddressReviewPanel();\u001b[39m\n\u001b[31m  } else {\u001b[39m\n\u001b[31m    // Manual mapping path — go straight to store table + add panel\u001b[39m\n\u001b[31m    renderStoreTable(mapping.docket >= 0 || docketIdx >= 0);\u001b[39m\n\u001b[31m    const addPanel = document.getElementById('storeAddPanel');\u001b[39m\n\u001b[31m    const addBtn   = document.getElementById('storeAddBtn');\u001b[39m\n\u001b[31m    const addTitle = document.getElementById('storeAddTitle');\u001b[39m\n\u001b[31m    const hasDockets = stores.some(s => s.docket);\u001b[39m\n\u001b[31m    addTitle.textContent = hasDockets\u001b[39m\n\u001b[31m      ? `${stores.filter(s=>s.docket).length} stores with dockets — add as consignees?`\u001b[39m\n\u001b[31m      : `Add ${stores.length} stores as consignees?`;\u001b[39m\n\u001b[31m    addBtn.textContent = `✓ Add ${stores.length} as Consignees`;\u001b[39m\n\u001b[31m    addPanel.style.display = 'block';\u001b[39m\n\u001b[31m    showToast(`${stores.length} stores mapped ✓`, 'ok');\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31m// ADDRESS REVIEW PANEL\u001b[39m\n\u001b[31m// Row-by-row review of AI-parsed addresses before consignees created\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31mfunction showAddressReviewPanel() {\u001b[39m\n\u001b[31m  const pane = document.getElementById('addrReviewPane');\u001b[39m\n\u001b[31m  if (!pane) return;\u001b[39m\n\u001b[31m  renderAddressReviewRows();\u001b[39m\n\u001b[31m  pane.style.display = 'block';\u001b[39m\n\u001b[31m  pane.scrollTop = 0;\u001b[39m\n\u001b[31m  showToast(`Review ${stores.length} parsed addresses`, 'info');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction renderAddressReviewRows() {\u001b[39m\n\u001b[31m  const list = document.getElementById('addrReviewList');\u001b[39m\n\u001b[31m  const summary = document.getElementById('addrReviewSummary');\u001b[39m\n\u001b[31m  if (!list) return;·\u001b[39m\n\u001b[31m  const highConf  = stores.filter(s => (s._confidence||0) >= 80).length;\u001b[39m\n\u001b[31m  const medConf   = stores.filter(s => (s._confidence||0) >= 50 && (s._confidence||0) < 80).length;\u001b[39m\n\u001b[31m  const lowConf   = stores.filter(s => (s._confidence||0) < 50).length;·\u001b[39m\n\u001b[31m  summary.innerHTML = `\u001b[39m\n\u001b[31m    <div class=\\\"vs-item\\\"><div class=\\\"vs-val\\\" style=\\\"color:var(--t1);\\\">${stores.length}</div><div class=\\\"vs-lbl\\\">Total</div></div>\u001b[39m\n\u001b[31m    <div class=\\\"vs-item\\\"><div class=\\\"vs-val\\\" style=\\\"color:var(--grn);\\\">${highConf}</div><div class=\\\"vs-lbl\\\">High conf</div></div>\u001b[39m\n\u001b[31m    <div class=\\\"vs-item\\\"><div class=\\\"vs-val\\\" style=\\\"color:var(--ylw);\\\">${medConf}</div><div class=\\\"vs-lbl\\\">Review</div></div>\u001b[39m\n\u001b[31m    <div class=\\\"vs-item\\\"><div class=\\\"vs-val\\\" style=\\\"color:var(--red);\\\">${lowConf}</div><div class=\\\"vs-lbl\\\">Check</div></div>\u001b[39m\n\u001b[31m    <div class=\\\"vs-item\\\" style=\\\"margin-left:auto;\\\">\u001b[39m\n\u001b[31m      <button class=\\\"btn-sm primary\\\" onclick=\\\"approveAllAddresses()\\\" style=\\\"font-size:.7rem;\\\">✓ Approve All</button>\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m  `;·\u001b[39m\n\u001b[31m  list.innerHTML = stores.map((s, i) => {\u001b[39m\n\u001b[31m    const conf = s._confidence || 0;\u001b[39m\n\u001b[31m    const confClass = conf >= 80 ? 'vf-ok' : conf >= 50 ? 'vf-nodocket' : 'vf-dupe';\u001b[39m\n\u001b[31m    const confLabel = conf >= 80 ? `${conf}% ✓` : conf >= 50 ? `${conf}% review` : `${conf}% check`;\u001b[39m\n\u001b[31m    const rowBorder = conf >= 80 ? 'var(--grn)' : conf >= 50 ? 'var(--ylw)' : 'var(--red)';·\u001b[39m\n\u001b[31m    return `\u001b[39m\n\u001b[31m    <div class=\\\"verify-row\\\" id=\\\"arow_${i}\\\" style=\\\"border-left:3px solid ${rowBorder};\\\">\u001b[39m\n\u001b[31m      <div class=\\\"verify-row-hdr\\\">\u001b[39m\n\u001b[31m        <span class=\\\"verify-num\\\">#${i+1}</span>\u001b[39m\n\u001b[31m        <span class=\\\"verify-store\\\" style=\\\"font-size:.78rem;color:var(--t2);font-weight:400;\u001b[39m\n\u001b[31m          font-family:var(--fnt-mono);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;flex:1;\\\"\u001b[39m\n\u001b[31m          title=\\\"${(s._rawCell||'').replace(/\\\"/g,\\\"'\\\")}\\\">\u001b[39m\n\u001b[31m          ${(s._rawCell||'').slice(0,50).split('\\\\n').join(' ↵ ')}       <span class=\\\"verify-flag ${confClass}\\\" style=\\\"flex-shrink:0;margin-right:6px;\\\">${confLabel} · ${s._parseSource||'?'}</span>\u001b[39m\n\u001b[31m        <button onclick=\\\"deleteReviewRow(${i})\\\"\u001b[39m\n\u001b[31m          style=\\\"background:none;border:1px solid rgba(239,68,68,.4);color:var(--red);\u001b[39m\n\u001b[31m          font-size:.65rem;padding:2px 8px;border-radius:6px;cursor:pointer;\u001b[39m\n\u001b[31m          font-family:var(--fnt-mono);flex-shrink:0;transition:background .2s;\\\"\u001b[39m\n\u001b[31m          onmouseover=\\\"this.style.background='rgba(239,68,68,.15)'\\\"\u001b[39m\n\u001b[31m          onmouseout=\\\"this.style.background='none'\\\">✕ Remove</button>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      <div class=\\\"verify-fields\\\" style=\\\"grid-template-columns:1fr 1fr 1fr 1fr 1fr;\\\">\u001b[39m\n\u001b[31m        <div class=\\\"verify-field\\\">\u001b[39m\n\u001b[31m          <label>Shop / Name <span style=\\\"color:var(--acc);\\\">*</span></label>\u001b[39m\n\u001b[31m          <input type=\\\"text\\\" value=\\\"${s.name||''}\\\" id=\\\"ar_name_${i}\\\"\u001b[39m\n\u001b[31m            oninput=\\\"updateAddrField(${i},'name',this)\\\"\u001b[39m\n\u001b[31m            style=\\\"${!s.name ? 'border-color:var(--red);' : ''}\\\">\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m        <div class=\\\"verify-field\\\">\u001b[39m\n\u001b[31m          <label>City</label>\u001b[39m\n\u001b[31m          <input type=\\\"text\\\" value=\\\"${s.city||''}\\\" id=\\\"ar_city_${i}\\\"\u001b[39m\n\u001b[31m            oninput=\\\"updateAddrField(${i},'city',this)\\\"\u001b[39m\n\u001b[31m            style=\\\"${!s.city ? 'border-color:var(--ylw);' : ''}\\\">\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m        <div class=\\\"verify-field\\\">\u001b[39m\n\u001b[31m          <label>Docket / Courier No.</label>\u001b[39m\n\u001b[31m          <input type=\\\"text\\\" value=\\\"${s.docket||''}\\\" id=\\\"ar_docket_${i}\\\"\u001b[39m\n\u001b[31m            oninput=\\\"updateAddrField(${i},'docket',this)\\\"\u001b[39m\n\u001b[31m            style=\\\"font-family:var(--fnt-mono);${!s.docket?'border-color:var(--ylw);':'color:var(--acc);'}\\\">\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m        <div class=\\\"verify-field\\\">\u001b[39m\n\u001b[31m          <label>Phone</label>\u001b[39m\n\u001b[31m          <input type=\\\"tel\\\" value=\\\"${s.phone||''}\\\" id=\\\"ar_phone_${i}\\\"\u001b[39m\n\u001b[31m            oninput=\\\"updateAddrField(${i},'phone',this)\\\">\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m        <div class=\\\"verify-field\\\">\u001b[39m\n\u001b[31m          <label>Pincode</label>\u001b[39m\n\u001b[31m          <input type=\\\"text\\\" value=\\\"${s.pincode||''}\\\" id=\\\"ar_pin_${i}\\\"\u001b[39m\n\u001b[31m            oninput=\\\"updateAddrField(${i},'pincode',this)\\\">\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      <div style=\\\"font-size:.65rem;color:var(--t3);font-family:var(--fnt-mono);\u001b[39m\n\u001b[31m        margin-top:4px;padding:4px 6px;background:var(--bg);border-radius:4px;word-break:break-all;\\\">\u001b[39m\n\u001b[31m        📋 ${(s._rawCell||'').split('\\\\n').join(' | ')}\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m    </div>`;\u001b[39m\n\u001b[31m  }).join('');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction updateAddrField(i, field, input) {\u001b[39m\n\u001b[31m  if (!stores[i]) return;\u001b[39m\n\u001b[31m  stores[i][field] = input.value.trim();\u001b[39m\n\u001b[31m  // Recompute confidence\u001b[39m\n\u001b[31m  let conf = 0;\u001b[39m\n\u001b[31m  if (stores[i].name)    conf += 30;\u001b[39m\n\u001b[31m  if (stores[i].city)    conf += 25;\u001b[39m\n\u001b[31m  if (stores[i].pincode) conf += 25;\u001b[39m\n\u001b[31m  if (stores[i].phone)   conf += 20;\u001b[39m\n\u001b[31m  stores[i]._confidence = conf;\u001b[39m\n\u001b[31m  input.classList.toggle('changed', true);\u001b[39m\n\u001b[31m  // Update border color on row\u001b[39m\n\u001b[31m  const row = document.getElementById('arow_' + i);\u001b[39m\n\u001b[31m  if (row) row.style.borderLeftColor = conf >= 80 ? 'var(--grn)' : conf >= 50 ? 'var(--ylw)' : 'var(--red)';\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction deleteReviewRow(i) {\u001b[39m\n\u001b[31m  // Mark as deleted — splice from stores array and re-render\u001b[39m\n\u001b[31m  stores.splice(i, 1);\u001b[39m\n\u001b[31m  // Re-assign IDs\u001b[39m\n\u001b[31m  stores.forEach((s, idx) => { s.id = idx; });\u001b[39m\n\u001b[31m  renderAddressReviewRows();\u001b[39m\n\u001b[31m  document.getElementById('storeCount').textContent = `${stores.length} stores loaded`;\u001b[39m\n\u001b[31m  showToast(`Row removed — ${stores.length} remaining`, 'info');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction backToMapping() {\u001b[39m\n\u001b[31m  // Hide review pane — store edits are preserved in stores[] array\u001b[39m\n\u001b[31m  document.getElementById('addrReviewPane').style.display = 'none';\u001b[39m\n\u001b[31m  // Show mapping UI again with current file\u001b[39m\n\u001b[31m  document.getElementById('storeTableArea').style.display = 'block';\u001b[39m\n\u001b[31m  document.getElementById('storeColInfo').style.display = 'block';\u001b[39m\n\u001b[31m  showToast('Your edits are preserved — re-apply mapping to reload', 'info');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction approveAllAddresses() {\u001b[39m\n\u001b[31m  stores.forEach((s, i) => { s._reviewed = true; });\u001b[39m\n\u001b[31m  document.getElementById('addrReviewPane').style.display = 'none';\u001b[39m\n\u001b[31m  applyReviewedStores();\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction confirmAddressReview() {\u001b[39m\n\u001b[31m  // Collect any pending edits, log corrections, close panel\u001b[39m\n\u001b[31m  const corrections = [];\u001b[39m\n\u001b[31m  stores.forEach((s, i) => {\u001b[39m\n\u001b[31m    const origName = s._origName || '';\u001b[39m\n\u001b[31m    const origCity = s._origCity || '';\u001b[39m\n\u001b[31m    if ((origName && origName !== s.name) || (origCity && origCity !== s.city)) {\u001b[39m\n\u001b[31m      corrections.push({ raw: s._rawCell||'', name: s.name, city: s.city, pincode: s.pincode });\u001b[39m\n\u001b[31m    }\u001b[39m\n\u001b[31m  });\u001b[39m\n\u001b[31m  // Save corrections locally for future Ollama prompts\u001b[39m\n\u001b[31m  corrections.forEach(c => saveCorrectionLocal(c));\u001b[39m\n\u001b[31m  // Log to Sheets asynchronously (fire and forget)\u001b[39m\n\u001b[31m  if (corrections.length > 0 && window.appConfig.webhook_url) {\u001b[39m\n\u001b[31m    logParseCorrectionsToSheets(corrections);\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m  document.getElementById('addrReviewPane').style.display = 'none';\u001b[39m\n\u001b[31m  applyReviewedStores();\u001b[39m\n\u001b[31m  showToast(`${corrections.length > 0 ? corrections.length + ' corrections logged · ' : ''}${stores.length} stores ready ✓`, 'ok');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// Clean street address: remove city, pincode, phone if they appear in address field\u001b[39m\n\u001b[31mfunction cleanAddress(addr, city, pincode, phone) {\u001b[39m\n\u001b[31m  if (!addr) return addr;\u001b[39m\n\u001b[31m  let s = addr;\u001b[39m\n\u001b[31m  // Remove phone\u001b[39m\n\u001b[31m  if (phone) s = s.replace(new RegExp(phone.replace(/[.*+?^${}()|[\\\\]\\\\\\\\]/g,'\\\\\\\\$&'), 'g'), '').trim();\u001b[39m\n\u001b[31m  // Remove pincode\u001b[39m\n\u001b[31m  if (pincode) s = s.replace(new RegExp('\\\\\\\\b' + pincode + '\\\\\\\\b', 'g'), '').trim();\u001b[39m\n\u001b[31m  // Remove city (case-insensitive)\u001b[39m\n\u001b[31m  if (city) s = s.replace(new RegExp('\\\\\\\\b' + city.replace(/[.*+?^${}()|[\\\\]\\\\\\\\]/g,'\\\\\\\\$&') + '\\\\\\\\b', 'ig'), '').trim();\u001b[39m\n\u001b[31m  // Remove state names and trailing noise\u001b[39m\n\u001b[31m  const noise = /\\\\b(rajasthan|gujarat|maharashtra|punjab|haryana|uttar pradesh|delhi|raj\\\\.)\\\\b/ig;\u001b[39m\n\u001b[31m  s = s.replace(noise, '');\u001b[39m\n\u001b[31m  // Remove \\\"Mobile :\\\", \\\"Phone :\\\", \\\"M.No.\\\" prefixes and dangling numbers\u001b[39m\n\u001b[31m  s = s.replace(/(?:Mobile|Phone|Mob|M\\\\.No\\\\.?|Ph\\\\.?)\\\\s*[:\\\\-]?\\\\s*/ig, '');\u001b[39m\n\u001b[31m  // Clean up multiple commas/spaces\u001b[39m\n\u001b[31m  s = s.replace(/,\\\\s*,/g, ',').replace(/\\\\s{2,}/g, ' ').replace(/^[\\\\s,]+|[\\\\s,]+$/g, '');\u001b[39m\n\u001b[31m  return s;\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction applyReviewedStores() {\u001b[39m\n\u001b[31m  // Clean address fields before loading stores\u001b[39m\n\u001b[31m  stores.forEach(s => {\u001b[39m\n\u001b[31m    s.address = cleanAddress(s.address, s.city, s.pincode, s.phone);\u001b[39m\n\u001b[31m  });\u001b[39m\n\u001b[31m  renderStoreTable(stores.some(s => s.docket));\u001b[39m\n\u001b[31m  const addPanel = document.getElementById('storeAddPanel');\u001b[39m\n\u001b[31m  const addTitle = document.getElementById('storeAddTitle');\u001b[39m\n\u001b[31m  const addBtn   = document.getElementById('storeAddBtn');\u001b[39m\n\u001b[31m  const hasDockets = stores.some(s => s.docket);\u001b[39m\n\u001b[31m  addTitle.textContent = hasDockets\u001b[39m\n\u001b[31m    ? `${stores.filter(s=>s.docket).length} stores with dockets — add as consignees?`\u001b[39m\n\u001b[31m    : `Add ${stores.length} stores as consignees?`;\u001b[39m\n\u001b[31m  addBtn.textContent = `✓ Add ${stores.length} as Consignees`;\u001b[39m\n\u001b[31m  addPanel.style.display = 'block';\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31masync function logParseCorrectionsToSheets(corrections) {\u001b[39m\n\u001b[31m  const url = window.appConfig.webhook_url;\u001b[39m\n\u001b[31m  if (!url) return;\u001b[39m\n\u001b[31m  try {\u001b[39m\n\u001b[31m    await fetch(url, {\u001b[39m\n\u001b[31m      method: 'POST', mode: 'no-cors',\u001b[39m\n\u001b[31m      headers: { 'Content-Type': 'application/json' },\u001b[39m\n\u001b[31m      body: JSON.stringify({\u001b[39m\n\u001b[31m        type: 'parse_correction',\u001b[39m\n\u001b[31m        corrections,\u001b[39m\n\u001b[31m        colFingerprint: getMappingKey(),\u001b[39m\n\u001b[31m        timestamp: new Date().toISOString()\u001b[39m\n\u001b[31m      })\u001b[39m\n\u001b[31m    });\u001b[39m\n\u001b[31m  } catch(e) {}\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction addStoresToConsignees() {\u001b[39m\n\u001b[31m  const consList = document.getElementById('consList');\u001b[39m\n\u001b[31m  consList.innerHTML = '';\u001b[39m\n\u001b[31m  stores.slice(0, 50).forEach(s => addConsignee(s));\u001b[39m\n\u001b[31m  document.getElementById('storeAddPanel').style.display = 'none';\u001b[39m\n\u001b[31m  showToast(`${Math.min(stores.length, 50)} consignees added ✓`, 'ok');\u001b[39m\n\u001b[31m  if (stores.some(s => s.docket)) setTimeout(() => autoCreateDispatchFromStoreList(), 600);\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction promptAddFromStores() { /* replaced by inline panel */ }··\u001b[39m\n\u001b[31mfunction findCol(headers, keywords) {\u001b[39m\n\u001b[31m  for (let i=0;i<headers.length;i++) {\u001b[39m\n\u001b[31m    const h = (headers[i]||'').toString().toLowerCase();\u001b[39m\n\u001b[31m    if (keywords.some(k => h.includes(k))) return i;\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m  return -1;\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction renderStoreTable(hasDocket) {\u001b[39m\n\u001b[31m  const t = document.getElementById('storeTable');\u001b[39m\n\u001b[31m  const cols = ['#','Store Name','City','Phone'];\u001b[39m\n\u001b[31m  if (hasDocket) cols.push('Docket','Status');\u001b[39m\n\u001b[31m  t.innerHTML = `<thead><tr>${cols.map(c=>`<th>${c}</th>`).join('')}</tr></thead>\u001b[39m\n\u001b[31m  <tbody>${stores.map((s,i) => {\u001b[39m\n\u001b[31m    const statusClass = s.docket ? (s.status === 'delivered' ? 'matched' : 'unmatched') : '';\u001b[39m\n\u001b[31m    const docketCell = hasDocket ? `<td class=\\\"${statusClass}\\\" style=\\\"font-family:var(--fnt-mono);\\\">${s.docket||'—'}</td>` : '';\u001b[39m\n\u001b[31m    const statusCell = hasDocket ? `<td>${s.status ? `<span class=\\\"status-pill sp-${s.status}\\\">${s.status}</span>` : '—'}</td>` : '';\u001b[39m\n\u001b[31m    return `<tr>\u001b[39m\n\u001b[31m      <td>${i+1}</td>\u001b[39m\n\u001b[31m      <td>${s.name}</td>\u001b[39m\n\u001b[31m      <td>${s.city}</td>\u001b[39m\n\u001b[31m      <td style=\\\"font-family:var(--fnt-mono);\\\">${s.phone||'—'}</td>\u001b[39m\n\u001b[31m      ${docketCell}${statusCell}\u001b[39m\n\u001b[31m    </tr>`;\u001b[39m\n\u001b[31m  }).join('')}</tbody>`;\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction clearStores() {\u001b[39m\n\u001b[31m  stores = [];\u001b[39m\n\u001b[31m  document.getElementById('storeTableArea').style.display = 'none';\u001b[39m\n\u001b[31m  document.getElementById('storeFile').value = '';\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction autoCreateDispatchFromStoreList() {\u001b[39m\n\u001b[31m  // Create/update dispatch with all stores that have dockets\u001b[39m\n\u001b[31m  const courier = document.getElementById('n_courier').value || 'SkyKing';\u001b[39m\n\u001b[31m  const storesWithDockets = stores.filter(s => s.docket);\u001b[39m\n\u001b[31m  if (!storesWithDockets.length) return;·\u001b[39m\n\u001b[31m  // Clear and rebuild consignee list from store data\u001b[39m\n\u001b[31m  const consList = document.getElementById('consList');\u001b[39m\n\u001b[31m  consList.innerHTML = '';\u001b[39m\n\u001b[31m  storesWithDockets.forEach(s => {\u001b[39m\n\u001b[31m    addConsignee(s);\u001b[39m\n\u001b[31m  });\u001b[39m\n\u001b[31m  showPane('new');\u001b[39m\n\u001b[31m  showToast(`${storesWithDockets.length} consignees added from store list`, 'ok');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction promptAddFromStores() {\u001b[39m\n\u001b[31m  // Replaced by inline storeAddPanel UI — no-op kept for safety\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction autoMatchDockets() {\u001b[39m\n\u001b[31m  // Match OCR dockets to stores by order\u001b[39m\n\u001b[31m  if (!slipOCRData.length) { showToast('Upload docket slip images first', 'err'); return; }\u001b[39m\n\u001b[31m  let matched = 0;\u001b[39m\n\u001b[31m  slipOCRData.forEach((slip, i) => {\u001b[39m\n\u001b[31m    if (i < stores.length && slip.docket) {\u001b[39m\n\u001b[31m      stores[i].docket = slip.docket;\u001b[39m\n\u001b[31m      matched++;\u001b[39m\n\u001b[31m    }\u001b[39m\n\u001b[31m  });\u001b[39m\n\u001b[31m  renderStoreTable(true);\u001b[39m\n\u001b[31m  showToast(`${matched} dockets matched to stores ✓`, 'ok');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31m// CONSIGNEES\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31mfunction addConsignee(storeData = null) {\u001b[39m\n\u001b[31m  const list = document.getElementById('consList');\u001b[39m\n\u001b[31m  const idx = list.children.length;\u001b[39m\n\u001b[31m  const card = document.createElement('div');\u001b[39m\n\u001b[31m  card.className = 'cons-card';\u001b[39m\n\u001b[31m  card.dataset.idx = idx;\u001b[39m\n\u001b[31m  const s = storeData || {};\u001b[39m\n\u001b[31m  card.innerHTML = `\u001b[39m\n\u001b[31m    <div class=\\\"cons-hdr\\\">\u001b[39m\n\u001b[31m      <span class=\\\"cons-num\\\">CONSIGNEE #${idx+1}</span>\u001b[39m\n\u001b[31m      <button class=\\\"cons-del\\\" onclick=\\\"this.closest('.cons-card').remove();renumberCons()\\\">✕</button>\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m    <div class=\\\"row2\\\">\u001b[39m\n\u001b[31m      <div class=\\\"f\\\">\u001b[39m\n\u001b[31m        <label>Store / Name <span class=\\\"req\\\">*</span></label>\u001b[39m\n\u001b[31m        <input type=\\\"text\\\" class=\\\"cons-name\\\" placeholder=\\\"Store name\\\" value=\\\"${s.name||''}\\\">\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      <div class=\\\"f\\\">\u001b[39m\n\u001b[31m        <label>City <span class=\\\"req\\\">*</span></label>\u001b[39m\n\u001b[31m        <input type=\\\"text\\\" class=\\\"cons-city\\\" placeholder=\\\"City\\\" value=\\\"${s.city||''}\\\">\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m    <div class=\\\"f\\\">\u001b[39m\n\u001b[31m      <label>Address</label>\u001b[39m\n\u001b[31m      <input type=\\\"text\\\" class=\\\"cons-addr\\\" placeholder=\\\"Full address\\\" value=\\\"${s.address||''}\\\">\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m    <div class=\\\"row2\\\">\u001b[39m\n\u001b[31m      <div class=\\\"f\\\">\u001b[39m\n\u001b[31m        <label>Phone</label>\u001b[39m\n\u001b[31m        <input type=\\\"tel\\\" class=\\\"cons-phone\\\" placeholder=\\\"Mobile\\\" value=\\\"${s.phone||''}\\\">\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      <div class=\\\"f\\\">\u001b[39m\n\u001b[31m        <label>Pincode</label>\u001b[39m\n\u001b[31m        <input type=\\\"text\\\" class=\\\"cons-pin\\\" placeholder=\\\"6-digit\\\" maxlength=\\\"6\\\">\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m    <div class=\\\"f\\\">\u001b[39m\n\u001b[31m      <label>Material / Contents <span class=\\\"req\\\">*</span></label>\u001b[39m\n\u001b[31m      <input type=\\\"text\\\" class=\\\"cons-material\\\" placeholder=\\\"e.g. Standees, Posters\\\">\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m    <div class=\\\"row2\\\">\u001b[39m\n\u001b[31m      <div class=\\\"f\\\">\u001b[39m\n\u001b[31m        <label>Qty</label>\u001b[39m\n\u001b[31m        <input type=\\\"number\\\" class=\\\"cons-qty\\\" placeholder=\\\"0\\\" min=\\\"0\\\">\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      <div class=\\\"f\\\">\u001b[39m\n\u001b[31m        <label>Docket No.</label>\u001b[39m\n\u001b[31m        <input type=\\\"text\\\" class=\\\"cons-docket-input\\\" placeholder=\\\"Docket #\\\"\u001b[39m\n\u001b[31m          style=\\\"font-family:var(--fnt-mono);\\\" value=\\\"${s.docket||''}\\\"\u001b[39m\n\u001b[31m          oninput=\\\"onDocketInput(this)\\\">\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m    ${s.docket ? `<div class=\\\"cons-docket-linked\\\"><span>🔗</span><span class=\\\"dkt\\\">${s.docket}</span></div>` : ''}\u001b[39m\n\u001b[31m  `;\u001b[39m\n\u001b[31m  list.appendChild(card);\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction onDocketInput(input) {\u001b[39m\n\u001b[31m  const card = input.closest('.cons-card');\u001b[39m\n\u001b[31m  let linked = card.querySelector('.cons-docket-linked');\u001b[39m\n\u001b[31m  if (input.value.trim()) {\u001b[39m\n\u001b[31m    if (!linked) {\u001b[39m\n\u001b[31m      linked = document.createElement('div');\u001b[39m\n\u001b[31m      linked.className = 'cons-docket-linked';\u001b[39m\n\u001b[31m      card.appendChild(linked);\u001b[39m\n\u001b[31m    }\u001b[39m\n\u001b[31m    linked.innerHTML = `<span>🔗</span><span class=\\\"dkt\\\">${input.value}</span>`;\u001b[39m\n\u001b[31m  } else if (linked) {\u001b[39m\n\u001b[31m    linked.remove();\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction showLinkedDocket(card, docket) {\u001b[39m\n\u001b[31m  const input = card.querySelector('.cons-docket-input');\u001b[39m\n\u001b[31m  if (input) { input.value = docket; onDocketInput(input); }\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction renumberCons() {\u001b[39m\n\u001b[31m  document.querySelectorAll('#consList .cons-card').forEach((c,i) => {\u001b[39m\n\u001b[31m    c.querySelector('.cons-num').textContent = `CONSIGNEE #${i+1}`;\u001b[39m\n\u001b[31m    c.dataset.idx = i;\u001b[39m\n\u001b[31m  });\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31m// JOB REFERENCE\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31mfunction onClientChange() {\u001b[39m\n\u001b[31m  const val = document.getElementById('n_client').value;\u001b[39m\n\u001b[31m  document.getElementById('customClientField').style.display = val === '__c' ? 'block' : 'none';\u001b[39m\n\u001b[31m  updatePill();\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31mfunction updatePill() {\u001b[39m\n\u001b[31m  const client = document.getElementById('n_client').value;\u001b[39m\n\u001b[31m  const job = document.getElementById('n_jobref').value.trim();\u001b[39m\n\u001b[31m  const cName = client === '__c' ? document.getElementById('n_customClient').value : client;\u001b[39m\n\u001b[31m  const pill = document.getElementById('jobPill');\u001b[39m\n\u001b[31m  if (cName && job) {\u001b[39m\n\u001b[31m    document.getElementById('jobPillTxt').textContent = `${cName} › ${job}`;\u001b[39m\n\u001b[31m    pill.style.display = 'inline-flex';\u001b[39m\n\u001b[31m  } else {\u001b[39m\n\u001b[31m    pill.style.display = 'none';\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31mfunction clearJob() {\u001b[39m\n\u001b[31m  document.getElementById('n_client').value = '';\u001b[39m\n\u001b[31m  document.getElementById('n_jobref').value = '';\u001b[39m\n\u001b[31m  document.getElementById('jobPill').style.display = 'none';\u001b[39m\n\u001b[31m  document.getElementById('customClientField').style.display = 'none';\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31m// GATHER / SAVE\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31mfunction gatherDispatch() {\u001b[39m\n\u001b[31m  const client = document.getElementById('n_client').value;\u001b[39m\n\u001b[31m  const clientName = client === '__c' ? document.getElementById('n_customClient').value : client;\u001b[39m\n\u001b[31m  const consignees = [];\u001b[39m\n\u001b[31m  document.querySelectorAll('#consList .cons-card').forEach(card => {\u001b[39m\n\u001b[31m    const docket = card.querySelector('.cons-docket-input')?.value.trim() || '';\u001b[39m\n\u001b[31m    consignees.push({\u001b[39m\n\u001b[31m      name: card.querySelector('.cons-name')?.value || '',\u001b[39m\n\u001b[31m      city: card.querySelector('.cons-city')?.value || '',\u001b[39m\n\u001b[31m      address: card.querySelector('.cons-addr')?.value || '',\u001b[39m\n\u001b[31m      phone: card.querySelector('.cons-phone')?.value || '',\u001b[39m\n\u001b[31m      pincode: card.querySelector('.cons-pin')?.value || '',\u001b[39m\n\u001b[31m      material: card.querySelector('.cons-material')?.value || '',\u001b[39m\n\u001b[31m      qty: card.querySelector('.cons-qty')?.value || '',\u001b[39m\n\u001b[31m      docket,\u001b[39m\n\u001b[31m      status: docket ? 'booked' : 'pending',\u001b[39m\n\u001b[31m      lastEvent: '',\u001b[39m\n\u001b[31m      lastUpdated: ''\u001b[39m\n\u001b[31m    });\u001b[39m\n\u001b[31m  });\u001b[39m\n\u001b[31m  return {\u001b[39m\n\u001b[31m    id: 'DSP-' + Date.now(),\u001b[39m\n\u001b[31m    mode: 'manual',\u001b[39m\n\u001b[31m    client: clientName,\u001b[39m\n\u001b[31m    jobref: document.getElementById('n_jobref').value,\u001b[39m\n\u001b[31m    courier: document.getElementById('n_courier').value,\u001b[39m\n\u001b[31m    dispatchDate: document.getElementById('n_date').value,\u001b[39m\n\u001b[31m    remarks: document.getElementById('n_remarks').value,\u001b[39m\n\u001b[31m    consignees,\u001b[39m\n\u001b[31m    slipCount: slipOCRData.length,\u001b[39m\n\u001b[31m    status: 'active',\u001b[39m\n\u001b[31m    createdAt: new Date().toISOString()\u001b[39m\n\u001b[31m  };\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction validate(d) {\u001b[39m\n\u001b[31m  if (!d.client) { showToast('Select a client', 'err'); return false; }\u001b[39m\n\u001b[31m  if (!d.courier) { showToast('Select courier', 'err'); return false; }\u001b[39m\n\u001b[31m  if (!d.consignees.length) { showToast('Add at least one consignee', 'err'); return false; }\u001b[39m\n\u001b[31m  for (let i = 0; i < d.consignees.length; i++) {\u001b[39m\n\u001b[31m    const c = d.consignees[i];\u001b[39m\n\u001b[31m    if (!c.name || !c.city) {\u001b[39m\n\u001b[31m      showToast(`Consignee #${i+1} (${c.name||'unnamed'}) — name & city required`, 'err');\u001b[39m\n\u001b[31m      // Scroll to and highlight the problem consignee card\u001b[39m\n\u001b[31m      const cards = document.querySelectorAll('#consList .cons-card');\u001b[39m\n\u001b[31m      if (cards[i]) {\u001b[39m\n\u001b[31m        cards[i].scrollIntoView({ behavior: 'smooth', block: 'center' });\u001b[39m\n\u001b[31m        cards[i].style.borderColor = 'var(--red)';\u001b[39m\n\u001b[31m        cards[i].style.boxShadow = '0 0 0 2px rgba(239,68,68,.3)';\u001b[39m\n\u001b[31m        setTimeout(() => {\u001b[39m\n\u001b[31m          if (cards[i]) { cards[i].style.borderColor = ''; cards[i].style.boxShadow = ''; }\u001b[39m\n\u001b[31m        }, 3000);\u001b[39m\n\u001b[31m      }\u001b[39m\n\u001b[31m      return false;\u001b[39m\n\u001b[31m    }\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m  return true;\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction saveDraft() {\u001b[39m\n\u001b[31m  const d = gatherDispatch();\u001b[39m\n\u001b[31m  d.status = 'draft';\u001b[39m\n\u001b[31m  dispatches.unshift(d);\u001b[39m\n\u001b[31m  saveData();\u001b[39m\n\u001b[31m  updateHistCount();\u001b[39m\n\u001b[31m  showToast('Draft saved ✓', 'ok');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// ── VERIFICATION SCREEN ─────────────────────────────────\u001b[39m\n\u001b[31m// Known docket lengths per courier for validation\u001b[39m\n\u001b[31mconst DOCKET_LENGTHS = {\u001b[39m\n\u001b[31m  'SkyKing': { min: 9, max: 12 },\u001b[39m\n\u001b[31m  'DTDC':    { min: 8, max: 15 },\u001b[39m\n\u001b[31m  'Delhivery': { min: 10, max: 18 },\u001b[39m\n\u001b[31m  'Blue Dart': { min: 10, max: 13 },\u001b[39m\n\u001b[31m  'FedEx':   { min: 10, max: 20 },\u001b[39m\n\u001b[31m  'Ecom Express': { min: 10, max: 14 },\u001b[39m\n\u001b[31m  'India Post': { min: 13, max: 22 },\u001b[39m\n\u001b[31m};·\u001b[39m\n\u001b[31mfunction finaliseDispatch() {\u001b[39m\n\u001b[31m  const d = gatherDispatch();\u001b[39m\n\u001b[31m  if (!validate(d)) return;\u001b[39m\n\u001b[31m  showVerifyScreen(d);\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction showVerifyScreen(d) {\u001b[39m\n\u001b[31m  const pane = document.getElementById('verifyPane');\u001b[39m\n\u001b[31m  const list = document.getElementById('verifyList');\u001b[39m\n\u001b[31m  const summary = document.getElementById('verifySummary');·\u001b[39m\n\u001b[31m  // Build flags\u001b[39m\n\u001b[31m  const docketsSeen = {};\u001b[39m\n\u001b[31m  const courier = d.courier;\u001b[39m\n\u001b[31m  const lenRule = DOCKET_LENGTHS[courier];·\u001b[39m\n\u001b[31m  d.consignees.forEach((c, i) => {\u001b[39m\n\u001b[31m    if (c.docket) docketsSeen[c.docket] = (docketsSeen[c.docket] || 0) + 1;\u001b[39m\n\u001b[31m  });·\u001b[39m\n\u001b[31m  const total     = d.consignees.length;\u001b[39m\n\u001b[31m  const withDock  = d.consignees.filter(c => c.docket).length;\u001b[39m\n\u001b[31m  const dupes     = Object.values(docketsSeen).filter(v => v > 1).length;\u001b[39m\n\u001b[31m  const noDock    = total - withDock;\u001b[39m\n\u001b[31m  const badLen    = lenRule ? d.consignees.filter(c => c.docket &&\u001b[39m\n\u001b[31m    (c.docket.length < lenRule.min || c.docket.length > lenRule.max)).length : 0;·\u001b[39m\n\u001b[31m  // Summary bar\u001b[39m\n\u001b[31m  summary.innerHTML = `\u001b[39m\n\u001b[31m    <div class=\\\"vs-item\\\"><div class=\\\"vs-val\\\" style=\\\"color:var(--t1);\\\">${total}</div><div class=\\\"vs-lbl\\\">Total</div></div>\u001b[39m\n\u001b[31m    <div class=\\\"vs-item\\\"><div class=\\\"vs-val\\\" style=\\\"color:var(--grn);\\\">${withDock}</div><div class=\\\"vs-lbl\\\">Docketed</div></div>\u001b[39m\n\u001b[31m    <div class=\\\"vs-item\\\"><div class=\\\"vs-val\\\" style=\\\"color:${noDock > 0 ? 'var(--ylw)' : 'var(--t3)'};\\\">${noDock}</div><div class=\\\"vs-lbl\\\">No Docket</div></div>\u001b[39m\n\u001b[31m    <div class=\\\"vs-item\\\"><div class=\\\"vs-val\\\" style=\\\"color:${dupes > 0 ? 'var(--red)' : 'var(--t3)'};\\\">${dupes}</div><div class=\\\"vs-lbl\\\">Duplicates</div></div>\u001b[39m\n\u001b[31m    ${badLen > 0 ? `<div class=\\\"vs-item\\\"><div class=\\\"vs-val\\\" style=\\\"color:var(--sky);\\\">${badLen}</div><div class=\\\"vs-lbl\\\">Short/Long</div></div>` : ''}\u001b[39m\n\u001b[31m    <div class=\\\"vs-item\\\" style=\\\"margin-left:auto;text-align:right;\\\">\u001b[39m\n\u001b[31m      <div class=\\\"vs-val\\\" style=\\\"font-size:.8rem;color:var(--t2);\\\">${d.courier||'—'}</div>\u001b[39m\n\u001b[31m      <div class=\\\"vs-lbl\\\">${d.client||'—'} ${d.jobref ? '› '+d.jobref : ''}</div>\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m  `;·\u001b[39m\n\u001b[31m  // Build consignee rows\u001b[39m\n\u001b[31m  list.innerHTML = d.consignees.map((c, i) => {\u001b[39m\n\u001b[31m    const isDupe   = c.docket && docketsSeen[c.docket] > 1;\u001b[39m\n\u001b[31m    const isNoDock = !c.docket;\u001b[39m\n\u001b[31m    const isShort  = lenRule && c.docket &&\u001b[39m\n\u001b[31m      (c.docket.length < lenRule.min || c.docket.length > lenRule.max);·\u001b[39m\n\u001b[31m    const rowClass = isDupe ? 'has-dupe' : (isNoDock ? 'no-docket' : 'ok');·\u001b[39m\n\u001b[31m    // Find matching OCR slip for thumbnail\u001b[39m\n\u001b[31m    const ocrMatch = slipOCRData[i];\u001b[39m\n\u001b[31m    const thumbHtml = ocrMatch?.imageData\u001b[39m\n\u001b[31m      ? `<img class=\\\"verify-thumb\\\" src=\\\"${ocrMatch.imageData}\\\" onclick=\\\"showSlipFull(${i})\\\" title=\\\"View slip\\\">`\u001b[39m\n\u001b[31m      : '';·\u001b[39m\n\u001b[31m    // Flags\u001b[39m\n\u001b[31m    let flags = '';\u001b[39m\n\u001b[31m    if (isDupe)   flags += `<span class=\\\"verify-flag vf-dupe\\\">⚠ Duplicate</span> `;\u001b[39m\n\u001b[31m    if (isNoDock) flags += `<span class=\\\"verify-flag vf-nodocket\\\">No Docket</span> `;\u001b[39m\n\u001b[31m    if (isShort)  flags += `<span class=\\\"verify-flag vf-short\\\">${c.docket.length} digits</span> `;\u001b[39m\n\u001b[31m    if (!isDupe && !isNoDock && !isShort) flags = `<span class=\\\"verify-flag vf-ok\\\">✓ OK</span>`;·\u001b[39m\n\u001b[31m    // OCR confidence note\u001b[39m\n\u001b[31m    const confNote = ocrMatch && ocrMatch.confidence\u001b[39m\n\u001b[31m      ? `<div class=\\\"ocr-correction-note\\\">OCR confidence: ${Math.round(ocrMatch.confidence*100)}% via ${ocrMatch.source||'—'}</div>` : '';·\u001b[39m\n\u001b[31m    // WhatsApp URL\u001b[39m\n\u001b[31m    const waMsg = encodeURIComponent(\u001b[39m\n\u001b[31m      `Dear ${c.name}, your material has been dispatched via ${d.courier}. Docket: ${c.docket||'TBD'}. Track: ${(COURIER_MASTER[d.courier]?.trackUrl||'').replace('{docket}', c.docket||'')}`\u001b[39m\n\u001b[31m    );\u001b[39m\n\u001b[31m    const waUrl = c.phone ? `https://wa.me/91${c.phone.replace(/\\\\D/g,'')}?text=${waMsg}` : '';·\u001b[39m\n\u001b[31m    return `\u001b[39m\n\u001b[31m    <div class=\\\"verify-row ${rowClass}\\\" id=\\\"vrow_${i}\\\">\u001b[39m\n\u001b[31m      <div class=\\\"verify-row-hdr\\\">\u001b[39m\n\u001b[31m        <span class=\\\"verify-num\\\">#${i+1}</span>\u001b[39m\n\u001b[31m        <span class=\\\"verify-store\\\">${c.name||'—'}</span>\u001b[39m\n\u001b[31m        ${flags}\u001b[39m\n\u001b[31m        ${thumbHtml}\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      <div class=\\\"verify-fields\\\" style=\\\"grid-template-columns:1fr 1fr 1fr;\\\">\u001b[39m\n\u001b[31m        <div class=\\\"verify-field\\\">\u001b[39m\n\u001b[31m          <label>Docket No. <span style=\\\"color:var(--acc);\\\">*</span></label>\u001b[39m\n\u001b[31m          <input type=\\\"text\\\" value=\\\"${c.docket||''}\\\" oninput=\\\"updateVerifyField(${i},'docket',this)\\\"\u001b[39m\n\u001b[31m            style=\\\"font-family:var(--fnt-mono);${isDupe||isShort?'border-color:var(--red);':!c.docket?'border-color:var(--ylw);':'color:var(--acc);'}\\\">\u001b[39m\n\u001b[31m          ${confNote}\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m        <div class=\\\"verify-field\\\">\u001b[39m\n\u001b[31m          <label>City</label>\u001b[39m\n\u001b[31m          <input type=\\\"text\\\" value=\\\"${c.city||''}\\\" oninput=\\\"updateVerifyField(${i},'city',this)\\\"\u001b[39m\n\u001b[31m            style=\\\"${!c.city ? 'border-color:var(--ylw);' : ''}\\\">\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m        <div class=\\\"verify-field\\\">\u001b[39m\n\u001b[31m          <label>Pincode</label>\u001b[39m\n\u001b[31m          <input type=\\\"text\\\" value=\\\"${c.pincode||''}\\\" oninput=\\\"updateVerifyField(${i},'pincode',this)\\\"\u001b[39m\n\u001b[31m            style=\\\"font-family:var(--fnt-mono);\\\">\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m        <div class=\\\"verify-field\\\">\u001b[39m\n\u001b[31m          <label>Phone</label>\u001b[39m\n\u001b[31m          <input type=\\\"tel\\\" value=\\\"${c.phone||''}\\\" oninput=\\\"updateVerifyField(${i},'phone',this)\\\">\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m        <div class=\\\"verify-field\\\">\u001b[39m\n\u001b[31m          <label>Material / Contents</label>\u001b[39m\n\u001b[31m          <input type=\\\"text\\\" value=\\\"${c.material||''}\\\" oninput=\\\"updateVerifyField(${i},'material',this)\\\">\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m        <div class=\\\"verify-field\\\">\u001b[39m\n\u001b[31m          <label>Street Address</label>\u001b[39m\n\u001b[31m          <input type=\\\"text\\\" value=\\\"${c.address||''}\\\" oninput=\\\"updateVerifyField(${i},'address',this)\\\"\u001b[39m\n\u001b[31m            placeholder=\\\"Street / locality only\\\">\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      <div class=\\\"verify-actions\\\">\u001b[39m\n\u001b[31m        ${c.docket && c.phone ? `<button class=\\\"btn-wa\\\" onclick=\\\"window.open('${waUrl}','_blank')\\\">📱 WhatsApp</button>` : ''}\u001b[39m\n\u001b[31m        <button class=\\\"btn-sm\\\" onclick=\\\"removeVerifyRow(${i})\\\">✕ Remove</button>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m    </div>`;\u001b[39m\n\u001b[31m  }).join('');·\u001b[39m\n\u001b[31m  // Store dispatch object for confirm\u001b[39m\n\u001b[31m  pane._pendingDispatch = d;\u001b[39m\n\u001b[31m  pane.style.display = 'block';\u001b[39m\n\u001b[31m  pane.scrollTop = 0;\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction updateVerifyField(idx, field, input) {\u001b[39m\n\u001b[31m  const pane = document.getElementById('verifyPane');\u001b[39m\n\u001b[31m  if (!pane._pendingDispatch) return;\u001b[39m\n\u001b[31m  const c = pane._pendingDispatch.consignees[idx];\u001b[39m\n\u001b[31m  if (!c) return;\u001b[39m\n\u001b[31m  c[field] = input.value.trim();\u001b[39m\n\u001b[31m  input.classList.toggle('changed', input.value.trim() !== '');\u001b[39m\n\u001b[31m  // Re-check dupe if docket changed\u001b[39m\n\u001b[31m  if (field === 'docket') refreshVerifyFlags(pane._pendingDispatch);\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction refreshVerifyFlags(d) {\u001b[39m\n\u001b[31m  const seen = {};\u001b[39m\n\u001b[31m  d.consignees.forEach(c => { if (c.docket) seen[c.docket] = (seen[c.docket]||0)+1; });\u001b[39m\n\u001b[31m  d.consignees.forEach((c, i) => {\u001b[39m\n\u001b[31m    const row = document.getElementById('vrow_' + i);\u001b[39m\n\u001b[31m    if (!row) return;\u001b[39m\n\u001b[31m    const isDupe = c.docket && seen[c.docket] > 1;\u001b[39m\n\u001b[31m    row.classList.toggle('has-dupe', isDupe);\u001b[39m\n\u001b[31m  });\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction removeVerifyRow(idx) {\u001b[39m\n\u001b[31m  const pane = document.getElementById('verifyPane');\u001b[39m\n\u001b[31m  if (!pane._pendingDispatch) return;\u001b[39m\n\u001b[31m  const el = document.getElementById('vrow_' + idx);\u001b[39m\n\u001b[31m  if (el) {\u001b[39m\n\u001b[31m    el.style.transition = 'opacity .25s';\u001b[39m\n\u001b[31m    el.style.opacity = '0';\u001b[39m\n\u001b[31m    setTimeout(() => {\u001b[39m\n\u001b[31m      pane._pendingDispatch.consignees.splice(idx, 1);\u001b[39m\n\u001b[31m      showVerifyScreen(pane._pendingDispatch); // re-render with updated indices\u001b[39m\n\u001b[31m    }, 250);\u001b[39m\n\u001b[31m  } else {\u001b[39m\n\u001b[31m    pane._pendingDispatch.consignees.splice(idx, 1);\u001b[39m\n\u001b[31m    showVerifyScreen(pane._pendingDispatch);\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction approveAll() {\u001b[39m\n\u001b[31m  showToast('All rows approved ✓', 'ok');\u001b[39m\n\u001b[31m  document.getElementById('btnConfirmFinalise').style.background = 'var(--grn)';\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction closeVerify() {\u001b[39m\n\u001b[31m  const pane = document.getElementById('verifyPane');\u001b[39m\n\u001b[31m  // If this dispatch was already finalised, warn before going back\u001b[39m\n\u001b[31m  if (_lastFinalisedId && pane._pendingDispatch?.id === _lastFinalisedId) {\u001b[39m\n\u001b[31m    if (!confirm(\u001b[39m\n\u001b[31m      'This dispatch has already been finalised.\\\\n\\\\n' +\u001b[39m\n\u001b[31m      'Going back will take you to the edit form — any changes will create a NEW dispatch record.\\\\n\\\\n' +\u001b[39m\n\u001b[31m      'Go back anyway?'\u001b[39m\n\u001b[31m    )) return;\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m  pane.style.display = 'none';\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction showSlipFull(idx) {\u001b[39m\n\u001b[31m  const s = slipOCRData[idx];\u001b[39m\n\u001b[31m  if (!s?.imageData) return;\u001b[39m\n\u001b[31m  const w = window.open('');\u001b[39m\n\u001b[31m  w.document.write(`<img src=\\\"${s.imageData}\\\" style=\\\"max-width:100%;height:auto;\\\">`);\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// Log OCR correction to sheets for future accuracy improvement\u001b[39m\n\u001b[31masync function logOCRCorrection(original, corrected, courier) {\u001b[39m\n\u001b[31m  if (!window.appConfig.webhook_url || original === corrected) return;\u001b[39m\n\u001b[31m  try {\u001b[39m\n\u001b[31m    await fetch(window.appConfig.webhook_url, {\u001b[39m\n\u001b[31m      method: 'POST', mode: 'no-cors',\u001b[39m\n\u001b[31m      headers: { 'Content-Type': 'application/json' },\u001b[39m\n\u001b[31m      body: JSON.stringify({\u001b[39m\n\u001b[31m        type: 'ocr_correction',\u001b[39m\n\u001b[31m        original, corrected, courier,\u001b[39m\n\u001b[31m        timestamp: new Date().toISOString()\u001b[39m\n\u001b[31m      })\u001b[39m\n\u001b[31m    });\u001b[39m\n\u001b[31m  } catch(e) {}\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mlet _lastFinalisedId = null; // track last finalised dispatch to warn on back·\u001b[39m\n\u001b[31masync function confirmAndFinalise() {\u001b[39m\n\u001b[31m  const pane = document.getElementById('verifyPane');\u001b[39m\n\u001b[31m  const d = pane._pendingDispatch;\u001b[39m\n\u001b[31m  if (!d) return;·\u001b[39m\n\u001b[31m  d.status = 'active';\u001b[39m\n\u001b[31m  d.finalisedAt = new Date().toISOString();·\u001b[39m\n\u001b[31m  // Log OCR corrections\u001b[39m\n\u001b[31m  slipOCRData.forEach((s, i) => {\u001b[39m\n\u001b[31m    const c = d.consignees[i];\u001b[39m\n\u001b[31m    if (c && s.docket && c.docket && s.docket !== c.docket) {\u001b[39m\n\u001b[31m      logOCRCorrection(s.docket, c.docket, d.courier);\u001b[39m\n\u001b[31m    }\u001b[39m\n\u001b[31m  });·\u001b[39m\n\u001b[31m  // Replace existing draft with same ID if present, else prepend\u001b[39m\n\u001b[31m  const existingIdx = dispatches.findIndex(x => x.id === d.id);\u001b[39m\n\u001b[31m  if (existingIdx >= 0) {\u001b[39m\n\u001b[31m    dispatches[existingIdx] = d;\u001b[39m\n\u001b[31m  } else {\u001b[39m\n\u001b[31m    dispatches.unshift(d);\u001b[39m\n\u001b[31m  }·\u001b[39m\n\u001b[31m  _lastFinalisedId = d.id;\u001b[39m\n\u001b[31m  saveData();\u001b[39m\n\u001b[31m  syncDispatchToDB(d); // push to Postgres (non-blocking)\u001b[39m\n\u001b[31m  updateHistCount();\u001b[39m\n\u001b[31m  if (window.appConfig.auto_log !== 'false' && window.appConfig.webhook_url) await logToSheets(d);\u001b[39m\n\u001b[31m  showToast('Dispatch finalised ✓', 'ok');\u001b[39m\n\u001b[31m  pane.style.display = 'none';\u001b[39m\n\u001b[31m  pane._pendingDispatch = null; // clear pending\u001b[39m\n\u001b[31m  resetForm();\u001b[39m\n\u001b[31m  showPane('dashboard');\u001b[39m\n\u001b[31m  renderDashboard();\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction resetForm() {\u001b[39m\n\u001b[31m  document.getElementById('n_client').value = '';\u001b[39m\n\u001b[31m  document.getElementById('n_jobref').value = '';\u001b[39m\n\u001b[31m  document.getElementById('n_courier').value = '';\u001b[39m\n\u001b[31m  document.getElementById('n_remarks').value = '';\u001b[39m\n\u001b[31m  document.getElementById('jobPill').style.display = 'none';\u001b[39m\n\u001b[31m  document.getElementById('customClientField').style.display = 'none';\u001b[39m\n\u001b[31m  document.getElementById('consList').innerHTML = '';\u001b[39m\n\u001b[31m  addConsignee();\u001b[39m\n\u001b[31m  clearSlips();\u001b[39m\n\u001b[31m  clearStores();\u001b[39m\n\u001b[31m  setToday();\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction newDispatch() { resetForm(); showPane('new'); }·\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31m// DASHBOARD\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31mfunction renderDashboard() {\u001b[39m\n\u001b[31m  // Exclude drafts from dashboard — only show active/finalised dispatches\u001b[39m\n\u001b[31m  const activeDis = dispatches.filter(d => d.status !== 'draft');\u001b[39m\n\u001b[31m  const allCons = activeDis.flatMap(d => d.consignees || []);\u001b[39m\n\u001b[31m  const countBy = (status) => allCons.filter(c => c.status === status).length;\u001b[39m\n\u001b[31m  document.getElementById('st-delivered').textContent = countBy('delivered');\u001b[39m\n\u001b[31m  document.getElementById('st-transit').textContent = countBy('transit') + countBy('in-transit');\u001b[39m\n\u001b[31m  document.getElementById('st-booked').textContent = countBy('booked');\u001b[39m\n\u001b[31m  document.getElementById('st-pending').textContent = allCons.filter(c => !c.docket || c.status === 'pending').length;·\u001b[39m\n\u001b[31m  const grid = document.getElementById('dashGrid');\u001b[39m\n\u001b[31m  // Flatten all consignees across dispatches\u001b[39m\n\u001b[31m  let items = activeDis.flatMap(d =>\u001b[39m\n\u001b[31m    (d.consignees || []).filter(c => c.docket).map(c => ({...c, dispatchId: d.id, courier: d.courier, client: d.client, jobref: d.jobref}))\u001b[39m\n\u001b[31m  );\u001b[39m\n\u001b[31m  if (dashFilter !== 'all') {\u001b[39m\n\u001b[31m    items = items.filter(c => {\u001b[39m\n\u001b[31m      if (dashFilter === 'delivered') return c.status === 'delivered';\u001b[39m\n\u001b[31m      if (dashFilter === 'transit') return c.status === 'transit' || c.status === 'in-transit';\u001b[39m\n\u001b[31m      if (dashFilter === 'booked') return c.status === 'booked';\u001b[39m\n\u001b[31m      if (dashFilter === 'pending') return !c.docket || c.status === 'pending';\u001b[39m\n\u001b[31m      return true;\u001b[39m\n\u001b[31m    });\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m  if (!items.length) {\u001b[39m\n\u001b[31m    grid.innerHTML = '<div class=\\\"empty\\\"><div class=\\\"e-icon\\\">📦</div><p>No docket-tracked consignees.<br>Add docket numbers to see tiles.</p></div>';\u001b[39m\n\u001b[31m    return;\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m  const progMap = { delivered:100, transit:60,'in-transit':60, booked:20, pending:5 };\u001b[39m\n\u001b[31m  const progClr = { delivered:'var(--grn)', transit:'var(--ylw)','in-transit':'var(--ylw)', booked:'var(--sky)', pending:'var(--t4)' };\u001b[39m\n\u001b[31m  grid.innerHTML = items.map(c => {\u001b[39m\n\u001b[31m    const sp = c.status || 'booked';\u001b[39m\n\u001b[31m    const prog = progMap[sp] || 20;\u001b[39m\n\u001b[31m    const clr = progClr[sp] || 'var(--t4)';\u001b[39m\n\u001b[31m    return `\u001b[39m\n\u001b[31m    <div class=\\\"docket-tile\\\" onclick=\\\"showDocketDetail('${c.docket}','${c.courier||''}','${c.dispatchId}')\\\">\u001b[39m\n\u001b[31m      <div class=\\\"docket-tile-hdr\\\">\u001b[39m\n\u001b[31m        <div class=\\\"docket-tile-left\\\">\u001b[39m\n\u001b[31m          <div class=\\\"docket-num\\\">${c.docket}</div>\u001b[39m\n\u001b[31m          <div class=\\\"docket-store\\\">${c.name} — ${c.city}</div>\u001b[39m\n\u001b[31m          <div class=\\\"docket-courier\\\">${c.courier||''} · ${c.client||''} ${c.jobref ? '› '+c.jobref : ''}</div>\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m        <span class=\\\"status-pill sp-${sp}\\\">${sp}</span>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      <div style=\\\"display:flex;gap:6px;margin-bottom:6px;align-items:center;\\\">\u001b[39m\n\u001b[31m        <input type=\\\"checkbox\\\" id=\\\"sel_${c.docket.replace(/[^a-z0-9]/gi,'_')}\\\"\u001b[39m\n\u001b[31m          onclick=\\\"event.stopPropagation();toggleTileSelect('${c.docket}')\\\"\u001b[39m\n\u001b[31m          style=\\\"width:14px;height:14px;cursor:pointer;accent-color:var(--acc);\\\">\u001b[39m\n\u001b[31m        <label style=\\\"font-size:.6rem;color:var(--t3);font-family:var(--fnt-mono);cursor:pointer;\\\"\u001b[39m\n\u001b[31m          onclick=\\\"event.stopPropagation();toggleTileSelect('${c.docket}')\\\">Select</label>\u001b[39m\n\u001b[31m        ${c.manualStatus ?\u001b[39m\n\u001b[31m          `<span style=\\\"font-size:.6rem;font-family:var(--fnt-mono);color:var(--grn);\\\">📞 ${c.manualStatus}</span>` : ''}\u001b[39m\n\u001b[31m        ${c.podUploaded ? '<span style=\\\"font-size:.6rem;color:var(--grn);font-family:var(--fnt-mono);\\\">✓ POD</span>' : ''}\u001b[39m\n\u001b[31m        <button onclick=\\\"event.stopPropagation();showManualTracking('${c.docket}','${c.dispatchId}','${c.name}','${c.phone||''}','${c.city||''}')\\\"\u001b[39m\n\u001b[31m          style=\\\"font-size:.6rem;background:rgba(56,189,248,.1);border:1px solid rgba(56,189,248,.3);\u001b[39m\n\u001b[31m          color:var(--sky);padding:2px 7px;border-radius:10px;cursor:pointer;font-family:var(--fnt-mono);\\\">📞 Call Log</button>\u001b[39m\n\u001b[31m        <button onclick=\\\"event.stopPropagation();showPODUpload('${c.docket}','${c.dispatchId}')\\\"\u001b[39m\n\u001b[31m          style=\\\"font-size:.6rem;background:none;border:1px solid var(--bdr);color:var(--t3);\u001b[39m\n\u001b[31m          padding:2px 7px;border-radius:10px;cursor:pointer;font-family:var(--fnt-mono);\\\">📸 POD</button>\u001b[39m\n\u001b[31m        ${c.manualDelivered ? `<button onclick=\\\"event.stopPropagation();resumeAutoTracking('${c.docket}','${c.dispatchId}')\\\"\u001b[39m\n\u001b[31m          style=\\\"font-size:.6rem;background:rgba(251,146,60,.1);border:1px solid rgba(251,146,60,.4);\u001b[39m\n\u001b[31m          color:#fb923c;padding:2px 7px;border-radius:10px;cursor:pointer;font-family:var(--fnt-mono);\\\">↩ Undo</button>` : ''}\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      <div class=\\\"docket-tile-footer\\\">\u001b[39m\n\u001b[31m        <div style=\\\"flex:1;min-width:0;\\\">\u001b[39m\n\u001b[31m          <span class=\\\"docket-last-event docket-evt-${c.docket}\\\" id=\\\"evt_${c.docket.replace(/[^a-z0-9]/gi,'_')}\\\">${c.lastEvent || 'No tracking yet'}</span>\u001b[39m\n\u001b[31m          <span class=\\\"track-spinner\\\" id=\\\"spin_${c.docket.replace(/[^a-z0-9]/gi,'_')}\\\" style=\\\"display:none;color:var(--ylw);font-family:var(--fnt-mono);font-size:.68rem;animation:blink 1s infinite;\\\">⏳ tracking...</span>\u001b[39m\n\u001b[31m          ${c.lastScanLocation && c.lastScanLocation !== c.lastEvent ?·\u001b[39m\n\u001b[31m            `<div style=\\\"font-size:.62rem;color:var(--t3);font-family:var(--fnt-mono);margin-top:2px;\\\">📍 ${c.lastScanLocation}</div>` : ''}\u001b[39m\n\u001b[31m          ${c.lastTrackedAt ?·\u001b[39m\n\u001b[31m            `<div style=\\\"font-size:.6rem;color:var(--t4);font-family:var(--fnt-mono);margin-top:1px;\\\">🕐 tracked ${new Date(c.lastTrackedAt).toLocaleTimeString('en-IN',{hour:'2-digit',minute:'2-digit'})}</div>` : ''}\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m        <button class=\\\"btn-track-live live-btn-${c.docket.replace(/[^a-z0-9]/gi,'_')}\\\" onclick=\\\"event.stopPropagation();liveTrackTile('${c.docket}','${c.courier||''}','${c.dispatchId}')\\\">\u001b[39m\n\u001b[31m          <span class=\\\"live-dot\\\"></span>LIVE\u001b[39m\n\u001b[31m        </button>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      <div class=\\\"docket-progress\\\">\u001b[39m\n\u001b[31m        <div class=\\\"docket-progress-fill\\\" style=\\\"width:${prog}%;background:${clr};\\\"></div>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m    </div>`;\u001b[39m\n\u001b[31m  }).join('');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction filterDash(f, el) {\u001b[39m\n\u001b[31m  dashFilter = f;\u001b[39m\n\u001b[31m  document.querySelectorAll('#dashFilters .btn-sm').forEach(b => b.classList.remove('primary'));\u001b[39m\n\u001b[31m  el.classList.add('primary');\u001b[39m\n\u001b[31m  renderDashboard();\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31masync function liveTrackTile(docket, courier, dispatchId) {\u001b[39m\n\u001b[31m  showToast(`Tracking ${docket}...`, 'info');\u001b[39m\n\u001b[31m  const result = await fetchTracking(docket, courier);\u001b[39m\n\u001b[31m  // Update status in dispatches\u001b[39m\n\u001b[31m  for (const d of dispatches) {\u001b[39m\n\u001b[31m    if (d.id !== dispatchId) continue;\u001b[39m\n\u001b[31m    for (const c of d.consignees) {\u001b[39m\n\u001b[31m      if (c.docket === docket) {\u001b[39m\n\u001b[31m        c.status = result.status || c.status;\u001b[39m\n\u001b[31m        c.lastEvent = result.lastEvent || c.lastEvent;\u001b[39m\n\u001b[31m        c.lastUpdated = new Date().toISOString();\u001b[39m\n\u001b[31m      }\u001b[39m\n\u001b[31m    }\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m  saveData();\u001b[39m\n\u001b[31m  renderDashboard();\u001b[39m\n\u001b[31m  if (result.status === 'failed') {\u001b[39m\n\u001b[31m    // Open courier site as fallback\u001b[39m\n\u001b[31m    openCourierTrackUrl(docket, courier);\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction showDocketDetail(docket, courier, dispatchId) {\u001b[39m\n\u001b[31m  const d = dispatches.find(x => x.id === dispatchId);\u001b[39m\n\u001b[31m  const c = d?.consignees?.find(x => x.docket === docket);\u001b[39m\n\u001b[31m  if (!c) return;\u001b[39m\n\u001b[31m  const content = document.getElementById('detailContent');\u001b[39m\n\u001b[31m  content.innerHTML = `\u001b[39m\n\u001b[31m    <div class=\\\"sheet-title\\\">${docket}</div>\u001b[39m\n\u001b[31m    <div class=\\\"info-tag\\\">📍 <strong>${c.name}</strong> — ${c.city}</div>\u001b[39m\n\u001b[31m    ${c.phone ? `<div class=\\\"info-tag\\\">📞 ${c.phone}</div>` : ''}\u001b[39m\n\u001b[31m    <div class=\\\"info-tag\\\">📦 ${c.material||'—'} ${c.qty ? '('+c.qty+' pcs)':''}</div>\u001b[39m\n\u001b[31m    <div class=\\\"info-tag\\\">🚚 ${courier||'—'} · <span class=\\\"status-pill sp-${c.status||'pending'}\\\">${c.status||'pending'}</span></div>\u001b[39m\n\u001b[31m    ${c.lastEvent ? `<div class=\\\"info-tag\\\">🕐 ${c.lastEvent}</div>` : ''}\u001b[39m\n\u001b[31m    <div class=\\\"sec-div\\\"><span>Tracking History</span></div>\u001b[39m\n\u001b[31m    <div id=\\\"docketTimeline\\\">\u001b[39m\n\u001b[31m      ${c.timeline?.length ?\u001b[39m\n\u001b[31m        c.timeline.map(e => `<div class=\\\"track-evt\\\"><span class=\\\"track-time\\\">${e.time||''}</span><div><div class=\\\"track-desc\\\">${e.desc||''}</div><div class=\\\"track-loc\\\">${e.loc||''}</div></div></div>`).join('') :\u001b[39m\n\u001b[31m        '<div style=\\\"color:var(--t3);font-size:.78rem;padding:10px 0;\\\">No timeline data. Click Live Track to fetch.</div>'}\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m    <div style=\\\"display:flex;gap:6px;flex-wrap:wrap;margin-top:14px;\\\">\u001b[39m\n\u001b[31m      <button class=\\\"btn btn-ghost\\\" style=\\\"flex:1;\\\" onclick=\\\"openCourierTrackUrl('${docket}','${courier}')\\\">🔗 Courier Site</button>\u001b[39m\n\u001b[31m      <button class=\\\"btn btn-orange\\\" style=\\\"flex:1;\\\" onclick=\\\"liveTrackTile('${docket}','${courier}','${dispatchId}');closeOverlay('detailOverlay')\\\">⚡ Live Track</button>\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m    <div style=\\\"display:flex;gap:6px;flex-wrap:wrap;margin-top:6px;\\\">\u001b[39m\n\u001b[31m      <button class=\\\"btn-wa\\\" style=\\\"flex:1;padding:8px;\\\" onclick=\\\"closeOverlay('detailOverlay');showPODUpload('${docket}','${dispatchId}')\\\">📸 Upload POD</button>\u001b[39m\n\u001b[31m      ${c.podUploaded ? '<span style=\\\"color:var(--grn);font-size:.72rem;font-family:var(--fnt-mono);padding:6px;\\\">✓ POD uploaded</span>' : ''}\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m  `;\u001b[39m\n\u001b[31m  openOverlay('detailOverlay');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31m// TRACKING ENGINE\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31mfunction _normaliseStatus(raw) {\u001b[39m\n\u001b[31m  const s = (raw || '').toLowerCase().trim();\u001b[39m\n\u001b[31m  if (!s || s === 'error') return null;\u001b[39m\n\u001b[31m  if (s.includes('undeliver') || s.includes('delivery_fail') ||\u001b[39m\n\u001b[31m      s.includes('failed_delivery') || s.includes('not deliver') ||\u001b[39m\n\u001b[31m      s.includes('return')  || s.includes('rto')) return 'pending';\u001b[39m\n\u001b[31m  if (s.includes('deliver'))                       return 'delivered';\u001b[39m\n\u001b[31m  if (s.includes('transit') || s.includes('hub') ||\u001b[39m\n\u001b[31m      s.includes('inbound') || s.includes('outbound') ||\u001b[39m\n\u001b[31m      s.includes('out for'))                       return 'transit';\u001b[39m\n\u001b[31m  if (s.includes('book') || s.includes('pick'))   return 'booked';\u001b[39m\n\u001b[31m  return s;\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31masync function _fetchWithRetry(url, opts, maxRetries = 1, retryDelayMs = 4000) {\u001b[39m\n\u001b[31m  for (let attempt = 0; attempt <= maxRetries; attempt++) {\u001b[39m\n\u001b[31m    try {\u001b[39m\n\u001b[31m      const resp = await fetch(url, opts);\u001b[39m\n\u001b[31m      if (resp.ok) return resp;\u001b[39m\n\u001b[31m    } catch(e) {\u001b[39m\n\u001b[31m      if (attempt === maxRetries) throw e;\u001b[39m\n\u001b[31m      await new Promise(r => setTimeout(r, retryDelayMs));\u001b[39m\n\u001b[31m    }\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m  return null;\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31masync function fetchTracking(docket, courier) {\u001b[39m\n\u001b[31m  // Strategy 1: VPS proxy (SkyKing + others that block direct fetch)\u001b[39m\n\u001b[31m  const proxyUrl = window.appConfig.proxy_url;\u001b[39m\n\u001b[31m  if (proxyUrl) {\u001b[39m\n\u001b[31m    try {\u001b[39m\n\u001b[31m      const url  = `${proxyUrl}?courier=${encodeURIComponent(courier)}&docket=${encodeURIComponent(docket)}`;\u001b[39m\n\u001b[31m      const resp = await _fetchWithRetry(url, { signal: AbortSignal.timeout(15000) });\u001b[39m\n\u001b[31m      if (resp) {\u001b[39m\n\u001b[31m        const data      = await resp.json();\u001b[39m\n\u001b[31m        const normStatus = _normaliseStatus(data.status);\u001b[39m\n\u001b[31m        if (normStatus) {\u001b[39m\n\u001b[31m          return { status: normStatus, lastEvent: data.lastEvent || '', timeline: data.timeline || [] };\u001b[39m\n\u001b[31m        }\u001b[39m\n\u001b[31m        openCourierTrackUrl(docket, courier);\u001b[39m\n\u001b[31m        return { status: 'transit', lastEvent: data.lastEvent || 'Check courier site', timeline: [] };\u001b[39m\n\u001b[31m      }\u001b[39m\n\u001b[31m    } catch(e) {}\u001b[39m\n\u001b[31m  }·\u001b[39m\n\u001b[31m  // Strategy 2: Delhivery direct API\u001b[39m\n\u001b[31m  if (courier === 'Delhivery') {\u001b[39m\n\u001b[31m    try {\u001b[39m\n\u001b[31m      const resp = await _fetchWithRetry(\u001b[39m\n\u001b[31m        `https://dlv-api.delhivery.com/v3/track?waybill=${docket}`,\u001b[39m\n\u001b[31m        { signal: AbortSignal.timeout(8000) }\u001b[39m\n\u001b[31m      );\u001b[39m\n\u001b[31m      if (resp) {\u001b[39m\n\u001b[31m        const data = await resp.json();\u001b[39m\n\u001b[31m        const pkg  = data.ShipmentData?.[0]?.Shipment;\u001b[39m\n\u001b[31m        if (pkg) {\u001b[39m\n\u001b[31m          const normStatus = _normaliseStatus(pkg.Status?.Status);\u001b[39m\n\u001b[31m          return { status: normStatus || 'transit', lastEvent: pkg.Status?.Instructions || '', timeline: [] };\u001b[39m\n\u001b[31m        }\u001b[39m\n\u001b[31m      }\u001b[39m\n\u001b[31m    } catch(e) {}\u001b[39m\n\u001b[31m  }·\u001b[39m\n\u001b[31m  return { status: 'failed', lastEvent: 'Open courier site to check', timeline: [] };\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction openCourierTrackUrl(docket, courier) {\u001b[39m\n\u001b[31m  const m = COURIER_MASTER[courier];\u001b[39m\n\u001b[31m  if (!m) return;\u001b[39m\n\u001b[31m  const url = m.trackUrl.replace('{docket}', docket);\u001b[39m\n\u001b[31m  window.open(url, '_blank');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// Track tab - multiple dockets\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31m// TRACK TAB — new logic\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31mlet _trackSelected = new Set(); // dockets currently selected for tracking·\u001b[39m\n\u001b[31m// Called when Track tab opens or courier changes\u001b[39m\n\u001b[31mfunction buildTrackDocketChips() {\u001b[39m\n\u001b[31m  const courier = document.getElementById('t_courier').value;\u001b[39m\n\u001b[31m  const m = COURIER_MASTER[courier];\u001b[39m\n\u001b[31m  const maxBulk = m?.maxBulk || 1;\u001b[39m\n\u001b[31m  const limitEl = document.getElementById('trackCourierLimit');\u001b[39m\n\u001b[31m  if (limitEl) limitEl.textContent = `Max ${maxBulk} at once for ${courier}`;·\u001b[39m\n\u001b[31m  // Gather all dockets from active dispatches for this courier\u001b[39m\n\u001b[31m  const activeDis = dispatches.filter(d => d.status !== 'draft');\u001b[39m\n\u001b[31m  const allDockets = activeDis.flatMap(d =>\u001b[39m\n\u001b[31m    (d.consignees || [])\u001b[39m\n\u001b[31m      .filter(c => c.docket && d.courier === courier)\u001b[39m\n\u001b[31m      .map(c => ({ docket: c.docket, name: c.name, city: c.city, status: c.status || 'booked' }))\u001b[39m\n\u001b[31m  );·\u001b[39m\n\u001b[31m  // Also include dockets from OTHER couriers as un-selectable reference\u001b[39m\n\u001b[31m  const otherDockets = activeDis.flatMap(d =>\u001b[39m\n\u001b[31m    (d.consignees || [])\u001b[39m\n\u001b[31m      .filter(c => c.docket && d.courier !== courier)\u001b[39m\n\u001b[31m      .map(c => ({ docket: c.docket, name: c.name, courier: d.courier }))\u001b[39m\n\u001b[31m  );·\u001b[39m\n\u001b[31m  const chips = document.getElementById('trackDocketChips');\u001b[39m\n\u001b[31m  if (!allDockets.length && !otherDockets.length) {\u001b[39m\n\u001b[31m    chips.innerHTML = '<span style=\\\"font-size:.72rem;color:var(--t3);font-family:var(--fnt-mono);\\\">No dispatched dockets yet — enter manually below</span>';\u001b[39m\n\u001b[31m    return;\u001b[39m\n\u001b[31m  }·\u001b[39m\n\u001b[31m  const spClr = { delivered:'var(--grn)', transit:'var(--ylw)', booked:'var(--sky)', pending:'var(--t4)' };·\u001b[39m\n\u001b[31m  chips.innerHTML = [\u001b[39m\n\u001b[31m    // This courier's dockets\u001b[39m\n\u001b[31m    ...allDockets.map(d => {\u001b[39m\n\u001b[31m      const sel = _trackSelected.has(d.docket);\u001b[39m\n\u001b[31m      const clr = spClr[d.status] || 'var(--t4)';\u001b[39m\n\u001b[31m      return `<button onclick=\\\"toggleTrackDocket('${d.docket}')\\\" id=\\\"chip_${d.docket}\\\"\u001b[39m\n\u001b[31m        style=\\\"font-family:var(--fnt-mono);font-size:.72rem;padding:4px 10px;border-radius:20px;\u001b[39m\n\u001b[31m        cursor:pointer;transition:all .2s;border:1px solid ${sel ? 'var(--acc)' : 'var(--bdr)'};\u001b[39m\n\u001b[31m        background:${sel ? 'rgba(249,115,22,.15)' : 'var(--sur2)'};\u001b[39m\n\u001b[31m        color:${sel ? 'var(--acc)' : 'var(--t2)'};\\\">\u001b[39m\n\u001b[31m        ${d.docket} <span style=\\\"font-size:.6rem;color:${clr};\\\">● ${d.status}</span>\u001b[39m\n\u001b[31m        ${d.city ? `<span style=\\\"font-size:.6rem;color:var(--t3);margin-left:4px;\\\">${d.city}</span>` : ''}\u001b[39m\n\u001b[31m      </button>`;\u001b[39m\n\u001b[31m    }),\u001b[39m\n\u001b[31m    // Other couriers — show greyed, click selects and switches courier\u001b[39m\n\u001b[31m    ...otherDockets.slice(0, 5).map(d =>\u001b[39m\n\u001b[31m      `<span title=\\\"${d.courier}\\\" style=\\\"font-family:var(--fnt-mono);font-size:.68rem;padding:3px 8px;\u001b[39m\n\u001b[31m        border-radius:20px;border:1px dashed var(--bdr);color:var(--t4);cursor:default;\\\">\u001b[39m\n\u001b[31m        ${d.docket} <span style=\\\"font-size:.55rem;\\\">${d.courier}</span>\u001b[39m\n\u001b[31m      </span>`\u001b[39m\n\u001b[31m    )\u001b[39m\n\u001b[31m  ].join('');·\u001b[39m\n\u001b[31m  updateTrackSummary();\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction toggleTrackDocket(docket) {\u001b[39m\n\u001b[31m  const courier = document.getElementById('t_courier').value;\u001b[39m\n\u001b[31m  const m = COURIER_MASTER[courier];\u001b[39m\n\u001b[31m  const maxBulk = m?.maxBulk || 1;·\u001b[39m\n\u001b[31m  if (_trackSelected.has(docket)) {\u001b[39m\n\u001b[31m    _trackSelected.delete(docket);\u001b[39m\n\u001b[31m  } else {\u001b[39m\n\u001b[31m    if (_trackSelected.size >= maxBulk) {\u001b[39m\n\u001b[31m      showToast(`${courier} supports max ${maxBulk} docket(s) at once`, 'err');\u001b[39m\n\u001b[31m      return;\u001b[39m\n\u001b[31m    }\u001b[39m\n\u001b[31m    _trackSelected.add(docket);\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m  buildTrackDocketChips(); // re-render chips with updated selection\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction addManualDockets() {\u001b[39m\n\u001b[31m  const raw = document.getElementById('t_manual_dockets').value;\u001b[39m\n\u001b[31m  const courier = document.getElementById('t_courier').value;\u001b[39m\n\u001b[31m  const m = COURIER_MASTER[courier];\u001b[39m\n\u001b[31m  const maxBulk = m?.maxBulk || 1;\u001b[39m\n\u001b[31m  const newDockets = raw.split(/[,\\\\n]/).map(d => d.trim()).filter(Boolean);\u001b[39m\n\u001b[31m  let added = 0;\u001b[39m\n\u001b[31m  for (const d of newDockets) {\u001b[39m\n\u001b[31m    if (_trackSelected.size >= maxBulk) {\u001b[39m\n\u001b[31m      showToast(`Max ${maxBulk} reached for ${courier}`, 'err');\u001b[39m\n\u001b[31m      break;\u001b[39m\n\u001b[31m    }\u001b[39m\n\u001b[31m    _trackSelected.add(d);\u001b[39m\n\u001b[31m    added++;\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m  document.getElementById('t_manual_dockets').value = '';\u001b[39m\n\u001b[31m  buildTrackDocketChips();\u001b[39m\n\u001b[31m  if (added) showToast(`${added} docket(s) added ✓`, 'ok');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction clearTrackSelection() {\u001b[39m\n\u001b[31m  _trackSelected.clear();\u001b[39m\n\u001b[31m  buildTrackDocketChips();\u001b[39m\n\u001b[31m  document.getElementById('trackResultsArea').innerHTML = '';\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction updateTrackSummary() {\u001b[39m\n\u001b[31m  const summary = document.getElementById('trackSelectedSummary');\u001b[39m\n\u001b[31m  const countEl = document.getElementById('trackSelectedCount');\u001b[39m\n\u001b[31m  const listEl  = document.getElementById('trackSelectedList');\u001b[39m\n\u001b[31m  if (!summary) return;\u001b[39m\n\u001b[31m  const arr = Array.from(_trackSelected);\u001b[39m\n\u001b[31m  summary.style.display = arr.length ? 'block' : 'none';\u001b[39m\n\u001b[31m  if (countEl) countEl.textContent = `${arr.length} docket(s) selected`;\u001b[39m\n\u001b[31m  if (listEl)  listEl.textContent  = arr.slice(0, 5).join(', ') + (arr.length > 5 ? `... +${arr.length-5}` : '');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction openTrackOnCourierSite() {\u001b[39m\n\u001b[31m  const courier = document.getElementById('t_courier').value;\u001b[39m\n\u001b[31m  const dockets = Array.from(_trackSelected);\u001b[39m\n\u001b[31m  if (!dockets.length) { showToast('Select dockets first', 'err'); return; }\u001b[39m\n\u001b[31m  const m = COURIER_MASTER[courier];\u001b[39m\n\u001b[31m  if (!m) return;\u001b[39m\n\u001b[31m  // SkyKing: open with all CNOs comma-separated in URL\u001b[39m\n\u001b[31m  let url;\u001b[39m\n\u001b[31m  if (courier === 'SkyKing') {\u001b[39m\n\u001b[31m    url = `https://skyking.co/track?cno=${dockets.join(',')}`;\u001b[39m\n\u001b[31m  } else if (dockets.length === 1) {\u001b[39m\n\u001b[31m    url = m.trackUrl.replace('{docket}', dockets[0]);\u001b[39m\n\u001b[31m  } else {\u001b[39m\n\u001b[31m    url = m.trackBulkUrl || m.trackUrl.replace('{docket}', dockets[0]);\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m  window.open(url, '_blank');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31masync function trackSelected() {\u001b[39m\n\u001b[31m  const courier = document.getElementById('t_courier').value;\u001b[39m\n\u001b[31m  const dockets = Array.from(_trackSelected);\u001b[39m\n\u001b[31m  if (!dockets.length) { showToast('Select at least one docket', 'err'); return; }·\u001b[39m\n\u001b[31m  const area = document.getElementById('trackResultsArea');\u001b[39m\n\u001b[31m  area.innerHTML = `<div class=\\\"card\\\" style=\\\"text-align:center;padding:20px;\\\">\u001b[39m\n\u001b[31m    <div style=\\\"font-size:1.5rem;margin-bottom:8px;\\\">⏳</div>\u001b[39m\n\u001b[31m    <div style=\\\"font-family:var(--fnt-mono);font-size:.78rem;color:var(--t2);\\\">Tracking ${dockets.length} docket(s) via ${courier}...</div>\u001b[39m\n\u001b[31m  </div>`;·\u001b[39m\n\u001b[31m  let resultsHTML = '';\u001b[39m\n\u001b[31m  for (const docket of dockets) {\u001b[39m\n\u001b[31m    const result = await fetchTracking(docket, courier);\u001b[39m\n\u001b[31m    const sp = (!result.status || result.status === 'failed') ? 'pending' : result.status;\u001b[39m\n\u001b[31m    const spClr = { delivered:'var(--grn)', transit:'var(--ylw)', booked:'var(--sky)', pending:'var(--t3)' };\u001b[39m\n\u001b[31m    const clr = spClr[sp] || 'var(--t3)';\u001b[39m\n\u001b[31m    // Update stored dispatch if this docket exists in our records\u001b[39m\n\u001b[31m    for (const d of dispatches) {\u001b[39m\n\u001b[31m      for (const c of (d.consignees||[])) {\u001b[39m\n\u001b[31m        if (c.docket === docket && result.status && result.status !== 'failed') {\u001b[39m\n\u001b[31m          c.status = result.status;\u001b[39m\n\u001b[31m          c.lastEvent = result.lastEvent;\u001b[39m\n\u001b[31m          c.lastTrackedAt = new Date().toISOString();\u001b[39m\n\u001b[31m        }\u001b[39m\n\u001b[31m      }\u001b[39m\n\u001b[31m    }\u001b[39m\n\u001b[31m    resultsHTML += `\u001b[39m\n\u001b[31m    <div style=\\\"background:var(--sur);border:1px solid var(--bdr);border-left:3px solid ${clr};\u001b[39m\n\u001b[31m      border-radius:var(--r-sm);padding:10px 12px;margin-bottom:8px;\\\">\u001b[39m\n\u001b[31m      <div style=\\\"display:flex;justify-content:space-between;align-items:center;margin-bottom:6px;\\\">\u001b[39m\n\u001b[31m        <span style=\\\"font-family:var(--fnt-mono);font-size:.9rem;color:var(--sky);\\\">${docket}</span>\u001b[39m\n\u001b[31m        <span class=\\\"status-pill sp-${sp}\\\">${sp}</span>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      <div style=\\\"font-size:.75rem;color:var(--t2);font-family:var(--fnt-mono);\\\">${result.lastEvent || '—'}</div>\u001b[39m\n\u001b[31m      ${result.lastScanLocation ? `<div style=\\\"font-size:.68rem;color:var(--t3);margin-top:3px;\\\">📍 ${result.lastScanLocation}</div>` : ''}\u001b[39m\n\u001b[31m      <div style=\\\"display:flex;gap:8px;margin-top:8px;\\\">\u001b[39m\n\u001b[31m        <button class=\\\"btn-sm\\\" onclick=\\\"openCourierTrackUrl('${docket}','${courier}')\\\">🔗 Open on ${courier}</button>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m    </div>`;\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m  saveData();\u001b[39m\n\u001b[31m  area.innerHTML = `<div style=\\\"margin-top:4px;\\\">${resultsHTML}</div>`;\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// Keep old names as aliases for compatibility\u001b[39m\n\u001b[31mfunction trackMultiple() { trackSelected(); }\u001b[39m\n\u001b[31mfunction openCourierSite() { openTrackOnCourierSite(); }·\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31m// HISTORY\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31mfunction renderHistory() {\u001b[39m\n\u001b[31m  const list = document.getElementById('historyList');\u001b[39m\n\u001b[31m  if (!dispatches.length) {\u001b[39m\n\u001b[31m    list.innerHTML = '<div class=\\\"empty\\\"><div class=\\\"e-icon\\\">📭</div><p>No dispatches yet.</p></div>';\u001b[39m\n\u001b[31m    return;\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m  list.innerHTML = dispatches.map(d => {\u001b[39m\n\u001b[31m    const cons = d.consignees?.length || 0;\u001b[39m\n\u001b[31m    const docketed = d.consignees?.filter(c=>c.docket).length || 0;\u001b[39m\n\u001b[31m    const clr = d.status === 'draft' ? 'var(--ylw)' : 'var(--grn)';\u001b[39m\n\u001b[31m    return `\u001b[39m\n\u001b[31m    <div class=\\\"docket-tile\\\" onclick=\\\"showDispatchDetail('${d.id}')\\\">\u001b[39m\n\u001b[31m      <div class=\\\"docket-tile-hdr\\\">\u001b[39m\n\u001b[31m        <div class=\\\"docket-tile-left\\\">\u001b[39m\n\u001b[31m          <div class=\\\"docket-num\\\" style=\\\"font-size:.78rem;\\\">${d.id}</div>\u001b[39m\n\u001b[31m          <div class=\\\"docket-store\\\">${d.client||'No client'} ${d.jobref ? '› '+d.jobref : ''}</div>\u001b[39m\n\u001b[31m          <div class=\\\"docket-courier\\\">📦 ${cons} consignees · ${docketed} docketed · ${d.courier||'—'} · ${d.dispatchDate||d.createdAt?.slice(0,10)||'—'}</div>\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m        <span class=\\\"status-pill\\\" style=\\\"background:rgba(0,0,0,.2);color:${clr};\\\">${d.status||'active'}</span>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m    </div>`;\u001b[39m\n\u001b[31m  }).join('');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction showDispatchDetail(id) {\u001b[39m\n\u001b[31m  const d = dispatches.find(x => x.id === id);\u001b[39m\n\u001b[31m  if (!d) return;\u001b[39m\n\u001b[31m  const content = document.getElementById('detailContent');\u001b[39m\n\u001b[31m  content.innerHTML = `\u001b[39m\n\u001b[31m    <div class=\\\"sheet-title\\\">${d.client} ${d.jobref ? '› '+d.jobref : ''}</div>\u001b[39m\n\u001b[31m    <div class=\\\"info-tag\\\"><strong>${d.id}</strong> · ${d.courier||'—'} · ${d.dispatchDate||'—'}</div>\u001b[39m\n\u001b[31m    <div class=\\\"sec-div\\\"><span>Consignees (${d.consignees?.length||0})</span></div>\u001b[39m\n\u001b[31m    ${(d.consignees||[]).map((c,i) => `\u001b[39m\n\u001b[31m      <div style=\\\"background:var(--sur2);border:1px solid var(--bdr);border-radius:var(--r-sm);padding:10px;margin-bottom:6px;\\\">\u001b[39m\n\u001b[31m        <div style=\\\"font-weight:600;margin-bottom:4px;\\\">${i+1}. ${c.name} — ${c.city}</div>\u001b[39m\n\u001b[31m        ${c.docket ? `<div style=\\\"font-family:var(--fnt-mono);font-size:.78rem;color:var(--sky);\\\">🔖 ${c.docket}</div>` : ''}\u001b[39m\n\u001b[31m        ${c.material ? `<div style=\\\"font-size:.75rem;color:var(--t2);\\\">📦 ${c.material} ${c.qty ? '('+c.qty+')':''}</div>` : ''}\u001b[39m\n\u001b[31m        <span class=\\\"status-pill sp-${c.status||'pending'}\\\" style=\\\"margin-top:4px;display:inline-block;\\\">${c.status||'pending'}</span>\u001b[39m\n\u001b[31m      </div>`).join('')}\u001b[39m\n\u001b[31m    ${d.remarks ? `<div class=\\\"info-tag\\\">📝 ${d.remarks}</div>` : ''}\u001b[39m\n\u001b[31m    <div class=\\\"btn-row\\\" style=\\\"margin-top:14px;\\\">\u001b[39m\n\u001b[31m      <button class=\\\"btn btn-ghost\\\" onclick=\\\"deleteDispatch('${d.id}');closeOverlay('detailOverlay')\\\">🗑️ Delete</button>\u001b[39m\n\u001b[31m      <button class=\\\"btn btn-orange\\\" onclick=\\\"closeOverlay('detailOverlay');showPane('dashboard')\\\">📊 View in Dashboard</button>\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m  `;\u001b[39m\n\u001b[31m  openOverlay('detailOverlay');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction deleteDispatch(id) {\u001b[39m\n\u001b[31m  if (!confirm('Delete this dispatch?')) return;\u001b[39m\n\u001b[31m  dispatches = dispatches.filter(d => d.id !== id);\u001b[39m\n\u001b[31m  saveData();\u001b[39m\n\u001b[31m  updateHistCount();\u001b[39m\n\u001b[31m  renderHistory();\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction updateHistCount() {\u001b[39m\n\u001b[31m  document.getElementById('histCount').textContent = dispatches.length;\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31m// GOOGLE SHEETS\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31masync function logToSheets(d) {\u001b[39m\n\u001b[31m  const url = window.appConfig.webhook_url;\u001b[39m\n\u001b[31m  if (!url) return;\u001b[39m\n\u001b[31m  try {\u001b[39m\n\u001b[31m    await fetch(url, {\u001b[39m\n\u001b[31m      method: 'POST', mode: 'no-cors',\u001b[39m\n\u001b[31m      headers: { 'Content-Type': 'application/json' },\u001b[39m\n\u001b[31m      body: JSON.stringify({\u001b[39m\n\u001b[31m        type: 'dispatch', id: d.id, client: d.client, jobref: d.jobref,\u001b[39m\n\u001b[31m        courier: d.courier, consignees: d.consignees?.length||0,\u001b[39m\n\u001b[31m        destinations: d.consignees?.map(c=>c.city).join(', ')||'',\u001b[39m\n\u001b[31m        dockets: d.consignees?.filter(c=>c.docket).map(c=>c.docket).join(', ')||'',\u001b[39m\n\u001b[31m        dispatchDate: d.dispatchDate, status: d.status,\u001b[39m\n\u001b[31m        timestamp: new Date().toISOString(), company: window.appConfig.company\u001b[39m\n\u001b[31m      })\u001b[39m\n\u001b[31m    });\u001b[39m\n\u001b[31m  } catch(e) {}\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31m// CONFIG — Google Sheet is the source of truth\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════·\u001b[39m\n\u001b[31mfunction onSheetsUrlChange() {\u001b[39m\n\u001b[31m  const url = document.getElementById('cfg_sheets_url').value.trim();\u001b[39m\n\u001b[31m  // Show sync button state\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction saveSheetsUrl() {\u001b[39m\n\u001b[31m  const url = document.getElementById('cfg_sheets_url').value.trim();\u001b[39m\n\u001b[31m  if (!url) { showToast('Enter the Apps Script URL first', 'err'); return; }\u001b[39m\n\u001b[31m  window.appConfig.sheets_url = url;\u001b[39m\n\u001b[31m  saveSheetsUrlLocal();\u001b[39m\n\u001b[31m  syncFromSheet(false);\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m/**\u001b[39m\n\u001b[31m * Fetch all config from Google Sheet and apply to appConfig.\u001b[39m\n\u001b[31m * Called on startup (silent=true) and manually (silent=false).\u001b[39m\n\u001b[31m */\u001b[39m\n\u001b[31masync function syncFromSheet(silent = false) {\u001b[39m\n\u001b[31m  const url = window.appConfig.sheets_url;\u001b[39m\n\u001b[31m  if (!url) { if (!silent) showToast('No Sheets URL configured', 'err'); return; }·\u001b[39m\n\u001b[31m  setSyncStatus('syncing', 'Syncing config from Google Sheet...');\u001b[39m\n\u001b[31m  try {\u001b[39m\n\u001b[31m    const resp = await fetch(url + '?action=pwa_config', {\u001b[39m\n\u001b[31m      method: 'GET',\u001b[39m\n\u001b[31m      redirect: 'follow'\u001b[39m\n\u001b[31m    });\u001b[39m\n\u001b[31m    if (!resp.ok) throw new Error('HTTP ' + resp.status);\u001b[39m\n\u001b[31m    const cfg = await resp.json();·\u001b[39m\n\u001b[31m    // Apply all values from sheet to appConfig\u001b[39m\n\u001b[31m    const keyMap = {\u001b[39m\n\u001b[31m      proxy_url:           'proxy_url',\u001b[39m\n\u001b[31m      webhook_url:         'webhook_url',\u001b[39m\n\u001b[31m      ollama_url:          'ollama_url',\u001b[39m\n\u001b[31m      ocr_mode:            'ocr_mode',\u001b[39m\n\u001b[31m      company:             'company',\u001b[39m\n\u001b[31m      city:                'city',\u001b[39m\n\u001b[31m      auto_log:            'auto_log',\u001b[39m\n\u001b[31m      refresh_on_load:     'refresh_on_load',\u001b[39m\n\u001b[31m      admin_password:      'admin_password',\u001b[39m\n\u001b[31m      n8n_dispatch_webhook:'n8n_dispatch_webhook',\u001b[39m\n\u001b[31m    };\u001b[39m\n\u001b[31m    Object.entries(keyMap).forEach(([sheetKey, cfgKey]) => {\u001b[39m\n\u001b[31m      if (cfg[sheetKey] !== undefined && cfg[sheetKey] !== '') {\u001b[39m\n\u001b[31m        window.appConfig[cfgKey] = cfg[sheetKey];\u001b[39m\n\u001b[31m      }\u001b[39m\n\u001b[31m    });·\u001b[39m\n\u001b[31m    // Apply courier master if provided\u001b[39m\n\u001b[31m    if (cfg._courier_master && Array.isArray(cfg._courier_master)) {\u001b[39m\n\u001b[31m      cfg._courier_master.forEach(c => {\u001b[39m\n\u001b[31m        if (c.name && c.trackUrl) {\u001b[39m\n\u001b[31m          COURIER_MASTER[c.name] = {\u001b[39m\n\u001b[31m            trackUrl: c.trackUrl,\u001b[39m\n\u001b[31m            trackBulkUrl: c.bulkUrl || '',\u001b[39m\n\u001b[31m            maxBulk: parseInt(c.maxBulk) || 1\u001b[39m\n\u001b[31m          };\u001b[39m\n\u001b[31m        }\u001b[39m\n\u001b[31m      });\u001b[39m\n\u001b[31m    }·\u001b[39m\n\u001b[31m    renderCourierMasterList();\u001b[39m\n\u001b[31m    renderSheetConfigDisplay(cfg);\u001b[39m\n\u001b[31m    setSyncStatus('ok', 'Synced from sheet · ' + new Date().toLocaleTimeString());\u001b[39m\n\u001b[31m    if (!silent) showToast('Config synced from sheet ✓', 'ok');·\u001b[39m\n\u001b[31m    // If refresh_on_load is true, trigger dashboard refresh\u001b[39m\n\u001b[31m    if (window.appConfig.refresh_on_load === 'true') {\u001b[39m\n\u001b[31m      setTimeout(refreshAllTracking, 2000);\u001b[39m\n\u001b[31m    }·\u001b[39m\n\u001b[31m  } catch(e) {\u001b[39m\n\u001b[31m    setSyncStatus('err', 'Sync failed: ' + e.message);\u001b[39m\n\u001b[31m    if (!silent) showToast('Sync failed — check URL', 'err');\u001b[39m\n\u001b[31m    console.error('Sheets sync error:', e);\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction setSyncStatus(state, msg) {\u001b[39m\n\u001b[31m  const el = document.getElementById('syncStatus');\u001b[39m\n\u001b[31m  const icon = document.getElementById('syncIcon');\u001b[39m\n\u001b[31m  const msgEl = document.getElementById('syncMsg');\u001b[39m\n\u001b[31m  el.style.display = 'flex';\u001b[39m\n\u001b[31m  icon.textContent = state === 'ok' ? '✅' : state === 'err' ? '❌' : '⏳';\u001b[39m\n\u001b[31m  msgEl.textContent = msg;\u001b[39m\n\u001b[31m  el.style.borderColor = state === 'ok' ? 'rgba(34,197,94,.3)' : state === 'err' ? 'rgba(239,68,68,.3)' : 'rgba(234,179,8,.3)';\u001b[39m\n\u001b[31m  el.style.background = state === 'ok' ? 'rgba(34,197,94,.05)' : state === 'err' ? 'rgba(239,68,68,.05)' : 'rgba(234,179,8,.05)';\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction renderSheetConfigDisplay(cfg) {\u001b[39m\n\u001b[31m  const display = document.getElementById('sheetConfigDisplay');\u001b[39m\n\u001b[31m  display.style.display = 'block';\u001b[39m\n\u001b[31m  document.getElementById('sheetSyncTime').textContent = 'Last sync: ' + new Date().toLocaleTimeString();\u001b[39m\n\u001b[31m  const showKeys = ['proxy_url','webhook_url','ollama_url','ocr_mode','company','city','auto_log'];\u001b[39m\n\u001b[31m  document.getElementById('sheetConfigRows').innerHTML = showKeys.map(k => {\u001b[39m\n\u001b[31m    const val = cfg[k] || window.appConfig[k] || '—';\u001b[39m\n\u001b[31m    const isEmpty = !cfg[k];\u001b[39m\n\u001b[31m    return `<div class=\\\"courier-master-row\\\">\u001b[39m\n\u001b[31m      <div class=\\\"cml\\\">\u001b[39m\n\u001b[31m        <div class=\\\"cml-name\\\" style=\\\"font-family:var(--fnt-mono);font-size:.75rem;\\\">${k}</div>\u001b[39m\n\u001b[31m        <div class=\\\"cml-url\\\" style=\\\"${isEmpty ? 'color:var(--red);' : ''}\\\">${isEmpty ? '⚠️ Not set in sheet' : val}</div>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m    </div>`;\u001b[39m\n\u001b[31m  }).join('');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m/**\u001b[39m\n\u001b[31m * Push current config changes back to the sheet.\u001b[39m\n\u001b[31m * Called when user edits config from PWA (rare — usually done in sheet directly).\u001b[39m\n\u001b[31m */\u001b[39m\n\u001b[31masync function pushConfigToSheet(updates) {\u001b[39m\n\u001b[31m  const url = window.appConfig.sheets_url;\u001b[39m\n\u001b[31m  if (!url) return;\u001b[39m\n\u001b[31m  try {\u001b[39m\n\u001b[31m    await fetch(url, {\u001b[39m\n\u001b[31m      method: 'POST', mode: 'no-cors',\u001b[39m\n\u001b[31m      headers: { 'Content-Type': 'application/json' },\u001b[39m\n\u001b[31m      body: JSON.stringify({ type: 'save_config', config: updates })\u001b[39m\n\u001b[31m    });\u001b[39m\n\u001b[31m  } catch(e) {}\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction clearAll() {\u001b[39m\n\u001b[31m  if (!confirm('Delete ALL dispatch data?')) return;\u001b[39m\n\u001b[31m  dispatches = [];\u001b[39m\n\u001b[31m  saveData();\u001b[39m\n\u001b[31m  updateHistCount();\u001b[39m\n\u001b[31m  renderDashboard();\u001b[39m\n\u001b[31m  renderHistory();\u001b[39m\n\u001b[31m  showToast('Cleared', 'ok');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction renderCourierMasterList() {\u001b[39m\n\u001b[31m  const el = document.getElementById('courierMasterList');\u001b[39m\n\u001b[31m  el.innerHTML = Object.entries(COURIER_MASTER).map(([name, info]) => `\u001b[39m\n\u001b[31m    <div class=\\\"courier-master-row\\\">\u001b[39m\n\u001b[31m      <div class=\\\"cml\\\">\u001b[39m\n\u001b[31m        <div class=\\\"cml-name\\\">${name}</div>\u001b[39m\n\u001b[31m        <div class=\\\"cml-url\\\">${info.trackUrl}</div>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      <button class=\\\"btn-sm\\\" onclick=\\\"window.open('${info.trackBulkUrl||info.trackUrl.replace('{docket}','')}','_blank')\\\">Open</button>\u001b[39m\n\u001b[31m    </div>`).join('');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction openCourierMaster() {\u001b[39m\n\u001b[31m  const ed = document.getElementById('courierMasterEditor');\u001b[39m\n\u001b[31m  ed.innerHTML = Object.entries(COURIER_MASTER).map(([name, info]) => `\u001b[39m\n\u001b[31m    <div style=\\\"margin-bottom:14px;\\\">\u001b[39m\n\u001b[31m      <div style=\\\"font-family:var(--fnt-head);font-size:.75rem;font-weight:700;color:var(--t2);text-transform:uppercase;letter-spacing:1.5px;margin-bottom:6px;\\\">${name}</div>\u001b[39m\n\u001b[31m      <div class=\\\"f\\\" style=\\\"margin-bottom:6px;\\\">\u001b[39m\n\u001b[31m        <label>Track URL (use {docket})</label>\u001b[39m\n\u001b[31m        <input type=\\\"text\\\" value=\\\"${info.trackUrl}\\\" id=\\\"cm_${name.replace(/\\\\s/g,'_')}\\\"\u001b[39m\n\u001b[31m          style=\\\"font-family:var(--fnt-mono);font-size:.75rem;\\\">\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m    </div>`).join('') +\u001b[39m\n\u001b[31m    `<button class=\\\"btn btn-orange\\\" onclick=\\\"saveCourierMaster()\\\">Save Courier Master</button>`;\u001b[39m\n\u001b[31m  openOverlay('courierMasterOverlay');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction saveCourierMaster() {\u001b[39m\n\u001b[31m  Object.keys(COURIER_MASTER).forEach(name => {\u001b[39m\n\u001b[31m    const el = document.getElementById('cm_' + name.replace(/\\\\s/g,'_'));\u001b[39m\n\u001b[31m    if (el) COURIER_MASTER[name].trackUrl = el.value;\u001b[39m\n\u001b[31m  });\u001b[39m\n\u001b[31m  closeOverlay('courierMasterOverlay');\u001b[39m\n\u001b[31m  renderCourierMasterList();\u001b[39m\n\u001b[31m  showToast('Courier master saved ✓', 'ok');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31m// UI UTILS\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31m// ── ADMIN AUTH ───────────────────────────────────────────────────────────\u001b[39m\n\u001b[31mlet _adminAuthed = false;\u001b[39m\n\u001b[31mlet _lastAdminPane = null;·\u001b[39m\n\u001b[31mfunction showPane(name) {\u001b[39m\n\u001b[31m  // Config tab requires admin auth — re-ask if navigated away\u001b[39m\n\u001b[31m  if (name === 'config') {\u001b[39m\n\u001b[31m    if (!_adminAuthed) {\u001b[39m\n\u001b[31m      _lastAdminPane = 'config';\u001b[39m\n\u001b[31m      showAdminLogin();\u001b[39m\n\u001b[31m      return;\u001b[39m\n\u001b[31m    }\u001b[39m\n\u001b[31m  } else {\u001b[39m\n\u001b[31m    // User navigated away from config — revoke auth\u001b[39m\n\u001b[31m    if (_lastAdminPane === 'config') _adminAuthed = false;\u001b[39m\n\u001b[31m    _lastAdminPane = name;\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m  document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));\u001b[39m\n\u001b[31m  document.querySelectorAll('.pane').forEach(p => p.classList.remove('active'));\u001b[39m\n\u001b[31m  document.getElementById('tab-'+name).classList.add('active');\u001b[39m\n\u001b[31m  document.getElementById('pane-'+name).classList.add('active');\u001b[39m\n\u001b[31m  if (name === 'dashboard') renderDashboard();\u001b[39m\n\u001b[31m  if (name === 'history') renderHistory();\u001b[39m\n\u001b[31m  if (name === 'track') buildTrackDocketChips();\u001b[39m\n\u001b[31m  if (name === 'config') { document.getElementById('cfg_sheets_url').value = window.appConfig.sheets_url || ''; }\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction showAdminLogin() {\u001b[39m\n\u001b[31m  // Password replaced by hub identity — only harish/pramod can access config\u001b[39m\n\u001b[31m  if (['harish', 'pramod'].includes(window._hubUser?.id)) {\u001b[39m\n\u001b[31m    _adminAuthed = true;\u001b[39m\n\u001b[31m    showPane('config');\u001b[39m\n\u001b[31m  } else {\u001b[39m\n\u001b[31m    showToast('Config access restricted to admin users', 'err');\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31masync function submitAdminLogin() {\u001b[39m\n\u001b[31m  showAdminLogin(); // no-op shim — kept for any lingering HTML onclick refs\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31mfunction openOverlay(id) { document.getElementById(id).classList.add('open'); }\u001b[39m\n\u001b[31mfunction closeOverlay(id) { document.getElementById(id).classList.remove('open'); }·\u001b[39m\n\u001b[31mlet toastTimer;\u001b[39m\n\u001b[31mfunction showToast(msg, type='') {\u001b[39m\n\u001b[31m  const t = document.getElementById('toast');\u001b[39m\n\u001b[31m  t.textContent = msg;\u001b[39m\n\u001b[31m  t.className = 'toast show' + (type ? ' '+type : '');\u001b[39m\n\u001b[31m  clearTimeout(toastTimer);\u001b[39m\n\u001b[31m  toastTimer = setTimeout(() => t.classList.remove('show'), 2800);\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction showOCRWorking(show, msg='') {\u001b[39m\n\u001b[31m  const el = document.getElementById('ocrWorking');\u001b[39m\n\u001b[31m  el.classList.toggle('visible', show);\u001b[39m\n\u001b[31m  if (msg) document.getElementById('ocrWorkingText').textContent = msg;\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction showAIProgress(label, pct) {\u001b[39m\n\u001b[31m  const el = document.getElementById('aiProgress');\u001b[39m\n\u001b[31m  el.classList.add('visible');\u001b[39m\n\u001b[31m  document.getElementById('aiProgLabel').textContent = label;\u001b[39m\n\u001b[31m  document.getElementById('aiProgFill').style.width = pct + '%';\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31mfunction hideAIProgress() {\u001b[39m\n\u001b[31m  document.getElementById('aiProgress').classList.remove('visible');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction updateNetDot() {\u001b[39m\n\u001b[31m  document.getElementById('netDot').style.background = navigator.onLine ? 'var(--grn)' : 'var(--red)';\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction setToday() {\u001b[39m\n\u001b[31m  document.getElementById('n_date').value = new Date().toISOString().split('T')[0];\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m// ── EXPORT CLEAN SHEET ───────────────────────────────────────────────────\u001b[39m\n\u001b[31mfunction exportCleanSheet() {\u001b[39m\n\u001b[31m  const pane = document.getElementById('verifyPane');\u001b[39m\n\u001b[31m  const d = pane._pendingDispatch;\u001b[39m\n\u001b[31m  if (!d || !d.consignees?.length) { showToast('No data to export', 'err'); return; }·\u001b[39m\n\u001b[31m  // Build CSV\u001b[39m\n\u001b[31m  const headers = ['S.No.','Store Name','City','Pincode','Phone','Address','Docket No.','Material','Courier','Status'];\u001b[39m\n\u001b[31m  const rows = d.consignees.map((c, i) => [\u001b[39m\n\u001b[31m    i + 1,\u001b[39m\n\u001b[31m    `\\\"${(c.name||'').replace(/\\\"/g,'\\\"\\\"')}\\\"`,\u001b[39m\n\u001b[31m    `\\\"${(c.city||'').replace(/\\\"/g,'\\\"\\\"')}\\\"`,\u001b[39m\n\u001b[31m    c.pincode || '',\u001b[39m\n\u001b[31m    c.phone || '',\u001b[39m\n\u001b[31m    `\\\"${(c.address||'').replace(/\\\"/g,'\\\"\\\"')}\\\"`,\u001b[39m\n\u001b[31m    c.docket || '',\u001b[39m\n\u001b[31m    `\\\"${(c.material||'').replace(/\\\"/g,'\\\"\\\"')}\\\"`,\u001b[39m\n\u001b[31m    d.courier || '',\u001b[39m\n\u001b[31m    c.status || 'pending'\u001b[39m\n\u001b[31m  ]);·\u001b[39m\n\u001b[31m  const csv = [headers.join(','), ...rows.map(r => r.join(','))].join('\\\\n');\u001b[39m\n\u001b[31m  const blob = new Blob([csv], { type: 'text/csv' });\u001b[39m\n\u001b[31m  const url = URL.createObjectURL(blob);\u001b[39m\n\u001b[31m  const a = document.createElement('a');\u001b[39m\n\u001b[31m  a.href = url;\u001b[39m\n\u001b[31m  a.download = `360LM_Dispatch_${d.client||'export'}_${d.dispatchDate||new Date().toISOString().slice(0,10)}_clean.csv`;\u001b[39m\n\u001b[31m  a.click();\u001b[39m\n\u001b[31m  URL.revokeObjectURL(url);\u001b[39m\n\u001b[31m  showToast('Clean sheet exported ✓', 'ok');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// ── DOCKET SLIP UPLOAD — SKIP OCR IF DOCKET ALREADY MAPPED ───────────────\u001b[39m\n\u001b[31mfunction shouldSkipOCR() {\u001b[39m\n\u001b[31m  // If any consignee already has a docket (from store list mapping), skip OCR extraction\u001b[39m\n\u001b[31m  const consList = document.getElementById('consList');\u001b[39m\n\u001b[31m  if (!consList) return false;\u001b[39m\n\u001b[31m  const cards = consList.querySelectorAll('.cons-card');\u001b[39m\n\u001b[31m  let hasAnyDocket = false;\u001b[39m\n\u001b[31m  cards.forEach(card => {\u001b[39m\n\u001b[31m    const d = card.querySelector('.cons-docket-input')?.value?.trim();\u001b[39m\n\u001b[31m    if (d) hasAnyDocket = true;\u001b[39m\n\u001b[31m  });\u001b[39m\n\u001b[31m  return hasAnyDocket;\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// ─── TRACKING UPDATE TO SHEETS ──────────────────────────────────────────\u001b[39m\n\u001b[31masync function logTrackingUpdate(docket, status, lastEvent, location) {\u001b[39m\n\u001b[31m  const url = window.appConfig.webhook_url;\u001b[39m\n\u001b[31m  if (!url) return;\u001b[39m\n\u001b[31m  try {\u001b[39m\n\u001b[31m    await fetch(url, {\u001b[39m\n\u001b[31m      method: 'POST', mode: 'no-cors',\u001b[39m\n\u001b[31m      headers: { 'Content-Type': 'application/json' },\u001b[39m\n\u001b[31m      body: JSON.stringify({\u001b[39m\n\u001b[31m        type: 'tracking_update',\u001b[39m\n\u001b[31m        docket, status, lastEvent, location,\u001b[39m\n\u001b[31m        timestamp: new Date().toISOString()\u001b[39m\n\u001b[31m      })\u001b[39m\n\u001b[31m    });\u001b[39m\n\u001b[31m  } catch(e) {}\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// ─── POD / RECIPIENT PHOTO UPLOAD ───────────────────────────────────────\u001b[39m\n\u001b[31m// Stores base64 locally, pushes to Sheets webhook (Apps Script saves to Drive)\u001b[39m\n\u001b[31mfunction showPODUpload(docket, dispatchId) {\u001b[39m\n\u001b[31m  const overlay = document.getElementById('podOverlay');\u001b[39m\n\u001b[31m  overlay.dataset.docket = docket;\u001b[39m\n\u001b[31m  overlay.dataset.dispatchId = dispatchId;\u001b[39m\n\u001b[31m  document.getElementById('podPreview').innerHTML = '';\u001b[39m\n\u001b[31m  document.getElementById('podFileInput').value = '';\u001b[39m\n\u001b[31m  document.getElementById('recipientFileInput').value = '';\u001b[39m\n\u001b[31m  openOverlay('podOverlay');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31masync function uploadPODFiles() {\u001b[39m\n\u001b[31m  const overlay = document.getElementById('podOverlay');\u001b[39m\n\u001b[31m  const docket = overlay.dataset.docket;\u001b[39m\n\u001b[31m  const dispatchId = overlay.dataset.dispatchId;\u001b[39m\n\u001b[31m  const podFile = document.getElementById('podFileInput').files[0];\u001b[39m\n\u001b[31m  const recipFile = document.getElementById('recipientFileInput').files[0];\u001b[39m\n\u001b[31m  if (!podFile && !recipFile) { showToast('Select at least one photo', 'err'); return; }·\u001b[39m\n\u001b[31m  showToast('Uploading...', 'info');\u001b[39m\n\u001b[31m  const toBase64 = f => new Promise((res,rej) => {\u001b[39m\n\u001b[31m    const r = new FileReader(); r.onload = e => res(e.target.result); r.onerror = rej; r.readAsDataURL(f);\u001b[39m\n\u001b[31m  });·\u001b[39m\n\u001b[31m  const payload = {\u001b[39m\n\u001b[31m    type: 'pod_upload',\u001b[39m\n\u001b[31m    docket, dispatchId,\u001b[39m\n\u001b[31m    timestamp: new Date().toISOString()\u001b[39m\n\u001b[31m  };\u001b[39m\n\u001b[31m  if (podFile)   payload.pod_image = await toBase64(podFile);\u001b[39m\n\u001b[31m  if (recipFile) payload.recipient_image = await toBase64(recipFile);·\u001b[39m\n\u001b[31m  // Update local store\u001b[39m\n\u001b[31m  for (const d of dispatches) {\u001b[39m\n\u001b[31m    if (d.id !== dispatchId) continue;\u001b[39m\n\u001b[31m    for (const c of d.consignees) {\u001b[39m\n\u001b[31m      if (c.docket === docket) {\u001b[39m\n\u001b[31m        c.podUploaded = true;\u001b[39m\n\u001b[31m        c.status = 'delivered';\u001b[39m\n\u001b[31m      }\u001b[39m\n\u001b[31m    }\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m  saveData();·\u001b[39m\n\u001b[31m  // Push to Sheets (Apps Script saves images to Drive, stores URL in sheet)\u001b[39m\n\u001b[31m  if (window.appConfig.webhook_url) {\u001b[39m\n\u001b[31m    try {\u001b[39m\n\u001b[31m      await fetch(window.appConfig.webhook_url, {\u001b[39m\n\u001b[31m        method: 'POST', mode: 'no-cors',\u001b[39m\n\u001b[31m        headers: { 'Content-Type': 'application/json' },\u001b[39m\n\u001b[31m        body: JSON.stringify(payload)\u001b[39m\n\u001b[31m      });\u001b[39m\n\u001b[31m    } catch(e) {}\u001b[39m\n\u001b[31m  }·\u001b[39m\n\u001b[31m  closeOverlay('podOverlay');\u001b[39m\n\u001b[31m  renderDashboard();\u001b[39m\n\u001b[31m  showToast('POD uploaded ✓ — marked as delivered', 'ok');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// ─── WHATSAPP DISPATCH TABLE SHARE ──────────────────────────────────────\u001b[39m\n\u001b[31mfunction shareDispatchToWhatsApp(dispatchId) {\u001b[39m\n\u001b[31m  const d = dispatches.find(x => x.id === dispatchId);\u001b[39m\n\u001b[31m  if (!d) return;\u001b[39m\n\u001b[31m  const lines = [\u001b[39m\n\u001b[31m    `📦 *Dispatch Update — ${d.client} ${d.jobref ? '› '+d.jobref : ''}*`,\u001b[39m\n\u001b[31m    `🚚 Courier: ${d.courier} | Date: ${d.dispatchDate || d.createdAt?.slice(0,10)}`,\u001b[39m\n\u001b[31m    ``,\u001b[39m\n\u001b[31m    ...d.consignees.filter(c => c.docket).map((c,i) =>\u001b[39m\n\u001b[31m      `${i+1}. *${c.name}* — ${c.city}\\\\n   Docket: ${c.docket} | Status: ${c.status||'booked'}`\u001b[39m\n\u001b[31m    ),\u001b[39m\n\u001b[31m    ``,\u001b[39m\n\u001b[31m    `_360 Degree Logical Marketing_`\u001b[39m\n\u001b[31m  ];\u001b[39m\n\u001b[31m  const msg = encodeURIComponent(lines.join('\\\\n'));\u001b[39m\n\u001b[31m  window.open('https://wa.me/?text=' + msg, '_blank');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// Trigger n8n webhook for automated WhatsApp/email share\u001b[39m\n\u001b[31masync function triggerN8nShare(dispatchId, channel) {\u001b[39m\n\u001b[31m  const d = dispatches.find(x => x.id === dispatchId);\u001b[39m\n\u001b[31m  if (!d) return;\u001b[39m\n\u001b[31m  const n8nUrl = window.appConfig.n8n_dispatch_webhook;\u001b[39m\n\u001b[31m  if (!n8nUrl) { showToast('n8n webhook not configured in Config sheet', 'err'); return; }\u001b[39m\n\u001b[31m  try {\u001b[39m\n\u001b[31m    await fetch(n8nUrl, {\u001b[39m\n\u001b[31m      method: 'POST',\u001b[39m\n\u001b[31m      headers: { 'Content-Type': 'application/json' },\u001b[39m\n\u001b[31m      body: JSON.stringify({\u001b[39m\n\u001b[31m        channel, // 'whatsapp' or 'email'\u001b[39m\n\u001b[31m        dispatch: d,\u001b[39m\n\u001b[31m        timestamp: new Date().toISOString()\u001b[39m\n\u001b[31m      })\u001b[39m\n\u001b[31m    });\u001b[39m\n\u001b[31m    showToast(`Sent to n8n for ${channel} delivery ✓`, 'ok');\u001b[39m\n\u001b[31m  } catch(e) {\u001b[39m\n\u001b[31m    showToast('n8n webhook failed', 'err');\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction previewPOD(input, type) {\u001b[39m\n\u001b[31m  const f = input.files[0];\u001b[39m\n\u001b[31m  if (!f) return;\u001b[39m\n\u001b[31m  const r = new FileReader();\u001b[39m\n\u001b[31m  r.onload = e => {\u001b[39m\n\u001b[31m    const prev = document.getElementById('podPreview');\u001b[39m\n\u001b[31m    const existing = document.getElementById('pod_prev_' + type);\u001b[39m\n\u001b[31m    if (existing) existing.remove();\u001b[39m\n\u001b[31m    const img = document.createElement('img');\u001b[39m\n\u001b[31m    img.id = 'pod_prev_' + type;\u001b[39m\n\u001b[31m    img.src = e.target.result;\u001b[39m\n\u001b[31m    img.style.cssText = 'width:80px;height:80px;object-fit:cover;border-radius:6px;border:1px solid var(--bdr);';\u001b[39m\n\u001b[31m    prev.appendChild(img);\u001b[39m\n\u001b[31m  };\u001b[39m\n\u001b[31m  r.readAsDataURL(f);\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// ─── MANUAL TRACKING ─────────────────────────────────────────────────────\u001b[39m\n\u001b[31mfunction showManualTracking(docket, dispatchId, storeName, phone, city) {\u001b[39m\n\u001b[31m  const ov = document.getElementById('manualTrackOverlay');\u001b[39m\n\u001b[31m  ov.dataset.docket = docket;\u001b[39m\n\u001b[31m  ov.dataset.dispatchId = dispatchId;\u001b[39m\n\u001b[31m  ov.dataset.storeName = storeName;\u001b[39m\n\u001b[31m  ov.dataset.city = city;·\u001b[39m\n\u001b[31m  document.getElementById('mtDocketDisplay').textContent =\u001b[39m\n\u001b[31m    docket + '  ·  ' + storeName + '  ·  ' + city;\u001b[39m\n\u001b[31m  document.getElementById('mtContactUsed').value = phone || '';\u001b[39m\n\u001b[31m  document.getElementById('mtContactPerson').value = '';\u001b[39m\n\u001b[31m  document.getElementById('mtRemarks').value = '';\u001b[39m\n\u001b[31m  document.getElementById('mtStatus').value = '';\u001b[39m\n\u001b[31m  document.getElementById('mtCalledBy').value = '';·\u001b[39m\n\u001b[31m  // Set current datetime\u001b[39m\n\u001b[31m  const now = new Date();\u001b[39m\n\u001b[31m  const pad = n => String(n).padStart(2,'0');\u001b[39m\n\u001b[31m  document.getElementById('mtCallTime').value =\u001b[39m\n\u001b[31m    `${now.getFullYear()}-${pad(now.getMonth()+1)}-${pad(now.getDate())}T${pad(now.getHours())}:${pad(now.getMinutes())}`;·\u001b[39m\n\u001b[31m  openOverlay('manualTrackOverlay');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31masync function submitManualTracking() {\u001b[39m\n\u001b[31m  const ov = document.getElementById('manualTrackOverlay');\u001b[39m\n\u001b[31m  const docket     = ov.dataset.docket;\u001b[39m\n\u001b[31m  const dispatchId = ov.dataset.dispatchId;\u001b[39m\n\u001b[31m  const status     = document.getElementById('mtStatus').value;\u001b[39m\n\u001b[31m  const calledBy   = document.getElementById('mtCalledBy').value.trim();·\u001b[39m\n\u001b[31m  if (!status)    { showToast('Select a status', 'err'); return; }\u001b[39m\n\u001b[31m  if (!calledBy)  { showToast('Enter your name', 'err'); return; }·\u001b[39m\n\u001b[31m  const payload = {\u001b[39m\n\u001b[31m    type:          'manual_tracking',\u001b[39m\n\u001b[31m    docket,        dispatchId,\u001b[39m\n\u001b[31m    storeName:     ov.dataset.storeName,\u001b[39m\n\u001b[31m    city:          ov.dataset.city,\u001b[39m\n\u001b[31m    manualStatus:  status,\u001b[39m\n\u001b[31m    calledBy,\u001b[39m\n\u001b[31m    contactUsed:   document.getElementById('mtContactUsed').value.trim(),\u001b[39m\n\u001b[31m    contactPerson: document.getElementById('mtContactPerson').value.trim(),\u001b[39m\n\u001b[31m    remarks:       document.getElementById('mtRemarks').value.trim(),\u001b[39m\n\u001b[31m    callTime:      document.getElementById('mtCallTime').value || new Date().toISOString(),\u001b[39m\n\u001b[31m    timestamp:     new Date().toISOString()\u001b[39m\n\u001b[31m  };·\u001b[39m\n\u001b[31m  // Update local dispatch record\u001b[39m\n\u001b[31m  for (const d of dispatches) {\u001b[39m\n\u001b[31m    if (d.id !== dispatchId) continue;\u001b[39m\n\u001b[31m    for (const c of d.consignees) {\u001b[39m\n\u001b[31m      if (c.docket === docket) {\u001b[39m\n\u001b[31m        c.manualStatus    = status;\u001b[39m\n\u001b[31m        c.manualCalledBy  = calledBy;\u001b[39m\n\u001b[31m        c.manualTime      = payload.callTime;\u001b[39m\n\u001b[31m        c.manualRemarks   = payload.remarks;\u001b[39m\n\u001b[31m        // If confirmed delivered — flag so dashboard shows manual badge\u001b[39m\n\u001b[31m        // (not permanently locked — use resumeAutoTracking() to re-enable)\u001b[39m\n\u001b[31m        if (status === 'delivered') {\u001b[39m\n\u001b[31m          c.status = 'delivered';\u001b[39m\n\u001b[31m          c.manualDelivered = true;\u001b[39m\n\u001b[31m        } else {\u001b[39m\n\u001b[31m          // Non-delivered manual update: clear any previous lock\u001b[39m\n\u001b[31m          c.manualDelivered = false;\u001b[39m\n\u001b[31m        }\u001b[39m\n\u001b[31m      }\u001b[39m\n\u001b[31m    }\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m  saveData();\u001b[39m\n\u001b[31m  renderDashboard();\u001b[39m\n\u001b[31m  // Sync updated consignee to Postgres\u001b[39m\n\u001b[31m  const _d = dispatches.find(x => x.id === dispatchId);\u001b[39m\n\u001b[31m  if (_d) {\u001b[39m\n\u001b[31m    const _idx = _d.consignees.findIndex(c => c.docket === docket);\u001b[39m\n\u001b[31m    if (_idx >= 0) syncConsigneeToDB(_d.consignees[_idx], dispatchId, _idx);\u001b[39m\n\u001b[31m  }·\u001b[39m\n\u001b[31m  // Log to Sheets\u001b[39m\n\u001b[31m  if (window.appConfig.webhook_url) {\u001b[39m\n\u001b[31m    try {\u001b[39m\n\u001b[31m      await fetch(window.appConfig.webhook_url, {\u001b[39m\n\u001b[31m        method: 'POST', mode: 'no-cors',\u001b[39m\n\u001b[31m        headers: { 'Content-Type': 'application/json' },\u001b[39m\n\u001b[31m        body: JSON.stringify(payload)\u001b[39m\n\u001b[31m      });\u001b[39m\n\u001b[31m    } catch(e) {}\u001b[39m\n\u001b[31m  }·\u001b[39m\n\u001b[31m  closeOverlay('manualTrackOverlay');\u001b[39m\n\u001b[31m  showToast('Manual tracking logged ✓', 'ok');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// ─── TILE SELECTION + PARTIAL REPORT SHARE ───────────────────────────────\u001b[39m\n\u001b[31mlet _selectedDockets = new Set();·\u001b[39m\n\u001b[31mfunction toggleTileSelect(docket) {\u001b[39m\n\u001b[31m  if (_selectedDockets.has(docket)) _selectedDockets.delete(docket);\u001b[39m\n\u001b[31m  else _selectedDockets.add(docket);\u001b[39m\n\u001b[31m  // Update checkbox visual\u001b[39m\n\u001b[31m  const chk = document.getElementById('sel_' + docket.replace(/[^a-z0-9]/gi,'_'));\u001b[39m\n\u001b[31m  if (chk) chk.checked = _selectedDockets.has(docket);\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction shareSelectedReport() {\u001b[39m\n\u001b[31m  if (_selectedDockets.size === 0) {\u001b[39m\n\u001b[31m    showToast('Select at least one consignee tile first', 'err');\u001b[39m\n\u001b[31m    return;\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m  const activeDis = dispatches.filter(d => d.status !== 'draft');\u001b[39m\n\u001b[31m  const allItems  = activeDis.flatMap(d =>\u001b[39m\n\u001b[31m    (d.consignees||[]).map(c => Object.assign({}, c, {courier:d.courier, client:d.client, jobref:d.jobref, dispatchId:d.id}))\u001b[39m\n\u001b[31m  );\u001b[39m\n\u001b[31m  const selected = allItems.filter(c => _selectedDockets.has(c.docket));\u001b[39m\n\u001b[31m  if (!selected.length) { showToast('Selected dockets not found', 'err'); return; }·\u001b[39m\n\u001b[31m  const NL = '\\\\n';\u001b[39m\n\u001b[31m  const statusEmoji = {\u001b[39m\n\u001b[31m    delivered:'\\\\u2705', transit:'\\\\uD83D\\\\uDE9A', 'in-transit':'\\\\uD83D\\\\uDE9A',\u001b[39m\n\u001b[31m    booked:'\\\\uD83D\\\\uDCEC', pending:'\\\\u23F3', 'not received':'\\\\u274C', forwarded:'\\\\u27A1'\u001b[39m\n\u001b[31m  };·\u001b[39m\n\u001b[31m  const header = '\\\\uD83D\\\\uDCE6 *Dispatch Status Report*' + NL +\u001b[39m\n\u001b[31m    (selected[0].client || '') + ' ' + (selected[0].jobref ? '> ' + selected[0].jobref : '') + NL +\u001b[39m\n\u001b[31m    new Date().toLocaleDateString('en-IN', {day:'2-digit',month:'short',year:'numeric'}) + NL + NL;·\u001b[39m\n\u001b[31m  const rows = selected.map(function(c, i) {\u001b[39m\n\u001b[31m    const finalStatus = c.manualStatus === 'delivered' ? 'delivered' : (c.status || 'booked');\u001b[39m\n\u001b[31m    const emoji  = statusEmoji[finalStatus] || '\\\\uD83D\\\\uDCE6';\u001b[39m\n\u001b[31m    const source = c.manualStatus ? '(manual)' : '(auto)';\u001b[39m\n\u001b[31m    const remark = c.manualRemarks ? (NL + '   Note: ' + c.manualRemarks) : '';\u001b[39m\n\u001b[31m    return (i+1) + '. *' + c.name + '* - ' + c.city + NL +\u001b[39m\n\u001b[31m      '   ' + emoji + ' ' + finalStatus + ' ' + source + NL +\u001b[39m\n\u001b[31m      '   Docket: ' + c.docket + remark;\u001b[39m\n\u001b[31m  }).join(NL + NL);·\u001b[39m\n\u001b[31m  const footer = NL + NL + '_360 Degree Logical Marketing_';\u001b[39m\n\u001b[31m  window.open('https://wa.me/?text=' + encodeURIComponent(header + rows + footer), '_blank');\u001b[39m\n\u001b[31m  showToast('Report for ' + selected.length + ' consignees ready', 'ok');\u001b[39m\n\u001b[31m}··\u001b[39m\n\u001b[31m// ── resumeAutoTracking: undo a manual-delivered lock ──────────────────────\u001b[39m\n\u001b[31mfunction resumeAutoTracking(docket, dispatchId) {\u001b[39m\n\u001b[31m  for (const d of dispatches) {\u001b[39m\n\u001b[31m    if (d.id !== dispatchId) continue;\u001b[39m\n\u001b[31m    for (const c of d.consignees) {\u001b[39m\n\u001b[31m      if (c.docket === docket) {\u001b[39m\n\u001b[31m        c.manualDelivered = false;\u001b[39m\n\u001b[31m        c.status          = 'transit';\u001b[39m\n\u001b[31m        c.manualStatus    = '';\u001b[39m\n\u001b[31m      }\u001b[39m\n\u001b[31m    }\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m  saveData();\u001b[39m\n\u001b[31m  renderDashboard();\u001b[39m\n\u001b[31m  showToast(`Auto-tracking resumed for ${docket}`, 'ok');\u001b[39m\n\u001b[31m  const _rd = dispatches.find(x => x.id === dispatchId);\u001b[39m\n\u001b[31m  if (_rd) {\u001b[39m\n\u001b[31m    const _ri = _rd.consignees.findIndex(c => c.docket === docket);\u001b[39m\n\u001b[31m    if (_ri >= 0) syncConsigneeToDB(_rd.consignees[_ri], dispatchId, _ri);\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// ── refreshAllTracking (consolidated, try/finally for safety) ─────────────\u001b[39m\n\u001b[31masync function refreshAllTracking() {\u001b[39m\n\u001b[31m  const activeDis  = dispatches.filter(d => d.status !== 'draft');\u001b[39m\n\u001b[31m  const allCons    = activeDis.flatMap(d =>\u001b[39m\n\u001b[31m    (d.consignees || []).map(c => ({ ...c, dispatchId: d.id, courier: d.courier }))\u001b[39m\n\u001b[31m  );\u001b[39m\n\u001b[31m  const withDockets = allCons.filter(c =>\u001b[39m\n\u001b[31m    c.docket && c.status !== 'delivered' && !c.manualDelivered\u001b[39m\n\u001b[31m  );\u001b[39m\n\u001b[31m  if (!withDockets.length) { showToast('No active dockets to refresh', 'info'); return; }·\u001b[39m\n\u001b[31m  showToast(`⏳ Tracking ${withDockets.length} dockets...`, 'info');\u001b[39m\n\u001b[31m  withDockets.forEach(c => {\u001b[39m\n\u001b[31m    const id    = c.docket.replace(/[^a-z0-9]/gi, '_');\u001b[39m\n\u001b[31m    const evtEl = document.getElementById('evt_'  + id);\u001b[39m\n\u001b[31m    const spnEl = document.getElementById('spin_' + id);\u001b[39m\n\u001b[31m    if (evtEl) evtEl.style.display = 'none';\u001b[39m\n\u001b[31m    if (spnEl) spnEl.style.display = 'inline';\u001b[39m\n\u001b[31m  });·\u001b[39m\n\u001b[31m  let done = 0;\u001b[39m\n\u001b[31m  try {\u001b[39m\n\u001b[31m    for (const c of withDockets) {\u001b[39m\n\u001b[31m      const result = await fetchTracking(c.docket, c.courier || '');\u001b[39m\n\u001b[31m      const id    = c.docket.replace(/[^a-z0-9]/gi, '_');·\u001b[39m\n\u001b[31m      // Batch-write result into state (never touch DOM here)\u001b[39m\n\u001b[31m      if (result.status && result.status !== 'failed') {\u001b[39m\n\u001b[31m        for (const d of dispatches) {\u001b[39m\n\u001b[31m          if (d.id !== c.dispatchId) continue;\u001b[39m\n\u001b[31m          for (const con of d.consignees) {\u001b[39m\n\u001b[31m            if (con.docket === c.docket && !con.manualDelivered) {\u001b[39m\n\u001b[31m              con.status           = result.status;\u001b[39m\n\u001b[31m              con.lastEvent        = result.lastEvent || con.lastEvent;\u001b[39m\n\u001b[31m              con.lastScanLocation = result.lastScanLocation || '';\u001b[39m\n\u001b[31m              con.lastTrackedAt    = new Date().toISOString();\u001b[39m\n\u001b[31m            }\u001b[39m\n\u001b[31m          }\u001b[39m\n\u001b[31m        }\u001b[39m\n\u001b[31m      }·\u001b[39m\n\u001b[31m      // Update tile spinner/event text in DOM (cosmetic only)\u001b[39m\n\u001b[31m      const spnEl = document.getElementById('spin_' + id);\u001b[39m\n\u001b[31m      const evtEl = document.getElementById('evt_'  + id);\u001b[39m\n\u001b[31m      if (spnEl) spnEl.style.display = 'none';\u001b[39m\n\u001b[31m      if (evtEl) { evtEl.style.display = 'inline'; evtEl.textContent = result.lastEvent || 'Checked'; }·\u001b[39m\n\u001b[31m      if (result.status && result.status !== 'failed' && window.appConfig.webhook_url) {\u001b[39m\n\u001b[31m        logTrackingUpdate(c.docket, result.status, result.lastEvent || '', result.lastScanLocation || '');\u001b[39m\n\u001b[31m      }\u001b[39m\n\u001b[31m      done++;\u001b[39m\n\u001b[31m    }\u001b[39m\n\u001b[31m    showToast(`✓ ${done} dockets refreshed`, 'ok');\u001b[39m\n\u001b[31m  } finally {\u001b[39m\n\u001b[31m    // Always persist and re-render — even if loop throws mid-way\u001b[39m\n\u001b[31m    saveData();\u001b[39m\n\u001b[31m    renderDashboard();\u001b[39m\n\u001b[31m    // Push updated consignee states to Postgres (fire-and-forget)\u001b[39m\n\u001b[31m    dispatches.filter(d => d.status !== 'draft').forEach(d => syncDispatchToDB(d));\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m</script>\u001b[39m\n\u001b[31m</body>\u001b[39m\n\u001b[31m</html>\"\u001b[39m",
                        "stack": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoContain\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m) // indexOf\u001b[22m\n\nExpected substring: \u001b[32m\"window.location.replace('/hub/')\"\u001b[39m\nReceived string:    \u001b[31m\"<!DOCTYPE html>\u001b[39m\n\u001b[31m<html lang=\\\"en\\\">\u001b[39m\n\u001b[31m<head>\u001b[39m\n\u001b[31m<meta charset=\\\"UTF-8\\\">\u001b[39m\n\u001b[31m<meta name=\\\"viewport\\\" content=\\\"width=device-width,initial-scale=1,maximum-scale=1,user-scalable=no\\\">\u001b[39m\n\u001b[31m<meta name=\\\"theme-color\\\" content=\\\"#080c12\\\">\u001b[39m\n\u001b[31m<meta name=\\\"apple-mobile-web-app-capable\\\" content=\\\"yes\\\">\u001b[39m\n\u001b[31m<meta name=\\\"mobile-web-app-capable\\\" content=\\\"yes\\\">\u001b[39m\n\u001b[31m<title>360LM Dispatch</title>\u001b[39m\n\u001b[31m<link rel=\\\"manifest\\\" href=\\\"manifest.json\\\">\u001b[39m\n\u001b[31m<link href=\\\"https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@300;400;500;600;700&family=Fira+Code:wght@400;500&family=Barlow+Condensed:wght@400;600;700;800&display=swap\\\" rel=\\\"stylesheet\\\">\u001b[39m\n\u001b[31m<style>\u001b[39m\n\u001b[31m/* ════ RESET & VARS ════ */\u001b[39m\n\u001b[31m*{margin:0;padding:0;box-sizing:border-box;-webkit-tap-highlight-color:transparent;}\u001b[39m\n\u001b[31m:root{\u001b[39m\n\u001b[31m  --bg:#080c12; --bg2:#0d1420; --bg3:#111a28;\u001b[39m\n\u001b[31m  --sur:#141f30; --sur2:#1a2840; --sur3:#1f3050;\u001b[39m\n\u001b[31m  --bdr:#1e2e45; --bdr2:#253650;\u001b[39m\n\u001b[31m  --t1:#f0f4f8; --t2:#8ba4bc; --t3:#3d5470; --t4:#243348;\u001b[39m\n\u001b[31m  --acc:#f97316; --acc2:#fb923c; --acc-glow:rgba(249,115,22,0.25);\u001b[39m\n\u001b[31m  --grn:#22c55e; --grn-dim:rgba(34,197,94,0.15);\u001b[39m\n\u001b[31m  --sky:#38bdf8; --sky-dim:rgba(56,189,248,0.15);\u001b[39m\n\u001b[31m  --red:#ef4444; --red-dim:rgba(239,68,68,0.15);\u001b[39m\n\u001b[31m  --ylw:#eab308; --ylw-dim:rgba(234,179,8,0.15);\u001b[39m\n\u001b[31m  --fnt-head:'Barlow Condensed',sans-serif;\u001b[39m\n\u001b[31m  --fnt-body:'Space Grotesk',sans-serif;\u001b[39m\n\u001b[31m  --fnt-mono:'Fira Code',monospace;\u001b[39m\n\u001b[31m  --r:10px; --r-sm:6px; --r-lg:16px;\u001b[39m\n\u001b[31m  --sh:0 4px 24px rgba(0,0,0,0.4);\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31mhtml,body{height:100%;background:var(--bg);color:var(--t1);font-family:var(--fnt-body);overflow:hidden;}·\u001b[39m\n\u001b[31m/* ════ SPLASH ════ */\u001b[39m\n\u001b[31m#splash{\u001b[39m\n\u001b[31m  position:fixed;inset:0;background:var(--bg);z-index:999;\u001b[39m\n\u001b[31m  display:flex;flex-direction:column;align-items:center;justify-content:center;\u001b[39m\n\u001b[31m  transition:opacity .4s .1s;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.splash-logo{font-family:var(--fnt-head);font-size:3rem;font-weight:800;letter-spacing:2px;line-height:1;}\u001b[39m\n\u001b[31m.splash-logo em{color:var(--acc);font-style:normal;}\u001b[39m\n\u001b[31m.splash-sub{color:var(--t3);font-family:var(--fnt-mono);font-size:.7rem;letter-spacing:4px;margin-top:8px;}\u001b[39m\n\u001b[31m.splash-track{width:200px;height:2px;background:var(--bdr);border-radius:2px;margin-top:40px;overflow:hidden;}\u001b[39m\n\u001b[31m.splash-track-fill{height:100%;width:0;background:linear-gradient(90deg,var(--acc),var(--acc2));animation:splashLoad 1.3s cubic-bezier(.4,0,.2,1) forwards;}\u001b[39m\n\u001b[31m@keyframes splashLoad{to{width:100%;}}·\u001b[39m\n\u001b[31m/* ════ LAYOUT ════ */\u001b[39m\n\u001b[31m#app{height:100%;display:flex;flex-direction:column;}\u001b[39m\n\u001b[31m.hdr{\u001b[39m\n\u001b[31m  background:var(--bg2);border-bottom:1px solid var(--bdr);\u001b[39m\n\u001b[31m  padding:10px 14px;display:flex;align-items:center;gap:10px;flex-shrink:0;\u001b[39m\n\u001b[31m  position:relative;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.hdr-brand{font-family:var(--fnt-head);font-size:1.3rem;font-weight:800;letter-spacing:1px;flex:1;}\u001b[39m\n\u001b[31m.hdr-brand em{color:var(--acc);font-style:normal;}\u001b[39m\n\u001b[31m.hdr-dot{width:7px;height:7px;border-radius:50%;background:var(--grn);transition:background .3s;animation:blink 2s infinite;}\u001b[39m\n\u001b[31m@keyframes blink{0%,100%{opacity:1;}50%{opacity:.4;}}\u001b[39m\n\u001b[31m.hdr-btn{\u001b[39m\n\u001b[31m  background:none;border:none;color:var(--t2);cursor:pointer;\u001b[39m\n\u001b[31m  padding:6px 8px;border-radius:var(--r-sm);transition:all .2s;\u001b[39m\n\u001b[31m  font-family:var(--fnt-head);font-size:.85rem;font-weight:700;letter-spacing:.5px;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.hdr-btn:hover{background:var(--bdr);color:var(--t1);}\u001b[39m\n\u001b[31m.hdr-btn.primary{background:var(--acc);color:#fff;padding:6px 14px;}\u001b[39m\n\u001b[31m.hdr-btn.primary:hover{background:var(--acc2);}·\u001b[39m\n\u001b[31m/* ════ TAB BAR ════ */\u001b[39m\n\u001b[31m.tabs{\u001b[39m\n\u001b[31m  display:flex;background:var(--bg2);border-bottom:1px solid var(--bdr);\u001b[39m\n\u001b[31m  overflow-x:auto;flex-shrink:0;padding:0 6px;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.tabs::-webkit-scrollbar{display:none;}\u001b[39m\n\u001b[31m.tab{\u001b[39m\n\u001b[31m  background:none;border:none;color:var(--t3);font-family:var(--fnt-head);\u001b[39m\n\u001b[31m  font-size:.85rem;font-weight:700;letter-spacing:1px;padding:10px 14px;\u001b[39m\n\u001b[31m  cursor:pointer;border-bottom:2px solid transparent;white-space:nowrap;\u001b[39m\n\u001b[31m  transition:all .2s;flex-shrink:0;display:flex;align-items:center;gap:6px;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.tab.active{color:var(--acc);border-bottom-color:var(--acc);}\u001b[39m\n\u001b[31m.tab:hover:not(.active){color:var(--t2);}\u001b[39m\n\u001b[31m.tab-badge{\u001b[39m\n\u001b[31m  background:var(--acc);color:#fff;font-size:.6rem;font-weight:700;\u001b[39m\n\u001b[31m  padding:1px 5px;border-radius:10px;font-family:var(--fnt-mono);\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m/* ════ CONTENT AREAS ════ */\u001b[39m\n\u001b[31m.pane{flex:1;overflow-y:auto;padding:14px;display:none;}\u001b[39m\n\u001b[31m.pane.active{display:block;}\u001b[39m\n\u001b[31m.pane::-webkit-scrollbar{width:3px;}\u001b[39m\n\u001b[31m.pane::-webkit-scrollbar-thumb{background:var(--bdr2);}·\u001b[39m\n\u001b[31m/* ════ CARDS ════ */\u001b[39m\n\u001b[31m.card{\u001b[39m\n\u001b[31m  background:var(--sur);border:1px solid var(--bdr);border-radius:var(--r);\u001b[39m\n\u001b[31m  padding:14px;margin-bottom:12px;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.card.accent-left{border-left:3px solid var(--acc);}\u001b[39m\n\u001b[31m.card-title{\u001b[39m\n\u001b[31m  font-family:var(--fnt-head);font-size:.72rem;font-weight:700;\u001b[39m\n\u001b[31m  color:var(--t3);text-transform:uppercase;letter-spacing:2.5px;\u001b[39m\n\u001b[31m  margin-bottom:12px;display:flex;align-items:center;gap:8px;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.card-title .ct-dot{width:5px;height:5px;border-radius:50%;background:var(--acc);flex-shrink:0;}·\u001b[39m\n\u001b[31m/* ════ FORM ════ */\u001b[39m\n\u001b[31m.f{margin-bottom:12px;}\u001b[39m\n\u001b[31m.f label{\u001b[39m\n\u001b[31m  display:block;font-size:.68rem;color:var(--t3);margin-bottom:5px;\u001b[39m\n\u001b[31m  font-weight:600;text-transform:uppercase;letter-spacing:1.5px;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.f label .req{color:var(--acc);margin-left:2px;}\u001b[39m\n\u001b[31m.f input,.f select,.f textarea{\u001b[39m\n\u001b[31m  width:100%;background:var(--sur2);border:1px solid var(--bdr);\u001b[39m\n\u001b[31m  border-radius:var(--r-sm);color:var(--t1);font-family:var(--fnt-body);\u001b[39m\n\u001b[31m  font-size:.88rem;padding:9px 11px;outline:none;transition:border-color .2s;\u001b[39m\n\u001b[31m  -webkit-appearance:none;appearance:none;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.f input:focus,.f select:focus,.f textarea:focus{border-color:var(--acc);}\u001b[39m\n\u001b[31m.f input::placeholder{color:var(--t4);}\u001b[39m\n\u001b[31m.f textarea{resize:vertical;min-height:65px;}\u001b[39m\n\u001b[31m.f select{\u001b[39m\n\u001b[31m  background-image:url(\\\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='7'%3E%3Cpath d='M1 1l4 4 4-4' stroke='%233d5470' stroke-width='1.5' fill='none' stroke-linecap='round'/%3E%3C/svg%3E\\\");\u001b[39m\n\u001b[31m  background-repeat:no-repeat;background-position:right 10px center;padding-right:28px;cursor:pointer;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.row2{display:grid;grid-template-columns:1fr 1fr;gap:10px;}\u001b[39m\n\u001b[31m.row3{display:grid;grid-template-columns:1fr 1fr 1fr;gap:8px;}·\u001b[39m\n\u001b[31m/* ════ BUTTONS ════ */\u001b[39m\n\u001b[31m.btn{\u001b[39m\n\u001b[31m  width:100%;padding:12px;border:none;border-radius:var(--r);\u001b[39m\n\u001b[31m  font-family:var(--fnt-head);font-size:.9rem;font-weight:700;letter-spacing:.5px;\u001b[39m\n\u001b[31m  cursor:pointer;transition:all .2s;display:flex;align-items:center;justify-content:center;gap:8px;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.btn-orange{background:var(--acc);color:#fff;}\u001b[39m\n\u001b[31m.btn-orange:hover{background:var(--acc2);}\u001b[39m\n\u001b[31m.btn-ghost{background:var(--sur2);border:1px solid var(--bdr);color:var(--t1);}\u001b[39m\n\u001b[31m.btn-ghost:hover{border-color:var(--acc);color:var(--acc);}\u001b[39m\n\u001b[31m.btn-grn{background:var(--grn);color:#000;}\u001b[39m\n\u001b[31m.btn-grn:hover{opacity:.85;}\u001b[39m\n\u001b[31m.btn:active{transform:scale(.98);}\u001b[39m\n\u001b[31m.btn-row{display:grid;grid-template-columns:1fr 1fr;gap:10px;margin-top:6px;}\u001b[39m\n\u001b[31m.btn-sm{\u001b[39m\n\u001b[31m  background:none;border:1px solid var(--bdr);color:var(--t2);\u001b[39m\n\u001b[31m  font-size:.7rem;padding:5px 10px;border-radius:var(--r-sm);cursor:pointer;\u001b[39m\n\u001b[31m  font-family:var(--fnt-body);transition:all .2s;white-space:nowrap;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.btn-sm:hover{border-color:var(--acc);color:var(--acc);}\u001b[39m\n\u001b[31m.btn-sm.primary{background:var(--acc);border-color:var(--acc);color:#fff;}·\u001b[39m\n\u001b[31m/* ════ OCR UPLOAD ZONE ════ */\u001b[39m\n\u001b[31m.upload-zone{\u001b[39m\n\u001b[31m  border:2px dashed var(--bdr2);border-radius:var(--r);\u001b[39m\n\u001b[31m  padding:20px;text-align:center;cursor:pointer;transition:all .25s;\u001b[39m\n\u001b[31m  background:var(--sur2);position:relative;overflow:hidden;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.upload-zone:hover,.upload-zone.drag{border-color:var(--acc);background:rgba(249,115,22,.05);}\u001b[39m\n\u001b[31m.upload-zone .uz-icon{font-size:2rem;margin-bottom:8px;}\u001b[39m\n\u001b[31m.upload-zone .uz-title{font-family:var(--fnt-head);font-size:1rem;font-weight:700;color:var(--t1);}\u001b[39m\n\u001b[31m.upload-zone .uz-sub{font-size:.72rem;color:var(--t3);margin-top:4px;font-family:var(--fnt-mono);}\u001b[39m\n\u001b[31m.upload-zone input{display:none;}\u001b[39m\n\u001b[31m.upload-zone.share-target{border-color:var(--grn);}·\u001b[39m\n\u001b[31m/* ════ OCR RESULTS ════ */\u001b[39m\n\u001b[31m.ocr-results{display:none;}\u001b[39m\n\u001b[31m.ocr-results.visible{display:block;}\u001b[39m\n\u001b[31m.ocr-slip-list{display:flex;flex-direction:column;gap:8px;margin-top:10px;}\u001b[39m\n\u001b[31m.ocr-slip{\u001b[39m\n\u001b[31m  background:var(--sur2);border:1px solid var(--bdr);border-radius:var(--r-sm);\u001b[39m\n\u001b[31m  padding:10px 12px;display:flex;gap:10px;align-items:flex-start;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.ocr-slip img{width:60px;height:60px;object-fit:cover;border-radius:6px;flex-shrink:0;border:1px solid var(--bdr2);}\u001b[39m\n\u001b[31m.ocr-slip-body{flex:1;min-width:0;}\u001b[39m\n\u001b[31m.ocr-slip-name{font-size:.7rem;color:var(--t3);font-family:var(--fnt-mono);margin-bottom:4px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}\u001b[39m\n\u001b[31m.ocr-slip-docket{\u001b[39m\n\u001b[31m  font-family:var(--fnt-mono);font-size:.9rem;font-weight:500;color:var(--acc);\u001b[39m\n\u001b[31m  background:var(--bg);border:1px solid var(--bdr);border-radius:4px;\u001b[39m\n\u001b[31m  padding:4px 8px;width:100%;outline:none;transition:border-color .2s;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.ocr-slip-docket:focus{border-color:var(--acc);}\u001b[39m\n\u001b[31m.ocr-slip-status{font-size:.65rem;margin-top:4px;display:flex;align-items:center;gap:6px;}\u001b[39m\n\u001b[31m.ocr-confidence{padding:2px 6px;border-radius:10px;font-family:var(--fnt-mono);}\u001b[39m\n\u001b[31m.conf-high{background:var(--grn-dim);color:var(--grn);}\u001b[39m\n\u001b[31m.conf-med{background:var(--ylw-dim);color:var(--ylw);}\u001b[39m\n\u001b[31m.conf-low{background:var(--red-dim);color:var(--red);}\u001b[39m\n\u001b[31m.ocr-ai-btn{\u001b[39m\n\u001b[31m  background:rgba(56,189,248,.15);border:1px solid rgba(56,189,248,.3);\u001b[39m\n\u001b[31m  color:var(--sky);font-size:.65rem;padding:3px 8px;border-radius:10px;\u001b[39m\n\u001b[31m  cursor:pointer;font-family:var(--fnt-mono);transition:all .2s;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.ocr-ai-btn:hover{background:rgba(56,189,248,.3);}\u001b[39m\n\u001b[31m.ocr-working{\u001b[39m\n\u001b[31m  background:var(--ylw-dim);border:1px solid rgba(234,179,8,.3);\u001b[39m\n\u001b[31m  border-radius:var(--r-sm);padding:8px 12px;margin-top:8px;\u001b[39m\n\u001b[31m  font-size:.75rem;color:var(--ylw);font-family:var(--fnt-mono);\u001b[39m\n\u001b[31m  display:none;align-items:center;gap:8px;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.ocr-working.visible{display:flex;}\u001b[39m\n\u001b[31m.spin{animation:spin .8s linear infinite;display:inline-block;}\u001b[39m\n\u001b[31m@keyframes spin{to{transform:rotate(360deg);}}·\u001b[39m\n\u001b[31m/* ════ STORE LIST ════ */\u001b[39m\n\u001b[31m.store-upload-zone{\u001b[39m\n\u001b[31m  border:2px dashed var(--bdr2);border-radius:var(--r);padding:16px;\u001b[39m\n\u001b[31m  text-align:center;cursor:pointer;transition:all .25s;background:var(--sur2);\u001b[39m\n\u001b[31m  position:relative;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.store-upload-zone:hover{border-color:var(--sky);background:rgba(56,189,248,.05);}\u001b[39m\n\u001b[31m.store-upload-zone input{display:none;}\u001b[39m\n\u001b[31m.store-table-wrap{max-height:200px;overflow-y:auto;margin-top:10px;border-radius:var(--r-sm);overflow:hidden;}\u001b[39m\n\u001b[31m.store-table{width:100%;border-collapse:collapse;font-size:.75rem;}\u001b[39m\n\u001b[31m.store-table th{\u001b[39m\n\u001b[31m  background:var(--bg);color:var(--t3);font-family:var(--fnt-head);\u001b[39m\n\u001b[31m  font-size:.65rem;letter-spacing:1.5px;text-transform:uppercase;\u001b[39m\n\u001b[31m  padding:7px 8px;text-align:left;border-bottom:1px solid var(--bdr);\u001b[39m\n\u001b[31m  position:sticky;top:0;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.store-table td{\u001b[39m\n\u001b[31m  padding:6px 8px;border-bottom:1px solid var(--bdr);color:var(--t2);\u001b[39m\n\u001b[31m  font-family:var(--fnt-mono);font-size:.72rem;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.store-table tr:hover td{background:var(--sur3);}\u001b[39m\n\u001b[31m.store-table .matched{color:var(--grn);}\u001b[39m\n\u001b[31m.store-table .unmatched{color:var(--ylw);}\u001b[39m\n\u001b[31m.store-map-btn{\u001b[39m\n\u001b[31m  font-size:.65rem;background:none;border:1px solid var(--bdr);color:var(--t3);\u001b[39m\n\u001b[31m  padding:2px 6px;border-radius:4px;cursor:pointer;transition:all .2s;\u001b[39m\n\u001b[31m  font-family:var(--fnt-mono);\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.store-map-btn:hover{border-color:var(--acc);color:var(--acc);}·\u001b[39m\n\u001b[31m/* ════ CONSIGNEE CARDS ════ */\u001b[39m\n\u001b[31m.cons-list{display:flex;flex-direction:column;gap:8px;margin-bottom:10px;}\u001b[39m\n\u001b[31m.cons-card{\u001b[39m\n\u001b[31m  background:var(--sur2);border:1px solid var(--bdr);border-radius:var(--r-sm);\u001b[39m\n\u001b[31m  padding:11px;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.cons-hdr{display:flex;justify-content:space-between;align-items:center;margin-bottom:8px;}\u001b[39m\n\u001b[31m.cons-num{font-family:var(--fnt-mono);font-size:.65rem;color:var(--t3);}\u001b[39m\n\u001b[31m.cons-del{\u001b[39m\n\u001b[31m  background:none;border:none;color:var(--t4);cursor:pointer;\u001b[39m\n\u001b[31m  font-size:.85rem;padding:2px 5px;border-radius:4px;transition:all .2s;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.cons-del:hover{background:var(--red-dim);color:var(--red);}\u001b[39m\n\u001b[31m.cons-docket-linked{\u001b[39m\n\u001b[31m  display:flex;align-items:center;gap:6px;\u001b[39m\n\u001b[31m  background:rgba(249,115,22,.1);border:1px solid rgba(249,115,22,.3);\u001b[39m\n\u001b[31m  border-radius:4px;padding:4px 8px;margin-top:6px;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.cons-docket-linked .dkt{font-family:var(--fnt-mono);font-size:.82rem;color:var(--acc);}\u001b[39m\n\u001b[31m.cons-docket-linked .unlink{background:none;border:none;color:var(--acc);cursor:pointer;font-size:.75rem;}·\u001b[39m\n\u001b[31m/* ════ DISPATCH DASHBOARD ════ */\u001b[39m\n\u001b[31m.dash-stats{\u001b[39m\n\u001b[31m  display:grid;grid-template-columns:repeat(4,1fr);gap:8px;margin-bottom:14px;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.stat-box{\u001b[39m\n\u001b[31m  background:var(--sur);border:1px solid var(--bdr);border-radius:var(--r);\u001b[39m\n\u001b[31m  padding:10px 8px;text-align:center;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.stat-val{font-family:var(--fnt-head);font-size:1.4rem;font-weight:800;}\u001b[39m\n\u001b[31m.stat-lbl{font-size:.6rem;color:var(--t3);font-family:var(--fnt-mono);text-transform:uppercase;letter-spacing:1px;margin-top:2px;}\u001b[39m\n\u001b[31m.stat-box.grn .stat-val{color:var(--grn);}\u001b[39m\n\u001b[31m.stat-box.ylw .stat-val{color:var(--ylw);}\u001b[39m\n\u001b[31m.stat-box.red .stat-val{color:var(--red);}\u001b[39m\n\u001b[31m.stat-box.sky .stat-val{color:var(--sky);}·\u001b[39m\n\u001b[31m/* ════ DOCKET TILES ════ */\u001b[39m\n\u001b[31m.docket-grid{display:flex;flex-direction:column;gap:8px;}\u001b[39m\n\u001b[31m.docket-tile{\u001b[39m\n\u001b[31m  background:var(--sur);border:1px solid var(--bdr);border-radius:var(--r);\u001b[39m\n\u001b[31m  padding:12px;transition:border-color .2s;cursor:pointer;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.docket-tile:hover{border-color:var(--acc);}\u001b[39m\n\u001b[31m.docket-tile-hdr{display:flex;justify-content:space-between;align-items:flex-start;margin-bottom:8px;}\u001b[39m\n\u001b[31m.docket-tile-left{flex:1;}\u001b[39m\n\u001b[31m.docket-num{font-family:var(--fnt-mono);font-size:.95rem;font-weight:500;color:var(--sky);letter-spacing:1px;}\u001b[39m\n\u001b[31m.docket-store{font-size:.82rem;color:var(--t1);font-weight:600;margin-top:2px;}\u001b[39m\n\u001b[31m.docket-courier{font-size:.68rem;color:var(--t3);font-family:var(--fnt-mono);margin-top:2px;}\u001b[39m\n\u001b[31m.status-pill{\u001b[39m\n\u001b[31m  font-size:.62rem;font-weight:700;font-family:var(--fnt-mono);\u001b[39m\n\u001b[31m  padding:3px 8px;border-radius:10px;text-transform:uppercase;letter-spacing:.5px;\u001b[39m\n\u001b[31m  flex-shrink:0;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.sp-delivered{background:var(--grn-dim);color:var(--grn);}\u001b[39m\n\u001b[31m.sp-transit{background:var(--ylw-dim);color:var(--ylw);}\u001b[39m\n\u001b[31m.sp-booked{background:var(--sky-dim);color:var(--sky);}\u001b[39m\n\u001b[31m.sp-pending{background:var(--bdr);color:var(--t3);}\u001b[39m\n\u001b[31m.sp-failed{background:var(--red-dim);color:var(--red);}\u001b[39m\n\u001b[31m.docket-tile-footer{display:flex;align-items:center;gap:8px;flex-wrap:wrap;}\u001b[39m\n\u001b[31m.docket-last-event{font-size:.7rem;color:var(--t3);flex:1;font-family:var(--fnt-mono);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}\u001b[39m\n\u001b[31m.btn-track-live{\u001b[39m\n\u001b[31m  background:none;border:1px solid var(--acc);color:var(--acc);\u001b[39m\n\u001b[31m  font-size:.65rem;padding:4px 10px;border-radius:20px;cursor:pointer;\u001b[39m\n\u001b[31m  font-family:var(--fnt-mono);transition:all .2s;white-space:nowrap;display:flex;align-items:center;gap:4px;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.btn-track-live:hover{background:var(--acc);color:#fff;}\u001b[39m\n\u001b[31m.btn-track-live .live-dot{width:5px;height:5px;border-radius:50%;background:var(--grn);animation:blink 1.5s infinite;flex-shrink:0;}\u001b[39m\n\u001b[31m.docket-progress{height:2px;background:var(--bdr);border-radius:2px;margin-top:8px;overflow:hidden;}\u001b[39m\n\u001b[31m.docket-progress-fill{height:100%;border-radius:2px;transition:width .5s;}·\u001b[39m\n\u001b[31m/* ════ TRACKING DETAIL ════ */\u001b[39m\n\u001b[31m.track-timeline{padding:4px 0;}\u001b[39m\n\u001b[31m.track-evt{display:flex;gap:10px;padding:6px 0;position:relative;padding-left:20px;}\u001b[39m\n\u001b[31m.track-evt::before{\u001b[39m\n\u001b[31m  content:'';position:absolute;left:5px;top:11px;\u001b[39m\n\u001b[31m  width:7px;height:7px;border-radius:50%;background:var(--bdr2);border:1px solid var(--bdr2);\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.track-evt:first-child::before{background:var(--grn);border-color:var(--grn);}\u001b[39m\n\u001b[31m.track-evt::after{\u001b[39m\n\u001b[31m  content:'';position:absolute;left:8px;top:18px;\u001b[39m\n\u001b[31m  width:1px;height:calc(100% - 6px);background:var(--bdr);\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.track-evt:last-child::after{display:none;}\u001b[39m\n\u001b[31m.track-time{font-family:var(--fnt-mono);font-size:.62rem;color:var(--t3);min-width:90px;flex-shrink:0;padding-top:2px;}\u001b[39m\n\u001b[31m.track-desc{font-size:.78rem;color:var(--t2);}\u001b[39m\n\u001b[31m.track-loc{font-size:.67rem;color:var(--t3);font-family:var(--fnt-mono);margin-top:2px;}·\u001b[39m\n\u001b[31m/* ════ SHARE TOAST ════ */\u001b[39m\n\u001b[31m.share-banner{\u001b[39m\n\u001b[31m  position:fixed;top:0;left:0;right:0;z-index:200;\u001b[39m\n\u001b[31m  background:rgba(34,197,94,.15);border-bottom:1px solid rgba(34,197,94,.4);\u001b[39m\n\u001b[31m  padding:10px 16px;font-size:.78rem;color:var(--grn);font-family:var(--fnt-mono);\u001b[39m\n\u001b[31m  display:flex;align-items:center;gap:8px;\u001b[39m\n\u001b[31m  transform:translateY(-100%);transition:transform .3s;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.share-banner.show{transform:translateY(0);}·\u001b[39m\n\u001b[31m/* ════ MODAL ════ */\u001b[39m\n\u001b[31m.overlay{position:fixed;inset:0;background:rgba(0,0,0,.75);z-index:100;display:flex;align-items:flex-end;opacity:0;pointer-events:none;transition:opacity .3s;}\u001b[39m\n\u001b[31m.overlay.open{opacity:1;pointer-events:all;}\u001b[39m\n\u001b[31m.sheet{\u001b[39m\n\u001b[31m  background:var(--sur);border-radius:var(--r-lg) var(--r-lg) 0 0;\u001b[39m\n\u001b[31m  width:100%;max-height:88vh;overflow-y:auto;padding:18px;\u001b[39m\n\u001b[31m  transform:translateY(100%);transition:transform .3s cubic-bezier(.32,.72,0,1);\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.overlay.open .sheet{transform:translateY(0);}\u001b[39m\n\u001b[31m.sheet-handle{width:36px;height:4px;background:var(--bdr2);border-radius:2px;margin:0 auto 18px;}\u001b[39m\n\u001b[31m.sheet-title{font-family:var(--fnt-head);font-size:1.2rem;font-weight:800;letter-spacing:.5px;margin-bottom:14px;}\u001b[39m\n\u001b[31m.sheet::-webkit-scrollbar{width:3px;}\u001b[39m\n\u001b[31m.sheet::-webkit-scrollbar-thumb{background:var(--bdr2);}·\u001b[39m\n\u001b[31m/* ════ TOAST ════ */\u001b[39m\n\u001b[31m.toast{\u001b[39m\n\u001b[31m  position:fixed;bottom:20px;left:50%;transform:translateX(-50%) translateY(70px);\u001b[39m\n\u001b[31m  background:var(--sur2);border:1px solid var(--bdr2);color:var(--t1);\u001b[39m\n\u001b[31m  font-size:.78rem;padding:9px 16px;border-radius:30px;z-index:300;\u001b[39m\n\u001b[31m  transition:transform .3s;white-space:nowrap;font-family:var(--fnt-mono);\u001b[39m\n\u001b[31m  pointer-events:none;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.toast.show{transform:translateX(-50%) translateY(0);}\u001b[39m\n\u001b[31m.toast.ok{border-color:var(--grn);color:var(--grn);}\u001b[39m\n\u001b[31m.toast.err{border-color:var(--red);color:var(--red);}\u001b[39m\n\u001b[31m.toast.info{border-color:var(--sky);color:var(--sky);}·\u001b[39m\n\u001b[31m/* ════ COURIER MASTER ════ */\u001b[39m\n\u001b[31m.courier-master-row{\u001b[39m\n\u001b[31m  display:flex;align-items:center;gap:8px;padding:8px 0;\u001b[39m\n\u001b[31m  border-bottom:1px solid var(--bdr);\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.courier-master-row:last-child{border-bottom:none;}\u001b[39m\n\u001b[31m.cml{flex:1;}\u001b[39m\n\u001b[31m.cml-name{font-weight:600;font-size:.85rem;}\u001b[39m\n\u001b[31m.cml-url{font-family:var(--fnt-mono);font-size:.65rem;color:var(--t3);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:180px;}\u001b[39m\n\u001b[31m.courier-logo{width:24px;height:24px;border-radius:4px;object-fit:contain;background:var(--sur3);padding:2px;}·\u001b[39m\n\u001b[31m/* ════ PROGRESS BAR ════ */\u001b[39m\n\u001b[31m.ai-progress{\u001b[39m\n\u001b[31m  background:var(--sur2);border:1px solid var(--bdr);border-radius:var(--r-sm);\u001b[39m\n\u001b[31m  padding:10px 12px;margin-top:8px;display:none;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.ai-progress.visible{display:block;}\u001b[39m\n\u001b[31m.ai-prog-bar{height:3px;background:var(--bdr);border-radius:3px;margin-top:6px;overflow:hidden;}\u001b[39m\n\u001b[31m.ai-prog-fill{height:100%;background:linear-gradient(90deg,var(--sky),var(--acc));width:0;transition:width .3s;}\u001b[39m\n\u001b[31m.ai-prog-label{font-size:.68rem;color:var(--sky);font-family:var(--fnt-mono);}·\u001b[39m\n\u001b[31m/* ════ EMPTY ════ */\u001b[39m\n\u001b[31m.empty{padding:40px 20px;text-align:center;color:var(--t4);}\u001b[39m\n\u001b[31m.empty .e-icon{font-size:2.5rem;margin-bottom:10px;}\u001b[39m\n\u001b[31m.empty p{font-size:.82rem;}·\u001b[39m\n\u001b[31m/* ════ INFO TAG ════ */\u001b[39m\n\u001b[31m.info-tag{\u001b[39m\n\u001b[31m  background:var(--sur2);border:1px solid var(--bdr);border-radius:var(--r-sm);\u001b[39m\n\u001b[31m  padding:7px 10px;font-size:.75rem;color:var(--t2);margin-bottom:8px;\u001b[39m\n\u001b[31m  display:flex;align-items:center;gap:8px;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.info-tag strong{color:var(--t1);}·\u001b[39m\n\u001b[31m/* ════ JOB PILL ════ */\u001b[39m\n\u001b[31m.job-pill{\u001b[39m\n\u001b[31m  display:inline-flex;align-items:center;gap:6px;\u001b[39m\n\u001b[31m  background:rgba(249,115,22,.1);border:1px solid rgba(249,115,22,.3);\u001b[39m\n\u001b[31m  color:var(--acc);font-family:var(--fnt-mono);font-size:.68rem;\u001b[39m\n\u001b[31m  padding:3px 9px;border-radius:20px;margin-top:4px;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.job-pill button{background:none;border:none;color:var(--acc);cursor:pointer;font-size:.7rem;}·\u001b[39m\n\u001b[31m/* ════ SECTION DIVIDER ════ */\u001b[39m\n\u001b[31m.sec-div{\u001b[39m\n\u001b[31m  display:flex;align-items:center;gap:10px;margin:14px 0 10px;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.sec-div span{font-family:var(--fnt-head);font-size:.7rem;font-weight:700;color:var(--t3);text-transform:uppercase;letter-spacing:2px;white-space:nowrap;}\u001b[39m\n\u001b[31m.sec-div::before,.sec-div::after{content:'';flex:1;height:1px;background:var(--bdr);}·\u001b[39m\n\u001b[31m/* ── MAPPING UI ── */\u001b[39m\n\u001b[31m.map-table{width:100%;border-collapse:collapse;font-size:.78rem;margin:10px 0;}\u001b[39m\n\u001b[31m.map-table th{background:var(--bg);color:var(--t3);font-family:var(--fnt-head);font-size:.65rem;\u001b[39m\n\u001b[31m  letter-spacing:1.5px;text-transform:uppercase;padding:7px 10px;text-align:left;\u001b[39m\n\u001b[31m  border-bottom:1px solid var(--bdr);}\u001b[39m\n\u001b[31m.map-table td{padding:7px 10px;border-bottom:1px solid var(--bdr);vertical-align:middle;}\u001b[39m\n\u001b[31m.map-table tr:last-child td{border-bottom:none;}\u001b[39m\n\u001b[31m.map-table select{background:var(--sur2);border:1px solid var(--bdr);color:var(--t1);\u001b[39m\n\u001b[31m  font-family:var(--fnt-mono);font-size:.75rem;padding:5px 8px;border-radius:var(--r-sm);\u001b[39m\n\u001b[31m  width:100%;outline:none;}\u001b[39m\n\u001b[31m.map-table select:focus{border-color:var(--acc);}\u001b[39m\n\u001b[31m.map-req{color:var(--acc);margin-left:2px;}\u001b[39m\n\u001b[31m.map-saved-badge{font-size:.6rem;background:rgba(57,255,143,.15);color:var(--accent3,#39ff8f);\u001b[39m\n\u001b[31m  border:1px solid rgba(57,255,143,.3);padding:2px 7px;border-radius:10px;\u001b[39m\n\u001b[31m  font-family:var(--fnt-mono);margin-left:6px;}·\u001b[39m\n\u001b[31m/* ── VERIFICATION SCREEN ── */\u001b[39m\n\u001b[31m#verifyPane{display:none;position:fixed;inset:0;background:var(--bg);z-index:50;\u001b[39m\n\u001b[31m  overflow-y:auto;padding:14px;}\u001b[39m\n\u001b[31m.verify-hdr{display:flex;align-items:center;gap:10px;margin-bottom:14px;\u001b[39m\n\u001b[31m  position:sticky;top:0;background:var(--bg);padding-bottom:10px;\u001b[39m\n\u001b[31m  border-bottom:1px solid var(--bdr);z-index:1;}\u001b[39m\n\u001b[31m.verify-title{font-family:var(--fnt-head);font-size:1.1rem;font-weight:800;flex:1;}\u001b[39m\n\u001b[31m.verify-row{background:var(--sur);border:1px solid var(--bdr);border-radius:var(--r-sm);\u001b[39m\n\u001b[31m  padding:10px 12px;margin-bottom:8px;position:relative;}\u001b[39m\n\u001b[31m.verify-row.has-dupe{border-color:var(--red);background:rgba(239,68,68,.04);}\u001b[39m\n\u001b[31m.verify-row.no-docket{border-color:var(--ylw);background:rgba(234,179,8,.04);}\u001b[39m\n\u001b[31m.verify-row.ok{border-left:3px solid var(--grn);}\u001b[39m\n\u001b[31m.verify-row-hdr{display:flex;align-items:center;gap:8px;margin-bottom:8px;}\u001b[39m\n\u001b[31m.verify-num{font-family:var(--fnt-mono);font-size:.65rem;color:var(--t3);}\u001b[39m\n\u001b[31m.verify-store{font-weight:600;font-size:.9rem;flex:1;}\u001b[39m\n\u001b[31m.verify-flag{font-size:.65rem;font-weight:700;font-family:var(--fnt-mono);\u001b[39m\n\u001b[31m  padding:2px 7px;border-radius:10px;text-transform:uppercase;}\u001b[39m\n\u001b[31m.vf-dupe{background:var(--red-dim);color:var(--red);}\u001b[39m\n\u001b[31m.vf-nodocket{background:var(--ylw-dim);color:var(--ylw);}\u001b[39m\n\u001b[31m.vf-ok{background:var(--grn-dim);color:var(--grn);}\u001b[39m\n\u001b[31m.vf-short{background:rgba(56,189,248,.15);color:var(--sky);}\u001b[39m\n\u001b[31m.verify-fields{display:grid;grid-template-columns:1fr 1fr;gap:8px;margin-bottom:8px;}\u001b[39m\n\u001b[31m.verify-field label{font-size:.62rem;color:var(--t3);text-transform:uppercase;\u001b[39m\n\u001b[31m  letter-spacing:1px;display:block;margin-bottom:3px;font-family:var(--fnt-mono);}\u001b[39m\n\u001b[31m.verify-field input{width:100%;background:var(--sur2);border:1px solid var(--bdr);\u001b[39m\n\u001b[31m  color:var(--t1);font-family:var(--fnt-mono);font-size:.82rem;padding:6px 9px;\u001b[39m\n\u001b[31m  border-radius:var(--r-sm);outline:none;}\u001b[39m\n\u001b[31m.verify-field input:focus{border-color:var(--acc);}\u001b[39m\n\u001b[31m.verify-field input.changed{border-color:var(--ylw);background:rgba(234,179,8,.06);}\u001b[39m\n\u001b[31m.verify-actions{display:flex;gap:6px;align-items:center;flex-wrap:wrap;}\u001b[39m\n\u001b[31m.btn-wa{background:rgba(37,211,102,.15);border:1px solid rgba(37,211,102,.3);\u001b[39m\n\u001b[31m  color:#25d366;font-size:.65rem;padding:4px 10px;border-radius:10px;cursor:pointer;\u001b[39m\n\u001b[31m  font-family:var(--fnt-mono);transition:all .2s;}\u001b[39m\n\u001b[31m.btn-wa:hover{background:rgba(37,211,102,.3);}\u001b[39m\n\u001b[31m.verify-thumb{width:48px;height:48px;object-fit:cover;border-radius:6px;\u001b[39m\n\u001b[31m  border:1px solid var(--bdr);cursor:pointer;flex-shrink:0;}\u001b[39m\n\u001b[31m.verify-summary{background:var(--sur);border:1px solid var(--bdr);border-radius:var(--r);\u001b[39m\n\u001b[31m  padding:12px 14px;margin-bottom:14px;display:flex;gap:16px;flex-wrap:wrap;}\u001b[39m\n\u001b[31m.vs-item{text-align:center;}\u001b[39m\n\u001b[31m.vs-val{font-family:var(--fnt-head);font-size:1.3rem;font-weight:800;}\u001b[39m\n\u001b[31m.vs-lbl{font-size:.6rem;color:var(--t3);font-family:var(--fnt-mono);text-transform:uppercase;\u001b[39m\n\u001b[31m  letter-spacing:.5px;margin-top:2px;}\u001b[39m\n\u001b[31m.ocr-correction-note{font-size:.65rem;color:var(--ylw);font-family:var(--fnt-mono);\u001b[39m\n\u001b[31m  margin-top:3px;}\u001b[39m\n\u001b[31m</style>\u001b[39m\n\u001b[31m</head>\u001b[39m\n\u001b[31m<body>·\u001b[39m\n\u001b[31m<!-- SPLASH -->\u001b[39m\n\u001b[31m<div id=\\\"splash\\\">\u001b[39m\n\u001b[31m  <div class=\\\"splash-logo\\\">360LM <em>DISPATCH</em></div>\u001b[39m\n\u001b[31m  <div class=\\\"splash-sub\\\">Courier · Bus · Field Ops</div>\u001b[39m\n\u001b[31m  <div class=\\\"splash-track\\\"><div class=\\\"splash-track-fill\\\"></div></div>\u001b[39m\n\u001b[31m</div>·\u001b[39m\n\u001b[31m<!-- SHARE BANNER -->\u001b[39m\n\u001b[31m<div class=\\\"share-banner\\\" id=\\\"shareBanner\\\">\u001b[39m\n\u001b[31m  📤 Images received via Share — processing for OCR...\u001b[39m\n\u001b[31m  <button style=\\\"background:none;border:none;color:var(--grn);cursor:pointer;margin-left:auto;\\\" onclick=\\\"dismissShare()\\\">✕</button>\u001b[39m\n\u001b[31m</div>·\u001b[39m\n\u001b[31m<!-- APP -->\u001b[39m\n\u001b[31m<div id=\\\"app\\\" style=\\\"display:none;\\\">·\u001b[39m\n\u001b[31m  <div class=\\\"hdr\\\">\u001b[39m\n\u001b[31m    <div class=\\\"hdr-brand\\\">360LM <em>DISPATCH</em></div>\u001b[39m\n\u001b[31m    <span class=\\\"hdr-dot\\\" id=\\\"netDot\\\"></span>\u001b[39m\n\u001b[31m    <button class=\\\"hdr-btn\\\" onclick=\\\"window.location.href='/hub/'\\\" title=\\\"Hub\\\">🏡</button>\u001b[39m\n\u001b[31m    <button class=\\\"hdr-btn\\\" onclick=\\\"showPane('dashboard')\\\">📊</button>\u001b[39m\n\u001b[31m    <button class=\\\"hdr-btn primary\\\" onclick=\\\"newDispatch()\\\">+ NEW</button>\u001b[39m\n\u001b[31m  </div>·\u001b[39m\n\u001b[31m  <div class=\\\"tabs\\\">\u001b[39m\n\u001b[31m    <button class=\\\"tab active\\\" onclick=\\\"showPane('new')\\\" id=\\\"tab-new\\\">NEW</button>\u001b[39m\n\u001b[31m    <button class=\\\"tab\\\" onclick=\\\"showPane('dashboard')\\\" id=\\\"tab-dashboard\\\">DASHBOARD</button>\u001b[39m\n\u001b[31m    <button class=\\\"tab\\\" onclick=\\\"showPane('track')\\\" id=\\\"tab-track\\\">TRACK</button>\u001b[39m\n\u001b[31m    <button class=\\\"tab\\\" onclick=\\\"showPane('history')\\\" id=\\\"tab-history\\\">HISTORY <span class=\\\"tab-badge\\\" id=\\\"histCount\\\">0</span></button>\u001b[39m\n\u001b[31m    <button class=\\\"tab\\\" onclick=\\\"showPane('config')\\\" id=\\\"tab-config\\\">CONFIG</button>\u001b[39m\n\u001b[31m  </div>·\u001b[39m\n\u001b[31m  <!-- ════════════ NEW DISPATCH ════════════ -->\u001b[39m\n\u001b[31m  <div class=\\\"pane active\\\" id=\\\"pane-new\\\">·\u001b[39m\n\u001b[31m    <!-- 1. JOB REFERENCE -->\u001b[39m\n\u001b[31m    <div class=\\\"card accent-left\\\">\u001b[39m\n\u001b[31m      <div class=\\\"card-title\\\"><span class=\\\"ct-dot\\\"></span>Job Reference</div>\u001b[39m\n\u001b[31m      <div class=\\\"row2\\\">\u001b[39m\n\u001b[31m        <div class=\\\"f\\\">\u001b[39m\n\u001b[31m          <label>Client <span class=\\\"req\\\">*</span></label>\u001b[39m\n\u001b[31m          <select id=\\\"n_client\\\" onchange=\\\"onClientChange()\\\">\u001b[39m\n\u001b[31m            <option value=\\\"\\\">Select...</option>\u001b[39m\n\u001b[31m            <option>Lenovo</option><option>HP</option>\u001b[39m\n\u001b[31m            <option>Philips</option><option>Liebherr</option>\u001b[39m\n\u001b[31m            <option value=\\\"__c\\\">+ Custom</option>\u001b[39m\n\u001b[31m          </select>\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m        <div class=\\\"f\\\">\u001b[39m\n\u001b[31m          <label>Job Ref</label>\u001b[39m\n\u001b[31m          <input type=\\\"text\\\" id=\\\"n_jobref\\\" placeholder=\\\"JOB-042\\\" oninput=\\\"updatePill()\\\">\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      <div class=\\\"f\\\" id=\\\"customClientField\\\" style=\\\"display:none;\\\">\u001b[39m\n\u001b[31m        <label>Custom Client</label>\u001b[39m\n\u001b[31m        <input type=\\\"text\\\" id=\\\"n_customClient\\\" placeholder=\\\"Client name\\\" oninput=\\\"updatePill()\\\">\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      <div id=\\\"jobPill\\\" style=\\\"display:none;\\\" class=\\\"job-pill\\\">\u001b[39m\n\u001b[31m        🔗 <span id=\\\"jobPillTxt\\\"></span>\u001b[39m\n\u001b[31m        <button onclick=\\\"clearJob()\\\">✕</button>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      <div class=\\\"row2\\\" style=\\\"margin-top:10px;\\\">\u001b[39m\n\u001b[31m        <div class=\\\"f\\\" style=\\\"margin-bottom:0;\\\">\u001b[39m\n\u001b[31m          <label>Dispatch Date</label>\u001b[39m\n\u001b[31m          <input type=\\\"date\\\" id=\\\"n_date\\\">\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m        <div class=\\\"f\\\" style=\\\"margin-bottom:0;\\\">\u001b[39m\n\u001b[31m          <label>Courier Company <span class=\\\"req\\\">*</span></label>\u001b[39m\n\u001b[31m          <select id=\\\"n_courier\\\">\u001b[39m\n\u001b[31m            <option value=\\\"\\\">Select...</option>\u001b[39m\n\u001b[31m            <option>SkyKing</option><option>DTDC</option>\u001b[39m\n\u001b[31m            <option>Delhivery</option><option>Blue Dart</option>\u001b[39m\n\u001b[31m            <option>FedEx</option><option>Ecom Express</option>\u001b[39m\n\u001b[31m            <option>India Post</option><option value=\\\"__o\\\">Other</option>\u001b[39m\n\u001b[31m          </select>\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m    </div>·\u001b[39m\n\u001b[31m    <!-- 2. STORE LIST UPLOAD -->\u001b[39m\n\u001b[31m    <div class=\\\"card\\\">\u001b[39m\n\u001b[31m      <div class=\\\"card-title\\\"><span class=\\\"ct-dot\\\" style=\\\"background:var(--sky);\\\"></span>Store / Destination List</div>\u001b[39m\n\u001b[31m      <div class=\\\"store-upload-zone\\\" id=\\\"storeZone\\\" onclick=\\\"document.getElementById('storeFile').click();event.stopPropagation();\\\" style=\\\"cursor:pointer;\\\">\u001b[39m\n\u001b[31m        <input type=\\\"file\\\" id=\\\"storeFile\\\" accept=\\\".xlsx,.xls,.csv\\\" onchange=\\\"handleStoreFile(this)\\\" onclick=\\\"event.stopPropagation();\\\" style=\\\"display:none;\\\">\u001b[39m\n\u001b[31m        <div style=\\\"font-size:1.5rem;margin-bottom:6px;\\\">📋</div>\u001b[39m\n\u001b[31m        <div style=\\\"font-family:var(--fnt-head);font-weight:700;font-size:.9rem;\\\">Upload Store List</div>\u001b[39m\n\u001b[31m        <div style=\\\"font-size:.68rem;color:var(--t3);margin-top:3px;font-family:var(--fnt-mono);\\\">Excel / CSV · stores, cities, contacts</div>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      <div id=\\\"storeTableArea\\\" style=\\\"display:none;\\\">\u001b[39m\n\u001b[31m        <!-- Column detection result -->\u001b[39m\n\u001b[31m        <div id=\\\"storeColInfo\\\" style=\\\"margin-top:10px;display:none;\\\"></div>\u001b[39m\n\u001b[31m        <div style=\\\"display:flex;justify-content:space-between;align-items:center;margin-top:10px;margin-bottom:6px;\\\">\u001b[39m\n\u001b[31m          <span style=\\\"font-family:var(--fnt-mono);font-size:.7rem;color:var(--sky);\\\" id=\\\"storeCount\\\">0 stores loaded</span>\u001b[39m\n\u001b[31m          <button class=\\\"btn-sm\\\" onclick=\\\"clearStores()\\\">Clear</button>\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m        <div class=\\\"store-table-wrap\\\">\u001b[39m\n\u001b[31m          <table class=\\\"store-table\\\" id=\\\"storeTable\\\"></table>\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m        <!-- Inline add-as-consignees UI instead of confirm() -->\u001b[39m\n\u001b[31m        <div id=\\\"storeAddPanel\\\" style=\\\"margin-top:10px;background:var(--sur3);border:1px solid var(--bdr2);border-radius:var(--r-sm);padding:10px 12px;display:none;\\\">\u001b[39m\n\u001b[31m          <div style=\\\"font-size:.75rem;color:var(--t1);margin-bottom:8px;font-weight:600;\\\" id=\\\"storeAddTitle\\\">Add stores as consignees?</div>\u001b[39m\n\u001b[31m          <div class=\\\"row2\\\">\u001b[39m\n\u001b[31m            <button class=\\\"btn btn-ghost\\\" style=\\\"font-size:.75rem;padding:8px;\\\" onclick=\\\"document.getElementById('storeAddPanel').style.display='none'\\\">Skip for now</button>\u001b[39m\n\u001b[31m            <button class=\\\"btn btn-orange\\\" style=\\\"font-size:.75rem;padding:8px;\\\" id=\\\"storeAddBtn\\\" onclick=\\\"addStoresToConsignees()\\\">✓ Add All as Consignees</button>\u001b[39m\n\u001b[31m          </div>\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m        <button class=\\\"btn btn-ghost\\\" style=\\\"margin-top:8px;font-size:.78rem;\\\" onclick=\\\"autoMatchDockets()\\\">\u001b[39m\n\u001b[31m          🔗 Auto-match Dockets to Stores\u001b[39m\n\u001b[31m        </button>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m    </div>·\u001b[39m\n\u001b[31m    <!-- 3. DOCKET SLIPS — AI OCR -->\u001b[39m\n\u001b[31m    <div class=\\\"card\\\">\u001b[39m\n\u001b[31m      <div class=\\\"card-title\\\"><span class=\\\"ct-dot\\\" style=\\\"background:var(--grn);\\\"></span>Docket Slips — AI OCR</div>·\u001b[39m\n\u001b[31m      <!-- Share target indicator -->\u001b[39m\n\u001b[31m      <div class=\\\"info-tag\\\" id=\\\"shareTargetInfo\\\" style=\\\"display:none;\\\">\u001b[39m\n\u001b[31m        <span>📤</span><span>Images shared from WhatsApp — <strong id=\\\"sharedCount\\\">0</strong> slips received</span>\u001b[39m\n\u001b[31m      </div>·\u001b[39m\n\u001b[31m      <label class=\\\"upload-zone\\\" id=\\\"slipZone\\\" for=\\\"slipFiles\\\" style=\\\"display:block;cursor:pointer;\\\">\u001b[39m\n\u001b[31m        <input type=\\\"file\\\" id=\\\"slipFiles\\\" accept=\\\"image/*\\\" multiple onchange=\\\"handleSlipFiles(this.files)\\\" style=\\\"display:none;\\\">\u001b[39m\n\u001b[31m        <div class=\\\"uz-icon\\\">🖼️</div>\u001b[39m\n\u001b[31m        <div class=\\\"uz-title\\\">Upload Courier Slips</div>\u001b[39m\n\u001b[31m        <div class=\\\"uz-sub\\\">Multiple images · Tap or drag · Camera capture supported</div>\u001b[39m\n\u001b[31m      </label>·\u001b[39m\n\u001b[31m      <!-- OCR Working indicator -->\u001b[39m\n\u001b[31m      <div class=\\\"ocr-working\\\" id=\\\"ocrWorking\\\">\u001b[39m\n\u001b[31m        <span class=\\\"spin\\\">⟳</span>\u001b[39m\n\u001b[31m        <span id=\\\"ocrWorkingText\\\">Running OCR...</span>\u001b[39m\n\u001b[31m      </div>·\u001b[39m\n\u001b[31m      <!-- AI Progress -->\u001b[39m\n\u001b[31m      <div class=\\\"ai-progress\\\" id=\\\"aiProgress\\\">\u001b[39m\n\u001b[31m        <div class=\\\"ai-prog-label\\\" id=\\\"aiProgLabel\\\">Initializing Transformers.js...</div>\u001b[39m\n\u001b[31m        <div class=\\\"ai-prog-bar\\\"><div class=\\\"ai-prog-fill\\\" id=\\\"aiProgFill\\\"></div></div>\u001b[39m\n\u001b[31m      </div>·\u001b[39m\n\u001b[31m      <!-- OCR Results -->\u001b[39m\n\u001b[31m      <div class=\\\"ocr-results\\\" id=\\\"ocrResults\\\">\u001b[39m\n\u001b[31m        <div class=\\\"sec-div\\\"><span>Detected Dockets</span></div>\u001b[39m\n\u001b[31m        <div id=\\\"ocrSummary\\\" style=\\\"display:none;font-size:.72rem;font-family:var(--fnt-mono);\u001b[39m\n\u001b[31m          padding:4px 8px 8px;gap:8px\\\"></div>\u001b[39m\n\u001b[31m        <div class=\\\"ocr-slip-list\\\" id=\\\"ocrSlipList\\\"></div>\u001b[39m\n\u001b[31m        <div class=\\\"btn-row\\\" style=\\\"margin-top:10px;\\\">\u001b[39m\n\u001b[31m          <button class=\\\"btn btn-ghost\\\" onclick=\\\"clearSlips()\\\">Clear All</button>\u001b[39m\n\u001b[31m          <button class=\\\"btn btn-grn\\\" onclick=\\\"applyAllDockets()\\\">✓ Apply All to Consignees</button>\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m    </div>·\u001b[39m\n\u001b[31m    <!-- 4. CONSIGNEES -->\u001b[39m\n\u001b[31m    <div class=\\\"card accent-left\\\">\u001b[39m\n\u001b[31m      <div class=\\\"card-title\\\"><span class=\\\"ct-dot\\\"></span>Consignees</div>\u001b[39m\n\u001b[31m      <div class=\\\"cons-list\\\" id=\\\"consList\\\"></div>\u001b[39m\n\u001b[31m      <button class=\\\"btn btn-ghost\\\" style=\\\"font-size:.8rem;\\\" onclick=\\\"addConsignee()\\\">+ Add Consignee</button>\u001b[39m\n\u001b[31m    </div>·\u001b[39m\n\u001b[31m    <!-- 5. REMARKS -->\u001b[39m\n\u001b[31m    <div class=\\\"card\\\">\u001b[39m\n\u001b[31m      <div class=\\\"card-title\\\"><span class=\\\"ct-dot\\\" style=\\\"background:var(--t3);\\\"></span>Remarks</div>\u001b[39m\n\u001b[31m      <div class=\\\"f\\\" style=\\\"margin-bottom:0;\\\">\u001b[39m\n\u001b[31m        <textarea id=\\\"n_remarks\\\" placeholder=\\\"Instructions, notes, special handling...\\\"></textarea>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m    </div>·\u001b[39m\n\u001b[31m    <!-- ACTIONS -->\u001b[39m\n\u001b[31m    <div class=\\\"btn-row\\\">\u001b[39m\n\u001b[31m      <button class=\\\"btn btn-ghost\\\" onclick=\\\"saveDraft()\\\">💾 Draft</button>\u001b[39m\n\u001b[31m      <button class=\\\"btn btn-orange\\\" onclick=\\\"finaliseDispatch()\\\">✓ Finalise & Log</button>\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m    <div style=\\\"height:16px;\\\"></div>\u001b[39m\n\u001b[31m  </div>·\u001b[39m\n\u001b[31m  <!-- ════════════ DASHBOARD ════════════ -->\u001b[39m\n\u001b[31m  <div class=\\\"pane\\\" id=\\\"pane-dashboard\\\">\u001b[39m\n\u001b[31m    <div class=\\\"dash-stats\\\">\u001b[39m\n\u001b[31m      <div class=\\\"stat-box grn\\\"><div class=\\\"stat-val\\\" id=\\\"st-delivered\\\">0</div><div class=\\\"stat-lbl\\\">Delivered</div></div>\u001b[39m\n\u001b[31m      <div class=\\\"stat-box ylw\\\"><div class=\\\"stat-val\\\" id=\\\"st-transit\\\">0</div><div class=\\\"stat-lbl\\\">In Transit</div></div>\u001b[39m\n\u001b[31m      <div class=\\\"stat-box sky\\\"><div class=\\\"stat-val\\\" id=\\\"st-booked\\\">0</div><div class=\\\"stat-lbl\\\">Booked</div></div>\u001b[39m\n\u001b[31m      <div class=\\\"stat-box red\\\"><div class=\\\"stat-val\\\" id=\\\"st-pending\\\">0</div><div class=\\\"stat-lbl\\\">Pending</div></div>\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m    <div id=\\\"dashFilters\\\" style=\\\"display:flex;gap:6px;flex-wrap:wrap;margin-bottom:12px;\\\">\u001b[39m\n\u001b[31m      <span class=\\\"btn-sm primary\\\" onclick=\\\"filterDash('all',this)\\\" style=\\\"cursor:pointer;\\\">All</span>\u001b[39m\n\u001b[31m      <span class=\\\"btn-sm\\\" onclick=\\\"filterDash('delivered',this)\\\" style=\\\"cursor:pointer;\\\">Delivered</span>\u001b[39m\n\u001b[31m      <span class=\\\"btn-sm\\\" onclick=\\\"filterDash('transit',this)\\\" style=\\\"cursor:pointer;\\\">In Transit</span>\u001b[39m\n\u001b[31m      <span class=\\\"btn-sm\\\" onclick=\\\"filterDash('booked',this)\\\" style=\\\"cursor:pointer;\\\">Booked</span>\u001b[39m\n\u001b[31m      <span class=\\\"btn-sm\\\" onclick=\\\"filterDash('pending',this)\\\" style=\\\"cursor:pointer;\\\">Pending</span>\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m    <div class=\\\"docket-grid\\\" id=\\\"dashGrid\\\">\u001b[39m\n\u001b[31m      <div class=\\\"empty\\\"><div class=\\\"e-icon\\\">📊</div><p>No dispatches yet.<br>Create one to see the dashboard.</p></div>\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m    <div style=\\\"display:flex;gap:8px;margin-top:10px;flex-wrap:wrap;\\\">\u001b[39m\n\u001b[31m      <button class=\\\"btn btn-ghost\\\" style=\\\"flex:2;font-size:.8rem;\\\" id=\\\"btnRefreshAll\\\" onclick=\\\"refreshAllTracking()\\\">\u001b[39m\n\u001b[31m        🔄 Refresh All Tracking\u001b[39m\n\u001b[31m      </button>\u001b[39m\n\u001b[31m      <button class=\\\"btn btn-ghost\\\" style=\\\"flex:1;font-size:.8rem;border-color:var(--sky);color:var(--sky);\\\"\u001b[39m\n\u001b[31m        onclick=\\\"shareSelectedReport()\\\">\u001b[39m\n\u001b[31m        📊 Share Selected\u001b[39m\n\u001b[31m      </button>\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m  </div>·\u001b[39m\n\u001b[31m  <!-- ════════════ TRACK ════════════ -->\u001b[39m\n\u001b[31m  <div class=\\\"pane\\\" id=\\\"pane-track\\\">·\u001b[39m\n\u001b[31m    <!-- Courier selector -->\u001b[39m\n\u001b[31m    <div class=\\\"card\\\" style=\\\"margin-bottom:10px;\\\">\u001b[39m\n\u001b[31m      <div class=\\\"card-title\\\"><span class=\\\"ct-dot\\\" style=\\\"background:var(--sky);\\\"></span>Manual Tracking</div>\u001b[39m\n\u001b[31m      <div style=\\\"font-size:.72rem;color:var(--t2);margin-bottom:10px;line-height:1.5;\\\">\u001b[39m\n\u001b[31m        Track any docket number — select from already dispatched dockets or enter new ones manually.\u001b[39m\n\u001b[31m        SkyKing supports up to <strong>10 dockets at once</strong>; others are tracked one at a time.\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      <div class=\\\"f\\\">\u001b[39m\n\u001b[31m        <label>Courier Company</label>\u001b[39m\n\u001b[31m        <select id=\\\"t_courier\\\" onchange=\\\"buildTrackDocketChips()\\\">\u001b[39m\n\u001b[31m          <option>SkyKing</option><option>DTDC</option><option>Delhivery</option>\u001b[39m\n\u001b[31m          <option>Blue Dart</option><option>FedEx</option><option>Ecom Express</option><option>India Post</option>\u001b[39m\n\u001b[31m        </select>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m    </div>·\u001b[39m\n\u001b[31m    <!-- Docket selector — from existing dispatches -->\u001b[39m\n\u001b[31m    <div class=\\\"card\\\" style=\\\"margin-bottom:10px;\\\">\u001b[39m\n\u001b[31m      <div class=\\\"card-title\\\"><span class=\\\"ct-dot\\\" style=\\\"background:var(--acc);\\\"></span>\u001b[39m\n\u001b[31m        Select from Dispatched Dockets\u001b[39m\n\u001b[31m        <span id=\\\"trackCourierLimit\\\" style=\\\"margin-left:auto;font-size:.65rem;color:var(--t3);font-weight:400;text-transform:none;letter-spacing:0;font-family:var(--fnt-mono);\\\"></span>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      <div id=\\\"trackDocketChips\\\" style=\\\"display:flex;flex-wrap:wrap;gap:6px;margin-bottom:10px;min-height:28px;\\\">\u001b[39m\n\u001b[31m        <span style=\\\"font-size:.72rem;color:var(--t3);font-family:var(--fnt-mono);\\\">No dispatched dockets yet</span>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      <div style=\\\"font-size:.68rem;color:var(--t3);font-family:var(--fnt-mono);margin-bottom:8px;\\\">\u001b[39m\n\u001b[31m        Tap a docket to add/remove from tracking list. Selected shown in orange.\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m    </div>·\u001b[39m\n\u001b[31m    <!-- Manual entry for new dockets -->\u001b[39m\n\u001b[31m    <div class=\\\"card\\\" style=\\\"margin-bottom:10px;\\\">\u001b[39m\n\u001b[31m      <div class=\\\"card-title\\\"><span class=\\\"ct-dot\\\" style=\\\"background:var(--grn);\\\"></span>Add New Docket Numbers</div>\u001b[39m\n\u001b[31m      <div class=\\\"f\\\" style=\\\"margin-bottom:6px;\\\">\u001b[39m\n\u001b[31m        <label>Enter docket numbers (comma or newline separated)</label>\u001b[39m\n\u001b[31m        <textarea id=\\\"t_manual_dockets\\\" placeholder=\\\"443005527&#10;443005528&#10;or: 443005527, 443005528\\\"\u001b[39m\n\u001b[31m          style=\\\"min-height:56px;font-family:var(--fnt-mono);font-size:.82rem;letter-spacing:.5px;\\\"></textarea>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      <button class=\\\"btn-sm primary\\\" onclick=\\\"addManualDockets()\\\" style=\\\"font-size:.72rem;\\\">+ Add to List</button>\u001b[39m\n\u001b[31m    </div>·\u001b[39m\n\u001b[31m    <!-- Selected dockets summary + track button -->\u001b[39m\n\u001b[31m    <div id=\\\"trackSelectedSummary\\\" style=\\\"display:none;\\\" class=\\\"card\\\" style=\\\"margin-bottom:10px;\\\">\u001b[39m\n\u001b[31m      <div style=\\\"display:flex;align-items:center;justify-content:space-between;margin-bottom:8px;\\\">\u001b[39m\n\u001b[31m        <div>\u001b[39m\n\u001b[31m          <span style=\\\"font-family:var(--fnt-mono);font-size:.72rem;color:var(--sky);\\\" id=\\\"trackSelectedCount\\\">0 selected</span>\u001b[39m\n\u001b[31m          <span style=\\\"font-family:var(--fnt-mono);font-size:.65rem;color:var(--t3);margin-left:8px;\\\" id=\\\"trackSelectedList\\\"></span>\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m        <button class=\\\"btn-sm\\\" onclick=\\\"clearTrackSelection()\\\">Clear</button>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      <div class=\\\"btn-row\\\">\u001b[39m\n\u001b[31m        <button class=\\\"btn btn-ghost\\\" onclick=\\\"openTrackOnCourierSite()\\\">🔗 Open Courier Site</button>\u001b[39m\n\u001b[31m        <button class=\\\"btn btn-orange\\\" onclick=\\\"trackSelected()\\\">🔍 Track All via Proxy</button>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m    </div>·\u001b[39m\n\u001b[31m    <div id=\\\"trackResultsArea\\\"></div>\u001b[39m\n\u001b[31m  </div>·\u001b[39m\n\u001b[31m  <!-- ════════════ HISTORY ════════════ -->\u001b[39m\n\u001b[31m  <div class=\\\"pane\\\" id=\\\"pane-history\\\">\u001b[39m\n\u001b[31m    <div id=\\\"historyList\\\">\u001b[39m\n\u001b[31m      <div class=\\\"empty\\\"><div class=\\\"e-icon\\\">📭</div><p>No dispatch history yet.</p></div>\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m  </div>·\u001b[39m\n\u001b[31m  <!-- ════════════ CONFIG ════════════ -->\u001b[39m\n\u001b[31m  <div class=\\\"pane\\\" id=\\\"pane-config\\\">·\u001b[39m\n\u001b[31m    <!-- SHEETS URL — the only thing you type manually in the PWA -->\u001b[39m\n\u001b[31m    <div class=\\\"card\\\" style=\\\"border-color:var(--acc);border-width:2px;\\\">\u001b[39m\n\u001b[31m      <div class=\\\"card-title\\\"><span class=\\\"ct-dot\\\"></span>Google Sheets — Master Config</div>\u001b[39m\n\u001b[31m      <div class=\\\"info-tag\\\" style=\\\"background:rgba(34,197,94,.07);border-color:rgba(34,197,94,.3);\\\">\u001b[39m\n\u001b[31m        <span>✅</span>\u001b[39m\n\u001b[31m        <span>URL is loaded automatically from <strong>config.json</strong> on the VPS — no manual entry needed. All other settings sync from your <strong>Google Sheet</strong>.</span>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      <div class=\\\"f\\\">\u001b[39m\n\u001b[31m        <label>Apps Script URL <span style=\\\"font-size:.65rem;color:var(--t3);font-weight:400;\\\">(auto-loaded from config.json)</span></label>\u001b[39m\n\u001b[31m        <input type=\\\"url\\\" id=\\\"cfg_sheets_url\\\"\u001b[39m\n\u001b[31m          placeholder=\\\"Auto-loaded from server...\\\"\u001b[39m\n\u001b[31m          oninput=\\\"onSheetsUrlChange()\\\" readonly\u001b[39m\n\u001b[31m          style=\\\"opacity:.7;cursor:default;\\\">\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      <div id=\\\"syncStatus\\\" class=\\\"info-tag\\\" style=\\\"display:none;\\\">\u001b[39m\n\u001b[31m        <span id=\\\"syncIcon\\\">⏳</span>\u001b[39m\n\u001b[31m        <span id=\\\"syncMsg\\\">Syncing from sheet...</span>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      <div class=\\\"btn-row\\\">\u001b[39m\n\u001b[31m        <button class=\\\"btn btn-ghost\\\" onclick=\\\"syncFromSheet()\\\">🔄 Sync Now</button>\u001b[39m\n\u001b[31m        <button class=\\\"btn btn-orange\\\" onclick=\\\"saveSheetsUrl()\\\">💾 Save & Sync</button>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m    </div>·\u001b[39m\n\u001b[31m    <!-- READ-ONLY display of what was loaded from sheet -->\u001b[39m\n\u001b[31m    <div class=\\\"card\\\" id=\\\"sheetConfigDisplay\\\" style=\\\"display:none;\\\">\u001b[39m\n\u001b[31m      <div class=\\\"card-title\\\"><span class=\\\"ct-dot\\\" style=\\\"background:var(--grn);\\\"></span>Config Loaded from Sheet\u001b[39m\n\u001b[31m        <span id=\\\"sheetSyncTime\\\" style=\\\"margin-left:auto;font-size:.62rem;color:var(--t3);font-family:var(--fnt-mono);font-weight:400;text-transform:none;letter-spacing:0;\\\"></span>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      <div id=\\\"sheetConfigRows\\\"></div>\u001b[39m\n\u001b[31m      <div class=\\\"info-tag\\\" style=\\\"margin-top:8px;\\\">\u001b[39m\n\u001b[31m        <span>✏️</span><span>To change any value, edit the <strong>Config sheet</strong> in Google Sheets, then tap Sync Now above.</span>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m    </div>·\u001b[39m\n\u001b[31m    <!-- Courier Master (from sheet) -->\u001b[39m\n\u001b[31m    <div class=\\\"card\\\">\u001b[39m\n\u001b[31m      <div class=\\\"card-title\\\"><span class=\\\"ct-dot\\\" style=\\\"background:var(--ylw);\\\"></span>Courier Master</div>\u001b[39m\n\u001b[31m      <div id=\\\"courierMasterList\\\"></div>\u001b[39m\n\u001b[31m      <button class=\\\"btn btn-ghost\\\" style=\\\"margin-top:8px;font-size:.78rem;\\\" onclick=\\\"openCourierMaster()\\\">✏️ Edit in Sheet / Override</button>\u001b[39m\n\u001b[31m    </div>·\u001b[39m\n\u001b[31m    <button class=\\\"btn\\\" style=\\\"background:var(--red-dim);color:var(--red);border:1px solid var(--red);margin-top:4px;\\\" onclick=\\\"clearAll()\\\">🗑️ Clear All Local Data</button>\u001b[39m\n\u001b[31m    <div style=\\\"height:20px;\\\"></div>\u001b[39m\n\u001b[31m  </div>··\u001b[39m\n\u001b[31m<!-- ════ ADDRESS REVIEW PANE ════ -->\u001b[39m\n\u001b[31m<div id=\\\"addrReviewPane\\\" style=\\\"display:none;position:fixed;inset:0;background:var(--bg);z-index:50;overflow-y:auto;padding:14px;\\\">\u001b[39m\n\u001b[31m  <div class=\\\"verify-hdr\\\">\u001b[39m\n\u001b[31m    <button class=\\\"btn-icon\\\" onclick=\\\"document.getElementById('addrReviewPane').style.display='none'\\\" style=\\\"font-size:1.1rem;\\\">←</button>\u001b[39m\n\u001b[31m    <div class=\\\"verify-title\\\">Review AI-Parsed Addresses</div>\u001b[39m\n\u001b[31m  </div>\u001b[39m\n\u001b[31m  <div style=\\\"background:rgba(56,189,248,.08);border:1px solid rgba(56,189,248,.3);border-radius:var(--r-sm);\u001b[39m\n\u001b[31m    padding:8px 12px;margin-bottom:12px;font-size:.72rem;color:var(--sky);\\\">\u001b[39m\n\u001b[31m    ✏️ Review each row — correct name and city if wrong. Raw address shown below each row for reference.\u001b[39m\n\u001b[31m    Corrections are saved and improve future AI parsing automatically.\u001b[39m\n\u001b[31m  </div>\u001b[39m\n\u001b[31m  <div class=\\\"verify-summary\\\" id=\\\"addrReviewSummary\\\"></div>\u001b[39m\n\u001b[31m  <div id=\\\"addrReviewList\\\"></div>\u001b[39m\n\u001b[31m  <div style=\\\"position:sticky;bottom:0;background:var(--bg);padding-top:10px;\u001b[39m\n\u001b[31m    border-top:1px solid var(--bdr);margin-top:8px;\\\">\u001b[39m\n\u001b[31m    <div style=\\\"display:flex;gap:8px;\\\">\u001b[39m\n\u001b[31m      <button class=\\\"btn btn-ghost\\\" onclick=\\\"backToMapping()\\\" style=\\\"flex:1;\\\">← Back to Mapping</button>\u001b[39m\n\u001b[31m      <button class=\\\"btn btn-orange\\\" onclick=\\\"confirmAddressReview()\\\" style=\\\"flex:2;\\\">✓ Confirm &amp; Use These Addresses</button>\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m    <div style=\\\"font-size:.65rem;color:var(--t3);text-align:center;margin-top:5px;font-family:var(--fnt-mono);\\\">\u001b[39m\n\u001b[31m      Edits you made above are preserved when you go back\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m  </div>\u001b[39m\n\u001b[31m</div>·\u001b[39m\n\u001b[31m<!-- ════ VERIFICATION SCREEN ════ -->\u001b[39m\n\u001b[31m<div id=\\\"verifyPane\\\">\u001b[39m\n\u001b[31m  <div class=\\\"verify-hdr\\\">\u001b[39m\n\u001b[31m    <button class=\\\"btn-icon\\\" onclick=\\\"closeVerify()\\\" style=\\\"font-size:1.1rem;\\\">←</button>\u001b[39m\n\u001b[31m    <div class=\\\"verify-title\\\">Verify Before Finalise</div>\u001b[39m\n\u001b[31m    <button class=\\\"btn-sm\\\" onclick=\\\"approveAll()\\\" style=\\\"white-space:nowrap;\\\">✓ Approve All</button>\u001b[39m\n\u001b[31m  </div>\u001b[39m\n\u001b[31m  <div class=\\\"verify-summary\\\" id=\\\"verifySummary\\\"></div>\u001b[39m\n\u001b[31m  <div id=\\\"verifyList\\\"></div>\u001b[39m\n\u001b[31m  <div style=\\\"position:sticky;bottom:0;background:var(--bg);padding-top:10px;\u001b[39m\n\u001b[31m    border-top:1px solid var(--bdr);margin-top:8px;\\\">\u001b[39m\n\u001b[31m    <div style=\\\"display:flex;gap:8px;flex-wrap:wrap;\\\">\u001b[39m\n\u001b[31m      <button class=\\\"btn btn-ghost\\\" onclick=\\\"closeVerify()\\\" style=\\\"flex:1;\\\">← Back to Edit</button>\u001b[39m\n\u001b[31m      <button class=\\\"btn btn-ghost\\\" onclick=\\\"exportCleanSheet()\\\" style=\\\"flex:1;border-color:var(--sky);color:var(--sky);\\\">\u001b[39m\n\u001b[31m        📥 Export Clean Sheet\u001b[39m\n\u001b[31m      </button>\u001b[39m\n\u001b[31m      <button class=\\\"btn btn-orange\\\" id=\\\"btnConfirmFinalise\\\" onclick=\\\"confirmAndFinalise()\\\" style=\\\"flex:2;\\\">\u001b[39m\n\u001b[31m        ✓ Finalise Dispatch\u001b[39m\n\u001b[31m      </button>\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m  </div>\u001b[39m\n\u001b[31m</div>·\u001b[39m\n\u001b[31m</div><!-- /app -->·\u001b[39m\n\u001b[31m<!-- ════ DISPATCH DETAIL SHEET ════ -->\u001b[39m\n\u001b[31m<div class=\\\"overlay\\\" id=\\\"detailOverlay\\\" onclick=\\\"closeOverlay('detailOverlay')\\\">\u001b[39m\n\u001b[31m  <div class=\\\"sheet\\\" onclick=\\\"event.stopPropagation()\\\">\u001b[39m\n\u001b[31m    <div class=\\\"sheet-handle\\\"></div>\u001b[39m\n\u001b[31m    <div id=\\\"detailContent\\\"></div>\u001b[39m\n\u001b[31m  </div>\u001b[39m\n\u001b[31m</div>·\u001b[39m\n\u001b[31m<!-- ════ COURIER MASTER EDITOR ════ -->\u001b[39m\n\u001b[31m<div class=\\\"overlay\\\" id=\\\"courierMasterOverlay\\\" onclick=\\\"closeOverlay('courierMasterOverlay')\\\">\u001b[39m\n\u001b[31m  <div class=\\\"sheet\\\" onclick=\\\"event.stopPropagation()\\\">\u001b[39m\n\u001b[31m    <div class=\\\"sheet-handle\\\"></div>\u001b[39m\n\u001b[31m    <div class=\\\"sheet-title\\\">✏️ Courier Master</div>\u001b[39m\n\u001b[31m    <div id=\\\"courierMasterEditor\\\"></div>\u001b[39m\n\u001b[31m  </div>\u001b[39m\n\u001b[31m</div>·\u001b[39m\n\u001b[31m<!-- ════ MANUAL TRACKING OVERLAY ════ -->\u001b[39m\n\u001b[31m<div class=\\\"overlay\\\" id=\\\"manualTrackOverlay\\\" onclick=\\\"closeOverlay('manualTrackOverlay')\\\" data-docket=\\\"\\\" data-dispatch-id=\\\"\\\">\u001b[39m\n\u001b[31m  <div class=\\\"sheet\\\" onclick=\\\"event.stopPropagation()\\\">\u001b[39m\n\u001b[31m    <div class=\\\"sheet-handle\\\"></div>\u001b[39m\n\u001b[31m    <div class=\\\"sheet-title\\\">📞 Manual Tracking Log</div>\u001b[39m\n\u001b[31m    <div style=\\\"background:var(--sur2);border:1px solid var(--bdr);border-radius:var(--r-sm);\u001b[39m\n\u001b[31m      padding:8px 12px;margin-bottom:12px;font-family:var(--fnt-mono);font-size:.82rem;color:var(--acc);\\\"\u001b[39m\n\u001b[31m      id=\\\"mtDocketDisplay\\\"></div>\u001b[39m\n\u001b[31m    <div class=\\\"row2\\\">\u001b[39m\n\u001b[31m      <div class=\\\"f\\\">\u001b[39m\n\u001b[31m        <label>Status Reported <span class=\\\"req\\\">*</span></label>\u001b[39m\n\u001b[31m        <select id=\\\"mtStatus\\\">\u001b[39m\n\u001b[31m          <option value=\\\"\\\">Select...</option>\u001b[39m\n\u001b[31m          <option value=\\\"delivered\\\">✅ Delivered</option>\u001b[39m\n\u001b[31m          <option value=\\\"not received\\\">❌ Not Received</option>\u001b[39m\n\u001b[31m          <option value=\\\"in transit\\\">🚚 In Transit / On the way</option>\u001b[39m\n\u001b[31m          <option value=\\\"will deliver tomorrow\\\">📅 Will Deliver Tomorrow</option>\u001b[39m\n\u001b[31m          <option value=\\\"refused\\\">🚫 Refused / Returned</option>\u001b[39m\n\u001b[31m          <option value=\\\"partial\\\">📦 Partial Delivery</option>\u001b[39m\n\u001b[31m        </select>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      <div class=\\\"f\\\">\u001b[39m\n\u001b[31m        <label>Called By (Staff) <span class=\\\"req\\\">*</span></label>\u001b[39m\n\u001b[31m        <input type=\\\"text\\\" id=\\\"mtCalledBy\\\" placeholder=\\\"Your name\\\" list=\\\"staffList\\\">\u001b[39m\n\u001b[31m        <datalist id=\\\"staffList\\\">\u001b[39m\n\u001b[31m          <option>Mukesh</option><option>Sachin</option><option>Shambhu</option>\u001b[39m\n\u001b[31m          <option>Umashankar</option><option>Narender</option><option>Rakesh</option>\u001b[39m\n\u001b[31m          <option>Gurpreet</option><option>Ashok</option>\u001b[39m\n\u001b[31m        </datalist>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m    <div class=\\\"row2\\\">\u001b[39m\n\u001b[31m      <div class=\\\"f\\\">\u001b[39m\n\u001b[31m        <label>Contact Number Used</label>\u001b[39m\n\u001b[31m        <input type=\\\"tel\\\" id=\\\"mtContactUsed\\\" placeholder=\\\"Mobile number called\\\">\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      <div class=\\\"f\\\">\u001b[39m\n\u001b[31m        <label>Person Who Confirmed</label>\u001b[39m\n\u001b[31m        <input type=\\\"text\\\" id=\\\"mtContactPerson\\\" placeholder=\\\"Name of person at store\\\">\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m    <div class=\\\"f\\\">\u001b[39m\n\u001b[31m      <label>Remarks</label>\u001b[39m\n\u001b[31m      <textarea id=\\\"mtRemarks\\\" placeholder=\\\"Any notes, delivery details, issues...\\\" style=\\\"min-height:60px;\\\"></textarea>\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m    <div class=\\\"f\\\">\u001b[39m\n\u001b[31m      <label>Call Date &amp; Time</label>\u001b[39m\n\u001b[31m      <input type=\\\"datetime-local\\\" id=\\\"mtCallTime\\\">\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m    <div class=\\\"btn-row\\\">\u001b[39m\n\u001b[31m      <button class=\\\"btn btn-ghost\\\" onclick=\\\"closeOverlay('manualTrackOverlay')\\\">Cancel</button>\u001b[39m\n\u001b[31m      <button class=\\\"btn btn-orange\\\" onclick=\\\"submitManualTracking()\\\">💾 Log &amp; Save</button>\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m  </div>\u001b[39m\n\u001b[31m</div>·\u001b[39m\n\u001b[31m<!-- ════ POD UPLOAD OVERLAY ════ -->\u001b[39m\n\u001b[31m<div class=\\\"overlay\\\" id=\\\"podOverlay\\\" onclick=\\\"closeOverlay('podOverlay')\\\" data-docket=\\\"\\\" data-dispatch-id=\\\"\\\">\u001b[39m\n\u001b[31m  <div class=\\\"sheet\\\" onclick=\\\"event.stopPropagation()\\\">\u001b[39m\n\u001b[31m    <div class=\\\"sheet-handle\\\"></div>\u001b[39m\n\u001b[31m    <div class=\\\"sheet-title\\\">📸 Upload Proof of Delivery</div>\u001b[39m\n\u001b[31m    <div class=\\\"f\\\">\u001b[39m\n\u001b[31m      <label>POD / Delivery Slip Photo</label>\u001b[39m\n\u001b[31m      <input type=\\\"file\\\" id=\\\"podFileInput\\\" accept=\\\"image/*\\\" capture=\\\"environment\\\"\u001b[39m\n\u001b[31m        onchange=\\\"previewPOD(this,'pod')\\\" style=\\\"font-size:.8rem;color:var(--t2);\\\">\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m    <div class=\\\"f\\\">\u001b[39m\n\u001b[31m      <label>Recipient / Receiver Photo (optional)</label>\u001b[39m\n\u001b[31m      <input type=\\\"file\\\" id=\\\"recipientFileInput\\\" accept=\\\"image/*\\\" capture=\\\"environment\\\"\u001b[39m\n\u001b[31m        onchange=\\\"previewPOD(this,'recip')\\\" style=\\\"font-size:.8rem;color:var(--t2);\\\">\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m    <div id=\\\"podPreview\\\" style=\\\"display:flex;gap:8px;flex-wrap:wrap;margin-bottom:12px;\\\"></div>\u001b[39m\n\u001b[31m    <div class=\\\"btn-row\\\">\u001b[39m\n\u001b[31m      <button class=\\\"btn btn-ghost\\\" onclick=\\\"closeOverlay('podOverlay')\\\">Cancel</button>\u001b[39m\n\u001b[31m      <button class=\\\"btn btn-orange\\\" onclick=\\\"uploadPODFiles()\\\">📤 Upload & Mark Delivered</button>\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m  </div>\u001b[39m\n\u001b[31m</div>·\u001b[39m\n\u001b[31m<!-- ════ ADMIN LOGIN OVERLAY ════ -->\u001b[39m\n\u001b[31m<div class=\\\"overlay\\\" id=\\\"adminLoginOverlay\\\" onclick=\\\"closeOverlay('adminLoginOverlay')\\\">\u001b[39m\n\u001b[31m  <div class=\\\"sheet\\\" onclick=\\\"event.stopPropagation()\\\" style=\\\"max-height:320px;\\\">\u001b[39m\n\u001b[31m    <div class=\\\"sheet-handle\\\"></div>\u001b[39m\n\u001b[31m    <div class=\\\"sheet-title\\\">🔐 Admin Access</div>\u001b[39m\n\u001b[31m    <div style=\\\"font-size:.78rem;color:var(--t2);margin-bottom:14px;\\\">Config settings require admin password.</div>\u001b[39m\n\u001b[31m    <div class=\\\"f\\\">\u001b[39m\n\u001b[31m      <label>Password</label>\u001b[39m\n\u001b[31m      <input type=\\\"password\\\" id=\\\"adminPwdInput\\\" placeholder=\\\"Enter admin password\\\"\u001b[39m\n\u001b[31m        onkeydown=\\\"if(event.key==='Enter')submitAdminLogin()\\\">\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m    <div class=\\\"btn-row\\\" style=\\\"margin-top:4px;\\\">\u001b[39m\n\u001b[31m      <button class=\\\"btn btn-ghost\\\" onclick=\\\"closeOverlay('adminLoginOverlay')\\\">Cancel</button>\u001b[39m\n\u001b[31m      <button class=\\\"btn btn-orange\\\" onclick=\\\"submitAdminLogin()\\\">🔓 Login</button>\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m  </div>\u001b[39m\n\u001b[31m</div>·\u001b[39m\n\u001b[31m<!-- ════ TOAST ════ -->\u001b[39m\n\u001b[31m<div class=\\\"toast\\\" id=\\\"toast\\\"></div>·\u001b[39m\n\u001b[31m<!-- ════ SCRIPTS ════ -->\u001b[39m\n\u001b[31m<!-- SheetJS for Excel/CSV parsing -->\u001b[39m\n\u001b[31m<script src=\\\"https://cdnjs.cloudflare.com/ajax/libs/xlsx/0.18.5/xlsx.full.min.js\\\"></script>·\u001b[39m\n\u001b[31m<script type=\\\"module\\\">\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31m// OCR — Transformers.js\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31mimport { pipeline, env } from 'https://cdn.jsdelivr.net/npm/@huggingface/transformers@3.5.2/dist/transformers.min.js';·\u001b[39m\n\u001b[31m// Cache models in browser\u001b[39m\n\u001b[31menv.allowLocalModels = false;\u001b[39m\n\u001b[31menv.useBrowserCache = true;·\u001b[39m\n\u001b[31mlet ocrPipeline = null;\u001b[39m\n\u001b[31mlet ocrLoading = false;·\u001b[39m\n\u001b[31mwindow.initOCR = async function() {\u001b[39m\n\u001b[31m  if (ocrPipeline || ocrLoading) return;\u001b[39m\n\u001b[31m  ocrLoading = true;\u001b[39m\n\u001b[31m  showAIProgress('Loading OCR model (first time ~120MB)...', 5);\u001b[39m\n\u001b[31m  try {\u001b[39m\n\u001b[31m    ocrPipeline = await pipeline('image-to-text', 'Xenova/trocr-base-printed', {\u001b[39m\n\u001b[31m      progress_callback: (p) => {\u001b[39m\n\u001b[31m        if (p.progress) {\u001b[39m\n\u001b[31m          showAIProgress(`Loading: ${p.file || 'model'} (${Math.round(p.progress)}%)`, p.progress);\u001b[39m\n\u001b[31m        }\u001b[39m\n\u001b[31m      }\u001b[39m\n\u001b[31m    });\u001b[39m\n\u001b[31m    showAIProgress('OCR model ready ✓', 100);\u001b[39m\n\u001b[31m    setTimeout(() => hideAIProgress(), 1500);\u001b[39m\n\u001b[31m    showToast('OCR ready ✓', 'ok');\u001b[39m\n\u001b[31m  } catch(e) {\u001b[39m\n\u001b[31m    console.warn('Transformers.js OCR failed:', e);\u001b[39m\n\u001b[31m    hideAIProgress();\u001b[39m\n\u001b[31m    showToast('OCR model load failed — will use Ollama fallback', 'info');\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m  ocrLoading = false;\u001b[39m\n\u001b[31m};·\u001b[39m\n\u001b[31mwindow.runOCROnImage = async function(imageData, filename) {\u001b[39m\n\u001b[31m  const cfg = window.appConfig;·\u001b[39m\n\u001b[31m  // Strategy 1: Transformers.js\u001b[39m\n\u001b[31m  if ((cfg.ocr_mode === 'transformers' || cfg.ocr_mode === 'both') && ocrPipeline) {\u001b[39m\n\u001b[31m    try {\u001b[39m\n\u001b[31m      const result = await ocrPipeline(imageData);\u001b[39m\n\u001b[31m      const text = result[0]?.generated_text || '';\u001b[39m\n\u001b[31m      return extractDocketFromText(text, 'transformers');\u001b[39m\n\u001b[31m    } catch(e) { /* fall through */ }\u001b[39m\n\u001b[31m  }·\u001b[39m\n\u001b[31m  // Strategy 2: Dispatch AI proxy → Ollama LLaVA (server-side, avoids CORS + port issues)\u001b[39m\n\u001b[31m  if (cfg.ocr_mode === 'ollama' || cfg.ocr_mode === 'both') {\u001b[39m\n\u001b[31m    try {\u001b[39m\n\u001b[31m      const resp = await fetch('/dispatch-ai/ocr', {\u001b[39m\n\u001b[31m        method: 'POST',\u001b[39m\n\u001b[31m        headers: { 'Content-Type': 'application/json' },\u001b[39m\n\u001b[31m        body: JSON.stringify({ imageBase64: imageData }),\u001b[39m\n\u001b[31m        signal: AbortSignal.timeout(180000)\u001b[39m\n\u001b[31m      });\u001b[39m\n\u001b[31m      const data = await resp.json();\u001b[39m\n\u001b[31m      if (data.success && data.docket) {\u001b[39m\n\u001b[31m        return { docket: data.docket, confidence: data.confidence, source: 'ollama' };\u001b[39m\n\u001b[31m      }\u001b[39m\n\u001b[31m    } catch(e) { /* fall through */ }\u001b[39m\n\u001b[31m  }·\u001b[39m\n\u001b[31m  return { docket: '', confidence: 0, source: 'failed' };\u001b[39m\n\u001b[31m};·\u001b[39m\n\u001b[31mfunction extractDocketFromText(text, source) {\u001b[39m\n\u001b[31m  // Pattern: typical courier docket = 8-15 digit numbers, sometimes with letters\u001b[39m\n\u001b[31m  const patterns = [\u001b[39m\n\u001b[31m    /\\\\b([A-Z]{0,3}\\\\d{8,15}[A-Z]{0,3})\\\\b/g,   // alphanumeric tracking\u001b[39m\n\u001b[31m    /\\\\b(\\\\d{9,15})\\\\b/g,                          // pure numeric\u001b[39m\n\u001b[31m    /\\\\b([A-Z]{2,4}\\\\d{6,12})\\\\b/g,               // letter prefix\u001b[39m\n\u001b[31m  ];\u001b[39m\n\u001b[31m  const candidates = [];\u001b[39m\n\u001b[31m  for (const pat of patterns) {\u001b[39m\n\u001b[31m    let m;\u001b[39m\n\u001b[31m    while ((m = pat.exec(text)) !== null) {\u001b[39m\n\u001b[31m      if (!candidates.includes(m[1])) candidates.push(m[1]);\u001b[39m\n\u001b[31m    }\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m  if (candidates.length === 0) return { docket: text.trim().slice(0, 20), confidence: 0.3, source };\u001b[39m\n\u001b[31m  // Pick longest candidate (usually most complete docket)\u001b[39m\n\u001b[31m  const best = candidates.sort((a, b) => b.length - a.length)[0];\u001b[39m\n\u001b[31m  const confidence = candidates.length === 1 ? 0.85 : 0.7;\u001b[39m\n\u001b[31m  return { docket: best, confidence, source };\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// Expose for non-module code\u001b[39m\n\u001b[31mwindow._ocrReady = () => ocrPipeline !== null;\u001b[39m\n\u001b[31m</script>·\u001b[39m\n\u001b[31m<script>\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31m// STATE & STORAGE\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31mlet dispatches = [];\u001b[39m\n\u001b[31mlet stores = [];\u001b[39m\n\u001b[31mlet slipOCRData = [];\u001b[39m\n\u001b[31mlet dashFilter = 'all';·\u001b[39m\n\u001b[31m// Only sheets_url is persisted locally — all other config comes from the sheet\u001b[39m\n\u001b[31mlet _trackingInProgress = false;\u001b[39m\n\u001b[31mwindow.appConfig = {\u001b[39m\n\u001b[31m  sheets_url:      'https://script.google.com/macros/s/AKfycbys8HrToDv2u_dRvSFnNqmO9bnyAO1s8oSprUydpVQnNQutq4tuUY71cVCH24urbvM9/exec',\u001b[39m\n\u001b[31m  webhook_url:     '',\u001b[39m\n\u001b[31m  proxy_url:       '',\u001b[39m\n\u001b[31m  ollama_url:      'http://srv1111289.hstgr.cloud:11434',\u001b[39m\n\u001b[31m  ocr_mode:        'both',\u001b[39m\n\u001b[31m  company:         '360 Degree Logical Marketing',\u001b[39m\n\u001b[31m  city:            'Chandigarh',\u001b[39m\n\u001b[31m  auto_log:        'true',\u001b[39m\n\u001b[31m  refresh_on_load: 'false',\u001b[39m\n\u001b[31m};·\u001b[39m\n\u001b[31mconst COURIER_MASTER = {\u001b[39m\n\u001b[31m  'SkyKing': {\u001b[39m\n\u001b[31m    trackUrl: 'https://skyking.co/track?cno={docket}',\u001b[39m\n\u001b[31m    trackBulkUrl: 'https://skyking.co/track',\u001b[39m\n\u001b[31m    apiProxy: '/track-proxy/skyking?cno={docket}',\u001b[39m\n\u001b[31m    maxBulk: 10\u001b[39m\n\u001b[31m  },\u001b[39m\n\u001b[31m  'DTDC': {\u001b[39m\n\u001b[31m    trackUrl: 'https://www.dtdc.in/tracking.asp',\u001b[39m\n\u001b[31m    queryParam: 'trkId',\u001b[39m\n\u001b[31m    maxBulk: 1\u001b[39m\n\u001b[31m  },\u001b[39m\n\u001b[31m  'Delhivery': {\u001b[39m\n\u001b[31m    trackUrl: 'https://www.delhivery.com/track/package/{docket}',\u001b[39m\n\u001b[31m    maxBulk: 1\u001b[39m\n\u001b[31m  },\u001b[39m\n\u001b[31m  'Blue Dart': {\u001b[39m\n\u001b[31m    trackUrl: 'https://www.bluedart.com/tracking',\u001b[39m\n\u001b[31m    maxBulk: 1\u001b[39m\n\u001b[31m  },\u001b[39m\n\u001b[31m  'FedEx': {\u001b[39m\n\u001b[31m    trackUrl: 'https://www.fedex.com/fedextrack/?trknbr={docket}',\u001b[39m\n\u001b[31m    maxBulk: 10\u001b[39m\n\u001b[31m  },\u001b[39m\n\u001b[31m  'Ecom Express': {\u001b[39m\n\u001b[31m    trackUrl: 'https://ecomexpress.in/tracking/?awb_field={docket}',\u001b[39m\n\u001b[31m    maxBulk: 1\u001b[39m\n\u001b[31m  },\u001b[39m\n\u001b[31m  'India Post': {\u001b[39m\n\u001b[31m    trackUrl: 'https://www.indiapost.gov.in/VAS/Pages/trackconsignment.aspx',\u001b[39m\n\u001b[31m    maxBulk: 1\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m};·\u001b[39m\n\u001b[31mfunction loadData() {\u001b[39m\n\u001b[31m  try {\u001b[39m\n\u001b[31m    dispatches = JSON.parse(localStorage.getItem('360lm_disp_v2') || '[]');\u001b[39m\n\u001b[31m    const saved = JSON.parse(localStorage.getItem('360lm_disp_sheets_url') || '{}');\u001b[39m\n\u001b[31m    if (saved.sheets_url) window.appConfig.sheets_url = saved.sheets_url;\u001b[39m\n\u001b[31m  } catch(e) {}\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31mfunction saveData() {\u001b[39m\n\u001b[31m  try {\u001b[39m\n\u001b[31m    localStorage.setItem('360lm_disp_v2', JSON.stringify(dispatches));\u001b[39m\n\u001b[31m  } catch(e) {\u001b[39m\n\u001b[31m    if (e.name === 'QuotaExceededError' || e.code === 22) {\u001b[39m\n\u001b[31m      showToast('Storage full — old dispatches cleared. Data is safe in Postgres.', 'err');\u001b[39m\n\u001b[31m      // Drop delivered dispatches older than 30 days to free space\u001b[39m\n\u001b[31m      const cutoff = Date.now() - 30 * 24 * 3600 * 1000;\u001b[39m\n\u001b[31m      dispatches = dispatches.filter(d =>\u001b[39m\n\u001b[31m        d.status !== 'draft' && (\u001b[39m\n\u001b[31m          d.status !== 'active' ||\u001b[39m\n\u001b[31m          new Date(d.createdAt || 0).getTime() > cutoff\u001b[39m\n\u001b[31m        ) ? true :\u001b[39m\n\u001b[31m        d.status === 'draft' || new Date(d.createdAt || 0).getTime() > cutoff\u001b[39m\n\u001b[31m      );\u001b[39m\n\u001b[31m      try { localStorage.setItem('360lm_disp_v2', JSON.stringify(dispatches)); } catch(_) {}\u001b[39m\n\u001b[31m    }\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// ── Postgres DB sync ──────────────────────────────────────────────────────\u001b[39m\n\u001b[31mconst _DB        = '/db';\u001b[39m\n\u001b[31m// PostgREST v12: Accept-Profile for reads, Content-Profile for writes\u001b[39m\n\u001b[31mconst _DHDR_READ  = { 'Accept-Profile': 'dispatch' };\u001b[39m\n\u001b[31mconst _DHDR_WRITE = { 'Content-Type': 'application/json', 'Content-Profile': 'dispatch', 'Accept-Profile': 'dispatch' };\u001b[39m\n\u001b[31mconst _DHDR_UPSERT = { ..._DHDR_WRITE, 'Prefer': 'resolution=merge-duplicates,return=minimal' };·\u001b[39m\n\u001b[31mfunction _dispRow(d) {\u001b[39m\n\u001b[31m  return {\u001b[39m\n\u001b[31m    dispatch_id:    d.id,\u001b[39m\n\u001b[31m    client:         d.client    || '',\u001b[39m\n\u001b[31m    job_ref:        d.jobref    || null,\u001b[39m\n\u001b[31m    courier:        d.courier   || '',\u001b[39m\n\u001b[31m    dispatch_date:  d.dispatchDate || null,\u001b[39m\n\u001b[31m    status:         d.status    || 'active',\u001b[39m\n\u001b[31m    total_count:    (d.consignees || []).length,\u001b[39m\n\u001b[31m    docketed_count: (d.consignees || []).filter(c => c.docket).length,\u001b[39m\n\u001b[31m    remarks:        d.remarks   || null,\u001b[39m\n\u001b[31m    created_by:     window._hubUser?.id || 'unknown',\u001b[39m\n\u001b[31m  };\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction _consRow(c, dispatchId, idx) {\u001b[39m\n\u001b[31m  return {\u001b[39m\n\u001b[31m    cons_id:            `${dispatchId}-${idx}`,\u001b[39m\n\u001b[31m    dispatch_id:        dispatchId,\u001b[39m\n\u001b[31m    seq_no:             idx,\u001b[39m\n\u001b[31m    name:               c.name          || '',\u001b[39m\n\u001b[31m    city:               c.city          || null,\u001b[39m\n\u001b[31m    address:            c.address       || null,\u001b[39m\n\u001b[31m    phone:              c.phone         || null,\u001b[39m\n\u001b[31m    pincode:            c.pincode       || null,\u001b[39m\n\u001b[31m    material:           c.material      || null,\u001b[39m\n\u001b[31m    qty:                c.qty           || null,\u001b[39m\n\u001b[31m    docket:             c.docket        || null,\u001b[39m\n\u001b[31m    status:             c.status        || 'pending',\u001b[39m\n\u001b[31m    last_event:         c.lastEvent     || null,\u001b[39m\n\u001b[31m    last_scan_location: c.lastScanLocation || null,\u001b[39m\n\u001b[31m    last_tracked_at:    c.lastTrackedAt || null,\u001b[39m\n\u001b[31m    manual_status:      c.manualStatus  || null,\u001b[39m\n\u001b[31m    manual_delivered:   !!c.manualDelivered,\u001b[39m\n\u001b[31m    manual_called_by:   c.manualCalledBy || null,\u001b[39m\n\u001b[31m    manual_remarks:     c.manualRemarks || null,\u001b[39m\n\u001b[31m    pod_uploaded:       !!c.podUploaded,\u001b[39m\n\u001b[31m  };\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31masync function syncDispatchToDB(d) {\u001b[39m\n\u001b[31m  if (!d || d.status === 'draft') return; // never sync drafts\u001b[39m\n\u001b[31m  try {\u001b[39m\n\u001b[31m    await fetch(`${_DB}/dispatches`, {\u001b[39m\n\u001b[31m      method: 'POST', headers: _DHDR_UPSERT,\u001b[39m\n\u001b[31m      body: JSON.stringify(_dispRow(d)),\u001b[39m\n\u001b[31m    });\u001b[39m\n\u001b[31m    const rows = (d.consignees || []).map((c, i) => _consRow(c, d.id, i));\u001b[39m\n\u001b[31m    if (rows.length) {\u001b[39m\n\u001b[31m      await fetch(`${_DB}/consignees`, {\u001b[39m\n\u001b[31m        method: 'POST', headers: _DHDR_UPSERT,\u001b[39m\n\u001b[31m        body: JSON.stringify(rows),\u001b[39m\n\u001b[31m      });\u001b[39m\n\u001b[31m    }\u001b[39m\n\u001b[31m  } catch(e) { console.warn('[dispatch-db] sync error:', e.message); }\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31masync function syncConsigneeToDB(c, dispatchId, idx) {\u001b[39m\n\u001b[31m  try {\u001b[39m\n\u001b[31m    await fetch(`${_DB}/consignees`, {\u001b[39m\n\u001b[31m      method: 'POST', headers: _DHDR_UPSERT,\u001b[39m\n\u001b[31m      body: JSON.stringify(_consRow(c, dispatchId, idx)),\u001b[39m\n\u001b[31m    });\u001b[39m\n\u001b[31m    // Log tracking event\u001b[39m\n\u001b[31m    if (c.lastEvent && c.status) {\u001b[39m\n\u001b[31m      fetch(`${_DB}/tracking_events`, {\u001b[39m\n\u001b[31m        method: 'POST', headers: _DHDR_WRITE,\u001b[39m\n\u001b[31m        body: JSON.stringify({\u001b[39m\n\u001b[31m          docket: c.docket, dispatch_id: dispatchId,\u001b[39m\n\u001b[31m          cons_id: `${dispatchId}-${idx}`,\u001b[39m\n\u001b[31m          status: c.status, event_text: c.lastEvent,\u001b[39m\n\u001b[31m          scan_location: c.lastScanLocation || null,\u001b[39m\n\u001b[31m        }),\u001b[39m\n\u001b[31m      }).catch(() => {});\u001b[39m\n\u001b[31m    }\u001b[39m\n\u001b[31m  } catch(e) { console.warn('[dispatch-db] consignee sync error:', e.message); }\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31masync function loadFromDB() {\u001b[39m\n\u001b[31m  try {\u001b[39m\n\u001b[31m    const [dResp, cResp] = await Promise.all([\u001b[39m\n\u001b[31m      fetch(`${_DB}/dispatches?order=created_at.desc&limit=100`, { headers: _DHDR_READ }),\u001b[39m\n\u001b[31m      fetch(`${_DB}/consignees?order=dispatch_id,seq_no`, { headers: _DHDR_READ }),\u001b[39m\n\u001b[31m    ]);\u001b[39m\n\u001b[31m    if (!dResp.ok) return;\u001b[39m\n\u001b[31m    const dbDispatches = await dResp.json();\u001b[39m\n\u001b[31m    const dbConsignees = cResp.ok ? await cResp.json() : [];\u001b[39m\n\u001b[31m    if (!dbDispatches.length) return;·\u001b[39m\n\u001b[31m    const dbIds = new Set(dbDispatches.map(d => d.dispatch_id));\u001b[39m\n\u001b[31m    const localOnly = dispatches.filter(d => !dbIds.has(d.id));·\u001b[39m\n\u001b[31m    const fromDB = dbDispatches.map(d => {\u001b[39m\n\u001b[31m      const cons = dbConsignees.filter(c => c.dispatch_id === d.dispatch_id);\u001b[39m\n\u001b[31m      return {\u001b[39m\n\u001b[31m        id:           d.dispatch_id,\u001b[39m\n\u001b[31m        client:       d.client,\u001b[39m\n\u001b[31m        jobref:       d.job_ref,\u001b[39m\n\u001b[31m        courier:      d.courier,\u001b[39m\n\u001b[31m        dispatchDate: d.dispatch_date,\u001b[39m\n\u001b[31m        status:       d.status,\u001b[39m\n\u001b[31m        remarks:      d.remarks,\u001b[39m\n\u001b[31m        createdAt:    d.created_at,\u001b[39m\n\u001b[31m        consignees:   cons.map(c => ({\u001b[39m\n\u001b[31m          name:              c.name,\u001b[39m\n\u001b[31m          city:              c.city,\u001b[39m\n\u001b[31m          address:           c.address,\u001b[39m\n\u001b[31m          phone:             c.phone,\u001b[39m\n\u001b[31m          pincode:           c.pincode,\u001b[39m\n\u001b[31m          material:          c.material,\u001b[39m\n\u001b[31m          qty:               c.qty,\u001b[39m\n\u001b[31m          docket:            c.docket,\u001b[39m\n\u001b[31m          status:            c.status,\u001b[39m\n\u001b[31m          lastEvent:         c.last_event,\u001b[39m\n\u001b[31m          lastScanLocation:  c.last_scan_location,\u001b[39m\n\u001b[31m          lastTrackedAt:     c.last_tracked_at,\u001b[39m\n\u001b[31m          manualStatus:      c.manual_status,\u001b[39m\n\u001b[31m          manualDelivered:   c.manual_delivered,\u001b[39m\n\u001b[31m          manualCalledBy:    c.manual_called_by,\u001b[39m\n\u001b[31m          manualRemarks:     c.manual_remarks,\u001b[39m\n\u001b[31m          podUploaded:       c.pod_uploaded,\u001b[39m\n\u001b[31m          _dbIdx:            c.seq_no,\u001b[39m\n\u001b[31m        })),\u001b[39m\n\u001b[31m      };\u001b[39m\n\u001b[31m    });·\u001b[39m\n\u001b[31m    dispatches = [...fromDB, ...localOnly];\u001b[39m\n\u001b[31m    saveData();\u001b[39m\n\u001b[31m    renderDashboard();\u001b[39m\n\u001b[31m    updateHistCount();\u001b[39m\n\u001b[31m  } catch(e) { console.warn('[dispatch-db] load error:', e.message); }\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31mfunction saveSheetsUrlLocal() {\u001b[39m\n\u001b[31m  localStorage.setItem('360lm_disp_sheets_url', JSON.stringify({ sheets_url: window.appConfig.sheets_url }));\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m/**\u001b[39m\n\u001b[31m * Fetch config.json from the VPS server (same origin).\u001b[39m\n\u001b[31m * This file contains the sheets_url — no manual entry needed.\u001b[39m\n\u001b[31m * Falls back to localStorage cache if fetch fails (offline).\u001b[39m\n\u001b[31m */\u001b[39m\n\u001b[31masync function loadServerConfig() {\u001b[39m\n\u001b[31m  try {\u001b[39m\n\u001b[31m    const resp = await fetch('./config.json', { cache: 'no-store' });\u001b[39m\n\u001b[31m    if (resp.ok) {\u001b[39m\n\u001b[31m      const cfg = await resp.json();\u001b[39m\n\u001b[31m      if (cfg.sheets_url) {\u001b[39m\n\u001b[31m        window.appConfig.sheets_url = cfg.sheets_url;\u001b[39m\n\u001b[31m        saveSheetsUrlLocal(); // cache it for offline use\u001b[39m\n\u001b[31m        document.getElementById('cfg_sheets_url').value = cfg.sheets_url;\u001b[39m\n\u001b[31m        return true;\u001b[39m\n\u001b[31m      }\u001b[39m\n\u001b[31m    }\u001b[39m\n\u001b[31m  } catch(e) {\u001b[39m\n\u001b[31m    console.log('config.json fetch failed, using cached URL:', e.message);\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m  return false;\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31m// INIT\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31m// ── Hub auth gate ─────────────────────────────────────────────────────────\u001b[39m\n\u001b[31mwindow._hubUser = null;\u001b[39m\n\u001b[31m(function() {\u001b[39m\n\u001b[31m  try {\u001b[39m\n\u001b[31m    const hubLogin = '/hub/?next=' + encodeURIComponent(location.pathname + location.search);\u001b[39m\n\u001b[31m    const s = JSON.parse(localStorage.getItem('lm360-session') || 'null');\u001b[39m\n\u001b[31m    if (!s || !s.empId || !s.name) { window.location.replace(hubLogin); return; }\u001b[39m\n\u001b[31m    if (Date.now() - new Date(s.loginAt).getTime() > 12 * 3600 * 1000) {\u001b[39m\n\u001b[31m      window.location.replace(hubLogin); return;\u001b[39m\n\u001b[31m    }\u001b[39m\n\u001b[31m    window._hubUser = { id: s.empId, name: s.name, role: s.role };\u001b[39m\n\u001b[31m  } catch(_) { window.location.replace('/hub/?next=' + encodeURIComponent(location.pathname + location.search)); }\u001b[39m\n\u001b[31m})();·\u001b[39m\n\u001b[31mwindow.addEventListener('load', async () => {\u001b[39m\n\u001b[31m  if (!window._hubUser) return; // should never reach here — gate redirected already\u001b[39m\n\u001b[31m  setTimeout(() => {\u001b[39m\n\u001b[31m    document.getElementById('splash').style.opacity = '0';\u001b[39m\n\u001b[31m    setTimeout(() => {\u001b[39m\n\u001b[31m      document.getElementById('splash').style.display = 'none';\u001b[39m\n\u001b[31m      document.getElementById('app').style.display = 'flex';\u001b[39m\n\u001b[31m    }, 450);\u001b[39m\n\u001b[31m  }, 1400);\u001b[39m\n\u001b[31m  loadData();\u001b[39m\n\u001b[31m  // Auto-authenticate config tab for admin users (no password needed)\u001b[39m\n\u001b[31m  if (['harish', 'pramod'].includes(window._hubUser?.id)) _adminAuthed = true;\u001b[39m\n\u001b[31m  setToday();\u001b[39m\n\u001b[31m  addConsignee();\u001b[39m\n\u001b[31m  updateHistCount();\u001b[39m\n\u001b[31m  setInterval(updateNetDot, 5000);\u001b[39m\n\u001b[31m  updateNetDot();\u001b[39m\n\u001b[31m  checkShareTarget();\u001b[39m\n\u001b[31m  setupDragDrop();·\u001b[39m\n\u001b[31m  // Step 1: fetch sheets_url from VPS config.json (auto, no user input)\u001b[39m\n\u001b[31m  await loadServerConfig();·\u001b[39m\n\u001b[31m  // Step 2: populate config tab field\u001b[39m\n\u001b[31m  document.getElementById('cfg_sheets_url').value = window.appConfig.sheets_url || '';·\u001b[39m\n\u001b[31m  // Step 3: load dispatches from Postgres in background (merge with local)\u001b[39m\n\u001b[31m  loadFromDB(); // non-blocking — updates UI when data arrives·\u001b[39m\n\u001b[31m  // Step 4: sync full config from Google Sheet silently\u001b[39m\n\u001b[31m  if (window.appConfig.sheets_url) {\u001b[39m\n\u001b[31m    syncFromSheet(true); // silent on startup\u001b[39m\n\u001b[31m  } else {\u001b[39m\n\u001b[31m    // Only show config tab if no URL found anywhere\u001b[39m\n\u001b[31m    showPane('config');\u001b[39m\n\u001b[31m    showToast('No config.json found on server', 'err');\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m  // Pre-warm OCR after delay\u001b[39m\n\u001b[31m  setTimeout(() => { if (typeof window.initOCR === 'function') window.initOCR(); }, 4000);\u001b[39m\n\u001b[31m});·\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31m// WEB SHARE TARGET\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31masync function checkShareTarget() {\u001b[39m\n\u001b[31m  // Check URL params for share target\u001b[39m\n\u001b[31m  const params = new URLSearchParams(window.location.search);\u001b[39m\n\u001b[31m  const action = params.get('action');\u001b[39m\n\u001b[31m  if (action !== 'share') return;·\u001b[39m\n\u001b[31m  // Check for shared files via Share Target API\u001b[39m\n\u001b[31m  if ('launchQueue' in window) {\u001b[39m\n\u001b[31m    window.launchQueue.setConsumer(async launchParams => {\u001b[39m\n\u001b[31m      if (!launchParams.files || launchParams.files.length === 0) return;\u001b[39m\n\u001b[31m      const files = await Promise.all(launchParams.files.map(f => f.getFile()));\u001b[39m\n\u001b[31m      const imageFiles = files.filter(f => f.type.startsWith('image/'));\u001b[39m\n\u001b[31m      if (imageFiles.length > 0) {\u001b[39m\n\u001b[31m        document.getElementById('shareTargetInfo').style.display = 'flex';\u001b[39m\n\u001b[31m        document.getElementById('sharedCount').textContent = imageFiles.length;\u001b[39m\n\u001b[31m        document.getElementById('shareBanner').classList.add('show');\u001b[39m\n\u001b[31m        setTimeout(() => document.getElementById('shareBanner').classList.remove('show'), 4000);\u001b[39m\n\u001b[31m        showPane('new');\u001b[39m\n\u001b[31m        await handleSlipFiles(imageFiles);\u001b[39m\n\u001b[31m      }\u001b[39m\n\u001b[31m    });\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction dismissShare() {\u001b[39m\n\u001b[31m  document.getElementById('shareBanner').classList.remove('show');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31m// DRAG & DROP\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31mfunction setupDragDrop() {\u001b[39m\n\u001b[31m  const zone = document.getElementById('slipZone');\u001b[39m\n\u001b[31m  zone.addEventListener('dragover', e => { e.preventDefault(); zone.classList.add('drag'); });\u001b[39m\n\u001b[31m  zone.addEventListener('dragleave', () => zone.classList.remove('drag'));\u001b[39m\n\u001b[31m  zone.addEventListener('drop', e => {\u001b[39m\n\u001b[31m    e.preventDefault();\u001b[39m\n\u001b[31m    zone.classList.remove('drag');\u001b[39m\n\u001b[31m    const files = Array.from(e.dataTransfer.files).filter(f => f.type.startsWith('image/'));\u001b[39m\n\u001b[31m    if (files.length) handleSlipFiles(files);\u001b[39m\n\u001b[31m  });\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31m// SLIP OCR HANDLER\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31masync function handleSlipFiles(files) {\u001b[39m\n\u001b[31m  const fileArr = Array.from(files);\u001b[39m\n\u001b[31m  if (!fileArr.length) return;\u001b[39m\n\u001b[31m  // If dockets already mapped from store list — allow upload but skip OCR extraction\u001b[39m\n\u001b[31m  const skipOCR = shouldSkipOCR();\u001b[39m\n\u001b[31m  if (skipOCR) {\u001b[39m\n\u001b[31m    showToast('Dockets already mapped — slips saved for reference only', 'info');\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m  showOCRWorking(true, `Processing ${fileArr.length} slip(s)...`);·\u001b[39m\n\u001b[31m  for (let i = 0; i < fileArr.length; i++) {\u001b[39m\n\u001b[31m    const file = fileArr[i];\u001b[39m\n\u001b[31m    showOCRWorking(true, `OCR: ${i+1}/${fileArr.length} — ${file.name}`);\u001b[39m\n\u001b[31m    const imageData = await readFileAsDataURL(file);\u001b[39m\n\u001b[31m    let result = { docket: '', confidence: 0, source: 'manual' };\u001b[39m\n\u001b[31m    if (!skipOCR) {\u001b[39m\n\u001b[31m      try {\u001b[39m\n\u001b[31m        result = await window.runOCROnImage(imageData, file.name);\u001b[39m\n\u001b[31m      } catch(e) {}\u001b[39m\n\u001b[31m    } else {\u001b[39m\n\u001b[31m      result = { docket: '', confidence: 1, source: 'skipped — docket pre-mapped' };\u001b[39m\n\u001b[31m    }\u001b[39m\n\u001b[31m    slipOCRData.push({ file, imageData, filename: file.name, ...result });\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m  showOCRWorking(false);\u001b[39m\n\u001b[31m  renderOCRResults();\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction readFileAsDataURL(file) {\u001b[39m\n\u001b[31m  return new Promise((res, rej) => {\u001b[39m\n\u001b[31m    const r = new FileReader();\u001b[39m\n\u001b[31m    r.onload = e => res(e.target.result);\u001b[39m\n\u001b[31m    r.onerror = rej;\u001b[39m\n\u001b[31m    r.readAsDataURL(file);\u001b[39m\n\u001b[31m  });\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction renderOCRResults() {\u001b[39m\n\u001b[31m  if (!slipOCRData.length) return;\u001b[39m\n\u001b[31m  const list = document.getElementById('ocrSlipList');\u001b[39m\n\u001b[31m  document.getElementById('ocrResults').classList.add('visible');·\u001b[39m\n\u001b[31m  const needReview = slipOCRData.filter(s => s.confidence < 0.65 && s.docket).length;\u001b[39m\n\u001b[31m  const autoOk     = slipOCRData.filter(s => s.confidence >= 0.88).length;\u001b[39m\n\u001b[31m  const summary    = document.getElementById('ocrSummary');\u001b[39m\n\u001b[31m  if (summary) {\u001b[39m\n\u001b[31m    summary.style.display = '';\u001b[39m\n\u001b[31m    summary.innerHTML =\u001b[39m\n\u001b[31m      `<span style=\\\"color:var(--grn)\\\">✓ ${autoOk} auto-accepted</span>` +\u001b[39m\n\u001b[31m      (needReview ? `&nbsp;·&nbsp;<span style=\\\"color:var(--ylw)\\\">⚠ ${needReview} need manual check</span>` : '');\u001b[39m\n\u001b[31m  }·\u001b[39m\n\u001b[31m  list.innerHTML = slipOCRData.map((s, i) => {\u001b[39m\n\u001b[31m    const autoAccepted = s.confidence >= 0.88 && s.docket;\u001b[39m\n\u001b[31m    const needsCheck   = s.confidence < 0.65  && s.docket;\u001b[39m\n\u001b[31m    const confClass = autoAccepted ? 'conf-high' : needsCheck ? 'conf-low' : 'conf-med';\u001b[39m\n\u001b[31m    const confLabel = autoAccepted\u001b[39m\n\u001b[31m      ? `✓ ${Math.round(s.confidence * 100)}% — auto`\u001b[39m\n\u001b[31m      : needsCheck\u001b[39m\n\u001b[31m        ? `⚠ ${Math.round(s.confidence * 100)}% — verify`\u001b[39m\n\u001b[31m        : `${Math.round(s.confidence * 100)}%`;\u001b[39m\n\u001b[31m    const borderStyle = needsCheck ? 'border:1px solid var(--ylw);' : '';\u001b[39m\n\u001b[31m    return `\u001b[39m\n\u001b[31m    <div class=\\\"ocr-slip\\\" id=\\\"ocrSlip_${i}\\\" style=\\\"${borderStyle}\\\">\u001b[39m\n\u001b[31m      <img src=\\\"${s.imageData}\\\" alt=\\\"slip ${i+1}\\\">\u001b[39m\n\u001b[31m      <div class=\\\"ocr-slip-body\\\">\u001b[39m\n\u001b[31m        <div class=\\\"ocr-slip-name\\\">${s.filename}</div>\u001b[39m\n\u001b[31m        <input type=\\\"text\\\" class=\\\"ocr-slip-docket\\\" id=\\\"ocrDocket_${i}\\\"\u001b[39m\n\u001b[31m          value=\\\"${s.docket}\\\" placeholder=\\\"Docket number\\\"\u001b[39m\n\u001b[31m          ${autoAccepted ? 'style=\\\"opacity:.6\\\"' : ''}\u001b[39m\n\u001b[31m          oninput=\\\"slipOCRData[${i}].docket=this.value;slipOCRData[${i}].confidence=0.5\\\">\u001b[39m\n\u001b[31m        <div class=\\\"ocr-slip-status\\\">\u001b[39m\n\u001b[31m          <span class=\\\"ocr-confidence ${confClass}\\\">${confLabel}</span>\u001b[39m\n\u001b[31m          <span style=\\\"color:var(--t4);font-family:var(--fnt-mono);font-size:.6rem;\\\">via ${s.source}</span>\u001b[39m\n\u001b[31m          <button class=\\\"ocr-ai-btn\\\" onclick=\\\"reOCR(${i})\\\">🔄 Re-OCR</button>\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m    </div>`;\u001b[39m\n\u001b[31m  }).join('');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31masync function reOCR(i) {\u001b[39m\n\u001b[31m  const s = slipOCRData[i];\u001b[39m\n\u001b[31m  showOCRWorking(true, `Re-running OCR on ${s.filename}...`);\u001b[39m\n\u001b[31m  try {\u001b[39m\n\u001b[31m    const result = await window.runOCROnImage(s.imageData, s.filename);\u001b[39m\n\u001b[31m    slipOCRData[i] = { ...s, ...result };\u001b[39m\n\u001b[31m    renderOCRResults();\u001b[39m\n\u001b[31m    showToast('Re-OCR complete', 'ok');\u001b[39m\n\u001b[31m  } catch(e) {\u001b[39m\n\u001b[31m    showToast('OCR failed', 'err');\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m  showOCRWorking(false);\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction clearSlips() {\u001b[39m\n\u001b[31m  slipOCRData = [];\u001b[39m\n\u001b[31m  document.getElementById('ocrResults').classList.remove('visible');\u001b[39m\n\u001b[31m  document.getElementById('ocrSlipList').innerHTML = '';\u001b[39m\n\u001b[31m  const sum = document.getElementById('ocrSummary');\u001b[39m\n\u001b[31m  if (sum) { sum.style.display = 'none'; sum.innerHTML = ''; }\u001b[39m\n\u001b[31m  document.getElementById('shareTargetInfo').style.display = 'none';\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction applyAllDockets() {\u001b[39m\n\u001b[31m  // Match each OCR result to a consignee by order or by name matching\u001b[39m\n\u001b[31m  const consList = document.getElementById('consList');\u001b[39m\n\u001b[31m  const cards = consList.querySelectorAll('.cons-card');\u001b[39m\n\u001b[31m  slipOCRData.forEach((s, i) => {\u001b[39m\n\u001b[31m    if (!s.docket) return;\u001b[39m\n\u001b[31m    const card = cards[i];\u001b[39m\n\u001b[31m    if (card) {\u001b[39m\n\u001b[31m      const docketInput = card.querySelector('.cons-docket-input');\u001b[39m\n\u001b[31m      if (docketInput) {\u001b[39m\n\u001b[31m        docketInput.value = s.docket;\u001b[39m\n\u001b[31m        showLinkedDocket(card, s.docket);\u001b[39m\n\u001b[31m      }\u001b[39m\n\u001b[31m    }\u001b[39m\n\u001b[31m  });\u001b[39m\n\u001b[31m  showToast(`Applied ${slipOCRData.filter(s=>s.docket).length} dockets ✓`, 'ok');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31m// STORE LIST — with column mapping UI\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31mlet _rawRows = [];      // raw rows from file, kept for re-mapping\u001b[39m\n\u001b[31mlet _rawHeaders = [];   // headers from file·\u001b[39m\n\u001b[31masync function handleStoreFile(input) {\u001b[39m\n\u001b[31m  const file = input.files[0];\u001b[39m\n\u001b[31m  if (!file) return;\u001b[39m\n\u001b[31m  showToast('Reading file...', 'info');·\u001b[39m\n\u001b[31m  const data = await readFileAsDataURL(file);\u001b[39m\n\u001b[31m  const base64 = data.split(',')[1];\u001b[39m\n\u001b[31m  const binary = atob(base64);\u001b[39m\n\u001b[31m  const bytes = new Uint8Array(binary.length);\u001b[39m\n\u001b[31m  for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);·\u001b[39m\n\u001b[31m  let rows = [];\u001b[39m\n\u001b[31m  try {\u001b[39m\n\u001b[31m    if (file.name.endsWith('.csv')) {\u001b[39m\n\u001b[31m      const text = new TextDecoder().decode(bytes);\u001b[39m\n\u001b[31m      rows = text.split('\\\\n').map(r => r.split(',').map(c => c.trim().replace(/^\\\"|\\\"$/g, '')));\u001b[39m\n\u001b[31m    } else {\u001b[39m\n\u001b[31m      const wb = XLSX.read(bytes, { type: 'array' });\u001b[39m\n\u001b[31m      const ws = wb.Sheets[wb.SheetNames[0]];\u001b[39m\n\u001b[31m      rows = XLSX.utils.sheet_to_json(ws, { header: 1 });\u001b[39m\n\u001b[31m    }\u001b[39m\n\u001b[31m  } catch(e) { showToast('File read error: ' + e.message, 'err'); return; }·\u001b[39m\n\u001b[31m  if (!rows.length || !rows[0]) { showToast('File appears empty', 'err'); return; }·\u001b[39m\n\u001b[31m  _rawHeaders = (rows[0] || []).map(h => (h || '').toString().trim());\u001b[39m\n\u001b[31m  _rawRows = rows.slice(1).filter(r => r && r.some(c => c !== '' && c != null));·\u001b[39m\n\u001b[31m  document.getElementById('storeTableArea').style.display = 'block';\u001b[39m\n\u001b[31m  renderMappingUI(file.name);\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// ── COLUMN MAPPING FIELDS ─────────────────────────────\u001b[39m\n\u001b[31mconst MAPPING_FIELDS = [\u001b[39m\n\u001b[31m  { key: 'name',    label: 'Store / Partner Name', required: true,\u001b[39m\n\u001b[31m    keywords: ['store','shop','name','outlet','consignee','destination','partner','dealer','client','retailer'] },\u001b[39m\n\u001b[31m  { key: 'city',    label: 'City / Location',       required: false,\u001b[39m\n\u001b[31m    keywords: ['city','town','district','location','area','region','place'] },\u001b[39m\n\u001b[31m  { key: 'phone',   label: 'Phone / Mobile',        required: false,\u001b[39m\n\u001b[31m    keywords: ['phone','mobile','contact','tel','number','mob'] },\u001b[39m\n\u001b[31m  { key: 'docket',  label: 'Docket / AWB No.',      required: false,\u001b[39m\n\u001b[31m    keywords: ['docket','awb','tracking','courier','dkt','consignment','cno','waybill'] },\u001b[39m\n\u001b[31m  { key: 'address', label: 'Address',               required: false,\u001b[39m\n\u001b[31m    keywords: ['address','addr','street','locality','area'] },\u001b[39m\n\u001b[31m  { key: 'pincode', label: 'Pincode / PIN',          required: false,\u001b[39m\n\u001b[31m    keywords: ['pin','pincode','postal','zip','code'] },\u001b[39m\n\u001b[31m];·\u001b[39m\n\u001b[31m// Saved mapping key based on column fingerprint (so same client file auto-maps)\u001b[39m\n\u001b[31mfunction getMappingKey() {\u001b[39m\n\u001b[31m  return 'colmap_' + _rawHeaders.join('|').toLowerCase().replace(/\\\\s+/g,'');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// ── Detect if a column is a combined/multiline address cell ─────────────\u001b[39m\n\u001b[31mfunction isCombinedColumn(colIdx) {\u001b[39m\n\u001b[31m  if (colIdx < 0) return false;\u001b[39m\n\u001b[31m  // Check first 3 data rows — if any cell contains newline or has 6+ digit number (pincode) = combined\u001b[39m\n\u001b[31m  const samples = _rawRows.slice(0, 3).map(r => (r[colIdx] || '').toString());\u001b[39m\n\u001b[31m  return samples.some(s => s.includes('\\\\n') || /\\\\b\\\\d{6}\\\\b/.test(s) || s.split(',').length >= 3);\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction renderMappingUI(filename) {\u001b[39m\n\u001b[31m  const saved = (() => { try { return JSON.parse(localStorage.getItem(getMappingKey()) || 'null'); } catch(e) { return null; } })();\u001b[39m\n\u001b[31m  const isSaved = !!saved;\u001b[39m\n\u001b[31m  const colInfoEl = document.getElementById('storeColInfo');\u001b[39m\n\u001b[31m  colInfoEl.style.display = 'block';·\u001b[39m\n\u001b[31m  const suggested = {};\u001b[39m\n\u001b[31m  MAPPING_FIELDS.forEach(f => {\u001b[39m\n\u001b[31m    const idx = findCol(_rawHeaders, f.keywords);\u001b[39m\n\u001b[31m    suggested[f.key] = saved ? (saved[f.key] ?? idx) : idx;\u001b[39m\n\u001b[31m  });·\u001b[39m\n\u001b[31m  // Detect if address column is combined\u001b[39m\n\u001b[31m  const addrIdx = suggested.address >= 0 ? suggested.address : findCol(_rawHeaders, ['address','addr']);\u001b[39m\n\u001b[31m  const looksCombo = isCombinedColumn(addrIdx) || isCombinedColumn(suggested.name);\u001b[39m\n\u001b[31m  const comboCol   = looksCombo ? (addrIdx >= 0 ? addrIdx : suggested.name) : -1;\u001b[39m\n\u001b[31m  const comboPreview = comboCol >= 0 && _rawRows[0] ? (_rawRows[0][comboCol] || '').toString().slice(0, 120) : '';·\u001b[39m\n\u001b[31m  colInfoEl.innerHTML = `\u001b[39m\n\u001b[31m    <div style=\\\"background:var(--sur2);border:1px solid var(--bdr);border-radius:var(--r-sm);padding:12px;\\\">\u001b[39m\n\u001b[31m      <div style=\\\"display:flex;align-items:center;justify-content:space-between;margin-bottom:10px;\\\">\u001b[39m\n\u001b[31m        <div style=\\\"font-family:var(--fnt-mono);font-size:.68rem;color:var(--sky);\\\">\u001b[39m\n\u001b[31m          📋 ${filename} &nbsp;·&nbsp; ${_rawRows.length} rows &nbsp;·&nbsp; ${_rawHeaders.length} columns\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m        ${isSaved ? '<span class=\\\"map-saved-badge\\\">✓ mapping remembered</span>' : ''}\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      <div style=\\\"font-size:.68rem;color:var(--t3);font-family:var(--fnt-mono);margin-bottom:10px;word-break:break-all;\\\">\u001b[39m\n\u001b[31m        Columns: <span style=\\\"color:var(--t2);\\\">${_rawHeaders.map((h,i)=>`[${i+1}] ${h||'?'}`).join(' · ')}</span>\u001b[39m\n\u001b[31m      </div>·\u001b[39m\n\u001b[31m      ${looksCombo ? `\u001b[39m\n\u001b[31m      <!-- COMBINED COLUMN DETECTED -->\u001b[39m\n\u001b[31m      <div style=\\\"background:rgba(56,189,248,.08);border:1px solid rgba(56,189,248,.3);\u001b[39m\n\u001b[31m        border-radius:var(--r-sm);padding:10px 12px;margin-bottom:10px;\\\">\u001b[39m\n\u001b[31m        <div style=\\\"font-size:.72rem;color:var(--sky);font-weight:600;margin-bottom:6px;\\\">\u001b[39m\n\u001b[31m          🤖 Combined Address Column Detected\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m        <div style=\\\"font-size:.68rem;color:var(--t2);font-family:var(--fnt-mono);\u001b[39m\n\u001b[31m          white-space:pre-wrap;background:var(--bg);padding:6px 8px;border-radius:4px;\u001b[39m\n\u001b[31m          margin-bottom:8px;max-height:80px;overflow:hidden;\\\">${comboPreview}</div>\u001b[39m\n\u001b[31m        <div style=\\\"font-size:.7rem;color:var(--t3);margin-bottom:8px;\\\">\u001b[39m\n\u001b[31m          AI will extract: Name · Address · City · Pincode · Phone from each cell.\u001b[39m\n\u001b[31m          Runs in browser (Transformers.js) with Ollama fallback.\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m        <div style=\\\"display:flex;align-items:center;gap:8px;\\\">\u001b[39m\n\u001b[31m          <label style=\\\"display:flex;align-items:center;gap:6px;font-size:.72rem;color:var(--t1);cursor:pointer;\\\">\u001b[39m\n\u001b[31m            <input type=\\\"checkbox\\\" id=\\\"useAIParse\\\" checked style=\\\"cursor:pointer;\\\">\u001b[39m\n\u001b[31m            Use AI to parse combined column\u001b[39m\n\u001b[31m          </label>\u001b[39m\n\u001b[31m          <select id=\\\"comboColSel\\\" style=\\\"background:var(--sur2);border:1px solid var(--bdr);\u001b[39m\n\u001b[31m            color:var(--t1);font-family:var(--fnt-mono);font-size:.72rem;padding:4px 8px;\u001b[39m\n\u001b[31m            border-radius:var(--r-sm);outline:none;\\\">\u001b[39m\n\u001b[31m            ${_rawHeaders.map((h,i)=>`<option value=\\\"${i}\\\" ${i===comboCol?'selected':''}>${h||'(col '+(i+1)+')'}</option>`).join('')}\u001b[39m\n\u001b[31m          </select>\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      ` : ''}·\u001b[39m\n\u001b[31m      <div id=\\\"manualMapSection\\\" style=\\\"${looksCombo ? 'display:none;' : ''}\\\">\u001b[39m\n\u001b[31m        <div style=\\\"font-size:.68rem;color:var(--t3);margin-bottom:6px;\\\">\u001b[39m\n\u001b[31m          ${looksCombo ? 'Manual mapping (used when AI parse is off, or for other columns):' : 'Map each field to your column:'}\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m        <table class=\\\"map-table\\\">\u001b[39m\n\u001b[31m          <thead><tr><th>Field</th><th>Your column</th><th>Preview (row 1)</th></tr></thead>\u001b[39m\n\u001b[31m          <tbody>\u001b[39m\n\u001b[31m            ${MAPPING_FIELDS.map(f => {\u001b[39m\n\u001b[31m              const selIdx = suggested[f.key] ?? -1;\u001b[39m\n\u001b[31m              const preview = selIdx >= 0 && _rawRows[0] ? ((_rawRows[0][selIdx]||'').toString().slice(0,40)) : '—';\u001b[39m\n\u001b[31m              return `<tr>\u001b[39m\n\u001b[31m                <td style=\\\"white-space:nowrap;color:var(--t1);font-weight:500;font-size:.75rem;\\\">\u001b[39m\n\u001b[31m                  ${f.label}${f.required ? '<span class=\\\"map-req\\\">*</span>' : ''}\u001b[39m\n\u001b[31m                </td>\u001b[39m\n\u001b[31m                <td>\u001b[39m\n\u001b[31m                  <select id=\\\"mapsel_${f.key}\\\" onchange=\\\"updateMappingPreview('${f.key}')\\\">\u001b[39m\n\u001b[31m                    ${_rawHeaders.map((h,i)=>\u001b[39m\n\u001b[31m                      `<option value=\\\"${i}\\\" ${i===selIdx?'selected':''}>${h||'(col '+(i+1)+')'}</option>`\u001b[39m\n\u001b[31m                    ).join('')}\u001b[39m\n\u001b[31m                    <option value=\\\"-1\\\" ${selIdx===-1?'selected':''}>— not in file —</option>\u001b[39m\n\u001b[31m                  </select>\u001b[39m\n\u001b[31m                </td>\u001b[39m\n\u001b[31m                <td id=\\\"mapprev_${f.key}\\\" style=\\\"font-family:var(--fnt-mono);font-size:.68rem;\u001b[39m\n\u001b[31m                  color:var(--t2);max-width:90px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;\\\">\u001b[39m\n\u001b[31m                  ${preview}\u001b[39m\n\u001b[31m                </td>\u001b[39m\n\u001b[31m              </tr>`;\u001b[39m\n\u001b[31m            }).join('')}\u001b[39m\n\u001b[31m          </tbody>\u001b[39m\n\u001b[31m        </table>\u001b[39m\n\u001b[31m      </div>·\u001b[39m\n\u001b[31m      <!-- Also map docket/courier no column separately — always shown -->\u001b[39m\n\u001b[31m      <div style=\\\"margin-top:10px;padding-top:10px;border-top:1px solid var(--bdr);\\\">\u001b[39m\n\u001b[31m        <div style=\\\"font-size:.68rem;color:var(--t3);margin-bottom:6px;font-family:var(--fnt-mono);\\\">\u001b[39m\n\u001b[31m          DOCKET / COURIER NO. COLUMN (required for tracking):\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m        <div style=\\\"display:flex;gap:8px;align-items:center;\\\">\u001b[39m\n\u001b[31m          <select id=\\\"docketColSel\\\" style=\\\"background:var(--sur2);border:1px solid var(--bdr);\u001b[39m\n\u001b[31m            color:var(--acc);font-family:var(--fnt-mono);font-size:.78rem;padding:6px 10px;\u001b[39m\n\u001b[31m            border-radius:var(--r-sm);outline:none;flex:1;\\\">\u001b[39m\n\u001b[31m            <option value=\\\"-1\\\">— Docket not in file —</option>\u001b[39m\n\u001b[31m            ${_rawHeaders.map((h,i)=>`<option value=\\\"${i}\\\" ${i===(suggested.docket??-1)?'selected':''}>${h||'(col '+(i+1)+')'}</option>`).join('')}\u001b[39m\n\u001b[31m          </select>\u001b[39m\n\u001b[31m          <span id=\\\"docketPrev\\\" style=\\\"font-family:var(--fnt-mono);font-size:.72rem;color:var(--acc);\\\">\u001b[39m\n\u001b[31m            ${suggested.docket >= 0 && _rawRows[0] ? (_rawRows[0][suggested.docket]||'') : ''}\u001b[39m\n\u001b[31m          </span>\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m      </div>·\u001b[39m\n\u001b[31m      <div class=\\\"btn-row\\\" style=\\\"margin-top:12px;\\\">\u001b[39m\n\u001b[31m        <button class=\\\"btn btn-ghost\\\" style=\\\"font-size:.78rem;\\\" onclick=\\\"clearStores()\\\">✕ Cancel</button>\u001b[39m\n\u001b[31m        <button class=\\\"btn btn-orange\\\" style=\\\"font-size:.78rem;\\\" onclick=\\\"applyMapping()\\\">✓ Apply Mapping</button>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m  `;\u001b[39m\n\u001b[31m  document.getElementById('storeTable').innerHTML = '';\u001b[39m\n\u001b[31m  document.getElementById('storeAddPanel').style.display = 'none';·\u001b[39m\n\u001b[31m  // Update docket preview on change\u001b[39m\n\u001b[31m  const docketSel = document.getElementById('docketColSel');\u001b[39m\n\u001b[31m  if (docketSel) docketSel.addEventListener('change', () => {\u001b[39m\n\u001b[31m    const idx = parseInt(docketSel.value);\u001b[39m\n\u001b[31m    const prev = document.getElementById('docketPrev');\u001b[39m\n\u001b[31m    if (prev) prev.textContent = idx >= 0 && _rawRows[0] ? (_rawRows[0][idx]||'') : '';\u001b[39m\n\u001b[31m  });·\u001b[39m\n\u001b[31m  // Toggle manual map opacity when AI checkbox changes\u001b[39m\n\u001b[31m  const aiChk = document.getElementById('useAIParse');\u001b[39m\n\u001b[31m  const manSec = document.getElementById('manualMapSection');\u001b[39m\n\u001b[31m  if (aiChk && manSec) {\u001b[39m\n\u001b[31m    aiChk.addEventListener('change', () => {\u001b[39m\n\u001b[31m      manSec.style.display = aiChk.checked ? 'none' : 'block';\u001b[39m\n\u001b[31m    });\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction updateMappingPreview(key) {\u001b[39m\n\u001b[31m  const sel = document.getElementById('mapsel_' + key);\u001b[39m\n\u001b[31m  if (!sel) return;\u001b[39m\n\u001b[31m  const idx = parseInt(sel.value);\u001b[39m\n\u001b[31m  const prev = document.getElementById('mapprev_' + key);\u001b[39m\n\u001b[31m  if (prev) prev.textContent = (idx >= 0 && _rawRows[0]) ? ((_rawRows[0][idx]||'').toString().slice(0,40)) : '—';\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// ── REGEX-FIRST ADDRESS PARSER ───────────────────────────────────────────\u001b[39m\n\u001b[31m// Improved Indian address parser\u001b[39m\n\u001b[31m// Handles formats seen in real BTL dispatch files\u001b[39m\n\u001b[31mfunction parseAddressRegex(raw) {\u001b[39m\n\u001b[31m  const text = raw.toString().replace(/\\\\r/g, '').trim();\u001b[39m\n\u001b[31m  const lines = text.split('\\\\n').map(l => l.trim()).filter(Boolean);\u001b[39m\n\u001b[31m  const result = { name:'', address:'', city:'', pincode:'', phone:'', confidence:0 };·\u001b[39m\n\u001b[31m  // ── PHONE: Indian mobile 6-9 prefix, 10 digits ──────────────────────────\u001b[39m\n\u001b[31m  // Also handles \\\"Phone : 9876543210\\\", \\\"M.No.-9876543210\\\", \\\"Mobile - 9876543210\\\"\u001b[39m\n\u001b[31m  const phoneMatch = text.match(/(?:Phone|Mobile|Mob|M\\\\.No\\\\.?|Ph\\\\.?)?\\\\s*[:\\\\-]?\\\\s*\\\\b([6-9]\\\\d{9})\\\\b/i);\u001b[39m\n\u001b[31m  if (phoneMatch) result.phone = phoneMatch[1];·\u001b[39m\n\u001b[31m  // ── PINCODE: 6-digit standalone number ──────────────────────────────────\u001b[39m\n\u001b[31m  // Must not be part of a longer number (e.g. not an account number)\u001b[39m\n\u001b[31m  const pinMatch = text.match(/(?<![\\\\d])([1-9]\\\\d{5})(?![\\\\d])/);\u001b[39m\n\u001b[31m  if (pinMatch) result.pincode = pinMatch[1];·\u001b[39m\n\u001b[31m  // ── CITY: the token immediately before the pincode ───────────────────────\u001b[39m\n\u001b[31m  const indianStates = ['rajasthan','gujarat','maharashtra','punjab','haryana',\u001b[39m\n\u001b[31m    'uttar pradesh','madhya pradesh','karnataka','tamil nadu','andhra pradesh',\u001b[39m\n\u001b[31m    'telangana','kerala','west bengal','bihar','odisha','jharkhand','assam',\u001b[39m\n\u001b[31m    'himachal pradesh','uttarakhand','chhattisgarh','goa','delhi','chandigarh',\u001b[39m\n\u001b[31m    'raj.','raj'];\u001b[39m\n\u001b[31m  if (result.pincode) {\u001b[39m\n\u001b[31m    const pinPos = text.indexOf(result.pincode);\u001b[39m\n\u001b[31m    const beforePin = text.slice(0, pinPos);\u001b[39m\n\u001b[31m    // Strategy 1: split by comma/newline, walk backwards skipping state names\u001b[39m\n\u001b[31m    const chunks = beforePin.split(/[,\\\\n]/).map(c => c.trim()).filter(Boolean);\u001b[39m\n\u001b[31m    for (let i = chunks.length - 1; i >= 0; i--) {\u001b[39m\n\u001b[31m      const chunk = chunks[i].replace(/\\\\d+/g,'').replace(/[()]/g,'').replace(/[.-]/g,'').trim();\u001b[39m\n\u001b[31m      const lower = chunk.toLowerCase();\u001b[39m\n\u001b[31m      if (!chunk || chunk.length < 2) continue;\u001b[39m\n\u001b[31m      if (indianStates.includes(lower)) continue;\u001b[39m\n\u001b[31m      // Skip chunks that look like addresses (contain numbers or long strings)\u001b[39m\n\u001b[31m      if (/\\\\d/.test(chunk)) continue;\u001b[39m\n\u001b[31m      result.city = chunk;\u001b[39m\n\u001b[31m      break;\u001b[39m\n\u001b[31m    }\u001b[39m\n\u001b[31m    // Strategy 2: if still no city, look for last capitalised word before pincode\u001b[39m\n\u001b[31m    // handles \\\"compound paota Jodhpur 342006\\\" → Jodhpur\u001b[39m\n\u001b[31m    if (!result.city) {\u001b[39m\n\u001b[31m      const wordsBefore = beforePin.replace(/[,\\\\n]/g,' ').trim().split(/\\\\s+/);\u001b[39m\n\u001b[31m      for (let i = wordsBefore.length - 1; i >= 0; i--) {\u001b[39m\n\u001b[31m        const w = wordsBefore[i].replace(/[^a-zA-Z]/g,'');\u001b[39m\n\u001b[31m        if (w.length < 3) continue;\u001b[39m\n\u001b[31m        if (indianStates.includes(w.toLowerCase())) continue;\u001b[39m\n\u001b[31m        // Prefer words that start with capital letter (likely a proper noun/city)\u001b[39m\n\u001b[31m        if (/^[A-Z]/.test(w)) { result.city = w; break; }\u001b[39m\n\u001b[31m      }\u001b[39m\n\u001b[31m    }\u001b[39m\n\u001b[31m  }·\u001b[39m\n\u001b[31m  // ── NAME: first meaningful line, extract shop name ───────────────────────\u001b[39m\n\u001b[31m  // Patterns:\u001b[39m\n\u001b[31m  //   \\\"Sanwar Mal Saini, C/o R G COMPUTERS\\\" → name = \\\"R G COMPUTERS\\\"\u001b[39m\n\u001b[31m  //   \\\"SANWAR MAL SAINI, C/o ADVANCE TECHNOLOGIES\\\" → name = \\\"ADVANCE TECHNOLOGIES\\\"\u001b[39m\n\u001b[31m  //   \\\"Sunil Vaishnav\\\" → name = \\\"Sunil Vaishnav\\\" (no C/o)\u001b[39m\n\u001b[31m  //   \\\"Baltej Singh, Professional computer care\\\" → name = \\\"Professional computer care\\\"\u001b[39m\n\u001b[31m  if (lines.length > 0) {\u001b[39m\n\u001b[31m    const firstLine = lines[0];\u001b[39m\n\u001b[31m    // Check for C/o or C/O pattern — shop name comes after it\u001b[39m\n\u001b[31m    const coMatch = firstLine.match(/C\\\\/?[Oo]\\\\.?\\\\s+(.+)/);\u001b[39m\n\u001b[31m    if (coMatch) {\u001b[39m\n\u001b[31m      result.name = coMatch[1].trim();\u001b[39m\n\u001b[31m    } else {\u001b[39m\n\u001b[31m      // Check if there is a comma — second part might be the shop name\u001b[39m\n\u001b[31m      const parts = firstLine.split(',').map(p => p.trim());\u001b[39m\n\u001b[31m      if (parts.length >= 2 && parts[1].length > 3) {\u001b[39m\n\u001b[31m        // Heuristic: if second part looks like a business name (has uppercase or >5 chars)\u001b[39m\n\u001b[31m        const hasUpperOrLong = /[A-Z]{2,}/.test(parts[1]) || parts[1].length > 8;\u001b[39m\n\u001b[31m        result.name = hasUpperOrLong ? parts[1] : parts[0];\u001b[39m\n\u001b[31m      } else {\u001b[39m\n\u001b[31m        result.name = parts[0];\u001b[39m\n\u001b[31m      }\u001b[39m\n\u001b[31m    }\u001b[39m\n\u001b[31m  }·\u001b[39m\n\u001b[31m  // ── ADDRESS: lines between name and phone/pincode line ───────────────────\u001b[39m\n\u001b[31m  const phoneLineIdx = lines.findIndex(l => result.phone && l.includes(result.phone));\u001b[39m\n\u001b[31m  const addrLines = lines.filter((l, i) => {\u001b[39m\n\u001b[31m    if (i === 0) return false;\u001b[39m\n\u001b[31m    if (i === phoneLineIdx) return false;\u001b[39m\n\u001b[31m    // Exclude lines that are just the phone number\u001b[39m\n\u001b[31m    if (/^[6-9]\\\\d{9}$/.test(l.replace(/\\\\s/g,''))) return false;\u001b[39m\n\u001b[31m    return true;\u001b[39m\n\u001b[31m  });\u001b[39m\n\u001b[31m  result.address = addrLines.join(', ').replace(/,\\\\s*,/g, ',').trim();·\u001b[39m\n\u001b[31m  // ── CONFIDENCE: simple score based on what we got ────────────────────────\u001b[39m\n\u001b[31m  let conf = 0;\u001b[39m\n\u001b[31m  if (result.name)    conf += 30;\u001b[39m\n\u001b[31m  if (result.city)    conf += 25;\u001b[39m\n\u001b[31m  if (result.pincode) conf += 25;\u001b[39m\n\u001b[31m  if (result.phone)   conf += 20;\u001b[39m\n\u001b[31m  result.confidence = conf;·\u001b[39m\n\u001b[31m  return result;\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// ── AI ADDRESS PARSER (Ollama fallback) ──────────────────────────────────\u001b[39m\n\u001b[31m// Fetches past corrections from localStorage to improve prompt accuracy\u001b[39m\n\u001b[31mfunction getPastCorrections() {\u001b[39m\n\u001b[31m  try {\u001b[39m\n\u001b[31m    const log = JSON.parse(localStorage.getItem('360lm_parse_corrections') || '[]');\u001b[39m\n\u001b[31m    return log.slice(-10); // last 10 corrections as examples\u001b[39m\n\u001b[31m  } catch(e) { return []; }\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction saveCorrectionLocal(entry) {\u001b[39m\n\u001b[31m  try {\u001b[39m\n\u001b[31m    const log = JSON.parse(localStorage.getItem('360lm_parse_corrections') || '[]');\u001b[39m\n\u001b[31m    log.push(entry);\u001b[39m\n\u001b[31m    // Keep only last 50\u001b[39m\n\u001b[31m    localStorage.setItem('360lm_parse_corrections', JSON.stringify(log.slice(-50)));\u001b[39m\n\u001b[31m  } catch(e) {}\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31masync function parseAddressWithOllama(raw) {\u001b[39m\n\u001b[31m  // Calls the server-side /dispatch-ai/address proxy (avoids CORS + port 11434 issues)\u001b[39m\n\u001b[31m  try {\u001b[39m\n\u001b[31m    const resp = await fetch('/dispatch-ai/address', {\u001b[39m\n\u001b[31m      method: 'POST',\u001b[39m\n\u001b[31m      headers: { 'Content-Type': 'application/json' },\u001b[39m\n\u001b[31m      body: JSON.stringify({ text: raw }),\u001b[39m\n\u001b[31m      signal: AbortSignal.timeout(120000)\u001b[39m\n\u001b[31m    });\u001b[39m\n\u001b[31m    const data = await resp.json();\u001b[39m\n\u001b[31m    if (data.success) return data;\u001b[39m\n\u001b[31m  } catch(e) {}\u001b[39m\n\u001b[31m  return null;\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31masync function parseAddressCell(raw) {\u001b[39m\n\u001b[31m  // Step 1: regex — always runs first, no network call\u001b[39m\n\u001b[31m  const regex = parseAddressRegex(raw);\u001b[39m\n\u001b[31m  if (regex.name) return { ...regex, source: 'regex' };·\u001b[39m\n\u001b[31m  // Step 2: Ollama only if regex got nothing AND Ollama is configured\u001b[39m\n\u001b[31m  // Uses a 3-second timeout to prevent browser hang\u001b[39m\n\u001b[31m  if (window.appConfig.ocr_mode !== 'transformers') {\u001b[39m\n\u001b[31m    try {\u001b[39m\n\u001b[31m      const ai = await Promise.race([\u001b[39m\n\u001b[31m        parseAddressWithOllama(raw),\u001b[39m\n\u001b[31m        new Promise((_, reject) => setTimeout(() => reject(new Error('timeout')), 120000))\u001b[39m\n\u001b[31m      ]);\u001b[39m\n\u001b[31m      if (ai && ai.name) return { ...ai, source: 'ollama' };\u001b[39m\n\u001b[31m    } catch(e) { /* timeout or error — continue to fallback */ }\u001b[39m\n\u001b[31m  }·\u001b[39m\n\u001b[31m  // Step 3: fallback — first line as name\u001b[39m\n\u001b[31m  const lines = raw.toString().split('\\\\n').map(l=>l.trim()).filter(Boolean);\u001b[39m\n\u001b[31m  return { name: lines[0]||raw.slice(0,40), address: raw, city:'', pincode:'', phone:'', source:'fallback' };\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31masync function applyMapping() {\u001b[39m\n\u001b[31m  const useAI   = document.getElementById('useAIParse')?.checked ?? false;\u001b[39m\n\u001b[31m  const comboSel = document.getElementById('comboColSel');\u001b[39m\n\u001b[31m  const comboIdx = comboSel ? parseInt(comboSel.value) : -1;\u001b[39m\n\u001b[31m  const docketIdx = parseInt(document.getElementById('docketColSel')?.value ?? -1);·\u001b[39m\n\u001b[31m  // Build manual mapping as fallback\u001b[39m\n\u001b[31m  const mapping = {};\u001b[39m\n\u001b[31m  MAPPING_FIELDS.forEach(f => {\u001b[39m\n\u001b[31m    const sel = document.getElementById('mapsel_' + f.key);\u001b[39m\n\u001b[31m    mapping[f.key] = sel ? parseInt(sel.value) : -1;\u001b[39m\n\u001b[31m  });\u001b[39m\n\u001b[31m  // Override docket from dedicated selector\u001b[39m\n\u001b[31m  mapping.docket = docketIdx;·\u001b[39m\n\u001b[31m  // Determine if we have a valid base for stores\u001b[39m\n\u001b[31m  const nameIdx = useAI && comboIdx >= 0 ? comboIdx : mapping.name;\u001b[39m\n\u001b[31m  if (nameIdx === -1 && !useAI) { showToast('Store Name column is required', 'err'); return; }·\u001b[39m\n\u001b[31m  // Save mapping\u001b[39m\n\u001b[31m  try { localStorage.setItem(getMappingKey(), JSON.stringify({...mapping, _useAI: useAI, _comboIdx: comboIdx})); } catch(e) {}·\u001b[39m\n\u001b[31m  if (useAI && comboIdx >= 0) {\u001b[39m\n\u001b[31m    // AI path — parse each combined cell\u001b[39m\n\u001b[31m    showToast('AI parsing addresses...', 'info');\u001b[39m\n\u001b[31m    showOCRWorking(true, `Parsing 0/${_rawRows.length} addresses...`);\u001b[39m\n\u001b[31m    stores = [];\u001b[39m\n\u001b[31m    for (let i = 0; i < _rawRows.length; i++) {\u001b[39m\n\u001b[31m      const r = _rawRows[i];\u001b[39m\n\u001b[31m      const raw = (r[comboIdx] || '').toString().trim();\u001b[39m\n\u001b[31m      if (!raw) continue;\u001b[39m\n\u001b[31m      showOCRWorking(true, `Parsing ${i+1}/${_rawRows.length} — ${raw.slice(0,30)}...`);\u001b[39m\n\u001b[31m      // Yield to browser every row to prevent UI freeze\u001b[39m\n\u001b[31m      await new Promise(r => setTimeout(r, 0));\u001b[39m\n\u001b[31m      const parsed = await parseAddressCell(raw);\u001b[39m\n\u001b[31m      stores.push({\u001b[39m\n\u001b[31m        id: i,\u001b[39m\n\u001b[31m        name:    parsed.name    || raw.slice(0,40),\u001b[39m\n\u001b[31m        city:    parsed.city    || '',\u001b[39m\n\u001b[31m        phone:   parsed.phone   || '',\u001b[39m\n\u001b[31m        address: parsed.address || raw,\u001b[39m\n\u001b[31m        pincode: parsed.pincode || '',\u001b[39m\n\u001b[31m        docket:  docketIdx >= 0 ? (r[docketIdx]||'').toString().trim() : '',\u001b[39m\n\u001b[31m        _parseSource: parsed.source,\u001b[39m\n\u001b[31m        _confidence:  parsed.confidence || 0,\u001b[39m\n\u001b[31m        _rawCell:     raw,\u001b[39m\n\u001b[31m        _origName:    parsed.name || '',\u001b[39m\n\u001b[31m        _origCity:    parsed.city || '',\u001b[39m\n\u001b[31m        matched: false\u001b[39m\n\u001b[31m      });\u001b[39m\n\u001b[31m    }\u001b[39m\n\u001b[31m    showOCRWorking(false);\u001b[39m\n\u001b[31m  } else {\u001b[39m\n\u001b[31m    // Manual mapping path\u001b[39m\n\u001b[31m    stores = _rawRows\u001b[39m\n\u001b[31m      .filter(r => r[nameIdx] && (r[nameIdx]+'').trim())\u001b[39m\n\u001b[31m      .map((r, i) => ({\u001b[39m\n\u001b[31m        id: i,\u001b[39m\n\u001b[31m        name:    mapping.name    >= 0 ? (r[mapping.name]   ||'').toString().trim() : '',\u001b[39m\n\u001b[31m        city:    mapping.city    >= 0 ? (r[mapping.city]   ||'').toString().trim() : '',\u001b[39m\n\u001b[31m        phone:   mapping.phone   >= 0 ? (r[mapping.phone]  ||'').toString().trim() : '',\u001b[39m\n\u001b[31m        docket:  mapping.docket  >= 0 ? (r[mapping.docket] ||'').toString().trim() : '',\u001b[39m\n\u001b[31m        address: mapping.address >= 0 ? (r[mapping.address]||'').toString().trim() : '',\u001b[39m\n\u001b[31m        pincode: mapping.pincode >= 0 ? (r[mapping.pincode]||'').toString().trim() : '',\u001b[39m\n\u001b[31m        matched: false\u001b[39m\n\u001b[31m      }));\u001b[39m\n\u001b[31m  }·\u001b[39m\n\u001b[31m  document.getElementById('storeCount').textContent = `${stores.length} stores loaded`;·\u001b[39m\n\u001b[31m  if (useAI && comboIdx >= 0) {\u001b[39m\n\u001b[31m    // AI path — show row-by-row review panel before adding consignees\u001b[39m\n\u001b[31m    showAddressReviewPanel();\u001b[39m\n\u001b[31m  } else {\u001b[39m\n\u001b[31m    // Manual mapping path — go straight to store table + add panel\u001b[39m\n\u001b[31m    renderStoreTable(mapping.docket >= 0 || docketIdx >= 0);\u001b[39m\n\u001b[31m    const addPanel = document.getElementById('storeAddPanel');\u001b[39m\n\u001b[31m    const addBtn   = document.getElementById('storeAddBtn');\u001b[39m\n\u001b[31m    const addTitle = document.getElementById('storeAddTitle');\u001b[39m\n\u001b[31m    const hasDockets = stores.some(s => s.docket);\u001b[39m\n\u001b[31m    addTitle.textContent = hasDockets\u001b[39m\n\u001b[31m      ? `${stores.filter(s=>s.docket).length} stores with dockets — add as consignees?`\u001b[39m\n\u001b[31m      : `Add ${stores.length} stores as consignees?`;\u001b[39m\n\u001b[31m    addBtn.textContent = `✓ Add ${stores.length} as Consignees`;\u001b[39m\n\u001b[31m    addPanel.style.display = 'block';\u001b[39m\n\u001b[31m    showToast(`${stores.length} stores mapped ✓`, 'ok');\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31m// ADDRESS REVIEW PANEL\u001b[39m\n\u001b[31m// Row-by-row review of AI-parsed addresses before consignees created\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31mfunction showAddressReviewPanel() {\u001b[39m\n\u001b[31m  const pane = document.getElementById('addrReviewPane');\u001b[39m\n\u001b[31m  if (!pane) return;\u001b[39m\n\u001b[31m  renderAddressReviewRows();\u001b[39m\n\u001b[31m  pane.style.display = 'block';\u001b[39m\n\u001b[31m  pane.scrollTop = 0;\u001b[39m\n\u001b[31m  showToast(`Review ${stores.length} parsed addresses`, 'info');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction renderAddressReviewRows() {\u001b[39m\n\u001b[31m  const list = document.getElementById('addrReviewList');\u001b[39m\n\u001b[31m  const summary = document.getElementById('addrReviewSummary');\u001b[39m\n\u001b[31m  if (!list) return;·\u001b[39m\n\u001b[31m  const highConf  = stores.filter(s => (s._confidence||0) >= 80).length;\u001b[39m\n\u001b[31m  const medConf   = stores.filter(s => (s._confidence||0) >= 50 && (s._confidence||0) < 80).length;\u001b[39m\n\u001b[31m  const lowConf   = stores.filter(s => (s._confidence||0) < 50).length;·\u001b[39m\n\u001b[31m  summary.innerHTML = `\u001b[39m\n\u001b[31m    <div class=\\\"vs-item\\\"><div class=\\\"vs-val\\\" style=\\\"color:var(--t1);\\\">${stores.length}</div><div class=\\\"vs-lbl\\\">Total</div></div>\u001b[39m\n\u001b[31m    <div class=\\\"vs-item\\\"><div class=\\\"vs-val\\\" style=\\\"color:var(--grn);\\\">${highConf}</div><div class=\\\"vs-lbl\\\">High conf</div></div>\u001b[39m\n\u001b[31m    <div class=\\\"vs-item\\\"><div class=\\\"vs-val\\\" style=\\\"color:var(--ylw);\\\">${medConf}</div><div class=\\\"vs-lbl\\\">Review</div></div>\u001b[39m\n\u001b[31m    <div class=\\\"vs-item\\\"><div class=\\\"vs-val\\\" style=\\\"color:var(--red);\\\">${lowConf}</div><div class=\\\"vs-lbl\\\">Check</div></div>\u001b[39m\n\u001b[31m    <div class=\\\"vs-item\\\" style=\\\"margin-left:auto;\\\">\u001b[39m\n\u001b[31m      <button class=\\\"btn-sm primary\\\" onclick=\\\"approveAllAddresses()\\\" style=\\\"font-size:.7rem;\\\">✓ Approve All</button>\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m  `;·\u001b[39m\n\u001b[31m  list.innerHTML = stores.map((s, i) => {\u001b[39m\n\u001b[31m    const conf = s._confidence || 0;\u001b[39m\n\u001b[31m    const confClass = conf >= 80 ? 'vf-ok' : conf >= 50 ? 'vf-nodocket' : 'vf-dupe';\u001b[39m\n\u001b[31m    const confLabel = conf >= 80 ? `${conf}% ✓` : conf >= 50 ? `${conf}% review` : `${conf}% check`;\u001b[39m\n\u001b[31m    const rowBorder = conf >= 80 ? 'var(--grn)' : conf >= 50 ? 'var(--ylw)' : 'var(--red)';·\u001b[39m\n\u001b[31m    return `\u001b[39m\n\u001b[31m    <div class=\\\"verify-row\\\" id=\\\"arow_${i}\\\" style=\\\"border-left:3px solid ${rowBorder};\\\">\u001b[39m\n\u001b[31m      <div class=\\\"verify-row-hdr\\\">\u001b[39m\n\u001b[31m        <span class=\\\"verify-num\\\">#${i+1}</span>\u001b[39m\n\u001b[31m        <span class=\\\"verify-store\\\" style=\\\"font-size:.78rem;color:var(--t2);font-weight:400;\u001b[39m\n\u001b[31m          font-family:var(--fnt-mono);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;flex:1;\\\"\u001b[39m\n\u001b[31m          title=\\\"${(s._rawCell||'').replace(/\\\"/g,\\\"'\\\")}\\\">\u001b[39m\n\u001b[31m          ${(s._rawCell||'').slice(0,50).split('\\\\n').join(' ↵ ')}       <span class=\\\"verify-flag ${confClass}\\\" style=\\\"flex-shrink:0;margin-right:6px;\\\">${confLabel} · ${s._parseSource||'?'}</span>\u001b[39m\n\u001b[31m        <button onclick=\\\"deleteReviewRow(${i})\\\"\u001b[39m\n\u001b[31m          style=\\\"background:none;border:1px solid rgba(239,68,68,.4);color:var(--red);\u001b[39m\n\u001b[31m          font-size:.65rem;padding:2px 8px;border-radius:6px;cursor:pointer;\u001b[39m\n\u001b[31m          font-family:var(--fnt-mono);flex-shrink:0;transition:background .2s;\\\"\u001b[39m\n\u001b[31m          onmouseover=\\\"this.style.background='rgba(239,68,68,.15)'\\\"\u001b[39m\n\u001b[31m          onmouseout=\\\"this.style.background='none'\\\">✕ Remove</button>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      <div class=\\\"verify-fields\\\" style=\\\"grid-template-columns:1fr 1fr 1fr 1fr 1fr;\\\">\u001b[39m\n\u001b[31m        <div class=\\\"verify-field\\\">\u001b[39m\n\u001b[31m          <label>Shop / Name <span style=\\\"color:var(--acc);\\\">*</span></label>\u001b[39m\n\u001b[31m          <input type=\\\"text\\\" value=\\\"${s.name||''}\\\" id=\\\"ar_name_${i}\\\"\u001b[39m\n\u001b[31m            oninput=\\\"updateAddrField(${i},'name',this)\\\"\u001b[39m\n\u001b[31m            style=\\\"${!s.name ? 'border-color:var(--red);' : ''}\\\">\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m        <div class=\\\"verify-field\\\">\u001b[39m\n\u001b[31m          <label>City</label>\u001b[39m\n\u001b[31m          <input type=\\\"text\\\" value=\\\"${s.city||''}\\\" id=\\\"ar_city_${i}\\\"\u001b[39m\n\u001b[31m            oninput=\\\"updateAddrField(${i},'city',this)\\\"\u001b[39m\n\u001b[31m            style=\\\"${!s.city ? 'border-color:var(--ylw);' : ''}\\\">\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m        <div class=\\\"verify-field\\\">\u001b[39m\n\u001b[31m          <label>Docket / Courier No.</label>\u001b[39m\n\u001b[31m          <input type=\\\"text\\\" value=\\\"${s.docket||''}\\\" id=\\\"ar_docket_${i}\\\"\u001b[39m\n\u001b[31m            oninput=\\\"updateAddrField(${i},'docket',this)\\\"\u001b[39m\n\u001b[31m            style=\\\"font-family:var(--fnt-mono);${!s.docket?'border-color:var(--ylw);':'color:var(--acc);'}\\\">\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m        <div class=\\\"verify-field\\\">\u001b[39m\n\u001b[31m          <label>Phone</label>\u001b[39m\n\u001b[31m          <input type=\\\"tel\\\" value=\\\"${s.phone||''}\\\" id=\\\"ar_phone_${i}\\\"\u001b[39m\n\u001b[31m            oninput=\\\"updateAddrField(${i},'phone',this)\\\">\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m        <div class=\\\"verify-field\\\">\u001b[39m\n\u001b[31m          <label>Pincode</label>\u001b[39m\n\u001b[31m          <input type=\\\"text\\\" value=\\\"${s.pincode||''}\\\" id=\\\"ar_pin_${i}\\\"\u001b[39m\n\u001b[31m            oninput=\\\"updateAddrField(${i},'pincode',this)\\\">\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      <div style=\\\"font-size:.65rem;color:var(--t3);font-family:var(--fnt-mono);\u001b[39m\n\u001b[31m        margin-top:4px;padding:4px 6px;background:var(--bg);border-radius:4px;word-break:break-all;\\\">\u001b[39m\n\u001b[31m        📋 ${(s._rawCell||'').split('\\\\n').join(' | ')}\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m    </div>`;\u001b[39m\n\u001b[31m  }).join('');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction updateAddrField(i, field, input) {\u001b[39m\n\u001b[31m  if (!stores[i]) return;\u001b[39m\n\u001b[31m  stores[i][field] = input.value.trim();\u001b[39m\n\u001b[31m  // Recompute confidence\u001b[39m\n\u001b[31m  let conf = 0;\u001b[39m\n\u001b[31m  if (stores[i].name)    conf += 30;\u001b[39m\n\u001b[31m  if (stores[i].city)    conf += 25;\u001b[39m\n\u001b[31m  if (stores[i].pincode) conf += 25;\u001b[39m\n\u001b[31m  if (stores[i].phone)   conf += 20;\u001b[39m\n\u001b[31m  stores[i]._confidence = conf;\u001b[39m\n\u001b[31m  input.classList.toggle('changed', true);\u001b[39m\n\u001b[31m  // Update border color on row\u001b[39m\n\u001b[31m  const row = document.getElementById('arow_' + i);\u001b[39m\n\u001b[31m  if (row) row.style.borderLeftColor = conf >= 80 ? 'var(--grn)' : conf >= 50 ? 'var(--ylw)' : 'var(--red)';\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction deleteReviewRow(i) {\u001b[39m\n\u001b[31m  // Mark as deleted — splice from stores array and re-render\u001b[39m\n\u001b[31m  stores.splice(i, 1);\u001b[39m\n\u001b[31m  // Re-assign IDs\u001b[39m\n\u001b[31m  stores.forEach((s, idx) => { s.id = idx; });\u001b[39m\n\u001b[31m  renderAddressReviewRows();\u001b[39m\n\u001b[31m  document.getElementById('storeCount').textContent = `${stores.length} stores loaded`;\u001b[39m\n\u001b[31m  showToast(`Row removed — ${stores.length} remaining`, 'info');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction backToMapping() {\u001b[39m\n\u001b[31m  // Hide review pane — store edits are preserved in stores[] array\u001b[39m\n\u001b[31m  document.getElementById('addrReviewPane').style.display = 'none';\u001b[39m\n\u001b[31m  // Show mapping UI again with current file\u001b[39m\n\u001b[31m  document.getElementById('storeTableArea').style.display = 'block';\u001b[39m\n\u001b[31m  document.getElementById('storeColInfo').style.display = 'block';\u001b[39m\n\u001b[31m  showToast('Your edits are preserved — re-apply mapping to reload', 'info');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction approveAllAddresses() {\u001b[39m\n\u001b[31m  stores.forEach((s, i) => { s._reviewed = true; });\u001b[39m\n\u001b[31m  document.getElementById('addrReviewPane').style.display = 'none';\u001b[39m\n\u001b[31m  applyReviewedStores();\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction confirmAddressReview() {\u001b[39m\n\u001b[31m  // Collect any pending edits, log corrections, close panel\u001b[39m\n\u001b[31m  const corrections = [];\u001b[39m\n\u001b[31m  stores.forEach((s, i) => {\u001b[39m\n\u001b[31m    const origName = s._origName || '';\u001b[39m\n\u001b[31m    const origCity = s._origCity || '';\u001b[39m\n\u001b[31m    if ((origName && origName !== s.name) || (origCity && origCity !== s.city)) {\u001b[39m\n\u001b[31m      corrections.push({ raw: s._rawCell||'', name: s.name, city: s.city, pincode: s.pincode });\u001b[39m\n\u001b[31m    }\u001b[39m\n\u001b[31m  });\u001b[39m\n\u001b[31m  // Save corrections locally for future Ollama prompts\u001b[39m\n\u001b[31m  corrections.forEach(c => saveCorrectionLocal(c));\u001b[39m\n\u001b[31m  // Log to Sheets asynchronously (fire and forget)\u001b[39m\n\u001b[31m  if (corrections.length > 0 && window.appConfig.webhook_url) {\u001b[39m\n\u001b[31m    logParseCorrectionsToSheets(corrections);\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m  document.getElementById('addrReviewPane').style.display = 'none';\u001b[39m\n\u001b[31m  applyReviewedStores();\u001b[39m\n\u001b[31m  showToast(`${corrections.length > 0 ? corrections.length + ' corrections logged · ' : ''}${stores.length} stores ready ✓`, 'ok');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// Clean street address: remove city, pincode, phone if they appear in address field\u001b[39m\n\u001b[31mfunction cleanAddress(addr, city, pincode, phone) {\u001b[39m\n\u001b[31m  if (!addr) return addr;\u001b[39m\n\u001b[31m  let s = addr;\u001b[39m\n\u001b[31m  // Remove phone\u001b[39m\n\u001b[31m  if (phone) s = s.replace(new RegExp(phone.replace(/[.*+?^${}()|[\\\\]\\\\\\\\]/g,'\\\\\\\\$&'), 'g'), '').trim();\u001b[39m\n\u001b[31m  // Remove pincode\u001b[39m\n\u001b[31m  if (pincode) s = s.replace(new RegExp('\\\\\\\\b' + pincode + '\\\\\\\\b', 'g'), '').trim();\u001b[39m\n\u001b[31m  // Remove city (case-insensitive)\u001b[39m\n\u001b[31m  if (city) s = s.replace(new RegExp('\\\\\\\\b' + city.replace(/[.*+?^${}()|[\\\\]\\\\\\\\]/g,'\\\\\\\\$&') + '\\\\\\\\b', 'ig'), '').trim();\u001b[39m\n\u001b[31m  // Remove state names and trailing noise\u001b[39m\n\u001b[31m  const noise = /\\\\b(rajasthan|gujarat|maharashtra|punjab|haryana|uttar pradesh|delhi|raj\\\\.)\\\\b/ig;\u001b[39m\n\u001b[31m  s = s.replace(noise, '');\u001b[39m\n\u001b[31m  // Remove \\\"Mobile :\\\", \\\"Phone :\\\", \\\"M.No.\\\" prefixes and dangling numbers\u001b[39m\n\u001b[31m  s = s.replace(/(?:Mobile|Phone|Mob|M\\\\.No\\\\.?|Ph\\\\.?)\\\\s*[:\\\\-]?\\\\s*/ig, '');\u001b[39m\n\u001b[31m  // Clean up multiple commas/spaces\u001b[39m\n\u001b[31m  s = s.replace(/,\\\\s*,/g, ',').replace(/\\\\s{2,}/g, ' ').replace(/^[\\\\s,]+|[\\\\s,]+$/g, '');\u001b[39m\n\u001b[31m  return s;\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction applyReviewedStores() {\u001b[39m\n\u001b[31m  // Clean address fields before loading stores\u001b[39m\n\u001b[31m  stores.forEach(s => {\u001b[39m\n\u001b[31m    s.address = cleanAddress(s.address, s.city, s.pincode, s.phone);\u001b[39m\n\u001b[31m  });\u001b[39m\n\u001b[31m  renderStoreTable(stores.some(s => s.docket));\u001b[39m\n\u001b[31m  const addPanel = document.getElementById('storeAddPanel');\u001b[39m\n\u001b[31m  const addTitle = document.getElementById('storeAddTitle');\u001b[39m\n\u001b[31m  const addBtn   = document.getElementById('storeAddBtn');\u001b[39m\n\u001b[31m  const hasDockets = stores.some(s => s.docket);\u001b[39m\n\u001b[31m  addTitle.textContent = hasDockets\u001b[39m\n\u001b[31m    ? `${stores.filter(s=>s.docket).length} stores with dockets — add as consignees?`\u001b[39m\n\u001b[31m    : `Add ${stores.length} stores as consignees?`;\u001b[39m\n\u001b[31m  addBtn.textContent = `✓ Add ${stores.length} as Consignees`;\u001b[39m\n\u001b[31m  addPanel.style.display = 'block';\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31masync function logParseCorrectionsToSheets(corrections) {\u001b[39m\n\u001b[31m  const url = window.appConfig.webhook_url;\u001b[39m\n\u001b[31m  if (!url) return;\u001b[39m\n\u001b[31m  try {\u001b[39m\n\u001b[31m    await fetch(url, {\u001b[39m\n\u001b[31m      method: 'POST', mode: 'no-cors',\u001b[39m\n\u001b[31m      headers: { 'Content-Type': 'application/json' },\u001b[39m\n\u001b[31m      body: JSON.stringify({\u001b[39m\n\u001b[31m        type: 'parse_correction',\u001b[39m\n\u001b[31m        corrections,\u001b[39m\n\u001b[31m        colFingerprint: getMappingKey(),\u001b[39m\n\u001b[31m        timestamp: new Date().toISOString()\u001b[39m\n\u001b[31m      })\u001b[39m\n\u001b[31m    });\u001b[39m\n\u001b[31m  } catch(e) {}\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction addStoresToConsignees() {\u001b[39m\n\u001b[31m  const consList = document.getElementById('consList');\u001b[39m\n\u001b[31m  consList.innerHTML = '';\u001b[39m\n\u001b[31m  stores.slice(0, 50).forEach(s => addConsignee(s));\u001b[39m\n\u001b[31m  document.getElementById('storeAddPanel').style.display = 'none';\u001b[39m\n\u001b[31m  showToast(`${Math.min(stores.length, 50)} consignees added ✓`, 'ok');\u001b[39m\n\u001b[31m  if (stores.some(s => s.docket)) setTimeout(() => autoCreateDispatchFromStoreList(), 600);\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction promptAddFromStores() { /* replaced by inline panel */ }··\u001b[39m\n\u001b[31mfunction findCol(headers, keywords) {\u001b[39m\n\u001b[31m  for (let i=0;i<headers.length;i++) {\u001b[39m\n\u001b[31m    const h = (headers[i]||'').toString().toLowerCase();\u001b[39m\n\u001b[31m    if (keywords.some(k => h.includes(k))) return i;\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m  return -1;\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction renderStoreTable(hasDocket) {\u001b[39m\n\u001b[31m  const t = document.getElementById('storeTable');\u001b[39m\n\u001b[31m  const cols = ['#','Store Name','City','Phone'];\u001b[39m\n\u001b[31m  if (hasDocket) cols.push('Docket','Status');\u001b[39m\n\u001b[31m  t.innerHTML = `<thead><tr>${cols.map(c=>`<th>${c}</th>`).join('')}</tr></thead>\u001b[39m\n\u001b[31m  <tbody>${stores.map((s,i) => {\u001b[39m\n\u001b[31m    const statusClass = s.docket ? (s.status === 'delivered' ? 'matched' : 'unmatched') : '';\u001b[39m\n\u001b[31m    const docketCell = hasDocket ? `<td class=\\\"${statusClass}\\\" style=\\\"font-family:var(--fnt-mono);\\\">${s.docket||'—'}</td>` : '';\u001b[39m\n\u001b[31m    const statusCell = hasDocket ? `<td>${s.status ? `<span class=\\\"status-pill sp-${s.status}\\\">${s.status}</span>` : '—'}</td>` : '';\u001b[39m\n\u001b[31m    return `<tr>\u001b[39m\n\u001b[31m      <td>${i+1}</td>\u001b[39m\n\u001b[31m      <td>${s.name}</td>\u001b[39m\n\u001b[31m      <td>${s.city}</td>\u001b[39m\n\u001b[31m      <td style=\\\"font-family:var(--fnt-mono);\\\">${s.phone||'—'}</td>\u001b[39m\n\u001b[31m      ${docketCell}${statusCell}\u001b[39m\n\u001b[31m    </tr>`;\u001b[39m\n\u001b[31m  }).join('')}</tbody>`;\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction clearStores() {\u001b[39m\n\u001b[31m  stores = [];\u001b[39m\n\u001b[31m  document.getElementById('storeTableArea').style.display = 'none';\u001b[39m\n\u001b[31m  document.getElementById('storeFile').value = '';\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction autoCreateDispatchFromStoreList() {\u001b[39m\n\u001b[31m  // Create/update dispatch with all stores that have dockets\u001b[39m\n\u001b[31m  const courier = document.getElementById('n_courier').value || 'SkyKing';\u001b[39m\n\u001b[31m  const storesWithDockets = stores.filter(s => s.docket);\u001b[39m\n\u001b[31m  if (!storesWithDockets.length) return;·\u001b[39m\n\u001b[31m  // Clear and rebuild consignee list from store data\u001b[39m\n\u001b[31m  const consList = document.getElementById('consList');\u001b[39m\n\u001b[31m  consList.innerHTML = '';\u001b[39m\n\u001b[31m  storesWithDockets.forEach(s => {\u001b[39m\n\u001b[31m    addConsignee(s);\u001b[39m\n\u001b[31m  });\u001b[39m\n\u001b[31m  showPane('new');\u001b[39m\n\u001b[31m  showToast(`${storesWithDockets.length} consignees added from store list`, 'ok');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction promptAddFromStores() {\u001b[39m\n\u001b[31m  // Replaced by inline storeAddPanel UI — no-op kept for safety\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction autoMatchDockets() {\u001b[39m\n\u001b[31m  // Match OCR dockets to stores by order\u001b[39m\n\u001b[31m  if (!slipOCRData.length) { showToast('Upload docket slip images first', 'err'); return; }\u001b[39m\n\u001b[31m  let matched = 0;\u001b[39m\n\u001b[31m  slipOCRData.forEach((slip, i) => {\u001b[39m\n\u001b[31m    if (i < stores.length && slip.docket) {\u001b[39m\n\u001b[31m      stores[i].docket = slip.docket;\u001b[39m\n\u001b[31m      matched++;\u001b[39m\n\u001b[31m    }\u001b[39m\n\u001b[31m  });\u001b[39m\n\u001b[31m  renderStoreTable(true);\u001b[39m\n\u001b[31m  showToast(`${matched} dockets matched to stores ✓`, 'ok');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31m// CONSIGNEES\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31mfunction addConsignee(storeData = null) {\u001b[39m\n\u001b[31m  const list = document.getElementById('consList');\u001b[39m\n\u001b[31m  const idx = list.children.length;\u001b[39m\n\u001b[31m  const card = document.createElement('div');\u001b[39m\n\u001b[31m  card.className = 'cons-card';\u001b[39m\n\u001b[31m  card.dataset.idx = idx;\u001b[39m\n\u001b[31m  const s = storeData || {};\u001b[39m\n\u001b[31m  card.innerHTML = `\u001b[39m\n\u001b[31m    <div class=\\\"cons-hdr\\\">\u001b[39m\n\u001b[31m      <span class=\\\"cons-num\\\">CONSIGNEE #${idx+1}</span>\u001b[39m\n\u001b[31m      <button class=\\\"cons-del\\\" onclick=\\\"this.closest('.cons-card').remove();renumberCons()\\\">✕</button>\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m    <div class=\\\"row2\\\">\u001b[39m\n\u001b[31m      <div class=\\\"f\\\">\u001b[39m\n\u001b[31m        <label>Store / Name <span class=\\\"req\\\">*</span></label>\u001b[39m\n\u001b[31m        <input type=\\\"text\\\" class=\\\"cons-name\\\" placeholder=\\\"Store name\\\" value=\\\"${s.name||''}\\\">\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      <div class=\\\"f\\\">\u001b[39m\n\u001b[31m        <label>City <span class=\\\"req\\\">*</span></label>\u001b[39m\n\u001b[31m        <input type=\\\"text\\\" class=\\\"cons-city\\\" placeholder=\\\"City\\\" value=\\\"${s.city||''}\\\">\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m    <div class=\\\"f\\\">\u001b[39m\n\u001b[31m      <label>Address</label>\u001b[39m\n\u001b[31m      <input type=\\\"text\\\" class=\\\"cons-addr\\\" placeholder=\\\"Full address\\\" value=\\\"${s.address||''}\\\">\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m    <div class=\\\"row2\\\">\u001b[39m\n\u001b[31m      <div class=\\\"f\\\">\u001b[39m\n\u001b[31m        <label>Phone</label>\u001b[39m\n\u001b[31m        <input type=\\\"tel\\\" class=\\\"cons-phone\\\" placeholder=\\\"Mobile\\\" value=\\\"${s.phone||''}\\\">\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      <div class=\\\"f\\\">\u001b[39m\n\u001b[31m        <label>Pincode</label>\u001b[39m\n\u001b[31m        <input type=\\\"text\\\" class=\\\"cons-pin\\\" placeholder=\\\"6-digit\\\" maxlength=\\\"6\\\">\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m    <div class=\\\"f\\\">\u001b[39m\n\u001b[31m      <label>Material / Contents <span class=\\\"req\\\">*</span></label>\u001b[39m\n\u001b[31m      <input type=\\\"text\\\" class=\\\"cons-material\\\" placeholder=\\\"e.g. Standees, Posters\\\">\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m    <div class=\\\"row2\\\">\u001b[39m\n\u001b[31m      <div class=\\\"f\\\">\u001b[39m\n\u001b[31m        <label>Qty</label>\u001b[39m\n\u001b[31m        <input type=\\\"number\\\" class=\\\"cons-qty\\\" placeholder=\\\"0\\\" min=\\\"0\\\">\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      <div class=\\\"f\\\">\u001b[39m\n\u001b[31m        <label>Docket No.</label>\u001b[39m\n\u001b[31m        <input type=\\\"text\\\" class=\\\"cons-docket-input\\\" placeholder=\\\"Docket #\\\"\u001b[39m\n\u001b[31m          style=\\\"font-family:var(--fnt-mono);\\\" value=\\\"${s.docket||''}\\\"\u001b[39m\n\u001b[31m          oninput=\\\"onDocketInput(this)\\\">\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m    ${s.docket ? `<div class=\\\"cons-docket-linked\\\"><span>🔗</span><span class=\\\"dkt\\\">${s.docket}</span></div>` : ''}\u001b[39m\n\u001b[31m  `;\u001b[39m\n\u001b[31m  list.appendChild(card);\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction onDocketInput(input) {\u001b[39m\n\u001b[31m  const card = input.closest('.cons-card');\u001b[39m\n\u001b[31m  let linked = card.querySelector('.cons-docket-linked');\u001b[39m\n\u001b[31m  if (input.value.trim()) {\u001b[39m\n\u001b[31m    if (!linked) {\u001b[39m\n\u001b[31m      linked = document.createElement('div');\u001b[39m\n\u001b[31m      linked.className = 'cons-docket-linked';\u001b[39m\n\u001b[31m      card.appendChild(linked);\u001b[39m\n\u001b[31m    }\u001b[39m\n\u001b[31m    linked.innerHTML = `<span>🔗</span><span class=\\\"dkt\\\">${input.value}</span>`;\u001b[39m\n\u001b[31m  } else if (linked) {\u001b[39m\n\u001b[31m    linked.remove();\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction showLinkedDocket(card, docket) {\u001b[39m\n\u001b[31m  const input = card.querySelector('.cons-docket-input');\u001b[39m\n\u001b[31m  if (input) { input.value = docket; onDocketInput(input); }\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction renumberCons() {\u001b[39m\n\u001b[31m  document.querySelectorAll('#consList .cons-card').forEach((c,i) => {\u001b[39m\n\u001b[31m    c.querySelector('.cons-num').textContent = `CONSIGNEE #${i+1}`;\u001b[39m\n\u001b[31m    c.dataset.idx = i;\u001b[39m\n\u001b[31m  });\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31m// JOB REFERENCE\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31mfunction onClientChange() {\u001b[39m\n\u001b[31m  const val = document.getElementById('n_client').value;\u001b[39m\n\u001b[31m  document.getElementById('customClientField').style.display = val === '__c' ? 'block' : 'none';\u001b[39m\n\u001b[31m  updatePill();\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31mfunction updatePill() {\u001b[39m\n\u001b[31m  const client = document.getElementById('n_client').value;\u001b[39m\n\u001b[31m  const job = document.getElementById('n_jobref').value.trim();\u001b[39m\n\u001b[31m  const cName = client === '__c' ? document.getElementById('n_customClient').value : client;\u001b[39m\n\u001b[31m  const pill = document.getElementById('jobPill');\u001b[39m\n\u001b[31m  if (cName && job) {\u001b[39m\n\u001b[31m    document.getElementById('jobPillTxt').textContent = `${cName} › ${job}`;\u001b[39m\n\u001b[31m    pill.style.display = 'inline-flex';\u001b[39m\n\u001b[31m  } else {\u001b[39m\n\u001b[31m    pill.style.display = 'none';\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31mfunction clearJob() {\u001b[39m\n\u001b[31m  document.getElementById('n_client').value = '';\u001b[39m\n\u001b[31m  document.getElementById('n_jobref').value = '';\u001b[39m\n\u001b[31m  document.getElementById('jobPill').style.display = 'none';\u001b[39m\n\u001b[31m  document.getElementById('customClientField').style.display = 'none';\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31m// GATHER / SAVE\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31mfunction gatherDispatch() {\u001b[39m\n\u001b[31m  const client = document.getElementById('n_client').value;\u001b[39m\n\u001b[31m  const clientName = client === '__c' ? document.getElementById('n_customClient').value : client;\u001b[39m\n\u001b[31m  const consignees = [];\u001b[39m\n\u001b[31m  document.querySelectorAll('#consList .cons-card').forEach(card => {\u001b[39m\n\u001b[31m    const docket = card.querySelector('.cons-docket-input')?.value.trim() || '';\u001b[39m\n\u001b[31m    consignees.push({\u001b[39m\n\u001b[31m      name: card.querySelector('.cons-name')?.value || '',\u001b[39m\n\u001b[31m      city: card.querySelector('.cons-city')?.value || '',\u001b[39m\n\u001b[31m      address: card.querySelector('.cons-addr')?.value || '',\u001b[39m\n\u001b[31m      phone: card.querySelector('.cons-phone')?.value || '',\u001b[39m\n\u001b[31m      pincode: card.querySelector('.cons-pin')?.value || '',\u001b[39m\n\u001b[31m      material: card.querySelector('.cons-material')?.value || '',\u001b[39m\n\u001b[31m      qty: card.querySelector('.cons-qty')?.value || '',\u001b[39m\n\u001b[31m      docket,\u001b[39m\n\u001b[31m      status: docket ? 'booked' : 'pending',\u001b[39m\n\u001b[31m      lastEvent: '',\u001b[39m\n\u001b[31m      lastUpdated: ''\u001b[39m\n\u001b[31m    });\u001b[39m\n\u001b[31m  });\u001b[39m\n\u001b[31m  return {\u001b[39m\n\u001b[31m    id: 'DSP-' + Date.now(),\u001b[39m\n\u001b[31m    mode: 'manual',\u001b[39m\n\u001b[31m    client: clientName,\u001b[39m\n\u001b[31m    jobref: document.getElementById('n_jobref').value,\u001b[39m\n\u001b[31m    courier: document.getElementById('n_courier').value,\u001b[39m\n\u001b[31m    dispatchDate: document.getElementById('n_date').value,\u001b[39m\n\u001b[31m    remarks: document.getElementById('n_remarks').value,\u001b[39m\n\u001b[31m    consignees,\u001b[39m\n\u001b[31m    slipCount: slipOCRData.length,\u001b[39m\n\u001b[31m    status: 'active',\u001b[39m\n\u001b[31m    createdAt: new Date().toISOString()\u001b[39m\n\u001b[31m  };\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction validate(d) {\u001b[39m\n\u001b[31m  if (!d.client) { showToast('Select a client', 'err'); return false; }\u001b[39m\n\u001b[31m  if (!d.courier) { showToast('Select courier', 'err'); return false; }\u001b[39m\n\u001b[31m  if (!d.consignees.length) { showToast('Add at least one consignee', 'err'); return false; }\u001b[39m\n\u001b[31m  for (let i = 0; i < d.consignees.length; i++) {\u001b[39m\n\u001b[31m    const c = d.consignees[i];\u001b[39m\n\u001b[31m    if (!c.name || !c.city) {\u001b[39m\n\u001b[31m      showToast(`Consignee #${i+1} (${c.name||'unnamed'}) — name & city required`, 'err');\u001b[39m\n\u001b[31m      // Scroll to and highlight the problem consignee card\u001b[39m\n\u001b[31m      const cards = document.querySelectorAll('#consList .cons-card');\u001b[39m\n\u001b[31m      if (cards[i]) {\u001b[39m\n\u001b[31m        cards[i].scrollIntoView({ behavior: 'smooth', block: 'center' });\u001b[39m\n\u001b[31m        cards[i].style.borderColor = 'var(--red)';\u001b[39m\n\u001b[31m        cards[i].style.boxShadow = '0 0 0 2px rgba(239,68,68,.3)';\u001b[39m\n\u001b[31m        setTimeout(() => {\u001b[39m\n\u001b[31m          if (cards[i]) { cards[i].style.borderColor = ''; cards[i].style.boxShadow = ''; }\u001b[39m\n\u001b[31m        }, 3000);\u001b[39m\n\u001b[31m      }\u001b[39m\n\u001b[31m      return false;\u001b[39m\n\u001b[31m    }\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m  return true;\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction saveDraft() {\u001b[39m\n\u001b[31m  const d = gatherDispatch();\u001b[39m\n\u001b[31m  d.status = 'draft';\u001b[39m\n\u001b[31m  dispatches.unshift(d);\u001b[39m\n\u001b[31m  saveData();\u001b[39m\n\u001b[31m  updateHistCount();\u001b[39m\n\u001b[31m  showToast('Draft saved ✓', 'ok');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// ── VERIFICATION SCREEN ─────────────────────────────────\u001b[39m\n\u001b[31m// Known docket lengths per courier for validation\u001b[39m\n\u001b[31mconst DOCKET_LENGTHS = {\u001b[39m\n\u001b[31m  'SkyKing': { min: 9, max: 12 },\u001b[39m\n\u001b[31m  'DTDC':    { min: 8, max: 15 },\u001b[39m\n\u001b[31m  'Delhivery': { min: 10, max: 18 },\u001b[39m\n\u001b[31m  'Blue Dart': { min: 10, max: 13 },\u001b[39m\n\u001b[31m  'FedEx':   { min: 10, max: 20 },\u001b[39m\n\u001b[31m  'Ecom Express': { min: 10, max: 14 },\u001b[39m\n\u001b[31m  'India Post': { min: 13, max: 22 },\u001b[39m\n\u001b[31m};·\u001b[39m\n\u001b[31mfunction finaliseDispatch() {\u001b[39m\n\u001b[31m  const d = gatherDispatch();\u001b[39m\n\u001b[31m  if (!validate(d)) return;\u001b[39m\n\u001b[31m  showVerifyScreen(d);\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction showVerifyScreen(d) {\u001b[39m\n\u001b[31m  const pane = document.getElementById('verifyPane');\u001b[39m\n\u001b[31m  const list = document.getElementById('verifyList');\u001b[39m\n\u001b[31m  const summary = document.getElementById('verifySummary');·\u001b[39m\n\u001b[31m  // Build flags\u001b[39m\n\u001b[31m  const docketsSeen = {};\u001b[39m\n\u001b[31m  const courier = d.courier;\u001b[39m\n\u001b[31m  const lenRule = DOCKET_LENGTHS[courier];·\u001b[39m\n\u001b[31m  d.consignees.forEach((c, i) => {\u001b[39m\n\u001b[31m    if (c.docket) docketsSeen[c.docket] = (docketsSeen[c.docket] || 0) + 1;\u001b[39m\n\u001b[31m  });·\u001b[39m\n\u001b[31m  const total     = d.consignees.length;\u001b[39m\n\u001b[31m  const withDock  = d.consignees.filter(c => c.docket).length;\u001b[39m\n\u001b[31m  const dupes     = Object.values(docketsSeen).filter(v => v > 1).length;\u001b[39m\n\u001b[31m  const noDock    = total - withDock;\u001b[39m\n\u001b[31m  const badLen    = lenRule ? d.consignees.filter(c => c.docket &&\u001b[39m\n\u001b[31m    (c.docket.length < lenRule.min || c.docket.length > lenRule.max)).length : 0;·\u001b[39m\n\u001b[31m  // Summary bar\u001b[39m\n\u001b[31m  summary.innerHTML = `\u001b[39m\n\u001b[31m    <div class=\\\"vs-item\\\"><div class=\\\"vs-val\\\" style=\\\"color:var(--t1);\\\">${total}</div><div class=\\\"vs-lbl\\\">Total</div></div>\u001b[39m\n\u001b[31m    <div class=\\\"vs-item\\\"><div class=\\\"vs-val\\\" style=\\\"color:var(--grn);\\\">${withDock}</div><div class=\\\"vs-lbl\\\">Docketed</div></div>\u001b[39m\n\u001b[31m    <div class=\\\"vs-item\\\"><div class=\\\"vs-val\\\" style=\\\"color:${noDock > 0 ? 'var(--ylw)' : 'var(--t3)'};\\\">${noDock}</div><div class=\\\"vs-lbl\\\">No Docket</div></div>\u001b[39m\n\u001b[31m    <div class=\\\"vs-item\\\"><div class=\\\"vs-val\\\" style=\\\"color:${dupes > 0 ? 'var(--red)' : 'var(--t3)'};\\\">${dupes}</div><div class=\\\"vs-lbl\\\">Duplicates</div></div>\u001b[39m\n\u001b[31m    ${badLen > 0 ? `<div class=\\\"vs-item\\\"><div class=\\\"vs-val\\\" style=\\\"color:var(--sky);\\\">${badLen}</div><div class=\\\"vs-lbl\\\">Short/Long</div></div>` : ''}\u001b[39m\n\u001b[31m    <div class=\\\"vs-item\\\" style=\\\"margin-left:auto;text-align:right;\\\">\u001b[39m\n\u001b[31m      <div class=\\\"vs-val\\\" style=\\\"font-size:.8rem;color:var(--t2);\\\">${d.courier||'—'}</div>\u001b[39m\n\u001b[31m      <div class=\\\"vs-lbl\\\">${d.client||'—'} ${d.jobref ? '› '+d.jobref : ''}</div>\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m  `;·\u001b[39m\n\u001b[31m  // Build consignee rows\u001b[39m\n\u001b[31m  list.innerHTML = d.consignees.map((c, i) => {\u001b[39m\n\u001b[31m    const isDupe   = c.docket && docketsSeen[c.docket] > 1;\u001b[39m\n\u001b[31m    const isNoDock = !c.docket;\u001b[39m\n\u001b[31m    const isShort  = lenRule && c.docket &&\u001b[39m\n\u001b[31m      (c.docket.length < lenRule.min || c.docket.length > lenRule.max);·\u001b[39m\n\u001b[31m    const rowClass = isDupe ? 'has-dupe' : (isNoDock ? 'no-docket' : 'ok');·\u001b[39m\n\u001b[31m    // Find matching OCR slip for thumbnail\u001b[39m\n\u001b[31m    const ocrMatch = slipOCRData[i];\u001b[39m\n\u001b[31m    const thumbHtml = ocrMatch?.imageData\u001b[39m\n\u001b[31m      ? `<img class=\\\"verify-thumb\\\" src=\\\"${ocrMatch.imageData}\\\" onclick=\\\"showSlipFull(${i})\\\" title=\\\"View slip\\\">`\u001b[39m\n\u001b[31m      : '';·\u001b[39m\n\u001b[31m    // Flags\u001b[39m\n\u001b[31m    let flags = '';\u001b[39m\n\u001b[31m    if (isDupe)   flags += `<span class=\\\"verify-flag vf-dupe\\\">⚠ Duplicate</span> `;\u001b[39m\n\u001b[31m    if (isNoDock) flags += `<span class=\\\"verify-flag vf-nodocket\\\">No Docket</span> `;\u001b[39m\n\u001b[31m    if (isShort)  flags += `<span class=\\\"verify-flag vf-short\\\">${c.docket.length} digits</span> `;\u001b[39m\n\u001b[31m    if (!isDupe && !isNoDock && !isShort) flags = `<span class=\\\"verify-flag vf-ok\\\">✓ OK</span>`;·\u001b[39m\n\u001b[31m    // OCR confidence note\u001b[39m\n\u001b[31m    const confNote = ocrMatch && ocrMatch.confidence\u001b[39m\n\u001b[31m      ? `<div class=\\\"ocr-correction-note\\\">OCR confidence: ${Math.round(ocrMatch.confidence*100)}% via ${ocrMatch.source||'—'}</div>` : '';·\u001b[39m\n\u001b[31m    // WhatsApp URL\u001b[39m\n\u001b[31m    const waMsg = encodeURIComponent(\u001b[39m\n\u001b[31m      `Dear ${c.name}, your material has been dispatched via ${d.courier}. Docket: ${c.docket||'TBD'}. Track: ${(COURIER_MASTER[d.courier]?.trackUrl||'').replace('{docket}', c.docket||'')}`\u001b[39m\n\u001b[31m    );\u001b[39m\n\u001b[31m    const waUrl = c.phone ? `https://wa.me/91${c.phone.replace(/\\\\D/g,'')}?text=${waMsg}` : '';·\u001b[39m\n\u001b[31m    return `\u001b[39m\n\u001b[31m    <div class=\\\"verify-row ${rowClass}\\\" id=\\\"vrow_${i}\\\">\u001b[39m\n\u001b[31m      <div class=\\\"verify-row-hdr\\\">\u001b[39m\n\u001b[31m        <span class=\\\"verify-num\\\">#${i+1}</span>\u001b[39m\n\u001b[31m        <span class=\\\"verify-store\\\">${c.name||'—'}</span>\u001b[39m\n\u001b[31m        ${flags}\u001b[39m\n\u001b[31m        ${thumbHtml}\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      <div class=\\\"verify-fields\\\" style=\\\"grid-template-columns:1fr 1fr 1fr;\\\">\u001b[39m\n\u001b[31m        <div class=\\\"verify-field\\\">\u001b[39m\n\u001b[31m          <label>Docket No. <span style=\\\"color:var(--acc);\\\">*</span></label>\u001b[39m\n\u001b[31m          <input type=\\\"text\\\" value=\\\"${c.docket||''}\\\" oninput=\\\"updateVerifyField(${i},'docket',this)\\\"\u001b[39m\n\u001b[31m            style=\\\"font-family:var(--fnt-mono);${isDupe||isShort?'border-color:var(--red);':!c.docket?'border-color:var(--ylw);':'color:var(--acc);'}\\\">\u001b[39m\n\u001b[31m          ${confNote}\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m        <div class=\\\"verify-field\\\">\u001b[39m\n\u001b[31m          <label>City</label>\u001b[39m\n\u001b[31m          <input type=\\\"text\\\" value=\\\"${c.city||''}\\\" oninput=\\\"updateVerifyField(${i},'city',this)\\\"\u001b[39m\n\u001b[31m            style=\\\"${!c.city ? 'border-color:var(--ylw);' : ''}\\\">\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m        <div class=\\\"verify-field\\\">\u001b[39m\n\u001b[31m          <label>Pincode</label>\u001b[39m\n\u001b[31m          <input type=\\\"text\\\" value=\\\"${c.pincode||''}\\\" oninput=\\\"updateVerifyField(${i},'pincode',this)\\\"\u001b[39m\n\u001b[31m            style=\\\"font-family:var(--fnt-mono);\\\">\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m        <div class=\\\"verify-field\\\">\u001b[39m\n\u001b[31m          <label>Phone</label>\u001b[39m\n\u001b[31m          <input type=\\\"tel\\\" value=\\\"${c.phone||''}\\\" oninput=\\\"updateVerifyField(${i},'phone',this)\\\">\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m        <div class=\\\"verify-field\\\">\u001b[39m\n\u001b[31m          <label>Material / Contents</label>\u001b[39m\n\u001b[31m          <input type=\\\"text\\\" value=\\\"${c.material||''}\\\" oninput=\\\"updateVerifyField(${i},'material',this)\\\">\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m        <div class=\\\"verify-field\\\">\u001b[39m\n\u001b[31m          <label>Street Address</label>\u001b[39m\n\u001b[31m          <input type=\\\"text\\\" value=\\\"${c.address||''}\\\" oninput=\\\"updateVerifyField(${i},'address',this)\\\"\u001b[39m\n\u001b[31m            placeholder=\\\"Street / locality only\\\">\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      <div class=\\\"verify-actions\\\">\u001b[39m\n\u001b[31m        ${c.docket && c.phone ? `<button class=\\\"btn-wa\\\" onclick=\\\"window.open('${waUrl}','_blank')\\\">📱 WhatsApp</button>` : ''}\u001b[39m\n\u001b[31m        <button class=\\\"btn-sm\\\" onclick=\\\"removeVerifyRow(${i})\\\">✕ Remove</button>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m    </div>`;\u001b[39m\n\u001b[31m  }).join('');·\u001b[39m\n\u001b[31m  // Store dispatch object for confirm\u001b[39m\n\u001b[31m  pane._pendingDispatch = d;\u001b[39m\n\u001b[31m  pane.style.display = 'block';\u001b[39m\n\u001b[31m  pane.scrollTop = 0;\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction updateVerifyField(idx, field, input) {\u001b[39m\n\u001b[31m  const pane = document.getElementById('verifyPane');\u001b[39m\n\u001b[31m  if (!pane._pendingDispatch) return;\u001b[39m\n\u001b[31m  const c = pane._pendingDispatch.consignees[idx];\u001b[39m\n\u001b[31m  if (!c) return;\u001b[39m\n\u001b[31m  c[field] = input.value.trim();\u001b[39m\n\u001b[31m  input.classList.toggle('changed', input.value.trim() !== '');\u001b[39m\n\u001b[31m  // Re-check dupe if docket changed\u001b[39m\n\u001b[31m  if (field === 'docket') refreshVerifyFlags(pane._pendingDispatch);\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction refreshVerifyFlags(d) {\u001b[39m\n\u001b[31m  const seen = {};\u001b[39m\n\u001b[31m  d.consignees.forEach(c => { if (c.docket) seen[c.docket] = (seen[c.docket]||0)+1; });\u001b[39m\n\u001b[31m  d.consignees.forEach((c, i) => {\u001b[39m\n\u001b[31m    const row = document.getElementById('vrow_' + i);\u001b[39m\n\u001b[31m    if (!row) return;\u001b[39m\n\u001b[31m    const isDupe = c.docket && seen[c.docket] > 1;\u001b[39m\n\u001b[31m    row.classList.toggle('has-dupe', isDupe);\u001b[39m\n\u001b[31m  });\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction removeVerifyRow(idx) {\u001b[39m\n\u001b[31m  const pane = document.getElementById('verifyPane');\u001b[39m\n\u001b[31m  if (!pane._pendingDispatch) return;\u001b[39m\n\u001b[31m  const el = document.getElementById('vrow_' + idx);\u001b[39m\n\u001b[31m  if (el) {\u001b[39m\n\u001b[31m    el.style.transition = 'opacity .25s';\u001b[39m\n\u001b[31m    el.style.opacity = '0';\u001b[39m\n\u001b[31m    setTimeout(() => {\u001b[39m\n\u001b[31m      pane._pendingDispatch.consignees.splice(idx, 1);\u001b[39m\n\u001b[31m      showVerifyScreen(pane._pendingDispatch); // re-render with updated indices\u001b[39m\n\u001b[31m    }, 250);\u001b[39m\n\u001b[31m  } else {\u001b[39m\n\u001b[31m    pane._pendingDispatch.consignees.splice(idx, 1);\u001b[39m\n\u001b[31m    showVerifyScreen(pane._pendingDispatch);\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction approveAll() {\u001b[39m\n\u001b[31m  showToast('All rows approved ✓', 'ok');\u001b[39m\n\u001b[31m  document.getElementById('btnConfirmFinalise').style.background = 'var(--grn)';\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction closeVerify() {\u001b[39m\n\u001b[31m  const pane = document.getElementById('verifyPane');\u001b[39m\n\u001b[31m  // If this dispatch was already finalised, warn before going back\u001b[39m\n\u001b[31m  if (_lastFinalisedId && pane._pendingDispatch?.id === _lastFinalisedId) {\u001b[39m\n\u001b[31m    if (!confirm(\u001b[39m\n\u001b[31m      'This dispatch has already been finalised.\\\\n\\\\n' +\u001b[39m\n\u001b[31m      'Going back will take you to the edit form — any changes will create a NEW dispatch record.\\\\n\\\\n' +\u001b[39m\n\u001b[31m      'Go back anyway?'\u001b[39m\n\u001b[31m    )) return;\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m  pane.style.display = 'none';\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction showSlipFull(idx) {\u001b[39m\n\u001b[31m  const s = slipOCRData[idx];\u001b[39m\n\u001b[31m  if (!s?.imageData) return;\u001b[39m\n\u001b[31m  const w = window.open('');\u001b[39m\n\u001b[31m  w.document.write(`<img src=\\\"${s.imageData}\\\" style=\\\"max-width:100%;height:auto;\\\">`);\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// Log OCR correction to sheets for future accuracy improvement\u001b[39m\n\u001b[31masync function logOCRCorrection(original, corrected, courier) {\u001b[39m\n\u001b[31m  if (!window.appConfig.webhook_url || original === corrected) return;\u001b[39m\n\u001b[31m  try {\u001b[39m\n\u001b[31m    await fetch(window.appConfig.webhook_url, {\u001b[39m\n\u001b[31m      method: 'POST', mode: 'no-cors',\u001b[39m\n\u001b[31m      headers: { 'Content-Type': 'application/json' },\u001b[39m\n\u001b[31m      body: JSON.stringify({\u001b[39m\n\u001b[31m        type: 'ocr_correction',\u001b[39m\n\u001b[31m        original, corrected, courier,\u001b[39m\n\u001b[31m        timestamp: new Date().toISOString()\u001b[39m\n\u001b[31m      })\u001b[39m\n\u001b[31m    });\u001b[39m\n\u001b[31m  } catch(e) {}\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mlet _lastFinalisedId = null; // track last finalised dispatch to warn on back·\u001b[39m\n\u001b[31masync function confirmAndFinalise() {\u001b[39m\n\u001b[31m  const pane = document.getElementById('verifyPane');\u001b[39m\n\u001b[31m  const d = pane._pendingDispatch;\u001b[39m\n\u001b[31m  if (!d) return;·\u001b[39m\n\u001b[31m  d.status = 'active';\u001b[39m\n\u001b[31m  d.finalisedAt = new Date().toISOString();·\u001b[39m\n\u001b[31m  // Log OCR corrections\u001b[39m\n\u001b[31m  slipOCRData.forEach((s, i) => {\u001b[39m\n\u001b[31m    const c = d.consignees[i];\u001b[39m\n\u001b[31m    if (c && s.docket && c.docket && s.docket !== c.docket) {\u001b[39m\n\u001b[31m      logOCRCorrection(s.docket, c.docket, d.courier);\u001b[39m\n\u001b[31m    }\u001b[39m\n\u001b[31m  });·\u001b[39m\n\u001b[31m  // Replace existing draft with same ID if present, else prepend\u001b[39m\n\u001b[31m  const existingIdx = dispatches.findIndex(x => x.id === d.id);\u001b[39m\n\u001b[31m  if (existingIdx >= 0) {\u001b[39m\n\u001b[31m    dispatches[existingIdx] = d;\u001b[39m\n\u001b[31m  } else {\u001b[39m\n\u001b[31m    dispatches.unshift(d);\u001b[39m\n\u001b[31m  }·\u001b[39m\n\u001b[31m  _lastFinalisedId = d.id;\u001b[39m\n\u001b[31m  saveData();\u001b[39m\n\u001b[31m  syncDispatchToDB(d); // push to Postgres (non-blocking)\u001b[39m\n\u001b[31m  updateHistCount();\u001b[39m\n\u001b[31m  if (window.appConfig.auto_log !== 'false' && window.appConfig.webhook_url) await logToSheets(d);\u001b[39m\n\u001b[31m  showToast('Dispatch finalised ✓', 'ok');\u001b[39m\n\u001b[31m  pane.style.display = 'none';\u001b[39m\n\u001b[31m  pane._pendingDispatch = null; // clear pending\u001b[39m\n\u001b[31m  resetForm();\u001b[39m\n\u001b[31m  showPane('dashboard');\u001b[39m\n\u001b[31m  renderDashboard();\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction resetForm() {\u001b[39m\n\u001b[31m  document.getElementById('n_client').value = '';\u001b[39m\n\u001b[31m  document.getElementById('n_jobref').value = '';\u001b[39m\n\u001b[31m  document.getElementById('n_courier').value = '';\u001b[39m\n\u001b[31m  document.getElementById('n_remarks').value = '';\u001b[39m\n\u001b[31m  document.getElementById('jobPill').style.display = 'none';\u001b[39m\n\u001b[31m  document.getElementById('customClientField').style.display = 'none';\u001b[39m\n\u001b[31m  document.getElementById('consList').innerHTML = '';\u001b[39m\n\u001b[31m  addConsignee();\u001b[39m\n\u001b[31m  clearSlips();\u001b[39m\n\u001b[31m  clearStores();\u001b[39m\n\u001b[31m  setToday();\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction newDispatch() { resetForm(); showPane('new'); }·\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31m// DASHBOARD\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31mfunction renderDashboard() {\u001b[39m\n\u001b[31m  // Exclude drafts from dashboard — only show active/finalised dispatches\u001b[39m\n\u001b[31m  const activeDis = dispatches.filter(d => d.status !== 'draft');\u001b[39m\n\u001b[31m  const allCons = activeDis.flatMap(d => d.consignees || []);\u001b[39m\n\u001b[31m  const countBy = (status) => allCons.filter(c => c.status === status).length;\u001b[39m\n\u001b[31m  document.getElementById('st-delivered').textContent = countBy('delivered');\u001b[39m\n\u001b[31m  document.getElementById('st-transit').textContent = countBy('transit') + countBy('in-transit');\u001b[39m\n\u001b[31m  document.getElementById('st-booked').textContent = countBy('booked');\u001b[39m\n\u001b[31m  document.getElementById('st-pending').textContent = allCons.filter(c => !c.docket || c.status === 'pending').length;·\u001b[39m\n\u001b[31m  const grid = document.getElementById('dashGrid');\u001b[39m\n\u001b[31m  // Flatten all consignees across dispatches\u001b[39m\n\u001b[31m  let items = activeDis.flatMap(d =>\u001b[39m\n\u001b[31m    (d.consignees || []).filter(c => c.docket).map(c => ({...c, dispatchId: d.id, courier: d.courier, client: d.client, jobref: d.jobref}))\u001b[39m\n\u001b[31m  );\u001b[39m\n\u001b[31m  if (dashFilter !== 'all') {\u001b[39m\n\u001b[31m    items = items.filter(c => {\u001b[39m\n\u001b[31m      if (dashFilter === 'delivered') return c.status === 'delivered';\u001b[39m\n\u001b[31m      if (dashFilter === 'transit') return c.status === 'transit' || c.status === 'in-transit';\u001b[39m\n\u001b[31m      if (dashFilter === 'booked') return c.status === 'booked';\u001b[39m\n\u001b[31m      if (dashFilter === 'pending') return !c.docket || c.status === 'pending';\u001b[39m\n\u001b[31m      return true;\u001b[39m\n\u001b[31m    });\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m  if (!items.length) {\u001b[39m\n\u001b[31m    grid.innerHTML = '<div class=\\\"empty\\\"><div class=\\\"e-icon\\\">📦</div><p>No docket-tracked consignees.<br>Add docket numbers to see tiles.</p></div>';\u001b[39m\n\u001b[31m    return;\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m  const progMap = { delivered:100, transit:60,'in-transit':60, booked:20, pending:5 };\u001b[39m\n\u001b[31m  const progClr = { delivered:'var(--grn)', transit:'var(--ylw)','in-transit':'var(--ylw)', booked:'var(--sky)', pending:'var(--t4)' };\u001b[39m\n\u001b[31m  grid.innerHTML = items.map(c => {\u001b[39m\n\u001b[31m    const sp = c.status || 'booked';\u001b[39m\n\u001b[31m    const prog = progMap[sp] || 20;\u001b[39m\n\u001b[31m    const clr = progClr[sp] || 'var(--t4)';\u001b[39m\n\u001b[31m    return `\u001b[39m\n\u001b[31m    <div class=\\\"docket-tile\\\" onclick=\\\"showDocketDetail('${c.docket}','${c.courier||''}','${c.dispatchId}')\\\">\u001b[39m\n\u001b[31m      <div class=\\\"docket-tile-hdr\\\">\u001b[39m\n\u001b[31m        <div class=\\\"docket-tile-left\\\">\u001b[39m\n\u001b[31m          <div class=\\\"docket-num\\\">${c.docket}</div>\u001b[39m\n\u001b[31m          <div class=\\\"docket-store\\\">${c.name} — ${c.city}</div>\u001b[39m\n\u001b[31m          <div class=\\\"docket-courier\\\">${c.courier||''} · ${c.client||''} ${c.jobref ? '› '+c.jobref : ''}</div>\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m        <span class=\\\"status-pill sp-${sp}\\\">${sp}</span>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      <div style=\\\"display:flex;gap:6px;margin-bottom:6px;align-items:center;\\\">\u001b[39m\n\u001b[31m        <input type=\\\"checkbox\\\" id=\\\"sel_${c.docket.replace(/[^a-z0-9]/gi,'_')}\\\"\u001b[39m\n\u001b[31m          onclick=\\\"event.stopPropagation();toggleTileSelect('${c.docket}')\\\"\u001b[39m\n\u001b[31m          style=\\\"width:14px;height:14px;cursor:pointer;accent-color:var(--acc);\\\">\u001b[39m\n\u001b[31m        <label style=\\\"font-size:.6rem;color:var(--t3);font-family:var(--fnt-mono);cursor:pointer;\\\"\u001b[39m\n\u001b[31m          onclick=\\\"event.stopPropagation();toggleTileSelect('${c.docket}')\\\">Select</label>\u001b[39m\n\u001b[31m        ${c.manualStatus ?\u001b[39m\n\u001b[31m          `<span style=\\\"font-size:.6rem;font-family:var(--fnt-mono);color:var(--grn);\\\">📞 ${c.manualStatus}</span>` : ''}\u001b[39m\n\u001b[31m        ${c.podUploaded ? '<span style=\\\"font-size:.6rem;color:var(--grn);font-family:var(--fnt-mono);\\\">✓ POD</span>' : ''}\u001b[39m\n\u001b[31m        <button onclick=\\\"event.stopPropagation();showManualTracking('${c.docket}','${c.dispatchId}','${c.name}','${c.phone||''}','${c.city||''}')\\\"\u001b[39m\n\u001b[31m          style=\\\"font-size:.6rem;background:rgba(56,189,248,.1);border:1px solid rgba(56,189,248,.3);\u001b[39m\n\u001b[31m          color:var(--sky);padding:2px 7px;border-radius:10px;cursor:pointer;font-family:var(--fnt-mono);\\\">📞 Call Log</button>\u001b[39m\n\u001b[31m        <button onclick=\\\"event.stopPropagation();showPODUpload('${c.docket}','${c.dispatchId}')\\\"\u001b[39m\n\u001b[31m          style=\\\"font-size:.6rem;background:none;border:1px solid var(--bdr);color:var(--t3);\u001b[39m\n\u001b[31m          padding:2px 7px;border-radius:10px;cursor:pointer;font-family:var(--fnt-mono);\\\">📸 POD</button>\u001b[39m\n\u001b[31m        ${c.manualDelivered ? `<button onclick=\\\"event.stopPropagation();resumeAutoTracking('${c.docket}','${c.dispatchId}')\\\"\u001b[39m\n\u001b[31m          style=\\\"font-size:.6rem;background:rgba(251,146,60,.1);border:1px solid rgba(251,146,60,.4);\u001b[39m\n\u001b[31m          color:#fb923c;padding:2px 7px;border-radius:10px;cursor:pointer;font-family:var(--fnt-mono);\\\">↩ Undo</button>` : ''}\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      <div class=\\\"docket-tile-footer\\\">\u001b[39m\n\u001b[31m        <div style=\\\"flex:1;min-width:0;\\\">\u001b[39m\n\u001b[31m          <span class=\\\"docket-last-event docket-evt-${c.docket}\\\" id=\\\"evt_${c.docket.replace(/[^a-z0-9]/gi,'_')}\\\">${c.lastEvent || 'No tracking yet'}</span>\u001b[39m\n\u001b[31m          <span class=\\\"track-spinner\\\" id=\\\"spin_${c.docket.replace(/[^a-z0-9]/gi,'_')}\\\" style=\\\"display:none;color:var(--ylw);font-family:var(--fnt-mono);font-size:.68rem;animation:blink 1s infinite;\\\">⏳ tracking...</span>\u001b[39m\n\u001b[31m          ${c.lastScanLocation && c.lastScanLocation !== c.lastEvent ?·\u001b[39m\n\u001b[31m            `<div style=\\\"font-size:.62rem;color:var(--t3);font-family:var(--fnt-mono);margin-top:2px;\\\">📍 ${c.lastScanLocation}</div>` : ''}\u001b[39m\n\u001b[31m          ${c.lastTrackedAt ?·\u001b[39m\n\u001b[31m            `<div style=\\\"font-size:.6rem;color:var(--t4);font-family:var(--fnt-mono);margin-top:1px;\\\">🕐 tracked ${new Date(c.lastTrackedAt).toLocaleTimeString('en-IN',{hour:'2-digit',minute:'2-digit'})}</div>` : ''}\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m        <button class=\\\"btn-track-live live-btn-${c.docket.replace(/[^a-z0-9]/gi,'_')}\\\" onclick=\\\"event.stopPropagation();liveTrackTile('${c.docket}','${c.courier||''}','${c.dispatchId}')\\\">\u001b[39m\n\u001b[31m          <span class=\\\"live-dot\\\"></span>LIVE\u001b[39m\n\u001b[31m        </button>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      <div class=\\\"docket-progress\\\">\u001b[39m\n\u001b[31m        <div class=\\\"docket-progress-fill\\\" style=\\\"width:${prog}%;background:${clr};\\\"></div>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m    </div>`;\u001b[39m\n\u001b[31m  }).join('');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction filterDash(f, el) {\u001b[39m\n\u001b[31m  dashFilter = f;\u001b[39m\n\u001b[31m  document.querySelectorAll('#dashFilters .btn-sm').forEach(b => b.classList.remove('primary'));\u001b[39m\n\u001b[31m  el.classList.add('primary');\u001b[39m\n\u001b[31m  renderDashboard();\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31masync function liveTrackTile(docket, courier, dispatchId) {\u001b[39m\n\u001b[31m  showToast(`Tracking ${docket}...`, 'info');\u001b[39m\n\u001b[31m  const result = await fetchTracking(docket, courier);\u001b[39m\n\u001b[31m  // Update status in dispatches\u001b[39m\n\u001b[31m  for (const d of dispatches) {\u001b[39m\n\u001b[31m    if (d.id !== dispatchId) continue;\u001b[39m\n\u001b[31m    for (const c of d.consignees) {\u001b[39m\n\u001b[31m      if (c.docket === docket) {\u001b[39m\n\u001b[31m        c.status = result.status || c.status;\u001b[39m\n\u001b[31m        c.lastEvent = result.lastEvent || c.lastEvent;\u001b[39m\n\u001b[31m        c.lastUpdated = new Date().toISOString();\u001b[39m\n\u001b[31m      }\u001b[39m\n\u001b[31m    }\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m  saveData();\u001b[39m\n\u001b[31m  renderDashboard();\u001b[39m\n\u001b[31m  if (result.status === 'failed') {\u001b[39m\n\u001b[31m    // Open courier site as fallback\u001b[39m\n\u001b[31m    openCourierTrackUrl(docket, courier);\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction showDocketDetail(docket, courier, dispatchId) {\u001b[39m\n\u001b[31m  const d = dispatches.find(x => x.id === dispatchId);\u001b[39m\n\u001b[31m  const c = d?.consignees?.find(x => x.docket === docket);\u001b[39m\n\u001b[31m  if (!c) return;\u001b[39m\n\u001b[31m  const content = document.getElementById('detailContent');\u001b[39m\n\u001b[31m  content.innerHTML = `\u001b[39m\n\u001b[31m    <div class=\\\"sheet-title\\\">${docket}</div>\u001b[39m\n\u001b[31m    <div class=\\\"info-tag\\\">📍 <strong>${c.name}</strong> — ${c.city}</div>\u001b[39m\n\u001b[31m    ${c.phone ? `<div class=\\\"info-tag\\\">📞 ${c.phone}</div>` : ''}\u001b[39m\n\u001b[31m    <div class=\\\"info-tag\\\">📦 ${c.material||'—'} ${c.qty ? '('+c.qty+' pcs)':''}</div>\u001b[39m\n\u001b[31m    <div class=\\\"info-tag\\\">🚚 ${courier||'—'} · <span class=\\\"status-pill sp-${c.status||'pending'}\\\">${c.status||'pending'}</span></div>\u001b[39m\n\u001b[31m    ${c.lastEvent ? `<div class=\\\"info-tag\\\">🕐 ${c.lastEvent}</div>` : ''}\u001b[39m\n\u001b[31m    <div class=\\\"sec-div\\\"><span>Tracking History</span></div>\u001b[39m\n\u001b[31m    <div id=\\\"docketTimeline\\\">\u001b[39m\n\u001b[31m      ${c.timeline?.length ?\u001b[39m\n\u001b[31m        c.timeline.map(e => `<div class=\\\"track-evt\\\"><span class=\\\"track-time\\\">${e.time||''}</span><div><div class=\\\"track-desc\\\">${e.desc||''}</div><div class=\\\"track-loc\\\">${e.loc||''}</div></div></div>`).join('') :\u001b[39m\n\u001b[31m        '<div style=\\\"color:var(--t3);font-size:.78rem;padding:10px 0;\\\">No timeline data. Click Live Track to fetch.</div>'}\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m    <div style=\\\"display:flex;gap:6px;flex-wrap:wrap;margin-top:14px;\\\">\u001b[39m\n\u001b[31m      <button class=\\\"btn btn-ghost\\\" style=\\\"flex:1;\\\" onclick=\\\"openCourierTrackUrl('${docket}','${courier}')\\\">🔗 Courier Site</button>\u001b[39m\n\u001b[31m      <button class=\\\"btn btn-orange\\\" style=\\\"flex:1;\\\" onclick=\\\"liveTrackTile('${docket}','${courier}','${dispatchId}');closeOverlay('detailOverlay')\\\">⚡ Live Track</button>\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m    <div style=\\\"display:flex;gap:6px;flex-wrap:wrap;margin-top:6px;\\\">\u001b[39m\n\u001b[31m      <button class=\\\"btn-wa\\\" style=\\\"flex:1;padding:8px;\\\" onclick=\\\"closeOverlay('detailOverlay');showPODUpload('${docket}','${dispatchId}')\\\">📸 Upload POD</button>\u001b[39m\n\u001b[31m      ${c.podUploaded ? '<span style=\\\"color:var(--grn);font-size:.72rem;font-family:var(--fnt-mono);padding:6px;\\\">✓ POD uploaded</span>' : ''}\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m  `;\u001b[39m\n\u001b[31m  openOverlay('detailOverlay');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31m// TRACKING ENGINE\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31mfunction _normaliseStatus(raw) {\u001b[39m\n\u001b[31m  const s = (raw || '').toLowerCase().trim();\u001b[39m\n\u001b[31m  if (!s || s === 'error') return null;\u001b[39m\n\u001b[31m  if (s.includes('undeliver') || s.includes('delivery_fail') ||\u001b[39m\n\u001b[31m      s.includes('failed_delivery') || s.includes('not deliver') ||\u001b[39m\n\u001b[31m      s.includes('return')  || s.includes('rto')) return 'pending';\u001b[39m\n\u001b[31m  if (s.includes('deliver'))                       return 'delivered';\u001b[39m\n\u001b[31m  if (s.includes('transit') || s.includes('hub') ||\u001b[39m\n\u001b[31m      s.includes('inbound') || s.includes('outbound') ||\u001b[39m\n\u001b[31m      s.includes('out for'))                       return 'transit';\u001b[39m\n\u001b[31m  if (s.includes('book') || s.includes('pick'))   return 'booked';\u001b[39m\n\u001b[31m  return s;\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31masync function _fetchWithRetry(url, opts, maxRetries = 1, retryDelayMs = 4000) {\u001b[39m\n\u001b[31m  for (let attempt = 0; attempt <= maxRetries; attempt++) {\u001b[39m\n\u001b[31m    try {\u001b[39m\n\u001b[31m      const resp = await fetch(url, opts);\u001b[39m\n\u001b[31m      if (resp.ok) return resp;\u001b[39m\n\u001b[31m    } catch(e) {\u001b[39m\n\u001b[31m      if (attempt === maxRetries) throw e;\u001b[39m\n\u001b[31m      await new Promise(r => setTimeout(r, retryDelayMs));\u001b[39m\n\u001b[31m    }\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m  return null;\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31masync function fetchTracking(docket, courier) {\u001b[39m\n\u001b[31m  // Strategy 1: VPS proxy (SkyKing + others that block direct fetch)\u001b[39m\n\u001b[31m  const proxyUrl = window.appConfig.proxy_url;\u001b[39m\n\u001b[31m  if (proxyUrl) {\u001b[39m\n\u001b[31m    try {\u001b[39m\n\u001b[31m      const url  = `${proxyUrl}?courier=${encodeURIComponent(courier)}&docket=${encodeURIComponent(docket)}`;\u001b[39m\n\u001b[31m      const resp = await _fetchWithRetry(url, { signal: AbortSignal.timeout(15000) });\u001b[39m\n\u001b[31m      if (resp) {\u001b[39m\n\u001b[31m        const data      = await resp.json();\u001b[39m\n\u001b[31m        const normStatus = _normaliseStatus(data.status);\u001b[39m\n\u001b[31m        if (normStatus) {\u001b[39m\n\u001b[31m          return { status: normStatus, lastEvent: data.lastEvent || '', timeline: data.timeline || [] };\u001b[39m\n\u001b[31m        }\u001b[39m\n\u001b[31m        openCourierTrackUrl(docket, courier);\u001b[39m\n\u001b[31m        return { status: 'transit', lastEvent: data.lastEvent || 'Check courier site', timeline: [] };\u001b[39m\n\u001b[31m      }\u001b[39m\n\u001b[31m    } catch(e) {}\u001b[39m\n\u001b[31m  }·\u001b[39m\n\u001b[31m  // Strategy 2: Delhivery direct API\u001b[39m\n\u001b[31m  if (courier === 'Delhivery') {\u001b[39m\n\u001b[31m    try {\u001b[39m\n\u001b[31m      const resp = await _fetchWithRetry(\u001b[39m\n\u001b[31m        `https://dlv-api.delhivery.com/v3/track?waybill=${docket}`,\u001b[39m\n\u001b[31m        { signal: AbortSignal.timeout(8000) }\u001b[39m\n\u001b[31m      );\u001b[39m\n\u001b[31m      if (resp) {\u001b[39m\n\u001b[31m        const data = await resp.json();\u001b[39m\n\u001b[31m        const pkg  = data.ShipmentData?.[0]?.Shipment;\u001b[39m\n\u001b[31m        if (pkg) {\u001b[39m\n\u001b[31m          const normStatus = _normaliseStatus(pkg.Status?.Status);\u001b[39m\n\u001b[31m          return { status: normStatus || 'transit', lastEvent: pkg.Status?.Instructions || '', timeline: [] };\u001b[39m\n\u001b[31m        }\u001b[39m\n\u001b[31m      }\u001b[39m\n\u001b[31m    } catch(e) {}\u001b[39m\n\u001b[31m  }·\u001b[39m\n\u001b[31m  return { status: 'failed', lastEvent: 'Open courier site to check', timeline: [] };\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction openCourierTrackUrl(docket, courier) {\u001b[39m\n\u001b[31m  const m = COURIER_MASTER[courier];\u001b[39m\n\u001b[31m  if (!m) return;\u001b[39m\n\u001b[31m  const url = m.trackUrl.replace('{docket}', docket);\u001b[39m\n\u001b[31m  window.open(url, '_blank');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// Track tab - multiple dockets\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31m// TRACK TAB — new logic\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31mlet _trackSelected = new Set(); // dockets currently selected for tracking·\u001b[39m\n\u001b[31m// Called when Track tab opens or courier changes\u001b[39m\n\u001b[31mfunction buildTrackDocketChips() {\u001b[39m\n\u001b[31m  const courier = document.getElementById('t_courier').value;\u001b[39m\n\u001b[31m  const m = COURIER_MASTER[courier];\u001b[39m\n\u001b[31m  const maxBulk = m?.maxBulk || 1;\u001b[39m\n\u001b[31m  const limitEl = document.getElementById('trackCourierLimit');\u001b[39m\n\u001b[31m  if (limitEl) limitEl.textContent = `Max ${maxBulk} at once for ${courier}`;·\u001b[39m\n\u001b[31m  // Gather all dockets from active dispatches for this courier\u001b[39m\n\u001b[31m  const activeDis = dispatches.filter(d => d.status !== 'draft');\u001b[39m\n\u001b[31m  const allDockets = activeDis.flatMap(d =>\u001b[39m\n\u001b[31m    (d.consignees || [])\u001b[39m\n\u001b[31m      .filter(c => c.docket && d.courier === courier)\u001b[39m\n\u001b[31m      .map(c => ({ docket: c.docket, name: c.name, city: c.city, status: c.status || 'booked' }))\u001b[39m\n\u001b[31m  );·\u001b[39m\n\u001b[31m  // Also include dockets from OTHER couriers as un-selectable reference\u001b[39m\n\u001b[31m  const otherDockets = activeDis.flatMap(d =>\u001b[39m\n\u001b[31m    (d.consignees || [])\u001b[39m\n\u001b[31m      .filter(c => c.docket && d.courier !== courier)\u001b[39m\n\u001b[31m      .map(c => ({ docket: c.docket, name: c.name, courier: d.courier }))\u001b[39m\n\u001b[31m  );·\u001b[39m\n\u001b[31m  const chips = document.getElementById('trackDocketChips');\u001b[39m\n\u001b[31m  if (!allDockets.length && !otherDockets.length) {\u001b[39m\n\u001b[31m    chips.innerHTML = '<span style=\\\"font-size:.72rem;color:var(--t3);font-family:var(--fnt-mono);\\\">No dispatched dockets yet — enter manually below</span>';\u001b[39m\n\u001b[31m    return;\u001b[39m\n\u001b[31m  }·\u001b[39m\n\u001b[31m  const spClr = { delivered:'var(--grn)', transit:'var(--ylw)', booked:'var(--sky)', pending:'var(--t4)' };·\u001b[39m\n\u001b[31m  chips.innerHTML = [\u001b[39m\n\u001b[31m    // This courier's dockets\u001b[39m\n\u001b[31m    ...allDockets.map(d => {\u001b[39m\n\u001b[31m      const sel = _trackSelected.has(d.docket);\u001b[39m\n\u001b[31m      const clr = spClr[d.status] || 'var(--t4)';\u001b[39m\n\u001b[31m      return `<button onclick=\\\"toggleTrackDocket('${d.docket}')\\\" id=\\\"chip_${d.docket}\\\"\u001b[39m\n\u001b[31m        style=\\\"font-family:var(--fnt-mono);font-size:.72rem;padding:4px 10px;border-radius:20px;\u001b[39m\n\u001b[31m        cursor:pointer;transition:all .2s;border:1px solid ${sel ? 'var(--acc)' : 'var(--bdr)'};\u001b[39m\n\u001b[31m        background:${sel ? 'rgba(249,115,22,.15)' : 'var(--sur2)'};\u001b[39m\n\u001b[31m        color:${sel ? 'var(--acc)' : 'var(--t2)'};\\\">\u001b[39m\n\u001b[31m        ${d.docket} <span style=\\\"font-size:.6rem;color:${clr};\\\">● ${d.status}</span>\u001b[39m\n\u001b[31m        ${d.city ? `<span style=\\\"font-size:.6rem;color:var(--t3);margin-left:4px;\\\">${d.city}</span>` : ''}\u001b[39m\n\u001b[31m      </button>`;\u001b[39m\n\u001b[31m    }),\u001b[39m\n\u001b[31m    // Other couriers — show greyed, click selects and switches courier\u001b[39m\n\u001b[31m    ...otherDockets.slice(0, 5).map(d =>\u001b[39m\n\u001b[31m      `<span title=\\\"${d.courier}\\\" style=\\\"font-family:var(--fnt-mono);font-size:.68rem;padding:3px 8px;\u001b[39m\n\u001b[31m        border-radius:20px;border:1px dashed var(--bdr);color:var(--t4);cursor:default;\\\">\u001b[39m\n\u001b[31m        ${d.docket} <span style=\\\"font-size:.55rem;\\\">${d.courier}</span>\u001b[39m\n\u001b[31m      </span>`\u001b[39m\n\u001b[31m    )\u001b[39m\n\u001b[31m  ].join('');·\u001b[39m\n\u001b[31m  updateTrackSummary();\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction toggleTrackDocket(docket) {\u001b[39m\n\u001b[31m  const courier = document.getElementById('t_courier').value;\u001b[39m\n\u001b[31m  const m = COURIER_MASTER[courier];\u001b[39m\n\u001b[31m  const maxBulk = m?.maxBulk || 1;·\u001b[39m\n\u001b[31m  if (_trackSelected.has(docket)) {\u001b[39m\n\u001b[31m    _trackSelected.delete(docket);\u001b[39m\n\u001b[31m  } else {\u001b[39m\n\u001b[31m    if (_trackSelected.size >= maxBulk) {\u001b[39m\n\u001b[31m      showToast(`${courier} supports max ${maxBulk} docket(s) at once`, 'err');\u001b[39m\n\u001b[31m      return;\u001b[39m\n\u001b[31m    }\u001b[39m\n\u001b[31m    _trackSelected.add(docket);\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m  buildTrackDocketChips(); // re-render chips with updated selection\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction addManualDockets() {\u001b[39m\n\u001b[31m  const raw = document.getElementById('t_manual_dockets').value;\u001b[39m\n\u001b[31m  const courier = document.getElementById('t_courier').value;\u001b[39m\n\u001b[31m  const m = COURIER_MASTER[courier];\u001b[39m\n\u001b[31m  const maxBulk = m?.maxBulk || 1;\u001b[39m\n\u001b[31m  const newDockets = raw.split(/[,\\\\n]/).map(d => d.trim()).filter(Boolean);\u001b[39m\n\u001b[31m  let added = 0;\u001b[39m\n\u001b[31m  for (const d of newDockets) {\u001b[39m\n\u001b[31m    if (_trackSelected.size >= maxBulk) {\u001b[39m\n\u001b[31m      showToast(`Max ${maxBulk} reached for ${courier}`, 'err');\u001b[39m\n\u001b[31m      break;\u001b[39m\n\u001b[31m    }\u001b[39m\n\u001b[31m    _trackSelected.add(d);\u001b[39m\n\u001b[31m    added++;\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m  document.getElementById('t_manual_dockets').value = '';\u001b[39m\n\u001b[31m  buildTrackDocketChips();\u001b[39m\n\u001b[31m  if (added) showToast(`${added} docket(s) added ✓`, 'ok');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction clearTrackSelection() {\u001b[39m\n\u001b[31m  _trackSelected.clear();\u001b[39m\n\u001b[31m  buildTrackDocketChips();\u001b[39m\n\u001b[31m  document.getElementById('trackResultsArea').innerHTML = '';\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction updateTrackSummary() {\u001b[39m\n\u001b[31m  const summary = document.getElementById('trackSelectedSummary');\u001b[39m\n\u001b[31m  const countEl = document.getElementById('trackSelectedCount');\u001b[39m\n\u001b[31m  const listEl  = document.getElementById('trackSelectedList');\u001b[39m\n\u001b[31m  if (!summary) return;\u001b[39m\n\u001b[31m  const arr = Array.from(_trackSelected);\u001b[39m\n\u001b[31m  summary.style.display = arr.length ? 'block' : 'none';\u001b[39m\n\u001b[31m  if (countEl) countEl.textContent = `${arr.length} docket(s) selected`;\u001b[39m\n\u001b[31m  if (listEl)  listEl.textContent  = arr.slice(0, 5).join(', ') + (arr.length > 5 ? `... +${arr.length-5}` : '');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction openTrackOnCourierSite() {\u001b[39m\n\u001b[31m  const courier = document.getElementById('t_courier').value;\u001b[39m\n\u001b[31m  const dockets = Array.from(_trackSelected);\u001b[39m\n\u001b[31m  if (!dockets.length) { showToast('Select dockets first', 'err'); return; }\u001b[39m\n\u001b[31m  const m = COURIER_MASTER[courier];\u001b[39m\n\u001b[31m  if (!m) return;\u001b[39m\n\u001b[31m  // SkyKing: open with all CNOs comma-separated in URL\u001b[39m\n\u001b[31m  let url;\u001b[39m\n\u001b[31m  if (courier === 'SkyKing') {\u001b[39m\n\u001b[31m    url = `https://skyking.co/track?cno=${dockets.join(',')}`;\u001b[39m\n\u001b[31m  } else if (dockets.length === 1) {\u001b[39m\n\u001b[31m    url = m.trackUrl.replace('{docket}', dockets[0]);\u001b[39m\n\u001b[31m  } else {\u001b[39m\n\u001b[31m    url = m.trackBulkUrl || m.trackUrl.replace('{docket}', dockets[0]);\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m  window.open(url, '_blank');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31masync function trackSelected() {\u001b[39m\n\u001b[31m  const courier = document.getElementById('t_courier').value;\u001b[39m\n\u001b[31m  const dockets = Array.from(_trackSelected);\u001b[39m\n\u001b[31m  if (!dockets.length) { showToast('Select at least one docket', 'err'); return; }·\u001b[39m\n\u001b[31m  const area = document.getElementById('trackResultsArea');\u001b[39m\n\u001b[31m  area.innerHTML = `<div class=\\\"card\\\" style=\\\"text-align:center;padding:20px;\\\">\u001b[39m\n\u001b[31m    <div style=\\\"font-size:1.5rem;margin-bottom:8px;\\\">⏳</div>\u001b[39m\n\u001b[31m    <div style=\\\"font-family:var(--fnt-mono);font-size:.78rem;color:var(--t2);\\\">Tracking ${dockets.length} docket(s) via ${courier}...</div>\u001b[39m\n\u001b[31m  </div>`;·\u001b[39m\n\u001b[31m  let resultsHTML = '';\u001b[39m\n\u001b[31m  for (const docket of dockets) {\u001b[39m\n\u001b[31m    const result = await fetchTracking(docket, courier);\u001b[39m\n\u001b[31m    const sp = (!result.status || result.status === 'failed') ? 'pending' : result.status;\u001b[39m\n\u001b[31m    const spClr = { delivered:'var(--grn)', transit:'var(--ylw)', booked:'var(--sky)', pending:'var(--t3)' };\u001b[39m\n\u001b[31m    const clr = spClr[sp] || 'var(--t3)';\u001b[39m\n\u001b[31m    // Update stored dispatch if this docket exists in our records\u001b[39m\n\u001b[31m    for (const d of dispatches) {\u001b[39m\n\u001b[31m      for (const c of (d.consignees||[])) {\u001b[39m\n\u001b[31m        if (c.docket === docket && result.status && result.status !== 'failed') {\u001b[39m\n\u001b[31m          c.status = result.status;\u001b[39m\n\u001b[31m          c.lastEvent = result.lastEvent;\u001b[39m\n\u001b[31m          c.lastTrackedAt = new Date().toISOString();\u001b[39m\n\u001b[31m        }\u001b[39m\n\u001b[31m      }\u001b[39m\n\u001b[31m    }\u001b[39m\n\u001b[31m    resultsHTML += `\u001b[39m\n\u001b[31m    <div style=\\\"background:var(--sur);border:1px solid var(--bdr);border-left:3px solid ${clr};\u001b[39m\n\u001b[31m      border-radius:var(--r-sm);padding:10px 12px;margin-bottom:8px;\\\">\u001b[39m\n\u001b[31m      <div style=\\\"display:flex;justify-content:space-between;align-items:center;margin-bottom:6px;\\\">\u001b[39m\n\u001b[31m        <span style=\\\"font-family:var(--fnt-mono);font-size:.9rem;color:var(--sky);\\\">${docket}</span>\u001b[39m\n\u001b[31m        <span class=\\\"status-pill sp-${sp}\\\">${sp}</span>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      <div style=\\\"font-size:.75rem;color:var(--t2);font-family:var(--fnt-mono);\\\">${result.lastEvent || '—'}</div>\u001b[39m\n\u001b[31m      ${result.lastScanLocation ? `<div style=\\\"font-size:.68rem;color:var(--t3);margin-top:3px;\\\">📍 ${result.lastScanLocation}</div>` : ''}\u001b[39m\n\u001b[31m      <div style=\\\"display:flex;gap:8px;margin-top:8px;\\\">\u001b[39m\n\u001b[31m        <button class=\\\"btn-sm\\\" onclick=\\\"openCourierTrackUrl('${docket}','${courier}')\\\">🔗 Open on ${courier}</button>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m    </div>`;\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m  saveData();\u001b[39m\n\u001b[31m  area.innerHTML = `<div style=\\\"margin-top:4px;\\\">${resultsHTML}</div>`;\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// Keep old names as aliases for compatibility\u001b[39m\n\u001b[31mfunction trackMultiple() { trackSelected(); }\u001b[39m\n\u001b[31mfunction openCourierSite() { openTrackOnCourierSite(); }·\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31m// HISTORY\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31mfunction renderHistory() {\u001b[39m\n\u001b[31m  const list = document.getElementById('historyList');\u001b[39m\n\u001b[31m  if (!dispatches.length) {\u001b[39m\n\u001b[31m    list.innerHTML = '<div class=\\\"empty\\\"><div class=\\\"e-icon\\\">📭</div><p>No dispatches yet.</p></div>';\u001b[39m\n\u001b[31m    return;\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m  list.innerHTML = dispatches.map(d => {\u001b[39m\n\u001b[31m    const cons = d.consignees?.length || 0;\u001b[39m\n\u001b[31m    const docketed = d.consignees?.filter(c=>c.docket).length || 0;\u001b[39m\n\u001b[31m    const clr = d.status === 'draft' ? 'var(--ylw)' : 'var(--grn)';\u001b[39m\n\u001b[31m    return `\u001b[39m\n\u001b[31m    <div class=\\\"docket-tile\\\" onclick=\\\"showDispatchDetail('${d.id}')\\\">\u001b[39m\n\u001b[31m      <div class=\\\"docket-tile-hdr\\\">\u001b[39m\n\u001b[31m        <div class=\\\"docket-tile-left\\\">\u001b[39m\n\u001b[31m          <div class=\\\"docket-num\\\" style=\\\"font-size:.78rem;\\\">${d.id}</div>\u001b[39m\n\u001b[31m          <div class=\\\"docket-store\\\">${d.client||'No client'} ${d.jobref ? '› '+d.jobref : ''}</div>\u001b[39m\n\u001b[31m          <div class=\\\"docket-courier\\\">📦 ${cons} consignees · ${docketed} docketed · ${d.courier||'—'} · ${d.dispatchDate||d.createdAt?.slice(0,10)||'—'}</div>\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m        <span class=\\\"status-pill\\\" style=\\\"background:rgba(0,0,0,.2);color:${clr};\\\">${d.status||'active'}</span>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m    </div>`;\u001b[39m\n\u001b[31m  }).join('');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction showDispatchDetail(id) {\u001b[39m\n\u001b[31m  const d = dispatches.find(x => x.id === id);\u001b[39m\n\u001b[31m  if (!d) return;\u001b[39m\n\u001b[31m  const content = document.getElementById('detailContent');\u001b[39m\n\u001b[31m  content.innerHTML = `\u001b[39m\n\u001b[31m    <div class=\\\"sheet-title\\\">${d.client} ${d.jobref ? '› '+d.jobref : ''}</div>\u001b[39m\n\u001b[31m    <div class=\\\"info-tag\\\"><strong>${d.id}</strong> · ${d.courier||'—'} · ${d.dispatchDate||'—'}</div>\u001b[39m\n\u001b[31m    <div class=\\\"sec-div\\\"><span>Consignees (${d.consignees?.length||0})</span></div>\u001b[39m\n\u001b[31m    ${(d.consignees||[]).map((c,i) => `\u001b[39m\n\u001b[31m      <div style=\\\"background:var(--sur2);border:1px solid var(--bdr);border-radius:var(--r-sm);padding:10px;margin-bottom:6px;\\\">\u001b[39m\n\u001b[31m        <div style=\\\"font-weight:600;margin-bottom:4px;\\\">${i+1}. ${c.name} — ${c.city}</div>\u001b[39m\n\u001b[31m        ${c.docket ? `<div style=\\\"font-family:var(--fnt-mono);font-size:.78rem;color:var(--sky);\\\">🔖 ${c.docket}</div>` : ''}\u001b[39m\n\u001b[31m        ${c.material ? `<div style=\\\"font-size:.75rem;color:var(--t2);\\\">📦 ${c.material} ${c.qty ? '('+c.qty+')':''}</div>` : ''}\u001b[39m\n\u001b[31m        <span class=\\\"status-pill sp-${c.status||'pending'}\\\" style=\\\"margin-top:4px;display:inline-block;\\\">${c.status||'pending'}</span>\u001b[39m\n\u001b[31m      </div>`).join('')}\u001b[39m\n\u001b[31m    ${d.remarks ? `<div class=\\\"info-tag\\\">📝 ${d.remarks}</div>` : ''}\u001b[39m\n\u001b[31m    <div class=\\\"btn-row\\\" style=\\\"margin-top:14px;\\\">\u001b[39m\n\u001b[31m      <button class=\\\"btn btn-ghost\\\" onclick=\\\"deleteDispatch('${d.id}');closeOverlay('detailOverlay')\\\">🗑️ Delete</button>\u001b[39m\n\u001b[31m      <button class=\\\"btn btn-orange\\\" onclick=\\\"closeOverlay('detailOverlay');showPane('dashboard')\\\">📊 View in Dashboard</button>\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m  `;\u001b[39m\n\u001b[31m  openOverlay('detailOverlay');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction deleteDispatch(id) {\u001b[39m\n\u001b[31m  if (!confirm('Delete this dispatch?')) return;\u001b[39m\n\u001b[31m  dispatches = dispatches.filter(d => d.id !== id);\u001b[39m\n\u001b[31m  saveData();\u001b[39m\n\u001b[31m  updateHistCount();\u001b[39m\n\u001b[31m  renderHistory();\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction updateHistCount() {\u001b[39m\n\u001b[31m  document.getElementById('histCount').textContent = dispatches.length;\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31m// GOOGLE SHEETS\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31masync function logToSheets(d) {\u001b[39m\n\u001b[31m  const url = window.appConfig.webhook_url;\u001b[39m\n\u001b[31m  if (!url) return;\u001b[39m\n\u001b[31m  try {\u001b[39m\n\u001b[31m    await fetch(url, {\u001b[39m\n\u001b[31m      method: 'POST', mode: 'no-cors',\u001b[39m\n\u001b[31m      headers: { 'Content-Type': 'application/json' },\u001b[39m\n\u001b[31m      body: JSON.stringify({\u001b[39m\n\u001b[31m        type: 'dispatch', id: d.id, client: d.client, jobref: d.jobref,\u001b[39m\n\u001b[31m        courier: d.courier, consignees: d.consignees?.length||0,\u001b[39m\n\u001b[31m        destinations: d.consignees?.map(c=>c.city).join(', ')||'',\u001b[39m\n\u001b[31m        dockets: d.consignees?.filter(c=>c.docket).map(c=>c.docket).join(', ')||'',\u001b[39m\n\u001b[31m        dispatchDate: d.dispatchDate, status: d.status,\u001b[39m\n\u001b[31m        timestamp: new Date().toISOString(), company: window.appConfig.company\u001b[39m\n\u001b[31m      })\u001b[39m\n\u001b[31m    });\u001b[39m\n\u001b[31m  } catch(e) {}\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31m// CONFIG — Google Sheet is the source of truth\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════·\u001b[39m\n\u001b[31mfunction onSheetsUrlChange() {\u001b[39m\n\u001b[31m  const url = document.getElementById('cfg_sheets_url').value.trim();\u001b[39m\n\u001b[31m  // Show sync button state\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction saveSheetsUrl() {\u001b[39m\n\u001b[31m  const url = document.getElementById('cfg_sheets_url').value.trim();\u001b[39m\n\u001b[31m  if (!url) { showToast('Enter the Apps Script URL first', 'err'); return; }\u001b[39m\n\u001b[31m  window.appConfig.sheets_url = url;\u001b[39m\n\u001b[31m  saveSheetsUrlLocal();\u001b[39m\n\u001b[31m  syncFromSheet(false);\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m/**\u001b[39m\n\u001b[31m * Fetch all config from Google Sheet and apply to appConfig.\u001b[39m\n\u001b[31m * Called on startup (silent=true) and manually (silent=false).\u001b[39m\n\u001b[31m */\u001b[39m\n\u001b[31masync function syncFromSheet(silent = false) {\u001b[39m\n\u001b[31m  const url = window.appConfig.sheets_url;\u001b[39m\n\u001b[31m  if (!url) { if (!silent) showToast('No Sheets URL configured', 'err'); return; }·\u001b[39m\n\u001b[31m  setSyncStatus('syncing', 'Syncing config from Google Sheet...');\u001b[39m\n\u001b[31m  try {\u001b[39m\n\u001b[31m    const resp = await fetch(url + '?action=pwa_config', {\u001b[39m\n\u001b[31m      method: 'GET',\u001b[39m\n\u001b[31m      redirect: 'follow'\u001b[39m\n\u001b[31m    });\u001b[39m\n\u001b[31m    if (!resp.ok) throw new Error('HTTP ' + resp.status);\u001b[39m\n\u001b[31m    const cfg = await resp.json();·\u001b[39m\n\u001b[31m    // Apply all values from sheet to appConfig\u001b[39m\n\u001b[31m    const keyMap = {\u001b[39m\n\u001b[31m      proxy_url:           'proxy_url',\u001b[39m\n\u001b[31m      webhook_url:         'webhook_url',\u001b[39m\n\u001b[31m      ollama_url:          'ollama_url',\u001b[39m\n\u001b[31m      ocr_mode:            'ocr_mode',\u001b[39m\n\u001b[31m      company:             'company',\u001b[39m\n\u001b[31m      city:                'city',\u001b[39m\n\u001b[31m      auto_log:            'auto_log',\u001b[39m\n\u001b[31m      refresh_on_load:     'refresh_on_load',\u001b[39m\n\u001b[31m      admin_password:      'admin_password',\u001b[39m\n\u001b[31m      n8n_dispatch_webhook:'n8n_dispatch_webhook',\u001b[39m\n\u001b[31m    };\u001b[39m\n\u001b[31m    Object.entries(keyMap).forEach(([sheetKey, cfgKey]) => {\u001b[39m\n\u001b[31m      if (cfg[sheetKey] !== undefined && cfg[sheetKey] !== '') {\u001b[39m\n\u001b[31m        window.appConfig[cfgKey] = cfg[sheetKey];\u001b[39m\n\u001b[31m      }\u001b[39m\n\u001b[31m    });·\u001b[39m\n\u001b[31m    // Apply courier master if provided\u001b[39m\n\u001b[31m    if (cfg._courier_master && Array.isArray(cfg._courier_master)) {\u001b[39m\n\u001b[31m      cfg._courier_master.forEach(c => {\u001b[39m\n\u001b[31m        if (c.name && c.trackUrl) {\u001b[39m\n\u001b[31m          COURIER_MASTER[c.name] = {\u001b[39m\n\u001b[31m            trackUrl: c.trackUrl,\u001b[39m\n\u001b[31m            trackBulkUrl: c.bulkUrl || '',\u001b[39m\n\u001b[31m            maxBulk: parseInt(c.maxBulk) || 1\u001b[39m\n\u001b[31m          };\u001b[39m\n\u001b[31m        }\u001b[39m\n\u001b[31m      });\u001b[39m\n\u001b[31m    }·\u001b[39m\n\u001b[31m    renderCourierMasterList();\u001b[39m\n\u001b[31m    renderSheetConfigDisplay(cfg);\u001b[39m\n\u001b[31m    setSyncStatus('ok', 'Synced from sheet · ' + new Date().toLocaleTimeString());\u001b[39m\n\u001b[31m    if (!silent) showToast('Config synced from sheet ✓', 'ok');·\u001b[39m\n\u001b[31m    // If refresh_on_load is true, trigger dashboard refresh\u001b[39m\n\u001b[31m    if (window.appConfig.refresh_on_load === 'true') {\u001b[39m\n\u001b[31m      setTimeout(refreshAllTracking, 2000);\u001b[39m\n\u001b[31m    }·\u001b[39m\n\u001b[31m  } catch(e) {\u001b[39m\n\u001b[31m    setSyncStatus('err', 'Sync failed: ' + e.message);\u001b[39m\n\u001b[31m    if (!silent) showToast('Sync failed — check URL', 'err');\u001b[39m\n\u001b[31m    console.error('Sheets sync error:', e);\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction setSyncStatus(state, msg) {\u001b[39m\n\u001b[31m  const el = document.getElementById('syncStatus');\u001b[39m\n\u001b[31m  const icon = document.getElementById('syncIcon');\u001b[39m\n\u001b[31m  const msgEl = document.getElementById('syncMsg');\u001b[39m\n\u001b[31m  el.style.display = 'flex';\u001b[39m\n\u001b[31m  icon.textContent = state === 'ok' ? '✅' : state === 'err' ? '❌' : '⏳';\u001b[39m\n\u001b[31m  msgEl.textContent = msg;\u001b[39m\n\u001b[31m  el.style.borderColor = state === 'ok' ? 'rgba(34,197,94,.3)' : state === 'err' ? 'rgba(239,68,68,.3)' : 'rgba(234,179,8,.3)';\u001b[39m\n\u001b[31m  el.style.background = state === 'ok' ? 'rgba(34,197,94,.05)' : state === 'err' ? 'rgba(239,68,68,.05)' : 'rgba(234,179,8,.05)';\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction renderSheetConfigDisplay(cfg) {\u001b[39m\n\u001b[31m  const display = document.getElementById('sheetConfigDisplay');\u001b[39m\n\u001b[31m  display.style.display = 'block';\u001b[39m\n\u001b[31m  document.getElementById('sheetSyncTime').textContent = 'Last sync: ' + new Date().toLocaleTimeString();\u001b[39m\n\u001b[31m  const showKeys = ['proxy_url','webhook_url','ollama_url','ocr_mode','company','city','auto_log'];\u001b[39m\n\u001b[31m  document.getElementById('sheetConfigRows').innerHTML = showKeys.map(k => {\u001b[39m\n\u001b[31m    const val = cfg[k] || window.appConfig[k] || '—';\u001b[39m\n\u001b[31m    const isEmpty = !cfg[k];\u001b[39m\n\u001b[31m    return `<div class=\\\"courier-master-row\\\">\u001b[39m\n\u001b[31m      <div class=\\\"cml\\\">\u001b[39m\n\u001b[31m        <div class=\\\"cml-name\\\" style=\\\"font-family:var(--fnt-mono);font-size:.75rem;\\\">${k}</div>\u001b[39m\n\u001b[31m        <div class=\\\"cml-url\\\" style=\\\"${isEmpty ? 'color:var(--red);' : ''}\\\">${isEmpty ? '⚠️ Not set in sheet' : val}</div>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m    </div>`;\u001b[39m\n\u001b[31m  }).join('');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m/**\u001b[39m\n\u001b[31m * Push current config changes back to the sheet.\u001b[39m\n\u001b[31m * Called when user edits config from PWA (rare — usually done in sheet directly).\u001b[39m\n\u001b[31m */\u001b[39m\n\u001b[31masync function pushConfigToSheet(updates) {\u001b[39m\n\u001b[31m  const url = window.appConfig.sheets_url;\u001b[39m\n\u001b[31m  if (!url) return;\u001b[39m\n\u001b[31m  try {\u001b[39m\n\u001b[31m    await fetch(url, {\u001b[39m\n\u001b[31m      method: 'POST', mode: 'no-cors',\u001b[39m\n\u001b[31m      headers: { 'Content-Type': 'application/json' },\u001b[39m\n\u001b[31m      body: JSON.stringify({ type: 'save_config', config: updates })\u001b[39m\n\u001b[31m    });\u001b[39m\n\u001b[31m  } catch(e) {}\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction clearAll() {\u001b[39m\n\u001b[31m  if (!confirm('Delete ALL dispatch data?')) return;\u001b[39m\n\u001b[31m  dispatches = [];\u001b[39m\n\u001b[31m  saveData();\u001b[39m\n\u001b[31m  updateHistCount();\u001b[39m\n\u001b[31m  renderDashboard();\u001b[39m\n\u001b[31m  renderHistory();\u001b[39m\n\u001b[31m  showToast('Cleared', 'ok');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction renderCourierMasterList() {\u001b[39m\n\u001b[31m  const el = document.getElementById('courierMasterList');\u001b[39m\n\u001b[31m  el.innerHTML = Object.entries(COURIER_MASTER).map(([name, info]) => `\u001b[39m\n\u001b[31m    <div class=\\\"courier-master-row\\\">\u001b[39m\n\u001b[31m      <div class=\\\"cml\\\">\u001b[39m\n\u001b[31m        <div class=\\\"cml-name\\\">${name}</div>\u001b[39m\n\u001b[31m        <div class=\\\"cml-url\\\">${info.trackUrl}</div>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      <button class=\\\"btn-sm\\\" onclick=\\\"window.open('${info.trackBulkUrl||info.trackUrl.replace('{docket}','')}','_blank')\\\">Open</button>\u001b[39m\n\u001b[31m    </div>`).join('');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction openCourierMaster() {\u001b[39m\n\u001b[31m  const ed = document.getElementById('courierMasterEditor');\u001b[39m\n\u001b[31m  ed.innerHTML = Object.entries(COURIER_MASTER).map(([name, info]) => `\u001b[39m\n\u001b[31m    <div style=\\\"margin-bottom:14px;\\\">\u001b[39m\n\u001b[31m      <div style=\\\"font-family:var(--fnt-head);font-size:.75rem;font-weight:700;color:var(--t2);text-transform:uppercase;letter-spacing:1.5px;margin-bottom:6px;\\\">${name}</div>\u001b[39m\n\u001b[31m      <div class=\\\"f\\\" style=\\\"margin-bottom:6px;\\\">\u001b[39m\n\u001b[31m        <label>Track URL (use {docket})</label>\u001b[39m\n\u001b[31m        <input type=\\\"text\\\" value=\\\"${info.trackUrl}\\\" id=\\\"cm_${name.replace(/\\\\s/g,'_')}\\\"\u001b[39m\n\u001b[31m          style=\\\"font-family:var(--fnt-mono);font-size:.75rem;\\\">\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m    </div>`).join('') +\u001b[39m\n\u001b[31m    `<button class=\\\"btn btn-orange\\\" onclick=\\\"saveCourierMaster()\\\">Save Courier Master</button>`;\u001b[39m\n\u001b[31m  openOverlay('courierMasterOverlay');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction saveCourierMaster() {\u001b[39m\n\u001b[31m  Object.keys(COURIER_MASTER).forEach(name => {\u001b[39m\n\u001b[31m    const el = document.getElementById('cm_' + name.replace(/\\\\s/g,'_'));\u001b[39m\n\u001b[31m    if (el) COURIER_MASTER[name].trackUrl = el.value;\u001b[39m\n\u001b[31m  });\u001b[39m\n\u001b[31m  closeOverlay('courierMasterOverlay');\u001b[39m\n\u001b[31m  renderCourierMasterList();\u001b[39m\n\u001b[31m  showToast('Courier master saved ✓', 'ok');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31m// UI UTILS\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31m// ── ADMIN AUTH ───────────────────────────────────────────────────────────\u001b[39m\n\u001b[31mlet _adminAuthed = false;\u001b[39m\n\u001b[31mlet _lastAdminPane = null;·\u001b[39m\n\u001b[31mfunction showPane(name) {\u001b[39m\n\u001b[31m  // Config tab requires admin auth — re-ask if navigated away\u001b[39m\n\u001b[31m  if (name === 'config') {\u001b[39m\n\u001b[31m    if (!_adminAuthed) {\u001b[39m\n\u001b[31m      _lastAdminPane = 'config';\u001b[39m\n\u001b[31m      showAdminLogin();\u001b[39m\n\u001b[31m      return;\u001b[39m\n\u001b[31m    }\u001b[39m\n\u001b[31m  } else {\u001b[39m\n\u001b[31m    // User navigated away from config — revoke auth\u001b[39m\n\u001b[31m    if (_lastAdminPane === 'config') _adminAuthed = false;\u001b[39m\n\u001b[31m    _lastAdminPane = name;\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m  document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));\u001b[39m\n\u001b[31m  document.querySelectorAll('.pane').forEach(p => p.classList.remove('active'));\u001b[39m\n\u001b[31m  document.getElementById('tab-'+name).classList.add('active');\u001b[39m\n\u001b[31m  document.getElementById('pane-'+name).classList.add('active');\u001b[39m\n\u001b[31m  if (name === 'dashboard') renderDashboard();\u001b[39m\n\u001b[31m  if (name === 'history') renderHistory();\u001b[39m\n\u001b[31m  if (name === 'track') buildTrackDocketChips();\u001b[39m\n\u001b[31m  if (name === 'config') { document.getElementById('cfg_sheets_url').value = window.appConfig.sheets_url || ''; }\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction showAdminLogin() {\u001b[39m\n\u001b[31m  // Password replaced by hub identity — only harish/pramod can access config\u001b[39m\n\u001b[31m  if (['harish', 'pramod'].includes(window._hubUser?.id)) {\u001b[39m\n\u001b[31m    _adminAuthed = true;\u001b[39m\n\u001b[31m    showPane('config');\u001b[39m\n\u001b[31m  } else {\u001b[39m\n\u001b[31m    showToast('Config access restricted to admin users', 'err');\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31masync function submitAdminLogin() {\u001b[39m\n\u001b[31m  showAdminLogin(); // no-op shim — kept for any lingering HTML onclick refs\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31mfunction openOverlay(id) { document.getElementById(id).classList.add('open'); }\u001b[39m\n\u001b[31mfunction closeOverlay(id) { document.getElementById(id).classList.remove('open'); }·\u001b[39m\n\u001b[31mlet toastTimer;\u001b[39m\n\u001b[31mfunction showToast(msg, type='') {\u001b[39m\n\u001b[31m  const t = document.getElementById('toast');\u001b[39m\n\u001b[31m  t.textContent = msg;\u001b[39m\n\u001b[31m  t.className = 'toast show' + (type ? ' '+type : '');\u001b[39m\n\u001b[31m  clearTimeout(toastTimer);\u001b[39m\n\u001b[31m  toastTimer = setTimeout(() => t.classList.remove('show'), 2800);\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction showOCRWorking(show, msg='') {\u001b[39m\n\u001b[31m  const el = document.getElementById('ocrWorking');\u001b[39m\n\u001b[31m  el.classList.toggle('visible', show);\u001b[39m\n\u001b[31m  if (msg) document.getElementById('ocrWorkingText').textContent = msg;\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction showAIProgress(label, pct) {\u001b[39m\n\u001b[31m  const el = document.getElementById('aiProgress');\u001b[39m\n\u001b[31m  el.classList.add('visible');\u001b[39m\n\u001b[31m  document.getElementById('aiProgLabel').textContent = label;\u001b[39m\n\u001b[31m  document.getElementById('aiProgFill').style.width = pct + '%';\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31mfunction hideAIProgress() {\u001b[39m\n\u001b[31m  document.getElementById('aiProgress').classList.remove('visible');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction updateNetDot() {\u001b[39m\n\u001b[31m  document.getElementById('netDot').style.background = navigator.onLine ? 'var(--grn)' : 'var(--red)';\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction setToday() {\u001b[39m\n\u001b[31m  document.getElementById('n_date').value = new Date().toISOString().split('T')[0];\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m// ── EXPORT CLEAN SHEET ───────────────────────────────────────────────────\u001b[39m\n\u001b[31mfunction exportCleanSheet() {\u001b[39m\n\u001b[31m  const pane = document.getElementById('verifyPane');\u001b[39m\n\u001b[31m  const d = pane._pendingDispatch;\u001b[39m\n\u001b[31m  if (!d || !d.consignees?.length) { showToast('No data to export', 'err'); return; }·\u001b[39m\n\u001b[31m  // Build CSV\u001b[39m\n\u001b[31m  const headers = ['S.No.','Store Name','City','Pincode','Phone','Address','Docket No.','Material','Courier','Status'];\u001b[39m\n\u001b[31m  const rows = d.consignees.map((c, i) => [\u001b[39m\n\u001b[31m    i + 1,\u001b[39m\n\u001b[31m    `\\\"${(c.name||'').replace(/\\\"/g,'\\\"\\\"')}\\\"`,\u001b[39m\n\u001b[31m    `\\\"${(c.city||'').replace(/\\\"/g,'\\\"\\\"')}\\\"`,\u001b[39m\n\u001b[31m    c.pincode || '',\u001b[39m\n\u001b[31m    c.phone || '',\u001b[39m\n\u001b[31m    `\\\"${(c.address||'').replace(/\\\"/g,'\\\"\\\"')}\\\"`,\u001b[39m\n\u001b[31m    c.docket || '',\u001b[39m\n\u001b[31m    `\\\"${(c.material||'').replace(/\\\"/g,'\\\"\\\"')}\\\"`,\u001b[39m\n\u001b[31m    d.courier || '',\u001b[39m\n\u001b[31m    c.status || 'pending'\u001b[39m\n\u001b[31m  ]);·\u001b[39m\n\u001b[31m  const csv = [headers.join(','), ...rows.map(r => r.join(','))].join('\\\\n');\u001b[39m\n\u001b[31m  const blob = new Blob([csv], { type: 'text/csv' });\u001b[39m\n\u001b[31m  const url = URL.createObjectURL(blob);\u001b[39m\n\u001b[31m  const a = document.createElement('a');\u001b[39m\n\u001b[31m  a.href = url;\u001b[39m\n\u001b[31m  a.download = `360LM_Dispatch_${d.client||'export'}_${d.dispatchDate||new Date().toISOString().slice(0,10)}_clean.csv`;\u001b[39m\n\u001b[31m  a.click();\u001b[39m\n\u001b[31m  URL.revokeObjectURL(url);\u001b[39m\n\u001b[31m  showToast('Clean sheet exported ✓', 'ok');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// ── DOCKET SLIP UPLOAD — SKIP OCR IF DOCKET ALREADY MAPPED ───────────────\u001b[39m\n\u001b[31mfunction shouldSkipOCR() {\u001b[39m\n\u001b[31m  // If any consignee already has a docket (from store list mapping), skip OCR extraction\u001b[39m\n\u001b[31m  const consList = document.getElementById('consList');\u001b[39m\n\u001b[31m  if (!consList) return false;\u001b[39m\n\u001b[31m  const cards = consList.querySelectorAll('.cons-card');\u001b[39m\n\u001b[31m  let hasAnyDocket = false;\u001b[39m\n\u001b[31m  cards.forEach(card => {\u001b[39m\n\u001b[31m    const d = card.querySelector('.cons-docket-input')?.value?.trim();\u001b[39m\n\u001b[31m    if (d) hasAnyDocket = true;\u001b[39m\n\u001b[31m  });\u001b[39m\n\u001b[31m  return hasAnyDocket;\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// ─── TRACKING UPDATE TO SHEETS ──────────────────────────────────────────\u001b[39m\n\u001b[31masync function logTrackingUpdate(docket, status, lastEvent, location) {\u001b[39m\n\u001b[31m  const url = window.appConfig.webhook_url;\u001b[39m\n\u001b[31m  if (!url) return;\u001b[39m\n\u001b[31m  try {\u001b[39m\n\u001b[31m    await fetch(url, {\u001b[39m\n\u001b[31m      method: 'POST', mode: 'no-cors',\u001b[39m\n\u001b[31m      headers: { 'Content-Type': 'application/json' },\u001b[39m\n\u001b[31m      body: JSON.stringify({\u001b[39m\n\u001b[31m        type: 'tracking_update',\u001b[39m\n\u001b[31m        docket, status, lastEvent, location,\u001b[39m\n\u001b[31m        timestamp: new Date().toISOString()\u001b[39m\n\u001b[31m      })\u001b[39m\n\u001b[31m    });\u001b[39m\n\u001b[31m  } catch(e) {}\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// ─── POD / RECIPIENT PHOTO UPLOAD ───────────────────────────────────────\u001b[39m\n\u001b[31m// Stores base64 locally, pushes to Sheets webhook (Apps Script saves to Drive)\u001b[39m\n\u001b[31mfunction showPODUpload(docket, dispatchId) {\u001b[39m\n\u001b[31m  const overlay = document.getElementById('podOverlay');\u001b[39m\n\u001b[31m  overlay.dataset.docket = docket;\u001b[39m\n\u001b[31m  overlay.dataset.dispatchId = dispatchId;\u001b[39m\n\u001b[31m  document.getElementById('podPreview').innerHTML = '';\u001b[39m\n\u001b[31m  document.getElementById('podFileInput').value = '';\u001b[39m\n\u001b[31m  document.getElementById('recipientFileInput').value = '';\u001b[39m\n\u001b[31m  openOverlay('podOverlay');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31masync function uploadPODFiles() {\u001b[39m\n\u001b[31m  const overlay = document.getElementById('podOverlay');\u001b[39m\n\u001b[31m  const docket = overlay.dataset.docket;\u001b[39m\n\u001b[31m  const dispatchId = overlay.dataset.dispatchId;\u001b[39m\n\u001b[31m  const podFile = document.getElementById('podFileInput').files[0];\u001b[39m\n\u001b[31m  const recipFile = document.getElementById('recipientFileInput').files[0];\u001b[39m\n\u001b[31m  if (!podFile && !recipFile) { showToast('Select at least one photo', 'err'); return; }·\u001b[39m\n\u001b[31m  showToast('Uploading...', 'info');\u001b[39m\n\u001b[31m  const toBase64 = f => new Promise((res,rej) => {\u001b[39m\n\u001b[31m    const r = new FileReader(); r.onload = e => res(e.target.result); r.onerror = rej; r.readAsDataURL(f);\u001b[39m\n\u001b[31m  });·\u001b[39m\n\u001b[31m  const payload = {\u001b[39m\n\u001b[31m    type: 'pod_upload',\u001b[39m\n\u001b[31m    docket, dispatchId,\u001b[39m\n\u001b[31m    timestamp: new Date().toISOString()\u001b[39m\n\u001b[31m  };\u001b[39m\n\u001b[31m  if (podFile)   payload.pod_image = await toBase64(podFile);\u001b[39m\n\u001b[31m  if (recipFile) payload.recipient_image = await toBase64(recipFile);·\u001b[39m\n\u001b[31m  // Update local store\u001b[39m\n\u001b[31m  for (const d of dispatches) {\u001b[39m\n\u001b[31m    if (d.id !== dispatchId) continue;\u001b[39m\n\u001b[31m    for (const c of d.consignees) {\u001b[39m\n\u001b[31m      if (c.docket === docket) {\u001b[39m\n\u001b[31m        c.podUploaded = true;\u001b[39m\n\u001b[31m        c.status = 'delivered';\u001b[39m\n\u001b[31m      }\u001b[39m\n\u001b[31m    }\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m  saveData();·\u001b[39m\n\u001b[31m  // Push to Sheets (Apps Script saves images to Drive, stores URL in sheet)\u001b[39m\n\u001b[31m  if (window.appConfig.webhook_url) {\u001b[39m\n\u001b[31m    try {\u001b[39m\n\u001b[31m      await fetch(window.appConfig.webhook_url, {\u001b[39m\n\u001b[31m        method: 'POST', mode: 'no-cors',\u001b[39m\n\u001b[31m        headers: { 'Content-Type': 'application/json' },\u001b[39m\n\u001b[31m        body: JSON.stringify(payload)\u001b[39m\n\u001b[31m      });\u001b[39m\n\u001b[31m    } catch(e) {}\u001b[39m\n\u001b[31m  }·\u001b[39m\n\u001b[31m  closeOverlay('podOverlay');\u001b[39m\n\u001b[31m  renderDashboard();\u001b[39m\n\u001b[31m  showToast('POD uploaded ✓ — marked as delivered', 'ok');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// ─── WHATSAPP DISPATCH TABLE SHARE ──────────────────────────────────────\u001b[39m\n\u001b[31mfunction shareDispatchToWhatsApp(dispatchId) {\u001b[39m\n\u001b[31m  const d = dispatches.find(x => x.id === dispatchId);\u001b[39m\n\u001b[31m  if (!d) return;\u001b[39m\n\u001b[31m  const lines = [\u001b[39m\n\u001b[31m    `📦 *Dispatch Update — ${d.client} ${d.jobref ? '› '+d.jobref : ''}*`,\u001b[39m\n\u001b[31m    `🚚 Courier: ${d.courier} | Date: ${d.dispatchDate || d.createdAt?.slice(0,10)}`,\u001b[39m\n\u001b[31m    ``,\u001b[39m\n\u001b[31m    ...d.consignees.filter(c => c.docket).map((c,i) =>\u001b[39m\n\u001b[31m      `${i+1}. *${c.name}* — ${c.city}\\\\n   Docket: ${c.docket} | Status: ${c.status||'booked'}`\u001b[39m\n\u001b[31m    ),\u001b[39m\n\u001b[31m    ``,\u001b[39m\n\u001b[31m    `_360 Degree Logical Marketing_`\u001b[39m\n\u001b[31m  ];\u001b[39m\n\u001b[31m  const msg = encodeURIComponent(lines.join('\\\\n'));\u001b[39m\n\u001b[31m  window.open('https://wa.me/?text=' + msg, '_blank');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// Trigger n8n webhook for automated WhatsApp/email share\u001b[39m\n\u001b[31masync function triggerN8nShare(dispatchId, channel) {\u001b[39m\n\u001b[31m  const d = dispatches.find(x => x.id === dispatchId);\u001b[39m\n\u001b[31m  if (!d) return;\u001b[39m\n\u001b[31m  const n8nUrl = window.appConfig.n8n_dispatch_webhook;\u001b[39m\n\u001b[31m  if (!n8nUrl) { showToast('n8n webhook not configured in Config sheet', 'err'); return; }\u001b[39m\n\u001b[31m  try {\u001b[39m\n\u001b[31m    await fetch(n8nUrl, {\u001b[39m\n\u001b[31m      method: 'POST',\u001b[39m\n\u001b[31m      headers: { 'Content-Type': 'application/json' },\u001b[39m\n\u001b[31m      body: JSON.stringify({\u001b[39m\n\u001b[31m        channel, // 'whatsapp' or 'email'\u001b[39m\n\u001b[31m        dispatch: d,\u001b[39m\n\u001b[31m        timestamp: new Date().toISOString()\u001b[39m\n\u001b[31m      })\u001b[39m\n\u001b[31m    });\u001b[39m\n\u001b[31m    showToast(`Sent to n8n for ${channel} delivery ✓`, 'ok');\u001b[39m\n\u001b[31m  } catch(e) {\u001b[39m\n\u001b[31m    showToast('n8n webhook failed', 'err');\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction previewPOD(input, type) {\u001b[39m\n\u001b[31m  const f = input.files[0];\u001b[39m\n\u001b[31m  if (!f) return;\u001b[39m\n\u001b[31m  const r = new FileReader();\u001b[39m\n\u001b[31m  r.onload = e => {\u001b[39m\n\u001b[31m    const prev = document.getElementById('podPreview');\u001b[39m\n\u001b[31m    const existing = document.getElementById('pod_prev_' + type);\u001b[39m\n\u001b[31m    if (existing) existing.remove();\u001b[39m\n\u001b[31m    const img = document.createElement('img');\u001b[39m\n\u001b[31m    img.id = 'pod_prev_' + type;\u001b[39m\n\u001b[31m    img.src = e.target.result;\u001b[39m\n\u001b[31m    img.style.cssText = 'width:80px;height:80px;object-fit:cover;border-radius:6px;border:1px solid var(--bdr);';\u001b[39m\n\u001b[31m    prev.appendChild(img);\u001b[39m\n\u001b[31m  };\u001b[39m\n\u001b[31m  r.readAsDataURL(f);\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// ─── MANUAL TRACKING ─────────────────────────────────────────────────────\u001b[39m\n\u001b[31mfunction showManualTracking(docket, dispatchId, storeName, phone, city) {\u001b[39m\n\u001b[31m  const ov = document.getElementById('manualTrackOverlay');\u001b[39m\n\u001b[31m  ov.dataset.docket = docket;\u001b[39m\n\u001b[31m  ov.dataset.dispatchId = dispatchId;\u001b[39m\n\u001b[31m  ov.dataset.storeName = storeName;\u001b[39m\n\u001b[31m  ov.dataset.city = city;·\u001b[39m\n\u001b[31m  document.getElementById('mtDocketDisplay').textContent =\u001b[39m\n\u001b[31m    docket + '  ·  ' + storeName + '  ·  ' + city;\u001b[39m\n\u001b[31m  document.getElementById('mtContactUsed').value = phone || '';\u001b[39m\n\u001b[31m  document.getElementById('mtContactPerson').value = '';\u001b[39m\n\u001b[31m  document.getElementById('mtRemarks').value = '';\u001b[39m\n\u001b[31m  document.getElementById('mtStatus').value = '';\u001b[39m\n\u001b[31m  document.getElementById('mtCalledBy').value = '';·\u001b[39m\n\u001b[31m  // Set current datetime\u001b[39m\n\u001b[31m  const now = new Date();\u001b[39m\n\u001b[31m  const pad = n => String(n).padStart(2,'0');\u001b[39m\n\u001b[31m  document.getElementById('mtCallTime').value =\u001b[39m\n\u001b[31m    `${now.getFullYear()}-${pad(now.getMonth()+1)}-${pad(now.getDate())}T${pad(now.getHours())}:${pad(now.getMinutes())}`;·\u001b[39m\n\u001b[31m  openOverlay('manualTrackOverlay');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31masync function submitManualTracking() {\u001b[39m\n\u001b[31m  const ov = document.getElementById('manualTrackOverlay');\u001b[39m\n\u001b[31m  const docket     = ov.dataset.docket;\u001b[39m\n\u001b[31m  const dispatchId = ov.dataset.dispatchId;\u001b[39m\n\u001b[31m  const status     = document.getElementById('mtStatus').value;\u001b[39m\n\u001b[31m  const calledBy   = document.getElementById('mtCalledBy').value.trim();·\u001b[39m\n\u001b[31m  if (!status)    { showToast('Select a status', 'err'); return; }\u001b[39m\n\u001b[31m  if (!calledBy)  { showToast('Enter your name', 'err'); return; }·\u001b[39m\n\u001b[31m  const payload = {\u001b[39m\n\u001b[31m    type:          'manual_tracking',\u001b[39m\n\u001b[31m    docket,        dispatchId,\u001b[39m\n\u001b[31m    storeName:     ov.dataset.storeName,\u001b[39m\n\u001b[31m    city:          ov.dataset.city,\u001b[39m\n\u001b[31m    manualStatus:  status,\u001b[39m\n\u001b[31m    calledBy,\u001b[39m\n\u001b[31m    contactUsed:   document.getElementById('mtContactUsed').value.trim(),\u001b[39m\n\u001b[31m    contactPerson: document.getElementById('mtContactPerson').value.trim(),\u001b[39m\n\u001b[31m    remarks:       document.getElementById('mtRemarks').value.trim(),\u001b[39m\n\u001b[31m    callTime:      document.getElementById('mtCallTime').value || new Date().toISOString(),\u001b[39m\n\u001b[31m    timestamp:     new Date().toISOString()\u001b[39m\n\u001b[31m  };·\u001b[39m\n\u001b[31m  // Update local dispatch record\u001b[39m\n\u001b[31m  for (const d of dispatches) {\u001b[39m\n\u001b[31m    if (d.id !== dispatchId) continue;\u001b[39m\n\u001b[31m    for (const c of d.consignees) {\u001b[39m\n\u001b[31m      if (c.docket === docket) {\u001b[39m\n\u001b[31m        c.manualStatus    = status;\u001b[39m\n\u001b[31m        c.manualCalledBy  = calledBy;\u001b[39m\n\u001b[31m        c.manualTime      = payload.callTime;\u001b[39m\n\u001b[31m        c.manualRemarks   = payload.remarks;\u001b[39m\n\u001b[31m        // If confirmed delivered — flag so dashboard shows manual badge\u001b[39m\n\u001b[31m        // (not permanently locked — use resumeAutoTracking() to re-enable)\u001b[39m\n\u001b[31m        if (status === 'delivered') {\u001b[39m\n\u001b[31m          c.status = 'delivered';\u001b[39m\n\u001b[31m          c.manualDelivered = true;\u001b[39m\n\u001b[31m        } else {\u001b[39m\n\u001b[31m          // Non-delivered manual update: clear any previous lock\u001b[39m\n\u001b[31m          c.manualDelivered = false;\u001b[39m\n\u001b[31m        }\u001b[39m\n\u001b[31m      }\u001b[39m\n\u001b[31m    }\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m  saveData();\u001b[39m\n\u001b[31m  renderDashboard();\u001b[39m\n\u001b[31m  // Sync updated consignee to Postgres\u001b[39m\n\u001b[31m  const _d = dispatches.find(x => x.id === dispatchId);\u001b[39m\n\u001b[31m  if (_d) {\u001b[39m\n\u001b[31m    const _idx = _d.consignees.findIndex(c => c.docket === docket);\u001b[39m\n\u001b[31m    if (_idx >= 0) syncConsigneeToDB(_d.consignees[_idx], dispatchId, _idx);\u001b[39m\n\u001b[31m  }·\u001b[39m\n\u001b[31m  // Log to Sheets\u001b[39m\n\u001b[31m  if (window.appConfig.webhook_url) {\u001b[39m\n\u001b[31m    try {\u001b[39m\n\u001b[31m      await fetch(window.appConfig.webhook_url, {\u001b[39m\n\u001b[31m        method: 'POST', mode: 'no-cors',\u001b[39m\n\u001b[31m        headers: { 'Content-Type': 'application/json' },\u001b[39m\n\u001b[31m        body: JSON.stringify(payload)\u001b[39m\n\u001b[31m      });\u001b[39m\n\u001b[31m    } catch(e) {}\u001b[39m\n\u001b[31m  }·\u001b[39m\n\u001b[31m  closeOverlay('manualTrackOverlay');\u001b[39m\n\u001b[31m  showToast('Manual tracking logged ✓', 'ok');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// ─── TILE SELECTION + PARTIAL REPORT SHARE ───────────────────────────────\u001b[39m\n\u001b[31mlet _selectedDockets = new Set();·\u001b[39m\n\u001b[31mfunction toggleTileSelect(docket) {\u001b[39m\n\u001b[31m  if (_selectedDockets.has(docket)) _selectedDockets.delete(docket);\u001b[39m\n\u001b[31m  else _selectedDockets.add(docket);\u001b[39m\n\u001b[31m  // Update checkbox visual\u001b[39m\n\u001b[31m  const chk = document.getElementById('sel_' + docket.replace(/[^a-z0-9]/gi,'_'));\u001b[39m\n\u001b[31m  if (chk) chk.checked = _selectedDockets.has(docket);\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction shareSelectedReport() {\u001b[39m\n\u001b[31m  if (_selectedDockets.size === 0) {\u001b[39m\n\u001b[31m    showToast('Select at least one consignee tile first', 'err');\u001b[39m\n\u001b[31m    return;\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m  const activeDis = dispatches.filter(d => d.status !== 'draft');\u001b[39m\n\u001b[31m  const allItems  = activeDis.flatMap(d =>\u001b[39m\n\u001b[31m    (d.consignees||[]).map(c => Object.assign({}, c, {courier:d.courier, client:d.client, jobref:d.jobref, dispatchId:d.id}))\u001b[39m\n\u001b[31m  );\u001b[39m\n\u001b[31m  const selected = allItems.filter(c => _selectedDockets.has(c.docket));\u001b[39m\n\u001b[31m  if (!selected.length) { showToast('Selected dockets not found', 'err'); return; }·\u001b[39m\n\u001b[31m  const NL = '\\\\n';\u001b[39m\n\u001b[31m  const statusEmoji = {\u001b[39m\n\u001b[31m    delivered:'\\\\u2705', transit:'\\\\uD83D\\\\uDE9A', 'in-transit':'\\\\uD83D\\\\uDE9A',\u001b[39m\n\u001b[31m    booked:'\\\\uD83D\\\\uDCEC', pending:'\\\\u23F3', 'not received':'\\\\u274C', forwarded:'\\\\u27A1'\u001b[39m\n\u001b[31m  };·\u001b[39m\n\u001b[31m  const header = '\\\\uD83D\\\\uDCE6 *Dispatch Status Report*' + NL +\u001b[39m\n\u001b[31m    (selected[0].client || '') + ' ' + (selected[0].jobref ? '> ' + selected[0].jobref : '') + NL +\u001b[39m\n\u001b[31m    new Date().toLocaleDateString('en-IN', {day:'2-digit',month:'short',year:'numeric'}) + NL + NL;·\u001b[39m\n\u001b[31m  const rows = selected.map(function(c, i) {\u001b[39m\n\u001b[31m    const finalStatus = c.manualStatus === 'delivered' ? 'delivered' : (c.status || 'booked');\u001b[39m\n\u001b[31m    const emoji  = statusEmoji[finalStatus] || '\\\\uD83D\\\\uDCE6';\u001b[39m\n\u001b[31m    const source = c.manualStatus ? '(manual)' : '(auto)';\u001b[39m\n\u001b[31m    const remark = c.manualRemarks ? (NL + '   Note: ' + c.manualRemarks) : '';\u001b[39m\n\u001b[31m    return (i+1) + '. *' + c.name + '* - ' + c.city + NL +\u001b[39m\n\u001b[31m      '   ' + emoji + ' ' + finalStatus + ' ' + source + NL +\u001b[39m\n\u001b[31m      '   Docket: ' + c.docket + remark;\u001b[39m\n\u001b[31m  }).join(NL + NL);·\u001b[39m\n\u001b[31m  const footer = NL + NL + '_360 Degree Logical Marketing_';\u001b[39m\n\u001b[31m  window.open('https://wa.me/?text=' + encodeURIComponent(header + rows + footer), '_blank');\u001b[39m\n\u001b[31m  showToast('Report for ' + selected.length + ' consignees ready', 'ok');\u001b[39m\n\u001b[31m}··\u001b[39m\n\u001b[31m// ── resumeAutoTracking: undo a manual-delivered lock ──────────────────────\u001b[39m\n\u001b[31mfunction resumeAutoTracking(docket, dispatchId) {\u001b[39m\n\u001b[31m  for (const d of dispatches) {\u001b[39m\n\u001b[31m    if (d.id !== dispatchId) continue;\u001b[39m\n\u001b[31m    for (const c of d.consignees) {\u001b[39m\n\u001b[31m      if (c.docket === docket) {\u001b[39m\n\u001b[31m        c.manualDelivered = false;\u001b[39m\n\u001b[31m        c.status          = 'transit';\u001b[39m\n\u001b[31m        c.manualStatus    = '';\u001b[39m\n\u001b[31m      }\u001b[39m\n\u001b[31m    }\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m  saveData();\u001b[39m\n\u001b[31m  renderDashboard();\u001b[39m\n\u001b[31m  showToast(`Auto-tracking resumed for ${docket}`, 'ok');\u001b[39m\n\u001b[31m  const _rd = dispatches.find(x => x.id === dispatchId);\u001b[39m\n\u001b[31m  if (_rd) {\u001b[39m\n\u001b[31m    const _ri = _rd.consignees.findIndex(c => c.docket === docket);\u001b[39m\n\u001b[31m    if (_ri >= 0) syncConsigneeToDB(_rd.consignees[_ri], dispatchId, _ri);\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// ── refreshAllTracking (consolidated, try/finally for safety) ─────────────\u001b[39m\n\u001b[31masync function refreshAllTracking() {\u001b[39m\n\u001b[31m  const activeDis  = dispatches.filter(d => d.status !== 'draft');\u001b[39m\n\u001b[31m  const allCons    = activeDis.flatMap(d =>\u001b[39m\n\u001b[31m    (d.consignees || []).map(c => ({ ...c, dispatchId: d.id, courier: d.courier }))\u001b[39m\n\u001b[31m  );\u001b[39m\n\u001b[31m  const withDockets = allCons.filter(c =>\u001b[39m\n\u001b[31m    c.docket && c.status !== 'delivered' && !c.manualDelivered\u001b[39m\n\u001b[31m  );\u001b[39m\n\u001b[31m  if (!withDockets.length) { showToast('No active dockets to refresh', 'info'); return; }·\u001b[39m\n\u001b[31m  showToast(`⏳ Tracking ${withDockets.length} dockets...`, 'info');\u001b[39m\n\u001b[31m  withDockets.forEach(c => {\u001b[39m\n\u001b[31m    const id    = c.docket.replace(/[^a-z0-9]/gi, '_');\u001b[39m\n\u001b[31m    const evtEl = document.getElementById('evt_'  + id);\u001b[39m\n\u001b[31m    const spnEl = document.getElementById('spin_' + id);\u001b[39m\n\u001b[31m    if (evtEl) evtEl.style.display = 'none';\u001b[39m\n\u001b[31m    if (spnEl) spnEl.style.display = 'inline';\u001b[39m\n\u001b[31m  });·\u001b[39m\n\u001b[31m  let done = 0;\u001b[39m\n\u001b[31m  try {\u001b[39m\n\u001b[31m    for (const c of withDockets) {\u001b[39m\n\u001b[31m      const result = await fetchTracking(c.docket, c.courier || '');\u001b[39m\n\u001b[31m      const id    = c.docket.replace(/[^a-z0-9]/gi, '_');·\u001b[39m\n\u001b[31m      // Batch-write result into state (never touch DOM here)\u001b[39m\n\u001b[31m      if (result.status && result.status !== 'failed') {\u001b[39m\n\u001b[31m        for (const d of dispatches) {\u001b[39m\n\u001b[31m          if (d.id !== c.dispatchId) continue;\u001b[39m\n\u001b[31m          for (const con of d.consignees) {\u001b[39m\n\u001b[31m            if (con.docket === c.docket && !con.manualDelivered) {\u001b[39m\n\u001b[31m              con.status           = result.status;\u001b[39m\n\u001b[31m              con.lastEvent        = result.lastEvent || con.lastEvent;\u001b[39m\n\u001b[31m              con.lastScanLocation = result.lastScanLocation || '';\u001b[39m\n\u001b[31m              con.lastTrackedAt    = new Date().toISOString();\u001b[39m\n\u001b[31m            }\u001b[39m\n\u001b[31m          }\u001b[39m\n\u001b[31m        }\u001b[39m\n\u001b[31m      }·\u001b[39m\n\u001b[31m      // Update tile spinner/event text in DOM (cosmetic only)\u001b[39m\n\u001b[31m      const spnEl = document.getElementById('spin_' + id);\u001b[39m\n\u001b[31m      const evtEl = document.getElementById('evt_'  + id);\u001b[39m\n\u001b[31m      if (spnEl) spnEl.style.display = 'none';\u001b[39m\n\u001b[31m      if (evtEl) { evtEl.style.display = 'inline'; evtEl.textContent = result.lastEvent || 'Checked'; }·\u001b[39m\n\u001b[31m      if (result.status && result.status !== 'failed' && window.appConfig.webhook_url) {\u001b[39m\n\u001b[31m        logTrackingUpdate(c.docket, result.status, result.lastEvent || '', result.lastScanLocation || '');\u001b[39m\n\u001b[31m      }\u001b[39m\n\u001b[31m      done++;\u001b[39m\n\u001b[31m    }\u001b[39m\n\u001b[31m    showToast(`✓ ${done} dockets refreshed`, 'ok');\u001b[39m\n\u001b[31m  } finally {\u001b[39m\n\u001b[31m    // Always persist and re-render — even if loop throws mid-way\u001b[39m\n\u001b[31m    saveData();\u001b[39m\n\u001b[31m    renderDashboard();\u001b[39m\n\u001b[31m    // Push updated consignee states to Postgres (fire-and-forget)\u001b[39m\n\u001b[31m    dispatches.filter(d => d.status !== 'draft').forEach(d => syncDispatchToDB(d));\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m</script>\u001b[39m\n\u001b[31m</body>\u001b[39m\n\u001b[31m</html>\"\u001b[39m\n    at /var/www/360lm/tests/dispatch.spec.js:53:18",
                        "location": {
                          "file": "/var/www/360lm/tests/dispatch.spec.js",
                          "column": 18,
                          "line": 53
                        },
                        "snippet": "  51 |\n  52 |   test('hub auth gate present', () => {\n> 53 |     expect(html).toContain(\"window.location.replace('/hub/')\");\n     |                  ^\n  54 |     expect(html).toContain('_hubUser');\n  55 |   });\n  56 | });"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/dispatch.spec.js",
                            "column": 18,
                            "line": 53
                          },
                          "message": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoContain\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m) // indexOf\u001b[22m\n\nExpected substring: \u001b[32m\"window.location.replace('/hub/')\"\u001b[39m\nReceived string:    \u001b[31m\"<!DOCTYPE html>\u001b[39m\n\u001b[31m<html lang=\\\"en\\\">\u001b[39m\n\u001b[31m<head>\u001b[39m\n\u001b[31m<meta charset=\\\"UTF-8\\\">\u001b[39m\n\u001b[31m<meta name=\\\"viewport\\\" content=\\\"width=device-width,initial-scale=1,maximum-scale=1,user-scalable=no\\\">\u001b[39m\n\u001b[31m<meta name=\\\"theme-color\\\" content=\\\"#080c12\\\">\u001b[39m\n\u001b[31m<meta name=\\\"apple-mobile-web-app-capable\\\" content=\\\"yes\\\">\u001b[39m\n\u001b[31m<meta name=\\\"mobile-web-app-capable\\\" content=\\\"yes\\\">\u001b[39m\n\u001b[31m<title>360LM Dispatch</title>\u001b[39m\n\u001b[31m<link rel=\\\"manifest\\\" href=\\\"manifest.json\\\">\u001b[39m\n\u001b[31m<link href=\\\"https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@300;400;500;600;700&family=Fira+Code:wght@400;500&family=Barlow+Condensed:wght@400;600;700;800&display=swap\\\" rel=\\\"stylesheet\\\">\u001b[39m\n\u001b[31m<style>\u001b[39m\n\u001b[31m/* ════ RESET & VARS ════ */\u001b[39m\n\u001b[31m*{margin:0;padding:0;box-sizing:border-box;-webkit-tap-highlight-color:transparent;}\u001b[39m\n\u001b[31m:root{\u001b[39m\n\u001b[31m  --bg:#080c12; --bg2:#0d1420; --bg3:#111a28;\u001b[39m\n\u001b[31m  --sur:#141f30; --sur2:#1a2840; --sur3:#1f3050;\u001b[39m\n\u001b[31m  --bdr:#1e2e45; --bdr2:#253650;\u001b[39m\n\u001b[31m  --t1:#f0f4f8; --t2:#8ba4bc; --t3:#3d5470; --t4:#243348;\u001b[39m\n\u001b[31m  --acc:#f97316; --acc2:#fb923c; --acc-glow:rgba(249,115,22,0.25);\u001b[39m\n\u001b[31m  --grn:#22c55e; --grn-dim:rgba(34,197,94,0.15);\u001b[39m\n\u001b[31m  --sky:#38bdf8; --sky-dim:rgba(56,189,248,0.15);\u001b[39m\n\u001b[31m  --red:#ef4444; --red-dim:rgba(239,68,68,0.15);\u001b[39m\n\u001b[31m  --ylw:#eab308; --ylw-dim:rgba(234,179,8,0.15);\u001b[39m\n\u001b[31m  --fnt-head:'Barlow Condensed',sans-serif;\u001b[39m\n\u001b[31m  --fnt-body:'Space Grotesk',sans-serif;\u001b[39m\n\u001b[31m  --fnt-mono:'Fira Code',monospace;\u001b[39m\n\u001b[31m  --r:10px; --r-sm:6px; --r-lg:16px;\u001b[39m\n\u001b[31m  --sh:0 4px 24px rgba(0,0,0,0.4);\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31mhtml,body{height:100%;background:var(--bg);color:var(--t1);font-family:var(--fnt-body);overflow:hidden;}·\u001b[39m\n\u001b[31m/* ════ SPLASH ════ */\u001b[39m\n\u001b[31m#splash{\u001b[39m\n\u001b[31m  position:fixed;inset:0;background:var(--bg);z-index:999;\u001b[39m\n\u001b[31m  display:flex;flex-direction:column;align-items:center;justify-content:center;\u001b[39m\n\u001b[31m  transition:opacity .4s .1s;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.splash-logo{font-family:var(--fnt-head);font-size:3rem;font-weight:800;letter-spacing:2px;line-height:1;}\u001b[39m\n\u001b[31m.splash-logo em{color:var(--acc);font-style:normal;}\u001b[39m\n\u001b[31m.splash-sub{color:var(--t3);font-family:var(--fnt-mono);font-size:.7rem;letter-spacing:4px;margin-top:8px;}\u001b[39m\n\u001b[31m.splash-track{width:200px;height:2px;background:var(--bdr);border-radius:2px;margin-top:40px;overflow:hidden;}\u001b[39m\n\u001b[31m.splash-track-fill{height:100%;width:0;background:linear-gradient(90deg,var(--acc),var(--acc2));animation:splashLoad 1.3s cubic-bezier(.4,0,.2,1) forwards;}\u001b[39m\n\u001b[31m@keyframes splashLoad{to{width:100%;}}·\u001b[39m\n\u001b[31m/* ════ LAYOUT ════ */\u001b[39m\n\u001b[31m#app{height:100%;display:flex;flex-direction:column;}\u001b[39m\n\u001b[31m.hdr{\u001b[39m\n\u001b[31m  background:var(--bg2);border-bottom:1px solid var(--bdr);\u001b[39m\n\u001b[31m  padding:10px 14px;display:flex;align-items:center;gap:10px;flex-shrink:0;\u001b[39m\n\u001b[31m  position:relative;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.hdr-brand{font-family:var(--fnt-head);font-size:1.3rem;font-weight:800;letter-spacing:1px;flex:1;}\u001b[39m\n\u001b[31m.hdr-brand em{color:var(--acc);font-style:normal;}\u001b[39m\n\u001b[31m.hdr-dot{width:7px;height:7px;border-radius:50%;background:var(--grn);transition:background .3s;animation:blink 2s infinite;}\u001b[39m\n\u001b[31m@keyframes blink{0%,100%{opacity:1;}50%{opacity:.4;}}\u001b[39m\n\u001b[31m.hdr-btn{\u001b[39m\n\u001b[31m  background:none;border:none;color:var(--t2);cursor:pointer;\u001b[39m\n\u001b[31m  padding:6px 8px;border-radius:var(--r-sm);transition:all .2s;\u001b[39m\n\u001b[31m  font-family:var(--fnt-head);font-size:.85rem;font-weight:700;letter-spacing:.5px;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.hdr-btn:hover{background:var(--bdr);color:var(--t1);}\u001b[39m\n\u001b[31m.hdr-btn.primary{background:var(--acc);color:#fff;padding:6px 14px;}\u001b[39m\n\u001b[31m.hdr-btn.primary:hover{background:var(--acc2);}·\u001b[39m\n\u001b[31m/* ════ TAB BAR ════ */\u001b[39m\n\u001b[31m.tabs{\u001b[39m\n\u001b[31m  display:flex;background:var(--bg2);border-bottom:1px solid var(--bdr);\u001b[39m\n\u001b[31m  overflow-x:auto;flex-shrink:0;padding:0 6px;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.tabs::-webkit-scrollbar{display:none;}\u001b[39m\n\u001b[31m.tab{\u001b[39m\n\u001b[31m  background:none;border:none;color:var(--t3);font-family:var(--fnt-head);\u001b[39m\n\u001b[31m  font-size:.85rem;font-weight:700;letter-spacing:1px;padding:10px 14px;\u001b[39m\n\u001b[31m  cursor:pointer;border-bottom:2px solid transparent;white-space:nowrap;\u001b[39m\n\u001b[31m  transition:all .2s;flex-shrink:0;display:flex;align-items:center;gap:6px;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.tab.active{color:var(--acc);border-bottom-color:var(--acc);}\u001b[39m\n\u001b[31m.tab:hover:not(.active){color:var(--t2);}\u001b[39m\n\u001b[31m.tab-badge{\u001b[39m\n\u001b[31m  background:var(--acc);color:#fff;font-size:.6rem;font-weight:700;\u001b[39m\n\u001b[31m  padding:1px 5px;border-radius:10px;font-family:var(--fnt-mono);\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m/* ════ CONTENT AREAS ════ */\u001b[39m\n\u001b[31m.pane{flex:1;overflow-y:auto;padding:14px;display:none;}\u001b[39m\n\u001b[31m.pane.active{display:block;}\u001b[39m\n\u001b[31m.pane::-webkit-scrollbar{width:3px;}\u001b[39m\n\u001b[31m.pane::-webkit-scrollbar-thumb{background:var(--bdr2);}·\u001b[39m\n\u001b[31m/* ════ CARDS ════ */\u001b[39m\n\u001b[31m.card{\u001b[39m\n\u001b[31m  background:var(--sur);border:1px solid var(--bdr);border-radius:var(--r);\u001b[39m\n\u001b[31m  padding:14px;margin-bottom:12px;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.card.accent-left{border-left:3px solid var(--acc);}\u001b[39m\n\u001b[31m.card-title{\u001b[39m\n\u001b[31m  font-family:var(--fnt-head);font-size:.72rem;font-weight:700;\u001b[39m\n\u001b[31m  color:var(--t3);text-transform:uppercase;letter-spacing:2.5px;\u001b[39m\n\u001b[31m  margin-bottom:12px;display:flex;align-items:center;gap:8px;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.card-title .ct-dot{width:5px;height:5px;border-radius:50%;background:var(--acc);flex-shrink:0;}·\u001b[39m\n\u001b[31m/* ════ FORM ════ */\u001b[39m\n\u001b[31m.f{margin-bottom:12px;}\u001b[39m\n\u001b[31m.f label{\u001b[39m\n\u001b[31m  display:block;font-size:.68rem;color:var(--t3);margin-bottom:5px;\u001b[39m\n\u001b[31m  font-weight:600;text-transform:uppercase;letter-spacing:1.5px;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.f label .req{color:var(--acc);margin-left:2px;}\u001b[39m\n\u001b[31m.f input,.f select,.f textarea{\u001b[39m\n\u001b[31m  width:100%;background:var(--sur2);border:1px solid var(--bdr);\u001b[39m\n\u001b[31m  border-radius:var(--r-sm);color:var(--t1);font-family:var(--fnt-body);\u001b[39m\n\u001b[31m  font-size:.88rem;padding:9px 11px;outline:none;transition:border-color .2s;\u001b[39m\n\u001b[31m  -webkit-appearance:none;appearance:none;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.f input:focus,.f select:focus,.f textarea:focus{border-color:var(--acc);}\u001b[39m\n\u001b[31m.f input::placeholder{color:var(--t4);}\u001b[39m\n\u001b[31m.f textarea{resize:vertical;min-height:65px;}\u001b[39m\n\u001b[31m.f select{\u001b[39m\n\u001b[31m  background-image:url(\\\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='7'%3E%3Cpath d='M1 1l4 4 4-4' stroke='%233d5470' stroke-width='1.5' fill='none' stroke-linecap='round'/%3E%3C/svg%3E\\\");\u001b[39m\n\u001b[31m  background-repeat:no-repeat;background-position:right 10px center;padding-right:28px;cursor:pointer;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.row2{display:grid;grid-template-columns:1fr 1fr;gap:10px;}\u001b[39m\n\u001b[31m.row3{display:grid;grid-template-columns:1fr 1fr 1fr;gap:8px;}·\u001b[39m\n\u001b[31m/* ════ BUTTONS ════ */\u001b[39m\n\u001b[31m.btn{\u001b[39m\n\u001b[31m  width:100%;padding:12px;border:none;border-radius:var(--r);\u001b[39m\n\u001b[31m  font-family:var(--fnt-head);font-size:.9rem;font-weight:700;letter-spacing:.5px;\u001b[39m\n\u001b[31m  cursor:pointer;transition:all .2s;display:flex;align-items:center;justify-content:center;gap:8px;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.btn-orange{background:var(--acc);color:#fff;}\u001b[39m\n\u001b[31m.btn-orange:hover{background:var(--acc2);}\u001b[39m\n\u001b[31m.btn-ghost{background:var(--sur2);border:1px solid var(--bdr);color:var(--t1);}\u001b[39m\n\u001b[31m.btn-ghost:hover{border-color:var(--acc);color:var(--acc);}\u001b[39m\n\u001b[31m.btn-grn{background:var(--grn);color:#000;}\u001b[39m\n\u001b[31m.btn-grn:hover{opacity:.85;}\u001b[39m\n\u001b[31m.btn:active{transform:scale(.98);}\u001b[39m\n\u001b[31m.btn-row{display:grid;grid-template-columns:1fr 1fr;gap:10px;margin-top:6px;}\u001b[39m\n\u001b[31m.btn-sm{\u001b[39m\n\u001b[31m  background:none;border:1px solid var(--bdr);color:var(--t2);\u001b[39m\n\u001b[31m  font-size:.7rem;padding:5px 10px;border-radius:var(--r-sm);cursor:pointer;\u001b[39m\n\u001b[31m  font-family:var(--fnt-body);transition:all .2s;white-space:nowrap;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.btn-sm:hover{border-color:var(--acc);color:var(--acc);}\u001b[39m\n\u001b[31m.btn-sm.primary{background:var(--acc);border-color:var(--acc);color:#fff;}·\u001b[39m\n\u001b[31m/* ════ OCR UPLOAD ZONE ════ */\u001b[39m\n\u001b[31m.upload-zone{\u001b[39m\n\u001b[31m  border:2px dashed var(--bdr2);border-radius:var(--r);\u001b[39m\n\u001b[31m  padding:20px;text-align:center;cursor:pointer;transition:all .25s;\u001b[39m\n\u001b[31m  background:var(--sur2);position:relative;overflow:hidden;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.upload-zone:hover,.upload-zone.drag{border-color:var(--acc);background:rgba(249,115,22,.05);}\u001b[39m\n\u001b[31m.upload-zone .uz-icon{font-size:2rem;margin-bottom:8px;}\u001b[39m\n\u001b[31m.upload-zone .uz-title{font-family:var(--fnt-head);font-size:1rem;font-weight:700;color:var(--t1);}\u001b[39m\n\u001b[31m.upload-zone .uz-sub{font-size:.72rem;color:var(--t3);margin-top:4px;font-family:var(--fnt-mono);}\u001b[39m\n\u001b[31m.upload-zone input{display:none;}\u001b[39m\n\u001b[31m.upload-zone.share-target{border-color:var(--grn);}·\u001b[39m\n\u001b[31m/* ════ OCR RESULTS ════ */\u001b[39m\n\u001b[31m.ocr-results{display:none;}\u001b[39m\n\u001b[31m.ocr-results.visible{display:block;}\u001b[39m\n\u001b[31m.ocr-slip-list{display:flex;flex-direction:column;gap:8px;margin-top:10px;}\u001b[39m\n\u001b[31m.ocr-slip{\u001b[39m\n\u001b[31m  background:var(--sur2);border:1px solid var(--bdr);border-radius:var(--r-sm);\u001b[39m\n\u001b[31m  padding:10px 12px;display:flex;gap:10px;align-items:flex-start;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.ocr-slip img{width:60px;height:60px;object-fit:cover;border-radius:6px;flex-shrink:0;border:1px solid var(--bdr2);}\u001b[39m\n\u001b[31m.ocr-slip-body{flex:1;min-width:0;}\u001b[39m\n\u001b[31m.ocr-slip-name{font-size:.7rem;color:var(--t3);font-family:var(--fnt-mono);margin-bottom:4px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}\u001b[39m\n\u001b[31m.ocr-slip-docket{\u001b[39m\n\u001b[31m  font-family:var(--fnt-mono);font-size:.9rem;font-weight:500;color:var(--acc);\u001b[39m\n\u001b[31m  background:var(--bg);border:1px solid var(--bdr);border-radius:4px;\u001b[39m\n\u001b[31m  padding:4px 8px;width:100%;outline:none;transition:border-color .2s;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.ocr-slip-docket:focus{border-color:var(--acc);}\u001b[39m\n\u001b[31m.ocr-slip-status{font-size:.65rem;margin-top:4px;display:flex;align-items:center;gap:6px;}\u001b[39m\n\u001b[31m.ocr-confidence{padding:2px 6px;border-radius:10px;font-family:var(--fnt-mono);}\u001b[39m\n\u001b[31m.conf-high{background:var(--grn-dim);color:var(--grn);}\u001b[39m\n\u001b[31m.conf-med{background:var(--ylw-dim);color:var(--ylw);}\u001b[39m\n\u001b[31m.conf-low{background:var(--red-dim);color:var(--red);}\u001b[39m\n\u001b[31m.ocr-ai-btn{\u001b[39m\n\u001b[31m  background:rgba(56,189,248,.15);border:1px solid rgba(56,189,248,.3);\u001b[39m\n\u001b[31m  color:var(--sky);font-size:.65rem;padding:3px 8px;border-radius:10px;\u001b[39m\n\u001b[31m  cursor:pointer;font-family:var(--fnt-mono);transition:all .2s;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.ocr-ai-btn:hover{background:rgba(56,189,248,.3);}\u001b[39m\n\u001b[31m.ocr-working{\u001b[39m\n\u001b[31m  background:var(--ylw-dim);border:1px solid rgba(234,179,8,.3);\u001b[39m\n\u001b[31m  border-radius:var(--r-sm);padding:8px 12px;margin-top:8px;\u001b[39m\n\u001b[31m  font-size:.75rem;color:var(--ylw);font-family:var(--fnt-mono);\u001b[39m\n\u001b[31m  display:none;align-items:center;gap:8px;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.ocr-working.visible{display:flex;}\u001b[39m\n\u001b[31m.spin{animation:spin .8s linear infinite;display:inline-block;}\u001b[39m\n\u001b[31m@keyframes spin{to{transform:rotate(360deg);}}·\u001b[39m\n\u001b[31m/* ════ STORE LIST ════ */\u001b[39m\n\u001b[31m.store-upload-zone{\u001b[39m\n\u001b[31m  border:2px dashed var(--bdr2);border-radius:var(--r);padding:16px;\u001b[39m\n\u001b[31m  text-align:center;cursor:pointer;transition:all .25s;background:var(--sur2);\u001b[39m\n\u001b[31m  position:relative;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.store-upload-zone:hover{border-color:var(--sky);background:rgba(56,189,248,.05);}\u001b[39m\n\u001b[31m.store-upload-zone input{display:none;}\u001b[39m\n\u001b[31m.store-table-wrap{max-height:200px;overflow-y:auto;margin-top:10px;border-radius:var(--r-sm);overflow:hidden;}\u001b[39m\n\u001b[31m.store-table{width:100%;border-collapse:collapse;font-size:.75rem;}\u001b[39m\n\u001b[31m.store-table th{\u001b[39m\n\u001b[31m  background:var(--bg);color:var(--t3);font-family:var(--fnt-head);\u001b[39m\n\u001b[31m  font-size:.65rem;letter-spacing:1.5px;text-transform:uppercase;\u001b[39m\n\u001b[31m  padding:7px 8px;text-align:left;border-bottom:1px solid var(--bdr);\u001b[39m\n\u001b[31m  position:sticky;top:0;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.store-table td{\u001b[39m\n\u001b[31m  padding:6px 8px;border-bottom:1px solid var(--bdr);color:var(--t2);\u001b[39m\n\u001b[31m  font-family:var(--fnt-mono);font-size:.72rem;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.store-table tr:hover td{background:var(--sur3);}\u001b[39m\n\u001b[31m.store-table .matched{color:var(--grn);}\u001b[39m\n\u001b[31m.store-table .unmatched{color:var(--ylw);}\u001b[39m\n\u001b[31m.store-map-btn{\u001b[39m\n\u001b[31m  font-size:.65rem;background:none;border:1px solid var(--bdr);color:var(--t3);\u001b[39m\n\u001b[31m  padding:2px 6px;border-radius:4px;cursor:pointer;transition:all .2s;\u001b[39m\n\u001b[31m  font-family:var(--fnt-mono);\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.store-map-btn:hover{border-color:var(--acc);color:var(--acc);}·\u001b[39m\n\u001b[31m/* ════ CONSIGNEE CARDS ════ */\u001b[39m\n\u001b[31m.cons-list{display:flex;flex-direction:column;gap:8px;margin-bottom:10px;}\u001b[39m\n\u001b[31m.cons-card{\u001b[39m\n\u001b[31m  background:var(--sur2);border:1px solid var(--bdr);border-radius:var(--r-sm);\u001b[39m\n\u001b[31m  padding:11px;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.cons-hdr{display:flex;justify-content:space-between;align-items:center;margin-bottom:8px;}\u001b[39m\n\u001b[31m.cons-num{font-family:var(--fnt-mono);font-size:.65rem;color:var(--t3);}\u001b[39m\n\u001b[31m.cons-del{\u001b[39m\n\u001b[31m  background:none;border:none;color:var(--t4);cursor:pointer;\u001b[39m\n\u001b[31m  font-size:.85rem;padding:2px 5px;border-radius:4px;transition:all .2s;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.cons-del:hover{background:var(--red-dim);color:var(--red);}\u001b[39m\n\u001b[31m.cons-docket-linked{\u001b[39m\n\u001b[31m  display:flex;align-items:center;gap:6px;\u001b[39m\n\u001b[31m  background:rgba(249,115,22,.1);border:1px solid rgba(249,115,22,.3);\u001b[39m\n\u001b[31m  border-radius:4px;padding:4px 8px;margin-top:6px;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.cons-docket-linked .dkt{font-family:var(--fnt-mono);font-size:.82rem;color:var(--acc);}\u001b[39m\n\u001b[31m.cons-docket-linked .unlink{background:none;border:none;color:var(--acc);cursor:pointer;font-size:.75rem;}·\u001b[39m\n\u001b[31m/* ════ DISPATCH DASHBOARD ════ */\u001b[39m\n\u001b[31m.dash-stats{\u001b[39m\n\u001b[31m  display:grid;grid-template-columns:repeat(4,1fr);gap:8px;margin-bottom:14px;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.stat-box{\u001b[39m\n\u001b[31m  background:var(--sur);border:1px solid var(--bdr);border-radius:var(--r);\u001b[39m\n\u001b[31m  padding:10px 8px;text-align:center;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.stat-val{font-family:var(--fnt-head);font-size:1.4rem;font-weight:800;}\u001b[39m\n\u001b[31m.stat-lbl{font-size:.6rem;color:var(--t3);font-family:var(--fnt-mono);text-transform:uppercase;letter-spacing:1px;margin-top:2px;}\u001b[39m\n\u001b[31m.stat-box.grn .stat-val{color:var(--grn);}\u001b[39m\n\u001b[31m.stat-box.ylw .stat-val{color:var(--ylw);}\u001b[39m\n\u001b[31m.stat-box.red .stat-val{color:var(--red);}\u001b[39m\n\u001b[31m.stat-box.sky .stat-val{color:var(--sky);}·\u001b[39m\n\u001b[31m/* ════ DOCKET TILES ════ */\u001b[39m\n\u001b[31m.docket-grid{display:flex;flex-direction:column;gap:8px;}\u001b[39m\n\u001b[31m.docket-tile{\u001b[39m\n\u001b[31m  background:var(--sur);border:1px solid var(--bdr);border-radius:var(--r);\u001b[39m\n\u001b[31m  padding:12px;transition:border-color .2s;cursor:pointer;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.docket-tile:hover{border-color:var(--acc);}\u001b[39m\n\u001b[31m.docket-tile-hdr{display:flex;justify-content:space-between;align-items:flex-start;margin-bottom:8px;}\u001b[39m\n\u001b[31m.docket-tile-left{flex:1;}\u001b[39m\n\u001b[31m.docket-num{font-family:var(--fnt-mono);font-size:.95rem;font-weight:500;color:var(--sky);letter-spacing:1px;}\u001b[39m\n\u001b[31m.docket-store{font-size:.82rem;color:var(--t1);font-weight:600;margin-top:2px;}\u001b[39m\n\u001b[31m.docket-courier{font-size:.68rem;color:var(--t3);font-family:var(--fnt-mono);margin-top:2px;}\u001b[39m\n\u001b[31m.status-pill{\u001b[39m\n\u001b[31m  font-size:.62rem;font-weight:700;font-family:var(--fnt-mono);\u001b[39m\n\u001b[31m  padding:3px 8px;border-radius:10px;text-transform:uppercase;letter-spacing:.5px;\u001b[39m\n\u001b[31m  flex-shrink:0;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.sp-delivered{background:var(--grn-dim);color:var(--grn);}\u001b[39m\n\u001b[31m.sp-transit{background:var(--ylw-dim);color:var(--ylw);}\u001b[39m\n\u001b[31m.sp-booked{background:var(--sky-dim);color:var(--sky);}\u001b[39m\n\u001b[31m.sp-pending{background:var(--bdr);color:var(--t3);}\u001b[39m\n\u001b[31m.sp-failed{background:var(--red-dim);color:var(--red);}\u001b[39m\n\u001b[31m.docket-tile-footer{display:flex;align-items:center;gap:8px;flex-wrap:wrap;}\u001b[39m\n\u001b[31m.docket-last-event{font-size:.7rem;color:var(--t3);flex:1;font-family:var(--fnt-mono);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}\u001b[39m\n\u001b[31m.btn-track-live{\u001b[39m\n\u001b[31m  background:none;border:1px solid var(--acc);color:var(--acc);\u001b[39m\n\u001b[31m  font-size:.65rem;padding:4px 10px;border-radius:20px;cursor:pointer;\u001b[39m\n\u001b[31m  font-family:var(--fnt-mono);transition:all .2s;white-space:nowrap;display:flex;align-items:center;gap:4px;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.btn-track-live:hover{background:var(--acc);color:#fff;}\u001b[39m\n\u001b[31m.btn-track-live .live-dot{width:5px;height:5px;border-radius:50%;background:var(--grn);animation:blink 1.5s infinite;flex-shrink:0;}\u001b[39m\n\u001b[31m.docket-progress{height:2px;background:var(--bdr);border-radius:2px;margin-top:8px;overflow:hidden;}\u001b[39m\n\u001b[31m.docket-progress-fill{height:100%;border-radius:2px;transition:width .5s;}·\u001b[39m\n\u001b[31m/* ════ TRACKING DETAIL ════ */\u001b[39m\n\u001b[31m.track-timeline{padding:4px 0;}\u001b[39m\n\u001b[31m.track-evt{display:flex;gap:10px;padding:6px 0;position:relative;padding-left:20px;}\u001b[39m\n\u001b[31m.track-evt::before{\u001b[39m\n\u001b[31m  content:'';position:absolute;left:5px;top:11px;\u001b[39m\n\u001b[31m  width:7px;height:7px;border-radius:50%;background:var(--bdr2);border:1px solid var(--bdr2);\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.track-evt:first-child::before{background:var(--grn);border-color:var(--grn);}\u001b[39m\n\u001b[31m.track-evt::after{\u001b[39m\n\u001b[31m  content:'';position:absolute;left:8px;top:18px;\u001b[39m\n\u001b[31m  width:1px;height:calc(100% - 6px);background:var(--bdr);\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.track-evt:last-child::after{display:none;}\u001b[39m\n\u001b[31m.track-time{font-family:var(--fnt-mono);font-size:.62rem;color:var(--t3);min-width:90px;flex-shrink:0;padding-top:2px;}\u001b[39m\n\u001b[31m.track-desc{font-size:.78rem;color:var(--t2);}\u001b[39m\n\u001b[31m.track-loc{font-size:.67rem;color:var(--t3);font-family:var(--fnt-mono);margin-top:2px;}·\u001b[39m\n\u001b[31m/* ════ SHARE TOAST ════ */\u001b[39m\n\u001b[31m.share-banner{\u001b[39m\n\u001b[31m  position:fixed;top:0;left:0;right:0;z-index:200;\u001b[39m\n\u001b[31m  background:rgba(34,197,94,.15);border-bottom:1px solid rgba(34,197,94,.4);\u001b[39m\n\u001b[31m  padding:10px 16px;font-size:.78rem;color:var(--grn);font-family:var(--fnt-mono);\u001b[39m\n\u001b[31m  display:flex;align-items:center;gap:8px;\u001b[39m\n\u001b[31m  transform:translateY(-100%);transition:transform .3s;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.share-banner.show{transform:translateY(0);}·\u001b[39m\n\u001b[31m/* ════ MODAL ════ */\u001b[39m\n\u001b[31m.overlay{position:fixed;inset:0;background:rgba(0,0,0,.75);z-index:100;display:flex;align-items:flex-end;opacity:0;pointer-events:none;transition:opacity .3s;}\u001b[39m\n\u001b[31m.overlay.open{opacity:1;pointer-events:all;}\u001b[39m\n\u001b[31m.sheet{\u001b[39m\n\u001b[31m  background:var(--sur);border-radius:var(--r-lg) var(--r-lg) 0 0;\u001b[39m\n\u001b[31m  width:100%;max-height:88vh;overflow-y:auto;padding:18px;\u001b[39m\n\u001b[31m  transform:translateY(100%);transition:transform .3s cubic-bezier(.32,.72,0,1);\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.overlay.open .sheet{transform:translateY(0);}\u001b[39m\n\u001b[31m.sheet-handle{width:36px;height:4px;background:var(--bdr2);border-radius:2px;margin:0 auto 18px;}\u001b[39m\n\u001b[31m.sheet-title{font-family:var(--fnt-head);font-size:1.2rem;font-weight:800;letter-spacing:.5px;margin-bottom:14px;}\u001b[39m\n\u001b[31m.sheet::-webkit-scrollbar{width:3px;}\u001b[39m\n\u001b[31m.sheet::-webkit-scrollbar-thumb{background:var(--bdr2);}·\u001b[39m\n\u001b[31m/* ════ TOAST ════ */\u001b[39m\n\u001b[31m.toast{\u001b[39m\n\u001b[31m  position:fixed;bottom:20px;left:50%;transform:translateX(-50%) translateY(70px);\u001b[39m\n\u001b[31m  background:var(--sur2);border:1px solid var(--bdr2);color:var(--t1);\u001b[39m\n\u001b[31m  font-size:.78rem;padding:9px 16px;border-radius:30px;z-index:300;\u001b[39m\n\u001b[31m  transition:transform .3s;white-space:nowrap;font-family:var(--fnt-mono);\u001b[39m\n\u001b[31m  pointer-events:none;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.toast.show{transform:translateX(-50%) translateY(0);}\u001b[39m\n\u001b[31m.toast.ok{border-color:var(--grn);color:var(--grn);}\u001b[39m\n\u001b[31m.toast.err{border-color:var(--red);color:var(--red);}\u001b[39m\n\u001b[31m.toast.info{border-color:var(--sky);color:var(--sky);}·\u001b[39m\n\u001b[31m/* ════ COURIER MASTER ════ */\u001b[39m\n\u001b[31m.courier-master-row{\u001b[39m\n\u001b[31m  display:flex;align-items:center;gap:8px;padding:8px 0;\u001b[39m\n\u001b[31m  border-bottom:1px solid var(--bdr);\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.courier-master-row:last-child{border-bottom:none;}\u001b[39m\n\u001b[31m.cml{flex:1;}\u001b[39m\n\u001b[31m.cml-name{font-weight:600;font-size:.85rem;}\u001b[39m\n\u001b[31m.cml-url{font-family:var(--fnt-mono);font-size:.65rem;color:var(--t3);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:180px;}\u001b[39m\n\u001b[31m.courier-logo{width:24px;height:24px;border-radius:4px;object-fit:contain;background:var(--sur3);padding:2px;}·\u001b[39m\n\u001b[31m/* ════ PROGRESS BAR ════ */\u001b[39m\n\u001b[31m.ai-progress{\u001b[39m\n\u001b[31m  background:var(--sur2);border:1px solid var(--bdr);border-radius:var(--r-sm);\u001b[39m\n\u001b[31m  padding:10px 12px;margin-top:8px;display:none;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.ai-progress.visible{display:block;}\u001b[39m\n\u001b[31m.ai-prog-bar{height:3px;background:var(--bdr);border-radius:3px;margin-top:6px;overflow:hidden;}\u001b[39m\n\u001b[31m.ai-prog-fill{height:100%;background:linear-gradient(90deg,var(--sky),var(--acc));width:0;transition:width .3s;}\u001b[39m\n\u001b[31m.ai-prog-label{font-size:.68rem;color:var(--sky);font-family:var(--fnt-mono);}·\u001b[39m\n\u001b[31m/* ════ EMPTY ════ */\u001b[39m\n\u001b[31m.empty{padding:40px 20px;text-align:center;color:var(--t4);}\u001b[39m\n\u001b[31m.empty .e-icon{font-size:2.5rem;margin-bottom:10px;}\u001b[39m\n\u001b[31m.empty p{font-size:.82rem;}·\u001b[39m\n\u001b[31m/* ════ INFO TAG ════ */\u001b[39m\n\u001b[31m.info-tag{\u001b[39m\n\u001b[31m  background:var(--sur2);border:1px solid var(--bdr);border-radius:var(--r-sm);\u001b[39m\n\u001b[31m  padding:7px 10px;font-size:.75rem;color:var(--t2);margin-bottom:8px;\u001b[39m\n\u001b[31m  display:flex;align-items:center;gap:8px;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.info-tag strong{color:var(--t1);}·\u001b[39m\n\u001b[31m/* ════ JOB PILL ════ */\u001b[39m\n\u001b[31m.job-pill{\u001b[39m\n\u001b[31m  display:inline-flex;align-items:center;gap:6px;\u001b[39m\n\u001b[31m  background:rgba(249,115,22,.1);border:1px solid rgba(249,115,22,.3);\u001b[39m\n\u001b[31m  color:var(--acc);font-family:var(--fnt-mono);font-size:.68rem;\u001b[39m\n\u001b[31m  padding:3px 9px;border-radius:20px;margin-top:4px;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.job-pill button{background:none;border:none;color:var(--acc);cursor:pointer;font-size:.7rem;}·\u001b[39m\n\u001b[31m/* ════ SECTION DIVIDER ════ */\u001b[39m\n\u001b[31m.sec-div{\u001b[39m\n\u001b[31m  display:flex;align-items:center;gap:10px;margin:14px 0 10px;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.sec-div span{font-family:var(--fnt-head);font-size:.7rem;font-weight:700;color:var(--t3);text-transform:uppercase;letter-spacing:2px;white-space:nowrap;}\u001b[39m\n\u001b[31m.sec-div::before,.sec-div::after{content:'';flex:1;height:1px;background:var(--bdr);}·\u001b[39m\n\u001b[31m/* ── MAPPING UI ── */\u001b[39m\n\u001b[31m.map-table{width:100%;border-collapse:collapse;font-size:.78rem;margin:10px 0;}\u001b[39m\n\u001b[31m.map-table th{background:var(--bg);color:var(--t3);font-family:var(--fnt-head);font-size:.65rem;\u001b[39m\n\u001b[31m  letter-spacing:1.5px;text-transform:uppercase;padding:7px 10px;text-align:left;\u001b[39m\n\u001b[31m  border-bottom:1px solid var(--bdr);}\u001b[39m\n\u001b[31m.map-table td{padding:7px 10px;border-bottom:1px solid var(--bdr);vertical-align:middle;}\u001b[39m\n\u001b[31m.map-table tr:last-child td{border-bottom:none;}\u001b[39m\n\u001b[31m.map-table select{background:var(--sur2);border:1px solid var(--bdr);color:var(--t1);\u001b[39m\n\u001b[31m  font-family:var(--fnt-mono);font-size:.75rem;padding:5px 8px;border-radius:var(--r-sm);\u001b[39m\n\u001b[31m  width:100%;outline:none;}\u001b[39m\n\u001b[31m.map-table select:focus{border-color:var(--acc);}\u001b[39m\n\u001b[31m.map-req{color:var(--acc);margin-left:2px;}\u001b[39m\n\u001b[31m.map-saved-badge{font-size:.6rem;background:rgba(57,255,143,.15);color:var(--accent3,#39ff8f);\u001b[39m\n\u001b[31m  border:1px solid rgba(57,255,143,.3);padding:2px 7px;border-radius:10px;\u001b[39m\n\u001b[31m  font-family:var(--fnt-mono);margin-left:6px;}·\u001b[39m\n\u001b[31m/* ── VERIFICATION SCREEN ── */\u001b[39m\n\u001b[31m#verifyPane{display:none;position:fixed;inset:0;background:var(--bg);z-index:50;\u001b[39m\n\u001b[31m  overflow-y:auto;padding:14px;}\u001b[39m\n\u001b[31m.verify-hdr{display:flex;align-items:center;gap:10px;margin-bottom:14px;\u001b[39m\n\u001b[31m  position:sticky;top:0;background:var(--bg);padding-bottom:10px;\u001b[39m\n\u001b[31m  border-bottom:1px solid var(--bdr);z-index:1;}\u001b[39m\n\u001b[31m.verify-title{font-family:var(--fnt-head);font-size:1.1rem;font-weight:800;flex:1;}\u001b[39m\n\u001b[31m.verify-row{background:var(--sur);border:1px solid var(--bdr);border-radius:var(--r-sm);\u001b[39m\n\u001b[31m  padding:10px 12px;margin-bottom:8px;position:relative;}\u001b[39m\n\u001b[31m.verify-row.has-dupe{border-color:var(--red);background:rgba(239,68,68,.04);}\u001b[39m\n\u001b[31m.verify-row.no-docket{border-color:var(--ylw);background:rgba(234,179,8,.04);}\u001b[39m\n\u001b[31m.verify-row.ok{border-left:3px solid var(--grn);}\u001b[39m\n\u001b[31m.verify-row-hdr{display:flex;align-items:center;gap:8px;margin-bottom:8px;}\u001b[39m\n\u001b[31m.verify-num{font-family:var(--fnt-mono);font-size:.65rem;color:var(--t3);}\u001b[39m\n\u001b[31m.verify-store{font-weight:600;font-size:.9rem;flex:1;}\u001b[39m\n\u001b[31m.verify-flag{font-size:.65rem;font-weight:700;font-family:var(--fnt-mono);\u001b[39m\n\u001b[31m  padding:2px 7px;border-radius:10px;text-transform:uppercase;}\u001b[39m\n\u001b[31m.vf-dupe{background:var(--red-dim);color:var(--red);}\u001b[39m\n\u001b[31m.vf-nodocket{background:var(--ylw-dim);color:var(--ylw);}\u001b[39m\n\u001b[31m.vf-ok{background:var(--grn-dim);color:var(--grn);}\u001b[39m\n\u001b[31m.vf-short{background:rgba(56,189,248,.15);color:var(--sky);}\u001b[39m\n\u001b[31m.verify-fields{display:grid;grid-template-columns:1fr 1fr;gap:8px;margin-bottom:8px;}\u001b[39m\n\u001b[31m.verify-field label{font-size:.62rem;color:var(--t3);text-transform:uppercase;\u001b[39m\n\u001b[31m  letter-spacing:1px;display:block;margin-bottom:3px;font-family:var(--fnt-mono);}\u001b[39m\n\u001b[31m.verify-field input{width:100%;background:var(--sur2);border:1px solid var(--bdr);\u001b[39m\n\u001b[31m  color:var(--t1);font-family:var(--fnt-mono);font-size:.82rem;padding:6px 9px;\u001b[39m\n\u001b[31m  border-radius:var(--r-sm);outline:none;}\u001b[39m\n\u001b[31m.verify-field input:focus{border-color:var(--acc);}\u001b[39m\n\u001b[31m.verify-field input.changed{border-color:var(--ylw);background:rgba(234,179,8,.06);}\u001b[39m\n\u001b[31m.verify-actions{display:flex;gap:6px;align-items:center;flex-wrap:wrap;}\u001b[39m\n\u001b[31m.btn-wa{background:rgba(37,211,102,.15);border:1px solid rgba(37,211,102,.3);\u001b[39m\n\u001b[31m  color:#25d366;font-size:.65rem;padding:4px 10px;border-radius:10px;cursor:pointer;\u001b[39m\n\u001b[31m  font-family:var(--fnt-mono);transition:all .2s;}\u001b[39m\n\u001b[31m.btn-wa:hover{background:rgba(37,211,102,.3);}\u001b[39m\n\u001b[31m.verify-thumb{width:48px;height:48px;object-fit:cover;border-radius:6px;\u001b[39m\n\u001b[31m  border:1px solid var(--bdr);cursor:pointer;flex-shrink:0;}\u001b[39m\n\u001b[31m.verify-summary{background:var(--sur);border:1px solid var(--bdr);border-radius:var(--r);\u001b[39m\n\u001b[31m  padding:12px 14px;margin-bottom:14px;display:flex;gap:16px;flex-wrap:wrap;}\u001b[39m\n\u001b[31m.vs-item{text-align:center;}\u001b[39m\n\u001b[31m.vs-val{font-family:var(--fnt-head);font-size:1.3rem;font-weight:800;}\u001b[39m\n\u001b[31m.vs-lbl{font-size:.6rem;color:var(--t3);font-family:var(--fnt-mono);text-transform:uppercase;\u001b[39m\n\u001b[31m  letter-spacing:.5px;margin-top:2px;}\u001b[39m\n\u001b[31m.ocr-correction-note{font-size:.65rem;color:var(--ylw);font-family:var(--fnt-mono);\u001b[39m\n\u001b[31m  margin-top:3px;}\u001b[39m\n\u001b[31m</style>\u001b[39m\n\u001b[31m</head>\u001b[39m\n\u001b[31m<body>·\u001b[39m\n\u001b[31m<!-- SPLASH -->\u001b[39m\n\u001b[31m<div id=\\\"splash\\\">\u001b[39m\n\u001b[31m  <div class=\\\"splash-logo\\\">360LM <em>DISPATCH</em></div>\u001b[39m\n\u001b[31m  <div class=\\\"splash-sub\\\">Courier · Bus · Field Ops</div>\u001b[39m\n\u001b[31m  <div class=\\\"splash-track\\\"><div class=\\\"splash-track-fill\\\"></div></div>\u001b[39m\n\u001b[31m</div>·\u001b[39m\n\u001b[31m<!-- SHARE BANNER -->\u001b[39m\n\u001b[31m<div class=\\\"share-banner\\\" id=\\\"shareBanner\\\">\u001b[39m\n\u001b[31m  📤 Images received via Share — processing for OCR...\u001b[39m\n\u001b[31m  <button style=\\\"background:none;border:none;color:var(--grn);cursor:pointer;margin-left:auto;\\\" onclick=\\\"dismissShare()\\\">✕</button>\u001b[39m\n\u001b[31m</div>·\u001b[39m\n\u001b[31m<!-- APP -->\u001b[39m\n\u001b[31m<div id=\\\"app\\\" style=\\\"display:none;\\\">·\u001b[39m\n\u001b[31m  <div class=\\\"hdr\\\">\u001b[39m\n\u001b[31m    <div class=\\\"hdr-brand\\\">360LM <em>DISPATCH</em></div>\u001b[39m\n\u001b[31m    <span class=\\\"hdr-dot\\\" id=\\\"netDot\\\"></span>\u001b[39m\n\u001b[31m    <button class=\\\"hdr-btn\\\" onclick=\\\"window.location.href='/hub/'\\\" title=\\\"Hub\\\">🏡</button>\u001b[39m\n\u001b[31m    <button class=\\\"hdr-btn\\\" onclick=\\\"showPane('dashboard')\\\">📊</button>\u001b[39m\n\u001b[31m    <button class=\\\"hdr-btn primary\\\" onclick=\\\"newDispatch()\\\">+ NEW</button>\u001b[39m\n\u001b[31m  </div>·\u001b[39m\n\u001b[31m  <div class=\\\"tabs\\\">\u001b[39m\n\u001b[31m    <button class=\\\"tab active\\\" onclick=\\\"showPane('new')\\\" id=\\\"tab-new\\\">NEW</button>\u001b[39m\n\u001b[31m    <button class=\\\"tab\\\" onclick=\\\"showPane('dashboard')\\\" id=\\\"tab-dashboard\\\">DASHBOARD</button>\u001b[39m\n\u001b[31m    <button class=\\\"tab\\\" onclick=\\\"showPane('track')\\\" id=\\\"tab-track\\\">TRACK</button>\u001b[39m\n\u001b[31m    <button class=\\\"tab\\\" onclick=\\\"showPane('history')\\\" id=\\\"tab-history\\\">HISTORY <span class=\\\"tab-badge\\\" id=\\\"histCount\\\">0</span></button>\u001b[39m\n\u001b[31m    <button class=\\\"tab\\\" onclick=\\\"showPane('config')\\\" id=\\\"tab-config\\\">CONFIG</button>\u001b[39m\n\u001b[31m  </div>·\u001b[39m\n\u001b[31m  <!-- ════════════ NEW DISPATCH ════════════ -->\u001b[39m\n\u001b[31m  <div class=\\\"pane active\\\" id=\\\"pane-new\\\">·\u001b[39m\n\u001b[31m    <!-- 1. JOB REFERENCE -->\u001b[39m\n\u001b[31m    <div class=\\\"card accent-left\\\">\u001b[39m\n\u001b[31m      <div class=\\\"card-title\\\"><span class=\\\"ct-dot\\\"></span>Job Reference</div>\u001b[39m\n\u001b[31m      <div class=\\\"row2\\\">\u001b[39m\n\u001b[31m        <div class=\\\"f\\\">\u001b[39m\n\u001b[31m          <label>Client <span class=\\\"req\\\">*</span></label>\u001b[39m\n\u001b[31m          <select id=\\\"n_client\\\" onchange=\\\"onClientChange()\\\">\u001b[39m\n\u001b[31m            <option value=\\\"\\\">Select...</option>\u001b[39m\n\u001b[31m            <option>Lenovo</option><option>HP</option>\u001b[39m\n\u001b[31m            <option>Philips</option><option>Liebherr</option>\u001b[39m\n\u001b[31m            <option value=\\\"__c\\\">+ Custom</option>\u001b[39m\n\u001b[31m          </select>\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m        <div class=\\\"f\\\">\u001b[39m\n\u001b[31m          <label>Job Ref</label>\u001b[39m\n\u001b[31m          <input type=\\\"text\\\" id=\\\"n_jobref\\\" placeholder=\\\"JOB-042\\\" oninput=\\\"updatePill()\\\">\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      <div class=\\\"f\\\" id=\\\"customClientField\\\" style=\\\"display:none;\\\">\u001b[39m\n\u001b[31m        <label>Custom Client</label>\u001b[39m\n\u001b[31m        <input type=\\\"text\\\" id=\\\"n_customClient\\\" placeholder=\\\"Client name\\\" oninput=\\\"updatePill()\\\">\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      <div id=\\\"jobPill\\\" style=\\\"display:none;\\\" class=\\\"job-pill\\\">\u001b[39m\n\u001b[31m        🔗 <span id=\\\"jobPillTxt\\\"></span>\u001b[39m\n\u001b[31m        <button onclick=\\\"clearJob()\\\">✕</button>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      <div class=\\\"row2\\\" style=\\\"margin-top:10px;\\\">\u001b[39m\n\u001b[31m        <div class=\\\"f\\\" style=\\\"margin-bottom:0;\\\">\u001b[39m\n\u001b[31m          <label>Dispatch Date</label>\u001b[39m\n\u001b[31m          <input type=\\\"date\\\" id=\\\"n_date\\\">\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m        <div class=\\\"f\\\" style=\\\"margin-bottom:0;\\\">\u001b[39m\n\u001b[31m          <label>Courier Company <span class=\\\"req\\\">*</span></label>\u001b[39m\n\u001b[31m          <select id=\\\"n_courier\\\">\u001b[39m\n\u001b[31m            <option value=\\\"\\\">Select...</option>\u001b[39m\n\u001b[31m            <option>SkyKing</option><option>DTDC</option>\u001b[39m\n\u001b[31m            <option>Delhivery</option><option>Blue Dart</option>\u001b[39m\n\u001b[31m            <option>FedEx</option><option>Ecom Express</option>\u001b[39m\n\u001b[31m            <option>India Post</option><option value=\\\"__o\\\">Other</option>\u001b[39m\n\u001b[31m          </select>\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m    </div>·\u001b[39m\n\u001b[31m    <!-- 2. STORE LIST UPLOAD -->\u001b[39m\n\u001b[31m    <div class=\\\"card\\\">\u001b[39m\n\u001b[31m      <div class=\\\"card-title\\\"><span class=\\\"ct-dot\\\" style=\\\"background:var(--sky);\\\"></span>Store / Destination List</div>\u001b[39m\n\u001b[31m      <div class=\\\"store-upload-zone\\\" id=\\\"storeZone\\\" onclick=\\\"document.getElementById('storeFile').click();event.stopPropagation();\\\" style=\\\"cursor:pointer;\\\">\u001b[39m\n\u001b[31m        <input type=\\\"file\\\" id=\\\"storeFile\\\" accept=\\\".xlsx,.xls,.csv\\\" onchange=\\\"handleStoreFile(this)\\\" onclick=\\\"event.stopPropagation();\\\" style=\\\"display:none;\\\">\u001b[39m\n\u001b[31m        <div style=\\\"font-size:1.5rem;margin-bottom:6px;\\\">📋</div>\u001b[39m\n\u001b[31m        <div style=\\\"font-family:var(--fnt-head);font-weight:700;font-size:.9rem;\\\">Upload Store List</div>\u001b[39m\n\u001b[31m        <div style=\\\"font-size:.68rem;color:var(--t3);margin-top:3px;font-family:var(--fnt-mono);\\\">Excel / CSV · stores, cities, contacts</div>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      <div id=\\\"storeTableArea\\\" style=\\\"display:none;\\\">\u001b[39m\n\u001b[31m        <!-- Column detection result -->\u001b[39m\n\u001b[31m        <div id=\\\"storeColInfo\\\" style=\\\"margin-top:10px;display:none;\\\"></div>\u001b[39m\n\u001b[31m        <div style=\\\"display:flex;justify-content:space-between;align-items:center;margin-top:10px;margin-bottom:6px;\\\">\u001b[39m\n\u001b[31m          <span style=\\\"font-family:var(--fnt-mono);font-size:.7rem;color:var(--sky);\\\" id=\\\"storeCount\\\">0 stores loaded</span>\u001b[39m\n\u001b[31m          <button class=\\\"btn-sm\\\" onclick=\\\"clearStores()\\\">Clear</button>\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m        <div class=\\\"store-table-wrap\\\">\u001b[39m\n\u001b[31m          <table class=\\\"store-table\\\" id=\\\"storeTable\\\"></table>\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m        <!-- Inline add-as-consignees UI instead of confirm() -->\u001b[39m\n\u001b[31m        <div id=\\\"storeAddPanel\\\" style=\\\"margin-top:10px;background:var(--sur3);border:1px solid var(--bdr2);border-radius:var(--r-sm);padding:10px 12px;display:none;\\\">\u001b[39m\n\u001b[31m          <div style=\\\"font-size:.75rem;color:var(--t1);margin-bottom:8px;font-weight:600;\\\" id=\\\"storeAddTitle\\\">Add stores as consignees?</div>\u001b[39m\n\u001b[31m          <div class=\\\"row2\\\">\u001b[39m\n\u001b[31m            <button class=\\\"btn btn-ghost\\\" style=\\\"font-size:.75rem;padding:8px;\\\" onclick=\\\"document.getElementById('storeAddPanel').style.display='none'\\\">Skip for now</button>\u001b[39m\n\u001b[31m            <button class=\\\"btn btn-orange\\\" style=\\\"font-size:.75rem;padding:8px;\\\" id=\\\"storeAddBtn\\\" onclick=\\\"addStoresToConsignees()\\\">✓ Add All as Consignees</button>\u001b[39m\n\u001b[31m          </div>\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m        <button class=\\\"btn btn-ghost\\\" style=\\\"margin-top:8px;font-size:.78rem;\\\" onclick=\\\"autoMatchDockets()\\\">\u001b[39m\n\u001b[31m          🔗 Auto-match Dockets to Stores\u001b[39m\n\u001b[31m        </button>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m    </div>·\u001b[39m\n\u001b[31m    <!-- 3. DOCKET SLIPS — AI OCR -->\u001b[39m\n\u001b[31m    <div class=\\\"card\\\">\u001b[39m\n\u001b[31m      <div class=\\\"card-title\\\"><span class=\\\"ct-dot\\\" style=\\\"background:var(--grn);\\\"></span>Docket Slips — AI OCR</div>·\u001b[39m\n\u001b[31m      <!-- Share target indicator -->\u001b[39m\n\u001b[31m      <div class=\\\"info-tag\\\" id=\\\"shareTargetInfo\\\" style=\\\"display:none;\\\">\u001b[39m\n\u001b[31m        <span>📤</span><span>Images shared from WhatsApp — <strong id=\\\"sharedCount\\\">0</strong> slips received</span>\u001b[39m\n\u001b[31m      </div>·\u001b[39m\n\u001b[31m      <label class=\\\"upload-zone\\\" id=\\\"slipZone\\\" for=\\\"slipFiles\\\" style=\\\"display:block;cursor:pointer;\\\">\u001b[39m\n\u001b[31m        <input type=\\\"file\\\" id=\\\"slipFiles\\\" accept=\\\"image/*\\\" multiple onchange=\\\"handleSlipFiles(this.files)\\\" style=\\\"display:none;\\\">\u001b[39m\n\u001b[31m        <div class=\\\"uz-icon\\\">🖼️</div>\u001b[39m\n\u001b[31m        <div class=\\\"uz-title\\\">Upload Courier Slips</div>\u001b[39m\n\u001b[31m        <div class=\\\"uz-sub\\\">Multiple images · Tap or drag · Camera capture supported</div>\u001b[39m\n\u001b[31m      </label>·\u001b[39m\n\u001b[31m      <!-- OCR Working indicator -->\u001b[39m\n\u001b[31m      <div class=\\\"ocr-working\\\" id=\\\"ocrWorking\\\">\u001b[39m\n\u001b[31m        <span class=\\\"spin\\\">⟳</span>\u001b[39m\n\u001b[31m        <span id=\\\"ocrWorkingText\\\">Running OCR...</span>\u001b[39m\n\u001b[31m      </div>·\u001b[39m\n\u001b[31m      <!-- AI Progress -->\u001b[39m\n\u001b[31m      <div class=\\\"ai-progress\\\" id=\\\"aiProgress\\\">\u001b[39m\n\u001b[31m        <div class=\\\"ai-prog-label\\\" id=\\\"aiProgLabel\\\">Initializing Transformers.js...</div>\u001b[39m\n\u001b[31m        <div class=\\\"ai-prog-bar\\\"><div class=\\\"ai-prog-fill\\\" id=\\\"aiProgFill\\\"></div></div>\u001b[39m\n\u001b[31m      </div>·\u001b[39m\n\u001b[31m      <!-- OCR Results -->\u001b[39m\n\u001b[31m      <div class=\\\"ocr-results\\\" id=\\\"ocrResults\\\">\u001b[39m\n\u001b[31m        <div class=\\\"sec-div\\\"><span>Detected Dockets</span></div>\u001b[39m\n\u001b[31m        <div id=\\\"ocrSummary\\\" style=\\\"display:none;font-size:.72rem;font-family:var(--fnt-mono);\u001b[39m\n\u001b[31m          padding:4px 8px 8px;gap:8px\\\"></div>\u001b[39m\n\u001b[31m        <div class=\\\"ocr-slip-list\\\" id=\\\"ocrSlipList\\\"></div>\u001b[39m\n\u001b[31m        <div class=\\\"btn-row\\\" style=\\\"margin-top:10px;\\\">\u001b[39m\n\u001b[31m          <button class=\\\"btn btn-ghost\\\" onclick=\\\"clearSlips()\\\">Clear All</button>\u001b[39m\n\u001b[31m          <button class=\\\"btn btn-grn\\\" onclick=\\\"applyAllDockets()\\\">✓ Apply All to Consignees</button>\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m    </div>·\u001b[39m\n\u001b[31m    <!-- 4. CONSIGNEES -->\u001b[39m\n\u001b[31m    <div class=\\\"card accent-left\\\">\u001b[39m\n\u001b[31m      <div class=\\\"card-title\\\"><span class=\\\"ct-dot\\\"></span>Consignees</div>\u001b[39m\n\u001b[31m      <div class=\\\"cons-list\\\" id=\\\"consList\\\"></div>\u001b[39m\n\u001b[31m      <button class=\\\"btn btn-ghost\\\" style=\\\"font-size:.8rem;\\\" onclick=\\\"addConsignee()\\\">+ Add Consignee</button>\u001b[39m\n\u001b[31m    </div>·\u001b[39m\n\u001b[31m    <!-- 5. REMARKS -->\u001b[39m\n\u001b[31m    <div class=\\\"card\\\">\u001b[39m\n\u001b[31m      <div class=\\\"card-title\\\"><span class=\\\"ct-dot\\\" style=\\\"background:var(--t3);\\\"></span>Remarks</div>\u001b[39m\n\u001b[31m      <div class=\\\"f\\\" style=\\\"margin-bottom:0;\\\">\u001b[39m\n\u001b[31m        <textarea id=\\\"n_remarks\\\" placeholder=\\\"Instructions, notes, special handling...\\\"></textarea>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m    </div>·\u001b[39m\n\u001b[31m    <!-- ACTIONS -->\u001b[39m\n\u001b[31m    <div class=\\\"btn-row\\\">\u001b[39m\n\u001b[31m      <button class=\\\"btn btn-ghost\\\" onclick=\\\"saveDraft()\\\">💾 Draft</button>\u001b[39m\n\u001b[31m      <button class=\\\"btn btn-orange\\\" onclick=\\\"finaliseDispatch()\\\">✓ Finalise & Log</button>\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m    <div style=\\\"height:16px;\\\"></div>\u001b[39m\n\u001b[31m  </div>·\u001b[39m\n\u001b[31m  <!-- ════════════ DASHBOARD ════════════ -->\u001b[39m\n\u001b[31m  <div class=\\\"pane\\\" id=\\\"pane-dashboard\\\">\u001b[39m\n\u001b[31m    <div class=\\\"dash-stats\\\">\u001b[39m\n\u001b[31m      <div class=\\\"stat-box grn\\\"><div class=\\\"stat-val\\\" id=\\\"st-delivered\\\">0</div><div class=\\\"stat-lbl\\\">Delivered</div></div>\u001b[39m\n\u001b[31m      <div class=\\\"stat-box ylw\\\"><div class=\\\"stat-val\\\" id=\\\"st-transit\\\">0</div><div class=\\\"stat-lbl\\\">In Transit</div></div>\u001b[39m\n\u001b[31m      <div class=\\\"stat-box sky\\\"><div class=\\\"stat-val\\\" id=\\\"st-booked\\\">0</div><div class=\\\"stat-lbl\\\">Booked</div></div>\u001b[39m\n\u001b[31m      <div class=\\\"stat-box red\\\"><div class=\\\"stat-val\\\" id=\\\"st-pending\\\">0</div><div class=\\\"stat-lbl\\\">Pending</div></div>\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m    <div id=\\\"dashFilters\\\" style=\\\"display:flex;gap:6px;flex-wrap:wrap;margin-bottom:12px;\\\">\u001b[39m\n\u001b[31m      <span class=\\\"btn-sm primary\\\" onclick=\\\"filterDash('all',this)\\\" style=\\\"cursor:pointer;\\\">All</span>\u001b[39m\n\u001b[31m      <span class=\\\"btn-sm\\\" onclick=\\\"filterDash('delivered',this)\\\" style=\\\"cursor:pointer;\\\">Delivered</span>\u001b[39m\n\u001b[31m      <span class=\\\"btn-sm\\\" onclick=\\\"filterDash('transit',this)\\\" style=\\\"cursor:pointer;\\\">In Transit</span>\u001b[39m\n\u001b[31m      <span class=\\\"btn-sm\\\" onclick=\\\"filterDash('booked',this)\\\" style=\\\"cursor:pointer;\\\">Booked</span>\u001b[39m\n\u001b[31m      <span class=\\\"btn-sm\\\" onclick=\\\"filterDash('pending',this)\\\" style=\\\"cursor:pointer;\\\">Pending</span>\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m    <div class=\\\"docket-grid\\\" id=\\\"dashGrid\\\">\u001b[39m\n\u001b[31m      <div class=\\\"empty\\\"><div class=\\\"e-icon\\\">📊</div><p>No dispatches yet.<br>Create one to see the dashboard.</p></div>\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m    <div style=\\\"display:flex;gap:8px;margin-top:10px;flex-wrap:wrap;\\\">\u001b[39m\n\u001b[31m      <button class=\\\"btn btn-ghost\\\" style=\\\"flex:2;font-size:.8rem;\\\" id=\\\"btnRefreshAll\\\" onclick=\\\"refreshAllTracking()\\\">\u001b[39m\n\u001b[31m        🔄 Refresh All Tracking\u001b[39m\n\u001b[31m      </button>\u001b[39m\n\u001b[31m      <button class=\\\"btn btn-ghost\\\" style=\\\"flex:1;font-size:.8rem;border-color:var(--sky);color:var(--sky);\\\"\u001b[39m\n\u001b[31m        onclick=\\\"shareSelectedReport()\\\">\u001b[39m\n\u001b[31m        📊 Share Selected\u001b[39m\n\u001b[31m      </button>\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m  </div>·\u001b[39m\n\u001b[31m  <!-- ════════════ TRACK ════════════ -->\u001b[39m\n\u001b[31m  <div class=\\\"pane\\\" id=\\\"pane-track\\\">·\u001b[39m\n\u001b[31m    <!-- Courier selector -->\u001b[39m\n\u001b[31m    <div class=\\\"card\\\" style=\\\"margin-bottom:10px;\\\">\u001b[39m\n\u001b[31m      <div class=\\\"card-title\\\"><span class=\\\"ct-dot\\\" style=\\\"background:var(--sky);\\\"></span>Manual Tracking</div>\u001b[39m\n\u001b[31m      <div style=\\\"font-size:.72rem;color:var(--t2);margin-bottom:10px;line-height:1.5;\\\">\u001b[39m\n\u001b[31m        Track any docket number — select from already dispatched dockets or enter new ones manually.\u001b[39m\n\u001b[31m        SkyKing supports up to <strong>10 dockets at once</strong>; others are tracked one at a time.\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      <div class=\\\"f\\\">\u001b[39m\n\u001b[31m        <label>Courier Company</label>\u001b[39m\n\u001b[31m        <select id=\\\"t_courier\\\" onchange=\\\"buildTrackDocketChips()\\\">\u001b[39m\n\u001b[31m          <option>SkyKing</option><option>DTDC</option><option>Delhivery</option>\u001b[39m\n\u001b[31m          <option>Blue Dart</option><option>FedEx</option><option>Ecom Express</option><option>India Post</option>\u001b[39m\n\u001b[31m        </select>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m    </div>·\u001b[39m\n\u001b[31m    <!-- Docket selector — from existing dispatches -->\u001b[39m\n\u001b[31m    <div class=\\\"card\\\" style=\\\"margin-bottom:10px;\\\">\u001b[39m\n\u001b[31m      <div class=\\\"card-title\\\"><span class=\\\"ct-dot\\\" style=\\\"background:var(--acc);\\\"></span>\u001b[39m\n\u001b[31m        Select from Dispatched Dockets\u001b[39m\n\u001b[31m        <span id=\\\"trackCourierLimit\\\" style=\\\"margin-left:auto;font-size:.65rem;color:var(--t3);font-weight:400;text-transform:none;letter-spacing:0;font-family:var(--fnt-mono);\\\"></span>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      <div id=\\\"trackDocketChips\\\" style=\\\"display:flex;flex-wrap:wrap;gap:6px;margin-bottom:10px;min-height:28px;\\\">\u001b[39m\n\u001b[31m        <span style=\\\"font-size:.72rem;color:var(--t3);font-family:var(--fnt-mono);\\\">No dispatched dockets yet</span>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      <div style=\\\"font-size:.68rem;color:var(--t3);font-family:var(--fnt-mono);margin-bottom:8px;\\\">\u001b[39m\n\u001b[31m        Tap a docket to add/remove from tracking list. Selected shown in orange.\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m    </div>·\u001b[39m\n\u001b[31m    <!-- Manual entry for new dockets -->\u001b[39m\n\u001b[31m    <div class=\\\"card\\\" style=\\\"margin-bottom:10px;\\\">\u001b[39m\n\u001b[31m      <div class=\\\"card-title\\\"><span class=\\\"ct-dot\\\" style=\\\"background:var(--grn);\\\"></span>Add New Docket Numbers</div>\u001b[39m\n\u001b[31m      <div class=\\\"f\\\" style=\\\"margin-bottom:6px;\\\">\u001b[39m\n\u001b[31m        <label>Enter docket numbers (comma or newline separated)</label>\u001b[39m\n\u001b[31m        <textarea id=\\\"t_manual_dockets\\\" placeholder=\\\"443005527&#10;443005528&#10;or: 443005527, 443005528\\\"\u001b[39m\n\u001b[31m          style=\\\"min-height:56px;font-family:var(--fnt-mono);font-size:.82rem;letter-spacing:.5px;\\\"></textarea>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      <button class=\\\"btn-sm primary\\\" onclick=\\\"addManualDockets()\\\" style=\\\"font-size:.72rem;\\\">+ Add to List</button>\u001b[39m\n\u001b[31m    </div>·\u001b[39m\n\u001b[31m    <!-- Selected dockets summary + track button -->\u001b[39m\n\u001b[31m    <div id=\\\"trackSelectedSummary\\\" style=\\\"display:none;\\\" class=\\\"card\\\" style=\\\"margin-bottom:10px;\\\">\u001b[39m\n\u001b[31m      <div style=\\\"display:flex;align-items:center;justify-content:space-between;margin-bottom:8px;\\\">\u001b[39m\n\u001b[31m        <div>\u001b[39m\n\u001b[31m          <span style=\\\"font-family:var(--fnt-mono);font-size:.72rem;color:var(--sky);\\\" id=\\\"trackSelectedCount\\\">0 selected</span>\u001b[39m\n\u001b[31m          <span style=\\\"font-family:var(--fnt-mono);font-size:.65rem;color:var(--t3);margin-left:8px;\\\" id=\\\"trackSelectedList\\\"></span>\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m        <button class=\\\"btn-sm\\\" onclick=\\\"clearTrackSelection()\\\">Clear</button>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      <div class=\\\"btn-row\\\">\u001b[39m\n\u001b[31m        <button class=\\\"btn btn-ghost\\\" onclick=\\\"openTrackOnCourierSite()\\\">🔗 Open Courier Site</button>\u001b[39m\n\u001b[31m        <button class=\\\"btn btn-orange\\\" onclick=\\\"trackSelected()\\\">🔍 Track All via Proxy</button>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m    </div>·\u001b[39m\n\u001b[31m    <div id=\\\"trackResultsArea\\\"></div>\u001b[39m\n\u001b[31m  </div>·\u001b[39m\n\u001b[31m  <!-- ════════════ HISTORY ════════════ -->\u001b[39m\n\u001b[31m  <div class=\\\"pane\\\" id=\\\"pane-history\\\">\u001b[39m\n\u001b[31m    <div id=\\\"historyList\\\">\u001b[39m\n\u001b[31m      <div class=\\\"empty\\\"><div class=\\\"e-icon\\\">📭</div><p>No dispatch history yet.</p></div>\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m  </div>·\u001b[39m\n\u001b[31m  <!-- ════════════ CONFIG ════════════ -->\u001b[39m\n\u001b[31m  <div class=\\\"pane\\\" id=\\\"pane-config\\\">·\u001b[39m\n\u001b[31m    <!-- SHEETS URL — the only thing you type manually in the PWA -->\u001b[39m\n\u001b[31m    <div class=\\\"card\\\" style=\\\"border-color:var(--acc);border-width:2px;\\\">\u001b[39m\n\u001b[31m      <div class=\\\"card-title\\\"><span class=\\\"ct-dot\\\"></span>Google Sheets — Master Config</div>\u001b[39m\n\u001b[31m      <div class=\\\"info-tag\\\" style=\\\"background:rgba(34,197,94,.07);border-color:rgba(34,197,94,.3);\\\">\u001b[39m\n\u001b[31m        <span>✅</span>\u001b[39m\n\u001b[31m        <span>URL is loaded automatically from <strong>config.json</strong> on the VPS — no manual entry needed. All other settings sync from your <strong>Google Sheet</strong>.</span>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      <div class=\\\"f\\\">\u001b[39m\n\u001b[31m        <label>Apps Script URL <span style=\\\"font-size:.65rem;color:var(--t3);font-weight:400;\\\">(auto-loaded from config.json)</span></label>\u001b[39m\n\u001b[31m        <input type=\\\"url\\\" id=\\\"cfg_sheets_url\\\"\u001b[39m\n\u001b[31m          placeholder=\\\"Auto-loaded from server...\\\"\u001b[39m\n\u001b[31m          oninput=\\\"onSheetsUrlChange()\\\" readonly\u001b[39m\n\u001b[31m          style=\\\"opacity:.7;cursor:default;\\\">\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      <div id=\\\"syncStatus\\\" class=\\\"info-tag\\\" style=\\\"display:none;\\\">\u001b[39m\n\u001b[31m        <span id=\\\"syncIcon\\\">⏳</span>\u001b[39m\n\u001b[31m        <span id=\\\"syncMsg\\\">Syncing from sheet...</span>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      <div class=\\\"btn-row\\\">\u001b[39m\n\u001b[31m        <button class=\\\"btn btn-ghost\\\" onclick=\\\"syncFromSheet()\\\">🔄 Sync Now</button>\u001b[39m\n\u001b[31m        <button class=\\\"btn btn-orange\\\" onclick=\\\"saveSheetsUrl()\\\">💾 Save & Sync</button>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m    </div>·\u001b[39m\n\u001b[31m    <!-- READ-ONLY display of what was loaded from sheet -->\u001b[39m\n\u001b[31m    <div class=\\\"card\\\" id=\\\"sheetConfigDisplay\\\" style=\\\"display:none;\\\">\u001b[39m\n\u001b[31m      <div class=\\\"card-title\\\"><span class=\\\"ct-dot\\\" style=\\\"background:var(--grn);\\\"></span>Config Loaded from Sheet\u001b[39m\n\u001b[31m        <span id=\\\"sheetSyncTime\\\" style=\\\"margin-left:auto;font-size:.62rem;color:var(--t3);font-family:var(--fnt-mono);font-weight:400;text-transform:none;letter-spacing:0;\\\"></span>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      <div id=\\\"sheetConfigRows\\\"></div>\u001b[39m\n\u001b[31m      <div class=\\\"info-tag\\\" style=\\\"margin-top:8px;\\\">\u001b[39m\n\u001b[31m        <span>✏️</span><span>To change any value, edit the <strong>Config sheet</strong> in Google Sheets, then tap Sync Now above.</span>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m    </div>·\u001b[39m\n\u001b[31m    <!-- Courier Master (from sheet) -->\u001b[39m\n\u001b[31m    <div class=\\\"card\\\">\u001b[39m\n\u001b[31m      <div class=\\\"card-title\\\"><span class=\\\"ct-dot\\\" style=\\\"background:var(--ylw);\\\"></span>Courier Master</div>\u001b[39m\n\u001b[31m      <div id=\\\"courierMasterList\\\"></div>\u001b[39m\n\u001b[31m      <button class=\\\"btn btn-ghost\\\" style=\\\"margin-top:8px;font-size:.78rem;\\\" onclick=\\\"openCourierMaster()\\\">✏️ Edit in Sheet / Override</button>\u001b[39m\n\u001b[31m    </div>·\u001b[39m\n\u001b[31m    <button class=\\\"btn\\\" style=\\\"background:var(--red-dim);color:var(--red);border:1px solid var(--red);margin-top:4px;\\\" onclick=\\\"clearAll()\\\">🗑️ Clear All Local Data</button>\u001b[39m\n\u001b[31m    <div style=\\\"height:20px;\\\"></div>\u001b[39m\n\u001b[31m  </div>··\u001b[39m\n\u001b[31m<!-- ════ ADDRESS REVIEW PANE ════ -->\u001b[39m\n\u001b[31m<div id=\\\"addrReviewPane\\\" style=\\\"display:none;position:fixed;inset:0;background:var(--bg);z-index:50;overflow-y:auto;padding:14px;\\\">\u001b[39m\n\u001b[31m  <div class=\\\"verify-hdr\\\">\u001b[39m\n\u001b[31m    <button class=\\\"btn-icon\\\" onclick=\\\"document.getElementById('addrReviewPane').style.display='none'\\\" style=\\\"font-size:1.1rem;\\\">←</button>\u001b[39m\n\u001b[31m    <div class=\\\"verify-title\\\">Review AI-Parsed Addresses</div>\u001b[39m\n\u001b[31m  </div>\u001b[39m\n\u001b[31m  <div style=\\\"background:rgba(56,189,248,.08);border:1px solid rgba(56,189,248,.3);border-radius:var(--r-sm);\u001b[39m\n\u001b[31m    padding:8px 12px;margin-bottom:12px;font-size:.72rem;color:var(--sky);\\\">\u001b[39m\n\u001b[31m    ✏️ Review each row — correct name and city if wrong. Raw address shown below each row for reference.\u001b[39m\n\u001b[31m    Corrections are saved and improve future AI parsing automatically.\u001b[39m\n\u001b[31m  </div>\u001b[39m\n\u001b[31m  <div class=\\\"verify-summary\\\" id=\\\"addrReviewSummary\\\"></div>\u001b[39m\n\u001b[31m  <div id=\\\"addrReviewList\\\"></div>\u001b[39m\n\u001b[31m  <div style=\\\"position:sticky;bottom:0;background:var(--bg);padding-top:10px;\u001b[39m\n\u001b[31m    border-top:1px solid var(--bdr);margin-top:8px;\\\">\u001b[39m\n\u001b[31m    <div style=\\\"display:flex;gap:8px;\\\">\u001b[39m\n\u001b[31m      <button class=\\\"btn btn-ghost\\\" onclick=\\\"backToMapping()\\\" style=\\\"flex:1;\\\">← Back to Mapping</button>\u001b[39m\n\u001b[31m      <button class=\\\"btn btn-orange\\\" onclick=\\\"confirmAddressReview()\\\" style=\\\"flex:2;\\\">✓ Confirm &amp; Use These Addresses</button>\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m    <div style=\\\"font-size:.65rem;color:var(--t3);text-align:center;margin-top:5px;font-family:var(--fnt-mono);\\\">\u001b[39m\n\u001b[31m      Edits you made above are preserved when you go back\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m  </div>\u001b[39m\n\u001b[31m</div>·\u001b[39m\n\u001b[31m<!-- ════ VERIFICATION SCREEN ════ -->\u001b[39m\n\u001b[31m<div id=\\\"verifyPane\\\">\u001b[39m\n\u001b[31m  <div class=\\\"verify-hdr\\\">\u001b[39m\n\u001b[31m    <button class=\\\"btn-icon\\\" onclick=\\\"closeVerify()\\\" style=\\\"font-size:1.1rem;\\\">←</button>\u001b[39m\n\u001b[31m    <div class=\\\"verify-title\\\">Verify Before Finalise</div>\u001b[39m\n\u001b[31m    <button class=\\\"btn-sm\\\" onclick=\\\"approveAll()\\\" style=\\\"white-space:nowrap;\\\">✓ Approve All</button>\u001b[39m\n\u001b[31m  </div>\u001b[39m\n\u001b[31m  <div class=\\\"verify-summary\\\" id=\\\"verifySummary\\\"></div>\u001b[39m\n\u001b[31m  <div id=\\\"verifyList\\\"></div>\u001b[39m\n\u001b[31m  <div style=\\\"position:sticky;bottom:0;background:var(--bg);padding-top:10px;\u001b[39m\n\u001b[31m    border-top:1px solid var(--bdr);margin-top:8px;\\\">\u001b[39m\n\u001b[31m    <div style=\\\"display:flex;gap:8px;flex-wrap:wrap;\\\">\u001b[39m\n\u001b[31m      <button class=\\\"btn btn-ghost\\\" onclick=\\\"closeVerify()\\\" style=\\\"flex:1;\\\">← Back to Edit</button>\u001b[39m\n\u001b[31m      <button class=\\\"btn btn-ghost\\\" onclick=\\\"exportCleanSheet()\\\" style=\\\"flex:1;border-color:var(--sky);color:var(--sky);\\\">\u001b[39m\n\u001b[31m        📥 Export Clean Sheet\u001b[39m\n\u001b[31m      </button>\u001b[39m\n\u001b[31m      <button class=\\\"btn btn-orange\\\" id=\\\"btnConfirmFinalise\\\" onclick=\\\"confirmAndFinalise()\\\" style=\\\"flex:2;\\\">\u001b[39m\n\u001b[31m        ✓ Finalise Dispatch\u001b[39m\n\u001b[31m      </button>\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m  </div>\u001b[39m\n\u001b[31m</div>·\u001b[39m\n\u001b[31m</div><!-- /app -->·\u001b[39m\n\u001b[31m<!-- ════ DISPATCH DETAIL SHEET ════ -->\u001b[39m\n\u001b[31m<div class=\\\"overlay\\\" id=\\\"detailOverlay\\\" onclick=\\\"closeOverlay('detailOverlay')\\\">\u001b[39m\n\u001b[31m  <div class=\\\"sheet\\\" onclick=\\\"event.stopPropagation()\\\">\u001b[39m\n\u001b[31m    <div class=\\\"sheet-handle\\\"></div>\u001b[39m\n\u001b[31m    <div id=\\\"detailContent\\\"></div>\u001b[39m\n\u001b[31m  </div>\u001b[39m\n\u001b[31m</div>·\u001b[39m\n\u001b[31m<!-- ════ COURIER MASTER EDITOR ════ -->\u001b[39m\n\u001b[31m<div class=\\\"overlay\\\" id=\\\"courierMasterOverlay\\\" onclick=\\\"closeOverlay('courierMasterOverlay')\\\">\u001b[39m\n\u001b[31m  <div class=\\\"sheet\\\" onclick=\\\"event.stopPropagation()\\\">\u001b[39m\n\u001b[31m    <div class=\\\"sheet-handle\\\"></div>\u001b[39m\n\u001b[31m    <div class=\\\"sheet-title\\\">✏️ Courier Master</div>\u001b[39m\n\u001b[31m    <div id=\\\"courierMasterEditor\\\"></div>\u001b[39m\n\u001b[31m  </div>\u001b[39m\n\u001b[31m</div>·\u001b[39m\n\u001b[31m<!-- ════ MANUAL TRACKING OVERLAY ════ -->\u001b[39m\n\u001b[31m<div class=\\\"overlay\\\" id=\\\"manualTrackOverlay\\\" onclick=\\\"closeOverlay('manualTrackOverlay')\\\" data-docket=\\\"\\\" data-dispatch-id=\\\"\\\">\u001b[39m\n\u001b[31m  <div class=\\\"sheet\\\" onclick=\\\"event.stopPropagation()\\\">\u001b[39m\n\u001b[31m    <div class=\\\"sheet-handle\\\"></div>\u001b[39m\n\u001b[31m    <div class=\\\"sheet-title\\\">📞 Manual Tracking Log</div>\u001b[39m\n\u001b[31m    <div style=\\\"background:var(--sur2);border:1px solid var(--bdr);border-radius:var(--r-sm);\u001b[39m\n\u001b[31m      padding:8px 12px;margin-bottom:12px;font-family:var(--fnt-mono);font-size:.82rem;color:var(--acc);\\\"\u001b[39m\n\u001b[31m      id=\\\"mtDocketDisplay\\\"></div>\u001b[39m\n\u001b[31m    <div class=\\\"row2\\\">\u001b[39m\n\u001b[31m      <div class=\\\"f\\\">\u001b[39m\n\u001b[31m        <label>Status Reported <span class=\\\"req\\\">*</span></label>\u001b[39m\n\u001b[31m        <select id=\\\"mtStatus\\\">\u001b[39m\n\u001b[31m          <option value=\\\"\\\">Select...</option>\u001b[39m\n\u001b[31m          <option value=\\\"delivered\\\">✅ Delivered</option>\u001b[39m\n\u001b[31m          <option value=\\\"not received\\\">❌ Not Received</option>\u001b[39m\n\u001b[31m          <option value=\\\"in transit\\\">🚚 In Transit / On the way</option>\u001b[39m\n\u001b[31m          <option value=\\\"will deliver tomorrow\\\">📅 Will Deliver Tomorrow</option>\u001b[39m\n\u001b[31m          <option value=\\\"refused\\\">🚫 Refused / Returned</option>\u001b[39m\n\u001b[31m          <option value=\\\"partial\\\">📦 Partial Delivery</option>\u001b[39m\n\u001b[31m        </select>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      <div class=\\\"f\\\">\u001b[39m\n\u001b[31m        <label>Called By (Staff) <span class=\\\"req\\\">*</span></label>\u001b[39m\n\u001b[31m        <input type=\\\"text\\\" id=\\\"mtCalledBy\\\" placeholder=\\\"Your name\\\" list=\\\"staffList\\\">\u001b[39m\n\u001b[31m        <datalist id=\\\"staffList\\\">\u001b[39m\n\u001b[31m          <option>Mukesh</option><option>Sachin</option><option>Shambhu</option>\u001b[39m\n\u001b[31m          <option>Umashankar</option><option>Narender</option><option>Rakesh</option>\u001b[39m\n\u001b[31m          <option>Gurpreet</option><option>Ashok</option>\u001b[39m\n\u001b[31m        </datalist>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m    <div class=\\\"row2\\\">\u001b[39m\n\u001b[31m      <div class=\\\"f\\\">\u001b[39m\n\u001b[31m        <label>Contact Number Used</label>\u001b[39m\n\u001b[31m        <input type=\\\"tel\\\" id=\\\"mtContactUsed\\\" placeholder=\\\"Mobile number called\\\">\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      <div class=\\\"f\\\">\u001b[39m\n\u001b[31m        <label>Person Who Confirmed</label>\u001b[39m\n\u001b[31m        <input type=\\\"text\\\" id=\\\"mtContactPerson\\\" placeholder=\\\"Name of person at store\\\">\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m    <div class=\\\"f\\\">\u001b[39m\n\u001b[31m      <label>Remarks</label>\u001b[39m\n\u001b[31m      <textarea id=\\\"mtRemarks\\\" placeholder=\\\"Any notes, delivery details, issues...\\\" style=\\\"min-height:60px;\\\"></textarea>\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m    <div class=\\\"f\\\">\u001b[39m\n\u001b[31m      <label>Call Date &amp; Time</label>\u001b[39m\n\u001b[31m      <input type=\\\"datetime-local\\\" id=\\\"mtCallTime\\\">\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m    <div class=\\\"btn-row\\\">\u001b[39m\n\u001b[31m      <button class=\\\"btn btn-ghost\\\" onclick=\\\"closeOverlay('manualTrackOverlay')\\\">Cancel</button>\u001b[39m\n\u001b[31m      <button class=\\\"btn btn-orange\\\" onclick=\\\"submitManualTracking()\\\">💾 Log &amp; Save</button>\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m  </div>\u001b[39m\n\u001b[31m</div>·\u001b[39m\n\u001b[31m<!-- ════ POD UPLOAD OVERLAY ════ -->\u001b[39m\n\u001b[31m<div class=\\\"overlay\\\" id=\\\"podOverlay\\\" onclick=\\\"closeOverlay('podOverlay')\\\" data-docket=\\\"\\\" data-dispatch-id=\\\"\\\">\u001b[39m\n\u001b[31m  <div class=\\\"sheet\\\" onclick=\\\"event.stopPropagation()\\\">\u001b[39m\n\u001b[31m    <div class=\\\"sheet-handle\\\"></div>\u001b[39m\n\u001b[31m    <div class=\\\"sheet-title\\\">📸 Upload Proof of Delivery</div>\u001b[39m\n\u001b[31m    <div class=\\\"f\\\">\u001b[39m\n\u001b[31m      <label>POD / Delivery Slip Photo</label>\u001b[39m\n\u001b[31m      <input type=\\\"file\\\" id=\\\"podFileInput\\\" accept=\\\"image/*\\\" capture=\\\"environment\\\"\u001b[39m\n\u001b[31m        onchange=\\\"previewPOD(this,'pod')\\\" style=\\\"font-size:.8rem;color:var(--t2);\\\">\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m    <div class=\\\"f\\\">\u001b[39m\n\u001b[31m      <label>Recipient / Receiver Photo (optional)</label>\u001b[39m\n\u001b[31m      <input type=\\\"file\\\" id=\\\"recipientFileInput\\\" accept=\\\"image/*\\\" capture=\\\"environment\\\"\u001b[39m\n\u001b[31m        onchange=\\\"previewPOD(this,'recip')\\\" style=\\\"font-size:.8rem;color:var(--t2);\\\">\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m    <div id=\\\"podPreview\\\" style=\\\"display:flex;gap:8px;flex-wrap:wrap;margin-bottom:12px;\\\"></div>\u001b[39m\n\u001b[31m    <div class=\\\"btn-row\\\">\u001b[39m\n\u001b[31m      <button class=\\\"btn btn-ghost\\\" onclick=\\\"closeOverlay('podOverlay')\\\">Cancel</button>\u001b[39m\n\u001b[31m      <button class=\\\"btn btn-orange\\\" onclick=\\\"uploadPODFiles()\\\">📤 Upload & Mark Delivered</button>\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m  </div>\u001b[39m\n\u001b[31m</div>·\u001b[39m\n\u001b[31m<!-- ════ ADMIN LOGIN OVERLAY ════ -->\u001b[39m\n\u001b[31m<div class=\\\"overlay\\\" id=\\\"adminLoginOverlay\\\" onclick=\\\"closeOverlay('adminLoginOverlay')\\\">\u001b[39m\n\u001b[31m  <div class=\\\"sheet\\\" onclick=\\\"event.stopPropagation()\\\" style=\\\"max-height:320px;\\\">\u001b[39m\n\u001b[31m    <div class=\\\"sheet-handle\\\"></div>\u001b[39m\n\u001b[31m    <div class=\\\"sheet-title\\\">🔐 Admin Access</div>\u001b[39m\n\u001b[31m    <div style=\\\"font-size:.78rem;color:var(--t2);margin-bottom:14px;\\\">Config settings require admin password.</div>\u001b[39m\n\u001b[31m    <div class=\\\"f\\\">\u001b[39m\n\u001b[31m      <label>Password</label>\u001b[39m\n\u001b[31m      <input type=\\\"password\\\" id=\\\"adminPwdInput\\\" placeholder=\\\"Enter admin password\\\"\u001b[39m\n\u001b[31m        onkeydown=\\\"if(event.key==='Enter')submitAdminLogin()\\\">\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m    <div class=\\\"btn-row\\\" style=\\\"margin-top:4px;\\\">\u001b[39m\n\u001b[31m      <button class=\\\"btn btn-ghost\\\" onclick=\\\"closeOverlay('adminLoginOverlay')\\\">Cancel</button>\u001b[39m\n\u001b[31m      <button class=\\\"btn btn-orange\\\" onclick=\\\"submitAdminLogin()\\\">🔓 Login</button>\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m  </div>\u001b[39m\n\u001b[31m</div>·\u001b[39m\n\u001b[31m<!-- ════ TOAST ════ -->\u001b[39m\n\u001b[31m<div class=\\\"toast\\\" id=\\\"toast\\\"></div>·\u001b[39m\n\u001b[31m<!-- ════ SCRIPTS ════ -->\u001b[39m\n\u001b[31m<!-- SheetJS for Excel/CSV parsing -->\u001b[39m\n\u001b[31m<script src=\\\"https://cdnjs.cloudflare.com/ajax/libs/xlsx/0.18.5/xlsx.full.min.js\\\"></script>·\u001b[39m\n\u001b[31m<script type=\\\"module\\\">\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31m// OCR — Transformers.js\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31mimport { pipeline, env } from 'https://cdn.jsdelivr.net/npm/@huggingface/transformers@3.5.2/dist/transformers.min.js';·\u001b[39m\n\u001b[31m// Cache models in browser\u001b[39m\n\u001b[31menv.allowLocalModels = false;\u001b[39m\n\u001b[31menv.useBrowserCache = true;·\u001b[39m\n\u001b[31mlet ocrPipeline = null;\u001b[39m\n\u001b[31mlet ocrLoading = false;·\u001b[39m\n\u001b[31mwindow.initOCR = async function() {\u001b[39m\n\u001b[31m  if (ocrPipeline || ocrLoading) return;\u001b[39m\n\u001b[31m  ocrLoading = true;\u001b[39m\n\u001b[31m  showAIProgress('Loading OCR model (first time ~120MB)...', 5);\u001b[39m\n\u001b[31m  try {\u001b[39m\n\u001b[31m    ocrPipeline = await pipeline('image-to-text', 'Xenova/trocr-base-printed', {\u001b[39m\n\u001b[31m      progress_callback: (p) => {\u001b[39m\n\u001b[31m        if (p.progress) {\u001b[39m\n\u001b[31m          showAIProgress(`Loading: ${p.file || 'model'} (${Math.round(p.progress)}%)`, p.progress);\u001b[39m\n\u001b[31m        }\u001b[39m\n\u001b[31m      }\u001b[39m\n\u001b[31m    });\u001b[39m\n\u001b[31m    showAIProgress('OCR model ready ✓', 100);\u001b[39m\n\u001b[31m    setTimeout(() => hideAIProgress(), 1500);\u001b[39m\n\u001b[31m    showToast('OCR ready ✓', 'ok');\u001b[39m\n\u001b[31m  } catch(e) {\u001b[39m\n\u001b[31m    console.warn('Transformers.js OCR failed:', e);\u001b[39m\n\u001b[31m    hideAIProgress();\u001b[39m\n\u001b[31m    showToast('OCR model load failed — will use Ollama fallback', 'info');\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m  ocrLoading = false;\u001b[39m\n\u001b[31m};·\u001b[39m\n\u001b[31mwindow.runOCROnImage = async function(imageData, filename) {\u001b[39m\n\u001b[31m  const cfg = window.appConfig;·\u001b[39m\n\u001b[31m  // Strategy 1: Transformers.js\u001b[39m\n\u001b[31m  if ((cfg.ocr_mode === 'transformers' || cfg.ocr_mode === 'both') && ocrPipeline) {\u001b[39m\n\u001b[31m    try {\u001b[39m\n\u001b[31m      const result = await ocrPipeline(imageData);\u001b[39m\n\u001b[31m      const text = result[0]?.generated_text || '';\u001b[39m\n\u001b[31m      return extractDocketFromText(text, 'transformers');\u001b[39m\n\u001b[31m    } catch(e) { /* fall through */ }\u001b[39m\n\u001b[31m  }·\u001b[39m\n\u001b[31m  // Strategy 2: Dispatch AI proxy → Ollama LLaVA (server-side, avoids CORS + port issues)\u001b[39m\n\u001b[31m  if (cfg.ocr_mode === 'ollama' || cfg.ocr_mode === 'both') {\u001b[39m\n\u001b[31m    try {\u001b[39m\n\u001b[31m      const resp = await fetch('/dispatch-ai/ocr', {\u001b[39m\n\u001b[31m        method: 'POST',\u001b[39m\n\u001b[31m        headers: { 'Content-Type': 'application/json' },\u001b[39m\n\u001b[31m        body: JSON.stringify({ imageBase64: imageData }),\u001b[39m\n\u001b[31m        signal: AbortSignal.timeout(180000)\u001b[39m\n\u001b[31m      });\u001b[39m\n\u001b[31m      const data = await resp.json();\u001b[39m\n\u001b[31m      if (data.success && data.docket) {\u001b[39m\n\u001b[31m        return { docket: data.docket, confidence: data.confidence, source: 'ollama' };\u001b[39m\n\u001b[31m      }\u001b[39m\n\u001b[31m    } catch(e) { /* fall through */ }\u001b[39m\n\u001b[31m  }·\u001b[39m\n\u001b[31m  return { docket: '', confidence: 0, source: 'failed' };\u001b[39m\n\u001b[31m};·\u001b[39m\n\u001b[31mfunction extractDocketFromText(text, source) {\u001b[39m\n\u001b[31m  // Pattern: typical courier docket = 8-15 digit numbers, sometimes with letters\u001b[39m\n\u001b[31m  const patterns = [\u001b[39m\n\u001b[31m    /\\\\b([A-Z]{0,3}\\\\d{8,15}[A-Z]{0,3})\\\\b/g,   // alphanumeric tracking\u001b[39m\n\u001b[31m    /\\\\b(\\\\d{9,15})\\\\b/g,                          // pure numeric\u001b[39m\n\u001b[31m    /\\\\b([A-Z]{2,4}\\\\d{6,12})\\\\b/g,               // letter prefix\u001b[39m\n\u001b[31m  ];\u001b[39m\n\u001b[31m  const candidates = [];\u001b[39m\n\u001b[31m  for (const pat of patterns) {\u001b[39m\n\u001b[31m    let m;\u001b[39m\n\u001b[31m    while ((m = pat.exec(text)) !== null) {\u001b[39m\n\u001b[31m      if (!candidates.includes(m[1])) candidates.push(m[1]);\u001b[39m\n\u001b[31m    }\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m  if (candidates.length === 0) return { docket: text.trim().slice(0, 20), confidence: 0.3, source };\u001b[39m\n\u001b[31m  // Pick longest candidate (usually most complete docket)\u001b[39m\n\u001b[31m  const best = candidates.sort((a, b) => b.length - a.length)[0];\u001b[39m\n\u001b[31m  const confidence = candidates.length === 1 ? 0.85 : 0.7;\u001b[39m\n\u001b[31m  return { docket: best, confidence, source };\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// Expose for non-module code\u001b[39m\n\u001b[31mwindow._ocrReady = () => ocrPipeline !== null;\u001b[39m\n\u001b[31m</script>·\u001b[39m\n\u001b[31m<script>\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31m// STATE & STORAGE\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31mlet dispatches = [];\u001b[39m\n\u001b[31mlet stores = [];\u001b[39m\n\u001b[31mlet slipOCRData = [];\u001b[39m\n\u001b[31mlet dashFilter = 'all';·\u001b[39m\n\u001b[31m// Only sheets_url is persisted locally — all other config comes from the sheet\u001b[39m\n\u001b[31mlet _trackingInProgress = false;\u001b[39m\n\u001b[31mwindow.appConfig = {\u001b[39m\n\u001b[31m  sheets_url:      'https://script.google.com/macros/s/AKfycbys8HrToDv2u_dRvSFnNqmO9bnyAO1s8oSprUydpVQnNQutq4tuUY71cVCH24urbvM9/exec',\u001b[39m\n\u001b[31m  webhook_url:     '',\u001b[39m\n\u001b[31m  proxy_url:       '',\u001b[39m\n\u001b[31m  ollama_url:      'http://srv1111289.hstgr.cloud:11434',\u001b[39m\n\u001b[31m  ocr_mode:        'both',\u001b[39m\n\u001b[31m  company:         '360 Degree Logical Marketing',\u001b[39m\n\u001b[31m  city:            'Chandigarh',\u001b[39m\n\u001b[31m  auto_log:        'true',\u001b[39m\n\u001b[31m  refresh_on_load: 'false',\u001b[39m\n\u001b[31m};·\u001b[39m\n\u001b[31mconst COURIER_MASTER = {\u001b[39m\n\u001b[31m  'SkyKing': {\u001b[39m\n\u001b[31m    trackUrl: 'https://skyking.co/track?cno={docket}',\u001b[39m\n\u001b[31m    trackBulkUrl: 'https://skyking.co/track',\u001b[39m\n\u001b[31m    apiProxy: '/track-proxy/skyking?cno={docket}',\u001b[39m\n\u001b[31m    maxBulk: 10\u001b[39m\n\u001b[31m  },\u001b[39m\n\u001b[31m  'DTDC': {\u001b[39m\n\u001b[31m    trackUrl: 'https://www.dtdc.in/tracking.asp',\u001b[39m\n\u001b[31m    queryParam: 'trkId',\u001b[39m\n\u001b[31m    maxBulk: 1\u001b[39m\n\u001b[31m  },\u001b[39m\n\u001b[31m  'Delhivery': {\u001b[39m\n\u001b[31m    trackUrl: 'https://www.delhivery.com/track/package/{docket}',\u001b[39m\n\u001b[31m    maxBulk: 1\u001b[39m\n\u001b[31m  },\u001b[39m\n\u001b[31m  'Blue Dart': {\u001b[39m\n\u001b[31m    trackUrl: 'https://www.bluedart.com/tracking',\u001b[39m\n\u001b[31m    maxBulk: 1\u001b[39m\n\u001b[31m  },\u001b[39m\n\u001b[31m  'FedEx': {\u001b[39m\n\u001b[31m    trackUrl: 'https://www.fedex.com/fedextrack/?trknbr={docket}',\u001b[39m\n\u001b[31m    maxBulk: 10\u001b[39m\n\u001b[31m  },\u001b[39m\n\u001b[31m  'Ecom Express': {\u001b[39m\n\u001b[31m    trackUrl: 'https://ecomexpress.in/tracking/?awb_field={docket}',\u001b[39m\n\u001b[31m    maxBulk: 1\u001b[39m\n\u001b[31m  },\u001b[39m\n\u001b[31m  'India Post': {\u001b[39m\n\u001b[31m    trackUrl: 'https://www.indiapost.gov.in/VAS/Pages/trackconsignment.aspx',\u001b[39m\n\u001b[31m    maxBulk: 1\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m};·\u001b[39m\n\u001b[31mfunction loadData() {\u001b[39m\n\u001b[31m  try {\u001b[39m\n\u001b[31m    dispatches = JSON.parse(localStorage.getItem('360lm_disp_v2') || '[]');\u001b[39m\n\u001b[31m    const saved = JSON.parse(localStorage.getItem('360lm_disp_sheets_url') || '{}');\u001b[39m\n\u001b[31m    if (saved.sheets_url) window.appConfig.sheets_url = saved.sheets_url;\u001b[39m\n\u001b[31m  } catch(e) {}\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31mfunction saveData() {\u001b[39m\n\u001b[31m  try {\u001b[39m\n\u001b[31m    localStorage.setItem('360lm_disp_v2', JSON.stringify(dispatches));\u001b[39m\n\u001b[31m  } catch(e) {\u001b[39m\n\u001b[31m    if (e.name === 'QuotaExceededError' || e.code === 22) {\u001b[39m\n\u001b[31m      showToast('Storage full — old dispatches cleared. Data is safe in Postgres.', 'err');\u001b[39m\n\u001b[31m      // Drop delivered dispatches older than 30 days to free space\u001b[39m\n\u001b[31m      const cutoff = Date.now() - 30 * 24 * 3600 * 1000;\u001b[39m\n\u001b[31m      dispatches = dispatches.filter(d =>\u001b[39m\n\u001b[31m        d.status !== 'draft' && (\u001b[39m\n\u001b[31m          d.status !== 'active' ||\u001b[39m\n\u001b[31m          new Date(d.createdAt || 0).getTime() > cutoff\u001b[39m\n\u001b[31m        ) ? true :\u001b[39m\n\u001b[31m        d.status === 'draft' || new Date(d.createdAt || 0).getTime() > cutoff\u001b[39m\n\u001b[31m      );\u001b[39m\n\u001b[31m      try { localStorage.setItem('360lm_disp_v2', JSON.stringify(dispatches)); } catch(_) {}\u001b[39m\n\u001b[31m    }\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// ── Postgres DB sync ──────────────────────────────────────────────────────\u001b[39m\n\u001b[31mconst _DB        = '/db';\u001b[39m\n\u001b[31m// PostgREST v12: Accept-Profile for reads, Content-Profile for writes\u001b[39m\n\u001b[31mconst _DHDR_READ  = { 'Accept-Profile': 'dispatch' };\u001b[39m\n\u001b[31mconst _DHDR_WRITE = { 'Content-Type': 'application/json', 'Content-Profile': 'dispatch', 'Accept-Profile': 'dispatch' };\u001b[39m\n\u001b[31mconst _DHDR_UPSERT = { ..._DHDR_WRITE, 'Prefer': 'resolution=merge-duplicates,return=minimal' };·\u001b[39m\n\u001b[31mfunction _dispRow(d) {\u001b[39m\n\u001b[31m  return {\u001b[39m\n\u001b[31m    dispatch_id:    d.id,\u001b[39m\n\u001b[31m    client:         d.client    || '',\u001b[39m\n\u001b[31m    job_ref:        d.jobref    || null,\u001b[39m\n\u001b[31m    courier:        d.courier   || '',\u001b[39m\n\u001b[31m    dispatch_date:  d.dispatchDate || null,\u001b[39m\n\u001b[31m    status:         d.status    || 'active',\u001b[39m\n\u001b[31m    total_count:    (d.consignees || []).length,\u001b[39m\n\u001b[31m    docketed_count: (d.consignees || []).filter(c => c.docket).length,\u001b[39m\n\u001b[31m    remarks:        d.remarks   || null,\u001b[39m\n\u001b[31m    created_by:     window._hubUser?.id || 'unknown',\u001b[39m\n\u001b[31m  };\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction _consRow(c, dispatchId, idx) {\u001b[39m\n\u001b[31m  return {\u001b[39m\n\u001b[31m    cons_id:            `${dispatchId}-${idx}`,\u001b[39m\n\u001b[31m    dispatch_id:        dispatchId,\u001b[39m\n\u001b[31m    seq_no:             idx,\u001b[39m\n\u001b[31m    name:               c.name          || '',\u001b[39m\n\u001b[31m    city:               c.city          || null,\u001b[39m\n\u001b[31m    address:            c.address       || null,\u001b[39m\n\u001b[31m    phone:              c.phone         || null,\u001b[39m\n\u001b[31m    pincode:            c.pincode       || null,\u001b[39m\n\u001b[31m    material:           c.material      || null,\u001b[39m\n\u001b[31m    qty:                c.qty           || null,\u001b[39m\n\u001b[31m    docket:             c.docket        || null,\u001b[39m\n\u001b[31m    status:             c.status        || 'pending',\u001b[39m\n\u001b[31m    last_event:         c.lastEvent     || null,\u001b[39m\n\u001b[31m    last_scan_location: c.lastScanLocation || null,\u001b[39m\n\u001b[31m    last_tracked_at:    c.lastTrackedAt || null,\u001b[39m\n\u001b[31m    manual_status:      c.manualStatus  || null,\u001b[39m\n\u001b[31m    manual_delivered:   !!c.manualDelivered,\u001b[39m\n\u001b[31m    manual_called_by:   c.manualCalledBy || null,\u001b[39m\n\u001b[31m    manual_remarks:     c.manualRemarks || null,\u001b[39m\n\u001b[31m    pod_uploaded:       !!c.podUploaded,\u001b[39m\n\u001b[31m  };\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31masync function syncDispatchToDB(d) {\u001b[39m\n\u001b[31m  if (!d || d.status === 'draft') return; // never sync drafts\u001b[39m\n\u001b[31m  try {\u001b[39m\n\u001b[31m    await fetch(`${_DB}/dispatches`, {\u001b[39m\n\u001b[31m      method: 'POST', headers: _DHDR_UPSERT,\u001b[39m\n\u001b[31m      body: JSON.stringify(_dispRow(d)),\u001b[39m\n\u001b[31m    });\u001b[39m\n\u001b[31m    const rows = (d.consignees || []).map((c, i) => _consRow(c, d.id, i));\u001b[39m\n\u001b[31m    if (rows.length) {\u001b[39m\n\u001b[31m      await fetch(`${_DB}/consignees`, {\u001b[39m\n\u001b[31m        method: 'POST', headers: _DHDR_UPSERT,\u001b[39m\n\u001b[31m        body: JSON.stringify(rows),\u001b[39m\n\u001b[31m      });\u001b[39m\n\u001b[31m    }\u001b[39m\n\u001b[31m  } catch(e) { console.warn('[dispatch-db] sync error:', e.message); }\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31masync function syncConsigneeToDB(c, dispatchId, idx) {\u001b[39m\n\u001b[31m  try {\u001b[39m\n\u001b[31m    await fetch(`${_DB}/consignees`, {\u001b[39m\n\u001b[31m      method: 'POST', headers: _DHDR_UPSERT,\u001b[39m\n\u001b[31m      body: JSON.stringify(_consRow(c, dispatchId, idx)),\u001b[39m\n\u001b[31m    });\u001b[39m\n\u001b[31m    // Log tracking event\u001b[39m\n\u001b[31m    if (c.lastEvent && c.status) {\u001b[39m\n\u001b[31m      fetch(`${_DB}/tracking_events`, {\u001b[39m\n\u001b[31m        method: 'POST', headers: _DHDR_WRITE,\u001b[39m\n\u001b[31m        body: JSON.stringify({\u001b[39m\n\u001b[31m          docket: c.docket, dispatch_id: dispatchId,\u001b[39m\n\u001b[31m          cons_id: `${dispatchId}-${idx}`,\u001b[39m\n\u001b[31m          status: c.status, event_text: c.lastEvent,\u001b[39m\n\u001b[31m          scan_location: c.lastScanLocation || null,\u001b[39m\n\u001b[31m        }),\u001b[39m\n\u001b[31m      }).catch(() => {});\u001b[39m\n\u001b[31m    }\u001b[39m\n\u001b[31m  } catch(e) { console.warn('[dispatch-db] consignee sync error:', e.message); }\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31masync function loadFromDB() {\u001b[39m\n\u001b[31m  try {\u001b[39m\n\u001b[31m    const [dResp, cResp] = await Promise.all([\u001b[39m\n\u001b[31m      fetch(`${_DB}/dispatches?order=created_at.desc&limit=100`, { headers: _DHDR_READ }),\u001b[39m\n\u001b[31m      fetch(`${_DB}/consignees?order=dispatch_id,seq_no`, { headers: _DHDR_READ }),\u001b[39m\n\u001b[31m    ]);\u001b[39m\n\u001b[31m    if (!dResp.ok) return;\u001b[39m\n\u001b[31m    const dbDispatches = await dResp.json();\u001b[39m\n\u001b[31m    const dbConsignees = cResp.ok ? await cResp.json() : [];\u001b[39m\n\u001b[31m    if (!dbDispatches.length) return;·\u001b[39m\n\u001b[31m    const dbIds = new Set(dbDispatches.map(d => d.dispatch_id));\u001b[39m\n\u001b[31m    const localOnly = dispatches.filter(d => !dbIds.has(d.id));·\u001b[39m\n\u001b[31m    const fromDB = dbDispatches.map(d => {\u001b[39m\n\u001b[31m      const cons = dbConsignees.filter(c => c.dispatch_id === d.dispatch_id);\u001b[39m\n\u001b[31m      return {\u001b[39m\n\u001b[31m        id:           d.dispatch_id,\u001b[39m\n\u001b[31m        client:       d.client,\u001b[39m\n\u001b[31m        jobref:       d.job_ref,\u001b[39m\n\u001b[31m        courier:      d.courier,\u001b[39m\n\u001b[31m        dispatchDate: d.dispatch_date,\u001b[39m\n\u001b[31m        status:       d.status,\u001b[39m\n\u001b[31m        remarks:      d.remarks,\u001b[39m\n\u001b[31m        createdAt:    d.created_at,\u001b[39m\n\u001b[31m        consignees:   cons.map(c => ({\u001b[39m\n\u001b[31m          name:              c.name,\u001b[39m\n\u001b[31m          city:              c.city,\u001b[39m\n\u001b[31m          address:           c.address,\u001b[39m\n\u001b[31m          phone:             c.phone,\u001b[39m\n\u001b[31m          pincode:           c.pincode,\u001b[39m\n\u001b[31m          material:          c.material,\u001b[39m\n\u001b[31m          qty:               c.qty,\u001b[39m\n\u001b[31m          docket:            c.docket,\u001b[39m\n\u001b[31m          status:            c.status,\u001b[39m\n\u001b[31m          lastEvent:         c.last_event,\u001b[39m\n\u001b[31m          lastScanLocation:  c.last_scan_location,\u001b[39m\n\u001b[31m          lastTrackedAt:     c.last_tracked_at,\u001b[39m\n\u001b[31m          manualStatus:      c.manual_status,\u001b[39m\n\u001b[31m          manualDelivered:   c.manual_delivered,\u001b[39m\n\u001b[31m          manualCalledBy:    c.manual_called_by,\u001b[39m\n\u001b[31m          manualRemarks:     c.manual_remarks,\u001b[39m\n\u001b[31m          podUploaded:       c.pod_uploaded,\u001b[39m\n\u001b[31m          _dbIdx:            c.seq_no,\u001b[39m\n\u001b[31m        })),\u001b[39m\n\u001b[31m      };\u001b[39m\n\u001b[31m    });·\u001b[39m\n\u001b[31m    dispatches = [...fromDB, ...localOnly];\u001b[39m\n\u001b[31m    saveData();\u001b[39m\n\u001b[31m    renderDashboard();\u001b[39m\n\u001b[31m    updateHistCount();\u001b[39m\n\u001b[31m  } catch(e) { console.warn('[dispatch-db] load error:', e.message); }\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31mfunction saveSheetsUrlLocal() {\u001b[39m\n\u001b[31m  localStorage.setItem('360lm_disp_sheets_url', JSON.stringify({ sheets_url: window.appConfig.sheets_url }));\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m/**\u001b[39m\n\u001b[31m * Fetch config.json from the VPS server (same origin).\u001b[39m\n\u001b[31m * This file contains the sheets_url — no manual entry needed.\u001b[39m\n\u001b[31m * Falls back to localStorage cache if fetch fails (offline).\u001b[39m\n\u001b[31m */\u001b[39m\n\u001b[31masync function loadServerConfig() {\u001b[39m\n\u001b[31m  try {\u001b[39m\n\u001b[31m    const resp = await fetch('./config.json', { cache: 'no-store' });\u001b[39m\n\u001b[31m    if (resp.ok) {\u001b[39m\n\u001b[31m      const cfg = await resp.json();\u001b[39m\n\u001b[31m      if (cfg.sheets_url) {\u001b[39m\n\u001b[31m        window.appConfig.sheets_url = cfg.sheets_url;\u001b[39m\n\u001b[31m        saveSheetsUrlLocal(); // cache it for offline use\u001b[39m\n\u001b[31m        document.getElementById('cfg_sheets_url').value = cfg.sheets_url;\u001b[39m\n\u001b[31m        return true;\u001b[39m\n\u001b[31m      }\u001b[39m\n\u001b[31m    }\u001b[39m\n\u001b[31m  } catch(e) {\u001b[39m\n\u001b[31m    console.log('config.json fetch failed, using cached URL:', e.message);\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m  return false;\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31m// INIT\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31m// ── Hub auth gate ─────────────────────────────────────────────────────────\u001b[39m\n\u001b[31mwindow._hubUser = null;\u001b[39m\n\u001b[31m(function() {\u001b[39m\n\u001b[31m  try {\u001b[39m\n\u001b[31m    const hubLogin = '/hub/?next=' + encodeURIComponent(location.pathname + location.search);\u001b[39m\n\u001b[31m    const s = JSON.parse(localStorage.getItem('lm360-session') || 'null');\u001b[39m\n\u001b[31m    if (!s || !s.empId || !s.name) { window.location.replace(hubLogin); return; }\u001b[39m\n\u001b[31m    if (Date.now() - new Date(s.loginAt).getTime() > 12 * 3600 * 1000) {\u001b[39m\n\u001b[31m      window.location.replace(hubLogin); return;\u001b[39m\n\u001b[31m    }\u001b[39m\n\u001b[31m    window._hubUser = { id: s.empId, name: s.name, role: s.role };\u001b[39m\n\u001b[31m  } catch(_) { window.location.replace('/hub/?next=' + encodeURIComponent(location.pathname + location.search)); }\u001b[39m\n\u001b[31m})();·\u001b[39m\n\u001b[31mwindow.addEventListener('load', async () => {\u001b[39m\n\u001b[31m  if (!window._hubUser) return; // should never reach here — gate redirected already\u001b[39m\n\u001b[31m  setTimeout(() => {\u001b[39m\n\u001b[31m    document.getElementById('splash').style.opacity = '0';\u001b[39m\n\u001b[31m    setTimeout(() => {\u001b[39m\n\u001b[31m      document.getElementById('splash').style.display = 'none';\u001b[39m\n\u001b[31m      document.getElementById('app').style.display = 'flex';\u001b[39m\n\u001b[31m    }, 450);\u001b[39m\n\u001b[31m  }, 1400);\u001b[39m\n\u001b[31m  loadData();\u001b[39m\n\u001b[31m  // Auto-authenticate config tab for admin users (no password needed)\u001b[39m\n\u001b[31m  if (['harish', 'pramod'].includes(window._hubUser?.id)) _adminAuthed = true;\u001b[39m\n\u001b[31m  setToday();\u001b[39m\n\u001b[31m  addConsignee();\u001b[39m\n\u001b[31m  updateHistCount();\u001b[39m\n\u001b[31m  setInterval(updateNetDot, 5000);\u001b[39m\n\u001b[31m  updateNetDot();\u001b[39m\n\u001b[31m  checkShareTarget();\u001b[39m\n\u001b[31m  setupDragDrop();·\u001b[39m\n\u001b[31m  // Step 1: fetch sheets_url from VPS config.json (auto, no user input)\u001b[39m\n\u001b[31m  await loadServerConfig();·\u001b[39m\n\u001b[31m  // Step 2: populate config tab field\u001b[39m\n\u001b[31m  document.getElementById('cfg_sheets_url').value = window.appConfig.sheets_url || '';·\u001b[39m\n\u001b[31m  // Step 3: load dispatches from Postgres in background (merge with local)\u001b[39m\n\u001b[31m  loadFromDB(); // non-blocking — updates UI when data arrives·\u001b[39m\n\u001b[31m  // Step 4: sync full config from Google Sheet silently\u001b[39m\n\u001b[31m  if (window.appConfig.sheets_url) {\u001b[39m\n\u001b[31m    syncFromSheet(true); // silent on startup\u001b[39m\n\u001b[31m  } else {\u001b[39m\n\u001b[31m    // Only show config tab if no URL found anywhere\u001b[39m\n\u001b[31m    showPane('config');\u001b[39m\n\u001b[31m    showToast('No config.json found on server', 'err');\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m  // Pre-warm OCR after delay\u001b[39m\n\u001b[31m  setTimeout(() => { if (typeof window.initOCR === 'function') window.initOCR(); }, 4000);\u001b[39m\n\u001b[31m});·\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31m// WEB SHARE TARGET\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31masync function checkShareTarget() {\u001b[39m\n\u001b[31m  // Check URL params for share target\u001b[39m\n\u001b[31m  const params = new URLSearchParams(window.location.search);\u001b[39m\n\u001b[31m  const action = params.get('action');\u001b[39m\n\u001b[31m  if (action !== 'share') return;·\u001b[39m\n\u001b[31m  // Check for shared files via Share Target API\u001b[39m\n\u001b[31m  if ('launchQueue' in window) {\u001b[39m\n\u001b[31m    window.launchQueue.setConsumer(async launchParams => {\u001b[39m\n\u001b[31m      if (!launchParams.files || launchParams.files.length === 0) return;\u001b[39m\n\u001b[31m      const files = await Promise.all(launchParams.files.map(f => f.getFile()));\u001b[39m\n\u001b[31m      const imageFiles = files.filter(f => f.type.startsWith('image/'));\u001b[39m\n\u001b[31m      if (imageFiles.length > 0) {\u001b[39m\n\u001b[31m        document.getElementById('shareTargetInfo').style.display = 'flex';\u001b[39m\n\u001b[31m        document.getElementById('sharedCount').textContent = imageFiles.length;\u001b[39m\n\u001b[31m        document.getElementById('shareBanner').classList.add('show');\u001b[39m\n\u001b[31m        setTimeout(() => document.getElementById('shareBanner').classList.remove('show'), 4000);\u001b[39m\n\u001b[31m        showPane('new');\u001b[39m\n\u001b[31m        await handleSlipFiles(imageFiles);\u001b[39m\n\u001b[31m      }\u001b[39m\n\u001b[31m    });\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction dismissShare() {\u001b[39m\n\u001b[31m  document.getElementById('shareBanner').classList.remove('show');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31m// DRAG & DROP\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31mfunction setupDragDrop() {\u001b[39m\n\u001b[31m  const zone = document.getElementById('slipZone');\u001b[39m\n\u001b[31m  zone.addEventListener('dragover', e => { e.preventDefault(); zone.classList.add('drag'); });\u001b[39m\n\u001b[31m  zone.addEventListener('dragleave', () => zone.classList.remove('drag'));\u001b[39m\n\u001b[31m  zone.addEventListener('drop', e => {\u001b[39m\n\u001b[31m    e.preventDefault();\u001b[39m\n\u001b[31m    zone.classList.remove('drag');\u001b[39m\n\u001b[31m    const files = Array.from(e.dataTransfer.files).filter(f => f.type.startsWith('image/'));\u001b[39m\n\u001b[31m    if (files.length) handleSlipFiles(files);\u001b[39m\n\u001b[31m  });\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31m// SLIP OCR HANDLER\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31masync function handleSlipFiles(files) {\u001b[39m\n\u001b[31m  const fileArr = Array.from(files);\u001b[39m\n\u001b[31m  if (!fileArr.length) return;\u001b[39m\n\u001b[31m  // If dockets already mapped from store list — allow upload but skip OCR extraction\u001b[39m\n\u001b[31m  const skipOCR = shouldSkipOCR();\u001b[39m\n\u001b[31m  if (skipOCR) {\u001b[39m\n\u001b[31m    showToast('Dockets already mapped — slips saved for reference only', 'info');\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m  showOCRWorking(true, `Processing ${fileArr.length} slip(s)...`);·\u001b[39m\n\u001b[31m  for (let i = 0; i < fileArr.length; i++) {\u001b[39m\n\u001b[31m    const file = fileArr[i];\u001b[39m\n\u001b[31m    showOCRWorking(true, `OCR: ${i+1}/${fileArr.length} — ${file.name}`);\u001b[39m\n\u001b[31m    const imageData = await readFileAsDataURL(file);\u001b[39m\n\u001b[31m    let result = { docket: '', confidence: 0, source: 'manual' };\u001b[39m\n\u001b[31m    if (!skipOCR) {\u001b[39m\n\u001b[31m      try {\u001b[39m\n\u001b[31m        result = await window.runOCROnImage(imageData, file.name);\u001b[39m\n\u001b[31m      } catch(e) {}\u001b[39m\n\u001b[31m    } else {\u001b[39m\n\u001b[31m      result = { docket: '', confidence: 1, source: 'skipped — docket pre-mapped' };\u001b[39m\n\u001b[31m    }\u001b[39m\n\u001b[31m    slipOCRData.push({ file, imageData, filename: file.name, ...result });\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m  showOCRWorking(false);\u001b[39m\n\u001b[31m  renderOCRResults();\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction readFileAsDataURL(file) {\u001b[39m\n\u001b[31m  return new Promise((res, rej) => {\u001b[39m\n\u001b[31m    const r = new FileReader();\u001b[39m\n\u001b[31m    r.onload = e => res(e.target.result);\u001b[39m\n\u001b[31m    r.onerror = rej;\u001b[39m\n\u001b[31m    r.readAsDataURL(file);\u001b[39m\n\u001b[31m  });\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction renderOCRResults() {\u001b[39m\n\u001b[31m  if (!slipOCRData.length) return;\u001b[39m\n\u001b[31m  const list = document.getElementById('ocrSlipList');\u001b[39m\n\u001b[31m  document.getElementById('ocrResults').classList.add('visible');·\u001b[39m\n\u001b[31m  const needReview = slipOCRData.filter(s => s.confidence < 0.65 && s.docket).length;\u001b[39m\n\u001b[31m  const autoOk     = slipOCRData.filter(s => s.confidence >= 0.88).length;\u001b[39m\n\u001b[31m  const summary    = document.getElementById('ocrSummary');\u001b[39m\n\u001b[31m  if (summary) {\u001b[39m\n\u001b[31m    summary.style.display = '';\u001b[39m\n\u001b[31m    summary.innerHTML =\u001b[39m\n\u001b[31m      `<span style=\\\"color:var(--grn)\\\">✓ ${autoOk} auto-accepted</span>` +\u001b[39m\n\u001b[31m      (needReview ? `&nbsp;·&nbsp;<span style=\\\"color:var(--ylw)\\\">⚠ ${needReview} need manual check</span>` : '');\u001b[39m\n\u001b[31m  }·\u001b[39m\n\u001b[31m  list.innerHTML = slipOCRData.map((s, i) => {\u001b[39m\n\u001b[31m    const autoAccepted = s.confidence >= 0.88 && s.docket;\u001b[39m\n\u001b[31m    const needsCheck   = s.confidence < 0.65  && s.docket;\u001b[39m\n\u001b[31m    const confClass = autoAccepted ? 'conf-high' : needsCheck ? 'conf-low' : 'conf-med';\u001b[39m\n\u001b[31m    const confLabel = autoAccepted\u001b[39m\n\u001b[31m      ? `✓ ${Math.round(s.confidence * 100)}% — auto`\u001b[39m\n\u001b[31m      : needsCheck\u001b[39m\n\u001b[31m        ? `⚠ ${Math.round(s.confidence * 100)}% — verify`\u001b[39m\n\u001b[31m        : `${Math.round(s.confidence * 100)}%`;\u001b[39m\n\u001b[31m    const borderStyle = needsCheck ? 'border:1px solid var(--ylw);' : '';\u001b[39m\n\u001b[31m    return `\u001b[39m\n\u001b[31m    <div class=\\\"ocr-slip\\\" id=\\\"ocrSlip_${i}\\\" style=\\\"${borderStyle}\\\">\u001b[39m\n\u001b[31m      <img src=\\\"${s.imageData}\\\" alt=\\\"slip ${i+1}\\\">\u001b[39m\n\u001b[31m      <div class=\\\"ocr-slip-body\\\">\u001b[39m\n\u001b[31m        <div class=\\\"ocr-slip-name\\\">${s.filename}</div>\u001b[39m\n\u001b[31m        <input type=\\\"text\\\" class=\\\"ocr-slip-docket\\\" id=\\\"ocrDocket_${i}\\\"\u001b[39m\n\u001b[31m          value=\\\"${s.docket}\\\" placeholder=\\\"Docket number\\\"\u001b[39m\n\u001b[31m          ${autoAccepted ? 'style=\\\"opacity:.6\\\"' : ''}\u001b[39m\n\u001b[31m          oninput=\\\"slipOCRData[${i}].docket=this.value;slipOCRData[${i}].confidence=0.5\\\">\u001b[39m\n\u001b[31m        <div class=\\\"ocr-slip-status\\\">\u001b[39m\n\u001b[31m          <span class=\\\"ocr-confidence ${confClass}\\\">${confLabel}</span>\u001b[39m\n\u001b[31m          <span style=\\\"color:var(--t4);font-family:var(--fnt-mono);font-size:.6rem;\\\">via ${s.source}</span>\u001b[39m\n\u001b[31m          <button class=\\\"ocr-ai-btn\\\" onclick=\\\"reOCR(${i})\\\">🔄 Re-OCR</button>\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m    </div>`;\u001b[39m\n\u001b[31m  }).join('');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31masync function reOCR(i) {\u001b[39m\n\u001b[31m  const s = slipOCRData[i];\u001b[39m\n\u001b[31m  showOCRWorking(true, `Re-running OCR on ${s.filename}...`);\u001b[39m\n\u001b[31m  try {\u001b[39m\n\u001b[31m    const result = await window.runOCROnImage(s.imageData, s.filename);\u001b[39m\n\u001b[31m    slipOCRData[i] = { ...s, ...result };\u001b[39m\n\u001b[31m    renderOCRResults();\u001b[39m\n\u001b[31m    showToast('Re-OCR complete', 'ok');\u001b[39m\n\u001b[31m  } catch(e) {\u001b[39m\n\u001b[31m    showToast('OCR failed', 'err');\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m  showOCRWorking(false);\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction clearSlips() {\u001b[39m\n\u001b[31m  slipOCRData = [];\u001b[39m\n\u001b[31m  document.getElementById('ocrResults').classList.remove('visible');\u001b[39m\n\u001b[31m  document.getElementById('ocrSlipList').innerHTML = '';\u001b[39m\n\u001b[31m  const sum = document.getElementById('ocrSummary');\u001b[39m\n\u001b[31m  if (sum) { sum.style.display = 'none'; sum.innerHTML = ''; }\u001b[39m\n\u001b[31m  document.getElementById('shareTargetInfo').style.display = 'none';\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction applyAllDockets() {\u001b[39m\n\u001b[31m  // Match each OCR result to a consignee by order or by name matching\u001b[39m\n\u001b[31m  const consList = document.getElementById('consList');\u001b[39m\n\u001b[31m  const cards = consList.querySelectorAll('.cons-card');\u001b[39m\n\u001b[31m  slipOCRData.forEach((s, i) => {\u001b[39m\n\u001b[31m    if (!s.docket) return;\u001b[39m\n\u001b[31m    const card = cards[i];\u001b[39m\n\u001b[31m    if (card) {\u001b[39m\n\u001b[31m      const docketInput = card.querySelector('.cons-docket-input');\u001b[39m\n\u001b[31m      if (docketInput) {\u001b[39m\n\u001b[31m        docketInput.value = s.docket;\u001b[39m\n\u001b[31m        showLinkedDocket(card, s.docket);\u001b[39m\n\u001b[31m      }\u001b[39m\n\u001b[31m    }\u001b[39m\n\u001b[31m  });\u001b[39m\n\u001b[31m  showToast(`Applied ${slipOCRData.filter(s=>s.docket).length} dockets ✓`, 'ok');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31m// STORE LIST — with column mapping UI\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31mlet _rawRows = [];      // raw rows from file, kept for re-mapping\u001b[39m\n\u001b[31mlet _rawHeaders = [];   // headers from file·\u001b[39m\n\u001b[31masync function handleStoreFile(input) {\u001b[39m\n\u001b[31m  const file = input.files[0];\u001b[39m\n\u001b[31m  if (!file) return;\u001b[39m\n\u001b[31m  showToast('Reading file...', 'info');·\u001b[39m\n\u001b[31m  const data = await readFileAsDataURL(file);\u001b[39m\n\u001b[31m  const base64 = data.split(',')[1];\u001b[39m\n\u001b[31m  const binary = atob(base64);\u001b[39m\n\u001b[31m  const bytes = new Uint8Array(binary.length);\u001b[39m\n\u001b[31m  for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);·\u001b[39m\n\u001b[31m  let rows = [];\u001b[39m\n\u001b[31m  try {\u001b[39m\n\u001b[31m    if (file.name.endsWith('.csv')) {\u001b[39m\n\u001b[31m      const text = new TextDecoder().decode(bytes);\u001b[39m\n\u001b[31m      rows = text.split('\\\\n').map(r => r.split(',').map(c => c.trim().replace(/^\\\"|\\\"$/g, '')));\u001b[39m\n\u001b[31m    } else {\u001b[39m\n\u001b[31m      const wb = XLSX.read(bytes, { type: 'array' });\u001b[39m\n\u001b[31m      const ws = wb.Sheets[wb.SheetNames[0]];\u001b[39m\n\u001b[31m      rows = XLSX.utils.sheet_to_json(ws, { header: 1 });\u001b[39m\n\u001b[31m    }\u001b[39m\n\u001b[31m  } catch(e) { showToast('File read error: ' + e.message, 'err'); return; }·\u001b[39m\n\u001b[31m  if (!rows.length || !rows[0]) { showToast('File appears empty', 'err'); return; }·\u001b[39m\n\u001b[31m  _rawHeaders = (rows[0] || []).map(h => (h || '').toString().trim());\u001b[39m\n\u001b[31m  _rawRows = rows.slice(1).filter(r => r && r.some(c => c !== '' && c != null));·\u001b[39m\n\u001b[31m  document.getElementById('storeTableArea').style.display = 'block';\u001b[39m\n\u001b[31m  renderMappingUI(file.name);\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// ── COLUMN MAPPING FIELDS ─────────────────────────────\u001b[39m\n\u001b[31mconst MAPPING_FIELDS = [\u001b[39m\n\u001b[31m  { key: 'name',    label: 'Store / Partner Name', required: true,\u001b[39m\n\u001b[31m    keywords: ['store','shop','name','outlet','consignee','destination','partner','dealer','client','retailer'] },\u001b[39m\n\u001b[31m  { key: 'city',    label: 'City / Location',       required: false,\u001b[39m\n\u001b[31m    keywords: ['city','town','district','location','area','region','place'] },\u001b[39m\n\u001b[31m  { key: 'phone',   label: 'Phone / Mobile',        required: false,\u001b[39m\n\u001b[31m    keywords: ['phone','mobile','contact','tel','number','mob'] },\u001b[39m\n\u001b[31m  { key: 'docket',  label: 'Docket / AWB No.',      required: false,\u001b[39m\n\u001b[31m    keywords: ['docket','awb','tracking','courier','dkt','consignment','cno','waybill'] },\u001b[39m\n\u001b[31m  { key: 'address', label: 'Address',               required: false,\u001b[39m\n\u001b[31m    keywords: ['address','addr','street','locality','area'] },\u001b[39m\n\u001b[31m  { key: 'pincode', label: 'Pincode / PIN',          required: false,\u001b[39m\n\u001b[31m    keywords: ['pin','pincode','postal','zip','code'] },\u001b[39m\n\u001b[31m];·\u001b[39m\n\u001b[31m// Saved mapping key based on column fingerprint (so same client file auto-maps)\u001b[39m\n\u001b[31mfunction getMappingKey() {\u001b[39m\n\u001b[31m  return 'colmap_' + _rawHeaders.join('|').toLowerCase().replace(/\\\\s+/g,'');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// ── Detect if a column is a combined/multiline address cell ─────────────\u001b[39m\n\u001b[31mfunction isCombinedColumn(colIdx) {\u001b[39m\n\u001b[31m  if (colIdx < 0) return false;\u001b[39m\n\u001b[31m  // Check first 3 data rows — if any cell contains newline or has 6+ digit number (pincode) = combined\u001b[39m\n\u001b[31m  const samples = _rawRows.slice(0, 3).map(r => (r[colIdx] || '').toString());\u001b[39m\n\u001b[31m  return samples.some(s => s.includes('\\\\n') || /\\\\b\\\\d{6}\\\\b/.test(s) || s.split(',').length >= 3);\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction renderMappingUI(filename) {\u001b[39m\n\u001b[31m  const saved = (() => { try { return JSON.parse(localStorage.getItem(getMappingKey()) || 'null'); } catch(e) { return null; } })();\u001b[39m\n\u001b[31m  const isSaved = !!saved;\u001b[39m\n\u001b[31m  const colInfoEl = document.getElementById('storeColInfo');\u001b[39m\n\u001b[31m  colInfoEl.style.display = 'block';·\u001b[39m\n\u001b[31m  const suggested = {};\u001b[39m\n\u001b[31m  MAPPING_FIELDS.forEach(f => {\u001b[39m\n\u001b[31m    const idx = findCol(_rawHeaders, f.keywords);\u001b[39m\n\u001b[31m    suggested[f.key] = saved ? (saved[f.key] ?? idx) : idx;\u001b[39m\n\u001b[31m  });·\u001b[39m\n\u001b[31m  // Detect if address column is combined\u001b[39m\n\u001b[31m  const addrIdx = suggested.address >= 0 ? suggested.address : findCol(_rawHeaders, ['address','addr']);\u001b[39m\n\u001b[31m  const looksCombo = isCombinedColumn(addrIdx) || isCombinedColumn(suggested.name);\u001b[39m\n\u001b[31m  const comboCol   = looksCombo ? (addrIdx >= 0 ? addrIdx : suggested.name) : -1;\u001b[39m\n\u001b[31m  const comboPreview = comboCol >= 0 && _rawRows[0] ? (_rawRows[0][comboCol] || '').toString().slice(0, 120) : '';·\u001b[39m\n\u001b[31m  colInfoEl.innerHTML = `\u001b[39m\n\u001b[31m    <div style=\\\"background:var(--sur2);border:1px solid var(--bdr);border-radius:var(--r-sm);padding:12px;\\\">\u001b[39m\n\u001b[31m      <div style=\\\"display:flex;align-items:center;justify-content:space-between;margin-bottom:10px;\\\">\u001b[39m\n\u001b[31m        <div style=\\\"font-family:var(--fnt-mono);font-size:.68rem;color:var(--sky);\\\">\u001b[39m\n\u001b[31m          📋 ${filename} &nbsp;·&nbsp; ${_rawRows.length} rows &nbsp;·&nbsp; ${_rawHeaders.length} columns\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m        ${isSaved ? '<span class=\\\"map-saved-badge\\\">✓ mapping remembered</span>' : ''}\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      <div style=\\\"font-size:.68rem;color:var(--t3);font-family:var(--fnt-mono);margin-bottom:10px;word-break:break-all;\\\">\u001b[39m\n\u001b[31m        Columns: <span style=\\\"color:var(--t2);\\\">${_rawHeaders.map((h,i)=>`[${i+1}] ${h||'?'}`).join(' · ')}</span>\u001b[39m\n\u001b[31m      </div>·\u001b[39m\n\u001b[31m      ${looksCombo ? `\u001b[39m\n\u001b[31m      <!-- COMBINED COLUMN DETECTED -->\u001b[39m\n\u001b[31m      <div style=\\\"background:rgba(56,189,248,.08);border:1px solid rgba(56,189,248,.3);\u001b[39m\n\u001b[31m        border-radius:var(--r-sm);padding:10px 12px;margin-bottom:10px;\\\">\u001b[39m\n\u001b[31m        <div style=\\\"font-size:.72rem;color:var(--sky);font-weight:600;margin-bottom:6px;\\\">\u001b[39m\n\u001b[31m          🤖 Combined Address Column Detected\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m        <div style=\\\"font-size:.68rem;color:var(--t2);font-family:var(--fnt-mono);\u001b[39m\n\u001b[31m          white-space:pre-wrap;background:var(--bg);padding:6px 8px;border-radius:4px;\u001b[39m\n\u001b[31m          margin-bottom:8px;max-height:80px;overflow:hidden;\\\">${comboPreview}</div>\u001b[39m\n\u001b[31m        <div style=\\\"font-size:.7rem;color:var(--t3);margin-bottom:8px;\\\">\u001b[39m\n\u001b[31m          AI will extract: Name · Address · City · Pincode · Phone from each cell.\u001b[39m\n\u001b[31m          Runs in browser (Transformers.js) with Ollama fallback.\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m        <div style=\\\"display:flex;align-items:center;gap:8px;\\\">\u001b[39m\n\u001b[31m          <label style=\\\"display:flex;align-items:center;gap:6px;font-size:.72rem;color:var(--t1);cursor:pointer;\\\">\u001b[39m\n\u001b[31m            <input type=\\\"checkbox\\\" id=\\\"useAIParse\\\" checked style=\\\"cursor:pointer;\\\">\u001b[39m\n\u001b[31m            Use AI to parse combined column\u001b[39m\n\u001b[31m          </label>\u001b[39m\n\u001b[31m          <select id=\\\"comboColSel\\\" style=\\\"background:var(--sur2);border:1px solid var(--bdr);\u001b[39m\n\u001b[31m            color:var(--t1);font-family:var(--fnt-mono);font-size:.72rem;padding:4px 8px;\u001b[39m\n\u001b[31m            border-radius:var(--r-sm);outline:none;\\\">\u001b[39m\n\u001b[31m            ${_rawHeaders.map((h,i)=>`<option value=\\\"${i}\\\" ${i===comboCol?'selected':''}>${h||'(col '+(i+1)+')'}</option>`).join('')}\u001b[39m\n\u001b[31m          </select>\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      ` : ''}·\u001b[39m\n\u001b[31m      <div id=\\\"manualMapSection\\\" style=\\\"${looksCombo ? 'display:none;' : ''}\\\">\u001b[39m\n\u001b[31m        <div style=\\\"font-size:.68rem;color:var(--t3);margin-bottom:6px;\\\">\u001b[39m\n\u001b[31m          ${looksCombo ? 'Manual mapping (used when AI parse is off, or for other columns):' : 'Map each field to your column:'}\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m        <table class=\\\"map-table\\\">\u001b[39m\n\u001b[31m          <thead><tr><th>Field</th><th>Your column</th><th>Preview (row 1)</th></tr></thead>\u001b[39m\n\u001b[31m          <tbody>\u001b[39m\n\u001b[31m            ${MAPPING_FIELDS.map(f => {\u001b[39m\n\u001b[31m              const selIdx = suggested[f.key] ?? -1;\u001b[39m\n\u001b[31m              const preview = selIdx >= 0 && _rawRows[0] ? ((_rawRows[0][selIdx]||'').toString().slice(0,40)) : '—';\u001b[39m\n\u001b[31m              return `<tr>\u001b[39m\n\u001b[31m                <td style=\\\"white-space:nowrap;color:var(--t1);font-weight:500;font-size:.75rem;\\\">\u001b[39m\n\u001b[31m                  ${f.label}${f.required ? '<span class=\\\"map-req\\\">*</span>' : ''}\u001b[39m\n\u001b[31m                </td>\u001b[39m\n\u001b[31m                <td>\u001b[39m\n\u001b[31m                  <select id=\\\"mapsel_${f.key}\\\" onchange=\\\"updateMappingPreview('${f.key}')\\\">\u001b[39m\n\u001b[31m                    ${_rawHeaders.map((h,i)=>\u001b[39m\n\u001b[31m                      `<option value=\\\"${i}\\\" ${i===selIdx?'selected':''}>${h||'(col '+(i+1)+')'}</option>`\u001b[39m\n\u001b[31m                    ).join('')}\u001b[39m\n\u001b[31m                    <option value=\\\"-1\\\" ${selIdx===-1?'selected':''}>— not in file —</option>\u001b[39m\n\u001b[31m                  </select>\u001b[39m\n\u001b[31m                </td>\u001b[39m\n\u001b[31m                <td id=\\\"mapprev_${f.key}\\\" style=\\\"font-family:var(--fnt-mono);font-size:.68rem;\u001b[39m\n\u001b[31m                  color:var(--t2);max-width:90px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;\\\">\u001b[39m\n\u001b[31m                  ${preview}\u001b[39m\n\u001b[31m                </td>\u001b[39m\n\u001b[31m              </tr>`;\u001b[39m\n\u001b[31m            }).join('')}\u001b[39m\n\u001b[31m          </tbody>\u001b[39m\n\u001b[31m        </table>\u001b[39m\n\u001b[31m      </div>·\u001b[39m\n\u001b[31m      <!-- Also map docket/courier no column separately — always shown -->\u001b[39m\n\u001b[31m      <div style=\\\"margin-top:10px;padding-top:10px;border-top:1px solid var(--bdr);\\\">\u001b[39m\n\u001b[31m        <div style=\\\"font-size:.68rem;color:var(--t3);margin-bottom:6px;font-family:var(--fnt-mono);\\\">\u001b[39m\n\u001b[31m          DOCKET / COURIER NO. COLUMN (required for tracking):\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m        <div style=\\\"display:flex;gap:8px;align-items:center;\\\">\u001b[39m\n\u001b[31m          <select id=\\\"docketColSel\\\" style=\\\"background:var(--sur2);border:1px solid var(--bdr);\u001b[39m\n\u001b[31m            color:var(--acc);font-family:var(--fnt-mono);font-size:.78rem;padding:6px 10px;\u001b[39m\n\u001b[31m            border-radius:var(--r-sm);outline:none;flex:1;\\\">\u001b[39m\n\u001b[31m            <option value=\\\"-1\\\">— Docket not in file —</option>\u001b[39m\n\u001b[31m            ${_rawHeaders.map((h,i)=>`<option value=\\\"${i}\\\" ${i===(suggested.docket??-1)?'selected':''}>${h||'(col '+(i+1)+')'}</option>`).join('')}\u001b[39m\n\u001b[31m          </select>\u001b[39m\n\u001b[31m          <span id=\\\"docketPrev\\\" style=\\\"font-family:var(--fnt-mono);font-size:.72rem;color:var(--acc);\\\">\u001b[39m\n\u001b[31m            ${suggested.docket >= 0 && _rawRows[0] ? (_rawRows[0][suggested.docket]||'') : ''}\u001b[39m\n\u001b[31m          </span>\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m      </div>·\u001b[39m\n\u001b[31m      <div class=\\\"btn-row\\\" style=\\\"margin-top:12px;\\\">\u001b[39m\n\u001b[31m        <button class=\\\"btn btn-ghost\\\" style=\\\"font-size:.78rem;\\\" onclick=\\\"clearStores()\\\">✕ Cancel</button>\u001b[39m\n\u001b[31m        <button class=\\\"btn btn-orange\\\" style=\\\"font-size:.78rem;\\\" onclick=\\\"applyMapping()\\\">✓ Apply Mapping</button>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m  `;\u001b[39m\n\u001b[31m  document.getElementById('storeTable').innerHTML = '';\u001b[39m\n\u001b[31m  document.getElementById('storeAddPanel').style.display = 'none';·\u001b[39m\n\u001b[31m  // Update docket preview on change\u001b[39m\n\u001b[31m  const docketSel = document.getElementById('docketColSel');\u001b[39m\n\u001b[31m  if (docketSel) docketSel.addEventListener('change', () => {\u001b[39m\n\u001b[31m    const idx = parseInt(docketSel.value);\u001b[39m\n\u001b[31m    const prev = document.getElementById('docketPrev');\u001b[39m\n\u001b[31m    if (prev) prev.textContent = idx >= 0 && _rawRows[0] ? (_rawRows[0][idx]||'') : '';\u001b[39m\n\u001b[31m  });·\u001b[39m\n\u001b[31m  // Toggle manual map opacity when AI checkbox changes\u001b[39m\n\u001b[31m  const aiChk = document.getElementById('useAIParse');\u001b[39m\n\u001b[31m  const manSec = document.getElementById('manualMapSection');\u001b[39m\n\u001b[31m  if (aiChk && manSec) {\u001b[39m\n\u001b[31m    aiChk.addEventListener('change', () => {\u001b[39m\n\u001b[31m      manSec.style.display = aiChk.checked ? 'none' : 'block';\u001b[39m\n\u001b[31m    });\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction updateMappingPreview(key) {\u001b[39m\n\u001b[31m  const sel = document.getElementById('mapsel_' + key);\u001b[39m\n\u001b[31m  if (!sel) return;\u001b[39m\n\u001b[31m  const idx = parseInt(sel.value);\u001b[39m\n\u001b[31m  const prev = document.getElementById('mapprev_' + key);\u001b[39m\n\u001b[31m  if (prev) prev.textContent = (idx >= 0 && _rawRows[0]) ? ((_rawRows[0][idx]||'').toString().slice(0,40)) : '—';\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// ── REGEX-FIRST ADDRESS PARSER ───────────────────────────────────────────\u001b[39m\n\u001b[31m// Improved Indian address parser\u001b[39m\n\u001b[31m// Handles formats seen in real BTL dispatch files\u001b[39m\n\u001b[31mfunction parseAddressRegex(raw) {\u001b[39m\n\u001b[31m  const text = raw.toString().replace(/\\\\r/g, '').trim();\u001b[39m\n\u001b[31m  const lines = text.split('\\\\n').map(l => l.trim()).filter(Boolean);\u001b[39m\n\u001b[31m  const result = { name:'', address:'', city:'', pincode:'', phone:'', confidence:0 };·\u001b[39m\n\u001b[31m  // ── PHONE: Indian mobile 6-9 prefix, 10 digits ──────────────────────────\u001b[39m\n\u001b[31m  // Also handles \\\"Phone : 9876543210\\\", \\\"M.No.-9876543210\\\", \\\"Mobile - 9876543210\\\"\u001b[39m\n\u001b[31m  const phoneMatch = text.match(/(?:Phone|Mobile|Mob|M\\\\.No\\\\.?|Ph\\\\.?)?\\\\s*[:\\\\-]?\\\\s*\\\\b([6-9]\\\\d{9})\\\\b/i);\u001b[39m\n\u001b[31m  if (phoneMatch) result.phone = phoneMatch[1];·\u001b[39m\n\u001b[31m  // ── PINCODE: 6-digit standalone number ──────────────────────────────────\u001b[39m\n\u001b[31m  // Must not be part of a longer number (e.g. not an account number)\u001b[39m\n\u001b[31m  const pinMatch = text.match(/(?<![\\\\d])([1-9]\\\\d{5})(?![\\\\d])/);\u001b[39m\n\u001b[31m  if (pinMatch) result.pincode = pinMatch[1];·\u001b[39m\n\u001b[31m  // ── CITY: the token immediately before the pincode ───────────────────────\u001b[39m\n\u001b[31m  const indianStates = ['rajasthan','gujarat','maharashtra','punjab','haryana',\u001b[39m\n\u001b[31m    'uttar pradesh','madhya pradesh','karnataka','tamil nadu','andhra pradesh',\u001b[39m\n\u001b[31m    'telangana','kerala','west bengal','bihar','odisha','jharkhand','assam',\u001b[39m\n\u001b[31m    'himachal pradesh','uttarakhand','chhattisgarh','goa','delhi','chandigarh',\u001b[39m\n\u001b[31m    'raj.','raj'];\u001b[39m\n\u001b[31m  if (result.pincode) {\u001b[39m\n\u001b[31m    const pinPos = text.indexOf(result.pincode);\u001b[39m\n\u001b[31m    const beforePin = text.slice(0, pinPos);\u001b[39m\n\u001b[31m    // Strategy 1: split by comma/newline, walk backwards skipping state names\u001b[39m\n\u001b[31m    const chunks = beforePin.split(/[,\\\\n]/).map(c => c.trim()).filter(Boolean);\u001b[39m\n\u001b[31m    for (let i = chunks.length - 1; i >= 0; i--) {\u001b[39m\n\u001b[31m      const chunk = chunks[i].replace(/\\\\d+/g,'').replace(/[()]/g,'').replace(/[.-]/g,'').trim();\u001b[39m\n\u001b[31m      const lower = chunk.toLowerCase();\u001b[39m\n\u001b[31m      if (!chunk || chunk.length < 2) continue;\u001b[39m\n\u001b[31m      if (indianStates.includes(lower)) continue;\u001b[39m\n\u001b[31m      // Skip chunks that look like addresses (contain numbers or long strings)\u001b[39m\n\u001b[31m      if (/\\\\d/.test(chunk)) continue;\u001b[39m\n\u001b[31m      result.city = chunk;\u001b[39m\n\u001b[31m      break;\u001b[39m\n\u001b[31m    }\u001b[39m\n\u001b[31m    // Strategy 2: if still no city, look for last capitalised word before pincode\u001b[39m\n\u001b[31m    // handles \\\"compound paota Jodhpur 342006\\\" → Jodhpur\u001b[39m\n\u001b[31m    if (!result.city) {\u001b[39m\n\u001b[31m      const wordsBefore = beforePin.replace(/[,\\\\n]/g,' ').trim().split(/\\\\s+/);\u001b[39m\n\u001b[31m      for (let i = wordsBefore.length - 1; i >= 0; i--) {\u001b[39m\n\u001b[31m        const w = wordsBefore[i].replace(/[^a-zA-Z]/g,'');\u001b[39m\n\u001b[31m        if (w.length < 3) continue;\u001b[39m\n\u001b[31m        if (indianStates.includes(w.toLowerCase())) continue;\u001b[39m\n\u001b[31m        // Prefer words that start with capital letter (likely a proper noun/city)\u001b[39m\n\u001b[31m        if (/^[A-Z]/.test(w)) { result.city = w; break; }\u001b[39m\n\u001b[31m      }\u001b[39m\n\u001b[31m    }\u001b[39m\n\u001b[31m  }·\u001b[39m\n\u001b[31m  // ── NAME: first meaningful line, extract shop name ───────────────────────\u001b[39m\n\u001b[31m  // Patterns:\u001b[39m\n\u001b[31m  //   \\\"Sanwar Mal Saini, C/o R G COMPUTERS\\\" → name = \\\"R G COMPUTERS\\\"\u001b[39m\n\u001b[31m  //   \\\"SANWAR MAL SAINI, C/o ADVANCE TECHNOLOGIES\\\" → name = \\\"ADVANCE TECHNOLOGIES\\\"\u001b[39m\n\u001b[31m  //   \\\"Sunil Vaishnav\\\" → name = \\\"Sunil Vaishnav\\\" (no C/o)\u001b[39m\n\u001b[31m  //   \\\"Baltej Singh, Professional computer care\\\" → name = \\\"Professional computer care\\\"\u001b[39m\n\u001b[31m  if (lines.length > 0) {\u001b[39m\n\u001b[31m    const firstLine = lines[0];\u001b[39m\n\u001b[31m    // Check for C/o or C/O pattern — shop name comes after it\u001b[39m\n\u001b[31m    const coMatch = firstLine.match(/C\\\\/?[Oo]\\\\.?\\\\s+(.+)/);\u001b[39m\n\u001b[31m    if (coMatch) {\u001b[39m\n\u001b[31m      result.name = coMatch[1].trim();\u001b[39m\n\u001b[31m    } else {\u001b[39m\n\u001b[31m      // Check if there is a comma — second part might be the shop name\u001b[39m\n\u001b[31m      const parts = firstLine.split(',').map(p => p.trim());\u001b[39m\n\u001b[31m      if (parts.length >= 2 && parts[1].length > 3) {\u001b[39m\n\u001b[31m        // Heuristic: if second part looks like a business name (has uppercase or >5 chars)\u001b[39m\n\u001b[31m        const hasUpperOrLong = /[A-Z]{2,}/.test(parts[1]) || parts[1].length > 8;\u001b[39m\n\u001b[31m        result.name = hasUpperOrLong ? parts[1] : parts[0];\u001b[39m\n\u001b[31m      } else {\u001b[39m\n\u001b[31m        result.name = parts[0];\u001b[39m\n\u001b[31m      }\u001b[39m\n\u001b[31m    }\u001b[39m\n\u001b[31m  }·\u001b[39m\n\u001b[31m  // ── ADDRESS: lines between name and phone/pincode line ───────────────────\u001b[39m\n\u001b[31m  const phoneLineIdx = lines.findIndex(l => result.phone && l.includes(result.phone));\u001b[39m\n\u001b[31m  const addrLines = lines.filter((l, i) => {\u001b[39m\n\u001b[31m    if (i === 0) return false;\u001b[39m\n\u001b[31m    if (i === phoneLineIdx) return false;\u001b[39m\n\u001b[31m    // Exclude lines that are just the phone number\u001b[39m\n\u001b[31m    if (/^[6-9]\\\\d{9}$/.test(l.replace(/\\\\s/g,''))) return false;\u001b[39m\n\u001b[31m    return true;\u001b[39m\n\u001b[31m  });\u001b[39m\n\u001b[31m  result.address = addrLines.join(', ').replace(/,\\\\s*,/g, ',').trim();·\u001b[39m\n\u001b[31m  // ── CONFIDENCE: simple score based on what we got ────────────────────────\u001b[39m\n\u001b[31m  let conf = 0;\u001b[39m\n\u001b[31m  if (result.name)    conf += 30;\u001b[39m\n\u001b[31m  if (result.city)    conf += 25;\u001b[39m\n\u001b[31m  if (result.pincode) conf += 25;\u001b[39m\n\u001b[31m  if (result.phone)   conf += 20;\u001b[39m\n\u001b[31m  result.confidence = conf;·\u001b[39m\n\u001b[31m  return result;\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// ── AI ADDRESS PARSER (Ollama fallback) ──────────────────────────────────\u001b[39m\n\u001b[31m// Fetches past corrections from localStorage to improve prompt accuracy\u001b[39m\n\u001b[31mfunction getPastCorrections() {\u001b[39m\n\u001b[31m  try {\u001b[39m\n\u001b[31m    const log = JSON.parse(localStorage.getItem('360lm_parse_corrections') || '[]');\u001b[39m\n\u001b[31m    return log.slice(-10); // last 10 corrections as examples\u001b[39m\n\u001b[31m  } catch(e) { return []; }\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction saveCorrectionLocal(entry) {\u001b[39m\n\u001b[31m  try {\u001b[39m\n\u001b[31m    const log = JSON.parse(localStorage.getItem('360lm_parse_corrections') || '[]');\u001b[39m\n\u001b[31m    log.push(entry);\u001b[39m\n\u001b[31m    // Keep only last 50\u001b[39m\n\u001b[31m    localStorage.setItem('360lm_parse_corrections', JSON.stringify(log.slice(-50)));\u001b[39m\n\u001b[31m  } catch(e) {}\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31masync function parseAddressWithOllama(raw) {\u001b[39m\n\u001b[31m  // Calls the server-side /dispatch-ai/address proxy (avoids CORS + port 11434 issues)\u001b[39m\n\u001b[31m  try {\u001b[39m\n\u001b[31m    const resp = await fetch('/dispatch-ai/address', {\u001b[39m\n\u001b[31m      method: 'POST',\u001b[39m\n\u001b[31m      headers: { 'Content-Type': 'application/json' },\u001b[39m\n\u001b[31m      body: JSON.stringify({ text: raw }),\u001b[39m\n\u001b[31m      signal: AbortSignal.timeout(120000)\u001b[39m\n\u001b[31m    });\u001b[39m\n\u001b[31m    const data = await resp.json();\u001b[39m\n\u001b[31m    if (data.success) return data;\u001b[39m\n\u001b[31m  } catch(e) {}\u001b[39m\n\u001b[31m  return null;\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31masync function parseAddressCell(raw) {\u001b[39m\n\u001b[31m  // Step 1: regex — always runs first, no network call\u001b[39m\n\u001b[31m  const regex = parseAddressRegex(raw);\u001b[39m\n\u001b[31m  if (regex.name) return { ...regex, source: 'regex' };·\u001b[39m\n\u001b[31m  // Step 2: Ollama only if regex got nothing AND Ollama is configured\u001b[39m\n\u001b[31m  // Uses a 3-second timeout to prevent browser hang\u001b[39m\n\u001b[31m  if (window.appConfig.ocr_mode !== 'transformers') {\u001b[39m\n\u001b[31m    try {\u001b[39m\n\u001b[31m      const ai = await Promise.race([\u001b[39m\n\u001b[31m        parseAddressWithOllama(raw),\u001b[39m\n\u001b[31m        new Promise((_, reject) => setTimeout(() => reject(new Error('timeout')), 120000))\u001b[39m\n\u001b[31m      ]);\u001b[39m\n\u001b[31m      if (ai && ai.name) return { ...ai, source: 'ollama' };\u001b[39m\n\u001b[31m    } catch(e) { /* timeout or error — continue to fallback */ }\u001b[39m\n\u001b[31m  }·\u001b[39m\n\u001b[31m  // Step 3: fallback — first line as name\u001b[39m\n\u001b[31m  const lines = raw.toString().split('\\\\n').map(l=>l.trim()).filter(Boolean);\u001b[39m\n\u001b[31m  return { name: lines[0]||raw.slice(0,40), address: raw, city:'', pincode:'', phone:'', source:'fallback' };\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31masync function applyMapping() {\u001b[39m\n\u001b[31m  const useAI   = document.getElementById('useAIParse')?.checked ?? false;\u001b[39m\n\u001b[31m  const comboSel = document.getElementById('comboColSel');\u001b[39m\n\u001b[31m  const comboIdx = comboSel ? parseInt(comboSel.value) : -1;\u001b[39m\n\u001b[31m  const docketIdx = parseInt(document.getElementById('docketColSel')?.value ?? -1);·\u001b[39m\n\u001b[31m  // Build manual mapping as fallback\u001b[39m\n\u001b[31m  const mapping = {};\u001b[39m\n\u001b[31m  MAPPING_FIELDS.forEach(f => {\u001b[39m\n\u001b[31m    const sel = document.getElementById('mapsel_' + f.key);\u001b[39m\n\u001b[31m    mapping[f.key] = sel ? parseInt(sel.value) : -1;\u001b[39m\n\u001b[31m  });\u001b[39m\n\u001b[31m  // Override docket from dedicated selector\u001b[39m\n\u001b[31m  mapping.docket = docketIdx;·\u001b[39m\n\u001b[31m  // Determine if we have a valid base for stores\u001b[39m\n\u001b[31m  const nameIdx = useAI && comboIdx >= 0 ? comboIdx : mapping.name;\u001b[39m\n\u001b[31m  if (nameIdx === -1 && !useAI) { showToast('Store Name column is required', 'err'); return; }·\u001b[39m\n\u001b[31m  // Save mapping\u001b[39m\n\u001b[31m  try { localStorage.setItem(getMappingKey(), JSON.stringify({...mapping, _useAI: useAI, _comboIdx: comboIdx})); } catch(e) {}·\u001b[39m\n\u001b[31m  if (useAI && comboIdx >= 0) {\u001b[39m\n\u001b[31m    // AI path — parse each combined cell\u001b[39m\n\u001b[31m    showToast('AI parsing addresses...', 'info');\u001b[39m\n\u001b[31m    showOCRWorking(true, `Parsing 0/${_rawRows.length} addresses...`);\u001b[39m\n\u001b[31m    stores = [];\u001b[39m\n\u001b[31m    for (let i = 0; i < _rawRows.length; i++) {\u001b[39m\n\u001b[31m      const r = _rawRows[i];\u001b[39m\n\u001b[31m      const raw = (r[comboIdx] || '').toString().trim();\u001b[39m\n\u001b[31m      if (!raw) continue;\u001b[39m\n\u001b[31m      showOCRWorking(true, `Parsing ${i+1}/${_rawRows.length} — ${raw.slice(0,30)}...`);\u001b[39m\n\u001b[31m      // Yield to browser every row to prevent UI freeze\u001b[39m\n\u001b[31m      await new Promise(r => setTimeout(r, 0));\u001b[39m\n\u001b[31m      const parsed = await parseAddressCell(raw);\u001b[39m\n\u001b[31m      stores.push({\u001b[39m\n\u001b[31m        id: i,\u001b[39m\n\u001b[31m        name:    parsed.name    || raw.slice(0,40),\u001b[39m\n\u001b[31m        city:    parsed.city    || '',\u001b[39m\n\u001b[31m        phone:   parsed.phone   || '',\u001b[39m\n\u001b[31m        address: parsed.address || raw,\u001b[39m\n\u001b[31m        pincode: parsed.pincode || '',\u001b[39m\n\u001b[31m        docket:  docketIdx >= 0 ? (r[docketIdx]||'').toString().trim() : '',\u001b[39m\n\u001b[31m        _parseSource: parsed.source,\u001b[39m\n\u001b[31m        _confidence:  parsed.confidence || 0,\u001b[39m\n\u001b[31m        _rawCell:     raw,\u001b[39m\n\u001b[31m        _origName:    parsed.name || '',\u001b[39m\n\u001b[31m        _origCity:    parsed.city || '',\u001b[39m\n\u001b[31m        matched: false\u001b[39m\n\u001b[31m      });\u001b[39m\n\u001b[31m    }\u001b[39m\n\u001b[31m    showOCRWorking(false);\u001b[39m\n\u001b[31m  } else {\u001b[39m\n\u001b[31m    // Manual mapping path\u001b[39m\n\u001b[31m    stores = _rawRows\u001b[39m\n\u001b[31m      .filter(r => r[nameIdx] && (r[nameIdx]+'').trim())\u001b[39m\n\u001b[31m      .map((r, i) => ({\u001b[39m\n\u001b[31m        id: i,\u001b[39m\n\u001b[31m        name:    mapping.name    >= 0 ? (r[mapping.name]   ||'').toString().trim() : '',\u001b[39m\n\u001b[31m        city:    mapping.city    >= 0 ? (r[mapping.city]   ||'').toString().trim() : '',\u001b[39m\n\u001b[31m        phone:   mapping.phone   >= 0 ? (r[mapping.phone]  ||'').toString().trim() : '',\u001b[39m\n\u001b[31m        docket:  mapping.docket  >= 0 ? (r[mapping.docket] ||'').toString().trim() : '',\u001b[39m\n\u001b[31m        address: mapping.address >= 0 ? (r[mapping.address]||'').toString().trim() : '',\u001b[39m\n\u001b[31m        pincode: mapping.pincode >= 0 ? (r[mapping.pincode]||'').toString().trim() : '',\u001b[39m\n\u001b[31m        matched: false\u001b[39m\n\u001b[31m      }));\u001b[39m\n\u001b[31m  }·\u001b[39m\n\u001b[31m  document.getElementById('storeCount').textContent = `${stores.length} stores loaded`;·\u001b[39m\n\u001b[31m  if (useAI && comboIdx >= 0) {\u001b[39m\n\u001b[31m    // AI path — show row-by-row review panel before adding consignees\u001b[39m\n\u001b[31m    showAddressReviewPanel();\u001b[39m\n\u001b[31m  } else {\u001b[39m\n\u001b[31m    // Manual mapping path — go straight to store table + add panel\u001b[39m\n\u001b[31m    renderStoreTable(mapping.docket >= 0 || docketIdx >= 0);\u001b[39m\n\u001b[31m    const addPanel = document.getElementById('storeAddPanel');\u001b[39m\n\u001b[31m    const addBtn   = document.getElementById('storeAddBtn');\u001b[39m\n\u001b[31m    const addTitle = document.getElementById('storeAddTitle');\u001b[39m\n\u001b[31m    const hasDockets = stores.some(s => s.docket);\u001b[39m\n\u001b[31m    addTitle.textContent = hasDockets\u001b[39m\n\u001b[31m      ? `${stores.filter(s=>s.docket).length} stores with dockets — add as consignees?`\u001b[39m\n\u001b[31m      : `Add ${stores.length} stores as consignees?`;\u001b[39m\n\u001b[31m    addBtn.textContent = `✓ Add ${stores.length} as Consignees`;\u001b[39m\n\u001b[31m    addPanel.style.display = 'block';\u001b[39m\n\u001b[31m    showToast(`${stores.length} stores mapped ✓`, 'ok');\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31m// ADDRESS REVIEW PANEL\u001b[39m\n\u001b[31m// Row-by-row review of AI-parsed addresses before consignees created\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31mfunction showAddressReviewPanel() {\u001b[39m\n\u001b[31m  const pane = document.getElementById('addrReviewPane');\u001b[39m\n\u001b[31m  if (!pane) return;\u001b[39m\n\u001b[31m  renderAddressReviewRows();\u001b[39m\n\u001b[31m  pane.style.display = 'block';\u001b[39m\n\u001b[31m  pane.scrollTop = 0;\u001b[39m\n\u001b[31m  showToast(`Review ${stores.length} parsed addresses`, 'info');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction renderAddressReviewRows() {\u001b[39m\n\u001b[31m  const list = document.getElementById('addrReviewList');\u001b[39m\n\u001b[31m  const summary = document.getElementById('addrReviewSummary');\u001b[39m\n\u001b[31m  if (!list) return;·\u001b[39m\n\u001b[31m  const highConf  = stores.filter(s => (s._confidence||0) >= 80).length;\u001b[39m\n\u001b[31m  const medConf   = stores.filter(s => (s._confidence||0) >= 50 && (s._confidence||0) < 80).length;\u001b[39m\n\u001b[31m  const lowConf   = stores.filter(s => (s._confidence||0) < 50).length;·\u001b[39m\n\u001b[31m  summary.innerHTML = `\u001b[39m\n\u001b[31m    <div class=\\\"vs-item\\\"><div class=\\\"vs-val\\\" style=\\\"color:var(--t1);\\\">${stores.length}</div><div class=\\\"vs-lbl\\\">Total</div></div>\u001b[39m\n\u001b[31m    <div class=\\\"vs-item\\\"><div class=\\\"vs-val\\\" style=\\\"color:var(--grn);\\\">${highConf}</div><div class=\\\"vs-lbl\\\">High conf</div></div>\u001b[39m\n\u001b[31m    <div class=\\\"vs-item\\\"><div class=\\\"vs-val\\\" style=\\\"color:var(--ylw);\\\">${medConf}</div><div class=\\\"vs-lbl\\\">Review</div></div>\u001b[39m\n\u001b[31m    <div class=\\\"vs-item\\\"><div class=\\\"vs-val\\\" style=\\\"color:var(--red);\\\">${lowConf}</div><div class=\\\"vs-lbl\\\">Check</div></div>\u001b[39m\n\u001b[31m    <div class=\\\"vs-item\\\" style=\\\"margin-left:auto;\\\">\u001b[39m\n\u001b[31m      <button class=\\\"btn-sm primary\\\" onclick=\\\"approveAllAddresses()\\\" style=\\\"font-size:.7rem;\\\">✓ Approve All</button>\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m  `;·\u001b[39m\n\u001b[31m  list.innerHTML = stores.map((s, i) => {\u001b[39m\n\u001b[31m    const conf = s._confidence || 0;\u001b[39m\n\u001b[31m    const confClass = conf >= 80 ? 'vf-ok' : conf >= 50 ? 'vf-nodocket' : 'vf-dupe';\u001b[39m\n\u001b[31m    const confLabel = conf >= 80 ? `${conf}% ✓` : conf >= 50 ? `${conf}% review` : `${conf}% check`;\u001b[39m\n\u001b[31m    const rowBorder = conf >= 80 ? 'var(--grn)' : conf >= 50 ? 'var(--ylw)' : 'var(--red)';·\u001b[39m\n\u001b[31m    return `\u001b[39m\n\u001b[31m    <div class=\\\"verify-row\\\" id=\\\"arow_${i}\\\" style=\\\"border-left:3px solid ${rowBorder};\\\">\u001b[39m\n\u001b[31m      <div class=\\\"verify-row-hdr\\\">\u001b[39m\n\u001b[31m        <span class=\\\"verify-num\\\">#${i+1}</span>\u001b[39m\n\u001b[31m        <span class=\\\"verify-store\\\" style=\\\"font-size:.78rem;color:var(--t2);font-weight:400;\u001b[39m\n\u001b[31m          font-family:var(--fnt-mono);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;flex:1;\\\"\u001b[39m\n\u001b[31m          title=\\\"${(s._rawCell||'').replace(/\\\"/g,\\\"'\\\")}\\\">\u001b[39m\n\u001b[31m          ${(s._rawCell||'').slice(0,50).split('\\\\n').join(' ↵ ')}       <span class=\\\"verify-flag ${confClass}\\\" style=\\\"flex-shrink:0;margin-right:6px;\\\">${confLabel} · ${s._parseSource||'?'}</span>\u001b[39m\n\u001b[31m        <button onclick=\\\"deleteReviewRow(${i})\\\"\u001b[39m\n\u001b[31m          style=\\\"background:none;border:1px solid rgba(239,68,68,.4);color:var(--red);\u001b[39m\n\u001b[31m          font-size:.65rem;padding:2px 8px;border-radius:6px;cursor:pointer;\u001b[39m\n\u001b[31m          font-family:var(--fnt-mono);flex-shrink:0;transition:background .2s;\\\"\u001b[39m\n\u001b[31m          onmouseover=\\\"this.style.background='rgba(239,68,68,.15)'\\\"\u001b[39m\n\u001b[31m          onmouseout=\\\"this.style.background='none'\\\">✕ Remove</button>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      <div class=\\\"verify-fields\\\" style=\\\"grid-template-columns:1fr 1fr 1fr 1fr 1fr;\\\">\u001b[39m\n\u001b[31m        <div class=\\\"verify-field\\\">\u001b[39m\n\u001b[31m          <label>Shop / Name <span style=\\\"color:var(--acc);\\\">*</span></label>\u001b[39m\n\u001b[31m          <input type=\\\"text\\\" value=\\\"${s.name||''}\\\" id=\\\"ar_name_${i}\\\"\u001b[39m\n\u001b[31m            oninput=\\\"updateAddrField(${i},'name',this)\\\"\u001b[39m\n\u001b[31m            style=\\\"${!s.name ? 'border-color:var(--red);' : ''}\\\">\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m        <div class=\\\"verify-field\\\">\u001b[39m\n\u001b[31m          <label>City</label>\u001b[39m\n\u001b[31m          <input type=\\\"text\\\" value=\\\"${s.city||''}\\\" id=\\\"ar_city_${i}\\\"\u001b[39m\n\u001b[31m            oninput=\\\"updateAddrField(${i},'city',this)\\\"\u001b[39m\n\u001b[31m            style=\\\"${!s.city ? 'border-color:var(--ylw);' : ''}\\\">\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m        <div class=\\\"verify-field\\\">\u001b[39m\n\u001b[31m          <label>Docket / Courier No.</label>\u001b[39m\n\u001b[31m          <input type=\\\"text\\\" value=\\\"${s.docket||''}\\\" id=\\\"ar_docket_${i}\\\"\u001b[39m\n\u001b[31m            oninput=\\\"updateAddrField(${i},'docket',this)\\\"\u001b[39m\n\u001b[31m            style=\\\"font-family:var(--fnt-mono);${!s.docket?'border-color:var(--ylw);':'color:var(--acc);'}\\\">\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m        <div class=\\\"verify-field\\\">\u001b[39m\n\u001b[31m          <label>Phone</label>\u001b[39m\n\u001b[31m          <input type=\\\"tel\\\" value=\\\"${s.phone||''}\\\" id=\\\"ar_phone_${i}\\\"\u001b[39m\n\u001b[31m            oninput=\\\"updateAddrField(${i},'phone',this)\\\">\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m        <div class=\\\"verify-field\\\">\u001b[39m\n\u001b[31m          <label>Pincode</label>\u001b[39m\n\u001b[31m          <input type=\\\"text\\\" value=\\\"${s.pincode||''}\\\" id=\\\"ar_pin_${i}\\\"\u001b[39m\n\u001b[31m            oninput=\\\"updateAddrField(${i},'pincode',this)\\\">\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      <div style=\\\"font-size:.65rem;color:var(--t3);font-family:var(--fnt-mono);\u001b[39m\n\u001b[31m        margin-top:4px;padding:4px 6px;background:var(--bg);border-radius:4px;word-break:break-all;\\\">\u001b[39m\n\u001b[31m        📋 ${(s._rawCell||'').split('\\\\n').join(' | ')}\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m    </div>`;\u001b[39m\n\u001b[31m  }).join('');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction updateAddrField(i, field, input) {\u001b[39m\n\u001b[31m  if (!stores[i]) return;\u001b[39m\n\u001b[31m  stores[i][field] = input.value.trim();\u001b[39m\n\u001b[31m  // Recompute confidence\u001b[39m\n\u001b[31m  let conf = 0;\u001b[39m\n\u001b[31m  if (stores[i].name)    conf += 30;\u001b[39m\n\u001b[31m  if (stores[i].city)    conf += 25;\u001b[39m\n\u001b[31m  if (stores[i].pincode) conf += 25;\u001b[39m\n\u001b[31m  if (stores[i].phone)   conf += 20;\u001b[39m\n\u001b[31m  stores[i]._confidence = conf;\u001b[39m\n\u001b[31m  input.classList.toggle('changed', true);\u001b[39m\n\u001b[31m  // Update border color on row\u001b[39m\n\u001b[31m  const row = document.getElementById('arow_' + i);\u001b[39m\n\u001b[31m  if (row) row.style.borderLeftColor = conf >= 80 ? 'var(--grn)' : conf >= 50 ? 'var(--ylw)' : 'var(--red)';\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction deleteReviewRow(i) {\u001b[39m\n\u001b[31m  // Mark as deleted — splice from stores array and re-render\u001b[39m\n\u001b[31m  stores.splice(i, 1);\u001b[39m\n\u001b[31m  // Re-assign IDs\u001b[39m\n\u001b[31m  stores.forEach((s, idx) => { s.id = idx; });\u001b[39m\n\u001b[31m  renderAddressReviewRows();\u001b[39m\n\u001b[31m  document.getElementById('storeCount').textContent = `${stores.length} stores loaded`;\u001b[39m\n\u001b[31m  showToast(`Row removed — ${stores.length} remaining`, 'info');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction backToMapping() {\u001b[39m\n\u001b[31m  // Hide review pane — store edits are preserved in stores[] array\u001b[39m\n\u001b[31m  document.getElementById('addrReviewPane').style.display = 'none';\u001b[39m\n\u001b[31m  // Show mapping UI again with current file\u001b[39m\n\u001b[31m  document.getElementById('storeTableArea').style.display = 'block';\u001b[39m\n\u001b[31m  document.getElementById('storeColInfo').style.display = 'block';\u001b[39m\n\u001b[31m  showToast('Your edits are preserved — re-apply mapping to reload', 'info');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction approveAllAddresses() {\u001b[39m\n\u001b[31m  stores.forEach((s, i) => { s._reviewed = true; });\u001b[39m\n\u001b[31m  document.getElementById('addrReviewPane').style.display = 'none';\u001b[39m\n\u001b[31m  applyReviewedStores();\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction confirmAddressReview() {\u001b[39m\n\u001b[31m  // Collect any pending edits, log corrections, close panel\u001b[39m\n\u001b[31m  const corrections = [];\u001b[39m\n\u001b[31m  stores.forEach((s, i) => {\u001b[39m\n\u001b[31m    const origName = s._origName || '';\u001b[39m\n\u001b[31m    const origCity = s._origCity || '';\u001b[39m\n\u001b[31m    if ((origName && origName !== s.name) || (origCity && origCity !== s.city)) {\u001b[39m\n\u001b[31m      corrections.push({ raw: s._rawCell||'', name: s.name, city: s.city, pincode: s.pincode });\u001b[39m\n\u001b[31m    }\u001b[39m\n\u001b[31m  });\u001b[39m\n\u001b[31m  // Save corrections locally for future Ollama prompts\u001b[39m\n\u001b[31m  corrections.forEach(c => saveCorrectionLocal(c));\u001b[39m\n\u001b[31m  // Log to Sheets asynchronously (fire and forget)\u001b[39m\n\u001b[31m  if (corrections.length > 0 && window.appConfig.webhook_url) {\u001b[39m\n\u001b[31m    logParseCorrectionsToSheets(corrections);\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m  document.getElementById('addrReviewPane').style.display = 'none';\u001b[39m\n\u001b[31m  applyReviewedStores();\u001b[39m\n\u001b[31m  showToast(`${corrections.length > 0 ? corrections.length + ' corrections logged · ' : ''}${stores.length} stores ready ✓`, 'ok');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// Clean street address: remove city, pincode, phone if they appear in address field\u001b[39m\n\u001b[31mfunction cleanAddress(addr, city, pincode, phone) {\u001b[39m\n\u001b[31m  if (!addr) return addr;\u001b[39m\n\u001b[31m  let s = addr;\u001b[39m\n\u001b[31m  // Remove phone\u001b[39m\n\u001b[31m  if (phone) s = s.replace(new RegExp(phone.replace(/[.*+?^${}()|[\\\\]\\\\\\\\]/g,'\\\\\\\\$&'), 'g'), '').trim();\u001b[39m\n\u001b[31m  // Remove pincode\u001b[39m\n\u001b[31m  if (pincode) s = s.replace(new RegExp('\\\\\\\\b' + pincode + '\\\\\\\\b', 'g'), '').trim();\u001b[39m\n\u001b[31m  // Remove city (case-insensitive)\u001b[39m\n\u001b[31m  if (city) s = s.replace(new RegExp('\\\\\\\\b' + city.replace(/[.*+?^${}()|[\\\\]\\\\\\\\]/g,'\\\\\\\\$&') + '\\\\\\\\b', 'ig'), '').trim();\u001b[39m\n\u001b[31m  // Remove state names and trailing noise\u001b[39m\n\u001b[31m  const noise = /\\\\b(rajasthan|gujarat|maharashtra|punjab|haryana|uttar pradesh|delhi|raj\\\\.)\\\\b/ig;\u001b[39m\n\u001b[31m  s = s.replace(noise, '');\u001b[39m\n\u001b[31m  // Remove \\\"Mobile :\\\", \\\"Phone :\\\", \\\"M.No.\\\" prefixes and dangling numbers\u001b[39m\n\u001b[31m  s = s.replace(/(?:Mobile|Phone|Mob|M\\\\.No\\\\.?|Ph\\\\.?)\\\\s*[:\\\\-]?\\\\s*/ig, '');\u001b[39m\n\u001b[31m  // Clean up multiple commas/spaces\u001b[39m\n\u001b[31m  s = s.replace(/,\\\\s*,/g, ',').replace(/\\\\s{2,}/g, ' ').replace(/^[\\\\s,]+|[\\\\s,]+$/g, '');\u001b[39m\n\u001b[31m  return s;\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction applyReviewedStores() {\u001b[39m\n\u001b[31m  // Clean address fields before loading stores\u001b[39m\n\u001b[31m  stores.forEach(s => {\u001b[39m\n\u001b[31m    s.address = cleanAddress(s.address, s.city, s.pincode, s.phone);\u001b[39m\n\u001b[31m  });\u001b[39m\n\u001b[31m  renderStoreTable(stores.some(s => s.docket));\u001b[39m\n\u001b[31m  const addPanel = document.getElementById('storeAddPanel');\u001b[39m\n\u001b[31m  const addTitle = document.getElementById('storeAddTitle');\u001b[39m\n\u001b[31m  const addBtn   = document.getElementById('storeAddBtn');\u001b[39m\n\u001b[31m  const hasDockets = stores.some(s => s.docket);\u001b[39m\n\u001b[31m  addTitle.textContent = hasDockets\u001b[39m\n\u001b[31m    ? `${stores.filter(s=>s.docket).length} stores with dockets — add as consignees?`\u001b[39m\n\u001b[31m    : `Add ${stores.length} stores as consignees?`;\u001b[39m\n\u001b[31m  addBtn.textContent = `✓ Add ${stores.length} as Consignees`;\u001b[39m\n\u001b[31m  addPanel.style.display = 'block';\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31masync function logParseCorrectionsToSheets(corrections) {\u001b[39m\n\u001b[31m  const url = window.appConfig.webhook_url;\u001b[39m\n\u001b[31m  if (!url) return;\u001b[39m\n\u001b[31m  try {\u001b[39m\n\u001b[31m    await fetch(url, {\u001b[39m\n\u001b[31m      method: 'POST', mode: 'no-cors',\u001b[39m\n\u001b[31m      headers: { 'Content-Type': 'application/json' },\u001b[39m\n\u001b[31m      body: JSON.stringify({\u001b[39m\n\u001b[31m        type: 'parse_correction',\u001b[39m\n\u001b[31m        corrections,\u001b[39m\n\u001b[31m        colFingerprint: getMappingKey(),\u001b[39m\n\u001b[31m        timestamp: new Date().toISOString()\u001b[39m\n\u001b[31m      })\u001b[39m\n\u001b[31m    });\u001b[39m\n\u001b[31m  } catch(e) {}\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction addStoresToConsignees() {\u001b[39m\n\u001b[31m  const consList = document.getElementById('consList');\u001b[39m\n\u001b[31m  consList.innerHTML = '';\u001b[39m\n\u001b[31m  stores.slice(0, 50).forEach(s => addConsignee(s));\u001b[39m\n\u001b[31m  document.getElementById('storeAddPanel').style.display = 'none';\u001b[39m\n\u001b[31m  showToast(`${Math.min(stores.length, 50)} consignees added ✓`, 'ok');\u001b[39m\n\u001b[31m  if (stores.some(s => s.docket)) setTimeout(() => autoCreateDispatchFromStoreList(), 600);\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction promptAddFromStores() { /* replaced by inline panel */ }··\u001b[39m\n\u001b[31mfunction findCol(headers, keywords) {\u001b[39m\n\u001b[31m  for (let i=0;i<headers.length;i++) {\u001b[39m\n\u001b[31m    const h = (headers[i]||'').toString().toLowerCase();\u001b[39m\n\u001b[31m    if (keywords.some(k => h.includes(k))) return i;\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m  return -1;\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction renderStoreTable(hasDocket) {\u001b[39m\n\u001b[31m  const t = document.getElementById('storeTable');\u001b[39m\n\u001b[31m  const cols = ['#','Store Name','City','Phone'];\u001b[39m\n\u001b[31m  if (hasDocket) cols.push('Docket','Status');\u001b[39m\n\u001b[31m  t.innerHTML = `<thead><tr>${cols.map(c=>`<th>${c}</th>`).join('')}</tr></thead>\u001b[39m\n\u001b[31m  <tbody>${stores.map((s,i) => {\u001b[39m\n\u001b[31m    const statusClass = s.docket ? (s.status === 'delivered' ? 'matched' : 'unmatched') : '';\u001b[39m\n\u001b[31m    const docketCell = hasDocket ? `<td class=\\\"${statusClass}\\\" style=\\\"font-family:var(--fnt-mono);\\\">${s.docket||'—'}</td>` : '';\u001b[39m\n\u001b[31m    const statusCell = hasDocket ? `<td>${s.status ? `<span class=\\\"status-pill sp-${s.status}\\\">${s.status}</span>` : '—'}</td>` : '';\u001b[39m\n\u001b[31m    return `<tr>\u001b[39m\n\u001b[31m      <td>${i+1}</td>\u001b[39m\n\u001b[31m      <td>${s.name}</td>\u001b[39m\n\u001b[31m      <td>${s.city}</td>\u001b[39m\n\u001b[31m      <td style=\\\"font-family:var(--fnt-mono);\\\">${s.phone||'—'}</td>\u001b[39m\n\u001b[31m      ${docketCell}${statusCell}\u001b[39m\n\u001b[31m    </tr>`;\u001b[39m\n\u001b[31m  }).join('')}</tbody>`;\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction clearStores() {\u001b[39m\n\u001b[31m  stores = [];\u001b[39m\n\u001b[31m  document.getElementById('storeTableArea').style.display = 'none';\u001b[39m\n\u001b[31m  document.getElementById('storeFile').value = '';\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction autoCreateDispatchFromStoreList() {\u001b[39m\n\u001b[31m  // Create/update dispatch with all stores that have dockets\u001b[39m\n\u001b[31m  const courier = document.getElementById('n_courier').value || 'SkyKing';\u001b[39m\n\u001b[31m  const storesWithDockets = stores.filter(s => s.docket);\u001b[39m\n\u001b[31m  if (!storesWithDockets.length) return;·\u001b[39m\n\u001b[31m  // Clear and rebuild consignee list from store data\u001b[39m\n\u001b[31m  const consList = document.getElementById('consList');\u001b[39m\n\u001b[31m  consList.innerHTML = '';\u001b[39m\n\u001b[31m  storesWithDockets.forEach(s => {\u001b[39m\n\u001b[31m    addConsignee(s);\u001b[39m\n\u001b[31m  });\u001b[39m\n\u001b[31m  showPane('new');\u001b[39m\n\u001b[31m  showToast(`${storesWithDockets.length} consignees added from store list`, 'ok');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction promptAddFromStores() {\u001b[39m\n\u001b[31m  // Replaced by inline storeAddPanel UI — no-op kept for safety\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction autoMatchDockets() {\u001b[39m\n\u001b[31m  // Match OCR dockets to stores by order\u001b[39m\n\u001b[31m  if (!slipOCRData.length) { showToast('Upload docket slip images first', 'err'); return; }\u001b[39m\n\u001b[31m  let matched = 0;\u001b[39m\n\u001b[31m  slipOCRData.forEach((slip, i) => {\u001b[39m\n\u001b[31m    if (i < stores.length && slip.docket) {\u001b[39m\n\u001b[31m      stores[i].docket = slip.docket;\u001b[39m\n\u001b[31m      matched++;\u001b[39m\n\u001b[31m    }\u001b[39m\n\u001b[31m  });\u001b[39m\n\u001b[31m  renderStoreTable(true);\u001b[39m\n\u001b[31m  showToast(`${matched} dockets matched to stores ✓`, 'ok');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31m// CONSIGNEES\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31mfunction addConsignee(storeData = null) {\u001b[39m\n\u001b[31m  const list = document.getElementById('consList');\u001b[39m\n\u001b[31m  const idx = list.children.length;\u001b[39m\n\u001b[31m  const card = document.createElement('div');\u001b[39m\n\u001b[31m  card.className = 'cons-card';\u001b[39m\n\u001b[31m  card.dataset.idx = idx;\u001b[39m\n\u001b[31m  const s = storeData || {};\u001b[39m\n\u001b[31m  card.innerHTML = `\u001b[39m\n\u001b[31m    <div class=\\\"cons-hdr\\\">\u001b[39m\n\u001b[31m      <span class=\\\"cons-num\\\">CONSIGNEE #${idx+1}</span>\u001b[39m\n\u001b[31m      <button class=\\\"cons-del\\\" onclick=\\\"this.closest('.cons-card').remove();renumberCons()\\\">✕</button>\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m    <div class=\\\"row2\\\">\u001b[39m\n\u001b[31m      <div class=\\\"f\\\">\u001b[39m\n\u001b[31m        <label>Store / Name <span class=\\\"req\\\">*</span></label>\u001b[39m\n\u001b[31m        <input type=\\\"text\\\" class=\\\"cons-name\\\" placeholder=\\\"Store name\\\" value=\\\"${s.name||''}\\\">\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      <div class=\\\"f\\\">\u001b[39m\n\u001b[31m        <label>City <span class=\\\"req\\\">*</span></label>\u001b[39m\n\u001b[31m        <input type=\\\"text\\\" class=\\\"cons-city\\\" placeholder=\\\"City\\\" value=\\\"${s.city||''}\\\">\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m    <div class=\\\"f\\\">\u001b[39m\n\u001b[31m      <label>Address</label>\u001b[39m\n\u001b[31m      <input type=\\\"text\\\" class=\\\"cons-addr\\\" placeholder=\\\"Full address\\\" value=\\\"${s.address||''}\\\">\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m    <div class=\\\"row2\\\">\u001b[39m\n\u001b[31m      <div class=\\\"f\\\">\u001b[39m\n\u001b[31m        <label>Phone</label>\u001b[39m\n\u001b[31m        <input type=\\\"tel\\\" class=\\\"cons-phone\\\" placeholder=\\\"Mobile\\\" value=\\\"${s.phone||''}\\\">\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      <div class=\\\"f\\\">\u001b[39m\n\u001b[31m        <label>Pincode</label>\u001b[39m\n\u001b[31m        <input type=\\\"text\\\" class=\\\"cons-pin\\\" placeholder=\\\"6-digit\\\" maxlength=\\\"6\\\">\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m    <div class=\\\"f\\\">\u001b[39m\n\u001b[31m      <label>Material / Contents <span class=\\\"req\\\">*</span></label>\u001b[39m\n\u001b[31m      <input type=\\\"text\\\" class=\\\"cons-material\\\" placeholder=\\\"e.g. Standees, Posters\\\">\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m    <div class=\\\"row2\\\">\u001b[39m\n\u001b[31m      <div class=\\\"f\\\">\u001b[39m\n\u001b[31m        <label>Qty</label>\u001b[39m\n\u001b[31m        <input type=\\\"number\\\" class=\\\"cons-qty\\\" placeholder=\\\"0\\\" min=\\\"0\\\">\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      <div class=\\\"f\\\">\u001b[39m\n\u001b[31m        <label>Docket No.</label>\u001b[39m\n\u001b[31m        <input type=\\\"text\\\" class=\\\"cons-docket-input\\\" placeholder=\\\"Docket #\\\"\u001b[39m\n\u001b[31m          style=\\\"font-family:var(--fnt-mono);\\\" value=\\\"${s.docket||''}\\\"\u001b[39m\n\u001b[31m          oninput=\\\"onDocketInput(this)\\\">\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m    ${s.docket ? `<div class=\\\"cons-docket-linked\\\"><span>🔗</span><span class=\\\"dkt\\\">${s.docket}</span></div>` : ''}\u001b[39m\n\u001b[31m  `;\u001b[39m\n\u001b[31m  list.appendChild(card);\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction onDocketInput(input) {\u001b[39m\n\u001b[31m  const card = input.closest('.cons-card');\u001b[39m\n\u001b[31m  let linked = card.querySelector('.cons-docket-linked');\u001b[39m\n\u001b[31m  if (input.value.trim()) {\u001b[39m\n\u001b[31m    if (!linked) {\u001b[39m\n\u001b[31m      linked = document.createElement('div');\u001b[39m\n\u001b[31m      linked.className = 'cons-docket-linked';\u001b[39m\n\u001b[31m      card.appendChild(linked);\u001b[39m\n\u001b[31m    }\u001b[39m\n\u001b[31m    linked.innerHTML = `<span>🔗</span><span class=\\\"dkt\\\">${input.value}</span>`;\u001b[39m\n\u001b[31m  } else if (linked) {\u001b[39m\n\u001b[31m    linked.remove();\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction showLinkedDocket(card, docket) {\u001b[39m\n\u001b[31m  const input = card.querySelector('.cons-docket-input');\u001b[39m\n\u001b[31m  if (input) { input.value = docket; onDocketInput(input); }\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction renumberCons() {\u001b[39m\n\u001b[31m  document.querySelectorAll('#consList .cons-card').forEach((c,i) => {\u001b[39m\n\u001b[31m    c.querySelector('.cons-num').textContent = `CONSIGNEE #${i+1}`;\u001b[39m\n\u001b[31m    c.dataset.idx = i;\u001b[39m\n\u001b[31m  });\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31m// JOB REFERENCE\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31mfunction onClientChange() {\u001b[39m\n\u001b[31m  const val = document.getElementById('n_client').value;\u001b[39m\n\u001b[31m  document.getElementById('customClientField').style.display = val === '__c' ? 'block' : 'none';\u001b[39m\n\u001b[31m  updatePill();\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31mfunction updatePill() {\u001b[39m\n\u001b[31m  const client = document.getElementById('n_client').value;\u001b[39m\n\u001b[31m  const job = document.getElementById('n_jobref').value.trim();\u001b[39m\n\u001b[31m  const cName = client === '__c' ? document.getElementById('n_customClient').value : client;\u001b[39m\n\u001b[31m  const pill = document.getElementById('jobPill');\u001b[39m\n\u001b[31m  if (cName && job) {\u001b[39m\n\u001b[31m    document.getElementById('jobPillTxt').textContent = `${cName} › ${job}`;\u001b[39m\n\u001b[31m    pill.style.display = 'inline-flex';\u001b[39m\n\u001b[31m  } else {\u001b[39m\n\u001b[31m    pill.style.display = 'none';\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31mfunction clearJob() {\u001b[39m\n\u001b[31m  document.getElementById('n_client').value = '';\u001b[39m\n\u001b[31m  document.getElementById('n_jobref').value = '';\u001b[39m\n\u001b[31m  document.getElementById('jobPill').style.display = 'none';\u001b[39m\n\u001b[31m  document.getElementById('customClientField').style.display = 'none';\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31m// GATHER / SAVE\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31mfunction gatherDispatch() {\u001b[39m\n\u001b[31m  const client = document.getElementById('n_client').value;\u001b[39m\n\u001b[31m  const clientName = client === '__c' ? document.getElementById('n_customClient').value : client;\u001b[39m\n\u001b[31m  const consignees = [];\u001b[39m\n\u001b[31m  document.querySelectorAll('#consList .cons-card').forEach(card => {\u001b[39m\n\u001b[31m    const docket = card.querySelector('.cons-docket-input')?.value.trim() || '';\u001b[39m\n\u001b[31m    consignees.push({\u001b[39m\n\u001b[31m      name: card.querySelector('.cons-name')?.value || '',\u001b[39m\n\u001b[31m      city: card.querySelector('.cons-city')?.value || '',\u001b[39m\n\u001b[31m      address: card.querySelector('.cons-addr')?.value || '',\u001b[39m\n\u001b[31m      phone: card.querySelector('.cons-phone')?.value || '',\u001b[39m\n\u001b[31m      pincode: card.querySelector('.cons-pin')?.value || '',\u001b[39m\n\u001b[31m      material: card.querySelector('.cons-material')?.value || '',\u001b[39m\n\u001b[31m      qty: card.querySelector('.cons-qty')?.value || '',\u001b[39m\n\u001b[31m      docket,\u001b[39m\n\u001b[31m      status: docket ? 'booked' : 'pending',\u001b[39m\n\u001b[31m      lastEvent: '',\u001b[39m\n\u001b[31m      lastUpdated: ''\u001b[39m\n\u001b[31m    });\u001b[39m\n\u001b[31m  });\u001b[39m\n\u001b[31m  return {\u001b[39m\n\u001b[31m    id: 'DSP-' + Date.now(),\u001b[39m\n\u001b[31m    mode: 'manual',\u001b[39m\n\u001b[31m    client: clientName,\u001b[39m\n\u001b[31m    jobref: document.getElementById('n_jobref').value,\u001b[39m\n\u001b[31m    courier: document.getElementById('n_courier').value,\u001b[39m\n\u001b[31m    dispatchDate: document.getElementById('n_date').value,\u001b[39m\n\u001b[31m    remarks: document.getElementById('n_remarks').value,\u001b[39m\n\u001b[31m    consignees,\u001b[39m\n\u001b[31m    slipCount: slipOCRData.length,\u001b[39m\n\u001b[31m    status: 'active',\u001b[39m\n\u001b[31m    createdAt: new Date().toISOString()\u001b[39m\n\u001b[31m  };\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction validate(d) {\u001b[39m\n\u001b[31m  if (!d.client) { showToast('Select a client', 'err'); return false; }\u001b[39m\n\u001b[31m  if (!d.courier) { showToast('Select courier', 'err'); return false; }\u001b[39m\n\u001b[31m  if (!d.consignees.length) { showToast('Add at least one consignee', 'err'); return false; }\u001b[39m\n\u001b[31m  for (let i = 0; i < d.consignees.length; i++) {\u001b[39m\n\u001b[31m    const c = d.consignees[i];\u001b[39m\n\u001b[31m    if (!c.name || !c.city) {\u001b[39m\n\u001b[31m      showToast(`Consignee #${i+1} (${c.name||'unnamed'}) — name & city required`, 'err');\u001b[39m\n\u001b[31m      // Scroll to and highlight the problem consignee card\u001b[39m\n\u001b[31m      const cards = document.querySelectorAll('#consList .cons-card');\u001b[39m\n\u001b[31m      if (cards[i]) {\u001b[39m\n\u001b[31m        cards[i].scrollIntoView({ behavior: 'smooth', block: 'center' });\u001b[39m\n\u001b[31m        cards[i].style.borderColor = 'var(--red)';\u001b[39m\n\u001b[31m        cards[i].style.boxShadow = '0 0 0 2px rgba(239,68,68,.3)';\u001b[39m\n\u001b[31m        setTimeout(() => {\u001b[39m\n\u001b[31m          if (cards[i]) { cards[i].style.borderColor = ''; cards[i].style.boxShadow = ''; }\u001b[39m\n\u001b[31m        }, 3000);\u001b[39m\n\u001b[31m      }\u001b[39m\n\u001b[31m      return false;\u001b[39m\n\u001b[31m    }\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m  return true;\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction saveDraft() {\u001b[39m\n\u001b[31m  const d = gatherDispatch();\u001b[39m\n\u001b[31m  d.status = 'draft';\u001b[39m\n\u001b[31m  dispatches.unshift(d);\u001b[39m\n\u001b[31m  saveData();\u001b[39m\n\u001b[31m  updateHistCount();\u001b[39m\n\u001b[31m  showToast('Draft saved ✓', 'ok');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// ── VERIFICATION SCREEN ─────────────────────────────────\u001b[39m\n\u001b[31m// Known docket lengths per courier for validation\u001b[39m\n\u001b[31mconst DOCKET_LENGTHS = {\u001b[39m\n\u001b[31m  'SkyKing': { min: 9, max: 12 },\u001b[39m\n\u001b[31m  'DTDC':    { min: 8, max: 15 },\u001b[39m\n\u001b[31m  'Delhivery': { min: 10, max: 18 },\u001b[39m\n\u001b[31m  'Blue Dart': { min: 10, max: 13 },\u001b[39m\n\u001b[31m  'FedEx':   { min: 10, max: 20 },\u001b[39m\n\u001b[31m  'Ecom Express': { min: 10, max: 14 },\u001b[39m\n\u001b[31m  'India Post': { min: 13, max: 22 },\u001b[39m\n\u001b[31m};·\u001b[39m\n\u001b[31mfunction finaliseDispatch() {\u001b[39m\n\u001b[31m  const d = gatherDispatch();\u001b[39m\n\u001b[31m  if (!validate(d)) return;\u001b[39m\n\u001b[31m  showVerifyScreen(d);\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction showVerifyScreen(d) {\u001b[39m\n\u001b[31m  const pane = document.getElementById('verifyPane');\u001b[39m\n\u001b[31m  const list = document.getElementById('verifyList');\u001b[39m\n\u001b[31m  const summary = document.getElementById('verifySummary');·\u001b[39m\n\u001b[31m  // Build flags\u001b[39m\n\u001b[31m  const docketsSeen = {};\u001b[39m\n\u001b[31m  const courier = d.courier;\u001b[39m\n\u001b[31m  const lenRule = DOCKET_LENGTHS[courier];·\u001b[39m\n\u001b[31m  d.consignees.forEach((c, i) => {\u001b[39m\n\u001b[31m    if (c.docket) docketsSeen[c.docket] = (docketsSeen[c.docket] || 0) + 1;\u001b[39m\n\u001b[31m  });·\u001b[39m\n\u001b[31m  const total     = d.consignees.length;\u001b[39m\n\u001b[31m  const withDock  = d.consignees.filter(c => c.docket).length;\u001b[39m\n\u001b[31m  const dupes     = Object.values(docketsSeen).filter(v => v > 1).length;\u001b[39m\n\u001b[31m  const noDock    = total - withDock;\u001b[39m\n\u001b[31m  const badLen    = lenRule ? d.consignees.filter(c => c.docket &&\u001b[39m\n\u001b[31m    (c.docket.length < lenRule.min || c.docket.length > lenRule.max)).length : 0;·\u001b[39m\n\u001b[31m  // Summary bar\u001b[39m\n\u001b[31m  summary.innerHTML = `\u001b[39m\n\u001b[31m    <div class=\\\"vs-item\\\"><div class=\\\"vs-val\\\" style=\\\"color:var(--t1);\\\">${total}</div><div class=\\\"vs-lbl\\\">Total</div></div>\u001b[39m\n\u001b[31m    <div class=\\\"vs-item\\\"><div class=\\\"vs-val\\\" style=\\\"color:var(--grn);\\\">${withDock}</div><div class=\\\"vs-lbl\\\">Docketed</div></div>\u001b[39m\n\u001b[31m    <div class=\\\"vs-item\\\"><div class=\\\"vs-val\\\" style=\\\"color:${noDock > 0 ? 'var(--ylw)' : 'var(--t3)'};\\\">${noDock}</div><div class=\\\"vs-lbl\\\">No Docket</div></div>\u001b[39m\n\u001b[31m    <div class=\\\"vs-item\\\"><div class=\\\"vs-val\\\" style=\\\"color:${dupes > 0 ? 'var(--red)' : 'var(--t3)'};\\\">${dupes}</div><div class=\\\"vs-lbl\\\">Duplicates</div></div>\u001b[39m\n\u001b[31m    ${badLen > 0 ? `<div class=\\\"vs-item\\\"><div class=\\\"vs-val\\\" style=\\\"color:var(--sky);\\\">${badLen}</div><div class=\\\"vs-lbl\\\">Short/Long</div></div>` : ''}\u001b[39m\n\u001b[31m    <div class=\\\"vs-item\\\" style=\\\"margin-left:auto;text-align:right;\\\">\u001b[39m\n\u001b[31m      <div class=\\\"vs-val\\\" style=\\\"font-size:.8rem;color:var(--t2);\\\">${d.courier||'—'}</div>\u001b[39m\n\u001b[31m      <div class=\\\"vs-lbl\\\">${d.client||'—'} ${d.jobref ? '› '+d.jobref : ''}</div>\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m  `;·\u001b[39m\n\u001b[31m  // Build consignee rows\u001b[39m\n\u001b[31m  list.innerHTML = d.consignees.map((c, i) => {\u001b[39m\n\u001b[31m    const isDupe   = c.docket && docketsSeen[c.docket] > 1;\u001b[39m\n\u001b[31m    const isNoDock = !c.docket;\u001b[39m\n\u001b[31m    const isShort  = lenRule && c.docket &&\u001b[39m\n\u001b[31m      (c.docket.length < lenRule.min || c.docket.length > lenRule.max);·\u001b[39m\n\u001b[31m    const rowClass = isDupe ? 'has-dupe' : (isNoDock ? 'no-docket' : 'ok');·\u001b[39m\n\u001b[31m    // Find matching OCR slip for thumbnail\u001b[39m\n\u001b[31m    const ocrMatch = slipOCRData[i];\u001b[39m\n\u001b[31m    const thumbHtml = ocrMatch?.imageData\u001b[39m\n\u001b[31m      ? `<img class=\\\"verify-thumb\\\" src=\\\"${ocrMatch.imageData}\\\" onclick=\\\"showSlipFull(${i})\\\" title=\\\"View slip\\\">`\u001b[39m\n\u001b[31m      : '';·\u001b[39m\n\u001b[31m    // Flags\u001b[39m\n\u001b[31m    let flags = '';\u001b[39m\n\u001b[31m    if (isDupe)   flags += `<span class=\\\"verify-flag vf-dupe\\\">⚠ Duplicate</span> `;\u001b[39m\n\u001b[31m    if (isNoDock) flags += `<span class=\\\"verify-flag vf-nodocket\\\">No Docket</span> `;\u001b[39m\n\u001b[31m    if (isShort)  flags += `<span class=\\\"verify-flag vf-short\\\">${c.docket.length} digits</span> `;\u001b[39m\n\u001b[31m    if (!isDupe && !isNoDock && !isShort) flags = `<span class=\\\"verify-flag vf-ok\\\">✓ OK</span>`;·\u001b[39m\n\u001b[31m    // OCR confidence note\u001b[39m\n\u001b[31m    const confNote = ocrMatch && ocrMatch.confidence\u001b[39m\n\u001b[31m      ? `<div class=\\\"ocr-correction-note\\\">OCR confidence: ${Math.round(ocrMatch.confidence*100)}% via ${ocrMatch.source||'—'}</div>` : '';·\u001b[39m\n\u001b[31m    // WhatsApp URL\u001b[39m\n\u001b[31m    const waMsg = encodeURIComponent(\u001b[39m\n\u001b[31m      `Dear ${c.name}, your material has been dispatched via ${d.courier}. Docket: ${c.docket||'TBD'}. Track: ${(COURIER_MASTER[d.courier]?.trackUrl||'').replace('{docket}', c.docket||'')}`\u001b[39m\n\u001b[31m    );\u001b[39m\n\u001b[31m    const waUrl = c.phone ? `https://wa.me/91${c.phone.replace(/\\\\D/g,'')}?text=${waMsg}` : '';·\u001b[39m\n\u001b[31m    return `\u001b[39m\n\u001b[31m    <div class=\\\"verify-row ${rowClass}\\\" id=\\\"vrow_${i}\\\">\u001b[39m\n\u001b[31m      <div class=\\\"verify-row-hdr\\\">\u001b[39m\n\u001b[31m        <span class=\\\"verify-num\\\">#${i+1}</span>\u001b[39m\n\u001b[31m        <span class=\\\"verify-store\\\">${c.name||'—'}</span>\u001b[39m\n\u001b[31m        ${flags}\u001b[39m\n\u001b[31m        ${thumbHtml}\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      <div class=\\\"verify-fields\\\" style=\\\"grid-template-columns:1fr 1fr 1fr;\\\">\u001b[39m\n\u001b[31m        <div class=\\\"verify-field\\\">\u001b[39m\n\u001b[31m          <label>Docket No. <span style=\\\"color:var(--acc);\\\">*</span></label>\u001b[39m\n\u001b[31m          <input type=\\\"text\\\" value=\\\"${c.docket||''}\\\" oninput=\\\"updateVerifyField(${i},'docket',this)\\\"\u001b[39m\n\u001b[31m            style=\\\"font-family:var(--fnt-mono);${isDupe||isShort?'border-color:var(--red);':!c.docket?'border-color:var(--ylw);':'color:var(--acc);'}\\\">\u001b[39m\n\u001b[31m          ${confNote}\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m        <div class=\\\"verify-field\\\">\u001b[39m\n\u001b[31m          <label>City</label>\u001b[39m\n\u001b[31m          <input type=\\\"text\\\" value=\\\"${c.city||''}\\\" oninput=\\\"updateVerifyField(${i},'city',this)\\\"\u001b[39m\n\u001b[31m            style=\\\"${!c.city ? 'border-color:var(--ylw);' : ''}\\\">\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m        <div class=\\\"verify-field\\\">\u001b[39m\n\u001b[31m          <label>Pincode</label>\u001b[39m\n\u001b[31m          <input type=\\\"text\\\" value=\\\"${c.pincode||''}\\\" oninput=\\\"updateVerifyField(${i},'pincode',this)\\\"\u001b[39m\n\u001b[31m            style=\\\"font-family:var(--fnt-mono);\\\">\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m        <div class=\\\"verify-field\\\">\u001b[39m\n\u001b[31m          <label>Phone</label>\u001b[39m\n\u001b[31m          <input type=\\\"tel\\\" value=\\\"${c.phone||''}\\\" oninput=\\\"updateVerifyField(${i},'phone',this)\\\">\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m        <div class=\\\"verify-field\\\">\u001b[39m\n\u001b[31m          <label>Material / Contents</label>\u001b[39m\n\u001b[31m          <input type=\\\"text\\\" value=\\\"${c.material||''}\\\" oninput=\\\"updateVerifyField(${i},'material',this)\\\">\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m        <div class=\\\"verify-field\\\">\u001b[39m\n\u001b[31m          <label>Street Address</label>\u001b[39m\n\u001b[31m          <input type=\\\"text\\\" value=\\\"${c.address||''}\\\" oninput=\\\"updateVerifyField(${i},'address',this)\\\"\u001b[39m\n\u001b[31m            placeholder=\\\"Street / locality only\\\">\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      <div class=\\\"verify-actions\\\">\u001b[39m\n\u001b[31m        ${c.docket && c.phone ? `<button class=\\\"btn-wa\\\" onclick=\\\"window.open('${waUrl}','_blank')\\\">📱 WhatsApp</button>` : ''}\u001b[39m\n\u001b[31m        <button class=\\\"btn-sm\\\" onclick=\\\"removeVerifyRow(${i})\\\">✕ Remove</button>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m    </div>`;\u001b[39m\n\u001b[31m  }).join('');·\u001b[39m\n\u001b[31m  // Store dispatch object for confirm\u001b[39m\n\u001b[31m  pane._pendingDispatch = d;\u001b[39m\n\u001b[31m  pane.style.display = 'block';\u001b[39m\n\u001b[31m  pane.scrollTop = 0;\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction updateVerifyField(idx, field, input) {\u001b[39m\n\u001b[31m  const pane = document.getElementById('verifyPane');\u001b[39m\n\u001b[31m  if (!pane._pendingDispatch) return;\u001b[39m\n\u001b[31m  const c = pane._pendingDispatch.consignees[idx];\u001b[39m\n\u001b[31m  if (!c) return;\u001b[39m\n\u001b[31m  c[field] = input.value.trim();\u001b[39m\n\u001b[31m  input.classList.toggle('changed', input.value.trim() !== '');\u001b[39m\n\u001b[31m  // Re-check dupe if docket changed\u001b[39m\n\u001b[31m  if (field === 'docket') refreshVerifyFlags(pane._pendingDispatch);\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction refreshVerifyFlags(d) {\u001b[39m\n\u001b[31m  const seen = {};\u001b[39m\n\u001b[31m  d.consignees.forEach(c => { if (c.docket) seen[c.docket] = (seen[c.docket]||0)+1; });\u001b[39m\n\u001b[31m  d.consignees.forEach((c, i) => {\u001b[39m\n\u001b[31m    const row = document.getElementById('vrow_' + i);\u001b[39m\n\u001b[31m    if (!row) return;\u001b[39m\n\u001b[31m    const isDupe = c.docket && seen[c.docket] > 1;\u001b[39m\n\u001b[31m    row.classList.toggle('has-dupe', isDupe);\u001b[39m\n\u001b[31m  });\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction removeVerifyRow(idx) {\u001b[39m\n\u001b[31m  const pane = document.getElementById('verifyPane');\u001b[39m\n\u001b[31m  if (!pane._pendingDispatch) return;\u001b[39m\n\u001b[31m  const el = document.getElementById('vrow_' + idx);\u001b[39m\n\u001b[31m  if (el) {\u001b[39m\n\u001b[31m    el.style.transition = 'opacity .25s';\u001b[39m\n\u001b[31m    el.style.opacity = '0';\u001b[39m\n\u001b[31m    setTimeout(() => {\u001b[39m\n\u001b[31m      pane._pendingDispatch.consignees.splice(idx, 1);\u001b[39m\n\u001b[31m      showVerifyScreen(pane._pendingDispatch); // re-render with updated indices\u001b[39m\n\u001b[31m    }, 250);\u001b[39m\n\u001b[31m  } else {\u001b[39m\n\u001b[31m    pane._pendingDispatch.consignees.splice(idx, 1);\u001b[39m\n\u001b[31m    showVerifyScreen(pane._pendingDispatch);\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction approveAll() {\u001b[39m\n\u001b[31m  showToast('All rows approved ✓', 'ok');\u001b[39m\n\u001b[31m  document.getElementById('btnConfirmFinalise').style.background = 'var(--grn)';\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction closeVerify() {\u001b[39m\n\u001b[31m  const pane = document.getElementById('verifyPane');\u001b[39m\n\u001b[31m  // If this dispatch was already finalised, warn before going back\u001b[39m\n\u001b[31m  if (_lastFinalisedId && pane._pendingDispatch?.id === _lastFinalisedId) {\u001b[39m\n\u001b[31m    if (!confirm(\u001b[39m\n\u001b[31m      'This dispatch has already been finalised.\\\\n\\\\n' +\u001b[39m\n\u001b[31m      'Going back will take you to the edit form — any changes will create a NEW dispatch record.\\\\n\\\\n' +\u001b[39m\n\u001b[31m      'Go back anyway?'\u001b[39m\n\u001b[31m    )) return;\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m  pane.style.display = 'none';\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction showSlipFull(idx) {\u001b[39m\n\u001b[31m  const s = slipOCRData[idx];\u001b[39m\n\u001b[31m  if (!s?.imageData) return;\u001b[39m\n\u001b[31m  const w = window.open('');\u001b[39m\n\u001b[31m  w.document.write(`<img src=\\\"${s.imageData}\\\" style=\\\"max-width:100%;height:auto;\\\">`);\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// Log OCR correction to sheets for future accuracy improvement\u001b[39m\n\u001b[31masync function logOCRCorrection(original, corrected, courier) {\u001b[39m\n\u001b[31m  if (!window.appConfig.webhook_url || original === corrected) return;\u001b[39m\n\u001b[31m  try {\u001b[39m\n\u001b[31m    await fetch(window.appConfig.webhook_url, {\u001b[39m\n\u001b[31m      method: 'POST', mode: 'no-cors',\u001b[39m\n\u001b[31m      headers: { 'Content-Type': 'application/json' },\u001b[39m\n\u001b[31m      body: JSON.stringify({\u001b[39m\n\u001b[31m        type: 'ocr_correction',\u001b[39m\n\u001b[31m        original, corrected, courier,\u001b[39m\n\u001b[31m        timestamp: new Date().toISOString()\u001b[39m\n\u001b[31m      })\u001b[39m\n\u001b[31m    });\u001b[39m\n\u001b[31m  } catch(e) {}\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mlet _lastFinalisedId = null; // track last finalised dispatch to warn on back·\u001b[39m\n\u001b[31masync function confirmAndFinalise() {\u001b[39m\n\u001b[31m  const pane = document.getElementById('verifyPane');\u001b[39m\n\u001b[31m  const d = pane._pendingDispatch;\u001b[39m\n\u001b[31m  if (!d) return;·\u001b[39m\n\u001b[31m  d.status = 'active';\u001b[39m\n\u001b[31m  d.finalisedAt = new Date().toISOString();·\u001b[39m\n\u001b[31m  // Log OCR corrections\u001b[39m\n\u001b[31m  slipOCRData.forEach((s, i) => {\u001b[39m\n\u001b[31m    const c = d.consignees[i];\u001b[39m\n\u001b[31m    if (c && s.docket && c.docket && s.docket !== c.docket) {\u001b[39m\n\u001b[31m      logOCRCorrection(s.docket, c.docket, d.courier);\u001b[39m\n\u001b[31m    }\u001b[39m\n\u001b[31m  });·\u001b[39m\n\u001b[31m  // Replace existing draft with same ID if present, else prepend\u001b[39m\n\u001b[31m  const existingIdx = dispatches.findIndex(x => x.id === d.id);\u001b[39m\n\u001b[31m  if (existingIdx >= 0) {\u001b[39m\n\u001b[31m    dispatches[existingIdx] = d;\u001b[39m\n\u001b[31m  } else {\u001b[39m\n\u001b[31m    dispatches.unshift(d);\u001b[39m\n\u001b[31m  }·\u001b[39m\n\u001b[31m  _lastFinalisedId = d.id;\u001b[39m\n\u001b[31m  saveData();\u001b[39m\n\u001b[31m  syncDispatchToDB(d); // push to Postgres (non-blocking)\u001b[39m\n\u001b[31m  updateHistCount();\u001b[39m\n\u001b[31m  if (window.appConfig.auto_log !== 'false' && window.appConfig.webhook_url) await logToSheets(d);\u001b[39m\n\u001b[31m  showToast('Dispatch finalised ✓', 'ok');\u001b[39m\n\u001b[31m  pane.style.display = 'none';\u001b[39m\n\u001b[31m  pane._pendingDispatch = null; // clear pending\u001b[39m\n\u001b[31m  resetForm();\u001b[39m\n\u001b[31m  showPane('dashboard');\u001b[39m\n\u001b[31m  renderDashboard();\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction resetForm() {\u001b[39m\n\u001b[31m  document.getElementById('n_client').value = '';\u001b[39m\n\u001b[31m  document.getElementById('n_jobref').value = '';\u001b[39m\n\u001b[31m  document.getElementById('n_courier').value = '';\u001b[39m\n\u001b[31m  document.getElementById('n_remarks').value = '';\u001b[39m\n\u001b[31m  document.getElementById('jobPill').style.display = 'none';\u001b[39m\n\u001b[31m  document.getElementById('customClientField').style.display = 'none';\u001b[39m\n\u001b[31m  document.getElementById('consList').innerHTML = '';\u001b[39m\n\u001b[31m  addConsignee();\u001b[39m\n\u001b[31m  clearSlips();\u001b[39m\n\u001b[31m  clearStores();\u001b[39m\n\u001b[31m  setToday();\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction newDispatch() { resetForm(); showPane('new'); }·\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31m// DASHBOARD\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31mfunction renderDashboard() {\u001b[39m\n\u001b[31m  // Exclude drafts from dashboard — only show active/finalised dispatches\u001b[39m\n\u001b[31m  const activeDis = dispatches.filter(d => d.status !== 'draft');\u001b[39m\n\u001b[31m  const allCons = activeDis.flatMap(d => d.consignees || []);\u001b[39m\n\u001b[31m  const countBy = (status) => allCons.filter(c => c.status === status).length;\u001b[39m\n\u001b[31m  document.getElementById('st-delivered').textContent = countBy('delivered');\u001b[39m\n\u001b[31m  document.getElementById('st-transit').textContent = countBy('transit') + countBy('in-transit');\u001b[39m\n\u001b[31m  document.getElementById('st-booked').textContent = countBy('booked');\u001b[39m\n\u001b[31m  document.getElementById('st-pending').textContent = allCons.filter(c => !c.docket || c.status === 'pending').length;·\u001b[39m\n\u001b[31m  const grid = document.getElementById('dashGrid');\u001b[39m\n\u001b[31m  // Flatten all consignees across dispatches\u001b[39m\n\u001b[31m  let items = activeDis.flatMap(d =>\u001b[39m\n\u001b[31m    (d.consignees || []).filter(c => c.docket).map(c => ({...c, dispatchId: d.id, courier: d.courier, client: d.client, jobref: d.jobref}))\u001b[39m\n\u001b[31m  );\u001b[39m\n\u001b[31m  if (dashFilter !== 'all') {\u001b[39m\n\u001b[31m    items = items.filter(c => {\u001b[39m\n\u001b[31m      if (dashFilter === 'delivered') return c.status === 'delivered';\u001b[39m\n\u001b[31m      if (dashFilter === 'transit') return c.status === 'transit' || c.status === 'in-transit';\u001b[39m\n\u001b[31m      if (dashFilter === 'booked') return c.status === 'booked';\u001b[39m\n\u001b[31m      if (dashFilter === 'pending') return !c.docket || c.status === 'pending';\u001b[39m\n\u001b[31m      return true;\u001b[39m\n\u001b[31m    });\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m  if (!items.length) {\u001b[39m\n\u001b[31m    grid.innerHTML = '<div class=\\\"empty\\\"><div class=\\\"e-icon\\\">📦</div><p>No docket-tracked consignees.<br>Add docket numbers to see tiles.</p></div>';\u001b[39m\n\u001b[31m    return;\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m  const progMap = { delivered:100, transit:60,'in-transit':60, booked:20, pending:5 };\u001b[39m\n\u001b[31m  const progClr = { delivered:'var(--grn)', transit:'var(--ylw)','in-transit':'var(--ylw)', booked:'var(--sky)', pending:'var(--t4)' };\u001b[39m\n\u001b[31m  grid.innerHTML = items.map(c => {\u001b[39m\n\u001b[31m    const sp = c.status || 'booked';\u001b[39m\n\u001b[31m    const prog = progMap[sp] || 20;\u001b[39m\n\u001b[31m    const clr = progClr[sp] || 'var(--t4)';\u001b[39m\n\u001b[31m    return `\u001b[39m\n\u001b[31m    <div class=\\\"docket-tile\\\" onclick=\\\"showDocketDetail('${c.docket}','${c.courier||''}','${c.dispatchId}')\\\">\u001b[39m\n\u001b[31m      <div class=\\\"docket-tile-hdr\\\">\u001b[39m\n\u001b[31m        <div class=\\\"docket-tile-left\\\">\u001b[39m\n\u001b[31m          <div class=\\\"docket-num\\\">${c.docket}</div>\u001b[39m\n\u001b[31m          <div class=\\\"docket-store\\\">${c.name} — ${c.city}</div>\u001b[39m\n\u001b[31m          <div class=\\\"docket-courier\\\">${c.courier||''} · ${c.client||''} ${c.jobref ? '› '+c.jobref : ''}</div>\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m        <span class=\\\"status-pill sp-${sp}\\\">${sp}</span>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      <div style=\\\"display:flex;gap:6px;margin-bottom:6px;align-items:center;\\\">\u001b[39m\n\u001b[31m        <input type=\\\"checkbox\\\" id=\\\"sel_${c.docket.replace(/[^a-z0-9]/gi,'_')}\\\"\u001b[39m\n\u001b[31m          onclick=\\\"event.stopPropagation();toggleTileSelect('${c.docket}')\\\"\u001b[39m\n\u001b[31m          style=\\\"width:14px;height:14px;cursor:pointer;accent-color:var(--acc);\\\">\u001b[39m\n\u001b[31m        <label style=\\\"font-size:.6rem;color:var(--t3);font-family:var(--fnt-mono);cursor:pointer;\\\"\u001b[39m\n\u001b[31m          onclick=\\\"event.stopPropagation();toggleTileSelect('${c.docket}')\\\">Select</label>\u001b[39m\n\u001b[31m        ${c.manualStatus ?\u001b[39m\n\u001b[31m          `<span style=\\\"font-size:.6rem;font-family:var(--fnt-mono);color:var(--grn);\\\">📞 ${c.manualStatus}</span>` : ''}\u001b[39m\n\u001b[31m        ${c.podUploaded ? '<span style=\\\"font-size:.6rem;color:var(--grn);font-family:var(--fnt-mono);\\\">✓ POD</span>' : ''}\u001b[39m\n\u001b[31m        <button onclick=\\\"event.stopPropagation();showManualTracking('${c.docket}','${c.dispatchId}','${c.name}','${c.phone||''}','${c.city||''}')\\\"\u001b[39m\n\u001b[31m          style=\\\"font-size:.6rem;background:rgba(56,189,248,.1);border:1px solid rgba(56,189,248,.3);\u001b[39m\n\u001b[31m          color:var(--sky);padding:2px 7px;border-radius:10px;cursor:pointer;font-family:var(--fnt-mono);\\\">📞 Call Log</button>\u001b[39m\n\u001b[31m        <button onclick=\\\"event.stopPropagation();showPODUpload('${c.docket}','${c.dispatchId}')\\\"\u001b[39m\n\u001b[31m          style=\\\"font-size:.6rem;background:none;border:1px solid var(--bdr);color:var(--t3);\u001b[39m\n\u001b[31m          padding:2px 7px;border-radius:10px;cursor:pointer;font-family:var(--fnt-mono);\\\">📸 POD</button>\u001b[39m\n\u001b[31m        ${c.manualDelivered ? `<button onclick=\\\"event.stopPropagation();resumeAutoTracking('${c.docket}','${c.dispatchId}')\\\"\u001b[39m\n\u001b[31m          style=\\\"font-size:.6rem;background:rgba(251,146,60,.1);border:1px solid rgba(251,146,60,.4);\u001b[39m\n\u001b[31m          color:#fb923c;padding:2px 7px;border-radius:10px;cursor:pointer;font-family:var(--fnt-mono);\\\">↩ Undo</button>` : ''}\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      <div class=\\\"docket-tile-footer\\\">\u001b[39m\n\u001b[31m        <div style=\\\"flex:1;min-width:0;\\\">\u001b[39m\n\u001b[31m          <span class=\\\"docket-last-event docket-evt-${c.docket}\\\" id=\\\"evt_${c.docket.replace(/[^a-z0-9]/gi,'_')}\\\">${c.lastEvent || 'No tracking yet'}</span>\u001b[39m\n\u001b[31m          <span class=\\\"track-spinner\\\" id=\\\"spin_${c.docket.replace(/[^a-z0-9]/gi,'_')}\\\" style=\\\"display:none;color:var(--ylw);font-family:var(--fnt-mono);font-size:.68rem;animation:blink 1s infinite;\\\">⏳ tracking...</span>\u001b[39m\n\u001b[31m          ${c.lastScanLocation && c.lastScanLocation !== c.lastEvent ?·\u001b[39m\n\u001b[31m            `<div style=\\\"font-size:.62rem;color:var(--t3);font-family:var(--fnt-mono);margin-top:2px;\\\">📍 ${c.lastScanLocation}</div>` : ''}\u001b[39m\n\u001b[31m          ${c.lastTrackedAt ?·\u001b[39m\n\u001b[31m            `<div style=\\\"font-size:.6rem;color:var(--t4);font-family:var(--fnt-mono);margin-top:1px;\\\">🕐 tracked ${new Date(c.lastTrackedAt).toLocaleTimeString('en-IN',{hour:'2-digit',minute:'2-digit'})}</div>` : ''}\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m        <button class=\\\"btn-track-live live-btn-${c.docket.replace(/[^a-z0-9]/gi,'_')}\\\" onclick=\\\"event.stopPropagation();liveTrackTile('${c.docket}','${c.courier||''}','${c.dispatchId}')\\\">\u001b[39m\n\u001b[31m          <span class=\\\"live-dot\\\"></span>LIVE\u001b[39m\n\u001b[31m        </button>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      <div class=\\\"docket-progress\\\">\u001b[39m\n\u001b[31m        <div class=\\\"docket-progress-fill\\\" style=\\\"width:${prog}%;background:${clr};\\\"></div>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m    </div>`;\u001b[39m\n\u001b[31m  }).join('');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction filterDash(f, el) {\u001b[39m\n\u001b[31m  dashFilter = f;\u001b[39m\n\u001b[31m  document.querySelectorAll('#dashFilters .btn-sm').forEach(b => b.classList.remove('primary'));\u001b[39m\n\u001b[31m  el.classList.add('primary');\u001b[39m\n\u001b[31m  renderDashboard();\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31masync function liveTrackTile(docket, courier, dispatchId) {\u001b[39m\n\u001b[31m  showToast(`Tracking ${docket}...`, 'info');\u001b[39m\n\u001b[31m  const result = await fetchTracking(docket, courier);\u001b[39m\n\u001b[31m  // Update status in dispatches\u001b[39m\n\u001b[31m  for (const d of dispatches) {\u001b[39m\n\u001b[31m    if (d.id !== dispatchId) continue;\u001b[39m\n\u001b[31m    for (const c of d.consignees) {\u001b[39m\n\u001b[31m      if (c.docket === docket) {\u001b[39m\n\u001b[31m        c.status = result.status || c.status;\u001b[39m\n\u001b[31m        c.lastEvent = result.lastEvent || c.lastEvent;\u001b[39m\n\u001b[31m        c.lastUpdated = new Date().toISOString();\u001b[39m\n\u001b[31m      }\u001b[39m\n\u001b[31m    }\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m  saveData();\u001b[39m\n\u001b[31m  renderDashboard();\u001b[39m\n\u001b[31m  if (result.status === 'failed') {\u001b[39m\n\u001b[31m    // Open courier site as fallback\u001b[39m\n\u001b[31m    openCourierTrackUrl(docket, courier);\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction showDocketDetail(docket, courier, dispatchId) {\u001b[39m\n\u001b[31m  const d = dispatches.find(x => x.id === dispatchId);\u001b[39m\n\u001b[31m  const c = d?.consignees?.find(x => x.docket === docket);\u001b[39m\n\u001b[31m  if (!c) return;\u001b[39m\n\u001b[31m  const content = document.getElementById('detailContent');\u001b[39m\n\u001b[31m  content.innerHTML = `\u001b[39m\n\u001b[31m    <div class=\\\"sheet-title\\\">${docket}</div>\u001b[39m\n\u001b[31m    <div class=\\\"info-tag\\\">📍 <strong>${c.name}</strong> — ${c.city}</div>\u001b[39m\n\u001b[31m    ${c.phone ? `<div class=\\\"info-tag\\\">📞 ${c.phone}</div>` : ''}\u001b[39m\n\u001b[31m    <div class=\\\"info-tag\\\">📦 ${c.material||'—'} ${c.qty ? '('+c.qty+' pcs)':''}</div>\u001b[39m\n\u001b[31m    <div class=\\\"info-tag\\\">🚚 ${courier||'—'} · <span class=\\\"status-pill sp-${c.status||'pending'}\\\">${c.status||'pending'}</span></div>\u001b[39m\n\u001b[31m    ${c.lastEvent ? `<div class=\\\"info-tag\\\">🕐 ${c.lastEvent}</div>` : ''}\u001b[39m\n\u001b[31m    <div class=\\\"sec-div\\\"><span>Tracking History</span></div>\u001b[39m\n\u001b[31m    <div id=\\\"docketTimeline\\\">\u001b[39m\n\u001b[31m      ${c.timeline?.length ?\u001b[39m\n\u001b[31m        c.timeline.map(e => `<div class=\\\"track-evt\\\"><span class=\\\"track-time\\\">${e.time||''}</span><div><div class=\\\"track-desc\\\">${e.desc||''}</div><div class=\\\"track-loc\\\">${e.loc||''}</div></div></div>`).join('') :\u001b[39m\n\u001b[31m        '<div style=\\\"color:var(--t3);font-size:.78rem;padding:10px 0;\\\">No timeline data. Click Live Track to fetch.</div>'}\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m    <div style=\\\"display:flex;gap:6px;flex-wrap:wrap;margin-top:14px;\\\">\u001b[39m\n\u001b[31m      <button class=\\\"btn btn-ghost\\\" style=\\\"flex:1;\\\" onclick=\\\"openCourierTrackUrl('${docket}','${courier}')\\\">🔗 Courier Site</button>\u001b[39m\n\u001b[31m      <button class=\\\"btn btn-orange\\\" style=\\\"flex:1;\\\" onclick=\\\"liveTrackTile('${docket}','${courier}','${dispatchId}');closeOverlay('detailOverlay')\\\">⚡ Live Track</button>\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m    <div style=\\\"display:flex;gap:6px;flex-wrap:wrap;margin-top:6px;\\\">\u001b[39m\n\u001b[31m      <button class=\\\"btn-wa\\\" style=\\\"flex:1;padding:8px;\\\" onclick=\\\"closeOverlay('detailOverlay');showPODUpload('${docket}','${dispatchId}')\\\">📸 Upload POD</button>\u001b[39m\n\u001b[31m      ${c.podUploaded ? '<span style=\\\"color:var(--grn);font-size:.72rem;font-family:var(--fnt-mono);padding:6px;\\\">✓ POD uploaded</span>' : ''}\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m  `;\u001b[39m\n\u001b[31m  openOverlay('detailOverlay');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31m// TRACKING ENGINE\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31mfunction _normaliseStatus(raw) {\u001b[39m\n\u001b[31m  const s = (raw || '').toLowerCase().trim();\u001b[39m\n\u001b[31m  if (!s || s === 'error') return null;\u001b[39m\n\u001b[31m  if (s.includes('undeliver') || s.includes('delivery_fail') ||\u001b[39m\n\u001b[31m      s.includes('failed_delivery') || s.includes('not deliver') ||\u001b[39m\n\u001b[31m      s.includes('return')  || s.includes('rto')) return 'pending';\u001b[39m\n\u001b[31m  if (s.includes('deliver'))                       return 'delivered';\u001b[39m\n\u001b[31m  if (s.includes('transit') || s.includes('hub') ||\u001b[39m\n\u001b[31m      s.includes('inbound') || s.includes('outbound') ||\u001b[39m\n\u001b[31m      s.includes('out for'))                       return 'transit';\u001b[39m\n\u001b[31m  if (s.includes('book') || s.includes('pick'))   return 'booked';\u001b[39m\n\u001b[31m  return s;\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31masync function _fetchWithRetry(url, opts, maxRetries = 1, retryDelayMs = 4000) {\u001b[39m\n\u001b[31m  for (let attempt = 0; attempt <= maxRetries; attempt++) {\u001b[39m\n\u001b[31m    try {\u001b[39m\n\u001b[31m      const resp = await fetch(url, opts);\u001b[39m\n\u001b[31m      if (resp.ok) return resp;\u001b[39m\n\u001b[31m    } catch(e) {\u001b[39m\n\u001b[31m      if (attempt === maxRetries) throw e;\u001b[39m\n\u001b[31m      await new Promise(r => setTimeout(r, retryDelayMs));\u001b[39m\n\u001b[31m    }\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m  return null;\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31masync function fetchTracking(docket, courier) {\u001b[39m\n\u001b[31m  // Strategy 1: VPS proxy (SkyKing + others that block direct fetch)\u001b[39m\n\u001b[31m  const proxyUrl = window.appConfig.proxy_url;\u001b[39m\n\u001b[31m  if (proxyUrl) {\u001b[39m\n\u001b[31m    try {\u001b[39m\n\u001b[31m      const url  = `${proxyUrl}?courier=${encodeURIComponent(courier)}&docket=${encodeURIComponent(docket)}`;\u001b[39m\n\u001b[31m      const resp = await _fetchWithRetry(url, { signal: AbortSignal.timeout(15000) });\u001b[39m\n\u001b[31m      if (resp) {\u001b[39m\n\u001b[31m        const data      = await resp.json();\u001b[39m\n\u001b[31m        const normStatus = _normaliseStatus(data.status);\u001b[39m\n\u001b[31m        if (normStatus) {\u001b[39m\n\u001b[31m          return { status: normStatus, lastEvent: data.lastEvent || '', timeline: data.timeline || [] };\u001b[39m\n\u001b[31m        }\u001b[39m\n\u001b[31m        openCourierTrackUrl(docket, courier);\u001b[39m\n\u001b[31m        return { status: 'transit', lastEvent: data.lastEvent || 'Check courier site', timeline: [] };\u001b[39m\n\u001b[31m      }\u001b[39m\n\u001b[31m    } catch(e) {}\u001b[39m\n\u001b[31m  }·\u001b[39m\n\u001b[31m  // Strategy 2: Delhivery direct API\u001b[39m\n\u001b[31m  if (courier === 'Delhivery') {\u001b[39m\n\u001b[31m    try {\u001b[39m\n\u001b[31m      const resp = await _fetchWithRetry(\u001b[39m\n\u001b[31m        `https://dlv-api.delhivery.com/v3/track?waybill=${docket}`,\u001b[39m\n\u001b[31m        { signal: AbortSignal.timeout(8000) }\u001b[39m\n\u001b[31m      );\u001b[39m\n\u001b[31m      if (resp) {\u001b[39m\n\u001b[31m        const data = await resp.json();\u001b[39m\n\u001b[31m        const pkg  = data.ShipmentData?.[0]?.Shipment;\u001b[39m\n\u001b[31m        if (pkg) {\u001b[39m\n\u001b[31m          const normStatus = _normaliseStatus(pkg.Status?.Status);\u001b[39m\n\u001b[31m          return { status: normStatus || 'transit', lastEvent: pkg.Status?.Instructions || '', timeline: [] };\u001b[39m\n\u001b[31m        }\u001b[39m\n\u001b[31m      }\u001b[39m\n\u001b[31m    } catch(e) {}\u001b[39m\n\u001b[31m  }·\u001b[39m\n\u001b[31m  return { status: 'failed', lastEvent: 'Open courier site to check', timeline: [] };\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction openCourierTrackUrl(docket, courier) {\u001b[39m\n\u001b[31m  const m = COURIER_MASTER[courier];\u001b[39m\n\u001b[31m  if (!m) return;\u001b[39m\n\u001b[31m  const url = m.trackUrl.replace('{docket}', docket);\u001b[39m\n\u001b[31m  window.open(url, '_blank');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// Track tab - multiple dockets\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31m// TRACK TAB — new logic\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31mlet _trackSelected = new Set(); // dockets currently selected for tracking·\u001b[39m\n\u001b[31m// Called when Track tab opens or courier changes\u001b[39m\n\u001b[31mfunction buildTrackDocketChips() {\u001b[39m\n\u001b[31m  const courier = document.getElementById('t_courier').value;\u001b[39m\n\u001b[31m  const m = COURIER_MASTER[courier];\u001b[39m\n\u001b[31m  const maxBulk = m?.maxBulk || 1;\u001b[39m\n\u001b[31m  const limitEl = document.getElementById('trackCourierLimit');\u001b[39m\n\u001b[31m  if (limitEl) limitEl.textContent = `Max ${maxBulk} at once for ${courier}`;·\u001b[39m\n\u001b[31m  // Gather all dockets from active dispatches for this courier\u001b[39m\n\u001b[31m  const activeDis = dispatches.filter(d => d.status !== 'draft');\u001b[39m\n\u001b[31m  const allDockets = activeDis.flatMap(d =>\u001b[39m\n\u001b[31m    (d.consignees || [])\u001b[39m\n\u001b[31m      .filter(c => c.docket && d.courier === courier)\u001b[39m\n\u001b[31m      .map(c => ({ docket: c.docket, name: c.name, city: c.city, status: c.status || 'booked' }))\u001b[39m\n\u001b[31m  );·\u001b[39m\n\u001b[31m  // Also include dockets from OTHER couriers as un-selectable reference\u001b[39m\n\u001b[31m  const otherDockets = activeDis.flatMap(d =>\u001b[39m\n\u001b[31m    (d.consignees || [])\u001b[39m\n\u001b[31m      .filter(c => c.docket && d.courier !== courier)\u001b[39m\n\u001b[31m      .map(c => ({ docket: c.docket, name: c.name, courier: d.courier }))\u001b[39m\n\u001b[31m  );·\u001b[39m\n\u001b[31m  const chips = document.getElementById('trackDocketChips');\u001b[39m\n\u001b[31m  if (!allDockets.length && !otherDockets.length) {\u001b[39m\n\u001b[31m    chips.innerHTML = '<span style=\\\"font-size:.72rem;color:var(--t3);font-family:var(--fnt-mono);\\\">No dispatched dockets yet — enter manually below</span>';\u001b[39m\n\u001b[31m    return;\u001b[39m\n\u001b[31m  }·\u001b[39m\n\u001b[31m  const spClr = { delivered:'var(--grn)', transit:'var(--ylw)', booked:'var(--sky)', pending:'var(--t4)' };·\u001b[39m\n\u001b[31m  chips.innerHTML = [\u001b[39m\n\u001b[31m    // This courier's dockets\u001b[39m\n\u001b[31m    ...allDockets.map(d => {\u001b[39m\n\u001b[31m      const sel = _trackSelected.has(d.docket);\u001b[39m\n\u001b[31m      const clr = spClr[d.status] || 'var(--t4)';\u001b[39m\n\u001b[31m      return `<button onclick=\\\"toggleTrackDocket('${d.docket}')\\\" id=\\\"chip_${d.docket}\\\"\u001b[39m\n\u001b[31m        style=\\\"font-family:var(--fnt-mono);font-size:.72rem;padding:4px 10px;border-radius:20px;\u001b[39m\n\u001b[31m        cursor:pointer;transition:all .2s;border:1px solid ${sel ? 'var(--acc)' : 'var(--bdr)'};\u001b[39m\n\u001b[31m        background:${sel ? 'rgba(249,115,22,.15)' : 'var(--sur2)'};\u001b[39m\n\u001b[31m        color:${sel ? 'var(--acc)' : 'var(--t2)'};\\\">\u001b[39m\n\u001b[31m        ${d.docket} <span style=\\\"font-size:.6rem;color:${clr};\\\">● ${d.status}</span>\u001b[39m\n\u001b[31m        ${d.city ? `<span style=\\\"font-size:.6rem;color:var(--t3);margin-left:4px;\\\">${d.city}</span>` : ''}\u001b[39m\n\u001b[31m      </button>`;\u001b[39m\n\u001b[31m    }),\u001b[39m\n\u001b[31m    // Other couriers — show greyed, click selects and switches courier\u001b[39m\n\u001b[31m    ...otherDockets.slice(0, 5).map(d =>\u001b[39m\n\u001b[31m      `<span title=\\\"${d.courier}\\\" style=\\\"font-family:var(--fnt-mono);font-size:.68rem;padding:3px 8px;\u001b[39m\n\u001b[31m        border-radius:20px;border:1px dashed var(--bdr);color:var(--t4);cursor:default;\\\">\u001b[39m\n\u001b[31m        ${d.docket} <span style=\\\"font-size:.55rem;\\\">${d.courier}</span>\u001b[39m\n\u001b[31m      </span>`\u001b[39m\n\u001b[31m    )\u001b[39m\n\u001b[31m  ].join('');·\u001b[39m\n\u001b[31m  updateTrackSummary();\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction toggleTrackDocket(docket) {\u001b[39m\n\u001b[31m  const courier = document.getElementById('t_courier').value;\u001b[39m\n\u001b[31m  const m = COURIER_MASTER[courier];\u001b[39m\n\u001b[31m  const maxBulk = m?.maxBulk || 1;·\u001b[39m\n\u001b[31m  if (_trackSelected.has(docket)) {\u001b[39m\n\u001b[31m    _trackSelected.delete(docket);\u001b[39m\n\u001b[31m  } else {\u001b[39m\n\u001b[31m    if (_trackSelected.size >= maxBulk) {\u001b[39m\n\u001b[31m      showToast(`${courier} supports max ${maxBulk} docket(s) at once`, 'err');\u001b[39m\n\u001b[31m      return;\u001b[39m\n\u001b[31m    }\u001b[39m\n\u001b[31m    _trackSelected.add(docket);\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m  buildTrackDocketChips(); // re-render chips with updated selection\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction addManualDockets() {\u001b[39m\n\u001b[31m  const raw = document.getElementById('t_manual_dockets').value;\u001b[39m\n\u001b[31m  const courier = document.getElementById('t_courier').value;\u001b[39m\n\u001b[31m  const m = COURIER_MASTER[courier];\u001b[39m\n\u001b[31m  const maxBulk = m?.maxBulk || 1;\u001b[39m\n\u001b[31m  const newDockets = raw.split(/[,\\\\n]/).map(d => d.trim()).filter(Boolean);\u001b[39m\n\u001b[31m  let added = 0;\u001b[39m\n\u001b[31m  for (const d of newDockets) {\u001b[39m\n\u001b[31m    if (_trackSelected.size >= maxBulk) {\u001b[39m\n\u001b[31m      showToast(`Max ${maxBulk} reached for ${courier}`, 'err');\u001b[39m\n\u001b[31m      break;\u001b[39m\n\u001b[31m    }\u001b[39m\n\u001b[31m    _trackSelected.add(d);\u001b[39m\n\u001b[31m    added++;\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m  document.getElementById('t_manual_dockets').value = '';\u001b[39m\n\u001b[31m  buildTrackDocketChips();\u001b[39m\n\u001b[31m  if (added) showToast(`${added} docket(s) added ✓`, 'ok');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction clearTrackSelection() {\u001b[39m\n\u001b[31m  _trackSelected.clear();\u001b[39m\n\u001b[31m  buildTrackDocketChips();\u001b[39m\n\u001b[31m  document.getElementById('trackResultsArea').innerHTML = '';\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction updateTrackSummary() {\u001b[39m\n\u001b[31m  const summary = document.getElementById('trackSelectedSummary');\u001b[39m\n\u001b[31m  const countEl = document.getElementById('trackSelectedCount');\u001b[39m\n\u001b[31m  const listEl  = document.getElementById('trackSelectedList');\u001b[39m\n\u001b[31m  if (!summary) return;\u001b[39m\n\u001b[31m  const arr = Array.from(_trackSelected);\u001b[39m\n\u001b[31m  summary.style.display = arr.length ? 'block' : 'none';\u001b[39m\n\u001b[31m  if (countEl) countEl.textContent = `${arr.length} docket(s) selected`;\u001b[39m\n\u001b[31m  if (listEl)  listEl.textContent  = arr.slice(0, 5).join(', ') + (arr.length > 5 ? `... +${arr.length-5}` : '');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction openTrackOnCourierSite() {\u001b[39m\n\u001b[31m  const courier = document.getElementById('t_courier').value;\u001b[39m\n\u001b[31m  const dockets = Array.from(_trackSelected);\u001b[39m\n\u001b[31m  if (!dockets.length) { showToast('Select dockets first', 'err'); return; }\u001b[39m\n\u001b[31m  const m = COURIER_MASTER[courier];\u001b[39m\n\u001b[31m  if (!m) return;\u001b[39m\n\u001b[31m  // SkyKing: open with all CNOs comma-separated in URL\u001b[39m\n\u001b[31m  let url;\u001b[39m\n\u001b[31m  if (courier === 'SkyKing') {\u001b[39m\n\u001b[31m    url = `https://skyking.co/track?cno=${dockets.join(',')}`;\u001b[39m\n\u001b[31m  } else if (dockets.length === 1) {\u001b[39m\n\u001b[31m    url = m.trackUrl.replace('{docket}', dockets[0]);\u001b[39m\n\u001b[31m  } else {\u001b[39m\n\u001b[31m    url = m.trackBulkUrl || m.trackUrl.replace('{docket}', dockets[0]);\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m  window.open(url, '_blank');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31masync function trackSelected() {\u001b[39m\n\u001b[31m  const courier = document.getElementById('t_courier').value;\u001b[39m\n\u001b[31m  const dockets = Array.from(_trackSelected);\u001b[39m\n\u001b[31m  if (!dockets.length) { showToast('Select at least one docket', 'err'); return; }·\u001b[39m\n\u001b[31m  const area = document.getElementById('trackResultsArea');\u001b[39m\n\u001b[31m  area.innerHTML = `<div class=\\\"card\\\" style=\\\"text-align:center;padding:20px;\\\">\u001b[39m\n\u001b[31m    <div style=\\\"font-size:1.5rem;margin-bottom:8px;\\\">⏳</div>\u001b[39m\n\u001b[31m    <div style=\\\"font-family:var(--fnt-mono);font-size:.78rem;color:var(--t2);\\\">Tracking ${dockets.length} docket(s) via ${courier}...</div>\u001b[39m\n\u001b[31m  </div>`;·\u001b[39m\n\u001b[31m  let resultsHTML = '';\u001b[39m\n\u001b[31m  for (const docket of dockets) {\u001b[39m\n\u001b[31m    const result = await fetchTracking(docket, courier);\u001b[39m\n\u001b[31m    const sp = (!result.status || result.status === 'failed') ? 'pending' : result.status;\u001b[39m\n\u001b[31m    const spClr = { delivered:'var(--grn)', transit:'var(--ylw)', booked:'var(--sky)', pending:'var(--t3)' };\u001b[39m\n\u001b[31m    const clr = spClr[sp] || 'var(--t3)';\u001b[39m\n\u001b[31m    // Update stored dispatch if this docket exists in our records\u001b[39m\n\u001b[31m    for (const d of dispatches) {\u001b[39m\n\u001b[31m      for (const c of (d.consignees||[])) {\u001b[39m\n\u001b[31m        if (c.docket === docket && result.status && result.status !== 'failed') {\u001b[39m\n\u001b[31m          c.status = result.status;\u001b[39m\n\u001b[31m          c.lastEvent = result.lastEvent;\u001b[39m\n\u001b[31m          c.lastTrackedAt = new Date().toISOString();\u001b[39m\n\u001b[31m        }\u001b[39m\n\u001b[31m      }\u001b[39m\n\u001b[31m    }\u001b[39m\n\u001b[31m    resultsHTML += `\u001b[39m\n\u001b[31m    <div style=\\\"background:var(--sur);border:1px solid var(--bdr);border-left:3px solid ${clr};\u001b[39m\n\u001b[31m      border-radius:var(--r-sm);padding:10px 12px;margin-bottom:8px;\\\">\u001b[39m\n\u001b[31m      <div style=\\\"display:flex;justify-content:space-between;align-items:center;margin-bottom:6px;\\\">\u001b[39m\n\u001b[31m        <span style=\\\"font-family:var(--fnt-mono);font-size:.9rem;color:var(--sky);\\\">${docket}</span>\u001b[39m\n\u001b[31m        <span class=\\\"status-pill sp-${sp}\\\">${sp}</span>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      <div style=\\\"font-size:.75rem;color:var(--t2);font-family:var(--fnt-mono);\\\">${result.lastEvent || '—'}</div>\u001b[39m\n\u001b[31m      ${result.lastScanLocation ? `<div style=\\\"font-size:.68rem;color:var(--t3);margin-top:3px;\\\">📍 ${result.lastScanLocation}</div>` : ''}\u001b[39m\n\u001b[31m      <div style=\\\"display:flex;gap:8px;margin-top:8px;\\\">\u001b[39m\n\u001b[31m        <button class=\\\"btn-sm\\\" onclick=\\\"openCourierTrackUrl('${docket}','${courier}')\\\">🔗 Open on ${courier}</button>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m    </div>`;\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m  saveData();\u001b[39m\n\u001b[31m  area.innerHTML = `<div style=\\\"margin-top:4px;\\\">${resultsHTML}</div>`;\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// Keep old names as aliases for compatibility\u001b[39m\n\u001b[31mfunction trackMultiple() { trackSelected(); }\u001b[39m\n\u001b[31mfunction openCourierSite() { openTrackOnCourierSite(); }·\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31m// HISTORY\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31mfunction renderHistory() {\u001b[39m\n\u001b[31m  const list = document.getElementById('historyList');\u001b[39m\n\u001b[31m  if (!dispatches.length) {\u001b[39m\n\u001b[31m    list.innerHTML = '<div class=\\\"empty\\\"><div class=\\\"e-icon\\\">📭</div><p>No dispatches yet.</p></div>';\u001b[39m\n\u001b[31m    return;\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m  list.innerHTML = dispatches.map(d => {\u001b[39m\n\u001b[31m    const cons = d.consignees?.length || 0;\u001b[39m\n\u001b[31m    const docketed = d.consignees?.filter(c=>c.docket).length || 0;\u001b[39m\n\u001b[31m    const clr = d.status === 'draft' ? 'var(--ylw)' : 'var(--grn)';\u001b[39m\n\u001b[31m    return `\u001b[39m\n\u001b[31m    <div class=\\\"docket-tile\\\" onclick=\\\"showDispatchDetail('${d.id}')\\\">\u001b[39m\n\u001b[31m      <div class=\\\"docket-tile-hdr\\\">\u001b[39m\n\u001b[31m        <div class=\\\"docket-tile-left\\\">\u001b[39m\n\u001b[31m          <div class=\\\"docket-num\\\" style=\\\"font-size:.78rem;\\\">${d.id}</div>\u001b[39m\n\u001b[31m          <div class=\\\"docket-store\\\">${d.client||'No client'} ${d.jobref ? '› '+d.jobref : ''}</div>\u001b[39m\n\u001b[31m          <div class=\\\"docket-courier\\\">📦 ${cons} consignees · ${docketed} docketed · ${d.courier||'—'} · ${d.dispatchDate||d.createdAt?.slice(0,10)||'—'}</div>\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m        <span class=\\\"status-pill\\\" style=\\\"background:rgba(0,0,0,.2);color:${clr};\\\">${d.status||'active'}</span>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m    </div>`;\u001b[39m\n\u001b[31m  }).join('');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction showDispatchDetail(id) {\u001b[39m\n\u001b[31m  const d = dispatches.find(x => x.id === id);\u001b[39m\n\u001b[31m  if (!d) return;\u001b[39m\n\u001b[31m  const content = document.getElementById('detailContent');\u001b[39m\n\u001b[31m  content.innerHTML = `\u001b[39m\n\u001b[31m    <div class=\\\"sheet-title\\\">${d.client} ${d.jobref ? '› '+d.jobref : ''}</div>\u001b[39m\n\u001b[31m    <div class=\\\"info-tag\\\"><strong>${d.id}</strong> · ${d.courier||'—'} · ${d.dispatchDate||'—'}</div>\u001b[39m\n\u001b[31m    <div class=\\\"sec-div\\\"><span>Consignees (${d.consignees?.length||0})</span></div>\u001b[39m\n\u001b[31m    ${(d.consignees||[]).map((c,i) => `\u001b[39m\n\u001b[31m      <div style=\\\"background:var(--sur2);border:1px solid var(--bdr);border-radius:var(--r-sm);padding:10px;margin-bottom:6px;\\\">\u001b[39m\n\u001b[31m        <div style=\\\"font-weight:600;margin-bottom:4px;\\\">${i+1}. ${c.name} — ${c.city}</div>\u001b[39m\n\u001b[31m        ${c.docket ? `<div style=\\\"font-family:var(--fnt-mono);font-size:.78rem;color:var(--sky);\\\">🔖 ${c.docket}</div>` : ''}\u001b[39m\n\u001b[31m        ${c.material ? `<div style=\\\"font-size:.75rem;color:var(--t2);\\\">📦 ${c.material} ${c.qty ? '('+c.qty+')':''}</div>` : ''}\u001b[39m\n\u001b[31m        <span class=\\\"status-pill sp-${c.status||'pending'}\\\" style=\\\"margin-top:4px;display:inline-block;\\\">${c.status||'pending'}</span>\u001b[39m\n\u001b[31m      </div>`).join('')}\u001b[39m\n\u001b[31m    ${d.remarks ? `<div class=\\\"info-tag\\\">📝 ${d.remarks}</div>` : ''}\u001b[39m\n\u001b[31m    <div class=\\\"btn-row\\\" style=\\\"margin-top:14px;\\\">\u001b[39m\n\u001b[31m      <button class=\\\"btn btn-ghost\\\" onclick=\\\"deleteDispatch('${d.id}');closeOverlay('detailOverlay')\\\">🗑️ Delete</button>\u001b[39m\n\u001b[31m      <button class=\\\"btn btn-orange\\\" onclick=\\\"closeOverlay('detailOverlay');showPane('dashboard')\\\">📊 View in Dashboard</button>\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m  `;\u001b[39m\n\u001b[31m  openOverlay('detailOverlay');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction deleteDispatch(id) {\u001b[39m\n\u001b[31m  if (!confirm('Delete this dispatch?')) return;\u001b[39m\n\u001b[31m  dispatches = dispatches.filter(d => d.id !== id);\u001b[39m\n\u001b[31m  saveData();\u001b[39m\n\u001b[31m  updateHistCount();\u001b[39m\n\u001b[31m  renderHistory();\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction updateHistCount() {\u001b[39m\n\u001b[31m  document.getElementById('histCount').textContent = dispatches.length;\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31m// GOOGLE SHEETS\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31masync function logToSheets(d) {\u001b[39m\n\u001b[31m  const url = window.appConfig.webhook_url;\u001b[39m\n\u001b[31m  if (!url) return;\u001b[39m\n\u001b[31m  try {\u001b[39m\n\u001b[31m    await fetch(url, {\u001b[39m\n\u001b[31m      method: 'POST', mode: 'no-cors',\u001b[39m\n\u001b[31m      headers: { 'Content-Type': 'application/json' },\u001b[39m\n\u001b[31m      body: JSON.stringify({\u001b[39m\n\u001b[31m        type: 'dispatch', id: d.id, client: d.client, jobref: d.jobref,\u001b[39m\n\u001b[31m        courier: d.courier, consignees: d.consignees?.length||0,\u001b[39m\n\u001b[31m        destinations: d.consignees?.map(c=>c.city).join(', ')||'',\u001b[39m\n\u001b[31m        dockets: d.consignees?.filter(c=>c.docket).map(c=>c.docket).join(', ')||'',\u001b[39m\n\u001b[31m        dispatchDate: d.dispatchDate, status: d.status,\u001b[39m\n\u001b[31m        timestamp: new Date().toISOString(), company: window.appConfig.company\u001b[39m\n\u001b[31m      })\u001b[39m\n\u001b[31m    });\u001b[39m\n\u001b[31m  } catch(e) {}\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31m// CONFIG — Google Sheet is the source of truth\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════·\u001b[39m\n\u001b[31mfunction onSheetsUrlChange() {\u001b[39m\n\u001b[31m  const url = document.getElementById('cfg_sheets_url').value.trim();\u001b[39m\n\u001b[31m  // Show sync button state\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction saveSheetsUrl() {\u001b[39m\n\u001b[31m  const url = document.getElementById('cfg_sheets_url').value.trim();\u001b[39m\n\u001b[31m  if (!url) { showToast('Enter the Apps Script URL first', 'err'); return; }\u001b[39m\n\u001b[31m  window.appConfig.sheets_url = url;\u001b[39m\n\u001b[31m  saveSheetsUrlLocal();\u001b[39m\n\u001b[31m  syncFromSheet(false);\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m/**\u001b[39m\n\u001b[31m * Fetch all config from Google Sheet and apply to appConfig.\u001b[39m\n\u001b[31m * Called on startup (silent=true) and manually (silent=false).\u001b[39m\n\u001b[31m */\u001b[39m\n\u001b[31masync function syncFromSheet(silent = false) {\u001b[39m\n\u001b[31m  const url = window.appConfig.sheets_url;\u001b[39m\n\u001b[31m  if (!url) { if (!silent) showToast('No Sheets URL configured', 'err'); return; }·\u001b[39m\n\u001b[31m  setSyncStatus('syncing', 'Syncing config from Google Sheet...');\u001b[39m\n\u001b[31m  try {\u001b[39m\n\u001b[31m    const resp = await fetch(url + '?action=pwa_config', {\u001b[39m\n\u001b[31m      method: 'GET',\u001b[39m\n\u001b[31m      redirect: 'follow'\u001b[39m\n\u001b[31m    });\u001b[39m\n\u001b[31m    if (!resp.ok) throw new Error('HTTP ' + resp.status);\u001b[39m\n\u001b[31m    const cfg = await resp.json();·\u001b[39m\n\u001b[31m    // Apply all values from sheet to appConfig\u001b[39m\n\u001b[31m    const keyMap = {\u001b[39m\n\u001b[31m      proxy_url:           'proxy_url',\u001b[39m\n\u001b[31m      webhook_url:         'webhook_url',\u001b[39m\n\u001b[31m      ollama_url:          'ollama_url',\u001b[39m\n\u001b[31m      ocr_mode:            'ocr_mode',\u001b[39m\n\u001b[31m      company:             'company',\u001b[39m\n\u001b[31m      city:                'city',\u001b[39m\n\u001b[31m      auto_log:            'auto_log',\u001b[39m\n\u001b[31m      refresh_on_load:     'refresh_on_load',\u001b[39m\n\u001b[31m      admin_password:      'admin_password',\u001b[39m\n\u001b[31m      n8n_dispatch_webhook:'n8n_dispatch_webhook',\u001b[39m\n\u001b[31m    };\u001b[39m\n\u001b[31m    Object.entries(keyMap).forEach(([sheetKey, cfgKey]) => {\u001b[39m\n\u001b[31m      if (cfg[sheetKey] !== undefined && cfg[sheetKey] !== '') {\u001b[39m\n\u001b[31m        window.appConfig[cfgKey] = cfg[sheetKey];\u001b[39m\n\u001b[31m      }\u001b[39m\n\u001b[31m    });·\u001b[39m\n\u001b[31m    // Apply courier master if provided\u001b[39m\n\u001b[31m    if (cfg._courier_master && Array.isArray(cfg._courier_master)) {\u001b[39m\n\u001b[31m      cfg._courier_master.forEach(c => {\u001b[39m\n\u001b[31m        if (c.name && c.trackUrl) {\u001b[39m\n\u001b[31m          COURIER_MASTER[c.name] = {\u001b[39m\n\u001b[31m            trackUrl: c.trackUrl,\u001b[39m\n\u001b[31m            trackBulkUrl: c.bulkUrl || '',\u001b[39m\n\u001b[31m            maxBulk: parseInt(c.maxBulk) || 1\u001b[39m\n\u001b[31m          };\u001b[39m\n\u001b[31m        }\u001b[39m\n\u001b[31m      });\u001b[39m\n\u001b[31m    }·\u001b[39m\n\u001b[31m    renderCourierMasterList();\u001b[39m\n\u001b[31m    renderSheetConfigDisplay(cfg);\u001b[39m\n\u001b[31m    setSyncStatus('ok', 'Synced from sheet · ' + new Date().toLocaleTimeString());\u001b[39m\n\u001b[31m    if (!silent) showToast('Config synced from sheet ✓', 'ok');·\u001b[39m\n\u001b[31m    // If refresh_on_load is true, trigger dashboard refresh\u001b[39m\n\u001b[31m    if (window.appConfig.refresh_on_load === 'true') {\u001b[39m\n\u001b[31m      setTimeout(refreshAllTracking, 2000);\u001b[39m\n\u001b[31m    }·\u001b[39m\n\u001b[31m  } catch(e) {\u001b[39m\n\u001b[31m    setSyncStatus('err', 'Sync failed: ' + e.message);\u001b[39m\n\u001b[31m    if (!silent) showToast('Sync failed — check URL', 'err');\u001b[39m\n\u001b[31m    console.error('Sheets sync error:', e);\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction setSyncStatus(state, msg) {\u001b[39m\n\u001b[31m  const el = document.getElementById('syncStatus');\u001b[39m\n\u001b[31m  const icon = document.getElementById('syncIcon');\u001b[39m\n\u001b[31m  const msgEl = document.getElementById('syncMsg');\u001b[39m\n\u001b[31m  el.style.display = 'flex';\u001b[39m\n\u001b[31m  icon.textContent = state === 'ok' ? '✅' : state === 'err' ? '❌' : '⏳';\u001b[39m\n\u001b[31m  msgEl.textContent = msg;\u001b[39m\n\u001b[31m  el.style.borderColor = state === 'ok' ? 'rgba(34,197,94,.3)' : state === 'err' ? 'rgba(239,68,68,.3)' : 'rgba(234,179,8,.3)';\u001b[39m\n\u001b[31m  el.style.background = state === 'ok' ? 'rgba(34,197,94,.05)' : state === 'err' ? 'rgba(239,68,68,.05)' : 'rgba(234,179,8,.05)';\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction renderSheetConfigDisplay(cfg) {\u001b[39m\n\u001b[31m  const display = document.getElementById('sheetConfigDisplay');\u001b[39m\n\u001b[31m  display.style.display = 'block';\u001b[39m\n\u001b[31m  document.getElementById('sheetSyncTime').textContent = 'Last sync: ' + new Date().toLocaleTimeString();\u001b[39m\n\u001b[31m  const showKeys = ['proxy_url','webhook_url','ollama_url','ocr_mode','company','city','auto_log'];\u001b[39m\n\u001b[31m  document.getElementById('sheetConfigRows').innerHTML = showKeys.map(k => {\u001b[39m\n\u001b[31m    const val = cfg[k] || window.appConfig[k] || '—';\u001b[39m\n\u001b[31m    const isEmpty = !cfg[k];\u001b[39m\n\u001b[31m    return `<div class=\\\"courier-master-row\\\">\u001b[39m\n\u001b[31m      <div class=\\\"cml\\\">\u001b[39m\n\u001b[31m        <div class=\\\"cml-name\\\" style=\\\"font-family:var(--fnt-mono);font-size:.75rem;\\\">${k}</div>\u001b[39m\n\u001b[31m        <div class=\\\"cml-url\\\" style=\\\"${isEmpty ? 'color:var(--red);' : ''}\\\">${isEmpty ? '⚠️ Not set in sheet' : val}</div>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m    </div>`;\u001b[39m\n\u001b[31m  }).join('');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m/**\u001b[39m\n\u001b[31m * Push current config changes back to the sheet.\u001b[39m\n\u001b[31m * Called when user edits config from PWA (rare — usually done in sheet directly).\u001b[39m\n\u001b[31m */\u001b[39m\n\u001b[31masync function pushConfigToSheet(updates) {\u001b[39m\n\u001b[31m  const url = window.appConfig.sheets_url;\u001b[39m\n\u001b[31m  if (!url) return;\u001b[39m\n\u001b[31m  try {\u001b[39m\n\u001b[31m    await fetch(url, {\u001b[39m\n\u001b[31m      method: 'POST', mode: 'no-cors',\u001b[39m\n\u001b[31m      headers: { 'Content-Type': 'application/json' },\u001b[39m\n\u001b[31m      body: JSON.stringify({ type: 'save_config', config: updates })\u001b[39m\n\u001b[31m    });\u001b[39m\n\u001b[31m  } catch(e) {}\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction clearAll() {\u001b[39m\n\u001b[31m  if (!confirm('Delete ALL dispatch data?')) return;\u001b[39m\n\u001b[31m  dispatches = [];\u001b[39m\n\u001b[31m  saveData();\u001b[39m\n\u001b[31m  updateHistCount();\u001b[39m\n\u001b[31m  renderDashboard();\u001b[39m\n\u001b[31m  renderHistory();\u001b[39m\n\u001b[31m  showToast('Cleared', 'ok');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction renderCourierMasterList() {\u001b[39m\n\u001b[31m  const el = document.getElementById('courierMasterList');\u001b[39m\n\u001b[31m  el.innerHTML = Object.entries(COURIER_MASTER).map(([name, info]) => `\u001b[39m\n\u001b[31m    <div class=\\\"courier-master-row\\\">\u001b[39m\n\u001b[31m      <div class=\\\"cml\\\">\u001b[39m\n\u001b[31m        <div class=\\\"cml-name\\\">${name}</div>\u001b[39m\n\u001b[31m        <div class=\\\"cml-url\\\">${info.trackUrl}</div>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      <button class=\\\"btn-sm\\\" onclick=\\\"window.open('${info.trackBulkUrl||info.trackUrl.replace('{docket}','')}','_blank')\\\">Open</button>\u001b[39m\n\u001b[31m    </div>`).join('');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction openCourierMaster() {\u001b[39m\n\u001b[31m  const ed = document.getElementById('courierMasterEditor');\u001b[39m\n\u001b[31m  ed.innerHTML = Object.entries(COURIER_MASTER).map(([name, info]) => `\u001b[39m\n\u001b[31m    <div style=\\\"margin-bottom:14px;\\\">\u001b[39m\n\u001b[31m      <div style=\\\"font-family:var(--fnt-head);font-size:.75rem;font-weight:700;color:var(--t2);text-transform:uppercase;letter-spacing:1.5px;margin-bottom:6px;\\\">${name}</div>\u001b[39m\n\u001b[31m      <div class=\\\"f\\\" style=\\\"margin-bottom:6px;\\\">\u001b[39m\n\u001b[31m        <label>Track URL (use {docket})</label>\u001b[39m\n\u001b[31m        <input type=\\\"text\\\" value=\\\"${info.trackUrl}\\\" id=\\\"cm_${name.replace(/\\\\s/g,'_')}\\\"\u001b[39m\n\u001b[31m          style=\\\"font-family:var(--fnt-mono);font-size:.75rem;\\\">\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m    </div>`).join('') +\u001b[39m\n\u001b[31m    `<button class=\\\"btn btn-orange\\\" onclick=\\\"saveCourierMaster()\\\">Save Courier Master</button>`;\u001b[39m\n\u001b[31m  openOverlay('courierMasterOverlay');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction saveCourierMaster() {\u001b[39m\n\u001b[31m  Object.keys(COURIER_MASTER).forEach(name => {\u001b[39m\n\u001b[31m    const el = document.getElementById('cm_' + name.replace(/\\\\s/g,'_'));\u001b[39m\n\u001b[31m    if (el) COURIER_MASTER[name].trackUrl = el.value;\u001b[39m\n\u001b[31m  });\u001b[39m\n\u001b[31m  closeOverlay('courierMasterOverlay');\u001b[39m\n\u001b[31m  renderCourierMasterList();\u001b[39m\n\u001b[31m  showToast('Courier master saved ✓', 'ok');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31m// UI UTILS\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31m// ── ADMIN AUTH ───────────────────────────────────────────────────────────\u001b[39m\n\u001b[31mlet _adminAuthed = false;\u001b[39m\n\u001b[31mlet _lastAdminPane = null;·\u001b[39m\n\u001b[31mfunction showPane(name) {\u001b[39m\n\u001b[31m  // Config tab requires admin auth — re-ask if navigated away\u001b[39m\n\u001b[31m  if (name === 'config') {\u001b[39m\n\u001b[31m    if (!_adminAuthed) {\u001b[39m\n\u001b[31m      _lastAdminPane = 'config';\u001b[39m\n\u001b[31m      showAdminLogin();\u001b[39m\n\u001b[31m      return;\u001b[39m\n\u001b[31m    }\u001b[39m\n\u001b[31m  } else {\u001b[39m\n\u001b[31m    // User navigated away from config — revoke auth\u001b[39m\n\u001b[31m    if (_lastAdminPane === 'config') _adminAuthed = false;\u001b[39m\n\u001b[31m    _lastAdminPane = name;\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m  document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));\u001b[39m\n\u001b[31m  document.querySelectorAll('.pane').forEach(p => p.classList.remove('active'));\u001b[39m\n\u001b[31m  document.getElementById('tab-'+name).classList.add('active');\u001b[39m\n\u001b[31m  document.getElementById('pane-'+name).classList.add('active');\u001b[39m\n\u001b[31m  if (name === 'dashboard') renderDashboard();\u001b[39m\n\u001b[31m  if (name === 'history') renderHistory();\u001b[39m\n\u001b[31m  if (name === 'track') buildTrackDocketChips();\u001b[39m\n\u001b[31m  if (name === 'config') { document.getElementById('cfg_sheets_url').value = window.appConfig.sheets_url || ''; }\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction showAdminLogin() {\u001b[39m\n\u001b[31m  // Password replaced by hub identity — only harish/pramod can access config\u001b[39m\n\u001b[31m  if (['harish', 'pramod'].includes(window._hubUser?.id)) {\u001b[39m\n\u001b[31m    _adminAuthed = true;\u001b[39m\n\u001b[31m    showPane('config');\u001b[39m\n\u001b[31m  } else {\u001b[39m\n\u001b[31m    showToast('Config access restricted to admin users', 'err');\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31masync function submitAdminLogin() {\u001b[39m\n\u001b[31m  showAdminLogin(); // no-op shim — kept for any lingering HTML onclick refs\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31mfunction openOverlay(id) { document.getElementById(id).classList.add('open'); }\u001b[39m\n\u001b[31mfunction closeOverlay(id) { document.getElementById(id).classList.remove('open'); }·\u001b[39m\n\u001b[31mlet toastTimer;\u001b[39m\n\u001b[31mfunction showToast(msg, type='') {\u001b[39m\n\u001b[31m  const t = document.getElementById('toast');\u001b[39m\n\u001b[31m  t.textContent = msg;\u001b[39m\n\u001b[31m  t.className = 'toast show' + (type ? ' '+type : '');\u001b[39m\n\u001b[31m  clearTimeout(toastTimer);\u001b[39m\n\u001b[31m  toastTimer = setTimeout(() => t.classList.remove('show'), 2800);\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction showOCRWorking(show, msg='') {\u001b[39m\n\u001b[31m  const el = document.getElementById('ocrWorking');\u001b[39m\n\u001b[31m  el.classList.toggle('visible', show);\u001b[39m\n\u001b[31m  if (msg) document.getElementById('ocrWorkingText').textContent = msg;\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction showAIProgress(label, pct) {\u001b[39m\n\u001b[31m  const el = document.getElementById('aiProgress');\u001b[39m\n\u001b[31m  el.classList.add('visible');\u001b[39m\n\u001b[31m  document.getElementById('aiProgLabel').textContent = label;\u001b[39m\n\u001b[31m  document.getElementById('aiProgFill').style.width = pct + '%';\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31mfunction hideAIProgress() {\u001b[39m\n\u001b[31m  document.getElementById('aiProgress').classList.remove('visible');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction updateNetDot() {\u001b[39m\n\u001b[31m  document.getElementById('netDot').style.background = navigator.onLine ? 'var(--grn)' : 'var(--red)';\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction setToday() {\u001b[39m\n\u001b[31m  document.getElementById('n_date').value = new Date().toISOString().split('T')[0];\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m// ── EXPORT CLEAN SHEET ───────────────────────────────────────────────────\u001b[39m\n\u001b[31mfunction exportCleanSheet() {\u001b[39m\n\u001b[31m  const pane = document.getElementById('verifyPane');\u001b[39m\n\u001b[31m  const d = pane._pendingDispatch;\u001b[39m\n\u001b[31m  if (!d || !d.consignees?.length) { showToast('No data to export', 'err'); return; }·\u001b[39m\n\u001b[31m  // Build CSV\u001b[39m\n\u001b[31m  const headers = ['S.No.','Store Name','City','Pincode','Phone','Address','Docket No.','Material','Courier','Status'];\u001b[39m\n\u001b[31m  const rows = d.consignees.map((c, i) => [\u001b[39m\n\u001b[31m    i + 1,\u001b[39m\n\u001b[31m    `\\\"${(c.name||'').replace(/\\\"/g,'\\\"\\\"')}\\\"`,\u001b[39m\n\u001b[31m    `\\\"${(c.city||'').replace(/\\\"/g,'\\\"\\\"')}\\\"`,\u001b[39m\n\u001b[31m    c.pincode || '',\u001b[39m\n\u001b[31m    c.phone || '',\u001b[39m\n\u001b[31m    `\\\"${(c.address||'').replace(/\\\"/g,'\\\"\\\"')}\\\"`,\u001b[39m\n\u001b[31m    c.docket || '',\u001b[39m\n\u001b[31m    `\\\"${(c.material||'').replace(/\\\"/g,'\\\"\\\"')}\\\"`,\u001b[39m\n\u001b[31m    d.courier || '',\u001b[39m\n\u001b[31m    c.status || 'pending'\u001b[39m\n\u001b[31m  ]);·\u001b[39m\n\u001b[31m  const csv = [headers.join(','), ...rows.map(r => r.join(','))].join('\\\\n');\u001b[39m\n\u001b[31m  const blob = new Blob([csv], { type: 'text/csv' });\u001b[39m\n\u001b[31m  const url = URL.createObjectURL(blob);\u001b[39m\n\u001b[31m  const a = document.createElement('a');\u001b[39m\n\u001b[31m  a.href = url;\u001b[39m\n\u001b[31m  a.download = `360LM_Dispatch_${d.client||'export'}_${d.dispatchDate||new Date().toISOString().slice(0,10)}_clean.csv`;\u001b[39m\n\u001b[31m  a.click();\u001b[39m\n\u001b[31m  URL.revokeObjectURL(url);\u001b[39m\n\u001b[31m  showToast('Clean sheet exported ✓', 'ok');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// ── DOCKET SLIP UPLOAD — SKIP OCR IF DOCKET ALREADY MAPPED ───────────────\u001b[39m\n\u001b[31mfunction shouldSkipOCR() {\u001b[39m\n\u001b[31m  // If any consignee already has a docket (from store list mapping), skip OCR extraction\u001b[39m\n\u001b[31m  const consList = document.getElementById('consList');\u001b[39m\n\u001b[31m  if (!consList) return false;\u001b[39m\n\u001b[31m  const cards = consList.querySelectorAll('.cons-card');\u001b[39m\n\u001b[31m  let hasAnyDocket = false;\u001b[39m\n\u001b[31m  cards.forEach(card => {\u001b[39m\n\u001b[31m    const d = card.querySelector('.cons-docket-input')?.value?.trim();\u001b[39m\n\u001b[31m    if (d) hasAnyDocket = true;\u001b[39m\n\u001b[31m  });\u001b[39m\n\u001b[31m  return hasAnyDocket;\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// ─── TRACKING UPDATE TO SHEETS ──────────────────────────────────────────\u001b[39m\n\u001b[31masync function logTrackingUpdate(docket, status, lastEvent, location) {\u001b[39m\n\u001b[31m  const url = window.appConfig.webhook_url;\u001b[39m\n\u001b[31m  if (!url) return;\u001b[39m\n\u001b[31m  try {\u001b[39m\n\u001b[31m    await fetch(url, {\u001b[39m\n\u001b[31m      method: 'POST', mode: 'no-cors',\u001b[39m\n\u001b[31m      headers: { 'Content-Type': 'application/json' },\u001b[39m\n\u001b[31m      body: JSON.stringify({\u001b[39m\n\u001b[31m        type: 'tracking_update',\u001b[39m\n\u001b[31m        docket, status, lastEvent, location,\u001b[39m\n\u001b[31m        timestamp: new Date().toISOString()\u001b[39m\n\u001b[31m      })\u001b[39m\n\u001b[31m    });\u001b[39m\n\u001b[31m  } catch(e) {}\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// ─── POD / RECIPIENT PHOTO UPLOAD ───────────────────────────────────────\u001b[39m\n\u001b[31m// Stores base64 locally, pushes to Sheets webhook (Apps Script saves to Drive)\u001b[39m\n\u001b[31mfunction showPODUpload(docket, dispatchId) {\u001b[39m\n\u001b[31m  const overlay = document.getElementById('podOverlay');\u001b[39m\n\u001b[31m  overlay.dataset.docket = docket;\u001b[39m\n\u001b[31m  overlay.dataset.dispatchId = dispatchId;\u001b[39m\n\u001b[31m  document.getElementById('podPreview').innerHTML = '';\u001b[39m\n\u001b[31m  document.getElementById('podFileInput').value = '';\u001b[39m\n\u001b[31m  document.getElementById('recipientFileInput').value = '';\u001b[39m\n\u001b[31m  openOverlay('podOverlay');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31masync function uploadPODFiles() {\u001b[39m\n\u001b[31m  const overlay = document.getElementById('podOverlay');\u001b[39m\n\u001b[31m  const docket = overlay.dataset.docket;\u001b[39m\n\u001b[31m  const dispatchId = overlay.dataset.dispatchId;\u001b[39m\n\u001b[31m  const podFile = document.getElementById('podFileInput').files[0];\u001b[39m\n\u001b[31m  const recipFile = document.getElementById('recipientFileInput').files[0];\u001b[39m\n\u001b[31m  if (!podFile && !recipFile) { showToast('Select at least one photo', 'err'); return; }·\u001b[39m\n\u001b[31m  showToast('Uploading...', 'info');\u001b[39m\n\u001b[31m  const toBase64 = f => new Promise((res,rej) => {\u001b[39m\n\u001b[31m    const r = new FileReader(); r.onload = e => res(e.target.result); r.onerror = rej; r.readAsDataURL(f);\u001b[39m\n\u001b[31m  });·\u001b[39m\n\u001b[31m  const payload = {\u001b[39m\n\u001b[31m    type: 'pod_upload',\u001b[39m\n\u001b[31m    docket, dispatchId,\u001b[39m\n\u001b[31m    timestamp: new Date().toISOString()\u001b[39m\n\u001b[31m  };\u001b[39m\n\u001b[31m  if (podFile)   payload.pod_image = await toBase64(podFile);\u001b[39m\n\u001b[31m  if (recipFile) payload.recipient_image = await toBase64(recipFile);·\u001b[39m\n\u001b[31m  // Update local store\u001b[39m\n\u001b[31m  for (const d of dispatches) {\u001b[39m\n\u001b[31m    if (d.id !== dispatchId) continue;\u001b[39m\n\u001b[31m    for (const c of d.consignees) {\u001b[39m\n\u001b[31m      if (c.docket === docket) {\u001b[39m\n\u001b[31m        c.podUploaded = true;\u001b[39m\n\u001b[31m        c.status = 'delivered';\u001b[39m\n\u001b[31m      }\u001b[39m\n\u001b[31m    }\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m  saveData();·\u001b[39m\n\u001b[31m  // Push to Sheets (Apps Script saves images to Drive, stores URL in sheet)\u001b[39m\n\u001b[31m  if (window.appConfig.webhook_url) {\u001b[39m\n\u001b[31m    try {\u001b[39m\n\u001b[31m      await fetch(window.appConfig.webhook_url, {\u001b[39m\n\u001b[31m        method: 'POST', mode: 'no-cors',\u001b[39m\n\u001b[31m        headers: { 'Content-Type': 'application/json' },\u001b[39m\n\u001b[31m        body: JSON.stringify(payload)\u001b[39m\n\u001b[31m      });\u001b[39m\n\u001b[31m    } catch(e) {}\u001b[39m\n\u001b[31m  }·\u001b[39m\n\u001b[31m  closeOverlay('podOverlay');\u001b[39m\n\u001b[31m  renderDashboard();\u001b[39m\n\u001b[31m  showToast('POD uploaded ✓ — marked as delivered', 'ok');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// ─── WHATSAPP DISPATCH TABLE SHARE ──────────────────────────────────────\u001b[39m\n\u001b[31mfunction shareDispatchToWhatsApp(dispatchId) {\u001b[39m\n\u001b[31m  const d = dispatches.find(x => x.id === dispatchId);\u001b[39m\n\u001b[31m  if (!d) return;\u001b[39m\n\u001b[31m  const lines = [\u001b[39m\n\u001b[31m    `📦 *Dispatch Update — ${d.client} ${d.jobref ? '› '+d.jobref : ''}*`,\u001b[39m\n\u001b[31m    `🚚 Courier: ${d.courier} | Date: ${d.dispatchDate || d.createdAt?.slice(0,10)}`,\u001b[39m\n\u001b[31m    ``,\u001b[39m\n\u001b[31m    ...d.consignees.filter(c => c.docket).map((c,i) =>\u001b[39m\n\u001b[31m      `${i+1}. *${c.name}* — ${c.city}\\\\n   Docket: ${c.docket} | Status: ${c.status||'booked'}`\u001b[39m\n\u001b[31m    ),\u001b[39m\n\u001b[31m    ``,\u001b[39m\n\u001b[31m    `_360 Degree Logical Marketing_`\u001b[39m\n\u001b[31m  ];\u001b[39m\n\u001b[31m  const msg = encodeURIComponent(lines.join('\\\\n'));\u001b[39m\n\u001b[31m  window.open('https://wa.me/?text=' + msg, '_blank');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// Trigger n8n webhook for automated WhatsApp/email share\u001b[39m\n\u001b[31masync function triggerN8nShare(dispatchId, channel) {\u001b[39m\n\u001b[31m  const d = dispatches.find(x => x.id === dispatchId);\u001b[39m\n\u001b[31m  if (!d) return;\u001b[39m\n\u001b[31m  const n8nUrl = window.appConfig.n8n_dispatch_webhook;\u001b[39m\n\u001b[31m  if (!n8nUrl) { showToast('n8n webhook not configured in Config sheet', 'err'); return; }\u001b[39m\n\u001b[31m  try {\u001b[39m\n\u001b[31m    await fetch(n8nUrl, {\u001b[39m\n\u001b[31m      method: 'POST',\u001b[39m\n\u001b[31m      headers: { 'Content-Type': 'application/json' },\u001b[39m\n\u001b[31m      body: JSON.stringify({\u001b[39m\n\u001b[31m        channel, // 'whatsapp' or 'email'\u001b[39m\n\u001b[31m        dispatch: d,\u001b[39m\n\u001b[31m        timestamp: new Date().toISOString()\u001b[39m\n\u001b[31m      })\u001b[39m\n\u001b[31m    });\u001b[39m\n\u001b[31m    showToast(`Sent to n8n for ${channel} delivery ✓`, 'ok');\u001b[39m\n\u001b[31m  } catch(e) {\u001b[39m\n\u001b[31m    showToast('n8n webhook failed', 'err');\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction previewPOD(input, type) {\u001b[39m\n\u001b[31m  const f = input.files[0];\u001b[39m\n\u001b[31m  if (!f) return;\u001b[39m\n\u001b[31m  const r = new FileReader();\u001b[39m\n\u001b[31m  r.onload = e => {\u001b[39m\n\u001b[31m    const prev = document.getElementById('podPreview');\u001b[39m\n\u001b[31m    const existing = document.getElementById('pod_prev_' + type);\u001b[39m\n\u001b[31m    if (existing) existing.remove();\u001b[39m\n\u001b[31m    const img = document.createElement('img');\u001b[39m\n\u001b[31m    img.id = 'pod_prev_' + type;\u001b[39m\n\u001b[31m    img.src = e.target.result;\u001b[39m\n\u001b[31m    img.style.cssText = 'width:80px;height:80px;object-fit:cover;border-radius:6px;border:1px solid var(--bdr);';\u001b[39m\n\u001b[31m    prev.appendChild(img);\u001b[39m\n\u001b[31m  };\u001b[39m\n\u001b[31m  r.readAsDataURL(f);\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// ─── MANUAL TRACKING ─────────────────────────────────────────────────────\u001b[39m\n\u001b[31mfunction showManualTracking(docket, dispatchId, storeName, phone, city) {\u001b[39m\n\u001b[31m  const ov = document.getElementById('manualTrackOverlay');\u001b[39m\n\u001b[31m  ov.dataset.docket = docket;\u001b[39m\n\u001b[31m  ov.dataset.dispatchId = dispatchId;\u001b[39m\n\u001b[31m  ov.dataset.storeName = storeName;\u001b[39m\n\u001b[31m  ov.dataset.city = city;·\u001b[39m\n\u001b[31m  document.getElementById('mtDocketDisplay').textContent =\u001b[39m\n\u001b[31m    docket + '  ·  ' + storeName + '  ·  ' + city;\u001b[39m\n\u001b[31m  document.getElementById('mtContactUsed').value = phone || '';\u001b[39m\n\u001b[31m  document.getElementById('mtContactPerson').value = '';\u001b[39m\n\u001b[31m  document.getElementById('mtRemarks').value = '';\u001b[39m\n\u001b[31m  document.getElementById('mtStatus').value = '';\u001b[39m\n\u001b[31m  document.getElementById('mtCalledBy').value = '';·\u001b[39m\n\u001b[31m  // Set current datetime\u001b[39m\n\u001b[31m  const now = new Date();\u001b[39m\n\u001b[31m  const pad = n => String(n).padStart(2,'0');\u001b[39m\n\u001b[31m  document.getElementById('mtCallTime').value =\u001b[39m\n\u001b[31m    `${now.getFullYear()}-${pad(now.getMonth()+1)}-${pad(now.getDate())}T${pad(now.getHours())}:${pad(now.getMinutes())}`;·\u001b[39m\n\u001b[31m  openOverlay('manualTrackOverlay');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31masync function submitManualTracking() {\u001b[39m\n\u001b[31m  const ov = document.getElementById('manualTrackOverlay');\u001b[39m\n\u001b[31m  const docket     = ov.dataset.docket;\u001b[39m\n\u001b[31m  const dispatchId = ov.dataset.dispatchId;\u001b[39m\n\u001b[31m  const status     = document.getElementById('mtStatus').value;\u001b[39m\n\u001b[31m  const calledBy   = document.getElementById('mtCalledBy').value.trim();·\u001b[39m\n\u001b[31m  if (!status)    { showToast('Select a status', 'err'); return; }\u001b[39m\n\u001b[31m  if (!calledBy)  { showToast('Enter your name', 'err'); return; }·\u001b[39m\n\u001b[31m  const payload = {\u001b[39m\n\u001b[31m    type:          'manual_tracking',\u001b[39m\n\u001b[31m    docket,        dispatchId,\u001b[39m\n\u001b[31m    storeName:     ov.dataset.storeName,\u001b[39m\n\u001b[31m    city:          ov.dataset.city,\u001b[39m\n\u001b[31m    manualStatus:  status,\u001b[39m\n\u001b[31m    calledBy,\u001b[39m\n\u001b[31m    contactUsed:   document.getElementById('mtContactUsed').value.trim(),\u001b[39m\n\u001b[31m    contactPerson: document.getElementById('mtContactPerson').value.trim(),\u001b[39m\n\u001b[31m    remarks:       document.getElementById('mtRemarks').value.trim(),\u001b[39m\n\u001b[31m    callTime:      document.getElementById('mtCallTime').value || new Date().toISOString(),\u001b[39m\n\u001b[31m    timestamp:     new Date().toISOString()\u001b[39m\n\u001b[31m  };·\u001b[39m\n\u001b[31m  // Update local dispatch record\u001b[39m\n\u001b[31m  for (const d of dispatches) {\u001b[39m\n\u001b[31m    if (d.id !== dispatchId) continue;\u001b[39m\n\u001b[31m    for (const c of d.consignees) {\u001b[39m\n\u001b[31m      if (c.docket === docket) {\u001b[39m\n\u001b[31m        c.manualStatus    = status;\u001b[39m\n\u001b[31m        c.manualCalledBy  = calledBy;\u001b[39m\n\u001b[31m        c.manualTime      = payload.callTime;\u001b[39m\n\u001b[31m        c.manualRemarks   = payload.remarks;\u001b[39m\n\u001b[31m        // If confirmed delivered — flag so dashboard shows manual badge\u001b[39m\n\u001b[31m        // (not permanently locked — use resumeAutoTracking() to re-enable)\u001b[39m\n\u001b[31m        if (status === 'delivered') {\u001b[39m\n\u001b[31m          c.status = 'delivered';\u001b[39m\n\u001b[31m          c.manualDelivered = true;\u001b[39m\n\u001b[31m        } else {\u001b[39m\n\u001b[31m          // Non-delivered manual update: clear any previous lock\u001b[39m\n\u001b[31m          c.manualDelivered = false;\u001b[39m\n\u001b[31m        }\u001b[39m\n\u001b[31m      }\u001b[39m\n\u001b[31m    }\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m  saveData();\u001b[39m\n\u001b[31m  renderDashboard();\u001b[39m\n\u001b[31m  // Sync updated consignee to Postgres\u001b[39m\n\u001b[31m  const _d = dispatches.find(x => x.id === dispatchId);\u001b[39m\n\u001b[31m  if (_d) {\u001b[39m\n\u001b[31m    const _idx = _d.consignees.findIndex(c => c.docket === docket);\u001b[39m\n\u001b[31m    if (_idx >= 0) syncConsigneeToDB(_d.consignees[_idx], dispatchId, _idx);\u001b[39m\n\u001b[31m  }·\u001b[39m\n\u001b[31m  // Log to Sheets\u001b[39m\n\u001b[31m  if (window.appConfig.webhook_url) {\u001b[39m\n\u001b[31m    try {\u001b[39m\n\u001b[31m      await fetch(window.appConfig.webhook_url, {\u001b[39m\n\u001b[31m        method: 'POST', mode: 'no-cors',\u001b[39m\n\u001b[31m        headers: { 'Content-Type': 'application/json' },\u001b[39m\n\u001b[31m        body: JSON.stringify(payload)\u001b[39m\n\u001b[31m      });\u001b[39m\n\u001b[31m    } catch(e) {}\u001b[39m\n\u001b[31m  }·\u001b[39m\n\u001b[31m  closeOverlay('manualTrackOverlay');\u001b[39m\n\u001b[31m  showToast('Manual tracking logged ✓', 'ok');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// ─── TILE SELECTION + PARTIAL REPORT SHARE ───────────────────────────────\u001b[39m\n\u001b[31mlet _selectedDockets = new Set();·\u001b[39m\n\u001b[31mfunction toggleTileSelect(docket) {\u001b[39m\n\u001b[31m  if (_selectedDockets.has(docket)) _selectedDockets.delete(docket);\u001b[39m\n\u001b[31m  else _selectedDockets.add(docket);\u001b[39m\n\u001b[31m  // Update checkbox visual\u001b[39m\n\u001b[31m  const chk = document.getElementById('sel_' + docket.replace(/[^a-z0-9]/gi,'_'));\u001b[39m\n\u001b[31m  if (chk) chk.checked = _selectedDockets.has(docket);\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction shareSelectedReport() {\u001b[39m\n\u001b[31m  if (_selectedDockets.size === 0) {\u001b[39m\n\u001b[31m    showToast('Select at least one consignee tile first', 'err');\u001b[39m\n\u001b[31m    return;\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m  const activeDis = dispatches.filter(d => d.status !== 'draft');\u001b[39m\n\u001b[31m  const allItems  = activeDis.flatMap(d =>\u001b[39m\n\u001b[31m    (d.consignees||[]).map(c => Object.assign({}, c, {courier:d.courier, client:d.client, jobref:d.jobref, dispatchId:d.id}))\u001b[39m\n\u001b[31m  );\u001b[39m\n\u001b[31m  const selected = allItems.filter(c => _selectedDockets.has(c.docket));\u001b[39m\n\u001b[31m  if (!selected.length) { showToast('Selected dockets not found', 'err'); return; }·\u001b[39m\n\u001b[31m  const NL = '\\\\n';\u001b[39m\n\u001b[31m  const statusEmoji = {\u001b[39m\n\u001b[31m    delivered:'\\\\u2705', transit:'\\\\uD83D\\\\uDE9A', 'in-transit':'\\\\uD83D\\\\uDE9A',\u001b[39m\n\u001b[31m    booked:'\\\\uD83D\\\\uDCEC', pending:'\\\\u23F3', 'not received':'\\\\u274C', forwarded:'\\\\u27A1'\u001b[39m\n\u001b[31m  };·\u001b[39m\n\u001b[31m  const header = '\\\\uD83D\\\\uDCE6 *Dispatch Status Report*' + NL +\u001b[39m\n\u001b[31m    (selected[0].client || '') + ' ' + (selected[0].jobref ? '> ' + selected[0].jobref : '') + NL +\u001b[39m\n\u001b[31m    new Date().toLocaleDateString('en-IN', {day:'2-digit',month:'short',year:'numeric'}) + NL + NL;·\u001b[39m\n\u001b[31m  const rows = selected.map(function(c, i) {\u001b[39m\n\u001b[31m    const finalStatus = c.manualStatus === 'delivered' ? 'delivered' : (c.status || 'booked');\u001b[39m\n\u001b[31m    const emoji  = statusEmoji[finalStatus] || '\\\\uD83D\\\\uDCE6';\u001b[39m\n\u001b[31m    const source = c.manualStatus ? '(manual)' : '(auto)';\u001b[39m\n\u001b[31m    const remark = c.manualRemarks ? (NL + '   Note: ' + c.manualRemarks) : '';\u001b[39m\n\u001b[31m    return (i+1) + '. *' + c.name + '* - ' + c.city + NL +\u001b[39m\n\u001b[31m      '   ' + emoji + ' ' + finalStatus + ' ' + source + NL +\u001b[39m\n\u001b[31m      '   Docket: ' + c.docket + remark;\u001b[39m\n\u001b[31m  }).join(NL + NL);·\u001b[39m\n\u001b[31m  const footer = NL + NL + '_360 Degree Logical Marketing_';\u001b[39m\n\u001b[31m  window.open('https://wa.me/?text=' + encodeURIComponent(header + rows + footer), '_blank');\u001b[39m\n\u001b[31m  showToast('Report for ' + selected.length + ' consignees ready', 'ok');\u001b[39m\n\u001b[31m}··\u001b[39m\n\u001b[31m// ── resumeAutoTracking: undo a manual-delivered lock ──────────────────────\u001b[39m\n\u001b[31mfunction resumeAutoTracking(docket, dispatchId) {\u001b[39m\n\u001b[31m  for (const d of dispatches) {\u001b[39m\n\u001b[31m    if (d.id !== dispatchId) continue;\u001b[39m\n\u001b[31m    for (const c of d.consignees) {\u001b[39m\n\u001b[31m      if (c.docket === docket) {\u001b[39m\n\u001b[31m        c.manualDelivered = false;\u001b[39m\n\u001b[31m        c.status          = 'transit';\u001b[39m\n\u001b[31m        c.manualStatus    = '';\u001b[39m\n\u001b[31m      }\u001b[39m\n\u001b[31m    }\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m  saveData();\u001b[39m\n\u001b[31m  renderDashboard();\u001b[39m\n\u001b[31m  showToast(`Auto-tracking resumed for ${docket}`, 'ok');\u001b[39m\n\u001b[31m  const _rd = dispatches.find(x => x.id === dispatchId);\u001b[39m\n\u001b[31m  if (_rd) {\u001b[39m\n\u001b[31m    const _ri = _rd.consignees.findIndex(c => c.docket === docket);\u001b[39m\n\u001b[31m    if (_ri >= 0) syncConsigneeToDB(_rd.consignees[_ri], dispatchId, _ri);\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// ── refreshAllTracking (consolidated, try/finally for safety) ─────────────\u001b[39m\n\u001b[31masync function refreshAllTracking() {\u001b[39m\n\u001b[31m  const activeDis  = dispatches.filter(d => d.status !== 'draft');\u001b[39m\n\u001b[31m  const allCons    = activeDis.flatMap(d =>\u001b[39m\n\u001b[31m    (d.consignees || []).map(c => ({ ...c, dispatchId: d.id, courier: d.courier }))\u001b[39m\n\u001b[31m  );\u001b[39m\n\u001b[31m  const withDockets = allCons.filter(c =>\u001b[39m\n\u001b[31m    c.docket && c.status !== 'delivered' && !c.manualDelivered\u001b[39m\n\u001b[31m  );\u001b[39m\n\u001b[31m  if (!withDockets.length) { showToast('No active dockets to refresh', 'info'); return; }·\u001b[39m\n\u001b[31m  showToast(`⏳ Tracking ${withDockets.length} dockets...`, 'info');\u001b[39m\n\u001b[31m  withDockets.forEach(c => {\u001b[39m\n\u001b[31m    const id    = c.docket.replace(/[^a-z0-9]/gi, '_');\u001b[39m\n\u001b[31m    const evtEl = document.getElementById('evt_'  + id);\u001b[39m\n\u001b[31m    const spnEl = document.getElementById('spin_' + id);\u001b[39m\n\u001b[31m    if (evtEl) evtEl.style.display = 'none';\u001b[39m\n\u001b[31m    if (spnEl) spnEl.style.display = 'inline';\u001b[39m\n\u001b[31m  });·\u001b[39m\n\u001b[31m  let done = 0;\u001b[39m\n\u001b[31m  try {\u001b[39m\n\u001b[31m    for (const c of withDockets) {\u001b[39m\n\u001b[31m      const result = await fetchTracking(c.docket, c.courier || '');\u001b[39m\n\u001b[31m      const id    = c.docket.replace(/[^a-z0-9]/gi, '_');·\u001b[39m\n\u001b[31m      // Batch-write result into state (never touch DOM here)\u001b[39m\n\u001b[31m      if (result.status && result.status !== 'failed') {\u001b[39m\n\u001b[31m        for (const d of dispatches) {\u001b[39m\n\u001b[31m          if (d.id !== c.dispatchId) continue;\u001b[39m\n\u001b[31m          for (const con of d.consignees) {\u001b[39m\n\u001b[31m            if (con.docket === c.docket && !con.manualDelivered) {\u001b[39m\n\u001b[31m              con.status           = result.status;\u001b[39m\n\u001b[31m              con.lastEvent        = result.lastEvent || con.lastEvent;\u001b[39m\n\u001b[31m              con.lastScanLocation = result.lastScanLocation || '';\u001b[39m\n\u001b[31m              con.lastTrackedAt    = new Date().toISOString();\u001b[39m\n\u001b[31m            }\u001b[39m\n\u001b[31m          }\u001b[39m\n\u001b[31m        }\u001b[39m\n\u001b[31m      }·\u001b[39m\n\u001b[31m      // Update tile spinner/event text in DOM (cosmetic only)\u001b[39m\n\u001b[31m      const spnEl = document.getElementById('spin_' + id);\u001b[39m\n\u001b[31m      const evtEl = document.getElementById('evt_'  + id);\u001b[39m\n\u001b[31m      if (spnEl) spnEl.style.display = 'none';\u001b[39m\n\u001b[31m      if (evtEl) { evtEl.style.display = 'inline'; evtEl.textContent = result.lastEvent || 'Checked'; }·\u001b[39m\n\u001b[31m      if (result.status && result.status !== 'failed' && window.appConfig.webhook_url) {\u001b[39m\n\u001b[31m        logTrackingUpdate(c.docket, result.status, result.lastEvent || '', result.lastScanLocation || '');\u001b[39m\n\u001b[31m      }\u001b[39m\n\u001b[31m      done++;\u001b[39m\n\u001b[31m    }\u001b[39m\n\u001b[31m    showToast(`✓ ${done} dockets refreshed`, 'ok');\u001b[39m\n\u001b[31m  } finally {\u001b[39m\n\u001b[31m    // Always persist and re-render — even if loop throws mid-way\u001b[39m\n\u001b[31m    saveData();\u001b[39m\n\u001b[31m    renderDashboard();\u001b[39m\n\u001b[31m    // Push updated consignee states to Postgres (fire-and-forget)\u001b[39m\n\u001b[31m    dispatches.filter(d => d.status !== 'draft').forEach(d => syncDispatchToDB(d));\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m</script>\u001b[39m\n\u001b[31m</body>\u001b[39m\n\u001b[31m</html>\"\u001b[39m\n\n  51 |\n  52 |   test('hub auth gate present', () => {\n> 53 |     expect(html).toContain(\"window.location.replace('/hub/')\");\n     |                  ^\n  54 |     expect(html).toContain('_hubUser');\n  55 |   });\n  56 | });\n    at /var/www/360lm/tests/dispatch.spec.js:53:18"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 0,
                      "startTime": "2026-07-12T12:57:25.937Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/dispatch-Dispatch-—-HTML-Structure-hub-auth-gate-present-android-chrome/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/dispatch.spec.js",
                        "column": 18,
                        "line": 53
                      }
                    },
                    {
                      "workerIndex": 1,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 35,
                      "error": {
                        "message": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoContain\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m) // indexOf\u001b[22m\n\nExpected substring: \u001b[32m\"window.location.replace('/hub/')\"\u001b[39m\nReceived string:    \u001b[31m\"<!DOCTYPE html>\u001b[39m\n\u001b[31m<html lang=\\\"en\\\">\u001b[39m\n\u001b[31m<head>\u001b[39m\n\u001b[31m<meta charset=\\\"UTF-8\\\">\u001b[39m\n\u001b[31m<meta name=\\\"viewport\\\" content=\\\"width=device-width,initial-scale=1,maximum-scale=1,user-scalable=no\\\">\u001b[39m\n\u001b[31m<meta name=\\\"theme-color\\\" content=\\\"#080c12\\\">\u001b[39m\n\u001b[31m<meta name=\\\"apple-mobile-web-app-capable\\\" content=\\\"yes\\\">\u001b[39m\n\u001b[31m<meta name=\\\"mobile-web-app-capable\\\" content=\\\"yes\\\">\u001b[39m\n\u001b[31m<title>360LM Dispatch</title>\u001b[39m\n\u001b[31m<link rel=\\\"manifest\\\" href=\\\"manifest.json\\\">\u001b[39m\n\u001b[31m<link href=\\\"https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@300;400;500;600;700&family=Fira+Code:wght@400;500&family=Barlow+Condensed:wght@400;600;700;800&display=swap\\\" rel=\\\"stylesheet\\\">\u001b[39m\n\u001b[31m<style>\u001b[39m\n\u001b[31m/* ════ RESET & VARS ════ */\u001b[39m\n\u001b[31m*{margin:0;padding:0;box-sizing:border-box;-webkit-tap-highlight-color:transparent;}\u001b[39m\n\u001b[31m:root{\u001b[39m\n\u001b[31m  --bg:#080c12; --bg2:#0d1420; --bg3:#111a28;\u001b[39m\n\u001b[31m  --sur:#141f30; --sur2:#1a2840; --sur3:#1f3050;\u001b[39m\n\u001b[31m  --bdr:#1e2e45; --bdr2:#253650;\u001b[39m\n\u001b[31m  --t1:#f0f4f8; --t2:#8ba4bc; --t3:#3d5470; --t4:#243348;\u001b[39m\n\u001b[31m  --acc:#f97316; --acc2:#fb923c; --acc-glow:rgba(249,115,22,0.25);\u001b[39m\n\u001b[31m  --grn:#22c55e; --grn-dim:rgba(34,197,94,0.15);\u001b[39m\n\u001b[31m  --sky:#38bdf8; --sky-dim:rgba(56,189,248,0.15);\u001b[39m\n\u001b[31m  --red:#ef4444; --red-dim:rgba(239,68,68,0.15);\u001b[39m\n\u001b[31m  --ylw:#eab308; --ylw-dim:rgba(234,179,8,0.15);\u001b[39m\n\u001b[31m  --fnt-head:'Barlow Condensed',sans-serif;\u001b[39m\n\u001b[31m  --fnt-body:'Space Grotesk',sans-serif;\u001b[39m\n\u001b[31m  --fnt-mono:'Fira Code',monospace;\u001b[39m\n\u001b[31m  --r:10px; --r-sm:6px; --r-lg:16px;\u001b[39m\n\u001b[31m  --sh:0 4px 24px rgba(0,0,0,0.4);\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31mhtml,body{height:100%;background:var(--bg);color:var(--t1);font-family:var(--fnt-body);overflow:hidden;}·\u001b[39m\n\u001b[31m/* ════ SPLASH ════ */\u001b[39m\n\u001b[31m#splash{\u001b[39m\n\u001b[31m  position:fixed;inset:0;background:var(--bg);z-index:999;\u001b[39m\n\u001b[31m  display:flex;flex-direction:column;align-items:center;justify-content:center;\u001b[39m\n\u001b[31m  transition:opacity .4s .1s;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.splash-logo{font-family:var(--fnt-head);font-size:3rem;font-weight:800;letter-spacing:2px;line-height:1;}\u001b[39m\n\u001b[31m.splash-logo em{color:var(--acc);font-style:normal;}\u001b[39m\n\u001b[31m.splash-sub{color:var(--t3);font-family:var(--fnt-mono);font-size:.7rem;letter-spacing:4px;margin-top:8px;}\u001b[39m\n\u001b[31m.splash-track{width:200px;height:2px;background:var(--bdr);border-radius:2px;margin-top:40px;overflow:hidden;}\u001b[39m\n\u001b[31m.splash-track-fill{height:100%;width:0;background:linear-gradient(90deg,var(--acc),var(--acc2));animation:splashLoad 1.3s cubic-bezier(.4,0,.2,1) forwards;}\u001b[39m\n\u001b[31m@keyframes splashLoad{to{width:100%;}}·\u001b[39m\n\u001b[31m/* ════ LAYOUT ════ */\u001b[39m\n\u001b[31m#app{height:100%;display:flex;flex-direction:column;}\u001b[39m\n\u001b[31m.hdr{\u001b[39m\n\u001b[31m  background:var(--bg2);border-bottom:1px solid var(--bdr);\u001b[39m\n\u001b[31m  padding:10px 14px;display:flex;align-items:center;gap:10px;flex-shrink:0;\u001b[39m\n\u001b[31m  position:relative;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.hdr-brand{font-family:var(--fnt-head);font-size:1.3rem;font-weight:800;letter-spacing:1px;flex:1;}\u001b[39m\n\u001b[31m.hdr-brand em{color:var(--acc);font-style:normal;}\u001b[39m\n\u001b[31m.hdr-dot{width:7px;height:7px;border-radius:50%;background:var(--grn);transition:background .3s;animation:blink 2s infinite;}\u001b[39m\n\u001b[31m@keyframes blink{0%,100%{opacity:1;}50%{opacity:.4;}}\u001b[39m\n\u001b[31m.hdr-btn{\u001b[39m\n\u001b[31m  background:none;border:none;color:var(--t2);cursor:pointer;\u001b[39m\n\u001b[31m  padding:6px 8px;border-radius:var(--r-sm);transition:all .2s;\u001b[39m\n\u001b[31m  font-family:var(--fnt-head);font-size:.85rem;font-weight:700;letter-spacing:.5px;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.hdr-btn:hover{background:var(--bdr);color:var(--t1);}\u001b[39m\n\u001b[31m.hdr-btn.primary{background:var(--acc);color:#fff;padding:6px 14px;}\u001b[39m\n\u001b[31m.hdr-btn.primary:hover{background:var(--acc2);}·\u001b[39m\n\u001b[31m/* ════ TAB BAR ════ */\u001b[39m\n\u001b[31m.tabs{\u001b[39m\n\u001b[31m  display:flex;background:var(--bg2);border-bottom:1px solid var(--bdr);\u001b[39m\n\u001b[31m  overflow-x:auto;flex-shrink:0;padding:0 6px;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.tabs::-webkit-scrollbar{display:none;}\u001b[39m\n\u001b[31m.tab{\u001b[39m\n\u001b[31m  background:none;border:none;color:var(--t3);font-family:var(--fnt-head);\u001b[39m\n\u001b[31m  font-size:.85rem;font-weight:700;letter-spacing:1px;padding:10px 14px;\u001b[39m\n\u001b[31m  cursor:pointer;border-bottom:2px solid transparent;white-space:nowrap;\u001b[39m\n\u001b[31m  transition:all .2s;flex-shrink:0;display:flex;align-items:center;gap:6px;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.tab.active{color:var(--acc);border-bottom-color:var(--acc);}\u001b[39m\n\u001b[31m.tab:hover:not(.active){color:var(--t2);}\u001b[39m\n\u001b[31m.tab-badge{\u001b[39m\n\u001b[31m  background:var(--acc);color:#fff;font-size:.6rem;font-weight:700;\u001b[39m\n\u001b[31m  padding:1px 5px;border-radius:10px;font-family:var(--fnt-mono);\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m/* ════ CONTENT AREAS ════ */\u001b[39m\n\u001b[31m.pane{flex:1;overflow-y:auto;padding:14px;display:none;}\u001b[39m\n\u001b[31m.pane.active{display:block;}\u001b[39m\n\u001b[31m.pane::-webkit-scrollbar{width:3px;}\u001b[39m\n\u001b[31m.pane::-webkit-scrollbar-thumb{background:var(--bdr2);}·\u001b[39m\n\u001b[31m/* ════ CARDS ════ */\u001b[39m\n\u001b[31m.card{\u001b[39m\n\u001b[31m  background:var(--sur);border:1px solid var(--bdr);border-radius:var(--r);\u001b[39m\n\u001b[31m  padding:14px;margin-bottom:12px;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.card.accent-left{border-left:3px solid var(--acc);}\u001b[39m\n\u001b[31m.card-title{\u001b[39m\n\u001b[31m  font-family:var(--fnt-head);font-size:.72rem;font-weight:700;\u001b[39m\n\u001b[31m  color:var(--t3);text-transform:uppercase;letter-spacing:2.5px;\u001b[39m\n\u001b[31m  margin-bottom:12px;display:flex;align-items:center;gap:8px;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.card-title .ct-dot{width:5px;height:5px;border-radius:50%;background:var(--acc);flex-shrink:0;}·\u001b[39m\n\u001b[31m/* ════ FORM ════ */\u001b[39m\n\u001b[31m.f{margin-bottom:12px;}\u001b[39m\n\u001b[31m.f label{\u001b[39m\n\u001b[31m  display:block;font-size:.68rem;color:var(--t3);margin-bottom:5px;\u001b[39m\n\u001b[31m  font-weight:600;text-transform:uppercase;letter-spacing:1.5px;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.f label .req{color:var(--acc);margin-left:2px;}\u001b[39m\n\u001b[31m.f input,.f select,.f textarea{\u001b[39m\n\u001b[31m  width:100%;background:var(--sur2);border:1px solid var(--bdr);\u001b[39m\n\u001b[31m  border-radius:var(--r-sm);color:var(--t1);font-family:var(--fnt-body);\u001b[39m\n\u001b[31m  font-size:.88rem;padding:9px 11px;outline:none;transition:border-color .2s;\u001b[39m\n\u001b[31m  -webkit-appearance:none;appearance:none;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.f input:focus,.f select:focus,.f textarea:focus{border-color:var(--acc);}\u001b[39m\n\u001b[31m.f input::placeholder{color:var(--t4);}\u001b[39m\n\u001b[31m.f textarea{resize:vertical;min-height:65px;}\u001b[39m\n\u001b[31m.f select{\u001b[39m\n\u001b[31m  background-image:url(\\\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='7'%3E%3Cpath d='M1 1l4 4 4-4' stroke='%233d5470' stroke-width='1.5' fill='none' stroke-linecap='round'/%3E%3C/svg%3E\\\");\u001b[39m\n\u001b[31m  background-repeat:no-repeat;background-position:right 10px center;padding-right:28px;cursor:pointer;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.row2{display:grid;grid-template-columns:1fr 1fr;gap:10px;}\u001b[39m\n\u001b[31m.row3{display:grid;grid-template-columns:1fr 1fr 1fr;gap:8px;}·\u001b[39m\n\u001b[31m/* ════ BUTTONS ════ */\u001b[39m\n\u001b[31m.btn{\u001b[39m\n\u001b[31m  width:100%;padding:12px;border:none;border-radius:var(--r);\u001b[39m\n\u001b[31m  font-family:var(--fnt-head);font-size:.9rem;font-weight:700;letter-spacing:.5px;\u001b[39m\n\u001b[31m  cursor:pointer;transition:all .2s;display:flex;align-items:center;justify-content:center;gap:8px;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.btn-orange{background:var(--acc);color:#fff;}\u001b[39m\n\u001b[31m.btn-orange:hover{background:var(--acc2);}\u001b[39m\n\u001b[31m.btn-ghost{background:var(--sur2);border:1px solid var(--bdr);color:var(--t1);}\u001b[39m\n\u001b[31m.btn-ghost:hover{border-color:var(--acc);color:var(--acc);}\u001b[39m\n\u001b[31m.btn-grn{background:var(--grn);color:#000;}\u001b[39m\n\u001b[31m.btn-grn:hover{opacity:.85;}\u001b[39m\n\u001b[31m.btn:active{transform:scale(.98);}\u001b[39m\n\u001b[31m.btn-row{display:grid;grid-template-columns:1fr 1fr;gap:10px;margin-top:6px;}\u001b[39m\n\u001b[31m.btn-sm{\u001b[39m\n\u001b[31m  background:none;border:1px solid var(--bdr);color:var(--t2);\u001b[39m\n\u001b[31m  font-size:.7rem;padding:5px 10px;border-radius:var(--r-sm);cursor:pointer;\u001b[39m\n\u001b[31m  font-family:var(--fnt-body);transition:all .2s;white-space:nowrap;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.btn-sm:hover{border-color:var(--acc);color:var(--acc);}\u001b[39m\n\u001b[31m.btn-sm.primary{background:var(--acc);border-color:var(--acc);color:#fff;}·\u001b[39m\n\u001b[31m/* ════ OCR UPLOAD ZONE ════ */\u001b[39m\n\u001b[31m.upload-zone{\u001b[39m\n\u001b[31m  border:2px dashed var(--bdr2);border-radius:var(--r);\u001b[39m\n\u001b[31m  padding:20px;text-align:center;cursor:pointer;transition:all .25s;\u001b[39m\n\u001b[31m  background:var(--sur2);position:relative;overflow:hidden;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.upload-zone:hover,.upload-zone.drag{border-color:var(--acc);background:rgba(249,115,22,.05);}\u001b[39m\n\u001b[31m.upload-zone .uz-icon{font-size:2rem;margin-bottom:8px;}\u001b[39m\n\u001b[31m.upload-zone .uz-title{font-family:var(--fnt-head);font-size:1rem;font-weight:700;color:var(--t1);}\u001b[39m\n\u001b[31m.upload-zone .uz-sub{font-size:.72rem;color:var(--t3);margin-top:4px;font-family:var(--fnt-mono);}\u001b[39m\n\u001b[31m.upload-zone input{display:none;}\u001b[39m\n\u001b[31m.upload-zone.share-target{border-color:var(--grn);}·\u001b[39m\n\u001b[31m/* ════ OCR RESULTS ════ */\u001b[39m\n\u001b[31m.ocr-results{display:none;}\u001b[39m\n\u001b[31m.ocr-results.visible{display:block;}\u001b[39m\n\u001b[31m.ocr-slip-list{display:flex;flex-direction:column;gap:8px;margin-top:10px;}\u001b[39m\n\u001b[31m.ocr-slip{\u001b[39m\n\u001b[31m  background:var(--sur2);border:1px solid var(--bdr);border-radius:var(--r-sm);\u001b[39m\n\u001b[31m  padding:10px 12px;display:flex;gap:10px;align-items:flex-start;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.ocr-slip img{width:60px;height:60px;object-fit:cover;border-radius:6px;flex-shrink:0;border:1px solid var(--bdr2);}\u001b[39m\n\u001b[31m.ocr-slip-body{flex:1;min-width:0;}\u001b[39m\n\u001b[31m.ocr-slip-name{font-size:.7rem;color:var(--t3);font-family:var(--fnt-mono);margin-bottom:4px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}\u001b[39m\n\u001b[31m.ocr-slip-docket{\u001b[39m\n\u001b[31m  font-family:var(--fnt-mono);font-size:.9rem;font-weight:500;color:var(--acc);\u001b[39m\n\u001b[31m  background:var(--bg);border:1px solid var(--bdr);border-radius:4px;\u001b[39m\n\u001b[31m  padding:4px 8px;width:100%;outline:none;transition:border-color .2s;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.ocr-slip-docket:focus{border-color:var(--acc);}\u001b[39m\n\u001b[31m.ocr-slip-status{font-size:.65rem;margin-top:4px;display:flex;align-items:center;gap:6px;}\u001b[39m\n\u001b[31m.ocr-confidence{padding:2px 6px;border-radius:10px;font-family:var(--fnt-mono);}\u001b[39m\n\u001b[31m.conf-high{background:var(--grn-dim);color:var(--grn);}\u001b[39m\n\u001b[31m.conf-med{background:var(--ylw-dim);color:var(--ylw);}\u001b[39m\n\u001b[31m.conf-low{background:var(--red-dim);color:var(--red);}\u001b[39m\n\u001b[31m.ocr-ai-btn{\u001b[39m\n\u001b[31m  background:rgba(56,189,248,.15);border:1px solid rgba(56,189,248,.3);\u001b[39m\n\u001b[31m  color:var(--sky);font-size:.65rem;padding:3px 8px;border-radius:10px;\u001b[39m\n\u001b[31m  cursor:pointer;font-family:var(--fnt-mono);transition:all .2s;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.ocr-ai-btn:hover{background:rgba(56,189,248,.3);}\u001b[39m\n\u001b[31m.ocr-working{\u001b[39m\n\u001b[31m  background:var(--ylw-dim);border:1px solid rgba(234,179,8,.3);\u001b[39m\n\u001b[31m  border-radius:var(--r-sm);padding:8px 12px;margin-top:8px;\u001b[39m\n\u001b[31m  font-size:.75rem;color:var(--ylw);font-family:var(--fnt-mono);\u001b[39m\n\u001b[31m  display:none;align-items:center;gap:8px;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.ocr-working.visible{display:flex;}\u001b[39m\n\u001b[31m.spin{animation:spin .8s linear infinite;display:inline-block;}\u001b[39m\n\u001b[31m@keyframes spin{to{transform:rotate(360deg);}}·\u001b[39m\n\u001b[31m/* ════ STORE LIST ════ */\u001b[39m\n\u001b[31m.store-upload-zone{\u001b[39m\n\u001b[31m  border:2px dashed var(--bdr2);border-radius:var(--r);padding:16px;\u001b[39m\n\u001b[31m  text-align:center;cursor:pointer;transition:all .25s;background:var(--sur2);\u001b[39m\n\u001b[31m  position:relative;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.store-upload-zone:hover{border-color:var(--sky);background:rgba(56,189,248,.05);}\u001b[39m\n\u001b[31m.store-upload-zone input{display:none;}\u001b[39m\n\u001b[31m.store-table-wrap{max-height:200px;overflow-y:auto;margin-top:10px;border-radius:var(--r-sm);overflow:hidden;}\u001b[39m\n\u001b[31m.store-table{width:100%;border-collapse:collapse;font-size:.75rem;}\u001b[39m\n\u001b[31m.store-table th{\u001b[39m\n\u001b[31m  background:var(--bg);color:var(--t3);font-family:var(--fnt-head);\u001b[39m\n\u001b[31m  font-size:.65rem;letter-spacing:1.5px;text-transform:uppercase;\u001b[39m\n\u001b[31m  padding:7px 8px;text-align:left;border-bottom:1px solid var(--bdr);\u001b[39m\n\u001b[31m  position:sticky;top:0;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.store-table td{\u001b[39m\n\u001b[31m  padding:6px 8px;border-bottom:1px solid var(--bdr);color:var(--t2);\u001b[39m\n\u001b[31m  font-family:var(--fnt-mono);font-size:.72rem;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.store-table tr:hover td{background:var(--sur3);}\u001b[39m\n\u001b[31m.store-table .matched{color:var(--grn);}\u001b[39m\n\u001b[31m.store-table .unmatched{color:var(--ylw);}\u001b[39m\n\u001b[31m.store-map-btn{\u001b[39m\n\u001b[31m  font-size:.65rem;background:none;border:1px solid var(--bdr);color:var(--t3);\u001b[39m\n\u001b[31m  padding:2px 6px;border-radius:4px;cursor:pointer;transition:all .2s;\u001b[39m\n\u001b[31m  font-family:var(--fnt-mono);\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.store-map-btn:hover{border-color:var(--acc);color:var(--acc);}·\u001b[39m\n\u001b[31m/* ════ CONSIGNEE CARDS ════ */\u001b[39m\n\u001b[31m.cons-list{display:flex;flex-direction:column;gap:8px;margin-bottom:10px;}\u001b[39m\n\u001b[31m.cons-card{\u001b[39m\n\u001b[31m  background:var(--sur2);border:1px solid var(--bdr);border-radius:var(--r-sm);\u001b[39m\n\u001b[31m  padding:11px;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.cons-hdr{display:flex;justify-content:space-between;align-items:center;margin-bottom:8px;}\u001b[39m\n\u001b[31m.cons-num{font-family:var(--fnt-mono);font-size:.65rem;color:var(--t3);}\u001b[39m\n\u001b[31m.cons-del{\u001b[39m\n\u001b[31m  background:none;border:none;color:var(--t4);cursor:pointer;\u001b[39m\n\u001b[31m  font-size:.85rem;padding:2px 5px;border-radius:4px;transition:all .2s;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.cons-del:hover{background:var(--red-dim);color:var(--red);}\u001b[39m\n\u001b[31m.cons-docket-linked{\u001b[39m\n\u001b[31m  display:flex;align-items:center;gap:6px;\u001b[39m\n\u001b[31m  background:rgba(249,115,22,.1);border:1px solid rgba(249,115,22,.3);\u001b[39m\n\u001b[31m  border-radius:4px;padding:4px 8px;margin-top:6px;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.cons-docket-linked .dkt{font-family:var(--fnt-mono);font-size:.82rem;color:var(--acc);}\u001b[39m\n\u001b[31m.cons-docket-linked .unlink{background:none;border:none;color:var(--acc);cursor:pointer;font-size:.75rem;}·\u001b[39m\n\u001b[31m/* ════ DISPATCH DASHBOARD ════ */\u001b[39m\n\u001b[31m.dash-stats{\u001b[39m\n\u001b[31m  display:grid;grid-template-columns:repeat(4,1fr);gap:8px;margin-bottom:14px;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.stat-box{\u001b[39m\n\u001b[31m  background:var(--sur);border:1px solid var(--bdr);border-radius:var(--r);\u001b[39m\n\u001b[31m  padding:10px 8px;text-align:center;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.stat-val{font-family:var(--fnt-head);font-size:1.4rem;font-weight:800;}\u001b[39m\n\u001b[31m.stat-lbl{font-size:.6rem;color:var(--t3);font-family:var(--fnt-mono);text-transform:uppercase;letter-spacing:1px;margin-top:2px;}\u001b[39m\n\u001b[31m.stat-box.grn .stat-val{color:var(--grn);}\u001b[39m\n\u001b[31m.stat-box.ylw .stat-val{color:var(--ylw);}\u001b[39m\n\u001b[31m.stat-box.red .stat-val{color:var(--red);}\u001b[39m\n\u001b[31m.stat-box.sky .stat-val{color:var(--sky);}·\u001b[39m\n\u001b[31m/* ════ DOCKET TILES ════ */\u001b[39m\n\u001b[31m.docket-grid{display:flex;flex-direction:column;gap:8px;}\u001b[39m\n\u001b[31m.docket-tile{\u001b[39m\n\u001b[31m  background:var(--sur);border:1px solid var(--bdr);border-radius:var(--r);\u001b[39m\n\u001b[31m  padding:12px;transition:border-color .2s;cursor:pointer;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.docket-tile:hover{border-color:var(--acc);}\u001b[39m\n\u001b[31m.docket-tile-hdr{display:flex;justify-content:space-between;align-items:flex-start;margin-bottom:8px;}\u001b[39m\n\u001b[31m.docket-tile-left{flex:1;}\u001b[39m\n\u001b[31m.docket-num{font-family:var(--fnt-mono);font-size:.95rem;font-weight:500;color:var(--sky);letter-spacing:1px;}\u001b[39m\n\u001b[31m.docket-store{font-size:.82rem;color:var(--t1);font-weight:600;margin-top:2px;}\u001b[39m\n\u001b[31m.docket-courier{font-size:.68rem;color:var(--t3);font-family:var(--fnt-mono);margin-top:2px;}\u001b[39m\n\u001b[31m.status-pill{\u001b[39m\n\u001b[31m  font-size:.62rem;font-weight:700;font-family:var(--fnt-mono);\u001b[39m\n\u001b[31m  padding:3px 8px;border-radius:10px;text-transform:uppercase;letter-spacing:.5px;\u001b[39m\n\u001b[31m  flex-shrink:0;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.sp-delivered{background:var(--grn-dim);color:var(--grn);}\u001b[39m\n\u001b[31m.sp-transit{background:var(--ylw-dim);color:var(--ylw);}\u001b[39m\n\u001b[31m.sp-booked{background:var(--sky-dim);color:var(--sky);}\u001b[39m\n\u001b[31m.sp-pending{background:var(--bdr);color:var(--t3);}\u001b[39m\n\u001b[31m.sp-failed{background:var(--red-dim);color:var(--red);}\u001b[39m\n\u001b[31m.docket-tile-footer{display:flex;align-items:center;gap:8px;flex-wrap:wrap;}\u001b[39m\n\u001b[31m.docket-last-event{font-size:.7rem;color:var(--t3);flex:1;font-family:var(--fnt-mono);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}\u001b[39m\n\u001b[31m.btn-track-live{\u001b[39m\n\u001b[31m  background:none;border:1px solid var(--acc);color:var(--acc);\u001b[39m\n\u001b[31m  font-size:.65rem;padding:4px 10px;border-radius:20px;cursor:pointer;\u001b[39m\n\u001b[31m  font-family:var(--fnt-mono);transition:all .2s;white-space:nowrap;display:flex;align-items:center;gap:4px;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.btn-track-live:hover{background:var(--acc);color:#fff;}\u001b[39m\n\u001b[31m.btn-track-live .live-dot{width:5px;height:5px;border-radius:50%;background:var(--grn);animation:blink 1.5s infinite;flex-shrink:0;}\u001b[39m\n\u001b[31m.docket-progress{height:2px;background:var(--bdr);border-radius:2px;margin-top:8px;overflow:hidden;}\u001b[39m\n\u001b[31m.docket-progress-fill{height:100%;border-radius:2px;transition:width .5s;}·\u001b[39m\n\u001b[31m/* ════ TRACKING DETAIL ════ */\u001b[39m\n\u001b[31m.track-timeline{padding:4px 0;}\u001b[39m\n\u001b[31m.track-evt{display:flex;gap:10px;padding:6px 0;position:relative;padding-left:20px;}\u001b[39m\n\u001b[31m.track-evt::before{\u001b[39m\n\u001b[31m  content:'';position:absolute;left:5px;top:11px;\u001b[39m\n\u001b[31m  width:7px;height:7px;border-radius:50%;background:var(--bdr2);border:1px solid var(--bdr2);\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.track-evt:first-child::before{background:var(--grn);border-color:var(--grn);}\u001b[39m\n\u001b[31m.track-evt::after{\u001b[39m\n\u001b[31m  content:'';position:absolute;left:8px;top:18px;\u001b[39m\n\u001b[31m  width:1px;height:calc(100% - 6px);background:var(--bdr);\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.track-evt:last-child::after{display:none;}\u001b[39m\n\u001b[31m.track-time{font-family:var(--fnt-mono);font-size:.62rem;color:var(--t3);min-width:90px;flex-shrink:0;padding-top:2px;}\u001b[39m\n\u001b[31m.track-desc{font-size:.78rem;color:var(--t2);}\u001b[39m\n\u001b[31m.track-loc{font-size:.67rem;color:var(--t3);font-family:var(--fnt-mono);margin-top:2px;}·\u001b[39m\n\u001b[31m/* ════ SHARE TOAST ════ */\u001b[39m\n\u001b[31m.share-banner{\u001b[39m\n\u001b[31m  position:fixed;top:0;left:0;right:0;z-index:200;\u001b[39m\n\u001b[31m  background:rgba(34,197,94,.15);border-bottom:1px solid rgba(34,197,94,.4);\u001b[39m\n\u001b[31m  padding:10px 16px;font-size:.78rem;color:var(--grn);font-family:var(--fnt-mono);\u001b[39m\n\u001b[31m  display:flex;align-items:center;gap:8px;\u001b[39m\n\u001b[31m  transform:translateY(-100%);transition:transform .3s;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.share-banner.show{transform:translateY(0);}·\u001b[39m\n\u001b[31m/* ════ MODAL ════ */\u001b[39m\n\u001b[31m.overlay{position:fixed;inset:0;background:rgba(0,0,0,.75);z-index:100;display:flex;align-items:flex-end;opacity:0;pointer-events:none;transition:opacity .3s;}\u001b[39m\n\u001b[31m.overlay.open{opacity:1;pointer-events:all;}\u001b[39m\n\u001b[31m.sheet{\u001b[39m\n\u001b[31m  background:var(--sur);border-radius:var(--r-lg) var(--r-lg) 0 0;\u001b[39m\n\u001b[31m  width:100%;max-height:88vh;overflow-y:auto;padding:18px;\u001b[39m\n\u001b[31m  transform:translateY(100%);transition:transform .3s cubic-bezier(.32,.72,0,1);\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.overlay.open .sheet{transform:translateY(0);}\u001b[39m\n\u001b[31m.sheet-handle{width:36px;height:4px;background:var(--bdr2);border-radius:2px;margin:0 auto 18px;}\u001b[39m\n\u001b[31m.sheet-title{font-family:var(--fnt-head);font-size:1.2rem;font-weight:800;letter-spacing:.5px;margin-bottom:14px;}\u001b[39m\n\u001b[31m.sheet::-webkit-scrollbar{width:3px;}\u001b[39m\n\u001b[31m.sheet::-webkit-scrollbar-thumb{background:var(--bdr2);}·\u001b[39m\n\u001b[31m/* ════ TOAST ════ */\u001b[39m\n\u001b[31m.toast{\u001b[39m\n\u001b[31m  position:fixed;bottom:20px;left:50%;transform:translateX(-50%) translateY(70px);\u001b[39m\n\u001b[31m  background:var(--sur2);border:1px solid var(--bdr2);color:var(--t1);\u001b[39m\n\u001b[31m  font-size:.78rem;padding:9px 16px;border-radius:30px;z-index:300;\u001b[39m\n\u001b[31m  transition:transform .3s;white-space:nowrap;font-family:var(--fnt-mono);\u001b[39m\n\u001b[31m  pointer-events:none;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.toast.show{transform:translateX(-50%) translateY(0);}\u001b[39m\n\u001b[31m.toast.ok{border-color:var(--grn);color:var(--grn);}\u001b[39m\n\u001b[31m.toast.err{border-color:var(--red);color:var(--red);}\u001b[39m\n\u001b[31m.toast.info{border-color:var(--sky);color:var(--sky);}·\u001b[39m\n\u001b[31m/* ════ COURIER MASTER ════ */\u001b[39m\n\u001b[31m.courier-master-row{\u001b[39m\n\u001b[31m  display:flex;align-items:center;gap:8px;padding:8px 0;\u001b[39m\n\u001b[31m  border-bottom:1px solid var(--bdr);\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.courier-master-row:last-child{border-bottom:none;}\u001b[39m\n\u001b[31m.cml{flex:1;}\u001b[39m\n\u001b[31m.cml-name{font-weight:600;font-size:.85rem;}\u001b[39m\n\u001b[31m.cml-url{font-family:var(--fnt-mono);font-size:.65rem;color:var(--t3);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:180px;}\u001b[39m\n\u001b[31m.courier-logo{width:24px;height:24px;border-radius:4px;object-fit:contain;background:var(--sur3);padding:2px;}·\u001b[39m\n\u001b[31m/* ════ PROGRESS BAR ════ */\u001b[39m\n\u001b[31m.ai-progress{\u001b[39m\n\u001b[31m  background:var(--sur2);border:1px solid var(--bdr);border-radius:var(--r-sm);\u001b[39m\n\u001b[31m  padding:10px 12px;margin-top:8px;display:none;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.ai-progress.visible{display:block;}\u001b[39m\n\u001b[31m.ai-prog-bar{height:3px;background:var(--bdr);border-radius:3px;margin-top:6px;overflow:hidden;}\u001b[39m\n\u001b[31m.ai-prog-fill{height:100%;background:linear-gradient(90deg,var(--sky),var(--acc));width:0;transition:width .3s;}\u001b[39m\n\u001b[31m.ai-prog-label{font-size:.68rem;color:var(--sky);font-family:var(--fnt-mono);}·\u001b[39m\n\u001b[31m/* ════ EMPTY ════ */\u001b[39m\n\u001b[31m.empty{padding:40px 20px;text-align:center;color:var(--t4);}\u001b[39m\n\u001b[31m.empty .e-icon{font-size:2.5rem;margin-bottom:10px;}\u001b[39m\n\u001b[31m.empty p{font-size:.82rem;}·\u001b[39m\n\u001b[31m/* ════ INFO TAG ════ */\u001b[39m\n\u001b[31m.info-tag{\u001b[39m\n\u001b[31m  background:var(--sur2);border:1px solid var(--bdr);border-radius:var(--r-sm);\u001b[39m\n\u001b[31m  padding:7px 10px;font-size:.75rem;color:var(--t2);margin-bottom:8px;\u001b[39m\n\u001b[31m  display:flex;align-items:center;gap:8px;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.info-tag strong{color:var(--t1);}·\u001b[39m\n\u001b[31m/* ════ JOB PILL ════ */\u001b[39m\n\u001b[31m.job-pill{\u001b[39m\n\u001b[31m  display:inline-flex;align-items:center;gap:6px;\u001b[39m\n\u001b[31m  background:rgba(249,115,22,.1);border:1px solid rgba(249,115,22,.3);\u001b[39m\n\u001b[31m  color:var(--acc);font-family:var(--fnt-mono);font-size:.68rem;\u001b[39m\n\u001b[31m  padding:3px 9px;border-radius:20px;margin-top:4px;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.job-pill button{background:none;border:none;color:var(--acc);cursor:pointer;font-size:.7rem;}·\u001b[39m\n\u001b[31m/* ════ SECTION DIVIDER ════ */\u001b[39m\n\u001b[31m.sec-div{\u001b[39m\n\u001b[31m  display:flex;align-items:center;gap:10px;margin:14px 0 10px;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.sec-div span{font-family:var(--fnt-head);font-size:.7rem;font-weight:700;color:var(--t3);text-transform:uppercase;letter-spacing:2px;white-space:nowrap;}\u001b[39m\n\u001b[31m.sec-div::before,.sec-div::after{content:'';flex:1;height:1px;background:var(--bdr);}·\u001b[39m\n\u001b[31m/* ── MAPPING UI ── */\u001b[39m\n\u001b[31m.map-table{width:100%;border-collapse:collapse;font-size:.78rem;margin:10px 0;}\u001b[39m\n\u001b[31m.map-table th{background:var(--bg);color:var(--t3);font-family:var(--fnt-head);font-size:.65rem;\u001b[39m\n\u001b[31m  letter-spacing:1.5px;text-transform:uppercase;padding:7px 10px;text-align:left;\u001b[39m\n\u001b[31m  border-bottom:1px solid var(--bdr);}\u001b[39m\n\u001b[31m.map-table td{padding:7px 10px;border-bottom:1px solid var(--bdr);vertical-align:middle;}\u001b[39m\n\u001b[31m.map-table tr:last-child td{border-bottom:none;}\u001b[39m\n\u001b[31m.map-table select{background:var(--sur2);border:1px solid var(--bdr);color:var(--t1);\u001b[39m\n\u001b[31m  font-family:var(--fnt-mono);font-size:.75rem;padding:5px 8px;border-radius:var(--r-sm);\u001b[39m\n\u001b[31m  width:100%;outline:none;}\u001b[39m\n\u001b[31m.map-table select:focus{border-color:var(--acc);}\u001b[39m\n\u001b[31m.map-req{color:var(--acc);margin-left:2px;}\u001b[39m\n\u001b[31m.map-saved-badge{font-size:.6rem;background:rgba(57,255,143,.15);color:var(--accent3,#39ff8f);\u001b[39m\n\u001b[31m  border:1px solid rgba(57,255,143,.3);padding:2px 7px;border-radius:10px;\u001b[39m\n\u001b[31m  font-family:var(--fnt-mono);margin-left:6px;}·\u001b[39m\n\u001b[31m/* ── VERIFICATION SCREEN ── */\u001b[39m\n\u001b[31m#verifyPane{display:none;position:fixed;inset:0;background:var(--bg);z-index:50;\u001b[39m\n\u001b[31m  overflow-y:auto;padding:14px;}\u001b[39m\n\u001b[31m.verify-hdr{display:flex;align-items:center;gap:10px;margin-bottom:14px;\u001b[39m\n\u001b[31m  position:sticky;top:0;background:var(--bg);padding-bottom:10px;\u001b[39m\n\u001b[31m  border-bottom:1px solid var(--bdr);z-index:1;}\u001b[39m\n\u001b[31m.verify-title{font-family:var(--fnt-head);font-size:1.1rem;font-weight:800;flex:1;}\u001b[39m\n\u001b[31m.verify-row{background:var(--sur);border:1px solid var(--bdr);border-radius:var(--r-sm);\u001b[39m\n\u001b[31m  padding:10px 12px;margin-bottom:8px;position:relative;}\u001b[39m\n\u001b[31m.verify-row.has-dupe{border-color:var(--red);background:rgba(239,68,68,.04);}\u001b[39m\n\u001b[31m.verify-row.no-docket{border-color:var(--ylw);background:rgba(234,179,8,.04);}\u001b[39m\n\u001b[31m.verify-row.ok{border-left:3px solid var(--grn);}\u001b[39m\n\u001b[31m.verify-row-hdr{display:flex;align-items:center;gap:8px;margin-bottom:8px;}\u001b[39m\n\u001b[31m.verify-num{font-family:var(--fnt-mono);font-size:.65rem;color:var(--t3);}\u001b[39m\n\u001b[31m.verify-store{font-weight:600;font-size:.9rem;flex:1;}\u001b[39m\n\u001b[31m.verify-flag{font-size:.65rem;font-weight:700;font-family:var(--fnt-mono);\u001b[39m\n\u001b[31m  padding:2px 7px;border-radius:10px;text-transform:uppercase;}\u001b[39m\n\u001b[31m.vf-dupe{background:var(--red-dim);color:var(--red);}\u001b[39m\n\u001b[31m.vf-nodocket{background:var(--ylw-dim);color:var(--ylw);}\u001b[39m\n\u001b[31m.vf-ok{background:var(--grn-dim);color:var(--grn);}\u001b[39m\n\u001b[31m.vf-short{background:rgba(56,189,248,.15);color:var(--sky);}\u001b[39m\n\u001b[31m.verify-fields{display:grid;grid-template-columns:1fr 1fr;gap:8px;margin-bottom:8px;}\u001b[39m\n\u001b[31m.verify-field label{font-size:.62rem;color:var(--t3);text-transform:uppercase;\u001b[39m\n\u001b[31m  letter-spacing:1px;display:block;margin-bottom:3px;font-family:var(--fnt-mono);}\u001b[39m\n\u001b[31m.verify-field input{width:100%;background:var(--sur2);border:1px solid var(--bdr);\u001b[39m\n\u001b[31m  color:var(--t1);font-family:var(--fnt-mono);font-size:.82rem;padding:6px 9px;\u001b[39m\n\u001b[31m  border-radius:var(--r-sm);outline:none;}\u001b[39m\n\u001b[31m.verify-field input:focus{border-color:var(--acc);}\u001b[39m\n\u001b[31m.verify-field input.changed{border-color:var(--ylw);background:rgba(234,179,8,.06);}\u001b[39m\n\u001b[31m.verify-actions{display:flex;gap:6px;align-items:center;flex-wrap:wrap;}\u001b[39m\n\u001b[31m.btn-wa{background:rgba(37,211,102,.15);border:1px solid rgba(37,211,102,.3);\u001b[39m\n\u001b[31m  color:#25d366;font-size:.65rem;padding:4px 10px;border-radius:10px;cursor:pointer;\u001b[39m\n\u001b[31m  font-family:var(--fnt-mono);transition:all .2s;}\u001b[39m\n\u001b[31m.btn-wa:hover{background:rgba(37,211,102,.3);}\u001b[39m\n\u001b[31m.verify-thumb{width:48px;height:48px;object-fit:cover;border-radius:6px;\u001b[39m\n\u001b[31m  border:1px solid var(--bdr);cursor:pointer;flex-shrink:0;}\u001b[39m\n\u001b[31m.verify-summary{background:var(--sur);border:1px solid var(--bdr);border-radius:var(--r);\u001b[39m\n\u001b[31m  padding:12px 14px;margin-bottom:14px;display:flex;gap:16px;flex-wrap:wrap;}\u001b[39m\n\u001b[31m.vs-item{text-align:center;}\u001b[39m\n\u001b[31m.vs-val{font-family:var(--fnt-head);font-size:1.3rem;font-weight:800;}\u001b[39m\n\u001b[31m.vs-lbl{font-size:.6rem;color:var(--t3);font-family:var(--fnt-mono);text-transform:uppercase;\u001b[39m\n\u001b[31m  letter-spacing:.5px;margin-top:2px;}\u001b[39m\n\u001b[31m.ocr-correction-note{font-size:.65rem;color:var(--ylw);font-family:var(--fnt-mono);\u001b[39m\n\u001b[31m  margin-top:3px;}\u001b[39m\n\u001b[31m</style>\u001b[39m\n\u001b[31m</head>\u001b[39m\n\u001b[31m<body>·\u001b[39m\n\u001b[31m<!-- SPLASH -->\u001b[39m\n\u001b[31m<div id=\\\"splash\\\">\u001b[39m\n\u001b[31m  <div class=\\\"splash-logo\\\">360LM <em>DISPATCH</em></div>\u001b[39m\n\u001b[31m  <div class=\\\"splash-sub\\\">Courier · Bus · Field Ops</div>\u001b[39m\n\u001b[31m  <div class=\\\"splash-track\\\"><div class=\\\"splash-track-fill\\\"></div></div>\u001b[39m\n\u001b[31m</div>·\u001b[39m\n\u001b[31m<!-- SHARE BANNER -->\u001b[39m\n\u001b[31m<div class=\\\"share-banner\\\" id=\\\"shareBanner\\\">\u001b[39m\n\u001b[31m  📤 Images received via Share — processing for OCR...\u001b[39m\n\u001b[31m  <button style=\\\"background:none;border:none;color:var(--grn);cursor:pointer;margin-left:auto;\\\" onclick=\\\"dismissShare()\\\">✕</button>\u001b[39m\n\u001b[31m</div>·\u001b[39m\n\u001b[31m<!-- APP -->\u001b[39m\n\u001b[31m<div id=\\\"app\\\" style=\\\"display:none;\\\">·\u001b[39m\n\u001b[31m  <div class=\\\"hdr\\\">\u001b[39m\n\u001b[31m    <div class=\\\"hdr-brand\\\">360LM <em>DISPATCH</em></div>\u001b[39m\n\u001b[31m    <span class=\\\"hdr-dot\\\" id=\\\"netDot\\\"></span>\u001b[39m\n\u001b[31m    <button class=\\\"hdr-btn\\\" onclick=\\\"window.location.href='/hub/'\\\" title=\\\"Hub\\\">🏡</button>\u001b[39m\n\u001b[31m    <button class=\\\"hdr-btn\\\" onclick=\\\"showPane('dashboard')\\\">📊</button>\u001b[39m\n\u001b[31m    <button class=\\\"hdr-btn primary\\\" onclick=\\\"newDispatch()\\\">+ NEW</button>\u001b[39m\n\u001b[31m  </div>·\u001b[39m\n\u001b[31m  <div class=\\\"tabs\\\">\u001b[39m\n\u001b[31m    <button class=\\\"tab active\\\" onclick=\\\"showPane('new')\\\" id=\\\"tab-new\\\">NEW</button>\u001b[39m\n\u001b[31m    <button class=\\\"tab\\\" onclick=\\\"showPane('dashboard')\\\" id=\\\"tab-dashboard\\\">DASHBOARD</button>\u001b[39m\n\u001b[31m    <button class=\\\"tab\\\" onclick=\\\"showPane('track')\\\" id=\\\"tab-track\\\">TRACK</button>\u001b[39m\n\u001b[31m    <button class=\\\"tab\\\" onclick=\\\"showPane('history')\\\" id=\\\"tab-history\\\">HISTORY <span class=\\\"tab-badge\\\" id=\\\"histCount\\\">0</span></button>\u001b[39m\n\u001b[31m    <button class=\\\"tab\\\" onclick=\\\"showPane('config')\\\" id=\\\"tab-config\\\">CONFIG</button>\u001b[39m\n\u001b[31m  </div>·\u001b[39m\n\u001b[31m  <!-- ════════════ NEW DISPATCH ════════════ -->\u001b[39m\n\u001b[31m  <div class=\\\"pane active\\\" id=\\\"pane-new\\\">·\u001b[39m\n\u001b[31m    <!-- 1. JOB REFERENCE -->\u001b[39m\n\u001b[31m    <div class=\\\"card accent-left\\\">\u001b[39m\n\u001b[31m      <div class=\\\"card-title\\\"><span class=\\\"ct-dot\\\"></span>Job Reference</div>\u001b[39m\n\u001b[31m      <div class=\\\"row2\\\">\u001b[39m\n\u001b[31m        <div class=\\\"f\\\">\u001b[39m\n\u001b[31m          <label>Client <span class=\\\"req\\\">*</span></label>\u001b[39m\n\u001b[31m          <select id=\\\"n_client\\\" onchange=\\\"onClientChange()\\\">\u001b[39m\n\u001b[31m            <option value=\\\"\\\">Select...</option>\u001b[39m\n\u001b[31m            <option>Lenovo</option><option>HP</option>\u001b[39m\n\u001b[31m            <option>Philips</option><option>Liebherr</option>\u001b[39m\n\u001b[31m            <option value=\\\"__c\\\">+ Custom</option>\u001b[39m\n\u001b[31m          </select>\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m        <div class=\\\"f\\\">\u001b[39m\n\u001b[31m          <label>Job Ref</label>\u001b[39m\n\u001b[31m          <input type=\\\"text\\\" id=\\\"n_jobref\\\" placeholder=\\\"JOB-042\\\" oninput=\\\"updatePill()\\\">\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      <div class=\\\"f\\\" id=\\\"customClientField\\\" style=\\\"display:none;\\\">\u001b[39m\n\u001b[31m        <label>Custom Client</label>\u001b[39m\n\u001b[31m        <input type=\\\"text\\\" id=\\\"n_customClient\\\" placeholder=\\\"Client name\\\" oninput=\\\"updatePill()\\\">\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      <div id=\\\"jobPill\\\" style=\\\"display:none;\\\" class=\\\"job-pill\\\">\u001b[39m\n\u001b[31m        🔗 <span id=\\\"jobPillTxt\\\"></span>\u001b[39m\n\u001b[31m        <button onclick=\\\"clearJob()\\\">✕</button>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      <div class=\\\"row2\\\" style=\\\"margin-top:10px;\\\">\u001b[39m\n\u001b[31m        <div class=\\\"f\\\" style=\\\"margin-bottom:0;\\\">\u001b[39m\n\u001b[31m          <label>Dispatch Date</label>\u001b[39m\n\u001b[31m          <input type=\\\"date\\\" id=\\\"n_date\\\">\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m        <div class=\\\"f\\\" style=\\\"margin-bottom:0;\\\">\u001b[39m\n\u001b[31m          <label>Courier Company <span class=\\\"req\\\">*</span></label>\u001b[39m\n\u001b[31m          <select id=\\\"n_courier\\\">\u001b[39m\n\u001b[31m            <option value=\\\"\\\">Select...</option>\u001b[39m\n\u001b[31m            <option>SkyKing</option><option>DTDC</option>\u001b[39m\n\u001b[31m            <option>Delhivery</option><option>Blue Dart</option>\u001b[39m\n\u001b[31m            <option>FedEx</option><option>Ecom Express</option>\u001b[39m\n\u001b[31m            <option>India Post</option><option value=\\\"__o\\\">Other</option>\u001b[39m\n\u001b[31m          </select>\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m    </div>·\u001b[39m\n\u001b[31m    <!-- 2. STORE LIST UPLOAD -->\u001b[39m\n\u001b[31m    <div class=\\\"card\\\">\u001b[39m\n\u001b[31m      <div class=\\\"card-title\\\"><span class=\\\"ct-dot\\\" style=\\\"background:var(--sky);\\\"></span>Store / Destination List</div>\u001b[39m\n\u001b[31m      <div class=\\\"store-upload-zone\\\" id=\\\"storeZone\\\" onclick=\\\"document.getElementById('storeFile').click();event.stopPropagation();\\\" style=\\\"cursor:pointer;\\\">\u001b[39m\n\u001b[31m        <input type=\\\"file\\\" id=\\\"storeFile\\\" accept=\\\".xlsx,.xls,.csv\\\" onchange=\\\"handleStoreFile(this)\\\" onclick=\\\"event.stopPropagation();\\\" style=\\\"display:none;\\\">\u001b[39m\n\u001b[31m        <div style=\\\"font-size:1.5rem;margin-bottom:6px;\\\">📋</div>\u001b[39m\n\u001b[31m        <div style=\\\"font-family:var(--fnt-head);font-weight:700;font-size:.9rem;\\\">Upload Store List</div>\u001b[39m\n\u001b[31m        <div style=\\\"font-size:.68rem;color:var(--t3);margin-top:3px;font-family:var(--fnt-mono);\\\">Excel / CSV · stores, cities, contacts</div>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      <div id=\\\"storeTableArea\\\" style=\\\"display:none;\\\">\u001b[39m\n\u001b[31m        <!-- Column detection result -->\u001b[39m\n\u001b[31m        <div id=\\\"storeColInfo\\\" style=\\\"margin-top:10px;display:none;\\\"></div>\u001b[39m\n\u001b[31m        <div style=\\\"display:flex;justify-content:space-between;align-items:center;margin-top:10px;margin-bottom:6px;\\\">\u001b[39m\n\u001b[31m          <span style=\\\"font-family:var(--fnt-mono);font-size:.7rem;color:var(--sky);\\\" id=\\\"storeCount\\\">0 stores loaded</span>\u001b[39m\n\u001b[31m          <button class=\\\"btn-sm\\\" onclick=\\\"clearStores()\\\">Clear</button>\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m        <div class=\\\"store-table-wrap\\\">\u001b[39m\n\u001b[31m          <table class=\\\"store-table\\\" id=\\\"storeTable\\\"></table>\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m        <!-- Inline add-as-consignees UI instead of confirm() -->\u001b[39m\n\u001b[31m        <div id=\\\"storeAddPanel\\\" style=\\\"margin-top:10px;background:var(--sur3);border:1px solid var(--bdr2);border-radius:var(--r-sm);padding:10px 12px;display:none;\\\">\u001b[39m\n\u001b[31m          <div style=\\\"font-size:.75rem;color:var(--t1);margin-bottom:8px;font-weight:600;\\\" id=\\\"storeAddTitle\\\">Add stores as consignees?</div>\u001b[39m\n\u001b[31m          <div class=\\\"row2\\\">\u001b[39m\n\u001b[31m            <button class=\\\"btn btn-ghost\\\" style=\\\"font-size:.75rem;padding:8px;\\\" onclick=\\\"document.getElementById('storeAddPanel').style.display='none'\\\">Skip for now</button>\u001b[39m\n\u001b[31m            <button class=\\\"btn btn-orange\\\" style=\\\"font-size:.75rem;padding:8px;\\\" id=\\\"storeAddBtn\\\" onclick=\\\"addStoresToConsignees()\\\">✓ Add All as Consignees</button>\u001b[39m\n\u001b[31m          </div>\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m        <button class=\\\"btn btn-ghost\\\" style=\\\"margin-top:8px;font-size:.78rem;\\\" onclick=\\\"autoMatchDockets()\\\">\u001b[39m\n\u001b[31m          🔗 Auto-match Dockets to Stores\u001b[39m\n\u001b[31m        </button>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m    </div>·\u001b[39m\n\u001b[31m    <!-- 3. DOCKET SLIPS — AI OCR -->\u001b[39m\n\u001b[31m    <div class=\\\"card\\\">\u001b[39m\n\u001b[31m      <div class=\\\"card-title\\\"><span class=\\\"ct-dot\\\" style=\\\"background:var(--grn);\\\"></span>Docket Slips — AI OCR</div>·\u001b[39m\n\u001b[31m      <!-- Share target indicator -->\u001b[39m\n\u001b[31m      <div class=\\\"info-tag\\\" id=\\\"shareTargetInfo\\\" style=\\\"display:none;\\\">\u001b[39m\n\u001b[31m        <span>📤</span><span>Images shared from WhatsApp — <strong id=\\\"sharedCount\\\">0</strong> slips received</span>\u001b[39m\n\u001b[31m      </div>·\u001b[39m\n\u001b[31m      <label class=\\\"upload-zone\\\" id=\\\"slipZone\\\" for=\\\"slipFiles\\\" style=\\\"display:block;cursor:pointer;\\\">\u001b[39m\n\u001b[31m        <input type=\\\"file\\\" id=\\\"slipFiles\\\" accept=\\\"image/*\\\" multiple onchange=\\\"handleSlipFiles(this.files)\\\" style=\\\"display:none;\\\">\u001b[39m\n\u001b[31m        <div class=\\\"uz-icon\\\">🖼️</div>\u001b[39m\n\u001b[31m        <div class=\\\"uz-title\\\">Upload Courier Slips</div>\u001b[39m\n\u001b[31m        <div class=\\\"uz-sub\\\">Multiple images · Tap or drag · Camera capture supported</div>\u001b[39m\n\u001b[31m      </label>·\u001b[39m\n\u001b[31m      <!-- OCR Working indicator -->\u001b[39m\n\u001b[31m      <div class=\\\"ocr-working\\\" id=\\\"ocrWorking\\\">\u001b[39m\n\u001b[31m        <span class=\\\"spin\\\">⟳</span>\u001b[39m\n\u001b[31m        <span id=\\\"ocrWorkingText\\\">Running OCR...</span>\u001b[39m\n\u001b[31m      </div>·\u001b[39m\n\u001b[31m      <!-- AI Progress -->\u001b[39m\n\u001b[31m      <div class=\\\"ai-progress\\\" id=\\\"aiProgress\\\">\u001b[39m\n\u001b[31m        <div class=\\\"ai-prog-label\\\" id=\\\"aiProgLabel\\\">Initializing Transformers.js...</div>\u001b[39m\n\u001b[31m        <div class=\\\"ai-prog-bar\\\"><div class=\\\"ai-prog-fill\\\" id=\\\"aiProgFill\\\"></div></div>\u001b[39m\n\u001b[31m      </div>·\u001b[39m\n\u001b[31m      <!-- OCR Results -->\u001b[39m\n\u001b[31m      <div class=\\\"ocr-results\\\" id=\\\"ocrResults\\\">\u001b[39m\n\u001b[31m        <div class=\\\"sec-div\\\"><span>Detected Dockets</span></div>\u001b[39m\n\u001b[31m        <div id=\\\"ocrSummary\\\" style=\\\"display:none;font-size:.72rem;font-family:var(--fnt-mono);\u001b[39m\n\u001b[31m          padding:4px 8px 8px;gap:8px\\\"></div>\u001b[39m\n\u001b[31m        <div class=\\\"ocr-slip-list\\\" id=\\\"ocrSlipList\\\"></div>\u001b[39m\n\u001b[31m        <div class=\\\"btn-row\\\" style=\\\"margin-top:10px;\\\">\u001b[39m\n\u001b[31m          <button class=\\\"btn btn-ghost\\\" onclick=\\\"clearSlips()\\\">Clear All</button>\u001b[39m\n\u001b[31m          <button class=\\\"btn btn-grn\\\" onclick=\\\"applyAllDockets()\\\">✓ Apply All to Consignees</button>\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m    </div>·\u001b[39m\n\u001b[31m    <!-- 4. CONSIGNEES -->\u001b[39m\n\u001b[31m    <div class=\\\"card accent-left\\\">\u001b[39m\n\u001b[31m      <div class=\\\"card-title\\\"><span class=\\\"ct-dot\\\"></span>Consignees</div>\u001b[39m\n\u001b[31m      <div class=\\\"cons-list\\\" id=\\\"consList\\\"></div>\u001b[39m\n\u001b[31m      <button class=\\\"btn btn-ghost\\\" style=\\\"font-size:.8rem;\\\" onclick=\\\"addConsignee()\\\">+ Add Consignee</button>\u001b[39m\n\u001b[31m    </div>·\u001b[39m\n\u001b[31m    <!-- 5. REMARKS -->\u001b[39m\n\u001b[31m    <div class=\\\"card\\\">\u001b[39m\n\u001b[31m      <div class=\\\"card-title\\\"><span class=\\\"ct-dot\\\" style=\\\"background:var(--t3);\\\"></span>Remarks</div>\u001b[39m\n\u001b[31m      <div class=\\\"f\\\" style=\\\"margin-bottom:0;\\\">\u001b[39m\n\u001b[31m        <textarea id=\\\"n_remarks\\\" placeholder=\\\"Instructions, notes, special handling...\\\"></textarea>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m    </div>·\u001b[39m\n\u001b[31m    <!-- ACTIONS -->\u001b[39m\n\u001b[31m    <div class=\\\"btn-row\\\">\u001b[39m\n\u001b[31m      <button class=\\\"btn btn-ghost\\\" onclick=\\\"saveDraft()\\\">💾 Draft</button>\u001b[39m\n\u001b[31m      <button class=\\\"btn btn-orange\\\" onclick=\\\"finaliseDispatch()\\\">✓ Finalise & Log</button>\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m    <div style=\\\"height:16px;\\\"></div>\u001b[39m\n\u001b[31m  </div>·\u001b[39m\n\u001b[31m  <!-- ════════════ DASHBOARD ════════════ -->\u001b[39m\n\u001b[31m  <div class=\\\"pane\\\" id=\\\"pane-dashboard\\\">\u001b[39m\n\u001b[31m    <div class=\\\"dash-stats\\\">\u001b[39m\n\u001b[31m      <div class=\\\"stat-box grn\\\"><div class=\\\"stat-val\\\" id=\\\"st-delivered\\\">0</div><div class=\\\"stat-lbl\\\">Delivered</div></div>\u001b[39m\n\u001b[31m      <div class=\\\"stat-box ylw\\\"><div class=\\\"stat-val\\\" id=\\\"st-transit\\\">0</div><div class=\\\"stat-lbl\\\">In Transit</div></div>\u001b[39m\n\u001b[31m      <div class=\\\"stat-box sky\\\"><div class=\\\"stat-val\\\" id=\\\"st-booked\\\">0</div><div class=\\\"stat-lbl\\\">Booked</div></div>\u001b[39m\n\u001b[31m      <div class=\\\"stat-box red\\\"><div class=\\\"stat-val\\\" id=\\\"st-pending\\\">0</div><div class=\\\"stat-lbl\\\">Pending</div></div>\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m    <div id=\\\"dashFilters\\\" style=\\\"display:flex;gap:6px;flex-wrap:wrap;margin-bottom:12px;\\\">\u001b[39m\n\u001b[31m      <span class=\\\"btn-sm primary\\\" onclick=\\\"filterDash('all',this)\\\" style=\\\"cursor:pointer;\\\">All</span>\u001b[39m\n\u001b[31m      <span class=\\\"btn-sm\\\" onclick=\\\"filterDash('delivered',this)\\\" style=\\\"cursor:pointer;\\\">Delivered</span>\u001b[39m\n\u001b[31m      <span class=\\\"btn-sm\\\" onclick=\\\"filterDash('transit',this)\\\" style=\\\"cursor:pointer;\\\">In Transit</span>\u001b[39m\n\u001b[31m      <span class=\\\"btn-sm\\\" onclick=\\\"filterDash('booked',this)\\\" style=\\\"cursor:pointer;\\\">Booked</span>\u001b[39m\n\u001b[31m      <span class=\\\"btn-sm\\\" onclick=\\\"filterDash('pending',this)\\\" style=\\\"cursor:pointer;\\\">Pending</span>\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m    <div class=\\\"docket-grid\\\" id=\\\"dashGrid\\\">\u001b[39m\n\u001b[31m      <div class=\\\"empty\\\"><div class=\\\"e-icon\\\">📊</div><p>No dispatches yet.<br>Create one to see the dashboard.</p></div>\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m    <div style=\\\"display:flex;gap:8px;margin-top:10px;flex-wrap:wrap;\\\">\u001b[39m\n\u001b[31m      <button class=\\\"btn btn-ghost\\\" style=\\\"flex:2;font-size:.8rem;\\\" id=\\\"btnRefreshAll\\\" onclick=\\\"refreshAllTracking()\\\">\u001b[39m\n\u001b[31m        🔄 Refresh All Tracking\u001b[39m\n\u001b[31m      </button>\u001b[39m\n\u001b[31m      <button class=\\\"btn btn-ghost\\\" style=\\\"flex:1;font-size:.8rem;border-color:var(--sky);color:var(--sky);\\\"\u001b[39m\n\u001b[31m        onclick=\\\"shareSelectedReport()\\\">\u001b[39m\n\u001b[31m        📊 Share Selected\u001b[39m\n\u001b[31m      </button>\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m  </div>·\u001b[39m\n\u001b[31m  <!-- ════════════ TRACK ════════════ -->\u001b[39m\n\u001b[31m  <div class=\\\"pane\\\" id=\\\"pane-track\\\">·\u001b[39m\n\u001b[31m    <!-- Courier selector -->\u001b[39m\n\u001b[31m    <div class=\\\"card\\\" style=\\\"margin-bottom:10px;\\\">\u001b[39m\n\u001b[31m      <div class=\\\"card-title\\\"><span class=\\\"ct-dot\\\" style=\\\"background:var(--sky);\\\"></span>Manual Tracking</div>\u001b[39m\n\u001b[31m      <div style=\\\"font-size:.72rem;color:var(--t2);margin-bottom:10px;line-height:1.5;\\\">\u001b[39m\n\u001b[31m        Track any docket number — select from already dispatched dockets or enter new ones manually.\u001b[39m\n\u001b[31m        SkyKing supports up to <strong>10 dockets at once</strong>; others are tracked one at a time.\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      <div class=\\\"f\\\">\u001b[39m\n\u001b[31m        <label>Courier Company</label>\u001b[39m\n\u001b[31m        <select id=\\\"t_courier\\\" onchange=\\\"buildTrackDocketChips()\\\">\u001b[39m\n\u001b[31m          <option>SkyKing</option><option>DTDC</option><option>Delhivery</option>\u001b[39m\n\u001b[31m          <option>Blue Dart</option><option>FedEx</option><option>Ecom Express</option><option>India Post</option>\u001b[39m\n\u001b[31m        </select>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m    </div>·\u001b[39m\n\u001b[31m    <!-- Docket selector — from existing dispatches -->\u001b[39m\n\u001b[31m    <div class=\\\"card\\\" style=\\\"margin-bottom:10px;\\\">\u001b[39m\n\u001b[31m      <div class=\\\"card-title\\\"><span class=\\\"ct-dot\\\" style=\\\"background:var(--acc);\\\"></span>\u001b[39m\n\u001b[31m        Select from Dispatched Dockets\u001b[39m\n\u001b[31m        <span id=\\\"trackCourierLimit\\\" style=\\\"margin-left:auto;font-size:.65rem;color:var(--t3);font-weight:400;text-transform:none;letter-spacing:0;font-family:var(--fnt-mono);\\\"></span>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      <div id=\\\"trackDocketChips\\\" style=\\\"display:flex;flex-wrap:wrap;gap:6px;margin-bottom:10px;min-height:28px;\\\">\u001b[39m\n\u001b[31m        <span style=\\\"font-size:.72rem;color:var(--t3);font-family:var(--fnt-mono);\\\">No dispatched dockets yet</span>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      <div style=\\\"font-size:.68rem;color:var(--t3);font-family:var(--fnt-mono);margin-bottom:8px;\\\">\u001b[39m\n\u001b[31m        Tap a docket to add/remove from tracking list. Selected shown in orange.\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m    </div>·\u001b[39m\n\u001b[31m    <!-- Manual entry for new dockets -->\u001b[39m\n\u001b[31m    <div class=\\\"card\\\" style=\\\"margin-bottom:10px;\\\">\u001b[39m\n\u001b[31m      <div class=\\\"card-title\\\"><span class=\\\"ct-dot\\\" style=\\\"background:var(--grn);\\\"></span>Add New Docket Numbers</div>\u001b[39m\n\u001b[31m      <div class=\\\"f\\\" style=\\\"margin-bottom:6px;\\\">\u001b[39m\n\u001b[31m        <label>Enter docket numbers (comma or newline separated)</label>\u001b[39m\n\u001b[31m        <textarea id=\\\"t_manual_dockets\\\" placeholder=\\\"443005527&#10;443005528&#10;or: 443005527, 443005528\\\"\u001b[39m\n\u001b[31m          style=\\\"min-height:56px;font-family:var(--fnt-mono);font-size:.82rem;letter-spacing:.5px;\\\"></textarea>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      <button class=\\\"btn-sm primary\\\" onclick=\\\"addManualDockets()\\\" style=\\\"font-size:.72rem;\\\">+ Add to List</button>\u001b[39m\n\u001b[31m    </div>·\u001b[39m\n\u001b[31m    <!-- Selected dockets summary + track button -->\u001b[39m\n\u001b[31m    <div id=\\\"trackSelectedSummary\\\" style=\\\"display:none;\\\" class=\\\"card\\\" style=\\\"margin-bottom:10px;\\\">\u001b[39m\n\u001b[31m      <div style=\\\"display:flex;align-items:center;justify-content:space-between;margin-bottom:8px;\\\">\u001b[39m\n\u001b[31m        <div>\u001b[39m\n\u001b[31m          <span style=\\\"font-family:var(--fnt-mono);font-size:.72rem;color:var(--sky);\\\" id=\\\"trackSelectedCount\\\">0 selected</span>\u001b[39m\n\u001b[31m          <span style=\\\"font-family:var(--fnt-mono);font-size:.65rem;color:var(--t3);margin-left:8px;\\\" id=\\\"trackSelectedList\\\"></span>\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m        <button class=\\\"btn-sm\\\" onclick=\\\"clearTrackSelection()\\\">Clear</button>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      <div class=\\\"btn-row\\\">\u001b[39m\n\u001b[31m        <button class=\\\"btn btn-ghost\\\" onclick=\\\"openTrackOnCourierSite()\\\">🔗 Open Courier Site</button>\u001b[39m\n\u001b[31m        <button class=\\\"btn btn-orange\\\" onclick=\\\"trackSelected()\\\">🔍 Track All via Proxy</button>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m    </div>·\u001b[39m\n\u001b[31m    <div id=\\\"trackResultsArea\\\"></div>\u001b[39m\n\u001b[31m  </div>·\u001b[39m\n\u001b[31m  <!-- ════════════ HISTORY ════════════ -->\u001b[39m\n\u001b[31m  <div class=\\\"pane\\\" id=\\\"pane-history\\\">\u001b[39m\n\u001b[31m    <div id=\\\"historyList\\\">\u001b[39m\n\u001b[31m      <div class=\\\"empty\\\"><div class=\\\"e-icon\\\">📭</div><p>No dispatch history yet.</p></div>\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m  </div>·\u001b[39m\n\u001b[31m  <!-- ════════════ CONFIG ════════════ -->\u001b[39m\n\u001b[31m  <div class=\\\"pane\\\" id=\\\"pane-config\\\">·\u001b[39m\n\u001b[31m    <!-- SHEETS URL — the only thing you type manually in the PWA -->\u001b[39m\n\u001b[31m    <div class=\\\"card\\\" style=\\\"border-color:var(--acc);border-width:2px;\\\">\u001b[39m\n\u001b[31m      <div class=\\\"card-title\\\"><span class=\\\"ct-dot\\\"></span>Google Sheets — Master Config</div>\u001b[39m\n\u001b[31m      <div class=\\\"info-tag\\\" style=\\\"background:rgba(34,197,94,.07);border-color:rgba(34,197,94,.3);\\\">\u001b[39m\n\u001b[31m        <span>✅</span>\u001b[39m\n\u001b[31m        <span>URL is loaded automatically from <strong>config.json</strong> on the VPS — no manual entry needed. All other settings sync from your <strong>Google Sheet</strong>.</span>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      <div class=\\\"f\\\">\u001b[39m\n\u001b[31m        <label>Apps Script URL <span style=\\\"font-size:.65rem;color:var(--t3);font-weight:400;\\\">(auto-loaded from config.json)</span></label>\u001b[39m\n\u001b[31m        <input type=\\\"url\\\" id=\\\"cfg_sheets_url\\\"\u001b[39m\n\u001b[31m          placeholder=\\\"Auto-loaded from server...\\\"\u001b[39m\n\u001b[31m          oninput=\\\"onSheetsUrlChange()\\\" readonly\u001b[39m\n\u001b[31m          style=\\\"opacity:.7;cursor:default;\\\">\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      <div id=\\\"syncStatus\\\" class=\\\"info-tag\\\" style=\\\"display:none;\\\">\u001b[39m\n\u001b[31m        <span id=\\\"syncIcon\\\">⏳</span>\u001b[39m\n\u001b[31m        <span id=\\\"syncMsg\\\">Syncing from sheet...</span>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      <div class=\\\"btn-row\\\">\u001b[39m\n\u001b[31m        <button class=\\\"btn btn-ghost\\\" onclick=\\\"syncFromSheet()\\\">🔄 Sync Now</button>\u001b[39m\n\u001b[31m        <button class=\\\"btn btn-orange\\\" onclick=\\\"saveSheetsUrl()\\\">💾 Save & Sync</button>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m    </div>·\u001b[39m\n\u001b[31m    <!-- READ-ONLY display of what was loaded from sheet -->\u001b[39m\n\u001b[31m    <div class=\\\"card\\\" id=\\\"sheetConfigDisplay\\\" style=\\\"display:none;\\\">\u001b[39m\n\u001b[31m      <div class=\\\"card-title\\\"><span class=\\\"ct-dot\\\" style=\\\"background:var(--grn);\\\"></span>Config Loaded from Sheet\u001b[39m\n\u001b[31m        <span id=\\\"sheetSyncTime\\\" style=\\\"margin-left:auto;font-size:.62rem;color:var(--t3);font-family:var(--fnt-mono);font-weight:400;text-transform:none;letter-spacing:0;\\\"></span>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      <div id=\\\"sheetConfigRows\\\"></div>\u001b[39m\n\u001b[31m      <div class=\\\"info-tag\\\" style=\\\"margin-top:8px;\\\">\u001b[39m\n\u001b[31m        <span>✏️</span><span>To change any value, edit the <strong>Config sheet</strong> in Google Sheets, then tap Sync Now above.</span>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m    </div>·\u001b[39m\n\u001b[31m    <!-- Courier Master (from sheet) -->\u001b[39m\n\u001b[31m    <div class=\\\"card\\\">\u001b[39m\n\u001b[31m      <div class=\\\"card-title\\\"><span class=\\\"ct-dot\\\" style=\\\"background:var(--ylw);\\\"></span>Courier Master</div>\u001b[39m\n\u001b[31m      <div id=\\\"courierMasterList\\\"></div>\u001b[39m\n\u001b[31m      <button class=\\\"btn btn-ghost\\\" style=\\\"margin-top:8px;font-size:.78rem;\\\" onclick=\\\"openCourierMaster()\\\">✏️ Edit in Sheet / Override</button>\u001b[39m\n\u001b[31m    </div>·\u001b[39m\n\u001b[31m    <button class=\\\"btn\\\" style=\\\"background:var(--red-dim);color:var(--red);border:1px solid var(--red);margin-top:4px;\\\" onclick=\\\"clearAll()\\\">🗑️ Clear All Local Data</button>\u001b[39m\n\u001b[31m    <div style=\\\"height:20px;\\\"></div>\u001b[39m\n\u001b[31m  </div>··\u001b[39m\n\u001b[31m<!-- ════ ADDRESS REVIEW PANE ════ -->\u001b[39m\n\u001b[31m<div id=\\\"addrReviewPane\\\" style=\\\"display:none;position:fixed;inset:0;background:var(--bg);z-index:50;overflow-y:auto;padding:14px;\\\">\u001b[39m\n\u001b[31m  <div class=\\\"verify-hdr\\\">\u001b[39m\n\u001b[31m    <button class=\\\"btn-icon\\\" onclick=\\\"document.getElementById('addrReviewPane').style.display='none'\\\" style=\\\"font-size:1.1rem;\\\">←</button>\u001b[39m\n\u001b[31m    <div class=\\\"verify-title\\\">Review AI-Parsed Addresses</div>\u001b[39m\n\u001b[31m  </div>\u001b[39m\n\u001b[31m  <div style=\\\"background:rgba(56,189,248,.08);border:1px solid rgba(56,189,248,.3);border-radius:var(--r-sm);\u001b[39m\n\u001b[31m    padding:8px 12px;margin-bottom:12px;font-size:.72rem;color:var(--sky);\\\">\u001b[39m\n\u001b[31m    ✏️ Review each row — correct name and city if wrong. Raw address shown below each row for reference.\u001b[39m\n\u001b[31m    Corrections are saved and improve future AI parsing automatically.\u001b[39m\n\u001b[31m  </div>\u001b[39m\n\u001b[31m  <div class=\\\"verify-summary\\\" id=\\\"addrReviewSummary\\\"></div>\u001b[39m\n\u001b[31m  <div id=\\\"addrReviewList\\\"></div>\u001b[39m\n\u001b[31m  <div style=\\\"position:sticky;bottom:0;background:var(--bg);padding-top:10px;\u001b[39m\n\u001b[31m    border-top:1px solid var(--bdr);margin-top:8px;\\\">\u001b[39m\n\u001b[31m    <div style=\\\"display:flex;gap:8px;\\\">\u001b[39m\n\u001b[31m      <button class=\\\"btn btn-ghost\\\" onclick=\\\"backToMapping()\\\" style=\\\"flex:1;\\\">← Back to Mapping</button>\u001b[39m\n\u001b[31m      <button class=\\\"btn btn-orange\\\" onclick=\\\"confirmAddressReview()\\\" style=\\\"flex:2;\\\">✓ Confirm &amp; Use These Addresses</button>\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m    <div style=\\\"font-size:.65rem;color:var(--t3);text-align:center;margin-top:5px;font-family:var(--fnt-mono);\\\">\u001b[39m\n\u001b[31m      Edits you made above are preserved when you go back\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m  </div>\u001b[39m\n\u001b[31m</div>·\u001b[39m\n\u001b[31m<!-- ════ VERIFICATION SCREEN ════ -->\u001b[39m\n\u001b[31m<div id=\\\"verifyPane\\\">\u001b[39m\n\u001b[31m  <div class=\\\"verify-hdr\\\">\u001b[39m\n\u001b[31m    <button class=\\\"btn-icon\\\" onclick=\\\"closeVerify()\\\" style=\\\"font-size:1.1rem;\\\">←</button>\u001b[39m\n\u001b[31m    <div class=\\\"verify-title\\\">Verify Before Finalise</div>\u001b[39m\n\u001b[31m    <button class=\\\"btn-sm\\\" onclick=\\\"approveAll()\\\" style=\\\"white-space:nowrap;\\\">✓ Approve All</button>\u001b[39m\n\u001b[31m  </div>\u001b[39m\n\u001b[31m  <div class=\\\"verify-summary\\\" id=\\\"verifySummary\\\"></div>\u001b[39m\n\u001b[31m  <div id=\\\"verifyList\\\"></div>\u001b[39m\n\u001b[31m  <div style=\\\"position:sticky;bottom:0;background:var(--bg);padding-top:10px;\u001b[39m\n\u001b[31m    border-top:1px solid var(--bdr);margin-top:8px;\\\">\u001b[39m\n\u001b[31m    <div style=\\\"display:flex;gap:8px;flex-wrap:wrap;\\\">\u001b[39m\n\u001b[31m      <button class=\\\"btn btn-ghost\\\" onclick=\\\"closeVerify()\\\" style=\\\"flex:1;\\\">← Back to Edit</button>\u001b[39m\n\u001b[31m      <button class=\\\"btn btn-ghost\\\" onclick=\\\"exportCleanSheet()\\\" style=\\\"flex:1;border-color:var(--sky);color:var(--sky);\\\">\u001b[39m\n\u001b[31m        📥 Export Clean Sheet\u001b[39m\n\u001b[31m      </button>\u001b[39m\n\u001b[31m      <button class=\\\"btn btn-orange\\\" id=\\\"btnConfirmFinalise\\\" onclick=\\\"confirmAndFinalise()\\\" style=\\\"flex:2;\\\">\u001b[39m\n\u001b[31m        ✓ Finalise Dispatch\u001b[39m\n\u001b[31m      </button>\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m  </div>\u001b[39m\n\u001b[31m</div>·\u001b[39m\n\u001b[31m</div><!-- /app -->·\u001b[39m\n\u001b[31m<!-- ════ DISPATCH DETAIL SHEET ════ -->\u001b[39m\n\u001b[31m<div class=\\\"overlay\\\" id=\\\"detailOverlay\\\" onclick=\\\"closeOverlay('detailOverlay')\\\">\u001b[39m\n\u001b[31m  <div class=\\\"sheet\\\" onclick=\\\"event.stopPropagation()\\\">\u001b[39m\n\u001b[31m    <div class=\\\"sheet-handle\\\"></div>\u001b[39m\n\u001b[31m    <div id=\\\"detailContent\\\"></div>\u001b[39m\n\u001b[31m  </div>\u001b[39m\n\u001b[31m</div>·\u001b[39m\n\u001b[31m<!-- ════ COURIER MASTER EDITOR ════ -->\u001b[39m\n\u001b[31m<div class=\\\"overlay\\\" id=\\\"courierMasterOverlay\\\" onclick=\\\"closeOverlay('courierMasterOverlay')\\\">\u001b[39m\n\u001b[31m  <div class=\\\"sheet\\\" onclick=\\\"event.stopPropagation()\\\">\u001b[39m\n\u001b[31m    <div class=\\\"sheet-handle\\\"></div>\u001b[39m\n\u001b[31m    <div class=\\\"sheet-title\\\">✏️ Courier Master</div>\u001b[39m\n\u001b[31m    <div id=\\\"courierMasterEditor\\\"></div>\u001b[39m\n\u001b[31m  </div>\u001b[39m\n\u001b[31m</div>·\u001b[39m\n\u001b[31m<!-- ════ MANUAL TRACKING OVERLAY ════ -->\u001b[39m\n\u001b[31m<div class=\\\"overlay\\\" id=\\\"manualTrackOverlay\\\" onclick=\\\"closeOverlay('manualTrackOverlay')\\\" data-docket=\\\"\\\" data-dispatch-id=\\\"\\\">\u001b[39m\n\u001b[31m  <div class=\\\"sheet\\\" onclick=\\\"event.stopPropagation()\\\">\u001b[39m\n\u001b[31m    <div class=\\\"sheet-handle\\\"></div>\u001b[39m\n\u001b[31m    <div class=\\\"sheet-title\\\">📞 Manual Tracking Log</div>\u001b[39m\n\u001b[31m    <div style=\\\"background:var(--sur2);border:1px solid var(--bdr);border-radius:var(--r-sm);\u001b[39m\n\u001b[31m      padding:8px 12px;margin-bottom:12px;font-family:var(--fnt-mono);font-size:.82rem;color:var(--acc);\\\"\u001b[39m\n\u001b[31m      id=\\\"mtDocketDisplay\\\"></div>\u001b[39m\n\u001b[31m    <div class=\\\"row2\\\">\u001b[39m\n\u001b[31m      <div class=\\\"f\\\">\u001b[39m\n\u001b[31m        <label>Status Reported <span class=\\\"req\\\">*</span></label>\u001b[39m\n\u001b[31m        <select id=\\\"mtStatus\\\">\u001b[39m\n\u001b[31m          <option value=\\\"\\\">Select...</option>\u001b[39m\n\u001b[31m          <option value=\\\"delivered\\\">✅ Delivered</option>\u001b[39m\n\u001b[31m          <option value=\\\"not received\\\">❌ Not Received</option>\u001b[39m\n\u001b[31m          <option value=\\\"in transit\\\">🚚 In Transit / On the way</option>\u001b[39m\n\u001b[31m          <option value=\\\"will deliver tomorrow\\\">📅 Will Deliver Tomorrow</option>\u001b[39m\n\u001b[31m          <option value=\\\"refused\\\">🚫 Refused / Returned</option>\u001b[39m\n\u001b[31m          <option value=\\\"partial\\\">📦 Partial Delivery</option>\u001b[39m\n\u001b[31m        </select>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      <div class=\\\"f\\\">\u001b[39m\n\u001b[31m        <label>Called By (Staff) <span class=\\\"req\\\">*</span></label>\u001b[39m\n\u001b[31m        <input type=\\\"text\\\" id=\\\"mtCalledBy\\\" placeholder=\\\"Your name\\\" list=\\\"staffList\\\">\u001b[39m\n\u001b[31m        <datalist id=\\\"staffList\\\">\u001b[39m\n\u001b[31m          <option>Mukesh</option><option>Sachin</option><option>Shambhu</option>\u001b[39m\n\u001b[31m          <option>Umashankar</option><option>Narender</option><option>Rakesh</option>\u001b[39m\n\u001b[31m          <option>Gurpreet</option><option>Ashok</option>\u001b[39m\n\u001b[31m        </datalist>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m    <div class=\\\"row2\\\">\u001b[39m\n\u001b[31m      <div class=\\\"f\\\">\u001b[39m\n\u001b[31m        <label>Contact Number Used</label>\u001b[39m\n\u001b[31m        <input type=\\\"tel\\\" id=\\\"mtContactUsed\\\" placeholder=\\\"Mobile number called\\\">\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      <div class=\\\"f\\\">\u001b[39m\n\u001b[31m        <label>Person Who Confirmed</label>\u001b[39m\n\u001b[31m        <input type=\\\"text\\\" id=\\\"mtContactPerson\\\" placeholder=\\\"Name of person at store\\\">\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m    <div class=\\\"f\\\">\u001b[39m\n\u001b[31m      <label>Remarks</label>\u001b[39m\n\u001b[31m      <textarea id=\\\"mtRemarks\\\" placeholder=\\\"Any notes, delivery details, issues...\\\" style=\\\"min-height:60px;\\\"></textarea>\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m    <div class=\\\"f\\\">\u001b[39m\n\u001b[31m      <label>Call Date &amp; Time</label>\u001b[39m\n\u001b[31m      <input type=\\\"datetime-local\\\" id=\\\"mtCallTime\\\">\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m    <div class=\\\"btn-row\\\">\u001b[39m\n\u001b[31m      <button class=\\\"btn btn-ghost\\\" onclick=\\\"closeOverlay('manualTrackOverlay')\\\">Cancel</button>\u001b[39m\n\u001b[31m      <button class=\\\"btn btn-orange\\\" onclick=\\\"submitManualTracking()\\\">💾 Log &amp; Save</button>\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m  </div>\u001b[39m\n\u001b[31m</div>·\u001b[39m\n\u001b[31m<!-- ════ POD UPLOAD OVERLAY ════ -->\u001b[39m\n\u001b[31m<div class=\\\"overlay\\\" id=\\\"podOverlay\\\" onclick=\\\"closeOverlay('podOverlay')\\\" data-docket=\\\"\\\" data-dispatch-id=\\\"\\\">\u001b[39m\n\u001b[31m  <div class=\\\"sheet\\\" onclick=\\\"event.stopPropagation()\\\">\u001b[39m\n\u001b[31m    <div class=\\\"sheet-handle\\\"></div>\u001b[39m\n\u001b[31m    <div class=\\\"sheet-title\\\">📸 Upload Proof of Delivery</div>\u001b[39m\n\u001b[31m    <div class=\\\"f\\\">\u001b[39m\n\u001b[31m      <label>POD / Delivery Slip Photo</label>\u001b[39m\n\u001b[31m      <input type=\\\"file\\\" id=\\\"podFileInput\\\" accept=\\\"image/*\\\" capture=\\\"environment\\\"\u001b[39m\n\u001b[31m        onchange=\\\"previewPOD(this,'pod')\\\" style=\\\"font-size:.8rem;color:var(--t2);\\\">\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m    <div class=\\\"f\\\">\u001b[39m\n\u001b[31m      <label>Recipient / Receiver Photo (optional)</label>\u001b[39m\n\u001b[31m      <input type=\\\"file\\\" id=\\\"recipientFileInput\\\" accept=\\\"image/*\\\" capture=\\\"environment\\\"\u001b[39m\n\u001b[31m        onchange=\\\"previewPOD(this,'recip')\\\" style=\\\"font-size:.8rem;color:var(--t2);\\\">\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m    <div id=\\\"podPreview\\\" style=\\\"display:flex;gap:8px;flex-wrap:wrap;margin-bottom:12px;\\\"></div>\u001b[39m\n\u001b[31m    <div class=\\\"btn-row\\\">\u001b[39m\n\u001b[31m      <button class=\\\"btn btn-ghost\\\" onclick=\\\"closeOverlay('podOverlay')\\\">Cancel</button>\u001b[39m\n\u001b[31m      <button class=\\\"btn btn-orange\\\" onclick=\\\"uploadPODFiles()\\\">📤 Upload & Mark Delivered</button>\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m  </div>\u001b[39m\n\u001b[31m</div>·\u001b[39m\n\u001b[31m<!-- ════ ADMIN LOGIN OVERLAY ════ -->\u001b[39m\n\u001b[31m<div class=\\\"overlay\\\" id=\\\"adminLoginOverlay\\\" onclick=\\\"closeOverlay('adminLoginOverlay')\\\">\u001b[39m\n\u001b[31m  <div class=\\\"sheet\\\" onclick=\\\"event.stopPropagation()\\\" style=\\\"max-height:320px;\\\">\u001b[39m\n\u001b[31m    <div class=\\\"sheet-handle\\\"></div>\u001b[39m\n\u001b[31m    <div class=\\\"sheet-title\\\">🔐 Admin Access</div>\u001b[39m\n\u001b[31m    <div style=\\\"font-size:.78rem;color:var(--t2);margin-bottom:14px;\\\">Config settings require admin password.</div>\u001b[39m\n\u001b[31m    <div class=\\\"f\\\">\u001b[39m\n\u001b[31m      <label>Password</label>\u001b[39m\n\u001b[31m      <input type=\\\"password\\\" id=\\\"adminPwdInput\\\" placeholder=\\\"Enter admin password\\\"\u001b[39m\n\u001b[31m        onkeydown=\\\"if(event.key==='Enter')submitAdminLogin()\\\">\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m    <div class=\\\"btn-row\\\" style=\\\"margin-top:4px;\\\">\u001b[39m\n\u001b[31m      <button class=\\\"btn btn-ghost\\\" onclick=\\\"closeOverlay('adminLoginOverlay')\\\">Cancel</button>\u001b[39m\n\u001b[31m      <button class=\\\"btn btn-orange\\\" onclick=\\\"submitAdminLogin()\\\">🔓 Login</button>\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m  </div>\u001b[39m\n\u001b[31m</div>·\u001b[39m\n\u001b[31m<!-- ════ TOAST ════ -->\u001b[39m\n\u001b[31m<div class=\\\"toast\\\" id=\\\"toast\\\"></div>·\u001b[39m\n\u001b[31m<!-- ════ SCRIPTS ════ -->\u001b[39m\n\u001b[31m<!-- SheetJS for Excel/CSV parsing -->\u001b[39m\n\u001b[31m<script src=\\\"https://cdnjs.cloudflare.com/ajax/libs/xlsx/0.18.5/xlsx.full.min.js\\\"></script>·\u001b[39m\n\u001b[31m<script type=\\\"module\\\">\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31m// OCR — Transformers.js\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31mimport { pipeline, env } from 'https://cdn.jsdelivr.net/npm/@huggingface/transformers@3.5.2/dist/transformers.min.js';·\u001b[39m\n\u001b[31m// Cache models in browser\u001b[39m\n\u001b[31menv.allowLocalModels = false;\u001b[39m\n\u001b[31menv.useBrowserCache = true;·\u001b[39m\n\u001b[31mlet ocrPipeline = null;\u001b[39m\n\u001b[31mlet ocrLoading = false;·\u001b[39m\n\u001b[31mwindow.initOCR = async function() {\u001b[39m\n\u001b[31m  if (ocrPipeline || ocrLoading) return;\u001b[39m\n\u001b[31m  ocrLoading = true;\u001b[39m\n\u001b[31m  showAIProgress('Loading OCR model (first time ~120MB)...', 5);\u001b[39m\n\u001b[31m  try {\u001b[39m\n\u001b[31m    ocrPipeline = await pipeline('image-to-text', 'Xenova/trocr-base-printed', {\u001b[39m\n\u001b[31m      progress_callback: (p) => {\u001b[39m\n\u001b[31m        if (p.progress) {\u001b[39m\n\u001b[31m          showAIProgress(`Loading: ${p.file || 'model'} (${Math.round(p.progress)}%)`, p.progress);\u001b[39m\n\u001b[31m        }\u001b[39m\n\u001b[31m      }\u001b[39m\n\u001b[31m    });\u001b[39m\n\u001b[31m    showAIProgress('OCR model ready ✓', 100);\u001b[39m\n\u001b[31m    setTimeout(() => hideAIProgress(), 1500);\u001b[39m\n\u001b[31m    showToast('OCR ready ✓', 'ok');\u001b[39m\n\u001b[31m  } catch(e) {\u001b[39m\n\u001b[31m    console.warn('Transformers.js OCR failed:', e);\u001b[39m\n\u001b[31m    hideAIProgress();\u001b[39m\n\u001b[31m    showToast('OCR model load failed — will use Ollama fallback', 'info');\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m  ocrLoading = false;\u001b[39m\n\u001b[31m};·\u001b[39m\n\u001b[31mwindow.runOCROnImage = async function(imageData, filename) {\u001b[39m\n\u001b[31m  const cfg = window.appConfig;·\u001b[39m\n\u001b[31m  // Strategy 1: Transformers.js\u001b[39m\n\u001b[31m  if ((cfg.ocr_mode === 'transformers' || cfg.ocr_mode === 'both') && ocrPipeline) {\u001b[39m\n\u001b[31m    try {\u001b[39m\n\u001b[31m      const result = await ocrPipeline(imageData);\u001b[39m\n\u001b[31m      const text = result[0]?.generated_text || '';\u001b[39m\n\u001b[31m      return extractDocketFromText(text, 'transformers');\u001b[39m\n\u001b[31m    } catch(e) { /* fall through */ }\u001b[39m\n\u001b[31m  }·\u001b[39m\n\u001b[31m  // Strategy 2: Dispatch AI proxy → Ollama LLaVA (server-side, avoids CORS + port issues)\u001b[39m\n\u001b[31m  if (cfg.ocr_mode === 'ollama' || cfg.ocr_mode === 'both') {\u001b[39m\n\u001b[31m    try {\u001b[39m\n\u001b[31m      const resp = await fetch('/dispatch-ai/ocr', {\u001b[39m\n\u001b[31m        method: 'POST',\u001b[39m\n\u001b[31m        headers: { 'Content-Type': 'application/json' },\u001b[39m\n\u001b[31m        body: JSON.stringify({ imageBase64: imageData }),\u001b[39m\n\u001b[31m        signal: AbortSignal.timeout(180000)\u001b[39m\n\u001b[31m      });\u001b[39m\n\u001b[31m      const data = await resp.json();\u001b[39m\n\u001b[31m      if (data.success && data.docket) {\u001b[39m\n\u001b[31m        return { docket: data.docket, confidence: data.confidence, source: 'ollama' };\u001b[39m\n\u001b[31m      }\u001b[39m\n\u001b[31m    } catch(e) { /* fall through */ }\u001b[39m\n\u001b[31m  }·\u001b[39m\n\u001b[31m  return { docket: '', confidence: 0, source: 'failed' };\u001b[39m\n\u001b[31m};·\u001b[39m\n\u001b[31mfunction extractDocketFromText(text, source) {\u001b[39m\n\u001b[31m  // Pattern: typical courier docket = 8-15 digit numbers, sometimes with letters\u001b[39m\n\u001b[31m  const patterns = [\u001b[39m\n\u001b[31m    /\\\\b([A-Z]{0,3}\\\\d{8,15}[A-Z]{0,3})\\\\b/g,   // alphanumeric tracking\u001b[39m\n\u001b[31m    /\\\\b(\\\\d{9,15})\\\\b/g,                          // pure numeric\u001b[39m\n\u001b[31m    /\\\\b([A-Z]{2,4}\\\\d{6,12})\\\\b/g,               // letter prefix\u001b[39m\n\u001b[31m  ];\u001b[39m\n\u001b[31m  const candidates = [];\u001b[39m\n\u001b[31m  for (const pat of patterns) {\u001b[39m\n\u001b[31m    let m;\u001b[39m\n\u001b[31m    while ((m = pat.exec(text)) !== null) {\u001b[39m\n\u001b[31m      if (!candidates.includes(m[1])) candidates.push(m[1]);\u001b[39m\n\u001b[31m    }\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m  if (candidates.length === 0) return { docket: text.trim().slice(0, 20), confidence: 0.3, source };\u001b[39m\n\u001b[31m  // Pick longest candidate (usually most complete docket)\u001b[39m\n\u001b[31m  const best = candidates.sort((a, b) => b.length - a.length)[0];\u001b[39m\n\u001b[31m  const confidence = candidates.length === 1 ? 0.85 : 0.7;\u001b[39m\n\u001b[31m  return { docket: best, confidence, source };\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// Expose for non-module code\u001b[39m\n\u001b[31mwindow._ocrReady = () => ocrPipeline !== null;\u001b[39m\n\u001b[31m</script>·\u001b[39m\n\u001b[31m<script>\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31m// STATE & STORAGE\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31mlet dispatches = [];\u001b[39m\n\u001b[31mlet stores = [];\u001b[39m\n\u001b[31mlet slipOCRData = [];\u001b[39m\n\u001b[31mlet dashFilter = 'all';·\u001b[39m\n\u001b[31m// Only sheets_url is persisted locally — all other config comes from the sheet\u001b[39m\n\u001b[31mlet _trackingInProgress = false;\u001b[39m\n\u001b[31mwindow.appConfig = {\u001b[39m\n\u001b[31m  sheets_url:      'https://script.google.com/macros/s/AKfycbys8HrToDv2u_dRvSFnNqmO9bnyAO1s8oSprUydpVQnNQutq4tuUY71cVCH24urbvM9/exec',\u001b[39m\n\u001b[31m  webhook_url:     '',\u001b[39m\n\u001b[31m  proxy_url:       '',\u001b[39m\n\u001b[31m  ollama_url:      'http://srv1111289.hstgr.cloud:11434',\u001b[39m\n\u001b[31m  ocr_mode:        'both',\u001b[39m\n\u001b[31m  company:         '360 Degree Logical Marketing',\u001b[39m\n\u001b[31m  city:            'Chandigarh',\u001b[39m\n\u001b[31m  auto_log:        'true',\u001b[39m\n\u001b[31m  refresh_on_load: 'false',\u001b[39m\n\u001b[31m};·\u001b[39m\n\u001b[31mconst COURIER_MASTER = {\u001b[39m\n\u001b[31m  'SkyKing': {\u001b[39m\n\u001b[31m    trackUrl: 'https://skyking.co/track?cno={docket}',\u001b[39m\n\u001b[31m    trackBulkUrl: 'https://skyking.co/track',\u001b[39m\n\u001b[31m    apiProxy: '/track-proxy/skyking?cno={docket}',\u001b[39m\n\u001b[31m    maxBulk: 10\u001b[39m\n\u001b[31m  },\u001b[39m\n\u001b[31m  'DTDC': {\u001b[39m\n\u001b[31m    trackUrl: 'https://www.dtdc.in/tracking.asp',\u001b[39m\n\u001b[31m    queryParam: 'trkId',\u001b[39m\n\u001b[31m    maxBulk: 1\u001b[39m\n\u001b[31m  },\u001b[39m\n\u001b[31m  'Delhivery': {\u001b[39m\n\u001b[31m    trackUrl: 'https://www.delhivery.com/track/package/{docket}',\u001b[39m\n\u001b[31m    maxBulk: 1\u001b[39m\n\u001b[31m  },\u001b[39m\n\u001b[31m  'Blue Dart': {\u001b[39m\n\u001b[31m    trackUrl: 'https://www.bluedart.com/tracking',\u001b[39m\n\u001b[31m    maxBulk: 1\u001b[39m\n\u001b[31m  },\u001b[39m\n\u001b[31m  'FedEx': {\u001b[39m\n\u001b[31m    trackUrl: 'https://www.fedex.com/fedextrack/?trknbr={docket}',\u001b[39m\n\u001b[31m    maxBulk: 10\u001b[39m\n\u001b[31m  },\u001b[39m\n\u001b[31m  'Ecom Express': {\u001b[39m\n\u001b[31m    trackUrl: 'https://ecomexpress.in/tracking/?awb_field={docket}',\u001b[39m\n\u001b[31m    maxBulk: 1\u001b[39m\n\u001b[31m  },\u001b[39m\n\u001b[31m  'India Post': {\u001b[39m\n\u001b[31m    trackUrl: 'https://www.indiapost.gov.in/VAS/Pages/trackconsignment.aspx',\u001b[39m\n\u001b[31m    maxBulk: 1\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m};·\u001b[39m\n\u001b[31mfunction loadData() {\u001b[39m\n\u001b[31m  try {\u001b[39m\n\u001b[31m    dispatches = JSON.parse(localStorage.getItem('360lm_disp_v2') || '[]');\u001b[39m\n\u001b[31m    const saved = JSON.parse(localStorage.getItem('360lm_disp_sheets_url') || '{}');\u001b[39m\n\u001b[31m    if (saved.sheets_url) window.appConfig.sheets_url = saved.sheets_url;\u001b[39m\n\u001b[31m  } catch(e) {}\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31mfunction saveData() {\u001b[39m\n\u001b[31m  try {\u001b[39m\n\u001b[31m    localStorage.setItem('360lm_disp_v2', JSON.stringify(dispatches));\u001b[39m\n\u001b[31m  } catch(e) {\u001b[39m\n\u001b[31m    if (e.name === 'QuotaExceededError' || e.code === 22) {\u001b[39m\n\u001b[31m      showToast('Storage full — old dispatches cleared. Data is safe in Postgres.', 'err');\u001b[39m\n\u001b[31m      // Drop delivered dispatches older than 30 days to free space\u001b[39m\n\u001b[31m      const cutoff = Date.now() - 30 * 24 * 3600 * 1000;\u001b[39m\n\u001b[31m      dispatches = dispatches.filter(d =>\u001b[39m\n\u001b[31m        d.status !== 'draft' && (\u001b[39m\n\u001b[31m          d.status !== 'active' ||\u001b[39m\n\u001b[31m          new Date(d.createdAt || 0).getTime() > cutoff\u001b[39m\n\u001b[31m        ) ? true :\u001b[39m\n\u001b[31m        d.status === 'draft' || new Date(d.createdAt || 0).getTime() > cutoff\u001b[39m\n\u001b[31m      );\u001b[39m\n\u001b[31m      try { localStorage.setItem('360lm_disp_v2', JSON.stringify(dispatches)); } catch(_) {}\u001b[39m\n\u001b[31m    }\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// ── Postgres DB sync ──────────────────────────────────────────────────────\u001b[39m\n\u001b[31mconst _DB        = '/db';\u001b[39m\n\u001b[31m// PostgREST v12: Accept-Profile for reads, Content-Profile for writes\u001b[39m\n\u001b[31mconst _DHDR_READ  = { 'Accept-Profile': 'dispatch' };\u001b[39m\n\u001b[31mconst _DHDR_WRITE = { 'Content-Type': 'application/json', 'Content-Profile': 'dispatch', 'Accept-Profile': 'dispatch' };\u001b[39m\n\u001b[31mconst _DHDR_UPSERT = { ..._DHDR_WRITE, 'Prefer': 'resolution=merge-duplicates,return=minimal' };·\u001b[39m\n\u001b[31mfunction _dispRow(d) {\u001b[39m\n\u001b[31m  return {\u001b[39m\n\u001b[31m    dispatch_id:    d.id,\u001b[39m\n\u001b[31m    client:         d.client    || '',\u001b[39m\n\u001b[31m    job_ref:        d.jobref    || null,\u001b[39m\n\u001b[31m    courier:        d.courier   || '',\u001b[39m\n\u001b[31m    dispatch_date:  d.dispatchDate || null,\u001b[39m\n\u001b[31m    status:         d.status    || 'active',\u001b[39m\n\u001b[31m    total_count:    (d.consignees || []).length,\u001b[39m\n\u001b[31m    docketed_count: (d.consignees || []).filter(c => c.docket).length,\u001b[39m\n\u001b[31m    remarks:        d.remarks   || null,\u001b[39m\n\u001b[31m    created_by:     window._hubUser?.id || 'unknown',\u001b[39m\n\u001b[31m  };\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction _consRow(c, dispatchId, idx) {\u001b[39m\n\u001b[31m  return {\u001b[39m\n\u001b[31m    cons_id:            `${dispatchId}-${idx}`,\u001b[39m\n\u001b[31m    dispatch_id:        dispatchId,\u001b[39m\n\u001b[31m    seq_no:             idx,\u001b[39m\n\u001b[31m    name:               c.name          || '',\u001b[39m\n\u001b[31m    city:               c.city          || null,\u001b[39m\n\u001b[31m    address:            c.address       || null,\u001b[39m\n\u001b[31m    phone:              c.phone         || null,\u001b[39m\n\u001b[31m    pincode:            c.pincode       || null,\u001b[39m\n\u001b[31m    material:           c.material      || null,\u001b[39m\n\u001b[31m    qty:                c.qty           || null,\u001b[39m\n\u001b[31m    docket:             c.docket        || null,\u001b[39m\n\u001b[31m    status:             c.status        || 'pending',\u001b[39m\n\u001b[31m    last_event:         c.lastEvent     || null,\u001b[39m\n\u001b[31m    last_scan_location: c.lastScanLocation || null,\u001b[39m\n\u001b[31m    last_tracked_at:    c.lastTrackedAt || null,\u001b[39m\n\u001b[31m    manual_status:      c.manualStatus  || null,\u001b[39m\n\u001b[31m    manual_delivered:   !!c.manualDelivered,\u001b[39m\n\u001b[31m    manual_called_by:   c.manualCalledBy || null,\u001b[39m\n\u001b[31m    manual_remarks:     c.manualRemarks || null,\u001b[39m\n\u001b[31m    pod_uploaded:       !!c.podUploaded,\u001b[39m\n\u001b[31m  };\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31masync function syncDispatchToDB(d) {\u001b[39m\n\u001b[31m  if (!d || d.status === 'draft') return; // never sync drafts\u001b[39m\n\u001b[31m  try {\u001b[39m\n\u001b[31m    await fetch(`${_DB}/dispatches`, {\u001b[39m\n\u001b[31m      method: 'POST', headers: _DHDR_UPSERT,\u001b[39m\n\u001b[31m      body: JSON.stringify(_dispRow(d)),\u001b[39m\n\u001b[31m    });\u001b[39m\n\u001b[31m    const rows = (d.consignees || []).map((c, i) => _consRow(c, d.id, i));\u001b[39m\n\u001b[31m    if (rows.length) {\u001b[39m\n\u001b[31m      await fetch(`${_DB}/consignees`, {\u001b[39m\n\u001b[31m        method: 'POST', headers: _DHDR_UPSERT,\u001b[39m\n\u001b[31m        body: JSON.stringify(rows),\u001b[39m\n\u001b[31m      });\u001b[39m\n\u001b[31m    }\u001b[39m\n\u001b[31m  } catch(e) { console.warn('[dispatch-db] sync error:', e.message); }\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31masync function syncConsigneeToDB(c, dispatchId, idx) {\u001b[39m\n\u001b[31m  try {\u001b[39m\n\u001b[31m    await fetch(`${_DB}/consignees`, {\u001b[39m\n\u001b[31m      method: 'POST', headers: _DHDR_UPSERT,\u001b[39m\n\u001b[31m      body: JSON.stringify(_consRow(c, dispatchId, idx)),\u001b[39m\n\u001b[31m    });\u001b[39m\n\u001b[31m    // Log tracking event\u001b[39m\n\u001b[31m    if (c.lastEvent && c.status) {\u001b[39m\n\u001b[31m      fetch(`${_DB}/tracking_events`, {\u001b[39m\n\u001b[31m        method: 'POST', headers: _DHDR_WRITE,\u001b[39m\n\u001b[31m        body: JSON.stringify({\u001b[39m\n\u001b[31m          docket: c.docket, dispatch_id: dispatchId,\u001b[39m\n\u001b[31m          cons_id: `${dispatchId}-${idx}`,\u001b[39m\n\u001b[31m          status: c.status, event_text: c.lastEvent,\u001b[39m\n\u001b[31m          scan_location: c.lastScanLocation || null,\u001b[39m\n\u001b[31m        }),\u001b[39m\n\u001b[31m      }).catch(() => {});\u001b[39m\n\u001b[31m    }\u001b[39m\n\u001b[31m  } catch(e) { console.warn('[dispatch-db] consignee sync error:', e.message); }\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31masync function loadFromDB() {\u001b[39m\n\u001b[31m  try {\u001b[39m\n\u001b[31m    const [dResp, cResp] = await Promise.all([\u001b[39m\n\u001b[31m      fetch(`${_DB}/dispatches?order=created_at.desc&limit=100`, { headers: _DHDR_READ }),\u001b[39m\n\u001b[31m      fetch(`${_DB}/consignees?order=dispatch_id,seq_no`, { headers: _DHDR_READ }),\u001b[39m\n\u001b[31m    ]);\u001b[39m\n\u001b[31m    if (!dResp.ok) return;\u001b[39m\n\u001b[31m    const dbDispatches = await dResp.json();\u001b[39m\n\u001b[31m    const dbConsignees = cResp.ok ? await cResp.json() : [];\u001b[39m\n\u001b[31m    if (!dbDispatches.length) return;·\u001b[39m\n\u001b[31m    const dbIds = new Set(dbDispatches.map(d => d.dispatch_id));\u001b[39m\n\u001b[31m    const localOnly = dispatches.filter(d => !dbIds.has(d.id));·\u001b[39m\n\u001b[31m    const fromDB = dbDispatches.map(d => {\u001b[39m\n\u001b[31m      const cons = dbConsignees.filter(c => c.dispatch_id === d.dispatch_id);\u001b[39m\n\u001b[31m      return {\u001b[39m\n\u001b[31m        id:           d.dispatch_id,\u001b[39m\n\u001b[31m        client:       d.client,\u001b[39m\n\u001b[31m        jobref:       d.job_ref,\u001b[39m\n\u001b[31m        courier:      d.courier,\u001b[39m\n\u001b[31m        dispatchDate: d.dispatch_date,\u001b[39m\n\u001b[31m        status:       d.status,\u001b[39m\n\u001b[31m        remarks:      d.remarks,\u001b[39m\n\u001b[31m        createdAt:    d.created_at,\u001b[39m\n\u001b[31m        consignees:   cons.map(c => ({\u001b[39m\n\u001b[31m          name:              c.name,\u001b[39m\n\u001b[31m          city:              c.city,\u001b[39m\n\u001b[31m          address:           c.address,\u001b[39m\n\u001b[31m          phone:             c.phone,\u001b[39m\n\u001b[31m          pincode:           c.pincode,\u001b[39m\n\u001b[31m          material:          c.material,\u001b[39m\n\u001b[31m          qty:               c.qty,\u001b[39m\n\u001b[31m          docket:            c.docket,\u001b[39m\n\u001b[31m          status:            c.status,\u001b[39m\n\u001b[31m          lastEvent:         c.last_event,\u001b[39m\n\u001b[31m          lastScanLocation:  c.last_scan_location,\u001b[39m\n\u001b[31m          lastTrackedAt:     c.last_tracked_at,\u001b[39m\n\u001b[31m          manualStatus:      c.manual_status,\u001b[39m\n\u001b[31m          manualDelivered:   c.manual_delivered,\u001b[39m\n\u001b[31m          manualCalledBy:    c.manual_called_by,\u001b[39m\n\u001b[31m          manualRemarks:     c.manual_remarks,\u001b[39m\n\u001b[31m          podUploaded:       c.pod_uploaded,\u001b[39m\n\u001b[31m          _dbIdx:            c.seq_no,\u001b[39m\n\u001b[31m        })),\u001b[39m\n\u001b[31m      };\u001b[39m\n\u001b[31m    });·\u001b[39m\n\u001b[31m    dispatches = [...fromDB, ...localOnly];\u001b[39m\n\u001b[31m    saveData();\u001b[39m\n\u001b[31m    renderDashboard();\u001b[39m\n\u001b[31m    updateHistCount();\u001b[39m\n\u001b[31m  } catch(e) { console.warn('[dispatch-db] load error:', e.message); }\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31mfunction saveSheetsUrlLocal() {\u001b[39m\n\u001b[31m  localStorage.setItem('360lm_disp_sheets_url', JSON.stringify({ sheets_url: window.appConfig.sheets_url }));\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m/**\u001b[39m\n\u001b[31m * Fetch config.json from the VPS server (same origin).\u001b[39m\n\u001b[31m * This file contains the sheets_url — no manual entry needed.\u001b[39m\n\u001b[31m * Falls back to localStorage cache if fetch fails (offline).\u001b[39m\n\u001b[31m */\u001b[39m\n\u001b[31masync function loadServerConfig() {\u001b[39m\n\u001b[31m  try {\u001b[39m\n\u001b[31m    const resp = await fetch('./config.json', { cache: 'no-store' });\u001b[39m\n\u001b[31m    if (resp.ok) {\u001b[39m\n\u001b[31m      const cfg = await resp.json();\u001b[39m\n\u001b[31m      if (cfg.sheets_url) {\u001b[39m\n\u001b[31m        window.appConfig.sheets_url = cfg.sheets_url;\u001b[39m\n\u001b[31m        saveSheetsUrlLocal(); // cache it for offline use\u001b[39m\n\u001b[31m        document.getElementById('cfg_sheets_url').value = cfg.sheets_url;\u001b[39m\n\u001b[31m        return true;\u001b[39m\n\u001b[31m      }\u001b[39m\n\u001b[31m    }\u001b[39m\n\u001b[31m  } catch(e) {\u001b[39m\n\u001b[31m    console.log('config.json fetch failed, using cached URL:', e.message);\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m  return false;\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31m// INIT\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31m// ── Hub auth gate ─────────────────────────────────────────────────────────\u001b[39m\n\u001b[31mwindow._hubUser = null;\u001b[39m\n\u001b[31m(function() {\u001b[39m\n\u001b[31m  try {\u001b[39m\n\u001b[31m    const hubLogin = '/hub/?next=' + encodeURIComponent(location.pathname + location.search);\u001b[39m\n\u001b[31m    const s = JSON.parse(localStorage.getItem('lm360-session') || 'null');\u001b[39m\n\u001b[31m    if (!s || !s.empId || !s.name) { window.location.replace(hubLogin); return; }\u001b[39m\n\u001b[31m    if (Date.now() - new Date(s.loginAt).getTime() > 12 * 3600 * 1000) {\u001b[39m\n\u001b[31m      window.location.replace(hubLogin); return;\u001b[39m\n\u001b[31m    }\u001b[39m\n\u001b[31m    window._hubUser = { id: s.empId, name: s.name, role: s.role };\u001b[39m\n\u001b[31m  } catch(_) { window.location.replace('/hub/?next=' + encodeURIComponent(location.pathname + location.search)); }\u001b[39m\n\u001b[31m})();·\u001b[39m\n\u001b[31mwindow.addEventListener('load', async () => {\u001b[39m\n\u001b[31m  if (!window._hubUser) return; // should never reach here — gate redirected already\u001b[39m\n\u001b[31m  setTimeout(() => {\u001b[39m\n\u001b[31m    document.getElementById('splash').style.opacity = '0';\u001b[39m\n\u001b[31m    setTimeout(() => {\u001b[39m\n\u001b[31m      document.getElementById('splash').style.display = 'none';\u001b[39m\n\u001b[31m      document.getElementById('app').style.display = 'flex';\u001b[39m\n\u001b[31m    }, 450);\u001b[39m\n\u001b[31m  }, 1400);\u001b[39m\n\u001b[31m  loadData();\u001b[39m\n\u001b[31m  // Auto-authenticate config tab for admin users (no password needed)\u001b[39m\n\u001b[31m  if (['harish', 'pramod'].includes(window._hubUser?.id)) _adminAuthed = true;\u001b[39m\n\u001b[31m  setToday();\u001b[39m\n\u001b[31m  addConsignee();\u001b[39m\n\u001b[31m  updateHistCount();\u001b[39m\n\u001b[31m  setInterval(updateNetDot, 5000);\u001b[39m\n\u001b[31m  updateNetDot();\u001b[39m\n\u001b[31m  checkShareTarget();\u001b[39m\n\u001b[31m  setupDragDrop();·\u001b[39m\n\u001b[31m  // Step 1: fetch sheets_url from VPS config.json (auto, no user input)\u001b[39m\n\u001b[31m  await loadServerConfig();·\u001b[39m\n\u001b[31m  // Step 2: populate config tab field\u001b[39m\n\u001b[31m  document.getElementById('cfg_sheets_url').value = window.appConfig.sheets_url || '';·\u001b[39m\n\u001b[31m  // Step 3: load dispatches from Postgres in background (merge with local)\u001b[39m\n\u001b[31m  loadFromDB(); // non-blocking — updates UI when data arrives·\u001b[39m\n\u001b[31m  // Step 4: sync full config from Google Sheet silently\u001b[39m\n\u001b[31m  if (window.appConfig.sheets_url) {\u001b[39m\n\u001b[31m    syncFromSheet(true); // silent on startup\u001b[39m\n\u001b[31m  } else {\u001b[39m\n\u001b[31m    // Only show config tab if no URL found anywhere\u001b[39m\n\u001b[31m    showPane('config');\u001b[39m\n\u001b[31m    showToast('No config.json found on server', 'err');\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m  // Pre-warm OCR after delay\u001b[39m\n\u001b[31m  setTimeout(() => { if (typeof window.initOCR === 'function') window.initOCR(); }, 4000);\u001b[39m\n\u001b[31m});·\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31m// WEB SHARE TARGET\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31masync function checkShareTarget() {\u001b[39m\n\u001b[31m  // Check URL params for share target\u001b[39m\n\u001b[31m  const params = new URLSearchParams(window.location.search);\u001b[39m\n\u001b[31m  const action = params.get('action');\u001b[39m\n\u001b[31m  if (action !== 'share') return;·\u001b[39m\n\u001b[31m  // Check for shared files via Share Target API\u001b[39m\n\u001b[31m  if ('launchQueue' in window) {\u001b[39m\n\u001b[31m    window.launchQueue.setConsumer(async launchParams => {\u001b[39m\n\u001b[31m      if (!launchParams.files || launchParams.files.length === 0) return;\u001b[39m\n\u001b[31m      const files = await Promise.all(launchParams.files.map(f => f.getFile()));\u001b[39m\n\u001b[31m      const imageFiles = files.filter(f => f.type.startsWith('image/'));\u001b[39m\n\u001b[31m      if (imageFiles.length > 0) {\u001b[39m\n\u001b[31m        document.getElementById('shareTargetInfo').style.display = 'flex';\u001b[39m\n\u001b[31m        document.getElementById('sharedCount').textContent = imageFiles.length;\u001b[39m\n\u001b[31m        document.getElementById('shareBanner').classList.add('show');\u001b[39m\n\u001b[31m        setTimeout(() => document.getElementById('shareBanner').classList.remove('show'), 4000);\u001b[39m\n\u001b[31m        showPane('new');\u001b[39m\n\u001b[31m        await handleSlipFiles(imageFiles);\u001b[39m\n\u001b[31m      }\u001b[39m\n\u001b[31m    });\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction dismissShare() {\u001b[39m\n\u001b[31m  document.getElementById('shareBanner').classList.remove('show');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31m// DRAG & DROP\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31mfunction setupDragDrop() {\u001b[39m\n\u001b[31m  const zone = document.getElementById('slipZone');\u001b[39m\n\u001b[31m  zone.addEventListener('dragover', e => { e.preventDefault(); zone.classList.add('drag'); });\u001b[39m\n\u001b[31m  zone.addEventListener('dragleave', () => zone.classList.remove('drag'));\u001b[39m\n\u001b[31m  zone.addEventListener('drop', e => {\u001b[39m\n\u001b[31m    e.preventDefault();\u001b[39m\n\u001b[31m    zone.classList.remove('drag');\u001b[39m\n\u001b[31m    const files = Array.from(e.dataTransfer.files).filter(f => f.type.startsWith('image/'));\u001b[39m\n\u001b[31m    if (files.length) handleSlipFiles(files);\u001b[39m\n\u001b[31m  });\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31m// SLIP OCR HANDLER\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31masync function handleSlipFiles(files) {\u001b[39m\n\u001b[31m  const fileArr = Array.from(files);\u001b[39m\n\u001b[31m  if (!fileArr.length) return;\u001b[39m\n\u001b[31m  // If dockets already mapped from store list — allow upload but skip OCR extraction\u001b[39m\n\u001b[31m  const skipOCR = shouldSkipOCR();\u001b[39m\n\u001b[31m  if (skipOCR) {\u001b[39m\n\u001b[31m    showToast('Dockets already mapped — slips saved for reference only', 'info');\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m  showOCRWorking(true, `Processing ${fileArr.length} slip(s)...`);·\u001b[39m\n\u001b[31m  for (let i = 0; i < fileArr.length; i++) {\u001b[39m\n\u001b[31m    const file = fileArr[i];\u001b[39m\n\u001b[31m    showOCRWorking(true, `OCR: ${i+1}/${fileArr.length} — ${file.name}`);\u001b[39m\n\u001b[31m    const imageData = await readFileAsDataURL(file);\u001b[39m\n\u001b[31m    let result = { docket: '', confidence: 0, source: 'manual' };\u001b[39m\n\u001b[31m    if (!skipOCR) {\u001b[39m\n\u001b[31m      try {\u001b[39m\n\u001b[31m        result = await window.runOCROnImage(imageData, file.name);\u001b[39m\n\u001b[31m      } catch(e) {}\u001b[39m\n\u001b[31m    } else {\u001b[39m\n\u001b[31m      result = { docket: '', confidence: 1, source: 'skipped — docket pre-mapped' };\u001b[39m\n\u001b[31m    }\u001b[39m\n\u001b[31m    slipOCRData.push({ file, imageData, filename: file.name, ...result });\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m  showOCRWorking(false);\u001b[39m\n\u001b[31m  renderOCRResults();\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction readFileAsDataURL(file) {\u001b[39m\n\u001b[31m  return new Promise((res, rej) => {\u001b[39m\n\u001b[31m    const r = new FileReader();\u001b[39m\n\u001b[31m    r.onload = e => res(e.target.result);\u001b[39m\n\u001b[31m    r.onerror = rej;\u001b[39m\n\u001b[31m    r.readAsDataURL(file);\u001b[39m\n\u001b[31m  });\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction renderOCRResults() {\u001b[39m\n\u001b[31m  if (!slipOCRData.length) return;\u001b[39m\n\u001b[31m  const list = document.getElementById('ocrSlipList');\u001b[39m\n\u001b[31m  document.getElementById('ocrResults').classList.add('visible');·\u001b[39m\n\u001b[31m  const needReview = slipOCRData.filter(s => s.confidence < 0.65 && s.docket).length;\u001b[39m\n\u001b[31m  const autoOk     = slipOCRData.filter(s => s.confidence >= 0.88).length;\u001b[39m\n\u001b[31m  const summary    = document.getElementById('ocrSummary');\u001b[39m\n\u001b[31m  if (summary) {\u001b[39m\n\u001b[31m    summary.style.display = '';\u001b[39m\n\u001b[31m    summary.innerHTML =\u001b[39m\n\u001b[31m      `<span style=\\\"color:var(--grn)\\\">✓ ${autoOk} auto-accepted</span>` +\u001b[39m\n\u001b[31m      (needReview ? `&nbsp;·&nbsp;<span style=\\\"color:var(--ylw)\\\">⚠ ${needReview} need manual check</span>` : '');\u001b[39m\n\u001b[31m  }·\u001b[39m\n\u001b[31m  list.innerHTML = slipOCRData.map((s, i) => {\u001b[39m\n\u001b[31m    const autoAccepted = s.confidence >= 0.88 && s.docket;\u001b[39m\n\u001b[31m    const needsCheck   = s.confidence < 0.65  && s.docket;\u001b[39m\n\u001b[31m    const confClass = autoAccepted ? 'conf-high' : needsCheck ? 'conf-low' : 'conf-med';\u001b[39m\n\u001b[31m    const confLabel = autoAccepted\u001b[39m\n\u001b[31m      ? `✓ ${Math.round(s.confidence * 100)}% — auto`\u001b[39m\n\u001b[31m      : needsCheck\u001b[39m\n\u001b[31m        ? `⚠ ${Math.round(s.confidence * 100)}% — verify`\u001b[39m\n\u001b[31m        : `${Math.round(s.confidence * 100)}%`;\u001b[39m\n\u001b[31m    const borderStyle = needsCheck ? 'border:1px solid var(--ylw);' : '';\u001b[39m\n\u001b[31m    return `\u001b[39m\n\u001b[31m    <div class=\\\"ocr-slip\\\" id=\\\"ocrSlip_${i}\\\" style=\\\"${borderStyle}\\\">\u001b[39m\n\u001b[31m      <img src=\\\"${s.imageData}\\\" alt=\\\"slip ${i+1}\\\">\u001b[39m\n\u001b[31m      <div class=\\\"ocr-slip-body\\\">\u001b[39m\n\u001b[31m        <div class=\\\"ocr-slip-name\\\">${s.filename}</div>\u001b[39m\n\u001b[31m        <input type=\\\"text\\\" class=\\\"ocr-slip-docket\\\" id=\\\"ocrDocket_${i}\\\"\u001b[39m\n\u001b[31m          value=\\\"${s.docket}\\\" placeholder=\\\"Docket number\\\"\u001b[39m\n\u001b[31m          ${autoAccepted ? 'style=\\\"opacity:.6\\\"' : ''}\u001b[39m\n\u001b[31m          oninput=\\\"slipOCRData[${i}].docket=this.value;slipOCRData[${i}].confidence=0.5\\\">\u001b[39m\n\u001b[31m        <div class=\\\"ocr-slip-status\\\">\u001b[39m\n\u001b[31m          <span class=\\\"ocr-confidence ${confClass}\\\">${confLabel}</span>\u001b[39m\n\u001b[31m          <span style=\\\"color:var(--t4);font-family:var(--fnt-mono);font-size:.6rem;\\\">via ${s.source}</span>\u001b[39m\n\u001b[31m          <button class=\\\"ocr-ai-btn\\\" onclick=\\\"reOCR(${i})\\\">🔄 Re-OCR</button>\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m    </div>`;\u001b[39m\n\u001b[31m  }).join('');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31masync function reOCR(i) {\u001b[39m\n\u001b[31m  const s = slipOCRData[i];\u001b[39m\n\u001b[31m  showOCRWorking(true, `Re-running OCR on ${s.filename}...`);\u001b[39m\n\u001b[31m  try {\u001b[39m\n\u001b[31m    const result = await window.runOCROnImage(s.imageData, s.filename);\u001b[39m\n\u001b[31m    slipOCRData[i] = { ...s, ...result };\u001b[39m\n\u001b[31m    renderOCRResults();\u001b[39m\n\u001b[31m    showToast('Re-OCR complete', 'ok');\u001b[39m\n\u001b[31m  } catch(e) {\u001b[39m\n\u001b[31m    showToast('OCR failed', 'err');\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m  showOCRWorking(false);\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction clearSlips() {\u001b[39m\n\u001b[31m  slipOCRData = [];\u001b[39m\n\u001b[31m  document.getElementById('ocrResults').classList.remove('visible');\u001b[39m\n\u001b[31m  document.getElementById('ocrSlipList').innerHTML = '';\u001b[39m\n\u001b[31m  const sum = document.getElementById('ocrSummary');\u001b[39m\n\u001b[31m  if (sum) { sum.style.display = 'none'; sum.innerHTML = ''; }\u001b[39m\n\u001b[31m  document.getElementById('shareTargetInfo').style.display = 'none';\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction applyAllDockets() {\u001b[39m\n\u001b[31m  // Match each OCR result to a consignee by order or by name matching\u001b[39m\n\u001b[31m  const consList = document.getElementById('consList');\u001b[39m\n\u001b[31m  const cards = consList.querySelectorAll('.cons-card');\u001b[39m\n\u001b[31m  slipOCRData.forEach((s, i) => {\u001b[39m\n\u001b[31m    if (!s.docket) return;\u001b[39m\n\u001b[31m    const card = cards[i];\u001b[39m\n\u001b[31m    if (card) {\u001b[39m\n\u001b[31m      const docketInput = card.querySelector('.cons-docket-input');\u001b[39m\n\u001b[31m      if (docketInput) {\u001b[39m\n\u001b[31m        docketInput.value = s.docket;\u001b[39m\n\u001b[31m        showLinkedDocket(card, s.docket);\u001b[39m\n\u001b[31m      }\u001b[39m\n\u001b[31m    }\u001b[39m\n\u001b[31m  });\u001b[39m\n\u001b[31m  showToast(`Applied ${slipOCRData.filter(s=>s.docket).length} dockets ✓`, 'ok');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31m// STORE LIST — with column mapping UI\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31mlet _rawRows = [];      // raw rows from file, kept for re-mapping\u001b[39m\n\u001b[31mlet _rawHeaders = [];   // headers from file·\u001b[39m\n\u001b[31masync function handleStoreFile(input) {\u001b[39m\n\u001b[31m  const file = input.files[0];\u001b[39m\n\u001b[31m  if (!file) return;\u001b[39m\n\u001b[31m  showToast('Reading file...', 'info');·\u001b[39m\n\u001b[31m  const data = await readFileAsDataURL(file);\u001b[39m\n\u001b[31m  const base64 = data.split(',')[1];\u001b[39m\n\u001b[31m  const binary = atob(base64);\u001b[39m\n\u001b[31m  const bytes = new Uint8Array(binary.length);\u001b[39m\n\u001b[31m  for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);·\u001b[39m\n\u001b[31m  let rows = [];\u001b[39m\n\u001b[31m  try {\u001b[39m\n\u001b[31m    if (file.name.endsWith('.csv')) {\u001b[39m\n\u001b[31m      const text = new TextDecoder().decode(bytes);\u001b[39m\n\u001b[31m      rows = text.split('\\\\n').map(r => r.split(',').map(c => c.trim().replace(/^\\\"|\\\"$/g, '')));\u001b[39m\n\u001b[31m    } else {\u001b[39m\n\u001b[31m      const wb = XLSX.read(bytes, { type: 'array' });\u001b[39m\n\u001b[31m      const ws = wb.Sheets[wb.SheetNames[0]];\u001b[39m\n\u001b[31m      rows = XLSX.utils.sheet_to_json(ws, { header: 1 });\u001b[39m\n\u001b[31m    }\u001b[39m\n\u001b[31m  } catch(e) { showToast('File read error: ' + e.message, 'err'); return; }·\u001b[39m\n\u001b[31m  if (!rows.length || !rows[0]) { showToast('File appears empty', 'err'); return; }·\u001b[39m\n\u001b[31m  _rawHeaders = (rows[0] || []).map(h => (h || '').toString().trim());\u001b[39m\n\u001b[31m  _rawRows = rows.slice(1).filter(r => r && r.some(c => c !== '' && c != null));·\u001b[39m\n\u001b[31m  document.getElementById('storeTableArea').style.display = 'block';\u001b[39m\n\u001b[31m  renderMappingUI(file.name);\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// ── COLUMN MAPPING FIELDS ─────────────────────────────\u001b[39m\n\u001b[31mconst MAPPING_FIELDS = [\u001b[39m\n\u001b[31m  { key: 'name',    label: 'Store / Partner Name', required: true,\u001b[39m\n\u001b[31m    keywords: ['store','shop','name','outlet','consignee','destination','partner','dealer','client','retailer'] },\u001b[39m\n\u001b[31m  { key: 'city',    label: 'City / Location',       required: false,\u001b[39m\n\u001b[31m    keywords: ['city','town','district','location','area','region','place'] },\u001b[39m\n\u001b[31m  { key: 'phone',   label: 'Phone / Mobile',        required: false,\u001b[39m\n\u001b[31m    keywords: ['phone','mobile','contact','tel','number','mob'] },\u001b[39m\n\u001b[31m  { key: 'docket',  label: 'Docket / AWB No.',      required: false,\u001b[39m\n\u001b[31m    keywords: ['docket','awb','tracking','courier','dkt','consignment','cno','waybill'] },\u001b[39m\n\u001b[31m  { key: 'address', label: 'Address',               required: false,\u001b[39m\n\u001b[31m    keywords: ['address','addr','street','locality','area'] },\u001b[39m\n\u001b[31m  { key: 'pincode', label: 'Pincode / PIN',          required: false,\u001b[39m\n\u001b[31m    keywords: ['pin','pincode','postal','zip','code'] },\u001b[39m\n\u001b[31m];·\u001b[39m\n\u001b[31m// Saved mapping key based on column fingerprint (so same client file auto-maps)\u001b[39m\n\u001b[31mfunction getMappingKey() {\u001b[39m\n\u001b[31m  return 'colmap_' + _rawHeaders.join('|').toLowerCase().replace(/\\\\s+/g,'');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// ── Detect if a column is a combined/multiline address cell ─────────────\u001b[39m\n\u001b[31mfunction isCombinedColumn(colIdx) {\u001b[39m\n\u001b[31m  if (colIdx < 0) return false;\u001b[39m\n\u001b[31m  // Check first 3 data rows — if any cell contains newline or has 6+ digit number (pincode) = combined\u001b[39m\n\u001b[31m  const samples = _rawRows.slice(0, 3).map(r => (r[colIdx] || '').toString());\u001b[39m\n\u001b[31m  return samples.some(s => s.includes('\\\\n') || /\\\\b\\\\d{6}\\\\b/.test(s) || s.split(',').length >= 3);\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction renderMappingUI(filename) {\u001b[39m\n\u001b[31m  const saved = (() => { try { return JSON.parse(localStorage.getItem(getMappingKey()) || 'null'); } catch(e) { return null; } })();\u001b[39m\n\u001b[31m  const isSaved = !!saved;\u001b[39m\n\u001b[31m  const colInfoEl = document.getElementById('storeColInfo');\u001b[39m\n\u001b[31m  colInfoEl.style.display = 'block';·\u001b[39m\n\u001b[31m  const suggested = {};\u001b[39m\n\u001b[31m  MAPPING_FIELDS.forEach(f => {\u001b[39m\n\u001b[31m    const idx = findCol(_rawHeaders, f.keywords);\u001b[39m\n\u001b[31m    suggested[f.key] = saved ? (saved[f.key] ?? idx) : idx;\u001b[39m\n\u001b[31m  });·\u001b[39m\n\u001b[31m  // Detect if address column is combined\u001b[39m\n\u001b[31m  const addrIdx = suggested.address >= 0 ? suggested.address : findCol(_rawHeaders, ['address','addr']);\u001b[39m\n\u001b[31m  const looksCombo = isCombinedColumn(addrIdx) || isCombinedColumn(suggested.name);\u001b[39m\n\u001b[31m  const comboCol   = looksCombo ? (addrIdx >= 0 ? addrIdx : suggested.name) : -1;\u001b[39m\n\u001b[31m  const comboPreview = comboCol >= 0 && _rawRows[0] ? (_rawRows[0][comboCol] || '').toString().slice(0, 120) : '';·\u001b[39m\n\u001b[31m  colInfoEl.innerHTML = `\u001b[39m\n\u001b[31m    <div style=\\\"background:var(--sur2);border:1px solid var(--bdr);border-radius:var(--r-sm);padding:12px;\\\">\u001b[39m\n\u001b[31m      <div style=\\\"display:flex;align-items:center;justify-content:space-between;margin-bottom:10px;\\\">\u001b[39m\n\u001b[31m        <div style=\\\"font-family:var(--fnt-mono);font-size:.68rem;color:var(--sky);\\\">\u001b[39m\n\u001b[31m          📋 ${filename} &nbsp;·&nbsp; ${_rawRows.length} rows &nbsp;·&nbsp; ${_rawHeaders.length} columns\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m        ${isSaved ? '<span class=\\\"map-saved-badge\\\">✓ mapping remembered</span>' : ''}\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      <div style=\\\"font-size:.68rem;color:var(--t3);font-family:var(--fnt-mono);margin-bottom:10px;word-break:break-all;\\\">\u001b[39m\n\u001b[31m        Columns: <span style=\\\"color:var(--t2);\\\">${_rawHeaders.map((h,i)=>`[${i+1}] ${h||'?'}`).join(' · ')}</span>\u001b[39m\n\u001b[31m      </div>·\u001b[39m\n\u001b[31m      ${looksCombo ? `\u001b[39m\n\u001b[31m      <!-- COMBINED COLUMN DETECTED -->\u001b[39m\n\u001b[31m      <div style=\\\"background:rgba(56,189,248,.08);border:1px solid rgba(56,189,248,.3);\u001b[39m\n\u001b[31m        border-radius:var(--r-sm);padding:10px 12px;margin-bottom:10px;\\\">\u001b[39m\n\u001b[31m        <div style=\\\"font-size:.72rem;color:var(--sky);font-weight:600;margin-bottom:6px;\\\">\u001b[39m\n\u001b[31m          🤖 Combined Address Column Detected\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m        <div style=\\\"font-size:.68rem;color:var(--t2);font-family:var(--fnt-mono);\u001b[39m\n\u001b[31m          white-space:pre-wrap;background:var(--bg);padding:6px 8px;border-radius:4px;\u001b[39m\n\u001b[31m          margin-bottom:8px;max-height:80px;overflow:hidden;\\\">${comboPreview}</div>\u001b[39m\n\u001b[31m        <div style=\\\"font-size:.7rem;color:var(--t3);margin-bottom:8px;\\\">\u001b[39m\n\u001b[31m          AI will extract: Name · Address · City · Pincode · Phone from each cell.\u001b[39m\n\u001b[31m          Runs in browser (Transformers.js) with Ollama fallback.\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m        <div style=\\\"display:flex;align-items:center;gap:8px;\\\">\u001b[39m\n\u001b[31m          <label style=\\\"display:flex;align-items:center;gap:6px;font-size:.72rem;color:var(--t1);cursor:pointer;\\\">\u001b[39m\n\u001b[31m            <input type=\\\"checkbox\\\" id=\\\"useAIParse\\\" checked style=\\\"cursor:pointer;\\\">\u001b[39m\n\u001b[31m            Use AI to parse combined column\u001b[39m\n\u001b[31m          </label>\u001b[39m\n\u001b[31m          <select id=\\\"comboColSel\\\" style=\\\"background:var(--sur2);border:1px solid var(--bdr);\u001b[39m\n\u001b[31m            color:var(--t1);font-family:var(--fnt-mono);font-size:.72rem;padding:4px 8px;\u001b[39m\n\u001b[31m            border-radius:var(--r-sm);outline:none;\\\">\u001b[39m\n\u001b[31m            ${_rawHeaders.map((h,i)=>`<option value=\\\"${i}\\\" ${i===comboCol?'selected':''}>${h||'(col '+(i+1)+')'}</option>`).join('')}\u001b[39m\n\u001b[31m          </select>\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      ` : ''}·\u001b[39m\n\u001b[31m      <div id=\\\"manualMapSection\\\" style=\\\"${looksCombo ? 'display:none;' : ''}\\\">\u001b[39m\n\u001b[31m        <div style=\\\"font-size:.68rem;color:var(--t3);margin-bottom:6px;\\\">\u001b[39m\n\u001b[31m          ${looksCombo ? 'Manual mapping (used when AI parse is off, or for other columns):' : 'Map each field to your column:'}\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m        <table class=\\\"map-table\\\">\u001b[39m\n\u001b[31m          <thead><tr><th>Field</th><th>Your column</th><th>Preview (row 1)</th></tr></thead>\u001b[39m\n\u001b[31m          <tbody>\u001b[39m\n\u001b[31m            ${MAPPING_FIELDS.map(f => {\u001b[39m\n\u001b[31m              const selIdx = suggested[f.key] ?? -1;\u001b[39m\n\u001b[31m              const preview = selIdx >= 0 && _rawRows[0] ? ((_rawRows[0][selIdx]||'').toString().slice(0,40)) : '—';\u001b[39m\n\u001b[31m              return `<tr>\u001b[39m\n\u001b[31m                <td style=\\\"white-space:nowrap;color:var(--t1);font-weight:500;font-size:.75rem;\\\">\u001b[39m\n\u001b[31m                  ${f.label}${f.required ? '<span class=\\\"map-req\\\">*</span>' : ''}\u001b[39m\n\u001b[31m                </td>\u001b[39m\n\u001b[31m                <td>\u001b[39m\n\u001b[31m                  <select id=\\\"mapsel_${f.key}\\\" onchange=\\\"updateMappingPreview('${f.key}')\\\">\u001b[39m\n\u001b[31m                    ${_rawHeaders.map((h,i)=>\u001b[39m\n\u001b[31m                      `<option value=\\\"${i}\\\" ${i===selIdx?'selected':''}>${h||'(col '+(i+1)+')'}</option>`\u001b[39m\n\u001b[31m                    ).join('')}\u001b[39m\n\u001b[31m                    <option value=\\\"-1\\\" ${selIdx===-1?'selected':''}>— not in file —</option>\u001b[39m\n\u001b[31m                  </select>\u001b[39m\n\u001b[31m                </td>\u001b[39m\n\u001b[31m                <td id=\\\"mapprev_${f.key}\\\" style=\\\"font-family:var(--fnt-mono);font-size:.68rem;\u001b[39m\n\u001b[31m                  color:var(--t2);max-width:90px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;\\\">\u001b[39m\n\u001b[31m                  ${preview}\u001b[39m\n\u001b[31m                </td>\u001b[39m\n\u001b[31m              </tr>`;\u001b[39m\n\u001b[31m            }).join('')}\u001b[39m\n\u001b[31m          </tbody>\u001b[39m\n\u001b[31m        </table>\u001b[39m\n\u001b[31m      </div>·\u001b[39m\n\u001b[31m      <!-- Also map docket/courier no column separately — always shown -->\u001b[39m\n\u001b[31m      <div style=\\\"margin-top:10px;padding-top:10px;border-top:1px solid var(--bdr);\\\">\u001b[39m\n\u001b[31m        <div style=\\\"font-size:.68rem;color:var(--t3);margin-bottom:6px;font-family:var(--fnt-mono);\\\">\u001b[39m\n\u001b[31m          DOCKET / COURIER NO. COLUMN (required for tracking):\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m        <div style=\\\"display:flex;gap:8px;align-items:center;\\\">\u001b[39m\n\u001b[31m          <select id=\\\"docketColSel\\\" style=\\\"background:var(--sur2);border:1px solid var(--bdr);\u001b[39m\n\u001b[31m            color:var(--acc);font-family:var(--fnt-mono);font-size:.78rem;padding:6px 10px;\u001b[39m\n\u001b[31m            border-radius:var(--r-sm);outline:none;flex:1;\\\">\u001b[39m\n\u001b[31m            <option value=\\\"-1\\\">— Docket not in file —</option>\u001b[39m\n\u001b[31m            ${_rawHeaders.map((h,i)=>`<option value=\\\"${i}\\\" ${i===(suggested.docket??-1)?'selected':''}>${h||'(col '+(i+1)+')'}</option>`).join('')}\u001b[39m\n\u001b[31m          </select>\u001b[39m\n\u001b[31m          <span id=\\\"docketPrev\\\" style=\\\"font-family:var(--fnt-mono);font-size:.72rem;color:var(--acc);\\\">\u001b[39m\n\u001b[31m            ${suggested.docket >= 0 && _rawRows[0] ? (_rawRows[0][suggested.docket]||'') : ''}\u001b[39m\n\u001b[31m          </span>\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m      </div>·\u001b[39m\n\u001b[31m      <div class=\\\"btn-row\\\" style=\\\"margin-top:12px;\\\">\u001b[39m\n\u001b[31m        <button class=\\\"btn btn-ghost\\\" style=\\\"font-size:.78rem;\\\" onclick=\\\"clearStores()\\\">✕ Cancel</button>\u001b[39m\n\u001b[31m        <button class=\\\"btn btn-orange\\\" style=\\\"font-size:.78rem;\\\" onclick=\\\"applyMapping()\\\">✓ Apply Mapping</button>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m  `;\u001b[39m\n\u001b[31m  document.getElementById('storeTable').innerHTML = '';\u001b[39m\n\u001b[31m  document.getElementById('storeAddPanel').style.display = 'none';·\u001b[39m\n\u001b[31m  // Update docket preview on change\u001b[39m\n\u001b[31m  const docketSel = document.getElementById('docketColSel');\u001b[39m\n\u001b[31m  if (docketSel) docketSel.addEventListener('change', () => {\u001b[39m\n\u001b[31m    const idx = parseInt(docketSel.value);\u001b[39m\n\u001b[31m    const prev = document.getElementById('docketPrev');\u001b[39m\n\u001b[31m    if (prev) prev.textContent = idx >= 0 && _rawRows[0] ? (_rawRows[0][idx]||'') : '';\u001b[39m\n\u001b[31m  });·\u001b[39m\n\u001b[31m  // Toggle manual map opacity when AI checkbox changes\u001b[39m\n\u001b[31m  const aiChk = document.getElementById('useAIParse');\u001b[39m\n\u001b[31m  const manSec = document.getElementById('manualMapSection');\u001b[39m\n\u001b[31m  if (aiChk && manSec) {\u001b[39m\n\u001b[31m    aiChk.addEventListener('change', () => {\u001b[39m\n\u001b[31m      manSec.style.display = aiChk.checked ? 'none' : 'block';\u001b[39m\n\u001b[31m    });\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction updateMappingPreview(key) {\u001b[39m\n\u001b[31m  const sel = document.getElementById('mapsel_' + key);\u001b[39m\n\u001b[31m  if (!sel) return;\u001b[39m\n\u001b[31m  const idx = parseInt(sel.value);\u001b[39m\n\u001b[31m  const prev = document.getElementById('mapprev_' + key);\u001b[39m\n\u001b[31m  if (prev) prev.textContent = (idx >= 0 && _rawRows[0]) ? ((_rawRows[0][idx]||'').toString().slice(0,40)) : '—';\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// ── REGEX-FIRST ADDRESS PARSER ───────────────────────────────────────────\u001b[39m\n\u001b[31m// Improved Indian address parser\u001b[39m\n\u001b[31m// Handles formats seen in real BTL dispatch files\u001b[39m\n\u001b[31mfunction parseAddressRegex(raw) {\u001b[39m\n\u001b[31m  const text = raw.toString().replace(/\\\\r/g, '').trim();\u001b[39m\n\u001b[31m  const lines = text.split('\\\\n').map(l => l.trim()).filter(Boolean);\u001b[39m\n\u001b[31m  const result = { name:'', address:'', city:'', pincode:'', phone:'', confidence:0 };·\u001b[39m\n\u001b[31m  // ── PHONE: Indian mobile 6-9 prefix, 10 digits ──────────────────────────\u001b[39m\n\u001b[31m  // Also handles \\\"Phone : 9876543210\\\", \\\"M.No.-9876543210\\\", \\\"Mobile - 9876543210\\\"\u001b[39m\n\u001b[31m  const phoneMatch = text.match(/(?:Phone|Mobile|Mob|M\\\\.No\\\\.?|Ph\\\\.?)?\\\\s*[:\\\\-]?\\\\s*\\\\b([6-9]\\\\d{9})\\\\b/i);\u001b[39m\n\u001b[31m  if (phoneMatch) result.phone = phoneMatch[1];·\u001b[39m\n\u001b[31m  // ── PINCODE: 6-digit standalone number ──────────────────────────────────\u001b[39m\n\u001b[31m  // Must not be part of a longer number (e.g. not an account number)\u001b[39m\n\u001b[31m  const pinMatch = text.match(/(?<![\\\\d])([1-9]\\\\d{5})(?![\\\\d])/);\u001b[39m\n\u001b[31m  if (pinMatch) result.pincode = pinMatch[1];·\u001b[39m\n\u001b[31m  // ── CITY: the token immediately before the pincode ───────────────────────\u001b[39m\n\u001b[31m  const indianStates = ['rajasthan','gujarat','maharashtra','punjab','haryana',\u001b[39m\n\u001b[31m    'uttar pradesh','madhya pradesh','karnataka','tamil nadu','andhra pradesh',\u001b[39m\n\u001b[31m    'telangana','kerala','west bengal','bihar','odisha','jharkhand','assam',\u001b[39m\n\u001b[31m    'himachal pradesh','uttarakhand','chhattisgarh','goa','delhi','chandigarh',\u001b[39m\n\u001b[31m    'raj.','raj'];\u001b[39m\n\u001b[31m  if (result.pincode) {\u001b[39m\n\u001b[31m    const pinPos = text.indexOf(result.pincode);\u001b[39m\n\u001b[31m    const beforePin = text.slice(0, pinPos);\u001b[39m\n\u001b[31m    // Strategy 1: split by comma/newline, walk backwards skipping state names\u001b[39m\n\u001b[31m    const chunks = beforePin.split(/[,\\\\n]/).map(c => c.trim()).filter(Boolean);\u001b[39m\n\u001b[31m    for (let i = chunks.length - 1; i >= 0; i--) {\u001b[39m\n\u001b[31m      const chunk = chunks[i].replace(/\\\\d+/g,'').replace(/[()]/g,'').replace(/[.-]/g,'').trim();\u001b[39m\n\u001b[31m      const lower = chunk.toLowerCase();\u001b[39m\n\u001b[31m      if (!chunk || chunk.length < 2) continue;\u001b[39m\n\u001b[31m      if (indianStates.includes(lower)) continue;\u001b[39m\n\u001b[31m      // Skip chunks that look like addresses (contain numbers or long strings)\u001b[39m\n\u001b[31m      if (/\\\\d/.test(chunk)) continue;\u001b[39m\n\u001b[31m      result.city = chunk;\u001b[39m\n\u001b[31m      break;\u001b[39m\n\u001b[31m    }\u001b[39m\n\u001b[31m    // Strategy 2: if still no city, look for last capitalised word before pincode\u001b[39m\n\u001b[31m    // handles \\\"compound paota Jodhpur 342006\\\" → Jodhpur\u001b[39m\n\u001b[31m    if (!result.city) {\u001b[39m\n\u001b[31m      const wordsBefore = beforePin.replace(/[,\\\\n]/g,' ').trim().split(/\\\\s+/);\u001b[39m\n\u001b[31m      for (let i = wordsBefore.length - 1; i >= 0; i--) {\u001b[39m\n\u001b[31m        const w = wordsBefore[i].replace(/[^a-zA-Z]/g,'');\u001b[39m\n\u001b[31m        if (w.length < 3) continue;\u001b[39m\n\u001b[31m        if (indianStates.includes(w.toLowerCase())) continue;\u001b[39m\n\u001b[31m        // Prefer words that start with capital letter (likely a proper noun/city)\u001b[39m\n\u001b[31m        if (/^[A-Z]/.test(w)) { result.city = w; break; }\u001b[39m\n\u001b[31m      }\u001b[39m\n\u001b[31m    }\u001b[39m\n\u001b[31m  }·\u001b[39m\n\u001b[31m  // ── NAME: first meaningful line, extract shop name ───────────────────────\u001b[39m\n\u001b[31m  // Patterns:\u001b[39m\n\u001b[31m  //   \\\"Sanwar Mal Saini, C/o R G COMPUTERS\\\" → name = \\\"R G COMPUTERS\\\"\u001b[39m\n\u001b[31m  //   \\\"SANWAR MAL SAINI, C/o ADVANCE TECHNOLOGIES\\\" → name = \\\"ADVANCE TECHNOLOGIES\\\"\u001b[39m\n\u001b[31m  //   \\\"Sunil Vaishnav\\\" → name = \\\"Sunil Vaishnav\\\" (no C/o)\u001b[39m\n\u001b[31m  //   \\\"Baltej Singh, Professional computer care\\\" → name = \\\"Professional computer care\\\"\u001b[39m\n\u001b[31m  if (lines.length > 0) {\u001b[39m\n\u001b[31m    const firstLine = lines[0];\u001b[39m\n\u001b[31m    // Check for C/o or C/O pattern — shop name comes after it\u001b[39m\n\u001b[31m    const coMatch = firstLine.match(/C\\\\/?[Oo]\\\\.?\\\\s+(.+)/);\u001b[39m\n\u001b[31m    if (coMatch) {\u001b[39m\n\u001b[31m      result.name = coMatch[1].trim();\u001b[39m\n\u001b[31m    } else {\u001b[39m\n\u001b[31m      // Check if there is a comma — second part might be the shop name\u001b[39m\n\u001b[31m      const parts = firstLine.split(',').map(p => p.trim());\u001b[39m\n\u001b[31m      if (parts.length >= 2 && parts[1].length > 3) {\u001b[39m\n\u001b[31m        // Heuristic: if second part looks like a business name (has uppercase or >5 chars)\u001b[39m\n\u001b[31m        const hasUpperOrLong = /[A-Z]{2,}/.test(parts[1]) || parts[1].length > 8;\u001b[39m\n\u001b[31m        result.name = hasUpperOrLong ? parts[1] : parts[0];\u001b[39m\n\u001b[31m      } else {\u001b[39m\n\u001b[31m        result.name = parts[0];\u001b[39m\n\u001b[31m      }\u001b[39m\n\u001b[31m    }\u001b[39m\n\u001b[31m  }·\u001b[39m\n\u001b[31m  // ── ADDRESS: lines between name and phone/pincode line ───────────────────\u001b[39m\n\u001b[31m  const phoneLineIdx = lines.findIndex(l => result.phone && l.includes(result.phone));\u001b[39m\n\u001b[31m  const addrLines = lines.filter((l, i) => {\u001b[39m\n\u001b[31m    if (i === 0) return false;\u001b[39m\n\u001b[31m    if (i === phoneLineIdx) return false;\u001b[39m\n\u001b[31m    // Exclude lines that are just the phone number\u001b[39m\n\u001b[31m    if (/^[6-9]\\\\d{9}$/.test(l.replace(/\\\\s/g,''))) return false;\u001b[39m\n\u001b[31m    return true;\u001b[39m\n\u001b[31m  });\u001b[39m\n\u001b[31m  result.address = addrLines.join(', ').replace(/,\\\\s*,/g, ',').trim();·\u001b[39m\n\u001b[31m  // ── CONFIDENCE: simple score based on what we got ────────────────────────\u001b[39m\n\u001b[31m  let conf = 0;\u001b[39m\n\u001b[31m  if (result.name)    conf += 30;\u001b[39m\n\u001b[31m  if (result.city)    conf += 25;\u001b[39m\n\u001b[31m  if (result.pincode) conf += 25;\u001b[39m\n\u001b[31m  if (result.phone)   conf += 20;\u001b[39m\n\u001b[31m  result.confidence = conf;·\u001b[39m\n\u001b[31m  return result;\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// ── AI ADDRESS PARSER (Ollama fallback) ──────────────────────────────────\u001b[39m\n\u001b[31m// Fetches past corrections from localStorage to improve prompt accuracy\u001b[39m\n\u001b[31mfunction getPastCorrections() {\u001b[39m\n\u001b[31m  try {\u001b[39m\n\u001b[31m    const log = JSON.parse(localStorage.getItem('360lm_parse_corrections') || '[]');\u001b[39m\n\u001b[31m    return log.slice(-10); // last 10 corrections as examples\u001b[39m\n\u001b[31m  } catch(e) { return []; }\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction saveCorrectionLocal(entry) {\u001b[39m\n\u001b[31m  try {\u001b[39m\n\u001b[31m    const log = JSON.parse(localStorage.getItem('360lm_parse_corrections') || '[]');\u001b[39m\n\u001b[31m    log.push(entry);\u001b[39m\n\u001b[31m    // Keep only last 50\u001b[39m\n\u001b[31m    localStorage.setItem('360lm_parse_corrections', JSON.stringify(log.slice(-50)));\u001b[39m\n\u001b[31m  } catch(e) {}\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31masync function parseAddressWithOllama(raw) {\u001b[39m\n\u001b[31m  // Calls the server-side /dispatch-ai/address proxy (avoids CORS + port 11434 issues)\u001b[39m\n\u001b[31m  try {\u001b[39m\n\u001b[31m    const resp = await fetch('/dispatch-ai/address', {\u001b[39m\n\u001b[31m      method: 'POST',\u001b[39m\n\u001b[31m      headers: { 'Content-Type': 'application/json' },\u001b[39m\n\u001b[31m      body: JSON.stringify({ text: raw }),\u001b[39m\n\u001b[31m      signal: AbortSignal.timeout(120000)\u001b[39m\n\u001b[31m    });\u001b[39m\n\u001b[31m    const data = await resp.json();\u001b[39m\n\u001b[31m    if (data.success) return data;\u001b[39m\n\u001b[31m  } catch(e) {}\u001b[39m\n\u001b[31m  return null;\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31masync function parseAddressCell(raw) {\u001b[39m\n\u001b[31m  // Step 1: regex — always runs first, no network call\u001b[39m\n\u001b[31m  const regex = parseAddressRegex(raw);\u001b[39m\n\u001b[31m  if (regex.name) return { ...regex, source: 'regex' };·\u001b[39m\n\u001b[31m  // Step 2: Ollama only if regex got nothing AND Ollama is configured\u001b[39m\n\u001b[31m  // Uses a 3-second timeout to prevent browser hang\u001b[39m\n\u001b[31m  if (window.appConfig.ocr_mode !== 'transformers') {\u001b[39m\n\u001b[31m    try {\u001b[39m\n\u001b[31m      const ai = await Promise.race([\u001b[39m\n\u001b[31m        parseAddressWithOllama(raw),\u001b[39m\n\u001b[31m        new Promise((_, reject) => setTimeout(() => reject(new Error('timeout')), 120000))\u001b[39m\n\u001b[31m      ]);\u001b[39m\n\u001b[31m      if (ai && ai.name) return { ...ai, source: 'ollama' };\u001b[39m\n\u001b[31m    } catch(e) { /* timeout or error — continue to fallback */ }\u001b[39m\n\u001b[31m  }·\u001b[39m\n\u001b[31m  // Step 3: fallback — first line as name\u001b[39m\n\u001b[31m  const lines = raw.toString().split('\\\\n').map(l=>l.trim()).filter(Boolean);\u001b[39m\n\u001b[31m  return { name: lines[0]||raw.slice(0,40), address: raw, city:'', pincode:'', phone:'', source:'fallback' };\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31masync function applyMapping() {\u001b[39m\n\u001b[31m  const useAI   = document.getElementById('useAIParse')?.checked ?? false;\u001b[39m\n\u001b[31m  const comboSel = document.getElementById('comboColSel');\u001b[39m\n\u001b[31m  const comboIdx = comboSel ? parseInt(comboSel.value) : -1;\u001b[39m\n\u001b[31m  const docketIdx = parseInt(document.getElementById('docketColSel')?.value ?? -1);·\u001b[39m\n\u001b[31m  // Build manual mapping as fallback\u001b[39m\n\u001b[31m  const mapping = {};\u001b[39m\n\u001b[31m  MAPPING_FIELDS.forEach(f => {\u001b[39m\n\u001b[31m    const sel = document.getElementById('mapsel_' + f.key);\u001b[39m\n\u001b[31m    mapping[f.key] = sel ? parseInt(sel.value) : -1;\u001b[39m\n\u001b[31m  });\u001b[39m\n\u001b[31m  // Override docket from dedicated selector\u001b[39m\n\u001b[31m  mapping.docket = docketIdx;·\u001b[39m\n\u001b[31m  // Determine if we have a valid base for stores\u001b[39m\n\u001b[31m  const nameIdx = useAI && comboIdx >= 0 ? comboIdx : mapping.name;\u001b[39m\n\u001b[31m  if (nameIdx === -1 && !useAI) { showToast('Store Name column is required', 'err'); return; }·\u001b[39m\n\u001b[31m  // Save mapping\u001b[39m\n\u001b[31m  try { localStorage.setItem(getMappingKey(), JSON.stringify({...mapping, _useAI: useAI, _comboIdx: comboIdx})); } catch(e) {}·\u001b[39m\n\u001b[31m  if (useAI && comboIdx >= 0) {\u001b[39m\n\u001b[31m    // AI path — parse each combined cell\u001b[39m\n\u001b[31m    showToast('AI parsing addresses...', 'info');\u001b[39m\n\u001b[31m    showOCRWorking(true, `Parsing 0/${_rawRows.length} addresses...`);\u001b[39m\n\u001b[31m    stores = [];\u001b[39m\n\u001b[31m    for (let i = 0; i < _rawRows.length; i++) {\u001b[39m\n\u001b[31m      const r = _rawRows[i];\u001b[39m\n\u001b[31m      const raw = (r[comboIdx] || '').toString().trim();\u001b[39m\n\u001b[31m      if (!raw) continue;\u001b[39m\n\u001b[31m      showOCRWorking(true, `Parsing ${i+1}/${_rawRows.length} — ${raw.slice(0,30)}...`);\u001b[39m\n\u001b[31m      // Yield to browser every row to prevent UI freeze\u001b[39m\n\u001b[31m      await new Promise(r => setTimeout(r, 0));\u001b[39m\n\u001b[31m      const parsed = await parseAddressCell(raw);\u001b[39m\n\u001b[31m      stores.push({\u001b[39m\n\u001b[31m        id: i,\u001b[39m\n\u001b[31m        name:    parsed.name    || raw.slice(0,40),\u001b[39m\n\u001b[31m        city:    parsed.city    || '',\u001b[39m\n\u001b[31m        phone:   parsed.phone   || '',\u001b[39m\n\u001b[31m        address: parsed.address || raw,\u001b[39m\n\u001b[31m        pincode: parsed.pincode || '',\u001b[39m\n\u001b[31m        docket:  docketIdx >= 0 ? (r[docketIdx]||'').toString().trim() : '',\u001b[39m\n\u001b[31m        _parseSource: parsed.source,\u001b[39m\n\u001b[31m        _confidence:  parsed.confidence || 0,\u001b[39m\n\u001b[31m        _rawCell:     raw,\u001b[39m\n\u001b[31m        _origName:    parsed.name || '',\u001b[39m\n\u001b[31m        _origCity:    parsed.city || '',\u001b[39m\n\u001b[31m        matched: false\u001b[39m\n\u001b[31m      });\u001b[39m\n\u001b[31m    }\u001b[39m\n\u001b[31m    showOCRWorking(false);\u001b[39m\n\u001b[31m  } else {\u001b[39m\n\u001b[31m    // Manual mapping path\u001b[39m\n\u001b[31m    stores = _rawRows\u001b[39m\n\u001b[31m      .filter(r => r[nameIdx] && (r[nameIdx]+'').trim())\u001b[39m\n\u001b[31m      .map((r, i) => ({\u001b[39m\n\u001b[31m        id: i,\u001b[39m\n\u001b[31m        name:    mapping.name    >= 0 ? (r[mapping.name]   ||'').toString().trim() : '',\u001b[39m\n\u001b[31m        city:    mapping.city    >= 0 ? (r[mapping.city]   ||'').toString().trim() : '',\u001b[39m\n\u001b[31m        phone:   mapping.phone   >= 0 ? (r[mapping.phone]  ||'').toString().trim() : '',\u001b[39m\n\u001b[31m        docket:  mapping.docket  >= 0 ? (r[mapping.docket] ||'').toString().trim() : '',\u001b[39m\n\u001b[31m        address: mapping.address >= 0 ? (r[mapping.address]||'').toString().trim() : '',\u001b[39m\n\u001b[31m        pincode: mapping.pincode >= 0 ? (r[mapping.pincode]||'').toString().trim() : '',\u001b[39m\n\u001b[31m        matched: false\u001b[39m\n\u001b[31m      }));\u001b[39m\n\u001b[31m  }·\u001b[39m\n\u001b[31m  document.getElementById('storeCount').textContent = `${stores.length} stores loaded`;·\u001b[39m\n\u001b[31m  if (useAI && comboIdx >= 0) {\u001b[39m\n\u001b[31m    // AI path — show row-by-row review panel before adding consignees\u001b[39m\n\u001b[31m    showAddressReviewPanel();\u001b[39m\n\u001b[31m  } else {\u001b[39m\n\u001b[31m    // Manual mapping path — go straight to store table + add panel\u001b[39m\n\u001b[31m    renderStoreTable(mapping.docket >= 0 || docketIdx >= 0);\u001b[39m\n\u001b[31m    const addPanel = document.getElementById('storeAddPanel');\u001b[39m\n\u001b[31m    const addBtn   = document.getElementById('storeAddBtn');\u001b[39m\n\u001b[31m    const addTitle = document.getElementById('storeAddTitle');\u001b[39m\n\u001b[31m    const hasDockets = stores.some(s => s.docket);\u001b[39m\n\u001b[31m    addTitle.textContent = hasDockets\u001b[39m\n\u001b[31m      ? `${stores.filter(s=>s.docket).length} stores with dockets — add as consignees?`\u001b[39m\n\u001b[31m      : `Add ${stores.length} stores as consignees?`;\u001b[39m\n\u001b[31m    addBtn.textContent = `✓ Add ${stores.length} as Consignees`;\u001b[39m\n\u001b[31m    addPanel.style.display = 'block';\u001b[39m\n\u001b[31m    showToast(`${stores.length} stores mapped ✓`, 'ok');\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31m// ADDRESS REVIEW PANEL\u001b[39m\n\u001b[31m// Row-by-row review of AI-parsed addresses before consignees created\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31mfunction showAddressReviewPanel() {\u001b[39m\n\u001b[31m  const pane = document.getElementById('addrReviewPane');\u001b[39m\n\u001b[31m  if (!pane) return;\u001b[39m\n\u001b[31m  renderAddressReviewRows();\u001b[39m\n\u001b[31m  pane.style.display = 'block';\u001b[39m\n\u001b[31m  pane.scrollTop = 0;\u001b[39m\n\u001b[31m  showToast(`Review ${stores.length} parsed addresses`, 'info');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction renderAddressReviewRows() {\u001b[39m\n\u001b[31m  const list = document.getElementById('addrReviewList');\u001b[39m\n\u001b[31m  const summary = document.getElementById('addrReviewSummary');\u001b[39m\n\u001b[31m  if (!list) return;·\u001b[39m\n\u001b[31m  const highConf  = stores.filter(s => (s._confidence||0) >= 80).length;\u001b[39m\n\u001b[31m  const medConf   = stores.filter(s => (s._confidence||0) >= 50 && (s._confidence||0) < 80).length;\u001b[39m\n\u001b[31m  const lowConf   = stores.filter(s => (s._confidence||0) < 50).length;·\u001b[39m\n\u001b[31m  summary.innerHTML = `\u001b[39m\n\u001b[31m    <div class=\\\"vs-item\\\"><div class=\\\"vs-val\\\" style=\\\"color:var(--t1);\\\">${stores.length}</div><div class=\\\"vs-lbl\\\">Total</div></div>\u001b[39m\n\u001b[31m    <div class=\\\"vs-item\\\"><div class=\\\"vs-val\\\" style=\\\"color:var(--grn);\\\">${highConf}</div><div class=\\\"vs-lbl\\\">High conf</div></div>\u001b[39m\n\u001b[31m    <div class=\\\"vs-item\\\"><div class=\\\"vs-val\\\" style=\\\"color:var(--ylw);\\\">${medConf}</div><div class=\\\"vs-lbl\\\">Review</div></div>\u001b[39m\n\u001b[31m    <div class=\\\"vs-item\\\"><div class=\\\"vs-val\\\" style=\\\"color:var(--red);\\\">${lowConf}</div><div class=\\\"vs-lbl\\\">Check</div></div>\u001b[39m\n\u001b[31m    <div class=\\\"vs-item\\\" style=\\\"margin-left:auto;\\\">\u001b[39m\n\u001b[31m      <button class=\\\"btn-sm primary\\\" onclick=\\\"approveAllAddresses()\\\" style=\\\"font-size:.7rem;\\\">✓ Approve All</button>\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m  `;·\u001b[39m\n\u001b[31m  list.innerHTML = stores.map((s, i) => {\u001b[39m\n\u001b[31m    const conf = s._confidence || 0;\u001b[39m\n\u001b[31m    const confClass = conf >= 80 ? 'vf-ok' : conf >= 50 ? 'vf-nodocket' : 'vf-dupe';\u001b[39m\n\u001b[31m    const confLabel = conf >= 80 ? `${conf}% ✓` : conf >= 50 ? `${conf}% review` : `${conf}% check`;\u001b[39m\n\u001b[31m    const rowBorder = conf >= 80 ? 'var(--grn)' : conf >= 50 ? 'var(--ylw)' : 'var(--red)';·\u001b[39m\n\u001b[31m    return `\u001b[39m\n\u001b[31m    <div class=\\\"verify-row\\\" id=\\\"arow_${i}\\\" style=\\\"border-left:3px solid ${rowBorder};\\\">\u001b[39m\n\u001b[31m      <div class=\\\"verify-row-hdr\\\">\u001b[39m\n\u001b[31m        <span class=\\\"verify-num\\\">#${i+1}</span>\u001b[39m\n\u001b[31m        <span class=\\\"verify-store\\\" style=\\\"font-size:.78rem;color:var(--t2);font-weight:400;\u001b[39m\n\u001b[31m          font-family:var(--fnt-mono);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;flex:1;\\\"\u001b[39m\n\u001b[31m          title=\\\"${(s._rawCell||'').replace(/\\\"/g,\\\"'\\\")}\\\">\u001b[39m\n\u001b[31m          ${(s._rawCell||'').slice(0,50).split('\\\\n').join(' ↵ ')}       <span class=\\\"verify-flag ${confClass}\\\" style=\\\"flex-shrink:0;margin-right:6px;\\\">${confLabel} · ${s._parseSource||'?'}</span>\u001b[39m\n\u001b[31m        <button onclick=\\\"deleteReviewRow(${i})\\\"\u001b[39m\n\u001b[31m          style=\\\"background:none;border:1px solid rgba(239,68,68,.4);color:var(--red);\u001b[39m\n\u001b[31m          font-size:.65rem;padding:2px 8px;border-radius:6px;cursor:pointer;\u001b[39m\n\u001b[31m          font-family:var(--fnt-mono);flex-shrink:0;transition:background .2s;\\\"\u001b[39m\n\u001b[31m          onmouseover=\\\"this.style.background='rgba(239,68,68,.15)'\\\"\u001b[39m\n\u001b[31m          onmouseout=\\\"this.style.background='none'\\\">✕ Remove</button>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      <div class=\\\"verify-fields\\\" style=\\\"grid-template-columns:1fr 1fr 1fr 1fr 1fr;\\\">\u001b[39m\n\u001b[31m        <div class=\\\"verify-field\\\">\u001b[39m\n\u001b[31m          <label>Shop / Name <span style=\\\"color:var(--acc);\\\">*</span></label>\u001b[39m\n\u001b[31m          <input type=\\\"text\\\" value=\\\"${s.name||''}\\\" id=\\\"ar_name_${i}\\\"\u001b[39m\n\u001b[31m            oninput=\\\"updateAddrField(${i},'name',this)\\\"\u001b[39m\n\u001b[31m            style=\\\"${!s.name ? 'border-color:var(--red);' : ''}\\\">\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m        <div class=\\\"verify-field\\\">\u001b[39m\n\u001b[31m          <label>City</label>\u001b[39m\n\u001b[31m          <input type=\\\"text\\\" value=\\\"${s.city||''}\\\" id=\\\"ar_city_${i}\\\"\u001b[39m\n\u001b[31m            oninput=\\\"updateAddrField(${i},'city',this)\\\"\u001b[39m\n\u001b[31m            style=\\\"${!s.city ? 'border-color:var(--ylw);' : ''}\\\">\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m        <div class=\\\"verify-field\\\">\u001b[39m\n\u001b[31m          <label>Docket / Courier No.</label>\u001b[39m\n\u001b[31m          <input type=\\\"text\\\" value=\\\"${s.docket||''}\\\" id=\\\"ar_docket_${i}\\\"\u001b[39m\n\u001b[31m            oninput=\\\"updateAddrField(${i},'docket',this)\\\"\u001b[39m\n\u001b[31m            style=\\\"font-family:var(--fnt-mono);${!s.docket?'border-color:var(--ylw);':'color:var(--acc);'}\\\">\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m        <div class=\\\"verify-field\\\">\u001b[39m\n\u001b[31m          <label>Phone</label>\u001b[39m\n\u001b[31m          <input type=\\\"tel\\\" value=\\\"${s.phone||''}\\\" id=\\\"ar_phone_${i}\\\"\u001b[39m\n\u001b[31m            oninput=\\\"updateAddrField(${i},'phone',this)\\\">\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m        <div class=\\\"verify-field\\\">\u001b[39m\n\u001b[31m          <label>Pincode</label>\u001b[39m\n\u001b[31m          <input type=\\\"text\\\" value=\\\"${s.pincode||''}\\\" id=\\\"ar_pin_${i}\\\"\u001b[39m\n\u001b[31m            oninput=\\\"updateAddrField(${i},'pincode',this)\\\">\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      <div style=\\\"font-size:.65rem;color:var(--t3);font-family:var(--fnt-mono);\u001b[39m\n\u001b[31m        margin-top:4px;padding:4px 6px;background:var(--bg);border-radius:4px;word-break:break-all;\\\">\u001b[39m\n\u001b[31m        📋 ${(s._rawCell||'').split('\\\\n').join(' | ')}\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m    </div>`;\u001b[39m\n\u001b[31m  }).join('');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction updateAddrField(i, field, input) {\u001b[39m\n\u001b[31m  if (!stores[i]) return;\u001b[39m\n\u001b[31m  stores[i][field] = input.value.trim();\u001b[39m\n\u001b[31m  // Recompute confidence\u001b[39m\n\u001b[31m  let conf = 0;\u001b[39m\n\u001b[31m  if (stores[i].name)    conf += 30;\u001b[39m\n\u001b[31m  if (stores[i].city)    conf += 25;\u001b[39m\n\u001b[31m  if (stores[i].pincode) conf += 25;\u001b[39m\n\u001b[31m  if (stores[i].phone)   conf += 20;\u001b[39m\n\u001b[31m  stores[i]._confidence = conf;\u001b[39m\n\u001b[31m  input.classList.toggle('changed', true);\u001b[39m\n\u001b[31m  // Update border color on row\u001b[39m\n\u001b[31m  const row = document.getElementById('arow_' + i);\u001b[39m\n\u001b[31m  if (row) row.style.borderLeftColor = conf >= 80 ? 'var(--grn)' : conf >= 50 ? 'var(--ylw)' : 'var(--red)';\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction deleteReviewRow(i) {\u001b[39m\n\u001b[31m  // Mark as deleted — splice from stores array and re-render\u001b[39m\n\u001b[31m  stores.splice(i, 1);\u001b[39m\n\u001b[31m  // Re-assign IDs\u001b[39m\n\u001b[31m  stores.forEach((s, idx) => { s.id = idx; });\u001b[39m\n\u001b[31m  renderAddressReviewRows();\u001b[39m\n\u001b[31m  document.getElementById('storeCount').textContent = `${stores.length} stores loaded`;\u001b[39m\n\u001b[31m  showToast(`Row removed — ${stores.length} remaining`, 'info');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction backToMapping() {\u001b[39m\n\u001b[31m  // Hide review pane — store edits are preserved in stores[] array\u001b[39m\n\u001b[31m  document.getElementById('addrReviewPane').style.display = 'none';\u001b[39m\n\u001b[31m  // Show mapping UI again with current file\u001b[39m\n\u001b[31m  document.getElementById('storeTableArea').style.display = 'block';\u001b[39m\n\u001b[31m  document.getElementById('storeColInfo').style.display = 'block';\u001b[39m\n\u001b[31m  showToast('Your edits are preserved — re-apply mapping to reload', 'info');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction approveAllAddresses() {\u001b[39m\n\u001b[31m  stores.forEach((s, i) => { s._reviewed = true; });\u001b[39m\n\u001b[31m  document.getElementById('addrReviewPane').style.display = 'none';\u001b[39m\n\u001b[31m  applyReviewedStores();\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction confirmAddressReview() {\u001b[39m\n\u001b[31m  // Collect any pending edits, log corrections, close panel\u001b[39m\n\u001b[31m  const corrections = [];\u001b[39m\n\u001b[31m  stores.forEach((s, i) => {\u001b[39m\n\u001b[31m    const origName = s._origName || '';\u001b[39m\n\u001b[31m    const origCity = s._origCity || '';\u001b[39m\n\u001b[31m    if ((origName && origName !== s.name) || (origCity && origCity !== s.city)) {\u001b[39m\n\u001b[31m      corrections.push({ raw: s._rawCell||'', name: s.name, city: s.city, pincode: s.pincode });\u001b[39m\n\u001b[31m    }\u001b[39m\n\u001b[31m  });\u001b[39m\n\u001b[31m  // Save corrections locally for future Ollama prompts\u001b[39m\n\u001b[31m  corrections.forEach(c => saveCorrectionLocal(c));\u001b[39m\n\u001b[31m  // Log to Sheets asynchronously (fire and forget)\u001b[39m\n\u001b[31m  if (corrections.length > 0 && window.appConfig.webhook_url) {\u001b[39m\n\u001b[31m    logParseCorrectionsToSheets(corrections);\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m  document.getElementById('addrReviewPane').style.display = 'none';\u001b[39m\n\u001b[31m  applyReviewedStores();\u001b[39m\n\u001b[31m  showToast(`${corrections.length > 0 ? corrections.length + ' corrections logged · ' : ''}${stores.length} stores ready ✓`, 'ok');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// Clean street address: remove city, pincode, phone if they appear in address field\u001b[39m\n\u001b[31mfunction cleanAddress(addr, city, pincode, phone) {\u001b[39m\n\u001b[31m  if (!addr) return addr;\u001b[39m\n\u001b[31m  let s = addr;\u001b[39m\n\u001b[31m  // Remove phone\u001b[39m\n\u001b[31m  if (phone) s = s.replace(new RegExp(phone.replace(/[.*+?^${}()|[\\\\]\\\\\\\\]/g,'\\\\\\\\$&'), 'g'), '').trim();\u001b[39m\n\u001b[31m  // Remove pincode\u001b[39m\n\u001b[31m  if (pincode) s = s.replace(new RegExp('\\\\\\\\b' + pincode + '\\\\\\\\b', 'g'), '').trim();\u001b[39m\n\u001b[31m  // Remove city (case-insensitive)\u001b[39m\n\u001b[31m  if (city) s = s.replace(new RegExp('\\\\\\\\b' + city.replace(/[.*+?^${}()|[\\\\]\\\\\\\\]/g,'\\\\\\\\$&') + '\\\\\\\\b', 'ig'), '').trim();\u001b[39m\n\u001b[31m  // Remove state names and trailing noise\u001b[39m\n\u001b[31m  const noise = /\\\\b(rajasthan|gujarat|maharashtra|punjab|haryana|uttar pradesh|delhi|raj\\\\.)\\\\b/ig;\u001b[39m\n\u001b[31m  s = s.replace(noise, '');\u001b[39m\n\u001b[31m  // Remove \\\"Mobile :\\\", \\\"Phone :\\\", \\\"M.No.\\\" prefixes and dangling numbers\u001b[39m\n\u001b[31m  s = s.replace(/(?:Mobile|Phone|Mob|M\\\\.No\\\\.?|Ph\\\\.?)\\\\s*[:\\\\-]?\\\\s*/ig, '');\u001b[39m\n\u001b[31m  // Clean up multiple commas/spaces\u001b[39m\n\u001b[31m  s = s.replace(/,\\\\s*,/g, ',').replace(/\\\\s{2,}/g, ' ').replace(/^[\\\\s,]+|[\\\\s,]+$/g, '');\u001b[39m\n\u001b[31m  return s;\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction applyReviewedStores() {\u001b[39m\n\u001b[31m  // Clean address fields before loading stores\u001b[39m\n\u001b[31m  stores.forEach(s => {\u001b[39m\n\u001b[31m    s.address = cleanAddress(s.address, s.city, s.pincode, s.phone);\u001b[39m\n\u001b[31m  });\u001b[39m\n\u001b[31m  renderStoreTable(stores.some(s => s.docket));\u001b[39m\n\u001b[31m  const addPanel = document.getElementById('storeAddPanel');\u001b[39m\n\u001b[31m  const addTitle = document.getElementById('storeAddTitle');\u001b[39m\n\u001b[31m  const addBtn   = document.getElementById('storeAddBtn');\u001b[39m\n\u001b[31m  const hasDockets = stores.some(s => s.docket);\u001b[39m\n\u001b[31m  addTitle.textContent = hasDockets\u001b[39m\n\u001b[31m    ? `${stores.filter(s=>s.docket).length} stores with dockets — add as consignees?`\u001b[39m\n\u001b[31m    : `Add ${stores.length} stores as consignees?`;\u001b[39m\n\u001b[31m  addBtn.textContent = `✓ Add ${stores.length} as Consignees`;\u001b[39m\n\u001b[31m  addPanel.style.display = 'block';\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31masync function logParseCorrectionsToSheets(corrections) {\u001b[39m\n\u001b[31m  const url = window.appConfig.webhook_url;\u001b[39m\n\u001b[31m  if (!url) return;\u001b[39m\n\u001b[31m  try {\u001b[39m\n\u001b[31m    await fetch(url, {\u001b[39m\n\u001b[31m      method: 'POST', mode: 'no-cors',\u001b[39m\n\u001b[31m      headers: { 'Content-Type': 'application/json' },\u001b[39m\n\u001b[31m      body: JSON.stringify({\u001b[39m\n\u001b[31m        type: 'parse_correction',\u001b[39m\n\u001b[31m        corrections,\u001b[39m\n\u001b[31m        colFingerprint: getMappingKey(),\u001b[39m\n\u001b[31m        timestamp: new Date().toISOString()\u001b[39m\n\u001b[31m      })\u001b[39m\n\u001b[31m    });\u001b[39m\n\u001b[31m  } catch(e) {}\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction addStoresToConsignees() {\u001b[39m\n\u001b[31m  const consList = document.getElementById('consList');\u001b[39m\n\u001b[31m  consList.innerHTML = '';\u001b[39m\n\u001b[31m  stores.slice(0, 50).forEach(s => addConsignee(s));\u001b[39m\n\u001b[31m  document.getElementById('storeAddPanel').style.display = 'none';\u001b[39m\n\u001b[31m  showToast(`${Math.min(stores.length, 50)} consignees added ✓`, 'ok');\u001b[39m\n\u001b[31m  if (stores.some(s => s.docket)) setTimeout(() => autoCreateDispatchFromStoreList(), 600);\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction promptAddFromStores() { /* replaced by inline panel */ }··\u001b[39m\n\u001b[31mfunction findCol(headers, keywords) {\u001b[39m\n\u001b[31m  for (let i=0;i<headers.length;i++) {\u001b[39m\n\u001b[31m    const h = (headers[i]||'').toString().toLowerCase();\u001b[39m\n\u001b[31m    if (keywords.some(k => h.includes(k))) return i;\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m  return -1;\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction renderStoreTable(hasDocket) {\u001b[39m\n\u001b[31m  const t = document.getElementById('storeTable');\u001b[39m\n\u001b[31m  const cols = ['#','Store Name','City','Phone'];\u001b[39m\n\u001b[31m  if (hasDocket) cols.push('Docket','Status');\u001b[39m\n\u001b[31m  t.innerHTML = `<thead><tr>${cols.map(c=>`<th>${c}</th>`).join('')}</tr></thead>\u001b[39m\n\u001b[31m  <tbody>${stores.map((s,i) => {\u001b[39m\n\u001b[31m    const statusClass = s.docket ? (s.status === 'delivered' ? 'matched' : 'unmatched') : '';\u001b[39m\n\u001b[31m    const docketCell = hasDocket ? `<td class=\\\"${statusClass}\\\" style=\\\"font-family:var(--fnt-mono);\\\">${s.docket||'—'}</td>` : '';\u001b[39m\n\u001b[31m    const statusCell = hasDocket ? `<td>${s.status ? `<span class=\\\"status-pill sp-${s.status}\\\">${s.status}</span>` : '—'}</td>` : '';\u001b[39m\n\u001b[31m    return `<tr>\u001b[39m\n\u001b[31m      <td>${i+1}</td>\u001b[39m\n\u001b[31m      <td>${s.name}</td>\u001b[39m\n\u001b[31m      <td>${s.city}</td>\u001b[39m\n\u001b[31m      <td style=\\\"font-family:var(--fnt-mono);\\\">${s.phone||'—'}</td>\u001b[39m\n\u001b[31m      ${docketCell}${statusCell}\u001b[39m\n\u001b[31m    </tr>`;\u001b[39m\n\u001b[31m  }).join('')}</tbody>`;\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction clearStores() {\u001b[39m\n\u001b[31m  stores = [];\u001b[39m\n\u001b[31m  document.getElementById('storeTableArea').style.display = 'none';\u001b[39m\n\u001b[31m  document.getElementById('storeFile').value = '';\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction autoCreateDispatchFromStoreList() {\u001b[39m\n\u001b[31m  // Create/update dispatch with all stores that have dockets\u001b[39m\n\u001b[31m  const courier = document.getElementById('n_courier').value || 'SkyKing';\u001b[39m\n\u001b[31m  const storesWithDockets = stores.filter(s => s.docket);\u001b[39m\n\u001b[31m  if (!storesWithDockets.length) return;·\u001b[39m\n\u001b[31m  // Clear and rebuild consignee list from store data\u001b[39m\n\u001b[31m  const consList = document.getElementById('consList');\u001b[39m\n\u001b[31m  consList.innerHTML = '';\u001b[39m\n\u001b[31m  storesWithDockets.forEach(s => {\u001b[39m\n\u001b[31m    addConsignee(s);\u001b[39m\n\u001b[31m  });\u001b[39m\n\u001b[31m  showPane('new');\u001b[39m\n\u001b[31m  showToast(`${storesWithDockets.length} consignees added from store list`, 'ok');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction promptAddFromStores() {\u001b[39m\n\u001b[31m  // Replaced by inline storeAddPanel UI — no-op kept for safety\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction autoMatchDockets() {\u001b[39m\n\u001b[31m  // Match OCR dockets to stores by order\u001b[39m\n\u001b[31m  if (!slipOCRData.length) { showToast('Upload docket slip images first', 'err'); return; }\u001b[39m\n\u001b[31m  let matched = 0;\u001b[39m\n\u001b[31m  slipOCRData.forEach((slip, i) => {\u001b[39m\n\u001b[31m    if (i < stores.length && slip.docket) {\u001b[39m\n\u001b[31m      stores[i].docket = slip.docket;\u001b[39m\n\u001b[31m      matched++;\u001b[39m\n\u001b[31m    }\u001b[39m\n\u001b[31m  });\u001b[39m\n\u001b[31m  renderStoreTable(true);\u001b[39m\n\u001b[31m  showToast(`${matched} dockets matched to stores ✓`, 'ok');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31m// CONSIGNEES\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31mfunction addConsignee(storeData = null) {\u001b[39m\n\u001b[31m  const list = document.getElementById('consList');\u001b[39m\n\u001b[31m  const idx = list.children.length;\u001b[39m\n\u001b[31m  const card = document.createElement('div');\u001b[39m\n\u001b[31m  card.className = 'cons-card';\u001b[39m\n\u001b[31m  card.dataset.idx = idx;\u001b[39m\n\u001b[31m  const s = storeData || {};\u001b[39m\n\u001b[31m  card.innerHTML = `\u001b[39m\n\u001b[31m    <div class=\\\"cons-hdr\\\">\u001b[39m\n\u001b[31m      <span class=\\\"cons-num\\\">CONSIGNEE #${idx+1}</span>\u001b[39m\n\u001b[31m      <button class=\\\"cons-del\\\" onclick=\\\"this.closest('.cons-card').remove();renumberCons()\\\">✕</button>\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m    <div class=\\\"row2\\\">\u001b[39m\n\u001b[31m      <div class=\\\"f\\\">\u001b[39m\n\u001b[31m        <label>Store / Name <span class=\\\"req\\\">*</span></label>\u001b[39m\n\u001b[31m        <input type=\\\"text\\\" class=\\\"cons-name\\\" placeholder=\\\"Store name\\\" value=\\\"${s.name||''}\\\">\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      <div class=\\\"f\\\">\u001b[39m\n\u001b[31m        <label>City <span class=\\\"req\\\">*</span></label>\u001b[39m\n\u001b[31m        <input type=\\\"text\\\" class=\\\"cons-city\\\" placeholder=\\\"City\\\" value=\\\"${s.city||''}\\\">\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m    <div class=\\\"f\\\">\u001b[39m\n\u001b[31m      <label>Address</label>\u001b[39m\n\u001b[31m      <input type=\\\"text\\\" class=\\\"cons-addr\\\" placeholder=\\\"Full address\\\" value=\\\"${s.address||''}\\\">\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m    <div class=\\\"row2\\\">\u001b[39m\n\u001b[31m      <div class=\\\"f\\\">\u001b[39m\n\u001b[31m        <label>Phone</label>\u001b[39m\n\u001b[31m        <input type=\\\"tel\\\" class=\\\"cons-phone\\\" placeholder=\\\"Mobile\\\" value=\\\"${s.phone||''}\\\">\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      <div class=\\\"f\\\">\u001b[39m\n\u001b[31m        <label>Pincode</label>\u001b[39m\n\u001b[31m        <input type=\\\"text\\\" class=\\\"cons-pin\\\" placeholder=\\\"6-digit\\\" maxlength=\\\"6\\\">\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m    <div class=\\\"f\\\">\u001b[39m\n\u001b[31m      <label>Material / Contents <span class=\\\"req\\\">*</span></label>\u001b[39m\n\u001b[31m      <input type=\\\"text\\\" class=\\\"cons-material\\\" placeholder=\\\"e.g. Standees, Posters\\\">\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m    <div class=\\\"row2\\\">\u001b[39m\n\u001b[31m      <div class=\\\"f\\\">\u001b[39m\n\u001b[31m        <label>Qty</label>\u001b[39m\n\u001b[31m        <input type=\\\"number\\\" class=\\\"cons-qty\\\" placeholder=\\\"0\\\" min=\\\"0\\\">\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      <div class=\\\"f\\\">\u001b[39m\n\u001b[31m        <label>Docket No.</label>\u001b[39m\n\u001b[31m        <input type=\\\"text\\\" class=\\\"cons-docket-input\\\" placeholder=\\\"Docket #\\\"\u001b[39m\n\u001b[31m          style=\\\"font-family:var(--fnt-mono);\\\" value=\\\"${s.docket||''}\\\"\u001b[39m\n\u001b[31m          oninput=\\\"onDocketInput(this)\\\">\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m    ${s.docket ? `<div class=\\\"cons-docket-linked\\\"><span>🔗</span><span class=\\\"dkt\\\">${s.docket}</span></div>` : ''}\u001b[39m\n\u001b[31m  `;\u001b[39m\n\u001b[31m  list.appendChild(card);\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction onDocketInput(input) {\u001b[39m\n\u001b[31m  const card = input.closest('.cons-card');\u001b[39m\n\u001b[31m  let linked = card.querySelector('.cons-docket-linked');\u001b[39m\n\u001b[31m  if (input.value.trim()) {\u001b[39m\n\u001b[31m    if (!linked) {\u001b[39m\n\u001b[31m      linked = document.createElement('div');\u001b[39m\n\u001b[31m      linked.className = 'cons-docket-linked';\u001b[39m\n\u001b[31m      card.appendChild(linked);\u001b[39m\n\u001b[31m    }\u001b[39m\n\u001b[31m    linked.innerHTML = `<span>🔗</span><span class=\\\"dkt\\\">${input.value}</span>`;\u001b[39m\n\u001b[31m  } else if (linked) {\u001b[39m\n\u001b[31m    linked.remove();\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction showLinkedDocket(card, docket) {\u001b[39m\n\u001b[31m  const input = card.querySelector('.cons-docket-input');\u001b[39m\n\u001b[31m  if (input) { input.value = docket; onDocketInput(input); }\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction renumberCons() {\u001b[39m\n\u001b[31m  document.querySelectorAll('#consList .cons-card').forEach((c,i) => {\u001b[39m\n\u001b[31m    c.querySelector('.cons-num').textContent = `CONSIGNEE #${i+1}`;\u001b[39m\n\u001b[31m    c.dataset.idx = i;\u001b[39m\n\u001b[31m  });\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31m// JOB REFERENCE\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31mfunction onClientChange() {\u001b[39m\n\u001b[31m  const val = document.getElementById('n_client').value;\u001b[39m\n\u001b[31m  document.getElementById('customClientField').style.display = val === '__c' ? 'block' : 'none';\u001b[39m\n\u001b[31m  updatePill();\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31mfunction updatePill() {\u001b[39m\n\u001b[31m  const client = document.getElementById('n_client').value;\u001b[39m\n\u001b[31m  const job = document.getElementById('n_jobref').value.trim();\u001b[39m\n\u001b[31m  const cName = client === '__c' ? document.getElementById('n_customClient').value : client;\u001b[39m\n\u001b[31m  const pill = document.getElementById('jobPill');\u001b[39m\n\u001b[31m  if (cName && job) {\u001b[39m\n\u001b[31m    document.getElementById('jobPillTxt').textContent = `${cName} › ${job}`;\u001b[39m\n\u001b[31m    pill.style.display = 'inline-flex';\u001b[39m\n\u001b[31m  } else {\u001b[39m\n\u001b[31m    pill.style.display = 'none';\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31mfunction clearJob() {\u001b[39m\n\u001b[31m  document.getElementById('n_client').value = '';\u001b[39m\n\u001b[31m  document.getElementById('n_jobref').value = '';\u001b[39m\n\u001b[31m  document.getElementById('jobPill').style.display = 'none';\u001b[39m\n\u001b[31m  document.getElementById('customClientField').style.display = 'none';\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31m// GATHER / SAVE\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31mfunction gatherDispatch() {\u001b[39m\n\u001b[31m  const client = document.getElementById('n_client').value;\u001b[39m\n\u001b[31m  const clientName = client === '__c' ? document.getElementById('n_customClient').value : client;\u001b[39m\n\u001b[31m  const consignees = [];\u001b[39m\n\u001b[31m  document.querySelectorAll('#consList .cons-card').forEach(card => {\u001b[39m\n\u001b[31m    const docket = card.querySelector('.cons-docket-input')?.value.trim() || '';\u001b[39m\n\u001b[31m    consignees.push({\u001b[39m\n\u001b[31m      name: card.querySelector('.cons-name')?.value || '',\u001b[39m\n\u001b[31m      city: card.querySelector('.cons-city')?.value || '',\u001b[39m\n\u001b[31m      address: card.querySelector('.cons-addr')?.value || '',\u001b[39m\n\u001b[31m      phone: card.querySelector('.cons-phone')?.value || '',\u001b[39m\n\u001b[31m      pincode: card.querySelector('.cons-pin')?.value || '',\u001b[39m\n\u001b[31m      material: card.querySelector('.cons-material')?.value || '',\u001b[39m\n\u001b[31m      qty: card.querySelector('.cons-qty')?.value || '',\u001b[39m\n\u001b[31m      docket,\u001b[39m\n\u001b[31m      status: docket ? 'booked' : 'pending',\u001b[39m\n\u001b[31m      lastEvent: '',\u001b[39m\n\u001b[31m      lastUpdated: ''\u001b[39m\n\u001b[31m    });\u001b[39m\n\u001b[31m  });\u001b[39m\n\u001b[31m  return {\u001b[39m\n\u001b[31m    id: 'DSP-' + Date.now(),\u001b[39m\n\u001b[31m    mode: 'manual',\u001b[39m\n\u001b[31m    client: clientName,\u001b[39m\n\u001b[31m    jobref: document.getElementById('n_jobref').value,\u001b[39m\n\u001b[31m    courier: document.getElementById('n_courier').value,\u001b[39m\n\u001b[31m    dispatchDate: document.getElementById('n_date').value,\u001b[39m\n\u001b[31m    remarks: document.getElementById('n_remarks').value,\u001b[39m\n\u001b[31m    consignees,\u001b[39m\n\u001b[31m    slipCount: slipOCRData.length,\u001b[39m\n\u001b[31m    status: 'active',\u001b[39m\n\u001b[31m    createdAt: new Date().toISOString()\u001b[39m\n\u001b[31m  };\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction validate(d) {\u001b[39m\n\u001b[31m  if (!d.client) { showToast('Select a client', 'err'); return false; }\u001b[39m\n\u001b[31m  if (!d.courier) { showToast('Select courier', 'err'); return false; }\u001b[39m\n\u001b[31m  if (!d.consignees.length) { showToast('Add at least one consignee', 'err'); return false; }\u001b[39m\n\u001b[31m  for (let i = 0; i < d.consignees.length; i++) {\u001b[39m\n\u001b[31m    const c = d.consignees[i];\u001b[39m\n\u001b[31m    if (!c.name || !c.city) {\u001b[39m\n\u001b[31m      showToast(`Consignee #${i+1} (${c.name||'unnamed'}) — name & city required`, 'err');\u001b[39m\n\u001b[31m      // Scroll to and highlight the problem consignee card\u001b[39m\n\u001b[31m      const cards = document.querySelectorAll('#consList .cons-card');\u001b[39m\n\u001b[31m      if (cards[i]) {\u001b[39m\n\u001b[31m        cards[i].scrollIntoView({ behavior: 'smooth', block: 'center' });\u001b[39m\n\u001b[31m        cards[i].style.borderColor = 'var(--red)';\u001b[39m\n\u001b[31m        cards[i].style.boxShadow = '0 0 0 2px rgba(239,68,68,.3)';\u001b[39m\n\u001b[31m        setTimeout(() => {\u001b[39m\n\u001b[31m          if (cards[i]) { cards[i].style.borderColor = ''; cards[i].style.boxShadow = ''; }\u001b[39m\n\u001b[31m        }, 3000);\u001b[39m\n\u001b[31m      }\u001b[39m\n\u001b[31m      return false;\u001b[39m\n\u001b[31m    }\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m  return true;\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction saveDraft() {\u001b[39m\n\u001b[31m  const d = gatherDispatch();\u001b[39m\n\u001b[31m  d.status = 'draft';\u001b[39m\n\u001b[31m  dispatches.unshift(d);\u001b[39m\n\u001b[31m  saveData();\u001b[39m\n\u001b[31m  updateHistCount();\u001b[39m\n\u001b[31m  showToast('Draft saved ✓', 'ok');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// ── VERIFICATION SCREEN ─────────────────────────────────\u001b[39m\n\u001b[31m// Known docket lengths per courier for validation\u001b[39m\n\u001b[31mconst DOCKET_LENGTHS = {\u001b[39m\n\u001b[31m  'SkyKing': { min: 9, max: 12 },\u001b[39m\n\u001b[31m  'DTDC':    { min: 8, max: 15 },\u001b[39m\n\u001b[31m  'Delhivery': { min: 10, max: 18 },\u001b[39m\n\u001b[31m  'Blue Dart': { min: 10, max: 13 },\u001b[39m\n\u001b[31m  'FedEx':   { min: 10, max: 20 },\u001b[39m\n\u001b[31m  'Ecom Express': { min: 10, max: 14 },\u001b[39m\n\u001b[31m  'India Post': { min: 13, max: 22 },\u001b[39m\n\u001b[31m};·\u001b[39m\n\u001b[31mfunction finaliseDispatch() {\u001b[39m\n\u001b[31m  const d = gatherDispatch();\u001b[39m\n\u001b[31m  if (!validate(d)) return;\u001b[39m\n\u001b[31m  showVerifyScreen(d);\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction showVerifyScreen(d) {\u001b[39m\n\u001b[31m  const pane = document.getElementById('verifyPane');\u001b[39m\n\u001b[31m  const list = document.getElementById('verifyList');\u001b[39m\n\u001b[31m  const summary = document.getElementById('verifySummary');·\u001b[39m\n\u001b[31m  // Build flags\u001b[39m\n\u001b[31m  const docketsSeen = {};\u001b[39m\n\u001b[31m  const courier = d.courier;\u001b[39m\n\u001b[31m  const lenRule = DOCKET_LENGTHS[courier];·\u001b[39m\n\u001b[31m  d.consignees.forEach((c, i) => {\u001b[39m\n\u001b[31m    if (c.docket) docketsSeen[c.docket] = (docketsSeen[c.docket] || 0) + 1;\u001b[39m\n\u001b[31m  });·\u001b[39m\n\u001b[31m  const total     = d.consignees.length;\u001b[39m\n\u001b[31m  const withDock  = d.consignees.filter(c => c.docket).length;\u001b[39m\n\u001b[31m  const dupes     = Object.values(docketsSeen).filter(v => v > 1).length;\u001b[39m\n\u001b[31m  const noDock    = total - withDock;\u001b[39m\n\u001b[31m  const badLen    = lenRule ? d.consignees.filter(c => c.docket &&\u001b[39m\n\u001b[31m    (c.docket.length < lenRule.min || c.docket.length > lenRule.max)).length : 0;·\u001b[39m\n\u001b[31m  // Summary bar\u001b[39m\n\u001b[31m  summary.innerHTML = `\u001b[39m\n\u001b[31m    <div class=\\\"vs-item\\\"><div class=\\\"vs-val\\\" style=\\\"color:var(--t1);\\\">${total}</div><div class=\\\"vs-lbl\\\">Total</div></div>\u001b[39m\n\u001b[31m    <div class=\\\"vs-item\\\"><div class=\\\"vs-val\\\" style=\\\"color:var(--grn);\\\">${withDock}</div><div class=\\\"vs-lbl\\\">Docketed</div></div>\u001b[39m\n\u001b[31m    <div class=\\\"vs-item\\\"><div class=\\\"vs-val\\\" style=\\\"color:${noDock > 0 ? 'var(--ylw)' : 'var(--t3)'};\\\">${noDock}</div><div class=\\\"vs-lbl\\\">No Docket</div></div>\u001b[39m\n\u001b[31m    <div class=\\\"vs-item\\\"><div class=\\\"vs-val\\\" style=\\\"color:${dupes > 0 ? 'var(--red)' : 'var(--t3)'};\\\">${dupes}</div><div class=\\\"vs-lbl\\\">Duplicates</div></div>\u001b[39m\n\u001b[31m    ${badLen > 0 ? `<div class=\\\"vs-item\\\"><div class=\\\"vs-val\\\" style=\\\"color:var(--sky);\\\">${badLen}</div><div class=\\\"vs-lbl\\\">Short/Long</div></div>` : ''}\u001b[39m\n\u001b[31m    <div class=\\\"vs-item\\\" style=\\\"margin-left:auto;text-align:right;\\\">\u001b[39m\n\u001b[31m      <div class=\\\"vs-val\\\" style=\\\"font-size:.8rem;color:var(--t2);\\\">${d.courier||'—'}</div>\u001b[39m\n\u001b[31m      <div class=\\\"vs-lbl\\\">${d.client||'—'} ${d.jobref ? '› '+d.jobref : ''}</div>\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m  `;·\u001b[39m\n\u001b[31m  // Build consignee rows\u001b[39m\n\u001b[31m  list.innerHTML = d.consignees.map((c, i) => {\u001b[39m\n\u001b[31m    const isDupe   = c.docket && docketsSeen[c.docket] > 1;\u001b[39m\n\u001b[31m    const isNoDock = !c.docket;\u001b[39m\n\u001b[31m    const isShort  = lenRule && c.docket &&\u001b[39m\n\u001b[31m      (c.docket.length < lenRule.min || c.docket.length > lenRule.max);·\u001b[39m\n\u001b[31m    const rowClass = isDupe ? 'has-dupe' : (isNoDock ? 'no-docket' : 'ok');·\u001b[39m\n\u001b[31m    // Find matching OCR slip for thumbnail\u001b[39m\n\u001b[31m    const ocrMatch = slipOCRData[i];\u001b[39m\n\u001b[31m    const thumbHtml = ocrMatch?.imageData\u001b[39m\n\u001b[31m      ? `<img class=\\\"verify-thumb\\\" src=\\\"${ocrMatch.imageData}\\\" onclick=\\\"showSlipFull(${i})\\\" title=\\\"View slip\\\">`\u001b[39m\n\u001b[31m      : '';·\u001b[39m\n\u001b[31m    // Flags\u001b[39m\n\u001b[31m    let flags = '';\u001b[39m\n\u001b[31m    if (isDupe)   flags += `<span class=\\\"verify-flag vf-dupe\\\">⚠ Duplicate</span> `;\u001b[39m\n\u001b[31m    if (isNoDock) flags += `<span class=\\\"verify-flag vf-nodocket\\\">No Docket</span> `;\u001b[39m\n\u001b[31m    if (isShort)  flags += `<span class=\\\"verify-flag vf-short\\\">${c.docket.length} digits</span> `;\u001b[39m\n\u001b[31m    if (!isDupe && !isNoDock && !isShort) flags = `<span class=\\\"verify-flag vf-ok\\\">✓ OK</span>`;·\u001b[39m\n\u001b[31m    // OCR confidence note\u001b[39m\n\u001b[31m    const confNote = ocrMatch && ocrMatch.confidence\u001b[39m\n\u001b[31m      ? `<div class=\\\"ocr-correction-note\\\">OCR confidence: ${Math.round(ocrMatch.confidence*100)}% via ${ocrMatch.source||'—'}</div>` : '';·\u001b[39m\n\u001b[31m    // WhatsApp URL\u001b[39m\n\u001b[31m    const waMsg = encodeURIComponent(\u001b[39m\n\u001b[31m      `Dear ${c.name}, your material has been dispatched via ${d.courier}. Docket: ${c.docket||'TBD'}. Track: ${(COURIER_MASTER[d.courier]?.trackUrl||'').replace('{docket}', c.docket||'')}`\u001b[39m\n\u001b[31m    );\u001b[39m\n\u001b[31m    const waUrl = c.phone ? `https://wa.me/91${c.phone.replace(/\\\\D/g,'')}?text=${waMsg}` : '';·\u001b[39m\n\u001b[31m    return `\u001b[39m\n\u001b[31m    <div class=\\\"verify-row ${rowClass}\\\" id=\\\"vrow_${i}\\\">\u001b[39m\n\u001b[31m      <div class=\\\"verify-row-hdr\\\">\u001b[39m\n\u001b[31m        <span class=\\\"verify-num\\\">#${i+1}</span>\u001b[39m\n\u001b[31m        <span class=\\\"verify-store\\\">${c.name||'—'}</span>\u001b[39m\n\u001b[31m        ${flags}\u001b[39m\n\u001b[31m        ${thumbHtml}\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      <div class=\\\"verify-fields\\\" style=\\\"grid-template-columns:1fr 1fr 1fr;\\\">\u001b[39m\n\u001b[31m        <div class=\\\"verify-field\\\">\u001b[39m\n\u001b[31m          <label>Docket No. <span style=\\\"color:var(--acc);\\\">*</span></label>\u001b[39m\n\u001b[31m          <input type=\\\"text\\\" value=\\\"${c.docket||''}\\\" oninput=\\\"updateVerifyField(${i},'docket',this)\\\"\u001b[39m\n\u001b[31m            style=\\\"font-family:var(--fnt-mono);${isDupe||isShort?'border-color:var(--red);':!c.docket?'border-color:var(--ylw);':'color:var(--acc);'}\\\">\u001b[39m\n\u001b[31m          ${confNote}\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m        <div class=\\\"verify-field\\\">\u001b[39m\n\u001b[31m          <label>City</label>\u001b[39m\n\u001b[31m          <input type=\\\"text\\\" value=\\\"${c.city||''}\\\" oninput=\\\"updateVerifyField(${i},'city',this)\\\"\u001b[39m\n\u001b[31m            style=\\\"${!c.city ? 'border-color:var(--ylw);' : ''}\\\">\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m        <div class=\\\"verify-field\\\">\u001b[39m\n\u001b[31m          <label>Pincode</label>\u001b[39m\n\u001b[31m          <input type=\\\"text\\\" value=\\\"${c.pincode||''}\\\" oninput=\\\"updateVerifyField(${i},'pincode',this)\\\"\u001b[39m\n\u001b[31m            style=\\\"font-family:var(--fnt-mono);\\\">\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m        <div class=\\\"verify-field\\\">\u001b[39m\n\u001b[31m          <label>Phone</label>\u001b[39m\n\u001b[31m          <input type=\\\"tel\\\" value=\\\"${c.phone||''}\\\" oninput=\\\"updateVerifyField(${i},'phone',this)\\\">\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m        <div class=\\\"verify-field\\\">\u001b[39m\n\u001b[31m          <label>Material / Contents</label>\u001b[39m\n\u001b[31m          <input type=\\\"text\\\" value=\\\"${c.material||''}\\\" oninput=\\\"updateVerifyField(${i},'material',this)\\\">\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m        <div class=\\\"verify-field\\\">\u001b[39m\n\u001b[31m          <label>Street Address</label>\u001b[39m\n\u001b[31m          <input type=\\\"text\\\" value=\\\"${c.address||''}\\\" oninput=\\\"updateVerifyField(${i},'address',this)\\\"\u001b[39m\n\u001b[31m            placeholder=\\\"Street / locality only\\\">\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      <div class=\\\"verify-actions\\\">\u001b[39m\n\u001b[31m        ${c.docket && c.phone ? `<button class=\\\"btn-wa\\\" onclick=\\\"window.open('${waUrl}','_blank')\\\">📱 WhatsApp</button>` : ''}\u001b[39m\n\u001b[31m        <button class=\\\"btn-sm\\\" onclick=\\\"removeVerifyRow(${i})\\\">✕ Remove</button>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m    </div>`;\u001b[39m\n\u001b[31m  }).join('');·\u001b[39m\n\u001b[31m  // Store dispatch object for confirm\u001b[39m\n\u001b[31m  pane._pendingDispatch = d;\u001b[39m\n\u001b[31m  pane.style.display = 'block';\u001b[39m\n\u001b[31m  pane.scrollTop = 0;\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction updateVerifyField(idx, field, input) {\u001b[39m\n\u001b[31m  const pane = document.getElementById('verifyPane');\u001b[39m\n\u001b[31m  if (!pane._pendingDispatch) return;\u001b[39m\n\u001b[31m  const c = pane._pendingDispatch.consignees[idx];\u001b[39m\n\u001b[31m  if (!c) return;\u001b[39m\n\u001b[31m  c[field] = input.value.trim();\u001b[39m\n\u001b[31m  input.classList.toggle('changed', input.value.trim() !== '');\u001b[39m\n\u001b[31m  // Re-check dupe if docket changed\u001b[39m\n\u001b[31m  if (field === 'docket') refreshVerifyFlags(pane._pendingDispatch);\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction refreshVerifyFlags(d) {\u001b[39m\n\u001b[31m  const seen = {};\u001b[39m\n\u001b[31m  d.consignees.forEach(c => { if (c.docket) seen[c.docket] = (seen[c.docket]||0)+1; });\u001b[39m\n\u001b[31m  d.consignees.forEach((c, i) => {\u001b[39m\n\u001b[31m    const row = document.getElementById('vrow_' + i);\u001b[39m\n\u001b[31m    if (!row) return;\u001b[39m\n\u001b[31m    const isDupe = c.docket && seen[c.docket] > 1;\u001b[39m\n\u001b[31m    row.classList.toggle('has-dupe', isDupe);\u001b[39m\n\u001b[31m  });\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction removeVerifyRow(idx) {\u001b[39m\n\u001b[31m  const pane = document.getElementById('verifyPane');\u001b[39m\n\u001b[31m  if (!pane._pendingDispatch) return;\u001b[39m\n\u001b[31m  const el = document.getElementById('vrow_' + idx);\u001b[39m\n\u001b[31m  if (el) {\u001b[39m\n\u001b[31m    el.style.transition = 'opacity .25s';\u001b[39m\n\u001b[31m    el.style.opacity = '0';\u001b[39m\n\u001b[31m    setTimeout(() => {\u001b[39m\n\u001b[31m      pane._pendingDispatch.consignees.splice(idx, 1);\u001b[39m\n\u001b[31m      showVerifyScreen(pane._pendingDispatch); // re-render with updated indices\u001b[39m\n\u001b[31m    }, 250);\u001b[39m\n\u001b[31m  } else {\u001b[39m\n\u001b[31m    pane._pendingDispatch.consignees.splice(idx, 1);\u001b[39m\n\u001b[31m    showVerifyScreen(pane._pendingDispatch);\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction approveAll() {\u001b[39m\n\u001b[31m  showToast('All rows approved ✓', 'ok');\u001b[39m\n\u001b[31m  document.getElementById('btnConfirmFinalise').style.background = 'var(--grn)';\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction closeVerify() {\u001b[39m\n\u001b[31m  const pane = document.getElementById('verifyPane');\u001b[39m\n\u001b[31m  // If this dispatch was already finalised, warn before going back\u001b[39m\n\u001b[31m  if (_lastFinalisedId && pane._pendingDispatch?.id === _lastFinalisedId) {\u001b[39m\n\u001b[31m    if (!confirm(\u001b[39m\n\u001b[31m      'This dispatch has already been finalised.\\\\n\\\\n' +\u001b[39m\n\u001b[31m      'Going back will take you to the edit form — any changes will create a NEW dispatch record.\\\\n\\\\n' +\u001b[39m\n\u001b[31m      'Go back anyway?'\u001b[39m\n\u001b[31m    )) return;\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m  pane.style.display = 'none';\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction showSlipFull(idx) {\u001b[39m\n\u001b[31m  const s = slipOCRData[idx];\u001b[39m\n\u001b[31m  if (!s?.imageData) return;\u001b[39m\n\u001b[31m  const w = window.open('');\u001b[39m\n\u001b[31m  w.document.write(`<img src=\\\"${s.imageData}\\\" style=\\\"max-width:100%;height:auto;\\\">`);\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// Log OCR correction to sheets for future accuracy improvement\u001b[39m\n\u001b[31masync function logOCRCorrection(original, corrected, courier) {\u001b[39m\n\u001b[31m  if (!window.appConfig.webhook_url || original === corrected) return;\u001b[39m\n\u001b[31m  try {\u001b[39m\n\u001b[31m    await fetch(window.appConfig.webhook_url, {\u001b[39m\n\u001b[31m      method: 'POST', mode: 'no-cors',\u001b[39m\n\u001b[31m      headers: { 'Content-Type': 'application/json' },\u001b[39m\n\u001b[31m      body: JSON.stringify({\u001b[39m\n\u001b[31m        type: 'ocr_correction',\u001b[39m\n\u001b[31m        original, corrected, courier,\u001b[39m\n\u001b[31m        timestamp: new Date().toISOString()\u001b[39m\n\u001b[31m      })\u001b[39m\n\u001b[31m    });\u001b[39m\n\u001b[31m  } catch(e) {}\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mlet _lastFinalisedId = null; // track last finalised dispatch to warn on back·\u001b[39m\n\u001b[31masync function confirmAndFinalise() {\u001b[39m\n\u001b[31m  const pane = document.getElementById('verifyPane');\u001b[39m\n\u001b[31m  const d = pane._pendingDispatch;\u001b[39m\n\u001b[31m  if (!d) return;·\u001b[39m\n\u001b[31m  d.status = 'active';\u001b[39m\n\u001b[31m  d.finalisedAt = new Date().toISOString();·\u001b[39m\n\u001b[31m  // Log OCR corrections\u001b[39m\n\u001b[31m  slipOCRData.forEach((s, i) => {\u001b[39m\n\u001b[31m    const c = d.consignees[i];\u001b[39m\n\u001b[31m    if (c && s.docket && c.docket && s.docket !== c.docket) {\u001b[39m\n\u001b[31m      logOCRCorrection(s.docket, c.docket, d.courier);\u001b[39m\n\u001b[31m    }\u001b[39m\n\u001b[31m  });·\u001b[39m\n\u001b[31m  // Replace existing draft with same ID if present, else prepend\u001b[39m\n\u001b[31m  const existingIdx = dispatches.findIndex(x => x.id === d.id);\u001b[39m\n\u001b[31m  if (existingIdx >= 0) {\u001b[39m\n\u001b[31m    dispatches[existingIdx] = d;\u001b[39m\n\u001b[31m  } else {\u001b[39m\n\u001b[31m    dispatches.unshift(d);\u001b[39m\n\u001b[31m  }·\u001b[39m\n\u001b[31m  _lastFinalisedId = d.id;\u001b[39m\n\u001b[31m  saveData();\u001b[39m\n\u001b[31m  syncDispatchToDB(d); // push to Postgres (non-blocking)\u001b[39m\n\u001b[31m  updateHistCount();\u001b[39m\n\u001b[31m  if (window.appConfig.auto_log !== 'false' && window.appConfig.webhook_url) await logToSheets(d);\u001b[39m\n\u001b[31m  showToast('Dispatch finalised ✓', 'ok');\u001b[39m\n\u001b[31m  pane.style.display = 'none';\u001b[39m\n\u001b[31m  pane._pendingDispatch = null; // clear pending\u001b[39m\n\u001b[31m  resetForm();\u001b[39m\n\u001b[31m  showPane('dashboard');\u001b[39m\n\u001b[31m  renderDashboard();\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction resetForm() {\u001b[39m\n\u001b[31m  document.getElementById('n_client').value = '';\u001b[39m\n\u001b[31m  document.getElementById('n_jobref').value = '';\u001b[39m\n\u001b[31m  document.getElementById('n_courier').value = '';\u001b[39m\n\u001b[31m  document.getElementById('n_remarks').value = '';\u001b[39m\n\u001b[31m  document.getElementById('jobPill').style.display = 'none';\u001b[39m\n\u001b[31m  document.getElementById('customClientField').style.display = 'none';\u001b[39m\n\u001b[31m  document.getElementById('consList').innerHTML = '';\u001b[39m\n\u001b[31m  addConsignee();\u001b[39m\n\u001b[31m  clearSlips();\u001b[39m\n\u001b[31m  clearStores();\u001b[39m\n\u001b[31m  setToday();\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction newDispatch() { resetForm(); showPane('new'); }·\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31m// DASHBOARD\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31mfunction renderDashboard() {\u001b[39m\n\u001b[31m  // Exclude drafts from dashboard — only show active/finalised dispatches\u001b[39m\n\u001b[31m  const activeDis = dispatches.filter(d => d.status !== 'draft');\u001b[39m\n\u001b[31m  const allCons = activeDis.flatMap(d => d.consignees || []);\u001b[39m\n\u001b[31m  const countBy = (status) => allCons.filter(c => c.status === status).length;\u001b[39m\n\u001b[31m  document.getElementById('st-delivered').textContent = countBy('delivered');\u001b[39m\n\u001b[31m  document.getElementById('st-transit').textContent = countBy('transit') + countBy('in-transit');\u001b[39m\n\u001b[31m  document.getElementById('st-booked').textContent = countBy('booked');\u001b[39m\n\u001b[31m  document.getElementById('st-pending').textContent = allCons.filter(c => !c.docket || c.status === 'pending').length;·\u001b[39m\n\u001b[31m  const grid = document.getElementById('dashGrid');\u001b[39m\n\u001b[31m  // Flatten all consignees across dispatches\u001b[39m\n\u001b[31m  let items = activeDis.flatMap(d =>\u001b[39m\n\u001b[31m    (d.consignees || []).filter(c => c.docket).map(c => ({...c, dispatchId: d.id, courier: d.courier, client: d.client, jobref: d.jobref}))\u001b[39m\n\u001b[31m  );\u001b[39m\n\u001b[31m  if (dashFilter !== 'all') {\u001b[39m\n\u001b[31m    items = items.filter(c => {\u001b[39m\n\u001b[31m      if (dashFilter === 'delivered') return c.status === 'delivered';\u001b[39m\n\u001b[31m      if (dashFilter === 'transit') return c.status === 'transit' || c.status === 'in-transit';\u001b[39m\n\u001b[31m      if (dashFilter === 'booked') return c.status === 'booked';\u001b[39m\n\u001b[31m      if (dashFilter === 'pending') return !c.docket || c.status === 'pending';\u001b[39m\n\u001b[31m      return true;\u001b[39m\n\u001b[31m    });\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m  if (!items.length) {\u001b[39m\n\u001b[31m    grid.innerHTML = '<div class=\\\"empty\\\"><div class=\\\"e-icon\\\">📦</div><p>No docket-tracked consignees.<br>Add docket numbers to see tiles.</p></div>';\u001b[39m\n\u001b[31m    return;\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m  const progMap = { delivered:100, transit:60,'in-transit':60, booked:20, pending:5 };\u001b[39m\n\u001b[31m  const progClr = { delivered:'var(--grn)', transit:'var(--ylw)','in-transit':'var(--ylw)', booked:'var(--sky)', pending:'var(--t4)' };\u001b[39m\n\u001b[31m  grid.innerHTML = items.map(c => {\u001b[39m\n\u001b[31m    const sp = c.status || 'booked';\u001b[39m\n\u001b[31m    const prog = progMap[sp] || 20;\u001b[39m\n\u001b[31m    const clr = progClr[sp] || 'var(--t4)';\u001b[39m\n\u001b[31m    return `\u001b[39m\n\u001b[31m    <div class=\\\"docket-tile\\\" onclick=\\\"showDocketDetail('${c.docket}','${c.courier||''}','${c.dispatchId}')\\\">\u001b[39m\n\u001b[31m      <div class=\\\"docket-tile-hdr\\\">\u001b[39m\n\u001b[31m        <div class=\\\"docket-tile-left\\\">\u001b[39m\n\u001b[31m          <div class=\\\"docket-num\\\">${c.docket}</div>\u001b[39m\n\u001b[31m          <div class=\\\"docket-store\\\">${c.name} — ${c.city}</div>\u001b[39m\n\u001b[31m          <div class=\\\"docket-courier\\\">${c.courier||''} · ${c.client||''} ${c.jobref ? '› '+c.jobref : ''}</div>\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m        <span class=\\\"status-pill sp-${sp}\\\">${sp}</span>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      <div style=\\\"display:flex;gap:6px;margin-bottom:6px;align-items:center;\\\">\u001b[39m\n\u001b[31m        <input type=\\\"checkbox\\\" id=\\\"sel_${c.docket.replace(/[^a-z0-9]/gi,'_')}\\\"\u001b[39m\n\u001b[31m          onclick=\\\"event.stopPropagation();toggleTileSelect('${c.docket}')\\\"\u001b[39m\n\u001b[31m          style=\\\"width:14px;height:14px;cursor:pointer;accent-color:var(--acc);\\\">\u001b[39m\n\u001b[31m        <label style=\\\"font-size:.6rem;color:var(--t3);font-family:var(--fnt-mono);cursor:pointer;\\\"\u001b[39m\n\u001b[31m          onclick=\\\"event.stopPropagation();toggleTileSelect('${c.docket}')\\\">Select</label>\u001b[39m\n\u001b[31m        ${c.manualStatus ?\u001b[39m\n\u001b[31m          `<span style=\\\"font-size:.6rem;font-family:var(--fnt-mono);color:var(--grn);\\\">📞 ${c.manualStatus}</span>` : ''}\u001b[39m\n\u001b[31m        ${c.podUploaded ? '<span style=\\\"font-size:.6rem;color:var(--grn);font-family:var(--fnt-mono);\\\">✓ POD</span>' : ''}\u001b[39m\n\u001b[31m        <button onclick=\\\"event.stopPropagation();showManualTracking('${c.docket}','${c.dispatchId}','${c.name}','${c.phone||''}','${c.city||''}')\\\"\u001b[39m\n\u001b[31m          style=\\\"font-size:.6rem;background:rgba(56,189,248,.1);border:1px solid rgba(56,189,248,.3);\u001b[39m\n\u001b[31m          color:var(--sky);padding:2px 7px;border-radius:10px;cursor:pointer;font-family:var(--fnt-mono);\\\">📞 Call Log</button>\u001b[39m\n\u001b[31m        <button onclick=\\\"event.stopPropagation();showPODUpload('${c.docket}','${c.dispatchId}')\\\"\u001b[39m\n\u001b[31m          style=\\\"font-size:.6rem;background:none;border:1px solid var(--bdr);color:var(--t3);\u001b[39m\n\u001b[31m          padding:2px 7px;border-radius:10px;cursor:pointer;font-family:var(--fnt-mono);\\\">📸 POD</button>\u001b[39m\n\u001b[31m        ${c.manualDelivered ? `<button onclick=\\\"event.stopPropagation();resumeAutoTracking('${c.docket}','${c.dispatchId}')\\\"\u001b[39m\n\u001b[31m          style=\\\"font-size:.6rem;background:rgba(251,146,60,.1);border:1px solid rgba(251,146,60,.4);\u001b[39m\n\u001b[31m          color:#fb923c;padding:2px 7px;border-radius:10px;cursor:pointer;font-family:var(--fnt-mono);\\\">↩ Undo</button>` : ''}\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      <div class=\\\"docket-tile-footer\\\">\u001b[39m\n\u001b[31m        <div style=\\\"flex:1;min-width:0;\\\">\u001b[39m\n\u001b[31m          <span class=\\\"docket-last-event docket-evt-${c.docket}\\\" id=\\\"evt_${c.docket.replace(/[^a-z0-9]/gi,'_')}\\\">${c.lastEvent || 'No tracking yet'}</span>\u001b[39m\n\u001b[31m          <span class=\\\"track-spinner\\\" id=\\\"spin_${c.docket.replace(/[^a-z0-9]/gi,'_')}\\\" style=\\\"display:none;color:var(--ylw);font-family:var(--fnt-mono);font-size:.68rem;animation:blink 1s infinite;\\\">⏳ tracking...</span>\u001b[39m\n\u001b[31m          ${c.lastScanLocation && c.lastScanLocation !== c.lastEvent ?·\u001b[39m\n\u001b[31m            `<div style=\\\"font-size:.62rem;color:var(--t3);font-family:var(--fnt-mono);margin-top:2px;\\\">📍 ${c.lastScanLocation}</div>` : ''}\u001b[39m\n\u001b[31m          ${c.lastTrackedAt ?·\u001b[39m\n\u001b[31m            `<div style=\\\"font-size:.6rem;color:var(--t4);font-family:var(--fnt-mono);margin-top:1px;\\\">🕐 tracked ${new Date(c.lastTrackedAt).toLocaleTimeString('en-IN',{hour:'2-digit',minute:'2-digit'})}</div>` : ''}\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m        <button class=\\\"btn-track-live live-btn-${c.docket.replace(/[^a-z0-9]/gi,'_')}\\\" onclick=\\\"event.stopPropagation();liveTrackTile('${c.docket}','${c.courier||''}','${c.dispatchId}')\\\">\u001b[39m\n\u001b[31m          <span class=\\\"live-dot\\\"></span>LIVE\u001b[39m\n\u001b[31m        </button>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      <div class=\\\"docket-progress\\\">\u001b[39m\n\u001b[31m        <div class=\\\"docket-progress-fill\\\" style=\\\"width:${prog}%;background:${clr};\\\"></div>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m    </div>`;\u001b[39m\n\u001b[31m  }).join('');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction filterDash(f, el) {\u001b[39m\n\u001b[31m  dashFilter = f;\u001b[39m\n\u001b[31m  document.querySelectorAll('#dashFilters .btn-sm').forEach(b => b.classList.remove('primary'));\u001b[39m\n\u001b[31m  el.classList.add('primary');\u001b[39m\n\u001b[31m  renderDashboard();\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31masync function liveTrackTile(docket, courier, dispatchId) {\u001b[39m\n\u001b[31m  showToast(`Tracking ${docket}...`, 'info');\u001b[39m\n\u001b[31m  const result = await fetchTracking(docket, courier);\u001b[39m\n\u001b[31m  // Update status in dispatches\u001b[39m\n\u001b[31m  for (const d of dispatches) {\u001b[39m\n\u001b[31m    if (d.id !== dispatchId) continue;\u001b[39m\n\u001b[31m    for (const c of d.consignees) {\u001b[39m\n\u001b[31m      if (c.docket === docket) {\u001b[39m\n\u001b[31m        c.status = result.status || c.status;\u001b[39m\n\u001b[31m        c.lastEvent = result.lastEvent || c.lastEvent;\u001b[39m\n\u001b[31m        c.lastUpdated = new Date().toISOString();\u001b[39m\n\u001b[31m      }\u001b[39m\n\u001b[31m    }\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m  saveData();\u001b[39m\n\u001b[31m  renderDashboard();\u001b[39m\n\u001b[31m  if (result.status === 'failed') {\u001b[39m\n\u001b[31m    // Open courier site as fallback\u001b[39m\n\u001b[31m    openCourierTrackUrl(docket, courier);\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction showDocketDetail(docket, courier, dispatchId) {\u001b[39m\n\u001b[31m  const d = dispatches.find(x => x.id === dispatchId);\u001b[39m\n\u001b[31m  const c = d?.consignees?.find(x => x.docket === docket);\u001b[39m\n\u001b[31m  if (!c) return;\u001b[39m\n\u001b[31m  const content = document.getElementById('detailContent');\u001b[39m\n\u001b[31m  content.innerHTML = `\u001b[39m\n\u001b[31m    <div class=\\\"sheet-title\\\">${docket}</div>\u001b[39m\n\u001b[31m    <div class=\\\"info-tag\\\">📍 <strong>${c.name}</strong> — ${c.city}</div>\u001b[39m\n\u001b[31m    ${c.phone ? `<div class=\\\"info-tag\\\">📞 ${c.phone}</div>` : ''}\u001b[39m\n\u001b[31m    <div class=\\\"info-tag\\\">📦 ${c.material||'—'} ${c.qty ? '('+c.qty+' pcs)':''}</div>\u001b[39m\n\u001b[31m    <div class=\\\"info-tag\\\">🚚 ${courier||'—'} · <span class=\\\"status-pill sp-${c.status||'pending'}\\\">${c.status||'pending'}</span></div>\u001b[39m\n\u001b[31m    ${c.lastEvent ? `<div class=\\\"info-tag\\\">🕐 ${c.lastEvent}</div>` : ''}\u001b[39m\n\u001b[31m    <div class=\\\"sec-div\\\"><span>Tracking History</span></div>\u001b[39m\n\u001b[31m    <div id=\\\"docketTimeline\\\">\u001b[39m\n\u001b[31m      ${c.timeline?.length ?\u001b[39m\n\u001b[31m        c.timeline.map(e => `<div class=\\\"track-evt\\\"><span class=\\\"track-time\\\">${e.time||''}</span><div><div class=\\\"track-desc\\\">${e.desc||''}</div><div class=\\\"track-loc\\\">${e.loc||''}</div></div></div>`).join('') :\u001b[39m\n\u001b[31m        '<div style=\\\"color:var(--t3);font-size:.78rem;padding:10px 0;\\\">No timeline data. Click Live Track to fetch.</div>'}\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m    <div style=\\\"display:flex;gap:6px;flex-wrap:wrap;margin-top:14px;\\\">\u001b[39m\n\u001b[31m      <button class=\\\"btn btn-ghost\\\" style=\\\"flex:1;\\\" onclick=\\\"openCourierTrackUrl('${docket}','${courier}')\\\">🔗 Courier Site</button>\u001b[39m\n\u001b[31m      <button class=\\\"btn btn-orange\\\" style=\\\"flex:1;\\\" onclick=\\\"liveTrackTile('${docket}','${courier}','${dispatchId}');closeOverlay('detailOverlay')\\\">⚡ Live Track</button>\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m    <div style=\\\"display:flex;gap:6px;flex-wrap:wrap;margin-top:6px;\\\">\u001b[39m\n\u001b[31m      <button class=\\\"btn-wa\\\" style=\\\"flex:1;padding:8px;\\\" onclick=\\\"closeOverlay('detailOverlay');showPODUpload('${docket}','${dispatchId}')\\\">📸 Upload POD</button>\u001b[39m\n\u001b[31m      ${c.podUploaded ? '<span style=\\\"color:var(--grn);font-size:.72rem;font-family:var(--fnt-mono);padding:6px;\\\">✓ POD uploaded</span>' : ''}\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m  `;\u001b[39m\n\u001b[31m  openOverlay('detailOverlay');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31m// TRACKING ENGINE\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31mfunction _normaliseStatus(raw) {\u001b[39m\n\u001b[31m  const s = (raw || '').toLowerCase().trim();\u001b[39m\n\u001b[31m  if (!s || s === 'error') return null;\u001b[39m\n\u001b[31m  if (s.includes('undeliver') || s.includes('delivery_fail') ||\u001b[39m\n\u001b[31m      s.includes('failed_delivery') || s.includes('not deliver') ||\u001b[39m\n\u001b[31m      s.includes('return')  || s.includes('rto')) return 'pending';\u001b[39m\n\u001b[31m  if (s.includes('deliver'))                       return 'delivered';\u001b[39m\n\u001b[31m  if (s.includes('transit') || s.includes('hub') ||\u001b[39m\n\u001b[31m      s.includes('inbound') || s.includes('outbound') ||\u001b[39m\n\u001b[31m      s.includes('out for'))                       return 'transit';\u001b[39m\n\u001b[31m  if (s.includes('book') || s.includes('pick'))   return 'booked';\u001b[39m\n\u001b[31m  return s;\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31masync function _fetchWithRetry(url, opts, maxRetries = 1, retryDelayMs = 4000) {\u001b[39m\n\u001b[31m  for (let attempt = 0; attempt <= maxRetries; attempt++) {\u001b[39m\n\u001b[31m    try {\u001b[39m\n\u001b[31m      const resp = await fetch(url, opts);\u001b[39m\n\u001b[31m      if (resp.ok) return resp;\u001b[39m\n\u001b[31m    } catch(e) {\u001b[39m\n\u001b[31m      if (attempt === maxRetries) throw e;\u001b[39m\n\u001b[31m      await new Promise(r => setTimeout(r, retryDelayMs));\u001b[39m\n\u001b[31m    }\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m  return null;\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31masync function fetchTracking(docket, courier) {\u001b[39m\n\u001b[31m  // Strategy 1: VPS proxy (SkyKing + others that block direct fetch)\u001b[39m\n\u001b[31m  const proxyUrl = window.appConfig.proxy_url;\u001b[39m\n\u001b[31m  if (proxyUrl) {\u001b[39m\n\u001b[31m    try {\u001b[39m\n\u001b[31m      const url  = `${proxyUrl}?courier=${encodeURIComponent(courier)}&docket=${encodeURIComponent(docket)}`;\u001b[39m\n\u001b[31m      const resp = await _fetchWithRetry(url, { signal: AbortSignal.timeout(15000) });\u001b[39m\n\u001b[31m      if (resp) {\u001b[39m\n\u001b[31m        const data      = await resp.json();\u001b[39m\n\u001b[31m        const normStatus = _normaliseStatus(data.status);\u001b[39m\n\u001b[31m        if (normStatus) {\u001b[39m\n\u001b[31m          return { status: normStatus, lastEvent: data.lastEvent || '', timeline: data.timeline || [] };\u001b[39m\n\u001b[31m        }\u001b[39m\n\u001b[31m        openCourierTrackUrl(docket, courier);\u001b[39m\n\u001b[31m        return { status: 'transit', lastEvent: data.lastEvent || 'Check courier site', timeline: [] };\u001b[39m\n\u001b[31m      }\u001b[39m\n\u001b[31m    } catch(e) {}\u001b[39m\n\u001b[31m  }·\u001b[39m\n\u001b[31m  // Strategy 2: Delhivery direct API\u001b[39m\n\u001b[31m  if (courier === 'Delhivery') {\u001b[39m\n\u001b[31m    try {\u001b[39m\n\u001b[31m      const resp = await _fetchWithRetry(\u001b[39m\n\u001b[31m        `https://dlv-api.delhivery.com/v3/track?waybill=${docket}`,\u001b[39m\n\u001b[31m        { signal: AbortSignal.timeout(8000) }\u001b[39m\n\u001b[31m      );\u001b[39m\n\u001b[31m      if (resp) {\u001b[39m\n\u001b[31m        const data = await resp.json();\u001b[39m\n\u001b[31m        const pkg  = data.ShipmentData?.[0]?.Shipment;\u001b[39m\n\u001b[31m        if (pkg) {\u001b[39m\n\u001b[31m          const normStatus = _normaliseStatus(pkg.Status?.Status);\u001b[39m\n\u001b[31m          return { status: normStatus || 'transit', lastEvent: pkg.Status?.Instructions || '', timeline: [] };\u001b[39m\n\u001b[31m        }\u001b[39m\n\u001b[31m      }\u001b[39m\n\u001b[31m    } catch(e) {}\u001b[39m\n\u001b[31m  }·\u001b[39m\n\u001b[31m  return { status: 'failed', lastEvent: 'Open courier site to check', timeline: [] };\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction openCourierTrackUrl(docket, courier) {\u001b[39m\n\u001b[31m  const m = COURIER_MASTER[courier];\u001b[39m\n\u001b[31m  if (!m) return;\u001b[39m\n\u001b[31m  const url = m.trackUrl.replace('{docket}', docket);\u001b[39m\n\u001b[31m  window.open(url, '_blank');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// Track tab - multiple dockets\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31m// TRACK TAB — new logic\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31mlet _trackSelected = new Set(); // dockets currently selected for tracking·\u001b[39m\n\u001b[31m// Called when Track tab opens or courier changes\u001b[39m\n\u001b[31mfunction buildTrackDocketChips() {\u001b[39m\n\u001b[31m  const courier = document.getElementById('t_courier').value;\u001b[39m\n\u001b[31m  const m = COURIER_MASTER[courier];\u001b[39m\n\u001b[31m  const maxBulk = m?.maxBulk || 1;\u001b[39m\n\u001b[31m  const limitEl = document.getElementById('trackCourierLimit');\u001b[39m\n\u001b[31m  if (limitEl) limitEl.textContent = `Max ${maxBulk} at once for ${courier}`;·\u001b[39m\n\u001b[31m  // Gather all dockets from active dispatches for this courier\u001b[39m\n\u001b[31m  const activeDis = dispatches.filter(d => d.status !== 'draft');\u001b[39m\n\u001b[31m  const allDockets = activeDis.flatMap(d =>\u001b[39m\n\u001b[31m    (d.consignees || [])\u001b[39m\n\u001b[31m      .filter(c => c.docket && d.courier === courier)\u001b[39m\n\u001b[31m      .map(c => ({ docket: c.docket, name: c.name, city: c.city, status: c.status || 'booked' }))\u001b[39m\n\u001b[31m  );·\u001b[39m\n\u001b[31m  // Also include dockets from OTHER couriers as un-selectable reference\u001b[39m\n\u001b[31m  const otherDockets = activeDis.flatMap(d =>\u001b[39m\n\u001b[31m    (d.consignees || [])\u001b[39m\n\u001b[31m      .filter(c => c.docket && d.courier !== courier)\u001b[39m\n\u001b[31m      .map(c => ({ docket: c.docket, name: c.name, courier: d.courier }))\u001b[39m\n\u001b[31m  );·\u001b[39m\n\u001b[31m  const chips = document.getElementById('trackDocketChips');\u001b[39m\n\u001b[31m  if (!allDockets.length && !otherDockets.length) {\u001b[39m\n\u001b[31m    chips.innerHTML = '<span style=\\\"font-size:.72rem;color:var(--t3);font-family:var(--fnt-mono);\\\">No dispatched dockets yet — enter manually below</span>';\u001b[39m\n\u001b[31m    return;\u001b[39m\n\u001b[31m  }·\u001b[39m\n\u001b[31m  const spClr = { delivered:'var(--grn)', transit:'var(--ylw)', booked:'var(--sky)', pending:'var(--t4)' };·\u001b[39m\n\u001b[31m  chips.innerHTML = [\u001b[39m\n\u001b[31m    // This courier's dockets\u001b[39m\n\u001b[31m    ...allDockets.map(d => {\u001b[39m\n\u001b[31m      const sel = _trackSelected.has(d.docket);\u001b[39m\n\u001b[31m      const clr = spClr[d.status] || 'var(--t4)';\u001b[39m\n\u001b[31m      return `<button onclick=\\\"toggleTrackDocket('${d.docket}')\\\" id=\\\"chip_${d.docket}\\\"\u001b[39m\n\u001b[31m        style=\\\"font-family:var(--fnt-mono);font-size:.72rem;padding:4px 10px;border-radius:20px;\u001b[39m\n\u001b[31m        cursor:pointer;transition:all .2s;border:1px solid ${sel ? 'var(--acc)' : 'var(--bdr)'};\u001b[39m\n\u001b[31m        background:${sel ? 'rgba(249,115,22,.15)' : 'var(--sur2)'};\u001b[39m\n\u001b[31m        color:${sel ? 'var(--acc)' : 'var(--t2)'};\\\">\u001b[39m\n\u001b[31m        ${d.docket} <span style=\\\"font-size:.6rem;color:${clr};\\\">● ${d.status}</span>\u001b[39m\n\u001b[31m        ${d.city ? `<span style=\\\"font-size:.6rem;color:var(--t3);margin-left:4px;\\\">${d.city}</span>` : ''}\u001b[39m\n\u001b[31m      </button>`;\u001b[39m\n\u001b[31m    }),\u001b[39m\n\u001b[31m    // Other couriers — show greyed, click selects and switches courier\u001b[39m\n\u001b[31m    ...otherDockets.slice(0, 5).map(d =>\u001b[39m\n\u001b[31m      `<span title=\\\"${d.courier}\\\" style=\\\"font-family:var(--fnt-mono);font-size:.68rem;padding:3px 8px;\u001b[39m\n\u001b[31m        border-radius:20px;border:1px dashed var(--bdr);color:var(--t4);cursor:default;\\\">\u001b[39m\n\u001b[31m        ${d.docket} <span style=\\\"font-size:.55rem;\\\">${d.courier}</span>\u001b[39m\n\u001b[31m      </span>`\u001b[39m\n\u001b[31m    )\u001b[39m\n\u001b[31m  ].join('');·\u001b[39m\n\u001b[31m  updateTrackSummary();\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction toggleTrackDocket(docket) {\u001b[39m\n\u001b[31m  const courier = document.getElementById('t_courier').value;\u001b[39m\n\u001b[31m  const m = COURIER_MASTER[courier];\u001b[39m\n\u001b[31m  const maxBulk = m?.maxBulk || 1;·\u001b[39m\n\u001b[31m  if (_trackSelected.has(docket)) {\u001b[39m\n\u001b[31m    _trackSelected.delete(docket);\u001b[39m\n\u001b[31m  } else {\u001b[39m\n\u001b[31m    if (_trackSelected.size >= maxBulk) {\u001b[39m\n\u001b[31m      showToast(`${courier} supports max ${maxBulk} docket(s) at once`, 'err');\u001b[39m\n\u001b[31m      return;\u001b[39m\n\u001b[31m    }\u001b[39m\n\u001b[31m    _trackSelected.add(docket);\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m  buildTrackDocketChips(); // re-render chips with updated selection\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction addManualDockets() {\u001b[39m\n\u001b[31m  const raw = document.getElementById('t_manual_dockets').value;\u001b[39m\n\u001b[31m  const courier = document.getElementById('t_courier').value;\u001b[39m\n\u001b[31m  const m = COURIER_MASTER[courier];\u001b[39m\n\u001b[31m  const maxBulk = m?.maxBulk || 1;\u001b[39m\n\u001b[31m  const newDockets = raw.split(/[,\\\\n]/).map(d => d.trim()).filter(Boolean);\u001b[39m\n\u001b[31m  let added = 0;\u001b[39m\n\u001b[31m  for (const d of newDockets) {\u001b[39m\n\u001b[31m    if (_trackSelected.size >= maxBulk) {\u001b[39m\n\u001b[31m      showToast(`Max ${maxBulk} reached for ${courier}`, 'err');\u001b[39m\n\u001b[31m      break;\u001b[39m\n\u001b[31m    }\u001b[39m\n\u001b[31m    _trackSelected.add(d);\u001b[39m\n\u001b[31m    added++;\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m  document.getElementById('t_manual_dockets').value = '';\u001b[39m\n\u001b[31m  buildTrackDocketChips();\u001b[39m\n\u001b[31m  if (added) showToast(`${added} docket(s) added ✓`, 'ok');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction clearTrackSelection() {\u001b[39m\n\u001b[31m  _trackSelected.clear();\u001b[39m\n\u001b[31m  buildTrackDocketChips();\u001b[39m\n\u001b[31m  document.getElementById('trackResultsArea').innerHTML = '';\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction updateTrackSummary() {\u001b[39m\n\u001b[31m  const summary = document.getElementById('trackSelectedSummary');\u001b[39m\n\u001b[31m  const countEl = document.getElementById('trackSelectedCount');\u001b[39m\n\u001b[31m  const listEl  = document.getElementById('trackSelectedList');\u001b[39m\n\u001b[31m  if (!summary) return;\u001b[39m\n\u001b[31m  const arr = Array.from(_trackSelected);\u001b[39m\n\u001b[31m  summary.style.display = arr.length ? 'block' : 'none';\u001b[39m\n\u001b[31m  if (countEl) countEl.textContent = `${arr.length} docket(s) selected`;\u001b[39m\n\u001b[31m  if (listEl)  listEl.textContent  = arr.slice(0, 5).join(', ') + (arr.length > 5 ? `... +${arr.length-5}` : '');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction openTrackOnCourierSite() {\u001b[39m\n\u001b[31m  const courier = document.getElementById('t_courier').value;\u001b[39m\n\u001b[31m  const dockets = Array.from(_trackSelected);\u001b[39m\n\u001b[31m  if (!dockets.length) { showToast('Select dockets first', 'err'); return; }\u001b[39m\n\u001b[31m  const m = COURIER_MASTER[courier];\u001b[39m\n\u001b[31m  if (!m) return;\u001b[39m\n\u001b[31m  // SkyKing: open with all CNOs comma-separated in URL\u001b[39m\n\u001b[31m  let url;\u001b[39m\n\u001b[31m  if (courier === 'SkyKing') {\u001b[39m\n\u001b[31m    url = `https://skyking.co/track?cno=${dockets.join(',')}`;\u001b[39m\n\u001b[31m  } else if (dockets.length === 1) {\u001b[39m\n\u001b[31m    url = m.trackUrl.replace('{docket}', dockets[0]);\u001b[39m\n\u001b[31m  } else {\u001b[39m\n\u001b[31m    url = m.trackBulkUrl || m.trackUrl.replace('{docket}', dockets[0]);\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m  window.open(url, '_blank');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31masync function trackSelected() {\u001b[39m\n\u001b[31m  const courier = document.getElementById('t_courier').value;\u001b[39m\n\u001b[31m  const dockets = Array.from(_trackSelected);\u001b[39m\n\u001b[31m  if (!dockets.length) { showToast('Select at least one docket', 'err'); return; }·\u001b[39m\n\u001b[31m  const area = document.getElementById('trackResultsArea');\u001b[39m\n\u001b[31m  area.innerHTML = `<div class=\\\"card\\\" style=\\\"text-align:center;padding:20px;\\\">\u001b[39m\n\u001b[31m    <div style=\\\"font-size:1.5rem;margin-bottom:8px;\\\">⏳</div>\u001b[39m\n\u001b[31m    <div style=\\\"font-family:var(--fnt-mono);font-size:.78rem;color:var(--t2);\\\">Tracking ${dockets.length} docket(s) via ${courier}...</div>\u001b[39m\n\u001b[31m  </div>`;·\u001b[39m\n\u001b[31m  let resultsHTML = '';\u001b[39m\n\u001b[31m  for (const docket of dockets) {\u001b[39m\n\u001b[31m    const result = await fetchTracking(docket, courier);\u001b[39m\n\u001b[31m    const sp = (!result.status || result.status === 'failed') ? 'pending' : result.status;\u001b[39m\n\u001b[31m    const spClr = { delivered:'var(--grn)', transit:'var(--ylw)', booked:'var(--sky)', pending:'var(--t3)' };\u001b[39m\n\u001b[31m    const clr = spClr[sp] || 'var(--t3)';\u001b[39m\n\u001b[31m    // Update stored dispatch if this docket exists in our records\u001b[39m\n\u001b[31m    for (const d of dispatches) {\u001b[39m\n\u001b[31m      for (const c of (d.consignees||[])) {\u001b[39m\n\u001b[31m        if (c.docket === docket && result.status && result.status !== 'failed') {\u001b[39m\n\u001b[31m          c.status = result.status;\u001b[39m\n\u001b[31m          c.lastEvent = result.lastEvent;\u001b[39m\n\u001b[31m          c.lastTrackedAt = new Date().toISOString();\u001b[39m\n\u001b[31m        }\u001b[39m\n\u001b[31m      }\u001b[39m\n\u001b[31m    }\u001b[39m\n\u001b[31m    resultsHTML += `\u001b[39m\n\u001b[31m    <div style=\\\"background:var(--sur);border:1px solid var(--bdr);border-left:3px solid ${clr};\u001b[39m\n\u001b[31m      border-radius:var(--r-sm);padding:10px 12px;margin-bottom:8px;\\\">\u001b[39m\n\u001b[31m      <div style=\\\"display:flex;justify-content:space-between;align-items:center;margin-bottom:6px;\\\">\u001b[39m\n\u001b[31m        <span style=\\\"font-family:var(--fnt-mono);font-size:.9rem;color:var(--sky);\\\">${docket}</span>\u001b[39m\n\u001b[31m        <span class=\\\"status-pill sp-${sp}\\\">${sp}</span>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      <div style=\\\"font-size:.75rem;color:var(--t2);font-family:var(--fnt-mono);\\\">${result.lastEvent || '—'}</div>\u001b[39m\n\u001b[31m      ${result.lastScanLocation ? `<div style=\\\"font-size:.68rem;color:var(--t3);margin-top:3px;\\\">📍 ${result.lastScanLocation}</div>` : ''}\u001b[39m\n\u001b[31m      <div style=\\\"display:flex;gap:8px;margin-top:8px;\\\">\u001b[39m\n\u001b[31m        <button class=\\\"btn-sm\\\" onclick=\\\"openCourierTrackUrl('${docket}','${courier}')\\\">🔗 Open on ${courier}</button>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m    </div>`;\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m  saveData();\u001b[39m\n\u001b[31m  area.innerHTML = `<div style=\\\"margin-top:4px;\\\">${resultsHTML}</div>`;\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// Keep old names as aliases for compatibility\u001b[39m\n\u001b[31mfunction trackMultiple() { trackSelected(); }\u001b[39m\n\u001b[31mfunction openCourierSite() { openTrackOnCourierSite(); }·\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31m// HISTORY\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31mfunction renderHistory() {\u001b[39m\n\u001b[31m  const list = document.getElementById('historyList');\u001b[39m\n\u001b[31m  if (!dispatches.length) {\u001b[39m\n\u001b[31m    list.innerHTML = '<div class=\\\"empty\\\"><div class=\\\"e-icon\\\">📭</div><p>No dispatches yet.</p></div>';\u001b[39m\n\u001b[31m    return;\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m  list.innerHTML = dispatches.map(d => {\u001b[39m\n\u001b[31m    const cons = d.consignees?.length || 0;\u001b[39m\n\u001b[31m    const docketed = d.consignees?.filter(c=>c.docket).length || 0;\u001b[39m\n\u001b[31m    const clr = d.status === 'draft' ? 'var(--ylw)' : 'var(--grn)';\u001b[39m\n\u001b[31m    return `\u001b[39m\n\u001b[31m    <div class=\\\"docket-tile\\\" onclick=\\\"showDispatchDetail('${d.id}')\\\">\u001b[39m\n\u001b[31m      <div class=\\\"docket-tile-hdr\\\">\u001b[39m\n\u001b[31m        <div class=\\\"docket-tile-left\\\">\u001b[39m\n\u001b[31m          <div class=\\\"docket-num\\\" style=\\\"font-size:.78rem;\\\">${d.id}</div>\u001b[39m\n\u001b[31m          <div class=\\\"docket-store\\\">${d.client||'No client'} ${d.jobref ? '› '+d.jobref : ''}</div>\u001b[39m\n\u001b[31m          <div class=\\\"docket-courier\\\">📦 ${cons} consignees · ${docketed} docketed · ${d.courier||'—'} · ${d.dispatchDate||d.createdAt?.slice(0,10)||'—'}</div>\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m        <span class=\\\"status-pill\\\" style=\\\"background:rgba(0,0,0,.2);color:${clr};\\\">${d.status||'active'}</span>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m    </div>`;\u001b[39m\n\u001b[31m  }).join('');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction showDispatchDetail(id) {\u001b[39m\n\u001b[31m  const d = dispatches.find(x => x.id === id);\u001b[39m\n\u001b[31m  if (!d) return;\u001b[39m\n\u001b[31m  const content = document.getElementById('detailContent');\u001b[39m\n\u001b[31m  content.innerHTML = `\u001b[39m\n\u001b[31m    <div class=\\\"sheet-title\\\">${d.client} ${d.jobref ? '› '+d.jobref : ''}</div>\u001b[39m\n\u001b[31m    <div class=\\\"info-tag\\\"><strong>${d.id}</strong> · ${d.courier||'—'} · ${d.dispatchDate||'—'}</div>\u001b[39m\n\u001b[31m    <div class=\\\"sec-div\\\"><span>Consignees (${d.consignees?.length||0})</span></div>\u001b[39m\n\u001b[31m    ${(d.consignees||[]).map((c,i) => `\u001b[39m\n\u001b[31m      <div style=\\\"background:var(--sur2);border:1px solid var(--bdr);border-radius:var(--r-sm);padding:10px;margin-bottom:6px;\\\">\u001b[39m\n\u001b[31m        <div style=\\\"font-weight:600;margin-bottom:4px;\\\">${i+1}. ${c.name} — ${c.city}</div>\u001b[39m\n\u001b[31m        ${c.docket ? `<div style=\\\"font-family:var(--fnt-mono);font-size:.78rem;color:var(--sky);\\\">🔖 ${c.docket}</div>` : ''}\u001b[39m\n\u001b[31m        ${c.material ? `<div style=\\\"font-size:.75rem;color:var(--t2);\\\">📦 ${c.material} ${c.qty ? '('+c.qty+')':''}</div>` : ''}\u001b[39m\n\u001b[31m        <span class=\\\"status-pill sp-${c.status||'pending'}\\\" style=\\\"margin-top:4px;display:inline-block;\\\">${c.status||'pending'}</span>\u001b[39m\n\u001b[31m      </div>`).join('')}\u001b[39m\n\u001b[31m    ${d.remarks ? `<div class=\\\"info-tag\\\">📝 ${d.remarks}</div>` : ''}\u001b[39m\n\u001b[31m    <div class=\\\"btn-row\\\" style=\\\"margin-top:14px;\\\">\u001b[39m\n\u001b[31m      <button class=\\\"btn btn-ghost\\\" onclick=\\\"deleteDispatch('${d.id}');closeOverlay('detailOverlay')\\\">🗑️ Delete</button>\u001b[39m\n\u001b[31m      <button class=\\\"btn btn-orange\\\" onclick=\\\"closeOverlay('detailOverlay');showPane('dashboard')\\\">📊 View in Dashboard</button>\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m  `;\u001b[39m\n\u001b[31m  openOverlay('detailOverlay');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction deleteDispatch(id) {\u001b[39m\n\u001b[31m  if (!confirm('Delete this dispatch?')) return;\u001b[39m\n\u001b[31m  dispatches = dispatches.filter(d => d.id !== id);\u001b[39m\n\u001b[31m  saveData();\u001b[39m\n\u001b[31m  updateHistCount();\u001b[39m\n\u001b[31m  renderHistory();\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction updateHistCount() {\u001b[39m\n\u001b[31m  document.getElementById('histCount').textContent = dispatches.length;\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31m// GOOGLE SHEETS\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31masync function logToSheets(d) {\u001b[39m\n\u001b[31m  const url = window.appConfig.webhook_url;\u001b[39m\n\u001b[31m  if (!url) return;\u001b[39m\n\u001b[31m  try {\u001b[39m\n\u001b[31m    await fetch(url, {\u001b[39m\n\u001b[31m      method: 'POST', mode: 'no-cors',\u001b[39m\n\u001b[31m      headers: { 'Content-Type': 'application/json' },\u001b[39m\n\u001b[31m      body: JSON.stringify({\u001b[39m\n\u001b[31m        type: 'dispatch', id: d.id, client: d.client, jobref: d.jobref,\u001b[39m\n\u001b[31m        courier: d.courier, consignees: d.consignees?.length||0,\u001b[39m\n\u001b[31m        destinations: d.consignees?.map(c=>c.city).join(', ')||'',\u001b[39m\n\u001b[31m        dockets: d.consignees?.filter(c=>c.docket).map(c=>c.docket).join(', ')||'',\u001b[39m\n\u001b[31m        dispatchDate: d.dispatchDate, status: d.status,\u001b[39m\n\u001b[31m        timestamp: new Date().toISOString(), company: window.appConfig.company\u001b[39m\n\u001b[31m      })\u001b[39m\n\u001b[31m    });\u001b[39m\n\u001b[31m  } catch(e) {}\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31m// CONFIG — Google Sheet is the source of truth\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════·\u001b[39m\n\u001b[31mfunction onSheetsUrlChange() {\u001b[39m\n\u001b[31m  const url = document.getElementById('cfg_sheets_url').value.trim();\u001b[39m\n\u001b[31m  // Show sync button state\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction saveSheetsUrl() {\u001b[39m\n\u001b[31m  const url = document.getElementById('cfg_sheets_url').value.trim();\u001b[39m\n\u001b[31m  if (!url) { showToast('Enter the Apps Script URL first', 'err'); return; }\u001b[39m\n\u001b[31m  window.appConfig.sheets_url = url;\u001b[39m\n\u001b[31m  saveSheetsUrlLocal();\u001b[39m\n\u001b[31m  syncFromSheet(false);\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m/**\u001b[39m\n\u001b[31m * Fetch all config from Google Sheet and apply to appConfig.\u001b[39m\n\u001b[31m * Called on startup (silent=true) and manually (silent=false).\u001b[39m\n\u001b[31m */\u001b[39m\n\u001b[31masync function syncFromSheet(silent = false) {\u001b[39m\n\u001b[31m  const url = window.appConfig.sheets_url;\u001b[39m\n\u001b[31m  if (!url) { if (!silent) showToast('No Sheets URL configured', 'err'); return; }·\u001b[39m\n\u001b[31m  setSyncStatus('syncing', 'Syncing config from Google Sheet...');\u001b[39m\n\u001b[31m  try {\u001b[39m\n\u001b[31m    const resp = await fetch(url + '?action=pwa_config', {\u001b[39m\n\u001b[31m      method: 'GET',\u001b[39m\n\u001b[31m      redirect: 'follow'\u001b[39m\n\u001b[31m    });\u001b[39m\n\u001b[31m    if (!resp.ok) throw new Error('HTTP ' + resp.status);\u001b[39m\n\u001b[31m    const cfg = await resp.json();·\u001b[39m\n\u001b[31m    // Apply all values from sheet to appConfig\u001b[39m\n\u001b[31m    const keyMap = {\u001b[39m\n\u001b[31m      proxy_url:           'proxy_url',\u001b[39m\n\u001b[31m      webhook_url:         'webhook_url',\u001b[39m\n\u001b[31m      ollama_url:          'ollama_url',\u001b[39m\n\u001b[31m      ocr_mode:            'ocr_mode',\u001b[39m\n\u001b[31m      company:             'company',\u001b[39m\n\u001b[31m      city:                'city',\u001b[39m\n\u001b[31m      auto_log:            'auto_log',\u001b[39m\n\u001b[31m      refresh_on_load:     'refresh_on_load',\u001b[39m\n\u001b[31m      admin_password:      'admin_password',\u001b[39m\n\u001b[31m      n8n_dispatch_webhook:'n8n_dispatch_webhook',\u001b[39m\n\u001b[31m    };\u001b[39m\n\u001b[31m    Object.entries(keyMap).forEach(([sheetKey, cfgKey]) => {\u001b[39m\n\u001b[31m      if (cfg[sheetKey] !== undefined && cfg[sheetKey] !== '') {\u001b[39m\n\u001b[31m        window.appConfig[cfgKey] = cfg[sheetKey];\u001b[39m\n\u001b[31m      }\u001b[39m\n\u001b[31m    });·\u001b[39m\n\u001b[31m    // Apply courier master if provided\u001b[39m\n\u001b[31m    if (cfg._courier_master && Array.isArray(cfg._courier_master)) {\u001b[39m\n\u001b[31m      cfg._courier_master.forEach(c => {\u001b[39m\n\u001b[31m        if (c.name && c.trackUrl) {\u001b[39m\n\u001b[31m          COURIER_MASTER[c.name] = {\u001b[39m\n\u001b[31m            trackUrl: c.trackUrl,\u001b[39m\n\u001b[31m            trackBulkUrl: c.bulkUrl || '',\u001b[39m\n\u001b[31m            maxBulk: parseInt(c.maxBulk) || 1\u001b[39m\n\u001b[31m          };\u001b[39m\n\u001b[31m        }\u001b[39m\n\u001b[31m      });\u001b[39m\n\u001b[31m    }·\u001b[39m\n\u001b[31m    renderCourierMasterList();\u001b[39m\n\u001b[31m    renderSheetConfigDisplay(cfg);\u001b[39m\n\u001b[31m    setSyncStatus('ok', 'Synced from sheet · ' + new Date().toLocaleTimeString());\u001b[39m\n\u001b[31m    if (!silent) showToast('Config synced from sheet ✓', 'ok');·\u001b[39m\n\u001b[31m    // If refresh_on_load is true, trigger dashboard refresh\u001b[39m\n\u001b[31m    if (window.appConfig.refresh_on_load === 'true') {\u001b[39m\n\u001b[31m      setTimeout(refreshAllTracking, 2000);\u001b[39m\n\u001b[31m    }·\u001b[39m\n\u001b[31m  } catch(e) {\u001b[39m\n\u001b[31m    setSyncStatus('err', 'Sync failed: ' + e.message);\u001b[39m\n\u001b[31m    if (!silent) showToast('Sync failed — check URL', 'err');\u001b[39m\n\u001b[31m    console.error('Sheets sync error:', e);\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction setSyncStatus(state, msg) {\u001b[39m\n\u001b[31m  const el = document.getElementById('syncStatus');\u001b[39m\n\u001b[31m  const icon = document.getElementById('syncIcon');\u001b[39m\n\u001b[31m  const msgEl = document.getElementById('syncMsg');\u001b[39m\n\u001b[31m  el.style.display = 'flex';\u001b[39m\n\u001b[31m  icon.textContent = state === 'ok' ? '✅' : state === 'err' ? '❌' : '⏳';\u001b[39m\n\u001b[31m  msgEl.textContent = msg;\u001b[39m\n\u001b[31m  el.style.borderColor = state === 'ok' ? 'rgba(34,197,94,.3)' : state === 'err' ? 'rgba(239,68,68,.3)' : 'rgba(234,179,8,.3)';\u001b[39m\n\u001b[31m  el.style.background = state === 'ok' ? 'rgba(34,197,94,.05)' : state === 'err' ? 'rgba(239,68,68,.05)' : 'rgba(234,179,8,.05)';\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction renderSheetConfigDisplay(cfg) {\u001b[39m\n\u001b[31m  const display = document.getElementById('sheetConfigDisplay');\u001b[39m\n\u001b[31m  display.style.display = 'block';\u001b[39m\n\u001b[31m  document.getElementById('sheetSyncTime').textContent = 'Last sync: ' + new Date().toLocaleTimeString();\u001b[39m\n\u001b[31m  const showKeys = ['proxy_url','webhook_url','ollama_url','ocr_mode','company','city','auto_log'];\u001b[39m\n\u001b[31m  document.getElementById('sheetConfigRows').innerHTML = showKeys.map(k => {\u001b[39m\n\u001b[31m    const val = cfg[k] || window.appConfig[k] || '—';\u001b[39m\n\u001b[31m    const isEmpty = !cfg[k];\u001b[39m\n\u001b[31m    return `<div class=\\\"courier-master-row\\\">\u001b[39m\n\u001b[31m      <div class=\\\"cml\\\">\u001b[39m\n\u001b[31m        <div class=\\\"cml-name\\\" style=\\\"font-family:var(--fnt-mono);font-size:.75rem;\\\">${k}</div>\u001b[39m\n\u001b[31m        <div class=\\\"cml-url\\\" style=\\\"${isEmpty ? 'color:var(--red);' : ''}\\\">${isEmpty ? '⚠️ Not set in sheet' : val}</div>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m    </div>`;\u001b[39m\n\u001b[31m  }).join('');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m/**\u001b[39m\n\u001b[31m * Push current config changes back to the sheet.\u001b[39m\n\u001b[31m * Called when user edits config from PWA (rare — usually done in sheet directly).\u001b[39m\n\u001b[31m */\u001b[39m\n\u001b[31masync function pushConfigToSheet(updates) {\u001b[39m\n\u001b[31m  const url = window.appConfig.sheets_url;\u001b[39m\n\u001b[31m  if (!url) return;\u001b[39m\n\u001b[31m  try {\u001b[39m\n\u001b[31m    await fetch(url, {\u001b[39m\n\u001b[31m      method: 'POST', mode: 'no-cors',\u001b[39m\n\u001b[31m      headers: { 'Content-Type': 'application/json' },\u001b[39m\n\u001b[31m      body: JSON.stringify({ type: 'save_config', config: updates })\u001b[39m\n\u001b[31m    });\u001b[39m\n\u001b[31m  } catch(e) {}\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction clearAll() {\u001b[39m\n\u001b[31m  if (!confirm('Delete ALL dispatch data?')) return;\u001b[39m\n\u001b[31m  dispatches = [];\u001b[39m\n\u001b[31m  saveData();\u001b[39m\n\u001b[31m  updateHistCount();\u001b[39m\n\u001b[31m  renderDashboard();\u001b[39m\n\u001b[31m  renderHistory();\u001b[39m\n\u001b[31m  showToast('Cleared', 'ok');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction renderCourierMasterList() {\u001b[39m\n\u001b[31m  const el = document.getElementById('courierMasterList');\u001b[39m\n\u001b[31m  el.innerHTML = Object.entries(COURIER_MASTER).map(([name, info]) => `\u001b[39m\n\u001b[31m    <div class=\\\"courier-master-row\\\">\u001b[39m\n\u001b[31m      <div class=\\\"cml\\\">\u001b[39m\n\u001b[31m        <div class=\\\"cml-name\\\">${name}</div>\u001b[39m\n\u001b[31m        <div class=\\\"cml-url\\\">${info.trackUrl}</div>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      <button class=\\\"btn-sm\\\" onclick=\\\"window.open('${info.trackBulkUrl||info.trackUrl.replace('{docket}','')}','_blank')\\\">Open</button>\u001b[39m\n\u001b[31m    </div>`).join('');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction openCourierMaster() {\u001b[39m\n\u001b[31m  const ed = document.getElementById('courierMasterEditor');\u001b[39m\n\u001b[31m  ed.innerHTML = Object.entries(COURIER_MASTER).map(([name, info]) => `\u001b[39m\n\u001b[31m    <div style=\\\"margin-bottom:14px;\\\">\u001b[39m\n\u001b[31m      <div style=\\\"font-family:var(--fnt-head);font-size:.75rem;font-weight:700;color:var(--t2);text-transform:uppercase;letter-spacing:1.5px;margin-bottom:6px;\\\">${name}</div>\u001b[39m\n\u001b[31m      <div class=\\\"f\\\" style=\\\"margin-bottom:6px;\\\">\u001b[39m\n\u001b[31m        <label>Track URL (use {docket})</label>\u001b[39m\n\u001b[31m        <input type=\\\"text\\\" value=\\\"${info.trackUrl}\\\" id=\\\"cm_${name.replace(/\\\\s/g,'_')}\\\"\u001b[39m\n\u001b[31m          style=\\\"font-family:var(--fnt-mono);font-size:.75rem;\\\">\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m    </div>`).join('') +\u001b[39m\n\u001b[31m    `<button class=\\\"btn btn-orange\\\" onclick=\\\"saveCourierMaster()\\\">Save Courier Master</button>`;\u001b[39m\n\u001b[31m  openOverlay('courierMasterOverlay');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction saveCourierMaster() {\u001b[39m\n\u001b[31m  Object.keys(COURIER_MASTER).forEach(name => {\u001b[39m\n\u001b[31m    const el = document.getElementById('cm_' + name.replace(/\\\\s/g,'_'));\u001b[39m\n\u001b[31m    if (el) COURIER_MASTER[name].trackUrl = el.value;\u001b[39m\n\u001b[31m  });\u001b[39m\n\u001b[31m  closeOverlay('courierMasterOverlay');\u001b[39m\n\u001b[31m  renderCourierMasterList();\u001b[39m\n\u001b[31m  showToast('Courier master saved ✓', 'ok');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31m// UI UTILS\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31m// ── ADMIN AUTH ───────────────────────────────────────────────────────────\u001b[39m\n\u001b[31mlet _adminAuthed = false;\u001b[39m\n\u001b[31mlet _lastAdminPane = null;·\u001b[39m\n\u001b[31mfunction showPane(name) {\u001b[39m\n\u001b[31m  // Config tab requires admin auth — re-ask if navigated away\u001b[39m\n\u001b[31m  if (name === 'config') {\u001b[39m\n\u001b[31m    if (!_adminAuthed) {\u001b[39m\n\u001b[31m      _lastAdminPane = 'config';\u001b[39m\n\u001b[31m      showAdminLogin();\u001b[39m\n\u001b[31m      return;\u001b[39m\n\u001b[31m    }\u001b[39m\n\u001b[31m  } else {\u001b[39m\n\u001b[31m    // User navigated away from config — revoke auth\u001b[39m\n\u001b[31m    if (_lastAdminPane === 'config') _adminAuthed = false;\u001b[39m\n\u001b[31m    _lastAdminPane = name;\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m  document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));\u001b[39m\n\u001b[31m  document.querySelectorAll('.pane').forEach(p => p.classList.remove('active'));\u001b[39m\n\u001b[31m  document.getElementById('tab-'+name).classList.add('active');\u001b[39m\n\u001b[31m  document.getElementById('pane-'+name).classList.add('active');\u001b[39m\n\u001b[31m  if (name === 'dashboard') renderDashboard();\u001b[39m\n\u001b[31m  if (name === 'history') renderHistory();\u001b[39m\n\u001b[31m  if (name === 'track') buildTrackDocketChips();\u001b[39m\n\u001b[31m  if (name === 'config') { document.getElementById('cfg_sheets_url').value = window.appConfig.sheets_url || ''; }\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction showAdminLogin() {\u001b[39m\n\u001b[31m  // Password replaced by hub identity — only harish/pramod can access config\u001b[39m\n\u001b[31m  if (['harish', 'pramod'].includes(window._hubUser?.id)) {\u001b[39m\n\u001b[31m    _adminAuthed = true;\u001b[39m\n\u001b[31m    showPane('config');\u001b[39m\n\u001b[31m  } else {\u001b[39m\n\u001b[31m    showToast('Config access restricted to admin users', 'err');\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31masync function submitAdminLogin() {\u001b[39m\n\u001b[31m  showAdminLogin(); // no-op shim — kept for any lingering HTML onclick refs\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31mfunction openOverlay(id) { document.getElementById(id).classList.add('open'); }\u001b[39m\n\u001b[31mfunction closeOverlay(id) { document.getElementById(id).classList.remove('open'); }·\u001b[39m\n\u001b[31mlet toastTimer;\u001b[39m\n\u001b[31mfunction showToast(msg, type='') {\u001b[39m\n\u001b[31m  const t = document.getElementById('toast');\u001b[39m\n\u001b[31m  t.textContent = msg;\u001b[39m\n\u001b[31m  t.className = 'toast show' + (type ? ' '+type : '');\u001b[39m\n\u001b[31m  clearTimeout(toastTimer);\u001b[39m\n\u001b[31m  toastTimer = setTimeout(() => t.classList.remove('show'), 2800);\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction showOCRWorking(show, msg='') {\u001b[39m\n\u001b[31m  const el = document.getElementById('ocrWorking');\u001b[39m\n\u001b[31m  el.classList.toggle('visible', show);\u001b[39m\n\u001b[31m  if (msg) document.getElementById('ocrWorkingText').textContent = msg;\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction showAIProgress(label, pct) {\u001b[39m\n\u001b[31m  const el = document.getElementById('aiProgress');\u001b[39m\n\u001b[31m  el.classList.add('visible');\u001b[39m\n\u001b[31m  document.getElementById('aiProgLabel').textContent = label;\u001b[39m\n\u001b[31m  document.getElementById('aiProgFill').style.width = pct + '%';\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31mfunction hideAIProgress() {\u001b[39m\n\u001b[31m  document.getElementById('aiProgress').classList.remove('visible');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction updateNetDot() {\u001b[39m\n\u001b[31m  document.getElementById('netDot').style.background = navigator.onLine ? 'var(--grn)' : 'var(--red)';\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction setToday() {\u001b[39m\n\u001b[31m  document.getElementById('n_date').value = new Date().toISOString().split('T')[0];\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m// ── EXPORT CLEAN SHEET ───────────────────────────────────────────────────\u001b[39m\n\u001b[31mfunction exportCleanSheet() {\u001b[39m\n\u001b[31m  const pane = document.getElementById('verifyPane');\u001b[39m\n\u001b[31m  const d = pane._pendingDispatch;\u001b[39m\n\u001b[31m  if (!d || !d.consignees?.length) { showToast('No data to export', 'err'); return; }·\u001b[39m\n\u001b[31m  // Build CSV\u001b[39m\n\u001b[31m  const headers = ['S.No.','Store Name','City','Pincode','Phone','Address','Docket No.','Material','Courier','Status'];\u001b[39m\n\u001b[31m  const rows = d.consignees.map((c, i) => [\u001b[39m\n\u001b[31m    i + 1,\u001b[39m\n\u001b[31m    `\\\"${(c.name||'').replace(/\\\"/g,'\\\"\\\"')}\\\"`,\u001b[39m\n\u001b[31m    `\\\"${(c.city||'').replace(/\\\"/g,'\\\"\\\"')}\\\"`,\u001b[39m\n\u001b[31m    c.pincode || '',\u001b[39m\n\u001b[31m    c.phone || '',\u001b[39m\n\u001b[31m    `\\\"${(c.address||'').replace(/\\\"/g,'\\\"\\\"')}\\\"`,\u001b[39m\n\u001b[31m    c.docket || '',\u001b[39m\n\u001b[31m    `\\\"${(c.material||'').replace(/\\\"/g,'\\\"\\\"')}\\\"`,\u001b[39m\n\u001b[31m    d.courier || '',\u001b[39m\n\u001b[31m    c.status || 'pending'\u001b[39m\n\u001b[31m  ]);·\u001b[39m\n\u001b[31m  const csv = [headers.join(','), ...rows.map(r => r.join(','))].join('\\\\n');\u001b[39m\n\u001b[31m  const blob = new Blob([csv], { type: 'text/csv' });\u001b[39m\n\u001b[31m  const url = URL.createObjectURL(blob);\u001b[39m\n\u001b[31m  const a = document.createElement('a');\u001b[39m\n\u001b[31m  a.href = url;\u001b[39m\n\u001b[31m  a.download = `360LM_Dispatch_${d.client||'export'}_${d.dispatchDate||new Date().toISOString().slice(0,10)}_clean.csv`;\u001b[39m\n\u001b[31m  a.click();\u001b[39m\n\u001b[31m  URL.revokeObjectURL(url);\u001b[39m\n\u001b[31m  showToast('Clean sheet exported ✓', 'ok');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// ── DOCKET SLIP UPLOAD — SKIP OCR IF DOCKET ALREADY MAPPED ───────────────\u001b[39m\n\u001b[31mfunction shouldSkipOCR() {\u001b[39m\n\u001b[31m  // If any consignee already has a docket (from store list mapping), skip OCR extraction\u001b[39m\n\u001b[31m  const consList = document.getElementById('consList');\u001b[39m\n\u001b[31m  if (!consList) return false;\u001b[39m\n\u001b[31m  const cards = consList.querySelectorAll('.cons-card');\u001b[39m\n\u001b[31m  let hasAnyDocket = false;\u001b[39m\n\u001b[31m  cards.forEach(card => {\u001b[39m\n\u001b[31m    const d = card.querySelector('.cons-docket-input')?.value?.trim();\u001b[39m\n\u001b[31m    if (d) hasAnyDocket = true;\u001b[39m\n\u001b[31m  });\u001b[39m\n\u001b[31m  return hasAnyDocket;\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// ─── TRACKING UPDATE TO SHEETS ──────────────────────────────────────────\u001b[39m\n\u001b[31masync function logTrackingUpdate(docket, status, lastEvent, location) {\u001b[39m\n\u001b[31m  const url = window.appConfig.webhook_url;\u001b[39m\n\u001b[31m  if (!url) return;\u001b[39m\n\u001b[31m  try {\u001b[39m\n\u001b[31m    await fetch(url, {\u001b[39m\n\u001b[31m      method: 'POST', mode: 'no-cors',\u001b[39m\n\u001b[31m      headers: { 'Content-Type': 'application/json' },\u001b[39m\n\u001b[31m      body: JSON.stringify({\u001b[39m\n\u001b[31m        type: 'tracking_update',\u001b[39m\n\u001b[31m        docket, status, lastEvent, location,\u001b[39m\n\u001b[31m        timestamp: new Date().toISOString()\u001b[39m\n\u001b[31m      })\u001b[39m\n\u001b[31m    });\u001b[39m\n\u001b[31m  } catch(e) {}\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// ─── POD / RECIPIENT PHOTO UPLOAD ───────────────────────────────────────\u001b[39m\n\u001b[31m// Stores base64 locally, pushes to Sheets webhook (Apps Script saves to Drive)\u001b[39m\n\u001b[31mfunction showPODUpload(docket, dispatchId) {\u001b[39m\n\u001b[31m  const overlay = document.getElementById('podOverlay');\u001b[39m\n\u001b[31m  overlay.dataset.docket = docket;\u001b[39m\n\u001b[31m  overlay.dataset.dispatchId = dispatchId;\u001b[39m\n\u001b[31m  document.getElementById('podPreview').innerHTML = '';\u001b[39m\n\u001b[31m  document.getElementById('podFileInput').value = '';\u001b[39m\n\u001b[31m  document.getElementById('recipientFileInput').value = '';\u001b[39m\n\u001b[31m  openOverlay('podOverlay');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31masync function uploadPODFiles() {\u001b[39m\n\u001b[31m  const overlay = document.getElementById('podOverlay');\u001b[39m\n\u001b[31m  const docket = overlay.dataset.docket;\u001b[39m\n\u001b[31m  const dispatchId = overlay.dataset.dispatchId;\u001b[39m\n\u001b[31m  const podFile = document.getElementById('podFileInput').files[0];\u001b[39m\n\u001b[31m  const recipFile = document.getElementById('recipientFileInput').files[0];\u001b[39m\n\u001b[31m  if (!podFile && !recipFile) { showToast('Select at least one photo', 'err'); return; }·\u001b[39m\n\u001b[31m  showToast('Uploading...', 'info');\u001b[39m\n\u001b[31m  const toBase64 = f => new Promise((res,rej) => {\u001b[39m\n\u001b[31m    const r = new FileReader(); r.onload = e => res(e.target.result); r.onerror = rej; r.readAsDataURL(f);\u001b[39m\n\u001b[31m  });·\u001b[39m\n\u001b[31m  const payload = {\u001b[39m\n\u001b[31m    type: 'pod_upload',\u001b[39m\n\u001b[31m    docket, dispatchId,\u001b[39m\n\u001b[31m    timestamp: new Date().toISOString()\u001b[39m\n\u001b[31m  };\u001b[39m\n\u001b[31m  if (podFile)   payload.pod_image = await toBase64(podFile);\u001b[39m\n\u001b[31m  if (recipFile) payload.recipient_image = await toBase64(recipFile);·\u001b[39m\n\u001b[31m  // Update local store\u001b[39m\n\u001b[31m  for (const d of dispatches) {\u001b[39m\n\u001b[31m    if (d.id !== dispatchId) continue;\u001b[39m\n\u001b[31m    for (const c of d.consignees) {\u001b[39m\n\u001b[31m      if (c.docket === docket) {\u001b[39m\n\u001b[31m        c.podUploaded = true;\u001b[39m\n\u001b[31m        c.status = 'delivered';\u001b[39m\n\u001b[31m      }\u001b[39m\n\u001b[31m    }\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m  saveData();·\u001b[39m\n\u001b[31m  // Push to Sheets (Apps Script saves images to Drive, stores URL in sheet)\u001b[39m\n\u001b[31m  if (window.appConfig.webhook_url) {\u001b[39m\n\u001b[31m    try {\u001b[39m\n\u001b[31m      await fetch(window.appConfig.webhook_url, {\u001b[39m\n\u001b[31m        method: 'POST', mode: 'no-cors',\u001b[39m\n\u001b[31m        headers: { 'Content-Type': 'application/json' },\u001b[39m\n\u001b[31m        body: JSON.stringify(payload)\u001b[39m\n\u001b[31m      });\u001b[39m\n\u001b[31m    } catch(e) {}\u001b[39m\n\u001b[31m  }·\u001b[39m\n\u001b[31m  closeOverlay('podOverlay');\u001b[39m\n\u001b[31m  renderDashboard();\u001b[39m\n\u001b[31m  showToast('POD uploaded ✓ — marked as delivered', 'ok');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// ─── WHATSAPP DISPATCH TABLE SHARE ──────────────────────────────────────\u001b[39m\n\u001b[31mfunction shareDispatchToWhatsApp(dispatchId) {\u001b[39m\n\u001b[31m  const d = dispatches.find(x => x.id === dispatchId);\u001b[39m\n\u001b[31m  if (!d) return;\u001b[39m\n\u001b[31m  const lines = [\u001b[39m\n\u001b[31m    `📦 *Dispatch Update — ${d.client} ${d.jobref ? '› '+d.jobref : ''}*`,\u001b[39m\n\u001b[31m    `🚚 Courier: ${d.courier} | Date: ${d.dispatchDate || d.createdAt?.slice(0,10)}`,\u001b[39m\n\u001b[31m    ``,\u001b[39m\n\u001b[31m    ...d.consignees.filter(c => c.docket).map((c,i) =>\u001b[39m\n\u001b[31m      `${i+1}. *${c.name}* — ${c.city}\\\\n   Docket: ${c.docket} | Status: ${c.status||'booked'}`\u001b[39m\n\u001b[31m    ),\u001b[39m\n\u001b[31m    ``,\u001b[39m\n\u001b[31m    `_360 Degree Logical Marketing_`\u001b[39m\n\u001b[31m  ];\u001b[39m\n\u001b[31m  const msg = encodeURIComponent(lines.join('\\\\n'));\u001b[39m\n\u001b[31m  window.open('https://wa.me/?text=' + msg, '_blank');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// Trigger n8n webhook for automated WhatsApp/email share\u001b[39m\n\u001b[31masync function triggerN8nShare(dispatchId, channel) {\u001b[39m\n\u001b[31m  const d = dispatches.find(x => x.id === dispatchId);\u001b[39m\n\u001b[31m  if (!d) return;\u001b[39m\n\u001b[31m  const n8nUrl = window.appConfig.n8n_dispatch_webhook;\u001b[39m\n\u001b[31m  if (!n8nUrl) { showToast('n8n webhook not configured in Config sheet', 'err'); return; }\u001b[39m\n\u001b[31m  try {\u001b[39m\n\u001b[31m    await fetch(n8nUrl, {\u001b[39m\n\u001b[31m      method: 'POST',\u001b[39m\n\u001b[31m      headers: { 'Content-Type': 'application/json' },\u001b[39m\n\u001b[31m      body: JSON.stringify({\u001b[39m\n\u001b[31m        channel, // 'whatsapp' or 'email'\u001b[39m\n\u001b[31m        dispatch: d,\u001b[39m\n\u001b[31m        timestamp: new Date().toISOString()\u001b[39m\n\u001b[31m      })\u001b[39m\n\u001b[31m    });\u001b[39m\n\u001b[31m    showToast(`Sent to n8n for ${channel} delivery ✓`, 'ok');\u001b[39m\n\u001b[31m  } catch(e) {\u001b[39m\n\u001b[31m    showToast('n8n webhook failed', 'err');\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction previewPOD(input, type) {\u001b[39m\n\u001b[31m  const f = input.files[0];\u001b[39m\n\u001b[31m  if (!f) return;\u001b[39m\n\u001b[31m  const r = new FileReader();\u001b[39m\n\u001b[31m  r.onload = e => {\u001b[39m\n\u001b[31m    const prev = document.getElementById('podPreview');\u001b[39m\n\u001b[31m    const existing = document.getElementById('pod_prev_' + type);\u001b[39m\n\u001b[31m    if (existing) existing.remove();\u001b[39m\n\u001b[31m    const img = document.createElement('img');\u001b[39m\n\u001b[31m    img.id = 'pod_prev_' + type;\u001b[39m\n\u001b[31m    img.src = e.target.result;\u001b[39m\n\u001b[31m    img.style.cssText = 'width:80px;height:80px;object-fit:cover;border-radius:6px;border:1px solid var(--bdr);';\u001b[39m\n\u001b[31m    prev.appendChild(img);\u001b[39m\n\u001b[31m  };\u001b[39m\n\u001b[31m  r.readAsDataURL(f);\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// ─── MANUAL TRACKING ─────────────────────────────────────────────────────\u001b[39m\n\u001b[31mfunction showManualTracking(docket, dispatchId, storeName, phone, city) {\u001b[39m\n\u001b[31m  const ov = document.getElementById('manualTrackOverlay');\u001b[39m\n\u001b[31m  ov.dataset.docket = docket;\u001b[39m\n\u001b[31m  ov.dataset.dispatchId = dispatchId;\u001b[39m\n\u001b[31m  ov.dataset.storeName = storeName;\u001b[39m\n\u001b[31m  ov.dataset.city = city;·\u001b[39m\n\u001b[31m  document.getElementById('mtDocketDisplay').textContent =\u001b[39m\n\u001b[31m    docket + '  ·  ' + storeName + '  ·  ' + city;\u001b[39m\n\u001b[31m  document.getElementById('mtContactUsed').value = phone || '';\u001b[39m\n\u001b[31m  document.getElementById('mtContactPerson').value = '';\u001b[39m\n\u001b[31m  document.getElementById('mtRemarks').value = '';\u001b[39m\n\u001b[31m  document.getElementById('mtStatus').value = '';\u001b[39m\n\u001b[31m  document.getElementById('mtCalledBy').value = '';·\u001b[39m\n\u001b[31m  // Set current datetime\u001b[39m\n\u001b[31m  const now = new Date();\u001b[39m\n\u001b[31m  const pad = n => String(n).padStart(2,'0');\u001b[39m\n\u001b[31m  document.getElementById('mtCallTime').value =\u001b[39m\n\u001b[31m    `${now.getFullYear()}-${pad(now.getMonth()+1)}-${pad(now.getDate())}T${pad(now.getHours())}:${pad(now.getMinutes())}`;·\u001b[39m\n\u001b[31m  openOverlay('manualTrackOverlay');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31masync function submitManualTracking() {\u001b[39m\n\u001b[31m  const ov = document.getElementById('manualTrackOverlay');\u001b[39m\n\u001b[31m  const docket     = ov.dataset.docket;\u001b[39m\n\u001b[31m  const dispatchId = ov.dataset.dispatchId;\u001b[39m\n\u001b[31m  const status     = document.getElementById('mtStatus').value;\u001b[39m\n\u001b[31m  const calledBy   = document.getElementById('mtCalledBy').value.trim();·\u001b[39m\n\u001b[31m  if (!status)    { showToast('Select a status', 'err'); return; }\u001b[39m\n\u001b[31m  if (!calledBy)  { showToast('Enter your name', 'err'); return; }·\u001b[39m\n\u001b[31m  const payload = {\u001b[39m\n\u001b[31m    type:          'manual_tracking',\u001b[39m\n\u001b[31m    docket,        dispatchId,\u001b[39m\n\u001b[31m    storeName:     ov.dataset.storeName,\u001b[39m\n\u001b[31m    city:          ov.dataset.city,\u001b[39m\n\u001b[31m    manualStatus:  status,\u001b[39m\n\u001b[31m    calledBy,\u001b[39m\n\u001b[31m    contactUsed:   document.getElementById('mtContactUsed').value.trim(),\u001b[39m\n\u001b[31m    contactPerson: document.getElementById('mtContactPerson').value.trim(),\u001b[39m\n\u001b[31m    remarks:       document.getElementById('mtRemarks').value.trim(),\u001b[39m\n\u001b[31m    callTime:      document.getElementById('mtCallTime').value || new Date().toISOString(),\u001b[39m\n\u001b[31m    timestamp:     new Date().toISOString()\u001b[39m\n\u001b[31m  };·\u001b[39m\n\u001b[31m  // Update local dispatch record\u001b[39m\n\u001b[31m  for (const d of dispatches) {\u001b[39m\n\u001b[31m    if (d.id !== dispatchId) continue;\u001b[39m\n\u001b[31m    for (const c of d.consignees) {\u001b[39m\n\u001b[31m      if (c.docket === docket) {\u001b[39m\n\u001b[31m        c.manualStatus    = status;\u001b[39m\n\u001b[31m        c.manualCalledBy  = calledBy;\u001b[39m\n\u001b[31m        c.manualTime      = payload.callTime;\u001b[39m\n\u001b[31m        c.manualRemarks   = payload.remarks;\u001b[39m\n\u001b[31m        // If confirmed delivered — flag so dashboard shows manual badge\u001b[39m\n\u001b[31m        // (not permanently locked — use resumeAutoTracking() to re-enable)\u001b[39m\n\u001b[31m        if (status === 'delivered') {\u001b[39m\n\u001b[31m          c.status = 'delivered';\u001b[39m\n\u001b[31m          c.manualDelivered = true;\u001b[39m\n\u001b[31m        } else {\u001b[39m\n\u001b[31m          // Non-delivered manual update: clear any previous lock\u001b[39m\n\u001b[31m          c.manualDelivered = false;\u001b[39m\n\u001b[31m        }\u001b[39m\n\u001b[31m      }\u001b[39m\n\u001b[31m    }\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m  saveData();\u001b[39m\n\u001b[31m  renderDashboard();\u001b[39m\n\u001b[31m  // Sync updated consignee to Postgres\u001b[39m\n\u001b[31m  const _d = dispatches.find(x => x.id === dispatchId);\u001b[39m\n\u001b[31m  if (_d) {\u001b[39m\n\u001b[31m    const _idx = _d.consignees.findIndex(c => c.docket === docket);\u001b[39m\n\u001b[31m    if (_idx >= 0) syncConsigneeToDB(_d.consignees[_idx], dispatchId, _idx);\u001b[39m\n\u001b[31m  }·\u001b[39m\n\u001b[31m  // Log to Sheets\u001b[39m\n\u001b[31m  if (window.appConfig.webhook_url) {\u001b[39m\n\u001b[31m    try {\u001b[39m\n\u001b[31m      await fetch(window.appConfig.webhook_url, {\u001b[39m\n\u001b[31m        method: 'POST', mode: 'no-cors',\u001b[39m\n\u001b[31m        headers: { 'Content-Type': 'application/json' },\u001b[39m\n\u001b[31m        body: JSON.stringify(payload)\u001b[39m\n\u001b[31m      });\u001b[39m\n\u001b[31m    } catch(e) {}\u001b[39m\n\u001b[31m  }·\u001b[39m\n\u001b[31m  closeOverlay('manualTrackOverlay');\u001b[39m\n\u001b[31m  showToast('Manual tracking logged ✓', 'ok');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// ─── TILE SELECTION + PARTIAL REPORT SHARE ───────────────────────────────\u001b[39m\n\u001b[31mlet _selectedDockets = new Set();·\u001b[39m\n\u001b[31mfunction toggleTileSelect(docket) {\u001b[39m\n\u001b[31m  if (_selectedDockets.has(docket)) _selectedDockets.delete(docket);\u001b[39m\n\u001b[31m  else _selectedDockets.add(docket);\u001b[39m\n\u001b[31m  // Update checkbox visual\u001b[39m\n\u001b[31m  const chk = document.getElementById('sel_' + docket.replace(/[^a-z0-9]/gi,'_'));\u001b[39m\n\u001b[31m  if (chk) chk.checked = _selectedDockets.has(docket);\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction shareSelectedReport() {\u001b[39m\n\u001b[31m  if (_selectedDockets.size === 0) {\u001b[39m\n\u001b[31m    showToast('Select at least one consignee tile first', 'err');\u001b[39m\n\u001b[31m    return;\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m  const activeDis = dispatches.filter(d => d.status !== 'draft');\u001b[39m\n\u001b[31m  const allItems  = activeDis.flatMap(d =>\u001b[39m\n\u001b[31m    (d.consignees||[]).map(c => Object.assign({}, c, {courier:d.courier, client:d.client, jobref:d.jobref, dispatchId:d.id}))\u001b[39m\n\u001b[31m  );\u001b[39m\n\u001b[31m  const selected = allItems.filter(c => _selectedDockets.has(c.docket));\u001b[39m\n\u001b[31m  if (!selected.length) { showToast('Selected dockets not found', 'err'); return; }·\u001b[39m\n\u001b[31m  const NL = '\\\\n';\u001b[39m\n\u001b[31m  const statusEmoji = {\u001b[39m\n\u001b[31m    delivered:'\\\\u2705', transit:'\\\\uD83D\\\\uDE9A', 'in-transit':'\\\\uD83D\\\\uDE9A',\u001b[39m\n\u001b[31m    booked:'\\\\uD83D\\\\uDCEC', pending:'\\\\u23F3', 'not received':'\\\\u274C', forwarded:'\\\\u27A1'\u001b[39m\n\u001b[31m  };·\u001b[39m\n\u001b[31m  const header = '\\\\uD83D\\\\uDCE6 *Dispatch Status Report*' + NL +\u001b[39m\n\u001b[31m    (selected[0].client || '') + ' ' + (selected[0].jobref ? '> ' + selected[0].jobref : '') + NL +\u001b[39m\n\u001b[31m    new Date().toLocaleDateString('en-IN', {day:'2-digit',month:'short',year:'numeric'}) + NL + NL;·\u001b[39m\n\u001b[31m  const rows = selected.map(function(c, i) {\u001b[39m\n\u001b[31m    const finalStatus = c.manualStatus === 'delivered' ? 'delivered' : (c.status || 'booked');\u001b[39m\n\u001b[31m    const emoji  = statusEmoji[finalStatus] || '\\\\uD83D\\\\uDCE6';\u001b[39m\n\u001b[31m    const source = c.manualStatus ? '(manual)' : '(auto)';\u001b[39m\n\u001b[31m    const remark = c.manualRemarks ? (NL + '   Note: ' + c.manualRemarks) : '';\u001b[39m\n\u001b[31m    return (i+1) + '. *' + c.name + '* - ' + c.city + NL +\u001b[39m\n\u001b[31m      '   ' + emoji + ' ' + finalStatus + ' ' + source + NL +\u001b[39m\n\u001b[31m      '   Docket: ' + c.docket + remark;\u001b[39m\n\u001b[31m  }).join(NL + NL);·\u001b[39m\n\u001b[31m  const footer = NL + NL + '_360 Degree Logical Marketing_';\u001b[39m\n\u001b[31m  window.open('https://wa.me/?text=' + encodeURIComponent(header + rows + footer), '_blank');\u001b[39m\n\u001b[31m  showToast('Report for ' + selected.length + ' consignees ready', 'ok');\u001b[39m\n\u001b[31m}··\u001b[39m\n\u001b[31m// ── resumeAutoTracking: undo a manual-delivered lock ──────────────────────\u001b[39m\n\u001b[31mfunction resumeAutoTracking(docket, dispatchId) {\u001b[39m\n\u001b[31m  for (const d of dispatches) {\u001b[39m\n\u001b[31m    if (d.id !== dispatchId) continue;\u001b[39m\n\u001b[31m    for (const c of d.consignees) {\u001b[39m\n\u001b[31m      if (c.docket === docket) {\u001b[39m\n\u001b[31m        c.manualDelivered = false;\u001b[39m\n\u001b[31m        c.status          = 'transit';\u001b[39m\n\u001b[31m        c.manualStatus    = '';\u001b[39m\n\u001b[31m      }\u001b[39m\n\u001b[31m    }\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m  saveData();\u001b[39m\n\u001b[31m  renderDashboard();\u001b[39m\n\u001b[31m  showToast(`Auto-tracking resumed for ${docket}`, 'ok');\u001b[39m\n\u001b[31m  const _rd = dispatches.find(x => x.id === dispatchId);\u001b[39m\n\u001b[31m  if (_rd) {\u001b[39m\n\u001b[31m    const _ri = _rd.consignees.findIndex(c => c.docket === docket);\u001b[39m\n\u001b[31m    if (_ri >= 0) syncConsigneeToDB(_rd.consignees[_ri], dispatchId, _ri);\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// ── refreshAllTracking (consolidated, try/finally for safety) ─────────────\u001b[39m\n\u001b[31masync function refreshAllTracking() {\u001b[39m\n\u001b[31m  const activeDis  = dispatches.filter(d => d.status !== 'draft');\u001b[39m\n\u001b[31m  const allCons    = activeDis.flatMap(d =>\u001b[39m\n\u001b[31m    (d.consignees || []).map(c => ({ ...c, dispatchId: d.id, courier: d.courier }))\u001b[39m\n\u001b[31m  );\u001b[39m\n\u001b[31m  const withDockets = allCons.filter(c =>\u001b[39m\n\u001b[31m    c.docket && c.status !== 'delivered' && !c.manualDelivered\u001b[39m\n\u001b[31m  );\u001b[39m\n\u001b[31m  if (!withDockets.length) { showToast('No active dockets to refresh', 'info'); return; }·\u001b[39m\n\u001b[31m  showToast(`⏳ Tracking ${withDockets.length} dockets...`, 'info');\u001b[39m\n\u001b[31m  withDockets.forEach(c => {\u001b[39m\n\u001b[31m    const id    = c.docket.replace(/[^a-z0-9]/gi, '_');\u001b[39m\n\u001b[31m    const evtEl = document.getElementById('evt_'  + id);\u001b[39m\n\u001b[31m    const spnEl = document.getElementById('spin_' + id);\u001b[39m\n\u001b[31m    if (evtEl) evtEl.style.display = 'none';\u001b[39m\n\u001b[31m    if (spnEl) spnEl.style.display = 'inline';\u001b[39m\n\u001b[31m  });·\u001b[39m\n\u001b[31m  let done = 0;\u001b[39m\n\u001b[31m  try {\u001b[39m\n\u001b[31m    for (const c of withDockets) {\u001b[39m\n\u001b[31m      const result = await fetchTracking(c.docket, c.courier || '');\u001b[39m\n\u001b[31m      const id    = c.docket.replace(/[^a-z0-9]/gi, '_');·\u001b[39m\n\u001b[31m      // Batch-write result into state (never touch DOM here)\u001b[39m\n\u001b[31m      if (result.status && result.status !== 'failed') {\u001b[39m\n\u001b[31m        for (const d of dispatches) {\u001b[39m\n\u001b[31m          if (d.id !== c.dispatchId) continue;\u001b[39m\n\u001b[31m          for (const con of d.consignees) {\u001b[39m\n\u001b[31m            if (con.docket === c.docket && !con.manualDelivered) {\u001b[39m\n\u001b[31m              con.status           = result.status;\u001b[39m\n\u001b[31m              con.lastEvent        = result.lastEvent || con.lastEvent;\u001b[39m\n\u001b[31m              con.lastScanLocation = result.lastScanLocation || '';\u001b[39m\n\u001b[31m              con.lastTrackedAt    = new Date().toISOString();\u001b[39m\n\u001b[31m            }\u001b[39m\n\u001b[31m          }\u001b[39m\n\u001b[31m        }\u001b[39m\n\u001b[31m      }·\u001b[39m\n\u001b[31m      // Update tile spinner/event text in DOM (cosmetic only)\u001b[39m\n\u001b[31m      const spnEl = document.getElementById('spin_' + id);\u001b[39m\n\u001b[31m      const evtEl = document.getElementById('evt_'  + id);\u001b[39m\n\u001b[31m      if (spnEl) spnEl.style.display = 'none';\u001b[39m\n\u001b[31m      if (evtEl) { evtEl.style.display = 'inline'; evtEl.textContent = result.lastEvent || 'Checked'; }·\u001b[39m\n\u001b[31m      if (result.status && result.status !== 'failed' && window.appConfig.webhook_url) {\u001b[39m\n\u001b[31m        logTrackingUpdate(c.docket, result.status, result.lastEvent || '', result.lastScanLocation || '');\u001b[39m\n\u001b[31m      }\u001b[39m\n\u001b[31m      done++;\u001b[39m\n\u001b[31m    }\u001b[39m\n\u001b[31m    showToast(`✓ ${done} dockets refreshed`, 'ok');\u001b[39m\n\u001b[31m  } finally {\u001b[39m\n\u001b[31m    // Always persist and re-render — even if loop throws mid-way\u001b[39m\n\u001b[31m    saveData();\u001b[39m\n\u001b[31m    renderDashboard();\u001b[39m\n\u001b[31m    // Push updated consignee states to Postgres (fire-and-forget)\u001b[39m\n\u001b[31m    dispatches.filter(d => d.status !== 'draft').forEach(d => syncDispatchToDB(d));\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m</script>\u001b[39m\n\u001b[31m</body>\u001b[39m\n\u001b[31m</html>\"\u001b[39m",
                        "stack": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoContain\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m) // indexOf\u001b[22m\n\nExpected substring: \u001b[32m\"window.location.replace('/hub/')\"\u001b[39m\nReceived string:    \u001b[31m\"<!DOCTYPE html>\u001b[39m\n\u001b[31m<html lang=\\\"en\\\">\u001b[39m\n\u001b[31m<head>\u001b[39m\n\u001b[31m<meta charset=\\\"UTF-8\\\">\u001b[39m\n\u001b[31m<meta name=\\\"viewport\\\" content=\\\"width=device-width,initial-scale=1,maximum-scale=1,user-scalable=no\\\">\u001b[39m\n\u001b[31m<meta name=\\\"theme-color\\\" content=\\\"#080c12\\\">\u001b[39m\n\u001b[31m<meta name=\\\"apple-mobile-web-app-capable\\\" content=\\\"yes\\\">\u001b[39m\n\u001b[31m<meta name=\\\"mobile-web-app-capable\\\" content=\\\"yes\\\">\u001b[39m\n\u001b[31m<title>360LM Dispatch</title>\u001b[39m\n\u001b[31m<link rel=\\\"manifest\\\" href=\\\"manifest.json\\\">\u001b[39m\n\u001b[31m<link href=\\\"https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@300;400;500;600;700&family=Fira+Code:wght@400;500&family=Barlow+Condensed:wght@400;600;700;800&display=swap\\\" rel=\\\"stylesheet\\\">\u001b[39m\n\u001b[31m<style>\u001b[39m\n\u001b[31m/* ════ RESET & VARS ════ */\u001b[39m\n\u001b[31m*{margin:0;padding:0;box-sizing:border-box;-webkit-tap-highlight-color:transparent;}\u001b[39m\n\u001b[31m:root{\u001b[39m\n\u001b[31m  --bg:#080c12; --bg2:#0d1420; --bg3:#111a28;\u001b[39m\n\u001b[31m  --sur:#141f30; --sur2:#1a2840; --sur3:#1f3050;\u001b[39m\n\u001b[31m  --bdr:#1e2e45; --bdr2:#253650;\u001b[39m\n\u001b[31m  --t1:#f0f4f8; --t2:#8ba4bc; --t3:#3d5470; --t4:#243348;\u001b[39m\n\u001b[31m  --acc:#f97316; --acc2:#fb923c; --acc-glow:rgba(249,115,22,0.25);\u001b[39m\n\u001b[31m  --grn:#22c55e; --grn-dim:rgba(34,197,94,0.15);\u001b[39m\n\u001b[31m  --sky:#38bdf8; --sky-dim:rgba(56,189,248,0.15);\u001b[39m\n\u001b[31m  --red:#ef4444; --red-dim:rgba(239,68,68,0.15);\u001b[39m\n\u001b[31m  --ylw:#eab308; --ylw-dim:rgba(234,179,8,0.15);\u001b[39m\n\u001b[31m  --fnt-head:'Barlow Condensed',sans-serif;\u001b[39m\n\u001b[31m  --fnt-body:'Space Grotesk',sans-serif;\u001b[39m\n\u001b[31m  --fnt-mono:'Fira Code',monospace;\u001b[39m\n\u001b[31m  --r:10px; --r-sm:6px; --r-lg:16px;\u001b[39m\n\u001b[31m  --sh:0 4px 24px rgba(0,0,0,0.4);\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31mhtml,body{height:100%;background:var(--bg);color:var(--t1);font-family:var(--fnt-body);overflow:hidden;}·\u001b[39m\n\u001b[31m/* ════ SPLASH ════ */\u001b[39m\n\u001b[31m#splash{\u001b[39m\n\u001b[31m  position:fixed;inset:0;background:var(--bg);z-index:999;\u001b[39m\n\u001b[31m  display:flex;flex-direction:column;align-items:center;justify-content:center;\u001b[39m\n\u001b[31m  transition:opacity .4s .1s;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.splash-logo{font-family:var(--fnt-head);font-size:3rem;font-weight:800;letter-spacing:2px;line-height:1;}\u001b[39m\n\u001b[31m.splash-logo em{color:var(--acc);font-style:normal;}\u001b[39m\n\u001b[31m.splash-sub{color:var(--t3);font-family:var(--fnt-mono);font-size:.7rem;letter-spacing:4px;margin-top:8px;}\u001b[39m\n\u001b[31m.splash-track{width:200px;height:2px;background:var(--bdr);border-radius:2px;margin-top:40px;overflow:hidden;}\u001b[39m\n\u001b[31m.splash-track-fill{height:100%;width:0;background:linear-gradient(90deg,var(--acc),var(--acc2));animation:splashLoad 1.3s cubic-bezier(.4,0,.2,1) forwards;}\u001b[39m\n\u001b[31m@keyframes splashLoad{to{width:100%;}}·\u001b[39m\n\u001b[31m/* ════ LAYOUT ════ */\u001b[39m\n\u001b[31m#app{height:100%;display:flex;flex-direction:column;}\u001b[39m\n\u001b[31m.hdr{\u001b[39m\n\u001b[31m  background:var(--bg2);border-bottom:1px solid var(--bdr);\u001b[39m\n\u001b[31m  padding:10px 14px;display:flex;align-items:center;gap:10px;flex-shrink:0;\u001b[39m\n\u001b[31m  position:relative;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.hdr-brand{font-family:var(--fnt-head);font-size:1.3rem;font-weight:800;letter-spacing:1px;flex:1;}\u001b[39m\n\u001b[31m.hdr-brand em{color:var(--acc);font-style:normal;}\u001b[39m\n\u001b[31m.hdr-dot{width:7px;height:7px;border-radius:50%;background:var(--grn);transition:background .3s;animation:blink 2s infinite;}\u001b[39m\n\u001b[31m@keyframes blink{0%,100%{opacity:1;}50%{opacity:.4;}}\u001b[39m\n\u001b[31m.hdr-btn{\u001b[39m\n\u001b[31m  background:none;border:none;color:var(--t2);cursor:pointer;\u001b[39m\n\u001b[31m  padding:6px 8px;border-radius:var(--r-sm);transition:all .2s;\u001b[39m\n\u001b[31m  font-family:var(--fnt-head);font-size:.85rem;font-weight:700;letter-spacing:.5px;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.hdr-btn:hover{background:var(--bdr);color:var(--t1);}\u001b[39m\n\u001b[31m.hdr-btn.primary{background:var(--acc);color:#fff;padding:6px 14px;}\u001b[39m\n\u001b[31m.hdr-btn.primary:hover{background:var(--acc2);}·\u001b[39m\n\u001b[31m/* ════ TAB BAR ════ */\u001b[39m\n\u001b[31m.tabs{\u001b[39m\n\u001b[31m  display:flex;background:var(--bg2);border-bottom:1px solid var(--bdr);\u001b[39m\n\u001b[31m  overflow-x:auto;flex-shrink:0;padding:0 6px;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.tabs::-webkit-scrollbar{display:none;}\u001b[39m\n\u001b[31m.tab{\u001b[39m\n\u001b[31m  background:none;border:none;color:var(--t3);font-family:var(--fnt-head);\u001b[39m\n\u001b[31m  font-size:.85rem;font-weight:700;letter-spacing:1px;padding:10px 14px;\u001b[39m\n\u001b[31m  cursor:pointer;border-bottom:2px solid transparent;white-space:nowrap;\u001b[39m\n\u001b[31m  transition:all .2s;flex-shrink:0;display:flex;align-items:center;gap:6px;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.tab.active{color:var(--acc);border-bottom-color:var(--acc);}\u001b[39m\n\u001b[31m.tab:hover:not(.active){color:var(--t2);}\u001b[39m\n\u001b[31m.tab-badge{\u001b[39m\n\u001b[31m  background:var(--acc);color:#fff;font-size:.6rem;font-weight:700;\u001b[39m\n\u001b[31m  padding:1px 5px;border-radius:10px;font-family:var(--fnt-mono);\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m/* ════ CONTENT AREAS ════ */\u001b[39m\n\u001b[31m.pane{flex:1;overflow-y:auto;padding:14px;display:none;}\u001b[39m\n\u001b[31m.pane.active{display:block;}\u001b[39m\n\u001b[31m.pane::-webkit-scrollbar{width:3px;}\u001b[39m\n\u001b[31m.pane::-webkit-scrollbar-thumb{background:var(--bdr2);}·\u001b[39m\n\u001b[31m/* ════ CARDS ════ */\u001b[39m\n\u001b[31m.card{\u001b[39m\n\u001b[31m  background:var(--sur);border:1px solid var(--bdr);border-radius:var(--r);\u001b[39m\n\u001b[31m  padding:14px;margin-bottom:12px;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.card.accent-left{border-left:3px solid var(--acc);}\u001b[39m\n\u001b[31m.card-title{\u001b[39m\n\u001b[31m  font-family:var(--fnt-head);font-size:.72rem;font-weight:700;\u001b[39m\n\u001b[31m  color:var(--t3);text-transform:uppercase;letter-spacing:2.5px;\u001b[39m\n\u001b[31m  margin-bottom:12px;display:flex;align-items:center;gap:8px;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.card-title .ct-dot{width:5px;height:5px;border-radius:50%;background:var(--acc);flex-shrink:0;}·\u001b[39m\n\u001b[31m/* ════ FORM ════ */\u001b[39m\n\u001b[31m.f{margin-bottom:12px;}\u001b[39m\n\u001b[31m.f label{\u001b[39m\n\u001b[31m  display:block;font-size:.68rem;color:var(--t3);margin-bottom:5px;\u001b[39m\n\u001b[31m  font-weight:600;text-transform:uppercase;letter-spacing:1.5px;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.f label .req{color:var(--acc);margin-left:2px;}\u001b[39m\n\u001b[31m.f input,.f select,.f textarea{\u001b[39m\n\u001b[31m  width:100%;background:var(--sur2);border:1px solid var(--bdr);\u001b[39m\n\u001b[31m  border-radius:var(--r-sm);color:var(--t1);font-family:var(--fnt-body);\u001b[39m\n\u001b[31m  font-size:.88rem;padding:9px 11px;outline:none;transition:border-color .2s;\u001b[39m\n\u001b[31m  -webkit-appearance:none;appearance:none;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.f input:focus,.f select:focus,.f textarea:focus{border-color:var(--acc);}\u001b[39m\n\u001b[31m.f input::placeholder{color:var(--t4);}\u001b[39m\n\u001b[31m.f textarea{resize:vertical;min-height:65px;}\u001b[39m\n\u001b[31m.f select{\u001b[39m\n\u001b[31m  background-image:url(\\\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='7'%3E%3Cpath d='M1 1l4 4 4-4' stroke='%233d5470' stroke-width='1.5' fill='none' stroke-linecap='round'/%3E%3C/svg%3E\\\");\u001b[39m\n\u001b[31m  background-repeat:no-repeat;background-position:right 10px center;padding-right:28px;cursor:pointer;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.row2{display:grid;grid-template-columns:1fr 1fr;gap:10px;}\u001b[39m\n\u001b[31m.row3{display:grid;grid-template-columns:1fr 1fr 1fr;gap:8px;}·\u001b[39m\n\u001b[31m/* ════ BUTTONS ════ */\u001b[39m\n\u001b[31m.btn{\u001b[39m\n\u001b[31m  width:100%;padding:12px;border:none;border-radius:var(--r);\u001b[39m\n\u001b[31m  font-family:var(--fnt-head);font-size:.9rem;font-weight:700;letter-spacing:.5px;\u001b[39m\n\u001b[31m  cursor:pointer;transition:all .2s;display:flex;align-items:center;justify-content:center;gap:8px;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.btn-orange{background:var(--acc);color:#fff;}\u001b[39m\n\u001b[31m.btn-orange:hover{background:var(--acc2);}\u001b[39m\n\u001b[31m.btn-ghost{background:var(--sur2);border:1px solid var(--bdr);color:var(--t1);}\u001b[39m\n\u001b[31m.btn-ghost:hover{border-color:var(--acc);color:var(--acc);}\u001b[39m\n\u001b[31m.btn-grn{background:var(--grn);color:#000;}\u001b[39m\n\u001b[31m.btn-grn:hover{opacity:.85;}\u001b[39m\n\u001b[31m.btn:active{transform:scale(.98);}\u001b[39m\n\u001b[31m.btn-row{display:grid;grid-template-columns:1fr 1fr;gap:10px;margin-top:6px;}\u001b[39m\n\u001b[31m.btn-sm{\u001b[39m\n\u001b[31m  background:none;border:1px solid var(--bdr);color:var(--t2);\u001b[39m\n\u001b[31m  font-size:.7rem;padding:5px 10px;border-radius:var(--r-sm);cursor:pointer;\u001b[39m\n\u001b[31m  font-family:var(--fnt-body);transition:all .2s;white-space:nowrap;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.btn-sm:hover{border-color:var(--acc);color:var(--acc);}\u001b[39m\n\u001b[31m.btn-sm.primary{background:var(--acc);border-color:var(--acc);color:#fff;}·\u001b[39m\n\u001b[31m/* ════ OCR UPLOAD ZONE ════ */\u001b[39m\n\u001b[31m.upload-zone{\u001b[39m\n\u001b[31m  border:2px dashed var(--bdr2);border-radius:var(--r);\u001b[39m\n\u001b[31m  padding:20px;text-align:center;cursor:pointer;transition:all .25s;\u001b[39m\n\u001b[31m  background:var(--sur2);position:relative;overflow:hidden;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.upload-zone:hover,.upload-zone.drag{border-color:var(--acc);background:rgba(249,115,22,.05);}\u001b[39m\n\u001b[31m.upload-zone .uz-icon{font-size:2rem;margin-bottom:8px;}\u001b[39m\n\u001b[31m.upload-zone .uz-title{font-family:var(--fnt-head);font-size:1rem;font-weight:700;color:var(--t1);}\u001b[39m\n\u001b[31m.upload-zone .uz-sub{font-size:.72rem;color:var(--t3);margin-top:4px;font-family:var(--fnt-mono);}\u001b[39m\n\u001b[31m.upload-zone input{display:none;}\u001b[39m\n\u001b[31m.upload-zone.share-target{border-color:var(--grn);}·\u001b[39m\n\u001b[31m/* ════ OCR RESULTS ════ */\u001b[39m\n\u001b[31m.ocr-results{display:none;}\u001b[39m\n\u001b[31m.ocr-results.visible{display:block;}\u001b[39m\n\u001b[31m.ocr-slip-list{display:flex;flex-direction:column;gap:8px;margin-top:10px;}\u001b[39m\n\u001b[31m.ocr-slip{\u001b[39m\n\u001b[31m  background:var(--sur2);border:1px solid var(--bdr);border-radius:var(--r-sm);\u001b[39m\n\u001b[31m  padding:10px 12px;display:flex;gap:10px;align-items:flex-start;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.ocr-slip img{width:60px;height:60px;object-fit:cover;border-radius:6px;flex-shrink:0;border:1px solid var(--bdr2);}\u001b[39m\n\u001b[31m.ocr-slip-body{flex:1;min-width:0;}\u001b[39m\n\u001b[31m.ocr-slip-name{font-size:.7rem;color:var(--t3);font-family:var(--fnt-mono);margin-bottom:4px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}\u001b[39m\n\u001b[31m.ocr-slip-docket{\u001b[39m\n\u001b[31m  font-family:var(--fnt-mono);font-size:.9rem;font-weight:500;color:var(--acc);\u001b[39m\n\u001b[31m  background:var(--bg);border:1px solid var(--bdr);border-radius:4px;\u001b[39m\n\u001b[31m  padding:4px 8px;width:100%;outline:none;transition:border-color .2s;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.ocr-slip-docket:focus{border-color:var(--acc);}\u001b[39m\n\u001b[31m.ocr-slip-status{font-size:.65rem;margin-top:4px;display:flex;align-items:center;gap:6px;}\u001b[39m\n\u001b[31m.ocr-confidence{padding:2px 6px;border-radius:10px;font-family:var(--fnt-mono);}\u001b[39m\n\u001b[31m.conf-high{background:var(--grn-dim);color:var(--grn);}\u001b[39m\n\u001b[31m.conf-med{background:var(--ylw-dim);color:var(--ylw);}\u001b[39m\n\u001b[31m.conf-low{background:var(--red-dim);color:var(--red);}\u001b[39m\n\u001b[31m.ocr-ai-btn{\u001b[39m\n\u001b[31m  background:rgba(56,189,248,.15);border:1px solid rgba(56,189,248,.3);\u001b[39m\n\u001b[31m  color:var(--sky);font-size:.65rem;padding:3px 8px;border-radius:10px;\u001b[39m\n\u001b[31m  cursor:pointer;font-family:var(--fnt-mono);transition:all .2s;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.ocr-ai-btn:hover{background:rgba(56,189,248,.3);}\u001b[39m\n\u001b[31m.ocr-working{\u001b[39m\n\u001b[31m  background:var(--ylw-dim);border:1px solid rgba(234,179,8,.3);\u001b[39m\n\u001b[31m  border-radius:var(--r-sm);padding:8px 12px;margin-top:8px;\u001b[39m\n\u001b[31m  font-size:.75rem;color:var(--ylw);font-family:var(--fnt-mono);\u001b[39m\n\u001b[31m  display:none;align-items:center;gap:8px;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.ocr-working.visible{display:flex;}\u001b[39m\n\u001b[31m.spin{animation:spin .8s linear infinite;display:inline-block;}\u001b[39m\n\u001b[31m@keyframes spin{to{transform:rotate(360deg);}}·\u001b[39m\n\u001b[31m/* ════ STORE LIST ════ */\u001b[39m\n\u001b[31m.store-upload-zone{\u001b[39m\n\u001b[31m  border:2px dashed var(--bdr2);border-radius:var(--r);padding:16px;\u001b[39m\n\u001b[31m  text-align:center;cursor:pointer;transition:all .25s;background:var(--sur2);\u001b[39m\n\u001b[31m  position:relative;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.store-upload-zone:hover{border-color:var(--sky);background:rgba(56,189,248,.05);}\u001b[39m\n\u001b[31m.store-upload-zone input{display:none;}\u001b[39m\n\u001b[31m.store-table-wrap{max-height:200px;overflow-y:auto;margin-top:10px;border-radius:var(--r-sm);overflow:hidden;}\u001b[39m\n\u001b[31m.store-table{width:100%;border-collapse:collapse;font-size:.75rem;}\u001b[39m\n\u001b[31m.store-table th{\u001b[39m\n\u001b[31m  background:var(--bg);color:var(--t3);font-family:var(--fnt-head);\u001b[39m\n\u001b[31m  font-size:.65rem;letter-spacing:1.5px;text-transform:uppercase;\u001b[39m\n\u001b[31m  padding:7px 8px;text-align:left;border-bottom:1px solid var(--bdr);\u001b[39m\n\u001b[31m  position:sticky;top:0;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.store-table td{\u001b[39m\n\u001b[31m  padding:6px 8px;border-bottom:1px solid var(--bdr);color:var(--t2);\u001b[39m\n\u001b[31m  font-family:var(--fnt-mono);font-size:.72rem;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.store-table tr:hover td{background:var(--sur3);}\u001b[39m\n\u001b[31m.store-table .matched{color:var(--grn);}\u001b[39m\n\u001b[31m.store-table .unmatched{color:var(--ylw);}\u001b[39m\n\u001b[31m.store-map-btn{\u001b[39m\n\u001b[31m  font-size:.65rem;background:none;border:1px solid var(--bdr);color:var(--t3);\u001b[39m\n\u001b[31m  padding:2px 6px;border-radius:4px;cursor:pointer;transition:all .2s;\u001b[39m\n\u001b[31m  font-family:var(--fnt-mono);\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.store-map-btn:hover{border-color:var(--acc);color:var(--acc);}·\u001b[39m\n\u001b[31m/* ════ CONSIGNEE CARDS ════ */\u001b[39m\n\u001b[31m.cons-list{display:flex;flex-direction:column;gap:8px;margin-bottom:10px;}\u001b[39m\n\u001b[31m.cons-card{\u001b[39m\n\u001b[31m  background:var(--sur2);border:1px solid var(--bdr);border-radius:var(--r-sm);\u001b[39m\n\u001b[31m  padding:11px;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.cons-hdr{display:flex;justify-content:space-between;align-items:center;margin-bottom:8px;}\u001b[39m\n\u001b[31m.cons-num{font-family:var(--fnt-mono);font-size:.65rem;color:var(--t3);}\u001b[39m\n\u001b[31m.cons-del{\u001b[39m\n\u001b[31m  background:none;border:none;color:var(--t4);cursor:pointer;\u001b[39m\n\u001b[31m  font-size:.85rem;padding:2px 5px;border-radius:4px;transition:all .2s;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.cons-del:hover{background:var(--red-dim);color:var(--red);}\u001b[39m\n\u001b[31m.cons-docket-linked{\u001b[39m\n\u001b[31m  display:flex;align-items:center;gap:6px;\u001b[39m\n\u001b[31m  background:rgba(249,115,22,.1);border:1px solid rgba(249,115,22,.3);\u001b[39m\n\u001b[31m  border-radius:4px;padding:4px 8px;margin-top:6px;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.cons-docket-linked .dkt{font-family:var(--fnt-mono);font-size:.82rem;color:var(--acc);}\u001b[39m\n\u001b[31m.cons-docket-linked .unlink{background:none;border:none;color:var(--acc);cursor:pointer;font-size:.75rem;}·\u001b[39m\n\u001b[31m/* ════ DISPATCH DASHBOARD ════ */\u001b[39m\n\u001b[31m.dash-stats{\u001b[39m\n\u001b[31m  display:grid;grid-template-columns:repeat(4,1fr);gap:8px;margin-bottom:14px;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.stat-box{\u001b[39m\n\u001b[31m  background:var(--sur);border:1px solid var(--bdr);border-radius:var(--r);\u001b[39m\n\u001b[31m  padding:10px 8px;text-align:center;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.stat-val{font-family:var(--fnt-head);font-size:1.4rem;font-weight:800;}\u001b[39m\n\u001b[31m.stat-lbl{font-size:.6rem;color:var(--t3);font-family:var(--fnt-mono);text-transform:uppercase;letter-spacing:1px;margin-top:2px;}\u001b[39m\n\u001b[31m.stat-box.grn .stat-val{color:var(--grn);}\u001b[39m\n\u001b[31m.stat-box.ylw .stat-val{color:var(--ylw);}\u001b[39m\n\u001b[31m.stat-box.red .stat-val{color:var(--red);}\u001b[39m\n\u001b[31m.stat-box.sky .stat-val{color:var(--sky);}·\u001b[39m\n\u001b[31m/* ════ DOCKET TILES ════ */\u001b[39m\n\u001b[31m.docket-grid{display:flex;flex-direction:column;gap:8px;}\u001b[39m\n\u001b[31m.docket-tile{\u001b[39m\n\u001b[31m  background:var(--sur);border:1px solid var(--bdr);border-radius:var(--r);\u001b[39m\n\u001b[31m  padding:12px;transition:border-color .2s;cursor:pointer;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.docket-tile:hover{border-color:var(--acc);}\u001b[39m\n\u001b[31m.docket-tile-hdr{display:flex;justify-content:space-between;align-items:flex-start;margin-bottom:8px;}\u001b[39m\n\u001b[31m.docket-tile-left{flex:1;}\u001b[39m\n\u001b[31m.docket-num{font-family:var(--fnt-mono);font-size:.95rem;font-weight:500;color:var(--sky);letter-spacing:1px;}\u001b[39m\n\u001b[31m.docket-store{font-size:.82rem;color:var(--t1);font-weight:600;margin-top:2px;}\u001b[39m\n\u001b[31m.docket-courier{font-size:.68rem;color:var(--t3);font-family:var(--fnt-mono);margin-top:2px;}\u001b[39m\n\u001b[31m.status-pill{\u001b[39m\n\u001b[31m  font-size:.62rem;font-weight:700;font-family:var(--fnt-mono);\u001b[39m\n\u001b[31m  padding:3px 8px;border-radius:10px;text-transform:uppercase;letter-spacing:.5px;\u001b[39m\n\u001b[31m  flex-shrink:0;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.sp-delivered{background:var(--grn-dim);color:var(--grn);}\u001b[39m\n\u001b[31m.sp-transit{background:var(--ylw-dim);color:var(--ylw);}\u001b[39m\n\u001b[31m.sp-booked{background:var(--sky-dim);color:var(--sky);}\u001b[39m\n\u001b[31m.sp-pending{background:var(--bdr);color:var(--t3);}\u001b[39m\n\u001b[31m.sp-failed{background:var(--red-dim);color:var(--red);}\u001b[39m\n\u001b[31m.docket-tile-footer{display:flex;align-items:center;gap:8px;flex-wrap:wrap;}\u001b[39m\n\u001b[31m.docket-last-event{font-size:.7rem;color:var(--t3);flex:1;font-family:var(--fnt-mono);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}\u001b[39m\n\u001b[31m.btn-track-live{\u001b[39m\n\u001b[31m  background:none;border:1px solid var(--acc);color:var(--acc);\u001b[39m\n\u001b[31m  font-size:.65rem;padding:4px 10px;border-radius:20px;cursor:pointer;\u001b[39m\n\u001b[31m  font-family:var(--fnt-mono);transition:all .2s;white-space:nowrap;display:flex;align-items:center;gap:4px;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.btn-track-live:hover{background:var(--acc);color:#fff;}\u001b[39m\n\u001b[31m.btn-track-live .live-dot{width:5px;height:5px;border-radius:50%;background:var(--grn);animation:blink 1.5s infinite;flex-shrink:0;}\u001b[39m\n\u001b[31m.docket-progress{height:2px;background:var(--bdr);border-radius:2px;margin-top:8px;overflow:hidden;}\u001b[39m\n\u001b[31m.docket-progress-fill{height:100%;border-radius:2px;transition:width .5s;}·\u001b[39m\n\u001b[31m/* ════ TRACKING DETAIL ════ */\u001b[39m\n\u001b[31m.track-timeline{padding:4px 0;}\u001b[39m\n\u001b[31m.track-evt{display:flex;gap:10px;padding:6px 0;position:relative;padding-left:20px;}\u001b[39m\n\u001b[31m.track-evt::before{\u001b[39m\n\u001b[31m  content:'';position:absolute;left:5px;top:11px;\u001b[39m\n\u001b[31m  width:7px;height:7px;border-radius:50%;background:var(--bdr2);border:1px solid var(--bdr2);\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.track-evt:first-child::before{background:var(--grn);border-color:var(--grn);}\u001b[39m\n\u001b[31m.track-evt::after{\u001b[39m\n\u001b[31m  content:'';position:absolute;left:8px;top:18px;\u001b[39m\n\u001b[31m  width:1px;height:calc(100% - 6px);background:var(--bdr);\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.track-evt:last-child::after{display:none;}\u001b[39m\n\u001b[31m.track-time{font-family:var(--fnt-mono);font-size:.62rem;color:var(--t3);min-width:90px;flex-shrink:0;padding-top:2px;}\u001b[39m\n\u001b[31m.track-desc{font-size:.78rem;color:var(--t2);}\u001b[39m\n\u001b[31m.track-loc{font-size:.67rem;color:var(--t3);font-family:var(--fnt-mono);margin-top:2px;}·\u001b[39m\n\u001b[31m/* ════ SHARE TOAST ════ */\u001b[39m\n\u001b[31m.share-banner{\u001b[39m\n\u001b[31m  position:fixed;top:0;left:0;right:0;z-index:200;\u001b[39m\n\u001b[31m  background:rgba(34,197,94,.15);border-bottom:1px solid rgba(34,197,94,.4);\u001b[39m\n\u001b[31m  padding:10px 16px;font-size:.78rem;color:var(--grn);font-family:var(--fnt-mono);\u001b[39m\n\u001b[31m  display:flex;align-items:center;gap:8px;\u001b[39m\n\u001b[31m  transform:translateY(-100%);transition:transform .3s;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.share-banner.show{transform:translateY(0);}·\u001b[39m\n\u001b[31m/* ════ MODAL ════ */\u001b[39m\n\u001b[31m.overlay{position:fixed;inset:0;background:rgba(0,0,0,.75);z-index:100;display:flex;align-items:flex-end;opacity:0;pointer-events:none;transition:opacity .3s;}\u001b[39m\n\u001b[31m.overlay.open{opacity:1;pointer-events:all;}\u001b[39m\n\u001b[31m.sheet{\u001b[39m\n\u001b[31m  background:var(--sur);border-radius:var(--r-lg) var(--r-lg) 0 0;\u001b[39m\n\u001b[31m  width:100%;max-height:88vh;overflow-y:auto;padding:18px;\u001b[39m\n\u001b[31m  transform:translateY(100%);transition:transform .3s cubic-bezier(.32,.72,0,1);\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.overlay.open .sheet{transform:translateY(0);}\u001b[39m\n\u001b[31m.sheet-handle{width:36px;height:4px;background:var(--bdr2);border-radius:2px;margin:0 auto 18px;}\u001b[39m\n\u001b[31m.sheet-title{font-family:var(--fnt-head);font-size:1.2rem;font-weight:800;letter-spacing:.5px;margin-bottom:14px;}\u001b[39m\n\u001b[31m.sheet::-webkit-scrollbar{width:3px;}\u001b[39m\n\u001b[31m.sheet::-webkit-scrollbar-thumb{background:var(--bdr2);}·\u001b[39m\n\u001b[31m/* ════ TOAST ════ */\u001b[39m\n\u001b[31m.toast{\u001b[39m\n\u001b[31m  position:fixed;bottom:20px;left:50%;transform:translateX(-50%) translateY(70px);\u001b[39m\n\u001b[31m  background:var(--sur2);border:1px solid var(--bdr2);color:var(--t1);\u001b[39m\n\u001b[31m  font-size:.78rem;padding:9px 16px;border-radius:30px;z-index:300;\u001b[39m\n\u001b[31m  transition:transform .3s;white-space:nowrap;font-family:var(--fnt-mono);\u001b[39m\n\u001b[31m  pointer-events:none;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.toast.show{transform:translateX(-50%) translateY(0);}\u001b[39m\n\u001b[31m.toast.ok{border-color:var(--grn);color:var(--grn);}\u001b[39m\n\u001b[31m.toast.err{border-color:var(--red);color:var(--red);}\u001b[39m\n\u001b[31m.toast.info{border-color:var(--sky);color:var(--sky);}·\u001b[39m\n\u001b[31m/* ════ COURIER MASTER ════ */\u001b[39m\n\u001b[31m.courier-master-row{\u001b[39m\n\u001b[31m  display:flex;align-items:center;gap:8px;padding:8px 0;\u001b[39m\n\u001b[31m  border-bottom:1px solid var(--bdr);\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.courier-master-row:last-child{border-bottom:none;}\u001b[39m\n\u001b[31m.cml{flex:1;}\u001b[39m\n\u001b[31m.cml-name{font-weight:600;font-size:.85rem;}\u001b[39m\n\u001b[31m.cml-url{font-family:var(--fnt-mono);font-size:.65rem;color:var(--t3);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:180px;}\u001b[39m\n\u001b[31m.courier-logo{width:24px;height:24px;border-radius:4px;object-fit:contain;background:var(--sur3);padding:2px;}·\u001b[39m\n\u001b[31m/* ════ PROGRESS BAR ════ */\u001b[39m\n\u001b[31m.ai-progress{\u001b[39m\n\u001b[31m  background:var(--sur2);border:1px solid var(--bdr);border-radius:var(--r-sm);\u001b[39m\n\u001b[31m  padding:10px 12px;margin-top:8px;display:none;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.ai-progress.visible{display:block;}\u001b[39m\n\u001b[31m.ai-prog-bar{height:3px;background:var(--bdr);border-radius:3px;margin-top:6px;overflow:hidden;}\u001b[39m\n\u001b[31m.ai-prog-fill{height:100%;background:linear-gradient(90deg,var(--sky),var(--acc));width:0;transition:width .3s;}\u001b[39m\n\u001b[31m.ai-prog-label{font-size:.68rem;color:var(--sky);font-family:var(--fnt-mono);}·\u001b[39m\n\u001b[31m/* ════ EMPTY ════ */\u001b[39m\n\u001b[31m.empty{padding:40px 20px;text-align:center;color:var(--t4);}\u001b[39m\n\u001b[31m.empty .e-icon{font-size:2.5rem;margin-bottom:10px;}\u001b[39m\n\u001b[31m.empty p{font-size:.82rem;}·\u001b[39m\n\u001b[31m/* ════ INFO TAG ════ */\u001b[39m\n\u001b[31m.info-tag{\u001b[39m\n\u001b[31m  background:var(--sur2);border:1px solid var(--bdr);border-radius:var(--r-sm);\u001b[39m\n\u001b[31m  padding:7px 10px;font-size:.75rem;color:var(--t2);margin-bottom:8px;\u001b[39m\n\u001b[31m  display:flex;align-items:center;gap:8px;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.info-tag strong{color:var(--t1);}·\u001b[39m\n\u001b[31m/* ════ JOB PILL ════ */\u001b[39m\n\u001b[31m.job-pill{\u001b[39m\n\u001b[31m  display:inline-flex;align-items:center;gap:6px;\u001b[39m\n\u001b[31m  background:rgba(249,115,22,.1);border:1px solid rgba(249,115,22,.3);\u001b[39m\n\u001b[31m  color:var(--acc);font-family:var(--fnt-mono);font-size:.68rem;\u001b[39m\n\u001b[31m  padding:3px 9px;border-radius:20px;margin-top:4px;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.job-pill button{background:none;border:none;color:var(--acc);cursor:pointer;font-size:.7rem;}·\u001b[39m\n\u001b[31m/* ════ SECTION DIVIDER ════ */\u001b[39m\n\u001b[31m.sec-div{\u001b[39m\n\u001b[31m  display:flex;align-items:center;gap:10px;margin:14px 0 10px;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.sec-div span{font-family:var(--fnt-head);font-size:.7rem;font-weight:700;color:var(--t3);text-transform:uppercase;letter-spacing:2px;white-space:nowrap;}\u001b[39m\n\u001b[31m.sec-div::before,.sec-div::after{content:'';flex:1;height:1px;background:var(--bdr);}·\u001b[39m\n\u001b[31m/* ── MAPPING UI ── */\u001b[39m\n\u001b[31m.map-table{width:100%;border-collapse:collapse;font-size:.78rem;margin:10px 0;}\u001b[39m\n\u001b[31m.map-table th{background:var(--bg);color:var(--t3);font-family:var(--fnt-head);font-size:.65rem;\u001b[39m\n\u001b[31m  letter-spacing:1.5px;text-transform:uppercase;padding:7px 10px;text-align:left;\u001b[39m\n\u001b[31m  border-bottom:1px solid var(--bdr);}\u001b[39m\n\u001b[31m.map-table td{padding:7px 10px;border-bottom:1px solid var(--bdr);vertical-align:middle;}\u001b[39m\n\u001b[31m.map-table tr:last-child td{border-bottom:none;}\u001b[39m\n\u001b[31m.map-table select{background:var(--sur2);border:1px solid var(--bdr);color:var(--t1);\u001b[39m\n\u001b[31m  font-family:var(--fnt-mono);font-size:.75rem;padding:5px 8px;border-radius:var(--r-sm);\u001b[39m\n\u001b[31m  width:100%;outline:none;}\u001b[39m\n\u001b[31m.map-table select:focus{border-color:var(--acc);}\u001b[39m\n\u001b[31m.map-req{color:var(--acc);margin-left:2px;}\u001b[39m\n\u001b[31m.map-saved-badge{font-size:.6rem;background:rgba(57,255,143,.15);color:var(--accent3,#39ff8f);\u001b[39m\n\u001b[31m  border:1px solid rgba(57,255,143,.3);padding:2px 7px;border-radius:10px;\u001b[39m\n\u001b[31m  font-family:var(--fnt-mono);margin-left:6px;}·\u001b[39m\n\u001b[31m/* ── VERIFICATION SCREEN ── */\u001b[39m\n\u001b[31m#verifyPane{display:none;position:fixed;inset:0;background:var(--bg);z-index:50;\u001b[39m\n\u001b[31m  overflow-y:auto;padding:14px;}\u001b[39m\n\u001b[31m.verify-hdr{display:flex;align-items:center;gap:10px;margin-bottom:14px;\u001b[39m\n\u001b[31m  position:sticky;top:0;background:var(--bg);padding-bottom:10px;\u001b[39m\n\u001b[31m  border-bottom:1px solid var(--bdr);z-index:1;}\u001b[39m\n\u001b[31m.verify-title{font-family:var(--fnt-head);font-size:1.1rem;font-weight:800;flex:1;}\u001b[39m\n\u001b[31m.verify-row{background:var(--sur);border:1px solid var(--bdr);border-radius:var(--r-sm);\u001b[39m\n\u001b[31m  padding:10px 12px;margin-bottom:8px;position:relative;}\u001b[39m\n\u001b[31m.verify-row.has-dupe{border-color:var(--red);background:rgba(239,68,68,.04);}\u001b[39m\n\u001b[31m.verify-row.no-docket{border-color:var(--ylw);background:rgba(234,179,8,.04);}\u001b[39m\n\u001b[31m.verify-row.ok{border-left:3px solid var(--grn);}\u001b[39m\n\u001b[31m.verify-row-hdr{display:flex;align-items:center;gap:8px;margin-bottom:8px;}\u001b[39m\n\u001b[31m.verify-num{font-family:var(--fnt-mono);font-size:.65rem;color:var(--t3);}\u001b[39m\n\u001b[31m.verify-store{font-weight:600;font-size:.9rem;flex:1;}\u001b[39m\n\u001b[31m.verify-flag{font-size:.65rem;font-weight:700;font-family:var(--fnt-mono);\u001b[39m\n\u001b[31m  padding:2px 7px;border-radius:10px;text-transform:uppercase;}\u001b[39m\n\u001b[31m.vf-dupe{background:var(--red-dim);color:var(--red);}\u001b[39m\n\u001b[31m.vf-nodocket{background:var(--ylw-dim);color:var(--ylw);}\u001b[39m\n\u001b[31m.vf-ok{background:var(--grn-dim);color:var(--grn);}\u001b[39m\n\u001b[31m.vf-short{background:rgba(56,189,248,.15);color:var(--sky);}\u001b[39m\n\u001b[31m.verify-fields{display:grid;grid-template-columns:1fr 1fr;gap:8px;margin-bottom:8px;}\u001b[39m\n\u001b[31m.verify-field label{font-size:.62rem;color:var(--t3);text-transform:uppercase;\u001b[39m\n\u001b[31m  letter-spacing:1px;display:block;margin-bottom:3px;font-family:var(--fnt-mono);}\u001b[39m\n\u001b[31m.verify-field input{width:100%;background:var(--sur2);border:1px solid var(--bdr);\u001b[39m\n\u001b[31m  color:var(--t1);font-family:var(--fnt-mono);font-size:.82rem;padding:6px 9px;\u001b[39m\n\u001b[31m  border-radius:var(--r-sm);outline:none;}\u001b[39m\n\u001b[31m.verify-field input:focus{border-color:var(--acc);}\u001b[39m\n\u001b[31m.verify-field input.changed{border-color:var(--ylw);background:rgba(234,179,8,.06);}\u001b[39m\n\u001b[31m.verify-actions{display:flex;gap:6px;align-items:center;flex-wrap:wrap;}\u001b[39m\n\u001b[31m.btn-wa{background:rgba(37,211,102,.15);border:1px solid rgba(37,211,102,.3);\u001b[39m\n\u001b[31m  color:#25d366;font-size:.65rem;padding:4px 10px;border-radius:10px;cursor:pointer;\u001b[39m\n\u001b[31m  font-family:var(--fnt-mono);transition:all .2s;}\u001b[39m\n\u001b[31m.btn-wa:hover{background:rgba(37,211,102,.3);}\u001b[39m\n\u001b[31m.verify-thumb{width:48px;height:48px;object-fit:cover;border-radius:6px;\u001b[39m\n\u001b[31m  border:1px solid var(--bdr);cursor:pointer;flex-shrink:0;}\u001b[39m\n\u001b[31m.verify-summary{background:var(--sur);border:1px solid var(--bdr);border-radius:var(--r);\u001b[39m\n\u001b[31m  padding:12px 14px;margin-bottom:14px;display:flex;gap:16px;flex-wrap:wrap;}\u001b[39m\n\u001b[31m.vs-item{text-align:center;}\u001b[39m\n\u001b[31m.vs-val{font-family:var(--fnt-head);font-size:1.3rem;font-weight:800;}\u001b[39m\n\u001b[31m.vs-lbl{font-size:.6rem;color:var(--t3);font-family:var(--fnt-mono);text-transform:uppercase;\u001b[39m\n\u001b[31m  letter-spacing:.5px;margin-top:2px;}\u001b[39m\n\u001b[31m.ocr-correction-note{font-size:.65rem;color:var(--ylw);font-family:var(--fnt-mono);\u001b[39m\n\u001b[31m  margin-top:3px;}\u001b[39m\n\u001b[31m</style>\u001b[39m\n\u001b[31m</head>\u001b[39m\n\u001b[31m<body>·\u001b[39m\n\u001b[31m<!-- SPLASH -->\u001b[39m\n\u001b[31m<div id=\\\"splash\\\">\u001b[39m\n\u001b[31m  <div class=\\\"splash-logo\\\">360LM <em>DISPATCH</em></div>\u001b[39m\n\u001b[31m  <div class=\\\"splash-sub\\\">Courier · Bus · Field Ops</div>\u001b[39m\n\u001b[31m  <div class=\\\"splash-track\\\"><div class=\\\"splash-track-fill\\\"></div></div>\u001b[39m\n\u001b[31m</div>·\u001b[39m\n\u001b[31m<!-- SHARE BANNER -->\u001b[39m\n\u001b[31m<div class=\\\"share-banner\\\" id=\\\"shareBanner\\\">\u001b[39m\n\u001b[31m  📤 Images received via Share — processing for OCR...\u001b[39m\n\u001b[31m  <button style=\\\"background:none;border:none;color:var(--grn);cursor:pointer;margin-left:auto;\\\" onclick=\\\"dismissShare()\\\">✕</button>\u001b[39m\n\u001b[31m</div>·\u001b[39m\n\u001b[31m<!-- APP -->\u001b[39m\n\u001b[31m<div id=\\\"app\\\" style=\\\"display:none;\\\">·\u001b[39m\n\u001b[31m  <div class=\\\"hdr\\\">\u001b[39m\n\u001b[31m    <div class=\\\"hdr-brand\\\">360LM <em>DISPATCH</em></div>\u001b[39m\n\u001b[31m    <span class=\\\"hdr-dot\\\" id=\\\"netDot\\\"></span>\u001b[39m\n\u001b[31m    <button class=\\\"hdr-btn\\\" onclick=\\\"window.location.href='/hub/'\\\" title=\\\"Hub\\\">🏡</button>\u001b[39m\n\u001b[31m    <button class=\\\"hdr-btn\\\" onclick=\\\"showPane('dashboard')\\\">📊</button>\u001b[39m\n\u001b[31m    <button class=\\\"hdr-btn primary\\\" onclick=\\\"newDispatch()\\\">+ NEW</button>\u001b[39m\n\u001b[31m  </div>·\u001b[39m\n\u001b[31m  <div class=\\\"tabs\\\">\u001b[39m\n\u001b[31m    <button class=\\\"tab active\\\" onclick=\\\"showPane('new')\\\" id=\\\"tab-new\\\">NEW</button>\u001b[39m\n\u001b[31m    <button class=\\\"tab\\\" onclick=\\\"showPane('dashboard')\\\" id=\\\"tab-dashboard\\\">DASHBOARD</button>\u001b[39m\n\u001b[31m    <button class=\\\"tab\\\" onclick=\\\"showPane('track')\\\" id=\\\"tab-track\\\">TRACK</button>\u001b[39m\n\u001b[31m    <button class=\\\"tab\\\" onclick=\\\"showPane('history')\\\" id=\\\"tab-history\\\">HISTORY <span class=\\\"tab-badge\\\" id=\\\"histCount\\\">0</span></button>\u001b[39m\n\u001b[31m    <button class=\\\"tab\\\" onclick=\\\"showPane('config')\\\" id=\\\"tab-config\\\">CONFIG</button>\u001b[39m\n\u001b[31m  </div>·\u001b[39m\n\u001b[31m  <!-- ════════════ NEW DISPATCH ════════════ -->\u001b[39m\n\u001b[31m  <div class=\\\"pane active\\\" id=\\\"pane-new\\\">·\u001b[39m\n\u001b[31m    <!-- 1. JOB REFERENCE -->\u001b[39m\n\u001b[31m    <div class=\\\"card accent-left\\\">\u001b[39m\n\u001b[31m      <div class=\\\"card-title\\\"><span class=\\\"ct-dot\\\"></span>Job Reference</div>\u001b[39m\n\u001b[31m      <div class=\\\"row2\\\">\u001b[39m\n\u001b[31m        <div class=\\\"f\\\">\u001b[39m\n\u001b[31m          <label>Client <span class=\\\"req\\\">*</span></label>\u001b[39m\n\u001b[31m          <select id=\\\"n_client\\\" onchange=\\\"onClientChange()\\\">\u001b[39m\n\u001b[31m            <option value=\\\"\\\">Select...</option>\u001b[39m\n\u001b[31m            <option>Lenovo</option><option>HP</option>\u001b[39m\n\u001b[31m            <option>Philips</option><option>Liebherr</option>\u001b[39m\n\u001b[31m            <option value=\\\"__c\\\">+ Custom</option>\u001b[39m\n\u001b[31m          </select>\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m        <div class=\\\"f\\\">\u001b[39m\n\u001b[31m          <label>Job Ref</label>\u001b[39m\n\u001b[31m          <input type=\\\"text\\\" id=\\\"n_jobref\\\" placeholder=\\\"JOB-042\\\" oninput=\\\"updatePill()\\\">\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      <div class=\\\"f\\\" id=\\\"customClientField\\\" style=\\\"display:none;\\\">\u001b[39m\n\u001b[31m        <label>Custom Client</label>\u001b[39m\n\u001b[31m        <input type=\\\"text\\\" id=\\\"n_customClient\\\" placeholder=\\\"Client name\\\" oninput=\\\"updatePill()\\\">\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      <div id=\\\"jobPill\\\" style=\\\"display:none;\\\" class=\\\"job-pill\\\">\u001b[39m\n\u001b[31m        🔗 <span id=\\\"jobPillTxt\\\"></span>\u001b[39m\n\u001b[31m        <button onclick=\\\"clearJob()\\\">✕</button>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      <div class=\\\"row2\\\" style=\\\"margin-top:10px;\\\">\u001b[39m\n\u001b[31m        <div class=\\\"f\\\" style=\\\"margin-bottom:0;\\\">\u001b[39m\n\u001b[31m          <label>Dispatch Date</label>\u001b[39m\n\u001b[31m          <input type=\\\"date\\\" id=\\\"n_date\\\">\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m        <div class=\\\"f\\\" style=\\\"margin-bottom:0;\\\">\u001b[39m\n\u001b[31m          <label>Courier Company <span class=\\\"req\\\">*</span></label>\u001b[39m\n\u001b[31m          <select id=\\\"n_courier\\\">\u001b[39m\n\u001b[31m            <option value=\\\"\\\">Select...</option>\u001b[39m\n\u001b[31m            <option>SkyKing</option><option>DTDC</option>\u001b[39m\n\u001b[31m            <option>Delhivery</option><option>Blue Dart</option>\u001b[39m\n\u001b[31m            <option>FedEx</option><option>Ecom Express</option>\u001b[39m\n\u001b[31m            <option>India Post</option><option value=\\\"__o\\\">Other</option>\u001b[39m\n\u001b[31m          </select>\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m    </div>·\u001b[39m\n\u001b[31m    <!-- 2. STORE LIST UPLOAD -->\u001b[39m\n\u001b[31m    <div class=\\\"card\\\">\u001b[39m\n\u001b[31m      <div class=\\\"card-title\\\"><span class=\\\"ct-dot\\\" style=\\\"background:var(--sky);\\\"></span>Store / Destination List</div>\u001b[39m\n\u001b[31m      <div class=\\\"store-upload-zone\\\" id=\\\"storeZone\\\" onclick=\\\"document.getElementById('storeFile').click();event.stopPropagation();\\\" style=\\\"cursor:pointer;\\\">\u001b[39m\n\u001b[31m        <input type=\\\"file\\\" id=\\\"storeFile\\\" accept=\\\".xlsx,.xls,.csv\\\" onchange=\\\"handleStoreFile(this)\\\" onclick=\\\"event.stopPropagation();\\\" style=\\\"display:none;\\\">\u001b[39m\n\u001b[31m        <div style=\\\"font-size:1.5rem;margin-bottom:6px;\\\">📋</div>\u001b[39m\n\u001b[31m        <div style=\\\"font-family:var(--fnt-head);font-weight:700;font-size:.9rem;\\\">Upload Store List</div>\u001b[39m\n\u001b[31m        <div style=\\\"font-size:.68rem;color:var(--t3);margin-top:3px;font-family:var(--fnt-mono);\\\">Excel / CSV · stores, cities, contacts</div>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      <div id=\\\"storeTableArea\\\" style=\\\"display:none;\\\">\u001b[39m\n\u001b[31m        <!-- Column detection result -->\u001b[39m\n\u001b[31m        <div id=\\\"storeColInfo\\\" style=\\\"margin-top:10px;display:none;\\\"></div>\u001b[39m\n\u001b[31m        <div style=\\\"display:flex;justify-content:space-between;align-items:center;margin-top:10px;margin-bottom:6px;\\\">\u001b[39m\n\u001b[31m          <span style=\\\"font-family:var(--fnt-mono);font-size:.7rem;color:var(--sky);\\\" id=\\\"storeCount\\\">0 stores loaded</span>\u001b[39m\n\u001b[31m          <button class=\\\"btn-sm\\\" onclick=\\\"clearStores()\\\">Clear</button>\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m        <div class=\\\"store-table-wrap\\\">\u001b[39m\n\u001b[31m          <table class=\\\"store-table\\\" id=\\\"storeTable\\\"></table>\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m        <!-- Inline add-as-consignees UI instead of confirm() -->\u001b[39m\n\u001b[31m        <div id=\\\"storeAddPanel\\\" style=\\\"margin-top:10px;background:var(--sur3);border:1px solid var(--bdr2);border-radius:var(--r-sm);padding:10px 12px;display:none;\\\">\u001b[39m\n\u001b[31m          <div style=\\\"font-size:.75rem;color:var(--t1);margin-bottom:8px;font-weight:600;\\\" id=\\\"storeAddTitle\\\">Add stores as consignees?</div>\u001b[39m\n\u001b[31m          <div class=\\\"row2\\\">\u001b[39m\n\u001b[31m            <button class=\\\"btn btn-ghost\\\" style=\\\"font-size:.75rem;padding:8px;\\\" onclick=\\\"document.getElementById('storeAddPanel').style.display='none'\\\">Skip for now</button>\u001b[39m\n\u001b[31m            <button class=\\\"btn btn-orange\\\" style=\\\"font-size:.75rem;padding:8px;\\\" id=\\\"storeAddBtn\\\" onclick=\\\"addStoresToConsignees()\\\">✓ Add All as Consignees</button>\u001b[39m\n\u001b[31m          </div>\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m        <button class=\\\"btn btn-ghost\\\" style=\\\"margin-top:8px;font-size:.78rem;\\\" onclick=\\\"autoMatchDockets()\\\">\u001b[39m\n\u001b[31m          🔗 Auto-match Dockets to Stores\u001b[39m\n\u001b[31m        </button>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m    </div>·\u001b[39m\n\u001b[31m    <!-- 3. DOCKET SLIPS — AI OCR -->\u001b[39m\n\u001b[31m    <div class=\\\"card\\\">\u001b[39m\n\u001b[31m      <div class=\\\"card-title\\\"><span class=\\\"ct-dot\\\" style=\\\"background:var(--grn);\\\"></span>Docket Slips — AI OCR</div>·\u001b[39m\n\u001b[31m      <!-- Share target indicator -->\u001b[39m\n\u001b[31m      <div class=\\\"info-tag\\\" id=\\\"shareTargetInfo\\\" style=\\\"display:none;\\\">\u001b[39m\n\u001b[31m        <span>📤</span><span>Images shared from WhatsApp — <strong id=\\\"sharedCount\\\">0</strong> slips received</span>\u001b[39m\n\u001b[31m      </div>·\u001b[39m\n\u001b[31m      <label class=\\\"upload-zone\\\" id=\\\"slipZone\\\" for=\\\"slipFiles\\\" style=\\\"display:block;cursor:pointer;\\\">\u001b[39m\n\u001b[31m        <input type=\\\"file\\\" id=\\\"slipFiles\\\" accept=\\\"image/*\\\" multiple onchange=\\\"handleSlipFiles(this.files)\\\" style=\\\"display:none;\\\">\u001b[39m\n\u001b[31m        <div class=\\\"uz-icon\\\">🖼️</div>\u001b[39m\n\u001b[31m        <div class=\\\"uz-title\\\">Upload Courier Slips</div>\u001b[39m\n\u001b[31m        <div class=\\\"uz-sub\\\">Multiple images · Tap or drag · Camera capture supported</div>\u001b[39m\n\u001b[31m      </label>·\u001b[39m\n\u001b[31m      <!-- OCR Working indicator -->\u001b[39m\n\u001b[31m      <div class=\\\"ocr-working\\\" id=\\\"ocrWorking\\\">\u001b[39m\n\u001b[31m        <span class=\\\"spin\\\">⟳</span>\u001b[39m\n\u001b[31m        <span id=\\\"ocrWorkingText\\\">Running OCR...</span>\u001b[39m\n\u001b[31m      </div>·\u001b[39m\n\u001b[31m      <!-- AI Progress -->\u001b[39m\n\u001b[31m      <div class=\\\"ai-progress\\\" id=\\\"aiProgress\\\">\u001b[39m\n\u001b[31m        <div class=\\\"ai-prog-label\\\" id=\\\"aiProgLabel\\\">Initializing Transformers.js...</div>\u001b[39m\n\u001b[31m        <div class=\\\"ai-prog-bar\\\"><div class=\\\"ai-prog-fill\\\" id=\\\"aiProgFill\\\"></div></div>\u001b[39m\n\u001b[31m      </div>·\u001b[39m\n\u001b[31m      <!-- OCR Results -->\u001b[39m\n\u001b[31m      <div class=\\\"ocr-results\\\" id=\\\"ocrResults\\\">\u001b[39m\n\u001b[31m        <div class=\\\"sec-div\\\"><span>Detected Dockets</span></div>\u001b[39m\n\u001b[31m        <div id=\\\"ocrSummary\\\" style=\\\"display:none;font-size:.72rem;font-family:var(--fnt-mono);\u001b[39m\n\u001b[31m          padding:4px 8px 8px;gap:8px\\\"></div>\u001b[39m\n\u001b[31m        <div class=\\\"ocr-slip-list\\\" id=\\\"ocrSlipList\\\"></div>\u001b[39m\n\u001b[31m        <div class=\\\"btn-row\\\" style=\\\"margin-top:10px;\\\">\u001b[39m\n\u001b[31m          <button class=\\\"btn btn-ghost\\\" onclick=\\\"clearSlips()\\\">Clear All</button>\u001b[39m\n\u001b[31m          <button class=\\\"btn btn-grn\\\" onclick=\\\"applyAllDockets()\\\">✓ Apply All to Consignees</button>\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m    </div>·\u001b[39m\n\u001b[31m    <!-- 4. CONSIGNEES -->\u001b[39m\n\u001b[31m    <div class=\\\"card accent-left\\\">\u001b[39m\n\u001b[31m      <div class=\\\"card-title\\\"><span class=\\\"ct-dot\\\"></span>Consignees</div>\u001b[39m\n\u001b[31m      <div class=\\\"cons-list\\\" id=\\\"consList\\\"></div>\u001b[39m\n\u001b[31m      <button class=\\\"btn btn-ghost\\\" style=\\\"font-size:.8rem;\\\" onclick=\\\"addConsignee()\\\">+ Add Consignee</button>\u001b[39m\n\u001b[31m    </div>·\u001b[39m\n\u001b[31m    <!-- 5. REMARKS -->\u001b[39m\n\u001b[31m    <div class=\\\"card\\\">\u001b[39m\n\u001b[31m      <div class=\\\"card-title\\\"><span class=\\\"ct-dot\\\" style=\\\"background:var(--t3);\\\"></span>Remarks</div>\u001b[39m\n\u001b[31m      <div class=\\\"f\\\" style=\\\"margin-bottom:0;\\\">\u001b[39m\n\u001b[31m        <textarea id=\\\"n_remarks\\\" placeholder=\\\"Instructions, notes, special handling...\\\"></textarea>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m    </div>·\u001b[39m\n\u001b[31m    <!-- ACTIONS -->\u001b[39m\n\u001b[31m    <div class=\\\"btn-row\\\">\u001b[39m\n\u001b[31m      <button class=\\\"btn btn-ghost\\\" onclick=\\\"saveDraft()\\\">💾 Draft</button>\u001b[39m\n\u001b[31m      <button class=\\\"btn btn-orange\\\" onclick=\\\"finaliseDispatch()\\\">✓ Finalise & Log</button>\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m    <div style=\\\"height:16px;\\\"></div>\u001b[39m\n\u001b[31m  </div>·\u001b[39m\n\u001b[31m  <!-- ════════════ DASHBOARD ════════════ -->\u001b[39m\n\u001b[31m  <div class=\\\"pane\\\" id=\\\"pane-dashboard\\\">\u001b[39m\n\u001b[31m    <div class=\\\"dash-stats\\\">\u001b[39m\n\u001b[31m      <div class=\\\"stat-box grn\\\"><div class=\\\"stat-val\\\" id=\\\"st-delivered\\\">0</div><div class=\\\"stat-lbl\\\">Delivered</div></div>\u001b[39m\n\u001b[31m      <div class=\\\"stat-box ylw\\\"><div class=\\\"stat-val\\\" id=\\\"st-transit\\\">0</div><div class=\\\"stat-lbl\\\">In Transit</div></div>\u001b[39m\n\u001b[31m      <div class=\\\"stat-box sky\\\"><div class=\\\"stat-val\\\" id=\\\"st-booked\\\">0</div><div class=\\\"stat-lbl\\\">Booked</div></div>\u001b[39m\n\u001b[31m      <div class=\\\"stat-box red\\\"><div class=\\\"stat-val\\\" id=\\\"st-pending\\\">0</div><div class=\\\"stat-lbl\\\">Pending</div></div>\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m    <div id=\\\"dashFilters\\\" style=\\\"display:flex;gap:6px;flex-wrap:wrap;margin-bottom:12px;\\\">\u001b[39m\n\u001b[31m      <span class=\\\"btn-sm primary\\\" onclick=\\\"filterDash('all',this)\\\" style=\\\"cursor:pointer;\\\">All</span>\u001b[39m\n\u001b[31m      <span class=\\\"btn-sm\\\" onclick=\\\"filterDash('delivered',this)\\\" style=\\\"cursor:pointer;\\\">Delivered</span>\u001b[39m\n\u001b[31m      <span class=\\\"btn-sm\\\" onclick=\\\"filterDash('transit',this)\\\" style=\\\"cursor:pointer;\\\">In Transit</span>\u001b[39m\n\u001b[31m      <span class=\\\"btn-sm\\\" onclick=\\\"filterDash('booked',this)\\\" style=\\\"cursor:pointer;\\\">Booked</span>\u001b[39m\n\u001b[31m      <span class=\\\"btn-sm\\\" onclick=\\\"filterDash('pending',this)\\\" style=\\\"cursor:pointer;\\\">Pending</span>\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m    <div class=\\\"docket-grid\\\" id=\\\"dashGrid\\\">\u001b[39m\n\u001b[31m      <div class=\\\"empty\\\"><div class=\\\"e-icon\\\">📊</div><p>No dispatches yet.<br>Create one to see the dashboard.</p></div>\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m    <div style=\\\"display:flex;gap:8px;margin-top:10px;flex-wrap:wrap;\\\">\u001b[39m\n\u001b[31m      <button class=\\\"btn btn-ghost\\\" style=\\\"flex:2;font-size:.8rem;\\\" id=\\\"btnRefreshAll\\\" onclick=\\\"refreshAllTracking()\\\">\u001b[39m\n\u001b[31m        🔄 Refresh All Tracking\u001b[39m\n\u001b[31m      </button>\u001b[39m\n\u001b[31m      <button class=\\\"btn btn-ghost\\\" style=\\\"flex:1;font-size:.8rem;border-color:var(--sky);color:var(--sky);\\\"\u001b[39m\n\u001b[31m        onclick=\\\"shareSelectedReport()\\\">\u001b[39m\n\u001b[31m        📊 Share Selected\u001b[39m\n\u001b[31m      </button>\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m  </div>·\u001b[39m\n\u001b[31m  <!-- ════════════ TRACK ════════════ -->\u001b[39m\n\u001b[31m  <div class=\\\"pane\\\" id=\\\"pane-track\\\">·\u001b[39m\n\u001b[31m    <!-- Courier selector -->\u001b[39m\n\u001b[31m    <div class=\\\"card\\\" style=\\\"margin-bottom:10px;\\\">\u001b[39m\n\u001b[31m      <div class=\\\"card-title\\\"><span class=\\\"ct-dot\\\" style=\\\"background:var(--sky);\\\"></span>Manual Tracking</div>\u001b[39m\n\u001b[31m      <div style=\\\"font-size:.72rem;color:var(--t2);margin-bottom:10px;line-height:1.5;\\\">\u001b[39m\n\u001b[31m        Track any docket number — select from already dispatched dockets or enter new ones manually.\u001b[39m\n\u001b[31m        SkyKing supports up to <strong>10 dockets at once</strong>; others are tracked one at a time.\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      <div class=\\\"f\\\">\u001b[39m\n\u001b[31m        <label>Courier Company</label>\u001b[39m\n\u001b[31m        <select id=\\\"t_courier\\\" onchange=\\\"buildTrackDocketChips()\\\">\u001b[39m\n\u001b[31m          <option>SkyKing</option><option>DTDC</option><option>Delhivery</option>\u001b[39m\n\u001b[31m          <option>Blue Dart</option><option>FedEx</option><option>Ecom Express</option><option>India Post</option>\u001b[39m\n\u001b[31m        </select>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m    </div>·\u001b[39m\n\u001b[31m    <!-- Docket selector — from existing dispatches -->\u001b[39m\n\u001b[31m    <div class=\\\"card\\\" style=\\\"margin-bottom:10px;\\\">\u001b[39m\n\u001b[31m      <div class=\\\"card-title\\\"><span class=\\\"ct-dot\\\" style=\\\"background:var(--acc);\\\"></span>\u001b[39m\n\u001b[31m        Select from Dispatched Dockets\u001b[39m\n\u001b[31m        <span id=\\\"trackCourierLimit\\\" style=\\\"margin-left:auto;font-size:.65rem;color:var(--t3);font-weight:400;text-transform:none;letter-spacing:0;font-family:var(--fnt-mono);\\\"></span>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      <div id=\\\"trackDocketChips\\\" style=\\\"display:flex;flex-wrap:wrap;gap:6px;margin-bottom:10px;min-height:28px;\\\">\u001b[39m\n\u001b[31m        <span style=\\\"font-size:.72rem;color:var(--t3);font-family:var(--fnt-mono);\\\">No dispatched dockets yet</span>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      <div style=\\\"font-size:.68rem;color:var(--t3);font-family:var(--fnt-mono);margin-bottom:8px;\\\">\u001b[39m\n\u001b[31m        Tap a docket to add/remove from tracking list. Selected shown in orange.\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m    </div>·\u001b[39m\n\u001b[31m    <!-- Manual entry for new dockets -->\u001b[39m\n\u001b[31m    <div class=\\\"card\\\" style=\\\"margin-bottom:10px;\\\">\u001b[39m\n\u001b[31m      <div class=\\\"card-title\\\"><span class=\\\"ct-dot\\\" style=\\\"background:var(--grn);\\\"></span>Add New Docket Numbers</div>\u001b[39m\n\u001b[31m      <div class=\\\"f\\\" style=\\\"margin-bottom:6px;\\\">\u001b[39m\n\u001b[31m        <label>Enter docket numbers (comma or newline separated)</label>\u001b[39m\n\u001b[31m        <textarea id=\\\"t_manual_dockets\\\" placeholder=\\\"443005527&#10;443005528&#10;or: 443005527, 443005528\\\"\u001b[39m\n\u001b[31m          style=\\\"min-height:56px;font-family:var(--fnt-mono);font-size:.82rem;letter-spacing:.5px;\\\"></textarea>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      <button class=\\\"btn-sm primary\\\" onclick=\\\"addManualDockets()\\\" style=\\\"font-size:.72rem;\\\">+ Add to List</button>\u001b[39m\n\u001b[31m    </div>·\u001b[39m\n\u001b[31m    <!-- Selected dockets summary + track button -->\u001b[39m\n\u001b[31m    <div id=\\\"trackSelectedSummary\\\" style=\\\"display:none;\\\" class=\\\"card\\\" style=\\\"margin-bottom:10px;\\\">\u001b[39m\n\u001b[31m      <div style=\\\"display:flex;align-items:center;justify-content:space-between;margin-bottom:8px;\\\">\u001b[39m\n\u001b[31m        <div>\u001b[39m\n\u001b[31m          <span style=\\\"font-family:var(--fnt-mono);font-size:.72rem;color:var(--sky);\\\" id=\\\"trackSelectedCount\\\">0 selected</span>\u001b[39m\n\u001b[31m          <span style=\\\"font-family:var(--fnt-mono);font-size:.65rem;color:var(--t3);margin-left:8px;\\\" id=\\\"trackSelectedList\\\"></span>\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m        <button class=\\\"btn-sm\\\" onclick=\\\"clearTrackSelection()\\\">Clear</button>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      <div class=\\\"btn-row\\\">\u001b[39m\n\u001b[31m        <button class=\\\"btn btn-ghost\\\" onclick=\\\"openTrackOnCourierSite()\\\">🔗 Open Courier Site</button>\u001b[39m\n\u001b[31m        <button class=\\\"btn btn-orange\\\" onclick=\\\"trackSelected()\\\">🔍 Track All via Proxy</button>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m    </div>·\u001b[39m\n\u001b[31m    <div id=\\\"trackResultsArea\\\"></div>\u001b[39m\n\u001b[31m  </div>·\u001b[39m\n\u001b[31m  <!-- ════════════ HISTORY ════════════ -->\u001b[39m\n\u001b[31m  <div class=\\\"pane\\\" id=\\\"pane-history\\\">\u001b[39m\n\u001b[31m    <div id=\\\"historyList\\\">\u001b[39m\n\u001b[31m      <div class=\\\"empty\\\"><div class=\\\"e-icon\\\">📭</div><p>No dispatch history yet.</p></div>\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m  </div>·\u001b[39m\n\u001b[31m  <!-- ════════════ CONFIG ════════════ -->\u001b[39m\n\u001b[31m  <div class=\\\"pane\\\" id=\\\"pane-config\\\">·\u001b[39m\n\u001b[31m    <!-- SHEETS URL — the only thing you type manually in the PWA -->\u001b[39m\n\u001b[31m    <div class=\\\"card\\\" style=\\\"border-color:var(--acc);border-width:2px;\\\">\u001b[39m\n\u001b[31m      <div class=\\\"card-title\\\"><span class=\\\"ct-dot\\\"></span>Google Sheets — Master Config</div>\u001b[39m\n\u001b[31m      <div class=\\\"info-tag\\\" style=\\\"background:rgba(34,197,94,.07);border-color:rgba(34,197,94,.3);\\\">\u001b[39m\n\u001b[31m        <span>✅</span>\u001b[39m\n\u001b[31m        <span>URL is loaded automatically from <strong>config.json</strong> on the VPS — no manual entry needed. All other settings sync from your <strong>Google Sheet</strong>.</span>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      <div class=\\\"f\\\">\u001b[39m\n\u001b[31m        <label>Apps Script URL <span style=\\\"font-size:.65rem;color:var(--t3);font-weight:400;\\\">(auto-loaded from config.json)</span></label>\u001b[39m\n\u001b[31m        <input type=\\\"url\\\" id=\\\"cfg_sheets_url\\\"\u001b[39m\n\u001b[31m          placeholder=\\\"Auto-loaded from server...\\\"\u001b[39m\n\u001b[31m          oninput=\\\"onSheetsUrlChange()\\\" readonly\u001b[39m\n\u001b[31m          style=\\\"opacity:.7;cursor:default;\\\">\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      <div id=\\\"syncStatus\\\" class=\\\"info-tag\\\" style=\\\"display:none;\\\">\u001b[39m\n\u001b[31m        <span id=\\\"syncIcon\\\">⏳</span>\u001b[39m\n\u001b[31m        <span id=\\\"syncMsg\\\">Syncing from sheet...</span>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      <div class=\\\"btn-row\\\">\u001b[39m\n\u001b[31m        <button class=\\\"btn btn-ghost\\\" onclick=\\\"syncFromSheet()\\\">🔄 Sync Now</button>\u001b[39m\n\u001b[31m        <button class=\\\"btn btn-orange\\\" onclick=\\\"saveSheetsUrl()\\\">💾 Save & Sync</button>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m    </div>·\u001b[39m\n\u001b[31m    <!-- READ-ONLY display of what was loaded from sheet -->\u001b[39m\n\u001b[31m    <div class=\\\"card\\\" id=\\\"sheetConfigDisplay\\\" style=\\\"display:none;\\\">\u001b[39m\n\u001b[31m      <div class=\\\"card-title\\\"><span class=\\\"ct-dot\\\" style=\\\"background:var(--grn);\\\"></span>Config Loaded from Sheet\u001b[39m\n\u001b[31m        <span id=\\\"sheetSyncTime\\\" style=\\\"margin-left:auto;font-size:.62rem;color:var(--t3);font-family:var(--fnt-mono);font-weight:400;text-transform:none;letter-spacing:0;\\\"></span>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      <div id=\\\"sheetConfigRows\\\"></div>\u001b[39m\n\u001b[31m      <div class=\\\"info-tag\\\" style=\\\"margin-top:8px;\\\">\u001b[39m\n\u001b[31m        <span>✏️</span><span>To change any value, edit the <strong>Config sheet</strong> in Google Sheets, then tap Sync Now above.</span>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m    </div>·\u001b[39m\n\u001b[31m    <!-- Courier Master (from sheet) -->\u001b[39m\n\u001b[31m    <div class=\\\"card\\\">\u001b[39m\n\u001b[31m      <div class=\\\"card-title\\\"><span class=\\\"ct-dot\\\" style=\\\"background:var(--ylw);\\\"></span>Courier Master</div>\u001b[39m\n\u001b[31m      <div id=\\\"courierMasterList\\\"></div>\u001b[39m\n\u001b[31m      <button class=\\\"btn btn-ghost\\\" style=\\\"margin-top:8px;font-size:.78rem;\\\" onclick=\\\"openCourierMaster()\\\">✏️ Edit in Sheet / Override</button>\u001b[39m\n\u001b[31m    </div>·\u001b[39m\n\u001b[31m    <button class=\\\"btn\\\" style=\\\"background:var(--red-dim);color:var(--red);border:1px solid var(--red);margin-top:4px;\\\" onclick=\\\"clearAll()\\\">🗑️ Clear All Local Data</button>\u001b[39m\n\u001b[31m    <div style=\\\"height:20px;\\\"></div>\u001b[39m\n\u001b[31m  </div>··\u001b[39m\n\u001b[31m<!-- ════ ADDRESS REVIEW PANE ════ -->\u001b[39m\n\u001b[31m<div id=\\\"addrReviewPane\\\" style=\\\"display:none;position:fixed;inset:0;background:var(--bg);z-index:50;overflow-y:auto;padding:14px;\\\">\u001b[39m\n\u001b[31m  <div class=\\\"verify-hdr\\\">\u001b[39m\n\u001b[31m    <button class=\\\"btn-icon\\\" onclick=\\\"document.getElementById('addrReviewPane').style.display='none'\\\" style=\\\"font-size:1.1rem;\\\">←</button>\u001b[39m\n\u001b[31m    <div class=\\\"verify-title\\\">Review AI-Parsed Addresses</div>\u001b[39m\n\u001b[31m  </div>\u001b[39m\n\u001b[31m  <div style=\\\"background:rgba(56,189,248,.08);border:1px solid rgba(56,189,248,.3);border-radius:var(--r-sm);\u001b[39m\n\u001b[31m    padding:8px 12px;margin-bottom:12px;font-size:.72rem;color:var(--sky);\\\">\u001b[39m\n\u001b[31m    ✏️ Review each row — correct name and city if wrong. Raw address shown below each row for reference.\u001b[39m\n\u001b[31m    Corrections are saved and improve future AI parsing automatically.\u001b[39m\n\u001b[31m  </div>\u001b[39m\n\u001b[31m  <div class=\\\"verify-summary\\\" id=\\\"addrReviewSummary\\\"></div>\u001b[39m\n\u001b[31m  <div id=\\\"addrReviewList\\\"></div>\u001b[39m\n\u001b[31m  <div style=\\\"position:sticky;bottom:0;background:var(--bg);padding-top:10px;\u001b[39m\n\u001b[31m    border-top:1px solid var(--bdr);margin-top:8px;\\\">\u001b[39m\n\u001b[31m    <div style=\\\"display:flex;gap:8px;\\\">\u001b[39m\n\u001b[31m      <button class=\\\"btn btn-ghost\\\" onclick=\\\"backToMapping()\\\" style=\\\"flex:1;\\\">← Back to Mapping</button>\u001b[39m\n\u001b[31m      <button class=\\\"btn btn-orange\\\" onclick=\\\"confirmAddressReview()\\\" style=\\\"flex:2;\\\">✓ Confirm &amp; Use These Addresses</button>\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m    <div style=\\\"font-size:.65rem;color:var(--t3);text-align:center;margin-top:5px;font-family:var(--fnt-mono);\\\">\u001b[39m\n\u001b[31m      Edits you made above are preserved when you go back\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m  </div>\u001b[39m\n\u001b[31m</div>·\u001b[39m\n\u001b[31m<!-- ════ VERIFICATION SCREEN ════ -->\u001b[39m\n\u001b[31m<div id=\\\"verifyPane\\\">\u001b[39m\n\u001b[31m  <div class=\\\"verify-hdr\\\">\u001b[39m\n\u001b[31m    <button class=\\\"btn-icon\\\" onclick=\\\"closeVerify()\\\" style=\\\"font-size:1.1rem;\\\">←</button>\u001b[39m\n\u001b[31m    <div class=\\\"verify-title\\\">Verify Before Finalise</div>\u001b[39m\n\u001b[31m    <button class=\\\"btn-sm\\\" onclick=\\\"approveAll()\\\" style=\\\"white-space:nowrap;\\\">✓ Approve All</button>\u001b[39m\n\u001b[31m  </div>\u001b[39m\n\u001b[31m  <div class=\\\"verify-summary\\\" id=\\\"verifySummary\\\"></div>\u001b[39m\n\u001b[31m  <div id=\\\"verifyList\\\"></div>\u001b[39m\n\u001b[31m  <div style=\\\"position:sticky;bottom:0;background:var(--bg);padding-top:10px;\u001b[39m\n\u001b[31m    border-top:1px solid var(--bdr);margin-top:8px;\\\">\u001b[39m\n\u001b[31m    <div style=\\\"display:flex;gap:8px;flex-wrap:wrap;\\\">\u001b[39m\n\u001b[31m      <button class=\\\"btn btn-ghost\\\" onclick=\\\"closeVerify()\\\" style=\\\"flex:1;\\\">← Back to Edit</button>\u001b[39m\n\u001b[31m      <button class=\\\"btn btn-ghost\\\" onclick=\\\"exportCleanSheet()\\\" style=\\\"flex:1;border-color:var(--sky);color:var(--sky);\\\">\u001b[39m\n\u001b[31m        📥 Export Clean Sheet\u001b[39m\n\u001b[31m      </button>\u001b[39m\n\u001b[31m      <button class=\\\"btn btn-orange\\\" id=\\\"btnConfirmFinalise\\\" onclick=\\\"confirmAndFinalise()\\\" style=\\\"flex:2;\\\">\u001b[39m\n\u001b[31m        ✓ Finalise Dispatch\u001b[39m\n\u001b[31m      </button>\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m  </div>\u001b[39m\n\u001b[31m</div>·\u001b[39m\n\u001b[31m</div><!-- /app -->·\u001b[39m\n\u001b[31m<!-- ════ DISPATCH DETAIL SHEET ════ -->\u001b[39m\n\u001b[31m<div class=\\\"overlay\\\" id=\\\"detailOverlay\\\" onclick=\\\"closeOverlay('detailOverlay')\\\">\u001b[39m\n\u001b[31m  <div class=\\\"sheet\\\" onclick=\\\"event.stopPropagation()\\\">\u001b[39m\n\u001b[31m    <div class=\\\"sheet-handle\\\"></div>\u001b[39m\n\u001b[31m    <div id=\\\"detailContent\\\"></div>\u001b[39m\n\u001b[31m  </div>\u001b[39m\n\u001b[31m</div>·\u001b[39m\n\u001b[31m<!-- ════ COURIER MASTER EDITOR ════ -->\u001b[39m\n\u001b[31m<div class=\\\"overlay\\\" id=\\\"courierMasterOverlay\\\" onclick=\\\"closeOverlay('courierMasterOverlay')\\\">\u001b[39m\n\u001b[31m  <div class=\\\"sheet\\\" onclick=\\\"event.stopPropagation()\\\">\u001b[39m\n\u001b[31m    <div class=\\\"sheet-handle\\\"></div>\u001b[39m\n\u001b[31m    <div class=\\\"sheet-title\\\">✏️ Courier Master</div>\u001b[39m\n\u001b[31m    <div id=\\\"courierMasterEditor\\\"></div>\u001b[39m\n\u001b[31m  </div>\u001b[39m\n\u001b[31m</div>·\u001b[39m\n\u001b[31m<!-- ════ MANUAL TRACKING OVERLAY ════ -->\u001b[39m\n\u001b[31m<div class=\\\"overlay\\\" id=\\\"manualTrackOverlay\\\" onclick=\\\"closeOverlay('manualTrackOverlay')\\\" data-docket=\\\"\\\" data-dispatch-id=\\\"\\\">\u001b[39m\n\u001b[31m  <div class=\\\"sheet\\\" onclick=\\\"event.stopPropagation()\\\">\u001b[39m\n\u001b[31m    <div class=\\\"sheet-handle\\\"></div>\u001b[39m\n\u001b[31m    <div class=\\\"sheet-title\\\">📞 Manual Tracking Log</div>\u001b[39m\n\u001b[31m    <div style=\\\"background:var(--sur2);border:1px solid var(--bdr);border-radius:var(--r-sm);\u001b[39m\n\u001b[31m      padding:8px 12px;margin-bottom:12px;font-family:var(--fnt-mono);font-size:.82rem;color:var(--acc);\\\"\u001b[39m\n\u001b[31m      id=\\\"mtDocketDisplay\\\"></div>\u001b[39m\n\u001b[31m    <div class=\\\"row2\\\">\u001b[39m\n\u001b[31m      <div class=\\\"f\\\">\u001b[39m\n\u001b[31m        <label>Status Reported <span class=\\\"req\\\">*</span></label>\u001b[39m\n\u001b[31m        <select id=\\\"mtStatus\\\">\u001b[39m\n\u001b[31m          <option value=\\\"\\\">Select...</option>\u001b[39m\n\u001b[31m          <option value=\\\"delivered\\\">✅ Delivered</option>\u001b[39m\n\u001b[31m          <option value=\\\"not received\\\">❌ Not Received</option>\u001b[39m\n\u001b[31m          <option value=\\\"in transit\\\">🚚 In Transit / On the way</option>\u001b[39m\n\u001b[31m          <option value=\\\"will deliver tomorrow\\\">📅 Will Deliver Tomorrow</option>\u001b[39m\n\u001b[31m          <option value=\\\"refused\\\">🚫 Refused / Returned</option>\u001b[39m\n\u001b[31m          <option value=\\\"partial\\\">📦 Partial Delivery</option>\u001b[39m\n\u001b[31m        </select>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      <div class=\\\"f\\\">\u001b[39m\n\u001b[31m        <label>Called By (Staff) <span class=\\\"req\\\">*</span></label>\u001b[39m\n\u001b[31m        <input type=\\\"text\\\" id=\\\"mtCalledBy\\\" placeholder=\\\"Your name\\\" list=\\\"staffList\\\">\u001b[39m\n\u001b[31m        <datalist id=\\\"staffList\\\">\u001b[39m\n\u001b[31m          <option>Mukesh</option><option>Sachin</option><option>Shambhu</option>\u001b[39m\n\u001b[31m          <option>Umashankar</option><option>Narender</option><option>Rakesh</option>\u001b[39m\n\u001b[31m          <option>Gurpreet</option><option>Ashok</option>\u001b[39m\n\u001b[31m        </datalist>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m    <div class=\\\"row2\\\">\u001b[39m\n\u001b[31m      <div class=\\\"f\\\">\u001b[39m\n\u001b[31m        <label>Contact Number Used</label>\u001b[39m\n\u001b[31m        <input type=\\\"tel\\\" id=\\\"mtContactUsed\\\" placeholder=\\\"Mobile number called\\\">\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      <div class=\\\"f\\\">\u001b[39m\n\u001b[31m        <label>Person Who Confirmed</label>\u001b[39m\n\u001b[31m        <input type=\\\"text\\\" id=\\\"mtContactPerson\\\" placeholder=\\\"Name of person at store\\\">\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m    <div class=\\\"f\\\">\u001b[39m\n\u001b[31m      <label>Remarks</label>\u001b[39m\n\u001b[31m      <textarea id=\\\"mtRemarks\\\" placeholder=\\\"Any notes, delivery details, issues...\\\" style=\\\"min-height:60px;\\\"></textarea>\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m    <div class=\\\"f\\\">\u001b[39m\n\u001b[31m      <label>Call Date &amp; Time</label>\u001b[39m\n\u001b[31m      <input type=\\\"datetime-local\\\" id=\\\"mtCallTime\\\">\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m    <div class=\\\"btn-row\\\">\u001b[39m\n\u001b[31m      <button class=\\\"btn btn-ghost\\\" onclick=\\\"closeOverlay('manualTrackOverlay')\\\">Cancel</button>\u001b[39m\n\u001b[31m      <button class=\\\"btn btn-orange\\\" onclick=\\\"submitManualTracking()\\\">💾 Log &amp; Save</button>\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m  </div>\u001b[39m\n\u001b[31m</div>·\u001b[39m\n\u001b[31m<!-- ════ POD UPLOAD OVERLAY ════ -->\u001b[39m\n\u001b[31m<div class=\\\"overlay\\\" id=\\\"podOverlay\\\" onclick=\\\"closeOverlay('podOverlay')\\\" data-docket=\\\"\\\" data-dispatch-id=\\\"\\\">\u001b[39m\n\u001b[31m  <div class=\\\"sheet\\\" onclick=\\\"event.stopPropagation()\\\">\u001b[39m\n\u001b[31m    <div class=\\\"sheet-handle\\\"></div>\u001b[39m\n\u001b[31m    <div class=\\\"sheet-title\\\">📸 Upload Proof of Delivery</div>\u001b[39m\n\u001b[31m    <div class=\\\"f\\\">\u001b[39m\n\u001b[31m      <label>POD / Delivery Slip Photo</label>\u001b[39m\n\u001b[31m      <input type=\\\"file\\\" id=\\\"podFileInput\\\" accept=\\\"image/*\\\" capture=\\\"environment\\\"\u001b[39m\n\u001b[31m        onchange=\\\"previewPOD(this,'pod')\\\" style=\\\"font-size:.8rem;color:var(--t2);\\\">\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m    <div class=\\\"f\\\">\u001b[39m\n\u001b[31m      <label>Recipient / Receiver Photo (optional)</label>\u001b[39m\n\u001b[31m      <input type=\\\"file\\\" id=\\\"recipientFileInput\\\" accept=\\\"image/*\\\" capture=\\\"environment\\\"\u001b[39m\n\u001b[31m        onchange=\\\"previewPOD(this,'recip')\\\" style=\\\"font-size:.8rem;color:var(--t2);\\\">\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m    <div id=\\\"podPreview\\\" style=\\\"display:flex;gap:8px;flex-wrap:wrap;margin-bottom:12px;\\\"></div>\u001b[39m\n\u001b[31m    <div class=\\\"btn-row\\\">\u001b[39m\n\u001b[31m      <button class=\\\"btn btn-ghost\\\" onclick=\\\"closeOverlay('podOverlay')\\\">Cancel</button>\u001b[39m\n\u001b[31m      <button class=\\\"btn btn-orange\\\" onclick=\\\"uploadPODFiles()\\\">📤 Upload & Mark Delivered</button>\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m  </div>\u001b[39m\n\u001b[31m</div>·\u001b[39m\n\u001b[31m<!-- ════ ADMIN LOGIN OVERLAY ════ -->\u001b[39m\n\u001b[31m<div class=\\\"overlay\\\" id=\\\"adminLoginOverlay\\\" onclick=\\\"closeOverlay('adminLoginOverlay')\\\">\u001b[39m\n\u001b[31m  <div class=\\\"sheet\\\" onclick=\\\"event.stopPropagation()\\\" style=\\\"max-height:320px;\\\">\u001b[39m\n\u001b[31m    <div class=\\\"sheet-handle\\\"></div>\u001b[39m\n\u001b[31m    <div class=\\\"sheet-title\\\">🔐 Admin Access</div>\u001b[39m\n\u001b[31m    <div style=\\\"font-size:.78rem;color:var(--t2);margin-bottom:14px;\\\">Config settings require admin password.</div>\u001b[39m\n\u001b[31m    <div class=\\\"f\\\">\u001b[39m\n\u001b[31m      <label>Password</label>\u001b[39m\n\u001b[31m      <input type=\\\"password\\\" id=\\\"adminPwdInput\\\" placeholder=\\\"Enter admin password\\\"\u001b[39m\n\u001b[31m        onkeydown=\\\"if(event.key==='Enter')submitAdminLogin()\\\">\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m    <div class=\\\"btn-row\\\" style=\\\"margin-top:4px;\\\">\u001b[39m\n\u001b[31m      <button class=\\\"btn btn-ghost\\\" onclick=\\\"closeOverlay('adminLoginOverlay')\\\">Cancel</button>\u001b[39m\n\u001b[31m      <button class=\\\"btn btn-orange\\\" onclick=\\\"submitAdminLogin()\\\">🔓 Login</button>\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m  </div>\u001b[39m\n\u001b[31m</div>·\u001b[39m\n\u001b[31m<!-- ════ TOAST ════ -->\u001b[39m\n\u001b[31m<div class=\\\"toast\\\" id=\\\"toast\\\"></div>·\u001b[39m\n\u001b[31m<!-- ════ SCRIPTS ════ -->\u001b[39m\n\u001b[31m<!-- SheetJS for Excel/CSV parsing -->\u001b[39m\n\u001b[31m<script src=\\\"https://cdnjs.cloudflare.com/ajax/libs/xlsx/0.18.5/xlsx.full.min.js\\\"></script>·\u001b[39m\n\u001b[31m<script type=\\\"module\\\">\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31m// OCR — Transformers.js\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31mimport { pipeline, env } from 'https://cdn.jsdelivr.net/npm/@huggingface/transformers@3.5.2/dist/transformers.min.js';·\u001b[39m\n\u001b[31m// Cache models in browser\u001b[39m\n\u001b[31menv.allowLocalModels = false;\u001b[39m\n\u001b[31menv.useBrowserCache = true;·\u001b[39m\n\u001b[31mlet ocrPipeline = null;\u001b[39m\n\u001b[31mlet ocrLoading = false;·\u001b[39m\n\u001b[31mwindow.initOCR = async function() {\u001b[39m\n\u001b[31m  if (ocrPipeline || ocrLoading) return;\u001b[39m\n\u001b[31m  ocrLoading = true;\u001b[39m\n\u001b[31m  showAIProgress('Loading OCR model (first time ~120MB)...', 5);\u001b[39m\n\u001b[31m  try {\u001b[39m\n\u001b[31m    ocrPipeline = await pipeline('image-to-text', 'Xenova/trocr-base-printed', {\u001b[39m\n\u001b[31m      progress_callback: (p) => {\u001b[39m\n\u001b[31m        if (p.progress) {\u001b[39m\n\u001b[31m          showAIProgress(`Loading: ${p.file || 'model'} (${Math.round(p.progress)}%)`, p.progress);\u001b[39m\n\u001b[31m        }\u001b[39m\n\u001b[31m      }\u001b[39m\n\u001b[31m    });\u001b[39m\n\u001b[31m    showAIProgress('OCR model ready ✓', 100);\u001b[39m\n\u001b[31m    setTimeout(() => hideAIProgress(), 1500);\u001b[39m\n\u001b[31m    showToast('OCR ready ✓', 'ok');\u001b[39m\n\u001b[31m  } catch(e) {\u001b[39m\n\u001b[31m    console.warn('Transformers.js OCR failed:', e);\u001b[39m\n\u001b[31m    hideAIProgress();\u001b[39m\n\u001b[31m    showToast('OCR model load failed — will use Ollama fallback', 'info');\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m  ocrLoading = false;\u001b[39m\n\u001b[31m};·\u001b[39m\n\u001b[31mwindow.runOCROnImage = async function(imageData, filename) {\u001b[39m\n\u001b[31m  const cfg = window.appConfig;·\u001b[39m\n\u001b[31m  // Strategy 1: Transformers.js\u001b[39m\n\u001b[31m  if ((cfg.ocr_mode === 'transformers' || cfg.ocr_mode === 'both') && ocrPipeline) {\u001b[39m\n\u001b[31m    try {\u001b[39m\n\u001b[31m      const result = await ocrPipeline(imageData);\u001b[39m\n\u001b[31m      const text = result[0]?.generated_text || '';\u001b[39m\n\u001b[31m      return extractDocketFromText(text, 'transformers');\u001b[39m\n\u001b[31m    } catch(e) { /* fall through */ }\u001b[39m\n\u001b[31m  }·\u001b[39m\n\u001b[31m  // Strategy 2: Dispatch AI proxy → Ollama LLaVA (server-side, avoids CORS + port issues)\u001b[39m\n\u001b[31m  if (cfg.ocr_mode === 'ollama' || cfg.ocr_mode === 'both') {\u001b[39m\n\u001b[31m    try {\u001b[39m\n\u001b[31m      const resp = await fetch('/dispatch-ai/ocr', {\u001b[39m\n\u001b[31m        method: 'POST',\u001b[39m\n\u001b[31m        headers: { 'Content-Type': 'application/json' },\u001b[39m\n\u001b[31m        body: JSON.stringify({ imageBase64: imageData }),\u001b[39m\n\u001b[31m        signal: AbortSignal.timeout(180000)\u001b[39m\n\u001b[31m      });\u001b[39m\n\u001b[31m      const data = await resp.json();\u001b[39m\n\u001b[31m      if (data.success && data.docket) {\u001b[39m\n\u001b[31m        return { docket: data.docket, confidence: data.confidence, source: 'ollama' };\u001b[39m\n\u001b[31m      }\u001b[39m\n\u001b[31m    } catch(e) { /* fall through */ }\u001b[39m\n\u001b[31m  }·\u001b[39m\n\u001b[31m  return { docket: '', confidence: 0, source: 'failed' };\u001b[39m\n\u001b[31m};·\u001b[39m\n\u001b[31mfunction extractDocketFromText(text, source) {\u001b[39m\n\u001b[31m  // Pattern: typical courier docket = 8-15 digit numbers, sometimes with letters\u001b[39m\n\u001b[31m  const patterns = [\u001b[39m\n\u001b[31m    /\\\\b([A-Z]{0,3}\\\\d{8,15}[A-Z]{0,3})\\\\b/g,   // alphanumeric tracking\u001b[39m\n\u001b[31m    /\\\\b(\\\\d{9,15})\\\\b/g,                          // pure numeric\u001b[39m\n\u001b[31m    /\\\\b([A-Z]{2,4}\\\\d{6,12})\\\\b/g,               // letter prefix\u001b[39m\n\u001b[31m  ];\u001b[39m\n\u001b[31m  const candidates = [];\u001b[39m\n\u001b[31m  for (const pat of patterns) {\u001b[39m\n\u001b[31m    let m;\u001b[39m\n\u001b[31m    while ((m = pat.exec(text)) !== null) {\u001b[39m\n\u001b[31m      if (!candidates.includes(m[1])) candidates.push(m[1]);\u001b[39m\n\u001b[31m    }\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m  if (candidates.length === 0) return { docket: text.trim().slice(0, 20), confidence: 0.3, source };\u001b[39m\n\u001b[31m  // Pick longest candidate (usually most complete docket)\u001b[39m\n\u001b[31m  const best = candidates.sort((a, b) => b.length - a.length)[0];\u001b[39m\n\u001b[31m  const confidence = candidates.length === 1 ? 0.85 : 0.7;\u001b[39m\n\u001b[31m  return { docket: best, confidence, source };\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// Expose for non-module code\u001b[39m\n\u001b[31mwindow._ocrReady = () => ocrPipeline !== null;\u001b[39m\n\u001b[31m</script>·\u001b[39m\n\u001b[31m<script>\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31m// STATE & STORAGE\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31mlet dispatches = [];\u001b[39m\n\u001b[31mlet stores = [];\u001b[39m\n\u001b[31mlet slipOCRData = [];\u001b[39m\n\u001b[31mlet dashFilter = 'all';·\u001b[39m\n\u001b[31m// Only sheets_url is persisted locally — all other config comes from the sheet\u001b[39m\n\u001b[31mlet _trackingInProgress = false;\u001b[39m\n\u001b[31mwindow.appConfig = {\u001b[39m\n\u001b[31m  sheets_url:      'https://script.google.com/macros/s/AKfycbys8HrToDv2u_dRvSFnNqmO9bnyAO1s8oSprUydpVQnNQutq4tuUY71cVCH24urbvM9/exec',\u001b[39m\n\u001b[31m  webhook_url:     '',\u001b[39m\n\u001b[31m  proxy_url:       '',\u001b[39m\n\u001b[31m  ollama_url:      'http://srv1111289.hstgr.cloud:11434',\u001b[39m\n\u001b[31m  ocr_mode:        'both',\u001b[39m\n\u001b[31m  company:         '360 Degree Logical Marketing',\u001b[39m\n\u001b[31m  city:            'Chandigarh',\u001b[39m\n\u001b[31m  auto_log:        'true',\u001b[39m\n\u001b[31m  refresh_on_load: 'false',\u001b[39m\n\u001b[31m};·\u001b[39m\n\u001b[31mconst COURIER_MASTER = {\u001b[39m\n\u001b[31m  'SkyKing': {\u001b[39m\n\u001b[31m    trackUrl: 'https://skyking.co/track?cno={docket}',\u001b[39m\n\u001b[31m    trackBulkUrl: 'https://skyking.co/track',\u001b[39m\n\u001b[31m    apiProxy: '/track-proxy/skyking?cno={docket}',\u001b[39m\n\u001b[31m    maxBulk: 10\u001b[39m\n\u001b[31m  },\u001b[39m\n\u001b[31m  'DTDC': {\u001b[39m\n\u001b[31m    trackUrl: 'https://www.dtdc.in/tracking.asp',\u001b[39m\n\u001b[31m    queryParam: 'trkId',\u001b[39m\n\u001b[31m    maxBulk: 1\u001b[39m\n\u001b[31m  },\u001b[39m\n\u001b[31m  'Delhivery': {\u001b[39m\n\u001b[31m    trackUrl: 'https://www.delhivery.com/track/package/{docket}',\u001b[39m\n\u001b[31m    maxBulk: 1\u001b[39m\n\u001b[31m  },\u001b[39m\n\u001b[31m  'Blue Dart': {\u001b[39m\n\u001b[31m    trackUrl: 'https://www.bluedart.com/tracking',\u001b[39m\n\u001b[31m    maxBulk: 1\u001b[39m\n\u001b[31m  },\u001b[39m\n\u001b[31m  'FedEx': {\u001b[39m\n\u001b[31m    trackUrl: 'https://www.fedex.com/fedextrack/?trknbr={docket}',\u001b[39m\n\u001b[31m    maxBulk: 10\u001b[39m\n\u001b[31m  },\u001b[39m\n\u001b[31m  'Ecom Express': {\u001b[39m\n\u001b[31m    trackUrl: 'https://ecomexpress.in/tracking/?awb_field={docket}',\u001b[39m\n\u001b[31m    maxBulk: 1\u001b[39m\n\u001b[31m  },\u001b[39m\n\u001b[31m  'India Post': {\u001b[39m\n\u001b[31m    trackUrl: 'https://www.indiapost.gov.in/VAS/Pages/trackconsignment.aspx',\u001b[39m\n\u001b[31m    maxBulk: 1\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m};·\u001b[39m\n\u001b[31mfunction loadData() {\u001b[39m\n\u001b[31m  try {\u001b[39m\n\u001b[31m    dispatches = JSON.parse(localStorage.getItem('360lm_disp_v2') || '[]');\u001b[39m\n\u001b[31m    const saved = JSON.parse(localStorage.getItem('360lm_disp_sheets_url') || '{}');\u001b[39m\n\u001b[31m    if (saved.sheets_url) window.appConfig.sheets_url = saved.sheets_url;\u001b[39m\n\u001b[31m  } catch(e) {}\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31mfunction saveData() {\u001b[39m\n\u001b[31m  try {\u001b[39m\n\u001b[31m    localStorage.setItem('360lm_disp_v2', JSON.stringify(dispatches));\u001b[39m\n\u001b[31m  } catch(e) {\u001b[39m\n\u001b[31m    if (e.name === 'QuotaExceededError' || e.code === 22) {\u001b[39m\n\u001b[31m      showToast('Storage full — old dispatches cleared. Data is safe in Postgres.', 'err');\u001b[39m\n\u001b[31m      // Drop delivered dispatches older than 30 days to free space\u001b[39m\n\u001b[31m      const cutoff = Date.now() - 30 * 24 * 3600 * 1000;\u001b[39m\n\u001b[31m      dispatches = dispatches.filter(d =>\u001b[39m\n\u001b[31m        d.status !== 'draft' && (\u001b[39m\n\u001b[31m          d.status !== 'active' ||\u001b[39m\n\u001b[31m          new Date(d.createdAt || 0).getTime() > cutoff\u001b[39m\n\u001b[31m        ) ? true :\u001b[39m\n\u001b[31m        d.status === 'draft' || new Date(d.createdAt || 0).getTime() > cutoff\u001b[39m\n\u001b[31m      );\u001b[39m\n\u001b[31m      try { localStorage.setItem('360lm_disp_v2', JSON.stringify(dispatches)); } catch(_) {}\u001b[39m\n\u001b[31m    }\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// ── Postgres DB sync ──────────────────────────────────────────────────────\u001b[39m\n\u001b[31mconst _DB        = '/db';\u001b[39m\n\u001b[31m// PostgREST v12: Accept-Profile for reads, Content-Profile for writes\u001b[39m\n\u001b[31mconst _DHDR_READ  = { 'Accept-Profile': 'dispatch' };\u001b[39m\n\u001b[31mconst _DHDR_WRITE = { 'Content-Type': 'application/json', 'Content-Profile': 'dispatch', 'Accept-Profile': 'dispatch' };\u001b[39m\n\u001b[31mconst _DHDR_UPSERT = { ..._DHDR_WRITE, 'Prefer': 'resolution=merge-duplicates,return=minimal' };·\u001b[39m\n\u001b[31mfunction _dispRow(d) {\u001b[39m\n\u001b[31m  return {\u001b[39m\n\u001b[31m    dispatch_id:    d.id,\u001b[39m\n\u001b[31m    client:         d.client    || '',\u001b[39m\n\u001b[31m    job_ref:        d.jobref    || null,\u001b[39m\n\u001b[31m    courier:        d.courier   || '',\u001b[39m\n\u001b[31m    dispatch_date:  d.dispatchDate || null,\u001b[39m\n\u001b[31m    status:         d.status    || 'active',\u001b[39m\n\u001b[31m    total_count:    (d.consignees || []).length,\u001b[39m\n\u001b[31m    docketed_count: (d.consignees || []).filter(c => c.docket).length,\u001b[39m\n\u001b[31m    remarks:        d.remarks   || null,\u001b[39m\n\u001b[31m    created_by:     window._hubUser?.id || 'unknown',\u001b[39m\n\u001b[31m  };\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction _consRow(c, dispatchId, idx) {\u001b[39m\n\u001b[31m  return {\u001b[39m\n\u001b[31m    cons_id:            `${dispatchId}-${idx}`,\u001b[39m\n\u001b[31m    dispatch_id:        dispatchId,\u001b[39m\n\u001b[31m    seq_no:             idx,\u001b[39m\n\u001b[31m    name:               c.name          || '',\u001b[39m\n\u001b[31m    city:               c.city          || null,\u001b[39m\n\u001b[31m    address:            c.address       || null,\u001b[39m\n\u001b[31m    phone:              c.phone         || null,\u001b[39m\n\u001b[31m    pincode:            c.pincode       || null,\u001b[39m\n\u001b[31m    material:           c.material      || null,\u001b[39m\n\u001b[31m    qty:                c.qty           || null,\u001b[39m\n\u001b[31m    docket:             c.docket        || null,\u001b[39m\n\u001b[31m    status:             c.status        || 'pending',\u001b[39m\n\u001b[31m    last_event:         c.lastEvent     || null,\u001b[39m\n\u001b[31m    last_scan_location: c.lastScanLocation || null,\u001b[39m\n\u001b[31m    last_tracked_at:    c.lastTrackedAt || null,\u001b[39m\n\u001b[31m    manual_status:      c.manualStatus  || null,\u001b[39m\n\u001b[31m    manual_delivered:   !!c.manualDelivered,\u001b[39m\n\u001b[31m    manual_called_by:   c.manualCalledBy || null,\u001b[39m\n\u001b[31m    manual_remarks:     c.manualRemarks || null,\u001b[39m\n\u001b[31m    pod_uploaded:       !!c.podUploaded,\u001b[39m\n\u001b[31m  };\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31masync function syncDispatchToDB(d) {\u001b[39m\n\u001b[31m  if (!d || d.status === 'draft') return; // never sync drafts\u001b[39m\n\u001b[31m  try {\u001b[39m\n\u001b[31m    await fetch(`${_DB}/dispatches`, {\u001b[39m\n\u001b[31m      method: 'POST', headers: _DHDR_UPSERT,\u001b[39m\n\u001b[31m      body: JSON.stringify(_dispRow(d)),\u001b[39m\n\u001b[31m    });\u001b[39m\n\u001b[31m    const rows = (d.consignees || []).map((c, i) => _consRow(c, d.id, i));\u001b[39m\n\u001b[31m    if (rows.length) {\u001b[39m\n\u001b[31m      await fetch(`${_DB}/consignees`, {\u001b[39m\n\u001b[31m        method: 'POST', headers: _DHDR_UPSERT,\u001b[39m\n\u001b[31m        body: JSON.stringify(rows),\u001b[39m\n\u001b[31m      });\u001b[39m\n\u001b[31m    }\u001b[39m\n\u001b[31m  } catch(e) { console.warn('[dispatch-db] sync error:', e.message); }\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31masync function syncConsigneeToDB(c, dispatchId, idx) {\u001b[39m\n\u001b[31m  try {\u001b[39m\n\u001b[31m    await fetch(`${_DB}/consignees`, {\u001b[39m\n\u001b[31m      method: 'POST', headers: _DHDR_UPSERT,\u001b[39m\n\u001b[31m      body: JSON.stringify(_consRow(c, dispatchId, idx)),\u001b[39m\n\u001b[31m    });\u001b[39m\n\u001b[31m    // Log tracking event\u001b[39m\n\u001b[31m    if (c.lastEvent && c.status) {\u001b[39m\n\u001b[31m      fetch(`${_DB}/tracking_events`, {\u001b[39m\n\u001b[31m        method: 'POST', headers: _DHDR_WRITE,\u001b[39m\n\u001b[31m        body: JSON.stringify({\u001b[39m\n\u001b[31m          docket: c.docket, dispatch_id: dispatchId,\u001b[39m\n\u001b[31m          cons_id: `${dispatchId}-${idx}`,\u001b[39m\n\u001b[31m          status: c.status, event_text: c.lastEvent,\u001b[39m\n\u001b[31m          scan_location: c.lastScanLocation || null,\u001b[39m\n\u001b[31m        }),\u001b[39m\n\u001b[31m      }).catch(() => {});\u001b[39m\n\u001b[31m    }\u001b[39m\n\u001b[31m  } catch(e) { console.warn('[dispatch-db] consignee sync error:', e.message); }\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31masync function loadFromDB() {\u001b[39m\n\u001b[31m  try {\u001b[39m\n\u001b[31m    const [dResp, cResp] = await Promise.all([\u001b[39m\n\u001b[31m      fetch(`${_DB}/dispatches?order=created_at.desc&limit=100`, { headers: _DHDR_READ }),\u001b[39m\n\u001b[31m      fetch(`${_DB}/consignees?order=dispatch_id,seq_no`, { headers: _DHDR_READ }),\u001b[39m\n\u001b[31m    ]);\u001b[39m\n\u001b[31m    if (!dResp.ok) return;\u001b[39m\n\u001b[31m    const dbDispatches = await dResp.json();\u001b[39m\n\u001b[31m    const dbConsignees = cResp.ok ? await cResp.json() : [];\u001b[39m\n\u001b[31m    if (!dbDispatches.length) return;·\u001b[39m\n\u001b[31m    const dbIds = new Set(dbDispatches.map(d => d.dispatch_id));\u001b[39m\n\u001b[31m    const localOnly = dispatches.filter(d => !dbIds.has(d.id));·\u001b[39m\n\u001b[31m    const fromDB = dbDispatches.map(d => {\u001b[39m\n\u001b[31m      const cons = dbConsignees.filter(c => c.dispatch_id === d.dispatch_id);\u001b[39m\n\u001b[31m      return {\u001b[39m\n\u001b[31m        id:           d.dispatch_id,\u001b[39m\n\u001b[31m        client:       d.client,\u001b[39m\n\u001b[31m        jobref:       d.job_ref,\u001b[39m\n\u001b[31m        courier:      d.courier,\u001b[39m\n\u001b[31m        dispatchDate: d.dispatch_date,\u001b[39m\n\u001b[31m        status:       d.status,\u001b[39m\n\u001b[31m        remarks:      d.remarks,\u001b[39m\n\u001b[31m        createdAt:    d.created_at,\u001b[39m\n\u001b[31m        consignees:   cons.map(c => ({\u001b[39m\n\u001b[31m          name:              c.name,\u001b[39m\n\u001b[31m          city:              c.city,\u001b[39m\n\u001b[31m          address:           c.address,\u001b[39m\n\u001b[31m          phone:             c.phone,\u001b[39m\n\u001b[31m          pincode:           c.pincode,\u001b[39m\n\u001b[31m          material:          c.material,\u001b[39m\n\u001b[31m          qty:               c.qty,\u001b[39m\n\u001b[31m          docket:            c.docket,\u001b[39m\n\u001b[31m          status:            c.status,\u001b[39m\n\u001b[31m          lastEvent:         c.last_event,\u001b[39m\n\u001b[31m          lastScanLocation:  c.last_scan_location,\u001b[39m\n\u001b[31m          lastTrackedAt:     c.last_tracked_at,\u001b[39m\n\u001b[31m          manualStatus:      c.manual_status,\u001b[39m\n\u001b[31m          manualDelivered:   c.manual_delivered,\u001b[39m\n\u001b[31m          manualCalledBy:    c.manual_called_by,\u001b[39m\n\u001b[31m          manualRemarks:     c.manual_remarks,\u001b[39m\n\u001b[31m          podUploaded:       c.pod_uploaded,\u001b[39m\n\u001b[31m          _dbIdx:            c.seq_no,\u001b[39m\n\u001b[31m        })),\u001b[39m\n\u001b[31m      };\u001b[39m\n\u001b[31m    });·\u001b[39m\n\u001b[31m    dispatches = [...fromDB, ...localOnly];\u001b[39m\n\u001b[31m    saveData();\u001b[39m\n\u001b[31m    renderDashboard();\u001b[39m\n\u001b[31m    updateHistCount();\u001b[39m\n\u001b[31m  } catch(e) { console.warn('[dispatch-db] load error:', e.message); }\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31mfunction saveSheetsUrlLocal() {\u001b[39m\n\u001b[31m  localStorage.setItem('360lm_disp_sheets_url', JSON.stringify({ sheets_url: window.appConfig.sheets_url }));\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m/**\u001b[39m\n\u001b[31m * Fetch config.json from the VPS server (same origin).\u001b[39m\n\u001b[31m * This file contains the sheets_url — no manual entry needed.\u001b[39m\n\u001b[31m * Falls back to localStorage cache if fetch fails (offline).\u001b[39m\n\u001b[31m */\u001b[39m\n\u001b[31masync function loadServerConfig() {\u001b[39m\n\u001b[31m  try {\u001b[39m\n\u001b[31m    const resp = await fetch('./config.json', { cache: 'no-store' });\u001b[39m\n\u001b[31m    if (resp.ok) {\u001b[39m\n\u001b[31m      const cfg = await resp.json();\u001b[39m\n\u001b[31m      if (cfg.sheets_url) {\u001b[39m\n\u001b[31m        window.appConfig.sheets_url = cfg.sheets_url;\u001b[39m\n\u001b[31m        saveSheetsUrlLocal(); // cache it for offline use\u001b[39m\n\u001b[31m        document.getElementById('cfg_sheets_url').value = cfg.sheets_url;\u001b[39m\n\u001b[31m        return true;\u001b[39m\n\u001b[31m      }\u001b[39m\n\u001b[31m    }\u001b[39m\n\u001b[31m  } catch(e) {\u001b[39m\n\u001b[31m    console.log('config.json fetch failed, using cached URL:', e.message);\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m  return false;\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31m// INIT\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31m// ── Hub auth gate ─────────────────────────────────────────────────────────\u001b[39m\n\u001b[31mwindow._hubUser = null;\u001b[39m\n\u001b[31m(function() {\u001b[39m\n\u001b[31m  try {\u001b[39m\n\u001b[31m    const hubLogin = '/hub/?next=' + encodeURIComponent(location.pathname + location.search);\u001b[39m\n\u001b[31m    const s = JSON.parse(localStorage.getItem('lm360-session') || 'null');\u001b[39m\n\u001b[31m    if (!s || !s.empId || !s.name) { window.location.replace(hubLogin); return; }\u001b[39m\n\u001b[31m    if (Date.now() - new Date(s.loginAt).getTime() > 12 * 3600 * 1000) {\u001b[39m\n\u001b[31m      window.location.replace(hubLogin); return;\u001b[39m\n\u001b[31m    }\u001b[39m\n\u001b[31m    window._hubUser = { id: s.empId, name: s.name, role: s.role };\u001b[39m\n\u001b[31m  } catch(_) { window.location.replace('/hub/?next=' + encodeURIComponent(location.pathname + location.search)); }\u001b[39m\n\u001b[31m})();·\u001b[39m\n\u001b[31mwindow.addEventListener('load', async () => {\u001b[39m\n\u001b[31m  if (!window._hubUser) return; // should never reach here — gate redirected already\u001b[39m\n\u001b[31m  setTimeout(() => {\u001b[39m\n\u001b[31m    document.getElementById('splash').style.opacity = '0';\u001b[39m\n\u001b[31m    setTimeout(() => {\u001b[39m\n\u001b[31m      document.getElementById('splash').style.display = 'none';\u001b[39m\n\u001b[31m      document.getElementById('app').style.display = 'flex';\u001b[39m\n\u001b[31m    }, 450);\u001b[39m\n\u001b[31m  }, 1400);\u001b[39m\n\u001b[31m  loadData();\u001b[39m\n\u001b[31m  // Auto-authenticate config tab for admin users (no password needed)\u001b[39m\n\u001b[31m  if (['harish', 'pramod'].includes(window._hubUser?.id)) _adminAuthed = true;\u001b[39m\n\u001b[31m  setToday();\u001b[39m\n\u001b[31m  addConsignee();\u001b[39m\n\u001b[31m  updateHistCount();\u001b[39m\n\u001b[31m  setInterval(updateNetDot, 5000);\u001b[39m\n\u001b[31m  updateNetDot();\u001b[39m\n\u001b[31m  checkShareTarget();\u001b[39m\n\u001b[31m  setupDragDrop();·\u001b[39m\n\u001b[31m  // Step 1: fetch sheets_url from VPS config.json (auto, no user input)\u001b[39m\n\u001b[31m  await loadServerConfig();·\u001b[39m\n\u001b[31m  // Step 2: populate config tab field\u001b[39m\n\u001b[31m  document.getElementById('cfg_sheets_url').value = window.appConfig.sheets_url || '';·\u001b[39m\n\u001b[31m  // Step 3: load dispatches from Postgres in background (merge with local)\u001b[39m\n\u001b[31m  loadFromDB(); // non-blocking — updates UI when data arrives·\u001b[39m\n\u001b[31m  // Step 4: sync full config from Google Sheet silently\u001b[39m\n\u001b[31m  if (window.appConfig.sheets_url) {\u001b[39m\n\u001b[31m    syncFromSheet(true); // silent on startup\u001b[39m\n\u001b[31m  } else {\u001b[39m\n\u001b[31m    // Only show config tab if no URL found anywhere\u001b[39m\n\u001b[31m    showPane('config');\u001b[39m\n\u001b[31m    showToast('No config.json found on server', 'err');\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m  // Pre-warm OCR after delay\u001b[39m\n\u001b[31m  setTimeout(() => { if (typeof window.initOCR === 'function') window.initOCR(); }, 4000);\u001b[39m\n\u001b[31m});·\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31m// WEB SHARE TARGET\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31masync function checkShareTarget() {\u001b[39m\n\u001b[31m  // Check URL params for share target\u001b[39m\n\u001b[31m  const params = new URLSearchParams(window.location.search);\u001b[39m\n\u001b[31m  const action = params.get('action');\u001b[39m\n\u001b[31m  if (action !== 'share') return;·\u001b[39m\n\u001b[31m  // Check for shared files via Share Target API\u001b[39m\n\u001b[31m  if ('launchQueue' in window) {\u001b[39m\n\u001b[31m    window.launchQueue.setConsumer(async launchParams => {\u001b[39m\n\u001b[31m      if (!launchParams.files || launchParams.files.length === 0) return;\u001b[39m\n\u001b[31m      const files = await Promise.all(launchParams.files.map(f => f.getFile()));\u001b[39m\n\u001b[31m      const imageFiles = files.filter(f => f.type.startsWith('image/'));\u001b[39m\n\u001b[31m      if (imageFiles.length > 0) {\u001b[39m\n\u001b[31m        document.getElementById('shareTargetInfo').style.display = 'flex';\u001b[39m\n\u001b[31m        document.getElementById('sharedCount').textContent = imageFiles.length;\u001b[39m\n\u001b[31m        document.getElementById('shareBanner').classList.add('show');\u001b[39m\n\u001b[31m        setTimeout(() => document.getElementById('shareBanner').classList.remove('show'), 4000);\u001b[39m\n\u001b[31m        showPane('new');\u001b[39m\n\u001b[31m        await handleSlipFiles(imageFiles);\u001b[39m\n\u001b[31m      }\u001b[39m\n\u001b[31m    });\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction dismissShare() {\u001b[39m\n\u001b[31m  document.getElementById('shareBanner').classList.remove('show');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31m// DRAG & DROP\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31mfunction setupDragDrop() {\u001b[39m\n\u001b[31m  const zone = document.getElementById('slipZone');\u001b[39m\n\u001b[31m  zone.addEventListener('dragover', e => { e.preventDefault(); zone.classList.add('drag'); });\u001b[39m\n\u001b[31m  zone.addEventListener('dragleave', () => zone.classList.remove('drag'));\u001b[39m\n\u001b[31m  zone.addEventListener('drop', e => {\u001b[39m\n\u001b[31m    e.preventDefault();\u001b[39m\n\u001b[31m    zone.classList.remove('drag');\u001b[39m\n\u001b[31m    const files = Array.from(e.dataTransfer.files).filter(f => f.type.startsWith('image/'));\u001b[39m\n\u001b[31m    if (files.length) handleSlipFiles(files);\u001b[39m\n\u001b[31m  });\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31m// SLIP OCR HANDLER\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31masync function handleSlipFiles(files) {\u001b[39m\n\u001b[31m  const fileArr = Array.from(files);\u001b[39m\n\u001b[31m  if (!fileArr.length) return;\u001b[39m\n\u001b[31m  // If dockets already mapped from store list — allow upload but skip OCR extraction\u001b[39m\n\u001b[31m  const skipOCR = shouldSkipOCR();\u001b[39m\n\u001b[31m  if (skipOCR) {\u001b[39m\n\u001b[31m    showToast('Dockets already mapped — slips saved for reference only', 'info');\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m  showOCRWorking(true, `Processing ${fileArr.length} slip(s)...`);·\u001b[39m\n\u001b[31m  for (let i = 0; i < fileArr.length; i++) {\u001b[39m\n\u001b[31m    const file = fileArr[i];\u001b[39m\n\u001b[31m    showOCRWorking(true, `OCR: ${i+1}/${fileArr.length} — ${file.name}`);\u001b[39m\n\u001b[31m    const imageData = await readFileAsDataURL(file);\u001b[39m\n\u001b[31m    let result = { docket: '', confidence: 0, source: 'manual' };\u001b[39m\n\u001b[31m    if (!skipOCR) {\u001b[39m\n\u001b[31m      try {\u001b[39m\n\u001b[31m        result = await window.runOCROnImage(imageData, file.name);\u001b[39m\n\u001b[31m      } catch(e) {}\u001b[39m\n\u001b[31m    } else {\u001b[39m\n\u001b[31m      result = { docket: '', confidence: 1, source: 'skipped — docket pre-mapped' };\u001b[39m\n\u001b[31m    }\u001b[39m\n\u001b[31m    slipOCRData.push({ file, imageData, filename: file.name, ...result });\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m  showOCRWorking(false);\u001b[39m\n\u001b[31m  renderOCRResults();\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction readFileAsDataURL(file) {\u001b[39m\n\u001b[31m  return new Promise((res, rej) => {\u001b[39m\n\u001b[31m    const r = new FileReader();\u001b[39m\n\u001b[31m    r.onload = e => res(e.target.result);\u001b[39m\n\u001b[31m    r.onerror = rej;\u001b[39m\n\u001b[31m    r.readAsDataURL(file);\u001b[39m\n\u001b[31m  });\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction renderOCRResults() {\u001b[39m\n\u001b[31m  if (!slipOCRData.length) return;\u001b[39m\n\u001b[31m  const list = document.getElementById('ocrSlipList');\u001b[39m\n\u001b[31m  document.getElementById('ocrResults').classList.add('visible');·\u001b[39m\n\u001b[31m  const needReview = slipOCRData.filter(s => s.confidence < 0.65 && s.docket).length;\u001b[39m\n\u001b[31m  const autoOk     = slipOCRData.filter(s => s.confidence >= 0.88).length;\u001b[39m\n\u001b[31m  const summary    = document.getElementById('ocrSummary');\u001b[39m\n\u001b[31m  if (summary) {\u001b[39m\n\u001b[31m    summary.style.display = '';\u001b[39m\n\u001b[31m    summary.innerHTML =\u001b[39m\n\u001b[31m      `<span style=\\\"color:var(--grn)\\\">✓ ${autoOk} auto-accepted</span>` +\u001b[39m\n\u001b[31m      (needReview ? `&nbsp;·&nbsp;<span style=\\\"color:var(--ylw)\\\">⚠ ${needReview} need manual check</span>` : '');\u001b[39m\n\u001b[31m  }·\u001b[39m\n\u001b[31m  list.innerHTML = slipOCRData.map((s, i) => {\u001b[39m\n\u001b[31m    const autoAccepted = s.confidence >= 0.88 && s.docket;\u001b[39m\n\u001b[31m    const needsCheck   = s.confidence < 0.65  && s.docket;\u001b[39m\n\u001b[31m    const confClass = autoAccepted ? 'conf-high' : needsCheck ? 'conf-low' : 'conf-med';\u001b[39m\n\u001b[31m    const confLabel = autoAccepted\u001b[39m\n\u001b[31m      ? `✓ ${Math.round(s.confidence * 100)}% — auto`\u001b[39m\n\u001b[31m      : needsCheck\u001b[39m\n\u001b[31m        ? `⚠ ${Math.round(s.confidence * 100)}% — verify`\u001b[39m\n\u001b[31m        : `${Math.round(s.confidence * 100)}%`;\u001b[39m\n\u001b[31m    const borderStyle = needsCheck ? 'border:1px solid var(--ylw);' : '';\u001b[39m\n\u001b[31m    return `\u001b[39m\n\u001b[31m    <div class=\\\"ocr-slip\\\" id=\\\"ocrSlip_${i}\\\" style=\\\"${borderStyle}\\\">\u001b[39m\n\u001b[31m      <img src=\\\"${s.imageData}\\\" alt=\\\"slip ${i+1}\\\">\u001b[39m\n\u001b[31m      <div class=\\\"ocr-slip-body\\\">\u001b[39m\n\u001b[31m        <div class=\\\"ocr-slip-name\\\">${s.filename}</div>\u001b[39m\n\u001b[31m        <input type=\\\"text\\\" class=\\\"ocr-slip-docket\\\" id=\\\"ocrDocket_${i}\\\"\u001b[39m\n\u001b[31m          value=\\\"${s.docket}\\\" placeholder=\\\"Docket number\\\"\u001b[39m\n\u001b[31m          ${autoAccepted ? 'style=\\\"opacity:.6\\\"' : ''}\u001b[39m\n\u001b[31m          oninput=\\\"slipOCRData[${i}].docket=this.value;slipOCRData[${i}].confidence=0.5\\\">\u001b[39m\n\u001b[31m        <div class=\\\"ocr-slip-status\\\">\u001b[39m\n\u001b[31m          <span class=\\\"ocr-confidence ${confClass}\\\">${confLabel}</span>\u001b[39m\n\u001b[31m          <span style=\\\"color:var(--t4);font-family:var(--fnt-mono);font-size:.6rem;\\\">via ${s.source}</span>\u001b[39m\n\u001b[31m          <button class=\\\"ocr-ai-btn\\\" onclick=\\\"reOCR(${i})\\\">🔄 Re-OCR</button>\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m    </div>`;\u001b[39m\n\u001b[31m  }).join('');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31masync function reOCR(i) {\u001b[39m\n\u001b[31m  const s = slipOCRData[i];\u001b[39m\n\u001b[31m  showOCRWorking(true, `Re-running OCR on ${s.filename}...`);\u001b[39m\n\u001b[31m  try {\u001b[39m\n\u001b[31m    const result = await window.runOCROnImage(s.imageData, s.filename);\u001b[39m\n\u001b[31m    slipOCRData[i] = { ...s, ...result };\u001b[39m\n\u001b[31m    renderOCRResults();\u001b[39m\n\u001b[31m    showToast('Re-OCR complete', 'ok');\u001b[39m\n\u001b[31m  } catch(e) {\u001b[39m\n\u001b[31m    showToast('OCR failed', 'err');\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m  showOCRWorking(false);\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction clearSlips() {\u001b[39m\n\u001b[31m  slipOCRData = [];\u001b[39m\n\u001b[31m  document.getElementById('ocrResults').classList.remove('visible');\u001b[39m\n\u001b[31m  document.getElementById('ocrSlipList').innerHTML = '';\u001b[39m\n\u001b[31m  const sum = document.getElementById('ocrSummary');\u001b[39m\n\u001b[31m  if (sum) { sum.style.display = 'none'; sum.innerHTML = ''; }\u001b[39m\n\u001b[31m  document.getElementById('shareTargetInfo').style.display = 'none';\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction applyAllDockets() {\u001b[39m\n\u001b[31m  // Match each OCR result to a consignee by order or by name matching\u001b[39m\n\u001b[31m  const consList = document.getElementById('consList');\u001b[39m\n\u001b[31m  const cards = consList.querySelectorAll('.cons-card');\u001b[39m\n\u001b[31m  slipOCRData.forEach((s, i) => {\u001b[39m\n\u001b[31m    if (!s.docket) return;\u001b[39m\n\u001b[31m    const card = cards[i];\u001b[39m\n\u001b[31m    if (card) {\u001b[39m\n\u001b[31m      const docketInput = card.querySelector('.cons-docket-input');\u001b[39m\n\u001b[31m      if (docketInput) {\u001b[39m\n\u001b[31m        docketInput.value = s.docket;\u001b[39m\n\u001b[31m        showLinkedDocket(card, s.docket);\u001b[39m\n\u001b[31m      }\u001b[39m\n\u001b[31m    }\u001b[39m\n\u001b[31m  });\u001b[39m\n\u001b[31m  showToast(`Applied ${slipOCRData.filter(s=>s.docket).length} dockets ✓`, 'ok');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31m// STORE LIST — with column mapping UI\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31mlet _rawRows = [];      // raw rows from file, kept for re-mapping\u001b[39m\n\u001b[31mlet _rawHeaders = [];   // headers from file·\u001b[39m\n\u001b[31masync function handleStoreFile(input) {\u001b[39m\n\u001b[31m  const file = input.files[0];\u001b[39m\n\u001b[31m  if (!file) return;\u001b[39m\n\u001b[31m  showToast('Reading file...', 'info');·\u001b[39m\n\u001b[31m  const data = await readFileAsDataURL(file);\u001b[39m\n\u001b[31m  const base64 = data.split(',')[1];\u001b[39m\n\u001b[31m  const binary = atob(base64);\u001b[39m\n\u001b[31m  const bytes = new Uint8Array(binary.length);\u001b[39m\n\u001b[31m  for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);·\u001b[39m\n\u001b[31m  let rows = [];\u001b[39m\n\u001b[31m  try {\u001b[39m\n\u001b[31m    if (file.name.endsWith('.csv')) {\u001b[39m\n\u001b[31m      const text = new TextDecoder().decode(bytes);\u001b[39m\n\u001b[31m      rows = text.split('\\\\n').map(r => r.split(',').map(c => c.trim().replace(/^\\\"|\\\"$/g, '')));\u001b[39m\n\u001b[31m    } else {\u001b[39m\n\u001b[31m      const wb = XLSX.read(bytes, { type: 'array' });\u001b[39m\n\u001b[31m      const ws = wb.Sheets[wb.SheetNames[0]];\u001b[39m\n\u001b[31m      rows = XLSX.utils.sheet_to_json(ws, { header: 1 });\u001b[39m\n\u001b[31m    }\u001b[39m\n\u001b[31m  } catch(e) { showToast('File read error: ' + e.message, 'err'); return; }·\u001b[39m\n\u001b[31m  if (!rows.length || !rows[0]) { showToast('File appears empty', 'err'); return; }·\u001b[39m\n\u001b[31m  _rawHeaders = (rows[0] || []).map(h => (h || '').toString().trim());\u001b[39m\n\u001b[31m  _rawRows = rows.slice(1).filter(r => r && r.some(c => c !== '' && c != null));·\u001b[39m\n\u001b[31m  document.getElementById('storeTableArea').style.display = 'block';\u001b[39m\n\u001b[31m  renderMappingUI(file.name);\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// ── COLUMN MAPPING FIELDS ─────────────────────────────\u001b[39m\n\u001b[31mconst MAPPING_FIELDS = [\u001b[39m\n\u001b[31m  { key: 'name',    label: 'Store / Partner Name', required: true,\u001b[39m\n\u001b[31m    keywords: ['store','shop','name','outlet','consignee','destination','partner','dealer','client','retailer'] },\u001b[39m\n\u001b[31m  { key: 'city',    label: 'City / Location',       required: false,\u001b[39m\n\u001b[31m    keywords: ['city','town','district','location','area','region','place'] },\u001b[39m\n\u001b[31m  { key: 'phone',   label: 'Phone / Mobile',        required: false,\u001b[39m\n\u001b[31m    keywords: ['phone','mobile','contact','tel','number','mob'] },\u001b[39m\n\u001b[31m  { key: 'docket',  label: 'Docket / AWB No.',      required: false,\u001b[39m\n\u001b[31m    keywords: ['docket','awb','tracking','courier','dkt','consignment','cno','waybill'] },\u001b[39m\n\u001b[31m  { key: 'address', label: 'Address',               required: false,\u001b[39m\n\u001b[31m    keywords: ['address','addr','street','locality','area'] },\u001b[39m\n\u001b[31m  { key: 'pincode', label: 'Pincode / PIN',          required: false,\u001b[39m\n\u001b[31m    keywords: ['pin','pincode','postal','zip','code'] },\u001b[39m\n\u001b[31m];·\u001b[39m\n\u001b[31m// Saved mapping key based on column fingerprint (so same client file auto-maps)\u001b[39m\n\u001b[31mfunction getMappingKey() {\u001b[39m\n\u001b[31m  return 'colmap_' + _rawHeaders.join('|').toLowerCase().replace(/\\\\s+/g,'');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// ── Detect if a column is a combined/multiline address cell ─────────────\u001b[39m\n\u001b[31mfunction isCombinedColumn(colIdx) {\u001b[39m\n\u001b[31m  if (colIdx < 0) return false;\u001b[39m\n\u001b[31m  // Check first 3 data rows — if any cell contains newline or has 6+ digit number (pincode) = combined\u001b[39m\n\u001b[31m  const samples = _rawRows.slice(0, 3).map(r => (r[colIdx] || '').toString());\u001b[39m\n\u001b[31m  return samples.some(s => s.includes('\\\\n') || /\\\\b\\\\d{6}\\\\b/.test(s) || s.split(',').length >= 3);\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction renderMappingUI(filename) {\u001b[39m\n\u001b[31m  const saved = (() => { try { return JSON.parse(localStorage.getItem(getMappingKey()) || 'null'); } catch(e) { return null; } })();\u001b[39m\n\u001b[31m  const isSaved = !!saved;\u001b[39m\n\u001b[31m  const colInfoEl = document.getElementById('storeColInfo');\u001b[39m\n\u001b[31m  colInfoEl.style.display = 'block';·\u001b[39m\n\u001b[31m  const suggested = {};\u001b[39m\n\u001b[31m  MAPPING_FIELDS.forEach(f => {\u001b[39m\n\u001b[31m    const idx = findCol(_rawHeaders, f.keywords);\u001b[39m\n\u001b[31m    suggested[f.key] = saved ? (saved[f.key] ?? idx) : idx;\u001b[39m\n\u001b[31m  });·\u001b[39m\n\u001b[31m  // Detect if address column is combined\u001b[39m\n\u001b[31m  const addrIdx = suggested.address >= 0 ? suggested.address : findCol(_rawHeaders, ['address','addr']);\u001b[39m\n\u001b[31m  const looksCombo = isCombinedColumn(addrIdx) || isCombinedColumn(suggested.name);\u001b[39m\n\u001b[31m  const comboCol   = looksCombo ? (addrIdx >= 0 ? addrIdx : suggested.name) : -1;\u001b[39m\n\u001b[31m  const comboPreview = comboCol >= 0 && _rawRows[0] ? (_rawRows[0][comboCol] || '').toString().slice(0, 120) : '';·\u001b[39m\n\u001b[31m  colInfoEl.innerHTML = `\u001b[39m\n\u001b[31m    <div style=\\\"background:var(--sur2);border:1px solid var(--bdr);border-radius:var(--r-sm);padding:12px;\\\">\u001b[39m\n\u001b[31m      <div style=\\\"display:flex;align-items:center;justify-content:space-between;margin-bottom:10px;\\\">\u001b[39m\n\u001b[31m        <div style=\\\"font-family:var(--fnt-mono);font-size:.68rem;color:var(--sky);\\\">\u001b[39m\n\u001b[31m          📋 ${filename} &nbsp;·&nbsp; ${_rawRows.length} rows &nbsp;·&nbsp; ${_rawHeaders.length} columns\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m        ${isSaved ? '<span class=\\\"map-saved-badge\\\">✓ mapping remembered</span>' : ''}\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      <div style=\\\"font-size:.68rem;color:var(--t3);font-family:var(--fnt-mono);margin-bottom:10px;word-break:break-all;\\\">\u001b[39m\n\u001b[31m        Columns: <span style=\\\"color:var(--t2);\\\">${_rawHeaders.map((h,i)=>`[${i+1}] ${h||'?'}`).join(' · ')}</span>\u001b[39m\n\u001b[31m      </div>·\u001b[39m\n\u001b[31m      ${looksCombo ? `\u001b[39m\n\u001b[31m      <!-- COMBINED COLUMN DETECTED -->\u001b[39m\n\u001b[31m      <div style=\\\"background:rgba(56,189,248,.08);border:1px solid rgba(56,189,248,.3);\u001b[39m\n\u001b[31m        border-radius:var(--r-sm);padding:10px 12px;margin-bottom:10px;\\\">\u001b[39m\n\u001b[31m        <div style=\\\"font-size:.72rem;color:var(--sky);font-weight:600;margin-bottom:6px;\\\">\u001b[39m\n\u001b[31m          🤖 Combined Address Column Detected\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m        <div style=\\\"font-size:.68rem;color:var(--t2);font-family:var(--fnt-mono);\u001b[39m\n\u001b[31m          white-space:pre-wrap;background:var(--bg);padding:6px 8px;border-radius:4px;\u001b[39m\n\u001b[31m          margin-bottom:8px;max-height:80px;overflow:hidden;\\\">${comboPreview}</div>\u001b[39m\n\u001b[31m        <div style=\\\"font-size:.7rem;color:var(--t3);margin-bottom:8px;\\\">\u001b[39m\n\u001b[31m          AI will extract: Name · Address · City · Pincode · Phone from each cell.\u001b[39m\n\u001b[31m          Runs in browser (Transformers.js) with Ollama fallback.\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m        <div style=\\\"display:flex;align-items:center;gap:8px;\\\">\u001b[39m\n\u001b[31m          <label style=\\\"display:flex;align-items:center;gap:6px;font-size:.72rem;color:var(--t1);cursor:pointer;\\\">\u001b[39m\n\u001b[31m            <input type=\\\"checkbox\\\" id=\\\"useAIParse\\\" checked style=\\\"cursor:pointer;\\\">\u001b[39m\n\u001b[31m            Use AI to parse combined column\u001b[39m\n\u001b[31m          </label>\u001b[39m\n\u001b[31m          <select id=\\\"comboColSel\\\" style=\\\"background:var(--sur2);border:1px solid var(--bdr);\u001b[39m\n\u001b[31m            color:var(--t1);font-family:var(--fnt-mono);font-size:.72rem;padding:4px 8px;\u001b[39m\n\u001b[31m            border-radius:var(--r-sm);outline:none;\\\">\u001b[39m\n\u001b[31m            ${_rawHeaders.map((h,i)=>`<option value=\\\"${i}\\\" ${i===comboCol?'selected':''}>${h||'(col '+(i+1)+')'}</option>`).join('')}\u001b[39m\n\u001b[31m          </select>\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      ` : ''}·\u001b[39m\n\u001b[31m      <div id=\\\"manualMapSection\\\" style=\\\"${looksCombo ? 'display:none;' : ''}\\\">\u001b[39m\n\u001b[31m        <div style=\\\"font-size:.68rem;color:var(--t3);margin-bottom:6px;\\\">\u001b[39m\n\u001b[31m          ${looksCombo ? 'Manual mapping (used when AI parse is off, or for other columns):' : 'Map each field to your column:'}\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m        <table class=\\\"map-table\\\">\u001b[39m\n\u001b[31m          <thead><tr><th>Field</th><th>Your column</th><th>Preview (row 1)</th></tr></thead>\u001b[39m\n\u001b[31m          <tbody>\u001b[39m\n\u001b[31m            ${MAPPING_FIELDS.map(f => {\u001b[39m\n\u001b[31m              const selIdx = suggested[f.key] ?? -1;\u001b[39m\n\u001b[31m              const preview = selIdx >= 0 && _rawRows[0] ? ((_rawRows[0][selIdx]||'').toString().slice(0,40)) : '—';\u001b[39m\n\u001b[31m              return `<tr>\u001b[39m\n\u001b[31m                <td style=\\\"white-space:nowrap;color:var(--t1);font-weight:500;font-size:.75rem;\\\">\u001b[39m\n\u001b[31m                  ${f.label}${f.required ? '<span class=\\\"map-req\\\">*</span>' : ''}\u001b[39m\n\u001b[31m                </td>\u001b[39m\n\u001b[31m                <td>\u001b[39m\n\u001b[31m                  <select id=\\\"mapsel_${f.key}\\\" onchange=\\\"updateMappingPreview('${f.key}')\\\">\u001b[39m\n\u001b[31m                    ${_rawHeaders.map((h,i)=>\u001b[39m\n\u001b[31m                      `<option value=\\\"${i}\\\" ${i===selIdx?'selected':''}>${h||'(col '+(i+1)+')'}</option>`\u001b[39m\n\u001b[31m                    ).join('')}\u001b[39m\n\u001b[31m                    <option value=\\\"-1\\\" ${selIdx===-1?'selected':''}>— not in file —</option>\u001b[39m\n\u001b[31m                  </select>\u001b[39m\n\u001b[31m                </td>\u001b[39m\n\u001b[31m                <td id=\\\"mapprev_${f.key}\\\" style=\\\"font-family:var(--fnt-mono);font-size:.68rem;\u001b[39m\n\u001b[31m                  color:var(--t2);max-width:90px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;\\\">\u001b[39m\n\u001b[31m                  ${preview}\u001b[39m\n\u001b[31m                </td>\u001b[39m\n\u001b[31m              </tr>`;\u001b[39m\n\u001b[31m            }).join('')}\u001b[39m\n\u001b[31m          </tbody>\u001b[39m\n\u001b[31m        </table>\u001b[39m\n\u001b[31m      </div>·\u001b[39m\n\u001b[31m      <!-- Also map docket/courier no column separately — always shown -->\u001b[39m\n\u001b[31m      <div style=\\\"margin-top:10px;padding-top:10px;border-top:1px solid var(--bdr);\\\">\u001b[39m\n\u001b[31m        <div style=\\\"font-size:.68rem;color:var(--t3);margin-bottom:6px;font-family:var(--fnt-mono);\\\">\u001b[39m\n\u001b[31m          DOCKET / COURIER NO. COLUMN (required for tracking):\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m        <div style=\\\"display:flex;gap:8px;align-items:center;\\\">\u001b[39m\n\u001b[31m          <select id=\\\"docketColSel\\\" style=\\\"background:var(--sur2);border:1px solid var(--bdr);\u001b[39m\n\u001b[31m            color:var(--acc);font-family:var(--fnt-mono);font-size:.78rem;padding:6px 10px;\u001b[39m\n\u001b[31m            border-radius:var(--r-sm);outline:none;flex:1;\\\">\u001b[39m\n\u001b[31m            <option value=\\\"-1\\\">— Docket not in file —</option>\u001b[39m\n\u001b[31m            ${_rawHeaders.map((h,i)=>`<option value=\\\"${i}\\\" ${i===(suggested.docket??-1)?'selected':''}>${h||'(col '+(i+1)+')'}</option>`).join('')}\u001b[39m\n\u001b[31m          </select>\u001b[39m\n\u001b[31m          <span id=\\\"docketPrev\\\" style=\\\"font-family:var(--fnt-mono);font-size:.72rem;color:var(--acc);\\\">\u001b[39m\n\u001b[31m            ${suggested.docket >= 0 && _rawRows[0] ? (_rawRows[0][suggested.docket]||'') : ''}\u001b[39m\n\u001b[31m          </span>\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m      </div>·\u001b[39m\n\u001b[31m      <div class=\\\"btn-row\\\" style=\\\"margin-top:12px;\\\">\u001b[39m\n\u001b[31m        <button class=\\\"btn btn-ghost\\\" style=\\\"font-size:.78rem;\\\" onclick=\\\"clearStores()\\\">✕ Cancel</button>\u001b[39m\n\u001b[31m        <button class=\\\"btn btn-orange\\\" style=\\\"font-size:.78rem;\\\" onclick=\\\"applyMapping()\\\">✓ Apply Mapping</button>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m  `;\u001b[39m\n\u001b[31m  document.getElementById('storeTable').innerHTML = '';\u001b[39m\n\u001b[31m  document.getElementById('storeAddPanel').style.display = 'none';·\u001b[39m\n\u001b[31m  // Update docket preview on change\u001b[39m\n\u001b[31m  const docketSel = document.getElementById('docketColSel');\u001b[39m\n\u001b[31m  if (docketSel) docketSel.addEventListener('change', () => {\u001b[39m\n\u001b[31m    const idx = parseInt(docketSel.value);\u001b[39m\n\u001b[31m    const prev = document.getElementById('docketPrev');\u001b[39m\n\u001b[31m    if (prev) prev.textContent = idx >= 0 && _rawRows[0] ? (_rawRows[0][idx]||'') : '';\u001b[39m\n\u001b[31m  });·\u001b[39m\n\u001b[31m  // Toggle manual map opacity when AI checkbox changes\u001b[39m\n\u001b[31m  const aiChk = document.getElementById('useAIParse');\u001b[39m\n\u001b[31m  const manSec = document.getElementById('manualMapSection');\u001b[39m\n\u001b[31m  if (aiChk && manSec) {\u001b[39m\n\u001b[31m    aiChk.addEventListener('change', () => {\u001b[39m\n\u001b[31m      manSec.style.display = aiChk.checked ? 'none' : 'block';\u001b[39m\n\u001b[31m    });\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction updateMappingPreview(key) {\u001b[39m\n\u001b[31m  const sel = document.getElementById('mapsel_' + key);\u001b[39m\n\u001b[31m  if (!sel) return;\u001b[39m\n\u001b[31m  const idx = parseInt(sel.value);\u001b[39m\n\u001b[31m  const prev = document.getElementById('mapprev_' + key);\u001b[39m\n\u001b[31m  if (prev) prev.textContent = (idx >= 0 && _rawRows[0]) ? ((_rawRows[0][idx]||'').toString().slice(0,40)) : '—';\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// ── REGEX-FIRST ADDRESS PARSER ───────────────────────────────────────────\u001b[39m\n\u001b[31m// Improved Indian address parser\u001b[39m\n\u001b[31m// Handles formats seen in real BTL dispatch files\u001b[39m\n\u001b[31mfunction parseAddressRegex(raw) {\u001b[39m\n\u001b[31m  const text = raw.toString().replace(/\\\\r/g, '').trim();\u001b[39m\n\u001b[31m  const lines = text.split('\\\\n').map(l => l.trim()).filter(Boolean);\u001b[39m\n\u001b[31m  const result = { name:'', address:'', city:'', pincode:'', phone:'', confidence:0 };·\u001b[39m\n\u001b[31m  // ── PHONE: Indian mobile 6-9 prefix, 10 digits ──────────────────────────\u001b[39m\n\u001b[31m  // Also handles \\\"Phone : 9876543210\\\", \\\"M.No.-9876543210\\\", \\\"Mobile - 9876543210\\\"\u001b[39m\n\u001b[31m  const phoneMatch = text.match(/(?:Phone|Mobile|Mob|M\\\\.No\\\\.?|Ph\\\\.?)?\\\\s*[:\\\\-]?\\\\s*\\\\b([6-9]\\\\d{9})\\\\b/i);\u001b[39m\n\u001b[31m  if (phoneMatch) result.phone = phoneMatch[1];·\u001b[39m\n\u001b[31m  // ── PINCODE: 6-digit standalone number ──────────────────────────────────\u001b[39m\n\u001b[31m  // Must not be part of a longer number (e.g. not an account number)\u001b[39m\n\u001b[31m  const pinMatch = text.match(/(?<![\\\\d])([1-9]\\\\d{5})(?![\\\\d])/);\u001b[39m\n\u001b[31m  if (pinMatch) result.pincode = pinMatch[1];·\u001b[39m\n\u001b[31m  // ── CITY: the token immediately before the pincode ───────────────────────\u001b[39m\n\u001b[31m  const indianStates = ['rajasthan','gujarat','maharashtra','punjab','haryana',\u001b[39m\n\u001b[31m    'uttar pradesh','madhya pradesh','karnataka','tamil nadu','andhra pradesh',\u001b[39m\n\u001b[31m    'telangana','kerala','west bengal','bihar','odisha','jharkhand','assam',\u001b[39m\n\u001b[31m    'himachal pradesh','uttarakhand','chhattisgarh','goa','delhi','chandigarh',\u001b[39m\n\u001b[31m    'raj.','raj'];\u001b[39m\n\u001b[31m  if (result.pincode) {\u001b[39m\n\u001b[31m    const pinPos = text.indexOf(result.pincode);\u001b[39m\n\u001b[31m    const beforePin = text.slice(0, pinPos);\u001b[39m\n\u001b[31m    // Strategy 1: split by comma/newline, walk backwards skipping state names\u001b[39m\n\u001b[31m    const chunks = beforePin.split(/[,\\\\n]/).map(c => c.trim()).filter(Boolean);\u001b[39m\n\u001b[31m    for (let i = chunks.length - 1; i >= 0; i--) {\u001b[39m\n\u001b[31m      const chunk = chunks[i].replace(/\\\\d+/g,'').replace(/[()]/g,'').replace(/[.-]/g,'').trim();\u001b[39m\n\u001b[31m      const lower = chunk.toLowerCase();\u001b[39m\n\u001b[31m      if (!chunk || chunk.length < 2) continue;\u001b[39m\n\u001b[31m      if (indianStates.includes(lower)) continue;\u001b[39m\n\u001b[31m      // Skip chunks that look like addresses (contain numbers or long strings)\u001b[39m\n\u001b[31m      if (/\\\\d/.test(chunk)) continue;\u001b[39m\n\u001b[31m      result.city = chunk;\u001b[39m\n\u001b[31m      break;\u001b[39m\n\u001b[31m    }\u001b[39m\n\u001b[31m    // Strategy 2: if still no city, look for last capitalised word before pincode\u001b[39m\n\u001b[31m    // handles \\\"compound paota Jodhpur 342006\\\" → Jodhpur\u001b[39m\n\u001b[31m    if (!result.city) {\u001b[39m\n\u001b[31m      const wordsBefore = beforePin.replace(/[,\\\\n]/g,' ').trim().split(/\\\\s+/);\u001b[39m\n\u001b[31m      for (let i = wordsBefore.length - 1; i >= 0; i--) {\u001b[39m\n\u001b[31m        const w = wordsBefore[i].replace(/[^a-zA-Z]/g,'');\u001b[39m\n\u001b[31m        if (w.length < 3) continue;\u001b[39m\n\u001b[31m        if (indianStates.includes(w.toLowerCase())) continue;\u001b[39m\n\u001b[31m        // Prefer words that start with capital letter (likely a proper noun/city)\u001b[39m\n\u001b[31m        if (/^[A-Z]/.test(w)) { result.city = w; break; }\u001b[39m\n\u001b[31m      }\u001b[39m\n\u001b[31m    }\u001b[39m\n\u001b[31m  }·\u001b[39m\n\u001b[31m  // ── NAME: first meaningful line, extract shop name ───────────────────────\u001b[39m\n\u001b[31m  // Patterns:\u001b[39m\n\u001b[31m  //   \\\"Sanwar Mal Saini, C/o R G COMPUTERS\\\" → name = \\\"R G COMPUTERS\\\"\u001b[39m\n\u001b[31m  //   \\\"SANWAR MAL SAINI, C/o ADVANCE TECHNOLOGIES\\\" → name = \\\"ADVANCE TECHNOLOGIES\\\"\u001b[39m\n\u001b[31m  //   \\\"Sunil Vaishnav\\\" → name = \\\"Sunil Vaishnav\\\" (no C/o)\u001b[39m\n\u001b[31m  //   \\\"Baltej Singh, Professional computer care\\\" → name = \\\"Professional computer care\\\"\u001b[39m\n\u001b[31m  if (lines.length > 0) {\u001b[39m\n\u001b[31m    const firstLine = lines[0];\u001b[39m\n\u001b[31m    // Check for C/o or C/O pattern — shop name comes after it\u001b[39m\n\u001b[31m    const coMatch = firstLine.match(/C\\\\/?[Oo]\\\\.?\\\\s+(.+)/);\u001b[39m\n\u001b[31m    if (coMatch) {\u001b[39m\n\u001b[31m      result.name = coMatch[1].trim();\u001b[39m\n\u001b[31m    } else {\u001b[39m\n\u001b[31m      // Check if there is a comma — second part might be the shop name\u001b[39m\n\u001b[31m      const parts = firstLine.split(',').map(p => p.trim());\u001b[39m\n\u001b[31m      if (parts.length >= 2 && parts[1].length > 3) {\u001b[39m\n\u001b[31m        // Heuristic: if second part looks like a business name (has uppercase or >5 chars)\u001b[39m\n\u001b[31m        const hasUpperOrLong = /[A-Z]{2,}/.test(parts[1]) || parts[1].length > 8;\u001b[39m\n\u001b[31m        result.name = hasUpperOrLong ? parts[1] : parts[0];\u001b[39m\n\u001b[31m      } else {\u001b[39m\n\u001b[31m        result.name = parts[0];\u001b[39m\n\u001b[31m      }\u001b[39m\n\u001b[31m    }\u001b[39m\n\u001b[31m  }·\u001b[39m\n\u001b[31m  // ── ADDRESS: lines between name and phone/pincode line ───────────────────\u001b[39m\n\u001b[31m  const phoneLineIdx = lines.findIndex(l => result.phone && l.includes(result.phone));\u001b[39m\n\u001b[31m  const addrLines = lines.filter((l, i) => {\u001b[39m\n\u001b[31m    if (i === 0) return false;\u001b[39m\n\u001b[31m    if (i === phoneLineIdx) return false;\u001b[39m\n\u001b[31m    // Exclude lines that are just the phone number\u001b[39m\n\u001b[31m    if (/^[6-9]\\\\d{9}$/.test(l.replace(/\\\\s/g,''))) return false;\u001b[39m\n\u001b[31m    return true;\u001b[39m\n\u001b[31m  });\u001b[39m\n\u001b[31m  result.address = addrLines.join(', ').replace(/,\\\\s*,/g, ',').trim();·\u001b[39m\n\u001b[31m  // ── CONFIDENCE: simple score based on what we got ────────────────────────\u001b[39m\n\u001b[31m  let conf = 0;\u001b[39m\n\u001b[31m  if (result.name)    conf += 30;\u001b[39m\n\u001b[31m  if (result.city)    conf += 25;\u001b[39m\n\u001b[31m  if (result.pincode) conf += 25;\u001b[39m\n\u001b[31m  if (result.phone)   conf += 20;\u001b[39m\n\u001b[31m  result.confidence = conf;·\u001b[39m\n\u001b[31m  return result;\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// ── AI ADDRESS PARSER (Ollama fallback) ──────────────────────────────────\u001b[39m\n\u001b[31m// Fetches past corrections from localStorage to improve prompt accuracy\u001b[39m\n\u001b[31mfunction getPastCorrections() {\u001b[39m\n\u001b[31m  try {\u001b[39m\n\u001b[31m    const log = JSON.parse(localStorage.getItem('360lm_parse_corrections') || '[]');\u001b[39m\n\u001b[31m    return log.slice(-10); // last 10 corrections as examples\u001b[39m\n\u001b[31m  } catch(e) { return []; }\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction saveCorrectionLocal(entry) {\u001b[39m\n\u001b[31m  try {\u001b[39m\n\u001b[31m    const log = JSON.parse(localStorage.getItem('360lm_parse_corrections') || '[]');\u001b[39m\n\u001b[31m    log.push(entry);\u001b[39m\n\u001b[31m    // Keep only last 50\u001b[39m\n\u001b[31m    localStorage.setItem('360lm_parse_corrections', JSON.stringify(log.slice(-50)));\u001b[39m\n\u001b[31m  } catch(e) {}\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31masync function parseAddressWithOllama(raw) {\u001b[39m\n\u001b[31m  // Calls the server-side /dispatch-ai/address proxy (avoids CORS + port 11434 issues)\u001b[39m\n\u001b[31m  try {\u001b[39m\n\u001b[31m    const resp = await fetch('/dispatch-ai/address', {\u001b[39m\n\u001b[31m      method: 'POST',\u001b[39m\n\u001b[31m      headers: { 'Content-Type': 'application/json' },\u001b[39m\n\u001b[31m      body: JSON.stringify({ text: raw }),\u001b[39m\n\u001b[31m      signal: AbortSignal.timeout(120000)\u001b[39m\n\u001b[31m    });\u001b[39m\n\u001b[31m    const data = await resp.json();\u001b[39m\n\u001b[31m    if (data.success) return data;\u001b[39m\n\u001b[31m  } catch(e) {}\u001b[39m\n\u001b[31m  return null;\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31masync function parseAddressCell(raw) {\u001b[39m\n\u001b[31m  // Step 1: regex — always runs first, no network call\u001b[39m\n\u001b[31m  const regex = parseAddressRegex(raw);\u001b[39m\n\u001b[31m  if (regex.name) return { ...regex, source: 'regex' };·\u001b[39m\n\u001b[31m  // Step 2: Ollama only if regex got nothing AND Ollama is configured\u001b[39m\n\u001b[31m  // Uses a 3-second timeout to prevent browser hang\u001b[39m\n\u001b[31m  if (window.appConfig.ocr_mode !== 'transformers') {\u001b[39m\n\u001b[31m    try {\u001b[39m\n\u001b[31m      const ai = await Promise.race([\u001b[39m\n\u001b[31m        parseAddressWithOllama(raw),\u001b[39m\n\u001b[31m        new Promise((_, reject) => setTimeout(() => reject(new Error('timeout')), 120000))\u001b[39m\n\u001b[31m      ]);\u001b[39m\n\u001b[31m      if (ai && ai.name) return { ...ai, source: 'ollama' };\u001b[39m\n\u001b[31m    } catch(e) { /* timeout or error — continue to fallback */ }\u001b[39m\n\u001b[31m  }·\u001b[39m\n\u001b[31m  // Step 3: fallback — first line as name\u001b[39m\n\u001b[31m  const lines = raw.toString().split('\\\\n').map(l=>l.trim()).filter(Boolean);\u001b[39m\n\u001b[31m  return { name: lines[0]||raw.slice(0,40), address: raw, city:'', pincode:'', phone:'', source:'fallback' };\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31masync function applyMapping() {\u001b[39m\n\u001b[31m  const useAI   = document.getElementById('useAIParse')?.checked ?? false;\u001b[39m\n\u001b[31m  const comboSel = document.getElementById('comboColSel');\u001b[39m\n\u001b[31m  const comboIdx = comboSel ? parseInt(comboSel.value) : -1;\u001b[39m\n\u001b[31m  const docketIdx = parseInt(document.getElementById('docketColSel')?.value ?? -1);·\u001b[39m\n\u001b[31m  // Build manual mapping as fallback\u001b[39m\n\u001b[31m  const mapping = {};\u001b[39m\n\u001b[31m  MAPPING_FIELDS.forEach(f => {\u001b[39m\n\u001b[31m    const sel = document.getElementById('mapsel_' + f.key);\u001b[39m\n\u001b[31m    mapping[f.key] = sel ? parseInt(sel.value) : -1;\u001b[39m\n\u001b[31m  });\u001b[39m\n\u001b[31m  // Override docket from dedicated selector\u001b[39m\n\u001b[31m  mapping.docket = docketIdx;·\u001b[39m\n\u001b[31m  // Determine if we have a valid base for stores\u001b[39m\n\u001b[31m  const nameIdx = useAI && comboIdx >= 0 ? comboIdx : mapping.name;\u001b[39m\n\u001b[31m  if (nameIdx === -1 && !useAI) { showToast('Store Name column is required', 'err'); return; }·\u001b[39m\n\u001b[31m  // Save mapping\u001b[39m\n\u001b[31m  try { localStorage.setItem(getMappingKey(), JSON.stringify({...mapping, _useAI: useAI, _comboIdx: comboIdx})); } catch(e) {}·\u001b[39m\n\u001b[31m  if (useAI && comboIdx >= 0) {\u001b[39m\n\u001b[31m    // AI path — parse each combined cell\u001b[39m\n\u001b[31m    showToast('AI parsing addresses...', 'info');\u001b[39m\n\u001b[31m    showOCRWorking(true, `Parsing 0/${_rawRows.length} addresses...`);\u001b[39m\n\u001b[31m    stores = [];\u001b[39m\n\u001b[31m    for (let i = 0; i < _rawRows.length; i++) {\u001b[39m\n\u001b[31m      const r = _rawRows[i];\u001b[39m\n\u001b[31m      const raw = (r[comboIdx] || '').toString().trim();\u001b[39m\n\u001b[31m      if (!raw) continue;\u001b[39m\n\u001b[31m      showOCRWorking(true, `Parsing ${i+1}/${_rawRows.length} — ${raw.slice(0,30)}...`);\u001b[39m\n\u001b[31m      // Yield to browser every row to prevent UI freeze\u001b[39m\n\u001b[31m      await new Promise(r => setTimeout(r, 0));\u001b[39m\n\u001b[31m      const parsed = await parseAddressCell(raw);\u001b[39m\n\u001b[31m      stores.push({\u001b[39m\n\u001b[31m        id: i,\u001b[39m\n\u001b[31m        name:    parsed.name    || raw.slice(0,40),\u001b[39m\n\u001b[31m        city:    parsed.city    || '',\u001b[39m\n\u001b[31m        phone:   parsed.phone   || '',\u001b[39m\n\u001b[31m        address: parsed.address || raw,\u001b[39m\n\u001b[31m        pincode: parsed.pincode || '',\u001b[39m\n\u001b[31m        docket:  docketIdx >= 0 ? (r[docketIdx]||'').toString().trim() : '',\u001b[39m\n\u001b[31m        _parseSource: parsed.source,\u001b[39m\n\u001b[31m        _confidence:  parsed.confidence || 0,\u001b[39m\n\u001b[31m        _rawCell:     raw,\u001b[39m\n\u001b[31m        _origName:    parsed.name || '',\u001b[39m\n\u001b[31m        _origCity:    parsed.city || '',\u001b[39m\n\u001b[31m        matched: false\u001b[39m\n\u001b[31m      });\u001b[39m\n\u001b[31m    }\u001b[39m\n\u001b[31m    showOCRWorking(false);\u001b[39m\n\u001b[31m  } else {\u001b[39m\n\u001b[31m    // Manual mapping path\u001b[39m\n\u001b[31m    stores = _rawRows\u001b[39m\n\u001b[31m      .filter(r => r[nameIdx] && (r[nameIdx]+'').trim())\u001b[39m\n\u001b[31m      .map((r, i) => ({\u001b[39m\n\u001b[31m        id: i,\u001b[39m\n\u001b[31m        name:    mapping.name    >= 0 ? (r[mapping.name]   ||'').toString().trim() : '',\u001b[39m\n\u001b[31m        city:    mapping.city    >= 0 ? (r[mapping.city]   ||'').toString().trim() : '',\u001b[39m\n\u001b[31m        phone:   mapping.phone   >= 0 ? (r[mapping.phone]  ||'').toString().trim() : '',\u001b[39m\n\u001b[31m        docket:  mapping.docket  >= 0 ? (r[mapping.docket] ||'').toString().trim() : '',\u001b[39m\n\u001b[31m        address: mapping.address >= 0 ? (r[mapping.address]||'').toString().trim() : '',\u001b[39m\n\u001b[31m        pincode: mapping.pincode >= 0 ? (r[mapping.pincode]||'').toString().trim() : '',\u001b[39m\n\u001b[31m        matched: false\u001b[39m\n\u001b[31m      }));\u001b[39m\n\u001b[31m  }·\u001b[39m\n\u001b[31m  document.getElementById('storeCount').textContent = `${stores.length} stores loaded`;·\u001b[39m\n\u001b[31m  if (useAI && comboIdx >= 0) {\u001b[39m\n\u001b[31m    // AI path — show row-by-row review panel before adding consignees\u001b[39m\n\u001b[31m    showAddressReviewPanel();\u001b[39m\n\u001b[31m  } else {\u001b[39m\n\u001b[31m    // Manual mapping path — go straight to store table + add panel\u001b[39m\n\u001b[31m    renderStoreTable(mapping.docket >= 0 || docketIdx >= 0);\u001b[39m\n\u001b[31m    const addPanel = document.getElementById('storeAddPanel');\u001b[39m\n\u001b[31m    const addBtn   = document.getElementById('storeAddBtn');\u001b[39m\n\u001b[31m    const addTitle = document.getElementById('storeAddTitle');\u001b[39m\n\u001b[31m    const hasDockets = stores.some(s => s.docket);\u001b[39m\n\u001b[31m    addTitle.textContent = hasDockets\u001b[39m\n\u001b[31m      ? `${stores.filter(s=>s.docket).length} stores with dockets — add as consignees?`\u001b[39m\n\u001b[31m      : `Add ${stores.length} stores as consignees?`;\u001b[39m\n\u001b[31m    addBtn.textContent = `✓ Add ${stores.length} as Consignees`;\u001b[39m\n\u001b[31m    addPanel.style.display = 'block';\u001b[39m\n\u001b[31m    showToast(`${stores.length} stores mapped ✓`, 'ok');\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31m// ADDRESS REVIEW PANEL\u001b[39m\n\u001b[31m// Row-by-row review of AI-parsed addresses before consignees created\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31mfunction showAddressReviewPanel() {\u001b[39m\n\u001b[31m  const pane = document.getElementById('addrReviewPane');\u001b[39m\n\u001b[31m  if (!pane) return;\u001b[39m\n\u001b[31m  renderAddressReviewRows();\u001b[39m\n\u001b[31m  pane.style.display = 'block';\u001b[39m\n\u001b[31m  pane.scrollTop = 0;\u001b[39m\n\u001b[31m  showToast(`Review ${stores.length} parsed addresses`, 'info');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction renderAddressReviewRows() {\u001b[39m\n\u001b[31m  const list = document.getElementById('addrReviewList');\u001b[39m\n\u001b[31m  const summary = document.getElementById('addrReviewSummary');\u001b[39m\n\u001b[31m  if (!list) return;·\u001b[39m\n\u001b[31m  const highConf  = stores.filter(s => (s._confidence||0) >= 80).length;\u001b[39m\n\u001b[31m  const medConf   = stores.filter(s => (s._confidence||0) >= 50 && (s._confidence||0) < 80).length;\u001b[39m\n\u001b[31m  const lowConf   = stores.filter(s => (s._confidence||0) < 50).length;·\u001b[39m\n\u001b[31m  summary.innerHTML = `\u001b[39m\n\u001b[31m    <div class=\\\"vs-item\\\"><div class=\\\"vs-val\\\" style=\\\"color:var(--t1);\\\">${stores.length}</div><div class=\\\"vs-lbl\\\">Total</div></div>\u001b[39m\n\u001b[31m    <div class=\\\"vs-item\\\"><div class=\\\"vs-val\\\" style=\\\"color:var(--grn);\\\">${highConf}</div><div class=\\\"vs-lbl\\\">High conf</div></div>\u001b[39m\n\u001b[31m    <div class=\\\"vs-item\\\"><div class=\\\"vs-val\\\" style=\\\"color:var(--ylw);\\\">${medConf}</div><div class=\\\"vs-lbl\\\">Review</div></div>\u001b[39m\n\u001b[31m    <div class=\\\"vs-item\\\"><div class=\\\"vs-val\\\" style=\\\"color:var(--red);\\\">${lowConf}</div><div class=\\\"vs-lbl\\\">Check</div></div>\u001b[39m\n\u001b[31m    <div class=\\\"vs-item\\\" style=\\\"margin-left:auto;\\\">\u001b[39m\n\u001b[31m      <button class=\\\"btn-sm primary\\\" onclick=\\\"approveAllAddresses()\\\" style=\\\"font-size:.7rem;\\\">✓ Approve All</button>\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m  `;·\u001b[39m\n\u001b[31m  list.innerHTML = stores.map((s, i) => {\u001b[39m\n\u001b[31m    const conf = s._confidence || 0;\u001b[39m\n\u001b[31m    const confClass = conf >= 80 ? 'vf-ok' : conf >= 50 ? 'vf-nodocket' : 'vf-dupe';\u001b[39m\n\u001b[31m    const confLabel = conf >= 80 ? `${conf}% ✓` : conf >= 50 ? `${conf}% review` : `${conf}% check`;\u001b[39m\n\u001b[31m    const rowBorder = conf >= 80 ? 'var(--grn)' : conf >= 50 ? 'var(--ylw)' : 'var(--red)';·\u001b[39m\n\u001b[31m    return `\u001b[39m\n\u001b[31m    <div class=\\\"verify-row\\\" id=\\\"arow_${i}\\\" style=\\\"border-left:3px solid ${rowBorder};\\\">\u001b[39m\n\u001b[31m      <div class=\\\"verify-row-hdr\\\">\u001b[39m\n\u001b[31m        <span class=\\\"verify-num\\\">#${i+1}</span>\u001b[39m\n\u001b[31m        <span class=\\\"verify-store\\\" style=\\\"font-size:.78rem;color:var(--t2);font-weight:400;\u001b[39m\n\u001b[31m          font-family:var(--fnt-mono);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;flex:1;\\\"\u001b[39m\n\u001b[31m          title=\\\"${(s._rawCell||'').replace(/\\\"/g,\\\"'\\\")}\\\">\u001b[39m\n\u001b[31m          ${(s._rawCell||'').slice(0,50).split('\\\\n').join(' ↵ ')}       <span class=\\\"verify-flag ${confClass}\\\" style=\\\"flex-shrink:0;margin-right:6px;\\\">${confLabel} · ${s._parseSource||'?'}</span>\u001b[39m\n\u001b[31m        <button onclick=\\\"deleteReviewRow(${i})\\\"\u001b[39m\n\u001b[31m          style=\\\"background:none;border:1px solid rgba(239,68,68,.4);color:var(--red);\u001b[39m\n\u001b[31m          font-size:.65rem;padding:2px 8px;border-radius:6px;cursor:pointer;\u001b[39m\n\u001b[31m          font-family:var(--fnt-mono);flex-shrink:0;transition:background .2s;\\\"\u001b[39m\n\u001b[31m          onmouseover=\\\"this.style.background='rgba(239,68,68,.15)'\\\"\u001b[39m\n\u001b[31m          onmouseout=\\\"this.style.background='none'\\\">✕ Remove</button>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      <div class=\\\"verify-fields\\\" style=\\\"grid-template-columns:1fr 1fr 1fr 1fr 1fr;\\\">\u001b[39m\n\u001b[31m        <div class=\\\"verify-field\\\">\u001b[39m\n\u001b[31m          <label>Shop / Name <span style=\\\"color:var(--acc);\\\">*</span></label>\u001b[39m\n\u001b[31m          <input type=\\\"text\\\" value=\\\"${s.name||''}\\\" id=\\\"ar_name_${i}\\\"\u001b[39m\n\u001b[31m            oninput=\\\"updateAddrField(${i},'name',this)\\\"\u001b[39m\n\u001b[31m            style=\\\"${!s.name ? 'border-color:var(--red);' : ''}\\\">\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m        <div class=\\\"verify-field\\\">\u001b[39m\n\u001b[31m          <label>City</label>\u001b[39m\n\u001b[31m          <input type=\\\"text\\\" value=\\\"${s.city||''}\\\" id=\\\"ar_city_${i}\\\"\u001b[39m\n\u001b[31m            oninput=\\\"updateAddrField(${i},'city',this)\\\"\u001b[39m\n\u001b[31m            style=\\\"${!s.city ? 'border-color:var(--ylw);' : ''}\\\">\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m        <div class=\\\"verify-field\\\">\u001b[39m\n\u001b[31m          <label>Docket / Courier No.</label>\u001b[39m\n\u001b[31m          <input type=\\\"text\\\" value=\\\"${s.docket||''}\\\" id=\\\"ar_docket_${i}\\\"\u001b[39m\n\u001b[31m            oninput=\\\"updateAddrField(${i},'docket',this)\\\"\u001b[39m\n\u001b[31m            style=\\\"font-family:var(--fnt-mono);${!s.docket?'border-color:var(--ylw);':'color:var(--acc);'}\\\">\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m        <div class=\\\"verify-field\\\">\u001b[39m\n\u001b[31m          <label>Phone</label>\u001b[39m\n\u001b[31m          <input type=\\\"tel\\\" value=\\\"${s.phone||''}\\\" id=\\\"ar_phone_${i}\\\"\u001b[39m\n\u001b[31m            oninput=\\\"updateAddrField(${i},'phone',this)\\\">\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m        <div class=\\\"verify-field\\\">\u001b[39m\n\u001b[31m          <label>Pincode</label>\u001b[39m\n\u001b[31m          <input type=\\\"text\\\" value=\\\"${s.pincode||''}\\\" id=\\\"ar_pin_${i}\\\"\u001b[39m\n\u001b[31m            oninput=\\\"updateAddrField(${i},'pincode',this)\\\">\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      <div style=\\\"font-size:.65rem;color:var(--t3);font-family:var(--fnt-mono);\u001b[39m\n\u001b[31m        margin-top:4px;padding:4px 6px;background:var(--bg);border-radius:4px;word-break:break-all;\\\">\u001b[39m\n\u001b[31m        📋 ${(s._rawCell||'').split('\\\\n').join(' | ')}\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m    </div>`;\u001b[39m\n\u001b[31m  }).join('');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction updateAddrField(i, field, input) {\u001b[39m\n\u001b[31m  if (!stores[i]) return;\u001b[39m\n\u001b[31m  stores[i][field] = input.value.trim();\u001b[39m\n\u001b[31m  // Recompute confidence\u001b[39m\n\u001b[31m  let conf = 0;\u001b[39m\n\u001b[31m  if (stores[i].name)    conf += 30;\u001b[39m\n\u001b[31m  if (stores[i].city)    conf += 25;\u001b[39m\n\u001b[31m  if (stores[i].pincode) conf += 25;\u001b[39m\n\u001b[31m  if (stores[i].phone)   conf += 20;\u001b[39m\n\u001b[31m  stores[i]._confidence = conf;\u001b[39m\n\u001b[31m  input.classList.toggle('changed', true);\u001b[39m\n\u001b[31m  // Update border color on row\u001b[39m\n\u001b[31m  const row = document.getElementById('arow_' + i);\u001b[39m\n\u001b[31m  if (row) row.style.borderLeftColor = conf >= 80 ? 'var(--grn)' : conf >= 50 ? 'var(--ylw)' : 'var(--red)';\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction deleteReviewRow(i) {\u001b[39m\n\u001b[31m  // Mark as deleted — splice from stores array and re-render\u001b[39m\n\u001b[31m  stores.splice(i, 1);\u001b[39m\n\u001b[31m  // Re-assign IDs\u001b[39m\n\u001b[31m  stores.forEach((s, idx) => { s.id = idx; });\u001b[39m\n\u001b[31m  renderAddressReviewRows();\u001b[39m\n\u001b[31m  document.getElementById('storeCount').textContent = `${stores.length} stores loaded`;\u001b[39m\n\u001b[31m  showToast(`Row removed — ${stores.length} remaining`, 'info');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction backToMapping() {\u001b[39m\n\u001b[31m  // Hide review pane — store edits are preserved in stores[] array\u001b[39m\n\u001b[31m  document.getElementById('addrReviewPane').style.display = 'none';\u001b[39m\n\u001b[31m  // Show mapping UI again with current file\u001b[39m\n\u001b[31m  document.getElementById('storeTableArea').style.display = 'block';\u001b[39m\n\u001b[31m  document.getElementById('storeColInfo').style.display = 'block';\u001b[39m\n\u001b[31m  showToast('Your edits are preserved — re-apply mapping to reload', 'info');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction approveAllAddresses() {\u001b[39m\n\u001b[31m  stores.forEach((s, i) => { s._reviewed = true; });\u001b[39m\n\u001b[31m  document.getElementById('addrReviewPane').style.display = 'none';\u001b[39m\n\u001b[31m  applyReviewedStores();\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction confirmAddressReview() {\u001b[39m\n\u001b[31m  // Collect any pending edits, log corrections, close panel\u001b[39m\n\u001b[31m  const corrections = [];\u001b[39m\n\u001b[31m  stores.forEach((s, i) => {\u001b[39m\n\u001b[31m    const origName = s._origName || '';\u001b[39m\n\u001b[31m    const origCity = s._origCity || '';\u001b[39m\n\u001b[31m    if ((origName && origName !== s.name) || (origCity && origCity !== s.city)) {\u001b[39m\n\u001b[31m      corrections.push({ raw: s._rawCell||'', name: s.name, city: s.city, pincode: s.pincode });\u001b[39m\n\u001b[31m    }\u001b[39m\n\u001b[31m  });\u001b[39m\n\u001b[31m  // Save corrections locally for future Ollama prompts\u001b[39m\n\u001b[31m  corrections.forEach(c => saveCorrectionLocal(c));\u001b[39m\n\u001b[31m  // Log to Sheets asynchronously (fire and forget)\u001b[39m\n\u001b[31m  if (corrections.length > 0 && window.appConfig.webhook_url) {\u001b[39m\n\u001b[31m    logParseCorrectionsToSheets(corrections);\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m  document.getElementById('addrReviewPane').style.display = 'none';\u001b[39m\n\u001b[31m  applyReviewedStores();\u001b[39m\n\u001b[31m  showToast(`${corrections.length > 0 ? corrections.length + ' corrections logged · ' : ''}${stores.length} stores ready ✓`, 'ok');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// Clean street address: remove city, pincode, phone if they appear in address field\u001b[39m\n\u001b[31mfunction cleanAddress(addr, city, pincode, phone) {\u001b[39m\n\u001b[31m  if (!addr) return addr;\u001b[39m\n\u001b[31m  let s = addr;\u001b[39m\n\u001b[31m  // Remove phone\u001b[39m\n\u001b[31m  if (phone) s = s.replace(new RegExp(phone.replace(/[.*+?^${}()|[\\\\]\\\\\\\\]/g,'\\\\\\\\$&'), 'g'), '').trim();\u001b[39m\n\u001b[31m  // Remove pincode\u001b[39m\n\u001b[31m  if (pincode) s = s.replace(new RegExp('\\\\\\\\b' + pincode + '\\\\\\\\b', 'g'), '').trim();\u001b[39m\n\u001b[31m  // Remove city (case-insensitive)\u001b[39m\n\u001b[31m  if (city) s = s.replace(new RegExp('\\\\\\\\b' + city.replace(/[.*+?^${}()|[\\\\]\\\\\\\\]/g,'\\\\\\\\$&') + '\\\\\\\\b', 'ig'), '').trim();\u001b[39m\n\u001b[31m  // Remove state names and trailing noise\u001b[39m\n\u001b[31m  const noise = /\\\\b(rajasthan|gujarat|maharashtra|punjab|haryana|uttar pradesh|delhi|raj\\\\.)\\\\b/ig;\u001b[39m\n\u001b[31m  s = s.replace(noise, '');\u001b[39m\n\u001b[31m  // Remove \\\"Mobile :\\\", \\\"Phone :\\\", \\\"M.No.\\\" prefixes and dangling numbers\u001b[39m\n\u001b[31m  s = s.replace(/(?:Mobile|Phone|Mob|M\\\\.No\\\\.?|Ph\\\\.?)\\\\s*[:\\\\-]?\\\\s*/ig, '');\u001b[39m\n\u001b[31m  // Clean up multiple commas/spaces\u001b[39m\n\u001b[31m  s = s.replace(/,\\\\s*,/g, ',').replace(/\\\\s{2,}/g, ' ').replace(/^[\\\\s,]+|[\\\\s,]+$/g, '');\u001b[39m\n\u001b[31m  return s;\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction applyReviewedStores() {\u001b[39m\n\u001b[31m  // Clean address fields before loading stores\u001b[39m\n\u001b[31m  stores.forEach(s => {\u001b[39m\n\u001b[31m    s.address = cleanAddress(s.address, s.city, s.pincode, s.phone);\u001b[39m\n\u001b[31m  });\u001b[39m\n\u001b[31m  renderStoreTable(stores.some(s => s.docket));\u001b[39m\n\u001b[31m  const addPanel = document.getElementById('storeAddPanel');\u001b[39m\n\u001b[31m  const addTitle = document.getElementById('storeAddTitle');\u001b[39m\n\u001b[31m  const addBtn   = document.getElementById('storeAddBtn');\u001b[39m\n\u001b[31m  const hasDockets = stores.some(s => s.docket);\u001b[39m\n\u001b[31m  addTitle.textContent = hasDockets\u001b[39m\n\u001b[31m    ? `${stores.filter(s=>s.docket).length} stores with dockets — add as consignees?`\u001b[39m\n\u001b[31m    : `Add ${stores.length} stores as consignees?`;\u001b[39m\n\u001b[31m  addBtn.textContent = `✓ Add ${stores.length} as Consignees`;\u001b[39m\n\u001b[31m  addPanel.style.display = 'block';\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31masync function logParseCorrectionsToSheets(corrections) {\u001b[39m\n\u001b[31m  const url = window.appConfig.webhook_url;\u001b[39m\n\u001b[31m  if (!url) return;\u001b[39m\n\u001b[31m  try {\u001b[39m\n\u001b[31m    await fetch(url, {\u001b[39m\n\u001b[31m      method: 'POST', mode: 'no-cors',\u001b[39m\n\u001b[31m      headers: { 'Content-Type': 'application/json' },\u001b[39m\n\u001b[31m      body: JSON.stringify({\u001b[39m\n\u001b[31m        type: 'parse_correction',\u001b[39m\n\u001b[31m        corrections,\u001b[39m\n\u001b[31m        colFingerprint: getMappingKey(),\u001b[39m\n\u001b[31m        timestamp: new Date().toISOString()\u001b[39m\n\u001b[31m      })\u001b[39m\n\u001b[31m    });\u001b[39m\n\u001b[31m  } catch(e) {}\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction addStoresToConsignees() {\u001b[39m\n\u001b[31m  const consList = document.getElementById('consList');\u001b[39m\n\u001b[31m  consList.innerHTML = '';\u001b[39m\n\u001b[31m  stores.slice(0, 50).forEach(s => addConsignee(s));\u001b[39m\n\u001b[31m  document.getElementById('storeAddPanel').style.display = 'none';\u001b[39m\n\u001b[31m  showToast(`${Math.min(stores.length, 50)} consignees added ✓`, 'ok');\u001b[39m\n\u001b[31m  if (stores.some(s => s.docket)) setTimeout(() => autoCreateDispatchFromStoreList(), 600);\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction promptAddFromStores() { /* replaced by inline panel */ }··\u001b[39m\n\u001b[31mfunction findCol(headers, keywords) {\u001b[39m\n\u001b[31m  for (let i=0;i<headers.length;i++) {\u001b[39m\n\u001b[31m    const h = (headers[i]||'').toString().toLowerCase();\u001b[39m\n\u001b[31m    if (keywords.some(k => h.includes(k))) return i;\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m  return -1;\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction renderStoreTable(hasDocket) {\u001b[39m\n\u001b[31m  const t = document.getElementById('storeTable');\u001b[39m\n\u001b[31m  const cols = ['#','Store Name','City','Phone'];\u001b[39m\n\u001b[31m  if (hasDocket) cols.push('Docket','Status');\u001b[39m\n\u001b[31m  t.innerHTML = `<thead><tr>${cols.map(c=>`<th>${c}</th>`).join('')}</tr></thead>\u001b[39m\n\u001b[31m  <tbody>${stores.map((s,i) => {\u001b[39m\n\u001b[31m    const statusClass = s.docket ? (s.status === 'delivered' ? 'matched' : 'unmatched') : '';\u001b[39m\n\u001b[31m    const docketCell = hasDocket ? `<td class=\\\"${statusClass}\\\" style=\\\"font-family:var(--fnt-mono);\\\">${s.docket||'—'}</td>` : '';\u001b[39m\n\u001b[31m    const statusCell = hasDocket ? `<td>${s.status ? `<span class=\\\"status-pill sp-${s.status}\\\">${s.status}</span>` : '—'}</td>` : '';\u001b[39m\n\u001b[31m    return `<tr>\u001b[39m\n\u001b[31m      <td>${i+1}</td>\u001b[39m\n\u001b[31m      <td>${s.name}</td>\u001b[39m\n\u001b[31m      <td>${s.city}</td>\u001b[39m\n\u001b[31m      <td style=\\\"font-family:var(--fnt-mono);\\\">${s.phone||'—'}</td>\u001b[39m\n\u001b[31m      ${docketCell}${statusCell}\u001b[39m\n\u001b[31m    </tr>`;\u001b[39m\n\u001b[31m  }).join('')}</tbody>`;\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction clearStores() {\u001b[39m\n\u001b[31m  stores = [];\u001b[39m\n\u001b[31m  document.getElementById('storeTableArea').style.display = 'none';\u001b[39m\n\u001b[31m  document.getElementById('storeFile').value = '';\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction autoCreateDispatchFromStoreList() {\u001b[39m\n\u001b[31m  // Create/update dispatch with all stores that have dockets\u001b[39m\n\u001b[31m  const courier = document.getElementById('n_courier').value || 'SkyKing';\u001b[39m\n\u001b[31m  const storesWithDockets = stores.filter(s => s.docket);\u001b[39m\n\u001b[31m  if (!storesWithDockets.length) return;·\u001b[39m\n\u001b[31m  // Clear and rebuild consignee list from store data\u001b[39m\n\u001b[31m  const consList = document.getElementById('consList');\u001b[39m\n\u001b[31m  consList.innerHTML = '';\u001b[39m\n\u001b[31m  storesWithDockets.forEach(s => {\u001b[39m\n\u001b[31m    addConsignee(s);\u001b[39m\n\u001b[31m  });\u001b[39m\n\u001b[31m  showPane('new');\u001b[39m\n\u001b[31m  showToast(`${storesWithDockets.length} consignees added from store list`, 'ok');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction promptAddFromStores() {\u001b[39m\n\u001b[31m  // Replaced by inline storeAddPanel UI — no-op kept for safety\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction autoMatchDockets() {\u001b[39m\n\u001b[31m  // Match OCR dockets to stores by order\u001b[39m\n\u001b[31m  if (!slipOCRData.length) { showToast('Upload docket slip images first', 'err'); return; }\u001b[39m\n\u001b[31m  let matched = 0;\u001b[39m\n\u001b[31m  slipOCRData.forEach((slip, i) => {\u001b[39m\n\u001b[31m    if (i < stores.length && slip.docket) {\u001b[39m\n\u001b[31m      stores[i].docket = slip.docket;\u001b[39m\n\u001b[31m      matched++;\u001b[39m\n\u001b[31m    }\u001b[39m\n\u001b[31m  });\u001b[39m\n\u001b[31m  renderStoreTable(true);\u001b[39m\n\u001b[31m  showToast(`${matched} dockets matched to stores ✓`, 'ok');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31m// CONSIGNEES\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31mfunction addConsignee(storeData = null) {\u001b[39m\n\u001b[31m  const list = document.getElementById('consList');\u001b[39m\n\u001b[31m  const idx = list.children.length;\u001b[39m\n\u001b[31m  const card = document.createElement('div');\u001b[39m\n\u001b[31m  card.className = 'cons-card';\u001b[39m\n\u001b[31m  card.dataset.idx = idx;\u001b[39m\n\u001b[31m  const s = storeData || {};\u001b[39m\n\u001b[31m  card.innerHTML = `\u001b[39m\n\u001b[31m    <div class=\\\"cons-hdr\\\">\u001b[39m\n\u001b[31m      <span class=\\\"cons-num\\\">CONSIGNEE #${idx+1}</span>\u001b[39m\n\u001b[31m      <button class=\\\"cons-del\\\" onclick=\\\"this.closest('.cons-card').remove();renumberCons()\\\">✕</button>\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m    <div class=\\\"row2\\\">\u001b[39m\n\u001b[31m      <div class=\\\"f\\\">\u001b[39m\n\u001b[31m        <label>Store / Name <span class=\\\"req\\\">*</span></label>\u001b[39m\n\u001b[31m        <input type=\\\"text\\\" class=\\\"cons-name\\\" placeholder=\\\"Store name\\\" value=\\\"${s.name||''}\\\">\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      <div class=\\\"f\\\">\u001b[39m\n\u001b[31m        <label>City <span class=\\\"req\\\">*</span></label>\u001b[39m\n\u001b[31m        <input type=\\\"text\\\" class=\\\"cons-city\\\" placeholder=\\\"City\\\" value=\\\"${s.city||''}\\\">\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m    <div class=\\\"f\\\">\u001b[39m\n\u001b[31m      <label>Address</label>\u001b[39m\n\u001b[31m      <input type=\\\"text\\\" class=\\\"cons-addr\\\" placeholder=\\\"Full address\\\" value=\\\"${s.address||''}\\\">\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m    <div class=\\\"row2\\\">\u001b[39m\n\u001b[31m      <div class=\\\"f\\\">\u001b[39m\n\u001b[31m        <label>Phone</label>\u001b[39m\n\u001b[31m        <input type=\\\"tel\\\" class=\\\"cons-phone\\\" placeholder=\\\"Mobile\\\" value=\\\"${s.phone||''}\\\">\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      <div class=\\\"f\\\">\u001b[39m\n\u001b[31m        <label>Pincode</label>\u001b[39m\n\u001b[31m        <input type=\\\"text\\\" class=\\\"cons-pin\\\" placeholder=\\\"6-digit\\\" maxlength=\\\"6\\\">\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m    <div class=\\\"f\\\">\u001b[39m\n\u001b[31m      <label>Material / Contents <span class=\\\"req\\\">*</span></label>\u001b[39m\n\u001b[31m      <input type=\\\"text\\\" class=\\\"cons-material\\\" placeholder=\\\"e.g. Standees, Posters\\\">\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m    <div class=\\\"row2\\\">\u001b[39m\n\u001b[31m      <div class=\\\"f\\\">\u001b[39m\n\u001b[31m        <label>Qty</label>\u001b[39m\n\u001b[31m        <input type=\\\"number\\\" class=\\\"cons-qty\\\" placeholder=\\\"0\\\" min=\\\"0\\\">\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      <div class=\\\"f\\\">\u001b[39m\n\u001b[31m        <label>Docket No.</label>\u001b[39m\n\u001b[31m        <input type=\\\"text\\\" class=\\\"cons-docket-input\\\" placeholder=\\\"Docket #\\\"\u001b[39m\n\u001b[31m          style=\\\"font-family:var(--fnt-mono);\\\" value=\\\"${s.docket||''}\\\"\u001b[39m\n\u001b[31m          oninput=\\\"onDocketInput(this)\\\">\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m    ${s.docket ? `<div class=\\\"cons-docket-linked\\\"><span>🔗</span><span class=\\\"dkt\\\">${s.docket}</span></div>` : ''}\u001b[39m\n\u001b[31m  `;\u001b[39m\n\u001b[31m  list.appendChild(card);\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction onDocketInput(input) {\u001b[39m\n\u001b[31m  const card = input.closest('.cons-card');\u001b[39m\n\u001b[31m  let linked = card.querySelector('.cons-docket-linked');\u001b[39m\n\u001b[31m  if (input.value.trim()) {\u001b[39m\n\u001b[31m    if (!linked) {\u001b[39m\n\u001b[31m      linked = document.createElement('div');\u001b[39m\n\u001b[31m      linked.className = 'cons-docket-linked';\u001b[39m\n\u001b[31m      card.appendChild(linked);\u001b[39m\n\u001b[31m    }\u001b[39m\n\u001b[31m    linked.innerHTML = `<span>🔗</span><span class=\\\"dkt\\\">${input.value}</span>`;\u001b[39m\n\u001b[31m  } else if (linked) {\u001b[39m\n\u001b[31m    linked.remove();\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction showLinkedDocket(card, docket) {\u001b[39m\n\u001b[31m  const input = card.querySelector('.cons-docket-input');\u001b[39m\n\u001b[31m  if (input) { input.value = docket; onDocketInput(input); }\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction renumberCons() {\u001b[39m\n\u001b[31m  document.querySelectorAll('#consList .cons-card').forEach((c,i) => {\u001b[39m\n\u001b[31m    c.querySelector('.cons-num').textContent = `CONSIGNEE #${i+1}`;\u001b[39m\n\u001b[31m    c.dataset.idx = i;\u001b[39m\n\u001b[31m  });\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31m// JOB REFERENCE\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31mfunction onClientChange() {\u001b[39m\n\u001b[31m  const val = document.getElementById('n_client').value;\u001b[39m\n\u001b[31m  document.getElementById('customClientField').style.display = val === '__c' ? 'block' : 'none';\u001b[39m\n\u001b[31m  updatePill();\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31mfunction updatePill() {\u001b[39m\n\u001b[31m  const client = document.getElementById('n_client').value;\u001b[39m\n\u001b[31m  const job = document.getElementById('n_jobref').value.trim();\u001b[39m\n\u001b[31m  const cName = client === '__c' ? document.getElementById('n_customClient').value : client;\u001b[39m\n\u001b[31m  const pill = document.getElementById('jobPill');\u001b[39m\n\u001b[31m  if (cName && job) {\u001b[39m\n\u001b[31m    document.getElementById('jobPillTxt').textContent = `${cName} › ${job}`;\u001b[39m\n\u001b[31m    pill.style.display = 'inline-flex';\u001b[39m\n\u001b[31m  } else {\u001b[39m\n\u001b[31m    pill.style.display = 'none';\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31mfunction clearJob() {\u001b[39m\n\u001b[31m  document.getElementById('n_client').value = '';\u001b[39m\n\u001b[31m  document.getElementById('n_jobref').value = '';\u001b[39m\n\u001b[31m  document.getElementById('jobPill').style.display = 'none';\u001b[39m\n\u001b[31m  document.getElementById('customClientField').style.display = 'none';\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31m// GATHER / SAVE\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31mfunction gatherDispatch() {\u001b[39m\n\u001b[31m  const client = document.getElementById('n_client').value;\u001b[39m\n\u001b[31m  const clientName = client === '__c' ? document.getElementById('n_customClient').value : client;\u001b[39m\n\u001b[31m  const consignees = [];\u001b[39m\n\u001b[31m  document.querySelectorAll('#consList .cons-card').forEach(card => {\u001b[39m\n\u001b[31m    const docket = card.querySelector('.cons-docket-input')?.value.trim() || '';\u001b[39m\n\u001b[31m    consignees.push({\u001b[39m\n\u001b[31m      name: card.querySelector('.cons-name')?.value || '',\u001b[39m\n\u001b[31m      city: card.querySelector('.cons-city')?.value || '',\u001b[39m\n\u001b[31m      address: card.querySelector('.cons-addr')?.value || '',\u001b[39m\n\u001b[31m      phone: card.querySelector('.cons-phone')?.value || '',\u001b[39m\n\u001b[31m      pincode: card.querySelector('.cons-pin')?.value || '',\u001b[39m\n\u001b[31m      material: card.querySelector('.cons-material')?.value || '',\u001b[39m\n\u001b[31m      qty: card.querySelector('.cons-qty')?.value || '',\u001b[39m\n\u001b[31m      docket,\u001b[39m\n\u001b[31m      status: docket ? 'booked' : 'pending',\u001b[39m\n\u001b[31m      lastEvent: '',\u001b[39m\n\u001b[31m      lastUpdated: ''\u001b[39m\n\u001b[31m    });\u001b[39m\n\u001b[31m  });\u001b[39m\n\u001b[31m  return {\u001b[39m\n\u001b[31m    id: 'DSP-' + Date.now(),\u001b[39m\n\u001b[31m    mode: 'manual',\u001b[39m\n\u001b[31m    client: clientName,\u001b[39m\n\u001b[31m    jobref: document.getElementById('n_jobref').value,\u001b[39m\n\u001b[31m    courier: document.getElementById('n_courier').value,\u001b[39m\n\u001b[31m    dispatchDate: document.getElementById('n_date').value,\u001b[39m\n\u001b[31m    remarks: document.getElementById('n_remarks').value,\u001b[39m\n\u001b[31m    consignees,\u001b[39m\n\u001b[31m    slipCount: slipOCRData.length,\u001b[39m\n\u001b[31m    status: 'active',\u001b[39m\n\u001b[31m    createdAt: new Date().toISOString()\u001b[39m\n\u001b[31m  };\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction validate(d) {\u001b[39m\n\u001b[31m  if (!d.client) { showToast('Select a client', 'err'); return false; }\u001b[39m\n\u001b[31m  if (!d.courier) { showToast('Select courier', 'err'); return false; }\u001b[39m\n\u001b[31m  if (!d.consignees.length) { showToast('Add at least one consignee', 'err'); return false; }\u001b[39m\n\u001b[31m  for (let i = 0; i < d.consignees.length; i++) {\u001b[39m\n\u001b[31m    const c = d.consignees[i];\u001b[39m\n\u001b[31m    if (!c.name || !c.city) {\u001b[39m\n\u001b[31m      showToast(`Consignee #${i+1} (${c.name||'unnamed'}) — name & city required`, 'err');\u001b[39m\n\u001b[31m      // Scroll to and highlight the problem consignee card\u001b[39m\n\u001b[31m      const cards = document.querySelectorAll('#consList .cons-card');\u001b[39m\n\u001b[31m      if (cards[i]) {\u001b[39m\n\u001b[31m        cards[i].scrollIntoView({ behavior: 'smooth', block: 'center' });\u001b[39m\n\u001b[31m        cards[i].style.borderColor = 'var(--red)';\u001b[39m\n\u001b[31m        cards[i].style.boxShadow = '0 0 0 2px rgba(239,68,68,.3)';\u001b[39m\n\u001b[31m        setTimeout(() => {\u001b[39m\n\u001b[31m          if (cards[i]) { cards[i].style.borderColor = ''; cards[i].style.boxShadow = ''; }\u001b[39m\n\u001b[31m        }, 3000);\u001b[39m\n\u001b[31m      }\u001b[39m\n\u001b[31m      return false;\u001b[39m\n\u001b[31m    }\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m  return true;\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction saveDraft() {\u001b[39m\n\u001b[31m  const d = gatherDispatch();\u001b[39m\n\u001b[31m  d.status = 'draft';\u001b[39m\n\u001b[31m  dispatches.unshift(d);\u001b[39m\n\u001b[31m  saveData();\u001b[39m\n\u001b[31m  updateHistCount();\u001b[39m\n\u001b[31m  showToast('Draft saved ✓', 'ok');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// ── VERIFICATION SCREEN ─────────────────────────────────\u001b[39m\n\u001b[31m// Known docket lengths per courier for validation\u001b[39m\n\u001b[31mconst DOCKET_LENGTHS = {\u001b[39m\n\u001b[31m  'SkyKing': { min: 9, max: 12 },\u001b[39m\n\u001b[31m  'DTDC':    { min: 8, max: 15 },\u001b[39m\n\u001b[31m  'Delhivery': { min: 10, max: 18 },\u001b[39m\n\u001b[31m  'Blue Dart': { min: 10, max: 13 },\u001b[39m\n\u001b[31m  'FedEx':   { min: 10, max: 20 },\u001b[39m\n\u001b[31m  'Ecom Express': { min: 10, max: 14 },\u001b[39m\n\u001b[31m  'India Post': { min: 13, max: 22 },\u001b[39m\n\u001b[31m};·\u001b[39m\n\u001b[31mfunction finaliseDispatch() {\u001b[39m\n\u001b[31m  const d = gatherDispatch();\u001b[39m\n\u001b[31m  if (!validate(d)) return;\u001b[39m\n\u001b[31m  showVerifyScreen(d);\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction showVerifyScreen(d) {\u001b[39m\n\u001b[31m  const pane = document.getElementById('verifyPane');\u001b[39m\n\u001b[31m  const list = document.getElementById('verifyList');\u001b[39m\n\u001b[31m  const summary = document.getElementById('verifySummary');·\u001b[39m\n\u001b[31m  // Build flags\u001b[39m\n\u001b[31m  const docketsSeen = {};\u001b[39m\n\u001b[31m  const courier = d.courier;\u001b[39m\n\u001b[31m  const lenRule = DOCKET_LENGTHS[courier];·\u001b[39m\n\u001b[31m  d.consignees.forEach((c, i) => {\u001b[39m\n\u001b[31m    if (c.docket) docketsSeen[c.docket] = (docketsSeen[c.docket] || 0) + 1;\u001b[39m\n\u001b[31m  });·\u001b[39m\n\u001b[31m  const total     = d.consignees.length;\u001b[39m\n\u001b[31m  const withDock  = d.consignees.filter(c => c.docket).length;\u001b[39m\n\u001b[31m  const dupes     = Object.values(docketsSeen).filter(v => v > 1).length;\u001b[39m\n\u001b[31m  const noDock    = total - withDock;\u001b[39m\n\u001b[31m  const badLen    = lenRule ? d.consignees.filter(c => c.docket &&\u001b[39m\n\u001b[31m    (c.docket.length < lenRule.min || c.docket.length > lenRule.max)).length : 0;·\u001b[39m\n\u001b[31m  // Summary bar\u001b[39m\n\u001b[31m  summary.innerHTML = `\u001b[39m\n\u001b[31m    <div class=\\\"vs-item\\\"><div class=\\\"vs-val\\\" style=\\\"color:var(--t1);\\\">${total}</div><div class=\\\"vs-lbl\\\">Total</div></div>\u001b[39m\n\u001b[31m    <div class=\\\"vs-item\\\"><div class=\\\"vs-val\\\" style=\\\"color:var(--grn);\\\">${withDock}</div><div class=\\\"vs-lbl\\\">Docketed</div></div>\u001b[39m\n\u001b[31m    <div class=\\\"vs-item\\\"><div class=\\\"vs-val\\\" style=\\\"color:${noDock > 0 ? 'var(--ylw)' : 'var(--t3)'};\\\">${noDock}</div><div class=\\\"vs-lbl\\\">No Docket</div></div>\u001b[39m\n\u001b[31m    <div class=\\\"vs-item\\\"><div class=\\\"vs-val\\\" style=\\\"color:${dupes > 0 ? 'var(--red)' : 'var(--t3)'};\\\">${dupes}</div><div class=\\\"vs-lbl\\\">Duplicates</div></div>\u001b[39m\n\u001b[31m    ${badLen > 0 ? `<div class=\\\"vs-item\\\"><div class=\\\"vs-val\\\" style=\\\"color:var(--sky);\\\">${badLen}</div><div class=\\\"vs-lbl\\\">Short/Long</div></div>` : ''}\u001b[39m\n\u001b[31m    <div class=\\\"vs-item\\\" style=\\\"margin-left:auto;text-align:right;\\\">\u001b[39m\n\u001b[31m      <div class=\\\"vs-val\\\" style=\\\"font-size:.8rem;color:var(--t2);\\\">${d.courier||'—'}</div>\u001b[39m\n\u001b[31m      <div class=\\\"vs-lbl\\\">${d.client||'—'} ${d.jobref ? '› '+d.jobref : ''}</div>\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m  `;·\u001b[39m\n\u001b[31m  // Build consignee rows\u001b[39m\n\u001b[31m  list.innerHTML = d.consignees.map((c, i) => {\u001b[39m\n\u001b[31m    const isDupe   = c.docket && docketsSeen[c.docket] > 1;\u001b[39m\n\u001b[31m    const isNoDock = !c.docket;\u001b[39m\n\u001b[31m    const isShort  = lenRule && c.docket &&\u001b[39m\n\u001b[31m      (c.docket.length < lenRule.min || c.docket.length > lenRule.max);·\u001b[39m\n\u001b[31m    const rowClass = isDupe ? 'has-dupe' : (isNoDock ? 'no-docket' : 'ok');·\u001b[39m\n\u001b[31m    // Find matching OCR slip for thumbnail\u001b[39m\n\u001b[31m    const ocrMatch = slipOCRData[i];\u001b[39m\n\u001b[31m    const thumbHtml = ocrMatch?.imageData\u001b[39m\n\u001b[31m      ? `<img class=\\\"verify-thumb\\\" src=\\\"${ocrMatch.imageData}\\\" onclick=\\\"showSlipFull(${i})\\\" title=\\\"View slip\\\">`\u001b[39m\n\u001b[31m      : '';·\u001b[39m\n\u001b[31m    // Flags\u001b[39m\n\u001b[31m    let flags = '';\u001b[39m\n\u001b[31m    if (isDupe)   flags += `<span class=\\\"verify-flag vf-dupe\\\">⚠ Duplicate</span> `;\u001b[39m\n\u001b[31m    if (isNoDock) flags += `<span class=\\\"verify-flag vf-nodocket\\\">No Docket</span> `;\u001b[39m\n\u001b[31m    if (isShort)  flags += `<span class=\\\"verify-flag vf-short\\\">${c.docket.length} digits</span> `;\u001b[39m\n\u001b[31m    if (!isDupe && !isNoDock && !isShort) flags = `<span class=\\\"verify-flag vf-ok\\\">✓ OK</span>`;·\u001b[39m\n\u001b[31m    // OCR confidence note\u001b[39m\n\u001b[31m    const confNote = ocrMatch && ocrMatch.confidence\u001b[39m\n\u001b[31m      ? `<div class=\\\"ocr-correction-note\\\">OCR confidence: ${Math.round(ocrMatch.confidence*100)}% via ${ocrMatch.source||'—'}</div>` : '';·\u001b[39m\n\u001b[31m    // WhatsApp URL\u001b[39m\n\u001b[31m    const waMsg = encodeURIComponent(\u001b[39m\n\u001b[31m      `Dear ${c.name}, your material has been dispatched via ${d.courier}. Docket: ${c.docket||'TBD'}. Track: ${(COURIER_MASTER[d.courier]?.trackUrl||'').replace('{docket}', c.docket||'')}`\u001b[39m\n\u001b[31m    );\u001b[39m\n\u001b[31m    const waUrl = c.phone ? `https://wa.me/91${c.phone.replace(/\\\\D/g,'')}?text=${waMsg}` : '';·\u001b[39m\n\u001b[31m    return `\u001b[39m\n\u001b[31m    <div class=\\\"verify-row ${rowClass}\\\" id=\\\"vrow_${i}\\\">\u001b[39m\n\u001b[31m      <div class=\\\"verify-row-hdr\\\">\u001b[39m\n\u001b[31m        <span class=\\\"verify-num\\\">#${i+1}</span>\u001b[39m\n\u001b[31m        <span class=\\\"verify-store\\\">${c.name||'—'}</span>\u001b[39m\n\u001b[31m        ${flags}\u001b[39m\n\u001b[31m        ${thumbHtml}\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      <div class=\\\"verify-fields\\\" style=\\\"grid-template-columns:1fr 1fr 1fr;\\\">\u001b[39m\n\u001b[31m        <div class=\\\"verify-field\\\">\u001b[39m\n\u001b[31m          <label>Docket No. <span style=\\\"color:var(--acc);\\\">*</span></label>\u001b[39m\n\u001b[31m          <input type=\\\"text\\\" value=\\\"${c.docket||''}\\\" oninput=\\\"updateVerifyField(${i},'docket',this)\\\"\u001b[39m\n\u001b[31m            style=\\\"font-family:var(--fnt-mono);${isDupe||isShort?'border-color:var(--red);':!c.docket?'border-color:var(--ylw);':'color:var(--acc);'}\\\">\u001b[39m\n\u001b[31m          ${confNote}\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m        <div class=\\\"verify-field\\\">\u001b[39m\n\u001b[31m          <label>City</label>\u001b[39m\n\u001b[31m          <input type=\\\"text\\\" value=\\\"${c.city||''}\\\" oninput=\\\"updateVerifyField(${i},'city',this)\\\"\u001b[39m\n\u001b[31m            style=\\\"${!c.city ? 'border-color:var(--ylw);' : ''}\\\">\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m        <div class=\\\"verify-field\\\">\u001b[39m\n\u001b[31m          <label>Pincode</label>\u001b[39m\n\u001b[31m          <input type=\\\"text\\\" value=\\\"${c.pincode||''}\\\" oninput=\\\"updateVerifyField(${i},'pincode',this)\\\"\u001b[39m\n\u001b[31m            style=\\\"font-family:var(--fnt-mono);\\\">\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m        <div class=\\\"verify-field\\\">\u001b[39m\n\u001b[31m          <label>Phone</label>\u001b[39m\n\u001b[31m          <input type=\\\"tel\\\" value=\\\"${c.phone||''}\\\" oninput=\\\"updateVerifyField(${i},'phone',this)\\\">\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m        <div class=\\\"verify-field\\\">\u001b[39m\n\u001b[31m          <label>Material / Contents</label>\u001b[39m\n\u001b[31m          <input type=\\\"text\\\" value=\\\"${c.material||''}\\\" oninput=\\\"updateVerifyField(${i},'material',this)\\\">\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m        <div class=\\\"verify-field\\\">\u001b[39m\n\u001b[31m          <label>Street Address</label>\u001b[39m\n\u001b[31m          <input type=\\\"text\\\" value=\\\"${c.address||''}\\\" oninput=\\\"updateVerifyField(${i},'address',this)\\\"\u001b[39m\n\u001b[31m            placeholder=\\\"Street / locality only\\\">\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      <div class=\\\"verify-actions\\\">\u001b[39m\n\u001b[31m        ${c.docket && c.phone ? `<button class=\\\"btn-wa\\\" onclick=\\\"window.open('${waUrl}','_blank')\\\">📱 WhatsApp</button>` : ''}\u001b[39m\n\u001b[31m        <button class=\\\"btn-sm\\\" onclick=\\\"removeVerifyRow(${i})\\\">✕ Remove</button>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m    </div>`;\u001b[39m\n\u001b[31m  }).join('');·\u001b[39m\n\u001b[31m  // Store dispatch object for confirm\u001b[39m\n\u001b[31m  pane._pendingDispatch = d;\u001b[39m\n\u001b[31m  pane.style.display = 'block';\u001b[39m\n\u001b[31m  pane.scrollTop = 0;\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction updateVerifyField(idx, field, input) {\u001b[39m\n\u001b[31m  const pane = document.getElementById('verifyPane');\u001b[39m\n\u001b[31m  if (!pane._pendingDispatch) return;\u001b[39m\n\u001b[31m  const c = pane._pendingDispatch.consignees[idx];\u001b[39m\n\u001b[31m  if (!c) return;\u001b[39m\n\u001b[31m  c[field] = input.value.trim();\u001b[39m\n\u001b[31m  input.classList.toggle('changed', input.value.trim() !== '');\u001b[39m\n\u001b[31m  // Re-check dupe if docket changed\u001b[39m\n\u001b[31m  if (field === 'docket') refreshVerifyFlags(pane._pendingDispatch);\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction refreshVerifyFlags(d) {\u001b[39m\n\u001b[31m  const seen = {};\u001b[39m\n\u001b[31m  d.consignees.forEach(c => { if (c.docket) seen[c.docket] = (seen[c.docket]||0)+1; });\u001b[39m\n\u001b[31m  d.consignees.forEach((c, i) => {\u001b[39m\n\u001b[31m    const row = document.getElementById('vrow_' + i);\u001b[39m\n\u001b[31m    if (!row) return;\u001b[39m\n\u001b[31m    const isDupe = c.docket && seen[c.docket] > 1;\u001b[39m\n\u001b[31m    row.classList.toggle('has-dupe', isDupe);\u001b[39m\n\u001b[31m  });\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction removeVerifyRow(idx) {\u001b[39m\n\u001b[31m  const pane = document.getElementById('verifyPane');\u001b[39m\n\u001b[31m  if (!pane._pendingDispatch) return;\u001b[39m\n\u001b[31m  const el = document.getElementById('vrow_' + idx);\u001b[39m\n\u001b[31m  if (el) {\u001b[39m\n\u001b[31m    el.style.transition = 'opacity .25s';\u001b[39m\n\u001b[31m    el.style.opacity = '0';\u001b[39m\n\u001b[31m    setTimeout(() => {\u001b[39m\n\u001b[31m      pane._pendingDispatch.consignees.splice(idx, 1);\u001b[39m\n\u001b[31m      showVerifyScreen(pane._pendingDispatch); // re-render with updated indices\u001b[39m\n\u001b[31m    }, 250);\u001b[39m\n\u001b[31m  } else {\u001b[39m\n\u001b[31m    pane._pendingDispatch.consignees.splice(idx, 1);\u001b[39m\n\u001b[31m    showVerifyScreen(pane._pendingDispatch);\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction approveAll() {\u001b[39m\n\u001b[31m  showToast('All rows approved ✓', 'ok');\u001b[39m\n\u001b[31m  document.getElementById('btnConfirmFinalise').style.background = 'var(--grn)';\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction closeVerify() {\u001b[39m\n\u001b[31m  const pane = document.getElementById('verifyPane');\u001b[39m\n\u001b[31m  // If this dispatch was already finalised, warn before going back\u001b[39m\n\u001b[31m  if (_lastFinalisedId && pane._pendingDispatch?.id === _lastFinalisedId) {\u001b[39m\n\u001b[31m    if (!confirm(\u001b[39m\n\u001b[31m      'This dispatch has already been finalised.\\\\n\\\\n' +\u001b[39m\n\u001b[31m      'Going back will take you to the edit form — any changes will create a NEW dispatch record.\\\\n\\\\n' +\u001b[39m\n\u001b[31m      'Go back anyway?'\u001b[39m\n\u001b[31m    )) return;\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m  pane.style.display = 'none';\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction showSlipFull(idx) {\u001b[39m\n\u001b[31m  const s = slipOCRData[idx];\u001b[39m\n\u001b[31m  if (!s?.imageData) return;\u001b[39m\n\u001b[31m  const w = window.open('');\u001b[39m\n\u001b[31m  w.document.write(`<img src=\\\"${s.imageData}\\\" style=\\\"max-width:100%;height:auto;\\\">`);\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// Log OCR correction to sheets for future accuracy improvement\u001b[39m\n\u001b[31masync function logOCRCorrection(original, corrected, courier) {\u001b[39m\n\u001b[31m  if (!window.appConfig.webhook_url || original === corrected) return;\u001b[39m\n\u001b[31m  try {\u001b[39m\n\u001b[31m    await fetch(window.appConfig.webhook_url, {\u001b[39m\n\u001b[31m      method: 'POST', mode: 'no-cors',\u001b[39m\n\u001b[31m      headers: { 'Content-Type': 'application/json' },\u001b[39m\n\u001b[31m      body: JSON.stringify({\u001b[39m\n\u001b[31m        type: 'ocr_correction',\u001b[39m\n\u001b[31m        original, corrected, courier,\u001b[39m\n\u001b[31m        timestamp: new Date().toISOString()\u001b[39m\n\u001b[31m      })\u001b[39m\n\u001b[31m    });\u001b[39m\n\u001b[31m  } catch(e) {}\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mlet _lastFinalisedId = null; // track last finalised dispatch to warn on back·\u001b[39m\n\u001b[31masync function confirmAndFinalise() {\u001b[39m\n\u001b[31m  const pane = document.getElementById('verifyPane');\u001b[39m\n\u001b[31m  const d = pane._pendingDispatch;\u001b[39m\n\u001b[31m  if (!d) return;·\u001b[39m\n\u001b[31m  d.status = 'active';\u001b[39m\n\u001b[31m  d.finalisedAt = new Date().toISOString();·\u001b[39m\n\u001b[31m  // Log OCR corrections\u001b[39m\n\u001b[31m  slipOCRData.forEach((s, i) => {\u001b[39m\n\u001b[31m    const c = d.consignees[i];\u001b[39m\n\u001b[31m    if (c && s.docket && c.docket && s.docket !== c.docket) {\u001b[39m\n\u001b[31m      logOCRCorrection(s.docket, c.docket, d.courier);\u001b[39m\n\u001b[31m    }\u001b[39m\n\u001b[31m  });·\u001b[39m\n\u001b[31m  // Replace existing draft with same ID if present, else prepend\u001b[39m\n\u001b[31m  const existingIdx = dispatches.findIndex(x => x.id === d.id);\u001b[39m\n\u001b[31m  if (existingIdx >= 0) {\u001b[39m\n\u001b[31m    dispatches[existingIdx] = d;\u001b[39m\n\u001b[31m  } else {\u001b[39m\n\u001b[31m    dispatches.unshift(d);\u001b[39m\n\u001b[31m  }·\u001b[39m\n\u001b[31m  _lastFinalisedId = d.id;\u001b[39m\n\u001b[31m  saveData();\u001b[39m\n\u001b[31m  syncDispatchToDB(d); // push to Postgres (non-blocking)\u001b[39m\n\u001b[31m  updateHistCount();\u001b[39m\n\u001b[31m  if (window.appConfig.auto_log !== 'false' && window.appConfig.webhook_url) await logToSheets(d);\u001b[39m\n\u001b[31m  showToast('Dispatch finalised ✓', 'ok');\u001b[39m\n\u001b[31m  pane.style.display = 'none';\u001b[39m\n\u001b[31m  pane._pendingDispatch = null; // clear pending\u001b[39m\n\u001b[31m  resetForm();\u001b[39m\n\u001b[31m  showPane('dashboard');\u001b[39m\n\u001b[31m  renderDashboard();\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction resetForm() {\u001b[39m\n\u001b[31m  document.getElementById('n_client').value = '';\u001b[39m\n\u001b[31m  document.getElementById('n_jobref').value = '';\u001b[39m\n\u001b[31m  document.getElementById('n_courier').value = '';\u001b[39m\n\u001b[31m  document.getElementById('n_remarks').value = '';\u001b[39m\n\u001b[31m  document.getElementById('jobPill').style.display = 'none';\u001b[39m\n\u001b[31m  document.getElementById('customClientField').style.display = 'none';\u001b[39m\n\u001b[31m  document.getElementById('consList').innerHTML = '';\u001b[39m\n\u001b[31m  addConsignee();\u001b[39m\n\u001b[31m  clearSlips();\u001b[39m\n\u001b[31m  clearStores();\u001b[39m\n\u001b[31m  setToday();\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction newDispatch() { resetForm(); showPane('new'); }·\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31m// DASHBOARD\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31mfunction renderDashboard() {\u001b[39m\n\u001b[31m  // Exclude drafts from dashboard — only show active/finalised dispatches\u001b[39m\n\u001b[31m  const activeDis = dispatches.filter(d => d.status !== 'draft');\u001b[39m\n\u001b[31m  const allCons = activeDis.flatMap(d => d.consignees || []);\u001b[39m\n\u001b[31m  const countBy = (status) => allCons.filter(c => c.status === status).length;\u001b[39m\n\u001b[31m  document.getElementById('st-delivered').textContent = countBy('delivered');\u001b[39m\n\u001b[31m  document.getElementById('st-transit').textContent = countBy('transit') + countBy('in-transit');\u001b[39m\n\u001b[31m  document.getElementById('st-booked').textContent = countBy('booked');\u001b[39m\n\u001b[31m  document.getElementById('st-pending').textContent = allCons.filter(c => !c.docket || c.status === 'pending').length;·\u001b[39m\n\u001b[31m  const grid = document.getElementById('dashGrid');\u001b[39m\n\u001b[31m  // Flatten all consignees across dispatches\u001b[39m\n\u001b[31m  let items = activeDis.flatMap(d =>\u001b[39m\n\u001b[31m    (d.consignees || []).filter(c => c.docket).map(c => ({...c, dispatchId: d.id, courier: d.courier, client: d.client, jobref: d.jobref}))\u001b[39m\n\u001b[31m  );\u001b[39m\n\u001b[31m  if (dashFilter !== 'all') {\u001b[39m\n\u001b[31m    items = items.filter(c => {\u001b[39m\n\u001b[31m      if (dashFilter === 'delivered') return c.status === 'delivered';\u001b[39m\n\u001b[31m      if (dashFilter === 'transit') return c.status === 'transit' || c.status === 'in-transit';\u001b[39m\n\u001b[31m      if (dashFilter === 'booked') return c.status === 'booked';\u001b[39m\n\u001b[31m      if (dashFilter === 'pending') return !c.docket || c.status === 'pending';\u001b[39m\n\u001b[31m      return true;\u001b[39m\n\u001b[31m    });\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m  if (!items.length) {\u001b[39m\n\u001b[31m    grid.innerHTML = '<div class=\\\"empty\\\"><div class=\\\"e-icon\\\">📦</div><p>No docket-tracked consignees.<br>Add docket numbers to see tiles.</p></div>';\u001b[39m\n\u001b[31m    return;\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m  const progMap = { delivered:100, transit:60,'in-transit':60, booked:20, pending:5 };\u001b[39m\n\u001b[31m  const progClr = { delivered:'var(--grn)', transit:'var(--ylw)','in-transit':'var(--ylw)', booked:'var(--sky)', pending:'var(--t4)' };\u001b[39m\n\u001b[31m  grid.innerHTML = items.map(c => {\u001b[39m\n\u001b[31m    const sp = c.status || 'booked';\u001b[39m\n\u001b[31m    const prog = progMap[sp] || 20;\u001b[39m\n\u001b[31m    const clr = progClr[sp] || 'var(--t4)';\u001b[39m\n\u001b[31m    return `\u001b[39m\n\u001b[31m    <div class=\\\"docket-tile\\\" onclick=\\\"showDocketDetail('${c.docket}','${c.courier||''}','${c.dispatchId}')\\\">\u001b[39m\n\u001b[31m      <div class=\\\"docket-tile-hdr\\\">\u001b[39m\n\u001b[31m        <div class=\\\"docket-tile-left\\\">\u001b[39m\n\u001b[31m          <div class=\\\"docket-num\\\">${c.docket}</div>\u001b[39m\n\u001b[31m          <div class=\\\"docket-store\\\">${c.name} — ${c.city}</div>\u001b[39m\n\u001b[31m          <div class=\\\"docket-courier\\\">${c.courier||''} · ${c.client||''} ${c.jobref ? '› '+c.jobref : ''}</div>\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m        <span class=\\\"status-pill sp-${sp}\\\">${sp}</span>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      <div style=\\\"display:flex;gap:6px;margin-bottom:6px;align-items:center;\\\">\u001b[39m\n\u001b[31m        <input type=\\\"checkbox\\\" id=\\\"sel_${c.docket.replace(/[^a-z0-9]/gi,'_')}\\\"\u001b[39m\n\u001b[31m          onclick=\\\"event.stopPropagation();toggleTileSelect('${c.docket}')\\\"\u001b[39m\n\u001b[31m          style=\\\"width:14px;height:14px;cursor:pointer;accent-color:var(--acc);\\\">\u001b[39m\n\u001b[31m        <label style=\\\"font-size:.6rem;color:var(--t3);font-family:var(--fnt-mono);cursor:pointer;\\\"\u001b[39m\n\u001b[31m          onclick=\\\"event.stopPropagation();toggleTileSelect('${c.docket}')\\\">Select</label>\u001b[39m\n\u001b[31m        ${c.manualStatus ?\u001b[39m\n\u001b[31m          `<span style=\\\"font-size:.6rem;font-family:var(--fnt-mono);color:var(--grn);\\\">📞 ${c.manualStatus}</span>` : ''}\u001b[39m\n\u001b[31m        ${c.podUploaded ? '<span style=\\\"font-size:.6rem;color:var(--grn);font-family:var(--fnt-mono);\\\">✓ POD</span>' : ''}\u001b[39m\n\u001b[31m        <button onclick=\\\"event.stopPropagation();showManualTracking('${c.docket}','${c.dispatchId}','${c.name}','${c.phone||''}','${c.city||''}')\\\"\u001b[39m\n\u001b[31m          style=\\\"font-size:.6rem;background:rgba(56,189,248,.1);border:1px solid rgba(56,189,248,.3);\u001b[39m\n\u001b[31m          color:var(--sky);padding:2px 7px;border-radius:10px;cursor:pointer;font-family:var(--fnt-mono);\\\">📞 Call Log</button>\u001b[39m\n\u001b[31m        <button onclick=\\\"event.stopPropagation();showPODUpload('${c.docket}','${c.dispatchId}')\\\"\u001b[39m\n\u001b[31m          style=\\\"font-size:.6rem;background:none;border:1px solid var(--bdr);color:var(--t3);\u001b[39m\n\u001b[31m          padding:2px 7px;border-radius:10px;cursor:pointer;font-family:var(--fnt-mono);\\\">📸 POD</button>\u001b[39m\n\u001b[31m        ${c.manualDelivered ? `<button onclick=\\\"event.stopPropagation();resumeAutoTracking('${c.docket}','${c.dispatchId}')\\\"\u001b[39m\n\u001b[31m          style=\\\"font-size:.6rem;background:rgba(251,146,60,.1);border:1px solid rgba(251,146,60,.4);\u001b[39m\n\u001b[31m          color:#fb923c;padding:2px 7px;border-radius:10px;cursor:pointer;font-family:var(--fnt-mono);\\\">↩ Undo</button>` : ''}\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      <div class=\\\"docket-tile-footer\\\">\u001b[39m\n\u001b[31m        <div style=\\\"flex:1;min-width:0;\\\">\u001b[39m\n\u001b[31m          <span class=\\\"docket-last-event docket-evt-${c.docket}\\\" id=\\\"evt_${c.docket.replace(/[^a-z0-9]/gi,'_')}\\\">${c.lastEvent || 'No tracking yet'}</span>\u001b[39m\n\u001b[31m          <span class=\\\"track-spinner\\\" id=\\\"spin_${c.docket.replace(/[^a-z0-9]/gi,'_')}\\\" style=\\\"display:none;color:var(--ylw);font-family:var(--fnt-mono);font-size:.68rem;animation:blink 1s infinite;\\\">⏳ tracking...</span>\u001b[39m\n\u001b[31m          ${c.lastScanLocation && c.lastScanLocation !== c.lastEvent ?·\u001b[39m\n\u001b[31m            `<div style=\\\"font-size:.62rem;color:var(--t3);font-family:var(--fnt-mono);margin-top:2px;\\\">📍 ${c.lastScanLocation}</div>` : ''}\u001b[39m\n\u001b[31m          ${c.lastTrackedAt ?·\u001b[39m\n\u001b[31m            `<div style=\\\"font-size:.6rem;color:var(--t4);font-family:var(--fnt-mono);margin-top:1px;\\\">🕐 tracked ${new Date(c.lastTrackedAt).toLocaleTimeString('en-IN',{hour:'2-digit',minute:'2-digit'})}</div>` : ''}\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m        <button class=\\\"btn-track-live live-btn-${c.docket.replace(/[^a-z0-9]/gi,'_')}\\\" onclick=\\\"event.stopPropagation();liveTrackTile('${c.docket}','${c.courier||''}','${c.dispatchId}')\\\">\u001b[39m\n\u001b[31m          <span class=\\\"live-dot\\\"></span>LIVE\u001b[39m\n\u001b[31m        </button>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      <div class=\\\"docket-progress\\\">\u001b[39m\n\u001b[31m        <div class=\\\"docket-progress-fill\\\" style=\\\"width:${prog}%;background:${clr};\\\"></div>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m    </div>`;\u001b[39m\n\u001b[31m  }).join('');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction filterDash(f, el) {\u001b[39m\n\u001b[31m  dashFilter = f;\u001b[39m\n\u001b[31m  document.querySelectorAll('#dashFilters .btn-sm').forEach(b => b.classList.remove('primary'));\u001b[39m\n\u001b[31m  el.classList.add('primary');\u001b[39m\n\u001b[31m  renderDashboard();\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31masync function liveTrackTile(docket, courier, dispatchId) {\u001b[39m\n\u001b[31m  showToast(`Tracking ${docket}...`, 'info');\u001b[39m\n\u001b[31m  const result = await fetchTracking(docket, courier);\u001b[39m\n\u001b[31m  // Update status in dispatches\u001b[39m\n\u001b[31m  for (const d of dispatches) {\u001b[39m\n\u001b[31m    if (d.id !== dispatchId) continue;\u001b[39m\n\u001b[31m    for (const c of d.consignees) {\u001b[39m\n\u001b[31m      if (c.docket === docket) {\u001b[39m\n\u001b[31m        c.status = result.status || c.status;\u001b[39m\n\u001b[31m        c.lastEvent = result.lastEvent || c.lastEvent;\u001b[39m\n\u001b[31m        c.lastUpdated = new Date().toISOString();\u001b[39m\n\u001b[31m      }\u001b[39m\n\u001b[31m    }\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m  saveData();\u001b[39m\n\u001b[31m  renderDashboard();\u001b[39m\n\u001b[31m  if (result.status === 'failed') {\u001b[39m\n\u001b[31m    // Open courier site as fallback\u001b[39m\n\u001b[31m    openCourierTrackUrl(docket, courier);\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction showDocketDetail(docket, courier, dispatchId) {\u001b[39m\n\u001b[31m  const d = dispatches.find(x => x.id === dispatchId);\u001b[39m\n\u001b[31m  const c = d?.consignees?.find(x => x.docket === docket);\u001b[39m\n\u001b[31m  if (!c) return;\u001b[39m\n\u001b[31m  const content = document.getElementById('detailContent');\u001b[39m\n\u001b[31m  content.innerHTML = `\u001b[39m\n\u001b[31m    <div class=\\\"sheet-title\\\">${docket}</div>\u001b[39m\n\u001b[31m    <div class=\\\"info-tag\\\">📍 <strong>${c.name}</strong> — ${c.city}</div>\u001b[39m\n\u001b[31m    ${c.phone ? `<div class=\\\"info-tag\\\">📞 ${c.phone}</div>` : ''}\u001b[39m\n\u001b[31m    <div class=\\\"info-tag\\\">📦 ${c.material||'—'} ${c.qty ? '('+c.qty+' pcs)':''}</div>\u001b[39m\n\u001b[31m    <div class=\\\"info-tag\\\">🚚 ${courier||'—'} · <span class=\\\"status-pill sp-${c.status||'pending'}\\\">${c.status||'pending'}</span></div>\u001b[39m\n\u001b[31m    ${c.lastEvent ? `<div class=\\\"info-tag\\\">🕐 ${c.lastEvent}</div>` : ''}\u001b[39m\n\u001b[31m    <div class=\\\"sec-div\\\"><span>Tracking History</span></div>\u001b[39m\n\u001b[31m    <div id=\\\"docketTimeline\\\">\u001b[39m\n\u001b[31m      ${c.timeline?.length ?\u001b[39m\n\u001b[31m        c.timeline.map(e => `<div class=\\\"track-evt\\\"><span class=\\\"track-time\\\">${e.time||''}</span><div><div class=\\\"track-desc\\\">${e.desc||''}</div><div class=\\\"track-loc\\\">${e.loc||''}</div></div></div>`).join('') :\u001b[39m\n\u001b[31m        '<div style=\\\"color:var(--t3);font-size:.78rem;padding:10px 0;\\\">No timeline data. Click Live Track to fetch.</div>'}\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m    <div style=\\\"display:flex;gap:6px;flex-wrap:wrap;margin-top:14px;\\\">\u001b[39m\n\u001b[31m      <button class=\\\"btn btn-ghost\\\" style=\\\"flex:1;\\\" onclick=\\\"openCourierTrackUrl('${docket}','${courier}')\\\">🔗 Courier Site</button>\u001b[39m\n\u001b[31m      <button class=\\\"btn btn-orange\\\" style=\\\"flex:1;\\\" onclick=\\\"liveTrackTile('${docket}','${courier}','${dispatchId}');closeOverlay('detailOverlay')\\\">⚡ Live Track</button>\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m    <div style=\\\"display:flex;gap:6px;flex-wrap:wrap;margin-top:6px;\\\">\u001b[39m\n\u001b[31m      <button class=\\\"btn-wa\\\" style=\\\"flex:1;padding:8px;\\\" onclick=\\\"closeOverlay('detailOverlay');showPODUpload('${docket}','${dispatchId}')\\\">📸 Upload POD</button>\u001b[39m\n\u001b[31m      ${c.podUploaded ? '<span style=\\\"color:var(--grn);font-size:.72rem;font-family:var(--fnt-mono);padding:6px;\\\">✓ POD uploaded</span>' : ''}\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m  `;\u001b[39m\n\u001b[31m  openOverlay('detailOverlay');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31m// TRACKING ENGINE\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31mfunction _normaliseStatus(raw) {\u001b[39m\n\u001b[31m  const s = (raw || '').toLowerCase().trim();\u001b[39m\n\u001b[31m  if (!s || s === 'error') return null;\u001b[39m\n\u001b[31m  if (s.includes('undeliver') || s.includes('delivery_fail') ||\u001b[39m\n\u001b[31m      s.includes('failed_delivery') || s.includes('not deliver') ||\u001b[39m\n\u001b[31m      s.includes('return')  || s.includes('rto')) return 'pending';\u001b[39m\n\u001b[31m  if (s.includes('deliver'))                       return 'delivered';\u001b[39m\n\u001b[31m  if (s.includes('transit') || s.includes('hub') ||\u001b[39m\n\u001b[31m      s.includes('inbound') || s.includes('outbound') ||\u001b[39m\n\u001b[31m      s.includes('out for'))                       return 'transit';\u001b[39m\n\u001b[31m  if (s.includes('book') || s.includes('pick'))   return 'booked';\u001b[39m\n\u001b[31m  return s;\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31masync function _fetchWithRetry(url, opts, maxRetries = 1, retryDelayMs = 4000) {\u001b[39m\n\u001b[31m  for (let attempt = 0; attempt <= maxRetries; attempt++) {\u001b[39m\n\u001b[31m    try {\u001b[39m\n\u001b[31m      const resp = await fetch(url, opts);\u001b[39m\n\u001b[31m      if (resp.ok) return resp;\u001b[39m\n\u001b[31m    } catch(e) {\u001b[39m\n\u001b[31m      if (attempt === maxRetries) throw e;\u001b[39m\n\u001b[31m      await new Promise(r => setTimeout(r, retryDelayMs));\u001b[39m\n\u001b[31m    }\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m  return null;\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31masync function fetchTracking(docket, courier) {\u001b[39m\n\u001b[31m  // Strategy 1: VPS proxy (SkyKing + others that block direct fetch)\u001b[39m\n\u001b[31m  const proxyUrl = window.appConfig.proxy_url;\u001b[39m\n\u001b[31m  if (proxyUrl) {\u001b[39m\n\u001b[31m    try {\u001b[39m\n\u001b[31m      const url  = `${proxyUrl}?courier=${encodeURIComponent(courier)}&docket=${encodeURIComponent(docket)}`;\u001b[39m\n\u001b[31m      const resp = await _fetchWithRetry(url, { signal: AbortSignal.timeout(15000) });\u001b[39m\n\u001b[31m      if (resp) {\u001b[39m\n\u001b[31m        const data      = await resp.json();\u001b[39m\n\u001b[31m        const normStatus = _normaliseStatus(data.status);\u001b[39m\n\u001b[31m        if (normStatus) {\u001b[39m\n\u001b[31m          return { status: normStatus, lastEvent: data.lastEvent || '', timeline: data.timeline || [] };\u001b[39m\n\u001b[31m        }\u001b[39m\n\u001b[31m        openCourierTrackUrl(docket, courier);\u001b[39m\n\u001b[31m        return { status: 'transit', lastEvent: data.lastEvent || 'Check courier site', timeline: [] };\u001b[39m\n\u001b[31m      }\u001b[39m\n\u001b[31m    } catch(e) {}\u001b[39m\n\u001b[31m  }·\u001b[39m\n\u001b[31m  // Strategy 2: Delhivery direct API\u001b[39m\n\u001b[31m  if (courier === 'Delhivery') {\u001b[39m\n\u001b[31m    try {\u001b[39m\n\u001b[31m      const resp = await _fetchWithRetry(\u001b[39m\n\u001b[31m        `https://dlv-api.delhivery.com/v3/track?waybill=${docket}`,\u001b[39m\n\u001b[31m        { signal: AbortSignal.timeout(8000) }\u001b[39m\n\u001b[31m      );\u001b[39m\n\u001b[31m      if (resp) {\u001b[39m\n\u001b[31m        const data = await resp.json();\u001b[39m\n\u001b[31m        const pkg  = data.ShipmentData?.[0]?.Shipment;\u001b[39m\n\u001b[31m        if (pkg) {\u001b[39m\n\u001b[31m          const normStatus = _normaliseStatus(pkg.Status?.Status);\u001b[39m\n\u001b[31m          return { status: normStatus || 'transit', lastEvent: pkg.Status?.Instructions || '', timeline: [] };\u001b[39m\n\u001b[31m        }\u001b[39m\n\u001b[31m      }\u001b[39m\n\u001b[31m    } catch(e) {}\u001b[39m\n\u001b[31m  }·\u001b[39m\n\u001b[31m  return { status: 'failed', lastEvent: 'Open courier site to check', timeline: [] };\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction openCourierTrackUrl(docket, courier) {\u001b[39m\n\u001b[31m  const m = COURIER_MASTER[courier];\u001b[39m\n\u001b[31m  if (!m) return;\u001b[39m\n\u001b[31m  const url = m.trackUrl.replace('{docket}', docket);\u001b[39m\n\u001b[31m  window.open(url, '_blank');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// Track tab - multiple dockets\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31m// TRACK TAB — new logic\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31mlet _trackSelected = new Set(); // dockets currently selected for tracking·\u001b[39m\n\u001b[31m// Called when Track tab opens or courier changes\u001b[39m\n\u001b[31mfunction buildTrackDocketChips() {\u001b[39m\n\u001b[31m  const courier = document.getElementById('t_courier').value;\u001b[39m\n\u001b[31m  const m = COURIER_MASTER[courier];\u001b[39m\n\u001b[31m  const maxBulk = m?.maxBulk || 1;\u001b[39m\n\u001b[31m  const limitEl = document.getElementById('trackCourierLimit');\u001b[39m\n\u001b[31m  if (limitEl) limitEl.textContent = `Max ${maxBulk} at once for ${courier}`;·\u001b[39m\n\u001b[31m  // Gather all dockets from active dispatches for this courier\u001b[39m\n\u001b[31m  const activeDis = dispatches.filter(d => d.status !== 'draft');\u001b[39m\n\u001b[31m  const allDockets = activeDis.flatMap(d =>\u001b[39m\n\u001b[31m    (d.consignees || [])\u001b[39m\n\u001b[31m      .filter(c => c.docket && d.courier === courier)\u001b[39m\n\u001b[31m      .map(c => ({ docket: c.docket, name: c.name, city: c.city, status: c.status || 'booked' }))\u001b[39m\n\u001b[31m  );·\u001b[39m\n\u001b[31m  // Also include dockets from OTHER couriers as un-selectable reference\u001b[39m\n\u001b[31m  const otherDockets = activeDis.flatMap(d =>\u001b[39m\n\u001b[31m    (d.consignees || [])\u001b[39m\n\u001b[31m      .filter(c => c.docket && d.courier !== courier)\u001b[39m\n\u001b[31m      .map(c => ({ docket: c.docket, name: c.name, courier: d.courier }))\u001b[39m\n\u001b[31m  );·\u001b[39m\n\u001b[31m  const chips = document.getElementById('trackDocketChips');\u001b[39m\n\u001b[31m  if (!allDockets.length && !otherDockets.length) {\u001b[39m\n\u001b[31m    chips.innerHTML = '<span style=\\\"font-size:.72rem;color:var(--t3);font-family:var(--fnt-mono);\\\">No dispatched dockets yet — enter manually below</span>';\u001b[39m\n\u001b[31m    return;\u001b[39m\n\u001b[31m  }·\u001b[39m\n\u001b[31m  const spClr = { delivered:'var(--grn)', transit:'var(--ylw)', booked:'var(--sky)', pending:'var(--t4)' };·\u001b[39m\n\u001b[31m  chips.innerHTML = [\u001b[39m\n\u001b[31m    // This courier's dockets\u001b[39m\n\u001b[31m    ...allDockets.map(d => {\u001b[39m\n\u001b[31m      const sel = _trackSelected.has(d.docket);\u001b[39m\n\u001b[31m      const clr = spClr[d.status] || 'var(--t4)';\u001b[39m\n\u001b[31m      return `<button onclick=\\\"toggleTrackDocket('${d.docket}')\\\" id=\\\"chip_${d.docket}\\\"\u001b[39m\n\u001b[31m        style=\\\"font-family:var(--fnt-mono);font-size:.72rem;padding:4px 10px;border-radius:20px;\u001b[39m\n\u001b[31m        cursor:pointer;transition:all .2s;border:1px solid ${sel ? 'var(--acc)' : 'var(--bdr)'};\u001b[39m\n\u001b[31m        background:${sel ? 'rgba(249,115,22,.15)' : 'var(--sur2)'};\u001b[39m\n\u001b[31m        color:${sel ? 'var(--acc)' : 'var(--t2)'};\\\">\u001b[39m\n\u001b[31m        ${d.docket} <span style=\\\"font-size:.6rem;color:${clr};\\\">● ${d.status}</span>\u001b[39m\n\u001b[31m        ${d.city ? `<span style=\\\"font-size:.6rem;color:var(--t3);margin-left:4px;\\\">${d.city}</span>` : ''}\u001b[39m\n\u001b[31m      </button>`;\u001b[39m\n\u001b[31m    }),\u001b[39m\n\u001b[31m    // Other couriers — show greyed, click selects and switches courier\u001b[39m\n\u001b[31m    ...otherDockets.slice(0, 5).map(d =>\u001b[39m\n\u001b[31m      `<span title=\\\"${d.courier}\\\" style=\\\"font-family:var(--fnt-mono);font-size:.68rem;padding:3px 8px;\u001b[39m\n\u001b[31m        border-radius:20px;border:1px dashed var(--bdr);color:var(--t4);cursor:default;\\\">\u001b[39m\n\u001b[31m        ${d.docket} <span style=\\\"font-size:.55rem;\\\">${d.courier}</span>\u001b[39m\n\u001b[31m      </span>`\u001b[39m\n\u001b[31m    )\u001b[39m\n\u001b[31m  ].join('');·\u001b[39m\n\u001b[31m  updateTrackSummary();\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction toggleTrackDocket(docket) {\u001b[39m\n\u001b[31m  const courier = document.getElementById('t_courier').value;\u001b[39m\n\u001b[31m  const m = COURIER_MASTER[courier];\u001b[39m\n\u001b[31m  const maxBulk = m?.maxBulk || 1;·\u001b[39m\n\u001b[31m  if (_trackSelected.has(docket)) {\u001b[39m\n\u001b[31m    _trackSelected.delete(docket);\u001b[39m\n\u001b[31m  } else {\u001b[39m\n\u001b[31m    if (_trackSelected.size >= maxBulk) {\u001b[39m\n\u001b[31m      showToast(`${courier} supports max ${maxBulk} docket(s) at once`, 'err');\u001b[39m\n\u001b[31m      return;\u001b[39m\n\u001b[31m    }\u001b[39m\n\u001b[31m    _trackSelected.add(docket);\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m  buildTrackDocketChips(); // re-render chips with updated selection\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction addManualDockets() {\u001b[39m\n\u001b[31m  const raw = document.getElementById('t_manual_dockets').value;\u001b[39m\n\u001b[31m  const courier = document.getElementById('t_courier').value;\u001b[39m\n\u001b[31m  const m = COURIER_MASTER[courier];\u001b[39m\n\u001b[31m  const maxBulk = m?.maxBulk || 1;\u001b[39m\n\u001b[31m  const newDockets = raw.split(/[,\\\\n]/).map(d => d.trim()).filter(Boolean);\u001b[39m\n\u001b[31m  let added = 0;\u001b[39m\n\u001b[31m  for (const d of newDockets) {\u001b[39m\n\u001b[31m    if (_trackSelected.size >= maxBulk) {\u001b[39m\n\u001b[31m      showToast(`Max ${maxBulk} reached for ${courier}`, 'err');\u001b[39m\n\u001b[31m      break;\u001b[39m\n\u001b[31m    }\u001b[39m\n\u001b[31m    _trackSelected.add(d);\u001b[39m\n\u001b[31m    added++;\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m  document.getElementById('t_manual_dockets').value = '';\u001b[39m\n\u001b[31m  buildTrackDocketChips();\u001b[39m\n\u001b[31m  if (added) showToast(`${added} docket(s) added ✓`, 'ok');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction clearTrackSelection() {\u001b[39m\n\u001b[31m  _trackSelected.clear();\u001b[39m\n\u001b[31m  buildTrackDocketChips();\u001b[39m\n\u001b[31m  document.getElementById('trackResultsArea').innerHTML = '';\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction updateTrackSummary() {\u001b[39m\n\u001b[31m  const summary = document.getElementById('trackSelectedSummary');\u001b[39m\n\u001b[31m  const countEl = document.getElementById('trackSelectedCount');\u001b[39m\n\u001b[31m  const listEl  = document.getElementById('trackSelectedList');\u001b[39m\n\u001b[31m  if (!summary) return;\u001b[39m\n\u001b[31m  const arr = Array.from(_trackSelected);\u001b[39m\n\u001b[31m  summary.style.display = arr.length ? 'block' : 'none';\u001b[39m\n\u001b[31m  if (countEl) countEl.textContent = `${arr.length} docket(s) selected`;\u001b[39m\n\u001b[31m  if (listEl)  listEl.textContent  = arr.slice(0, 5).join(', ') + (arr.length > 5 ? `... +${arr.length-5}` : '');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction openTrackOnCourierSite() {\u001b[39m\n\u001b[31m  const courier = document.getElementById('t_courier').value;\u001b[39m\n\u001b[31m  const dockets = Array.from(_trackSelected);\u001b[39m\n\u001b[31m  if (!dockets.length) { showToast('Select dockets first', 'err'); return; }\u001b[39m\n\u001b[31m  const m = COURIER_MASTER[courier];\u001b[39m\n\u001b[31m  if (!m) return;\u001b[39m\n\u001b[31m  // SkyKing: open with all CNOs comma-separated in URL\u001b[39m\n\u001b[31m  let url;\u001b[39m\n\u001b[31m  if (courier === 'SkyKing') {\u001b[39m\n\u001b[31m    url = `https://skyking.co/track?cno=${dockets.join(',')}`;\u001b[39m\n\u001b[31m  } else if (dockets.length === 1) {\u001b[39m\n\u001b[31m    url = m.trackUrl.replace('{docket}', dockets[0]);\u001b[39m\n\u001b[31m  } else {\u001b[39m\n\u001b[31m    url = m.trackBulkUrl || m.trackUrl.replace('{docket}', dockets[0]);\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m  window.open(url, '_blank');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31masync function trackSelected() {\u001b[39m\n\u001b[31m  const courier = document.getElementById('t_courier').value;\u001b[39m\n\u001b[31m  const dockets = Array.from(_trackSelected);\u001b[39m\n\u001b[31m  if (!dockets.length) { showToast('Select at least one docket', 'err'); return; }·\u001b[39m\n\u001b[31m  const area = document.getElementById('trackResultsArea');\u001b[39m\n\u001b[31m  area.innerHTML = `<div class=\\\"card\\\" style=\\\"text-align:center;padding:20px;\\\">\u001b[39m\n\u001b[31m    <div style=\\\"font-size:1.5rem;margin-bottom:8px;\\\">⏳</div>\u001b[39m\n\u001b[31m    <div style=\\\"font-family:var(--fnt-mono);font-size:.78rem;color:var(--t2);\\\">Tracking ${dockets.length} docket(s) via ${courier}...</div>\u001b[39m\n\u001b[31m  </div>`;·\u001b[39m\n\u001b[31m  let resultsHTML = '';\u001b[39m\n\u001b[31m  for (const docket of dockets) {\u001b[39m\n\u001b[31m    const result = await fetchTracking(docket, courier);\u001b[39m\n\u001b[31m    const sp = (!result.status || result.status === 'failed') ? 'pending' : result.status;\u001b[39m\n\u001b[31m    const spClr = { delivered:'var(--grn)', transit:'var(--ylw)', booked:'var(--sky)', pending:'var(--t3)' };\u001b[39m\n\u001b[31m    const clr = spClr[sp] || 'var(--t3)';\u001b[39m\n\u001b[31m    // Update stored dispatch if this docket exists in our records\u001b[39m\n\u001b[31m    for (const d of dispatches) {\u001b[39m\n\u001b[31m      for (const c of (d.consignees||[])) {\u001b[39m\n\u001b[31m        if (c.docket === docket && result.status && result.status !== 'failed') {\u001b[39m\n\u001b[31m          c.status = result.status;\u001b[39m\n\u001b[31m          c.lastEvent = result.lastEvent;\u001b[39m\n\u001b[31m          c.lastTrackedAt = new Date().toISOString();\u001b[39m\n\u001b[31m        }\u001b[39m\n\u001b[31m      }\u001b[39m\n\u001b[31m    }\u001b[39m\n\u001b[31m    resultsHTML += `\u001b[39m\n\u001b[31m    <div style=\\\"background:var(--sur);border:1px solid var(--bdr);border-left:3px solid ${clr};\u001b[39m\n\u001b[31m      border-radius:var(--r-sm);padding:10px 12px;margin-bottom:8px;\\\">\u001b[39m\n\u001b[31m      <div style=\\\"display:flex;justify-content:space-between;align-items:center;margin-bottom:6px;\\\">\u001b[39m\n\u001b[31m        <span style=\\\"font-family:var(--fnt-mono);font-size:.9rem;color:var(--sky);\\\">${docket}</span>\u001b[39m\n\u001b[31m        <span class=\\\"status-pill sp-${sp}\\\">${sp}</span>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      <div style=\\\"font-size:.75rem;color:var(--t2);font-family:var(--fnt-mono);\\\">${result.lastEvent || '—'}</div>\u001b[39m\n\u001b[31m      ${result.lastScanLocation ? `<div style=\\\"font-size:.68rem;color:var(--t3);margin-top:3px;\\\">📍 ${result.lastScanLocation}</div>` : ''}\u001b[39m\n\u001b[31m      <div style=\\\"display:flex;gap:8px;margin-top:8px;\\\">\u001b[39m\n\u001b[31m        <button class=\\\"btn-sm\\\" onclick=\\\"openCourierTrackUrl('${docket}','${courier}')\\\">🔗 Open on ${courier}</button>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m    </div>`;\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m  saveData();\u001b[39m\n\u001b[31m  area.innerHTML = `<div style=\\\"margin-top:4px;\\\">${resultsHTML}</div>`;\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// Keep old names as aliases for compatibility\u001b[39m\n\u001b[31mfunction trackMultiple() { trackSelected(); }\u001b[39m\n\u001b[31mfunction openCourierSite() { openTrackOnCourierSite(); }·\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31m// HISTORY\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31mfunction renderHistory() {\u001b[39m\n\u001b[31m  const list = document.getElementById('historyList');\u001b[39m\n\u001b[31m  if (!dispatches.length) {\u001b[39m\n\u001b[31m    list.innerHTML = '<div class=\\\"empty\\\"><div class=\\\"e-icon\\\">📭</div><p>No dispatches yet.</p></div>';\u001b[39m\n\u001b[31m    return;\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m  list.innerHTML = dispatches.map(d => {\u001b[39m\n\u001b[31m    const cons = d.consignees?.length || 0;\u001b[39m\n\u001b[31m    const docketed = d.consignees?.filter(c=>c.docket).length || 0;\u001b[39m\n\u001b[31m    const clr = d.status === 'draft' ? 'var(--ylw)' : 'var(--grn)';\u001b[39m\n\u001b[31m    return `\u001b[39m\n\u001b[31m    <div class=\\\"docket-tile\\\" onclick=\\\"showDispatchDetail('${d.id}')\\\">\u001b[39m\n\u001b[31m      <div class=\\\"docket-tile-hdr\\\">\u001b[39m\n\u001b[31m        <div class=\\\"docket-tile-left\\\">\u001b[39m\n\u001b[31m          <div class=\\\"docket-num\\\" style=\\\"font-size:.78rem;\\\">${d.id}</div>\u001b[39m\n\u001b[31m          <div class=\\\"docket-store\\\">${d.client||'No client'} ${d.jobref ? '› '+d.jobref : ''}</div>\u001b[39m\n\u001b[31m          <div class=\\\"docket-courier\\\">📦 ${cons} consignees · ${docketed} docketed · ${d.courier||'—'} · ${d.dispatchDate||d.createdAt?.slice(0,10)||'—'}</div>\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m        <span class=\\\"status-pill\\\" style=\\\"background:rgba(0,0,0,.2);color:${clr};\\\">${d.status||'active'}</span>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m    </div>`;\u001b[39m\n\u001b[31m  }).join('');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction showDispatchDetail(id) {\u001b[39m\n\u001b[31m  const d = dispatches.find(x => x.id === id);\u001b[39m\n\u001b[31m  if (!d) return;\u001b[39m\n\u001b[31m  const content = document.getElementById('detailContent');\u001b[39m\n\u001b[31m  content.innerHTML = `\u001b[39m\n\u001b[31m    <div class=\\\"sheet-title\\\">${d.client} ${d.jobref ? '› '+d.jobref : ''}</div>\u001b[39m\n\u001b[31m    <div class=\\\"info-tag\\\"><strong>${d.id}</strong> · ${d.courier||'—'} · ${d.dispatchDate||'—'}</div>\u001b[39m\n\u001b[31m    <div class=\\\"sec-div\\\"><span>Consignees (${d.consignees?.length||0})</span></div>\u001b[39m\n\u001b[31m    ${(d.consignees||[]).map((c,i) => `\u001b[39m\n\u001b[31m      <div style=\\\"background:var(--sur2);border:1px solid var(--bdr);border-radius:var(--r-sm);padding:10px;margin-bottom:6px;\\\">\u001b[39m\n\u001b[31m        <div style=\\\"font-weight:600;margin-bottom:4px;\\\">${i+1}. ${c.name} — ${c.city}</div>\u001b[39m\n\u001b[31m        ${c.docket ? `<div style=\\\"font-family:var(--fnt-mono);font-size:.78rem;color:var(--sky);\\\">🔖 ${c.docket}</div>` : ''}\u001b[39m\n\u001b[31m        ${c.material ? `<div style=\\\"font-size:.75rem;color:var(--t2);\\\">📦 ${c.material} ${c.qty ? '('+c.qty+')':''}</div>` : ''}\u001b[39m\n\u001b[31m        <span class=\\\"status-pill sp-${c.status||'pending'}\\\" style=\\\"margin-top:4px;display:inline-block;\\\">${c.status||'pending'}</span>\u001b[39m\n\u001b[31m      </div>`).join('')}\u001b[39m\n\u001b[31m    ${d.remarks ? `<div class=\\\"info-tag\\\">📝 ${d.remarks}</div>` : ''}\u001b[39m\n\u001b[31m    <div class=\\\"btn-row\\\" style=\\\"margin-top:14px;\\\">\u001b[39m\n\u001b[31m      <button class=\\\"btn btn-ghost\\\" onclick=\\\"deleteDispatch('${d.id}');closeOverlay('detailOverlay')\\\">🗑️ Delete</button>\u001b[39m\n\u001b[31m      <button class=\\\"btn btn-orange\\\" onclick=\\\"closeOverlay('detailOverlay');showPane('dashboard')\\\">📊 View in Dashboard</button>\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m  `;\u001b[39m\n\u001b[31m  openOverlay('detailOverlay');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction deleteDispatch(id) {\u001b[39m\n\u001b[31m  if (!confirm('Delete this dispatch?')) return;\u001b[39m\n\u001b[31m  dispatches = dispatches.filter(d => d.id !== id);\u001b[39m\n\u001b[31m  saveData();\u001b[39m\n\u001b[31m  updateHistCount();\u001b[39m\n\u001b[31m  renderHistory();\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction updateHistCount() {\u001b[39m\n\u001b[31m  document.getElementById('histCount').textContent = dispatches.length;\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31m// GOOGLE SHEETS\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31masync function logToSheets(d) {\u001b[39m\n\u001b[31m  const url = window.appConfig.webhook_url;\u001b[39m\n\u001b[31m  if (!url) return;\u001b[39m\n\u001b[31m  try {\u001b[39m\n\u001b[31m    await fetch(url, {\u001b[39m\n\u001b[31m      method: 'POST', mode: 'no-cors',\u001b[39m\n\u001b[31m      headers: { 'Content-Type': 'application/json' },\u001b[39m\n\u001b[31m      body: JSON.stringify({\u001b[39m\n\u001b[31m        type: 'dispatch', id: d.id, client: d.client, jobref: d.jobref,\u001b[39m\n\u001b[31m        courier: d.courier, consignees: d.consignees?.length||0,\u001b[39m\n\u001b[31m        destinations: d.consignees?.map(c=>c.city).join(', ')||'',\u001b[39m\n\u001b[31m        dockets: d.consignees?.filter(c=>c.docket).map(c=>c.docket).join(', ')||'',\u001b[39m\n\u001b[31m        dispatchDate: d.dispatchDate, status: d.status,\u001b[39m\n\u001b[31m        timestamp: new Date().toISOString(), company: window.appConfig.company\u001b[39m\n\u001b[31m      })\u001b[39m\n\u001b[31m    });\u001b[39m\n\u001b[31m  } catch(e) {}\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31m// CONFIG — Google Sheet is the source of truth\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════·\u001b[39m\n\u001b[31mfunction onSheetsUrlChange() {\u001b[39m\n\u001b[31m  const url = document.getElementById('cfg_sheets_url').value.trim();\u001b[39m\n\u001b[31m  // Show sync button state\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction saveSheetsUrl() {\u001b[39m\n\u001b[31m  const url = document.getElementById('cfg_sheets_url').value.trim();\u001b[39m\n\u001b[31m  if (!url) { showToast('Enter the Apps Script URL first', 'err'); return; }\u001b[39m\n\u001b[31m  window.appConfig.sheets_url = url;\u001b[39m\n\u001b[31m  saveSheetsUrlLocal();\u001b[39m\n\u001b[31m  syncFromSheet(false);\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m/**\u001b[39m\n\u001b[31m * Fetch all config from Google Sheet and apply to appConfig.\u001b[39m\n\u001b[31m * Called on startup (silent=true) and manually (silent=false).\u001b[39m\n\u001b[31m */\u001b[39m\n\u001b[31masync function syncFromSheet(silent = false) {\u001b[39m\n\u001b[31m  const url = window.appConfig.sheets_url;\u001b[39m\n\u001b[31m  if (!url) { if (!silent) showToast('No Sheets URL configured', 'err'); return; }·\u001b[39m\n\u001b[31m  setSyncStatus('syncing', 'Syncing config from Google Sheet...');\u001b[39m\n\u001b[31m  try {\u001b[39m\n\u001b[31m    const resp = await fetch(url + '?action=pwa_config', {\u001b[39m\n\u001b[31m      method: 'GET',\u001b[39m\n\u001b[31m      redirect: 'follow'\u001b[39m\n\u001b[31m    });\u001b[39m\n\u001b[31m    if (!resp.ok) throw new Error('HTTP ' + resp.status);\u001b[39m\n\u001b[31m    const cfg = await resp.json();·\u001b[39m\n\u001b[31m    // Apply all values from sheet to appConfig\u001b[39m\n\u001b[31m    const keyMap = {\u001b[39m\n\u001b[31m      proxy_url:           'proxy_url',\u001b[39m\n\u001b[31m      webhook_url:         'webhook_url',\u001b[39m\n\u001b[31m      ollama_url:          'ollama_url',\u001b[39m\n\u001b[31m      ocr_mode:            'ocr_mode',\u001b[39m\n\u001b[31m      company:             'company',\u001b[39m\n\u001b[31m      city:                'city',\u001b[39m\n\u001b[31m      auto_log:            'auto_log',\u001b[39m\n\u001b[31m      refresh_on_load:     'refresh_on_load',\u001b[39m\n\u001b[31m      admin_password:      'admin_password',\u001b[39m\n\u001b[31m      n8n_dispatch_webhook:'n8n_dispatch_webhook',\u001b[39m\n\u001b[31m    };\u001b[39m\n\u001b[31m    Object.entries(keyMap).forEach(([sheetKey, cfgKey]) => {\u001b[39m\n\u001b[31m      if (cfg[sheetKey] !== undefined && cfg[sheetKey] !== '') {\u001b[39m\n\u001b[31m        window.appConfig[cfgKey] = cfg[sheetKey];\u001b[39m\n\u001b[31m      }\u001b[39m\n\u001b[31m    });·\u001b[39m\n\u001b[31m    // Apply courier master if provided\u001b[39m\n\u001b[31m    if (cfg._courier_master && Array.isArray(cfg._courier_master)) {\u001b[39m\n\u001b[31m      cfg._courier_master.forEach(c => {\u001b[39m\n\u001b[31m        if (c.name && c.trackUrl) {\u001b[39m\n\u001b[31m          COURIER_MASTER[c.name] = {\u001b[39m\n\u001b[31m            trackUrl: c.trackUrl,\u001b[39m\n\u001b[31m            trackBulkUrl: c.bulkUrl || '',\u001b[39m\n\u001b[31m            maxBulk: parseInt(c.maxBulk) || 1\u001b[39m\n\u001b[31m          };\u001b[39m\n\u001b[31m        }\u001b[39m\n\u001b[31m      });\u001b[39m\n\u001b[31m    }·\u001b[39m\n\u001b[31m    renderCourierMasterList();\u001b[39m\n\u001b[31m    renderSheetConfigDisplay(cfg);\u001b[39m\n\u001b[31m    setSyncStatus('ok', 'Synced from sheet · ' + new Date().toLocaleTimeString());\u001b[39m\n\u001b[31m    if (!silent) showToast('Config synced from sheet ✓', 'ok');·\u001b[39m\n\u001b[31m    // If refresh_on_load is true, trigger dashboard refresh\u001b[39m\n\u001b[31m    if (window.appConfig.refresh_on_load === 'true') {\u001b[39m\n\u001b[31m      setTimeout(refreshAllTracking, 2000);\u001b[39m\n\u001b[31m    }·\u001b[39m\n\u001b[31m  } catch(e) {\u001b[39m\n\u001b[31m    setSyncStatus('err', 'Sync failed: ' + e.message);\u001b[39m\n\u001b[31m    if (!silent) showToast('Sync failed — check URL', 'err');\u001b[39m\n\u001b[31m    console.error('Sheets sync error:', e);\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction setSyncStatus(state, msg) {\u001b[39m\n\u001b[31m  const el = document.getElementById('syncStatus');\u001b[39m\n\u001b[31m  const icon = document.getElementById('syncIcon');\u001b[39m\n\u001b[31m  const msgEl = document.getElementById('syncMsg');\u001b[39m\n\u001b[31m  el.style.display = 'flex';\u001b[39m\n\u001b[31m  icon.textContent = state === 'ok' ? '✅' : state === 'err' ? '❌' : '⏳';\u001b[39m\n\u001b[31m  msgEl.textContent = msg;\u001b[39m\n\u001b[31m  el.style.borderColor = state === 'ok' ? 'rgba(34,197,94,.3)' : state === 'err' ? 'rgba(239,68,68,.3)' : 'rgba(234,179,8,.3)';\u001b[39m\n\u001b[31m  el.style.background = state === 'ok' ? 'rgba(34,197,94,.05)' : state === 'err' ? 'rgba(239,68,68,.05)' : 'rgba(234,179,8,.05)';\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction renderSheetConfigDisplay(cfg) {\u001b[39m\n\u001b[31m  const display = document.getElementById('sheetConfigDisplay');\u001b[39m\n\u001b[31m  display.style.display = 'block';\u001b[39m\n\u001b[31m  document.getElementById('sheetSyncTime').textContent = 'Last sync: ' + new Date().toLocaleTimeString();\u001b[39m\n\u001b[31m  const showKeys = ['proxy_url','webhook_url','ollama_url','ocr_mode','company','city','auto_log'];\u001b[39m\n\u001b[31m  document.getElementById('sheetConfigRows').innerHTML = showKeys.map(k => {\u001b[39m\n\u001b[31m    const val = cfg[k] || window.appConfig[k] || '—';\u001b[39m\n\u001b[31m    const isEmpty = !cfg[k];\u001b[39m\n\u001b[31m    return `<div class=\\\"courier-master-row\\\">\u001b[39m\n\u001b[31m      <div class=\\\"cml\\\">\u001b[39m\n\u001b[31m        <div class=\\\"cml-name\\\" style=\\\"font-family:var(--fnt-mono);font-size:.75rem;\\\">${k}</div>\u001b[39m\n\u001b[31m        <div class=\\\"cml-url\\\" style=\\\"${isEmpty ? 'color:var(--red);' : ''}\\\">${isEmpty ? '⚠️ Not set in sheet' : val}</div>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m    </div>`;\u001b[39m\n\u001b[31m  }).join('');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m/**\u001b[39m\n\u001b[31m * Push current config changes back to the sheet.\u001b[39m\n\u001b[31m * Called when user edits config from PWA (rare — usually done in sheet directly).\u001b[39m\n\u001b[31m */\u001b[39m\n\u001b[31masync function pushConfigToSheet(updates) {\u001b[39m\n\u001b[31m  const url = window.appConfig.sheets_url;\u001b[39m\n\u001b[31m  if (!url) return;\u001b[39m\n\u001b[31m  try {\u001b[39m\n\u001b[31m    await fetch(url, {\u001b[39m\n\u001b[31m      method: 'POST', mode: 'no-cors',\u001b[39m\n\u001b[31m      headers: { 'Content-Type': 'application/json' },\u001b[39m\n\u001b[31m      body: JSON.stringify({ type: 'save_config', config: updates })\u001b[39m\n\u001b[31m    });\u001b[39m\n\u001b[31m  } catch(e) {}\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction clearAll() {\u001b[39m\n\u001b[31m  if (!confirm('Delete ALL dispatch data?')) return;\u001b[39m\n\u001b[31m  dispatches = [];\u001b[39m\n\u001b[31m  saveData();\u001b[39m\n\u001b[31m  updateHistCount();\u001b[39m\n\u001b[31m  renderDashboard();\u001b[39m\n\u001b[31m  renderHistory();\u001b[39m\n\u001b[31m  showToast('Cleared', 'ok');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction renderCourierMasterList() {\u001b[39m\n\u001b[31m  const el = document.getElementById('courierMasterList');\u001b[39m\n\u001b[31m  el.innerHTML = Object.entries(COURIER_MASTER).map(([name, info]) => `\u001b[39m\n\u001b[31m    <div class=\\\"courier-master-row\\\">\u001b[39m\n\u001b[31m      <div class=\\\"cml\\\">\u001b[39m\n\u001b[31m        <div class=\\\"cml-name\\\">${name}</div>\u001b[39m\n\u001b[31m        <div class=\\\"cml-url\\\">${info.trackUrl}</div>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      <button class=\\\"btn-sm\\\" onclick=\\\"window.open('${info.trackBulkUrl||info.trackUrl.replace('{docket}','')}','_blank')\\\">Open</button>\u001b[39m\n\u001b[31m    </div>`).join('');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction openCourierMaster() {\u001b[39m\n\u001b[31m  const ed = document.getElementById('courierMasterEditor');\u001b[39m\n\u001b[31m  ed.innerHTML = Object.entries(COURIER_MASTER).map(([name, info]) => `\u001b[39m\n\u001b[31m    <div style=\\\"margin-bottom:14px;\\\">\u001b[39m\n\u001b[31m      <div style=\\\"font-family:var(--fnt-head);font-size:.75rem;font-weight:700;color:var(--t2);text-transform:uppercase;letter-spacing:1.5px;margin-bottom:6px;\\\">${name}</div>\u001b[39m\n\u001b[31m      <div class=\\\"f\\\" style=\\\"margin-bottom:6px;\\\">\u001b[39m\n\u001b[31m        <label>Track URL (use {docket})</label>\u001b[39m\n\u001b[31m        <input type=\\\"text\\\" value=\\\"${info.trackUrl}\\\" id=\\\"cm_${name.replace(/\\\\s/g,'_')}\\\"\u001b[39m\n\u001b[31m          style=\\\"font-family:var(--fnt-mono);font-size:.75rem;\\\">\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m    </div>`).join('') +\u001b[39m\n\u001b[31m    `<button class=\\\"btn btn-orange\\\" onclick=\\\"saveCourierMaster()\\\">Save Courier Master</button>`;\u001b[39m\n\u001b[31m  openOverlay('courierMasterOverlay');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction saveCourierMaster() {\u001b[39m\n\u001b[31m  Object.keys(COURIER_MASTER).forEach(name => {\u001b[39m\n\u001b[31m    const el = document.getElementById('cm_' + name.replace(/\\\\s/g,'_'));\u001b[39m\n\u001b[31m    if (el) COURIER_MASTER[name].trackUrl = el.value;\u001b[39m\n\u001b[31m  });\u001b[39m\n\u001b[31m  closeOverlay('courierMasterOverlay');\u001b[39m\n\u001b[31m  renderCourierMasterList();\u001b[39m\n\u001b[31m  showToast('Courier master saved ✓', 'ok');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31m// UI UTILS\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31m// ── ADMIN AUTH ───────────────────────────────────────────────────────────\u001b[39m\n\u001b[31mlet _adminAuthed = false;\u001b[39m\n\u001b[31mlet _lastAdminPane = null;·\u001b[39m\n\u001b[31mfunction showPane(name) {\u001b[39m\n\u001b[31m  // Config tab requires admin auth — re-ask if navigated away\u001b[39m\n\u001b[31m  if (name === 'config') {\u001b[39m\n\u001b[31m    if (!_adminAuthed) {\u001b[39m\n\u001b[31m      _lastAdminPane = 'config';\u001b[39m\n\u001b[31m      showAdminLogin();\u001b[39m\n\u001b[31m      return;\u001b[39m\n\u001b[31m    }\u001b[39m\n\u001b[31m  } else {\u001b[39m\n\u001b[31m    // User navigated away from config — revoke auth\u001b[39m\n\u001b[31m    if (_lastAdminPane === 'config') _adminAuthed = false;\u001b[39m\n\u001b[31m    _lastAdminPane = name;\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m  document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));\u001b[39m\n\u001b[31m  document.querySelectorAll('.pane').forEach(p => p.classList.remove('active'));\u001b[39m\n\u001b[31m  document.getElementById('tab-'+name).classList.add('active');\u001b[39m\n\u001b[31m  document.getElementById('pane-'+name).classList.add('active');\u001b[39m\n\u001b[31m  if (name === 'dashboard') renderDashboard();\u001b[39m\n\u001b[31m  if (name === 'history') renderHistory();\u001b[39m\n\u001b[31m  if (name === 'track') buildTrackDocketChips();\u001b[39m\n\u001b[31m  if (name === 'config') { document.getElementById('cfg_sheets_url').value = window.appConfig.sheets_url || ''; }\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction showAdminLogin() {\u001b[39m\n\u001b[31m  // Password replaced by hub identity — only harish/pramod can access config\u001b[39m\n\u001b[31m  if (['harish', 'pramod'].includes(window._hubUser?.id)) {\u001b[39m\n\u001b[31m    _adminAuthed = true;\u001b[39m\n\u001b[31m    showPane('config');\u001b[39m\n\u001b[31m  } else {\u001b[39m\n\u001b[31m    showToast('Config access restricted to admin users', 'err');\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31masync function submitAdminLogin() {\u001b[39m\n\u001b[31m  showAdminLogin(); // no-op shim — kept for any lingering HTML onclick refs\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31mfunction openOverlay(id) { document.getElementById(id).classList.add('open'); }\u001b[39m\n\u001b[31mfunction closeOverlay(id) { document.getElementById(id).classList.remove('open'); }·\u001b[39m\n\u001b[31mlet toastTimer;\u001b[39m\n\u001b[31mfunction showToast(msg, type='') {\u001b[39m\n\u001b[31m  const t = document.getElementById('toast');\u001b[39m\n\u001b[31m  t.textContent = msg;\u001b[39m\n\u001b[31m  t.className = 'toast show' + (type ? ' '+type : '');\u001b[39m\n\u001b[31m  clearTimeout(toastTimer);\u001b[39m\n\u001b[31m  toastTimer = setTimeout(() => t.classList.remove('show'), 2800);\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction showOCRWorking(show, msg='') {\u001b[39m\n\u001b[31m  const el = document.getElementById('ocrWorking');\u001b[39m\n\u001b[31m  el.classList.toggle('visible', show);\u001b[39m\n\u001b[31m  if (msg) document.getElementById('ocrWorkingText').textContent = msg;\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction showAIProgress(label, pct) {\u001b[39m\n\u001b[31m  const el = document.getElementById('aiProgress');\u001b[39m\n\u001b[31m  el.classList.add('visible');\u001b[39m\n\u001b[31m  document.getElementById('aiProgLabel').textContent = label;\u001b[39m\n\u001b[31m  document.getElementById('aiProgFill').style.width = pct + '%';\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31mfunction hideAIProgress() {\u001b[39m\n\u001b[31m  document.getElementById('aiProgress').classList.remove('visible');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction updateNetDot() {\u001b[39m\n\u001b[31m  document.getElementById('netDot').style.background = navigator.onLine ? 'var(--grn)' : 'var(--red)';\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction setToday() {\u001b[39m\n\u001b[31m  document.getElementById('n_date').value = new Date().toISOString().split('T')[0];\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m// ── EXPORT CLEAN SHEET ───────────────────────────────────────────────────\u001b[39m\n\u001b[31mfunction exportCleanSheet() {\u001b[39m\n\u001b[31m  const pane = document.getElementById('verifyPane');\u001b[39m\n\u001b[31m  const d = pane._pendingDispatch;\u001b[39m\n\u001b[31m  if (!d || !d.consignees?.length) { showToast('No data to export', 'err'); return; }·\u001b[39m\n\u001b[31m  // Build CSV\u001b[39m\n\u001b[31m  const headers = ['S.No.','Store Name','City','Pincode','Phone','Address','Docket No.','Material','Courier','Status'];\u001b[39m\n\u001b[31m  const rows = d.consignees.map((c, i) => [\u001b[39m\n\u001b[31m    i + 1,\u001b[39m\n\u001b[31m    `\\\"${(c.name||'').replace(/\\\"/g,'\\\"\\\"')}\\\"`,\u001b[39m\n\u001b[31m    `\\\"${(c.city||'').replace(/\\\"/g,'\\\"\\\"')}\\\"`,\u001b[39m\n\u001b[31m    c.pincode || '',\u001b[39m\n\u001b[31m    c.phone || '',\u001b[39m\n\u001b[31m    `\\\"${(c.address||'').replace(/\\\"/g,'\\\"\\\"')}\\\"`,\u001b[39m\n\u001b[31m    c.docket || '',\u001b[39m\n\u001b[31m    `\\\"${(c.material||'').replace(/\\\"/g,'\\\"\\\"')}\\\"`,\u001b[39m\n\u001b[31m    d.courier || '',\u001b[39m\n\u001b[31m    c.status || 'pending'\u001b[39m\n\u001b[31m  ]);·\u001b[39m\n\u001b[31m  const csv = [headers.join(','), ...rows.map(r => r.join(','))].join('\\\\n');\u001b[39m\n\u001b[31m  const blob = new Blob([csv], { type: 'text/csv' });\u001b[39m\n\u001b[31m  const url = URL.createObjectURL(blob);\u001b[39m\n\u001b[31m  const a = document.createElement('a');\u001b[39m\n\u001b[31m  a.href = url;\u001b[39m\n\u001b[31m  a.download = `360LM_Dispatch_${d.client||'export'}_${d.dispatchDate||new Date().toISOString().slice(0,10)}_clean.csv`;\u001b[39m\n\u001b[31m  a.click();\u001b[39m\n\u001b[31m  URL.revokeObjectURL(url);\u001b[39m\n\u001b[31m  showToast('Clean sheet exported ✓', 'ok');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// ── DOCKET SLIP UPLOAD — SKIP OCR IF DOCKET ALREADY MAPPED ───────────────\u001b[39m\n\u001b[31mfunction shouldSkipOCR() {\u001b[39m\n\u001b[31m  // If any consignee already has a docket (from store list mapping), skip OCR extraction\u001b[39m\n\u001b[31m  const consList = document.getElementById('consList');\u001b[39m\n\u001b[31m  if (!consList) return false;\u001b[39m\n\u001b[31m  const cards = consList.querySelectorAll('.cons-card');\u001b[39m\n\u001b[31m  let hasAnyDocket = false;\u001b[39m\n\u001b[31m  cards.forEach(card => {\u001b[39m\n\u001b[31m    const d = card.querySelector('.cons-docket-input')?.value?.trim();\u001b[39m\n\u001b[31m    if (d) hasAnyDocket = true;\u001b[39m\n\u001b[31m  });\u001b[39m\n\u001b[31m  return hasAnyDocket;\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// ─── TRACKING UPDATE TO SHEETS ──────────────────────────────────────────\u001b[39m\n\u001b[31masync function logTrackingUpdate(docket, status, lastEvent, location) {\u001b[39m\n\u001b[31m  const url = window.appConfig.webhook_url;\u001b[39m\n\u001b[31m  if (!url) return;\u001b[39m\n\u001b[31m  try {\u001b[39m\n\u001b[31m    await fetch(url, {\u001b[39m\n\u001b[31m      method: 'POST', mode: 'no-cors',\u001b[39m\n\u001b[31m      headers: { 'Content-Type': 'application/json' },\u001b[39m\n\u001b[31m      body: JSON.stringify({\u001b[39m\n\u001b[31m        type: 'tracking_update',\u001b[39m\n\u001b[31m        docket, status, lastEvent, location,\u001b[39m\n\u001b[31m        timestamp: new Date().toISOString()\u001b[39m\n\u001b[31m      })\u001b[39m\n\u001b[31m    });\u001b[39m\n\u001b[31m  } catch(e) {}\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// ─── POD / RECIPIENT PHOTO UPLOAD ───────────────────────────────────────\u001b[39m\n\u001b[31m// Stores base64 locally, pushes to Sheets webhook (Apps Script saves to Drive)\u001b[39m\n\u001b[31mfunction showPODUpload(docket, dispatchId) {\u001b[39m\n\u001b[31m  const overlay = document.getElementById('podOverlay');\u001b[39m\n\u001b[31m  overlay.dataset.docket = docket;\u001b[39m\n\u001b[31m  overlay.dataset.dispatchId = dispatchId;\u001b[39m\n\u001b[31m  document.getElementById('podPreview').innerHTML = '';\u001b[39m\n\u001b[31m  document.getElementById('podFileInput').value = '';\u001b[39m\n\u001b[31m  document.getElementById('recipientFileInput').value = '';\u001b[39m\n\u001b[31m  openOverlay('podOverlay');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31masync function uploadPODFiles() {\u001b[39m\n\u001b[31m  const overlay = document.getElementById('podOverlay');\u001b[39m\n\u001b[31m  const docket = overlay.dataset.docket;\u001b[39m\n\u001b[31m  const dispatchId = overlay.dataset.dispatchId;\u001b[39m\n\u001b[31m  const podFile = document.getElementById('podFileInput').files[0];\u001b[39m\n\u001b[31m  const recipFile = document.getElementById('recipientFileInput').files[0];\u001b[39m\n\u001b[31m  if (!podFile && !recipFile) { showToast('Select at least one photo', 'err'); return; }·\u001b[39m\n\u001b[31m  showToast('Uploading...', 'info');\u001b[39m\n\u001b[31m  const toBase64 = f => new Promise((res,rej) => {\u001b[39m\n\u001b[31m    const r = new FileReader(); r.onload = e => res(e.target.result); r.onerror = rej; r.readAsDataURL(f);\u001b[39m\n\u001b[31m  });·\u001b[39m\n\u001b[31m  const payload = {\u001b[39m\n\u001b[31m    type: 'pod_upload',\u001b[39m\n\u001b[31m    docket, dispatchId,\u001b[39m\n\u001b[31m    timestamp: new Date().toISOString()\u001b[39m\n\u001b[31m  };\u001b[39m\n\u001b[31m  if (podFile)   payload.pod_image = await toBase64(podFile);\u001b[39m\n\u001b[31m  if (recipFile) payload.recipient_image = await toBase64(recipFile);·\u001b[39m\n\u001b[31m  // Update local store\u001b[39m\n\u001b[31m  for (const d of dispatches) {\u001b[39m\n\u001b[31m    if (d.id !== dispatchId) continue;\u001b[39m\n\u001b[31m    for (const c of d.consignees) {\u001b[39m\n\u001b[31m      if (c.docket === docket) {\u001b[39m\n\u001b[31m        c.podUploaded = true;\u001b[39m\n\u001b[31m        c.status = 'delivered';\u001b[39m\n\u001b[31m      }\u001b[39m\n\u001b[31m    }\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m  saveData();·\u001b[39m\n\u001b[31m  // Push to Sheets (Apps Script saves images to Drive, stores URL in sheet)\u001b[39m\n\u001b[31m  if (window.appConfig.webhook_url) {\u001b[39m\n\u001b[31m    try {\u001b[39m\n\u001b[31m      await fetch(window.appConfig.webhook_url, {\u001b[39m\n\u001b[31m        method: 'POST', mode: 'no-cors',\u001b[39m\n\u001b[31m        headers: { 'Content-Type': 'application/json' },\u001b[39m\n\u001b[31m        body: JSON.stringify(payload)\u001b[39m\n\u001b[31m      });\u001b[39m\n\u001b[31m    } catch(e) {}\u001b[39m\n\u001b[31m  }·\u001b[39m\n\u001b[31m  closeOverlay('podOverlay');\u001b[39m\n\u001b[31m  renderDashboard();\u001b[39m\n\u001b[31m  showToast('POD uploaded ✓ — marked as delivered', 'ok');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// ─── WHATSAPP DISPATCH TABLE SHARE ──────────────────────────────────────\u001b[39m\n\u001b[31mfunction shareDispatchToWhatsApp(dispatchId) {\u001b[39m\n\u001b[31m  const d = dispatches.find(x => x.id === dispatchId);\u001b[39m\n\u001b[31m  if (!d) return;\u001b[39m\n\u001b[31m  const lines = [\u001b[39m\n\u001b[31m    `📦 *Dispatch Update — ${d.client} ${d.jobref ? '› '+d.jobref : ''}*`,\u001b[39m\n\u001b[31m    `🚚 Courier: ${d.courier} | Date: ${d.dispatchDate || d.createdAt?.slice(0,10)}`,\u001b[39m\n\u001b[31m    ``,\u001b[39m\n\u001b[31m    ...d.consignees.filter(c => c.docket).map((c,i) =>\u001b[39m\n\u001b[31m      `${i+1}. *${c.name}* — ${c.city}\\\\n   Docket: ${c.docket} | Status: ${c.status||'booked'}`\u001b[39m\n\u001b[31m    ),\u001b[39m\n\u001b[31m    ``,\u001b[39m\n\u001b[31m    `_360 Degree Logical Marketing_`\u001b[39m\n\u001b[31m  ];\u001b[39m\n\u001b[31m  const msg = encodeURIComponent(lines.join('\\\\n'));\u001b[39m\n\u001b[31m  window.open('https://wa.me/?text=' + msg, '_blank');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// Trigger n8n webhook for automated WhatsApp/email share\u001b[39m\n\u001b[31masync function triggerN8nShare(dispatchId, channel) {\u001b[39m\n\u001b[31m  const d = dispatches.find(x => x.id === dispatchId);\u001b[39m\n\u001b[31m  if (!d) return;\u001b[39m\n\u001b[31m  const n8nUrl = window.appConfig.n8n_dispatch_webhook;\u001b[39m\n\u001b[31m  if (!n8nUrl) { showToast('n8n webhook not configured in Config sheet', 'err'); return; }\u001b[39m\n\u001b[31m  try {\u001b[39m\n\u001b[31m    await fetch(n8nUrl, {\u001b[39m\n\u001b[31m      method: 'POST',\u001b[39m\n\u001b[31m      headers: { 'Content-Type': 'application/json' },\u001b[39m\n\u001b[31m      body: JSON.stringify({\u001b[39m\n\u001b[31m        channel, // 'whatsapp' or 'email'\u001b[39m\n\u001b[31m        dispatch: d,\u001b[39m\n\u001b[31m        timestamp: new Date().toISOString()\u001b[39m\n\u001b[31m      })\u001b[39m\n\u001b[31m    });\u001b[39m\n\u001b[31m    showToast(`Sent to n8n for ${channel} delivery ✓`, 'ok');\u001b[39m\n\u001b[31m  } catch(e) {\u001b[39m\n\u001b[31m    showToast('n8n webhook failed', 'err');\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction previewPOD(input, type) {\u001b[39m\n\u001b[31m  const f = input.files[0];\u001b[39m\n\u001b[31m  if (!f) return;\u001b[39m\n\u001b[31m  const r = new FileReader();\u001b[39m\n\u001b[31m  r.onload = e => {\u001b[39m\n\u001b[31m    const prev = document.getElementById('podPreview');\u001b[39m\n\u001b[31m    const existing = document.getElementById('pod_prev_' + type);\u001b[39m\n\u001b[31m    if (existing) existing.remove();\u001b[39m\n\u001b[31m    const img = document.createElement('img');\u001b[39m\n\u001b[31m    img.id = 'pod_prev_' + type;\u001b[39m\n\u001b[31m    img.src = e.target.result;\u001b[39m\n\u001b[31m    img.style.cssText = 'width:80px;height:80px;object-fit:cover;border-radius:6px;border:1px solid var(--bdr);';\u001b[39m\n\u001b[31m    prev.appendChild(img);\u001b[39m\n\u001b[31m  };\u001b[39m\n\u001b[31m  r.readAsDataURL(f);\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// ─── MANUAL TRACKING ─────────────────────────────────────────────────────\u001b[39m\n\u001b[31mfunction showManualTracking(docket, dispatchId, storeName, phone, city) {\u001b[39m\n\u001b[31m  const ov = document.getElementById('manualTrackOverlay');\u001b[39m\n\u001b[31m  ov.dataset.docket = docket;\u001b[39m\n\u001b[31m  ov.dataset.dispatchId = dispatchId;\u001b[39m\n\u001b[31m  ov.dataset.storeName = storeName;\u001b[39m\n\u001b[31m  ov.dataset.city = city;·\u001b[39m\n\u001b[31m  document.getElementById('mtDocketDisplay').textContent =\u001b[39m\n\u001b[31m    docket + '  ·  ' + storeName + '  ·  ' + city;\u001b[39m\n\u001b[31m  document.getElementById('mtContactUsed').value = phone || '';\u001b[39m\n\u001b[31m  document.getElementById('mtContactPerson').value = '';\u001b[39m\n\u001b[31m  document.getElementById('mtRemarks').value = '';\u001b[39m\n\u001b[31m  document.getElementById('mtStatus').value = '';\u001b[39m\n\u001b[31m  document.getElementById('mtCalledBy').value = '';·\u001b[39m\n\u001b[31m  // Set current datetime\u001b[39m\n\u001b[31m  const now = new Date();\u001b[39m\n\u001b[31m  const pad = n => String(n).padStart(2,'0');\u001b[39m\n\u001b[31m  document.getElementById('mtCallTime').value =\u001b[39m\n\u001b[31m    `${now.getFullYear()}-${pad(now.getMonth()+1)}-${pad(now.getDate())}T${pad(now.getHours())}:${pad(now.getMinutes())}`;·\u001b[39m\n\u001b[31m  openOverlay('manualTrackOverlay');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31masync function submitManualTracking() {\u001b[39m\n\u001b[31m  const ov = document.getElementById('manualTrackOverlay');\u001b[39m\n\u001b[31m  const docket     = ov.dataset.docket;\u001b[39m\n\u001b[31m  const dispatchId = ov.dataset.dispatchId;\u001b[39m\n\u001b[31m  const status     = document.getElementById('mtStatus').value;\u001b[39m\n\u001b[31m  const calledBy   = document.getElementById('mtCalledBy').value.trim();·\u001b[39m\n\u001b[31m  if (!status)    { showToast('Select a status', 'err'); return; }\u001b[39m\n\u001b[31m  if (!calledBy)  { showToast('Enter your name', 'err'); return; }·\u001b[39m\n\u001b[31m  const payload = {\u001b[39m\n\u001b[31m    type:          'manual_tracking',\u001b[39m\n\u001b[31m    docket,        dispatchId,\u001b[39m\n\u001b[31m    storeName:     ov.dataset.storeName,\u001b[39m\n\u001b[31m    city:          ov.dataset.city,\u001b[39m\n\u001b[31m    manualStatus:  status,\u001b[39m\n\u001b[31m    calledBy,\u001b[39m\n\u001b[31m    contactUsed:   document.getElementById('mtContactUsed').value.trim(),\u001b[39m\n\u001b[31m    contactPerson: document.getElementById('mtContactPerson').value.trim(),\u001b[39m\n\u001b[31m    remarks:       document.getElementById('mtRemarks').value.trim(),\u001b[39m\n\u001b[31m    callTime:      document.getElementById('mtCallTime').value || new Date().toISOString(),\u001b[39m\n\u001b[31m    timestamp:     new Date().toISOString()\u001b[39m\n\u001b[31m  };·\u001b[39m\n\u001b[31m  // Update local dispatch record\u001b[39m\n\u001b[31m  for (const d of dispatches) {\u001b[39m\n\u001b[31m    if (d.id !== dispatchId) continue;\u001b[39m\n\u001b[31m    for (const c of d.consignees) {\u001b[39m\n\u001b[31m      if (c.docket === docket) {\u001b[39m\n\u001b[31m        c.manualStatus    = status;\u001b[39m\n\u001b[31m        c.manualCalledBy  = calledBy;\u001b[39m\n\u001b[31m        c.manualTime      = payload.callTime;\u001b[39m\n\u001b[31m        c.manualRemarks   = payload.remarks;\u001b[39m\n\u001b[31m        // If confirmed delivered — flag so dashboard shows manual badge\u001b[39m\n\u001b[31m        // (not permanently locked — use resumeAutoTracking() to re-enable)\u001b[39m\n\u001b[31m        if (status === 'delivered') {\u001b[39m\n\u001b[31m          c.status = 'delivered';\u001b[39m\n\u001b[31m          c.manualDelivered = true;\u001b[39m\n\u001b[31m        } else {\u001b[39m\n\u001b[31m          // Non-delivered manual update: clear any previous lock\u001b[39m\n\u001b[31m          c.manualDelivered = false;\u001b[39m\n\u001b[31m        }\u001b[39m\n\u001b[31m      }\u001b[39m\n\u001b[31m    }\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m  saveData();\u001b[39m\n\u001b[31m  renderDashboard();\u001b[39m\n\u001b[31m  // Sync updated consignee to Postgres\u001b[39m\n\u001b[31m  const _d = dispatches.find(x => x.id === dispatchId);\u001b[39m\n\u001b[31m  if (_d) {\u001b[39m\n\u001b[31m    const _idx = _d.consignees.findIndex(c => c.docket === docket);\u001b[39m\n\u001b[31m    if (_idx >= 0) syncConsigneeToDB(_d.consignees[_idx], dispatchId, _idx);\u001b[39m\n\u001b[31m  }·\u001b[39m\n\u001b[31m  // Log to Sheets\u001b[39m\n\u001b[31m  if (window.appConfig.webhook_url) {\u001b[39m\n\u001b[31m    try {\u001b[39m\n\u001b[31m      await fetch(window.appConfig.webhook_url, {\u001b[39m\n\u001b[31m        method: 'POST', mode: 'no-cors',\u001b[39m\n\u001b[31m        headers: { 'Content-Type': 'application/json' },\u001b[39m\n\u001b[31m        body: JSON.stringify(payload)\u001b[39m\n\u001b[31m      });\u001b[39m\n\u001b[31m    } catch(e) {}\u001b[39m\n\u001b[31m  }·\u001b[39m\n\u001b[31m  closeOverlay('manualTrackOverlay');\u001b[39m\n\u001b[31m  showToast('Manual tracking logged ✓', 'ok');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// ─── TILE SELECTION + PARTIAL REPORT SHARE ───────────────────────────────\u001b[39m\n\u001b[31mlet _selectedDockets = new Set();·\u001b[39m\n\u001b[31mfunction toggleTileSelect(docket) {\u001b[39m\n\u001b[31m  if (_selectedDockets.has(docket)) _selectedDockets.delete(docket);\u001b[39m\n\u001b[31m  else _selectedDockets.add(docket);\u001b[39m\n\u001b[31m  // Update checkbox visual\u001b[39m\n\u001b[31m  const chk = document.getElementById('sel_' + docket.replace(/[^a-z0-9]/gi,'_'));\u001b[39m\n\u001b[31m  if (chk) chk.checked = _selectedDockets.has(docket);\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction shareSelectedReport() {\u001b[39m\n\u001b[31m  if (_selectedDockets.size === 0) {\u001b[39m\n\u001b[31m    showToast('Select at least one consignee tile first', 'err');\u001b[39m\n\u001b[31m    return;\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m  const activeDis = dispatches.filter(d => d.status !== 'draft');\u001b[39m\n\u001b[31m  const allItems  = activeDis.flatMap(d =>\u001b[39m\n\u001b[31m    (d.consignees||[]).map(c => Object.assign({}, c, {courier:d.courier, client:d.client, jobref:d.jobref, dispatchId:d.id}))\u001b[39m\n\u001b[31m  );\u001b[39m\n\u001b[31m  const selected = allItems.filter(c => _selectedDockets.has(c.docket));\u001b[39m\n\u001b[31m  if (!selected.length) { showToast('Selected dockets not found', 'err'); return; }·\u001b[39m\n\u001b[31m  const NL = '\\\\n';\u001b[39m\n\u001b[31m  const statusEmoji = {\u001b[39m\n\u001b[31m    delivered:'\\\\u2705', transit:'\\\\uD83D\\\\uDE9A', 'in-transit':'\\\\uD83D\\\\uDE9A',\u001b[39m\n\u001b[31m    booked:'\\\\uD83D\\\\uDCEC', pending:'\\\\u23F3', 'not received':'\\\\u274C', forwarded:'\\\\u27A1'\u001b[39m\n\u001b[31m  };·\u001b[39m\n\u001b[31m  const header = '\\\\uD83D\\\\uDCE6 *Dispatch Status Report*' + NL +\u001b[39m\n\u001b[31m    (selected[0].client || '') + ' ' + (selected[0].jobref ? '> ' + selected[0].jobref : '') + NL +\u001b[39m\n\u001b[31m    new Date().toLocaleDateString('en-IN', {day:'2-digit',month:'short',year:'numeric'}) + NL + NL;·\u001b[39m\n\u001b[31m  const rows = selected.map(function(c, i) {\u001b[39m\n\u001b[31m    const finalStatus = c.manualStatus === 'delivered' ? 'delivered' : (c.status || 'booked');\u001b[39m\n\u001b[31m    const emoji  = statusEmoji[finalStatus] || '\\\\uD83D\\\\uDCE6';\u001b[39m\n\u001b[31m    const source = c.manualStatus ? '(manual)' : '(auto)';\u001b[39m\n\u001b[31m    const remark = c.manualRemarks ? (NL + '   Note: ' + c.manualRemarks) : '';\u001b[39m\n\u001b[31m    return (i+1) + '. *' + c.name + '* - ' + c.city + NL +\u001b[39m\n\u001b[31m      '   ' + emoji + ' ' + finalStatus + ' ' + source + NL +\u001b[39m\n\u001b[31m      '   Docket: ' + c.docket + remark;\u001b[39m\n\u001b[31m  }).join(NL + NL);·\u001b[39m\n\u001b[31m  const footer = NL + NL + '_360 Degree Logical Marketing_';\u001b[39m\n\u001b[31m  window.open('https://wa.me/?text=' + encodeURIComponent(header + rows + footer), '_blank');\u001b[39m\n\u001b[31m  showToast('Report for ' + selected.length + ' consignees ready', 'ok');\u001b[39m\n\u001b[31m}··\u001b[39m\n\u001b[31m// ── resumeAutoTracking: undo a manual-delivered lock ──────────────────────\u001b[39m\n\u001b[31mfunction resumeAutoTracking(docket, dispatchId) {\u001b[39m\n\u001b[31m  for (const d of dispatches) {\u001b[39m\n\u001b[31m    if (d.id !== dispatchId) continue;\u001b[39m\n\u001b[31m    for (const c of d.consignees) {\u001b[39m\n\u001b[31m      if (c.docket === docket) {\u001b[39m\n\u001b[31m        c.manualDelivered = false;\u001b[39m\n\u001b[31m        c.status          = 'transit';\u001b[39m\n\u001b[31m        c.manualStatus    = '';\u001b[39m\n\u001b[31m      }\u001b[39m\n\u001b[31m    }\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m  saveData();\u001b[39m\n\u001b[31m  renderDashboard();\u001b[39m\n\u001b[31m  showToast(`Auto-tracking resumed for ${docket}`, 'ok');\u001b[39m\n\u001b[31m  const _rd = dispatches.find(x => x.id === dispatchId);\u001b[39m\n\u001b[31m  if (_rd) {\u001b[39m\n\u001b[31m    const _ri = _rd.consignees.findIndex(c => c.docket === docket);\u001b[39m\n\u001b[31m    if (_ri >= 0) syncConsigneeToDB(_rd.consignees[_ri], dispatchId, _ri);\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// ── refreshAllTracking (consolidated, try/finally for safety) ─────────────\u001b[39m\n\u001b[31masync function refreshAllTracking() {\u001b[39m\n\u001b[31m  const activeDis  = dispatches.filter(d => d.status !== 'draft');\u001b[39m\n\u001b[31m  const allCons    = activeDis.flatMap(d =>\u001b[39m\n\u001b[31m    (d.consignees || []).map(c => ({ ...c, dispatchId: d.id, courier: d.courier }))\u001b[39m\n\u001b[31m  );\u001b[39m\n\u001b[31m  const withDockets = allCons.filter(c =>\u001b[39m\n\u001b[31m    c.docket && c.status !== 'delivered' && !c.manualDelivered\u001b[39m\n\u001b[31m  );\u001b[39m\n\u001b[31m  if (!withDockets.length) { showToast('No active dockets to refresh', 'info'); return; }·\u001b[39m\n\u001b[31m  showToast(`⏳ Tracking ${withDockets.length} dockets...`, 'info');\u001b[39m\n\u001b[31m  withDockets.forEach(c => {\u001b[39m\n\u001b[31m    const id    = c.docket.replace(/[^a-z0-9]/gi, '_');\u001b[39m\n\u001b[31m    const evtEl = document.getElementById('evt_'  + id);\u001b[39m\n\u001b[31m    const spnEl = document.getElementById('spin_' + id);\u001b[39m\n\u001b[31m    if (evtEl) evtEl.style.display = 'none';\u001b[39m\n\u001b[31m    if (spnEl) spnEl.style.display = 'inline';\u001b[39m\n\u001b[31m  });·\u001b[39m\n\u001b[31m  let done = 0;\u001b[39m\n\u001b[31m  try {\u001b[39m\n\u001b[31m    for (const c of withDockets) {\u001b[39m\n\u001b[31m      const result = await fetchTracking(c.docket, c.courier || '');\u001b[39m\n\u001b[31m      const id    = c.docket.replace(/[^a-z0-9]/gi, '_');·\u001b[39m\n\u001b[31m      // Batch-write result into state (never touch DOM here)\u001b[39m\n\u001b[31m      if (result.status && result.status !== 'failed') {\u001b[39m\n\u001b[31m        for (const d of dispatches) {\u001b[39m\n\u001b[31m          if (d.id !== c.dispatchId) continue;\u001b[39m\n\u001b[31m          for (const con of d.consignees) {\u001b[39m\n\u001b[31m            if (con.docket === c.docket && !con.manualDelivered) {\u001b[39m\n\u001b[31m              con.status           = result.status;\u001b[39m\n\u001b[31m              con.lastEvent        = result.lastEvent || con.lastEvent;\u001b[39m\n\u001b[31m              con.lastScanLocation = result.lastScanLocation || '';\u001b[39m\n\u001b[31m              con.lastTrackedAt    = new Date().toISOString();\u001b[39m\n\u001b[31m            }\u001b[39m\n\u001b[31m          }\u001b[39m\n\u001b[31m        }\u001b[39m\n\u001b[31m      }·\u001b[39m\n\u001b[31m      // Update tile spinner/event text in DOM (cosmetic only)\u001b[39m\n\u001b[31m      const spnEl = document.getElementById('spin_' + id);\u001b[39m\n\u001b[31m      const evtEl = document.getElementById('evt_'  + id);\u001b[39m\n\u001b[31m      if (spnEl) spnEl.style.display = 'none';\u001b[39m\n\u001b[31m      if (evtEl) { evtEl.style.display = 'inline'; evtEl.textContent = result.lastEvent || 'Checked'; }·\u001b[39m\n\u001b[31m      if (result.status && result.status !== 'failed' && window.appConfig.webhook_url) {\u001b[39m\n\u001b[31m        logTrackingUpdate(c.docket, result.status, result.lastEvent || '', result.lastScanLocation || '');\u001b[39m\n\u001b[31m      }\u001b[39m\n\u001b[31m      done++;\u001b[39m\n\u001b[31m    }\u001b[39m\n\u001b[31m    showToast(`✓ ${done} dockets refreshed`, 'ok');\u001b[39m\n\u001b[31m  } finally {\u001b[39m\n\u001b[31m    // Always persist and re-render — even if loop throws mid-way\u001b[39m\n\u001b[31m    saveData();\u001b[39m\n\u001b[31m    renderDashboard();\u001b[39m\n\u001b[31m    // Push updated consignee states to Postgres (fire-and-forget)\u001b[39m\n\u001b[31m    dispatches.filter(d => d.status !== 'draft').forEach(d => syncDispatchToDB(d));\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m</script>\u001b[39m\n\u001b[31m</body>\u001b[39m\n\u001b[31m</html>\"\u001b[39m\n    at /var/www/360lm/tests/dispatch.spec.js:53:18",
                        "location": {
                          "file": "/var/www/360lm/tests/dispatch.spec.js",
                          "column": 18,
                          "line": 53
                        },
                        "snippet": "  51 |\n  52 |   test('hub auth gate present', () => {\n> 53 |     expect(html).toContain(\"window.location.replace('/hub/')\");\n     |                  ^\n  54 |     expect(html).toContain('_hubUser');\n  55 |   });\n  56 | });"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/dispatch.spec.js",
                            "column": 18,
                            "line": 53
                          },
                          "message": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoContain\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m) // indexOf\u001b[22m\n\nExpected substring: \u001b[32m\"window.location.replace('/hub/')\"\u001b[39m\nReceived string:    \u001b[31m\"<!DOCTYPE html>\u001b[39m\n\u001b[31m<html lang=\\\"en\\\">\u001b[39m\n\u001b[31m<head>\u001b[39m\n\u001b[31m<meta charset=\\\"UTF-8\\\">\u001b[39m\n\u001b[31m<meta name=\\\"viewport\\\" content=\\\"width=device-width,initial-scale=1,maximum-scale=1,user-scalable=no\\\">\u001b[39m\n\u001b[31m<meta name=\\\"theme-color\\\" content=\\\"#080c12\\\">\u001b[39m\n\u001b[31m<meta name=\\\"apple-mobile-web-app-capable\\\" content=\\\"yes\\\">\u001b[39m\n\u001b[31m<meta name=\\\"mobile-web-app-capable\\\" content=\\\"yes\\\">\u001b[39m\n\u001b[31m<title>360LM Dispatch</title>\u001b[39m\n\u001b[31m<link rel=\\\"manifest\\\" href=\\\"manifest.json\\\">\u001b[39m\n\u001b[31m<link href=\\\"https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@300;400;500;600;700&family=Fira+Code:wght@400;500&family=Barlow+Condensed:wght@400;600;700;800&display=swap\\\" rel=\\\"stylesheet\\\">\u001b[39m\n\u001b[31m<style>\u001b[39m\n\u001b[31m/* ════ RESET & VARS ════ */\u001b[39m\n\u001b[31m*{margin:0;padding:0;box-sizing:border-box;-webkit-tap-highlight-color:transparent;}\u001b[39m\n\u001b[31m:root{\u001b[39m\n\u001b[31m  --bg:#080c12; --bg2:#0d1420; --bg3:#111a28;\u001b[39m\n\u001b[31m  --sur:#141f30; --sur2:#1a2840; --sur3:#1f3050;\u001b[39m\n\u001b[31m  --bdr:#1e2e45; --bdr2:#253650;\u001b[39m\n\u001b[31m  --t1:#f0f4f8; --t2:#8ba4bc; --t3:#3d5470; --t4:#243348;\u001b[39m\n\u001b[31m  --acc:#f97316; --acc2:#fb923c; --acc-glow:rgba(249,115,22,0.25);\u001b[39m\n\u001b[31m  --grn:#22c55e; --grn-dim:rgba(34,197,94,0.15);\u001b[39m\n\u001b[31m  --sky:#38bdf8; --sky-dim:rgba(56,189,248,0.15);\u001b[39m\n\u001b[31m  --red:#ef4444; --red-dim:rgba(239,68,68,0.15);\u001b[39m\n\u001b[31m  --ylw:#eab308; --ylw-dim:rgba(234,179,8,0.15);\u001b[39m\n\u001b[31m  --fnt-head:'Barlow Condensed',sans-serif;\u001b[39m\n\u001b[31m  --fnt-body:'Space Grotesk',sans-serif;\u001b[39m\n\u001b[31m  --fnt-mono:'Fira Code',monospace;\u001b[39m\n\u001b[31m  --r:10px; --r-sm:6px; --r-lg:16px;\u001b[39m\n\u001b[31m  --sh:0 4px 24px rgba(0,0,0,0.4);\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31mhtml,body{height:100%;background:var(--bg);color:var(--t1);font-family:var(--fnt-body);overflow:hidden;}·\u001b[39m\n\u001b[31m/* ════ SPLASH ════ */\u001b[39m\n\u001b[31m#splash{\u001b[39m\n\u001b[31m  position:fixed;inset:0;background:var(--bg);z-index:999;\u001b[39m\n\u001b[31m  display:flex;flex-direction:column;align-items:center;justify-content:center;\u001b[39m\n\u001b[31m  transition:opacity .4s .1s;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.splash-logo{font-family:var(--fnt-head);font-size:3rem;font-weight:800;letter-spacing:2px;line-height:1;}\u001b[39m\n\u001b[31m.splash-logo em{color:var(--acc);font-style:normal;}\u001b[39m\n\u001b[31m.splash-sub{color:var(--t3);font-family:var(--fnt-mono);font-size:.7rem;letter-spacing:4px;margin-top:8px;}\u001b[39m\n\u001b[31m.splash-track{width:200px;height:2px;background:var(--bdr);border-radius:2px;margin-top:40px;overflow:hidden;}\u001b[39m\n\u001b[31m.splash-track-fill{height:100%;width:0;background:linear-gradient(90deg,var(--acc),var(--acc2));animation:splashLoad 1.3s cubic-bezier(.4,0,.2,1) forwards;}\u001b[39m\n\u001b[31m@keyframes splashLoad{to{width:100%;}}·\u001b[39m\n\u001b[31m/* ════ LAYOUT ════ */\u001b[39m\n\u001b[31m#app{height:100%;display:flex;flex-direction:column;}\u001b[39m\n\u001b[31m.hdr{\u001b[39m\n\u001b[31m  background:var(--bg2);border-bottom:1px solid var(--bdr);\u001b[39m\n\u001b[31m  padding:10px 14px;display:flex;align-items:center;gap:10px;flex-shrink:0;\u001b[39m\n\u001b[31m  position:relative;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.hdr-brand{font-family:var(--fnt-head);font-size:1.3rem;font-weight:800;letter-spacing:1px;flex:1;}\u001b[39m\n\u001b[31m.hdr-brand em{color:var(--acc);font-style:normal;}\u001b[39m\n\u001b[31m.hdr-dot{width:7px;height:7px;border-radius:50%;background:var(--grn);transition:background .3s;animation:blink 2s infinite;}\u001b[39m\n\u001b[31m@keyframes blink{0%,100%{opacity:1;}50%{opacity:.4;}}\u001b[39m\n\u001b[31m.hdr-btn{\u001b[39m\n\u001b[31m  background:none;border:none;color:var(--t2);cursor:pointer;\u001b[39m\n\u001b[31m  padding:6px 8px;border-radius:var(--r-sm);transition:all .2s;\u001b[39m\n\u001b[31m  font-family:var(--fnt-head);font-size:.85rem;font-weight:700;letter-spacing:.5px;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.hdr-btn:hover{background:var(--bdr);color:var(--t1);}\u001b[39m\n\u001b[31m.hdr-btn.primary{background:var(--acc);color:#fff;padding:6px 14px;}\u001b[39m\n\u001b[31m.hdr-btn.primary:hover{background:var(--acc2);}·\u001b[39m\n\u001b[31m/* ════ TAB BAR ════ */\u001b[39m\n\u001b[31m.tabs{\u001b[39m\n\u001b[31m  display:flex;background:var(--bg2);border-bottom:1px solid var(--bdr);\u001b[39m\n\u001b[31m  overflow-x:auto;flex-shrink:0;padding:0 6px;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.tabs::-webkit-scrollbar{display:none;}\u001b[39m\n\u001b[31m.tab{\u001b[39m\n\u001b[31m  background:none;border:none;color:var(--t3);font-family:var(--fnt-head);\u001b[39m\n\u001b[31m  font-size:.85rem;font-weight:700;letter-spacing:1px;padding:10px 14px;\u001b[39m\n\u001b[31m  cursor:pointer;border-bottom:2px solid transparent;white-space:nowrap;\u001b[39m\n\u001b[31m  transition:all .2s;flex-shrink:0;display:flex;align-items:center;gap:6px;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.tab.active{color:var(--acc);border-bottom-color:var(--acc);}\u001b[39m\n\u001b[31m.tab:hover:not(.active){color:var(--t2);}\u001b[39m\n\u001b[31m.tab-badge{\u001b[39m\n\u001b[31m  background:var(--acc);color:#fff;font-size:.6rem;font-weight:700;\u001b[39m\n\u001b[31m  padding:1px 5px;border-radius:10px;font-family:var(--fnt-mono);\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m/* ════ CONTENT AREAS ════ */\u001b[39m\n\u001b[31m.pane{flex:1;overflow-y:auto;padding:14px;display:none;}\u001b[39m\n\u001b[31m.pane.active{display:block;}\u001b[39m\n\u001b[31m.pane::-webkit-scrollbar{width:3px;}\u001b[39m\n\u001b[31m.pane::-webkit-scrollbar-thumb{background:var(--bdr2);}·\u001b[39m\n\u001b[31m/* ════ CARDS ════ */\u001b[39m\n\u001b[31m.card{\u001b[39m\n\u001b[31m  background:var(--sur);border:1px solid var(--bdr);border-radius:var(--r);\u001b[39m\n\u001b[31m  padding:14px;margin-bottom:12px;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.card.accent-left{border-left:3px solid var(--acc);}\u001b[39m\n\u001b[31m.card-title{\u001b[39m\n\u001b[31m  font-family:var(--fnt-head);font-size:.72rem;font-weight:700;\u001b[39m\n\u001b[31m  color:var(--t3);text-transform:uppercase;letter-spacing:2.5px;\u001b[39m\n\u001b[31m  margin-bottom:12px;display:flex;align-items:center;gap:8px;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.card-title .ct-dot{width:5px;height:5px;border-radius:50%;background:var(--acc);flex-shrink:0;}·\u001b[39m\n\u001b[31m/* ════ FORM ════ */\u001b[39m\n\u001b[31m.f{margin-bottom:12px;}\u001b[39m\n\u001b[31m.f label{\u001b[39m\n\u001b[31m  display:block;font-size:.68rem;color:var(--t3);margin-bottom:5px;\u001b[39m\n\u001b[31m  font-weight:600;text-transform:uppercase;letter-spacing:1.5px;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.f label .req{color:var(--acc);margin-left:2px;}\u001b[39m\n\u001b[31m.f input,.f select,.f textarea{\u001b[39m\n\u001b[31m  width:100%;background:var(--sur2);border:1px solid var(--bdr);\u001b[39m\n\u001b[31m  border-radius:var(--r-sm);color:var(--t1);font-family:var(--fnt-body);\u001b[39m\n\u001b[31m  font-size:.88rem;padding:9px 11px;outline:none;transition:border-color .2s;\u001b[39m\n\u001b[31m  -webkit-appearance:none;appearance:none;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.f input:focus,.f select:focus,.f textarea:focus{border-color:var(--acc);}\u001b[39m\n\u001b[31m.f input::placeholder{color:var(--t4);}\u001b[39m\n\u001b[31m.f textarea{resize:vertical;min-height:65px;}\u001b[39m\n\u001b[31m.f select{\u001b[39m\n\u001b[31m  background-image:url(\\\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='7'%3E%3Cpath d='M1 1l4 4 4-4' stroke='%233d5470' stroke-width='1.5' fill='none' stroke-linecap='round'/%3E%3C/svg%3E\\\");\u001b[39m\n\u001b[31m  background-repeat:no-repeat;background-position:right 10px center;padding-right:28px;cursor:pointer;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.row2{display:grid;grid-template-columns:1fr 1fr;gap:10px;}\u001b[39m\n\u001b[31m.row3{display:grid;grid-template-columns:1fr 1fr 1fr;gap:8px;}·\u001b[39m\n\u001b[31m/* ════ BUTTONS ════ */\u001b[39m\n\u001b[31m.btn{\u001b[39m\n\u001b[31m  width:100%;padding:12px;border:none;border-radius:var(--r);\u001b[39m\n\u001b[31m  font-family:var(--fnt-head);font-size:.9rem;font-weight:700;letter-spacing:.5px;\u001b[39m\n\u001b[31m  cursor:pointer;transition:all .2s;display:flex;align-items:center;justify-content:center;gap:8px;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.btn-orange{background:var(--acc);color:#fff;}\u001b[39m\n\u001b[31m.btn-orange:hover{background:var(--acc2);}\u001b[39m\n\u001b[31m.btn-ghost{background:var(--sur2);border:1px solid var(--bdr);color:var(--t1);}\u001b[39m\n\u001b[31m.btn-ghost:hover{border-color:var(--acc);color:var(--acc);}\u001b[39m\n\u001b[31m.btn-grn{background:var(--grn);color:#000;}\u001b[39m\n\u001b[31m.btn-grn:hover{opacity:.85;}\u001b[39m\n\u001b[31m.btn:active{transform:scale(.98);}\u001b[39m\n\u001b[31m.btn-row{display:grid;grid-template-columns:1fr 1fr;gap:10px;margin-top:6px;}\u001b[39m\n\u001b[31m.btn-sm{\u001b[39m\n\u001b[31m  background:none;border:1px solid var(--bdr);color:var(--t2);\u001b[39m\n\u001b[31m  font-size:.7rem;padding:5px 10px;border-radius:var(--r-sm);cursor:pointer;\u001b[39m\n\u001b[31m  font-family:var(--fnt-body);transition:all .2s;white-space:nowrap;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.btn-sm:hover{border-color:var(--acc);color:var(--acc);}\u001b[39m\n\u001b[31m.btn-sm.primary{background:var(--acc);border-color:var(--acc);color:#fff;}·\u001b[39m\n\u001b[31m/* ════ OCR UPLOAD ZONE ════ */\u001b[39m\n\u001b[31m.upload-zone{\u001b[39m\n\u001b[31m  border:2px dashed var(--bdr2);border-radius:var(--r);\u001b[39m\n\u001b[31m  padding:20px;text-align:center;cursor:pointer;transition:all .25s;\u001b[39m\n\u001b[31m  background:var(--sur2);position:relative;overflow:hidden;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.upload-zone:hover,.upload-zone.drag{border-color:var(--acc);background:rgba(249,115,22,.05);}\u001b[39m\n\u001b[31m.upload-zone .uz-icon{font-size:2rem;margin-bottom:8px;}\u001b[39m\n\u001b[31m.upload-zone .uz-title{font-family:var(--fnt-head);font-size:1rem;font-weight:700;color:var(--t1);}\u001b[39m\n\u001b[31m.upload-zone .uz-sub{font-size:.72rem;color:var(--t3);margin-top:4px;font-family:var(--fnt-mono);}\u001b[39m\n\u001b[31m.upload-zone input{display:none;}\u001b[39m\n\u001b[31m.upload-zone.share-target{border-color:var(--grn);}·\u001b[39m\n\u001b[31m/* ════ OCR RESULTS ════ */\u001b[39m\n\u001b[31m.ocr-results{display:none;}\u001b[39m\n\u001b[31m.ocr-results.visible{display:block;}\u001b[39m\n\u001b[31m.ocr-slip-list{display:flex;flex-direction:column;gap:8px;margin-top:10px;}\u001b[39m\n\u001b[31m.ocr-slip{\u001b[39m\n\u001b[31m  background:var(--sur2);border:1px solid var(--bdr);border-radius:var(--r-sm);\u001b[39m\n\u001b[31m  padding:10px 12px;display:flex;gap:10px;align-items:flex-start;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.ocr-slip img{width:60px;height:60px;object-fit:cover;border-radius:6px;flex-shrink:0;border:1px solid var(--bdr2);}\u001b[39m\n\u001b[31m.ocr-slip-body{flex:1;min-width:0;}\u001b[39m\n\u001b[31m.ocr-slip-name{font-size:.7rem;color:var(--t3);font-family:var(--fnt-mono);margin-bottom:4px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}\u001b[39m\n\u001b[31m.ocr-slip-docket{\u001b[39m\n\u001b[31m  font-family:var(--fnt-mono);font-size:.9rem;font-weight:500;color:var(--acc);\u001b[39m\n\u001b[31m  background:var(--bg);border:1px solid var(--bdr);border-radius:4px;\u001b[39m\n\u001b[31m  padding:4px 8px;width:100%;outline:none;transition:border-color .2s;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.ocr-slip-docket:focus{border-color:var(--acc);}\u001b[39m\n\u001b[31m.ocr-slip-status{font-size:.65rem;margin-top:4px;display:flex;align-items:center;gap:6px;}\u001b[39m\n\u001b[31m.ocr-confidence{padding:2px 6px;border-radius:10px;font-family:var(--fnt-mono);}\u001b[39m\n\u001b[31m.conf-high{background:var(--grn-dim);color:var(--grn);}\u001b[39m\n\u001b[31m.conf-med{background:var(--ylw-dim);color:var(--ylw);}\u001b[39m\n\u001b[31m.conf-low{background:var(--red-dim);color:var(--red);}\u001b[39m\n\u001b[31m.ocr-ai-btn{\u001b[39m\n\u001b[31m  background:rgba(56,189,248,.15);border:1px solid rgba(56,189,248,.3);\u001b[39m\n\u001b[31m  color:var(--sky);font-size:.65rem;padding:3px 8px;border-radius:10px;\u001b[39m\n\u001b[31m  cursor:pointer;font-family:var(--fnt-mono);transition:all .2s;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.ocr-ai-btn:hover{background:rgba(56,189,248,.3);}\u001b[39m\n\u001b[31m.ocr-working{\u001b[39m\n\u001b[31m  background:var(--ylw-dim);border:1px solid rgba(234,179,8,.3);\u001b[39m\n\u001b[31m  border-radius:var(--r-sm);padding:8px 12px;margin-top:8px;\u001b[39m\n\u001b[31m  font-size:.75rem;color:var(--ylw);font-family:var(--fnt-mono);\u001b[39m\n\u001b[31m  display:none;align-items:center;gap:8px;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.ocr-working.visible{display:flex;}\u001b[39m\n\u001b[31m.spin{animation:spin .8s linear infinite;display:inline-block;}\u001b[39m\n\u001b[31m@keyframes spin{to{transform:rotate(360deg);}}·\u001b[39m\n\u001b[31m/* ════ STORE LIST ════ */\u001b[39m\n\u001b[31m.store-upload-zone{\u001b[39m\n\u001b[31m  border:2px dashed var(--bdr2);border-radius:var(--r);padding:16px;\u001b[39m\n\u001b[31m  text-align:center;cursor:pointer;transition:all .25s;background:var(--sur2);\u001b[39m\n\u001b[31m  position:relative;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.store-upload-zone:hover{border-color:var(--sky);background:rgba(56,189,248,.05);}\u001b[39m\n\u001b[31m.store-upload-zone input{display:none;}\u001b[39m\n\u001b[31m.store-table-wrap{max-height:200px;overflow-y:auto;margin-top:10px;border-radius:var(--r-sm);overflow:hidden;}\u001b[39m\n\u001b[31m.store-table{width:100%;border-collapse:collapse;font-size:.75rem;}\u001b[39m\n\u001b[31m.store-table th{\u001b[39m\n\u001b[31m  background:var(--bg);color:var(--t3);font-family:var(--fnt-head);\u001b[39m\n\u001b[31m  font-size:.65rem;letter-spacing:1.5px;text-transform:uppercase;\u001b[39m\n\u001b[31m  padding:7px 8px;text-align:left;border-bottom:1px solid var(--bdr);\u001b[39m\n\u001b[31m  position:sticky;top:0;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.store-table td{\u001b[39m\n\u001b[31m  padding:6px 8px;border-bottom:1px solid var(--bdr);color:var(--t2);\u001b[39m\n\u001b[31m  font-family:var(--fnt-mono);font-size:.72rem;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.store-table tr:hover td{background:var(--sur3);}\u001b[39m\n\u001b[31m.store-table .matched{color:var(--grn);}\u001b[39m\n\u001b[31m.store-table .unmatched{color:var(--ylw);}\u001b[39m\n\u001b[31m.store-map-btn{\u001b[39m\n\u001b[31m  font-size:.65rem;background:none;border:1px solid var(--bdr);color:var(--t3);\u001b[39m\n\u001b[31m  padding:2px 6px;border-radius:4px;cursor:pointer;transition:all .2s;\u001b[39m\n\u001b[31m  font-family:var(--fnt-mono);\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.store-map-btn:hover{border-color:var(--acc);color:var(--acc);}·\u001b[39m\n\u001b[31m/* ════ CONSIGNEE CARDS ════ */\u001b[39m\n\u001b[31m.cons-list{display:flex;flex-direction:column;gap:8px;margin-bottom:10px;}\u001b[39m\n\u001b[31m.cons-card{\u001b[39m\n\u001b[31m  background:var(--sur2);border:1px solid var(--bdr);border-radius:var(--r-sm);\u001b[39m\n\u001b[31m  padding:11px;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.cons-hdr{display:flex;justify-content:space-between;align-items:center;margin-bottom:8px;}\u001b[39m\n\u001b[31m.cons-num{font-family:var(--fnt-mono);font-size:.65rem;color:var(--t3);}\u001b[39m\n\u001b[31m.cons-del{\u001b[39m\n\u001b[31m  background:none;border:none;color:var(--t4);cursor:pointer;\u001b[39m\n\u001b[31m  font-size:.85rem;padding:2px 5px;border-radius:4px;transition:all .2s;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.cons-del:hover{background:var(--red-dim);color:var(--red);}\u001b[39m\n\u001b[31m.cons-docket-linked{\u001b[39m\n\u001b[31m  display:flex;align-items:center;gap:6px;\u001b[39m\n\u001b[31m  background:rgba(249,115,22,.1);border:1px solid rgba(249,115,22,.3);\u001b[39m\n\u001b[31m  border-radius:4px;padding:4px 8px;margin-top:6px;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.cons-docket-linked .dkt{font-family:var(--fnt-mono);font-size:.82rem;color:var(--acc);}\u001b[39m\n\u001b[31m.cons-docket-linked .unlink{background:none;border:none;color:var(--acc);cursor:pointer;font-size:.75rem;}·\u001b[39m\n\u001b[31m/* ════ DISPATCH DASHBOARD ════ */\u001b[39m\n\u001b[31m.dash-stats{\u001b[39m\n\u001b[31m  display:grid;grid-template-columns:repeat(4,1fr);gap:8px;margin-bottom:14px;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.stat-box{\u001b[39m\n\u001b[31m  background:var(--sur);border:1px solid var(--bdr);border-radius:var(--r);\u001b[39m\n\u001b[31m  padding:10px 8px;text-align:center;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.stat-val{font-family:var(--fnt-head);font-size:1.4rem;font-weight:800;}\u001b[39m\n\u001b[31m.stat-lbl{font-size:.6rem;color:var(--t3);font-family:var(--fnt-mono);text-transform:uppercase;letter-spacing:1px;margin-top:2px;}\u001b[39m\n\u001b[31m.stat-box.grn .stat-val{color:var(--grn);}\u001b[39m\n\u001b[31m.stat-box.ylw .stat-val{color:var(--ylw);}\u001b[39m\n\u001b[31m.stat-box.red .stat-val{color:var(--red);}\u001b[39m\n\u001b[31m.stat-box.sky .stat-val{color:var(--sky);}·\u001b[39m\n\u001b[31m/* ════ DOCKET TILES ════ */\u001b[39m\n\u001b[31m.docket-grid{display:flex;flex-direction:column;gap:8px;}\u001b[39m\n\u001b[31m.docket-tile{\u001b[39m\n\u001b[31m  background:var(--sur);border:1px solid var(--bdr);border-radius:var(--r);\u001b[39m\n\u001b[31m  padding:12px;transition:border-color .2s;cursor:pointer;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.docket-tile:hover{border-color:var(--acc);}\u001b[39m\n\u001b[31m.docket-tile-hdr{display:flex;justify-content:space-between;align-items:flex-start;margin-bottom:8px;}\u001b[39m\n\u001b[31m.docket-tile-left{flex:1;}\u001b[39m\n\u001b[31m.docket-num{font-family:var(--fnt-mono);font-size:.95rem;font-weight:500;color:var(--sky);letter-spacing:1px;}\u001b[39m\n\u001b[31m.docket-store{font-size:.82rem;color:var(--t1);font-weight:600;margin-top:2px;}\u001b[39m\n\u001b[31m.docket-courier{font-size:.68rem;color:var(--t3);font-family:var(--fnt-mono);margin-top:2px;}\u001b[39m\n\u001b[31m.status-pill{\u001b[39m\n\u001b[31m  font-size:.62rem;font-weight:700;font-family:var(--fnt-mono);\u001b[39m\n\u001b[31m  padding:3px 8px;border-radius:10px;text-transform:uppercase;letter-spacing:.5px;\u001b[39m\n\u001b[31m  flex-shrink:0;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.sp-delivered{background:var(--grn-dim);color:var(--grn);}\u001b[39m\n\u001b[31m.sp-transit{background:var(--ylw-dim);color:var(--ylw);}\u001b[39m\n\u001b[31m.sp-booked{background:var(--sky-dim);color:var(--sky);}\u001b[39m\n\u001b[31m.sp-pending{background:var(--bdr);color:var(--t3);}\u001b[39m\n\u001b[31m.sp-failed{background:var(--red-dim);color:var(--red);}\u001b[39m\n\u001b[31m.docket-tile-footer{display:flex;align-items:center;gap:8px;flex-wrap:wrap;}\u001b[39m\n\u001b[31m.docket-last-event{font-size:.7rem;color:var(--t3);flex:1;font-family:var(--fnt-mono);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}\u001b[39m\n\u001b[31m.btn-track-live{\u001b[39m\n\u001b[31m  background:none;border:1px solid var(--acc);color:var(--acc);\u001b[39m\n\u001b[31m  font-size:.65rem;padding:4px 10px;border-radius:20px;cursor:pointer;\u001b[39m\n\u001b[31m  font-family:var(--fnt-mono);transition:all .2s;white-space:nowrap;display:flex;align-items:center;gap:4px;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.btn-track-live:hover{background:var(--acc);color:#fff;}\u001b[39m\n\u001b[31m.btn-track-live .live-dot{width:5px;height:5px;border-radius:50%;background:var(--grn);animation:blink 1.5s infinite;flex-shrink:0;}\u001b[39m\n\u001b[31m.docket-progress{height:2px;background:var(--bdr);border-radius:2px;margin-top:8px;overflow:hidden;}\u001b[39m\n\u001b[31m.docket-progress-fill{height:100%;border-radius:2px;transition:width .5s;}·\u001b[39m\n\u001b[31m/* ════ TRACKING DETAIL ════ */\u001b[39m\n\u001b[31m.track-timeline{padding:4px 0;}\u001b[39m\n\u001b[31m.track-evt{display:flex;gap:10px;padding:6px 0;position:relative;padding-left:20px;}\u001b[39m\n\u001b[31m.track-evt::before{\u001b[39m\n\u001b[31m  content:'';position:absolute;left:5px;top:11px;\u001b[39m\n\u001b[31m  width:7px;height:7px;border-radius:50%;background:var(--bdr2);border:1px solid var(--bdr2);\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.track-evt:first-child::before{background:var(--grn);border-color:var(--grn);}\u001b[39m\n\u001b[31m.track-evt::after{\u001b[39m\n\u001b[31m  content:'';position:absolute;left:8px;top:18px;\u001b[39m\n\u001b[31m  width:1px;height:calc(100% - 6px);background:var(--bdr);\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.track-evt:last-child::after{display:none;}\u001b[39m\n\u001b[31m.track-time{font-family:var(--fnt-mono);font-size:.62rem;color:var(--t3);min-width:90px;flex-shrink:0;padding-top:2px;}\u001b[39m\n\u001b[31m.track-desc{font-size:.78rem;color:var(--t2);}\u001b[39m\n\u001b[31m.track-loc{font-size:.67rem;color:var(--t3);font-family:var(--fnt-mono);margin-top:2px;}·\u001b[39m\n\u001b[31m/* ════ SHARE TOAST ════ */\u001b[39m\n\u001b[31m.share-banner{\u001b[39m\n\u001b[31m  position:fixed;top:0;left:0;right:0;z-index:200;\u001b[39m\n\u001b[31m  background:rgba(34,197,94,.15);border-bottom:1px solid rgba(34,197,94,.4);\u001b[39m\n\u001b[31m  padding:10px 16px;font-size:.78rem;color:var(--grn);font-family:var(--fnt-mono);\u001b[39m\n\u001b[31m  display:flex;align-items:center;gap:8px;\u001b[39m\n\u001b[31m  transform:translateY(-100%);transition:transform .3s;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.share-banner.show{transform:translateY(0);}·\u001b[39m\n\u001b[31m/* ════ MODAL ════ */\u001b[39m\n\u001b[31m.overlay{position:fixed;inset:0;background:rgba(0,0,0,.75);z-index:100;display:flex;align-items:flex-end;opacity:0;pointer-events:none;transition:opacity .3s;}\u001b[39m\n\u001b[31m.overlay.open{opacity:1;pointer-events:all;}\u001b[39m\n\u001b[31m.sheet{\u001b[39m\n\u001b[31m  background:var(--sur);border-radius:var(--r-lg) var(--r-lg) 0 0;\u001b[39m\n\u001b[31m  width:100%;max-height:88vh;overflow-y:auto;padding:18px;\u001b[39m\n\u001b[31m  transform:translateY(100%);transition:transform .3s cubic-bezier(.32,.72,0,1);\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.overlay.open .sheet{transform:translateY(0);}\u001b[39m\n\u001b[31m.sheet-handle{width:36px;height:4px;background:var(--bdr2);border-radius:2px;margin:0 auto 18px;}\u001b[39m\n\u001b[31m.sheet-title{font-family:var(--fnt-head);font-size:1.2rem;font-weight:800;letter-spacing:.5px;margin-bottom:14px;}\u001b[39m\n\u001b[31m.sheet::-webkit-scrollbar{width:3px;}\u001b[39m\n\u001b[31m.sheet::-webkit-scrollbar-thumb{background:var(--bdr2);}·\u001b[39m\n\u001b[31m/* ════ TOAST ════ */\u001b[39m\n\u001b[31m.toast{\u001b[39m\n\u001b[31m  position:fixed;bottom:20px;left:50%;transform:translateX(-50%) translateY(70px);\u001b[39m\n\u001b[31m  background:var(--sur2);border:1px solid var(--bdr2);color:var(--t1);\u001b[39m\n\u001b[31m  font-size:.78rem;padding:9px 16px;border-radius:30px;z-index:300;\u001b[39m\n\u001b[31m  transition:transform .3s;white-space:nowrap;font-family:var(--fnt-mono);\u001b[39m\n\u001b[31m  pointer-events:none;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.toast.show{transform:translateX(-50%) translateY(0);}\u001b[39m\n\u001b[31m.toast.ok{border-color:var(--grn);color:var(--grn);}\u001b[39m\n\u001b[31m.toast.err{border-color:var(--red);color:var(--red);}\u001b[39m\n\u001b[31m.toast.info{border-color:var(--sky);color:var(--sky);}·\u001b[39m\n\u001b[31m/* ════ COURIER MASTER ════ */\u001b[39m\n\u001b[31m.courier-master-row{\u001b[39m\n\u001b[31m  display:flex;align-items:center;gap:8px;padding:8px 0;\u001b[39m\n\u001b[31m  border-bottom:1px solid var(--bdr);\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.courier-master-row:last-child{border-bottom:none;}\u001b[39m\n\u001b[31m.cml{flex:1;}\u001b[39m\n\u001b[31m.cml-name{font-weight:600;font-size:.85rem;}\u001b[39m\n\u001b[31m.cml-url{font-family:var(--fnt-mono);font-size:.65rem;color:var(--t3);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:180px;}\u001b[39m\n\u001b[31m.courier-logo{width:24px;height:24px;border-radius:4px;object-fit:contain;background:var(--sur3);padding:2px;}·\u001b[39m\n\u001b[31m/* ════ PROGRESS BAR ════ */\u001b[39m\n\u001b[31m.ai-progress{\u001b[39m\n\u001b[31m  background:var(--sur2);border:1px solid var(--bdr);border-radius:var(--r-sm);\u001b[39m\n\u001b[31m  padding:10px 12px;margin-top:8px;display:none;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.ai-progress.visible{display:block;}\u001b[39m\n\u001b[31m.ai-prog-bar{height:3px;background:var(--bdr);border-radius:3px;margin-top:6px;overflow:hidden;}\u001b[39m\n\u001b[31m.ai-prog-fill{height:100%;background:linear-gradient(90deg,var(--sky),var(--acc));width:0;transition:width .3s;}\u001b[39m\n\u001b[31m.ai-prog-label{font-size:.68rem;color:var(--sky);font-family:var(--fnt-mono);}·\u001b[39m\n\u001b[31m/* ════ EMPTY ════ */\u001b[39m\n\u001b[31m.empty{padding:40px 20px;text-align:center;color:var(--t4);}\u001b[39m\n\u001b[31m.empty .e-icon{font-size:2.5rem;margin-bottom:10px;}\u001b[39m\n\u001b[31m.empty p{font-size:.82rem;}·\u001b[39m\n\u001b[31m/* ════ INFO TAG ════ */\u001b[39m\n\u001b[31m.info-tag{\u001b[39m\n\u001b[31m  background:var(--sur2);border:1px solid var(--bdr);border-radius:var(--r-sm);\u001b[39m\n\u001b[31m  padding:7px 10px;font-size:.75rem;color:var(--t2);margin-bottom:8px;\u001b[39m\n\u001b[31m  display:flex;align-items:center;gap:8px;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.info-tag strong{color:var(--t1);}·\u001b[39m\n\u001b[31m/* ════ JOB PILL ════ */\u001b[39m\n\u001b[31m.job-pill{\u001b[39m\n\u001b[31m  display:inline-flex;align-items:center;gap:6px;\u001b[39m\n\u001b[31m  background:rgba(249,115,22,.1);border:1px solid rgba(249,115,22,.3);\u001b[39m\n\u001b[31m  color:var(--acc);font-family:var(--fnt-mono);font-size:.68rem;\u001b[39m\n\u001b[31m  padding:3px 9px;border-radius:20px;margin-top:4px;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.job-pill button{background:none;border:none;color:var(--acc);cursor:pointer;font-size:.7rem;}·\u001b[39m\n\u001b[31m/* ════ SECTION DIVIDER ════ */\u001b[39m\n\u001b[31m.sec-div{\u001b[39m\n\u001b[31m  display:flex;align-items:center;gap:10px;margin:14px 0 10px;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.sec-div span{font-family:var(--fnt-head);font-size:.7rem;font-weight:700;color:var(--t3);text-transform:uppercase;letter-spacing:2px;white-space:nowrap;}\u001b[39m\n\u001b[31m.sec-div::before,.sec-div::after{content:'';flex:1;height:1px;background:var(--bdr);}·\u001b[39m\n\u001b[31m/* ── MAPPING UI ── */\u001b[39m\n\u001b[31m.map-table{width:100%;border-collapse:collapse;font-size:.78rem;margin:10px 0;}\u001b[39m\n\u001b[31m.map-table th{background:var(--bg);color:var(--t3);font-family:var(--fnt-head);font-size:.65rem;\u001b[39m\n\u001b[31m  letter-spacing:1.5px;text-transform:uppercase;padding:7px 10px;text-align:left;\u001b[39m\n\u001b[31m  border-bottom:1px solid var(--bdr);}\u001b[39m\n\u001b[31m.map-table td{padding:7px 10px;border-bottom:1px solid var(--bdr);vertical-align:middle;}\u001b[39m\n\u001b[31m.map-table tr:last-child td{border-bottom:none;}\u001b[39m\n\u001b[31m.map-table select{background:var(--sur2);border:1px solid var(--bdr);color:var(--t1);\u001b[39m\n\u001b[31m  font-family:var(--fnt-mono);font-size:.75rem;padding:5px 8px;border-radius:var(--r-sm);\u001b[39m\n\u001b[31m  width:100%;outline:none;}\u001b[39m\n\u001b[31m.map-table select:focus{border-color:var(--acc);}\u001b[39m\n\u001b[31m.map-req{color:var(--acc);margin-left:2px;}\u001b[39m\n\u001b[31m.map-saved-badge{font-size:.6rem;background:rgba(57,255,143,.15);color:var(--accent3,#39ff8f);\u001b[39m\n\u001b[31m  border:1px solid rgba(57,255,143,.3);padding:2px 7px;border-radius:10px;\u001b[39m\n\u001b[31m  font-family:var(--fnt-mono);margin-left:6px;}·\u001b[39m\n\u001b[31m/* ── VERIFICATION SCREEN ── */\u001b[39m\n\u001b[31m#verifyPane{display:none;position:fixed;inset:0;background:var(--bg);z-index:50;\u001b[39m\n\u001b[31m  overflow-y:auto;padding:14px;}\u001b[39m\n\u001b[31m.verify-hdr{display:flex;align-items:center;gap:10px;margin-bottom:14px;\u001b[39m\n\u001b[31m  position:sticky;top:0;background:var(--bg);padding-bottom:10px;\u001b[39m\n\u001b[31m  border-bottom:1px solid var(--bdr);z-index:1;}\u001b[39m\n\u001b[31m.verify-title{font-family:var(--fnt-head);font-size:1.1rem;font-weight:800;flex:1;}\u001b[39m\n\u001b[31m.verify-row{background:var(--sur);border:1px solid var(--bdr);border-radius:var(--r-sm);\u001b[39m\n\u001b[31m  padding:10px 12px;margin-bottom:8px;position:relative;}\u001b[39m\n\u001b[31m.verify-row.has-dupe{border-color:var(--red);background:rgba(239,68,68,.04);}\u001b[39m\n\u001b[31m.verify-row.no-docket{border-color:var(--ylw);background:rgba(234,179,8,.04);}\u001b[39m\n\u001b[31m.verify-row.ok{border-left:3px solid var(--grn);}\u001b[39m\n\u001b[31m.verify-row-hdr{display:flex;align-items:center;gap:8px;margin-bottom:8px;}\u001b[39m\n\u001b[31m.verify-num{font-family:var(--fnt-mono);font-size:.65rem;color:var(--t3);}\u001b[39m\n\u001b[31m.verify-store{font-weight:600;font-size:.9rem;flex:1;}\u001b[39m\n\u001b[31m.verify-flag{font-size:.65rem;font-weight:700;font-family:var(--fnt-mono);\u001b[39m\n\u001b[31m  padding:2px 7px;border-radius:10px;text-transform:uppercase;}\u001b[39m\n\u001b[31m.vf-dupe{background:var(--red-dim);color:var(--red);}\u001b[39m\n\u001b[31m.vf-nodocket{background:var(--ylw-dim);color:var(--ylw);}\u001b[39m\n\u001b[31m.vf-ok{background:var(--grn-dim);color:var(--grn);}\u001b[39m\n\u001b[31m.vf-short{background:rgba(56,189,248,.15);color:var(--sky);}\u001b[39m\n\u001b[31m.verify-fields{display:grid;grid-template-columns:1fr 1fr;gap:8px;margin-bottom:8px;}\u001b[39m\n\u001b[31m.verify-field label{font-size:.62rem;color:var(--t3);text-transform:uppercase;\u001b[39m\n\u001b[31m  letter-spacing:1px;display:block;margin-bottom:3px;font-family:var(--fnt-mono);}\u001b[39m\n\u001b[31m.verify-field input{width:100%;background:var(--sur2);border:1px solid var(--bdr);\u001b[39m\n\u001b[31m  color:var(--t1);font-family:var(--fnt-mono);font-size:.82rem;padding:6px 9px;\u001b[39m\n\u001b[31m  border-radius:var(--r-sm);outline:none;}\u001b[39m\n\u001b[31m.verify-field input:focus{border-color:var(--acc);}\u001b[39m\n\u001b[31m.verify-field input.changed{border-color:var(--ylw);background:rgba(234,179,8,.06);}\u001b[39m\n\u001b[31m.verify-actions{display:flex;gap:6px;align-items:center;flex-wrap:wrap;}\u001b[39m\n\u001b[31m.btn-wa{background:rgba(37,211,102,.15);border:1px solid rgba(37,211,102,.3);\u001b[39m\n\u001b[31m  color:#25d366;font-size:.65rem;padding:4px 10px;border-radius:10px;cursor:pointer;\u001b[39m\n\u001b[31m  font-family:var(--fnt-mono);transition:all .2s;}\u001b[39m\n\u001b[31m.btn-wa:hover{background:rgba(37,211,102,.3);}\u001b[39m\n\u001b[31m.verify-thumb{width:48px;height:48px;object-fit:cover;border-radius:6px;\u001b[39m\n\u001b[31m  border:1px solid var(--bdr);cursor:pointer;flex-shrink:0;}\u001b[39m\n\u001b[31m.verify-summary{background:var(--sur);border:1px solid var(--bdr);border-radius:var(--r);\u001b[39m\n\u001b[31m  padding:12px 14px;margin-bottom:14px;display:flex;gap:16px;flex-wrap:wrap;}\u001b[39m\n\u001b[31m.vs-item{text-align:center;}\u001b[39m\n\u001b[31m.vs-val{font-family:var(--fnt-head);font-size:1.3rem;font-weight:800;}\u001b[39m\n\u001b[31m.vs-lbl{font-size:.6rem;color:var(--t3);font-family:var(--fnt-mono);text-transform:uppercase;\u001b[39m\n\u001b[31m  letter-spacing:.5px;margin-top:2px;}\u001b[39m\n\u001b[31m.ocr-correction-note{font-size:.65rem;color:var(--ylw);font-family:var(--fnt-mono);\u001b[39m\n\u001b[31m  margin-top:3px;}\u001b[39m\n\u001b[31m</style>\u001b[39m\n\u001b[31m</head>\u001b[39m\n\u001b[31m<body>·\u001b[39m\n\u001b[31m<!-- SPLASH -->\u001b[39m\n\u001b[31m<div id=\\\"splash\\\">\u001b[39m\n\u001b[31m  <div class=\\\"splash-logo\\\">360LM <em>DISPATCH</em></div>\u001b[39m\n\u001b[31m  <div class=\\\"splash-sub\\\">Courier · Bus · Field Ops</div>\u001b[39m\n\u001b[31m  <div class=\\\"splash-track\\\"><div class=\\\"splash-track-fill\\\"></div></div>\u001b[39m\n\u001b[31m</div>·\u001b[39m\n\u001b[31m<!-- SHARE BANNER -->\u001b[39m\n\u001b[31m<div class=\\\"share-banner\\\" id=\\\"shareBanner\\\">\u001b[39m\n\u001b[31m  📤 Images received via Share — processing for OCR...\u001b[39m\n\u001b[31m  <button style=\\\"background:none;border:none;color:var(--grn);cursor:pointer;margin-left:auto;\\\" onclick=\\\"dismissShare()\\\">✕</button>\u001b[39m\n\u001b[31m</div>·\u001b[39m\n\u001b[31m<!-- APP -->\u001b[39m\n\u001b[31m<div id=\\\"app\\\" style=\\\"display:none;\\\">·\u001b[39m\n\u001b[31m  <div class=\\\"hdr\\\">\u001b[39m\n\u001b[31m    <div class=\\\"hdr-brand\\\">360LM <em>DISPATCH</em></div>\u001b[39m\n\u001b[31m    <span class=\\\"hdr-dot\\\" id=\\\"netDot\\\"></span>\u001b[39m\n\u001b[31m    <button class=\\\"hdr-btn\\\" onclick=\\\"window.location.href='/hub/'\\\" title=\\\"Hub\\\">🏡</button>\u001b[39m\n\u001b[31m    <button class=\\\"hdr-btn\\\" onclick=\\\"showPane('dashboard')\\\">📊</button>\u001b[39m\n\u001b[31m    <button class=\\\"hdr-btn primary\\\" onclick=\\\"newDispatch()\\\">+ NEW</button>\u001b[39m\n\u001b[31m  </div>·\u001b[39m\n\u001b[31m  <div class=\\\"tabs\\\">\u001b[39m\n\u001b[31m    <button class=\\\"tab active\\\" onclick=\\\"showPane('new')\\\" id=\\\"tab-new\\\">NEW</button>\u001b[39m\n\u001b[31m    <button class=\\\"tab\\\" onclick=\\\"showPane('dashboard')\\\" id=\\\"tab-dashboard\\\">DASHBOARD</button>\u001b[39m\n\u001b[31m    <button class=\\\"tab\\\" onclick=\\\"showPane('track')\\\" id=\\\"tab-track\\\">TRACK</button>\u001b[39m\n\u001b[31m    <button class=\\\"tab\\\" onclick=\\\"showPane('history')\\\" id=\\\"tab-history\\\">HISTORY <span class=\\\"tab-badge\\\" id=\\\"histCount\\\">0</span></button>\u001b[39m\n\u001b[31m    <button class=\\\"tab\\\" onclick=\\\"showPane('config')\\\" id=\\\"tab-config\\\">CONFIG</button>\u001b[39m\n\u001b[31m  </div>·\u001b[39m\n\u001b[31m  <!-- ════════════ NEW DISPATCH ════════════ -->\u001b[39m\n\u001b[31m  <div class=\\\"pane active\\\" id=\\\"pane-new\\\">·\u001b[39m\n\u001b[31m    <!-- 1. JOB REFERENCE -->\u001b[39m\n\u001b[31m    <div class=\\\"card accent-left\\\">\u001b[39m\n\u001b[31m      <div class=\\\"card-title\\\"><span class=\\\"ct-dot\\\"></span>Job Reference</div>\u001b[39m\n\u001b[31m      <div class=\\\"row2\\\">\u001b[39m\n\u001b[31m        <div class=\\\"f\\\">\u001b[39m\n\u001b[31m          <label>Client <span class=\\\"req\\\">*</span></label>\u001b[39m\n\u001b[31m          <select id=\\\"n_client\\\" onchange=\\\"onClientChange()\\\">\u001b[39m\n\u001b[31m            <option value=\\\"\\\">Select...</option>\u001b[39m\n\u001b[31m            <option>Lenovo</option><option>HP</option>\u001b[39m\n\u001b[31m            <option>Philips</option><option>Liebherr</option>\u001b[39m\n\u001b[31m            <option value=\\\"__c\\\">+ Custom</option>\u001b[39m\n\u001b[31m          </select>\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m        <div class=\\\"f\\\">\u001b[39m\n\u001b[31m          <label>Job Ref</label>\u001b[39m\n\u001b[31m          <input type=\\\"text\\\" id=\\\"n_jobref\\\" placeholder=\\\"JOB-042\\\" oninput=\\\"updatePill()\\\">\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      <div class=\\\"f\\\" id=\\\"customClientField\\\" style=\\\"display:none;\\\">\u001b[39m\n\u001b[31m        <label>Custom Client</label>\u001b[39m\n\u001b[31m        <input type=\\\"text\\\" id=\\\"n_customClient\\\" placeholder=\\\"Client name\\\" oninput=\\\"updatePill()\\\">\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      <div id=\\\"jobPill\\\" style=\\\"display:none;\\\" class=\\\"job-pill\\\">\u001b[39m\n\u001b[31m        🔗 <span id=\\\"jobPillTxt\\\"></span>\u001b[39m\n\u001b[31m        <button onclick=\\\"clearJob()\\\">✕</button>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      <div class=\\\"row2\\\" style=\\\"margin-top:10px;\\\">\u001b[39m\n\u001b[31m        <div class=\\\"f\\\" style=\\\"margin-bottom:0;\\\">\u001b[39m\n\u001b[31m          <label>Dispatch Date</label>\u001b[39m\n\u001b[31m          <input type=\\\"date\\\" id=\\\"n_date\\\">\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m        <div class=\\\"f\\\" style=\\\"margin-bottom:0;\\\">\u001b[39m\n\u001b[31m          <label>Courier Company <span class=\\\"req\\\">*</span></label>\u001b[39m\n\u001b[31m          <select id=\\\"n_courier\\\">\u001b[39m\n\u001b[31m            <option value=\\\"\\\">Select...</option>\u001b[39m\n\u001b[31m            <option>SkyKing</option><option>DTDC</option>\u001b[39m\n\u001b[31m            <option>Delhivery</option><option>Blue Dart</option>\u001b[39m\n\u001b[31m            <option>FedEx</option><option>Ecom Express</option>\u001b[39m\n\u001b[31m            <option>India Post</option><option value=\\\"__o\\\">Other</option>\u001b[39m\n\u001b[31m          </select>\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m    </div>·\u001b[39m\n\u001b[31m    <!-- 2. STORE LIST UPLOAD -->\u001b[39m\n\u001b[31m    <div class=\\\"card\\\">\u001b[39m\n\u001b[31m      <div class=\\\"card-title\\\"><span class=\\\"ct-dot\\\" style=\\\"background:var(--sky);\\\"></span>Store / Destination List</div>\u001b[39m\n\u001b[31m      <div class=\\\"store-upload-zone\\\" id=\\\"storeZone\\\" onclick=\\\"document.getElementById('storeFile').click();event.stopPropagation();\\\" style=\\\"cursor:pointer;\\\">\u001b[39m\n\u001b[31m        <input type=\\\"file\\\" id=\\\"storeFile\\\" accept=\\\".xlsx,.xls,.csv\\\" onchange=\\\"handleStoreFile(this)\\\" onclick=\\\"event.stopPropagation();\\\" style=\\\"display:none;\\\">\u001b[39m\n\u001b[31m        <div style=\\\"font-size:1.5rem;margin-bottom:6px;\\\">📋</div>\u001b[39m\n\u001b[31m        <div style=\\\"font-family:var(--fnt-head);font-weight:700;font-size:.9rem;\\\">Upload Store List</div>\u001b[39m\n\u001b[31m        <div style=\\\"font-size:.68rem;color:var(--t3);margin-top:3px;font-family:var(--fnt-mono);\\\">Excel / CSV · stores, cities, contacts</div>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      <div id=\\\"storeTableArea\\\" style=\\\"display:none;\\\">\u001b[39m\n\u001b[31m        <!-- Column detection result -->\u001b[39m\n\u001b[31m        <div id=\\\"storeColInfo\\\" style=\\\"margin-top:10px;display:none;\\\"></div>\u001b[39m\n\u001b[31m        <div style=\\\"display:flex;justify-content:space-between;align-items:center;margin-top:10px;margin-bottom:6px;\\\">\u001b[39m\n\u001b[31m          <span style=\\\"font-family:var(--fnt-mono);font-size:.7rem;color:var(--sky);\\\" id=\\\"storeCount\\\">0 stores loaded</span>\u001b[39m\n\u001b[31m          <button class=\\\"btn-sm\\\" onclick=\\\"clearStores()\\\">Clear</button>\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m        <div class=\\\"store-table-wrap\\\">\u001b[39m\n\u001b[31m          <table class=\\\"store-table\\\" id=\\\"storeTable\\\"></table>\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m        <!-- Inline add-as-consignees UI instead of confirm() -->\u001b[39m\n\u001b[31m        <div id=\\\"storeAddPanel\\\" style=\\\"margin-top:10px;background:var(--sur3);border:1px solid var(--bdr2);border-radius:var(--r-sm);padding:10px 12px;display:none;\\\">\u001b[39m\n\u001b[31m          <div style=\\\"font-size:.75rem;color:var(--t1);margin-bottom:8px;font-weight:600;\\\" id=\\\"storeAddTitle\\\">Add stores as consignees?</div>\u001b[39m\n\u001b[31m          <div class=\\\"row2\\\">\u001b[39m\n\u001b[31m            <button class=\\\"btn btn-ghost\\\" style=\\\"font-size:.75rem;padding:8px;\\\" onclick=\\\"document.getElementById('storeAddPanel').style.display='none'\\\">Skip for now</button>\u001b[39m\n\u001b[31m            <button class=\\\"btn btn-orange\\\" style=\\\"font-size:.75rem;padding:8px;\\\" id=\\\"storeAddBtn\\\" onclick=\\\"addStoresToConsignees()\\\">✓ Add All as Consignees</button>\u001b[39m\n\u001b[31m          </div>\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m        <button class=\\\"btn btn-ghost\\\" style=\\\"margin-top:8px;font-size:.78rem;\\\" onclick=\\\"autoMatchDockets()\\\">\u001b[39m\n\u001b[31m          🔗 Auto-match Dockets to Stores\u001b[39m\n\u001b[31m        </button>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m    </div>·\u001b[39m\n\u001b[31m    <!-- 3. DOCKET SLIPS — AI OCR -->\u001b[39m\n\u001b[31m    <div class=\\\"card\\\">\u001b[39m\n\u001b[31m      <div class=\\\"card-title\\\"><span class=\\\"ct-dot\\\" style=\\\"background:var(--grn);\\\"></span>Docket Slips — AI OCR</div>·\u001b[39m\n\u001b[31m      <!-- Share target indicator -->\u001b[39m\n\u001b[31m      <div class=\\\"info-tag\\\" id=\\\"shareTargetInfo\\\" style=\\\"display:none;\\\">\u001b[39m\n\u001b[31m        <span>📤</span><span>Images shared from WhatsApp — <strong id=\\\"sharedCount\\\">0</strong> slips received</span>\u001b[39m\n\u001b[31m      </div>·\u001b[39m\n\u001b[31m      <label class=\\\"upload-zone\\\" id=\\\"slipZone\\\" for=\\\"slipFiles\\\" style=\\\"display:block;cursor:pointer;\\\">\u001b[39m\n\u001b[31m        <input type=\\\"file\\\" id=\\\"slipFiles\\\" accept=\\\"image/*\\\" multiple onchange=\\\"handleSlipFiles(this.files)\\\" style=\\\"display:none;\\\">\u001b[39m\n\u001b[31m        <div class=\\\"uz-icon\\\">🖼️</div>\u001b[39m\n\u001b[31m        <div class=\\\"uz-title\\\">Upload Courier Slips</div>\u001b[39m\n\u001b[31m        <div class=\\\"uz-sub\\\">Multiple images · Tap or drag · Camera capture supported</div>\u001b[39m\n\u001b[31m      </label>·\u001b[39m\n\u001b[31m      <!-- OCR Working indicator -->\u001b[39m\n\u001b[31m      <div class=\\\"ocr-working\\\" id=\\\"ocrWorking\\\">\u001b[39m\n\u001b[31m        <span class=\\\"spin\\\">⟳</span>\u001b[39m\n\u001b[31m        <span id=\\\"ocrWorkingText\\\">Running OCR...</span>\u001b[39m\n\u001b[31m      </div>·\u001b[39m\n\u001b[31m      <!-- AI Progress -->\u001b[39m\n\u001b[31m      <div class=\\\"ai-progress\\\" id=\\\"aiProgress\\\">\u001b[39m\n\u001b[31m        <div class=\\\"ai-prog-label\\\" id=\\\"aiProgLabel\\\">Initializing Transformers.js...</div>\u001b[39m\n\u001b[31m        <div class=\\\"ai-prog-bar\\\"><div class=\\\"ai-prog-fill\\\" id=\\\"aiProgFill\\\"></div></div>\u001b[39m\n\u001b[31m      </div>·\u001b[39m\n\u001b[31m      <!-- OCR Results -->\u001b[39m\n\u001b[31m      <div class=\\\"ocr-results\\\" id=\\\"ocrResults\\\">\u001b[39m\n\u001b[31m        <div class=\\\"sec-div\\\"><span>Detected Dockets</span></div>\u001b[39m\n\u001b[31m        <div id=\\\"ocrSummary\\\" style=\\\"display:none;font-size:.72rem;font-family:var(--fnt-mono);\u001b[39m\n\u001b[31m          padding:4px 8px 8px;gap:8px\\\"></div>\u001b[39m\n\u001b[31m        <div class=\\\"ocr-slip-list\\\" id=\\\"ocrSlipList\\\"></div>\u001b[39m\n\u001b[31m        <div class=\\\"btn-row\\\" style=\\\"margin-top:10px;\\\">\u001b[39m\n\u001b[31m          <button class=\\\"btn btn-ghost\\\" onclick=\\\"clearSlips()\\\">Clear All</button>\u001b[39m\n\u001b[31m          <button class=\\\"btn btn-grn\\\" onclick=\\\"applyAllDockets()\\\">✓ Apply All to Consignees</button>\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m    </div>·\u001b[39m\n\u001b[31m    <!-- 4. CONSIGNEES -->\u001b[39m\n\u001b[31m    <div class=\\\"card accent-left\\\">\u001b[39m\n\u001b[31m      <div class=\\\"card-title\\\"><span class=\\\"ct-dot\\\"></span>Consignees</div>\u001b[39m\n\u001b[31m      <div class=\\\"cons-list\\\" id=\\\"consList\\\"></div>\u001b[39m\n\u001b[31m      <button class=\\\"btn btn-ghost\\\" style=\\\"font-size:.8rem;\\\" onclick=\\\"addConsignee()\\\">+ Add Consignee</button>\u001b[39m\n\u001b[31m    </div>·\u001b[39m\n\u001b[31m    <!-- 5. REMARKS -->\u001b[39m\n\u001b[31m    <div class=\\\"card\\\">\u001b[39m\n\u001b[31m      <div class=\\\"card-title\\\"><span class=\\\"ct-dot\\\" style=\\\"background:var(--t3);\\\"></span>Remarks</div>\u001b[39m\n\u001b[31m      <div class=\\\"f\\\" style=\\\"margin-bottom:0;\\\">\u001b[39m\n\u001b[31m        <textarea id=\\\"n_remarks\\\" placeholder=\\\"Instructions, notes, special handling...\\\"></textarea>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m    </div>·\u001b[39m\n\u001b[31m    <!-- ACTIONS -->\u001b[39m\n\u001b[31m    <div class=\\\"btn-row\\\">\u001b[39m\n\u001b[31m      <button class=\\\"btn btn-ghost\\\" onclick=\\\"saveDraft()\\\">💾 Draft</button>\u001b[39m\n\u001b[31m      <button class=\\\"btn btn-orange\\\" onclick=\\\"finaliseDispatch()\\\">✓ Finalise & Log</button>\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m    <div style=\\\"height:16px;\\\"></div>\u001b[39m\n\u001b[31m  </div>·\u001b[39m\n\u001b[31m  <!-- ════════════ DASHBOARD ════════════ -->\u001b[39m\n\u001b[31m  <div class=\\\"pane\\\" id=\\\"pane-dashboard\\\">\u001b[39m\n\u001b[31m    <div class=\\\"dash-stats\\\">\u001b[39m\n\u001b[31m      <div class=\\\"stat-box grn\\\"><div class=\\\"stat-val\\\" id=\\\"st-delivered\\\">0</div><div class=\\\"stat-lbl\\\">Delivered</div></div>\u001b[39m\n\u001b[31m      <div class=\\\"stat-box ylw\\\"><div class=\\\"stat-val\\\" id=\\\"st-transit\\\">0</div><div class=\\\"stat-lbl\\\">In Transit</div></div>\u001b[39m\n\u001b[31m      <div class=\\\"stat-box sky\\\"><div class=\\\"stat-val\\\" id=\\\"st-booked\\\">0</div><div class=\\\"stat-lbl\\\">Booked</div></div>\u001b[39m\n\u001b[31m      <div class=\\\"stat-box red\\\"><div class=\\\"stat-val\\\" id=\\\"st-pending\\\">0</div><div class=\\\"stat-lbl\\\">Pending</div></div>\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m    <div id=\\\"dashFilters\\\" style=\\\"display:flex;gap:6px;flex-wrap:wrap;margin-bottom:12px;\\\">\u001b[39m\n\u001b[31m      <span class=\\\"btn-sm primary\\\" onclick=\\\"filterDash('all',this)\\\" style=\\\"cursor:pointer;\\\">All</span>\u001b[39m\n\u001b[31m      <span class=\\\"btn-sm\\\" onclick=\\\"filterDash('delivered',this)\\\" style=\\\"cursor:pointer;\\\">Delivered</span>\u001b[39m\n\u001b[31m      <span class=\\\"btn-sm\\\" onclick=\\\"filterDash('transit',this)\\\" style=\\\"cursor:pointer;\\\">In Transit</span>\u001b[39m\n\u001b[31m      <span class=\\\"btn-sm\\\" onclick=\\\"filterDash('booked',this)\\\" style=\\\"cursor:pointer;\\\">Booked</span>\u001b[39m\n\u001b[31m      <span class=\\\"btn-sm\\\" onclick=\\\"filterDash('pending',this)\\\" style=\\\"cursor:pointer;\\\">Pending</span>\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m    <div class=\\\"docket-grid\\\" id=\\\"dashGrid\\\">\u001b[39m\n\u001b[31m      <div class=\\\"empty\\\"><div class=\\\"e-icon\\\">📊</div><p>No dispatches yet.<br>Create one to see the dashboard.</p></div>\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m    <div style=\\\"display:flex;gap:8px;margin-top:10px;flex-wrap:wrap;\\\">\u001b[39m\n\u001b[31m      <button class=\\\"btn btn-ghost\\\" style=\\\"flex:2;font-size:.8rem;\\\" id=\\\"btnRefreshAll\\\" onclick=\\\"refreshAllTracking()\\\">\u001b[39m\n\u001b[31m        🔄 Refresh All Tracking\u001b[39m\n\u001b[31m      </button>\u001b[39m\n\u001b[31m      <button class=\\\"btn btn-ghost\\\" style=\\\"flex:1;font-size:.8rem;border-color:var(--sky);color:var(--sky);\\\"\u001b[39m\n\u001b[31m        onclick=\\\"shareSelectedReport()\\\">\u001b[39m\n\u001b[31m        📊 Share Selected\u001b[39m\n\u001b[31m      </button>\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m  </div>·\u001b[39m\n\u001b[31m  <!-- ════════════ TRACK ════════════ -->\u001b[39m\n\u001b[31m  <div class=\\\"pane\\\" id=\\\"pane-track\\\">·\u001b[39m\n\u001b[31m    <!-- Courier selector -->\u001b[39m\n\u001b[31m    <div class=\\\"card\\\" style=\\\"margin-bottom:10px;\\\">\u001b[39m\n\u001b[31m      <div class=\\\"card-title\\\"><span class=\\\"ct-dot\\\" style=\\\"background:var(--sky);\\\"></span>Manual Tracking</div>\u001b[39m\n\u001b[31m      <div style=\\\"font-size:.72rem;color:var(--t2);margin-bottom:10px;line-height:1.5;\\\">\u001b[39m\n\u001b[31m        Track any docket number — select from already dispatched dockets or enter new ones manually.\u001b[39m\n\u001b[31m        SkyKing supports up to <strong>10 dockets at once</strong>; others are tracked one at a time.\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      <div class=\\\"f\\\">\u001b[39m\n\u001b[31m        <label>Courier Company</label>\u001b[39m\n\u001b[31m        <select id=\\\"t_courier\\\" onchange=\\\"buildTrackDocketChips()\\\">\u001b[39m\n\u001b[31m          <option>SkyKing</option><option>DTDC</option><option>Delhivery</option>\u001b[39m\n\u001b[31m          <option>Blue Dart</option><option>FedEx</option><option>Ecom Express</option><option>India Post</option>\u001b[39m\n\u001b[31m        </select>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m    </div>·\u001b[39m\n\u001b[31m    <!-- Docket selector — from existing dispatches -->\u001b[39m\n\u001b[31m    <div class=\\\"card\\\" style=\\\"margin-bottom:10px;\\\">\u001b[39m\n\u001b[31m      <div class=\\\"card-title\\\"><span class=\\\"ct-dot\\\" style=\\\"background:var(--acc);\\\"></span>\u001b[39m\n\u001b[31m        Select from Dispatched Dockets\u001b[39m\n\u001b[31m        <span id=\\\"trackCourierLimit\\\" style=\\\"margin-left:auto;font-size:.65rem;color:var(--t3);font-weight:400;text-transform:none;letter-spacing:0;font-family:var(--fnt-mono);\\\"></span>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      <div id=\\\"trackDocketChips\\\" style=\\\"display:flex;flex-wrap:wrap;gap:6px;margin-bottom:10px;min-height:28px;\\\">\u001b[39m\n\u001b[31m        <span style=\\\"font-size:.72rem;color:var(--t3);font-family:var(--fnt-mono);\\\">No dispatched dockets yet</span>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      <div style=\\\"font-size:.68rem;color:var(--t3);font-family:var(--fnt-mono);margin-bottom:8px;\\\">\u001b[39m\n\u001b[31m        Tap a docket to add/remove from tracking list. Selected shown in orange.\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m    </div>·\u001b[39m\n\u001b[31m    <!-- Manual entry for new dockets -->\u001b[39m\n\u001b[31m    <div class=\\\"card\\\" style=\\\"margin-bottom:10px;\\\">\u001b[39m\n\u001b[31m      <div class=\\\"card-title\\\"><span class=\\\"ct-dot\\\" style=\\\"background:var(--grn);\\\"></span>Add New Docket Numbers</div>\u001b[39m\n\u001b[31m      <div class=\\\"f\\\" style=\\\"margin-bottom:6px;\\\">\u001b[39m\n\u001b[31m        <label>Enter docket numbers (comma or newline separated)</label>\u001b[39m\n\u001b[31m        <textarea id=\\\"t_manual_dockets\\\" placeholder=\\\"443005527&#10;443005528&#10;or: 443005527, 443005528\\\"\u001b[39m\n\u001b[31m          style=\\\"min-height:56px;font-family:var(--fnt-mono);font-size:.82rem;letter-spacing:.5px;\\\"></textarea>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      <button class=\\\"btn-sm primary\\\" onclick=\\\"addManualDockets()\\\" style=\\\"font-size:.72rem;\\\">+ Add to List</button>\u001b[39m\n\u001b[31m    </div>·\u001b[39m\n\u001b[31m    <!-- Selected dockets summary + track button -->\u001b[39m\n\u001b[31m    <div id=\\\"trackSelectedSummary\\\" style=\\\"display:none;\\\" class=\\\"card\\\" style=\\\"margin-bottom:10px;\\\">\u001b[39m\n\u001b[31m      <div style=\\\"display:flex;align-items:center;justify-content:space-between;margin-bottom:8px;\\\">\u001b[39m\n\u001b[31m        <div>\u001b[39m\n\u001b[31m          <span style=\\\"font-family:var(--fnt-mono);font-size:.72rem;color:var(--sky);\\\" id=\\\"trackSelectedCount\\\">0 selected</span>\u001b[39m\n\u001b[31m          <span style=\\\"font-family:var(--fnt-mono);font-size:.65rem;color:var(--t3);margin-left:8px;\\\" id=\\\"trackSelectedList\\\"></span>\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m        <button class=\\\"btn-sm\\\" onclick=\\\"clearTrackSelection()\\\">Clear</button>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      <div class=\\\"btn-row\\\">\u001b[39m\n\u001b[31m        <button class=\\\"btn btn-ghost\\\" onclick=\\\"openTrackOnCourierSite()\\\">🔗 Open Courier Site</button>\u001b[39m\n\u001b[31m        <button class=\\\"btn btn-orange\\\" onclick=\\\"trackSelected()\\\">🔍 Track All via Proxy</button>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m    </div>·\u001b[39m\n\u001b[31m    <div id=\\\"trackResultsArea\\\"></div>\u001b[39m\n\u001b[31m  </div>·\u001b[39m\n\u001b[31m  <!-- ════════════ HISTORY ════════════ -->\u001b[39m\n\u001b[31m  <div class=\\\"pane\\\" id=\\\"pane-history\\\">\u001b[39m\n\u001b[31m    <div id=\\\"historyList\\\">\u001b[39m\n\u001b[31m      <div class=\\\"empty\\\"><div class=\\\"e-icon\\\">📭</div><p>No dispatch history yet.</p></div>\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m  </div>·\u001b[39m\n\u001b[31m  <!-- ════════════ CONFIG ════════════ -->\u001b[39m\n\u001b[31m  <div class=\\\"pane\\\" id=\\\"pane-config\\\">·\u001b[39m\n\u001b[31m    <!-- SHEETS URL — the only thing you type manually in the PWA -->\u001b[39m\n\u001b[31m    <div class=\\\"card\\\" style=\\\"border-color:var(--acc);border-width:2px;\\\">\u001b[39m\n\u001b[31m      <div class=\\\"card-title\\\"><span class=\\\"ct-dot\\\"></span>Google Sheets — Master Config</div>\u001b[39m\n\u001b[31m      <div class=\\\"info-tag\\\" style=\\\"background:rgba(34,197,94,.07);border-color:rgba(34,197,94,.3);\\\">\u001b[39m\n\u001b[31m        <span>✅</span>\u001b[39m\n\u001b[31m        <span>URL is loaded automatically from <strong>config.json</strong> on the VPS — no manual entry needed. All other settings sync from your <strong>Google Sheet</strong>.</span>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      <div class=\\\"f\\\">\u001b[39m\n\u001b[31m        <label>Apps Script URL <span style=\\\"font-size:.65rem;color:var(--t3);font-weight:400;\\\">(auto-loaded from config.json)</span></label>\u001b[39m\n\u001b[31m        <input type=\\\"url\\\" id=\\\"cfg_sheets_url\\\"\u001b[39m\n\u001b[31m          placeholder=\\\"Auto-loaded from server...\\\"\u001b[39m\n\u001b[31m          oninput=\\\"onSheetsUrlChange()\\\" readonly\u001b[39m\n\u001b[31m          style=\\\"opacity:.7;cursor:default;\\\">\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      <div id=\\\"syncStatus\\\" class=\\\"info-tag\\\" style=\\\"display:none;\\\">\u001b[39m\n\u001b[31m        <span id=\\\"syncIcon\\\">⏳</span>\u001b[39m\n\u001b[31m        <span id=\\\"syncMsg\\\">Syncing from sheet...</span>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      <div class=\\\"btn-row\\\">\u001b[39m\n\u001b[31m        <button class=\\\"btn btn-ghost\\\" onclick=\\\"syncFromSheet()\\\">🔄 Sync Now</button>\u001b[39m\n\u001b[31m        <button class=\\\"btn btn-orange\\\" onclick=\\\"saveSheetsUrl()\\\">💾 Save & Sync</button>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m    </div>·\u001b[39m\n\u001b[31m    <!-- READ-ONLY display of what was loaded from sheet -->\u001b[39m\n\u001b[31m    <div class=\\\"card\\\" id=\\\"sheetConfigDisplay\\\" style=\\\"display:none;\\\">\u001b[39m\n\u001b[31m      <div class=\\\"card-title\\\"><span class=\\\"ct-dot\\\" style=\\\"background:var(--grn);\\\"></span>Config Loaded from Sheet\u001b[39m\n\u001b[31m        <span id=\\\"sheetSyncTime\\\" style=\\\"margin-left:auto;font-size:.62rem;color:var(--t3);font-family:var(--fnt-mono);font-weight:400;text-transform:none;letter-spacing:0;\\\"></span>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      <div id=\\\"sheetConfigRows\\\"></div>\u001b[39m\n\u001b[31m      <div class=\\\"info-tag\\\" style=\\\"margin-top:8px;\\\">\u001b[39m\n\u001b[31m        <span>✏️</span><span>To change any value, edit the <strong>Config sheet</strong> in Google Sheets, then tap Sync Now above.</span>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m    </div>·\u001b[39m\n\u001b[31m    <!-- Courier Master (from sheet) -->\u001b[39m\n\u001b[31m    <div class=\\\"card\\\">\u001b[39m\n\u001b[31m      <div class=\\\"card-title\\\"><span class=\\\"ct-dot\\\" style=\\\"background:var(--ylw);\\\"></span>Courier Master</div>\u001b[39m\n\u001b[31m      <div id=\\\"courierMasterList\\\"></div>\u001b[39m\n\u001b[31m      <button class=\\\"btn btn-ghost\\\" style=\\\"margin-top:8px;font-size:.78rem;\\\" onclick=\\\"openCourierMaster()\\\">✏️ Edit in Sheet / Override</button>\u001b[39m\n\u001b[31m    </div>·\u001b[39m\n\u001b[31m    <button class=\\\"btn\\\" style=\\\"background:var(--red-dim);color:var(--red);border:1px solid var(--red);margin-top:4px;\\\" onclick=\\\"clearAll()\\\">🗑️ Clear All Local Data</button>\u001b[39m\n\u001b[31m    <div style=\\\"height:20px;\\\"></div>\u001b[39m\n\u001b[31m  </div>··\u001b[39m\n\u001b[31m<!-- ════ ADDRESS REVIEW PANE ════ -->\u001b[39m\n\u001b[31m<div id=\\\"addrReviewPane\\\" style=\\\"display:none;position:fixed;inset:0;background:var(--bg);z-index:50;overflow-y:auto;padding:14px;\\\">\u001b[39m\n\u001b[31m  <div class=\\\"verify-hdr\\\">\u001b[39m\n\u001b[31m    <button class=\\\"btn-icon\\\" onclick=\\\"document.getElementById('addrReviewPane').style.display='none'\\\" style=\\\"font-size:1.1rem;\\\">←</button>\u001b[39m\n\u001b[31m    <div class=\\\"verify-title\\\">Review AI-Parsed Addresses</div>\u001b[39m\n\u001b[31m  </div>\u001b[39m\n\u001b[31m  <div style=\\\"background:rgba(56,189,248,.08);border:1px solid rgba(56,189,248,.3);border-radius:var(--r-sm);\u001b[39m\n\u001b[31m    padding:8px 12px;margin-bottom:12px;font-size:.72rem;color:var(--sky);\\\">\u001b[39m\n\u001b[31m    ✏️ Review each row — correct name and city if wrong. Raw address shown below each row for reference.\u001b[39m\n\u001b[31m    Corrections are saved and improve future AI parsing automatically.\u001b[39m\n\u001b[31m  </div>\u001b[39m\n\u001b[31m  <div class=\\\"verify-summary\\\" id=\\\"addrReviewSummary\\\"></div>\u001b[39m\n\u001b[31m  <div id=\\\"addrReviewList\\\"></div>\u001b[39m\n\u001b[31m  <div style=\\\"position:sticky;bottom:0;background:var(--bg);padding-top:10px;\u001b[39m\n\u001b[31m    border-top:1px solid var(--bdr);margin-top:8px;\\\">\u001b[39m\n\u001b[31m    <div style=\\\"display:flex;gap:8px;\\\">\u001b[39m\n\u001b[31m      <button class=\\\"btn btn-ghost\\\" onclick=\\\"backToMapping()\\\" style=\\\"flex:1;\\\">← Back to Mapping</button>\u001b[39m\n\u001b[31m      <button class=\\\"btn btn-orange\\\" onclick=\\\"confirmAddressReview()\\\" style=\\\"flex:2;\\\">✓ Confirm &amp; Use These Addresses</button>\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m    <div style=\\\"font-size:.65rem;color:var(--t3);text-align:center;margin-top:5px;font-family:var(--fnt-mono);\\\">\u001b[39m\n\u001b[31m      Edits you made above are preserved when you go back\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m  </div>\u001b[39m\n\u001b[31m</div>·\u001b[39m\n\u001b[31m<!-- ════ VERIFICATION SCREEN ════ -->\u001b[39m\n\u001b[31m<div id=\\\"verifyPane\\\">\u001b[39m\n\u001b[31m  <div class=\\\"verify-hdr\\\">\u001b[39m\n\u001b[31m    <button class=\\\"btn-icon\\\" onclick=\\\"closeVerify()\\\" style=\\\"font-size:1.1rem;\\\">←</button>\u001b[39m\n\u001b[31m    <div class=\\\"verify-title\\\">Verify Before Finalise</div>\u001b[39m\n\u001b[31m    <button class=\\\"btn-sm\\\" onclick=\\\"approveAll()\\\" style=\\\"white-space:nowrap;\\\">✓ Approve All</button>\u001b[39m\n\u001b[31m  </div>\u001b[39m\n\u001b[31m  <div class=\\\"verify-summary\\\" id=\\\"verifySummary\\\"></div>\u001b[39m\n\u001b[31m  <div id=\\\"verifyList\\\"></div>\u001b[39m\n\u001b[31m  <div style=\\\"position:sticky;bottom:0;background:var(--bg);padding-top:10px;\u001b[39m\n\u001b[31m    border-top:1px solid var(--bdr);margin-top:8px;\\\">\u001b[39m\n\u001b[31m    <div style=\\\"display:flex;gap:8px;flex-wrap:wrap;\\\">\u001b[39m\n\u001b[31m      <button class=\\\"btn btn-ghost\\\" onclick=\\\"closeVerify()\\\" style=\\\"flex:1;\\\">← Back to Edit</button>\u001b[39m\n\u001b[31m      <button class=\\\"btn btn-ghost\\\" onclick=\\\"exportCleanSheet()\\\" style=\\\"flex:1;border-color:var(--sky);color:var(--sky);\\\">\u001b[39m\n\u001b[31m        📥 Export Clean Sheet\u001b[39m\n\u001b[31m      </button>\u001b[39m\n\u001b[31m      <button class=\\\"btn btn-orange\\\" id=\\\"btnConfirmFinalise\\\" onclick=\\\"confirmAndFinalise()\\\" style=\\\"flex:2;\\\">\u001b[39m\n\u001b[31m        ✓ Finalise Dispatch\u001b[39m\n\u001b[31m      </button>\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m  </div>\u001b[39m\n\u001b[31m</div>·\u001b[39m\n\u001b[31m</div><!-- /app -->·\u001b[39m\n\u001b[31m<!-- ════ DISPATCH DETAIL SHEET ════ -->\u001b[39m\n\u001b[31m<div class=\\\"overlay\\\" id=\\\"detailOverlay\\\" onclick=\\\"closeOverlay('detailOverlay')\\\">\u001b[39m\n\u001b[31m  <div class=\\\"sheet\\\" onclick=\\\"event.stopPropagation()\\\">\u001b[39m\n\u001b[31m    <div class=\\\"sheet-handle\\\"></div>\u001b[39m\n\u001b[31m    <div id=\\\"detailContent\\\"></div>\u001b[39m\n\u001b[31m  </div>\u001b[39m\n\u001b[31m</div>·\u001b[39m\n\u001b[31m<!-- ════ COURIER MASTER EDITOR ════ -->\u001b[39m\n\u001b[31m<div class=\\\"overlay\\\" id=\\\"courierMasterOverlay\\\" onclick=\\\"closeOverlay('courierMasterOverlay')\\\">\u001b[39m\n\u001b[31m  <div class=\\\"sheet\\\" onclick=\\\"event.stopPropagation()\\\">\u001b[39m\n\u001b[31m    <div class=\\\"sheet-handle\\\"></div>\u001b[39m\n\u001b[31m    <div class=\\\"sheet-title\\\">✏️ Courier Master</div>\u001b[39m\n\u001b[31m    <div id=\\\"courierMasterEditor\\\"></div>\u001b[39m\n\u001b[31m  </div>\u001b[39m\n\u001b[31m</div>·\u001b[39m\n\u001b[31m<!-- ════ MANUAL TRACKING OVERLAY ════ -->\u001b[39m\n\u001b[31m<div class=\\\"overlay\\\" id=\\\"manualTrackOverlay\\\" onclick=\\\"closeOverlay('manualTrackOverlay')\\\" data-docket=\\\"\\\" data-dispatch-id=\\\"\\\">\u001b[39m\n\u001b[31m  <div class=\\\"sheet\\\" onclick=\\\"event.stopPropagation()\\\">\u001b[39m\n\u001b[31m    <div class=\\\"sheet-handle\\\"></div>\u001b[39m\n\u001b[31m    <div class=\\\"sheet-title\\\">📞 Manual Tracking Log</div>\u001b[39m\n\u001b[31m    <div style=\\\"background:var(--sur2);border:1px solid var(--bdr);border-radius:var(--r-sm);\u001b[39m\n\u001b[31m      padding:8px 12px;margin-bottom:12px;font-family:var(--fnt-mono);font-size:.82rem;color:var(--acc);\\\"\u001b[39m\n\u001b[31m      id=\\\"mtDocketDisplay\\\"></div>\u001b[39m\n\u001b[31m    <div class=\\\"row2\\\">\u001b[39m\n\u001b[31m      <div class=\\\"f\\\">\u001b[39m\n\u001b[31m        <label>Status Reported <span class=\\\"req\\\">*</span></label>\u001b[39m\n\u001b[31m        <select id=\\\"mtStatus\\\">\u001b[39m\n\u001b[31m          <option value=\\\"\\\">Select...</option>\u001b[39m\n\u001b[31m          <option value=\\\"delivered\\\">✅ Delivered</option>\u001b[39m\n\u001b[31m          <option value=\\\"not received\\\">❌ Not Received</option>\u001b[39m\n\u001b[31m          <option value=\\\"in transit\\\">🚚 In Transit / On the way</option>\u001b[39m\n\u001b[31m          <option value=\\\"will deliver tomorrow\\\">📅 Will Deliver Tomorrow</option>\u001b[39m\n\u001b[31m          <option value=\\\"refused\\\">🚫 Refused / Returned</option>\u001b[39m\n\u001b[31m          <option value=\\\"partial\\\">📦 Partial Delivery</option>\u001b[39m\n\u001b[31m        </select>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      <div class=\\\"f\\\">\u001b[39m\n\u001b[31m        <label>Called By (Staff) <span class=\\\"req\\\">*</span></label>\u001b[39m\n\u001b[31m        <input type=\\\"text\\\" id=\\\"mtCalledBy\\\" placeholder=\\\"Your name\\\" list=\\\"staffList\\\">\u001b[39m\n\u001b[31m        <datalist id=\\\"staffList\\\">\u001b[39m\n\u001b[31m          <option>Mukesh</option><option>Sachin</option><option>Shambhu</option>\u001b[39m\n\u001b[31m          <option>Umashankar</option><option>Narender</option><option>Rakesh</option>\u001b[39m\n\u001b[31m          <option>Gurpreet</option><option>Ashok</option>\u001b[39m\n\u001b[31m        </datalist>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m    <div class=\\\"row2\\\">\u001b[39m\n\u001b[31m      <div class=\\\"f\\\">\u001b[39m\n\u001b[31m        <label>Contact Number Used</label>\u001b[39m\n\u001b[31m        <input type=\\\"tel\\\" id=\\\"mtContactUsed\\\" placeholder=\\\"Mobile number called\\\">\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      <div class=\\\"f\\\">\u001b[39m\n\u001b[31m        <label>Person Who Confirmed</label>\u001b[39m\n\u001b[31m        <input type=\\\"text\\\" id=\\\"mtContactPerson\\\" placeholder=\\\"Name of person at store\\\">\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m    <div class=\\\"f\\\">\u001b[39m\n\u001b[31m      <label>Remarks</label>\u001b[39m\n\u001b[31m      <textarea id=\\\"mtRemarks\\\" placeholder=\\\"Any notes, delivery details, issues...\\\" style=\\\"min-height:60px;\\\"></textarea>\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m    <div class=\\\"f\\\">\u001b[39m\n\u001b[31m      <label>Call Date &amp; Time</label>\u001b[39m\n\u001b[31m      <input type=\\\"datetime-local\\\" id=\\\"mtCallTime\\\">\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m    <div class=\\\"btn-row\\\">\u001b[39m\n\u001b[31m      <button class=\\\"btn btn-ghost\\\" onclick=\\\"closeOverlay('manualTrackOverlay')\\\">Cancel</button>\u001b[39m\n\u001b[31m      <button class=\\\"btn btn-orange\\\" onclick=\\\"submitManualTracking()\\\">💾 Log &amp; Save</button>\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m  </div>\u001b[39m\n\u001b[31m</div>·\u001b[39m\n\u001b[31m<!-- ════ POD UPLOAD OVERLAY ════ -->\u001b[39m\n\u001b[31m<div class=\\\"overlay\\\" id=\\\"podOverlay\\\" onclick=\\\"closeOverlay('podOverlay')\\\" data-docket=\\\"\\\" data-dispatch-id=\\\"\\\">\u001b[39m\n\u001b[31m  <div class=\\\"sheet\\\" onclick=\\\"event.stopPropagation()\\\">\u001b[39m\n\u001b[31m    <div class=\\\"sheet-handle\\\"></div>\u001b[39m\n\u001b[31m    <div class=\\\"sheet-title\\\">📸 Upload Proof of Delivery</div>\u001b[39m\n\u001b[31m    <div class=\\\"f\\\">\u001b[39m\n\u001b[31m      <label>POD / Delivery Slip Photo</label>\u001b[39m\n\u001b[31m      <input type=\\\"file\\\" id=\\\"podFileInput\\\" accept=\\\"image/*\\\" capture=\\\"environment\\\"\u001b[39m\n\u001b[31m        onchange=\\\"previewPOD(this,'pod')\\\" style=\\\"font-size:.8rem;color:var(--t2);\\\">\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m    <div class=\\\"f\\\">\u001b[39m\n\u001b[31m      <label>Recipient / Receiver Photo (optional)</label>\u001b[39m\n\u001b[31m      <input type=\\\"file\\\" id=\\\"recipientFileInput\\\" accept=\\\"image/*\\\" capture=\\\"environment\\\"\u001b[39m\n\u001b[31m        onchange=\\\"previewPOD(this,'recip')\\\" style=\\\"font-size:.8rem;color:var(--t2);\\\">\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m    <div id=\\\"podPreview\\\" style=\\\"display:flex;gap:8px;flex-wrap:wrap;margin-bottom:12px;\\\"></div>\u001b[39m\n\u001b[31m    <div class=\\\"btn-row\\\">\u001b[39m\n\u001b[31m      <button class=\\\"btn btn-ghost\\\" onclick=\\\"closeOverlay('podOverlay')\\\">Cancel</button>\u001b[39m\n\u001b[31m      <button class=\\\"btn btn-orange\\\" onclick=\\\"uploadPODFiles()\\\">📤 Upload & Mark Delivered</button>\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m  </div>\u001b[39m\n\u001b[31m</div>·\u001b[39m\n\u001b[31m<!-- ════ ADMIN LOGIN OVERLAY ════ -->\u001b[39m\n\u001b[31m<div class=\\\"overlay\\\" id=\\\"adminLoginOverlay\\\" onclick=\\\"closeOverlay('adminLoginOverlay')\\\">\u001b[39m\n\u001b[31m  <div class=\\\"sheet\\\" onclick=\\\"event.stopPropagation()\\\" style=\\\"max-height:320px;\\\">\u001b[39m\n\u001b[31m    <div class=\\\"sheet-handle\\\"></div>\u001b[39m\n\u001b[31m    <div class=\\\"sheet-title\\\">🔐 Admin Access</div>\u001b[39m\n\u001b[31m    <div style=\\\"font-size:.78rem;color:var(--t2);margin-bottom:14px;\\\">Config settings require admin password.</div>\u001b[39m\n\u001b[31m    <div class=\\\"f\\\">\u001b[39m\n\u001b[31m      <label>Password</label>\u001b[39m\n\u001b[31m      <input type=\\\"password\\\" id=\\\"adminPwdInput\\\" placeholder=\\\"Enter admin password\\\"\u001b[39m\n\u001b[31m        onkeydown=\\\"if(event.key==='Enter')submitAdminLogin()\\\">\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m    <div class=\\\"btn-row\\\" style=\\\"margin-top:4px;\\\">\u001b[39m\n\u001b[31m      <button class=\\\"btn btn-ghost\\\" onclick=\\\"closeOverlay('adminLoginOverlay')\\\">Cancel</button>\u001b[39m\n\u001b[31m      <button class=\\\"btn btn-orange\\\" onclick=\\\"submitAdminLogin()\\\">🔓 Login</button>\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m  </div>\u001b[39m\n\u001b[31m</div>·\u001b[39m\n\u001b[31m<!-- ════ TOAST ════ -->\u001b[39m\n\u001b[31m<div class=\\\"toast\\\" id=\\\"toast\\\"></div>·\u001b[39m\n\u001b[31m<!-- ════ SCRIPTS ════ -->\u001b[39m\n\u001b[31m<!-- SheetJS for Excel/CSV parsing -->\u001b[39m\n\u001b[31m<script src=\\\"https://cdnjs.cloudflare.com/ajax/libs/xlsx/0.18.5/xlsx.full.min.js\\\"></script>·\u001b[39m\n\u001b[31m<script type=\\\"module\\\">\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31m// OCR — Transformers.js\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31mimport { pipeline, env } from 'https://cdn.jsdelivr.net/npm/@huggingface/transformers@3.5.2/dist/transformers.min.js';·\u001b[39m\n\u001b[31m// Cache models in browser\u001b[39m\n\u001b[31menv.allowLocalModels = false;\u001b[39m\n\u001b[31menv.useBrowserCache = true;·\u001b[39m\n\u001b[31mlet ocrPipeline = null;\u001b[39m\n\u001b[31mlet ocrLoading = false;·\u001b[39m\n\u001b[31mwindow.initOCR = async function() {\u001b[39m\n\u001b[31m  if (ocrPipeline || ocrLoading) return;\u001b[39m\n\u001b[31m  ocrLoading = true;\u001b[39m\n\u001b[31m  showAIProgress('Loading OCR model (first time ~120MB)...', 5);\u001b[39m\n\u001b[31m  try {\u001b[39m\n\u001b[31m    ocrPipeline = await pipeline('image-to-text', 'Xenova/trocr-base-printed', {\u001b[39m\n\u001b[31m      progress_callback: (p) => {\u001b[39m\n\u001b[31m        if (p.progress) {\u001b[39m\n\u001b[31m          showAIProgress(`Loading: ${p.file || 'model'} (${Math.round(p.progress)}%)`, p.progress);\u001b[39m\n\u001b[31m        }\u001b[39m\n\u001b[31m      }\u001b[39m\n\u001b[31m    });\u001b[39m\n\u001b[31m    showAIProgress('OCR model ready ✓', 100);\u001b[39m\n\u001b[31m    setTimeout(() => hideAIProgress(), 1500);\u001b[39m\n\u001b[31m    showToast('OCR ready ✓', 'ok');\u001b[39m\n\u001b[31m  } catch(e) {\u001b[39m\n\u001b[31m    console.warn('Transformers.js OCR failed:', e);\u001b[39m\n\u001b[31m    hideAIProgress();\u001b[39m\n\u001b[31m    showToast('OCR model load failed — will use Ollama fallback', 'info');\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m  ocrLoading = false;\u001b[39m\n\u001b[31m};·\u001b[39m\n\u001b[31mwindow.runOCROnImage = async function(imageData, filename) {\u001b[39m\n\u001b[31m  const cfg = window.appConfig;·\u001b[39m\n\u001b[31m  // Strategy 1: Transformers.js\u001b[39m\n\u001b[31m  if ((cfg.ocr_mode === 'transformers' || cfg.ocr_mode === 'both') && ocrPipeline) {\u001b[39m\n\u001b[31m    try {\u001b[39m\n\u001b[31m      const result = await ocrPipeline(imageData);\u001b[39m\n\u001b[31m      const text = result[0]?.generated_text || '';\u001b[39m\n\u001b[31m      return extractDocketFromText(text, 'transformers');\u001b[39m\n\u001b[31m    } catch(e) { /* fall through */ }\u001b[39m\n\u001b[31m  }·\u001b[39m\n\u001b[31m  // Strategy 2: Dispatch AI proxy → Ollama LLaVA (server-side, avoids CORS + port issues)\u001b[39m\n\u001b[31m  if (cfg.ocr_mode === 'ollama' || cfg.ocr_mode === 'both') {\u001b[39m\n\u001b[31m    try {\u001b[39m\n\u001b[31m      const resp = await fetch('/dispatch-ai/ocr', {\u001b[39m\n\u001b[31m        method: 'POST',\u001b[39m\n\u001b[31m        headers: { 'Content-Type': 'application/json' },\u001b[39m\n\u001b[31m        body: JSON.stringify({ imageBase64: imageData }),\u001b[39m\n\u001b[31m        signal: AbortSignal.timeout(180000)\u001b[39m\n\u001b[31m      });\u001b[39m\n\u001b[31m      const data = await resp.json();\u001b[39m\n\u001b[31m      if (data.success && data.docket) {\u001b[39m\n\u001b[31m        return { docket: data.docket, confidence: data.confidence, source: 'ollama' };\u001b[39m\n\u001b[31m      }\u001b[39m\n\u001b[31m    } catch(e) { /* fall through */ }\u001b[39m\n\u001b[31m  }·\u001b[39m\n\u001b[31m  return { docket: '', confidence: 0, source: 'failed' };\u001b[39m\n\u001b[31m};·\u001b[39m\n\u001b[31mfunction extractDocketFromText(text, source) {\u001b[39m\n\u001b[31m  // Pattern: typical courier docket = 8-15 digit numbers, sometimes with letters\u001b[39m\n\u001b[31m  const patterns = [\u001b[39m\n\u001b[31m    /\\\\b([A-Z]{0,3}\\\\d{8,15}[A-Z]{0,3})\\\\b/g,   // alphanumeric tracking\u001b[39m\n\u001b[31m    /\\\\b(\\\\d{9,15})\\\\b/g,                          // pure numeric\u001b[39m\n\u001b[31m    /\\\\b([A-Z]{2,4}\\\\d{6,12})\\\\b/g,               // letter prefix\u001b[39m\n\u001b[31m  ];\u001b[39m\n\u001b[31m  const candidates = [];\u001b[39m\n\u001b[31m  for (const pat of patterns) {\u001b[39m\n\u001b[31m    let m;\u001b[39m\n\u001b[31m    while ((m = pat.exec(text)) !== null) {\u001b[39m\n\u001b[31m      if (!candidates.includes(m[1])) candidates.push(m[1]);\u001b[39m\n\u001b[31m    }\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m  if (candidates.length === 0) return { docket: text.trim().slice(0, 20), confidence: 0.3, source };\u001b[39m\n\u001b[31m  // Pick longest candidate (usually most complete docket)\u001b[39m\n\u001b[31m  const best = candidates.sort((a, b) => b.length - a.length)[0];\u001b[39m\n\u001b[31m  const confidence = candidates.length === 1 ? 0.85 : 0.7;\u001b[39m\n\u001b[31m  return { docket: best, confidence, source };\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// Expose for non-module code\u001b[39m\n\u001b[31mwindow._ocrReady = () => ocrPipeline !== null;\u001b[39m\n\u001b[31m</script>·\u001b[39m\n\u001b[31m<script>\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31m// STATE & STORAGE\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31mlet dispatches = [];\u001b[39m\n\u001b[31mlet stores = [];\u001b[39m\n\u001b[31mlet slipOCRData = [];\u001b[39m\n\u001b[31mlet dashFilter = 'all';·\u001b[39m\n\u001b[31m// Only sheets_url is persisted locally — all other config comes from the sheet\u001b[39m\n\u001b[31mlet _trackingInProgress = false;\u001b[39m\n\u001b[31mwindow.appConfig = {\u001b[39m\n\u001b[31m  sheets_url:      'https://script.google.com/macros/s/AKfycbys8HrToDv2u_dRvSFnNqmO9bnyAO1s8oSprUydpVQnNQutq4tuUY71cVCH24urbvM9/exec',\u001b[39m\n\u001b[31m  webhook_url:     '',\u001b[39m\n\u001b[31m  proxy_url:       '',\u001b[39m\n\u001b[31m  ollama_url:      'http://srv1111289.hstgr.cloud:11434',\u001b[39m\n\u001b[31m  ocr_mode:        'both',\u001b[39m\n\u001b[31m  company:         '360 Degree Logical Marketing',\u001b[39m\n\u001b[31m  city:            'Chandigarh',\u001b[39m\n\u001b[31m  auto_log:        'true',\u001b[39m\n\u001b[31m  refresh_on_load: 'false',\u001b[39m\n\u001b[31m};·\u001b[39m\n\u001b[31mconst COURIER_MASTER = {\u001b[39m\n\u001b[31m  'SkyKing': {\u001b[39m\n\u001b[31m    trackUrl: 'https://skyking.co/track?cno={docket}',\u001b[39m\n\u001b[31m    trackBulkUrl: 'https://skyking.co/track',\u001b[39m\n\u001b[31m    apiProxy: '/track-proxy/skyking?cno={docket}',\u001b[39m\n\u001b[31m    maxBulk: 10\u001b[39m\n\u001b[31m  },\u001b[39m\n\u001b[31m  'DTDC': {\u001b[39m\n\u001b[31m    trackUrl: 'https://www.dtdc.in/tracking.asp',\u001b[39m\n\u001b[31m    queryParam: 'trkId',\u001b[39m\n\u001b[31m    maxBulk: 1\u001b[39m\n\u001b[31m  },\u001b[39m\n\u001b[31m  'Delhivery': {\u001b[39m\n\u001b[31m    trackUrl: 'https://www.delhivery.com/track/package/{docket}',\u001b[39m\n\u001b[31m    maxBulk: 1\u001b[39m\n\u001b[31m  },\u001b[39m\n\u001b[31m  'Blue Dart': {\u001b[39m\n\u001b[31m    trackUrl: 'https://www.bluedart.com/tracking',\u001b[39m\n\u001b[31m    maxBulk: 1\u001b[39m\n\u001b[31m  },\u001b[39m\n\u001b[31m  'FedEx': {\u001b[39m\n\u001b[31m    trackUrl: 'https://www.fedex.com/fedextrack/?trknbr={docket}',\u001b[39m\n\u001b[31m    maxBulk: 10\u001b[39m\n\u001b[31m  },\u001b[39m\n\u001b[31m  'Ecom Express': {\u001b[39m\n\u001b[31m    trackUrl: 'https://ecomexpress.in/tracking/?awb_field={docket}',\u001b[39m\n\u001b[31m    maxBulk: 1\u001b[39m\n\u001b[31m  },\u001b[39m\n\u001b[31m  'India Post': {\u001b[39m\n\u001b[31m    trackUrl: 'https://www.indiapost.gov.in/VAS/Pages/trackconsignment.aspx',\u001b[39m\n\u001b[31m    maxBulk: 1\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m};·\u001b[39m\n\u001b[31mfunction loadData() {\u001b[39m\n\u001b[31m  try {\u001b[39m\n\u001b[31m    dispatches = JSON.parse(localStorage.getItem('360lm_disp_v2') || '[]');\u001b[39m\n\u001b[31m    const saved = JSON.parse(localStorage.getItem('360lm_disp_sheets_url') || '{}');\u001b[39m\n\u001b[31m    if (saved.sheets_url) window.appConfig.sheets_url = saved.sheets_url;\u001b[39m\n\u001b[31m  } catch(e) {}\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31mfunction saveData() {\u001b[39m\n\u001b[31m  try {\u001b[39m\n\u001b[31m    localStorage.setItem('360lm_disp_v2', JSON.stringify(dispatches));\u001b[39m\n\u001b[31m  } catch(e) {\u001b[39m\n\u001b[31m    if (e.name === 'QuotaExceededError' || e.code === 22) {\u001b[39m\n\u001b[31m      showToast('Storage full — old dispatches cleared. Data is safe in Postgres.', 'err');\u001b[39m\n\u001b[31m      // Drop delivered dispatches older than 30 days to free space\u001b[39m\n\u001b[31m      const cutoff = Date.now() - 30 * 24 * 3600 * 1000;\u001b[39m\n\u001b[31m      dispatches = dispatches.filter(d =>\u001b[39m\n\u001b[31m        d.status !== 'draft' && (\u001b[39m\n\u001b[31m          d.status !== 'active' ||\u001b[39m\n\u001b[31m          new Date(d.createdAt || 0).getTime() > cutoff\u001b[39m\n\u001b[31m        ) ? true :\u001b[39m\n\u001b[31m        d.status === 'draft' || new Date(d.createdAt || 0).getTime() > cutoff\u001b[39m\n\u001b[31m      );\u001b[39m\n\u001b[31m      try { localStorage.setItem('360lm_disp_v2', JSON.stringify(dispatches)); } catch(_) {}\u001b[39m\n\u001b[31m    }\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// ── Postgres DB sync ──────────────────────────────────────────────────────\u001b[39m\n\u001b[31mconst _DB        = '/db';\u001b[39m\n\u001b[31m// PostgREST v12: Accept-Profile for reads, Content-Profile for writes\u001b[39m\n\u001b[31mconst _DHDR_READ  = { 'Accept-Profile': 'dispatch' };\u001b[39m\n\u001b[31mconst _DHDR_WRITE = { 'Content-Type': 'application/json', 'Content-Profile': 'dispatch', 'Accept-Profile': 'dispatch' };\u001b[39m\n\u001b[31mconst _DHDR_UPSERT = { ..._DHDR_WRITE, 'Prefer': 'resolution=merge-duplicates,return=minimal' };·\u001b[39m\n\u001b[31mfunction _dispRow(d) {\u001b[39m\n\u001b[31m  return {\u001b[39m\n\u001b[31m    dispatch_id:    d.id,\u001b[39m\n\u001b[31m    client:         d.client    || '',\u001b[39m\n\u001b[31m    job_ref:        d.jobref    || null,\u001b[39m\n\u001b[31m    courier:        d.courier   || '',\u001b[39m\n\u001b[31m    dispatch_date:  d.dispatchDate || null,\u001b[39m\n\u001b[31m    status:         d.status    || 'active',\u001b[39m\n\u001b[31m    total_count:    (d.consignees || []).length,\u001b[39m\n\u001b[31m    docketed_count: (d.consignees || []).filter(c => c.docket).length,\u001b[39m\n\u001b[31m    remarks:        d.remarks   || null,\u001b[39m\n\u001b[31m    created_by:     window._hubUser?.id || 'unknown',\u001b[39m\n\u001b[31m  };\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction _consRow(c, dispatchId, idx) {\u001b[39m\n\u001b[31m  return {\u001b[39m\n\u001b[31m    cons_id:            `${dispatchId}-${idx}`,\u001b[39m\n\u001b[31m    dispatch_id:        dispatchId,\u001b[39m\n\u001b[31m    seq_no:             idx,\u001b[39m\n\u001b[31m    name:               c.name          || '',\u001b[39m\n\u001b[31m    city:               c.city          || null,\u001b[39m\n\u001b[31m    address:            c.address       || null,\u001b[39m\n\u001b[31m    phone:              c.phone         || null,\u001b[39m\n\u001b[31m    pincode:            c.pincode       || null,\u001b[39m\n\u001b[31m    material:           c.material      || null,\u001b[39m\n\u001b[31m    qty:                c.qty           || null,\u001b[39m\n\u001b[31m    docket:             c.docket        || null,\u001b[39m\n\u001b[31m    status:             c.status        || 'pending',\u001b[39m\n\u001b[31m    last_event:         c.lastEvent     || null,\u001b[39m\n\u001b[31m    last_scan_location: c.lastScanLocation || null,\u001b[39m\n\u001b[31m    last_tracked_at:    c.lastTrackedAt || null,\u001b[39m\n\u001b[31m    manual_status:      c.manualStatus  || null,\u001b[39m\n\u001b[31m    manual_delivered:   !!c.manualDelivered,\u001b[39m\n\u001b[31m    manual_called_by:   c.manualCalledBy || null,\u001b[39m\n\u001b[31m    manual_remarks:     c.manualRemarks || null,\u001b[39m\n\u001b[31m    pod_uploaded:       !!c.podUploaded,\u001b[39m\n\u001b[31m  };\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31masync function syncDispatchToDB(d) {\u001b[39m\n\u001b[31m  if (!d || d.status === 'draft') return; // never sync drafts\u001b[39m\n\u001b[31m  try {\u001b[39m\n\u001b[31m    await fetch(`${_DB}/dispatches`, {\u001b[39m\n\u001b[31m      method: 'POST', headers: _DHDR_UPSERT,\u001b[39m\n\u001b[31m      body: JSON.stringify(_dispRow(d)),\u001b[39m\n\u001b[31m    });\u001b[39m\n\u001b[31m    const rows = (d.consignees || []).map((c, i) => _consRow(c, d.id, i));\u001b[39m\n\u001b[31m    if (rows.length) {\u001b[39m\n\u001b[31m      await fetch(`${_DB}/consignees`, {\u001b[39m\n\u001b[31m        method: 'POST', headers: _DHDR_UPSERT,\u001b[39m\n\u001b[31m        body: JSON.stringify(rows),\u001b[39m\n\u001b[31m      });\u001b[39m\n\u001b[31m    }\u001b[39m\n\u001b[31m  } catch(e) { console.warn('[dispatch-db] sync error:', e.message); }\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31masync function syncConsigneeToDB(c, dispatchId, idx) {\u001b[39m\n\u001b[31m  try {\u001b[39m\n\u001b[31m    await fetch(`${_DB}/consignees`, {\u001b[39m\n\u001b[31m      method: 'POST', headers: _DHDR_UPSERT,\u001b[39m\n\u001b[31m      body: JSON.stringify(_consRow(c, dispatchId, idx)),\u001b[39m\n\u001b[31m    });\u001b[39m\n\u001b[31m    // Log tracking event\u001b[39m\n\u001b[31m    if (c.lastEvent && c.status) {\u001b[39m\n\u001b[31m      fetch(`${_DB}/tracking_events`, {\u001b[39m\n\u001b[31m        method: 'POST', headers: _DHDR_WRITE,\u001b[39m\n\u001b[31m        body: JSON.stringify({\u001b[39m\n\u001b[31m          docket: c.docket, dispatch_id: dispatchId,\u001b[39m\n\u001b[31m          cons_id: `${dispatchId}-${idx}`,\u001b[39m\n\u001b[31m          status: c.status, event_text: c.lastEvent,\u001b[39m\n\u001b[31m          scan_location: c.lastScanLocation || null,\u001b[39m\n\u001b[31m        }),\u001b[39m\n\u001b[31m      }).catch(() => {});\u001b[39m\n\u001b[31m    }\u001b[39m\n\u001b[31m  } catch(e) { console.warn('[dispatch-db] consignee sync error:', e.message); }\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31masync function loadFromDB() {\u001b[39m\n\u001b[31m  try {\u001b[39m\n\u001b[31m    const [dResp, cResp] = await Promise.all([\u001b[39m\n\u001b[31m      fetch(`${_DB}/dispatches?order=created_at.desc&limit=100`, { headers: _DHDR_READ }),\u001b[39m\n\u001b[31m      fetch(`${_DB}/consignees?order=dispatch_id,seq_no`, { headers: _DHDR_READ }),\u001b[39m\n\u001b[31m    ]);\u001b[39m\n\u001b[31m    if (!dResp.ok) return;\u001b[39m\n\u001b[31m    const dbDispatches = await dResp.json();\u001b[39m\n\u001b[31m    const dbConsignees = cResp.ok ? await cResp.json() : [];\u001b[39m\n\u001b[31m    if (!dbDispatches.length) return;·\u001b[39m\n\u001b[31m    const dbIds = new Set(dbDispatches.map(d => d.dispatch_id));\u001b[39m\n\u001b[31m    const localOnly = dispatches.filter(d => !dbIds.has(d.id));·\u001b[39m\n\u001b[31m    const fromDB = dbDispatches.map(d => {\u001b[39m\n\u001b[31m      const cons = dbConsignees.filter(c => c.dispatch_id === d.dispatch_id);\u001b[39m\n\u001b[31m      return {\u001b[39m\n\u001b[31m        id:           d.dispatch_id,\u001b[39m\n\u001b[31m        client:       d.client,\u001b[39m\n\u001b[31m        jobref:       d.job_ref,\u001b[39m\n\u001b[31m        courier:      d.courier,\u001b[39m\n\u001b[31m        dispatchDate: d.dispatch_date,\u001b[39m\n\u001b[31m        status:       d.status,\u001b[39m\n\u001b[31m        remarks:      d.remarks,\u001b[39m\n\u001b[31m        createdAt:    d.created_at,\u001b[39m\n\u001b[31m        consignees:   cons.map(c => ({\u001b[39m\n\u001b[31m          name:              c.name,\u001b[39m\n\u001b[31m          city:              c.city,\u001b[39m\n\u001b[31m          address:           c.address,\u001b[39m\n\u001b[31m          phone:             c.phone,\u001b[39m\n\u001b[31m          pincode:           c.pincode,\u001b[39m\n\u001b[31m          material:          c.material,\u001b[39m\n\u001b[31m          qty:               c.qty,\u001b[39m\n\u001b[31m          docket:            c.docket,\u001b[39m\n\u001b[31m          status:            c.status,\u001b[39m\n\u001b[31m          lastEvent:         c.last_event,\u001b[39m\n\u001b[31m          lastScanLocation:  c.last_scan_location,\u001b[39m\n\u001b[31m          lastTrackedAt:     c.last_tracked_at,\u001b[39m\n\u001b[31m          manualStatus:      c.manual_status,\u001b[39m\n\u001b[31m          manualDelivered:   c.manual_delivered,\u001b[39m\n\u001b[31m          manualCalledBy:    c.manual_called_by,\u001b[39m\n\u001b[31m          manualRemarks:     c.manual_remarks,\u001b[39m\n\u001b[31m          podUploaded:       c.pod_uploaded,\u001b[39m\n\u001b[31m          _dbIdx:            c.seq_no,\u001b[39m\n\u001b[31m        })),\u001b[39m\n\u001b[31m      };\u001b[39m\n\u001b[31m    });·\u001b[39m\n\u001b[31m    dispatches = [...fromDB, ...localOnly];\u001b[39m\n\u001b[31m    saveData();\u001b[39m\n\u001b[31m    renderDashboard();\u001b[39m\n\u001b[31m    updateHistCount();\u001b[39m\n\u001b[31m  } catch(e) { console.warn('[dispatch-db] load error:', e.message); }\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31mfunction saveSheetsUrlLocal() {\u001b[39m\n\u001b[31m  localStorage.setItem('360lm_disp_sheets_url', JSON.stringify({ sheets_url: window.appConfig.sheets_url }));\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m/**\u001b[39m\n\u001b[31m * Fetch config.json from the VPS server (same origin).\u001b[39m\n\u001b[31m * This file contains the sheets_url — no manual entry needed.\u001b[39m\n\u001b[31m * Falls back to localStorage cache if fetch fails (offline).\u001b[39m\n\u001b[31m */\u001b[39m\n\u001b[31masync function loadServerConfig() {\u001b[39m\n\u001b[31m  try {\u001b[39m\n\u001b[31m    const resp = await fetch('./config.json', { cache: 'no-store' });\u001b[39m\n\u001b[31m    if (resp.ok) {\u001b[39m\n\u001b[31m      const cfg = await resp.json();\u001b[39m\n\u001b[31m      if (cfg.sheets_url) {\u001b[39m\n\u001b[31m        window.appConfig.sheets_url = cfg.sheets_url;\u001b[39m\n\u001b[31m        saveSheetsUrlLocal(); // cache it for offline use\u001b[39m\n\u001b[31m        document.getElementById('cfg_sheets_url').value = cfg.sheets_url;\u001b[39m\n\u001b[31m        return true;\u001b[39m\n\u001b[31m      }\u001b[39m\n\u001b[31m    }\u001b[39m\n\u001b[31m  } catch(e) {\u001b[39m\n\u001b[31m    console.log('config.json fetch failed, using cached URL:', e.message);\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m  return false;\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31m// INIT\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31m// ── Hub auth gate ─────────────────────────────────────────────────────────\u001b[39m\n\u001b[31mwindow._hubUser = null;\u001b[39m\n\u001b[31m(function() {\u001b[39m\n\u001b[31m  try {\u001b[39m\n\u001b[31m    const hubLogin = '/hub/?next=' + encodeURIComponent(location.pathname + location.search);\u001b[39m\n\u001b[31m    const s = JSON.parse(localStorage.getItem('lm360-session') || 'null');\u001b[39m\n\u001b[31m    if (!s || !s.empId || !s.name) { window.location.replace(hubLogin); return; }\u001b[39m\n\u001b[31m    if (Date.now() - new Date(s.loginAt).getTime() > 12 * 3600 * 1000) {\u001b[39m\n\u001b[31m      window.location.replace(hubLogin); return;\u001b[39m\n\u001b[31m    }\u001b[39m\n\u001b[31m    window._hubUser = { id: s.empId, name: s.name, role: s.role };\u001b[39m\n\u001b[31m  } catch(_) { window.location.replace('/hub/?next=' + encodeURIComponent(location.pathname + location.search)); }\u001b[39m\n\u001b[31m})();·\u001b[39m\n\u001b[31mwindow.addEventListener('load', async () => {\u001b[39m\n\u001b[31m  if (!window._hubUser) return; // should never reach here — gate redirected already\u001b[39m\n\u001b[31m  setTimeout(() => {\u001b[39m\n\u001b[31m    document.getElementById('splash').style.opacity = '0';\u001b[39m\n\u001b[31m    setTimeout(() => {\u001b[39m\n\u001b[31m      document.getElementById('splash').style.display = 'none';\u001b[39m\n\u001b[31m      document.getElementById('app').style.display = 'flex';\u001b[39m\n\u001b[31m    }, 450);\u001b[39m\n\u001b[31m  }, 1400);\u001b[39m\n\u001b[31m  loadData();\u001b[39m\n\u001b[31m  // Auto-authenticate config tab for admin users (no password needed)\u001b[39m\n\u001b[31m  if (['harish', 'pramod'].includes(window._hubUser?.id)) _adminAuthed = true;\u001b[39m\n\u001b[31m  setToday();\u001b[39m\n\u001b[31m  addConsignee();\u001b[39m\n\u001b[31m  updateHistCount();\u001b[39m\n\u001b[31m  setInterval(updateNetDot, 5000);\u001b[39m\n\u001b[31m  updateNetDot();\u001b[39m\n\u001b[31m  checkShareTarget();\u001b[39m\n\u001b[31m  setupDragDrop();·\u001b[39m\n\u001b[31m  // Step 1: fetch sheets_url from VPS config.json (auto, no user input)\u001b[39m\n\u001b[31m  await loadServerConfig();·\u001b[39m\n\u001b[31m  // Step 2: populate config tab field\u001b[39m\n\u001b[31m  document.getElementById('cfg_sheets_url').value = window.appConfig.sheets_url || '';·\u001b[39m\n\u001b[31m  // Step 3: load dispatches from Postgres in background (merge with local)\u001b[39m\n\u001b[31m  loadFromDB(); // non-blocking — updates UI when data arrives·\u001b[39m\n\u001b[31m  // Step 4: sync full config from Google Sheet silently\u001b[39m\n\u001b[31m  if (window.appConfig.sheets_url) {\u001b[39m\n\u001b[31m    syncFromSheet(true); // silent on startup\u001b[39m\n\u001b[31m  } else {\u001b[39m\n\u001b[31m    // Only show config tab if no URL found anywhere\u001b[39m\n\u001b[31m    showPane('config');\u001b[39m\n\u001b[31m    showToast('No config.json found on server', 'err');\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m  // Pre-warm OCR after delay\u001b[39m\n\u001b[31m  setTimeout(() => { if (typeof window.initOCR === 'function') window.initOCR(); }, 4000);\u001b[39m\n\u001b[31m});·\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31m// WEB SHARE TARGET\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31masync function checkShareTarget() {\u001b[39m\n\u001b[31m  // Check URL params for share target\u001b[39m\n\u001b[31m  const params = new URLSearchParams(window.location.search);\u001b[39m\n\u001b[31m  const action = params.get('action');\u001b[39m\n\u001b[31m  if (action !== 'share') return;·\u001b[39m\n\u001b[31m  // Check for shared files via Share Target API\u001b[39m\n\u001b[31m  if ('launchQueue' in window) {\u001b[39m\n\u001b[31m    window.launchQueue.setConsumer(async launchParams => {\u001b[39m\n\u001b[31m      if (!launchParams.files || launchParams.files.length === 0) return;\u001b[39m\n\u001b[31m      const files = await Promise.all(launchParams.files.map(f => f.getFile()));\u001b[39m\n\u001b[31m      const imageFiles = files.filter(f => f.type.startsWith('image/'));\u001b[39m\n\u001b[31m      if (imageFiles.length > 0) {\u001b[39m\n\u001b[31m        document.getElementById('shareTargetInfo').style.display = 'flex';\u001b[39m\n\u001b[31m        document.getElementById('sharedCount').textContent = imageFiles.length;\u001b[39m\n\u001b[31m        document.getElementById('shareBanner').classList.add('show');\u001b[39m\n\u001b[31m        setTimeout(() => document.getElementById('shareBanner').classList.remove('show'), 4000);\u001b[39m\n\u001b[31m        showPane('new');\u001b[39m\n\u001b[31m        await handleSlipFiles(imageFiles);\u001b[39m\n\u001b[31m      }\u001b[39m\n\u001b[31m    });\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction dismissShare() {\u001b[39m\n\u001b[31m  document.getElementById('shareBanner').classList.remove('show');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31m// DRAG & DROP\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31mfunction setupDragDrop() {\u001b[39m\n\u001b[31m  const zone = document.getElementById('slipZone');\u001b[39m\n\u001b[31m  zone.addEventListener('dragover', e => { e.preventDefault(); zone.classList.add('drag'); });\u001b[39m\n\u001b[31m  zone.addEventListener('dragleave', () => zone.classList.remove('drag'));\u001b[39m\n\u001b[31m  zone.addEventListener('drop', e => {\u001b[39m\n\u001b[31m    e.preventDefault();\u001b[39m\n\u001b[31m    zone.classList.remove('drag');\u001b[39m\n\u001b[31m    const files = Array.from(e.dataTransfer.files).filter(f => f.type.startsWith('image/'));\u001b[39m\n\u001b[31m    if (files.length) handleSlipFiles(files);\u001b[39m\n\u001b[31m  });\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31m// SLIP OCR HANDLER\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31masync function handleSlipFiles(files) {\u001b[39m\n\u001b[31m  const fileArr = Array.from(files);\u001b[39m\n\u001b[31m  if (!fileArr.length) return;\u001b[39m\n\u001b[31m  // If dockets already mapped from store list — allow upload but skip OCR extraction\u001b[39m\n\u001b[31m  const skipOCR = shouldSkipOCR();\u001b[39m\n\u001b[31m  if (skipOCR) {\u001b[39m\n\u001b[31m    showToast('Dockets already mapped — slips saved for reference only', 'info');\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m  showOCRWorking(true, `Processing ${fileArr.length} slip(s)...`);·\u001b[39m\n\u001b[31m  for (let i = 0; i < fileArr.length; i++) {\u001b[39m\n\u001b[31m    const file = fileArr[i];\u001b[39m\n\u001b[31m    showOCRWorking(true, `OCR: ${i+1}/${fileArr.length} — ${file.name}`);\u001b[39m\n\u001b[31m    const imageData = await readFileAsDataURL(file);\u001b[39m\n\u001b[31m    let result = { docket: '', confidence: 0, source: 'manual' };\u001b[39m\n\u001b[31m    if (!skipOCR) {\u001b[39m\n\u001b[31m      try {\u001b[39m\n\u001b[31m        result = await window.runOCROnImage(imageData, file.name);\u001b[39m\n\u001b[31m      } catch(e) {}\u001b[39m\n\u001b[31m    } else {\u001b[39m\n\u001b[31m      result = { docket: '', confidence: 1, source: 'skipped — docket pre-mapped' };\u001b[39m\n\u001b[31m    }\u001b[39m\n\u001b[31m    slipOCRData.push({ file, imageData, filename: file.name, ...result });\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m  showOCRWorking(false);\u001b[39m\n\u001b[31m  renderOCRResults();\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction readFileAsDataURL(file) {\u001b[39m\n\u001b[31m  return new Promise((res, rej) => {\u001b[39m\n\u001b[31m    const r = new FileReader();\u001b[39m\n\u001b[31m    r.onload = e => res(e.target.result);\u001b[39m\n\u001b[31m    r.onerror = rej;\u001b[39m\n\u001b[31m    r.readAsDataURL(file);\u001b[39m\n\u001b[31m  });\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction renderOCRResults() {\u001b[39m\n\u001b[31m  if (!slipOCRData.length) return;\u001b[39m\n\u001b[31m  const list = document.getElementById('ocrSlipList');\u001b[39m\n\u001b[31m  document.getElementById('ocrResults').classList.add('visible');·\u001b[39m\n\u001b[31m  const needReview = slipOCRData.filter(s => s.confidence < 0.65 && s.docket).length;\u001b[39m\n\u001b[31m  const autoOk     = slipOCRData.filter(s => s.confidence >= 0.88).length;\u001b[39m\n\u001b[31m  const summary    = document.getElementById('ocrSummary');\u001b[39m\n\u001b[31m  if (summary) {\u001b[39m\n\u001b[31m    summary.style.display = '';\u001b[39m\n\u001b[31m    summary.innerHTML =\u001b[39m\n\u001b[31m      `<span style=\\\"color:var(--grn)\\\">✓ ${autoOk} auto-accepted</span>` +\u001b[39m\n\u001b[31m      (needReview ? `&nbsp;·&nbsp;<span style=\\\"color:var(--ylw)\\\">⚠ ${needReview} need manual check</span>` : '');\u001b[39m\n\u001b[31m  }·\u001b[39m\n\u001b[31m  list.innerHTML = slipOCRData.map((s, i) => {\u001b[39m\n\u001b[31m    const autoAccepted = s.confidence >= 0.88 && s.docket;\u001b[39m\n\u001b[31m    const needsCheck   = s.confidence < 0.65  && s.docket;\u001b[39m\n\u001b[31m    const confClass = autoAccepted ? 'conf-high' : needsCheck ? 'conf-low' : 'conf-med';\u001b[39m\n\u001b[31m    const confLabel = autoAccepted\u001b[39m\n\u001b[31m      ? `✓ ${Math.round(s.confidence * 100)}% — auto`\u001b[39m\n\u001b[31m      : needsCheck\u001b[39m\n\u001b[31m        ? `⚠ ${Math.round(s.confidence * 100)}% — verify`\u001b[39m\n\u001b[31m        : `${Math.round(s.confidence * 100)}%`;\u001b[39m\n\u001b[31m    const borderStyle = needsCheck ? 'border:1px solid var(--ylw);' : '';\u001b[39m\n\u001b[31m    return `\u001b[39m\n\u001b[31m    <div class=\\\"ocr-slip\\\" id=\\\"ocrSlip_${i}\\\" style=\\\"${borderStyle}\\\">\u001b[39m\n\u001b[31m      <img src=\\\"${s.imageData}\\\" alt=\\\"slip ${i+1}\\\">\u001b[39m\n\u001b[31m      <div class=\\\"ocr-slip-body\\\">\u001b[39m\n\u001b[31m        <div class=\\\"ocr-slip-name\\\">${s.filename}</div>\u001b[39m\n\u001b[31m        <input type=\\\"text\\\" class=\\\"ocr-slip-docket\\\" id=\\\"ocrDocket_${i}\\\"\u001b[39m\n\u001b[31m          value=\\\"${s.docket}\\\" placeholder=\\\"Docket number\\\"\u001b[39m\n\u001b[31m          ${autoAccepted ? 'style=\\\"opacity:.6\\\"' : ''}\u001b[39m\n\u001b[31m          oninput=\\\"slipOCRData[${i}].docket=this.value;slipOCRData[${i}].confidence=0.5\\\">\u001b[39m\n\u001b[31m        <div class=\\\"ocr-slip-status\\\">\u001b[39m\n\u001b[31m          <span class=\\\"ocr-confidence ${confClass}\\\">${confLabel}</span>\u001b[39m\n\u001b[31m          <span style=\\\"color:var(--t4);font-family:var(--fnt-mono);font-size:.6rem;\\\">via ${s.source}</span>\u001b[39m\n\u001b[31m          <button class=\\\"ocr-ai-btn\\\" onclick=\\\"reOCR(${i})\\\">🔄 Re-OCR</button>\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m    </div>`;\u001b[39m\n\u001b[31m  }).join('');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31masync function reOCR(i) {\u001b[39m\n\u001b[31m  const s = slipOCRData[i];\u001b[39m\n\u001b[31m  showOCRWorking(true, `Re-running OCR on ${s.filename}...`);\u001b[39m\n\u001b[31m  try {\u001b[39m\n\u001b[31m    const result = await window.runOCROnImage(s.imageData, s.filename);\u001b[39m\n\u001b[31m    slipOCRData[i] = { ...s, ...result };\u001b[39m\n\u001b[31m    renderOCRResults();\u001b[39m\n\u001b[31m    showToast('Re-OCR complete', 'ok');\u001b[39m\n\u001b[31m  } catch(e) {\u001b[39m\n\u001b[31m    showToast('OCR failed', 'err');\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m  showOCRWorking(false);\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction clearSlips() {\u001b[39m\n\u001b[31m  slipOCRData = [];\u001b[39m\n\u001b[31m  document.getElementById('ocrResults').classList.remove('visible');\u001b[39m\n\u001b[31m  document.getElementById('ocrSlipList').innerHTML = '';\u001b[39m\n\u001b[31m  const sum = document.getElementById('ocrSummary');\u001b[39m\n\u001b[31m  if (sum) { sum.style.display = 'none'; sum.innerHTML = ''; }\u001b[39m\n\u001b[31m  document.getElementById('shareTargetInfo').style.display = 'none';\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction applyAllDockets() {\u001b[39m\n\u001b[31m  // Match each OCR result to a consignee by order or by name matching\u001b[39m\n\u001b[31m  const consList = document.getElementById('consList');\u001b[39m\n\u001b[31m  const cards = consList.querySelectorAll('.cons-card');\u001b[39m\n\u001b[31m  slipOCRData.forEach((s, i) => {\u001b[39m\n\u001b[31m    if (!s.docket) return;\u001b[39m\n\u001b[31m    const card = cards[i];\u001b[39m\n\u001b[31m    if (card) {\u001b[39m\n\u001b[31m      const docketInput = card.querySelector('.cons-docket-input');\u001b[39m\n\u001b[31m      if (docketInput) {\u001b[39m\n\u001b[31m        docketInput.value = s.docket;\u001b[39m\n\u001b[31m        showLinkedDocket(card, s.docket);\u001b[39m\n\u001b[31m      }\u001b[39m\n\u001b[31m    }\u001b[39m\n\u001b[31m  });\u001b[39m\n\u001b[31m  showToast(`Applied ${slipOCRData.filter(s=>s.docket).length} dockets ✓`, 'ok');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31m// STORE LIST — with column mapping UI\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31mlet _rawRows = [];      // raw rows from file, kept for re-mapping\u001b[39m\n\u001b[31mlet _rawHeaders = [];   // headers from file·\u001b[39m\n\u001b[31masync function handleStoreFile(input) {\u001b[39m\n\u001b[31m  const file = input.files[0];\u001b[39m\n\u001b[31m  if (!file) return;\u001b[39m\n\u001b[31m  showToast('Reading file...', 'info');·\u001b[39m\n\u001b[31m  const data = await readFileAsDataURL(file);\u001b[39m\n\u001b[31m  const base64 = data.split(',')[1];\u001b[39m\n\u001b[31m  const binary = atob(base64);\u001b[39m\n\u001b[31m  const bytes = new Uint8Array(binary.length);\u001b[39m\n\u001b[31m  for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);·\u001b[39m\n\u001b[31m  let rows = [];\u001b[39m\n\u001b[31m  try {\u001b[39m\n\u001b[31m    if (file.name.endsWith('.csv')) {\u001b[39m\n\u001b[31m      const text = new TextDecoder().decode(bytes);\u001b[39m\n\u001b[31m      rows = text.split('\\\\n').map(r => r.split(',').map(c => c.trim().replace(/^\\\"|\\\"$/g, '')));\u001b[39m\n\u001b[31m    } else {\u001b[39m\n\u001b[31m      const wb = XLSX.read(bytes, { type: 'array' });\u001b[39m\n\u001b[31m      const ws = wb.Sheets[wb.SheetNames[0]];\u001b[39m\n\u001b[31m      rows = XLSX.utils.sheet_to_json(ws, { header: 1 });\u001b[39m\n\u001b[31m    }\u001b[39m\n\u001b[31m  } catch(e) { showToast('File read error: ' + e.message, 'err'); return; }·\u001b[39m\n\u001b[31m  if (!rows.length || !rows[0]) { showToast('File appears empty', 'err'); return; }·\u001b[39m\n\u001b[31m  _rawHeaders = (rows[0] || []).map(h => (h || '').toString().trim());\u001b[39m\n\u001b[31m  _rawRows = rows.slice(1).filter(r => r && r.some(c => c !== '' && c != null));·\u001b[39m\n\u001b[31m  document.getElementById('storeTableArea').style.display = 'block';\u001b[39m\n\u001b[31m  renderMappingUI(file.name);\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// ── COLUMN MAPPING FIELDS ─────────────────────────────\u001b[39m\n\u001b[31mconst MAPPING_FIELDS = [\u001b[39m\n\u001b[31m  { key: 'name',    label: 'Store / Partner Name', required: true,\u001b[39m\n\u001b[31m    keywords: ['store','shop','name','outlet','consignee','destination','partner','dealer','client','retailer'] },\u001b[39m\n\u001b[31m  { key: 'city',    label: 'City / Location',       required: false,\u001b[39m\n\u001b[31m    keywords: ['city','town','district','location','area','region','place'] },\u001b[39m\n\u001b[31m  { key: 'phone',   label: 'Phone / Mobile',        required: false,\u001b[39m\n\u001b[31m    keywords: ['phone','mobile','contact','tel','number','mob'] },\u001b[39m\n\u001b[31m  { key: 'docket',  label: 'Docket / AWB No.',      required: false,\u001b[39m\n\u001b[31m    keywords: ['docket','awb','tracking','courier','dkt','consignment','cno','waybill'] },\u001b[39m\n\u001b[31m  { key: 'address', label: 'Address',               required: false,\u001b[39m\n\u001b[31m    keywords: ['address','addr','street','locality','area'] },\u001b[39m\n\u001b[31m  { key: 'pincode', label: 'Pincode / PIN',          required: false,\u001b[39m\n\u001b[31m    keywords: ['pin','pincode','postal','zip','code'] },\u001b[39m\n\u001b[31m];·\u001b[39m\n\u001b[31m// Saved mapping key based on column fingerprint (so same client file auto-maps)\u001b[39m\n\u001b[31mfunction getMappingKey() {\u001b[39m\n\u001b[31m  return 'colmap_' + _rawHeaders.join('|').toLowerCase().replace(/\\\\s+/g,'');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// ── Detect if a column is a combined/multiline address cell ─────────────\u001b[39m\n\u001b[31mfunction isCombinedColumn(colIdx) {\u001b[39m\n\u001b[31m  if (colIdx < 0) return false;\u001b[39m\n\u001b[31m  // Check first 3 data rows — if any cell contains newline or has 6+ digit number (pincode) = combined\u001b[39m\n\u001b[31m  const samples = _rawRows.slice(0, 3).map(r => (r[colIdx] || '').toString());\u001b[39m\n\u001b[31m  return samples.some(s => s.includes('\\\\n') || /\\\\b\\\\d{6}\\\\b/.test(s) || s.split(',').length >= 3);\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction renderMappingUI(filename) {\u001b[39m\n\u001b[31m  const saved = (() => { try { return JSON.parse(localStorage.getItem(getMappingKey()) || 'null'); } catch(e) { return null; } })();\u001b[39m\n\u001b[31m  const isSaved = !!saved;\u001b[39m\n\u001b[31m  const colInfoEl = document.getElementById('storeColInfo');\u001b[39m\n\u001b[31m  colInfoEl.style.display = 'block';·\u001b[39m\n\u001b[31m  const suggested = {};\u001b[39m\n\u001b[31m  MAPPING_FIELDS.forEach(f => {\u001b[39m\n\u001b[31m    const idx = findCol(_rawHeaders, f.keywords);\u001b[39m\n\u001b[31m    suggested[f.key] = saved ? (saved[f.key] ?? idx) : idx;\u001b[39m\n\u001b[31m  });·\u001b[39m\n\u001b[31m  // Detect if address column is combined\u001b[39m\n\u001b[31m  const addrIdx = suggested.address >= 0 ? suggested.address : findCol(_rawHeaders, ['address','addr']);\u001b[39m\n\u001b[31m  const looksCombo = isCombinedColumn(addrIdx) || isCombinedColumn(suggested.name);\u001b[39m\n\u001b[31m  const comboCol   = looksCombo ? (addrIdx >= 0 ? addrIdx : suggested.name) : -1;\u001b[39m\n\u001b[31m  const comboPreview = comboCol >= 0 && _rawRows[0] ? (_rawRows[0][comboCol] || '').toString().slice(0, 120) : '';·\u001b[39m\n\u001b[31m  colInfoEl.innerHTML = `\u001b[39m\n\u001b[31m    <div style=\\\"background:var(--sur2);border:1px solid var(--bdr);border-radius:var(--r-sm);padding:12px;\\\">\u001b[39m\n\u001b[31m      <div style=\\\"display:flex;align-items:center;justify-content:space-between;margin-bottom:10px;\\\">\u001b[39m\n\u001b[31m        <div style=\\\"font-family:var(--fnt-mono);font-size:.68rem;color:var(--sky);\\\">\u001b[39m\n\u001b[31m          📋 ${filename} &nbsp;·&nbsp; ${_rawRows.length} rows &nbsp;·&nbsp; ${_rawHeaders.length} columns\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m        ${isSaved ? '<span class=\\\"map-saved-badge\\\">✓ mapping remembered</span>' : ''}\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      <div style=\\\"font-size:.68rem;color:var(--t3);font-family:var(--fnt-mono);margin-bottom:10px;word-break:break-all;\\\">\u001b[39m\n\u001b[31m        Columns: <span style=\\\"color:var(--t2);\\\">${_rawHeaders.map((h,i)=>`[${i+1}] ${h||'?'}`).join(' · ')}</span>\u001b[39m\n\u001b[31m      </div>·\u001b[39m\n\u001b[31m      ${looksCombo ? `\u001b[39m\n\u001b[31m      <!-- COMBINED COLUMN DETECTED -->\u001b[39m\n\u001b[31m      <div style=\\\"background:rgba(56,189,248,.08);border:1px solid rgba(56,189,248,.3);\u001b[39m\n\u001b[31m        border-radius:var(--r-sm);padding:10px 12px;margin-bottom:10px;\\\">\u001b[39m\n\u001b[31m        <div style=\\\"font-size:.72rem;color:var(--sky);font-weight:600;margin-bottom:6px;\\\">\u001b[39m\n\u001b[31m          🤖 Combined Address Column Detected\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m        <div style=\\\"font-size:.68rem;color:var(--t2);font-family:var(--fnt-mono);\u001b[39m\n\u001b[31m          white-space:pre-wrap;background:var(--bg);padding:6px 8px;border-radius:4px;\u001b[39m\n\u001b[31m          margin-bottom:8px;max-height:80px;overflow:hidden;\\\">${comboPreview}</div>\u001b[39m\n\u001b[31m        <div style=\\\"font-size:.7rem;color:var(--t3);margin-bottom:8px;\\\">\u001b[39m\n\u001b[31m          AI will extract: Name · Address · City · Pincode · Phone from each cell.\u001b[39m\n\u001b[31m          Runs in browser (Transformers.js) with Ollama fallback.\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m        <div style=\\\"display:flex;align-items:center;gap:8px;\\\">\u001b[39m\n\u001b[31m          <label style=\\\"display:flex;align-items:center;gap:6px;font-size:.72rem;color:var(--t1);cursor:pointer;\\\">\u001b[39m\n\u001b[31m            <input type=\\\"checkbox\\\" id=\\\"useAIParse\\\" checked style=\\\"cursor:pointer;\\\">\u001b[39m\n\u001b[31m            Use AI to parse combined column\u001b[39m\n\u001b[31m          </label>\u001b[39m\n\u001b[31m          <select id=\\\"comboColSel\\\" style=\\\"background:var(--sur2);border:1px solid var(--bdr);\u001b[39m\n\u001b[31m            color:var(--t1);font-family:var(--fnt-mono);font-size:.72rem;padding:4px 8px;\u001b[39m\n\u001b[31m            border-radius:var(--r-sm);outline:none;\\\">\u001b[39m\n\u001b[31m            ${_rawHeaders.map((h,i)=>`<option value=\\\"${i}\\\" ${i===comboCol?'selected':''}>${h||'(col '+(i+1)+')'}</option>`).join('')}\u001b[39m\n\u001b[31m          </select>\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      ` : ''}·\u001b[39m\n\u001b[31m      <div id=\\\"manualMapSection\\\" style=\\\"${looksCombo ? 'display:none;' : ''}\\\">\u001b[39m\n\u001b[31m        <div style=\\\"font-size:.68rem;color:var(--t3);margin-bottom:6px;\\\">\u001b[39m\n\u001b[31m          ${looksCombo ? 'Manual mapping (used when AI parse is off, or for other columns):' : 'Map each field to your column:'}\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m        <table class=\\\"map-table\\\">\u001b[39m\n\u001b[31m          <thead><tr><th>Field</th><th>Your column</th><th>Preview (row 1)</th></tr></thead>\u001b[39m\n\u001b[31m          <tbody>\u001b[39m\n\u001b[31m            ${MAPPING_FIELDS.map(f => {\u001b[39m\n\u001b[31m              const selIdx = suggested[f.key] ?? -1;\u001b[39m\n\u001b[31m              const preview = selIdx >= 0 && _rawRows[0] ? ((_rawRows[0][selIdx]||'').toString().slice(0,40)) : '—';\u001b[39m\n\u001b[31m              return `<tr>\u001b[39m\n\u001b[31m                <td style=\\\"white-space:nowrap;color:var(--t1);font-weight:500;font-size:.75rem;\\\">\u001b[39m\n\u001b[31m                  ${f.label}${f.required ? '<span class=\\\"map-req\\\">*</span>' : ''}\u001b[39m\n\u001b[31m                </td>\u001b[39m\n\u001b[31m                <td>\u001b[39m\n\u001b[31m                  <select id=\\\"mapsel_${f.key}\\\" onchange=\\\"updateMappingPreview('${f.key}')\\\">\u001b[39m\n\u001b[31m                    ${_rawHeaders.map((h,i)=>\u001b[39m\n\u001b[31m                      `<option value=\\\"${i}\\\" ${i===selIdx?'selected':''}>${h||'(col '+(i+1)+')'}</option>`\u001b[39m\n\u001b[31m                    ).join('')}\u001b[39m\n\u001b[31m                    <option value=\\\"-1\\\" ${selIdx===-1?'selected':''}>— not in file —</option>\u001b[39m\n\u001b[31m                  </select>\u001b[39m\n\u001b[31m                </td>\u001b[39m\n\u001b[31m                <td id=\\\"mapprev_${f.key}\\\" style=\\\"font-family:var(--fnt-mono);font-size:.68rem;\u001b[39m\n\u001b[31m                  color:var(--t2);max-width:90px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;\\\">\u001b[39m\n\u001b[31m                  ${preview}\u001b[39m\n\u001b[31m                </td>\u001b[39m\n\u001b[31m              </tr>`;\u001b[39m\n\u001b[31m            }).join('')}\u001b[39m\n\u001b[31m          </tbody>\u001b[39m\n\u001b[31m        </table>\u001b[39m\n\u001b[31m      </div>·\u001b[39m\n\u001b[31m      <!-- Also map docket/courier no column separately — always shown -->\u001b[39m\n\u001b[31m      <div style=\\\"margin-top:10px;padding-top:10px;border-top:1px solid var(--bdr);\\\">\u001b[39m\n\u001b[31m        <div style=\\\"font-size:.68rem;color:var(--t3);margin-bottom:6px;font-family:var(--fnt-mono);\\\">\u001b[39m\n\u001b[31m          DOCKET / COURIER NO. COLUMN (required for tracking):\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m        <div style=\\\"display:flex;gap:8px;align-items:center;\\\">\u001b[39m\n\u001b[31m          <select id=\\\"docketColSel\\\" style=\\\"background:var(--sur2);border:1px solid var(--bdr);\u001b[39m\n\u001b[31m            color:var(--acc);font-family:var(--fnt-mono);font-size:.78rem;padding:6px 10px;\u001b[39m\n\u001b[31m            border-radius:var(--r-sm);outline:none;flex:1;\\\">\u001b[39m\n\u001b[31m            <option value=\\\"-1\\\">— Docket not in file —</option>\u001b[39m\n\u001b[31m            ${_rawHeaders.map((h,i)=>`<option value=\\\"${i}\\\" ${i===(suggested.docket??-1)?'selected':''}>${h||'(col '+(i+1)+')'}</option>`).join('')}\u001b[39m\n\u001b[31m          </select>\u001b[39m\n\u001b[31m          <span id=\\\"docketPrev\\\" style=\\\"font-family:var(--fnt-mono);font-size:.72rem;color:var(--acc);\\\">\u001b[39m\n\u001b[31m            ${suggested.docket >= 0 && _rawRows[0] ? (_rawRows[0][suggested.docket]||'') : ''}\u001b[39m\n\u001b[31m          </span>\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m      </div>·\u001b[39m\n\u001b[31m      <div class=\\\"btn-row\\\" style=\\\"margin-top:12px;\\\">\u001b[39m\n\u001b[31m        <button class=\\\"btn btn-ghost\\\" style=\\\"font-size:.78rem;\\\" onclick=\\\"clearStores()\\\">✕ Cancel</button>\u001b[39m\n\u001b[31m        <button class=\\\"btn btn-orange\\\" style=\\\"font-size:.78rem;\\\" onclick=\\\"applyMapping()\\\">✓ Apply Mapping</button>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m  `;\u001b[39m\n\u001b[31m  document.getElementById('storeTable').innerHTML = '';\u001b[39m\n\u001b[31m  document.getElementById('storeAddPanel').style.display = 'none';·\u001b[39m\n\u001b[31m  // Update docket preview on change\u001b[39m\n\u001b[31m  const docketSel = document.getElementById('docketColSel');\u001b[39m\n\u001b[31m  if (docketSel) docketSel.addEventListener('change', () => {\u001b[39m\n\u001b[31m    const idx = parseInt(docketSel.value);\u001b[39m\n\u001b[31m    const prev = document.getElementById('docketPrev');\u001b[39m\n\u001b[31m    if (prev) prev.textContent = idx >= 0 && _rawRows[0] ? (_rawRows[0][idx]||'') : '';\u001b[39m\n\u001b[31m  });·\u001b[39m\n\u001b[31m  // Toggle manual map opacity when AI checkbox changes\u001b[39m\n\u001b[31m  const aiChk = document.getElementById('useAIParse');\u001b[39m\n\u001b[31m  const manSec = document.getElementById('manualMapSection');\u001b[39m\n\u001b[31m  if (aiChk && manSec) {\u001b[39m\n\u001b[31m    aiChk.addEventListener('change', () => {\u001b[39m\n\u001b[31m      manSec.style.display = aiChk.checked ? 'none' : 'block';\u001b[39m\n\u001b[31m    });\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction updateMappingPreview(key) {\u001b[39m\n\u001b[31m  const sel = document.getElementById('mapsel_' + key);\u001b[39m\n\u001b[31m  if (!sel) return;\u001b[39m\n\u001b[31m  const idx = parseInt(sel.value);\u001b[39m\n\u001b[31m  const prev = document.getElementById('mapprev_' + key);\u001b[39m\n\u001b[31m  if (prev) prev.textContent = (idx >= 0 && _rawRows[0]) ? ((_rawRows[0][idx]||'').toString().slice(0,40)) : '—';\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// ── REGEX-FIRST ADDRESS PARSER ───────────────────────────────────────────\u001b[39m\n\u001b[31m// Improved Indian address parser\u001b[39m\n\u001b[31m// Handles formats seen in real BTL dispatch files\u001b[39m\n\u001b[31mfunction parseAddressRegex(raw) {\u001b[39m\n\u001b[31m  const text = raw.toString().replace(/\\\\r/g, '').trim();\u001b[39m\n\u001b[31m  const lines = text.split('\\\\n').map(l => l.trim()).filter(Boolean);\u001b[39m\n\u001b[31m  const result = { name:'', address:'', city:'', pincode:'', phone:'', confidence:0 };·\u001b[39m\n\u001b[31m  // ── PHONE: Indian mobile 6-9 prefix, 10 digits ──────────────────────────\u001b[39m\n\u001b[31m  // Also handles \\\"Phone : 9876543210\\\", \\\"M.No.-9876543210\\\", \\\"Mobile - 9876543210\\\"\u001b[39m\n\u001b[31m  const phoneMatch = text.match(/(?:Phone|Mobile|Mob|M\\\\.No\\\\.?|Ph\\\\.?)?\\\\s*[:\\\\-]?\\\\s*\\\\b([6-9]\\\\d{9})\\\\b/i);\u001b[39m\n\u001b[31m  if (phoneMatch) result.phone = phoneMatch[1];·\u001b[39m\n\u001b[31m  // ── PINCODE: 6-digit standalone number ──────────────────────────────────\u001b[39m\n\u001b[31m  // Must not be part of a longer number (e.g. not an account number)\u001b[39m\n\u001b[31m  const pinMatch = text.match(/(?<![\\\\d])([1-9]\\\\d{5})(?![\\\\d])/);\u001b[39m\n\u001b[31m  if (pinMatch) result.pincode = pinMatch[1];·\u001b[39m\n\u001b[31m  // ── CITY: the token immediately before the pincode ───────────────────────\u001b[39m\n\u001b[31m  const indianStates = ['rajasthan','gujarat','maharashtra','punjab','haryana',\u001b[39m\n\u001b[31m    'uttar pradesh','madhya pradesh','karnataka','tamil nadu','andhra pradesh',\u001b[39m\n\u001b[31m    'telangana','kerala','west bengal','bihar','odisha','jharkhand','assam',\u001b[39m\n\u001b[31m    'himachal pradesh','uttarakhand','chhattisgarh','goa','delhi','chandigarh',\u001b[39m\n\u001b[31m    'raj.','raj'];\u001b[39m\n\u001b[31m  if (result.pincode) {\u001b[39m\n\u001b[31m    const pinPos = text.indexOf(result.pincode);\u001b[39m\n\u001b[31m    const beforePin = text.slice(0, pinPos);\u001b[39m\n\u001b[31m    // Strategy 1: split by comma/newline, walk backwards skipping state names\u001b[39m\n\u001b[31m    const chunks = beforePin.split(/[,\\\\n]/).map(c => c.trim()).filter(Boolean);\u001b[39m\n\u001b[31m    for (let i = chunks.length - 1; i >= 0; i--) {\u001b[39m\n\u001b[31m      const chunk = chunks[i].replace(/\\\\d+/g,'').replace(/[()]/g,'').replace(/[.-]/g,'').trim();\u001b[39m\n\u001b[31m      const lower = chunk.toLowerCase();\u001b[39m\n\u001b[31m      if (!chunk || chunk.length < 2) continue;\u001b[39m\n\u001b[31m      if (indianStates.includes(lower)) continue;\u001b[39m\n\u001b[31m      // Skip chunks that look like addresses (contain numbers or long strings)\u001b[39m\n\u001b[31m      if (/\\\\d/.test(chunk)) continue;\u001b[39m\n\u001b[31m      result.city = chunk;\u001b[39m\n\u001b[31m      break;\u001b[39m\n\u001b[31m    }\u001b[39m\n\u001b[31m    // Strategy 2: if still no city, look for last capitalised word before pincode\u001b[39m\n\u001b[31m    // handles \\\"compound paota Jodhpur 342006\\\" → Jodhpur\u001b[39m\n\u001b[31m    if (!result.city) {\u001b[39m\n\u001b[31m      const wordsBefore = beforePin.replace(/[,\\\\n]/g,' ').trim().split(/\\\\s+/);\u001b[39m\n\u001b[31m      for (let i = wordsBefore.length - 1; i >= 0; i--) {\u001b[39m\n\u001b[31m        const w = wordsBefore[i].replace(/[^a-zA-Z]/g,'');\u001b[39m\n\u001b[31m        if (w.length < 3) continue;\u001b[39m\n\u001b[31m        if (indianStates.includes(w.toLowerCase())) continue;\u001b[39m\n\u001b[31m        // Prefer words that start with capital letter (likely a proper noun/city)\u001b[39m\n\u001b[31m        if (/^[A-Z]/.test(w)) { result.city = w; break; }\u001b[39m\n\u001b[31m      }\u001b[39m\n\u001b[31m    }\u001b[39m\n\u001b[31m  }·\u001b[39m\n\u001b[31m  // ── NAME: first meaningful line, extract shop name ───────────────────────\u001b[39m\n\u001b[31m  // Patterns:\u001b[39m\n\u001b[31m  //   \\\"Sanwar Mal Saini, C/o R G COMPUTERS\\\" → name = \\\"R G COMPUTERS\\\"\u001b[39m\n\u001b[31m  //   \\\"SANWAR MAL SAINI, C/o ADVANCE TECHNOLOGIES\\\" → name = \\\"ADVANCE TECHNOLOGIES\\\"\u001b[39m\n\u001b[31m  //   \\\"Sunil Vaishnav\\\" → name = \\\"Sunil Vaishnav\\\" (no C/o)\u001b[39m\n\u001b[31m  //   \\\"Baltej Singh, Professional computer care\\\" → name = \\\"Professional computer care\\\"\u001b[39m\n\u001b[31m  if (lines.length > 0) {\u001b[39m\n\u001b[31m    const firstLine = lines[0];\u001b[39m\n\u001b[31m    // Check for C/o or C/O pattern — shop name comes after it\u001b[39m\n\u001b[31m    const coMatch = firstLine.match(/C\\\\/?[Oo]\\\\.?\\\\s+(.+)/);\u001b[39m\n\u001b[31m    if (coMatch) {\u001b[39m\n\u001b[31m      result.name = coMatch[1].trim();\u001b[39m\n\u001b[31m    } else {\u001b[39m\n\u001b[31m      // Check if there is a comma — second part might be the shop name\u001b[39m\n\u001b[31m      const parts = firstLine.split(',').map(p => p.trim());\u001b[39m\n\u001b[31m      if (parts.length >= 2 && parts[1].length > 3) {\u001b[39m\n\u001b[31m        // Heuristic: if second part looks like a business name (has uppercase or >5 chars)\u001b[39m\n\u001b[31m        const hasUpperOrLong = /[A-Z]{2,}/.test(parts[1]) || parts[1].length > 8;\u001b[39m\n\u001b[31m        result.name = hasUpperOrLong ? parts[1] : parts[0];\u001b[39m\n\u001b[31m      } else {\u001b[39m\n\u001b[31m        result.name = parts[0];\u001b[39m\n\u001b[31m      }\u001b[39m\n\u001b[31m    }\u001b[39m\n\u001b[31m  }·\u001b[39m\n\u001b[31m  // ── ADDRESS: lines between name and phone/pincode line ───────────────────\u001b[39m\n\u001b[31m  const phoneLineIdx = lines.findIndex(l => result.phone && l.includes(result.phone));\u001b[39m\n\u001b[31m  const addrLines = lines.filter((l, i) => {\u001b[39m\n\u001b[31m    if (i === 0) return false;\u001b[39m\n\u001b[31m    if (i === phoneLineIdx) return false;\u001b[39m\n\u001b[31m    // Exclude lines that are just the phone number\u001b[39m\n\u001b[31m    if (/^[6-9]\\\\d{9}$/.test(l.replace(/\\\\s/g,''))) return false;\u001b[39m\n\u001b[31m    return true;\u001b[39m\n\u001b[31m  });\u001b[39m\n\u001b[31m  result.address = addrLines.join(', ').replace(/,\\\\s*,/g, ',').trim();·\u001b[39m\n\u001b[31m  // ── CONFIDENCE: simple score based on what we got ────────────────────────\u001b[39m\n\u001b[31m  let conf = 0;\u001b[39m\n\u001b[31m  if (result.name)    conf += 30;\u001b[39m\n\u001b[31m  if (result.city)    conf += 25;\u001b[39m\n\u001b[31m  if (result.pincode) conf += 25;\u001b[39m\n\u001b[31m  if (result.phone)   conf += 20;\u001b[39m\n\u001b[31m  result.confidence = conf;·\u001b[39m\n\u001b[31m  return result;\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// ── AI ADDRESS PARSER (Ollama fallback) ──────────────────────────────────\u001b[39m\n\u001b[31m// Fetches past corrections from localStorage to improve prompt accuracy\u001b[39m\n\u001b[31mfunction getPastCorrections() {\u001b[39m\n\u001b[31m  try {\u001b[39m\n\u001b[31m    const log = JSON.parse(localStorage.getItem('360lm_parse_corrections') || '[]');\u001b[39m\n\u001b[31m    return log.slice(-10); // last 10 corrections as examples\u001b[39m\n\u001b[31m  } catch(e) { return []; }\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction saveCorrectionLocal(entry) {\u001b[39m\n\u001b[31m  try {\u001b[39m\n\u001b[31m    const log = JSON.parse(localStorage.getItem('360lm_parse_corrections') || '[]');\u001b[39m\n\u001b[31m    log.push(entry);\u001b[39m\n\u001b[31m    // Keep only last 50\u001b[39m\n\u001b[31m    localStorage.setItem('360lm_parse_corrections', JSON.stringify(log.slice(-50)));\u001b[39m\n\u001b[31m  } catch(e) {}\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31masync function parseAddressWithOllama(raw) {\u001b[39m\n\u001b[31m  // Calls the server-side /dispatch-ai/address proxy (avoids CORS + port 11434 issues)\u001b[39m\n\u001b[31m  try {\u001b[39m\n\u001b[31m    const resp = await fetch('/dispatch-ai/address', {\u001b[39m\n\u001b[31m      method: 'POST',\u001b[39m\n\u001b[31m      headers: { 'Content-Type': 'application/json' },\u001b[39m\n\u001b[31m      body: JSON.stringify({ text: raw }),\u001b[39m\n\u001b[31m      signal: AbortSignal.timeout(120000)\u001b[39m\n\u001b[31m    });\u001b[39m\n\u001b[31m    const data = await resp.json();\u001b[39m\n\u001b[31m    if (data.success) return data;\u001b[39m\n\u001b[31m  } catch(e) {}\u001b[39m\n\u001b[31m  return null;\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31masync function parseAddressCell(raw) {\u001b[39m\n\u001b[31m  // Step 1: regex — always runs first, no network call\u001b[39m\n\u001b[31m  const regex = parseAddressRegex(raw);\u001b[39m\n\u001b[31m  if (regex.name) return { ...regex, source: 'regex' };·\u001b[39m\n\u001b[31m  // Step 2: Ollama only if regex got nothing AND Ollama is configured\u001b[39m\n\u001b[31m  // Uses a 3-second timeout to prevent browser hang\u001b[39m\n\u001b[31m  if (window.appConfig.ocr_mode !== 'transformers') {\u001b[39m\n\u001b[31m    try {\u001b[39m\n\u001b[31m      const ai = await Promise.race([\u001b[39m\n\u001b[31m        parseAddressWithOllama(raw),\u001b[39m\n\u001b[31m        new Promise((_, reject) => setTimeout(() => reject(new Error('timeout')), 120000))\u001b[39m\n\u001b[31m      ]);\u001b[39m\n\u001b[31m      if (ai && ai.name) return { ...ai, source: 'ollama' };\u001b[39m\n\u001b[31m    } catch(e) { /* timeout or error — continue to fallback */ }\u001b[39m\n\u001b[31m  }·\u001b[39m\n\u001b[31m  // Step 3: fallback — first line as name\u001b[39m\n\u001b[31m  const lines = raw.toString().split('\\\\n').map(l=>l.trim()).filter(Boolean);\u001b[39m\n\u001b[31m  return { name: lines[0]||raw.slice(0,40), address: raw, city:'', pincode:'', phone:'', source:'fallback' };\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31masync function applyMapping() {\u001b[39m\n\u001b[31m  const useAI   = document.getElementById('useAIParse')?.checked ?? false;\u001b[39m\n\u001b[31m  const comboSel = document.getElementById('comboColSel');\u001b[39m\n\u001b[31m  const comboIdx = comboSel ? parseInt(comboSel.value) : -1;\u001b[39m\n\u001b[31m  const docketIdx = parseInt(document.getElementById('docketColSel')?.value ?? -1);·\u001b[39m\n\u001b[31m  // Build manual mapping as fallback\u001b[39m\n\u001b[31m  const mapping = {};\u001b[39m\n\u001b[31m  MAPPING_FIELDS.forEach(f => {\u001b[39m\n\u001b[31m    const sel = document.getElementById('mapsel_' + f.key);\u001b[39m\n\u001b[31m    mapping[f.key] = sel ? parseInt(sel.value) : -1;\u001b[39m\n\u001b[31m  });\u001b[39m\n\u001b[31m  // Override docket from dedicated selector\u001b[39m\n\u001b[31m  mapping.docket = docketIdx;·\u001b[39m\n\u001b[31m  // Determine if we have a valid base for stores\u001b[39m\n\u001b[31m  const nameIdx = useAI && comboIdx >= 0 ? comboIdx : mapping.name;\u001b[39m\n\u001b[31m  if (nameIdx === -1 && !useAI) { showToast('Store Name column is required', 'err'); return; }·\u001b[39m\n\u001b[31m  // Save mapping\u001b[39m\n\u001b[31m  try { localStorage.setItem(getMappingKey(), JSON.stringify({...mapping, _useAI: useAI, _comboIdx: comboIdx})); } catch(e) {}·\u001b[39m\n\u001b[31m  if (useAI && comboIdx >= 0) {\u001b[39m\n\u001b[31m    // AI path — parse each combined cell\u001b[39m\n\u001b[31m    showToast('AI parsing addresses...', 'info');\u001b[39m\n\u001b[31m    showOCRWorking(true, `Parsing 0/${_rawRows.length} addresses...`);\u001b[39m\n\u001b[31m    stores = [];\u001b[39m\n\u001b[31m    for (let i = 0; i < _rawRows.length; i++) {\u001b[39m\n\u001b[31m      const r = _rawRows[i];\u001b[39m\n\u001b[31m      const raw = (r[comboIdx] || '').toString().trim();\u001b[39m\n\u001b[31m      if (!raw) continue;\u001b[39m\n\u001b[31m      showOCRWorking(true, `Parsing ${i+1}/${_rawRows.length} — ${raw.slice(0,30)}...`);\u001b[39m\n\u001b[31m      // Yield to browser every row to prevent UI freeze\u001b[39m\n\u001b[31m      await new Promise(r => setTimeout(r, 0));\u001b[39m\n\u001b[31m      const parsed = await parseAddressCell(raw);\u001b[39m\n\u001b[31m      stores.push({\u001b[39m\n\u001b[31m        id: i,\u001b[39m\n\u001b[31m        name:    parsed.name    || raw.slice(0,40),\u001b[39m\n\u001b[31m        city:    parsed.city    || '',\u001b[39m\n\u001b[31m        phone:   parsed.phone   || '',\u001b[39m\n\u001b[31m        address: parsed.address || raw,\u001b[39m\n\u001b[31m        pincode: parsed.pincode || '',\u001b[39m\n\u001b[31m        docket:  docketIdx >= 0 ? (r[docketIdx]||'').toString().trim() : '',\u001b[39m\n\u001b[31m        _parseSource: parsed.source,\u001b[39m\n\u001b[31m        _confidence:  parsed.confidence || 0,\u001b[39m\n\u001b[31m        _rawCell:     raw,\u001b[39m\n\u001b[31m        _origName:    parsed.name || '',\u001b[39m\n\u001b[31m        _origCity:    parsed.city || '',\u001b[39m\n\u001b[31m        matched: false\u001b[39m\n\u001b[31m      });\u001b[39m\n\u001b[31m    }\u001b[39m\n\u001b[31m    showOCRWorking(false);\u001b[39m\n\u001b[31m  } else {\u001b[39m\n\u001b[31m    // Manual mapping path\u001b[39m\n\u001b[31m    stores = _rawRows\u001b[39m\n\u001b[31m      .filter(r => r[nameIdx] && (r[nameIdx]+'').trim())\u001b[39m\n\u001b[31m      .map((r, i) => ({\u001b[39m\n\u001b[31m        id: i,\u001b[39m\n\u001b[31m        name:    mapping.name    >= 0 ? (r[mapping.name]   ||'').toString().trim() : '',\u001b[39m\n\u001b[31m        city:    mapping.city    >= 0 ? (r[mapping.city]   ||'').toString().trim() : '',\u001b[39m\n\u001b[31m        phone:   mapping.phone   >= 0 ? (r[mapping.phone]  ||'').toString().trim() : '',\u001b[39m\n\u001b[31m        docket:  mapping.docket  >= 0 ? (r[mapping.docket] ||'').toString().trim() : '',\u001b[39m\n\u001b[31m        address: mapping.address >= 0 ? (r[mapping.address]||'').toString().trim() : '',\u001b[39m\n\u001b[31m        pincode: mapping.pincode >= 0 ? (r[mapping.pincode]||'').toString().trim() : '',\u001b[39m\n\u001b[31m        matched: false\u001b[39m\n\u001b[31m      }));\u001b[39m\n\u001b[31m  }·\u001b[39m\n\u001b[31m  document.getElementById('storeCount').textContent = `${stores.length} stores loaded`;·\u001b[39m\n\u001b[31m  if (useAI && comboIdx >= 0) {\u001b[39m\n\u001b[31m    // AI path — show row-by-row review panel before adding consignees\u001b[39m\n\u001b[31m    showAddressReviewPanel();\u001b[39m\n\u001b[31m  } else {\u001b[39m\n\u001b[31m    // Manual mapping path — go straight to store table + add panel\u001b[39m\n\u001b[31m    renderStoreTable(mapping.docket >= 0 || docketIdx >= 0);\u001b[39m\n\u001b[31m    const addPanel = document.getElementById('storeAddPanel');\u001b[39m\n\u001b[31m    const addBtn   = document.getElementById('storeAddBtn');\u001b[39m\n\u001b[31m    const addTitle = document.getElementById('storeAddTitle');\u001b[39m\n\u001b[31m    const hasDockets = stores.some(s => s.docket);\u001b[39m\n\u001b[31m    addTitle.textContent = hasDockets\u001b[39m\n\u001b[31m      ? `${stores.filter(s=>s.docket).length} stores with dockets — add as consignees?`\u001b[39m\n\u001b[31m      : `Add ${stores.length} stores as consignees?`;\u001b[39m\n\u001b[31m    addBtn.textContent = `✓ Add ${stores.length} as Consignees`;\u001b[39m\n\u001b[31m    addPanel.style.display = 'block';\u001b[39m\n\u001b[31m    showToast(`${stores.length} stores mapped ✓`, 'ok');\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31m// ADDRESS REVIEW PANEL\u001b[39m\n\u001b[31m// Row-by-row review of AI-parsed addresses before consignees created\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31mfunction showAddressReviewPanel() {\u001b[39m\n\u001b[31m  const pane = document.getElementById('addrReviewPane');\u001b[39m\n\u001b[31m  if (!pane) return;\u001b[39m\n\u001b[31m  renderAddressReviewRows();\u001b[39m\n\u001b[31m  pane.style.display = 'block';\u001b[39m\n\u001b[31m  pane.scrollTop = 0;\u001b[39m\n\u001b[31m  showToast(`Review ${stores.length} parsed addresses`, 'info');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction renderAddressReviewRows() {\u001b[39m\n\u001b[31m  const list = document.getElementById('addrReviewList');\u001b[39m\n\u001b[31m  const summary = document.getElementById('addrReviewSummary');\u001b[39m\n\u001b[31m  if (!list) return;·\u001b[39m\n\u001b[31m  const highConf  = stores.filter(s => (s._confidence||0) >= 80).length;\u001b[39m\n\u001b[31m  const medConf   = stores.filter(s => (s._confidence||0) >= 50 && (s._confidence||0) < 80).length;\u001b[39m\n\u001b[31m  const lowConf   = stores.filter(s => (s._confidence||0) < 50).length;·\u001b[39m\n\u001b[31m  summary.innerHTML = `\u001b[39m\n\u001b[31m    <div class=\\\"vs-item\\\"><div class=\\\"vs-val\\\" style=\\\"color:var(--t1);\\\">${stores.length}</div><div class=\\\"vs-lbl\\\">Total</div></div>\u001b[39m\n\u001b[31m    <div class=\\\"vs-item\\\"><div class=\\\"vs-val\\\" style=\\\"color:var(--grn);\\\">${highConf}</div><div class=\\\"vs-lbl\\\">High conf</div></div>\u001b[39m\n\u001b[31m    <div class=\\\"vs-item\\\"><div class=\\\"vs-val\\\" style=\\\"color:var(--ylw);\\\">${medConf}</div><div class=\\\"vs-lbl\\\">Review</div></div>\u001b[39m\n\u001b[31m    <div class=\\\"vs-item\\\"><div class=\\\"vs-val\\\" style=\\\"color:var(--red);\\\">${lowConf}</div><div class=\\\"vs-lbl\\\">Check</div></div>\u001b[39m\n\u001b[31m    <div class=\\\"vs-item\\\" style=\\\"margin-left:auto;\\\">\u001b[39m\n\u001b[31m      <button class=\\\"btn-sm primary\\\" onclick=\\\"approveAllAddresses()\\\" style=\\\"font-size:.7rem;\\\">✓ Approve All</button>\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m  `;·\u001b[39m\n\u001b[31m  list.innerHTML = stores.map((s, i) => {\u001b[39m\n\u001b[31m    const conf = s._confidence || 0;\u001b[39m\n\u001b[31m    const confClass = conf >= 80 ? 'vf-ok' : conf >= 50 ? 'vf-nodocket' : 'vf-dupe';\u001b[39m\n\u001b[31m    const confLabel = conf >= 80 ? `${conf}% ✓` : conf >= 50 ? `${conf}% review` : `${conf}% check`;\u001b[39m\n\u001b[31m    const rowBorder = conf >= 80 ? 'var(--grn)' : conf >= 50 ? 'var(--ylw)' : 'var(--red)';·\u001b[39m\n\u001b[31m    return `\u001b[39m\n\u001b[31m    <div class=\\\"verify-row\\\" id=\\\"arow_${i}\\\" style=\\\"border-left:3px solid ${rowBorder};\\\">\u001b[39m\n\u001b[31m      <div class=\\\"verify-row-hdr\\\">\u001b[39m\n\u001b[31m        <span class=\\\"verify-num\\\">#${i+1}</span>\u001b[39m\n\u001b[31m        <span class=\\\"verify-store\\\" style=\\\"font-size:.78rem;color:var(--t2);font-weight:400;\u001b[39m\n\u001b[31m          font-family:var(--fnt-mono);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;flex:1;\\\"\u001b[39m\n\u001b[31m          title=\\\"${(s._rawCell||'').replace(/\\\"/g,\\\"'\\\")}\\\">\u001b[39m\n\u001b[31m          ${(s._rawCell||'').slice(0,50).split('\\\\n').join(' ↵ ')}       <span class=\\\"verify-flag ${confClass}\\\" style=\\\"flex-shrink:0;margin-right:6px;\\\">${confLabel} · ${s._parseSource||'?'}</span>\u001b[39m\n\u001b[31m        <button onclick=\\\"deleteReviewRow(${i})\\\"\u001b[39m\n\u001b[31m          style=\\\"background:none;border:1px solid rgba(239,68,68,.4);color:var(--red);\u001b[39m\n\u001b[31m          font-size:.65rem;padding:2px 8px;border-radius:6px;cursor:pointer;\u001b[39m\n\u001b[31m          font-family:var(--fnt-mono);flex-shrink:0;transition:background .2s;\\\"\u001b[39m\n\u001b[31m          onmouseover=\\\"this.style.background='rgba(239,68,68,.15)'\\\"\u001b[39m\n\u001b[31m          onmouseout=\\\"this.style.background='none'\\\">✕ Remove</button>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      <div class=\\\"verify-fields\\\" style=\\\"grid-template-columns:1fr 1fr 1fr 1fr 1fr;\\\">\u001b[39m\n\u001b[31m        <div class=\\\"verify-field\\\">\u001b[39m\n\u001b[31m          <label>Shop / Name <span style=\\\"color:var(--acc);\\\">*</span></label>\u001b[39m\n\u001b[31m          <input type=\\\"text\\\" value=\\\"${s.name||''}\\\" id=\\\"ar_name_${i}\\\"\u001b[39m\n\u001b[31m            oninput=\\\"updateAddrField(${i},'name',this)\\\"\u001b[39m\n\u001b[31m            style=\\\"${!s.name ? 'border-color:var(--red);' : ''}\\\">\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m        <div class=\\\"verify-field\\\">\u001b[39m\n\u001b[31m          <label>City</label>\u001b[39m\n\u001b[31m          <input type=\\\"text\\\" value=\\\"${s.city||''}\\\" id=\\\"ar_city_${i}\\\"\u001b[39m\n\u001b[31m            oninput=\\\"updateAddrField(${i},'city',this)\\\"\u001b[39m\n\u001b[31m            style=\\\"${!s.city ? 'border-color:var(--ylw);' : ''}\\\">\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m        <div class=\\\"verify-field\\\">\u001b[39m\n\u001b[31m          <label>Docket / Courier No.</label>\u001b[39m\n\u001b[31m          <input type=\\\"text\\\" value=\\\"${s.docket||''}\\\" id=\\\"ar_docket_${i}\\\"\u001b[39m\n\u001b[31m            oninput=\\\"updateAddrField(${i},'docket',this)\\\"\u001b[39m\n\u001b[31m            style=\\\"font-family:var(--fnt-mono);${!s.docket?'border-color:var(--ylw);':'color:var(--acc);'}\\\">\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m        <div class=\\\"verify-field\\\">\u001b[39m\n\u001b[31m          <label>Phone</label>\u001b[39m\n\u001b[31m          <input type=\\\"tel\\\" value=\\\"${s.phone||''}\\\" id=\\\"ar_phone_${i}\\\"\u001b[39m\n\u001b[31m            oninput=\\\"updateAddrField(${i},'phone',this)\\\">\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m        <div class=\\\"verify-field\\\">\u001b[39m\n\u001b[31m          <label>Pincode</label>\u001b[39m\n\u001b[31m          <input type=\\\"text\\\" value=\\\"${s.pincode||''}\\\" id=\\\"ar_pin_${i}\\\"\u001b[39m\n\u001b[31m            oninput=\\\"updateAddrField(${i},'pincode',this)\\\">\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      <div style=\\\"font-size:.65rem;color:var(--t3);font-family:var(--fnt-mono);\u001b[39m\n\u001b[31m        margin-top:4px;padding:4px 6px;background:var(--bg);border-radius:4px;word-break:break-all;\\\">\u001b[39m\n\u001b[31m        📋 ${(s._rawCell||'').split('\\\\n').join(' | ')}\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m    </div>`;\u001b[39m\n\u001b[31m  }).join('');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction updateAddrField(i, field, input) {\u001b[39m\n\u001b[31m  if (!stores[i]) return;\u001b[39m\n\u001b[31m  stores[i][field] = input.value.trim();\u001b[39m\n\u001b[31m  // Recompute confidence\u001b[39m\n\u001b[31m  let conf = 0;\u001b[39m\n\u001b[31m  if (stores[i].name)    conf += 30;\u001b[39m\n\u001b[31m  if (stores[i].city)    conf += 25;\u001b[39m\n\u001b[31m  if (stores[i].pincode) conf += 25;\u001b[39m\n\u001b[31m  if (stores[i].phone)   conf += 20;\u001b[39m\n\u001b[31m  stores[i]._confidence = conf;\u001b[39m\n\u001b[31m  input.classList.toggle('changed', true);\u001b[39m\n\u001b[31m  // Update border color on row\u001b[39m\n\u001b[31m  const row = document.getElementById('arow_' + i);\u001b[39m\n\u001b[31m  if (row) row.style.borderLeftColor = conf >= 80 ? 'var(--grn)' : conf >= 50 ? 'var(--ylw)' : 'var(--red)';\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction deleteReviewRow(i) {\u001b[39m\n\u001b[31m  // Mark as deleted — splice from stores array and re-render\u001b[39m\n\u001b[31m  stores.splice(i, 1);\u001b[39m\n\u001b[31m  // Re-assign IDs\u001b[39m\n\u001b[31m  stores.forEach((s, idx) => { s.id = idx; });\u001b[39m\n\u001b[31m  renderAddressReviewRows();\u001b[39m\n\u001b[31m  document.getElementById('storeCount').textContent = `${stores.length} stores loaded`;\u001b[39m\n\u001b[31m  showToast(`Row removed — ${stores.length} remaining`, 'info');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction backToMapping() {\u001b[39m\n\u001b[31m  // Hide review pane — store edits are preserved in stores[] array\u001b[39m\n\u001b[31m  document.getElementById('addrReviewPane').style.display = 'none';\u001b[39m\n\u001b[31m  // Show mapping UI again with current file\u001b[39m\n\u001b[31m  document.getElementById('storeTableArea').style.display = 'block';\u001b[39m\n\u001b[31m  document.getElementById('storeColInfo').style.display = 'block';\u001b[39m\n\u001b[31m  showToast('Your edits are preserved — re-apply mapping to reload', 'info');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction approveAllAddresses() {\u001b[39m\n\u001b[31m  stores.forEach((s, i) => { s._reviewed = true; });\u001b[39m\n\u001b[31m  document.getElementById('addrReviewPane').style.display = 'none';\u001b[39m\n\u001b[31m  applyReviewedStores();\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction confirmAddressReview() {\u001b[39m\n\u001b[31m  // Collect any pending edits, log corrections, close panel\u001b[39m\n\u001b[31m  const corrections = [];\u001b[39m\n\u001b[31m  stores.forEach((s, i) => {\u001b[39m\n\u001b[31m    const origName = s._origName || '';\u001b[39m\n\u001b[31m    const origCity = s._origCity || '';\u001b[39m\n\u001b[31m    if ((origName && origName !== s.name) || (origCity && origCity !== s.city)) {\u001b[39m\n\u001b[31m      corrections.push({ raw: s._rawCell||'', name: s.name, city: s.city, pincode: s.pincode });\u001b[39m\n\u001b[31m    }\u001b[39m\n\u001b[31m  });\u001b[39m\n\u001b[31m  // Save corrections locally for future Ollama prompts\u001b[39m\n\u001b[31m  corrections.forEach(c => saveCorrectionLocal(c));\u001b[39m\n\u001b[31m  // Log to Sheets asynchronously (fire and forget)\u001b[39m\n\u001b[31m  if (corrections.length > 0 && window.appConfig.webhook_url) {\u001b[39m\n\u001b[31m    logParseCorrectionsToSheets(corrections);\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m  document.getElementById('addrReviewPane').style.display = 'none';\u001b[39m\n\u001b[31m  applyReviewedStores();\u001b[39m\n\u001b[31m  showToast(`${corrections.length > 0 ? corrections.length + ' corrections logged · ' : ''}${stores.length} stores ready ✓`, 'ok');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// Clean street address: remove city, pincode, phone if they appear in address field\u001b[39m\n\u001b[31mfunction cleanAddress(addr, city, pincode, phone) {\u001b[39m\n\u001b[31m  if (!addr) return addr;\u001b[39m\n\u001b[31m  let s = addr;\u001b[39m\n\u001b[31m  // Remove phone\u001b[39m\n\u001b[31m  if (phone) s = s.replace(new RegExp(phone.replace(/[.*+?^${}()|[\\\\]\\\\\\\\]/g,'\\\\\\\\$&'), 'g'), '').trim();\u001b[39m\n\u001b[31m  // Remove pincode\u001b[39m\n\u001b[31m  if (pincode) s = s.replace(new RegExp('\\\\\\\\b' + pincode + '\\\\\\\\b', 'g'), '').trim();\u001b[39m\n\u001b[31m  // Remove city (case-insensitive)\u001b[39m\n\u001b[31m  if (city) s = s.replace(new RegExp('\\\\\\\\b' + city.replace(/[.*+?^${}()|[\\\\]\\\\\\\\]/g,'\\\\\\\\$&') + '\\\\\\\\b', 'ig'), '').trim();\u001b[39m\n\u001b[31m  // Remove state names and trailing noise\u001b[39m\n\u001b[31m  const noise = /\\\\b(rajasthan|gujarat|maharashtra|punjab|haryana|uttar pradesh|delhi|raj\\\\.)\\\\b/ig;\u001b[39m\n\u001b[31m  s = s.replace(noise, '');\u001b[39m\n\u001b[31m  // Remove \\\"Mobile :\\\", \\\"Phone :\\\", \\\"M.No.\\\" prefixes and dangling numbers\u001b[39m\n\u001b[31m  s = s.replace(/(?:Mobile|Phone|Mob|M\\\\.No\\\\.?|Ph\\\\.?)\\\\s*[:\\\\-]?\\\\s*/ig, '');\u001b[39m\n\u001b[31m  // Clean up multiple commas/spaces\u001b[39m\n\u001b[31m  s = s.replace(/,\\\\s*,/g, ',').replace(/\\\\s{2,}/g, ' ').replace(/^[\\\\s,]+|[\\\\s,]+$/g, '');\u001b[39m\n\u001b[31m  return s;\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction applyReviewedStores() {\u001b[39m\n\u001b[31m  // Clean address fields before loading stores\u001b[39m\n\u001b[31m  stores.forEach(s => {\u001b[39m\n\u001b[31m    s.address = cleanAddress(s.address, s.city, s.pincode, s.phone);\u001b[39m\n\u001b[31m  });\u001b[39m\n\u001b[31m  renderStoreTable(stores.some(s => s.docket));\u001b[39m\n\u001b[31m  const addPanel = document.getElementById('storeAddPanel');\u001b[39m\n\u001b[31m  const addTitle = document.getElementById('storeAddTitle');\u001b[39m\n\u001b[31m  const addBtn   = document.getElementById('storeAddBtn');\u001b[39m\n\u001b[31m  const hasDockets = stores.some(s => s.docket);\u001b[39m\n\u001b[31m  addTitle.textContent = hasDockets\u001b[39m\n\u001b[31m    ? `${stores.filter(s=>s.docket).length} stores with dockets — add as consignees?`\u001b[39m\n\u001b[31m    : `Add ${stores.length} stores as consignees?`;\u001b[39m\n\u001b[31m  addBtn.textContent = `✓ Add ${stores.length} as Consignees`;\u001b[39m\n\u001b[31m  addPanel.style.display = 'block';\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31masync function logParseCorrectionsToSheets(corrections) {\u001b[39m\n\u001b[31m  const url = window.appConfig.webhook_url;\u001b[39m\n\u001b[31m  if (!url) return;\u001b[39m\n\u001b[31m  try {\u001b[39m\n\u001b[31m    await fetch(url, {\u001b[39m\n\u001b[31m      method: 'POST', mode: 'no-cors',\u001b[39m\n\u001b[31m      headers: { 'Content-Type': 'application/json' },\u001b[39m\n\u001b[31m      body: JSON.stringify({\u001b[39m\n\u001b[31m        type: 'parse_correction',\u001b[39m\n\u001b[31m        corrections,\u001b[39m\n\u001b[31m        colFingerprint: getMappingKey(),\u001b[39m\n\u001b[31m        timestamp: new Date().toISOString()\u001b[39m\n\u001b[31m      })\u001b[39m\n\u001b[31m    });\u001b[39m\n\u001b[31m  } catch(e) {}\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction addStoresToConsignees() {\u001b[39m\n\u001b[31m  const consList = document.getElementById('consList');\u001b[39m\n\u001b[31m  consList.innerHTML = '';\u001b[39m\n\u001b[31m  stores.slice(0, 50).forEach(s => addConsignee(s));\u001b[39m\n\u001b[31m  document.getElementById('storeAddPanel').style.display = 'none';\u001b[39m\n\u001b[31m  showToast(`${Math.min(stores.length, 50)} consignees added ✓`, 'ok');\u001b[39m\n\u001b[31m  if (stores.some(s => s.docket)) setTimeout(() => autoCreateDispatchFromStoreList(), 600);\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction promptAddFromStores() { /* replaced by inline panel */ }··\u001b[39m\n\u001b[31mfunction findCol(headers, keywords) {\u001b[39m\n\u001b[31m  for (let i=0;i<headers.length;i++) {\u001b[39m\n\u001b[31m    const h = (headers[i]||'').toString().toLowerCase();\u001b[39m\n\u001b[31m    if (keywords.some(k => h.includes(k))) return i;\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m  return -1;\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction renderStoreTable(hasDocket) {\u001b[39m\n\u001b[31m  const t = document.getElementById('storeTable');\u001b[39m\n\u001b[31m  const cols = ['#','Store Name','City','Phone'];\u001b[39m\n\u001b[31m  if (hasDocket) cols.push('Docket','Status');\u001b[39m\n\u001b[31m  t.innerHTML = `<thead><tr>${cols.map(c=>`<th>${c}</th>`).join('')}</tr></thead>\u001b[39m\n\u001b[31m  <tbody>${stores.map((s,i) => {\u001b[39m\n\u001b[31m    const statusClass = s.docket ? (s.status === 'delivered' ? 'matched' : 'unmatched') : '';\u001b[39m\n\u001b[31m    const docketCell = hasDocket ? `<td class=\\\"${statusClass}\\\" style=\\\"font-family:var(--fnt-mono);\\\">${s.docket||'—'}</td>` : '';\u001b[39m\n\u001b[31m    const statusCell = hasDocket ? `<td>${s.status ? `<span class=\\\"status-pill sp-${s.status}\\\">${s.status}</span>` : '—'}</td>` : '';\u001b[39m\n\u001b[31m    return `<tr>\u001b[39m\n\u001b[31m      <td>${i+1}</td>\u001b[39m\n\u001b[31m      <td>${s.name}</td>\u001b[39m\n\u001b[31m      <td>${s.city}</td>\u001b[39m\n\u001b[31m      <td style=\\\"font-family:var(--fnt-mono);\\\">${s.phone||'—'}</td>\u001b[39m\n\u001b[31m      ${docketCell}${statusCell}\u001b[39m\n\u001b[31m    </tr>`;\u001b[39m\n\u001b[31m  }).join('')}</tbody>`;\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction clearStores() {\u001b[39m\n\u001b[31m  stores = [];\u001b[39m\n\u001b[31m  document.getElementById('storeTableArea').style.display = 'none';\u001b[39m\n\u001b[31m  document.getElementById('storeFile').value = '';\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction autoCreateDispatchFromStoreList() {\u001b[39m\n\u001b[31m  // Create/update dispatch with all stores that have dockets\u001b[39m\n\u001b[31m  const courier = document.getElementById('n_courier').value || 'SkyKing';\u001b[39m\n\u001b[31m  const storesWithDockets = stores.filter(s => s.docket);\u001b[39m\n\u001b[31m  if (!storesWithDockets.length) return;·\u001b[39m\n\u001b[31m  // Clear and rebuild consignee list from store data\u001b[39m\n\u001b[31m  const consList = document.getElementById('consList');\u001b[39m\n\u001b[31m  consList.innerHTML = '';\u001b[39m\n\u001b[31m  storesWithDockets.forEach(s => {\u001b[39m\n\u001b[31m    addConsignee(s);\u001b[39m\n\u001b[31m  });\u001b[39m\n\u001b[31m  showPane('new');\u001b[39m\n\u001b[31m  showToast(`${storesWithDockets.length} consignees added from store list`, 'ok');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction promptAddFromStores() {\u001b[39m\n\u001b[31m  // Replaced by inline storeAddPanel UI — no-op kept for safety\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction autoMatchDockets() {\u001b[39m\n\u001b[31m  // Match OCR dockets to stores by order\u001b[39m\n\u001b[31m  if (!slipOCRData.length) { showToast('Upload docket slip images first', 'err'); return; }\u001b[39m\n\u001b[31m  let matched = 0;\u001b[39m\n\u001b[31m  slipOCRData.forEach((slip, i) => {\u001b[39m\n\u001b[31m    if (i < stores.length && slip.docket) {\u001b[39m\n\u001b[31m      stores[i].docket = slip.docket;\u001b[39m\n\u001b[31m      matched++;\u001b[39m\n\u001b[31m    }\u001b[39m\n\u001b[31m  });\u001b[39m\n\u001b[31m  renderStoreTable(true);\u001b[39m\n\u001b[31m  showToast(`${matched} dockets matched to stores ✓`, 'ok');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31m// CONSIGNEES\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31mfunction addConsignee(storeData = null) {\u001b[39m\n\u001b[31m  const list = document.getElementById('consList');\u001b[39m\n\u001b[31m  const idx = list.children.length;\u001b[39m\n\u001b[31m  const card = document.createElement('div');\u001b[39m\n\u001b[31m  card.className = 'cons-card';\u001b[39m\n\u001b[31m  card.dataset.idx = idx;\u001b[39m\n\u001b[31m  const s = storeData || {};\u001b[39m\n\u001b[31m  card.innerHTML = `\u001b[39m\n\u001b[31m    <div class=\\\"cons-hdr\\\">\u001b[39m\n\u001b[31m      <span class=\\\"cons-num\\\">CONSIGNEE #${idx+1}</span>\u001b[39m\n\u001b[31m      <button class=\\\"cons-del\\\" onclick=\\\"this.closest('.cons-card').remove();renumberCons()\\\">✕</button>\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m    <div class=\\\"row2\\\">\u001b[39m\n\u001b[31m      <div class=\\\"f\\\">\u001b[39m\n\u001b[31m        <label>Store / Name <span class=\\\"req\\\">*</span></label>\u001b[39m\n\u001b[31m        <input type=\\\"text\\\" class=\\\"cons-name\\\" placeholder=\\\"Store name\\\" value=\\\"${s.name||''}\\\">\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      <div class=\\\"f\\\">\u001b[39m\n\u001b[31m        <label>City <span class=\\\"req\\\">*</span></label>\u001b[39m\n\u001b[31m        <input type=\\\"text\\\" class=\\\"cons-city\\\" placeholder=\\\"City\\\" value=\\\"${s.city||''}\\\">\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m    <div class=\\\"f\\\">\u001b[39m\n\u001b[31m      <label>Address</label>\u001b[39m\n\u001b[31m      <input type=\\\"text\\\" class=\\\"cons-addr\\\" placeholder=\\\"Full address\\\" value=\\\"${s.address||''}\\\">\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m    <div class=\\\"row2\\\">\u001b[39m\n\u001b[31m      <div class=\\\"f\\\">\u001b[39m\n\u001b[31m        <label>Phone</label>\u001b[39m\n\u001b[31m        <input type=\\\"tel\\\" class=\\\"cons-phone\\\" placeholder=\\\"Mobile\\\" value=\\\"${s.phone||''}\\\">\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      <div class=\\\"f\\\">\u001b[39m\n\u001b[31m        <label>Pincode</label>\u001b[39m\n\u001b[31m        <input type=\\\"text\\\" class=\\\"cons-pin\\\" placeholder=\\\"6-digit\\\" maxlength=\\\"6\\\">\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m    <div class=\\\"f\\\">\u001b[39m\n\u001b[31m      <label>Material / Contents <span class=\\\"req\\\">*</span></label>\u001b[39m\n\u001b[31m      <input type=\\\"text\\\" class=\\\"cons-material\\\" placeholder=\\\"e.g. Standees, Posters\\\">\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m    <div class=\\\"row2\\\">\u001b[39m\n\u001b[31m      <div class=\\\"f\\\">\u001b[39m\n\u001b[31m        <label>Qty</label>\u001b[39m\n\u001b[31m        <input type=\\\"number\\\" class=\\\"cons-qty\\\" placeholder=\\\"0\\\" min=\\\"0\\\">\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      <div class=\\\"f\\\">\u001b[39m\n\u001b[31m        <label>Docket No.</label>\u001b[39m\n\u001b[31m        <input type=\\\"text\\\" class=\\\"cons-docket-input\\\" placeholder=\\\"Docket #\\\"\u001b[39m\n\u001b[31m          style=\\\"font-family:var(--fnt-mono);\\\" value=\\\"${s.docket||''}\\\"\u001b[39m\n\u001b[31m          oninput=\\\"onDocketInput(this)\\\">\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m    ${s.docket ? `<div class=\\\"cons-docket-linked\\\"><span>🔗</span><span class=\\\"dkt\\\">${s.docket}</span></div>` : ''}\u001b[39m\n\u001b[31m  `;\u001b[39m\n\u001b[31m  list.appendChild(card);\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction onDocketInput(input) {\u001b[39m\n\u001b[31m  const card = input.closest('.cons-card');\u001b[39m\n\u001b[31m  let linked = card.querySelector('.cons-docket-linked');\u001b[39m\n\u001b[31m  if (input.value.trim()) {\u001b[39m\n\u001b[31m    if (!linked) {\u001b[39m\n\u001b[31m      linked = document.createElement('div');\u001b[39m\n\u001b[31m      linked.className = 'cons-docket-linked';\u001b[39m\n\u001b[31m      card.appendChild(linked);\u001b[39m\n\u001b[31m    }\u001b[39m\n\u001b[31m    linked.innerHTML = `<span>🔗</span><span class=\\\"dkt\\\">${input.value}</span>`;\u001b[39m\n\u001b[31m  } else if (linked) {\u001b[39m\n\u001b[31m    linked.remove();\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction showLinkedDocket(card, docket) {\u001b[39m\n\u001b[31m  const input = card.querySelector('.cons-docket-input');\u001b[39m\n\u001b[31m  if (input) { input.value = docket; onDocketInput(input); }\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction renumberCons() {\u001b[39m\n\u001b[31m  document.querySelectorAll('#consList .cons-card').forEach((c,i) => {\u001b[39m\n\u001b[31m    c.querySelector('.cons-num').textContent = `CONSIGNEE #${i+1}`;\u001b[39m\n\u001b[31m    c.dataset.idx = i;\u001b[39m\n\u001b[31m  });\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31m// JOB REFERENCE\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31mfunction onClientChange() {\u001b[39m\n\u001b[31m  const val = document.getElementById('n_client').value;\u001b[39m\n\u001b[31m  document.getElementById('customClientField').style.display = val === '__c' ? 'block' : 'none';\u001b[39m\n\u001b[31m  updatePill();\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31mfunction updatePill() {\u001b[39m\n\u001b[31m  const client = document.getElementById('n_client').value;\u001b[39m\n\u001b[31m  const job = document.getElementById('n_jobref').value.trim();\u001b[39m\n\u001b[31m  const cName = client === '__c' ? document.getElementById('n_customClient').value : client;\u001b[39m\n\u001b[31m  const pill = document.getElementById('jobPill');\u001b[39m\n\u001b[31m  if (cName && job) {\u001b[39m\n\u001b[31m    document.getElementById('jobPillTxt').textContent = `${cName} › ${job}`;\u001b[39m\n\u001b[31m    pill.style.display = 'inline-flex';\u001b[39m\n\u001b[31m  } else {\u001b[39m\n\u001b[31m    pill.style.display = 'none';\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31mfunction clearJob() {\u001b[39m\n\u001b[31m  document.getElementById('n_client').value = '';\u001b[39m\n\u001b[31m  document.getElementById('n_jobref').value = '';\u001b[39m\n\u001b[31m  document.getElementById('jobPill').style.display = 'none';\u001b[39m\n\u001b[31m  document.getElementById('customClientField').style.display = 'none';\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31m// GATHER / SAVE\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31mfunction gatherDispatch() {\u001b[39m\n\u001b[31m  const client = document.getElementById('n_client').value;\u001b[39m\n\u001b[31m  const clientName = client === '__c' ? document.getElementById('n_customClient').value : client;\u001b[39m\n\u001b[31m  const consignees = [];\u001b[39m\n\u001b[31m  document.querySelectorAll('#consList .cons-card').forEach(card => {\u001b[39m\n\u001b[31m    const docket = card.querySelector('.cons-docket-input')?.value.trim() || '';\u001b[39m\n\u001b[31m    consignees.push({\u001b[39m\n\u001b[31m      name: card.querySelector('.cons-name')?.value || '',\u001b[39m\n\u001b[31m      city: card.querySelector('.cons-city')?.value || '',\u001b[39m\n\u001b[31m      address: card.querySelector('.cons-addr')?.value || '',\u001b[39m\n\u001b[31m      phone: card.querySelector('.cons-phone')?.value || '',\u001b[39m\n\u001b[31m      pincode: card.querySelector('.cons-pin')?.value || '',\u001b[39m\n\u001b[31m      material: card.querySelector('.cons-material')?.value || '',\u001b[39m\n\u001b[31m      qty: card.querySelector('.cons-qty')?.value || '',\u001b[39m\n\u001b[31m      docket,\u001b[39m\n\u001b[31m      status: docket ? 'booked' : 'pending',\u001b[39m\n\u001b[31m      lastEvent: '',\u001b[39m\n\u001b[31m      lastUpdated: ''\u001b[39m\n\u001b[31m    });\u001b[39m\n\u001b[31m  });\u001b[39m\n\u001b[31m  return {\u001b[39m\n\u001b[31m    id: 'DSP-' + Date.now(),\u001b[39m\n\u001b[31m    mode: 'manual',\u001b[39m\n\u001b[31m    client: clientName,\u001b[39m\n\u001b[31m    jobref: document.getElementById('n_jobref').value,\u001b[39m\n\u001b[31m    courier: document.getElementById('n_courier').value,\u001b[39m\n\u001b[31m    dispatchDate: document.getElementById('n_date').value,\u001b[39m\n\u001b[31m    remarks: document.getElementById('n_remarks').value,\u001b[39m\n\u001b[31m    consignees,\u001b[39m\n\u001b[31m    slipCount: slipOCRData.length,\u001b[39m\n\u001b[31m    status: 'active',\u001b[39m\n\u001b[31m    createdAt: new Date().toISOString()\u001b[39m\n\u001b[31m  };\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction validate(d) {\u001b[39m\n\u001b[31m  if (!d.client) { showToast('Select a client', 'err'); return false; }\u001b[39m\n\u001b[31m  if (!d.courier) { showToast('Select courier', 'err'); return false; }\u001b[39m\n\u001b[31m  if (!d.consignees.length) { showToast('Add at least one consignee', 'err'); return false; }\u001b[39m\n\u001b[31m  for (let i = 0; i < d.consignees.length; i++) {\u001b[39m\n\u001b[31m    const c = d.consignees[i];\u001b[39m\n\u001b[31m    if (!c.name || !c.city) {\u001b[39m\n\u001b[31m      showToast(`Consignee #${i+1} (${c.name||'unnamed'}) — name & city required`, 'err');\u001b[39m\n\u001b[31m      // Scroll to and highlight the problem consignee card\u001b[39m\n\u001b[31m      const cards = document.querySelectorAll('#consList .cons-card');\u001b[39m\n\u001b[31m      if (cards[i]) {\u001b[39m\n\u001b[31m        cards[i].scrollIntoView({ behavior: 'smooth', block: 'center' });\u001b[39m\n\u001b[31m        cards[i].style.borderColor = 'var(--red)';\u001b[39m\n\u001b[31m        cards[i].style.boxShadow = '0 0 0 2px rgba(239,68,68,.3)';\u001b[39m\n\u001b[31m        setTimeout(() => {\u001b[39m\n\u001b[31m          if (cards[i]) { cards[i].style.borderColor = ''; cards[i].style.boxShadow = ''; }\u001b[39m\n\u001b[31m        }, 3000);\u001b[39m\n\u001b[31m      }\u001b[39m\n\u001b[31m      return false;\u001b[39m\n\u001b[31m    }\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m  return true;\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction saveDraft() {\u001b[39m\n\u001b[31m  const d = gatherDispatch();\u001b[39m\n\u001b[31m  d.status = 'draft';\u001b[39m\n\u001b[31m  dispatches.unshift(d);\u001b[39m\n\u001b[31m  saveData();\u001b[39m\n\u001b[31m  updateHistCount();\u001b[39m\n\u001b[31m  showToast('Draft saved ✓', 'ok');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// ── VERIFICATION SCREEN ─────────────────────────────────\u001b[39m\n\u001b[31m// Known docket lengths per courier for validation\u001b[39m\n\u001b[31mconst DOCKET_LENGTHS = {\u001b[39m\n\u001b[31m  'SkyKing': { min: 9, max: 12 },\u001b[39m\n\u001b[31m  'DTDC':    { min: 8, max: 15 },\u001b[39m\n\u001b[31m  'Delhivery': { min: 10, max: 18 },\u001b[39m\n\u001b[31m  'Blue Dart': { min: 10, max: 13 },\u001b[39m\n\u001b[31m  'FedEx':   { min: 10, max: 20 },\u001b[39m\n\u001b[31m  'Ecom Express': { min: 10, max: 14 },\u001b[39m\n\u001b[31m  'India Post': { min: 13, max: 22 },\u001b[39m\n\u001b[31m};·\u001b[39m\n\u001b[31mfunction finaliseDispatch() {\u001b[39m\n\u001b[31m  const d = gatherDispatch();\u001b[39m\n\u001b[31m  if (!validate(d)) return;\u001b[39m\n\u001b[31m  showVerifyScreen(d);\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction showVerifyScreen(d) {\u001b[39m\n\u001b[31m  const pane = document.getElementById('verifyPane');\u001b[39m\n\u001b[31m  const list = document.getElementById('verifyList');\u001b[39m\n\u001b[31m  const summary = document.getElementById('verifySummary');·\u001b[39m\n\u001b[31m  // Build flags\u001b[39m\n\u001b[31m  const docketsSeen = {};\u001b[39m\n\u001b[31m  const courier = d.courier;\u001b[39m\n\u001b[31m  const lenRule = DOCKET_LENGTHS[courier];·\u001b[39m\n\u001b[31m  d.consignees.forEach((c, i) => {\u001b[39m\n\u001b[31m    if (c.docket) docketsSeen[c.docket] = (docketsSeen[c.docket] || 0) + 1;\u001b[39m\n\u001b[31m  });·\u001b[39m\n\u001b[31m  const total     = d.consignees.length;\u001b[39m\n\u001b[31m  const withDock  = d.consignees.filter(c => c.docket).length;\u001b[39m\n\u001b[31m  const dupes     = Object.values(docketsSeen).filter(v => v > 1).length;\u001b[39m\n\u001b[31m  const noDock    = total - withDock;\u001b[39m\n\u001b[31m  const badLen    = lenRule ? d.consignees.filter(c => c.docket &&\u001b[39m\n\u001b[31m    (c.docket.length < lenRule.min || c.docket.length > lenRule.max)).length : 0;·\u001b[39m\n\u001b[31m  // Summary bar\u001b[39m\n\u001b[31m  summary.innerHTML = `\u001b[39m\n\u001b[31m    <div class=\\\"vs-item\\\"><div class=\\\"vs-val\\\" style=\\\"color:var(--t1);\\\">${total}</div><div class=\\\"vs-lbl\\\">Total</div></div>\u001b[39m\n\u001b[31m    <div class=\\\"vs-item\\\"><div class=\\\"vs-val\\\" style=\\\"color:var(--grn);\\\">${withDock}</div><div class=\\\"vs-lbl\\\">Docketed</div></div>\u001b[39m\n\u001b[31m    <div class=\\\"vs-item\\\"><div class=\\\"vs-val\\\" style=\\\"color:${noDock > 0 ? 'var(--ylw)' : 'var(--t3)'};\\\">${noDock}</div><div class=\\\"vs-lbl\\\">No Docket</div></div>\u001b[39m\n\u001b[31m    <div class=\\\"vs-item\\\"><div class=\\\"vs-val\\\" style=\\\"color:${dupes > 0 ? 'var(--red)' : 'var(--t3)'};\\\">${dupes}</div><div class=\\\"vs-lbl\\\">Duplicates</div></div>\u001b[39m\n\u001b[31m    ${badLen > 0 ? `<div class=\\\"vs-item\\\"><div class=\\\"vs-val\\\" style=\\\"color:var(--sky);\\\">${badLen}</div><div class=\\\"vs-lbl\\\">Short/Long</div></div>` : ''}\u001b[39m\n\u001b[31m    <div class=\\\"vs-item\\\" style=\\\"margin-left:auto;text-align:right;\\\">\u001b[39m\n\u001b[31m      <div class=\\\"vs-val\\\" style=\\\"font-size:.8rem;color:var(--t2);\\\">${d.courier||'—'}</div>\u001b[39m\n\u001b[31m      <div class=\\\"vs-lbl\\\">${d.client||'—'} ${d.jobref ? '› '+d.jobref : ''}</div>\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m  `;·\u001b[39m\n\u001b[31m  // Build consignee rows\u001b[39m\n\u001b[31m  list.innerHTML = d.consignees.map((c, i) => {\u001b[39m\n\u001b[31m    const isDupe   = c.docket && docketsSeen[c.docket] > 1;\u001b[39m\n\u001b[31m    const isNoDock = !c.docket;\u001b[39m\n\u001b[31m    const isShort  = lenRule && c.docket &&\u001b[39m\n\u001b[31m      (c.docket.length < lenRule.min || c.docket.length > lenRule.max);·\u001b[39m\n\u001b[31m    const rowClass = isDupe ? 'has-dupe' : (isNoDock ? 'no-docket' : 'ok');·\u001b[39m\n\u001b[31m    // Find matching OCR slip for thumbnail\u001b[39m\n\u001b[31m    const ocrMatch = slipOCRData[i];\u001b[39m\n\u001b[31m    const thumbHtml = ocrMatch?.imageData\u001b[39m\n\u001b[31m      ? `<img class=\\\"verify-thumb\\\" src=\\\"${ocrMatch.imageData}\\\" onclick=\\\"showSlipFull(${i})\\\" title=\\\"View slip\\\">`\u001b[39m\n\u001b[31m      : '';·\u001b[39m\n\u001b[31m    // Flags\u001b[39m\n\u001b[31m    let flags = '';\u001b[39m\n\u001b[31m    if (isDupe)   flags += `<span class=\\\"verify-flag vf-dupe\\\">⚠ Duplicate</span> `;\u001b[39m\n\u001b[31m    if (isNoDock) flags += `<span class=\\\"verify-flag vf-nodocket\\\">No Docket</span> `;\u001b[39m\n\u001b[31m    if (isShort)  flags += `<span class=\\\"verify-flag vf-short\\\">${c.docket.length} digits</span> `;\u001b[39m\n\u001b[31m    if (!isDupe && !isNoDock && !isShort) flags = `<span class=\\\"verify-flag vf-ok\\\">✓ OK</span>`;·\u001b[39m\n\u001b[31m    // OCR confidence note\u001b[39m\n\u001b[31m    const confNote = ocrMatch && ocrMatch.confidence\u001b[39m\n\u001b[31m      ? `<div class=\\\"ocr-correction-note\\\">OCR confidence: ${Math.round(ocrMatch.confidence*100)}% via ${ocrMatch.source||'—'}</div>` : '';·\u001b[39m\n\u001b[31m    // WhatsApp URL\u001b[39m\n\u001b[31m    const waMsg = encodeURIComponent(\u001b[39m\n\u001b[31m      `Dear ${c.name}, your material has been dispatched via ${d.courier}. Docket: ${c.docket||'TBD'}. Track: ${(COURIER_MASTER[d.courier]?.trackUrl||'').replace('{docket}', c.docket||'')}`\u001b[39m\n\u001b[31m    );\u001b[39m\n\u001b[31m    const waUrl = c.phone ? `https://wa.me/91${c.phone.replace(/\\\\D/g,'')}?text=${waMsg}` : '';·\u001b[39m\n\u001b[31m    return `\u001b[39m\n\u001b[31m    <div class=\\\"verify-row ${rowClass}\\\" id=\\\"vrow_${i}\\\">\u001b[39m\n\u001b[31m      <div class=\\\"verify-row-hdr\\\">\u001b[39m\n\u001b[31m        <span class=\\\"verify-num\\\">#${i+1}</span>\u001b[39m\n\u001b[31m        <span class=\\\"verify-store\\\">${c.name||'—'}</span>\u001b[39m\n\u001b[31m        ${flags}\u001b[39m\n\u001b[31m        ${thumbHtml}\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      <div class=\\\"verify-fields\\\" style=\\\"grid-template-columns:1fr 1fr 1fr;\\\">\u001b[39m\n\u001b[31m        <div class=\\\"verify-field\\\">\u001b[39m\n\u001b[31m          <label>Docket No. <span style=\\\"color:var(--acc);\\\">*</span></label>\u001b[39m\n\u001b[31m          <input type=\\\"text\\\" value=\\\"${c.docket||''}\\\" oninput=\\\"updateVerifyField(${i},'docket',this)\\\"\u001b[39m\n\u001b[31m            style=\\\"font-family:var(--fnt-mono);${isDupe||isShort?'border-color:var(--red);':!c.docket?'border-color:var(--ylw);':'color:var(--acc);'}\\\">\u001b[39m\n\u001b[31m          ${confNote}\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m        <div class=\\\"verify-field\\\">\u001b[39m\n\u001b[31m          <label>City</label>\u001b[39m\n\u001b[31m          <input type=\\\"text\\\" value=\\\"${c.city||''}\\\" oninput=\\\"updateVerifyField(${i},'city',this)\\\"\u001b[39m\n\u001b[31m            style=\\\"${!c.city ? 'border-color:var(--ylw);' : ''}\\\">\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m        <div class=\\\"verify-field\\\">\u001b[39m\n\u001b[31m          <label>Pincode</label>\u001b[39m\n\u001b[31m          <input type=\\\"text\\\" value=\\\"${c.pincode||''}\\\" oninput=\\\"updateVerifyField(${i},'pincode',this)\\\"\u001b[39m\n\u001b[31m            style=\\\"font-family:var(--fnt-mono);\\\">\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m        <div class=\\\"verify-field\\\">\u001b[39m\n\u001b[31m          <label>Phone</label>\u001b[39m\n\u001b[31m          <input type=\\\"tel\\\" value=\\\"${c.phone||''}\\\" oninput=\\\"updateVerifyField(${i},'phone',this)\\\">\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m        <div class=\\\"verify-field\\\">\u001b[39m\n\u001b[31m          <label>Material / Contents</label>\u001b[39m\n\u001b[31m          <input type=\\\"text\\\" value=\\\"${c.material||''}\\\" oninput=\\\"updateVerifyField(${i},'material',this)\\\">\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m        <div class=\\\"verify-field\\\">\u001b[39m\n\u001b[31m          <label>Street Address</label>\u001b[39m\n\u001b[31m          <input type=\\\"text\\\" value=\\\"${c.address||''}\\\" oninput=\\\"updateVerifyField(${i},'address',this)\\\"\u001b[39m\n\u001b[31m            placeholder=\\\"Street / locality only\\\">\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      <div class=\\\"verify-actions\\\">\u001b[39m\n\u001b[31m        ${c.docket && c.phone ? `<button class=\\\"btn-wa\\\" onclick=\\\"window.open('${waUrl}','_blank')\\\">📱 WhatsApp</button>` : ''}\u001b[39m\n\u001b[31m        <button class=\\\"btn-sm\\\" onclick=\\\"removeVerifyRow(${i})\\\">✕ Remove</button>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m    </div>`;\u001b[39m\n\u001b[31m  }).join('');·\u001b[39m\n\u001b[31m  // Store dispatch object for confirm\u001b[39m\n\u001b[31m  pane._pendingDispatch = d;\u001b[39m\n\u001b[31m  pane.style.display = 'block';\u001b[39m\n\u001b[31m  pane.scrollTop = 0;\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction updateVerifyField(idx, field, input) {\u001b[39m\n\u001b[31m  const pane = document.getElementById('verifyPane');\u001b[39m\n\u001b[31m  if (!pane._pendingDispatch) return;\u001b[39m\n\u001b[31m  const c = pane._pendingDispatch.consignees[idx];\u001b[39m\n\u001b[31m  if (!c) return;\u001b[39m\n\u001b[31m  c[field] = input.value.trim();\u001b[39m\n\u001b[31m  input.classList.toggle('changed', input.value.trim() !== '');\u001b[39m\n\u001b[31m  // Re-check dupe if docket changed\u001b[39m\n\u001b[31m  if (field === 'docket') refreshVerifyFlags(pane._pendingDispatch);\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction refreshVerifyFlags(d) {\u001b[39m\n\u001b[31m  const seen = {};\u001b[39m\n\u001b[31m  d.consignees.forEach(c => { if (c.docket) seen[c.docket] = (seen[c.docket]||0)+1; });\u001b[39m\n\u001b[31m  d.consignees.forEach((c, i) => {\u001b[39m\n\u001b[31m    const row = document.getElementById('vrow_' + i);\u001b[39m\n\u001b[31m    if (!row) return;\u001b[39m\n\u001b[31m    const isDupe = c.docket && seen[c.docket] > 1;\u001b[39m\n\u001b[31m    row.classList.toggle('has-dupe', isDupe);\u001b[39m\n\u001b[31m  });\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction removeVerifyRow(idx) {\u001b[39m\n\u001b[31m  const pane = document.getElementById('verifyPane');\u001b[39m\n\u001b[31m  if (!pane._pendingDispatch) return;\u001b[39m\n\u001b[31m  const el = document.getElementById('vrow_' + idx);\u001b[39m\n\u001b[31m  if (el) {\u001b[39m\n\u001b[31m    el.style.transition = 'opacity .25s';\u001b[39m\n\u001b[31m    el.style.opacity = '0';\u001b[39m\n\u001b[31m    setTimeout(() => {\u001b[39m\n\u001b[31m      pane._pendingDispatch.consignees.splice(idx, 1);\u001b[39m\n\u001b[31m      showVerifyScreen(pane._pendingDispatch); // re-render with updated indices\u001b[39m\n\u001b[31m    }, 250);\u001b[39m\n\u001b[31m  } else {\u001b[39m\n\u001b[31m    pane._pendingDispatch.consignees.splice(idx, 1);\u001b[39m\n\u001b[31m    showVerifyScreen(pane._pendingDispatch);\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction approveAll() {\u001b[39m\n\u001b[31m  showToast('All rows approved ✓', 'ok');\u001b[39m\n\u001b[31m  document.getElementById('btnConfirmFinalise').style.background = 'var(--grn)';\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction closeVerify() {\u001b[39m\n\u001b[31m  const pane = document.getElementById('verifyPane');\u001b[39m\n\u001b[31m  // If this dispatch was already finalised, warn before going back\u001b[39m\n\u001b[31m  if (_lastFinalisedId && pane._pendingDispatch?.id === _lastFinalisedId) {\u001b[39m\n\u001b[31m    if (!confirm(\u001b[39m\n\u001b[31m      'This dispatch has already been finalised.\\\\n\\\\n' +\u001b[39m\n\u001b[31m      'Going back will take you to the edit form — any changes will create a NEW dispatch record.\\\\n\\\\n' +\u001b[39m\n\u001b[31m      'Go back anyway?'\u001b[39m\n\u001b[31m    )) return;\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m  pane.style.display = 'none';\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction showSlipFull(idx) {\u001b[39m\n\u001b[31m  const s = slipOCRData[idx];\u001b[39m\n\u001b[31m  if (!s?.imageData) return;\u001b[39m\n\u001b[31m  const w = window.open('');\u001b[39m\n\u001b[31m  w.document.write(`<img src=\\\"${s.imageData}\\\" style=\\\"max-width:100%;height:auto;\\\">`);\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// Log OCR correction to sheets for future accuracy improvement\u001b[39m\n\u001b[31masync function logOCRCorrection(original, corrected, courier) {\u001b[39m\n\u001b[31m  if (!window.appConfig.webhook_url || original === corrected) return;\u001b[39m\n\u001b[31m  try {\u001b[39m\n\u001b[31m    await fetch(window.appConfig.webhook_url, {\u001b[39m\n\u001b[31m      method: 'POST', mode: 'no-cors',\u001b[39m\n\u001b[31m      headers: { 'Content-Type': 'application/json' },\u001b[39m\n\u001b[31m      body: JSON.stringify({\u001b[39m\n\u001b[31m        type: 'ocr_correction',\u001b[39m\n\u001b[31m        original, corrected, courier,\u001b[39m\n\u001b[31m        timestamp: new Date().toISOString()\u001b[39m\n\u001b[31m      })\u001b[39m\n\u001b[31m    });\u001b[39m\n\u001b[31m  } catch(e) {}\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mlet _lastFinalisedId = null; // track last finalised dispatch to warn on back·\u001b[39m\n\u001b[31masync function confirmAndFinalise() {\u001b[39m\n\u001b[31m  const pane = document.getElementById('verifyPane');\u001b[39m\n\u001b[31m  const d = pane._pendingDispatch;\u001b[39m\n\u001b[31m  if (!d) return;·\u001b[39m\n\u001b[31m  d.status = 'active';\u001b[39m\n\u001b[31m  d.finalisedAt = new Date().toISOString();·\u001b[39m\n\u001b[31m  // Log OCR corrections\u001b[39m\n\u001b[31m  slipOCRData.forEach((s, i) => {\u001b[39m\n\u001b[31m    const c = d.consignees[i];\u001b[39m\n\u001b[31m    if (c && s.docket && c.docket && s.docket !== c.docket) {\u001b[39m\n\u001b[31m      logOCRCorrection(s.docket, c.docket, d.courier);\u001b[39m\n\u001b[31m    }\u001b[39m\n\u001b[31m  });·\u001b[39m\n\u001b[31m  // Replace existing draft with same ID if present, else prepend\u001b[39m\n\u001b[31m  const existingIdx = dispatches.findIndex(x => x.id === d.id);\u001b[39m\n\u001b[31m  if (existingIdx >= 0) {\u001b[39m\n\u001b[31m    dispatches[existingIdx] = d;\u001b[39m\n\u001b[31m  } else {\u001b[39m\n\u001b[31m    dispatches.unshift(d);\u001b[39m\n\u001b[31m  }·\u001b[39m\n\u001b[31m  _lastFinalisedId = d.id;\u001b[39m\n\u001b[31m  saveData();\u001b[39m\n\u001b[31m  syncDispatchToDB(d); // push to Postgres (non-blocking)\u001b[39m\n\u001b[31m  updateHistCount();\u001b[39m\n\u001b[31m  if (window.appConfig.auto_log !== 'false' && window.appConfig.webhook_url) await logToSheets(d);\u001b[39m\n\u001b[31m  showToast('Dispatch finalised ✓', 'ok');\u001b[39m\n\u001b[31m  pane.style.display = 'none';\u001b[39m\n\u001b[31m  pane._pendingDispatch = null; // clear pending\u001b[39m\n\u001b[31m  resetForm();\u001b[39m\n\u001b[31m  showPane('dashboard');\u001b[39m\n\u001b[31m  renderDashboard();\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction resetForm() {\u001b[39m\n\u001b[31m  document.getElementById('n_client').value = '';\u001b[39m\n\u001b[31m  document.getElementById('n_jobref').value = '';\u001b[39m\n\u001b[31m  document.getElementById('n_courier').value = '';\u001b[39m\n\u001b[31m  document.getElementById('n_remarks').value = '';\u001b[39m\n\u001b[31m  document.getElementById('jobPill').style.display = 'none';\u001b[39m\n\u001b[31m  document.getElementById('customClientField').style.display = 'none';\u001b[39m\n\u001b[31m  document.getElementById('consList').innerHTML = '';\u001b[39m\n\u001b[31m  addConsignee();\u001b[39m\n\u001b[31m  clearSlips();\u001b[39m\n\u001b[31m  clearStores();\u001b[39m\n\u001b[31m  setToday();\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction newDispatch() { resetForm(); showPane('new'); }·\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31m// DASHBOARD\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31mfunction renderDashboard() {\u001b[39m\n\u001b[31m  // Exclude drafts from dashboard — only show active/finalised dispatches\u001b[39m\n\u001b[31m  const activeDis = dispatches.filter(d => d.status !== 'draft');\u001b[39m\n\u001b[31m  const allCons = activeDis.flatMap(d => d.consignees || []);\u001b[39m\n\u001b[31m  const countBy = (status) => allCons.filter(c => c.status === status).length;\u001b[39m\n\u001b[31m  document.getElementById('st-delivered').textContent = countBy('delivered');\u001b[39m\n\u001b[31m  document.getElementById('st-transit').textContent = countBy('transit') + countBy('in-transit');\u001b[39m\n\u001b[31m  document.getElementById('st-booked').textContent = countBy('booked');\u001b[39m\n\u001b[31m  document.getElementById('st-pending').textContent = allCons.filter(c => !c.docket || c.status === 'pending').length;·\u001b[39m\n\u001b[31m  const grid = document.getElementById('dashGrid');\u001b[39m\n\u001b[31m  // Flatten all consignees across dispatches\u001b[39m\n\u001b[31m  let items = activeDis.flatMap(d =>\u001b[39m\n\u001b[31m    (d.consignees || []).filter(c => c.docket).map(c => ({...c, dispatchId: d.id, courier: d.courier, client: d.client, jobref: d.jobref}))\u001b[39m\n\u001b[31m  );\u001b[39m\n\u001b[31m  if (dashFilter !== 'all') {\u001b[39m\n\u001b[31m    items = items.filter(c => {\u001b[39m\n\u001b[31m      if (dashFilter === 'delivered') return c.status === 'delivered';\u001b[39m\n\u001b[31m      if (dashFilter === 'transit') return c.status === 'transit' || c.status === 'in-transit';\u001b[39m\n\u001b[31m      if (dashFilter === 'booked') return c.status === 'booked';\u001b[39m\n\u001b[31m      if (dashFilter === 'pending') return !c.docket || c.status === 'pending';\u001b[39m\n\u001b[31m      return true;\u001b[39m\n\u001b[31m    });\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m  if (!items.length) {\u001b[39m\n\u001b[31m    grid.innerHTML = '<div class=\\\"empty\\\"><div class=\\\"e-icon\\\">📦</div><p>No docket-tracked consignees.<br>Add docket numbers to see tiles.</p></div>';\u001b[39m\n\u001b[31m    return;\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m  const progMap = { delivered:100, transit:60,'in-transit':60, booked:20, pending:5 };\u001b[39m\n\u001b[31m  const progClr = { delivered:'var(--grn)', transit:'var(--ylw)','in-transit':'var(--ylw)', booked:'var(--sky)', pending:'var(--t4)' };\u001b[39m\n\u001b[31m  grid.innerHTML = items.map(c => {\u001b[39m\n\u001b[31m    const sp = c.status || 'booked';\u001b[39m\n\u001b[31m    const prog = progMap[sp] || 20;\u001b[39m\n\u001b[31m    const clr = progClr[sp] || 'var(--t4)';\u001b[39m\n\u001b[31m    return `\u001b[39m\n\u001b[31m    <div class=\\\"docket-tile\\\" onclick=\\\"showDocketDetail('${c.docket}','${c.courier||''}','${c.dispatchId}')\\\">\u001b[39m\n\u001b[31m      <div class=\\\"docket-tile-hdr\\\">\u001b[39m\n\u001b[31m        <div class=\\\"docket-tile-left\\\">\u001b[39m\n\u001b[31m          <div class=\\\"docket-num\\\">${c.docket}</div>\u001b[39m\n\u001b[31m          <div class=\\\"docket-store\\\">${c.name} — ${c.city}</div>\u001b[39m\n\u001b[31m          <div class=\\\"docket-courier\\\">${c.courier||''} · ${c.client||''} ${c.jobref ? '› '+c.jobref : ''}</div>\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m        <span class=\\\"status-pill sp-${sp}\\\">${sp}</span>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      <div style=\\\"display:flex;gap:6px;margin-bottom:6px;align-items:center;\\\">\u001b[39m\n\u001b[31m        <input type=\\\"checkbox\\\" id=\\\"sel_${c.docket.replace(/[^a-z0-9]/gi,'_')}\\\"\u001b[39m\n\u001b[31m          onclick=\\\"event.stopPropagation();toggleTileSelect('${c.docket}')\\\"\u001b[39m\n\u001b[31m          style=\\\"width:14px;height:14px;cursor:pointer;accent-color:var(--acc);\\\">\u001b[39m\n\u001b[31m        <label style=\\\"font-size:.6rem;color:var(--t3);font-family:var(--fnt-mono);cursor:pointer;\\\"\u001b[39m\n\u001b[31m          onclick=\\\"event.stopPropagation();toggleTileSelect('${c.docket}')\\\">Select</label>\u001b[39m\n\u001b[31m        ${c.manualStatus ?\u001b[39m\n\u001b[31m          `<span style=\\\"font-size:.6rem;font-family:var(--fnt-mono);color:var(--grn);\\\">📞 ${c.manualStatus}</span>` : ''}\u001b[39m\n\u001b[31m        ${c.podUploaded ? '<span style=\\\"font-size:.6rem;color:var(--grn);font-family:var(--fnt-mono);\\\">✓ POD</span>' : ''}\u001b[39m\n\u001b[31m        <button onclick=\\\"event.stopPropagation();showManualTracking('${c.docket}','${c.dispatchId}','${c.name}','${c.phone||''}','${c.city||''}')\\\"\u001b[39m\n\u001b[31m          style=\\\"font-size:.6rem;background:rgba(56,189,248,.1);border:1px solid rgba(56,189,248,.3);\u001b[39m\n\u001b[31m          color:var(--sky);padding:2px 7px;border-radius:10px;cursor:pointer;font-family:var(--fnt-mono);\\\">📞 Call Log</button>\u001b[39m\n\u001b[31m        <button onclick=\\\"event.stopPropagation();showPODUpload('${c.docket}','${c.dispatchId}')\\\"\u001b[39m\n\u001b[31m          style=\\\"font-size:.6rem;background:none;border:1px solid var(--bdr);color:var(--t3);\u001b[39m\n\u001b[31m          padding:2px 7px;border-radius:10px;cursor:pointer;font-family:var(--fnt-mono);\\\">📸 POD</button>\u001b[39m\n\u001b[31m        ${c.manualDelivered ? `<button onclick=\\\"event.stopPropagation();resumeAutoTracking('${c.docket}','${c.dispatchId}')\\\"\u001b[39m\n\u001b[31m          style=\\\"font-size:.6rem;background:rgba(251,146,60,.1);border:1px solid rgba(251,146,60,.4);\u001b[39m\n\u001b[31m          color:#fb923c;padding:2px 7px;border-radius:10px;cursor:pointer;font-family:var(--fnt-mono);\\\">↩ Undo</button>` : ''}\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      <div class=\\\"docket-tile-footer\\\">\u001b[39m\n\u001b[31m        <div style=\\\"flex:1;min-width:0;\\\">\u001b[39m\n\u001b[31m          <span class=\\\"docket-last-event docket-evt-${c.docket}\\\" id=\\\"evt_${c.docket.replace(/[^a-z0-9]/gi,'_')}\\\">${c.lastEvent || 'No tracking yet'}</span>\u001b[39m\n\u001b[31m          <span class=\\\"track-spinner\\\" id=\\\"spin_${c.docket.replace(/[^a-z0-9]/gi,'_')}\\\" style=\\\"display:none;color:var(--ylw);font-family:var(--fnt-mono);font-size:.68rem;animation:blink 1s infinite;\\\">⏳ tracking...</span>\u001b[39m\n\u001b[31m          ${c.lastScanLocation && c.lastScanLocation !== c.lastEvent ?·\u001b[39m\n\u001b[31m            `<div style=\\\"font-size:.62rem;color:var(--t3);font-family:var(--fnt-mono);margin-top:2px;\\\">📍 ${c.lastScanLocation}</div>` : ''}\u001b[39m\n\u001b[31m          ${c.lastTrackedAt ?·\u001b[39m\n\u001b[31m            `<div style=\\\"font-size:.6rem;color:var(--t4);font-family:var(--fnt-mono);margin-top:1px;\\\">🕐 tracked ${new Date(c.lastTrackedAt).toLocaleTimeString('en-IN',{hour:'2-digit',minute:'2-digit'})}</div>` : ''}\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m        <button class=\\\"btn-track-live live-btn-${c.docket.replace(/[^a-z0-9]/gi,'_')}\\\" onclick=\\\"event.stopPropagation();liveTrackTile('${c.docket}','${c.courier||''}','${c.dispatchId}')\\\">\u001b[39m\n\u001b[31m          <span class=\\\"live-dot\\\"></span>LIVE\u001b[39m\n\u001b[31m        </button>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      <div class=\\\"docket-progress\\\">\u001b[39m\n\u001b[31m        <div class=\\\"docket-progress-fill\\\" style=\\\"width:${prog}%;background:${clr};\\\"></div>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m    </div>`;\u001b[39m\n\u001b[31m  }).join('');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction filterDash(f, el) {\u001b[39m\n\u001b[31m  dashFilter = f;\u001b[39m\n\u001b[31m  document.querySelectorAll('#dashFilters .btn-sm').forEach(b => b.classList.remove('primary'));\u001b[39m\n\u001b[31m  el.classList.add('primary');\u001b[39m\n\u001b[31m  renderDashboard();\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31masync function liveTrackTile(docket, courier, dispatchId) {\u001b[39m\n\u001b[31m  showToast(`Tracking ${docket}...`, 'info');\u001b[39m\n\u001b[31m  const result = await fetchTracking(docket, courier);\u001b[39m\n\u001b[31m  // Update status in dispatches\u001b[39m\n\u001b[31m  for (const d of dispatches) {\u001b[39m\n\u001b[31m    if (d.id !== dispatchId) continue;\u001b[39m\n\u001b[31m    for (const c of d.consignees) {\u001b[39m\n\u001b[31m      if (c.docket === docket) {\u001b[39m\n\u001b[31m        c.status = result.status || c.status;\u001b[39m\n\u001b[31m        c.lastEvent = result.lastEvent || c.lastEvent;\u001b[39m\n\u001b[31m        c.lastUpdated = new Date().toISOString();\u001b[39m\n\u001b[31m      }\u001b[39m\n\u001b[31m    }\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m  saveData();\u001b[39m\n\u001b[31m  renderDashboard();\u001b[39m\n\u001b[31m  if (result.status === 'failed') {\u001b[39m\n\u001b[31m    // Open courier site as fallback\u001b[39m\n\u001b[31m    openCourierTrackUrl(docket, courier);\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction showDocketDetail(docket, courier, dispatchId) {\u001b[39m\n\u001b[31m  const d = dispatches.find(x => x.id === dispatchId);\u001b[39m\n\u001b[31m  const c = d?.consignees?.find(x => x.docket === docket);\u001b[39m\n\u001b[31m  if (!c) return;\u001b[39m\n\u001b[31m  const content = document.getElementById('detailContent');\u001b[39m\n\u001b[31m  content.innerHTML = `\u001b[39m\n\u001b[31m    <div class=\\\"sheet-title\\\">${docket}</div>\u001b[39m\n\u001b[31m    <div class=\\\"info-tag\\\">📍 <strong>${c.name}</strong> — ${c.city}</div>\u001b[39m\n\u001b[31m    ${c.phone ? `<div class=\\\"info-tag\\\">📞 ${c.phone}</div>` : ''}\u001b[39m\n\u001b[31m    <div class=\\\"info-tag\\\">📦 ${c.material||'—'} ${c.qty ? '('+c.qty+' pcs)':''}</div>\u001b[39m\n\u001b[31m    <div class=\\\"info-tag\\\">🚚 ${courier||'—'} · <span class=\\\"status-pill sp-${c.status||'pending'}\\\">${c.status||'pending'}</span></div>\u001b[39m\n\u001b[31m    ${c.lastEvent ? `<div class=\\\"info-tag\\\">🕐 ${c.lastEvent}</div>` : ''}\u001b[39m\n\u001b[31m    <div class=\\\"sec-div\\\"><span>Tracking History</span></div>\u001b[39m\n\u001b[31m    <div id=\\\"docketTimeline\\\">\u001b[39m\n\u001b[31m      ${c.timeline?.length ?\u001b[39m\n\u001b[31m        c.timeline.map(e => `<div class=\\\"track-evt\\\"><span class=\\\"track-time\\\">${e.time||''}</span><div><div class=\\\"track-desc\\\">${e.desc||''}</div><div class=\\\"track-loc\\\">${e.loc||''}</div></div></div>`).join('') :\u001b[39m\n\u001b[31m        '<div style=\\\"color:var(--t3);font-size:.78rem;padding:10px 0;\\\">No timeline data. Click Live Track to fetch.</div>'}\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m    <div style=\\\"display:flex;gap:6px;flex-wrap:wrap;margin-top:14px;\\\">\u001b[39m\n\u001b[31m      <button class=\\\"btn btn-ghost\\\" style=\\\"flex:1;\\\" onclick=\\\"openCourierTrackUrl('${docket}','${courier}')\\\">🔗 Courier Site</button>\u001b[39m\n\u001b[31m      <button class=\\\"btn btn-orange\\\" style=\\\"flex:1;\\\" onclick=\\\"liveTrackTile('${docket}','${courier}','${dispatchId}');closeOverlay('detailOverlay')\\\">⚡ Live Track</button>\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m    <div style=\\\"display:flex;gap:6px;flex-wrap:wrap;margin-top:6px;\\\">\u001b[39m\n\u001b[31m      <button class=\\\"btn-wa\\\" style=\\\"flex:1;padding:8px;\\\" onclick=\\\"closeOverlay('detailOverlay');showPODUpload('${docket}','${dispatchId}')\\\">📸 Upload POD</button>\u001b[39m\n\u001b[31m      ${c.podUploaded ? '<span style=\\\"color:var(--grn);font-size:.72rem;font-family:var(--fnt-mono);padding:6px;\\\">✓ POD uploaded</span>' : ''}\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m  `;\u001b[39m\n\u001b[31m  openOverlay('detailOverlay');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31m// TRACKING ENGINE\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31mfunction _normaliseStatus(raw) {\u001b[39m\n\u001b[31m  const s = (raw || '').toLowerCase().trim();\u001b[39m\n\u001b[31m  if (!s || s === 'error') return null;\u001b[39m\n\u001b[31m  if (s.includes('undeliver') || s.includes('delivery_fail') ||\u001b[39m\n\u001b[31m      s.includes('failed_delivery') || s.includes('not deliver') ||\u001b[39m\n\u001b[31m      s.includes('return')  || s.includes('rto')) return 'pending';\u001b[39m\n\u001b[31m  if (s.includes('deliver'))                       return 'delivered';\u001b[39m\n\u001b[31m  if (s.includes('transit') || s.includes('hub') ||\u001b[39m\n\u001b[31m      s.includes('inbound') || s.includes('outbound') ||\u001b[39m\n\u001b[31m      s.includes('out for'))                       return 'transit';\u001b[39m\n\u001b[31m  if (s.includes('book') || s.includes('pick'))   return 'booked';\u001b[39m\n\u001b[31m  return s;\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31masync function _fetchWithRetry(url, opts, maxRetries = 1, retryDelayMs = 4000) {\u001b[39m\n\u001b[31m  for (let attempt = 0; attempt <= maxRetries; attempt++) {\u001b[39m\n\u001b[31m    try {\u001b[39m\n\u001b[31m      const resp = await fetch(url, opts);\u001b[39m\n\u001b[31m      if (resp.ok) return resp;\u001b[39m\n\u001b[31m    } catch(e) {\u001b[39m\n\u001b[31m      if (attempt === maxRetries) throw e;\u001b[39m\n\u001b[31m      await new Promise(r => setTimeout(r, retryDelayMs));\u001b[39m\n\u001b[31m    }\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m  return null;\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31masync function fetchTracking(docket, courier) {\u001b[39m\n\u001b[31m  // Strategy 1: VPS proxy (SkyKing + others that block direct fetch)\u001b[39m\n\u001b[31m  const proxyUrl = window.appConfig.proxy_url;\u001b[39m\n\u001b[31m  if (proxyUrl) {\u001b[39m\n\u001b[31m    try {\u001b[39m\n\u001b[31m      const url  = `${proxyUrl}?courier=${encodeURIComponent(courier)}&docket=${encodeURIComponent(docket)}`;\u001b[39m\n\u001b[31m      const resp = await _fetchWithRetry(url, { signal: AbortSignal.timeout(15000) });\u001b[39m\n\u001b[31m      if (resp) {\u001b[39m\n\u001b[31m        const data      = await resp.json();\u001b[39m\n\u001b[31m        const normStatus = _normaliseStatus(data.status);\u001b[39m\n\u001b[31m        if (normStatus) {\u001b[39m\n\u001b[31m          return { status: normStatus, lastEvent: data.lastEvent || '', timeline: data.timeline || [] };\u001b[39m\n\u001b[31m        }\u001b[39m\n\u001b[31m        openCourierTrackUrl(docket, courier);\u001b[39m\n\u001b[31m        return { status: 'transit', lastEvent: data.lastEvent || 'Check courier site', timeline: [] };\u001b[39m\n\u001b[31m      }\u001b[39m\n\u001b[31m    } catch(e) {}\u001b[39m\n\u001b[31m  }·\u001b[39m\n\u001b[31m  // Strategy 2: Delhivery direct API\u001b[39m\n\u001b[31m  if (courier === 'Delhivery') {\u001b[39m\n\u001b[31m    try {\u001b[39m\n\u001b[31m      const resp = await _fetchWithRetry(\u001b[39m\n\u001b[31m        `https://dlv-api.delhivery.com/v3/track?waybill=${docket}`,\u001b[39m\n\u001b[31m        { signal: AbortSignal.timeout(8000) }\u001b[39m\n\u001b[31m      );\u001b[39m\n\u001b[31m      if (resp) {\u001b[39m\n\u001b[31m        const data = await resp.json();\u001b[39m\n\u001b[31m        const pkg  = data.ShipmentData?.[0]?.Shipment;\u001b[39m\n\u001b[31m        if (pkg) {\u001b[39m\n\u001b[31m          const normStatus = _normaliseStatus(pkg.Status?.Status);\u001b[39m\n\u001b[31m          return { status: normStatus || 'transit', lastEvent: pkg.Status?.Instructions || '', timeline: [] };\u001b[39m\n\u001b[31m        }\u001b[39m\n\u001b[31m      }\u001b[39m\n\u001b[31m    } catch(e) {}\u001b[39m\n\u001b[31m  }·\u001b[39m\n\u001b[31m  return { status: 'failed', lastEvent: 'Open courier site to check', timeline: [] };\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction openCourierTrackUrl(docket, courier) {\u001b[39m\n\u001b[31m  const m = COURIER_MASTER[courier];\u001b[39m\n\u001b[31m  if (!m) return;\u001b[39m\n\u001b[31m  const url = m.trackUrl.replace('{docket}', docket);\u001b[39m\n\u001b[31m  window.open(url, '_blank');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// Track tab - multiple dockets\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31m// TRACK TAB — new logic\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31mlet _trackSelected = new Set(); // dockets currently selected for tracking·\u001b[39m\n\u001b[31m// Called when Track tab opens or courier changes\u001b[39m\n\u001b[31mfunction buildTrackDocketChips() {\u001b[39m\n\u001b[31m  const courier = document.getElementById('t_courier').value;\u001b[39m\n\u001b[31m  const m = COURIER_MASTER[courier];\u001b[39m\n\u001b[31m  const maxBulk = m?.maxBulk || 1;\u001b[39m\n\u001b[31m  const limitEl = document.getElementById('trackCourierLimit');\u001b[39m\n\u001b[31m  if (limitEl) limitEl.textContent = `Max ${maxBulk} at once for ${courier}`;·\u001b[39m\n\u001b[31m  // Gather all dockets from active dispatches for this courier\u001b[39m\n\u001b[31m  const activeDis = dispatches.filter(d => d.status !== 'draft');\u001b[39m\n\u001b[31m  const allDockets = activeDis.flatMap(d =>\u001b[39m\n\u001b[31m    (d.consignees || [])\u001b[39m\n\u001b[31m      .filter(c => c.docket && d.courier === courier)\u001b[39m\n\u001b[31m      .map(c => ({ docket: c.docket, name: c.name, city: c.city, status: c.status || 'booked' }))\u001b[39m\n\u001b[31m  );·\u001b[39m\n\u001b[31m  // Also include dockets from OTHER couriers as un-selectable reference\u001b[39m\n\u001b[31m  const otherDockets = activeDis.flatMap(d =>\u001b[39m\n\u001b[31m    (d.consignees || [])\u001b[39m\n\u001b[31m      .filter(c => c.docket && d.courier !== courier)\u001b[39m\n\u001b[31m      .map(c => ({ docket: c.docket, name: c.name, courier: d.courier }))\u001b[39m\n\u001b[31m  );·\u001b[39m\n\u001b[31m  const chips = document.getElementById('trackDocketChips');\u001b[39m\n\u001b[31m  if (!allDockets.length && !otherDockets.length) {\u001b[39m\n\u001b[31m    chips.innerHTML = '<span style=\\\"font-size:.72rem;color:var(--t3);font-family:var(--fnt-mono);\\\">No dispatched dockets yet — enter manually below</span>';\u001b[39m\n\u001b[31m    return;\u001b[39m\n\u001b[31m  }·\u001b[39m\n\u001b[31m  const spClr = { delivered:'var(--grn)', transit:'var(--ylw)', booked:'var(--sky)', pending:'var(--t4)' };·\u001b[39m\n\u001b[31m  chips.innerHTML = [\u001b[39m\n\u001b[31m    // This courier's dockets\u001b[39m\n\u001b[31m    ...allDockets.map(d => {\u001b[39m\n\u001b[31m      const sel = _trackSelected.has(d.docket);\u001b[39m\n\u001b[31m      const clr = spClr[d.status] || 'var(--t4)';\u001b[39m\n\u001b[31m      return `<button onclick=\\\"toggleTrackDocket('${d.docket}')\\\" id=\\\"chip_${d.docket}\\\"\u001b[39m\n\u001b[31m        style=\\\"font-family:var(--fnt-mono);font-size:.72rem;padding:4px 10px;border-radius:20px;\u001b[39m\n\u001b[31m        cursor:pointer;transition:all .2s;border:1px solid ${sel ? 'var(--acc)' : 'var(--bdr)'};\u001b[39m\n\u001b[31m        background:${sel ? 'rgba(249,115,22,.15)' : 'var(--sur2)'};\u001b[39m\n\u001b[31m        color:${sel ? 'var(--acc)' : 'var(--t2)'};\\\">\u001b[39m\n\u001b[31m        ${d.docket} <span style=\\\"font-size:.6rem;color:${clr};\\\">● ${d.status}</span>\u001b[39m\n\u001b[31m        ${d.city ? `<span style=\\\"font-size:.6rem;color:var(--t3);margin-left:4px;\\\">${d.city}</span>` : ''}\u001b[39m\n\u001b[31m      </button>`;\u001b[39m\n\u001b[31m    }),\u001b[39m\n\u001b[31m    // Other couriers — show greyed, click selects and switches courier\u001b[39m\n\u001b[31m    ...otherDockets.slice(0, 5).map(d =>\u001b[39m\n\u001b[31m      `<span title=\\\"${d.courier}\\\" style=\\\"font-family:var(--fnt-mono);font-size:.68rem;padding:3px 8px;\u001b[39m\n\u001b[31m        border-radius:20px;border:1px dashed var(--bdr);color:var(--t4);cursor:default;\\\">\u001b[39m\n\u001b[31m        ${d.docket} <span style=\\\"font-size:.55rem;\\\">${d.courier}</span>\u001b[39m\n\u001b[31m      </span>`\u001b[39m\n\u001b[31m    )\u001b[39m\n\u001b[31m  ].join('');·\u001b[39m\n\u001b[31m  updateTrackSummary();\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction toggleTrackDocket(docket) {\u001b[39m\n\u001b[31m  const courier = document.getElementById('t_courier').value;\u001b[39m\n\u001b[31m  const m = COURIER_MASTER[courier];\u001b[39m\n\u001b[31m  const maxBulk = m?.maxBulk || 1;·\u001b[39m\n\u001b[31m  if (_trackSelected.has(docket)) {\u001b[39m\n\u001b[31m    _trackSelected.delete(docket);\u001b[39m\n\u001b[31m  } else {\u001b[39m\n\u001b[31m    if (_trackSelected.size >= maxBulk) {\u001b[39m\n\u001b[31m      showToast(`${courier} supports max ${maxBulk} docket(s) at once`, 'err');\u001b[39m\n\u001b[31m      return;\u001b[39m\n\u001b[31m    }\u001b[39m\n\u001b[31m    _trackSelected.add(docket);\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m  buildTrackDocketChips(); // re-render chips with updated selection\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction addManualDockets() {\u001b[39m\n\u001b[31m  const raw = document.getElementById('t_manual_dockets').value;\u001b[39m\n\u001b[31m  const courier = document.getElementById('t_courier').value;\u001b[39m\n\u001b[31m  const m = COURIER_MASTER[courier];\u001b[39m\n\u001b[31m  const maxBulk = m?.maxBulk || 1;\u001b[39m\n\u001b[31m  const newDockets = raw.split(/[,\\\\n]/).map(d => d.trim()).filter(Boolean);\u001b[39m\n\u001b[31m  let added = 0;\u001b[39m\n\u001b[31m  for (const d of newDockets) {\u001b[39m\n\u001b[31m    if (_trackSelected.size >= maxBulk) {\u001b[39m\n\u001b[31m      showToast(`Max ${maxBulk} reached for ${courier}`, 'err');\u001b[39m\n\u001b[31m      break;\u001b[39m\n\u001b[31m    }\u001b[39m\n\u001b[31m    _trackSelected.add(d);\u001b[39m\n\u001b[31m    added++;\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m  document.getElementById('t_manual_dockets').value = '';\u001b[39m\n\u001b[31m  buildTrackDocketChips();\u001b[39m\n\u001b[31m  if (added) showToast(`${added} docket(s) added ✓`, 'ok');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction clearTrackSelection() {\u001b[39m\n\u001b[31m  _trackSelected.clear();\u001b[39m\n\u001b[31m  buildTrackDocketChips();\u001b[39m\n\u001b[31m  document.getElementById('trackResultsArea').innerHTML = '';\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction updateTrackSummary() {\u001b[39m\n\u001b[31m  const summary = document.getElementById('trackSelectedSummary');\u001b[39m\n\u001b[31m  const countEl = document.getElementById('trackSelectedCount');\u001b[39m\n\u001b[31m  const listEl  = document.getElementById('trackSelectedList');\u001b[39m\n\u001b[31m  if (!summary) return;\u001b[39m\n\u001b[31m  const arr = Array.from(_trackSelected);\u001b[39m\n\u001b[31m  summary.style.display = arr.length ? 'block' : 'none';\u001b[39m\n\u001b[31m  if (countEl) countEl.textContent = `${arr.length} docket(s) selected`;\u001b[39m\n\u001b[31m  if (listEl)  listEl.textContent  = arr.slice(0, 5).join(', ') + (arr.length > 5 ? `... +${arr.length-5}` : '');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction openTrackOnCourierSite() {\u001b[39m\n\u001b[31m  const courier = document.getElementById('t_courier').value;\u001b[39m\n\u001b[31m  const dockets = Array.from(_trackSelected);\u001b[39m\n\u001b[31m  if (!dockets.length) { showToast('Select dockets first', 'err'); return; }\u001b[39m\n\u001b[31m  const m = COURIER_MASTER[courier];\u001b[39m\n\u001b[31m  if (!m) return;\u001b[39m\n\u001b[31m  // SkyKing: open with all CNOs comma-separated in URL\u001b[39m\n\u001b[31m  let url;\u001b[39m\n\u001b[31m  if (courier === 'SkyKing') {\u001b[39m\n\u001b[31m    url = `https://skyking.co/track?cno=${dockets.join(',')}`;\u001b[39m\n\u001b[31m  } else if (dockets.length === 1) {\u001b[39m\n\u001b[31m    url = m.trackUrl.replace('{docket}', dockets[0]);\u001b[39m\n\u001b[31m  } else {\u001b[39m\n\u001b[31m    url = m.trackBulkUrl || m.trackUrl.replace('{docket}', dockets[0]);\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m  window.open(url, '_blank');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31masync function trackSelected() {\u001b[39m\n\u001b[31m  const courier = document.getElementById('t_courier').value;\u001b[39m\n\u001b[31m  const dockets = Array.from(_trackSelected);\u001b[39m\n\u001b[31m  if (!dockets.length) { showToast('Select at least one docket', 'err'); return; }·\u001b[39m\n\u001b[31m  const area = document.getElementById('trackResultsArea');\u001b[39m\n\u001b[31m  area.innerHTML = `<div class=\\\"card\\\" style=\\\"text-align:center;padding:20px;\\\">\u001b[39m\n\u001b[31m    <div style=\\\"font-size:1.5rem;margin-bottom:8px;\\\">⏳</div>\u001b[39m\n\u001b[31m    <div style=\\\"font-family:var(--fnt-mono);font-size:.78rem;color:var(--t2);\\\">Tracking ${dockets.length} docket(s) via ${courier}...</div>\u001b[39m\n\u001b[31m  </div>`;·\u001b[39m\n\u001b[31m  let resultsHTML = '';\u001b[39m\n\u001b[31m  for (const docket of dockets) {\u001b[39m\n\u001b[31m    const result = await fetchTracking(docket, courier);\u001b[39m\n\u001b[31m    const sp = (!result.status || result.status === 'failed') ? 'pending' : result.status;\u001b[39m\n\u001b[31m    const spClr = { delivered:'var(--grn)', transit:'var(--ylw)', booked:'var(--sky)', pending:'var(--t3)' };\u001b[39m\n\u001b[31m    const clr = spClr[sp] || 'var(--t3)';\u001b[39m\n\u001b[31m    // Update stored dispatch if this docket exists in our records\u001b[39m\n\u001b[31m    for (const d of dispatches) {\u001b[39m\n\u001b[31m      for (const c of (d.consignees||[])) {\u001b[39m\n\u001b[31m        if (c.docket === docket && result.status && result.status !== 'failed') {\u001b[39m\n\u001b[31m          c.status = result.status;\u001b[39m\n\u001b[31m          c.lastEvent = result.lastEvent;\u001b[39m\n\u001b[31m          c.lastTrackedAt = new Date().toISOString();\u001b[39m\n\u001b[31m        }\u001b[39m\n\u001b[31m      }\u001b[39m\n\u001b[31m    }\u001b[39m\n\u001b[31m    resultsHTML += `\u001b[39m\n\u001b[31m    <div style=\\\"background:var(--sur);border:1px solid var(--bdr);border-left:3px solid ${clr};\u001b[39m\n\u001b[31m      border-radius:var(--r-sm);padding:10px 12px;margin-bottom:8px;\\\">\u001b[39m\n\u001b[31m      <div style=\\\"display:flex;justify-content:space-between;align-items:center;margin-bottom:6px;\\\">\u001b[39m\n\u001b[31m        <span style=\\\"font-family:var(--fnt-mono);font-size:.9rem;color:var(--sky);\\\">${docket}</span>\u001b[39m\n\u001b[31m        <span class=\\\"status-pill sp-${sp}\\\">${sp}</span>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      <div style=\\\"font-size:.75rem;color:var(--t2);font-family:var(--fnt-mono);\\\">${result.lastEvent || '—'}</div>\u001b[39m\n\u001b[31m      ${result.lastScanLocation ? `<div style=\\\"font-size:.68rem;color:var(--t3);margin-top:3px;\\\">📍 ${result.lastScanLocation}</div>` : ''}\u001b[39m\n\u001b[31m      <div style=\\\"display:flex;gap:8px;margin-top:8px;\\\">\u001b[39m\n\u001b[31m        <button class=\\\"btn-sm\\\" onclick=\\\"openCourierTrackUrl('${docket}','${courier}')\\\">🔗 Open on ${courier}</button>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m    </div>`;\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m  saveData();\u001b[39m\n\u001b[31m  area.innerHTML = `<div style=\\\"margin-top:4px;\\\">${resultsHTML}</div>`;\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// Keep old names as aliases for compatibility\u001b[39m\n\u001b[31mfunction trackMultiple() { trackSelected(); }\u001b[39m\n\u001b[31mfunction openCourierSite() { openTrackOnCourierSite(); }·\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31m// HISTORY\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31mfunction renderHistory() {\u001b[39m\n\u001b[31m  const list = document.getElementById('historyList');\u001b[39m\n\u001b[31m  if (!dispatches.length) {\u001b[39m\n\u001b[31m    list.innerHTML = '<div class=\\\"empty\\\"><div class=\\\"e-icon\\\">📭</div><p>No dispatches yet.</p></div>';\u001b[39m\n\u001b[31m    return;\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m  list.innerHTML = dispatches.map(d => {\u001b[39m\n\u001b[31m    const cons = d.consignees?.length || 0;\u001b[39m\n\u001b[31m    const docketed = d.consignees?.filter(c=>c.docket).length || 0;\u001b[39m\n\u001b[31m    const clr = d.status === 'draft' ? 'var(--ylw)' : 'var(--grn)';\u001b[39m\n\u001b[31m    return `\u001b[39m\n\u001b[31m    <div class=\\\"docket-tile\\\" onclick=\\\"showDispatchDetail('${d.id}')\\\">\u001b[39m\n\u001b[31m      <div class=\\\"docket-tile-hdr\\\">\u001b[39m\n\u001b[31m        <div class=\\\"docket-tile-left\\\">\u001b[39m\n\u001b[31m          <div class=\\\"docket-num\\\" style=\\\"font-size:.78rem;\\\">${d.id}</div>\u001b[39m\n\u001b[31m          <div class=\\\"docket-store\\\">${d.client||'No client'} ${d.jobref ? '› '+d.jobref : ''}</div>\u001b[39m\n\u001b[31m          <div class=\\\"docket-courier\\\">📦 ${cons} consignees · ${docketed} docketed · ${d.courier||'—'} · ${d.dispatchDate||d.createdAt?.slice(0,10)||'—'}</div>\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m        <span class=\\\"status-pill\\\" style=\\\"background:rgba(0,0,0,.2);color:${clr};\\\">${d.status||'active'}</span>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m    </div>`;\u001b[39m\n\u001b[31m  }).join('');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction showDispatchDetail(id) {\u001b[39m\n\u001b[31m  const d = dispatches.find(x => x.id === id);\u001b[39m\n\u001b[31m  if (!d) return;\u001b[39m\n\u001b[31m  const content = document.getElementById('detailContent');\u001b[39m\n\u001b[31m  content.innerHTML = `\u001b[39m\n\u001b[31m    <div class=\\\"sheet-title\\\">${d.client} ${d.jobref ? '› '+d.jobref : ''}</div>\u001b[39m\n\u001b[31m    <div class=\\\"info-tag\\\"><strong>${d.id}</strong> · ${d.courier||'—'} · ${d.dispatchDate||'—'}</div>\u001b[39m\n\u001b[31m    <div class=\\\"sec-div\\\"><span>Consignees (${d.consignees?.length||0})</span></div>\u001b[39m\n\u001b[31m    ${(d.consignees||[]).map((c,i) => `\u001b[39m\n\u001b[31m      <div style=\\\"background:var(--sur2);border:1px solid var(--bdr);border-radius:var(--r-sm);padding:10px;margin-bottom:6px;\\\">\u001b[39m\n\u001b[31m        <div style=\\\"font-weight:600;margin-bottom:4px;\\\">${i+1}. ${c.name} — ${c.city}</div>\u001b[39m\n\u001b[31m        ${c.docket ? `<div style=\\\"font-family:var(--fnt-mono);font-size:.78rem;color:var(--sky);\\\">🔖 ${c.docket}</div>` : ''}\u001b[39m\n\u001b[31m        ${c.material ? `<div style=\\\"font-size:.75rem;color:var(--t2);\\\">📦 ${c.material} ${c.qty ? '('+c.qty+')':''}</div>` : ''}\u001b[39m\n\u001b[31m        <span class=\\\"status-pill sp-${c.status||'pending'}\\\" style=\\\"margin-top:4px;display:inline-block;\\\">${c.status||'pending'}</span>\u001b[39m\n\u001b[31m      </div>`).join('')}\u001b[39m\n\u001b[31m    ${d.remarks ? `<div class=\\\"info-tag\\\">📝 ${d.remarks}</div>` : ''}\u001b[39m\n\u001b[31m    <div class=\\\"btn-row\\\" style=\\\"margin-top:14px;\\\">\u001b[39m\n\u001b[31m      <button class=\\\"btn btn-ghost\\\" onclick=\\\"deleteDispatch('${d.id}');closeOverlay('detailOverlay')\\\">🗑️ Delete</button>\u001b[39m\n\u001b[31m      <button class=\\\"btn btn-orange\\\" onclick=\\\"closeOverlay('detailOverlay');showPane('dashboard')\\\">📊 View in Dashboard</button>\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m  `;\u001b[39m\n\u001b[31m  openOverlay('detailOverlay');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction deleteDispatch(id) {\u001b[39m\n\u001b[31m  if (!confirm('Delete this dispatch?')) return;\u001b[39m\n\u001b[31m  dispatches = dispatches.filter(d => d.id !== id);\u001b[39m\n\u001b[31m  saveData();\u001b[39m\n\u001b[31m  updateHistCount();\u001b[39m\n\u001b[31m  renderHistory();\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction updateHistCount() {\u001b[39m\n\u001b[31m  document.getElementById('histCount').textContent = dispatches.length;\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31m// GOOGLE SHEETS\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31masync function logToSheets(d) {\u001b[39m\n\u001b[31m  const url = window.appConfig.webhook_url;\u001b[39m\n\u001b[31m  if (!url) return;\u001b[39m\n\u001b[31m  try {\u001b[39m\n\u001b[31m    await fetch(url, {\u001b[39m\n\u001b[31m      method: 'POST', mode: 'no-cors',\u001b[39m\n\u001b[31m      headers: { 'Content-Type': 'application/json' },\u001b[39m\n\u001b[31m      body: JSON.stringify({\u001b[39m\n\u001b[31m        type: 'dispatch', id: d.id, client: d.client, jobref: d.jobref,\u001b[39m\n\u001b[31m        courier: d.courier, consignees: d.consignees?.length||0,\u001b[39m\n\u001b[31m        destinations: d.consignees?.map(c=>c.city).join(', ')||'',\u001b[39m\n\u001b[31m        dockets: d.consignees?.filter(c=>c.docket).map(c=>c.docket).join(', ')||'',\u001b[39m\n\u001b[31m        dispatchDate: d.dispatchDate, status: d.status,\u001b[39m\n\u001b[31m        timestamp: new Date().toISOString(), company: window.appConfig.company\u001b[39m\n\u001b[31m      })\u001b[39m\n\u001b[31m    });\u001b[39m\n\u001b[31m  } catch(e) {}\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31m// CONFIG — Google Sheet is the source of truth\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════·\u001b[39m\n\u001b[31mfunction onSheetsUrlChange() {\u001b[39m\n\u001b[31m  const url = document.getElementById('cfg_sheets_url').value.trim();\u001b[39m\n\u001b[31m  // Show sync button state\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction saveSheetsUrl() {\u001b[39m\n\u001b[31m  const url = document.getElementById('cfg_sheets_url').value.trim();\u001b[39m\n\u001b[31m  if (!url) { showToast('Enter the Apps Script URL first', 'err'); return; }\u001b[39m\n\u001b[31m  window.appConfig.sheets_url = url;\u001b[39m\n\u001b[31m  saveSheetsUrlLocal();\u001b[39m\n\u001b[31m  syncFromSheet(false);\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m/**\u001b[39m\n\u001b[31m * Fetch all config from Google Sheet and apply to appConfig.\u001b[39m\n\u001b[31m * Called on startup (silent=true) and manually (silent=false).\u001b[39m\n\u001b[31m */\u001b[39m\n\u001b[31masync function syncFromSheet(silent = false) {\u001b[39m\n\u001b[31m  const url = window.appConfig.sheets_url;\u001b[39m\n\u001b[31m  if (!url) { if (!silent) showToast('No Sheets URL configured', 'err'); return; }·\u001b[39m\n\u001b[31m  setSyncStatus('syncing', 'Syncing config from Google Sheet...');\u001b[39m\n\u001b[31m  try {\u001b[39m\n\u001b[31m    const resp = await fetch(url + '?action=pwa_config', {\u001b[39m\n\u001b[31m      method: 'GET',\u001b[39m\n\u001b[31m      redirect: 'follow'\u001b[39m\n\u001b[31m    });\u001b[39m\n\u001b[31m    if (!resp.ok) throw new Error('HTTP ' + resp.status);\u001b[39m\n\u001b[31m    const cfg = await resp.json();·\u001b[39m\n\u001b[31m    // Apply all values from sheet to appConfig\u001b[39m\n\u001b[31m    const keyMap = {\u001b[39m\n\u001b[31m      proxy_url:           'proxy_url',\u001b[39m\n\u001b[31m      webhook_url:         'webhook_url',\u001b[39m\n\u001b[31m      ollama_url:          'ollama_url',\u001b[39m\n\u001b[31m      ocr_mode:            'ocr_mode',\u001b[39m\n\u001b[31m      company:             'company',\u001b[39m\n\u001b[31m      city:                'city',\u001b[39m\n\u001b[31m      auto_log:            'auto_log',\u001b[39m\n\u001b[31m      refresh_on_load:     'refresh_on_load',\u001b[39m\n\u001b[31m      admin_password:      'admin_password',\u001b[39m\n\u001b[31m      n8n_dispatch_webhook:'n8n_dispatch_webhook',\u001b[39m\n\u001b[31m    };\u001b[39m\n\u001b[31m    Object.entries(keyMap).forEach(([sheetKey, cfgKey]) => {\u001b[39m\n\u001b[31m      if (cfg[sheetKey] !== undefined && cfg[sheetKey] !== '') {\u001b[39m\n\u001b[31m        window.appConfig[cfgKey] = cfg[sheetKey];\u001b[39m\n\u001b[31m      }\u001b[39m\n\u001b[31m    });·\u001b[39m\n\u001b[31m    // Apply courier master if provided\u001b[39m\n\u001b[31m    if (cfg._courier_master && Array.isArray(cfg._courier_master)) {\u001b[39m\n\u001b[31m      cfg._courier_master.forEach(c => {\u001b[39m\n\u001b[31m        if (c.name && c.trackUrl) {\u001b[39m\n\u001b[31m          COURIER_MASTER[c.name] = {\u001b[39m\n\u001b[31m            trackUrl: c.trackUrl,\u001b[39m\n\u001b[31m            trackBulkUrl: c.bulkUrl || '',\u001b[39m\n\u001b[31m            maxBulk: parseInt(c.maxBulk) || 1\u001b[39m\n\u001b[31m          };\u001b[39m\n\u001b[31m        }\u001b[39m\n\u001b[31m      });\u001b[39m\n\u001b[31m    }·\u001b[39m\n\u001b[31m    renderCourierMasterList();\u001b[39m\n\u001b[31m    renderSheetConfigDisplay(cfg);\u001b[39m\n\u001b[31m    setSyncStatus('ok', 'Synced from sheet · ' + new Date().toLocaleTimeString());\u001b[39m\n\u001b[31m    if (!silent) showToast('Config synced from sheet ✓', 'ok');·\u001b[39m\n\u001b[31m    // If refresh_on_load is true, trigger dashboard refresh\u001b[39m\n\u001b[31m    if (window.appConfig.refresh_on_load === 'true') {\u001b[39m\n\u001b[31m      setTimeout(refreshAllTracking, 2000);\u001b[39m\n\u001b[31m    }·\u001b[39m\n\u001b[31m  } catch(e) {\u001b[39m\n\u001b[31m    setSyncStatus('err', 'Sync failed: ' + e.message);\u001b[39m\n\u001b[31m    if (!silent) showToast('Sync failed — check URL', 'err');\u001b[39m\n\u001b[31m    console.error('Sheets sync error:', e);\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction setSyncStatus(state, msg) {\u001b[39m\n\u001b[31m  const el = document.getElementById('syncStatus');\u001b[39m\n\u001b[31m  const icon = document.getElementById('syncIcon');\u001b[39m\n\u001b[31m  const msgEl = document.getElementById('syncMsg');\u001b[39m\n\u001b[31m  el.style.display = 'flex';\u001b[39m\n\u001b[31m  icon.textContent = state === 'ok' ? '✅' : state === 'err' ? '❌' : '⏳';\u001b[39m\n\u001b[31m  msgEl.textContent = msg;\u001b[39m\n\u001b[31m  el.style.borderColor = state === 'ok' ? 'rgba(34,197,94,.3)' : state === 'err' ? 'rgba(239,68,68,.3)' : 'rgba(234,179,8,.3)';\u001b[39m\n\u001b[31m  el.style.background = state === 'ok' ? 'rgba(34,197,94,.05)' : state === 'err' ? 'rgba(239,68,68,.05)' : 'rgba(234,179,8,.05)';\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction renderSheetConfigDisplay(cfg) {\u001b[39m\n\u001b[31m  const display = document.getElementById('sheetConfigDisplay');\u001b[39m\n\u001b[31m  display.style.display = 'block';\u001b[39m\n\u001b[31m  document.getElementById('sheetSyncTime').textContent = 'Last sync: ' + new Date().toLocaleTimeString();\u001b[39m\n\u001b[31m  const showKeys = ['proxy_url','webhook_url','ollama_url','ocr_mode','company','city','auto_log'];\u001b[39m\n\u001b[31m  document.getElementById('sheetConfigRows').innerHTML = showKeys.map(k => {\u001b[39m\n\u001b[31m    const val = cfg[k] || window.appConfig[k] || '—';\u001b[39m\n\u001b[31m    const isEmpty = !cfg[k];\u001b[39m\n\u001b[31m    return `<div class=\\\"courier-master-row\\\">\u001b[39m\n\u001b[31m      <div class=\\\"cml\\\">\u001b[39m\n\u001b[31m        <div class=\\\"cml-name\\\" style=\\\"font-family:var(--fnt-mono);font-size:.75rem;\\\">${k}</div>\u001b[39m\n\u001b[31m        <div class=\\\"cml-url\\\" style=\\\"${isEmpty ? 'color:var(--red);' : ''}\\\">${isEmpty ? '⚠️ Not set in sheet' : val}</div>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m    </div>`;\u001b[39m\n\u001b[31m  }).join('');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m/**\u001b[39m\n\u001b[31m * Push current config changes back to the sheet.\u001b[39m\n\u001b[31m * Called when user edits config from PWA (rare — usually done in sheet directly).\u001b[39m\n\u001b[31m */\u001b[39m\n\u001b[31masync function pushConfigToSheet(updates) {\u001b[39m\n\u001b[31m  const url = window.appConfig.sheets_url;\u001b[39m\n\u001b[31m  if (!url) return;\u001b[39m\n\u001b[31m  try {\u001b[39m\n\u001b[31m    await fetch(url, {\u001b[39m\n\u001b[31m      method: 'POST', mode: 'no-cors',\u001b[39m\n\u001b[31m      headers: { 'Content-Type': 'application/json' },\u001b[39m\n\u001b[31m      body: JSON.stringify({ type: 'save_config', config: updates })\u001b[39m\n\u001b[31m    });\u001b[39m\n\u001b[31m  } catch(e) {}\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction clearAll() {\u001b[39m\n\u001b[31m  if (!confirm('Delete ALL dispatch data?')) return;\u001b[39m\n\u001b[31m  dispatches = [];\u001b[39m\n\u001b[31m  saveData();\u001b[39m\n\u001b[31m  updateHistCount();\u001b[39m\n\u001b[31m  renderDashboard();\u001b[39m\n\u001b[31m  renderHistory();\u001b[39m\n\u001b[31m  showToast('Cleared', 'ok');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction renderCourierMasterList() {\u001b[39m\n\u001b[31m  const el = document.getElementById('courierMasterList');\u001b[39m\n\u001b[31m  el.innerHTML = Object.entries(COURIER_MASTER).map(([name, info]) => `\u001b[39m\n\u001b[31m    <div class=\\\"courier-master-row\\\">\u001b[39m\n\u001b[31m      <div class=\\\"cml\\\">\u001b[39m\n\u001b[31m        <div class=\\\"cml-name\\\">${name}</div>\u001b[39m\n\u001b[31m        <div class=\\\"cml-url\\\">${info.trackUrl}</div>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      <button class=\\\"btn-sm\\\" onclick=\\\"window.open('${info.trackBulkUrl||info.trackUrl.replace('{docket}','')}','_blank')\\\">Open</button>\u001b[39m\n\u001b[31m    </div>`).join('');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction openCourierMaster() {\u001b[39m\n\u001b[31m  const ed = document.getElementById('courierMasterEditor');\u001b[39m\n\u001b[31m  ed.innerHTML = Object.entries(COURIER_MASTER).map(([name, info]) => `\u001b[39m\n\u001b[31m    <div style=\\\"margin-bottom:14px;\\\">\u001b[39m\n\u001b[31m      <div style=\\\"font-family:var(--fnt-head);font-size:.75rem;font-weight:700;color:var(--t2);text-transform:uppercase;letter-spacing:1.5px;margin-bottom:6px;\\\">${name}</div>\u001b[39m\n\u001b[31m      <div class=\\\"f\\\" style=\\\"margin-bottom:6px;\\\">\u001b[39m\n\u001b[31m        <label>Track URL (use {docket})</label>\u001b[39m\n\u001b[31m        <input type=\\\"text\\\" value=\\\"${info.trackUrl}\\\" id=\\\"cm_${name.replace(/\\\\s/g,'_')}\\\"\u001b[39m\n\u001b[31m          style=\\\"font-family:var(--fnt-mono);font-size:.75rem;\\\">\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m    </div>`).join('') +\u001b[39m\n\u001b[31m    `<button class=\\\"btn btn-orange\\\" onclick=\\\"saveCourierMaster()\\\">Save Courier Master</button>`;\u001b[39m\n\u001b[31m  openOverlay('courierMasterOverlay');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction saveCourierMaster() {\u001b[39m\n\u001b[31m  Object.keys(COURIER_MASTER).forEach(name => {\u001b[39m\n\u001b[31m    const el = document.getElementById('cm_' + name.replace(/\\\\s/g,'_'));\u001b[39m\n\u001b[31m    if (el) COURIER_MASTER[name].trackUrl = el.value;\u001b[39m\n\u001b[31m  });\u001b[39m\n\u001b[31m  closeOverlay('courierMasterOverlay');\u001b[39m\n\u001b[31m  renderCourierMasterList();\u001b[39m\n\u001b[31m  showToast('Courier master saved ✓', 'ok');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31m// UI UTILS\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31m// ── ADMIN AUTH ───────────────────────────────────────────────────────────\u001b[39m\n\u001b[31mlet _adminAuthed = false;\u001b[39m\n\u001b[31mlet _lastAdminPane = null;·\u001b[39m\n\u001b[31mfunction showPane(name) {\u001b[39m\n\u001b[31m  // Config tab requires admin auth — re-ask if navigated away\u001b[39m\n\u001b[31m  if (name === 'config') {\u001b[39m\n\u001b[31m    if (!_adminAuthed) {\u001b[39m\n\u001b[31m      _lastAdminPane = 'config';\u001b[39m\n\u001b[31m      showAdminLogin();\u001b[39m\n\u001b[31m      return;\u001b[39m\n\u001b[31m    }\u001b[39m\n\u001b[31m  } else {\u001b[39m\n\u001b[31m    // User navigated away from config — revoke auth\u001b[39m\n\u001b[31m    if (_lastAdminPane === 'config') _adminAuthed = false;\u001b[39m\n\u001b[31m    _lastAdminPane = name;\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m  document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));\u001b[39m\n\u001b[31m  document.querySelectorAll('.pane').forEach(p => p.classList.remove('active'));\u001b[39m\n\u001b[31m  document.getElementById('tab-'+name).classList.add('active');\u001b[39m\n\u001b[31m  document.getElementById('pane-'+name).classList.add('active');\u001b[39m\n\u001b[31m  if (name === 'dashboard') renderDashboard();\u001b[39m\n\u001b[31m  if (name === 'history') renderHistory();\u001b[39m\n\u001b[31m  if (name === 'track') buildTrackDocketChips();\u001b[39m\n\u001b[31m  if (name === 'config') { document.getElementById('cfg_sheets_url').value = window.appConfig.sheets_url || ''; }\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction showAdminLogin() {\u001b[39m\n\u001b[31m  // Password replaced by hub identity — only harish/pramod can access config\u001b[39m\n\u001b[31m  if (['harish', 'pramod'].includes(window._hubUser?.id)) {\u001b[39m\n\u001b[31m    _adminAuthed = true;\u001b[39m\n\u001b[31m    showPane('config');\u001b[39m\n\u001b[31m  } else {\u001b[39m\n\u001b[31m    showToast('Config access restricted to admin users', 'err');\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31masync function submitAdminLogin() {\u001b[39m\n\u001b[31m  showAdminLogin(); // no-op shim — kept for any lingering HTML onclick refs\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31mfunction openOverlay(id) { document.getElementById(id).classList.add('open'); }\u001b[39m\n\u001b[31mfunction closeOverlay(id) { document.getElementById(id).classList.remove('open'); }·\u001b[39m\n\u001b[31mlet toastTimer;\u001b[39m\n\u001b[31mfunction showToast(msg, type='') {\u001b[39m\n\u001b[31m  const t = document.getElementById('toast');\u001b[39m\n\u001b[31m  t.textContent = msg;\u001b[39m\n\u001b[31m  t.className = 'toast show' + (type ? ' '+type : '');\u001b[39m\n\u001b[31m  clearTimeout(toastTimer);\u001b[39m\n\u001b[31m  toastTimer = setTimeout(() => t.classList.remove('show'), 2800);\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction showOCRWorking(show, msg='') {\u001b[39m\n\u001b[31m  const el = document.getElementById('ocrWorking');\u001b[39m\n\u001b[31m  el.classList.toggle('visible', show);\u001b[39m\n\u001b[31m  if (msg) document.getElementById('ocrWorkingText').textContent = msg;\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction showAIProgress(label, pct) {\u001b[39m\n\u001b[31m  const el = document.getElementById('aiProgress');\u001b[39m\n\u001b[31m  el.classList.add('visible');\u001b[39m\n\u001b[31m  document.getElementById('aiProgLabel').textContent = label;\u001b[39m\n\u001b[31m  document.getElementById('aiProgFill').style.width = pct + '%';\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31mfunction hideAIProgress() {\u001b[39m\n\u001b[31m  document.getElementById('aiProgress').classList.remove('visible');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction updateNetDot() {\u001b[39m\n\u001b[31m  document.getElementById('netDot').style.background = navigator.onLine ? 'var(--grn)' : 'var(--red)';\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction setToday() {\u001b[39m\n\u001b[31m  document.getElementById('n_date').value = new Date().toISOString().split('T')[0];\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m// ── EXPORT CLEAN SHEET ───────────────────────────────────────────────────\u001b[39m\n\u001b[31mfunction exportCleanSheet() {\u001b[39m\n\u001b[31m  const pane = document.getElementById('verifyPane');\u001b[39m\n\u001b[31m  const d = pane._pendingDispatch;\u001b[39m\n\u001b[31m  if (!d || !d.consignees?.length) { showToast('No data to export', 'err'); return; }·\u001b[39m\n\u001b[31m  // Build CSV\u001b[39m\n\u001b[31m  const headers = ['S.No.','Store Name','City','Pincode','Phone','Address','Docket No.','Material','Courier','Status'];\u001b[39m\n\u001b[31m  const rows = d.consignees.map((c, i) => [\u001b[39m\n\u001b[31m    i + 1,\u001b[39m\n\u001b[31m    `\\\"${(c.name||'').replace(/\\\"/g,'\\\"\\\"')}\\\"`,\u001b[39m\n\u001b[31m    `\\\"${(c.city||'').replace(/\\\"/g,'\\\"\\\"')}\\\"`,\u001b[39m\n\u001b[31m    c.pincode || '',\u001b[39m\n\u001b[31m    c.phone || '',\u001b[39m\n\u001b[31m    `\\\"${(c.address||'').replace(/\\\"/g,'\\\"\\\"')}\\\"`,\u001b[39m\n\u001b[31m    c.docket || '',\u001b[39m\n\u001b[31m    `\\\"${(c.material||'').replace(/\\\"/g,'\\\"\\\"')}\\\"`,\u001b[39m\n\u001b[31m    d.courier || '',\u001b[39m\n\u001b[31m    c.status || 'pending'\u001b[39m\n\u001b[31m  ]);·\u001b[39m\n\u001b[31m  const csv = [headers.join(','), ...rows.map(r => r.join(','))].join('\\\\n');\u001b[39m\n\u001b[31m  const blob = new Blob([csv], { type: 'text/csv' });\u001b[39m\n\u001b[31m  const url = URL.createObjectURL(blob);\u001b[39m\n\u001b[31m  const a = document.createElement('a');\u001b[39m\n\u001b[31m  a.href = url;\u001b[39m\n\u001b[31m  a.download = `360LM_Dispatch_${d.client||'export'}_${d.dispatchDate||new Date().toISOString().slice(0,10)}_clean.csv`;\u001b[39m\n\u001b[31m  a.click();\u001b[39m\n\u001b[31m  URL.revokeObjectURL(url);\u001b[39m\n\u001b[31m  showToast('Clean sheet exported ✓', 'ok');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// ── DOCKET SLIP UPLOAD — SKIP OCR IF DOCKET ALREADY MAPPED ───────────────\u001b[39m\n\u001b[31mfunction shouldSkipOCR() {\u001b[39m\n\u001b[31m  // If any consignee already has a docket (from store list mapping), skip OCR extraction\u001b[39m\n\u001b[31m  const consList = document.getElementById('consList');\u001b[39m\n\u001b[31m  if (!consList) return false;\u001b[39m\n\u001b[31m  const cards = consList.querySelectorAll('.cons-card');\u001b[39m\n\u001b[31m  let hasAnyDocket = false;\u001b[39m\n\u001b[31m  cards.forEach(card => {\u001b[39m\n\u001b[31m    const d = card.querySelector('.cons-docket-input')?.value?.trim();\u001b[39m\n\u001b[31m    if (d) hasAnyDocket = true;\u001b[39m\n\u001b[31m  });\u001b[39m\n\u001b[31m  return hasAnyDocket;\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// ─── TRACKING UPDATE TO SHEETS ──────────────────────────────────────────\u001b[39m\n\u001b[31masync function logTrackingUpdate(docket, status, lastEvent, location) {\u001b[39m\n\u001b[31m  const url = window.appConfig.webhook_url;\u001b[39m\n\u001b[31m  if (!url) return;\u001b[39m\n\u001b[31m  try {\u001b[39m\n\u001b[31m    await fetch(url, {\u001b[39m\n\u001b[31m      method: 'POST', mode: 'no-cors',\u001b[39m\n\u001b[31m      headers: { 'Content-Type': 'application/json' },\u001b[39m\n\u001b[31m      body: JSON.stringify({\u001b[39m\n\u001b[31m        type: 'tracking_update',\u001b[39m\n\u001b[31m        docket, status, lastEvent, location,\u001b[39m\n\u001b[31m        timestamp: new Date().toISOString()\u001b[39m\n\u001b[31m      })\u001b[39m\n\u001b[31m    });\u001b[39m\n\u001b[31m  } catch(e) {}\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// ─── POD / RECIPIENT PHOTO UPLOAD ───────────────────────────────────────\u001b[39m\n\u001b[31m// Stores base64 locally, pushes to Sheets webhook (Apps Script saves to Drive)\u001b[39m\n\u001b[31mfunction showPODUpload(docket, dispatchId) {\u001b[39m\n\u001b[31m  const overlay = document.getElementById('podOverlay');\u001b[39m\n\u001b[31m  overlay.dataset.docket = docket;\u001b[39m\n\u001b[31m  overlay.dataset.dispatchId = dispatchId;\u001b[39m\n\u001b[31m  document.getElementById('podPreview').innerHTML = '';\u001b[39m\n\u001b[31m  document.getElementById('podFileInput').value = '';\u001b[39m\n\u001b[31m  document.getElementById('recipientFileInput').value = '';\u001b[39m\n\u001b[31m  openOverlay('podOverlay');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31masync function uploadPODFiles() {\u001b[39m\n\u001b[31m  const overlay = document.getElementById('podOverlay');\u001b[39m\n\u001b[31m  const docket = overlay.dataset.docket;\u001b[39m\n\u001b[31m  const dispatchId = overlay.dataset.dispatchId;\u001b[39m\n\u001b[31m  const podFile = document.getElementById('podFileInput').files[0];\u001b[39m\n\u001b[31m  const recipFile = document.getElementById('recipientFileInput').files[0];\u001b[39m\n\u001b[31m  if (!podFile && !recipFile) { showToast('Select at least one photo', 'err'); return; }·\u001b[39m\n\u001b[31m  showToast('Uploading...', 'info');\u001b[39m\n\u001b[31m  const toBase64 = f => new Promise((res,rej) => {\u001b[39m\n\u001b[31m    const r = new FileReader(); r.onload = e => res(e.target.result); r.onerror = rej; r.readAsDataURL(f);\u001b[39m\n\u001b[31m  });·\u001b[39m\n\u001b[31m  const payload = {\u001b[39m\n\u001b[31m    type: 'pod_upload',\u001b[39m\n\u001b[31m    docket, dispatchId,\u001b[39m\n\u001b[31m    timestamp: new Date().toISOString()\u001b[39m\n\u001b[31m  };\u001b[39m\n\u001b[31m  if (podFile)   payload.pod_image = await toBase64(podFile);\u001b[39m\n\u001b[31m  if (recipFile) payload.recipient_image = await toBase64(recipFile);·\u001b[39m\n\u001b[31m  // Update local store\u001b[39m\n\u001b[31m  for (const d of dispatches) {\u001b[39m\n\u001b[31m    if (d.id !== dispatchId) continue;\u001b[39m\n\u001b[31m    for (const c of d.consignees) {\u001b[39m\n\u001b[31m      if (c.docket === docket) {\u001b[39m\n\u001b[31m        c.podUploaded = true;\u001b[39m\n\u001b[31m        c.status = 'delivered';\u001b[39m\n\u001b[31m      }\u001b[39m\n\u001b[31m    }\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m  saveData();·\u001b[39m\n\u001b[31m  // Push to Sheets (Apps Script saves images to Drive, stores URL in sheet)\u001b[39m\n\u001b[31m  if (window.appConfig.webhook_url) {\u001b[39m\n\u001b[31m    try {\u001b[39m\n\u001b[31m      await fetch(window.appConfig.webhook_url, {\u001b[39m\n\u001b[31m        method: 'POST', mode: 'no-cors',\u001b[39m\n\u001b[31m        headers: { 'Content-Type': 'application/json' },\u001b[39m\n\u001b[31m        body: JSON.stringify(payload)\u001b[39m\n\u001b[31m      });\u001b[39m\n\u001b[31m    } catch(e) {}\u001b[39m\n\u001b[31m  }·\u001b[39m\n\u001b[31m  closeOverlay('podOverlay');\u001b[39m\n\u001b[31m  renderDashboard();\u001b[39m\n\u001b[31m  showToast('POD uploaded ✓ — marked as delivered', 'ok');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// ─── WHATSAPP DISPATCH TABLE SHARE ──────────────────────────────────────\u001b[39m\n\u001b[31mfunction shareDispatchToWhatsApp(dispatchId) {\u001b[39m\n\u001b[31m  const d = dispatches.find(x => x.id === dispatchId);\u001b[39m\n\u001b[31m  if (!d) return;\u001b[39m\n\u001b[31m  const lines = [\u001b[39m\n\u001b[31m    `📦 *Dispatch Update — ${d.client} ${d.jobref ? '› '+d.jobref : ''}*`,\u001b[39m\n\u001b[31m    `🚚 Courier: ${d.courier} | Date: ${d.dispatchDate || d.createdAt?.slice(0,10)}`,\u001b[39m\n\u001b[31m    ``,\u001b[39m\n\u001b[31m    ...d.consignees.filter(c => c.docket).map((c,i) =>\u001b[39m\n\u001b[31m      `${i+1}. *${c.name}* — ${c.city}\\\\n   Docket: ${c.docket} | Status: ${c.status||'booked'}`\u001b[39m\n\u001b[31m    ),\u001b[39m\n\u001b[31m    ``,\u001b[39m\n\u001b[31m    `_360 Degree Logical Marketing_`\u001b[39m\n\u001b[31m  ];\u001b[39m\n\u001b[31m  const msg = encodeURIComponent(lines.join('\\\\n'));\u001b[39m\n\u001b[31m  window.open('https://wa.me/?text=' + msg, '_blank');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// Trigger n8n webhook for automated WhatsApp/email share\u001b[39m\n\u001b[31masync function triggerN8nShare(dispatchId, channel) {\u001b[39m\n\u001b[31m  const d = dispatches.find(x => x.id === dispatchId);\u001b[39m\n\u001b[31m  if (!d) return;\u001b[39m\n\u001b[31m  const n8nUrl = window.appConfig.n8n_dispatch_webhook;\u001b[39m\n\u001b[31m  if (!n8nUrl) { showToast('n8n webhook not configured in Config sheet', 'err'); return; }\u001b[39m\n\u001b[31m  try {\u001b[39m\n\u001b[31m    await fetch(n8nUrl, {\u001b[39m\n\u001b[31m      method: 'POST',\u001b[39m\n\u001b[31m      headers: { 'Content-Type': 'application/json' },\u001b[39m\n\u001b[31m      body: JSON.stringify({\u001b[39m\n\u001b[31m        channel, // 'whatsapp' or 'email'\u001b[39m\n\u001b[31m        dispatch: d,\u001b[39m\n\u001b[31m        timestamp: new Date().toISOString()\u001b[39m\n\u001b[31m      })\u001b[39m\n\u001b[31m    });\u001b[39m\n\u001b[31m    showToast(`Sent to n8n for ${channel} delivery ✓`, 'ok');\u001b[39m\n\u001b[31m  } catch(e) {\u001b[39m\n\u001b[31m    showToast('n8n webhook failed', 'err');\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction previewPOD(input, type) {\u001b[39m\n\u001b[31m  const f = input.files[0];\u001b[39m\n\u001b[31m  if (!f) return;\u001b[39m\n\u001b[31m  const r = new FileReader();\u001b[39m\n\u001b[31m  r.onload = e => {\u001b[39m\n\u001b[31m    const prev = document.getElementById('podPreview');\u001b[39m\n\u001b[31m    const existing = document.getElementById('pod_prev_' + type);\u001b[39m\n\u001b[31m    if (existing) existing.remove();\u001b[39m\n\u001b[31m    const img = document.createElement('img');\u001b[39m\n\u001b[31m    img.id = 'pod_prev_' + type;\u001b[39m\n\u001b[31m    img.src = e.target.result;\u001b[39m\n\u001b[31m    img.style.cssText = 'width:80px;height:80px;object-fit:cover;border-radius:6px;border:1px solid var(--bdr);';\u001b[39m\n\u001b[31m    prev.appendChild(img);\u001b[39m\n\u001b[31m  };\u001b[39m\n\u001b[31m  r.readAsDataURL(f);\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// ─── MANUAL TRACKING ─────────────────────────────────────────────────────\u001b[39m\n\u001b[31mfunction showManualTracking(docket, dispatchId, storeName, phone, city) {\u001b[39m\n\u001b[31m  const ov = document.getElementById('manualTrackOverlay');\u001b[39m\n\u001b[31m  ov.dataset.docket = docket;\u001b[39m\n\u001b[31m  ov.dataset.dispatchId = dispatchId;\u001b[39m\n\u001b[31m  ov.dataset.storeName = storeName;\u001b[39m\n\u001b[31m  ov.dataset.city = city;·\u001b[39m\n\u001b[31m  document.getElementById('mtDocketDisplay').textContent =\u001b[39m\n\u001b[31m    docket + '  ·  ' + storeName + '  ·  ' + city;\u001b[39m\n\u001b[31m  document.getElementById('mtContactUsed').value = phone || '';\u001b[39m\n\u001b[31m  document.getElementById('mtContactPerson').value = '';\u001b[39m\n\u001b[31m  document.getElementById('mtRemarks').value = '';\u001b[39m\n\u001b[31m  document.getElementById('mtStatus').value = '';\u001b[39m\n\u001b[31m  document.getElementById('mtCalledBy').value = '';·\u001b[39m\n\u001b[31m  // Set current datetime\u001b[39m\n\u001b[31m  const now = new Date();\u001b[39m\n\u001b[31m  const pad = n => String(n).padStart(2,'0');\u001b[39m\n\u001b[31m  document.getElementById('mtCallTime').value =\u001b[39m\n\u001b[31m    `${now.getFullYear()}-${pad(now.getMonth()+1)}-${pad(now.getDate())}T${pad(now.getHours())}:${pad(now.getMinutes())}`;·\u001b[39m\n\u001b[31m  openOverlay('manualTrackOverlay');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31masync function submitManualTracking() {\u001b[39m\n\u001b[31m  const ov = document.getElementById('manualTrackOverlay');\u001b[39m\n\u001b[31m  const docket     = ov.dataset.docket;\u001b[39m\n\u001b[31m  const dispatchId = ov.dataset.dispatchId;\u001b[39m\n\u001b[31m  const status     = document.getElementById('mtStatus').value;\u001b[39m\n\u001b[31m  const calledBy   = document.getElementById('mtCalledBy').value.trim();·\u001b[39m\n\u001b[31m  if (!status)    { showToast('Select a status', 'err'); return; }\u001b[39m\n\u001b[31m  if (!calledBy)  { showToast('Enter your name', 'err'); return; }·\u001b[39m\n\u001b[31m  const payload = {\u001b[39m\n\u001b[31m    type:          'manual_tracking',\u001b[39m\n\u001b[31m    docket,        dispatchId,\u001b[39m\n\u001b[31m    storeName:     ov.dataset.storeName,\u001b[39m\n\u001b[31m    city:          ov.dataset.city,\u001b[39m\n\u001b[31m    manualStatus:  status,\u001b[39m\n\u001b[31m    calledBy,\u001b[39m\n\u001b[31m    contactUsed:   document.getElementById('mtContactUsed').value.trim(),\u001b[39m\n\u001b[31m    contactPerson: document.getElementById('mtContactPerson').value.trim(),\u001b[39m\n\u001b[31m    remarks:       document.getElementById('mtRemarks').value.trim(),\u001b[39m\n\u001b[31m    callTime:      document.getElementById('mtCallTime').value || new Date().toISOString(),\u001b[39m\n\u001b[31m    timestamp:     new Date().toISOString()\u001b[39m\n\u001b[31m  };·\u001b[39m\n\u001b[31m  // Update local dispatch record\u001b[39m\n\u001b[31m  for (const d of dispatches) {\u001b[39m\n\u001b[31m    if (d.id !== dispatchId) continue;\u001b[39m\n\u001b[31m    for (const c of d.consignees) {\u001b[39m\n\u001b[31m      if (c.docket === docket) {\u001b[39m\n\u001b[31m        c.manualStatus    = status;\u001b[39m\n\u001b[31m        c.manualCalledBy  = calledBy;\u001b[39m\n\u001b[31m        c.manualTime      = payload.callTime;\u001b[39m\n\u001b[31m        c.manualRemarks   = payload.remarks;\u001b[39m\n\u001b[31m        // If confirmed delivered — flag so dashboard shows manual badge\u001b[39m\n\u001b[31m        // (not permanently locked — use resumeAutoTracking() to re-enable)\u001b[39m\n\u001b[31m        if (status === 'delivered') {\u001b[39m\n\u001b[31m          c.status = 'delivered';\u001b[39m\n\u001b[31m          c.manualDelivered = true;\u001b[39m\n\u001b[31m        } else {\u001b[39m\n\u001b[31m          // Non-delivered manual update: clear any previous lock\u001b[39m\n\u001b[31m          c.manualDelivered = false;\u001b[39m\n\u001b[31m        }\u001b[39m\n\u001b[31m      }\u001b[39m\n\u001b[31m    }\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m  saveData();\u001b[39m\n\u001b[31m  renderDashboard();\u001b[39m\n\u001b[31m  // Sync updated consignee to Postgres\u001b[39m\n\u001b[31m  const _d = dispatches.find(x => x.id === dispatchId);\u001b[39m\n\u001b[31m  if (_d) {\u001b[39m\n\u001b[31m    const _idx = _d.consignees.findIndex(c => c.docket === docket);\u001b[39m\n\u001b[31m    if (_idx >= 0) syncConsigneeToDB(_d.consignees[_idx], dispatchId, _idx);\u001b[39m\n\u001b[31m  }·\u001b[39m\n\u001b[31m  // Log to Sheets\u001b[39m\n\u001b[31m  if (window.appConfig.webhook_url) {\u001b[39m\n\u001b[31m    try {\u001b[39m\n\u001b[31m      await fetch(window.appConfig.webhook_url, {\u001b[39m\n\u001b[31m        method: 'POST', mode: 'no-cors',\u001b[39m\n\u001b[31m        headers: { 'Content-Type': 'application/json' },\u001b[39m\n\u001b[31m        body: JSON.stringify(payload)\u001b[39m\n\u001b[31m      });\u001b[39m\n\u001b[31m    } catch(e) {}\u001b[39m\n\u001b[31m  }·\u001b[39m\n\u001b[31m  closeOverlay('manualTrackOverlay');\u001b[39m\n\u001b[31m  showToast('Manual tracking logged ✓', 'ok');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// ─── TILE SELECTION + PARTIAL REPORT SHARE ───────────────────────────────\u001b[39m\n\u001b[31mlet _selectedDockets = new Set();·\u001b[39m\n\u001b[31mfunction toggleTileSelect(docket) {\u001b[39m\n\u001b[31m  if (_selectedDockets.has(docket)) _selectedDockets.delete(docket);\u001b[39m\n\u001b[31m  else _selectedDockets.add(docket);\u001b[39m\n\u001b[31m  // Update checkbox visual\u001b[39m\n\u001b[31m  const chk = document.getElementById('sel_' + docket.replace(/[^a-z0-9]/gi,'_'));\u001b[39m\n\u001b[31m  if (chk) chk.checked = _selectedDockets.has(docket);\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction shareSelectedReport() {\u001b[39m\n\u001b[31m  if (_selectedDockets.size === 0) {\u001b[39m\n\u001b[31m    showToast('Select at least one consignee tile first', 'err');\u001b[39m\n\u001b[31m    return;\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m  const activeDis = dispatches.filter(d => d.status !== 'draft');\u001b[39m\n\u001b[31m  const allItems  = activeDis.flatMap(d =>\u001b[39m\n\u001b[31m    (d.consignees||[]).map(c => Object.assign({}, c, {courier:d.courier, client:d.client, jobref:d.jobref, dispatchId:d.id}))\u001b[39m\n\u001b[31m  );\u001b[39m\n\u001b[31m  const selected = allItems.filter(c => _selectedDockets.has(c.docket));\u001b[39m\n\u001b[31m  if (!selected.length) { showToast('Selected dockets not found', 'err'); return; }·\u001b[39m\n\u001b[31m  const NL = '\\\\n';\u001b[39m\n\u001b[31m  const statusEmoji = {\u001b[39m\n\u001b[31m    delivered:'\\\\u2705', transit:'\\\\uD83D\\\\uDE9A', 'in-transit':'\\\\uD83D\\\\uDE9A',\u001b[39m\n\u001b[31m    booked:'\\\\uD83D\\\\uDCEC', pending:'\\\\u23F3', 'not received':'\\\\u274C', forwarded:'\\\\u27A1'\u001b[39m\n\u001b[31m  };·\u001b[39m\n\u001b[31m  const header = '\\\\uD83D\\\\uDCE6 *Dispatch Status Report*' + NL +\u001b[39m\n\u001b[31m    (selected[0].client || '') + ' ' + (selected[0].jobref ? '> ' + selected[0].jobref : '') + NL +\u001b[39m\n\u001b[31m    new Date().toLocaleDateString('en-IN', {day:'2-digit',month:'short',year:'numeric'}) + NL + NL;·\u001b[39m\n\u001b[31m  const rows = selected.map(function(c, i) {\u001b[39m\n\u001b[31m    const finalStatus = c.manualStatus === 'delivered' ? 'delivered' : (c.status || 'booked');\u001b[39m\n\u001b[31m    const emoji  = statusEmoji[finalStatus] || '\\\\uD83D\\\\uDCE6';\u001b[39m\n\u001b[31m    const source = c.manualStatus ? '(manual)' : '(auto)';\u001b[39m\n\u001b[31m    const remark = c.manualRemarks ? (NL + '   Note: ' + c.manualRemarks) : '';\u001b[39m\n\u001b[31m    return (i+1) + '. *' + c.name + '* - ' + c.city + NL +\u001b[39m\n\u001b[31m      '   ' + emoji + ' ' + finalStatus + ' ' + source + NL +\u001b[39m\n\u001b[31m      '   Docket: ' + c.docket + remark;\u001b[39m\n\u001b[31m  }).join(NL + NL);·\u001b[39m\n\u001b[31m  const footer = NL + NL + '_360 Degree Logical Marketing_';\u001b[39m\n\u001b[31m  window.open('https://wa.me/?text=' + encodeURIComponent(header + rows + footer), '_blank');\u001b[39m\n\u001b[31m  showToast('Report for ' + selected.length + ' consignees ready', 'ok');\u001b[39m\n\u001b[31m}··\u001b[39m\n\u001b[31m// ── resumeAutoTracking: undo a manual-delivered lock ──────────────────────\u001b[39m\n\u001b[31mfunction resumeAutoTracking(docket, dispatchId) {\u001b[39m\n\u001b[31m  for (const d of dispatches) {\u001b[39m\n\u001b[31m    if (d.id !== dispatchId) continue;\u001b[39m\n\u001b[31m    for (const c of d.consignees) {\u001b[39m\n\u001b[31m      if (c.docket === docket) {\u001b[39m\n\u001b[31m        c.manualDelivered = false;\u001b[39m\n\u001b[31m        c.status          = 'transit';\u001b[39m\n\u001b[31m        c.manualStatus    = '';\u001b[39m\n\u001b[31m      }\u001b[39m\n\u001b[31m    }\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m  saveData();\u001b[39m\n\u001b[31m  renderDashboard();\u001b[39m\n\u001b[31m  showToast(`Auto-tracking resumed for ${docket}`, 'ok');\u001b[39m\n\u001b[31m  const _rd = dispatches.find(x => x.id === dispatchId);\u001b[39m\n\u001b[31m  if (_rd) {\u001b[39m\n\u001b[31m    const _ri = _rd.consignees.findIndex(c => c.docket === docket);\u001b[39m\n\u001b[31m    if (_ri >= 0) syncConsigneeToDB(_rd.consignees[_ri], dispatchId, _ri);\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// ── refreshAllTracking (consolidated, try/finally for safety) ─────────────\u001b[39m\n\u001b[31masync function refreshAllTracking() {\u001b[39m\n\u001b[31m  const activeDis  = dispatches.filter(d => d.status !== 'draft');\u001b[39m\n\u001b[31m  const allCons    = activeDis.flatMap(d =>\u001b[39m\n\u001b[31m    (d.consignees || []).map(c => ({ ...c, dispatchId: d.id, courier: d.courier }))\u001b[39m\n\u001b[31m  );\u001b[39m\n\u001b[31m  const withDockets = allCons.filter(c =>\u001b[39m\n\u001b[31m    c.docket && c.status !== 'delivered' && !c.manualDelivered\u001b[39m\n\u001b[31m  );\u001b[39m\n\u001b[31m  if (!withDockets.length) { showToast('No active dockets to refresh', 'info'); return; }·\u001b[39m\n\u001b[31m  showToast(`⏳ Tracking ${withDockets.length} dockets...`, 'info');\u001b[39m\n\u001b[31m  withDockets.forEach(c => {\u001b[39m\n\u001b[31m    const id    = c.docket.replace(/[^a-z0-9]/gi, '_');\u001b[39m\n\u001b[31m    const evtEl = document.getElementById('evt_'  + id);\u001b[39m\n\u001b[31m    const spnEl = document.getElementById('spin_' + id);\u001b[39m\n\u001b[31m    if (evtEl) evtEl.style.display = 'none';\u001b[39m\n\u001b[31m    if (spnEl) spnEl.style.display = 'inline';\u001b[39m\n\u001b[31m  });·\u001b[39m\n\u001b[31m  let done = 0;\u001b[39m\n\u001b[31m  try {\u001b[39m\n\u001b[31m    for (const c of withDockets) {\u001b[39m\n\u001b[31m      const result = await fetchTracking(c.docket, c.courier || '');\u001b[39m\n\u001b[31m      const id    = c.docket.replace(/[^a-z0-9]/gi, '_');·\u001b[39m\n\u001b[31m      // Batch-write result into state (never touch DOM here)\u001b[39m\n\u001b[31m      if (result.status && result.status !== 'failed') {\u001b[39m\n\u001b[31m        for (const d of dispatches) {\u001b[39m\n\u001b[31m          if (d.id !== c.dispatchId) continue;\u001b[39m\n\u001b[31m          for (const con of d.consignees) {\u001b[39m\n\u001b[31m            if (con.docket === c.docket && !con.manualDelivered) {\u001b[39m\n\u001b[31m              con.status           = result.status;\u001b[39m\n\u001b[31m              con.lastEvent        = result.lastEvent || con.lastEvent;\u001b[39m\n\u001b[31m              con.lastScanLocation = result.lastScanLocation || '';\u001b[39m\n\u001b[31m              con.lastTrackedAt    = new Date().toISOString();\u001b[39m\n\u001b[31m            }\u001b[39m\n\u001b[31m          }\u001b[39m\n\u001b[31m        }\u001b[39m\n\u001b[31m      }·\u001b[39m\n\u001b[31m      // Update tile spinner/event text in DOM (cosmetic only)\u001b[39m\n\u001b[31m      const spnEl = document.getElementById('spin_' + id);\u001b[39m\n\u001b[31m      const evtEl = document.getElementById('evt_'  + id);\u001b[39m\n\u001b[31m      if (spnEl) spnEl.style.display = 'none';\u001b[39m\n\u001b[31m      if (evtEl) { evtEl.style.display = 'inline'; evtEl.textContent = result.lastEvent || 'Checked'; }·\u001b[39m\n\u001b[31m      if (result.status && result.status !== 'failed' && window.appConfig.webhook_url) {\u001b[39m\n\u001b[31m        logTrackingUpdate(c.docket, result.status, result.lastEvent || '', result.lastScanLocation || '');\u001b[39m\n\u001b[31m      }\u001b[39m\n\u001b[31m      done++;\u001b[39m\n\u001b[31m    }\u001b[39m\n\u001b[31m    showToast(`✓ ${done} dockets refreshed`, 'ok');\u001b[39m\n\u001b[31m  } finally {\u001b[39m\n\u001b[31m    // Always persist and re-render — even if loop throws mid-way\u001b[39m\n\u001b[31m    saveData();\u001b[39m\n\u001b[31m    renderDashboard();\u001b[39m\n\u001b[31m    // Push updated consignee states to Postgres (fire-and-forget)\u001b[39m\n\u001b[31m    dispatches.filter(d => d.status !== 'draft').forEach(d => syncDispatchToDB(d));\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m</script>\u001b[39m\n\u001b[31m</body>\u001b[39m\n\u001b[31m</html>\"\u001b[39m\n\n  51 |\n  52 |   test('hub auth gate present', () => {\n> 53 |     expect(html).toContain(\"window.location.replace('/hub/')\");\n     |                  ^\n  54 |     expect(html).toContain('_hubUser');\n  55 |   });\n  56 | });\n    at /var/www/360lm/tests/dispatch.spec.js:53:18"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 1,
                      "startTime": "2026-07-12T12:57:26.923Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/dispatch-Dispatch-—-HTML-Structure-hub-auth-gate-present-android-chrome-retry1/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/dispatch.spec.js",
                        "column": 18,
                        "line": 53
                      }
                    }
                  ],
                  "status": "unexpected"
                }
              ],
              "id": "3510226c3ce1e611dacb-5405793d42ef9b57804f",
              "file": "dispatch.spec.js",
              "line": 52,
              "column": 3
            },
            {
              "title": "all panes present",
              "ok": true,
              "tags": [],
              "tests": [
                {
                  "timeout": 30000,
                  "annotations": [],
                  "expectedStatus": "passed",
                  "projectId": "desktop-chrome",
                  "projectName": "desktop-chrome",
                  "results": [
                    {
                      "workerIndex": 176,
                      "parallelIndex": 0,
                      "status": "passed",
                      "duration": 7,
                      "errors": [],
                      "stdout": [],
                      "stderr": [],
                      "retry": 0,
                      "startTime": "2026-07-12T13:17:41.385Z",
                      "annotations": [],
                      "attachments": []
                    }
                  ],
                  "status": "expected"
                }
              ],
              "id": "3510226c3ce1e611dacb-713d9f2b949293fae3a1",
              "file": "dispatch.spec.js",
              "line": 39,
              "column": 3
            },
            {
              "title": "hub navigation button present",
              "ok": true,
              "tags": [],
              "tests": [
                {
                  "timeout": 30000,
                  "annotations": [],
                  "expectedStatus": "passed",
                  "projectId": "desktop-chrome",
                  "projectName": "desktop-chrome",
                  "results": [
                    {
                      "workerIndex": 176,
                      "parallelIndex": 0,
                      "status": "passed",
                      "duration": 3,
                      "errors": [],
                      "stdout": [],
                      "stderr": [],
                      "retry": 0,
                      "startTime": "2026-07-12T13:17:41.433Z",
                      "annotations": [],
                      "attachments": []
                    }
                  ],
                  "status": "expected"
                }
              ],
              "id": "3510226c3ce1e611dacb-eaf7c31f791ad5c872d7",
              "file": "dispatch.spec.js",
              "line": 47,
              "column": 3
            },
            {
              "title": "hub auth gate present",
              "ok": false,
              "tags": [],
              "tests": [
                {
                  "timeout": 30000,
                  "annotations": [],
                  "expectedStatus": "passed",
                  "projectId": "desktop-chrome",
                  "projectName": "desktop-chrome",
                  "results": [
                    {
                      "workerIndex": 176,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 29,
                      "error": {
                        "message": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoContain\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m) // indexOf\u001b[22m\n\nExpected substring: \u001b[32m\"window.location.replace('/hub/')\"\u001b[39m\nReceived string:    \u001b[31m\"<!DOCTYPE html>\u001b[39m\n\u001b[31m<html lang=\\\"en\\\">\u001b[39m\n\u001b[31m<head>\u001b[39m\n\u001b[31m<meta charset=\\\"UTF-8\\\">\u001b[39m\n\u001b[31m<meta name=\\\"viewport\\\" content=\\\"width=device-width,initial-scale=1,maximum-scale=1,user-scalable=no\\\">\u001b[39m\n\u001b[31m<meta name=\\\"theme-color\\\" content=\\\"#080c12\\\">\u001b[39m\n\u001b[31m<meta name=\\\"apple-mobile-web-app-capable\\\" content=\\\"yes\\\">\u001b[39m\n\u001b[31m<meta name=\\\"mobile-web-app-capable\\\" content=\\\"yes\\\">\u001b[39m\n\u001b[31m<title>360LM Dispatch</title>\u001b[39m\n\u001b[31m<link rel=\\\"manifest\\\" href=\\\"manifest.json\\\">\u001b[39m\n\u001b[31m<link href=\\\"https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@300;400;500;600;700&family=Fira+Code:wght@400;500&family=Barlow+Condensed:wght@400;600;700;800&display=swap\\\" rel=\\\"stylesheet\\\">\u001b[39m\n\u001b[31m<style>\u001b[39m\n\u001b[31m/* ════ RESET & VARS ════ */\u001b[39m\n\u001b[31m*{margin:0;padding:0;box-sizing:border-box;-webkit-tap-highlight-color:transparent;}\u001b[39m\n\u001b[31m:root{\u001b[39m\n\u001b[31m  --bg:#080c12; --bg2:#0d1420; --bg3:#111a28;\u001b[39m\n\u001b[31m  --sur:#141f30; --sur2:#1a2840; --sur3:#1f3050;\u001b[39m\n\u001b[31m  --bdr:#1e2e45; --bdr2:#253650;\u001b[39m\n\u001b[31m  --t1:#f0f4f8; --t2:#8ba4bc; --t3:#3d5470; --t4:#243348;\u001b[39m\n\u001b[31m  --acc:#f97316; --acc2:#fb923c; --acc-glow:rgba(249,115,22,0.25);\u001b[39m\n\u001b[31m  --grn:#22c55e; --grn-dim:rgba(34,197,94,0.15);\u001b[39m\n\u001b[31m  --sky:#38bdf8; --sky-dim:rgba(56,189,248,0.15);\u001b[39m\n\u001b[31m  --red:#ef4444; --red-dim:rgba(239,68,68,0.15);\u001b[39m\n\u001b[31m  --ylw:#eab308; --ylw-dim:rgba(234,179,8,0.15);\u001b[39m\n\u001b[31m  --fnt-head:'Barlow Condensed',sans-serif;\u001b[39m\n\u001b[31m  --fnt-body:'Space Grotesk',sans-serif;\u001b[39m\n\u001b[31m  --fnt-mono:'Fira Code',monospace;\u001b[39m\n\u001b[31m  --r:10px; --r-sm:6px; --r-lg:16px;\u001b[39m\n\u001b[31m  --sh:0 4px 24px rgba(0,0,0,0.4);\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31mhtml,body{height:100%;background:var(--bg);color:var(--t1);font-family:var(--fnt-body);overflow:hidden;}·\u001b[39m\n\u001b[31m/* ════ SPLASH ════ */\u001b[39m\n\u001b[31m#splash{\u001b[39m\n\u001b[31m  position:fixed;inset:0;background:var(--bg);z-index:999;\u001b[39m\n\u001b[31m  display:flex;flex-direction:column;align-items:center;justify-content:center;\u001b[39m\n\u001b[31m  transition:opacity .4s .1s;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.splash-logo{font-family:var(--fnt-head);font-size:3rem;font-weight:800;letter-spacing:2px;line-height:1;}\u001b[39m\n\u001b[31m.splash-logo em{color:var(--acc);font-style:normal;}\u001b[39m\n\u001b[31m.splash-sub{color:var(--t3);font-family:var(--fnt-mono);font-size:.7rem;letter-spacing:4px;margin-top:8px;}\u001b[39m\n\u001b[31m.splash-track{width:200px;height:2px;background:var(--bdr);border-radius:2px;margin-top:40px;overflow:hidden;}\u001b[39m\n\u001b[31m.splash-track-fill{height:100%;width:0;background:linear-gradient(90deg,var(--acc),var(--acc2));animation:splashLoad 1.3s cubic-bezier(.4,0,.2,1) forwards;}\u001b[39m\n\u001b[31m@keyframes splashLoad{to{width:100%;}}·\u001b[39m\n\u001b[31m/* ════ LAYOUT ════ */\u001b[39m\n\u001b[31m#app{height:100%;display:flex;flex-direction:column;}\u001b[39m\n\u001b[31m.hdr{\u001b[39m\n\u001b[31m  background:var(--bg2);border-bottom:1px solid var(--bdr);\u001b[39m\n\u001b[31m  padding:10px 14px;display:flex;align-items:center;gap:10px;flex-shrink:0;\u001b[39m\n\u001b[31m  position:relative;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.hdr-brand{font-family:var(--fnt-head);font-size:1.3rem;font-weight:800;letter-spacing:1px;flex:1;}\u001b[39m\n\u001b[31m.hdr-brand em{color:var(--acc);font-style:normal;}\u001b[39m\n\u001b[31m.hdr-dot{width:7px;height:7px;border-radius:50%;background:var(--grn);transition:background .3s;animation:blink 2s infinite;}\u001b[39m\n\u001b[31m@keyframes blink{0%,100%{opacity:1;}50%{opacity:.4;}}\u001b[39m\n\u001b[31m.hdr-btn{\u001b[39m\n\u001b[31m  background:none;border:none;color:var(--t2);cursor:pointer;\u001b[39m\n\u001b[31m  padding:6px 8px;border-radius:var(--r-sm);transition:all .2s;\u001b[39m\n\u001b[31m  font-family:var(--fnt-head);font-size:.85rem;font-weight:700;letter-spacing:.5px;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.hdr-btn:hover{background:var(--bdr);color:var(--t1);}\u001b[39m\n\u001b[31m.hdr-btn.primary{background:var(--acc);color:#fff;padding:6px 14px;}\u001b[39m\n\u001b[31m.hdr-btn.primary:hover{background:var(--acc2);}·\u001b[39m\n\u001b[31m/* ════ TAB BAR ════ */\u001b[39m\n\u001b[31m.tabs{\u001b[39m\n\u001b[31m  display:flex;background:var(--bg2);border-bottom:1px solid var(--bdr);\u001b[39m\n\u001b[31m  overflow-x:auto;flex-shrink:0;padding:0 6px;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.tabs::-webkit-scrollbar{display:none;}\u001b[39m\n\u001b[31m.tab{\u001b[39m\n\u001b[31m  background:none;border:none;color:var(--t3);font-family:var(--fnt-head);\u001b[39m\n\u001b[31m  font-size:.85rem;font-weight:700;letter-spacing:1px;padding:10px 14px;\u001b[39m\n\u001b[31m  cursor:pointer;border-bottom:2px solid transparent;white-space:nowrap;\u001b[39m\n\u001b[31m  transition:all .2s;flex-shrink:0;display:flex;align-items:center;gap:6px;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.tab.active{color:var(--acc);border-bottom-color:var(--acc);}\u001b[39m\n\u001b[31m.tab:hover:not(.active){color:var(--t2);}\u001b[39m\n\u001b[31m.tab-badge{\u001b[39m\n\u001b[31m  background:var(--acc);color:#fff;font-size:.6rem;font-weight:700;\u001b[39m\n\u001b[31m  padding:1px 5px;border-radius:10px;font-family:var(--fnt-mono);\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m/* ════ CONTENT AREAS ════ */\u001b[39m\n\u001b[31m.pane{flex:1;overflow-y:auto;padding:14px;display:none;}\u001b[39m\n\u001b[31m.pane.active{display:block;}\u001b[39m\n\u001b[31m.pane::-webkit-scrollbar{width:3px;}\u001b[39m\n\u001b[31m.pane::-webkit-scrollbar-thumb{background:var(--bdr2);}·\u001b[39m\n\u001b[31m/* ════ CARDS ════ */\u001b[39m\n\u001b[31m.card{\u001b[39m\n\u001b[31m  background:var(--sur);border:1px solid var(--bdr);border-radius:var(--r);\u001b[39m\n\u001b[31m  padding:14px;margin-bottom:12px;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.card.accent-left{border-left:3px solid var(--acc);}\u001b[39m\n\u001b[31m.card-title{\u001b[39m\n\u001b[31m  font-family:var(--fnt-head);font-size:.72rem;font-weight:700;\u001b[39m\n\u001b[31m  color:var(--t3);text-transform:uppercase;letter-spacing:2.5px;\u001b[39m\n\u001b[31m  margin-bottom:12px;display:flex;align-items:center;gap:8px;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.card-title .ct-dot{width:5px;height:5px;border-radius:50%;background:var(--acc);flex-shrink:0;}·\u001b[39m\n\u001b[31m/* ════ FORM ════ */\u001b[39m\n\u001b[31m.f{margin-bottom:12px;}\u001b[39m\n\u001b[31m.f label{\u001b[39m\n\u001b[31m  display:block;font-size:.68rem;color:var(--t3);margin-bottom:5px;\u001b[39m\n\u001b[31m  font-weight:600;text-transform:uppercase;letter-spacing:1.5px;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.f label .req{color:var(--acc);margin-left:2px;}\u001b[39m\n\u001b[31m.f input,.f select,.f textarea{\u001b[39m\n\u001b[31m  width:100%;background:var(--sur2);border:1px solid var(--bdr);\u001b[39m\n\u001b[31m  border-radius:var(--r-sm);color:var(--t1);font-family:var(--fnt-body);\u001b[39m\n\u001b[31m  font-size:.88rem;padding:9px 11px;outline:none;transition:border-color .2s;\u001b[39m\n\u001b[31m  -webkit-appearance:none;appearance:none;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.f input:focus,.f select:focus,.f textarea:focus{border-color:var(--acc);}\u001b[39m\n\u001b[31m.f input::placeholder{color:var(--t4);}\u001b[39m\n\u001b[31m.f textarea{resize:vertical;min-height:65px;}\u001b[39m\n\u001b[31m.f select{\u001b[39m\n\u001b[31m  background-image:url(\\\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='7'%3E%3Cpath d='M1 1l4 4 4-4' stroke='%233d5470' stroke-width='1.5' fill='none' stroke-linecap='round'/%3E%3C/svg%3E\\\");\u001b[39m\n\u001b[31m  background-repeat:no-repeat;background-position:right 10px center;padding-right:28px;cursor:pointer;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.row2{display:grid;grid-template-columns:1fr 1fr;gap:10px;}\u001b[39m\n\u001b[31m.row3{display:grid;grid-template-columns:1fr 1fr 1fr;gap:8px;}·\u001b[39m\n\u001b[31m/* ════ BUTTONS ════ */\u001b[39m\n\u001b[31m.btn{\u001b[39m\n\u001b[31m  width:100%;padding:12px;border:none;border-radius:var(--r);\u001b[39m\n\u001b[31m  font-family:var(--fnt-head);font-size:.9rem;font-weight:700;letter-spacing:.5px;\u001b[39m\n\u001b[31m  cursor:pointer;transition:all .2s;display:flex;align-items:center;justify-content:center;gap:8px;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.btn-orange{background:var(--acc);color:#fff;}\u001b[39m\n\u001b[31m.btn-orange:hover{background:var(--acc2);}\u001b[39m\n\u001b[31m.btn-ghost{background:var(--sur2);border:1px solid var(--bdr);color:var(--t1);}\u001b[39m\n\u001b[31m.btn-ghost:hover{border-color:var(--acc);color:var(--acc);}\u001b[39m\n\u001b[31m.btn-grn{background:var(--grn);color:#000;}\u001b[39m\n\u001b[31m.btn-grn:hover{opacity:.85;}\u001b[39m\n\u001b[31m.btn:active{transform:scale(.98);}\u001b[39m\n\u001b[31m.btn-row{display:grid;grid-template-columns:1fr 1fr;gap:10px;margin-top:6px;}\u001b[39m\n\u001b[31m.btn-sm{\u001b[39m\n\u001b[31m  background:none;border:1px solid var(--bdr);color:var(--t2);\u001b[39m\n\u001b[31m  font-size:.7rem;padding:5px 10px;border-radius:var(--r-sm);cursor:pointer;\u001b[39m\n\u001b[31m  font-family:var(--fnt-body);transition:all .2s;white-space:nowrap;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.btn-sm:hover{border-color:var(--acc);color:var(--acc);}\u001b[39m\n\u001b[31m.btn-sm.primary{background:var(--acc);border-color:var(--acc);color:#fff;}·\u001b[39m\n\u001b[31m/* ════ OCR UPLOAD ZONE ════ */\u001b[39m\n\u001b[31m.upload-zone{\u001b[39m\n\u001b[31m  border:2px dashed var(--bdr2);border-radius:var(--r);\u001b[39m\n\u001b[31m  padding:20px;text-align:center;cursor:pointer;transition:all .25s;\u001b[39m\n\u001b[31m  background:var(--sur2);position:relative;overflow:hidden;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.upload-zone:hover,.upload-zone.drag{border-color:var(--acc);background:rgba(249,115,22,.05);}\u001b[39m\n\u001b[31m.upload-zone .uz-icon{font-size:2rem;margin-bottom:8px;}\u001b[39m\n\u001b[31m.upload-zone .uz-title{font-family:var(--fnt-head);font-size:1rem;font-weight:700;color:var(--t1);}\u001b[39m\n\u001b[31m.upload-zone .uz-sub{font-size:.72rem;color:var(--t3);margin-top:4px;font-family:var(--fnt-mono);}\u001b[39m\n\u001b[31m.upload-zone input{display:none;}\u001b[39m\n\u001b[31m.upload-zone.share-target{border-color:var(--grn);}·\u001b[39m\n\u001b[31m/* ════ OCR RESULTS ════ */\u001b[39m\n\u001b[31m.ocr-results{display:none;}\u001b[39m\n\u001b[31m.ocr-results.visible{display:block;}\u001b[39m\n\u001b[31m.ocr-slip-list{display:flex;flex-direction:column;gap:8px;margin-top:10px;}\u001b[39m\n\u001b[31m.ocr-slip{\u001b[39m\n\u001b[31m  background:var(--sur2);border:1px solid var(--bdr);border-radius:var(--r-sm);\u001b[39m\n\u001b[31m  padding:10px 12px;display:flex;gap:10px;align-items:flex-start;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.ocr-slip img{width:60px;height:60px;object-fit:cover;border-radius:6px;flex-shrink:0;border:1px solid var(--bdr2);}\u001b[39m\n\u001b[31m.ocr-slip-body{flex:1;min-width:0;}\u001b[39m\n\u001b[31m.ocr-slip-name{font-size:.7rem;color:var(--t3);font-family:var(--fnt-mono);margin-bottom:4px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}\u001b[39m\n\u001b[31m.ocr-slip-docket{\u001b[39m\n\u001b[31m  font-family:var(--fnt-mono);font-size:.9rem;font-weight:500;color:var(--acc);\u001b[39m\n\u001b[31m  background:var(--bg);border:1px solid var(--bdr);border-radius:4px;\u001b[39m\n\u001b[31m  padding:4px 8px;width:100%;outline:none;transition:border-color .2s;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.ocr-slip-docket:focus{border-color:var(--acc);}\u001b[39m\n\u001b[31m.ocr-slip-status{font-size:.65rem;margin-top:4px;display:flex;align-items:center;gap:6px;}\u001b[39m\n\u001b[31m.ocr-confidence{padding:2px 6px;border-radius:10px;font-family:var(--fnt-mono);}\u001b[39m\n\u001b[31m.conf-high{background:var(--grn-dim);color:var(--grn);}\u001b[39m\n\u001b[31m.conf-med{background:var(--ylw-dim);color:var(--ylw);}\u001b[39m\n\u001b[31m.conf-low{background:var(--red-dim);color:var(--red);}\u001b[39m\n\u001b[31m.ocr-ai-btn{\u001b[39m\n\u001b[31m  background:rgba(56,189,248,.15);border:1px solid rgba(56,189,248,.3);\u001b[39m\n\u001b[31m  color:var(--sky);font-size:.65rem;padding:3px 8px;border-radius:10px;\u001b[39m\n\u001b[31m  cursor:pointer;font-family:var(--fnt-mono);transition:all .2s;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.ocr-ai-btn:hover{background:rgba(56,189,248,.3);}\u001b[39m\n\u001b[31m.ocr-working{\u001b[39m\n\u001b[31m  background:var(--ylw-dim);border:1px solid rgba(234,179,8,.3);\u001b[39m\n\u001b[31m  border-radius:var(--r-sm);padding:8px 12px;margin-top:8px;\u001b[39m\n\u001b[31m  font-size:.75rem;color:var(--ylw);font-family:var(--fnt-mono);\u001b[39m\n\u001b[31m  display:none;align-items:center;gap:8px;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.ocr-working.visible{display:flex;}\u001b[39m\n\u001b[31m.spin{animation:spin .8s linear infinite;display:inline-block;}\u001b[39m\n\u001b[31m@keyframes spin{to{transform:rotate(360deg);}}·\u001b[39m\n\u001b[31m/* ════ STORE LIST ════ */\u001b[39m\n\u001b[31m.store-upload-zone{\u001b[39m\n\u001b[31m  border:2px dashed var(--bdr2);border-radius:var(--r);padding:16px;\u001b[39m\n\u001b[31m  text-align:center;cursor:pointer;transition:all .25s;background:var(--sur2);\u001b[39m\n\u001b[31m  position:relative;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.store-upload-zone:hover{border-color:var(--sky);background:rgba(56,189,248,.05);}\u001b[39m\n\u001b[31m.store-upload-zone input{display:none;}\u001b[39m\n\u001b[31m.store-table-wrap{max-height:200px;overflow-y:auto;margin-top:10px;border-radius:var(--r-sm);overflow:hidden;}\u001b[39m\n\u001b[31m.store-table{width:100%;border-collapse:collapse;font-size:.75rem;}\u001b[39m\n\u001b[31m.store-table th{\u001b[39m\n\u001b[31m  background:var(--bg);color:var(--t3);font-family:var(--fnt-head);\u001b[39m\n\u001b[31m  font-size:.65rem;letter-spacing:1.5px;text-transform:uppercase;\u001b[39m\n\u001b[31m  padding:7px 8px;text-align:left;border-bottom:1px solid var(--bdr);\u001b[39m\n\u001b[31m  position:sticky;top:0;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.store-table td{\u001b[39m\n\u001b[31m  padding:6px 8px;border-bottom:1px solid var(--bdr);color:var(--t2);\u001b[39m\n\u001b[31m  font-family:var(--fnt-mono);font-size:.72rem;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.store-table tr:hover td{background:var(--sur3);}\u001b[39m\n\u001b[31m.store-table .matched{color:var(--grn);}\u001b[39m\n\u001b[31m.store-table .unmatched{color:var(--ylw);}\u001b[39m\n\u001b[31m.store-map-btn{\u001b[39m\n\u001b[31m  font-size:.65rem;background:none;border:1px solid var(--bdr);color:var(--t3);\u001b[39m\n\u001b[31m  padding:2px 6px;border-radius:4px;cursor:pointer;transition:all .2s;\u001b[39m\n\u001b[31m  font-family:var(--fnt-mono);\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.store-map-btn:hover{border-color:var(--acc);color:var(--acc);}·\u001b[39m\n\u001b[31m/* ════ CONSIGNEE CARDS ════ */\u001b[39m\n\u001b[31m.cons-list{display:flex;flex-direction:column;gap:8px;margin-bottom:10px;}\u001b[39m\n\u001b[31m.cons-card{\u001b[39m\n\u001b[31m  background:var(--sur2);border:1px solid var(--bdr);border-radius:var(--r-sm);\u001b[39m\n\u001b[31m  padding:11px;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.cons-hdr{display:flex;justify-content:space-between;align-items:center;margin-bottom:8px;}\u001b[39m\n\u001b[31m.cons-num{font-family:var(--fnt-mono);font-size:.65rem;color:var(--t3);}\u001b[39m\n\u001b[31m.cons-del{\u001b[39m\n\u001b[31m  background:none;border:none;color:var(--t4);cursor:pointer;\u001b[39m\n\u001b[31m  font-size:.85rem;padding:2px 5px;border-radius:4px;transition:all .2s;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.cons-del:hover{background:var(--red-dim);color:var(--red);}\u001b[39m\n\u001b[31m.cons-docket-linked{\u001b[39m\n\u001b[31m  display:flex;align-items:center;gap:6px;\u001b[39m\n\u001b[31m  background:rgba(249,115,22,.1);border:1px solid rgba(249,115,22,.3);\u001b[39m\n\u001b[31m  border-radius:4px;padding:4px 8px;margin-top:6px;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.cons-docket-linked .dkt{font-family:var(--fnt-mono);font-size:.82rem;color:var(--acc);}\u001b[39m\n\u001b[31m.cons-docket-linked .unlink{background:none;border:none;color:var(--acc);cursor:pointer;font-size:.75rem;}·\u001b[39m\n\u001b[31m/* ════ DISPATCH DASHBOARD ════ */\u001b[39m\n\u001b[31m.dash-stats{\u001b[39m\n\u001b[31m  display:grid;grid-template-columns:repeat(4,1fr);gap:8px;margin-bottom:14px;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.stat-box{\u001b[39m\n\u001b[31m  background:var(--sur);border:1px solid var(--bdr);border-radius:var(--r);\u001b[39m\n\u001b[31m  padding:10px 8px;text-align:center;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.stat-val{font-family:var(--fnt-head);font-size:1.4rem;font-weight:800;}\u001b[39m\n\u001b[31m.stat-lbl{font-size:.6rem;color:var(--t3);font-family:var(--fnt-mono);text-transform:uppercase;letter-spacing:1px;margin-top:2px;}\u001b[39m\n\u001b[31m.stat-box.grn .stat-val{color:var(--grn);}\u001b[39m\n\u001b[31m.stat-box.ylw .stat-val{color:var(--ylw);}\u001b[39m\n\u001b[31m.stat-box.red .stat-val{color:var(--red);}\u001b[39m\n\u001b[31m.stat-box.sky .stat-val{color:var(--sky);}·\u001b[39m\n\u001b[31m/* ════ DOCKET TILES ════ */\u001b[39m\n\u001b[31m.docket-grid{display:flex;flex-direction:column;gap:8px;}\u001b[39m\n\u001b[31m.docket-tile{\u001b[39m\n\u001b[31m  background:var(--sur);border:1px solid var(--bdr);border-radius:var(--r);\u001b[39m\n\u001b[31m  padding:12px;transition:border-color .2s;cursor:pointer;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.docket-tile:hover{border-color:var(--acc);}\u001b[39m\n\u001b[31m.docket-tile-hdr{display:flex;justify-content:space-between;align-items:flex-start;margin-bottom:8px;}\u001b[39m\n\u001b[31m.docket-tile-left{flex:1;}\u001b[39m\n\u001b[31m.docket-num{font-family:var(--fnt-mono);font-size:.95rem;font-weight:500;color:var(--sky);letter-spacing:1px;}\u001b[39m\n\u001b[31m.docket-store{font-size:.82rem;color:var(--t1);font-weight:600;margin-top:2px;}\u001b[39m\n\u001b[31m.docket-courier{font-size:.68rem;color:var(--t3);font-family:var(--fnt-mono);margin-top:2px;}\u001b[39m\n\u001b[31m.status-pill{\u001b[39m\n\u001b[31m  font-size:.62rem;font-weight:700;font-family:var(--fnt-mono);\u001b[39m\n\u001b[31m  padding:3px 8px;border-radius:10px;text-transform:uppercase;letter-spacing:.5px;\u001b[39m\n\u001b[31m  flex-shrink:0;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.sp-delivered{background:var(--grn-dim);color:var(--grn);}\u001b[39m\n\u001b[31m.sp-transit{background:var(--ylw-dim);color:var(--ylw);}\u001b[39m\n\u001b[31m.sp-booked{background:var(--sky-dim);color:var(--sky);}\u001b[39m\n\u001b[31m.sp-pending{background:var(--bdr);color:var(--t3);}\u001b[39m\n\u001b[31m.sp-failed{background:var(--red-dim);color:var(--red);}\u001b[39m\n\u001b[31m.docket-tile-footer{display:flex;align-items:center;gap:8px;flex-wrap:wrap;}\u001b[39m\n\u001b[31m.docket-last-event{font-size:.7rem;color:var(--t3);flex:1;font-family:var(--fnt-mono);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}\u001b[39m\n\u001b[31m.btn-track-live{\u001b[39m\n\u001b[31m  background:none;border:1px solid var(--acc);color:var(--acc);\u001b[39m\n\u001b[31m  font-size:.65rem;padding:4px 10px;border-radius:20px;cursor:pointer;\u001b[39m\n\u001b[31m  font-family:var(--fnt-mono);transition:all .2s;white-space:nowrap;display:flex;align-items:center;gap:4px;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.btn-track-live:hover{background:var(--acc);color:#fff;}\u001b[39m\n\u001b[31m.btn-track-live .live-dot{width:5px;height:5px;border-radius:50%;background:var(--grn);animation:blink 1.5s infinite;flex-shrink:0;}\u001b[39m\n\u001b[31m.docket-progress{height:2px;background:var(--bdr);border-radius:2px;margin-top:8px;overflow:hidden;}\u001b[39m\n\u001b[31m.docket-progress-fill{height:100%;border-radius:2px;transition:width .5s;}·\u001b[39m\n\u001b[31m/* ════ TRACKING DETAIL ════ */\u001b[39m\n\u001b[31m.track-timeline{padding:4px 0;}\u001b[39m\n\u001b[31m.track-evt{display:flex;gap:10px;padding:6px 0;position:relative;padding-left:20px;}\u001b[39m\n\u001b[31m.track-evt::before{\u001b[39m\n\u001b[31m  content:'';position:absolute;left:5px;top:11px;\u001b[39m\n\u001b[31m  width:7px;height:7px;border-radius:50%;background:var(--bdr2);border:1px solid var(--bdr2);\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.track-evt:first-child::before{background:var(--grn);border-color:var(--grn);}\u001b[39m\n\u001b[31m.track-evt::after{\u001b[39m\n\u001b[31m  content:'';position:absolute;left:8px;top:18px;\u001b[39m\n\u001b[31m  width:1px;height:calc(100% - 6px);background:var(--bdr);\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.track-evt:last-child::after{display:none;}\u001b[39m\n\u001b[31m.track-time{font-family:var(--fnt-mono);font-size:.62rem;color:var(--t3);min-width:90px;flex-shrink:0;padding-top:2px;}\u001b[39m\n\u001b[31m.track-desc{font-size:.78rem;color:var(--t2);}\u001b[39m\n\u001b[31m.track-loc{font-size:.67rem;color:var(--t3);font-family:var(--fnt-mono);margin-top:2px;}·\u001b[39m\n\u001b[31m/* ════ SHARE TOAST ════ */\u001b[39m\n\u001b[31m.share-banner{\u001b[39m\n\u001b[31m  position:fixed;top:0;left:0;right:0;z-index:200;\u001b[39m\n\u001b[31m  background:rgba(34,197,94,.15);border-bottom:1px solid rgba(34,197,94,.4);\u001b[39m\n\u001b[31m  padding:10px 16px;font-size:.78rem;color:var(--grn);font-family:var(--fnt-mono);\u001b[39m\n\u001b[31m  display:flex;align-items:center;gap:8px;\u001b[39m\n\u001b[31m  transform:translateY(-100%);transition:transform .3s;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.share-banner.show{transform:translateY(0);}·\u001b[39m\n\u001b[31m/* ════ MODAL ════ */\u001b[39m\n\u001b[31m.overlay{position:fixed;inset:0;background:rgba(0,0,0,.75);z-index:100;display:flex;align-items:flex-end;opacity:0;pointer-events:none;transition:opacity .3s;}\u001b[39m\n\u001b[31m.overlay.open{opacity:1;pointer-events:all;}\u001b[39m\n\u001b[31m.sheet{\u001b[39m\n\u001b[31m  background:var(--sur);border-radius:var(--r-lg) var(--r-lg) 0 0;\u001b[39m\n\u001b[31m  width:100%;max-height:88vh;overflow-y:auto;padding:18px;\u001b[39m\n\u001b[31m  transform:translateY(100%);transition:transform .3s cubic-bezier(.32,.72,0,1);\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.overlay.open .sheet{transform:translateY(0);}\u001b[39m\n\u001b[31m.sheet-handle{width:36px;height:4px;background:var(--bdr2);border-radius:2px;margin:0 auto 18px;}\u001b[39m\n\u001b[31m.sheet-title{font-family:var(--fnt-head);font-size:1.2rem;font-weight:800;letter-spacing:.5px;margin-bottom:14px;}\u001b[39m\n\u001b[31m.sheet::-webkit-scrollbar{width:3px;}\u001b[39m\n\u001b[31m.sheet::-webkit-scrollbar-thumb{background:var(--bdr2);}·\u001b[39m\n\u001b[31m/* ════ TOAST ════ */\u001b[39m\n\u001b[31m.toast{\u001b[39m\n\u001b[31m  position:fixed;bottom:20px;left:50%;transform:translateX(-50%) translateY(70px);\u001b[39m\n\u001b[31m  background:var(--sur2);border:1px solid var(--bdr2);color:var(--t1);\u001b[39m\n\u001b[31m  font-size:.78rem;padding:9px 16px;border-radius:30px;z-index:300;\u001b[39m\n\u001b[31m  transition:transform .3s;white-space:nowrap;font-family:var(--fnt-mono);\u001b[39m\n\u001b[31m  pointer-events:none;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.toast.show{transform:translateX(-50%) translateY(0);}\u001b[39m\n\u001b[31m.toast.ok{border-color:var(--grn);color:var(--grn);}\u001b[39m\n\u001b[31m.toast.err{border-color:var(--red);color:var(--red);}\u001b[39m\n\u001b[31m.toast.info{border-color:var(--sky);color:var(--sky);}·\u001b[39m\n\u001b[31m/* ════ COURIER MASTER ════ */\u001b[39m\n\u001b[31m.courier-master-row{\u001b[39m\n\u001b[31m  display:flex;align-items:center;gap:8px;padding:8px 0;\u001b[39m\n\u001b[31m  border-bottom:1px solid var(--bdr);\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.courier-master-row:last-child{border-bottom:none;}\u001b[39m\n\u001b[31m.cml{flex:1;}\u001b[39m\n\u001b[31m.cml-name{font-weight:600;font-size:.85rem;}\u001b[39m\n\u001b[31m.cml-url{font-family:var(--fnt-mono);font-size:.65rem;color:var(--t3);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:180px;}\u001b[39m\n\u001b[31m.courier-logo{width:24px;height:24px;border-radius:4px;object-fit:contain;background:var(--sur3);padding:2px;}·\u001b[39m\n\u001b[31m/* ════ PROGRESS BAR ════ */\u001b[39m\n\u001b[31m.ai-progress{\u001b[39m\n\u001b[31m  background:var(--sur2);border:1px solid var(--bdr);border-radius:var(--r-sm);\u001b[39m\n\u001b[31m  padding:10px 12px;margin-top:8px;display:none;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.ai-progress.visible{display:block;}\u001b[39m\n\u001b[31m.ai-prog-bar{height:3px;background:var(--bdr);border-radius:3px;margin-top:6px;overflow:hidden;}\u001b[39m\n\u001b[31m.ai-prog-fill{height:100%;background:linear-gradient(90deg,var(--sky),var(--acc));width:0;transition:width .3s;}\u001b[39m\n\u001b[31m.ai-prog-label{font-size:.68rem;color:var(--sky);font-family:var(--fnt-mono);}·\u001b[39m\n\u001b[31m/* ════ EMPTY ════ */\u001b[39m\n\u001b[31m.empty{padding:40px 20px;text-align:center;color:var(--t4);}\u001b[39m\n\u001b[31m.empty .e-icon{font-size:2.5rem;margin-bottom:10px;}\u001b[39m\n\u001b[31m.empty p{font-size:.82rem;}·\u001b[39m\n\u001b[31m/* ════ INFO TAG ════ */\u001b[39m\n\u001b[31m.info-tag{\u001b[39m\n\u001b[31m  background:var(--sur2);border:1px solid var(--bdr);border-radius:var(--r-sm);\u001b[39m\n\u001b[31m  padding:7px 10px;font-size:.75rem;color:var(--t2);margin-bottom:8px;\u001b[39m\n\u001b[31m  display:flex;align-items:center;gap:8px;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.info-tag strong{color:var(--t1);}·\u001b[39m\n\u001b[31m/* ════ JOB PILL ════ */\u001b[39m\n\u001b[31m.job-pill{\u001b[39m\n\u001b[31m  display:inline-flex;align-items:center;gap:6px;\u001b[39m\n\u001b[31m  background:rgba(249,115,22,.1);border:1px solid rgba(249,115,22,.3);\u001b[39m\n\u001b[31m  color:var(--acc);font-family:var(--fnt-mono);font-size:.68rem;\u001b[39m\n\u001b[31m  padding:3px 9px;border-radius:20px;margin-top:4px;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.job-pill button{background:none;border:none;color:var(--acc);cursor:pointer;font-size:.7rem;}·\u001b[39m\n\u001b[31m/* ════ SECTION DIVIDER ════ */\u001b[39m\n\u001b[31m.sec-div{\u001b[39m\n\u001b[31m  display:flex;align-items:center;gap:10px;margin:14px 0 10px;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.sec-div span{font-family:var(--fnt-head);font-size:.7rem;font-weight:700;color:var(--t3);text-transform:uppercase;letter-spacing:2px;white-space:nowrap;}\u001b[39m\n\u001b[31m.sec-div::before,.sec-div::after{content:'';flex:1;height:1px;background:var(--bdr);}·\u001b[39m\n\u001b[31m/* ── MAPPING UI ── */\u001b[39m\n\u001b[31m.map-table{width:100%;border-collapse:collapse;font-size:.78rem;margin:10px 0;}\u001b[39m\n\u001b[31m.map-table th{background:var(--bg);color:var(--t3);font-family:var(--fnt-head);font-size:.65rem;\u001b[39m\n\u001b[31m  letter-spacing:1.5px;text-transform:uppercase;padding:7px 10px;text-align:left;\u001b[39m\n\u001b[31m  border-bottom:1px solid var(--bdr);}\u001b[39m\n\u001b[31m.map-table td{padding:7px 10px;border-bottom:1px solid var(--bdr);vertical-align:middle;}\u001b[39m\n\u001b[31m.map-table tr:last-child td{border-bottom:none;}\u001b[39m\n\u001b[31m.map-table select{background:var(--sur2);border:1px solid var(--bdr);color:var(--t1);\u001b[39m\n\u001b[31m  font-family:var(--fnt-mono);font-size:.75rem;padding:5px 8px;border-radius:var(--r-sm);\u001b[39m\n\u001b[31m  width:100%;outline:none;}\u001b[39m\n\u001b[31m.map-table select:focus{border-color:var(--acc);}\u001b[39m\n\u001b[31m.map-req{color:var(--acc);margin-left:2px;}\u001b[39m\n\u001b[31m.map-saved-badge{font-size:.6rem;background:rgba(57,255,143,.15);color:var(--accent3,#39ff8f);\u001b[39m\n\u001b[31m  border:1px solid rgba(57,255,143,.3);padding:2px 7px;border-radius:10px;\u001b[39m\n\u001b[31m  font-family:var(--fnt-mono);margin-left:6px;}·\u001b[39m\n\u001b[31m/* ── VERIFICATION SCREEN ── */\u001b[39m\n\u001b[31m#verifyPane{display:none;position:fixed;inset:0;background:var(--bg);z-index:50;\u001b[39m\n\u001b[31m  overflow-y:auto;padding:14px;}\u001b[39m\n\u001b[31m.verify-hdr{display:flex;align-items:center;gap:10px;margin-bottom:14px;\u001b[39m\n\u001b[31m  position:sticky;top:0;background:var(--bg);padding-bottom:10px;\u001b[39m\n\u001b[31m  border-bottom:1px solid var(--bdr);z-index:1;}\u001b[39m\n\u001b[31m.verify-title{font-family:var(--fnt-head);font-size:1.1rem;font-weight:800;flex:1;}\u001b[39m\n\u001b[31m.verify-row{background:var(--sur);border:1px solid var(--bdr);border-radius:var(--r-sm);\u001b[39m\n\u001b[31m  padding:10px 12px;margin-bottom:8px;position:relative;}\u001b[39m\n\u001b[31m.verify-row.has-dupe{border-color:var(--red);background:rgba(239,68,68,.04);}\u001b[39m\n\u001b[31m.verify-row.no-docket{border-color:var(--ylw);background:rgba(234,179,8,.04);}\u001b[39m\n\u001b[31m.verify-row.ok{border-left:3px solid var(--grn);}\u001b[39m\n\u001b[31m.verify-row-hdr{display:flex;align-items:center;gap:8px;margin-bottom:8px;}\u001b[39m\n\u001b[31m.verify-num{font-family:var(--fnt-mono);font-size:.65rem;color:var(--t3);}\u001b[39m\n\u001b[31m.verify-store{font-weight:600;font-size:.9rem;flex:1;}\u001b[39m\n\u001b[31m.verify-flag{font-size:.65rem;font-weight:700;font-family:var(--fnt-mono);\u001b[39m\n\u001b[31m  padding:2px 7px;border-radius:10px;text-transform:uppercase;}\u001b[39m\n\u001b[31m.vf-dupe{background:var(--red-dim);color:var(--red);}\u001b[39m\n\u001b[31m.vf-nodocket{background:var(--ylw-dim);color:var(--ylw);}\u001b[39m\n\u001b[31m.vf-ok{background:var(--grn-dim);color:var(--grn);}\u001b[39m\n\u001b[31m.vf-short{background:rgba(56,189,248,.15);color:var(--sky);}\u001b[39m\n\u001b[31m.verify-fields{display:grid;grid-template-columns:1fr 1fr;gap:8px;margin-bottom:8px;}\u001b[39m\n\u001b[31m.verify-field label{font-size:.62rem;color:var(--t3);text-transform:uppercase;\u001b[39m\n\u001b[31m  letter-spacing:1px;display:block;margin-bottom:3px;font-family:var(--fnt-mono);}\u001b[39m\n\u001b[31m.verify-field input{width:100%;background:var(--sur2);border:1px solid var(--bdr);\u001b[39m\n\u001b[31m  color:var(--t1);font-family:var(--fnt-mono);font-size:.82rem;padding:6px 9px;\u001b[39m\n\u001b[31m  border-radius:var(--r-sm);outline:none;}\u001b[39m\n\u001b[31m.verify-field input:focus{border-color:var(--acc);}\u001b[39m\n\u001b[31m.verify-field input.changed{border-color:var(--ylw);background:rgba(234,179,8,.06);}\u001b[39m\n\u001b[31m.verify-actions{display:flex;gap:6px;align-items:center;flex-wrap:wrap;}\u001b[39m\n\u001b[31m.btn-wa{background:rgba(37,211,102,.15);border:1px solid rgba(37,211,102,.3);\u001b[39m\n\u001b[31m  color:#25d366;font-size:.65rem;padding:4px 10px;border-radius:10px;cursor:pointer;\u001b[39m\n\u001b[31m  font-family:var(--fnt-mono);transition:all .2s;}\u001b[39m\n\u001b[31m.btn-wa:hover{background:rgba(37,211,102,.3);}\u001b[39m\n\u001b[31m.verify-thumb{width:48px;height:48px;object-fit:cover;border-radius:6px;\u001b[39m\n\u001b[31m  border:1px solid var(--bdr);cursor:pointer;flex-shrink:0;}\u001b[39m\n\u001b[31m.verify-summary{background:var(--sur);border:1px solid var(--bdr);border-radius:var(--r);\u001b[39m\n\u001b[31m  padding:12px 14px;margin-bottom:14px;display:flex;gap:16px;flex-wrap:wrap;}\u001b[39m\n\u001b[31m.vs-item{text-align:center;}\u001b[39m\n\u001b[31m.vs-val{font-family:var(--fnt-head);font-size:1.3rem;font-weight:800;}\u001b[39m\n\u001b[31m.vs-lbl{font-size:.6rem;color:var(--t3);font-family:var(--fnt-mono);text-transform:uppercase;\u001b[39m\n\u001b[31m  letter-spacing:.5px;margin-top:2px;}\u001b[39m\n\u001b[31m.ocr-correction-note{font-size:.65rem;color:var(--ylw);font-family:var(--fnt-mono);\u001b[39m\n\u001b[31m  margin-top:3px;}\u001b[39m\n\u001b[31m</style>\u001b[39m\n\u001b[31m</head>\u001b[39m\n\u001b[31m<body>·\u001b[39m\n\u001b[31m<!-- SPLASH -->\u001b[39m\n\u001b[31m<div id=\\\"splash\\\">\u001b[39m\n\u001b[31m  <div class=\\\"splash-logo\\\">360LM <em>DISPATCH</em></div>\u001b[39m\n\u001b[31m  <div class=\\\"splash-sub\\\">Courier · Bus · Field Ops</div>\u001b[39m\n\u001b[31m  <div class=\\\"splash-track\\\"><div class=\\\"splash-track-fill\\\"></div></div>\u001b[39m\n\u001b[31m</div>·\u001b[39m\n\u001b[31m<!-- SHARE BANNER -->\u001b[39m\n\u001b[31m<div class=\\\"share-banner\\\" id=\\\"shareBanner\\\">\u001b[39m\n\u001b[31m  📤 Images received via Share — processing for OCR...\u001b[39m\n\u001b[31m  <button style=\\\"background:none;border:none;color:var(--grn);cursor:pointer;margin-left:auto;\\\" onclick=\\\"dismissShare()\\\">✕</button>\u001b[39m\n\u001b[31m</div>·\u001b[39m\n\u001b[31m<!-- APP -->\u001b[39m\n\u001b[31m<div id=\\\"app\\\" style=\\\"display:none;\\\">·\u001b[39m\n\u001b[31m  <div class=\\\"hdr\\\">\u001b[39m\n\u001b[31m    <div class=\\\"hdr-brand\\\">360LM <em>DISPATCH</em></div>\u001b[39m\n\u001b[31m    <span class=\\\"hdr-dot\\\" id=\\\"netDot\\\"></span>\u001b[39m\n\u001b[31m    <button class=\\\"hdr-btn\\\" onclick=\\\"window.location.href='/hub/'\\\" title=\\\"Hub\\\">🏡</button>\u001b[39m\n\u001b[31m    <button class=\\\"hdr-btn\\\" onclick=\\\"showPane('dashboard')\\\">📊</button>\u001b[39m\n\u001b[31m    <button class=\\\"hdr-btn primary\\\" onclick=\\\"newDispatch()\\\">+ NEW</button>\u001b[39m\n\u001b[31m  </div>·\u001b[39m\n\u001b[31m  <div class=\\\"tabs\\\">\u001b[39m\n\u001b[31m    <button class=\\\"tab active\\\" onclick=\\\"showPane('new')\\\" id=\\\"tab-new\\\">NEW</button>\u001b[39m\n\u001b[31m    <button class=\\\"tab\\\" onclick=\\\"showPane('dashboard')\\\" id=\\\"tab-dashboard\\\">DASHBOARD</button>\u001b[39m\n\u001b[31m    <button class=\\\"tab\\\" onclick=\\\"showPane('track')\\\" id=\\\"tab-track\\\">TRACK</button>\u001b[39m\n\u001b[31m    <button class=\\\"tab\\\" onclick=\\\"showPane('history')\\\" id=\\\"tab-history\\\">HISTORY <span class=\\\"tab-badge\\\" id=\\\"histCount\\\">0</span></button>\u001b[39m\n\u001b[31m    <button class=\\\"tab\\\" onclick=\\\"showPane('config')\\\" id=\\\"tab-config\\\">CONFIG</button>\u001b[39m\n\u001b[31m  </div>·\u001b[39m\n\u001b[31m  <!-- ════════════ NEW DISPATCH ════════════ -->\u001b[39m\n\u001b[31m  <div class=\\\"pane active\\\" id=\\\"pane-new\\\">·\u001b[39m\n\u001b[31m    <!-- 1. JOB REFERENCE -->\u001b[39m\n\u001b[31m    <div class=\\\"card accent-left\\\">\u001b[39m\n\u001b[31m      <div class=\\\"card-title\\\"><span class=\\\"ct-dot\\\"></span>Job Reference</div>\u001b[39m\n\u001b[31m      <div class=\\\"row2\\\">\u001b[39m\n\u001b[31m        <div class=\\\"f\\\">\u001b[39m\n\u001b[31m          <label>Client <span class=\\\"req\\\">*</span></label>\u001b[39m\n\u001b[31m          <select id=\\\"n_client\\\" onchange=\\\"onClientChange()\\\">\u001b[39m\n\u001b[31m            <option value=\\\"\\\">Select...</option>\u001b[39m\n\u001b[31m            <option>Lenovo</option><option>HP</option>\u001b[39m\n\u001b[31m            <option>Philips</option><option>Liebherr</option>\u001b[39m\n\u001b[31m            <option value=\\\"__c\\\">+ Custom</option>\u001b[39m\n\u001b[31m          </select>\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m        <div class=\\\"f\\\">\u001b[39m\n\u001b[31m          <label>Job Ref</label>\u001b[39m\n\u001b[31m          <input type=\\\"text\\\" id=\\\"n_jobref\\\" placeholder=\\\"JOB-042\\\" oninput=\\\"updatePill()\\\">\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      <div class=\\\"f\\\" id=\\\"customClientField\\\" style=\\\"display:none;\\\">\u001b[39m\n\u001b[31m        <label>Custom Client</label>\u001b[39m\n\u001b[31m        <input type=\\\"text\\\" id=\\\"n_customClient\\\" placeholder=\\\"Client name\\\" oninput=\\\"updatePill()\\\">\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      <div id=\\\"jobPill\\\" style=\\\"display:none;\\\" class=\\\"job-pill\\\">\u001b[39m\n\u001b[31m        🔗 <span id=\\\"jobPillTxt\\\"></span>\u001b[39m\n\u001b[31m        <button onclick=\\\"clearJob()\\\">✕</button>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      <div class=\\\"row2\\\" style=\\\"margin-top:10px;\\\">\u001b[39m\n\u001b[31m        <div class=\\\"f\\\" style=\\\"margin-bottom:0;\\\">\u001b[39m\n\u001b[31m          <label>Dispatch Date</label>\u001b[39m\n\u001b[31m          <input type=\\\"date\\\" id=\\\"n_date\\\">\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m        <div class=\\\"f\\\" style=\\\"margin-bottom:0;\\\">\u001b[39m\n\u001b[31m          <label>Courier Company <span class=\\\"req\\\">*</span></label>\u001b[39m\n\u001b[31m          <select id=\\\"n_courier\\\">\u001b[39m\n\u001b[31m            <option value=\\\"\\\">Select...</option>\u001b[39m\n\u001b[31m            <option>SkyKing</option><option>DTDC</option>\u001b[39m\n\u001b[31m            <option>Delhivery</option><option>Blue Dart</option>\u001b[39m\n\u001b[31m            <option>FedEx</option><option>Ecom Express</option>\u001b[39m\n\u001b[31m            <option>India Post</option><option value=\\\"__o\\\">Other</option>\u001b[39m\n\u001b[31m          </select>\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m    </div>·\u001b[39m\n\u001b[31m    <!-- 2. STORE LIST UPLOAD -->\u001b[39m\n\u001b[31m    <div class=\\\"card\\\">\u001b[39m\n\u001b[31m      <div class=\\\"card-title\\\"><span class=\\\"ct-dot\\\" style=\\\"background:var(--sky);\\\"></span>Store / Destination List</div>\u001b[39m\n\u001b[31m      <div class=\\\"store-upload-zone\\\" id=\\\"storeZone\\\" onclick=\\\"document.getElementById('storeFile').click();event.stopPropagation();\\\" style=\\\"cursor:pointer;\\\">\u001b[39m\n\u001b[31m        <input type=\\\"file\\\" id=\\\"storeFile\\\" accept=\\\".xlsx,.xls,.csv\\\" onchange=\\\"handleStoreFile(this)\\\" onclick=\\\"event.stopPropagation();\\\" style=\\\"display:none;\\\">\u001b[39m\n\u001b[31m        <div style=\\\"font-size:1.5rem;margin-bottom:6px;\\\">📋</div>\u001b[39m\n\u001b[31m        <div style=\\\"font-family:var(--fnt-head);font-weight:700;font-size:.9rem;\\\">Upload Store List</div>\u001b[39m\n\u001b[31m        <div style=\\\"font-size:.68rem;color:var(--t3);margin-top:3px;font-family:var(--fnt-mono);\\\">Excel / CSV · stores, cities, contacts</div>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      <div id=\\\"storeTableArea\\\" style=\\\"display:none;\\\">\u001b[39m\n\u001b[31m        <!-- Column detection result -->\u001b[39m\n\u001b[31m        <div id=\\\"storeColInfo\\\" style=\\\"margin-top:10px;display:none;\\\"></div>\u001b[39m\n\u001b[31m        <div style=\\\"display:flex;justify-content:space-between;align-items:center;margin-top:10px;margin-bottom:6px;\\\">\u001b[39m\n\u001b[31m          <span style=\\\"font-family:var(--fnt-mono);font-size:.7rem;color:var(--sky);\\\" id=\\\"storeCount\\\">0 stores loaded</span>\u001b[39m\n\u001b[31m          <button class=\\\"btn-sm\\\" onclick=\\\"clearStores()\\\">Clear</button>\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m        <div class=\\\"store-table-wrap\\\">\u001b[39m\n\u001b[31m          <table class=\\\"store-table\\\" id=\\\"storeTable\\\"></table>\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m        <!-- Inline add-as-consignees UI instead of confirm() -->\u001b[39m\n\u001b[31m        <div id=\\\"storeAddPanel\\\" style=\\\"margin-top:10px;background:var(--sur3);border:1px solid var(--bdr2);border-radius:var(--r-sm);padding:10px 12px;display:none;\\\">\u001b[39m\n\u001b[31m          <div style=\\\"font-size:.75rem;color:var(--t1);margin-bottom:8px;font-weight:600;\\\" id=\\\"storeAddTitle\\\">Add stores as consignees?</div>\u001b[39m\n\u001b[31m          <div class=\\\"row2\\\">\u001b[39m\n\u001b[31m            <button class=\\\"btn btn-ghost\\\" style=\\\"font-size:.75rem;padding:8px;\\\" onclick=\\\"document.getElementById('storeAddPanel').style.display='none'\\\">Skip for now</button>\u001b[39m\n\u001b[31m            <button class=\\\"btn btn-orange\\\" style=\\\"font-size:.75rem;padding:8px;\\\" id=\\\"storeAddBtn\\\" onclick=\\\"addStoresToConsignees()\\\">✓ Add All as Consignees</button>\u001b[39m\n\u001b[31m          </div>\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m        <button class=\\\"btn btn-ghost\\\" style=\\\"margin-top:8px;font-size:.78rem;\\\" onclick=\\\"autoMatchDockets()\\\">\u001b[39m\n\u001b[31m          🔗 Auto-match Dockets to Stores\u001b[39m\n\u001b[31m        </button>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m    </div>·\u001b[39m\n\u001b[31m    <!-- 3. DOCKET SLIPS — AI OCR -->\u001b[39m\n\u001b[31m    <div class=\\\"card\\\">\u001b[39m\n\u001b[31m      <div class=\\\"card-title\\\"><span class=\\\"ct-dot\\\" style=\\\"background:var(--grn);\\\"></span>Docket Slips — AI OCR</div>·\u001b[39m\n\u001b[31m      <!-- Share target indicator -->\u001b[39m\n\u001b[31m      <div class=\\\"info-tag\\\" id=\\\"shareTargetInfo\\\" style=\\\"display:none;\\\">\u001b[39m\n\u001b[31m        <span>📤</span><span>Images shared from WhatsApp — <strong id=\\\"sharedCount\\\">0</strong> slips received</span>\u001b[39m\n\u001b[31m      </div>·\u001b[39m\n\u001b[31m      <label class=\\\"upload-zone\\\" id=\\\"slipZone\\\" for=\\\"slipFiles\\\" style=\\\"display:block;cursor:pointer;\\\">\u001b[39m\n\u001b[31m        <input type=\\\"file\\\" id=\\\"slipFiles\\\" accept=\\\"image/*\\\" multiple onchange=\\\"handleSlipFiles(this.files)\\\" style=\\\"display:none;\\\">\u001b[39m\n\u001b[31m        <div class=\\\"uz-icon\\\">🖼️</div>\u001b[39m\n\u001b[31m        <div class=\\\"uz-title\\\">Upload Courier Slips</div>\u001b[39m\n\u001b[31m        <div class=\\\"uz-sub\\\">Multiple images · Tap or drag · Camera capture supported</div>\u001b[39m\n\u001b[31m      </label>·\u001b[39m\n\u001b[31m      <!-- OCR Working indicator -->\u001b[39m\n\u001b[31m      <div class=\\\"ocr-working\\\" id=\\\"ocrWorking\\\">\u001b[39m\n\u001b[31m        <span class=\\\"spin\\\">⟳</span>\u001b[39m\n\u001b[31m        <span id=\\\"ocrWorkingText\\\">Running OCR...</span>\u001b[39m\n\u001b[31m      </div>·\u001b[39m\n\u001b[31m      <!-- AI Progress -->\u001b[39m\n\u001b[31m      <div class=\\\"ai-progress\\\" id=\\\"aiProgress\\\">\u001b[39m\n\u001b[31m        <div class=\\\"ai-prog-label\\\" id=\\\"aiProgLabel\\\">Initializing Transformers.js...</div>\u001b[39m\n\u001b[31m        <div class=\\\"ai-prog-bar\\\"><div class=\\\"ai-prog-fill\\\" id=\\\"aiProgFill\\\"></div></div>\u001b[39m\n\u001b[31m      </div>·\u001b[39m\n\u001b[31m      <!-- OCR Results -->\u001b[39m\n\u001b[31m      <div class=\\\"ocr-results\\\" id=\\\"ocrResults\\\">\u001b[39m\n\u001b[31m        <div class=\\\"sec-div\\\"><span>Detected Dockets</span></div>\u001b[39m\n\u001b[31m        <div id=\\\"ocrSummary\\\" style=\\\"display:none;font-size:.72rem;font-family:var(--fnt-mono);\u001b[39m\n\u001b[31m          padding:4px 8px 8px;gap:8px\\\"></div>\u001b[39m\n\u001b[31m        <div class=\\\"ocr-slip-list\\\" id=\\\"ocrSlipList\\\"></div>\u001b[39m\n\u001b[31m        <div class=\\\"btn-row\\\" style=\\\"margin-top:10px;\\\">\u001b[39m\n\u001b[31m          <button class=\\\"btn btn-ghost\\\" onclick=\\\"clearSlips()\\\">Clear All</button>\u001b[39m\n\u001b[31m          <button class=\\\"btn btn-grn\\\" onclick=\\\"applyAllDockets()\\\">✓ Apply All to Consignees</button>\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m    </div>·\u001b[39m\n\u001b[31m    <!-- 4. CONSIGNEES -->\u001b[39m\n\u001b[31m    <div class=\\\"card accent-left\\\">\u001b[39m\n\u001b[31m      <div class=\\\"card-title\\\"><span class=\\\"ct-dot\\\"></span>Consignees</div>\u001b[39m\n\u001b[31m      <div class=\\\"cons-list\\\" id=\\\"consList\\\"></div>\u001b[39m\n\u001b[31m      <button class=\\\"btn btn-ghost\\\" style=\\\"font-size:.8rem;\\\" onclick=\\\"addConsignee()\\\">+ Add Consignee</button>\u001b[39m\n\u001b[31m    </div>·\u001b[39m\n\u001b[31m    <!-- 5. REMARKS -->\u001b[39m\n\u001b[31m    <div class=\\\"card\\\">\u001b[39m\n\u001b[31m      <div class=\\\"card-title\\\"><span class=\\\"ct-dot\\\" style=\\\"background:var(--t3);\\\"></span>Remarks</div>\u001b[39m\n\u001b[31m      <div class=\\\"f\\\" style=\\\"margin-bottom:0;\\\">\u001b[39m\n\u001b[31m        <textarea id=\\\"n_remarks\\\" placeholder=\\\"Instructions, notes, special handling...\\\"></textarea>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m    </div>·\u001b[39m\n\u001b[31m    <!-- ACTIONS -->\u001b[39m\n\u001b[31m    <div class=\\\"btn-row\\\">\u001b[39m\n\u001b[31m      <button class=\\\"btn btn-ghost\\\" onclick=\\\"saveDraft()\\\">💾 Draft</button>\u001b[39m\n\u001b[31m      <button class=\\\"btn btn-orange\\\" onclick=\\\"finaliseDispatch()\\\">✓ Finalise & Log</button>\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m    <div style=\\\"height:16px;\\\"></div>\u001b[39m\n\u001b[31m  </div>·\u001b[39m\n\u001b[31m  <!-- ════════════ DASHBOARD ════════════ -->\u001b[39m\n\u001b[31m  <div class=\\\"pane\\\" id=\\\"pane-dashboard\\\">\u001b[39m\n\u001b[31m    <div class=\\\"dash-stats\\\">\u001b[39m\n\u001b[31m      <div class=\\\"stat-box grn\\\"><div class=\\\"stat-val\\\" id=\\\"st-delivered\\\">0</div><div class=\\\"stat-lbl\\\">Delivered</div></div>\u001b[39m\n\u001b[31m      <div class=\\\"stat-box ylw\\\"><div class=\\\"stat-val\\\" id=\\\"st-transit\\\">0</div><div class=\\\"stat-lbl\\\">In Transit</div></div>\u001b[39m\n\u001b[31m      <div class=\\\"stat-box sky\\\"><div class=\\\"stat-val\\\" id=\\\"st-booked\\\">0</div><div class=\\\"stat-lbl\\\">Booked</div></div>\u001b[39m\n\u001b[31m      <div class=\\\"stat-box red\\\"><div class=\\\"stat-val\\\" id=\\\"st-pending\\\">0</div><div class=\\\"stat-lbl\\\">Pending</div></div>\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m    <div id=\\\"dashFilters\\\" style=\\\"display:flex;gap:6px;flex-wrap:wrap;margin-bottom:12px;\\\">\u001b[39m\n\u001b[31m      <span class=\\\"btn-sm primary\\\" onclick=\\\"filterDash('all',this)\\\" style=\\\"cursor:pointer;\\\">All</span>\u001b[39m\n\u001b[31m      <span class=\\\"btn-sm\\\" onclick=\\\"filterDash('delivered',this)\\\" style=\\\"cursor:pointer;\\\">Delivered</span>\u001b[39m\n\u001b[31m      <span class=\\\"btn-sm\\\" onclick=\\\"filterDash('transit',this)\\\" style=\\\"cursor:pointer;\\\">In Transit</span>\u001b[39m\n\u001b[31m      <span class=\\\"btn-sm\\\" onclick=\\\"filterDash('booked',this)\\\" style=\\\"cursor:pointer;\\\">Booked</span>\u001b[39m\n\u001b[31m      <span class=\\\"btn-sm\\\" onclick=\\\"filterDash('pending',this)\\\" style=\\\"cursor:pointer;\\\">Pending</span>\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m    <div class=\\\"docket-grid\\\" id=\\\"dashGrid\\\">\u001b[39m\n\u001b[31m      <div class=\\\"empty\\\"><div class=\\\"e-icon\\\">📊</div><p>No dispatches yet.<br>Create one to see the dashboard.</p></div>\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m    <div style=\\\"display:flex;gap:8px;margin-top:10px;flex-wrap:wrap;\\\">\u001b[39m\n\u001b[31m      <button class=\\\"btn btn-ghost\\\" style=\\\"flex:2;font-size:.8rem;\\\" id=\\\"btnRefreshAll\\\" onclick=\\\"refreshAllTracking()\\\">\u001b[39m\n\u001b[31m        🔄 Refresh All Tracking\u001b[39m\n\u001b[31m      </button>\u001b[39m\n\u001b[31m      <button class=\\\"btn btn-ghost\\\" style=\\\"flex:1;font-size:.8rem;border-color:var(--sky);color:var(--sky);\\\"\u001b[39m\n\u001b[31m        onclick=\\\"shareSelectedReport()\\\">\u001b[39m\n\u001b[31m        📊 Share Selected\u001b[39m\n\u001b[31m      </button>\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m  </div>·\u001b[39m\n\u001b[31m  <!-- ════════════ TRACK ════════════ -->\u001b[39m\n\u001b[31m  <div class=\\\"pane\\\" id=\\\"pane-track\\\">·\u001b[39m\n\u001b[31m    <!-- Courier selector -->\u001b[39m\n\u001b[31m    <div class=\\\"card\\\" style=\\\"margin-bottom:10px;\\\">\u001b[39m\n\u001b[31m      <div class=\\\"card-title\\\"><span class=\\\"ct-dot\\\" style=\\\"background:var(--sky);\\\"></span>Manual Tracking</div>\u001b[39m\n\u001b[31m      <div style=\\\"font-size:.72rem;color:var(--t2);margin-bottom:10px;line-height:1.5;\\\">\u001b[39m\n\u001b[31m        Track any docket number — select from already dispatched dockets or enter new ones manually.\u001b[39m\n\u001b[31m        SkyKing supports up to <strong>10 dockets at once</strong>; others are tracked one at a time.\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      <div class=\\\"f\\\">\u001b[39m\n\u001b[31m        <label>Courier Company</label>\u001b[39m\n\u001b[31m        <select id=\\\"t_courier\\\" onchange=\\\"buildTrackDocketChips()\\\">\u001b[39m\n\u001b[31m          <option>SkyKing</option><option>DTDC</option><option>Delhivery</option>\u001b[39m\n\u001b[31m          <option>Blue Dart</option><option>FedEx</option><option>Ecom Express</option><option>India Post</option>\u001b[39m\n\u001b[31m        </select>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m    </div>·\u001b[39m\n\u001b[31m    <!-- Docket selector — from existing dispatches -->\u001b[39m\n\u001b[31m    <div class=\\\"card\\\" style=\\\"margin-bottom:10px;\\\">\u001b[39m\n\u001b[31m      <div class=\\\"card-title\\\"><span class=\\\"ct-dot\\\" style=\\\"background:var(--acc);\\\"></span>\u001b[39m\n\u001b[31m        Select from Dispatched Dockets\u001b[39m\n\u001b[31m        <span id=\\\"trackCourierLimit\\\" style=\\\"margin-left:auto;font-size:.65rem;color:var(--t3);font-weight:400;text-transform:none;letter-spacing:0;font-family:var(--fnt-mono);\\\"></span>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      <div id=\\\"trackDocketChips\\\" style=\\\"display:flex;flex-wrap:wrap;gap:6px;margin-bottom:10px;min-height:28px;\\\">\u001b[39m\n\u001b[31m        <span style=\\\"font-size:.72rem;color:var(--t3);font-family:var(--fnt-mono);\\\">No dispatched dockets yet</span>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      <div style=\\\"font-size:.68rem;color:var(--t3);font-family:var(--fnt-mono);margin-bottom:8px;\\\">\u001b[39m\n\u001b[31m        Tap a docket to add/remove from tracking list. Selected shown in orange.\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m    </div>·\u001b[39m\n\u001b[31m    <!-- Manual entry for new dockets -->\u001b[39m\n\u001b[31m    <div class=\\\"card\\\" style=\\\"margin-bottom:10px;\\\">\u001b[39m\n\u001b[31m      <div class=\\\"card-title\\\"><span class=\\\"ct-dot\\\" style=\\\"background:var(--grn);\\\"></span>Add New Docket Numbers</div>\u001b[39m\n\u001b[31m      <div class=\\\"f\\\" style=\\\"margin-bottom:6px;\\\">\u001b[39m\n\u001b[31m        <label>Enter docket numbers (comma or newline separated)</label>\u001b[39m\n\u001b[31m        <textarea id=\\\"t_manual_dockets\\\" placeholder=\\\"443005527&#10;443005528&#10;or: 443005527, 443005528\\\"\u001b[39m\n\u001b[31m          style=\\\"min-height:56px;font-family:var(--fnt-mono);font-size:.82rem;letter-spacing:.5px;\\\"></textarea>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      <button class=\\\"btn-sm primary\\\" onclick=\\\"addManualDockets()\\\" style=\\\"font-size:.72rem;\\\">+ Add to List</button>\u001b[39m\n\u001b[31m    </div>·\u001b[39m\n\u001b[31m    <!-- Selected dockets summary + track button -->\u001b[39m\n\u001b[31m    <div id=\\\"trackSelectedSummary\\\" style=\\\"display:none;\\\" class=\\\"card\\\" style=\\\"margin-bottom:10px;\\\">\u001b[39m\n\u001b[31m      <div style=\\\"display:flex;align-items:center;justify-content:space-between;margin-bottom:8px;\\\">\u001b[39m\n\u001b[31m        <div>\u001b[39m\n\u001b[31m          <span style=\\\"font-family:var(--fnt-mono);font-size:.72rem;color:var(--sky);\\\" id=\\\"trackSelectedCount\\\">0 selected</span>\u001b[39m\n\u001b[31m          <span style=\\\"font-family:var(--fnt-mono);font-size:.65rem;color:var(--t3);margin-left:8px;\\\" id=\\\"trackSelectedList\\\"></span>\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m        <button class=\\\"btn-sm\\\" onclick=\\\"clearTrackSelection()\\\">Clear</button>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      <div class=\\\"btn-row\\\">\u001b[39m\n\u001b[31m        <button class=\\\"btn btn-ghost\\\" onclick=\\\"openTrackOnCourierSite()\\\">🔗 Open Courier Site</button>\u001b[39m\n\u001b[31m        <button class=\\\"btn btn-orange\\\" onclick=\\\"trackSelected()\\\">🔍 Track All via Proxy</button>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m    </div>·\u001b[39m\n\u001b[31m    <div id=\\\"trackResultsArea\\\"></div>\u001b[39m\n\u001b[31m  </div>·\u001b[39m\n\u001b[31m  <!-- ════════════ HISTORY ════════════ -->\u001b[39m\n\u001b[31m  <div class=\\\"pane\\\" id=\\\"pane-history\\\">\u001b[39m\n\u001b[31m    <div id=\\\"historyList\\\">\u001b[39m\n\u001b[31m      <div class=\\\"empty\\\"><div class=\\\"e-icon\\\">📭</div><p>No dispatch history yet.</p></div>\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m  </div>·\u001b[39m\n\u001b[31m  <!-- ════════════ CONFIG ════════════ -->\u001b[39m\n\u001b[31m  <div class=\\\"pane\\\" id=\\\"pane-config\\\">·\u001b[39m\n\u001b[31m    <!-- SHEETS URL — the only thing you type manually in the PWA -->\u001b[39m\n\u001b[31m    <div class=\\\"card\\\" style=\\\"border-color:var(--acc);border-width:2px;\\\">\u001b[39m\n\u001b[31m      <div class=\\\"card-title\\\"><span class=\\\"ct-dot\\\"></span>Google Sheets — Master Config</div>\u001b[39m\n\u001b[31m      <div class=\\\"info-tag\\\" style=\\\"background:rgba(34,197,94,.07);border-color:rgba(34,197,94,.3);\\\">\u001b[39m\n\u001b[31m        <span>✅</span>\u001b[39m\n\u001b[31m        <span>URL is loaded automatically from <strong>config.json</strong> on the VPS — no manual entry needed. All other settings sync from your <strong>Google Sheet</strong>.</span>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      <div class=\\\"f\\\">\u001b[39m\n\u001b[31m        <label>Apps Script URL <span style=\\\"font-size:.65rem;color:var(--t3);font-weight:400;\\\">(auto-loaded from config.json)</span></label>\u001b[39m\n\u001b[31m        <input type=\\\"url\\\" id=\\\"cfg_sheets_url\\\"\u001b[39m\n\u001b[31m          placeholder=\\\"Auto-loaded from server...\\\"\u001b[39m\n\u001b[31m          oninput=\\\"onSheetsUrlChange()\\\" readonly\u001b[39m\n\u001b[31m          style=\\\"opacity:.7;cursor:default;\\\">\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      <div id=\\\"syncStatus\\\" class=\\\"info-tag\\\" style=\\\"display:none;\\\">\u001b[39m\n\u001b[31m        <span id=\\\"syncIcon\\\">⏳</span>\u001b[39m\n\u001b[31m        <span id=\\\"syncMsg\\\">Syncing from sheet...</span>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      <div class=\\\"btn-row\\\">\u001b[39m\n\u001b[31m        <button class=\\\"btn btn-ghost\\\" onclick=\\\"syncFromSheet()\\\">🔄 Sync Now</button>\u001b[39m\n\u001b[31m        <button class=\\\"btn btn-orange\\\" onclick=\\\"saveSheetsUrl()\\\">💾 Save & Sync</button>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m    </div>·\u001b[39m\n\u001b[31m    <!-- READ-ONLY display of what was loaded from sheet -->\u001b[39m\n\u001b[31m    <div class=\\\"card\\\" id=\\\"sheetConfigDisplay\\\" style=\\\"display:none;\\\">\u001b[39m\n\u001b[31m      <div class=\\\"card-title\\\"><span class=\\\"ct-dot\\\" style=\\\"background:var(--grn);\\\"></span>Config Loaded from Sheet\u001b[39m\n\u001b[31m        <span id=\\\"sheetSyncTime\\\" style=\\\"margin-left:auto;font-size:.62rem;color:var(--t3);font-family:var(--fnt-mono);font-weight:400;text-transform:none;letter-spacing:0;\\\"></span>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      <div id=\\\"sheetConfigRows\\\"></div>\u001b[39m\n\u001b[31m      <div class=\\\"info-tag\\\" style=\\\"margin-top:8px;\\\">\u001b[39m\n\u001b[31m        <span>✏️</span><span>To change any value, edit the <strong>Config sheet</strong> in Google Sheets, then tap Sync Now above.</span>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m    </div>·\u001b[39m\n\u001b[31m    <!-- Courier Master (from sheet) -->\u001b[39m\n\u001b[31m    <div class=\\\"card\\\">\u001b[39m\n\u001b[31m      <div class=\\\"card-title\\\"><span class=\\\"ct-dot\\\" style=\\\"background:var(--ylw);\\\"></span>Courier Master</div>\u001b[39m\n\u001b[31m      <div id=\\\"courierMasterList\\\"></div>\u001b[39m\n\u001b[31m      <button class=\\\"btn btn-ghost\\\" style=\\\"margin-top:8px;font-size:.78rem;\\\" onclick=\\\"openCourierMaster()\\\">✏️ Edit in Sheet / Override</button>\u001b[39m\n\u001b[31m    </div>·\u001b[39m\n\u001b[31m    <button class=\\\"btn\\\" style=\\\"background:var(--red-dim);color:var(--red);border:1px solid var(--red);margin-top:4px;\\\" onclick=\\\"clearAll()\\\">🗑️ Clear All Local Data</button>\u001b[39m\n\u001b[31m    <div style=\\\"height:20px;\\\"></div>\u001b[39m\n\u001b[31m  </div>··\u001b[39m\n\u001b[31m<!-- ════ ADDRESS REVIEW PANE ════ -->\u001b[39m\n\u001b[31m<div id=\\\"addrReviewPane\\\" style=\\\"display:none;position:fixed;inset:0;background:var(--bg);z-index:50;overflow-y:auto;padding:14px;\\\">\u001b[39m\n\u001b[31m  <div class=\\\"verify-hdr\\\">\u001b[39m\n\u001b[31m    <button class=\\\"btn-icon\\\" onclick=\\\"document.getElementById('addrReviewPane').style.display='none'\\\" style=\\\"font-size:1.1rem;\\\">←</button>\u001b[39m\n\u001b[31m    <div class=\\\"verify-title\\\">Review AI-Parsed Addresses</div>\u001b[39m\n\u001b[31m  </div>\u001b[39m\n\u001b[31m  <div style=\\\"background:rgba(56,189,248,.08);border:1px solid rgba(56,189,248,.3);border-radius:var(--r-sm);\u001b[39m\n\u001b[31m    padding:8px 12px;margin-bottom:12px;font-size:.72rem;color:var(--sky);\\\">\u001b[39m\n\u001b[31m    ✏️ Review each row — correct name and city if wrong. Raw address shown below each row for reference.\u001b[39m\n\u001b[31m    Corrections are saved and improve future AI parsing automatically.\u001b[39m\n\u001b[31m  </div>\u001b[39m\n\u001b[31m  <div class=\\\"verify-summary\\\" id=\\\"addrReviewSummary\\\"></div>\u001b[39m\n\u001b[31m  <div id=\\\"addrReviewList\\\"></div>\u001b[39m\n\u001b[31m  <div style=\\\"position:sticky;bottom:0;background:var(--bg);padding-top:10px;\u001b[39m\n\u001b[31m    border-top:1px solid var(--bdr);margin-top:8px;\\\">\u001b[39m\n\u001b[31m    <div style=\\\"display:flex;gap:8px;\\\">\u001b[39m\n\u001b[31m      <button class=\\\"btn btn-ghost\\\" onclick=\\\"backToMapping()\\\" style=\\\"flex:1;\\\">← Back to Mapping</button>\u001b[39m\n\u001b[31m      <button class=\\\"btn btn-orange\\\" onclick=\\\"confirmAddressReview()\\\" style=\\\"flex:2;\\\">✓ Confirm &amp; Use These Addresses</button>\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m    <div style=\\\"font-size:.65rem;color:var(--t3);text-align:center;margin-top:5px;font-family:var(--fnt-mono);\\\">\u001b[39m\n\u001b[31m      Edits you made above are preserved when you go back\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m  </div>\u001b[39m\n\u001b[31m</div>·\u001b[39m\n\u001b[31m<!-- ════ VERIFICATION SCREEN ════ -->\u001b[39m\n\u001b[31m<div id=\\\"verifyPane\\\">\u001b[39m\n\u001b[31m  <div class=\\\"verify-hdr\\\">\u001b[39m\n\u001b[31m    <button class=\\\"btn-icon\\\" onclick=\\\"closeVerify()\\\" style=\\\"font-size:1.1rem;\\\">←</button>\u001b[39m\n\u001b[31m    <div class=\\\"verify-title\\\">Verify Before Finalise</div>\u001b[39m\n\u001b[31m    <button class=\\\"btn-sm\\\" onclick=\\\"approveAll()\\\" style=\\\"white-space:nowrap;\\\">✓ Approve All</button>\u001b[39m\n\u001b[31m  </div>\u001b[39m\n\u001b[31m  <div class=\\\"verify-summary\\\" id=\\\"verifySummary\\\"></div>\u001b[39m\n\u001b[31m  <div id=\\\"verifyList\\\"></div>\u001b[39m\n\u001b[31m  <div style=\\\"position:sticky;bottom:0;background:var(--bg);padding-top:10px;\u001b[39m\n\u001b[31m    border-top:1px solid var(--bdr);margin-top:8px;\\\">\u001b[39m\n\u001b[31m    <div style=\\\"display:flex;gap:8px;flex-wrap:wrap;\\\">\u001b[39m\n\u001b[31m      <button class=\\\"btn btn-ghost\\\" onclick=\\\"closeVerify()\\\" style=\\\"flex:1;\\\">← Back to Edit</button>\u001b[39m\n\u001b[31m      <button class=\\\"btn btn-ghost\\\" onclick=\\\"exportCleanSheet()\\\" style=\\\"flex:1;border-color:var(--sky);color:var(--sky);\\\">\u001b[39m\n\u001b[31m        📥 Export Clean Sheet\u001b[39m\n\u001b[31m      </button>\u001b[39m\n\u001b[31m      <button class=\\\"btn btn-orange\\\" id=\\\"btnConfirmFinalise\\\" onclick=\\\"confirmAndFinalise()\\\" style=\\\"flex:2;\\\">\u001b[39m\n\u001b[31m        ✓ Finalise Dispatch\u001b[39m\n\u001b[31m      </button>\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m  </div>\u001b[39m\n\u001b[31m</div>·\u001b[39m\n\u001b[31m</div><!-- /app -->·\u001b[39m\n\u001b[31m<!-- ════ DISPATCH DETAIL SHEET ════ -->\u001b[39m\n\u001b[31m<div class=\\\"overlay\\\" id=\\\"detailOverlay\\\" onclick=\\\"closeOverlay('detailOverlay')\\\">\u001b[39m\n\u001b[31m  <div class=\\\"sheet\\\" onclick=\\\"event.stopPropagation()\\\">\u001b[39m\n\u001b[31m    <div class=\\\"sheet-handle\\\"></div>\u001b[39m\n\u001b[31m    <div id=\\\"detailContent\\\"></div>\u001b[39m\n\u001b[31m  </div>\u001b[39m\n\u001b[31m</div>·\u001b[39m\n\u001b[31m<!-- ════ COURIER MASTER EDITOR ════ -->\u001b[39m\n\u001b[31m<div class=\\\"overlay\\\" id=\\\"courierMasterOverlay\\\" onclick=\\\"closeOverlay('courierMasterOverlay')\\\">\u001b[39m\n\u001b[31m  <div class=\\\"sheet\\\" onclick=\\\"event.stopPropagation()\\\">\u001b[39m\n\u001b[31m    <div class=\\\"sheet-handle\\\"></div>\u001b[39m\n\u001b[31m    <div class=\\\"sheet-title\\\">✏️ Courier Master</div>\u001b[39m\n\u001b[31m    <div id=\\\"courierMasterEditor\\\"></div>\u001b[39m\n\u001b[31m  </div>\u001b[39m\n\u001b[31m</div>·\u001b[39m\n\u001b[31m<!-- ════ MANUAL TRACKING OVERLAY ════ -->\u001b[39m\n\u001b[31m<div class=\\\"overlay\\\" id=\\\"manualTrackOverlay\\\" onclick=\\\"closeOverlay('manualTrackOverlay')\\\" data-docket=\\\"\\\" data-dispatch-id=\\\"\\\">\u001b[39m\n\u001b[31m  <div class=\\\"sheet\\\" onclick=\\\"event.stopPropagation()\\\">\u001b[39m\n\u001b[31m    <div class=\\\"sheet-handle\\\"></div>\u001b[39m\n\u001b[31m    <div class=\\\"sheet-title\\\">📞 Manual Tracking Log</div>\u001b[39m\n\u001b[31m    <div style=\\\"background:var(--sur2);border:1px solid var(--bdr);border-radius:var(--r-sm);\u001b[39m\n\u001b[31m      padding:8px 12px;margin-bottom:12px;font-family:var(--fnt-mono);font-size:.82rem;color:var(--acc);\\\"\u001b[39m\n\u001b[31m      id=\\\"mtDocketDisplay\\\"></div>\u001b[39m\n\u001b[31m    <div class=\\\"row2\\\">\u001b[39m\n\u001b[31m      <div class=\\\"f\\\">\u001b[39m\n\u001b[31m        <label>Status Reported <span class=\\\"req\\\">*</span></label>\u001b[39m\n\u001b[31m        <select id=\\\"mtStatus\\\">\u001b[39m\n\u001b[31m          <option value=\\\"\\\">Select...</option>\u001b[39m\n\u001b[31m          <option value=\\\"delivered\\\">✅ Delivered</option>\u001b[39m\n\u001b[31m          <option value=\\\"not received\\\">❌ Not Received</option>\u001b[39m\n\u001b[31m          <option value=\\\"in transit\\\">🚚 In Transit / On the way</option>\u001b[39m\n\u001b[31m          <option value=\\\"will deliver tomorrow\\\">📅 Will Deliver Tomorrow</option>\u001b[39m\n\u001b[31m          <option value=\\\"refused\\\">🚫 Refused / Returned</option>\u001b[39m\n\u001b[31m          <option value=\\\"partial\\\">📦 Partial Delivery</option>\u001b[39m\n\u001b[31m        </select>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      <div class=\\\"f\\\">\u001b[39m\n\u001b[31m        <label>Called By (Staff) <span class=\\\"req\\\">*</span></label>\u001b[39m\n\u001b[31m        <input type=\\\"text\\\" id=\\\"mtCalledBy\\\" placeholder=\\\"Your name\\\" list=\\\"staffList\\\">\u001b[39m\n\u001b[31m        <datalist id=\\\"staffList\\\">\u001b[39m\n\u001b[31m          <option>Mukesh</option><option>Sachin</option><option>Shambhu</option>\u001b[39m\n\u001b[31m          <option>Umashankar</option><option>Narender</option><option>Rakesh</option>\u001b[39m\n\u001b[31m          <option>Gurpreet</option><option>Ashok</option>\u001b[39m\n\u001b[31m        </datalist>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m    <div class=\\\"row2\\\">\u001b[39m\n\u001b[31m      <div class=\\\"f\\\">\u001b[39m\n\u001b[31m        <label>Contact Number Used</label>\u001b[39m\n\u001b[31m        <input type=\\\"tel\\\" id=\\\"mtContactUsed\\\" placeholder=\\\"Mobile number called\\\">\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      <div class=\\\"f\\\">\u001b[39m\n\u001b[31m        <label>Person Who Confirmed</label>\u001b[39m\n\u001b[31m        <input type=\\\"text\\\" id=\\\"mtContactPerson\\\" placeholder=\\\"Name of person at store\\\">\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m    <div class=\\\"f\\\">\u001b[39m\n\u001b[31m      <label>Remarks</label>\u001b[39m\n\u001b[31m      <textarea id=\\\"mtRemarks\\\" placeholder=\\\"Any notes, delivery details, issues...\\\" style=\\\"min-height:60px;\\\"></textarea>\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m    <div class=\\\"f\\\">\u001b[39m\n\u001b[31m      <label>Call Date &amp; Time</label>\u001b[39m\n\u001b[31m      <input type=\\\"datetime-local\\\" id=\\\"mtCallTime\\\">\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m    <div class=\\\"btn-row\\\">\u001b[39m\n\u001b[31m      <button class=\\\"btn btn-ghost\\\" onclick=\\\"closeOverlay('manualTrackOverlay')\\\">Cancel</button>\u001b[39m\n\u001b[31m      <button class=\\\"btn btn-orange\\\" onclick=\\\"submitManualTracking()\\\">💾 Log &amp; Save</button>\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m  </div>\u001b[39m\n\u001b[31m</div>·\u001b[39m\n\u001b[31m<!-- ════ POD UPLOAD OVERLAY ════ -->\u001b[39m\n\u001b[31m<div class=\\\"overlay\\\" id=\\\"podOverlay\\\" onclick=\\\"closeOverlay('podOverlay')\\\" data-docket=\\\"\\\" data-dispatch-id=\\\"\\\">\u001b[39m\n\u001b[31m  <div class=\\\"sheet\\\" onclick=\\\"event.stopPropagation()\\\">\u001b[39m\n\u001b[31m    <div class=\\\"sheet-handle\\\"></div>\u001b[39m\n\u001b[31m    <div class=\\\"sheet-title\\\">📸 Upload Proof of Delivery</div>\u001b[39m\n\u001b[31m    <div class=\\\"f\\\">\u001b[39m\n\u001b[31m      <label>POD / Delivery Slip Photo</label>\u001b[39m\n\u001b[31m      <input type=\\\"file\\\" id=\\\"podFileInput\\\" accept=\\\"image/*\\\" capture=\\\"environment\\\"\u001b[39m\n\u001b[31m        onchange=\\\"previewPOD(this,'pod')\\\" style=\\\"font-size:.8rem;color:var(--t2);\\\">\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m    <div class=\\\"f\\\">\u001b[39m\n\u001b[31m      <label>Recipient / Receiver Photo (optional)</label>\u001b[39m\n\u001b[31m      <input type=\\\"file\\\" id=\\\"recipientFileInput\\\" accept=\\\"image/*\\\" capture=\\\"environment\\\"\u001b[39m\n\u001b[31m        onchange=\\\"previewPOD(this,'recip')\\\" style=\\\"font-size:.8rem;color:var(--t2);\\\">\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m    <div id=\\\"podPreview\\\" style=\\\"display:flex;gap:8px;flex-wrap:wrap;margin-bottom:12px;\\\"></div>\u001b[39m\n\u001b[31m    <div class=\\\"btn-row\\\">\u001b[39m\n\u001b[31m      <button class=\\\"btn btn-ghost\\\" onclick=\\\"closeOverlay('podOverlay')\\\">Cancel</button>\u001b[39m\n\u001b[31m      <button class=\\\"btn btn-orange\\\" onclick=\\\"uploadPODFiles()\\\">📤 Upload & Mark Delivered</button>\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m  </div>\u001b[39m\n\u001b[31m</div>·\u001b[39m\n\u001b[31m<!-- ════ ADMIN LOGIN OVERLAY ════ -->\u001b[39m\n\u001b[31m<div class=\\\"overlay\\\" id=\\\"adminLoginOverlay\\\" onclick=\\\"closeOverlay('adminLoginOverlay')\\\">\u001b[39m\n\u001b[31m  <div class=\\\"sheet\\\" onclick=\\\"event.stopPropagation()\\\" style=\\\"max-height:320px;\\\">\u001b[39m\n\u001b[31m    <div class=\\\"sheet-handle\\\"></div>\u001b[39m\n\u001b[31m    <div class=\\\"sheet-title\\\">🔐 Admin Access</div>\u001b[39m\n\u001b[31m    <div style=\\\"font-size:.78rem;color:var(--t2);margin-bottom:14px;\\\">Config settings require admin password.</div>\u001b[39m\n\u001b[31m    <div class=\\\"f\\\">\u001b[39m\n\u001b[31m      <label>Password</label>\u001b[39m\n\u001b[31m      <input type=\\\"password\\\" id=\\\"adminPwdInput\\\" placeholder=\\\"Enter admin password\\\"\u001b[39m\n\u001b[31m        onkeydown=\\\"if(event.key==='Enter')submitAdminLogin()\\\">\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m    <div class=\\\"btn-row\\\" style=\\\"margin-top:4px;\\\">\u001b[39m\n\u001b[31m      <button class=\\\"btn btn-ghost\\\" onclick=\\\"closeOverlay('adminLoginOverlay')\\\">Cancel</button>\u001b[39m\n\u001b[31m      <button class=\\\"btn btn-orange\\\" onclick=\\\"submitAdminLogin()\\\">🔓 Login</button>\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m  </div>\u001b[39m\n\u001b[31m</div>·\u001b[39m\n\u001b[31m<!-- ════ TOAST ════ -->\u001b[39m\n\u001b[31m<div class=\\\"toast\\\" id=\\\"toast\\\"></div>·\u001b[39m\n\u001b[31m<!-- ════ SCRIPTS ════ -->\u001b[39m\n\u001b[31m<!-- SheetJS for Excel/CSV parsing -->\u001b[39m\n\u001b[31m<script src=\\\"https://cdnjs.cloudflare.com/ajax/libs/xlsx/0.18.5/xlsx.full.min.js\\\"></script>·\u001b[39m\n\u001b[31m<script type=\\\"module\\\">\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31m// OCR — Transformers.js\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31mimport { pipeline, env } from 'https://cdn.jsdelivr.net/npm/@huggingface/transformers@3.5.2/dist/transformers.min.js';·\u001b[39m\n\u001b[31m// Cache models in browser\u001b[39m\n\u001b[31menv.allowLocalModels = false;\u001b[39m\n\u001b[31menv.useBrowserCache = true;·\u001b[39m\n\u001b[31mlet ocrPipeline = null;\u001b[39m\n\u001b[31mlet ocrLoading = false;·\u001b[39m\n\u001b[31mwindow.initOCR = async function() {\u001b[39m\n\u001b[31m  if (ocrPipeline || ocrLoading) return;\u001b[39m\n\u001b[31m  ocrLoading = true;\u001b[39m\n\u001b[31m  showAIProgress('Loading OCR model (first time ~120MB)...', 5);\u001b[39m\n\u001b[31m  try {\u001b[39m\n\u001b[31m    ocrPipeline = await pipeline('image-to-text', 'Xenova/trocr-base-printed', {\u001b[39m\n\u001b[31m      progress_callback: (p) => {\u001b[39m\n\u001b[31m        if (p.progress) {\u001b[39m\n\u001b[31m          showAIProgress(`Loading: ${p.file || 'model'} (${Math.round(p.progress)}%)`, p.progress);\u001b[39m\n\u001b[31m        }\u001b[39m\n\u001b[31m      }\u001b[39m\n\u001b[31m    });\u001b[39m\n\u001b[31m    showAIProgress('OCR model ready ✓', 100);\u001b[39m\n\u001b[31m    setTimeout(() => hideAIProgress(), 1500);\u001b[39m\n\u001b[31m    showToast('OCR ready ✓', 'ok');\u001b[39m\n\u001b[31m  } catch(e) {\u001b[39m\n\u001b[31m    console.warn('Transformers.js OCR failed:', e);\u001b[39m\n\u001b[31m    hideAIProgress();\u001b[39m\n\u001b[31m    showToast('OCR model load failed — will use Ollama fallback', 'info');\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m  ocrLoading = false;\u001b[39m\n\u001b[31m};·\u001b[39m\n\u001b[31mwindow.runOCROnImage = async function(imageData, filename) {\u001b[39m\n\u001b[31m  const cfg = window.appConfig;·\u001b[39m\n\u001b[31m  // Strategy 1: Transformers.js\u001b[39m\n\u001b[31m  if ((cfg.ocr_mode === 'transformers' || cfg.ocr_mode === 'both') && ocrPipeline) {\u001b[39m\n\u001b[31m    try {\u001b[39m\n\u001b[31m      const result = await ocrPipeline(imageData);\u001b[39m\n\u001b[31m      const text = result[0]?.generated_text || '';\u001b[39m\n\u001b[31m      return extractDocketFromText(text, 'transformers');\u001b[39m\n\u001b[31m    } catch(e) { /* fall through */ }\u001b[39m\n\u001b[31m  }·\u001b[39m\n\u001b[31m  // Strategy 2: Dispatch AI proxy → Ollama LLaVA (server-side, avoids CORS + port issues)\u001b[39m\n\u001b[31m  if (cfg.ocr_mode === 'ollama' || cfg.ocr_mode === 'both') {\u001b[39m\n\u001b[31m    try {\u001b[39m\n\u001b[31m      const resp = await fetch('/dispatch-ai/ocr', {\u001b[39m\n\u001b[31m        method: 'POST',\u001b[39m\n\u001b[31m        headers: { 'Content-Type': 'application/json' },\u001b[39m\n\u001b[31m        body: JSON.stringify({ imageBase64: imageData }),\u001b[39m\n\u001b[31m        signal: AbortSignal.timeout(180000)\u001b[39m\n\u001b[31m      });\u001b[39m\n\u001b[31m      const data = await resp.json();\u001b[39m\n\u001b[31m      if (data.success && data.docket) {\u001b[39m\n\u001b[31m        return { docket: data.docket, confidence: data.confidence, source: 'ollama' };\u001b[39m\n\u001b[31m      }\u001b[39m\n\u001b[31m    } catch(e) { /* fall through */ }\u001b[39m\n\u001b[31m  }·\u001b[39m\n\u001b[31m  return { docket: '', confidence: 0, source: 'failed' };\u001b[39m\n\u001b[31m};·\u001b[39m\n\u001b[31mfunction extractDocketFromText(text, source) {\u001b[39m\n\u001b[31m  // Pattern: typical courier docket = 8-15 digit numbers, sometimes with letters\u001b[39m\n\u001b[31m  const patterns = [\u001b[39m\n\u001b[31m    /\\\\b([A-Z]{0,3}\\\\d{8,15}[A-Z]{0,3})\\\\b/g,   // alphanumeric tracking\u001b[39m\n\u001b[31m    /\\\\b(\\\\d{9,15})\\\\b/g,                          // pure numeric\u001b[39m\n\u001b[31m    /\\\\b([A-Z]{2,4}\\\\d{6,12})\\\\b/g,               // letter prefix\u001b[39m\n\u001b[31m  ];\u001b[39m\n\u001b[31m  const candidates = [];\u001b[39m\n\u001b[31m  for (const pat of patterns) {\u001b[39m\n\u001b[31m    let m;\u001b[39m\n\u001b[31m    while ((m = pat.exec(text)) !== null) {\u001b[39m\n\u001b[31m      if (!candidates.includes(m[1])) candidates.push(m[1]);\u001b[39m\n\u001b[31m    }\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m  if (candidates.length === 0) return { docket: text.trim().slice(0, 20), confidence: 0.3, source };\u001b[39m\n\u001b[31m  // Pick longest candidate (usually most complete docket)\u001b[39m\n\u001b[31m  const best = candidates.sort((a, b) => b.length - a.length)[0];\u001b[39m\n\u001b[31m  const confidence = candidates.length === 1 ? 0.85 : 0.7;\u001b[39m\n\u001b[31m  return { docket: best, confidence, source };\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// Expose for non-module code\u001b[39m\n\u001b[31mwindow._ocrReady = () => ocrPipeline !== null;\u001b[39m\n\u001b[31m</script>·\u001b[39m\n\u001b[31m<script>\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31m// STATE & STORAGE\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31mlet dispatches = [];\u001b[39m\n\u001b[31mlet stores = [];\u001b[39m\n\u001b[31mlet slipOCRData = [];\u001b[39m\n\u001b[31mlet dashFilter = 'all';·\u001b[39m\n\u001b[31m// Only sheets_url is persisted locally — all other config comes from the sheet\u001b[39m\n\u001b[31mlet _trackingInProgress = false;\u001b[39m\n\u001b[31mwindow.appConfig = {\u001b[39m\n\u001b[31m  sheets_url:      'https://script.google.com/macros/s/AKfycbys8HrToDv2u_dRvSFnNqmO9bnyAO1s8oSprUydpVQnNQutq4tuUY71cVCH24urbvM9/exec',\u001b[39m\n\u001b[31m  webhook_url:     '',\u001b[39m\n\u001b[31m  proxy_url:       '',\u001b[39m\n\u001b[31m  ollama_url:      'http://srv1111289.hstgr.cloud:11434',\u001b[39m\n\u001b[31m  ocr_mode:        'both',\u001b[39m\n\u001b[31m  company:         '360 Degree Logical Marketing',\u001b[39m\n\u001b[31m  city:            'Chandigarh',\u001b[39m\n\u001b[31m  auto_log:        'true',\u001b[39m\n\u001b[31m  refresh_on_load: 'false',\u001b[39m\n\u001b[31m};·\u001b[39m\n\u001b[31mconst COURIER_MASTER = {\u001b[39m\n\u001b[31m  'SkyKing': {\u001b[39m\n\u001b[31m    trackUrl: 'https://skyking.co/track?cno={docket}',\u001b[39m\n\u001b[31m    trackBulkUrl: 'https://skyking.co/track',\u001b[39m\n\u001b[31m    apiProxy: '/track-proxy/skyking?cno={docket}',\u001b[39m\n\u001b[31m    maxBulk: 10\u001b[39m\n\u001b[31m  },\u001b[39m\n\u001b[31m  'DTDC': {\u001b[39m\n\u001b[31m    trackUrl: 'https://www.dtdc.in/tracking.asp',\u001b[39m\n\u001b[31m    queryParam: 'trkId',\u001b[39m\n\u001b[31m    maxBulk: 1\u001b[39m\n\u001b[31m  },\u001b[39m\n\u001b[31m  'Delhivery': {\u001b[39m\n\u001b[31m    trackUrl: 'https://www.delhivery.com/track/package/{docket}',\u001b[39m\n\u001b[31m    maxBulk: 1\u001b[39m\n\u001b[31m  },\u001b[39m\n\u001b[31m  'Blue Dart': {\u001b[39m\n\u001b[31m    trackUrl: 'https://www.bluedart.com/tracking',\u001b[39m\n\u001b[31m    maxBulk: 1\u001b[39m\n\u001b[31m  },\u001b[39m\n\u001b[31m  'FedEx': {\u001b[39m\n\u001b[31m    trackUrl: 'https://www.fedex.com/fedextrack/?trknbr={docket}',\u001b[39m\n\u001b[31m    maxBulk: 10\u001b[39m\n\u001b[31m  },\u001b[39m\n\u001b[31m  'Ecom Express': {\u001b[39m\n\u001b[31m    trackUrl: 'https://ecomexpress.in/tracking/?awb_field={docket}',\u001b[39m\n\u001b[31m    maxBulk: 1\u001b[39m\n\u001b[31m  },\u001b[39m\n\u001b[31m  'India Post': {\u001b[39m\n\u001b[31m    trackUrl: 'https://www.indiapost.gov.in/VAS/Pages/trackconsignment.aspx',\u001b[39m\n\u001b[31m    maxBulk: 1\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m};·\u001b[39m\n\u001b[31mfunction loadData() {\u001b[39m\n\u001b[31m  try {\u001b[39m\n\u001b[31m    dispatches = JSON.parse(localStorage.getItem('360lm_disp_v2') || '[]');\u001b[39m\n\u001b[31m    const saved = JSON.parse(localStorage.getItem('360lm_disp_sheets_url') || '{}');\u001b[39m\n\u001b[31m    if (saved.sheets_url) window.appConfig.sheets_url = saved.sheets_url;\u001b[39m\n\u001b[31m  } catch(e) {}\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31mfunction saveData() {\u001b[39m\n\u001b[31m  try {\u001b[39m\n\u001b[31m    localStorage.setItem('360lm_disp_v2', JSON.stringify(dispatches));\u001b[39m\n\u001b[31m  } catch(e) {\u001b[39m\n\u001b[31m    if (e.name === 'QuotaExceededError' || e.code === 22) {\u001b[39m\n\u001b[31m      showToast('Storage full — old dispatches cleared. Data is safe in Postgres.', 'err');\u001b[39m\n\u001b[31m      // Drop delivered dispatches older than 30 days to free space\u001b[39m\n\u001b[31m      const cutoff = Date.now() - 30 * 24 * 3600 * 1000;\u001b[39m\n\u001b[31m      dispatches = dispatches.filter(d =>\u001b[39m\n\u001b[31m        d.status !== 'draft' && (\u001b[39m\n\u001b[31m          d.status !== 'active' ||\u001b[39m\n\u001b[31m          new Date(d.createdAt || 0).getTime() > cutoff\u001b[39m\n\u001b[31m        ) ? true :\u001b[39m\n\u001b[31m        d.status === 'draft' || new Date(d.createdAt || 0).getTime() > cutoff\u001b[39m\n\u001b[31m      );\u001b[39m\n\u001b[31m      try { localStorage.setItem('360lm_disp_v2', JSON.stringify(dispatches)); } catch(_) {}\u001b[39m\n\u001b[31m    }\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// ── Postgres DB sync ──────────────────────────────────────────────────────\u001b[39m\n\u001b[31mconst _DB        = '/db';\u001b[39m\n\u001b[31m// PostgREST v12: Accept-Profile for reads, Content-Profile for writes\u001b[39m\n\u001b[31mconst _DHDR_READ  = { 'Accept-Profile': 'dispatch' };\u001b[39m\n\u001b[31mconst _DHDR_WRITE = { 'Content-Type': 'application/json', 'Content-Profile': 'dispatch', 'Accept-Profile': 'dispatch' };\u001b[39m\n\u001b[31mconst _DHDR_UPSERT = { ..._DHDR_WRITE, 'Prefer': 'resolution=merge-duplicates,return=minimal' };·\u001b[39m\n\u001b[31mfunction _dispRow(d) {\u001b[39m\n\u001b[31m  return {\u001b[39m\n\u001b[31m    dispatch_id:    d.id,\u001b[39m\n\u001b[31m    client:         d.client    || '',\u001b[39m\n\u001b[31m    job_ref:        d.jobref    || null,\u001b[39m\n\u001b[31m    courier:        d.courier   || '',\u001b[39m\n\u001b[31m    dispatch_date:  d.dispatchDate || null,\u001b[39m\n\u001b[31m    status:         d.status    || 'active',\u001b[39m\n\u001b[31m    total_count:    (d.consignees || []).length,\u001b[39m\n\u001b[31m    docketed_count: (d.consignees || []).filter(c => c.docket).length,\u001b[39m\n\u001b[31m    remarks:        d.remarks   || null,\u001b[39m\n\u001b[31m    created_by:     window._hubUser?.id || 'unknown',\u001b[39m\n\u001b[31m  };\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction _consRow(c, dispatchId, idx) {\u001b[39m\n\u001b[31m  return {\u001b[39m\n\u001b[31m    cons_id:            `${dispatchId}-${idx}`,\u001b[39m\n\u001b[31m    dispatch_id:        dispatchId,\u001b[39m\n\u001b[31m    seq_no:             idx,\u001b[39m\n\u001b[31m    name:               c.name          || '',\u001b[39m\n\u001b[31m    city:               c.city          || null,\u001b[39m\n\u001b[31m    address:            c.address       || null,\u001b[39m\n\u001b[31m    phone:              c.phone         || null,\u001b[39m\n\u001b[31m    pincode:            c.pincode       || null,\u001b[39m\n\u001b[31m    material:           c.material      || null,\u001b[39m\n\u001b[31m    qty:                c.qty           || null,\u001b[39m\n\u001b[31m    docket:             c.docket        || null,\u001b[39m\n\u001b[31m    status:             c.status        || 'pending',\u001b[39m\n\u001b[31m    last_event:         c.lastEvent     || null,\u001b[39m\n\u001b[31m    last_scan_location: c.lastScanLocation || null,\u001b[39m\n\u001b[31m    last_tracked_at:    c.lastTrackedAt || null,\u001b[39m\n\u001b[31m    manual_status:      c.manualStatus  || null,\u001b[39m\n\u001b[31m    manual_delivered:   !!c.manualDelivered,\u001b[39m\n\u001b[31m    manual_called_by:   c.manualCalledBy || null,\u001b[39m\n\u001b[31m    manual_remarks:     c.manualRemarks || null,\u001b[39m\n\u001b[31m    pod_uploaded:       !!c.podUploaded,\u001b[39m\n\u001b[31m  };\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31masync function syncDispatchToDB(d) {\u001b[39m\n\u001b[31m  if (!d || d.status === 'draft') return; // never sync drafts\u001b[39m\n\u001b[31m  try {\u001b[39m\n\u001b[31m    await fetch(`${_DB}/dispatches`, {\u001b[39m\n\u001b[31m      method: 'POST', headers: _DHDR_UPSERT,\u001b[39m\n\u001b[31m      body: JSON.stringify(_dispRow(d)),\u001b[39m\n\u001b[31m    });\u001b[39m\n\u001b[31m    const rows = (d.consignees || []).map((c, i) => _consRow(c, d.id, i));\u001b[39m\n\u001b[31m    if (rows.length) {\u001b[39m\n\u001b[31m      await fetch(`${_DB}/consignees`, {\u001b[39m\n\u001b[31m        method: 'POST', headers: _DHDR_UPSERT,\u001b[39m\n\u001b[31m        body: JSON.stringify(rows),\u001b[39m\n\u001b[31m      });\u001b[39m\n\u001b[31m    }\u001b[39m\n\u001b[31m  } catch(e) { console.warn('[dispatch-db] sync error:', e.message); }\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31masync function syncConsigneeToDB(c, dispatchId, idx) {\u001b[39m\n\u001b[31m  try {\u001b[39m\n\u001b[31m    await fetch(`${_DB}/consignees`, {\u001b[39m\n\u001b[31m      method: 'POST', headers: _DHDR_UPSERT,\u001b[39m\n\u001b[31m      body: JSON.stringify(_consRow(c, dispatchId, idx)),\u001b[39m\n\u001b[31m    });\u001b[39m\n\u001b[31m    // Log tracking event\u001b[39m\n\u001b[31m    if (c.lastEvent && c.status) {\u001b[39m\n\u001b[31m      fetch(`${_DB}/tracking_events`, {\u001b[39m\n\u001b[31m        method: 'POST', headers: _DHDR_WRITE,\u001b[39m\n\u001b[31m        body: JSON.stringify({\u001b[39m\n\u001b[31m          docket: c.docket, dispatch_id: dispatchId,\u001b[39m\n\u001b[31m          cons_id: `${dispatchId}-${idx}`,\u001b[39m\n\u001b[31m          status: c.status, event_text: c.lastEvent,\u001b[39m\n\u001b[31m          scan_location: c.lastScanLocation || null,\u001b[39m\n\u001b[31m        }),\u001b[39m\n\u001b[31m      }).catch(() => {});\u001b[39m\n\u001b[31m    }\u001b[39m\n\u001b[31m  } catch(e) { console.warn('[dispatch-db] consignee sync error:', e.message); }\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31masync function loadFromDB() {\u001b[39m\n\u001b[31m  try {\u001b[39m\n\u001b[31m    const [dResp, cResp] = await Promise.all([\u001b[39m\n\u001b[31m      fetch(`${_DB}/dispatches?order=created_at.desc&limit=100`, { headers: _DHDR_READ }),\u001b[39m\n\u001b[31m      fetch(`${_DB}/consignees?order=dispatch_id,seq_no`, { headers: _DHDR_READ }),\u001b[39m\n\u001b[31m    ]);\u001b[39m\n\u001b[31m    if (!dResp.ok) return;\u001b[39m\n\u001b[31m    const dbDispatches = await dResp.json();\u001b[39m\n\u001b[31m    const dbConsignees = cResp.ok ? await cResp.json() : [];\u001b[39m\n\u001b[31m    if (!dbDispatches.length) return;·\u001b[39m\n\u001b[31m    const dbIds = new Set(dbDispatches.map(d => d.dispatch_id));\u001b[39m\n\u001b[31m    const localOnly = dispatches.filter(d => !dbIds.has(d.id));·\u001b[39m\n\u001b[31m    const fromDB = dbDispatches.map(d => {\u001b[39m\n\u001b[31m      const cons = dbConsignees.filter(c => c.dispatch_id === d.dispatch_id);\u001b[39m\n\u001b[31m      return {\u001b[39m\n\u001b[31m        id:           d.dispatch_id,\u001b[39m\n\u001b[31m        client:       d.client,\u001b[39m\n\u001b[31m        jobref:       d.job_ref,\u001b[39m\n\u001b[31m        courier:      d.courier,\u001b[39m\n\u001b[31m        dispatchDate: d.dispatch_date,\u001b[39m\n\u001b[31m        status:       d.status,\u001b[39m\n\u001b[31m        remarks:      d.remarks,\u001b[39m\n\u001b[31m        createdAt:    d.created_at,\u001b[39m\n\u001b[31m        consignees:   cons.map(c => ({\u001b[39m\n\u001b[31m          name:              c.name,\u001b[39m\n\u001b[31m          city:              c.city,\u001b[39m\n\u001b[31m          address:           c.address,\u001b[39m\n\u001b[31m          phone:             c.phone,\u001b[39m\n\u001b[31m          pincode:           c.pincode,\u001b[39m\n\u001b[31m          material:          c.material,\u001b[39m\n\u001b[31m          qty:               c.qty,\u001b[39m\n\u001b[31m          docket:            c.docket,\u001b[39m\n\u001b[31m          status:            c.status,\u001b[39m\n\u001b[31m          lastEvent:         c.last_event,\u001b[39m\n\u001b[31m          lastScanLocation:  c.last_scan_location,\u001b[39m\n\u001b[31m          lastTrackedAt:     c.last_tracked_at,\u001b[39m\n\u001b[31m          manualStatus:      c.manual_status,\u001b[39m\n\u001b[31m          manualDelivered:   c.manual_delivered,\u001b[39m\n\u001b[31m          manualCalledBy:    c.manual_called_by,\u001b[39m\n\u001b[31m          manualRemarks:     c.manual_remarks,\u001b[39m\n\u001b[31m          podUploaded:       c.pod_uploaded,\u001b[39m\n\u001b[31m          _dbIdx:            c.seq_no,\u001b[39m\n\u001b[31m        })),\u001b[39m\n\u001b[31m      };\u001b[39m\n\u001b[31m    });·\u001b[39m\n\u001b[31m    dispatches = [...fromDB, ...localOnly];\u001b[39m\n\u001b[31m    saveData();\u001b[39m\n\u001b[31m    renderDashboard();\u001b[39m\n\u001b[31m    updateHistCount();\u001b[39m\n\u001b[31m  } catch(e) { console.warn('[dispatch-db] load error:', e.message); }\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31mfunction saveSheetsUrlLocal() {\u001b[39m\n\u001b[31m  localStorage.setItem('360lm_disp_sheets_url', JSON.stringify({ sheets_url: window.appConfig.sheets_url }));\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m/**\u001b[39m\n\u001b[31m * Fetch config.json from the VPS server (same origin).\u001b[39m\n\u001b[31m * This file contains the sheets_url — no manual entry needed.\u001b[39m\n\u001b[31m * Falls back to localStorage cache if fetch fails (offline).\u001b[39m\n\u001b[31m */\u001b[39m\n\u001b[31masync function loadServerConfig() {\u001b[39m\n\u001b[31m  try {\u001b[39m\n\u001b[31m    const resp = await fetch('./config.json', { cache: 'no-store' });\u001b[39m\n\u001b[31m    if (resp.ok) {\u001b[39m\n\u001b[31m      const cfg = await resp.json();\u001b[39m\n\u001b[31m      if (cfg.sheets_url) {\u001b[39m\n\u001b[31m        window.appConfig.sheets_url = cfg.sheets_url;\u001b[39m\n\u001b[31m        saveSheetsUrlLocal(); // cache it for offline use\u001b[39m\n\u001b[31m        document.getElementById('cfg_sheets_url').value = cfg.sheets_url;\u001b[39m\n\u001b[31m        return true;\u001b[39m\n\u001b[31m      }\u001b[39m\n\u001b[31m    }\u001b[39m\n\u001b[31m  } catch(e) {\u001b[39m\n\u001b[31m    console.log('config.json fetch failed, using cached URL:', e.message);\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m  return false;\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31m// INIT\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31m// ── Hub auth gate ─────────────────────────────────────────────────────────\u001b[39m\n\u001b[31mwindow._hubUser = null;\u001b[39m\n\u001b[31m(function() {\u001b[39m\n\u001b[31m  try {\u001b[39m\n\u001b[31m    const hubLogin = '/hub/?next=' + encodeURIComponent(location.pathname + location.search);\u001b[39m\n\u001b[31m    const s = JSON.parse(localStorage.getItem('lm360-session') || 'null');\u001b[39m\n\u001b[31m    if (!s || !s.empId || !s.name) { window.location.replace(hubLogin); return; }\u001b[39m\n\u001b[31m    if (Date.now() - new Date(s.loginAt).getTime() > 12 * 3600 * 1000) {\u001b[39m\n\u001b[31m      window.location.replace(hubLogin); return;\u001b[39m\n\u001b[31m    }\u001b[39m\n\u001b[31m    window._hubUser = { id: s.empId, name: s.name, role: s.role };\u001b[39m\n\u001b[31m  } catch(_) { window.location.replace('/hub/?next=' + encodeURIComponent(location.pathname + location.search)); }\u001b[39m\n\u001b[31m})();·\u001b[39m\n\u001b[31mwindow.addEventListener('load', async () => {\u001b[39m\n\u001b[31m  if (!window._hubUser) return; // should never reach here — gate redirected already\u001b[39m\n\u001b[31m  setTimeout(() => {\u001b[39m\n\u001b[31m    document.getElementById('splash').style.opacity = '0';\u001b[39m\n\u001b[31m    setTimeout(() => {\u001b[39m\n\u001b[31m      document.getElementById('splash').style.display = 'none';\u001b[39m\n\u001b[31m      document.getElementById('app').style.display = 'flex';\u001b[39m\n\u001b[31m    }, 450);\u001b[39m\n\u001b[31m  }, 1400);\u001b[39m\n\u001b[31m  loadData();\u001b[39m\n\u001b[31m  // Auto-authenticate config tab for admin users (no password needed)\u001b[39m\n\u001b[31m  if (['harish', 'pramod'].includes(window._hubUser?.id)) _adminAuthed = true;\u001b[39m\n\u001b[31m  setToday();\u001b[39m\n\u001b[31m  addConsignee();\u001b[39m\n\u001b[31m  updateHistCount();\u001b[39m\n\u001b[31m  setInterval(updateNetDot, 5000);\u001b[39m\n\u001b[31m  updateNetDot();\u001b[39m\n\u001b[31m  checkShareTarget();\u001b[39m\n\u001b[31m  setupDragDrop();·\u001b[39m\n\u001b[31m  // Step 1: fetch sheets_url from VPS config.json (auto, no user input)\u001b[39m\n\u001b[31m  await loadServerConfig();·\u001b[39m\n\u001b[31m  // Step 2: populate config tab field\u001b[39m\n\u001b[31m  document.getElementById('cfg_sheets_url').value = window.appConfig.sheets_url || '';·\u001b[39m\n\u001b[31m  // Step 3: load dispatches from Postgres in background (merge with local)\u001b[39m\n\u001b[31m  loadFromDB(); // non-blocking — updates UI when data arrives·\u001b[39m\n\u001b[31m  // Step 4: sync full config from Google Sheet silently\u001b[39m\n\u001b[31m  if (window.appConfig.sheets_url) {\u001b[39m\n\u001b[31m    syncFromSheet(true); // silent on startup\u001b[39m\n\u001b[31m  } else {\u001b[39m\n\u001b[31m    // Only show config tab if no URL found anywhere\u001b[39m\n\u001b[31m    showPane('config');\u001b[39m\n\u001b[31m    showToast('No config.json found on server', 'err');\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m  // Pre-warm OCR after delay\u001b[39m\n\u001b[31m  setTimeout(() => { if (typeof window.initOCR === 'function') window.initOCR(); }, 4000);\u001b[39m\n\u001b[31m});·\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31m// WEB SHARE TARGET\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31masync function checkShareTarget() {\u001b[39m\n\u001b[31m  // Check URL params for share target\u001b[39m\n\u001b[31m  const params = new URLSearchParams(window.location.search);\u001b[39m\n\u001b[31m  const action = params.get('action');\u001b[39m\n\u001b[31m  if (action !== 'share') return;·\u001b[39m\n\u001b[31m  // Check for shared files via Share Target API\u001b[39m\n\u001b[31m  if ('launchQueue' in window) {\u001b[39m\n\u001b[31m    window.launchQueue.setConsumer(async launchParams => {\u001b[39m\n\u001b[31m      if (!launchParams.files || launchParams.files.length === 0) return;\u001b[39m\n\u001b[31m      const files = await Promise.all(launchParams.files.map(f => f.getFile()));\u001b[39m\n\u001b[31m      const imageFiles = files.filter(f => f.type.startsWith('image/'));\u001b[39m\n\u001b[31m      if (imageFiles.length > 0) {\u001b[39m\n\u001b[31m        document.getElementById('shareTargetInfo').style.display = 'flex';\u001b[39m\n\u001b[31m        document.getElementById('sharedCount').textContent = imageFiles.length;\u001b[39m\n\u001b[31m        document.getElementById('shareBanner').classList.add('show');\u001b[39m\n\u001b[31m        setTimeout(() => document.getElementById('shareBanner').classList.remove('show'), 4000);\u001b[39m\n\u001b[31m        showPane('new');\u001b[39m\n\u001b[31m        await handleSlipFiles(imageFiles);\u001b[39m\n\u001b[31m      }\u001b[39m\n\u001b[31m    });\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction dismissShare() {\u001b[39m\n\u001b[31m  document.getElementById('shareBanner').classList.remove('show');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31m// DRAG & DROP\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31mfunction setupDragDrop() {\u001b[39m\n\u001b[31m  const zone = document.getElementById('slipZone');\u001b[39m\n\u001b[31m  zone.addEventListener('dragover', e => { e.preventDefault(); zone.classList.add('drag'); });\u001b[39m\n\u001b[31m  zone.addEventListener('dragleave', () => zone.classList.remove('drag'));\u001b[39m\n\u001b[31m  zone.addEventListener('drop', e => {\u001b[39m\n\u001b[31m    e.preventDefault();\u001b[39m\n\u001b[31m    zone.classList.remove('drag');\u001b[39m\n\u001b[31m    const files = Array.from(e.dataTransfer.files).filter(f => f.type.startsWith('image/'));\u001b[39m\n\u001b[31m    if (files.length) handleSlipFiles(files);\u001b[39m\n\u001b[31m  });\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31m// SLIP OCR HANDLER\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31masync function handleSlipFiles(files) {\u001b[39m\n\u001b[31m  const fileArr = Array.from(files);\u001b[39m\n\u001b[31m  if (!fileArr.length) return;\u001b[39m\n\u001b[31m  // If dockets already mapped from store list — allow upload but skip OCR extraction\u001b[39m\n\u001b[31m  const skipOCR = shouldSkipOCR();\u001b[39m\n\u001b[31m  if (skipOCR) {\u001b[39m\n\u001b[31m    showToast('Dockets already mapped — slips saved for reference only', 'info');\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m  showOCRWorking(true, `Processing ${fileArr.length} slip(s)...`);·\u001b[39m\n\u001b[31m  for (let i = 0; i < fileArr.length; i++) {\u001b[39m\n\u001b[31m    const file = fileArr[i];\u001b[39m\n\u001b[31m    showOCRWorking(true, `OCR: ${i+1}/${fileArr.length} — ${file.name}`);\u001b[39m\n\u001b[31m    const imageData = await readFileAsDataURL(file);\u001b[39m\n\u001b[31m    let result = { docket: '', confidence: 0, source: 'manual' };\u001b[39m\n\u001b[31m    if (!skipOCR) {\u001b[39m\n\u001b[31m      try {\u001b[39m\n\u001b[31m        result = await window.runOCROnImage(imageData, file.name);\u001b[39m\n\u001b[31m      } catch(e) {}\u001b[39m\n\u001b[31m    } else {\u001b[39m\n\u001b[31m      result = { docket: '', confidence: 1, source: 'skipped — docket pre-mapped' };\u001b[39m\n\u001b[31m    }\u001b[39m\n\u001b[31m    slipOCRData.push({ file, imageData, filename: file.name, ...result });\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m  showOCRWorking(false);\u001b[39m\n\u001b[31m  renderOCRResults();\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction readFileAsDataURL(file) {\u001b[39m\n\u001b[31m  return new Promise((res, rej) => {\u001b[39m\n\u001b[31m    const r = new FileReader();\u001b[39m\n\u001b[31m    r.onload = e => res(e.target.result);\u001b[39m\n\u001b[31m    r.onerror = rej;\u001b[39m\n\u001b[31m    r.readAsDataURL(file);\u001b[39m\n\u001b[31m  });\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction renderOCRResults() {\u001b[39m\n\u001b[31m  if (!slipOCRData.length) return;\u001b[39m\n\u001b[31m  const list = document.getElementById('ocrSlipList');\u001b[39m\n\u001b[31m  document.getElementById('ocrResults').classList.add('visible');·\u001b[39m\n\u001b[31m  const needReview = slipOCRData.filter(s => s.confidence < 0.65 && s.docket).length;\u001b[39m\n\u001b[31m  const autoOk     = slipOCRData.filter(s => s.confidence >= 0.88).length;\u001b[39m\n\u001b[31m  const summary    = document.getElementById('ocrSummary');\u001b[39m\n\u001b[31m  if (summary) {\u001b[39m\n\u001b[31m    summary.style.display = '';\u001b[39m\n\u001b[31m    summary.innerHTML =\u001b[39m\n\u001b[31m      `<span style=\\\"color:var(--grn)\\\">✓ ${autoOk} auto-accepted</span>` +\u001b[39m\n\u001b[31m      (needReview ? `&nbsp;·&nbsp;<span style=\\\"color:var(--ylw)\\\">⚠ ${needReview} need manual check</span>` : '');\u001b[39m\n\u001b[31m  }·\u001b[39m\n\u001b[31m  list.innerHTML = slipOCRData.map((s, i) => {\u001b[39m\n\u001b[31m    const autoAccepted = s.confidence >= 0.88 && s.docket;\u001b[39m\n\u001b[31m    const needsCheck   = s.confidence < 0.65  && s.docket;\u001b[39m\n\u001b[31m    const confClass = autoAccepted ? 'conf-high' : needsCheck ? 'conf-low' : 'conf-med';\u001b[39m\n\u001b[31m    const confLabel = autoAccepted\u001b[39m\n\u001b[31m      ? `✓ ${Math.round(s.confidence * 100)}% — auto`\u001b[39m\n\u001b[31m      : needsCheck\u001b[39m\n\u001b[31m        ? `⚠ ${Math.round(s.confidence * 100)}% — verify`\u001b[39m\n\u001b[31m        : `${Math.round(s.confidence * 100)}%`;\u001b[39m\n\u001b[31m    const borderStyle = needsCheck ? 'border:1px solid var(--ylw);' : '';\u001b[39m\n\u001b[31m    return `\u001b[39m\n\u001b[31m    <div class=\\\"ocr-slip\\\" id=\\\"ocrSlip_${i}\\\" style=\\\"${borderStyle}\\\">\u001b[39m\n\u001b[31m      <img src=\\\"${s.imageData}\\\" alt=\\\"slip ${i+1}\\\">\u001b[39m\n\u001b[31m      <div class=\\\"ocr-slip-body\\\">\u001b[39m\n\u001b[31m        <div class=\\\"ocr-slip-name\\\">${s.filename}</div>\u001b[39m\n\u001b[31m        <input type=\\\"text\\\" class=\\\"ocr-slip-docket\\\" id=\\\"ocrDocket_${i}\\\"\u001b[39m\n\u001b[31m          value=\\\"${s.docket}\\\" placeholder=\\\"Docket number\\\"\u001b[39m\n\u001b[31m          ${autoAccepted ? 'style=\\\"opacity:.6\\\"' : ''}\u001b[39m\n\u001b[31m          oninput=\\\"slipOCRData[${i}].docket=this.value;slipOCRData[${i}].confidence=0.5\\\">\u001b[39m\n\u001b[31m        <div class=\\\"ocr-slip-status\\\">\u001b[39m\n\u001b[31m          <span class=\\\"ocr-confidence ${confClass}\\\">${confLabel}</span>\u001b[39m\n\u001b[31m          <span style=\\\"color:var(--t4);font-family:var(--fnt-mono);font-size:.6rem;\\\">via ${s.source}</span>\u001b[39m\n\u001b[31m          <button class=\\\"ocr-ai-btn\\\" onclick=\\\"reOCR(${i})\\\">🔄 Re-OCR</button>\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m    </div>`;\u001b[39m\n\u001b[31m  }).join('');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31masync function reOCR(i) {\u001b[39m\n\u001b[31m  const s = slipOCRData[i];\u001b[39m\n\u001b[31m  showOCRWorking(true, `Re-running OCR on ${s.filename}...`);\u001b[39m\n\u001b[31m  try {\u001b[39m\n\u001b[31m    const result = await window.runOCROnImage(s.imageData, s.filename);\u001b[39m\n\u001b[31m    slipOCRData[i] = { ...s, ...result };\u001b[39m\n\u001b[31m    renderOCRResults();\u001b[39m\n\u001b[31m    showToast('Re-OCR complete', 'ok');\u001b[39m\n\u001b[31m  } catch(e) {\u001b[39m\n\u001b[31m    showToast('OCR failed', 'err');\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m  showOCRWorking(false);\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction clearSlips() {\u001b[39m\n\u001b[31m  slipOCRData = [];\u001b[39m\n\u001b[31m  document.getElementById('ocrResults').classList.remove('visible');\u001b[39m\n\u001b[31m  document.getElementById('ocrSlipList').innerHTML = '';\u001b[39m\n\u001b[31m  const sum = document.getElementById('ocrSummary');\u001b[39m\n\u001b[31m  if (sum) { sum.style.display = 'none'; sum.innerHTML = ''; }\u001b[39m\n\u001b[31m  document.getElementById('shareTargetInfo').style.display = 'none';\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction applyAllDockets() {\u001b[39m\n\u001b[31m  // Match each OCR result to a consignee by order or by name matching\u001b[39m\n\u001b[31m  const consList = document.getElementById('consList');\u001b[39m\n\u001b[31m  const cards = consList.querySelectorAll('.cons-card');\u001b[39m\n\u001b[31m  slipOCRData.forEach((s, i) => {\u001b[39m\n\u001b[31m    if (!s.docket) return;\u001b[39m\n\u001b[31m    const card = cards[i];\u001b[39m\n\u001b[31m    if (card) {\u001b[39m\n\u001b[31m      const docketInput = card.querySelector('.cons-docket-input');\u001b[39m\n\u001b[31m      if (docketInput) {\u001b[39m\n\u001b[31m        docketInput.value = s.docket;\u001b[39m\n\u001b[31m        showLinkedDocket(card, s.docket);\u001b[39m\n\u001b[31m      }\u001b[39m\n\u001b[31m    }\u001b[39m\n\u001b[31m  });\u001b[39m\n\u001b[31m  showToast(`Applied ${slipOCRData.filter(s=>s.docket).length} dockets ✓`, 'ok');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31m// STORE LIST — with column mapping UI\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31mlet _rawRows = [];      // raw rows from file, kept for re-mapping\u001b[39m\n\u001b[31mlet _rawHeaders = [];   // headers from file·\u001b[39m\n\u001b[31masync function handleStoreFile(input) {\u001b[39m\n\u001b[31m  const file = input.files[0];\u001b[39m\n\u001b[31m  if (!file) return;\u001b[39m\n\u001b[31m  showToast('Reading file...', 'info');·\u001b[39m\n\u001b[31m  const data = await readFileAsDataURL(file);\u001b[39m\n\u001b[31m  const base64 = data.split(',')[1];\u001b[39m\n\u001b[31m  const binary = atob(base64);\u001b[39m\n\u001b[31m  const bytes = new Uint8Array(binary.length);\u001b[39m\n\u001b[31m  for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);·\u001b[39m\n\u001b[31m  let rows = [];\u001b[39m\n\u001b[31m  try {\u001b[39m\n\u001b[31m    if (file.name.endsWith('.csv')) {\u001b[39m\n\u001b[31m      const text = new TextDecoder().decode(bytes);\u001b[39m\n\u001b[31m      rows = text.split('\\\\n').map(r => r.split(',').map(c => c.trim().replace(/^\\\"|\\\"$/g, '')));\u001b[39m\n\u001b[31m    } else {\u001b[39m\n\u001b[31m      const wb = XLSX.read(bytes, { type: 'array' });\u001b[39m\n\u001b[31m      const ws = wb.Sheets[wb.SheetNames[0]];\u001b[39m\n\u001b[31m      rows = XLSX.utils.sheet_to_json(ws, { header: 1 });\u001b[39m\n\u001b[31m    }\u001b[39m\n\u001b[31m  } catch(e) { showToast('File read error: ' + e.message, 'err'); return; }·\u001b[39m\n\u001b[31m  if (!rows.length || !rows[0]) { showToast('File appears empty', 'err'); return; }·\u001b[39m\n\u001b[31m  _rawHeaders = (rows[0] || []).map(h => (h || '').toString().trim());\u001b[39m\n\u001b[31m  _rawRows = rows.slice(1).filter(r => r && r.some(c => c !== '' && c != null));·\u001b[39m\n\u001b[31m  document.getElementById('storeTableArea').style.display = 'block';\u001b[39m\n\u001b[31m  renderMappingUI(file.name);\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// ── COLUMN MAPPING FIELDS ─────────────────────────────\u001b[39m\n\u001b[31mconst MAPPING_FIELDS = [\u001b[39m\n\u001b[31m  { key: 'name',    label: 'Store / Partner Name', required: true,\u001b[39m\n\u001b[31m    keywords: ['store','shop','name','outlet','consignee','destination','partner','dealer','client','retailer'] },\u001b[39m\n\u001b[31m  { key: 'city',    label: 'City / Location',       required: false,\u001b[39m\n\u001b[31m    keywords: ['city','town','district','location','area','region','place'] },\u001b[39m\n\u001b[31m  { key: 'phone',   label: 'Phone / Mobile',        required: false,\u001b[39m\n\u001b[31m    keywords: ['phone','mobile','contact','tel','number','mob'] },\u001b[39m\n\u001b[31m  { key: 'docket',  label: 'Docket / AWB No.',      required: false,\u001b[39m\n\u001b[31m    keywords: ['docket','awb','tracking','courier','dkt','consignment','cno','waybill'] },\u001b[39m\n\u001b[31m  { key: 'address', label: 'Address',               required: false,\u001b[39m\n\u001b[31m    keywords: ['address','addr','street','locality','area'] },\u001b[39m\n\u001b[31m  { key: 'pincode', label: 'Pincode / PIN',          required: false,\u001b[39m\n\u001b[31m    keywords: ['pin','pincode','postal','zip','code'] },\u001b[39m\n\u001b[31m];·\u001b[39m\n\u001b[31m// Saved mapping key based on column fingerprint (so same client file auto-maps)\u001b[39m\n\u001b[31mfunction getMappingKey() {\u001b[39m\n\u001b[31m  return 'colmap_' + _rawHeaders.join('|').toLowerCase().replace(/\\\\s+/g,'');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// ── Detect if a column is a combined/multiline address cell ─────────────\u001b[39m\n\u001b[31mfunction isCombinedColumn(colIdx) {\u001b[39m\n\u001b[31m  if (colIdx < 0) return false;\u001b[39m\n\u001b[31m  // Check first 3 data rows — if any cell contains newline or has 6+ digit number (pincode) = combined\u001b[39m\n\u001b[31m  const samples = _rawRows.slice(0, 3).map(r => (r[colIdx] || '').toString());\u001b[39m\n\u001b[31m  return samples.some(s => s.includes('\\\\n') || /\\\\b\\\\d{6}\\\\b/.test(s) || s.split(',').length >= 3);\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction renderMappingUI(filename) {\u001b[39m\n\u001b[31m  const saved = (() => { try { return JSON.parse(localStorage.getItem(getMappingKey()) || 'null'); } catch(e) { return null; } })();\u001b[39m\n\u001b[31m  const isSaved = !!saved;\u001b[39m\n\u001b[31m  const colInfoEl = document.getElementById('storeColInfo');\u001b[39m\n\u001b[31m  colInfoEl.style.display = 'block';·\u001b[39m\n\u001b[31m  const suggested = {};\u001b[39m\n\u001b[31m  MAPPING_FIELDS.forEach(f => {\u001b[39m\n\u001b[31m    const idx = findCol(_rawHeaders, f.keywords);\u001b[39m\n\u001b[31m    suggested[f.key] = saved ? (saved[f.key] ?? idx) : idx;\u001b[39m\n\u001b[31m  });·\u001b[39m\n\u001b[31m  // Detect if address column is combined\u001b[39m\n\u001b[31m  const addrIdx = suggested.address >= 0 ? suggested.address : findCol(_rawHeaders, ['address','addr']);\u001b[39m\n\u001b[31m  const looksCombo = isCombinedColumn(addrIdx) || isCombinedColumn(suggested.name);\u001b[39m\n\u001b[31m  const comboCol   = looksCombo ? (addrIdx >= 0 ? addrIdx : suggested.name) : -1;\u001b[39m\n\u001b[31m  const comboPreview = comboCol >= 0 && _rawRows[0] ? (_rawRows[0][comboCol] || '').toString().slice(0, 120) : '';·\u001b[39m\n\u001b[31m  colInfoEl.innerHTML = `\u001b[39m\n\u001b[31m    <div style=\\\"background:var(--sur2);border:1px solid var(--bdr);border-radius:var(--r-sm);padding:12px;\\\">\u001b[39m\n\u001b[31m      <div style=\\\"display:flex;align-items:center;justify-content:space-between;margin-bottom:10px;\\\">\u001b[39m\n\u001b[31m        <div style=\\\"font-family:var(--fnt-mono);font-size:.68rem;color:var(--sky);\\\">\u001b[39m\n\u001b[31m          📋 ${filename} &nbsp;·&nbsp; ${_rawRows.length} rows &nbsp;·&nbsp; ${_rawHeaders.length} columns\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m        ${isSaved ? '<span class=\\\"map-saved-badge\\\">✓ mapping remembered</span>' : ''}\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      <div style=\\\"font-size:.68rem;color:var(--t3);font-family:var(--fnt-mono);margin-bottom:10px;word-break:break-all;\\\">\u001b[39m\n\u001b[31m        Columns: <span style=\\\"color:var(--t2);\\\">${_rawHeaders.map((h,i)=>`[${i+1}] ${h||'?'}`).join(' · ')}</span>\u001b[39m\n\u001b[31m      </div>·\u001b[39m\n\u001b[31m      ${looksCombo ? `\u001b[39m\n\u001b[31m      <!-- COMBINED COLUMN DETECTED -->\u001b[39m\n\u001b[31m      <div style=\\\"background:rgba(56,189,248,.08);border:1px solid rgba(56,189,248,.3);\u001b[39m\n\u001b[31m        border-radius:var(--r-sm);padding:10px 12px;margin-bottom:10px;\\\">\u001b[39m\n\u001b[31m        <div style=\\\"font-size:.72rem;color:var(--sky);font-weight:600;margin-bottom:6px;\\\">\u001b[39m\n\u001b[31m          🤖 Combined Address Column Detected\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m        <div style=\\\"font-size:.68rem;color:var(--t2);font-family:var(--fnt-mono);\u001b[39m\n\u001b[31m          white-space:pre-wrap;background:var(--bg);padding:6px 8px;border-radius:4px;\u001b[39m\n\u001b[31m          margin-bottom:8px;max-height:80px;overflow:hidden;\\\">${comboPreview}</div>\u001b[39m\n\u001b[31m        <div style=\\\"font-size:.7rem;color:var(--t3);margin-bottom:8px;\\\">\u001b[39m\n\u001b[31m          AI will extract: Name · Address · City · Pincode · Phone from each cell.\u001b[39m\n\u001b[31m          Runs in browser (Transformers.js) with Ollama fallback.\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m        <div style=\\\"display:flex;align-items:center;gap:8px;\\\">\u001b[39m\n\u001b[31m          <label style=\\\"display:flex;align-items:center;gap:6px;font-size:.72rem;color:var(--t1);cursor:pointer;\\\">\u001b[39m\n\u001b[31m            <input type=\\\"checkbox\\\" id=\\\"useAIParse\\\" checked style=\\\"cursor:pointer;\\\">\u001b[39m\n\u001b[31m            Use AI to parse combined column\u001b[39m\n\u001b[31m          </label>\u001b[39m\n\u001b[31m          <select id=\\\"comboColSel\\\" style=\\\"background:var(--sur2);border:1px solid var(--bdr);\u001b[39m\n\u001b[31m            color:var(--t1);font-family:var(--fnt-mono);font-size:.72rem;padding:4px 8px;\u001b[39m\n\u001b[31m            border-radius:var(--r-sm);outline:none;\\\">\u001b[39m\n\u001b[31m            ${_rawHeaders.map((h,i)=>`<option value=\\\"${i}\\\" ${i===comboCol?'selected':''}>${h||'(col '+(i+1)+')'}</option>`).join('')}\u001b[39m\n\u001b[31m          </select>\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      ` : ''}·\u001b[39m\n\u001b[31m      <div id=\\\"manualMapSection\\\" style=\\\"${looksCombo ? 'display:none;' : ''}\\\">\u001b[39m\n\u001b[31m        <div style=\\\"font-size:.68rem;color:var(--t3);margin-bottom:6px;\\\">\u001b[39m\n\u001b[31m          ${looksCombo ? 'Manual mapping (used when AI parse is off, or for other columns):' : 'Map each field to your column:'}\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m        <table class=\\\"map-table\\\">\u001b[39m\n\u001b[31m          <thead><tr><th>Field</th><th>Your column</th><th>Preview (row 1)</th></tr></thead>\u001b[39m\n\u001b[31m          <tbody>\u001b[39m\n\u001b[31m            ${MAPPING_FIELDS.map(f => {\u001b[39m\n\u001b[31m              const selIdx = suggested[f.key] ?? -1;\u001b[39m\n\u001b[31m              const preview = selIdx >= 0 && _rawRows[0] ? ((_rawRows[0][selIdx]||'').toString().slice(0,40)) : '—';\u001b[39m\n\u001b[31m              return `<tr>\u001b[39m\n\u001b[31m                <td style=\\\"white-space:nowrap;color:var(--t1);font-weight:500;font-size:.75rem;\\\">\u001b[39m\n\u001b[31m                  ${f.label}${f.required ? '<span class=\\\"map-req\\\">*</span>' : ''}\u001b[39m\n\u001b[31m                </td>\u001b[39m\n\u001b[31m                <td>\u001b[39m\n\u001b[31m                  <select id=\\\"mapsel_${f.key}\\\" onchange=\\\"updateMappingPreview('${f.key}')\\\">\u001b[39m\n\u001b[31m                    ${_rawHeaders.map((h,i)=>\u001b[39m\n\u001b[31m                      `<option value=\\\"${i}\\\" ${i===selIdx?'selected':''}>${h||'(col '+(i+1)+')'}</option>`\u001b[39m\n\u001b[31m                    ).join('')}\u001b[39m\n\u001b[31m                    <option value=\\\"-1\\\" ${selIdx===-1?'selected':''}>— not in file —</option>\u001b[39m\n\u001b[31m                  </select>\u001b[39m\n\u001b[31m                </td>\u001b[39m\n\u001b[31m                <td id=\\\"mapprev_${f.key}\\\" style=\\\"font-family:var(--fnt-mono);font-size:.68rem;\u001b[39m\n\u001b[31m                  color:var(--t2);max-width:90px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;\\\">\u001b[39m\n\u001b[31m                  ${preview}\u001b[39m\n\u001b[31m                </td>\u001b[39m\n\u001b[31m              </tr>`;\u001b[39m\n\u001b[31m            }).join('')}\u001b[39m\n\u001b[31m          </tbody>\u001b[39m\n\u001b[31m        </table>\u001b[39m\n\u001b[31m      </div>·\u001b[39m\n\u001b[31m      <!-- Also map docket/courier no column separately — always shown -->\u001b[39m\n\u001b[31m      <div style=\\\"margin-top:10px;padding-top:10px;border-top:1px solid var(--bdr);\\\">\u001b[39m\n\u001b[31m        <div style=\\\"font-size:.68rem;color:var(--t3);margin-bottom:6px;font-family:var(--fnt-mono);\\\">\u001b[39m\n\u001b[31m          DOCKET / COURIER NO. COLUMN (required for tracking):\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m        <div style=\\\"display:flex;gap:8px;align-items:center;\\\">\u001b[39m\n\u001b[31m          <select id=\\\"docketColSel\\\" style=\\\"background:var(--sur2);border:1px solid var(--bdr);\u001b[39m\n\u001b[31m            color:var(--acc);font-family:var(--fnt-mono);font-size:.78rem;padding:6px 10px;\u001b[39m\n\u001b[31m            border-radius:var(--r-sm);outline:none;flex:1;\\\">\u001b[39m\n\u001b[31m            <option value=\\\"-1\\\">— Docket not in file —</option>\u001b[39m\n\u001b[31m            ${_rawHeaders.map((h,i)=>`<option value=\\\"${i}\\\" ${i===(suggested.docket??-1)?'selected':''}>${h||'(col '+(i+1)+')'}</option>`).join('')}\u001b[39m\n\u001b[31m          </select>\u001b[39m\n\u001b[31m          <span id=\\\"docketPrev\\\" style=\\\"font-family:var(--fnt-mono);font-size:.72rem;color:var(--acc);\\\">\u001b[39m\n\u001b[31m            ${suggested.docket >= 0 && _rawRows[0] ? (_rawRows[0][suggested.docket]||'') : ''}\u001b[39m\n\u001b[31m          </span>\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m      </div>·\u001b[39m\n\u001b[31m      <div class=\\\"btn-row\\\" style=\\\"margin-top:12px;\\\">\u001b[39m\n\u001b[31m        <button class=\\\"btn btn-ghost\\\" style=\\\"font-size:.78rem;\\\" onclick=\\\"clearStores()\\\">✕ Cancel</button>\u001b[39m\n\u001b[31m        <button class=\\\"btn btn-orange\\\" style=\\\"font-size:.78rem;\\\" onclick=\\\"applyMapping()\\\">✓ Apply Mapping</button>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m  `;\u001b[39m\n\u001b[31m  document.getElementById('storeTable').innerHTML = '';\u001b[39m\n\u001b[31m  document.getElementById('storeAddPanel').style.display = 'none';·\u001b[39m\n\u001b[31m  // Update docket preview on change\u001b[39m\n\u001b[31m  const docketSel = document.getElementById('docketColSel');\u001b[39m\n\u001b[31m  if (docketSel) docketSel.addEventListener('change', () => {\u001b[39m\n\u001b[31m    const idx = parseInt(docketSel.value);\u001b[39m\n\u001b[31m    const prev = document.getElementById('docketPrev');\u001b[39m\n\u001b[31m    if (prev) prev.textContent = idx >= 0 && _rawRows[0] ? (_rawRows[0][idx]||'') : '';\u001b[39m\n\u001b[31m  });·\u001b[39m\n\u001b[31m  // Toggle manual map opacity when AI checkbox changes\u001b[39m\n\u001b[31m  const aiChk = document.getElementById('useAIParse');\u001b[39m\n\u001b[31m  const manSec = document.getElementById('manualMapSection');\u001b[39m\n\u001b[31m  if (aiChk && manSec) {\u001b[39m\n\u001b[31m    aiChk.addEventListener('change', () => {\u001b[39m\n\u001b[31m      manSec.style.display = aiChk.checked ? 'none' : 'block';\u001b[39m\n\u001b[31m    });\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction updateMappingPreview(key) {\u001b[39m\n\u001b[31m  const sel = document.getElementById('mapsel_' + key);\u001b[39m\n\u001b[31m  if (!sel) return;\u001b[39m\n\u001b[31m  const idx = parseInt(sel.value);\u001b[39m\n\u001b[31m  const prev = document.getElementById('mapprev_' + key);\u001b[39m\n\u001b[31m  if (prev) prev.textContent = (idx >= 0 && _rawRows[0]) ? ((_rawRows[0][idx]||'').toString().slice(0,40)) : '—';\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// ── REGEX-FIRST ADDRESS PARSER ───────────────────────────────────────────\u001b[39m\n\u001b[31m// Improved Indian address parser\u001b[39m\n\u001b[31m// Handles formats seen in real BTL dispatch files\u001b[39m\n\u001b[31mfunction parseAddressRegex(raw) {\u001b[39m\n\u001b[31m  const text = raw.toString().replace(/\\\\r/g, '').trim();\u001b[39m\n\u001b[31m  const lines = text.split('\\\\n').map(l => l.trim()).filter(Boolean);\u001b[39m\n\u001b[31m  const result = { name:'', address:'', city:'', pincode:'', phone:'', confidence:0 };·\u001b[39m\n\u001b[31m  // ── PHONE: Indian mobile 6-9 prefix, 10 digits ──────────────────────────\u001b[39m\n\u001b[31m  // Also handles \\\"Phone : 9876543210\\\", \\\"M.No.-9876543210\\\", \\\"Mobile - 9876543210\\\"\u001b[39m\n\u001b[31m  const phoneMatch = text.match(/(?:Phone|Mobile|Mob|M\\\\.No\\\\.?|Ph\\\\.?)?\\\\s*[:\\\\-]?\\\\s*\\\\b([6-9]\\\\d{9})\\\\b/i);\u001b[39m\n\u001b[31m  if (phoneMatch) result.phone = phoneMatch[1];·\u001b[39m\n\u001b[31m  // ── PINCODE: 6-digit standalone number ──────────────────────────────────\u001b[39m\n\u001b[31m  // Must not be part of a longer number (e.g. not an account number)\u001b[39m\n\u001b[31m  const pinMatch = text.match(/(?<![\\\\d])([1-9]\\\\d{5})(?![\\\\d])/);\u001b[39m\n\u001b[31m  if (pinMatch) result.pincode = pinMatch[1];·\u001b[39m\n\u001b[31m  // ── CITY: the token immediately before the pincode ───────────────────────\u001b[39m\n\u001b[31m  const indianStates = ['rajasthan','gujarat','maharashtra','punjab','haryana',\u001b[39m\n\u001b[31m    'uttar pradesh','madhya pradesh','karnataka','tamil nadu','andhra pradesh',\u001b[39m\n\u001b[31m    'telangana','kerala','west bengal','bihar','odisha','jharkhand','assam',\u001b[39m\n\u001b[31m    'himachal pradesh','uttarakhand','chhattisgarh','goa','delhi','chandigarh',\u001b[39m\n\u001b[31m    'raj.','raj'];\u001b[39m\n\u001b[31m  if (result.pincode) {\u001b[39m\n\u001b[31m    const pinPos = text.indexOf(result.pincode);\u001b[39m\n\u001b[31m    const beforePin = text.slice(0, pinPos);\u001b[39m\n\u001b[31m    // Strategy 1: split by comma/newline, walk backwards skipping state names\u001b[39m\n\u001b[31m    const chunks = beforePin.split(/[,\\\\n]/).map(c => c.trim()).filter(Boolean);\u001b[39m\n\u001b[31m    for (let i = chunks.length - 1; i >= 0; i--) {\u001b[39m\n\u001b[31m      const chunk = chunks[i].replace(/\\\\d+/g,'').replace(/[()]/g,'').replace(/[.-]/g,'').trim();\u001b[39m\n\u001b[31m      const lower = chunk.toLowerCase();\u001b[39m\n\u001b[31m      if (!chunk || chunk.length < 2) continue;\u001b[39m\n\u001b[31m      if (indianStates.includes(lower)) continue;\u001b[39m\n\u001b[31m      // Skip chunks that look like addresses (contain numbers or long strings)\u001b[39m\n\u001b[31m      if (/\\\\d/.test(chunk)) continue;\u001b[39m\n\u001b[31m      result.city = chunk;\u001b[39m\n\u001b[31m      break;\u001b[39m\n\u001b[31m    }\u001b[39m\n\u001b[31m    // Strategy 2: if still no city, look for last capitalised word before pincode\u001b[39m\n\u001b[31m    // handles \\\"compound paota Jodhpur 342006\\\" → Jodhpur\u001b[39m\n\u001b[31m    if (!result.city) {\u001b[39m\n\u001b[31m      const wordsBefore = beforePin.replace(/[,\\\\n]/g,' ').trim().split(/\\\\s+/);\u001b[39m\n\u001b[31m      for (let i = wordsBefore.length - 1; i >= 0; i--) {\u001b[39m\n\u001b[31m        const w = wordsBefore[i].replace(/[^a-zA-Z]/g,'');\u001b[39m\n\u001b[31m        if (w.length < 3) continue;\u001b[39m\n\u001b[31m        if (indianStates.includes(w.toLowerCase())) continue;\u001b[39m\n\u001b[31m        // Prefer words that start with capital letter (likely a proper noun/city)\u001b[39m\n\u001b[31m        if (/^[A-Z]/.test(w)) { result.city = w; break; }\u001b[39m\n\u001b[31m      }\u001b[39m\n\u001b[31m    }\u001b[39m\n\u001b[31m  }·\u001b[39m\n\u001b[31m  // ── NAME: first meaningful line, extract shop name ───────────────────────\u001b[39m\n\u001b[31m  // Patterns:\u001b[39m\n\u001b[31m  //   \\\"Sanwar Mal Saini, C/o R G COMPUTERS\\\" → name = \\\"R G COMPUTERS\\\"\u001b[39m\n\u001b[31m  //   \\\"SANWAR MAL SAINI, C/o ADVANCE TECHNOLOGIES\\\" → name = \\\"ADVANCE TECHNOLOGIES\\\"\u001b[39m\n\u001b[31m  //   \\\"Sunil Vaishnav\\\" → name = \\\"Sunil Vaishnav\\\" (no C/o)\u001b[39m\n\u001b[31m  //   \\\"Baltej Singh, Professional computer care\\\" → name = \\\"Professional computer care\\\"\u001b[39m\n\u001b[31m  if (lines.length > 0) {\u001b[39m\n\u001b[31m    const firstLine = lines[0];\u001b[39m\n\u001b[31m    // Check for C/o or C/O pattern — shop name comes after it\u001b[39m\n\u001b[31m    const coMatch = firstLine.match(/C\\\\/?[Oo]\\\\.?\\\\s+(.+)/);\u001b[39m\n\u001b[31m    if (coMatch) {\u001b[39m\n\u001b[31m      result.name = coMatch[1].trim();\u001b[39m\n\u001b[31m    } else {\u001b[39m\n\u001b[31m      // Check if there is a comma — second part might be the shop name\u001b[39m\n\u001b[31m      const parts = firstLine.split(',').map(p => p.trim());\u001b[39m\n\u001b[31m      if (parts.length >= 2 && parts[1].length > 3) {\u001b[39m\n\u001b[31m        // Heuristic: if second part looks like a business name (has uppercase or >5 chars)\u001b[39m\n\u001b[31m        const hasUpperOrLong = /[A-Z]{2,}/.test(parts[1]) || parts[1].length > 8;\u001b[39m\n\u001b[31m        result.name = hasUpperOrLong ? parts[1] : parts[0];\u001b[39m\n\u001b[31m      } else {\u001b[39m\n\u001b[31m        result.name = parts[0];\u001b[39m\n\u001b[31m      }\u001b[39m\n\u001b[31m    }\u001b[39m\n\u001b[31m  }·\u001b[39m\n\u001b[31m  // ── ADDRESS: lines between name and phone/pincode line ───────────────────\u001b[39m\n\u001b[31m  const phoneLineIdx = lines.findIndex(l => result.phone && l.includes(result.phone));\u001b[39m\n\u001b[31m  const addrLines = lines.filter((l, i) => {\u001b[39m\n\u001b[31m    if (i === 0) return false;\u001b[39m\n\u001b[31m    if (i === phoneLineIdx) return false;\u001b[39m\n\u001b[31m    // Exclude lines that are just the phone number\u001b[39m\n\u001b[31m    if (/^[6-9]\\\\d{9}$/.test(l.replace(/\\\\s/g,''))) return false;\u001b[39m\n\u001b[31m    return true;\u001b[39m\n\u001b[31m  });\u001b[39m\n\u001b[31m  result.address = addrLines.join(', ').replace(/,\\\\s*,/g, ',').trim();·\u001b[39m\n\u001b[31m  // ── CONFIDENCE: simple score based on what we got ────────────────────────\u001b[39m\n\u001b[31m  let conf = 0;\u001b[39m\n\u001b[31m  if (result.name)    conf += 30;\u001b[39m\n\u001b[31m  if (result.city)    conf += 25;\u001b[39m\n\u001b[31m  if (result.pincode) conf += 25;\u001b[39m\n\u001b[31m  if (result.phone)   conf += 20;\u001b[39m\n\u001b[31m  result.confidence = conf;·\u001b[39m\n\u001b[31m  return result;\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// ── AI ADDRESS PARSER (Ollama fallback) ──────────────────────────────────\u001b[39m\n\u001b[31m// Fetches past corrections from localStorage to improve prompt accuracy\u001b[39m\n\u001b[31mfunction getPastCorrections() {\u001b[39m\n\u001b[31m  try {\u001b[39m\n\u001b[31m    const log = JSON.parse(localStorage.getItem('360lm_parse_corrections') || '[]');\u001b[39m\n\u001b[31m    return log.slice(-10); // last 10 corrections as examples\u001b[39m\n\u001b[31m  } catch(e) { return []; }\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction saveCorrectionLocal(entry) {\u001b[39m\n\u001b[31m  try {\u001b[39m\n\u001b[31m    const log = JSON.parse(localStorage.getItem('360lm_parse_corrections') || '[]');\u001b[39m\n\u001b[31m    log.push(entry);\u001b[39m\n\u001b[31m    // Keep only last 50\u001b[39m\n\u001b[31m    localStorage.setItem('360lm_parse_corrections', JSON.stringify(log.slice(-50)));\u001b[39m\n\u001b[31m  } catch(e) {}\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31masync function parseAddressWithOllama(raw) {\u001b[39m\n\u001b[31m  // Calls the server-side /dispatch-ai/address proxy (avoids CORS + port 11434 issues)\u001b[39m\n\u001b[31m  try {\u001b[39m\n\u001b[31m    const resp = await fetch('/dispatch-ai/address', {\u001b[39m\n\u001b[31m      method: 'POST',\u001b[39m\n\u001b[31m      headers: { 'Content-Type': 'application/json' },\u001b[39m\n\u001b[31m      body: JSON.stringify({ text: raw }),\u001b[39m\n\u001b[31m      signal: AbortSignal.timeout(120000)\u001b[39m\n\u001b[31m    });\u001b[39m\n\u001b[31m    const data = await resp.json();\u001b[39m\n\u001b[31m    if (data.success) return data;\u001b[39m\n\u001b[31m  } catch(e) {}\u001b[39m\n\u001b[31m  return null;\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31masync function parseAddressCell(raw) {\u001b[39m\n\u001b[31m  // Step 1: regex — always runs first, no network call\u001b[39m\n\u001b[31m  const regex = parseAddressRegex(raw);\u001b[39m\n\u001b[31m  if (regex.name) return { ...regex, source: 'regex' };·\u001b[39m\n\u001b[31m  // Step 2: Ollama only if regex got nothing AND Ollama is configured\u001b[39m\n\u001b[31m  // Uses a 3-second timeout to prevent browser hang\u001b[39m\n\u001b[31m  if (window.appConfig.ocr_mode !== 'transformers') {\u001b[39m\n\u001b[31m    try {\u001b[39m\n\u001b[31m      const ai = await Promise.race([\u001b[39m\n\u001b[31m        parseAddressWithOllama(raw),\u001b[39m\n\u001b[31m        new Promise((_, reject) => setTimeout(() => reject(new Error('timeout')), 120000))\u001b[39m\n\u001b[31m      ]);\u001b[39m\n\u001b[31m      if (ai && ai.name) return { ...ai, source: 'ollama' };\u001b[39m\n\u001b[31m    } catch(e) { /* timeout or error — continue to fallback */ }\u001b[39m\n\u001b[31m  }·\u001b[39m\n\u001b[31m  // Step 3: fallback — first line as name\u001b[39m\n\u001b[31m  const lines = raw.toString().split('\\\\n').map(l=>l.trim()).filter(Boolean);\u001b[39m\n\u001b[31m  return { name: lines[0]||raw.slice(0,40), address: raw, city:'', pincode:'', phone:'', source:'fallback' };\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31masync function applyMapping() {\u001b[39m\n\u001b[31m  const useAI   = document.getElementById('useAIParse')?.checked ?? false;\u001b[39m\n\u001b[31m  const comboSel = document.getElementById('comboColSel');\u001b[39m\n\u001b[31m  const comboIdx = comboSel ? parseInt(comboSel.value) : -1;\u001b[39m\n\u001b[31m  const docketIdx = parseInt(document.getElementById('docketColSel')?.value ?? -1);·\u001b[39m\n\u001b[31m  // Build manual mapping as fallback\u001b[39m\n\u001b[31m  const mapping = {};\u001b[39m\n\u001b[31m  MAPPING_FIELDS.forEach(f => {\u001b[39m\n\u001b[31m    const sel = document.getElementById('mapsel_' + f.key);\u001b[39m\n\u001b[31m    mapping[f.key] = sel ? parseInt(sel.value) : -1;\u001b[39m\n\u001b[31m  });\u001b[39m\n\u001b[31m  // Override docket from dedicated selector\u001b[39m\n\u001b[31m  mapping.docket = docketIdx;·\u001b[39m\n\u001b[31m  // Determine if we have a valid base for stores\u001b[39m\n\u001b[31m  const nameIdx = useAI && comboIdx >= 0 ? comboIdx : mapping.name;\u001b[39m\n\u001b[31m  if (nameIdx === -1 && !useAI) { showToast('Store Name column is required', 'err'); return; }·\u001b[39m\n\u001b[31m  // Save mapping\u001b[39m\n\u001b[31m  try { localStorage.setItem(getMappingKey(), JSON.stringify({...mapping, _useAI: useAI, _comboIdx: comboIdx})); } catch(e) {}·\u001b[39m\n\u001b[31m  if (useAI && comboIdx >= 0) {\u001b[39m\n\u001b[31m    // AI path — parse each combined cell\u001b[39m\n\u001b[31m    showToast('AI parsing addresses...', 'info');\u001b[39m\n\u001b[31m    showOCRWorking(true, `Parsing 0/${_rawRows.length} addresses...`);\u001b[39m\n\u001b[31m    stores = [];\u001b[39m\n\u001b[31m    for (let i = 0; i < _rawRows.length; i++) {\u001b[39m\n\u001b[31m      const r = _rawRows[i];\u001b[39m\n\u001b[31m      const raw = (r[comboIdx] || '').toString().trim();\u001b[39m\n\u001b[31m      if (!raw) continue;\u001b[39m\n\u001b[31m      showOCRWorking(true, `Parsing ${i+1}/${_rawRows.length} — ${raw.slice(0,30)}...`);\u001b[39m\n\u001b[31m      // Yield to browser every row to prevent UI freeze\u001b[39m\n\u001b[31m      await new Promise(r => setTimeout(r, 0));\u001b[39m\n\u001b[31m      const parsed = await parseAddressCell(raw);\u001b[39m\n\u001b[31m      stores.push({\u001b[39m\n\u001b[31m        id: i,\u001b[39m\n\u001b[31m        name:    parsed.name    || raw.slice(0,40),\u001b[39m\n\u001b[31m        city:    parsed.city    || '',\u001b[39m\n\u001b[31m        phone:   parsed.phone   || '',\u001b[39m\n\u001b[31m        address: parsed.address || raw,\u001b[39m\n\u001b[31m        pincode: parsed.pincode || '',\u001b[39m\n\u001b[31m        docket:  docketIdx >= 0 ? (r[docketIdx]||'').toString().trim() : '',\u001b[39m\n\u001b[31m        _parseSource: parsed.source,\u001b[39m\n\u001b[31m        _confidence:  parsed.confidence || 0,\u001b[39m\n\u001b[31m        _rawCell:     raw,\u001b[39m\n\u001b[31m        _origName:    parsed.name || '',\u001b[39m\n\u001b[31m        _origCity:    parsed.city || '',\u001b[39m\n\u001b[31m        matched: false\u001b[39m\n\u001b[31m      });\u001b[39m\n\u001b[31m    }\u001b[39m\n\u001b[31m    showOCRWorking(false);\u001b[39m\n\u001b[31m  } else {\u001b[39m\n\u001b[31m    // Manual mapping path\u001b[39m\n\u001b[31m    stores = _rawRows\u001b[39m\n\u001b[31m      .filter(r => r[nameIdx] && (r[nameIdx]+'').trim())\u001b[39m\n\u001b[31m      .map((r, i) => ({\u001b[39m\n\u001b[31m        id: i,\u001b[39m\n\u001b[31m        name:    mapping.name    >= 0 ? (r[mapping.name]   ||'').toString().trim() : '',\u001b[39m\n\u001b[31m        city:    mapping.city    >= 0 ? (r[mapping.city]   ||'').toString().trim() : '',\u001b[39m\n\u001b[31m        phone:   mapping.phone   >= 0 ? (r[mapping.phone]  ||'').toString().trim() : '',\u001b[39m\n\u001b[31m        docket:  mapping.docket  >= 0 ? (r[mapping.docket] ||'').toString().trim() : '',\u001b[39m\n\u001b[31m        address: mapping.address >= 0 ? (r[mapping.address]||'').toString().trim() : '',\u001b[39m\n\u001b[31m        pincode: mapping.pincode >= 0 ? (r[mapping.pincode]||'').toString().trim() : '',\u001b[39m\n\u001b[31m        matched: false\u001b[39m\n\u001b[31m      }));\u001b[39m\n\u001b[31m  }·\u001b[39m\n\u001b[31m  document.getElementById('storeCount').textContent = `${stores.length} stores loaded`;·\u001b[39m\n\u001b[31m  if (useAI && comboIdx >= 0) {\u001b[39m\n\u001b[31m    // AI path — show row-by-row review panel before adding consignees\u001b[39m\n\u001b[31m    showAddressReviewPanel();\u001b[39m\n\u001b[31m  } else {\u001b[39m\n\u001b[31m    // Manual mapping path — go straight to store table + add panel\u001b[39m\n\u001b[31m    renderStoreTable(mapping.docket >= 0 || docketIdx >= 0);\u001b[39m\n\u001b[31m    const addPanel = document.getElementById('storeAddPanel');\u001b[39m\n\u001b[31m    const addBtn   = document.getElementById('storeAddBtn');\u001b[39m\n\u001b[31m    const addTitle = document.getElementById('storeAddTitle');\u001b[39m\n\u001b[31m    const hasDockets = stores.some(s => s.docket);\u001b[39m\n\u001b[31m    addTitle.textContent = hasDockets\u001b[39m\n\u001b[31m      ? `${stores.filter(s=>s.docket).length} stores with dockets — add as consignees?`\u001b[39m\n\u001b[31m      : `Add ${stores.length} stores as consignees?`;\u001b[39m\n\u001b[31m    addBtn.textContent = `✓ Add ${stores.length} as Consignees`;\u001b[39m\n\u001b[31m    addPanel.style.display = 'block';\u001b[39m\n\u001b[31m    showToast(`${stores.length} stores mapped ✓`, 'ok');\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31m// ADDRESS REVIEW PANEL\u001b[39m\n\u001b[31m// Row-by-row review of AI-parsed addresses before consignees created\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31mfunction showAddressReviewPanel() {\u001b[39m\n\u001b[31m  const pane = document.getElementById('addrReviewPane');\u001b[39m\n\u001b[31m  if (!pane) return;\u001b[39m\n\u001b[31m  renderAddressReviewRows();\u001b[39m\n\u001b[31m  pane.style.display = 'block';\u001b[39m\n\u001b[31m  pane.scrollTop = 0;\u001b[39m\n\u001b[31m  showToast(`Review ${stores.length} parsed addresses`, 'info');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction renderAddressReviewRows() {\u001b[39m\n\u001b[31m  const list = document.getElementById('addrReviewList');\u001b[39m\n\u001b[31m  const summary = document.getElementById('addrReviewSummary');\u001b[39m\n\u001b[31m  if (!list) return;·\u001b[39m\n\u001b[31m  const highConf  = stores.filter(s => (s._confidence||0) >= 80).length;\u001b[39m\n\u001b[31m  const medConf   = stores.filter(s => (s._confidence||0) >= 50 && (s._confidence||0) < 80).length;\u001b[39m\n\u001b[31m  const lowConf   = stores.filter(s => (s._confidence||0) < 50).length;·\u001b[39m\n\u001b[31m  summary.innerHTML = `\u001b[39m\n\u001b[31m    <div class=\\\"vs-item\\\"><div class=\\\"vs-val\\\" style=\\\"color:var(--t1);\\\">${stores.length}</div><div class=\\\"vs-lbl\\\">Total</div></div>\u001b[39m\n\u001b[31m    <div class=\\\"vs-item\\\"><div class=\\\"vs-val\\\" style=\\\"color:var(--grn);\\\">${highConf}</div><div class=\\\"vs-lbl\\\">High conf</div></div>\u001b[39m\n\u001b[31m    <div class=\\\"vs-item\\\"><div class=\\\"vs-val\\\" style=\\\"color:var(--ylw);\\\">${medConf}</div><div class=\\\"vs-lbl\\\">Review</div></div>\u001b[39m\n\u001b[31m    <div class=\\\"vs-item\\\"><div class=\\\"vs-val\\\" style=\\\"color:var(--red);\\\">${lowConf}</div><div class=\\\"vs-lbl\\\">Check</div></div>\u001b[39m\n\u001b[31m    <div class=\\\"vs-item\\\" style=\\\"margin-left:auto;\\\">\u001b[39m\n\u001b[31m      <button class=\\\"btn-sm primary\\\" onclick=\\\"approveAllAddresses()\\\" style=\\\"font-size:.7rem;\\\">✓ Approve All</button>\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m  `;·\u001b[39m\n\u001b[31m  list.innerHTML = stores.map((s, i) => {\u001b[39m\n\u001b[31m    const conf = s._confidence || 0;\u001b[39m\n\u001b[31m    const confClass = conf >= 80 ? 'vf-ok' : conf >= 50 ? 'vf-nodocket' : 'vf-dupe';\u001b[39m\n\u001b[31m    const confLabel = conf >= 80 ? `${conf}% ✓` : conf >= 50 ? `${conf}% review` : `${conf}% check`;\u001b[39m\n\u001b[31m    const rowBorder = conf >= 80 ? 'var(--grn)' : conf >= 50 ? 'var(--ylw)' : 'var(--red)';·\u001b[39m\n\u001b[31m    return `\u001b[39m\n\u001b[31m    <div class=\\\"verify-row\\\" id=\\\"arow_${i}\\\" style=\\\"border-left:3px solid ${rowBorder};\\\">\u001b[39m\n\u001b[31m      <div class=\\\"verify-row-hdr\\\">\u001b[39m\n\u001b[31m        <span class=\\\"verify-num\\\">#${i+1}</span>\u001b[39m\n\u001b[31m        <span class=\\\"verify-store\\\" style=\\\"font-size:.78rem;color:var(--t2);font-weight:400;\u001b[39m\n\u001b[31m          font-family:var(--fnt-mono);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;flex:1;\\\"\u001b[39m\n\u001b[31m          title=\\\"${(s._rawCell||'').replace(/\\\"/g,\\\"'\\\")}\\\">\u001b[39m\n\u001b[31m          ${(s._rawCell||'').slice(0,50).split('\\\\n').join(' ↵ ')}       <span class=\\\"verify-flag ${confClass}\\\" style=\\\"flex-shrink:0;margin-right:6px;\\\">${confLabel} · ${s._parseSource||'?'}</span>\u001b[39m\n\u001b[31m        <button onclick=\\\"deleteReviewRow(${i})\\\"\u001b[39m\n\u001b[31m          style=\\\"background:none;border:1px solid rgba(239,68,68,.4);color:var(--red);\u001b[39m\n\u001b[31m          font-size:.65rem;padding:2px 8px;border-radius:6px;cursor:pointer;\u001b[39m\n\u001b[31m          font-family:var(--fnt-mono);flex-shrink:0;transition:background .2s;\\\"\u001b[39m\n\u001b[31m          onmouseover=\\\"this.style.background='rgba(239,68,68,.15)'\\\"\u001b[39m\n\u001b[31m          onmouseout=\\\"this.style.background='none'\\\">✕ Remove</button>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      <div class=\\\"verify-fields\\\" style=\\\"grid-template-columns:1fr 1fr 1fr 1fr 1fr;\\\">\u001b[39m\n\u001b[31m        <div class=\\\"verify-field\\\">\u001b[39m\n\u001b[31m          <label>Shop / Name <span style=\\\"color:var(--acc);\\\">*</span></label>\u001b[39m\n\u001b[31m          <input type=\\\"text\\\" value=\\\"${s.name||''}\\\" id=\\\"ar_name_${i}\\\"\u001b[39m\n\u001b[31m            oninput=\\\"updateAddrField(${i},'name',this)\\\"\u001b[39m\n\u001b[31m            style=\\\"${!s.name ? 'border-color:var(--red);' : ''}\\\">\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m        <div class=\\\"verify-field\\\">\u001b[39m\n\u001b[31m          <label>City</label>\u001b[39m\n\u001b[31m          <input type=\\\"text\\\" value=\\\"${s.city||''}\\\" id=\\\"ar_city_${i}\\\"\u001b[39m\n\u001b[31m            oninput=\\\"updateAddrField(${i},'city',this)\\\"\u001b[39m\n\u001b[31m            style=\\\"${!s.city ? 'border-color:var(--ylw);' : ''}\\\">\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m        <div class=\\\"verify-field\\\">\u001b[39m\n\u001b[31m          <label>Docket / Courier No.</label>\u001b[39m\n\u001b[31m          <input type=\\\"text\\\" value=\\\"${s.docket||''}\\\" id=\\\"ar_docket_${i}\\\"\u001b[39m\n\u001b[31m            oninput=\\\"updateAddrField(${i},'docket',this)\\\"\u001b[39m\n\u001b[31m            style=\\\"font-family:var(--fnt-mono);${!s.docket?'border-color:var(--ylw);':'color:var(--acc);'}\\\">\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m        <div class=\\\"verify-field\\\">\u001b[39m\n\u001b[31m          <label>Phone</label>\u001b[39m\n\u001b[31m          <input type=\\\"tel\\\" value=\\\"${s.phone||''}\\\" id=\\\"ar_phone_${i}\\\"\u001b[39m\n\u001b[31m            oninput=\\\"updateAddrField(${i},'phone',this)\\\">\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m        <div class=\\\"verify-field\\\">\u001b[39m\n\u001b[31m          <label>Pincode</label>\u001b[39m\n\u001b[31m          <input type=\\\"text\\\" value=\\\"${s.pincode||''}\\\" id=\\\"ar_pin_${i}\\\"\u001b[39m\n\u001b[31m            oninput=\\\"updateAddrField(${i},'pincode',this)\\\">\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      <div style=\\\"font-size:.65rem;color:var(--t3);font-family:var(--fnt-mono);\u001b[39m\n\u001b[31m        margin-top:4px;padding:4px 6px;background:var(--bg);border-radius:4px;word-break:break-all;\\\">\u001b[39m\n\u001b[31m        📋 ${(s._rawCell||'').split('\\\\n').join(' | ')}\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m    </div>`;\u001b[39m\n\u001b[31m  }).join('');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction updateAddrField(i, field, input) {\u001b[39m\n\u001b[31m  if (!stores[i]) return;\u001b[39m\n\u001b[31m  stores[i][field] = input.value.trim();\u001b[39m\n\u001b[31m  // Recompute confidence\u001b[39m\n\u001b[31m  let conf = 0;\u001b[39m\n\u001b[31m  if (stores[i].name)    conf += 30;\u001b[39m\n\u001b[31m  if (stores[i].city)    conf += 25;\u001b[39m\n\u001b[31m  if (stores[i].pincode) conf += 25;\u001b[39m\n\u001b[31m  if (stores[i].phone)   conf += 20;\u001b[39m\n\u001b[31m  stores[i]._confidence = conf;\u001b[39m\n\u001b[31m  input.classList.toggle('changed', true);\u001b[39m\n\u001b[31m  // Update border color on row\u001b[39m\n\u001b[31m  const row = document.getElementById('arow_' + i);\u001b[39m\n\u001b[31m  if (row) row.style.borderLeftColor = conf >= 80 ? 'var(--grn)' : conf >= 50 ? 'var(--ylw)' : 'var(--red)';\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction deleteReviewRow(i) {\u001b[39m\n\u001b[31m  // Mark as deleted — splice from stores array and re-render\u001b[39m\n\u001b[31m  stores.splice(i, 1);\u001b[39m\n\u001b[31m  // Re-assign IDs\u001b[39m\n\u001b[31m  stores.forEach((s, idx) => { s.id = idx; });\u001b[39m\n\u001b[31m  renderAddressReviewRows();\u001b[39m\n\u001b[31m  document.getElementById('storeCount').textContent = `${stores.length} stores loaded`;\u001b[39m\n\u001b[31m  showToast(`Row removed — ${stores.length} remaining`, 'info');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction backToMapping() {\u001b[39m\n\u001b[31m  // Hide review pane — store edits are preserved in stores[] array\u001b[39m\n\u001b[31m  document.getElementById('addrReviewPane').style.display = 'none';\u001b[39m\n\u001b[31m  // Show mapping UI again with current file\u001b[39m\n\u001b[31m  document.getElementById('storeTableArea').style.display = 'block';\u001b[39m\n\u001b[31m  document.getElementById('storeColInfo').style.display = 'block';\u001b[39m\n\u001b[31m  showToast('Your edits are preserved — re-apply mapping to reload', 'info');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction approveAllAddresses() {\u001b[39m\n\u001b[31m  stores.forEach((s, i) => { s._reviewed = true; });\u001b[39m\n\u001b[31m  document.getElementById('addrReviewPane').style.display = 'none';\u001b[39m\n\u001b[31m  applyReviewedStores();\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction confirmAddressReview() {\u001b[39m\n\u001b[31m  // Collect any pending edits, log corrections, close panel\u001b[39m\n\u001b[31m  const corrections = [];\u001b[39m\n\u001b[31m  stores.forEach((s, i) => {\u001b[39m\n\u001b[31m    const origName = s._origName || '';\u001b[39m\n\u001b[31m    const origCity = s._origCity || '';\u001b[39m\n\u001b[31m    if ((origName && origName !== s.name) || (origCity && origCity !== s.city)) {\u001b[39m\n\u001b[31m      corrections.push({ raw: s._rawCell||'', name: s.name, city: s.city, pincode: s.pincode });\u001b[39m\n\u001b[31m    }\u001b[39m\n\u001b[31m  });\u001b[39m\n\u001b[31m  // Save corrections locally for future Ollama prompts\u001b[39m\n\u001b[31m  corrections.forEach(c => saveCorrectionLocal(c));\u001b[39m\n\u001b[31m  // Log to Sheets asynchronously (fire and forget)\u001b[39m\n\u001b[31m  if (corrections.length > 0 && window.appConfig.webhook_url) {\u001b[39m\n\u001b[31m    logParseCorrectionsToSheets(corrections);\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m  document.getElementById('addrReviewPane').style.display = 'none';\u001b[39m\n\u001b[31m  applyReviewedStores();\u001b[39m\n\u001b[31m  showToast(`${corrections.length > 0 ? corrections.length + ' corrections logged · ' : ''}${stores.length} stores ready ✓`, 'ok');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// Clean street address: remove city, pincode, phone if they appear in address field\u001b[39m\n\u001b[31mfunction cleanAddress(addr, city, pincode, phone) {\u001b[39m\n\u001b[31m  if (!addr) return addr;\u001b[39m\n\u001b[31m  let s = addr;\u001b[39m\n\u001b[31m  // Remove phone\u001b[39m\n\u001b[31m  if (phone) s = s.replace(new RegExp(phone.replace(/[.*+?^${}()|[\\\\]\\\\\\\\]/g,'\\\\\\\\$&'), 'g'), '').trim();\u001b[39m\n\u001b[31m  // Remove pincode\u001b[39m\n\u001b[31m  if (pincode) s = s.replace(new RegExp('\\\\\\\\b' + pincode + '\\\\\\\\b', 'g'), '').trim();\u001b[39m\n\u001b[31m  // Remove city (case-insensitive)\u001b[39m\n\u001b[31m  if (city) s = s.replace(new RegExp('\\\\\\\\b' + city.replace(/[.*+?^${}()|[\\\\]\\\\\\\\]/g,'\\\\\\\\$&') + '\\\\\\\\b', 'ig'), '').trim();\u001b[39m\n\u001b[31m  // Remove state names and trailing noise\u001b[39m\n\u001b[31m  const noise = /\\\\b(rajasthan|gujarat|maharashtra|punjab|haryana|uttar pradesh|delhi|raj\\\\.)\\\\b/ig;\u001b[39m\n\u001b[31m  s = s.replace(noise, '');\u001b[39m\n\u001b[31m  // Remove \\\"Mobile :\\\", \\\"Phone :\\\", \\\"M.No.\\\" prefixes and dangling numbers\u001b[39m\n\u001b[31m  s = s.replace(/(?:Mobile|Phone|Mob|M\\\\.No\\\\.?|Ph\\\\.?)\\\\s*[:\\\\-]?\\\\s*/ig, '');\u001b[39m\n\u001b[31m  // Clean up multiple commas/spaces\u001b[39m\n\u001b[31m  s = s.replace(/,\\\\s*,/g, ',').replace(/\\\\s{2,}/g, ' ').replace(/^[\\\\s,]+|[\\\\s,]+$/g, '');\u001b[39m\n\u001b[31m  return s;\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction applyReviewedStores() {\u001b[39m\n\u001b[31m  // Clean address fields before loading stores\u001b[39m\n\u001b[31m  stores.forEach(s => {\u001b[39m\n\u001b[31m    s.address = cleanAddress(s.address, s.city, s.pincode, s.phone);\u001b[39m\n\u001b[31m  });\u001b[39m\n\u001b[31m  renderStoreTable(stores.some(s => s.docket));\u001b[39m\n\u001b[31m  const addPanel = document.getElementById('storeAddPanel');\u001b[39m\n\u001b[31m  const addTitle = document.getElementById('storeAddTitle');\u001b[39m\n\u001b[31m  const addBtn   = document.getElementById('storeAddBtn');\u001b[39m\n\u001b[31m  const hasDockets = stores.some(s => s.docket);\u001b[39m\n\u001b[31m  addTitle.textContent = hasDockets\u001b[39m\n\u001b[31m    ? `${stores.filter(s=>s.docket).length} stores with dockets — add as consignees?`\u001b[39m\n\u001b[31m    : `Add ${stores.length} stores as consignees?`;\u001b[39m\n\u001b[31m  addBtn.textContent = `✓ Add ${stores.length} as Consignees`;\u001b[39m\n\u001b[31m  addPanel.style.display = 'block';\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31masync function logParseCorrectionsToSheets(corrections) {\u001b[39m\n\u001b[31m  const url = window.appConfig.webhook_url;\u001b[39m\n\u001b[31m  if (!url) return;\u001b[39m\n\u001b[31m  try {\u001b[39m\n\u001b[31m    await fetch(url, {\u001b[39m\n\u001b[31m      method: 'POST', mode: 'no-cors',\u001b[39m\n\u001b[31m      headers: { 'Content-Type': 'application/json' },\u001b[39m\n\u001b[31m      body: JSON.stringify({\u001b[39m\n\u001b[31m        type: 'parse_correction',\u001b[39m\n\u001b[31m        corrections,\u001b[39m\n\u001b[31m        colFingerprint: getMappingKey(),\u001b[39m\n\u001b[31m        timestamp: new Date().toISOString()\u001b[39m\n\u001b[31m      })\u001b[39m\n\u001b[31m    });\u001b[39m\n\u001b[31m  } catch(e) {}\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction addStoresToConsignees() {\u001b[39m\n\u001b[31m  const consList = document.getElementById('consList');\u001b[39m\n\u001b[31m  consList.innerHTML = '';\u001b[39m\n\u001b[31m  stores.slice(0, 50).forEach(s => addConsignee(s));\u001b[39m\n\u001b[31m  document.getElementById('storeAddPanel').style.display = 'none';\u001b[39m\n\u001b[31m  showToast(`${Math.min(stores.length, 50)} consignees added ✓`, 'ok');\u001b[39m\n\u001b[31m  if (stores.some(s => s.docket)) setTimeout(() => autoCreateDispatchFromStoreList(), 600);\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction promptAddFromStores() { /* replaced by inline panel */ }··\u001b[39m\n\u001b[31mfunction findCol(headers, keywords) {\u001b[39m\n\u001b[31m  for (let i=0;i<headers.length;i++) {\u001b[39m\n\u001b[31m    const h = (headers[i]||'').toString().toLowerCase();\u001b[39m\n\u001b[31m    if (keywords.some(k => h.includes(k))) return i;\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m  return -1;\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction renderStoreTable(hasDocket) {\u001b[39m\n\u001b[31m  const t = document.getElementById('storeTable');\u001b[39m\n\u001b[31m  const cols = ['#','Store Name','City','Phone'];\u001b[39m\n\u001b[31m  if (hasDocket) cols.push('Docket','Status');\u001b[39m\n\u001b[31m  t.innerHTML = `<thead><tr>${cols.map(c=>`<th>${c}</th>`).join('')}</tr></thead>\u001b[39m\n\u001b[31m  <tbody>${stores.map((s,i) => {\u001b[39m\n\u001b[31m    const statusClass = s.docket ? (s.status === 'delivered' ? 'matched' : 'unmatched') : '';\u001b[39m\n\u001b[31m    const docketCell = hasDocket ? `<td class=\\\"${statusClass}\\\" style=\\\"font-family:var(--fnt-mono);\\\">${s.docket||'—'}</td>` : '';\u001b[39m\n\u001b[31m    const statusCell = hasDocket ? `<td>${s.status ? `<span class=\\\"status-pill sp-${s.status}\\\">${s.status}</span>` : '—'}</td>` : '';\u001b[39m\n\u001b[31m    return `<tr>\u001b[39m\n\u001b[31m      <td>${i+1}</td>\u001b[39m\n\u001b[31m      <td>${s.name}</td>\u001b[39m\n\u001b[31m      <td>${s.city}</td>\u001b[39m\n\u001b[31m      <td style=\\\"font-family:var(--fnt-mono);\\\">${s.phone||'—'}</td>\u001b[39m\n\u001b[31m      ${docketCell}${statusCell}\u001b[39m\n\u001b[31m    </tr>`;\u001b[39m\n\u001b[31m  }).join('')}</tbody>`;\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction clearStores() {\u001b[39m\n\u001b[31m  stores = [];\u001b[39m\n\u001b[31m  document.getElementById('storeTableArea').style.display = 'none';\u001b[39m\n\u001b[31m  document.getElementById('storeFile').value = '';\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction autoCreateDispatchFromStoreList() {\u001b[39m\n\u001b[31m  // Create/update dispatch with all stores that have dockets\u001b[39m\n\u001b[31m  const courier = document.getElementById('n_courier').value || 'SkyKing';\u001b[39m\n\u001b[31m  const storesWithDockets = stores.filter(s => s.docket);\u001b[39m\n\u001b[31m  if (!storesWithDockets.length) return;·\u001b[39m\n\u001b[31m  // Clear and rebuild consignee list from store data\u001b[39m\n\u001b[31m  const consList = document.getElementById('consList');\u001b[39m\n\u001b[31m  consList.innerHTML = '';\u001b[39m\n\u001b[31m  storesWithDockets.forEach(s => {\u001b[39m\n\u001b[31m    addConsignee(s);\u001b[39m\n\u001b[31m  });\u001b[39m\n\u001b[31m  showPane('new');\u001b[39m\n\u001b[31m  showToast(`${storesWithDockets.length} consignees added from store list`, 'ok');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction promptAddFromStores() {\u001b[39m\n\u001b[31m  // Replaced by inline storeAddPanel UI — no-op kept for safety\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction autoMatchDockets() {\u001b[39m\n\u001b[31m  // Match OCR dockets to stores by order\u001b[39m\n\u001b[31m  if (!slipOCRData.length) { showToast('Upload docket slip images first', 'err'); return; }\u001b[39m\n\u001b[31m  let matched = 0;\u001b[39m\n\u001b[31m  slipOCRData.forEach((slip, i) => {\u001b[39m\n\u001b[31m    if (i < stores.length && slip.docket) {\u001b[39m\n\u001b[31m      stores[i].docket = slip.docket;\u001b[39m\n\u001b[31m      matched++;\u001b[39m\n\u001b[31m    }\u001b[39m\n\u001b[31m  });\u001b[39m\n\u001b[31m  renderStoreTable(true);\u001b[39m\n\u001b[31m  showToast(`${matched} dockets matched to stores ✓`, 'ok');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31m// CONSIGNEES\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31mfunction addConsignee(storeData = null) {\u001b[39m\n\u001b[31m  const list = document.getElementById('consList');\u001b[39m\n\u001b[31m  const idx = list.children.length;\u001b[39m\n\u001b[31m  const card = document.createElement('div');\u001b[39m\n\u001b[31m  card.className = 'cons-card';\u001b[39m\n\u001b[31m  card.dataset.idx = idx;\u001b[39m\n\u001b[31m  const s = storeData || {};\u001b[39m\n\u001b[31m  card.innerHTML = `\u001b[39m\n\u001b[31m    <div class=\\\"cons-hdr\\\">\u001b[39m\n\u001b[31m      <span class=\\\"cons-num\\\">CONSIGNEE #${idx+1}</span>\u001b[39m\n\u001b[31m      <button class=\\\"cons-del\\\" onclick=\\\"this.closest('.cons-card').remove();renumberCons()\\\">✕</button>\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m    <div class=\\\"row2\\\">\u001b[39m\n\u001b[31m      <div class=\\\"f\\\">\u001b[39m\n\u001b[31m        <label>Store / Name <span class=\\\"req\\\">*</span></label>\u001b[39m\n\u001b[31m        <input type=\\\"text\\\" class=\\\"cons-name\\\" placeholder=\\\"Store name\\\" value=\\\"${s.name||''}\\\">\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      <div class=\\\"f\\\">\u001b[39m\n\u001b[31m        <label>City <span class=\\\"req\\\">*</span></label>\u001b[39m\n\u001b[31m        <input type=\\\"text\\\" class=\\\"cons-city\\\" placeholder=\\\"City\\\" value=\\\"${s.city||''}\\\">\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m    <div class=\\\"f\\\">\u001b[39m\n\u001b[31m      <label>Address</label>\u001b[39m\n\u001b[31m      <input type=\\\"text\\\" class=\\\"cons-addr\\\" placeholder=\\\"Full address\\\" value=\\\"${s.address||''}\\\">\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m    <div class=\\\"row2\\\">\u001b[39m\n\u001b[31m      <div class=\\\"f\\\">\u001b[39m\n\u001b[31m        <label>Phone</label>\u001b[39m\n\u001b[31m        <input type=\\\"tel\\\" class=\\\"cons-phone\\\" placeholder=\\\"Mobile\\\" value=\\\"${s.phone||''}\\\">\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      <div class=\\\"f\\\">\u001b[39m\n\u001b[31m        <label>Pincode</label>\u001b[39m\n\u001b[31m        <input type=\\\"text\\\" class=\\\"cons-pin\\\" placeholder=\\\"6-digit\\\" maxlength=\\\"6\\\">\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m    <div class=\\\"f\\\">\u001b[39m\n\u001b[31m      <label>Material / Contents <span class=\\\"req\\\">*</span></label>\u001b[39m\n\u001b[31m      <input type=\\\"text\\\" class=\\\"cons-material\\\" placeholder=\\\"e.g. Standees, Posters\\\">\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m    <div class=\\\"row2\\\">\u001b[39m\n\u001b[31m      <div class=\\\"f\\\">\u001b[39m\n\u001b[31m        <label>Qty</label>\u001b[39m\n\u001b[31m        <input type=\\\"number\\\" class=\\\"cons-qty\\\" placeholder=\\\"0\\\" min=\\\"0\\\">\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      <div class=\\\"f\\\">\u001b[39m\n\u001b[31m        <label>Docket No.</label>\u001b[39m\n\u001b[31m        <input type=\\\"text\\\" class=\\\"cons-docket-input\\\" placeholder=\\\"Docket #\\\"\u001b[39m\n\u001b[31m          style=\\\"font-family:var(--fnt-mono);\\\" value=\\\"${s.docket||''}\\\"\u001b[39m\n\u001b[31m          oninput=\\\"onDocketInput(this)\\\">\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m    ${s.docket ? `<div class=\\\"cons-docket-linked\\\"><span>🔗</span><span class=\\\"dkt\\\">${s.docket}</span></div>` : ''}\u001b[39m\n\u001b[31m  `;\u001b[39m\n\u001b[31m  list.appendChild(card);\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction onDocketInput(input) {\u001b[39m\n\u001b[31m  const card = input.closest('.cons-card');\u001b[39m\n\u001b[31m  let linked = card.querySelector('.cons-docket-linked');\u001b[39m\n\u001b[31m  if (input.value.trim()) {\u001b[39m\n\u001b[31m    if (!linked) {\u001b[39m\n\u001b[31m      linked = document.createElement('div');\u001b[39m\n\u001b[31m      linked.className = 'cons-docket-linked';\u001b[39m\n\u001b[31m      card.appendChild(linked);\u001b[39m\n\u001b[31m    }\u001b[39m\n\u001b[31m    linked.innerHTML = `<span>🔗</span><span class=\\\"dkt\\\">${input.value}</span>`;\u001b[39m\n\u001b[31m  } else if (linked) {\u001b[39m\n\u001b[31m    linked.remove();\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction showLinkedDocket(card, docket) {\u001b[39m\n\u001b[31m  const input = card.querySelector('.cons-docket-input');\u001b[39m\n\u001b[31m  if (input) { input.value = docket; onDocketInput(input); }\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction renumberCons() {\u001b[39m\n\u001b[31m  document.querySelectorAll('#consList .cons-card').forEach((c,i) => {\u001b[39m\n\u001b[31m    c.querySelector('.cons-num').textContent = `CONSIGNEE #${i+1}`;\u001b[39m\n\u001b[31m    c.dataset.idx = i;\u001b[39m\n\u001b[31m  });\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31m// JOB REFERENCE\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31mfunction onClientChange() {\u001b[39m\n\u001b[31m  const val = document.getElementById('n_client').value;\u001b[39m\n\u001b[31m  document.getElementById('customClientField').style.display = val === '__c' ? 'block' : 'none';\u001b[39m\n\u001b[31m  updatePill();\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31mfunction updatePill() {\u001b[39m\n\u001b[31m  const client = document.getElementById('n_client').value;\u001b[39m\n\u001b[31m  const job = document.getElementById('n_jobref').value.trim();\u001b[39m\n\u001b[31m  const cName = client === '__c' ? document.getElementById('n_customClient').value : client;\u001b[39m\n\u001b[31m  const pill = document.getElementById('jobPill');\u001b[39m\n\u001b[31m  if (cName && job) {\u001b[39m\n\u001b[31m    document.getElementById('jobPillTxt').textContent = `${cName} › ${job}`;\u001b[39m\n\u001b[31m    pill.style.display = 'inline-flex';\u001b[39m\n\u001b[31m  } else {\u001b[39m\n\u001b[31m    pill.style.display = 'none';\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31mfunction clearJob() {\u001b[39m\n\u001b[31m  document.getElementById('n_client').value = '';\u001b[39m\n\u001b[31m  document.getElementById('n_jobref').value = '';\u001b[39m\n\u001b[31m  document.getElementById('jobPill').style.display = 'none';\u001b[39m\n\u001b[31m  document.getElementById('customClientField').style.display = 'none';\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31m// GATHER / SAVE\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31mfunction gatherDispatch() {\u001b[39m\n\u001b[31m  const client = document.getElementById('n_client').value;\u001b[39m\n\u001b[31m  const clientName = client === '__c' ? document.getElementById('n_customClient').value : client;\u001b[39m\n\u001b[31m  const consignees = [];\u001b[39m\n\u001b[31m  document.querySelectorAll('#consList .cons-card').forEach(card => {\u001b[39m\n\u001b[31m    const docket = card.querySelector('.cons-docket-input')?.value.trim() || '';\u001b[39m\n\u001b[31m    consignees.push({\u001b[39m\n\u001b[31m      name: card.querySelector('.cons-name')?.value || '',\u001b[39m\n\u001b[31m      city: card.querySelector('.cons-city')?.value || '',\u001b[39m\n\u001b[31m      address: card.querySelector('.cons-addr')?.value || '',\u001b[39m\n\u001b[31m      phone: card.querySelector('.cons-phone')?.value || '',\u001b[39m\n\u001b[31m      pincode: card.querySelector('.cons-pin')?.value || '',\u001b[39m\n\u001b[31m      material: card.querySelector('.cons-material')?.value || '',\u001b[39m\n\u001b[31m      qty: card.querySelector('.cons-qty')?.value || '',\u001b[39m\n\u001b[31m      docket,\u001b[39m\n\u001b[31m      status: docket ? 'booked' : 'pending',\u001b[39m\n\u001b[31m      lastEvent: '',\u001b[39m\n\u001b[31m      lastUpdated: ''\u001b[39m\n\u001b[31m    });\u001b[39m\n\u001b[31m  });\u001b[39m\n\u001b[31m  return {\u001b[39m\n\u001b[31m    id: 'DSP-' + Date.now(),\u001b[39m\n\u001b[31m    mode: 'manual',\u001b[39m\n\u001b[31m    client: clientName,\u001b[39m\n\u001b[31m    jobref: document.getElementById('n_jobref').value,\u001b[39m\n\u001b[31m    courier: document.getElementById('n_courier').value,\u001b[39m\n\u001b[31m    dispatchDate: document.getElementById('n_date').value,\u001b[39m\n\u001b[31m    remarks: document.getElementById('n_remarks').value,\u001b[39m\n\u001b[31m    consignees,\u001b[39m\n\u001b[31m    slipCount: slipOCRData.length,\u001b[39m\n\u001b[31m    status: 'active',\u001b[39m\n\u001b[31m    createdAt: new Date().toISOString()\u001b[39m\n\u001b[31m  };\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction validate(d) {\u001b[39m\n\u001b[31m  if (!d.client) { showToast('Select a client', 'err'); return false; }\u001b[39m\n\u001b[31m  if (!d.courier) { showToast('Select courier', 'err'); return false; }\u001b[39m\n\u001b[31m  if (!d.consignees.length) { showToast('Add at least one consignee', 'err'); return false; }\u001b[39m\n\u001b[31m  for (let i = 0; i < d.consignees.length; i++) {\u001b[39m\n\u001b[31m    const c = d.consignees[i];\u001b[39m\n\u001b[31m    if (!c.name || !c.city) {\u001b[39m\n\u001b[31m      showToast(`Consignee #${i+1} (${c.name||'unnamed'}) — name & city required`, 'err');\u001b[39m\n\u001b[31m      // Scroll to and highlight the problem consignee card\u001b[39m\n\u001b[31m      const cards = document.querySelectorAll('#consList .cons-card');\u001b[39m\n\u001b[31m      if (cards[i]) {\u001b[39m\n\u001b[31m        cards[i].scrollIntoView({ behavior: 'smooth', block: 'center' });\u001b[39m\n\u001b[31m        cards[i].style.borderColor = 'var(--red)';\u001b[39m\n\u001b[31m        cards[i].style.boxShadow = '0 0 0 2px rgba(239,68,68,.3)';\u001b[39m\n\u001b[31m        setTimeout(() => {\u001b[39m\n\u001b[31m          if (cards[i]) { cards[i].style.borderColor = ''; cards[i].style.boxShadow = ''; }\u001b[39m\n\u001b[31m        }, 3000);\u001b[39m\n\u001b[31m      }\u001b[39m\n\u001b[31m      return false;\u001b[39m\n\u001b[31m    }\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m  return true;\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction saveDraft() {\u001b[39m\n\u001b[31m  const d = gatherDispatch();\u001b[39m\n\u001b[31m  d.status = 'draft';\u001b[39m\n\u001b[31m  dispatches.unshift(d);\u001b[39m\n\u001b[31m  saveData();\u001b[39m\n\u001b[31m  updateHistCount();\u001b[39m\n\u001b[31m  showToast('Draft saved ✓', 'ok');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// ── VERIFICATION SCREEN ─────────────────────────────────\u001b[39m\n\u001b[31m// Known docket lengths per courier for validation\u001b[39m\n\u001b[31mconst DOCKET_LENGTHS = {\u001b[39m\n\u001b[31m  'SkyKing': { min: 9, max: 12 },\u001b[39m\n\u001b[31m  'DTDC':    { min: 8, max: 15 },\u001b[39m\n\u001b[31m  'Delhivery': { min: 10, max: 18 },\u001b[39m\n\u001b[31m  'Blue Dart': { min: 10, max: 13 },\u001b[39m\n\u001b[31m  'FedEx':   { min: 10, max: 20 },\u001b[39m\n\u001b[31m  'Ecom Express': { min: 10, max: 14 },\u001b[39m\n\u001b[31m  'India Post': { min: 13, max: 22 },\u001b[39m\n\u001b[31m};·\u001b[39m\n\u001b[31mfunction finaliseDispatch() {\u001b[39m\n\u001b[31m  const d = gatherDispatch();\u001b[39m\n\u001b[31m  if (!validate(d)) return;\u001b[39m\n\u001b[31m  showVerifyScreen(d);\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction showVerifyScreen(d) {\u001b[39m\n\u001b[31m  const pane = document.getElementById('verifyPane');\u001b[39m\n\u001b[31m  const list = document.getElementById('verifyList');\u001b[39m\n\u001b[31m  const summary = document.getElementById('verifySummary');·\u001b[39m\n\u001b[31m  // Build flags\u001b[39m\n\u001b[31m  const docketsSeen = {};\u001b[39m\n\u001b[31m  const courier = d.courier;\u001b[39m\n\u001b[31m  const lenRule = DOCKET_LENGTHS[courier];·\u001b[39m\n\u001b[31m  d.consignees.forEach((c, i) => {\u001b[39m\n\u001b[31m    if (c.docket) docketsSeen[c.docket] = (docketsSeen[c.docket] || 0) + 1;\u001b[39m\n\u001b[31m  });·\u001b[39m\n\u001b[31m  const total     = d.consignees.length;\u001b[39m\n\u001b[31m  const withDock  = d.consignees.filter(c => c.docket).length;\u001b[39m\n\u001b[31m  const dupes     = Object.values(docketsSeen).filter(v => v > 1).length;\u001b[39m\n\u001b[31m  const noDock    = total - withDock;\u001b[39m\n\u001b[31m  const badLen    = lenRule ? d.consignees.filter(c => c.docket &&\u001b[39m\n\u001b[31m    (c.docket.length < lenRule.min || c.docket.length > lenRule.max)).length : 0;·\u001b[39m\n\u001b[31m  // Summary bar\u001b[39m\n\u001b[31m  summary.innerHTML = `\u001b[39m\n\u001b[31m    <div class=\\\"vs-item\\\"><div class=\\\"vs-val\\\" style=\\\"color:var(--t1);\\\">${total}</div><div class=\\\"vs-lbl\\\">Total</div></div>\u001b[39m\n\u001b[31m    <div class=\\\"vs-item\\\"><div class=\\\"vs-val\\\" style=\\\"color:var(--grn);\\\">${withDock}</div><div class=\\\"vs-lbl\\\">Docketed</div></div>\u001b[39m\n\u001b[31m    <div class=\\\"vs-item\\\"><div class=\\\"vs-val\\\" style=\\\"color:${noDock > 0 ? 'var(--ylw)' : 'var(--t3)'};\\\">${noDock}</div><div class=\\\"vs-lbl\\\">No Docket</div></div>\u001b[39m\n\u001b[31m    <div class=\\\"vs-item\\\"><div class=\\\"vs-val\\\" style=\\\"color:${dupes > 0 ? 'var(--red)' : 'var(--t3)'};\\\">${dupes}</div><div class=\\\"vs-lbl\\\">Duplicates</div></div>\u001b[39m\n\u001b[31m    ${badLen > 0 ? `<div class=\\\"vs-item\\\"><div class=\\\"vs-val\\\" style=\\\"color:var(--sky);\\\">${badLen}</div><div class=\\\"vs-lbl\\\">Short/Long</div></div>` : ''}\u001b[39m\n\u001b[31m    <div class=\\\"vs-item\\\" style=\\\"margin-left:auto;text-align:right;\\\">\u001b[39m\n\u001b[31m      <div class=\\\"vs-val\\\" style=\\\"font-size:.8rem;color:var(--t2);\\\">${d.courier||'—'}</div>\u001b[39m\n\u001b[31m      <div class=\\\"vs-lbl\\\">${d.client||'—'} ${d.jobref ? '› '+d.jobref : ''}</div>\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m  `;·\u001b[39m\n\u001b[31m  // Build consignee rows\u001b[39m\n\u001b[31m  list.innerHTML = d.consignees.map((c, i) => {\u001b[39m\n\u001b[31m    const isDupe   = c.docket && docketsSeen[c.docket] > 1;\u001b[39m\n\u001b[31m    const isNoDock = !c.docket;\u001b[39m\n\u001b[31m    const isShort  = lenRule && c.docket &&\u001b[39m\n\u001b[31m      (c.docket.length < lenRule.min || c.docket.length > lenRule.max);·\u001b[39m\n\u001b[31m    const rowClass = isDupe ? 'has-dupe' : (isNoDock ? 'no-docket' : 'ok');·\u001b[39m\n\u001b[31m    // Find matching OCR slip for thumbnail\u001b[39m\n\u001b[31m    const ocrMatch = slipOCRData[i];\u001b[39m\n\u001b[31m    const thumbHtml = ocrMatch?.imageData\u001b[39m\n\u001b[31m      ? `<img class=\\\"verify-thumb\\\" src=\\\"${ocrMatch.imageData}\\\" onclick=\\\"showSlipFull(${i})\\\" title=\\\"View slip\\\">`\u001b[39m\n\u001b[31m      : '';·\u001b[39m\n\u001b[31m    // Flags\u001b[39m\n\u001b[31m    let flags = '';\u001b[39m\n\u001b[31m    if (isDupe)   flags += `<span class=\\\"verify-flag vf-dupe\\\">⚠ Duplicate</span> `;\u001b[39m\n\u001b[31m    if (isNoDock) flags += `<span class=\\\"verify-flag vf-nodocket\\\">No Docket</span> `;\u001b[39m\n\u001b[31m    if (isShort)  flags += `<span class=\\\"verify-flag vf-short\\\">${c.docket.length} digits</span> `;\u001b[39m\n\u001b[31m    if (!isDupe && !isNoDock && !isShort) flags = `<span class=\\\"verify-flag vf-ok\\\">✓ OK</span>`;·\u001b[39m\n\u001b[31m    // OCR confidence note\u001b[39m\n\u001b[31m    const confNote = ocrMatch && ocrMatch.confidence\u001b[39m\n\u001b[31m      ? `<div class=\\\"ocr-correction-note\\\">OCR confidence: ${Math.round(ocrMatch.confidence*100)}% via ${ocrMatch.source||'—'}</div>` : '';·\u001b[39m\n\u001b[31m    // WhatsApp URL\u001b[39m\n\u001b[31m    const waMsg = encodeURIComponent(\u001b[39m\n\u001b[31m      `Dear ${c.name}, your material has been dispatched via ${d.courier}. Docket: ${c.docket||'TBD'}. Track: ${(COURIER_MASTER[d.courier]?.trackUrl||'').replace('{docket}', c.docket||'')}`\u001b[39m\n\u001b[31m    );\u001b[39m\n\u001b[31m    const waUrl = c.phone ? `https://wa.me/91${c.phone.replace(/\\\\D/g,'')}?text=${waMsg}` : '';·\u001b[39m\n\u001b[31m    return `\u001b[39m\n\u001b[31m    <div class=\\\"verify-row ${rowClass}\\\" id=\\\"vrow_${i}\\\">\u001b[39m\n\u001b[31m      <div class=\\\"verify-row-hdr\\\">\u001b[39m\n\u001b[31m        <span class=\\\"verify-num\\\">#${i+1}</span>\u001b[39m\n\u001b[31m        <span class=\\\"verify-store\\\">${c.name||'—'}</span>\u001b[39m\n\u001b[31m        ${flags}\u001b[39m\n\u001b[31m        ${thumbHtml}\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      <div class=\\\"verify-fields\\\" style=\\\"grid-template-columns:1fr 1fr 1fr;\\\">\u001b[39m\n\u001b[31m        <div class=\\\"verify-field\\\">\u001b[39m\n\u001b[31m          <label>Docket No. <span style=\\\"color:var(--acc);\\\">*</span></label>\u001b[39m\n\u001b[31m          <input type=\\\"text\\\" value=\\\"${c.docket||''}\\\" oninput=\\\"updateVerifyField(${i},'docket',this)\\\"\u001b[39m\n\u001b[31m            style=\\\"font-family:var(--fnt-mono);${isDupe||isShort?'border-color:var(--red);':!c.docket?'border-color:var(--ylw);':'color:var(--acc);'}\\\">\u001b[39m\n\u001b[31m          ${confNote}\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m        <div class=\\\"verify-field\\\">\u001b[39m\n\u001b[31m          <label>City</label>\u001b[39m\n\u001b[31m          <input type=\\\"text\\\" value=\\\"${c.city||''}\\\" oninput=\\\"updateVerifyField(${i},'city',this)\\\"\u001b[39m\n\u001b[31m            style=\\\"${!c.city ? 'border-color:var(--ylw);' : ''}\\\">\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m        <div class=\\\"verify-field\\\">\u001b[39m\n\u001b[31m          <label>Pincode</label>\u001b[39m\n\u001b[31m          <input type=\\\"text\\\" value=\\\"${c.pincode||''}\\\" oninput=\\\"updateVerifyField(${i},'pincode',this)\\\"\u001b[39m\n\u001b[31m            style=\\\"font-family:var(--fnt-mono);\\\">\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m        <div class=\\\"verify-field\\\">\u001b[39m\n\u001b[31m          <label>Phone</label>\u001b[39m\n\u001b[31m          <input type=\\\"tel\\\" value=\\\"${c.phone||''}\\\" oninput=\\\"updateVerifyField(${i},'phone',this)\\\">\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m        <div class=\\\"verify-field\\\">\u001b[39m\n\u001b[31m          <label>Material / Contents</label>\u001b[39m\n\u001b[31m          <input type=\\\"text\\\" value=\\\"${c.material||''}\\\" oninput=\\\"updateVerifyField(${i},'material',this)\\\">\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m        <div class=\\\"verify-field\\\">\u001b[39m\n\u001b[31m          <label>Street Address</label>\u001b[39m\n\u001b[31m          <input type=\\\"text\\\" value=\\\"${c.address||''}\\\" oninput=\\\"updateVerifyField(${i},'address',this)\\\"\u001b[39m\n\u001b[31m            placeholder=\\\"Street / locality only\\\">\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      <div class=\\\"verify-actions\\\">\u001b[39m\n\u001b[31m        ${c.docket && c.phone ? `<button class=\\\"btn-wa\\\" onclick=\\\"window.open('${waUrl}','_blank')\\\">📱 WhatsApp</button>` : ''}\u001b[39m\n\u001b[31m        <button class=\\\"btn-sm\\\" onclick=\\\"removeVerifyRow(${i})\\\">✕ Remove</button>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m    </div>`;\u001b[39m\n\u001b[31m  }).join('');·\u001b[39m\n\u001b[31m  // Store dispatch object for confirm\u001b[39m\n\u001b[31m  pane._pendingDispatch = d;\u001b[39m\n\u001b[31m  pane.style.display = 'block';\u001b[39m\n\u001b[31m  pane.scrollTop = 0;\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction updateVerifyField(idx, field, input) {\u001b[39m\n\u001b[31m  const pane = document.getElementById('verifyPane');\u001b[39m\n\u001b[31m  if (!pane._pendingDispatch) return;\u001b[39m\n\u001b[31m  const c = pane._pendingDispatch.consignees[idx];\u001b[39m\n\u001b[31m  if (!c) return;\u001b[39m\n\u001b[31m  c[field] = input.value.trim();\u001b[39m\n\u001b[31m  input.classList.toggle('changed', input.value.trim() !== '');\u001b[39m\n\u001b[31m  // Re-check dupe if docket changed\u001b[39m\n\u001b[31m  if (field === 'docket') refreshVerifyFlags(pane._pendingDispatch);\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction refreshVerifyFlags(d) {\u001b[39m\n\u001b[31m  const seen = {};\u001b[39m\n\u001b[31m  d.consignees.forEach(c => { if (c.docket) seen[c.docket] = (seen[c.docket]||0)+1; });\u001b[39m\n\u001b[31m  d.consignees.forEach((c, i) => {\u001b[39m\n\u001b[31m    const row = document.getElementById('vrow_' + i);\u001b[39m\n\u001b[31m    if (!row) return;\u001b[39m\n\u001b[31m    const isDupe = c.docket && seen[c.docket] > 1;\u001b[39m\n\u001b[31m    row.classList.toggle('has-dupe', isDupe);\u001b[39m\n\u001b[31m  });\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction removeVerifyRow(idx) {\u001b[39m\n\u001b[31m  const pane = document.getElementById('verifyPane');\u001b[39m\n\u001b[31m  if (!pane._pendingDispatch) return;\u001b[39m\n\u001b[31m  const el = document.getElementById('vrow_' + idx);\u001b[39m\n\u001b[31m  if (el) {\u001b[39m\n\u001b[31m    el.style.transition = 'opacity .25s';\u001b[39m\n\u001b[31m    el.style.opacity = '0';\u001b[39m\n\u001b[31m    setTimeout(() => {\u001b[39m\n\u001b[31m      pane._pendingDispatch.consignees.splice(idx, 1);\u001b[39m\n\u001b[31m      showVerifyScreen(pane._pendingDispatch); // re-render with updated indices\u001b[39m\n\u001b[31m    }, 250);\u001b[39m\n\u001b[31m  } else {\u001b[39m\n\u001b[31m    pane._pendingDispatch.consignees.splice(idx, 1);\u001b[39m\n\u001b[31m    showVerifyScreen(pane._pendingDispatch);\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction approveAll() {\u001b[39m\n\u001b[31m  showToast('All rows approved ✓', 'ok');\u001b[39m\n\u001b[31m  document.getElementById('btnConfirmFinalise').style.background = 'var(--grn)';\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction closeVerify() {\u001b[39m\n\u001b[31m  const pane = document.getElementById('verifyPane');\u001b[39m\n\u001b[31m  // If this dispatch was already finalised, warn before going back\u001b[39m\n\u001b[31m  if (_lastFinalisedId && pane._pendingDispatch?.id === _lastFinalisedId) {\u001b[39m\n\u001b[31m    if (!confirm(\u001b[39m\n\u001b[31m      'This dispatch has already been finalised.\\\\n\\\\n' +\u001b[39m\n\u001b[31m      'Going back will take you to the edit form — any changes will create a NEW dispatch record.\\\\n\\\\n' +\u001b[39m\n\u001b[31m      'Go back anyway?'\u001b[39m\n\u001b[31m    )) return;\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m  pane.style.display = 'none';\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction showSlipFull(idx) {\u001b[39m\n\u001b[31m  const s = slipOCRData[idx];\u001b[39m\n\u001b[31m  if (!s?.imageData) return;\u001b[39m\n\u001b[31m  const w = window.open('');\u001b[39m\n\u001b[31m  w.document.write(`<img src=\\\"${s.imageData}\\\" style=\\\"max-width:100%;height:auto;\\\">`);\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// Log OCR correction to sheets for future accuracy improvement\u001b[39m\n\u001b[31masync function logOCRCorrection(original, corrected, courier) {\u001b[39m\n\u001b[31m  if (!window.appConfig.webhook_url || original === corrected) return;\u001b[39m\n\u001b[31m  try {\u001b[39m\n\u001b[31m    await fetch(window.appConfig.webhook_url, {\u001b[39m\n\u001b[31m      method: 'POST', mode: 'no-cors',\u001b[39m\n\u001b[31m      headers: { 'Content-Type': 'application/json' },\u001b[39m\n\u001b[31m      body: JSON.stringify({\u001b[39m\n\u001b[31m        type: 'ocr_correction',\u001b[39m\n\u001b[31m        original, corrected, courier,\u001b[39m\n\u001b[31m        timestamp: new Date().toISOString()\u001b[39m\n\u001b[31m      })\u001b[39m\n\u001b[31m    });\u001b[39m\n\u001b[31m  } catch(e) {}\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mlet _lastFinalisedId = null; // track last finalised dispatch to warn on back·\u001b[39m\n\u001b[31masync function confirmAndFinalise() {\u001b[39m\n\u001b[31m  const pane = document.getElementById('verifyPane');\u001b[39m\n\u001b[31m  const d = pane._pendingDispatch;\u001b[39m\n\u001b[31m  if (!d) return;·\u001b[39m\n\u001b[31m  d.status = 'active';\u001b[39m\n\u001b[31m  d.finalisedAt = new Date().toISOString();·\u001b[39m\n\u001b[31m  // Log OCR corrections\u001b[39m\n\u001b[31m  slipOCRData.forEach((s, i) => {\u001b[39m\n\u001b[31m    const c = d.consignees[i];\u001b[39m\n\u001b[31m    if (c && s.docket && c.docket && s.docket !== c.docket) {\u001b[39m\n\u001b[31m      logOCRCorrection(s.docket, c.docket, d.courier);\u001b[39m\n\u001b[31m    }\u001b[39m\n\u001b[31m  });·\u001b[39m\n\u001b[31m  // Replace existing draft with same ID if present, else prepend\u001b[39m\n\u001b[31m  const existingIdx = dispatches.findIndex(x => x.id === d.id);\u001b[39m\n\u001b[31m  if (existingIdx >= 0) {\u001b[39m\n\u001b[31m    dispatches[existingIdx] = d;\u001b[39m\n\u001b[31m  } else {\u001b[39m\n\u001b[31m    dispatches.unshift(d);\u001b[39m\n\u001b[31m  }·\u001b[39m\n\u001b[31m  _lastFinalisedId = d.id;\u001b[39m\n\u001b[31m  saveData();\u001b[39m\n\u001b[31m  syncDispatchToDB(d); // push to Postgres (non-blocking)\u001b[39m\n\u001b[31m  updateHistCount();\u001b[39m\n\u001b[31m  if (window.appConfig.auto_log !== 'false' && window.appConfig.webhook_url) await logToSheets(d);\u001b[39m\n\u001b[31m  showToast('Dispatch finalised ✓', 'ok');\u001b[39m\n\u001b[31m  pane.style.display = 'none';\u001b[39m\n\u001b[31m  pane._pendingDispatch = null; // clear pending\u001b[39m\n\u001b[31m  resetForm();\u001b[39m\n\u001b[31m  showPane('dashboard');\u001b[39m\n\u001b[31m  renderDashboard();\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction resetForm() {\u001b[39m\n\u001b[31m  document.getElementById('n_client').value = '';\u001b[39m\n\u001b[31m  document.getElementById('n_jobref').value = '';\u001b[39m\n\u001b[31m  document.getElementById('n_courier').value = '';\u001b[39m\n\u001b[31m  document.getElementById('n_remarks').value = '';\u001b[39m\n\u001b[31m  document.getElementById('jobPill').style.display = 'none';\u001b[39m\n\u001b[31m  document.getElementById('customClientField').style.display = 'none';\u001b[39m\n\u001b[31m  document.getElementById('consList').innerHTML = '';\u001b[39m\n\u001b[31m  addConsignee();\u001b[39m\n\u001b[31m  clearSlips();\u001b[39m\n\u001b[31m  clearStores();\u001b[39m\n\u001b[31m  setToday();\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction newDispatch() { resetForm(); showPane('new'); }·\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31m// DASHBOARD\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31mfunction renderDashboard() {\u001b[39m\n\u001b[31m  // Exclude drafts from dashboard — only show active/finalised dispatches\u001b[39m\n\u001b[31m  const activeDis = dispatches.filter(d => d.status !== 'draft');\u001b[39m\n\u001b[31m  const allCons = activeDis.flatMap(d => d.consignees || []);\u001b[39m\n\u001b[31m  const countBy = (status) => allCons.filter(c => c.status === status).length;\u001b[39m\n\u001b[31m  document.getElementById('st-delivered').textContent = countBy('delivered');\u001b[39m\n\u001b[31m  document.getElementById('st-transit').textContent = countBy('transit') + countBy('in-transit');\u001b[39m\n\u001b[31m  document.getElementById('st-booked').textContent = countBy('booked');\u001b[39m\n\u001b[31m  document.getElementById('st-pending').textContent = allCons.filter(c => !c.docket || c.status === 'pending').length;·\u001b[39m\n\u001b[31m  const grid = document.getElementById('dashGrid');\u001b[39m\n\u001b[31m  // Flatten all consignees across dispatches\u001b[39m\n\u001b[31m  let items = activeDis.flatMap(d =>\u001b[39m\n\u001b[31m    (d.consignees || []).filter(c => c.docket).map(c => ({...c, dispatchId: d.id, courier: d.courier, client: d.client, jobref: d.jobref}))\u001b[39m\n\u001b[31m  );\u001b[39m\n\u001b[31m  if (dashFilter !== 'all') {\u001b[39m\n\u001b[31m    items = items.filter(c => {\u001b[39m\n\u001b[31m      if (dashFilter === 'delivered') return c.status === 'delivered';\u001b[39m\n\u001b[31m      if (dashFilter === 'transit') return c.status === 'transit' || c.status === 'in-transit';\u001b[39m\n\u001b[31m      if (dashFilter === 'booked') return c.status === 'booked';\u001b[39m\n\u001b[31m      if (dashFilter === 'pending') return !c.docket || c.status === 'pending';\u001b[39m\n\u001b[31m      return true;\u001b[39m\n\u001b[31m    });\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m  if (!items.length) {\u001b[39m\n\u001b[31m    grid.innerHTML = '<div class=\\\"empty\\\"><div class=\\\"e-icon\\\">📦</div><p>No docket-tracked consignees.<br>Add docket numbers to see tiles.</p></div>';\u001b[39m\n\u001b[31m    return;\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m  const progMap = { delivered:100, transit:60,'in-transit':60, booked:20, pending:5 };\u001b[39m\n\u001b[31m  const progClr = { delivered:'var(--grn)', transit:'var(--ylw)','in-transit':'var(--ylw)', booked:'var(--sky)', pending:'var(--t4)' };\u001b[39m\n\u001b[31m  grid.innerHTML = items.map(c => {\u001b[39m\n\u001b[31m    const sp = c.status || 'booked';\u001b[39m\n\u001b[31m    const prog = progMap[sp] || 20;\u001b[39m\n\u001b[31m    const clr = progClr[sp] || 'var(--t4)';\u001b[39m\n\u001b[31m    return `\u001b[39m\n\u001b[31m    <div class=\\\"docket-tile\\\" onclick=\\\"showDocketDetail('${c.docket}','${c.courier||''}','${c.dispatchId}')\\\">\u001b[39m\n\u001b[31m      <div class=\\\"docket-tile-hdr\\\">\u001b[39m\n\u001b[31m        <div class=\\\"docket-tile-left\\\">\u001b[39m\n\u001b[31m          <div class=\\\"docket-num\\\">${c.docket}</div>\u001b[39m\n\u001b[31m          <div class=\\\"docket-store\\\">${c.name} — ${c.city}</div>\u001b[39m\n\u001b[31m          <div class=\\\"docket-courier\\\">${c.courier||''} · ${c.client||''} ${c.jobref ? '› '+c.jobref : ''}</div>\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m        <span class=\\\"status-pill sp-${sp}\\\">${sp}</span>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      <div style=\\\"display:flex;gap:6px;margin-bottom:6px;align-items:center;\\\">\u001b[39m\n\u001b[31m        <input type=\\\"checkbox\\\" id=\\\"sel_${c.docket.replace(/[^a-z0-9]/gi,'_')}\\\"\u001b[39m\n\u001b[31m          onclick=\\\"event.stopPropagation();toggleTileSelect('${c.docket}')\\\"\u001b[39m\n\u001b[31m          style=\\\"width:14px;height:14px;cursor:pointer;accent-color:var(--acc);\\\">\u001b[39m\n\u001b[31m        <label style=\\\"font-size:.6rem;color:var(--t3);font-family:var(--fnt-mono);cursor:pointer;\\\"\u001b[39m\n\u001b[31m          onclick=\\\"event.stopPropagation();toggleTileSelect('${c.docket}')\\\">Select</label>\u001b[39m\n\u001b[31m        ${c.manualStatus ?\u001b[39m\n\u001b[31m          `<span style=\\\"font-size:.6rem;font-family:var(--fnt-mono);color:var(--grn);\\\">📞 ${c.manualStatus}</span>` : ''}\u001b[39m\n\u001b[31m        ${c.podUploaded ? '<span style=\\\"font-size:.6rem;color:var(--grn);font-family:var(--fnt-mono);\\\">✓ POD</span>' : ''}\u001b[39m\n\u001b[31m        <button onclick=\\\"event.stopPropagation();showManualTracking('${c.docket}','${c.dispatchId}','${c.name}','${c.phone||''}','${c.city||''}')\\\"\u001b[39m\n\u001b[31m          style=\\\"font-size:.6rem;background:rgba(56,189,248,.1);border:1px solid rgba(56,189,248,.3);\u001b[39m\n\u001b[31m          color:var(--sky);padding:2px 7px;border-radius:10px;cursor:pointer;font-family:var(--fnt-mono);\\\">📞 Call Log</button>\u001b[39m\n\u001b[31m        <button onclick=\\\"event.stopPropagation();showPODUpload('${c.docket}','${c.dispatchId}')\\\"\u001b[39m\n\u001b[31m          style=\\\"font-size:.6rem;background:none;border:1px solid var(--bdr);color:var(--t3);\u001b[39m\n\u001b[31m          padding:2px 7px;border-radius:10px;cursor:pointer;font-family:var(--fnt-mono);\\\">📸 POD</button>\u001b[39m\n\u001b[31m        ${c.manualDelivered ? `<button onclick=\\\"event.stopPropagation();resumeAutoTracking('${c.docket}','${c.dispatchId}')\\\"\u001b[39m\n\u001b[31m          style=\\\"font-size:.6rem;background:rgba(251,146,60,.1);border:1px solid rgba(251,146,60,.4);\u001b[39m\n\u001b[31m          color:#fb923c;padding:2px 7px;border-radius:10px;cursor:pointer;font-family:var(--fnt-mono);\\\">↩ Undo</button>` : ''}\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      <div class=\\\"docket-tile-footer\\\">\u001b[39m\n\u001b[31m        <div style=\\\"flex:1;min-width:0;\\\">\u001b[39m\n\u001b[31m          <span class=\\\"docket-last-event docket-evt-${c.docket}\\\" id=\\\"evt_${c.docket.replace(/[^a-z0-9]/gi,'_')}\\\">${c.lastEvent || 'No tracking yet'}</span>\u001b[39m\n\u001b[31m          <span class=\\\"track-spinner\\\" id=\\\"spin_${c.docket.replace(/[^a-z0-9]/gi,'_')}\\\" style=\\\"display:none;color:var(--ylw);font-family:var(--fnt-mono);font-size:.68rem;animation:blink 1s infinite;\\\">⏳ tracking...</span>\u001b[39m\n\u001b[31m          ${c.lastScanLocation && c.lastScanLocation !== c.lastEvent ?·\u001b[39m\n\u001b[31m            `<div style=\\\"font-size:.62rem;color:var(--t3);font-family:var(--fnt-mono);margin-top:2px;\\\">📍 ${c.lastScanLocation}</div>` : ''}\u001b[39m\n\u001b[31m          ${c.lastTrackedAt ?·\u001b[39m\n\u001b[31m            `<div style=\\\"font-size:.6rem;color:var(--t4);font-family:var(--fnt-mono);margin-top:1px;\\\">🕐 tracked ${new Date(c.lastTrackedAt).toLocaleTimeString('en-IN',{hour:'2-digit',minute:'2-digit'})}</div>` : ''}\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m        <button class=\\\"btn-track-live live-btn-${c.docket.replace(/[^a-z0-9]/gi,'_')}\\\" onclick=\\\"event.stopPropagation();liveTrackTile('${c.docket}','${c.courier||''}','${c.dispatchId}')\\\">\u001b[39m\n\u001b[31m          <span class=\\\"live-dot\\\"></span>LIVE\u001b[39m\n\u001b[31m        </button>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      <div class=\\\"docket-progress\\\">\u001b[39m\n\u001b[31m        <div class=\\\"docket-progress-fill\\\" style=\\\"width:${prog}%;background:${clr};\\\"></div>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m    </div>`;\u001b[39m\n\u001b[31m  }).join('');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction filterDash(f, el) {\u001b[39m\n\u001b[31m  dashFilter = f;\u001b[39m\n\u001b[31m  document.querySelectorAll('#dashFilters .btn-sm').forEach(b => b.classList.remove('primary'));\u001b[39m\n\u001b[31m  el.classList.add('primary');\u001b[39m\n\u001b[31m  renderDashboard();\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31masync function liveTrackTile(docket, courier, dispatchId) {\u001b[39m\n\u001b[31m  showToast(`Tracking ${docket}...`, 'info');\u001b[39m\n\u001b[31m  const result = await fetchTracking(docket, courier);\u001b[39m\n\u001b[31m  // Update status in dispatches\u001b[39m\n\u001b[31m  for (const d of dispatches) {\u001b[39m\n\u001b[31m    if (d.id !== dispatchId) continue;\u001b[39m\n\u001b[31m    for (const c of d.consignees) {\u001b[39m\n\u001b[31m      if (c.docket === docket) {\u001b[39m\n\u001b[31m        c.status = result.status || c.status;\u001b[39m\n\u001b[31m        c.lastEvent = result.lastEvent || c.lastEvent;\u001b[39m\n\u001b[31m        c.lastUpdated = new Date().toISOString();\u001b[39m\n\u001b[31m      }\u001b[39m\n\u001b[31m    }\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m  saveData();\u001b[39m\n\u001b[31m  renderDashboard();\u001b[39m\n\u001b[31m  if (result.status === 'failed') {\u001b[39m\n\u001b[31m    // Open courier site as fallback\u001b[39m\n\u001b[31m    openCourierTrackUrl(docket, courier);\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction showDocketDetail(docket, courier, dispatchId) {\u001b[39m\n\u001b[31m  const d = dispatches.find(x => x.id === dispatchId);\u001b[39m\n\u001b[31m  const c = d?.consignees?.find(x => x.docket === docket);\u001b[39m\n\u001b[31m  if (!c) return;\u001b[39m\n\u001b[31m  const content = document.getElementById('detailContent');\u001b[39m\n\u001b[31m  content.innerHTML = `\u001b[39m\n\u001b[31m    <div class=\\\"sheet-title\\\">${docket}</div>\u001b[39m\n\u001b[31m    <div class=\\\"info-tag\\\">📍 <strong>${c.name}</strong> — ${c.city}</div>\u001b[39m\n\u001b[31m    ${c.phone ? `<div class=\\\"info-tag\\\">📞 ${c.phone}</div>` : ''}\u001b[39m\n\u001b[31m    <div class=\\\"info-tag\\\">📦 ${c.material||'—'} ${c.qty ? '('+c.qty+' pcs)':''}</div>\u001b[39m\n\u001b[31m    <div class=\\\"info-tag\\\">🚚 ${courier||'—'} · <span class=\\\"status-pill sp-${c.status||'pending'}\\\">${c.status||'pending'}</span></div>\u001b[39m\n\u001b[31m    ${c.lastEvent ? `<div class=\\\"info-tag\\\">🕐 ${c.lastEvent}</div>` : ''}\u001b[39m\n\u001b[31m    <div class=\\\"sec-div\\\"><span>Tracking History</span></div>\u001b[39m\n\u001b[31m    <div id=\\\"docketTimeline\\\">\u001b[39m\n\u001b[31m      ${c.timeline?.length ?\u001b[39m\n\u001b[31m        c.timeline.map(e => `<div class=\\\"track-evt\\\"><span class=\\\"track-time\\\">${e.time||''}</span><div><div class=\\\"track-desc\\\">${e.desc||''}</div><div class=\\\"track-loc\\\">${e.loc||''}</div></div></div>`).join('') :\u001b[39m\n\u001b[31m        '<div style=\\\"color:var(--t3);font-size:.78rem;padding:10px 0;\\\">No timeline data. Click Live Track to fetch.</div>'}\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m    <div style=\\\"display:flex;gap:6px;flex-wrap:wrap;margin-top:14px;\\\">\u001b[39m\n\u001b[31m      <button class=\\\"btn btn-ghost\\\" style=\\\"flex:1;\\\" onclick=\\\"openCourierTrackUrl('${docket}','${courier}')\\\">🔗 Courier Site</button>\u001b[39m\n\u001b[31m      <button class=\\\"btn btn-orange\\\" style=\\\"flex:1;\\\" onclick=\\\"liveTrackTile('${docket}','${courier}','${dispatchId}');closeOverlay('detailOverlay')\\\">⚡ Live Track</button>\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m    <div style=\\\"display:flex;gap:6px;flex-wrap:wrap;margin-top:6px;\\\">\u001b[39m\n\u001b[31m      <button class=\\\"btn-wa\\\" style=\\\"flex:1;padding:8px;\\\" onclick=\\\"closeOverlay('detailOverlay');showPODUpload('${docket}','${dispatchId}')\\\">📸 Upload POD</button>\u001b[39m\n\u001b[31m      ${c.podUploaded ? '<span style=\\\"color:var(--grn);font-size:.72rem;font-family:var(--fnt-mono);padding:6px;\\\">✓ POD uploaded</span>' : ''}\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m  `;\u001b[39m\n\u001b[31m  openOverlay('detailOverlay');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31m// TRACKING ENGINE\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31mfunction _normaliseStatus(raw) {\u001b[39m\n\u001b[31m  const s = (raw || '').toLowerCase().trim();\u001b[39m\n\u001b[31m  if (!s || s === 'error') return null;\u001b[39m\n\u001b[31m  if (s.includes('undeliver') || s.includes('delivery_fail') ||\u001b[39m\n\u001b[31m      s.includes('failed_delivery') || s.includes('not deliver') ||\u001b[39m\n\u001b[31m      s.includes('return')  || s.includes('rto')) return 'pending';\u001b[39m\n\u001b[31m  if (s.includes('deliver'))                       return 'delivered';\u001b[39m\n\u001b[31m  if (s.includes('transit') || s.includes('hub') ||\u001b[39m\n\u001b[31m      s.includes('inbound') || s.includes('outbound') ||\u001b[39m\n\u001b[31m      s.includes('out for'))                       return 'transit';\u001b[39m\n\u001b[31m  if (s.includes('book') || s.includes('pick'))   return 'booked';\u001b[39m\n\u001b[31m  return s;\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31masync function _fetchWithRetry(url, opts, maxRetries = 1, retryDelayMs = 4000) {\u001b[39m\n\u001b[31m  for (let attempt = 0; attempt <= maxRetries; attempt++) {\u001b[39m\n\u001b[31m    try {\u001b[39m\n\u001b[31m      const resp = await fetch(url, opts);\u001b[39m\n\u001b[31m      if (resp.ok) return resp;\u001b[39m\n\u001b[31m    } catch(e) {\u001b[39m\n\u001b[31m      if (attempt === maxRetries) throw e;\u001b[39m\n\u001b[31m      await new Promise(r => setTimeout(r, retryDelayMs));\u001b[39m\n\u001b[31m    }\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m  return null;\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31masync function fetchTracking(docket, courier) {\u001b[39m\n\u001b[31m  // Strategy 1: VPS proxy (SkyKing + others that block direct fetch)\u001b[39m\n\u001b[31m  const proxyUrl = window.appConfig.proxy_url;\u001b[39m\n\u001b[31m  if (proxyUrl) {\u001b[39m\n\u001b[31m    try {\u001b[39m\n\u001b[31m      const url  = `${proxyUrl}?courier=${encodeURIComponent(courier)}&docket=${encodeURIComponent(docket)}`;\u001b[39m\n\u001b[31m      const resp = await _fetchWithRetry(url, { signal: AbortSignal.timeout(15000) });\u001b[39m\n\u001b[31m      if (resp) {\u001b[39m\n\u001b[31m        const data      = await resp.json();\u001b[39m\n\u001b[31m        const normStatus = _normaliseStatus(data.status);\u001b[39m\n\u001b[31m        if (normStatus) {\u001b[39m\n\u001b[31m          return { status: normStatus, lastEvent: data.lastEvent || '', timeline: data.timeline || [] };\u001b[39m\n\u001b[31m        }\u001b[39m\n\u001b[31m        openCourierTrackUrl(docket, courier);\u001b[39m\n\u001b[31m        return { status: 'transit', lastEvent: data.lastEvent || 'Check courier site', timeline: [] };\u001b[39m\n\u001b[31m      }\u001b[39m\n\u001b[31m    } catch(e) {}\u001b[39m\n\u001b[31m  }·\u001b[39m\n\u001b[31m  // Strategy 2: Delhivery direct API\u001b[39m\n\u001b[31m  if (courier === 'Delhivery') {\u001b[39m\n\u001b[31m    try {\u001b[39m\n\u001b[31m      const resp = await _fetchWithRetry(\u001b[39m\n\u001b[31m        `https://dlv-api.delhivery.com/v3/track?waybill=${docket}`,\u001b[39m\n\u001b[31m        { signal: AbortSignal.timeout(8000) }\u001b[39m\n\u001b[31m      );\u001b[39m\n\u001b[31m      if (resp) {\u001b[39m\n\u001b[31m        const data = await resp.json();\u001b[39m\n\u001b[31m        const pkg  = data.ShipmentData?.[0]?.Shipment;\u001b[39m\n\u001b[31m        if (pkg) {\u001b[39m\n\u001b[31m          const normStatus = _normaliseStatus(pkg.Status?.Status);\u001b[39m\n\u001b[31m          return { status: normStatus || 'transit', lastEvent: pkg.Status?.Instructions || '', timeline: [] };\u001b[39m\n\u001b[31m        }\u001b[39m\n\u001b[31m      }\u001b[39m\n\u001b[31m    } catch(e) {}\u001b[39m\n\u001b[31m  }·\u001b[39m\n\u001b[31m  return { status: 'failed', lastEvent: 'Open courier site to check', timeline: [] };\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction openCourierTrackUrl(docket, courier) {\u001b[39m\n\u001b[31m  const m = COURIER_MASTER[courier];\u001b[39m\n\u001b[31m  if (!m) return;\u001b[39m\n\u001b[31m  const url = m.trackUrl.replace('{docket}', docket);\u001b[39m\n\u001b[31m  window.open(url, '_blank');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// Track tab - multiple dockets\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31m// TRACK TAB — new logic\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31mlet _trackSelected = new Set(); // dockets currently selected for tracking·\u001b[39m\n\u001b[31m// Called when Track tab opens or courier changes\u001b[39m\n\u001b[31mfunction buildTrackDocketChips() {\u001b[39m\n\u001b[31m  const courier = document.getElementById('t_courier').value;\u001b[39m\n\u001b[31m  const m = COURIER_MASTER[courier];\u001b[39m\n\u001b[31m  const maxBulk = m?.maxBulk || 1;\u001b[39m\n\u001b[31m  const limitEl = document.getElementById('trackCourierLimit');\u001b[39m\n\u001b[31m  if (limitEl) limitEl.textContent = `Max ${maxBulk} at once for ${courier}`;·\u001b[39m\n\u001b[31m  // Gather all dockets from active dispatches for this courier\u001b[39m\n\u001b[31m  const activeDis = dispatches.filter(d => d.status !== 'draft');\u001b[39m\n\u001b[31m  const allDockets = activeDis.flatMap(d =>\u001b[39m\n\u001b[31m    (d.consignees || [])\u001b[39m\n\u001b[31m      .filter(c => c.docket && d.courier === courier)\u001b[39m\n\u001b[31m      .map(c => ({ docket: c.docket, name: c.name, city: c.city, status: c.status || 'booked' }))\u001b[39m\n\u001b[31m  );·\u001b[39m\n\u001b[31m  // Also include dockets from OTHER couriers as un-selectable reference\u001b[39m\n\u001b[31m  const otherDockets = activeDis.flatMap(d =>\u001b[39m\n\u001b[31m    (d.consignees || [])\u001b[39m\n\u001b[31m      .filter(c => c.docket && d.courier !== courier)\u001b[39m\n\u001b[31m      .map(c => ({ docket: c.docket, name: c.name, courier: d.courier }))\u001b[39m\n\u001b[31m  );·\u001b[39m\n\u001b[31m  const chips = document.getElementById('trackDocketChips');\u001b[39m\n\u001b[31m  if (!allDockets.length && !otherDockets.length) {\u001b[39m\n\u001b[31m    chips.innerHTML = '<span style=\\\"font-size:.72rem;color:var(--t3);font-family:var(--fnt-mono);\\\">No dispatched dockets yet — enter manually below</span>';\u001b[39m\n\u001b[31m    return;\u001b[39m\n\u001b[31m  }·\u001b[39m\n\u001b[31m  const spClr = { delivered:'var(--grn)', transit:'var(--ylw)', booked:'var(--sky)', pending:'var(--t4)' };·\u001b[39m\n\u001b[31m  chips.innerHTML = [\u001b[39m\n\u001b[31m    // This courier's dockets\u001b[39m\n\u001b[31m    ...allDockets.map(d => {\u001b[39m\n\u001b[31m      const sel = _trackSelected.has(d.docket);\u001b[39m\n\u001b[31m      const clr = spClr[d.status] || 'var(--t4)';\u001b[39m\n\u001b[31m      return `<button onclick=\\\"toggleTrackDocket('${d.docket}')\\\" id=\\\"chip_${d.docket}\\\"\u001b[39m\n\u001b[31m        style=\\\"font-family:var(--fnt-mono);font-size:.72rem;padding:4px 10px;border-radius:20px;\u001b[39m\n\u001b[31m        cursor:pointer;transition:all .2s;border:1px solid ${sel ? 'var(--acc)' : 'var(--bdr)'};\u001b[39m\n\u001b[31m        background:${sel ? 'rgba(249,115,22,.15)' : 'var(--sur2)'};\u001b[39m\n\u001b[31m        color:${sel ? 'var(--acc)' : 'var(--t2)'};\\\">\u001b[39m\n\u001b[31m        ${d.docket} <span style=\\\"font-size:.6rem;color:${clr};\\\">● ${d.status}</span>\u001b[39m\n\u001b[31m        ${d.city ? `<span style=\\\"font-size:.6rem;color:var(--t3);margin-left:4px;\\\">${d.city}</span>` : ''}\u001b[39m\n\u001b[31m      </button>`;\u001b[39m\n\u001b[31m    }),\u001b[39m\n\u001b[31m    // Other couriers — show greyed, click selects and switches courier\u001b[39m\n\u001b[31m    ...otherDockets.slice(0, 5).map(d =>\u001b[39m\n\u001b[31m      `<span title=\\\"${d.courier}\\\" style=\\\"font-family:var(--fnt-mono);font-size:.68rem;padding:3px 8px;\u001b[39m\n\u001b[31m        border-radius:20px;border:1px dashed var(--bdr);color:var(--t4);cursor:default;\\\">\u001b[39m\n\u001b[31m        ${d.docket} <span style=\\\"font-size:.55rem;\\\">${d.courier}</span>\u001b[39m\n\u001b[31m      </span>`\u001b[39m\n\u001b[31m    )\u001b[39m\n\u001b[31m  ].join('');·\u001b[39m\n\u001b[31m  updateTrackSummary();\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction toggleTrackDocket(docket) {\u001b[39m\n\u001b[31m  const courier = document.getElementById('t_courier').value;\u001b[39m\n\u001b[31m  const m = COURIER_MASTER[courier];\u001b[39m\n\u001b[31m  const maxBulk = m?.maxBulk || 1;·\u001b[39m\n\u001b[31m  if (_trackSelected.has(docket)) {\u001b[39m\n\u001b[31m    _trackSelected.delete(docket);\u001b[39m\n\u001b[31m  } else {\u001b[39m\n\u001b[31m    if (_trackSelected.size >= maxBulk) {\u001b[39m\n\u001b[31m      showToast(`${courier} supports max ${maxBulk} docket(s) at once`, 'err');\u001b[39m\n\u001b[31m      return;\u001b[39m\n\u001b[31m    }\u001b[39m\n\u001b[31m    _trackSelected.add(docket);\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m  buildTrackDocketChips(); // re-render chips with updated selection\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction addManualDockets() {\u001b[39m\n\u001b[31m  const raw = document.getElementById('t_manual_dockets').value;\u001b[39m\n\u001b[31m  const courier = document.getElementById('t_courier').value;\u001b[39m\n\u001b[31m  const m = COURIER_MASTER[courier];\u001b[39m\n\u001b[31m  const maxBulk = m?.maxBulk || 1;\u001b[39m\n\u001b[31m  const newDockets = raw.split(/[,\\\\n]/).map(d => d.trim()).filter(Boolean);\u001b[39m\n\u001b[31m  let added = 0;\u001b[39m\n\u001b[31m  for (const d of newDockets) {\u001b[39m\n\u001b[31m    if (_trackSelected.size >= maxBulk) {\u001b[39m\n\u001b[31m      showToast(`Max ${maxBulk} reached for ${courier}`, 'err');\u001b[39m\n\u001b[31m      break;\u001b[39m\n\u001b[31m    }\u001b[39m\n\u001b[31m    _trackSelected.add(d);\u001b[39m\n\u001b[31m    added++;\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m  document.getElementById('t_manual_dockets').value = '';\u001b[39m\n\u001b[31m  buildTrackDocketChips();\u001b[39m\n\u001b[31m  if (added) showToast(`${added} docket(s) added ✓`, 'ok');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction clearTrackSelection() {\u001b[39m\n\u001b[31m  _trackSelected.clear();\u001b[39m\n\u001b[31m  buildTrackDocketChips();\u001b[39m\n\u001b[31m  document.getElementById('trackResultsArea').innerHTML = '';\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction updateTrackSummary() {\u001b[39m\n\u001b[31m  const summary = document.getElementById('trackSelectedSummary');\u001b[39m\n\u001b[31m  const countEl = document.getElementById('trackSelectedCount');\u001b[39m\n\u001b[31m  const listEl  = document.getElementById('trackSelectedList');\u001b[39m\n\u001b[31m  if (!summary) return;\u001b[39m\n\u001b[31m  const arr = Array.from(_trackSelected);\u001b[39m\n\u001b[31m  summary.style.display = arr.length ? 'block' : 'none';\u001b[39m\n\u001b[31m  if (countEl) countEl.textContent = `${arr.length} docket(s) selected`;\u001b[39m\n\u001b[31m  if (listEl)  listEl.textContent  = arr.slice(0, 5).join(', ') + (arr.length > 5 ? `... +${arr.length-5}` : '');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction openTrackOnCourierSite() {\u001b[39m\n\u001b[31m  const courier = document.getElementById('t_courier').value;\u001b[39m\n\u001b[31m  const dockets = Array.from(_trackSelected);\u001b[39m\n\u001b[31m  if (!dockets.length) { showToast('Select dockets first', 'err'); return; }\u001b[39m\n\u001b[31m  const m = COURIER_MASTER[courier];\u001b[39m\n\u001b[31m  if (!m) return;\u001b[39m\n\u001b[31m  // SkyKing: open with all CNOs comma-separated in URL\u001b[39m\n\u001b[31m  let url;\u001b[39m\n\u001b[31m  if (courier === 'SkyKing') {\u001b[39m\n\u001b[31m    url = `https://skyking.co/track?cno=${dockets.join(',')}`;\u001b[39m\n\u001b[31m  } else if (dockets.length === 1) {\u001b[39m\n\u001b[31m    url = m.trackUrl.replace('{docket}', dockets[0]);\u001b[39m\n\u001b[31m  } else {\u001b[39m\n\u001b[31m    url = m.trackBulkUrl || m.trackUrl.replace('{docket}', dockets[0]);\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m  window.open(url, '_blank');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31masync function trackSelected() {\u001b[39m\n\u001b[31m  const courier = document.getElementById('t_courier').value;\u001b[39m\n\u001b[31m  const dockets = Array.from(_trackSelected);\u001b[39m\n\u001b[31m  if (!dockets.length) { showToast('Select at least one docket', 'err'); return; }·\u001b[39m\n\u001b[31m  const area = document.getElementById('trackResultsArea');\u001b[39m\n\u001b[31m  area.innerHTML = `<div class=\\\"card\\\" style=\\\"text-align:center;padding:20px;\\\">\u001b[39m\n\u001b[31m    <div style=\\\"font-size:1.5rem;margin-bottom:8px;\\\">⏳</div>\u001b[39m\n\u001b[31m    <div style=\\\"font-family:var(--fnt-mono);font-size:.78rem;color:var(--t2);\\\">Tracking ${dockets.length} docket(s) via ${courier}...</div>\u001b[39m\n\u001b[31m  </div>`;·\u001b[39m\n\u001b[31m  let resultsHTML = '';\u001b[39m\n\u001b[31m  for (const docket of dockets) {\u001b[39m\n\u001b[31m    const result = await fetchTracking(docket, courier);\u001b[39m\n\u001b[31m    const sp = (!result.status || result.status === 'failed') ? 'pending' : result.status;\u001b[39m\n\u001b[31m    const spClr = { delivered:'var(--grn)', transit:'var(--ylw)', booked:'var(--sky)', pending:'var(--t3)' };\u001b[39m\n\u001b[31m    const clr = spClr[sp] || 'var(--t3)';\u001b[39m\n\u001b[31m    // Update stored dispatch if this docket exists in our records\u001b[39m\n\u001b[31m    for (const d of dispatches) {\u001b[39m\n\u001b[31m      for (const c of (d.consignees||[])) {\u001b[39m\n\u001b[31m        if (c.docket === docket && result.status && result.status !== 'failed') {\u001b[39m\n\u001b[31m          c.status = result.status;\u001b[39m\n\u001b[31m          c.lastEvent = result.lastEvent;\u001b[39m\n\u001b[31m          c.lastTrackedAt = new Date().toISOString();\u001b[39m\n\u001b[31m        }\u001b[39m\n\u001b[31m      }\u001b[39m\n\u001b[31m    }\u001b[39m\n\u001b[31m    resultsHTML += `\u001b[39m\n\u001b[31m    <div style=\\\"background:var(--sur);border:1px solid var(--bdr);border-left:3px solid ${clr};\u001b[39m\n\u001b[31m      border-radius:var(--r-sm);padding:10px 12px;margin-bottom:8px;\\\">\u001b[39m\n\u001b[31m      <div style=\\\"display:flex;justify-content:space-between;align-items:center;margin-bottom:6px;\\\">\u001b[39m\n\u001b[31m        <span style=\\\"font-family:var(--fnt-mono);font-size:.9rem;color:var(--sky);\\\">${docket}</span>\u001b[39m\n\u001b[31m        <span class=\\\"status-pill sp-${sp}\\\">${sp}</span>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      <div style=\\\"font-size:.75rem;color:var(--t2);font-family:var(--fnt-mono);\\\">${result.lastEvent || '—'}</div>\u001b[39m\n\u001b[31m      ${result.lastScanLocation ? `<div style=\\\"font-size:.68rem;color:var(--t3);margin-top:3px;\\\">📍 ${result.lastScanLocation}</div>` : ''}\u001b[39m\n\u001b[31m      <div style=\\\"display:flex;gap:8px;margin-top:8px;\\\">\u001b[39m\n\u001b[31m        <button class=\\\"btn-sm\\\" onclick=\\\"openCourierTrackUrl('${docket}','${courier}')\\\">🔗 Open on ${courier}</button>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m    </div>`;\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m  saveData();\u001b[39m\n\u001b[31m  area.innerHTML = `<div style=\\\"margin-top:4px;\\\">${resultsHTML}</div>`;\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// Keep old names as aliases for compatibility\u001b[39m\n\u001b[31mfunction trackMultiple() { trackSelected(); }\u001b[39m\n\u001b[31mfunction openCourierSite() { openTrackOnCourierSite(); }·\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31m// HISTORY\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31mfunction renderHistory() {\u001b[39m\n\u001b[31m  const list = document.getElementById('historyList');\u001b[39m\n\u001b[31m  if (!dispatches.length) {\u001b[39m\n\u001b[31m    list.innerHTML = '<div class=\\\"empty\\\"><div class=\\\"e-icon\\\">📭</div><p>No dispatches yet.</p></div>';\u001b[39m\n\u001b[31m    return;\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m  list.innerHTML = dispatches.map(d => {\u001b[39m\n\u001b[31m    const cons = d.consignees?.length || 0;\u001b[39m\n\u001b[31m    const docketed = d.consignees?.filter(c=>c.docket).length || 0;\u001b[39m\n\u001b[31m    const clr = d.status === 'draft' ? 'var(--ylw)' : 'var(--grn)';\u001b[39m\n\u001b[31m    return `\u001b[39m\n\u001b[31m    <div class=\\\"docket-tile\\\" onclick=\\\"showDispatchDetail('${d.id}')\\\">\u001b[39m\n\u001b[31m      <div class=\\\"docket-tile-hdr\\\">\u001b[39m\n\u001b[31m        <div class=\\\"docket-tile-left\\\">\u001b[39m\n\u001b[31m          <div class=\\\"docket-num\\\" style=\\\"font-size:.78rem;\\\">${d.id}</div>\u001b[39m\n\u001b[31m          <div class=\\\"docket-store\\\">${d.client||'No client'} ${d.jobref ? '› '+d.jobref : ''}</div>\u001b[39m\n\u001b[31m          <div class=\\\"docket-courier\\\">📦 ${cons} consignees · ${docketed} docketed · ${d.courier||'—'} · ${d.dispatchDate||d.createdAt?.slice(0,10)||'—'}</div>\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m        <span class=\\\"status-pill\\\" style=\\\"background:rgba(0,0,0,.2);color:${clr};\\\">${d.status||'active'}</span>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m    </div>`;\u001b[39m\n\u001b[31m  }).join('');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction showDispatchDetail(id) {\u001b[39m\n\u001b[31m  const d = dispatches.find(x => x.id === id);\u001b[39m\n\u001b[31m  if (!d) return;\u001b[39m\n\u001b[31m  const content = document.getElementById('detailContent');\u001b[39m\n\u001b[31m  content.innerHTML = `\u001b[39m\n\u001b[31m    <div class=\\\"sheet-title\\\">${d.client} ${d.jobref ? '› '+d.jobref : ''}</div>\u001b[39m\n\u001b[31m    <div class=\\\"info-tag\\\"><strong>${d.id}</strong> · ${d.courier||'—'} · ${d.dispatchDate||'—'}</div>\u001b[39m\n\u001b[31m    <div class=\\\"sec-div\\\"><span>Consignees (${d.consignees?.length||0})</span></div>\u001b[39m\n\u001b[31m    ${(d.consignees||[]).map((c,i) => `\u001b[39m\n\u001b[31m      <div style=\\\"background:var(--sur2);border:1px solid var(--bdr);border-radius:var(--r-sm);padding:10px;margin-bottom:6px;\\\">\u001b[39m\n\u001b[31m        <div style=\\\"font-weight:600;margin-bottom:4px;\\\">${i+1}. ${c.name} — ${c.city}</div>\u001b[39m\n\u001b[31m        ${c.docket ? `<div style=\\\"font-family:var(--fnt-mono);font-size:.78rem;color:var(--sky);\\\">🔖 ${c.docket}</div>` : ''}\u001b[39m\n\u001b[31m        ${c.material ? `<div style=\\\"font-size:.75rem;color:var(--t2);\\\">📦 ${c.material} ${c.qty ? '('+c.qty+')':''}</div>` : ''}\u001b[39m\n\u001b[31m        <span class=\\\"status-pill sp-${c.status||'pending'}\\\" style=\\\"margin-top:4px;display:inline-block;\\\">${c.status||'pending'}</span>\u001b[39m\n\u001b[31m      </div>`).join('')}\u001b[39m\n\u001b[31m    ${d.remarks ? `<div class=\\\"info-tag\\\">📝 ${d.remarks}</div>` : ''}\u001b[39m\n\u001b[31m    <div class=\\\"btn-row\\\" style=\\\"margin-top:14px;\\\">\u001b[39m\n\u001b[31m      <button class=\\\"btn btn-ghost\\\" onclick=\\\"deleteDispatch('${d.id}');closeOverlay('detailOverlay')\\\">🗑️ Delete</button>\u001b[39m\n\u001b[31m      <button class=\\\"btn btn-orange\\\" onclick=\\\"closeOverlay('detailOverlay');showPane('dashboard')\\\">📊 View in Dashboard</button>\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m  `;\u001b[39m\n\u001b[31m  openOverlay('detailOverlay');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction deleteDispatch(id) {\u001b[39m\n\u001b[31m  if (!confirm('Delete this dispatch?')) return;\u001b[39m\n\u001b[31m  dispatches = dispatches.filter(d => d.id !== id);\u001b[39m\n\u001b[31m  saveData();\u001b[39m\n\u001b[31m  updateHistCount();\u001b[39m\n\u001b[31m  renderHistory();\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction updateHistCount() {\u001b[39m\n\u001b[31m  document.getElementById('histCount').textContent = dispatches.length;\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31m// GOOGLE SHEETS\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31masync function logToSheets(d) {\u001b[39m\n\u001b[31m  const url = window.appConfig.webhook_url;\u001b[39m\n\u001b[31m  if (!url) return;\u001b[39m\n\u001b[31m  try {\u001b[39m\n\u001b[31m    await fetch(url, {\u001b[39m\n\u001b[31m      method: 'POST', mode: 'no-cors',\u001b[39m\n\u001b[31m      headers: { 'Content-Type': 'application/json' },\u001b[39m\n\u001b[31m      body: JSON.stringify({\u001b[39m\n\u001b[31m        type: 'dispatch', id: d.id, client: d.client, jobref: d.jobref,\u001b[39m\n\u001b[31m        courier: d.courier, consignees: d.consignees?.length||0,\u001b[39m\n\u001b[31m        destinations: d.consignees?.map(c=>c.city).join(', ')||'',\u001b[39m\n\u001b[31m        dockets: d.consignees?.filter(c=>c.docket).map(c=>c.docket).join(', ')||'',\u001b[39m\n\u001b[31m        dispatchDate: d.dispatchDate, status: d.status,\u001b[39m\n\u001b[31m        timestamp: new Date().toISOString(), company: window.appConfig.company\u001b[39m\n\u001b[31m      })\u001b[39m\n\u001b[31m    });\u001b[39m\n\u001b[31m  } catch(e) {}\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31m// CONFIG — Google Sheet is the source of truth\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════·\u001b[39m\n\u001b[31mfunction onSheetsUrlChange() {\u001b[39m\n\u001b[31m  const url = document.getElementById('cfg_sheets_url').value.trim();\u001b[39m\n\u001b[31m  // Show sync button state\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction saveSheetsUrl() {\u001b[39m\n\u001b[31m  const url = document.getElementById('cfg_sheets_url').value.trim();\u001b[39m\n\u001b[31m  if (!url) { showToast('Enter the Apps Script URL first', 'err'); return; }\u001b[39m\n\u001b[31m  window.appConfig.sheets_url = url;\u001b[39m\n\u001b[31m  saveSheetsUrlLocal();\u001b[39m\n\u001b[31m  syncFromSheet(false);\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m/**\u001b[39m\n\u001b[31m * Fetch all config from Google Sheet and apply to appConfig.\u001b[39m\n\u001b[31m * Called on startup (silent=true) and manually (silent=false).\u001b[39m\n\u001b[31m */\u001b[39m\n\u001b[31masync function syncFromSheet(silent = false) {\u001b[39m\n\u001b[31m  const url = window.appConfig.sheets_url;\u001b[39m\n\u001b[31m  if (!url) { if (!silent) showToast('No Sheets URL configured', 'err'); return; }·\u001b[39m\n\u001b[31m  setSyncStatus('syncing', 'Syncing config from Google Sheet...');\u001b[39m\n\u001b[31m  try {\u001b[39m\n\u001b[31m    const resp = await fetch(url + '?action=pwa_config', {\u001b[39m\n\u001b[31m      method: 'GET',\u001b[39m\n\u001b[31m      redirect: 'follow'\u001b[39m\n\u001b[31m    });\u001b[39m\n\u001b[31m    if (!resp.ok) throw new Error('HTTP ' + resp.status);\u001b[39m\n\u001b[31m    const cfg = await resp.json();·\u001b[39m\n\u001b[31m    // Apply all values from sheet to appConfig\u001b[39m\n\u001b[31m    const keyMap = {\u001b[39m\n\u001b[31m      proxy_url:           'proxy_url',\u001b[39m\n\u001b[31m      webhook_url:         'webhook_url',\u001b[39m\n\u001b[31m      ollama_url:          'ollama_url',\u001b[39m\n\u001b[31m      ocr_mode:            'ocr_mode',\u001b[39m\n\u001b[31m      company:             'company',\u001b[39m\n\u001b[31m      city:                'city',\u001b[39m\n\u001b[31m      auto_log:            'auto_log',\u001b[39m\n\u001b[31m      refresh_on_load:     'refresh_on_load',\u001b[39m\n\u001b[31m      admin_password:      'admin_password',\u001b[39m\n\u001b[31m      n8n_dispatch_webhook:'n8n_dispatch_webhook',\u001b[39m\n\u001b[31m    };\u001b[39m\n\u001b[31m    Object.entries(keyMap).forEach(([sheetKey, cfgKey]) => {\u001b[39m\n\u001b[31m      if (cfg[sheetKey] !== undefined && cfg[sheetKey] !== '') {\u001b[39m\n\u001b[31m        window.appConfig[cfgKey] = cfg[sheetKey];\u001b[39m\n\u001b[31m      }\u001b[39m\n\u001b[31m    });·\u001b[39m\n\u001b[31m    // Apply courier master if provided\u001b[39m\n\u001b[31m    if (cfg._courier_master && Array.isArray(cfg._courier_master)) {\u001b[39m\n\u001b[31m      cfg._courier_master.forEach(c => {\u001b[39m\n\u001b[31m        if (c.name && c.trackUrl) {\u001b[39m\n\u001b[31m          COURIER_MASTER[c.name] = {\u001b[39m\n\u001b[31m            trackUrl: c.trackUrl,\u001b[39m\n\u001b[31m            trackBulkUrl: c.bulkUrl || '',\u001b[39m\n\u001b[31m            maxBulk: parseInt(c.maxBulk) || 1\u001b[39m\n\u001b[31m          };\u001b[39m\n\u001b[31m        }\u001b[39m\n\u001b[31m      });\u001b[39m\n\u001b[31m    }·\u001b[39m\n\u001b[31m    renderCourierMasterList();\u001b[39m\n\u001b[31m    renderSheetConfigDisplay(cfg);\u001b[39m\n\u001b[31m    setSyncStatus('ok', 'Synced from sheet · ' + new Date().toLocaleTimeString());\u001b[39m\n\u001b[31m    if (!silent) showToast('Config synced from sheet ✓', 'ok');·\u001b[39m\n\u001b[31m    // If refresh_on_load is true, trigger dashboard refresh\u001b[39m\n\u001b[31m    if (window.appConfig.refresh_on_load === 'true') {\u001b[39m\n\u001b[31m      setTimeout(refreshAllTracking, 2000);\u001b[39m\n\u001b[31m    }·\u001b[39m\n\u001b[31m  } catch(e) {\u001b[39m\n\u001b[31m    setSyncStatus('err', 'Sync failed: ' + e.message);\u001b[39m\n\u001b[31m    if (!silent) showToast('Sync failed — check URL', 'err');\u001b[39m\n\u001b[31m    console.error('Sheets sync error:', e);\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction setSyncStatus(state, msg) {\u001b[39m\n\u001b[31m  const el = document.getElementById('syncStatus');\u001b[39m\n\u001b[31m  const icon = document.getElementById('syncIcon');\u001b[39m\n\u001b[31m  const msgEl = document.getElementById('syncMsg');\u001b[39m\n\u001b[31m  el.style.display = 'flex';\u001b[39m\n\u001b[31m  icon.textContent = state === 'ok' ? '✅' : state === 'err' ? '❌' : '⏳';\u001b[39m\n\u001b[31m  msgEl.textContent = msg;\u001b[39m\n\u001b[31m  el.style.borderColor = state === 'ok' ? 'rgba(34,197,94,.3)' : state === 'err' ? 'rgba(239,68,68,.3)' : 'rgba(234,179,8,.3)';\u001b[39m\n\u001b[31m  el.style.background = state === 'ok' ? 'rgba(34,197,94,.05)' : state === 'err' ? 'rgba(239,68,68,.05)' : 'rgba(234,179,8,.05)';\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction renderSheetConfigDisplay(cfg) {\u001b[39m\n\u001b[31m  const display = document.getElementById('sheetConfigDisplay');\u001b[39m\n\u001b[31m  display.style.display = 'block';\u001b[39m\n\u001b[31m  document.getElementById('sheetSyncTime').textContent = 'Last sync: ' + new Date().toLocaleTimeString();\u001b[39m\n\u001b[31m  const showKeys = ['proxy_url','webhook_url','ollama_url','ocr_mode','company','city','auto_log'];\u001b[39m\n\u001b[31m  document.getElementById('sheetConfigRows').innerHTML = showKeys.map(k => {\u001b[39m\n\u001b[31m    const val = cfg[k] || window.appConfig[k] || '—';\u001b[39m\n\u001b[31m    const isEmpty = !cfg[k];\u001b[39m\n\u001b[31m    return `<div class=\\\"courier-master-row\\\">\u001b[39m\n\u001b[31m      <div class=\\\"cml\\\">\u001b[39m\n\u001b[31m        <div class=\\\"cml-name\\\" style=\\\"font-family:var(--fnt-mono);font-size:.75rem;\\\">${k}</div>\u001b[39m\n\u001b[31m        <div class=\\\"cml-url\\\" style=\\\"${isEmpty ? 'color:var(--red);' : ''}\\\">${isEmpty ? '⚠️ Not set in sheet' : val}</div>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m    </div>`;\u001b[39m\n\u001b[31m  }).join('');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m/**\u001b[39m\n\u001b[31m * Push current config changes back to the sheet.\u001b[39m\n\u001b[31m * Called when user edits config from PWA (rare — usually done in sheet directly).\u001b[39m\n\u001b[31m */\u001b[39m\n\u001b[31masync function pushConfigToSheet(updates) {\u001b[39m\n\u001b[31m  const url = window.appConfig.sheets_url;\u001b[39m\n\u001b[31m  if (!url) return;\u001b[39m\n\u001b[31m  try {\u001b[39m\n\u001b[31m    await fetch(url, {\u001b[39m\n\u001b[31m      method: 'POST', mode: 'no-cors',\u001b[39m\n\u001b[31m      headers: { 'Content-Type': 'application/json' },\u001b[39m\n\u001b[31m      body: JSON.stringify({ type: 'save_config', config: updates })\u001b[39m\n\u001b[31m    });\u001b[39m\n\u001b[31m  } catch(e) {}\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction clearAll() {\u001b[39m\n\u001b[31m  if (!confirm('Delete ALL dispatch data?')) return;\u001b[39m\n\u001b[31m  dispatches = [];\u001b[39m\n\u001b[31m  saveData();\u001b[39m\n\u001b[31m  updateHistCount();\u001b[39m\n\u001b[31m  renderDashboard();\u001b[39m\n\u001b[31m  renderHistory();\u001b[39m\n\u001b[31m  showToast('Cleared', 'ok');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction renderCourierMasterList() {\u001b[39m\n\u001b[31m  const el = document.getElementById('courierMasterList');\u001b[39m\n\u001b[31m  el.innerHTML = Object.entries(COURIER_MASTER).map(([name, info]) => `\u001b[39m\n\u001b[31m    <div class=\\\"courier-master-row\\\">\u001b[39m\n\u001b[31m      <div class=\\\"cml\\\">\u001b[39m\n\u001b[31m        <div class=\\\"cml-name\\\">${name}</div>\u001b[39m\n\u001b[31m        <div class=\\\"cml-url\\\">${info.trackUrl}</div>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      <button class=\\\"btn-sm\\\" onclick=\\\"window.open('${info.trackBulkUrl||info.trackUrl.replace('{docket}','')}','_blank')\\\">Open</button>\u001b[39m\n\u001b[31m    </div>`).join('');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction openCourierMaster() {\u001b[39m\n\u001b[31m  const ed = document.getElementById('courierMasterEditor');\u001b[39m\n\u001b[31m  ed.innerHTML = Object.entries(COURIER_MASTER).map(([name, info]) => `\u001b[39m\n\u001b[31m    <div style=\\\"margin-bottom:14px;\\\">\u001b[39m\n\u001b[31m      <div style=\\\"font-family:var(--fnt-head);font-size:.75rem;font-weight:700;color:var(--t2);text-transform:uppercase;letter-spacing:1.5px;margin-bottom:6px;\\\">${name}</div>\u001b[39m\n\u001b[31m      <div class=\\\"f\\\" style=\\\"margin-bottom:6px;\\\">\u001b[39m\n\u001b[31m        <label>Track URL (use {docket})</label>\u001b[39m\n\u001b[31m        <input type=\\\"text\\\" value=\\\"${info.trackUrl}\\\" id=\\\"cm_${name.replace(/\\\\s/g,'_')}\\\"\u001b[39m\n\u001b[31m          style=\\\"font-family:var(--fnt-mono);font-size:.75rem;\\\">\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m    </div>`).join('') +\u001b[39m\n\u001b[31m    `<button class=\\\"btn btn-orange\\\" onclick=\\\"saveCourierMaster()\\\">Save Courier Master</button>`;\u001b[39m\n\u001b[31m  openOverlay('courierMasterOverlay');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction saveCourierMaster() {\u001b[39m\n\u001b[31m  Object.keys(COURIER_MASTER).forEach(name => {\u001b[39m\n\u001b[31m    const el = document.getElementById('cm_' + name.replace(/\\\\s/g,'_'));\u001b[39m\n\u001b[31m    if (el) COURIER_MASTER[name].trackUrl = el.value;\u001b[39m\n\u001b[31m  });\u001b[39m\n\u001b[31m  closeOverlay('courierMasterOverlay');\u001b[39m\n\u001b[31m  renderCourierMasterList();\u001b[39m\n\u001b[31m  showToast('Courier master saved ✓', 'ok');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31m// UI UTILS\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31m// ── ADMIN AUTH ───────────────────────────────────────────────────────────\u001b[39m\n\u001b[31mlet _adminAuthed = false;\u001b[39m\n\u001b[31mlet _lastAdminPane = null;·\u001b[39m\n\u001b[31mfunction showPane(name) {\u001b[39m\n\u001b[31m  // Config tab requires admin auth — re-ask if navigated away\u001b[39m\n\u001b[31m  if (name === 'config') {\u001b[39m\n\u001b[31m    if (!_adminAuthed) {\u001b[39m\n\u001b[31m      _lastAdminPane = 'config';\u001b[39m\n\u001b[31m      showAdminLogin();\u001b[39m\n\u001b[31m      return;\u001b[39m\n\u001b[31m    }\u001b[39m\n\u001b[31m  } else {\u001b[39m\n\u001b[31m    // User navigated away from config — revoke auth\u001b[39m\n\u001b[31m    if (_lastAdminPane === 'config') _adminAuthed = false;\u001b[39m\n\u001b[31m    _lastAdminPane = name;\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m  document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));\u001b[39m\n\u001b[31m  document.querySelectorAll('.pane').forEach(p => p.classList.remove('active'));\u001b[39m\n\u001b[31m  document.getElementById('tab-'+name).classList.add('active');\u001b[39m\n\u001b[31m  document.getElementById('pane-'+name).classList.add('active');\u001b[39m\n\u001b[31m  if (name === 'dashboard') renderDashboard();\u001b[39m\n\u001b[31m  if (name === 'history') renderHistory();\u001b[39m\n\u001b[31m  if (name === 'track') buildTrackDocketChips();\u001b[39m\n\u001b[31m  if (name === 'config') { document.getElementById('cfg_sheets_url').value = window.appConfig.sheets_url || ''; }\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction showAdminLogin() {\u001b[39m\n\u001b[31m  // Password replaced by hub identity — only harish/pramod can access config\u001b[39m\n\u001b[31m  if (['harish', 'pramod'].includes(window._hubUser?.id)) {\u001b[39m\n\u001b[31m    _adminAuthed = true;\u001b[39m\n\u001b[31m    showPane('config');\u001b[39m\n\u001b[31m  } else {\u001b[39m\n\u001b[31m    showToast('Config access restricted to admin users', 'err');\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31masync function submitAdminLogin() {\u001b[39m\n\u001b[31m  showAdminLogin(); // no-op shim — kept for any lingering HTML onclick refs\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31mfunction openOverlay(id) { document.getElementById(id).classList.add('open'); }\u001b[39m\n\u001b[31mfunction closeOverlay(id) { document.getElementById(id).classList.remove('open'); }·\u001b[39m\n\u001b[31mlet toastTimer;\u001b[39m\n\u001b[31mfunction showToast(msg, type='') {\u001b[39m\n\u001b[31m  const t = document.getElementById('toast');\u001b[39m\n\u001b[31m  t.textContent = msg;\u001b[39m\n\u001b[31m  t.className = 'toast show' + (type ? ' '+type : '');\u001b[39m\n\u001b[31m  clearTimeout(toastTimer);\u001b[39m\n\u001b[31m  toastTimer = setTimeout(() => t.classList.remove('show'), 2800);\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction showOCRWorking(show, msg='') {\u001b[39m\n\u001b[31m  const el = document.getElementById('ocrWorking');\u001b[39m\n\u001b[31m  el.classList.toggle('visible', show);\u001b[39m\n\u001b[31m  if (msg) document.getElementById('ocrWorkingText').textContent = msg;\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction showAIProgress(label, pct) {\u001b[39m\n\u001b[31m  const el = document.getElementById('aiProgress');\u001b[39m\n\u001b[31m  el.classList.add('visible');\u001b[39m\n\u001b[31m  document.getElementById('aiProgLabel').textContent = label;\u001b[39m\n\u001b[31m  document.getElementById('aiProgFill').style.width = pct + '%';\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31mfunction hideAIProgress() {\u001b[39m\n\u001b[31m  document.getElementById('aiProgress').classList.remove('visible');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction updateNetDot() {\u001b[39m\n\u001b[31m  document.getElementById('netDot').style.background = navigator.onLine ? 'var(--grn)' : 'var(--red)';\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction setToday() {\u001b[39m\n\u001b[31m  document.getElementById('n_date').value = new Date().toISOString().split('T')[0];\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m// ── EXPORT CLEAN SHEET ───────────────────────────────────────────────────\u001b[39m\n\u001b[31mfunction exportCleanSheet() {\u001b[39m\n\u001b[31m  const pane = document.getElementById('verifyPane');\u001b[39m\n\u001b[31m  const d = pane._pendingDispatch;\u001b[39m\n\u001b[31m  if (!d || !d.consignees?.length) { showToast('No data to export', 'err'); return; }·\u001b[39m\n\u001b[31m  // Build CSV\u001b[39m\n\u001b[31m  const headers = ['S.No.','Store Name','City','Pincode','Phone','Address','Docket No.','Material','Courier','Status'];\u001b[39m\n\u001b[31m  const rows = d.consignees.map((c, i) => [\u001b[39m\n\u001b[31m    i + 1,\u001b[39m\n\u001b[31m    `\\\"${(c.name||'').replace(/\\\"/g,'\\\"\\\"')}\\\"`,\u001b[39m\n\u001b[31m    `\\\"${(c.city||'').replace(/\\\"/g,'\\\"\\\"')}\\\"`,\u001b[39m\n\u001b[31m    c.pincode || '',\u001b[39m\n\u001b[31m    c.phone || '',\u001b[39m\n\u001b[31m    `\\\"${(c.address||'').replace(/\\\"/g,'\\\"\\\"')}\\\"`,\u001b[39m\n\u001b[31m    c.docket || '',\u001b[39m\n\u001b[31m    `\\\"${(c.material||'').replace(/\\\"/g,'\\\"\\\"')}\\\"`,\u001b[39m\n\u001b[31m    d.courier || '',\u001b[39m\n\u001b[31m    c.status || 'pending'\u001b[39m\n\u001b[31m  ]);·\u001b[39m\n\u001b[31m  const csv = [headers.join(','), ...rows.map(r => r.join(','))].join('\\\\n');\u001b[39m\n\u001b[31m  const blob = new Blob([csv], { type: 'text/csv' });\u001b[39m\n\u001b[31m  const url = URL.createObjectURL(blob);\u001b[39m\n\u001b[31m  const a = document.createElement('a');\u001b[39m\n\u001b[31m  a.href = url;\u001b[39m\n\u001b[31m  a.download = `360LM_Dispatch_${d.client||'export'}_${d.dispatchDate||new Date().toISOString().slice(0,10)}_clean.csv`;\u001b[39m\n\u001b[31m  a.click();\u001b[39m\n\u001b[31m  URL.revokeObjectURL(url);\u001b[39m\n\u001b[31m  showToast('Clean sheet exported ✓', 'ok');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// ── DOCKET SLIP UPLOAD — SKIP OCR IF DOCKET ALREADY MAPPED ───────────────\u001b[39m\n\u001b[31mfunction shouldSkipOCR() {\u001b[39m\n\u001b[31m  // If any consignee already has a docket (from store list mapping), skip OCR extraction\u001b[39m\n\u001b[31m  const consList = document.getElementById('consList');\u001b[39m\n\u001b[31m  if (!consList) return false;\u001b[39m\n\u001b[31m  const cards = consList.querySelectorAll('.cons-card');\u001b[39m\n\u001b[31m  let hasAnyDocket = false;\u001b[39m\n\u001b[31m  cards.forEach(card => {\u001b[39m\n\u001b[31m    const d = card.querySelector('.cons-docket-input')?.value?.trim();\u001b[39m\n\u001b[31m    if (d) hasAnyDocket = true;\u001b[39m\n\u001b[31m  });\u001b[39m\n\u001b[31m  return hasAnyDocket;\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// ─── TRACKING UPDATE TO SHEETS ──────────────────────────────────────────\u001b[39m\n\u001b[31masync function logTrackingUpdate(docket, status, lastEvent, location) {\u001b[39m\n\u001b[31m  const url = window.appConfig.webhook_url;\u001b[39m\n\u001b[31m  if (!url) return;\u001b[39m\n\u001b[31m  try {\u001b[39m\n\u001b[31m    await fetch(url, {\u001b[39m\n\u001b[31m      method: 'POST', mode: 'no-cors',\u001b[39m\n\u001b[31m      headers: { 'Content-Type': 'application/json' },\u001b[39m\n\u001b[31m      body: JSON.stringify({\u001b[39m\n\u001b[31m        type: 'tracking_update',\u001b[39m\n\u001b[31m        docket, status, lastEvent, location,\u001b[39m\n\u001b[31m        timestamp: new Date().toISOString()\u001b[39m\n\u001b[31m      })\u001b[39m\n\u001b[31m    });\u001b[39m\n\u001b[31m  } catch(e) {}\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// ─── POD / RECIPIENT PHOTO UPLOAD ───────────────────────────────────────\u001b[39m\n\u001b[31m// Stores base64 locally, pushes to Sheets webhook (Apps Script saves to Drive)\u001b[39m\n\u001b[31mfunction showPODUpload(docket, dispatchId) {\u001b[39m\n\u001b[31m  const overlay = document.getElementById('podOverlay');\u001b[39m\n\u001b[31m  overlay.dataset.docket = docket;\u001b[39m\n\u001b[31m  overlay.dataset.dispatchId = dispatchId;\u001b[39m\n\u001b[31m  document.getElementById('podPreview').innerHTML = '';\u001b[39m\n\u001b[31m  document.getElementById('podFileInput').value = '';\u001b[39m\n\u001b[31m  document.getElementById('recipientFileInput').value = '';\u001b[39m\n\u001b[31m  openOverlay('podOverlay');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31masync function uploadPODFiles() {\u001b[39m\n\u001b[31m  const overlay = document.getElementById('podOverlay');\u001b[39m\n\u001b[31m  const docket = overlay.dataset.docket;\u001b[39m\n\u001b[31m  const dispatchId = overlay.dataset.dispatchId;\u001b[39m\n\u001b[31m  const podFile = document.getElementById('podFileInput').files[0];\u001b[39m\n\u001b[31m  const recipFile = document.getElementById('recipientFileInput').files[0];\u001b[39m\n\u001b[31m  if (!podFile && !recipFile) { showToast('Select at least one photo', 'err'); return; }·\u001b[39m\n\u001b[31m  showToast('Uploading...', 'info');\u001b[39m\n\u001b[31m  const toBase64 = f => new Promise((res,rej) => {\u001b[39m\n\u001b[31m    const r = new FileReader(); r.onload = e => res(e.target.result); r.onerror = rej; r.readAsDataURL(f);\u001b[39m\n\u001b[31m  });·\u001b[39m\n\u001b[31m  const payload = {\u001b[39m\n\u001b[31m    type: 'pod_upload',\u001b[39m\n\u001b[31m    docket, dispatchId,\u001b[39m\n\u001b[31m    timestamp: new Date().toISOString()\u001b[39m\n\u001b[31m  };\u001b[39m\n\u001b[31m  if (podFile)   payload.pod_image = await toBase64(podFile);\u001b[39m\n\u001b[31m  if (recipFile) payload.recipient_image = await toBase64(recipFile);·\u001b[39m\n\u001b[31m  // Update local store\u001b[39m\n\u001b[31m  for (const d of dispatches) {\u001b[39m\n\u001b[31m    if (d.id !== dispatchId) continue;\u001b[39m\n\u001b[31m    for (const c of d.consignees) {\u001b[39m\n\u001b[31m      if (c.docket === docket) {\u001b[39m\n\u001b[31m        c.podUploaded = true;\u001b[39m\n\u001b[31m        c.status = 'delivered';\u001b[39m\n\u001b[31m      }\u001b[39m\n\u001b[31m    }\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m  saveData();·\u001b[39m\n\u001b[31m  // Push to Sheets (Apps Script saves images to Drive, stores URL in sheet)\u001b[39m\n\u001b[31m  if (window.appConfig.webhook_url) {\u001b[39m\n\u001b[31m    try {\u001b[39m\n\u001b[31m      await fetch(window.appConfig.webhook_url, {\u001b[39m\n\u001b[31m        method: 'POST', mode: 'no-cors',\u001b[39m\n\u001b[31m        headers: { 'Content-Type': 'application/json' },\u001b[39m\n\u001b[31m        body: JSON.stringify(payload)\u001b[39m\n\u001b[31m      });\u001b[39m\n\u001b[31m    } catch(e) {}\u001b[39m\n\u001b[31m  }·\u001b[39m\n\u001b[31m  closeOverlay('podOverlay');\u001b[39m\n\u001b[31m  renderDashboard();\u001b[39m\n\u001b[31m  showToast('POD uploaded ✓ — marked as delivered', 'ok');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// ─── WHATSAPP DISPATCH TABLE SHARE ──────────────────────────────────────\u001b[39m\n\u001b[31mfunction shareDispatchToWhatsApp(dispatchId) {\u001b[39m\n\u001b[31m  const d = dispatches.find(x => x.id === dispatchId);\u001b[39m\n\u001b[31m  if (!d) return;\u001b[39m\n\u001b[31m  const lines = [\u001b[39m\n\u001b[31m    `📦 *Dispatch Update — ${d.client} ${d.jobref ? '› '+d.jobref : ''}*`,\u001b[39m\n\u001b[31m    `🚚 Courier: ${d.courier} | Date: ${d.dispatchDate || d.createdAt?.slice(0,10)}`,\u001b[39m\n\u001b[31m    ``,\u001b[39m\n\u001b[31m    ...d.consignees.filter(c => c.docket).map((c,i) =>\u001b[39m\n\u001b[31m      `${i+1}. *${c.name}* — ${c.city}\\\\n   Docket: ${c.docket} | Status: ${c.status||'booked'}`\u001b[39m\n\u001b[31m    ),\u001b[39m\n\u001b[31m    ``,\u001b[39m\n\u001b[31m    `_360 Degree Logical Marketing_`\u001b[39m\n\u001b[31m  ];\u001b[39m\n\u001b[31m  const msg = encodeURIComponent(lines.join('\\\\n'));\u001b[39m\n\u001b[31m  window.open('https://wa.me/?text=' + msg, '_blank');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// Trigger n8n webhook for automated WhatsApp/email share\u001b[39m\n\u001b[31masync function triggerN8nShare(dispatchId, channel) {\u001b[39m\n\u001b[31m  const d = dispatches.find(x => x.id === dispatchId);\u001b[39m\n\u001b[31m  if (!d) return;\u001b[39m\n\u001b[31m  const n8nUrl = window.appConfig.n8n_dispatch_webhook;\u001b[39m\n\u001b[31m  if (!n8nUrl) { showToast('n8n webhook not configured in Config sheet', 'err'); return; }\u001b[39m\n\u001b[31m  try {\u001b[39m\n\u001b[31m    await fetch(n8nUrl, {\u001b[39m\n\u001b[31m      method: 'POST',\u001b[39m\n\u001b[31m      headers: { 'Content-Type': 'application/json' },\u001b[39m\n\u001b[31m      body: JSON.stringify({\u001b[39m\n\u001b[31m        channel, // 'whatsapp' or 'email'\u001b[39m\n\u001b[31m        dispatch: d,\u001b[39m\n\u001b[31m        timestamp: new Date().toISOString()\u001b[39m\n\u001b[31m      })\u001b[39m\n\u001b[31m    });\u001b[39m\n\u001b[31m    showToast(`Sent to n8n for ${channel} delivery ✓`, 'ok');\u001b[39m\n\u001b[31m  } catch(e) {\u001b[39m\n\u001b[31m    showToast('n8n webhook failed', 'err');\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction previewPOD(input, type) {\u001b[39m\n\u001b[31m  const f = input.files[0];\u001b[39m\n\u001b[31m  if (!f) return;\u001b[39m\n\u001b[31m  const r = new FileReader();\u001b[39m\n\u001b[31m  r.onload = e => {\u001b[39m\n\u001b[31m    const prev = document.getElementById('podPreview');\u001b[39m\n\u001b[31m    const existing = document.getElementById('pod_prev_' + type);\u001b[39m\n\u001b[31m    if (existing) existing.remove();\u001b[39m\n\u001b[31m    const img = document.createElement('img');\u001b[39m\n\u001b[31m    img.id = 'pod_prev_' + type;\u001b[39m\n\u001b[31m    img.src = e.target.result;\u001b[39m\n\u001b[31m    img.style.cssText = 'width:80px;height:80px;object-fit:cover;border-radius:6px;border:1px solid var(--bdr);';\u001b[39m\n\u001b[31m    prev.appendChild(img);\u001b[39m\n\u001b[31m  };\u001b[39m\n\u001b[31m  r.readAsDataURL(f);\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// ─── MANUAL TRACKING ─────────────────────────────────────────────────────\u001b[39m\n\u001b[31mfunction showManualTracking(docket, dispatchId, storeName, phone, city) {\u001b[39m\n\u001b[31m  const ov = document.getElementById('manualTrackOverlay');\u001b[39m\n\u001b[31m  ov.dataset.docket = docket;\u001b[39m\n\u001b[31m  ov.dataset.dispatchId = dispatchId;\u001b[39m\n\u001b[31m  ov.dataset.storeName = storeName;\u001b[39m\n\u001b[31m  ov.dataset.city = city;·\u001b[39m\n\u001b[31m  document.getElementById('mtDocketDisplay').textContent =\u001b[39m\n\u001b[31m    docket + '  ·  ' + storeName + '  ·  ' + city;\u001b[39m\n\u001b[31m  document.getElementById('mtContactUsed').value = phone || '';\u001b[39m\n\u001b[31m  document.getElementById('mtContactPerson').value = '';\u001b[39m\n\u001b[31m  document.getElementById('mtRemarks').value = '';\u001b[39m\n\u001b[31m  document.getElementById('mtStatus').value = '';\u001b[39m\n\u001b[31m  document.getElementById('mtCalledBy').value = '';·\u001b[39m\n\u001b[31m  // Set current datetime\u001b[39m\n\u001b[31m  const now = new Date();\u001b[39m\n\u001b[31m  const pad = n => String(n).padStart(2,'0');\u001b[39m\n\u001b[31m  document.getElementById('mtCallTime').value =\u001b[39m\n\u001b[31m    `${now.getFullYear()}-${pad(now.getMonth()+1)}-${pad(now.getDate())}T${pad(now.getHours())}:${pad(now.getMinutes())}`;·\u001b[39m\n\u001b[31m  openOverlay('manualTrackOverlay');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31masync function submitManualTracking() {\u001b[39m\n\u001b[31m  const ov = document.getElementById('manualTrackOverlay');\u001b[39m\n\u001b[31m  const docket     = ov.dataset.docket;\u001b[39m\n\u001b[31m  const dispatchId = ov.dataset.dispatchId;\u001b[39m\n\u001b[31m  const status     = document.getElementById('mtStatus').value;\u001b[39m\n\u001b[31m  const calledBy   = document.getElementById('mtCalledBy').value.trim();·\u001b[39m\n\u001b[31m  if (!status)    { showToast('Select a status', 'err'); return; }\u001b[39m\n\u001b[31m  if (!calledBy)  { showToast('Enter your name', 'err'); return; }·\u001b[39m\n\u001b[31m  const payload = {\u001b[39m\n\u001b[31m    type:          'manual_tracking',\u001b[39m\n\u001b[31m    docket,        dispatchId,\u001b[39m\n\u001b[31m    storeName:     ov.dataset.storeName,\u001b[39m\n\u001b[31m    city:          ov.dataset.city,\u001b[39m\n\u001b[31m    manualStatus:  status,\u001b[39m\n\u001b[31m    calledBy,\u001b[39m\n\u001b[31m    contactUsed:   document.getElementById('mtContactUsed').value.trim(),\u001b[39m\n\u001b[31m    contactPerson: document.getElementById('mtContactPerson').value.trim(),\u001b[39m\n\u001b[31m    remarks:       document.getElementById('mtRemarks').value.trim(),\u001b[39m\n\u001b[31m    callTime:      document.getElementById('mtCallTime').value || new Date().toISOString(),\u001b[39m\n\u001b[31m    timestamp:     new Date().toISOString()\u001b[39m\n\u001b[31m  };·\u001b[39m\n\u001b[31m  // Update local dispatch record\u001b[39m\n\u001b[31m  for (const d of dispatches) {\u001b[39m\n\u001b[31m    if (d.id !== dispatchId) continue;\u001b[39m\n\u001b[31m    for (const c of d.consignees) {\u001b[39m\n\u001b[31m      if (c.docket === docket) {\u001b[39m\n\u001b[31m        c.manualStatus    = status;\u001b[39m\n\u001b[31m        c.manualCalledBy  = calledBy;\u001b[39m\n\u001b[31m        c.manualTime      = payload.callTime;\u001b[39m\n\u001b[31m        c.manualRemarks   = payload.remarks;\u001b[39m\n\u001b[31m        // If confirmed delivered — flag so dashboard shows manual badge\u001b[39m\n\u001b[31m        // (not permanently locked — use resumeAutoTracking() to re-enable)\u001b[39m\n\u001b[31m        if (status === 'delivered') {\u001b[39m\n\u001b[31m          c.status = 'delivered';\u001b[39m\n\u001b[31m          c.manualDelivered = true;\u001b[39m\n\u001b[31m        } else {\u001b[39m\n\u001b[31m          // Non-delivered manual update: clear any previous lock\u001b[39m\n\u001b[31m          c.manualDelivered = false;\u001b[39m\n\u001b[31m        }\u001b[39m\n\u001b[31m      }\u001b[39m\n\u001b[31m    }\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m  saveData();\u001b[39m\n\u001b[31m  renderDashboard();\u001b[39m\n\u001b[31m  // Sync updated consignee to Postgres\u001b[39m\n\u001b[31m  const _d = dispatches.find(x => x.id === dispatchId);\u001b[39m\n\u001b[31m  if (_d) {\u001b[39m\n\u001b[31m    const _idx = _d.consignees.findIndex(c => c.docket === docket);\u001b[39m\n\u001b[31m    if (_idx >= 0) syncConsigneeToDB(_d.consignees[_idx], dispatchId, _idx);\u001b[39m\n\u001b[31m  }·\u001b[39m\n\u001b[31m  // Log to Sheets\u001b[39m\n\u001b[31m  if (window.appConfig.webhook_url) {\u001b[39m\n\u001b[31m    try {\u001b[39m\n\u001b[31m      await fetch(window.appConfig.webhook_url, {\u001b[39m\n\u001b[31m        method: 'POST', mode: 'no-cors',\u001b[39m\n\u001b[31m        headers: { 'Content-Type': 'application/json' },\u001b[39m\n\u001b[31m        body: JSON.stringify(payload)\u001b[39m\n\u001b[31m      });\u001b[39m\n\u001b[31m    } catch(e) {}\u001b[39m\n\u001b[31m  }·\u001b[39m\n\u001b[31m  closeOverlay('manualTrackOverlay');\u001b[39m\n\u001b[31m  showToast('Manual tracking logged ✓', 'ok');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// ─── TILE SELECTION + PARTIAL REPORT SHARE ───────────────────────────────\u001b[39m\n\u001b[31mlet _selectedDockets = new Set();·\u001b[39m\n\u001b[31mfunction toggleTileSelect(docket) {\u001b[39m\n\u001b[31m  if (_selectedDockets.has(docket)) _selectedDockets.delete(docket);\u001b[39m\n\u001b[31m  else _selectedDockets.add(docket);\u001b[39m\n\u001b[31m  // Update checkbox visual\u001b[39m\n\u001b[31m  const chk = document.getElementById('sel_' + docket.replace(/[^a-z0-9]/gi,'_'));\u001b[39m\n\u001b[31m  if (chk) chk.checked = _selectedDockets.has(docket);\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction shareSelectedReport() {\u001b[39m\n\u001b[31m  if (_selectedDockets.size === 0) {\u001b[39m\n\u001b[31m    showToast('Select at least one consignee tile first', 'err');\u001b[39m\n\u001b[31m    return;\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m  const activeDis = dispatches.filter(d => d.status !== 'draft');\u001b[39m\n\u001b[31m  const allItems  = activeDis.flatMap(d =>\u001b[39m\n\u001b[31m    (d.consignees||[]).map(c => Object.assign({}, c, {courier:d.courier, client:d.client, jobref:d.jobref, dispatchId:d.id}))\u001b[39m\n\u001b[31m  );\u001b[39m\n\u001b[31m  const selected = allItems.filter(c => _selectedDockets.has(c.docket));\u001b[39m\n\u001b[31m  if (!selected.length) { showToast('Selected dockets not found', 'err'); return; }·\u001b[39m\n\u001b[31m  const NL = '\\\\n';\u001b[39m\n\u001b[31m  const statusEmoji = {\u001b[39m\n\u001b[31m    delivered:'\\\\u2705', transit:'\\\\uD83D\\\\uDE9A', 'in-transit':'\\\\uD83D\\\\uDE9A',\u001b[39m\n\u001b[31m    booked:'\\\\uD83D\\\\uDCEC', pending:'\\\\u23F3', 'not received':'\\\\u274C', forwarded:'\\\\u27A1'\u001b[39m\n\u001b[31m  };·\u001b[39m\n\u001b[31m  const header = '\\\\uD83D\\\\uDCE6 *Dispatch Status Report*' + NL +\u001b[39m\n\u001b[31m    (selected[0].client || '') + ' ' + (selected[0].jobref ? '> ' + selected[0].jobref : '') + NL +\u001b[39m\n\u001b[31m    new Date().toLocaleDateString('en-IN', {day:'2-digit',month:'short',year:'numeric'}) + NL + NL;·\u001b[39m\n\u001b[31m  const rows = selected.map(function(c, i) {\u001b[39m\n\u001b[31m    const finalStatus = c.manualStatus === 'delivered' ? 'delivered' : (c.status || 'booked');\u001b[39m\n\u001b[31m    const emoji  = statusEmoji[finalStatus] || '\\\\uD83D\\\\uDCE6';\u001b[39m\n\u001b[31m    const source = c.manualStatus ? '(manual)' : '(auto)';\u001b[39m\n\u001b[31m    const remark = c.manualRemarks ? (NL + '   Note: ' + c.manualRemarks) : '';\u001b[39m\n\u001b[31m    return (i+1) + '. *' + c.name + '* - ' + c.city + NL +\u001b[39m\n\u001b[31m      '   ' + emoji + ' ' + finalStatus + ' ' + source + NL +\u001b[39m\n\u001b[31m      '   Docket: ' + c.docket + remark;\u001b[39m\n\u001b[31m  }).join(NL + NL);·\u001b[39m\n\u001b[31m  const footer = NL + NL + '_360 Degree Logical Marketing_';\u001b[39m\n\u001b[31m  window.open('https://wa.me/?text=' + encodeURIComponent(header + rows + footer), '_blank');\u001b[39m\n\u001b[31m  showToast('Report for ' + selected.length + ' consignees ready', 'ok');\u001b[39m\n\u001b[31m}··\u001b[39m\n\u001b[31m// ── resumeAutoTracking: undo a manual-delivered lock ──────────────────────\u001b[39m\n\u001b[31mfunction resumeAutoTracking(docket, dispatchId) {\u001b[39m\n\u001b[31m  for (const d of dispatches) {\u001b[39m\n\u001b[31m    if (d.id !== dispatchId) continue;\u001b[39m\n\u001b[31m    for (const c of d.consignees) {\u001b[39m\n\u001b[31m      if (c.docket === docket) {\u001b[39m\n\u001b[31m        c.manualDelivered = false;\u001b[39m\n\u001b[31m        c.status          = 'transit';\u001b[39m\n\u001b[31m        c.manualStatus    = '';\u001b[39m\n\u001b[31m      }\u001b[39m\n\u001b[31m    }\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m  saveData();\u001b[39m\n\u001b[31m  renderDashboard();\u001b[39m\n\u001b[31m  showToast(`Auto-tracking resumed for ${docket}`, 'ok');\u001b[39m\n\u001b[31m  const _rd = dispatches.find(x => x.id === dispatchId);\u001b[39m\n\u001b[31m  if (_rd) {\u001b[39m\n\u001b[31m    const _ri = _rd.consignees.findIndex(c => c.docket === docket);\u001b[39m\n\u001b[31m    if (_ri >= 0) syncConsigneeToDB(_rd.consignees[_ri], dispatchId, _ri);\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// ── refreshAllTracking (consolidated, try/finally for safety) ─────────────\u001b[39m\n\u001b[31masync function refreshAllTracking() {\u001b[39m\n\u001b[31m  const activeDis  = dispatches.filter(d => d.status !== 'draft');\u001b[39m\n\u001b[31m  const allCons    = activeDis.flatMap(d =>\u001b[39m\n\u001b[31m    (d.consignees || []).map(c => ({ ...c, dispatchId: d.id, courier: d.courier }))\u001b[39m\n\u001b[31m  );\u001b[39m\n\u001b[31m  const withDockets = allCons.filter(c =>\u001b[39m\n\u001b[31m    c.docket && c.status !== 'delivered' && !c.manualDelivered\u001b[39m\n\u001b[31m  );\u001b[39m\n\u001b[31m  if (!withDockets.length) { showToast('No active dockets to refresh', 'info'); return; }·\u001b[39m\n\u001b[31m  showToast(`⏳ Tracking ${withDockets.length} dockets...`, 'info');\u001b[39m\n\u001b[31m  withDockets.forEach(c => {\u001b[39m\n\u001b[31m    const id    = c.docket.replace(/[^a-z0-9]/gi, '_');\u001b[39m\n\u001b[31m    const evtEl = document.getElementById('evt_'  + id);\u001b[39m\n\u001b[31m    const spnEl = document.getElementById('spin_' + id);\u001b[39m\n\u001b[31m    if (evtEl) evtEl.style.display = 'none';\u001b[39m\n\u001b[31m    if (spnEl) spnEl.style.display = 'inline';\u001b[39m\n\u001b[31m  });·\u001b[39m\n\u001b[31m  let done = 0;\u001b[39m\n\u001b[31m  try {\u001b[39m\n\u001b[31m    for (const c of withDockets) {\u001b[39m\n\u001b[31m      const result = await fetchTracking(c.docket, c.courier || '');\u001b[39m\n\u001b[31m      const id    = c.docket.replace(/[^a-z0-9]/gi, '_');·\u001b[39m\n\u001b[31m      // Batch-write result into state (never touch DOM here)\u001b[39m\n\u001b[31m      if (result.status && result.status !== 'failed') {\u001b[39m\n\u001b[31m        for (const d of dispatches) {\u001b[39m\n\u001b[31m          if (d.id !== c.dispatchId) continue;\u001b[39m\n\u001b[31m          for (const con of d.consignees) {\u001b[39m\n\u001b[31m            if (con.docket === c.docket && !con.manualDelivered) {\u001b[39m\n\u001b[31m              con.status           = result.status;\u001b[39m\n\u001b[31m              con.lastEvent        = result.lastEvent || con.lastEvent;\u001b[39m\n\u001b[31m              con.lastScanLocation = result.lastScanLocation || '';\u001b[39m\n\u001b[31m              con.lastTrackedAt    = new Date().toISOString();\u001b[39m\n\u001b[31m            }\u001b[39m\n\u001b[31m          }\u001b[39m\n\u001b[31m        }\u001b[39m\n\u001b[31m      }·\u001b[39m\n\u001b[31m      // Update tile spinner/event text in DOM (cosmetic only)\u001b[39m\n\u001b[31m      const spnEl = document.getElementById('spin_' + id);\u001b[39m\n\u001b[31m      const evtEl = document.getElementById('evt_'  + id);\u001b[39m\n\u001b[31m      if (spnEl) spnEl.style.display = 'none';\u001b[39m\n\u001b[31m      if (evtEl) { evtEl.style.display = 'inline'; evtEl.textContent = result.lastEvent || 'Checked'; }·\u001b[39m\n\u001b[31m      if (result.status && result.status !== 'failed' && window.appConfig.webhook_url) {\u001b[39m\n\u001b[31m        logTrackingUpdate(c.docket, result.status, result.lastEvent || '', result.lastScanLocation || '');\u001b[39m\n\u001b[31m      }\u001b[39m\n\u001b[31m      done++;\u001b[39m\n\u001b[31m    }\u001b[39m\n\u001b[31m    showToast(`✓ ${done} dockets refreshed`, 'ok');\u001b[39m\n\u001b[31m  } finally {\u001b[39m\n\u001b[31m    // Always persist and re-render — even if loop throws mid-way\u001b[39m\n\u001b[31m    saveData();\u001b[39m\n\u001b[31m    renderDashboard();\u001b[39m\n\u001b[31m    // Push updated consignee states to Postgres (fire-and-forget)\u001b[39m\n\u001b[31m    dispatches.filter(d => d.status !== 'draft').forEach(d => syncDispatchToDB(d));\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m</script>\u001b[39m\n\u001b[31m</body>\u001b[39m\n\u001b[31m</html>\"\u001b[39m",
                        "stack": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoContain\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m) // indexOf\u001b[22m\n\nExpected substring: \u001b[32m\"window.location.replace('/hub/')\"\u001b[39m\nReceived string:    \u001b[31m\"<!DOCTYPE html>\u001b[39m\n\u001b[31m<html lang=\\\"en\\\">\u001b[39m\n\u001b[31m<head>\u001b[39m\n\u001b[31m<meta charset=\\\"UTF-8\\\">\u001b[39m\n\u001b[31m<meta name=\\\"viewport\\\" content=\\\"width=device-width,initial-scale=1,maximum-scale=1,user-scalable=no\\\">\u001b[39m\n\u001b[31m<meta name=\\\"theme-color\\\" content=\\\"#080c12\\\">\u001b[39m\n\u001b[31m<meta name=\\\"apple-mobile-web-app-capable\\\" content=\\\"yes\\\">\u001b[39m\n\u001b[31m<meta name=\\\"mobile-web-app-capable\\\" content=\\\"yes\\\">\u001b[39m\n\u001b[31m<title>360LM Dispatch</title>\u001b[39m\n\u001b[31m<link rel=\\\"manifest\\\" href=\\\"manifest.json\\\">\u001b[39m\n\u001b[31m<link href=\\\"https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@300;400;500;600;700&family=Fira+Code:wght@400;500&family=Barlow+Condensed:wght@400;600;700;800&display=swap\\\" rel=\\\"stylesheet\\\">\u001b[39m\n\u001b[31m<style>\u001b[39m\n\u001b[31m/* ════ RESET & VARS ════ */\u001b[39m\n\u001b[31m*{margin:0;padding:0;box-sizing:border-box;-webkit-tap-highlight-color:transparent;}\u001b[39m\n\u001b[31m:root{\u001b[39m\n\u001b[31m  --bg:#080c12; --bg2:#0d1420; --bg3:#111a28;\u001b[39m\n\u001b[31m  --sur:#141f30; --sur2:#1a2840; --sur3:#1f3050;\u001b[39m\n\u001b[31m  --bdr:#1e2e45; --bdr2:#253650;\u001b[39m\n\u001b[31m  --t1:#f0f4f8; --t2:#8ba4bc; --t3:#3d5470; --t4:#243348;\u001b[39m\n\u001b[31m  --acc:#f97316; --acc2:#fb923c; --acc-glow:rgba(249,115,22,0.25);\u001b[39m\n\u001b[31m  --grn:#22c55e; --grn-dim:rgba(34,197,94,0.15);\u001b[39m\n\u001b[31m  --sky:#38bdf8; --sky-dim:rgba(56,189,248,0.15);\u001b[39m\n\u001b[31m  --red:#ef4444; --red-dim:rgba(239,68,68,0.15);\u001b[39m\n\u001b[31m  --ylw:#eab308; --ylw-dim:rgba(234,179,8,0.15);\u001b[39m\n\u001b[31m  --fnt-head:'Barlow Condensed',sans-serif;\u001b[39m\n\u001b[31m  --fnt-body:'Space Grotesk',sans-serif;\u001b[39m\n\u001b[31m  --fnt-mono:'Fira Code',monospace;\u001b[39m\n\u001b[31m  --r:10px; --r-sm:6px; --r-lg:16px;\u001b[39m\n\u001b[31m  --sh:0 4px 24px rgba(0,0,0,0.4);\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31mhtml,body{height:100%;background:var(--bg);color:var(--t1);font-family:var(--fnt-body);overflow:hidden;}·\u001b[39m\n\u001b[31m/* ════ SPLASH ════ */\u001b[39m\n\u001b[31m#splash{\u001b[39m\n\u001b[31m  position:fixed;inset:0;background:var(--bg);z-index:999;\u001b[39m\n\u001b[31m  display:flex;flex-direction:column;align-items:center;justify-content:center;\u001b[39m\n\u001b[31m  transition:opacity .4s .1s;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.splash-logo{font-family:var(--fnt-head);font-size:3rem;font-weight:800;letter-spacing:2px;line-height:1;}\u001b[39m\n\u001b[31m.splash-logo em{color:var(--acc);font-style:normal;}\u001b[39m\n\u001b[31m.splash-sub{color:var(--t3);font-family:var(--fnt-mono);font-size:.7rem;letter-spacing:4px;margin-top:8px;}\u001b[39m\n\u001b[31m.splash-track{width:200px;height:2px;background:var(--bdr);border-radius:2px;margin-top:40px;overflow:hidden;}\u001b[39m\n\u001b[31m.splash-track-fill{height:100%;width:0;background:linear-gradient(90deg,var(--acc),var(--acc2));animation:splashLoad 1.3s cubic-bezier(.4,0,.2,1) forwards;}\u001b[39m\n\u001b[31m@keyframes splashLoad{to{width:100%;}}·\u001b[39m\n\u001b[31m/* ════ LAYOUT ════ */\u001b[39m\n\u001b[31m#app{height:100%;display:flex;flex-direction:column;}\u001b[39m\n\u001b[31m.hdr{\u001b[39m\n\u001b[31m  background:var(--bg2);border-bottom:1px solid var(--bdr);\u001b[39m\n\u001b[31m  padding:10px 14px;display:flex;align-items:center;gap:10px;flex-shrink:0;\u001b[39m\n\u001b[31m  position:relative;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.hdr-brand{font-family:var(--fnt-head);font-size:1.3rem;font-weight:800;letter-spacing:1px;flex:1;}\u001b[39m\n\u001b[31m.hdr-brand em{color:var(--acc);font-style:normal;}\u001b[39m\n\u001b[31m.hdr-dot{width:7px;height:7px;border-radius:50%;background:var(--grn);transition:background .3s;animation:blink 2s infinite;}\u001b[39m\n\u001b[31m@keyframes blink{0%,100%{opacity:1;}50%{opacity:.4;}}\u001b[39m\n\u001b[31m.hdr-btn{\u001b[39m\n\u001b[31m  background:none;border:none;color:var(--t2);cursor:pointer;\u001b[39m\n\u001b[31m  padding:6px 8px;border-radius:var(--r-sm);transition:all .2s;\u001b[39m\n\u001b[31m  font-family:var(--fnt-head);font-size:.85rem;font-weight:700;letter-spacing:.5px;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.hdr-btn:hover{background:var(--bdr);color:var(--t1);}\u001b[39m\n\u001b[31m.hdr-btn.primary{background:var(--acc);color:#fff;padding:6px 14px;}\u001b[39m\n\u001b[31m.hdr-btn.primary:hover{background:var(--acc2);}·\u001b[39m\n\u001b[31m/* ════ TAB BAR ════ */\u001b[39m\n\u001b[31m.tabs{\u001b[39m\n\u001b[31m  display:flex;background:var(--bg2);border-bottom:1px solid var(--bdr);\u001b[39m\n\u001b[31m  overflow-x:auto;flex-shrink:0;padding:0 6px;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.tabs::-webkit-scrollbar{display:none;}\u001b[39m\n\u001b[31m.tab{\u001b[39m\n\u001b[31m  background:none;border:none;color:var(--t3);font-family:var(--fnt-head);\u001b[39m\n\u001b[31m  font-size:.85rem;font-weight:700;letter-spacing:1px;padding:10px 14px;\u001b[39m\n\u001b[31m  cursor:pointer;border-bottom:2px solid transparent;white-space:nowrap;\u001b[39m\n\u001b[31m  transition:all .2s;flex-shrink:0;display:flex;align-items:center;gap:6px;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.tab.active{color:var(--acc);border-bottom-color:var(--acc);}\u001b[39m\n\u001b[31m.tab:hover:not(.active){color:var(--t2);}\u001b[39m\n\u001b[31m.tab-badge{\u001b[39m\n\u001b[31m  background:var(--acc);color:#fff;font-size:.6rem;font-weight:700;\u001b[39m\n\u001b[31m  padding:1px 5px;border-radius:10px;font-family:var(--fnt-mono);\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m/* ════ CONTENT AREAS ════ */\u001b[39m\n\u001b[31m.pane{flex:1;overflow-y:auto;padding:14px;display:none;}\u001b[39m\n\u001b[31m.pane.active{display:block;}\u001b[39m\n\u001b[31m.pane::-webkit-scrollbar{width:3px;}\u001b[39m\n\u001b[31m.pane::-webkit-scrollbar-thumb{background:var(--bdr2);}·\u001b[39m\n\u001b[31m/* ════ CARDS ════ */\u001b[39m\n\u001b[31m.card{\u001b[39m\n\u001b[31m  background:var(--sur);border:1px solid var(--bdr);border-radius:var(--r);\u001b[39m\n\u001b[31m  padding:14px;margin-bottom:12px;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.card.accent-left{border-left:3px solid var(--acc);}\u001b[39m\n\u001b[31m.card-title{\u001b[39m\n\u001b[31m  font-family:var(--fnt-head);font-size:.72rem;font-weight:700;\u001b[39m\n\u001b[31m  color:var(--t3);text-transform:uppercase;letter-spacing:2.5px;\u001b[39m\n\u001b[31m  margin-bottom:12px;display:flex;align-items:center;gap:8px;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.card-title .ct-dot{width:5px;height:5px;border-radius:50%;background:var(--acc);flex-shrink:0;}·\u001b[39m\n\u001b[31m/* ════ FORM ════ */\u001b[39m\n\u001b[31m.f{margin-bottom:12px;}\u001b[39m\n\u001b[31m.f label{\u001b[39m\n\u001b[31m  display:block;font-size:.68rem;color:var(--t3);margin-bottom:5px;\u001b[39m\n\u001b[31m  font-weight:600;text-transform:uppercase;letter-spacing:1.5px;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.f label .req{color:var(--acc);margin-left:2px;}\u001b[39m\n\u001b[31m.f input,.f select,.f textarea{\u001b[39m\n\u001b[31m  width:100%;background:var(--sur2);border:1px solid var(--bdr);\u001b[39m\n\u001b[31m  border-radius:var(--r-sm);color:var(--t1);font-family:var(--fnt-body);\u001b[39m\n\u001b[31m  font-size:.88rem;padding:9px 11px;outline:none;transition:border-color .2s;\u001b[39m\n\u001b[31m  -webkit-appearance:none;appearance:none;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.f input:focus,.f select:focus,.f textarea:focus{border-color:var(--acc);}\u001b[39m\n\u001b[31m.f input::placeholder{color:var(--t4);}\u001b[39m\n\u001b[31m.f textarea{resize:vertical;min-height:65px;}\u001b[39m\n\u001b[31m.f select{\u001b[39m\n\u001b[31m  background-image:url(\\\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='7'%3E%3Cpath d='M1 1l4 4 4-4' stroke='%233d5470' stroke-width='1.5' fill='none' stroke-linecap='round'/%3E%3C/svg%3E\\\");\u001b[39m\n\u001b[31m  background-repeat:no-repeat;background-position:right 10px center;padding-right:28px;cursor:pointer;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.row2{display:grid;grid-template-columns:1fr 1fr;gap:10px;}\u001b[39m\n\u001b[31m.row3{display:grid;grid-template-columns:1fr 1fr 1fr;gap:8px;}·\u001b[39m\n\u001b[31m/* ════ BUTTONS ════ */\u001b[39m\n\u001b[31m.btn{\u001b[39m\n\u001b[31m  width:100%;padding:12px;border:none;border-radius:var(--r);\u001b[39m\n\u001b[31m  font-family:var(--fnt-head);font-size:.9rem;font-weight:700;letter-spacing:.5px;\u001b[39m\n\u001b[31m  cursor:pointer;transition:all .2s;display:flex;align-items:center;justify-content:center;gap:8px;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.btn-orange{background:var(--acc);color:#fff;}\u001b[39m\n\u001b[31m.btn-orange:hover{background:var(--acc2);}\u001b[39m\n\u001b[31m.btn-ghost{background:var(--sur2);border:1px solid var(--bdr);color:var(--t1);}\u001b[39m\n\u001b[31m.btn-ghost:hover{border-color:var(--acc);color:var(--acc);}\u001b[39m\n\u001b[31m.btn-grn{background:var(--grn);color:#000;}\u001b[39m\n\u001b[31m.btn-grn:hover{opacity:.85;}\u001b[39m\n\u001b[31m.btn:active{transform:scale(.98);}\u001b[39m\n\u001b[31m.btn-row{display:grid;grid-template-columns:1fr 1fr;gap:10px;margin-top:6px;}\u001b[39m\n\u001b[31m.btn-sm{\u001b[39m\n\u001b[31m  background:none;border:1px solid var(--bdr);color:var(--t2);\u001b[39m\n\u001b[31m  font-size:.7rem;padding:5px 10px;border-radius:var(--r-sm);cursor:pointer;\u001b[39m\n\u001b[31m  font-family:var(--fnt-body);transition:all .2s;white-space:nowrap;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.btn-sm:hover{border-color:var(--acc);color:var(--acc);}\u001b[39m\n\u001b[31m.btn-sm.primary{background:var(--acc);border-color:var(--acc);color:#fff;}·\u001b[39m\n\u001b[31m/* ════ OCR UPLOAD ZONE ════ */\u001b[39m\n\u001b[31m.upload-zone{\u001b[39m\n\u001b[31m  border:2px dashed var(--bdr2);border-radius:var(--r);\u001b[39m\n\u001b[31m  padding:20px;text-align:center;cursor:pointer;transition:all .25s;\u001b[39m\n\u001b[31m  background:var(--sur2);position:relative;overflow:hidden;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.upload-zone:hover,.upload-zone.drag{border-color:var(--acc);background:rgba(249,115,22,.05);}\u001b[39m\n\u001b[31m.upload-zone .uz-icon{font-size:2rem;margin-bottom:8px;}\u001b[39m\n\u001b[31m.upload-zone .uz-title{font-family:var(--fnt-head);font-size:1rem;font-weight:700;color:var(--t1);}\u001b[39m\n\u001b[31m.upload-zone .uz-sub{font-size:.72rem;color:var(--t3);margin-top:4px;font-family:var(--fnt-mono);}\u001b[39m\n\u001b[31m.upload-zone input{display:none;}\u001b[39m\n\u001b[31m.upload-zone.share-target{border-color:var(--grn);}·\u001b[39m\n\u001b[31m/* ════ OCR RESULTS ════ */\u001b[39m\n\u001b[31m.ocr-results{display:none;}\u001b[39m\n\u001b[31m.ocr-results.visible{display:block;}\u001b[39m\n\u001b[31m.ocr-slip-list{display:flex;flex-direction:column;gap:8px;margin-top:10px;}\u001b[39m\n\u001b[31m.ocr-slip{\u001b[39m\n\u001b[31m  background:var(--sur2);border:1px solid var(--bdr);border-radius:var(--r-sm);\u001b[39m\n\u001b[31m  padding:10px 12px;display:flex;gap:10px;align-items:flex-start;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.ocr-slip img{width:60px;height:60px;object-fit:cover;border-radius:6px;flex-shrink:0;border:1px solid var(--bdr2);}\u001b[39m\n\u001b[31m.ocr-slip-body{flex:1;min-width:0;}\u001b[39m\n\u001b[31m.ocr-slip-name{font-size:.7rem;color:var(--t3);font-family:var(--fnt-mono);margin-bottom:4px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}\u001b[39m\n\u001b[31m.ocr-slip-docket{\u001b[39m\n\u001b[31m  font-family:var(--fnt-mono);font-size:.9rem;font-weight:500;color:var(--acc);\u001b[39m\n\u001b[31m  background:var(--bg);border:1px solid var(--bdr);border-radius:4px;\u001b[39m\n\u001b[31m  padding:4px 8px;width:100%;outline:none;transition:border-color .2s;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.ocr-slip-docket:focus{border-color:var(--acc);}\u001b[39m\n\u001b[31m.ocr-slip-status{font-size:.65rem;margin-top:4px;display:flex;align-items:center;gap:6px;}\u001b[39m\n\u001b[31m.ocr-confidence{padding:2px 6px;border-radius:10px;font-family:var(--fnt-mono);}\u001b[39m\n\u001b[31m.conf-high{background:var(--grn-dim);color:var(--grn);}\u001b[39m\n\u001b[31m.conf-med{background:var(--ylw-dim);color:var(--ylw);}\u001b[39m\n\u001b[31m.conf-low{background:var(--red-dim);color:var(--red);}\u001b[39m\n\u001b[31m.ocr-ai-btn{\u001b[39m\n\u001b[31m  background:rgba(56,189,248,.15);border:1px solid rgba(56,189,248,.3);\u001b[39m\n\u001b[31m  color:var(--sky);font-size:.65rem;padding:3px 8px;border-radius:10px;\u001b[39m\n\u001b[31m  cursor:pointer;font-family:var(--fnt-mono);transition:all .2s;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.ocr-ai-btn:hover{background:rgba(56,189,248,.3);}\u001b[39m\n\u001b[31m.ocr-working{\u001b[39m\n\u001b[31m  background:var(--ylw-dim);border:1px solid rgba(234,179,8,.3);\u001b[39m\n\u001b[31m  border-radius:var(--r-sm);padding:8px 12px;margin-top:8px;\u001b[39m\n\u001b[31m  font-size:.75rem;color:var(--ylw);font-family:var(--fnt-mono);\u001b[39m\n\u001b[31m  display:none;align-items:center;gap:8px;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.ocr-working.visible{display:flex;}\u001b[39m\n\u001b[31m.spin{animation:spin .8s linear infinite;display:inline-block;}\u001b[39m\n\u001b[31m@keyframes spin{to{transform:rotate(360deg);}}·\u001b[39m\n\u001b[31m/* ════ STORE LIST ════ */\u001b[39m\n\u001b[31m.store-upload-zone{\u001b[39m\n\u001b[31m  border:2px dashed var(--bdr2);border-radius:var(--r);padding:16px;\u001b[39m\n\u001b[31m  text-align:center;cursor:pointer;transition:all .25s;background:var(--sur2);\u001b[39m\n\u001b[31m  position:relative;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.store-upload-zone:hover{border-color:var(--sky);background:rgba(56,189,248,.05);}\u001b[39m\n\u001b[31m.store-upload-zone input{display:none;}\u001b[39m\n\u001b[31m.store-table-wrap{max-height:200px;overflow-y:auto;margin-top:10px;border-radius:var(--r-sm);overflow:hidden;}\u001b[39m\n\u001b[31m.store-table{width:100%;border-collapse:collapse;font-size:.75rem;}\u001b[39m\n\u001b[31m.store-table th{\u001b[39m\n\u001b[31m  background:var(--bg);color:var(--t3);font-family:var(--fnt-head);\u001b[39m\n\u001b[31m  font-size:.65rem;letter-spacing:1.5px;text-transform:uppercase;\u001b[39m\n\u001b[31m  padding:7px 8px;text-align:left;border-bottom:1px solid var(--bdr);\u001b[39m\n\u001b[31m  position:sticky;top:0;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.store-table td{\u001b[39m\n\u001b[31m  padding:6px 8px;border-bottom:1px solid var(--bdr);color:var(--t2);\u001b[39m\n\u001b[31m  font-family:var(--fnt-mono);font-size:.72rem;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.store-table tr:hover td{background:var(--sur3);}\u001b[39m\n\u001b[31m.store-table .matched{color:var(--grn);}\u001b[39m\n\u001b[31m.store-table .unmatched{color:var(--ylw);}\u001b[39m\n\u001b[31m.store-map-btn{\u001b[39m\n\u001b[31m  font-size:.65rem;background:none;border:1px solid var(--bdr);color:var(--t3);\u001b[39m\n\u001b[31m  padding:2px 6px;border-radius:4px;cursor:pointer;transition:all .2s;\u001b[39m\n\u001b[31m  font-family:var(--fnt-mono);\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.store-map-btn:hover{border-color:var(--acc);color:var(--acc);}·\u001b[39m\n\u001b[31m/* ════ CONSIGNEE CARDS ════ */\u001b[39m\n\u001b[31m.cons-list{display:flex;flex-direction:column;gap:8px;margin-bottom:10px;}\u001b[39m\n\u001b[31m.cons-card{\u001b[39m\n\u001b[31m  background:var(--sur2);border:1px solid var(--bdr);border-radius:var(--r-sm);\u001b[39m\n\u001b[31m  padding:11px;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.cons-hdr{display:flex;justify-content:space-between;align-items:center;margin-bottom:8px;}\u001b[39m\n\u001b[31m.cons-num{font-family:var(--fnt-mono);font-size:.65rem;color:var(--t3);}\u001b[39m\n\u001b[31m.cons-del{\u001b[39m\n\u001b[31m  background:none;border:none;color:var(--t4);cursor:pointer;\u001b[39m\n\u001b[31m  font-size:.85rem;padding:2px 5px;border-radius:4px;transition:all .2s;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.cons-del:hover{background:var(--red-dim);color:var(--red);}\u001b[39m\n\u001b[31m.cons-docket-linked{\u001b[39m\n\u001b[31m  display:flex;align-items:center;gap:6px;\u001b[39m\n\u001b[31m  background:rgba(249,115,22,.1);border:1px solid rgba(249,115,22,.3);\u001b[39m\n\u001b[31m  border-radius:4px;padding:4px 8px;margin-top:6px;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.cons-docket-linked .dkt{font-family:var(--fnt-mono);font-size:.82rem;color:var(--acc);}\u001b[39m\n\u001b[31m.cons-docket-linked .unlink{background:none;border:none;color:var(--acc);cursor:pointer;font-size:.75rem;}·\u001b[39m\n\u001b[31m/* ════ DISPATCH DASHBOARD ════ */\u001b[39m\n\u001b[31m.dash-stats{\u001b[39m\n\u001b[31m  display:grid;grid-template-columns:repeat(4,1fr);gap:8px;margin-bottom:14px;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.stat-box{\u001b[39m\n\u001b[31m  background:var(--sur);border:1px solid var(--bdr);border-radius:var(--r);\u001b[39m\n\u001b[31m  padding:10px 8px;text-align:center;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.stat-val{font-family:var(--fnt-head);font-size:1.4rem;font-weight:800;}\u001b[39m\n\u001b[31m.stat-lbl{font-size:.6rem;color:var(--t3);font-family:var(--fnt-mono);text-transform:uppercase;letter-spacing:1px;margin-top:2px;}\u001b[39m\n\u001b[31m.stat-box.grn .stat-val{color:var(--grn);}\u001b[39m\n\u001b[31m.stat-box.ylw .stat-val{color:var(--ylw);}\u001b[39m\n\u001b[31m.stat-box.red .stat-val{color:var(--red);}\u001b[39m\n\u001b[31m.stat-box.sky .stat-val{color:var(--sky);}·\u001b[39m\n\u001b[31m/* ════ DOCKET TILES ════ */\u001b[39m\n\u001b[31m.docket-grid{display:flex;flex-direction:column;gap:8px;}\u001b[39m\n\u001b[31m.docket-tile{\u001b[39m\n\u001b[31m  background:var(--sur);border:1px solid var(--bdr);border-radius:var(--r);\u001b[39m\n\u001b[31m  padding:12px;transition:border-color .2s;cursor:pointer;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.docket-tile:hover{border-color:var(--acc);}\u001b[39m\n\u001b[31m.docket-tile-hdr{display:flex;justify-content:space-between;align-items:flex-start;margin-bottom:8px;}\u001b[39m\n\u001b[31m.docket-tile-left{flex:1;}\u001b[39m\n\u001b[31m.docket-num{font-family:var(--fnt-mono);font-size:.95rem;font-weight:500;color:var(--sky);letter-spacing:1px;}\u001b[39m\n\u001b[31m.docket-store{font-size:.82rem;color:var(--t1);font-weight:600;margin-top:2px;}\u001b[39m\n\u001b[31m.docket-courier{font-size:.68rem;color:var(--t3);font-family:var(--fnt-mono);margin-top:2px;}\u001b[39m\n\u001b[31m.status-pill{\u001b[39m\n\u001b[31m  font-size:.62rem;font-weight:700;font-family:var(--fnt-mono);\u001b[39m\n\u001b[31m  padding:3px 8px;border-radius:10px;text-transform:uppercase;letter-spacing:.5px;\u001b[39m\n\u001b[31m  flex-shrink:0;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.sp-delivered{background:var(--grn-dim);color:var(--grn);}\u001b[39m\n\u001b[31m.sp-transit{background:var(--ylw-dim);color:var(--ylw);}\u001b[39m\n\u001b[31m.sp-booked{background:var(--sky-dim);color:var(--sky);}\u001b[39m\n\u001b[31m.sp-pending{background:var(--bdr);color:var(--t3);}\u001b[39m\n\u001b[31m.sp-failed{background:var(--red-dim);color:var(--red);}\u001b[39m\n\u001b[31m.docket-tile-footer{display:flex;align-items:center;gap:8px;flex-wrap:wrap;}\u001b[39m\n\u001b[31m.docket-last-event{font-size:.7rem;color:var(--t3);flex:1;font-family:var(--fnt-mono);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}\u001b[39m\n\u001b[31m.btn-track-live{\u001b[39m\n\u001b[31m  background:none;border:1px solid var(--acc);color:var(--acc);\u001b[39m\n\u001b[31m  font-size:.65rem;padding:4px 10px;border-radius:20px;cursor:pointer;\u001b[39m\n\u001b[31m  font-family:var(--fnt-mono);transition:all .2s;white-space:nowrap;display:flex;align-items:center;gap:4px;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.btn-track-live:hover{background:var(--acc);color:#fff;}\u001b[39m\n\u001b[31m.btn-track-live .live-dot{width:5px;height:5px;border-radius:50%;background:var(--grn);animation:blink 1.5s infinite;flex-shrink:0;}\u001b[39m\n\u001b[31m.docket-progress{height:2px;background:var(--bdr);border-radius:2px;margin-top:8px;overflow:hidden;}\u001b[39m\n\u001b[31m.docket-progress-fill{height:100%;border-radius:2px;transition:width .5s;}·\u001b[39m\n\u001b[31m/* ════ TRACKING DETAIL ════ */\u001b[39m\n\u001b[31m.track-timeline{padding:4px 0;}\u001b[39m\n\u001b[31m.track-evt{display:flex;gap:10px;padding:6px 0;position:relative;padding-left:20px;}\u001b[39m\n\u001b[31m.track-evt::before{\u001b[39m\n\u001b[31m  content:'';position:absolute;left:5px;top:11px;\u001b[39m\n\u001b[31m  width:7px;height:7px;border-radius:50%;background:var(--bdr2);border:1px solid var(--bdr2);\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.track-evt:first-child::before{background:var(--grn);border-color:var(--grn);}\u001b[39m\n\u001b[31m.track-evt::after{\u001b[39m\n\u001b[31m  content:'';position:absolute;left:8px;top:18px;\u001b[39m\n\u001b[31m  width:1px;height:calc(100% - 6px);background:var(--bdr);\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.track-evt:last-child::after{display:none;}\u001b[39m\n\u001b[31m.track-time{font-family:var(--fnt-mono);font-size:.62rem;color:var(--t3);min-width:90px;flex-shrink:0;padding-top:2px;}\u001b[39m\n\u001b[31m.track-desc{font-size:.78rem;color:var(--t2);}\u001b[39m\n\u001b[31m.track-loc{font-size:.67rem;color:var(--t3);font-family:var(--fnt-mono);margin-top:2px;}·\u001b[39m\n\u001b[31m/* ════ SHARE TOAST ════ */\u001b[39m\n\u001b[31m.share-banner{\u001b[39m\n\u001b[31m  position:fixed;top:0;left:0;right:0;z-index:200;\u001b[39m\n\u001b[31m  background:rgba(34,197,94,.15);border-bottom:1px solid rgba(34,197,94,.4);\u001b[39m\n\u001b[31m  padding:10px 16px;font-size:.78rem;color:var(--grn);font-family:var(--fnt-mono);\u001b[39m\n\u001b[31m  display:flex;align-items:center;gap:8px;\u001b[39m\n\u001b[31m  transform:translateY(-100%);transition:transform .3s;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.share-banner.show{transform:translateY(0);}·\u001b[39m\n\u001b[31m/* ════ MODAL ════ */\u001b[39m\n\u001b[31m.overlay{position:fixed;inset:0;background:rgba(0,0,0,.75);z-index:100;display:flex;align-items:flex-end;opacity:0;pointer-events:none;transition:opacity .3s;}\u001b[39m\n\u001b[31m.overlay.open{opacity:1;pointer-events:all;}\u001b[39m\n\u001b[31m.sheet{\u001b[39m\n\u001b[31m  background:var(--sur);border-radius:var(--r-lg) var(--r-lg) 0 0;\u001b[39m\n\u001b[31m  width:100%;max-height:88vh;overflow-y:auto;padding:18px;\u001b[39m\n\u001b[31m  transform:translateY(100%);transition:transform .3s cubic-bezier(.32,.72,0,1);\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.overlay.open .sheet{transform:translateY(0);}\u001b[39m\n\u001b[31m.sheet-handle{width:36px;height:4px;background:var(--bdr2);border-radius:2px;margin:0 auto 18px;}\u001b[39m\n\u001b[31m.sheet-title{font-family:var(--fnt-head);font-size:1.2rem;font-weight:800;letter-spacing:.5px;margin-bottom:14px;}\u001b[39m\n\u001b[31m.sheet::-webkit-scrollbar{width:3px;}\u001b[39m\n\u001b[31m.sheet::-webkit-scrollbar-thumb{background:var(--bdr2);}·\u001b[39m\n\u001b[31m/* ════ TOAST ════ */\u001b[39m\n\u001b[31m.toast{\u001b[39m\n\u001b[31m  position:fixed;bottom:20px;left:50%;transform:translateX(-50%) translateY(70px);\u001b[39m\n\u001b[31m  background:var(--sur2);border:1px solid var(--bdr2);color:var(--t1);\u001b[39m\n\u001b[31m  font-size:.78rem;padding:9px 16px;border-radius:30px;z-index:300;\u001b[39m\n\u001b[31m  transition:transform .3s;white-space:nowrap;font-family:var(--fnt-mono);\u001b[39m\n\u001b[31m  pointer-events:none;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.toast.show{transform:translateX(-50%) translateY(0);}\u001b[39m\n\u001b[31m.toast.ok{border-color:var(--grn);color:var(--grn);}\u001b[39m\n\u001b[31m.toast.err{border-color:var(--red);color:var(--red);}\u001b[39m\n\u001b[31m.toast.info{border-color:var(--sky);color:var(--sky);}·\u001b[39m\n\u001b[31m/* ════ COURIER MASTER ════ */\u001b[39m\n\u001b[31m.courier-master-row{\u001b[39m\n\u001b[31m  display:flex;align-items:center;gap:8px;padding:8px 0;\u001b[39m\n\u001b[31m  border-bottom:1px solid var(--bdr);\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.courier-master-row:last-child{border-bottom:none;}\u001b[39m\n\u001b[31m.cml{flex:1;}\u001b[39m\n\u001b[31m.cml-name{font-weight:600;font-size:.85rem;}\u001b[39m\n\u001b[31m.cml-url{font-family:var(--fnt-mono);font-size:.65rem;color:var(--t3);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:180px;}\u001b[39m\n\u001b[31m.courier-logo{width:24px;height:24px;border-radius:4px;object-fit:contain;background:var(--sur3);padding:2px;}·\u001b[39m\n\u001b[31m/* ════ PROGRESS BAR ════ */\u001b[39m\n\u001b[31m.ai-progress{\u001b[39m\n\u001b[31m  background:var(--sur2);border:1px solid var(--bdr);border-radius:var(--r-sm);\u001b[39m\n\u001b[31m  padding:10px 12px;margin-top:8px;display:none;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.ai-progress.visible{display:block;}\u001b[39m\n\u001b[31m.ai-prog-bar{height:3px;background:var(--bdr);border-radius:3px;margin-top:6px;overflow:hidden;}\u001b[39m\n\u001b[31m.ai-prog-fill{height:100%;background:linear-gradient(90deg,var(--sky),var(--acc));width:0;transition:width .3s;}\u001b[39m\n\u001b[31m.ai-prog-label{font-size:.68rem;color:var(--sky);font-family:var(--fnt-mono);}·\u001b[39m\n\u001b[31m/* ════ EMPTY ════ */\u001b[39m\n\u001b[31m.empty{padding:40px 20px;text-align:center;color:var(--t4);}\u001b[39m\n\u001b[31m.empty .e-icon{font-size:2.5rem;margin-bottom:10px;}\u001b[39m\n\u001b[31m.empty p{font-size:.82rem;}·\u001b[39m\n\u001b[31m/* ════ INFO TAG ════ */\u001b[39m\n\u001b[31m.info-tag{\u001b[39m\n\u001b[31m  background:var(--sur2);border:1px solid var(--bdr);border-radius:var(--r-sm);\u001b[39m\n\u001b[31m  padding:7px 10px;font-size:.75rem;color:var(--t2);margin-bottom:8px;\u001b[39m\n\u001b[31m  display:flex;align-items:center;gap:8px;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.info-tag strong{color:var(--t1);}·\u001b[39m\n\u001b[31m/* ════ JOB PILL ════ */\u001b[39m\n\u001b[31m.job-pill{\u001b[39m\n\u001b[31m  display:inline-flex;align-items:center;gap:6px;\u001b[39m\n\u001b[31m  background:rgba(249,115,22,.1);border:1px solid rgba(249,115,22,.3);\u001b[39m\n\u001b[31m  color:var(--acc);font-family:var(--fnt-mono);font-size:.68rem;\u001b[39m\n\u001b[31m  padding:3px 9px;border-radius:20px;margin-top:4px;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.job-pill button{background:none;border:none;color:var(--acc);cursor:pointer;font-size:.7rem;}·\u001b[39m\n\u001b[31m/* ════ SECTION DIVIDER ════ */\u001b[39m\n\u001b[31m.sec-div{\u001b[39m\n\u001b[31m  display:flex;align-items:center;gap:10px;margin:14px 0 10px;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.sec-div span{font-family:var(--fnt-head);font-size:.7rem;font-weight:700;color:var(--t3);text-transform:uppercase;letter-spacing:2px;white-space:nowrap;}\u001b[39m\n\u001b[31m.sec-div::before,.sec-div::after{content:'';flex:1;height:1px;background:var(--bdr);}·\u001b[39m\n\u001b[31m/* ── MAPPING UI ── */\u001b[39m\n\u001b[31m.map-table{width:100%;border-collapse:collapse;font-size:.78rem;margin:10px 0;}\u001b[39m\n\u001b[31m.map-table th{background:var(--bg);color:var(--t3);font-family:var(--fnt-head);font-size:.65rem;\u001b[39m\n\u001b[31m  letter-spacing:1.5px;text-transform:uppercase;padding:7px 10px;text-align:left;\u001b[39m\n\u001b[31m  border-bottom:1px solid var(--bdr);}\u001b[39m\n\u001b[31m.map-table td{padding:7px 10px;border-bottom:1px solid var(--bdr);vertical-align:middle;}\u001b[39m\n\u001b[31m.map-table tr:last-child td{border-bottom:none;}\u001b[39m\n\u001b[31m.map-table select{background:var(--sur2);border:1px solid var(--bdr);color:var(--t1);\u001b[39m\n\u001b[31m  font-family:var(--fnt-mono);font-size:.75rem;padding:5px 8px;border-radius:var(--r-sm);\u001b[39m\n\u001b[31m  width:100%;outline:none;}\u001b[39m\n\u001b[31m.map-table select:focus{border-color:var(--acc);}\u001b[39m\n\u001b[31m.map-req{color:var(--acc);margin-left:2px;}\u001b[39m\n\u001b[31m.map-saved-badge{font-size:.6rem;background:rgba(57,255,143,.15);color:var(--accent3,#39ff8f);\u001b[39m\n\u001b[31m  border:1px solid rgba(57,255,143,.3);padding:2px 7px;border-radius:10px;\u001b[39m\n\u001b[31m  font-family:var(--fnt-mono);margin-left:6px;}·\u001b[39m\n\u001b[31m/* ── VERIFICATION SCREEN ── */\u001b[39m\n\u001b[31m#verifyPane{display:none;position:fixed;inset:0;background:var(--bg);z-index:50;\u001b[39m\n\u001b[31m  overflow-y:auto;padding:14px;}\u001b[39m\n\u001b[31m.verify-hdr{display:flex;align-items:center;gap:10px;margin-bottom:14px;\u001b[39m\n\u001b[31m  position:sticky;top:0;background:var(--bg);padding-bottom:10px;\u001b[39m\n\u001b[31m  border-bottom:1px solid var(--bdr);z-index:1;}\u001b[39m\n\u001b[31m.verify-title{font-family:var(--fnt-head);font-size:1.1rem;font-weight:800;flex:1;}\u001b[39m\n\u001b[31m.verify-row{background:var(--sur);border:1px solid var(--bdr);border-radius:var(--r-sm);\u001b[39m\n\u001b[31m  padding:10px 12px;margin-bottom:8px;position:relative;}\u001b[39m\n\u001b[31m.verify-row.has-dupe{border-color:var(--red);background:rgba(239,68,68,.04);}\u001b[39m\n\u001b[31m.verify-row.no-docket{border-color:var(--ylw);background:rgba(234,179,8,.04);}\u001b[39m\n\u001b[31m.verify-row.ok{border-left:3px solid var(--grn);}\u001b[39m\n\u001b[31m.verify-row-hdr{display:flex;align-items:center;gap:8px;margin-bottom:8px;}\u001b[39m\n\u001b[31m.verify-num{font-family:var(--fnt-mono);font-size:.65rem;color:var(--t3);}\u001b[39m\n\u001b[31m.verify-store{font-weight:600;font-size:.9rem;flex:1;}\u001b[39m\n\u001b[31m.verify-flag{font-size:.65rem;font-weight:700;font-family:var(--fnt-mono);\u001b[39m\n\u001b[31m  padding:2px 7px;border-radius:10px;text-transform:uppercase;}\u001b[39m\n\u001b[31m.vf-dupe{background:var(--red-dim);color:var(--red);}\u001b[39m\n\u001b[31m.vf-nodocket{background:var(--ylw-dim);color:var(--ylw);}\u001b[39m\n\u001b[31m.vf-ok{background:var(--grn-dim);color:var(--grn);}\u001b[39m\n\u001b[31m.vf-short{background:rgba(56,189,248,.15);color:var(--sky);}\u001b[39m\n\u001b[31m.verify-fields{display:grid;grid-template-columns:1fr 1fr;gap:8px;margin-bottom:8px;}\u001b[39m\n\u001b[31m.verify-field label{font-size:.62rem;color:var(--t3);text-transform:uppercase;\u001b[39m\n\u001b[31m  letter-spacing:1px;display:block;margin-bottom:3px;font-family:var(--fnt-mono);}\u001b[39m\n\u001b[31m.verify-field input{width:100%;background:var(--sur2);border:1px solid var(--bdr);\u001b[39m\n\u001b[31m  color:var(--t1);font-family:var(--fnt-mono);font-size:.82rem;padding:6px 9px;\u001b[39m\n\u001b[31m  border-radius:var(--r-sm);outline:none;}\u001b[39m\n\u001b[31m.verify-field input:focus{border-color:var(--acc);}\u001b[39m\n\u001b[31m.verify-field input.changed{border-color:var(--ylw);background:rgba(234,179,8,.06);}\u001b[39m\n\u001b[31m.verify-actions{display:flex;gap:6px;align-items:center;flex-wrap:wrap;}\u001b[39m\n\u001b[31m.btn-wa{background:rgba(37,211,102,.15);border:1px solid rgba(37,211,102,.3);\u001b[39m\n\u001b[31m  color:#25d366;font-size:.65rem;padding:4px 10px;border-radius:10px;cursor:pointer;\u001b[39m\n\u001b[31m  font-family:var(--fnt-mono);transition:all .2s;}\u001b[39m\n\u001b[31m.btn-wa:hover{background:rgba(37,211,102,.3);}\u001b[39m\n\u001b[31m.verify-thumb{width:48px;height:48px;object-fit:cover;border-radius:6px;\u001b[39m\n\u001b[31m  border:1px solid var(--bdr);cursor:pointer;flex-shrink:0;}\u001b[39m\n\u001b[31m.verify-summary{background:var(--sur);border:1px solid var(--bdr);border-radius:var(--r);\u001b[39m\n\u001b[31m  padding:12px 14px;margin-bottom:14px;display:flex;gap:16px;flex-wrap:wrap;}\u001b[39m\n\u001b[31m.vs-item{text-align:center;}\u001b[39m\n\u001b[31m.vs-val{font-family:var(--fnt-head);font-size:1.3rem;font-weight:800;}\u001b[39m\n\u001b[31m.vs-lbl{font-size:.6rem;color:var(--t3);font-family:var(--fnt-mono);text-transform:uppercase;\u001b[39m\n\u001b[31m  letter-spacing:.5px;margin-top:2px;}\u001b[39m\n\u001b[31m.ocr-correction-note{font-size:.65rem;color:var(--ylw);font-family:var(--fnt-mono);\u001b[39m\n\u001b[31m  margin-top:3px;}\u001b[39m\n\u001b[31m</style>\u001b[39m\n\u001b[31m</head>\u001b[39m\n\u001b[31m<body>·\u001b[39m\n\u001b[31m<!-- SPLASH -->\u001b[39m\n\u001b[31m<div id=\\\"splash\\\">\u001b[39m\n\u001b[31m  <div class=\\\"splash-logo\\\">360LM <em>DISPATCH</em></div>\u001b[39m\n\u001b[31m  <div class=\\\"splash-sub\\\">Courier · Bus · Field Ops</div>\u001b[39m\n\u001b[31m  <div class=\\\"splash-track\\\"><div class=\\\"splash-track-fill\\\"></div></div>\u001b[39m\n\u001b[31m</div>·\u001b[39m\n\u001b[31m<!-- SHARE BANNER -->\u001b[39m\n\u001b[31m<div class=\\\"share-banner\\\" id=\\\"shareBanner\\\">\u001b[39m\n\u001b[31m  📤 Images received via Share — processing for OCR...\u001b[39m\n\u001b[31m  <button style=\\\"background:none;border:none;color:var(--grn);cursor:pointer;margin-left:auto;\\\" onclick=\\\"dismissShare()\\\">✕</button>\u001b[39m\n\u001b[31m</div>·\u001b[39m\n\u001b[31m<!-- APP -->\u001b[39m\n\u001b[31m<div id=\\\"app\\\" style=\\\"display:none;\\\">·\u001b[39m\n\u001b[31m  <div class=\\\"hdr\\\">\u001b[39m\n\u001b[31m    <div class=\\\"hdr-brand\\\">360LM <em>DISPATCH</em></div>\u001b[39m\n\u001b[31m    <span class=\\\"hdr-dot\\\" id=\\\"netDot\\\"></span>\u001b[39m\n\u001b[31m    <button class=\\\"hdr-btn\\\" onclick=\\\"window.location.href='/hub/'\\\" title=\\\"Hub\\\">🏡</button>\u001b[39m\n\u001b[31m    <button class=\\\"hdr-btn\\\" onclick=\\\"showPane('dashboard')\\\">📊</button>\u001b[39m\n\u001b[31m    <button class=\\\"hdr-btn primary\\\" onclick=\\\"newDispatch()\\\">+ NEW</button>\u001b[39m\n\u001b[31m  </div>·\u001b[39m\n\u001b[31m  <div class=\\\"tabs\\\">\u001b[39m\n\u001b[31m    <button class=\\\"tab active\\\" onclick=\\\"showPane('new')\\\" id=\\\"tab-new\\\">NEW</button>\u001b[39m\n\u001b[31m    <button class=\\\"tab\\\" onclick=\\\"showPane('dashboard')\\\" id=\\\"tab-dashboard\\\">DASHBOARD</button>\u001b[39m\n\u001b[31m    <button class=\\\"tab\\\" onclick=\\\"showPane('track')\\\" id=\\\"tab-track\\\">TRACK</button>\u001b[39m\n\u001b[31m    <button class=\\\"tab\\\" onclick=\\\"showPane('history')\\\" id=\\\"tab-history\\\">HISTORY <span class=\\\"tab-badge\\\" id=\\\"histCount\\\">0</span></button>\u001b[39m\n\u001b[31m    <button class=\\\"tab\\\" onclick=\\\"showPane('config')\\\" id=\\\"tab-config\\\">CONFIG</button>\u001b[39m\n\u001b[31m  </div>·\u001b[39m\n\u001b[31m  <!-- ════════════ NEW DISPATCH ════════════ -->\u001b[39m\n\u001b[31m  <div class=\\\"pane active\\\" id=\\\"pane-new\\\">·\u001b[39m\n\u001b[31m    <!-- 1. JOB REFERENCE -->\u001b[39m\n\u001b[31m    <div class=\\\"card accent-left\\\">\u001b[39m\n\u001b[31m      <div class=\\\"card-title\\\"><span class=\\\"ct-dot\\\"></span>Job Reference</div>\u001b[39m\n\u001b[31m      <div class=\\\"row2\\\">\u001b[39m\n\u001b[31m        <div class=\\\"f\\\">\u001b[39m\n\u001b[31m          <label>Client <span class=\\\"req\\\">*</span></label>\u001b[39m\n\u001b[31m          <select id=\\\"n_client\\\" onchange=\\\"onClientChange()\\\">\u001b[39m\n\u001b[31m            <option value=\\\"\\\">Select...</option>\u001b[39m\n\u001b[31m            <option>Lenovo</option><option>HP</option>\u001b[39m\n\u001b[31m            <option>Philips</option><option>Liebherr</option>\u001b[39m\n\u001b[31m            <option value=\\\"__c\\\">+ Custom</option>\u001b[39m\n\u001b[31m          </select>\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m        <div class=\\\"f\\\">\u001b[39m\n\u001b[31m          <label>Job Ref</label>\u001b[39m\n\u001b[31m          <input type=\\\"text\\\" id=\\\"n_jobref\\\" placeholder=\\\"JOB-042\\\" oninput=\\\"updatePill()\\\">\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      <div class=\\\"f\\\" id=\\\"customClientField\\\" style=\\\"display:none;\\\">\u001b[39m\n\u001b[31m        <label>Custom Client</label>\u001b[39m\n\u001b[31m        <input type=\\\"text\\\" id=\\\"n_customClient\\\" placeholder=\\\"Client name\\\" oninput=\\\"updatePill()\\\">\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      <div id=\\\"jobPill\\\" style=\\\"display:none;\\\" class=\\\"job-pill\\\">\u001b[39m\n\u001b[31m        🔗 <span id=\\\"jobPillTxt\\\"></span>\u001b[39m\n\u001b[31m        <button onclick=\\\"clearJob()\\\">✕</button>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      <div class=\\\"row2\\\" style=\\\"margin-top:10px;\\\">\u001b[39m\n\u001b[31m        <div class=\\\"f\\\" style=\\\"margin-bottom:0;\\\">\u001b[39m\n\u001b[31m          <label>Dispatch Date</label>\u001b[39m\n\u001b[31m          <input type=\\\"date\\\" id=\\\"n_date\\\">\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m        <div class=\\\"f\\\" style=\\\"margin-bottom:0;\\\">\u001b[39m\n\u001b[31m          <label>Courier Company <span class=\\\"req\\\">*</span></label>\u001b[39m\n\u001b[31m          <select id=\\\"n_courier\\\">\u001b[39m\n\u001b[31m            <option value=\\\"\\\">Select...</option>\u001b[39m\n\u001b[31m            <option>SkyKing</option><option>DTDC</option>\u001b[39m\n\u001b[31m            <option>Delhivery</option><option>Blue Dart</option>\u001b[39m\n\u001b[31m            <option>FedEx</option><option>Ecom Express</option>\u001b[39m\n\u001b[31m            <option>India Post</option><option value=\\\"__o\\\">Other</option>\u001b[39m\n\u001b[31m          </select>\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m    </div>·\u001b[39m\n\u001b[31m    <!-- 2. STORE LIST UPLOAD -->\u001b[39m\n\u001b[31m    <div class=\\\"card\\\">\u001b[39m\n\u001b[31m      <div class=\\\"card-title\\\"><span class=\\\"ct-dot\\\" style=\\\"background:var(--sky);\\\"></span>Store / Destination List</div>\u001b[39m\n\u001b[31m      <div class=\\\"store-upload-zone\\\" id=\\\"storeZone\\\" onclick=\\\"document.getElementById('storeFile').click();event.stopPropagation();\\\" style=\\\"cursor:pointer;\\\">\u001b[39m\n\u001b[31m        <input type=\\\"file\\\" id=\\\"storeFile\\\" accept=\\\".xlsx,.xls,.csv\\\" onchange=\\\"handleStoreFile(this)\\\" onclick=\\\"event.stopPropagation();\\\" style=\\\"display:none;\\\">\u001b[39m\n\u001b[31m        <div style=\\\"font-size:1.5rem;margin-bottom:6px;\\\">📋</div>\u001b[39m\n\u001b[31m        <div style=\\\"font-family:var(--fnt-head);font-weight:700;font-size:.9rem;\\\">Upload Store List</div>\u001b[39m\n\u001b[31m        <div style=\\\"font-size:.68rem;color:var(--t3);margin-top:3px;font-family:var(--fnt-mono);\\\">Excel / CSV · stores, cities, contacts</div>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      <div id=\\\"storeTableArea\\\" style=\\\"display:none;\\\">\u001b[39m\n\u001b[31m        <!-- Column detection result -->\u001b[39m\n\u001b[31m        <div id=\\\"storeColInfo\\\" style=\\\"margin-top:10px;display:none;\\\"></div>\u001b[39m\n\u001b[31m        <div style=\\\"display:flex;justify-content:space-between;align-items:center;margin-top:10px;margin-bottom:6px;\\\">\u001b[39m\n\u001b[31m          <span style=\\\"font-family:var(--fnt-mono);font-size:.7rem;color:var(--sky);\\\" id=\\\"storeCount\\\">0 stores loaded</span>\u001b[39m\n\u001b[31m          <button class=\\\"btn-sm\\\" onclick=\\\"clearStores()\\\">Clear</button>\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m        <div class=\\\"store-table-wrap\\\">\u001b[39m\n\u001b[31m          <table class=\\\"store-table\\\" id=\\\"storeTable\\\"></table>\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m        <!-- Inline add-as-consignees UI instead of confirm() -->\u001b[39m\n\u001b[31m        <div id=\\\"storeAddPanel\\\" style=\\\"margin-top:10px;background:var(--sur3);border:1px solid var(--bdr2);border-radius:var(--r-sm);padding:10px 12px;display:none;\\\">\u001b[39m\n\u001b[31m          <div style=\\\"font-size:.75rem;color:var(--t1);margin-bottom:8px;font-weight:600;\\\" id=\\\"storeAddTitle\\\">Add stores as consignees?</div>\u001b[39m\n\u001b[31m          <div class=\\\"row2\\\">\u001b[39m\n\u001b[31m            <button class=\\\"btn btn-ghost\\\" style=\\\"font-size:.75rem;padding:8px;\\\" onclick=\\\"document.getElementById('storeAddPanel').style.display='none'\\\">Skip for now</button>\u001b[39m\n\u001b[31m            <button class=\\\"btn btn-orange\\\" style=\\\"font-size:.75rem;padding:8px;\\\" id=\\\"storeAddBtn\\\" onclick=\\\"addStoresToConsignees()\\\">✓ Add All as Consignees</button>\u001b[39m\n\u001b[31m          </div>\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m        <button class=\\\"btn btn-ghost\\\" style=\\\"margin-top:8px;font-size:.78rem;\\\" onclick=\\\"autoMatchDockets()\\\">\u001b[39m\n\u001b[31m          🔗 Auto-match Dockets to Stores\u001b[39m\n\u001b[31m        </button>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m    </div>·\u001b[39m\n\u001b[31m    <!-- 3. DOCKET SLIPS — AI OCR -->\u001b[39m\n\u001b[31m    <div class=\\\"card\\\">\u001b[39m\n\u001b[31m      <div class=\\\"card-title\\\"><span class=\\\"ct-dot\\\" style=\\\"background:var(--grn);\\\"></span>Docket Slips — AI OCR</div>·\u001b[39m\n\u001b[31m      <!-- Share target indicator -->\u001b[39m\n\u001b[31m      <div class=\\\"info-tag\\\" id=\\\"shareTargetInfo\\\" style=\\\"display:none;\\\">\u001b[39m\n\u001b[31m        <span>📤</span><span>Images shared from WhatsApp — <strong id=\\\"sharedCount\\\">0</strong> slips received</span>\u001b[39m\n\u001b[31m      </div>·\u001b[39m\n\u001b[31m      <label class=\\\"upload-zone\\\" id=\\\"slipZone\\\" for=\\\"slipFiles\\\" style=\\\"display:block;cursor:pointer;\\\">\u001b[39m\n\u001b[31m        <input type=\\\"file\\\" id=\\\"slipFiles\\\" accept=\\\"image/*\\\" multiple onchange=\\\"handleSlipFiles(this.files)\\\" style=\\\"display:none;\\\">\u001b[39m\n\u001b[31m        <div class=\\\"uz-icon\\\">🖼️</div>\u001b[39m\n\u001b[31m        <div class=\\\"uz-title\\\">Upload Courier Slips</div>\u001b[39m\n\u001b[31m        <div class=\\\"uz-sub\\\">Multiple images · Tap or drag · Camera capture supported</div>\u001b[39m\n\u001b[31m      </label>·\u001b[39m\n\u001b[31m      <!-- OCR Working indicator -->\u001b[39m\n\u001b[31m      <div class=\\\"ocr-working\\\" id=\\\"ocrWorking\\\">\u001b[39m\n\u001b[31m        <span class=\\\"spin\\\">⟳</span>\u001b[39m\n\u001b[31m        <span id=\\\"ocrWorkingText\\\">Running OCR...</span>\u001b[39m\n\u001b[31m      </div>·\u001b[39m\n\u001b[31m      <!-- AI Progress -->\u001b[39m\n\u001b[31m      <div class=\\\"ai-progress\\\" id=\\\"aiProgress\\\">\u001b[39m\n\u001b[31m        <div class=\\\"ai-prog-label\\\" id=\\\"aiProgLabel\\\">Initializing Transformers.js...</div>\u001b[39m\n\u001b[31m        <div class=\\\"ai-prog-bar\\\"><div class=\\\"ai-prog-fill\\\" id=\\\"aiProgFill\\\"></div></div>\u001b[39m\n\u001b[31m      </div>·\u001b[39m\n\u001b[31m      <!-- OCR Results -->\u001b[39m\n\u001b[31m      <div class=\\\"ocr-results\\\" id=\\\"ocrResults\\\">\u001b[39m\n\u001b[31m        <div class=\\\"sec-div\\\"><span>Detected Dockets</span></div>\u001b[39m\n\u001b[31m        <div id=\\\"ocrSummary\\\" style=\\\"display:none;font-size:.72rem;font-family:var(--fnt-mono);\u001b[39m\n\u001b[31m          padding:4px 8px 8px;gap:8px\\\"></div>\u001b[39m\n\u001b[31m        <div class=\\\"ocr-slip-list\\\" id=\\\"ocrSlipList\\\"></div>\u001b[39m\n\u001b[31m        <div class=\\\"btn-row\\\" style=\\\"margin-top:10px;\\\">\u001b[39m\n\u001b[31m          <button class=\\\"btn btn-ghost\\\" onclick=\\\"clearSlips()\\\">Clear All</button>\u001b[39m\n\u001b[31m          <button class=\\\"btn btn-grn\\\" onclick=\\\"applyAllDockets()\\\">✓ Apply All to Consignees</button>\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m    </div>·\u001b[39m\n\u001b[31m    <!-- 4. CONSIGNEES -->\u001b[39m\n\u001b[31m    <div class=\\\"card accent-left\\\">\u001b[39m\n\u001b[31m      <div class=\\\"card-title\\\"><span class=\\\"ct-dot\\\"></span>Consignees</div>\u001b[39m\n\u001b[31m      <div class=\\\"cons-list\\\" id=\\\"consList\\\"></div>\u001b[39m\n\u001b[31m      <button class=\\\"btn btn-ghost\\\" style=\\\"font-size:.8rem;\\\" onclick=\\\"addConsignee()\\\">+ Add Consignee</button>\u001b[39m\n\u001b[31m    </div>·\u001b[39m\n\u001b[31m    <!-- 5. REMARKS -->\u001b[39m\n\u001b[31m    <div class=\\\"card\\\">\u001b[39m\n\u001b[31m      <div class=\\\"card-title\\\"><span class=\\\"ct-dot\\\" style=\\\"background:var(--t3);\\\"></span>Remarks</div>\u001b[39m\n\u001b[31m      <div class=\\\"f\\\" style=\\\"margin-bottom:0;\\\">\u001b[39m\n\u001b[31m        <textarea id=\\\"n_remarks\\\" placeholder=\\\"Instructions, notes, special handling...\\\"></textarea>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m    </div>·\u001b[39m\n\u001b[31m    <!-- ACTIONS -->\u001b[39m\n\u001b[31m    <div class=\\\"btn-row\\\">\u001b[39m\n\u001b[31m      <button class=\\\"btn btn-ghost\\\" onclick=\\\"saveDraft()\\\">💾 Draft</button>\u001b[39m\n\u001b[31m      <button class=\\\"btn btn-orange\\\" onclick=\\\"finaliseDispatch()\\\">✓ Finalise & Log</button>\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m    <div style=\\\"height:16px;\\\"></div>\u001b[39m\n\u001b[31m  </div>·\u001b[39m\n\u001b[31m  <!-- ════════════ DASHBOARD ════════════ -->\u001b[39m\n\u001b[31m  <div class=\\\"pane\\\" id=\\\"pane-dashboard\\\">\u001b[39m\n\u001b[31m    <div class=\\\"dash-stats\\\">\u001b[39m\n\u001b[31m      <div class=\\\"stat-box grn\\\"><div class=\\\"stat-val\\\" id=\\\"st-delivered\\\">0</div><div class=\\\"stat-lbl\\\">Delivered</div></div>\u001b[39m\n\u001b[31m      <div class=\\\"stat-box ylw\\\"><div class=\\\"stat-val\\\" id=\\\"st-transit\\\">0</div><div class=\\\"stat-lbl\\\">In Transit</div></div>\u001b[39m\n\u001b[31m      <div class=\\\"stat-box sky\\\"><div class=\\\"stat-val\\\" id=\\\"st-booked\\\">0</div><div class=\\\"stat-lbl\\\">Booked</div></div>\u001b[39m\n\u001b[31m      <div class=\\\"stat-box red\\\"><div class=\\\"stat-val\\\" id=\\\"st-pending\\\">0</div><div class=\\\"stat-lbl\\\">Pending</div></div>\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m    <div id=\\\"dashFilters\\\" style=\\\"display:flex;gap:6px;flex-wrap:wrap;margin-bottom:12px;\\\">\u001b[39m\n\u001b[31m      <span class=\\\"btn-sm primary\\\" onclick=\\\"filterDash('all',this)\\\" style=\\\"cursor:pointer;\\\">All</span>\u001b[39m\n\u001b[31m      <span class=\\\"btn-sm\\\" onclick=\\\"filterDash('delivered',this)\\\" style=\\\"cursor:pointer;\\\">Delivered</span>\u001b[39m\n\u001b[31m      <span class=\\\"btn-sm\\\" onclick=\\\"filterDash('transit',this)\\\" style=\\\"cursor:pointer;\\\">In Transit</span>\u001b[39m\n\u001b[31m      <span class=\\\"btn-sm\\\" onclick=\\\"filterDash('booked',this)\\\" style=\\\"cursor:pointer;\\\">Booked</span>\u001b[39m\n\u001b[31m      <span class=\\\"btn-sm\\\" onclick=\\\"filterDash('pending',this)\\\" style=\\\"cursor:pointer;\\\">Pending</span>\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m    <div class=\\\"docket-grid\\\" id=\\\"dashGrid\\\">\u001b[39m\n\u001b[31m      <div class=\\\"empty\\\"><div class=\\\"e-icon\\\">📊</div><p>No dispatches yet.<br>Create one to see the dashboard.</p></div>\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m    <div style=\\\"display:flex;gap:8px;margin-top:10px;flex-wrap:wrap;\\\">\u001b[39m\n\u001b[31m      <button class=\\\"btn btn-ghost\\\" style=\\\"flex:2;font-size:.8rem;\\\" id=\\\"btnRefreshAll\\\" onclick=\\\"refreshAllTracking()\\\">\u001b[39m\n\u001b[31m        🔄 Refresh All Tracking\u001b[39m\n\u001b[31m      </button>\u001b[39m\n\u001b[31m      <button class=\\\"btn btn-ghost\\\" style=\\\"flex:1;font-size:.8rem;border-color:var(--sky);color:var(--sky);\\\"\u001b[39m\n\u001b[31m        onclick=\\\"shareSelectedReport()\\\">\u001b[39m\n\u001b[31m        📊 Share Selected\u001b[39m\n\u001b[31m      </button>\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m  </div>·\u001b[39m\n\u001b[31m  <!-- ════════════ TRACK ════════════ -->\u001b[39m\n\u001b[31m  <div class=\\\"pane\\\" id=\\\"pane-track\\\">·\u001b[39m\n\u001b[31m    <!-- Courier selector -->\u001b[39m\n\u001b[31m    <div class=\\\"card\\\" style=\\\"margin-bottom:10px;\\\">\u001b[39m\n\u001b[31m      <div class=\\\"card-title\\\"><span class=\\\"ct-dot\\\" style=\\\"background:var(--sky);\\\"></span>Manual Tracking</div>\u001b[39m\n\u001b[31m      <div style=\\\"font-size:.72rem;color:var(--t2);margin-bottom:10px;line-height:1.5;\\\">\u001b[39m\n\u001b[31m        Track any docket number — select from already dispatched dockets or enter new ones manually.\u001b[39m\n\u001b[31m        SkyKing supports up to <strong>10 dockets at once</strong>; others are tracked one at a time.\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      <div class=\\\"f\\\">\u001b[39m\n\u001b[31m        <label>Courier Company</label>\u001b[39m\n\u001b[31m        <select id=\\\"t_courier\\\" onchange=\\\"buildTrackDocketChips()\\\">\u001b[39m\n\u001b[31m          <option>SkyKing</option><option>DTDC</option><option>Delhivery</option>\u001b[39m\n\u001b[31m          <option>Blue Dart</option><option>FedEx</option><option>Ecom Express</option><option>India Post</option>\u001b[39m\n\u001b[31m        </select>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m    </div>·\u001b[39m\n\u001b[31m    <!-- Docket selector — from existing dispatches -->\u001b[39m\n\u001b[31m    <div class=\\\"card\\\" style=\\\"margin-bottom:10px;\\\">\u001b[39m\n\u001b[31m      <div class=\\\"card-title\\\"><span class=\\\"ct-dot\\\" style=\\\"background:var(--acc);\\\"></span>\u001b[39m\n\u001b[31m        Select from Dispatched Dockets\u001b[39m\n\u001b[31m        <span id=\\\"trackCourierLimit\\\" style=\\\"margin-left:auto;font-size:.65rem;color:var(--t3);font-weight:400;text-transform:none;letter-spacing:0;font-family:var(--fnt-mono);\\\"></span>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      <div id=\\\"trackDocketChips\\\" style=\\\"display:flex;flex-wrap:wrap;gap:6px;margin-bottom:10px;min-height:28px;\\\">\u001b[39m\n\u001b[31m        <span style=\\\"font-size:.72rem;color:var(--t3);font-family:var(--fnt-mono);\\\">No dispatched dockets yet</span>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      <div style=\\\"font-size:.68rem;color:var(--t3);font-family:var(--fnt-mono);margin-bottom:8px;\\\">\u001b[39m\n\u001b[31m        Tap a docket to add/remove from tracking list. Selected shown in orange.\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m    </div>·\u001b[39m\n\u001b[31m    <!-- Manual entry for new dockets -->\u001b[39m\n\u001b[31m    <div class=\\\"card\\\" style=\\\"margin-bottom:10px;\\\">\u001b[39m\n\u001b[31m      <div class=\\\"card-title\\\"><span class=\\\"ct-dot\\\" style=\\\"background:var(--grn);\\\"></span>Add New Docket Numbers</div>\u001b[39m\n\u001b[31m      <div class=\\\"f\\\" style=\\\"margin-bottom:6px;\\\">\u001b[39m\n\u001b[31m        <label>Enter docket numbers (comma or newline separated)</label>\u001b[39m\n\u001b[31m        <textarea id=\\\"t_manual_dockets\\\" placeholder=\\\"443005527&#10;443005528&#10;or: 443005527, 443005528\\\"\u001b[39m\n\u001b[31m          style=\\\"min-height:56px;font-family:var(--fnt-mono);font-size:.82rem;letter-spacing:.5px;\\\"></textarea>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      <button class=\\\"btn-sm primary\\\" onclick=\\\"addManualDockets()\\\" style=\\\"font-size:.72rem;\\\">+ Add to List</button>\u001b[39m\n\u001b[31m    </div>·\u001b[39m\n\u001b[31m    <!-- Selected dockets summary + track button -->\u001b[39m\n\u001b[31m    <div id=\\\"trackSelectedSummary\\\" style=\\\"display:none;\\\" class=\\\"card\\\" style=\\\"margin-bottom:10px;\\\">\u001b[39m\n\u001b[31m      <div style=\\\"display:flex;align-items:center;justify-content:space-between;margin-bottom:8px;\\\">\u001b[39m\n\u001b[31m        <div>\u001b[39m\n\u001b[31m          <span style=\\\"font-family:var(--fnt-mono);font-size:.72rem;color:var(--sky);\\\" id=\\\"trackSelectedCount\\\">0 selected</span>\u001b[39m\n\u001b[31m          <span style=\\\"font-family:var(--fnt-mono);font-size:.65rem;color:var(--t3);margin-left:8px;\\\" id=\\\"trackSelectedList\\\"></span>\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m        <button class=\\\"btn-sm\\\" onclick=\\\"clearTrackSelection()\\\">Clear</button>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      <div class=\\\"btn-row\\\">\u001b[39m\n\u001b[31m        <button class=\\\"btn btn-ghost\\\" onclick=\\\"openTrackOnCourierSite()\\\">🔗 Open Courier Site</button>\u001b[39m\n\u001b[31m        <button class=\\\"btn btn-orange\\\" onclick=\\\"trackSelected()\\\">🔍 Track All via Proxy</button>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m    </div>·\u001b[39m\n\u001b[31m    <div id=\\\"trackResultsArea\\\"></div>\u001b[39m\n\u001b[31m  </div>·\u001b[39m\n\u001b[31m  <!-- ════════════ HISTORY ════════════ -->\u001b[39m\n\u001b[31m  <div class=\\\"pane\\\" id=\\\"pane-history\\\">\u001b[39m\n\u001b[31m    <div id=\\\"historyList\\\">\u001b[39m\n\u001b[31m      <div class=\\\"empty\\\"><div class=\\\"e-icon\\\">📭</div><p>No dispatch history yet.</p></div>\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m  </div>·\u001b[39m\n\u001b[31m  <!-- ════════════ CONFIG ════════════ -->\u001b[39m\n\u001b[31m  <div class=\\\"pane\\\" id=\\\"pane-config\\\">·\u001b[39m\n\u001b[31m    <!-- SHEETS URL — the only thing you type manually in the PWA -->\u001b[39m\n\u001b[31m    <div class=\\\"card\\\" style=\\\"border-color:var(--acc);border-width:2px;\\\">\u001b[39m\n\u001b[31m      <div class=\\\"card-title\\\"><span class=\\\"ct-dot\\\"></span>Google Sheets — Master Config</div>\u001b[39m\n\u001b[31m      <div class=\\\"info-tag\\\" style=\\\"background:rgba(34,197,94,.07);border-color:rgba(34,197,94,.3);\\\">\u001b[39m\n\u001b[31m        <span>✅</span>\u001b[39m\n\u001b[31m        <span>URL is loaded automatically from <strong>config.json</strong> on the VPS — no manual entry needed. All other settings sync from your <strong>Google Sheet</strong>.</span>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      <div class=\\\"f\\\">\u001b[39m\n\u001b[31m        <label>Apps Script URL <span style=\\\"font-size:.65rem;color:var(--t3);font-weight:400;\\\">(auto-loaded from config.json)</span></label>\u001b[39m\n\u001b[31m        <input type=\\\"url\\\" id=\\\"cfg_sheets_url\\\"\u001b[39m\n\u001b[31m          placeholder=\\\"Auto-loaded from server...\\\"\u001b[39m\n\u001b[31m          oninput=\\\"onSheetsUrlChange()\\\" readonly\u001b[39m\n\u001b[31m          style=\\\"opacity:.7;cursor:default;\\\">\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      <div id=\\\"syncStatus\\\" class=\\\"info-tag\\\" style=\\\"display:none;\\\">\u001b[39m\n\u001b[31m        <span id=\\\"syncIcon\\\">⏳</span>\u001b[39m\n\u001b[31m        <span id=\\\"syncMsg\\\">Syncing from sheet...</span>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      <div class=\\\"btn-row\\\">\u001b[39m\n\u001b[31m        <button class=\\\"btn btn-ghost\\\" onclick=\\\"syncFromSheet()\\\">🔄 Sync Now</button>\u001b[39m\n\u001b[31m        <button class=\\\"btn btn-orange\\\" onclick=\\\"saveSheetsUrl()\\\">💾 Save & Sync</button>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m    </div>·\u001b[39m\n\u001b[31m    <!-- READ-ONLY display of what was loaded from sheet -->\u001b[39m\n\u001b[31m    <div class=\\\"card\\\" id=\\\"sheetConfigDisplay\\\" style=\\\"display:none;\\\">\u001b[39m\n\u001b[31m      <div class=\\\"card-title\\\"><span class=\\\"ct-dot\\\" style=\\\"background:var(--grn);\\\"></span>Config Loaded from Sheet\u001b[39m\n\u001b[31m        <span id=\\\"sheetSyncTime\\\" style=\\\"margin-left:auto;font-size:.62rem;color:var(--t3);font-family:var(--fnt-mono);font-weight:400;text-transform:none;letter-spacing:0;\\\"></span>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      <div id=\\\"sheetConfigRows\\\"></div>\u001b[39m\n\u001b[31m      <div class=\\\"info-tag\\\" style=\\\"margin-top:8px;\\\">\u001b[39m\n\u001b[31m        <span>✏️</span><span>To change any value, edit the <strong>Config sheet</strong> in Google Sheets, then tap Sync Now above.</span>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m    </div>·\u001b[39m\n\u001b[31m    <!-- Courier Master (from sheet) -->\u001b[39m\n\u001b[31m    <div class=\\\"card\\\">\u001b[39m\n\u001b[31m      <div class=\\\"card-title\\\"><span class=\\\"ct-dot\\\" style=\\\"background:var(--ylw);\\\"></span>Courier Master</div>\u001b[39m\n\u001b[31m      <div id=\\\"courierMasterList\\\"></div>\u001b[39m\n\u001b[31m      <button class=\\\"btn btn-ghost\\\" style=\\\"margin-top:8px;font-size:.78rem;\\\" onclick=\\\"openCourierMaster()\\\">✏️ Edit in Sheet / Override</button>\u001b[39m\n\u001b[31m    </div>·\u001b[39m\n\u001b[31m    <button class=\\\"btn\\\" style=\\\"background:var(--red-dim);color:var(--red);border:1px solid var(--red);margin-top:4px;\\\" onclick=\\\"clearAll()\\\">🗑️ Clear All Local Data</button>\u001b[39m\n\u001b[31m    <div style=\\\"height:20px;\\\"></div>\u001b[39m\n\u001b[31m  </div>··\u001b[39m\n\u001b[31m<!-- ════ ADDRESS REVIEW PANE ════ -->\u001b[39m\n\u001b[31m<div id=\\\"addrReviewPane\\\" style=\\\"display:none;position:fixed;inset:0;background:var(--bg);z-index:50;overflow-y:auto;padding:14px;\\\">\u001b[39m\n\u001b[31m  <div class=\\\"verify-hdr\\\">\u001b[39m\n\u001b[31m    <button class=\\\"btn-icon\\\" onclick=\\\"document.getElementById('addrReviewPane').style.display='none'\\\" style=\\\"font-size:1.1rem;\\\">←</button>\u001b[39m\n\u001b[31m    <div class=\\\"verify-title\\\">Review AI-Parsed Addresses</div>\u001b[39m\n\u001b[31m  </div>\u001b[39m\n\u001b[31m  <div style=\\\"background:rgba(56,189,248,.08);border:1px solid rgba(56,189,248,.3);border-radius:var(--r-sm);\u001b[39m\n\u001b[31m    padding:8px 12px;margin-bottom:12px;font-size:.72rem;color:var(--sky);\\\">\u001b[39m\n\u001b[31m    ✏️ Review each row — correct name and city if wrong. Raw address shown below each row for reference.\u001b[39m\n\u001b[31m    Corrections are saved and improve future AI parsing automatically.\u001b[39m\n\u001b[31m  </div>\u001b[39m\n\u001b[31m  <div class=\\\"verify-summary\\\" id=\\\"addrReviewSummary\\\"></div>\u001b[39m\n\u001b[31m  <div id=\\\"addrReviewList\\\"></div>\u001b[39m\n\u001b[31m  <div style=\\\"position:sticky;bottom:0;background:var(--bg);padding-top:10px;\u001b[39m\n\u001b[31m    border-top:1px solid var(--bdr);margin-top:8px;\\\">\u001b[39m\n\u001b[31m    <div style=\\\"display:flex;gap:8px;\\\">\u001b[39m\n\u001b[31m      <button class=\\\"btn btn-ghost\\\" onclick=\\\"backToMapping()\\\" style=\\\"flex:1;\\\">← Back to Mapping</button>\u001b[39m\n\u001b[31m      <button class=\\\"btn btn-orange\\\" onclick=\\\"confirmAddressReview()\\\" style=\\\"flex:2;\\\">✓ Confirm &amp; Use These Addresses</button>\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m    <div style=\\\"font-size:.65rem;color:var(--t3);text-align:center;margin-top:5px;font-family:var(--fnt-mono);\\\">\u001b[39m\n\u001b[31m      Edits you made above are preserved when you go back\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m  </div>\u001b[39m\n\u001b[31m</div>·\u001b[39m\n\u001b[31m<!-- ════ VERIFICATION SCREEN ════ -->\u001b[39m\n\u001b[31m<div id=\\\"verifyPane\\\">\u001b[39m\n\u001b[31m  <div class=\\\"verify-hdr\\\">\u001b[39m\n\u001b[31m    <button class=\\\"btn-icon\\\" onclick=\\\"closeVerify()\\\" style=\\\"font-size:1.1rem;\\\">←</button>\u001b[39m\n\u001b[31m    <div class=\\\"verify-title\\\">Verify Before Finalise</div>\u001b[39m\n\u001b[31m    <button class=\\\"btn-sm\\\" onclick=\\\"approveAll()\\\" style=\\\"white-space:nowrap;\\\">✓ Approve All</button>\u001b[39m\n\u001b[31m  </div>\u001b[39m\n\u001b[31m  <div class=\\\"verify-summary\\\" id=\\\"verifySummary\\\"></div>\u001b[39m\n\u001b[31m  <div id=\\\"verifyList\\\"></div>\u001b[39m\n\u001b[31m  <div style=\\\"position:sticky;bottom:0;background:var(--bg);padding-top:10px;\u001b[39m\n\u001b[31m    border-top:1px solid var(--bdr);margin-top:8px;\\\">\u001b[39m\n\u001b[31m    <div style=\\\"display:flex;gap:8px;flex-wrap:wrap;\\\">\u001b[39m\n\u001b[31m      <button class=\\\"btn btn-ghost\\\" onclick=\\\"closeVerify()\\\" style=\\\"flex:1;\\\">← Back to Edit</button>\u001b[39m\n\u001b[31m      <button class=\\\"btn btn-ghost\\\" onclick=\\\"exportCleanSheet()\\\" style=\\\"flex:1;border-color:var(--sky);color:var(--sky);\\\">\u001b[39m\n\u001b[31m        📥 Export Clean Sheet\u001b[39m\n\u001b[31m      </button>\u001b[39m\n\u001b[31m      <button class=\\\"btn btn-orange\\\" id=\\\"btnConfirmFinalise\\\" onclick=\\\"confirmAndFinalise()\\\" style=\\\"flex:2;\\\">\u001b[39m\n\u001b[31m        ✓ Finalise Dispatch\u001b[39m\n\u001b[31m      </button>\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m  </div>\u001b[39m\n\u001b[31m</div>·\u001b[39m\n\u001b[31m</div><!-- /app -->·\u001b[39m\n\u001b[31m<!-- ════ DISPATCH DETAIL SHEET ════ -->\u001b[39m\n\u001b[31m<div class=\\\"overlay\\\" id=\\\"detailOverlay\\\" onclick=\\\"closeOverlay('detailOverlay')\\\">\u001b[39m\n\u001b[31m  <div class=\\\"sheet\\\" onclick=\\\"event.stopPropagation()\\\">\u001b[39m\n\u001b[31m    <div class=\\\"sheet-handle\\\"></div>\u001b[39m\n\u001b[31m    <div id=\\\"detailContent\\\"></div>\u001b[39m\n\u001b[31m  </div>\u001b[39m\n\u001b[31m</div>·\u001b[39m\n\u001b[31m<!-- ════ COURIER MASTER EDITOR ════ -->\u001b[39m\n\u001b[31m<div class=\\\"overlay\\\" id=\\\"courierMasterOverlay\\\" onclick=\\\"closeOverlay('courierMasterOverlay')\\\">\u001b[39m\n\u001b[31m  <div class=\\\"sheet\\\" onclick=\\\"event.stopPropagation()\\\">\u001b[39m\n\u001b[31m    <div class=\\\"sheet-handle\\\"></div>\u001b[39m\n\u001b[31m    <div class=\\\"sheet-title\\\">✏️ Courier Master</div>\u001b[39m\n\u001b[31m    <div id=\\\"courierMasterEditor\\\"></div>\u001b[39m\n\u001b[31m  </div>\u001b[39m\n\u001b[31m</div>·\u001b[39m\n\u001b[31m<!-- ════ MANUAL TRACKING OVERLAY ════ -->\u001b[39m\n\u001b[31m<div class=\\\"overlay\\\" id=\\\"manualTrackOverlay\\\" onclick=\\\"closeOverlay('manualTrackOverlay')\\\" data-docket=\\\"\\\" data-dispatch-id=\\\"\\\">\u001b[39m\n\u001b[31m  <div class=\\\"sheet\\\" onclick=\\\"event.stopPropagation()\\\">\u001b[39m\n\u001b[31m    <div class=\\\"sheet-handle\\\"></div>\u001b[39m\n\u001b[31m    <div class=\\\"sheet-title\\\">📞 Manual Tracking Log</div>\u001b[39m\n\u001b[31m    <div style=\\\"background:var(--sur2);border:1px solid var(--bdr);border-radius:var(--r-sm);\u001b[39m\n\u001b[31m      padding:8px 12px;margin-bottom:12px;font-family:var(--fnt-mono);font-size:.82rem;color:var(--acc);\\\"\u001b[39m\n\u001b[31m      id=\\\"mtDocketDisplay\\\"></div>\u001b[39m\n\u001b[31m    <div class=\\\"row2\\\">\u001b[39m\n\u001b[31m      <div class=\\\"f\\\">\u001b[39m\n\u001b[31m        <label>Status Reported <span class=\\\"req\\\">*</span></label>\u001b[39m\n\u001b[31m        <select id=\\\"mtStatus\\\">\u001b[39m\n\u001b[31m          <option value=\\\"\\\">Select...</option>\u001b[39m\n\u001b[31m          <option value=\\\"delivered\\\">✅ Delivered</option>\u001b[39m\n\u001b[31m          <option value=\\\"not received\\\">❌ Not Received</option>\u001b[39m\n\u001b[31m          <option value=\\\"in transit\\\">🚚 In Transit / On the way</option>\u001b[39m\n\u001b[31m          <option value=\\\"will deliver tomorrow\\\">📅 Will Deliver Tomorrow</option>\u001b[39m\n\u001b[31m          <option value=\\\"refused\\\">🚫 Refused / Returned</option>\u001b[39m\n\u001b[31m          <option value=\\\"partial\\\">📦 Partial Delivery</option>\u001b[39m\n\u001b[31m        </select>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      <div class=\\\"f\\\">\u001b[39m\n\u001b[31m        <label>Called By (Staff) <span class=\\\"req\\\">*</span></label>\u001b[39m\n\u001b[31m        <input type=\\\"text\\\" id=\\\"mtCalledBy\\\" placeholder=\\\"Your name\\\" list=\\\"staffList\\\">\u001b[39m\n\u001b[31m        <datalist id=\\\"staffList\\\">\u001b[39m\n\u001b[31m          <option>Mukesh</option><option>Sachin</option><option>Shambhu</option>\u001b[39m\n\u001b[31m          <option>Umashankar</option><option>Narender</option><option>Rakesh</option>\u001b[39m\n\u001b[31m          <option>Gurpreet</option><option>Ashok</option>\u001b[39m\n\u001b[31m        </datalist>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m    <div class=\\\"row2\\\">\u001b[39m\n\u001b[31m      <div class=\\\"f\\\">\u001b[39m\n\u001b[31m        <label>Contact Number Used</label>\u001b[39m\n\u001b[31m        <input type=\\\"tel\\\" id=\\\"mtContactUsed\\\" placeholder=\\\"Mobile number called\\\">\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      <div class=\\\"f\\\">\u001b[39m\n\u001b[31m        <label>Person Who Confirmed</label>\u001b[39m\n\u001b[31m        <input type=\\\"text\\\" id=\\\"mtContactPerson\\\" placeholder=\\\"Name of person at store\\\">\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m    <div class=\\\"f\\\">\u001b[39m\n\u001b[31m      <label>Remarks</label>\u001b[39m\n\u001b[31m      <textarea id=\\\"mtRemarks\\\" placeholder=\\\"Any notes, delivery details, issues...\\\" style=\\\"min-height:60px;\\\"></textarea>\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m    <div class=\\\"f\\\">\u001b[39m\n\u001b[31m      <label>Call Date &amp; Time</label>\u001b[39m\n\u001b[31m      <input type=\\\"datetime-local\\\" id=\\\"mtCallTime\\\">\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m    <div class=\\\"btn-row\\\">\u001b[39m\n\u001b[31m      <button class=\\\"btn btn-ghost\\\" onclick=\\\"closeOverlay('manualTrackOverlay')\\\">Cancel</button>\u001b[39m\n\u001b[31m      <button class=\\\"btn btn-orange\\\" onclick=\\\"submitManualTracking()\\\">💾 Log &amp; Save</button>\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m  </div>\u001b[39m\n\u001b[31m</div>·\u001b[39m\n\u001b[31m<!-- ════ POD UPLOAD OVERLAY ════ -->\u001b[39m\n\u001b[31m<div class=\\\"overlay\\\" id=\\\"podOverlay\\\" onclick=\\\"closeOverlay('podOverlay')\\\" data-docket=\\\"\\\" data-dispatch-id=\\\"\\\">\u001b[39m\n\u001b[31m  <div class=\\\"sheet\\\" onclick=\\\"event.stopPropagation()\\\">\u001b[39m\n\u001b[31m    <div class=\\\"sheet-handle\\\"></div>\u001b[39m\n\u001b[31m    <div class=\\\"sheet-title\\\">📸 Upload Proof of Delivery</div>\u001b[39m\n\u001b[31m    <div class=\\\"f\\\">\u001b[39m\n\u001b[31m      <label>POD / Delivery Slip Photo</label>\u001b[39m\n\u001b[31m      <input type=\\\"file\\\" id=\\\"podFileInput\\\" accept=\\\"image/*\\\" capture=\\\"environment\\\"\u001b[39m\n\u001b[31m        onchange=\\\"previewPOD(this,'pod')\\\" style=\\\"font-size:.8rem;color:var(--t2);\\\">\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m    <div class=\\\"f\\\">\u001b[39m\n\u001b[31m      <label>Recipient / Receiver Photo (optional)</label>\u001b[39m\n\u001b[31m      <input type=\\\"file\\\" id=\\\"recipientFileInput\\\" accept=\\\"image/*\\\" capture=\\\"environment\\\"\u001b[39m\n\u001b[31m        onchange=\\\"previewPOD(this,'recip')\\\" style=\\\"font-size:.8rem;color:var(--t2);\\\">\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m    <div id=\\\"podPreview\\\" style=\\\"display:flex;gap:8px;flex-wrap:wrap;margin-bottom:12px;\\\"></div>\u001b[39m\n\u001b[31m    <div class=\\\"btn-row\\\">\u001b[39m\n\u001b[31m      <button class=\\\"btn btn-ghost\\\" onclick=\\\"closeOverlay('podOverlay')\\\">Cancel</button>\u001b[39m\n\u001b[31m      <button class=\\\"btn btn-orange\\\" onclick=\\\"uploadPODFiles()\\\">📤 Upload & Mark Delivered</button>\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m  </div>\u001b[39m\n\u001b[31m</div>·\u001b[39m\n\u001b[31m<!-- ════ ADMIN LOGIN OVERLAY ════ -->\u001b[39m\n\u001b[31m<div class=\\\"overlay\\\" id=\\\"adminLoginOverlay\\\" onclick=\\\"closeOverlay('adminLoginOverlay')\\\">\u001b[39m\n\u001b[31m  <div class=\\\"sheet\\\" onclick=\\\"event.stopPropagation()\\\" style=\\\"max-height:320px;\\\">\u001b[39m\n\u001b[31m    <div class=\\\"sheet-handle\\\"></div>\u001b[39m\n\u001b[31m    <div class=\\\"sheet-title\\\">🔐 Admin Access</div>\u001b[39m\n\u001b[31m    <div style=\\\"font-size:.78rem;color:var(--t2);margin-bottom:14px;\\\">Config settings require admin password.</div>\u001b[39m\n\u001b[31m    <div class=\\\"f\\\">\u001b[39m\n\u001b[31m      <label>Password</label>\u001b[39m\n\u001b[31m      <input type=\\\"password\\\" id=\\\"adminPwdInput\\\" placeholder=\\\"Enter admin password\\\"\u001b[39m\n\u001b[31m        onkeydown=\\\"if(event.key==='Enter')submitAdminLogin()\\\">\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m    <div class=\\\"btn-row\\\" style=\\\"margin-top:4px;\\\">\u001b[39m\n\u001b[31m      <button class=\\\"btn btn-ghost\\\" onclick=\\\"closeOverlay('adminLoginOverlay')\\\">Cancel</button>\u001b[39m\n\u001b[31m      <button class=\\\"btn btn-orange\\\" onclick=\\\"submitAdminLogin()\\\">🔓 Login</button>\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m  </div>\u001b[39m\n\u001b[31m</div>·\u001b[39m\n\u001b[31m<!-- ════ TOAST ════ -->\u001b[39m\n\u001b[31m<div class=\\\"toast\\\" id=\\\"toast\\\"></div>·\u001b[39m\n\u001b[31m<!-- ════ SCRIPTS ════ -->\u001b[39m\n\u001b[31m<!-- SheetJS for Excel/CSV parsing -->\u001b[39m\n\u001b[31m<script src=\\\"https://cdnjs.cloudflare.com/ajax/libs/xlsx/0.18.5/xlsx.full.min.js\\\"></script>·\u001b[39m\n\u001b[31m<script type=\\\"module\\\">\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31m// OCR — Transformers.js\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31mimport { pipeline, env } from 'https://cdn.jsdelivr.net/npm/@huggingface/transformers@3.5.2/dist/transformers.min.js';·\u001b[39m\n\u001b[31m// Cache models in browser\u001b[39m\n\u001b[31menv.allowLocalModels = false;\u001b[39m\n\u001b[31menv.useBrowserCache = true;·\u001b[39m\n\u001b[31mlet ocrPipeline = null;\u001b[39m\n\u001b[31mlet ocrLoading = false;·\u001b[39m\n\u001b[31mwindow.initOCR = async function() {\u001b[39m\n\u001b[31m  if (ocrPipeline || ocrLoading) return;\u001b[39m\n\u001b[31m  ocrLoading = true;\u001b[39m\n\u001b[31m  showAIProgress('Loading OCR model (first time ~120MB)...', 5);\u001b[39m\n\u001b[31m  try {\u001b[39m\n\u001b[31m    ocrPipeline = await pipeline('image-to-text', 'Xenova/trocr-base-printed', {\u001b[39m\n\u001b[31m      progress_callback: (p) => {\u001b[39m\n\u001b[31m        if (p.progress) {\u001b[39m\n\u001b[31m          showAIProgress(`Loading: ${p.file || 'model'} (${Math.round(p.progress)}%)`, p.progress);\u001b[39m\n\u001b[31m        }\u001b[39m\n\u001b[31m      }\u001b[39m\n\u001b[31m    });\u001b[39m\n\u001b[31m    showAIProgress('OCR model ready ✓', 100);\u001b[39m\n\u001b[31m    setTimeout(() => hideAIProgress(), 1500);\u001b[39m\n\u001b[31m    showToast('OCR ready ✓', 'ok');\u001b[39m\n\u001b[31m  } catch(e) {\u001b[39m\n\u001b[31m    console.warn('Transformers.js OCR failed:', e);\u001b[39m\n\u001b[31m    hideAIProgress();\u001b[39m\n\u001b[31m    showToast('OCR model load failed — will use Ollama fallback', 'info');\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m  ocrLoading = false;\u001b[39m\n\u001b[31m};·\u001b[39m\n\u001b[31mwindow.runOCROnImage = async function(imageData, filename) {\u001b[39m\n\u001b[31m  const cfg = window.appConfig;·\u001b[39m\n\u001b[31m  // Strategy 1: Transformers.js\u001b[39m\n\u001b[31m  if ((cfg.ocr_mode === 'transformers' || cfg.ocr_mode === 'both') && ocrPipeline) {\u001b[39m\n\u001b[31m    try {\u001b[39m\n\u001b[31m      const result = await ocrPipeline(imageData);\u001b[39m\n\u001b[31m      const text = result[0]?.generated_text || '';\u001b[39m\n\u001b[31m      return extractDocketFromText(text, 'transformers');\u001b[39m\n\u001b[31m    } catch(e) { /* fall through */ }\u001b[39m\n\u001b[31m  }·\u001b[39m\n\u001b[31m  // Strategy 2: Dispatch AI proxy → Ollama LLaVA (server-side, avoids CORS + port issues)\u001b[39m\n\u001b[31m  if (cfg.ocr_mode === 'ollama' || cfg.ocr_mode === 'both') {\u001b[39m\n\u001b[31m    try {\u001b[39m\n\u001b[31m      const resp = await fetch('/dispatch-ai/ocr', {\u001b[39m\n\u001b[31m        method: 'POST',\u001b[39m\n\u001b[31m        headers: { 'Content-Type': 'application/json' },\u001b[39m\n\u001b[31m        body: JSON.stringify({ imageBase64: imageData }),\u001b[39m\n\u001b[31m        signal: AbortSignal.timeout(180000)\u001b[39m\n\u001b[31m      });\u001b[39m\n\u001b[31m      const data = await resp.json();\u001b[39m\n\u001b[31m      if (data.success && data.docket) {\u001b[39m\n\u001b[31m        return { docket: data.docket, confidence: data.confidence, source: 'ollama' };\u001b[39m\n\u001b[31m      }\u001b[39m\n\u001b[31m    } catch(e) { /* fall through */ }\u001b[39m\n\u001b[31m  }·\u001b[39m\n\u001b[31m  return { docket: '', confidence: 0, source: 'failed' };\u001b[39m\n\u001b[31m};·\u001b[39m\n\u001b[31mfunction extractDocketFromText(text, source) {\u001b[39m\n\u001b[31m  // Pattern: typical courier docket = 8-15 digit numbers, sometimes with letters\u001b[39m\n\u001b[31m  const patterns = [\u001b[39m\n\u001b[31m    /\\\\b([A-Z]{0,3}\\\\d{8,15}[A-Z]{0,3})\\\\b/g,   // alphanumeric tracking\u001b[39m\n\u001b[31m    /\\\\b(\\\\d{9,15})\\\\b/g,                          // pure numeric\u001b[39m\n\u001b[31m    /\\\\b([A-Z]{2,4}\\\\d{6,12})\\\\b/g,               // letter prefix\u001b[39m\n\u001b[31m  ];\u001b[39m\n\u001b[31m  const candidates = [];\u001b[39m\n\u001b[31m  for (const pat of patterns) {\u001b[39m\n\u001b[31m    let m;\u001b[39m\n\u001b[31m    while ((m = pat.exec(text)) !== null) {\u001b[39m\n\u001b[31m      if (!candidates.includes(m[1])) candidates.push(m[1]);\u001b[39m\n\u001b[31m    }\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m  if (candidates.length === 0) return { docket: text.trim().slice(0, 20), confidence: 0.3, source };\u001b[39m\n\u001b[31m  // Pick longest candidate (usually most complete docket)\u001b[39m\n\u001b[31m  const best = candidates.sort((a, b) => b.length - a.length)[0];\u001b[39m\n\u001b[31m  const confidence = candidates.length === 1 ? 0.85 : 0.7;\u001b[39m\n\u001b[31m  return { docket: best, confidence, source };\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// Expose for non-module code\u001b[39m\n\u001b[31mwindow._ocrReady = () => ocrPipeline !== null;\u001b[39m\n\u001b[31m</script>·\u001b[39m\n\u001b[31m<script>\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31m// STATE & STORAGE\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31mlet dispatches = [];\u001b[39m\n\u001b[31mlet stores = [];\u001b[39m\n\u001b[31mlet slipOCRData = [];\u001b[39m\n\u001b[31mlet dashFilter = 'all';·\u001b[39m\n\u001b[31m// Only sheets_url is persisted locally — all other config comes from the sheet\u001b[39m\n\u001b[31mlet _trackingInProgress = false;\u001b[39m\n\u001b[31mwindow.appConfig = {\u001b[39m\n\u001b[31m  sheets_url:      'https://script.google.com/macros/s/AKfycbys8HrToDv2u_dRvSFnNqmO9bnyAO1s8oSprUydpVQnNQutq4tuUY71cVCH24urbvM9/exec',\u001b[39m\n\u001b[31m  webhook_url:     '',\u001b[39m\n\u001b[31m  proxy_url:       '',\u001b[39m\n\u001b[31m  ollama_url:      'http://srv1111289.hstgr.cloud:11434',\u001b[39m\n\u001b[31m  ocr_mode:        'both',\u001b[39m\n\u001b[31m  company:         '360 Degree Logical Marketing',\u001b[39m\n\u001b[31m  city:            'Chandigarh',\u001b[39m\n\u001b[31m  auto_log:        'true',\u001b[39m\n\u001b[31m  refresh_on_load: 'false',\u001b[39m\n\u001b[31m};·\u001b[39m\n\u001b[31mconst COURIER_MASTER = {\u001b[39m\n\u001b[31m  'SkyKing': {\u001b[39m\n\u001b[31m    trackUrl: 'https://skyking.co/track?cno={docket}',\u001b[39m\n\u001b[31m    trackBulkUrl: 'https://skyking.co/track',\u001b[39m\n\u001b[31m    apiProxy: '/track-proxy/skyking?cno={docket}',\u001b[39m\n\u001b[31m    maxBulk: 10\u001b[39m\n\u001b[31m  },\u001b[39m\n\u001b[31m  'DTDC': {\u001b[39m\n\u001b[31m    trackUrl: 'https://www.dtdc.in/tracking.asp',\u001b[39m\n\u001b[31m    queryParam: 'trkId',\u001b[39m\n\u001b[31m    maxBulk: 1\u001b[39m\n\u001b[31m  },\u001b[39m\n\u001b[31m  'Delhivery': {\u001b[39m\n\u001b[31m    trackUrl: 'https://www.delhivery.com/track/package/{docket}',\u001b[39m\n\u001b[31m    maxBulk: 1\u001b[39m\n\u001b[31m  },\u001b[39m\n\u001b[31m  'Blue Dart': {\u001b[39m\n\u001b[31m    trackUrl: 'https://www.bluedart.com/tracking',\u001b[39m\n\u001b[31m    maxBulk: 1\u001b[39m\n\u001b[31m  },\u001b[39m\n\u001b[31m  'FedEx': {\u001b[39m\n\u001b[31m    trackUrl: 'https://www.fedex.com/fedextrack/?trknbr={docket}',\u001b[39m\n\u001b[31m    maxBulk: 10\u001b[39m\n\u001b[31m  },\u001b[39m\n\u001b[31m  'Ecom Express': {\u001b[39m\n\u001b[31m    trackUrl: 'https://ecomexpress.in/tracking/?awb_field={docket}',\u001b[39m\n\u001b[31m    maxBulk: 1\u001b[39m\n\u001b[31m  },\u001b[39m\n\u001b[31m  'India Post': {\u001b[39m\n\u001b[31m    trackUrl: 'https://www.indiapost.gov.in/VAS/Pages/trackconsignment.aspx',\u001b[39m\n\u001b[31m    maxBulk: 1\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m};·\u001b[39m\n\u001b[31mfunction loadData() {\u001b[39m\n\u001b[31m  try {\u001b[39m\n\u001b[31m    dispatches = JSON.parse(localStorage.getItem('360lm_disp_v2') || '[]');\u001b[39m\n\u001b[31m    const saved = JSON.parse(localStorage.getItem('360lm_disp_sheets_url') || '{}');\u001b[39m\n\u001b[31m    if (saved.sheets_url) window.appConfig.sheets_url = saved.sheets_url;\u001b[39m\n\u001b[31m  } catch(e) {}\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31mfunction saveData() {\u001b[39m\n\u001b[31m  try {\u001b[39m\n\u001b[31m    localStorage.setItem('360lm_disp_v2', JSON.stringify(dispatches));\u001b[39m\n\u001b[31m  } catch(e) {\u001b[39m\n\u001b[31m    if (e.name === 'QuotaExceededError' || e.code === 22) {\u001b[39m\n\u001b[31m      showToast('Storage full — old dispatches cleared. Data is safe in Postgres.', 'err');\u001b[39m\n\u001b[31m      // Drop delivered dispatches older than 30 days to free space\u001b[39m\n\u001b[31m      const cutoff = Date.now() - 30 * 24 * 3600 * 1000;\u001b[39m\n\u001b[31m      dispatches = dispatches.filter(d =>\u001b[39m\n\u001b[31m        d.status !== 'draft' && (\u001b[39m\n\u001b[31m          d.status !== 'active' ||\u001b[39m\n\u001b[31m          new Date(d.createdAt || 0).getTime() > cutoff\u001b[39m\n\u001b[31m        ) ? true :\u001b[39m\n\u001b[31m        d.status === 'draft' || new Date(d.createdAt || 0).getTime() > cutoff\u001b[39m\n\u001b[31m      );\u001b[39m\n\u001b[31m      try { localStorage.setItem('360lm_disp_v2', JSON.stringify(dispatches)); } catch(_) {}\u001b[39m\n\u001b[31m    }\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// ── Postgres DB sync ──────────────────────────────────────────────────────\u001b[39m\n\u001b[31mconst _DB        = '/db';\u001b[39m\n\u001b[31m// PostgREST v12: Accept-Profile for reads, Content-Profile for writes\u001b[39m\n\u001b[31mconst _DHDR_READ  = { 'Accept-Profile': 'dispatch' };\u001b[39m\n\u001b[31mconst _DHDR_WRITE = { 'Content-Type': 'application/json', 'Content-Profile': 'dispatch', 'Accept-Profile': 'dispatch' };\u001b[39m\n\u001b[31mconst _DHDR_UPSERT = { ..._DHDR_WRITE, 'Prefer': 'resolution=merge-duplicates,return=minimal' };·\u001b[39m\n\u001b[31mfunction _dispRow(d) {\u001b[39m\n\u001b[31m  return {\u001b[39m\n\u001b[31m    dispatch_id:    d.id,\u001b[39m\n\u001b[31m    client:         d.client    || '',\u001b[39m\n\u001b[31m    job_ref:        d.jobref    || null,\u001b[39m\n\u001b[31m    courier:        d.courier   || '',\u001b[39m\n\u001b[31m    dispatch_date:  d.dispatchDate || null,\u001b[39m\n\u001b[31m    status:         d.status    || 'active',\u001b[39m\n\u001b[31m    total_count:    (d.consignees || []).length,\u001b[39m\n\u001b[31m    docketed_count: (d.consignees || []).filter(c => c.docket).length,\u001b[39m\n\u001b[31m    remarks:        d.remarks   || null,\u001b[39m\n\u001b[31m    created_by:     window._hubUser?.id || 'unknown',\u001b[39m\n\u001b[31m  };\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction _consRow(c, dispatchId, idx) {\u001b[39m\n\u001b[31m  return {\u001b[39m\n\u001b[31m    cons_id:            `${dispatchId}-${idx}`,\u001b[39m\n\u001b[31m    dispatch_id:        dispatchId,\u001b[39m\n\u001b[31m    seq_no:             idx,\u001b[39m\n\u001b[31m    name:               c.name          || '',\u001b[39m\n\u001b[31m    city:               c.city          || null,\u001b[39m\n\u001b[31m    address:            c.address       || null,\u001b[39m\n\u001b[31m    phone:              c.phone         || null,\u001b[39m\n\u001b[31m    pincode:            c.pincode       || null,\u001b[39m\n\u001b[31m    material:           c.material      || null,\u001b[39m\n\u001b[31m    qty:                c.qty           || null,\u001b[39m\n\u001b[31m    docket:             c.docket        || null,\u001b[39m\n\u001b[31m    status:             c.status        || 'pending',\u001b[39m\n\u001b[31m    last_event:         c.lastEvent     || null,\u001b[39m\n\u001b[31m    last_scan_location: c.lastScanLocation || null,\u001b[39m\n\u001b[31m    last_tracked_at:    c.lastTrackedAt || null,\u001b[39m\n\u001b[31m    manual_status:      c.manualStatus  || null,\u001b[39m\n\u001b[31m    manual_delivered:   !!c.manualDelivered,\u001b[39m\n\u001b[31m    manual_called_by:   c.manualCalledBy || null,\u001b[39m\n\u001b[31m    manual_remarks:     c.manualRemarks || null,\u001b[39m\n\u001b[31m    pod_uploaded:       !!c.podUploaded,\u001b[39m\n\u001b[31m  };\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31masync function syncDispatchToDB(d) {\u001b[39m\n\u001b[31m  if (!d || d.status === 'draft') return; // never sync drafts\u001b[39m\n\u001b[31m  try {\u001b[39m\n\u001b[31m    await fetch(`${_DB}/dispatches`, {\u001b[39m\n\u001b[31m      method: 'POST', headers: _DHDR_UPSERT,\u001b[39m\n\u001b[31m      body: JSON.stringify(_dispRow(d)),\u001b[39m\n\u001b[31m    });\u001b[39m\n\u001b[31m    const rows = (d.consignees || []).map((c, i) => _consRow(c, d.id, i));\u001b[39m\n\u001b[31m    if (rows.length) {\u001b[39m\n\u001b[31m      await fetch(`${_DB}/consignees`, {\u001b[39m\n\u001b[31m        method: 'POST', headers: _DHDR_UPSERT,\u001b[39m\n\u001b[31m        body: JSON.stringify(rows),\u001b[39m\n\u001b[31m      });\u001b[39m\n\u001b[31m    }\u001b[39m\n\u001b[31m  } catch(e) { console.warn('[dispatch-db] sync error:', e.message); }\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31masync function syncConsigneeToDB(c, dispatchId, idx) {\u001b[39m\n\u001b[31m  try {\u001b[39m\n\u001b[31m    await fetch(`${_DB}/consignees`, {\u001b[39m\n\u001b[31m      method: 'POST', headers: _DHDR_UPSERT,\u001b[39m\n\u001b[31m      body: JSON.stringify(_consRow(c, dispatchId, idx)),\u001b[39m\n\u001b[31m    });\u001b[39m\n\u001b[31m    // Log tracking event\u001b[39m\n\u001b[31m    if (c.lastEvent && c.status) {\u001b[39m\n\u001b[31m      fetch(`${_DB}/tracking_events`, {\u001b[39m\n\u001b[31m        method: 'POST', headers: _DHDR_WRITE,\u001b[39m\n\u001b[31m        body: JSON.stringify({\u001b[39m\n\u001b[31m          docket: c.docket, dispatch_id: dispatchId,\u001b[39m\n\u001b[31m          cons_id: `${dispatchId}-${idx}`,\u001b[39m\n\u001b[31m          status: c.status, event_text: c.lastEvent,\u001b[39m\n\u001b[31m          scan_location: c.lastScanLocation || null,\u001b[39m\n\u001b[31m        }),\u001b[39m\n\u001b[31m      }).catch(() => {});\u001b[39m\n\u001b[31m    }\u001b[39m\n\u001b[31m  } catch(e) { console.warn('[dispatch-db] consignee sync error:', e.message); }\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31masync function loadFromDB() {\u001b[39m\n\u001b[31m  try {\u001b[39m\n\u001b[31m    const [dResp, cResp] = await Promise.all([\u001b[39m\n\u001b[31m      fetch(`${_DB}/dispatches?order=created_at.desc&limit=100`, { headers: _DHDR_READ }),\u001b[39m\n\u001b[31m      fetch(`${_DB}/consignees?order=dispatch_id,seq_no`, { headers: _DHDR_READ }),\u001b[39m\n\u001b[31m    ]);\u001b[39m\n\u001b[31m    if (!dResp.ok) return;\u001b[39m\n\u001b[31m    const dbDispatches = await dResp.json();\u001b[39m\n\u001b[31m    const dbConsignees = cResp.ok ? await cResp.json() : [];\u001b[39m\n\u001b[31m    if (!dbDispatches.length) return;·\u001b[39m\n\u001b[31m    const dbIds = new Set(dbDispatches.map(d => d.dispatch_id));\u001b[39m\n\u001b[31m    const localOnly = dispatches.filter(d => !dbIds.has(d.id));·\u001b[39m\n\u001b[31m    const fromDB = dbDispatches.map(d => {\u001b[39m\n\u001b[31m      const cons = dbConsignees.filter(c => c.dispatch_id === d.dispatch_id);\u001b[39m\n\u001b[31m      return {\u001b[39m\n\u001b[31m        id:           d.dispatch_id,\u001b[39m\n\u001b[31m        client:       d.client,\u001b[39m\n\u001b[31m        jobref:       d.job_ref,\u001b[39m\n\u001b[31m        courier:      d.courier,\u001b[39m\n\u001b[31m        dispatchDate: d.dispatch_date,\u001b[39m\n\u001b[31m        status:       d.status,\u001b[39m\n\u001b[31m        remarks:      d.remarks,\u001b[39m\n\u001b[31m        createdAt:    d.created_at,\u001b[39m\n\u001b[31m        consignees:   cons.map(c => ({\u001b[39m\n\u001b[31m          name:              c.name,\u001b[39m\n\u001b[31m          city:              c.city,\u001b[39m\n\u001b[31m          address:           c.address,\u001b[39m\n\u001b[31m          phone:             c.phone,\u001b[39m\n\u001b[31m          pincode:           c.pincode,\u001b[39m\n\u001b[31m          material:          c.material,\u001b[39m\n\u001b[31m          qty:               c.qty,\u001b[39m\n\u001b[31m          docket:            c.docket,\u001b[39m\n\u001b[31m          status:            c.status,\u001b[39m\n\u001b[31m          lastEvent:         c.last_event,\u001b[39m\n\u001b[31m          lastScanLocation:  c.last_scan_location,\u001b[39m\n\u001b[31m          lastTrackedAt:     c.last_tracked_at,\u001b[39m\n\u001b[31m          manualStatus:      c.manual_status,\u001b[39m\n\u001b[31m          manualDelivered:   c.manual_delivered,\u001b[39m\n\u001b[31m          manualCalledBy:    c.manual_called_by,\u001b[39m\n\u001b[31m          manualRemarks:     c.manual_remarks,\u001b[39m\n\u001b[31m          podUploaded:       c.pod_uploaded,\u001b[39m\n\u001b[31m          _dbIdx:            c.seq_no,\u001b[39m\n\u001b[31m        })),\u001b[39m\n\u001b[31m      };\u001b[39m\n\u001b[31m    });·\u001b[39m\n\u001b[31m    dispatches = [...fromDB, ...localOnly];\u001b[39m\n\u001b[31m    saveData();\u001b[39m\n\u001b[31m    renderDashboard();\u001b[39m\n\u001b[31m    updateHistCount();\u001b[39m\n\u001b[31m  } catch(e) { console.warn('[dispatch-db] load error:', e.message); }\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31mfunction saveSheetsUrlLocal() {\u001b[39m\n\u001b[31m  localStorage.setItem('360lm_disp_sheets_url', JSON.stringify({ sheets_url: window.appConfig.sheets_url }));\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m/**\u001b[39m\n\u001b[31m * Fetch config.json from the VPS server (same origin).\u001b[39m\n\u001b[31m * This file contains the sheets_url — no manual entry needed.\u001b[39m\n\u001b[31m * Falls back to localStorage cache if fetch fails (offline).\u001b[39m\n\u001b[31m */\u001b[39m\n\u001b[31masync function loadServerConfig() {\u001b[39m\n\u001b[31m  try {\u001b[39m\n\u001b[31m    const resp = await fetch('./config.json', { cache: 'no-store' });\u001b[39m\n\u001b[31m    if (resp.ok) {\u001b[39m\n\u001b[31m      const cfg = await resp.json();\u001b[39m\n\u001b[31m      if (cfg.sheets_url) {\u001b[39m\n\u001b[31m        window.appConfig.sheets_url = cfg.sheets_url;\u001b[39m\n\u001b[31m        saveSheetsUrlLocal(); // cache it for offline use\u001b[39m\n\u001b[31m        document.getElementById('cfg_sheets_url').value = cfg.sheets_url;\u001b[39m\n\u001b[31m        return true;\u001b[39m\n\u001b[31m      }\u001b[39m\n\u001b[31m    }\u001b[39m\n\u001b[31m  } catch(e) {\u001b[39m\n\u001b[31m    console.log('config.json fetch failed, using cached URL:', e.message);\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m  return false;\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31m// INIT\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31m// ── Hub auth gate ─────────────────────────────────────────────────────────\u001b[39m\n\u001b[31mwindow._hubUser = null;\u001b[39m\n\u001b[31m(function() {\u001b[39m\n\u001b[31m  try {\u001b[39m\n\u001b[31m    const hubLogin = '/hub/?next=' + encodeURIComponent(location.pathname + location.search);\u001b[39m\n\u001b[31m    const s = JSON.parse(localStorage.getItem('lm360-session') || 'null');\u001b[39m\n\u001b[31m    if (!s || !s.empId || !s.name) { window.location.replace(hubLogin); return; }\u001b[39m\n\u001b[31m    if (Date.now() - new Date(s.loginAt).getTime() > 12 * 3600 * 1000) {\u001b[39m\n\u001b[31m      window.location.replace(hubLogin); return;\u001b[39m\n\u001b[31m    }\u001b[39m\n\u001b[31m    window._hubUser = { id: s.empId, name: s.name, role: s.role };\u001b[39m\n\u001b[31m  } catch(_) { window.location.replace('/hub/?next=' + encodeURIComponent(location.pathname + location.search)); }\u001b[39m\n\u001b[31m})();·\u001b[39m\n\u001b[31mwindow.addEventListener('load', async () => {\u001b[39m\n\u001b[31m  if (!window._hubUser) return; // should never reach here — gate redirected already\u001b[39m\n\u001b[31m  setTimeout(() => {\u001b[39m\n\u001b[31m    document.getElementById('splash').style.opacity = '0';\u001b[39m\n\u001b[31m    setTimeout(() => {\u001b[39m\n\u001b[31m      document.getElementById('splash').style.display = 'none';\u001b[39m\n\u001b[31m      document.getElementById('app').style.display = 'flex';\u001b[39m\n\u001b[31m    }, 450);\u001b[39m\n\u001b[31m  }, 1400);\u001b[39m\n\u001b[31m  loadData();\u001b[39m\n\u001b[31m  // Auto-authenticate config tab for admin users (no password needed)\u001b[39m\n\u001b[31m  if (['harish', 'pramod'].includes(window._hubUser?.id)) _adminAuthed = true;\u001b[39m\n\u001b[31m  setToday();\u001b[39m\n\u001b[31m  addConsignee();\u001b[39m\n\u001b[31m  updateHistCount();\u001b[39m\n\u001b[31m  setInterval(updateNetDot, 5000);\u001b[39m\n\u001b[31m  updateNetDot();\u001b[39m\n\u001b[31m  checkShareTarget();\u001b[39m\n\u001b[31m  setupDragDrop();·\u001b[39m\n\u001b[31m  // Step 1: fetch sheets_url from VPS config.json (auto, no user input)\u001b[39m\n\u001b[31m  await loadServerConfig();·\u001b[39m\n\u001b[31m  // Step 2: populate config tab field\u001b[39m\n\u001b[31m  document.getElementById('cfg_sheets_url').value = window.appConfig.sheets_url || '';·\u001b[39m\n\u001b[31m  // Step 3: load dispatches from Postgres in background (merge with local)\u001b[39m\n\u001b[31m  loadFromDB(); // non-blocking — updates UI when data arrives·\u001b[39m\n\u001b[31m  // Step 4: sync full config from Google Sheet silently\u001b[39m\n\u001b[31m  if (window.appConfig.sheets_url) {\u001b[39m\n\u001b[31m    syncFromSheet(true); // silent on startup\u001b[39m\n\u001b[31m  } else {\u001b[39m\n\u001b[31m    // Only show config tab if no URL found anywhere\u001b[39m\n\u001b[31m    showPane('config');\u001b[39m\n\u001b[31m    showToast('No config.json found on server', 'err');\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m  // Pre-warm OCR after delay\u001b[39m\n\u001b[31m  setTimeout(() => { if (typeof window.initOCR === 'function') window.initOCR(); }, 4000);\u001b[39m\n\u001b[31m});·\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31m// WEB SHARE TARGET\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31masync function checkShareTarget() {\u001b[39m\n\u001b[31m  // Check URL params for share target\u001b[39m\n\u001b[31m  const params = new URLSearchParams(window.location.search);\u001b[39m\n\u001b[31m  const action = params.get('action');\u001b[39m\n\u001b[31m  if (action !== 'share') return;·\u001b[39m\n\u001b[31m  // Check for shared files via Share Target API\u001b[39m\n\u001b[31m  if ('launchQueue' in window) {\u001b[39m\n\u001b[31m    window.launchQueue.setConsumer(async launchParams => {\u001b[39m\n\u001b[31m      if (!launchParams.files || launchParams.files.length === 0) return;\u001b[39m\n\u001b[31m      const files = await Promise.all(launchParams.files.map(f => f.getFile()));\u001b[39m\n\u001b[31m      const imageFiles = files.filter(f => f.type.startsWith('image/'));\u001b[39m\n\u001b[31m      if (imageFiles.length > 0) {\u001b[39m\n\u001b[31m        document.getElementById('shareTargetInfo').style.display = 'flex';\u001b[39m\n\u001b[31m        document.getElementById('sharedCount').textContent = imageFiles.length;\u001b[39m\n\u001b[31m        document.getElementById('shareBanner').classList.add('show');\u001b[39m\n\u001b[31m        setTimeout(() => document.getElementById('shareBanner').classList.remove('show'), 4000);\u001b[39m\n\u001b[31m        showPane('new');\u001b[39m\n\u001b[31m        await handleSlipFiles(imageFiles);\u001b[39m\n\u001b[31m      }\u001b[39m\n\u001b[31m    });\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction dismissShare() {\u001b[39m\n\u001b[31m  document.getElementById('shareBanner').classList.remove('show');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31m// DRAG & DROP\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31mfunction setupDragDrop() {\u001b[39m\n\u001b[31m  const zone = document.getElementById('slipZone');\u001b[39m\n\u001b[31m  zone.addEventListener('dragover', e => { e.preventDefault(); zone.classList.add('drag'); });\u001b[39m\n\u001b[31m  zone.addEventListener('dragleave', () => zone.classList.remove('drag'));\u001b[39m\n\u001b[31m  zone.addEventListener('drop', e => {\u001b[39m\n\u001b[31m    e.preventDefault();\u001b[39m\n\u001b[31m    zone.classList.remove('drag');\u001b[39m\n\u001b[31m    const files = Array.from(e.dataTransfer.files).filter(f => f.type.startsWith('image/'));\u001b[39m\n\u001b[31m    if (files.length) handleSlipFiles(files);\u001b[39m\n\u001b[31m  });\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31m// SLIP OCR HANDLER\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31masync function handleSlipFiles(files) {\u001b[39m\n\u001b[31m  const fileArr = Array.from(files);\u001b[39m\n\u001b[31m  if (!fileArr.length) return;\u001b[39m\n\u001b[31m  // If dockets already mapped from store list — allow upload but skip OCR extraction\u001b[39m\n\u001b[31m  const skipOCR = shouldSkipOCR();\u001b[39m\n\u001b[31m  if (skipOCR) {\u001b[39m\n\u001b[31m    showToast('Dockets already mapped — slips saved for reference only', 'info');\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m  showOCRWorking(true, `Processing ${fileArr.length} slip(s)...`);·\u001b[39m\n\u001b[31m  for (let i = 0; i < fileArr.length; i++) {\u001b[39m\n\u001b[31m    const file = fileArr[i];\u001b[39m\n\u001b[31m    showOCRWorking(true, `OCR: ${i+1}/${fileArr.length} — ${file.name}`);\u001b[39m\n\u001b[31m    const imageData = await readFileAsDataURL(file);\u001b[39m\n\u001b[31m    let result = { docket: '', confidence: 0, source: 'manual' };\u001b[39m\n\u001b[31m    if (!skipOCR) {\u001b[39m\n\u001b[31m      try {\u001b[39m\n\u001b[31m        result = await window.runOCROnImage(imageData, file.name);\u001b[39m\n\u001b[31m      } catch(e) {}\u001b[39m\n\u001b[31m    } else {\u001b[39m\n\u001b[31m      result = { docket: '', confidence: 1, source: 'skipped — docket pre-mapped' };\u001b[39m\n\u001b[31m    }\u001b[39m\n\u001b[31m    slipOCRData.push({ file, imageData, filename: file.name, ...result });\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m  showOCRWorking(false);\u001b[39m\n\u001b[31m  renderOCRResults();\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction readFileAsDataURL(file) {\u001b[39m\n\u001b[31m  return new Promise((res, rej) => {\u001b[39m\n\u001b[31m    const r = new FileReader();\u001b[39m\n\u001b[31m    r.onload = e => res(e.target.result);\u001b[39m\n\u001b[31m    r.onerror = rej;\u001b[39m\n\u001b[31m    r.readAsDataURL(file);\u001b[39m\n\u001b[31m  });\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction renderOCRResults() {\u001b[39m\n\u001b[31m  if (!slipOCRData.length) return;\u001b[39m\n\u001b[31m  const list = document.getElementById('ocrSlipList');\u001b[39m\n\u001b[31m  document.getElementById('ocrResults').classList.add('visible');·\u001b[39m\n\u001b[31m  const needReview = slipOCRData.filter(s => s.confidence < 0.65 && s.docket).length;\u001b[39m\n\u001b[31m  const autoOk     = slipOCRData.filter(s => s.confidence >= 0.88).length;\u001b[39m\n\u001b[31m  const summary    = document.getElementById('ocrSummary');\u001b[39m\n\u001b[31m  if (summary) {\u001b[39m\n\u001b[31m    summary.style.display = '';\u001b[39m\n\u001b[31m    summary.innerHTML =\u001b[39m\n\u001b[31m      `<span style=\\\"color:var(--grn)\\\">✓ ${autoOk} auto-accepted</span>` +\u001b[39m\n\u001b[31m      (needReview ? `&nbsp;·&nbsp;<span style=\\\"color:var(--ylw)\\\">⚠ ${needReview} need manual check</span>` : '');\u001b[39m\n\u001b[31m  }·\u001b[39m\n\u001b[31m  list.innerHTML = slipOCRData.map((s, i) => {\u001b[39m\n\u001b[31m    const autoAccepted = s.confidence >= 0.88 && s.docket;\u001b[39m\n\u001b[31m    const needsCheck   = s.confidence < 0.65  && s.docket;\u001b[39m\n\u001b[31m    const confClass = autoAccepted ? 'conf-high' : needsCheck ? 'conf-low' : 'conf-med';\u001b[39m\n\u001b[31m    const confLabel = autoAccepted\u001b[39m\n\u001b[31m      ? `✓ ${Math.round(s.confidence * 100)}% — auto`\u001b[39m\n\u001b[31m      : needsCheck\u001b[39m\n\u001b[31m        ? `⚠ ${Math.round(s.confidence * 100)}% — verify`\u001b[39m\n\u001b[31m        : `${Math.round(s.confidence * 100)}%`;\u001b[39m\n\u001b[31m    const borderStyle = needsCheck ? 'border:1px solid var(--ylw);' : '';\u001b[39m\n\u001b[31m    return `\u001b[39m\n\u001b[31m    <div class=\\\"ocr-slip\\\" id=\\\"ocrSlip_${i}\\\" style=\\\"${borderStyle}\\\">\u001b[39m\n\u001b[31m      <img src=\\\"${s.imageData}\\\" alt=\\\"slip ${i+1}\\\">\u001b[39m\n\u001b[31m      <div class=\\\"ocr-slip-body\\\">\u001b[39m\n\u001b[31m        <div class=\\\"ocr-slip-name\\\">${s.filename}</div>\u001b[39m\n\u001b[31m        <input type=\\\"text\\\" class=\\\"ocr-slip-docket\\\" id=\\\"ocrDocket_${i}\\\"\u001b[39m\n\u001b[31m          value=\\\"${s.docket}\\\" placeholder=\\\"Docket number\\\"\u001b[39m\n\u001b[31m          ${autoAccepted ? 'style=\\\"opacity:.6\\\"' : ''}\u001b[39m\n\u001b[31m          oninput=\\\"slipOCRData[${i}].docket=this.value;slipOCRData[${i}].confidence=0.5\\\">\u001b[39m\n\u001b[31m        <div class=\\\"ocr-slip-status\\\">\u001b[39m\n\u001b[31m          <span class=\\\"ocr-confidence ${confClass}\\\">${confLabel}</span>\u001b[39m\n\u001b[31m          <span style=\\\"color:var(--t4);font-family:var(--fnt-mono);font-size:.6rem;\\\">via ${s.source}</span>\u001b[39m\n\u001b[31m          <button class=\\\"ocr-ai-btn\\\" onclick=\\\"reOCR(${i})\\\">🔄 Re-OCR</button>\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m    </div>`;\u001b[39m\n\u001b[31m  }).join('');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31masync function reOCR(i) {\u001b[39m\n\u001b[31m  const s = slipOCRData[i];\u001b[39m\n\u001b[31m  showOCRWorking(true, `Re-running OCR on ${s.filename}...`);\u001b[39m\n\u001b[31m  try {\u001b[39m\n\u001b[31m    const result = await window.runOCROnImage(s.imageData, s.filename);\u001b[39m\n\u001b[31m    slipOCRData[i] = { ...s, ...result };\u001b[39m\n\u001b[31m    renderOCRResults();\u001b[39m\n\u001b[31m    showToast('Re-OCR complete', 'ok');\u001b[39m\n\u001b[31m  } catch(e) {\u001b[39m\n\u001b[31m    showToast('OCR failed', 'err');\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m  showOCRWorking(false);\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction clearSlips() {\u001b[39m\n\u001b[31m  slipOCRData = [];\u001b[39m\n\u001b[31m  document.getElementById('ocrResults').classList.remove('visible');\u001b[39m\n\u001b[31m  document.getElementById('ocrSlipList').innerHTML = '';\u001b[39m\n\u001b[31m  const sum = document.getElementById('ocrSummary');\u001b[39m\n\u001b[31m  if (sum) { sum.style.display = 'none'; sum.innerHTML = ''; }\u001b[39m\n\u001b[31m  document.getElementById('shareTargetInfo').style.display = 'none';\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction applyAllDockets() {\u001b[39m\n\u001b[31m  // Match each OCR result to a consignee by order or by name matching\u001b[39m\n\u001b[31m  const consList = document.getElementById('consList');\u001b[39m\n\u001b[31m  const cards = consList.querySelectorAll('.cons-card');\u001b[39m\n\u001b[31m  slipOCRData.forEach((s, i) => {\u001b[39m\n\u001b[31m    if (!s.docket) return;\u001b[39m\n\u001b[31m    const card = cards[i];\u001b[39m\n\u001b[31m    if (card) {\u001b[39m\n\u001b[31m      const docketInput = card.querySelector('.cons-docket-input');\u001b[39m\n\u001b[31m      if (docketInput) {\u001b[39m\n\u001b[31m        docketInput.value = s.docket;\u001b[39m\n\u001b[31m        showLinkedDocket(card, s.docket);\u001b[39m\n\u001b[31m      }\u001b[39m\n\u001b[31m    }\u001b[39m\n\u001b[31m  });\u001b[39m\n\u001b[31m  showToast(`Applied ${slipOCRData.filter(s=>s.docket).length} dockets ✓`, 'ok');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31m// STORE LIST — with column mapping UI\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31mlet _rawRows = [];      // raw rows from file, kept for re-mapping\u001b[39m\n\u001b[31mlet _rawHeaders = [];   // headers from file·\u001b[39m\n\u001b[31masync function handleStoreFile(input) {\u001b[39m\n\u001b[31m  const file = input.files[0];\u001b[39m\n\u001b[31m  if (!file) return;\u001b[39m\n\u001b[31m  showToast('Reading file...', 'info');·\u001b[39m\n\u001b[31m  const data = await readFileAsDataURL(file);\u001b[39m\n\u001b[31m  const base64 = data.split(',')[1];\u001b[39m\n\u001b[31m  const binary = atob(base64);\u001b[39m\n\u001b[31m  const bytes = new Uint8Array(binary.length);\u001b[39m\n\u001b[31m  for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);·\u001b[39m\n\u001b[31m  let rows = [];\u001b[39m\n\u001b[31m  try {\u001b[39m\n\u001b[31m    if (file.name.endsWith('.csv')) {\u001b[39m\n\u001b[31m      const text = new TextDecoder().decode(bytes);\u001b[39m\n\u001b[31m      rows = text.split('\\\\n').map(r => r.split(',').map(c => c.trim().replace(/^\\\"|\\\"$/g, '')));\u001b[39m\n\u001b[31m    } else {\u001b[39m\n\u001b[31m      const wb = XLSX.read(bytes, { type: 'array' });\u001b[39m\n\u001b[31m      const ws = wb.Sheets[wb.SheetNames[0]];\u001b[39m\n\u001b[31m      rows = XLSX.utils.sheet_to_json(ws, { header: 1 });\u001b[39m\n\u001b[31m    }\u001b[39m\n\u001b[31m  } catch(e) { showToast('File read error: ' + e.message, 'err'); return; }·\u001b[39m\n\u001b[31m  if (!rows.length || !rows[0]) { showToast('File appears empty', 'err'); return; }·\u001b[39m\n\u001b[31m  _rawHeaders = (rows[0] || []).map(h => (h || '').toString().trim());\u001b[39m\n\u001b[31m  _rawRows = rows.slice(1).filter(r => r && r.some(c => c !== '' && c != null));·\u001b[39m\n\u001b[31m  document.getElementById('storeTableArea').style.display = 'block';\u001b[39m\n\u001b[31m  renderMappingUI(file.name);\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// ── COLUMN MAPPING FIELDS ─────────────────────────────\u001b[39m\n\u001b[31mconst MAPPING_FIELDS = [\u001b[39m\n\u001b[31m  { key: 'name',    label: 'Store / Partner Name', required: true,\u001b[39m\n\u001b[31m    keywords: ['store','shop','name','outlet','consignee','destination','partner','dealer','client','retailer'] },\u001b[39m\n\u001b[31m  { key: 'city',    label: 'City / Location',       required: false,\u001b[39m\n\u001b[31m    keywords: ['city','town','district','location','area','region','place'] },\u001b[39m\n\u001b[31m  { key: 'phone',   label: 'Phone / Mobile',        required: false,\u001b[39m\n\u001b[31m    keywords: ['phone','mobile','contact','tel','number','mob'] },\u001b[39m\n\u001b[31m  { key: 'docket',  label: 'Docket / AWB No.',      required: false,\u001b[39m\n\u001b[31m    keywords: ['docket','awb','tracking','courier','dkt','consignment','cno','waybill'] },\u001b[39m\n\u001b[31m  { key: 'address', label: 'Address',               required: false,\u001b[39m\n\u001b[31m    keywords: ['address','addr','street','locality','area'] },\u001b[39m\n\u001b[31m  { key: 'pincode', label: 'Pincode / PIN',          required: false,\u001b[39m\n\u001b[31m    keywords: ['pin','pincode','postal','zip','code'] },\u001b[39m\n\u001b[31m];·\u001b[39m\n\u001b[31m// Saved mapping key based on column fingerprint (so same client file auto-maps)\u001b[39m\n\u001b[31mfunction getMappingKey() {\u001b[39m\n\u001b[31m  return 'colmap_' + _rawHeaders.join('|').toLowerCase().replace(/\\\\s+/g,'');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// ── Detect if a column is a combined/multiline address cell ─────────────\u001b[39m\n\u001b[31mfunction isCombinedColumn(colIdx) {\u001b[39m\n\u001b[31m  if (colIdx < 0) return false;\u001b[39m\n\u001b[31m  // Check first 3 data rows — if any cell contains newline or has 6+ digit number (pincode) = combined\u001b[39m\n\u001b[31m  const samples = _rawRows.slice(0, 3).map(r => (r[colIdx] || '').toString());\u001b[39m\n\u001b[31m  return samples.some(s => s.includes('\\\\n') || /\\\\b\\\\d{6}\\\\b/.test(s) || s.split(',').length >= 3);\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction renderMappingUI(filename) {\u001b[39m\n\u001b[31m  const saved = (() => { try { return JSON.parse(localStorage.getItem(getMappingKey()) || 'null'); } catch(e) { return null; } })();\u001b[39m\n\u001b[31m  const isSaved = !!saved;\u001b[39m\n\u001b[31m  const colInfoEl = document.getElementById('storeColInfo');\u001b[39m\n\u001b[31m  colInfoEl.style.display = 'block';·\u001b[39m\n\u001b[31m  const suggested = {};\u001b[39m\n\u001b[31m  MAPPING_FIELDS.forEach(f => {\u001b[39m\n\u001b[31m    const idx = findCol(_rawHeaders, f.keywords);\u001b[39m\n\u001b[31m    suggested[f.key] = saved ? (saved[f.key] ?? idx) : idx;\u001b[39m\n\u001b[31m  });·\u001b[39m\n\u001b[31m  // Detect if address column is combined\u001b[39m\n\u001b[31m  const addrIdx = suggested.address >= 0 ? suggested.address : findCol(_rawHeaders, ['address','addr']);\u001b[39m\n\u001b[31m  const looksCombo = isCombinedColumn(addrIdx) || isCombinedColumn(suggested.name);\u001b[39m\n\u001b[31m  const comboCol   = looksCombo ? (addrIdx >= 0 ? addrIdx : suggested.name) : -1;\u001b[39m\n\u001b[31m  const comboPreview = comboCol >= 0 && _rawRows[0] ? (_rawRows[0][comboCol] || '').toString().slice(0, 120) : '';·\u001b[39m\n\u001b[31m  colInfoEl.innerHTML = `\u001b[39m\n\u001b[31m    <div style=\\\"background:var(--sur2);border:1px solid var(--bdr);border-radius:var(--r-sm);padding:12px;\\\">\u001b[39m\n\u001b[31m      <div style=\\\"display:flex;align-items:center;justify-content:space-between;margin-bottom:10px;\\\">\u001b[39m\n\u001b[31m        <div style=\\\"font-family:var(--fnt-mono);font-size:.68rem;color:var(--sky);\\\">\u001b[39m\n\u001b[31m          📋 ${filename} &nbsp;·&nbsp; ${_rawRows.length} rows &nbsp;·&nbsp; ${_rawHeaders.length} columns\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m        ${isSaved ? '<span class=\\\"map-saved-badge\\\">✓ mapping remembered</span>' : ''}\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      <div style=\\\"font-size:.68rem;color:var(--t3);font-family:var(--fnt-mono);margin-bottom:10px;word-break:break-all;\\\">\u001b[39m\n\u001b[31m        Columns: <span style=\\\"color:var(--t2);\\\">${_rawHeaders.map((h,i)=>`[${i+1}] ${h||'?'}`).join(' · ')}</span>\u001b[39m\n\u001b[31m      </div>·\u001b[39m\n\u001b[31m      ${looksCombo ? `\u001b[39m\n\u001b[31m      <!-- COMBINED COLUMN DETECTED -->\u001b[39m\n\u001b[31m      <div style=\\\"background:rgba(56,189,248,.08);border:1px solid rgba(56,189,248,.3);\u001b[39m\n\u001b[31m        border-radius:var(--r-sm);padding:10px 12px;margin-bottom:10px;\\\">\u001b[39m\n\u001b[31m        <div style=\\\"font-size:.72rem;color:var(--sky);font-weight:600;margin-bottom:6px;\\\">\u001b[39m\n\u001b[31m          🤖 Combined Address Column Detected\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m        <div style=\\\"font-size:.68rem;color:var(--t2);font-family:var(--fnt-mono);\u001b[39m\n\u001b[31m          white-space:pre-wrap;background:var(--bg);padding:6px 8px;border-radius:4px;\u001b[39m\n\u001b[31m          margin-bottom:8px;max-height:80px;overflow:hidden;\\\">${comboPreview}</div>\u001b[39m\n\u001b[31m        <div style=\\\"font-size:.7rem;color:var(--t3);margin-bottom:8px;\\\">\u001b[39m\n\u001b[31m          AI will extract: Name · Address · City · Pincode · Phone from each cell.\u001b[39m\n\u001b[31m          Runs in browser (Transformers.js) with Ollama fallback.\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m        <div style=\\\"display:flex;align-items:center;gap:8px;\\\">\u001b[39m\n\u001b[31m          <label style=\\\"display:flex;align-items:center;gap:6px;font-size:.72rem;color:var(--t1);cursor:pointer;\\\">\u001b[39m\n\u001b[31m            <input type=\\\"checkbox\\\" id=\\\"useAIParse\\\" checked style=\\\"cursor:pointer;\\\">\u001b[39m\n\u001b[31m            Use AI to parse combined column\u001b[39m\n\u001b[31m          </label>\u001b[39m\n\u001b[31m          <select id=\\\"comboColSel\\\" style=\\\"background:var(--sur2);border:1px solid var(--bdr);\u001b[39m\n\u001b[31m            color:var(--t1);font-family:var(--fnt-mono);font-size:.72rem;padding:4px 8px;\u001b[39m\n\u001b[31m            border-radius:var(--r-sm);outline:none;\\\">\u001b[39m\n\u001b[31m            ${_rawHeaders.map((h,i)=>`<option value=\\\"${i}\\\" ${i===comboCol?'selected':''}>${h||'(col '+(i+1)+')'}</option>`).join('')}\u001b[39m\n\u001b[31m          </select>\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      ` : ''}·\u001b[39m\n\u001b[31m      <div id=\\\"manualMapSection\\\" style=\\\"${looksCombo ? 'display:none;' : ''}\\\">\u001b[39m\n\u001b[31m        <div style=\\\"font-size:.68rem;color:var(--t3);margin-bottom:6px;\\\">\u001b[39m\n\u001b[31m          ${looksCombo ? 'Manual mapping (used when AI parse is off, or for other columns):' : 'Map each field to your column:'}\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m        <table class=\\\"map-table\\\">\u001b[39m\n\u001b[31m          <thead><tr><th>Field</th><th>Your column</th><th>Preview (row 1)</th></tr></thead>\u001b[39m\n\u001b[31m          <tbody>\u001b[39m\n\u001b[31m            ${MAPPING_FIELDS.map(f => {\u001b[39m\n\u001b[31m              const selIdx = suggested[f.key] ?? -1;\u001b[39m\n\u001b[31m              const preview = selIdx >= 0 && _rawRows[0] ? ((_rawRows[0][selIdx]||'').toString().slice(0,40)) : '—';\u001b[39m\n\u001b[31m              return `<tr>\u001b[39m\n\u001b[31m                <td style=\\\"white-space:nowrap;color:var(--t1);font-weight:500;font-size:.75rem;\\\">\u001b[39m\n\u001b[31m                  ${f.label}${f.required ? '<span class=\\\"map-req\\\">*</span>' : ''}\u001b[39m\n\u001b[31m                </td>\u001b[39m\n\u001b[31m                <td>\u001b[39m\n\u001b[31m                  <select id=\\\"mapsel_${f.key}\\\" onchange=\\\"updateMappingPreview('${f.key}')\\\">\u001b[39m\n\u001b[31m                    ${_rawHeaders.map((h,i)=>\u001b[39m\n\u001b[31m                      `<option value=\\\"${i}\\\" ${i===selIdx?'selected':''}>${h||'(col '+(i+1)+')'}</option>`\u001b[39m\n\u001b[31m                    ).join('')}\u001b[39m\n\u001b[31m                    <option value=\\\"-1\\\" ${selIdx===-1?'selected':''}>— not in file —</option>\u001b[39m\n\u001b[31m                  </select>\u001b[39m\n\u001b[31m                </td>\u001b[39m\n\u001b[31m                <td id=\\\"mapprev_${f.key}\\\" style=\\\"font-family:var(--fnt-mono);font-size:.68rem;\u001b[39m\n\u001b[31m                  color:var(--t2);max-width:90px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;\\\">\u001b[39m\n\u001b[31m                  ${preview}\u001b[39m\n\u001b[31m                </td>\u001b[39m\n\u001b[31m              </tr>`;\u001b[39m\n\u001b[31m            }).join('')}\u001b[39m\n\u001b[31m          </tbody>\u001b[39m\n\u001b[31m        </table>\u001b[39m\n\u001b[31m      </div>·\u001b[39m\n\u001b[31m      <!-- Also map docket/courier no column separately — always shown -->\u001b[39m\n\u001b[31m      <div style=\\\"margin-top:10px;padding-top:10px;border-top:1px solid var(--bdr);\\\">\u001b[39m\n\u001b[31m        <div style=\\\"font-size:.68rem;color:var(--t3);margin-bottom:6px;font-family:var(--fnt-mono);\\\">\u001b[39m\n\u001b[31m          DOCKET / COURIER NO. COLUMN (required for tracking):\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m        <div style=\\\"display:flex;gap:8px;align-items:center;\\\">\u001b[39m\n\u001b[31m          <select id=\\\"docketColSel\\\" style=\\\"background:var(--sur2);border:1px solid var(--bdr);\u001b[39m\n\u001b[31m            color:var(--acc);font-family:var(--fnt-mono);font-size:.78rem;padding:6px 10px;\u001b[39m\n\u001b[31m            border-radius:var(--r-sm);outline:none;flex:1;\\\">\u001b[39m\n\u001b[31m            <option value=\\\"-1\\\">— Docket not in file —</option>\u001b[39m\n\u001b[31m            ${_rawHeaders.map((h,i)=>`<option value=\\\"${i}\\\" ${i===(suggested.docket??-1)?'selected':''}>${h||'(col '+(i+1)+')'}</option>`).join('')}\u001b[39m\n\u001b[31m          </select>\u001b[39m\n\u001b[31m          <span id=\\\"docketPrev\\\" style=\\\"font-family:var(--fnt-mono);font-size:.72rem;color:var(--acc);\\\">\u001b[39m\n\u001b[31m            ${suggested.docket >= 0 && _rawRows[0] ? (_rawRows[0][suggested.docket]||'') : ''}\u001b[39m\n\u001b[31m          </span>\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m      </div>·\u001b[39m\n\u001b[31m      <div class=\\\"btn-row\\\" style=\\\"margin-top:12px;\\\">\u001b[39m\n\u001b[31m        <button class=\\\"btn btn-ghost\\\" style=\\\"font-size:.78rem;\\\" onclick=\\\"clearStores()\\\">✕ Cancel</button>\u001b[39m\n\u001b[31m        <button class=\\\"btn btn-orange\\\" style=\\\"font-size:.78rem;\\\" onclick=\\\"applyMapping()\\\">✓ Apply Mapping</button>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m  `;\u001b[39m\n\u001b[31m  document.getElementById('storeTable').innerHTML = '';\u001b[39m\n\u001b[31m  document.getElementById('storeAddPanel').style.display = 'none';·\u001b[39m\n\u001b[31m  // Update docket preview on change\u001b[39m\n\u001b[31m  const docketSel = document.getElementById('docketColSel');\u001b[39m\n\u001b[31m  if (docketSel) docketSel.addEventListener('change', () => {\u001b[39m\n\u001b[31m    const idx = parseInt(docketSel.value);\u001b[39m\n\u001b[31m    const prev = document.getElementById('docketPrev');\u001b[39m\n\u001b[31m    if (prev) prev.textContent = idx >= 0 && _rawRows[0] ? (_rawRows[0][idx]||'') : '';\u001b[39m\n\u001b[31m  });·\u001b[39m\n\u001b[31m  // Toggle manual map opacity when AI checkbox changes\u001b[39m\n\u001b[31m  const aiChk = document.getElementById('useAIParse');\u001b[39m\n\u001b[31m  const manSec = document.getElementById('manualMapSection');\u001b[39m\n\u001b[31m  if (aiChk && manSec) {\u001b[39m\n\u001b[31m    aiChk.addEventListener('change', () => {\u001b[39m\n\u001b[31m      manSec.style.display = aiChk.checked ? 'none' : 'block';\u001b[39m\n\u001b[31m    });\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction updateMappingPreview(key) {\u001b[39m\n\u001b[31m  const sel = document.getElementById('mapsel_' + key);\u001b[39m\n\u001b[31m  if (!sel) return;\u001b[39m\n\u001b[31m  const idx = parseInt(sel.value);\u001b[39m\n\u001b[31m  const prev = document.getElementById('mapprev_' + key);\u001b[39m\n\u001b[31m  if (prev) prev.textContent = (idx >= 0 && _rawRows[0]) ? ((_rawRows[0][idx]||'').toString().slice(0,40)) : '—';\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// ── REGEX-FIRST ADDRESS PARSER ───────────────────────────────────────────\u001b[39m\n\u001b[31m// Improved Indian address parser\u001b[39m\n\u001b[31m// Handles formats seen in real BTL dispatch files\u001b[39m\n\u001b[31mfunction parseAddressRegex(raw) {\u001b[39m\n\u001b[31m  const text = raw.toString().replace(/\\\\r/g, '').trim();\u001b[39m\n\u001b[31m  const lines = text.split('\\\\n').map(l => l.trim()).filter(Boolean);\u001b[39m\n\u001b[31m  const result = { name:'', address:'', city:'', pincode:'', phone:'', confidence:0 };·\u001b[39m\n\u001b[31m  // ── PHONE: Indian mobile 6-9 prefix, 10 digits ──────────────────────────\u001b[39m\n\u001b[31m  // Also handles \\\"Phone : 9876543210\\\", \\\"M.No.-9876543210\\\", \\\"Mobile - 9876543210\\\"\u001b[39m\n\u001b[31m  const phoneMatch = text.match(/(?:Phone|Mobile|Mob|M\\\\.No\\\\.?|Ph\\\\.?)?\\\\s*[:\\\\-]?\\\\s*\\\\b([6-9]\\\\d{9})\\\\b/i);\u001b[39m\n\u001b[31m  if (phoneMatch) result.phone = phoneMatch[1];·\u001b[39m\n\u001b[31m  // ── PINCODE: 6-digit standalone number ──────────────────────────────────\u001b[39m\n\u001b[31m  // Must not be part of a longer number (e.g. not an account number)\u001b[39m\n\u001b[31m  const pinMatch = text.match(/(?<![\\\\d])([1-9]\\\\d{5})(?![\\\\d])/);\u001b[39m\n\u001b[31m  if (pinMatch) result.pincode = pinMatch[1];·\u001b[39m\n\u001b[31m  // ── CITY: the token immediately before the pincode ───────────────────────\u001b[39m\n\u001b[31m  const indianStates = ['rajasthan','gujarat','maharashtra','punjab','haryana',\u001b[39m\n\u001b[31m    'uttar pradesh','madhya pradesh','karnataka','tamil nadu','andhra pradesh',\u001b[39m\n\u001b[31m    'telangana','kerala','west bengal','bihar','odisha','jharkhand','assam',\u001b[39m\n\u001b[31m    'himachal pradesh','uttarakhand','chhattisgarh','goa','delhi','chandigarh',\u001b[39m\n\u001b[31m    'raj.','raj'];\u001b[39m\n\u001b[31m  if (result.pincode) {\u001b[39m\n\u001b[31m    const pinPos = text.indexOf(result.pincode);\u001b[39m\n\u001b[31m    const beforePin = text.slice(0, pinPos);\u001b[39m\n\u001b[31m    // Strategy 1: split by comma/newline, walk backwards skipping state names\u001b[39m\n\u001b[31m    const chunks = beforePin.split(/[,\\\\n]/).map(c => c.trim()).filter(Boolean);\u001b[39m\n\u001b[31m    for (let i = chunks.length - 1; i >= 0; i--) {\u001b[39m\n\u001b[31m      const chunk = chunks[i].replace(/\\\\d+/g,'').replace(/[()]/g,'').replace(/[.-]/g,'').trim();\u001b[39m\n\u001b[31m      const lower = chunk.toLowerCase();\u001b[39m\n\u001b[31m      if (!chunk || chunk.length < 2) continue;\u001b[39m\n\u001b[31m      if (indianStates.includes(lower)) continue;\u001b[39m\n\u001b[31m      // Skip chunks that look like addresses (contain numbers or long strings)\u001b[39m\n\u001b[31m      if (/\\\\d/.test(chunk)) continue;\u001b[39m\n\u001b[31m      result.city = chunk;\u001b[39m\n\u001b[31m      break;\u001b[39m\n\u001b[31m    }\u001b[39m\n\u001b[31m    // Strategy 2: if still no city, look for last capitalised word before pincode\u001b[39m\n\u001b[31m    // handles \\\"compound paota Jodhpur 342006\\\" → Jodhpur\u001b[39m\n\u001b[31m    if (!result.city) {\u001b[39m\n\u001b[31m      const wordsBefore = beforePin.replace(/[,\\\\n]/g,' ').trim().split(/\\\\s+/);\u001b[39m\n\u001b[31m      for (let i = wordsBefore.length - 1; i >= 0; i--) {\u001b[39m\n\u001b[31m        const w = wordsBefore[i].replace(/[^a-zA-Z]/g,'');\u001b[39m\n\u001b[31m        if (w.length < 3) continue;\u001b[39m\n\u001b[31m        if (indianStates.includes(w.toLowerCase())) continue;\u001b[39m\n\u001b[31m        // Prefer words that start with capital letter (likely a proper noun/city)\u001b[39m\n\u001b[31m        if (/^[A-Z]/.test(w)) { result.city = w; break; }\u001b[39m\n\u001b[31m      }\u001b[39m\n\u001b[31m    }\u001b[39m\n\u001b[31m  }·\u001b[39m\n\u001b[31m  // ── NAME: first meaningful line, extract shop name ───────────────────────\u001b[39m\n\u001b[31m  // Patterns:\u001b[39m\n\u001b[31m  //   \\\"Sanwar Mal Saini, C/o R G COMPUTERS\\\" → name = \\\"R G COMPUTERS\\\"\u001b[39m\n\u001b[31m  //   \\\"SANWAR MAL SAINI, C/o ADVANCE TECHNOLOGIES\\\" → name = \\\"ADVANCE TECHNOLOGIES\\\"\u001b[39m\n\u001b[31m  //   \\\"Sunil Vaishnav\\\" → name = \\\"Sunil Vaishnav\\\" (no C/o)\u001b[39m\n\u001b[31m  //   \\\"Baltej Singh, Professional computer care\\\" → name = \\\"Professional computer care\\\"\u001b[39m\n\u001b[31m  if (lines.length > 0) {\u001b[39m\n\u001b[31m    const firstLine = lines[0];\u001b[39m\n\u001b[31m    // Check for C/o or C/O pattern — shop name comes after it\u001b[39m\n\u001b[31m    const coMatch = firstLine.match(/C\\\\/?[Oo]\\\\.?\\\\s+(.+)/);\u001b[39m\n\u001b[31m    if (coMatch) {\u001b[39m\n\u001b[31m      result.name = coMatch[1].trim();\u001b[39m\n\u001b[31m    } else {\u001b[39m\n\u001b[31m      // Check if there is a comma — second part might be the shop name\u001b[39m\n\u001b[31m      const parts = firstLine.split(',').map(p => p.trim());\u001b[39m\n\u001b[31m      if (parts.length >= 2 && parts[1].length > 3) {\u001b[39m\n\u001b[31m        // Heuristic: if second part looks like a business name (has uppercase or >5 chars)\u001b[39m\n\u001b[31m        const hasUpperOrLong = /[A-Z]{2,}/.test(parts[1]) || parts[1].length > 8;\u001b[39m\n\u001b[31m        result.name = hasUpperOrLong ? parts[1] : parts[0];\u001b[39m\n\u001b[31m      } else {\u001b[39m\n\u001b[31m        result.name = parts[0];\u001b[39m\n\u001b[31m      }\u001b[39m\n\u001b[31m    }\u001b[39m\n\u001b[31m  }·\u001b[39m\n\u001b[31m  // ── ADDRESS: lines between name and phone/pincode line ───────────────────\u001b[39m\n\u001b[31m  const phoneLineIdx = lines.findIndex(l => result.phone && l.includes(result.phone));\u001b[39m\n\u001b[31m  const addrLines = lines.filter((l, i) => {\u001b[39m\n\u001b[31m    if (i === 0) return false;\u001b[39m\n\u001b[31m    if (i === phoneLineIdx) return false;\u001b[39m\n\u001b[31m    // Exclude lines that are just the phone number\u001b[39m\n\u001b[31m    if (/^[6-9]\\\\d{9}$/.test(l.replace(/\\\\s/g,''))) return false;\u001b[39m\n\u001b[31m    return true;\u001b[39m\n\u001b[31m  });\u001b[39m\n\u001b[31m  result.address = addrLines.join(', ').replace(/,\\\\s*,/g, ',').trim();·\u001b[39m\n\u001b[31m  // ── CONFIDENCE: simple score based on what we got ────────────────────────\u001b[39m\n\u001b[31m  let conf = 0;\u001b[39m\n\u001b[31m  if (result.name)    conf += 30;\u001b[39m\n\u001b[31m  if (result.city)    conf += 25;\u001b[39m\n\u001b[31m  if (result.pincode) conf += 25;\u001b[39m\n\u001b[31m  if (result.phone)   conf += 20;\u001b[39m\n\u001b[31m  result.confidence = conf;·\u001b[39m\n\u001b[31m  return result;\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// ── AI ADDRESS PARSER (Ollama fallback) ──────────────────────────────────\u001b[39m\n\u001b[31m// Fetches past corrections from localStorage to improve prompt accuracy\u001b[39m\n\u001b[31mfunction getPastCorrections() {\u001b[39m\n\u001b[31m  try {\u001b[39m\n\u001b[31m    const log = JSON.parse(localStorage.getItem('360lm_parse_corrections') || '[]');\u001b[39m\n\u001b[31m    return log.slice(-10); // last 10 corrections as examples\u001b[39m\n\u001b[31m  } catch(e) { return []; }\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction saveCorrectionLocal(entry) {\u001b[39m\n\u001b[31m  try {\u001b[39m\n\u001b[31m    const log = JSON.parse(localStorage.getItem('360lm_parse_corrections') || '[]');\u001b[39m\n\u001b[31m    log.push(entry);\u001b[39m\n\u001b[31m    // Keep only last 50\u001b[39m\n\u001b[31m    localStorage.setItem('360lm_parse_corrections', JSON.stringify(log.slice(-50)));\u001b[39m\n\u001b[31m  } catch(e) {}\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31masync function parseAddressWithOllama(raw) {\u001b[39m\n\u001b[31m  // Calls the server-side /dispatch-ai/address proxy (avoids CORS + port 11434 issues)\u001b[39m\n\u001b[31m  try {\u001b[39m\n\u001b[31m    const resp = await fetch('/dispatch-ai/address', {\u001b[39m\n\u001b[31m      method: 'POST',\u001b[39m\n\u001b[31m      headers: { 'Content-Type': 'application/json' },\u001b[39m\n\u001b[31m      body: JSON.stringify({ text: raw }),\u001b[39m\n\u001b[31m      signal: AbortSignal.timeout(120000)\u001b[39m\n\u001b[31m    });\u001b[39m\n\u001b[31m    const data = await resp.json();\u001b[39m\n\u001b[31m    if (data.success) return data;\u001b[39m\n\u001b[31m  } catch(e) {}\u001b[39m\n\u001b[31m  return null;\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31masync function parseAddressCell(raw) {\u001b[39m\n\u001b[31m  // Step 1: regex — always runs first, no network call\u001b[39m\n\u001b[31m  const regex = parseAddressRegex(raw);\u001b[39m\n\u001b[31m  if (regex.name) return { ...regex, source: 'regex' };·\u001b[39m\n\u001b[31m  // Step 2: Ollama only if regex got nothing AND Ollama is configured\u001b[39m\n\u001b[31m  // Uses a 3-second timeout to prevent browser hang\u001b[39m\n\u001b[31m  if (window.appConfig.ocr_mode !== 'transformers') {\u001b[39m\n\u001b[31m    try {\u001b[39m\n\u001b[31m      const ai = await Promise.race([\u001b[39m\n\u001b[31m        parseAddressWithOllama(raw),\u001b[39m\n\u001b[31m        new Promise((_, reject) => setTimeout(() => reject(new Error('timeout')), 120000))\u001b[39m\n\u001b[31m      ]);\u001b[39m\n\u001b[31m      if (ai && ai.name) return { ...ai, source: 'ollama' };\u001b[39m\n\u001b[31m    } catch(e) { /* timeout or error — continue to fallback */ }\u001b[39m\n\u001b[31m  }·\u001b[39m\n\u001b[31m  // Step 3: fallback — first line as name\u001b[39m\n\u001b[31m  const lines = raw.toString().split('\\\\n').map(l=>l.trim()).filter(Boolean);\u001b[39m\n\u001b[31m  return { name: lines[0]||raw.slice(0,40), address: raw, city:'', pincode:'', phone:'', source:'fallback' };\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31masync function applyMapping() {\u001b[39m\n\u001b[31m  const useAI   = document.getElementById('useAIParse')?.checked ?? false;\u001b[39m\n\u001b[31m  const comboSel = document.getElementById('comboColSel');\u001b[39m\n\u001b[31m  const comboIdx = comboSel ? parseInt(comboSel.value) : -1;\u001b[39m\n\u001b[31m  const docketIdx = parseInt(document.getElementById('docketColSel')?.value ?? -1);·\u001b[39m\n\u001b[31m  // Build manual mapping as fallback\u001b[39m\n\u001b[31m  const mapping = {};\u001b[39m\n\u001b[31m  MAPPING_FIELDS.forEach(f => {\u001b[39m\n\u001b[31m    const sel = document.getElementById('mapsel_' + f.key);\u001b[39m\n\u001b[31m    mapping[f.key] = sel ? parseInt(sel.value) : -1;\u001b[39m\n\u001b[31m  });\u001b[39m\n\u001b[31m  // Override docket from dedicated selector\u001b[39m\n\u001b[31m  mapping.docket = docketIdx;·\u001b[39m\n\u001b[31m  // Determine if we have a valid base for stores\u001b[39m\n\u001b[31m  const nameIdx = useAI && comboIdx >= 0 ? comboIdx : mapping.name;\u001b[39m\n\u001b[31m  if (nameIdx === -1 && !useAI) { showToast('Store Name column is required', 'err'); return; }·\u001b[39m\n\u001b[31m  // Save mapping\u001b[39m\n\u001b[31m  try { localStorage.setItem(getMappingKey(), JSON.stringify({...mapping, _useAI: useAI, _comboIdx: comboIdx})); } catch(e) {}·\u001b[39m\n\u001b[31m  if (useAI && comboIdx >= 0) {\u001b[39m\n\u001b[31m    // AI path — parse each combined cell\u001b[39m\n\u001b[31m    showToast('AI parsing addresses...', 'info');\u001b[39m\n\u001b[31m    showOCRWorking(true, `Parsing 0/${_rawRows.length} addresses...`);\u001b[39m\n\u001b[31m    stores = [];\u001b[39m\n\u001b[31m    for (let i = 0; i < _rawRows.length; i++) {\u001b[39m\n\u001b[31m      const r = _rawRows[i];\u001b[39m\n\u001b[31m      const raw = (r[comboIdx] || '').toString().trim();\u001b[39m\n\u001b[31m      if (!raw) continue;\u001b[39m\n\u001b[31m      showOCRWorking(true, `Parsing ${i+1}/${_rawRows.length} — ${raw.slice(0,30)}...`);\u001b[39m\n\u001b[31m      // Yield to browser every row to prevent UI freeze\u001b[39m\n\u001b[31m      await new Promise(r => setTimeout(r, 0));\u001b[39m\n\u001b[31m      const parsed = await parseAddressCell(raw);\u001b[39m\n\u001b[31m      stores.push({\u001b[39m\n\u001b[31m        id: i,\u001b[39m\n\u001b[31m        name:    parsed.name    || raw.slice(0,40),\u001b[39m\n\u001b[31m        city:    parsed.city    || '',\u001b[39m\n\u001b[31m        phone:   parsed.phone   || '',\u001b[39m\n\u001b[31m        address: parsed.address || raw,\u001b[39m\n\u001b[31m        pincode: parsed.pincode || '',\u001b[39m\n\u001b[31m        docket:  docketIdx >= 0 ? (r[docketIdx]||'').toString().trim() : '',\u001b[39m\n\u001b[31m        _parseSource: parsed.source,\u001b[39m\n\u001b[31m        _confidence:  parsed.confidence || 0,\u001b[39m\n\u001b[31m        _rawCell:     raw,\u001b[39m\n\u001b[31m        _origName:    parsed.name || '',\u001b[39m\n\u001b[31m        _origCity:    parsed.city || '',\u001b[39m\n\u001b[31m        matched: false\u001b[39m\n\u001b[31m      });\u001b[39m\n\u001b[31m    }\u001b[39m\n\u001b[31m    showOCRWorking(false);\u001b[39m\n\u001b[31m  } else {\u001b[39m\n\u001b[31m    // Manual mapping path\u001b[39m\n\u001b[31m    stores = _rawRows\u001b[39m\n\u001b[31m      .filter(r => r[nameIdx] && (r[nameIdx]+'').trim())\u001b[39m\n\u001b[31m      .map((r, i) => ({\u001b[39m\n\u001b[31m        id: i,\u001b[39m\n\u001b[31m        name:    mapping.name    >= 0 ? (r[mapping.name]   ||'').toString().trim() : '',\u001b[39m\n\u001b[31m        city:    mapping.city    >= 0 ? (r[mapping.city]   ||'').toString().trim() : '',\u001b[39m\n\u001b[31m        phone:   mapping.phone   >= 0 ? (r[mapping.phone]  ||'').toString().trim() : '',\u001b[39m\n\u001b[31m        docket:  mapping.docket  >= 0 ? (r[mapping.docket] ||'').toString().trim() : '',\u001b[39m\n\u001b[31m        address: mapping.address >= 0 ? (r[mapping.address]||'').toString().trim() : '',\u001b[39m\n\u001b[31m        pincode: mapping.pincode >= 0 ? (r[mapping.pincode]||'').toString().trim() : '',\u001b[39m\n\u001b[31m        matched: false\u001b[39m\n\u001b[31m      }));\u001b[39m\n\u001b[31m  }·\u001b[39m\n\u001b[31m  document.getElementById('storeCount').textContent = `${stores.length} stores loaded`;·\u001b[39m\n\u001b[31m  if (useAI && comboIdx >= 0) {\u001b[39m\n\u001b[31m    // AI path — show row-by-row review panel before adding consignees\u001b[39m\n\u001b[31m    showAddressReviewPanel();\u001b[39m\n\u001b[31m  } else {\u001b[39m\n\u001b[31m    // Manual mapping path — go straight to store table + add panel\u001b[39m\n\u001b[31m    renderStoreTable(mapping.docket >= 0 || docketIdx >= 0);\u001b[39m\n\u001b[31m    const addPanel = document.getElementById('storeAddPanel');\u001b[39m\n\u001b[31m    const addBtn   = document.getElementById('storeAddBtn');\u001b[39m\n\u001b[31m    const addTitle = document.getElementById('storeAddTitle');\u001b[39m\n\u001b[31m    const hasDockets = stores.some(s => s.docket);\u001b[39m\n\u001b[31m    addTitle.textContent = hasDockets\u001b[39m\n\u001b[31m      ? `${stores.filter(s=>s.docket).length} stores with dockets — add as consignees?`\u001b[39m\n\u001b[31m      : `Add ${stores.length} stores as consignees?`;\u001b[39m\n\u001b[31m    addBtn.textContent = `✓ Add ${stores.length} as Consignees`;\u001b[39m\n\u001b[31m    addPanel.style.display = 'block';\u001b[39m\n\u001b[31m    showToast(`${stores.length} stores mapped ✓`, 'ok');\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31m// ADDRESS REVIEW PANEL\u001b[39m\n\u001b[31m// Row-by-row review of AI-parsed addresses before consignees created\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31mfunction showAddressReviewPanel() {\u001b[39m\n\u001b[31m  const pane = document.getElementById('addrReviewPane');\u001b[39m\n\u001b[31m  if (!pane) return;\u001b[39m\n\u001b[31m  renderAddressReviewRows();\u001b[39m\n\u001b[31m  pane.style.display = 'block';\u001b[39m\n\u001b[31m  pane.scrollTop = 0;\u001b[39m\n\u001b[31m  showToast(`Review ${stores.length} parsed addresses`, 'info');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction renderAddressReviewRows() {\u001b[39m\n\u001b[31m  const list = document.getElementById('addrReviewList');\u001b[39m\n\u001b[31m  const summary = document.getElementById('addrReviewSummary');\u001b[39m\n\u001b[31m  if (!list) return;·\u001b[39m\n\u001b[31m  const highConf  = stores.filter(s => (s._confidence||0) >= 80).length;\u001b[39m\n\u001b[31m  const medConf   = stores.filter(s => (s._confidence||0) >= 50 && (s._confidence||0) < 80).length;\u001b[39m\n\u001b[31m  const lowConf   = stores.filter(s => (s._confidence||0) < 50).length;·\u001b[39m\n\u001b[31m  summary.innerHTML = `\u001b[39m\n\u001b[31m    <div class=\\\"vs-item\\\"><div class=\\\"vs-val\\\" style=\\\"color:var(--t1);\\\">${stores.length}</div><div class=\\\"vs-lbl\\\">Total</div></div>\u001b[39m\n\u001b[31m    <div class=\\\"vs-item\\\"><div class=\\\"vs-val\\\" style=\\\"color:var(--grn);\\\">${highConf}</div><div class=\\\"vs-lbl\\\">High conf</div></div>\u001b[39m\n\u001b[31m    <div class=\\\"vs-item\\\"><div class=\\\"vs-val\\\" style=\\\"color:var(--ylw);\\\">${medConf}</div><div class=\\\"vs-lbl\\\">Review</div></div>\u001b[39m\n\u001b[31m    <div class=\\\"vs-item\\\"><div class=\\\"vs-val\\\" style=\\\"color:var(--red);\\\">${lowConf}</div><div class=\\\"vs-lbl\\\">Check</div></div>\u001b[39m\n\u001b[31m    <div class=\\\"vs-item\\\" style=\\\"margin-left:auto;\\\">\u001b[39m\n\u001b[31m      <button class=\\\"btn-sm primary\\\" onclick=\\\"approveAllAddresses()\\\" style=\\\"font-size:.7rem;\\\">✓ Approve All</button>\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m  `;·\u001b[39m\n\u001b[31m  list.innerHTML = stores.map((s, i) => {\u001b[39m\n\u001b[31m    const conf = s._confidence || 0;\u001b[39m\n\u001b[31m    const confClass = conf >= 80 ? 'vf-ok' : conf >= 50 ? 'vf-nodocket' : 'vf-dupe';\u001b[39m\n\u001b[31m    const confLabel = conf >= 80 ? `${conf}% ✓` : conf >= 50 ? `${conf}% review` : `${conf}% check`;\u001b[39m\n\u001b[31m    const rowBorder = conf >= 80 ? 'var(--grn)' : conf >= 50 ? 'var(--ylw)' : 'var(--red)';·\u001b[39m\n\u001b[31m    return `\u001b[39m\n\u001b[31m    <div class=\\\"verify-row\\\" id=\\\"arow_${i}\\\" style=\\\"border-left:3px solid ${rowBorder};\\\">\u001b[39m\n\u001b[31m      <div class=\\\"verify-row-hdr\\\">\u001b[39m\n\u001b[31m        <span class=\\\"verify-num\\\">#${i+1}</span>\u001b[39m\n\u001b[31m        <span class=\\\"verify-store\\\" style=\\\"font-size:.78rem;color:var(--t2);font-weight:400;\u001b[39m\n\u001b[31m          font-family:var(--fnt-mono);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;flex:1;\\\"\u001b[39m\n\u001b[31m          title=\\\"${(s._rawCell||'').replace(/\\\"/g,\\\"'\\\")}\\\">\u001b[39m\n\u001b[31m          ${(s._rawCell||'').slice(0,50).split('\\\\n').join(' ↵ ')}       <span class=\\\"verify-flag ${confClass}\\\" style=\\\"flex-shrink:0;margin-right:6px;\\\">${confLabel} · ${s._parseSource||'?'}</span>\u001b[39m\n\u001b[31m        <button onclick=\\\"deleteReviewRow(${i})\\\"\u001b[39m\n\u001b[31m          style=\\\"background:none;border:1px solid rgba(239,68,68,.4);color:var(--red);\u001b[39m\n\u001b[31m          font-size:.65rem;padding:2px 8px;border-radius:6px;cursor:pointer;\u001b[39m\n\u001b[31m          font-family:var(--fnt-mono);flex-shrink:0;transition:background .2s;\\\"\u001b[39m\n\u001b[31m          onmouseover=\\\"this.style.background='rgba(239,68,68,.15)'\\\"\u001b[39m\n\u001b[31m          onmouseout=\\\"this.style.background='none'\\\">✕ Remove</button>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      <div class=\\\"verify-fields\\\" style=\\\"grid-template-columns:1fr 1fr 1fr 1fr 1fr;\\\">\u001b[39m\n\u001b[31m        <div class=\\\"verify-field\\\">\u001b[39m\n\u001b[31m          <label>Shop / Name <span style=\\\"color:var(--acc);\\\">*</span></label>\u001b[39m\n\u001b[31m          <input type=\\\"text\\\" value=\\\"${s.name||''}\\\" id=\\\"ar_name_${i}\\\"\u001b[39m\n\u001b[31m            oninput=\\\"updateAddrField(${i},'name',this)\\\"\u001b[39m\n\u001b[31m            style=\\\"${!s.name ? 'border-color:var(--red);' : ''}\\\">\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m        <div class=\\\"verify-field\\\">\u001b[39m\n\u001b[31m          <label>City</label>\u001b[39m\n\u001b[31m          <input type=\\\"text\\\" value=\\\"${s.city||''}\\\" id=\\\"ar_city_${i}\\\"\u001b[39m\n\u001b[31m            oninput=\\\"updateAddrField(${i},'city',this)\\\"\u001b[39m\n\u001b[31m            style=\\\"${!s.city ? 'border-color:var(--ylw);' : ''}\\\">\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m        <div class=\\\"verify-field\\\">\u001b[39m\n\u001b[31m          <label>Docket / Courier No.</label>\u001b[39m\n\u001b[31m          <input type=\\\"text\\\" value=\\\"${s.docket||''}\\\" id=\\\"ar_docket_${i}\\\"\u001b[39m\n\u001b[31m            oninput=\\\"updateAddrField(${i},'docket',this)\\\"\u001b[39m\n\u001b[31m            style=\\\"font-family:var(--fnt-mono);${!s.docket?'border-color:var(--ylw);':'color:var(--acc);'}\\\">\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m        <div class=\\\"verify-field\\\">\u001b[39m\n\u001b[31m          <label>Phone</label>\u001b[39m\n\u001b[31m          <input type=\\\"tel\\\" value=\\\"${s.phone||''}\\\" id=\\\"ar_phone_${i}\\\"\u001b[39m\n\u001b[31m            oninput=\\\"updateAddrField(${i},'phone',this)\\\">\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m        <div class=\\\"verify-field\\\">\u001b[39m\n\u001b[31m          <label>Pincode</label>\u001b[39m\n\u001b[31m          <input type=\\\"text\\\" value=\\\"${s.pincode||''}\\\" id=\\\"ar_pin_${i}\\\"\u001b[39m\n\u001b[31m            oninput=\\\"updateAddrField(${i},'pincode',this)\\\">\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      <div style=\\\"font-size:.65rem;color:var(--t3);font-family:var(--fnt-mono);\u001b[39m\n\u001b[31m        margin-top:4px;padding:4px 6px;background:var(--bg);border-radius:4px;word-break:break-all;\\\">\u001b[39m\n\u001b[31m        📋 ${(s._rawCell||'').split('\\\\n').join(' | ')}\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m    </div>`;\u001b[39m\n\u001b[31m  }).join('');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction updateAddrField(i, field, input) {\u001b[39m\n\u001b[31m  if (!stores[i]) return;\u001b[39m\n\u001b[31m  stores[i][field] = input.value.trim();\u001b[39m\n\u001b[31m  // Recompute confidence\u001b[39m\n\u001b[31m  let conf = 0;\u001b[39m\n\u001b[31m  if (stores[i].name)    conf += 30;\u001b[39m\n\u001b[31m  if (stores[i].city)    conf += 25;\u001b[39m\n\u001b[31m  if (stores[i].pincode) conf += 25;\u001b[39m\n\u001b[31m  if (stores[i].phone)   conf += 20;\u001b[39m\n\u001b[31m  stores[i]._confidence = conf;\u001b[39m\n\u001b[31m  input.classList.toggle('changed', true);\u001b[39m\n\u001b[31m  // Update border color on row\u001b[39m\n\u001b[31m  const row = document.getElementById('arow_' + i);\u001b[39m\n\u001b[31m  if (row) row.style.borderLeftColor = conf >= 80 ? 'var(--grn)' : conf >= 50 ? 'var(--ylw)' : 'var(--red)';\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction deleteReviewRow(i) {\u001b[39m\n\u001b[31m  // Mark as deleted — splice from stores array and re-render\u001b[39m\n\u001b[31m  stores.splice(i, 1);\u001b[39m\n\u001b[31m  // Re-assign IDs\u001b[39m\n\u001b[31m  stores.forEach((s, idx) => { s.id = idx; });\u001b[39m\n\u001b[31m  renderAddressReviewRows();\u001b[39m\n\u001b[31m  document.getElementById('storeCount').textContent = `${stores.length} stores loaded`;\u001b[39m\n\u001b[31m  showToast(`Row removed — ${stores.length} remaining`, 'info');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction backToMapping() {\u001b[39m\n\u001b[31m  // Hide review pane — store edits are preserved in stores[] array\u001b[39m\n\u001b[31m  document.getElementById('addrReviewPane').style.display = 'none';\u001b[39m\n\u001b[31m  // Show mapping UI again with current file\u001b[39m\n\u001b[31m  document.getElementById('storeTableArea').style.display = 'block';\u001b[39m\n\u001b[31m  document.getElementById('storeColInfo').style.display = 'block';\u001b[39m\n\u001b[31m  showToast('Your edits are preserved — re-apply mapping to reload', 'info');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction approveAllAddresses() {\u001b[39m\n\u001b[31m  stores.forEach((s, i) => { s._reviewed = true; });\u001b[39m\n\u001b[31m  document.getElementById('addrReviewPane').style.display = 'none';\u001b[39m\n\u001b[31m  applyReviewedStores();\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction confirmAddressReview() {\u001b[39m\n\u001b[31m  // Collect any pending edits, log corrections, close panel\u001b[39m\n\u001b[31m  const corrections = [];\u001b[39m\n\u001b[31m  stores.forEach((s, i) => {\u001b[39m\n\u001b[31m    const origName = s._origName || '';\u001b[39m\n\u001b[31m    const origCity = s._origCity || '';\u001b[39m\n\u001b[31m    if ((origName && origName !== s.name) || (origCity && origCity !== s.city)) {\u001b[39m\n\u001b[31m      corrections.push({ raw: s._rawCell||'', name: s.name, city: s.city, pincode: s.pincode });\u001b[39m\n\u001b[31m    }\u001b[39m\n\u001b[31m  });\u001b[39m\n\u001b[31m  // Save corrections locally for future Ollama prompts\u001b[39m\n\u001b[31m  corrections.forEach(c => saveCorrectionLocal(c));\u001b[39m\n\u001b[31m  // Log to Sheets asynchronously (fire and forget)\u001b[39m\n\u001b[31m  if (corrections.length > 0 && window.appConfig.webhook_url) {\u001b[39m\n\u001b[31m    logParseCorrectionsToSheets(corrections);\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m  document.getElementById('addrReviewPane').style.display = 'none';\u001b[39m\n\u001b[31m  applyReviewedStores();\u001b[39m\n\u001b[31m  showToast(`${corrections.length > 0 ? corrections.length + ' corrections logged · ' : ''}${stores.length} stores ready ✓`, 'ok');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// Clean street address: remove city, pincode, phone if they appear in address field\u001b[39m\n\u001b[31mfunction cleanAddress(addr, city, pincode, phone) {\u001b[39m\n\u001b[31m  if (!addr) return addr;\u001b[39m\n\u001b[31m  let s = addr;\u001b[39m\n\u001b[31m  // Remove phone\u001b[39m\n\u001b[31m  if (phone) s = s.replace(new RegExp(phone.replace(/[.*+?^${}()|[\\\\]\\\\\\\\]/g,'\\\\\\\\$&'), 'g'), '').trim();\u001b[39m\n\u001b[31m  // Remove pincode\u001b[39m\n\u001b[31m  if (pincode) s = s.replace(new RegExp('\\\\\\\\b' + pincode + '\\\\\\\\b', 'g'), '').trim();\u001b[39m\n\u001b[31m  // Remove city (case-insensitive)\u001b[39m\n\u001b[31m  if (city) s = s.replace(new RegExp('\\\\\\\\b' + city.replace(/[.*+?^${}()|[\\\\]\\\\\\\\]/g,'\\\\\\\\$&') + '\\\\\\\\b', 'ig'), '').trim();\u001b[39m\n\u001b[31m  // Remove state names and trailing noise\u001b[39m\n\u001b[31m  const noise = /\\\\b(rajasthan|gujarat|maharashtra|punjab|haryana|uttar pradesh|delhi|raj\\\\.)\\\\b/ig;\u001b[39m\n\u001b[31m  s = s.replace(noise, '');\u001b[39m\n\u001b[31m  // Remove \\\"Mobile :\\\", \\\"Phone :\\\", \\\"M.No.\\\" prefixes and dangling numbers\u001b[39m\n\u001b[31m  s = s.replace(/(?:Mobile|Phone|Mob|M\\\\.No\\\\.?|Ph\\\\.?)\\\\s*[:\\\\-]?\\\\s*/ig, '');\u001b[39m\n\u001b[31m  // Clean up multiple commas/spaces\u001b[39m\n\u001b[31m  s = s.replace(/,\\\\s*,/g, ',').replace(/\\\\s{2,}/g, ' ').replace(/^[\\\\s,]+|[\\\\s,]+$/g, '');\u001b[39m\n\u001b[31m  return s;\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction applyReviewedStores() {\u001b[39m\n\u001b[31m  // Clean address fields before loading stores\u001b[39m\n\u001b[31m  stores.forEach(s => {\u001b[39m\n\u001b[31m    s.address = cleanAddress(s.address, s.city, s.pincode, s.phone);\u001b[39m\n\u001b[31m  });\u001b[39m\n\u001b[31m  renderStoreTable(stores.some(s => s.docket));\u001b[39m\n\u001b[31m  const addPanel = document.getElementById('storeAddPanel');\u001b[39m\n\u001b[31m  const addTitle = document.getElementById('storeAddTitle');\u001b[39m\n\u001b[31m  const addBtn   = document.getElementById('storeAddBtn');\u001b[39m\n\u001b[31m  const hasDockets = stores.some(s => s.docket);\u001b[39m\n\u001b[31m  addTitle.textContent = hasDockets\u001b[39m\n\u001b[31m    ? `${stores.filter(s=>s.docket).length} stores with dockets — add as consignees?`\u001b[39m\n\u001b[31m    : `Add ${stores.length} stores as consignees?`;\u001b[39m\n\u001b[31m  addBtn.textContent = `✓ Add ${stores.length} as Consignees`;\u001b[39m\n\u001b[31m  addPanel.style.display = 'block';\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31masync function logParseCorrectionsToSheets(corrections) {\u001b[39m\n\u001b[31m  const url = window.appConfig.webhook_url;\u001b[39m\n\u001b[31m  if (!url) return;\u001b[39m\n\u001b[31m  try {\u001b[39m\n\u001b[31m    await fetch(url, {\u001b[39m\n\u001b[31m      method: 'POST', mode: 'no-cors',\u001b[39m\n\u001b[31m      headers: { 'Content-Type': 'application/json' },\u001b[39m\n\u001b[31m      body: JSON.stringify({\u001b[39m\n\u001b[31m        type: 'parse_correction',\u001b[39m\n\u001b[31m        corrections,\u001b[39m\n\u001b[31m        colFingerprint: getMappingKey(),\u001b[39m\n\u001b[31m        timestamp: new Date().toISOString()\u001b[39m\n\u001b[31m      })\u001b[39m\n\u001b[31m    });\u001b[39m\n\u001b[31m  } catch(e) {}\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction addStoresToConsignees() {\u001b[39m\n\u001b[31m  const consList = document.getElementById('consList');\u001b[39m\n\u001b[31m  consList.innerHTML = '';\u001b[39m\n\u001b[31m  stores.slice(0, 50).forEach(s => addConsignee(s));\u001b[39m\n\u001b[31m  document.getElementById('storeAddPanel').style.display = 'none';\u001b[39m\n\u001b[31m  showToast(`${Math.min(stores.length, 50)} consignees added ✓`, 'ok');\u001b[39m\n\u001b[31m  if (stores.some(s => s.docket)) setTimeout(() => autoCreateDispatchFromStoreList(), 600);\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction promptAddFromStores() { /* replaced by inline panel */ }··\u001b[39m\n\u001b[31mfunction findCol(headers, keywords) {\u001b[39m\n\u001b[31m  for (let i=0;i<headers.length;i++) {\u001b[39m\n\u001b[31m    const h = (headers[i]||'').toString().toLowerCase();\u001b[39m\n\u001b[31m    if (keywords.some(k => h.includes(k))) return i;\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m  return -1;\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction renderStoreTable(hasDocket) {\u001b[39m\n\u001b[31m  const t = document.getElementById('storeTable');\u001b[39m\n\u001b[31m  const cols = ['#','Store Name','City','Phone'];\u001b[39m\n\u001b[31m  if (hasDocket) cols.push('Docket','Status');\u001b[39m\n\u001b[31m  t.innerHTML = `<thead><tr>${cols.map(c=>`<th>${c}</th>`).join('')}</tr></thead>\u001b[39m\n\u001b[31m  <tbody>${stores.map((s,i) => {\u001b[39m\n\u001b[31m    const statusClass = s.docket ? (s.status === 'delivered' ? 'matched' : 'unmatched') : '';\u001b[39m\n\u001b[31m    const docketCell = hasDocket ? `<td class=\\\"${statusClass}\\\" style=\\\"font-family:var(--fnt-mono);\\\">${s.docket||'—'}</td>` : '';\u001b[39m\n\u001b[31m    const statusCell = hasDocket ? `<td>${s.status ? `<span class=\\\"status-pill sp-${s.status}\\\">${s.status}</span>` : '—'}</td>` : '';\u001b[39m\n\u001b[31m    return `<tr>\u001b[39m\n\u001b[31m      <td>${i+1}</td>\u001b[39m\n\u001b[31m      <td>${s.name}</td>\u001b[39m\n\u001b[31m      <td>${s.city}</td>\u001b[39m\n\u001b[31m      <td style=\\\"font-family:var(--fnt-mono);\\\">${s.phone||'—'}</td>\u001b[39m\n\u001b[31m      ${docketCell}${statusCell}\u001b[39m\n\u001b[31m    </tr>`;\u001b[39m\n\u001b[31m  }).join('')}</tbody>`;\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction clearStores() {\u001b[39m\n\u001b[31m  stores = [];\u001b[39m\n\u001b[31m  document.getElementById('storeTableArea').style.display = 'none';\u001b[39m\n\u001b[31m  document.getElementById('storeFile').value = '';\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction autoCreateDispatchFromStoreList() {\u001b[39m\n\u001b[31m  // Create/update dispatch with all stores that have dockets\u001b[39m\n\u001b[31m  const courier = document.getElementById('n_courier').value || 'SkyKing';\u001b[39m\n\u001b[31m  const storesWithDockets = stores.filter(s => s.docket);\u001b[39m\n\u001b[31m  if (!storesWithDockets.length) return;·\u001b[39m\n\u001b[31m  // Clear and rebuild consignee list from store data\u001b[39m\n\u001b[31m  const consList = document.getElementById('consList');\u001b[39m\n\u001b[31m  consList.innerHTML = '';\u001b[39m\n\u001b[31m  storesWithDockets.forEach(s => {\u001b[39m\n\u001b[31m    addConsignee(s);\u001b[39m\n\u001b[31m  });\u001b[39m\n\u001b[31m  showPane('new');\u001b[39m\n\u001b[31m  showToast(`${storesWithDockets.length} consignees added from store list`, 'ok');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction promptAddFromStores() {\u001b[39m\n\u001b[31m  // Replaced by inline storeAddPanel UI — no-op kept for safety\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction autoMatchDockets() {\u001b[39m\n\u001b[31m  // Match OCR dockets to stores by order\u001b[39m\n\u001b[31m  if (!slipOCRData.length) { showToast('Upload docket slip images first', 'err'); return; }\u001b[39m\n\u001b[31m  let matched = 0;\u001b[39m\n\u001b[31m  slipOCRData.forEach((slip, i) => {\u001b[39m\n\u001b[31m    if (i < stores.length && slip.docket) {\u001b[39m\n\u001b[31m      stores[i].docket = slip.docket;\u001b[39m\n\u001b[31m      matched++;\u001b[39m\n\u001b[31m    }\u001b[39m\n\u001b[31m  });\u001b[39m\n\u001b[31m  renderStoreTable(true);\u001b[39m\n\u001b[31m  showToast(`${matched} dockets matched to stores ✓`, 'ok');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31m// CONSIGNEES\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31mfunction addConsignee(storeData = null) {\u001b[39m\n\u001b[31m  const list = document.getElementById('consList');\u001b[39m\n\u001b[31m  const idx = list.children.length;\u001b[39m\n\u001b[31m  const card = document.createElement('div');\u001b[39m\n\u001b[31m  card.className = 'cons-card';\u001b[39m\n\u001b[31m  card.dataset.idx = idx;\u001b[39m\n\u001b[31m  const s = storeData || {};\u001b[39m\n\u001b[31m  card.innerHTML = `\u001b[39m\n\u001b[31m    <div class=\\\"cons-hdr\\\">\u001b[39m\n\u001b[31m      <span class=\\\"cons-num\\\">CONSIGNEE #${idx+1}</span>\u001b[39m\n\u001b[31m      <button class=\\\"cons-del\\\" onclick=\\\"this.closest('.cons-card').remove();renumberCons()\\\">✕</button>\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m    <div class=\\\"row2\\\">\u001b[39m\n\u001b[31m      <div class=\\\"f\\\">\u001b[39m\n\u001b[31m        <label>Store / Name <span class=\\\"req\\\">*</span></label>\u001b[39m\n\u001b[31m        <input type=\\\"text\\\" class=\\\"cons-name\\\" placeholder=\\\"Store name\\\" value=\\\"${s.name||''}\\\">\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      <div class=\\\"f\\\">\u001b[39m\n\u001b[31m        <label>City <span class=\\\"req\\\">*</span></label>\u001b[39m\n\u001b[31m        <input type=\\\"text\\\" class=\\\"cons-city\\\" placeholder=\\\"City\\\" value=\\\"${s.city||''}\\\">\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m    <div class=\\\"f\\\">\u001b[39m\n\u001b[31m      <label>Address</label>\u001b[39m\n\u001b[31m      <input type=\\\"text\\\" class=\\\"cons-addr\\\" placeholder=\\\"Full address\\\" value=\\\"${s.address||''}\\\">\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m    <div class=\\\"row2\\\">\u001b[39m\n\u001b[31m      <div class=\\\"f\\\">\u001b[39m\n\u001b[31m        <label>Phone</label>\u001b[39m\n\u001b[31m        <input type=\\\"tel\\\" class=\\\"cons-phone\\\" placeholder=\\\"Mobile\\\" value=\\\"${s.phone||''}\\\">\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      <div class=\\\"f\\\">\u001b[39m\n\u001b[31m        <label>Pincode</label>\u001b[39m\n\u001b[31m        <input type=\\\"text\\\" class=\\\"cons-pin\\\" placeholder=\\\"6-digit\\\" maxlength=\\\"6\\\">\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m    <div class=\\\"f\\\">\u001b[39m\n\u001b[31m      <label>Material / Contents <span class=\\\"req\\\">*</span></label>\u001b[39m\n\u001b[31m      <input type=\\\"text\\\" class=\\\"cons-material\\\" placeholder=\\\"e.g. Standees, Posters\\\">\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m    <div class=\\\"row2\\\">\u001b[39m\n\u001b[31m      <div class=\\\"f\\\">\u001b[39m\n\u001b[31m        <label>Qty</label>\u001b[39m\n\u001b[31m        <input type=\\\"number\\\" class=\\\"cons-qty\\\" placeholder=\\\"0\\\" min=\\\"0\\\">\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      <div class=\\\"f\\\">\u001b[39m\n\u001b[31m        <label>Docket No.</label>\u001b[39m\n\u001b[31m        <input type=\\\"text\\\" class=\\\"cons-docket-input\\\" placeholder=\\\"Docket #\\\"\u001b[39m\n\u001b[31m          style=\\\"font-family:var(--fnt-mono);\\\" value=\\\"${s.docket||''}\\\"\u001b[39m\n\u001b[31m          oninput=\\\"onDocketInput(this)\\\">\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m    ${s.docket ? `<div class=\\\"cons-docket-linked\\\"><span>🔗</span><span class=\\\"dkt\\\">${s.docket}</span></div>` : ''}\u001b[39m\n\u001b[31m  `;\u001b[39m\n\u001b[31m  list.appendChild(card);\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction onDocketInput(input) {\u001b[39m\n\u001b[31m  const card = input.closest('.cons-card');\u001b[39m\n\u001b[31m  let linked = card.querySelector('.cons-docket-linked');\u001b[39m\n\u001b[31m  if (input.value.trim()) {\u001b[39m\n\u001b[31m    if (!linked) {\u001b[39m\n\u001b[31m      linked = document.createElement('div');\u001b[39m\n\u001b[31m      linked.className = 'cons-docket-linked';\u001b[39m\n\u001b[31m      card.appendChild(linked);\u001b[39m\n\u001b[31m    }\u001b[39m\n\u001b[31m    linked.innerHTML = `<span>🔗</span><span class=\\\"dkt\\\">${input.value}</span>`;\u001b[39m\n\u001b[31m  } else if (linked) {\u001b[39m\n\u001b[31m    linked.remove();\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction showLinkedDocket(card, docket) {\u001b[39m\n\u001b[31m  const input = card.querySelector('.cons-docket-input');\u001b[39m\n\u001b[31m  if (input) { input.value = docket; onDocketInput(input); }\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction renumberCons() {\u001b[39m\n\u001b[31m  document.querySelectorAll('#consList .cons-card').forEach((c,i) => {\u001b[39m\n\u001b[31m    c.querySelector('.cons-num').textContent = `CONSIGNEE #${i+1}`;\u001b[39m\n\u001b[31m    c.dataset.idx = i;\u001b[39m\n\u001b[31m  });\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31m// JOB REFERENCE\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31mfunction onClientChange() {\u001b[39m\n\u001b[31m  const val = document.getElementById('n_client').value;\u001b[39m\n\u001b[31m  document.getElementById('customClientField').style.display = val === '__c' ? 'block' : 'none';\u001b[39m\n\u001b[31m  updatePill();\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31mfunction updatePill() {\u001b[39m\n\u001b[31m  const client = document.getElementById('n_client').value;\u001b[39m\n\u001b[31m  const job = document.getElementById('n_jobref').value.trim();\u001b[39m\n\u001b[31m  const cName = client === '__c' ? document.getElementById('n_customClient').value : client;\u001b[39m\n\u001b[31m  const pill = document.getElementById('jobPill');\u001b[39m\n\u001b[31m  if (cName && job) {\u001b[39m\n\u001b[31m    document.getElementById('jobPillTxt').textContent = `${cName} › ${job}`;\u001b[39m\n\u001b[31m    pill.style.display = 'inline-flex';\u001b[39m\n\u001b[31m  } else {\u001b[39m\n\u001b[31m    pill.style.display = 'none';\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31mfunction clearJob() {\u001b[39m\n\u001b[31m  document.getElementById('n_client').value = '';\u001b[39m\n\u001b[31m  document.getElementById('n_jobref').value = '';\u001b[39m\n\u001b[31m  document.getElementById('jobPill').style.display = 'none';\u001b[39m\n\u001b[31m  document.getElementById('customClientField').style.display = 'none';\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31m// GATHER / SAVE\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31mfunction gatherDispatch() {\u001b[39m\n\u001b[31m  const client = document.getElementById('n_client').value;\u001b[39m\n\u001b[31m  const clientName = client === '__c' ? document.getElementById('n_customClient').value : client;\u001b[39m\n\u001b[31m  const consignees = [];\u001b[39m\n\u001b[31m  document.querySelectorAll('#consList .cons-card').forEach(card => {\u001b[39m\n\u001b[31m    const docket = card.querySelector('.cons-docket-input')?.value.trim() || '';\u001b[39m\n\u001b[31m    consignees.push({\u001b[39m\n\u001b[31m      name: card.querySelector('.cons-name')?.value || '',\u001b[39m\n\u001b[31m      city: card.querySelector('.cons-city')?.value || '',\u001b[39m\n\u001b[31m      address: card.querySelector('.cons-addr')?.value || '',\u001b[39m\n\u001b[31m      phone: card.querySelector('.cons-phone')?.value || '',\u001b[39m\n\u001b[31m      pincode: card.querySelector('.cons-pin')?.value || '',\u001b[39m\n\u001b[31m      material: card.querySelector('.cons-material')?.value || '',\u001b[39m\n\u001b[31m      qty: card.querySelector('.cons-qty')?.value || '',\u001b[39m\n\u001b[31m      docket,\u001b[39m\n\u001b[31m      status: docket ? 'booked' : 'pending',\u001b[39m\n\u001b[31m      lastEvent: '',\u001b[39m\n\u001b[31m      lastUpdated: ''\u001b[39m\n\u001b[31m    });\u001b[39m\n\u001b[31m  });\u001b[39m\n\u001b[31m  return {\u001b[39m\n\u001b[31m    id: 'DSP-' + Date.now(),\u001b[39m\n\u001b[31m    mode: 'manual',\u001b[39m\n\u001b[31m    client: clientName,\u001b[39m\n\u001b[31m    jobref: document.getElementById('n_jobref').value,\u001b[39m\n\u001b[31m    courier: document.getElementById('n_courier').value,\u001b[39m\n\u001b[31m    dispatchDate: document.getElementById('n_date').value,\u001b[39m\n\u001b[31m    remarks: document.getElementById('n_remarks').value,\u001b[39m\n\u001b[31m    consignees,\u001b[39m\n\u001b[31m    slipCount: slipOCRData.length,\u001b[39m\n\u001b[31m    status: 'active',\u001b[39m\n\u001b[31m    createdAt: new Date().toISOString()\u001b[39m\n\u001b[31m  };\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction validate(d) {\u001b[39m\n\u001b[31m  if (!d.client) { showToast('Select a client', 'err'); return false; }\u001b[39m\n\u001b[31m  if (!d.courier) { showToast('Select courier', 'err'); return false; }\u001b[39m\n\u001b[31m  if (!d.consignees.length) { showToast('Add at least one consignee', 'err'); return false; }\u001b[39m\n\u001b[31m  for (let i = 0; i < d.consignees.length; i++) {\u001b[39m\n\u001b[31m    const c = d.consignees[i];\u001b[39m\n\u001b[31m    if (!c.name || !c.city) {\u001b[39m\n\u001b[31m      showToast(`Consignee #${i+1} (${c.name||'unnamed'}) — name & city required`, 'err');\u001b[39m\n\u001b[31m      // Scroll to and highlight the problem consignee card\u001b[39m\n\u001b[31m      const cards = document.querySelectorAll('#consList .cons-card');\u001b[39m\n\u001b[31m      if (cards[i]) {\u001b[39m\n\u001b[31m        cards[i].scrollIntoView({ behavior: 'smooth', block: 'center' });\u001b[39m\n\u001b[31m        cards[i].style.borderColor = 'var(--red)';\u001b[39m\n\u001b[31m        cards[i].style.boxShadow = '0 0 0 2px rgba(239,68,68,.3)';\u001b[39m\n\u001b[31m        setTimeout(() => {\u001b[39m\n\u001b[31m          if (cards[i]) { cards[i].style.borderColor = ''; cards[i].style.boxShadow = ''; }\u001b[39m\n\u001b[31m        }, 3000);\u001b[39m\n\u001b[31m      }\u001b[39m\n\u001b[31m      return false;\u001b[39m\n\u001b[31m    }\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m  return true;\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction saveDraft() {\u001b[39m\n\u001b[31m  const d = gatherDispatch();\u001b[39m\n\u001b[31m  d.status = 'draft';\u001b[39m\n\u001b[31m  dispatches.unshift(d);\u001b[39m\n\u001b[31m  saveData();\u001b[39m\n\u001b[31m  updateHistCount();\u001b[39m\n\u001b[31m  showToast('Draft saved ✓', 'ok');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// ── VERIFICATION SCREEN ─────────────────────────────────\u001b[39m\n\u001b[31m// Known docket lengths per courier for validation\u001b[39m\n\u001b[31mconst DOCKET_LENGTHS = {\u001b[39m\n\u001b[31m  'SkyKing': { min: 9, max: 12 },\u001b[39m\n\u001b[31m  'DTDC':    { min: 8, max: 15 },\u001b[39m\n\u001b[31m  'Delhivery': { min: 10, max: 18 },\u001b[39m\n\u001b[31m  'Blue Dart': { min: 10, max: 13 },\u001b[39m\n\u001b[31m  'FedEx':   { min: 10, max: 20 },\u001b[39m\n\u001b[31m  'Ecom Express': { min: 10, max: 14 },\u001b[39m\n\u001b[31m  'India Post': { min: 13, max: 22 },\u001b[39m\n\u001b[31m};·\u001b[39m\n\u001b[31mfunction finaliseDispatch() {\u001b[39m\n\u001b[31m  const d = gatherDispatch();\u001b[39m\n\u001b[31m  if (!validate(d)) return;\u001b[39m\n\u001b[31m  showVerifyScreen(d);\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction showVerifyScreen(d) {\u001b[39m\n\u001b[31m  const pane = document.getElementById('verifyPane');\u001b[39m\n\u001b[31m  const list = document.getElementById('verifyList');\u001b[39m\n\u001b[31m  const summary = document.getElementById('verifySummary');·\u001b[39m\n\u001b[31m  // Build flags\u001b[39m\n\u001b[31m  const docketsSeen = {};\u001b[39m\n\u001b[31m  const courier = d.courier;\u001b[39m\n\u001b[31m  const lenRule = DOCKET_LENGTHS[courier];·\u001b[39m\n\u001b[31m  d.consignees.forEach((c, i) => {\u001b[39m\n\u001b[31m    if (c.docket) docketsSeen[c.docket] = (docketsSeen[c.docket] || 0) + 1;\u001b[39m\n\u001b[31m  });·\u001b[39m\n\u001b[31m  const total     = d.consignees.length;\u001b[39m\n\u001b[31m  const withDock  = d.consignees.filter(c => c.docket).length;\u001b[39m\n\u001b[31m  const dupes     = Object.values(docketsSeen).filter(v => v > 1).length;\u001b[39m\n\u001b[31m  const noDock    = total - withDock;\u001b[39m\n\u001b[31m  const badLen    = lenRule ? d.consignees.filter(c => c.docket &&\u001b[39m\n\u001b[31m    (c.docket.length < lenRule.min || c.docket.length > lenRule.max)).length : 0;·\u001b[39m\n\u001b[31m  // Summary bar\u001b[39m\n\u001b[31m  summary.innerHTML = `\u001b[39m\n\u001b[31m    <div class=\\\"vs-item\\\"><div class=\\\"vs-val\\\" style=\\\"color:var(--t1);\\\">${total}</div><div class=\\\"vs-lbl\\\">Total</div></div>\u001b[39m\n\u001b[31m    <div class=\\\"vs-item\\\"><div class=\\\"vs-val\\\" style=\\\"color:var(--grn);\\\">${withDock}</div><div class=\\\"vs-lbl\\\">Docketed</div></div>\u001b[39m\n\u001b[31m    <div class=\\\"vs-item\\\"><div class=\\\"vs-val\\\" style=\\\"color:${noDock > 0 ? 'var(--ylw)' : 'var(--t3)'};\\\">${noDock}</div><div class=\\\"vs-lbl\\\">No Docket</div></div>\u001b[39m\n\u001b[31m    <div class=\\\"vs-item\\\"><div class=\\\"vs-val\\\" style=\\\"color:${dupes > 0 ? 'var(--red)' : 'var(--t3)'};\\\">${dupes}</div><div class=\\\"vs-lbl\\\">Duplicates</div></div>\u001b[39m\n\u001b[31m    ${badLen > 0 ? `<div class=\\\"vs-item\\\"><div class=\\\"vs-val\\\" style=\\\"color:var(--sky);\\\">${badLen}</div><div class=\\\"vs-lbl\\\">Short/Long</div></div>` : ''}\u001b[39m\n\u001b[31m    <div class=\\\"vs-item\\\" style=\\\"margin-left:auto;text-align:right;\\\">\u001b[39m\n\u001b[31m      <div class=\\\"vs-val\\\" style=\\\"font-size:.8rem;color:var(--t2);\\\">${d.courier||'—'}</div>\u001b[39m\n\u001b[31m      <div class=\\\"vs-lbl\\\">${d.client||'—'} ${d.jobref ? '› '+d.jobref : ''}</div>\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m  `;·\u001b[39m\n\u001b[31m  // Build consignee rows\u001b[39m\n\u001b[31m  list.innerHTML = d.consignees.map((c, i) => {\u001b[39m\n\u001b[31m    const isDupe   = c.docket && docketsSeen[c.docket] > 1;\u001b[39m\n\u001b[31m    const isNoDock = !c.docket;\u001b[39m\n\u001b[31m    const isShort  = lenRule && c.docket &&\u001b[39m\n\u001b[31m      (c.docket.length < lenRule.min || c.docket.length > lenRule.max);·\u001b[39m\n\u001b[31m    const rowClass = isDupe ? 'has-dupe' : (isNoDock ? 'no-docket' : 'ok');·\u001b[39m\n\u001b[31m    // Find matching OCR slip for thumbnail\u001b[39m\n\u001b[31m    const ocrMatch = slipOCRData[i];\u001b[39m\n\u001b[31m    const thumbHtml = ocrMatch?.imageData\u001b[39m\n\u001b[31m      ? `<img class=\\\"verify-thumb\\\" src=\\\"${ocrMatch.imageData}\\\" onclick=\\\"showSlipFull(${i})\\\" title=\\\"View slip\\\">`\u001b[39m\n\u001b[31m      : '';·\u001b[39m\n\u001b[31m    // Flags\u001b[39m\n\u001b[31m    let flags = '';\u001b[39m\n\u001b[31m    if (isDupe)   flags += `<span class=\\\"verify-flag vf-dupe\\\">⚠ Duplicate</span> `;\u001b[39m\n\u001b[31m    if (isNoDock) flags += `<span class=\\\"verify-flag vf-nodocket\\\">No Docket</span> `;\u001b[39m\n\u001b[31m    if (isShort)  flags += `<span class=\\\"verify-flag vf-short\\\">${c.docket.length} digits</span> `;\u001b[39m\n\u001b[31m    if (!isDupe && !isNoDock && !isShort) flags = `<span class=\\\"verify-flag vf-ok\\\">✓ OK</span>`;·\u001b[39m\n\u001b[31m    // OCR confidence note\u001b[39m\n\u001b[31m    const confNote = ocrMatch && ocrMatch.confidence\u001b[39m\n\u001b[31m      ? `<div class=\\\"ocr-correction-note\\\">OCR confidence: ${Math.round(ocrMatch.confidence*100)}% via ${ocrMatch.source||'—'}</div>` : '';·\u001b[39m\n\u001b[31m    // WhatsApp URL\u001b[39m\n\u001b[31m    const waMsg = encodeURIComponent(\u001b[39m\n\u001b[31m      `Dear ${c.name}, your material has been dispatched via ${d.courier}. Docket: ${c.docket||'TBD'}. Track: ${(COURIER_MASTER[d.courier]?.trackUrl||'').replace('{docket}', c.docket||'')}`\u001b[39m\n\u001b[31m    );\u001b[39m\n\u001b[31m    const waUrl = c.phone ? `https://wa.me/91${c.phone.replace(/\\\\D/g,'')}?text=${waMsg}` : '';·\u001b[39m\n\u001b[31m    return `\u001b[39m\n\u001b[31m    <div class=\\\"verify-row ${rowClass}\\\" id=\\\"vrow_${i}\\\">\u001b[39m\n\u001b[31m      <div class=\\\"verify-row-hdr\\\">\u001b[39m\n\u001b[31m        <span class=\\\"verify-num\\\">#${i+1}</span>\u001b[39m\n\u001b[31m        <span class=\\\"verify-store\\\">${c.name||'—'}</span>\u001b[39m\n\u001b[31m        ${flags}\u001b[39m\n\u001b[31m        ${thumbHtml}\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      <div class=\\\"verify-fields\\\" style=\\\"grid-template-columns:1fr 1fr 1fr;\\\">\u001b[39m\n\u001b[31m        <div class=\\\"verify-field\\\">\u001b[39m\n\u001b[31m          <label>Docket No. <span style=\\\"color:var(--acc);\\\">*</span></label>\u001b[39m\n\u001b[31m          <input type=\\\"text\\\" value=\\\"${c.docket||''}\\\" oninput=\\\"updateVerifyField(${i},'docket',this)\\\"\u001b[39m\n\u001b[31m            style=\\\"font-family:var(--fnt-mono);${isDupe||isShort?'border-color:var(--red);':!c.docket?'border-color:var(--ylw);':'color:var(--acc);'}\\\">\u001b[39m\n\u001b[31m          ${confNote}\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m        <div class=\\\"verify-field\\\">\u001b[39m\n\u001b[31m          <label>City</label>\u001b[39m\n\u001b[31m          <input type=\\\"text\\\" value=\\\"${c.city||''}\\\" oninput=\\\"updateVerifyField(${i},'city',this)\\\"\u001b[39m\n\u001b[31m            style=\\\"${!c.city ? 'border-color:var(--ylw);' : ''}\\\">\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m        <div class=\\\"verify-field\\\">\u001b[39m\n\u001b[31m          <label>Pincode</label>\u001b[39m\n\u001b[31m          <input type=\\\"text\\\" value=\\\"${c.pincode||''}\\\" oninput=\\\"updateVerifyField(${i},'pincode',this)\\\"\u001b[39m\n\u001b[31m            style=\\\"font-family:var(--fnt-mono);\\\">\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m        <div class=\\\"verify-field\\\">\u001b[39m\n\u001b[31m          <label>Phone</label>\u001b[39m\n\u001b[31m          <input type=\\\"tel\\\" value=\\\"${c.phone||''}\\\" oninput=\\\"updateVerifyField(${i},'phone',this)\\\">\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m        <div class=\\\"verify-field\\\">\u001b[39m\n\u001b[31m          <label>Material / Contents</label>\u001b[39m\n\u001b[31m          <input type=\\\"text\\\" value=\\\"${c.material||''}\\\" oninput=\\\"updateVerifyField(${i},'material',this)\\\">\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m        <div class=\\\"verify-field\\\">\u001b[39m\n\u001b[31m          <label>Street Address</label>\u001b[39m\n\u001b[31m          <input type=\\\"text\\\" value=\\\"${c.address||''}\\\" oninput=\\\"updateVerifyField(${i},'address',this)\\\"\u001b[39m\n\u001b[31m            placeholder=\\\"Street / locality only\\\">\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      <div class=\\\"verify-actions\\\">\u001b[39m\n\u001b[31m        ${c.docket && c.phone ? `<button class=\\\"btn-wa\\\" onclick=\\\"window.open('${waUrl}','_blank')\\\">📱 WhatsApp</button>` : ''}\u001b[39m\n\u001b[31m        <button class=\\\"btn-sm\\\" onclick=\\\"removeVerifyRow(${i})\\\">✕ Remove</button>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m    </div>`;\u001b[39m\n\u001b[31m  }).join('');·\u001b[39m\n\u001b[31m  // Store dispatch object for confirm\u001b[39m\n\u001b[31m  pane._pendingDispatch = d;\u001b[39m\n\u001b[31m  pane.style.display = 'block';\u001b[39m\n\u001b[31m  pane.scrollTop = 0;\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction updateVerifyField(idx, field, input) {\u001b[39m\n\u001b[31m  const pane = document.getElementById('verifyPane');\u001b[39m\n\u001b[31m  if (!pane._pendingDispatch) return;\u001b[39m\n\u001b[31m  const c = pane._pendingDispatch.consignees[idx];\u001b[39m\n\u001b[31m  if (!c) return;\u001b[39m\n\u001b[31m  c[field] = input.value.trim();\u001b[39m\n\u001b[31m  input.classList.toggle('changed', input.value.trim() !== '');\u001b[39m\n\u001b[31m  // Re-check dupe if docket changed\u001b[39m\n\u001b[31m  if (field === 'docket') refreshVerifyFlags(pane._pendingDispatch);\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction refreshVerifyFlags(d) {\u001b[39m\n\u001b[31m  const seen = {};\u001b[39m\n\u001b[31m  d.consignees.forEach(c => { if (c.docket) seen[c.docket] = (seen[c.docket]||0)+1; });\u001b[39m\n\u001b[31m  d.consignees.forEach((c, i) => {\u001b[39m\n\u001b[31m    const row = document.getElementById('vrow_' + i);\u001b[39m\n\u001b[31m    if (!row) return;\u001b[39m\n\u001b[31m    const isDupe = c.docket && seen[c.docket] > 1;\u001b[39m\n\u001b[31m    row.classList.toggle('has-dupe', isDupe);\u001b[39m\n\u001b[31m  });\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction removeVerifyRow(idx) {\u001b[39m\n\u001b[31m  const pane = document.getElementById('verifyPane');\u001b[39m\n\u001b[31m  if (!pane._pendingDispatch) return;\u001b[39m\n\u001b[31m  const el = document.getElementById('vrow_' + idx);\u001b[39m\n\u001b[31m  if (el) {\u001b[39m\n\u001b[31m    el.style.transition = 'opacity .25s';\u001b[39m\n\u001b[31m    el.style.opacity = '0';\u001b[39m\n\u001b[31m    setTimeout(() => {\u001b[39m\n\u001b[31m      pane._pendingDispatch.consignees.splice(idx, 1);\u001b[39m\n\u001b[31m      showVerifyScreen(pane._pendingDispatch); // re-render with updated indices\u001b[39m\n\u001b[31m    }, 250);\u001b[39m\n\u001b[31m  } else {\u001b[39m\n\u001b[31m    pane._pendingDispatch.consignees.splice(idx, 1);\u001b[39m\n\u001b[31m    showVerifyScreen(pane._pendingDispatch);\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction approveAll() {\u001b[39m\n\u001b[31m  showToast('All rows approved ✓', 'ok');\u001b[39m\n\u001b[31m  document.getElementById('btnConfirmFinalise').style.background = 'var(--grn)';\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction closeVerify() {\u001b[39m\n\u001b[31m  const pane = document.getElementById('verifyPane');\u001b[39m\n\u001b[31m  // If this dispatch was already finalised, warn before going back\u001b[39m\n\u001b[31m  if (_lastFinalisedId && pane._pendingDispatch?.id === _lastFinalisedId) {\u001b[39m\n\u001b[31m    if (!confirm(\u001b[39m\n\u001b[31m      'This dispatch has already been finalised.\\\\n\\\\n' +\u001b[39m\n\u001b[31m      'Going back will take you to the edit form — any changes will create a NEW dispatch record.\\\\n\\\\n' +\u001b[39m\n\u001b[31m      'Go back anyway?'\u001b[39m\n\u001b[31m    )) return;\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m  pane.style.display = 'none';\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction showSlipFull(idx) {\u001b[39m\n\u001b[31m  const s = slipOCRData[idx];\u001b[39m\n\u001b[31m  if (!s?.imageData) return;\u001b[39m\n\u001b[31m  const w = window.open('');\u001b[39m\n\u001b[31m  w.document.write(`<img src=\\\"${s.imageData}\\\" style=\\\"max-width:100%;height:auto;\\\">`);\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// Log OCR correction to sheets for future accuracy improvement\u001b[39m\n\u001b[31masync function logOCRCorrection(original, corrected, courier) {\u001b[39m\n\u001b[31m  if (!window.appConfig.webhook_url || original === corrected) return;\u001b[39m\n\u001b[31m  try {\u001b[39m\n\u001b[31m    await fetch(window.appConfig.webhook_url, {\u001b[39m\n\u001b[31m      method: 'POST', mode: 'no-cors',\u001b[39m\n\u001b[31m      headers: { 'Content-Type': 'application/json' },\u001b[39m\n\u001b[31m      body: JSON.stringify({\u001b[39m\n\u001b[31m        type: 'ocr_correction',\u001b[39m\n\u001b[31m        original, corrected, courier,\u001b[39m\n\u001b[31m        timestamp: new Date().toISOString()\u001b[39m\n\u001b[31m      })\u001b[39m\n\u001b[31m    });\u001b[39m\n\u001b[31m  } catch(e) {}\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mlet _lastFinalisedId = null; // track last finalised dispatch to warn on back·\u001b[39m\n\u001b[31masync function confirmAndFinalise() {\u001b[39m\n\u001b[31m  const pane = document.getElementById('verifyPane');\u001b[39m\n\u001b[31m  const d = pane._pendingDispatch;\u001b[39m\n\u001b[31m  if (!d) return;·\u001b[39m\n\u001b[31m  d.status = 'active';\u001b[39m\n\u001b[31m  d.finalisedAt = new Date().toISOString();·\u001b[39m\n\u001b[31m  // Log OCR corrections\u001b[39m\n\u001b[31m  slipOCRData.forEach((s, i) => {\u001b[39m\n\u001b[31m    const c = d.consignees[i];\u001b[39m\n\u001b[31m    if (c && s.docket && c.docket && s.docket !== c.docket) {\u001b[39m\n\u001b[31m      logOCRCorrection(s.docket, c.docket, d.courier);\u001b[39m\n\u001b[31m    }\u001b[39m\n\u001b[31m  });·\u001b[39m\n\u001b[31m  // Replace existing draft with same ID if present, else prepend\u001b[39m\n\u001b[31m  const existingIdx = dispatches.findIndex(x => x.id === d.id);\u001b[39m\n\u001b[31m  if (existingIdx >= 0) {\u001b[39m\n\u001b[31m    dispatches[existingIdx] = d;\u001b[39m\n\u001b[31m  } else {\u001b[39m\n\u001b[31m    dispatches.unshift(d);\u001b[39m\n\u001b[31m  }·\u001b[39m\n\u001b[31m  _lastFinalisedId = d.id;\u001b[39m\n\u001b[31m  saveData();\u001b[39m\n\u001b[31m  syncDispatchToDB(d); // push to Postgres (non-blocking)\u001b[39m\n\u001b[31m  updateHistCount();\u001b[39m\n\u001b[31m  if (window.appConfig.auto_log !== 'false' && window.appConfig.webhook_url) await logToSheets(d);\u001b[39m\n\u001b[31m  showToast('Dispatch finalised ✓', 'ok');\u001b[39m\n\u001b[31m  pane.style.display = 'none';\u001b[39m\n\u001b[31m  pane._pendingDispatch = null; // clear pending\u001b[39m\n\u001b[31m  resetForm();\u001b[39m\n\u001b[31m  showPane('dashboard');\u001b[39m\n\u001b[31m  renderDashboard();\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction resetForm() {\u001b[39m\n\u001b[31m  document.getElementById('n_client').value = '';\u001b[39m\n\u001b[31m  document.getElementById('n_jobref').value = '';\u001b[39m\n\u001b[31m  document.getElementById('n_courier').value = '';\u001b[39m\n\u001b[31m  document.getElementById('n_remarks').value = '';\u001b[39m\n\u001b[31m  document.getElementById('jobPill').style.display = 'none';\u001b[39m\n\u001b[31m  document.getElementById('customClientField').style.display = 'none';\u001b[39m\n\u001b[31m  document.getElementById('consList').innerHTML = '';\u001b[39m\n\u001b[31m  addConsignee();\u001b[39m\n\u001b[31m  clearSlips();\u001b[39m\n\u001b[31m  clearStores();\u001b[39m\n\u001b[31m  setToday();\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction newDispatch() { resetForm(); showPane('new'); }·\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31m// DASHBOARD\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31mfunction renderDashboard() {\u001b[39m\n\u001b[31m  // Exclude drafts from dashboard — only show active/finalised dispatches\u001b[39m\n\u001b[31m  const activeDis = dispatches.filter(d => d.status !== 'draft');\u001b[39m\n\u001b[31m  const allCons = activeDis.flatMap(d => d.consignees || []);\u001b[39m\n\u001b[31m  const countBy = (status) => allCons.filter(c => c.status === status).length;\u001b[39m\n\u001b[31m  document.getElementById('st-delivered').textContent = countBy('delivered');\u001b[39m\n\u001b[31m  document.getElementById('st-transit').textContent = countBy('transit') + countBy('in-transit');\u001b[39m\n\u001b[31m  document.getElementById('st-booked').textContent = countBy('booked');\u001b[39m\n\u001b[31m  document.getElementById('st-pending').textContent = allCons.filter(c => !c.docket || c.status === 'pending').length;·\u001b[39m\n\u001b[31m  const grid = document.getElementById('dashGrid');\u001b[39m\n\u001b[31m  // Flatten all consignees across dispatches\u001b[39m\n\u001b[31m  let items = activeDis.flatMap(d =>\u001b[39m\n\u001b[31m    (d.consignees || []).filter(c => c.docket).map(c => ({...c, dispatchId: d.id, courier: d.courier, client: d.client, jobref: d.jobref}))\u001b[39m\n\u001b[31m  );\u001b[39m\n\u001b[31m  if (dashFilter !== 'all') {\u001b[39m\n\u001b[31m    items = items.filter(c => {\u001b[39m\n\u001b[31m      if (dashFilter === 'delivered') return c.status === 'delivered';\u001b[39m\n\u001b[31m      if (dashFilter === 'transit') return c.status === 'transit' || c.status === 'in-transit';\u001b[39m\n\u001b[31m      if (dashFilter === 'booked') return c.status === 'booked';\u001b[39m\n\u001b[31m      if (dashFilter === 'pending') return !c.docket || c.status === 'pending';\u001b[39m\n\u001b[31m      return true;\u001b[39m\n\u001b[31m    });\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m  if (!items.length) {\u001b[39m\n\u001b[31m    grid.innerHTML = '<div class=\\\"empty\\\"><div class=\\\"e-icon\\\">📦</div><p>No docket-tracked consignees.<br>Add docket numbers to see tiles.</p></div>';\u001b[39m\n\u001b[31m    return;\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m  const progMap = { delivered:100, transit:60,'in-transit':60, booked:20, pending:5 };\u001b[39m\n\u001b[31m  const progClr = { delivered:'var(--grn)', transit:'var(--ylw)','in-transit':'var(--ylw)', booked:'var(--sky)', pending:'var(--t4)' };\u001b[39m\n\u001b[31m  grid.innerHTML = items.map(c => {\u001b[39m\n\u001b[31m    const sp = c.status || 'booked';\u001b[39m\n\u001b[31m    const prog = progMap[sp] || 20;\u001b[39m\n\u001b[31m    const clr = progClr[sp] || 'var(--t4)';\u001b[39m\n\u001b[31m    return `\u001b[39m\n\u001b[31m    <div class=\\\"docket-tile\\\" onclick=\\\"showDocketDetail('${c.docket}','${c.courier||''}','${c.dispatchId}')\\\">\u001b[39m\n\u001b[31m      <div class=\\\"docket-tile-hdr\\\">\u001b[39m\n\u001b[31m        <div class=\\\"docket-tile-left\\\">\u001b[39m\n\u001b[31m          <div class=\\\"docket-num\\\">${c.docket}</div>\u001b[39m\n\u001b[31m          <div class=\\\"docket-store\\\">${c.name} — ${c.city}</div>\u001b[39m\n\u001b[31m          <div class=\\\"docket-courier\\\">${c.courier||''} · ${c.client||''} ${c.jobref ? '› '+c.jobref : ''}</div>\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m        <span class=\\\"status-pill sp-${sp}\\\">${sp}</span>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      <div style=\\\"display:flex;gap:6px;margin-bottom:6px;align-items:center;\\\">\u001b[39m\n\u001b[31m        <input type=\\\"checkbox\\\" id=\\\"sel_${c.docket.replace(/[^a-z0-9]/gi,'_')}\\\"\u001b[39m\n\u001b[31m          onclick=\\\"event.stopPropagation();toggleTileSelect('${c.docket}')\\\"\u001b[39m\n\u001b[31m          style=\\\"width:14px;height:14px;cursor:pointer;accent-color:var(--acc);\\\">\u001b[39m\n\u001b[31m        <label style=\\\"font-size:.6rem;color:var(--t3);font-family:var(--fnt-mono);cursor:pointer;\\\"\u001b[39m\n\u001b[31m          onclick=\\\"event.stopPropagation();toggleTileSelect('${c.docket}')\\\">Select</label>\u001b[39m\n\u001b[31m        ${c.manualStatus ?\u001b[39m\n\u001b[31m          `<span style=\\\"font-size:.6rem;font-family:var(--fnt-mono);color:var(--grn);\\\">📞 ${c.manualStatus}</span>` : ''}\u001b[39m\n\u001b[31m        ${c.podUploaded ? '<span style=\\\"font-size:.6rem;color:var(--grn);font-family:var(--fnt-mono);\\\">✓ POD</span>' : ''}\u001b[39m\n\u001b[31m        <button onclick=\\\"event.stopPropagation();showManualTracking('${c.docket}','${c.dispatchId}','${c.name}','${c.phone||''}','${c.city||''}')\\\"\u001b[39m\n\u001b[31m          style=\\\"font-size:.6rem;background:rgba(56,189,248,.1);border:1px solid rgba(56,189,248,.3);\u001b[39m\n\u001b[31m          color:var(--sky);padding:2px 7px;border-radius:10px;cursor:pointer;font-family:var(--fnt-mono);\\\">📞 Call Log</button>\u001b[39m\n\u001b[31m        <button onclick=\\\"event.stopPropagation();showPODUpload('${c.docket}','${c.dispatchId}')\\\"\u001b[39m\n\u001b[31m          style=\\\"font-size:.6rem;background:none;border:1px solid var(--bdr);color:var(--t3);\u001b[39m\n\u001b[31m          padding:2px 7px;border-radius:10px;cursor:pointer;font-family:var(--fnt-mono);\\\">📸 POD</button>\u001b[39m\n\u001b[31m        ${c.manualDelivered ? `<button onclick=\\\"event.stopPropagation();resumeAutoTracking('${c.docket}','${c.dispatchId}')\\\"\u001b[39m\n\u001b[31m          style=\\\"font-size:.6rem;background:rgba(251,146,60,.1);border:1px solid rgba(251,146,60,.4);\u001b[39m\n\u001b[31m          color:#fb923c;padding:2px 7px;border-radius:10px;cursor:pointer;font-family:var(--fnt-mono);\\\">↩ Undo</button>` : ''}\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      <div class=\\\"docket-tile-footer\\\">\u001b[39m\n\u001b[31m        <div style=\\\"flex:1;min-width:0;\\\">\u001b[39m\n\u001b[31m          <span class=\\\"docket-last-event docket-evt-${c.docket}\\\" id=\\\"evt_${c.docket.replace(/[^a-z0-9]/gi,'_')}\\\">${c.lastEvent || 'No tracking yet'}</span>\u001b[39m\n\u001b[31m          <span class=\\\"track-spinner\\\" id=\\\"spin_${c.docket.replace(/[^a-z0-9]/gi,'_')}\\\" style=\\\"display:none;color:var(--ylw);font-family:var(--fnt-mono);font-size:.68rem;animation:blink 1s infinite;\\\">⏳ tracking...</span>\u001b[39m\n\u001b[31m          ${c.lastScanLocation && c.lastScanLocation !== c.lastEvent ?·\u001b[39m\n\u001b[31m            `<div style=\\\"font-size:.62rem;color:var(--t3);font-family:var(--fnt-mono);margin-top:2px;\\\">📍 ${c.lastScanLocation}</div>` : ''}\u001b[39m\n\u001b[31m          ${c.lastTrackedAt ?·\u001b[39m\n\u001b[31m            `<div style=\\\"font-size:.6rem;color:var(--t4);font-family:var(--fnt-mono);margin-top:1px;\\\">🕐 tracked ${new Date(c.lastTrackedAt).toLocaleTimeString('en-IN',{hour:'2-digit',minute:'2-digit'})}</div>` : ''}\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m        <button class=\\\"btn-track-live live-btn-${c.docket.replace(/[^a-z0-9]/gi,'_')}\\\" onclick=\\\"event.stopPropagation();liveTrackTile('${c.docket}','${c.courier||''}','${c.dispatchId}')\\\">\u001b[39m\n\u001b[31m          <span class=\\\"live-dot\\\"></span>LIVE\u001b[39m\n\u001b[31m        </button>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      <div class=\\\"docket-progress\\\">\u001b[39m\n\u001b[31m        <div class=\\\"docket-progress-fill\\\" style=\\\"width:${prog}%;background:${clr};\\\"></div>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m    </div>`;\u001b[39m\n\u001b[31m  }).join('');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction filterDash(f, el) {\u001b[39m\n\u001b[31m  dashFilter = f;\u001b[39m\n\u001b[31m  document.querySelectorAll('#dashFilters .btn-sm').forEach(b => b.classList.remove('primary'));\u001b[39m\n\u001b[31m  el.classList.add('primary');\u001b[39m\n\u001b[31m  renderDashboard();\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31masync function liveTrackTile(docket, courier, dispatchId) {\u001b[39m\n\u001b[31m  showToast(`Tracking ${docket}...`, 'info');\u001b[39m\n\u001b[31m  const result = await fetchTracking(docket, courier);\u001b[39m\n\u001b[31m  // Update status in dispatches\u001b[39m\n\u001b[31m  for (const d of dispatches) {\u001b[39m\n\u001b[31m    if (d.id !== dispatchId) continue;\u001b[39m\n\u001b[31m    for (const c of d.consignees) {\u001b[39m\n\u001b[31m      if (c.docket === docket) {\u001b[39m\n\u001b[31m        c.status = result.status || c.status;\u001b[39m\n\u001b[31m        c.lastEvent = result.lastEvent || c.lastEvent;\u001b[39m\n\u001b[31m        c.lastUpdated = new Date().toISOString();\u001b[39m\n\u001b[31m      }\u001b[39m\n\u001b[31m    }\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m  saveData();\u001b[39m\n\u001b[31m  renderDashboard();\u001b[39m\n\u001b[31m  if (result.status === 'failed') {\u001b[39m\n\u001b[31m    // Open courier site as fallback\u001b[39m\n\u001b[31m    openCourierTrackUrl(docket, courier);\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction showDocketDetail(docket, courier, dispatchId) {\u001b[39m\n\u001b[31m  const d = dispatches.find(x => x.id === dispatchId);\u001b[39m\n\u001b[31m  const c = d?.consignees?.find(x => x.docket === docket);\u001b[39m\n\u001b[31m  if (!c) return;\u001b[39m\n\u001b[31m  const content = document.getElementById('detailContent');\u001b[39m\n\u001b[31m  content.innerHTML = `\u001b[39m\n\u001b[31m    <div class=\\\"sheet-title\\\">${docket}</div>\u001b[39m\n\u001b[31m    <div class=\\\"info-tag\\\">📍 <strong>${c.name}</strong> — ${c.city}</div>\u001b[39m\n\u001b[31m    ${c.phone ? `<div class=\\\"info-tag\\\">📞 ${c.phone}</div>` : ''}\u001b[39m\n\u001b[31m    <div class=\\\"info-tag\\\">📦 ${c.material||'—'} ${c.qty ? '('+c.qty+' pcs)':''}</div>\u001b[39m\n\u001b[31m    <div class=\\\"info-tag\\\">🚚 ${courier||'—'} · <span class=\\\"status-pill sp-${c.status||'pending'}\\\">${c.status||'pending'}</span></div>\u001b[39m\n\u001b[31m    ${c.lastEvent ? `<div class=\\\"info-tag\\\">🕐 ${c.lastEvent}</div>` : ''}\u001b[39m\n\u001b[31m    <div class=\\\"sec-div\\\"><span>Tracking History</span></div>\u001b[39m\n\u001b[31m    <div id=\\\"docketTimeline\\\">\u001b[39m\n\u001b[31m      ${c.timeline?.length ?\u001b[39m\n\u001b[31m        c.timeline.map(e => `<div class=\\\"track-evt\\\"><span class=\\\"track-time\\\">${e.time||''}</span><div><div class=\\\"track-desc\\\">${e.desc||''}</div><div class=\\\"track-loc\\\">${e.loc||''}</div></div></div>`).join('') :\u001b[39m\n\u001b[31m        '<div style=\\\"color:var(--t3);font-size:.78rem;padding:10px 0;\\\">No timeline data. Click Live Track to fetch.</div>'}\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m    <div style=\\\"display:flex;gap:6px;flex-wrap:wrap;margin-top:14px;\\\">\u001b[39m\n\u001b[31m      <button class=\\\"btn btn-ghost\\\" style=\\\"flex:1;\\\" onclick=\\\"openCourierTrackUrl('${docket}','${courier}')\\\">🔗 Courier Site</button>\u001b[39m\n\u001b[31m      <button class=\\\"btn btn-orange\\\" style=\\\"flex:1;\\\" onclick=\\\"liveTrackTile('${docket}','${courier}','${dispatchId}');closeOverlay('detailOverlay')\\\">⚡ Live Track</button>\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m    <div style=\\\"display:flex;gap:6px;flex-wrap:wrap;margin-top:6px;\\\">\u001b[39m\n\u001b[31m      <button class=\\\"btn-wa\\\" style=\\\"flex:1;padding:8px;\\\" onclick=\\\"closeOverlay('detailOverlay');showPODUpload('${docket}','${dispatchId}')\\\">📸 Upload POD</button>\u001b[39m\n\u001b[31m      ${c.podUploaded ? '<span style=\\\"color:var(--grn);font-size:.72rem;font-family:var(--fnt-mono);padding:6px;\\\">✓ POD uploaded</span>' : ''}\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m  `;\u001b[39m\n\u001b[31m  openOverlay('detailOverlay');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31m// TRACKING ENGINE\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31mfunction _normaliseStatus(raw) {\u001b[39m\n\u001b[31m  const s = (raw || '').toLowerCase().trim();\u001b[39m\n\u001b[31m  if (!s || s === 'error') return null;\u001b[39m\n\u001b[31m  if (s.includes('undeliver') || s.includes('delivery_fail') ||\u001b[39m\n\u001b[31m      s.includes('failed_delivery') || s.includes('not deliver') ||\u001b[39m\n\u001b[31m      s.includes('return')  || s.includes('rto')) return 'pending';\u001b[39m\n\u001b[31m  if (s.includes('deliver'))                       return 'delivered';\u001b[39m\n\u001b[31m  if (s.includes('transit') || s.includes('hub') ||\u001b[39m\n\u001b[31m      s.includes('inbound') || s.includes('outbound') ||\u001b[39m\n\u001b[31m      s.includes('out for'))                       return 'transit';\u001b[39m\n\u001b[31m  if (s.includes('book') || s.includes('pick'))   return 'booked';\u001b[39m\n\u001b[31m  return s;\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31masync function _fetchWithRetry(url, opts, maxRetries = 1, retryDelayMs = 4000) {\u001b[39m\n\u001b[31m  for (let attempt = 0; attempt <= maxRetries; attempt++) {\u001b[39m\n\u001b[31m    try {\u001b[39m\n\u001b[31m      const resp = await fetch(url, opts);\u001b[39m\n\u001b[31m      if (resp.ok) return resp;\u001b[39m\n\u001b[31m    } catch(e) {\u001b[39m\n\u001b[31m      if (attempt === maxRetries) throw e;\u001b[39m\n\u001b[31m      await new Promise(r => setTimeout(r, retryDelayMs));\u001b[39m\n\u001b[31m    }\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m  return null;\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31masync function fetchTracking(docket, courier) {\u001b[39m\n\u001b[31m  // Strategy 1: VPS proxy (SkyKing + others that block direct fetch)\u001b[39m\n\u001b[31m  const proxyUrl = window.appConfig.proxy_url;\u001b[39m\n\u001b[31m  if (proxyUrl) {\u001b[39m\n\u001b[31m    try {\u001b[39m\n\u001b[31m      const url  = `${proxyUrl}?courier=${encodeURIComponent(courier)}&docket=${encodeURIComponent(docket)}`;\u001b[39m\n\u001b[31m      const resp = await _fetchWithRetry(url, { signal: AbortSignal.timeout(15000) });\u001b[39m\n\u001b[31m      if (resp) {\u001b[39m\n\u001b[31m        const data      = await resp.json();\u001b[39m\n\u001b[31m        const normStatus = _normaliseStatus(data.status);\u001b[39m\n\u001b[31m        if (normStatus) {\u001b[39m\n\u001b[31m          return { status: normStatus, lastEvent: data.lastEvent || '', timeline: data.timeline || [] };\u001b[39m\n\u001b[31m        }\u001b[39m\n\u001b[31m        openCourierTrackUrl(docket, courier);\u001b[39m\n\u001b[31m        return { status: 'transit', lastEvent: data.lastEvent || 'Check courier site', timeline: [] };\u001b[39m\n\u001b[31m      }\u001b[39m\n\u001b[31m    } catch(e) {}\u001b[39m\n\u001b[31m  }·\u001b[39m\n\u001b[31m  // Strategy 2: Delhivery direct API\u001b[39m\n\u001b[31m  if (courier === 'Delhivery') {\u001b[39m\n\u001b[31m    try {\u001b[39m\n\u001b[31m      const resp = await _fetchWithRetry(\u001b[39m\n\u001b[31m        `https://dlv-api.delhivery.com/v3/track?waybill=${docket}`,\u001b[39m\n\u001b[31m        { signal: AbortSignal.timeout(8000) }\u001b[39m\n\u001b[31m      );\u001b[39m\n\u001b[31m      if (resp) {\u001b[39m\n\u001b[31m        const data = await resp.json();\u001b[39m\n\u001b[31m        const pkg  = data.ShipmentData?.[0]?.Shipment;\u001b[39m\n\u001b[31m        if (pkg) {\u001b[39m\n\u001b[31m          const normStatus = _normaliseStatus(pkg.Status?.Status);\u001b[39m\n\u001b[31m          return { status: normStatus || 'transit', lastEvent: pkg.Status?.Instructions || '', timeline: [] };\u001b[39m\n\u001b[31m        }\u001b[39m\n\u001b[31m      }\u001b[39m\n\u001b[31m    } catch(e) {}\u001b[39m\n\u001b[31m  }·\u001b[39m\n\u001b[31m  return { status: 'failed', lastEvent: 'Open courier site to check', timeline: [] };\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction openCourierTrackUrl(docket, courier) {\u001b[39m\n\u001b[31m  const m = COURIER_MASTER[courier];\u001b[39m\n\u001b[31m  if (!m) return;\u001b[39m\n\u001b[31m  const url = m.trackUrl.replace('{docket}', docket);\u001b[39m\n\u001b[31m  window.open(url, '_blank');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// Track tab - multiple dockets\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31m// TRACK TAB — new logic\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31mlet _trackSelected = new Set(); // dockets currently selected for tracking·\u001b[39m\n\u001b[31m// Called when Track tab opens or courier changes\u001b[39m\n\u001b[31mfunction buildTrackDocketChips() {\u001b[39m\n\u001b[31m  const courier = document.getElementById('t_courier').value;\u001b[39m\n\u001b[31m  const m = COURIER_MASTER[courier];\u001b[39m\n\u001b[31m  const maxBulk = m?.maxBulk || 1;\u001b[39m\n\u001b[31m  const limitEl = document.getElementById('trackCourierLimit');\u001b[39m\n\u001b[31m  if (limitEl) limitEl.textContent = `Max ${maxBulk} at once for ${courier}`;·\u001b[39m\n\u001b[31m  // Gather all dockets from active dispatches for this courier\u001b[39m\n\u001b[31m  const activeDis = dispatches.filter(d => d.status !== 'draft');\u001b[39m\n\u001b[31m  const allDockets = activeDis.flatMap(d =>\u001b[39m\n\u001b[31m    (d.consignees || [])\u001b[39m\n\u001b[31m      .filter(c => c.docket && d.courier === courier)\u001b[39m\n\u001b[31m      .map(c => ({ docket: c.docket, name: c.name, city: c.city, status: c.status || 'booked' }))\u001b[39m\n\u001b[31m  );·\u001b[39m\n\u001b[31m  // Also include dockets from OTHER couriers as un-selectable reference\u001b[39m\n\u001b[31m  const otherDockets = activeDis.flatMap(d =>\u001b[39m\n\u001b[31m    (d.consignees || [])\u001b[39m\n\u001b[31m      .filter(c => c.docket && d.courier !== courier)\u001b[39m\n\u001b[31m      .map(c => ({ docket: c.docket, name: c.name, courier: d.courier }))\u001b[39m\n\u001b[31m  );·\u001b[39m\n\u001b[31m  const chips = document.getElementById('trackDocketChips');\u001b[39m\n\u001b[31m  if (!allDockets.length && !otherDockets.length) {\u001b[39m\n\u001b[31m    chips.innerHTML = '<span style=\\\"font-size:.72rem;color:var(--t3);font-family:var(--fnt-mono);\\\">No dispatched dockets yet — enter manually below</span>';\u001b[39m\n\u001b[31m    return;\u001b[39m\n\u001b[31m  }·\u001b[39m\n\u001b[31m  const spClr = { delivered:'var(--grn)', transit:'var(--ylw)', booked:'var(--sky)', pending:'var(--t4)' };·\u001b[39m\n\u001b[31m  chips.innerHTML = [\u001b[39m\n\u001b[31m    // This courier's dockets\u001b[39m\n\u001b[31m    ...allDockets.map(d => {\u001b[39m\n\u001b[31m      const sel = _trackSelected.has(d.docket);\u001b[39m\n\u001b[31m      const clr = spClr[d.status] || 'var(--t4)';\u001b[39m\n\u001b[31m      return `<button onclick=\\\"toggleTrackDocket('${d.docket}')\\\" id=\\\"chip_${d.docket}\\\"\u001b[39m\n\u001b[31m        style=\\\"font-family:var(--fnt-mono);font-size:.72rem;padding:4px 10px;border-radius:20px;\u001b[39m\n\u001b[31m        cursor:pointer;transition:all .2s;border:1px solid ${sel ? 'var(--acc)' : 'var(--bdr)'};\u001b[39m\n\u001b[31m        background:${sel ? 'rgba(249,115,22,.15)' : 'var(--sur2)'};\u001b[39m\n\u001b[31m        color:${sel ? 'var(--acc)' : 'var(--t2)'};\\\">\u001b[39m\n\u001b[31m        ${d.docket} <span style=\\\"font-size:.6rem;color:${clr};\\\">● ${d.status}</span>\u001b[39m\n\u001b[31m        ${d.city ? `<span style=\\\"font-size:.6rem;color:var(--t3);margin-left:4px;\\\">${d.city}</span>` : ''}\u001b[39m\n\u001b[31m      </button>`;\u001b[39m\n\u001b[31m    }),\u001b[39m\n\u001b[31m    // Other couriers — show greyed, click selects and switches courier\u001b[39m\n\u001b[31m    ...otherDockets.slice(0, 5).map(d =>\u001b[39m\n\u001b[31m      `<span title=\\\"${d.courier}\\\" style=\\\"font-family:var(--fnt-mono);font-size:.68rem;padding:3px 8px;\u001b[39m\n\u001b[31m        border-radius:20px;border:1px dashed var(--bdr);color:var(--t4);cursor:default;\\\">\u001b[39m\n\u001b[31m        ${d.docket} <span style=\\\"font-size:.55rem;\\\">${d.courier}</span>\u001b[39m\n\u001b[31m      </span>`\u001b[39m\n\u001b[31m    )\u001b[39m\n\u001b[31m  ].join('');·\u001b[39m\n\u001b[31m  updateTrackSummary();\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction toggleTrackDocket(docket) {\u001b[39m\n\u001b[31m  const courier = document.getElementById('t_courier').value;\u001b[39m\n\u001b[31m  const m = COURIER_MASTER[courier];\u001b[39m\n\u001b[31m  const maxBulk = m?.maxBulk || 1;·\u001b[39m\n\u001b[31m  if (_trackSelected.has(docket)) {\u001b[39m\n\u001b[31m    _trackSelected.delete(docket);\u001b[39m\n\u001b[31m  } else {\u001b[39m\n\u001b[31m    if (_trackSelected.size >= maxBulk) {\u001b[39m\n\u001b[31m      showToast(`${courier} supports max ${maxBulk} docket(s) at once`, 'err');\u001b[39m\n\u001b[31m      return;\u001b[39m\n\u001b[31m    }\u001b[39m\n\u001b[31m    _trackSelected.add(docket);\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m  buildTrackDocketChips(); // re-render chips with updated selection\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction addManualDockets() {\u001b[39m\n\u001b[31m  const raw = document.getElementById('t_manual_dockets').value;\u001b[39m\n\u001b[31m  const courier = document.getElementById('t_courier').value;\u001b[39m\n\u001b[31m  const m = COURIER_MASTER[courier];\u001b[39m\n\u001b[31m  const maxBulk = m?.maxBulk || 1;\u001b[39m\n\u001b[31m  const newDockets = raw.split(/[,\\\\n]/).map(d => d.trim()).filter(Boolean);\u001b[39m\n\u001b[31m  let added = 0;\u001b[39m\n\u001b[31m  for (const d of newDockets) {\u001b[39m\n\u001b[31m    if (_trackSelected.size >= maxBulk) {\u001b[39m\n\u001b[31m      showToast(`Max ${maxBulk} reached for ${courier}`, 'err');\u001b[39m\n\u001b[31m      break;\u001b[39m\n\u001b[31m    }\u001b[39m\n\u001b[31m    _trackSelected.add(d);\u001b[39m\n\u001b[31m    added++;\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m  document.getElementById('t_manual_dockets').value = '';\u001b[39m\n\u001b[31m  buildTrackDocketChips();\u001b[39m\n\u001b[31m  if (added) showToast(`${added} docket(s) added ✓`, 'ok');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction clearTrackSelection() {\u001b[39m\n\u001b[31m  _trackSelected.clear();\u001b[39m\n\u001b[31m  buildTrackDocketChips();\u001b[39m\n\u001b[31m  document.getElementById('trackResultsArea').innerHTML = '';\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction updateTrackSummary() {\u001b[39m\n\u001b[31m  const summary = document.getElementById('trackSelectedSummary');\u001b[39m\n\u001b[31m  const countEl = document.getElementById('trackSelectedCount');\u001b[39m\n\u001b[31m  const listEl  = document.getElementById('trackSelectedList');\u001b[39m\n\u001b[31m  if (!summary) return;\u001b[39m\n\u001b[31m  const arr = Array.from(_trackSelected);\u001b[39m\n\u001b[31m  summary.style.display = arr.length ? 'block' : 'none';\u001b[39m\n\u001b[31m  if (countEl) countEl.textContent = `${arr.length} docket(s) selected`;\u001b[39m\n\u001b[31m  if (listEl)  listEl.textContent  = arr.slice(0, 5).join(', ') + (arr.length > 5 ? `... +${arr.length-5}` : '');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction openTrackOnCourierSite() {\u001b[39m\n\u001b[31m  const courier = document.getElementById('t_courier').value;\u001b[39m\n\u001b[31m  const dockets = Array.from(_trackSelected);\u001b[39m\n\u001b[31m  if (!dockets.length) { showToast('Select dockets first', 'err'); return; }\u001b[39m\n\u001b[31m  const m = COURIER_MASTER[courier];\u001b[39m\n\u001b[31m  if (!m) return;\u001b[39m\n\u001b[31m  // SkyKing: open with all CNOs comma-separated in URL\u001b[39m\n\u001b[31m  let url;\u001b[39m\n\u001b[31m  if (courier === 'SkyKing') {\u001b[39m\n\u001b[31m    url = `https://skyking.co/track?cno=${dockets.join(',')}`;\u001b[39m\n\u001b[31m  } else if (dockets.length === 1) {\u001b[39m\n\u001b[31m    url = m.trackUrl.replace('{docket}', dockets[0]);\u001b[39m\n\u001b[31m  } else {\u001b[39m\n\u001b[31m    url = m.trackBulkUrl || m.trackUrl.replace('{docket}', dockets[0]);\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m  window.open(url, '_blank');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31masync function trackSelected() {\u001b[39m\n\u001b[31m  const courier = document.getElementById('t_courier').value;\u001b[39m\n\u001b[31m  const dockets = Array.from(_trackSelected);\u001b[39m\n\u001b[31m  if (!dockets.length) { showToast('Select at least one docket', 'err'); return; }·\u001b[39m\n\u001b[31m  const area = document.getElementById('trackResultsArea');\u001b[39m\n\u001b[31m  area.innerHTML = `<div class=\\\"card\\\" style=\\\"text-align:center;padding:20px;\\\">\u001b[39m\n\u001b[31m    <div style=\\\"font-size:1.5rem;margin-bottom:8px;\\\">⏳</div>\u001b[39m\n\u001b[31m    <div style=\\\"font-family:var(--fnt-mono);font-size:.78rem;color:var(--t2);\\\">Tracking ${dockets.length} docket(s) via ${courier}...</div>\u001b[39m\n\u001b[31m  </div>`;·\u001b[39m\n\u001b[31m  let resultsHTML = '';\u001b[39m\n\u001b[31m  for (const docket of dockets) {\u001b[39m\n\u001b[31m    const result = await fetchTracking(docket, courier);\u001b[39m\n\u001b[31m    const sp = (!result.status || result.status === 'failed') ? 'pending' : result.status;\u001b[39m\n\u001b[31m    const spClr = { delivered:'var(--grn)', transit:'var(--ylw)', booked:'var(--sky)', pending:'var(--t3)' };\u001b[39m\n\u001b[31m    const clr = spClr[sp] || 'var(--t3)';\u001b[39m\n\u001b[31m    // Update stored dispatch if this docket exists in our records\u001b[39m\n\u001b[31m    for (const d of dispatches) {\u001b[39m\n\u001b[31m      for (const c of (d.consignees||[])) {\u001b[39m\n\u001b[31m        if (c.docket === docket && result.status && result.status !== 'failed') {\u001b[39m\n\u001b[31m          c.status = result.status;\u001b[39m\n\u001b[31m          c.lastEvent = result.lastEvent;\u001b[39m\n\u001b[31m          c.lastTrackedAt = new Date().toISOString();\u001b[39m\n\u001b[31m        }\u001b[39m\n\u001b[31m      }\u001b[39m\n\u001b[31m    }\u001b[39m\n\u001b[31m    resultsHTML += `\u001b[39m\n\u001b[31m    <div style=\\\"background:var(--sur);border:1px solid var(--bdr);border-left:3px solid ${clr};\u001b[39m\n\u001b[31m      border-radius:var(--r-sm);padding:10px 12px;margin-bottom:8px;\\\">\u001b[39m\n\u001b[31m      <div style=\\\"display:flex;justify-content:space-between;align-items:center;margin-bottom:6px;\\\">\u001b[39m\n\u001b[31m        <span style=\\\"font-family:var(--fnt-mono);font-size:.9rem;color:var(--sky);\\\">${docket}</span>\u001b[39m\n\u001b[31m        <span class=\\\"status-pill sp-${sp}\\\">${sp}</span>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      <div style=\\\"font-size:.75rem;color:var(--t2);font-family:var(--fnt-mono);\\\">${result.lastEvent || '—'}</div>\u001b[39m\n\u001b[31m      ${result.lastScanLocation ? `<div style=\\\"font-size:.68rem;color:var(--t3);margin-top:3px;\\\">📍 ${result.lastScanLocation}</div>` : ''}\u001b[39m\n\u001b[31m      <div style=\\\"display:flex;gap:8px;margin-top:8px;\\\">\u001b[39m\n\u001b[31m        <button class=\\\"btn-sm\\\" onclick=\\\"openCourierTrackUrl('${docket}','${courier}')\\\">🔗 Open on ${courier}</button>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m    </div>`;\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m  saveData();\u001b[39m\n\u001b[31m  area.innerHTML = `<div style=\\\"margin-top:4px;\\\">${resultsHTML}</div>`;\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// Keep old names as aliases for compatibility\u001b[39m\n\u001b[31mfunction trackMultiple() { trackSelected(); }\u001b[39m\n\u001b[31mfunction openCourierSite() { openTrackOnCourierSite(); }·\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31m// HISTORY\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31mfunction renderHistory() {\u001b[39m\n\u001b[31m  const list = document.getElementById('historyList');\u001b[39m\n\u001b[31m  if (!dispatches.length) {\u001b[39m\n\u001b[31m    list.innerHTML = '<div class=\\\"empty\\\"><div class=\\\"e-icon\\\">📭</div><p>No dispatches yet.</p></div>';\u001b[39m\n\u001b[31m    return;\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m  list.innerHTML = dispatches.map(d => {\u001b[39m\n\u001b[31m    const cons = d.consignees?.length || 0;\u001b[39m\n\u001b[31m    const docketed = d.consignees?.filter(c=>c.docket).length || 0;\u001b[39m\n\u001b[31m    const clr = d.status === 'draft' ? 'var(--ylw)' : 'var(--grn)';\u001b[39m\n\u001b[31m    return `\u001b[39m\n\u001b[31m    <div class=\\\"docket-tile\\\" onclick=\\\"showDispatchDetail('${d.id}')\\\">\u001b[39m\n\u001b[31m      <div class=\\\"docket-tile-hdr\\\">\u001b[39m\n\u001b[31m        <div class=\\\"docket-tile-left\\\">\u001b[39m\n\u001b[31m          <div class=\\\"docket-num\\\" style=\\\"font-size:.78rem;\\\">${d.id}</div>\u001b[39m\n\u001b[31m          <div class=\\\"docket-store\\\">${d.client||'No client'} ${d.jobref ? '› '+d.jobref : ''}</div>\u001b[39m\n\u001b[31m          <div class=\\\"docket-courier\\\">📦 ${cons} consignees · ${docketed} docketed · ${d.courier||'—'} · ${d.dispatchDate||d.createdAt?.slice(0,10)||'—'}</div>\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m        <span class=\\\"status-pill\\\" style=\\\"background:rgba(0,0,0,.2);color:${clr};\\\">${d.status||'active'}</span>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m    </div>`;\u001b[39m\n\u001b[31m  }).join('');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction showDispatchDetail(id) {\u001b[39m\n\u001b[31m  const d = dispatches.find(x => x.id === id);\u001b[39m\n\u001b[31m  if (!d) return;\u001b[39m\n\u001b[31m  const content = document.getElementById('detailContent');\u001b[39m\n\u001b[31m  content.innerHTML = `\u001b[39m\n\u001b[31m    <div class=\\\"sheet-title\\\">${d.client} ${d.jobref ? '› '+d.jobref : ''}</div>\u001b[39m\n\u001b[31m    <div class=\\\"info-tag\\\"><strong>${d.id}</strong> · ${d.courier||'—'} · ${d.dispatchDate||'—'}</div>\u001b[39m\n\u001b[31m    <div class=\\\"sec-div\\\"><span>Consignees (${d.consignees?.length||0})</span></div>\u001b[39m\n\u001b[31m    ${(d.consignees||[]).map((c,i) => `\u001b[39m\n\u001b[31m      <div style=\\\"background:var(--sur2);border:1px solid var(--bdr);border-radius:var(--r-sm);padding:10px;margin-bottom:6px;\\\">\u001b[39m\n\u001b[31m        <div style=\\\"font-weight:600;margin-bottom:4px;\\\">${i+1}. ${c.name} — ${c.city}</div>\u001b[39m\n\u001b[31m        ${c.docket ? `<div style=\\\"font-family:var(--fnt-mono);font-size:.78rem;color:var(--sky);\\\">🔖 ${c.docket}</div>` : ''}\u001b[39m\n\u001b[31m        ${c.material ? `<div style=\\\"font-size:.75rem;color:var(--t2);\\\">📦 ${c.material} ${c.qty ? '('+c.qty+')':''}</div>` : ''}\u001b[39m\n\u001b[31m        <span class=\\\"status-pill sp-${c.status||'pending'}\\\" style=\\\"margin-top:4px;display:inline-block;\\\">${c.status||'pending'}</span>\u001b[39m\n\u001b[31m      </div>`).join('')}\u001b[39m\n\u001b[31m    ${d.remarks ? `<div class=\\\"info-tag\\\">📝 ${d.remarks}</div>` : ''}\u001b[39m\n\u001b[31m    <div class=\\\"btn-row\\\" style=\\\"margin-top:14px;\\\">\u001b[39m\n\u001b[31m      <button class=\\\"btn btn-ghost\\\" onclick=\\\"deleteDispatch('${d.id}');closeOverlay('detailOverlay')\\\">🗑️ Delete</button>\u001b[39m\n\u001b[31m      <button class=\\\"btn btn-orange\\\" onclick=\\\"closeOverlay('detailOverlay');showPane('dashboard')\\\">📊 View in Dashboard</button>\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m  `;\u001b[39m\n\u001b[31m  openOverlay('detailOverlay');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction deleteDispatch(id) {\u001b[39m\n\u001b[31m  if (!confirm('Delete this dispatch?')) return;\u001b[39m\n\u001b[31m  dispatches = dispatches.filter(d => d.id !== id);\u001b[39m\n\u001b[31m  saveData();\u001b[39m\n\u001b[31m  updateHistCount();\u001b[39m\n\u001b[31m  renderHistory();\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction updateHistCount() {\u001b[39m\n\u001b[31m  document.getElementById('histCount').textContent = dispatches.length;\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31m// GOOGLE SHEETS\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31masync function logToSheets(d) {\u001b[39m\n\u001b[31m  const url = window.appConfig.webhook_url;\u001b[39m\n\u001b[31m  if (!url) return;\u001b[39m\n\u001b[31m  try {\u001b[39m\n\u001b[31m    await fetch(url, {\u001b[39m\n\u001b[31m      method: 'POST', mode: 'no-cors',\u001b[39m\n\u001b[31m      headers: { 'Content-Type': 'application/json' },\u001b[39m\n\u001b[31m      body: JSON.stringify({\u001b[39m\n\u001b[31m        type: 'dispatch', id: d.id, client: d.client, jobref: d.jobref,\u001b[39m\n\u001b[31m        courier: d.courier, consignees: d.consignees?.length||0,\u001b[39m\n\u001b[31m        destinations: d.consignees?.map(c=>c.city).join(', ')||'',\u001b[39m\n\u001b[31m        dockets: d.consignees?.filter(c=>c.docket).map(c=>c.docket).join(', ')||'',\u001b[39m\n\u001b[31m        dispatchDate: d.dispatchDate, status: d.status,\u001b[39m\n\u001b[31m        timestamp: new Date().toISOString(), company: window.appConfig.company\u001b[39m\n\u001b[31m      })\u001b[39m\n\u001b[31m    });\u001b[39m\n\u001b[31m  } catch(e) {}\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31m// CONFIG — Google Sheet is the source of truth\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════·\u001b[39m\n\u001b[31mfunction onSheetsUrlChange() {\u001b[39m\n\u001b[31m  const url = document.getElementById('cfg_sheets_url').value.trim();\u001b[39m\n\u001b[31m  // Show sync button state\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction saveSheetsUrl() {\u001b[39m\n\u001b[31m  const url = document.getElementById('cfg_sheets_url').value.trim();\u001b[39m\n\u001b[31m  if (!url) { showToast('Enter the Apps Script URL first', 'err'); return; }\u001b[39m\n\u001b[31m  window.appConfig.sheets_url = url;\u001b[39m\n\u001b[31m  saveSheetsUrlLocal();\u001b[39m\n\u001b[31m  syncFromSheet(false);\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m/**\u001b[39m\n\u001b[31m * Fetch all config from Google Sheet and apply to appConfig.\u001b[39m\n\u001b[31m * Called on startup (silent=true) and manually (silent=false).\u001b[39m\n\u001b[31m */\u001b[39m\n\u001b[31masync function syncFromSheet(silent = false) {\u001b[39m\n\u001b[31m  const url = window.appConfig.sheets_url;\u001b[39m\n\u001b[31m  if (!url) { if (!silent) showToast('No Sheets URL configured', 'err'); return; }·\u001b[39m\n\u001b[31m  setSyncStatus('syncing', 'Syncing config from Google Sheet...');\u001b[39m\n\u001b[31m  try {\u001b[39m\n\u001b[31m    const resp = await fetch(url + '?action=pwa_config', {\u001b[39m\n\u001b[31m      method: 'GET',\u001b[39m\n\u001b[31m      redirect: 'follow'\u001b[39m\n\u001b[31m    });\u001b[39m\n\u001b[31m    if (!resp.ok) throw new Error('HTTP ' + resp.status);\u001b[39m\n\u001b[31m    const cfg = await resp.json();·\u001b[39m\n\u001b[31m    // Apply all values from sheet to appConfig\u001b[39m\n\u001b[31m    const keyMap = {\u001b[39m\n\u001b[31m      proxy_url:           'proxy_url',\u001b[39m\n\u001b[31m      webhook_url:         'webhook_url',\u001b[39m\n\u001b[31m      ollama_url:          'ollama_url',\u001b[39m\n\u001b[31m      ocr_mode:            'ocr_mode',\u001b[39m\n\u001b[31m      company:             'company',\u001b[39m\n\u001b[31m      city:                'city',\u001b[39m\n\u001b[31m      auto_log:            'auto_log',\u001b[39m\n\u001b[31m      refresh_on_load:     'refresh_on_load',\u001b[39m\n\u001b[31m      admin_password:      'admin_password',\u001b[39m\n\u001b[31m      n8n_dispatch_webhook:'n8n_dispatch_webhook',\u001b[39m\n\u001b[31m    };\u001b[39m\n\u001b[31m    Object.entries(keyMap).forEach(([sheetKey, cfgKey]) => {\u001b[39m\n\u001b[31m      if (cfg[sheetKey] !== undefined && cfg[sheetKey] !== '') {\u001b[39m\n\u001b[31m        window.appConfig[cfgKey] = cfg[sheetKey];\u001b[39m\n\u001b[31m      }\u001b[39m\n\u001b[31m    });·\u001b[39m\n\u001b[31m    // Apply courier master if provided\u001b[39m\n\u001b[31m    if (cfg._courier_master && Array.isArray(cfg._courier_master)) {\u001b[39m\n\u001b[31m      cfg._courier_master.forEach(c => {\u001b[39m\n\u001b[31m        if (c.name && c.trackUrl) {\u001b[39m\n\u001b[31m          COURIER_MASTER[c.name] = {\u001b[39m\n\u001b[31m            trackUrl: c.trackUrl,\u001b[39m\n\u001b[31m            trackBulkUrl: c.bulkUrl || '',\u001b[39m\n\u001b[31m            maxBulk: parseInt(c.maxBulk) || 1\u001b[39m\n\u001b[31m          };\u001b[39m\n\u001b[31m        }\u001b[39m\n\u001b[31m      });\u001b[39m\n\u001b[31m    }·\u001b[39m\n\u001b[31m    renderCourierMasterList();\u001b[39m\n\u001b[31m    renderSheetConfigDisplay(cfg);\u001b[39m\n\u001b[31m    setSyncStatus('ok', 'Synced from sheet · ' + new Date().toLocaleTimeString());\u001b[39m\n\u001b[31m    if (!silent) showToast('Config synced from sheet ✓', 'ok');·\u001b[39m\n\u001b[31m    // If refresh_on_load is true, trigger dashboard refresh\u001b[39m\n\u001b[31m    if (window.appConfig.refresh_on_load === 'true') {\u001b[39m\n\u001b[31m      setTimeout(refreshAllTracking, 2000);\u001b[39m\n\u001b[31m    }·\u001b[39m\n\u001b[31m  } catch(e) {\u001b[39m\n\u001b[31m    setSyncStatus('err', 'Sync failed: ' + e.message);\u001b[39m\n\u001b[31m    if (!silent) showToast('Sync failed — check URL', 'err');\u001b[39m\n\u001b[31m    console.error('Sheets sync error:', e);\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction setSyncStatus(state, msg) {\u001b[39m\n\u001b[31m  const el = document.getElementById('syncStatus');\u001b[39m\n\u001b[31m  const icon = document.getElementById('syncIcon');\u001b[39m\n\u001b[31m  const msgEl = document.getElementById('syncMsg');\u001b[39m\n\u001b[31m  el.style.display = 'flex';\u001b[39m\n\u001b[31m  icon.textContent = state === 'ok' ? '✅' : state === 'err' ? '❌' : '⏳';\u001b[39m\n\u001b[31m  msgEl.textContent = msg;\u001b[39m\n\u001b[31m  el.style.borderColor = state === 'ok' ? 'rgba(34,197,94,.3)' : state === 'err' ? 'rgba(239,68,68,.3)' : 'rgba(234,179,8,.3)';\u001b[39m\n\u001b[31m  el.style.background = state === 'ok' ? 'rgba(34,197,94,.05)' : state === 'err' ? 'rgba(239,68,68,.05)' : 'rgba(234,179,8,.05)';\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction renderSheetConfigDisplay(cfg) {\u001b[39m\n\u001b[31m  const display = document.getElementById('sheetConfigDisplay');\u001b[39m\n\u001b[31m  display.style.display = 'block';\u001b[39m\n\u001b[31m  document.getElementById('sheetSyncTime').textContent = 'Last sync: ' + new Date().toLocaleTimeString();\u001b[39m\n\u001b[31m  const showKeys = ['proxy_url','webhook_url','ollama_url','ocr_mode','company','city','auto_log'];\u001b[39m\n\u001b[31m  document.getElementById('sheetConfigRows').innerHTML = showKeys.map(k => {\u001b[39m\n\u001b[31m    const val = cfg[k] || window.appConfig[k] || '—';\u001b[39m\n\u001b[31m    const isEmpty = !cfg[k];\u001b[39m\n\u001b[31m    return `<div class=\\\"courier-master-row\\\">\u001b[39m\n\u001b[31m      <div class=\\\"cml\\\">\u001b[39m\n\u001b[31m        <div class=\\\"cml-name\\\" style=\\\"font-family:var(--fnt-mono);font-size:.75rem;\\\">${k}</div>\u001b[39m\n\u001b[31m        <div class=\\\"cml-url\\\" style=\\\"${isEmpty ? 'color:var(--red);' : ''}\\\">${isEmpty ? '⚠️ Not set in sheet' : val}</div>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m    </div>`;\u001b[39m\n\u001b[31m  }).join('');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m/**\u001b[39m\n\u001b[31m * Push current config changes back to the sheet.\u001b[39m\n\u001b[31m * Called when user edits config from PWA (rare — usually done in sheet directly).\u001b[39m\n\u001b[31m */\u001b[39m\n\u001b[31masync function pushConfigToSheet(updates) {\u001b[39m\n\u001b[31m  const url = window.appConfig.sheets_url;\u001b[39m\n\u001b[31m  if (!url) return;\u001b[39m\n\u001b[31m  try {\u001b[39m\n\u001b[31m    await fetch(url, {\u001b[39m\n\u001b[31m      method: 'POST', mode: 'no-cors',\u001b[39m\n\u001b[31m      headers: { 'Content-Type': 'application/json' },\u001b[39m\n\u001b[31m      body: JSON.stringify({ type: 'save_config', config: updates })\u001b[39m\n\u001b[31m    });\u001b[39m\n\u001b[31m  } catch(e) {}\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction clearAll() {\u001b[39m\n\u001b[31m  if (!confirm('Delete ALL dispatch data?')) return;\u001b[39m\n\u001b[31m  dispatches = [];\u001b[39m\n\u001b[31m  saveData();\u001b[39m\n\u001b[31m  updateHistCount();\u001b[39m\n\u001b[31m  renderDashboard();\u001b[39m\n\u001b[31m  renderHistory();\u001b[39m\n\u001b[31m  showToast('Cleared', 'ok');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction renderCourierMasterList() {\u001b[39m\n\u001b[31m  const el = document.getElementById('courierMasterList');\u001b[39m\n\u001b[31m  el.innerHTML = Object.entries(COURIER_MASTER).map(([name, info]) => `\u001b[39m\n\u001b[31m    <div class=\\\"courier-master-row\\\">\u001b[39m\n\u001b[31m      <div class=\\\"cml\\\">\u001b[39m\n\u001b[31m        <div class=\\\"cml-name\\\">${name}</div>\u001b[39m\n\u001b[31m        <div class=\\\"cml-url\\\">${info.trackUrl}</div>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      <button class=\\\"btn-sm\\\" onclick=\\\"window.open('${info.trackBulkUrl||info.trackUrl.replace('{docket}','')}','_blank')\\\">Open</button>\u001b[39m\n\u001b[31m    </div>`).join('');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction openCourierMaster() {\u001b[39m\n\u001b[31m  const ed = document.getElementById('courierMasterEditor');\u001b[39m\n\u001b[31m  ed.innerHTML = Object.entries(COURIER_MASTER).map(([name, info]) => `\u001b[39m\n\u001b[31m    <div style=\\\"margin-bottom:14px;\\\">\u001b[39m\n\u001b[31m      <div style=\\\"font-family:var(--fnt-head);font-size:.75rem;font-weight:700;color:var(--t2);text-transform:uppercase;letter-spacing:1.5px;margin-bottom:6px;\\\">${name}</div>\u001b[39m\n\u001b[31m      <div class=\\\"f\\\" style=\\\"margin-bottom:6px;\\\">\u001b[39m\n\u001b[31m        <label>Track URL (use {docket})</label>\u001b[39m\n\u001b[31m        <input type=\\\"text\\\" value=\\\"${info.trackUrl}\\\" id=\\\"cm_${name.replace(/\\\\s/g,'_')}\\\"\u001b[39m\n\u001b[31m          style=\\\"font-family:var(--fnt-mono);font-size:.75rem;\\\">\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m    </div>`).join('') +\u001b[39m\n\u001b[31m    `<button class=\\\"btn btn-orange\\\" onclick=\\\"saveCourierMaster()\\\">Save Courier Master</button>`;\u001b[39m\n\u001b[31m  openOverlay('courierMasterOverlay');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction saveCourierMaster() {\u001b[39m\n\u001b[31m  Object.keys(COURIER_MASTER).forEach(name => {\u001b[39m\n\u001b[31m    const el = document.getElementById('cm_' + name.replace(/\\\\s/g,'_'));\u001b[39m\n\u001b[31m    if (el) COURIER_MASTER[name].trackUrl = el.value;\u001b[39m\n\u001b[31m  });\u001b[39m\n\u001b[31m  closeOverlay('courierMasterOverlay');\u001b[39m\n\u001b[31m  renderCourierMasterList();\u001b[39m\n\u001b[31m  showToast('Courier master saved ✓', 'ok');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31m// UI UTILS\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31m// ── ADMIN AUTH ───────────────────────────────────────────────────────────\u001b[39m\n\u001b[31mlet _adminAuthed = false;\u001b[39m\n\u001b[31mlet _lastAdminPane = null;·\u001b[39m\n\u001b[31mfunction showPane(name) {\u001b[39m\n\u001b[31m  // Config tab requires admin auth — re-ask if navigated away\u001b[39m\n\u001b[31m  if (name === 'config') {\u001b[39m\n\u001b[31m    if (!_adminAuthed) {\u001b[39m\n\u001b[31m      _lastAdminPane = 'config';\u001b[39m\n\u001b[31m      showAdminLogin();\u001b[39m\n\u001b[31m      return;\u001b[39m\n\u001b[31m    }\u001b[39m\n\u001b[31m  } else {\u001b[39m\n\u001b[31m    // User navigated away from config — revoke auth\u001b[39m\n\u001b[31m    if (_lastAdminPane === 'config') _adminAuthed = false;\u001b[39m\n\u001b[31m    _lastAdminPane = name;\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m  document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));\u001b[39m\n\u001b[31m  document.querySelectorAll('.pane').forEach(p => p.classList.remove('active'));\u001b[39m\n\u001b[31m  document.getElementById('tab-'+name).classList.add('active');\u001b[39m\n\u001b[31m  document.getElementById('pane-'+name).classList.add('active');\u001b[39m\n\u001b[31m  if (name === 'dashboard') renderDashboard();\u001b[39m\n\u001b[31m  if (name === 'history') renderHistory();\u001b[39m\n\u001b[31m  if (name === 'track') buildTrackDocketChips();\u001b[39m\n\u001b[31m  if (name === 'config') { document.getElementById('cfg_sheets_url').value = window.appConfig.sheets_url || ''; }\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction showAdminLogin() {\u001b[39m\n\u001b[31m  // Password replaced by hub identity — only harish/pramod can access config\u001b[39m\n\u001b[31m  if (['harish', 'pramod'].includes(window._hubUser?.id)) {\u001b[39m\n\u001b[31m    _adminAuthed = true;\u001b[39m\n\u001b[31m    showPane('config');\u001b[39m\n\u001b[31m  } else {\u001b[39m\n\u001b[31m    showToast('Config access restricted to admin users', 'err');\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31masync function submitAdminLogin() {\u001b[39m\n\u001b[31m  showAdminLogin(); // no-op shim — kept for any lingering HTML onclick refs\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31mfunction openOverlay(id) { document.getElementById(id).classList.add('open'); }\u001b[39m\n\u001b[31mfunction closeOverlay(id) { document.getElementById(id).classList.remove('open'); }·\u001b[39m\n\u001b[31mlet toastTimer;\u001b[39m\n\u001b[31mfunction showToast(msg, type='') {\u001b[39m\n\u001b[31m  const t = document.getElementById('toast');\u001b[39m\n\u001b[31m  t.textContent = msg;\u001b[39m\n\u001b[31m  t.className = 'toast show' + (type ? ' '+type : '');\u001b[39m\n\u001b[31m  clearTimeout(toastTimer);\u001b[39m\n\u001b[31m  toastTimer = setTimeout(() => t.classList.remove('show'), 2800);\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction showOCRWorking(show, msg='') {\u001b[39m\n\u001b[31m  const el = document.getElementById('ocrWorking');\u001b[39m\n\u001b[31m  el.classList.toggle('visible', show);\u001b[39m\n\u001b[31m  if (msg) document.getElementById('ocrWorkingText').textContent = msg;\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction showAIProgress(label, pct) {\u001b[39m\n\u001b[31m  const el = document.getElementById('aiProgress');\u001b[39m\n\u001b[31m  el.classList.add('visible');\u001b[39m\n\u001b[31m  document.getElementById('aiProgLabel').textContent = label;\u001b[39m\n\u001b[31m  document.getElementById('aiProgFill').style.width = pct + '%';\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31mfunction hideAIProgress() {\u001b[39m\n\u001b[31m  document.getElementById('aiProgress').classList.remove('visible');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction updateNetDot() {\u001b[39m\n\u001b[31m  document.getElementById('netDot').style.background = navigator.onLine ? 'var(--grn)' : 'var(--red)';\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction setToday() {\u001b[39m\n\u001b[31m  document.getElementById('n_date').value = new Date().toISOString().split('T')[0];\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m// ── EXPORT CLEAN SHEET ───────────────────────────────────────────────────\u001b[39m\n\u001b[31mfunction exportCleanSheet() {\u001b[39m\n\u001b[31m  const pane = document.getElementById('verifyPane');\u001b[39m\n\u001b[31m  const d = pane._pendingDispatch;\u001b[39m\n\u001b[31m  if (!d || !d.consignees?.length) { showToast('No data to export', 'err'); return; }·\u001b[39m\n\u001b[31m  // Build CSV\u001b[39m\n\u001b[31m  const headers = ['S.No.','Store Name','City','Pincode','Phone','Address','Docket No.','Material','Courier','Status'];\u001b[39m\n\u001b[31m  const rows = d.consignees.map((c, i) => [\u001b[39m\n\u001b[31m    i + 1,\u001b[39m\n\u001b[31m    `\\\"${(c.name||'').replace(/\\\"/g,'\\\"\\\"')}\\\"`,\u001b[39m\n\u001b[31m    `\\\"${(c.city||'').replace(/\\\"/g,'\\\"\\\"')}\\\"`,\u001b[39m\n\u001b[31m    c.pincode || '',\u001b[39m\n\u001b[31m    c.phone || '',\u001b[39m\n\u001b[31m    `\\\"${(c.address||'').replace(/\\\"/g,'\\\"\\\"')}\\\"`,\u001b[39m\n\u001b[31m    c.docket || '',\u001b[39m\n\u001b[31m    `\\\"${(c.material||'').replace(/\\\"/g,'\\\"\\\"')}\\\"`,\u001b[39m\n\u001b[31m    d.courier || '',\u001b[39m\n\u001b[31m    c.status || 'pending'\u001b[39m\n\u001b[31m  ]);·\u001b[39m\n\u001b[31m  const csv = [headers.join(','), ...rows.map(r => r.join(','))].join('\\\\n');\u001b[39m\n\u001b[31m  const blob = new Blob([csv], { type: 'text/csv' });\u001b[39m\n\u001b[31m  const url = URL.createObjectURL(blob);\u001b[39m\n\u001b[31m  const a = document.createElement('a');\u001b[39m\n\u001b[31m  a.href = url;\u001b[39m\n\u001b[31m  a.download = `360LM_Dispatch_${d.client||'export'}_${d.dispatchDate||new Date().toISOString().slice(0,10)}_clean.csv`;\u001b[39m\n\u001b[31m  a.click();\u001b[39m\n\u001b[31m  URL.revokeObjectURL(url);\u001b[39m\n\u001b[31m  showToast('Clean sheet exported ✓', 'ok');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// ── DOCKET SLIP UPLOAD — SKIP OCR IF DOCKET ALREADY MAPPED ───────────────\u001b[39m\n\u001b[31mfunction shouldSkipOCR() {\u001b[39m\n\u001b[31m  // If any consignee already has a docket (from store list mapping), skip OCR extraction\u001b[39m\n\u001b[31m  const consList = document.getElementById('consList');\u001b[39m\n\u001b[31m  if (!consList) return false;\u001b[39m\n\u001b[31m  const cards = consList.querySelectorAll('.cons-card');\u001b[39m\n\u001b[31m  let hasAnyDocket = false;\u001b[39m\n\u001b[31m  cards.forEach(card => {\u001b[39m\n\u001b[31m    const d = card.querySelector('.cons-docket-input')?.value?.trim();\u001b[39m\n\u001b[31m    if (d) hasAnyDocket = true;\u001b[39m\n\u001b[31m  });\u001b[39m\n\u001b[31m  return hasAnyDocket;\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// ─── TRACKING UPDATE TO SHEETS ──────────────────────────────────────────\u001b[39m\n\u001b[31masync function logTrackingUpdate(docket, status, lastEvent, location) {\u001b[39m\n\u001b[31m  const url = window.appConfig.webhook_url;\u001b[39m\n\u001b[31m  if (!url) return;\u001b[39m\n\u001b[31m  try {\u001b[39m\n\u001b[31m    await fetch(url, {\u001b[39m\n\u001b[31m      method: 'POST', mode: 'no-cors',\u001b[39m\n\u001b[31m      headers: { 'Content-Type': 'application/json' },\u001b[39m\n\u001b[31m      body: JSON.stringify({\u001b[39m\n\u001b[31m        type: 'tracking_update',\u001b[39m\n\u001b[31m        docket, status, lastEvent, location,\u001b[39m\n\u001b[31m        timestamp: new Date().toISOString()\u001b[39m\n\u001b[31m      })\u001b[39m\n\u001b[31m    });\u001b[39m\n\u001b[31m  } catch(e) {}\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// ─── POD / RECIPIENT PHOTO UPLOAD ───────────────────────────────────────\u001b[39m\n\u001b[31m// Stores base64 locally, pushes to Sheets webhook (Apps Script saves to Drive)\u001b[39m\n\u001b[31mfunction showPODUpload(docket, dispatchId) {\u001b[39m\n\u001b[31m  const overlay = document.getElementById('podOverlay');\u001b[39m\n\u001b[31m  overlay.dataset.docket = docket;\u001b[39m\n\u001b[31m  overlay.dataset.dispatchId = dispatchId;\u001b[39m\n\u001b[31m  document.getElementById('podPreview').innerHTML = '';\u001b[39m\n\u001b[31m  document.getElementById('podFileInput').value = '';\u001b[39m\n\u001b[31m  document.getElementById('recipientFileInput').value = '';\u001b[39m\n\u001b[31m  openOverlay('podOverlay');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31masync function uploadPODFiles() {\u001b[39m\n\u001b[31m  const overlay = document.getElementById('podOverlay');\u001b[39m\n\u001b[31m  const docket = overlay.dataset.docket;\u001b[39m\n\u001b[31m  const dispatchId = overlay.dataset.dispatchId;\u001b[39m\n\u001b[31m  const podFile = document.getElementById('podFileInput').files[0];\u001b[39m\n\u001b[31m  const recipFile = document.getElementById('recipientFileInput').files[0];\u001b[39m\n\u001b[31m  if (!podFile && !recipFile) { showToast('Select at least one photo', 'err'); return; }·\u001b[39m\n\u001b[31m  showToast('Uploading...', 'info');\u001b[39m\n\u001b[31m  const toBase64 = f => new Promise((res,rej) => {\u001b[39m\n\u001b[31m    const r = new FileReader(); r.onload = e => res(e.target.result); r.onerror = rej; r.readAsDataURL(f);\u001b[39m\n\u001b[31m  });·\u001b[39m\n\u001b[31m  const payload = {\u001b[39m\n\u001b[31m    type: 'pod_upload',\u001b[39m\n\u001b[31m    docket, dispatchId,\u001b[39m\n\u001b[31m    timestamp: new Date().toISOString()\u001b[39m\n\u001b[31m  };\u001b[39m\n\u001b[31m  if (podFile)   payload.pod_image = await toBase64(podFile);\u001b[39m\n\u001b[31m  if (recipFile) payload.recipient_image = await toBase64(recipFile);·\u001b[39m\n\u001b[31m  // Update local store\u001b[39m\n\u001b[31m  for (const d of dispatches) {\u001b[39m\n\u001b[31m    if (d.id !== dispatchId) continue;\u001b[39m\n\u001b[31m    for (const c of d.consignees) {\u001b[39m\n\u001b[31m      if (c.docket === docket) {\u001b[39m\n\u001b[31m        c.podUploaded = true;\u001b[39m\n\u001b[31m        c.status = 'delivered';\u001b[39m\n\u001b[31m      }\u001b[39m\n\u001b[31m    }\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m  saveData();·\u001b[39m\n\u001b[31m  // Push to Sheets (Apps Script saves images to Drive, stores URL in sheet)\u001b[39m\n\u001b[31m  if (window.appConfig.webhook_url) {\u001b[39m\n\u001b[31m    try {\u001b[39m\n\u001b[31m      await fetch(window.appConfig.webhook_url, {\u001b[39m\n\u001b[31m        method: 'POST', mode: 'no-cors',\u001b[39m\n\u001b[31m        headers: { 'Content-Type': 'application/json' },\u001b[39m\n\u001b[31m        body: JSON.stringify(payload)\u001b[39m\n\u001b[31m      });\u001b[39m\n\u001b[31m    } catch(e) {}\u001b[39m\n\u001b[31m  }·\u001b[39m\n\u001b[31m  closeOverlay('podOverlay');\u001b[39m\n\u001b[31m  renderDashboard();\u001b[39m\n\u001b[31m  showToast('POD uploaded ✓ — marked as delivered', 'ok');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// ─── WHATSAPP DISPATCH TABLE SHARE ──────────────────────────────────────\u001b[39m\n\u001b[31mfunction shareDispatchToWhatsApp(dispatchId) {\u001b[39m\n\u001b[31m  const d = dispatches.find(x => x.id === dispatchId);\u001b[39m\n\u001b[31m  if (!d) return;\u001b[39m\n\u001b[31m  const lines = [\u001b[39m\n\u001b[31m    `📦 *Dispatch Update — ${d.client} ${d.jobref ? '› '+d.jobref : ''}*`,\u001b[39m\n\u001b[31m    `🚚 Courier: ${d.courier} | Date: ${d.dispatchDate || d.createdAt?.slice(0,10)}`,\u001b[39m\n\u001b[31m    ``,\u001b[39m\n\u001b[31m    ...d.consignees.filter(c => c.docket).map((c,i) =>\u001b[39m\n\u001b[31m      `${i+1}. *${c.name}* — ${c.city}\\\\n   Docket: ${c.docket} | Status: ${c.status||'booked'}`\u001b[39m\n\u001b[31m    ),\u001b[39m\n\u001b[31m    ``,\u001b[39m\n\u001b[31m    `_360 Degree Logical Marketing_`\u001b[39m\n\u001b[31m  ];\u001b[39m\n\u001b[31m  const msg = encodeURIComponent(lines.join('\\\\n'));\u001b[39m\n\u001b[31m  window.open('https://wa.me/?text=' + msg, '_blank');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// Trigger n8n webhook for automated WhatsApp/email share\u001b[39m\n\u001b[31masync function triggerN8nShare(dispatchId, channel) {\u001b[39m\n\u001b[31m  const d = dispatches.find(x => x.id === dispatchId);\u001b[39m\n\u001b[31m  if (!d) return;\u001b[39m\n\u001b[31m  const n8nUrl = window.appConfig.n8n_dispatch_webhook;\u001b[39m\n\u001b[31m  if (!n8nUrl) { showToast('n8n webhook not configured in Config sheet', 'err'); return; }\u001b[39m\n\u001b[31m  try {\u001b[39m\n\u001b[31m    await fetch(n8nUrl, {\u001b[39m\n\u001b[31m      method: 'POST',\u001b[39m\n\u001b[31m      headers: { 'Content-Type': 'application/json' },\u001b[39m\n\u001b[31m      body: JSON.stringify({\u001b[39m\n\u001b[31m        channel, // 'whatsapp' or 'email'\u001b[39m\n\u001b[31m        dispatch: d,\u001b[39m\n\u001b[31m        timestamp: new Date().toISOString()\u001b[39m\n\u001b[31m      })\u001b[39m\n\u001b[31m    });\u001b[39m\n\u001b[31m    showToast(`Sent to n8n for ${channel} delivery ✓`, 'ok');\u001b[39m\n\u001b[31m  } catch(e) {\u001b[39m\n\u001b[31m    showToast('n8n webhook failed', 'err');\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction previewPOD(input, type) {\u001b[39m\n\u001b[31m  const f = input.files[0];\u001b[39m\n\u001b[31m  if (!f) return;\u001b[39m\n\u001b[31m  const r = new FileReader();\u001b[39m\n\u001b[31m  r.onload = e => {\u001b[39m\n\u001b[31m    const prev = document.getElementById('podPreview');\u001b[39m\n\u001b[31m    const existing = document.getElementById('pod_prev_' + type);\u001b[39m\n\u001b[31m    if (existing) existing.remove();\u001b[39m\n\u001b[31m    const img = document.createElement('img');\u001b[39m\n\u001b[31m    img.id = 'pod_prev_' + type;\u001b[39m\n\u001b[31m    img.src = e.target.result;\u001b[39m\n\u001b[31m    img.style.cssText = 'width:80px;height:80px;object-fit:cover;border-radius:6px;border:1px solid var(--bdr);';\u001b[39m\n\u001b[31m    prev.appendChild(img);\u001b[39m\n\u001b[31m  };\u001b[39m\n\u001b[31m  r.readAsDataURL(f);\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// ─── MANUAL TRACKING ─────────────────────────────────────────────────────\u001b[39m\n\u001b[31mfunction showManualTracking(docket, dispatchId, storeName, phone, city) {\u001b[39m\n\u001b[31m  const ov = document.getElementById('manualTrackOverlay');\u001b[39m\n\u001b[31m  ov.dataset.docket = docket;\u001b[39m\n\u001b[31m  ov.dataset.dispatchId = dispatchId;\u001b[39m\n\u001b[31m  ov.dataset.storeName = storeName;\u001b[39m\n\u001b[31m  ov.dataset.city = city;·\u001b[39m\n\u001b[31m  document.getElementById('mtDocketDisplay').textContent =\u001b[39m\n\u001b[31m    docket + '  ·  ' + storeName + '  ·  ' + city;\u001b[39m\n\u001b[31m  document.getElementById('mtContactUsed').value = phone || '';\u001b[39m\n\u001b[31m  document.getElementById('mtContactPerson').value = '';\u001b[39m\n\u001b[31m  document.getElementById('mtRemarks').value = '';\u001b[39m\n\u001b[31m  document.getElementById('mtStatus').value = '';\u001b[39m\n\u001b[31m  document.getElementById('mtCalledBy').value = '';·\u001b[39m\n\u001b[31m  // Set current datetime\u001b[39m\n\u001b[31m  const now = new Date();\u001b[39m\n\u001b[31m  const pad = n => String(n).padStart(2,'0');\u001b[39m\n\u001b[31m  document.getElementById('mtCallTime').value =\u001b[39m\n\u001b[31m    `${now.getFullYear()}-${pad(now.getMonth()+1)}-${pad(now.getDate())}T${pad(now.getHours())}:${pad(now.getMinutes())}`;·\u001b[39m\n\u001b[31m  openOverlay('manualTrackOverlay');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31masync function submitManualTracking() {\u001b[39m\n\u001b[31m  const ov = document.getElementById('manualTrackOverlay');\u001b[39m\n\u001b[31m  const docket     = ov.dataset.docket;\u001b[39m\n\u001b[31m  const dispatchId = ov.dataset.dispatchId;\u001b[39m\n\u001b[31m  const status     = document.getElementById('mtStatus').value;\u001b[39m\n\u001b[31m  const calledBy   = document.getElementById('mtCalledBy').value.trim();·\u001b[39m\n\u001b[31m  if (!status)    { showToast('Select a status', 'err'); return; }\u001b[39m\n\u001b[31m  if (!calledBy)  { showToast('Enter your name', 'err'); return; }·\u001b[39m\n\u001b[31m  const payload = {\u001b[39m\n\u001b[31m    type:          'manual_tracking',\u001b[39m\n\u001b[31m    docket,        dispatchId,\u001b[39m\n\u001b[31m    storeName:     ov.dataset.storeName,\u001b[39m\n\u001b[31m    city:          ov.dataset.city,\u001b[39m\n\u001b[31m    manualStatus:  status,\u001b[39m\n\u001b[31m    calledBy,\u001b[39m\n\u001b[31m    contactUsed:   document.getElementById('mtContactUsed').value.trim(),\u001b[39m\n\u001b[31m    contactPerson: document.getElementById('mtContactPerson').value.trim(),\u001b[39m\n\u001b[31m    remarks:       document.getElementById('mtRemarks').value.trim(),\u001b[39m\n\u001b[31m    callTime:      document.getElementById('mtCallTime').value || new Date().toISOString(),\u001b[39m\n\u001b[31m    timestamp:     new Date().toISOString()\u001b[39m\n\u001b[31m  };·\u001b[39m\n\u001b[31m  // Update local dispatch record\u001b[39m\n\u001b[31m  for (const d of dispatches) {\u001b[39m\n\u001b[31m    if (d.id !== dispatchId) continue;\u001b[39m\n\u001b[31m    for (const c of d.consignees) {\u001b[39m\n\u001b[31m      if (c.docket === docket) {\u001b[39m\n\u001b[31m        c.manualStatus    = status;\u001b[39m\n\u001b[31m        c.manualCalledBy  = calledBy;\u001b[39m\n\u001b[31m        c.manualTime      = payload.callTime;\u001b[39m\n\u001b[31m        c.manualRemarks   = payload.remarks;\u001b[39m\n\u001b[31m        // If confirmed delivered — flag so dashboard shows manual badge\u001b[39m\n\u001b[31m        // (not permanently locked — use resumeAutoTracking() to re-enable)\u001b[39m\n\u001b[31m        if (status === 'delivered') {\u001b[39m\n\u001b[31m          c.status = 'delivered';\u001b[39m\n\u001b[31m          c.manualDelivered = true;\u001b[39m\n\u001b[31m        } else {\u001b[39m\n\u001b[31m          // Non-delivered manual update: clear any previous lock\u001b[39m\n\u001b[31m          c.manualDelivered = false;\u001b[39m\n\u001b[31m        }\u001b[39m\n\u001b[31m      }\u001b[39m\n\u001b[31m    }\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m  saveData();\u001b[39m\n\u001b[31m  renderDashboard();\u001b[39m\n\u001b[31m  // Sync updated consignee to Postgres\u001b[39m\n\u001b[31m  const _d = dispatches.find(x => x.id === dispatchId);\u001b[39m\n\u001b[31m  if (_d) {\u001b[39m\n\u001b[31m    const _idx = _d.consignees.findIndex(c => c.docket === docket);\u001b[39m\n\u001b[31m    if (_idx >= 0) syncConsigneeToDB(_d.consignees[_idx], dispatchId, _idx);\u001b[39m\n\u001b[31m  }·\u001b[39m\n\u001b[31m  // Log to Sheets\u001b[39m\n\u001b[31m  if (window.appConfig.webhook_url) {\u001b[39m\n\u001b[31m    try {\u001b[39m\n\u001b[31m      await fetch(window.appConfig.webhook_url, {\u001b[39m\n\u001b[31m        method: 'POST', mode: 'no-cors',\u001b[39m\n\u001b[31m        headers: { 'Content-Type': 'application/json' },\u001b[39m\n\u001b[31m        body: JSON.stringify(payload)\u001b[39m\n\u001b[31m      });\u001b[39m\n\u001b[31m    } catch(e) {}\u001b[39m\n\u001b[31m  }·\u001b[39m\n\u001b[31m  closeOverlay('manualTrackOverlay');\u001b[39m\n\u001b[31m  showToast('Manual tracking logged ✓', 'ok');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// ─── TILE SELECTION + PARTIAL REPORT SHARE ───────────────────────────────\u001b[39m\n\u001b[31mlet _selectedDockets = new Set();·\u001b[39m\n\u001b[31mfunction toggleTileSelect(docket) {\u001b[39m\n\u001b[31m  if (_selectedDockets.has(docket)) _selectedDockets.delete(docket);\u001b[39m\n\u001b[31m  else _selectedDockets.add(docket);\u001b[39m\n\u001b[31m  // Update checkbox visual\u001b[39m\n\u001b[31m  const chk = document.getElementById('sel_' + docket.replace(/[^a-z0-9]/gi,'_'));\u001b[39m\n\u001b[31m  if (chk) chk.checked = _selectedDockets.has(docket);\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction shareSelectedReport() {\u001b[39m\n\u001b[31m  if (_selectedDockets.size === 0) {\u001b[39m\n\u001b[31m    showToast('Select at least one consignee tile first', 'err');\u001b[39m\n\u001b[31m    return;\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m  const activeDis = dispatches.filter(d => d.status !== 'draft');\u001b[39m\n\u001b[31m  const allItems  = activeDis.flatMap(d =>\u001b[39m\n\u001b[31m    (d.consignees||[]).map(c => Object.assign({}, c, {courier:d.courier, client:d.client, jobref:d.jobref, dispatchId:d.id}))\u001b[39m\n\u001b[31m  );\u001b[39m\n\u001b[31m  const selected = allItems.filter(c => _selectedDockets.has(c.docket));\u001b[39m\n\u001b[31m  if (!selected.length) { showToast('Selected dockets not found', 'err'); return; }·\u001b[39m\n\u001b[31m  const NL = '\\\\n';\u001b[39m\n\u001b[31m  const statusEmoji = {\u001b[39m\n\u001b[31m    delivered:'\\\\u2705', transit:'\\\\uD83D\\\\uDE9A', 'in-transit':'\\\\uD83D\\\\uDE9A',\u001b[39m\n\u001b[31m    booked:'\\\\uD83D\\\\uDCEC', pending:'\\\\u23F3', 'not received':'\\\\u274C', forwarded:'\\\\u27A1'\u001b[39m\n\u001b[31m  };·\u001b[39m\n\u001b[31m  const header = '\\\\uD83D\\\\uDCE6 *Dispatch Status Report*' + NL +\u001b[39m\n\u001b[31m    (selected[0].client || '') + ' ' + (selected[0].jobref ? '> ' + selected[0].jobref : '') + NL +\u001b[39m\n\u001b[31m    new Date().toLocaleDateString('en-IN', {day:'2-digit',month:'short',year:'numeric'}) + NL + NL;·\u001b[39m\n\u001b[31m  const rows = selected.map(function(c, i) {\u001b[39m\n\u001b[31m    const finalStatus = c.manualStatus === 'delivered' ? 'delivered' : (c.status || 'booked');\u001b[39m\n\u001b[31m    const emoji  = statusEmoji[finalStatus] || '\\\\uD83D\\\\uDCE6';\u001b[39m\n\u001b[31m    const source = c.manualStatus ? '(manual)' : '(auto)';\u001b[39m\n\u001b[31m    const remark = c.manualRemarks ? (NL + '   Note: ' + c.manualRemarks) : '';\u001b[39m\n\u001b[31m    return (i+1) + '. *' + c.name + '* - ' + c.city + NL +\u001b[39m\n\u001b[31m      '   ' + emoji + ' ' + finalStatus + ' ' + source + NL +\u001b[39m\n\u001b[31m      '   Docket: ' + c.docket + remark;\u001b[39m\n\u001b[31m  }).join(NL + NL);·\u001b[39m\n\u001b[31m  const footer = NL + NL + '_360 Degree Logical Marketing_';\u001b[39m\n\u001b[31m  window.open('https://wa.me/?text=' + encodeURIComponent(header + rows + footer), '_blank');\u001b[39m\n\u001b[31m  showToast('Report for ' + selected.length + ' consignees ready', 'ok');\u001b[39m\n\u001b[31m}··\u001b[39m\n\u001b[31m// ── resumeAutoTracking: undo a manual-delivered lock ──────────────────────\u001b[39m\n\u001b[31mfunction resumeAutoTracking(docket, dispatchId) {\u001b[39m\n\u001b[31m  for (const d of dispatches) {\u001b[39m\n\u001b[31m    if (d.id !== dispatchId) continue;\u001b[39m\n\u001b[31m    for (const c of d.consignees) {\u001b[39m\n\u001b[31m      if (c.docket === docket) {\u001b[39m\n\u001b[31m        c.manualDelivered = false;\u001b[39m\n\u001b[31m        c.status          = 'transit';\u001b[39m\n\u001b[31m        c.manualStatus    = '';\u001b[39m\n\u001b[31m      }\u001b[39m\n\u001b[31m    }\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m  saveData();\u001b[39m\n\u001b[31m  renderDashboard();\u001b[39m\n\u001b[31m  showToast(`Auto-tracking resumed for ${docket}`, 'ok');\u001b[39m\n\u001b[31m  const _rd = dispatches.find(x => x.id === dispatchId);\u001b[39m\n\u001b[31m  if (_rd) {\u001b[39m\n\u001b[31m    const _ri = _rd.consignees.findIndex(c => c.docket === docket);\u001b[39m\n\u001b[31m    if (_ri >= 0) syncConsigneeToDB(_rd.consignees[_ri], dispatchId, _ri);\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// ── refreshAllTracking (consolidated, try/finally for safety) ─────────────\u001b[39m\n\u001b[31masync function refreshAllTracking() {\u001b[39m\n\u001b[31m  const activeDis  = dispatches.filter(d => d.status !== 'draft');\u001b[39m\n\u001b[31m  const allCons    = activeDis.flatMap(d =>\u001b[39m\n\u001b[31m    (d.consignees || []).map(c => ({ ...c, dispatchId: d.id, courier: d.courier }))\u001b[39m\n\u001b[31m  );\u001b[39m\n\u001b[31m  const withDockets = allCons.filter(c =>\u001b[39m\n\u001b[31m    c.docket && c.status !== 'delivered' && !c.manualDelivered\u001b[39m\n\u001b[31m  );\u001b[39m\n\u001b[31m  if (!withDockets.length) { showToast('No active dockets to refresh', 'info'); return; }·\u001b[39m\n\u001b[31m  showToast(`⏳ Tracking ${withDockets.length} dockets...`, 'info');\u001b[39m\n\u001b[31m  withDockets.forEach(c => {\u001b[39m\n\u001b[31m    const id    = c.docket.replace(/[^a-z0-9]/gi, '_');\u001b[39m\n\u001b[31m    const evtEl = document.getElementById('evt_'  + id);\u001b[39m\n\u001b[31m    const spnEl = document.getElementById('spin_' + id);\u001b[39m\n\u001b[31m    if (evtEl) evtEl.style.display = 'none';\u001b[39m\n\u001b[31m    if (spnEl) spnEl.style.display = 'inline';\u001b[39m\n\u001b[31m  });·\u001b[39m\n\u001b[31m  let done = 0;\u001b[39m\n\u001b[31m  try {\u001b[39m\n\u001b[31m    for (const c of withDockets) {\u001b[39m\n\u001b[31m      const result = await fetchTracking(c.docket, c.courier || '');\u001b[39m\n\u001b[31m      const id    = c.docket.replace(/[^a-z0-9]/gi, '_');·\u001b[39m\n\u001b[31m      // Batch-write result into state (never touch DOM here)\u001b[39m\n\u001b[31m      if (result.status && result.status !== 'failed') {\u001b[39m\n\u001b[31m        for (const d of dispatches) {\u001b[39m\n\u001b[31m          if (d.id !== c.dispatchId) continue;\u001b[39m\n\u001b[31m          for (const con of d.consignees) {\u001b[39m\n\u001b[31m            if (con.docket === c.docket && !con.manualDelivered) {\u001b[39m\n\u001b[31m              con.status           = result.status;\u001b[39m\n\u001b[31m              con.lastEvent        = result.lastEvent || con.lastEvent;\u001b[39m\n\u001b[31m              con.lastScanLocation = result.lastScanLocation || '';\u001b[39m\n\u001b[31m              con.lastTrackedAt    = new Date().toISOString();\u001b[39m\n\u001b[31m            }\u001b[39m\n\u001b[31m          }\u001b[39m\n\u001b[31m        }\u001b[39m\n\u001b[31m      }·\u001b[39m\n\u001b[31m      // Update tile spinner/event text in DOM (cosmetic only)\u001b[39m\n\u001b[31m      const spnEl = document.getElementById('spin_' + id);\u001b[39m\n\u001b[31m      const evtEl = document.getElementById('evt_'  + id);\u001b[39m\n\u001b[31m      if (spnEl) spnEl.style.display = 'none';\u001b[39m\n\u001b[31m      if (evtEl) { evtEl.style.display = 'inline'; evtEl.textContent = result.lastEvent || 'Checked'; }·\u001b[39m\n\u001b[31m      if (result.status && result.status !== 'failed' && window.appConfig.webhook_url) {\u001b[39m\n\u001b[31m        logTrackingUpdate(c.docket, result.status, result.lastEvent || '', result.lastScanLocation || '');\u001b[39m\n\u001b[31m      }\u001b[39m\n\u001b[31m      done++;\u001b[39m\n\u001b[31m    }\u001b[39m\n\u001b[31m    showToast(`✓ ${done} dockets refreshed`, 'ok');\u001b[39m\n\u001b[31m  } finally {\u001b[39m\n\u001b[31m    // Always persist and re-render — even if loop throws mid-way\u001b[39m\n\u001b[31m    saveData();\u001b[39m\n\u001b[31m    renderDashboard();\u001b[39m\n\u001b[31m    // Push updated consignee states to Postgres (fire-and-forget)\u001b[39m\n\u001b[31m    dispatches.filter(d => d.status !== 'draft').forEach(d => syncDispatchToDB(d));\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m</script>\u001b[39m\n\u001b[31m</body>\u001b[39m\n\u001b[31m</html>\"\u001b[39m\n    at /var/www/360lm/tests/dispatch.spec.js:53:18",
                        "location": {
                          "file": "/var/www/360lm/tests/dispatch.spec.js",
                          "column": 18,
                          "line": 53
                        },
                        "snippet": "  51 |\n  52 |   test('hub auth gate present', () => {\n> 53 |     expect(html).toContain(\"window.location.replace('/hub/')\");\n     |                  ^\n  54 |     expect(html).toContain('_hubUser');\n  55 |   });\n  56 | });"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/dispatch.spec.js",
                            "column": 18,
                            "line": 53
                          },
                          "message": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoContain\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m) // indexOf\u001b[22m\n\nExpected substring: \u001b[32m\"window.location.replace('/hub/')\"\u001b[39m\nReceived string:    \u001b[31m\"<!DOCTYPE html>\u001b[39m\n\u001b[31m<html lang=\\\"en\\\">\u001b[39m\n\u001b[31m<head>\u001b[39m\n\u001b[31m<meta charset=\\\"UTF-8\\\">\u001b[39m\n\u001b[31m<meta name=\\\"viewport\\\" content=\\\"width=device-width,initial-scale=1,maximum-scale=1,user-scalable=no\\\">\u001b[39m\n\u001b[31m<meta name=\\\"theme-color\\\" content=\\\"#080c12\\\">\u001b[39m\n\u001b[31m<meta name=\\\"apple-mobile-web-app-capable\\\" content=\\\"yes\\\">\u001b[39m\n\u001b[31m<meta name=\\\"mobile-web-app-capable\\\" content=\\\"yes\\\">\u001b[39m\n\u001b[31m<title>360LM Dispatch</title>\u001b[39m\n\u001b[31m<link rel=\\\"manifest\\\" href=\\\"manifest.json\\\">\u001b[39m\n\u001b[31m<link href=\\\"https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@300;400;500;600;700&family=Fira+Code:wght@400;500&family=Barlow+Condensed:wght@400;600;700;800&display=swap\\\" rel=\\\"stylesheet\\\">\u001b[39m\n\u001b[31m<style>\u001b[39m\n\u001b[31m/* ════ RESET & VARS ════ */\u001b[39m\n\u001b[31m*{margin:0;padding:0;box-sizing:border-box;-webkit-tap-highlight-color:transparent;}\u001b[39m\n\u001b[31m:root{\u001b[39m\n\u001b[31m  --bg:#080c12; --bg2:#0d1420; --bg3:#111a28;\u001b[39m\n\u001b[31m  --sur:#141f30; --sur2:#1a2840; --sur3:#1f3050;\u001b[39m\n\u001b[31m  --bdr:#1e2e45; --bdr2:#253650;\u001b[39m\n\u001b[31m  --t1:#f0f4f8; --t2:#8ba4bc; --t3:#3d5470; --t4:#243348;\u001b[39m\n\u001b[31m  --acc:#f97316; --acc2:#fb923c; --acc-glow:rgba(249,115,22,0.25);\u001b[39m\n\u001b[31m  --grn:#22c55e; --grn-dim:rgba(34,197,94,0.15);\u001b[39m\n\u001b[31m  --sky:#38bdf8; --sky-dim:rgba(56,189,248,0.15);\u001b[39m\n\u001b[31m  --red:#ef4444; --red-dim:rgba(239,68,68,0.15);\u001b[39m\n\u001b[31m  --ylw:#eab308; --ylw-dim:rgba(234,179,8,0.15);\u001b[39m\n\u001b[31m  --fnt-head:'Barlow Condensed',sans-serif;\u001b[39m\n\u001b[31m  --fnt-body:'Space Grotesk',sans-serif;\u001b[39m\n\u001b[31m  --fnt-mono:'Fira Code',monospace;\u001b[39m\n\u001b[31m  --r:10px; --r-sm:6px; --r-lg:16px;\u001b[39m\n\u001b[31m  --sh:0 4px 24px rgba(0,0,0,0.4);\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31mhtml,body{height:100%;background:var(--bg);color:var(--t1);font-family:var(--fnt-body);overflow:hidden;}·\u001b[39m\n\u001b[31m/* ════ SPLASH ════ */\u001b[39m\n\u001b[31m#splash{\u001b[39m\n\u001b[31m  position:fixed;inset:0;background:var(--bg);z-index:999;\u001b[39m\n\u001b[31m  display:flex;flex-direction:column;align-items:center;justify-content:center;\u001b[39m\n\u001b[31m  transition:opacity .4s .1s;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.splash-logo{font-family:var(--fnt-head);font-size:3rem;font-weight:800;letter-spacing:2px;line-height:1;}\u001b[39m\n\u001b[31m.splash-logo em{color:var(--acc);font-style:normal;}\u001b[39m\n\u001b[31m.splash-sub{color:var(--t3);font-family:var(--fnt-mono);font-size:.7rem;letter-spacing:4px;margin-top:8px;}\u001b[39m\n\u001b[31m.splash-track{width:200px;height:2px;background:var(--bdr);border-radius:2px;margin-top:40px;overflow:hidden;}\u001b[39m\n\u001b[31m.splash-track-fill{height:100%;width:0;background:linear-gradient(90deg,var(--acc),var(--acc2));animation:splashLoad 1.3s cubic-bezier(.4,0,.2,1) forwards;}\u001b[39m\n\u001b[31m@keyframes splashLoad{to{width:100%;}}·\u001b[39m\n\u001b[31m/* ════ LAYOUT ════ */\u001b[39m\n\u001b[31m#app{height:100%;display:flex;flex-direction:column;}\u001b[39m\n\u001b[31m.hdr{\u001b[39m\n\u001b[31m  background:var(--bg2);border-bottom:1px solid var(--bdr);\u001b[39m\n\u001b[31m  padding:10px 14px;display:flex;align-items:center;gap:10px;flex-shrink:0;\u001b[39m\n\u001b[31m  position:relative;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.hdr-brand{font-family:var(--fnt-head);font-size:1.3rem;font-weight:800;letter-spacing:1px;flex:1;}\u001b[39m\n\u001b[31m.hdr-brand em{color:var(--acc);font-style:normal;}\u001b[39m\n\u001b[31m.hdr-dot{width:7px;height:7px;border-radius:50%;background:var(--grn);transition:background .3s;animation:blink 2s infinite;}\u001b[39m\n\u001b[31m@keyframes blink{0%,100%{opacity:1;}50%{opacity:.4;}}\u001b[39m\n\u001b[31m.hdr-btn{\u001b[39m\n\u001b[31m  background:none;border:none;color:var(--t2);cursor:pointer;\u001b[39m\n\u001b[31m  padding:6px 8px;border-radius:var(--r-sm);transition:all .2s;\u001b[39m\n\u001b[31m  font-family:var(--fnt-head);font-size:.85rem;font-weight:700;letter-spacing:.5px;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.hdr-btn:hover{background:var(--bdr);color:var(--t1);}\u001b[39m\n\u001b[31m.hdr-btn.primary{background:var(--acc);color:#fff;padding:6px 14px;}\u001b[39m\n\u001b[31m.hdr-btn.primary:hover{background:var(--acc2);}·\u001b[39m\n\u001b[31m/* ════ TAB BAR ════ */\u001b[39m\n\u001b[31m.tabs{\u001b[39m\n\u001b[31m  display:flex;background:var(--bg2);border-bottom:1px solid var(--bdr);\u001b[39m\n\u001b[31m  overflow-x:auto;flex-shrink:0;padding:0 6px;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.tabs::-webkit-scrollbar{display:none;}\u001b[39m\n\u001b[31m.tab{\u001b[39m\n\u001b[31m  background:none;border:none;color:var(--t3);font-family:var(--fnt-head);\u001b[39m\n\u001b[31m  font-size:.85rem;font-weight:700;letter-spacing:1px;padding:10px 14px;\u001b[39m\n\u001b[31m  cursor:pointer;border-bottom:2px solid transparent;white-space:nowrap;\u001b[39m\n\u001b[31m  transition:all .2s;flex-shrink:0;display:flex;align-items:center;gap:6px;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.tab.active{color:var(--acc);border-bottom-color:var(--acc);}\u001b[39m\n\u001b[31m.tab:hover:not(.active){color:var(--t2);}\u001b[39m\n\u001b[31m.tab-badge{\u001b[39m\n\u001b[31m  background:var(--acc);color:#fff;font-size:.6rem;font-weight:700;\u001b[39m\n\u001b[31m  padding:1px 5px;border-radius:10px;font-family:var(--fnt-mono);\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m/* ════ CONTENT AREAS ════ */\u001b[39m\n\u001b[31m.pane{flex:1;overflow-y:auto;padding:14px;display:none;}\u001b[39m\n\u001b[31m.pane.active{display:block;}\u001b[39m\n\u001b[31m.pane::-webkit-scrollbar{width:3px;}\u001b[39m\n\u001b[31m.pane::-webkit-scrollbar-thumb{background:var(--bdr2);}·\u001b[39m\n\u001b[31m/* ════ CARDS ════ */\u001b[39m\n\u001b[31m.card{\u001b[39m\n\u001b[31m  background:var(--sur);border:1px solid var(--bdr);border-radius:var(--r);\u001b[39m\n\u001b[31m  padding:14px;margin-bottom:12px;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.card.accent-left{border-left:3px solid var(--acc);}\u001b[39m\n\u001b[31m.card-title{\u001b[39m\n\u001b[31m  font-family:var(--fnt-head);font-size:.72rem;font-weight:700;\u001b[39m\n\u001b[31m  color:var(--t3);text-transform:uppercase;letter-spacing:2.5px;\u001b[39m\n\u001b[31m  margin-bottom:12px;display:flex;align-items:center;gap:8px;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.card-title .ct-dot{width:5px;height:5px;border-radius:50%;background:var(--acc);flex-shrink:0;}·\u001b[39m\n\u001b[31m/* ════ FORM ════ */\u001b[39m\n\u001b[31m.f{margin-bottom:12px;}\u001b[39m\n\u001b[31m.f label{\u001b[39m\n\u001b[31m  display:block;font-size:.68rem;color:var(--t3);margin-bottom:5px;\u001b[39m\n\u001b[31m  font-weight:600;text-transform:uppercase;letter-spacing:1.5px;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.f label .req{color:var(--acc);margin-left:2px;}\u001b[39m\n\u001b[31m.f input,.f select,.f textarea{\u001b[39m\n\u001b[31m  width:100%;background:var(--sur2);border:1px solid var(--bdr);\u001b[39m\n\u001b[31m  border-radius:var(--r-sm);color:var(--t1);font-family:var(--fnt-body);\u001b[39m\n\u001b[31m  font-size:.88rem;padding:9px 11px;outline:none;transition:border-color .2s;\u001b[39m\n\u001b[31m  -webkit-appearance:none;appearance:none;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.f input:focus,.f select:focus,.f textarea:focus{border-color:var(--acc);}\u001b[39m\n\u001b[31m.f input::placeholder{color:var(--t4);}\u001b[39m\n\u001b[31m.f textarea{resize:vertical;min-height:65px;}\u001b[39m\n\u001b[31m.f select{\u001b[39m\n\u001b[31m  background-image:url(\\\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='7'%3E%3Cpath d='M1 1l4 4 4-4' stroke='%233d5470' stroke-width='1.5' fill='none' stroke-linecap='round'/%3E%3C/svg%3E\\\");\u001b[39m\n\u001b[31m  background-repeat:no-repeat;background-position:right 10px center;padding-right:28px;cursor:pointer;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.row2{display:grid;grid-template-columns:1fr 1fr;gap:10px;}\u001b[39m\n\u001b[31m.row3{display:grid;grid-template-columns:1fr 1fr 1fr;gap:8px;}·\u001b[39m\n\u001b[31m/* ════ BUTTONS ════ */\u001b[39m\n\u001b[31m.btn{\u001b[39m\n\u001b[31m  width:100%;padding:12px;border:none;border-radius:var(--r);\u001b[39m\n\u001b[31m  font-family:var(--fnt-head);font-size:.9rem;font-weight:700;letter-spacing:.5px;\u001b[39m\n\u001b[31m  cursor:pointer;transition:all .2s;display:flex;align-items:center;justify-content:center;gap:8px;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.btn-orange{background:var(--acc);color:#fff;}\u001b[39m\n\u001b[31m.btn-orange:hover{background:var(--acc2);}\u001b[39m\n\u001b[31m.btn-ghost{background:var(--sur2);border:1px solid var(--bdr);color:var(--t1);}\u001b[39m\n\u001b[31m.btn-ghost:hover{border-color:var(--acc);color:var(--acc);}\u001b[39m\n\u001b[31m.btn-grn{background:var(--grn);color:#000;}\u001b[39m\n\u001b[31m.btn-grn:hover{opacity:.85;}\u001b[39m\n\u001b[31m.btn:active{transform:scale(.98);}\u001b[39m\n\u001b[31m.btn-row{display:grid;grid-template-columns:1fr 1fr;gap:10px;margin-top:6px;}\u001b[39m\n\u001b[31m.btn-sm{\u001b[39m\n\u001b[31m  background:none;border:1px solid var(--bdr);color:var(--t2);\u001b[39m\n\u001b[31m  font-size:.7rem;padding:5px 10px;border-radius:var(--r-sm);cursor:pointer;\u001b[39m\n\u001b[31m  font-family:var(--fnt-body);transition:all .2s;white-space:nowrap;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.btn-sm:hover{border-color:var(--acc);color:var(--acc);}\u001b[39m\n\u001b[31m.btn-sm.primary{background:var(--acc);border-color:var(--acc);color:#fff;}·\u001b[39m\n\u001b[31m/* ════ OCR UPLOAD ZONE ════ */\u001b[39m\n\u001b[31m.upload-zone{\u001b[39m\n\u001b[31m  border:2px dashed var(--bdr2);border-radius:var(--r);\u001b[39m\n\u001b[31m  padding:20px;text-align:center;cursor:pointer;transition:all .25s;\u001b[39m\n\u001b[31m  background:var(--sur2);position:relative;overflow:hidden;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.upload-zone:hover,.upload-zone.drag{border-color:var(--acc);background:rgba(249,115,22,.05);}\u001b[39m\n\u001b[31m.upload-zone .uz-icon{font-size:2rem;margin-bottom:8px;}\u001b[39m\n\u001b[31m.upload-zone .uz-title{font-family:var(--fnt-head);font-size:1rem;font-weight:700;color:var(--t1);}\u001b[39m\n\u001b[31m.upload-zone .uz-sub{font-size:.72rem;color:var(--t3);margin-top:4px;font-family:var(--fnt-mono);}\u001b[39m\n\u001b[31m.upload-zone input{display:none;}\u001b[39m\n\u001b[31m.upload-zone.share-target{border-color:var(--grn);}·\u001b[39m\n\u001b[31m/* ════ OCR RESULTS ════ */\u001b[39m\n\u001b[31m.ocr-results{display:none;}\u001b[39m\n\u001b[31m.ocr-results.visible{display:block;}\u001b[39m\n\u001b[31m.ocr-slip-list{display:flex;flex-direction:column;gap:8px;margin-top:10px;}\u001b[39m\n\u001b[31m.ocr-slip{\u001b[39m\n\u001b[31m  background:var(--sur2);border:1px solid var(--bdr);border-radius:var(--r-sm);\u001b[39m\n\u001b[31m  padding:10px 12px;display:flex;gap:10px;align-items:flex-start;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.ocr-slip img{width:60px;height:60px;object-fit:cover;border-radius:6px;flex-shrink:0;border:1px solid var(--bdr2);}\u001b[39m\n\u001b[31m.ocr-slip-body{flex:1;min-width:0;}\u001b[39m\n\u001b[31m.ocr-slip-name{font-size:.7rem;color:var(--t3);font-family:var(--fnt-mono);margin-bottom:4px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}\u001b[39m\n\u001b[31m.ocr-slip-docket{\u001b[39m\n\u001b[31m  font-family:var(--fnt-mono);font-size:.9rem;font-weight:500;color:var(--acc);\u001b[39m\n\u001b[31m  background:var(--bg);border:1px solid var(--bdr);border-radius:4px;\u001b[39m\n\u001b[31m  padding:4px 8px;width:100%;outline:none;transition:border-color .2s;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.ocr-slip-docket:focus{border-color:var(--acc);}\u001b[39m\n\u001b[31m.ocr-slip-status{font-size:.65rem;margin-top:4px;display:flex;align-items:center;gap:6px;}\u001b[39m\n\u001b[31m.ocr-confidence{padding:2px 6px;border-radius:10px;font-family:var(--fnt-mono);}\u001b[39m\n\u001b[31m.conf-high{background:var(--grn-dim);color:var(--grn);}\u001b[39m\n\u001b[31m.conf-med{background:var(--ylw-dim);color:var(--ylw);}\u001b[39m\n\u001b[31m.conf-low{background:var(--red-dim);color:var(--red);}\u001b[39m\n\u001b[31m.ocr-ai-btn{\u001b[39m\n\u001b[31m  background:rgba(56,189,248,.15);border:1px solid rgba(56,189,248,.3);\u001b[39m\n\u001b[31m  color:var(--sky);font-size:.65rem;padding:3px 8px;border-radius:10px;\u001b[39m\n\u001b[31m  cursor:pointer;font-family:var(--fnt-mono);transition:all .2s;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.ocr-ai-btn:hover{background:rgba(56,189,248,.3);}\u001b[39m\n\u001b[31m.ocr-working{\u001b[39m\n\u001b[31m  background:var(--ylw-dim);border:1px solid rgba(234,179,8,.3);\u001b[39m\n\u001b[31m  border-radius:var(--r-sm);padding:8px 12px;margin-top:8px;\u001b[39m\n\u001b[31m  font-size:.75rem;color:var(--ylw);font-family:var(--fnt-mono);\u001b[39m\n\u001b[31m  display:none;align-items:center;gap:8px;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.ocr-working.visible{display:flex;}\u001b[39m\n\u001b[31m.spin{animation:spin .8s linear infinite;display:inline-block;}\u001b[39m\n\u001b[31m@keyframes spin{to{transform:rotate(360deg);}}·\u001b[39m\n\u001b[31m/* ════ STORE LIST ════ */\u001b[39m\n\u001b[31m.store-upload-zone{\u001b[39m\n\u001b[31m  border:2px dashed var(--bdr2);border-radius:var(--r);padding:16px;\u001b[39m\n\u001b[31m  text-align:center;cursor:pointer;transition:all .25s;background:var(--sur2);\u001b[39m\n\u001b[31m  position:relative;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.store-upload-zone:hover{border-color:var(--sky);background:rgba(56,189,248,.05);}\u001b[39m\n\u001b[31m.store-upload-zone input{display:none;}\u001b[39m\n\u001b[31m.store-table-wrap{max-height:200px;overflow-y:auto;margin-top:10px;border-radius:var(--r-sm);overflow:hidden;}\u001b[39m\n\u001b[31m.store-table{width:100%;border-collapse:collapse;font-size:.75rem;}\u001b[39m\n\u001b[31m.store-table th{\u001b[39m\n\u001b[31m  background:var(--bg);color:var(--t3);font-family:var(--fnt-head);\u001b[39m\n\u001b[31m  font-size:.65rem;letter-spacing:1.5px;text-transform:uppercase;\u001b[39m\n\u001b[31m  padding:7px 8px;text-align:left;border-bottom:1px solid var(--bdr);\u001b[39m\n\u001b[31m  position:sticky;top:0;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.store-table td{\u001b[39m\n\u001b[31m  padding:6px 8px;border-bottom:1px solid var(--bdr);color:var(--t2);\u001b[39m\n\u001b[31m  font-family:var(--fnt-mono);font-size:.72rem;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.store-table tr:hover td{background:var(--sur3);}\u001b[39m\n\u001b[31m.store-table .matched{color:var(--grn);}\u001b[39m\n\u001b[31m.store-table .unmatched{color:var(--ylw);}\u001b[39m\n\u001b[31m.store-map-btn{\u001b[39m\n\u001b[31m  font-size:.65rem;background:none;border:1px solid var(--bdr);color:var(--t3);\u001b[39m\n\u001b[31m  padding:2px 6px;border-radius:4px;cursor:pointer;transition:all .2s;\u001b[39m\n\u001b[31m  font-family:var(--fnt-mono);\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.store-map-btn:hover{border-color:var(--acc);color:var(--acc);}·\u001b[39m\n\u001b[31m/* ════ CONSIGNEE CARDS ════ */\u001b[39m\n\u001b[31m.cons-list{display:flex;flex-direction:column;gap:8px;margin-bottom:10px;}\u001b[39m\n\u001b[31m.cons-card{\u001b[39m\n\u001b[31m  background:var(--sur2);border:1px solid var(--bdr);border-radius:var(--r-sm);\u001b[39m\n\u001b[31m  padding:11px;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.cons-hdr{display:flex;justify-content:space-between;align-items:center;margin-bottom:8px;}\u001b[39m\n\u001b[31m.cons-num{font-family:var(--fnt-mono);font-size:.65rem;color:var(--t3);}\u001b[39m\n\u001b[31m.cons-del{\u001b[39m\n\u001b[31m  background:none;border:none;color:var(--t4);cursor:pointer;\u001b[39m\n\u001b[31m  font-size:.85rem;padding:2px 5px;border-radius:4px;transition:all .2s;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.cons-del:hover{background:var(--red-dim);color:var(--red);}\u001b[39m\n\u001b[31m.cons-docket-linked{\u001b[39m\n\u001b[31m  display:flex;align-items:center;gap:6px;\u001b[39m\n\u001b[31m  background:rgba(249,115,22,.1);border:1px solid rgba(249,115,22,.3);\u001b[39m\n\u001b[31m  border-radius:4px;padding:4px 8px;margin-top:6px;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.cons-docket-linked .dkt{font-family:var(--fnt-mono);font-size:.82rem;color:var(--acc);}\u001b[39m\n\u001b[31m.cons-docket-linked .unlink{background:none;border:none;color:var(--acc);cursor:pointer;font-size:.75rem;}·\u001b[39m\n\u001b[31m/* ════ DISPATCH DASHBOARD ════ */\u001b[39m\n\u001b[31m.dash-stats{\u001b[39m\n\u001b[31m  display:grid;grid-template-columns:repeat(4,1fr);gap:8px;margin-bottom:14px;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.stat-box{\u001b[39m\n\u001b[31m  background:var(--sur);border:1px solid var(--bdr);border-radius:var(--r);\u001b[39m\n\u001b[31m  padding:10px 8px;text-align:center;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.stat-val{font-family:var(--fnt-head);font-size:1.4rem;font-weight:800;}\u001b[39m\n\u001b[31m.stat-lbl{font-size:.6rem;color:var(--t3);font-family:var(--fnt-mono);text-transform:uppercase;letter-spacing:1px;margin-top:2px;}\u001b[39m\n\u001b[31m.stat-box.grn .stat-val{color:var(--grn);}\u001b[39m\n\u001b[31m.stat-box.ylw .stat-val{color:var(--ylw);}\u001b[39m\n\u001b[31m.stat-box.red .stat-val{color:var(--red);}\u001b[39m\n\u001b[31m.stat-box.sky .stat-val{color:var(--sky);}·\u001b[39m\n\u001b[31m/* ════ DOCKET TILES ════ */\u001b[39m\n\u001b[31m.docket-grid{display:flex;flex-direction:column;gap:8px;}\u001b[39m\n\u001b[31m.docket-tile{\u001b[39m\n\u001b[31m  background:var(--sur);border:1px solid var(--bdr);border-radius:var(--r);\u001b[39m\n\u001b[31m  padding:12px;transition:border-color .2s;cursor:pointer;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.docket-tile:hover{border-color:var(--acc);}\u001b[39m\n\u001b[31m.docket-tile-hdr{display:flex;justify-content:space-between;align-items:flex-start;margin-bottom:8px;}\u001b[39m\n\u001b[31m.docket-tile-left{flex:1;}\u001b[39m\n\u001b[31m.docket-num{font-family:var(--fnt-mono);font-size:.95rem;font-weight:500;color:var(--sky);letter-spacing:1px;}\u001b[39m\n\u001b[31m.docket-store{font-size:.82rem;color:var(--t1);font-weight:600;margin-top:2px;}\u001b[39m\n\u001b[31m.docket-courier{font-size:.68rem;color:var(--t3);font-family:var(--fnt-mono);margin-top:2px;}\u001b[39m\n\u001b[31m.status-pill{\u001b[39m\n\u001b[31m  font-size:.62rem;font-weight:700;font-family:var(--fnt-mono);\u001b[39m\n\u001b[31m  padding:3px 8px;border-radius:10px;text-transform:uppercase;letter-spacing:.5px;\u001b[39m\n\u001b[31m  flex-shrink:0;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.sp-delivered{background:var(--grn-dim);color:var(--grn);}\u001b[39m\n\u001b[31m.sp-transit{background:var(--ylw-dim);color:var(--ylw);}\u001b[39m\n\u001b[31m.sp-booked{background:var(--sky-dim);color:var(--sky);}\u001b[39m\n\u001b[31m.sp-pending{background:var(--bdr);color:var(--t3);}\u001b[39m\n\u001b[31m.sp-failed{background:var(--red-dim);color:var(--red);}\u001b[39m\n\u001b[31m.docket-tile-footer{display:flex;align-items:center;gap:8px;flex-wrap:wrap;}\u001b[39m\n\u001b[31m.docket-last-event{font-size:.7rem;color:var(--t3);flex:1;font-family:var(--fnt-mono);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}\u001b[39m\n\u001b[31m.btn-track-live{\u001b[39m\n\u001b[31m  background:none;border:1px solid var(--acc);color:var(--acc);\u001b[39m\n\u001b[31m  font-size:.65rem;padding:4px 10px;border-radius:20px;cursor:pointer;\u001b[39m\n\u001b[31m  font-family:var(--fnt-mono);transition:all .2s;white-space:nowrap;display:flex;align-items:center;gap:4px;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.btn-track-live:hover{background:var(--acc);color:#fff;}\u001b[39m\n\u001b[31m.btn-track-live .live-dot{width:5px;height:5px;border-radius:50%;background:var(--grn);animation:blink 1.5s infinite;flex-shrink:0;}\u001b[39m\n\u001b[31m.docket-progress{height:2px;background:var(--bdr);border-radius:2px;margin-top:8px;overflow:hidden;}\u001b[39m\n\u001b[31m.docket-progress-fill{height:100%;border-radius:2px;transition:width .5s;}·\u001b[39m\n\u001b[31m/* ════ TRACKING DETAIL ════ */\u001b[39m\n\u001b[31m.track-timeline{padding:4px 0;}\u001b[39m\n\u001b[31m.track-evt{display:flex;gap:10px;padding:6px 0;position:relative;padding-left:20px;}\u001b[39m\n\u001b[31m.track-evt::before{\u001b[39m\n\u001b[31m  content:'';position:absolute;left:5px;top:11px;\u001b[39m\n\u001b[31m  width:7px;height:7px;border-radius:50%;background:var(--bdr2);border:1px solid var(--bdr2);\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.track-evt:first-child::before{background:var(--grn);border-color:var(--grn);}\u001b[39m\n\u001b[31m.track-evt::after{\u001b[39m\n\u001b[31m  content:'';position:absolute;left:8px;top:18px;\u001b[39m\n\u001b[31m  width:1px;height:calc(100% - 6px);background:var(--bdr);\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.track-evt:last-child::after{display:none;}\u001b[39m\n\u001b[31m.track-time{font-family:var(--fnt-mono);font-size:.62rem;color:var(--t3);min-width:90px;flex-shrink:0;padding-top:2px;}\u001b[39m\n\u001b[31m.track-desc{font-size:.78rem;color:var(--t2);}\u001b[39m\n\u001b[31m.track-loc{font-size:.67rem;color:var(--t3);font-family:var(--fnt-mono);margin-top:2px;}·\u001b[39m\n\u001b[31m/* ════ SHARE TOAST ════ */\u001b[39m\n\u001b[31m.share-banner{\u001b[39m\n\u001b[31m  position:fixed;top:0;left:0;right:0;z-index:200;\u001b[39m\n\u001b[31m  background:rgba(34,197,94,.15);border-bottom:1px solid rgba(34,197,94,.4);\u001b[39m\n\u001b[31m  padding:10px 16px;font-size:.78rem;color:var(--grn);font-family:var(--fnt-mono);\u001b[39m\n\u001b[31m  display:flex;align-items:center;gap:8px;\u001b[39m\n\u001b[31m  transform:translateY(-100%);transition:transform .3s;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.share-banner.show{transform:translateY(0);}·\u001b[39m\n\u001b[31m/* ════ MODAL ════ */\u001b[39m\n\u001b[31m.overlay{position:fixed;inset:0;background:rgba(0,0,0,.75);z-index:100;display:flex;align-items:flex-end;opacity:0;pointer-events:none;transition:opacity .3s;}\u001b[39m\n\u001b[31m.overlay.open{opacity:1;pointer-events:all;}\u001b[39m\n\u001b[31m.sheet{\u001b[39m\n\u001b[31m  background:var(--sur);border-radius:var(--r-lg) var(--r-lg) 0 0;\u001b[39m\n\u001b[31m  width:100%;max-height:88vh;overflow-y:auto;padding:18px;\u001b[39m\n\u001b[31m  transform:translateY(100%);transition:transform .3s cubic-bezier(.32,.72,0,1);\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.overlay.open .sheet{transform:translateY(0);}\u001b[39m\n\u001b[31m.sheet-handle{width:36px;height:4px;background:var(--bdr2);border-radius:2px;margin:0 auto 18px;}\u001b[39m\n\u001b[31m.sheet-title{font-family:var(--fnt-head);font-size:1.2rem;font-weight:800;letter-spacing:.5px;margin-bottom:14px;}\u001b[39m\n\u001b[31m.sheet::-webkit-scrollbar{width:3px;}\u001b[39m\n\u001b[31m.sheet::-webkit-scrollbar-thumb{background:var(--bdr2);}·\u001b[39m\n\u001b[31m/* ════ TOAST ════ */\u001b[39m\n\u001b[31m.toast{\u001b[39m\n\u001b[31m  position:fixed;bottom:20px;left:50%;transform:translateX(-50%) translateY(70px);\u001b[39m\n\u001b[31m  background:var(--sur2);border:1px solid var(--bdr2);color:var(--t1);\u001b[39m\n\u001b[31m  font-size:.78rem;padding:9px 16px;border-radius:30px;z-index:300;\u001b[39m\n\u001b[31m  transition:transform .3s;white-space:nowrap;font-family:var(--fnt-mono);\u001b[39m\n\u001b[31m  pointer-events:none;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.toast.show{transform:translateX(-50%) translateY(0);}\u001b[39m\n\u001b[31m.toast.ok{border-color:var(--grn);color:var(--grn);}\u001b[39m\n\u001b[31m.toast.err{border-color:var(--red);color:var(--red);}\u001b[39m\n\u001b[31m.toast.info{border-color:var(--sky);color:var(--sky);}·\u001b[39m\n\u001b[31m/* ════ COURIER MASTER ════ */\u001b[39m\n\u001b[31m.courier-master-row{\u001b[39m\n\u001b[31m  display:flex;align-items:center;gap:8px;padding:8px 0;\u001b[39m\n\u001b[31m  border-bottom:1px solid var(--bdr);\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.courier-master-row:last-child{border-bottom:none;}\u001b[39m\n\u001b[31m.cml{flex:1;}\u001b[39m\n\u001b[31m.cml-name{font-weight:600;font-size:.85rem;}\u001b[39m\n\u001b[31m.cml-url{font-family:var(--fnt-mono);font-size:.65rem;color:var(--t3);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:180px;}\u001b[39m\n\u001b[31m.courier-logo{width:24px;height:24px;border-radius:4px;object-fit:contain;background:var(--sur3);padding:2px;}·\u001b[39m\n\u001b[31m/* ════ PROGRESS BAR ════ */\u001b[39m\n\u001b[31m.ai-progress{\u001b[39m\n\u001b[31m  background:var(--sur2);border:1px solid var(--bdr);border-radius:var(--r-sm);\u001b[39m\n\u001b[31m  padding:10px 12px;margin-top:8px;display:none;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.ai-progress.visible{display:block;}\u001b[39m\n\u001b[31m.ai-prog-bar{height:3px;background:var(--bdr);border-radius:3px;margin-top:6px;overflow:hidden;}\u001b[39m\n\u001b[31m.ai-prog-fill{height:100%;background:linear-gradient(90deg,var(--sky),var(--acc));width:0;transition:width .3s;}\u001b[39m\n\u001b[31m.ai-prog-label{font-size:.68rem;color:var(--sky);font-family:var(--fnt-mono);}·\u001b[39m\n\u001b[31m/* ════ EMPTY ════ */\u001b[39m\n\u001b[31m.empty{padding:40px 20px;text-align:center;color:var(--t4);}\u001b[39m\n\u001b[31m.empty .e-icon{font-size:2.5rem;margin-bottom:10px;}\u001b[39m\n\u001b[31m.empty p{font-size:.82rem;}·\u001b[39m\n\u001b[31m/* ════ INFO TAG ════ */\u001b[39m\n\u001b[31m.info-tag{\u001b[39m\n\u001b[31m  background:var(--sur2);border:1px solid var(--bdr);border-radius:var(--r-sm);\u001b[39m\n\u001b[31m  padding:7px 10px;font-size:.75rem;color:var(--t2);margin-bottom:8px;\u001b[39m\n\u001b[31m  display:flex;align-items:center;gap:8px;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.info-tag strong{color:var(--t1);}·\u001b[39m\n\u001b[31m/* ════ JOB PILL ════ */\u001b[39m\n\u001b[31m.job-pill{\u001b[39m\n\u001b[31m  display:inline-flex;align-items:center;gap:6px;\u001b[39m\n\u001b[31m  background:rgba(249,115,22,.1);border:1px solid rgba(249,115,22,.3);\u001b[39m\n\u001b[31m  color:var(--acc);font-family:var(--fnt-mono);font-size:.68rem;\u001b[39m\n\u001b[31m  padding:3px 9px;border-radius:20px;margin-top:4px;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.job-pill button{background:none;border:none;color:var(--acc);cursor:pointer;font-size:.7rem;}·\u001b[39m\n\u001b[31m/* ════ SECTION DIVIDER ════ */\u001b[39m\n\u001b[31m.sec-div{\u001b[39m\n\u001b[31m  display:flex;align-items:center;gap:10px;margin:14px 0 10px;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.sec-div span{font-family:var(--fnt-head);font-size:.7rem;font-weight:700;color:var(--t3);text-transform:uppercase;letter-spacing:2px;white-space:nowrap;}\u001b[39m\n\u001b[31m.sec-div::before,.sec-div::after{content:'';flex:1;height:1px;background:var(--bdr);}·\u001b[39m\n\u001b[31m/* ── MAPPING UI ── */\u001b[39m\n\u001b[31m.map-table{width:100%;border-collapse:collapse;font-size:.78rem;margin:10px 0;}\u001b[39m\n\u001b[31m.map-table th{background:var(--bg);color:var(--t3);font-family:var(--fnt-head);font-size:.65rem;\u001b[39m\n\u001b[31m  letter-spacing:1.5px;text-transform:uppercase;padding:7px 10px;text-align:left;\u001b[39m\n\u001b[31m  border-bottom:1px solid var(--bdr);}\u001b[39m\n\u001b[31m.map-table td{padding:7px 10px;border-bottom:1px solid var(--bdr);vertical-align:middle;}\u001b[39m\n\u001b[31m.map-table tr:last-child td{border-bottom:none;}\u001b[39m\n\u001b[31m.map-table select{background:var(--sur2);border:1px solid var(--bdr);color:var(--t1);\u001b[39m\n\u001b[31m  font-family:var(--fnt-mono);font-size:.75rem;padding:5px 8px;border-radius:var(--r-sm);\u001b[39m\n\u001b[31m  width:100%;outline:none;}\u001b[39m\n\u001b[31m.map-table select:focus{border-color:var(--acc);}\u001b[39m\n\u001b[31m.map-req{color:var(--acc);margin-left:2px;}\u001b[39m\n\u001b[31m.map-saved-badge{font-size:.6rem;background:rgba(57,255,143,.15);color:var(--accent3,#39ff8f);\u001b[39m\n\u001b[31m  border:1px solid rgba(57,255,143,.3);padding:2px 7px;border-radius:10px;\u001b[39m\n\u001b[31m  font-family:var(--fnt-mono);margin-left:6px;}·\u001b[39m\n\u001b[31m/* ── VERIFICATION SCREEN ── */\u001b[39m\n\u001b[31m#verifyPane{display:none;position:fixed;inset:0;background:var(--bg);z-index:50;\u001b[39m\n\u001b[31m  overflow-y:auto;padding:14px;}\u001b[39m\n\u001b[31m.verify-hdr{display:flex;align-items:center;gap:10px;margin-bottom:14px;\u001b[39m\n\u001b[31m  position:sticky;top:0;background:var(--bg);padding-bottom:10px;\u001b[39m\n\u001b[31m  border-bottom:1px solid var(--bdr);z-index:1;}\u001b[39m\n\u001b[31m.verify-title{font-family:var(--fnt-head);font-size:1.1rem;font-weight:800;flex:1;}\u001b[39m\n\u001b[31m.verify-row{background:var(--sur);border:1px solid var(--bdr);border-radius:var(--r-sm);\u001b[39m\n\u001b[31m  padding:10px 12px;margin-bottom:8px;position:relative;}\u001b[39m\n\u001b[31m.verify-row.has-dupe{border-color:var(--red);background:rgba(239,68,68,.04);}\u001b[39m\n\u001b[31m.verify-row.no-docket{border-color:var(--ylw);background:rgba(234,179,8,.04);}\u001b[39m\n\u001b[31m.verify-row.ok{border-left:3px solid var(--grn);}\u001b[39m\n\u001b[31m.verify-row-hdr{display:flex;align-items:center;gap:8px;margin-bottom:8px;}\u001b[39m\n\u001b[31m.verify-num{font-family:var(--fnt-mono);font-size:.65rem;color:var(--t3);}\u001b[39m\n\u001b[31m.verify-store{font-weight:600;font-size:.9rem;flex:1;}\u001b[39m\n\u001b[31m.verify-flag{font-size:.65rem;font-weight:700;font-family:var(--fnt-mono);\u001b[39m\n\u001b[31m  padding:2px 7px;border-radius:10px;text-transform:uppercase;}\u001b[39m\n\u001b[31m.vf-dupe{background:var(--red-dim);color:var(--red);}\u001b[39m\n\u001b[31m.vf-nodocket{background:var(--ylw-dim);color:var(--ylw);}\u001b[39m\n\u001b[31m.vf-ok{background:var(--grn-dim);color:var(--grn);}\u001b[39m\n\u001b[31m.vf-short{background:rgba(56,189,248,.15);color:var(--sky);}\u001b[39m\n\u001b[31m.verify-fields{display:grid;grid-template-columns:1fr 1fr;gap:8px;margin-bottom:8px;}\u001b[39m\n\u001b[31m.verify-field label{font-size:.62rem;color:var(--t3);text-transform:uppercase;\u001b[39m\n\u001b[31m  letter-spacing:1px;display:block;margin-bottom:3px;font-family:var(--fnt-mono);}\u001b[39m\n\u001b[31m.verify-field input{width:100%;background:var(--sur2);border:1px solid var(--bdr);\u001b[39m\n\u001b[31m  color:var(--t1);font-family:var(--fnt-mono);font-size:.82rem;padding:6px 9px;\u001b[39m\n\u001b[31m  border-radius:var(--r-sm);outline:none;}\u001b[39m\n\u001b[31m.verify-field input:focus{border-color:var(--acc);}\u001b[39m\n\u001b[31m.verify-field input.changed{border-color:var(--ylw);background:rgba(234,179,8,.06);}\u001b[39m\n\u001b[31m.verify-actions{display:flex;gap:6px;align-items:center;flex-wrap:wrap;}\u001b[39m\n\u001b[31m.btn-wa{background:rgba(37,211,102,.15);border:1px solid rgba(37,211,102,.3);\u001b[39m\n\u001b[31m  color:#25d366;font-size:.65rem;padding:4px 10px;border-radius:10px;cursor:pointer;\u001b[39m\n\u001b[31m  font-family:var(--fnt-mono);transition:all .2s;}\u001b[39m\n\u001b[31m.btn-wa:hover{background:rgba(37,211,102,.3);}\u001b[39m\n\u001b[31m.verify-thumb{width:48px;height:48px;object-fit:cover;border-radius:6px;\u001b[39m\n\u001b[31m  border:1px solid var(--bdr);cursor:pointer;flex-shrink:0;}\u001b[39m\n\u001b[31m.verify-summary{background:var(--sur);border:1px solid var(--bdr);border-radius:var(--r);\u001b[39m\n\u001b[31m  padding:12px 14px;margin-bottom:14px;display:flex;gap:16px;flex-wrap:wrap;}\u001b[39m\n\u001b[31m.vs-item{text-align:center;}\u001b[39m\n\u001b[31m.vs-val{font-family:var(--fnt-head);font-size:1.3rem;font-weight:800;}\u001b[39m\n\u001b[31m.vs-lbl{font-size:.6rem;color:var(--t3);font-family:var(--fnt-mono);text-transform:uppercase;\u001b[39m\n\u001b[31m  letter-spacing:.5px;margin-top:2px;}\u001b[39m\n\u001b[31m.ocr-correction-note{font-size:.65rem;color:var(--ylw);font-family:var(--fnt-mono);\u001b[39m\n\u001b[31m  margin-top:3px;}\u001b[39m\n\u001b[31m</style>\u001b[39m\n\u001b[31m</head>\u001b[39m\n\u001b[31m<body>·\u001b[39m\n\u001b[31m<!-- SPLASH -->\u001b[39m\n\u001b[31m<div id=\\\"splash\\\">\u001b[39m\n\u001b[31m  <div class=\\\"splash-logo\\\">360LM <em>DISPATCH</em></div>\u001b[39m\n\u001b[31m  <div class=\\\"splash-sub\\\">Courier · Bus · Field Ops</div>\u001b[39m\n\u001b[31m  <div class=\\\"splash-track\\\"><div class=\\\"splash-track-fill\\\"></div></div>\u001b[39m\n\u001b[31m</div>·\u001b[39m\n\u001b[31m<!-- SHARE BANNER -->\u001b[39m\n\u001b[31m<div class=\\\"share-banner\\\" id=\\\"shareBanner\\\">\u001b[39m\n\u001b[31m  📤 Images received via Share — processing for OCR...\u001b[39m\n\u001b[31m  <button style=\\\"background:none;border:none;color:var(--grn);cursor:pointer;margin-left:auto;\\\" onclick=\\\"dismissShare()\\\">✕</button>\u001b[39m\n\u001b[31m</div>·\u001b[39m\n\u001b[31m<!-- APP -->\u001b[39m\n\u001b[31m<div id=\\\"app\\\" style=\\\"display:none;\\\">·\u001b[39m\n\u001b[31m  <div class=\\\"hdr\\\">\u001b[39m\n\u001b[31m    <div class=\\\"hdr-brand\\\">360LM <em>DISPATCH</em></div>\u001b[39m\n\u001b[31m    <span class=\\\"hdr-dot\\\" id=\\\"netDot\\\"></span>\u001b[39m\n\u001b[31m    <button class=\\\"hdr-btn\\\" onclick=\\\"window.location.href='/hub/'\\\" title=\\\"Hub\\\">🏡</button>\u001b[39m\n\u001b[31m    <button class=\\\"hdr-btn\\\" onclick=\\\"showPane('dashboard')\\\">📊</button>\u001b[39m\n\u001b[31m    <button class=\\\"hdr-btn primary\\\" onclick=\\\"newDispatch()\\\">+ NEW</button>\u001b[39m\n\u001b[31m  </div>·\u001b[39m\n\u001b[31m  <div class=\\\"tabs\\\">\u001b[39m\n\u001b[31m    <button class=\\\"tab active\\\" onclick=\\\"showPane('new')\\\" id=\\\"tab-new\\\">NEW</button>\u001b[39m\n\u001b[31m    <button class=\\\"tab\\\" onclick=\\\"showPane('dashboard')\\\" id=\\\"tab-dashboard\\\">DASHBOARD</button>\u001b[39m\n\u001b[31m    <button class=\\\"tab\\\" onclick=\\\"showPane('track')\\\" id=\\\"tab-track\\\">TRACK</button>\u001b[39m\n\u001b[31m    <button class=\\\"tab\\\" onclick=\\\"showPane('history')\\\" id=\\\"tab-history\\\">HISTORY <span class=\\\"tab-badge\\\" id=\\\"histCount\\\">0</span></button>\u001b[39m\n\u001b[31m    <button class=\\\"tab\\\" onclick=\\\"showPane('config')\\\" id=\\\"tab-config\\\">CONFIG</button>\u001b[39m\n\u001b[31m  </div>·\u001b[39m\n\u001b[31m  <!-- ════════════ NEW DISPATCH ════════════ -->\u001b[39m\n\u001b[31m  <div class=\\\"pane active\\\" id=\\\"pane-new\\\">·\u001b[39m\n\u001b[31m    <!-- 1. JOB REFERENCE -->\u001b[39m\n\u001b[31m    <div class=\\\"card accent-left\\\">\u001b[39m\n\u001b[31m      <div class=\\\"card-title\\\"><span class=\\\"ct-dot\\\"></span>Job Reference</div>\u001b[39m\n\u001b[31m      <div class=\\\"row2\\\">\u001b[39m\n\u001b[31m        <div class=\\\"f\\\">\u001b[39m\n\u001b[31m          <label>Client <span class=\\\"req\\\">*</span></label>\u001b[39m\n\u001b[31m          <select id=\\\"n_client\\\" onchange=\\\"onClientChange()\\\">\u001b[39m\n\u001b[31m            <option value=\\\"\\\">Select...</option>\u001b[39m\n\u001b[31m            <option>Lenovo</option><option>HP</option>\u001b[39m\n\u001b[31m            <option>Philips</option><option>Liebherr</option>\u001b[39m\n\u001b[31m            <option value=\\\"__c\\\">+ Custom</option>\u001b[39m\n\u001b[31m          </select>\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m        <div class=\\\"f\\\">\u001b[39m\n\u001b[31m          <label>Job Ref</label>\u001b[39m\n\u001b[31m          <input type=\\\"text\\\" id=\\\"n_jobref\\\" placeholder=\\\"JOB-042\\\" oninput=\\\"updatePill()\\\">\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      <div class=\\\"f\\\" id=\\\"customClientField\\\" style=\\\"display:none;\\\">\u001b[39m\n\u001b[31m        <label>Custom Client</label>\u001b[39m\n\u001b[31m        <input type=\\\"text\\\" id=\\\"n_customClient\\\" placeholder=\\\"Client name\\\" oninput=\\\"updatePill()\\\">\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      <div id=\\\"jobPill\\\" style=\\\"display:none;\\\" class=\\\"job-pill\\\">\u001b[39m\n\u001b[31m        🔗 <span id=\\\"jobPillTxt\\\"></span>\u001b[39m\n\u001b[31m        <button onclick=\\\"clearJob()\\\">✕</button>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      <div class=\\\"row2\\\" style=\\\"margin-top:10px;\\\">\u001b[39m\n\u001b[31m        <div class=\\\"f\\\" style=\\\"margin-bottom:0;\\\">\u001b[39m\n\u001b[31m          <label>Dispatch Date</label>\u001b[39m\n\u001b[31m          <input type=\\\"date\\\" id=\\\"n_date\\\">\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m        <div class=\\\"f\\\" style=\\\"margin-bottom:0;\\\">\u001b[39m\n\u001b[31m          <label>Courier Company <span class=\\\"req\\\">*</span></label>\u001b[39m\n\u001b[31m          <select id=\\\"n_courier\\\">\u001b[39m\n\u001b[31m            <option value=\\\"\\\">Select...</option>\u001b[39m\n\u001b[31m            <option>SkyKing</option><option>DTDC</option>\u001b[39m\n\u001b[31m            <option>Delhivery</option><option>Blue Dart</option>\u001b[39m\n\u001b[31m            <option>FedEx</option><option>Ecom Express</option>\u001b[39m\n\u001b[31m            <option>India Post</option><option value=\\\"__o\\\">Other</option>\u001b[39m\n\u001b[31m          </select>\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m    </div>·\u001b[39m\n\u001b[31m    <!-- 2. STORE LIST UPLOAD -->\u001b[39m\n\u001b[31m    <div class=\\\"card\\\">\u001b[39m\n\u001b[31m      <div class=\\\"card-title\\\"><span class=\\\"ct-dot\\\" style=\\\"background:var(--sky);\\\"></span>Store / Destination List</div>\u001b[39m\n\u001b[31m      <div class=\\\"store-upload-zone\\\" id=\\\"storeZone\\\" onclick=\\\"document.getElementById('storeFile').click();event.stopPropagation();\\\" style=\\\"cursor:pointer;\\\">\u001b[39m\n\u001b[31m        <input type=\\\"file\\\" id=\\\"storeFile\\\" accept=\\\".xlsx,.xls,.csv\\\" onchange=\\\"handleStoreFile(this)\\\" onclick=\\\"event.stopPropagation();\\\" style=\\\"display:none;\\\">\u001b[39m\n\u001b[31m        <div style=\\\"font-size:1.5rem;margin-bottom:6px;\\\">📋</div>\u001b[39m\n\u001b[31m        <div style=\\\"font-family:var(--fnt-head);font-weight:700;font-size:.9rem;\\\">Upload Store List</div>\u001b[39m\n\u001b[31m        <div style=\\\"font-size:.68rem;color:var(--t3);margin-top:3px;font-family:var(--fnt-mono);\\\">Excel / CSV · stores, cities, contacts</div>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      <div id=\\\"storeTableArea\\\" style=\\\"display:none;\\\">\u001b[39m\n\u001b[31m        <!-- Column detection result -->\u001b[39m\n\u001b[31m        <div id=\\\"storeColInfo\\\" style=\\\"margin-top:10px;display:none;\\\"></div>\u001b[39m\n\u001b[31m        <div style=\\\"display:flex;justify-content:space-between;align-items:center;margin-top:10px;margin-bottom:6px;\\\">\u001b[39m\n\u001b[31m          <span style=\\\"font-family:var(--fnt-mono);font-size:.7rem;color:var(--sky);\\\" id=\\\"storeCount\\\">0 stores loaded</span>\u001b[39m\n\u001b[31m          <button class=\\\"btn-sm\\\" onclick=\\\"clearStores()\\\">Clear</button>\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m        <div class=\\\"store-table-wrap\\\">\u001b[39m\n\u001b[31m          <table class=\\\"store-table\\\" id=\\\"storeTable\\\"></table>\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m        <!-- Inline add-as-consignees UI instead of confirm() -->\u001b[39m\n\u001b[31m        <div id=\\\"storeAddPanel\\\" style=\\\"margin-top:10px;background:var(--sur3);border:1px solid var(--bdr2);border-radius:var(--r-sm);padding:10px 12px;display:none;\\\">\u001b[39m\n\u001b[31m          <div style=\\\"font-size:.75rem;color:var(--t1);margin-bottom:8px;font-weight:600;\\\" id=\\\"storeAddTitle\\\">Add stores as consignees?</div>\u001b[39m\n\u001b[31m          <div class=\\\"row2\\\">\u001b[39m\n\u001b[31m            <button class=\\\"btn btn-ghost\\\" style=\\\"font-size:.75rem;padding:8px;\\\" onclick=\\\"document.getElementById('storeAddPanel').style.display='none'\\\">Skip for now</button>\u001b[39m\n\u001b[31m            <button class=\\\"btn btn-orange\\\" style=\\\"font-size:.75rem;padding:8px;\\\" id=\\\"storeAddBtn\\\" onclick=\\\"addStoresToConsignees()\\\">✓ Add All as Consignees</button>\u001b[39m\n\u001b[31m          </div>\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m        <button class=\\\"btn btn-ghost\\\" style=\\\"margin-top:8px;font-size:.78rem;\\\" onclick=\\\"autoMatchDockets()\\\">\u001b[39m\n\u001b[31m          🔗 Auto-match Dockets to Stores\u001b[39m\n\u001b[31m        </button>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m    </div>·\u001b[39m\n\u001b[31m    <!-- 3. DOCKET SLIPS — AI OCR -->\u001b[39m\n\u001b[31m    <div class=\\\"card\\\">\u001b[39m\n\u001b[31m      <div class=\\\"card-title\\\"><span class=\\\"ct-dot\\\" style=\\\"background:var(--grn);\\\"></span>Docket Slips — AI OCR</div>·\u001b[39m\n\u001b[31m      <!-- Share target indicator -->\u001b[39m\n\u001b[31m      <div class=\\\"info-tag\\\" id=\\\"shareTargetInfo\\\" style=\\\"display:none;\\\">\u001b[39m\n\u001b[31m        <span>📤</span><span>Images shared from WhatsApp — <strong id=\\\"sharedCount\\\">0</strong> slips received</span>\u001b[39m\n\u001b[31m      </div>·\u001b[39m\n\u001b[31m      <label class=\\\"upload-zone\\\" id=\\\"slipZone\\\" for=\\\"slipFiles\\\" style=\\\"display:block;cursor:pointer;\\\">\u001b[39m\n\u001b[31m        <input type=\\\"file\\\" id=\\\"slipFiles\\\" accept=\\\"image/*\\\" multiple onchange=\\\"handleSlipFiles(this.files)\\\" style=\\\"display:none;\\\">\u001b[39m\n\u001b[31m        <div class=\\\"uz-icon\\\">🖼️</div>\u001b[39m\n\u001b[31m        <div class=\\\"uz-title\\\">Upload Courier Slips</div>\u001b[39m\n\u001b[31m        <div class=\\\"uz-sub\\\">Multiple images · Tap or drag · Camera capture supported</div>\u001b[39m\n\u001b[31m      </label>·\u001b[39m\n\u001b[31m      <!-- OCR Working indicator -->\u001b[39m\n\u001b[31m      <div class=\\\"ocr-working\\\" id=\\\"ocrWorking\\\">\u001b[39m\n\u001b[31m        <span class=\\\"spin\\\">⟳</span>\u001b[39m\n\u001b[31m        <span id=\\\"ocrWorkingText\\\">Running OCR...</span>\u001b[39m\n\u001b[31m      </div>·\u001b[39m\n\u001b[31m      <!-- AI Progress -->\u001b[39m\n\u001b[31m      <div class=\\\"ai-progress\\\" id=\\\"aiProgress\\\">\u001b[39m\n\u001b[31m        <div class=\\\"ai-prog-label\\\" id=\\\"aiProgLabel\\\">Initializing Transformers.js...</div>\u001b[39m\n\u001b[31m        <div class=\\\"ai-prog-bar\\\"><div class=\\\"ai-prog-fill\\\" id=\\\"aiProgFill\\\"></div></div>\u001b[39m\n\u001b[31m      </div>·\u001b[39m\n\u001b[31m      <!-- OCR Results -->\u001b[39m\n\u001b[31m      <div class=\\\"ocr-results\\\" id=\\\"ocrResults\\\">\u001b[39m\n\u001b[31m        <div class=\\\"sec-div\\\"><span>Detected Dockets</span></div>\u001b[39m\n\u001b[31m        <div id=\\\"ocrSummary\\\" style=\\\"display:none;font-size:.72rem;font-family:var(--fnt-mono);\u001b[39m\n\u001b[31m          padding:4px 8px 8px;gap:8px\\\"></div>\u001b[39m\n\u001b[31m        <div class=\\\"ocr-slip-list\\\" id=\\\"ocrSlipList\\\"></div>\u001b[39m\n\u001b[31m        <div class=\\\"btn-row\\\" style=\\\"margin-top:10px;\\\">\u001b[39m\n\u001b[31m          <button class=\\\"btn btn-ghost\\\" onclick=\\\"clearSlips()\\\">Clear All</button>\u001b[39m\n\u001b[31m          <button class=\\\"btn btn-grn\\\" onclick=\\\"applyAllDockets()\\\">✓ Apply All to Consignees</button>\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m    </div>·\u001b[39m\n\u001b[31m    <!-- 4. CONSIGNEES -->\u001b[39m\n\u001b[31m    <div class=\\\"card accent-left\\\">\u001b[39m\n\u001b[31m      <div class=\\\"card-title\\\"><span class=\\\"ct-dot\\\"></span>Consignees</div>\u001b[39m\n\u001b[31m      <div class=\\\"cons-list\\\" id=\\\"consList\\\"></div>\u001b[39m\n\u001b[31m      <button class=\\\"btn btn-ghost\\\" style=\\\"font-size:.8rem;\\\" onclick=\\\"addConsignee()\\\">+ Add Consignee</button>\u001b[39m\n\u001b[31m    </div>·\u001b[39m\n\u001b[31m    <!-- 5. REMARKS -->\u001b[39m\n\u001b[31m    <div class=\\\"card\\\">\u001b[39m\n\u001b[31m      <div class=\\\"card-title\\\"><span class=\\\"ct-dot\\\" style=\\\"background:var(--t3);\\\"></span>Remarks</div>\u001b[39m\n\u001b[31m      <div class=\\\"f\\\" style=\\\"margin-bottom:0;\\\">\u001b[39m\n\u001b[31m        <textarea id=\\\"n_remarks\\\" placeholder=\\\"Instructions, notes, special handling...\\\"></textarea>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m    </div>·\u001b[39m\n\u001b[31m    <!-- ACTIONS -->\u001b[39m\n\u001b[31m    <div class=\\\"btn-row\\\">\u001b[39m\n\u001b[31m      <button class=\\\"btn btn-ghost\\\" onclick=\\\"saveDraft()\\\">💾 Draft</button>\u001b[39m\n\u001b[31m      <button class=\\\"btn btn-orange\\\" onclick=\\\"finaliseDispatch()\\\">✓ Finalise & Log</button>\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m    <div style=\\\"height:16px;\\\"></div>\u001b[39m\n\u001b[31m  </div>·\u001b[39m\n\u001b[31m  <!-- ════════════ DASHBOARD ════════════ -->\u001b[39m\n\u001b[31m  <div class=\\\"pane\\\" id=\\\"pane-dashboard\\\">\u001b[39m\n\u001b[31m    <div class=\\\"dash-stats\\\">\u001b[39m\n\u001b[31m      <div class=\\\"stat-box grn\\\"><div class=\\\"stat-val\\\" id=\\\"st-delivered\\\">0</div><div class=\\\"stat-lbl\\\">Delivered</div></div>\u001b[39m\n\u001b[31m      <div class=\\\"stat-box ylw\\\"><div class=\\\"stat-val\\\" id=\\\"st-transit\\\">0</div><div class=\\\"stat-lbl\\\">In Transit</div></div>\u001b[39m\n\u001b[31m      <div class=\\\"stat-box sky\\\"><div class=\\\"stat-val\\\" id=\\\"st-booked\\\">0</div><div class=\\\"stat-lbl\\\">Booked</div></div>\u001b[39m\n\u001b[31m      <div class=\\\"stat-box red\\\"><div class=\\\"stat-val\\\" id=\\\"st-pending\\\">0</div><div class=\\\"stat-lbl\\\">Pending</div></div>\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m    <div id=\\\"dashFilters\\\" style=\\\"display:flex;gap:6px;flex-wrap:wrap;margin-bottom:12px;\\\">\u001b[39m\n\u001b[31m      <span class=\\\"btn-sm primary\\\" onclick=\\\"filterDash('all',this)\\\" style=\\\"cursor:pointer;\\\">All</span>\u001b[39m\n\u001b[31m      <span class=\\\"btn-sm\\\" onclick=\\\"filterDash('delivered',this)\\\" style=\\\"cursor:pointer;\\\">Delivered</span>\u001b[39m\n\u001b[31m      <span class=\\\"btn-sm\\\" onclick=\\\"filterDash('transit',this)\\\" style=\\\"cursor:pointer;\\\">In Transit</span>\u001b[39m\n\u001b[31m      <span class=\\\"btn-sm\\\" onclick=\\\"filterDash('booked',this)\\\" style=\\\"cursor:pointer;\\\">Booked</span>\u001b[39m\n\u001b[31m      <span class=\\\"btn-sm\\\" onclick=\\\"filterDash('pending',this)\\\" style=\\\"cursor:pointer;\\\">Pending</span>\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m    <div class=\\\"docket-grid\\\" id=\\\"dashGrid\\\">\u001b[39m\n\u001b[31m      <div class=\\\"empty\\\"><div class=\\\"e-icon\\\">📊</div><p>No dispatches yet.<br>Create one to see the dashboard.</p></div>\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m    <div style=\\\"display:flex;gap:8px;margin-top:10px;flex-wrap:wrap;\\\">\u001b[39m\n\u001b[31m      <button class=\\\"btn btn-ghost\\\" style=\\\"flex:2;font-size:.8rem;\\\" id=\\\"btnRefreshAll\\\" onclick=\\\"refreshAllTracking()\\\">\u001b[39m\n\u001b[31m        🔄 Refresh All Tracking\u001b[39m\n\u001b[31m      </button>\u001b[39m\n\u001b[31m      <button class=\\\"btn btn-ghost\\\" style=\\\"flex:1;font-size:.8rem;border-color:var(--sky);color:var(--sky);\\\"\u001b[39m\n\u001b[31m        onclick=\\\"shareSelectedReport()\\\">\u001b[39m\n\u001b[31m        📊 Share Selected\u001b[39m\n\u001b[31m      </button>\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m  </div>·\u001b[39m\n\u001b[31m  <!-- ════════════ TRACK ════════════ -->\u001b[39m\n\u001b[31m  <div class=\\\"pane\\\" id=\\\"pane-track\\\">·\u001b[39m\n\u001b[31m    <!-- Courier selector -->\u001b[39m\n\u001b[31m    <div class=\\\"card\\\" style=\\\"margin-bottom:10px;\\\">\u001b[39m\n\u001b[31m      <div class=\\\"card-title\\\"><span class=\\\"ct-dot\\\" style=\\\"background:var(--sky);\\\"></span>Manual Tracking</div>\u001b[39m\n\u001b[31m      <div style=\\\"font-size:.72rem;color:var(--t2);margin-bottom:10px;line-height:1.5;\\\">\u001b[39m\n\u001b[31m        Track any docket number — select from already dispatched dockets or enter new ones manually.\u001b[39m\n\u001b[31m        SkyKing supports up to <strong>10 dockets at once</strong>; others are tracked one at a time.\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      <div class=\\\"f\\\">\u001b[39m\n\u001b[31m        <label>Courier Company</label>\u001b[39m\n\u001b[31m        <select id=\\\"t_courier\\\" onchange=\\\"buildTrackDocketChips()\\\">\u001b[39m\n\u001b[31m          <option>SkyKing</option><option>DTDC</option><option>Delhivery</option>\u001b[39m\n\u001b[31m          <option>Blue Dart</option><option>FedEx</option><option>Ecom Express</option><option>India Post</option>\u001b[39m\n\u001b[31m        </select>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m    </div>·\u001b[39m\n\u001b[31m    <!-- Docket selector — from existing dispatches -->\u001b[39m\n\u001b[31m    <div class=\\\"card\\\" style=\\\"margin-bottom:10px;\\\">\u001b[39m\n\u001b[31m      <div class=\\\"card-title\\\"><span class=\\\"ct-dot\\\" style=\\\"background:var(--acc);\\\"></span>\u001b[39m\n\u001b[31m        Select from Dispatched Dockets\u001b[39m\n\u001b[31m        <span id=\\\"trackCourierLimit\\\" style=\\\"margin-left:auto;font-size:.65rem;color:var(--t3);font-weight:400;text-transform:none;letter-spacing:0;font-family:var(--fnt-mono);\\\"></span>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      <div id=\\\"trackDocketChips\\\" style=\\\"display:flex;flex-wrap:wrap;gap:6px;margin-bottom:10px;min-height:28px;\\\">\u001b[39m\n\u001b[31m        <span style=\\\"font-size:.72rem;color:var(--t3);font-family:var(--fnt-mono);\\\">No dispatched dockets yet</span>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      <div style=\\\"font-size:.68rem;color:var(--t3);font-family:var(--fnt-mono);margin-bottom:8px;\\\">\u001b[39m\n\u001b[31m        Tap a docket to add/remove from tracking list. Selected shown in orange.\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m    </div>·\u001b[39m\n\u001b[31m    <!-- Manual entry for new dockets -->\u001b[39m\n\u001b[31m    <div class=\\\"card\\\" style=\\\"margin-bottom:10px;\\\">\u001b[39m\n\u001b[31m      <div class=\\\"card-title\\\"><span class=\\\"ct-dot\\\" style=\\\"background:var(--grn);\\\"></span>Add New Docket Numbers</div>\u001b[39m\n\u001b[31m      <div class=\\\"f\\\" style=\\\"margin-bottom:6px;\\\">\u001b[39m\n\u001b[31m        <label>Enter docket numbers (comma or newline separated)</label>\u001b[39m\n\u001b[31m        <textarea id=\\\"t_manual_dockets\\\" placeholder=\\\"443005527&#10;443005528&#10;or: 443005527, 443005528\\\"\u001b[39m\n\u001b[31m          style=\\\"min-height:56px;font-family:var(--fnt-mono);font-size:.82rem;letter-spacing:.5px;\\\"></textarea>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      <button class=\\\"btn-sm primary\\\" onclick=\\\"addManualDockets()\\\" style=\\\"font-size:.72rem;\\\">+ Add to List</button>\u001b[39m\n\u001b[31m    </div>·\u001b[39m\n\u001b[31m    <!-- Selected dockets summary + track button -->\u001b[39m\n\u001b[31m    <div id=\\\"trackSelectedSummary\\\" style=\\\"display:none;\\\" class=\\\"card\\\" style=\\\"margin-bottom:10px;\\\">\u001b[39m\n\u001b[31m      <div style=\\\"display:flex;align-items:center;justify-content:space-between;margin-bottom:8px;\\\">\u001b[39m\n\u001b[31m        <div>\u001b[39m\n\u001b[31m          <span style=\\\"font-family:var(--fnt-mono);font-size:.72rem;color:var(--sky);\\\" id=\\\"trackSelectedCount\\\">0 selected</span>\u001b[39m\n\u001b[31m          <span style=\\\"font-family:var(--fnt-mono);font-size:.65rem;color:var(--t3);margin-left:8px;\\\" id=\\\"trackSelectedList\\\"></span>\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m        <button class=\\\"btn-sm\\\" onclick=\\\"clearTrackSelection()\\\">Clear</button>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      <div class=\\\"btn-row\\\">\u001b[39m\n\u001b[31m        <button class=\\\"btn btn-ghost\\\" onclick=\\\"openTrackOnCourierSite()\\\">🔗 Open Courier Site</button>\u001b[39m\n\u001b[31m        <button class=\\\"btn btn-orange\\\" onclick=\\\"trackSelected()\\\">🔍 Track All via Proxy</button>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m    </div>·\u001b[39m\n\u001b[31m    <div id=\\\"trackResultsArea\\\"></div>\u001b[39m\n\u001b[31m  </div>·\u001b[39m\n\u001b[31m  <!-- ════════════ HISTORY ════════════ -->\u001b[39m\n\u001b[31m  <div class=\\\"pane\\\" id=\\\"pane-history\\\">\u001b[39m\n\u001b[31m    <div id=\\\"historyList\\\">\u001b[39m\n\u001b[31m      <div class=\\\"empty\\\"><div class=\\\"e-icon\\\">📭</div><p>No dispatch history yet.</p></div>\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m  </div>·\u001b[39m\n\u001b[31m  <!-- ════════════ CONFIG ════════════ -->\u001b[39m\n\u001b[31m  <div class=\\\"pane\\\" id=\\\"pane-config\\\">·\u001b[39m\n\u001b[31m    <!-- SHEETS URL — the only thing you type manually in the PWA -->\u001b[39m\n\u001b[31m    <div class=\\\"card\\\" style=\\\"border-color:var(--acc);border-width:2px;\\\">\u001b[39m\n\u001b[31m      <div class=\\\"card-title\\\"><span class=\\\"ct-dot\\\"></span>Google Sheets — Master Config</div>\u001b[39m\n\u001b[31m      <div class=\\\"info-tag\\\" style=\\\"background:rgba(34,197,94,.07);border-color:rgba(34,197,94,.3);\\\">\u001b[39m\n\u001b[31m        <span>✅</span>\u001b[39m\n\u001b[31m        <span>URL is loaded automatically from <strong>config.json</strong> on the VPS — no manual entry needed. All other settings sync from your <strong>Google Sheet</strong>.</span>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      <div class=\\\"f\\\">\u001b[39m\n\u001b[31m        <label>Apps Script URL <span style=\\\"font-size:.65rem;color:var(--t3);font-weight:400;\\\">(auto-loaded from config.json)</span></label>\u001b[39m\n\u001b[31m        <input type=\\\"url\\\" id=\\\"cfg_sheets_url\\\"\u001b[39m\n\u001b[31m          placeholder=\\\"Auto-loaded from server...\\\"\u001b[39m\n\u001b[31m          oninput=\\\"onSheetsUrlChange()\\\" readonly\u001b[39m\n\u001b[31m          style=\\\"opacity:.7;cursor:default;\\\">\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      <div id=\\\"syncStatus\\\" class=\\\"info-tag\\\" style=\\\"display:none;\\\">\u001b[39m\n\u001b[31m        <span id=\\\"syncIcon\\\">⏳</span>\u001b[39m\n\u001b[31m        <span id=\\\"syncMsg\\\">Syncing from sheet...</span>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      <div class=\\\"btn-row\\\">\u001b[39m\n\u001b[31m        <button class=\\\"btn btn-ghost\\\" onclick=\\\"syncFromSheet()\\\">🔄 Sync Now</button>\u001b[39m\n\u001b[31m        <button class=\\\"btn btn-orange\\\" onclick=\\\"saveSheetsUrl()\\\">💾 Save & Sync</button>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m    </div>·\u001b[39m\n\u001b[31m    <!-- READ-ONLY display of what was loaded from sheet -->\u001b[39m\n\u001b[31m    <div class=\\\"card\\\" id=\\\"sheetConfigDisplay\\\" style=\\\"display:none;\\\">\u001b[39m\n\u001b[31m      <div class=\\\"card-title\\\"><span class=\\\"ct-dot\\\" style=\\\"background:var(--grn);\\\"></span>Config Loaded from Sheet\u001b[39m\n\u001b[31m        <span id=\\\"sheetSyncTime\\\" style=\\\"margin-left:auto;font-size:.62rem;color:var(--t3);font-family:var(--fnt-mono);font-weight:400;text-transform:none;letter-spacing:0;\\\"></span>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      <div id=\\\"sheetConfigRows\\\"></div>\u001b[39m\n\u001b[31m      <div class=\\\"info-tag\\\" style=\\\"margin-top:8px;\\\">\u001b[39m\n\u001b[31m        <span>✏️</span><span>To change any value, edit the <strong>Config sheet</strong> in Google Sheets, then tap Sync Now above.</span>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m    </div>·\u001b[39m\n\u001b[31m    <!-- Courier Master (from sheet) -->\u001b[39m\n\u001b[31m    <div class=\\\"card\\\">\u001b[39m\n\u001b[31m      <div class=\\\"card-title\\\"><span class=\\\"ct-dot\\\" style=\\\"background:var(--ylw);\\\"></span>Courier Master</div>\u001b[39m\n\u001b[31m      <div id=\\\"courierMasterList\\\"></div>\u001b[39m\n\u001b[31m      <button class=\\\"btn btn-ghost\\\" style=\\\"margin-top:8px;font-size:.78rem;\\\" onclick=\\\"openCourierMaster()\\\">✏️ Edit in Sheet / Override</button>\u001b[39m\n\u001b[31m    </div>·\u001b[39m\n\u001b[31m    <button class=\\\"btn\\\" style=\\\"background:var(--red-dim);color:var(--red);border:1px solid var(--red);margin-top:4px;\\\" onclick=\\\"clearAll()\\\">🗑️ Clear All Local Data</button>\u001b[39m\n\u001b[31m    <div style=\\\"height:20px;\\\"></div>\u001b[39m\n\u001b[31m  </div>··\u001b[39m\n\u001b[31m<!-- ════ ADDRESS REVIEW PANE ════ -->\u001b[39m\n\u001b[31m<div id=\\\"addrReviewPane\\\" style=\\\"display:none;position:fixed;inset:0;background:var(--bg);z-index:50;overflow-y:auto;padding:14px;\\\">\u001b[39m\n\u001b[31m  <div class=\\\"verify-hdr\\\">\u001b[39m\n\u001b[31m    <button class=\\\"btn-icon\\\" onclick=\\\"document.getElementById('addrReviewPane').style.display='none'\\\" style=\\\"font-size:1.1rem;\\\">←</button>\u001b[39m\n\u001b[31m    <div class=\\\"verify-title\\\">Review AI-Parsed Addresses</div>\u001b[39m\n\u001b[31m  </div>\u001b[39m\n\u001b[31m  <div style=\\\"background:rgba(56,189,248,.08);border:1px solid rgba(56,189,248,.3);border-radius:var(--r-sm);\u001b[39m\n\u001b[31m    padding:8px 12px;margin-bottom:12px;font-size:.72rem;color:var(--sky);\\\">\u001b[39m\n\u001b[31m    ✏️ Review each row — correct name and city if wrong. Raw address shown below each row for reference.\u001b[39m\n\u001b[31m    Corrections are saved and improve future AI parsing automatically.\u001b[39m\n\u001b[31m  </div>\u001b[39m\n\u001b[31m  <div class=\\\"verify-summary\\\" id=\\\"addrReviewSummary\\\"></div>\u001b[39m\n\u001b[31m  <div id=\\\"addrReviewList\\\"></div>\u001b[39m\n\u001b[31m  <div style=\\\"position:sticky;bottom:0;background:var(--bg);padding-top:10px;\u001b[39m\n\u001b[31m    border-top:1px solid var(--bdr);margin-top:8px;\\\">\u001b[39m\n\u001b[31m    <div style=\\\"display:flex;gap:8px;\\\">\u001b[39m\n\u001b[31m      <button class=\\\"btn btn-ghost\\\" onclick=\\\"backToMapping()\\\" style=\\\"flex:1;\\\">← Back to Mapping</button>\u001b[39m\n\u001b[31m      <button class=\\\"btn btn-orange\\\" onclick=\\\"confirmAddressReview()\\\" style=\\\"flex:2;\\\">✓ Confirm &amp; Use These Addresses</button>\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m    <div style=\\\"font-size:.65rem;color:var(--t3);text-align:center;margin-top:5px;font-family:var(--fnt-mono);\\\">\u001b[39m\n\u001b[31m      Edits you made above are preserved when you go back\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m  </div>\u001b[39m\n\u001b[31m</div>·\u001b[39m\n\u001b[31m<!-- ════ VERIFICATION SCREEN ════ -->\u001b[39m\n\u001b[31m<div id=\\\"verifyPane\\\">\u001b[39m\n\u001b[31m  <div class=\\\"verify-hdr\\\">\u001b[39m\n\u001b[31m    <button class=\\\"btn-icon\\\" onclick=\\\"closeVerify()\\\" style=\\\"font-size:1.1rem;\\\">←</button>\u001b[39m\n\u001b[31m    <div class=\\\"verify-title\\\">Verify Before Finalise</div>\u001b[39m\n\u001b[31m    <button class=\\\"btn-sm\\\" onclick=\\\"approveAll()\\\" style=\\\"white-space:nowrap;\\\">✓ Approve All</button>\u001b[39m\n\u001b[31m  </div>\u001b[39m\n\u001b[31m  <div class=\\\"verify-summary\\\" id=\\\"verifySummary\\\"></div>\u001b[39m\n\u001b[31m  <div id=\\\"verifyList\\\"></div>\u001b[39m\n\u001b[31m  <div style=\\\"position:sticky;bottom:0;background:var(--bg);padding-top:10px;\u001b[39m\n\u001b[31m    border-top:1px solid var(--bdr);margin-top:8px;\\\">\u001b[39m\n\u001b[31m    <div style=\\\"display:flex;gap:8px;flex-wrap:wrap;\\\">\u001b[39m\n\u001b[31m      <button class=\\\"btn btn-ghost\\\" onclick=\\\"closeVerify()\\\" style=\\\"flex:1;\\\">← Back to Edit</button>\u001b[39m\n\u001b[31m      <button class=\\\"btn btn-ghost\\\" onclick=\\\"exportCleanSheet()\\\" style=\\\"flex:1;border-color:var(--sky);color:var(--sky);\\\">\u001b[39m\n\u001b[31m        📥 Export Clean Sheet\u001b[39m\n\u001b[31m      </button>\u001b[39m\n\u001b[31m      <button class=\\\"btn btn-orange\\\" id=\\\"btnConfirmFinalise\\\" onclick=\\\"confirmAndFinalise()\\\" style=\\\"flex:2;\\\">\u001b[39m\n\u001b[31m        ✓ Finalise Dispatch\u001b[39m\n\u001b[31m      </button>\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m  </div>\u001b[39m\n\u001b[31m</div>·\u001b[39m\n\u001b[31m</div><!-- /app -->·\u001b[39m\n\u001b[31m<!-- ════ DISPATCH DETAIL SHEET ════ -->\u001b[39m\n\u001b[31m<div class=\\\"overlay\\\" id=\\\"detailOverlay\\\" onclick=\\\"closeOverlay('detailOverlay')\\\">\u001b[39m\n\u001b[31m  <div class=\\\"sheet\\\" onclick=\\\"event.stopPropagation()\\\">\u001b[39m\n\u001b[31m    <div class=\\\"sheet-handle\\\"></div>\u001b[39m\n\u001b[31m    <div id=\\\"detailContent\\\"></div>\u001b[39m\n\u001b[31m  </div>\u001b[39m\n\u001b[31m</div>·\u001b[39m\n\u001b[31m<!-- ════ COURIER MASTER EDITOR ════ -->\u001b[39m\n\u001b[31m<div class=\\\"overlay\\\" id=\\\"courierMasterOverlay\\\" onclick=\\\"closeOverlay('courierMasterOverlay')\\\">\u001b[39m\n\u001b[31m  <div class=\\\"sheet\\\" onclick=\\\"event.stopPropagation()\\\">\u001b[39m\n\u001b[31m    <div class=\\\"sheet-handle\\\"></div>\u001b[39m\n\u001b[31m    <div class=\\\"sheet-title\\\">✏️ Courier Master</div>\u001b[39m\n\u001b[31m    <div id=\\\"courierMasterEditor\\\"></div>\u001b[39m\n\u001b[31m  </div>\u001b[39m\n\u001b[31m</div>·\u001b[39m\n\u001b[31m<!-- ════ MANUAL TRACKING OVERLAY ════ -->\u001b[39m\n\u001b[31m<div class=\\\"overlay\\\" id=\\\"manualTrackOverlay\\\" onclick=\\\"closeOverlay('manualTrackOverlay')\\\" data-docket=\\\"\\\" data-dispatch-id=\\\"\\\">\u001b[39m\n\u001b[31m  <div class=\\\"sheet\\\" onclick=\\\"event.stopPropagation()\\\">\u001b[39m\n\u001b[31m    <div class=\\\"sheet-handle\\\"></div>\u001b[39m\n\u001b[31m    <div class=\\\"sheet-title\\\">📞 Manual Tracking Log</div>\u001b[39m\n\u001b[31m    <div style=\\\"background:var(--sur2);border:1px solid var(--bdr);border-radius:var(--r-sm);\u001b[39m\n\u001b[31m      padding:8px 12px;margin-bottom:12px;font-family:var(--fnt-mono);font-size:.82rem;color:var(--acc);\\\"\u001b[39m\n\u001b[31m      id=\\\"mtDocketDisplay\\\"></div>\u001b[39m\n\u001b[31m    <div class=\\\"row2\\\">\u001b[39m\n\u001b[31m      <div class=\\\"f\\\">\u001b[39m\n\u001b[31m        <label>Status Reported <span class=\\\"req\\\">*</span></label>\u001b[39m\n\u001b[31m        <select id=\\\"mtStatus\\\">\u001b[39m\n\u001b[31m          <option value=\\\"\\\">Select...</option>\u001b[39m\n\u001b[31m          <option value=\\\"delivered\\\">✅ Delivered</option>\u001b[39m\n\u001b[31m          <option value=\\\"not received\\\">❌ Not Received</option>\u001b[39m\n\u001b[31m          <option value=\\\"in transit\\\">🚚 In Transit / On the way</option>\u001b[39m\n\u001b[31m          <option value=\\\"will deliver tomorrow\\\">📅 Will Deliver Tomorrow</option>\u001b[39m\n\u001b[31m          <option value=\\\"refused\\\">🚫 Refused / Returned</option>\u001b[39m\n\u001b[31m          <option value=\\\"partial\\\">📦 Partial Delivery</option>\u001b[39m\n\u001b[31m        </select>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      <div class=\\\"f\\\">\u001b[39m\n\u001b[31m        <label>Called By (Staff) <span class=\\\"req\\\">*</span></label>\u001b[39m\n\u001b[31m        <input type=\\\"text\\\" id=\\\"mtCalledBy\\\" placeholder=\\\"Your name\\\" list=\\\"staffList\\\">\u001b[39m\n\u001b[31m        <datalist id=\\\"staffList\\\">\u001b[39m\n\u001b[31m          <option>Mukesh</option><option>Sachin</option><option>Shambhu</option>\u001b[39m\n\u001b[31m          <option>Umashankar</option><option>Narender</option><option>Rakesh</option>\u001b[39m\n\u001b[31m          <option>Gurpreet</option><option>Ashok</option>\u001b[39m\n\u001b[31m        </datalist>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m    <div class=\\\"row2\\\">\u001b[39m\n\u001b[31m      <div class=\\\"f\\\">\u001b[39m\n\u001b[31m        <label>Contact Number Used</label>\u001b[39m\n\u001b[31m        <input type=\\\"tel\\\" id=\\\"mtContactUsed\\\" placeholder=\\\"Mobile number called\\\">\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      <div class=\\\"f\\\">\u001b[39m\n\u001b[31m        <label>Person Who Confirmed</label>\u001b[39m\n\u001b[31m        <input type=\\\"text\\\" id=\\\"mtContactPerson\\\" placeholder=\\\"Name of person at store\\\">\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m    <div class=\\\"f\\\">\u001b[39m\n\u001b[31m      <label>Remarks</label>\u001b[39m\n\u001b[31m      <textarea id=\\\"mtRemarks\\\" placeholder=\\\"Any notes, delivery details, issues...\\\" style=\\\"min-height:60px;\\\"></textarea>\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m    <div class=\\\"f\\\">\u001b[39m\n\u001b[31m      <label>Call Date &amp; Time</label>\u001b[39m\n\u001b[31m      <input type=\\\"datetime-local\\\" id=\\\"mtCallTime\\\">\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m    <div class=\\\"btn-row\\\">\u001b[39m\n\u001b[31m      <button class=\\\"btn btn-ghost\\\" onclick=\\\"closeOverlay('manualTrackOverlay')\\\">Cancel</button>\u001b[39m\n\u001b[31m      <button class=\\\"btn btn-orange\\\" onclick=\\\"submitManualTracking()\\\">💾 Log &amp; Save</button>\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m  </div>\u001b[39m\n\u001b[31m</div>·\u001b[39m\n\u001b[31m<!-- ════ POD UPLOAD OVERLAY ════ -->\u001b[39m\n\u001b[31m<div class=\\\"overlay\\\" id=\\\"podOverlay\\\" onclick=\\\"closeOverlay('podOverlay')\\\" data-docket=\\\"\\\" data-dispatch-id=\\\"\\\">\u001b[39m\n\u001b[31m  <div class=\\\"sheet\\\" onclick=\\\"event.stopPropagation()\\\">\u001b[39m\n\u001b[31m    <div class=\\\"sheet-handle\\\"></div>\u001b[39m\n\u001b[31m    <div class=\\\"sheet-title\\\">📸 Upload Proof of Delivery</div>\u001b[39m\n\u001b[31m    <div class=\\\"f\\\">\u001b[39m\n\u001b[31m      <label>POD / Delivery Slip Photo</label>\u001b[39m\n\u001b[31m      <input type=\\\"file\\\" id=\\\"podFileInput\\\" accept=\\\"image/*\\\" capture=\\\"environment\\\"\u001b[39m\n\u001b[31m        onchange=\\\"previewPOD(this,'pod')\\\" style=\\\"font-size:.8rem;color:var(--t2);\\\">\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m    <div class=\\\"f\\\">\u001b[39m\n\u001b[31m      <label>Recipient / Receiver Photo (optional)</label>\u001b[39m\n\u001b[31m      <input type=\\\"file\\\" id=\\\"recipientFileInput\\\" accept=\\\"image/*\\\" capture=\\\"environment\\\"\u001b[39m\n\u001b[31m        onchange=\\\"previewPOD(this,'recip')\\\" style=\\\"font-size:.8rem;color:var(--t2);\\\">\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m    <div id=\\\"podPreview\\\" style=\\\"display:flex;gap:8px;flex-wrap:wrap;margin-bottom:12px;\\\"></div>\u001b[39m\n\u001b[31m    <div class=\\\"btn-row\\\">\u001b[39m\n\u001b[31m      <button class=\\\"btn btn-ghost\\\" onclick=\\\"closeOverlay('podOverlay')\\\">Cancel</button>\u001b[39m\n\u001b[31m      <button class=\\\"btn btn-orange\\\" onclick=\\\"uploadPODFiles()\\\">📤 Upload & Mark Delivered</button>\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m  </div>\u001b[39m\n\u001b[31m</div>·\u001b[39m\n\u001b[31m<!-- ════ ADMIN LOGIN OVERLAY ════ -->\u001b[39m\n\u001b[31m<div class=\\\"overlay\\\" id=\\\"adminLoginOverlay\\\" onclick=\\\"closeOverlay('adminLoginOverlay')\\\">\u001b[39m\n\u001b[31m  <div class=\\\"sheet\\\" onclick=\\\"event.stopPropagation()\\\" style=\\\"max-height:320px;\\\">\u001b[39m\n\u001b[31m    <div class=\\\"sheet-handle\\\"></div>\u001b[39m\n\u001b[31m    <div class=\\\"sheet-title\\\">🔐 Admin Access</div>\u001b[39m\n\u001b[31m    <div style=\\\"font-size:.78rem;color:var(--t2);margin-bottom:14px;\\\">Config settings require admin password.</div>\u001b[39m\n\u001b[31m    <div class=\\\"f\\\">\u001b[39m\n\u001b[31m      <label>Password</label>\u001b[39m\n\u001b[31m      <input type=\\\"password\\\" id=\\\"adminPwdInput\\\" placeholder=\\\"Enter admin password\\\"\u001b[39m\n\u001b[31m        onkeydown=\\\"if(event.key==='Enter')submitAdminLogin()\\\">\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m    <div class=\\\"btn-row\\\" style=\\\"margin-top:4px;\\\">\u001b[39m\n\u001b[31m      <button class=\\\"btn btn-ghost\\\" onclick=\\\"closeOverlay('adminLoginOverlay')\\\">Cancel</button>\u001b[39m\n\u001b[31m      <button class=\\\"btn btn-orange\\\" onclick=\\\"submitAdminLogin()\\\">🔓 Login</button>\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m  </div>\u001b[39m\n\u001b[31m</div>·\u001b[39m\n\u001b[31m<!-- ════ TOAST ════ -->\u001b[39m\n\u001b[31m<div class=\\\"toast\\\" id=\\\"toast\\\"></div>·\u001b[39m\n\u001b[31m<!-- ════ SCRIPTS ════ -->\u001b[39m\n\u001b[31m<!-- SheetJS for Excel/CSV parsing -->\u001b[39m\n\u001b[31m<script src=\\\"https://cdnjs.cloudflare.com/ajax/libs/xlsx/0.18.5/xlsx.full.min.js\\\"></script>·\u001b[39m\n\u001b[31m<script type=\\\"module\\\">\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31m// OCR — Transformers.js\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31mimport { pipeline, env } from 'https://cdn.jsdelivr.net/npm/@huggingface/transformers@3.5.2/dist/transformers.min.js';·\u001b[39m\n\u001b[31m// Cache models in browser\u001b[39m\n\u001b[31menv.allowLocalModels = false;\u001b[39m\n\u001b[31menv.useBrowserCache = true;·\u001b[39m\n\u001b[31mlet ocrPipeline = null;\u001b[39m\n\u001b[31mlet ocrLoading = false;·\u001b[39m\n\u001b[31mwindow.initOCR = async function() {\u001b[39m\n\u001b[31m  if (ocrPipeline || ocrLoading) return;\u001b[39m\n\u001b[31m  ocrLoading = true;\u001b[39m\n\u001b[31m  showAIProgress('Loading OCR model (first time ~120MB)...', 5);\u001b[39m\n\u001b[31m  try {\u001b[39m\n\u001b[31m    ocrPipeline = await pipeline('image-to-text', 'Xenova/trocr-base-printed', {\u001b[39m\n\u001b[31m      progress_callback: (p) => {\u001b[39m\n\u001b[31m        if (p.progress) {\u001b[39m\n\u001b[31m          showAIProgress(`Loading: ${p.file || 'model'} (${Math.round(p.progress)}%)`, p.progress);\u001b[39m\n\u001b[31m        }\u001b[39m\n\u001b[31m      }\u001b[39m\n\u001b[31m    });\u001b[39m\n\u001b[31m    showAIProgress('OCR model ready ✓', 100);\u001b[39m\n\u001b[31m    setTimeout(() => hideAIProgress(), 1500);\u001b[39m\n\u001b[31m    showToast('OCR ready ✓', 'ok');\u001b[39m\n\u001b[31m  } catch(e) {\u001b[39m\n\u001b[31m    console.warn('Transformers.js OCR failed:', e);\u001b[39m\n\u001b[31m    hideAIProgress();\u001b[39m\n\u001b[31m    showToast('OCR model load failed — will use Ollama fallback', 'info');\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m  ocrLoading = false;\u001b[39m\n\u001b[31m};·\u001b[39m\n\u001b[31mwindow.runOCROnImage = async function(imageData, filename) {\u001b[39m\n\u001b[31m  const cfg = window.appConfig;·\u001b[39m\n\u001b[31m  // Strategy 1: Transformers.js\u001b[39m\n\u001b[31m  if ((cfg.ocr_mode === 'transformers' || cfg.ocr_mode === 'both') && ocrPipeline) {\u001b[39m\n\u001b[31m    try {\u001b[39m\n\u001b[31m      const result = await ocrPipeline(imageData);\u001b[39m\n\u001b[31m      const text = result[0]?.generated_text || '';\u001b[39m\n\u001b[31m      return extractDocketFromText(text, 'transformers');\u001b[39m\n\u001b[31m    } catch(e) { /* fall through */ }\u001b[39m\n\u001b[31m  }·\u001b[39m\n\u001b[31m  // Strategy 2: Dispatch AI proxy → Ollama LLaVA (server-side, avoids CORS + port issues)\u001b[39m\n\u001b[31m  if (cfg.ocr_mode === 'ollama' || cfg.ocr_mode === 'both') {\u001b[39m\n\u001b[31m    try {\u001b[39m\n\u001b[31m      const resp = await fetch('/dispatch-ai/ocr', {\u001b[39m\n\u001b[31m        method: 'POST',\u001b[39m\n\u001b[31m        headers: { 'Content-Type': 'application/json' },\u001b[39m\n\u001b[31m        body: JSON.stringify({ imageBase64: imageData }),\u001b[39m\n\u001b[31m        signal: AbortSignal.timeout(180000)\u001b[39m\n\u001b[31m      });\u001b[39m\n\u001b[31m      const data = await resp.json();\u001b[39m\n\u001b[31m      if (data.success && data.docket) {\u001b[39m\n\u001b[31m        return { docket: data.docket, confidence: data.confidence, source: 'ollama' };\u001b[39m\n\u001b[31m      }\u001b[39m\n\u001b[31m    } catch(e) { /* fall through */ }\u001b[39m\n\u001b[31m  }·\u001b[39m\n\u001b[31m  return { docket: '', confidence: 0, source: 'failed' };\u001b[39m\n\u001b[31m};·\u001b[39m\n\u001b[31mfunction extractDocketFromText(text, source) {\u001b[39m\n\u001b[31m  // Pattern: typical courier docket = 8-15 digit numbers, sometimes with letters\u001b[39m\n\u001b[31m  const patterns = [\u001b[39m\n\u001b[31m    /\\\\b([A-Z]{0,3}\\\\d{8,15}[A-Z]{0,3})\\\\b/g,   // alphanumeric tracking\u001b[39m\n\u001b[31m    /\\\\b(\\\\d{9,15})\\\\b/g,                          // pure numeric\u001b[39m\n\u001b[31m    /\\\\b([A-Z]{2,4}\\\\d{6,12})\\\\b/g,               // letter prefix\u001b[39m\n\u001b[31m  ];\u001b[39m\n\u001b[31m  const candidates = [];\u001b[39m\n\u001b[31m  for (const pat of patterns) {\u001b[39m\n\u001b[31m    let m;\u001b[39m\n\u001b[31m    while ((m = pat.exec(text)) !== null) {\u001b[39m\n\u001b[31m      if (!candidates.includes(m[1])) candidates.push(m[1]);\u001b[39m\n\u001b[31m    }\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m  if (candidates.length === 0) return { docket: text.trim().slice(0, 20), confidence: 0.3, source };\u001b[39m\n\u001b[31m  // Pick longest candidate (usually most complete docket)\u001b[39m\n\u001b[31m  const best = candidates.sort((a, b) => b.length - a.length)[0];\u001b[39m\n\u001b[31m  const confidence = candidates.length === 1 ? 0.85 : 0.7;\u001b[39m\n\u001b[31m  return { docket: best, confidence, source };\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// Expose for non-module code\u001b[39m\n\u001b[31mwindow._ocrReady = () => ocrPipeline !== null;\u001b[39m\n\u001b[31m</script>·\u001b[39m\n\u001b[31m<script>\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31m// STATE & STORAGE\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31mlet dispatches = [];\u001b[39m\n\u001b[31mlet stores = [];\u001b[39m\n\u001b[31mlet slipOCRData = [];\u001b[39m\n\u001b[31mlet dashFilter = 'all';·\u001b[39m\n\u001b[31m// Only sheets_url is persisted locally — all other config comes from the sheet\u001b[39m\n\u001b[31mlet _trackingInProgress = false;\u001b[39m\n\u001b[31mwindow.appConfig = {\u001b[39m\n\u001b[31m  sheets_url:      'https://script.google.com/macros/s/AKfycbys8HrToDv2u_dRvSFnNqmO9bnyAO1s8oSprUydpVQnNQutq4tuUY71cVCH24urbvM9/exec',\u001b[39m\n\u001b[31m  webhook_url:     '',\u001b[39m\n\u001b[31m  proxy_url:       '',\u001b[39m\n\u001b[31m  ollama_url:      'http://srv1111289.hstgr.cloud:11434',\u001b[39m\n\u001b[31m  ocr_mode:        'both',\u001b[39m\n\u001b[31m  company:         '360 Degree Logical Marketing',\u001b[39m\n\u001b[31m  city:            'Chandigarh',\u001b[39m\n\u001b[31m  auto_log:        'true',\u001b[39m\n\u001b[31m  refresh_on_load: 'false',\u001b[39m\n\u001b[31m};·\u001b[39m\n\u001b[31mconst COURIER_MASTER = {\u001b[39m\n\u001b[31m  'SkyKing': {\u001b[39m\n\u001b[31m    trackUrl: 'https://skyking.co/track?cno={docket}',\u001b[39m\n\u001b[31m    trackBulkUrl: 'https://skyking.co/track',\u001b[39m\n\u001b[31m    apiProxy: '/track-proxy/skyking?cno={docket}',\u001b[39m\n\u001b[31m    maxBulk: 10\u001b[39m\n\u001b[31m  },\u001b[39m\n\u001b[31m  'DTDC': {\u001b[39m\n\u001b[31m    trackUrl: 'https://www.dtdc.in/tracking.asp',\u001b[39m\n\u001b[31m    queryParam: 'trkId',\u001b[39m\n\u001b[31m    maxBulk: 1\u001b[39m\n\u001b[31m  },\u001b[39m\n\u001b[31m  'Delhivery': {\u001b[39m\n\u001b[31m    trackUrl: 'https://www.delhivery.com/track/package/{docket}',\u001b[39m\n\u001b[31m    maxBulk: 1\u001b[39m\n\u001b[31m  },\u001b[39m\n\u001b[31m  'Blue Dart': {\u001b[39m\n\u001b[31m    trackUrl: 'https://www.bluedart.com/tracking',\u001b[39m\n\u001b[31m    maxBulk: 1\u001b[39m\n\u001b[31m  },\u001b[39m\n\u001b[31m  'FedEx': {\u001b[39m\n\u001b[31m    trackUrl: 'https://www.fedex.com/fedextrack/?trknbr={docket}',\u001b[39m\n\u001b[31m    maxBulk: 10\u001b[39m\n\u001b[31m  },\u001b[39m\n\u001b[31m  'Ecom Express': {\u001b[39m\n\u001b[31m    trackUrl: 'https://ecomexpress.in/tracking/?awb_field={docket}',\u001b[39m\n\u001b[31m    maxBulk: 1\u001b[39m\n\u001b[31m  },\u001b[39m\n\u001b[31m  'India Post': {\u001b[39m\n\u001b[31m    trackUrl: 'https://www.indiapost.gov.in/VAS/Pages/trackconsignment.aspx',\u001b[39m\n\u001b[31m    maxBulk: 1\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m};·\u001b[39m\n\u001b[31mfunction loadData() {\u001b[39m\n\u001b[31m  try {\u001b[39m\n\u001b[31m    dispatches = JSON.parse(localStorage.getItem('360lm_disp_v2') || '[]');\u001b[39m\n\u001b[31m    const saved = JSON.parse(localStorage.getItem('360lm_disp_sheets_url') || '{}');\u001b[39m\n\u001b[31m    if (saved.sheets_url) window.appConfig.sheets_url = saved.sheets_url;\u001b[39m\n\u001b[31m  } catch(e) {}\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31mfunction saveData() {\u001b[39m\n\u001b[31m  try {\u001b[39m\n\u001b[31m    localStorage.setItem('360lm_disp_v2', JSON.stringify(dispatches));\u001b[39m\n\u001b[31m  } catch(e) {\u001b[39m\n\u001b[31m    if (e.name === 'QuotaExceededError' || e.code === 22) {\u001b[39m\n\u001b[31m      showToast('Storage full — old dispatches cleared. Data is safe in Postgres.', 'err');\u001b[39m\n\u001b[31m      // Drop delivered dispatches older than 30 days to free space\u001b[39m\n\u001b[31m      const cutoff = Date.now() - 30 * 24 * 3600 * 1000;\u001b[39m\n\u001b[31m      dispatches = dispatches.filter(d =>\u001b[39m\n\u001b[31m        d.status !== 'draft' && (\u001b[39m\n\u001b[31m          d.status !== 'active' ||\u001b[39m\n\u001b[31m          new Date(d.createdAt || 0).getTime() > cutoff\u001b[39m\n\u001b[31m        ) ? true :\u001b[39m\n\u001b[31m        d.status === 'draft' || new Date(d.createdAt || 0).getTime() > cutoff\u001b[39m\n\u001b[31m      );\u001b[39m\n\u001b[31m      try { localStorage.setItem('360lm_disp_v2', JSON.stringify(dispatches)); } catch(_) {}\u001b[39m\n\u001b[31m    }\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// ── Postgres DB sync ──────────────────────────────────────────────────────\u001b[39m\n\u001b[31mconst _DB        = '/db';\u001b[39m\n\u001b[31m// PostgREST v12: Accept-Profile for reads, Content-Profile for writes\u001b[39m\n\u001b[31mconst _DHDR_READ  = { 'Accept-Profile': 'dispatch' };\u001b[39m\n\u001b[31mconst _DHDR_WRITE = { 'Content-Type': 'application/json', 'Content-Profile': 'dispatch', 'Accept-Profile': 'dispatch' };\u001b[39m\n\u001b[31mconst _DHDR_UPSERT = { ..._DHDR_WRITE, 'Prefer': 'resolution=merge-duplicates,return=minimal' };·\u001b[39m\n\u001b[31mfunction _dispRow(d) {\u001b[39m\n\u001b[31m  return {\u001b[39m\n\u001b[31m    dispatch_id:    d.id,\u001b[39m\n\u001b[31m    client:         d.client    || '',\u001b[39m\n\u001b[31m    job_ref:        d.jobref    || null,\u001b[39m\n\u001b[31m    courier:        d.courier   || '',\u001b[39m\n\u001b[31m    dispatch_date:  d.dispatchDate || null,\u001b[39m\n\u001b[31m    status:         d.status    || 'active',\u001b[39m\n\u001b[31m    total_count:    (d.consignees || []).length,\u001b[39m\n\u001b[31m    docketed_count: (d.consignees || []).filter(c => c.docket).length,\u001b[39m\n\u001b[31m    remarks:        d.remarks   || null,\u001b[39m\n\u001b[31m    created_by:     window._hubUser?.id || 'unknown',\u001b[39m\n\u001b[31m  };\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction _consRow(c, dispatchId, idx) {\u001b[39m\n\u001b[31m  return {\u001b[39m\n\u001b[31m    cons_id:            `${dispatchId}-${idx}`,\u001b[39m\n\u001b[31m    dispatch_id:        dispatchId,\u001b[39m\n\u001b[31m    seq_no:             idx,\u001b[39m\n\u001b[31m    name:               c.name          || '',\u001b[39m\n\u001b[31m    city:               c.city          || null,\u001b[39m\n\u001b[31m    address:            c.address       || null,\u001b[39m\n\u001b[31m    phone:              c.phone         || null,\u001b[39m\n\u001b[31m    pincode:            c.pincode       || null,\u001b[39m\n\u001b[31m    material:           c.material      || null,\u001b[39m\n\u001b[31m    qty:                c.qty           || null,\u001b[39m\n\u001b[31m    docket:             c.docket        || null,\u001b[39m\n\u001b[31m    status:             c.status        || 'pending',\u001b[39m\n\u001b[31m    last_event:         c.lastEvent     || null,\u001b[39m\n\u001b[31m    last_scan_location: c.lastScanLocation || null,\u001b[39m\n\u001b[31m    last_tracked_at:    c.lastTrackedAt || null,\u001b[39m\n\u001b[31m    manual_status:      c.manualStatus  || null,\u001b[39m\n\u001b[31m    manual_delivered:   !!c.manualDelivered,\u001b[39m\n\u001b[31m    manual_called_by:   c.manualCalledBy || null,\u001b[39m\n\u001b[31m    manual_remarks:     c.manualRemarks || null,\u001b[39m\n\u001b[31m    pod_uploaded:       !!c.podUploaded,\u001b[39m\n\u001b[31m  };\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31masync function syncDispatchToDB(d) {\u001b[39m\n\u001b[31m  if (!d || d.status === 'draft') return; // never sync drafts\u001b[39m\n\u001b[31m  try {\u001b[39m\n\u001b[31m    await fetch(`${_DB}/dispatches`, {\u001b[39m\n\u001b[31m      method: 'POST', headers: _DHDR_UPSERT,\u001b[39m\n\u001b[31m      body: JSON.stringify(_dispRow(d)),\u001b[39m\n\u001b[31m    });\u001b[39m\n\u001b[31m    const rows = (d.consignees || []).map((c, i) => _consRow(c, d.id, i));\u001b[39m\n\u001b[31m    if (rows.length) {\u001b[39m\n\u001b[31m      await fetch(`${_DB}/consignees`, {\u001b[39m\n\u001b[31m        method: 'POST', headers: _DHDR_UPSERT,\u001b[39m\n\u001b[31m        body: JSON.stringify(rows),\u001b[39m\n\u001b[31m      });\u001b[39m\n\u001b[31m    }\u001b[39m\n\u001b[31m  } catch(e) { console.warn('[dispatch-db] sync error:', e.message); }\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31masync function syncConsigneeToDB(c, dispatchId, idx) {\u001b[39m\n\u001b[31m  try {\u001b[39m\n\u001b[31m    await fetch(`${_DB}/consignees`, {\u001b[39m\n\u001b[31m      method: 'POST', headers: _DHDR_UPSERT,\u001b[39m\n\u001b[31m      body: JSON.stringify(_consRow(c, dispatchId, idx)),\u001b[39m\n\u001b[31m    });\u001b[39m\n\u001b[31m    // Log tracking event\u001b[39m\n\u001b[31m    if (c.lastEvent && c.status) {\u001b[39m\n\u001b[31m      fetch(`${_DB}/tracking_events`, {\u001b[39m\n\u001b[31m        method: 'POST', headers: _DHDR_WRITE,\u001b[39m\n\u001b[31m        body: JSON.stringify({\u001b[39m\n\u001b[31m          docket: c.docket, dispatch_id: dispatchId,\u001b[39m\n\u001b[31m          cons_id: `${dispatchId}-${idx}`,\u001b[39m\n\u001b[31m          status: c.status, event_text: c.lastEvent,\u001b[39m\n\u001b[31m          scan_location: c.lastScanLocation || null,\u001b[39m\n\u001b[31m        }),\u001b[39m\n\u001b[31m      }).catch(() => {});\u001b[39m\n\u001b[31m    }\u001b[39m\n\u001b[31m  } catch(e) { console.warn('[dispatch-db] consignee sync error:', e.message); }\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31masync function loadFromDB() {\u001b[39m\n\u001b[31m  try {\u001b[39m\n\u001b[31m    const [dResp, cResp] = await Promise.all([\u001b[39m\n\u001b[31m      fetch(`${_DB}/dispatches?order=created_at.desc&limit=100`, { headers: _DHDR_READ }),\u001b[39m\n\u001b[31m      fetch(`${_DB}/consignees?order=dispatch_id,seq_no`, { headers: _DHDR_READ }),\u001b[39m\n\u001b[31m    ]);\u001b[39m\n\u001b[31m    if (!dResp.ok) return;\u001b[39m\n\u001b[31m    const dbDispatches = await dResp.json();\u001b[39m\n\u001b[31m    const dbConsignees = cResp.ok ? await cResp.json() : [];\u001b[39m\n\u001b[31m    if (!dbDispatches.length) return;·\u001b[39m\n\u001b[31m    const dbIds = new Set(dbDispatches.map(d => d.dispatch_id));\u001b[39m\n\u001b[31m    const localOnly = dispatches.filter(d => !dbIds.has(d.id));·\u001b[39m\n\u001b[31m    const fromDB = dbDispatches.map(d => {\u001b[39m\n\u001b[31m      const cons = dbConsignees.filter(c => c.dispatch_id === d.dispatch_id);\u001b[39m\n\u001b[31m      return {\u001b[39m\n\u001b[31m        id:           d.dispatch_id,\u001b[39m\n\u001b[31m        client:       d.client,\u001b[39m\n\u001b[31m        jobref:       d.job_ref,\u001b[39m\n\u001b[31m        courier:      d.courier,\u001b[39m\n\u001b[31m        dispatchDate: d.dispatch_date,\u001b[39m\n\u001b[31m        status:       d.status,\u001b[39m\n\u001b[31m        remarks:      d.remarks,\u001b[39m\n\u001b[31m        createdAt:    d.created_at,\u001b[39m\n\u001b[31m        consignees:   cons.map(c => ({\u001b[39m\n\u001b[31m          name:              c.name,\u001b[39m\n\u001b[31m          city:              c.city,\u001b[39m\n\u001b[31m          address:           c.address,\u001b[39m\n\u001b[31m          phone:             c.phone,\u001b[39m\n\u001b[31m          pincode:           c.pincode,\u001b[39m\n\u001b[31m          material:          c.material,\u001b[39m\n\u001b[31m          qty:               c.qty,\u001b[39m\n\u001b[31m          docket:            c.docket,\u001b[39m\n\u001b[31m          status:            c.status,\u001b[39m\n\u001b[31m          lastEvent:         c.last_event,\u001b[39m\n\u001b[31m          lastScanLocation:  c.last_scan_location,\u001b[39m\n\u001b[31m          lastTrackedAt:     c.last_tracked_at,\u001b[39m\n\u001b[31m          manualStatus:      c.manual_status,\u001b[39m\n\u001b[31m          manualDelivered:   c.manual_delivered,\u001b[39m\n\u001b[31m          manualCalledBy:    c.manual_called_by,\u001b[39m\n\u001b[31m          manualRemarks:     c.manual_remarks,\u001b[39m\n\u001b[31m          podUploaded:       c.pod_uploaded,\u001b[39m\n\u001b[31m          _dbIdx:            c.seq_no,\u001b[39m\n\u001b[31m        })),\u001b[39m\n\u001b[31m      };\u001b[39m\n\u001b[31m    });·\u001b[39m\n\u001b[31m    dispatches = [...fromDB, ...localOnly];\u001b[39m\n\u001b[31m    saveData();\u001b[39m\n\u001b[31m    renderDashboard();\u001b[39m\n\u001b[31m    updateHistCount();\u001b[39m\n\u001b[31m  } catch(e) { console.warn('[dispatch-db] load error:', e.message); }\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31mfunction saveSheetsUrlLocal() {\u001b[39m\n\u001b[31m  localStorage.setItem('360lm_disp_sheets_url', JSON.stringify({ sheets_url: window.appConfig.sheets_url }));\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m/**\u001b[39m\n\u001b[31m * Fetch config.json from the VPS server (same origin).\u001b[39m\n\u001b[31m * This file contains the sheets_url — no manual entry needed.\u001b[39m\n\u001b[31m * Falls back to localStorage cache if fetch fails (offline).\u001b[39m\n\u001b[31m */\u001b[39m\n\u001b[31masync function loadServerConfig() {\u001b[39m\n\u001b[31m  try {\u001b[39m\n\u001b[31m    const resp = await fetch('./config.json', { cache: 'no-store' });\u001b[39m\n\u001b[31m    if (resp.ok) {\u001b[39m\n\u001b[31m      const cfg = await resp.json();\u001b[39m\n\u001b[31m      if (cfg.sheets_url) {\u001b[39m\n\u001b[31m        window.appConfig.sheets_url = cfg.sheets_url;\u001b[39m\n\u001b[31m        saveSheetsUrlLocal(); // cache it for offline use\u001b[39m\n\u001b[31m        document.getElementById('cfg_sheets_url').value = cfg.sheets_url;\u001b[39m\n\u001b[31m        return true;\u001b[39m\n\u001b[31m      }\u001b[39m\n\u001b[31m    }\u001b[39m\n\u001b[31m  } catch(e) {\u001b[39m\n\u001b[31m    console.log('config.json fetch failed, using cached URL:', e.message);\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m  return false;\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31m// INIT\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31m// ── Hub auth gate ─────────────────────────────────────────────────────────\u001b[39m\n\u001b[31mwindow._hubUser = null;\u001b[39m\n\u001b[31m(function() {\u001b[39m\n\u001b[31m  try {\u001b[39m\n\u001b[31m    const hubLogin = '/hub/?next=' + encodeURIComponent(location.pathname + location.search);\u001b[39m\n\u001b[31m    const s = JSON.parse(localStorage.getItem('lm360-session') || 'null');\u001b[39m\n\u001b[31m    if (!s || !s.empId || !s.name) { window.location.replace(hubLogin); return; }\u001b[39m\n\u001b[31m    if (Date.now() - new Date(s.loginAt).getTime() > 12 * 3600 * 1000) {\u001b[39m\n\u001b[31m      window.location.replace(hubLogin); return;\u001b[39m\n\u001b[31m    }\u001b[39m\n\u001b[31m    window._hubUser = { id: s.empId, name: s.name, role: s.role };\u001b[39m\n\u001b[31m  } catch(_) { window.location.replace('/hub/?next=' + encodeURIComponent(location.pathname + location.search)); }\u001b[39m\n\u001b[31m})();·\u001b[39m\n\u001b[31mwindow.addEventListener('load', async () => {\u001b[39m\n\u001b[31m  if (!window._hubUser) return; // should never reach here — gate redirected already\u001b[39m\n\u001b[31m  setTimeout(() => {\u001b[39m\n\u001b[31m    document.getElementById('splash').style.opacity = '0';\u001b[39m\n\u001b[31m    setTimeout(() => {\u001b[39m\n\u001b[31m      document.getElementById('splash').style.display = 'none';\u001b[39m\n\u001b[31m      document.getElementById('app').style.display = 'flex';\u001b[39m\n\u001b[31m    }, 450);\u001b[39m\n\u001b[31m  }, 1400);\u001b[39m\n\u001b[31m  loadData();\u001b[39m\n\u001b[31m  // Auto-authenticate config tab for admin users (no password needed)\u001b[39m\n\u001b[31m  if (['harish', 'pramod'].includes(window._hubUser?.id)) _adminAuthed = true;\u001b[39m\n\u001b[31m  setToday();\u001b[39m\n\u001b[31m  addConsignee();\u001b[39m\n\u001b[31m  updateHistCount();\u001b[39m\n\u001b[31m  setInterval(updateNetDot, 5000);\u001b[39m\n\u001b[31m  updateNetDot();\u001b[39m\n\u001b[31m  checkShareTarget();\u001b[39m\n\u001b[31m  setupDragDrop();·\u001b[39m\n\u001b[31m  // Step 1: fetch sheets_url from VPS config.json (auto, no user input)\u001b[39m\n\u001b[31m  await loadServerConfig();·\u001b[39m\n\u001b[31m  // Step 2: populate config tab field\u001b[39m\n\u001b[31m  document.getElementById('cfg_sheets_url').value = window.appConfig.sheets_url || '';·\u001b[39m\n\u001b[31m  // Step 3: load dispatches from Postgres in background (merge with local)\u001b[39m\n\u001b[31m  loadFromDB(); // non-blocking — updates UI when data arrives·\u001b[39m\n\u001b[31m  // Step 4: sync full config from Google Sheet silently\u001b[39m\n\u001b[31m  if (window.appConfig.sheets_url) {\u001b[39m\n\u001b[31m    syncFromSheet(true); // silent on startup\u001b[39m\n\u001b[31m  } else {\u001b[39m\n\u001b[31m    // Only show config tab if no URL found anywhere\u001b[39m\n\u001b[31m    showPane('config');\u001b[39m\n\u001b[31m    showToast('No config.json found on server', 'err');\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m  // Pre-warm OCR after delay\u001b[39m\n\u001b[31m  setTimeout(() => { if (typeof window.initOCR === 'function') window.initOCR(); }, 4000);\u001b[39m\n\u001b[31m});·\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31m// WEB SHARE TARGET\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31masync function checkShareTarget() {\u001b[39m\n\u001b[31m  // Check URL params for share target\u001b[39m\n\u001b[31m  const params = new URLSearchParams(window.location.search);\u001b[39m\n\u001b[31m  const action = params.get('action');\u001b[39m\n\u001b[31m  if (action !== 'share') return;·\u001b[39m\n\u001b[31m  // Check for shared files via Share Target API\u001b[39m\n\u001b[31m  if ('launchQueue' in window) {\u001b[39m\n\u001b[31m    window.launchQueue.setConsumer(async launchParams => {\u001b[39m\n\u001b[31m      if (!launchParams.files || launchParams.files.length === 0) return;\u001b[39m\n\u001b[31m      const files = await Promise.all(launchParams.files.map(f => f.getFile()));\u001b[39m\n\u001b[31m      const imageFiles = files.filter(f => f.type.startsWith('image/'));\u001b[39m\n\u001b[31m      if (imageFiles.length > 0) {\u001b[39m\n\u001b[31m        document.getElementById('shareTargetInfo').style.display = 'flex';\u001b[39m\n\u001b[31m        document.getElementById('sharedCount').textContent = imageFiles.length;\u001b[39m\n\u001b[31m        document.getElementById('shareBanner').classList.add('show');\u001b[39m\n\u001b[31m        setTimeout(() => document.getElementById('shareBanner').classList.remove('show'), 4000);\u001b[39m\n\u001b[31m        showPane('new');\u001b[39m\n\u001b[31m        await handleSlipFiles(imageFiles);\u001b[39m\n\u001b[31m      }\u001b[39m\n\u001b[31m    });\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction dismissShare() {\u001b[39m\n\u001b[31m  document.getElementById('shareBanner').classList.remove('show');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31m// DRAG & DROP\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31mfunction setupDragDrop() {\u001b[39m\n\u001b[31m  const zone = document.getElementById('slipZone');\u001b[39m\n\u001b[31m  zone.addEventListener('dragover', e => { e.preventDefault(); zone.classList.add('drag'); });\u001b[39m\n\u001b[31m  zone.addEventListener('dragleave', () => zone.classList.remove('drag'));\u001b[39m\n\u001b[31m  zone.addEventListener('drop', e => {\u001b[39m\n\u001b[31m    e.preventDefault();\u001b[39m\n\u001b[31m    zone.classList.remove('drag');\u001b[39m\n\u001b[31m    const files = Array.from(e.dataTransfer.files).filter(f => f.type.startsWith('image/'));\u001b[39m\n\u001b[31m    if (files.length) handleSlipFiles(files);\u001b[39m\n\u001b[31m  });\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31m// SLIP OCR HANDLER\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31masync function handleSlipFiles(files) {\u001b[39m\n\u001b[31m  const fileArr = Array.from(files);\u001b[39m\n\u001b[31m  if (!fileArr.length) return;\u001b[39m\n\u001b[31m  // If dockets already mapped from store list — allow upload but skip OCR extraction\u001b[39m\n\u001b[31m  const skipOCR = shouldSkipOCR();\u001b[39m\n\u001b[31m  if (skipOCR) {\u001b[39m\n\u001b[31m    showToast('Dockets already mapped — slips saved for reference only', 'info');\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m  showOCRWorking(true, `Processing ${fileArr.length} slip(s)...`);·\u001b[39m\n\u001b[31m  for (let i = 0; i < fileArr.length; i++) {\u001b[39m\n\u001b[31m    const file = fileArr[i];\u001b[39m\n\u001b[31m    showOCRWorking(true, `OCR: ${i+1}/${fileArr.length} — ${file.name}`);\u001b[39m\n\u001b[31m    const imageData = await readFileAsDataURL(file);\u001b[39m\n\u001b[31m    let result = { docket: '', confidence: 0, source: 'manual' };\u001b[39m\n\u001b[31m    if (!skipOCR) {\u001b[39m\n\u001b[31m      try {\u001b[39m\n\u001b[31m        result = await window.runOCROnImage(imageData, file.name);\u001b[39m\n\u001b[31m      } catch(e) {}\u001b[39m\n\u001b[31m    } else {\u001b[39m\n\u001b[31m      result = { docket: '', confidence: 1, source: 'skipped — docket pre-mapped' };\u001b[39m\n\u001b[31m    }\u001b[39m\n\u001b[31m    slipOCRData.push({ file, imageData, filename: file.name, ...result });\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m  showOCRWorking(false);\u001b[39m\n\u001b[31m  renderOCRResults();\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction readFileAsDataURL(file) {\u001b[39m\n\u001b[31m  return new Promise((res, rej) => {\u001b[39m\n\u001b[31m    const r = new FileReader();\u001b[39m\n\u001b[31m    r.onload = e => res(e.target.result);\u001b[39m\n\u001b[31m    r.onerror = rej;\u001b[39m\n\u001b[31m    r.readAsDataURL(file);\u001b[39m\n\u001b[31m  });\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction renderOCRResults() {\u001b[39m\n\u001b[31m  if (!slipOCRData.length) return;\u001b[39m\n\u001b[31m  const list = document.getElementById('ocrSlipList');\u001b[39m\n\u001b[31m  document.getElementById('ocrResults').classList.add('visible');·\u001b[39m\n\u001b[31m  const needReview = slipOCRData.filter(s => s.confidence < 0.65 && s.docket).length;\u001b[39m\n\u001b[31m  const autoOk     = slipOCRData.filter(s => s.confidence >= 0.88).length;\u001b[39m\n\u001b[31m  const summary    = document.getElementById('ocrSummary');\u001b[39m\n\u001b[31m  if (summary) {\u001b[39m\n\u001b[31m    summary.style.display = '';\u001b[39m\n\u001b[31m    summary.innerHTML =\u001b[39m\n\u001b[31m      `<span style=\\\"color:var(--grn)\\\">✓ ${autoOk} auto-accepted</span>` +\u001b[39m\n\u001b[31m      (needReview ? `&nbsp;·&nbsp;<span style=\\\"color:var(--ylw)\\\">⚠ ${needReview} need manual check</span>` : '');\u001b[39m\n\u001b[31m  }·\u001b[39m\n\u001b[31m  list.innerHTML = slipOCRData.map((s, i) => {\u001b[39m\n\u001b[31m    const autoAccepted = s.confidence >= 0.88 && s.docket;\u001b[39m\n\u001b[31m    const needsCheck   = s.confidence < 0.65  && s.docket;\u001b[39m\n\u001b[31m    const confClass = autoAccepted ? 'conf-high' : needsCheck ? 'conf-low' : 'conf-med';\u001b[39m\n\u001b[31m    const confLabel = autoAccepted\u001b[39m\n\u001b[31m      ? `✓ ${Math.round(s.confidence * 100)}% — auto`\u001b[39m\n\u001b[31m      : needsCheck\u001b[39m\n\u001b[31m        ? `⚠ ${Math.round(s.confidence * 100)}% — verify`\u001b[39m\n\u001b[31m        : `${Math.round(s.confidence * 100)}%`;\u001b[39m\n\u001b[31m    const borderStyle = needsCheck ? 'border:1px solid var(--ylw);' : '';\u001b[39m\n\u001b[31m    return `\u001b[39m\n\u001b[31m    <div class=\\\"ocr-slip\\\" id=\\\"ocrSlip_${i}\\\" style=\\\"${borderStyle}\\\">\u001b[39m\n\u001b[31m      <img src=\\\"${s.imageData}\\\" alt=\\\"slip ${i+1}\\\">\u001b[39m\n\u001b[31m      <div class=\\\"ocr-slip-body\\\">\u001b[39m\n\u001b[31m        <div class=\\\"ocr-slip-name\\\">${s.filename}</div>\u001b[39m\n\u001b[31m        <input type=\\\"text\\\" class=\\\"ocr-slip-docket\\\" id=\\\"ocrDocket_${i}\\\"\u001b[39m\n\u001b[31m          value=\\\"${s.docket}\\\" placeholder=\\\"Docket number\\\"\u001b[39m\n\u001b[31m          ${autoAccepted ? 'style=\\\"opacity:.6\\\"' : ''}\u001b[39m\n\u001b[31m          oninput=\\\"slipOCRData[${i}].docket=this.value;slipOCRData[${i}].confidence=0.5\\\">\u001b[39m\n\u001b[31m        <div class=\\\"ocr-slip-status\\\">\u001b[39m\n\u001b[31m          <span class=\\\"ocr-confidence ${confClass}\\\">${confLabel}</span>\u001b[39m\n\u001b[31m          <span style=\\\"color:var(--t4);font-family:var(--fnt-mono);font-size:.6rem;\\\">via ${s.source}</span>\u001b[39m\n\u001b[31m          <button class=\\\"ocr-ai-btn\\\" onclick=\\\"reOCR(${i})\\\">🔄 Re-OCR</button>\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m    </div>`;\u001b[39m\n\u001b[31m  }).join('');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31masync function reOCR(i) {\u001b[39m\n\u001b[31m  const s = slipOCRData[i];\u001b[39m\n\u001b[31m  showOCRWorking(true, `Re-running OCR on ${s.filename}...`);\u001b[39m\n\u001b[31m  try {\u001b[39m\n\u001b[31m    const result = await window.runOCROnImage(s.imageData, s.filename);\u001b[39m\n\u001b[31m    slipOCRData[i] = { ...s, ...result };\u001b[39m\n\u001b[31m    renderOCRResults();\u001b[39m\n\u001b[31m    showToast('Re-OCR complete', 'ok');\u001b[39m\n\u001b[31m  } catch(e) {\u001b[39m\n\u001b[31m    showToast('OCR failed', 'err');\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m  showOCRWorking(false);\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction clearSlips() {\u001b[39m\n\u001b[31m  slipOCRData = [];\u001b[39m\n\u001b[31m  document.getElementById('ocrResults').classList.remove('visible');\u001b[39m\n\u001b[31m  document.getElementById('ocrSlipList').innerHTML = '';\u001b[39m\n\u001b[31m  const sum = document.getElementById('ocrSummary');\u001b[39m\n\u001b[31m  if (sum) { sum.style.display = 'none'; sum.innerHTML = ''; }\u001b[39m\n\u001b[31m  document.getElementById('shareTargetInfo').style.display = 'none';\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction applyAllDockets() {\u001b[39m\n\u001b[31m  // Match each OCR result to a consignee by order or by name matching\u001b[39m\n\u001b[31m  const consList = document.getElementById('consList');\u001b[39m\n\u001b[31m  const cards = consList.querySelectorAll('.cons-card');\u001b[39m\n\u001b[31m  slipOCRData.forEach((s, i) => {\u001b[39m\n\u001b[31m    if (!s.docket) return;\u001b[39m\n\u001b[31m    const card = cards[i];\u001b[39m\n\u001b[31m    if (card) {\u001b[39m\n\u001b[31m      const docketInput = card.querySelector('.cons-docket-input');\u001b[39m\n\u001b[31m      if (docketInput) {\u001b[39m\n\u001b[31m        docketInput.value = s.docket;\u001b[39m\n\u001b[31m        showLinkedDocket(card, s.docket);\u001b[39m\n\u001b[31m      }\u001b[39m\n\u001b[31m    }\u001b[39m\n\u001b[31m  });\u001b[39m\n\u001b[31m  showToast(`Applied ${slipOCRData.filter(s=>s.docket).length} dockets ✓`, 'ok');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31m// STORE LIST — with column mapping UI\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31mlet _rawRows = [];      // raw rows from file, kept for re-mapping\u001b[39m\n\u001b[31mlet _rawHeaders = [];   // headers from file·\u001b[39m\n\u001b[31masync function handleStoreFile(input) {\u001b[39m\n\u001b[31m  const file = input.files[0];\u001b[39m\n\u001b[31m  if (!file) return;\u001b[39m\n\u001b[31m  showToast('Reading file...', 'info');·\u001b[39m\n\u001b[31m  const data = await readFileAsDataURL(file);\u001b[39m\n\u001b[31m  const base64 = data.split(',')[1];\u001b[39m\n\u001b[31m  const binary = atob(base64);\u001b[39m\n\u001b[31m  const bytes = new Uint8Array(binary.length);\u001b[39m\n\u001b[31m  for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);·\u001b[39m\n\u001b[31m  let rows = [];\u001b[39m\n\u001b[31m  try {\u001b[39m\n\u001b[31m    if (file.name.endsWith('.csv')) {\u001b[39m\n\u001b[31m      const text = new TextDecoder().decode(bytes);\u001b[39m\n\u001b[31m      rows = text.split('\\\\n').map(r => r.split(',').map(c => c.trim().replace(/^\\\"|\\\"$/g, '')));\u001b[39m\n\u001b[31m    } else {\u001b[39m\n\u001b[31m      const wb = XLSX.read(bytes, { type: 'array' });\u001b[39m\n\u001b[31m      const ws = wb.Sheets[wb.SheetNames[0]];\u001b[39m\n\u001b[31m      rows = XLSX.utils.sheet_to_json(ws, { header: 1 });\u001b[39m\n\u001b[31m    }\u001b[39m\n\u001b[31m  } catch(e) { showToast('File read error: ' + e.message, 'err'); return; }·\u001b[39m\n\u001b[31m  if (!rows.length || !rows[0]) { showToast('File appears empty', 'err'); return; }·\u001b[39m\n\u001b[31m  _rawHeaders = (rows[0] || []).map(h => (h || '').toString().trim());\u001b[39m\n\u001b[31m  _rawRows = rows.slice(1).filter(r => r && r.some(c => c !== '' && c != null));·\u001b[39m\n\u001b[31m  document.getElementById('storeTableArea').style.display = 'block';\u001b[39m\n\u001b[31m  renderMappingUI(file.name);\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// ── COLUMN MAPPING FIELDS ─────────────────────────────\u001b[39m\n\u001b[31mconst MAPPING_FIELDS = [\u001b[39m\n\u001b[31m  { key: 'name',    label: 'Store / Partner Name', required: true,\u001b[39m\n\u001b[31m    keywords: ['store','shop','name','outlet','consignee','destination','partner','dealer','client','retailer'] },\u001b[39m\n\u001b[31m  { key: 'city',    label: 'City / Location',       required: false,\u001b[39m\n\u001b[31m    keywords: ['city','town','district','location','area','region','place'] },\u001b[39m\n\u001b[31m  { key: 'phone',   label: 'Phone / Mobile',        required: false,\u001b[39m\n\u001b[31m    keywords: ['phone','mobile','contact','tel','number','mob'] },\u001b[39m\n\u001b[31m  { key: 'docket',  label: 'Docket / AWB No.',      required: false,\u001b[39m\n\u001b[31m    keywords: ['docket','awb','tracking','courier','dkt','consignment','cno','waybill'] },\u001b[39m\n\u001b[31m  { key: 'address', label: 'Address',               required: false,\u001b[39m\n\u001b[31m    keywords: ['address','addr','street','locality','area'] },\u001b[39m\n\u001b[31m  { key: 'pincode', label: 'Pincode / PIN',          required: false,\u001b[39m\n\u001b[31m    keywords: ['pin','pincode','postal','zip','code'] },\u001b[39m\n\u001b[31m];·\u001b[39m\n\u001b[31m// Saved mapping key based on column fingerprint (so same client file auto-maps)\u001b[39m\n\u001b[31mfunction getMappingKey() {\u001b[39m\n\u001b[31m  return 'colmap_' + _rawHeaders.join('|').toLowerCase().replace(/\\\\s+/g,'');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// ── Detect if a column is a combined/multiline address cell ─────────────\u001b[39m\n\u001b[31mfunction isCombinedColumn(colIdx) {\u001b[39m\n\u001b[31m  if (colIdx < 0) return false;\u001b[39m\n\u001b[31m  // Check first 3 data rows — if any cell contains newline or has 6+ digit number (pincode) = combined\u001b[39m\n\u001b[31m  const samples = _rawRows.slice(0, 3).map(r => (r[colIdx] || '').toString());\u001b[39m\n\u001b[31m  return samples.some(s => s.includes('\\\\n') || /\\\\b\\\\d{6}\\\\b/.test(s) || s.split(',').length >= 3);\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction renderMappingUI(filename) {\u001b[39m\n\u001b[31m  const saved = (() => { try { return JSON.parse(localStorage.getItem(getMappingKey()) || 'null'); } catch(e) { return null; } })();\u001b[39m\n\u001b[31m  const isSaved = !!saved;\u001b[39m\n\u001b[31m  const colInfoEl = document.getElementById('storeColInfo');\u001b[39m\n\u001b[31m  colInfoEl.style.display = 'block';·\u001b[39m\n\u001b[31m  const suggested = {};\u001b[39m\n\u001b[31m  MAPPING_FIELDS.forEach(f => {\u001b[39m\n\u001b[31m    const idx = findCol(_rawHeaders, f.keywords);\u001b[39m\n\u001b[31m    suggested[f.key] = saved ? (saved[f.key] ?? idx) : idx;\u001b[39m\n\u001b[31m  });·\u001b[39m\n\u001b[31m  // Detect if address column is combined\u001b[39m\n\u001b[31m  const addrIdx = suggested.address >= 0 ? suggested.address : findCol(_rawHeaders, ['address','addr']);\u001b[39m\n\u001b[31m  const looksCombo = isCombinedColumn(addrIdx) || isCombinedColumn(suggested.name);\u001b[39m\n\u001b[31m  const comboCol   = looksCombo ? (addrIdx >= 0 ? addrIdx : suggested.name) : -1;\u001b[39m\n\u001b[31m  const comboPreview = comboCol >= 0 && _rawRows[0] ? (_rawRows[0][comboCol] || '').toString().slice(0, 120) : '';·\u001b[39m\n\u001b[31m  colInfoEl.innerHTML = `\u001b[39m\n\u001b[31m    <div style=\\\"background:var(--sur2);border:1px solid var(--bdr);border-radius:var(--r-sm);padding:12px;\\\">\u001b[39m\n\u001b[31m      <div style=\\\"display:flex;align-items:center;justify-content:space-between;margin-bottom:10px;\\\">\u001b[39m\n\u001b[31m        <div style=\\\"font-family:var(--fnt-mono);font-size:.68rem;color:var(--sky);\\\">\u001b[39m\n\u001b[31m          📋 ${filename} &nbsp;·&nbsp; ${_rawRows.length} rows &nbsp;·&nbsp; ${_rawHeaders.length} columns\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m        ${isSaved ? '<span class=\\\"map-saved-badge\\\">✓ mapping remembered</span>' : ''}\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      <div style=\\\"font-size:.68rem;color:var(--t3);font-family:var(--fnt-mono);margin-bottom:10px;word-break:break-all;\\\">\u001b[39m\n\u001b[31m        Columns: <span style=\\\"color:var(--t2);\\\">${_rawHeaders.map((h,i)=>`[${i+1}] ${h||'?'}`).join(' · ')}</span>\u001b[39m\n\u001b[31m      </div>·\u001b[39m\n\u001b[31m      ${looksCombo ? `\u001b[39m\n\u001b[31m      <!-- COMBINED COLUMN DETECTED -->\u001b[39m\n\u001b[31m      <div style=\\\"background:rgba(56,189,248,.08);border:1px solid rgba(56,189,248,.3);\u001b[39m\n\u001b[31m        border-radius:var(--r-sm);padding:10px 12px;margin-bottom:10px;\\\">\u001b[39m\n\u001b[31m        <div style=\\\"font-size:.72rem;color:var(--sky);font-weight:600;margin-bottom:6px;\\\">\u001b[39m\n\u001b[31m          🤖 Combined Address Column Detected\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m        <div style=\\\"font-size:.68rem;color:var(--t2);font-family:var(--fnt-mono);\u001b[39m\n\u001b[31m          white-space:pre-wrap;background:var(--bg);padding:6px 8px;border-radius:4px;\u001b[39m\n\u001b[31m          margin-bottom:8px;max-height:80px;overflow:hidden;\\\">${comboPreview}</div>\u001b[39m\n\u001b[31m        <div style=\\\"font-size:.7rem;color:var(--t3);margin-bottom:8px;\\\">\u001b[39m\n\u001b[31m          AI will extract: Name · Address · City · Pincode · Phone from each cell.\u001b[39m\n\u001b[31m          Runs in browser (Transformers.js) with Ollama fallback.\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m        <div style=\\\"display:flex;align-items:center;gap:8px;\\\">\u001b[39m\n\u001b[31m          <label style=\\\"display:flex;align-items:center;gap:6px;font-size:.72rem;color:var(--t1);cursor:pointer;\\\">\u001b[39m\n\u001b[31m            <input type=\\\"checkbox\\\" id=\\\"useAIParse\\\" checked style=\\\"cursor:pointer;\\\">\u001b[39m\n\u001b[31m            Use AI to parse combined column\u001b[39m\n\u001b[31m          </label>\u001b[39m\n\u001b[31m          <select id=\\\"comboColSel\\\" style=\\\"background:var(--sur2);border:1px solid var(--bdr);\u001b[39m\n\u001b[31m            color:var(--t1);font-family:var(--fnt-mono);font-size:.72rem;padding:4px 8px;\u001b[39m\n\u001b[31m            border-radius:var(--r-sm);outline:none;\\\">\u001b[39m\n\u001b[31m            ${_rawHeaders.map((h,i)=>`<option value=\\\"${i}\\\" ${i===comboCol?'selected':''}>${h||'(col '+(i+1)+')'}</option>`).join('')}\u001b[39m\n\u001b[31m          </select>\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      ` : ''}·\u001b[39m\n\u001b[31m      <div id=\\\"manualMapSection\\\" style=\\\"${looksCombo ? 'display:none;' : ''}\\\">\u001b[39m\n\u001b[31m        <div style=\\\"font-size:.68rem;color:var(--t3);margin-bottom:6px;\\\">\u001b[39m\n\u001b[31m          ${looksCombo ? 'Manual mapping (used when AI parse is off, or for other columns):' : 'Map each field to your column:'}\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m        <table class=\\\"map-table\\\">\u001b[39m\n\u001b[31m          <thead><tr><th>Field</th><th>Your column</th><th>Preview (row 1)</th></tr></thead>\u001b[39m\n\u001b[31m          <tbody>\u001b[39m\n\u001b[31m            ${MAPPING_FIELDS.map(f => {\u001b[39m\n\u001b[31m              const selIdx = suggested[f.key] ?? -1;\u001b[39m\n\u001b[31m              const preview = selIdx >= 0 && _rawRows[0] ? ((_rawRows[0][selIdx]||'').toString().slice(0,40)) : '—';\u001b[39m\n\u001b[31m              return `<tr>\u001b[39m\n\u001b[31m                <td style=\\\"white-space:nowrap;color:var(--t1);font-weight:500;font-size:.75rem;\\\">\u001b[39m\n\u001b[31m                  ${f.label}${f.required ? '<span class=\\\"map-req\\\">*</span>' : ''}\u001b[39m\n\u001b[31m                </td>\u001b[39m\n\u001b[31m                <td>\u001b[39m\n\u001b[31m                  <select id=\\\"mapsel_${f.key}\\\" onchange=\\\"updateMappingPreview('${f.key}')\\\">\u001b[39m\n\u001b[31m                    ${_rawHeaders.map((h,i)=>\u001b[39m\n\u001b[31m                      `<option value=\\\"${i}\\\" ${i===selIdx?'selected':''}>${h||'(col '+(i+1)+')'}</option>`\u001b[39m\n\u001b[31m                    ).join('')}\u001b[39m\n\u001b[31m                    <option value=\\\"-1\\\" ${selIdx===-1?'selected':''}>— not in file —</option>\u001b[39m\n\u001b[31m                  </select>\u001b[39m\n\u001b[31m                </td>\u001b[39m\n\u001b[31m                <td id=\\\"mapprev_${f.key}\\\" style=\\\"font-family:var(--fnt-mono);font-size:.68rem;\u001b[39m\n\u001b[31m                  color:var(--t2);max-width:90px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;\\\">\u001b[39m\n\u001b[31m                  ${preview}\u001b[39m\n\u001b[31m                </td>\u001b[39m\n\u001b[31m              </tr>`;\u001b[39m\n\u001b[31m            }).join('')}\u001b[39m\n\u001b[31m          </tbody>\u001b[39m\n\u001b[31m        </table>\u001b[39m\n\u001b[31m      </div>·\u001b[39m\n\u001b[31m      <!-- Also map docket/courier no column separately — always shown -->\u001b[39m\n\u001b[31m      <div style=\\\"margin-top:10px;padding-top:10px;border-top:1px solid var(--bdr);\\\">\u001b[39m\n\u001b[31m        <div style=\\\"font-size:.68rem;color:var(--t3);margin-bottom:6px;font-family:var(--fnt-mono);\\\">\u001b[39m\n\u001b[31m          DOCKET / COURIER NO. COLUMN (required for tracking):\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m        <div style=\\\"display:flex;gap:8px;align-items:center;\\\">\u001b[39m\n\u001b[31m          <select id=\\\"docketColSel\\\" style=\\\"background:var(--sur2);border:1px solid var(--bdr);\u001b[39m\n\u001b[31m            color:var(--acc);font-family:var(--fnt-mono);font-size:.78rem;padding:6px 10px;\u001b[39m\n\u001b[31m            border-radius:var(--r-sm);outline:none;flex:1;\\\">\u001b[39m\n\u001b[31m            <option value=\\\"-1\\\">— Docket not in file —</option>\u001b[39m\n\u001b[31m            ${_rawHeaders.map((h,i)=>`<option value=\\\"${i}\\\" ${i===(suggested.docket??-1)?'selected':''}>${h||'(col '+(i+1)+')'}</option>`).join('')}\u001b[39m\n\u001b[31m          </select>\u001b[39m\n\u001b[31m          <span id=\\\"docketPrev\\\" style=\\\"font-family:var(--fnt-mono);font-size:.72rem;color:var(--acc);\\\">\u001b[39m\n\u001b[31m            ${suggested.docket >= 0 && _rawRows[0] ? (_rawRows[0][suggested.docket]||'') : ''}\u001b[39m\n\u001b[31m          </span>\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m      </div>·\u001b[39m\n\u001b[31m      <div class=\\\"btn-row\\\" style=\\\"margin-top:12px;\\\">\u001b[39m\n\u001b[31m        <button class=\\\"btn btn-ghost\\\" style=\\\"font-size:.78rem;\\\" onclick=\\\"clearStores()\\\">✕ Cancel</button>\u001b[39m\n\u001b[31m        <button class=\\\"btn btn-orange\\\" style=\\\"font-size:.78rem;\\\" onclick=\\\"applyMapping()\\\">✓ Apply Mapping</button>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m  `;\u001b[39m\n\u001b[31m  document.getElementById('storeTable').innerHTML = '';\u001b[39m\n\u001b[31m  document.getElementById('storeAddPanel').style.display = 'none';·\u001b[39m\n\u001b[31m  // Update docket preview on change\u001b[39m\n\u001b[31m  const docketSel = document.getElementById('docketColSel');\u001b[39m\n\u001b[31m  if (docketSel) docketSel.addEventListener('change', () => {\u001b[39m\n\u001b[31m    const idx = parseInt(docketSel.value);\u001b[39m\n\u001b[31m    const prev = document.getElementById('docketPrev');\u001b[39m\n\u001b[31m    if (prev) prev.textContent = idx >= 0 && _rawRows[0] ? (_rawRows[0][idx]||'') : '';\u001b[39m\n\u001b[31m  });·\u001b[39m\n\u001b[31m  // Toggle manual map opacity when AI checkbox changes\u001b[39m\n\u001b[31m  const aiChk = document.getElementById('useAIParse');\u001b[39m\n\u001b[31m  const manSec = document.getElementById('manualMapSection');\u001b[39m\n\u001b[31m  if (aiChk && manSec) {\u001b[39m\n\u001b[31m    aiChk.addEventListener('change', () => {\u001b[39m\n\u001b[31m      manSec.style.display = aiChk.checked ? 'none' : 'block';\u001b[39m\n\u001b[31m    });\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction updateMappingPreview(key) {\u001b[39m\n\u001b[31m  const sel = document.getElementById('mapsel_' + key);\u001b[39m\n\u001b[31m  if (!sel) return;\u001b[39m\n\u001b[31m  const idx = parseInt(sel.value);\u001b[39m\n\u001b[31m  const prev = document.getElementById('mapprev_' + key);\u001b[39m\n\u001b[31m  if (prev) prev.textContent = (idx >= 0 && _rawRows[0]) ? ((_rawRows[0][idx]||'').toString().slice(0,40)) : '—';\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// ── REGEX-FIRST ADDRESS PARSER ───────────────────────────────────────────\u001b[39m\n\u001b[31m// Improved Indian address parser\u001b[39m\n\u001b[31m// Handles formats seen in real BTL dispatch files\u001b[39m\n\u001b[31mfunction parseAddressRegex(raw) {\u001b[39m\n\u001b[31m  const text = raw.toString().replace(/\\\\r/g, '').trim();\u001b[39m\n\u001b[31m  const lines = text.split('\\\\n').map(l => l.trim()).filter(Boolean);\u001b[39m\n\u001b[31m  const result = { name:'', address:'', city:'', pincode:'', phone:'', confidence:0 };·\u001b[39m\n\u001b[31m  // ── PHONE: Indian mobile 6-9 prefix, 10 digits ──────────────────────────\u001b[39m\n\u001b[31m  // Also handles \\\"Phone : 9876543210\\\", \\\"M.No.-9876543210\\\", \\\"Mobile - 9876543210\\\"\u001b[39m\n\u001b[31m  const phoneMatch = text.match(/(?:Phone|Mobile|Mob|M\\\\.No\\\\.?|Ph\\\\.?)?\\\\s*[:\\\\-]?\\\\s*\\\\b([6-9]\\\\d{9})\\\\b/i);\u001b[39m\n\u001b[31m  if (phoneMatch) result.phone = phoneMatch[1];·\u001b[39m\n\u001b[31m  // ── PINCODE: 6-digit standalone number ──────────────────────────────────\u001b[39m\n\u001b[31m  // Must not be part of a longer number (e.g. not an account number)\u001b[39m\n\u001b[31m  const pinMatch = text.match(/(?<![\\\\d])([1-9]\\\\d{5})(?![\\\\d])/);\u001b[39m\n\u001b[31m  if (pinMatch) result.pincode = pinMatch[1];·\u001b[39m\n\u001b[31m  // ── CITY: the token immediately before the pincode ───────────────────────\u001b[39m\n\u001b[31m  const indianStates = ['rajasthan','gujarat','maharashtra','punjab','haryana',\u001b[39m\n\u001b[31m    'uttar pradesh','madhya pradesh','karnataka','tamil nadu','andhra pradesh',\u001b[39m\n\u001b[31m    'telangana','kerala','west bengal','bihar','odisha','jharkhand','assam',\u001b[39m\n\u001b[31m    'himachal pradesh','uttarakhand','chhattisgarh','goa','delhi','chandigarh',\u001b[39m\n\u001b[31m    'raj.','raj'];\u001b[39m\n\u001b[31m  if (result.pincode) {\u001b[39m\n\u001b[31m    const pinPos = text.indexOf(result.pincode);\u001b[39m\n\u001b[31m    const beforePin = text.slice(0, pinPos);\u001b[39m\n\u001b[31m    // Strategy 1: split by comma/newline, walk backwards skipping state names\u001b[39m\n\u001b[31m    const chunks = beforePin.split(/[,\\\\n]/).map(c => c.trim()).filter(Boolean);\u001b[39m\n\u001b[31m    for (let i = chunks.length - 1; i >= 0; i--) {\u001b[39m\n\u001b[31m      const chunk = chunks[i].replace(/\\\\d+/g,'').replace(/[()]/g,'').replace(/[.-]/g,'').trim();\u001b[39m\n\u001b[31m      const lower = chunk.toLowerCase();\u001b[39m\n\u001b[31m      if (!chunk || chunk.length < 2) continue;\u001b[39m\n\u001b[31m      if (indianStates.includes(lower)) continue;\u001b[39m\n\u001b[31m      // Skip chunks that look like addresses (contain numbers or long strings)\u001b[39m\n\u001b[31m      if (/\\\\d/.test(chunk)) continue;\u001b[39m\n\u001b[31m      result.city = chunk;\u001b[39m\n\u001b[31m      break;\u001b[39m\n\u001b[31m    }\u001b[39m\n\u001b[31m    // Strategy 2: if still no city, look for last capitalised word before pincode\u001b[39m\n\u001b[31m    // handles \\\"compound paota Jodhpur 342006\\\" → Jodhpur\u001b[39m\n\u001b[31m    if (!result.city) {\u001b[39m\n\u001b[31m      const wordsBefore = beforePin.replace(/[,\\\\n]/g,' ').trim().split(/\\\\s+/);\u001b[39m\n\u001b[31m      for (let i = wordsBefore.length - 1; i >= 0; i--) {\u001b[39m\n\u001b[31m        const w = wordsBefore[i].replace(/[^a-zA-Z]/g,'');\u001b[39m\n\u001b[31m        if (w.length < 3) continue;\u001b[39m\n\u001b[31m        if (indianStates.includes(w.toLowerCase())) continue;\u001b[39m\n\u001b[31m        // Prefer words that start with capital letter (likely a proper noun/city)\u001b[39m\n\u001b[31m        if (/^[A-Z]/.test(w)) { result.city = w; break; }\u001b[39m\n\u001b[31m      }\u001b[39m\n\u001b[31m    }\u001b[39m\n\u001b[31m  }·\u001b[39m\n\u001b[31m  // ── NAME: first meaningful line, extract shop name ───────────────────────\u001b[39m\n\u001b[31m  // Patterns:\u001b[39m\n\u001b[31m  //   \\\"Sanwar Mal Saini, C/o R G COMPUTERS\\\" → name = \\\"R G COMPUTERS\\\"\u001b[39m\n\u001b[31m  //   \\\"SANWAR MAL SAINI, C/o ADVANCE TECHNOLOGIES\\\" → name = \\\"ADVANCE TECHNOLOGIES\\\"\u001b[39m\n\u001b[31m  //   \\\"Sunil Vaishnav\\\" → name = \\\"Sunil Vaishnav\\\" (no C/o)\u001b[39m\n\u001b[31m  //   \\\"Baltej Singh, Professional computer care\\\" → name = \\\"Professional computer care\\\"\u001b[39m\n\u001b[31m  if (lines.length > 0) {\u001b[39m\n\u001b[31m    const firstLine = lines[0];\u001b[39m\n\u001b[31m    // Check for C/o or C/O pattern — shop name comes after it\u001b[39m\n\u001b[31m    const coMatch = firstLine.match(/C\\\\/?[Oo]\\\\.?\\\\s+(.+)/);\u001b[39m\n\u001b[31m    if (coMatch) {\u001b[39m\n\u001b[31m      result.name = coMatch[1].trim();\u001b[39m\n\u001b[31m    } else {\u001b[39m\n\u001b[31m      // Check if there is a comma — second part might be the shop name\u001b[39m\n\u001b[31m      const parts = firstLine.split(',').map(p => p.trim());\u001b[39m\n\u001b[31m      if (parts.length >= 2 && parts[1].length > 3) {\u001b[39m\n\u001b[31m        // Heuristic: if second part looks like a business name (has uppercase or >5 chars)\u001b[39m\n\u001b[31m        const hasUpperOrLong = /[A-Z]{2,}/.test(parts[1]) || parts[1].length > 8;\u001b[39m\n\u001b[31m        result.name = hasUpperOrLong ? parts[1] : parts[0];\u001b[39m\n\u001b[31m      } else {\u001b[39m\n\u001b[31m        result.name = parts[0];\u001b[39m\n\u001b[31m      }\u001b[39m\n\u001b[31m    }\u001b[39m\n\u001b[31m  }·\u001b[39m\n\u001b[31m  // ── ADDRESS: lines between name and phone/pincode line ───────────────────\u001b[39m\n\u001b[31m  const phoneLineIdx = lines.findIndex(l => result.phone && l.includes(result.phone));\u001b[39m\n\u001b[31m  const addrLines = lines.filter((l, i) => {\u001b[39m\n\u001b[31m    if (i === 0) return false;\u001b[39m\n\u001b[31m    if (i === phoneLineIdx) return false;\u001b[39m\n\u001b[31m    // Exclude lines that are just the phone number\u001b[39m\n\u001b[31m    if (/^[6-9]\\\\d{9}$/.test(l.replace(/\\\\s/g,''))) return false;\u001b[39m\n\u001b[31m    return true;\u001b[39m\n\u001b[31m  });\u001b[39m\n\u001b[31m  result.address = addrLines.join(', ').replace(/,\\\\s*,/g, ',').trim();·\u001b[39m\n\u001b[31m  // ── CONFIDENCE: simple score based on what we got ────────────────────────\u001b[39m\n\u001b[31m  let conf = 0;\u001b[39m\n\u001b[31m  if (result.name)    conf += 30;\u001b[39m\n\u001b[31m  if (result.city)    conf += 25;\u001b[39m\n\u001b[31m  if (result.pincode) conf += 25;\u001b[39m\n\u001b[31m  if (result.phone)   conf += 20;\u001b[39m\n\u001b[31m  result.confidence = conf;·\u001b[39m\n\u001b[31m  return result;\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// ── AI ADDRESS PARSER (Ollama fallback) ──────────────────────────────────\u001b[39m\n\u001b[31m// Fetches past corrections from localStorage to improve prompt accuracy\u001b[39m\n\u001b[31mfunction getPastCorrections() {\u001b[39m\n\u001b[31m  try {\u001b[39m\n\u001b[31m    const log = JSON.parse(localStorage.getItem('360lm_parse_corrections') || '[]');\u001b[39m\n\u001b[31m    return log.slice(-10); // last 10 corrections as examples\u001b[39m\n\u001b[31m  } catch(e) { return []; }\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction saveCorrectionLocal(entry) {\u001b[39m\n\u001b[31m  try {\u001b[39m\n\u001b[31m    const log = JSON.parse(localStorage.getItem('360lm_parse_corrections') || '[]');\u001b[39m\n\u001b[31m    log.push(entry);\u001b[39m\n\u001b[31m    // Keep only last 50\u001b[39m\n\u001b[31m    localStorage.setItem('360lm_parse_corrections', JSON.stringify(log.slice(-50)));\u001b[39m\n\u001b[31m  } catch(e) {}\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31masync function parseAddressWithOllama(raw) {\u001b[39m\n\u001b[31m  // Calls the server-side /dispatch-ai/address proxy (avoids CORS + port 11434 issues)\u001b[39m\n\u001b[31m  try {\u001b[39m\n\u001b[31m    const resp = await fetch('/dispatch-ai/address', {\u001b[39m\n\u001b[31m      method: 'POST',\u001b[39m\n\u001b[31m      headers: { 'Content-Type': 'application/json' },\u001b[39m\n\u001b[31m      body: JSON.stringify({ text: raw }),\u001b[39m\n\u001b[31m      signal: AbortSignal.timeout(120000)\u001b[39m\n\u001b[31m    });\u001b[39m\n\u001b[31m    const data = await resp.json();\u001b[39m\n\u001b[31m    if (data.success) return data;\u001b[39m\n\u001b[31m  } catch(e) {}\u001b[39m\n\u001b[31m  return null;\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31masync function parseAddressCell(raw) {\u001b[39m\n\u001b[31m  // Step 1: regex — always runs first, no network call\u001b[39m\n\u001b[31m  const regex = parseAddressRegex(raw);\u001b[39m\n\u001b[31m  if (regex.name) return { ...regex, source: 'regex' };·\u001b[39m\n\u001b[31m  // Step 2: Ollama only if regex got nothing AND Ollama is configured\u001b[39m\n\u001b[31m  // Uses a 3-second timeout to prevent browser hang\u001b[39m\n\u001b[31m  if (window.appConfig.ocr_mode !== 'transformers') {\u001b[39m\n\u001b[31m    try {\u001b[39m\n\u001b[31m      const ai = await Promise.race([\u001b[39m\n\u001b[31m        parseAddressWithOllama(raw),\u001b[39m\n\u001b[31m        new Promise((_, reject) => setTimeout(() => reject(new Error('timeout')), 120000))\u001b[39m\n\u001b[31m      ]);\u001b[39m\n\u001b[31m      if (ai && ai.name) return { ...ai, source: 'ollama' };\u001b[39m\n\u001b[31m    } catch(e) { /* timeout or error — continue to fallback */ }\u001b[39m\n\u001b[31m  }·\u001b[39m\n\u001b[31m  // Step 3: fallback — first line as name\u001b[39m\n\u001b[31m  const lines = raw.toString().split('\\\\n').map(l=>l.trim()).filter(Boolean);\u001b[39m\n\u001b[31m  return { name: lines[0]||raw.slice(0,40), address: raw, city:'', pincode:'', phone:'', source:'fallback' };\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31masync function applyMapping() {\u001b[39m\n\u001b[31m  const useAI   = document.getElementById('useAIParse')?.checked ?? false;\u001b[39m\n\u001b[31m  const comboSel = document.getElementById('comboColSel');\u001b[39m\n\u001b[31m  const comboIdx = comboSel ? parseInt(comboSel.value) : -1;\u001b[39m\n\u001b[31m  const docketIdx = parseInt(document.getElementById('docketColSel')?.value ?? -1);·\u001b[39m\n\u001b[31m  // Build manual mapping as fallback\u001b[39m\n\u001b[31m  const mapping = {};\u001b[39m\n\u001b[31m  MAPPING_FIELDS.forEach(f => {\u001b[39m\n\u001b[31m    const sel = document.getElementById('mapsel_' + f.key);\u001b[39m\n\u001b[31m    mapping[f.key] = sel ? parseInt(sel.value) : -1;\u001b[39m\n\u001b[31m  });\u001b[39m\n\u001b[31m  // Override docket from dedicated selector\u001b[39m\n\u001b[31m  mapping.docket = docketIdx;·\u001b[39m\n\u001b[31m  // Determine if we have a valid base for stores\u001b[39m\n\u001b[31m  const nameIdx = useAI && comboIdx >= 0 ? comboIdx : mapping.name;\u001b[39m\n\u001b[31m  if (nameIdx === -1 && !useAI) { showToast('Store Name column is required', 'err'); return; }·\u001b[39m\n\u001b[31m  // Save mapping\u001b[39m\n\u001b[31m  try { localStorage.setItem(getMappingKey(), JSON.stringify({...mapping, _useAI: useAI, _comboIdx: comboIdx})); } catch(e) {}·\u001b[39m\n\u001b[31m  if (useAI && comboIdx >= 0) {\u001b[39m\n\u001b[31m    // AI path — parse each combined cell\u001b[39m\n\u001b[31m    showToast('AI parsing addresses...', 'info');\u001b[39m\n\u001b[31m    showOCRWorking(true, `Parsing 0/${_rawRows.length} addresses...`);\u001b[39m\n\u001b[31m    stores = [];\u001b[39m\n\u001b[31m    for (let i = 0; i < _rawRows.length; i++) {\u001b[39m\n\u001b[31m      const r = _rawRows[i];\u001b[39m\n\u001b[31m      const raw = (r[comboIdx] || '').toString().trim();\u001b[39m\n\u001b[31m      if (!raw) continue;\u001b[39m\n\u001b[31m      showOCRWorking(true, `Parsing ${i+1}/${_rawRows.length} — ${raw.slice(0,30)}...`);\u001b[39m\n\u001b[31m      // Yield to browser every row to prevent UI freeze\u001b[39m\n\u001b[31m      await new Promise(r => setTimeout(r, 0));\u001b[39m\n\u001b[31m      const parsed = await parseAddressCell(raw);\u001b[39m\n\u001b[31m      stores.push({\u001b[39m\n\u001b[31m        id: i,\u001b[39m\n\u001b[31m        name:    parsed.name    || raw.slice(0,40),\u001b[39m\n\u001b[31m        city:    parsed.city    || '',\u001b[39m\n\u001b[31m        phone:   parsed.phone   || '',\u001b[39m\n\u001b[31m        address: parsed.address || raw,\u001b[39m\n\u001b[31m        pincode: parsed.pincode || '',\u001b[39m\n\u001b[31m        docket:  docketIdx >= 0 ? (r[docketIdx]||'').toString().trim() : '',\u001b[39m\n\u001b[31m        _parseSource: parsed.source,\u001b[39m\n\u001b[31m        _confidence:  parsed.confidence || 0,\u001b[39m\n\u001b[31m        _rawCell:     raw,\u001b[39m\n\u001b[31m        _origName:    parsed.name || '',\u001b[39m\n\u001b[31m        _origCity:    parsed.city || '',\u001b[39m\n\u001b[31m        matched: false\u001b[39m\n\u001b[31m      });\u001b[39m\n\u001b[31m    }\u001b[39m\n\u001b[31m    showOCRWorking(false);\u001b[39m\n\u001b[31m  } else {\u001b[39m\n\u001b[31m    // Manual mapping path\u001b[39m\n\u001b[31m    stores = _rawRows\u001b[39m\n\u001b[31m      .filter(r => r[nameIdx] && (r[nameIdx]+'').trim())\u001b[39m\n\u001b[31m      .map((r, i) => ({\u001b[39m\n\u001b[31m        id: i,\u001b[39m\n\u001b[31m        name:    mapping.name    >= 0 ? (r[mapping.name]   ||'').toString().trim() : '',\u001b[39m\n\u001b[31m        city:    mapping.city    >= 0 ? (r[mapping.city]   ||'').toString().trim() : '',\u001b[39m\n\u001b[31m        phone:   mapping.phone   >= 0 ? (r[mapping.phone]  ||'').toString().trim() : '',\u001b[39m\n\u001b[31m        docket:  mapping.docket  >= 0 ? (r[mapping.docket] ||'').toString().trim() : '',\u001b[39m\n\u001b[31m        address: mapping.address >= 0 ? (r[mapping.address]||'').toString().trim() : '',\u001b[39m\n\u001b[31m        pincode: mapping.pincode >= 0 ? (r[mapping.pincode]||'').toString().trim() : '',\u001b[39m\n\u001b[31m        matched: false\u001b[39m\n\u001b[31m      }));\u001b[39m\n\u001b[31m  }·\u001b[39m\n\u001b[31m  document.getElementById('storeCount').textContent = `${stores.length} stores loaded`;·\u001b[39m\n\u001b[31m  if (useAI && comboIdx >= 0) {\u001b[39m\n\u001b[31m    // AI path — show row-by-row review panel before adding consignees\u001b[39m\n\u001b[31m    showAddressReviewPanel();\u001b[39m\n\u001b[31m  } else {\u001b[39m\n\u001b[31m    // Manual mapping path — go straight to store table + add panel\u001b[39m\n\u001b[31m    renderStoreTable(mapping.docket >= 0 || docketIdx >= 0);\u001b[39m\n\u001b[31m    const addPanel = document.getElementById('storeAddPanel');\u001b[39m\n\u001b[31m    const addBtn   = document.getElementById('storeAddBtn');\u001b[39m\n\u001b[31m    const addTitle = document.getElementById('storeAddTitle');\u001b[39m\n\u001b[31m    const hasDockets = stores.some(s => s.docket);\u001b[39m\n\u001b[31m    addTitle.textContent = hasDockets\u001b[39m\n\u001b[31m      ? `${stores.filter(s=>s.docket).length} stores with dockets — add as consignees?`\u001b[39m\n\u001b[31m      : `Add ${stores.length} stores as consignees?`;\u001b[39m\n\u001b[31m    addBtn.textContent = `✓ Add ${stores.length} as Consignees`;\u001b[39m\n\u001b[31m    addPanel.style.display = 'block';\u001b[39m\n\u001b[31m    showToast(`${stores.length} stores mapped ✓`, 'ok');\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31m// ADDRESS REVIEW PANEL\u001b[39m\n\u001b[31m// Row-by-row review of AI-parsed addresses before consignees created\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31mfunction showAddressReviewPanel() {\u001b[39m\n\u001b[31m  const pane = document.getElementById('addrReviewPane');\u001b[39m\n\u001b[31m  if (!pane) return;\u001b[39m\n\u001b[31m  renderAddressReviewRows();\u001b[39m\n\u001b[31m  pane.style.display = 'block';\u001b[39m\n\u001b[31m  pane.scrollTop = 0;\u001b[39m\n\u001b[31m  showToast(`Review ${stores.length} parsed addresses`, 'info');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction renderAddressReviewRows() {\u001b[39m\n\u001b[31m  const list = document.getElementById('addrReviewList');\u001b[39m\n\u001b[31m  const summary = document.getElementById('addrReviewSummary');\u001b[39m\n\u001b[31m  if (!list) return;·\u001b[39m\n\u001b[31m  const highConf  = stores.filter(s => (s._confidence||0) >= 80).length;\u001b[39m\n\u001b[31m  const medConf   = stores.filter(s => (s._confidence||0) >= 50 && (s._confidence||0) < 80).length;\u001b[39m\n\u001b[31m  const lowConf   = stores.filter(s => (s._confidence||0) < 50).length;·\u001b[39m\n\u001b[31m  summary.innerHTML = `\u001b[39m\n\u001b[31m    <div class=\\\"vs-item\\\"><div class=\\\"vs-val\\\" style=\\\"color:var(--t1);\\\">${stores.length}</div><div class=\\\"vs-lbl\\\">Total</div></div>\u001b[39m\n\u001b[31m    <div class=\\\"vs-item\\\"><div class=\\\"vs-val\\\" style=\\\"color:var(--grn);\\\">${highConf}</div><div class=\\\"vs-lbl\\\">High conf</div></div>\u001b[39m\n\u001b[31m    <div class=\\\"vs-item\\\"><div class=\\\"vs-val\\\" style=\\\"color:var(--ylw);\\\">${medConf}</div><div class=\\\"vs-lbl\\\">Review</div></div>\u001b[39m\n\u001b[31m    <div class=\\\"vs-item\\\"><div class=\\\"vs-val\\\" style=\\\"color:var(--red);\\\">${lowConf}</div><div class=\\\"vs-lbl\\\">Check</div></div>\u001b[39m\n\u001b[31m    <div class=\\\"vs-item\\\" style=\\\"margin-left:auto;\\\">\u001b[39m\n\u001b[31m      <button class=\\\"btn-sm primary\\\" onclick=\\\"approveAllAddresses()\\\" style=\\\"font-size:.7rem;\\\">✓ Approve All</button>\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m  `;·\u001b[39m\n\u001b[31m  list.innerHTML = stores.map((s, i) => {\u001b[39m\n\u001b[31m    const conf = s._confidence || 0;\u001b[39m\n\u001b[31m    const confClass = conf >= 80 ? 'vf-ok' : conf >= 50 ? 'vf-nodocket' : 'vf-dupe';\u001b[39m\n\u001b[31m    const confLabel = conf >= 80 ? `${conf}% ✓` : conf >= 50 ? `${conf}% review` : `${conf}% check`;\u001b[39m\n\u001b[31m    const rowBorder = conf >= 80 ? 'var(--grn)' : conf >= 50 ? 'var(--ylw)' : 'var(--red)';·\u001b[39m\n\u001b[31m    return `\u001b[39m\n\u001b[31m    <div class=\\\"verify-row\\\" id=\\\"arow_${i}\\\" style=\\\"border-left:3px solid ${rowBorder};\\\">\u001b[39m\n\u001b[31m      <div class=\\\"verify-row-hdr\\\">\u001b[39m\n\u001b[31m        <span class=\\\"verify-num\\\">#${i+1}</span>\u001b[39m\n\u001b[31m        <span class=\\\"verify-store\\\" style=\\\"font-size:.78rem;color:var(--t2);font-weight:400;\u001b[39m\n\u001b[31m          font-family:var(--fnt-mono);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;flex:1;\\\"\u001b[39m\n\u001b[31m          title=\\\"${(s._rawCell||'').replace(/\\\"/g,\\\"'\\\")}\\\">\u001b[39m\n\u001b[31m          ${(s._rawCell||'').slice(0,50).split('\\\\n').join(' ↵ ')}       <span class=\\\"verify-flag ${confClass}\\\" style=\\\"flex-shrink:0;margin-right:6px;\\\">${confLabel} · ${s._parseSource||'?'}</span>\u001b[39m\n\u001b[31m        <button onclick=\\\"deleteReviewRow(${i})\\\"\u001b[39m\n\u001b[31m          style=\\\"background:none;border:1px solid rgba(239,68,68,.4);color:var(--red);\u001b[39m\n\u001b[31m          font-size:.65rem;padding:2px 8px;border-radius:6px;cursor:pointer;\u001b[39m\n\u001b[31m          font-family:var(--fnt-mono);flex-shrink:0;transition:background .2s;\\\"\u001b[39m\n\u001b[31m          onmouseover=\\\"this.style.background='rgba(239,68,68,.15)'\\\"\u001b[39m\n\u001b[31m          onmouseout=\\\"this.style.background='none'\\\">✕ Remove</button>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      <div class=\\\"verify-fields\\\" style=\\\"grid-template-columns:1fr 1fr 1fr 1fr 1fr;\\\">\u001b[39m\n\u001b[31m        <div class=\\\"verify-field\\\">\u001b[39m\n\u001b[31m          <label>Shop / Name <span style=\\\"color:var(--acc);\\\">*</span></label>\u001b[39m\n\u001b[31m          <input type=\\\"text\\\" value=\\\"${s.name||''}\\\" id=\\\"ar_name_${i}\\\"\u001b[39m\n\u001b[31m            oninput=\\\"updateAddrField(${i},'name',this)\\\"\u001b[39m\n\u001b[31m            style=\\\"${!s.name ? 'border-color:var(--red);' : ''}\\\">\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m        <div class=\\\"verify-field\\\">\u001b[39m\n\u001b[31m          <label>City</label>\u001b[39m\n\u001b[31m          <input type=\\\"text\\\" value=\\\"${s.city||''}\\\" id=\\\"ar_city_${i}\\\"\u001b[39m\n\u001b[31m            oninput=\\\"updateAddrField(${i},'city',this)\\\"\u001b[39m\n\u001b[31m            style=\\\"${!s.city ? 'border-color:var(--ylw);' : ''}\\\">\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m        <div class=\\\"verify-field\\\">\u001b[39m\n\u001b[31m          <label>Docket / Courier No.</label>\u001b[39m\n\u001b[31m          <input type=\\\"text\\\" value=\\\"${s.docket||''}\\\" id=\\\"ar_docket_${i}\\\"\u001b[39m\n\u001b[31m            oninput=\\\"updateAddrField(${i},'docket',this)\\\"\u001b[39m\n\u001b[31m            style=\\\"font-family:var(--fnt-mono);${!s.docket?'border-color:var(--ylw);':'color:var(--acc);'}\\\">\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m        <div class=\\\"verify-field\\\">\u001b[39m\n\u001b[31m          <label>Phone</label>\u001b[39m\n\u001b[31m          <input type=\\\"tel\\\" value=\\\"${s.phone||''}\\\" id=\\\"ar_phone_${i}\\\"\u001b[39m\n\u001b[31m            oninput=\\\"updateAddrField(${i},'phone',this)\\\">\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m        <div class=\\\"verify-field\\\">\u001b[39m\n\u001b[31m          <label>Pincode</label>\u001b[39m\n\u001b[31m          <input type=\\\"text\\\" value=\\\"${s.pincode||''}\\\" id=\\\"ar_pin_${i}\\\"\u001b[39m\n\u001b[31m            oninput=\\\"updateAddrField(${i},'pincode',this)\\\">\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      <div style=\\\"font-size:.65rem;color:var(--t3);font-family:var(--fnt-mono);\u001b[39m\n\u001b[31m        margin-top:4px;padding:4px 6px;background:var(--bg);border-radius:4px;word-break:break-all;\\\">\u001b[39m\n\u001b[31m        📋 ${(s._rawCell||'').split('\\\\n').join(' | ')}\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m    </div>`;\u001b[39m\n\u001b[31m  }).join('');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction updateAddrField(i, field, input) {\u001b[39m\n\u001b[31m  if (!stores[i]) return;\u001b[39m\n\u001b[31m  stores[i][field] = input.value.trim();\u001b[39m\n\u001b[31m  // Recompute confidence\u001b[39m\n\u001b[31m  let conf = 0;\u001b[39m\n\u001b[31m  if (stores[i].name)    conf += 30;\u001b[39m\n\u001b[31m  if (stores[i].city)    conf += 25;\u001b[39m\n\u001b[31m  if (stores[i].pincode) conf += 25;\u001b[39m\n\u001b[31m  if (stores[i].phone)   conf += 20;\u001b[39m\n\u001b[31m  stores[i]._confidence = conf;\u001b[39m\n\u001b[31m  input.classList.toggle('changed', true);\u001b[39m\n\u001b[31m  // Update border color on row\u001b[39m\n\u001b[31m  const row = document.getElementById('arow_' + i);\u001b[39m\n\u001b[31m  if (row) row.style.borderLeftColor = conf >= 80 ? 'var(--grn)' : conf >= 50 ? 'var(--ylw)' : 'var(--red)';\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction deleteReviewRow(i) {\u001b[39m\n\u001b[31m  // Mark as deleted — splice from stores array and re-render\u001b[39m\n\u001b[31m  stores.splice(i, 1);\u001b[39m\n\u001b[31m  // Re-assign IDs\u001b[39m\n\u001b[31m  stores.forEach((s, idx) => { s.id = idx; });\u001b[39m\n\u001b[31m  renderAddressReviewRows();\u001b[39m\n\u001b[31m  document.getElementById('storeCount').textContent = `${stores.length} stores loaded`;\u001b[39m\n\u001b[31m  showToast(`Row removed — ${stores.length} remaining`, 'info');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction backToMapping() {\u001b[39m\n\u001b[31m  // Hide review pane — store edits are preserved in stores[] array\u001b[39m\n\u001b[31m  document.getElementById('addrReviewPane').style.display = 'none';\u001b[39m\n\u001b[31m  // Show mapping UI again with current file\u001b[39m\n\u001b[31m  document.getElementById('storeTableArea').style.display = 'block';\u001b[39m\n\u001b[31m  document.getElementById('storeColInfo').style.display = 'block';\u001b[39m\n\u001b[31m  showToast('Your edits are preserved — re-apply mapping to reload', 'info');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction approveAllAddresses() {\u001b[39m\n\u001b[31m  stores.forEach((s, i) => { s._reviewed = true; });\u001b[39m\n\u001b[31m  document.getElementById('addrReviewPane').style.display = 'none';\u001b[39m\n\u001b[31m  applyReviewedStores();\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction confirmAddressReview() {\u001b[39m\n\u001b[31m  // Collect any pending edits, log corrections, close panel\u001b[39m\n\u001b[31m  const corrections = [];\u001b[39m\n\u001b[31m  stores.forEach((s, i) => {\u001b[39m\n\u001b[31m    const origName = s._origName || '';\u001b[39m\n\u001b[31m    const origCity = s._origCity || '';\u001b[39m\n\u001b[31m    if ((origName && origName !== s.name) || (origCity && origCity !== s.city)) {\u001b[39m\n\u001b[31m      corrections.push({ raw: s._rawCell||'', name: s.name, city: s.city, pincode: s.pincode });\u001b[39m\n\u001b[31m    }\u001b[39m\n\u001b[31m  });\u001b[39m\n\u001b[31m  // Save corrections locally for future Ollama prompts\u001b[39m\n\u001b[31m  corrections.forEach(c => saveCorrectionLocal(c));\u001b[39m\n\u001b[31m  // Log to Sheets asynchronously (fire and forget)\u001b[39m\n\u001b[31m  if (corrections.length > 0 && window.appConfig.webhook_url) {\u001b[39m\n\u001b[31m    logParseCorrectionsToSheets(corrections);\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m  document.getElementById('addrReviewPane').style.display = 'none';\u001b[39m\n\u001b[31m  applyReviewedStores();\u001b[39m\n\u001b[31m  showToast(`${corrections.length > 0 ? corrections.length + ' corrections logged · ' : ''}${stores.length} stores ready ✓`, 'ok');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// Clean street address: remove city, pincode, phone if they appear in address field\u001b[39m\n\u001b[31mfunction cleanAddress(addr, city, pincode, phone) {\u001b[39m\n\u001b[31m  if (!addr) return addr;\u001b[39m\n\u001b[31m  let s = addr;\u001b[39m\n\u001b[31m  // Remove phone\u001b[39m\n\u001b[31m  if (phone) s = s.replace(new RegExp(phone.replace(/[.*+?^${}()|[\\\\]\\\\\\\\]/g,'\\\\\\\\$&'), 'g'), '').trim();\u001b[39m\n\u001b[31m  // Remove pincode\u001b[39m\n\u001b[31m  if (pincode) s = s.replace(new RegExp('\\\\\\\\b' + pincode + '\\\\\\\\b', 'g'), '').trim();\u001b[39m\n\u001b[31m  // Remove city (case-insensitive)\u001b[39m\n\u001b[31m  if (city) s = s.replace(new RegExp('\\\\\\\\b' + city.replace(/[.*+?^${}()|[\\\\]\\\\\\\\]/g,'\\\\\\\\$&') + '\\\\\\\\b', 'ig'), '').trim();\u001b[39m\n\u001b[31m  // Remove state names and trailing noise\u001b[39m\n\u001b[31m  const noise = /\\\\b(rajasthan|gujarat|maharashtra|punjab|haryana|uttar pradesh|delhi|raj\\\\.)\\\\b/ig;\u001b[39m\n\u001b[31m  s = s.replace(noise, '');\u001b[39m\n\u001b[31m  // Remove \\\"Mobile :\\\", \\\"Phone :\\\", \\\"M.No.\\\" prefixes and dangling numbers\u001b[39m\n\u001b[31m  s = s.replace(/(?:Mobile|Phone|Mob|M\\\\.No\\\\.?|Ph\\\\.?)\\\\s*[:\\\\-]?\\\\s*/ig, '');\u001b[39m\n\u001b[31m  // Clean up multiple commas/spaces\u001b[39m\n\u001b[31m  s = s.replace(/,\\\\s*,/g, ',').replace(/\\\\s{2,}/g, ' ').replace(/^[\\\\s,]+|[\\\\s,]+$/g, '');\u001b[39m\n\u001b[31m  return s;\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction applyReviewedStores() {\u001b[39m\n\u001b[31m  // Clean address fields before loading stores\u001b[39m\n\u001b[31m  stores.forEach(s => {\u001b[39m\n\u001b[31m    s.address = cleanAddress(s.address, s.city, s.pincode, s.phone);\u001b[39m\n\u001b[31m  });\u001b[39m\n\u001b[31m  renderStoreTable(stores.some(s => s.docket));\u001b[39m\n\u001b[31m  const addPanel = document.getElementById('storeAddPanel');\u001b[39m\n\u001b[31m  const addTitle = document.getElementById('storeAddTitle');\u001b[39m\n\u001b[31m  const addBtn   = document.getElementById('storeAddBtn');\u001b[39m\n\u001b[31m  const hasDockets = stores.some(s => s.docket);\u001b[39m\n\u001b[31m  addTitle.textContent = hasDockets\u001b[39m\n\u001b[31m    ? `${stores.filter(s=>s.docket).length} stores with dockets — add as consignees?`\u001b[39m\n\u001b[31m    : `Add ${stores.length} stores as consignees?`;\u001b[39m\n\u001b[31m  addBtn.textContent = `✓ Add ${stores.length} as Consignees`;\u001b[39m\n\u001b[31m  addPanel.style.display = 'block';\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31masync function logParseCorrectionsToSheets(corrections) {\u001b[39m\n\u001b[31m  const url = window.appConfig.webhook_url;\u001b[39m\n\u001b[31m  if (!url) return;\u001b[39m\n\u001b[31m  try {\u001b[39m\n\u001b[31m    await fetch(url, {\u001b[39m\n\u001b[31m      method: 'POST', mode: 'no-cors',\u001b[39m\n\u001b[31m      headers: { 'Content-Type': 'application/json' },\u001b[39m\n\u001b[31m      body: JSON.stringify({\u001b[39m\n\u001b[31m        type: 'parse_correction',\u001b[39m\n\u001b[31m        corrections,\u001b[39m\n\u001b[31m        colFingerprint: getMappingKey(),\u001b[39m\n\u001b[31m        timestamp: new Date().toISOString()\u001b[39m\n\u001b[31m      })\u001b[39m\n\u001b[31m    });\u001b[39m\n\u001b[31m  } catch(e) {}\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction addStoresToConsignees() {\u001b[39m\n\u001b[31m  const consList = document.getElementById('consList');\u001b[39m\n\u001b[31m  consList.innerHTML = '';\u001b[39m\n\u001b[31m  stores.slice(0, 50).forEach(s => addConsignee(s));\u001b[39m\n\u001b[31m  document.getElementById('storeAddPanel').style.display = 'none';\u001b[39m\n\u001b[31m  showToast(`${Math.min(stores.length, 50)} consignees added ✓`, 'ok');\u001b[39m\n\u001b[31m  if (stores.some(s => s.docket)) setTimeout(() => autoCreateDispatchFromStoreList(), 600);\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction promptAddFromStores() { /* replaced by inline panel */ }··\u001b[39m\n\u001b[31mfunction findCol(headers, keywords) {\u001b[39m\n\u001b[31m  for (let i=0;i<headers.length;i++) {\u001b[39m\n\u001b[31m    const h = (headers[i]||'').toString().toLowerCase();\u001b[39m\n\u001b[31m    if (keywords.some(k => h.includes(k))) return i;\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m  return -1;\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction renderStoreTable(hasDocket) {\u001b[39m\n\u001b[31m  const t = document.getElementById('storeTable');\u001b[39m\n\u001b[31m  const cols = ['#','Store Name','City','Phone'];\u001b[39m\n\u001b[31m  if (hasDocket) cols.push('Docket','Status');\u001b[39m\n\u001b[31m  t.innerHTML = `<thead><tr>${cols.map(c=>`<th>${c}</th>`).join('')}</tr></thead>\u001b[39m\n\u001b[31m  <tbody>${stores.map((s,i) => {\u001b[39m\n\u001b[31m    const statusClass = s.docket ? (s.status === 'delivered' ? 'matched' : 'unmatched') : '';\u001b[39m\n\u001b[31m    const docketCell = hasDocket ? `<td class=\\\"${statusClass}\\\" style=\\\"font-family:var(--fnt-mono);\\\">${s.docket||'—'}</td>` : '';\u001b[39m\n\u001b[31m    const statusCell = hasDocket ? `<td>${s.status ? `<span class=\\\"status-pill sp-${s.status}\\\">${s.status}</span>` : '—'}</td>` : '';\u001b[39m\n\u001b[31m    return `<tr>\u001b[39m\n\u001b[31m      <td>${i+1}</td>\u001b[39m\n\u001b[31m      <td>${s.name}</td>\u001b[39m\n\u001b[31m      <td>${s.city}</td>\u001b[39m\n\u001b[31m      <td style=\\\"font-family:var(--fnt-mono);\\\">${s.phone||'—'}</td>\u001b[39m\n\u001b[31m      ${docketCell}${statusCell}\u001b[39m\n\u001b[31m    </tr>`;\u001b[39m\n\u001b[31m  }).join('')}</tbody>`;\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction clearStores() {\u001b[39m\n\u001b[31m  stores = [];\u001b[39m\n\u001b[31m  document.getElementById('storeTableArea').style.display = 'none';\u001b[39m\n\u001b[31m  document.getElementById('storeFile').value = '';\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction autoCreateDispatchFromStoreList() {\u001b[39m\n\u001b[31m  // Create/update dispatch with all stores that have dockets\u001b[39m\n\u001b[31m  const courier = document.getElementById('n_courier').value || 'SkyKing';\u001b[39m\n\u001b[31m  const storesWithDockets = stores.filter(s => s.docket);\u001b[39m\n\u001b[31m  if (!storesWithDockets.length) return;·\u001b[39m\n\u001b[31m  // Clear and rebuild consignee list from store data\u001b[39m\n\u001b[31m  const consList = document.getElementById('consList');\u001b[39m\n\u001b[31m  consList.innerHTML = '';\u001b[39m\n\u001b[31m  storesWithDockets.forEach(s => {\u001b[39m\n\u001b[31m    addConsignee(s);\u001b[39m\n\u001b[31m  });\u001b[39m\n\u001b[31m  showPane('new');\u001b[39m\n\u001b[31m  showToast(`${storesWithDockets.length} consignees added from store list`, 'ok');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction promptAddFromStores() {\u001b[39m\n\u001b[31m  // Replaced by inline storeAddPanel UI — no-op kept for safety\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction autoMatchDockets() {\u001b[39m\n\u001b[31m  // Match OCR dockets to stores by order\u001b[39m\n\u001b[31m  if (!slipOCRData.length) { showToast('Upload docket slip images first', 'err'); return; }\u001b[39m\n\u001b[31m  let matched = 0;\u001b[39m\n\u001b[31m  slipOCRData.forEach((slip, i) => {\u001b[39m\n\u001b[31m    if (i < stores.length && slip.docket) {\u001b[39m\n\u001b[31m      stores[i].docket = slip.docket;\u001b[39m\n\u001b[31m      matched++;\u001b[39m\n\u001b[31m    }\u001b[39m\n\u001b[31m  });\u001b[39m\n\u001b[31m  renderStoreTable(true);\u001b[39m\n\u001b[31m  showToast(`${matched} dockets matched to stores ✓`, 'ok');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31m// CONSIGNEES\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31mfunction addConsignee(storeData = null) {\u001b[39m\n\u001b[31m  const list = document.getElementById('consList');\u001b[39m\n\u001b[31m  const idx = list.children.length;\u001b[39m\n\u001b[31m  const card = document.createElement('div');\u001b[39m\n\u001b[31m  card.className = 'cons-card';\u001b[39m\n\u001b[31m  card.dataset.idx = idx;\u001b[39m\n\u001b[31m  const s = storeData || {};\u001b[39m\n\u001b[31m  card.innerHTML = `\u001b[39m\n\u001b[31m    <div class=\\\"cons-hdr\\\">\u001b[39m\n\u001b[31m      <span class=\\\"cons-num\\\">CONSIGNEE #${idx+1}</span>\u001b[39m\n\u001b[31m      <button class=\\\"cons-del\\\" onclick=\\\"this.closest('.cons-card').remove();renumberCons()\\\">✕</button>\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m    <div class=\\\"row2\\\">\u001b[39m\n\u001b[31m      <div class=\\\"f\\\">\u001b[39m\n\u001b[31m        <label>Store / Name <span class=\\\"req\\\">*</span></label>\u001b[39m\n\u001b[31m        <input type=\\\"text\\\" class=\\\"cons-name\\\" placeholder=\\\"Store name\\\" value=\\\"${s.name||''}\\\">\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      <div class=\\\"f\\\">\u001b[39m\n\u001b[31m        <label>City <span class=\\\"req\\\">*</span></label>\u001b[39m\n\u001b[31m        <input type=\\\"text\\\" class=\\\"cons-city\\\" placeholder=\\\"City\\\" value=\\\"${s.city||''}\\\">\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m    <div class=\\\"f\\\">\u001b[39m\n\u001b[31m      <label>Address</label>\u001b[39m\n\u001b[31m      <input type=\\\"text\\\" class=\\\"cons-addr\\\" placeholder=\\\"Full address\\\" value=\\\"${s.address||''}\\\">\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m    <div class=\\\"row2\\\">\u001b[39m\n\u001b[31m      <div class=\\\"f\\\">\u001b[39m\n\u001b[31m        <label>Phone</label>\u001b[39m\n\u001b[31m        <input type=\\\"tel\\\" class=\\\"cons-phone\\\" placeholder=\\\"Mobile\\\" value=\\\"${s.phone||''}\\\">\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      <div class=\\\"f\\\">\u001b[39m\n\u001b[31m        <label>Pincode</label>\u001b[39m\n\u001b[31m        <input type=\\\"text\\\" class=\\\"cons-pin\\\" placeholder=\\\"6-digit\\\" maxlength=\\\"6\\\">\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m    <div class=\\\"f\\\">\u001b[39m\n\u001b[31m      <label>Material / Contents <span class=\\\"req\\\">*</span></label>\u001b[39m\n\u001b[31m      <input type=\\\"text\\\" class=\\\"cons-material\\\" placeholder=\\\"e.g. Standees, Posters\\\">\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m    <div class=\\\"row2\\\">\u001b[39m\n\u001b[31m      <div class=\\\"f\\\">\u001b[39m\n\u001b[31m        <label>Qty</label>\u001b[39m\n\u001b[31m        <input type=\\\"number\\\" class=\\\"cons-qty\\\" placeholder=\\\"0\\\" min=\\\"0\\\">\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      <div class=\\\"f\\\">\u001b[39m\n\u001b[31m        <label>Docket No.</label>\u001b[39m\n\u001b[31m        <input type=\\\"text\\\" class=\\\"cons-docket-input\\\" placeholder=\\\"Docket #\\\"\u001b[39m\n\u001b[31m          style=\\\"font-family:var(--fnt-mono);\\\" value=\\\"${s.docket||''}\\\"\u001b[39m\n\u001b[31m          oninput=\\\"onDocketInput(this)\\\">\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m    ${s.docket ? `<div class=\\\"cons-docket-linked\\\"><span>🔗</span><span class=\\\"dkt\\\">${s.docket}</span></div>` : ''}\u001b[39m\n\u001b[31m  `;\u001b[39m\n\u001b[31m  list.appendChild(card);\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction onDocketInput(input) {\u001b[39m\n\u001b[31m  const card = input.closest('.cons-card');\u001b[39m\n\u001b[31m  let linked = card.querySelector('.cons-docket-linked');\u001b[39m\n\u001b[31m  if (input.value.trim()) {\u001b[39m\n\u001b[31m    if (!linked) {\u001b[39m\n\u001b[31m      linked = document.createElement('div');\u001b[39m\n\u001b[31m      linked.className = 'cons-docket-linked';\u001b[39m\n\u001b[31m      card.appendChild(linked);\u001b[39m\n\u001b[31m    }\u001b[39m\n\u001b[31m    linked.innerHTML = `<span>🔗</span><span class=\\\"dkt\\\">${input.value}</span>`;\u001b[39m\n\u001b[31m  } else if (linked) {\u001b[39m\n\u001b[31m    linked.remove();\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction showLinkedDocket(card, docket) {\u001b[39m\n\u001b[31m  const input = card.querySelector('.cons-docket-input');\u001b[39m\n\u001b[31m  if (input) { input.value = docket; onDocketInput(input); }\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction renumberCons() {\u001b[39m\n\u001b[31m  document.querySelectorAll('#consList .cons-card').forEach((c,i) => {\u001b[39m\n\u001b[31m    c.querySelector('.cons-num').textContent = `CONSIGNEE #${i+1}`;\u001b[39m\n\u001b[31m    c.dataset.idx = i;\u001b[39m\n\u001b[31m  });\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31m// JOB REFERENCE\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31mfunction onClientChange() {\u001b[39m\n\u001b[31m  const val = document.getElementById('n_client').value;\u001b[39m\n\u001b[31m  document.getElementById('customClientField').style.display = val === '__c' ? 'block' : 'none';\u001b[39m\n\u001b[31m  updatePill();\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31mfunction updatePill() {\u001b[39m\n\u001b[31m  const client = document.getElementById('n_client').value;\u001b[39m\n\u001b[31m  const job = document.getElementById('n_jobref').value.trim();\u001b[39m\n\u001b[31m  const cName = client === '__c' ? document.getElementById('n_customClient').value : client;\u001b[39m\n\u001b[31m  const pill = document.getElementById('jobPill');\u001b[39m\n\u001b[31m  if (cName && job) {\u001b[39m\n\u001b[31m    document.getElementById('jobPillTxt').textContent = `${cName} › ${job}`;\u001b[39m\n\u001b[31m    pill.style.display = 'inline-flex';\u001b[39m\n\u001b[31m  } else {\u001b[39m\n\u001b[31m    pill.style.display = 'none';\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31mfunction clearJob() {\u001b[39m\n\u001b[31m  document.getElementById('n_client').value = '';\u001b[39m\n\u001b[31m  document.getElementById('n_jobref').value = '';\u001b[39m\n\u001b[31m  document.getElementById('jobPill').style.display = 'none';\u001b[39m\n\u001b[31m  document.getElementById('customClientField').style.display = 'none';\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31m// GATHER / SAVE\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31mfunction gatherDispatch() {\u001b[39m\n\u001b[31m  const client = document.getElementById('n_client').value;\u001b[39m\n\u001b[31m  const clientName = client === '__c' ? document.getElementById('n_customClient').value : client;\u001b[39m\n\u001b[31m  const consignees = [];\u001b[39m\n\u001b[31m  document.querySelectorAll('#consList .cons-card').forEach(card => {\u001b[39m\n\u001b[31m    const docket = card.querySelector('.cons-docket-input')?.value.trim() || '';\u001b[39m\n\u001b[31m    consignees.push({\u001b[39m\n\u001b[31m      name: card.querySelector('.cons-name')?.value || '',\u001b[39m\n\u001b[31m      city: card.querySelector('.cons-city')?.value || '',\u001b[39m\n\u001b[31m      address: card.querySelector('.cons-addr')?.value || '',\u001b[39m\n\u001b[31m      phone: card.querySelector('.cons-phone')?.value || '',\u001b[39m\n\u001b[31m      pincode: card.querySelector('.cons-pin')?.value || '',\u001b[39m\n\u001b[31m      material: card.querySelector('.cons-material')?.value || '',\u001b[39m\n\u001b[31m      qty: card.querySelector('.cons-qty')?.value || '',\u001b[39m\n\u001b[31m      docket,\u001b[39m\n\u001b[31m      status: docket ? 'booked' : 'pending',\u001b[39m\n\u001b[31m      lastEvent: '',\u001b[39m\n\u001b[31m      lastUpdated: ''\u001b[39m\n\u001b[31m    });\u001b[39m\n\u001b[31m  });\u001b[39m\n\u001b[31m  return {\u001b[39m\n\u001b[31m    id: 'DSP-' + Date.now(),\u001b[39m\n\u001b[31m    mode: 'manual',\u001b[39m\n\u001b[31m    client: clientName,\u001b[39m\n\u001b[31m    jobref: document.getElementById('n_jobref').value,\u001b[39m\n\u001b[31m    courier: document.getElementById('n_courier').value,\u001b[39m\n\u001b[31m    dispatchDate: document.getElementById('n_date').value,\u001b[39m\n\u001b[31m    remarks: document.getElementById('n_remarks').value,\u001b[39m\n\u001b[31m    consignees,\u001b[39m\n\u001b[31m    slipCount: slipOCRData.length,\u001b[39m\n\u001b[31m    status: 'active',\u001b[39m\n\u001b[31m    createdAt: new Date().toISOString()\u001b[39m\n\u001b[31m  };\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction validate(d) {\u001b[39m\n\u001b[31m  if (!d.client) { showToast('Select a client', 'err'); return false; }\u001b[39m\n\u001b[31m  if (!d.courier) { showToast('Select courier', 'err'); return false; }\u001b[39m\n\u001b[31m  if (!d.consignees.length) { showToast('Add at least one consignee', 'err'); return false; }\u001b[39m\n\u001b[31m  for (let i = 0; i < d.consignees.length; i++) {\u001b[39m\n\u001b[31m    const c = d.consignees[i];\u001b[39m\n\u001b[31m    if (!c.name || !c.city) {\u001b[39m\n\u001b[31m      showToast(`Consignee #${i+1} (${c.name||'unnamed'}) — name & city required`, 'err');\u001b[39m\n\u001b[31m      // Scroll to and highlight the problem consignee card\u001b[39m\n\u001b[31m      const cards = document.querySelectorAll('#consList .cons-card');\u001b[39m\n\u001b[31m      if (cards[i]) {\u001b[39m\n\u001b[31m        cards[i].scrollIntoView({ behavior: 'smooth', block: 'center' });\u001b[39m\n\u001b[31m        cards[i].style.borderColor = 'var(--red)';\u001b[39m\n\u001b[31m        cards[i].style.boxShadow = '0 0 0 2px rgba(239,68,68,.3)';\u001b[39m\n\u001b[31m        setTimeout(() => {\u001b[39m\n\u001b[31m          if (cards[i]) { cards[i].style.borderColor = ''; cards[i].style.boxShadow = ''; }\u001b[39m\n\u001b[31m        }, 3000);\u001b[39m\n\u001b[31m      }\u001b[39m\n\u001b[31m      return false;\u001b[39m\n\u001b[31m    }\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m  return true;\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction saveDraft() {\u001b[39m\n\u001b[31m  const d = gatherDispatch();\u001b[39m\n\u001b[31m  d.status = 'draft';\u001b[39m\n\u001b[31m  dispatches.unshift(d);\u001b[39m\n\u001b[31m  saveData();\u001b[39m\n\u001b[31m  updateHistCount();\u001b[39m\n\u001b[31m  showToast('Draft saved ✓', 'ok');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// ── VERIFICATION SCREEN ─────────────────────────────────\u001b[39m\n\u001b[31m// Known docket lengths per courier for validation\u001b[39m\n\u001b[31mconst DOCKET_LENGTHS = {\u001b[39m\n\u001b[31m  'SkyKing': { min: 9, max: 12 },\u001b[39m\n\u001b[31m  'DTDC':    { min: 8, max: 15 },\u001b[39m\n\u001b[31m  'Delhivery': { min: 10, max: 18 },\u001b[39m\n\u001b[31m  'Blue Dart': { min: 10, max: 13 },\u001b[39m\n\u001b[31m  'FedEx':   { min: 10, max: 20 },\u001b[39m\n\u001b[31m  'Ecom Express': { min: 10, max: 14 },\u001b[39m\n\u001b[31m  'India Post': { min: 13, max: 22 },\u001b[39m\n\u001b[31m};·\u001b[39m\n\u001b[31mfunction finaliseDispatch() {\u001b[39m\n\u001b[31m  const d = gatherDispatch();\u001b[39m\n\u001b[31m  if (!validate(d)) return;\u001b[39m\n\u001b[31m  showVerifyScreen(d);\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction showVerifyScreen(d) {\u001b[39m\n\u001b[31m  const pane = document.getElementById('verifyPane');\u001b[39m\n\u001b[31m  const list = document.getElementById('verifyList');\u001b[39m\n\u001b[31m  const summary = document.getElementById('verifySummary');·\u001b[39m\n\u001b[31m  // Build flags\u001b[39m\n\u001b[31m  const docketsSeen = {};\u001b[39m\n\u001b[31m  const courier = d.courier;\u001b[39m\n\u001b[31m  const lenRule = DOCKET_LENGTHS[courier];·\u001b[39m\n\u001b[31m  d.consignees.forEach((c, i) => {\u001b[39m\n\u001b[31m    if (c.docket) docketsSeen[c.docket] = (docketsSeen[c.docket] || 0) + 1;\u001b[39m\n\u001b[31m  });·\u001b[39m\n\u001b[31m  const total     = d.consignees.length;\u001b[39m\n\u001b[31m  const withDock  = d.consignees.filter(c => c.docket).length;\u001b[39m\n\u001b[31m  const dupes     = Object.values(docketsSeen).filter(v => v > 1).length;\u001b[39m\n\u001b[31m  const noDock    = total - withDock;\u001b[39m\n\u001b[31m  const badLen    = lenRule ? d.consignees.filter(c => c.docket &&\u001b[39m\n\u001b[31m    (c.docket.length < lenRule.min || c.docket.length > lenRule.max)).length : 0;·\u001b[39m\n\u001b[31m  // Summary bar\u001b[39m\n\u001b[31m  summary.innerHTML = `\u001b[39m\n\u001b[31m    <div class=\\\"vs-item\\\"><div class=\\\"vs-val\\\" style=\\\"color:var(--t1);\\\">${total}</div><div class=\\\"vs-lbl\\\">Total</div></div>\u001b[39m\n\u001b[31m    <div class=\\\"vs-item\\\"><div class=\\\"vs-val\\\" style=\\\"color:var(--grn);\\\">${withDock}</div><div class=\\\"vs-lbl\\\">Docketed</div></div>\u001b[39m\n\u001b[31m    <div class=\\\"vs-item\\\"><div class=\\\"vs-val\\\" style=\\\"color:${noDock > 0 ? 'var(--ylw)' : 'var(--t3)'};\\\">${noDock}</div><div class=\\\"vs-lbl\\\">No Docket</div></div>\u001b[39m\n\u001b[31m    <div class=\\\"vs-item\\\"><div class=\\\"vs-val\\\" style=\\\"color:${dupes > 0 ? 'var(--red)' : 'var(--t3)'};\\\">${dupes}</div><div class=\\\"vs-lbl\\\">Duplicates</div></div>\u001b[39m\n\u001b[31m    ${badLen > 0 ? `<div class=\\\"vs-item\\\"><div class=\\\"vs-val\\\" style=\\\"color:var(--sky);\\\">${badLen}</div><div class=\\\"vs-lbl\\\">Short/Long</div></div>` : ''}\u001b[39m\n\u001b[31m    <div class=\\\"vs-item\\\" style=\\\"margin-left:auto;text-align:right;\\\">\u001b[39m\n\u001b[31m      <div class=\\\"vs-val\\\" style=\\\"font-size:.8rem;color:var(--t2);\\\">${d.courier||'—'}</div>\u001b[39m\n\u001b[31m      <div class=\\\"vs-lbl\\\">${d.client||'—'} ${d.jobref ? '› '+d.jobref : ''}</div>\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m  `;·\u001b[39m\n\u001b[31m  // Build consignee rows\u001b[39m\n\u001b[31m  list.innerHTML = d.consignees.map((c, i) => {\u001b[39m\n\u001b[31m    const isDupe   = c.docket && docketsSeen[c.docket] > 1;\u001b[39m\n\u001b[31m    const isNoDock = !c.docket;\u001b[39m\n\u001b[31m    const isShort  = lenRule && c.docket &&\u001b[39m\n\u001b[31m      (c.docket.length < lenRule.min || c.docket.length > lenRule.max);·\u001b[39m\n\u001b[31m    const rowClass = isDupe ? 'has-dupe' : (isNoDock ? 'no-docket' : 'ok');·\u001b[39m\n\u001b[31m    // Find matching OCR slip for thumbnail\u001b[39m\n\u001b[31m    const ocrMatch = slipOCRData[i];\u001b[39m\n\u001b[31m    const thumbHtml = ocrMatch?.imageData\u001b[39m\n\u001b[31m      ? `<img class=\\\"verify-thumb\\\" src=\\\"${ocrMatch.imageData}\\\" onclick=\\\"showSlipFull(${i})\\\" title=\\\"View slip\\\">`\u001b[39m\n\u001b[31m      : '';·\u001b[39m\n\u001b[31m    // Flags\u001b[39m\n\u001b[31m    let flags = '';\u001b[39m\n\u001b[31m    if (isDupe)   flags += `<span class=\\\"verify-flag vf-dupe\\\">⚠ Duplicate</span> `;\u001b[39m\n\u001b[31m    if (isNoDock) flags += `<span class=\\\"verify-flag vf-nodocket\\\">No Docket</span> `;\u001b[39m\n\u001b[31m    if (isShort)  flags += `<span class=\\\"verify-flag vf-short\\\">${c.docket.length} digits</span> `;\u001b[39m\n\u001b[31m    if (!isDupe && !isNoDock && !isShort) flags = `<span class=\\\"verify-flag vf-ok\\\">✓ OK</span>`;·\u001b[39m\n\u001b[31m    // OCR confidence note\u001b[39m\n\u001b[31m    const confNote = ocrMatch && ocrMatch.confidence\u001b[39m\n\u001b[31m      ? `<div class=\\\"ocr-correction-note\\\">OCR confidence: ${Math.round(ocrMatch.confidence*100)}% via ${ocrMatch.source||'—'}</div>` : '';·\u001b[39m\n\u001b[31m    // WhatsApp URL\u001b[39m\n\u001b[31m    const waMsg = encodeURIComponent(\u001b[39m\n\u001b[31m      `Dear ${c.name}, your material has been dispatched via ${d.courier}. Docket: ${c.docket||'TBD'}. Track: ${(COURIER_MASTER[d.courier]?.trackUrl||'').replace('{docket}', c.docket||'')}`\u001b[39m\n\u001b[31m    );\u001b[39m\n\u001b[31m    const waUrl = c.phone ? `https://wa.me/91${c.phone.replace(/\\\\D/g,'')}?text=${waMsg}` : '';·\u001b[39m\n\u001b[31m    return `\u001b[39m\n\u001b[31m    <div class=\\\"verify-row ${rowClass}\\\" id=\\\"vrow_${i}\\\">\u001b[39m\n\u001b[31m      <div class=\\\"verify-row-hdr\\\">\u001b[39m\n\u001b[31m        <span class=\\\"verify-num\\\">#${i+1}</span>\u001b[39m\n\u001b[31m        <span class=\\\"verify-store\\\">${c.name||'—'}</span>\u001b[39m\n\u001b[31m        ${flags}\u001b[39m\n\u001b[31m        ${thumbHtml}\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      <div class=\\\"verify-fields\\\" style=\\\"grid-template-columns:1fr 1fr 1fr;\\\">\u001b[39m\n\u001b[31m        <div class=\\\"verify-field\\\">\u001b[39m\n\u001b[31m          <label>Docket No. <span style=\\\"color:var(--acc);\\\">*</span></label>\u001b[39m\n\u001b[31m          <input type=\\\"text\\\" value=\\\"${c.docket||''}\\\" oninput=\\\"updateVerifyField(${i},'docket',this)\\\"\u001b[39m\n\u001b[31m            style=\\\"font-family:var(--fnt-mono);${isDupe||isShort?'border-color:var(--red);':!c.docket?'border-color:var(--ylw);':'color:var(--acc);'}\\\">\u001b[39m\n\u001b[31m          ${confNote}\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m        <div class=\\\"verify-field\\\">\u001b[39m\n\u001b[31m          <label>City</label>\u001b[39m\n\u001b[31m          <input type=\\\"text\\\" value=\\\"${c.city||''}\\\" oninput=\\\"updateVerifyField(${i},'city',this)\\\"\u001b[39m\n\u001b[31m            style=\\\"${!c.city ? 'border-color:var(--ylw);' : ''}\\\">\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m        <div class=\\\"verify-field\\\">\u001b[39m\n\u001b[31m          <label>Pincode</label>\u001b[39m\n\u001b[31m          <input type=\\\"text\\\" value=\\\"${c.pincode||''}\\\" oninput=\\\"updateVerifyField(${i},'pincode',this)\\\"\u001b[39m\n\u001b[31m            style=\\\"font-family:var(--fnt-mono);\\\">\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m        <div class=\\\"verify-field\\\">\u001b[39m\n\u001b[31m          <label>Phone</label>\u001b[39m\n\u001b[31m          <input type=\\\"tel\\\" value=\\\"${c.phone||''}\\\" oninput=\\\"updateVerifyField(${i},'phone',this)\\\">\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m        <div class=\\\"verify-field\\\">\u001b[39m\n\u001b[31m          <label>Material / Contents</label>\u001b[39m\n\u001b[31m          <input type=\\\"text\\\" value=\\\"${c.material||''}\\\" oninput=\\\"updateVerifyField(${i},'material',this)\\\">\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m        <div class=\\\"verify-field\\\">\u001b[39m\n\u001b[31m          <label>Street Address</label>\u001b[39m\n\u001b[31m          <input type=\\\"text\\\" value=\\\"${c.address||''}\\\" oninput=\\\"updateVerifyField(${i},'address',this)\\\"\u001b[39m\n\u001b[31m            placeholder=\\\"Street / locality only\\\">\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      <div class=\\\"verify-actions\\\">\u001b[39m\n\u001b[31m        ${c.docket && c.phone ? `<button class=\\\"btn-wa\\\" onclick=\\\"window.open('${waUrl}','_blank')\\\">📱 WhatsApp</button>` : ''}\u001b[39m\n\u001b[31m        <button class=\\\"btn-sm\\\" onclick=\\\"removeVerifyRow(${i})\\\">✕ Remove</button>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m    </div>`;\u001b[39m\n\u001b[31m  }).join('');·\u001b[39m\n\u001b[31m  // Store dispatch object for confirm\u001b[39m\n\u001b[31m  pane._pendingDispatch = d;\u001b[39m\n\u001b[31m  pane.style.display = 'block';\u001b[39m\n\u001b[31m  pane.scrollTop = 0;\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction updateVerifyField(idx, field, input) {\u001b[39m\n\u001b[31m  const pane = document.getElementById('verifyPane');\u001b[39m\n\u001b[31m  if (!pane._pendingDispatch) return;\u001b[39m\n\u001b[31m  const c = pane._pendingDispatch.consignees[idx];\u001b[39m\n\u001b[31m  if (!c) return;\u001b[39m\n\u001b[31m  c[field] = input.value.trim();\u001b[39m\n\u001b[31m  input.classList.toggle('changed', input.value.trim() !== '');\u001b[39m\n\u001b[31m  // Re-check dupe if docket changed\u001b[39m\n\u001b[31m  if (field === 'docket') refreshVerifyFlags(pane._pendingDispatch);\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction refreshVerifyFlags(d) {\u001b[39m\n\u001b[31m  const seen = {};\u001b[39m\n\u001b[31m  d.consignees.forEach(c => { if (c.docket) seen[c.docket] = (seen[c.docket]||0)+1; });\u001b[39m\n\u001b[31m  d.consignees.forEach((c, i) => {\u001b[39m\n\u001b[31m    const row = document.getElementById('vrow_' + i);\u001b[39m\n\u001b[31m    if (!row) return;\u001b[39m\n\u001b[31m    const isDupe = c.docket && seen[c.docket] > 1;\u001b[39m\n\u001b[31m    row.classList.toggle('has-dupe', isDupe);\u001b[39m\n\u001b[31m  });\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction removeVerifyRow(idx) {\u001b[39m\n\u001b[31m  const pane = document.getElementById('verifyPane');\u001b[39m\n\u001b[31m  if (!pane._pendingDispatch) return;\u001b[39m\n\u001b[31m  const el = document.getElementById('vrow_' + idx);\u001b[39m\n\u001b[31m  if (el) {\u001b[39m\n\u001b[31m    el.style.transition = 'opacity .25s';\u001b[39m\n\u001b[31m    el.style.opacity = '0';\u001b[39m\n\u001b[31m    setTimeout(() => {\u001b[39m\n\u001b[31m      pane._pendingDispatch.consignees.splice(idx, 1);\u001b[39m\n\u001b[31m      showVerifyScreen(pane._pendingDispatch); // re-render with updated indices\u001b[39m\n\u001b[31m    }, 250);\u001b[39m\n\u001b[31m  } else {\u001b[39m\n\u001b[31m    pane._pendingDispatch.consignees.splice(idx, 1);\u001b[39m\n\u001b[31m    showVerifyScreen(pane._pendingDispatch);\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction approveAll() {\u001b[39m\n\u001b[31m  showToast('All rows approved ✓', 'ok');\u001b[39m\n\u001b[31m  document.getElementById('btnConfirmFinalise').style.background = 'var(--grn)';\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction closeVerify() {\u001b[39m\n\u001b[31m  const pane = document.getElementById('verifyPane');\u001b[39m\n\u001b[31m  // If this dispatch was already finalised, warn before going back\u001b[39m\n\u001b[31m  if (_lastFinalisedId && pane._pendingDispatch?.id === _lastFinalisedId) {\u001b[39m\n\u001b[31m    if (!confirm(\u001b[39m\n\u001b[31m      'This dispatch has already been finalised.\\\\n\\\\n' +\u001b[39m\n\u001b[31m      'Going back will take you to the edit form — any changes will create a NEW dispatch record.\\\\n\\\\n' +\u001b[39m\n\u001b[31m      'Go back anyway?'\u001b[39m\n\u001b[31m    )) return;\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m  pane.style.display = 'none';\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction showSlipFull(idx) {\u001b[39m\n\u001b[31m  const s = slipOCRData[idx];\u001b[39m\n\u001b[31m  if (!s?.imageData) return;\u001b[39m\n\u001b[31m  const w = window.open('');\u001b[39m\n\u001b[31m  w.document.write(`<img src=\\\"${s.imageData}\\\" style=\\\"max-width:100%;height:auto;\\\">`);\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// Log OCR correction to sheets for future accuracy improvement\u001b[39m\n\u001b[31masync function logOCRCorrection(original, corrected, courier) {\u001b[39m\n\u001b[31m  if (!window.appConfig.webhook_url || original === corrected) return;\u001b[39m\n\u001b[31m  try {\u001b[39m\n\u001b[31m    await fetch(window.appConfig.webhook_url, {\u001b[39m\n\u001b[31m      method: 'POST', mode: 'no-cors',\u001b[39m\n\u001b[31m      headers: { 'Content-Type': 'application/json' },\u001b[39m\n\u001b[31m      body: JSON.stringify({\u001b[39m\n\u001b[31m        type: 'ocr_correction',\u001b[39m\n\u001b[31m        original, corrected, courier,\u001b[39m\n\u001b[31m        timestamp: new Date().toISOString()\u001b[39m\n\u001b[31m      })\u001b[39m\n\u001b[31m    });\u001b[39m\n\u001b[31m  } catch(e) {}\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mlet _lastFinalisedId = null; // track last finalised dispatch to warn on back·\u001b[39m\n\u001b[31masync function confirmAndFinalise() {\u001b[39m\n\u001b[31m  const pane = document.getElementById('verifyPane');\u001b[39m\n\u001b[31m  const d = pane._pendingDispatch;\u001b[39m\n\u001b[31m  if (!d) return;·\u001b[39m\n\u001b[31m  d.status = 'active';\u001b[39m\n\u001b[31m  d.finalisedAt = new Date().toISOString();·\u001b[39m\n\u001b[31m  // Log OCR corrections\u001b[39m\n\u001b[31m  slipOCRData.forEach((s, i) => {\u001b[39m\n\u001b[31m    const c = d.consignees[i];\u001b[39m\n\u001b[31m    if (c && s.docket && c.docket && s.docket !== c.docket) {\u001b[39m\n\u001b[31m      logOCRCorrection(s.docket, c.docket, d.courier);\u001b[39m\n\u001b[31m    }\u001b[39m\n\u001b[31m  });·\u001b[39m\n\u001b[31m  // Replace existing draft with same ID if present, else prepend\u001b[39m\n\u001b[31m  const existingIdx = dispatches.findIndex(x => x.id === d.id);\u001b[39m\n\u001b[31m  if (existingIdx >= 0) {\u001b[39m\n\u001b[31m    dispatches[existingIdx] = d;\u001b[39m\n\u001b[31m  } else {\u001b[39m\n\u001b[31m    dispatches.unshift(d);\u001b[39m\n\u001b[31m  }·\u001b[39m\n\u001b[31m  _lastFinalisedId = d.id;\u001b[39m\n\u001b[31m  saveData();\u001b[39m\n\u001b[31m  syncDispatchToDB(d); // push to Postgres (non-blocking)\u001b[39m\n\u001b[31m  updateHistCount();\u001b[39m\n\u001b[31m  if (window.appConfig.auto_log !== 'false' && window.appConfig.webhook_url) await logToSheets(d);\u001b[39m\n\u001b[31m  showToast('Dispatch finalised ✓', 'ok');\u001b[39m\n\u001b[31m  pane.style.display = 'none';\u001b[39m\n\u001b[31m  pane._pendingDispatch = null; // clear pending\u001b[39m\n\u001b[31m  resetForm();\u001b[39m\n\u001b[31m  showPane('dashboard');\u001b[39m\n\u001b[31m  renderDashboard();\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction resetForm() {\u001b[39m\n\u001b[31m  document.getElementById('n_client').value = '';\u001b[39m\n\u001b[31m  document.getElementById('n_jobref').value = '';\u001b[39m\n\u001b[31m  document.getElementById('n_courier').value = '';\u001b[39m\n\u001b[31m  document.getElementById('n_remarks').value = '';\u001b[39m\n\u001b[31m  document.getElementById('jobPill').style.display = 'none';\u001b[39m\n\u001b[31m  document.getElementById('customClientField').style.display = 'none';\u001b[39m\n\u001b[31m  document.getElementById('consList').innerHTML = '';\u001b[39m\n\u001b[31m  addConsignee();\u001b[39m\n\u001b[31m  clearSlips();\u001b[39m\n\u001b[31m  clearStores();\u001b[39m\n\u001b[31m  setToday();\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction newDispatch() { resetForm(); showPane('new'); }·\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31m// DASHBOARD\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31mfunction renderDashboard() {\u001b[39m\n\u001b[31m  // Exclude drafts from dashboard — only show active/finalised dispatches\u001b[39m\n\u001b[31m  const activeDis = dispatches.filter(d => d.status !== 'draft');\u001b[39m\n\u001b[31m  const allCons = activeDis.flatMap(d => d.consignees || []);\u001b[39m\n\u001b[31m  const countBy = (status) => allCons.filter(c => c.status === status).length;\u001b[39m\n\u001b[31m  document.getElementById('st-delivered').textContent = countBy('delivered');\u001b[39m\n\u001b[31m  document.getElementById('st-transit').textContent = countBy('transit') + countBy('in-transit');\u001b[39m\n\u001b[31m  document.getElementById('st-booked').textContent = countBy('booked');\u001b[39m\n\u001b[31m  document.getElementById('st-pending').textContent = allCons.filter(c => !c.docket || c.status === 'pending').length;·\u001b[39m\n\u001b[31m  const grid = document.getElementById('dashGrid');\u001b[39m\n\u001b[31m  // Flatten all consignees across dispatches\u001b[39m\n\u001b[31m  let items = activeDis.flatMap(d =>\u001b[39m\n\u001b[31m    (d.consignees || []).filter(c => c.docket).map(c => ({...c, dispatchId: d.id, courier: d.courier, client: d.client, jobref: d.jobref}))\u001b[39m\n\u001b[31m  );\u001b[39m\n\u001b[31m  if (dashFilter !== 'all') {\u001b[39m\n\u001b[31m    items = items.filter(c => {\u001b[39m\n\u001b[31m      if (dashFilter === 'delivered') return c.status === 'delivered';\u001b[39m\n\u001b[31m      if (dashFilter === 'transit') return c.status === 'transit' || c.status === 'in-transit';\u001b[39m\n\u001b[31m      if (dashFilter === 'booked') return c.status === 'booked';\u001b[39m\n\u001b[31m      if (dashFilter === 'pending') return !c.docket || c.status === 'pending';\u001b[39m\n\u001b[31m      return true;\u001b[39m\n\u001b[31m    });\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m  if (!items.length) {\u001b[39m\n\u001b[31m    grid.innerHTML = '<div class=\\\"empty\\\"><div class=\\\"e-icon\\\">📦</div><p>No docket-tracked consignees.<br>Add docket numbers to see tiles.</p></div>';\u001b[39m\n\u001b[31m    return;\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m  const progMap = { delivered:100, transit:60,'in-transit':60, booked:20, pending:5 };\u001b[39m\n\u001b[31m  const progClr = { delivered:'var(--grn)', transit:'var(--ylw)','in-transit':'var(--ylw)', booked:'var(--sky)', pending:'var(--t4)' };\u001b[39m\n\u001b[31m  grid.innerHTML = items.map(c => {\u001b[39m\n\u001b[31m    const sp = c.status || 'booked';\u001b[39m\n\u001b[31m    const prog = progMap[sp] || 20;\u001b[39m\n\u001b[31m    const clr = progClr[sp] || 'var(--t4)';\u001b[39m\n\u001b[31m    return `\u001b[39m\n\u001b[31m    <div class=\\\"docket-tile\\\" onclick=\\\"showDocketDetail('${c.docket}','${c.courier||''}','${c.dispatchId}')\\\">\u001b[39m\n\u001b[31m      <div class=\\\"docket-tile-hdr\\\">\u001b[39m\n\u001b[31m        <div class=\\\"docket-tile-left\\\">\u001b[39m\n\u001b[31m          <div class=\\\"docket-num\\\">${c.docket}</div>\u001b[39m\n\u001b[31m          <div class=\\\"docket-store\\\">${c.name} — ${c.city}</div>\u001b[39m\n\u001b[31m          <div class=\\\"docket-courier\\\">${c.courier||''} · ${c.client||''} ${c.jobref ? '› '+c.jobref : ''}</div>\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m        <span class=\\\"status-pill sp-${sp}\\\">${sp}</span>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      <div style=\\\"display:flex;gap:6px;margin-bottom:6px;align-items:center;\\\">\u001b[39m\n\u001b[31m        <input type=\\\"checkbox\\\" id=\\\"sel_${c.docket.replace(/[^a-z0-9]/gi,'_')}\\\"\u001b[39m\n\u001b[31m          onclick=\\\"event.stopPropagation();toggleTileSelect('${c.docket}')\\\"\u001b[39m\n\u001b[31m          style=\\\"width:14px;height:14px;cursor:pointer;accent-color:var(--acc);\\\">\u001b[39m\n\u001b[31m        <label style=\\\"font-size:.6rem;color:var(--t3);font-family:var(--fnt-mono);cursor:pointer;\\\"\u001b[39m\n\u001b[31m          onclick=\\\"event.stopPropagation();toggleTileSelect('${c.docket}')\\\">Select</label>\u001b[39m\n\u001b[31m        ${c.manualStatus ?\u001b[39m\n\u001b[31m          `<span style=\\\"font-size:.6rem;font-family:var(--fnt-mono);color:var(--grn);\\\">📞 ${c.manualStatus}</span>` : ''}\u001b[39m\n\u001b[31m        ${c.podUploaded ? '<span style=\\\"font-size:.6rem;color:var(--grn);font-family:var(--fnt-mono);\\\">✓ POD</span>' : ''}\u001b[39m\n\u001b[31m        <button onclick=\\\"event.stopPropagation();showManualTracking('${c.docket}','${c.dispatchId}','${c.name}','${c.phone||''}','${c.city||''}')\\\"\u001b[39m\n\u001b[31m          style=\\\"font-size:.6rem;background:rgba(56,189,248,.1);border:1px solid rgba(56,189,248,.3);\u001b[39m\n\u001b[31m          color:var(--sky);padding:2px 7px;border-radius:10px;cursor:pointer;font-family:var(--fnt-mono);\\\">📞 Call Log</button>\u001b[39m\n\u001b[31m        <button onclick=\\\"event.stopPropagation();showPODUpload('${c.docket}','${c.dispatchId}')\\\"\u001b[39m\n\u001b[31m          style=\\\"font-size:.6rem;background:none;border:1px solid var(--bdr);color:var(--t3);\u001b[39m\n\u001b[31m          padding:2px 7px;border-radius:10px;cursor:pointer;font-family:var(--fnt-mono);\\\">📸 POD</button>\u001b[39m\n\u001b[31m        ${c.manualDelivered ? `<button onclick=\\\"event.stopPropagation();resumeAutoTracking('${c.docket}','${c.dispatchId}')\\\"\u001b[39m\n\u001b[31m          style=\\\"font-size:.6rem;background:rgba(251,146,60,.1);border:1px solid rgba(251,146,60,.4);\u001b[39m\n\u001b[31m          color:#fb923c;padding:2px 7px;border-radius:10px;cursor:pointer;font-family:var(--fnt-mono);\\\">↩ Undo</button>` : ''}\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      <div class=\\\"docket-tile-footer\\\">\u001b[39m\n\u001b[31m        <div style=\\\"flex:1;min-width:0;\\\">\u001b[39m\n\u001b[31m          <span class=\\\"docket-last-event docket-evt-${c.docket}\\\" id=\\\"evt_${c.docket.replace(/[^a-z0-9]/gi,'_')}\\\">${c.lastEvent || 'No tracking yet'}</span>\u001b[39m\n\u001b[31m          <span class=\\\"track-spinner\\\" id=\\\"spin_${c.docket.replace(/[^a-z0-9]/gi,'_')}\\\" style=\\\"display:none;color:var(--ylw);font-family:var(--fnt-mono);font-size:.68rem;animation:blink 1s infinite;\\\">⏳ tracking...</span>\u001b[39m\n\u001b[31m          ${c.lastScanLocation && c.lastScanLocation !== c.lastEvent ?·\u001b[39m\n\u001b[31m            `<div style=\\\"font-size:.62rem;color:var(--t3);font-family:var(--fnt-mono);margin-top:2px;\\\">📍 ${c.lastScanLocation}</div>` : ''}\u001b[39m\n\u001b[31m          ${c.lastTrackedAt ?·\u001b[39m\n\u001b[31m            `<div style=\\\"font-size:.6rem;color:var(--t4);font-family:var(--fnt-mono);margin-top:1px;\\\">🕐 tracked ${new Date(c.lastTrackedAt).toLocaleTimeString('en-IN',{hour:'2-digit',minute:'2-digit'})}</div>` : ''}\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m        <button class=\\\"btn-track-live live-btn-${c.docket.replace(/[^a-z0-9]/gi,'_')}\\\" onclick=\\\"event.stopPropagation();liveTrackTile('${c.docket}','${c.courier||''}','${c.dispatchId}')\\\">\u001b[39m\n\u001b[31m          <span class=\\\"live-dot\\\"></span>LIVE\u001b[39m\n\u001b[31m        </button>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      <div class=\\\"docket-progress\\\">\u001b[39m\n\u001b[31m        <div class=\\\"docket-progress-fill\\\" style=\\\"width:${prog}%;background:${clr};\\\"></div>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m    </div>`;\u001b[39m\n\u001b[31m  }).join('');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction filterDash(f, el) {\u001b[39m\n\u001b[31m  dashFilter = f;\u001b[39m\n\u001b[31m  document.querySelectorAll('#dashFilters .btn-sm').forEach(b => b.classList.remove('primary'));\u001b[39m\n\u001b[31m  el.classList.add('primary');\u001b[39m\n\u001b[31m  renderDashboard();\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31masync function liveTrackTile(docket, courier, dispatchId) {\u001b[39m\n\u001b[31m  showToast(`Tracking ${docket}...`, 'info');\u001b[39m\n\u001b[31m  const result = await fetchTracking(docket, courier);\u001b[39m\n\u001b[31m  // Update status in dispatches\u001b[39m\n\u001b[31m  for (const d of dispatches) {\u001b[39m\n\u001b[31m    if (d.id !== dispatchId) continue;\u001b[39m\n\u001b[31m    for (const c of d.consignees) {\u001b[39m\n\u001b[31m      if (c.docket === docket) {\u001b[39m\n\u001b[31m        c.status = result.status || c.status;\u001b[39m\n\u001b[31m        c.lastEvent = result.lastEvent || c.lastEvent;\u001b[39m\n\u001b[31m        c.lastUpdated = new Date().toISOString();\u001b[39m\n\u001b[31m      }\u001b[39m\n\u001b[31m    }\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m  saveData();\u001b[39m\n\u001b[31m  renderDashboard();\u001b[39m\n\u001b[31m  if (result.status === 'failed') {\u001b[39m\n\u001b[31m    // Open courier site as fallback\u001b[39m\n\u001b[31m    openCourierTrackUrl(docket, courier);\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction showDocketDetail(docket, courier, dispatchId) {\u001b[39m\n\u001b[31m  const d = dispatches.find(x => x.id === dispatchId);\u001b[39m\n\u001b[31m  const c = d?.consignees?.find(x => x.docket === docket);\u001b[39m\n\u001b[31m  if (!c) return;\u001b[39m\n\u001b[31m  const content = document.getElementById('detailContent');\u001b[39m\n\u001b[31m  content.innerHTML = `\u001b[39m\n\u001b[31m    <div class=\\\"sheet-title\\\">${docket}</div>\u001b[39m\n\u001b[31m    <div class=\\\"info-tag\\\">📍 <strong>${c.name}</strong> — ${c.city}</div>\u001b[39m\n\u001b[31m    ${c.phone ? `<div class=\\\"info-tag\\\">📞 ${c.phone}</div>` : ''}\u001b[39m\n\u001b[31m    <div class=\\\"info-tag\\\">📦 ${c.material||'—'} ${c.qty ? '('+c.qty+' pcs)':''}</div>\u001b[39m\n\u001b[31m    <div class=\\\"info-tag\\\">🚚 ${courier||'—'} · <span class=\\\"status-pill sp-${c.status||'pending'}\\\">${c.status||'pending'}</span></div>\u001b[39m\n\u001b[31m    ${c.lastEvent ? `<div class=\\\"info-tag\\\">🕐 ${c.lastEvent}</div>` : ''}\u001b[39m\n\u001b[31m    <div class=\\\"sec-div\\\"><span>Tracking History</span></div>\u001b[39m\n\u001b[31m    <div id=\\\"docketTimeline\\\">\u001b[39m\n\u001b[31m      ${c.timeline?.length ?\u001b[39m\n\u001b[31m        c.timeline.map(e => `<div class=\\\"track-evt\\\"><span class=\\\"track-time\\\">${e.time||''}</span><div><div class=\\\"track-desc\\\">${e.desc||''}</div><div class=\\\"track-loc\\\">${e.loc||''}</div></div></div>`).join('') :\u001b[39m\n\u001b[31m        '<div style=\\\"color:var(--t3);font-size:.78rem;padding:10px 0;\\\">No timeline data. Click Live Track to fetch.</div>'}\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m    <div style=\\\"display:flex;gap:6px;flex-wrap:wrap;margin-top:14px;\\\">\u001b[39m\n\u001b[31m      <button class=\\\"btn btn-ghost\\\" style=\\\"flex:1;\\\" onclick=\\\"openCourierTrackUrl('${docket}','${courier}')\\\">🔗 Courier Site</button>\u001b[39m\n\u001b[31m      <button class=\\\"btn btn-orange\\\" style=\\\"flex:1;\\\" onclick=\\\"liveTrackTile('${docket}','${courier}','${dispatchId}');closeOverlay('detailOverlay')\\\">⚡ Live Track</button>\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m    <div style=\\\"display:flex;gap:6px;flex-wrap:wrap;margin-top:6px;\\\">\u001b[39m\n\u001b[31m      <button class=\\\"btn-wa\\\" style=\\\"flex:1;padding:8px;\\\" onclick=\\\"closeOverlay('detailOverlay');showPODUpload('${docket}','${dispatchId}')\\\">📸 Upload POD</button>\u001b[39m\n\u001b[31m      ${c.podUploaded ? '<span style=\\\"color:var(--grn);font-size:.72rem;font-family:var(--fnt-mono);padding:6px;\\\">✓ POD uploaded</span>' : ''}\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m  `;\u001b[39m\n\u001b[31m  openOverlay('detailOverlay');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31m// TRACKING ENGINE\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31mfunction _normaliseStatus(raw) {\u001b[39m\n\u001b[31m  const s = (raw || '').toLowerCase().trim();\u001b[39m\n\u001b[31m  if (!s || s === 'error') return null;\u001b[39m\n\u001b[31m  if (s.includes('undeliver') || s.includes('delivery_fail') ||\u001b[39m\n\u001b[31m      s.includes('failed_delivery') || s.includes('not deliver') ||\u001b[39m\n\u001b[31m      s.includes('return')  || s.includes('rto')) return 'pending';\u001b[39m\n\u001b[31m  if (s.includes('deliver'))                       return 'delivered';\u001b[39m\n\u001b[31m  if (s.includes('transit') || s.includes('hub') ||\u001b[39m\n\u001b[31m      s.includes('inbound') || s.includes('outbound') ||\u001b[39m\n\u001b[31m      s.includes('out for'))                       return 'transit';\u001b[39m\n\u001b[31m  if (s.includes('book') || s.includes('pick'))   return 'booked';\u001b[39m\n\u001b[31m  return s;\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31masync function _fetchWithRetry(url, opts, maxRetries = 1, retryDelayMs = 4000) {\u001b[39m\n\u001b[31m  for (let attempt = 0; attempt <= maxRetries; attempt++) {\u001b[39m\n\u001b[31m    try {\u001b[39m\n\u001b[31m      const resp = await fetch(url, opts);\u001b[39m\n\u001b[31m      if (resp.ok) return resp;\u001b[39m\n\u001b[31m    } catch(e) {\u001b[39m\n\u001b[31m      if (attempt === maxRetries) throw e;\u001b[39m\n\u001b[31m      await new Promise(r => setTimeout(r, retryDelayMs));\u001b[39m\n\u001b[31m    }\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m  return null;\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31masync function fetchTracking(docket, courier) {\u001b[39m\n\u001b[31m  // Strategy 1: VPS proxy (SkyKing + others that block direct fetch)\u001b[39m\n\u001b[31m  const proxyUrl = window.appConfig.proxy_url;\u001b[39m\n\u001b[31m  if (proxyUrl) {\u001b[39m\n\u001b[31m    try {\u001b[39m\n\u001b[31m      const url  = `${proxyUrl}?courier=${encodeURIComponent(courier)}&docket=${encodeURIComponent(docket)}`;\u001b[39m\n\u001b[31m      const resp = await _fetchWithRetry(url, { signal: AbortSignal.timeout(15000) });\u001b[39m\n\u001b[31m      if (resp) {\u001b[39m\n\u001b[31m        const data      = await resp.json();\u001b[39m\n\u001b[31m        const normStatus = _normaliseStatus(data.status);\u001b[39m\n\u001b[31m        if (normStatus) {\u001b[39m\n\u001b[31m          return { status: normStatus, lastEvent: data.lastEvent || '', timeline: data.timeline || [] };\u001b[39m\n\u001b[31m        }\u001b[39m\n\u001b[31m        openCourierTrackUrl(docket, courier);\u001b[39m\n\u001b[31m        return { status: 'transit', lastEvent: data.lastEvent || 'Check courier site', timeline: [] };\u001b[39m\n\u001b[31m      }\u001b[39m\n\u001b[31m    } catch(e) {}\u001b[39m\n\u001b[31m  }·\u001b[39m\n\u001b[31m  // Strategy 2: Delhivery direct API\u001b[39m\n\u001b[31m  if (courier === 'Delhivery') {\u001b[39m\n\u001b[31m    try {\u001b[39m\n\u001b[31m      const resp = await _fetchWithRetry(\u001b[39m\n\u001b[31m        `https://dlv-api.delhivery.com/v3/track?waybill=${docket}`,\u001b[39m\n\u001b[31m        { signal: AbortSignal.timeout(8000) }\u001b[39m\n\u001b[31m      );\u001b[39m\n\u001b[31m      if (resp) {\u001b[39m\n\u001b[31m        const data = await resp.json();\u001b[39m\n\u001b[31m        const pkg  = data.ShipmentData?.[0]?.Shipment;\u001b[39m\n\u001b[31m        if (pkg) {\u001b[39m\n\u001b[31m          const normStatus = _normaliseStatus(pkg.Status?.Status);\u001b[39m\n\u001b[31m          return { status: normStatus || 'transit', lastEvent: pkg.Status?.Instructions || '', timeline: [] };\u001b[39m\n\u001b[31m        }\u001b[39m\n\u001b[31m      }\u001b[39m\n\u001b[31m    } catch(e) {}\u001b[39m\n\u001b[31m  }·\u001b[39m\n\u001b[31m  return { status: 'failed', lastEvent: 'Open courier site to check', timeline: [] };\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction openCourierTrackUrl(docket, courier) {\u001b[39m\n\u001b[31m  const m = COURIER_MASTER[courier];\u001b[39m\n\u001b[31m  if (!m) return;\u001b[39m\n\u001b[31m  const url = m.trackUrl.replace('{docket}', docket);\u001b[39m\n\u001b[31m  window.open(url, '_blank');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// Track tab - multiple dockets\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31m// TRACK TAB — new logic\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31mlet _trackSelected = new Set(); // dockets currently selected for tracking·\u001b[39m\n\u001b[31m// Called when Track tab opens or courier changes\u001b[39m\n\u001b[31mfunction buildTrackDocketChips() {\u001b[39m\n\u001b[31m  const courier = document.getElementById('t_courier').value;\u001b[39m\n\u001b[31m  const m = COURIER_MASTER[courier];\u001b[39m\n\u001b[31m  const maxBulk = m?.maxBulk || 1;\u001b[39m\n\u001b[31m  const limitEl = document.getElementById('trackCourierLimit');\u001b[39m\n\u001b[31m  if (limitEl) limitEl.textContent = `Max ${maxBulk} at once for ${courier}`;·\u001b[39m\n\u001b[31m  // Gather all dockets from active dispatches for this courier\u001b[39m\n\u001b[31m  const activeDis = dispatches.filter(d => d.status !== 'draft');\u001b[39m\n\u001b[31m  const allDockets = activeDis.flatMap(d =>\u001b[39m\n\u001b[31m    (d.consignees || [])\u001b[39m\n\u001b[31m      .filter(c => c.docket && d.courier === courier)\u001b[39m\n\u001b[31m      .map(c => ({ docket: c.docket, name: c.name, city: c.city, status: c.status || 'booked' }))\u001b[39m\n\u001b[31m  );·\u001b[39m\n\u001b[31m  // Also include dockets from OTHER couriers as un-selectable reference\u001b[39m\n\u001b[31m  const otherDockets = activeDis.flatMap(d =>\u001b[39m\n\u001b[31m    (d.consignees || [])\u001b[39m\n\u001b[31m      .filter(c => c.docket && d.courier !== courier)\u001b[39m\n\u001b[31m      .map(c => ({ docket: c.docket, name: c.name, courier: d.courier }))\u001b[39m\n\u001b[31m  );·\u001b[39m\n\u001b[31m  const chips = document.getElementById('trackDocketChips');\u001b[39m\n\u001b[31m  if (!allDockets.length && !otherDockets.length) {\u001b[39m\n\u001b[31m    chips.innerHTML = '<span style=\\\"font-size:.72rem;color:var(--t3);font-family:var(--fnt-mono);\\\">No dispatched dockets yet — enter manually below</span>';\u001b[39m\n\u001b[31m    return;\u001b[39m\n\u001b[31m  }·\u001b[39m\n\u001b[31m  const spClr = { delivered:'var(--grn)', transit:'var(--ylw)', booked:'var(--sky)', pending:'var(--t4)' };·\u001b[39m\n\u001b[31m  chips.innerHTML = [\u001b[39m\n\u001b[31m    // This courier's dockets\u001b[39m\n\u001b[31m    ...allDockets.map(d => {\u001b[39m\n\u001b[31m      const sel = _trackSelected.has(d.docket);\u001b[39m\n\u001b[31m      const clr = spClr[d.status] || 'var(--t4)';\u001b[39m\n\u001b[31m      return `<button onclick=\\\"toggleTrackDocket('${d.docket}')\\\" id=\\\"chip_${d.docket}\\\"\u001b[39m\n\u001b[31m        style=\\\"font-family:var(--fnt-mono);font-size:.72rem;padding:4px 10px;border-radius:20px;\u001b[39m\n\u001b[31m        cursor:pointer;transition:all .2s;border:1px solid ${sel ? 'var(--acc)' : 'var(--bdr)'};\u001b[39m\n\u001b[31m        background:${sel ? 'rgba(249,115,22,.15)' : 'var(--sur2)'};\u001b[39m\n\u001b[31m        color:${sel ? 'var(--acc)' : 'var(--t2)'};\\\">\u001b[39m\n\u001b[31m        ${d.docket} <span style=\\\"font-size:.6rem;color:${clr};\\\">● ${d.status}</span>\u001b[39m\n\u001b[31m        ${d.city ? `<span style=\\\"font-size:.6rem;color:var(--t3);margin-left:4px;\\\">${d.city}</span>` : ''}\u001b[39m\n\u001b[31m      </button>`;\u001b[39m\n\u001b[31m    }),\u001b[39m\n\u001b[31m    // Other couriers — show greyed, click selects and switches courier\u001b[39m\n\u001b[31m    ...otherDockets.slice(0, 5).map(d =>\u001b[39m\n\u001b[31m      `<span title=\\\"${d.courier}\\\" style=\\\"font-family:var(--fnt-mono);font-size:.68rem;padding:3px 8px;\u001b[39m\n\u001b[31m        border-radius:20px;border:1px dashed var(--bdr);color:var(--t4);cursor:default;\\\">\u001b[39m\n\u001b[31m        ${d.docket} <span style=\\\"font-size:.55rem;\\\">${d.courier}</span>\u001b[39m\n\u001b[31m      </span>`\u001b[39m\n\u001b[31m    )\u001b[39m\n\u001b[31m  ].join('');·\u001b[39m\n\u001b[31m  updateTrackSummary();\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction toggleTrackDocket(docket) {\u001b[39m\n\u001b[31m  const courier = document.getElementById('t_courier').value;\u001b[39m\n\u001b[31m  const m = COURIER_MASTER[courier];\u001b[39m\n\u001b[31m  const maxBulk = m?.maxBulk || 1;·\u001b[39m\n\u001b[31m  if (_trackSelected.has(docket)) {\u001b[39m\n\u001b[31m    _trackSelected.delete(docket);\u001b[39m\n\u001b[31m  } else {\u001b[39m\n\u001b[31m    if (_trackSelected.size >= maxBulk) {\u001b[39m\n\u001b[31m      showToast(`${courier} supports max ${maxBulk} docket(s) at once`, 'err');\u001b[39m\n\u001b[31m      return;\u001b[39m\n\u001b[31m    }\u001b[39m\n\u001b[31m    _trackSelected.add(docket);\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m  buildTrackDocketChips(); // re-render chips with updated selection\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction addManualDockets() {\u001b[39m\n\u001b[31m  const raw = document.getElementById('t_manual_dockets').value;\u001b[39m\n\u001b[31m  const courier = document.getElementById('t_courier').value;\u001b[39m\n\u001b[31m  const m = COURIER_MASTER[courier];\u001b[39m\n\u001b[31m  const maxBulk = m?.maxBulk || 1;\u001b[39m\n\u001b[31m  const newDockets = raw.split(/[,\\\\n]/).map(d => d.trim()).filter(Boolean);\u001b[39m\n\u001b[31m  let added = 0;\u001b[39m\n\u001b[31m  for (const d of newDockets) {\u001b[39m\n\u001b[31m    if (_trackSelected.size >= maxBulk) {\u001b[39m\n\u001b[31m      showToast(`Max ${maxBulk} reached for ${courier}`, 'err');\u001b[39m\n\u001b[31m      break;\u001b[39m\n\u001b[31m    }\u001b[39m\n\u001b[31m    _trackSelected.add(d);\u001b[39m\n\u001b[31m    added++;\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m  document.getElementById('t_manual_dockets').value = '';\u001b[39m\n\u001b[31m  buildTrackDocketChips();\u001b[39m\n\u001b[31m  if (added) showToast(`${added} docket(s) added ✓`, 'ok');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction clearTrackSelection() {\u001b[39m\n\u001b[31m  _trackSelected.clear();\u001b[39m\n\u001b[31m  buildTrackDocketChips();\u001b[39m\n\u001b[31m  document.getElementById('trackResultsArea').innerHTML = '';\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction updateTrackSummary() {\u001b[39m\n\u001b[31m  const summary = document.getElementById('trackSelectedSummary');\u001b[39m\n\u001b[31m  const countEl = document.getElementById('trackSelectedCount');\u001b[39m\n\u001b[31m  const listEl  = document.getElementById('trackSelectedList');\u001b[39m\n\u001b[31m  if (!summary) return;\u001b[39m\n\u001b[31m  const arr = Array.from(_trackSelected);\u001b[39m\n\u001b[31m  summary.style.display = arr.length ? 'block' : 'none';\u001b[39m\n\u001b[31m  if (countEl) countEl.textContent = `${arr.length} docket(s) selected`;\u001b[39m\n\u001b[31m  if (listEl)  listEl.textContent  = arr.slice(0, 5).join(', ') + (arr.length > 5 ? `... +${arr.length-5}` : '');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction openTrackOnCourierSite() {\u001b[39m\n\u001b[31m  const courier = document.getElementById('t_courier').value;\u001b[39m\n\u001b[31m  const dockets = Array.from(_trackSelected);\u001b[39m\n\u001b[31m  if (!dockets.length) { showToast('Select dockets first', 'err'); return; }\u001b[39m\n\u001b[31m  const m = COURIER_MASTER[courier];\u001b[39m\n\u001b[31m  if (!m) return;\u001b[39m\n\u001b[31m  // SkyKing: open with all CNOs comma-separated in URL\u001b[39m\n\u001b[31m  let url;\u001b[39m\n\u001b[31m  if (courier === 'SkyKing') {\u001b[39m\n\u001b[31m    url = `https://skyking.co/track?cno=${dockets.join(',')}`;\u001b[39m\n\u001b[31m  } else if (dockets.length === 1) {\u001b[39m\n\u001b[31m    url = m.trackUrl.replace('{docket}', dockets[0]);\u001b[39m\n\u001b[31m  } else {\u001b[39m\n\u001b[31m    url = m.trackBulkUrl || m.trackUrl.replace('{docket}', dockets[0]);\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m  window.open(url, '_blank');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31masync function trackSelected() {\u001b[39m\n\u001b[31m  const courier = document.getElementById('t_courier').value;\u001b[39m\n\u001b[31m  const dockets = Array.from(_trackSelected);\u001b[39m\n\u001b[31m  if (!dockets.length) { showToast('Select at least one docket', 'err'); return; }·\u001b[39m\n\u001b[31m  const area = document.getElementById('trackResultsArea');\u001b[39m\n\u001b[31m  area.innerHTML = `<div class=\\\"card\\\" style=\\\"text-align:center;padding:20px;\\\">\u001b[39m\n\u001b[31m    <div style=\\\"font-size:1.5rem;margin-bottom:8px;\\\">⏳</div>\u001b[39m\n\u001b[31m    <div style=\\\"font-family:var(--fnt-mono);font-size:.78rem;color:var(--t2);\\\">Tracking ${dockets.length} docket(s) via ${courier}...</div>\u001b[39m\n\u001b[31m  </div>`;·\u001b[39m\n\u001b[31m  let resultsHTML = '';\u001b[39m\n\u001b[31m  for (const docket of dockets) {\u001b[39m\n\u001b[31m    const result = await fetchTracking(docket, courier);\u001b[39m\n\u001b[31m    const sp = (!result.status || result.status === 'failed') ? 'pending' : result.status;\u001b[39m\n\u001b[31m    const spClr = { delivered:'var(--grn)', transit:'var(--ylw)', booked:'var(--sky)', pending:'var(--t3)' };\u001b[39m\n\u001b[31m    const clr = spClr[sp] || 'var(--t3)';\u001b[39m\n\u001b[31m    // Update stored dispatch if this docket exists in our records\u001b[39m\n\u001b[31m    for (const d of dispatches) {\u001b[39m\n\u001b[31m      for (const c of (d.consignees||[])) {\u001b[39m\n\u001b[31m        if (c.docket === docket && result.status && result.status !== 'failed') {\u001b[39m\n\u001b[31m          c.status = result.status;\u001b[39m\n\u001b[31m          c.lastEvent = result.lastEvent;\u001b[39m\n\u001b[31m          c.lastTrackedAt = new Date().toISOString();\u001b[39m\n\u001b[31m        }\u001b[39m\n\u001b[31m      }\u001b[39m\n\u001b[31m    }\u001b[39m\n\u001b[31m    resultsHTML += `\u001b[39m\n\u001b[31m    <div style=\\\"background:var(--sur);border:1px solid var(--bdr);border-left:3px solid ${clr};\u001b[39m\n\u001b[31m      border-radius:var(--r-sm);padding:10px 12px;margin-bottom:8px;\\\">\u001b[39m\n\u001b[31m      <div style=\\\"display:flex;justify-content:space-between;align-items:center;margin-bottom:6px;\\\">\u001b[39m\n\u001b[31m        <span style=\\\"font-family:var(--fnt-mono);font-size:.9rem;color:var(--sky);\\\">${docket}</span>\u001b[39m\n\u001b[31m        <span class=\\\"status-pill sp-${sp}\\\">${sp}</span>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      <div style=\\\"font-size:.75rem;color:var(--t2);font-family:var(--fnt-mono);\\\">${result.lastEvent || '—'}</div>\u001b[39m\n\u001b[31m      ${result.lastScanLocation ? `<div style=\\\"font-size:.68rem;color:var(--t3);margin-top:3px;\\\">📍 ${result.lastScanLocation}</div>` : ''}\u001b[39m\n\u001b[31m      <div style=\\\"display:flex;gap:8px;margin-top:8px;\\\">\u001b[39m\n\u001b[31m        <button class=\\\"btn-sm\\\" onclick=\\\"openCourierTrackUrl('${docket}','${courier}')\\\">🔗 Open on ${courier}</button>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m    </div>`;\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m  saveData();\u001b[39m\n\u001b[31m  area.innerHTML = `<div style=\\\"margin-top:4px;\\\">${resultsHTML}</div>`;\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// Keep old names as aliases for compatibility\u001b[39m\n\u001b[31mfunction trackMultiple() { trackSelected(); }\u001b[39m\n\u001b[31mfunction openCourierSite() { openTrackOnCourierSite(); }·\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31m// HISTORY\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31mfunction renderHistory() {\u001b[39m\n\u001b[31m  const list = document.getElementById('historyList');\u001b[39m\n\u001b[31m  if (!dispatches.length) {\u001b[39m\n\u001b[31m    list.innerHTML = '<div class=\\\"empty\\\"><div class=\\\"e-icon\\\">📭</div><p>No dispatches yet.</p></div>';\u001b[39m\n\u001b[31m    return;\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m  list.innerHTML = dispatches.map(d => {\u001b[39m\n\u001b[31m    const cons = d.consignees?.length || 0;\u001b[39m\n\u001b[31m    const docketed = d.consignees?.filter(c=>c.docket).length || 0;\u001b[39m\n\u001b[31m    const clr = d.status === 'draft' ? 'var(--ylw)' : 'var(--grn)';\u001b[39m\n\u001b[31m    return `\u001b[39m\n\u001b[31m    <div class=\\\"docket-tile\\\" onclick=\\\"showDispatchDetail('${d.id}')\\\">\u001b[39m\n\u001b[31m      <div class=\\\"docket-tile-hdr\\\">\u001b[39m\n\u001b[31m        <div class=\\\"docket-tile-left\\\">\u001b[39m\n\u001b[31m          <div class=\\\"docket-num\\\" style=\\\"font-size:.78rem;\\\">${d.id}</div>\u001b[39m\n\u001b[31m          <div class=\\\"docket-store\\\">${d.client||'No client'} ${d.jobref ? '› '+d.jobref : ''}</div>\u001b[39m\n\u001b[31m          <div class=\\\"docket-courier\\\">📦 ${cons} consignees · ${docketed} docketed · ${d.courier||'—'} · ${d.dispatchDate||d.createdAt?.slice(0,10)||'—'}</div>\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m        <span class=\\\"status-pill\\\" style=\\\"background:rgba(0,0,0,.2);color:${clr};\\\">${d.status||'active'}</span>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m    </div>`;\u001b[39m\n\u001b[31m  }).join('');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction showDispatchDetail(id) {\u001b[39m\n\u001b[31m  const d = dispatches.find(x => x.id === id);\u001b[39m\n\u001b[31m  if (!d) return;\u001b[39m\n\u001b[31m  const content = document.getElementById('detailContent');\u001b[39m\n\u001b[31m  content.innerHTML = `\u001b[39m\n\u001b[31m    <div class=\\\"sheet-title\\\">${d.client} ${d.jobref ? '› '+d.jobref : ''}</div>\u001b[39m\n\u001b[31m    <div class=\\\"info-tag\\\"><strong>${d.id}</strong> · ${d.courier||'—'} · ${d.dispatchDate||'—'}</div>\u001b[39m\n\u001b[31m    <div class=\\\"sec-div\\\"><span>Consignees (${d.consignees?.length||0})</span></div>\u001b[39m\n\u001b[31m    ${(d.consignees||[]).map((c,i) => `\u001b[39m\n\u001b[31m      <div style=\\\"background:var(--sur2);border:1px solid var(--bdr);border-radius:var(--r-sm);padding:10px;margin-bottom:6px;\\\">\u001b[39m\n\u001b[31m        <div style=\\\"font-weight:600;margin-bottom:4px;\\\">${i+1}. ${c.name} — ${c.city}</div>\u001b[39m\n\u001b[31m        ${c.docket ? `<div style=\\\"font-family:var(--fnt-mono);font-size:.78rem;color:var(--sky);\\\">🔖 ${c.docket}</div>` : ''}\u001b[39m\n\u001b[31m        ${c.material ? `<div style=\\\"font-size:.75rem;color:var(--t2);\\\">📦 ${c.material} ${c.qty ? '('+c.qty+')':''}</div>` : ''}\u001b[39m\n\u001b[31m        <span class=\\\"status-pill sp-${c.status||'pending'}\\\" style=\\\"margin-top:4px;display:inline-block;\\\">${c.status||'pending'}</span>\u001b[39m\n\u001b[31m      </div>`).join('')}\u001b[39m\n\u001b[31m    ${d.remarks ? `<div class=\\\"info-tag\\\">📝 ${d.remarks}</div>` : ''}\u001b[39m\n\u001b[31m    <div class=\\\"btn-row\\\" style=\\\"margin-top:14px;\\\">\u001b[39m\n\u001b[31m      <button class=\\\"btn btn-ghost\\\" onclick=\\\"deleteDispatch('${d.id}');closeOverlay('detailOverlay')\\\">🗑️ Delete</button>\u001b[39m\n\u001b[31m      <button class=\\\"btn btn-orange\\\" onclick=\\\"closeOverlay('detailOverlay');showPane('dashboard')\\\">📊 View in Dashboard</button>\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m  `;\u001b[39m\n\u001b[31m  openOverlay('detailOverlay');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction deleteDispatch(id) {\u001b[39m\n\u001b[31m  if (!confirm('Delete this dispatch?')) return;\u001b[39m\n\u001b[31m  dispatches = dispatches.filter(d => d.id !== id);\u001b[39m\n\u001b[31m  saveData();\u001b[39m\n\u001b[31m  updateHistCount();\u001b[39m\n\u001b[31m  renderHistory();\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction updateHistCount() {\u001b[39m\n\u001b[31m  document.getElementById('histCount').textContent = dispatches.length;\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31m// GOOGLE SHEETS\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31masync function logToSheets(d) {\u001b[39m\n\u001b[31m  const url = window.appConfig.webhook_url;\u001b[39m\n\u001b[31m  if (!url) return;\u001b[39m\n\u001b[31m  try {\u001b[39m\n\u001b[31m    await fetch(url, {\u001b[39m\n\u001b[31m      method: 'POST', mode: 'no-cors',\u001b[39m\n\u001b[31m      headers: { 'Content-Type': 'application/json' },\u001b[39m\n\u001b[31m      body: JSON.stringify({\u001b[39m\n\u001b[31m        type: 'dispatch', id: d.id, client: d.client, jobref: d.jobref,\u001b[39m\n\u001b[31m        courier: d.courier, consignees: d.consignees?.length||0,\u001b[39m\n\u001b[31m        destinations: d.consignees?.map(c=>c.city).join(', ')||'',\u001b[39m\n\u001b[31m        dockets: d.consignees?.filter(c=>c.docket).map(c=>c.docket).join(', ')||'',\u001b[39m\n\u001b[31m        dispatchDate: d.dispatchDate, status: d.status,\u001b[39m\n\u001b[31m        timestamp: new Date().toISOString(), company: window.appConfig.company\u001b[39m\n\u001b[31m      })\u001b[39m\n\u001b[31m    });\u001b[39m\n\u001b[31m  } catch(e) {}\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31m// CONFIG — Google Sheet is the source of truth\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════·\u001b[39m\n\u001b[31mfunction onSheetsUrlChange() {\u001b[39m\n\u001b[31m  const url = document.getElementById('cfg_sheets_url').value.trim();\u001b[39m\n\u001b[31m  // Show sync button state\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction saveSheetsUrl() {\u001b[39m\n\u001b[31m  const url = document.getElementById('cfg_sheets_url').value.trim();\u001b[39m\n\u001b[31m  if (!url) { showToast('Enter the Apps Script URL first', 'err'); return; }\u001b[39m\n\u001b[31m  window.appConfig.sheets_url = url;\u001b[39m\n\u001b[31m  saveSheetsUrlLocal();\u001b[39m\n\u001b[31m  syncFromSheet(false);\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m/**\u001b[39m\n\u001b[31m * Fetch all config from Google Sheet and apply to appConfig.\u001b[39m\n\u001b[31m * Called on startup (silent=true) and manually (silent=false).\u001b[39m\n\u001b[31m */\u001b[39m\n\u001b[31masync function syncFromSheet(silent = false) {\u001b[39m\n\u001b[31m  const url = window.appConfig.sheets_url;\u001b[39m\n\u001b[31m  if (!url) { if (!silent) showToast('No Sheets URL configured', 'err'); return; }·\u001b[39m\n\u001b[31m  setSyncStatus('syncing', 'Syncing config from Google Sheet...');\u001b[39m\n\u001b[31m  try {\u001b[39m\n\u001b[31m    const resp = await fetch(url + '?action=pwa_config', {\u001b[39m\n\u001b[31m      method: 'GET',\u001b[39m\n\u001b[31m      redirect: 'follow'\u001b[39m\n\u001b[31m    });\u001b[39m\n\u001b[31m    if (!resp.ok) throw new Error('HTTP ' + resp.status);\u001b[39m\n\u001b[31m    const cfg = await resp.json();·\u001b[39m\n\u001b[31m    // Apply all values from sheet to appConfig\u001b[39m\n\u001b[31m    const keyMap = {\u001b[39m\n\u001b[31m      proxy_url:           'proxy_url',\u001b[39m\n\u001b[31m      webhook_url:         'webhook_url',\u001b[39m\n\u001b[31m      ollama_url:          'ollama_url',\u001b[39m\n\u001b[31m      ocr_mode:            'ocr_mode',\u001b[39m\n\u001b[31m      company:             'company',\u001b[39m\n\u001b[31m      city:                'city',\u001b[39m\n\u001b[31m      auto_log:            'auto_log',\u001b[39m\n\u001b[31m      refresh_on_load:     'refresh_on_load',\u001b[39m\n\u001b[31m      admin_password:      'admin_password',\u001b[39m\n\u001b[31m      n8n_dispatch_webhook:'n8n_dispatch_webhook',\u001b[39m\n\u001b[31m    };\u001b[39m\n\u001b[31m    Object.entries(keyMap).forEach(([sheetKey, cfgKey]) => {\u001b[39m\n\u001b[31m      if (cfg[sheetKey] !== undefined && cfg[sheetKey] !== '') {\u001b[39m\n\u001b[31m        window.appConfig[cfgKey] = cfg[sheetKey];\u001b[39m\n\u001b[31m      }\u001b[39m\n\u001b[31m    });·\u001b[39m\n\u001b[31m    // Apply courier master if provided\u001b[39m\n\u001b[31m    if (cfg._courier_master && Array.isArray(cfg._courier_master)) {\u001b[39m\n\u001b[31m      cfg._courier_master.forEach(c => {\u001b[39m\n\u001b[31m        if (c.name && c.trackUrl) {\u001b[39m\n\u001b[31m          COURIER_MASTER[c.name] = {\u001b[39m\n\u001b[31m            trackUrl: c.trackUrl,\u001b[39m\n\u001b[31m            trackBulkUrl: c.bulkUrl || '',\u001b[39m\n\u001b[31m            maxBulk: parseInt(c.maxBulk) || 1\u001b[39m\n\u001b[31m          };\u001b[39m\n\u001b[31m        }\u001b[39m\n\u001b[31m      });\u001b[39m\n\u001b[31m    }·\u001b[39m\n\u001b[31m    renderCourierMasterList();\u001b[39m\n\u001b[31m    renderSheetConfigDisplay(cfg);\u001b[39m\n\u001b[31m    setSyncStatus('ok', 'Synced from sheet · ' + new Date().toLocaleTimeString());\u001b[39m\n\u001b[31m    if (!silent) showToast('Config synced from sheet ✓', 'ok');·\u001b[39m\n\u001b[31m    // If refresh_on_load is true, trigger dashboard refresh\u001b[39m\n\u001b[31m    if (window.appConfig.refresh_on_load === 'true') {\u001b[39m\n\u001b[31m      setTimeout(refreshAllTracking, 2000);\u001b[39m\n\u001b[31m    }·\u001b[39m\n\u001b[31m  } catch(e) {\u001b[39m\n\u001b[31m    setSyncStatus('err', 'Sync failed: ' + e.message);\u001b[39m\n\u001b[31m    if (!silent) showToast('Sync failed — check URL', 'err');\u001b[39m\n\u001b[31m    console.error('Sheets sync error:', e);\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction setSyncStatus(state, msg) {\u001b[39m\n\u001b[31m  const el = document.getElementById('syncStatus');\u001b[39m\n\u001b[31m  const icon = document.getElementById('syncIcon');\u001b[39m\n\u001b[31m  const msgEl = document.getElementById('syncMsg');\u001b[39m\n\u001b[31m  el.style.display = 'flex';\u001b[39m\n\u001b[31m  icon.textContent = state === 'ok' ? '✅' : state === 'err' ? '❌' : '⏳';\u001b[39m\n\u001b[31m  msgEl.textContent = msg;\u001b[39m\n\u001b[31m  el.style.borderColor = state === 'ok' ? 'rgba(34,197,94,.3)' : state === 'err' ? 'rgba(239,68,68,.3)' : 'rgba(234,179,8,.3)';\u001b[39m\n\u001b[31m  el.style.background = state === 'ok' ? 'rgba(34,197,94,.05)' : state === 'err' ? 'rgba(239,68,68,.05)' : 'rgba(234,179,8,.05)';\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction renderSheetConfigDisplay(cfg) {\u001b[39m\n\u001b[31m  const display = document.getElementById('sheetConfigDisplay');\u001b[39m\n\u001b[31m  display.style.display = 'block';\u001b[39m\n\u001b[31m  document.getElementById('sheetSyncTime').textContent = 'Last sync: ' + new Date().toLocaleTimeString();\u001b[39m\n\u001b[31m  const showKeys = ['proxy_url','webhook_url','ollama_url','ocr_mode','company','city','auto_log'];\u001b[39m\n\u001b[31m  document.getElementById('sheetConfigRows').innerHTML = showKeys.map(k => {\u001b[39m\n\u001b[31m    const val = cfg[k] || window.appConfig[k] || '—';\u001b[39m\n\u001b[31m    const isEmpty = !cfg[k];\u001b[39m\n\u001b[31m    return `<div class=\\\"courier-master-row\\\">\u001b[39m\n\u001b[31m      <div class=\\\"cml\\\">\u001b[39m\n\u001b[31m        <div class=\\\"cml-name\\\" style=\\\"font-family:var(--fnt-mono);font-size:.75rem;\\\">${k}</div>\u001b[39m\n\u001b[31m        <div class=\\\"cml-url\\\" style=\\\"${isEmpty ? 'color:var(--red);' : ''}\\\">${isEmpty ? '⚠️ Not set in sheet' : val}</div>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m    </div>`;\u001b[39m\n\u001b[31m  }).join('');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m/**\u001b[39m\n\u001b[31m * Push current config changes back to the sheet.\u001b[39m\n\u001b[31m * Called when user edits config from PWA (rare — usually done in sheet directly).\u001b[39m\n\u001b[31m */\u001b[39m\n\u001b[31masync function pushConfigToSheet(updates) {\u001b[39m\n\u001b[31m  const url = window.appConfig.sheets_url;\u001b[39m\n\u001b[31m  if (!url) return;\u001b[39m\n\u001b[31m  try {\u001b[39m\n\u001b[31m    await fetch(url, {\u001b[39m\n\u001b[31m      method: 'POST', mode: 'no-cors',\u001b[39m\n\u001b[31m      headers: { 'Content-Type': 'application/json' },\u001b[39m\n\u001b[31m      body: JSON.stringify({ type: 'save_config', config: updates })\u001b[39m\n\u001b[31m    });\u001b[39m\n\u001b[31m  } catch(e) {}\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction clearAll() {\u001b[39m\n\u001b[31m  if (!confirm('Delete ALL dispatch data?')) return;\u001b[39m\n\u001b[31m  dispatches = [];\u001b[39m\n\u001b[31m  saveData();\u001b[39m\n\u001b[31m  updateHistCount();\u001b[39m\n\u001b[31m  renderDashboard();\u001b[39m\n\u001b[31m  renderHistory();\u001b[39m\n\u001b[31m  showToast('Cleared', 'ok');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction renderCourierMasterList() {\u001b[39m\n\u001b[31m  const el = document.getElementById('courierMasterList');\u001b[39m\n\u001b[31m  el.innerHTML = Object.entries(COURIER_MASTER).map(([name, info]) => `\u001b[39m\n\u001b[31m    <div class=\\\"courier-master-row\\\">\u001b[39m\n\u001b[31m      <div class=\\\"cml\\\">\u001b[39m\n\u001b[31m        <div class=\\\"cml-name\\\">${name}</div>\u001b[39m\n\u001b[31m        <div class=\\\"cml-url\\\">${info.trackUrl}</div>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      <button class=\\\"btn-sm\\\" onclick=\\\"window.open('${info.trackBulkUrl||info.trackUrl.replace('{docket}','')}','_blank')\\\">Open</button>\u001b[39m\n\u001b[31m    </div>`).join('');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction openCourierMaster() {\u001b[39m\n\u001b[31m  const ed = document.getElementById('courierMasterEditor');\u001b[39m\n\u001b[31m  ed.innerHTML = Object.entries(COURIER_MASTER).map(([name, info]) => `\u001b[39m\n\u001b[31m    <div style=\\\"margin-bottom:14px;\\\">\u001b[39m\n\u001b[31m      <div style=\\\"font-family:var(--fnt-head);font-size:.75rem;font-weight:700;color:var(--t2);text-transform:uppercase;letter-spacing:1.5px;margin-bottom:6px;\\\">${name}</div>\u001b[39m\n\u001b[31m      <div class=\\\"f\\\" style=\\\"margin-bottom:6px;\\\">\u001b[39m\n\u001b[31m        <label>Track URL (use {docket})</label>\u001b[39m\n\u001b[31m        <input type=\\\"text\\\" value=\\\"${info.trackUrl}\\\" id=\\\"cm_${name.replace(/\\\\s/g,'_')}\\\"\u001b[39m\n\u001b[31m          style=\\\"font-family:var(--fnt-mono);font-size:.75rem;\\\">\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m    </div>`).join('') +\u001b[39m\n\u001b[31m    `<button class=\\\"btn btn-orange\\\" onclick=\\\"saveCourierMaster()\\\">Save Courier Master</button>`;\u001b[39m\n\u001b[31m  openOverlay('courierMasterOverlay');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction saveCourierMaster() {\u001b[39m\n\u001b[31m  Object.keys(COURIER_MASTER).forEach(name => {\u001b[39m\n\u001b[31m    const el = document.getElementById('cm_' + name.replace(/\\\\s/g,'_'));\u001b[39m\n\u001b[31m    if (el) COURIER_MASTER[name].trackUrl = el.value;\u001b[39m\n\u001b[31m  });\u001b[39m\n\u001b[31m  closeOverlay('courierMasterOverlay');\u001b[39m\n\u001b[31m  renderCourierMasterList();\u001b[39m\n\u001b[31m  showToast('Courier master saved ✓', 'ok');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31m// UI UTILS\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31m// ── ADMIN AUTH ───────────────────────────────────────────────────────────\u001b[39m\n\u001b[31mlet _adminAuthed = false;\u001b[39m\n\u001b[31mlet _lastAdminPane = null;·\u001b[39m\n\u001b[31mfunction showPane(name) {\u001b[39m\n\u001b[31m  // Config tab requires admin auth — re-ask if navigated away\u001b[39m\n\u001b[31m  if (name === 'config') {\u001b[39m\n\u001b[31m    if (!_adminAuthed) {\u001b[39m\n\u001b[31m      _lastAdminPane = 'config';\u001b[39m\n\u001b[31m      showAdminLogin();\u001b[39m\n\u001b[31m      return;\u001b[39m\n\u001b[31m    }\u001b[39m\n\u001b[31m  } else {\u001b[39m\n\u001b[31m    // User navigated away from config — revoke auth\u001b[39m\n\u001b[31m    if (_lastAdminPane === 'config') _adminAuthed = false;\u001b[39m\n\u001b[31m    _lastAdminPane = name;\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m  document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));\u001b[39m\n\u001b[31m  document.querySelectorAll('.pane').forEach(p => p.classList.remove('active'));\u001b[39m\n\u001b[31m  document.getElementById('tab-'+name).classList.add('active');\u001b[39m\n\u001b[31m  document.getElementById('pane-'+name).classList.add('active');\u001b[39m\n\u001b[31m  if (name === 'dashboard') renderDashboard();\u001b[39m\n\u001b[31m  if (name === 'history') renderHistory();\u001b[39m\n\u001b[31m  if (name === 'track') buildTrackDocketChips();\u001b[39m\n\u001b[31m  if (name === 'config') { document.getElementById('cfg_sheets_url').value = window.appConfig.sheets_url || ''; }\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction showAdminLogin() {\u001b[39m\n\u001b[31m  // Password replaced by hub identity — only harish/pramod can access config\u001b[39m\n\u001b[31m  if (['harish', 'pramod'].includes(window._hubUser?.id)) {\u001b[39m\n\u001b[31m    _adminAuthed = true;\u001b[39m\n\u001b[31m    showPane('config');\u001b[39m\n\u001b[31m  } else {\u001b[39m\n\u001b[31m    showToast('Config access restricted to admin users', 'err');\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31masync function submitAdminLogin() {\u001b[39m\n\u001b[31m  showAdminLogin(); // no-op shim — kept for any lingering HTML onclick refs\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31mfunction openOverlay(id) { document.getElementById(id).classList.add('open'); }\u001b[39m\n\u001b[31mfunction closeOverlay(id) { document.getElementById(id).classList.remove('open'); }·\u001b[39m\n\u001b[31mlet toastTimer;\u001b[39m\n\u001b[31mfunction showToast(msg, type='') {\u001b[39m\n\u001b[31m  const t = document.getElementById('toast');\u001b[39m\n\u001b[31m  t.textContent = msg;\u001b[39m\n\u001b[31m  t.className = 'toast show' + (type ? ' '+type : '');\u001b[39m\n\u001b[31m  clearTimeout(toastTimer);\u001b[39m\n\u001b[31m  toastTimer = setTimeout(() => t.classList.remove('show'), 2800);\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction showOCRWorking(show, msg='') {\u001b[39m\n\u001b[31m  const el = document.getElementById('ocrWorking');\u001b[39m\n\u001b[31m  el.classList.toggle('visible', show);\u001b[39m\n\u001b[31m  if (msg) document.getElementById('ocrWorkingText').textContent = msg;\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction showAIProgress(label, pct) {\u001b[39m\n\u001b[31m  const el = document.getElementById('aiProgress');\u001b[39m\n\u001b[31m  el.classList.add('visible');\u001b[39m\n\u001b[31m  document.getElementById('aiProgLabel').textContent = label;\u001b[39m\n\u001b[31m  document.getElementById('aiProgFill').style.width = pct + '%';\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31mfunction hideAIProgress() {\u001b[39m\n\u001b[31m  document.getElementById('aiProgress').classList.remove('visible');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction updateNetDot() {\u001b[39m\n\u001b[31m  document.getElementById('netDot').style.background = navigator.onLine ? 'var(--grn)' : 'var(--red)';\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction setToday() {\u001b[39m\n\u001b[31m  document.getElementById('n_date').value = new Date().toISOString().split('T')[0];\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m// ── EXPORT CLEAN SHEET ───────────────────────────────────────────────────\u001b[39m\n\u001b[31mfunction exportCleanSheet() {\u001b[39m\n\u001b[31m  const pane = document.getElementById('verifyPane');\u001b[39m\n\u001b[31m  const d = pane._pendingDispatch;\u001b[39m\n\u001b[31m  if (!d || !d.consignees?.length) { showToast('No data to export', 'err'); return; }·\u001b[39m\n\u001b[31m  // Build CSV\u001b[39m\n\u001b[31m  const headers = ['S.No.','Store Name','City','Pincode','Phone','Address','Docket No.','Material','Courier','Status'];\u001b[39m\n\u001b[31m  const rows = d.consignees.map((c, i) => [\u001b[39m\n\u001b[31m    i + 1,\u001b[39m\n\u001b[31m    `\\\"${(c.name||'').replace(/\\\"/g,'\\\"\\\"')}\\\"`,\u001b[39m\n\u001b[31m    `\\\"${(c.city||'').replace(/\\\"/g,'\\\"\\\"')}\\\"`,\u001b[39m\n\u001b[31m    c.pincode || '',\u001b[39m\n\u001b[31m    c.phone || '',\u001b[39m\n\u001b[31m    `\\\"${(c.address||'').replace(/\\\"/g,'\\\"\\\"')}\\\"`,\u001b[39m\n\u001b[31m    c.docket || '',\u001b[39m\n\u001b[31m    `\\\"${(c.material||'').replace(/\\\"/g,'\\\"\\\"')}\\\"`,\u001b[39m\n\u001b[31m    d.courier || '',\u001b[39m\n\u001b[31m    c.status || 'pending'\u001b[39m\n\u001b[31m  ]);·\u001b[39m\n\u001b[31m  const csv = [headers.join(','), ...rows.map(r => r.join(','))].join('\\\\n');\u001b[39m\n\u001b[31m  const blob = new Blob([csv], { type: 'text/csv' });\u001b[39m\n\u001b[31m  const url = URL.createObjectURL(blob);\u001b[39m\n\u001b[31m  const a = document.createElement('a');\u001b[39m\n\u001b[31m  a.href = url;\u001b[39m\n\u001b[31m  a.download = `360LM_Dispatch_${d.client||'export'}_${d.dispatchDate||new Date().toISOString().slice(0,10)}_clean.csv`;\u001b[39m\n\u001b[31m  a.click();\u001b[39m\n\u001b[31m  URL.revokeObjectURL(url);\u001b[39m\n\u001b[31m  showToast('Clean sheet exported ✓', 'ok');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// ── DOCKET SLIP UPLOAD — SKIP OCR IF DOCKET ALREADY MAPPED ───────────────\u001b[39m\n\u001b[31mfunction shouldSkipOCR() {\u001b[39m\n\u001b[31m  // If any consignee already has a docket (from store list mapping), skip OCR extraction\u001b[39m\n\u001b[31m  const consList = document.getElementById('consList');\u001b[39m\n\u001b[31m  if (!consList) return false;\u001b[39m\n\u001b[31m  const cards = consList.querySelectorAll('.cons-card');\u001b[39m\n\u001b[31m  let hasAnyDocket = false;\u001b[39m\n\u001b[31m  cards.forEach(card => {\u001b[39m\n\u001b[31m    const d = card.querySelector('.cons-docket-input')?.value?.trim();\u001b[39m\n\u001b[31m    if (d) hasAnyDocket = true;\u001b[39m\n\u001b[31m  });\u001b[39m\n\u001b[31m  return hasAnyDocket;\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// ─── TRACKING UPDATE TO SHEETS ──────────────────────────────────────────\u001b[39m\n\u001b[31masync function logTrackingUpdate(docket, status, lastEvent, location) {\u001b[39m\n\u001b[31m  const url = window.appConfig.webhook_url;\u001b[39m\n\u001b[31m  if (!url) return;\u001b[39m\n\u001b[31m  try {\u001b[39m\n\u001b[31m    await fetch(url, {\u001b[39m\n\u001b[31m      method: 'POST', mode: 'no-cors',\u001b[39m\n\u001b[31m      headers: { 'Content-Type': 'application/json' },\u001b[39m\n\u001b[31m      body: JSON.stringify({\u001b[39m\n\u001b[31m        type: 'tracking_update',\u001b[39m\n\u001b[31m        docket, status, lastEvent, location,\u001b[39m\n\u001b[31m        timestamp: new Date().toISOString()\u001b[39m\n\u001b[31m      })\u001b[39m\n\u001b[31m    });\u001b[39m\n\u001b[31m  } catch(e) {}\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// ─── POD / RECIPIENT PHOTO UPLOAD ───────────────────────────────────────\u001b[39m\n\u001b[31m// Stores base64 locally, pushes to Sheets webhook (Apps Script saves to Drive)\u001b[39m\n\u001b[31mfunction showPODUpload(docket, dispatchId) {\u001b[39m\n\u001b[31m  const overlay = document.getElementById('podOverlay');\u001b[39m\n\u001b[31m  overlay.dataset.docket = docket;\u001b[39m\n\u001b[31m  overlay.dataset.dispatchId = dispatchId;\u001b[39m\n\u001b[31m  document.getElementById('podPreview').innerHTML = '';\u001b[39m\n\u001b[31m  document.getElementById('podFileInput').value = '';\u001b[39m\n\u001b[31m  document.getElementById('recipientFileInput').value = '';\u001b[39m\n\u001b[31m  openOverlay('podOverlay');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31masync function uploadPODFiles() {\u001b[39m\n\u001b[31m  const overlay = document.getElementById('podOverlay');\u001b[39m\n\u001b[31m  const docket = overlay.dataset.docket;\u001b[39m\n\u001b[31m  const dispatchId = overlay.dataset.dispatchId;\u001b[39m\n\u001b[31m  const podFile = document.getElementById('podFileInput').files[0];\u001b[39m\n\u001b[31m  const recipFile = document.getElementById('recipientFileInput').files[0];\u001b[39m\n\u001b[31m  if (!podFile && !recipFile) { showToast('Select at least one photo', 'err'); return; }·\u001b[39m\n\u001b[31m  showToast('Uploading...', 'info');\u001b[39m\n\u001b[31m  const toBase64 = f => new Promise((res,rej) => {\u001b[39m\n\u001b[31m    const r = new FileReader(); r.onload = e => res(e.target.result); r.onerror = rej; r.readAsDataURL(f);\u001b[39m\n\u001b[31m  });·\u001b[39m\n\u001b[31m  const payload = {\u001b[39m\n\u001b[31m    type: 'pod_upload',\u001b[39m\n\u001b[31m    docket, dispatchId,\u001b[39m\n\u001b[31m    timestamp: new Date().toISOString()\u001b[39m\n\u001b[31m  };\u001b[39m\n\u001b[31m  if (podFile)   payload.pod_image = await toBase64(podFile);\u001b[39m\n\u001b[31m  if (recipFile) payload.recipient_image = await toBase64(recipFile);·\u001b[39m\n\u001b[31m  // Update local store\u001b[39m\n\u001b[31m  for (const d of dispatches) {\u001b[39m\n\u001b[31m    if (d.id !== dispatchId) continue;\u001b[39m\n\u001b[31m    for (const c of d.consignees) {\u001b[39m\n\u001b[31m      if (c.docket === docket) {\u001b[39m\n\u001b[31m        c.podUploaded = true;\u001b[39m\n\u001b[31m        c.status = 'delivered';\u001b[39m\n\u001b[31m      }\u001b[39m\n\u001b[31m    }\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m  saveData();·\u001b[39m\n\u001b[31m  // Push to Sheets (Apps Script saves images to Drive, stores URL in sheet)\u001b[39m\n\u001b[31m  if (window.appConfig.webhook_url) {\u001b[39m\n\u001b[31m    try {\u001b[39m\n\u001b[31m      await fetch(window.appConfig.webhook_url, {\u001b[39m\n\u001b[31m        method: 'POST', mode: 'no-cors',\u001b[39m\n\u001b[31m        headers: { 'Content-Type': 'application/json' },\u001b[39m\n\u001b[31m        body: JSON.stringify(payload)\u001b[39m\n\u001b[31m      });\u001b[39m\n\u001b[31m    } catch(e) {}\u001b[39m\n\u001b[31m  }·\u001b[39m\n\u001b[31m  closeOverlay('podOverlay');\u001b[39m\n\u001b[31m  renderDashboard();\u001b[39m\n\u001b[31m  showToast('POD uploaded ✓ — marked as delivered', 'ok');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// ─── WHATSAPP DISPATCH TABLE SHARE ──────────────────────────────────────\u001b[39m\n\u001b[31mfunction shareDispatchToWhatsApp(dispatchId) {\u001b[39m\n\u001b[31m  const d = dispatches.find(x => x.id === dispatchId);\u001b[39m\n\u001b[31m  if (!d) return;\u001b[39m\n\u001b[31m  const lines = [\u001b[39m\n\u001b[31m    `📦 *Dispatch Update — ${d.client} ${d.jobref ? '› '+d.jobref : ''}*`,\u001b[39m\n\u001b[31m    `🚚 Courier: ${d.courier} | Date: ${d.dispatchDate || d.createdAt?.slice(0,10)}`,\u001b[39m\n\u001b[31m    ``,\u001b[39m\n\u001b[31m    ...d.consignees.filter(c => c.docket).map((c,i) =>\u001b[39m\n\u001b[31m      `${i+1}. *${c.name}* — ${c.city}\\\\n   Docket: ${c.docket} | Status: ${c.status||'booked'}`\u001b[39m\n\u001b[31m    ),\u001b[39m\n\u001b[31m    ``,\u001b[39m\n\u001b[31m    `_360 Degree Logical Marketing_`\u001b[39m\n\u001b[31m  ];\u001b[39m\n\u001b[31m  const msg = encodeURIComponent(lines.join('\\\\n'));\u001b[39m\n\u001b[31m  window.open('https://wa.me/?text=' + msg, '_blank');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// Trigger n8n webhook for automated WhatsApp/email share\u001b[39m\n\u001b[31masync function triggerN8nShare(dispatchId, channel) {\u001b[39m\n\u001b[31m  const d = dispatches.find(x => x.id === dispatchId);\u001b[39m\n\u001b[31m  if (!d) return;\u001b[39m\n\u001b[31m  const n8nUrl = window.appConfig.n8n_dispatch_webhook;\u001b[39m\n\u001b[31m  if (!n8nUrl) { showToast('n8n webhook not configured in Config sheet', 'err'); return; }\u001b[39m\n\u001b[31m  try {\u001b[39m\n\u001b[31m    await fetch(n8nUrl, {\u001b[39m\n\u001b[31m      method: 'POST',\u001b[39m\n\u001b[31m      headers: { 'Content-Type': 'application/json' },\u001b[39m\n\u001b[31m      body: JSON.stringify({\u001b[39m\n\u001b[31m        channel, // 'whatsapp' or 'email'\u001b[39m\n\u001b[31m        dispatch: d,\u001b[39m\n\u001b[31m        timestamp: new Date().toISOString()\u001b[39m\n\u001b[31m      })\u001b[39m\n\u001b[31m    });\u001b[39m\n\u001b[31m    showToast(`Sent to n8n for ${channel} delivery ✓`, 'ok');\u001b[39m\n\u001b[31m  } catch(e) {\u001b[39m\n\u001b[31m    showToast('n8n webhook failed', 'err');\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction previewPOD(input, type) {\u001b[39m\n\u001b[31m  const f = input.files[0];\u001b[39m\n\u001b[31m  if (!f) return;\u001b[39m\n\u001b[31m  const r = new FileReader();\u001b[39m\n\u001b[31m  r.onload = e => {\u001b[39m\n\u001b[31m    const prev = document.getElementById('podPreview');\u001b[39m\n\u001b[31m    const existing = document.getElementById('pod_prev_' + type);\u001b[39m\n\u001b[31m    if (existing) existing.remove();\u001b[39m\n\u001b[31m    const img = document.createElement('img');\u001b[39m\n\u001b[31m    img.id = 'pod_prev_' + type;\u001b[39m\n\u001b[31m    img.src = e.target.result;\u001b[39m\n\u001b[31m    img.style.cssText = 'width:80px;height:80px;object-fit:cover;border-radius:6px;border:1px solid var(--bdr);';\u001b[39m\n\u001b[31m    prev.appendChild(img);\u001b[39m\n\u001b[31m  };\u001b[39m\n\u001b[31m  r.readAsDataURL(f);\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// ─── MANUAL TRACKING ─────────────────────────────────────────────────────\u001b[39m\n\u001b[31mfunction showManualTracking(docket, dispatchId, storeName, phone, city) {\u001b[39m\n\u001b[31m  const ov = document.getElementById('manualTrackOverlay');\u001b[39m\n\u001b[31m  ov.dataset.docket = docket;\u001b[39m\n\u001b[31m  ov.dataset.dispatchId = dispatchId;\u001b[39m\n\u001b[31m  ov.dataset.storeName = storeName;\u001b[39m\n\u001b[31m  ov.dataset.city = city;·\u001b[39m\n\u001b[31m  document.getElementById('mtDocketDisplay').textContent =\u001b[39m\n\u001b[31m    docket + '  ·  ' + storeName + '  ·  ' + city;\u001b[39m\n\u001b[31m  document.getElementById('mtContactUsed').value = phone || '';\u001b[39m\n\u001b[31m  document.getElementById('mtContactPerson').value = '';\u001b[39m\n\u001b[31m  document.getElementById('mtRemarks').value = '';\u001b[39m\n\u001b[31m  document.getElementById('mtStatus').value = '';\u001b[39m\n\u001b[31m  document.getElementById('mtCalledBy').value = '';·\u001b[39m\n\u001b[31m  // Set current datetime\u001b[39m\n\u001b[31m  const now = new Date();\u001b[39m\n\u001b[31m  const pad = n => String(n).padStart(2,'0');\u001b[39m\n\u001b[31m  document.getElementById('mtCallTime').value =\u001b[39m\n\u001b[31m    `${now.getFullYear()}-${pad(now.getMonth()+1)}-${pad(now.getDate())}T${pad(now.getHours())}:${pad(now.getMinutes())}`;·\u001b[39m\n\u001b[31m  openOverlay('manualTrackOverlay');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31masync function submitManualTracking() {\u001b[39m\n\u001b[31m  const ov = document.getElementById('manualTrackOverlay');\u001b[39m\n\u001b[31m  const docket     = ov.dataset.docket;\u001b[39m\n\u001b[31m  const dispatchId = ov.dataset.dispatchId;\u001b[39m\n\u001b[31m  const status     = document.getElementById('mtStatus').value;\u001b[39m\n\u001b[31m  const calledBy   = document.getElementById('mtCalledBy').value.trim();·\u001b[39m\n\u001b[31m  if (!status)    { showToast('Select a status', 'err'); return; }\u001b[39m\n\u001b[31m  if (!calledBy)  { showToast('Enter your name', 'err'); return; }·\u001b[39m\n\u001b[31m  const payload = {\u001b[39m\n\u001b[31m    type:          'manual_tracking',\u001b[39m\n\u001b[31m    docket,        dispatchId,\u001b[39m\n\u001b[31m    storeName:     ov.dataset.storeName,\u001b[39m\n\u001b[31m    city:          ov.dataset.city,\u001b[39m\n\u001b[31m    manualStatus:  status,\u001b[39m\n\u001b[31m    calledBy,\u001b[39m\n\u001b[31m    contactUsed:   document.getElementById('mtContactUsed').value.trim(),\u001b[39m\n\u001b[31m    contactPerson: document.getElementById('mtContactPerson').value.trim(),\u001b[39m\n\u001b[31m    remarks:       document.getElementById('mtRemarks').value.trim(),\u001b[39m\n\u001b[31m    callTime:      document.getElementById('mtCallTime').value || new Date().toISOString(),\u001b[39m\n\u001b[31m    timestamp:     new Date().toISOString()\u001b[39m\n\u001b[31m  };·\u001b[39m\n\u001b[31m  // Update local dispatch record\u001b[39m\n\u001b[31m  for (const d of dispatches) {\u001b[39m\n\u001b[31m    if (d.id !== dispatchId) continue;\u001b[39m\n\u001b[31m    for (const c of d.consignees) {\u001b[39m\n\u001b[31m      if (c.docket === docket) {\u001b[39m\n\u001b[31m        c.manualStatus    = status;\u001b[39m\n\u001b[31m        c.manualCalledBy  = calledBy;\u001b[39m\n\u001b[31m        c.manualTime      = payload.callTime;\u001b[39m\n\u001b[31m        c.manualRemarks   = payload.remarks;\u001b[39m\n\u001b[31m        // If confirmed delivered — flag so dashboard shows manual badge\u001b[39m\n\u001b[31m        // (not permanently locked — use resumeAutoTracking() to re-enable)\u001b[39m\n\u001b[31m        if (status === 'delivered') {\u001b[39m\n\u001b[31m          c.status = 'delivered';\u001b[39m\n\u001b[31m          c.manualDelivered = true;\u001b[39m\n\u001b[31m        } else {\u001b[39m\n\u001b[31m          // Non-delivered manual update: clear any previous lock\u001b[39m\n\u001b[31m          c.manualDelivered = false;\u001b[39m\n\u001b[31m        }\u001b[39m\n\u001b[31m      }\u001b[39m\n\u001b[31m    }\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m  saveData();\u001b[39m\n\u001b[31m  renderDashboard();\u001b[39m\n\u001b[31m  // Sync updated consignee to Postgres\u001b[39m\n\u001b[31m  const _d = dispatches.find(x => x.id === dispatchId);\u001b[39m\n\u001b[31m  if (_d) {\u001b[39m\n\u001b[31m    const _idx = _d.consignees.findIndex(c => c.docket === docket);\u001b[39m\n\u001b[31m    if (_idx >= 0) syncConsigneeToDB(_d.consignees[_idx], dispatchId, _idx);\u001b[39m\n\u001b[31m  }·\u001b[39m\n\u001b[31m  // Log to Sheets\u001b[39m\n\u001b[31m  if (window.appConfig.webhook_url) {\u001b[39m\n\u001b[31m    try {\u001b[39m\n\u001b[31m      await fetch(window.appConfig.webhook_url, {\u001b[39m\n\u001b[31m        method: 'POST', mode: 'no-cors',\u001b[39m\n\u001b[31m        headers: { 'Content-Type': 'application/json' },\u001b[39m\n\u001b[31m        body: JSON.stringify(payload)\u001b[39m\n\u001b[31m      });\u001b[39m\n\u001b[31m    } catch(e) {}\u001b[39m\n\u001b[31m  }·\u001b[39m\n\u001b[31m  closeOverlay('manualTrackOverlay');\u001b[39m\n\u001b[31m  showToast('Manual tracking logged ✓', 'ok');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// ─── TILE SELECTION + PARTIAL REPORT SHARE ───────────────────────────────\u001b[39m\n\u001b[31mlet _selectedDockets = new Set();·\u001b[39m\n\u001b[31mfunction toggleTileSelect(docket) {\u001b[39m\n\u001b[31m  if (_selectedDockets.has(docket)) _selectedDockets.delete(docket);\u001b[39m\n\u001b[31m  else _selectedDockets.add(docket);\u001b[39m\n\u001b[31m  // Update checkbox visual\u001b[39m\n\u001b[31m  const chk = document.getElementById('sel_' + docket.replace(/[^a-z0-9]/gi,'_'));\u001b[39m\n\u001b[31m  if (chk) chk.checked = _selectedDockets.has(docket);\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction shareSelectedReport() {\u001b[39m\n\u001b[31m  if (_selectedDockets.size === 0) {\u001b[39m\n\u001b[31m    showToast('Select at least one consignee tile first', 'err');\u001b[39m\n\u001b[31m    return;\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m  const activeDis = dispatches.filter(d => d.status !== 'draft');\u001b[39m\n\u001b[31m  const allItems  = activeDis.flatMap(d =>\u001b[39m\n\u001b[31m    (d.consignees||[]).map(c => Object.assign({}, c, {courier:d.courier, client:d.client, jobref:d.jobref, dispatchId:d.id}))\u001b[39m\n\u001b[31m  );\u001b[39m\n\u001b[31m  const selected = allItems.filter(c => _selectedDockets.has(c.docket));\u001b[39m\n\u001b[31m  if (!selected.length) { showToast('Selected dockets not found', 'err'); return; }·\u001b[39m\n\u001b[31m  const NL = '\\\\n';\u001b[39m\n\u001b[31m  const statusEmoji = {\u001b[39m\n\u001b[31m    delivered:'\\\\u2705', transit:'\\\\uD83D\\\\uDE9A', 'in-transit':'\\\\uD83D\\\\uDE9A',\u001b[39m\n\u001b[31m    booked:'\\\\uD83D\\\\uDCEC', pending:'\\\\u23F3', 'not received':'\\\\u274C', forwarded:'\\\\u27A1'\u001b[39m\n\u001b[31m  };·\u001b[39m\n\u001b[31m  const header = '\\\\uD83D\\\\uDCE6 *Dispatch Status Report*' + NL +\u001b[39m\n\u001b[31m    (selected[0].client || '') + ' ' + (selected[0].jobref ? '> ' + selected[0].jobref : '') + NL +\u001b[39m\n\u001b[31m    new Date().toLocaleDateString('en-IN', {day:'2-digit',month:'short',year:'numeric'}) + NL + NL;·\u001b[39m\n\u001b[31m  const rows = selected.map(function(c, i) {\u001b[39m\n\u001b[31m    const finalStatus = c.manualStatus === 'delivered' ? 'delivered' : (c.status || 'booked');\u001b[39m\n\u001b[31m    const emoji  = statusEmoji[finalStatus] || '\\\\uD83D\\\\uDCE6';\u001b[39m\n\u001b[31m    const source = c.manualStatus ? '(manual)' : '(auto)';\u001b[39m\n\u001b[31m    const remark = c.manualRemarks ? (NL + '   Note: ' + c.manualRemarks) : '';\u001b[39m\n\u001b[31m    return (i+1) + '. *' + c.name + '* - ' + c.city + NL +\u001b[39m\n\u001b[31m      '   ' + emoji + ' ' + finalStatus + ' ' + source + NL +\u001b[39m\n\u001b[31m      '   Docket: ' + c.docket + remark;\u001b[39m\n\u001b[31m  }).join(NL + NL);·\u001b[39m\n\u001b[31m  const footer = NL + NL + '_360 Degree Logical Marketing_';\u001b[39m\n\u001b[31m  window.open('https://wa.me/?text=' + encodeURIComponent(header + rows + footer), '_blank');\u001b[39m\n\u001b[31m  showToast('Report for ' + selected.length + ' consignees ready', 'ok');\u001b[39m\n\u001b[31m}··\u001b[39m\n\u001b[31m// ── resumeAutoTracking: undo a manual-delivered lock ──────────────────────\u001b[39m\n\u001b[31mfunction resumeAutoTracking(docket, dispatchId) {\u001b[39m\n\u001b[31m  for (const d of dispatches) {\u001b[39m\n\u001b[31m    if (d.id !== dispatchId) continue;\u001b[39m\n\u001b[31m    for (const c of d.consignees) {\u001b[39m\n\u001b[31m      if (c.docket === docket) {\u001b[39m\n\u001b[31m        c.manualDelivered = false;\u001b[39m\n\u001b[31m        c.status          = 'transit';\u001b[39m\n\u001b[31m        c.manualStatus    = '';\u001b[39m\n\u001b[31m      }\u001b[39m\n\u001b[31m    }\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m  saveData();\u001b[39m\n\u001b[31m  renderDashboard();\u001b[39m\n\u001b[31m  showToast(`Auto-tracking resumed for ${docket}`, 'ok');\u001b[39m\n\u001b[31m  const _rd = dispatches.find(x => x.id === dispatchId);\u001b[39m\n\u001b[31m  if (_rd) {\u001b[39m\n\u001b[31m    const _ri = _rd.consignees.findIndex(c => c.docket === docket);\u001b[39m\n\u001b[31m    if (_ri >= 0) syncConsigneeToDB(_rd.consignees[_ri], dispatchId, _ri);\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// ── refreshAllTracking (consolidated, try/finally for safety) ─────────────\u001b[39m\n\u001b[31masync function refreshAllTracking() {\u001b[39m\n\u001b[31m  const activeDis  = dispatches.filter(d => d.status !== 'draft');\u001b[39m\n\u001b[31m  const allCons    = activeDis.flatMap(d =>\u001b[39m\n\u001b[31m    (d.consignees || []).map(c => ({ ...c, dispatchId: d.id, courier: d.courier }))\u001b[39m\n\u001b[31m  );\u001b[39m\n\u001b[31m  const withDockets = allCons.filter(c =>\u001b[39m\n\u001b[31m    c.docket && c.status !== 'delivered' && !c.manualDelivered\u001b[39m\n\u001b[31m  );\u001b[39m\n\u001b[31m  if (!withDockets.length) { showToast('No active dockets to refresh', 'info'); return; }·\u001b[39m\n\u001b[31m  showToast(`⏳ Tracking ${withDockets.length} dockets...`, 'info');\u001b[39m\n\u001b[31m  withDockets.forEach(c => {\u001b[39m\n\u001b[31m    const id    = c.docket.replace(/[^a-z0-9]/gi, '_');\u001b[39m\n\u001b[31m    const evtEl = document.getElementById('evt_'  + id);\u001b[39m\n\u001b[31m    const spnEl = document.getElementById('spin_' + id);\u001b[39m\n\u001b[31m    if (evtEl) evtEl.style.display = 'none';\u001b[39m\n\u001b[31m    if (spnEl) spnEl.style.display = 'inline';\u001b[39m\n\u001b[31m  });·\u001b[39m\n\u001b[31m  let done = 0;\u001b[39m\n\u001b[31m  try {\u001b[39m\n\u001b[31m    for (const c of withDockets) {\u001b[39m\n\u001b[31m      const result = await fetchTracking(c.docket, c.courier || '');\u001b[39m\n\u001b[31m      const id    = c.docket.replace(/[^a-z0-9]/gi, '_');·\u001b[39m\n\u001b[31m      // Batch-write result into state (never touch DOM here)\u001b[39m\n\u001b[31m      if (result.status && result.status !== 'failed') {\u001b[39m\n\u001b[31m        for (const d of dispatches) {\u001b[39m\n\u001b[31m          if (d.id !== c.dispatchId) continue;\u001b[39m\n\u001b[31m          for (const con of d.consignees) {\u001b[39m\n\u001b[31m            if (con.docket === c.docket && !con.manualDelivered) {\u001b[39m\n\u001b[31m              con.status           = result.status;\u001b[39m\n\u001b[31m              con.lastEvent        = result.lastEvent || con.lastEvent;\u001b[39m\n\u001b[31m              con.lastScanLocation = result.lastScanLocation || '';\u001b[39m\n\u001b[31m              con.lastTrackedAt    = new Date().toISOString();\u001b[39m\n\u001b[31m            }\u001b[39m\n\u001b[31m          }\u001b[39m\n\u001b[31m        }\u001b[39m\n\u001b[31m      }·\u001b[39m\n\u001b[31m      // Update tile spinner/event text in DOM (cosmetic only)\u001b[39m\n\u001b[31m      const spnEl = document.getElementById('spin_' + id);\u001b[39m\n\u001b[31m      const evtEl = document.getElementById('evt_'  + id);\u001b[39m\n\u001b[31m      if (spnEl) spnEl.style.display = 'none';\u001b[39m\n\u001b[31m      if (evtEl) { evtEl.style.display = 'inline'; evtEl.textContent = result.lastEvent || 'Checked'; }·\u001b[39m\n\u001b[31m      if (result.status && result.status !== 'failed' && window.appConfig.webhook_url) {\u001b[39m\n\u001b[31m        logTrackingUpdate(c.docket, result.status, result.lastEvent || '', result.lastScanLocation || '');\u001b[39m\n\u001b[31m      }\u001b[39m\n\u001b[31m      done++;\u001b[39m\n\u001b[31m    }\u001b[39m\n\u001b[31m    showToast(`✓ ${done} dockets refreshed`, 'ok');\u001b[39m\n\u001b[31m  } finally {\u001b[39m\n\u001b[31m    // Always persist and re-render — even if loop throws mid-way\u001b[39m\n\u001b[31m    saveData();\u001b[39m\n\u001b[31m    renderDashboard();\u001b[39m\n\u001b[31m    // Push updated consignee states to Postgres (fire-and-forget)\u001b[39m\n\u001b[31m    dispatches.filter(d => d.status !== 'draft').forEach(d => syncDispatchToDB(d));\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m</script>\u001b[39m\n\u001b[31m</body>\u001b[39m\n\u001b[31m</html>\"\u001b[39m\n\n  51 |\n  52 |   test('hub auth gate present', () => {\n> 53 |     expect(html).toContain(\"window.location.replace('/hub/')\");\n     |                  ^\n  54 |     expect(html).toContain('_hubUser');\n  55 |   });\n  56 | });\n    at /var/www/360lm/tests/dispatch.spec.js:53:18"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 0,
                      "startTime": "2026-07-12T13:17:41.449Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/dispatch-Dispatch-—-HTML-Structure-hub-auth-gate-present-desktop-chrome/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/dispatch.spec.js",
                        "column": 18,
                        "line": 53
                      }
                    },
                    {
                      "workerIndex": 177,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 31,
                      "error": {
                        "message": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoContain\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m) // indexOf\u001b[22m\n\nExpected substring: \u001b[32m\"window.location.replace('/hub/')\"\u001b[39m\nReceived string:    \u001b[31m\"<!DOCTYPE html>\u001b[39m\n\u001b[31m<html lang=\\\"en\\\">\u001b[39m\n\u001b[31m<head>\u001b[39m\n\u001b[31m<meta charset=\\\"UTF-8\\\">\u001b[39m\n\u001b[31m<meta name=\\\"viewport\\\" content=\\\"width=device-width,initial-scale=1,maximum-scale=1,user-scalable=no\\\">\u001b[39m\n\u001b[31m<meta name=\\\"theme-color\\\" content=\\\"#080c12\\\">\u001b[39m\n\u001b[31m<meta name=\\\"apple-mobile-web-app-capable\\\" content=\\\"yes\\\">\u001b[39m\n\u001b[31m<meta name=\\\"mobile-web-app-capable\\\" content=\\\"yes\\\">\u001b[39m\n\u001b[31m<title>360LM Dispatch</title>\u001b[39m\n\u001b[31m<link rel=\\\"manifest\\\" href=\\\"manifest.json\\\">\u001b[39m\n\u001b[31m<link href=\\\"https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@300;400;500;600;700&family=Fira+Code:wght@400;500&family=Barlow+Condensed:wght@400;600;700;800&display=swap\\\" rel=\\\"stylesheet\\\">\u001b[39m\n\u001b[31m<style>\u001b[39m\n\u001b[31m/* ════ RESET & VARS ════ */\u001b[39m\n\u001b[31m*{margin:0;padding:0;box-sizing:border-box;-webkit-tap-highlight-color:transparent;}\u001b[39m\n\u001b[31m:root{\u001b[39m\n\u001b[31m  --bg:#080c12; --bg2:#0d1420; --bg3:#111a28;\u001b[39m\n\u001b[31m  --sur:#141f30; --sur2:#1a2840; --sur3:#1f3050;\u001b[39m\n\u001b[31m  --bdr:#1e2e45; --bdr2:#253650;\u001b[39m\n\u001b[31m  --t1:#f0f4f8; --t2:#8ba4bc; --t3:#3d5470; --t4:#243348;\u001b[39m\n\u001b[31m  --acc:#f97316; --acc2:#fb923c; --acc-glow:rgba(249,115,22,0.25);\u001b[39m\n\u001b[31m  --grn:#22c55e; --grn-dim:rgba(34,197,94,0.15);\u001b[39m\n\u001b[31m  --sky:#38bdf8; --sky-dim:rgba(56,189,248,0.15);\u001b[39m\n\u001b[31m  --red:#ef4444; --red-dim:rgba(239,68,68,0.15);\u001b[39m\n\u001b[31m  --ylw:#eab308; --ylw-dim:rgba(234,179,8,0.15);\u001b[39m\n\u001b[31m  --fnt-head:'Barlow Condensed',sans-serif;\u001b[39m\n\u001b[31m  --fnt-body:'Space Grotesk',sans-serif;\u001b[39m\n\u001b[31m  --fnt-mono:'Fira Code',monospace;\u001b[39m\n\u001b[31m  --r:10px; --r-sm:6px; --r-lg:16px;\u001b[39m\n\u001b[31m  --sh:0 4px 24px rgba(0,0,0,0.4);\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31mhtml,body{height:100%;background:var(--bg);color:var(--t1);font-family:var(--fnt-body);overflow:hidden;}·\u001b[39m\n\u001b[31m/* ════ SPLASH ════ */\u001b[39m\n\u001b[31m#splash{\u001b[39m\n\u001b[31m  position:fixed;inset:0;background:var(--bg);z-index:999;\u001b[39m\n\u001b[31m  display:flex;flex-direction:column;align-items:center;justify-content:center;\u001b[39m\n\u001b[31m  transition:opacity .4s .1s;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.splash-logo{font-family:var(--fnt-head);font-size:3rem;font-weight:800;letter-spacing:2px;line-height:1;}\u001b[39m\n\u001b[31m.splash-logo em{color:var(--acc);font-style:normal;}\u001b[39m\n\u001b[31m.splash-sub{color:var(--t3);font-family:var(--fnt-mono);font-size:.7rem;letter-spacing:4px;margin-top:8px;}\u001b[39m\n\u001b[31m.splash-track{width:200px;height:2px;background:var(--bdr);border-radius:2px;margin-top:40px;overflow:hidden;}\u001b[39m\n\u001b[31m.splash-track-fill{height:100%;width:0;background:linear-gradient(90deg,var(--acc),var(--acc2));animation:splashLoad 1.3s cubic-bezier(.4,0,.2,1) forwards;}\u001b[39m\n\u001b[31m@keyframes splashLoad{to{width:100%;}}·\u001b[39m\n\u001b[31m/* ════ LAYOUT ════ */\u001b[39m\n\u001b[31m#app{height:100%;display:flex;flex-direction:column;}\u001b[39m\n\u001b[31m.hdr{\u001b[39m\n\u001b[31m  background:var(--bg2);border-bottom:1px solid var(--bdr);\u001b[39m\n\u001b[31m  padding:10px 14px;display:flex;align-items:center;gap:10px;flex-shrink:0;\u001b[39m\n\u001b[31m  position:relative;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.hdr-brand{font-family:var(--fnt-head);font-size:1.3rem;font-weight:800;letter-spacing:1px;flex:1;}\u001b[39m\n\u001b[31m.hdr-brand em{color:var(--acc);font-style:normal;}\u001b[39m\n\u001b[31m.hdr-dot{width:7px;height:7px;border-radius:50%;background:var(--grn);transition:background .3s;animation:blink 2s infinite;}\u001b[39m\n\u001b[31m@keyframes blink{0%,100%{opacity:1;}50%{opacity:.4;}}\u001b[39m\n\u001b[31m.hdr-btn{\u001b[39m\n\u001b[31m  background:none;border:none;color:var(--t2);cursor:pointer;\u001b[39m\n\u001b[31m  padding:6px 8px;border-radius:var(--r-sm);transition:all .2s;\u001b[39m\n\u001b[31m  font-family:var(--fnt-head);font-size:.85rem;font-weight:700;letter-spacing:.5px;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.hdr-btn:hover{background:var(--bdr);color:var(--t1);}\u001b[39m\n\u001b[31m.hdr-btn.primary{background:var(--acc);color:#fff;padding:6px 14px;}\u001b[39m\n\u001b[31m.hdr-btn.primary:hover{background:var(--acc2);}·\u001b[39m\n\u001b[31m/* ════ TAB BAR ════ */\u001b[39m\n\u001b[31m.tabs{\u001b[39m\n\u001b[31m  display:flex;background:var(--bg2);border-bottom:1px solid var(--bdr);\u001b[39m\n\u001b[31m  overflow-x:auto;flex-shrink:0;padding:0 6px;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.tabs::-webkit-scrollbar{display:none;}\u001b[39m\n\u001b[31m.tab{\u001b[39m\n\u001b[31m  background:none;border:none;color:var(--t3);font-family:var(--fnt-head);\u001b[39m\n\u001b[31m  font-size:.85rem;font-weight:700;letter-spacing:1px;padding:10px 14px;\u001b[39m\n\u001b[31m  cursor:pointer;border-bottom:2px solid transparent;white-space:nowrap;\u001b[39m\n\u001b[31m  transition:all .2s;flex-shrink:0;display:flex;align-items:center;gap:6px;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.tab.active{color:var(--acc);border-bottom-color:var(--acc);}\u001b[39m\n\u001b[31m.tab:hover:not(.active){color:var(--t2);}\u001b[39m\n\u001b[31m.tab-badge{\u001b[39m\n\u001b[31m  background:var(--acc);color:#fff;font-size:.6rem;font-weight:700;\u001b[39m\n\u001b[31m  padding:1px 5px;border-radius:10px;font-family:var(--fnt-mono);\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m/* ════ CONTENT AREAS ════ */\u001b[39m\n\u001b[31m.pane{flex:1;overflow-y:auto;padding:14px;display:none;}\u001b[39m\n\u001b[31m.pane.active{display:block;}\u001b[39m\n\u001b[31m.pane::-webkit-scrollbar{width:3px;}\u001b[39m\n\u001b[31m.pane::-webkit-scrollbar-thumb{background:var(--bdr2);}·\u001b[39m\n\u001b[31m/* ════ CARDS ════ */\u001b[39m\n\u001b[31m.card{\u001b[39m\n\u001b[31m  background:var(--sur);border:1px solid var(--bdr);border-radius:var(--r);\u001b[39m\n\u001b[31m  padding:14px;margin-bottom:12px;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.card.accent-left{border-left:3px solid var(--acc);}\u001b[39m\n\u001b[31m.card-title{\u001b[39m\n\u001b[31m  font-family:var(--fnt-head);font-size:.72rem;font-weight:700;\u001b[39m\n\u001b[31m  color:var(--t3);text-transform:uppercase;letter-spacing:2.5px;\u001b[39m\n\u001b[31m  margin-bottom:12px;display:flex;align-items:center;gap:8px;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.card-title .ct-dot{width:5px;height:5px;border-radius:50%;background:var(--acc);flex-shrink:0;}·\u001b[39m\n\u001b[31m/* ════ FORM ════ */\u001b[39m\n\u001b[31m.f{margin-bottom:12px;}\u001b[39m\n\u001b[31m.f label{\u001b[39m\n\u001b[31m  display:block;font-size:.68rem;color:var(--t3);margin-bottom:5px;\u001b[39m\n\u001b[31m  font-weight:600;text-transform:uppercase;letter-spacing:1.5px;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.f label .req{color:var(--acc);margin-left:2px;}\u001b[39m\n\u001b[31m.f input,.f select,.f textarea{\u001b[39m\n\u001b[31m  width:100%;background:var(--sur2);border:1px solid var(--bdr);\u001b[39m\n\u001b[31m  border-radius:var(--r-sm);color:var(--t1);font-family:var(--fnt-body);\u001b[39m\n\u001b[31m  font-size:.88rem;padding:9px 11px;outline:none;transition:border-color .2s;\u001b[39m\n\u001b[31m  -webkit-appearance:none;appearance:none;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.f input:focus,.f select:focus,.f textarea:focus{border-color:var(--acc);}\u001b[39m\n\u001b[31m.f input::placeholder{color:var(--t4);}\u001b[39m\n\u001b[31m.f textarea{resize:vertical;min-height:65px;}\u001b[39m\n\u001b[31m.f select{\u001b[39m\n\u001b[31m  background-image:url(\\\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='7'%3E%3Cpath d='M1 1l4 4 4-4' stroke='%233d5470' stroke-width='1.5' fill='none' stroke-linecap='round'/%3E%3C/svg%3E\\\");\u001b[39m\n\u001b[31m  background-repeat:no-repeat;background-position:right 10px center;padding-right:28px;cursor:pointer;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.row2{display:grid;grid-template-columns:1fr 1fr;gap:10px;}\u001b[39m\n\u001b[31m.row3{display:grid;grid-template-columns:1fr 1fr 1fr;gap:8px;}·\u001b[39m\n\u001b[31m/* ════ BUTTONS ════ */\u001b[39m\n\u001b[31m.btn{\u001b[39m\n\u001b[31m  width:100%;padding:12px;border:none;border-radius:var(--r);\u001b[39m\n\u001b[31m  font-family:var(--fnt-head);font-size:.9rem;font-weight:700;letter-spacing:.5px;\u001b[39m\n\u001b[31m  cursor:pointer;transition:all .2s;display:flex;align-items:center;justify-content:center;gap:8px;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.btn-orange{background:var(--acc);color:#fff;}\u001b[39m\n\u001b[31m.btn-orange:hover{background:var(--acc2);}\u001b[39m\n\u001b[31m.btn-ghost{background:var(--sur2);border:1px solid var(--bdr);color:var(--t1);}\u001b[39m\n\u001b[31m.btn-ghost:hover{border-color:var(--acc);color:var(--acc);}\u001b[39m\n\u001b[31m.btn-grn{background:var(--grn);color:#000;}\u001b[39m\n\u001b[31m.btn-grn:hover{opacity:.85;}\u001b[39m\n\u001b[31m.btn:active{transform:scale(.98);}\u001b[39m\n\u001b[31m.btn-row{display:grid;grid-template-columns:1fr 1fr;gap:10px;margin-top:6px;}\u001b[39m\n\u001b[31m.btn-sm{\u001b[39m\n\u001b[31m  background:none;border:1px solid var(--bdr);color:var(--t2);\u001b[39m\n\u001b[31m  font-size:.7rem;padding:5px 10px;border-radius:var(--r-sm);cursor:pointer;\u001b[39m\n\u001b[31m  font-family:var(--fnt-body);transition:all .2s;white-space:nowrap;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.btn-sm:hover{border-color:var(--acc);color:var(--acc);}\u001b[39m\n\u001b[31m.btn-sm.primary{background:var(--acc);border-color:var(--acc);color:#fff;}·\u001b[39m\n\u001b[31m/* ════ OCR UPLOAD ZONE ════ */\u001b[39m\n\u001b[31m.upload-zone{\u001b[39m\n\u001b[31m  border:2px dashed var(--bdr2);border-radius:var(--r);\u001b[39m\n\u001b[31m  padding:20px;text-align:center;cursor:pointer;transition:all .25s;\u001b[39m\n\u001b[31m  background:var(--sur2);position:relative;overflow:hidden;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.upload-zone:hover,.upload-zone.drag{border-color:var(--acc);background:rgba(249,115,22,.05);}\u001b[39m\n\u001b[31m.upload-zone .uz-icon{font-size:2rem;margin-bottom:8px;}\u001b[39m\n\u001b[31m.upload-zone .uz-title{font-family:var(--fnt-head);font-size:1rem;font-weight:700;color:var(--t1);}\u001b[39m\n\u001b[31m.upload-zone .uz-sub{font-size:.72rem;color:var(--t3);margin-top:4px;font-family:var(--fnt-mono);}\u001b[39m\n\u001b[31m.upload-zone input{display:none;}\u001b[39m\n\u001b[31m.upload-zone.share-target{border-color:var(--grn);}·\u001b[39m\n\u001b[31m/* ════ OCR RESULTS ════ */\u001b[39m\n\u001b[31m.ocr-results{display:none;}\u001b[39m\n\u001b[31m.ocr-results.visible{display:block;}\u001b[39m\n\u001b[31m.ocr-slip-list{display:flex;flex-direction:column;gap:8px;margin-top:10px;}\u001b[39m\n\u001b[31m.ocr-slip{\u001b[39m\n\u001b[31m  background:var(--sur2);border:1px solid var(--bdr);border-radius:var(--r-sm);\u001b[39m\n\u001b[31m  padding:10px 12px;display:flex;gap:10px;align-items:flex-start;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.ocr-slip img{width:60px;height:60px;object-fit:cover;border-radius:6px;flex-shrink:0;border:1px solid var(--bdr2);}\u001b[39m\n\u001b[31m.ocr-slip-body{flex:1;min-width:0;}\u001b[39m\n\u001b[31m.ocr-slip-name{font-size:.7rem;color:var(--t3);font-family:var(--fnt-mono);margin-bottom:4px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}\u001b[39m\n\u001b[31m.ocr-slip-docket{\u001b[39m\n\u001b[31m  font-family:var(--fnt-mono);font-size:.9rem;font-weight:500;color:var(--acc);\u001b[39m\n\u001b[31m  background:var(--bg);border:1px solid var(--bdr);border-radius:4px;\u001b[39m\n\u001b[31m  padding:4px 8px;width:100%;outline:none;transition:border-color .2s;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.ocr-slip-docket:focus{border-color:var(--acc);}\u001b[39m\n\u001b[31m.ocr-slip-status{font-size:.65rem;margin-top:4px;display:flex;align-items:center;gap:6px;}\u001b[39m\n\u001b[31m.ocr-confidence{padding:2px 6px;border-radius:10px;font-family:var(--fnt-mono);}\u001b[39m\n\u001b[31m.conf-high{background:var(--grn-dim);color:var(--grn);}\u001b[39m\n\u001b[31m.conf-med{background:var(--ylw-dim);color:var(--ylw);}\u001b[39m\n\u001b[31m.conf-low{background:var(--red-dim);color:var(--red);}\u001b[39m\n\u001b[31m.ocr-ai-btn{\u001b[39m\n\u001b[31m  background:rgba(56,189,248,.15);border:1px solid rgba(56,189,248,.3);\u001b[39m\n\u001b[31m  color:var(--sky);font-size:.65rem;padding:3px 8px;border-radius:10px;\u001b[39m\n\u001b[31m  cursor:pointer;font-family:var(--fnt-mono);transition:all .2s;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.ocr-ai-btn:hover{background:rgba(56,189,248,.3);}\u001b[39m\n\u001b[31m.ocr-working{\u001b[39m\n\u001b[31m  background:var(--ylw-dim);border:1px solid rgba(234,179,8,.3);\u001b[39m\n\u001b[31m  border-radius:var(--r-sm);padding:8px 12px;margin-top:8px;\u001b[39m\n\u001b[31m  font-size:.75rem;color:var(--ylw);font-family:var(--fnt-mono);\u001b[39m\n\u001b[31m  display:none;align-items:center;gap:8px;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.ocr-working.visible{display:flex;}\u001b[39m\n\u001b[31m.spin{animation:spin .8s linear infinite;display:inline-block;}\u001b[39m\n\u001b[31m@keyframes spin{to{transform:rotate(360deg);}}·\u001b[39m\n\u001b[31m/* ════ STORE LIST ════ */\u001b[39m\n\u001b[31m.store-upload-zone{\u001b[39m\n\u001b[31m  border:2px dashed var(--bdr2);border-radius:var(--r);padding:16px;\u001b[39m\n\u001b[31m  text-align:center;cursor:pointer;transition:all .25s;background:var(--sur2);\u001b[39m\n\u001b[31m  position:relative;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.store-upload-zone:hover{border-color:var(--sky);background:rgba(56,189,248,.05);}\u001b[39m\n\u001b[31m.store-upload-zone input{display:none;}\u001b[39m\n\u001b[31m.store-table-wrap{max-height:200px;overflow-y:auto;margin-top:10px;border-radius:var(--r-sm);overflow:hidden;}\u001b[39m\n\u001b[31m.store-table{width:100%;border-collapse:collapse;font-size:.75rem;}\u001b[39m\n\u001b[31m.store-table th{\u001b[39m\n\u001b[31m  background:var(--bg);color:var(--t3);font-family:var(--fnt-head);\u001b[39m\n\u001b[31m  font-size:.65rem;letter-spacing:1.5px;text-transform:uppercase;\u001b[39m\n\u001b[31m  padding:7px 8px;text-align:left;border-bottom:1px solid var(--bdr);\u001b[39m\n\u001b[31m  position:sticky;top:0;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.store-table td{\u001b[39m\n\u001b[31m  padding:6px 8px;border-bottom:1px solid var(--bdr);color:var(--t2);\u001b[39m\n\u001b[31m  font-family:var(--fnt-mono);font-size:.72rem;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.store-table tr:hover td{background:var(--sur3);}\u001b[39m\n\u001b[31m.store-table .matched{color:var(--grn);}\u001b[39m\n\u001b[31m.store-table .unmatched{color:var(--ylw);}\u001b[39m\n\u001b[31m.store-map-btn{\u001b[39m\n\u001b[31m  font-size:.65rem;background:none;border:1px solid var(--bdr);color:var(--t3);\u001b[39m\n\u001b[31m  padding:2px 6px;border-radius:4px;cursor:pointer;transition:all .2s;\u001b[39m\n\u001b[31m  font-family:var(--fnt-mono);\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.store-map-btn:hover{border-color:var(--acc);color:var(--acc);}·\u001b[39m\n\u001b[31m/* ════ CONSIGNEE CARDS ════ */\u001b[39m\n\u001b[31m.cons-list{display:flex;flex-direction:column;gap:8px;margin-bottom:10px;}\u001b[39m\n\u001b[31m.cons-card{\u001b[39m\n\u001b[31m  background:var(--sur2);border:1px solid var(--bdr);border-radius:var(--r-sm);\u001b[39m\n\u001b[31m  padding:11px;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.cons-hdr{display:flex;justify-content:space-between;align-items:center;margin-bottom:8px;}\u001b[39m\n\u001b[31m.cons-num{font-family:var(--fnt-mono);font-size:.65rem;color:var(--t3);}\u001b[39m\n\u001b[31m.cons-del{\u001b[39m\n\u001b[31m  background:none;border:none;color:var(--t4);cursor:pointer;\u001b[39m\n\u001b[31m  font-size:.85rem;padding:2px 5px;border-radius:4px;transition:all .2s;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.cons-del:hover{background:var(--red-dim);color:var(--red);}\u001b[39m\n\u001b[31m.cons-docket-linked{\u001b[39m\n\u001b[31m  display:flex;align-items:center;gap:6px;\u001b[39m\n\u001b[31m  background:rgba(249,115,22,.1);border:1px solid rgba(249,115,22,.3);\u001b[39m\n\u001b[31m  border-radius:4px;padding:4px 8px;margin-top:6px;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.cons-docket-linked .dkt{font-family:var(--fnt-mono);font-size:.82rem;color:var(--acc);}\u001b[39m\n\u001b[31m.cons-docket-linked .unlink{background:none;border:none;color:var(--acc);cursor:pointer;font-size:.75rem;}·\u001b[39m\n\u001b[31m/* ════ DISPATCH DASHBOARD ════ */\u001b[39m\n\u001b[31m.dash-stats{\u001b[39m\n\u001b[31m  display:grid;grid-template-columns:repeat(4,1fr);gap:8px;margin-bottom:14px;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.stat-box{\u001b[39m\n\u001b[31m  background:var(--sur);border:1px solid var(--bdr);border-radius:var(--r);\u001b[39m\n\u001b[31m  padding:10px 8px;text-align:center;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.stat-val{font-family:var(--fnt-head);font-size:1.4rem;font-weight:800;}\u001b[39m\n\u001b[31m.stat-lbl{font-size:.6rem;color:var(--t3);font-family:var(--fnt-mono);text-transform:uppercase;letter-spacing:1px;margin-top:2px;}\u001b[39m\n\u001b[31m.stat-box.grn .stat-val{color:var(--grn);}\u001b[39m\n\u001b[31m.stat-box.ylw .stat-val{color:var(--ylw);}\u001b[39m\n\u001b[31m.stat-box.red .stat-val{color:var(--red);}\u001b[39m\n\u001b[31m.stat-box.sky .stat-val{color:var(--sky);}·\u001b[39m\n\u001b[31m/* ════ DOCKET TILES ════ */\u001b[39m\n\u001b[31m.docket-grid{display:flex;flex-direction:column;gap:8px;}\u001b[39m\n\u001b[31m.docket-tile{\u001b[39m\n\u001b[31m  background:var(--sur);border:1px solid var(--bdr);border-radius:var(--r);\u001b[39m\n\u001b[31m  padding:12px;transition:border-color .2s;cursor:pointer;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.docket-tile:hover{border-color:var(--acc);}\u001b[39m\n\u001b[31m.docket-tile-hdr{display:flex;justify-content:space-between;align-items:flex-start;margin-bottom:8px;}\u001b[39m\n\u001b[31m.docket-tile-left{flex:1;}\u001b[39m\n\u001b[31m.docket-num{font-family:var(--fnt-mono);font-size:.95rem;font-weight:500;color:var(--sky);letter-spacing:1px;}\u001b[39m\n\u001b[31m.docket-store{font-size:.82rem;color:var(--t1);font-weight:600;margin-top:2px;}\u001b[39m\n\u001b[31m.docket-courier{font-size:.68rem;color:var(--t3);font-family:var(--fnt-mono);margin-top:2px;}\u001b[39m\n\u001b[31m.status-pill{\u001b[39m\n\u001b[31m  font-size:.62rem;font-weight:700;font-family:var(--fnt-mono);\u001b[39m\n\u001b[31m  padding:3px 8px;border-radius:10px;text-transform:uppercase;letter-spacing:.5px;\u001b[39m\n\u001b[31m  flex-shrink:0;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.sp-delivered{background:var(--grn-dim);color:var(--grn);}\u001b[39m\n\u001b[31m.sp-transit{background:var(--ylw-dim);color:var(--ylw);}\u001b[39m\n\u001b[31m.sp-booked{background:var(--sky-dim);color:var(--sky);}\u001b[39m\n\u001b[31m.sp-pending{background:var(--bdr);color:var(--t3);}\u001b[39m\n\u001b[31m.sp-failed{background:var(--red-dim);color:var(--red);}\u001b[39m\n\u001b[31m.docket-tile-footer{display:flex;align-items:center;gap:8px;flex-wrap:wrap;}\u001b[39m\n\u001b[31m.docket-last-event{font-size:.7rem;color:var(--t3);flex:1;font-family:var(--fnt-mono);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}\u001b[39m\n\u001b[31m.btn-track-live{\u001b[39m\n\u001b[31m  background:none;border:1px solid var(--acc);color:var(--acc);\u001b[39m\n\u001b[31m  font-size:.65rem;padding:4px 10px;border-radius:20px;cursor:pointer;\u001b[39m\n\u001b[31m  font-family:var(--fnt-mono);transition:all .2s;white-space:nowrap;display:flex;align-items:center;gap:4px;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.btn-track-live:hover{background:var(--acc);color:#fff;}\u001b[39m\n\u001b[31m.btn-track-live .live-dot{width:5px;height:5px;border-radius:50%;background:var(--grn);animation:blink 1.5s infinite;flex-shrink:0;}\u001b[39m\n\u001b[31m.docket-progress{height:2px;background:var(--bdr);border-radius:2px;margin-top:8px;overflow:hidden;}\u001b[39m\n\u001b[31m.docket-progress-fill{height:100%;border-radius:2px;transition:width .5s;}·\u001b[39m\n\u001b[31m/* ════ TRACKING DETAIL ════ */\u001b[39m\n\u001b[31m.track-timeline{padding:4px 0;}\u001b[39m\n\u001b[31m.track-evt{display:flex;gap:10px;padding:6px 0;position:relative;padding-left:20px;}\u001b[39m\n\u001b[31m.track-evt::before{\u001b[39m\n\u001b[31m  content:'';position:absolute;left:5px;top:11px;\u001b[39m\n\u001b[31m  width:7px;height:7px;border-radius:50%;background:var(--bdr2);border:1px solid var(--bdr2);\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.track-evt:first-child::before{background:var(--grn);border-color:var(--grn);}\u001b[39m\n\u001b[31m.track-evt::after{\u001b[39m\n\u001b[31m  content:'';position:absolute;left:8px;top:18px;\u001b[39m\n\u001b[31m  width:1px;height:calc(100% - 6px);background:var(--bdr);\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.track-evt:last-child::after{display:none;}\u001b[39m\n\u001b[31m.track-time{font-family:var(--fnt-mono);font-size:.62rem;color:var(--t3);min-width:90px;flex-shrink:0;padding-top:2px;}\u001b[39m\n\u001b[31m.track-desc{font-size:.78rem;color:var(--t2);}\u001b[39m\n\u001b[31m.track-loc{font-size:.67rem;color:var(--t3);font-family:var(--fnt-mono);margin-top:2px;}·\u001b[39m\n\u001b[31m/* ════ SHARE TOAST ════ */\u001b[39m\n\u001b[31m.share-banner{\u001b[39m\n\u001b[31m  position:fixed;top:0;left:0;right:0;z-index:200;\u001b[39m\n\u001b[31m  background:rgba(34,197,94,.15);border-bottom:1px solid rgba(34,197,94,.4);\u001b[39m\n\u001b[31m  padding:10px 16px;font-size:.78rem;color:var(--grn);font-family:var(--fnt-mono);\u001b[39m\n\u001b[31m  display:flex;align-items:center;gap:8px;\u001b[39m\n\u001b[31m  transform:translateY(-100%);transition:transform .3s;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.share-banner.show{transform:translateY(0);}·\u001b[39m\n\u001b[31m/* ════ MODAL ════ */\u001b[39m\n\u001b[31m.overlay{position:fixed;inset:0;background:rgba(0,0,0,.75);z-index:100;display:flex;align-items:flex-end;opacity:0;pointer-events:none;transition:opacity .3s;}\u001b[39m\n\u001b[31m.overlay.open{opacity:1;pointer-events:all;}\u001b[39m\n\u001b[31m.sheet{\u001b[39m\n\u001b[31m  background:var(--sur);border-radius:var(--r-lg) var(--r-lg) 0 0;\u001b[39m\n\u001b[31m  width:100%;max-height:88vh;overflow-y:auto;padding:18px;\u001b[39m\n\u001b[31m  transform:translateY(100%);transition:transform .3s cubic-bezier(.32,.72,0,1);\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.overlay.open .sheet{transform:translateY(0);}\u001b[39m\n\u001b[31m.sheet-handle{width:36px;height:4px;background:var(--bdr2);border-radius:2px;margin:0 auto 18px;}\u001b[39m\n\u001b[31m.sheet-title{font-family:var(--fnt-head);font-size:1.2rem;font-weight:800;letter-spacing:.5px;margin-bottom:14px;}\u001b[39m\n\u001b[31m.sheet::-webkit-scrollbar{width:3px;}\u001b[39m\n\u001b[31m.sheet::-webkit-scrollbar-thumb{background:var(--bdr2);}·\u001b[39m\n\u001b[31m/* ════ TOAST ════ */\u001b[39m\n\u001b[31m.toast{\u001b[39m\n\u001b[31m  position:fixed;bottom:20px;left:50%;transform:translateX(-50%) translateY(70px);\u001b[39m\n\u001b[31m  background:var(--sur2);border:1px solid var(--bdr2);color:var(--t1);\u001b[39m\n\u001b[31m  font-size:.78rem;padding:9px 16px;border-radius:30px;z-index:300;\u001b[39m\n\u001b[31m  transition:transform .3s;white-space:nowrap;font-family:var(--fnt-mono);\u001b[39m\n\u001b[31m  pointer-events:none;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.toast.show{transform:translateX(-50%) translateY(0);}\u001b[39m\n\u001b[31m.toast.ok{border-color:var(--grn);color:var(--grn);}\u001b[39m\n\u001b[31m.toast.err{border-color:var(--red);color:var(--red);}\u001b[39m\n\u001b[31m.toast.info{border-color:var(--sky);color:var(--sky);}·\u001b[39m\n\u001b[31m/* ════ COURIER MASTER ════ */\u001b[39m\n\u001b[31m.courier-master-row{\u001b[39m\n\u001b[31m  display:flex;align-items:center;gap:8px;padding:8px 0;\u001b[39m\n\u001b[31m  border-bottom:1px solid var(--bdr);\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.courier-master-row:last-child{border-bottom:none;}\u001b[39m\n\u001b[31m.cml{flex:1;}\u001b[39m\n\u001b[31m.cml-name{font-weight:600;font-size:.85rem;}\u001b[39m\n\u001b[31m.cml-url{font-family:var(--fnt-mono);font-size:.65rem;color:var(--t3);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:180px;}\u001b[39m\n\u001b[31m.courier-logo{width:24px;height:24px;border-radius:4px;object-fit:contain;background:var(--sur3);padding:2px;}·\u001b[39m\n\u001b[31m/* ════ PROGRESS BAR ════ */\u001b[39m\n\u001b[31m.ai-progress{\u001b[39m\n\u001b[31m  background:var(--sur2);border:1px solid var(--bdr);border-radius:var(--r-sm);\u001b[39m\n\u001b[31m  padding:10px 12px;margin-top:8px;display:none;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.ai-progress.visible{display:block;}\u001b[39m\n\u001b[31m.ai-prog-bar{height:3px;background:var(--bdr);border-radius:3px;margin-top:6px;overflow:hidden;}\u001b[39m\n\u001b[31m.ai-prog-fill{height:100%;background:linear-gradient(90deg,var(--sky),var(--acc));width:0;transition:width .3s;}\u001b[39m\n\u001b[31m.ai-prog-label{font-size:.68rem;color:var(--sky);font-family:var(--fnt-mono);}·\u001b[39m\n\u001b[31m/* ════ EMPTY ════ */\u001b[39m\n\u001b[31m.empty{padding:40px 20px;text-align:center;color:var(--t4);}\u001b[39m\n\u001b[31m.empty .e-icon{font-size:2.5rem;margin-bottom:10px;}\u001b[39m\n\u001b[31m.empty p{font-size:.82rem;}·\u001b[39m\n\u001b[31m/* ════ INFO TAG ════ */\u001b[39m\n\u001b[31m.info-tag{\u001b[39m\n\u001b[31m  background:var(--sur2);border:1px solid var(--bdr);border-radius:var(--r-sm);\u001b[39m\n\u001b[31m  padding:7px 10px;font-size:.75rem;color:var(--t2);margin-bottom:8px;\u001b[39m\n\u001b[31m  display:flex;align-items:center;gap:8px;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.info-tag strong{color:var(--t1);}·\u001b[39m\n\u001b[31m/* ════ JOB PILL ════ */\u001b[39m\n\u001b[31m.job-pill{\u001b[39m\n\u001b[31m  display:inline-flex;align-items:center;gap:6px;\u001b[39m\n\u001b[31m  background:rgba(249,115,22,.1);border:1px solid rgba(249,115,22,.3);\u001b[39m\n\u001b[31m  color:var(--acc);font-family:var(--fnt-mono);font-size:.68rem;\u001b[39m\n\u001b[31m  padding:3px 9px;border-radius:20px;margin-top:4px;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.job-pill button{background:none;border:none;color:var(--acc);cursor:pointer;font-size:.7rem;}·\u001b[39m\n\u001b[31m/* ════ SECTION DIVIDER ════ */\u001b[39m\n\u001b[31m.sec-div{\u001b[39m\n\u001b[31m  display:flex;align-items:center;gap:10px;margin:14px 0 10px;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.sec-div span{font-family:var(--fnt-head);font-size:.7rem;font-weight:700;color:var(--t3);text-transform:uppercase;letter-spacing:2px;white-space:nowrap;}\u001b[39m\n\u001b[31m.sec-div::before,.sec-div::after{content:'';flex:1;height:1px;background:var(--bdr);}·\u001b[39m\n\u001b[31m/* ── MAPPING UI ── */\u001b[39m\n\u001b[31m.map-table{width:100%;border-collapse:collapse;font-size:.78rem;margin:10px 0;}\u001b[39m\n\u001b[31m.map-table th{background:var(--bg);color:var(--t3);font-family:var(--fnt-head);font-size:.65rem;\u001b[39m\n\u001b[31m  letter-spacing:1.5px;text-transform:uppercase;padding:7px 10px;text-align:left;\u001b[39m\n\u001b[31m  border-bottom:1px solid var(--bdr);}\u001b[39m\n\u001b[31m.map-table td{padding:7px 10px;border-bottom:1px solid var(--bdr);vertical-align:middle;}\u001b[39m\n\u001b[31m.map-table tr:last-child td{border-bottom:none;}\u001b[39m\n\u001b[31m.map-table select{background:var(--sur2);border:1px solid var(--bdr);color:var(--t1);\u001b[39m\n\u001b[31m  font-family:var(--fnt-mono);font-size:.75rem;padding:5px 8px;border-radius:var(--r-sm);\u001b[39m\n\u001b[31m  width:100%;outline:none;}\u001b[39m\n\u001b[31m.map-table select:focus{border-color:var(--acc);}\u001b[39m\n\u001b[31m.map-req{color:var(--acc);margin-left:2px;}\u001b[39m\n\u001b[31m.map-saved-badge{font-size:.6rem;background:rgba(57,255,143,.15);color:var(--accent3,#39ff8f);\u001b[39m\n\u001b[31m  border:1px solid rgba(57,255,143,.3);padding:2px 7px;border-radius:10px;\u001b[39m\n\u001b[31m  font-family:var(--fnt-mono);margin-left:6px;}·\u001b[39m\n\u001b[31m/* ── VERIFICATION SCREEN ── */\u001b[39m\n\u001b[31m#verifyPane{display:none;position:fixed;inset:0;background:var(--bg);z-index:50;\u001b[39m\n\u001b[31m  overflow-y:auto;padding:14px;}\u001b[39m\n\u001b[31m.verify-hdr{display:flex;align-items:center;gap:10px;margin-bottom:14px;\u001b[39m\n\u001b[31m  position:sticky;top:0;background:var(--bg);padding-bottom:10px;\u001b[39m\n\u001b[31m  border-bottom:1px solid var(--bdr);z-index:1;}\u001b[39m\n\u001b[31m.verify-title{font-family:var(--fnt-head);font-size:1.1rem;font-weight:800;flex:1;}\u001b[39m\n\u001b[31m.verify-row{background:var(--sur);border:1px solid var(--bdr);border-radius:var(--r-sm);\u001b[39m\n\u001b[31m  padding:10px 12px;margin-bottom:8px;position:relative;}\u001b[39m\n\u001b[31m.verify-row.has-dupe{border-color:var(--red);background:rgba(239,68,68,.04);}\u001b[39m\n\u001b[31m.verify-row.no-docket{border-color:var(--ylw);background:rgba(234,179,8,.04);}\u001b[39m\n\u001b[31m.verify-row.ok{border-left:3px solid var(--grn);}\u001b[39m\n\u001b[31m.verify-row-hdr{display:flex;align-items:center;gap:8px;margin-bottom:8px;}\u001b[39m\n\u001b[31m.verify-num{font-family:var(--fnt-mono);font-size:.65rem;color:var(--t3);}\u001b[39m\n\u001b[31m.verify-store{font-weight:600;font-size:.9rem;flex:1;}\u001b[39m\n\u001b[31m.verify-flag{font-size:.65rem;font-weight:700;font-family:var(--fnt-mono);\u001b[39m\n\u001b[31m  padding:2px 7px;border-radius:10px;text-transform:uppercase;}\u001b[39m\n\u001b[31m.vf-dupe{background:var(--red-dim);color:var(--red);}\u001b[39m\n\u001b[31m.vf-nodocket{background:var(--ylw-dim);color:var(--ylw);}\u001b[39m\n\u001b[31m.vf-ok{background:var(--grn-dim);color:var(--grn);}\u001b[39m\n\u001b[31m.vf-short{background:rgba(56,189,248,.15);color:var(--sky);}\u001b[39m\n\u001b[31m.verify-fields{display:grid;grid-template-columns:1fr 1fr;gap:8px;margin-bottom:8px;}\u001b[39m\n\u001b[31m.verify-field label{font-size:.62rem;color:var(--t3);text-transform:uppercase;\u001b[39m\n\u001b[31m  letter-spacing:1px;display:block;margin-bottom:3px;font-family:var(--fnt-mono);}\u001b[39m\n\u001b[31m.verify-field input{width:100%;background:var(--sur2);border:1px solid var(--bdr);\u001b[39m\n\u001b[31m  color:var(--t1);font-family:var(--fnt-mono);font-size:.82rem;padding:6px 9px;\u001b[39m\n\u001b[31m  border-radius:var(--r-sm);outline:none;}\u001b[39m\n\u001b[31m.verify-field input:focus{border-color:var(--acc);}\u001b[39m\n\u001b[31m.verify-field input.changed{border-color:var(--ylw);background:rgba(234,179,8,.06);}\u001b[39m\n\u001b[31m.verify-actions{display:flex;gap:6px;align-items:center;flex-wrap:wrap;}\u001b[39m\n\u001b[31m.btn-wa{background:rgba(37,211,102,.15);border:1px solid rgba(37,211,102,.3);\u001b[39m\n\u001b[31m  color:#25d366;font-size:.65rem;padding:4px 10px;border-radius:10px;cursor:pointer;\u001b[39m\n\u001b[31m  font-family:var(--fnt-mono);transition:all .2s;}\u001b[39m\n\u001b[31m.btn-wa:hover{background:rgba(37,211,102,.3);}\u001b[39m\n\u001b[31m.verify-thumb{width:48px;height:48px;object-fit:cover;border-radius:6px;\u001b[39m\n\u001b[31m  border:1px solid var(--bdr);cursor:pointer;flex-shrink:0;}\u001b[39m\n\u001b[31m.verify-summary{background:var(--sur);border:1px solid var(--bdr);border-radius:var(--r);\u001b[39m\n\u001b[31m  padding:12px 14px;margin-bottom:14px;display:flex;gap:16px;flex-wrap:wrap;}\u001b[39m\n\u001b[31m.vs-item{text-align:center;}\u001b[39m\n\u001b[31m.vs-val{font-family:var(--fnt-head);font-size:1.3rem;font-weight:800;}\u001b[39m\n\u001b[31m.vs-lbl{font-size:.6rem;color:var(--t3);font-family:var(--fnt-mono);text-transform:uppercase;\u001b[39m\n\u001b[31m  letter-spacing:.5px;margin-top:2px;}\u001b[39m\n\u001b[31m.ocr-correction-note{font-size:.65rem;color:var(--ylw);font-family:var(--fnt-mono);\u001b[39m\n\u001b[31m  margin-top:3px;}\u001b[39m\n\u001b[31m</style>\u001b[39m\n\u001b[31m</head>\u001b[39m\n\u001b[31m<body>·\u001b[39m\n\u001b[31m<!-- SPLASH -->\u001b[39m\n\u001b[31m<div id=\\\"splash\\\">\u001b[39m\n\u001b[31m  <div class=\\\"splash-logo\\\">360LM <em>DISPATCH</em></div>\u001b[39m\n\u001b[31m  <div class=\\\"splash-sub\\\">Courier · Bus · Field Ops</div>\u001b[39m\n\u001b[31m  <div class=\\\"splash-track\\\"><div class=\\\"splash-track-fill\\\"></div></div>\u001b[39m\n\u001b[31m</div>·\u001b[39m\n\u001b[31m<!-- SHARE BANNER -->\u001b[39m\n\u001b[31m<div class=\\\"share-banner\\\" id=\\\"shareBanner\\\">\u001b[39m\n\u001b[31m  📤 Images received via Share — processing for OCR...\u001b[39m\n\u001b[31m  <button style=\\\"background:none;border:none;color:var(--grn);cursor:pointer;margin-left:auto;\\\" onclick=\\\"dismissShare()\\\">✕</button>\u001b[39m\n\u001b[31m</div>·\u001b[39m\n\u001b[31m<!-- APP -->\u001b[39m\n\u001b[31m<div id=\\\"app\\\" style=\\\"display:none;\\\">·\u001b[39m\n\u001b[31m  <div class=\\\"hdr\\\">\u001b[39m\n\u001b[31m    <div class=\\\"hdr-brand\\\">360LM <em>DISPATCH</em></div>\u001b[39m\n\u001b[31m    <span class=\\\"hdr-dot\\\" id=\\\"netDot\\\"></span>\u001b[39m\n\u001b[31m    <button class=\\\"hdr-btn\\\" onclick=\\\"window.location.href='/hub/'\\\" title=\\\"Hub\\\">🏡</button>\u001b[39m\n\u001b[31m    <button class=\\\"hdr-btn\\\" onclick=\\\"showPane('dashboard')\\\">📊</button>\u001b[39m\n\u001b[31m    <button class=\\\"hdr-btn primary\\\" onclick=\\\"newDispatch()\\\">+ NEW</button>\u001b[39m\n\u001b[31m  </div>·\u001b[39m\n\u001b[31m  <div class=\\\"tabs\\\">\u001b[39m\n\u001b[31m    <button class=\\\"tab active\\\" onclick=\\\"showPane('new')\\\" id=\\\"tab-new\\\">NEW</button>\u001b[39m\n\u001b[31m    <button class=\\\"tab\\\" onclick=\\\"showPane('dashboard')\\\" id=\\\"tab-dashboard\\\">DASHBOARD</button>\u001b[39m\n\u001b[31m    <button class=\\\"tab\\\" onclick=\\\"showPane('track')\\\" id=\\\"tab-track\\\">TRACK</button>\u001b[39m\n\u001b[31m    <button class=\\\"tab\\\" onclick=\\\"showPane('history')\\\" id=\\\"tab-history\\\">HISTORY <span class=\\\"tab-badge\\\" id=\\\"histCount\\\">0</span></button>\u001b[39m\n\u001b[31m    <button class=\\\"tab\\\" onclick=\\\"showPane('config')\\\" id=\\\"tab-config\\\">CONFIG</button>\u001b[39m\n\u001b[31m  </div>·\u001b[39m\n\u001b[31m  <!-- ════════════ NEW DISPATCH ════════════ -->\u001b[39m\n\u001b[31m  <div class=\\\"pane active\\\" id=\\\"pane-new\\\">·\u001b[39m\n\u001b[31m    <!-- 1. JOB REFERENCE -->\u001b[39m\n\u001b[31m    <div class=\\\"card accent-left\\\">\u001b[39m\n\u001b[31m      <div class=\\\"card-title\\\"><span class=\\\"ct-dot\\\"></span>Job Reference</div>\u001b[39m\n\u001b[31m      <div class=\\\"row2\\\">\u001b[39m\n\u001b[31m        <div class=\\\"f\\\">\u001b[39m\n\u001b[31m          <label>Client <span class=\\\"req\\\">*</span></label>\u001b[39m\n\u001b[31m          <select id=\\\"n_client\\\" onchange=\\\"onClientChange()\\\">\u001b[39m\n\u001b[31m            <option value=\\\"\\\">Select...</option>\u001b[39m\n\u001b[31m            <option>Lenovo</option><option>HP</option>\u001b[39m\n\u001b[31m            <option>Philips</option><option>Liebherr</option>\u001b[39m\n\u001b[31m            <option value=\\\"__c\\\">+ Custom</option>\u001b[39m\n\u001b[31m          </select>\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m        <div class=\\\"f\\\">\u001b[39m\n\u001b[31m          <label>Job Ref</label>\u001b[39m\n\u001b[31m          <input type=\\\"text\\\" id=\\\"n_jobref\\\" placeholder=\\\"JOB-042\\\" oninput=\\\"updatePill()\\\">\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      <div class=\\\"f\\\" id=\\\"customClientField\\\" style=\\\"display:none;\\\">\u001b[39m\n\u001b[31m        <label>Custom Client</label>\u001b[39m\n\u001b[31m        <input type=\\\"text\\\" id=\\\"n_customClient\\\" placeholder=\\\"Client name\\\" oninput=\\\"updatePill()\\\">\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      <div id=\\\"jobPill\\\" style=\\\"display:none;\\\" class=\\\"job-pill\\\">\u001b[39m\n\u001b[31m        🔗 <span id=\\\"jobPillTxt\\\"></span>\u001b[39m\n\u001b[31m        <button onclick=\\\"clearJob()\\\">✕</button>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      <div class=\\\"row2\\\" style=\\\"margin-top:10px;\\\">\u001b[39m\n\u001b[31m        <div class=\\\"f\\\" style=\\\"margin-bottom:0;\\\">\u001b[39m\n\u001b[31m          <label>Dispatch Date</label>\u001b[39m\n\u001b[31m          <input type=\\\"date\\\" id=\\\"n_date\\\">\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m        <div class=\\\"f\\\" style=\\\"margin-bottom:0;\\\">\u001b[39m\n\u001b[31m          <label>Courier Company <span class=\\\"req\\\">*</span></label>\u001b[39m\n\u001b[31m          <select id=\\\"n_courier\\\">\u001b[39m\n\u001b[31m            <option value=\\\"\\\">Select...</option>\u001b[39m\n\u001b[31m            <option>SkyKing</option><option>DTDC</option>\u001b[39m\n\u001b[31m            <option>Delhivery</option><option>Blue Dart</option>\u001b[39m\n\u001b[31m            <option>FedEx</option><option>Ecom Express</option>\u001b[39m\n\u001b[31m            <option>India Post</option><option value=\\\"__o\\\">Other</option>\u001b[39m\n\u001b[31m          </select>\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m    </div>·\u001b[39m\n\u001b[31m    <!-- 2. STORE LIST UPLOAD -->\u001b[39m\n\u001b[31m    <div class=\\\"card\\\">\u001b[39m\n\u001b[31m      <div class=\\\"card-title\\\"><span class=\\\"ct-dot\\\" style=\\\"background:var(--sky);\\\"></span>Store / Destination List</div>\u001b[39m\n\u001b[31m      <div class=\\\"store-upload-zone\\\" id=\\\"storeZone\\\" onclick=\\\"document.getElementById('storeFile').click();event.stopPropagation();\\\" style=\\\"cursor:pointer;\\\">\u001b[39m\n\u001b[31m        <input type=\\\"file\\\" id=\\\"storeFile\\\" accept=\\\".xlsx,.xls,.csv\\\" onchange=\\\"handleStoreFile(this)\\\" onclick=\\\"event.stopPropagation();\\\" style=\\\"display:none;\\\">\u001b[39m\n\u001b[31m        <div style=\\\"font-size:1.5rem;margin-bottom:6px;\\\">📋</div>\u001b[39m\n\u001b[31m        <div style=\\\"font-family:var(--fnt-head);font-weight:700;font-size:.9rem;\\\">Upload Store List</div>\u001b[39m\n\u001b[31m        <div style=\\\"font-size:.68rem;color:var(--t3);margin-top:3px;font-family:var(--fnt-mono);\\\">Excel / CSV · stores, cities, contacts</div>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      <div id=\\\"storeTableArea\\\" style=\\\"display:none;\\\">\u001b[39m\n\u001b[31m        <!-- Column detection result -->\u001b[39m\n\u001b[31m        <div id=\\\"storeColInfo\\\" style=\\\"margin-top:10px;display:none;\\\"></div>\u001b[39m\n\u001b[31m        <div style=\\\"display:flex;justify-content:space-between;align-items:center;margin-top:10px;margin-bottom:6px;\\\">\u001b[39m\n\u001b[31m          <span style=\\\"font-family:var(--fnt-mono);font-size:.7rem;color:var(--sky);\\\" id=\\\"storeCount\\\">0 stores loaded</span>\u001b[39m\n\u001b[31m          <button class=\\\"btn-sm\\\" onclick=\\\"clearStores()\\\">Clear</button>\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m        <div class=\\\"store-table-wrap\\\">\u001b[39m\n\u001b[31m          <table class=\\\"store-table\\\" id=\\\"storeTable\\\"></table>\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m        <!-- Inline add-as-consignees UI instead of confirm() -->\u001b[39m\n\u001b[31m        <div id=\\\"storeAddPanel\\\" style=\\\"margin-top:10px;background:var(--sur3);border:1px solid var(--bdr2);border-radius:var(--r-sm);padding:10px 12px;display:none;\\\">\u001b[39m\n\u001b[31m          <div style=\\\"font-size:.75rem;color:var(--t1);margin-bottom:8px;font-weight:600;\\\" id=\\\"storeAddTitle\\\">Add stores as consignees?</div>\u001b[39m\n\u001b[31m          <div class=\\\"row2\\\">\u001b[39m\n\u001b[31m            <button class=\\\"btn btn-ghost\\\" style=\\\"font-size:.75rem;padding:8px;\\\" onclick=\\\"document.getElementById('storeAddPanel').style.display='none'\\\">Skip for now</button>\u001b[39m\n\u001b[31m            <button class=\\\"btn btn-orange\\\" style=\\\"font-size:.75rem;padding:8px;\\\" id=\\\"storeAddBtn\\\" onclick=\\\"addStoresToConsignees()\\\">✓ Add All as Consignees</button>\u001b[39m\n\u001b[31m          </div>\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m        <button class=\\\"btn btn-ghost\\\" style=\\\"margin-top:8px;font-size:.78rem;\\\" onclick=\\\"autoMatchDockets()\\\">\u001b[39m\n\u001b[31m          🔗 Auto-match Dockets to Stores\u001b[39m\n\u001b[31m        </button>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m    </div>·\u001b[39m\n\u001b[31m    <!-- 3. DOCKET SLIPS — AI OCR -->\u001b[39m\n\u001b[31m    <div class=\\\"card\\\">\u001b[39m\n\u001b[31m      <div class=\\\"card-title\\\"><span class=\\\"ct-dot\\\" style=\\\"background:var(--grn);\\\"></span>Docket Slips — AI OCR</div>·\u001b[39m\n\u001b[31m      <!-- Share target indicator -->\u001b[39m\n\u001b[31m      <div class=\\\"info-tag\\\" id=\\\"shareTargetInfo\\\" style=\\\"display:none;\\\">\u001b[39m\n\u001b[31m        <span>📤</span><span>Images shared from WhatsApp — <strong id=\\\"sharedCount\\\">0</strong> slips received</span>\u001b[39m\n\u001b[31m      </div>·\u001b[39m\n\u001b[31m      <label class=\\\"upload-zone\\\" id=\\\"slipZone\\\" for=\\\"slipFiles\\\" style=\\\"display:block;cursor:pointer;\\\">\u001b[39m\n\u001b[31m        <input type=\\\"file\\\" id=\\\"slipFiles\\\" accept=\\\"image/*\\\" multiple onchange=\\\"handleSlipFiles(this.files)\\\" style=\\\"display:none;\\\">\u001b[39m\n\u001b[31m        <div class=\\\"uz-icon\\\">🖼️</div>\u001b[39m\n\u001b[31m        <div class=\\\"uz-title\\\">Upload Courier Slips</div>\u001b[39m\n\u001b[31m        <div class=\\\"uz-sub\\\">Multiple images · Tap or drag · Camera capture supported</div>\u001b[39m\n\u001b[31m      </label>·\u001b[39m\n\u001b[31m      <!-- OCR Working indicator -->\u001b[39m\n\u001b[31m      <div class=\\\"ocr-working\\\" id=\\\"ocrWorking\\\">\u001b[39m\n\u001b[31m        <span class=\\\"spin\\\">⟳</span>\u001b[39m\n\u001b[31m        <span id=\\\"ocrWorkingText\\\">Running OCR...</span>\u001b[39m\n\u001b[31m      </div>·\u001b[39m\n\u001b[31m      <!-- AI Progress -->\u001b[39m\n\u001b[31m      <div class=\\\"ai-progress\\\" id=\\\"aiProgress\\\">\u001b[39m\n\u001b[31m        <div class=\\\"ai-prog-label\\\" id=\\\"aiProgLabel\\\">Initializing Transformers.js...</div>\u001b[39m\n\u001b[31m        <div class=\\\"ai-prog-bar\\\"><div class=\\\"ai-prog-fill\\\" id=\\\"aiProgFill\\\"></div></div>\u001b[39m\n\u001b[31m      </div>·\u001b[39m\n\u001b[31m      <!-- OCR Results -->\u001b[39m\n\u001b[31m      <div class=\\\"ocr-results\\\" id=\\\"ocrResults\\\">\u001b[39m\n\u001b[31m        <div class=\\\"sec-div\\\"><span>Detected Dockets</span></div>\u001b[39m\n\u001b[31m        <div id=\\\"ocrSummary\\\" style=\\\"display:none;font-size:.72rem;font-family:var(--fnt-mono);\u001b[39m\n\u001b[31m          padding:4px 8px 8px;gap:8px\\\"></div>\u001b[39m\n\u001b[31m        <div class=\\\"ocr-slip-list\\\" id=\\\"ocrSlipList\\\"></div>\u001b[39m\n\u001b[31m        <div class=\\\"btn-row\\\" style=\\\"margin-top:10px;\\\">\u001b[39m\n\u001b[31m          <button class=\\\"btn btn-ghost\\\" onclick=\\\"clearSlips()\\\">Clear All</button>\u001b[39m\n\u001b[31m          <button class=\\\"btn btn-grn\\\" onclick=\\\"applyAllDockets()\\\">✓ Apply All to Consignees</button>\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m    </div>·\u001b[39m\n\u001b[31m    <!-- 4. CONSIGNEES -->\u001b[39m\n\u001b[31m    <div class=\\\"card accent-left\\\">\u001b[39m\n\u001b[31m      <div class=\\\"card-title\\\"><span class=\\\"ct-dot\\\"></span>Consignees</div>\u001b[39m\n\u001b[31m      <div class=\\\"cons-list\\\" id=\\\"consList\\\"></div>\u001b[39m\n\u001b[31m      <button class=\\\"btn btn-ghost\\\" style=\\\"font-size:.8rem;\\\" onclick=\\\"addConsignee()\\\">+ Add Consignee</button>\u001b[39m\n\u001b[31m    </div>·\u001b[39m\n\u001b[31m    <!-- 5. REMARKS -->\u001b[39m\n\u001b[31m    <div class=\\\"card\\\">\u001b[39m\n\u001b[31m      <div class=\\\"card-title\\\"><span class=\\\"ct-dot\\\" style=\\\"background:var(--t3);\\\"></span>Remarks</div>\u001b[39m\n\u001b[31m      <div class=\\\"f\\\" style=\\\"margin-bottom:0;\\\">\u001b[39m\n\u001b[31m        <textarea id=\\\"n_remarks\\\" placeholder=\\\"Instructions, notes, special handling...\\\"></textarea>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m    </div>·\u001b[39m\n\u001b[31m    <!-- ACTIONS -->\u001b[39m\n\u001b[31m    <div class=\\\"btn-row\\\">\u001b[39m\n\u001b[31m      <button class=\\\"btn btn-ghost\\\" onclick=\\\"saveDraft()\\\">💾 Draft</button>\u001b[39m\n\u001b[31m      <button class=\\\"btn btn-orange\\\" onclick=\\\"finaliseDispatch()\\\">✓ Finalise & Log</button>\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m    <div style=\\\"height:16px;\\\"></div>\u001b[39m\n\u001b[31m  </div>·\u001b[39m\n\u001b[31m  <!-- ════════════ DASHBOARD ════════════ -->\u001b[39m\n\u001b[31m  <div class=\\\"pane\\\" id=\\\"pane-dashboard\\\">\u001b[39m\n\u001b[31m    <div class=\\\"dash-stats\\\">\u001b[39m\n\u001b[31m      <div class=\\\"stat-box grn\\\"><div class=\\\"stat-val\\\" id=\\\"st-delivered\\\">0</div><div class=\\\"stat-lbl\\\">Delivered</div></div>\u001b[39m\n\u001b[31m      <div class=\\\"stat-box ylw\\\"><div class=\\\"stat-val\\\" id=\\\"st-transit\\\">0</div><div class=\\\"stat-lbl\\\">In Transit</div></div>\u001b[39m\n\u001b[31m      <div class=\\\"stat-box sky\\\"><div class=\\\"stat-val\\\" id=\\\"st-booked\\\">0</div><div class=\\\"stat-lbl\\\">Booked</div></div>\u001b[39m\n\u001b[31m      <div class=\\\"stat-box red\\\"><div class=\\\"stat-val\\\" id=\\\"st-pending\\\">0</div><div class=\\\"stat-lbl\\\">Pending</div></div>\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m    <div id=\\\"dashFilters\\\" style=\\\"display:flex;gap:6px;flex-wrap:wrap;margin-bottom:12px;\\\">\u001b[39m\n\u001b[31m      <span class=\\\"btn-sm primary\\\" onclick=\\\"filterDash('all',this)\\\" style=\\\"cursor:pointer;\\\">All</span>\u001b[39m\n\u001b[31m      <span class=\\\"btn-sm\\\" onclick=\\\"filterDash('delivered',this)\\\" style=\\\"cursor:pointer;\\\">Delivered</span>\u001b[39m\n\u001b[31m      <span class=\\\"btn-sm\\\" onclick=\\\"filterDash('transit',this)\\\" style=\\\"cursor:pointer;\\\">In Transit</span>\u001b[39m\n\u001b[31m      <span class=\\\"btn-sm\\\" onclick=\\\"filterDash('booked',this)\\\" style=\\\"cursor:pointer;\\\">Booked</span>\u001b[39m\n\u001b[31m      <span class=\\\"btn-sm\\\" onclick=\\\"filterDash('pending',this)\\\" style=\\\"cursor:pointer;\\\">Pending</span>\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m    <div class=\\\"docket-grid\\\" id=\\\"dashGrid\\\">\u001b[39m\n\u001b[31m      <div class=\\\"empty\\\"><div class=\\\"e-icon\\\">📊</div><p>No dispatches yet.<br>Create one to see the dashboard.</p></div>\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m    <div style=\\\"display:flex;gap:8px;margin-top:10px;flex-wrap:wrap;\\\">\u001b[39m\n\u001b[31m      <button class=\\\"btn btn-ghost\\\" style=\\\"flex:2;font-size:.8rem;\\\" id=\\\"btnRefreshAll\\\" onclick=\\\"refreshAllTracking()\\\">\u001b[39m\n\u001b[31m        🔄 Refresh All Tracking\u001b[39m\n\u001b[31m      </button>\u001b[39m\n\u001b[31m      <button class=\\\"btn btn-ghost\\\" style=\\\"flex:1;font-size:.8rem;border-color:var(--sky);color:var(--sky);\\\"\u001b[39m\n\u001b[31m        onclick=\\\"shareSelectedReport()\\\">\u001b[39m\n\u001b[31m        📊 Share Selected\u001b[39m\n\u001b[31m      </button>\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m  </div>·\u001b[39m\n\u001b[31m  <!-- ════════════ TRACK ════════════ -->\u001b[39m\n\u001b[31m  <div class=\\\"pane\\\" id=\\\"pane-track\\\">·\u001b[39m\n\u001b[31m    <!-- Courier selector -->\u001b[39m\n\u001b[31m    <div class=\\\"card\\\" style=\\\"margin-bottom:10px;\\\">\u001b[39m\n\u001b[31m      <div class=\\\"card-title\\\"><span class=\\\"ct-dot\\\" style=\\\"background:var(--sky);\\\"></span>Manual Tracking</div>\u001b[39m\n\u001b[31m      <div style=\\\"font-size:.72rem;color:var(--t2);margin-bottom:10px;line-height:1.5;\\\">\u001b[39m\n\u001b[31m        Track any docket number — select from already dispatched dockets or enter new ones manually.\u001b[39m\n\u001b[31m        SkyKing supports up to <strong>10 dockets at once</strong>; others are tracked one at a time.\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      <div class=\\\"f\\\">\u001b[39m\n\u001b[31m        <label>Courier Company</label>\u001b[39m\n\u001b[31m        <select id=\\\"t_courier\\\" onchange=\\\"buildTrackDocketChips()\\\">\u001b[39m\n\u001b[31m          <option>SkyKing</option><option>DTDC</option><option>Delhivery</option>\u001b[39m\n\u001b[31m          <option>Blue Dart</option><option>FedEx</option><option>Ecom Express</option><option>India Post</option>\u001b[39m\n\u001b[31m        </select>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m    </div>·\u001b[39m\n\u001b[31m    <!-- Docket selector — from existing dispatches -->\u001b[39m\n\u001b[31m    <div class=\\\"card\\\" style=\\\"margin-bottom:10px;\\\">\u001b[39m\n\u001b[31m      <div class=\\\"card-title\\\"><span class=\\\"ct-dot\\\" style=\\\"background:var(--acc);\\\"></span>\u001b[39m\n\u001b[31m        Select from Dispatched Dockets\u001b[39m\n\u001b[31m        <span id=\\\"trackCourierLimit\\\" style=\\\"margin-left:auto;font-size:.65rem;color:var(--t3);font-weight:400;text-transform:none;letter-spacing:0;font-family:var(--fnt-mono);\\\"></span>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      <div id=\\\"trackDocketChips\\\" style=\\\"display:flex;flex-wrap:wrap;gap:6px;margin-bottom:10px;min-height:28px;\\\">\u001b[39m\n\u001b[31m        <span style=\\\"font-size:.72rem;color:var(--t3);font-family:var(--fnt-mono);\\\">No dispatched dockets yet</span>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      <div style=\\\"font-size:.68rem;color:var(--t3);font-family:var(--fnt-mono);margin-bottom:8px;\\\">\u001b[39m\n\u001b[31m        Tap a docket to add/remove from tracking list. Selected shown in orange.\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m    </div>·\u001b[39m\n\u001b[31m    <!-- Manual entry for new dockets -->\u001b[39m\n\u001b[31m    <div class=\\\"card\\\" style=\\\"margin-bottom:10px;\\\">\u001b[39m\n\u001b[31m      <div class=\\\"card-title\\\"><span class=\\\"ct-dot\\\" style=\\\"background:var(--grn);\\\"></span>Add New Docket Numbers</div>\u001b[39m\n\u001b[31m      <div class=\\\"f\\\" style=\\\"margin-bottom:6px;\\\">\u001b[39m\n\u001b[31m        <label>Enter docket numbers (comma or newline separated)</label>\u001b[39m\n\u001b[31m        <textarea id=\\\"t_manual_dockets\\\" placeholder=\\\"443005527&#10;443005528&#10;or: 443005527, 443005528\\\"\u001b[39m\n\u001b[31m          style=\\\"min-height:56px;font-family:var(--fnt-mono);font-size:.82rem;letter-spacing:.5px;\\\"></textarea>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      <button class=\\\"btn-sm primary\\\" onclick=\\\"addManualDockets()\\\" style=\\\"font-size:.72rem;\\\">+ Add to List</button>\u001b[39m\n\u001b[31m    </div>·\u001b[39m\n\u001b[31m    <!-- Selected dockets summary + track button -->\u001b[39m\n\u001b[31m    <div id=\\\"trackSelectedSummary\\\" style=\\\"display:none;\\\" class=\\\"card\\\" style=\\\"margin-bottom:10px;\\\">\u001b[39m\n\u001b[31m      <div style=\\\"display:flex;align-items:center;justify-content:space-between;margin-bottom:8px;\\\">\u001b[39m\n\u001b[31m        <div>\u001b[39m\n\u001b[31m          <span style=\\\"font-family:var(--fnt-mono);font-size:.72rem;color:var(--sky);\\\" id=\\\"trackSelectedCount\\\">0 selected</span>\u001b[39m\n\u001b[31m          <span style=\\\"font-family:var(--fnt-mono);font-size:.65rem;color:var(--t3);margin-left:8px;\\\" id=\\\"trackSelectedList\\\"></span>\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m        <button class=\\\"btn-sm\\\" onclick=\\\"clearTrackSelection()\\\">Clear</button>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      <div class=\\\"btn-row\\\">\u001b[39m\n\u001b[31m        <button class=\\\"btn btn-ghost\\\" onclick=\\\"openTrackOnCourierSite()\\\">🔗 Open Courier Site</button>\u001b[39m\n\u001b[31m        <button class=\\\"btn btn-orange\\\" onclick=\\\"trackSelected()\\\">🔍 Track All via Proxy</button>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m    </div>·\u001b[39m\n\u001b[31m    <div id=\\\"trackResultsArea\\\"></div>\u001b[39m\n\u001b[31m  </div>·\u001b[39m\n\u001b[31m  <!-- ════════════ HISTORY ════════════ -->\u001b[39m\n\u001b[31m  <div class=\\\"pane\\\" id=\\\"pane-history\\\">\u001b[39m\n\u001b[31m    <div id=\\\"historyList\\\">\u001b[39m\n\u001b[31m      <div class=\\\"empty\\\"><div class=\\\"e-icon\\\">📭</div><p>No dispatch history yet.</p></div>\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m  </div>·\u001b[39m\n\u001b[31m  <!-- ════════════ CONFIG ════════════ -->\u001b[39m\n\u001b[31m  <div class=\\\"pane\\\" id=\\\"pane-config\\\">·\u001b[39m\n\u001b[31m    <!-- SHEETS URL — the only thing you type manually in the PWA -->\u001b[39m\n\u001b[31m    <div class=\\\"card\\\" style=\\\"border-color:var(--acc);border-width:2px;\\\">\u001b[39m\n\u001b[31m      <div class=\\\"card-title\\\"><span class=\\\"ct-dot\\\"></span>Google Sheets — Master Config</div>\u001b[39m\n\u001b[31m      <div class=\\\"info-tag\\\" style=\\\"background:rgba(34,197,94,.07);border-color:rgba(34,197,94,.3);\\\">\u001b[39m\n\u001b[31m        <span>✅</span>\u001b[39m\n\u001b[31m        <span>URL is loaded automatically from <strong>config.json</strong> on the VPS — no manual entry needed. All other settings sync from your <strong>Google Sheet</strong>.</span>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      <div class=\\\"f\\\">\u001b[39m\n\u001b[31m        <label>Apps Script URL <span style=\\\"font-size:.65rem;color:var(--t3);font-weight:400;\\\">(auto-loaded from config.json)</span></label>\u001b[39m\n\u001b[31m        <input type=\\\"url\\\" id=\\\"cfg_sheets_url\\\"\u001b[39m\n\u001b[31m          placeholder=\\\"Auto-loaded from server...\\\"\u001b[39m\n\u001b[31m          oninput=\\\"onSheetsUrlChange()\\\" readonly\u001b[39m\n\u001b[31m          style=\\\"opacity:.7;cursor:default;\\\">\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      <div id=\\\"syncStatus\\\" class=\\\"info-tag\\\" style=\\\"display:none;\\\">\u001b[39m\n\u001b[31m        <span id=\\\"syncIcon\\\">⏳</span>\u001b[39m\n\u001b[31m        <span id=\\\"syncMsg\\\">Syncing from sheet...</span>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      <div class=\\\"btn-row\\\">\u001b[39m\n\u001b[31m        <button class=\\\"btn btn-ghost\\\" onclick=\\\"syncFromSheet()\\\">🔄 Sync Now</button>\u001b[39m\n\u001b[31m        <button class=\\\"btn btn-orange\\\" onclick=\\\"saveSheetsUrl()\\\">💾 Save & Sync</button>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m    </div>·\u001b[39m\n\u001b[31m    <!-- READ-ONLY display of what was loaded from sheet -->\u001b[39m\n\u001b[31m    <div class=\\\"card\\\" id=\\\"sheetConfigDisplay\\\" style=\\\"display:none;\\\">\u001b[39m\n\u001b[31m      <div class=\\\"card-title\\\"><span class=\\\"ct-dot\\\" style=\\\"background:var(--grn);\\\"></span>Config Loaded from Sheet\u001b[39m\n\u001b[31m        <span id=\\\"sheetSyncTime\\\" style=\\\"margin-left:auto;font-size:.62rem;color:var(--t3);font-family:var(--fnt-mono);font-weight:400;text-transform:none;letter-spacing:0;\\\"></span>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      <div id=\\\"sheetConfigRows\\\"></div>\u001b[39m\n\u001b[31m      <div class=\\\"info-tag\\\" style=\\\"margin-top:8px;\\\">\u001b[39m\n\u001b[31m        <span>✏️</span><span>To change any value, edit the <strong>Config sheet</strong> in Google Sheets, then tap Sync Now above.</span>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m    </div>·\u001b[39m\n\u001b[31m    <!-- Courier Master (from sheet) -->\u001b[39m\n\u001b[31m    <div class=\\\"card\\\">\u001b[39m\n\u001b[31m      <div class=\\\"card-title\\\"><span class=\\\"ct-dot\\\" style=\\\"background:var(--ylw);\\\"></span>Courier Master</div>\u001b[39m\n\u001b[31m      <div id=\\\"courierMasterList\\\"></div>\u001b[39m\n\u001b[31m      <button class=\\\"btn btn-ghost\\\" style=\\\"margin-top:8px;font-size:.78rem;\\\" onclick=\\\"openCourierMaster()\\\">✏️ Edit in Sheet / Override</button>\u001b[39m\n\u001b[31m    </div>·\u001b[39m\n\u001b[31m    <button class=\\\"btn\\\" style=\\\"background:var(--red-dim);color:var(--red);border:1px solid var(--red);margin-top:4px;\\\" onclick=\\\"clearAll()\\\">🗑️ Clear All Local Data</button>\u001b[39m\n\u001b[31m    <div style=\\\"height:20px;\\\"></div>\u001b[39m\n\u001b[31m  </div>··\u001b[39m\n\u001b[31m<!-- ════ ADDRESS REVIEW PANE ════ -->\u001b[39m\n\u001b[31m<div id=\\\"addrReviewPane\\\" style=\\\"display:none;position:fixed;inset:0;background:var(--bg);z-index:50;overflow-y:auto;padding:14px;\\\">\u001b[39m\n\u001b[31m  <div class=\\\"verify-hdr\\\">\u001b[39m\n\u001b[31m    <button class=\\\"btn-icon\\\" onclick=\\\"document.getElementById('addrReviewPane').style.display='none'\\\" style=\\\"font-size:1.1rem;\\\">←</button>\u001b[39m\n\u001b[31m    <div class=\\\"verify-title\\\">Review AI-Parsed Addresses</div>\u001b[39m\n\u001b[31m  </div>\u001b[39m\n\u001b[31m  <div style=\\\"background:rgba(56,189,248,.08);border:1px solid rgba(56,189,248,.3);border-radius:var(--r-sm);\u001b[39m\n\u001b[31m    padding:8px 12px;margin-bottom:12px;font-size:.72rem;color:var(--sky);\\\">\u001b[39m\n\u001b[31m    ✏️ Review each row — correct name and city if wrong. Raw address shown below each row for reference.\u001b[39m\n\u001b[31m    Corrections are saved and improve future AI parsing automatically.\u001b[39m\n\u001b[31m  </div>\u001b[39m\n\u001b[31m  <div class=\\\"verify-summary\\\" id=\\\"addrReviewSummary\\\"></div>\u001b[39m\n\u001b[31m  <div id=\\\"addrReviewList\\\"></div>\u001b[39m\n\u001b[31m  <div style=\\\"position:sticky;bottom:0;background:var(--bg);padding-top:10px;\u001b[39m\n\u001b[31m    border-top:1px solid var(--bdr);margin-top:8px;\\\">\u001b[39m\n\u001b[31m    <div style=\\\"display:flex;gap:8px;\\\">\u001b[39m\n\u001b[31m      <button class=\\\"btn btn-ghost\\\" onclick=\\\"backToMapping()\\\" style=\\\"flex:1;\\\">← Back to Mapping</button>\u001b[39m\n\u001b[31m      <button class=\\\"btn btn-orange\\\" onclick=\\\"confirmAddressReview()\\\" style=\\\"flex:2;\\\">✓ Confirm &amp; Use These Addresses</button>\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m    <div style=\\\"font-size:.65rem;color:var(--t3);text-align:center;margin-top:5px;font-family:var(--fnt-mono);\\\">\u001b[39m\n\u001b[31m      Edits you made above are preserved when you go back\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m  </div>\u001b[39m\n\u001b[31m</div>·\u001b[39m\n\u001b[31m<!-- ════ VERIFICATION SCREEN ════ -->\u001b[39m\n\u001b[31m<div id=\\\"verifyPane\\\">\u001b[39m\n\u001b[31m  <div class=\\\"verify-hdr\\\">\u001b[39m\n\u001b[31m    <button class=\\\"btn-icon\\\" onclick=\\\"closeVerify()\\\" style=\\\"font-size:1.1rem;\\\">←</button>\u001b[39m\n\u001b[31m    <div class=\\\"verify-title\\\">Verify Before Finalise</div>\u001b[39m\n\u001b[31m    <button class=\\\"btn-sm\\\" onclick=\\\"approveAll()\\\" style=\\\"white-space:nowrap;\\\">✓ Approve All</button>\u001b[39m\n\u001b[31m  </div>\u001b[39m\n\u001b[31m  <div class=\\\"verify-summary\\\" id=\\\"verifySummary\\\"></div>\u001b[39m\n\u001b[31m  <div id=\\\"verifyList\\\"></div>\u001b[39m\n\u001b[31m  <div style=\\\"position:sticky;bottom:0;background:var(--bg);padding-top:10px;\u001b[39m\n\u001b[31m    border-top:1px solid var(--bdr);margin-top:8px;\\\">\u001b[39m\n\u001b[31m    <div style=\\\"display:flex;gap:8px;flex-wrap:wrap;\\\">\u001b[39m\n\u001b[31m      <button class=\\\"btn btn-ghost\\\" onclick=\\\"closeVerify()\\\" style=\\\"flex:1;\\\">← Back to Edit</button>\u001b[39m\n\u001b[31m      <button class=\\\"btn btn-ghost\\\" onclick=\\\"exportCleanSheet()\\\" style=\\\"flex:1;border-color:var(--sky);color:var(--sky);\\\">\u001b[39m\n\u001b[31m        📥 Export Clean Sheet\u001b[39m\n\u001b[31m      </button>\u001b[39m\n\u001b[31m      <button class=\\\"btn btn-orange\\\" id=\\\"btnConfirmFinalise\\\" onclick=\\\"confirmAndFinalise()\\\" style=\\\"flex:2;\\\">\u001b[39m\n\u001b[31m        ✓ Finalise Dispatch\u001b[39m\n\u001b[31m      </button>\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m  </div>\u001b[39m\n\u001b[31m</div>·\u001b[39m\n\u001b[31m</div><!-- /app -->·\u001b[39m\n\u001b[31m<!-- ════ DISPATCH DETAIL SHEET ════ -->\u001b[39m\n\u001b[31m<div class=\\\"overlay\\\" id=\\\"detailOverlay\\\" onclick=\\\"closeOverlay('detailOverlay')\\\">\u001b[39m\n\u001b[31m  <div class=\\\"sheet\\\" onclick=\\\"event.stopPropagation()\\\">\u001b[39m\n\u001b[31m    <div class=\\\"sheet-handle\\\"></div>\u001b[39m\n\u001b[31m    <div id=\\\"detailContent\\\"></div>\u001b[39m\n\u001b[31m  </div>\u001b[39m\n\u001b[31m</div>·\u001b[39m\n\u001b[31m<!-- ════ COURIER MASTER EDITOR ════ -->\u001b[39m\n\u001b[31m<div class=\\\"overlay\\\" id=\\\"courierMasterOverlay\\\" onclick=\\\"closeOverlay('courierMasterOverlay')\\\">\u001b[39m\n\u001b[31m  <div class=\\\"sheet\\\" onclick=\\\"event.stopPropagation()\\\">\u001b[39m\n\u001b[31m    <div class=\\\"sheet-handle\\\"></div>\u001b[39m\n\u001b[31m    <div class=\\\"sheet-title\\\">✏️ Courier Master</div>\u001b[39m\n\u001b[31m    <div id=\\\"courierMasterEditor\\\"></div>\u001b[39m\n\u001b[31m  </div>\u001b[39m\n\u001b[31m</div>·\u001b[39m\n\u001b[31m<!-- ════ MANUAL TRACKING OVERLAY ════ -->\u001b[39m\n\u001b[31m<div class=\\\"overlay\\\" id=\\\"manualTrackOverlay\\\" onclick=\\\"closeOverlay('manualTrackOverlay')\\\" data-docket=\\\"\\\" data-dispatch-id=\\\"\\\">\u001b[39m\n\u001b[31m  <div class=\\\"sheet\\\" onclick=\\\"event.stopPropagation()\\\">\u001b[39m\n\u001b[31m    <div class=\\\"sheet-handle\\\"></div>\u001b[39m\n\u001b[31m    <div class=\\\"sheet-title\\\">📞 Manual Tracking Log</div>\u001b[39m\n\u001b[31m    <div style=\\\"background:var(--sur2);border:1px solid var(--bdr);border-radius:var(--r-sm);\u001b[39m\n\u001b[31m      padding:8px 12px;margin-bottom:12px;font-family:var(--fnt-mono);font-size:.82rem;color:var(--acc);\\\"\u001b[39m\n\u001b[31m      id=\\\"mtDocketDisplay\\\"></div>\u001b[39m\n\u001b[31m    <div class=\\\"row2\\\">\u001b[39m\n\u001b[31m      <div class=\\\"f\\\">\u001b[39m\n\u001b[31m        <label>Status Reported <span class=\\\"req\\\">*</span></label>\u001b[39m\n\u001b[31m        <select id=\\\"mtStatus\\\">\u001b[39m\n\u001b[31m          <option value=\\\"\\\">Select...</option>\u001b[39m\n\u001b[31m          <option value=\\\"delivered\\\">✅ Delivered</option>\u001b[39m\n\u001b[31m          <option value=\\\"not received\\\">❌ Not Received</option>\u001b[39m\n\u001b[31m          <option value=\\\"in transit\\\">🚚 In Transit / On the way</option>\u001b[39m\n\u001b[31m          <option value=\\\"will deliver tomorrow\\\">📅 Will Deliver Tomorrow</option>\u001b[39m\n\u001b[31m          <option value=\\\"refused\\\">🚫 Refused / Returned</option>\u001b[39m\n\u001b[31m          <option value=\\\"partial\\\">📦 Partial Delivery</option>\u001b[39m\n\u001b[31m        </select>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      <div class=\\\"f\\\">\u001b[39m\n\u001b[31m        <label>Called By (Staff) <span class=\\\"req\\\">*</span></label>\u001b[39m\n\u001b[31m        <input type=\\\"text\\\" id=\\\"mtCalledBy\\\" placeholder=\\\"Your name\\\" list=\\\"staffList\\\">\u001b[39m\n\u001b[31m        <datalist id=\\\"staffList\\\">\u001b[39m\n\u001b[31m          <option>Mukesh</option><option>Sachin</option><option>Shambhu</option>\u001b[39m\n\u001b[31m          <option>Umashankar</option><option>Narender</option><option>Rakesh</option>\u001b[39m\n\u001b[31m          <option>Gurpreet</option><option>Ashok</option>\u001b[39m\n\u001b[31m        </datalist>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m    <div class=\\\"row2\\\">\u001b[39m\n\u001b[31m      <div class=\\\"f\\\">\u001b[39m\n\u001b[31m        <label>Contact Number Used</label>\u001b[39m\n\u001b[31m        <input type=\\\"tel\\\" id=\\\"mtContactUsed\\\" placeholder=\\\"Mobile number called\\\">\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      <div class=\\\"f\\\">\u001b[39m\n\u001b[31m        <label>Person Who Confirmed</label>\u001b[39m\n\u001b[31m        <input type=\\\"text\\\" id=\\\"mtContactPerson\\\" placeholder=\\\"Name of person at store\\\">\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m    <div class=\\\"f\\\">\u001b[39m\n\u001b[31m      <label>Remarks</label>\u001b[39m\n\u001b[31m      <textarea id=\\\"mtRemarks\\\" placeholder=\\\"Any notes, delivery details, issues...\\\" style=\\\"min-height:60px;\\\"></textarea>\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m    <div class=\\\"f\\\">\u001b[39m\n\u001b[31m      <label>Call Date &amp; Time</label>\u001b[39m\n\u001b[31m      <input type=\\\"datetime-local\\\" id=\\\"mtCallTime\\\">\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m    <div class=\\\"btn-row\\\">\u001b[39m\n\u001b[31m      <button class=\\\"btn btn-ghost\\\" onclick=\\\"closeOverlay('manualTrackOverlay')\\\">Cancel</button>\u001b[39m\n\u001b[31m      <button class=\\\"btn btn-orange\\\" onclick=\\\"submitManualTracking()\\\">💾 Log &amp; Save</button>\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m  </div>\u001b[39m\n\u001b[31m</div>·\u001b[39m\n\u001b[31m<!-- ════ POD UPLOAD OVERLAY ════ -->\u001b[39m\n\u001b[31m<div class=\\\"overlay\\\" id=\\\"podOverlay\\\" onclick=\\\"closeOverlay('podOverlay')\\\" data-docket=\\\"\\\" data-dispatch-id=\\\"\\\">\u001b[39m\n\u001b[31m  <div class=\\\"sheet\\\" onclick=\\\"event.stopPropagation()\\\">\u001b[39m\n\u001b[31m    <div class=\\\"sheet-handle\\\"></div>\u001b[39m\n\u001b[31m    <div class=\\\"sheet-title\\\">📸 Upload Proof of Delivery</div>\u001b[39m\n\u001b[31m    <div class=\\\"f\\\">\u001b[39m\n\u001b[31m      <label>POD / Delivery Slip Photo</label>\u001b[39m\n\u001b[31m      <input type=\\\"file\\\" id=\\\"podFileInput\\\" accept=\\\"image/*\\\" capture=\\\"environment\\\"\u001b[39m\n\u001b[31m        onchange=\\\"previewPOD(this,'pod')\\\" style=\\\"font-size:.8rem;color:var(--t2);\\\">\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m    <div class=\\\"f\\\">\u001b[39m\n\u001b[31m      <label>Recipient / Receiver Photo (optional)</label>\u001b[39m\n\u001b[31m      <input type=\\\"file\\\" id=\\\"recipientFileInput\\\" accept=\\\"image/*\\\" capture=\\\"environment\\\"\u001b[39m\n\u001b[31m        onchange=\\\"previewPOD(this,'recip')\\\" style=\\\"font-size:.8rem;color:var(--t2);\\\">\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m    <div id=\\\"podPreview\\\" style=\\\"display:flex;gap:8px;flex-wrap:wrap;margin-bottom:12px;\\\"></div>\u001b[39m\n\u001b[31m    <div class=\\\"btn-row\\\">\u001b[39m\n\u001b[31m      <button class=\\\"btn btn-ghost\\\" onclick=\\\"closeOverlay('podOverlay')\\\">Cancel</button>\u001b[39m\n\u001b[31m      <button class=\\\"btn btn-orange\\\" onclick=\\\"uploadPODFiles()\\\">📤 Upload & Mark Delivered</button>\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m  </div>\u001b[39m\n\u001b[31m</div>·\u001b[39m\n\u001b[31m<!-- ════ ADMIN LOGIN OVERLAY ════ -->\u001b[39m\n\u001b[31m<div class=\\\"overlay\\\" id=\\\"adminLoginOverlay\\\" onclick=\\\"closeOverlay('adminLoginOverlay')\\\">\u001b[39m\n\u001b[31m  <div class=\\\"sheet\\\" onclick=\\\"event.stopPropagation()\\\" style=\\\"max-height:320px;\\\">\u001b[39m\n\u001b[31m    <div class=\\\"sheet-handle\\\"></div>\u001b[39m\n\u001b[31m    <div class=\\\"sheet-title\\\">🔐 Admin Access</div>\u001b[39m\n\u001b[31m    <div style=\\\"font-size:.78rem;color:var(--t2);margin-bottom:14px;\\\">Config settings require admin password.</div>\u001b[39m\n\u001b[31m    <div class=\\\"f\\\">\u001b[39m\n\u001b[31m      <label>Password</label>\u001b[39m\n\u001b[31m      <input type=\\\"password\\\" id=\\\"adminPwdInput\\\" placeholder=\\\"Enter admin password\\\"\u001b[39m\n\u001b[31m        onkeydown=\\\"if(event.key==='Enter')submitAdminLogin()\\\">\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m    <div class=\\\"btn-row\\\" style=\\\"margin-top:4px;\\\">\u001b[39m\n\u001b[31m      <button class=\\\"btn btn-ghost\\\" onclick=\\\"closeOverlay('adminLoginOverlay')\\\">Cancel</button>\u001b[39m\n\u001b[31m      <button class=\\\"btn btn-orange\\\" onclick=\\\"submitAdminLogin()\\\">🔓 Login</button>\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m  </div>\u001b[39m\n\u001b[31m</div>·\u001b[39m\n\u001b[31m<!-- ════ TOAST ════ -->\u001b[39m\n\u001b[31m<div class=\\\"toast\\\" id=\\\"toast\\\"></div>·\u001b[39m\n\u001b[31m<!-- ════ SCRIPTS ════ -->\u001b[39m\n\u001b[31m<!-- SheetJS for Excel/CSV parsing -->\u001b[39m\n\u001b[31m<script src=\\\"https://cdnjs.cloudflare.com/ajax/libs/xlsx/0.18.5/xlsx.full.min.js\\\"></script>·\u001b[39m\n\u001b[31m<script type=\\\"module\\\">\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31m// OCR — Transformers.js\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31mimport { pipeline, env } from 'https://cdn.jsdelivr.net/npm/@huggingface/transformers@3.5.2/dist/transformers.min.js';·\u001b[39m\n\u001b[31m// Cache models in browser\u001b[39m\n\u001b[31menv.allowLocalModels = false;\u001b[39m\n\u001b[31menv.useBrowserCache = true;·\u001b[39m\n\u001b[31mlet ocrPipeline = null;\u001b[39m\n\u001b[31mlet ocrLoading = false;·\u001b[39m\n\u001b[31mwindow.initOCR = async function() {\u001b[39m\n\u001b[31m  if (ocrPipeline || ocrLoading) return;\u001b[39m\n\u001b[31m  ocrLoading = true;\u001b[39m\n\u001b[31m  showAIProgress('Loading OCR model (first time ~120MB)...', 5);\u001b[39m\n\u001b[31m  try {\u001b[39m\n\u001b[31m    ocrPipeline = await pipeline('image-to-text', 'Xenova/trocr-base-printed', {\u001b[39m\n\u001b[31m      progress_callback: (p) => {\u001b[39m\n\u001b[31m        if (p.progress) {\u001b[39m\n\u001b[31m          showAIProgress(`Loading: ${p.file || 'model'} (${Math.round(p.progress)}%)`, p.progress);\u001b[39m\n\u001b[31m        }\u001b[39m\n\u001b[31m      }\u001b[39m\n\u001b[31m    });\u001b[39m\n\u001b[31m    showAIProgress('OCR model ready ✓', 100);\u001b[39m\n\u001b[31m    setTimeout(() => hideAIProgress(), 1500);\u001b[39m\n\u001b[31m    showToast('OCR ready ✓', 'ok');\u001b[39m\n\u001b[31m  } catch(e) {\u001b[39m\n\u001b[31m    console.warn('Transformers.js OCR failed:', e);\u001b[39m\n\u001b[31m    hideAIProgress();\u001b[39m\n\u001b[31m    showToast('OCR model load failed — will use Ollama fallback', 'info');\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m  ocrLoading = false;\u001b[39m\n\u001b[31m};·\u001b[39m\n\u001b[31mwindow.runOCROnImage = async function(imageData, filename) {\u001b[39m\n\u001b[31m  const cfg = window.appConfig;·\u001b[39m\n\u001b[31m  // Strategy 1: Transformers.js\u001b[39m\n\u001b[31m  if ((cfg.ocr_mode === 'transformers' || cfg.ocr_mode === 'both') && ocrPipeline) {\u001b[39m\n\u001b[31m    try {\u001b[39m\n\u001b[31m      const result = await ocrPipeline(imageData);\u001b[39m\n\u001b[31m      const text = result[0]?.generated_text || '';\u001b[39m\n\u001b[31m      return extractDocketFromText(text, 'transformers');\u001b[39m\n\u001b[31m    } catch(e) { /* fall through */ }\u001b[39m\n\u001b[31m  }·\u001b[39m\n\u001b[31m  // Strategy 2: Dispatch AI proxy → Ollama LLaVA (server-side, avoids CORS + port issues)\u001b[39m\n\u001b[31m  if (cfg.ocr_mode === 'ollama' || cfg.ocr_mode === 'both') {\u001b[39m\n\u001b[31m    try {\u001b[39m\n\u001b[31m      const resp = await fetch('/dispatch-ai/ocr', {\u001b[39m\n\u001b[31m        method: 'POST',\u001b[39m\n\u001b[31m        headers: { 'Content-Type': 'application/json' },\u001b[39m\n\u001b[31m        body: JSON.stringify({ imageBase64: imageData }),\u001b[39m\n\u001b[31m        signal: AbortSignal.timeout(180000)\u001b[39m\n\u001b[31m      });\u001b[39m\n\u001b[31m      const data = await resp.json();\u001b[39m\n\u001b[31m      if (data.success && data.docket) {\u001b[39m\n\u001b[31m        return { docket: data.docket, confidence: data.confidence, source: 'ollama' };\u001b[39m\n\u001b[31m      }\u001b[39m\n\u001b[31m    } catch(e) { /* fall through */ }\u001b[39m\n\u001b[31m  }·\u001b[39m\n\u001b[31m  return { docket: '', confidence: 0, source: 'failed' };\u001b[39m\n\u001b[31m};·\u001b[39m\n\u001b[31mfunction extractDocketFromText(text, source) {\u001b[39m\n\u001b[31m  // Pattern: typical courier docket = 8-15 digit numbers, sometimes with letters\u001b[39m\n\u001b[31m  const patterns = [\u001b[39m\n\u001b[31m    /\\\\b([A-Z]{0,3}\\\\d{8,15}[A-Z]{0,3})\\\\b/g,   // alphanumeric tracking\u001b[39m\n\u001b[31m    /\\\\b(\\\\d{9,15})\\\\b/g,                          // pure numeric\u001b[39m\n\u001b[31m    /\\\\b([A-Z]{2,4}\\\\d{6,12})\\\\b/g,               // letter prefix\u001b[39m\n\u001b[31m  ];\u001b[39m\n\u001b[31m  const candidates = [];\u001b[39m\n\u001b[31m  for (const pat of patterns) {\u001b[39m\n\u001b[31m    let m;\u001b[39m\n\u001b[31m    while ((m = pat.exec(text)) !== null) {\u001b[39m\n\u001b[31m      if (!candidates.includes(m[1])) candidates.push(m[1]);\u001b[39m\n\u001b[31m    }\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m  if (candidates.length === 0) return { docket: text.trim().slice(0, 20), confidence: 0.3, source };\u001b[39m\n\u001b[31m  // Pick longest candidate (usually most complete docket)\u001b[39m\n\u001b[31m  const best = candidates.sort((a, b) => b.length - a.length)[0];\u001b[39m\n\u001b[31m  const confidence = candidates.length === 1 ? 0.85 : 0.7;\u001b[39m\n\u001b[31m  return { docket: best, confidence, source };\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// Expose for non-module code\u001b[39m\n\u001b[31mwindow._ocrReady = () => ocrPipeline !== null;\u001b[39m\n\u001b[31m</script>·\u001b[39m\n\u001b[31m<script>\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31m// STATE & STORAGE\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31mlet dispatches = [];\u001b[39m\n\u001b[31mlet stores = [];\u001b[39m\n\u001b[31mlet slipOCRData = [];\u001b[39m\n\u001b[31mlet dashFilter = 'all';·\u001b[39m\n\u001b[31m// Only sheets_url is persisted locally — all other config comes from the sheet\u001b[39m\n\u001b[31mlet _trackingInProgress = false;\u001b[39m\n\u001b[31mwindow.appConfig = {\u001b[39m\n\u001b[31m  sheets_url:      'https://script.google.com/macros/s/AKfycbys8HrToDv2u_dRvSFnNqmO9bnyAO1s8oSprUydpVQnNQutq4tuUY71cVCH24urbvM9/exec',\u001b[39m\n\u001b[31m  webhook_url:     '',\u001b[39m\n\u001b[31m  proxy_url:       '',\u001b[39m\n\u001b[31m  ollama_url:      'http://srv1111289.hstgr.cloud:11434',\u001b[39m\n\u001b[31m  ocr_mode:        'both',\u001b[39m\n\u001b[31m  company:         '360 Degree Logical Marketing',\u001b[39m\n\u001b[31m  city:            'Chandigarh',\u001b[39m\n\u001b[31m  auto_log:        'true',\u001b[39m\n\u001b[31m  refresh_on_load: 'false',\u001b[39m\n\u001b[31m};·\u001b[39m\n\u001b[31mconst COURIER_MASTER = {\u001b[39m\n\u001b[31m  'SkyKing': {\u001b[39m\n\u001b[31m    trackUrl: 'https://skyking.co/track?cno={docket}',\u001b[39m\n\u001b[31m    trackBulkUrl: 'https://skyking.co/track',\u001b[39m\n\u001b[31m    apiProxy: '/track-proxy/skyking?cno={docket}',\u001b[39m\n\u001b[31m    maxBulk: 10\u001b[39m\n\u001b[31m  },\u001b[39m\n\u001b[31m  'DTDC': {\u001b[39m\n\u001b[31m    trackUrl: 'https://www.dtdc.in/tracking.asp',\u001b[39m\n\u001b[31m    queryParam: 'trkId',\u001b[39m\n\u001b[31m    maxBulk: 1\u001b[39m\n\u001b[31m  },\u001b[39m\n\u001b[31m  'Delhivery': {\u001b[39m\n\u001b[31m    trackUrl: 'https://www.delhivery.com/track/package/{docket}',\u001b[39m\n\u001b[31m    maxBulk: 1\u001b[39m\n\u001b[31m  },\u001b[39m\n\u001b[31m  'Blue Dart': {\u001b[39m\n\u001b[31m    trackUrl: 'https://www.bluedart.com/tracking',\u001b[39m\n\u001b[31m    maxBulk: 1\u001b[39m\n\u001b[31m  },\u001b[39m\n\u001b[31m  'FedEx': {\u001b[39m\n\u001b[31m    trackUrl: 'https://www.fedex.com/fedextrack/?trknbr={docket}',\u001b[39m\n\u001b[31m    maxBulk: 10\u001b[39m\n\u001b[31m  },\u001b[39m\n\u001b[31m  'Ecom Express': {\u001b[39m\n\u001b[31m    trackUrl: 'https://ecomexpress.in/tracking/?awb_field={docket}',\u001b[39m\n\u001b[31m    maxBulk: 1\u001b[39m\n\u001b[31m  },\u001b[39m\n\u001b[31m  'India Post': {\u001b[39m\n\u001b[31m    trackUrl: 'https://www.indiapost.gov.in/VAS/Pages/trackconsignment.aspx',\u001b[39m\n\u001b[31m    maxBulk: 1\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m};·\u001b[39m\n\u001b[31mfunction loadData() {\u001b[39m\n\u001b[31m  try {\u001b[39m\n\u001b[31m    dispatches = JSON.parse(localStorage.getItem('360lm_disp_v2') || '[]');\u001b[39m\n\u001b[31m    const saved = JSON.parse(localStorage.getItem('360lm_disp_sheets_url') || '{}');\u001b[39m\n\u001b[31m    if (saved.sheets_url) window.appConfig.sheets_url = saved.sheets_url;\u001b[39m\n\u001b[31m  } catch(e) {}\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31mfunction saveData() {\u001b[39m\n\u001b[31m  try {\u001b[39m\n\u001b[31m    localStorage.setItem('360lm_disp_v2', JSON.stringify(dispatches));\u001b[39m\n\u001b[31m  } catch(e) {\u001b[39m\n\u001b[31m    if (e.name === 'QuotaExceededError' || e.code === 22) {\u001b[39m\n\u001b[31m      showToast('Storage full — old dispatches cleared. Data is safe in Postgres.', 'err');\u001b[39m\n\u001b[31m      // Drop delivered dispatches older than 30 days to free space\u001b[39m\n\u001b[31m      const cutoff = Date.now() - 30 * 24 * 3600 * 1000;\u001b[39m\n\u001b[31m      dispatches = dispatches.filter(d =>\u001b[39m\n\u001b[31m        d.status !== 'draft' && (\u001b[39m\n\u001b[31m          d.status !== 'active' ||\u001b[39m\n\u001b[31m          new Date(d.createdAt || 0).getTime() > cutoff\u001b[39m\n\u001b[31m        ) ? true :\u001b[39m\n\u001b[31m        d.status === 'draft' || new Date(d.createdAt || 0).getTime() > cutoff\u001b[39m\n\u001b[31m      );\u001b[39m\n\u001b[31m      try { localStorage.setItem('360lm_disp_v2', JSON.stringify(dispatches)); } catch(_) {}\u001b[39m\n\u001b[31m    }\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// ── Postgres DB sync ──────────────────────────────────────────────────────\u001b[39m\n\u001b[31mconst _DB        = '/db';\u001b[39m\n\u001b[31m// PostgREST v12: Accept-Profile for reads, Content-Profile for writes\u001b[39m\n\u001b[31mconst _DHDR_READ  = { 'Accept-Profile': 'dispatch' };\u001b[39m\n\u001b[31mconst _DHDR_WRITE = { 'Content-Type': 'application/json', 'Content-Profile': 'dispatch', 'Accept-Profile': 'dispatch' };\u001b[39m\n\u001b[31mconst _DHDR_UPSERT = { ..._DHDR_WRITE, 'Prefer': 'resolution=merge-duplicates,return=minimal' };·\u001b[39m\n\u001b[31mfunction _dispRow(d) {\u001b[39m\n\u001b[31m  return {\u001b[39m\n\u001b[31m    dispatch_id:    d.id,\u001b[39m\n\u001b[31m    client:         d.client    || '',\u001b[39m\n\u001b[31m    job_ref:        d.jobref    || null,\u001b[39m\n\u001b[31m    courier:        d.courier   || '',\u001b[39m\n\u001b[31m    dispatch_date:  d.dispatchDate || null,\u001b[39m\n\u001b[31m    status:         d.status    || 'active',\u001b[39m\n\u001b[31m    total_count:    (d.consignees || []).length,\u001b[39m\n\u001b[31m    docketed_count: (d.consignees || []).filter(c => c.docket).length,\u001b[39m\n\u001b[31m    remarks:        d.remarks   || null,\u001b[39m\n\u001b[31m    created_by:     window._hubUser?.id || 'unknown',\u001b[39m\n\u001b[31m  };\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction _consRow(c, dispatchId, idx) {\u001b[39m\n\u001b[31m  return {\u001b[39m\n\u001b[31m    cons_id:            `${dispatchId}-${idx}`,\u001b[39m\n\u001b[31m    dispatch_id:        dispatchId,\u001b[39m\n\u001b[31m    seq_no:             idx,\u001b[39m\n\u001b[31m    name:               c.name          || '',\u001b[39m\n\u001b[31m    city:               c.city          || null,\u001b[39m\n\u001b[31m    address:            c.address       || null,\u001b[39m\n\u001b[31m    phone:              c.phone         || null,\u001b[39m\n\u001b[31m    pincode:            c.pincode       || null,\u001b[39m\n\u001b[31m    material:           c.material      || null,\u001b[39m\n\u001b[31m    qty:                c.qty           || null,\u001b[39m\n\u001b[31m    docket:             c.docket        || null,\u001b[39m\n\u001b[31m    status:             c.status        || 'pending',\u001b[39m\n\u001b[31m    last_event:         c.lastEvent     || null,\u001b[39m\n\u001b[31m    last_scan_location: c.lastScanLocation || null,\u001b[39m\n\u001b[31m    last_tracked_at:    c.lastTrackedAt || null,\u001b[39m\n\u001b[31m    manual_status:      c.manualStatus  || null,\u001b[39m\n\u001b[31m    manual_delivered:   !!c.manualDelivered,\u001b[39m\n\u001b[31m    manual_called_by:   c.manualCalledBy || null,\u001b[39m\n\u001b[31m    manual_remarks:     c.manualRemarks || null,\u001b[39m\n\u001b[31m    pod_uploaded:       !!c.podUploaded,\u001b[39m\n\u001b[31m  };\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31masync function syncDispatchToDB(d) {\u001b[39m\n\u001b[31m  if (!d || d.status === 'draft') return; // never sync drafts\u001b[39m\n\u001b[31m  try {\u001b[39m\n\u001b[31m    await fetch(`${_DB}/dispatches`, {\u001b[39m\n\u001b[31m      method: 'POST', headers: _DHDR_UPSERT,\u001b[39m\n\u001b[31m      body: JSON.stringify(_dispRow(d)),\u001b[39m\n\u001b[31m    });\u001b[39m\n\u001b[31m    const rows = (d.consignees || []).map((c, i) => _consRow(c, d.id, i));\u001b[39m\n\u001b[31m    if (rows.length) {\u001b[39m\n\u001b[31m      await fetch(`${_DB}/consignees`, {\u001b[39m\n\u001b[31m        method: 'POST', headers: _DHDR_UPSERT,\u001b[39m\n\u001b[31m        body: JSON.stringify(rows),\u001b[39m\n\u001b[31m      });\u001b[39m\n\u001b[31m    }\u001b[39m\n\u001b[31m  } catch(e) { console.warn('[dispatch-db] sync error:', e.message); }\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31masync function syncConsigneeToDB(c, dispatchId, idx) {\u001b[39m\n\u001b[31m  try {\u001b[39m\n\u001b[31m    await fetch(`${_DB}/consignees`, {\u001b[39m\n\u001b[31m      method: 'POST', headers: _DHDR_UPSERT,\u001b[39m\n\u001b[31m      body: JSON.stringify(_consRow(c, dispatchId, idx)),\u001b[39m\n\u001b[31m    });\u001b[39m\n\u001b[31m    // Log tracking event\u001b[39m\n\u001b[31m    if (c.lastEvent && c.status) {\u001b[39m\n\u001b[31m      fetch(`${_DB}/tracking_events`, {\u001b[39m\n\u001b[31m        method: 'POST', headers: _DHDR_WRITE,\u001b[39m\n\u001b[31m        body: JSON.stringify({\u001b[39m\n\u001b[31m          docket: c.docket, dispatch_id: dispatchId,\u001b[39m\n\u001b[31m          cons_id: `${dispatchId}-${idx}`,\u001b[39m\n\u001b[31m          status: c.status, event_text: c.lastEvent,\u001b[39m\n\u001b[31m          scan_location: c.lastScanLocation || null,\u001b[39m\n\u001b[31m        }),\u001b[39m\n\u001b[31m      }).catch(() => {});\u001b[39m\n\u001b[31m    }\u001b[39m\n\u001b[31m  } catch(e) { console.warn('[dispatch-db] consignee sync error:', e.message); }\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31masync function loadFromDB() {\u001b[39m\n\u001b[31m  try {\u001b[39m\n\u001b[31m    const [dResp, cResp] = await Promise.all([\u001b[39m\n\u001b[31m      fetch(`${_DB}/dispatches?order=created_at.desc&limit=100`, { headers: _DHDR_READ }),\u001b[39m\n\u001b[31m      fetch(`${_DB}/consignees?order=dispatch_id,seq_no`, { headers: _DHDR_READ }),\u001b[39m\n\u001b[31m    ]);\u001b[39m\n\u001b[31m    if (!dResp.ok) return;\u001b[39m\n\u001b[31m    const dbDispatches = await dResp.json();\u001b[39m\n\u001b[31m    const dbConsignees = cResp.ok ? await cResp.json() : [];\u001b[39m\n\u001b[31m    if (!dbDispatches.length) return;·\u001b[39m\n\u001b[31m    const dbIds = new Set(dbDispatches.map(d => d.dispatch_id));\u001b[39m\n\u001b[31m    const localOnly = dispatches.filter(d => !dbIds.has(d.id));·\u001b[39m\n\u001b[31m    const fromDB = dbDispatches.map(d => {\u001b[39m\n\u001b[31m      const cons = dbConsignees.filter(c => c.dispatch_id === d.dispatch_id);\u001b[39m\n\u001b[31m      return {\u001b[39m\n\u001b[31m        id:           d.dispatch_id,\u001b[39m\n\u001b[31m        client:       d.client,\u001b[39m\n\u001b[31m        jobref:       d.job_ref,\u001b[39m\n\u001b[31m        courier:      d.courier,\u001b[39m\n\u001b[31m        dispatchDate: d.dispatch_date,\u001b[39m\n\u001b[31m        status:       d.status,\u001b[39m\n\u001b[31m        remarks:      d.remarks,\u001b[39m\n\u001b[31m        createdAt:    d.created_at,\u001b[39m\n\u001b[31m        consignees:   cons.map(c => ({\u001b[39m\n\u001b[31m          name:              c.name,\u001b[39m\n\u001b[31m          city:              c.city,\u001b[39m\n\u001b[31m          address:           c.address,\u001b[39m\n\u001b[31m          phone:             c.phone,\u001b[39m\n\u001b[31m          pincode:           c.pincode,\u001b[39m\n\u001b[31m          material:          c.material,\u001b[39m\n\u001b[31m          qty:               c.qty,\u001b[39m\n\u001b[31m          docket:            c.docket,\u001b[39m\n\u001b[31m          status:            c.status,\u001b[39m\n\u001b[31m          lastEvent:         c.last_event,\u001b[39m\n\u001b[31m          lastScanLocation:  c.last_scan_location,\u001b[39m\n\u001b[31m          lastTrackedAt:     c.last_tracked_at,\u001b[39m\n\u001b[31m          manualStatus:      c.manual_status,\u001b[39m\n\u001b[31m          manualDelivered:   c.manual_delivered,\u001b[39m\n\u001b[31m          manualCalledBy:    c.manual_called_by,\u001b[39m\n\u001b[31m          manualRemarks:     c.manual_remarks,\u001b[39m\n\u001b[31m          podUploaded:       c.pod_uploaded,\u001b[39m\n\u001b[31m          _dbIdx:            c.seq_no,\u001b[39m\n\u001b[31m        })),\u001b[39m\n\u001b[31m      };\u001b[39m\n\u001b[31m    });·\u001b[39m\n\u001b[31m    dispatches = [...fromDB, ...localOnly];\u001b[39m\n\u001b[31m    saveData();\u001b[39m\n\u001b[31m    renderDashboard();\u001b[39m\n\u001b[31m    updateHistCount();\u001b[39m\n\u001b[31m  } catch(e) { console.warn('[dispatch-db] load error:', e.message); }\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31mfunction saveSheetsUrlLocal() {\u001b[39m\n\u001b[31m  localStorage.setItem('360lm_disp_sheets_url', JSON.stringify({ sheets_url: window.appConfig.sheets_url }));\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m/**\u001b[39m\n\u001b[31m * Fetch config.json from the VPS server (same origin).\u001b[39m\n\u001b[31m * This file contains the sheets_url — no manual entry needed.\u001b[39m\n\u001b[31m * Falls back to localStorage cache if fetch fails (offline).\u001b[39m\n\u001b[31m */\u001b[39m\n\u001b[31masync function loadServerConfig() {\u001b[39m\n\u001b[31m  try {\u001b[39m\n\u001b[31m    const resp = await fetch('./config.json', { cache: 'no-store' });\u001b[39m\n\u001b[31m    if (resp.ok) {\u001b[39m\n\u001b[31m      const cfg = await resp.json();\u001b[39m\n\u001b[31m      if (cfg.sheets_url) {\u001b[39m\n\u001b[31m        window.appConfig.sheets_url = cfg.sheets_url;\u001b[39m\n\u001b[31m        saveSheetsUrlLocal(); // cache it for offline use\u001b[39m\n\u001b[31m        document.getElementById('cfg_sheets_url').value = cfg.sheets_url;\u001b[39m\n\u001b[31m        return true;\u001b[39m\n\u001b[31m      }\u001b[39m\n\u001b[31m    }\u001b[39m\n\u001b[31m  } catch(e) {\u001b[39m\n\u001b[31m    console.log('config.json fetch failed, using cached URL:', e.message);\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m  return false;\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31m// INIT\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31m// ── Hub auth gate ─────────────────────────────────────────────────────────\u001b[39m\n\u001b[31mwindow._hubUser = null;\u001b[39m\n\u001b[31m(function() {\u001b[39m\n\u001b[31m  try {\u001b[39m\n\u001b[31m    const hubLogin = '/hub/?next=' + encodeURIComponent(location.pathname + location.search);\u001b[39m\n\u001b[31m    const s = JSON.parse(localStorage.getItem('lm360-session') || 'null');\u001b[39m\n\u001b[31m    if (!s || !s.empId || !s.name) { window.location.replace(hubLogin); return; }\u001b[39m\n\u001b[31m    if (Date.now() - new Date(s.loginAt).getTime() > 12 * 3600 * 1000) {\u001b[39m\n\u001b[31m      window.location.replace(hubLogin); return;\u001b[39m\n\u001b[31m    }\u001b[39m\n\u001b[31m    window._hubUser = { id: s.empId, name: s.name, role: s.role };\u001b[39m\n\u001b[31m  } catch(_) { window.location.replace('/hub/?next=' + encodeURIComponent(location.pathname + location.search)); }\u001b[39m\n\u001b[31m})();·\u001b[39m\n\u001b[31mwindow.addEventListener('load', async () => {\u001b[39m\n\u001b[31m  if (!window._hubUser) return; // should never reach here — gate redirected already\u001b[39m\n\u001b[31m  setTimeout(() => {\u001b[39m\n\u001b[31m    document.getElementById('splash').style.opacity = '0';\u001b[39m\n\u001b[31m    setTimeout(() => {\u001b[39m\n\u001b[31m      document.getElementById('splash').style.display = 'none';\u001b[39m\n\u001b[31m      document.getElementById('app').style.display = 'flex';\u001b[39m\n\u001b[31m    }, 450);\u001b[39m\n\u001b[31m  }, 1400);\u001b[39m\n\u001b[31m  loadData();\u001b[39m\n\u001b[31m  // Auto-authenticate config tab for admin users (no password needed)\u001b[39m\n\u001b[31m  if (['harish', 'pramod'].includes(window._hubUser?.id)) _adminAuthed = true;\u001b[39m\n\u001b[31m  setToday();\u001b[39m\n\u001b[31m  addConsignee();\u001b[39m\n\u001b[31m  updateHistCount();\u001b[39m\n\u001b[31m  setInterval(updateNetDot, 5000);\u001b[39m\n\u001b[31m  updateNetDot();\u001b[39m\n\u001b[31m  checkShareTarget();\u001b[39m\n\u001b[31m  setupDragDrop();·\u001b[39m\n\u001b[31m  // Step 1: fetch sheets_url from VPS config.json (auto, no user input)\u001b[39m\n\u001b[31m  await loadServerConfig();·\u001b[39m\n\u001b[31m  // Step 2: populate config tab field\u001b[39m\n\u001b[31m  document.getElementById('cfg_sheets_url').value = window.appConfig.sheets_url || '';·\u001b[39m\n\u001b[31m  // Step 3: load dispatches from Postgres in background (merge with local)\u001b[39m\n\u001b[31m  loadFromDB(); // non-blocking — updates UI when data arrives·\u001b[39m\n\u001b[31m  // Step 4: sync full config from Google Sheet silently\u001b[39m\n\u001b[31m  if (window.appConfig.sheets_url) {\u001b[39m\n\u001b[31m    syncFromSheet(true); // silent on startup\u001b[39m\n\u001b[31m  } else {\u001b[39m\n\u001b[31m    // Only show config tab if no URL found anywhere\u001b[39m\n\u001b[31m    showPane('config');\u001b[39m\n\u001b[31m    showToast('No config.json found on server', 'err');\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m  // Pre-warm OCR after delay\u001b[39m\n\u001b[31m  setTimeout(() => { if (typeof window.initOCR === 'function') window.initOCR(); }, 4000);\u001b[39m\n\u001b[31m});·\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31m// WEB SHARE TARGET\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31masync function checkShareTarget() {\u001b[39m\n\u001b[31m  // Check URL params for share target\u001b[39m\n\u001b[31m  const params = new URLSearchParams(window.location.search);\u001b[39m\n\u001b[31m  const action = params.get('action');\u001b[39m\n\u001b[31m  if (action !== 'share') return;·\u001b[39m\n\u001b[31m  // Check for shared files via Share Target API\u001b[39m\n\u001b[31m  if ('launchQueue' in window) {\u001b[39m\n\u001b[31m    window.launchQueue.setConsumer(async launchParams => {\u001b[39m\n\u001b[31m      if (!launchParams.files || launchParams.files.length === 0) return;\u001b[39m\n\u001b[31m      const files = await Promise.all(launchParams.files.map(f => f.getFile()));\u001b[39m\n\u001b[31m      const imageFiles = files.filter(f => f.type.startsWith('image/'));\u001b[39m\n\u001b[31m      if (imageFiles.length > 0) {\u001b[39m\n\u001b[31m        document.getElementById('shareTargetInfo').style.display = 'flex';\u001b[39m\n\u001b[31m        document.getElementById('sharedCount').textContent = imageFiles.length;\u001b[39m\n\u001b[31m        document.getElementById('shareBanner').classList.add('show');\u001b[39m\n\u001b[31m        setTimeout(() => document.getElementById('shareBanner').classList.remove('show'), 4000);\u001b[39m\n\u001b[31m        showPane('new');\u001b[39m\n\u001b[31m        await handleSlipFiles(imageFiles);\u001b[39m\n\u001b[31m      }\u001b[39m\n\u001b[31m    });\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction dismissShare() {\u001b[39m\n\u001b[31m  document.getElementById('shareBanner').classList.remove('show');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31m// DRAG & DROP\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31mfunction setupDragDrop() {\u001b[39m\n\u001b[31m  const zone = document.getElementById('slipZone');\u001b[39m\n\u001b[31m  zone.addEventListener('dragover', e => { e.preventDefault(); zone.classList.add('drag'); });\u001b[39m\n\u001b[31m  zone.addEventListener('dragleave', () => zone.classList.remove('drag'));\u001b[39m\n\u001b[31m  zone.addEventListener('drop', e => {\u001b[39m\n\u001b[31m    e.preventDefault();\u001b[39m\n\u001b[31m    zone.classList.remove('drag');\u001b[39m\n\u001b[31m    const files = Array.from(e.dataTransfer.files).filter(f => f.type.startsWith('image/'));\u001b[39m\n\u001b[31m    if (files.length) handleSlipFiles(files);\u001b[39m\n\u001b[31m  });\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31m// SLIP OCR HANDLER\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31masync function handleSlipFiles(files) {\u001b[39m\n\u001b[31m  const fileArr = Array.from(files);\u001b[39m\n\u001b[31m  if (!fileArr.length) return;\u001b[39m\n\u001b[31m  // If dockets already mapped from store list — allow upload but skip OCR extraction\u001b[39m\n\u001b[31m  const skipOCR = shouldSkipOCR();\u001b[39m\n\u001b[31m  if (skipOCR) {\u001b[39m\n\u001b[31m    showToast('Dockets already mapped — slips saved for reference only', 'info');\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m  showOCRWorking(true, `Processing ${fileArr.length} slip(s)...`);·\u001b[39m\n\u001b[31m  for (let i = 0; i < fileArr.length; i++) {\u001b[39m\n\u001b[31m    const file = fileArr[i];\u001b[39m\n\u001b[31m    showOCRWorking(true, `OCR: ${i+1}/${fileArr.length} — ${file.name}`);\u001b[39m\n\u001b[31m    const imageData = await readFileAsDataURL(file);\u001b[39m\n\u001b[31m    let result = { docket: '', confidence: 0, source: 'manual' };\u001b[39m\n\u001b[31m    if (!skipOCR) {\u001b[39m\n\u001b[31m      try {\u001b[39m\n\u001b[31m        result = await window.runOCROnImage(imageData, file.name);\u001b[39m\n\u001b[31m      } catch(e) {}\u001b[39m\n\u001b[31m    } else {\u001b[39m\n\u001b[31m      result = { docket: '', confidence: 1, source: 'skipped — docket pre-mapped' };\u001b[39m\n\u001b[31m    }\u001b[39m\n\u001b[31m    slipOCRData.push({ file, imageData, filename: file.name, ...result });\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m  showOCRWorking(false);\u001b[39m\n\u001b[31m  renderOCRResults();\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction readFileAsDataURL(file) {\u001b[39m\n\u001b[31m  return new Promise((res, rej) => {\u001b[39m\n\u001b[31m    const r = new FileReader();\u001b[39m\n\u001b[31m    r.onload = e => res(e.target.result);\u001b[39m\n\u001b[31m    r.onerror = rej;\u001b[39m\n\u001b[31m    r.readAsDataURL(file);\u001b[39m\n\u001b[31m  });\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction renderOCRResults() {\u001b[39m\n\u001b[31m  if (!slipOCRData.length) return;\u001b[39m\n\u001b[31m  const list = document.getElementById('ocrSlipList');\u001b[39m\n\u001b[31m  document.getElementById('ocrResults').classList.add('visible');·\u001b[39m\n\u001b[31m  const needReview = slipOCRData.filter(s => s.confidence < 0.65 && s.docket).length;\u001b[39m\n\u001b[31m  const autoOk     = slipOCRData.filter(s => s.confidence >= 0.88).length;\u001b[39m\n\u001b[31m  const summary    = document.getElementById('ocrSummary');\u001b[39m\n\u001b[31m  if (summary) {\u001b[39m\n\u001b[31m    summary.style.display = '';\u001b[39m\n\u001b[31m    summary.innerHTML =\u001b[39m\n\u001b[31m      `<span style=\\\"color:var(--grn)\\\">✓ ${autoOk} auto-accepted</span>` +\u001b[39m\n\u001b[31m      (needReview ? `&nbsp;·&nbsp;<span style=\\\"color:var(--ylw)\\\">⚠ ${needReview} need manual check</span>` : '');\u001b[39m\n\u001b[31m  }·\u001b[39m\n\u001b[31m  list.innerHTML = slipOCRData.map((s, i) => {\u001b[39m\n\u001b[31m    const autoAccepted = s.confidence >= 0.88 && s.docket;\u001b[39m\n\u001b[31m    const needsCheck   = s.confidence < 0.65  && s.docket;\u001b[39m\n\u001b[31m    const confClass = autoAccepted ? 'conf-high' : needsCheck ? 'conf-low' : 'conf-med';\u001b[39m\n\u001b[31m    const confLabel = autoAccepted\u001b[39m\n\u001b[31m      ? `✓ ${Math.round(s.confidence * 100)}% — auto`\u001b[39m\n\u001b[31m      : needsCheck\u001b[39m\n\u001b[31m        ? `⚠ ${Math.round(s.confidence * 100)}% — verify`\u001b[39m\n\u001b[31m        : `${Math.round(s.confidence * 100)}%`;\u001b[39m\n\u001b[31m    const borderStyle = needsCheck ? 'border:1px solid var(--ylw);' : '';\u001b[39m\n\u001b[31m    return `\u001b[39m\n\u001b[31m    <div class=\\\"ocr-slip\\\" id=\\\"ocrSlip_${i}\\\" style=\\\"${borderStyle}\\\">\u001b[39m\n\u001b[31m      <img src=\\\"${s.imageData}\\\" alt=\\\"slip ${i+1}\\\">\u001b[39m\n\u001b[31m      <div class=\\\"ocr-slip-body\\\">\u001b[39m\n\u001b[31m        <div class=\\\"ocr-slip-name\\\">${s.filename}</div>\u001b[39m\n\u001b[31m        <input type=\\\"text\\\" class=\\\"ocr-slip-docket\\\" id=\\\"ocrDocket_${i}\\\"\u001b[39m\n\u001b[31m          value=\\\"${s.docket}\\\" placeholder=\\\"Docket number\\\"\u001b[39m\n\u001b[31m          ${autoAccepted ? 'style=\\\"opacity:.6\\\"' : ''}\u001b[39m\n\u001b[31m          oninput=\\\"slipOCRData[${i}].docket=this.value;slipOCRData[${i}].confidence=0.5\\\">\u001b[39m\n\u001b[31m        <div class=\\\"ocr-slip-status\\\">\u001b[39m\n\u001b[31m          <span class=\\\"ocr-confidence ${confClass}\\\">${confLabel}</span>\u001b[39m\n\u001b[31m          <span style=\\\"color:var(--t4);font-family:var(--fnt-mono);font-size:.6rem;\\\">via ${s.source}</span>\u001b[39m\n\u001b[31m          <button class=\\\"ocr-ai-btn\\\" onclick=\\\"reOCR(${i})\\\">🔄 Re-OCR</button>\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m    </div>`;\u001b[39m\n\u001b[31m  }).join('');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31masync function reOCR(i) {\u001b[39m\n\u001b[31m  const s = slipOCRData[i];\u001b[39m\n\u001b[31m  showOCRWorking(true, `Re-running OCR on ${s.filename}...`);\u001b[39m\n\u001b[31m  try {\u001b[39m\n\u001b[31m    const result = await window.runOCROnImage(s.imageData, s.filename);\u001b[39m\n\u001b[31m    slipOCRData[i] = { ...s, ...result };\u001b[39m\n\u001b[31m    renderOCRResults();\u001b[39m\n\u001b[31m    showToast('Re-OCR complete', 'ok');\u001b[39m\n\u001b[31m  } catch(e) {\u001b[39m\n\u001b[31m    showToast('OCR failed', 'err');\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m  showOCRWorking(false);\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction clearSlips() {\u001b[39m\n\u001b[31m  slipOCRData = [];\u001b[39m\n\u001b[31m  document.getElementById('ocrResults').classList.remove('visible');\u001b[39m\n\u001b[31m  document.getElementById('ocrSlipList').innerHTML = '';\u001b[39m\n\u001b[31m  const sum = document.getElementById('ocrSummary');\u001b[39m\n\u001b[31m  if (sum) { sum.style.display = 'none'; sum.innerHTML = ''; }\u001b[39m\n\u001b[31m  document.getElementById('shareTargetInfo').style.display = 'none';\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction applyAllDockets() {\u001b[39m\n\u001b[31m  // Match each OCR result to a consignee by order or by name matching\u001b[39m\n\u001b[31m  const consList = document.getElementById('consList');\u001b[39m\n\u001b[31m  const cards = consList.querySelectorAll('.cons-card');\u001b[39m\n\u001b[31m  slipOCRData.forEach((s, i) => {\u001b[39m\n\u001b[31m    if (!s.docket) return;\u001b[39m\n\u001b[31m    const card = cards[i];\u001b[39m\n\u001b[31m    if (card) {\u001b[39m\n\u001b[31m      const docketInput = card.querySelector('.cons-docket-input');\u001b[39m\n\u001b[31m      if (docketInput) {\u001b[39m\n\u001b[31m        docketInput.value = s.docket;\u001b[39m\n\u001b[31m        showLinkedDocket(card, s.docket);\u001b[39m\n\u001b[31m      }\u001b[39m\n\u001b[31m    }\u001b[39m\n\u001b[31m  });\u001b[39m\n\u001b[31m  showToast(`Applied ${slipOCRData.filter(s=>s.docket).length} dockets ✓`, 'ok');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31m// STORE LIST — with column mapping UI\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31mlet _rawRows = [];      // raw rows from file, kept for re-mapping\u001b[39m\n\u001b[31mlet _rawHeaders = [];   // headers from file·\u001b[39m\n\u001b[31masync function handleStoreFile(input) {\u001b[39m\n\u001b[31m  const file = input.files[0];\u001b[39m\n\u001b[31m  if (!file) return;\u001b[39m\n\u001b[31m  showToast('Reading file...', 'info');·\u001b[39m\n\u001b[31m  const data = await readFileAsDataURL(file);\u001b[39m\n\u001b[31m  const base64 = data.split(',')[1];\u001b[39m\n\u001b[31m  const binary = atob(base64);\u001b[39m\n\u001b[31m  const bytes = new Uint8Array(binary.length);\u001b[39m\n\u001b[31m  for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);·\u001b[39m\n\u001b[31m  let rows = [];\u001b[39m\n\u001b[31m  try {\u001b[39m\n\u001b[31m    if (file.name.endsWith('.csv')) {\u001b[39m\n\u001b[31m      const text = new TextDecoder().decode(bytes);\u001b[39m\n\u001b[31m      rows = text.split('\\\\n').map(r => r.split(',').map(c => c.trim().replace(/^\\\"|\\\"$/g, '')));\u001b[39m\n\u001b[31m    } else {\u001b[39m\n\u001b[31m      const wb = XLSX.read(bytes, { type: 'array' });\u001b[39m\n\u001b[31m      const ws = wb.Sheets[wb.SheetNames[0]];\u001b[39m\n\u001b[31m      rows = XLSX.utils.sheet_to_json(ws, { header: 1 });\u001b[39m\n\u001b[31m    }\u001b[39m\n\u001b[31m  } catch(e) { showToast('File read error: ' + e.message, 'err'); return; }·\u001b[39m\n\u001b[31m  if (!rows.length || !rows[0]) { showToast('File appears empty', 'err'); return; }·\u001b[39m\n\u001b[31m  _rawHeaders = (rows[0] || []).map(h => (h || '').toString().trim());\u001b[39m\n\u001b[31m  _rawRows = rows.slice(1).filter(r => r && r.some(c => c !== '' && c != null));·\u001b[39m\n\u001b[31m  document.getElementById('storeTableArea').style.display = 'block';\u001b[39m\n\u001b[31m  renderMappingUI(file.name);\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// ── COLUMN MAPPING FIELDS ─────────────────────────────\u001b[39m\n\u001b[31mconst MAPPING_FIELDS = [\u001b[39m\n\u001b[31m  { key: 'name',    label: 'Store / Partner Name', required: true,\u001b[39m\n\u001b[31m    keywords: ['store','shop','name','outlet','consignee','destination','partner','dealer','client','retailer'] },\u001b[39m\n\u001b[31m  { key: 'city',    label: 'City / Location',       required: false,\u001b[39m\n\u001b[31m    keywords: ['city','town','district','location','area','region','place'] },\u001b[39m\n\u001b[31m  { key: 'phone',   label: 'Phone / Mobile',        required: false,\u001b[39m\n\u001b[31m    keywords: ['phone','mobile','contact','tel','number','mob'] },\u001b[39m\n\u001b[31m  { key: 'docket',  label: 'Docket / AWB No.',      required: false,\u001b[39m\n\u001b[31m    keywords: ['docket','awb','tracking','courier','dkt','consignment','cno','waybill'] },\u001b[39m\n\u001b[31m  { key: 'address', label: 'Address',               required: false,\u001b[39m\n\u001b[31m    keywords: ['address','addr','street','locality','area'] },\u001b[39m\n\u001b[31m  { key: 'pincode', label: 'Pincode / PIN',          required: false,\u001b[39m\n\u001b[31m    keywords: ['pin','pincode','postal','zip','code'] },\u001b[39m\n\u001b[31m];·\u001b[39m\n\u001b[31m// Saved mapping key based on column fingerprint (so same client file auto-maps)\u001b[39m\n\u001b[31mfunction getMappingKey() {\u001b[39m\n\u001b[31m  return 'colmap_' + _rawHeaders.join('|').toLowerCase().replace(/\\\\s+/g,'');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// ── Detect if a column is a combined/multiline address cell ─────────────\u001b[39m\n\u001b[31mfunction isCombinedColumn(colIdx) {\u001b[39m\n\u001b[31m  if (colIdx < 0) return false;\u001b[39m\n\u001b[31m  // Check first 3 data rows — if any cell contains newline or has 6+ digit number (pincode) = combined\u001b[39m\n\u001b[31m  const samples = _rawRows.slice(0, 3).map(r => (r[colIdx] || '').toString());\u001b[39m\n\u001b[31m  return samples.some(s => s.includes('\\\\n') || /\\\\b\\\\d{6}\\\\b/.test(s) || s.split(',').length >= 3);\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction renderMappingUI(filename) {\u001b[39m\n\u001b[31m  const saved = (() => { try { return JSON.parse(localStorage.getItem(getMappingKey()) || 'null'); } catch(e) { return null; } })();\u001b[39m\n\u001b[31m  const isSaved = !!saved;\u001b[39m\n\u001b[31m  const colInfoEl = document.getElementById('storeColInfo');\u001b[39m\n\u001b[31m  colInfoEl.style.display = 'block';·\u001b[39m\n\u001b[31m  const suggested = {};\u001b[39m\n\u001b[31m  MAPPING_FIELDS.forEach(f => {\u001b[39m\n\u001b[31m    const idx = findCol(_rawHeaders, f.keywords);\u001b[39m\n\u001b[31m    suggested[f.key] = saved ? (saved[f.key] ?? idx) : idx;\u001b[39m\n\u001b[31m  });·\u001b[39m\n\u001b[31m  // Detect if address column is combined\u001b[39m\n\u001b[31m  const addrIdx = suggested.address >= 0 ? suggested.address : findCol(_rawHeaders, ['address','addr']);\u001b[39m\n\u001b[31m  const looksCombo = isCombinedColumn(addrIdx) || isCombinedColumn(suggested.name);\u001b[39m\n\u001b[31m  const comboCol   = looksCombo ? (addrIdx >= 0 ? addrIdx : suggested.name) : -1;\u001b[39m\n\u001b[31m  const comboPreview = comboCol >= 0 && _rawRows[0] ? (_rawRows[0][comboCol] || '').toString().slice(0, 120) : '';·\u001b[39m\n\u001b[31m  colInfoEl.innerHTML = `\u001b[39m\n\u001b[31m    <div style=\\\"background:var(--sur2);border:1px solid var(--bdr);border-radius:var(--r-sm);padding:12px;\\\">\u001b[39m\n\u001b[31m      <div style=\\\"display:flex;align-items:center;justify-content:space-between;margin-bottom:10px;\\\">\u001b[39m\n\u001b[31m        <div style=\\\"font-family:var(--fnt-mono);font-size:.68rem;color:var(--sky);\\\">\u001b[39m\n\u001b[31m          📋 ${filename} &nbsp;·&nbsp; ${_rawRows.length} rows &nbsp;·&nbsp; ${_rawHeaders.length} columns\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m        ${isSaved ? '<span class=\\\"map-saved-badge\\\">✓ mapping remembered</span>' : ''}\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      <div style=\\\"font-size:.68rem;color:var(--t3);font-family:var(--fnt-mono);margin-bottom:10px;word-break:break-all;\\\">\u001b[39m\n\u001b[31m        Columns: <span style=\\\"color:var(--t2);\\\">${_rawHeaders.map((h,i)=>`[${i+1}] ${h||'?'}`).join(' · ')}</span>\u001b[39m\n\u001b[31m      </div>·\u001b[39m\n\u001b[31m      ${looksCombo ? `\u001b[39m\n\u001b[31m      <!-- COMBINED COLUMN DETECTED -->\u001b[39m\n\u001b[31m      <div style=\\\"background:rgba(56,189,248,.08);border:1px solid rgba(56,189,248,.3);\u001b[39m\n\u001b[31m        border-radius:var(--r-sm);padding:10px 12px;margin-bottom:10px;\\\">\u001b[39m\n\u001b[31m        <div style=\\\"font-size:.72rem;color:var(--sky);font-weight:600;margin-bottom:6px;\\\">\u001b[39m\n\u001b[31m          🤖 Combined Address Column Detected\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m        <div style=\\\"font-size:.68rem;color:var(--t2);font-family:var(--fnt-mono);\u001b[39m\n\u001b[31m          white-space:pre-wrap;background:var(--bg);padding:6px 8px;border-radius:4px;\u001b[39m\n\u001b[31m          margin-bottom:8px;max-height:80px;overflow:hidden;\\\">${comboPreview}</div>\u001b[39m\n\u001b[31m        <div style=\\\"font-size:.7rem;color:var(--t3);margin-bottom:8px;\\\">\u001b[39m\n\u001b[31m          AI will extract: Name · Address · City · Pincode · Phone from each cell.\u001b[39m\n\u001b[31m          Runs in browser (Transformers.js) with Ollama fallback.\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m        <div style=\\\"display:flex;align-items:center;gap:8px;\\\">\u001b[39m\n\u001b[31m          <label style=\\\"display:flex;align-items:center;gap:6px;font-size:.72rem;color:var(--t1);cursor:pointer;\\\">\u001b[39m\n\u001b[31m            <input type=\\\"checkbox\\\" id=\\\"useAIParse\\\" checked style=\\\"cursor:pointer;\\\">\u001b[39m\n\u001b[31m            Use AI to parse combined column\u001b[39m\n\u001b[31m          </label>\u001b[39m\n\u001b[31m          <select id=\\\"comboColSel\\\" style=\\\"background:var(--sur2);border:1px solid var(--bdr);\u001b[39m\n\u001b[31m            color:var(--t1);font-family:var(--fnt-mono);font-size:.72rem;padding:4px 8px;\u001b[39m\n\u001b[31m            border-radius:var(--r-sm);outline:none;\\\">\u001b[39m\n\u001b[31m            ${_rawHeaders.map((h,i)=>`<option value=\\\"${i}\\\" ${i===comboCol?'selected':''}>${h||'(col '+(i+1)+')'}</option>`).join('')}\u001b[39m\n\u001b[31m          </select>\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      ` : ''}·\u001b[39m\n\u001b[31m      <div id=\\\"manualMapSection\\\" style=\\\"${looksCombo ? 'display:none;' : ''}\\\">\u001b[39m\n\u001b[31m        <div style=\\\"font-size:.68rem;color:var(--t3);margin-bottom:6px;\\\">\u001b[39m\n\u001b[31m          ${looksCombo ? 'Manual mapping (used when AI parse is off, or for other columns):' : 'Map each field to your column:'}\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m        <table class=\\\"map-table\\\">\u001b[39m\n\u001b[31m          <thead><tr><th>Field</th><th>Your column</th><th>Preview (row 1)</th></tr></thead>\u001b[39m\n\u001b[31m          <tbody>\u001b[39m\n\u001b[31m            ${MAPPING_FIELDS.map(f => {\u001b[39m\n\u001b[31m              const selIdx = suggested[f.key] ?? -1;\u001b[39m\n\u001b[31m              const preview = selIdx >= 0 && _rawRows[0] ? ((_rawRows[0][selIdx]||'').toString().slice(0,40)) : '—';\u001b[39m\n\u001b[31m              return `<tr>\u001b[39m\n\u001b[31m                <td style=\\\"white-space:nowrap;color:var(--t1);font-weight:500;font-size:.75rem;\\\">\u001b[39m\n\u001b[31m                  ${f.label}${f.required ? '<span class=\\\"map-req\\\">*</span>' : ''}\u001b[39m\n\u001b[31m                </td>\u001b[39m\n\u001b[31m                <td>\u001b[39m\n\u001b[31m                  <select id=\\\"mapsel_${f.key}\\\" onchange=\\\"updateMappingPreview('${f.key}')\\\">\u001b[39m\n\u001b[31m                    ${_rawHeaders.map((h,i)=>\u001b[39m\n\u001b[31m                      `<option value=\\\"${i}\\\" ${i===selIdx?'selected':''}>${h||'(col '+(i+1)+')'}</option>`\u001b[39m\n\u001b[31m                    ).join('')}\u001b[39m\n\u001b[31m                    <option value=\\\"-1\\\" ${selIdx===-1?'selected':''}>— not in file —</option>\u001b[39m\n\u001b[31m                  </select>\u001b[39m\n\u001b[31m                </td>\u001b[39m\n\u001b[31m                <td id=\\\"mapprev_${f.key}\\\" style=\\\"font-family:var(--fnt-mono);font-size:.68rem;\u001b[39m\n\u001b[31m                  color:var(--t2);max-width:90px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;\\\">\u001b[39m\n\u001b[31m                  ${preview}\u001b[39m\n\u001b[31m                </td>\u001b[39m\n\u001b[31m              </tr>`;\u001b[39m\n\u001b[31m            }).join('')}\u001b[39m\n\u001b[31m          </tbody>\u001b[39m\n\u001b[31m        </table>\u001b[39m\n\u001b[31m      </div>·\u001b[39m\n\u001b[31m      <!-- Also map docket/courier no column separately — always shown -->\u001b[39m\n\u001b[31m      <div style=\\\"margin-top:10px;padding-top:10px;border-top:1px solid var(--bdr);\\\">\u001b[39m\n\u001b[31m        <div style=\\\"font-size:.68rem;color:var(--t3);margin-bottom:6px;font-family:var(--fnt-mono);\\\">\u001b[39m\n\u001b[31m          DOCKET / COURIER NO. COLUMN (required for tracking):\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m        <div style=\\\"display:flex;gap:8px;align-items:center;\\\">\u001b[39m\n\u001b[31m          <select id=\\\"docketColSel\\\" style=\\\"background:var(--sur2);border:1px solid var(--bdr);\u001b[39m\n\u001b[31m            color:var(--acc);font-family:var(--fnt-mono);font-size:.78rem;padding:6px 10px;\u001b[39m\n\u001b[31m            border-radius:var(--r-sm);outline:none;flex:1;\\\">\u001b[39m\n\u001b[31m            <option value=\\\"-1\\\">— Docket not in file —</option>\u001b[39m\n\u001b[31m            ${_rawHeaders.map((h,i)=>`<option value=\\\"${i}\\\" ${i===(suggested.docket??-1)?'selected':''}>${h||'(col '+(i+1)+')'}</option>`).join('')}\u001b[39m\n\u001b[31m          </select>\u001b[39m\n\u001b[31m          <span id=\\\"docketPrev\\\" style=\\\"font-family:var(--fnt-mono);font-size:.72rem;color:var(--acc);\\\">\u001b[39m\n\u001b[31m            ${suggested.docket >= 0 && _rawRows[0] ? (_rawRows[0][suggested.docket]||'') : ''}\u001b[39m\n\u001b[31m          </span>\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m      </div>·\u001b[39m\n\u001b[31m      <div class=\\\"btn-row\\\" style=\\\"margin-top:12px;\\\">\u001b[39m\n\u001b[31m        <button class=\\\"btn btn-ghost\\\" style=\\\"font-size:.78rem;\\\" onclick=\\\"clearStores()\\\">✕ Cancel</button>\u001b[39m\n\u001b[31m        <button class=\\\"btn btn-orange\\\" style=\\\"font-size:.78rem;\\\" onclick=\\\"applyMapping()\\\">✓ Apply Mapping</button>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m  `;\u001b[39m\n\u001b[31m  document.getElementById('storeTable').innerHTML = '';\u001b[39m\n\u001b[31m  document.getElementById('storeAddPanel').style.display = 'none';·\u001b[39m\n\u001b[31m  // Update docket preview on change\u001b[39m\n\u001b[31m  const docketSel = document.getElementById('docketColSel');\u001b[39m\n\u001b[31m  if (docketSel) docketSel.addEventListener('change', () => {\u001b[39m\n\u001b[31m    const idx = parseInt(docketSel.value);\u001b[39m\n\u001b[31m    const prev = document.getElementById('docketPrev');\u001b[39m\n\u001b[31m    if (prev) prev.textContent = idx >= 0 && _rawRows[0] ? (_rawRows[0][idx]||'') : '';\u001b[39m\n\u001b[31m  });·\u001b[39m\n\u001b[31m  // Toggle manual map opacity when AI checkbox changes\u001b[39m\n\u001b[31m  const aiChk = document.getElementById('useAIParse');\u001b[39m\n\u001b[31m  const manSec = document.getElementById('manualMapSection');\u001b[39m\n\u001b[31m  if (aiChk && manSec) {\u001b[39m\n\u001b[31m    aiChk.addEventListener('change', () => {\u001b[39m\n\u001b[31m      manSec.style.display = aiChk.checked ? 'none' : 'block';\u001b[39m\n\u001b[31m    });\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction updateMappingPreview(key) {\u001b[39m\n\u001b[31m  const sel = document.getElementById('mapsel_' + key);\u001b[39m\n\u001b[31m  if (!sel) return;\u001b[39m\n\u001b[31m  const idx = parseInt(sel.value);\u001b[39m\n\u001b[31m  const prev = document.getElementById('mapprev_' + key);\u001b[39m\n\u001b[31m  if (prev) prev.textContent = (idx >= 0 && _rawRows[0]) ? ((_rawRows[0][idx]||'').toString().slice(0,40)) : '—';\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// ── REGEX-FIRST ADDRESS PARSER ───────────────────────────────────────────\u001b[39m\n\u001b[31m// Improved Indian address parser\u001b[39m\n\u001b[31m// Handles formats seen in real BTL dispatch files\u001b[39m\n\u001b[31mfunction parseAddressRegex(raw) {\u001b[39m\n\u001b[31m  const text = raw.toString().replace(/\\\\r/g, '').trim();\u001b[39m\n\u001b[31m  const lines = text.split('\\\\n').map(l => l.trim()).filter(Boolean);\u001b[39m\n\u001b[31m  const result = { name:'', address:'', city:'', pincode:'', phone:'', confidence:0 };·\u001b[39m\n\u001b[31m  // ── PHONE: Indian mobile 6-9 prefix, 10 digits ──────────────────────────\u001b[39m\n\u001b[31m  // Also handles \\\"Phone : 9876543210\\\", \\\"M.No.-9876543210\\\", \\\"Mobile - 9876543210\\\"\u001b[39m\n\u001b[31m  const phoneMatch = text.match(/(?:Phone|Mobile|Mob|M\\\\.No\\\\.?|Ph\\\\.?)?\\\\s*[:\\\\-]?\\\\s*\\\\b([6-9]\\\\d{9})\\\\b/i);\u001b[39m\n\u001b[31m  if (phoneMatch) result.phone = phoneMatch[1];·\u001b[39m\n\u001b[31m  // ── PINCODE: 6-digit standalone number ──────────────────────────────────\u001b[39m\n\u001b[31m  // Must not be part of a longer number (e.g. not an account number)\u001b[39m\n\u001b[31m  const pinMatch = text.match(/(?<![\\\\d])([1-9]\\\\d{5})(?![\\\\d])/);\u001b[39m\n\u001b[31m  if (pinMatch) result.pincode = pinMatch[1];·\u001b[39m\n\u001b[31m  // ── CITY: the token immediately before the pincode ───────────────────────\u001b[39m\n\u001b[31m  const indianStates = ['rajasthan','gujarat','maharashtra','punjab','haryana',\u001b[39m\n\u001b[31m    'uttar pradesh','madhya pradesh','karnataka','tamil nadu','andhra pradesh',\u001b[39m\n\u001b[31m    'telangana','kerala','west bengal','bihar','odisha','jharkhand','assam',\u001b[39m\n\u001b[31m    'himachal pradesh','uttarakhand','chhattisgarh','goa','delhi','chandigarh',\u001b[39m\n\u001b[31m    'raj.','raj'];\u001b[39m\n\u001b[31m  if (result.pincode) {\u001b[39m\n\u001b[31m    const pinPos = text.indexOf(result.pincode);\u001b[39m\n\u001b[31m    const beforePin = text.slice(0, pinPos);\u001b[39m\n\u001b[31m    // Strategy 1: split by comma/newline, walk backwards skipping state names\u001b[39m\n\u001b[31m    const chunks = beforePin.split(/[,\\\\n]/).map(c => c.trim()).filter(Boolean);\u001b[39m\n\u001b[31m    for (let i = chunks.length - 1; i >= 0; i--) {\u001b[39m\n\u001b[31m      const chunk = chunks[i].replace(/\\\\d+/g,'').replace(/[()]/g,'').replace(/[.-]/g,'').trim();\u001b[39m\n\u001b[31m      const lower = chunk.toLowerCase();\u001b[39m\n\u001b[31m      if (!chunk || chunk.length < 2) continue;\u001b[39m\n\u001b[31m      if (indianStates.includes(lower)) continue;\u001b[39m\n\u001b[31m      // Skip chunks that look like addresses (contain numbers or long strings)\u001b[39m\n\u001b[31m      if (/\\\\d/.test(chunk)) continue;\u001b[39m\n\u001b[31m      result.city = chunk;\u001b[39m\n\u001b[31m      break;\u001b[39m\n\u001b[31m    }\u001b[39m\n\u001b[31m    // Strategy 2: if still no city, look for last capitalised word before pincode\u001b[39m\n\u001b[31m    // handles \\\"compound paota Jodhpur 342006\\\" → Jodhpur\u001b[39m\n\u001b[31m    if (!result.city) {\u001b[39m\n\u001b[31m      const wordsBefore = beforePin.replace(/[,\\\\n]/g,' ').trim().split(/\\\\s+/);\u001b[39m\n\u001b[31m      for (let i = wordsBefore.length - 1; i >= 0; i--) {\u001b[39m\n\u001b[31m        const w = wordsBefore[i].replace(/[^a-zA-Z]/g,'');\u001b[39m\n\u001b[31m        if (w.length < 3) continue;\u001b[39m\n\u001b[31m        if (indianStates.includes(w.toLowerCase())) continue;\u001b[39m\n\u001b[31m        // Prefer words that start with capital letter (likely a proper noun/city)\u001b[39m\n\u001b[31m        if (/^[A-Z]/.test(w)) { result.city = w; break; }\u001b[39m\n\u001b[31m      }\u001b[39m\n\u001b[31m    }\u001b[39m\n\u001b[31m  }·\u001b[39m\n\u001b[31m  // ── NAME: first meaningful line, extract shop name ───────────────────────\u001b[39m\n\u001b[31m  // Patterns:\u001b[39m\n\u001b[31m  //   \\\"Sanwar Mal Saini, C/o R G COMPUTERS\\\" → name = \\\"R G COMPUTERS\\\"\u001b[39m\n\u001b[31m  //   \\\"SANWAR MAL SAINI, C/o ADVANCE TECHNOLOGIES\\\" → name = \\\"ADVANCE TECHNOLOGIES\\\"\u001b[39m\n\u001b[31m  //   \\\"Sunil Vaishnav\\\" → name = \\\"Sunil Vaishnav\\\" (no C/o)\u001b[39m\n\u001b[31m  //   \\\"Baltej Singh, Professional computer care\\\" → name = \\\"Professional computer care\\\"\u001b[39m\n\u001b[31m  if (lines.length > 0) {\u001b[39m\n\u001b[31m    const firstLine = lines[0];\u001b[39m\n\u001b[31m    // Check for C/o or C/O pattern — shop name comes after it\u001b[39m\n\u001b[31m    const coMatch = firstLine.match(/C\\\\/?[Oo]\\\\.?\\\\s+(.+)/);\u001b[39m\n\u001b[31m    if (coMatch) {\u001b[39m\n\u001b[31m      result.name = coMatch[1].trim();\u001b[39m\n\u001b[31m    } else {\u001b[39m\n\u001b[31m      // Check if there is a comma — second part might be the shop name\u001b[39m\n\u001b[31m      const parts = firstLine.split(',').map(p => p.trim());\u001b[39m\n\u001b[31m      if (parts.length >= 2 && parts[1].length > 3) {\u001b[39m\n\u001b[31m        // Heuristic: if second part looks like a business name (has uppercase or >5 chars)\u001b[39m\n\u001b[31m        const hasUpperOrLong = /[A-Z]{2,}/.test(parts[1]) || parts[1].length > 8;\u001b[39m\n\u001b[31m        result.name = hasUpperOrLong ? parts[1] : parts[0];\u001b[39m\n\u001b[31m      } else {\u001b[39m\n\u001b[31m        result.name = parts[0];\u001b[39m\n\u001b[31m      }\u001b[39m\n\u001b[31m    }\u001b[39m\n\u001b[31m  }·\u001b[39m\n\u001b[31m  // ── ADDRESS: lines between name and phone/pincode line ───────────────────\u001b[39m\n\u001b[31m  const phoneLineIdx = lines.findIndex(l => result.phone && l.includes(result.phone));\u001b[39m\n\u001b[31m  const addrLines = lines.filter((l, i) => {\u001b[39m\n\u001b[31m    if (i === 0) return false;\u001b[39m\n\u001b[31m    if (i === phoneLineIdx) return false;\u001b[39m\n\u001b[31m    // Exclude lines that are just the phone number\u001b[39m\n\u001b[31m    if (/^[6-9]\\\\d{9}$/.test(l.replace(/\\\\s/g,''))) return false;\u001b[39m\n\u001b[31m    return true;\u001b[39m\n\u001b[31m  });\u001b[39m\n\u001b[31m  result.address = addrLines.join(', ').replace(/,\\\\s*,/g, ',').trim();·\u001b[39m\n\u001b[31m  // ── CONFIDENCE: simple score based on what we got ────────────────────────\u001b[39m\n\u001b[31m  let conf = 0;\u001b[39m\n\u001b[31m  if (result.name)    conf += 30;\u001b[39m\n\u001b[31m  if (result.city)    conf += 25;\u001b[39m\n\u001b[31m  if (result.pincode) conf += 25;\u001b[39m\n\u001b[31m  if (result.phone)   conf += 20;\u001b[39m\n\u001b[31m  result.confidence = conf;·\u001b[39m\n\u001b[31m  return result;\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// ── AI ADDRESS PARSER (Ollama fallback) ──────────────────────────────────\u001b[39m\n\u001b[31m// Fetches past corrections from localStorage to improve prompt accuracy\u001b[39m\n\u001b[31mfunction getPastCorrections() {\u001b[39m\n\u001b[31m  try {\u001b[39m\n\u001b[31m    const log = JSON.parse(localStorage.getItem('360lm_parse_corrections') || '[]');\u001b[39m\n\u001b[31m    return log.slice(-10); // last 10 corrections as examples\u001b[39m\n\u001b[31m  } catch(e) { return []; }\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction saveCorrectionLocal(entry) {\u001b[39m\n\u001b[31m  try {\u001b[39m\n\u001b[31m    const log = JSON.parse(localStorage.getItem('360lm_parse_corrections') || '[]');\u001b[39m\n\u001b[31m    log.push(entry);\u001b[39m\n\u001b[31m    // Keep only last 50\u001b[39m\n\u001b[31m    localStorage.setItem('360lm_parse_corrections', JSON.stringify(log.slice(-50)));\u001b[39m\n\u001b[31m  } catch(e) {}\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31masync function parseAddressWithOllama(raw) {\u001b[39m\n\u001b[31m  // Calls the server-side /dispatch-ai/address proxy (avoids CORS + port 11434 issues)\u001b[39m\n\u001b[31m  try {\u001b[39m\n\u001b[31m    const resp = await fetch('/dispatch-ai/address', {\u001b[39m\n\u001b[31m      method: 'POST',\u001b[39m\n\u001b[31m      headers: { 'Content-Type': 'application/json' },\u001b[39m\n\u001b[31m      body: JSON.stringify({ text: raw }),\u001b[39m\n\u001b[31m      signal: AbortSignal.timeout(120000)\u001b[39m\n\u001b[31m    });\u001b[39m\n\u001b[31m    const data = await resp.json();\u001b[39m\n\u001b[31m    if (data.success) return data;\u001b[39m\n\u001b[31m  } catch(e) {}\u001b[39m\n\u001b[31m  return null;\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31masync function parseAddressCell(raw) {\u001b[39m\n\u001b[31m  // Step 1: regex — always runs first, no network call\u001b[39m\n\u001b[31m  const regex = parseAddressRegex(raw);\u001b[39m\n\u001b[31m  if (regex.name) return { ...regex, source: 'regex' };·\u001b[39m\n\u001b[31m  // Step 2: Ollama only if regex got nothing AND Ollama is configured\u001b[39m\n\u001b[31m  // Uses a 3-second timeout to prevent browser hang\u001b[39m\n\u001b[31m  if (window.appConfig.ocr_mode !== 'transformers') {\u001b[39m\n\u001b[31m    try {\u001b[39m\n\u001b[31m      const ai = await Promise.race([\u001b[39m\n\u001b[31m        parseAddressWithOllama(raw),\u001b[39m\n\u001b[31m        new Promise((_, reject) => setTimeout(() => reject(new Error('timeout')), 120000))\u001b[39m\n\u001b[31m      ]);\u001b[39m\n\u001b[31m      if (ai && ai.name) return { ...ai, source: 'ollama' };\u001b[39m\n\u001b[31m    } catch(e) { /* timeout or error — continue to fallback */ }\u001b[39m\n\u001b[31m  }·\u001b[39m\n\u001b[31m  // Step 3: fallback — first line as name\u001b[39m\n\u001b[31m  const lines = raw.toString().split('\\\\n').map(l=>l.trim()).filter(Boolean);\u001b[39m\n\u001b[31m  return { name: lines[0]||raw.slice(0,40), address: raw, city:'', pincode:'', phone:'', source:'fallback' };\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31masync function applyMapping() {\u001b[39m\n\u001b[31m  const useAI   = document.getElementById('useAIParse')?.checked ?? false;\u001b[39m\n\u001b[31m  const comboSel = document.getElementById('comboColSel');\u001b[39m\n\u001b[31m  const comboIdx = comboSel ? parseInt(comboSel.value) : -1;\u001b[39m\n\u001b[31m  const docketIdx = parseInt(document.getElementById('docketColSel')?.value ?? -1);·\u001b[39m\n\u001b[31m  // Build manual mapping as fallback\u001b[39m\n\u001b[31m  const mapping = {};\u001b[39m\n\u001b[31m  MAPPING_FIELDS.forEach(f => {\u001b[39m\n\u001b[31m    const sel = document.getElementById('mapsel_' + f.key);\u001b[39m\n\u001b[31m    mapping[f.key] = sel ? parseInt(sel.value) : -1;\u001b[39m\n\u001b[31m  });\u001b[39m\n\u001b[31m  // Override docket from dedicated selector\u001b[39m\n\u001b[31m  mapping.docket = docketIdx;·\u001b[39m\n\u001b[31m  // Determine if we have a valid base for stores\u001b[39m\n\u001b[31m  const nameIdx = useAI && comboIdx >= 0 ? comboIdx : mapping.name;\u001b[39m\n\u001b[31m  if (nameIdx === -1 && !useAI) { showToast('Store Name column is required', 'err'); return; }·\u001b[39m\n\u001b[31m  // Save mapping\u001b[39m\n\u001b[31m  try { localStorage.setItem(getMappingKey(), JSON.stringify({...mapping, _useAI: useAI, _comboIdx: comboIdx})); } catch(e) {}·\u001b[39m\n\u001b[31m  if (useAI && comboIdx >= 0) {\u001b[39m\n\u001b[31m    // AI path — parse each combined cell\u001b[39m\n\u001b[31m    showToast('AI parsing addresses...', 'info');\u001b[39m\n\u001b[31m    showOCRWorking(true, `Parsing 0/${_rawRows.length} addresses...`);\u001b[39m\n\u001b[31m    stores = [];\u001b[39m\n\u001b[31m    for (let i = 0; i < _rawRows.length; i++) {\u001b[39m\n\u001b[31m      const r = _rawRows[i];\u001b[39m\n\u001b[31m      const raw = (r[comboIdx] || '').toString().trim();\u001b[39m\n\u001b[31m      if (!raw) continue;\u001b[39m\n\u001b[31m      showOCRWorking(true, `Parsing ${i+1}/${_rawRows.length} — ${raw.slice(0,30)}...`);\u001b[39m\n\u001b[31m      // Yield to browser every row to prevent UI freeze\u001b[39m\n\u001b[31m      await new Promise(r => setTimeout(r, 0));\u001b[39m\n\u001b[31m      const parsed = await parseAddressCell(raw);\u001b[39m\n\u001b[31m      stores.push({\u001b[39m\n\u001b[31m        id: i,\u001b[39m\n\u001b[31m        name:    parsed.name    || raw.slice(0,40),\u001b[39m\n\u001b[31m        city:    parsed.city    || '',\u001b[39m\n\u001b[31m        phone:   parsed.phone   || '',\u001b[39m\n\u001b[31m        address: parsed.address || raw,\u001b[39m\n\u001b[31m        pincode: parsed.pincode || '',\u001b[39m\n\u001b[31m        docket:  docketIdx >= 0 ? (r[docketIdx]||'').toString().trim() : '',\u001b[39m\n\u001b[31m        _parseSource: parsed.source,\u001b[39m\n\u001b[31m        _confidence:  parsed.confidence || 0,\u001b[39m\n\u001b[31m        _rawCell:     raw,\u001b[39m\n\u001b[31m        _origName:    parsed.name || '',\u001b[39m\n\u001b[31m        _origCity:    parsed.city || '',\u001b[39m\n\u001b[31m        matched: false\u001b[39m\n\u001b[31m      });\u001b[39m\n\u001b[31m    }\u001b[39m\n\u001b[31m    showOCRWorking(false);\u001b[39m\n\u001b[31m  } else {\u001b[39m\n\u001b[31m    // Manual mapping path\u001b[39m\n\u001b[31m    stores = _rawRows\u001b[39m\n\u001b[31m      .filter(r => r[nameIdx] && (r[nameIdx]+'').trim())\u001b[39m\n\u001b[31m      .map((r, i) => ({\u001b[39m\n\u001b[31m        id: i,\u001b[39m\n\u001b[31m        name:    mapping.name    >= 0 ? (r[mapping.name]   ||'').toString().trim() : '',\u001b[39m\n\u001b[31m        city:    mapping.city    >= 0 ? (r[mapping.city]   ||'').toString().trim() : '',\u001b[39m\n\u001b[31m        phone:   mapping.phone   >= 0 ? (r[mapping.phone]  ||'').toString().trim() : '',\u001b[39m\n\u001b[31m        docket:  mapping.docket  >= 0 ? (r[mapping.docket] ||'').toString().trim() : '',\u001b[39m\n\u001b[31m        address: mapping.address >= 0 ? (r[mapping.address]||'').toString().trim() : '',\u001b[39m\n\u001b[31m        pincode: mapping.pincode >= 0 ? (r[mapping.pincode]||'').toString().trim() : '',\u001b[39m\n\u001b[31m        matched: false\u001b[39m\n\u001b[31m      }));\u001b[39m\n\u001b[31m  }·\u001b[39m\n\u001b[31m  document.getElementById('storeCount').textContent = `${stores.length} stores loaded`;·\u001b[39m\n\u001b[31m  if (useAI && comboIdx >= 0) {\u001b[39m\n\u001b[31m    // AI path — show row-by-row review panel before adding consignees\u001b[39m\n\u001b[31m    showAddressReviewPanel();\u001b[39m\n\u001b[31m  } else {\u001b[39m\n\u001b[31m    // Manual mapping path — go straight to store table + add panel\u001b[39m\n\u001b[31m    renderStoreTable(mapping.docket >= 0 || docketIdx >= 0);\u001b[39m\n\u001b[31m    const addPanel = document.getElementById('storeAddPanel');\u001b[39m\n\u001b[31m    const addBtn   = document.getElementById('storeAddBtn');\u001b[39m\n\u001b[31m    const addTitle = document.getElementById('storeAddTitle');\u001b[39m\n\u001b[31m    const hasDockets = stores.some(s => s.docket);\u001b[39m\n\u001b[31m    addTitle.textContent = hasDockets\u001b[39m\n\u001b[31m      ? `${stores.filter(s=>s.docket).length} stores with dockets — add as consignees?`\u001b[39m\n\u001b[31m      : `Add ${stores.length} stores as consignees?`;\u001b[39m\n\u001b[31m    addBtn.textContent = `✓ Add ${stores.length} as Consignees`;\u001b[39m\n\u001b[31m    addPanel.style.display = 'block';\u001b[39m\n\u001b[31m    showToast(`${stores.length} stores mapped ✓`, 'ok');\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31m// ADDRESS REVIEW PANEL\u001b[39m\n\u001b[31m// Row-by-row review of AI-parsed addresses before consignees created\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31mfunction showAddressReviewPanel() {\u001b[39m\n\u001b[31m  const pane = document.getElementById('addrReviewPane');\u001b[39m\n\u001b[31m  if (!pane) return;\u001b[39m\n\u001b[31m  renderAddressReviewRows();\u001b[39m\n\u001b[31m  pane.style.display = 'block';\u001b[39m\n\u001b[31m  pane.scrollTop = 0;\u001b[39m\n\u001b[31m  showToast(`Review ${stores.length} parsed addresses`, 'info');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction renderAddressReviewRows() {\u001b[39m\n\u001b[31m  const list = document.getElementById('addrReviewList');\u001b[39m\n\u001b[31m  const summary = document.getElementById('addrReviewSummary');\u001b[39m\n\u001b[31m  if (!list) return;·\u001b[39m\n\u001b[31m  const highConf  = stores.filter(s => (s._confidence||0) >= 80).length;\u001b[39m\n\u001b[31m  const medConf   = stores.filter(s => (s._confidence||0) >= 50 && (s._confidence||0) < 80).length;\u001b[39m\n\u001b[31m  const lowConf   = stores.filter(s => (s._confidence||0) < 50).length;·\u001b[39m\n\u001b[31m  summary.innerHTML = `\u001b[39m\n\u001b[31m    <div class=\\\"vs-item\\\"><div class=\\\"vs-val\\\" style=\\\"color:var(--t1);\\\">${stores.length}</div><div class=\\\"vs-lbl\\\">Total</div></div>\u001b[39m\n\u001b[31m    <div class=\\\"vs-item\\\"><div class=\\\"vs-val\\\" style=\\\"color:var(--grn);\\\">${highConf}</div><div class=\\\"vs-lbl\\\">High conf</div></div>\u001b[39m\n\u001b[31m    <div class=\\\"vs-item\\\"><div class=\\\"vs-val\\\" style=\\\"color:var(--ylw);\\\">${medConf}</div><div class=\\\"vs-lbl\\\">Review</div></div>\u001b[39m\n\u001b[31m    <div class=\\\"vs-item\\\"><div class=\\\"vs-val\\\" style=\\\"color:var(--red);\\\">${lowConf}</div><div class=\\\"vs-lbl\\\">Check</div></div>\u001b[39m\n\u001b[31m    <div class=\\\"vs-item\\\" style=\\\"margin-left:auto;\\\">\u001b[39m\n\u001b[31m      <button class=\\\"btn-sm primary\\\" onclick=\\\"approveAllAddresses()\\\" style=\\\"font-size:.7rem;\\\">✓ Approve All</button>\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m  `;·\u001b[39m\n\u001b[31m  list.innerHTML = stores.map((s, i) => {\u001b[39m\n\u001b[31m    const conf = s._confidence || 0;\u001b[39m\n\u001b[31m    const confClass = conf >= 80 ? 'vf-ok' : conf >= 50 ? 'vf-nodocket' : 'vf-dupe';\u001b[39m\n\u001b[31m    const confLabel = conf >= 80 ? `${conf}% ✓` : conf >= 50 ? `${conf}% review` : `${conf}% check`;\u001b[39m\n\u001b[31m    const rowBorder = conf >= 80 ? 'var(--grn)' : conf >= 50 ? 'var(--ylw)' : 'var(--red)';·\u001b[39m\n\u001b[31m    return `\u001b[39m\n\u001b[31m    <div class=\\\"verify-row\\\" id=\\\"arow_${i}\\\" style=\\\"border-left:3px solid ${rowBorder};\\\">\u001b[39m\n\u001b[31m      <div class=\\\"verify-row-hdr\\\">\u001b[39m\n\u001b[31m        <span class=\\\"verify-num\\\">#${i+1}</span>\u001b[39m\n\u001b[31m        <span class=\\\"verify-store\\\" style=\\\"font-size:.78rem;color:var(--t2);font-weight:400;\u001b[39m\n\u001b[31m          font-family:var(--fnt-mono);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;flex:1;\\\"\u001b[39m\n\u001b[31m          title=\\\"${(s._rawCell||'').replace(/\\\"/g,\\\"'\\\")}\\\">\u001b[39m\n\u001b[31m          ${(s._rawCell||'').slice(0,50).split('\\\\n').join(' ↵ ')}       <span class=\\\"verify-flag ${confClass}\\\" style=\\\"flex-shrink:0;margin-right:6px;\\\">${confLabel} · ${s._parseSource||'?'}</span>\u001b[39m\n\u001b[31m        <button onclick=\\\"deleteReviewRow(${i})\\\"\u001b[39m\n\u001b[31m          style=\\\"background:none;border:1px solid rgba(239,68,68,.4);color:var(--red);\u001b[39m\n\u001b[31m          font-size:.65rem;padding:2px 8px;border-radius:6px;cursor:pointer;\u001b[39m\n\u001b[31m          font-family:var(--fnt-mono);flex-shrink:0;transition:background .2s;\\\"\u001b[39m\n\u001b[31m          onmouseover=\\\"this.style.background='rgba(239,68,68,.15)'\\\"\u001b[39m\n\u001b[31m          onmouseout=\\\"this.style.background='none'\\\">✕ Remove</button>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      <div class=\\\"verify-fields\\\" style=\\\"grid-template-columns:1fr 1fr 1fr 1fr 1fr;\\\">\u001b[39m\n\u001b[31m        <div class=\\\"verify-field\\\">\u001b[39m\n\u001b[31m          <label>Shop / Name <span style=\\\"color:var(--acc);\\\">*</span></label>\u001b[39m\n\u001b[31m          <input type=\\\"text\\\" value=\\\"${s.name||''}\\\" id=\\\"ar_name_${i}\\\"\u001b[39m\n\u001b[31m            oninput=\\\"updateAddrField(${i},'name',this)\\\"\u001b[39m\n\u001b[31m            style=\\\"${!s.name ? 'border-color:var(--red);' : ''}\\\">\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m        <div class=\\\"verify-field\\\">\u001b[39m\n\u001b[31m          <label>City</label>\u001b[39m\n\u001b[31m          <input type=\\\"text\\\" value=\\\"${s.city||''}\\\" id=\\\"ar_city_${i}\\\"\u001b[39m\n\u001b[31m            oninput=\\\"updateAddrField(${i},'city',this)\\\"\u001b[39m\n\u001b[31m            style=\\\"${!s.city ? 'border-color:var(--ylw);' : ''}\\\">\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m        <div class=\\\"verify-field\\\">\u001b[39m\n\u001b[31m          <label>Docket / Courier No.</label>\u001b[39m\n\u001b[31m          <input type=\\\"text\\\" value=\\\"${s.docket||''}\\\" id=\\\"ar_docket_${i}\\\"\u001b[39m\n\u001b[31m            oninput=\\\"updateAddrField(${i},'docket',this)\\\"\u001b[39m\n\u001b[31m            style=\\\"font-family:var(--fnt-mono);${!s.docket?'border-color:var(--ylw);':'color:var(--acc);'}\\\">\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m        <div class=\\\"verify-field\\\">\u001b[39m\n\u001b[31m          <label>Phone</label>\u001b[39m\n\u001b[31m          <input type=\\\"tel\\\" value=\\\"${s.phone||''}\\\" id=\\\"ar_phone_${i}\\\"\u001b[39m\n\u001b[31m            oninput=\\\"updateAddrField(${i},'phone',this)\\\">\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m        <div class=\\\"verify-field\\\">\u001b[39m\n\u001b[31m          <label>Pincode</label>\u001b[39m\n\u001b[31m          <input type=\\\"text\\\" value=\\\"${s.pincode||''}\\\" id=\\\"ar_pin_${i}\\\"\u001b[39m\n\u001b[31m            oninput=\\\"updateAddrField(${i},'pincode',this)\\\">\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      <div style=\\\"font-size:.65rem;color:var(--t3);font-family:var(--fnt-mono);\u001b[39m\n\u001b[31m        margin-top:4px;padding:4px 6px;background:var(--bg);border-radius:4px;word-break:break-all;\\\">\u001b[39m\n\u001b[31m        📋 ${(s._rawCell||'').split('\\\\n').join(' | ')}\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m    </div>`;\u001b[39m\n\u001b[31m  }).join('');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction updateAddrField(i, field, input) {\u001b[39m\n\u001b[31m  if (!stores[i]) return;\u001b[39m\n\u001b[31m  stores[i][field] = input.value.trim();\u001b[39m\n\u001b[31m  // Recompute confidence\u001b[39m\n\u001b[31m  let conf = 0;\u001b[39m\n\u001b[31m  if (stores[i].name)    conf += 30;\u001b[39m\n\u001b[31m  if (stores[i].city)    conf += 25;\u001b[39m\n\u001b[31m  if (stores[i].pincode) conf += 25;\u001b[39m\n\u001b[31m  if (stores[i].phone)   conf += 20;\u001b[39m\n\u001b[31m  stores[i]._confidence = conf;\u001b[39m\n\u001b[31m  input.classList.toggle('changed', true);\u001b[39m\n\u001b[31m  // Update border color on row\u001b[39m\n\u001b[31m  const row = document.getElementById('arow_' + i);\u001b[39m\n\u001b[31m  if (row) row.style.borderLeftColor = conf >= 80 ? 'var(--grn)' : conf >= 50 ? 'var(--ylw)' : 'var(--red)';\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction deleteReviewRow(i) {\u001b[39m\n\u001b[31m  // Mark as deleted — splice from stores array and re-render\u001b[39m\n\u001b[31m  stores.splice(i, 1);\u001b[39m\n\u001b[31m  // Re-assign IDs\u001b[39m\n\u001b[31m  stores.forEach((s, idx) => { s.id = idx; });\u001b[39m\n\u001b[31m  renderAddressReviewRows();\u001b[39m\n\u001b[31m  document.getElementById('storeCount').textContent = `${stores.length} stores loaded`;\u001b[39m\n\u001b[31m  showToast(`Row removed — ${stores.length} remaining`, 'info');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction backToMapping() {\u001b[39m\n\u001b[31m  // Hide review pane — store edits are preserved in stores[] array\u001b[39m\n\u001b[31m  document.getElementById('addrReviewPane').style.display = 'none';\u001b[39m\n\u001b[31m  // Show mapping UI again with current file\u001b[39m\n\u001b[31m  document.getElementById('storeTableArea').style.display = 'block';\u001b[39m\n\u001b[31m  document.getElementById('storeColInfo').style.display = 'block';\u001b[39m\n\u001b[31m  showToast('Your edits are preserved — re-apply mapping to reload', 'info');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction approveAllAddresses() {\u001b[39m\n\u001b[31m  stores.forEach((s, i) => { s._reviewed = true; });\u001b[39m\n\u001b[31m  document.getElementById('addrReviewPane').style.display = 'none';\u001b[39m\n\u001b[31m  applyReviewedStores();\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction confirmAddressReview() {\u001b[39m\n\u001b[31m  // Collect any pending edits, log corrections, close panel\u001b[39m\n\u001b[31m  const corrections = [];\u001b[39m\n\u001b[31m  stores.forEach((s, i) => {\u001b[39m\n\u001b[31m    const origName = s._origName || '';\u001b[39m\n\u001b[31m    const origCity = s._origCity || '';\u001b[39m\n\u001b[31m    if ((origName && origName !== s.name) || (origCity && origCity !== s.city)) {\u001b[39m\n\u001b[31m      corrections.push({ raw: s._rawCell||'', name: s.name, city: s.city, pincode: s.pincode });\u001b[39m\n\u001b[31m    }\u001b[39m\n\u001b[31m  });\u001b[39m\n\u001b[31m  // Save corrections locally for future Ollama prompts\u001b[39m\n\u001b[31m  corrections.forEach(c => saveCorrectionLocal(c));\u001b[39m\n\u001b[31m  // Log to Sheets asynchronously (fire and forget)\u001b[39m\n\u001b[31m  if (corrections.length > 0 && window.appConfig.webhook_url) {\u001b[39m\n\u001b[31m    logParseCorrectionsToSheets(corrections);\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m  document.getElementById('addrReviewPane').style.display = 'none';\u001b[39m\n\u001b[31m  applyReviewedStores();\u001b[39m\n\u001b[31m  showToast(`${corrections.length > 0 ? corrections.length + ' corrections logged · ' : ''}${stores.length} stores ready ✓`, 'ok');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// Clean street address: remove city, pincode, phone if they appear in address field\u001b[39m\n\u001b[31mfunction cleanAddress(addr, city, pincode, phone) {\u001b[39m\n\u001b[31m  if (!addr) return addr;\u001b[39m\n\u001b[31m  let s = addr;\u001b[39m\n\u001b[31m  // Remove phone\u001b[39m\n\u001b[31m  if (phone) s = s.replace(new RegExp(phone.replace(/[.*+?^${}()|[\\\\]\\\\\\\\]/g,'\\\\\\\\$&'), 'g'), '').trim();\u001b[39m\n\u001b[31m  // Remove pincode\u001b[39m\n\u001b[31m  if (pincode) s = s.replace(new RegExp('\\\\\\\\b' + pincode + '\\\\\\\\b', 'g'), '').trim();\u001b[39m\n\u001b[31m  // Remove city (case-insensitive)\u001b[39m\n\u001b[31m  if (city) s = s.replace(new RegExp('\\\\\\\\b' + city.replace(/[.*+?^${}()|[\\\\]\\\\\\\\]/g,'\\\\\\\\$&') + '\\\\\\\\b', 'ig'), '').trim();\u001b[39m\n\u001b[31m  // Remove state names and trailing noise\u001b[39m\n\u001b[31m  const noise = /\\\\b(rajasthan|gujarat|maharashtra|punjab|haryana|uttar pradesh|delhi|raj\\\\.)\\\\b/ig;\u001b[39m\n\u001b[31m  s = s.replace(noise, '');\u001b[39m\n\u001b[31m  // Remove \\\"Mobile :\\\", \\\"Phone :\\\", \\\"M.No.\\\" prefixes and dangling numbers\u001b[39m\n\u001b[31m  s = s.replace(/(?:Mobile|Phone|Mob|M\\\\.No\\\\.?|Ph\\\\.?)\\\\s*[:\\\\-]?\\\\s*/ig, '');\u001b[39m\n\u001b[31m  // Clean up multiple commas/spaces\u001b[39m\n\u001b[31m  s = s.replace(/,\\\\s*,/g, ',').replace(/\\\\s{2,}/g, ' ').replace(/^[\\\\s,]+|[\\\\s,]+$/g, '');\u001b[39m\n\u001b[31m  return s;\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction applyReviewedStores() {\u001b[39m\n\u001b[31m  // Clean address fields before loading stores\u001b[39m\n\u001b[31m  stores.forEach(s => {\u001b[39m\n\u001b[31m    s.address = cleanAddress(s.address, s.city, s.pincode, s.phone);\u001b[39m\n\u001b[31m  });\u001b[39m\n\u001b[31m  renderStoreTable(stores.some(s => s.docket));\u001b[39m\n\u001b[31m  const addPanel = document.getElementById('storeAddPanel');\u001b[39m\n\u001b[31m  const addTitle = document.getElementById('storeAddTitle');\u001b[39m\n\u001b[31m  const addBtn   = document.getElementById('storeAddBtn');\u001b[39m\n\u001b[31m  const hasDockets = stores.some(s => s.docket);\u001b[39m\n\u001b[31m  addTitle.textContent = hasDockets\u001b[39m\n\u001b[31m    ? `${stores.filter(s=>s.docket).length} stores with dockets — add as consignees?`\u001b[39m\n\u001b[31m    : `Add ${stores.length} stores as consignees?`;\u001b[39m\n\u001b[31m  addBtn.textContent = `✓ Add ${stores.length} as Consignees`;\u001b[39m\n\u001b[31m  addPanel.style.display = 'block';\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31masync function logParseCorrectionsToSheets(corrections) {\u001b[39m\n\u001b[31m  const url = window.appConfig.webhook_url;\u001b[39m\n\u001b[31m  if (!url) return;\u001b[39m\n\u001b[31m  try {\u001b[39m\n\u001b[31m    await fetch(url, {\u001b[39m\n\u001b[31m      method: 'POST', mode: 'no-cors',\u001b[39m\n\u001b[31m      headers: { 'Content-Type': 'application/json' },\u001b[39m\n\u001b[31m      body: JSON.stringify({\u001b[39m\n\u001b[31m        type: 'parse_correction',\u001b[39m\n\u001b[31m        corrections,\u001b[39m\n\u001b[31m        colFingerprint: getMappingKey(),\u001b[39m\n\u001b[31m        timestamp: new Date().toISOString()\u001b[39m\n\u001b[31m      })\u001b[39m\n\u001b[31m    });\u001b[39m\n\u001b[31m  } catch(e) {}\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction addStoresToConsignees() {\u001b[39m\n\u001b[31m  const consList = document.getElementById('consList');\u001b[39m\n\u001b[31m  consList.innerHTML = '';\u001b[39m\n\u001b[31m  stores.slice(0, 50).forEach(s => addConsignee(s));\u001b[39m\n\u001b[31m  document.getElementById('storeAddPanel').style.display = 'none';\u001b[39m\n\u001b[31m  showToast(`${Math.min(stores.length, 50)} consignees added ✓`, 'ok');\u001b[39m\n\u001b[31m  if (stores.some(s => s.docket)) setTimeout(() => autoCreateDispatchFromStoreList(), 600);\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction promptAddFromStores() { /* replaced by inline panel */ }··\u001b[39m\n\u001b[31mfunction findCol(headers, keywords) {\u001b[39m\n\u001b[31m  for (let i=0;i<headers.length;i++) {\u001b[39m\n\u001b[31m    const h = (headers[i]||'').toString().toLowerCase();\u001b[39m\n\u001b[31m    if (keywords.some(k => h.includes(k))) return i;\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m  return -1;\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction renderStoreTable(hasDocket) {\u001b[39m\n\u001b[31m  const t = document.getElementById('storeTable');\u001b[39m\n\u001b[31m  const cols = ['#','Store Name','City','Phone'];\u001b[39m\n\u001b[31m  if (hasDocket) cols.push('Docket','Status');\u001b[39m\n\u001b[31m  t.innerHTML = `<thead><tr>${cols.map(c=>`<th>${c}</th>`).join('')}</tr></thead>\u001b[39m\n\u001b[31m  <tbody>${stores.map((s,i) => {\u001b[39m\n\u001b[31m    const statusClass = s.docket ? (s.status === 'delivered' ? 'matched' : 'unmatched') : '';\u001b[39m\n\u001b[31m    const docketCell = hasDocket ? `<td class=\\\"${statusClass}\\\" style=\\\"font-family:var(--fnt-mono);\\\">${s.docket||'—'}</td>` : '';\u001b[39m\n\u001b[31m    const statusCell = hasDocket ? `<td>${s.status ? `<span class=\\\"status-pill sp-${s.status}\\\">${s.status}</span>` : '—'}</td>` : '';\u001b[39m\n\u001b[31m    return `<tr>\u001b[39m\n\u001b[31m      <td>${i+1}</td>\u001b[39m\n\u001b[31m      <td>${s.name}</td>\u001b[39m\n\u001b[31m      <td>${s.city}</td>\u001b[39m\n\u001b[31m      <td style=\\\"font-family:var(--fnt-mono);\\\">${s.phone||'—'}</td>\u001b[39m\n\u001b[31m      ${docketCell}${statusCell}\u001b[39m\n\u001b[31m    </tr>`;\u001b[39m\n\u001b[31m  }).join('')}</tbody>`;\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction clearStores() {\u001b[39m\n\u001b[31m  stores = [];\u001b[39m\n\u001b[31m  document.getElementById('storeTableArea').style.display = 'none';\u001b[39m\n\u001b[31m  document.getElementById('storeFile').value = '';\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction autoCreateDispatchFromStoreList() {\u001b[39m\n\u001b[31m  // Create/update dispatch with all stores that have dockets\u001b[39m\n\u001b[31m  const courier = document.getElementById('n_courier').value || 'SkyKing';\u001b[39m\n\u001b[31m  const storesWithDockets = stores.filter(s => s.docket);\u001b[39m\n\u001b[31m  if (!storesWithDockets.length) return;·\u001b[39m\n\u001b[31m  // Clear and rebuild consignee list from store data\u001b[39m\n\u001b[31m  const consList = document.getElementById('consList');\u001b[39m\n\u001b[31m  consList.innerHTML = '';\u001b[39m\n\u001b[31m  storesWithDockets.forEach(s => {\u001b[39m\n\u001b[31m    addConsignee(s);\u001b[39m\n\u001b[31m  });\u001b[39m\n\u001b[31m  showPane('new');\u001b[39m\n\u001b[31m  showToast(`${storesWithDockets.length} consignees added from store list`, 'ok');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction promptAddFromStores() {\u001b[39m\n\u001b[31m  // Replaced by inline storeAddPanel UI — no-op kept for safety\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction autoMatchDockets() {\u001b[39m\n\u001b[31m  // Match OCR dockets to stores by order\u001b[39m\n\u001b[31m  if (!slipOCRData.length) { showToast('Upload docket slip images first', 'err'); return; }\u001b[39m\n\u001b[31m  let matched = 0;\u001b[39m\n\u001b[31m  slipOCRData.forEach((slip, i) => {\u001b[39m\n\u001b[31m    if (i < stores.length && slip.docket) {\u001b[39m\n\u001b[31m      stores[i].docket = slip.docket;\u001b[39m\n\u001b[31m      matched++;\u001b[39m\n\u001b[31m    }\u001b[39m\n\u001b[31m  });\u001b[39m\n\u001b[31m  renderStoreTable(true);\u001b[39m\n\u001b[31m  showToast(`${matched} dockets matched to stores ✓`, 'ok');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31m// CONSIGNEES\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31mfunction addConsignee(storeData = null) {\u001b[39m\n\u001b[31m  const list = document.getElementById('consList');\u001b[39m\n\u001b[31m  const idx = list.children.length;\u001b[39m\n\u001b[31m  const card = document.createElement('div');\u001b[39m\n\u001b[31m  card.className = 'cons-card';\u001b[39m\n\u001b[31m  card.dataset.idx = idx;\u001b[39m\n\u001b[31m  const s = storeData || {};\u001b[39m\n\u001b[31m  card.innerHTML = `\u001b[39m\n\u001b[31m    <div class=\\\"cons-hdr\\\">\u001b[39m\n\u001b[31m      <span class=\\\"cons-num\\\">CONSIGNEE #${idx+1}</span>\u001b[39m\n\u001b[31m      <button class=\\\"cons-del\\\" onclick=\\\"this.closest('.cons-card').remove();renumberCons()\\\">✕</button>\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m    <div class=\\\"row2\\\">\u001b[39m\n\u001b[31m      <div class=\\\"f\\\">\u001b[39m\n\u001b[31m        <label>Store / Name <span class=\\\"req\\\">*</span></label>\u001b[39m\n\u001b[31m        <input type=\\\"text\\\" class=\\\"cons-name\\\" placeholder=\\\"Store name\\\" value=\\\"${s.name||''}\\\">\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      <div class=\\\"f\\\">\u001b[39m\n\u001b[31m        <label>City <span class=\\\"req\\\">*</span></label>\u001b[39m\n\u001b[31m        <input type=\\\"text\\\" class=\\\"cons-city\\\" placeholder=\\\"City\\\" value=\\\"${s.city||''}\\\">\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m    <div class=\\\"f\\\">\u001b[39m\n\u001b[31m      <label>Address</label>\u001b[39m\n\u001b[31m      <input type=\\\"text\\\" class=\\\"cons-addr\\\" placeholder=\\\"Full address\\\" value=\\\"${s.address||''}\\\">\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m    <div class=\\\"row2\\\">\u001b[39m\n\u001b[31m      <div class=\\\"f\\\">\u001b[39m\n\u001b[31m        <label>Phone</label>\u001b[39m\n\u001b[31m        <input type=\\\"tel\\\" class=\\\"cons-phone\\\" placeholder=\\\"Mobile\\\" value=\\\"${s.phone||''}\\\">\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      <div class=\\\"f\\\">\u001b[39m\n\u001b[31m        <label>Pincode</label>\u001b[39m\n\u001b[31m        <input type=\\\"text\\\" class=\\\"cons-pin\\\" placeholder=\\\"6-digit\\\" maxlength=\\\"6\\\">\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m    <div class=\\\"f\\\">\u001b[39m\n\u001b[31m      <label>Material / Contents <span class=\\\"req\\\">*</span></label>\u001b[39m\n\u001b[31m      <input type=\\\"text\\\" class=\\\"cons-material\\\" placeholder=\\\"e.g. Standees, Posters\\\">\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m    <div class=\\\"row2\\\">\u001b[39m\n\u001b[31m      <div class=\\\"f\\\">\u001b[39m\n\u001b[31m        <label>Qty</label>\u001b[39m\n\u001b[31m        <input type=\\\"number\\\" class=\\\"cons-qty\\\" placeholder=\\\"0\\\" min=\\\"0\\\">\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      <div class=\\\"f\\\">\u001b[39m\n\u001b[31m        <label>Docket No.</label>\u001b[39m\n\u001b[31m        <input type=\\\"text\\\" class=\\\"cons-docket-input\\\" placeholder=\\\"Docket #\\\"\u001b[39m\n\u001b[31m          style=\\\"font-family:var(--fnt-mono);\\\" value=\\\"${s.docket||''}\\\"\u001b[39m\n\u001b[31m          oninput=\\\"onDocketInput(this)\\\">\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m    ${s.docket ? `<div class=\\\"cons-docket-linked\\\"><span>🔗</span><span class=\\\"dkt\\\">${s.docket}</span></div>` : ''}\u001b[39m\n\u001b[31m  `;\u001b[39m\n\u001b[31m  list.appendChild(card);\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction onDocketInput(input) {\u001b[39m\n\u001b[31m  const card = input.closest('.cons-card');\u001b[39m\n\u001b[31m  let linked = card.querySelector('.cons-docket-linked');\u001b[39m\n\u001b[31m  if (input.value.trim()) {\u001b[39m\n\u001b[31m    if (!linked) {\u001b[39m\n\u001b[31m      linked = document.createElement('div');\u001b[39m\n\u001b[31m      linked.className = 'cons-docket-linked';\u001b[39m\n\u001b[31m      card.appendChild(linked);\u001b[39m\n\u001b[31m    }\u001b[39m\n\u001b[31m    linked.innerHTML = `<span>🔗</span><span class=\\\"dkt\\\">${input.value}</span>`;\u001b[39m\n\u001b[31m  } else if (linked) {\u001b[39m\n\u001b[31m    linked.remove();\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction showLinkedDocket(card, docket) {\u001b[39m\n\u001b[31m  const input = card.querySelector('.cons-docket-input');\u001b[39m\n\u001b[31m  if (input) { input.value = docket; onDocketInput(input); }\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction renumberCons() {\u001b[39m\n\u001b[31m  document.querySelectorAll('#consList .cons-card').forEach((c,i) => {\u001b[39m\n\u001b[31m    c.querySelector('.cons-num').textContent = `CONSIGNEE #${i+1}`;\u001b[39m\n\u001b[31m    c.dataset.idx = i;\u001b[39m\n\u001b[31m  });\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31m// JOB REFERENCE\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31mfunction onClientChange() {\u001b[39m\n\u001b[31m  const val = document.getElementById('n_client').value;\u001b[39m\n\u001b[31m  document.getElementById('customClientField').style.display = val === '__c' ? 'block' : 'none';\u001b[39m\n\u001b[31m  updatePill();\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31mfunction updatePill() {\u001b[39m\n\u001b[31m  const client = document.getElementById('n_client').value;\u001b[39m\n\u001b[31m  const job = document.getElementById('n_jobref').value.trim();\u001b[39m\n\u001b[31m  const cName = client === '__c' ? document.getElementById('n_customClient').value : client;\u001b[39m\n\u001b[31m  const pill = document.getElementById('jobPill');\u001b[39m\n\u001b[31m  if (cName && job) {\u001b[39m\n\u001b[31m    document.getElementById('jobPillTxt').textContent = `${cName} › ${job}`;\u001b[39m\n\u001b[31m    pill.style.display = 'inline-flex';\u001b[39m\n\u001b[31m  } else {\u001b[39m\n\u001b[31m    pill.style.display = 'none';\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31mfunction clearJob() {\u001b[39m\n\u001b[31m  document.getElementById('n_client').value = '';\u001b[39m\n\u001b[31m  document.getElementById('n_jobref').value = '';\u001b[39m\n\u001b[31m  document.getElementById('jobPill').style.display = 'none';\u001b[39m\n\u001b[31m  document.getElementById('customClientField').style.display = 'none';\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31m// GATHER / SAVE\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31mfunction gatherDispatch() {\u001b[39m\n\u001b[31m  const client = document.getElementById('n_client').value;\u001b[39m\n\u001b[31m  const clientName = client === '__c' ? document.getElementById('n_customClient').value : client;\u001b[39m\n\u001b[31m  const consignees = [];\u001b[39m\n\u001b[31m  document.querySelectorAll('#consList .cons-card').forEach(card => {\u001b[39m\n\u001b[31m    const docket = card.querySelector('.cons-docket-input')?.value.trim() || '';\u001b[39m\n\u001b[31m    consignees.push({\u001b[39m\n\u001b[31m      name: card.querySelector('.cons-name')?.value || '',\u001b[39m\n\u001b[31m      city: card.querySelector('.cons-city')?.value || '',\u001b[39m\n\u001b[31m      address: card.querySelector('.cons-addr')?.value || '',\u001b[39m\n\u001b[31m      phone: card.querySelector('.cons-phone')?.value || '',\u001b[39m\n\u001b[31m      pincode: card.querySelector('.cons-pin')?.value || '',\u001b[39m\n\u001b[31m      material: card.querySelector('.cons-material')?.value || '',\u001b[39m\n\u001b[31m      qty: card.querySelector('.cons-qty')?.value || '',\u001b[39m\n\u001b[31m      docket,\u001b[39m\n\u001b[31m      status: docket ? 'booked' : 'pending',\u001b[39m\n\u001b[31m      lastEvent: '',\u001b[39m\n\u001b[31m      lastUpdated: ''\u001b[39m\n\u001b[31m    });\u001b[39m\n\u001b[31m  });\u001b[39m\n\u001b[31m  return {\u001b[39m\n\u001b[31m    id: 'DSP-' + Date.now(),\u001b[39m\n\u001b[31m    mode: 'manual',\u001b[39m\n\u001b[31m    client: clientName,\u001b[39m\n\u001b[31m    jobref: document.getElementById('n_jobref').value,\u001b[39m\n\u001b[31m    courier: document.getElementById('n_courier').value,\u001b[39m\n\u001b[31m    dispatchDate: document.getElementById('n_date').value,\u001b[39m\n\u001b[31m    remarks: document.getElementById('n_remarks').value,\u001b[39m\n\u001b[31m    consignees,\u001b[39m\n\u001b[31m    slipCount: slipOCRData.length,\u001b[39m\n\u001b[31m    status: 'active',\u001b[39m\n\u001b[31m    createdAt: new Date().toISOString()\u001b[39m\n\u001b[31m  };\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction validate(d) {\u001b[39m\n\u001b[31m  if (!d.client) { showToast('Select a client', 'err'); return false; }\u001b[39m\n\u001b[31m  if (!d.courier) { showToast('Select courier', 'err'); return false; }\u001b[39m\n\u001b[31m  if (!d.consignees.length) { showToast('Add at least one consignee', 'err'); return false; }\u001b[39m\n\u001b[31m  for (let i = 0; i < d.consignees.length; i++) {\u001b[39m\n\u001b[31m    const c = d.consignees[i];\u001b[39m\n\u001b[31m    if (!c.name || !c.city) {\u001b[39m\n\u001b[31m      showToast(`Consignee #${i+1} (${c.name||'unnamed'}) — name & city required`, 'err');\u001b[39m\n\u001b[31m      // Scroll to and highlight the problem consignee card\u001b[39m\n\u001b[31m      const cards = document.querySelectorAll('#consList .cons-card');\u001b[39m\n\u001b[31m      if (cards[i]) {\u001b[39m\n\u001b[31m        cards[i].scrollIntoView({ behavior: 'smooth', block: 'center' });\u001b[39m\n\u001b[31m        cards[i].style.borderColor = 'var(--red)';\u001b[39m\n\u001b[31m        cards[i].style.boxShadow = '0 0 0 2px rgba(239,68,68,.3)';\u001b[39m\n\u001b[31m        setTimeout(() => {\u001b[39m\n\u001b[31m          if (cards[i]) { cards[i].style.borderColor = ''; cards[i].style.boxShadow = ''; }\u001b[39m\n\u001b[31m        }, 3000);\u001b[39m\n\u001b[31m      }\u001b[39m\n\u001b[31m      return false;\u001b[39m\n\u001b[31m    }\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m  return true;\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction saveDraft() {\u001b[39m\n\u001b[31m  const d = gatherDispatch();\u001b[39m\n\u001b[31m  d.status = 'draft';\u001b[39m\n\u001b[31m  dispatches.unshift(d);\u001b[39m\n\u001b[31m  saveData();\u001b[39m\n\u001b[31m  updateHistCount();\u001b[39m\n\u001b[31m  showToast('Draft saved ✓', 'ok');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// ── VERIFICATION SCREEN ─────────────────────────────────\u001b[39m\n\u001b[31m// Known docket lengths per courier for validation\u001b[39m\n\u001b[31mconst DOCKET_LENGTHS = {\u001b[39m\n\u001b[31m  'SkyKing': { min: 9, max: 12 },\u001b[39m\n\u001b[31m  'DTDC':    { min: 8, max: 15 },\u001b[39m\n\u001b[31m  'Delhivery': { min: 10, max: 18 },\u001b[39m\n\u001b[31m  'Blue Dart': { min: 10, max: 13 },\u001b[39m\n\u001b[31m  'FedEx':   { min: 10, max: 20 },\u001b[39m\n\u001b[31m  'Ecom Express': { min: 10, max: 14 },\u001b[39m\n\u001b[31m  'India Post': { min: 13, max: 22 },\u001b[39m\n\u001b[31m};·\u001b[39m\n\u001b[31mfunction finaliseDispatch() {\u001b[39m\n\u001b[31m  const d = gatherDispatch();\u001b[39m\n\u001b[31m  if (!validate(d)) return;\u001b[39m\n\u001b[31m  showVerifyScreen(d);\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction showVerifyScreen(d) {\u001b[39m\n\u001b[31m  const pane = document.getElementById('verifyPane');\u001b[39m\n\u001b[31m  const list = document.getElementById('verifyList');\u001b[39m\n\u001b[31m  const summary = document.getElementById('verifySummary');·\u001b[39m\n\u001b[31m  // Build flags\u001b[39m\n\u001b[31m  const docketsSeen = {};\u001b[39m\n\u001b[31m  const courier = d.courier;\u001b[39m\n\u001b[31m  const lenRule = DOCKET_LENGTHS[courier];·\u001b[39m\n\u001b[31m  d.consignees.forEach((c, i) => {\u001b[39m\n\u001b[31m    if (c.docket) docketsSeen[c.docket] = (docketsSeen[c.docket] || 0) + 1;\u001b[39m\n\u001b[31m  });·\u001b[39m\n\u001b[31m  const total     = d.consignees.length;\u001b[39m\n\u001b[31m  const withDock  = d.consignees.filter(c => c.docket).length;\u001b[39m\n\u001b[31m  const dupes     = Object.values(docketsSeen).filter(v => v > 1).length;\u001b[39m\n\u001b[31m  const noDock    = total - withDock;\u001b[39m\n\u001b[31m  const badLen    = lenRule ? d.consignees.filter(c => c.docket &&\u001b[39m\n\u001b[31m    (c.docket.length < lenRule.min || c.docket.length > lenRule.max)).length : 0;·\u001b[39m\n\u001b[31m  // Summary bar\u001b[39m\n\u001b[31m  summary.innerHTML = `\u001b[39m\n\u001b[31m    <div class=\\\"vs-item\\\"><div class=\\\"vs-val\\\" style=\\\"color:var(--t1);\\\">${total}</div><div class=\\\"vs-lbl\\\">Total</div></div>\u001b[39m\n\u001b[31m    <div class=\\\"vs-item\\\"><div class=\\\"vs-val\\\" style=\\\"color:var(--grn);\\\">${withDock}</div><div class=\\\"vs-lbl\\\">Docketed</div></div>\u001b[39m\n\u001b[31m    <div class=\\\"vs-item\\\"><div class=\\\"vs-val\\\" style=\\\"color:${noDock > 0 ? 'var(--ylw)' : 'var(--t3)'};\\\">${noDock}</div><div class=\\\"vs-lbl\\\">No Docket</div></div>\u001b[39m\n\u001b[31m    <div class=\\\"vs-item\\\"><div class=\\\"vs-val\\\" style=\\\"color:${dupes > 0 ? 'var(--red)' : 'var(--t3)'};\\\">${dupes}</div><div class=\\\"vs-lbl\\\">Duplicates</div></div>\u001b[39m\n\u001b[31m    ${badLen > 0 ? `<div class=\\\"vs-item\\\"><div class=\\\"vs-val\\\" style=\\\"color:var(--sky);\\\">${badLen}</div><div class=\\\"vs-lbl\\\">Short/Long</div></div>` : ''}\u001b[39m\n\u001b[31m    <div class=\\\"vs-item\\\" style=\\\"margin-left:auto;text-align:right;\\\">\u001b[39m\n\u001b[31m      <div class=\\\"vs-val\\\" style=\\\"font-size:.8rem;color:var(--t2);\\\">${d.courier||'—'}</div>\u001b[39m\n\u001b[31m      <div class=\\\"vs-lbl\\\">${d.client||'—'} ${d.jobref ? '› '+d.jobref : ''}</div>\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m  `;·\u001b[39m\n\u001b[31m  // Build consignee rows\u001b[39m\n\u001b[31m  list.innerHTML = d.consignees.map((c, i) => {\u001b[39m\n\u001b[31m    const isDupe   = c.docket && docketsSeen[c.docket] > 1;\u001b[39m\n\u001b[31m    const isNoDock = !c.docket;\u001b[39m\n\u001b[31m    const isShort  = lenRule && c.docket &&\u001b[39m\n\u001b[31m      (c.docket.length < lenRule.min || c.docket.length > lenRule.max);·\u001b[39m\n\u001b[31m    const rowClass = isDupe ? 'has-dupe' : (isNoDock ? 'no-docket' : 'ok');·\u001b[39m\n\u001b[31m    // Find matching OCR slip for thumbnail\u001b[39m\n\u001b[31m    const ocrMatch = slipOCRData[i];\u001b[39m\n\u001b[31m    const thumbHtml = ocrMatch?.imageData\u001b[39m\n\u001b[31m      ? `<img class=\\\"verify-thumb\\\" src=\\\"${ocrMatch.imageData}\\\" onclick=\\\"showSlipFull(${i})\\\" title=\\\"View slip\\\">`\u001b[39m\n\u001b[31m      : '';·\u001b[39m\n\u001b[31m    // Flags\u001b[39m\n\u001b[31m    let flags = '';\u001b[39m\n\u001b[31m    if (isDupe)   flags += `<span class=\\\"verify-flag vf-dupe\\\">⚠ Duplicate</span> `;\u001b[39m\n\u001b[31m    if (isNoDock) flags += `<span class=\\\"verify-flag vf-nodocket\\\">No Docket</span> `;\u001b[39m\n\u001b[31m    if (isShort)  flags += `<span class=\\\"verify-flag vf-short\\\">${c.docket.length} digits</span> `;\u001b[39m\n\u001b[31m    if (!isDupe && !isNoDock && !isShort) flags = `<span class=\\\"verify-flag vf-ok\\\">✓ OK</span>`;·\u001b[39m\n\u001b[31m    // OCR confidence note\u001b[39m\n\u001b[31m    const confNote = ocrMatch && ocrMatch.confidence\u001b[39m\n\u001b[31m      ? `<div class=\\\"ocr-correction-note\\\">OCR confidence: ${Math.round(ocrMatch.confidence*100)}% via ${ocrMatch.source||'—'}</div>` : '';·\u001b[39m\n\u001b[31m    // WhatsApp URL\u001b[39m\n\u001b[31m    const waMsg = encodeURIComponent(\u001b[39m\n\u001b[31m      `Dear ${c.name}, your material has been dispatched via ${d.courier}. Docket: ${c.docket||'TBD'}. Track: ${(COURIER_MASTER[d.courier]?.trackUrl||'').replace('{docket}', c.docket||'')}`\u001b[39m\n\u001b[31m    );\u001b[39m\n\u001b[31m    const waUrl = c.phone ? `https://wa.me/91${c.phone.replace(/\\\\D/g,'')}?text=${waMsg}` : '';·\u001b[39m\n\u001b[31m    return `\u001b[39m\n\u001b[31m    <div class=\\\"verify-row ${rowClass}\\\" id=\\\"vrow_${i}\\\">\u001b[39m\n\u001b[31m      <div class=\\\"verify-row-hdr\\\">\u001b[39m\n\u001b[31m        <span class=\\\"verify-num\\\">#${i+1}</span>\u001b[39m\n\u001b[31m        <span class=\\\"verify-store\\\">${c.name||'—'}</span>\u001b[39m\n\u001b[31m        ${flags}\u001b[39m\n\u001b[31m        ${thumbHtml}\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      <div class=\\\"verify-fields\\\" style=\\\"grid-template-columns:1fr 1fr 1fr;\\\">\u001b[39m\n\u001b[31m        <div class=\\\"verify-field\\\">\u001b[39m\n\u001b[31m          <label>Docket No. <span style=\\\"color:var(--acc);\\\">*</span></label>\u001b[39m\n\u001b[31m          <input type=\\\"text\\\" value=\\\"${c.docket||''}\\\" oninput=\\\"updateVerifyField(${i},'docket',this)\\\"\u001b[39m\n\u001b[31m            style=\\\"font-family:var(--fnt-mono);${isDupe||isShort?'border-color:var(--red);':!c.docket?'border-color:var(--ylw);':'color:var(--acc);'}\\\">\u001b[39m\n\u001b[31m          ${confNote}\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m        <div class=\\\"verify-field\\\">\u001b[39m\n\u001b[31m          <label>City</label>\u001b[39m\n\u001b[31m          <input type=\\\"text\\\" value=\\\"${c.city||''}\\\" oninput=\\\"updateVerifyField(${i},'city',this)\\\"\u001b[39m\n\u001b[31m            style=\\\"${!c.city ? 'border-color:var(--ylw);' : ''}\\\">\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m        <div class=\\\"verify-field\\\">\u001b[39m\n\u001b[31m          <label>Pincode</label>\u001b[39m\n\u001b[31m          <input type=\\\"text\\\" value=\\\"${c.pincode||''}\\\" oninput=\\\"updateVerifyField(${i},'pincode',this)\\\"\u001b[39m\n\u001b[31m            style=\\\"font-family:var(--fnt-mono);\\\">\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m        <div class=\\\"verify-field\\\">\u001b[39m\n\u001b[31m          <label>Phone</label>\u001b[39m\n\u001b[31m          <input type=\\\"tel\\\" value=\\\"${c.phone||''}\\\" oninput=\\\"updateVerifyField(${i},'phone',this)\\\">\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m        <div class=\\\"verify-field\\\">\u001b[39m\n\u001b[31m          <label>Material / Contents</label>\u001b[39m\n\u001b[31m          <input type=\\\"text\\\" value=\\\"${c.material||''}\\\" oninput=\\\"updateVerifyField(${i},'material',this)\\\">\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m        <div class=\\\"verify-field\\\">\u001b[39m\n\u001b[31m          <label>Street Address</label>\u001b[39m\n\u001b[31m          <input type=\\\"text\\\" value=\\\"${c.address||''}\\\" oninput=\\\"updateVerifyField(${i},'address',this)\\\"\u001b[39m\n\u001b[31m            placeholder=\\\"Street / locality only\\\">\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      <div class=\\\"verify-actions\\\">\u001b[39m\n\u001b[31m        ${c.docket && c.phone ? `<button class=\\\"btn-wa\\\" onclick=\\\"window.open('${waUrl}','_blank')\\\">📱 WhatsApp</button>` : ''}\u001b[39m\n\u001b[31m        <button class=\\\"btn-sm\\\" onclick=\\\"removeVerifyRow(${i})\\\">✕ Remove</button>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m    </div>`;\u001b[39m\n\u001b[31m  }).join('');·\u001b[39m\n\u001b[31m  // Store dispatch object for confirm\u001b[39m\n\u001b[31m  pane._pendingDispatch = d;\u001b[39m\n\u001b[31m  pane.style.display = 'block';\u001b[39m\n\u001b[31m  pane.scrollTop = 0;\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction updateVerifyField(idx, field, input) {\u001b[39m\n\u001b[31m  const pane = document.getElementById('verifyPane');\u001b[39m\n\u001b[31m  if (!pane._pendingDispatch) return;\u001b[39m\n\u001b[31m  const c = pane._pendingDispatch.consignees[idx];\u001b[39m\n\u001b[31m  if (!c) return;\u001b[39m\n\u001b[31m  c[field] = input.value.trim();\u001b[39m\n\u001b[31m  input.classList.toggle('changed', input.value.trim() !== '');\u001b[39m\n\u001b[31m  // Re-check dupe if docket changed\u001b[39m\n\u001b[31m  if (field === 'docket') refreshVerifyFlags(pane._pendingDispatch);\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction refreshVerifyFlags(d) {\u001b[39m\n\u001b[31m  const seen = {};\u001b[39m\n\u001b[31m  d.consignees.forEach(c => { if (c.docket) seen[c.docket] = (seen[c.docket]||0)+1; });\u001b[39m\n\u001b[31m  d.consignees.forEach((c, i) => {\u001b[39m\n\u001b[31m    const row = document.getElementById('vrow_' + i);\u001b[39m\n\u001b[31m    if (!row) return;\u001b[39m\n\u001b[31m    const isDupe = c.docket && seen[c.docket] > 1;\u001b[39m\n\u001b[31m    row.classList.toggle('has-dupe', isDupe);\u001b[39m\n\u001b[31m  });\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction removeVerifyRow(idx) {\u001b[39m\n\u001b[31m  const pane = document.getElementById('verifyPane');\u001b[39m\n\u001b[31m  if (!pane._pendingDispatch) return;\u001b[39m\n\u001b[31m  const el = document.getElementById('vrow_' + idx);\u001b[39m\n\u001b[31m  if (el) {\u001b[39m\n\u001b[31m    el.style.transition = 'opacity .25s';\u001b[39m\n\u001b[31m    el.style.opacity = '0';\u001b[39m\n\u001b[31m    setTimeout(() => {\u001b[39m\n\u001b[31m      pane._pendingDispatch.consignees.splice(idx, 1);\u001b[39m\n\u001b[31m      showVerifyScreen(pane._pendingDispatch); // re-render with updated indices\u001b[39m\n\u001b[31m    }, 250);\u001b[39m\n\u001b[31m  } else {\u001b[39m\n\u001b[31m    pane._pendingDispatch.consignees.splice(idx, 1);\u001b[39m\n\u001b[31m    showVerifyScreen(pane._pendingDispatch);\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction approveAll() {\u001b[39m\n\u001b[31m  showToast('All rows approved ✓', 'ok');\u001b[39m\n\u001b[31m  document.getElementById('btnConfirmFinalise').style.background = 'var(--grn)';\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction closeVerify() {\u001b[39m\n\u001b[31m  const pane = document.getElementById('verifyPane');\u001b[39m\n\u001b[31m  // If this dispatch was already finalised, warn before going back\u001b[39m\n\u001b[31m  if (_lastFinalisedId && pane._pendingDispatch?.id === _lastFinalisedId) {\u001b[39m\n\u001b[31m    if (!confirm(\u001b[39m\n\u001b[31m      'This dispatch has already been finalised.\\\\n\\\\n' +\u001b[39m\n\u001b[31m      'Going back will take you to the edit form — any changes will create a NEW dispatch record.\\\\n\\\\n' +\u001b[39m\n\u001b[31m      'Go back anyway?'\u001b[39m\n\u001b[31m    )) return;\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m  pane.style.display = 'none';\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction showSlipFull(idx) {\u001b[39m\n\u001b[31m  const s = slipOCRData[idx];\u001b[39m\n\u001b[31m  if (!s?.imageData) return;\u001b[39m\n\u001b[31m  const w = window.open('');\u001b[39m\n\u001b[31m  w.document.write(`<img src=\\\"${s.imageData}\\\" style=\\\"max-width:100%;height:auto;\\\">`);\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// Log OCR correction to sheets for future accuracy improvement\u001b[39m\n\u001b[31masync function logOCRCorrection(original, corrected, courier) {\u001b[39m\n\u001b[31m  if (!window.appConfig.webhook_url || original === corrected) return;\u001b[39m\n\u001b[31m  try {\u001b[39m\n\u001b[31m    await fetch(window.appConfig.webhook_url, {\u001b[39m\n\u001b[31m      method: 'POST', mode: 'no-cors',\u001b[39m\n\u001b[31m      headers: { 'Content-Type': 'application/json' },\u001b[39m\n\u001b[31m      body: JSON.stringify({\u001b[39m\n\u001b[31m        type: 'ocr_correction',\u001b[39m\n\u001b[31m        original, corrected, courier,\u001b[39m\n\u001b[31m        timestamp: new Date().toISOString()\u001b[39m\n\u001b[31m      })\u001b[39m\n\u001b[31m    });\u001b[39m\n\u001b[31m  } catch(e) {}\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mlet _lastFinalisedId = null; // track last finalised dispatch to warn on back·\u001b[39m\n\u001b[31masync function confirmAndFinalise() {\u001b[39m\n\u001b[31m  const pane = document.getElementById('verifyPane');\u001b[39m\n\u001b[31m  const d = pane._pendingDispatch;\u001b[39m\n\u001b[31m  if (!d) return;·\u001b[39m\n\u001b[31m  d.status = 'active';\u001b[39m\n\u001b[31m  d.finalisedAt = new Date().toISOString();·\u001b[39m\n\u001b[31m  // Log OCR corrections\u001b[39m\n\u001b[31m  slipOCRData.forEach((s, i) => {\u001b[39m\n\u001b[31m    const c = d.consignees[i];\u001b[39m\n\u001b[31m    if (c && s.docket && c.docket && s.docket !== c.docket) {\u001b[39m\n\u001b[31m      logOCRCorrection(s.docket, c.docket, d.courier);\u001b[39m\n\u001b[31m    }\u001b[39m\n\u001b[31m  });·\u001b[39m\n\u001b[31m  // Replace existing draft with same ID if present, else prepend\u001b[39m\n\u001b[31m  const existingIdx = dispatches.findIndex(x => x.id === d.id);\u001b[39m\n\u001b[31m  if (existingIdx >= 0) {\u001b[39m\n\u001b[31m    dispatches[existingIdx] = d;\u001b[39m\n\u001b[31m  } else {\u001b[39m\n\u001b[31m    dispatches.unshift(d);\u001b[39m\n\u001b[31m  }·\u001b[39m\n\u001b[31m  _lastFinalisedId = d.id;\u001b[39m\n\u001b[31m  saveData();\u001b[39m\n\u001b[31m  syncDispatchToDB(d); // push to Postgres (non-blocking)\u001b[39m\n\u001b[31m  updateHistCount();\u001b[39m\n\u001b[31m  if (window.appConfig.auto_log !== 'false' && window.appConfig.webhook_url) await logToSheets(d);\u001b[39m\n\u001b[31m  showToast('Dispatch finalised ✓', 'ok');\u001b[39m\n\u001b[31m  pane.style.display = 'none';\u001b[39m\n\u001b[31m  pane._pendingDispatch = null; // clear pending\u001b[39m\n\u001b[31m  resetForm();\u001b[39m\n\u001b[31m  showPane('dashboard');\u001b[39m\n\u001b[31m  renderDashboard();\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction resetForm() {\u001b[39m\n\u001b[31m  document.getElementById('n_client').value = '';\u001b[39m\n\u001b[31m  document.getElementById('n_jobref').value = '';\u001b[39m\n\u001b[31m  document.getElementById('n_courier').value = '';\u001b[39m\n\u001b[31m  document.getElementById('n_remarks').value = '';\u001b[39m\n\u001b[31m  document.getElementById('jobPill').style.display = 'none';\u001b[39m\n\u001b[31m  document.getElementById('customClientField').style.display = 'none';\u001b[39m\n\u001b[31m  document.getElementById('consList').innerHTML = '';\u001b[39m\n\u001b[31m  addConsignee();\u001b[39m\n\u001b[31m  clearSlips();\u001b[39m\n\u001b[31m  clearStores();\u001b[39m\n\u001b[31m  setToday();\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction newDispatch() { resetForm(); showPane('new'); }·\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31m// DASHBOARD\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31mfunction renderDashboard() {\u001b[39m\n\u001b[31m  // Exclude drafts from dashboard — only show active/finalised dispatches\u001b[39m\n\u001b[31m  const activeDis = dispatches.filter(d => d.status !== 'draft');\u001b[39m\n\u001b[31m  const allCons = activeDis.flatMap(d => d.consignees || []);\u001b[39m\n\u001b[31m  const countBy = (status) => allCons.filter(c => c.status === status).length;\u001b[39m\n\u001b[31m  document.getElementById('st-delivered').textContent = countBy('delivered');\u001b[39m\n\u001b[31m  document.getElementById('st-transit').textContent = countBy('transit') + countBy('in-transit');\u001b[39m\n\u001b[31m  document.getElementById('st-booked').textContent = countBy('booked');\u001b[39m\n\u001b[31m  document.getElementById('st-pending').textContent = allCons.filter(c => !c.docket || c.status === 'pending').length;·\u001b[39m\n\u001b[31m  const grid = document.getElementById('dashGrid');\u001b[39m\n\u001b[31m  // Flatten all consignees across dispatches\u001b[39m\n\u001b[31m  let items = activeDis.flatMap(d =>\u001b[39m\n\u001b[31m    (d.consignees || []).filter(c => c.docket).map(c => ({...c, dispatchId: d.id, courier: d.courier, client: d.client, jobref: d.jobref}))\u001b[39m\n\u001b[31m  );\u001b[39m\n\u001b[31m  if (dashFilter !== 'all') {\u001b[39m\n\u001b[31m    items = items.filter(c => {\u001b[39m\n\u001b[31m      if (dashFilter === 'delivered') return c.status === 'delivered';\u001b[39m\n\u001b[31m      if (dashFilter === 'transit') return c.status === 'transit' || c.status === 'in-transit';\u001b[39m\n\u001b[31m      if (dashFilter === 'booked') return c.status === 'booked';\u001b[39m\n\u001b[31m      if (dashFilter === 'pending') return !c.docket || c.status === 'pending';\u001b[39m\n\u001b[31m      return true;\u001b[39m\n\u001b[31m    });\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m  if (!items.length) {\u001b[39m\n\u001b[31m    grid.innerHTML = '<div class=\\\"empty\\\"><div class=\\\"e-icon\\\">📦</div><p>No docket-tracked consignees.<br>Add docket numbers to see tiles.</p></div>';\u001b[39m\n\u001b[31m    return;\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m  const progMap = { delivered:100, transit:60,'in-transit':60, booked:20, pending:5 };\u001b[39m\n\u001b[31m  const progClr = { delivered:'var(--grn)', transit:'var(--ylw)','in-transit':'var(--ylw)', booked:'var(--sky)', pending:'var(--t4)' };\u001b[39m\n\u001b[31m  grid.innerHTML = items.map(c => {\u001b[39m\n\u001b[31m    const sp = c.status || 'booked';\u001b[39m\n\u001b[31m    const prog = progMap[sp] || 20;\u001b[39m\n\u001b[31m    const clr = progClr[sp] || 'var(--t4)';\u001b[39m\n\u001b[31m    return `\u001b[39m\n\u001b[31m    <div class=\\\"docket-tile\\\" onclick=\\\"showDocketDetail('${c.docket}','${c.courier||''}','${c.dispatchId}')\\\">\u001b[39m\n\u001b[31m      <div class=\\\"docket-tile-hdr\\\">\u001b[39m\n\u001b[31m        <div class=\\\"docket-tile-left\\\">\u001b[39m\n\u001b[31m          <div class=\\\"docket-num\\\">${c.docket}</div>\u001b[39m\n\u001b[31m          <div class=\\\"docket-store\\\">${c.name} — ${c.city}</div>\u001b[39m\n\u001b[31m          <div class=\\\"docket-courier\\\">${c.courier||''} · ${c.client||''} ${c.jobref ? '› '+c.jobref : ''}</div>\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m        <span class=\\\"status-pill sp-${sp}\\\">${sp}</span>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      <div style=\\\"display:flex;gap:6px;margin-bottom:6px;align-items:center;\\\">\u001b[39m\n\u001b[31m        <input type=\\\"checkbox\\\" id=\\\"sel_${c.docket.replace(/[^a-z0-9]/gi,'_')}\\\"\u001b[39m\n\u001b[31m          onclick=\\\"event.stopPropagation();toggleTileSelect('${c.docket}')\\\"\u001b[39m\n\u001b[31m          style=\\\"width:14px;height:14px;cursor:pointer;accent-color:var(--acc);\\\">\u001b[39m\n\u001b[31m        <label style=\\\"font-size:.6rem;color:var(--t3);font-family:var(--fnt-mono);cursor:pointer;\\\"\u001b[39m\n\u001b[31m          onclick=\\\"event.stopPropagation();toggleTileSelect('${c.docket}')\\\">Select</label>\u001b[39m\n\u001b[31m        ${c.manualStatus ?\u001b[39m\n\u001b[31m          `<span style=\\\"font-size:.6rem;font-family:var(--fnt-mono);color:var(--grn);\\\">📞 ${c.manualStatus}</span>` : ''}\u001b[39m\n\u001b[31m        ${c.podUploaded ? '<span style=\\\"font-size:.6rem;color:var(--grn);font-family:var(--fnt-mono);\\\">✓ POD</span>' : ''}\u001b[39m\n\u001b[31m        <button onclick=\\\"event.stopPropagation();showManualTracking('${c.docket}','${c.dispatchId}','${c.name}','${c.phone||''}','${c.city||''}')\\\"\u001b[39m\n\u001b[31m          style=\\\"font-size:.6rem;background:rgba(56,189,248,.1);border:1px solid rgba(56,189,248,.3);\u001b[39m\n\u001b[31m          color:var(--sky);padding:2px 7px;border-radius:10px;cursor:pointer;font-family:var(--fnt-mono);\\\">📞 Call Log</button>\u001b[39m\n\u001b[31m        <button onclick=\\\"event.stopPropagation();showPODUpload('${c.docket}','${c.dispatchId}')\\\"\u001b[39m\n\u001b[31m          style=\\\"font-size:.6rem;background:none;border:1px solid var(--bdr);color:var(--t3);\u001b[39m\n\u001b[31m          padding:2px 7px;border-radius:10px;cursor:pointer;font-family:var(--fnt-mono);\\\">📸 POD</button>\u001b[39m\n\u001b[31m        ${c.manualDelivered ? `<button onclick=\\\"event.stopPropagation();resumeAutoTracking('${c.docket}','${c.dispatchId}')\\\"\u001b[39m\n\u001b[31m          style=\\\"font-size:.6rem;background:rgba(251,146,60,.1);border:1px solid rgba(251,146,60,.4);\u001b[39m\n\u001b[31m          color:#fb923c;padding:2px 7px;border-radius:10px;cursor:pointer;font-family:var(--fnt-mono);\\\">↩ Undo</button>` : ''}\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      <div class=\\\"docket-tile-footer\\\">\u001b[39m\n\u001b[31m        <div style=\\\"flex:1;min-width:0;\\\">\u001b[39m\n\u001b[31m          <span class=\\\"docket-last-event docket-evt-${c.docket}\\\" id=\\\"evt_${c.docket.replace(/[^a-z0-9]/gi,'_')}\\\">${c.lastEvent || 'No tracking yet'}</span>\u001b[39m\n\u001b[31m          <span class=\\\"track-spinner\\\" id=\\\"spin_${c.docket.replace(/[^a-z0-9]/gi,'_')}\\\" style=\\\"display:none;color:var(--ylw);font-family:var(--fnt-mono);font-size:.68rem;animation:blink 1s infinite;\\\">⏳ tracking...</span>\u001b[39m\n\u001b[31m          ${c.lastScanLocation && c.lastScanLocation !== c.lastEvent ?·\u001b[39m\n\u001b[31m            `<div style=\\\"font-size:.62rem;color:var(--t3);font-family:var(--fnt-mono);margin-top:2px;\\\">📍 ${c.lastScanLocation}</div>` : ''}\u001b[39m\n\u001b[31m          ${c.lastTrackedAt ?·\u001b[39m\n\u001b[31m            `<div style=\\\"font-size:.6rem;color:var(--t4);font-family:var(--fnt-mono);margin-top:1px;\\\">🕐 tracked ${new Date(c.lastTrackedAt).toLocaleTimeString('en-IN',{hour:'2-digit',minute:'2-digit'})}</div>` : ''}\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m        <button class=\\\"btn-track-live live-btn-${c.docket.replace(/[^a-z0-9]/gi,'_')}\\\" onclick=\\\"event.stopPropagation();liveTrackTile('${c.docket}','${c.courier||''}','${c.dispatchId}')\\\">\u001b[39m\n\u001b[31m          <span class=\\\"live-dot\\\"></span>LIVE\u001b[39m\n\u001b[31m        </button>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      <div class=\\\"docket-progress\\\">\u001b[39m\n\u001b[31m        <div class=\\\"docket-progress-fill\\\" style=\\\"width:${prog}%;background:${clr};\\\"></div>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m    </div>`;\u001b[39m\n\u001b[31m  }).join('');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction filterDash(f, el) {\u001b[39m\n\u001b[31m  dashFilter = f;\u001b[39m\n\u001b[31m  document.querySelectorAll('#dashFilters .btn-sm').forEach(b => b.classList.remove('primary'));\u001b[39m\n\u001b[31m  el.classList.add('primary');\u001b[39m\n\u001b[31m  renderDashboard();\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31masync function liveTrackTile(docket, courier, dispatchId) {\u001b[39m\n\u001b[31m  showToast(`Tracking ${docket}...`, 'info');\u001b[39m\n\u001b[31m  const result = await fetchTracking(docket, courier);\u001b[39m\n\u001b[31m  // Update status in dispatches\u001b[39m\n\u001b[31m  for (const d of dispatches) {\u001b[39m\n\u001b[31m    if (d.id !== dispatchId) continue;\u001b[39m\n\u001b[31m    for (const c of d.consignees) {\u001b[39m\n\u001b[31m      if (c.docket === docket) {\u001b[39m\n\u001b[31m        c.status = result.status || c.status;\u001b[39m\n\u001b[31m        c.lastEvent = result.lastEvent || c.lastEvent;\u001b[39m\n\u001b[31m        c.lastUpdated = new Date().toISOString();\u001b[39m\n\u001b[31m      }\u001b[39m\n\u001b[31m    }\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m  saveData();\u001b[39m\n\u001b[31m  renderDashboard();\u001b[39m\n\u001b[31m  if (result.status === 'failed') {\u001b[39m\n\u001b[31m    // Open courier site as fallback\u001b[39m\n\u001b[31m    openCourierTrackUrl(docket, courier);\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction showDocketDetail(docket, courier, dispatchId) {\u001b[39m\n\u001b[31m  const d = dispatches.find(x => x.id === dispatchId);\u001b[39m\n\u001b[31m  const c = d?.consignees?.find(x => x.docket === docket);\u001b[39m\n\u001b[31m  if (!c) return;\u001b[39m\n\u001b[31m  const content = document.getElementById('detailContent');\u001b[39m\n\u001b[31m  content.innerHTML = `\u001b[39m\n\u001b[31m    <div class=\\\"sheet-title\\\">${docket}</div>\u001b[39m\n\u001b[31m    <div class=\\\"info-tag\\\">📍 <strong>${c.name}</strong> — ${c.city}</div>\u001b[39m\n\u001b[31m    ${c.phone ? `<div class=\\\"info-tag\\\">📞 ${c.phone}</div>` : ''}\u001b[39m\n\u001b[31m    <div class=\\\"info-tag\\\">📦 ${c.material||'—'} ${c.qty ? '('+c.qty+' pcs)':''}</div>\u001b[39m\n\u001b[31m    <div class=\\\"info-tag\\\">🚚 ${courier||'—'} · <span class=\\\"status-pill sp-${c.status||'pending'}\\\">${c.status||'pending'}</span></div>\u001b[39m\n\u001b[31m    ${c.lastEvent ? `<div class=\\\"info-tag\\\">🕐 ${c.lastEvent}</div>` : ''}\u001b[39m\n\u001b[31m    <div class=\\\"sec-div\\\"><span>Tracking History</span></div>\u001b[39m\n\u001b[31m    <div id=\\\"docketTimeline\\\">\u001b[39m\n\u001b[31m      ${c.timeline?.length ?\u001b[39m\n\u001b[31m        c.timeline.map(e => `<div class=\\\"track-evt\\\"><span class=\\\"track-time\\\">${e.time||''}</span><div><div class=\\\"track-desc\\\">${e.desc||''}</div><div class=\\\"track-loc\\\">${e.loc||''}</div></div></div>`).join('') :\u001b[39m\n\u001b[31m        '<div style=\\\"color:var(--t3);font-size:.78rem;padding:10px 0;\\\">No timeline data. Click Live Track to fetch.</div>'}\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m    <div style=\\\"display:flex;gap:6px;flex-wrap:wrap;margin-top:14px;\\\">\u001b[39m\n\u001b[31m      <button class=\\\"btn btn-ghost\\\" style=\\\"flex:1;\\\" onclick=\\\"openCourierTrackUrl('${docket}','${courier}')\\\">🔗 Courier Site</button>\u001b[39m\n\u001b[31m      <button class=\\\"btn btn-orange\\\" style=\\\"flex:1;\\\" onclick=\\\"liveTrackTile('${docket}','${courier}','${dispatchId}');closeOverlay('detailOverlay')\\\">⚡ Live Track</button>\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m    <div style=\\\"display:flex;gap:6px;flex-wrap:wrap;margin-top:6px;\\\">\u001b[39m\n\u001b[31m      <button class=\\\"btn-wa\\\" style=\\\"flex:1;padding:8px;\\\" onclick=\\\"closeOverlay('detailOverlay');showPODUpload('${docket}','${dispatchId}')\\\">📸 Upload POD</button>\u001b[39m\n\u001b[31m      ${c.podUploaded ? '<span style=\\\"color:var(--grn);font-size:.72rem;font-family:var(--fnt-mono);padding:6px;\\\">✓ POD uploaded</span>' : ''}\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m  `;\u001b[39m\n\u001b[31m  openOverlay('detailOverlay');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31m// TRACKING ENGINE\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31mfunction _normaliseStatus(raw) {\u001b[39m\n\u001b[31m  const s = (raw || '').toLowerCase().trim();\u001b[39m\n\u001b[31m  if (!s || s === 'error') return null;\u001b[39m\n\u001b[31m  if (s.includes('undeliver') || s.includes('delivery_fail') ||\u001b[39m\n\u001b[31m      s.includes('failed_delivery') || s.includes('not deliver') ||\u001b[39m\n\u001b[31m      s.includes('return')  || s.includes('rto')) return 'pending';\u001b[39m\n\u001b[31m  if (s.includes('deliver'))                       return 'delivered';\u001b[39m\n\u001b[31m  if (s.includes('transit') || s.includes('hub') ||\u001b[39m\n\u001b[31m      s.includes('inbound') || s.includes('outbound') ||\u001b[39m\n\u001b[31m      s.includes('out for'))                       return 'transit';\u001b[39m\n\u001b[31m  if (s.includes('book') || s.includes('pick'))   return 'booked';\u001b[39m\n\u001b[31m  return s;\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31masync function _fetchWithRetry(url, opts, maxRetries = 1, retryDelayMs = 4000) {\u001b[39m\n\u001b[31m  for (let attempt = 0; attempt <= maxRetries; attempt++) {\u001b[39m\n\u001b[31m    try {\u001b[39m\n\u001b[31m      const resp = await fetch(url, opts);\u001b[39m\n\u001b[31m      if (resp.ok) return resp;\u001b[39m\n\u001b[31m    } catch(e) {\u001b[39m\n\u001b[31m      if (attempt === maxRetries) throw e;\u001b[39m\n\u001b[31m      await new Promise(r => setTimeout(r, retryDelayMs));\u001b[39m\n\u001b[31m    }\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m  return null;\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31masync function fetchTracking(docket, courier) {\u001b[39m\n\u001b[31m  // Strategy 1: VPS proxy (SkyKing + others that block direct fetch)\u001b[39m\n\u001b[31m  const proxyUrl = window.appConfig.proxy_url;\u001b[39m\n\u001b[31m  if (proxyUrl) {\u001b[39m\n\u001b[31m    try {\u001b[39m\n\u001b[31m      const url  = `${proxyUrl}?courier=${encodeURIComponent(courier)}&docket=${encodeURIComponent(docket)}`;\u001b[39m\n\u001b[31m      const resp = await _fetchWithRetry(url, { signal: AbortSignal.timeout(15000) });\u001b[39m\n\u001b[31m      if (resp) {\u001b[39m\n\u001b[31m        const data      = await resp.json();\u001b[39m\n\u001b[31m        const normStatus = _normaliseStatus(data.status);\u001b[39m\n\u001b[31m        if (normStatus) {\u001b[39m\n\u001b[31m          return { status: normStatus, lastEvent: data.lastEvent || '', timeline: data.timeline || [] };\u001b[39m\n\u001b[31m        }\u001b[39m\n\u001b[31m        openCourierTrackUrl(docket, courier);\u001b[39m\n\u001b[31m        return { status: 'transit', lastEvent: data.lastEvent || 'Check courier site', timeline: [] };\u001b[39m\n\u001b[31m      }\u001b[39m\n\u001b[31m    } catch(e) {}\u001b[39m\n\u001b[31m  }·\u001b[39m\n\u001b[31m  // Strategy 2: Delhivery direct API\u001b[39m\n\u001b[31m  if (courier === 'Delhivery') {\u001b[39m\n\u001b[31m    try {\u001b[39m\n\u001b[31m      const resp = await _fetchWithRetry(\u001b[39m\n\u001b[31m        `https://dlv-api.delhivery.com/v3/track?waybill=${docket}`,\u001b[39m\n\u001b[31m        { signal: AbortSignal.timeout(8000) }\u001b[39m\n\u001b[31m      );\u001b[39m\n\u001b[31m      if (resp) {\u001b[39m\n\u001b[31m        const data = await resp.json();\u001b[39m\n\u001b[31m        const pkg  = data.ShipmentData?.[0]?.Shipment;\u001b[39m\n\u001b[31m        if (pkg) {\u001b[39m\n\u001b[31m          const normStatus = _normaliseStatus(pkg.Status?.Status);\u001b[39m\n\u001b[31m          return { status: normStatus || 'transit', lastEvent: pkg.Status?.Instructions || '', timeline: [] };\u001b[39m\n\u001b[31m        }\u001b[39m\n\u001b[31m      }\u001b[39m\n\u001b[31m    } catch(e) {}\u001b[39m\n\u001b[31m  }·\u001b[39m\n\u001b[31m  return { status: 'failed', lastEvent: 'Open courier site to check', timeline: [] };\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction openCourierTrackUrl(docket, courier) {\u001b[39m\n\u001b[31m  const m = COURIER_MASTER[courier];\u001b[39m\n\u001b[31m  if (!m) return;\u001b[39m\n\u001b[31m  const url = m.trackUrl.replace('{docket}', docket);\u001b[39m\n\u001b[31m  window.open(url, '_blank');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// Track tab - multiple dockets\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31m// TRACK TAB — new logic\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31mlet _trackSelected = new Set(); // dockets currently selected for tracking·\u001b[39m\n\u001b[31m// Called when Track tab opens or courier changes\u001b[39m\n\u001b[31mfunction buildTrackDocketChips() {\u001b[39m\n\u001b[31m  const courier = document.getElementById('t_courier').value;\u001b[39m\n\u001b[31m  const m = COURIER_MASTER[courier];\u001b[39m\n\u001b[31m  const maxBulk = m?.maxBulk || 1;\u001b[39m\n\u001b[31m  const limitEl = document.getElementById('trackCourierLimit');\u001b[39m\n\u001b[31m  if (limitEl) limitEl.textContent = `Max ${maxBulk} at once for ${courier}`;·\u001b[39m\n\u001b[31m  // Gather all dockets from active dispatches for this courier\u001b[39m\n\u001b[31m  const activeDis = dispatches.filter(d => d.status !== 'draft');\u001b[39m\n\u001b[31m  const allDockets = activeDis.flatMap(d =>\u001b[39m\n\u001b[31m    (d.consignees || [])\u001b[39m\n\u001b[31m      .filter(c => c.docket && d.courier === courier)\u001b[39m\n\u001b[31m      .map(c => ({ docket: c.docket, name: c.name, city: c.city, status: c.status || 'booked' }))\u001b[39m\n\u001b[31m  );·\u001b[39m\n\u001b[31m  // Also include dockets from OTHER couriers as un-selectable reference\u001b[39m\n\u001b[31m  const otherDockets = activeDis.flatMap(d =>\u001b[39m\n\u001b[31m    (d.consignees || [])\u001b[39m\n\u001b[31m      .filter(c => c.docket && d.courier !== courier)\u001b[39m\n\u001b[31m      .map(c => ({ docket: c.docket, name: c.name, courier: d.courier }))\u001b[39m\n\u001b[31m  );·\u001b[39m\n\u001b[31m  const chips = document.getElementById('trackDocketChips');\u001b[39m\n\u001b[31m  if (!allDockets.length && !otherDockets.length) {\u001b[39m\n\u001b[31m    chips.innerHTML = '<span style=\\\"font-size:.72rem;color:var(--t3);font-family:var(--fnt-mono);\\\">No dispatched dockets yet — enter manually below</span>';\u001b[39m\n\u001b[31m    return;\u001b[39m\n\u001b[31m  }·\u001b[39m\n\u001b[31m  const spClr = { delivered:'var(--grn)', transit:'var(--ylw)', booked:'var(--sky)', pending:'var(--t4)' };·\u001b[39m\n\u001b[31m  chips.innerHTML = [\u001b[39m\n\u001b[31m    // This courier's dockets\u001b[39m\n\u001b[31m    ...allDockets.map(d => {\u001b[39m\n\u001b[31m      const sel = _trackSelected.has(d.docket);\u001b[39m\n\u001b[31m      const clr = spClr[d.status] || 'var(--t4)';\u001b[39m\n\u001b[31m      return `<button onclick=\\\"toggleTrackDocket('${d.docket}')\\\" id=\\\"chip_${d.docket}\\\"\u001b[39m\n\u001b[31m        style=\\\"font-family:var(--fnt-mono);font-size:.72rem;padding:4px 10px;border-radius:20px;\u001b[39m\n\u001b[31m        cursor:pointer;transition:all .2s;border:1px solid ${sel ? 'var(--acc)' : 'var(--bdr)'};\u001b[39m\n\u001b[31m        background:${sel ? 'rgba(249,115,22,.15)' : 'var(--sur2)'};\u001b[39m\n\u001b[31m        color:${sel ? 'var(--acc)' : 'var(--t2)'};\\\">\u001b[39m\n\u001b[31m        ${d.docket} <span style=\\\"font-size:.6rem;color:${clr};\\\">● ${d.status}</span>\u001b[39m\n\u001b[31m        ${d.city ? `<span style=\\\"font-size:.6rem;color:var(--t3);margin-left:4px;\\\">${d.city}</span>` : ''}\u001b[39m\n\u001b[31m      </button>`;\u001b[39m\n\u001b[31m    }),\u001b[39m\n\u001b[31m    // Other couriers — show greyed, click selects and switches courier\u001b[39m\n\u001b[31m    ...otherDockets.slice(0, 5).map(d =>\u001b[39m\n\u001b[31m      `<span title=\\\"${d.courier}\\\" style=\\\"font-family:var(--fnt-mono);font-size:.68rem;padding:3px 8px;\u001b[39m\n\u001b[31m        border-radius:20px;border:1px dashed var(--bdr);color:var(--t4);cursor:default;\\\">\u001b[39m\n\u001b[31m        ${d.docket} <span style=\\\"font-size:.55rem;\\\">${d.courier}</span>\u001b[39m\n\u001b[31m      </span>`\u001b[39m\n\u001b[31m    )\u001b[39m\n\u001b[31m  ].join('');·\u001b[39m\n\u001b[31m  updateTrackSummary();\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction toggleTrackDocket(docket) {\u001b[39m\n\u001b[31m  const courier = document.getElementById('t_courier').value;\u001b[39m\n\u001b[31m  const m = COURIER_MASTER[courier];\u001b[39m\n\u001b[31m  const maxBulk = m?.maxBulk || 1;·\u001b[39m\n\u001b[31m  if (_trackSelected.has(docket)) {\u001b[39m\n\u001b[31m    _trackSelected.delete(docket);\u001b[39m\n\u001b[31m  } else {\u001b[39m\n\u001b[31m    if (_trackSelected.size >= maxBulk) {\u001b[39m\n\u001b[31m      showToast(`${courier} supports max ${maxBulk} docket(s) at once`, 'err');\u001b[39m\n\u001b[31m      return;\u001b[39m\n\u001b[31m    }\u001b[39m\n\u001b[31m    _trackSelected.add(docket);\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m  buildTrackDocketChips(); // re-render chips with updated selection\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction addManualDockets() {\u001b[39m\n\u001b[31m  const raw = document.getElementById('t_manual_dockets').value;\u001b[39m\n\u001b[31m  const courier = document.getElementById('t_courier').value;\u001b[39m\n\u001b[31m  const m = COURIER_MASTER[courier];\u001b[39m\n\u001b[31m  const maxBulk = m?.maxBulk || 1;\u001b[39m\n\u001b[31m  const newDockets = raw.split(/[,\\\\n]/).map(d => d.trim()).filter(Boolean);\u001b[39m\n\u001b[31m  let added = 0;\u001b[39m\n\u001b[31m  for (const d of newDockets) {\u001b[39m\n\u001b[31m    if (_trackSelected.size >= maxBulk) {\u001b[39m\n\u001b[31m      showToast(`Max ${maxBulk} reached for ${courier}`, 'err');\u001b[39m\n\u001b[31m      break;\u001b[39m\n\u001b[31m    }\u001b[39m\n\u001b[31m    _trackSelected.add(d);\u001b[39m\n\u001b[31m    added++;\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m  document.getElementById('t_manual_dockets').value = '';\u001b[39m\n\u001b[31m  buildTrackDocketChips();\u001b[39m\n\u001b[31m  if (added) showToast(`${added} docket(s) added ✓`, 'ok');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction clearTrackSelection() {\u001b[39m\n\u001b[31m  _trackSelected.clear();\u001b[39m\n\u001b[31m  buildTrackDocketChips();\u001b[39m\n\u001b[31m  document.getElementById('trackResultsArea').innerHTML = '';\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction updateTrackSummary() {\u001b[39m\n\u001b[31m  const summary = document.getElementById('trackSelectedSummary');\u001b[39m\n\u001b[31m  const countEl = document.getElementById('trackSelectedCount');\u001b[39m\n\u001b[31m  const listEl  = document.getElementById('trackSelectedList');\u001b[39m\n\u001b[31m  if (!summary) return;\u001b[39m\n\u001b[31m  const arr = Array.from(_trackSelected);\u001b[39m\n\u001b[31m  summary.style.display = arr.length ? 'block' : 'none';\u001b[39m\n\u001b[31m  if (countEl) countEl.textContent = `${arr.length} docket(s) selected`;\u001b[39m\n\u001b[31m  if (listEl)  listEl.textContent  = arr.slice(0, 5).join(', ') + (arr.length > 5 ? `... +${arr.length-5}` : '');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction openTrackOnCourierSite() {\u001b[39m\n\u001b[31m  const courier = document.getElementById('t_courier').value;\u001b[39m\n\u001b[31m  const dockets = Array.from(_trackSelected);\u001b[39m\n\u001b[31m  if (!dockets.length) { showToast('Select dockets first', 'err'); return; }\u001b[39m\n\u001b[31m  const m = COURIER_MASTER[courier];\u001b[39m\n\u001b[31m  if (!m) return;\u001b[39m\n\u001b[31m  // SkyKing: open with all CNOs comma-separated in URL\u001b[39m\n\u001b[31m  let url;\u001b[39m\n\u001b[31m  if (courier === 'SkyKing') {\u001b[39m\n\u001b[31m    url = `https://skyking.co/track?cno=${dockets.join(',')}`;\u001b[39m\n\u001b[31m  } else if (dockets.length === 1) {\u001b[39m\n\u001b[31m    url = m.trackUrl.replace('{docket}', dockets[0]);\u001b[39m\n\u001b[31m  } else {\u001b[39m\n\u001b[31m    url = m.trackBulkUrl || m.trackUrl.replace('{docket}', dockets[0]);\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m  window.open(url, '_blank');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31masync function trackSelected() {\u001b[39m\n\u001b[31m  const courier = document.getElementById('t_courier').value;\u001b[39m\n\u001b[31m  const dockets = Array.from(_trackSelected);\u001b[39m\n\u001b[31m  if (!dockets.length) { showToast('Select at least one docket', 'err'); return; }·\u001b[39m\n\u001b[31m  const area = document.getElementById('trackResultsArea');\u001b[39m\n\u001b[31m  area.innerHTML = `<div class=\\\"card\\\" style=\\\"text-align:center;padding:20px;\\\">\u001b[39m\n\u001b[31m    <div style=\\\"font-size:1.5rem;margin-bottom:8px;\\\">⏳</div>\u001b[39m\n\u001b[31m    <div style=\\\"font-family:var(--fnt-mono);font-size:.78rem;color:var(--t2);\\\">Tracking ${dockets.length} docket(s) via ${courier}...</div>\u001b[39m\n\u001b[31m  </div>`;·\u001b[39m\n\u001b[31m  let resultsHTML = '';\u001b[39m\n\u001b[31m  for (const docket of dockets) {\u001b[39m\n\u001b[31m    const result = await fetchTracking(docket, courier);\u001b[39m\n\u001b[31m    const sp = (!result.status || result.status === 'failed') ? 'pending' : result.status;\u001b[39m\n\u001b[31m    const spClr = { delivered:'var(--grn)', transit:'var(--ylw)', booked:'var(--sky)', pending:'var(--t3)' };\u001b[39m\n\u001b[31m    const clr = spClr[sp] || 'var(--t3)';\u001b[39m\n\u001b[31m    // Update stored dispatch if this docket exists in our records\u001b[39m\n\u001b[31m    for (const d of dispatches) {\u001b[39m\n\u001b[31m      for (const c of (d.consignees||[])) {\u001b[39m\n\u001b[31m        if (c.docket === docket && result.status && result.status !== 'failed') {\u001b[39m\n\u001b[31m          c.status = result.status;\u001b[39m\n\u001b[31m          c.lastEvent = result.lastEvent;\u001b[39m\n\u001b[31m          c.lastTrackedAt = new Date().toISOString();\u001b[39m\n\u001b[31m        }\u001b[39m\n\u001b[31m      }\u001b[39m\n\u001b[31m    }\u001b[39m\n\u001b[31m    resultsHTML += `\u001b[39m\n\u001b[31m    <div style=\\\"background:var(--sur);border:1px solid var(--bdr);border-left:3px solid ${clr};\u001b[39m\n\u001b[31m      border-radius:var(--r-sm);padding:10px 12px;margin-bottom:8px;\\\">\u001b[39m\n\u001b[31m      <div style=\\\"display:flex;justify-content:space-between;align-items:center;margin-bottom:6px;\\\">\u001b[39m\n\u001b[31m        <span style=\\\"font-family:var(--fnt-mono);font-size:.9rem;color:var(--sky);\\\">${docket}</span>\u001b[39m\n\u001b[31m        <span class=\\\"status-pill sp-${sp}\\\">${sp}</span>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      <div style=\\\"font-size:.75rem;color:var(--t2);font-family:var(--fnt-mono);\\\">${result.lastEvent || '—'}</div>\u001b[39m\n\u001b[31m      ${result.lastScanLocation ? `<div style=\\\"font-size:.68rem;color:var(--t3);margin-top:3px;\\\">📍 ${result.lastScanLocation}</div>` : ''}\u001b[39m\n\u001b[31m      <div style=\\\"display:flex;gap:8px;margin-top:8px;\\\">\u001b[39m\n\u001b[31m        <button class=\\\"btn-sm\\\" onclick=\\\"openCourierTrackUrl('${docket}','${courier}')\\\">🔗 Open on ${courier}</button>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m    </div>`;\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m  saveData();\u001b[39m\n\u001b[31m  area.innerHTML = `<div style=\\\"margin-top:4px;\\\">${resultsHTML}</div>`;\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// Keep old names as aliases for compatibility\u001b[39m\n\u001b[31mfunction trackMultiple() { trackSelected(); }\u001b[39m\n\u001b[31mfunction openCourierSite() { openTrackOnCourierSite(); }·\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31m// HISTORY\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31mfunction renderHistory() {\u001b[39m\n\u001b[31m  const list = document.getElementById('historyList');\u001b[39m\n\u001b[31m  if (!dispatches.length) {\u001b[39m\n\u001b[31m    list.innerHTML = '<div class=\\\"empty\\\"><div class=\\\"e-icon\\\">📭</div><p>No dispatches yet.</p></div>';\u001b[39m\n\u001b[31m    return;\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m  list.innerHTML = dispatches.map(d => {\u001b[39m\n\u001b[31m    const cons = d.consignees?.length || 0;\u001b[39m\n\u001b[31m    const docketed = d.consignees?.filter(c=>c.docket).length || 0;\u001b[39m\n\u001b[31m    const clr = d.status === 'draft' ? 'var(--ylw)' : 'var(--grn)';\u001b[39m\n\u001b[31m    return `\u001b[39m\n\u001b[31m    <div class=\\\"docket-tile\\\" onclick=\\\"showDispatchDetail('${d.id}')\\\">\u001b[39m\n\u001b[31m      <div class=\\\"docket-tile-hdr\\\">\u001b[39m\n\u001b[31m        <div class=\\\"docket-tile-left\\\">\u001b[39m\n\u001b[31m          <div class=\\\"docket-num\\\" style=\\\"font-size:.78rem;\\\">${d.id}</div>\u001b[39m\n\u001b[31m          <div class=\\\"docket-store\\\">${d.client||'No client'} ${d.jobref ? '› '+d.jobref : ''}</div>\u001b[39m\n\u001b[31m          <div class=\\\"docket-courier\\\">📦 ${cons} consignees · ${docketed} docketed · ${d.courier||'—'} · ${d.dispatchDate||d.createdAt?.slice(0,10)||'—'}</div>\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m        <span class=\\\"status-pill\\\" style=\\\"background:rgba(0,0,0,.2);color:${clr};\\\">${d.status||'active'}</span>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m    </div>`;\u001b[39m\n\u001b[31m  }).join('');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction showDispatchDetail(id) {\u001b[39m\n\u001b[31m  const d = dispatches.find(x => x.id === id);\u001b[39m\n\u001b[31m  if (!d) return;\u001b[39m\n\u001b[31m  const content = document.getElementById('detailContent');\u001b[39m\n\u001b[31m  content.innerHTML = `\u001b[39m\n\u001b[31m    <div class=\\\"sheet-title\\\">${d.client} ${d.jobref ? '› '+d.jobref : ''}</div>\u001b[39m\n\u001b[31m    <div class=\\\"info-tag\\\"><strong>${d.id}</strong> · ${d.courier||'—'} · ${d.dispatchDate||'—'}</div>\u001b[39m\n\u001b[31m    <div class=\\\"sec-div\\\"><span>Consignees (${d.consignees?.length||0})</span></div>\u001b[39m\n\u001b[31m    ${(d.consignees||[]).map((c,i) => `\u001b[39m\n\u001b[31m      <div style=\\\"background:var(--sur2);border:1px solid var(--bdr);border-radius:var(--r-sm);padding:10px;margin-bottom:6px;\\\">\u001b[39m\n\u001b[31m        <div style=\\\"font-weight:600;margin-bottom:4px;\\\">${i+1}. ${c.name} — ${c.city}</div>\u001b[39m\n\u001b[31m        ${c.docket ? `<div style=\\\"font-family:var(--fnt-mono);font-size:.78rem;color:var(--sky);\\\">🔖 ${c.docket}</div>` : ''}\u001b[39m\n\u001b[31m        ${c.material ? `<div style=\\\"font-size:.75rem;color:var(--t2);\\\">📦 ${c.material} ${c.qty ? '('+c.qty+')':''}</div>` : ''}\u001b[39m\n\u001b[31m        <span class=\\\"status-pill sp-${c.status||'pending'}\\\" style=\\\"margin-top:4px;display:inline-block;\\\">${c.status||'pending'}</span>\u001b[39m\n\u001b[31m      </div>`).join('')}\u001b[39m\n\u001b[31m    ${d.remarks ? `<div class=\\\"info-tag\\\">📝 ${d.remarks}</div>` : ''}\u001b[39m\n\u001b[31m    <div class=\\\"btn-row\\\" style=\\\"margin-top:14px;\\\">\u001b[39m\n\u001b[31m      <button class=\\\"btn btn-ghost\\\" onclick=\\\"deleteDispatch('${d.id}');closeOverlay('detailOverlay')\\\">🗑️ Delete</button>\u001b[39m\n\u001b[31m      <button class=\\\"btn btn-orange\\\" onclick=\\\"closeOverlay('detailOverlay');showPane('dashboard')\\\">📊 View in Dashboard</button>\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m  `;\u001b[39m\n\u001b[31m  openOverlay('detailOverlay');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction deleteDispatch(id) {\u001b[39m\n\u001b[31m  if (!confirm('Delete this dispatch?')) return;\u001b[39m\n\u001b[31m  dispatches = dispatches.filter(d => d.id !== id);\u001b[39m\n\u001b[31m  saveData();\u001b[39m\n\u001b[31m  updateHistCount();\u001b[39m\n\u001b[31m  renderHistory();\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction updateHistCount() {\u001b[39m\n\u001b[31m  document.getElementById('histCount').textContent = dispatches.length;\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31m// GOOGLE SHEETS\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31masync function logToSheets(d) {\u001b[39m\n\u001b[31m  const url = window.appConfig.webhook_url;\u001b[39m\n\u001b[31m  if (!url) return;\u001b[39m\n\u001b[31m  try {\u001b[39m\n\u001b[31m    await fetch(url, {\u001b[39m\n\u001b[31m      method: 'POST', mode: 'no-cors',\u001b[39m\n\u001b[31m      headers: { 'Content-Type': 'application/json' },\u001b[39m\n\u001b[31m      body: JSON.stringify({\u001b[39m\n\u001b[31m        type: 'dispatch', id: d.id, client: d.client, jobref: d.jobref,\u001b[39m\n\u001b[31m        courier: d.courier, consignees: d.consignees?.length||0,\u001b[39m\n\u001b[31m        destinations: d.consignees?.map(c=>c.city).join(', ')||'',\u001b[39m\n\u001b[31m        dockets: d.consignees?.filter(c=>c.docket).map(c=>c.docket).join(', ')||'',\u001b[39m\n\u001b[31m        dispatchDate: d.dispatchDate, status: d.status,\u001b[39m\n\u001b[31m        timestamp: new Date().toISOString(), company: window.appConfig.company\u001b[39m\n\u001b[31m      })\u001b[39m\n\u001b[31m    });\u001b[39m\n\u001b[31m  } catch(e) {}\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31m// CONFIG — Google Sheet is the source of truth\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════·\u001b[39m\n\u001b[31mfunction onSheetsUrlChange() {\u001b[39m\n\u001b[31m  const url = document.getElementById('cfg_sheets_url').value.trim();\u001b[39m\n\u001b[31m  // Show sync button state\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction saveSheetsUrl() {\u001b[39m\n\u001b[31m  const url = document.getElementById('cfg_sheets_url').value.trim();\u001b[39m\n\u001b[31m  if (!url) { showToast('Enter the Apps Script URL first', 'err'); return; }\u001b[39m\n\u001b[31m  window.appConfig.sheets_url = url;\u001b[39m\n\u001b[31m  saveSheetsUrlLocal();\u001b[39m\n\u001b[31m  syncFromSheet(false);\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m/**\u001b[39m\n\u001b[31m * Fetch all config from Google Sheet and apply to appConfig.\u001b[39m\n\u001b[31m * Called on startup (silent=true) and manually (silent=false).\u001b[39m\n\u001b[31m */\u001b[39m\n\u001b[31masync function syncFromSheet(silent = false) {\u001b[39m\n\u001b[31m  const url = window.appConfig.sheets_url;\u001b[39m\n\u001b[31m  if (!url) { if (!silent) showToast('No Sheets URL configured', 'err'); return; }·\u001b[39m\n\u001b[31m  setSyncStatus('syncing', 'Syncing config from Google Sheet...');\u001b[39m\n\u001b[31m  try {\u001b[39m\n\u001b[31m    const resp = await fetch(url + '?action=pwa_config', {\u001b[39m\n\u001b[31m      method: 'GET',\u001b[39m\n\u001b[31m      redirect: 'follow'\u001b[39m\n\u001b[31m    });\u001b[39m\n\u001b[31m    if (!resp.ok) throw new Error('HTTP ' + resp.status);\u001b[39m\n\u001b[31m    const cfg = await resp.json();·\u001b[39m\n\u001b[31m    // Apply all values from sheet to appConfig\u001b[39m\n\u001b[31m    const keyMap = {\u001b[39m\n\u001b[31m      proxy_url:           'proxy_url',\u001b[39m\n\u001b[31m      webhook_url:         'webhook_url',\u001b[39m\n\u001b[31m      ollama_url:          'ollama_url',\u001b[39m\n\u001b[31m      ocr_mode:            'ocr_mode',\u001b[39m\n\u001b[31m      company:             'company',\u001b[39m\n\u001b[31m      city:                'city',\u001b[39m\n\u001b[31m      auto_log:            'auto_log',\u001b[39m\n\u001b[31m      refresh_on_load:     'refresh_on_load',\u001b[39m\n\u001b[31m      admin_password:      'admin_password',\u001b[39m\n\u001b[31m      n8n_dispatch_webhook:'n8n_dispatch_webhook',\u001b[39m\n\u001b[31m    };\u001b[39m\n\u001b[31m    Object.entries(keyMap).forEach(([sheetKey, cfgKey]) => {\u001b[39m\n\u001b[31m      if (cfg[sheetKey] !== undefined && cfg[sheetKey] !== '') {\u001b[39m\n\u001b[31m        window.appConfig[cfgKey] = cfg[sheetKey];\u001b[39m\n\u001b[31m      }\u001b[39m\n\u001b[31m    });·\u001b[39m\n\u001b[31m    // Apply courier master if provided\u001b[39m\n\u001b[31m    if (cfg._courier_master && Array.isArray(cfg._courier_master)) {\u001b[39m\n\u001b[31m      cfg._courier_master.forEach(c => {\u001b[39m\n\u001b[31m        if (c.name && c.trackUrl) {\u001b[39m\n\u001b[31m          COURIER_MASTER[c.name] = {\u001b[39m\n\u001b[31m            trackUrl: c.trackUrl,\u001b[39m\n\u001b[31m            trackBulkUrl: c.bulkUrl || '',\u001b[39m\n\u001b[31m            maxBulk: parseInt(c.maxBulk) || 1\u001b[39m\n\u001b[31m          };\u001b[39m\n\u001b[31m        }\u001b[39m\n\u001b[31m      });\u001b[39m\n\u001b[31m    }·\u001b[39m\n\u001b[31m    renderCourierMasterList();\u001b[39m\n\u001b[31m    renderSheetConfigDisplay(cfg);\u001b[39m\n\u001b[31m    setSyncStatus('ok', 'Synced from sheet · ' + new Date().toLocaleTimeString());\u001b[39m\n\u001b[31m    if (!silent) showToast('Config synced from sheet ✓', 'ok');·\u001b[39m\n\u001b[31m    // If refresh_on_load is true, trigger dashboard refresh\u001b[39m\n\u001b[31m    if (window.appConfig.refresh_on_load === 'true') {\u001b[39m\n\u001b[31m      setTimeout(refreshAllTracking, 2000);\u001b[39m\n\u001b[31m    }·\u001b[39m\n\u001b[31m  } catch(e) {\u001b[39m\n\u001b[31m    setSyncStatus('err', 'Sync failed: ' + e.message);\u001b[39m\n\u001b[31m    if (!silent) showToast('Sync failed — check URL', 'err');\u001b[39m\n\u001b[31m    console.error('Sheets sync error:', e);\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction setSyncStatus(state, msg) {\u001b[39m\n\u001b[31m  const el = document.getElementById('syncStatus');\u001b[39m\n\u001b[31m  const icon = document.getElementById('syncIcon');\u001b[39m\n\u001b[31m  const msgEl = document.getElementById('syncMsg');\u001b[39m\n\u001b[31m  el.style.display = 'flex';\u001b[39m\n\u001b[31m  icon.textContent = state === 'ok' ? '✅' : state === 'err' ? '❌' : '⏳';\u001b[39m\n\u001b[31m  msgEl.textContent = msg;\u001b[39m\n\u001b[31m  el.style.borderColor = state === 'ok' ? 'rgba(34,197,94,.3)' : state === 'err' ? 'rgba(239,68,68,.3)' : 'rgba(234,179,8,.3)';\u001b[39m\n\u001b[31m  el.style.background = state === 'ok' ? 'rgba(34,197,94,.05)' : state === 'err' ? 'rgba(239,68,68,.05)' : 'rgba(234,179,8,.05)';\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction renderSheetConfigDisplay(cfg) {\u001b[39m\n\u001b[31m  const display = document.getElementById('sheetConfigDisplay');\u001b[39m\n\u001b[31m  display.style.display = 'block';\u001b[39m\n\u001b[31m  document.getElementById('sheetSyncTime').textContent = 'Last sync: ' + new Date().toLocaleTimeString();\u001b[39m\n\u001b[31m  const showKeys = ['proxy_url','webhook_url','ollama_url','ocr_mode','company','city','auto_log'];\u001b[39m\n\u001b[31m  document.getElementById('sheetConfigRows').innerHTML = showKeys.map(k => {\u001b[39m\n\u001b[31m    const val = cfg[k] || window.appConfig[k] || '—';\u001b[39m\n\u001b[31m    const isEmpty = !cfg[k];\u001b[39m\n\u001b[31m    return `<div class=\\\"courier-master-row\\\">\u001b[39m\n\u001b[31m      <div class=\\\"cml\\\">\u001b[39m\n\u001b[31m        <div class=\\\"cml-name\\\" style=\\\"font-family:var(--fnt-mono);font-size:.75rem;\\\">${k}</div>\u001b[39m\n\u001b[31m        <div class=\\\"cml-url\\\" style=\\\"${isEmpty ? 'color:var(--red);' : ''}\\\">${isEmpty ? '⚠️ Not set in sheet' : val}</div>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m    </div>`;\u001b[39m\n\u001b[31m  }).join('');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m/**\u001b[39m\n\u001b[31m * Push current config changes back to the sheet.\u001b[39m\n\u001b[31m * Called when user edits config from PWA (rare — usually done in sheet directly).\u001b[39m\n\u001b[31m */\u001b[39m\n\u001b[31masync function pushConfigToSheet(updates) {\u001b[39m\n\u001b[31m  const url = window.appConfig.sheets_url;\u001b[39m\n\u001b[31m  if (!url) return;\u001b[39m\n\u001b[31m  try {\u001b[39m\n\u001b[31m    await fetch(url, {\u001b[39m\n\u001b[31m      method: 'POST', mode: 'no-cors',\u001b[39m\n\u001b[31m      headers: { 'Content-Type': 'application/json' },\u001b[39m\n\u001b[31m      body: JSON.stringify({ type: 'save_config', config: updates })\u001b[39m\n\u001b[31m    });\u001b[39m\n\u001b[31m  } catch(e) {}\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction clearAll() {\u001b[39m\n\u001b[31m  if (!confirm('Delete ALL dispatch data?')) return;\u001b[39m\n\u001b[31m  dispatches = [];\u001b[39m\n\u001b[31m  saveData();\u001b[39m\n\u001b[31m  updateHistCount();\u001b[39m\n\u001b[31m  renderDashboard();\u001b[39m\n\u001b[31m  renderHistory();\u001b[39m\n\u001b[31m  showToast('Cleared', 'ok');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction renderCourierMasterList() {\u001b[39m\n\u001b[31m  const el = document.getElementById('courierMasterList');\u001b[39m\n\u001b[31m  el.innerHTML = Object.entries(COURIER_MASTER).map(([name, info]) => `\u001b[39m\n\u001b[31m    <div class=\\\"courier-master-row\\\">\u001b[39m\n\u001b[31m      <div class=\\\"cml\\\">\u001b[39m\n\u001b[31m        <div class=\\\"cml-name\\\">${name}</div>\u001b[39m\n\u001b[31m        <div class=\\\"cml-url\\\">${info.trackUrl}</div>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      <button class=\\\"btn-sm\\\" onclick=\\\"window.open('${info.trackBulkUrl||info.trackUrl.replace('{docket}','')}','_blank')\\\">Open</button>\u001b[39m\n\u001b[31m    </div>`).join('');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction openCourierMaster() {\u001b[39m\n\u001b[31m  const ed = document.getElementById('courierMasterEditor');\u001b[39m\n\u001b[31m  ed.innerHTML = Object.entries(COURIER_MASTER).map(([name, info]) => `\u001b[39m\n\u001b[31m    <div style=\\\"margin-bottom:14px;\\\">\u001b[39m\n\u001b[31m      <div style=\\\"font-family:var(--fnt-head);font-size:.75rem;font-weight:700;color:var(--t2);text-transform:uppercase;letter-spacing:1.5px;margin-bottom:6px;\\\">${name}</div>\u001b[39m\n\u001b[31m      <div class=\\\"f\\\" style=\\\"margin-bottom:6px;\\\">\u001b[39m\n\u001b[31m        <label>Track URL (use {docket})</label>\u001b[39m\n\u001b[31m        <input type=\\\"text\\\" value=\\\"${info.trackUrl}\\\" id=\\\"cm_${name.replace(/\\\\s/g,'_')}\\\"\u001b[39m\n\u001b[31m          style=\\\"font-family:var(--fnt-mono);font-size:.75rem;\\\">\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m    </div>`).join('') +\u001b[39m\n\u001b[31m    `<button class=\\\"btn btn-orange\\\" onclick=\\\"saveCourierMaster()\\\">Save Courier Master</button>`;\u001b[39m\n\u001b[31m  openOverlay('courierMasterOverlay');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction saveCourierMaster() {\u001b[39m\n\u001b[31m  Object.keys(COURIER_MASTER).forEach(name => {\u001b[39m\n\u001b[31m    const el = document.getElementById('cm_' + name.replace(/\\\\s/g,'_'));\u001b[39m\n\u001b[31m    if (el) COURIER_MASTER[name].trackUrl = el.value;\u001b[39m\n\u001b[31m  });\u001b[39m\n\u001b[31m  closeOverlay('courierMasterOverlay');\u001b[39m\n\u001b[31m  renderCourierMasterList();\u001b[39m\n\u001b[31m  showToast('Courier master saved ✓', 'ok');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31m// UI UTILS\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31m// ── ADMIN AUTH ───────────────────────────────────────────────────────────\u001b[39m\n\u001b[31mlet _adminAuthed = false;\u001b[39m\n\u001b[31mlet _lastAdminPane = null;·\u001b[39m\n\u001b[31mfunction showPane(name) {\u001b[39m\n\u001b[31m  // Config tab requires admin auth — re-ask if navigated away\u001b[39m\n\u001b[31m  if (name === 'config') {\u001b[39m\n\u001b[31m    if (!_adminAuthed) {\u001b[39m\n\u001b[31m      _lastAdminPane = 'config';\u001b[39m\n\u001b[31m      showAdminLogin();\u001b[39m\n\u001b[31m      return;\u001b[39m\n\u001b[31m    }\u001b[39m\n\u001b[31m  } else {\u001b[39m\n\u001b[31m    // User navigated away from config — revoke auth\u001b[39m\n\u001b[31m    if (_lastAdminPane === 'config') _adminAuthed = false;\u001b[39m\n\u001b[31m    _lastAdminPane = name;\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m  document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));\u001b[39m\n\u001b[31m  document.querySelectorAll('.pane').forEach(p => p.classList.remove('active'));\u001b[39m\n\u001b[31m  document.getElementById('tab-'+name).classList.add('active');\u001b[39m\n\u001b[31m  document.getElementById('pane-'+name).classList.add('active');\u001b[39m\n\u001b[31m  if (name === 'dashboard') renderDashboard();\u001b[39m\n\u001b[31m  if (name === 'history') renderHistory();\u001b[39m\n\u001b[31m  if (name === 'track') buildTrackDocketChips();\u001b[39m\n\u001b[31m  if (name === 'config') { document.getElementById('cfg_sheets_url').value = window.appConfig.sheets_url || ''; }\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction showAdminLogin() {\u001b[39m\n\u001b[31m  // Password replaced by hub identity — only harish/pramod can access config\u001b[39m\n\u001b[31m  if (['harish', 'pramod'].includes(window._hubUser?.id)) {\u001b[39m\n\u001b[31m    _adminAuthed = true;\u001b[39m\n\u001b[31m    showPane('config');\u001b[39m\n\u001b[31m  } else {\u001b[39m\n\u001b[31m    showToast('Config access restricted to admin users', 'err');\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31masync function submitAdminLogin() {\u001b[39m\n\u001b[31m  showAdminLogin(); // no-op shim — kept for any lingering HTML onclick refs\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31mfunction openOverlay(id) { document.getElementById(id).classList.add('open'); }\u001b[39m\n\u001b[31mfunction closeOverlay(id) { document.getElementById(id).classList.remove('open'); }·\u001b[39m\n\u001b[31mlet toastTimer;\u001b[39m\n\u001b[31mfunction showToast(msg, type='') {\u001b[39m\n\u001b[31m  const t = document.getElementById('toast');\u001b[39m\n\u001b[31m  t.textContent = msg;\u001b[39m\n\u001b[31m  t.className = 'toast show' + (type ? ' '+type : '');\u001b[39m\n\u001b[31m  clearTimeout(toastTimer);\u001b[39m\n\u001b[31m  toastTimer = setTimeout(() => t.classList.remove('show'), 2800);\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction showOCRWorking(show, msg='') {\u001b[39m\n\u001b[31m  const el = document.getElementById('ocrWorking');\u001b[39m\n\u001b[31m  el.classList.toggle('visible', show);\u001b[39m\n\u001b[31m  if (msg) document.getElementById('ocrWorkingText').textContent = msg;\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction showAIProgress(label, pct) {\u001b[39m\n\u001b[31m  const el = document.getElementById('aiProgress');\u001b[39m\n\u001b[31m  el.classList.add('visible');\u001b[39m\n\u001b[31m  document.getElementById('aiProgLabel').textContent = label;\u001b[39m\n\u001b[31m  document.getElementById('aiProgFill').style.width = pct + '%';\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31mfunction hideAIProgress() {\u001b[39m\n\u001b[31m  document.getElementById('aiProgress').classList.remove('visible');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction updateNetDot() {\u001b[39m\n\u001b[31m  document.getElementById('netDot').style.background = navigator.onLine ? 'var(--grn)' : 'var(--red)';\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction setToday() {\u001b[39m\n\u001b[31m  document.getElementById('n_date').value = new Date().toISOString().split('T')[0];\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m// ── EXPORT CLEAN SHEET ───────────────────────────────────────────────────\u001b[39m\n\u001b[31mfunction exportCleanSheet() {\u001b[39m\n\u001b[31m  const pane = document.getElementById('verifyPane');\u001b[39m\n\u001b[31m  const d = pane._pendingDispatch;\u001b[39m\n\u001b[31m  if (!d || !d.consignees?.length) { showToast('No data to export', 'err'); return; }·\u001b[39m\n\u001b[31m  // Build CSV\u001b[39m\n\u001b[31m  const headers = ['S.No.','Store Name','City','Pincode','Phone','Address','Docket No.','Material','Courier','Status'];\u001b[39m\n\u001b[31m  const rows = d.consignees.map((c, i) => [\u001b[39m\n\u001b[31m    i + 1,\u001b[39m\n\u001b[31m    `\\\"${(c.name||'').replace(/\\\"/g,'\\\"\\\"')}\\\"`,\u001b[39m\n\u001b[31m    `\\\"${(c.city||'').replace(/\\\"/g,'\\\"\\\"')}\\\"`,\u001b[39m\n\u001b[31m    c.pincode || '',\u001b[39m\n\u001b[31m    c.phone || '',\u001b[39m\n\u001b[31m    `\\\"${(c.address||'').replace(/\\\"/g,'\\\"\\\"')}\\\"`,\u001b[39m\n\u001b[31m    c.docket || '',\u001b[39m\n\u001b[31m    `\\\"${(c.material||'').replace(/\\\"/g,'\\\"\\\"')}\\\"`,\u001b[39m\n\u001b[31m    d.courier || '',\u001b[39m\n\u001b[31m    c.status || 'pending'\u001b[39m\n\u001b[31m  ]);·\u001b[39m\n\u001b[31m  const csv = [headers.join(','), ...rows.map(r => r.join(','))].join('\\\\n');\u001b[39m\n\u001b[31m  const blob = new Blob([csv], { type: 'text/csv' });\u001b[39m\n\u001b[31m  const url = URL.createObjectURL(blob);\u001b[39m\n\u001b[31m  const a = document.createElement('a');\u001b[39m\n\u001b[31m  a.href = url;\u001b[39m\n\u001b[31m  a.download = `360LM_Dispatch_${d.client||'export'}_${d.dispatchDate||new Date().toISOString().slice(0,10)}_clean.csv`;\u001b[39m\n\u001b[31m  a.click();\u001b[39m\n\u001b[31m  URL.revokeObjectURL(url);\u001b[39m\n\u001b[31m  showToast('Clean sheet exported ✓', 'ok');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// ── DOCKET SLIP UPLOAD — SKIP OCR IF DOCKET ALREADY MAPPED ───────────────\u001b[39m\n\u001b[31mfunction shouldSkipOCR() {\u001b[39m\n\u001b[31m  // If any consignee already has a docket (from store list mapping), skip OCR extraction\u001b[39m\n\u001b[31m  const consList = document.getElementById('consList');\u001b[39m\n\u001b[31m  if (!consList) return false;\u001b[39m\n\u001b[31m  const cards = consList.querySelectorAll('.cons-card');\u001b[39m\n\u001b[31m  let hasAnyDocket = false;\u001b[39m\n\u001b[31m  cards.forEach(card => {\u001b[39m\n\u001b[31m    const d = card.querySelector('.cons-docket-input')?.value?.trim();\u001b[39m\n\u001b[31m    if (d) hasAnyDocket = true;\u001b[39m\n\u001b[31m  });\u001b[39m\n\u001b[31m  return hasAnyDocket;\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// ─── TRACKING UPDATE TO SHEETS ──────────────────────────────────────────\u001b[39m\n\u001b[31masync function logTrackingUpdate(docket, status, lastEvent, location) {\u001b[39m\n\u001b[31m  const url = window.appConfig.webhook_url;\u001b[39m\n\u001b[31m  if (!url) return;\u001b[39m\n\u001b[31m  try {\u001b[39m\n\u001b[31m    await fetch(url, {\u001b[39m\n\u001b[31m      method: 'POST', mode: 'no-cors',\u001b[39m\n\u001b[31m      headers: { 'Content-Type': 'application/json' },\u001b[39m\n\u001b[31m      body: JSON.stringify({\u001b[39m\n\u001b[31m        type: 'tracking_update',\u001b[39m\n\u001b[31m        docket, status, lastEvent, location,\u001b[39m\n\u001b[31m        timestamp: new Date().toISOString()\u001b[39m\n\u001b[31m      })\u001b[39m\n\u001b[31m    });\u001b[39m\n\u001b[31m  } catch(e) {}\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// ─── POD / RECIPIENT PHOTO UPLOAD ───────────────────────────────────────\u001b[39m\n\u001b[31m// Stores base64 locally, pushes to Sheets webhook (Apps Script saves to Drive)\u001b[39m\n\u001b[31mfunction showPODUpload(docket, dispatchId) {\u001b[39m\n\u001b[31m  const overlay = document.getElementById('podOverlay');\u001b[39m\n\u001b[31m  overlay.dataset.docket = docket;\u001b[39m\n\u001b[31m  overlay.dataset.dispatchId = dispatchId;\u001b[39m\n\u001b[31m  document.getElementById('podPreview').innerHTML = '';\u001b[39m\n\u001b[31m  document.getElementById('podFileInput').value = '';\u001b[39m\n\u001b[31m  document.getElementById('recipientFileInput').value = '';\u001b[39m\n\u001b[31m  openOverlay('podOverlay');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31masync function uploadPODFiles() {\u001b[39m\n\u001b[31m  const overlay = document.getElementById('podOverlay');\u001b[39m\n\u001b[31m  const docket = overlay.dataset.docket;\u001b[39m\n\u001b[31m  const dispatchId = overlay.dataset.dispatchId;\u001b[39m\n\u001b[31m  const podFile = document.getElementById('podFileInput').files[0];\u001b[39m\n\u001b[31m  const recipFile = document.getElementById('recipientFileInput').files[0];\u001b[39m\n\u001b[31m  if (!podFile && !recipFile) { showToast('Select at least one photo', 'err'); return; }·\u001b[39m\n\u001b[31m  showToast('Uploading...', 'info');\u001b[39m\n\u001b[31m  const toBase64 = f => new Promise((res,rej) => {\u001b[39m\n\u001b[31m    const r = new FileReader(); r.onload = e => res(e.target.result); r.onerror = rej; r.readAsDataURL(f);\u001b[39m\n\u001b[31m  });·\u001b[39m\n\u001b[31m  const payload = {\u001b[39m\n\u001b[31m    type: 'pod_upload',\u001b[39m\n\u001b[31m    docket, dispatchId,\u001b[39m\n\u001b[31m    timestamp: new Date().toISOString()\u001b[39m\n\u001b[31m  };\u001b[39m\n\u001b[31m  if (podFile)   payload.pod_image = await toBase64(podFile);\u001b[39m\n\u001b[31m  if (recipFile) payload.recipient_image = await toBase64(recipFile);·\u001b[39m\n\u001b[31m  // Update local store\u001b[39m\n\u001b[31m  for (const d of dispatches) {\u001b[39m\n\u001b[31m    if (d.id !== dispatchId) continue;\u001b[39m\n\u001b[31m    for (const c of d.consignees) {\u001b[39m\n\u001b[31m      if (c.docket === docket) {\u001b[39m\n\u001b[31m        c.podUploaded = true;\u001b[39m\n\u001b[31m        c.status = 'delivered';\u001b[39m\n\u001b[31m      }\u001b[39m\n\u001b[31m    }\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m  saveData();·\u001b[39m\n\u001b[31m  // Push to Sheets (Apps Script saves images to Drive, stores URL in sheet)\u001b[39m\n\u001b[31m  if (window.appConfig.webhook_url) {\u001b[39m\n\u001b[31m    try {\u001b[39m\n\u001b[31m      await fetch(window.appConfig.webhook_url, {\u001b[39m\n\u001b[31m        method: 'POST', mode: 'no-cors',\u001b[39m\n\u001b[31m        headers: { 'Content-Type': 'application/json' },\u001b[39m\n\u001b[31m        body: JSON.stringify(payload)\u001b[39m\n\u001b[31m      });\u001b[39m\n\u001b[31m    } catch(e) {}\u001b[39m\n\u001b[31m  }·\u001b[39m\n\u001b[31m  closeOverlay('podOverlay');\u001b[39m\n\u001b[31m  renderDashboard();\u001b[39m\n\u001b[31m  showToast('POD uploaded ✓ — marked as delivered', 'ok');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// ─── WHATSAPP DISPATCH TABLE SHARE ──────────────────────────────────────\u001b[39m\n\u001b[31mfunction shareDispatchToWhatsApp(dispatchId) {\u001b[39m\n\u001b[31m  const d = dispatches.find(x => x.id === dispatchId);\u001b[39m\n\u001b[31m  if (!d) return;\u001b[39m\n\u001b[31m  const lines = [\u001b[39m\n\u001b[31m    `📦 *Dispatch Update — ${d.client} ${d.jobref ? '› '+d.jobref : ''}*`,\u001b[39m\n\u001b[31m    `🚚 Courier: ${d.courier} | Date: ${d.dispatchDate || d.createdAt?.slice(0,10)}`,\u001b[39m\n\u001b[31m    ``,\u001b[39m\n\u001b[31m    ...d.consignees.filter(c => c.docket).map((c,i) =>\u001b[39m\n\u001b[31m      `${i+1}. *${c.name}* — ${c.city}\\\\n   Docket: ${c.docket} | Status: ${c.status||'booked'}`\u001b[39m\n\u001b[31m    ),\u001b[39m\n\u001b[31m    ``,\u001b[39m\n\u001b[31m    `_360 Degree Logical Marketing_`\u001b[39m\n\u001b[31m  ];\u001b[39m\n\u001b[31m  const msg = encodeURIComponent(lines.join('\\\\n'));\u001b[39m\n\u001b[31m  window.open('https://wa.me/?text=' + msg, '_blank');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// Trigger n8n webhook for automated WhatsApp/email share\u001b[39m\n\u001b[31masync function triggerN8nShare(dispatchId, channel) {\u001b[39m\n\u001b[31m  const d = dispatches.find(x => x.id === dispatchId);\u001b[39m\n\u001b[31m  if (!d) return;\u001b[39m\n\u001b[31m  const n8nUrl = window.appConfig.n8n_dispatch_webhook;\u001b[39m\n\u001b[31m  if (!n8nUrl) { showToast('n8n webhook not configured in Config sheet', 'err'); return; }\u001b[39m\n\u001b[31m  try {\u001b[39m\n\u001b[31m    await fetch(n8nUrl, {\u001b[39m\n\u001b[31m      method: 'POST',\u001b[39m\n\u001b[31m      headers: { 'Content-Type': 'application/json' },\u001b[39m\n\u001b[31m      body: JSON.stringify({\u001b[39m\n\u001b[31m        channel, // 'whatsapp' or 'email'\u001b[39m\n\u001b[31m        dispatch: d,\u001b[39m\n\u001b[31m        timestamp: new Date().toISOString()\u001b[39m\n\u001b[31m      })\u001b[39m\n\u001b[31m    });\u001b[39m\n\u001b[31m    showToast(`Sent to n8n for ${channel} delivery ✓`, 'ok');\u001b[39m\n\u001b[31m  } catch(e) {\u001b[39m\n\u001b[31m    showToast('n8n webhook failed', 'err');\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction previewPOD(input, type) {\u001b[39m\n\u001b[31m  const f = input.files[0];\u001b[39m\n\u001b[31m  if (!f) return;\u001b[39m\n\u001b[31m  const r = new FileReader();\u001b[39m\n\u001b[31m  r.onload = e => {\u001b[39m\n\u001b[31m    const prev = document.getElementById('podPreview');\u001b[39m\n\u001b[31m    const existing = document.getElementById('pod_prev_' + type);\u001b[39m\n\u001b[31m    if (existing) existing.remove();\u001b[39m\n\u001b[31m    const img = document.createElement('img');\u001b[39m\n\u001b[31m    img.id = 'pod_prev_' + type;\u001b[39m\n\u001b[31m    img.src = e.target.result;\u001b[39m\n\u001b[31m    img.style.cssText = 'width:80px;height:80px;object-fit:cover;border-radius:6px;border:1px solid var(--bdr);';\u001b[39m\n\u001b[31m    prev.appendChild(img);\u001b[39m\n\u001b[31m  };\u001b[39m\n\u001b[31m  r.readAsDataURL(f);\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// ─── MANUAL TRACKING ─────────────────────────────────────────────────────\u001b[39m\n\u001b[31mfunction showManualTracking(docket, dispatchId, storeName, phone, city) {\u001b[39m\n\u001b[31m  const ov = document.getElementById('manualTrackOverlay');\u001b[39m\n\u001b[31m  ov.dataset.docket = docket;\u001b[39m\n\u001b[31m  ov.dataset.dispatchId = dispatchId;\u001b[39m\n\u001b[31m  ov.dataset.storeName = storeName;\u001b[39m\n\u001b[31m  ov.dataset.city = city;·\u001b[39m\n\u001b[31m  document.getElementById('mtDocketDisplay').textContent =\u001b[39m\n\u001b[31m    docket + '  ·  ' + storeName + '  ·  ' + city;\u001b[39m\n\u001b[31m  document.getElementById('mtContactUsed').value = phone || '';\u001b[39m\n\u001b[31m  document.getElementById('mtContactPerson').value = '';\u001b[39m\n\u001b[31m  document.getElementById('mtRemarks').value = '';\u001b[39m\n\u001b[31m  document.getElementById('mtStatus').value = '';\u001b[39m\n\u001b[31m  document.getElementById('mtCalledBy').value = '';·\u001b[39m\n\u001b[31m  // Set current datetime\u001b[39m\n\u001b[31m  const now = new Date();\u001b[39m\n\u001b[31m  const pad = n => String(n).padStart(2,'0');\u001b[39m\n\u001b[31m  document.getElementById('mtCallTime').value =\u001b[39m\n\u001b[31m    `${now.getFullYear()}-${pad(now.getMonth()+1)}-${pad(now.getDate())}T${pad(now.getHours())}:${pad(now.getMinutes())}`;·\u001b[39m\n\u001b[31m  openOverlay('manualTrackOverlay');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31masync function submitManualTracking() {\u001b[39m\n\u001b[31m  const ov = document.getElementById('manualTrackOverlay');\u001b[39m\n\u001b[31m  const docket     = ov.dataset.docket;\u001b[39m\n\u001b[31m  const dispatchId = ov.dataset.dispatchId;\u001b[39m\n\u001b[31m  const status     = document.getElementById('mtStatus').value;\u001b[39m\n\u001b[31m  const calledBy   = document.getElementById('mtCalledBy').value.trim();·\u001b[39m\n\u001b[31m  if (!status)    { showToast('Select a status', 'err'); return; }\u001b[39m\n\u001b[31m  if (!calledBy)  { showToast('Enter your name', 'err'); return; }·\u001b[39m\n\u001b[31m  const payload = {\u001b[39m\n\u001b[31m    type:          'manual_tracking',\u001b[39m\n\u001b[31m    docket,        dispatchId,\u001b[39m\n\u001b[31m    storeName:     ov.dataset.storeName,\u001b[39m\n\u001b[31m    city:          ov.dataset.city,\u001b[39m\n\u001b[31m    manualStatus:  status,\u001b[39m\n\u001b[31m    calledBy,\u001b[39m\n\u001b[31m    contactUsed:   document.getElementById('mtContactUsed').value.trim(),\u001b[39m\n\u001b[31m    contactPerson: document.getElementById('mtContactPerson').value.trim(),\u001b[39m\n\u001b[31m    remarks:       document.getElementById('mtRemarks').value.trim(),\u001b[39m\n\u001b[31m    callTime:      document.getElementById('mtCallTime').value || new Date().toISOString(),\u001b[39m\n\u001b[31m    timestamp:     new Date().toISOString()\u001b[39m\n\u001b[31m  };·\u001b[39m\n\u001b[31m  // Update local dispatch record\u001b[39m\n\u001b[31m  for (const d of dispatches) {\u001b[39m\n\u001b[31m    if (d.id !== dispatchId) continue;\u001b[39m\n\u001b[31m    for (const c of d.consignees) {\u001b[39m\n\u001b[31m      if (c.docket === docket) {\u001b[39m\n\u001b[31m        c.manualStatus    = status;\u001b[39m\n\u001b[31m        c.manualCalledBy  = calledBy;\u001b[39m\n\u001b[31m        c.manualTime      = payload.callTime;\u001b[39m\n\u001b[31m        c.manualRemarks   = payload.remarks;\u001b[39m\n\u001b[31m        // If confirmed delivered — flag so dashboard shows manual badge\u001b[39m\n\u001b[31m        // (not permanently locked — use resumeAutoTracking() to re-enable)\u001b[39m\n\u001b[31m        if (status === 'delivered') {\u001b[39m\n\u001b[31m          c.status = 'delivered';\u001b[39m\n\u001b[31m          c.manualDelivered = true;\u001b[39m\n\u001b[31m        } else {\u001b[39m\n\u001b[31m          // Non-delivered manual update: clear any previous lock\u001b[39m\n\u001b[31m          c.manualDelivered = false;\u001b[39m\n\u001b[31m        }\u001b[39m\n\u001b[31m      }\u001b[39m\n\u001b[31m    }\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m  saveData();\u001b[39m\n\u001b[31m  renderDashboard();\u001b[39m\n\u001b[31m  // Sync updated consignee to Postgres\u001b[39m\n\u001b[31m  const _d = dispatches.find(x => x.id === dispatchId);\u001b[39m\n\u001b[31m  if (_d) {\u001b[39m\n\u001b[31m    const _idx = _d.consignees.findIndex(c => c.docket === docket);\u001b[39m\n\u001b[31m    if (_idx >= 0) syncConsigneeToDB(_d.consignees[_idx], dispatchId, _idx);\u001b[39m\n\u001b[31m  }·\u001b[39m\n\u001b[31m  // Log to Sheets\u001b[39m\n\u001b[31m  if (window.appConfig.webhook_url) {\u001b[39m\n\u001b[31m    try {\u001b[39m\n\u001b[31m      await fetch(window.appConfig.webhook_url, {\u001b[39m\n\u001b[31m        method: 'POST', mode: 'no-cors',\u001b[39m\n\u001b[31m        headers: { 'Content-Type': 'application/json' },\u001b[39m\n\u001b[31m        body: JSON.stringify(payload)\u001b[39m\n\u001b[31m      });\u001b[39m\n\u001b[31m    } catch(e) {}\u001b[39m\n\u001b[31m  }·\u001b[39m\n\u001b[31m  closeOverlay('manualTrackOverlay');\u001b[39m\n\u001b[31m  showToast('Manual tracking logged ✓', 'ok');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// ─── TILE SELECTION + PARTIAL REPORT SHARE ───────────────────────────────\u001b[39m\n\u001b[31mlet _selectedDockets = new Set();·\u001b[39m\n\u001b[31mfunction toggleTileSelect(docket) {\u001b[39m\n\u001b[31m  if (_selectedDockets.has(docket)) _selectedDockets.delete(docket);\u001b[39m\n\u001b[31m  else _selectedDockets.add(docket);\u001b[39m\n\u001b[31m  // Update checkbox visual\u001b[39m\n\u001b[31m  const chk = document.getElementById('sel_' + docket.replace(/[^a-z0-9]/gi,'_'));\u001b[39m\n\u001b[31m  if (chk) chk.checked = _selectedDockets.has(docket);\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction shareSelectedReport() {\u001b[39m\n\u001b[31m  if (_selectedDockets.size === 0) {\u001b[39m\n\u001b[31m    showToast('Select at least one consignee tile first', 'err');\u001b[39m\n\u001b[31m    return;\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m  const activeDis = dispatches.filter(d => d.status !== 'draft');\u001b[39m\n\u001b[31m  const allItems  = activeDis.flatMap(d =>\u001b[39m\n\u001b[31m    (d.consignees||[]).map(c => Object.assign({}, c, {courier:d.courier, client:d.client, jobref:d.jobref, dispatchId:d.id}))\u001b[39m\n\u001b[31m  );\u001b[39m\n\u001b[31m  const selected = allItems.filter(c => _selectedDockets.has(c.docket));\u001b[39m\n\u001b[31m  if (!selected.length) { showToast('Selected dockets not found', 'err'); return; }·\u001b[39m\n\u001b[31m  const NL = '\\\\n';\u001b[39m\n\u001b[31m  const statusEmoji = {\u001b[39m\n\u001b[31m    delivered:'\\\\u2705', transit:'\\\\uD83D\\\\uDE9A', 'in-transit':'\\\\uD83D\\\\uDE9A',\u001b[39m\n\u001b[31m    booked:'\\\\uD83D\\\\uDCEC', pending:'\\\\u23F3', 'not received':'\\\\u274C', forwarded:'\\\\u27A1'\u001b[39m\n\u001b[31m  };·\u001b[39m\n\u001b[31m  const header = '\\\\uD83D\\\\uDCE6 *Dispatch Status Report*' + NL +\u001b[39m\n\u001b[31m    (selected[0].client || '') + ' ' + (selected[0].jobref ? '> ' + selected[0].jobref : '') + NL +\u001b[39m\n\u001b[31m    new Date().toLocaleDateString('en-IN', {day:'2-digit',month:'short',year:'numeric'}) + NL + NL;·\u001b[39m\n\u001b[31m  const rows = selected.map(function(c, i) {\u001b[39m\n\u001b[31m    const finalStatus = c.manualStatus === 'delivered' ? 'delivered' : (c.status || 'booked');\u001b[39m\n\u001b[31m    const emoji  = statusEmoji[finalStatus] || '\\\\uD83D\\\\uDCE6';\u001b[39m\n\u001b[31m    const source = c.manualStatus ? '(manual)' : '(auto)';\u001b[39m\n\u001b[31m    const remark = c.manualRemarks ? (NL + '   Note: ' + c.manualRemarks) : '';\u001b[39m\n\u001b[31m    return (i+1) + '. *' + c.name + '* - ' + c.city + NL +\u001b[39m\n\u001b[31m      '   ' + emoji + ' ' + finalStatus + ' ' + source + NL +\u001b[39m\n\u001b[31m      '   Docket: ' + c.docket + remark;\u001b[39m\n\u001b[31m  }).join(NL + NL);·\u001b[39m\n\u001b[31m  const footer = NL + NL + '_360 Degree Logical Marketing_';\u001b[39m\n\u001b[31m  window.open('https://wa.me/?text=' + encodeURIComponent(header + rows + footer), '_blank');\u001b[39m\n\u001b[31m  showToast('Report for ' + selected.length + ' consignees ready', 'ok');\u001b[39m\n\u001b[31m}··\u001b[39m\n\u001b[31m// ── resumeAutoTracking: undo a manual-delivered lock ──────────────────────\u001b[39m\n\u001b[31mfunction resumeAutoTracking(docket, dispatchId) {\u001b[39m\n\u001b[31m  for (const d of dispatches) {\u001b[39m\n\u001b[31m    if (d.id !== dispatchId) continue;\u001b[39m\n\u001b[31m    for (const c of d.consignees) {\u001b[39m\n\u001b[31m      if (c.docket === docket) {\u001b[39m\n\u001b[31m        c.manualDelivered = false;\u001b[39m\n\u001b[31m        c.status          = 'transit';\u001b[39m\n\u001b[31m        c.manualStatus    = '';\u001b[39m\n\u001b[31m      }\u001b[39m\n\u001b[31m    }\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m  saveData();\u001b[39m\n\u001b[31m  renderDashboard();\u001b[39m\n\u001b[31m  showToast(`Auto-tracking resumed for ${docket}`, 'ok');\u001b[39m\n\u001b[31m  const _rd = dispatches.find(x => x.id === dispatchId);\u001b[39m\n\u001b[31m  if (_rd) {\u001b[39m\n\u001b[31m    const _ri = _rd.consignees.findIndex(c => c.docket === docket);\u001b[39m\n\u001b[31m    if (_ri >= 0) syncConsigneeToDB(_rd.consignees[_ri], dispatchId, _ri);\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// ── refreshAllTracking (consolidated, try/finally for safety) ─────────────\u001b[39m\n\u001b[31masync function refreshAllTracking() {\u001b[39m\n\u001b[31m  const activeDis  = dispatches.filter(d => d.status !== 'draft');\u001b[39m\n\u001b[31m  const allCons    = activeDis.flatMap(d =>\u001b[39m\n\u001b[31m    (d.consignees || []).map(c => ({ ...c, dispatchId: d.id, courier: d.courier }))\u001b[39m\n\u001b[31m  );\u001b[39m\n\u001b[31m  const withDockets = allCons.filter(c =>\u001b[39m\n\u001b[31m    c.docket && c.status !== 'delivered' && !c.manualDelivered\u001b[39m\n\u001b[31m  );\u001b[39m\n\u001b[31m  if (!withDockets.length) { showToast('No active dockets to refresh', 'info'); return; }·\u001b[39m\n\u001b[31m  showToast(`⏳ Tracking ${withDockets.length} dockets...`, 'info');\u001b[39m\n\u001b[31m  withDockets.forEach(c => {\u001b[39m\n\u001b[31m    const id    = c.docket.replace(/[^a-z0-9]/gi, '_');\u001b[39m\n\u001b[31m    const evtEl = document.getElementById('evt_'  + id);\u001b[39m\n\u001b[31m    const spnEl = document.getElementById('spin_' + id);\u001b[39m\n\u001b[31m    if (evtEl) evtEl.style.display = 'none';\u001b[39m\n\u001b[31m    if (spnEl) spnEl.style.display = 'inline';\u001b[39m\n\u001b[31m  });·\u001b[39m\n\u001b[31m  let done = 0;\u001b[39m\n\u001b[31m  try {\u001b[39m\n\u001b[31m    for (const c of withDockets) {\u001b[39m\n\u001b[31m      const result = await fetchTracking(c.docket, c.courier || '');\u001b[39m\n\u001b[31m      const id    = c.docket.replace(/[^a-z0-9]/gi, '_');·\u001b[39m\n\u001b[31m      // Batch-write result into state (never touch DOM here)\u001b[39m\n\u001b[31m      if (result.status && result.status !== 'failed') {\u001b[39m\n\u001b[31m        for (const d of dispatches) {\u001b[39m\n\u001b[31m          if (d.id !== c.dispatchId) continue;\u001b[39m\n\u001b[31m          for (const con of d.consignees) {\u001b[39m\n\u001b[31m            if (con.docket === c.docket && !con.manualDelivered) {\u001b[39m\n\u001b[31m              con.status           = result.status;\u001b[39m\n\u001b[31m              con.lastEvent        = result.lastEvent || con.lastEvent;\u001b[39m\n\u001b[31m              con.lastScanLocation = result.lastScanLocation || '';\u001b[39m\n\u001b[31m              con.lastTrackedAt    = new Date().toISOString();\u001b[39m\n\u001b[31m            }\u001b[39m\n\u001b[31m          }\u001b[39m\n\u001b[31m        }\u001b[39m\n\u001b[31m      }·\u001b[39m\n\u001b[31m      // Update tile spinner/event text in DOM (cosmetic only)\u001b[39m\n\u001b[31m      const spnEl = document.getElementById('spin_' + id);\u001b[39m\n\u001b[31m      const evtEl = document.getElementById('evt_'  + id);\u001b[39m\n\u001b[31m      if (spnEl) spnEl.style.display = 'none';\u001b[39m\n\u001b[31m      if (evtEl) { evtEl.style.display = 'inline'; evtEl.textContent = result.lastEvent || 'Checked'; }·\u001b[39m\n\u001b[31m      if (result.status && result.status !== 'failed' && window.appConfig.webhook_url) {\u001b[39m\n\u001b[31m        logTrackingUpdate(c.docket, result.status, result.lastEvent || '', result.lastScanLocation || '');\u001b[39m\n\u001b[31m      }\u001b[39m\n\u001b[31m      done++;\u001b[39m\n\u001b[31m    }\u001b[39m\n\u001b[31m    showToast(`✓ ${done} dockets refreshed`, 'ok');\u001b[39m\n\u001b[31m  } finally {\u001b[39m\n\u001b[31m    // Always persist and re-render — even if loop throws mid-way\u001b[39m\n\u001b[31m    saveData();\u001b[39m\n\u001b[31m    renderDashboard();\u001b[39m\n\u001b[31m    // Push updated consignee states to Postgres (fire-and-forget)\u001b[39m\n\u001b[31m    dispatches.filter(d => d.status !== 'draft').forEach(d => syncDispatchToDB(d));\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m</script>\u001b[39m\n\u001b[31m</body>\u001b[39m\n\u001b[31m</html>\"\u001b[39m",
                        "stack": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoContain\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m) // indexOf\u001b[22m\n\nExpected substring: \u001b[32m\"window.location.replace('/hub/')\"\u001b[39m\nReceived string:    \u001b[31m\"<!DOCTYPE html>\u001b[39m\n\u001b[31m<html lang=\\\"en\\\">\u001b[39m\n\u001b[31m<head>\u001b[39m\n\u001b[31m<meta charset=\\\"UTF-8\\\">\u001b[39m\n\u001b[31m<meta name=\\\"viewport\\\" content=\\\"width=device-width,initial-scale=1,maximum-scale=1,user-scalable=no\\\">\u001b[39m\n\u001b[31m<meta name=\\\"theme-color\\\" content=\\\"#080c12\\\">\u001b[39m\n\u001b[31m<meta name=\\\"apple-mobile-web-app-capable\\\" content=\\\"yes\\\">\u001b[39m\n\u001b[31m<meta name=\\\"mobile-web-app-capable\\\" content=\\\"yes\\\">\u001b[39m\n\u001b[31m<title>360LM Dispatch</title>\u001b[39m\n\u001b[31m<link rel=\\\"manifest\\\" href=\\\"manifest.json\\\">\u001b[39m\n\u001b[31m<link href=\\\"https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@300;400;500;600;700&family=Fira+Code:wght@400;500&family=Barlow+Condensed:wght@400;600;700;800&display=swap\\\" rel=\\\"stylesheet\\\">\u001b[39m\n\u001b[31m<style>\u001b[39m\n\u001b[31m/* ════ RESET & VARS ════ */\u001b[39m\n\u001b[31m*{margin:0;padding:0;box-sizing:border-box;-webkit-tap-highlight-color:transparent;}\u001b[39m\n\u001b[31m:root{\u001b[39m\n\u001b[31m  --bg:#080c12; --bg2:#0d1420; --bg3:#111a28;\u001b[39m\n\u001b[31m  --sur:#141f30; --sur2:#1a2840; --sur3:#1f3050;\u001b[39m\n\u001b[31m  --bdr:#1e2e45; --bdr2:#253650;\u001b[39m\n\u001b[31m  --t1:#f0f4f8; --t2:#8ba4bc; --t3:#3d5470; --t4:#243348;\u001b[39m\n\u001b[31m  --acc:#f97316; --acc2:#fb923c; --acc-glow:rgba(249,115,22,0.25);\u001b[39m\n\u001b[31m  --grn:#22c55e; --grn-dim:rgba(34,197,94,0.15);\u001b[39m\n\u001b[31m  --sky:#38bdf8; --sky-dim:rgba(56,189,248,0.15);\u001b[39m\n\u001b[31m  --red:#ef4444; --red-dim:rgba(239,68,68,0.15);\u001b[39m\n\u001b[31m  --ylw:#eab308; --ylw-dim:rgba(234,179,8,0.15);\u001b[39m\n\u001b[31m  --fnt-head:'Barlow Condensed',sans-serif;\u001b[39m\n\u001b[31m  --fnt-body:'Space Grotesk',sans-serif;\u001b[39m\n\u001b[31m  --fnt-mono:'Fira Code',monospace;\u001b[39m\n\u001b[31m  --r:10px; --r-sm:6px; --r-lg:16px;\u001b[39m\n\u001b[31m  --sh:0 4px 24px rgba(0,0,0,0.4);\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31mhtml,body{height:100%;background:var(--bg);color:var(--t1);font-family:var(--fnt-body);overflow:hidden;}·\u001b[39m\n\u001b[31m/* ════ SPLASH ════ */\u001b[39m\n\u001b[31m#splash{\u001b[39m\n\u001b[31m  position:fixed;inset:0;background:var(--bg);z-index:999;\u001b[39m\n\u001b[31m  display:flex;flex-direction:column;align-items:center;justify-content:center;\u001b[39m\n\u001b[31m  transition:opacity .4s .1s;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.splash-logo{font-family:var(--fnt-head);font-size:3rem;font-weight:800;letter-spacing:2px;line-height:1;}\u001b[39m\n\u001b[31m.splash-logo em{color:var(--acc);font-style:normal;}\u001b[39m\n\u001b[31m.splash-sub{color:var(--t3);font-family:var(--fnt-mono);font-size:.7rem;letter-spacing:4px;margin-top:8px;}\u001b[39m\n\u001b[31m.splash-track{width:200px;height:2px;background:var(--bdr);border-radius:2px;margin-top:40px;overflow:hidden;}\u001b[39m\n\u001b[31m.splash-track-fill{height:100%;width:0;background:linear-gradient(90deg,var(--acc),var(--acc2));animation:splashLoad 1.3s cubic-bezier(.4,0,.2,1) forwards;}\u001b[39m\n\u001b[31m@keyframes splashLoad{to{width:100%;}}·\u001b[39m\n\u001b[31m/* ════ LAYOUT ════ */\u001b[39m\n\u001b[31m#app{height:100%;display:flex;flex-direction:column;}\u001b[39m\n\u001b[31m.hdr{\u001b[39m\n\u001b[31m  background:var(--bg2);border-bottom:1px solid var(--bdr);\u001b[39m\n\u001b[31m  padding:10px 14px;display:flex;align-items:center;gap:10px;flex-shrink:0;\u001b[39m\n\u001b[31m  position:relative;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.hdr-brand{font-family:var(--fnt-head);font-size:1.3rem;font-weight:800;letter-spacing:1px;flex:1;}\u001b[39m\n\u001b[31m.hdr-brand em{color:var(--acc);font-style:normal;}\u001b[39m\n\u001b[31m.hdr-dot{width:7px;height:7px;border-radius:50%;background:var(--grn);transition:background .3s;animation:blink 2s infinite;}\u001b[39m\n\u001b[31m@keyframes blink{0%,100%{opacity:1;}50%{opacity:.4;}}\u001b[39m\n\u001b[31m.hdr-btn{\u001b[39m\n\u001b[31m  background:none;border:none;color:var(--t2);cursor:pointer;\u001b[39m\n\u001b[31m  padding:6px 8px;border-radius:var(--r-sm);transition:all .2s;\u001b[39m\n\u001b[31m  font-family:var(--fnt-head);font-size:.85rem;font-weight:700;letter-spacing:.5px;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.hdr-btn:hover{background:var(--bdr);color:var(--t1);}\u001b[39m\n\u001b[31m.hdr-btn.primary{background:var(--acc);color:#fff;padding:6px 14px;}\u001b[39m\n\u001b[31m.hdr-btn.primary:hover{background:var(--acc2);}·\u001b[39m\n\u001b[31m/* ════ TAB BAR ════ */\u001b[39m\n\u001b[31m.tabs{\u001b[39m\n\u001b[31m  display:flex;background:var(--bg2);border-bottom:1px solid var(--bdr);\u001b[39m\n\u001b[31m  overflow-x:auto;flex-shrink:0;padding:0 6px;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.tabs::-webkit-scrollbar{display:none;}\u001b[39m\n\u001b[31m.tab{\u001b[39m\n\u001b[31m  background:none;border:none;color:var(--t3);font-family:var(--fnt-head);\u001b[39m\n\u001b[31m  font-size:.85rem;font-weight:700;letter-spacing:1px;padding:10px 14px;\u001b[39m\n\u001b[31m  cursor:pointer;border-bottom:2px solid transparent;white-space:nowrap;\u001b[39m\n\u001b[31m  transition:all .2s;flex-shrink:0;display:flex;align-items:center;gap:6px;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.tab.active{color:var(--acc);border-bottom-color:var(--acc);}\u001b[39m\n\u001b[31m.tab:hover:not(.active){color:var(--t2);}\u001b[39m\n\u001b[31m.tab-badge{\u001b[39m\n\u001b[31m  background:var(--acc);color:#fff;font-size:.6rem;font-weight:700;\u001b[39m\n\u001b[31m  padding:1px 5px;border-radius:10px;font-family:var(--fnt-mono);\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m/* ════ CONTENT AREAS ════ */\u001b[39m\n\u001b[31m.pane{flex:1;overflow-y:auto;padding:14px;display:none;}\u001b[39m\n\u001b[31m.pane.active{display:block;}\u001b[39m\n\u001b[31m.pane::-webkit-scrollbar{width:3px;}\u001b[39m\n\u001b[31m.pane::-webkit-scrollbar-thumb{background:var(--bdr2);}·\u001b[39m\n\u001b[31m/* ════ CARDS ════ */\u001b[39m\n\u001b[31m.card{\u001b[39m\n\u001b[31m  background:var(--sur);border:1px solid var(--bdr);border-radius:var(--r);\u001b[39m\n\u001b[31m  padding:14px;margin-bottom:12px;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.card.accent-left{border-left:3px solid var(--acc);}\u001b[39m\n\u001b[31m.card-title{\u001b[39m\n\u001b[31m  font-family:var(--fnt-head);font-size:.72rem;font-weight:700;\u001b[39m\n\u001b[31m  color:var(--t3);text-transform:uppercase;letter-spacing:2.5px;\u001b[39m\n\u001b[31m  margin-bottom:12px;display:flex;align-items:center;gap:8px;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.card-title .ct-dot{width:5px;height:5px;border-radius:50%;background:var(--acc);flex-shrink:0;}·\u001b[39m\n\u001b[31m/* ════ FORM ════ */\u001b[39m\n\u001b[31m.f{margin-bottom:12px;}\u001b[39m\n\u001b[31m.f label{\u001b[39m\n\u001b[31m  display:block;font-size:.68rem;color:var(--t3);margin-bottom:5px;\u001b[39m\n\u001b[31m  font-weight:600;text-transform:uppercase;letter-spacing:1.5px;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.f label .req{color:var(--acc);margin-left:2px;}\u001b[39m\n\u001b[31m.f input,.f select,.f textarea{\u001b[39m\n\u001b[31m  width:100%;background:var(--sur2);border:1px solid var(--bdr);\u001b[39m\n\u001b[31m  border-radius:var(--r-sm);color:var(--t1);font-family:var(--fnt-body);\u001b[39m\n\u001b[31m  font-size:.88rem;padding:9px 11px;outline:none;transition:border-color .2s;\u001b[39m\n\u001b[31m  -webkit-appearance:none;appearance:none;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.f input:focus,.f select:focus,.f textarea:focus{border-color:var(--acc);}\u001b[39m\n\u001b[31m.f input::placeholder{color:var(--t4);}\u001b[39m\n\u001b[31m.f textarea{resize:vertical;min-height:65px;}\u001b[39m\n\u001b[31m.f select{\u001b[39m\n\u001b[31m  background-image:url(\\\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='7'%3E%3Cpath d='M1 1l4 4 4-4' stroke='%233d5470' stroke-width='1.5' fill='none' stroke-linecap='round'/%3E%3C/svg%3E\\\");\u001b[39m\n\u001b[31m  background-repeat:no-repeat;background-position:right 10px center;padding-right:28px;cursor:pointer;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.row2{display:grid;grid-template-columns:1fr 1fr;gap:10px;}\u001b[39m\n\u001b[31m.row3{display:grid;grid-template-columns:1fr 1fr 1fr;gap:8px;}·\u001b[39m\n\u001b[31m/* ════ BUTTONS ════ */\u001b[39m\n\u001b[31m.btn{\u001b[39m\n\u001b[31m  width:100%;padding:12px;border:none;border-radius:var(--r);\u001b[39m\n\u001b[31m  font-family:var(--fnt-head);font-size:.9rem;font-weight:700;letter-spacing:.5px;\u001b[39m\n\u001b[31m  cursor:pointer;transition:all .2s;display:flex;align-items:center;justify-content:center;gap:8px;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.btn-orange{background:var(--acc);color:#fff;}\u001b[39m\n\u001b[31m.btn-orange:hover{background:var(--acc2);}\u001b[39m\n\u001b[31m.btn-ghost{background:var(--sur2);border:1px solid var(--bdr);color:var(--t1);}\u001b[39m\n\u001b[31m.btn-ghost:hover{border-color:var(--acc);color:var(--acc);}\u001b[39m\n\u001b[31m.btn-grn{background:var(--grn);color:#000;}\u001b[39m\n\u001b[31m.btn-grn:hover{opacity:.85;}\u001b[39m\n\u001b[31m.btn:active{transform:scale(.98);}\u001b[39m\n\u001b[31m.btn-row{display:grid;grid-template-columns:1fr 1fr;gap:10px;margin-top:6px;}\u001b[39m\n\u001b[31m.btn-sm{\u001b[39m\n\u001b[31m  background:none;border:1px solid var(--bdr);color:var(--t2);\u001b[39m\n\u001b[31m  font-size:.7rem;padding:5px 10px;border-radius:var(--r-sm);cursor:pointer;\u001b[39m\n\u001b[31m  font-family:var(--fnt-body);transition:all .2s;white-space:nowrap;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.btn-sm:hover{border-color:var(--acc);color:var(--acc);}\u001b[39m\n\u001b[31m.btn-sm.primary{background:var(--acc);border-color:var(--acc);color:#fff;}·\u001b[39m\n\u001b[31m/* ════ OCR UPLOAD ZONE ════ */\u001b[39m\n\u001b[31m.upload-zone{\u001b[39m\n\u001b[31m  border:2px dashed var(--bdr2);border-radius:var(--r);\u001b[39m\n\u001b[31m  padding:20px;text-align:center;cursor:pointer;transition:all .25s;\u001b[39m\n\u001b[31m  background:var(--sur2);position:relative;overflow:hidden;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.upload-zone:hover,.upload-zone.drag{border-color:var(--acc);background:rgba(249,115,22,.05);}\u001b[39m\n\u001b[31m.upload-zone .uz-icon{font-size:2rem;margin-bottom:8px;}\u001b[39m\n\u001b[31m.upload-zone .uz-title{font-family:var(--fnt-head);font-size:1rem;font-weight:700;color:var(--t1);}\u001b[39m\n\u001b[31m.upload-zone .uz-sub{font-size:.72rem;color:var(--t3);margin-top:4px;font-family:var(--fnt-mono);}\u001b[39m\n\u001b[31m.upload-zone input{display:none;}\u001b[39m\n\u001b[31m.upload-zone.share-target{border-color:var(--grn);}·\u001b[39m\n\u001b[31m/* ════ OCR RESULTS ════ */\u001b[39m\n\u001b[31m.ocr-results{display:none;}\u001b[39m\n\u001b[31m.ocr-results.visible{display:block;}\u001b[39m\n\u001b[31m.ocr-slip-list{display:flex;flex-direction:column;gap:8px;margin-top:10px;}\u001b[39m\n\u001b[31m.ocr-slip{\u001b[39m\n\u001b[31m  background:var(--sur2);border:1px solid var(--bdr);border-radius:var(--r-sm);\u001b[39m\n\u001b[31m  padding:10px 12px;display:flex;gap:10px;align-items:flex-start;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.ocr-slip img{width:60px;height:60px;object-fit:cover;border-radius:6px;flex-shrink:0;border:1px solid var(--bdr2);}\u001b[39m\n\u001b[31m.ocr-slip-body{flex:1;min-width:0;}\u001b[39m\n\u001b[31m.ocr-slip-name{font-size:.7rem;color:var(--t3);font-family:var(--fnt-mono);margin-bottom:4px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}\u001b[39m\n\u001b[31m.ocr-slip-docket{\u001b[39m\n\u001b[31m  font-family:var(--fnt-mono);font-size:.9rem;font-weight:500;color:var(--acc);\u001b[39m\n\u001b[31m  background:var(--bg);border:1px solid var(--bdr);border-radius:4px;\u001b[39m\n\u001b[31m  padding:4px 8px;width:100%;outline:none;transition:border-color .2s;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.ocr-slip-docket:focus{border-color:var(--acc);}\u001b[39m\n\u001b[31m.ocr-slip-status{font-size:.65rem;margin-top:4px;display:flex;align-items:center;gap:6px;}\u001b[39m\n\u001b[31m.ocr-confidence{padding:2px 6px;border-radius:10px;font-family:var(--fnt-mono);}\u001b[39m\n\u001b[31m.conf-high{background:var(--grn-dim);color:var(--grn);}\u001b[39m\n\u001b[31m.conf-med{background:var(--ylw-dim);color:var(--ylw);}\u001b[39m\n\u001b[31m.conf-low{background:var(--red-dim);color:var(--red);}\u001b[39m\n\u001b[31m.ocr-ai-btn{\u001b[39m\n\u001b[31m  background:rgba(56,189,248,.15);border:1px solid rgba(56,189,248,.3);\u001b[39m\n\u001b[31m  color:var(--sky);font-size:.65rem;padding:3px 8px;border-radius:10px;\u001b[39m\n\u001b[31m  cursor:pointer;font-family:var(--fnt-mono);transition:all .2s;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.ocr-ai-btn:hover{background:rgba(56,189,248,.3);}\u001b[39m\n\u001b[31m.ocr-working{\u001b[39m\n\u001b[31m  background:var(--ylw-dim);border:1px solid rgba(234,179,8,.3);\u001b[39m\n\u001b[31m  border-radius:var(--r-sm);padding:8px 12px;margin-top:8px;\u001b[39m\n\u001b[31m  font-size:.75rem;color:var(--ylw);font-family:var(--fnt-mono);\u001b[39m\n\u001b[31m  display:none;align-items:center;gap:8px;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.ocr-working.visible{display:flex;}\u001b[39m\n\u001b[31m.spin{animation:spin .8s linear infinite;display:inline-block;}\u001b[39m\n\u001b[31m@keyframes spin{to{transform:rotate(360deg);}}·\u001b[39m\n\u001b[31m/* ════ STORE LIST ════ */\u001b[39m\n\u001b[31m.store-upload-zone{\u001b[39m\n\u001b[31m  border:2px dashed var(--bdr2);border-radius:var(--r);padding:16px;\u001b[39m\n\u001b[31m  text-align:center;cursor:pointer;transition:all .25s;background:var(--sur2);\u001b[39m\n\u001b[31m  position:relative;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.store-upload-zone:hover{border-color:var(--sky);background:rgba(56,189,248,.05);}\u001b[39m\n\u001b[31m.store-upload-zone input{display:none;}\u001b[39m\n\u001b[31m.store-table-wrap{max-height:200px;overflow-y:auto;margin-top:10px;border-radius:var(--r-sm);overflow:hidden;}\u001b[39m\n\u001b[31m.store-table{width:100%;border-collapse:collapse;font-size:.75rem;}\u001b[39m\n\u001b[31m.store-table th{\u001b[39m\n\u001b[31m  background:var(--bg);color:var(--t3);font-family:var(--fnt-head);\u001b[39m\n\u001b[31m  font-size:.65rem;letter-spacing:1.5px;text-transform:uppercase;\u001b[39m\n\u001b[31m  padding:7px 8px;text-align:left;border-bottom:1px solid var(--bdr);\u001b[39m\n\u001b[31m  position:sticky;top:0;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.store-table td{\u001b[39m\n\u001b[31m  padding:6px 8px;border-bottom:1px solid var(--bdr);color:var(--t2);\u001b[39m\n\u001b[31m  font-family:var(--fnt-mono);font-size:.72rem;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.store-table tr:hover td{background:var(--sur3);}\u001b[39m\n\u001b[31m.store-table .matched{color:var(--grn);}\u001b[39m\n\u001b[31m.store-table .unmatched{color:var(--ylw);}\u001b[39m\n\u001b[31m.store-map-btn{\u001b[39m\n\u001b[31m  font-size:.65rem;background:none;border:1px solid var(--bdr);color:var(--t3);\u001b[39m\n\u001b[31m  padding:2px 6px;border-radius:4px;cursor:pointer;transition:all .2s;\u001b[39m\n\u001b[31m  font-family:var(--fnt-mono);\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.store-map-btn:hover{border-color:var(--acc);color:var(--acc);}·\u001b[39m\n\u001b[31m/* ════ CONSIGNEE CARDS ════ */\u001b[39m\n\u001b[31m.cons-list{display:flex;flex-direction:column;gap:8px;margin-bottom:10px;}\u001b[39m\n\u001b[31m.cons-card{\u001b[39m\n\u001b[31m  background:var(--sur2);border:1px solid var(--bdr);border-radius:var(--r-sm);\u001b[39m\n\u001b[31m  padding:11px;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.cons-hdr{display:flex;justify-content:space-between;align-items:center;margin-bottom:8px;}\u001b[39m\n\u001b[31m.cons-num{font-family:var(--fnt-mono);font-size:.65rem;color:var(--t3);}\u001b[39m\n\u001b[31m.cons-del{\u001b[39m\n\u001b[31m  background:none;border:none;color:var(--t4);cursor:pointer;\u001b[39m\n\u001b[31m  font-size:.85rem;padding:2px 5px;border-radius:4px;transition:all .2s;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.cons-del:hover{background:var(--red-dim);color:var(--red);}\u001b[39m\n\u001b[31m.cons-docket-linked{\u001b[39m\n\u001b[31m  display:flex;align-items:center;gap:6px;\u001b[39m\n\u001b[31m  background:rgba(249,115,22,.1);border:1px solid rgba(249,115,22,.3);\u001b[39m\n\u001b[31m  border-radius:4px;padding:4px 8px;margin-top:6px;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.cons-docket-linked .dkt{font-family:var(--fnt-mono);font-size:.82rem;color:var(--acc);}\u001b[39m\n\u001b[31m.cons-docket-linked .unlink{background:none;border:none;color:var(--acc);cursor:pointer;font-size:.75rem;}·\u001b[39m\n\u001b[31m/* ════ DISPATCH DASHBOARD ════ */\u001b[39m\n\u001b[31m.dash-stats{\u001b[39m\n\u001b[31m  display:grid;grid-template-columns:repeat(4,1fr);gap:8px;margin-bottom:14px;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.stat-box{\u001b[39m\n\u001b[31m  background:var(--sur);border:1px solid var(--bdr);border-radius:var(--r);\u001b[39m\n\u001b[31m  padding:10px 8px;text-align:center;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.stat-val{font-family:var(--fnt-head);font-size:1.4rem;font-weight:800;}\u001b[39m\n\u001b[31m.stat-lbl{font-size:.6rem;color:var(--t3);font-family:var(--fnt-mono);text-transform:uppercase;letter-spacing:1px;margin-top:2px;}\u001b[39m\n\u001b[31m.stat-box.grn .stat-val{color:var(--grn);}\u001b[39m\n\u001b[31m.stat-box.ylw .stat-val{color:var(--ylw);}\u001b[39m\n\u001b[31m.stat-box.red .stat-val{color:var(--red);}\u001b[39m\n\u001b[31m.stat-box.sky .stat-val{color:var(--sky);}·\u001b[39m\n\u001b[31m/* ════ DOCKET TILES ════ */\u001b[39m\n\u001b[31m.docket-grid{display:flex;flex-direction:column;gap:8px;}\u001b[39m\n\u001b[31m.docket-tile{\u001b[39m\n\u001b[31m  background:var(--sur);border:1px solid var(--bdr);border-radius:var(--r);\u001b[39m\n\u001b[31m  padding:12px;transition:border-color .2s;cursor:pointer;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.docket-tile:hover{border-color:var(--acc);}\u001b[39m\n\u001b[31m.docket-tile-hdr{display:flex;justify-content:space-between;align-items:flex-start;margin-bottom:8px;}\u001b[39m\n\u001b[31m.docket-tile-left{flex:1;}\u001b[39m\n\u001b[31m.docket-num{font-family:var(--fnt-mono);font-size:.95rem;font-weight:500;color:var(--sky);letter-spacing:1px;}\u001b[39m\n\u001b[31m.docket-store{font-size:.82rem;color:var(--t1);font-weight:600;margin-top:2px;}\u001b[39m\n\u001b[31m.docket-courier{font-size:.68rem;color:var(--t3);font-family:var(--fnt-mono);margin-top:2px;}\u001b[39m\n\u001b[31m.status-pill{\u001b[39m\n\u001b[31m  font-size:.62rem;font-weight:700;font-family:var(--fnt-mono);\u001b[39m\n\u001b[31m  padding:3px 8px;border-radius:10px;text-transform:uppercase;letter-spacing:.5px;\u001b[39m\n\u001b[31m  flex-shrink:0;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.sp-delivered{background:var(--grn-dim);color:var(--grn);}\u001b[39m\n\u001b[31m.sp-transit{background:var(--ylw-dim);color:var(--ylw);}\u001b[39m\n\u001b[31m.sp-booked{background:var(--sky-dim);color:var(--sky);}\u001b[39m\n\u001b[31m.sp-pending{background:var(--bdr);color:var(--t3);}\u001b[39m\n\u001b[31m.sp-failed{background:var(--red-dim);color:var(--red);}\u001b[39m\n\u001b[31m.docket-tile-footer{display:flex;align-items:center;gap:8px;flex-wrap:wrap;}\u001b[39m\n\u001b[31m.docket-last-event{font-size:.7rem;color:var(--t3);flex:1;font-family:var(--fnt-mono);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}\u001b[39m\n\u001b[31m.btn-track-live{\u001b[39m\n\u001b[31m  background:none;border:1px solid var(--acc);color:var(--acc);\u001b[39m\n\u001b[31m  font-size:.65rem;padding:4px 10px;border-radius:20px;cursor:pointer;\u001b[39m\n\u001b[31m  font-family:var(--fnt-mono);transition:all .2s;white-space:nowrap;display:flex;align-items:center;gap:4px;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.btn-track-live:hover{background:var(--acc);color:#fff;}\u001b[39m\n\u001b[31m.btn-track-live .live-dot{width:5px;height:5px;border-radius:50%;background:var(--grn);animation:blink 1.5s infinite;flex-shrink:0;}\u001b[39m\n\u001b[31m.docket-progress{height:2px;background:var(--bdr);border-radius:2px;margin-top:8px;overflow:hidden;}\u001b[39m\n\u001b[31m.docket-progress-fill{height:100%;border-radius:2px;transition:width .5s;}·\u001b[39m\n\u001b[31m/* ════ TRACKING DETAIL ════ */\u001b[39m\n\u001b[31m.track-timeline{padding:4px 0;}\u001b[39m\n\u001b[31m.track-evt{display:flex;gap:10px;padding:6px 0;position:relative;padding-left:20px;}\u001b[39m\n\u001b[31m.track-evt::before{\u001b[39m\n\u001b[31m  content:'';position:absolute;left:5px;top:11px;\u001b[39m\n\u001b[31m  width:7px;height:7px;border-radius:50%;background:var(--bdr2);border:1px solid var(--bdr2);\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.track-evt:first-child::before{background:var(--grn);border-color:var(--grn);}\u001b[39m\n\u001b[31m.track-evt::after{\u001b[39m\n\u001b[31m  content:'';position:absolute;left:8px;top:18px;\u001b[39m\n\u001b[31m  width:1px;height:calc(100% - 6px);background:var(--bdr);\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.track-evt:last-child::after{display:none;}\u001b[39m\n\u001b[31m.track-time{font-family:var(--fnt-mono);font-size:.62rem;color:var(--t3);min-width:90px;flex-shrink:0;padding-top:2px;}\u001b[39m\n\u001b[31m.track-desc{font-size:.78rem;color:var(--t2);}\u001b[39m\n\u001b[31m.track-loc{font-size:.67rem;color:var(--t3);font-family:var(--fnt-mono);margin-top:2px;}·\u001b[39m\n\u001b[31m/* ════ SHARE TOAST ════ */\u001b[39m\n\u001b[31m.share-banner{\u001b[39m\n\u001b[31m  position:fixed;top:0;left:0;right:0;z-index:200;\u001b[39m\n\u001b[31m  background:rgba(34,197,94,.15);border-bottom:1px solid rgba(34,197,94,.4);\u001b[39m\n\u001b[31m  padding:10px 16px;font-size:.78rem;color:var(--grn);font-family:var(--fnt-mono);\u001b[39m\n\u001b[31m  display:flex;align-items:center;gap:8px;\u001b[39m\n\u001b[31m  transform:translateY(-100%);transition:transform .3s;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.share-banner.show{transform:translateY(0);}·\u001b[39m\n\u001b[31m/* ════ MODAL ════ */\u001b[39m\n\u001b[31m.overlay{position:fixed;inset:0;background:rgba(0,0,0,.75);z-index:100;display:flex;align-items:flex-end;opacity:0;pointer-events:none;transition:opacity .3s;}\u001b[39m\n\u001b[31m.overlay.open{opacity:1;pointer-events:all;}\u001b[39m\n\u001b[31m.sheet{\u001b[39m\n\u001b[31m  background:var(--sur);border-radius:var(--r-lg) var(--r-lg) 0 0;\u001b[39m\n\u001b[31m  width:100%;max-height:88vh;overflow-y:auto;padding:18px;\u001b[39m\n\u001b[31m  transform:translateY(100%);transition:transform .3s cubic-bezier(.32,.72,0,1);\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.overlay.open .sheet{transform:translateY(0);}\u001b[39m\n\u001b[31m.sheet-handle{width:36px;height:4px;background:var(--bdr2);border-radius:2px;margin:0 auto 18px;}\u001b[39m\n\u001b[31m.sheet-title{font-family:var(--fnt-head);font-size:1.2rem;font-weight:800;letter-spacing:.5px;margin-bottom:14px;}\u001b[39m\n\u001b[31m.sheet::-webkit-scrollbar{width:3px;}\u001b[39m\n\u001b[31m.sheet::-webkit-scrollbar-thumb{background:var(--bdr2);}·\u001b[39m\n\u001b[31m/* ════ TOAST ════ */\u001b[39m\n\u001b[31m.toast{\u001b[39m\n\u001b[31m  position:fixed;bottom:20px;left:50%;transform:translateX(-50%) translateY(70px);\u001b[39m\n\u001b[31m  background:var(--sur2);border:1px solid var(--bdr2);color:var(--t1);\u001b[39m\n\u001b[31m  font-size:.78rem;padding:9px 16px;border-radius:30px;z-index:300;\u001b[39m\n\u001b[31m  transition:transform .3s;white-space:nowrap;font-family:var(--fnt-mono);\u001b[39m\n\u001b[31m  pointer-events:none;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.toast.show{transform:translateX(-50%) translateY(0);}\u001b[39m\n\u001b[31m.toast.ok{border-color:var(--grn);color:var(--grn);}\u001b[39m\n\u001b[31m.toast.err{border-color:var(--red);color:var(--red);}\u001b[39m\n\u001b[31m.toast.info{border-color:var(--sky);color:var(--sky);}·\u001b[39m\n\u001b[31m/* ════ COURIER MASTER ════ */\u001b[39m\n\u001b[31m.courier-master-row{\u001b[39m\n\u001b[31m  display:flex;align-items:center;gap:8px;padding:8px 0;\u001b[39m\n\u001b[31m  border-bottom:1px solid var(--bdr);\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.courier-master-row:last-child{border-bottom:none;}\u001b[39m\n\u001b[31m.cml{flex:1;}\u001b[39m\n\u001b[31m.cml-name{font-weight:600;font-size:.85rem;}\u001b[39m\n\u001b[31m.cml-url{font-family:var(--fnt-mono);font-size:.65rem;color:var(--t3);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:180px;}\u001b[39m\n\u001b[31m.courier-logo{width:24px;height:24px;border-radius:4px;object-fit:contain;background:var(--sur3);padding:2px;}·\u001b[39m\n\u001b[31m/* ════ PROGRESS BAR ════ */\u001b[39m\n\u001b[31m.ai-progress{\u001b[39m\n\u001b[31m  background:var(--sur2);border:1px solid var(--bdr);border-radius:var(--r-sm);\u001b[39m\n\u001b[31m  padding:10px 12px;margin-top:8px;display:none;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.ai-progress.visible{display:block;}\u001b[39m\n\u001b[31m.ai-prog-bar{height:3px;background:var(--bdr);border-radius:3px;margin-top:6px;overflow:hidden;}\u001b[39m\n\u001b[31m.ai-prog-fill{height:100%;background:linear-gradient(90deg,var(--sky),var(--acc));width:0;transition:width .3s;}\u001b[39m\n\u001b[31m.ai-prog-label{font-size:.68rem;color:var(--sky);font-family:var(--fnt-mono);}·\u001b[39m\n\u001b[31m/* ════ EMPTY ════ */\u001b[39m\n\u001b[31m.empty{padding:40px 20px;text-align:center;color:var(--t4);}\u001b[39m\n\u001b[31m.empty .e-icon{font-size:2.5rem;margin-bottom:10px;}\u001b[39m\n\u001b[31m.empty p{font-size:.82rem;}·\u001b[39m\n\u001b[31m/* ════ INFO TAG ════ */\u001b[39m\n\u001b[31m.info-tag{\u001b[39m\n\u001b[31m  background:var(--sur2);border:1px solid var(--bdr);border-radius:var(--r-sm);\u001b[39m\n\u001b[31m  padding:7px 10px;font-size:.75rem;color:var(--t2);margin-bottom:8px;\u001b[39m\n\u001b[31m  display:flex;align-items:center;gap:8px;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.info-tag strong{color:var(--t1);}·\u001b[39m\n\u001b[31m/* ════ JOB PILL ════ */\u001b[39m\n\u001b[31m.job-pill{\u001b[39m\n\u001b[31m  display:inline-flex;align-items:center;gap:6px;\u001b[39m\n\u001b[31m  background:rgba(249,115,22,.1);border:1px solid rgba(249,115,22,.3);\u001b[39m\n\u001b[31m  color:var(--acc);font-family:var(--fnt-mono);font-size:.68rem;\u001b[39m\n\u001b[31m  padding:3px 9px;border-radius:20px;margin-top:4px;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.job-pill button{background:none;border:none;color:var(--acc);cursor:pointer;font-size:.7rem;}·\u001b[39m\n\u001b[31m/* ════ SECTION DIVIDER ════ */\u001b[39m\n\u001b[31m.sec-div{\u001b[39m\n\u001b[31m  display:flex;align-items:center;gap:10px;margin:14px 0 10px;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.sec-div span{font-family:var(--fnt-head);font-size:.7rem;font-weight:700;color:var(--t3);text-transform:uppercase;letter-spacing:2px;white-space:nowrap;}\u001b[39m\n\u001b[31m.sec-div::before,.sec-div::after{content:'';flex:1;height:1px;background:var(--bdr);}·\u001b[39m\n\u001b[31m/* ── MAPPING UI ── */\u001b[39m\n\u001b[31m.map-table{width:100%;border-collapse:collapse;font-size:.78rem;margin:10px 0;}\u001b[39m\n\u001b[31m.map-table th{background:var(--bg);color:var(--t3);font-family:var(--fnt-head);font-size:.65rem;\u001b[39m\n\u001b[31m  letter-spacing:1.5px;text-transform:uppercase;padding:7px 10px;text-align:left;\u001b[39m\n\u001b[31m  border-bottom:1px solid var(--bdr);}\u001b[39m\n\u001b[31m.map-table td{padding:7px 10px;border-bottom:1px solid var(--bdr);vertical-align:middle;}\u001b[39m\n\u001b[31m.map-table tr:last-child td{border-bottom:none;}\u001b[39m\n\u001b[31m.map-table select{background:var(--sur2);border:1px solid var(--bdr);color:var(--t1);\u001b[39m\n\u001b[31m  font-family:var(--fnt-mono);font-size:.75rem;padding:5px 8px;border-radius:var(--r-sm);\u001b[39m\n\u001b[31m  width:100%;outline:none;}\u001b[39m\n\u001b[31m.map-table select:focus{border-color:var(--acc);}\u001b[39m\n\u001b[31m.map-req{color:var(--acc);margin-left:2px;}\u001b[39m\n\u001b[31m.map-saved-badge{font-size:.6rem;background:rgba(57,255,143,.15);color:var(--accent3,#39ff8f);\u001b[39m\n\u001b[31m  border:1px solid rgba(57,255,143,.3);padding:2px 7px;border-radius:10px;\u001b[39m\n\u001b[31m  font-family:var(--fnt-mono);margin-left:6px;}·\u001b[39m\n\u001b[31m/* ── VERIFICATION SCREEN ── */\u001b[39m\n\u001b[31m#verifyPane{display:none;position:fixed;inset:0;background:var(--bg);z-index:50;\u001b[39m\n\u001b[31m  overflow-y:auto;padding:14px;}\u001b[39m\n\u001b[31m.verify-hdr{display:flex;align-items:center;gap:10px;margin-bottom:14px;\u001b[39m\n\u001b[31m  position:sticky;top:0;background:var(--bg);padding-bottom:10px;\u001b[39m\n\u001b[31m  border-bottom:1px solid var(--bdr);z-index:1;}\u001b[39m\n\u001b[31m.verify-title{font-family:var(--fnt-head);font-size:1.1rem;font-weight:800;flex:1;}\u001b[39m\n\u001b[31m.verify-row{background:var(--sur);border:1px solid var(--bdr);border-radius:var(--r-sm);\u001b[39m\n\u001b[31m  padding:10px 12px;margin-bottom:8px;position:relative;}\u001b[39m\n\u001b[31m.verify-row.has-dupe{border-color:var(--red);background:rgba(239,68,68,.04);}\u001b[39m\n\u001b[31m.verify-row.no-docket{border-color:var(--ylw);background:rgba(234,179,8,.04);}\u001b[39m\n\u001b[31m.verify-row.ok{border-left:3px solid var(--grn);}\u001b[39m\n\u001b[31m.verify-row-hdr{display:flex;align-items:center;gap:8px;margin-bottom:8px;}\u001b[39m\n\u001b[31m.verify-num{font-family:var(--fnt-mono);font-size:.65rem;color:var(--t3);}\u001b[39m\n\u001b[31m.verify-store{font-weight:600;font-size:.9rem;flex:1;}\u001b[39m\n\u001b[31m.verify-flag{font-size:.65rem;font-weight:700;font-family:var(--fnt-mono);\u001b[39m\n\u001b[31m  padding:2px 7px;border-radius:10px;text-transform:uppercase;}\u001b[39m\n\u001b[31m.vf-dupe{background:var(--red-dim);color:var(--red);}\u001b[39m\n\u001b[31m.vf-nodocket{background:var(--ylw-dim);color:var(--ylw);}\u001b[39m\n\u001b[31m.vf-ok{background:var(--grn-dim);color:var(--grn);}\u001b[39m\n\u001b[31m.vf-short{background:rgba(56,189,248,.15);color:var(--sky);}\u001b[39m\n\u001b[31m.verify-fields{display:grid;grid-template-columns:1fr 1fr;gap:8px;margin-bottom:8px;}\u001b[39m\n\u001b[31m.verify-field label{font-size:.62rem;color:var(--t3);text-transform:uppercase;\u001b[39m\n\u001b[31m  letter-spacing:1px;display:block;margin-bottom:3px;font-family:var(--fnt-mono);}\u001b[39m\n\u001b[31m.verify-field input{width:100%;background:var(--sur2);border:1px solid var(--bdr);\u001b[39m\n\u001b[31m  color:var(--t1);font-family:var(--fnt-mono);font-size:.82rem;padding:6px 9px;\u001b[39m\n\u001b[31m  border-radius:var(--r-sm);outline:none;}\u001b[39m\n\u001b[31m.verify-field input:focus{border-color:var(--acc);}\u001b[39m\n\u001b[31m.verify-field input.changed{border-color:var(--ylw);background:rgba(234,179,8,.06);}\u001b[39m\n\u001b[31m.verify-actions{display:flex;gap:6px;align-items:center;flex-wrap:wrap;}\u001b[39m\n\u001b[31m.btn-wa{background:rgba(37,211,102,.15);border:1px solid rgba(37,211,102,.3);\u001b[39m\n\u001b[31m  color:#25d366;font-size:.65rem;padding:4px 10px;border-radius:10px;cursor:pointer;\u001b[39m\n\u001b[31m  font-family:var(--fnt-mono);transition:all .2s;}\u001b[39m\n\u001b[31m.btn-wa:hover{background:rgba(37,211,102,.3);}\u001b[39m\n\u001b[31m.verify-thumb{width:48px;height:48px;object-fit:cover;border-radius:6px;\u001b[39m\n\u001b[31m  border:1px solid var(--bdr);cursor:pointer;flex-shrink:0;}\u001b[39m\n\u001b[31m.verify-summary{background:var(--sur);border:1px solid var(--bdr);border-radius:var(--r);\u001b[39m\n\u001b[31m  padding:12px 14px;margin-bottom:14px;display:flex;gap:16px;flex-wrap:wrap;}\u001b[39m\n\u001b[31m.vs-item{text-align:center;}\u001b[39m\n\u001b[31m.vs-val{font-family:var(--fnt-head);font-size:1.3rem;font-weight:800;}\u001b[39m\n\u001b[31m.vs-lbl{font-size:.6rem;color:var(--t3);font-family:var(--fnt-mono);text-transform:uppercase;\u001b[39m\n\u001b[31m  letter-spacing:.5px;margin-top:2px;}\u001b[39m\n\u001b[31m.ocr-correction-note{font-size:.65rem;color:var(--ylw);font-family:var(--fnt-mono);\u001b[39m\n\u001b[31m  margin-top:3px;}\u001b[39m\n\u001b[31m</style>\u001b[39m\n\u001b[31m</head>\u001b[39m\n\u001b[31m<body>·\u001b[39m\n\u001b[31m<!-- SPLASH -->\u001b[39m\n\u001b[31m<div id=\\\"splash\\\">\u001b[39m\n\u001b[31m  <div class=\\\"splash-logo\\\">360LM <em>DISPATCH</em></div>\u001b[39m\n\u001b[31m  <div class=\\\"splash-sub\\\">Courier · Bus · Field Ops</div>\u001b[39m\n\u001b[31m  <div class=\\\"splash-track\\\"><div class=\\\"splash-track-fill\\\"></div></div>\u001b[39m\n\u001b[31m</div>·\u001b[39m\n\u001b[31m<!-- SHARE BANNER -->\u001b[39m\n\u001b[31m<div class=\\\"share-banner\\\" id=\\\"shareBanner\\\">\u001b[39m\n\u001b[31m  📤 Images received via Share — processing for OCR...\u001b[39m\n\u001b[31m  <button style=\\\"background:none;border:none;color:var(--grn);cursor:pointer;margin-left:auto;\\\" onclick=\\\"dismissShare()\\\">✕</button>\u001b[39m\n\u001b[31m</div>·\u001b[39m\n\u001b[31m<!-- APP -->\u001b[39m\n\u001b[31m<div id=\\\"app\\\" style=\\\"display:none;\\\">·\u001b[39m\n\u001b[31m  <div class=\\\"hdr\\\">\u001b[39m\n\u001b[31m    <div class=\\\"hdr-brand\\\">360LM <em>DISPATCH</em></div>\u001b[39m\n\u001b[31m    <span class=\\\"hdr-dot\\\" id=\\\"netDot\\\"></span>\u001b[39m\n\u001b[31m    <button class=\\\"hdr-btn\\\" onclick=\\\"window.location.href='/hub/'\\\" title=\\\"Hub\\\">🏡</button>\u001b[39m\n\u001b[31m    <button class=\\\"hdr-btn\\\" onclick=\\\"showPane('dashboard')\\\">📊</button>\u001b[39m\n\u001b[31m    <button class=\\\"hdr-btn primary\\\" onclick=\\\"newDispatch()\\\">+ NEW</button>\u001b[39m\n\u001b[31m  </div>·\u001b[39m\n\u001b[31m  <div class=\\\"tabs\\\">\u001b[39m\n\u001b[31m    <button class=\\\"tab active\\\" onclick=\\\"showPane('new')\\\" id=\\\"tab-new\\\">NEW</button>\u001b[39m\n\u001b[31m    <button class=\\\"tab\\\" onclick=\\\"showPane('dashboard')\\\" id=\\\"tab-dashboard\\\">DASHBOARD</button>\u001b[39m\n\u001b[31m    <button class=\\\"tab\\\" onclick=\\\"showPane('track')\\\" id=\\\"tab-track\\\">TRACK</button>\u001b[39m\n\u001b[31m    <button class=\\\"tab\\\" onclick=\\\"showPane('history')\\\" id=\\\"tab-history\\\">HISTORY <span class=\\\"tab-badge\\\" id=\\\"histCount\\\">0</span></button>\u001b[39m\n\u001b[31m    <button class=\\\"tab\\\" onclick=\\\"showPane('config')\\\" id=\\\"tab-config\\\">CONFIG</button>\u001b[39m\n\u001b[31m  </div>·\u001b[39m\n\u001b[31m  <!-- ════════════ NEW DISPATCH ════════════ -->\u001b[39m\n\u001b[31m  <div class=\\\"pane active\\\" id=\\\"pane-new\\\">·\u001b[39m\n\u001b[31m    <!-- 1. JOB REFERENCE -->\u001b[39m\n\u001b[31m    <div class=\\\"card accent-left\\\">\u001b[39m\n\u001b[31m      <div class=\\\"card-title\\\"><span class=\\\"ct-dot\\\"></span>Job Reference</div>\u001b[39m\n\u001b[31m      <div class=\\\"row2\\\">\u001b[39m\n\u001b[31m        <div class=\\\"f\\\">\u001b[39m\n\u001b[31m          <label>Client <span class=\\\"req\\\">*</span></label>\u001b[39m\n\u001b[31m          <select id=\\\"n_client\\\" onchange=\\\"onClientChange()\\\">\u001b[39m\n\u001b[31m            <option value=\\\"\\\">Select...</option>\u001b[39m\n\u001b[31m            <option>Lenovo</option><option>HP</option>\u001b[39m\n\u001b[31m            <option>Philips</option><option>Liebherr</option>\u001b[39m\n\u001b[31m            <option value=\\\"__c\\\">+ Custom</option>\u001b[39m\n\u001b[31m          </select>\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m        <div class=\\\"f\\\">\u001b[39m\n\u001b[31m          <label>Job Ref</label>\u001b[39m\n\u001b[31m          <input type=\\\"text\\\" id=\\\"n_jobref\\\" placeholder=\\\"JOB-042\\\" oninput=\\\"updatePill()\\\">\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      <div class=\\\"f\\\" id=\\\"customClientField\\\" style=\\\"display:none;\\\">\u001b[39m\n\u001b[31m        <label>Custom Client</label>\u001b[39m\n\u001b[31m        <input type=\\\"text\\\" id=\\\"n_customClient\\\" placeholder=\\\"Client name\\\" oninput=\\\"updatePill()\\\">\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      <div id=\\\"jobPill\\\" style=\\\"display:none;\\\" class=\\\"job-pill\\\">\u001b[39m\n\u001b[31m        🔗 <span id=\\\"jobPillTxt\\\"></span>\u001b[39m\n\u001b[31m        <button onclick=\\\"clearJob()\\\">✕</button>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      <div class=\\\"row2\\\" style=\\\"margin-top:10px;\\\">\u001b[39m\n\u001b[31m        <div class=\\\"f\\\" style=\\\"margin-bottom:0;\\\">\u001b[39m\n\u001b[31m          <label>Dispatch Date</label>\u001b[39m\n\u001b[31m          <input type=\\\"date\\\" id=\\\"n_date\\\">\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m        <div class=\\\"f\\\" style=\\\"margin-bottom:0;\\\">\u001b[39m\n\u001b[31m          <label>Courier Company <span class=\\\"req\\\">*</span></label>\u001b[39m\n\u001b[31m          <select id=\\\"n_courier\\\">\u001b[39m\n\u001b[31m            <option value=\\\"\\\">Select...</option>\u001b[39m\n\u001b[31m            <option>SkyKing</option><option>DTDC</option>\u001b[39m\n\u001b[31m            <option>Delhivery</option><option>Blue Dart</option>\u001b[39m\n\u001b[31m            <option>FedEx</option><option>Ecom Express</option>\u001b[39m\n\u001b[31m            <option>India Post</option><option value=\\\"__o\\\">Other</option>\u001b[39m\n\u001b[31m          </select>\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m    </div>·\u001b[39m\n\u001b[31m    <!-- 2. STORE LIST UPLOAD -->\u001b[39m\n\u001b[31m    <div class=\\\"card\\\">\u001b[39m\n\u001b[31m      <div class=\\\"card-title\\\"><span class=\\\"ct-dot\\\" style=\\\"background:var(--sky);\\\"></span>Store / Destination List</div>\u001b[39m\n\u001b[31m      <div class=\\\"store-upload-zone\\\" id=\\\"storeZone\\\" onclick=\\\"document.getElementById('storeFile').click();event.stopPropagation();\\\" style=\\\"cursor:pointer;\\\">\u001b[39m\n\u001b[31m        <input type=\\\"file\\\" id=\\\"storeFile\\\" accept=\\\".xlsx,.xls,.csv\\\" onchange=\\\"handleStoreFile(this)\\\" onclick=\\\"event.stopPropagation();\\\" style=\\\"display:none;\\\">\u001b[39m\n\u001b[31m        <div style=\\\"font-size:1.5rem;margin-bottom:6px;\\\">📋</div>\u001b[39m\n\u001b[31m        <div style=\\\"font-family:var(--fnt-head);font-weight:700;font-size:.9rem;\\\">Upload Store List</div>\u001b[39m\n\u001b[31m        <div style=\\\"font-size:.68rem;color:var(--t3);margin-top:3px;font-family:var(--fnt-mono);\\\">Excel / CSV · stores, cities, contacts</div>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      <div id=\\\"storeTableArea\\\" style=\\\"display:none;\\\">\u001b[39m\n\u001b[31m        <!-- Column detection result -->\u001b[39m\n\u001b[31m        <div id=\\\"storeColInfo\\\" style=\\\"margin-top:10px;display:none;\\\"></div>\u001b[39m\n\u001b[31m        <div style=\\\"display:flex;justify-content:space-between;align-items:center;margin-top:10px;margin-bottom:6px;\\\">\u001b[39m\n\u001b[31m          <span style=\\\"font-family:var(--fnt-mono);font-size:.7rem;color:var(--sky);\\\" id=\\\"storeCount\\\">0 stores loaded</span>\u001b[39m\n\u001b[31m          <button class=\\\"btn-sm\\\" onclick=\\\"clearStores()\\\">Clear</button>\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m        <div class=\\\"store-table-wrap\\\">\u001b[39m\n\u001b[31m          <table class=\\\"store-table\\\" id=\\\"storeTable\\\"></table>\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m        <!-- Inline add-as-consignees UI instead of confirm() -->\u001b[39m\n\u001b[31m        <div id=\\\"storeAddPanel\\\" style=\\\"margin-top:10px;background:var(--sur3);border:1px solid var(--bdr2);border-radius:var(--r-sm);padding:10px 12px;display:none;\\\">\u001b[39m\n\u001b[31m          <div style=\\\"font-size:.75rem;color:var(--t1);margin-bottom:8px;font-weight:600;\\\" id=\\\"storeAddTitle\\\">Add stores as consignees?</div>\u001b[39m\n\u001b[31m          <div class=\\\"row2\\\">\u001b[39m\n\u001b[31m            <button class=\\\"btn btn-ghost\\\" style=\\\"font-size:.75rem;padding:8px;\\\" onclick=\\\"document.getElementById('storeAddPanel').style.display='none'\\\">Skip for now</button>\u001b[39m\n\u001b[31m            <button class=\\\"btn btn-orange\\\" style=\\\"font-size:.75rem;padding:8px;\\\" id=\\\"storeAddBtn\\\" onclick=\\\"addStoresToConsignees()\\\">✓ Add All as Consignees</button>\u001b[39m\n\u001b[31m          </div>\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m        <button class=\\\"btn btn-ghost\\\" style=\\\"margin-top:8px;font-size:.78rem;\\\" onclick=\\\"autoMatchDockets()\\\">\u001b[39m\n\u001b[31m          🔗 Auto-match Dockets to Stores\u001b[39m\n\u001b[31m        </button>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m    </div>·\u001b[39m\n\u001b[31m    <!-- 3. DOCKET SLIPS — AI OCR -->\u001b[39m\n\u001b[31m    <div class=\\\"card\\\">\u001b[39m\n\u001b[31m      <div class=\\\"card-title\\\"><span class=\\\"ct-dot\\\" style=\\\"background:var(--grn);\\\"></span>Docket Slips — AI OCR</div>·\u001b[39m\n\u001b[31m      <!-- Share target indicator -->\u001b[39m\n\u001b[31m      <div class=\\\"info-tag\\\" id=\\\"shareTargetInfo\\\" style=\\\"display:none;\\\">\u001b[39m\n\u001b[31m        <span>📤</span><span>Images shared from WhatsApp — <strong id=\\\"sharedCount\\\">0</strong> slips received</span>\u001b[39m\n\u001b[31m      </div>·\u001b[39m\n\u001b[31m      <label class=\\\"upload-zone\\\" id=\\\"slipZone\\\" for=\\\"slipFiles\\\" style=\\\"display:block;cursor:pointer;\\\">\u001b[39m\n\u001b[31m        <input type=\\\"file\\\" id=\\\"slipFiles\\\" accept=\\\"image/*\\\" multiple onchange=\\\"handleSlipFiles(this.files)\\\" style=\\\"display:none;\\\">\u001b[39m\n\u001b[31m        <div class=\\\"uz-icon\\\">🖼️</div>\u001b[39m\n\u001b[31m        <div class=\\\"uz-title\\\">Upload Courier Slips</div>\u001b[39m\n\u001b[31m        <div class=\\\"uz-sub\\\">Multiple images · Tap or drag · Camera capture supported</div>\u001b[39m\n\u001b[31m      </label>·\u001b[39m\n\u001b[31m      <!-- OCR Working indicator -->\u001b[39m\n\u001b[31m      <div class=\\\"ocr-working\\\" id=\\\"ocrWorking\\\">\u001b[39m\n\u001b[31m        <span class=\\\"spin\\\">⟳</span>\u001b[39m\n\u001b[31m        <span id=\\\"ocrWorkingText\\\">Running OCR...</span>\u001b[39m\n\u001b[31m      </div>·\u001b[39m\n\u001b[31m      <!-- AI Progress -->\u001b[39m\n\u001b[31m      <div class=\\\"ai-progress\\\" id=\\\"aiProgress\\\">\u001b[39m\n\u001b[31m        <div class=\\\"ai-prog-label\\\" id=\\\"aiProgLabel\\\">Initializing Transformers.js...</div>\u001b[39m\n\u001b[31m        <div class=\\\"ai-prog-bar\\\"><div class=\\\"ai-prog-fill\\\" id=\\\"aiProgFill\\\"></div></div>\u001b[39m\n\u001b[31m      </div>·\u001b[39m\n\u001b[31m      <!-- OCR Results -->\u001b[39m\n\u001b[31m      <div class=\\\"ocr-results\\\" id=\\\"ocrResults\\\">\u001b[39m\n\u001b[31m        <div class=\\\"sec-div\\\"><span>Detected Dockets</span></div>\u001b[39m\n\u001b[31m        <div id=\\\"ocrSummary\\\" style=\\\"display:none;font-size:.72rem;font-family:var(--fnt-mono);\u001b[39m\n\u001b[31m          padding:4px 8px 8px;gap:8px\\\"></div>\u001b[39m\n\u001b[31m        <div class=\\\"ocr-slip-list\\\" id=\\\"ocrSlipList\\\"></div>\u001b[39m\n\u001b[31m        <div class=\\\"btn-row\\\" style=\\\"margin-top:10px;\\\">\u001b[39m\n\u001b[31m          <button class=\\\"btn btn-ghost\\\" onclick=\\\"clearSlips()\\\">Clear All</button>\u001b[39m\n\u001b[31m          <button class=\\\"btn btn-grn\\\" onclick=\\\"applyAllDockets()\\\">✓ Apply All to Consignees</button>\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m    </div>·\u001b[39m\n\u001b[31m    <!-- 4. CONSIGNEES -->\u001b[39m\n\u001b[31m    <div class=\\\"card accent-left\\\">\u001b[39m\n\u001b[31m      <div class=\\\"card-title\\\"><span class=\\\"ct-dot\\\"></span>Consignees</div>\u001b[39m\n\u001b[31m      <div class=\\\"cons-list\\\" id=\\\"consList\\\"></div>\u001b[39m\n\u001b[31m      <button class=\\\"btn btn-ghost\\\" style=\\\"font-size:.8rem;\\\" onclick=\\\"addConsignee()\\\">+ Add Consignee</button>\u001b[39m\n\u001b[31m    </div>·\u001b[39m\n\u001b[31m    <!-- 5. REMARKS -->\u001b[39m\n\u001b[31m    <div class=\\\"card\\\">\u001b[39m\n\u001b[31m      <div class=\\\"card-title\\\"><span class=\\\"ct-dot\\\" style=\\\"background:var(--t3);\\\"></span>Remarks</div>\u001b[39m\n\u001b[31m      <div class=\\\"f\\\" style=\\\"margin-bottom:0;\\\">\u001b[39m\n\u001b[31m        <textarea id=\\\"n_remarks\\\" placeholder=\\\"Instructions, notes, special handling...\\\"></textarea>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m    </div>·\u001b[39m\n\u001b[31m    <!-- ACTIONS -->\u001b[39m\n\u001b[31m    <div class=\\\"btn-row\\\">\u001b[39m\n\u001b[31m      <button class=\\\"btn btn-ghost\\\" onclick=\\\"saveDraft()\\\">💾 Draft</button>\u001b[39m\n\u001b[31m      <button class=\\\"btn btn-orange\\\" onclick=\\\"finaliseDispatch()\\\">✓ Finalise & Log</button>\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m    <div style=\\\"height:16px;\\\"></div>\u001b[39m\n\u001b[31m  </div>·\u001b[39m\n\u001b[31m  <!-- ════════════ DASHBOARD ════════════ -->\u001b[39m\n\u001b[31m  <div class=\\\"pane\\\" id=\\\"pane-dashboard\\\">\u001b[39m\n\u001b[31m    <div class=\\\"dash-stats\\\">\u001b[39m\n\u001b[31m      <div class=\\\"stat-box grn\\\"><div class=\\\"stat-val\\\" id=\\\"st-delivered\\\">0</div><div class=\\\"stat-lbl\\\">Delivered</div></div>\u001b[39m\n\u001b[31m      <div class=\\\"stat-box ylw\\\"><div class=\\\"stat-val\\\" id=\\\"st-transit\\\">0</div><div class=\\\"stat-lbl\\\">In Transit</div></div>\u001b[39m\n\u001b[31m      <div class=\\\"stat-box sky\\\"><div class=\\\"stat-val\\\" id=\\\"st-booked\\\">0</div><div class=\\\"stat-lbl\\\">Booked</div></div>\u001b[39m\n\u001b[31m      <div class=\\\"stat-box red\\\"><div class=\\\"stat-val\\\" id=\\\"st-pending\\\">0</div><div class=\\\"stat-lbl\\\">Pending</div></div>\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m    <div id=\\\"dashFilters\\\" style=\\\"display:flex;gap:6px;flex-wrap:wrap;margin-bottom:12px;\\\">\u001b[39m\n\u001b[31m      <span class=\\\"btn-sm primary\\\" onclick=\\\"filterDash('all',this)\\\" style=\\\"cursor:pointer;\\\">All</span>\u001b[39m\n\u001b[31m      <span class=\\\"btn-sm\\\" onclick=\\\"filterDash('delivered',this)\\\" style=\\\"cursor:pointer;\\\">Delivered</span>\u001b[39m\n\u001b[31m      <span class=\\\"btn-sm\\\" onclick=\\\"filterDash('transit',this)\\\" style=\\\"cursor:pointer;\\\">In Transit</span>\u001b[39m\n\u001b[31m      <span class=\\\"btn-sm\\\" onclick=\\\"filterDash('booked',this)\\\" style=\\\"cursor:pointer;\\\">Booked</span>\u001b[39m\n\u001b[31m      <span class=\\\"btn-sm\\\" onclick=\\\"filterDash('pending',this)\\\" style=\\\"cursor:pointer;\\\">Pending</span>\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m    <div class=\\\"docket-grid\\\" id=\\\"dashGrid\\\">\u001b[39m\n\u001b[31m      <div class=\\\"empty\\\"><div class=\\\"e-icon\\\">📊</div><p>No dispatches yet.<br>Create one to see the dashboard.</p></div>\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m    <div style=\\\"display:flex;gap:8px;margin-top:10px;flex-wrap:wrap;\\\">\u001b[39m\n\u001b[31m      <button class=\\\"btn btn-ghost\\\" style=\\\"flex:2;font-size:.8rem;\\\" id=\\\"btnRefreshAll\\\" onclick=\\\"refreshAllTracking()\\\">\u001b[39m\n\u001b[31m        🔄 Refresh All Tracking\u001b[39m\n\u001b[31m      </button>\u001b[39m\n\u001b[31m      <button class=\\\"btn btn-ghost\\\" style=\\\"flex:1;font-size:.8rem;border-color:var(--sky);color:var(--sky);\\\"\u001b[39m\n\u001b[31m        onclick=\\\"shareSelectedReport()\\\">\u001b[39m\n\u001b[31m        📊 Share Selected\u001b[39m\n\u001b[31m      </button>\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m  </div>·\u001b[39m\n\u001b[31m  <!-- ════════════ TRACK ════════════ -->\u001b[39m\n\u001b[31m  <div class=\\\"pane\\\" id=\\\"pane-track\\\">·\u001b[39m\n\u001b[31m    <!-- Courier selector -->\u001b[39m\n\u001b[31m    <div class=\\\"card\\\" style=\\\"margin-bottom:10px;\\\">\u001b[39m\n\u001b[31m      <div class=\\\"card-title\\\"><span class=\\\"ct-dot\\\" style=\\\"background:var(--sky);\\\"></span>Manual Tracking</div>\u001b[39m\n\u001b[31m      <div style=\\\"font-size:.72rem;color:var(--t2);margin-bottom:10px;line-height:1.5;\\\">\u001b[39m\n\u001b[31m        Track any docket number — select from already dispatched dockets or enter new ones manually.\u001b[39m\n\u001b[31m        SkyKing supports up to <strong>10 dockets at once</strong>; others are tracked one at a time.\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      <div class=\\\"f\\\">\u001b[39m\n\u001b[31m        <label>Courier Company</label>\u001b[39m\n\u001b[31m        <select id=\\\"t_courier\\\" onchange=\\\"buildTrackDocketChips()\\\">\u001b[39m\n\u001b[31m          <option>SkyKing</option><option>DTDC</option><option>Delhivery</option>\u001b[39m\n\u001b[31m          <option>Blue Dart</option><option>FedEx</option><option>Ecom Express</option><option>India Post</option>\u001b[39m\n\u001b[31m        </select>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m    </div>·\u001b[39m\n\u001b[31m    <!-- Docket selector — from existing dispatches -->\u001b[39m\n\u001b[31m    <div class=\\\"card\\\" style=\\\"margin-bottom:10px;\\\">\u001b[39m\n\u001b[31m      <div class=\\\"card-title\\\"><span class=\\\"ct-dot\\\" style=\\\"background:var(--acc);\\\"></span>\u001b[39m\n\u001b[31m        Select from Dispatched Dockets\u001b[39m\n\u001b[31m        <span id=\\\"trackCourierLimit\\\" style=\\\"margin-left:auto;font-size:.65rem;color:var(--t3);font-weight:400;text-transform:none;letter-spacing:0;font-family:var(--fnt-mono);\\\"></span>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      <div id=\\\"trackDocketChips\\\" style=\\\"display:flex;flex-wrap:wrap;gap:6px;margin-bottom:10px;min-height:28px;\\\">\u001b[39m\n\u001b[31m        <span style=\\\"font-size:.72rem;color:var(--t3);font-family:var(--fnt-mono);\\\">No dispatched dockets yet</span>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      <div style=\\\"font-size:.68rem;color:var(--t3);font-family:var(--fnt-mono);margin-bottom:8px;\\\">\u001b[39m\n\u001b[31m        Tap a docket to add/remove from tracking list. Selected shown in orange.\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m    </div>·\u001b[39m\n\u001b[31m    <!-- Manual entry for new dockets -->\u001b[39m\n\u001b[31m    <div class=\\\"card\\\" style=\\\"margin-bottom:10px;\\\">\u001b[39m\n\u001b[31m      <div class=\\\"card-title\\\"><span class=\\\"ct-dot\\\" style=\\\"background:var(--grn);\\\"></span>Add New Docket Numbers</div>\u001b[39m\n\u001b[31m      <div class=\\\"f\\\" style=\\\"margin-bottom:6px;\\\">\u001b[39m\n\u001b[31m        <label>Enter docket numbers (comma or newline separated)</label>\u001b[39m\n\u001b[31m        <textarea id=\\\"t_manual_dockets\\\" placeholder=\\\"443005527&#10;443005528&#10;or: 443005527, 443005528\\\"\u001b[39m\n\u001b[31m          style=\\\"min-height:56px;font-family:var(--fnt-mono);font-size:.82rem;letter-spacing:.5px;\\\"></textarea>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      <button class=\\\"btn-sm primary\\\" onclick=\\\"addManualDockets()\\\" style=\\\"font-size:.72rem;\\\">+ Add to List</button>\u001b[39m\n\u001b[31m    </div>·\u001b[39m\n\u001b[31m    <!-- Selected dockets summary + track button -->\u001b[39m\n\u001b[31m    <div id=\\\"trackSelectedSummary\\\" style=\\\"display:none;\\\" class=\\\"card\\\" style=\\\"margin-bottom:10px;\\\">\u001b[39m\n\u001b[31m      <div style=\\\"display:flex;align-items:center;justify-content:space-between;margin-bottom:8px;\\\">\u001b[39m\n\u001b[31m        <div>\u001b[39m\n\u001b[31m          <span style=\\\"font-family:var(--fnt-mono);font-size:.72rem;color:var(--sky);\\\" id=\\\"trackSelectedCount\\\">0 selected</span>\u001b[39m\n\u001b[31m          <span style=\\\"font-family:var(--fnt-mono);font-size:.65rem;color:var(--t3);margin-left:8px;\\\" id=\\\"trackSelectedList\\\"></span>\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m        <button class=\\\"btn-sm\\\" onclick=\\\"clearTrackSelection()\\\">Clear</button>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      <div class=\\\"btn-row\\\">\u001b[39m\n\u001b[31m        <button class=\\\"btn btn-ghost\\\" onclick=\\\"openTrackOnCourierSite()\\\">🔗 Open Courier Site</button>\u001b[39m\n\u001b[31m        <button class=\\\"btn btn-orange\\\" onclick=\\\"trackSelected()\\\">🔍 Track All via Proxy</button>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m    </div>·\u001b[39m\n\u001b[31m    <div id=\\\"trackResultsArea\\\"></div>\u001b[39m\n\u001b[31m  </div>·\u001b[39m\n\u001b[31m  <!-- ════════════ HISTORY ════════════ -->\u001b[39m\n\u001b[31m  <div class=\\\"pane\\\" id=\\\"pane-history\\\">\u001b[39m\n\u001b[31m    <div id=\\\"historyList\\\">\u001b[39m\n\u001b[31m      <div class=\\\"empty\\\"><div class=\\\"e-icon\\\">📭</div><p>No dispatch history yet.</p></div>\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m  </div>·\u001b[39m\n\u001b[31m  <!-- ════════════ CONFIG ════════════ -->\u001b[39m\n\u001b[31m  <div class=\\\"pane\\\" id=\\\"pane-config\\\">·\u001b[39m\n\u001b[31m    <!-- SHEETS URL — the only thing you type manually in the PWA -->\u001b[39m\n\u001b[31m    <div class=\\\"card\\\" style=\\\"border-color:var(--acc);border-width:2px;\\\">\u001b[39m\n\u001b[31m      <div class=\\\"card-title\\\"><span class=\\\"ct-dot\\\"></span>Google Sheets — Master Config</div>\u001b[39m\n\u001b[31m      <div class=\\\"info-tag\\\" style=\\\"background:rgba(34,197,94,.07);border-color:rgba(34,197,94,.3);\\\">\u001b[39m\n\u001b[31m        <span>✅</span>\u001b[39m\n\u001b[31m        <span>URL is loaded automatically from <strong>config.json</strong> on the VPS — no manual entry needed. All other settings sync from your <strong>Google Sheet</strong>.</span>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      <div class=\\\"f\\\">\u001b[39m\n\u001b[31m        <label>Apps Script URL <span style=\\\"font-size:.65rem;color:var(--t3);font-weight:400;\\\">(auto-loaded from config.json)</span></label>\u001b[39m\n\u001b[31m        <input type=\\\"url\\\" id=\\\"cfg_sheets_url\\\"\u001b[39m\n\u001b[31m          placeholder=\\\"Auto-loaded from server...\\\"\u001b[39m\n\u001b[31m          oninput=\\\"onSheetsUrlChange()\\\" readonly\u001b[39m\n\u001b[31m          style=\\\"opacity:.7;cursor:default;\\\">\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      <div id=\\\"syncStatus\\\" class=\\\"info-tag\\\" style=\\\"display:none;\\\">\u001b[39m\n\u001b[31m        <span id=\\\"syncIcon\\\">⏳</span>\u001b[39m\n\u001b[31m        <span id=\\\"syncMsg\\\">Syncing from sheet...</span>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      <div class=\\\"btn-row\\\">\u001b[39m\n\u001b[31m        <button class=\\\"btn btn-ghost\\\" onclick=\\\"syncFromSheet()\\\">🔄 Sync Now</button>\u001b[39m\n\u001b[31m        <button class=\\\"btn btn-orange\\\" onclick=\\\"saveSheetsUrl()\\\">💾 Save & Sync</button>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m    </div>·\u001b[39m\n\u001b[31m    <!-- READ-ONLY display of what was loaded from sheet -->\u001b[39m\n\u001b[31m    <div class=\\\"card\\\" id=\\\"sheetConfigDisplay\\\" style=\\\"display:none;\\\">\u001b[39m\n\u001b[31m      <div class=\\\"card-title\\\"><span class=\\\"ct-dot\\\" style=\\\"background:var(--grn);\\\"></span>Config Loaded from Sheet\u001b[39m\n\u001b[31m        <span id=\\\"sheetSyncTime\\\" style=\\\"margin-left:auto;font-size:.62rem;color:var(--t3);font-family:var(--fnt-mono);font-weight:400;text-transform:none;letter-spacing:0;\\\"></span>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      <div id=\\\"sheetConfigRows\\\"></div>\u001b[39m\n\u001b[31m      <div class=\\\"info-tag\\\" style=\\\"margin-top:8px;\\\">\u001b[39m\n\u001b[31m        <span>✏️</span><span>To change any value, edit the <strong>Config sheet</strong> in Google Sheets, then tap Sync Now above.</span>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m    </div>·\u001b[39m\n\u001b[31m    <!-- Courier Master (from sheet) -->\u001b[39m\n\u001b[31m    <div class=\\\"card\\\">\u001b[39m\n\u001b[31m      <div class=\\\"card-title\\\"><span class=\\\"ct-dot\\\" style=\\\"background:var(--ylw);\\\"></span>Courier Master</div>\u001b[39m\n\u001b[31m      <div id=\\\"courierMasterList\\\"></div>\u001b[39m\n\u001b[31m      <button class=\\\"btn btn-ghost\\\" style=\\\"margin-top:8px;font-size:.78rem;\\\" onclick=\\\"openCourierMaster()\\\">✏️ Edit in Sheet / Override</button>\u001b[39m\n\u001b[31m    </div>·\u001b[39m\n\u001b[31m    <button class=\\\"btn\\\" style=\\\"background:var(--red-dim);color:var(--red);border:1px solid var(--red);margin-top:4px;\\\" onclick=\\\"clearAll()\\\">🗑️ Clear All Local Data</button>\u001b[39m\n\u001b[31m    <div style=\\\"height:20px;\\\"></div>\u001b[39m\n\u001b[31m  </div>··\u001b[39m\n\u001b[31m<!-- ════ ADDRESS REVIEW PANE ════ -->\u001b[39m\n\u001b[31m<div id=\\\"addrReviewPane\\\" style=\\\"display:none;position:fixed;inset:0;background:var(--bg);z-index:50;overflow-y:auto;padding:14px;\\\">\u001b[39m\n\u001b[31m  <div class=\\\"verify-hdr\\\">\u001b[39m\n\u001b[31m    <button class=\\\"btn-icon\\\" onclick=\\\"document.getElementById('addrReviewPane').style.display='none'\\\" style=\\\"font-size:1.1rem;\\\">←</button>\u001b[39m\n\u001b[31m    <div class=\\\"verify-title\\\">Review AI-Parsed Addresses</div>\u001b[39m\n\u001b[31m  </div>\u001b[39m\n\u001b[31m  <div style=\\\"background:rgba(56,189,248,.08);border:1px solid rgba(56,189,248,.3);border-radius:var(--r-sm);\u001b[39m\n\u001b[31m    padding:8px 12px;margin-bottom:12px;font-size:.72rem;color:var(--sky);\\\">\u001b[39m\n\u001b[31m    ✏️ Review each row — correct name and city if wrong. Raw address shown below each row for reference.\u001b[39m\n\u001b[31m    Corrections are saved and improve future AI parsing automatically.\u001b[39m\n\u001b[31m  </div>\u001b[39m\n\u001b[31m  <div class=\\\"verify-summary\\\" id=\\\"addrReviewSummary\\\"></div>\u001b[39m\n\u001b[31m  <div id=\\\"addrReviewList\\\"></div>\u001b[39m\n\u001b[31m  <div style=\\\"position:sticky;bottom:0;background:var(--bg);padding-top:10px;\u001b[39m\n\u001b[31m    border-top:1px solid var(--bdr);margin-top:8px;\\\">\u001b[39m\n\u001b[31m    <div style=\\\"display:flex;gap:8px;\\\">\u001b[39m\n\u001b[31m      <button class=\\\"btn btn-ghost\\\" onclick=\\\"backToMapping()\\\" style=\\\"flex:1;\\\">← Back to Mapping</button>\u001b[39m\n\u001b[31m      <button class=\\\"btn btn-orange\\\" onclick=\\\"confirmAddressReview()\\\" style=\\\"flex:2;\\\">✓ Confirm &amp; Use These Addresses</button>\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m    <div style=\\\"font-size:.65rem;color:var(--t3);text-align:center;margin-top:5px;font-family:var(--fnt-mono);\\\">\u001b[39m\n\u001b[31m      Edits you made above are preserved when you go back\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m  </div>\u001b[39m\n\u001b[31m</div>·\u001b[39m\n\u001b[31m<!-- ════ VERIFICATION SCREEN ════ -->\u001b[39m\n\u001b[31m<div id=\\\"verifyPane\\\">\u001b[39m\n\u001b[31m  <div class=\\\"verify-hdr\\\">\u001b[39m\n\u001b[31m    <button class=\\\"btn-icon\\\" onclick=\\\"closeVerify()\\\" style=\\\"font-size:1.1rem;\\\">←</button>\u001b[39m\n\u001b[31m    <div class=\\\"verify-title\\\">Verify Before Finalise</div>\u001b[39m\n\u001b[31m    <button class=\\\"btn-sm\\\" onclick=\\\"approveAll()\\\" style=\\\"white-space:nowrap;\\\">✓ Approve All</button>\u001b[39m\n\u001b[31m  </div>\u001b[39m\n\u001b[31m  <div class=\\\"verify-summary\\\" id=\\\"verifySummary\\\"></div>\u001b[39m\n\u001b[31m  <div id=\\\"verifyList\\\"></div>\u001b[39m\n\u001b[31m  <div style=\\\"position:sticky;bottom:0;background:var(--bg);padding-top:10px;\u001b[39m\n\u001b[31m    border-top:1px solid var(--bdr);margin-top:8px;\\\">\u001b[39m\n\u001b[31m    <div style=\\\"display:flex;gap:8px;flex-wrap:wrap;\\\">\u001b[39m\n\u001b[31m      <button class=\\\"btn btn-ghost\\\" onclick=\\\"closeVerify()\\\" style=\\\"flex:1;\\\">← Back to Edit</button>\u001b[39m\n\u001b[31m      <button class=\\\"btn btn-ghost\\\" onclick=\\\"exportCleanSheet()\\\" style=\\\"flex:1;border-color:var(--sky);color:var(--sky);\\\">\u001b[39m\n\u001b[31m        📥 Export Clean Sheet\u001b[39m\n\u001b[31m      </button>\u001b[39m\n\u001b[31m      <button class=\\\"btn btn-orange\\\" id=\\\"btnConfirmFinalise\\\" onclick=\\\"confirmAndFinalise()\\\" style=\\\"flex:2;\\\">\u001b[39m\n\u001b[31m        ✓ Finalise Dispatch\u001b[39m\n\u001b[31m      </button>\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m  </div>\u001b[39m\n\u001b[31m</div>·\u001b[39m\n\u001b[31m</div><!-- /app -->·\u001b[39m\n\u001b[31m<!-- ════ DISPATCH DETAIL SHEET ════ -->\u001b[39m\n\u001b[31m<div class=\\\"overlay\\\" id=\\\"detailOverlay\\\" onclick=\\\"closeOverlay('detailOverlay')\\\">\u001b[39m\n\u001b[31m  <div class=\\\"sheet\\\" onclick=\\\"event.stopPropagation()\\\">\u001b[39m\n\u001b[31m    <div class=\\\"sheet-handle\\\"></div>\u001b[39m\n\u001b[31m    <div id=\\\"detailContent\\\"></div>\u001b[39m\n\u001b[31m  </div>\u001b[39m\n\u001b[31m</div>·\u001b[39m\n\u001b[31m<!-- ════ COURIER MASTER EDITOR ════ -->\u001b[39m\n\u001b[31m<div class=\\\"overlay\\\" id=\\\"courierMasterOverlay\\\" onclick=\\\"closeOverlay('courierMasterOverlay')\\\">\u001b[39m\n\u001b[31m  <div class=\\\"sheet\\\" onclick=\\\"event.stopPropagation()\\\">\u001b[39m\n\u001b[31m    <div class=\\\"sheet-handle\\\"></div>\u001b[39m\n\u001b[31m    <div class=\\\"sheet-title\\\">✏️ Courier Master</div>\u001b[39m\n\u001b[31m    <div id=\\\"courierMasterEditor\\\"></div>\u001b[39m\n\u001b[31m  </div>\u001b[39m\n\u001b[31m</div>·\u001b[39m\n\u001b[31m<!-- ════ MANUAL TRACKING OVERLAY ════ -->\u001b[39m\n\u001b[31m<div class=\\\"overlay\\\" id=\\\"manualTrackOverlay\\\" onclick=\\\"closeOverlay('manualTrackOverlay')\\\" data-docket=\\\"\\\" data-dispatch-id=\\\"\\\">\u001b[39m\n\u001b[31m  <div class=\\\"sheet\\\" onclick=\\\"event.stopPropagation()\\\">\u001b[39m\n\u001b[31m    <div class=\\\"sheet-handle\\\"></div>\u001b[39m\n\u001b[31m    <div class=\\\"sheet-title\\\">📞 Manual Tracking Log</div>\u001b[39m\n\u001b[31m    <div style=\\\"background:var(--sur2);border:1px solid var(--bdr);border-radius:var(--r-sm);\u001b[39m\n\u001b[31m      padding:8px 12px;margin-bottom:12px;font-family:var(--fnt-mono);font-size:.82rem;color:var(--acc);\\\"\u001b[39m\n\u001b[31m      id=\\\"mtDocketDisplay\\\"></div>\u001b[39m\n\u001b[31m    <div class=\\\"row2\\\">\u001b[39m\n\u001b[31m      <div class=\\\"f\\\">\u001b[39m\n\u001b[31m        <label>Status Reported <span class=\\\"req\\\">*</span></label>\u001b[39m\n\u001b[31m        <select id=\\\"mtStatus\\\">\u001b[39m\n\u001b[31m          <option value=\\\"\\\">Select...</option>\u001b[39m\n\u001b[31m          <option value=\\\"delivered\\\">✅ Delivered</option>\u001b[39m\n\u001b[31m          <option value=\\\"not received\\\">❌ Not Received</option>\u001b[39m\n\u001b[31m          <option value=\\\"in transit\\\">🚚 In Transit / On the way</option>\u001b[39m\n\u001b[31m          <option value=\\\"will deliver tomorrow\\\">📅 Will Deliver Tomorrow</option>\u001b[39m\n\u001b[31m          <option value=\\\"refused\\\">🚫 Refused / Returned</option>\u001b[39m\n\u001b[31m          <option value=\\\"partial\\\">📦 Partial Delivery</option>\u001b[39m\n\u001b[31m        </select>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      <div class=\\\"f\\\">\u001b[39m\n\u001b[31m        <label>Called By (Staff) <span class=\\\"req\\\">*</span></label>\u001b[39m\n\u001b[31m        <input type=\\\"text\\\" id=\\\"mtCalledBy\\\" placeholder=\\\"Your name\\\" list=\\\"staffList\\\">\u001b[39m\n\u001b[31m        <datalist id=\\\"staffList\\\">\u001b[39m\n\u001b[31m          <option>Mukesh</option><option>Sachin</option><option>Shambhu</option>\u001b[39m\n\u001b[31m          <option>Umashankar</option><option>Narender</option><option>Rakesh</option>\u001b[39m\n\u001b[31m          <option>Gurpreet</option><option>Ashok</option>\u001b[39m\n\u001b[31m        </datalist>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m    <div class=\\\"row2\\\">\u001b[39m\n\u001b[31m      <div class=\\\"f\\\">\u001b[39m\n\u001b[31m        <label>Contact Number Used</label>\u001b[39m\n\u001b[31m        <input type=\\\"tel\\\" id=\\\"mtContactUsed\\\" placeholder=\\\"Mobile number called\\\">\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      <div class=\\\"f\\\">\u001b[39m\n\u001b[31m        <label>Person Who Confirmed</label>\u001b[39m\n\u001b[31m        <input type=\\\"text\\\" id=\\\"mtContactPerson\\\" placeholder=\\\"Name of person at store\\\">\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m    <div class=\\\"f\\\">\u001b[39m\n\u001b[31m      <label>Remarks</label>\u001b[39m\n\u001b[31m      <textarea id=\\\"mtRemarks\\\" placeholder=\\\"Any notes, delivery details, issues...\\\" style=\\\"min-height:60px;\\\"></textarea>\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m    <div class=\\\"f\\\">\u001b[39m\n\u001b[31m      <label>Call Date &amp; Time</label>\u001b[39m\n\u001b[31m      <input type=\\\"datetime-local\\\" id=\\\"mtCallTime\\\">\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m    <div class=\\\"btn-row\\\">\u001b[39m\n\u001b[31m      <button class=\\\"btn btn-ghost\\\" onclick=\\\"closeOverlay('manualTrackOverlay')\\\">Cancel</button>\u001b[39m\n\u001b[31m      <button class=\\\"btn btn-orange\\\" onclick=\\\"submitManualTracking()\\\">💾 Log &amp; Save</button>\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m  </div>\u001b[39m\n\u001b[31m</div>·\u001b[39m\n\u001b[31m<!-- ════ POD UPLOAD OVERLAY ════ -->\u001b[39m\n\u001b[31m<div class=\\\"overlay\\\" id=\\\"podOverlay\\\" onclick=\\\"closeOverlay('podOverlay')\\\" data-docket=\\\"\\\" data-dispatch-id=\\\"\\\">\u001b[39m\n\u001b[31m  <div class=\\\"sheet\\\" onclick=\\\"event.stopPropagation()\\\">\u001b[39m\n\u001b[31m    <div class=\\\"sheet-handle\\\"></div>\u001b[39m\n\u001b[31m    <div class=\\\"sheet-title\\\">📸 Upload Proof of Delivery</div>\u001b[39m\n\u001b[31m    <div class=\\\"f\\\">\u001b[39m\n\u001b[31m      <label>POD / Delivery Slip Photo</label>\u001b[39m\n\u001b[31m      <input type=\\\"file\\\" id=\\\"podFileInput\\\" accept=\\\"image/*\\\" capture=\\\"environment\\\"\u001b[39m\n\u001b[31m        onchange=\\\"previewPOD(this,'pod')\\\" style=\\\"font-size:.8rem;color:var(--t2);\\\">\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m    <div class=\\\"f\\\">\u001b[39m\n\u001b[31m      <label>Recipient / Receiver Photo (optional)</label>\u001b[39m\n\u001b[31m      <input type=\\\"file\\\" id=\\\"recipientFileInput\\\" accept=\\\"image/*\\\" capture=\\\"environment\\\"\u001b[39m\n\u001b[31m        onchange=\\\"previewPOD(this,'recip')\\\" style=\\\"font-size:.8rem;color:var(--t2);\\\">\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m    <div id=\\\"podPreview\\\" style=\\\"display:flex;gap:8px;flex-wrap:wrap;margin-bottom:12px;\\\"></div>\u001b[39m\n\u001b[31m    <div class=\\\"btn-row\\\">\u001b[39m\n\u001b[31m      <button class=\\\"btn btn-ghost\\\" onclick=\\\"closeOverlay('podOverlay')\\\">Cancel</button>\u001b[39m\n\u001b[31m      <button class=\\\"btn btn-orange\\\" onclick=\\\"uploadPODFiles()\\\">📤 Upload & Mark Delivered</button>\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m  </div>\u001b[39m\n\u001b[31m</div>·\u001b[39m\n\u001b[31m<!-- ════ ADMIN LOGIN OVERLAY ════ -->\u001b[39m\n\u001b[31m<div class=\\\"overlay\\\" id=\\\"adminLoginOverlay\\\" onclick=\\\"closeOverlay('adminLoginOverlay')\\\">\u001b[39m\n\u001b[31m  <div class=\\\"sheet\\\" onclick=\\\"event.stopPropagation()\\\" style=\\\"max-height:320px;\\\">\u001b[39m\n\u001b[31m    <div class=\\\"sheet-handle\\\"></div>\u001b[39m\n\u001b[31m    <div class=\\\"sheet-title\\\">🔐 Admin Access</div>\u001b[39m\n\u001b[31m    <div style=\\\"font-size:.78rem;color:var(--t2);margin-bottom:14px;\\\">Config settings require admin password.</div>\u001b[39m\n\u001b[31m    <div class=\\\"f\\\">\u001b[39m\n\u001b[31m      <label>Password</label>\u001b[39m\n\u001b[31m      <input type=\\\"password\\\" id=\\\"adminPwdInput\\\" placeholder=\\\"Enter admin password\\\"\u001b[39m\n\u001b[31m        onkeydown=\\\"if(event.key==='Enter')submitAdminLogin()\\\">\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m    <div class=\\\"btn-row\\\" style=\\\"margin-top:4px;\\\">\u001b[39m\n\u001b[31m      <button class=\\\"btn btn-ghost\\\" onclick=\\\"closeOverlay('adminLoginOverlay')\\\">Cancel</button>\u001b[39m\n\u001b[31m      <button class=\\\"btn btn-orange\\\" onclick=\\\"submitAdminLogin()\\\">🔓 Login</button>\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m  </div>\u001b[39m\n\u001b[31m</div>·\u001b[39m\n\u001b[31m<!-- ════ TOAST ════ -->\u001b[39m\n\u001b[31m<div class=\\\"toast\\\" id=\\\"toast\\\"></div>·\u001b[39m\n\u001b[31m<!-- ════ SCRIPTS ════ -->\u001b[39m\n\u001b[31m<!-- SheetJS for Excel/CSV parsing -->\u001b[39m\n\u001b[31m<script src=\\\"https://cdnjs.cloudflare.com/ajax/libs/xlsx/0.18.5/xlsx.full.min.js\\\"></script>·\u001b[39m\n\u001b[31m<script type=\\\"module\\\">\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31m// OCR — Transformers.js\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31mimport { pipeline, env } from 'https://cdn.jsdelivr.net/npm/@huggingface/transformers@3.5.2/dist/transformers.min.js';·\u001b[39m\n\u001b[31m// Cache models in browser\u001b[39m\n\u001b[31menv.allowLocalModels = false;\u001b[39m\n\u001b[31menv.useBrowserCache = true;·\u001b[39m\n\u001b[31mlet ocrPipeline = null;\u001b[39m\n\u001b[31mlet ocrLoading = false;·\u001b[39m\n\u001b[31mwindow.initOCR = async function() {\u001b[39m\n\u001b[31m  if (ocrPipeline || ocrLoading) return;\u001b[39m\n\u001b[31m  ocrLoading = true;\u001b[39m\n\u001b[31m  showAIProgress('Loading OCR model (first time ~120MB)...', 5);\u001b[39m\n\u001b[31m  try {\u001b[39m\n\u001b[31m    ocrPipeline = await pipeline('image-to-text', 'Xenova/trocr-base-printed', {\u001b[39m\n\u001b[31m      progress_callback: (p) => {\u001b[39m\n\u001b[31m        if (p.progress) {\u001b[39m\n\u001b[31m          showAIProgress(`Loading: ${p.file || 'model'} (${Math.round(p.progress)}%)`, p.progress);\u001b[39m\n\u001b[31m        }\u001b[39m\n\u001b[31m      }\u001b[39m\n\u001b[31m    });\u001b[39m\n\u001b[31m    showAIProgress('OCR model ready ✓', 100);\u001b[39m\n\u001b[31m    setTimeout(() => hideAIProgress(), 1500);\u001b[39m\n\u001b[31m    showToast('OCR ready ✓', 'ok');\u001b[39m\n\u001b[31m  } catch(e) {\u001b[39m\n\u001b[31m    console.warn('Transformers.js OCR failed:', e);\u001b[39m\n\u001b[31m    hideAIProgress();\u001b[39m\n\u001b[31m    showToast('OCR model load failed — will use Ollama fallback', 'info');\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m  ocrLoading = false;\u001b[39m\n\u001b[31m};·\u001b[39m\n\u001b[31mwindow.runOCROnImage = async function(imageData, filename) {\u001b[39m\n\u001b[31m  const cfg = window.appConfig;·\u001b[39m\n\u001b[31m  // Strategy 1: Transformers.js\u001b[39m\n\u001b[31m  if ((cfg.ocr_mode === 'transformers' || cfg.ocr_mode === 'both') && ocrPipeline) {\u001b[39m\n\u001b[31m    try {\u001b[39m\n\u001b[31m      const result = await ocrPipeline(imageData);\u001b[39m\n\u001b[31m      const text = result[0]?.generated_text || '';\u001b[39m\n\u001b[31m      return extractDocketFromText(text, 'transformers');\u001b[39m\n\u001b[31m    } catch(e) { /* fall through */ }\u001b[39m\n\u001b[31m  }·\u001b[39m\n\u001b[31m  // Strategy 2: Dispatch AI proxy → Ollama LLaVA (server-side, avoids CORS + port issues)\u001b[39m\n\u001b[31m  if (cfg.ocr_mode === 'ollama' || cfg.ocr_mode === 'both') {\u001b[39m\n\u001b[31m    try {\u001b[39m\n\u001b[31m      const resp = await fetch('/dispatch-ai/ocr', {\u001b[39m\n\u001b[31m        method: 'POST',\u001b[39m\n\u001b[31m        headers: { 'Content-Type': 'application/json' },\u001b[39m\n\u001b[31m        body: JSON.stringify({ imageBase64: imageData }),\u001b[39m\n\u001b[31m        signal: AbortSignal.timeout(180000)\u001b[39m\n\u001b[31m      });\u001b[39m\n\u001b[31m      const data = await resp.json();\u001b[39m\n\u001b[31m      if (data.success && data.docket) {\u001b[39m\n\u001b[31m        return { docket: data.docket, confidence: data.confidence, source: 'ollama' };\u001b[39m\n\u001b[31m      }\u001b[39m\n\u001b[31m    } catch(e) { /* fall through */ }\u001b[39m\n\u001b[31m  }·\u001b[39m\n\u001b[31m  return { docket: '', confidence: 0, source: 'failed' };\u001b[39m\n\u001b[31m};·\u001b[39m\n\u001b[31mfunction extractDocketFromText(text, source) {\u001b[39m\n\u001b[31m  // Pattern: typical courier docket = 8-15 digit numbers, sometimes with letters\u001b[39m\n\u001b[31m  const patterns = [\u001b[39m\n\u001b[31m    /\\\\b([A-Z]{0,3}\\\\d{8,15}[A-Z]{0,3})\\\\b/g,   // alphanumeric tracking\u001b[39m\n\u001b[31m    /\\\\b(\\\\d{9,15})\\\\b/g,                          // pure numeric\u001b[39m\n\u001b[31m    /\\\\b([A-Z]{2,4}\\\\d{6,12})\\\\b/g,               // letter prefix\u001b[39m\n\u001b[31m  ];\u001b[39m\n\u001b[31m  const candidates = [];\u001b[39m\n\u001b[31m  for (const pat of patterns) {\u001b[39m\n\u001b[31m    let m;\u001b[39m\n\u001b[31m    while ((m = pat.exec(text)) !== null) {\u001b[39m\n\u001b[31m      if (!candidates.includes(m[1])) candidates.push(m[1]);\u001b[39m\n\u001b[31m    }\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m  if (candidates.length === 0) return { docket: text.trim().slice(0, 20), confidence: 0.3, source };\u001b[39m\n\u001b[31m  // Pick longest candidate (usually most complete docket)\u001b[39m\n\u001b[31m  const best = candidates.sort((a, b) => b.length - a.length)[0];\u001b[39m\n\u001b[31m  const confidence = candidates.length === 1 ? 0.85 : 0.7;\u001b[39m\n\u001b[31m  return { docket: best, confidence, source };\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// Expose for non-module code\u001b[39m\n\u001b[31mwindow._ocrReady = () => ocrPipeline !== null;\u001b[39m\n\u001b[31m</script>·\u001b[39m\n\u001b[31m<script>\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31m// STATE & STORAGE\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31mlet dispatches = [];\u001b[39m\n\u001b[31mlet stores = [];\u001b[39m\n\u001b[31mlet slipOCRData = [];\u001b[39m\n\u001b[31mlet dashFilter = 'all';·\u001b[39m\n\u001b[31m// Only sheets_url is persisted locally — all other config comes from the sheet\u001b[39m\n\u001b[31mlet _trackingInProgress = false;\u001b[39m\n\u001b[31mwindow.appConfig = {\u001b[39m\n\u001b[31m  sheets_url:      'https://script.google.com/macros/s/AKfycbys8HrToDv2u_dRvSFnNqmO9bnyAO1s8oSprUydpVQnNQutq4tuUY71cVCH24urbvM9/exec',\u001b[39m\n\u001b[31m  webhook_url:     '',\u001b[39m\n\u001b[31m  proxy_url:       '',\u001b[39m\n\u001b[31m  ollama_url:      'http://srv1111289.hstgr.cloud:11434',\u001b[39m\n\u001b[31m  ocr_mode:        'both',\u001b[39m\n\u001b[31m  company:         '360 Degree Logical Marketing',\u001b[39m\n\u001b[31m  city:            'Chandigarh',\u001b[39m\n\u001b[31m  auto_log:        'true',\u001b[39m\n\u001b[31m  refresh_on_load: 'false',\u001b[39m\n\u001b[31m};·\u001b[39m\n\u001b[31mconst COURIER_MASTER = {\u001b[39m\n\u001b[31m  'SkyKing': {\u001b[39m\n\u001b[31m    trackUrl: 'https://skyking.co/track?cno={docket}',\u001b[39m\n\u001b[31m    trackBulkUrl: 'https://skyking.co/track',\u001b[39m\n\u001b[31m    apiProxy: '/track-proxy/skyking?cno={docket}',\u001b[39m\n\u001b[31m    maxBulk: 10\u001b[39m\n\u001b[31m  },\u001b[39m\n\u001b[31m  'DTDC': {\u001b[39m\n\u001b[31m    trackUrl: 'https://www.dtdc.in/tracking.asp',\u001b[39m\n\u001b[31m    queryParam: 'trkId',\u001b[39m\n\u001b[31m    maxBulk: 1\u001b[39m\n\u001b[31m  },\u001b[39m\n\u001b[31m  'Delhivery': {\u001b[39m\n\u001b[31m    trackUrl: 'https://www.delhivery.com/track/package/{docket}',\u001b[39m\n\u001b[31m    maxBulk: 1\u001b[39m\n\u001b[31m  },\u001b[39m\n\u001b[31m  'Blue Dart': {\u001b[39m\n\u001b[31m    trackUrl: 'https://www.bluedart.com/tracking',\u001b[39m\n\u001b[31m    maxBulk: 1\u001b[39m\n\u001b[31m  },\u001b[39m\n\u001b[31m  'FedEx': {\u001b[39m\n\u001b[31m    trackUrl: 'https://www.fedex.com/fedextrack/?trknbr={docket}',\u001b[39m\n\u001b[31m    maxBulk: 10\u001b[39m\n\u001b[31m  },\u001b[39m\n\u001b[31m  'Ecom Express': {\u001b[39m\n\u001b[31m    trackUrl: 'https://ecomexpress.in/tracking/?awb_field={docket}',\u001b[39m\n\u001b[31m    maxBulk: 1\u001b[39m\n\u001b[31m  },\u001b[39m\n\u001b[31m  'India Post': {\u001b[39m\n\u001b[31m    trackUrl: 'https://www.indiapost.gov.in/VAS/Pages/trackconsignment.aspx',\u001b[39m\n\u001b[31m    maxBulk: 1\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m};·\u001b[39m\n\u001b[31mfunction loadData() {\u001b[39m\n\u001b[31m  try {\u001b[39m\n\u001b[31m    dispatches = JSON.parse(localStorage.getItem('360lm_disp_v2') || '[]');\u001b[39m\n\u001b[31m    const saved = JSON.parse(localStorage.getItem('360lm_disp_sheets_url') || '{}');\u001b[39m\n\u001b[31m    if (saved.sheets_url) window.appConfig.sheets_url = saved.sheets_url;\u001b[39m\n\u001b[31m  } catch(e) {}\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31mfunction saveData() {\u001b[39m\n\u001b[31m  try {\u001b[39m\n\u001b[31m    localStorage.setItem('360lm_disp_v2', JSON.stringify(dispatches));\u001b[39m\n\u001b[31m  } catch(e) {\u001b[39m\n\u001b[31m    if (e.name === 'QuotaExceededError' || e.code === 22) {\u001b[39m\n\u001b[31m      showToast('Storage full — old dispatches cleared. Data is safe in Postgres.', 'err');\u001b[39m\n\u001b[31m      // Drop delivered dispatches older than 30 days to free space\u001b[39m\n\u001b[31m      const cutoff = Date.now() - 30 * 24 * 3600 * 1000;\u001b[39m\n\u001b[31m      dispatches = dispatches.filter(d =>\u001b[39m\n\u001b[31m        d.status !== 'draft' && (\u001b[39m\n\u001b[31m          d.status !== 'active' ||\u001b[39m\n\u001b[31m          new Date(d.createdAt || 0).getTime() > cutoff\u001b[39m\n\u001b[31m        ) ? true :\u001b[39m\n\u001b[31m        d.status === 'draft' || new Date(d.createdAt || 0).getTime() > cutoff\u001b[39m\n\u001b[31m      );\u001b[39m\n\u001b[31m      try { localStorage.setItem('360lm_disp_v2', JSON.stringify(dispatches)); } catch(_) {}\u001b[39m\n\u001b[31m    }\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// ── Postgres DB sync ──────────────────────────────────────────────────────\u001b[39m\n\u001b[31mconst _DB        = '/db';\u001b[39m\n\u001b[31m// PostgREST v12: Accept-Profile for reads, Content-Profile for writes\u001b[39m\n\u001b[31mconst _DHDR_READ  = { 'Accept-Profile': 'dispatch' };\u001b[39m\n\u001b[31mconst _DHDR_WRITE = { 'Content-Type': 'application/json', 'Content-Profile': 'dispatch', 'Accept-Profile': 'dispatch' };\u001b[39m\n\u001b[31mconst _DHDR_UPSERT = { ..._DHDR_WRITE, 'Prefer': 'resolution=merge-duplicates,return=minimal' };·\u001b[39m\n\u001b[31mfunction _dispRow(d) {\u001b[39m\n\u001b[31m  return {\u001b[39m\n\u001b[31m    dispatch_id:    d.id,\u001b[39m\n\u001b[31m    client:         d.client    || '',\u001b[39m\n\u001b[31m    job_ref:        d.jobref    || null,\u001b[39m\n\u001b[31m    courier:        d.courier   || '',\u001b[39m\n\u001b[31m    dispatch_date:  d.dispatchDate || null,\u001b[39m\n\u001b[31m    status:         d.status    || 'active',\u001b[39m\n\u001b[31m    total_count:    (d.consignees || []).length,\u001b[39m\n\u001b[31m    docketed_count: (d.consignees || []).filter(c => c.docket).length,\u001b[39m\n\u001b[31m    remarks:        d.remarks   || null,\u001b[39m\n\u001b[31m    created_by:     window._hubUser?.id || 'unknown',\u001b[39m\n\u001b[31m  };\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction _consRow(c, dispatchId, idx) {\u001b[39m\n\u001b[31m  return {\u001b[39m\n\u001b[31m    cons_id:            `${dispatchId}-${idx}`,\u001b[39m\n\u001b[31m    dispatch_id:        dispatchId,\u001b[39m\n\u001b[31m    seq_no:             idx,\u001b[39m\n\u001b[31m    name:               c.name          || '',\u001b[39m\n\u001b[31m    city:               c.city          || null,\u001b[39m\n\u001b[31m    address:            c.address       || null,\u001b[39m\n\u001b[31m    phone:              c.phone         || null,\u001b[39m\n\u001b[31m    pincode:            c.pincode       || null,\u001b[39m\n\u001b[31m    material:           c.material      || null,\u001b[39m\n\u001b[31m    qty:                c.qty           || null,\u001b[39m\n\u001b[31m    docket:             c.docket        || null,\u001b[39m\n\u001b[31m    status:             c.status        || 'pending',\u001b[39m\n\u001b[31m    last_event:         c.lastEvent     || null,\u001b[39m\n\u001b[31m    last_scan_location: c.lastScanLocation || null,\u001b[39m\n\u001b[31m    last_tracked_at:    c.lastTrackedAt || null,\u001b[39m\n\u001b[31m    manual_status:      c.manualStatus  || null,\u001b[39m\n\u001b[31m    manual_delivered:   !!c.manualDelivered,\u001b[39m\n\u001b[31m    manual_called_by:   c.manualCalledBy || null,\u001b[39m\n\u001b[31m    manual_remarks:     c.manualRemarks || null,\u001b[39m\n\u001b[31m    pod_uploaded:       !!c.podUploaded,\u001b[39m\n\u001b[31m  };\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31masync function syncDispatchToDB(d) {\u001b[39m\n\u001b[31m  if (!d || d.status === 'draft') return; // never sync drafts\u001b[39m\n\u001b[31m  try {\u001b[39m\n\u001b[31m    await fetch(`${_DB}/dispatches`, {\u001b[39m\n\u001b[31m      method: 'POST', headers: _DHDR_UPSERT,\u001b[39m\n\u001b[31m      body: JSON.stringify(_dispRow(d)),\u001b[39m\n\u001b[31m    });\u001b[39m\n\u001b[31m    const rows = (d.consignees || []).map((c, i) => _consRow(c, d.id, i));\u001b[39m\n\u001b[31m    if (rows.length) {\u001b[39m\n\u001b[31m      await fetch(`${_DB}/consignees`, {\u001b[39m\n\u001b[31m        method: 'POST', headers: _DHDR_UPSERT,\u001b[39m\n\u001b[31m        body: JSON.stringify(rows),\u001b[39m\n\u001b[31m      });\u001b[39m\n\u001b[31m    }\u001b[39m\n\u001b[31m  } catch(e) { console.warn('[dispatch-db] sync error:', e.message); }\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31masync function syncConsigneeToDB(c, dispatchId, idx) {\u001b[39m\n\u001b[31m  try {\u001b[39m\n\u001b[31m    await fetch(`${_DB}/consignees`, {\u001b[39m\n\u001b[31m      method: 'POST', headers: _DHDR_UPSERT,\u001b[39m\n\u001b[31m      body: JSON.stringify(_consRow(c, dispatchId, idx)),\u001b[39m\n\u001b[31m    });\u001b[39m\n\u001b[31m    // Log tracking event\u001b[39m\n\u001b[31m    if (c.lastEvent && c.status) {\u001b[39m\n\u001b[31m      fetch(`${_DB}/tracking_events`, {\u001b[39m\n\u001b[31m        method: 'POST', headers: _DHDR_WRITE,\u001b[39m\n\u001b[31m        body: JSON.stringify({\u001b[39m\n\u001b[31m          docket: c.docket, dispatch_id: dispatchId,\u001b[39m\n\u001b[31m          cons_id: `${dispatchId}-${idx}`,\u001b[39m\n\u001b[31m          status: c.status, event_text: c.lastEvent,\u001b[39m\n\u001b[31m          scan_location: c.lastScanLocation || null,\u001b[39m\n\u001b[31m        }),\u001b[39m\n\u001b[31m      }).catch(() => {});\u001b[39m\n\u001b[31m    }\u001b[39m\n\u001b[31m  } catch(e) { console.warn('[dispatch-db] consignee sync error:', e.message); }\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31masync function loadFromDB() {\u001b[39m\n\u001b[31m  try {\u001b[39m\n\u001b[31m    const [dResp, cResp] = await Promise.all([\u001b[39m\n\u001b[31m      fetch(`${_DB}/dispatches?order=created_at.desc&limit=100`, { headers: _DHDR_READ }),\u001b[39m\n\u001b[31m      fetch(`${_DB}/consignees?order=dispatch_id,seq_no`, { headers: _DHDR_READ }),\u001b[39m\n\u001b[31m    ]);\u001b[39m\n\u001b[31m    if (!dResp.ok) return;\u001b[39m\n\u001b[31m    const dbDispatches = await dResp.json();\u001b[39m\n\u001b[31m    const dbConsignees = cResp.ok ? await cResp.json() : [];\u001b[39m\n\u001b[31m    if (!dbDispatches.length) return;·\u001b[39m\n\u001b[31m    const dbIds = new Set(dbDispatches.map(d => d.dispatch_id));\u001b[39m\n\u001b[31m    const localOnly = dispatches.filter(d => !dbIds.has(d.id));·\u001b[39m\n\u001b[31m    const fromDB = dbDispatches.map(d => {\u001b[39m\n\u001b[31m      const cons = dbConsignees.filter(c => c.dispatch_id === d.dispatch_id);\u001b[39m\n\u001b[31m      return {\u001b[39m\n\u001b[31m        id:           d.dispatch_id,\u001b[39m\n\u001b[31m        client:       d.client,\u001b[39m\n\u001b[31m        jobref:       d.job_ref,\u001b[39m\n\u001b[31m        courier:      d.courier,\u001b[39m\n\u001b[31m        dispatchDate: d.dispatch_date,\u001b[39m\n\u001b[31m        status:       d.status,\u001b[39m\n\u001b[31m        remarks:      d.remarks,\u001b[39m\n\u001b[31m        createdAt:    d.created_at,\u001b[39m\n\u001b[31m        consignees:   cons.map(c => ({\u001b[39m\n\u001b[31m          name:              c.name,\u001b[39m\n\u001b[31m          city:              c.city,\u001b[39m\n\u001b[31m          address:           c.address,\u001b[39m\n\u001b[31m          phone:             c.phone,\u001b[39m\n\u001b[31m          pincode:           c.pincode,\u001b[39m\n\u001b[31m          material:          c.material,\u001b[39m\n\u001b[31m          qty:               c.qty,\u001b[39m\n\u001b[31m          docket:            c.docket,\u001b[39m\n\u001b[31m          status:            c.status,\u001b[39m\n\u001b[31m          lastEvent:         c.last_event,\u001b[39m\n\u001b[31m          lastScanLocation:  c.last_scan_location,\u001b[39m\n\u001b[31m          lastTrackedAt:     c.last_tracked_at,\u001b[39m\n\u001b[31m          manualStatus:      c.manual_status,\u001b[39m\n\u001b[31m          manualDelivered:   c.manual_delivered,\u001b[39m\n\u001b[31m          manualCalledBy:    c.manual_called_by,\u001b[39m\n\u001b[31m          manualRemarks:     c.manual_remarks,\u001b[39m\n\u001b[31m          podUploaded:       c.pod_uploaded,\u001b[39m\n\u001b[31m          _dbIdx:            c.seq_no,\u001b[39m\n\u001b[31m        })),\u001b[39m\n\u001b[31m      };\u001b[39m\n\u001b[31m    });·\u001b[39m\n\u001b[31m    dispatches = [...fromDB, ...localOnly];\u001b[39m\n\u001b[31m    saveData();\u001b[39m\n\u001b[31m    renderDashboard();\u001b[39m\n\u001b[31m    updateHistCount();\u001b[39m\n\u001b[31m  } catch(e) { console.warn('[dispatch-db] load error:', e.message); }\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31mfunction saveSheetsUrlLocal() {\u001b[39m\n\u001b[31m  localStorage.setItem('360lm_disp_sheets_url', JSON.stringify({ sheets_url: window.appConfig.sheets_url }));\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m/**\u001b[39m\n\u001b[31m * Fetch config.json from the VPS server (same origin).\u001b[39m\n\u001b[31m * This file contains the sheets_url — no manual entry needed.\u001b[39m\n\u001b[31m * Falls back to localStorage cache if fetch fails (offline).\u001b[39m\n\u001b[31m */\u001b[39m\n\u001b[31masync function loadServerConfig() {\u001b[39m\n\u001b[31m  try {\u001b[39m\n\u001b[31m    const resp = await fetch('./config.json', { cache: 'no-store' });\u001b[39m\n\u001b[31m    if (resp.ok) {\u001b[39m\n\u001b[31m      const cfg = await resp.json();\u001b[39m\n\u001b[31m      if (cfg.sheets_url) {\u001b[39m\n\u001b[31m        window.appConfig.sheets_url = cfg.sheets_url;\u001b[39m\n\u001b[31m        saveSheetsUrlLocal(); // cache it for offline use\u001b[39m\n\u001b[31m        document.getElementById('cfg_sheets_url').value = cfg.sheets_url;\u001b[39m\n\u001b[31m        return true;\u001b[39m\n\u001b[31m      }\u001b[39m\n\u001b[31m    }\u001b[39m\n\u001b[31m  } catch(e) {\u001b[39m\n\u001b[31m    console.log('config.json fetch failed, using cached URL:', e.message);\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m  return false;\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31m// INIT\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31m// ── Hub auth gate ─────────────────────────────────────────────────────────\u001b[39m\n\u001b[31mwindow._hubUser = null;\u001b[39m\n\u001b[31m(function() {\u001b[39m\n\u001b[31m  try {\u001b[39m\n\u001b[31m    const hubLogin = '/hub/?next=' + encodeURIComponent(location.pathname + location.search);\u001b[39m\n\u001b[31m    const s = JSON.parse(localStorage.getItem('lm360-session') || 'null');\u001b[39m\n\u001b[31m    if (!s || !s.empId || !s.name) { window.location.replace(hubLogin); return; }\u001b[39m\n\u001b[31m    if (Date.now() - new Date(s.loginAt).getTime() > 12 * 3600 * 1000) {\u001b[39m\n\u001b[31m      window.location.replace(hubLogin); return;\u001b[39m\n\u001b[31m    }\u001b[39m\n\u001b[31m    window._hubUser = { id: s.empId, name: s.name, role: s.role };\u001b[39m\n\u001b[31m  } catch(_) { window.location.replace('/hub/?next=' + encodeURIComponent(location.pathname + location.search)); }\u001b[39m\n\u001b[31m})();·\u001b[39m\n\u001b[31mwindow.addEventListener('load', async () => {\u001b[39m\n\u001b[31m  if (!window._hubUser) return; // should never reach here — gate redirected already\u001b[39m\n\u001b[31m  setTimeout(() => {\u001b[39m\n\u001b[31m    document.getElementById('splash').style.opacity = '0';\u001b[39m\n\u001b[31m    setTimeout(() => {\u001b[39m\n\u001b[31m      document.getElementById('splash').style.display = 'none';\u001b[39m\n\u001b[31m      document.getElementById('app').style.display = 'flex';\u001b[39m\n\u001b[31m    }, 450);\u001b[39m\n\u001b[31m  }, 1400);\u001b[39m\n\u001b[31m  loadData();\u001b[39m\n\u001b[31m  // Auto-authenticate config tab for admin users (no password needed)\u001b[39m\n\u001b[31m  if (['harish', 'pramod'].includes(window._hubUser?.id)) _adminAuthed = true;\u001b[39m\n\u001b[31m  setToday();\u001b[39m\n\u001b[31m  addConsignee();\u001b[39m\n\u001b[31m  updateHistCount();\u001b[39m\n\u001b[31m  setInterval(updateNetDot, 5000);\u001b[39m\n\u001b[31m  updateNetDot();\u001b[39m\n\u001b[31m  checkShareTarget();\u001b[39m\n\u001b[31m  setupDragDrop();·\u001b[39m\n\u001b[31m  // Step 1: fetch sheets_url from VPS config.json (auto, no user input)\u001b[39m\n\u001b[31m  await loadServerConfig();·\u001b[39m\n\u001b[31m  // Step 2: populate config tab field\u001b[39m\n\u001b[31m  document.getElementById('cfg_sheets_url').value = window.appConfig.sheets_url || '';·\u001b[39m\n\u001b[31m  // Step 3: load dispatches from Postgres in background (merge with local)\u001b[39m\n\u001b[31m  loadFromDB(); // non-blocking — updates UI when data arrives·\u001b[39m\n\u001b[31m  // Step 4: sync full config from Google Sheet silently\u001b[39m\n\u001b[31m  if (window.appConfig.sheets_url) {\u001b[39m\n\u001b[31m    syncFromSheet(true); // silent on startup\u001b[39m\n\u001b[31m  } else {\u001b[39m\n\u001b[31m    // Only show config tab if no URL found anywhere\u001b[39m\n\u001b[31m    showPane('config');\u001b[39m\n\u001b[31m    showToast('No config.json found on server', 'err');\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m  // Pre-warm OCR after delay\u001b[39m\n\u001b[31m  setTimeout(() => { if (typeof window.initOCR === 'function') window.initOCR(); }, 4000);\u001b[39m\n\u001b[31m});·\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31m// WEB SHARE TARGET\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31masync function checkShareTarget() {\u001b[39m\n\u001b[31m  // Check URL params for share target\u001b[39m\n\u001b[31m  const params = new URLSearchParams(window.location.search);\u001b[39m\n\u001b[31m  const action = params.get('action');\u001b[39m\n\u001b[31m  if (action !== 'share') return;·\u001b[39m\n\u001b[31m  // Check for shared files via Share Target API\u001b[39m\n\u001b[31m  if ('launchQueue' in window) {\u001b[39m\n\u001b[31m    window.launchQueue.setConsumer(async launchParams => {\u001b[39m\n\u001b[31m      if (!launchParams.files || launchParams.files.length === 0) return;\u001b[39m\n\u001b[31m      const files = await Promise.all(launchParams.files.map(f => f.getFile()));\u001b[39m\n\u001b[31m      const imageFiles = files.filter(f => f.type.startsWith('image/'));\u001b[39m\n\u001b[31m      if (imageFiles.length > 0) {\u001b[39m\n\u001b[31m        document.getElementById('shareTargetInfo').style.display = 'flex';\u001b[39m\n\u001b[31m        document.getElementById('sharedCount').textContent = imageFiles.length;\u001b[39m\n\u001b[31m        document.getElementById('shareBanner').classList.add('show');\u001b[39m\n\u001b[31m        setTimeout(() => document.getElementById('shareBanner').classList.remove('show'), 4000);\u001b[39m\n\u001b[31m        showPane('new');\u001b[39m\n\u001b[31m        await handleSlipFiles(imageFiles);\u001b[39m\n\u001b[31m      }\u001b[39m\n\u001b[31m    });\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction dismissShare() {\u001b[39m\n\u001b[31m  document.getElementById('shareBanner').classList.remove('show');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31m// DRAG & DROP\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31mfunction setupDragDrop() {\u001b[39m\n\u001b[31m  const zone = document.getElementById('slipZone');\u001b[39m\n\u001b[31m  zone.addEventListener('dragover', e => { e.preventDefault(); zone.classList.add('drag'); });\u001b[39m\n\u001b[31m  zone.addEventListener('dragleave', () => zone.classList.remove('drag'));\u001b[39m\n\u001b[31m  zone.addEventListener('drop', e => {\u001b[39m\n\u001b[31m    e.preventDefault();\u001b[39m\n\u001b[31m    zone.classList.remove('drag');\u001b[39m\n\u001b[31m    const files = Array.from(e.dataTransfer.files).filter(f => f.type.startsWith('image/'));\u001b[39m\n\u001b[31m    if (files.length) handleSlipFiles(files);\u001b[39m\n\u001b[31m  });\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31m// SLIP OCR HANDLER\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31masync function handleSlipFiles(files) {\u001b[39m\n\u001b[31m  const fileArr = Array.from(files);\u001b[39m\n\u001b[31m  if (!fileArr.length) return;\u001b[39m\n\u001b[31m  // If dockets already mapped from store list — allow upload but skip OCR extraction\u001b[39m\n\u001b[31m  const skipOCR = shouldSkipOCR();\u001b[39m\n\u001b[31m  if (skipOCR) {\u001b[39m\n\u001b[31m    showToast('Dockets already mapped — slips saved for reference only', 'info');\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m  showOCRWorking(true, `Processing ${fileArr.length} slip(s)...`);·\u001b[39m\n\u001b[31m  for (let i = 0; i < fileArr.length; i++) {\u001b[39m\n\u001b[31m    const file = fileArr[i];\u001b[39m\n\u001b[31m    showOCRWorking(true, `OCR: ${i+1}/${fileArr.length} — ${file.name}`);\u001b[39m\n\u001b[31m    const imageData = await readFileAsDataURL(file);\u001b[39m\n\u001b[31m    let result = { docket: '', confidence: 0, source: 'manual' };\u001b[39m\n\u001b[31m    if (!skipOCR) {\u001b[39m\n\u001b[31m      try {\u001b[39m\n\u001b[31m        result = await window.runOCROnImage(imageData, file.name);\u001b[39m\n\u001b[31m      } catch(e) {}\u001b[39m\n\u001b[31m    } else {\u001b[39m\n\u001b[31m      result = { docket: '', confidence: 1, source: 'skipped — docket pre-mapped' };\u001b[39m\n\u001b[31m    }\u001b[39m\n\u001b[31m    slipOCRData.push({ file, imageData, filename: file.name, ...result });\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m  showOCRWorking(false);\u001b[39m\n\u001b[31m  renderOCRResults();\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction readFileAsDataURL(file) {\u001b[39m\n\u001b[31m  return new Promise((res, rej) => {\u001b[39m\n\u001b[31m    const r = new FileReader();\u001b[39m\n\u001b[31m    r.onload = e => res(e.target.result);\u001b[39m\n\u001b[31m    r.onerror = rej;\u001b[39m\n\u001b[31m    r.readAsDataURL(file);\u001b[39m\n\u001b[31m  });\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction renderOCRResults() {\u001b[39m\n\u001b[31m  if (!slipOCRData.length) return;\u001b[39m\n\u001b[31m  const list = document.getElementById('ocrSlipList');\u001b[39m\n\u001b[31m  document.getElementById('ocrResults').classList.add('visible');·\u001b[39m\n\u001b[31m  const needReview = slipOCRData.filter(s => s.confidence < 0.65 && s.docket).length;\u001b[39m\n\u001b[31m  const autoOk     = slipOCRData.filter(s => s.confidence >= 0.88).length;\u001b[39m\n\u001b[31m  const summary    = document.getElementById('ocrSummary');\u001b[39m\n\u001b[31m  if (summary) {\u001b[39m\n\u001b[31m    summary.style.display = '';\u001b[39m\n\u001b[31m    summary.innerHTML =\u001b[39m\n\u001b[31m      `<span style=\\\"color:var(--grn)\\\">✓ ${autoOk} auto-accepted</span>` +\u001b[39m\n\u001b[31m      (needReview ? `&nbsp;·&nbsp;<span style=\\\"color:var(--ylw)\\\">⚠ ${needReview} need manual check</span>` : '');\u001b[39m\n\u001b[31m  }·\u001b[39m\n\u001b[31m  list.innerHTML = slipOCRData.map((s, i) => {\u001b[39m\n\u001b[31m    const autoAccepted = s.confidence >= 0.88 && s.docket;\u001b[39m\n\u001b[31m    const needsCheck   = s.confidence < 0.65  && s.docket;\u001b[39m\n\u001b[31m    const confClass = autoAccepted ? 'conf-high' : needsCheck ? 'conf-low' : 'conf-med';\u001b[39m\n\u001b[31m    const confLabel = autoAccepted\u001b[39m\n\u001b[31m      ? `✓ ${Math.round(s.confidence * 100)}% — auto`\u001b[39m\n\u001b[31m      : needsCheck\u001b[39m\n\u001b[31m        ? `⚠ ${Math.round(s.confidence * 100)}% — verify`\u001b[39m\n\u001b[31m        : `${Math.round(s.confidence * 100)}%`;\u001b[39m\n\u001b[31m    const borderStyle = needsCheck ? 'border:1px solid var(--ylw);' : '';\u001b[39m\n\u001b[31m    return `\u001b[39m\n\u001b[31m    <div class=\\\"ocr-slip\\\" id=\\\"ocrSlip_${i}\\\" style=\\\"${borderStyle}\\\">\u001b[39m\n\u001b[31m      <img src=\\\"${s.imageData}\\\" alt=\\\"slip ${i+1}\\\">\u001b[39m\n\u001b[31m      <div class=\\\"ocr-slip-body\\\">\u001b[39m\n\u001b[31m        <div class=\\\"ocr-slip-name\\\">${s.filename}</div>\u001b[39m\n\u001b[31m        <input type=\\\"text\\\" class=\\\"ocr-slip-docket\\\" id=\\\"ocrDocket_${i}\\\"\u001b[39m\n\u001b[31m          value=\\\"${s.docket}\\\" placeholder=\\\"Docket number\\\"\u001b[39m\n\u001b[31m          ${autoAccepted ? 'style=\\\"opacity:.6\\\"' : ''}\u001b[39m\n\u001b[31m          oninput=\\\"slipOCRData[${i}].docket=this.value;slipOCRData[${i}].confidence=0.5\\\">\u001b[39m\n\u001b[31m        <div class=\\\"ocr-slip-status\\\">\u001b[39m\n\u001b[31m          <span class=\\\"ocr-confidence ${confClass}\\\">${confLabel}</span>\u001b[39m\n\u001b[31m          <span style=\\\"color:var(--t4);font-family:var(--fnt-mono);font-size:.6rem;\\\">via ${s.source}</span>\u001b[39m\n\u001b[31m          <button class=\\\"ocr-ai-btn\\\" onclick=\\\"reOCR(${i})\\\">🔄 Re-OCR</button>\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m    </div>`;\u001b[39m\n\u001b[31m  }).join('');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31masync function reOCR(i) {\u001b[39m\n\u001b[31m  const s = slipOCRData[i];\u001b[39m\n\u001b[31m  showOCRWorking(true, `Re-running OCR on ${s.filename}...`);\u001b[39m\n\u001b[31m  try {\u001b[39m\n\u001b[31m    const result = await window.runOCROnImage(s.imageData, s.filename);\u001b[39m\n\u001b[31m    slipOCRData[i] = { ...s, ...result };\u001b[39m\n\u001b[31m    renderOCRResults();\u001b[39m\n\u001b[31m    showToast('Re-OCR complete', 'ok');\u001b[39m\n\u001b[31m  } catch(e) {\u001b[39m\n\u001b[31m    showToast('OCR failed', 'err');\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m  showOCRWorking(false);\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction clearSlips() {\u001b[39m\n\u001b[31m  slipOCRData = [];\u001b[39m\n\u001b[31m  document.getElementById('ocrResults').classList.remove('visible');\u001b[39m\n\u001b[31m  document.getElementById('ocrSlipList').innerHTML = '';\u001b[39m\n\u001b[31m  const sum = document.getElementById('ocrSummary');\u001b[39m\n\u001b[31m  if (sum) { sum.style.display = 'none'; sum.innerHTML = ''; }\u001b[39m\n\u001b[31m  document.getElementById('shareTargetInfo').style.display = 'none';\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction applyAllDockets() {\u001b[39m\n\u001b[31m  // Match each OCR result to a consignee by order or by name matching\u001b[39m\n\u001b[31m  const consList = document.getElementById('consList');\u001b[39m\n\u001b[31m  const cards = consList.querySelectorAll('.cons-card');\u001b[39m\n\u001b[31m  slipOCRData.forEach((s, i) => {\u001b[39m\n\u001b[31m    if (!s.docket) return;\u001b[39m\n\u001b[31m    const card = cards[i];\u001b[39m\n\u001b[31m    if (card) {\u001b[39m\n\u001b[31m      const docketInput = card.querySelector('.cons-docket-input');\u001b[39m\n\u001b[31m      if (docketInput) {\u001b[39m\n\u001b[31m        docketInput.value = s.docket;\u001b[39m\n\u001b[31m        showLinkedDocket(card, s.docket);\u001b[39m\n\u001b[31m      }\u001b[39m\n\u001b[31m    }\u001b[39m\n\u001b[31m  });\u001b[39m\n\u001b[31m  showToast(`Applied ${slipOCRData.filter(s=>s.docket).length} dockets ✓`, 'ok');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31m// STORE LIST — with column mapping UI\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31mlet _rawRows = [];      // raw rows from file, kept for re-mapping\u001b[39m\n\u001b[31mlet _rawHeaders = [];   // headers from file·\u001b[39m\n\u001b[31masync function handleStoreFile(input) {\u001b[39m\n\u001b[31m  const file = input.files[0];\u001b[39m\n\u001b[31m  if (!file) return;\u001b[39m\n\u001b[31m  showToast('Reading file...', 'info');·\u001b[39m\n\u001b[31m  const data = await readFileAsDataURL(file);\u001b[39m\n\u001b[31m  const base64 = data.split(',')[1];\u001b[39m\n\u001b[31m  const binary = atob(base64);\u001b[39m\n\u001b[31m  const bytes = new Uint8Array(binary.length);\u001b[39m\n\u001b[31m  for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);·\u001b[39m\n\u001b[31m  let rows = [];\u001b[39m\n\u001b[31m  try {\u001b[39m\n\u001b[31m    if (file.name.endsWith('.csv')) {\u001b[39m\n\u001b[31m      const text = new TextDecoder().decode(bytes);\u001b[39m\n\u001b[31m      rows = text.split('\\\\n').map(r => r.split(',').map(c => c.trim().replace(/^\\\"|\\\"$/g, '')));\u001b[39m\n\u001b[31m    } else {\u001b[39m\n\u001b[31m      const wb = XLSX.read(bytes, { type: 'array' });\u001b[39m\n\u001b[31m      const ws = wb.Sheets[wb.SheetNames[0]];\u001b[39m\n\u001b[31m      rows = XLSX.utils.sheet_to_json(ws, { header: 1 });\u001b[39m\n\u001b[31m    }\u001b[39m\n\u001b[31m  } catch(e) { showToast('File read error: ' + e.message, 'err'); return; }·\u001b[39m\n\u001b[31m  if (!rows.length || !rows[0]) { showToast('File appears empty', 'err'); return; }·\u001b[39m\n\u001b[31m  _rawHeaders = (rows[0] || []).map(h => (h || '').toString().trim());\u001b[39m\n\u001b[31m  _rawRows = rows.slice(1).filter(r => r && r.some(c => c !== '' && c != null));·\u001b[39m\n\u001b[31m  document.getElementById('storeTableArea').style.display = 'block';\u001b[39m\n\u001b[31m  renderMappingUI(file.name);\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// ── COLUMN MAPPING FIELDS ─────────────────────────────\u001b[39m\n\u001b[31mconst MAPPING_FIELDS = [\u001b[39m\n\u001b[31m  { key: 'name',    label: 'Store / Partner Name', required: true,\u001b[39m\n\u001b[31m    keywords: ['store','shop','name','outlet','consignee','destination','partner','dealer','client','retailer'] },\u001b[39m\n\u001b[31m  { key: 'city',    label: 'City / Location',       required: false,\u001b[39m\n\u001b[31m    keywords: ['city','town','district','location','area','region','place'] },\u001b[39m\n\u001b[31m  { key: 'phone',   label: 'Phone / Mobile',        required: false,\u001b[39m\n\u001b[31m    keywords: ['phone','mobile','contact','tel','number','mob'] },\u001b[39m\n\u001b[31m  { key: 'docket',  label: 'Docket / AWB No.',      required: false,\u001b[39m\n\u001b[31m    keywords: ['docket','awb','tracking','courier','dkt','consignment','cno','waybill'] },\u001b[39m\n\u001b[31m  { key: 'address', label: 'Address',               required: false,\u001b[39m\n\u001b[31m    keywords: ['address','addr','street','locality','area'] },\u001b[39m\n\u001b[31m  { key: 'pincode', label: 'Pincode / PIN',          required: false,\u001b[39m\n\u001b[31m    keywords: ['pin','pincode','postal','zip','code'] },\u001b[39m\n\u001b[31m];·\u001b[39m\n\u001b[31m// Saved mapping key based on column fingerprint (so same client file auto-maps)\u001b[39m\n\u001b[31mfunction getMappingKey() {\u001b[39m\n\u001b[31m  return 'colmap_' + _rawHeaders.join('|').toLowerCase().replace(/\\\\s+/g,'');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// ── Detect if a column is a combined/multiline address cell ─────────────\u001b[39m\n\u001b[31mfunction isCombinedColumn(colIdx) {\u001b[39m\n\u001b[31m  if (colIdx < 0) return false;\u001b[39m\n\u001b[31m  // Check first 3 data rows — if any cell contains newline or has 6+ digit number (pincode) = combined\u001b[39m\n\u001b[31m  const samples = _rawRows.slice(0, 3).map(r => (r[colIdx] || '').toString());\u001b[39m\n\u001b[31m  return samples.some(s => s.includes('\\\\n') || /\\\\b\\\\d{6}\\\\b/.test(s) || s.split(',').length >= 3);\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction renderMappingUI(filename) {\u001b[39m\n\u001b[31m  const saved = (() => { try { return JSON.parse(localStorage.getItem(getMappingKey()) || 'null'); } catch(e) { return null; } })();\u001b[39m\n\u001b[31m  const isSaved = !!saved;\u001b[39m\n\u001b[31m  const colInfoEl = document.getElementById('storeColInfo');\u001b[39m\n\u001b[31m  colInfoEl.style.display = 'block';·\u001b[39m\n\u001b[31m  const suggested = {};\u001b[39m\n\u001b[31m  MAPPING_FIELDS.forEach(f => {\u001b[39m\n\u001b[31m    const idx = findCol(_rawHeaders, f.keywords);\u001b[39m\n\u001b[31m    suggested[f.key] = saved ? (saved[f.key] ?? idx) : idx;\u001b[39m\n\u001b[31m  });·\u001b[39m\n\u001b[31m  // Detect if address column is combined\u001b[39m\n\u001b[31m  const addrIdx = suggested.address >= 0 ? suggested.address : findCol(_rawHeaders, ['address','addr']);\u001b[39m\n\u001b[31m  const looksCombo = isCombinedColumn(addrIdx) || isCombinedColumn(suggested.name);\u001b[39m\n\u001b[31m  const comboCol   = looksCombo ? (addrIdx >= 0 ? addrIdx : suggested.name) : -1;\u001b[39m\n\u001b[31m  const comboPreview = comboCol >= 0 && _rawRows[0] ? (_rawRows[0][comboCol] || '').toString().slice(0, 120) : '';·\u001b[39m\n\u001b[31m  colInfoEl.innerHTML = `\u001b[39m\n\u001b[31m    <div style=\\\"background:var(--sur2);border:1px solid var(--bdr);border-radius:var(--r-sm);padding:12px;\\\">\u001b[39m\n\u001b[31m      <div style=\\\"display:flex;align-items:center;justify-content:space-between;margin-bottom:10px;\\\">\u001b[39m\n\u001b[31m        <div style=\\\"font-family:var(--fnt-mono);font-size:.68rem;color:var(--sky);\\\">\u001b[39m\n\u001b[31m          📋 ${filename} &nbsp;·&nbsp; ${_rawRows.length} rows &nbsp;·&nbsp; ${_rawHeaders.length} columns\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m        ${isSaved ? '<span class=\\\"map-saved-badge\\\">✓ mapping remembered</span>' : ''}\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      <div style=\\\"font-size:.68rem;color:var(--t3);font-family:var(--fnt-mono);margin-bottom:10px;word-break:break-all;\\\">\u001b[39m\n\u001b[31m        Columns: <span style=\\\"color:var(--t2);\\\">${_rawHeaders.map((h,i)=>`[${i+1}] ${h||'?'}`).join(' · ')}</span>\u001b[39m\n\u001b[31m      </div>·\u001b[39m\n\u001b[31m      ${looksCombo ? `\u001b[39m\n\u001b[31m      <!-- COMBINED COLUMN DETECTED -->\u001b[39m\n\u001b[31m      <div style=\\\"background:rgba(56,189,248,.08);border:1px solid rgba(56,189,248,.3);\u001b[39m\n\u001b[31m        border-radius:var(--r-sm);padding:10px 12px;margin-bottom:10px;\\\">\u001b[39m\n\u001b[31m        <div style=\\\"font-size:.72rem;color:var(--sky);font-weight:600;margin-bottom:6px;\\\">\u001b[39m\n\u001b[31m          🤖 Combined Address Column Detected\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m        <div style=\\\"font-size:.68rem;color:var(--t2);font-family:var(--fnt-mono);\u001b[39m\n\u001b[31m          white-space:pre-wrap;background:var(--bg);padding:6px 8px;border-radius:4px;\u001b[39m\n\u001b[31m          margin-bottom:8px;max-height:80px;overflow:hidden;\\\">${comboPreview}</div>\u001b[39m\n\u001b[31m        <div style=\\\"font-size:.7rem;color:var(--t3);margin-bottom:8px;\\\">\u001b[39m\n\u001b[31m          AI will extract: Name · Address · City · Pincode · Phone from each cell.\u001b[39m\n\u001b[31m          Runs in browser (Transformers.js) with Ollama fallback.\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m        <div style=\\\"display:flex;align-items:center;gap:8px;\\\">\u001b[39m\n\u001b[31m          <label style=\\\"display:flex;align-items:center;gap:6px;font-size:.72rem;color:var(--t1);cursor:pointer;\\\">\u001b[39m\n\u001b[31m            <input type=\\\"checkbox\\\" id=\\\"useAIParse\\\" checked style=\\\"cursor:pointer;\\\">\u001b[39m\n\u001b[31m            Use AI to parse combined column\u001b[39m\n\u001b[31m          </label>\u001b[39m\n\u001b[31m          <select id=\\\"comboColSel\\\" style=\\\"background:var(--sur2);border:1px solid var(--bdr);\u001b[39m\n\u001b[31m            color:var(--t1);font-family:var(--fnt-mono);font-size:.72rem;padding:4px 8px;\u001b[39m\n\u001b[31m            border-radius:var(--r-sm);outline:none;\\\">\u001b[39m\n\u001b[31m            ${_rawHeaders.map((h,i)=>`<option value=\\\"${i}\\\" ${i===comboCol?'selected':''}>${h||'(col '+(i+1)+')'}</option>`).join('')}\u001b[39m\n\u001b[31m          </select>\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      ` : ''}·\u001b[39m\n\u001b[31m      <div id=\\\"manualMapSection\\\" style=\\\"${looksCombo ? 'display:none;' : ''}\\\">\u001b[39m\n\u001b[31m        <div style=\\\"font-size:.68rem;color:var(--t3);margin-bottom:6px;\\\">\u001b[39m\n\u001b[31m          ${looksCombo ? 'Manual mapping (used when AI parse is off, or for other columns):' : 'Map each field to your column:'}\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m        <table class=\\\"map-table\\\">\u001b[39m\n\u001b[31m          <thead><tr><th>Field</th><th>Your column</th><th>Preview (row 1)</th></tr></thead>\u001b[39m\n\u001b[31m          <tbody>\u001b[39m\n\u001b[31m            ${MAPPING_FIELDS.map(f => {\u001b[39m\n\u001b[31m              const selIdx = suggested[f.key] ?? -1;\u001b[39m\n\u001b[31m              const preview = selIdx >= 0 && _rawRows[0] ? ((_rawRows[0][selIdx]||'').toString().slice(0,40)) : '—';\u001b[39m\n\u001b[31m              return `<tr>\u001b[39m\n\u001b[31m                <td style=\\\"white-space:nowrap;color:var(--t1);font-weight:500;font-size:.75rem;\\\">\u001b[39m\n\u001b[31m                  ${f.label}${f.required ? '<span class=\\\"map-req\\\">*</span>' : ''}\u001b[39m\n\u001b[31m                </td>\u001b[39m\n\u001b[31m                <td>\u001b[39m\n\u001b[31m                  <select id=\\\"mapsel_${f.key}\\\" onchange=\\\"updateMappingPreview('${f.key}')\\\">\u001b[39m\n\u001b[31m                    ${_rawHeaders.map((h,i)=>\u001b[39m\n\u001b[31m                      `<option value=\\\"${i}\\\" ${i===selIdx?'selected':''}>${h||'(col '+(i+1)+')'}</option>`\u001b[39m\n\u001b[31m                    ).join('')}\u001b[39m\n\u001b[31m                    <option value=\\\"-1\\\" ${selIdx===-1?'selected':''}>— not in file —</option>\u001b[39m\n\u001b[31m                  </select>\u001b[39m\n\u001b[31m                </td>\u001b[39m\n\u001b[31m                <td id=\\\"mapprev_${f.key}\\\" style=\\\"font-family:var(--fnt-mono);font-size:.68rem;\u001b[39m\n\u001b[31m                  color:var(--t2);max-width:90px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;\\\">\u001b[39m\n\u001b[31m                  ${preview}\u001b[39m\n\u001b[31m                </td>\u001b[39m\n\u001b[31m              </tr>`;\u001b[39m\n\u001b[31m            }).join('')}\u001b[39m\n\u001b[31m          </tbody>\u001b[39m\n\u001b[31m        </table>\u001b[39m\n\u001b[31m      </div>·\u001b[39m\n\u001b[31m      <!-- Also map docket/courier no column separately — always shown -->\u001b[39m\n\u001b[31m      <div style=\\\"margin-top:10px;padding-top:10px;border-top:1px solid var(--bdr);\\\">\u001b[39m\n\u001b[31m        <div style=\\\"font-size:.68rem;color:var(--t3);margin-bottom:6px;font-family:var(--fnt-mono);\\\">\u001b[39m\n\u001b[31m          DOCKET / COURIER NO. COLUMN (required for tracking):\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m        <div style=\\\"display:flex;gap:8px;align-items:center;\\\">\u001b[39m\n\u001b[31m          <select id=\\\"docketColSel\\\" style=\\\"background:var(--sur2);border:1px solid var(--bdr);\u001b[39m\n\u001b[31m            color:var(--acc);font-family:var(--fnt-mono);font-size:.78rem;padding:6px 10px;\u001b[39m\n\u001b[31m            border-radius:var(--r-sm);outline:none;flex:1;\\\">\u001b[39m\n\u001b[31m            <option value=\\\"-1\\\">— Docket not in file —</option>\u001b[39m\n\u001b[31m            ${_rawHeaders.map((h,i)=>`<option value=\\\"${i}\\\" ${i===(suggested.docket??-1)?'selected':''}>${h||'(col '+(i+1)+')'}</option>`).join('')}\u001b[39m\n\u001b[31m          </select>\u001b[39m\n\u001b[31m          <span id=\\\"docketPrev\\\" style=\\\"font-family:var(--fnt-mono);font-size:.72rem;color:var(--acc);\\\">\u001b[39m\n\u001b[31m            ${suggested.docket >= 0 && _rawRows[0] ? (_rawRows[0][suggested.docket]||'') : ''}\u001b[39m\n\u001b[31m          </span>\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m      </div>·\u001b[39m\n\u001b[31m      <div class=\\\"btn-row\\\" style=\\\"margin-top:12px;\\\">\u001b[39m\n\u001b[31m        <button class=\\\"btn btn-ghost\\\" style=\\\"font-size:.78rem;\\\" onclick=\\\"clearStores()\\\">✕ Cancel</button>\u001b[39m\n\u001b[31m        <button class=\\\"btn btn-orange\\\" style=\\\"font-size:.78rem;\\\" onclick=\\\"applyMapping()\\\">✓ Apply Mapping</button>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m  `;\u001b[39m\n\u001b[31m  document.getElementById('storeTable').innerHTML = '';\u001b[39m\n\u001b[31m  document.getElementById('storeAddPanel').style.display = 'none';·\u001b[39m\n\u001b[31m  // Update docket preview on change\u001b[39m\n\u001b[31m  const docketSel = document.getElementById('docketColSel');\u001b[39m\n\u001b[31m  if (docketSel) docketSel.addEventListener('change', () => {\u001b[39m\n\u001b[31m    const idx = parseInt(docketSel.value);\u001b[39m\n\u001b[31m    const prev = document.getElementById('docketPrev');\u001b[39m\n\u001b[31m    if (prev) prev.textContent = idx >= 0 && _rawRows[0] ? (_rawRows[0][idx]||'') : '';\u001b[39m\n\u001b[31m  });·\u001b[39m\n\u001b[31m  // Toggle manual map opacity when AI checkbox changes\u001b[39m\n\u001b[31m  const aiChk = document.getElementById('useAIParse');\u001b[39m\n\u001b[31m  const manSec = document.getElementById('manualMapSection');\u001b[39m\n\u001b[31m  if (aiChk && manSec) {\u001b[39m\n\u001b[31m    aiChk.addEventListener('change', () => {\u001b[39m\n\u001b[31m      manSec.style.display = aiChk.checked ? 'none' : 'block';\u001b[39m\n\u001b[31m    });\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction updateMappingPreview(key) {\u001b[39m\n\u001b[31m  const sel = document.getElementById('mapsel_' + key);\u001b[39m\n\u001b[31m  if (!sel) return;\u001b[39m\n\u001b[31m  const idx = parseInt(sel.value);\u001b[39m\n\u001b[31m  const prev = document.getElementById('mapprev_' + key);\u001b[39m\n\u001b[31m  if (prev) prev.textContent = (idx >= 0 && _rawRows[0]) ? ((_rawRows[0][idx]||'').toString().slice(0,40)) : '—';\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// ── REGEX-FIRST ADDRESS PARSER ───────────────────────────────────────────\u001b[39m\n\u001b[31m// Improved Indian address parser\u001b[39m\n\u001b[31m// Handles formats seen in real BTL dispatch files\u001b[39m\n\u001b[31mfunction parseAddressRegex(raw) {\u001b[39m\n\u001b[31m  const text = raw.toString().replace(/\\\\r/g, '').trim();\u001b[39m\n\u001b[31m  const lines = text.split('\\\\n').map(l => l.trim()).filter(Boolean);\u001b[39m\n\u001b[31m  const result = { name:'', address:'', city:'', pincode:'', phone:'', confidence:0 };·\u001b[39m\n\u001b[31m  // ── PHONE: Indian mobile 6-9 prefix, 10 digits ──────────────────────────\u001b[39m\n\u001b[31m  // Also handles \\\"Phone : 9876543210\\\", \\\"M.No.-9876543210\\\", \\\"Mobile - 9876543210\\\"\u001b[39m\n\u001b[31m  const phoneMatch = text.match(/(?:Phone|Mobile|Mob|M\\\\.No\\\\.?|Ph\\\\.?)?\\\\s*[:\\\\-]?\\\\s*\\\\b([6-9]\\\\d{9})\\\\b/i);\u001b[39m\n\u001b[31m  if (phoneMatch) result.phone = phoneMatch[1];·\u001b[39m\n\u001b[31m  // ── PINCODE: 6-digit standalone number ──────────────────────────────────\u001b[39m\n\u001b[31m  // Must not be part of a longer number (e.g. not an account number)\u001b[39m\n\u001b[31m  const pinMatch = text.match(/(?<![\\\\d])([1-9]\\\\d{5})(?![\\\\d])/);\u001b[39m\n\u001b[31m  if (pinMatch) result.pincode = pinMatch[1];·\u001b[39m\n\u001b[31m  // ── CITY: the token immediately before the pincode ───────────────────────\u001b[39m\n\u001b[31m  const indianStates = ['rajasthan','gujarat','maharashtra','punjab','haryana',\u001b[39m\n\u001b[31m    'uttar pradesh','madhya pradesh','karnataka','tamil nadu','andhra pradesh',\u001b[39m\n\u001b[31m    'telangana','kerala','west bengal','bihar','odisha','jharkhand','assam',\u001b[39m\n\u001b[31m    'himachal pradesh','uttarakhand','chhattisgarh','goa','delhi','chandigarh',\u001b[39m\n\u001b[31m    'raj.','raj'];\u001b[39m\n\u001b[31m  if (result.pincode) {\u001b[39m\n\u001b[31m    const pinPos = text.indexOf(result.pincode);\u001b[39m\n\u001b[31m    const beforePin = text.slice(0, pinPos);\u001b[39m\n\u001b[31m    // Strategy 1: split by comma/newline, walk backwards skipping state names\u001b[39m\n\u001b[31m    const chunks = beforePin.split(/[,\\\\n]/).map(c => c.trim()).filter(Boolean);\u001b[39m\n\u001b[31m    for (let i = chunks.length - 1; i >= 0; i--) {\u001b[39m\n\u001b[31m      const chunk = chunks[i].replace(/\\\\d+/g,'').replace(/[()]/g,'').replace(/[.-]/g,'').trim();\u001b[39m\n\u001b[31m      const lower = chunk.toLowerCase();\u001b[39m\n\u001b[31m      if (!chunk || chunk.length < 2) continue;\u001b[39m\n\u001b[31m      if (indianStates.includes(lower)) continue;\u001b[39m\n\u001b[31m      // Skip chunks that look like addresses (contain numbers or long strings)\u001b[39m\n\u001b[31m      if (/\\\\d/.test(chunk)) continue;\u001b[39m\n\u001b[31m      result.city = chunk;\u001b[39m\n\u001b[31m      break;\u001b[39m\n\u001b[31m    }\u001b[39m\n\u001b[31m    // Strategy 2: if still no city, look for last capitalised word before pincode\u001b[39m\n\u001b[31m    // handles \\\"compound paota Jodhpur 342006\\\" → Jodhpur\u001b[39m\n\u001b[31m    if (!result.city) {\u001b[39m\n\u001b[31m      const wordsBefore = beforePin.replace(/[,\\\\n]/g,' ').trim().split(/\\\\s+/);\u001b[39m\n\u001b[31m      for (let i = wordsBefore.length - 1; i >= 0; i--) {\u001b[39m\n\u001b[31m        const w = wordsBefore[i].replace(/[^a-zA-Z]/g,'');\u001b[39m\n\u001b[31m        if (w.length < 3) continue;\u001b[39m\n\u001b[31m        if (indianStates.includes(w.toLowerCase())) continue;\u001b[39m\n\u001b[31m        // Prefer words that start with capital letter (likely a proper noun/city)\u001b[39m\n\u001b[31m        if (/^[A-Z]/.test(w)) { result.city = w; break; }\u001b[39m\n\u001b[31m      }\u001b[39m\n\u001b[31m    }\u001b[39m\n\u001b[31m  }·\u001b[39m\n\u001b[31m  // ── NAME: first meaningful line, extract shop name ───────────────────────\u001b[39m\n\u001b[31m  // Patterns:\u001b[39m\n\u001b[31m  //   \\\"Sanwar Mal Saini, C/o R G COMPUTERS\\\" → name = \\\"R G COMPUTERS\\\"\u001b[39m\n\u001b[31m  //   \\\"SANWAR MAL SAINI, C/o ADVANCE TECHNOLOGIES\\\" → name = \\\"ADVANCE TECHNOLOGIES\\\"\u001b[39m\n\u001b[31m  //   \\\"Sunil Vaishnav\\\" → name = \\\"Sunil Vaishnav\\\" (no C/o)\u001b[39m\n\u001b[31m  //   \\\"Baltej Singh, Professional computer care\\\" → name = \\\"Professional computer care\\\"\u001b[39m\n\u001b[31m  if (lines.length > 0) {\u001b[39m\n\u001b[31m    const firstLine = lines[0];\u001b[39m\n\u001b[31m    // Check for C/o or C/O pattern — shop name comes after it\u001b[39m\n\u001b[31m    const coMatch = firstLine.match(/C\\\\/?[Oo]\\\\.?\\\\s+(.+)/);\u001b[39m\n\u001b[31m    if (coMatch) {\u001b[39m\n\u001b[31m      result.name = coMatch[1].trim();\u001b[39m\n\u001b[31m    } else {\u001b[39m\n\u001b[31m      // Check if there is a comma — second part might be the shop name\u001b[39m\n\u001b[31m      const parts = firstLine.split(',').map(p => p.trim());\u001b[39m\n\u001b[31m      if (parts.length >= 2 && parts[1].length > 3) {\u001b[39m\n\u001b[31m        // Heuristic: if second part looks like a business name (has uppercase or >5 chars)\u001b[39m\n\u001b[31m        const hasUpperOrLong = /[A-Z]{2,}/.test(parts[1]) || parts[1].length > 8;\u001b[39m\n\u001b[31m        result.name = hasUpperOrLong ? parts[1] : parts[0];\u001b[39m\n\u001b[31m      } else {\u001b[39m\n\u001b[31m        result.name = parts[0];\u001b[39m\n\u001b[31m      }\u001b[39m\n\u001b[31m    }\u001b[39m\n\u001b[31m  }·\u001b[39m\n\u001b[31m  // ── ADDRESS: lines between name and phone/pincode line ───────────────────\u001b[39m\n\u001b[31m  const phoneLineIdx = lines.findIndex(l => result.phone && l.includes(result.phone));\u001b[39m\n\u001b[31m  const addrLines = lines.filter((l, i) => {\u001b[39m\n\u001b[31m    if (i === 0) return false;\u001b[39m\n\u001b[31m    if (i === phoneLineIdx) return false;\u001b[39m\n\u001b[31m    // Exclude lines that are just the phone number\u001b[39m\n\u001b[31m    if (/^[6-9]\\\\d{9}$/.test(l.replace(/\\\\s/g,''))) return false;\u001b[39m\n\u001b[31m    return true;\u001b[39m\n\u001b[31m  });\u001b[39m\n\u001b[31m  result.address = addrLines.join(', ').replace(/,\\\\s*,/g, ',').trim();·\u001b[39m\n\u001b[31m  // ── CONFIDENCE: simple score based on what we got ────────────────────────\u001b[39m\n\u001b[31m  let conf = 0;\u001b[39m\n\u001b[31m  if (result.name)    conf += 30;\u001b[39m\n\u001b[31m  if (result.city)    conf += 25;\u001b[39m\n\u001b[31m  if (result.pincode) conf += 25;\u001b[39m\n\u001b[31m  if (result.phone)   conf += 20;\u001b[39m\n\u001b[31m  result.confidence = conf;·\u001b[39m\n\u001b[31m  return result;\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// ── AI ADDRESS PARSER (Ollama fallback) ──────────────────────────────────\u001b[39m\n\u001b[31m// Fetches past corrections from localStorage to improve prompt accuracy\u001b[39m\n\u001b[31mfunction getPastCorrections() {\u001b[39m\n\u001b[31m  try {\u001b[39m\n\u001b[31m    const log = JSON.parse(localStorage.getItem('360lm_parse_corrections') || '[]');\u001b[39m\n\u001b[31m    return log.slice(-10); // last 10 corrections as examples\u001b[39m\n\u001b[31m  } catch(e) { return []; }\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction saveCorrectionLocal(entry) {\u001b[39m\n\u001b[31m  try {\u001b[39m\n\u001b[31m    const log = JSON.parse(localStorage.getItem('360lm_parse_corrections') || '[]');\u001b[39m\n\u001b[31m    log.push(entry);\u001b[39m\n\u001b[31m    // Keep only last 50\u001b[39m\n\u001b[31m    localStorage.setItem('360lm_parse_corrections', JSON.stringify(log.slice(-50)));\u001b[39m\n\u001b[31m  } catch(e) {}\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31masync function parseAddressWithOllama(raw) {\u001b[39m\n\u001b[31m  // Calls the server-side /dispatch-ai/address proxy (avoids CORS + port 11434 issues)\u001b[39m\n\u001b[31m  try {\u001b[39m\n\u001b[31m    const resp = await fetch('/dispatch-ai/address', {\u001b[39m\n\u001b[31m      method: 'POST',\u001b[39m\n\u001b[31m      headers: { 'Content-Type': 'application/json' },\u001b[39m\n\u001b[31m      body: JSON.stringify({ text: raw }),\u001b[39m\n\u001b[31m      signal: AbortSignal.timeout(120000)\u001b[39m\n\u001b[31m    });\u001b[39m\n\u001b[31m    const data = await resp.json();\u001b[39m\n\u001b[31m    if (data.success) return data;\u001b[39m\n\u001b[31m  } catch(e) {}\u001b[39m\n\u001b[31m  return null;\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31masync function parseAddressCell(raw) {\u001b[39m\n\u001b[31m  // Step 1: regex — always runs first, no network call\u001b[39m\n\u001b[31m  const regex = parseAddressRegex(raw);\u001b[39m\n\u001b[31m  if (regex.name) return { ...regex, source: 'regex' };·\u001b[39m\n\u001b[31m  // Step 2: Ollama only if regex got nothing AND Ollama is configured\u001b[39m\n\u001b[31m  // Uses a 3-second timeout to prevent browser hang\u001b[39m\n\u001b[31m  if (window.appConfig.ocr_mode !== 'transformers') {\u001b[39m\n\u001b[31m    try {\u001b[39m\n\u001b[31m      const ai = await Promise.race([\u001b[39m\n\u001b[31m        parseAddressWithOllama(raw),\u001b[39m\n\u001b[31m        new Promise((_, reject) => setTimeout(() => reject(new Error('timeout')), 120000))\u001b[39m\n\u001b[31m      ]);\u001b[39m\n\u001b[31m      if (ai && ai.name) return { ...ai, source: 'ollama' };\u001b[39m\n\u001b[31m    } catch(e) { /* timeout or error — continue to fallback */ }\u001b[39m\n\u001b[31m  }·\u001b[39m\n\u001b[31m  // Step 3: fallback — first line as name\u001b[39m\n\u001b[31m  const lines = raw.toString().split('\\\\n').map(l=>l.trim()).filter(Boolean);\u001b[39m\n\u001b[31m  return { name: lines[0]||raw.slice(0,40), address: raw, city:'', pincode:'', phone:'', source:'fallback' };\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31masync function applyMapping() {\u001b[39m\n\u001b[31m  const useAI   = document.getElementById('useAIParse')?.checked ?? false;\u001b[39m\n\u001b[31m  const comboSel = document.getElementById('comboColSel');\u001b[39m\n\u001b[31m  const comboIdx = comboSel ? parseInt(comboSel.value) : -1;\u001b[39m\n\u001b[31m  const docketIdx = parseInt(document.getElementById('docketColSel')?.value ?? -1);·\u001b[39m\n\u001b[31m  // Build manual mapping as fallback\u001b[39m\n\u001b[31m  const mapping = {};\u001b[39m\n\u001b[31m  MAPPING_FIELDS.forEach(f => {\u001b[39m\n\u001b[31m    const sel = document.getElementById('mapsel_' + f.key);\u001b[39m\n\u001b[31m    mapping[f.key] = sel ? parseInt(sel.value) : -1;\u001b[39m\n\u001b[31m  });\u001b[39m\n\u001b[31m  // Override docket from dedicated selector\u001b[39m\n\u001b[31m  mapping.docket = docketIdx;·\u001b[39m\n\u001b[31m  // Determine if we have a valid base for stores\u001b[39m\n\u001b[31m  const nameIdx = useAI && comboIdx >= 0 ? comboIdx : mapping.name;\u001b[39m\n\u001b[31m  if (nameIdx === -1 && !useAI) { showToast('Store Name column is required', 'err'); return; }·\u001b[39m\n\u001b[31m  // Save mapping\u001b[39m\n\u001b[31m  try { localStorage.setItem(getMappingKey(), JSON.stringify({...mapping, _useAI: useAI, _comboIdx: comboIdx})); } catch(e) {}·\u001b[39m\n\u001b[31m  if (useAI && comboIdx >= 0) {\u001b[39m\n\u001b[31m    // AI path — parse each combined cell\u001b[39m\n\u001b[31m    showToast('AI parsing addresses...', 'info');\u001b[39m\n\u001b[31m    showOCRWorking(true, `Parsing 0/${_rawRows.length} addresses...`);\u001b[39m\n\u001b[31m    stores = [];\u001b[39m\n\u001b[31m    for (let i = 0; i < _rawRows.length; i++) {\u001b[39m\n\u001b[31m      const r = _rawRows[i];\u001b[39m\n\u001b[31m      const raw = (r[comboIdx] || '').toString().trim();\u001b[39m\n\u001b[31m      if (!raw) continue;\u001b[39m\n\u001b[31m      showOCRWorking(true, `Parsing ${i+1}/${_rawRows.length} — ${raw.slice(0,30)}...`);\u001b[39m\n\u001b[31m      // Yield to browser every row to prevent UI freeze\u001b[39m\n\u001b[31m      await new Promise(r => setTimeout(r, 0));\u001b[39m\n\u001b[31m      const parsed = await parseAddressCell(raw);\u001b[39m\n\u001b[31m      stores.push({\u001b[39m\n\u001b[31m        id: i,\u001b[39m\n\u001b[31m        name:    parsed.name    || raw.slice(0,40),\u001b[39m\n\u001b[31m        city:    parsed.city    || '',\u001b[39m\n\u001b[31m        phone:   parsed.phone   || '',\u001b[39m\n\u001b[31m        address: parsed.address || raw,\u001b[39m\n\u001b[31m        pincode: parsed.pincode || '',\u001b[39m\n\u001b[31m        docket:  docketIdx >= 0 ? (r[docketIdx]||'').toString().trim() : '',\u001b[39m\n\u001b[31m        _parseSource: parsed.source,\u001b[39m\n\u001b[31m        _confidence:  parsed.confidence || 0,\u001b[39m\n\u001b[31m        _rawCell:     raw,\u001b[39m\n\u001b[31m        _origName:    parsed.name || '',\u001b[39m\n\u001b[31m        _origCity:    parsed.city || '',\u001b[39m\n\u001b[31m        matched: false\u001b[39m\n\u001b[31m      });\u001b[39m\n\u001b[31m    }\u001b[39m\n\u001b[31m    showOCRWorking(false);\u001b[39m\n\u001b[31m  } else {\u001b[39m\n\u001b[31m    // Manual mapping path\u001b[39m\n\u001b[31m    stores = _rawRows\u001b[39m\n\u001b[31m      .filter(r => r[nameIdx] && (r[nameIdx]+'').trim())\u001b[39m\n\u001b[31m      .map((r, i) => ({\u001b[39m\n\u001b[31m        id: i,\u001b[39m\n\u001b[31m        name:    mapping.name    >= 0 ? (r[mapping.name]   ||'').toString().trim() : '',\u001b[39m\n\u001b[31m        city:    mapping.city    >= 0 ? (r[mapping.city]   ||'').toString().trim() : '',\u001b[39m\n\u001b[31m        phone:   mapping.phone   >= 0 ? (r[mapping.phone]  ||'').toString().trim() : '',\u001b[39m\n\u001b[31m        docket:  mapping.docket  >= 0 ? (r[mapping.docket] ||'').toString().trim() : '',\u001b[39m\n\u001b[31m        address: mapping.address >= 0 ? (r[mapping.address]||'').toString().trim() : '',\u001b[39m\n\u001b[31m        pincode: mapping.pincode >= 0 ? (r[mapping.pincode]||'').toString().trim() : '',\u001b[39m\n\u001b[31m        matched: false\u001b[39m\n\u001b[31m      }));\u001b[39m\n\u001b[31m  }·\u001b[39m\n\u001b[31m  document.getElementById('storeCount').textContent = `${stores.length} stores loaded`;·\u001b[39m\n\u001b[31m  if (useAI && comboIdx >= 0) {\u001b[39m\n\u001b[31m    // AI path — show row-by-row review panel before adding consignees\u001b[39m\n\u001b[31m    showAddressReviewPanel();\u001b[39m\n\u001b[31m  } else {\u001b[39m\n\u001b[31m    // Manual mapping path — go straight to store table + add panel\u001b[39m\n\u001b[31m    renderStoreTable(mapping.docket >= 0 || docketIdx >= 0);\u001b[39m\n\u001b[31m    const addPanel = document.getElementById('storeAddPanel');\u001b[39m\n\u001b[31m    const addBtn   = document.getElementById('storeAddBtn');\u001b[39m\n\u001b[31m    const addTitle = document.getElementById('storeAddTitle');\u001b[39m\n\u001b[31m    const hasDockets = stores.some(s => s.docket);\u001b[39m\n\u001b[31m    addTitle.textContent = hasDockets\u001b[39m\n\u001b[31m      ? `${stores.filter(s=>s.docket).length} stores with dockets — add as consignees?`\u001b[39m\n\u001b[31m      : `Add ${stores.length} stores as consignees?`;\u001b[39m\n\u001b[31m    addBtn.textContent = `✓ Add ${stores.length} as Consignees`;\u001b[39m\n\u001b[31m    addPanel.style.display = 'block';\u001b[39m\n\u001b[31m    showToast(`${stores.length} stores mapped ✓`, 'ok');\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31m// ADDRESS REVIEW PANEL\u001b[39m\n\u001b[31m// Row-by-row review of AI-parsed addresses before consignees created\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31mfunction showAddressReviewPanel() {\u001b[39m\n\u001b[31m  const pane = document.getElementById('addrReviewPane');\u001b[39m\n\u001b[31m  if (!pane) return;\u001b[39m\n\u001b[31m  renderAddressReviewRows();\u001b[39m\n\u001b[31m  pane.style.display = 'block';\u001b[39m\n\u001b[31m  pane.scrollTop = 0;\u001b[39m\n\u001b[31m  showToast(`Review ${stores.length} parsed addresses`, 'info');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction renderAddressReviewRows() {\u001b[39m\n\u001b[31m  const list = document.getElementById('addrReviewList');\u001b[39m\n\u001b[31m  const summary = document.getElementById('addrReviewSummary');\u001b[39m\n\u001b[31m  if (!list) return;·\u001b[39m\n\u001b[31m  const highConf  = stores.filter(s => (s._confidence||0) >= 80).length;\u001b[39m\n\u001b[31m  const medConf   = stores.filter(s => (s._confidence||0) >= 50 && (s._confidence||0) < 80).length;\u001b[39m\n\u001b[31m  const lowConf   = stores.filter(s => (s._confidence||0) < 50).length;·\u001b[39m\n\u001b[31m  summary.innerHTML = `\u001b[39m\n\u001b[31m    <div class=\\\"vs-item\\\"><div class=\\\"vs-val\\\" style=\\\"color:var(--t1);\\\">${stores.length}</div><div class=\\\"vs-lbl\\\">Total</div></div>\u001b[39m\n\u001b[31m    <div class=\\\"vs-item\\\"><div class=\\\"vs-val\\\" style=\\\"color:var(--grn);\\\">${highConf}</div><div class=\\\"vs-lbl\\\">High conf</div></div>\u001b[39m\n\u001b[31m    <div class=\\\"vs-item\\\"><div class=\\\"vs-val\\\" style=\\\"color:var(--ylw);\\\">${medConf}</div><div class=\\\"vs-lbl\\\">Review</div></div>\u001b[39m\n\u001b[31m    <div class=\\\"vs-item\\\"><div class=\\\"vs-val\\\" style=\\\"color:var(--red);\\\">${lowConf}</div><div class=\\\"vs-lbl\\\">Check</div></div>\u001b[39m\n\u001b[31m    <div class=\\\"vs-item\\\" style=\\\"margin-left:auto;\\\">\u001b[39m\n\u001b[31m      <button class=\\\"btn-sm primary\\\" onclick=\\\"approveAllAddresses()\\\" style=\\\"font-size:.7rem;\\\">✓ Approve All</button>\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m  `;·\u001b[39m\n\u001b[31m  list.innerHTML = stores.map((s, i) => {\u001b[39m\n\u001b[31m    const conf = s._confidence || 0;\u001b[39m\n\u001b[31m    const confClass = conf >= 80 ? 'vf-ok' : conf >= 50 ? 'vf-nodocket' : 'vf-dupe';\u001b[39m\n\u001b[31m    const confLabel = conf >= 80 ? `${conf}% ✓` : conf >= 50 ? `${conf}% review` : `${conf}% check`;\u001b[39m\n\u001b[31m    const rowBorder = conf >= 80 ? 'var(--grn)' : conf >= 50 ? 'var(--ylw)' : 'var(--red)';·\u001b[39m\n\u001b[31m    return `\u001b[39m\n\u001b[31m    <div class=\\\"verify-row\\\" id=\\\"arow_${i}\\\" style=\\\"border-left:3px solid ${rowBorder};\\\">\u001b[39m\n\u001b[31m      <div class=\\\"verify-row-hdr\\\">\u001b[39m\n\u001b[31m        <span class=\\\"verify-num\\\">#${i+1}</span>\u001b[39m\n\u001b[31m        <span class=\\\"verify-store\\\" style=\\\"font-size:.78rem;color:var(--t2);font-weight:400;\u001b[39m\n\u001b[31m          font-family:var(--fnt-mono);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;flex:1;\\\"\u001b[39m\n\u001b[31m          title=\\\"${(s._rawCell||'').replace(/\\\"/g,\\\"'\\\")}\\\">\u001b[39m\n\u001b[31m          ${(s._rawCell||'').slice(0,50).split('\\\\n').join(' ↵ ')}       <span class=\\\"verify-flag ${confClass}\\\" style=\\\"flex-shrink:0;margin-right:6px;\\\">${confLabel} · ${s._parseSource||'?'}</span>\u001b[39m\n\u001b[31m        <button onclick=\\\"deleteReviewRow(${i})\\\"\u001b[39m\n\u001b[31m          style=\\\"background:none;border:1px solid rgba(239,68,68,.4);color:var(--red);\u001b[39m\n\u001b[31m          font-size:.65rem;padding:2px 8px;border-radius:6px;cursor:pointer;\u001b[39m\n\u001b[31m          font-family:var(--fnt-mono);flex-shrink:0;transition:background .2s;\\\"\u001b[39m\n\u001b[31m          onmouseover=\\\"this.style.background='rgba(239,68,68,.15)'\\\"\u001b[39m\n\u001b[31m          onmouseout=\\\"this.style.background='none'\\\">✕ Remove</button>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      <div class=\\\"verify-fields\\\" style=\\\"grid-template-columns:1fr 1fr 1fr 1fr 1fr;\\\">\u001b[39m\n\u001b[31m        <div class=\\\"verify-field\\\">\u001b[39m\n\u001b[31m          <label>Shop / Name <span style=\\\"color:var(--acc);\\\">*</span></label>\u001b[39m\n\u001b[31m          <input type=\\\"text\\\" value=\\\"${s.name||''}\\\" id=\\\"ar_name_${i}\\\"\u001b[39m\n\u001b[31m            oninput=\\\"updateAddrField(${i},'name',this)\\\"\u001b[39m\n\u001b[31m            style=\\\"${!s.name ? 'border-color:var(--red);' : ''}\\\">\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m        <div class=\\\"verify-field\\\">\u001b[39m\n\u001b[31m          <label>City</label>\u001b[39m\n\u001b[31m          <input type=\\\"text\\\" value=\\\"${s.city||''}\\\" id=\\\"ar_city_${i}\\\"\u001b[39m\n\u001b[31m            oninput=\\\"updateAddrField(${i},'city',this)\\\"\u001b[39m\n\u001b[31m            style=\\\"${!s.city ? 'border-color:var(--ylw);' : ''}\\\">\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m        <div class=\\\"verify-field\\\">\u001b[39m\n\u001b[31m          <label>Docket / Courier No.</label>\u001b[39m\n\u001b[31m          <input type=\\\"text\\\" value=\\\"${s.docket||''}\\\" id=\\\"ar_docket_${i}\\\"\u001b[39m\n\u001b[31m            oninput=\\\"updateAddrField(${i},'docket',this)\\\"\u001b[39m\n\u001b[31m            style=\\\"font-family:var(--fnt-mono);${!s.docket?'border-color:var(--ylw);':'color:var(--acc);'}\\\">\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m        <div class=\\\"verify-field\\\">\u001b[39m\n\u001b[31m          <label>Phone</label>\u001b[39m\n\u001b[31m          <input type=\\\"tel\\\" value=\\\"${s.phone||''}\\\" id=\\\"ar_phone_${i}\\\"\u001b[39m\n\u001b[31m            oninput=\\\"updateAddrField(${i},'phone',this)\\\">\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m        <div class=\\\"verify-field\\\">\u001b[39m\n\u001b[31m          <label>Pincode</label>\u001b[39m\n\u001b[31m          <input type=\\\"text\\\" value=\\\"${s.pincode||''}\\\" id=\\\"ar_pin_${i}\\\"\u001b[39m\n\u001b[31m            oninput=\\\"updateAddrField(${i},'pincode',this)\\\">\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      <div style=\\\"font-size:.65rem;color:var(--t3);font-family:var(--fnt-mono);\u001b[39m\n\u001b[31m        margin-top:4px;padding:4px 6px;background:var(--bg);border-radius:4px;word-break:break-all;\\\">\u001b[39m\n\u001b[31m        📋 ${(s._rawCell||'').split('\\\\n').join(' | ')}\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m    </div>`;\u001b[39m\n\u001b[31m  }).join('');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction updateAddrField(i, field, input) {\u001b[39m\n\u001b[31m  if (!stores[i]) return;\u001b[39m\n\u001b[31m  stores[i][field] = input.value.trim();\u001b[39m\n\u001b[31m  // Recompute confidence\u001b[39m\n\u001b[31m  let conf = 0;\u001b[39m\n\u001b[31m  if (stores[i].name)    conf += 30;\u001b[39m\n\u001b[31m  if (stores[i].city)    conf += 25;\u001b[39m\n\u001b[31m  if (stores[i].pincode) conf += 25;\u001b[39m\n\u001b[31m  if (stores[i].phone)   conf += 20;\u001b[39m\n\u001b[31m  stores[i]._confidence = conf;\u001b[39m\n\u001b[31m  input.classList.toggle('changed', true);\u001b[39m\n\u001b[31m  // Update border color on row\u001b[39m\n\u001b[31m  const row = document.getElementById('arow_' + i);\u001b[39m\n\u001b[31m  if (row) row.style.borderLeftColor = conf >= 80 ? 'var(--grn)' : conf >= 50 ? 'var(--ylw)' : 'var(--red)';\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction deleteReviewRow(i) {\u001b[39m\n\u001b[31m  // Mark as deleted — splice from stores array and re-render\u001b[39m\n\u001b[31m  stores.splice(i, 1);\u001b[39m\n\u001b[31m  // Re-assign IDs\u001b[39m\n\u001b[31m  stores.forEach((s, idx) => { s.id = idx; });\u001b[39m\n\u001b[31m  renderAddressReviewRows();\u001b[39m\n\u001b[31m  document.getElementById('storeCount').textContent = `${stores.length} stores loaded`;\u001b[39m\n\u001b[31m  showToast(`Row removed — ${stores.length} remaining`, 'info');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction backToMapping() {\u001b[39m\n\u001b[31m  // Hide review pane — store edits are preserved in stores[] array\u001b[39m\n\u001b[31m  document.getElementById('addrReviewPane').style.display = 'none';\u001b[39m\n\u001b[31m  // Show mapping UI again with current file\u001b[39m\n\u001b[31m  document.getElementById('storeTableArea').style.display = 'block';\u001b[39m\n\u001b[31m  document.getElementById('storeColInfo').style.display = 'block';\u001b[39m\n\u001b[31m  showToast('Your edits are preserved — re-apply mapping to reload', 'info');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction approveAllAddresses() {\u001b[39m\n\u001b[31m  stores.forEach((s, i) => { s._reviewed = true; });\u001b[39m\n\u001b[31m  document.getElementById('addrReviewPane').style.display = 'none';\u001b[39m\n\u001b[31m  applyReviewedStores();\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction confirmAddressReview() {\u001b[39m\n\u001b[31m  // Collect any pending edits, log corrections, close panel\u001b[39m\n\u001b[31m  const corrections = [];\u001b[39m\n\u001b[31m  stores.forEach((s, i) => {\u001b[39m\n\u001b[31m    const origName = s._origName || '';\u001b[39m\n\u001b[31m    const origCity = s._origCity || '';\u001b[39m\n\u001b[31m    if ((origName && origName !== s.name) || (origCity && origCity !== s.city)) {\u001b[39m\n\u001b[31m      corrections.push({ raw: s._rawCell||'', name: s.name, city: s.city, pincode: s.pincode });\u001b[39m\n\u001b[31m    }\u001b[39m\n\u001b[31m  });\u001b[39m\n\u001b[31m  // Save corrections locally for future Ollama prompts\u001b[39m\n\u001b[31m  corrections.forEach(c => saveCorrectionLocal(c));\u001b[39m\n\u001b[31m  // Log to Sheets asynchronously (fire and forget)\u001b[39m\n\u001b[31m  if (corrections.length > 0 && window.appConfig.webhook_url) {\u001b[39m\n\u001b[31m    logParseCorrectionsToSheets(corrections);\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m  document.getElementById('addrReviewPane').style.display = 'none';\u001b[39m\n\u001b[31m  applyReviewedStores();\u001b[39m\n\u001b[31m  showToast(`${corrections.length > 0 ? corrections.length + ' corrections logged · ' : ''}${stores.length} stores ready ✓`, 'ok');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// Clean street address: remove city, pincode, phone if they appear in address field\u001b[39m\n\u001b[31mfunction cleanAddress(addr, city, pincode, phone) {\u001b[39m\n\u001b[31m  if (!addr) return addr;\u001b[39m\n\u001b[31m  let s = addr;\u001b[39m\n\u001b[31m  // Remove phone\u001b[39m\n\u001b[31m  if (phone) s = s.replace(new RegExp(phone.replace(/[.*+?^${}()|[\\\\]\\\\\\\\]/g,'\\\\\\\\$&'), 'g'), '').trim();\u001b[39m\n\u001b[31m  // Remove pincode\u001b[39m\n\u001b[31m  if (pincode) s = s.replace(new RegExp('\\\\\\\\b' + pincode + '\\\\\\\\b', 'g'), '').trim();\u001b[39m\n\u001b[31m  // Remove city (case-insensitive)\u001b[39m\n\u001b[31m  if (city) s = s.replace(new RegExp('\\\\\\\\b' + city.replace(/[.*+?^${}()|[\\\\]\\\\\\\\]/g,'\\\\\\\\$&') + '\\\\\\\\b', 'ig'), '').trim();\u001b[39m\n\u001b[31m  // Remove state names and trailing noise\u001b[39m\n\u001b[31m  const noise = /\\\\b(rajasthan|gujarat|maharashtra|punjab|haryana|uttar pradesh|delhi|raj\\\\.)\\\\b/ig;\u001b[39m\n\u001b[31m  s = s.replace(noise, '');\u001b[39m\n\u001b[31m  // Remove \\\"Mobile :\\\", \\\"Phone :\\\", \\\"M.No.\\\" prefixes and dangling numbers\u001b[39m\n\u001b[31m  s = s.replace(/(?:Mobile|Phone|Mob|M\\\\.No\\\\.?|Ph\\\\.?)\\\\s*[:\\\\-]?\\\\s*/ig, '');\u001b[39m\n\u001b[31m  // Clean up multiple commas/spaces\u001b[39m\n\u001b[31m  s = s.replace(/,\\\\s*,/g, ',').replace(/\\\\s{2,}/g, ' ').replace(/^[\\\\s,]+|[\\\\s,]+$/g, '');\u001b[39m\n\u001b[31m  return s;\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction applyReviewedStores() {\u001b[39m\n\u001b[31m  // Clean address fields before loading stores\u001b[39m\n\u001b[31m  stores.forEach(s => {\u001b[39m\n\u001b[31m    s.address = cleanAddress(s.address, s.city, s.pincode, s.phone);\u001b[39m\n\u001b[31m  });\u001b[39m\n\u001b[31m  renderStoreTable(stores.some(s => s.docket));\u001b[39m\n\u001b[31m  const addPanel = document.getElementById('storeAddPanel');\u001b[39m\n\u001b[31m  const addTitle = document.getElementById('storeAddTitle');\u001b[39m\n\u001b[31m  const addBtn   = document.getElementById('storeAddBtn');\u001b[39m\n\u001b[31m  const hasDockets = stores.some(s => s.docket);\u001b[39m\n\u001b[31m  addTitle.textContent = hasDockets\u001b[39m\n\u001b[31m    ? `${stores.filter(s=>s.docket).length} stores with dockets — add as consignees?`\u001b[39m\n\u001b[31m    : `Add ${stores.length} stores as consignees?`;\u001b[39m\n\u001b[31m  addBtn.textContent = `✓ Add ${stores.length} as Consignees`;\u001b[39m\n\u001b[31m  addPanel.style.display = 'block';\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31masync function logParseCorrectionsToSheets(corrections) {\u001b[39m\n\u001b[31m  const url = window.appConfig.webhook_url;\u001b[39m\n\u001b[31m  if (!url) return;\u001b[39m\n\u001b[31m  try {\u001b[39m\n\u001b[31m    await fetch(url, {\u001b[39m\n\u001b[31m      method: 'POST', mode: 'no-cors',\u001b[39m\n\u001b[31m      headers: { 'Content-Type': 'application/json' },\u001b[39m\n\u001b[31m      body: JSON.stringify({\u001b[39m\n\u001b[31m        type: 'parse_correction',\u001b[39m\n\u001b[31m        corrections,\u001b[39m\n\u001b[31m        colFingerprint: getMappingKey(),\u001b[39m\n\u001b[31m        timestamp: new Date().toISOString()\u001b[39m\n\u001b[31m      })\u001b[39m\n\u001b[31m    });\u001b[39m\n\u001b[31m  } catch(e) {}\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction addStoresToConsignees() {\u001b[39m\n\u001b[31m  const consList = document.getElementById('consList');\u001b[39m\n\u001b[31m  consList.innerHTML = '';\u001b[39m\n\u001b[31m  stores.slice(0, 50).forEach(s => addConsignee(s));\u001b[39m\n\u001b[31m  document.getElementById('storeAddPanel').style.display = 'none';\u001b[39m\n\u001b[31m  showToast(`${Math.min(stores.length, 50)} consignees added ✓`, 'ok');\u001b[39m\n\u001b[31m  if (stores.some(s => s.docket)) setTimeout(() => autoCreateDispatchFromStoreList(), 600);\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction promptAddFromStores() { /* replaced by inline panel */ }··\u001b[39m\n\u001b[31mfunction findCol(headers, keywords) {\u001b[39m\n\u001b[31m  for (let i=0;i<headers.length;i++) {\u001b[39m\n\u001b[31m    const h = (headers[i]||'').toString().toLowerCase();\u001b[39m\n\u001b[31m    if (keywords.some(k => h.includes(k))) return i;\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m  return -1;\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction renderStoreTable(hasDocket) {\u001b[39m\n\u001b[31m  const t = document.getElementById('storeTable');\u001b[39m\n\u001b[31m  const cols = ['#','Store Name','City','Phone'];\u001b[39m\n\u001b[31m  if (hasDocket) cols.push('Docket','Status');\u001b[39m\n\u001b[31m  t.innerHTML = `<thead><tr>${cols.map(c=>`<th>${c}</th>`).join('')}</tr></thead>\u001b[39m\n\u001b[31m  <tbody>${stores.map((s,i) => {\u001b[39m\n\u001b[31m    const statusClass = s.docket ? (s.status === 'delivered' ? 'matched' : 'unmatched') : '';\u001b[39m\n\u001b[31m    const docketCell = hasDocket ? `<td class=\\\"${statusClass}\\\" style=\\\"font-family:var(--fnt-mono);\\\">${s.docket||'—'}</td>` : '';\u001b[39m\n\u001b[31m    const statusCell = hasDocket ? `<td>${s.status ? `<span class=\\\"status-pill sp-${s.status}\\\">${s.status}</span>` : '—'}</td>` : '';\u001b[39m\n\u001b[31m    return `<tr>\u001b[39m\n\u001b[31m      <td>${i+1}</td>\u001b[39m\n\u001b[31m      <td>${s.name}</td>\u001b[39m\n\u001b[31m      <td>${s.city}</td>\u001b[39m\n\u001b[31m      <td style=\\\"font-family:var(--fnt-mono);\\\">${s.phone||'—'}</td>\u001b[39m\n\u001b[31m      ${docketCell}${statusCell}\u001b[39m\n\u001b[31m    </tr>`;\u001b[39m\n\u001b[31m  }).join('')}</tbody>`;\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction clearStores() {\u001b[39m\n\u001b[31m  stores = [];\u001b[39m\n\u001b[31m  document.getElementById('storeTableArea').style.display = 'none';\u001b[39m\n\u001b[31m  document.getElementById('storeFile').value = '';\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction autoCreateDispatchFromStoreList() {\u001b[39m\n\u001b[31m  // Create/update dispatch with all stores that have dockets\u001b[39m\n\u001b[31m  const courier = document.getElementById('n_courier').value || 'SkyKing';\u001b[39m\n\u001b[31m  const storesWithDockets = stores.filter(s => s.docket);\u001b[39m\n\u001b[31m  if (!storesWithDockets.length) return;·\u001b[39m\n\u001b[31m  // Clear and rebuild consignee list from store data\u001b[39m\n\u001b[31m  const consList = document.getElementById('consList');\u001b[39m\n\u001b[31m  consList.innerHTML = '';\u001b[39m\n\u001b[31m  storesWithDockets.forEach(s => {\u001b[39m\n\u001b[31m    addConsignee(s);\u001b[39m\n\u001b[31m  });\u001b[39m\n\u001b[31m  showPane('new');\u001b[39m\n\u001b[31m  showToast(`${storesWithDockets.length} consignees added from store list`, 'ok');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction promptAddFromStores() {\u001b[39m\n\u001b[31m  // Replaced by inline storeAddPanel UI — no-op kept for safety\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction autoMatchDockets() {\u001b[39m\n\u001b[31m  // Match OCR dockets to stores by order\u001b[39m\n\u001b[31m  if (!slipOCRData.length) { showToast('Upload docket slip images first', 'err'); return; }\u001b[39m\n\u001b[31m  let matched = 0;\u001b[39m\n\u001b[31m  slipOCRData.forEach((slip, i) => {\u001b[39m\n\u001b[31m    if (i < stores.length && slip.docket) {\u001b[39m\n\u001b[31m      stores[i].docket = slip.docket;\u001b[39m\n\u001b[31m      matched++;\u001b[39m\n\u001b[31m    }\u001b[39m\n\u001b[31m  });\u001b[39m\n\u001b[31m  renderStoreTable(true);\u001b[39m\n\u001b[31m  showToast(`${matched} dockets matched to stores ✓`, 'ok');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31m// CONSIGNEES\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31mfunction addConsignee(storeData = null) {\u001b[39m\n\u001b[31m  const list = document.getElementById('consList');\u001b[39m\n\u001b[31m  const idx = list.children.length;\u001b[39m\n\u001b[31m  const card = document.createElement('div');\u001b[39m\n\u001b[31m  card.className = 'cons-card';\u001b[39m\n\u001b[31m  card.dataset.idx = idx;\u001b[39m\n\u001b[31m  const s = storeData || {};\u001b[39m\n\u001b[31m  card.innerHTML = `\u001b[39m\n\u001b[31m    <div class=\\\"cons-hdr\\\">\u001b[39m\n\u001b[31m      <span class=\\\"cons-num\\\">CONSIGNEE #${idx+1}</span>\u001b[39m\n\u001b[31m      <button class=\\\"cons-del\\\" onclick=\\\"this.closest('.cons-card').remove();renumberCons()\\\">✕</button>\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m    <div class=\\\"row2\\\">\u001b[39m\n\u001b[31m      <div class=\\\"f\\\">\u001b[39m\n\u001b[31m        <label>Store / Name <span class=\\\"req\\\">*</span></label>\u001b[39m\n\u001b[31m        <input type=\\\"text\\\" class=\\\"cons-name\\\" placeholder=\\\"Store name\\\" value=\\\"${s.name||''}\\\">\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      <div class=\\\"f\\\">\u001b[39m\n\u001b[31m        <label>City <span class=\\\"req\\\">*</span></label>\u001b[39m\n\u001b[31m        <input type=\\\"text\\\" class=\\\"cons-city\\\" placeholder=\\\"City\\\" value=\\\"${s.city||''}\\\">\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m    <div class=\\\"f\\\">\u001b[39m\n\u001b[31m      <label>Address</label>\u001b[39m\n\u001b[31m      <input type=\\\"text\\\" class=\\\"cons-addr\\\" placeholder=\\\"Full address\\\" value=\\\"${s.address||''}\\\">\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m    <div class=\\\"row2\\\">\u001b[39m\n\u001b[31m      <div class=\\\"f\\\">\u001b[39m\n\u001b[31m        <label>Phone</label>\u001b[39m\n\u001b[31m        <input type=\\\"tel\\\" class=\\\"cons-phone\\\" placeholder=\\\"Mobile\\\" value=\\\"${s.phone||''}\\\">\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      <div class=\\\"f\\\">\u001b[39m\n\u001b[31m        <label>Pincode</label>\u001b[39m\n\u001b[31m        <input type=\\\"text\\\" class=\\\"cons-pin\\\" placeholder=\\\"6-digit\\\" maxlength=\\\"6\\\">\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m    <div class=\\\"f\\\">\u001b[39m\n\u001b[31m      <label>Material / Contents <span class=\\\"req\\\">*</span></label>\u001b[39m\n\u001b[31m      <input type=\\\"text\\\" class=\\\"cons-material\\\" placeholder=\\\"e.g. Standees, Posters\\\">\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m    <div class=\\\"row2\\\">\u001b[39m\n\u001b[31m      <div class=\\\"f\\\">\u001b[39m\n\u001b[31m        <label>Qty</label>\u001b[39m\n\u001b[31m        <input type=\\\"number\\\" class=\\\"cons-qty\\\" placeholder=\\\"0\\\" min=\\\"0\\\">\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      <div class=\\\"f\\\">\u001b[39m\n\u001b[31m        <label>Docket No.</label>\u001b[39m\n\u001b[31m        <input type=\\\"text\\\" class=\\\"cons-docket-input\\\" placeholder=\\\"Docket #\\\"\u001b[39m\n\u001b[31m          style=\\\"font-family:var(--fnt-mono);\\\" value=\\\"${s.docket||''}\\\"\u001b[39m\n\u001b[31m          oninput=\\\"onDocketInput(this)\\\">\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m    ${s.docket ? `<div class=\\\"cons-docket-linked\\\"><span>🔗</span><span class=\\\"dkt\\\">${s.docket}</span></div>` : ''}\u001b[39m\n\u001b[31m  `;\u001b[39m\n\u001b[31m  list.appendChild(card);\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction onDocketInput(input) {\u001b[39m\n\u001b[31m  const card = input.closest('.cons-card');\u001b[39m\n\u001b[31m  let linked = card.querySelector('.cons-docket-linked');\u001b[39m\n\u001b[31m  if (input.value.trim()) {\u001b[39m\n\u001b[31m    if (!linked) {\u001b[39m\n\u001b[31m      linked = document.createElement('div');\u001b[39m\n\u001b[31m      linked.className = 'cons-docket-linked';\u001b[39m\n\u001b[31m      card.appendChild(linked);\u001b[39m\n\u001b[31m    }\u001b[39m\n\u001b[31m    linked.innerHTML = `<span>🔗</span><span class=\\\"dkt\\\">${input.value}</span>`;\u001b[39m\n\u001b[31m  } else if (linked) {\u001b[39m\n\u001b[31m    linked.remove();\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction showLinkedDocket(card, docket) {\u001b[39m\n\u001b[31m  const input = card.querySelector('.cons-docket-input');\u001b[39m\n\u001b[31m  if (input) { input.value = docket; onDocketInput(input); }\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction renumberCons() {\u001b[39m\n\u001b[31m  document.querySelectorAll('#consList .cons-card').forEach((c,i) => {\u001b[39m\n\u001b[31m    c.querySelector('.cons-num').textContent = `CONSIGNEE #${i+1}`;\u001b[39m\n\u001b[31m    c.dataset.idx = i;\u001b[39m\n\u001b[31m  });\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31m// JOB REFERENCE\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31mfunction onClientChange() {\u001b[39m\n\u001b[31m  const val = document.getElementById('n_client').value;\u001b[39m\n\u001b[31m  document.getElementById('customClientField').style.display = val === '__c' ? 'block' : 'none';\u001b[39m\n\u001b[31m  updatePill();\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31mfunction updatePill() {\u001b[39m\n\u001b[31m  const client = document.getElementById('n_client').value;\u001b[39m\n\u001b[31m  const job = document.getElementById('n_jobref').value.trim();\u001b[39m\n\u001b[31m  const cName = client === '__c' ? document.getElementById('n_customClient').value : client;\u001b[39m\n\u001b[31m  const pill = document.getElementById('jobPill');\u001b[39m\n\u001b[31m  if (cName && job) {\u001b[39m\n\u001b[31m    document.getElementById('jobPillTxt').textContent = `${cName} › ${job}`;\u001b[39m\n\u001b[31m    pill.style.display = 'inline-flex';\u001b[39m\n\u001b[31m  } else {\u001b[39m\n\u001b[31m    pill.style.display = 'none';\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31mfunction clearJob() {\u001b[39m\n\u001b[31m  document.getElementById('n_client').value = '';\u001b[39m\n\u001b[31m  document.getElementById('n_jobref').value = '';\u001b[39m\n\u001b[31m  document.getElementById('jobPill').style.display = 'none';\u001b[39m\n\u001b[31m  document.getElementById('customClientField').style.display = 'none';\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31m// GATHER / SAVE\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31mfunction gatherDispatch() {\u001b[39m\n\u001b[31m  const client = document.getElementById('n_client').value;\u001b[39m\n\u001b[31m  const clientName = client === '__c' ? document.getElementById('n_customClient').value : client;\u001b[39m\n\u001b[31m  const consignees = [];\u001b[39m\n\u001b[31m  document.querySelectorAll('#consList .cons-card').forEach(card => {\u001b[39m\n\u001b[31m    const docket = card.querySelector('.cons-docket-input')?.value.trim() || '';\u001b[39m\n\u001b[31m    consignees.push({\u001b[39m\n\u001b[31m      name: card.querySelector('.cons-name')?.value || '',\u001b[39m\n\u001b[31m      city: card.querySelector('.cons-city')?.value || '',\u001b[39m\n\u001b[31m      address: card.querySelector('.cons-addr')?.value || '',\u001b[39m\n\u001b[31m      phone: card.querySelector('.cons-phone')?.value || '',\u001b[39m\n\u001b[31m      pincode: card.querySelector('.cons-pin')?.value || '',\u001b[39m\n\u001b[31m      material: card.querySelector('.cons-material')?.value || '',\u001b[39m\n\u001b[31m      qty: card.querySelector('.cons-qty')?.value || '',\u001b[39m\n\u001b[31m      docket,\u001b[39m\n\u001b[31m      status: docket ? 'booked' : 'pending',\u001b[39m\n\u001b[31m      lastEvent: '',\u001b[39m\n\u001b[31m      lastUpdated: ''\u001b[39m\n\u001b[31m    });\u001b[39m\n\u001b[31m  });\u001b[39m\n\u001b[31m  return {\u001b[39m\n\u001b[31m    id: 'DSP-' + Date.now(),\u001b[39m\n\u001b[31m    mode: 'manual',\u001b[39m\n\u001b[31m    client: clientName,\u001b[39m\n\u001b[31m    jobref: document.getElementById('n_jobref').value,\u001b[39m\n\u001b[31m    courier: document.getElementById('n_courier').value,\u001b[39m\n\u001b[31m    dispatchDate: document.getElementById('n_date').value,\u001b[39m\n\u001b[31m    remarks: document.getElementById('n_remarks').value,\u001b[39m\n\u001b[31m    consignees,\u001b[39m\n\u001b[31m    slipCount: slipOCRData.length,\u001b[39m\n\u001b[31m    status: 'active',\u001b[39m\n\u001b[31m    createdAt: new Date().toISOString()\u001b[39m\n\u001b[31m  };\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction validate(d) {\u001b[39m\n\u001b[31m  if (!d.client) { showToast('Select a client', 'err'); return false; }\u001b[39m\n\u001b[31m  if (!d.courier) { showToast('Select courier', 'err'); return false; }\u001b[39m\n\u001b[31m  if (!d.consignees.length) { showToast('Add at least one consignee', 'err'); return false; }\u001b[39m\n\u001b[31m  for (let i = 0; i < d.consignees.length; i++) {\u001b[39m\n\u001b[31m    const c = d.consignees[i];\u001b[39m\n\u001b[31m    if (!c.name || !c.city) {\u001b[39m\n\u001b[31m      showToast(`Consignee #${i+1} (${c.name||'unnamed'}) — name & city required`, 'err');\u001b[39m\n\u001b[31m      // Scroll to and highlight the problem consignee card\u001b[39m\n\u001b[31m      const cards = document.querySelectorAll('#consList .cons-card');\u001b[39m\n\u001b[31m      if (cards[i]) {\u001b[39m\n\u001b[31m        cards[i].scrollIntoView({ behavior: 'smooth', block: 'center' });\u001b[39m\n\u001b[31m        cards[i].style.borderColor = 'var(--red)';\u001b[39m\n\u001b[31m        cards[i].style.boxShadow = '0 0 0 2px rgba(239,68,68,.3)';\u001b[39m\n\u001b[31m        setTimeout(() => {\u001b[39m\n\u001b[31m          if (cards[i]) { cards[i].style.borderColor = ''; cards[i].style.boxShadow = ''; }\u001b[39m\n\u001b[31m        }, 3000);\u001b[39m\n\u001b[31m      }\u001b[39m\n\u001b[31m      return false;\u001b[39m\n\u001b[31m    }\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m  return true;\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction saveDraft() {\u001b[39m\n\u001b[31m  const d = gatherDispatch();\u001b[39m\n\u001b[31m  d.status = 'draft';\u001b[39m\n\u001b[31m  dispatches.unshift(d);\u001b[39m\n\u001b[31m  saveData();\u001b[39m\n\u001b[31m  updateHistCount();\u001b[39m\n\u001b[31m  showToast('Draft saved ✓', 'ok');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// ── VERIFICATION SCREEN ─────────────────────────────────\u001b[39m\n\u001b[31m// Known docket lengths per courier for validation\u001b[39m\n\u001b[31mconst DOCKET_LENGTHS = {\u001b[39m\n\u001b[31m  'SkyKing': { min: 9, max: 12 },\u001b[39m\n\u001b[31m  'DTDC':    { min: 8, max: 15 },\u001b[39m\n\u001b[31m  'Delhivery': { min: 10, max: 18 },\u001b[39m\n\u001b[31m  'Blue Dart': { min: 10, max: 13 },\u001b[39m\n\u001b[31m  'FedEx':   { min: 10, max: 20 },\u001b[39m\n\u001b[31m  'Ecom Express': { min: 10, max: 14 },\u001b[39m\n\u001b[31m  'India Post': { min: 13, max: 22 },\u001b[39m\n\u001b[31m};·\u001b[39m\n\u001b[31mfunction finaliseDispatch() {\u001b[39m\n\u001b[31m  const d = gatherDispatch();\u001b[39m\n\u001b[31m  if (!validate(d)) return;\u001b[39m\n\u001b[31m  showVerifyScreen(d);\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction showVerifyScreen(d) {\u001b[39m\n\u001b[31m  const pane = document.getElementById('verifyPane');\u001b[39m\n\u001b[31m  const list = document.getElementById('verifyList');\u001b[39m\n\u001b[31m  const summary = document.getElementById('verifySummary');·\u001b[39m\n\u001b[31m  // Build flags\u001b[39m\n\u001b[31m  const docketsSeen = {};\u001b[39m\n\u001b[31m  const courier = d.courier;\u001b[39m\n\u001b[31m  const lenRule = DOCKET_LENGTHS[courier];·\u001b[39m\n\u001b[31m  d.consignees.forEach((c, i) => {\u001b[39m\n\u001b[31m    if (c.docket) docketsSeen[c.docket] = (docketsSeen[c.docket] || 0) + 1;\u001b[39m\n\u001b[31m  });·\u001b[39m\n\u001b[31m  const total     = d.consignees.length;\u001b[39m\n\u001b[31m  const withDock  = d.consignees.filter(c => c.docket).length;\u001b[39m\n\u001b[31m  const dupes     = Object.values(docketsSeen).filter(v => v > 1).length;\u001b[39m\n\u001b[31m  const noDock    = total - withDock;\u001b[39m\n\u001b[31m  const badLen    = lenRule ? d.consignees.filter(c => c.docket &&\u001b[39m\n\u001b[31m    (c.docket.length < lenRule.min || c.docket.length > lenRule.max)).length : 0;·\u001b[39m\n\u001b[31m  // Summary bar\u001b[39m\n\u001b[31m  summary.innerHTML = `\u001b[39m\n\u001b[31m    <div class=\\\"vs-item\\\"><div class=\\\"vs-val\\\" style=\\\"color:var(--t1);\\\">${total}</div><div class=\\\"vs-lbl\\\">Total</div></div>\u001b[39m\n\u001b[31m    <div class=\\\"vs-item\\\"><div class=\\\"vs-val\\\" style=\\\"color:var(--grn);\\\">${withDock}</div><div class=\\\"vs-lbl\\\">Docketed</div></div>\u001b[39m\n\u001b[31m    <div class=\\\"vs-item\\\"><div class=\\\"vs-val\\\" style=\\\"color:${noDock > 0 ? 'var(--ylw)' : 'var(--t3)'};\\\">${noDock}</div><div class=\\\"vs-lbl\\\">No Docket</div></div>\u001b[39m\n\u001b[31m    <div class=\\\"vs-item\\\"><div class=\\\"vs-val\\\" style=\\\"color:${dupes > 0 ? 'var(--red)' : 'var(--t3)'};\\\">${dupes}</div><div class=\\\"vs-lbl\\\">Duplicates</div></div>\u001b[39m\n\u001b[31m    ${badLen > 0 ? `<div class=\\\"vs-item\\\"><div class=\\\"vs-val\\\" style=\\\"color:var(--sky);\\\">${badLen}</div><div class=\\\"vs-lbl\\\">Short/Long</div></div>` : ''}\u001b[39m\n\u001b[31m    <div class=\\\"vs-item\\\" style=\\\"margin-left:auto;text-align:right;\\\">\u001b[39m\n\u001b[31m      <div class=\\\"vs-val\\\" style=\\\"font-size:.8rem;color:var(--t2);\\\">${d.courier||'—'}</div>\u001b[39m\n\u001b[31m      <div class=\\\"vs-lbl\\\">${d.client||'—'} ${d.jobref ? '› '+d.jobref : ''}</div>\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m  `;·\u001b[39m\n\u001b[31m  // Build consignee rows\u001b[39m\n\u001b[31m  list.innerHTML = d.consignees.map((c, i) => {\u001b[39m\n\u001b[31m    const isDupe   = c.docket && docketsSeen[c.docket] > 1;\u001b[39m\n\u001b[31m    const isNoDock = !c.docket;\u001b[39m\n\u001b[31m    const isShort  = lenRule && c.docket &&\u001b[39m\n\u001b[31m      (c.docket.length < lenRule.min || c.docket.length > lenRule.max);·\u001b[39m\n\u001b[31m    const rowClass = isDupe ? 'has-dupe' : (isNoDock ? 'no-docket' : 'ok');·\u001b[39m\n\u001b[31m    // Find matching OCR slip for thumbnail\u001b[39m\n\u001b[31m    const ocrMatch = slipOCRData[i];\u001b[39m\n\u001b[31m    const thumbHtml = ocrMatch?.imageData\u001b[39m\n\u001b[31m      ? `<img class=\\\"verify-thumb\\\" src=\\\"${ocrMatch.imageData}\\\" onclick=\\\"showSlipFull(${i})\\\" title=\\\"View slip\\\">`\u001b[39m\n\u001b[31m      : '';·\u001b[39m\n\u001b[31m    // Flags\u001b[39m\n\u001b[31m    let flags = '';\u001b[39m\n\u001b[31m    if (isDupe)   flags += `<span class=\\\"verify-flag vf-dupe\\\">⚠ Duplicate</span> `;\u001b[39m\n\u001b[31m    if (isNoDock) flags += `<span class=\\\"verify-flag vf-nodocket\\\">No Docket</span> `;\u001b[39m\n\u001b[31m    if (isShort)  flags += `<span class=\\\"verify-flag vf-short\\\">${c.docket.length} digits</span> `;\u001b[39m\n\u001b[31m    if (!isDupe && !isNoDock && !isShort) flags = `<span class=\\\"verify-flag vf-ok\\\">✓ OK</span>`;·\u001b[39m\n\u001b[31m    // OCR confidence note\u001b[39m\n\u001b[31m    const confNote = ocrMatch && ocrMatch.confidence\u001b[39m\n\u001b[31m      ? `<div class=\\\"ocr-correction-note\\\">OCR confidence: ${Math.round(ocrMatch.confidence*100)}% via ${ocrMatch.source||'—'}</div>` : '';·\u001b[39m\n\u001b[31m    // WhatsApp URL\u001b[39m\n\u001b[31m    const waMsg = encodeURIComponent(\u001b[39m\n\u001b[31m      `Dear ${c.name}, your material has been dispatched via ${d.courier}. Docket: ${c.docket||'TBD'}. Track: ${(COURIER_MASTER[d.courier]?.trackUrl||'').replace('{docket}', c.docket||'')}`\u001b[39m\n\u001b[31m    );\u001b[39m\n\u001b[31m    const waUrl = c.phone ? `https://wa.me/91${c.phone.replace(/\\\\D/g,'')}?text=${waMsg}` : '';·\u001b[39m\n\u001b[31m    return `\u001b[39m\n\u001b[31m    <div class=\\\"verify-row ${rowClass}\\\" id=\\\"vrow_${i}\\\">\u001b[39m\n\u001b[31m      <div class=\\\"verify-row-hdr\\\">\u001b[39m\n\u001b[31m        <span class=\\\"verify-num\\\">#${i+1}</span>\u001b[39m\n\u001b[31m        <span class=\\\"verify-store\\\">${c.name||'—'}</span>\u001b[39m\n\u001b[31m        ${flags}\u001b[39m\n\u001b[31m        ${thumbHtml}\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      <div class=\\\"verify-fields\\\" style=\\\"grid-template-columns:1fr 1fr 1fr;\\\">\u001b[39m\n\u001b[31m        <div class=\\\"verify-field\\\">\u001b[39m\n\u001b[31m          <label>Docket No. <span style=\\\"color:var(--acc);\\\">*</span></label>\u001b[39m\n\u001b[31m          <input type=\\\"text\\\" value=\\\"${c.docket||''}\\\" oninput=\\\"updateVerifyField(${i},'docket',this)\\\"\u001b[39m\n\u001b[31m            style=\\\"font-family:var(--fnt-mono);${isDupe||isShort?'border-color:var(--red);':!c.docket?'border-color:var(--ylw);':'color:var(--acc);'}\\\">\u001b[39m\n\u001b[31m          ${confNote}\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m        <div class=\\\"verify-field\\\">\u001b[39m\n\u001b[31m          <label>City</label>\u001b[39m\n\u001b[31m          <input type=\\\"text\\\" value=\\\"${c.city||''}\\\" oninput=\\\"updateVerifyField(${i},'city',this)\\\"\u001b[39m\n\u001b[31m            style=\\\"${!c.city ? 'border-color:var(--ylw);' : ''}\\\">\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m        <div class=\\\"verify-field\\\">\u001b[39m\n\u001b[31m          <label>Pincode</label>\u001b[39m\n\u001b[31m          <input type=\\\"text\\\" value=\\\"${c.pincode||''}\\\" oninput=\\\"updateVerifyField(${i},'pincode',this)\\\"\u001b[39m\n\u001b[31m            style=\\\"font-family:var(--fnt-mono);\\\">\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m        <div class=\\\"verify-field\\\">\u001b[39m\n\u001b[31m          <label>Phone</label>\u001b[39m\n\u001b[31m          <input type=\\\"tel\\\" value=\\\"${c.phone||''}\\\" oninput=\\\"updateVerifyField(${i},'phone',this)\\\">\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m        <div class=\\\"verify-field\\\">\u001b[39m\n\u001b[31m          <label>Material / Contents</label>\u001b[39m\n\u001b[31m          <input type=\\\"text\\\" value=\\\"${c.material||''}\\\" oninput=\\\"updateVerifyField(${i},'material',this)\\\">\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m        <div class=\\\"verify-field\\\">\u001b[39m\n\u001b[31m          <label>Street Address</label>\u001b[39m\n\u001b[31m          <input type=\\\"text\\\" value=\\\"${c.address||''}\\\" oninput=\\\"updateVerifyField(${i},'address',this)\\\"\u001b[39m\n\u001b[31m            placeholder=\\\"Street / locality only\\\">\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      <div class=\\\"verify-actions\\\">\u001b[39m\n\u001b[31m        ${c.docket && c.phone ? `<button class=\\\"btn-wa\\\" onclick=\\\"window.open('${waUrl}','_blank')\\\">📱 WhatsApp</button>` : ''}\u001b[39m\n\u001b[31m        <button class=\\\"btn-sm\\\" onclick=\\\"removeVerifyRow(${i})\\\">✕ Remove</button>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m    </div>`;\u001b[39m\n\u001b[31m  }).join('');·\u001b[39m\n\u001b[31m  // Store dispatch object for confirm\u001b[39m\n\u001b[31m  pane._pendingDispatch = d;\u001b[39m\n\u001b[31m  pane.style.display = 'block';\u001b[39m\n\u001b[31m  pane.scrollTop = 0;\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction updateVerifyField(idx, field, input) {\u001b[39m\n\u001b[31m  const pane = document.getElementById('verifyPane');\u001b[39m\n\u001b[31m  if (!pane._pendingDispatch) return;\u001b[39m\n\u001b[31m  const c = pane._pendingDispatch.consignees[idx];\u001b[39m\n\u001b[31m  if (!c) return;\u001b[39m\n\u001b[31m  c[field] = input.value.trim();\u001b[39m\n\u001b[31m  input.classList.toggle('changed', input.value.trim() !== '');\u001b[39m\n\u001b[31m  // Re-check dupe if docket changed\u001b[39m\n\u001b[31m  if (field === 'docket') refreshVerifyFlags(pane._pendingDispatch);\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction refreshVerifyFlags(d) {\u001b[39m\n\u001b[31m  const seen = {};\u001b[39m\n\u001b[31m  d.consignees.forEach(c => { if (c.docket) seen[c.docket] = (seen[c.docket]||0)+1; });\u001b[39m\n\u001b[31m  d.consignees.forEach((c, i) => {\u001b[39m\n\u001b[31m    const row = document.getElementById('vrow_' + i);\u001b[39m\n\u001b[31m    if (!row) return;\u001b[39m\n\u001b[31m    const isDupe = c.docket && seen[c.docket] > 1;\u001b[39m\n\u001b[31m    row.classList.toggle('has-dupe', isDupe);\u001b[39m\n\u001b[31m  });\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction removeVerifyRow(idx) {\u001b[39m\n\u001b[31m  const pane = document.getElementById('verifyPane');\u001b[39m\n\u001b[31m  if (!pane._pendingDispatch) return;\u001b[39m\n\u001b[31m  const el = document.getElementById('vrow_' + idx);\u001b[39m\n\u001b[31m  if (el) {\u001b[39m\n\u001b[31m    el.style.transition = 'opacity .25s';\u001b[39m\n\u001b[31m    el.style.opacity = '0';\u001b[39m\n\u001b[31m    setTimeout(() => {\u001b[39m\n\u001b[31m      pane._pendingDispatch.consignees.splice(idx, 1);\u001b[39m\n\u001b[31m      showVerifyScreen(pane._pendingDispatch); // re-render with updated indices\u001b[39m\n\u001b[31m    }, 250);\u001b[39m\n\u001b[31m  } else {\u001b[39m\n\u001b[31m    pane._pendingDispatch.consignees.splice(idx, 1);\u001b[39m\n\u001b[31m    showVerifyScreen(pane._pendingDispatch);\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction approveAll() {\u001b[39m\n\u001b[31m  showToast('All rows approved ✓', 'ok');\u001b[39m\n\u001b[31m  document.getElementById('btnConfirmFinalise').style.background = 'var(--grn)';\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction closeVerify() {\u001b[39m\n\u001b[31m  const pane = document.getElementById('verifyPane');\u001b[39m\n\u001b[31m  // If this dispatch was already finalised, warn before going back\u001b[39m\n\u001b[31m  if (_lastFinalisedId && pane._pendingDispatch?.id === _lastFinalisedId) {\u001b[39m\n\u001b[31m    if (!confirm(\u001b[39m\n\u001b[31m      'This dispatch has already been finalised.\\\\n\\\\n' +\u001b[39m\n\u001b[31m      'Going back will take you to the edit form — any changes will create a NEW dispatch record.\\\\n\\\\n' +\u001b[39m\n\u001b[31m      'Go back anyway?'\u001b[39m\n\u001b[31m    )) return;\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m  pane.style.display = 'none';\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction showSlipFull(idx) {\u001b[39m\n\u001b[31m  const s = slipOCRData[idx];\u001b[39m\n\u001b[31m  if (!s?.imageData) return;\u001b[39m\n\u001b[31m  const w = window.open('');\u001b[39m\n\u001b[31m  w.document.write(`<img src=\\\"${s.imageData}\\\" style=\\\"max-width:100%;height:auto;\\\">`);\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// Log OCR correction to sheets for future accuracy improvement\u001b[39m\n\u001b[31masync function logOCRCorrection(original, corrected, courier) {\u001b[39m\n\u001b[31m  if (!window.appConfig.webhook_url || original === corrected) return;\u001b[39m\n\u001b[31m  try {\u001b[39m\n\u001b[31m    await fetch(window.appConfig.webhook_url, {\u001b[39m\n\u001b[31m      method: 'POST', mode: 'no-cors',\u001b[39m\n\u001b[31m      headers: { 'Content-Type': 'application/json' },\u001b[39m\n\u001b[31m      body: JSON.stringify({\u001b[39m\n\u001b[31m        type: 'ocr_correction',\u001b[39m\n\u001b[31m        original, corrected, courier,\u001b[39m\n\u001b[31m        timestamp: new Date().toISOString()\u001b[39m\n\u001b[31m      })\u001b[39m\n\u001b[31m    });\u001b[39m\n\u001b[31m  } catch(e) {}\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mlet _lastFinalisedId = null; // track last finalised dispatch to warn on back·\u001b[39m\n\u001b[31masync function confirmAndFinalise() {\u001b[39m\n\u001b[31m  const pane = document.getElementById('verifyPane');\u001b[39m\n\u001b[31m  const d = pane._pendingDispatch;\u001b[39m\n\u001b[31m  if (!d) return;·\u001b[39m\n\u001b[31m  d.status = 'active';\u001b[39m\n\u001b[31m  d.finalisedAt = new Date().toISOString();·\u001b[39m\n\u001b[31m  // Log OCR corrections\u001b[39m\n\u001b[31m  slipOCRData.forEach((s, i) => {\u001b[39m\n\u001b[31m    const c = d.consignees[i];\u001b[39m\n\u001b[31m    if (c && s.docket && c.docket && s.docket !== c.docket) {\u001b[39m\n\u001b[31m      logOCRCorrection(s.docket, c.docket, d.courier);\u001b[39m\n\u001b[31m    }\u001b[39m\n\u001b[31m  });·\u001b[39m\n\u001b[31m  // Replace existing draft with same ID if present, else prepend\u001b[39m\n\u001b[31m  const existingIdx = dispatches.findIndex(x => x.id === d.id);\u001b[39m\n\u001b[31m  if (existingIdx >= 0) {\u001b[39m\n\u001b[31m    dispatches[existingIdx] = d;\u001b[39m\n\u001b[31m  } else {\u001b[39m\n\u001b[31m    dispatches.unshift(d);\u001b[39m\n\u001b[31m  }·\u001b[39m\n\u001b[31m  _lastFinalisedId = d.id;\u001b[39m\n\u001b[31m  saveData();\u001b[39m\n\u001b[31m  syncDispatchToDB(d); // push to Postgres (non-blocking)\u001b[39m\n\u001b[31m  updateHistCount();\u001b[39m\n\u001b[31m  if (window.appConfig.auto_log !== 'false' && window.appConfig.webhook_url) await logToSheets(d);\u001b[39m\n\u001b[31m  showToast('Dispatch finalised ✓', 'ok');\u001b[39m\n\u001b[31m  pane.style.display = 'none';\u001b[39m\n\u001b[31m  pane._pendingDispatch = null; // clear pending\u001b[39m\n\u001b[31m  resetForm();\u001b[39m\n\u001b[31m  showPane('dashboard');\u001b[39m\n\u001b[31m  renderDashboard();\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction resetForm() {\u001b[39m\n\u001b[31m  document.getElementById('n_client').value = '';\u001b[39m\n\u001b[31m  document.getElementById('n_jobref').value = '';\u001b[39m\n\u001b[31m  document.getElementById('n_courier').value = '';\u001b[39m\n\u001b[31m  document.getElementById('n_remarks').value = '';\u001b[39m\n\u001b[31m  document.getElementById('jobPill').style.display = 'none';\u001b[39m\n\u001b[31m  document.getElementById('customClientField').style.display = 'none';\u001b[39m\n\u001b[31m  document.getElementById('consList').innerHTML = '';\u001b[39m\n\u001b[31m  addConsignee();\u001b[39m\n\u001b[31m  clearSlips();\u001b[39m\n\u001b[31m  clearStores();\u001b[39m\n\u001b[31m  setToday();\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction newDispatch() { resetForm(); showPane('new'); }·\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31m// DASHBOARD\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31mfunction renderDashboard() {\u001b[39m\n\u001b[31m  // Exclude drafts from dashboard — only show active/finalised dispatches\u001b[39m\n\u001b[31m  const activeDis = dispatches.filter(d => d.status !== 'draft');\u001b[39m\n\u001b[31m  const allCons = activeDis.flatMap(d => d.consignees || []);\u001b[39m\n\u001b[31m  const countBy = (status) => allCons.filter(c => c.status === status).length;\u001b[39m\n\u001b[31m  document.getElementById('st-delivered').textContent = countBy('delivered');\u001b[39m\n\u001b[31m  document.getElementById('st-transit').textContent = countBy('transit') + countBy('in-transit');\u001b[39m\n\u001b[31m  document.getElementById('st-booked').textContent = countBy('booked');\u001b[39m\n\u001b[31m  document.getElementById('st-pending').textContent = allCons.filter(c => !c.docket || c.status === 'pending').length;·\u001b[39m\n\u001b[31m  const grid = document.getElementById('dashGrid');\u001b[39m\n\u001b[31m  // Flatten all consignees across dispatches\u001b[39m\n\u001b[31m  let items = activeDis.flatMap(d =>\u001b[39m\n\u001b[31m    (d.consignees || []).filter(c => c.docket).map(c => ({...c, dispatchId: d.id, courier: d.courier, client: d.client, jobref: d.jobref}))\u001b[39m\n\u001b[31m  );\u001b[39m\n\u001b[31m  if (dashFilter !== 'all') {\u001b[39m\n\u001b[31m    items = items.filter(c => {\u001b[39m\n\u001b[31m      if (dashFilter === 'delivered') return c.status === 'delivered';\u001b[39m\n\u001b[31m      if (dashFilter === 'transit') return c.status === 'transit' || c.status === 'in-transit';\u001b[39m\n\u001b[31m      if (dashFilter === 'booked') return c.status === 'booked';\u001b[39m\n\u001b[31m      if (dashFilter === 'pending') return !c.docket || c.status === 'pending';\u001b[39m\n\u001b[31m      return true;\u001b[39m\n\u001b[31m    });\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m  if (!items.length) {\u001b[39m\n\u001b[31m    grid.innerHTML = '<div class=\\\"empty\\\"><div class=\\\"e-icon\\\">📦</div><p>No docket-tracked consignees.<br>Add docket numbers to see tiles.</p></div>';\u001b[39m\n\u001b[31m    return;\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m  const progMap = { delivered:100, transit:60,'in-transit':60, booked:20, pending:5 };\u001b[39m\n\u001b[31m  const progClr = { delivered:'var(--grn)', transit:'var(--ylw)','in-transit':'var(--ylw)', booked:'var(--sky)', pending:'var(--t4)' };\u001b[39m\n\u001b[31m  grid.innerHTML = items.map(c => {\u001b[39m\n\u001b[31m    const sp = c.status || 'booked';\u001b[39m\n\u001b[31m    const prog = progMap[sp] || 20;\u001b[39m\n\u001b[31m    const clr = progClr[sp] || 'var(--t4)';\u001b[39m\n\u001b[31m    return `\u001b[39m\n\u001b[31m    <div class=\\\"docket-tile\\\" onclick=\\\"showDocketDetail('${c.docket}','${c.courier||''}','${c.dispatchId}')\\\">\u001b[39m\n\u001b[31m      <div class=\\\"docket-tile-hdr\\\">\u001b[39m\n\u001b[31m        <div class=\\\"docket-tile-left\\\">\u001b[39m\n\u001b[31m          <div class=\\\"docket-num\\\">${c.docket}</div>\u001b[39m\n\u001b[31m          <div class=\\\"docket-store\\\">${c.name} — ${c.city}</div>\u001b[39m\n\u001b[31m          <div class=\\\"docket-courier\\\">${c.courier||''} · ${c.client||''} ${c.jobref ? '› '+c.jobref : ''}</div>\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m        <span class=\\\"status-pill sp-${sp}\\\">${sp}</span>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      <div style=\\\"display:flex;gap:6px;margin-bottom:6px;align-items:center;\\\">\u001b[39m\n\u001b[31m        <input type=\\\"checkbox\\\" id=\\\"sel_${c.docket.replace(/[^a-z0-9]/gi,'_')}\\\"\u001b[39m\n\u001b[31m          onclick=\\\"event.stopPropagation();toggleTileSelect('${c.docket}')\\\"\u001b[39m\n\u001b[31m          style=\\\"width:14px;height:14px;cursor:pointer;accent-color:var(--acc);\\\">\u001b[39m\n\u001b[31m        <label style=\\\"font-size:.6rem;color:var(--t3);font-family:var(--fnt-mono);cursor:pointer;\\\"\u001b[39m\n\u001b[31m          onclick=\\\"event.stopPropagation();toggleTileSelect('${c.docket}')\\\">Select</label>\u001b[39m\n\u001b[31m        ${c.manualStatus ?\u001b[39m\n\u001b[31m          `<span style=\\\"font-size:.6rem;font-family:var(--fnt-mono);color:var(--grn);\\\">📞 ${c.manualStatus}</span>` : ''}\u001b[39m\n\u001b[31m        ${c.podUploaded ? '<span style=\\\"font-size:.6rem;color:var(--grn);font-family:var(--fnt-mono);\\\">✓ POD</span>' : ''}\u001b[39m\n\u001b[31m        <button onclick=\\\"event.stopPropagation();showManualTracking('${c.docket}','${c.dispatchId}','${c.name}','${c.phone||''}','${c.city||''}')\\\"\u001b[39m\n\u001b[31m          style=\\\"font-size:.6rem;background:rgba(56,189,248,.1);border:1px solid rgba(56,189,248,.3);\u001b[39m\n\u001b[31m          color:var(--sky);padding:2px 7px;border-radius:10px;cursor:pointer;font-family:var(--fnt-mono);\\\">📞 Call Log</button>\u001b[39m\n\u001b[31m        <button onclick=\\\"event.stopPropagation();showPODUpload('${c.docket}','${c.dispatchId}')\\\"\u001b[39m\n\u001b[31m          style=\\\"font-size:.6rem;background:none;border:1px solid var(--bdr);color:var(--t3);\u001b[39m\n\u001b[31m          padding:2px 7px;border-radius:10px;cursor:pointer;font-family:var(--fnt-mono);\\\">📸 POD</button>\u001b[39m\n\u001b[31m        ${c.manualDelivered ? `<button onclick=\\\"event.stopPropagation();resumeAutoTracking('${c.docket}','${c.dispatchId}')\\\"\u001b[39m\n\u001b[31m          style=\\\"font-size:.6rem;background:rgba(251,146,60,.1);border:1px solid rgba(251,146,60,.4);\u001b[39m\n\u001b[31m          color:#fb923c;padding:2px 7px;border-radius:10px;cursor:pointer;font-family:var(--fnt-mono);\\\">↩ Undo</button>` : ''}\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      <div class=\\\"docket-tile-footer\\\">\u001b[39m\n\u001b[31m        <div style=\\\"flex:1;min-width:0;\\\">\u001b[39m\n\u001b[31m          <span class=\\\"docket-last-event docket-evt-${c.docket}\\\" id=\\\"evt_${c.docket.replace(/[^a-z0-9]/gi,'_')}\\\">${c.lastEvent || 'No tracking yet'}</span>\u001b[39m\n\u001b[31m          <span class=\\\"track-spinner\\\" id=\\\"spin_${c.docket.replace(/[^a-z0-9]/gi,'_')}\\\" style=\\\"display:none;color:var(--ylw);font-family:var(--fnt-mono);font-size:.68rem;animation:blink 1s infinite;\\\">⏳ tracking...</span>\u001b[39m\n\u001b[31m          ${c.lastScanLocation && c.lastScanLocation !== c.lastEvent ?·\u001b[39m\n\u001b[31m            `<div style=\\\"font-size:.62rem;color:var(--t3);font-family:var(--fnt-mono);margin-top:2px;\\\">📍 ${c.lastScanLocation}</div>` : ''}\u001b[39m\n\u001b[31m          ${c.lastTrackedAt ?·\u001b[39m\n\u001b[31m            `<div style=\\\"font-size:.6rem;color:var(--t4);font-family:var(--fnt-mono);margin-top:1px;\\\">🕐 tracked ${new Date(c.lastTrackedAt).toLocaleTimeString('en-IN',{hour:'2-digit',minute:'2-digit'})}</div>` : ''}\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m        <button class=\\\"btn-track-live live-btn-${c.docket.replace(/[^a-z0-9]/gi,'_')}\\\" onclick=\\\"event.stopPropagation();liveTrackTile('${c.docket}','${c.courier||''}','${c.dispatchId}')\\\">\u001b[39m\n\u001b[31m          <span class=\\\"live-dot\\\"></span>LIVE\u001b[39m\n\u001b[31m        </button>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      <div class=\\\"docket-progress\\\">\u001b[39m\n\u001b[31m        <div class=\\\"docket-progress-fill\\\" style=\\\"width:${prog}%;background:${clr};\\\"></div>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m    </div>`;\u001b[39m\n\u001b[31m  }).join('');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction filterDash(f, el) {\u001b[39m\n\u001b[31m  dashFilter = f;\u001b[39m\n\u001b[31m  document.querySelectorAll('#dashFilters .btn-sm').forEach(b => b.classList.remove('primary'));\u001b[39m\n\u001b[31m  el.classList.add('primary');\u001b[39m\n\u001b[31m  renderDashboard();\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31masync function liveTrackTile(docket, courier, dispatchId) {\u001b[39m\n\u001b[31m  showToast(`Tracking ${docket}...`, 'info');\u001b[39m\n\u001b[31m  const result = await fetchTracking(docket, courier);\u001b[39m\n\u001b[31m  // Update status in dispatches\u001b[39m\n\u001b[31m  for (const d of dispatches) {\u001b[39m\n\u001b[31m    if (d.id !== dispatchId) continue;\u001b[39m\n\u001b[31m    for (const c of d.consignees) {\u001b[39m\n\u001b[31m      if (c.docket === docket) {\u001b[39m\n\u001b[31m        c.status = result.status || c.status;\u001b[39m\n\u001b[31m        c.lastEvent = result.lastEvent || c.lastEvent;\u001b[39m\n\u001b[31m        c.lastUpdated = new Date().toISOString();\u001b[39m\n\u001b[31m      }\u001b[39m\n\u001b[31m    }\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m  saveData();\u001b[39m\n\u001b[31m  renderDashboard();\u001b[39m\n\u001b[31m  if (result.status === 'failed') {\u001b[39m\n\u001b[31m    // Open courier site as fallback\u001b[39m\n\u001b[31m    openCourierTrackUrl(docket, courier);\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction showDocketDetail(docket, courier, dispatchId) {\u001b[39m\n\u001b[31m  const d = dispatches.find(x => x.id === dispatchId);\u001b[39m\n\u001b[31m  const c = d?.consignees?.find(x => x.docket === docket);\u001b[39m\n\u001b[31m  if (!c) return;\u001b[39m\n\u001b[31m  const content = document.getElementById('detailContent');\u001b[39m\n\u001b[31m  content.innerHTML = `\u001b[39m\n\u001b[31m    <div class=\\\"sheet-title\\\">${docket}</div>\u001b[39m\n\u001b[31m    <div class=\\\"info-tag\\\">📍 <strong>${c.name}</strong> — ${c.city}</div>\u001b[39m\n\u001b[31m    ${c.phone ? `<div class=\\\"info-tag\\\">📞 ${c.phone}</div>` : ''}\u001b[39m\n\u001b[31m    <div class=\\\"info-tag\\\">📦 ${c.material||'—'} ${c.qty ? '('+c.qty+' pcs)':''}</div>\u001b[39m\n\u001b[31m    <div class=\\\"info-tag\\\">🚚 ${courier||'—'} · <span class=\\\"status-pill sp-${c.status||'pending'}\\\">${c.status||'pending'}</span></div>\u001b[39m\n\u001b[31m    ${c.lastEvent ? `<div class=\\\"info-tag\\\">🕐 ${c.lastEvent}</div>` : ''}\u001b[39m\n\u001b[31m    <div class=\\\"sec-div\\\"><span>Tracking History</span></div>\u001b[39m\n\u001b[31m    <div id=\\\"docketTimeline\\\">\u001b[39m\n\u001b[31m      ${c.timeline?.length ?\u001b[39m\n\u001b[31m        c.timeline.map(e => `<div class=\\\"track-evt\\\"><span class=\\\"track-time\\\">${e.time||''}</span><div><div class=\\\"track-desc\\\">${e.desc||''}</div><div class=\\\"track-loc\\\">${e.loc||''}</div></div></div>`).join('') :\u001b[39m\n\u001b[31m        '<div style=\\\"color:var(--t3);font-size:.78rem;padding:10px 0;\\\">No timeline data. Click Live Track to fetch.</div>'}\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m    <div style=\\\"display:flex;gap:6px;flex-wrap:wrap;margin-top:14px;\\\">\u001b[39m\n\u001b[31m      <button class=\\\"btn btn-ghost\\\" style=\\\"flex:1;\\\" onclick=\\\"openCourierTrackUrl('${docket}','${courier}')\\\">🔗 Courier Site</button>\u001b[39m\n\u001b[31m      <button class=\\\"btn btn-orange\\\" style=\\\"flex:1;\\\" onclick=\\\"liveTrackTile('${docket}','${courier}','${dispatchId}');closeOverlay('detailOverlay')\\\">⚡ Live Track</button>\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m    <div style=\\\"display:flex;gap:6px;flex-wrap:wrap;margin-top:6px;\\\">\u001b[39m\n\u001b[31m      <button class=\\\"btn-wa\\\" style=\\\"flex:1;padding:8px;\\\" onclick=\\\"closeOverlay('detailOverlay');showPODUpload('${docket}','${dispatchId}')\\\">📸 Upload POD</button>\u001b[39m\n\u001b[31m      ${c.podUploaded ? '<span style=\\\"color:var(--grn);font-size:.72rem;font-family:var(--fnt-mono);padding:6px;\\\">✓ POD uploaded</span>' : ''}\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m  `;\u001b[39m\n\u001b[31m  openOverlay('detailOverlay');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31m// TRACKING ENGINE\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31mfunction _normaliseStatus(raw) {\u001b[39m\n\u001b[31m  const s = (raw || '').toLowerCase().trim();\u001b[39m\n\u001b[31m  if (!s || s === 'error') return null;\u001b[39m\n\u001b[31m  if (s.includes('undeliver') || s.includes('delivery_fail') ||\u001b[39m\n\u001b[31m      s.includes('failed_delivery') || s.includes('not deliver') ||\u001b[39m\n\u001b[31m      s.includes('return')  || s.includes('rto')) return 'pending';\u001b[39m\n\u001b[31m  if (s.includes('deliver'))                       return 'delivered';\u001b[39m\n\u001b[31m  if (s.includes('transit') || s.includes('hub') ||\u001b[39m\n\u001b[31m      s.includes('inbound') || s.includes('outbound') ||\u001b[39m\n\u001b[31m      s.includes('out for'))                       return 'transit';\u001b[39m\n\u001b[31m  if (s.includes('book') || s.includes('pick'))   return 'booked';\u001b[39m\n\u001b[31m  return s;\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31masync function _fetchWithRetry(url, opts, maxRetries = 1, retryDelayMs = 4000) {\u001b[39m\n\u001b[31m  for (let attempt = 0; attempt <= maxRetries; attempt++) {\u001b[39m\n\u001b[31m    try {\u001b[39m\n\u001b[31m      const resp = await fetch(url, opts);\u001b[39m\n\u001b[31m      if (resp.ok) return resp;\u001b[39m\n\u001b[31m    } catch(e) {\u001b[39m\n\u001b[31m      if (attempt === maxRetries) throw e;\u001b[39m\n\u001b[31m      await new Promise(r => setTimeout(r, retryDelayMs));\u001b[39m\n\u001b[31m    }\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m  return null;\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31masync function fetchTracking(docket, courier) {\u001b[39m\n\u001b[31m  // Strategy 1: VPS proxy (SkyKing + others that block direct fetch)\u001b[39m\n\u001b[31m  const proxyUrl = window.appConfig.proxy_url;\u001b[39m\n\u001b[31m  if (proxyUrl) {\u001b[39m\n\u001b[31m    try {\u001b[39m\n\u001b[31m      const url  = `${proxyUrl}?courier=${encodeURIComponent(courier)}&docket=${encodeURIComponent(docket)}`;\u001b[39m\n\u001b[31m      const resp = await _fetchWithRetry(url, { signal: AbortSignal.timeout(15000) });\u001b[39m\n\u001b[31m      if (resp) {\u001b[39m\n\u001b[31m        const data      = await resp.json();\u001b[39m\n\u001b[31m        const normStatus = _normaliseStatus(data.status);\u001b[39m\n\u001b[31m        if (normStatus) {\u001b[39m\n\u001b[31m          return { status: normStatus, lastEvent: data.lastEvent || '', timeline: data.timeline || [] };\u001b[39m\n\u001b[31m        }\u001b[39m\n\u001b[31m        openCourierTrackUrl(docket, courier);\u001b[39m\n\u001b[31m        return { status: 'transit', lastEvent: data.lastEvent || 'Check courier site', timeline: [] };\u001b[39m\n\u001b[31m      }\u001b[39m\n\u001b[31m    } catch(e) {}\u001b[39m\n\u001b[31m  }·\u001b[39m\n\u001b[31m  // Strategy 2: Delhivery direct API\u001b[39m\n\u001b[31m  if (courier === 'Delhivery') {\u001b[39m\n\u001b[31m    try {\u001b[39m\n\u001b[31m      const resp = await _fetchWithRetry(\u001b[39m\n\u001b[31m        `https://dlv-api.delhivery.com/v3/track?waybill=${docket}`,\u001b[39m\n\u001b[31m        { signal: AbortSignal.timeout(8000) }\u001b[39m\n\u001b[31m      );\u001b[39m\n\u001b[31m      if (resp) {\u001b[39m\n\u001b[31m        const data = await resp.json();\u001b[39m\n\u001b[31m        const pkg  = data.ShipmentData?.[0]?.Shipment;\u001b[39m\n\u001b[31m        if (pkg) {\u001b[39m\n\u001b[31m          const normStatus = _normaliseStatus(pkg.Status?.Status);\u001b[39m\n\u001b[31m          return { status: normStatus || 'transit', lastEvent: pkg.Status?.Instructions || '', timeline: [] };\u001b[39m\n\u001b[31m        }\u001b[39m\n\u001b[31m      }\u001b[39m\n\u001b[31m    } catch(e) {}\u001b[39m\n\u001b[31m  }·\u001b[39m\n\u001b[31m  return { status: 'failed', lastEvent: 'Open courier site to check', timeline: [] };\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction openCourierTrackUrl(docket, courier) {\u001b[39m\n\u001b[31m  const m = COURIER_MASTER[courier];\u001b[39m\n\u001b[31m  if (!m) return;\u001b[39m\n\u001b[31m  const url = m.trackUrl.replace('{docket}', docket);\u001b[39m\n\u001b[31m  window.open(url, '_blank');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// Track tab - multiple dockets\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31m// TRACK TAB — new logic\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31mlet _trackSelected = new Set(); // dockets currently selected for tracking·\u001b[39m\n\u001b[31m// Called when Track tab opens or courier changes\u001b[39m\n\u001b[31mfunction buildTrackDocketChips() {\u001b[39m\n\u001b[31m  const courier = document.getElementById('t_courier').value;\u001b[39m\n\u001b[31m  const m = COURIER_MASTER[courier];\u001b[39m\n\u001b[31m  const maxBulk = m?.maxBulk || 1;\u001b[39m\n\u001b[31m  const limitEl = document.getElementById('trackCourierLimit');\u001b[39m\n\u001b[31m  if (limitEl) limitEl.textContent = `Max ${maxBulk} at once for ${courier}`;·\u001b[39m\n\u001b[31m  // Gather all dockets from active dispatches for this courier\u001b[39m\n\u001b[31m  const activeDis = dispatches.filter(d => d.status !== 'draft');\u001b[39m\n\u001b[31m  const allDockets = activeDis.flatMap(d =>\u001b[39m\n\u001b[31m    (d.consignees || [])\u001b[39m\n\u001b[31m      .filter(c => c.docket && d.courier === courier)\u001b[39m\n\u001b[31m      .map(c => ({ docket: c.docket, name: c.name, city: c.city, status: c.status || 'booked' }))\u001b[39m\n\u001b[31m  );·\u001b[39m\n\u001b[31m  // Also include dockets from OTHER couriers as un-selectable reference\u001b[39m\n\u001b[31m  const otherDockets = activeDis.flatMap(d =>\u001b[39m\n\u001b[31m    (d.consignees || [])\u001b[39m\n\u001b[31m      .filter(c => c.docket && d.courier !== courier)\u001b[39m\n\u001b[31m      .map(c => ({ docket: c.docket, name: c.name, courier: d.courier }))\u001b[39m\n\u001b[31m  );·\u001b[39m\n\u001b[31m  const chips = document.getElementById('trackDocketChips');\u001b[39m\n\u001b[31m  if (!allDockets.length && !otherDockets.length) {\u001b[39m\n\u001b[31m    chips.innerHTML = '<span style=\\\"font-size:.72rem;color:var(--t3);font-family:var(--fnt-mono);\\\">No dispatched dockets yet — enter manually below</span>';\u001b[39m\n\u001b[31m    return;\u001b[39m\n\u001b[31m  }·\u001b[39m\n\u001b[31m  const spClr = { delivered:'var(--grn)', transit:'var(--ylw)', booked:'var(--sky)', pending:'var(--t4)' };·\u001b[39m\n\u001b[31m  chips.innerHTML = [\u001b[39m\n\u001b[31m    // This courier's dockets\u001b[39m\n\u001b[31m    ...allDockets.map(d => {\u001b[39m\n\u001b[31m      const sel = _trackSelected.has(d.docket);\u001b[39m\n\u001b[31m      const clr = spClr[d.status] || 'var(--t4)';\u001b[39m\n\u001b[31m      return `<button onclick=\\\"toggleTrackDocket('${d.docket}')\\\" id=\\\"chip_${d.docket}\\\"\u001b[39m\n\u001b[31m        style=\\\"font-family:var(--fnt-mono);font-size:.72rem;padding:4px 10px;border-radius:20px;\u001b[39m\n\u001b[31m        cursor:pointer;transition:all .2s;border:1px solid ${sel ? 'var(--acc)' : 'var(--bdr)'};\u001b[39m\n\u001b[31m        background:${sel ? 'rgba(249,115,22,.15)' : 'var(--sur2)'};\u001b[39m\n\u001b[31m        color:${sel ? 'var(--acc)' : 'var(--t2)'};\\\">\u001b[39m\n\u001b[31m        ${d.docket} <span style=\\\"font-size:.6rem;color:${clr};\\\">● ${d.status}</span>\u001b[39m\n\u001b[31m        ${d.city ? `<span style=\\\"font-size:.6rem;color:var(--t3);margin-left:4px;\\\">${d.city}</span>` : ''}\u001b[39m\n\u001b[31m      </button>`;\u001b[39m\n\u001b[31m    }),\u001b[39m\n\u001b[31m    // Other couriers — show greyed, click selects and switches courier\u001b[39m\n\u001b[31m    ...otherDockets.slice(0, 5).map(d =>\u001b[39m\n\u001b[31m      `<span title=\\\"${d.courier}\\\" style=\\\"font-family:var(--fnt-mono);font-size:.68rem;padding:3px 8px;\u001b[39m\n\u001b[31m        border-radius:20px;border:1px dashed var(--bdr);color:var(--t4);cursor:default;\\\">\u001b[39m\n\u001b[31m        ${d.docket} <span style=\\\"font-size:.55rem;\\\">${d.courier}</span>\u001b[39m\n\u001b[31m      </span>`\u001b[39m\n\u001b[31m    )\u001b[39m\n\u001b[31m  ].join('');·\u001b[39m\n\u001b[31m  updateTrackSummary();\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction toggleTrackDocket(docket) {\u001b[39m\n\u001b[31m  const courier = document.getElementById('t_courier').value;\u001b[39m\n\u001b[31m  const m = COURIER_MASTER[courier];\u001b[39m\n\u001b[31m  const maxBulk = m?.maxBulk || 1;·\u001b[39m\n\u001b[31m  if (_trackSelected.has(docket)) {\u001b[39m\n\u001b[31m    _trackSelected.delete(docket);\u001b[39m\n\u001b[31m  } else {\u001b[39m\n\u001b[31m    if (_trackSelected.size >= maxBulk) {\u001b[39m\n\u001b[31m      showToast(`${courier} supports max ${maxBulk} docket(s) at once`, 'err');\u001b[39m\n\u001b[31m      return;\u001b[39m\n\u001b[31m    }\u001b[39m\n\u001b[31m    _trackSelected.add(docket);\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m  buildTrackDocketChips(); // re-render chips with updated selection\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction addManualDockets() {\u001b[39m\n\u001b[31m  const raw = document.getElementById('t_manual_dockets').value;\u001b[39m\n\u001b[31m  const courier = document.getElementById('t_courier').value;\u001b[39m\n\u001b[31m  const m = COURIER_MASTER[courier];\u001b[39m\n\u001b[31m  const maxBulk = m?.maxBulk || 1;\u001b[39m\n\u001b[31m  const newDockets = raw.split(/[,\\\\n]/).map(d => d.trim()).filter(Boolean);\u001b[39m\n\u001b[31m  let added = 0;\u001b[39m\n\u001b[31m  for (const d of newDockets) {\u001b[39m\n\u001b[31m    if (_trackSelected.size >= maxBulk) {\u001b[39m\n\u001b[31m      showToast(`Max ${maxBulk} reached for ${courier}`, 'err');\u001b[39m\n\u001b[31m      break;\u001b[39m\n\u001b[31m    }\u001b[39m\n\u001b[31m    _trackSelected.add(d);\u001b[39m\n\u001b[31m    added++;\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m  document.getElementById('t_manual_dockets').value = '';\u001b[39m\n\u001b[31m  buildTrackDocketChips();\u001b[39m\n\u001b[31m  if (added) showToast(`${added} docket(s) added ✓`, 'ok');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction clearTrackSelection() {\u001b[39m\n\u001b[31m  _trackSelected.clear();\u001b[39m\n\u001b[31m  buildTrackDocketChips();\u001b[39m\n\u001b[31m  document.getElementById('trackResultsArea').innerHTML = '';\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction updateTrackSummary() {\u001b[39m\n\u001b[31m  const summary = document.getElementById('trackSelectedSummary');\u001b[39m\n\u001b[31m  const countEl = document.getElementById('trackSelectedCount');\u001b[39m\n\u001b[31m  const listEl  = document.getElementById('trackSelectedList');\u001b[39m\n\u001b[31m  if (!summary) return;\u001b[39m\n\u001b[31m  const arr = Array.from(_trackSelected);\u001b[39m\n\u001b[31m  summary.style.display = arr.length ? 'block' : 'none';\u001b[39m\n\u001b[31m  if (countEl) countEl.textContent = `${arr.length} docket(s) selected`;\u001b[39m\n\u001b[31m  if (listEl)  listEl.textContent  = arr.slice(0, 5).join(', ') + (arr.length > 5 ? `... +${arr.length-5}` : '');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction openTrackOnCourierSite() {\u001b[39m\n\u001b[31m  const courier = document.getElementById('t_courier').value;\u001b[39m\n\u001b[31m  const dockets = Array.from(_trackSelected);\u001b[39m\n\u001b[31m  if (!dockets.length) { showToast('Select dockets first', 'err'); return; }\u001b[39m\n\u001b[31m  const m = COURIER_MASTER[courier];\u001b[39m\n\u001b[31m  if (!m) return;\u001b[39m\n\u001b[31m  // SkyKing: open with all CNOs comma-separated in URL\u001b[39m\n\u001b[31m  let url;\u001b[39m\n\u001b[31m  if (courier === 'SkyKing') {\u001b[39m\n\u001b[31m    url = `https://skyking.co/track?cno=${dockets.join(',')}`;\u001b[39m\n\u001b[31m  } else if (dockets.length === 1) {\u001b[39m\n\u001b[31m    url = m.trackUrl.replace('{docket}', dockets[0]);\u001b[39m\n\u001b[31m  } else {\u001b[39m\n\u001b[31m    url = m.trackBulkUrl || m.trackUrl.replace('{docket}', dockets[0]);\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m  window.open(url, '_blank');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31masync function trackSelected() {\u001b[39m\n\u001b[31m  const courier = document.getElementById('t_courier').value;\u001b[39m\n\u001b[31m  const dockets = Array.from(_trackSelected);\u001b[39m\n\u001b[31m  if (!dockets.length) { showToast('Select at least one docket', 'err'); return; }·\u001b[39m\n\u001b[31m  const area = document.getElementById('trackResultsArea');\u001b[39m\n\u001b[31m  area.innerHTML = `<div class=\\\"card\\\" style=\\\"text-align:center;padding:20px;\\\">\u001b[39m\n\u001b[31m    <div style=\\\"font-size:1.5rem;margin-bottom:8px;\\\">⏳</div>\u001b[39m\n\u001b[31m    <div style=\\\"font-family:var(--fnt-mono);font-size:.78rem;color:var(--t2);\\\">Tracking ${dockets.length} docket(s) via ${courier}...</div>\u001b[39m\n\u001b[31m  </div>`;·\u001b[39m\n\u001b[31m  let resultsHTML = '';\u001b[39m\n\u001b[31m  for (const docket of dockets) {\u001b[39m\n\u001b[31m    const result = await fetchTracking(docket, courier);\u001b[39m\n\u001b[31m    const sp = (!result.status || result.status === 'failed') ? 'pending' : result.status;\u001b[39m\n\u001b[31m    const spClr = { delivered:'var(--grn)', transit:'var(--ylw)', booked:'var(--sky)', pending:'var(--t3)' };\u001b[39m\n\u001b[31m    const clr = spClr[sp] || 'var(--t3)';\u001b[39m\n\u001b[31m    // Update stored dispatch if this docket exists in our records\u001b[39m\n\u001b[31m    for (const d of dispatches) {\u001b[39m\n\u001b[31m      for (const c of (d.consignees||[])) {\u001b[39m\n\u001b[31m        if (c.docket === docket && result.status && result.status !== 'failed') {\u001b[39m\n\u001b[31m          c.status = result.status;\u001b[39m\n\u001b[31m          c.lastEvent = result.lastEvent;\u001b[39m\n\u001b[31m          c.lastTrackedAt = new Date().toISOString();\u001b[39m\n\u001b[31m        }\u001b[39m\n\u001b[31m      }\u001b[39m\n\u001b[31m    }\u001b[39m\n\u001b[31m    resultsHTML += `\u001b[39m\n\u001b[31m    <div style=\\\"background:var(--sur);border:1px solid var(--bdr);border-left:3px solid ${clr};\u001b[39m\n\u001b[31m      border-radius:var(--r-sm);padding:10px 12px;margin-bottom:8px;\\\">\u001b[39m\n\u001b[31m      <div style=\\\"display:flex;justify-content:space-between;align-items:center;margin-bottom:6px;\\\">\u001b[39m\n\u001b[31m        <span style=\\\"font-family:var(--fnt-mono);font-size:.9rem;color:var(--sky);\\\">${docket}</span>\u001b[39m\n\u001b[31m        <span class=\\\"status-pill sp-${sp}\\\">${sp}</span>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      <div style=\\\"font-size:.75rem;color:var(--t2);font-family:var(--fnt-mono);\\\">${result.lastEvent || '—'}</div>\u001b[39m\n\u001b[31m      ${result.lastScanLocation ? `<div style=\\\"font-size:.68rem;color:var(--t3);margin-top:3px;\\\">📍 ${result.lastScanLocation}</div>` : ''}\u001b[39m\n\u001b[31m      <div style=\\\"display:flex;gap:8px;margin-top:8px;\\\">\u001b[39m\n\u001b[31m        <button class=\\\"btn-sm\\\" onclick=\\\"openCourierTrackUrl('${docket}','${courier}')\\\">🔗 Open on ${courier}</button>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m    </div>`;\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m  saveData();\u001b[39m\n\u001b[31m  area.innerHTML = `<div style=\\\"margin-top:4px;\\\">${resultsHTML}</div>`;\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// Keep old names as aliases for compatibility\u001b[39m\n\u001b[31mfunction trackMultiple() { trackSelected(); }\u001b[39m\n\u001b[31mfunction openCourierSite() { openTrackOnCourierSite(); }·\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31m// HISTORY\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31mfunction renderHistory() {\u001b[39m\n\u001b[31m  const list = document.getElementById('historyList');\u001b[39m\n\u001b[31m  if (!dispatches.length) {\u001b[39m\n\u001b[31m    list.innerHTML = '<div class=\\\"empty\\\"><div class=\\\"e-icon\\\">📭</div><p>No dispatches yet.</p></div>';\u001b[39m\n\u001b[31m    return;\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m  list.innerHTML = dispatches.map(d => {\u001b[39m\n\u001b[31m    const cons = d.consignees?.length || 0;\u001b[39m\n\u001b[31m    const docketed = d.consignees?.filter(c=>c.docket).length || 0;\u001b[39m\n\u001b[31m    const clr = d.status === 'draft' ? 'var(--ylw)' : 'var(--grn)';\u001b[39m\n\u001b[31m    return `\u001b[39m\n\u001b[31m    <div class=\\\"docket-tile\\\" onclick=\\\"showDispatchDetail('${d.id}')\\\">\u001b[39m\n\u001b[31m      <div class=\\\"docket-tile-hdr\\\">\u001b[39m\n\u001b[31m        <div class=\\\"docket-tile-left\\\">\u001b[39m\n\u001b[31m          <div class=\\\"docket-num\\\" style=\\\"font-size:.78rem;\\\">${d.id}</div>\u001b[39m\n\u001b[31m          <div class=\\\"docket-store\\\">${d.client||'No client'} ${d.jobref ? '› '+d.jobref : ''}</div>\u001b[39m\n\u001b[31m          <div class=\\\"docket-courier\\\">📦 ${cons} consignees · ${docketed} docketed · ${d.courier||'—'} · ${d.dispatchDate||d.createdAt?.slice(0,10)||'—'}</div>\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m        <span class=\\\"status-pill\\\" style=\\\"background:rgba(0,0,0,.2);color:${clr};\\\">${d.status||'active'}</span>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m    </div>`;\u001b[39m\n\u001b[31m  }).join('');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction showDispatchDetail(id) {\u001b[39m\n\u001b[31m  const d = dispatches.find(x => x.id === id);\u001b[39m\n\u001b[31m  if (!d) return;\u001b[39m\n\u001b[31m  const content = document.getElementById('detailContent');\u001b[39m\n\u001b[31m  content.innerHTML = `\u001b[39m\n\u001b[31m    <div class=\\\"sheet-title\\\">${d.client} ${d.jobref ? '› '+d.jobref : ''}</div>\u001b[39m\n\u001b[31m    <div class=\\\"info-tag\\\"><strong>${d.id}</strong> · ${d.courier||'—'} · ${d.dispatchDate||'—'}</div>\u001b[39m\n\u001b[31m    <div class=\\\"sec-div\\\"><span>Consignees (${d.consignees?.length||0})</span></div>\u001b[39m\n\u001b[31m    ${(d.consignees||[]).map((c,i) => `\u001b[39m\n\u001b[31m      <div style=\\\"background:var(--sur2);border:1px solid var(--bdr);border-radius:var(--r-sm);padding:10px;margin-bottom:6px;\\\">\u001b[39m\n\u001b[31m        <div style=\\\"font-weight:600;margin-bottom:4px;\\\">${i+1}. ${c.name} — ${c.city}</div>\u001b[39m\n\u001b[31m        ${c.docket ? `<div style=\\\"font-family:var(--fnt-mono);font-size:.78rem;color:var(--sky);\\\">🔖 ${c.docket}</div>` : ''}\u001b[39m\n\u001b[31m        ${c.material ? `<div style=\\\"font-size:.75rem;color:var(--t2);\\\">📦 ${c.material} ${c.qty ? '('+c.qty+')':''}</div>` : ''}\u001b[39m\n\u001b[31m        <span class=\\\"status-pill sp-${c.status||'pending'}\\\" style=\\\"margin-top:4px;display:inline-block;\\\">${c.status||'pending'}</span>\u001b[39m\n\u001b[31m      </div>`).join('')}\u001b[39m\n\u001b[31m    ${d.remarks ? `<div class=\\\"info-tag\\\">📝 ${d.remarks}</div>` : ''}\u001b[39m\n\u001b[31m    <div class=\\\"btn-row\\\" style=\\\"margin-top:14px;\\\">\u001b[39m\n\u001b[31m      <button class=\\\"btn btn-ghost\\\" onclick=\\\"deleteDispatch('${d.id}');closeOverlay('detailOverlay')\\\">🗑️ Delete</button>\u001b[39m\n\u001b[31m      <button class=\\\"btn btn-orange\\\" onclick=\\\"closeOverlay('detailOverlay');showPane('dashboard')\\\">📊 View in Dashboard</button>\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m  `;\u001b[39m\n\u001b[31m  openOverlay('detailOverlay');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction deleteDispatch(id) {\u001b[39m\n\u001b[31m  if (!confirm('Delete this dispatch?')) return;\u001b[39m\n\u001b[31m  dispatches = dispatches.filter(d => d.id !== id);\u001b[39m\n\u001b[31m  saveData();\u001b[39m\n\u001b[31m  updateHistCount();\u001b[39m\n\u001b[31m  renderHistory();\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction updateHistCount() {\u001b[39m\n\u001b[31m  document.getElementById('histCount').textContent = dispatches.length;\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31m// GOOGLE SHEETS\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31masync function logToSheets(d) {\u001b[39m\n\u001b[31m  const url = window.appConfig.webhook_url;\u001b[39m\n\u001b[31m  if (!url) return;\u001b[39m\n\u001b[31m  try {\u001b[39m\n\u001b[31m    await fetch(url, {\u001b[39m\n\u001b[31m      method: 'POST', mode: 'no-cors',\u001b[39m\n\u001b[31m      headers: { 'Content-Type': 'application/json' },\u001b[39m\n\u001b[31m      body: JSON.stringify({\u001b[39m\n\u001b[31m        type: 'dispatch', id: d.id, client: d.client, jobref: d.jobref,\u001b[39m\n\u001b[31m        courier: d.courier, consignees: d.consignees?.length||0,\u001b[39m\n\u001b[31m        destinations: d.consignees?.map(c=>c.city).join(', ')||'',\u001b[39m\n\u001b[31m        dockets: d.consignees?.filter(c=>c.docket).map(c=>c.docket).join(', ')||'',\u001b[39m\n\u001b[31m        dispatchDate: d.dispatchDate, status: d.status,\u001b[39m\n\u001b[31m        timestamp: new Date().toISOString(), company: window.appConfig.company\u001b[39m\n\u001b[31m      })\u001b[39m\n\u001b[31m    });\u001b[39m\n\u001b[31m  } catch(e) {}\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31m// CONFIG — Google Sheet is the source of truth\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════·\u001b[39m\n\u001b[31mfunction onSheetsUrlChange() {\u001b[39m\n\u001b[31m  const url = document.getElementById('cfg_sheets_url').value.trim();\u001b[39m\n\u001b[31m  // Show sync button state\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction saveSheetsUrl() {\u001b[39m\n\u001b[31m  const url = document.getElementById('cfg_sheets_url').value.trim();\u001b[39m\n\u001b[31m  if (!url) { showToast('Enter the Apps Script URL first', 'err'); return; }\u001b[39m\n\u001b[31m  window.appConfig.sheets_url = url;\u001b[39m\n\u001b[31m  saveSheetsUrlLocal();\u001b[39m\n\u001b[31m  syncFromSheet(false);\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m/**\u001b[39m\n\u001b[31m * Fetch all config from Google Sheet and apply to appConfig.\u001b[39m\n\u001b[31m * Called on startup (silent=true) and manually (silent=false).\u001b[39m\n\u001b[31m */\u001b[39m\n\u001b[31masync function syncFromSheet(silent = false) {\u001b[39m\n\u001b[31m  const url = window.appConfig.sheets_url;\u001b[39m\n\u001b[31m  if (!url) { if (!silent) showToast('No Sheets URL configured', 'err'); return; }·\u001b[39m\n\u001b[31m  setSyncStatus('syncing', 'Syncing config from Google Sheet...');\u001b[39m\n\u001b[31m  try {\u001b[39m\n\u001b[31m    const resp = await fetch(url + '?action=pwa_config', {\u001b[39m\n\u001b[31m      method: 'GET',\u001b[39m\n\u001b[31m      redirect: 'follow'\u001b[39m\n\u001b[31m    });\u001b[39m\n\u001b[31m    if (!resp.ok) throw new Error('HTTP ' + resp.status);\u001b[39m\n\u001b[31m    const cfg = await resp.json();·\u001b[39m\n\u001b[31m    // Apply all values from sheet to appConfig\u001b[39m\n\u001b[31m    const keyMap = {\u001b[39m\n\u001b[31m      proxy_url:           'proxy_url',\u001b[39m\n\u001b[31m      webhook_url:         'webhook_url',\u001b[39m\n\u001b[31m      ollama_url:          'ollama_url',\u001b[39m\n\u001b[31m      ocr_mode:            'ocr_mode',\u001b[39m\n\u001b[31m      company:             'company',\u001b[39m\n\u001b[31m      city:                'city',\u001b[39m\n\u001b[31m      auto_log:            'auto_log',\u001b[39m\n\u001b[31m      refresh_on_load:     'refresh_on_load',\u001b[39m\n\u001b[31m      admin_password:      'admin_password',\u001b[39m\n\u001b[31m      n8n_dispatch_webhook:'n8n_dispatch_webhook',\u001b[39m\n\u001b[31m    };\u001b[39m\n\u001b[31m    Object.entries(keyMap).forEach(([sheetKey, cfgKey]) => {\u001b[39m\n\u001b[31m      if (cfg[sheetKey] !== undefined && cfg[sheetKey] !== '') {\u001b[39m\n\u001b[31m        window.appConfig[cfgKey] = cfg[sheetKey];\u001b[39m\n\u001b[31m      }\u001b[39m\n\u001b[31m    });·\u001b[39m\n\u001b[31m    // Apply courier master if provided\u001b[39m\n\u001b[31m    if (cfg._courier_master && Array.isArray(cfg._courier_master)) {\u001b[39m\n\u001b[31m      cfg._courier_master.forEach(c => {\u001b[39m\n\u001b[31m        if (c.name && c.trackUrl) {\u001b[39m\n\u001b[31m          COURIER_MASTER[c.name] = {\u001b[39m\n\u001b[31m            trackUrl: c.trackUrl,\u001b[39m\n\u001b[31m            trackBulkUrl: c.bulkUrl || '',\u001b[39m\n\u001b[31m            maxBulk: parseInt(c.maxBulk) || 1\u001b[39m\n\u001b[31m          };\u001b[39m\n\u001b[31m        }\u001b[39m\n\u001b[31m      });\u001b[39m\n\u001b[31m    }·\u001b[39m\n\u001b[31m    renderCourierMasterList();\u001b[39m\n\u001b[31m    renderSheetConfigDisplay(cfg);\u001b[39m\n\u001b[31m    setSyncStatus('ok', 'Synced from sheet · ' + new Date().toLocaleTimeString());\u001b[39m\n\u001b[31m    if (!silent) showToast('Config synced from sheet ✓', 'ok');·\u001b[39m\n\u001b[31m    // If refresh_on_load is true, trigger dashboard refresh\u001b[39m\n\u001b[31m    if (window.appConfig.refresh_on_load === 'true') {\u001b[39m\n\u001b[31m      setTimeout(refreshAllTracking, 2000);\u001b[39m\n\u001b[31m    }·\u001b[39m\n\u001b[31m  } catch(e) {\u001b[39m\n\u001b[31m    setSyncStatus('err', 'Sync failed: ' + e.message);\u001b[39m\n\u001b[31m    if (!silent) showToast('Sync failed — check URL', 'err');\u001b[39m\n\u001b[31m    console.error('Sheets sync error:', e);\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction setSyncStatus(state, msg) {\u001b[39m\n\u001b[31m  const el = document.getElementById('syncStatus');\u001b[39m\n\u001b[31m  const icon = document.getElementById('syncIcon');\u001b[39m\n\u001b[31m  const msgEl = document.getElementById('syncMsg');\u001b[39m\n\u001b[31m  el.style.display = 'flex';\u001b[39m\n\u001b[31m  icon.textContent = state === 'ok' ? '✅' : state === 'err' ? '❌' : '⏳';\u001b[39m\n\u001b[31m  msgEl.textContent = msg;\u001b[39m\n\u001b[31m  el.style.borderColor = state === 'ok' ? 'rgba(34,197,94,.3)' : state === 'err' ? 'rgba(239,68,68,.3)' : 'rgba(234,179,8,.3)';\u001b[39m\n\u001b[31m  el.style.background = state === 'ok' ? 'rgba(34,197,94,.05)' : state === 'err' ? 'rgba(239,68,68,.05)' : 'rgba(234,179,8,.05)';\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction renderSheetConfigDisplay(cfg) {\u001b[39m\n\u001b[31m  const display = document.getElementById('sheetConfigDisplay');\u001b[39m\n\u001b[31m  display.style.display = 'block';\u001b[39m\n\u001b[31m  document.getElementById('sheetSyncTime').textContent = 'Last sync: ' + new Date().toLocaleTimeString();\u001b[39m\n\u001b[31m  const showKeys = ['proxy_url','webhook_url','ollama_url','ocr_mode','company','city','auto_log'];\u001b[39m\n\u001b[31m  document.getElementById('sheetConfigRows').innerHTML = showKeys.map(k => {\u001b[39m\n\u001b[31m    const val = cfg[k] || window.appConfig[k] || '—';\u001b[39m\n\u001b[31m    const isEmpty = !cfg[k];\u001b[39m\n\u001b[31m    return `<div class=\\\"courier-master-row\\\">\u001b[39m\n\u001b[31m      <div class=\\\"cml\\\">\u001b[39m\n\u001b[31m        <div class=\\\"cml-name\\\" style=\\\"font-family:var(--fnt-mono);font-size:.75rem;\\\">${k}</div>\u001b[39m\n\u001b[31m        <div class=\\\"cml-url\\\" style=\\\"${isEmpty ? 'color:var(--red);' : ''}\\\">${isEmpty ? '⚠️ Not set in sheet' : val}</div>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m    </div>`;\u001b[39m\n\u001b[31m  }).join('');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m/**\u001b[39m\n\u001b[31m * Push current config changes back to the sheet.\u001b[39m\n\u001b[31m * Called when user edits config from PWA (rare — usually done in sheet directly).\u001b[39m\n\u001b[31m */\u001b[39m\n\u001b[31masync function pushConfigToSheet(updates) {\u001b[39m\n\u001b[31m  const url = window.appConfig.sheets_url;\u001b[39m\n\u001b[31m  if (!url) return;\u001b[39m\n\u001b[31m  try {\u001b[39m\n\u001b[31m    await fetch(url, {\u001b[39m\n\u001b[31m      method: 'POST', mode: 'no-cors',\u001b[39m\n\u001b[31m      headers: { 'Content-Type': 'application/json' },\u001b[39m\n\u001b[31m      body: JSON.stringify({ type: 'save_config', config: updates })\u001b[39m\n\u001b[31m    });\u001b[39m\n\u001b[31m  } catch(e) {}\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction clearAll() {\u001b[39m\n\u001b[31m  if (!confirm('Delete ALL dispatch data?')) return;\u001b[39m\n\u001b[31m  dispatches = [];\u001b[39m\n\u001b[31m  saveData();\u001b[39m\n\u001b[31m  updateHistCount();\u001b[39m\n\u001b[31m  renderDashboard();\u001b[39m\n\u001b[31m  renderHistory();\u001b[39m\n\u001b[31m  showToast('Cleared', 'ok');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction renderCourierMasterList() {\u001b[39m\n\u001b[31m  const el = document.getElementById('courierMasterList');\u001b[39m\n\u001b[31m  el.innerHTML = Object.entries(COURIER_MASTER).map(([name, info]) => `\u001b[39m\n\u001b[31m    <div class=\\\"courier-master-row\\\">\u001b[39m\n\u001b[31m      <div class=\\\"cml\\\">\u001b[39m\n\u001b[31m        <div class=\\\"cml-name\\\">${name}</div>\u001b[39m\n\u001b[31m        <div class=\\\"cml-url\\\">${info.trackUrl}</div>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      <button class=\\\"btn-sm\\\" onclick=\\\"window.open('${info.trackBulkUrl||info.trackUrl.replace('{docket}','')}','_blank')\\\">Open</button>\u001b[39m\n\u001b[31m    </div>`).join('');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction openCourierMaster() {\u001b[39m\n\u001b[31m  const ed = document.getElementById('courierMasterEditor');\u001b[39m\n\u001b[31m  ed.innerHTML = Object.entries(COURIER_MASTER).map(([name, info]) => `\u001b[39m\n\u001b[31m    <div style=\\\"margin-bottom:14px;\\\">\u001b[39m\n\u001b[31m      <div style=\\\"font-family:var(--fnt-head);font-size:.75rem;font-weight:700;color:var(--t2);text-transform:uppercase;letter-spacing:1.5px;margin-bottom:6px;\\\">${name}</div>\u001b[39m\n\u001b[31m      <div class=\\\"f\\\" style=\\\"margin-bottom:6px;\\\">\u001b[39m\n\u001b[31m        <label>Track URL (use {docket})</label>\u001b[39m\n\u001b[31m        <input type=\\\"text\\\" value=\\\"${info.trackUrl}\\\" id=\\\"cm_${name.replace(/\\\\s/g,'_')}\\\"\u001b[39m\n\u001b[31m          style=\\\"font-family:var(--fnt-mono);font-size:.75rem;\\\">\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m    </div>`).join('') +\u001b[39m\n\u001b[31m    `<button class=\\\"btn btn-orange\\\" onclick=\\\"saveCourierMaster()\\\">Save Courier Master</button>`;\u001b[39m\n\u001b[31m  openOverlay('courierMasterOverlay');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction saveCourierMaster() {\u001b[39m\n\u001b[31m  Object.keys(COURIER_MASTER).forEach(name => {\u001b[39m\n\u001b[31m    const el = document.getElementById('cm_' + name.replace(/\\\\s/g,'_'));\u001b[39m\n\u001b[31m    if (el) COURIER_MASTER[name].trackUrl = el.value;\u001b[39m\n\u001b[31m  });\u001b[39m\n\u001b[31m  closeOverlay('courierMasterOverlay');\u001b[39m\n\u001b[31m  renderCourierMasterList();\u001b[39m\n\u001b[31m  showToast('Courier master saved ✓', 'ok');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31m// UI UTILS\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31m// ── ADMIN AUTH ───────────────────────────────────────────────────────────\u001b[39m\n\u001b[31mlet _adminAuthed = false;\u001b[39m\n\u001b[31mlet _lastAdminPane = null;·\u001b[39m\n\u001b[31mfunction showPane(name) {\u001b[39m\n\u001b[31m  // Config tab requires admin auth — re-ask if navigated away\u001b[39m\n\u001b[31m  if (name === 'config') {\u001b[39m\n\u001b[31m    if (!_adminAuthed) {\u001b[39m\n\u001b[31m      _lastAdminPane = 'config';\u001b[39m\n\u001b[31m      showAdminLogin();\u001b[39m\n\u001b[31m      return;\u001b[39m\n\u001b[31m    }\u001b[39m\n\u001b[31m  } else {\u001b[39m\n\u001b[31m    // User navigated away from config — revoke auth\u001b[39m\n\u001b[31m    if (_lastAdminPane === 'config') _adminAuthed = false;\u001b[39m\n\u001b[31m    _lastAdminPane = name;\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m  document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));\u001b[39m\n\u001b[31m  document.querySelectorAll('.pane').forEach(p => p.classList.remove('active'));\u001b[39m\n\u001b[31m  document.getElementById('tab-'+name).classList.add('active');\u001b[39m\n\u001b[31m  document.getElementById('pane-'+name).classList.add('active');\u001b[39m\n\u001b[31m  if (name === 'dashboard') renderDashboard();\u001b[39m\n\u001b[31m  if (name === 'history') renderHistory();\u001b[39m\n\u001b[31m  if (name === 'track') buildTrackDocketChips();\u001b[39m\n\u001b[31m  if (name === 'config') { document.getElementById('cfg_sheets_url').value = window.appConfig.sheets_url || ''; }\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction showAdminLogin() {\u001b[39m\n\u001b[31m  // Password replaced by hub identity — only harish/pramod can access config\u001b[39m\n\u001b[31m  if (['harish', 'pramod'].includes(window._hubUser?.id)) {\u001b[39m\n\u001b[31m    _adminAuthed = true;\u001b[39m\n\u001b[31m    showPane('config');\u001b[39m\n\u001b[31m  } else {\u001b[39m\n\u001b[31m    showToast('Config access restricted to admin users', 'err');\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31masync function submitAdminLogin() {\u001b[39m\n\u001b[31m  showAdminLogin(); // no-op shim — kept for any lingering HTML onclick refs\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31mfunction openOverlay(id) { document.getElementById(id).classList.add('open'); }\u001b[39m\n\u001b[31mfunction closeOverlay(id) { document.getElementById(id).classList.remove('open'); }·\u001b[39m\n\u001b[31mlet toastTimer;\u001b[39m\n\u001b[31mfunction showToast(msg, type='') {\u001b[39m\n\u001b[31m  const t = document.getElementById('toast');\u001b[39m\n\u001b[31m  t.textContent = msg;\u001b[39m\n\u001b[31m  t.className = 'toast show' + (type ? ' '+type : '');\u001b[39m\n\u001b[31m  clearTimeout(toastTimer);\u001b[39m\n\u001b[31m  toastTimer = setTimeout(() => t.classList.remove('show'), 2800);\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction showOCRWorking(show, msg='') {\u001b[39m\n\u001b[31m  const el = document.getElementById('ocrWorking');\u001b[39m\n\u001b[31m  el.classList.toggle('visible', show);\u001b[39m\n\u001b[31m  if (msg) document.getElementById('ocrWorkingText').textContent = msg;\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction showAIProgress(label, pct) {\u001b[39m\n\u001b[31m  const el = document.getElementById('aiProgress');\u001b[39m\n\u001b[31m  el.classList.add('visible');\u001b[39m\n\u001b[31m  document.getElementById('aiProgLabel').textContent = label;\u001b[39m\n\u001b[31m  document.getElementById('aiProgFill').style.width = pct + '%';\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31mfunction hideAIProgress() {\u001b[39m\n\u001b[31m  document.getElementById('aiProgress').classList.remove('visible');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction updateNetDot() {\u001b[39m\n\u001b[31m  document.getElementById('netDot').style.background = navigator.onLine ? 'var(--grn)' : 'var(--red)';\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction setToday() {\u001b[39m\n\u001b[31m  document.getElementById('n_date').value = new Date().toISOString().split('T')[0];\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m// ── EXPORT CLEAN SHEET ───────────────────────────────────────────────────\u001b[39m\n\u001b[31mfunction exportCleanSheet() {\u001b[39m\n\u001b[31m  const pane = document.getElementById('verifyPane');\u001b[39m\n\u001b[31m  const d = pane._pendingDispatch;\u001b[39m\n\u001b[31m  if (!d || !d.consignees?.length) { showToast('No data to export', 'err'); return; }·\u001b[39m\n\u001b[31m  // Build CSV\u001b[39m\n\u001b[31m  const headers = ['S.No.','Store Name','City','Pincode','Phone','Address','Docket No.','Material','Courier','Status'];\u001b[39m\n\u001b[31m  const rows = d.consignees.map((c, i) => [\u001b[39m\n\u001b[31m    i + 1,\u001b[39m\n\u001b[31m    `\\\"${(c.name||'').replace(/\\\"/g,'\\\"\\\"')}\\\"`,\u001b[39m\n\u001b[31m    `\\\"${(c.city||'').replace(/\\\"/g,'\\\"\\\"')}\\\"`,\u001b[39m\n\u001b[31m    c.pincode || '',\u001b[39m\n\u001b[31m    c.phone || '',\u001b[39m\n\u001b[31m    `\\\"${(c.address||'').replace(/\\\"/g,'\\\"\\\"')}\\\"`,\u001b[39m\n\u001b[31m    c.docket || '',\u001b[39m\n\u001b[31m    `\\\"${(c.material||'').replace(/\\\"/g,'\\\"\\\"')}\\\"`,\u001b[39m\n\u001b[31m    d.courier || '',\u001b[39m\n\u001b[31m    c.status || 'pending'\u001b[39m\n\u001b[31m  ]);·\u001b[39m\n\u001b[31m  const csv = [headers.join(','), ...rows.map(r => r.join(','))].join('\\\\n');\u001b[39m\n\u001b[31m  const blob = new Blob([csv], { type: 'text/csv' });\u001b[39m\n\u001b[31m  const url = URL.createObjectURL(blob);\u001b[39m\n\u001b[31m  const a = document.createElement('a');\u001b[39m\n\u001b[31m  a.href = url;\u001b[39m\n\u001b[31m  a.download = `360LM_Dispatch_${d.client||'export'}_${d.dispatchDate||new Date().toISOString().slice(0,10)}_clean.csv`;\u001b[39m\n\u001b[31m  a.click();\u001b[39m\n\u001b[31m  URL.revokeObjectURL(url);\u001b[39m\n\u001b[31m  showToast('Clean sheet exported ✓', 'ok');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// ── DOCKET SLIP UPLOAD — SKIP OCR IF DOCKET ALREADY MAPPED ───────────────\u001b[39m\n\u001b[31mfunction shouldSkipOCR() {\u001b[39m\n\u001b[31m  // If any consignee already has a docket (from store list mapping), skip OCR extraction\u001b[39m\n\u001b[31m  const consList = document.getElementById('consList');\u001b[39m\n\u001b[31m  if (!consList) return false;\u001b[39m\n\u001b[31m  const cards = consList.querySelectorAll('.cons-card');\u001b[39m\n\u001b[31m  let hasAnyDocket = false;\u001b[39m\n\u001b[31m  cards.forEach(card => {\u001b[39m\n\u001b[31m    const d = card.querySelector('.cons-docket-input')?.value?.trim();\u001b[39m\n\u001b[31m    if (d) hasAnyDocket = true;\u001b[39m\n\u001b[31m  });\u001b[39m\n\u001b[31m  return hasAnyDocket;\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// ─── TRACKING UPDATE TO SHEETS ──────────────────────────────────────────\u001b[39m\n\u001b[31masync function logTrackingUpdate(docket, status, lastEvent, location) {\u001b[39m\n\u001b[31m  const url = window.appConfig.webhook_url;\u001b[39m\n\u001b[31m  if (!url) return;\u001b[39m\n\u001b[31m  try {\u001b[39m\n\u001b[31m    await fetch(url, {\u001b[39m\n\u001b[31m      method: 'POST', mode: 'no-cors',\u001b[39m\n\u001b[31m      headers: { 'Content-Type': 'application/json' },\u001b[39m\n\u001b[31m      body: JSON.stringify({\u001b[39m\n\u001b[31m        type: 'tracking_update',\u001b[39m\n\u001b[31m        docket, status, lastEvent, location,\u001b[39m\n\u001b[31m        timestamp: new Date().toISOString()\u001b[39m\n\u001b[31m      })\u001b[39m\n\u001b[31m    });\u001b[39m\n\u001b[31m  } catch(e) {}\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// ─── POD / RECIPIENT PHOTO UPLOAD ───────────────────────────────────────\u001b[39m\n\u001b[31m// Stores base64 locally, pushes to Sheets webhook (Apps Script saves to Drive)\u001b[39m\n\u001b[31mfunction showPODUpload(docket, dispatchId) {\u001b[39m\n\u001b[31m  const overlay = document.getElementById('podOverlay');\u001b[39m\n\u001b[31m  overlay.dataset.docket = docket;\u001b[39m\n\u001b[31m  overlay.dataset.dispatchId = dispatchId;\u001b[39m\n\u001b[31m  document.getElementById('podPreview').innerHTML = '';\u001b[39m\n\u001b[31m  document.getElementById('podFileInput').value = '';\u001b[39m\n\u001b[31m  document.getElementById('recipientFileInput').value = '';\u001b[39m\n\u001b[31m  openOverlay('podOverlay');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31masync function uploadPODFiles() {\u001b[39m\n\u001b[31m  const overlay = document.getElementById('podOverlay');\u001b[39m\n\u001b[31m  const docket = overlay.dataset.docket;\u001b[39m\n\u001b[31m  const dispatchId = overlay.dataset.dispatchId;\u001b[39m\n\u001b[31m  const podFile = document.getElementById('podFileInput').files[0];\u001b[39m\n\u001b[31m  const recipFile = document.getElementById('recipientFileInput').files[0];\u001b[39m\n\u001b[31m  if (!podFile && !recipFile) { showToast('Select at least one photo', 'err'); return; }·\u001b[39m\n\u001b[31m  showToast('Uploading...', 'info');\u001b[39m\n\u001b[31m  const toBase64 = f => new Promise((res,rej) => {\u001b[39m\n\u001b[31m    const r = new FileReader(); r.onload = e => res(e.target.result); r.onerror = rej; r.readAsDataURL(f);\u001b[39m\n\u001b[31m  });·\u001b[39m\n\u001b[31m  const payload = {\u001b[39m\n\u001b[31m    type: 'pod_upload',\u001b[39m\n\u001b[31m    docket, dispatchId,\u001b[39m\n\u001b[31m    timestamp: new Date().toISOString()\u001b[39m\n\u001b[31m  };\u001b[39m\n\u001b[31m  if (podFile)   payload.pod_image = await toBase64(podFile);\u001b[39m\n\u001b[31m  if (recipFile) payload.recipient_image = await toBase64(recipFile);·\u001b[39m\n\u001b[31m  // Update local store\u001b[39m\n\u001b[31m  for (const d of dispatches) {\u001b[39m\n\u001b[31m    if (d.id !== dispatchId) continue;\u001b[39m\n\u001b[31m    for (const c of d.consignees) {\u001b[39m\n\u001b[31m      if (c.docket === docket) {\u001b[39m\n\u001b[31m        c.podUploaded = true;\u001b[39m\n\u001b[31m        c.status = 'delivered';\u001b[39m\n\u001b[31m      }\u001b[39m\n\u001b[31m    }\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m  saveData();·\u001b[39m\n\u001b[31m  // Push to Sheets (Apps Script saves images to Drive, stores URL in sheet)\u001b[39m\n\u001b[31m  if (window.appConfig.webhook_url) {\u001b[39m\n\u001b[31m    try {\u001b[39m\n\u001b[31m      await fetch(window.appConfig.webhook_url, {\u001b[39m\n\u001b[31m        method: 'POST', mode: 'no-cors',\u001b[39m\n\u001b[31m        headers: { 'Content-Type': 'application/json' },\u001b[39m\n\u001b[31m        body: JSON.stringify(payload)\u001b[39m\n\u001b[31m      });\u001b[39m\n\u001b[31m    } catch(e) {}\u001b[39m\n\u001b[31m  }·\u001b[39m\n\u001b[31m  closeOverlay('podOverlay');\u001b[39m\n\u001b[31m  renderDashboard();\u001b[39m\n\u001b[31m  showToast('POD uploaded ✓ — marked as delivered', 'ok');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// ─── WHATSAPP DISPATCH TABLE SHARE ──────────────────────────────────────\u001b[39m\n\u001b[31mfunction shareDispatchToWhatsApp(dispatchId) {\u001b[39m\n\u001b[31m  const d = dispatches.find(x => x.id === dispatchId);\u001b[39m\n\u001b[31m  if (!d) return;\u001b[39m\n\u001b[31m  const lines = [\u001b[39m\n\u001b[31m    `📦 *Dispatch Update — ${d.client} ${d.jobref ? '› '+d.jobref : ''}*`,\u001b[39m\n\u001b[31m    `🚚 Courier: ${d.courier} | Date: ${d.dispatchDate || d.createdAt?.slice(0,10)}`,\u001b[39m\n\u001b[31m    ``,\u001b[39m\n\u001b[31m    ...d.consignees.filter(c => c.docket).map((c,i) =>\u001b[39m\n\u001b[31m      `${i+1}. *${c.name}* — ${c.city}\\\\n   Docket: ${c.docket} | Status: ${c.status||'booked'}`\u001b[39m\n\u001b[31m    ),\u001b[39m\n\u001b[31m    ``,\u001b[39m\n\u001b[31m    `_360 Degree Logical Marketing_`\u001b[39m\n\u001b[31m  ];\u001b[39m\n\u001b[31m  const msg = encodeURIComponent(lines.join('\\\\n'));\u001b[39m\n\u001b[31m  window.open('https://wa.me/?text=' + msg, '_blank');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// Trigger n8n webhook for automated WhatsApp/email share\u001b[39m\n\u001b[31masync function triggerN8nShare(dispatchId, channel) {\u001b[39m\n\u001b[31m  const d = dispatches.find(x => x.id === dispatchId);\u001b[39m\n\u001b[31m  if (!d) return;\u001b[39m\n\u001b[31m  const n8nUrl = window.appConfig.n8n_dispatch_webhook;\u001b[39m\n\u001b[31m  if (!n8nUrl) { showToast('n8n webhook not configured in Config sheet', 'err'); return; }\u001b[39m\n\u001b[31m  try {\u001b[39m\n\u001b[31m    await fetch(n8nUrl, {\u001b[39m\n\u001b[31m      method: 'POST',\u001b[39m\n\u001b[31m      headers: { 'Content-Type': 'application/json' },\u001b[39m\n\u001b[31m      body: JSON.stringify({\u001b[39m\n\u001b[31m        channel, // 'whatsapp' or 'email'\u001b[39m\n\u001b[31m        dispatch: d,\u001b[39m\n\u001b[31m        timestamp: new Date().toISOString()\u001b[39m\n\u001b[31m      })\u001b[39m\n\u001b[31m    });\u001b[39m\n\u001b[31m    showToast(`Sent to n8n for ${channel} delivery ✓`, 'ok');\u001b[39m\n\u001b[31m  } catch(e) {\u001b[39m\n\u001b[31m    showToast('n8n webhook failed', 'err');\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction previewPOD(input, type) {\u001b[39m\n\u001b[31m  const f = input.files[0];\u001b[39m\n\u001b[31m  if (!f) return;\u001b[39m\n\u001b[31m  const r = new FileReader();\u001b[39m\n\u001b[31m  r.onload = e => {\u001b[39m\n\u001b[31m    const prev = document.getElementById('podPreview');\u001b[39m\n\u001b[31m    const existing = document.getElementById('pod_prev_' + type);\u001b[39m\n\u001b[31m    if (existing) existing.remove();\u001b[39m\n\u001b[31m    const img = document.createElement('img');\u001b[39m\n\u001b[31m    img.id = 'pod_prev_' + type;\u001b[39m\n\u001b[31m    img.src = e.target.result;\u001b[39m\n\u001b[31m    img.style.cssText = 'width:80px;height:80px;object-fit:cover;border-radius:6px;border:1px solid var(--bdr);';\u001b[39m\n\u001b[31m    prev.appendChild(img);\u001b[39m\n\u001b[31m  };\u001b[39m\n\u001b[31m  r.readAsDataURL(f);\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// ─── MANUAL TRACKING ─────────────────────────────────────────────────────\u001b[39m\n\u001b[31mfunction showManualTracking(docket, dispatchId, storeName, phone, city) {\u001b[39m\n\u001b[31m  const ov = document.getElementById('manualTrackOverlay');\u001b[39m\n\u001b[31m  ov.dataset.docket = docket;\u001b[39m\n\u001b[31m  ov.dataset.dispatchId = dispatchId;\u001b[39m\n\u001b[31m  ov.dataset.storeName = storeName;\u001b[39m\n\u001b[31m  ov.dataset.city = city;·\u001b[39m\n\u001b[31m  document.getElementById('mtDocketDisplay').textContent =\u001b[39m\n\u001b[31m    docket + '  ·  ' + storeName + '  ·  ' + city;\u001b[39m\n\u001b[31m  document.getElementById('mtContactUsed').value = phone || '';\u001b[39m\n\u001b[31m  document.getElementById('mtContactPerson').value = '';\u001b[39m\n\u001b[31m  document.getElementById('mtRemarks').value = '';\u001b[39m\n\u001b[31m  document.getElementById('mtStatus').value = '';\u001b[39m\n\u001b[31m  document.getElementById('mtCalledBy').value = '';·\u001b[39m\n\u001b[31m  // Set current datetime\u001b[39m\n\u001b[31m  const now = new Date();\u001b[39m\n\u001b[31m  const pad = n => String(n).padStart(2,'0');\u001b[39m\n\u001b[31m  document.getElementById('mtCallTime').value =\u001b[39m\n\u001b[31m    `${now.getFullYear()}-${pad(now.getMonth()+1)}-${pad(now.getDate())}T${pad(now.getHours())}:${pad(now.getMinutes())}`;·\u001b[39m\n\u001b[31m  openOverlay('manualTrackOverlay');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31masync function submitManualTracking() {\u001b[39m\n\u001b[31m  const ov = document.getElementById('manualTrackOverlay');\u001b[39m\n\u001b[31m  const docket     = ov.dataset.docket;\u001b[39m\n\u001b[31m  const dispatchId = ov.dataset.dispatchId;\u001b[39m\n\u001b[31m  const status     = document.getElementById('mtStatus').value;\u001b[39m\n\u001b[31m  const calledBy   = document.getElementById('mtCalledBy').value.trim();·\u001b[39m\n\u001b[31m  if (!status)    { showToast('Select a status', 'err'); return; }\u001b[39m\n\u001b[31m  if (!calledBy)  { showToast('Enter your name', 'err'); return; }·\u001b[39m\n\u001b[31m  const payload = {\u001b[39m\n\u001b[31m    type:          'manual_tracking',\u001b[39m\n\u001b[31m    docket,        dispatchId,\u001b[39m\n\u001b[31m    storeName:     ov.dataset.storeName,\u001b[39m\n\u001b[31m    city:          ov.dataset.city,\u001b[39m\n\u001b[31m    manualStatus:  status,\u001b[39m\n\u001b[31m    calledBy,\u001b[39m\n\u001b[31m    contactUsed:   document.getElementById('mtContactUsed').value.trim(),\u001b[39m\n\u001b[31m    contactPerson: document.getElementById('mtContactPerson').value.trim(),\u001b[39m\n\u001b[31m    remarks:       document.getElementById('mtRemarks').value.trim(),\u001b[39m\n\u001b[31m    callTime:      document.getElementById('mtCallTime').value || new Date().toISOString(),\u001b[39m\n\u001b[31m    timestamp:     new Date().toISOString()\u001b[39m\n\u001b[31m  };·\u001b[39m\n\u001b[31m  // Update local dispatch record\u001b[39m\n\u001b[31m  for (const d of dispatches) {\u001b[39m\n\u001b[31m    if (d.id !== dispatchId) continue;\u001b[39m\n\u001b[31m    for (const c of d.consignees) {\u001b[39m\n\u001b[31m      if (c.docket === docket) {\u001b[39m\n\u001b[31m        c.manualStatus    = status;\u001b[39m\n\u001b[31m        c.manualCalledBy  = calledBy;\u001b[39m\n\u001b[31m        c.manualTime      = payload.callTime;\u001b[39m\n\u001b[31m        c.manualRemarks   = payload.remarks;\u001b[39m\n\u001b[31m        // If confirmed delivered — flag so dashboard shows manual badge\u001b[39m\n\u001b[31m        // (not permanently locked — use resumeAutoTracking() to re-enable)\u001b[39m\n\u001b[31m        if (status === 'delivered') {\u001b[39m\n\u001b[31m          c.status = 'delivered';\u001b[39m\n\u001b[31m          c.manualDelivered = true;\u001b[39m\n\u001b[31m        } else {\u001b[39m\n\u001b[31m          // Non-delivered manual update: clear any previous lock\u001b[39m\n\u001b[31m          c.manualDelivered = false;\u001b[39m\n\u001b[31m        }\u001b[39m\n\u001b[31m      }\u001b[39m\n\u001b[31m    }\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m  saveData();\u001b[39m\n\u001b[31m  renderDashboard();\u001b[39m\n\u001b[31m  // Sync updated consignee to Postgres\u001b[39m\n\u001b[31m  const _d = dispatches.find(x => x.id === dispatchId);\u001b[39m\n\u001b[31m  if (_d) {\u001b[39m\n\u001b[31m    const _idx = _d.consignees.findIndex(c => c.docket === docket);\u001b[39m\n\u001b[31m    if (_idx >= 0) syncConsigneeToDB(_d.consignees[_idx], dispatchId, _idx);\u001b[39m\n\u001b[31m  }·\u001b[39m\n\u001b[31m  // Log to Sheets\u001b[39m\n\u001b[31m  if (window.appConfig.webhook_url) {\u001b[39m\n\u001b[31m    try {\u001b[39m\n\u001b[31m      await fetch(window.appConfig.webhook_url, {\u001b[39m\n\u001b[31m        method: 'POST', mode: 'no-cors',\u001b[39m\n\u001b[31m        headers: { 'Content-Type': 'application/json' },\u001b[39m\n\u001b[31m        body: JSON.stringify(payload)\u001b[39m\n\u001b[31m      });\u001b[39m\n\u001b[31m    } catch(e) {}\u001b[39m\n\u001b[31m  }·\u001b[39m\n\u001b[31m  closeOverlay('manualTrackOverlay');\u001b[39m\n\u001b[31m  showToast('Manual tracking logged ✓', 'ok');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// ─── TILE SELECTION + PARTIAL REPORT SHARE ───────────────────────────────\u001b[39m\n\u001b[31mlet _selectedDockets = new Set();·\u001b[39m\n\u001b[31mfunction toggleTileSelect(docket) {\u001b[39m\n\u001b[31m  if (_selectedDockets.has(docket)) _selectedDockets.delete(docket);\u001b[39m\n\u001b[31m  else _selectedDockets.add(docket);\u001b[39m\n\u001b[31m  // Update checkbox visual\u001b[39m\n\u001b[31m  const chk = document.getElementById('sel_' + docket.replace(/[^a-z0-9]/gi,'_'));\u001b[39m\n\u001b[31m  if (chk) chk.checked = _selectedDockets.has(docket);\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction shareSelectedReport() {\u001b[39m\n\u001b[31m  if (_selectedDockets.size === 0) {\u001b[39m\n\u001b[31m    showToast('Select at least one consignee tile first', 'err');\u001b[39m\n\u001b[31m    return;\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m  const activeDis = dispatches.filter(d => d.status !== 'draft');\u001b[39m\n\u001b[31m  const allItems  = activeDis.flatMap(d =>\u001b[39m\n\u001b[31m    (d.consignees||[]).map(c => Object.assign({}, c, {courier:d.courier, client:d.client, jobref:d.jobref, dispatchId:d.id}))\u001b[39m\n\u001b[31m  );\u001b[39m\n\u001b[31m  const selected = allItems.filter(c => _selectedDockets.has(c.docket));\u001b[39m\n\u001b[31m  if (!selected.length) { showToast('Selected dockets not found', 'err'); return; }·\u001b[39m\n\u001b[31m  const NL = '\\\\n';\u001b[39m\n\u001b[31m  const statusEmoji = {\u001b[39m\n\u001b[31m    delivered:'\\\\u2705', transit:'\\\\uD83D\\\\uDE9A', 'in-transit':'\\\\uD83D\\\\uDE9A',\u001b[39m\n\u001b[31m    booked:'\\\\uD83D\\\\uDCEC', pending:'\\\\u23F3', 'not received':'\\\\u274C', forwarded:'\\\\u27A1'\u001b[39m\n\u001b[31m  };·\u001b[39m\n\u001b[31m  const header = '\\\\uD83D\\\\uDCE6 *Dispatch Status Report*' + NL +\u001b[39m\n\u001b[31m    (selected[0].client || '') + ' ' + (selected[0].jobref ? '> ' + selected[0].jobref : '') + NL +\u001b[39m\n\u001b[31m    new Date().toLocaleDateString('en-IN', {day:'2-digit',month:'short',year:'numeric'}) + NL + NL;·\u001b[39m\n\u001b[31m  const rows = selected.map(function(c, i) {\u001b[39m\n\u001b[31m    const finalStatus = c.manualStatus === 'delivered' ? 'delivered' : (c.status || 'booked');\u001b[39m\n\u001b[31m    const emoji  = statusEmoji[finalStatus] || '\\\\uD83D\\\\uDCE6';\u001b[39m\n\u001b[31m    const source = c.manualStatus ? '(manual)' : '(auto)';\u001b[39m\n\u001b[31m    const remark = c.manualRemarks ? (NL + '   Note: ' + c.manualRemarks) : '';\u001b[39m\n\u001b[31m    return (i+1) + '. *' + c.name + '* - ' + c.city + NL +\u001b[39m\n\u001b[31m      '   ' + emoji + ' ' + finalStatus + ' ' + source + NL +\u001b[39m\n\u001b[31m      '   Docket: ' + c.docket + remark;\u001b[39m\n\u001b[31m  }).join(NL + NL);·\u001b[39m\n\u001b[31m  const footer = NL + NL + '_360 Degree Logical Marketing_';\u001b[39m\n\u001b[31m  window.open('https://wa.me/?text=' + encodeURIComponent(header + rows + footer), '_blank');\u001b[39m\n\u001b[31m  showToast('Report for ' + selected.length + ' consignees ready', 'ok');\u001b[39m\n\u001b[31m}··\u001b[39m\n\u001b[31m// ── resumeAutoTracking: undo a manual-delivered lock ──────────────────────\u001b[39m\n\u001b[31mfunction resumeAutoTracking(docket, dispatchId) {\u001b[39m\n\u001b[31m  for (const d of dispatches) {\u001b[39m\n\u001b[31m    if (d.id !== dispatchId) continue;\u001b[39m\n\u001b[31m    for (const c of d.consignees) {\u001b[39m\n\u001b[31m      if (c.docket === docket) {\u001b[39m\n\u001b[31m        c.manualDelivered = false;\u001b[39m\n\u001b[31m        c.status          = 'transit';\u001b[39m\n\u001b[31m        c.manualStatus    = '';\u001b[39m\n\u001b[31m      }\u001b[39m\n\u001b[31m    }\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m  saveData();\u001b[39m\n\u001b[31m  renderDashboard();\u001b[39m\n\u001b[31m  showToast(`Auto-tracking resumed for ${docket}`, 'ok');\u001b[39m\n\u001b[31m  const _rd = dispatches.find(x => x.id === dispatchId);\u001b[39m\n\u001b[31m  if (_rd) {\u001b[39m\n\u001b[31m    const _ri = _rd.consignees.findIndex(c => c.docket === docket);\u001b[39m\n\u001b[31m    if (_ri >= 0) syncConsigneeToDB(_rd.consignees[_ri], dispatchId, _ri);\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// ── refreshAllTracking (consolidated, try/finally for safety) ─────────────\u001b[39m\n\u001b[31masync function refreshAllTracking() {\u001b[39m\n\u001b[31m  const activeDis  = dispatches.filter(d => d.status !== 'draft');\u001b[39m\n\u001b[31m  const allCons    = activeDis.flatMap(d =>\u001b[39m\n\u001b[31m    (d.consignees || []).map(c => ({ ...c, dispatchId: d.id, courier: d.courier }))\u001b[39m\n\u001b[31m  );\u001b[39m\n\u001b[31m  const withDockets = allCons.filter(c =>\u001b[39m\n\u001b[31m    c.docket && c.status !== 'delivered' && !c.manualDelivered\u001b[39m\n\u001b[31m  );\u001b[39m\n\u001b[31m  if (!withDockets.length) { showToast('No active dockets to refresh', 'info'); return; }·\u001b[39m\n\u001b[31m  showToast(`⏳ Tracking ${withDockets.length} dockets...`, 'info');\u001b[39m\n\u001b[31m  withDockets.forEach(c => {\u001b[39m\n\u001b[31m    const id    = c.docket.replace(/[^a-z0-9]/gi, '_');\u001b[39m\n\u001b[31m    const evtEl = document.getElementById('evt_'  + id);\u001b[39m\n\u001b[31m    const spnEl = document.getElementById('spin_' + id);\u001b[39m\n\u001b[31m    if (evtEl) evtEl.style.display = 'none';\u001b[39m\n\u001b[31m    if (spnEl) spnEl.style.display = 'inline';\u001b[39m\n\u001b[31m  });·\u001b[39m\n\u001b[31m  let done = 0;\u001b[39m\n\u001b[31m  try {\u001b[39m\n\u001b[31m    for (const c of withDockets) {\u001b[39m\n\u001b[31m      const result = await fetchTracking(c.docket, c.courier || '');\u001b[39m\n\u001b[31m      const id    = c.docket.replace(/[^a-z0-9]/gi, '_');·\u001b[39m\n\u001b[31m      // Batch-write result into state (never touch DOM here)\u001b[39m\n\u001b[31m      if (result.status && result.status !== 'failed') {\u001b[39m\n\u001b[31m        for (const d of dispatches) {\u001b[39m\n\u001b[31m          if (d.id !== c.dispatchId) continue;\u001b[39m\n\u001b[31m          for (const con of d.consignees) {\u001b[39m\n\u001b[31m            if (con.docket === c.docket && !con.manualDelivered) {\u001b[39m\n\u001b[31m              con.status           = result.status;\u001b[39m\n\u001b[31m              con.lastEvent        = result.lastEvent || con.lastEvent;\u001b[39m\n\u001b[31m              con.lastScanLocation = result.lastScanLocation || '';\u001b[39m\n\u001b[31m              con.lastTrackedAt    = new Date().toISOString();\u001b[39m\n\u001b[31m            }\u001b[39m\n\u001b[31m          }\u001b[39m\n\u001b[31m        }\u001b[39m\n\u001b[31m      }·\u001b[39m\n\u001b[31m      // Update tile spinner/event text in DOM (cosmetic only)\u001b[39m\n\u001b[31m      const spnEl = document.getElementById('spin_' + id);\u001b[39m\n\u001b[31m      const evtEl = document.getElementById('evt_'  + id);\u001b[39m\n\u001b[31m      if (spnEl) spnEl.style.display = 'none';\u001b[39m\n\u001b[31m      if (evtEl) { evtEl.style.display = 'inline'; evtEl.textContent = result.lastEvent || 'Checked'; }·\u001b[39m\n\u001b[31m      if (result.status && result.status !== 'failed' && window.appConfig.webhook_url) {\u001b[39m\n\u001b[31m        logTrackingUpdate(c.docket, result.status, result.lastEvent || '', result.lastScanLocation || '');\u001b[39m\n\u001b[31m      }\u001b[39m\n\u001b[31m      done++;\u001b[39m\n\u001b[31m    }\u001b[39m\n\u001b[31m    showToast(`✓ ${done} dockets refreshed`, 'ok');\u001b[39m\n\u001b[31m  } finally {\u001b[39m\n\u001b[31m    // Always persist and re-render — even if loop throws mid-way\u001b[39m\n\u001b[31m    saveData();\u001b[39m\n\u001b[31m    renderDashboard();\u001b[39m\n\u001b[31m    // Push updated consignee states to Postgres (fire-and-forget)\u001b[39m\n\u001b[31m    dispatches.filter(d => d.status !== 'draft').forEach(d => syncDispatchToDB(d));\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m</script>\u001b[39m\n\u001b[31m</body>\u001b[39m\n\u001b[31m</html>\"\u001b[39m\n    at /var/www/360lm/tests/dispatch.spec.js:53:18",
                        "location": {
                          "file": "/var/www/360lm/tests/dispatch.spec.js",
                          "column": 18,
                          "line": 53
                        },
                        "snippet": "  51 |\n  52 |   test('hub auth gate present', () => {\n> 53 |     expect(html).toContain(\"window.location.replace('/hub/')\");\n     |                  ^\n  54 |     expect(html).toContain('_hubUser');\n  55 |   });\n  56 | });"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/dispatch.spec.js",
                            "column": 18,
                            "line": 53
                          },
                          "message": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoContain\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m) // indexOf\u001b[22m\n\nExpected substring: \u001b[32m\"window.location.replace('/hub/')\"\u001b[39m\nReceived string:    \u001b[31m\"<!DOCTYPE html>\u001b[39m\n\u001b[31m<html lang=\\\"en\\\">\u001b[39m\n\u001b[31m<head>\u001b[39m\n\u001b[31m<meta charset=\\\"UTF-8\\\">\u001b[39m\n\u001b[31m<meta name=\\\"viewport\\\" content=\\\"width=device-width,initial-scale=1,maximum-scale=1,user-scalable=no\\\">\u001b[39m\n\u001b[31m<meta name=\\\"theme-color\\\" content=\\\"#080c12\\\">\u001b[39m\n\u001b[31m<meta name=\\\"apple-mobile-web-app-capable\\\" content=\\\"yes\\\">\u001b[39m\n\u001b[31m<meta name=\\\"mobile-web-app-capable\\\" content=\\\"yes\\\">\u001b[39m\n\u001b[31m<title>360LM Dispatch</title>\u001b[39m\n\u001b[31m<link rel=\\\"manifest\\\" href=\\\"manifest.json\\\">\u001b[39m\n\u001b[31m<link href=\\\"https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@300;400;500;600;700&family=Fira+Code:wght@400;500&family=Barlow+Condensed:wght@400;600;700;800&display=swap\\\" rel=\\\"stylesheet\\\">\u001b[39m\n\u001b[31m<style>\u001b[39m\n\u001b[31m/* ════ RESET & VARS ════ */\u001b[39m\n\u001b[31m*{margin:0;padding:0;box-sizing:border-box;-webkit-tap-highlight-color:transparent;}\u001b[39m\n\u001b[31m:root{\u001b[39m\n\u001b[31m  --bg:#080c12; --bg2:#0d1420; --bg3:#111a28;\u001b[39m\n\u001b[31m  --sur:#141f30; --sur2:#1a2840; --sur3:#1f3050;\u001b[39m\n\u001b[31m  --bdr:#1e2e45; --bdr2:#253650;\u001b[39m\n\u001b[31m  --t1:#f0f4f8; --t2:#8ba4bc; --t3:#3d5470; --t4:#243348;\u001b[39m\n\u001b[31m  --acc:#f97316; --acc2:#fb923c; --acc-glow:rgba(249,115,22,0.25);\u001b[39m\n\u001b[31m  --grn:#22c55e; --grn-dim:rgba(34,197,94,0.15);\u001b[39m\n\u001b[31m  --sky:#38bdf8; --sky-dim:rgba(56,189,248,0.15);\u001b[39m\n\u001b[31m  --red:#ef4444; --red-dim:rgba(239,68,68,0.15);\u001b[39m\n\u001b[31m  --ylw:#eab308; --ylw-dim:rgba(234,179,8,0.15);\u001b[39m\n\u001b[31m  --fnt-head:'Barlow Condensed',sans-serif;\u001b[39m\n\u001b[31m  --fnt-body:'Space Grotesk',sans-serif;\u001b[39m\n\u001b[31m  --fnt-mono:'Fira Code',monospace;\u001b[39m\n\u001b[31m  --r:10px; --r-sm:6px; --r-lg:16px;\u001b[39m\n\u001b[31m  --sh:0 4px 24px rgba(0,0,0,0.4);\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31mhtml,body{height:100%;background:var(--bg);color:var(--t1);font-family:var(--fnt-body);overflow:hidden;}·\u001b[39m\n\u001b[31m/* ════ SPLASH ════ */\u001b[39m\n\u001b[31m#splash{\u001b[39m\n\u001b[31m  position:fixed;inset:0;background:var(--bg);z-index:999;\u001b[39m\n\u001b[31m  display:flex;flex-direction:column;align-items:center;justify-content:center;\u001b[39m\n\u001b[31m  transition:opacity .4s .1s;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.splash-logo{font-family:var(--fnt-head);font-size:3rem;font-weight:800;letter-spacing:2px;line-height:1;}\u001b[39m\n\u001b[31m.splash-logo em{color:var(--acc);font-style:normal;}\u001b[39m\n\u001b[31m.splash-sub{color:var(--t3);font-family:var(--fnt-mono);font-size:.7rem;letter-spacing:4px;margin-top:8px;}\u001b[39m\n\u001b[31m.splash-track{width:200px;height:2px;background:var(--bdr);border-radius:2px;margin-top:40px;overflow:hidden;}\u001b[39m\n\u001b[31m.splash-track-fill{height:100%;width:0;background:linear-gradient(90deg,var(--acc),var(--acc2));animation:splashLoad 1.3s cubic-bezier(.4,0,.2,1) forwards;}\u001b[39m\n\u001b[31m@keyframes splashLoad{to{width:100%;}}·\u001b[39m\n\u001b[31m/* ════ LAYOUT ════ */\u001b[39m\n\u001b[31m#app{height:100%;display:flex;flex-direction:column;}\u001b[39m\n\u001b[31m.hdr{\u001b[39m\n\u001b[31m  background:var(--bg2);border-bottom:1px solid var(--bdr);\u001b[39m\n\u001b[31m  padding:10px 14px;display:flex;align-items:center;gap:10px;flex-shrink:0;\u001b[39m\n\u001b[31m  position:relative;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.hdr-brand{font-family:var(--fnt-head);font-size:1.3rem;font-weight:800;letter-spacing:1px;flex:1;}\u001b[39m\n\u001b[31m.hdr-brand em{color:var(--acc);font-style:normal;}\u001b[39m\n\u001b[31m.hdr-dot{width:7px;height:7px;border-radius:50%;background:var(--grn);transition:background .3s;animation:blink 2s infinite;}\u001b[39m\n\u001b[31m@keyframes blink{0%,100%{opacity:1;}50%{opacity:.4;}}\u001b[39m\n\u001b[31m.hdr-btn{\u001b[39m\n\u001b[31m  background:none;border:none;color:var(--t2);cursor:pointer;\u001b[39m\n\u001b[31m  padding:6px 8px;border-radius:var(--r-sm);transition:all .2s;\u001b[39m\n\u001b[31m  font-family:var(--fnt-head);font-size:.85rem;font-weight:700;letter-spacing:.5px;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.hdr-btn:hover{background:var(--bdr);color:var(--t1);}\u001b[39m\n\u001b[31m.hdr-btn.primary{background:var(--acc);color:#fff;padding:6px 14px;}\u001b[39m\n\u001b[31m.hdr-btn.primary:hover{background:var(--acc2);}·\u001b[39m\n\u001b[31m/* ════ TAB BAR ════ */\u001b[39m\n\u001b[31m.tabs{\u001b[39m\n\u001b[31m  display:flex;background:var(--bg2);border-bottom:1px solid var(--bdr);\u001b[39m\n\u001b[31m  overflow-x:auto;flex-shrink:0;padding:0 6px;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.tabs::-webkit-scrollbar{display:none;}\u001b[39m\n\u001b[31m.tab{\u001b[39m\n\u001b[31m  background:none;border:none;color:var(--t3);font-family:var(--fnt-head);\u001b[39m\n\u001b[31m  font-size:.85rem;font-weight:700;letter-spacing:1px;padding:10px 14px;\u001b[39m\n\u001b[31m  cursor:pointer;border-bottom:2px solid transparent;white-space:nowrap;\u001b[39m\n\u001b[31m  transition:all .2s;flex-shrink:0;display:flex;align-items:center;gap:6px;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.tab.active{color:var(--acc);border-bottom-color:var(--acc);}\u001b[39m\n\u001b[31m.tab:hover:not(.active){color:var(--t2);}\u001b[39m\n\u001b[31m.tab-badge{\u001b[39m\n\u001b[31m  background:var(--acc);color:#fff;font-size:.6rem;font-weight:700;\u001b[39m\n\u001b[31m  padding:1px 5px;border-radius:10px;font-family:var(--fnt-mono);\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m/* ════ CONTENT AREAS ════ */\u001b[39m\n\u001b[31m.pane{flex:1;overflow-y:auto;padding:14px;display:none;}\u001b[39m\n\u001b[31m.pane.active{display:block;}\u001b[39m\n\u001b[31m.pane::-webkit-scrollbar{width:3px;}\u001b[39m\n\u001b[31m.pane::-webkit-scrollbar-thumb{background:var(--bdr2);}·\u001b[39m\n\u001b[31m/* ════ CARDS ════ */\u001b[39m\n\u001b[31m.card{\u001b[39m\n\u001b[31m  background:var(--sur);border:1px solid var(--bdr);border-radius:var(--r);\u001b[39m\n\u001b[31m  padding:14px;margin-bottom:12px;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.card.accent-left{border-left:3px solid var(--acc);}\u001b[39m\n\u001b[31m.card-title{\u001b[39m\n\u001b[31m  font-family:var(--fnt-head);font-size:.72rem;font-weight:700;\u001b[39m\n\u001b[31m  color:var(--t3);text-transform:uppercase;letter-spacing:2.5px;\u001b[39m\n\u001b[31m  margin-bottom:12px;display:flex;align-items:center;gap:8px;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.card-title .ct-dot{width:5px;height:5px;border-radius:50%;background:var(--acc);flex-shrink:0;}·\u001b[39m\n\u001b[31m/* ════ FORM ════ */\u001b[39m\n\u001b[31m.f{margin-bottom:12px;}\u001b[39m\n\u001b[31m.f label{\u001b[39m\n\u001b[31m  display:block;font-size:.68rem;color:var(--t3);margin-bottom:5px;\u001b[39m\n\u001b[31m  font-weight:600;text-transform:uppercase;letter-spacing:1.5px;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.f label .req{color:var(--acc);margin-left:2px;}\u001b[39m\n\u001b[31m.f input,.f select,.f textarea{\u001b[39m\n\u001b[31m  width:100%;background:var(--sur2);border:1px solid var(--bdr);\u001b[39m\n\u001b[31m  border-radius:var(--r-sm);color:var(--t1);font-family:var(--fnt-body);\u001b[39m\n\u001b[31m  font-size:.88rem;padding:9px 11px;outline:none;transition:border-color .2s;\u001b[39m\n\u001b[31m  -webkit-appearance:none;appearance:none;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.f input:focus,.f select:focus,.f textarea:focus{border-color:var(--acc);}\u001b[39m\n\u001b[31m.f input::placeholder{color:var(--t4);}\u001b[39m\n\u001b[31m.f textarea{resize:vertical;min-height:65px;}\u001b[39m\n\u001b[31m.f select{\u001b[39m\n\u001b[31m  background-image:url(\\\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='7'%3E%3Cpath d='M1 1l4 4 4-4' stroke='%233d5470' stroke-width='1.5' fill='none' stroke-linecap='round'/%3E%3C/svg%3E\\\");\u001b[39m\n\u001b[31m  background-repeat:no-repeat;background-position:right 10px center;padding-right:28px;cursor:pointer;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.row2{display:grid;grid-template-columns:1fr 1fr;gap:10px;}\u001b[39m\n\u001b[31m.row3{display:grid;grid-template-columns:1fr 1fr 1fr;gap:8px;}·\u001b[39m\n\u001b[31m/* ════ BUTTONS ════ */\u001b[39m\n\u001b[31m.btn{\u001b[39m\n\u001b[31m  width:100%;padding:12px;border:none;border-radius:var(--r);\u001b[39m\n\u001b[31m  font-family:var(--fnt-head);font-size:.9rem;font-weight:700;letter-spacing:.5px;\u001b[39m\n\u001b[31m  cursor:pointer;transition:all .2s;display:flex;align-items:center;justify-content:center;gap:8px;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.btn-orange{background:var(--acc);color:#fff;}\u001b[39m\n\u001b[31m.btn-orange:hover{background:var(--acc2);}\u001b[39m\n\u001b[31m.btn-ghost{background:var(--sur2);border:1px solid var(--bdr);color:var(--t1);}\u001b[39m\n\u001b[31m.btn-ghost:hover{border-color:var(--acc);color:var(--acc);}\u001b[39m\n\u001b[31m.btn-grn{background:var(--grn);color:#000;}\u001b[39m\n\u001b[31m.btn-grn:hover{opacity:.85;}\u001b[39m\n\u001b[31m.btn:active{transform:scale(.98);}\u001b[39m\n\u001b[31m.btn-row{display:grid;grid-template-columns:1fr 1fr;gap:10px;margin-top:6px;}\u001b[39m\n\u001b[31m.btn-sm{\u001b[39m\n\u001b[31m  background:none;border:1px solid var(--bdr);color:var(--t2);\u001b[39m\n\u001b[31m  font-size:.7rem;padding:5px 10px;border-radius:var(--r-sm);cursor:pointer;\u001b[39m\n\u001b[31m  font-family:var(--fnt-body);transition:all .2s;white-space:nowrap;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.btn-sm:hover{border-color:var(--acc);color:var(--acc);}\u001b[39m\n\u001b[31m.btn-sm.primary{background:var(--acc);border-color:var(--acc);color:#fff;}·\u001b[39m\n\u001b[31m/* ════ OCR UPLOAD ZONE ════ */\u001b[39m\n\u001b[31m.upload-zone{\u001b[39m\n\u001b[31m  border:2px dashed var(--bdr2);border-radius:var(--r);\u001b[39m\n\u001b[31m  padding:20px;text-align:center;cursor:pointer;transition:all .25s;\u001b[39m\n\u001b[31m  background:var(--sur2);position:relative;overflow:hidden;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.upload-zone:hover,.upload-zone.drag{border-color:var(--acc);background:rgba(249,115,22,.05);}\u001b[39m\n\u001b[31m.upload-zone .uz-icon{font-size:2rem;margin-bottom:8px;}\u001b[39m\n\u001b[31m.upload-zone .uz-title{font-family:var(--fnt-head);font-size:1rem;font-weight:700;color:var(--t1);}\u001b[39m\n\u001b[31m.upload-zone .uz-sub{font-size:.72rem;color:var(--t3);margin-top:4px;font-family:var(--fnt-mono);}\u001b[39m\n\u001b[31m.upload-zone input{display:none;}\u001b[39m\n\u001b[31m.upload-zone.share-target{border-color:var(--grn);}·\u001b[39m\n\u001b[31m/* ════ OCR RESULTS ════ */\u001b[39m\n\u001b[31m.ocr-results{display:none;}\u001b[39m\n\u001b[31m.ocr-results.visible{display:block;}\u001b[39m\n\u001b[31m.ocr-slip-list{display:flex;flex-direction:column;gap:8px;margin-top:10px;}\u001b[39m\n\u001b[31m.ocr-slip{\u001b[39m\n\u001b[31m  background:var(--sur2);border:1px solid var(--bdr);border-radius:var(--r-sm);\u001b[39m\n\u001b[31m  padding:10px 12px;display:flex;gap:10px;align-items:flex-start;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.ocr-slip img{width:60px;height:60px;object-fit:cover;border-radius:6px;flex-shrink:0;border:1px solid var(--bdr2);}\u001b[39m\n\u001b[31m.ocr-slip-body{flex:1;min-width:0;}\u001b[39m\n\u001b[31m.ocr-slip-name{font-size:.7rem;color:var(--t3);font-family:var(--fnt-mono);margin-bottom:4px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}\u001b[39m\n\u001b[31m.ocr-slip-docket{\u001b[39m\n\u001b[31m  font-family:var(--fnt-mono);font-size:.9rem;font-weight:500;color:var(--acc);\u001b[39m\n\u001b[31m  background:var(--bg);border:1px solid var(--bdr);border-radius:4px;\u001b[39m\n\u001b[31m  padding:4px 8px;width:100%;outline:none;transition:border-color .2s;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.ocr-slip-docket:focus{border-color:var(--acc);}\u001b[39m\n\u001b[31m.ocr-slip-status{font-size:.65rem;margin-top:4px;display:flex;align-items:center;gap:6px;}\u001b[39m\n\u001b[31m.ocr-confidence{padding:2px 6px;border-radius:10px;font-family:var(--fnt-mono);}\u001b[39m\n\u001b[31m.conf-high{background:var(--grn-dim);color:var(--grn);}\u001b[39m\n\u001b[31m.conf-med{background:var(--ylw-dim);color:var(--ylw);}\u001b[39m\n\u001b[31m.conf-low{background:var(--red-dim);color:var(--red);}\u001b[39m\n\u001b[31m.ocr-ai-btn{\u001b[39m\n\u001b[31m  background:rgba(56,189,248,.15);border:1px solid rgba(56,189,248,.3);\u001b[39m\n\u001b[31m  color:var(--sky);font-size:.65rem;padding:3px 8px;border-radius:10px;\u001b[39m\n\u001b[31m  cursor:pointer;font-family:var(--fnt-mono);transition:all .2s;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.ocr-ai-btn:hover{background:rgba(56,189,248,.3);}\u001b[39m\n\u001b[31m.ocr-working{\u001b[39m\n\u001b[31m  background:var(--ylw-dim);border:1px solid rgba(234,179,8,.3);\u001b[39m\n\u001b[31m  border-radius:var(--r-sm);padding:8px 12px;margin-top:8px;\u001b[39m\n\u001b[31m  font-size:.75rem;color:var(--ylw);font-family:var(--fnt-mono);\u001b[39m\n\u001b[31m  display:none;align-items:center;gap:8px;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.ocr-working.visible{display:flex;}\u001b[39m\n\u001b[31m.spin{animation:spin .8s linear infinite;display:inline-block;}\u001b[39m\n\u001b[31m@keyframes spin{to{transform:rotate(360deg);}}·\u001b[39m\n\u001b[31m/* ════ STORE LIST ════ */\u001b[39m\n\u001b[31m.store-upload-zone{\u001b[39m\n\u001b[31m  border:2px dashed var(--bdr2);border-radius:var(--r);padding:16px;\u001b[39m\n\u001b[31m  text-align:center;cursor:pointer;transition:all .25s;background:var(--sur2);\u001b[39m\n\u001b[31m  position:relative;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.store-upload-zone:hover{border-color:var(--sky);background:rgba(56,189,248,.05);}\u001b[39m\n\u001b[31m.store-upload-zone input{display:none;}\u001b[39m\n\u001b[31m.store-table-wrap{max-height:200px;overflow-y:auto;margin-top:10px;border-radius:var(--r-sm);overflow:hidden;}\u001b[39m\n\u001b[31m.store-table{width:100%;border-collapse:collapse;font-size:.75rem;}\u001b[39m\n\u001b[31m.store-table th{\u001b[39m\n\u001b[31m  background:var(--bg);color:var(--t3);font-family:var(--fnt-head);\u001b[39m\n\u001b[31m  font-size:.65rem;letter-spacing:1.5px;text-transform:uppercase;\u001b[39m\n\u001b[31m  padding:7px 8px;text-align:left;border-bottom:1px solid var(--bdr);\u001b[39m\n\u001b[31m  position:sticky;top:0;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.store-table td{\u001b[39m\n\u001b[31m  padding:6px 8px;border-bottom:1px solid var(--bdr);color:var(--t2);\u001b[39m\n\u001b[31m  font-family:var(--fnt-mono);font-size:.72rem;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.store-table tr:hover td{background:var(--sur3);}\u001b[39m\n\u001b[31m.store-table .matched{color:var(--grn);}\u001b[39m\n\u001b[31m.store-table .unmatched{color:var(--ylw);}\u001b[39m\n\u001b[31m.store-map-btn{\u001b[39m\n\u001b[31m  font-size:.65rem;background:none;border:1px solid var(--bdr);color:var(--t3);\u001b[39m\n\u001b[31m  padding:2px 6px;border-radius:4px;cursor:pointer;transition:all .2s;\u001b[39m\n\u001b[31m  font-family:var(--fnt-mono);\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.store-map-btn:hover{border-color:var(--acc);color:var(--acc);}·\u001b[39m\n\u001b[31m/* ════ CONSIGNEE CARDS ════ */\u001b[39m\n\u001b[31m.cons-list{display:flex;flex-direction:column;gap:8px;margin-bottom:10px;}\u001b[39m\n\u001b[31m.cons-card{\u001b[39m\n\u001b[31m  background:var(--sur2);border:1px solid var(--bdr);border-radius:var(--r-sm);\u001b[39m\n\u001b[31m  padding:11px;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.cons-hdr{display:flex;justify-content:space-between;align-items:center;margin-bottom:8px;}\u001b[39m\n\u001b[31m.cons-num{font-family:var(--fnt-mono);font-size:.65rem;color:var(--t3);}\u001b[39m\n\u001b[31m.cons-del{\u001b[39m\n\u001b[31m  background:none;border:none;color:var(--t4);cursor:pointer;\u001b[39m\n\u001b[31m  font-size:.85rem;padding:2px 5px;border-radius:4px;transition:all .2s;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.cons-del:hover{background:var(--red-dim);color:var(--red);}\u001b[39m\n\u001b[31m.cons-docket-linked{\u001b[39m\n\u001b[31m  display:flex;align-items:center;gap:6px;\u001b[39m\n\u001b[31m  background:rgba(249,115,22,.1);border:1px solid rgba(249,115,22,.3);\u001b[39m\n\u001b[31m  border-radius:4px;padding:4px 8px;margin-top:6px;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.cons-docket-linked .dkt{font-family:var(--fnt-mono);font-size:.82rem;color:var(--acc);}\u001b[39m\n\u001b[31m.cons-docket-linked .unlink{background:none;border:none;color:var(--acc);cursor:pointer;font-size:.75rem;}·\u001b[39m\n\u001b[31m/* ════ DISPATCH DASHBOARD ════ */\u001b[39m\n\u001b[31m.dash-stats{\u001b[39m\n\u001b[31m  display:grid;grid-template-columns:repeat(4,1fr);gap:8px;margin-bottom:14px;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.stat-box{\u001b[39m\n\u001b[31m  background:var(--sur);border:1px solid var(--bdr);border-radius:var(--r);\u001b[39m\n\u001b[31m  padding:10px 8px;text-align:center;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.stat-val{font-family:var(--fnt-head);font-size:1.4rem;font-weight:800;}\u001b[39m\n\u001b[31m.stat-lbl{font-size:.6rem;color:var(--t3);font-family:var(--fnt-mono);text-transform:uppercase;letter-spacing:1px;margin-top:2px;}\u001b[39m\n\u001b[31m.stat-box.grn .stat-val{color:var(--grn);}\u001b[39m\n\u001b[31m.stat-box.ylw .stat-val{color:var(--ylw);}\u001b[39m\n\u001b[31m.stat-box.red .stat-val{color:var(--red);}\u001b[39m\n\u001b[31m.stat-box.sky .stat-val{color:var(--sky);}·\u001b[39m\n\u001b[31m/* ════ DOCKET TILES ════ */\u001b[39m\n\u001b[31m.docket-grid{display:flex;flex-direction:column;gap:8px;}\u001b[39m\n\u001b[31m.docket-tile{\u001b[39m\n\u001b[31m  background:var(--sur);border:1px solid var(--bdr);border-radius:var(--r);\u001b[39m\n\u001b[31m  padding:12px;transition:border-color .2s;cursor:pointer;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.docket-tile:hover{border-color:var(--acc);}\u001b[39m\n\u001b[31m.docket-tile-hdr{display:flex;justify-content:space-between;align-items:flex-start;margin-bottom:8px;}\u001b[39m\n\u001b[31m.docket-tile-left{flex:1;}\u001b[39m\n\u001b[31m.docket-num{font-family:var(--fnt-mono);font-size:.95rem;font-weight:500;color:var(--sky);letter-spacing:1px;}\u001b[39m\n\u001b[31m.docket-store{font-size:.82rem;color:var(--t1);font-weight:600;margin-top:2px;}\u001b[39m\n\u001b[31m.docket-courier{font-size:.68rem;color:var(--t3);font-family:var(--fnt-mono);margin-top:2px;}\u001b[39m\n\u001b[31m.status-pill{\u001b[39m\n\u001b[31m  font-size:.62rem;font-weight:700;font-family:var(--fnt-mono);\u001b[39m\n\u001b[31m  padding:3px 8px;border-radius:10px;text-transform:uppercase;letter-spacing:.5px;\u001b[39m\n\u001b[31m  flex-shrink:0;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.sp-delivered{background:var(--grn-dim);color:var(--grn);}\u001b[39m\n\u001b[31m.sp-transit{background:var(--ylw-dim);color:var(--ylw);}\u001b[39m\n\u001b[31m.sp-booked{background:var(--sky-dim);color:var(--sky);}\u001b[39m\n\u001b[31m.sp-pending{background:var(--bdr);color:var(--t3);}\u001b[39m\n\u001b[31m.sp-failed{background:var(--red-dim);color:var(--red);}\u001b[39m\n\u001b[31m.docket-tile-footer{display:flex;align-items:center;gap:8px;flex-wrap:wrap;}\u001b[39m\n\u001b[31m.docket-last-event{font-size:.7rem;color:var(--t3);flex:1;font-family:var(--fnt-mono);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}\u001b[39m\n\u001b[31m.btn-track-live{\u001b[39m\n\u001b[31m  background:none;border:1px solid var(--acc);color:var(--acc);\u001b[39m\n\u001b[31m  font-size:.65rem;padding:4px 10px;border-radius:20px;cursor:pointer;\u001b[39m\n\u001b[31m  font-family:var(--fnt-mono);transition:all .2s;white-space:nowrap;display:flex;align-items:center;gap:4px;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.btn-track-live:hover{background:var(--acc);color:#fff;}\u001b[39m\n\u001b[31m.btn-track-live .live-dot{width:5px;height:5px;border-radius:50%;background:var(--grn);animation:blink 1.5s infinite;flex-shrink:0;}\u001b[39m\n\u001b[31m.docket-progress{height:2px;background:var(--bdr);border-radius:2px;margin-top:8px;overflow:hidden;}\u001b[39m\n\u001b[31m.docket-progress-fill{height:100%;border-radius:2px;transition:width .5s;}·\u001b[39m\n\u001b[31m/* ════ TRACKING DETAIL ════ */\u001b[39m\n\u001b[31m.track-timeline{padding:4px 0;}\u001b[39m\n\u001b[31m.track-evt{display:flex;gap:10px;padding:6px 0;position:relative;padding-left:20px;}\u001b[39m\n\u001b[31m.track-evt::before{\u001b[39m\n\u001b[31m  content:'';position:absolute;left:5px;top:11px;\u001b[39m\n\u001b[31m  width:7px;height:7px;border-radius:50%;background:var(--bdr2);border:1px solid var(--bdr2);\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.track-evt:first-child::before{background:var(--grn);border-color:var(--grn);}\u001b[39m\n\u001b[31m.track-evt::after{\u001b[39m\n\u001b[31m  content:'';position:absolute;left:8px;top:18px;\u001b[39m\n\u001b[31m  width:1px;height:calc(100% - 6px);background:var(--bdr);\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.track-evt:last-child::after{display:none;}\u001b[39m\n\u001b[31m.track-time{font-family:var(--fnt-mono);font-size:.62rem;color:var(--t3);min-width:90px;flex-shrink:0;padding-top:2px;}\u001b[39m\n\u001b[31m.track-desc{font-size:.78rem;color:var(--t2);}\u001b[39m\n\u001b[31m.track-loc{font-size:.67rem;color:var(--t3);font-family:var(--fnt-mono);margin-top:2px;}·\u001b[39m\n\u001b[31m/* ════ SHARE TOAST ════ */\u001b[39m\n\u001b[31m.share-banner{\u001b[39m\n\u001b[31m  position:fixed;top:0;left:0;right:0;z-index:200;\u001b[39m\n\u001b[31m  background:rgba(34,197,94,.15);border-bottom:1px solid rgba(34,197,94,.4);\u001b[39m\n\u001b[31m  padding:10px 16px;font-size:.78rem;color:var(--grn);font-family:var(--fnt-mono);\u001b[39m\n\u001b[31m  display:flex;align-items:center;gap:8px;\u001b[39m\n\u001b[31m  transform:translateY(-100%);transition:transform .3s;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.share-banner.show{transform:translateY(0);}·\u001b[39m\n\u001b[31m/* ════ MODAL ════ */\u001b[39m\n\u001b[31m.overlay{position:fixed;inset:0;background:rgba(0,0,0,.75);z-index:100;display:flex;align-items:flex-end;opacity:0;pointer-events:none;transition:opacity .3s;}\u001b[39m\n\u001b[31m.overlay.open{opacity:1;pointer-events:all;}\u001b[39m\n\u001b[31m.sheet{\u001b[39m\n\u001b[31m  background:var(--sur);border-radius:var(--r-lg) var(--r-lg) 0 0;\u001b[39m\n\u001b[31m  width:100%;max-height:88vh;overflow-y:auto;padding:18px;\u001b[39m\n\u001b[31m  transform:translateY(100%);transition:transform .3s cubic-bezier(.32,.72,0,1);\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.overlay.open .sheet{transform:translateY(0);}\u001b[39m\n\u001b[31m.sheet-handle{width:36px;height:4px;background:var(--bdr2);border-radius:2px;margin:0 auto 18px;}\u001b[39m\n\u001b[31m.sheet-title{font-family:var(--fnt-head);font-size:1.2rem;font-weight:800;letter-spacing:.5px;margin-bottom:14px;}\u001b[39m\n\u001b[31m.sheet::-webkit-scrollbar{width:3px;}\u001b[39m\n\u001b[31m.sheet::-webkit-scrollbar-thumb{background:var(--bdr2);}·\u001b[39m\n\u001b[31m/* ════ TOAST ════ */\u001b[39m\n\u001b[31m.toast{\u001b[39m\n\u001b[31m  position:fixed;bottom:20px;left:50%;transform:translateX(-50%) translateY(70px);\u001b[39m\n\u001b[31m  background:var(--sur2);border:1px solid var(--bdr2);color:var(--t1);\u001b[39m\n\u001b[31m  font-size:.78rem;padding:9px 16px;border-radius:30px;z-index:300;\u001b[39m\n\u001b[31m  transition:transform .3s;white-space:nowrap;font-family:var(--fnt-mono);\u001b[39m\n\u001b[31m  pointer-events:none;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.toast.show{transform:translateX(-50%) translateY(0);}\u001b[39m\n\u001b[31m.toast.ok{border-color:var(--grn);color:var(--grn);}\u001b[39m\n\u001b[31m.toast.err{border-color:var(--red);color:var(--red);}\u001b[39m\n\u001b[31m.toast.info{border-color:var(--sky);color:var(--sky);}·\u001b[39m\n\u001b[31m/* ════ COURIER MASTER ════ */\u001b[39m\n\u001b[31m.courier-master-row{\u001b[39m\n\u001b[31m  display:flex;align-items:center;gap:8px;padding:8px 0;\u001b[39m\n\u001b[31m  border-bottom:1px solid var(--bdr);\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.courier-master-row:last-child{border-bottom:none;}\u001b[39m\n\u001b[31m.cml{flex:1;}\u001b[39m\n\u001b[31m.cml-name{font-weight:600;font-size:.85rem;}\u001b[39m\n\u001b[31m.cml-url{font-family:var(--fnt-mono);font-size:.65rem;color:var(--t3);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:180px;}\u001b[39m\n\u001b[31m.courier-logo{width:24px;height:24px;border-radius:4px;object-fit:contain;background:var(--sur3);padding:2px;}·\u001b[39m\n\u001b[31m/* ════ PROGRESS BAR ════ */\u001b[39m\n\u001b[31m.ai-progress{\u001b[39m\n\u001b[31m  background:var(--sur2);border:1px solid var(--bdr);border-radius:var(--r-sm);\u001b[39m\n\u001b[31m  padding:10px 12px;margin-top:8px;display:none;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.ai-progress.visible{display:block;}\u001b[39m\n\u001b[31m.ai-prog-bar{height:3px;background:var(--bdr);border-radius:3px;margin-top:6px;overflow:hidden;}\u001b[39m\n\u001b[31m.ai-prog-fill{height:100%;background:linear-gradient(90deg,var(--sky),var(--acc));width:0;transition:width .3s;}\u001b[39m\n\u001b[31m.ai-prog-label{font-size:.68rem;color:var(--sky);font-family:var(--fnt-mono);}·\u001b[39m\n\u001b[31m/* ════ EMPTY ════ */\u001b[39m\n\u001b[31m.empty{padding:40px 20px;text-align:center;color:var(--t4);}\u001b[39m\n\u001b[31m.empty .e-icon{font-size:2.5rem;margin-bottom:10px;}\u001b[39m\n\u001b[31m.empty p{font-size:.82rem;}·\u001b[39m\n\u001b[31m/* ════ INFO TAG ════ */\u001b[39m\n\u001b[31m.info-tag{\u001b[39m\n\u001b[31m  background:var(--sur2);border:1px solid var(--bdr);border-radius:var(--r-sm);\u001b[39m\n\u001b[31m  padding:7px 10px;font-size:.75rem;color:var(--t2);margin-bottom:8px;\u001b[39m\n\u001b[31m  display:flex;align-items:center;gap:8px;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.info-tag strong{color:var(--t1);}·\u001b[39m\n\u001b[31m/* ════ JOB PILL ════ */\u001b[39m\n\u001b[31m.job-pill{\u001b[39m\n\u001b[31m  display:inline-flex;align-items:center;gap:6px;\u001b[39m\n\u001b[31m  background:rgba(249,115,22,.1);border:1px solid rgba(249,115,22,.3);\u001b[39m\n\u001b[31m  color:var(--acc);font-family:var(--fnt-mono);font-size:.68rem;\u001b[39m\n\u001b[31m  padding:3px 9px;border-radius:20px;margin-top:4px;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.job-pill button{background:none;border:none;color:var(--acc);cursor:pointer;font-size:.7rem;}·\u001b[39m\n\u001b[31m/* ════ SECTION DIVIDER ════ */\u001b[39m\n\u001b[31m.sec-div{\u001b[39m\n\u001b[31m  display:flex;align-items:center;gap:10px;margin:14px 0 10px;\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m.sec-div span{font-family:var(--fnt-head);font-size:.7rem;font-weight:700;color:var(--t3);text-transform:uppercase;letter-spacing:2px;white-space:nowrap;}\u001b[39m\n\u001b[31m.sec-div::before,.sec-div::after{content:'';flex:1;height:1px;background:var(--bdr);}·\u001b[39m\n\u001b[31m/* ── MAPPING UI ── */\u001b[39m\n\u001b[31m.map-table{width:100%;border-collapse:collapse;font-size:.78rem;margin:10px 0;}\u001b[39m\n\u001b[31m.map-table th{background:var(--bg);color:var(--t3);font-family:var(--fnt-head);font-size:.65rem;\u001b[39m\n\u001b[31m  letter-spacing:1.5px;text-transform:uppercase;padding:7px 10px;text-align:left;\u001b[39m\n\u001b[31m  border-bottom:1px solid var(--bdr);}\u001b[39m\n\u001b[31m.map-table td{padding:7px 10px;border-bottom:1px solid var(--bdr);vertical-align:middle;}\u001b[39m\n\u001b[31m.map-table tr:last-child td{border-bottom:none;}\u001b[39m\n\u001b[31m.map-table select{background:var(--sur2);border:1px solid var(--bdr);color:var(--t1);\u001b[39m\n\u001b[31m  font-family:var(--fnt-mono);font-size:.75rem;padding:5px 8px;border-radius:var(--r-sm);\u001b[39m\n\u001b[31m  width:100%;outline:none;}\u001b[39m\n\u001b[31m.map-table select:focus{border-color:var(--acc);}\u001b[39m\n\u001b[31m.map-req{color:var(--acc);margin-left:2px;}\u001b[39m\n\u001b[31m.map-saved-badge{font-size:.6rem;background:rgba(57,255,143,.15);color:var(--accent3,#39ff8f);\u001b[39m\n\u001b[31m  border:1px solid rgba(57,255,143,.3);padding:2px 7px;border-radius:10px;\u001b[39m\n\u001b[31m  font-family:var(--fnt-mono);margin-left:6px;}·\u001b[39m\n\u001b[31m/* ── VERIFICATION SCREEN ── */\u001b[39m\n\u001b[31m#verifyPane{display:none;position:fixed;inset:0;background:var(--bg);z-index:50;\u001b[39m\n\u001b[31m  overflow-y:auto;padding:14px;}\u001b[39m\n\u001b[31m.verify-hdr{display:flex;align-items:center;gap:10px;margin-bottom:14px;\u001b[39m\n\u001b[31m  position:sticky;top:0;background:var(--bg);padding-bottom:10px;\u001b[39m\n\u001b[31m  border-bottom:1px solid var(--bdr);z-index:1;}\u001b[39m\n\u001b[31m.verify-title{font-family:var(--fnt-head);font-size:1.1rem;font-weight:800;flex:1;}\u001b[39m\n\u001b[31m.verify-row{background:var(--sur);border:1px solid var(--bdr);border-radius:var(--r-sm);\u001b[39m\n\u001b[31m  padding:10px 12px;margin-bottom:8px;position:relative;}\u001b[39m\n\u001b[31m.verify-row.has-dupe{border-color:var(--red);background:rgba(239,68,68,.04);}\u001b[39m\n\u001b[31m.verify-row.no-docket{border-color:var(--ylw);background:rgba(234,179,8,.04);}\u001b[39m\n\u001b[31m.verify-row.ok{border-left:3px solid var(--grn);}\u001b[39m\n\u001b[31m.verify-row-hdr{display:flex;align-items:center;gap:8px;margin-bottom:8px;}\u001b[39m\n\u001b[31m.verify-num{font-family:var(--fnt-mono);font-size:.65rem;color:var(--t3);}\u001b[39m\n\u001b[31m.verify-store{font-weight:600;font-size:.9rem;flex:1;}\u001b[39m\n\u001b[31m.verify-flag{font-size:.65rem;font-weight:700;font-family:var(--fnt-mono);\u001b[39m\n\u001b[31m  padding:2px 7px;border-radius:10px;text-transform:uppercase;}\u001b[39m\n\u001b[31m.vf-dupe{background:var(--red-dim);color:var(--red);}\u001b[39m\n\u001b[31m.vf-nodocket{background:var(--ylw-dim);color:var(--ylw);}\u001b[39m\n\u001b[31m.vf-ok{background:var(--grn-dim);color:var(--grn);}\u001b[39m\n\u001b[31m.vf-short{background:rgba(56,189,248,.15);color:var(--sky);}\u001b[39m\n\u001b[31m.verify-fields{display:grid;grid-template-columns:1fr 1fr;gap:8px;margin-bottom:8px;}\u001b[39m\n\u001b[31m.verify-field label{font-size:.62rem;color:var(--t3);text-transform:uppercase;\u001b[39m\n\u001b[31m  letter-spacing:1px;display:block;margin-bottom:3px;font-family:var(--fnt-mono);}\u001b[39m\n\u001b[31m.verify-field input{width:100%;background:var(--sur2);border:1px solid var(--bdr);\u001b[39m\n\u001b[31m  color:var(--t1);font-family:var(--fnt-mono);font-size:.82rem;padding:6px 9px;\u001b[39m\n\u001b[31m  border-radius:var(--r-sm);outline:none;}\u001b[39m\n\u001b[31m.verify-field input:focus{border-color:var(--acc);}\u001b[39m\n\u001b[31m.verify-field input.changed{border-color:var(--ylw);background:rgba(234,179,8,.06);}\u001b[39m\n\u001b[31m.verify-actions{display:flex;gap:6px;align-items:center;flex-wrap:wrap;}\u001b[39m\n\u001b[31m.btn-wa{background:rgba(37,211,102,.15);border:1px solid rgba(37,211,102,.3);\u001b[39m\n\u001b[31m  color:#25d366;font-size:.65rem;padding:4px 10px;border-radius:10px;cursor:pointer;\u001b[39m\n\u001b[31m  font-family:var(--fnt-mono);transition:all .2s;}\u001b[39m\n\u001b[31m.btn-wa:hover{background:rgba(37,211,102,.3);}\u001b[39m\n\u001b[31m.verify-thumb{width:48px;height:48px;object-fit:cover;border-radius:6px;\u001b[39m\n\u001b[31m  border:1px solid var(--bdr);cursor:pointer;flex-shrink:0;}\u001b[39m\n\u001b[31m.verify-summary{background:var(--sur);border:1px solid var(--bdr);border-radius:var(--r);\u001b[39m\n\u001b[31m  padding:12px 14px;margin-bottom:14px;display:flex;gap:16px;flex-wrap:wrap;}\u001b[39m\n\u001b[31m.vs-item{text-align:center;}\u001b[39m\n\u001b[31m.vs-val{font-family:var(--fnt-head);font-size:1.3rem;font-weight:800;}\u001b[39m\n\u001b[31m.vs-lbl{font-size:.6rem;color:var(--t3);font-family:var(--fnt-mono);text-transform:uppercase;\u001b[39m\n\u001b[31m  letter-spacing:.5px;margin-top:2px;}\u001b[39m\n\u001b[31m.ocr-correction-note{font-size:.65rem;color:var(--ylw);font-family:var(--fnt-mono);\u001b[39m\n\u001b[31m  margin-top:3px;}\u001b[39m\n\u001b[31m</style>\u001b[39m\n\u001b[31m</head>\u001b[39m\n\u001b[31m<body>·\u001b[39m\n\u001b[31m<!-- SPLASH -->\u001b[39m\n\u001b[31m<div id=\\\"splash\\\">\u001b[39m\n\u001b[31m  <div class=\\\"splash-logo\\\">360LM <em>DISPATCH</em></div>\u001b[39m\n\u001b[31m  <div class=\\\"splash-sub\\\">Courier · Bus · Field Ops</div>\u001b[39m\n\u001b[31m  <div class=\\\"splash-track\\\"><div class=\\\"splash-track-fill\\\"></div></div>\u001b[39m\n\u001b[31m</div>·\u001b[39m\n\u001b[31m<!-- SHARE BANNER -->\u001b[39m\n\u001b[31m<div class=\\\"share-banner\\\" id=\\\"shareBanner\\\">\u001b[39m\n\u001b[31m  📤 Images received via Share — processing for OCR...\u001b[39m\n\u001b[31m  <button style=\\\"background:none;border:none;color:var(--grn);cursor:pointer;margin-left:auto;\\\" onclick=\\\"dismissShare()\\\">✕</button>\u001b[39m\n\u001b[31m</div>·\u001b[39m\n\u001b[31m<!-- APP -->\u001b[39m\n\u001b[31m<div id=\\\"app\\\" style=\\\"display:none;\\\">·\u001b[39m\n\u001b[31m  <div class=\\\"hdr\\\">\u001b[39m\n\u001b[31m    <div class=\\\"hdr-brand\\\">360LM <em>DISPATCH</em></div>\u001b[39m\n\u001b[31m    <span class=\\\"hdr-dot\\\" id=\\\"netDot\\\"></span>\u001b[39m\n\u001b[31m    <button class=\\\"hdr-btn\\\" onclick=\\\"window.location.href='/hub/'\\\" title=\\\"Hub\\\">🏡</button>\u001b[39m\n\u001b[31m    <button class=\\\"hdr-btn\\\" onclick=\\\"showPane('dashboard')\\\">📊</button>\u001b[39m\n\u001b[31m    <button class=\\\"hdr-btn primary\\\" onclick=\\\"newDispatch()\\\">+ NEW</button>\u001b[39m\n\u001b[31m  </div>·\u001b[39m\n\u001b[31m  <div class=\\\"tabs\\\">\u001b[39m\n\u001b[31m    <button class=\\\"tab active\\\" onclick=\\\"showPane('new')\\\" id=\\\"tab-new\\\">NEW</button>\u001b[39m\n\u001b[31m    <button class=\\\"tab\\\" onclick=\\\"showPane('dashboard')\\\" id=\\\"tab-dashboard\\\">DASHBOARD</button>\u001b[39m\n\u001b[31m    <button class=\\\"tab\\\" onclick=\\\"showPane('track')\\\" id=\\\"tab-track\\\">TRACK</button>\u001b[39m\n\u001b[31m    <button class=\\\"tab\\\" onclick=\\\"showPane('history')\\\" id=\\\"tab-history\\\">HISTORY <span class=\\\"tab-badge\\\" id=\\\"histCount\\\">0</span></button>\u001b[39m\n\u001b[31m    <button class=\\\"tab\\\" onclick=\\\"showPane('config')\\\" id=\\\"tab-config\\\">CONFIG</button>\u001b[39m\n\u001b[31m  </div>·\u001b[39m\n\u001b[31m  <!-- ════════════ NEW DISPATCH ════════════ -->\u001b[39m\n\u001b[31m  <div class=\\\"pane active\\\" id=\\\"pane-new\\\">·\u001b[39m\n\u001b[31m    <!-- 1. JOB REFERENCE -->\u001b[39m\n\u001b[31m    <div class=\\\"card accent-left\\\">\u001b[39m\n\u001b[31m      <div class=\\\"card-title\\\"><span class=\\\"ct-dot\\\"></span>Job Reference</div>\u001b[39m\n\u001b[31m      <div class=\\\"row2\\\">\u001b[39m\n\u001b[31m        <div class=\\\"f\\\">\u001b[39m\n\u001b[31m          <label>Client <span class=\\\"req\\\">*</span></label>\u001b[39m\n\u001b[31m          <select id=\\\"n_client\\\" onchange=\\\"onClientChange()\\\">\u001b[39m\n\u001b[31m            <option value=\\\"\\\">Select...</option>\u001b[39m\n\u001b[31m            <option>Lenovo</option><option>HP</option>\u001b[39m\n\u001b[31m            <option>Philips</option><option>Liebherr</option>\u001b[39m\n\u001b[31m            <option value=\\\"__c\\\">+ Custom</option>\u001b[39m\n\u001b[31m          </select>\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m        <div class=\\\"f\\\">\u001b[39m\n\u001b[31m          <label>Job Ref</label>\u001b[39m\n\u001b[31m          <input type=\\\"text\\\" id=\\\"n_jobref\\\" placeholder=\\\"JOB-042\\\" oninput=\\\"updatePill()\\\">\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      <div class=\\\"f\\\" id=\\\"customClientField\\\" style=\\\"display:none;\\\">\u001b[39m\n\u001b[31m        <label>Custom Client</label>\u001b[39m\n\u001b[31m        <input type=\\\"text\\\" id=\\\"n_customClient\\\" placeholder=\\\"Client name\\\" oninput=\\\"updatePill()\\\">\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      <div id=\\\"jobPill\\\" style=\\\"display:none;\\\" class=\\\"job-pill\\\">\u001b[39m\n\u001b[31m        🔗 <span id=\\\"jobPillTxt\\\"></span>\u001b[39m\n\u001b[31m        <button onclick=\\\"clearJob()\\\">✕</button>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      <div class=\\\"row2\\\" style=\\\"margin-top:10px;\\\">\u001b[39m\n\u001b[31m        <div class=\\\"f\\\" style=\\\"margin-bottom:0;\\\">\u001b[39m\n\u001b[31m          <label>Dispatch Date</label>\u001b[39m\n\u001b[31m          <input type=\\\"date\\\" id=\\\"n_date\\\">\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m        <div class=\\\"f\\\" style=\\\"margin-bottom:0;\\\">\u001b[39m\n\u001b[31m          <label>Courier Company <span class=\\\"req\\\">*</span></label>\u001b[39m\n\u001b[31m          <select id=\\\"n_courier\\\">\u001b[39m\n\u001b[31m            <option value=\\\"\\\">Select...</option>\u001b[39m\n\u001b[31m            <option>SkyKing</option><option>DTDC</option>\u001b[39m\n\u001b[31m            <option>Delhivery</option><option>Blue Dart</option>\u001b[39m\n\u001b[31m            <option>FedEx</option><option>Ecom Express</option>\u001b[39m\n\u001b[31m            <option>India Post</option><option value=\\\"__o\\\">Other</option>\u001b[39m\n\u001b[31m          </select>\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m    </div>·\u001b[39m\n\u001b[31m    <!-- 2. STORE LIST UPLOAD -->\u001b[39m\n\u001b[31m    <div class=\\\"card\\\">\u001b[39m\n\u001b[31m      <div class=\\\"card-title\\\"><span class=\\\"ct-dot\\\" style=\\\"background:var(--sky);\\\"></span>Store / Destination List</div>\u001b[39m\n\u001b[31m      <div class=\\\"store-upload-zone\\\" id=\\\"storeZone\\\" onclick=\\\"document.getElementById('storeFile').click();event.stopPropagation();\\\" style=\\\"cursor:pointer;\\\">\u001b[39m\n\u001b[31m        <input type=\\\"file\\\" id=\\\"storeFile\\\" accept=\\\".xlsx,.xls,.csv\\\" onchange=\\\"handleStoreFile(this)\\\" onclick=\\\"event.stopPropagation();\\\" style=\\\"display:none;\\\">\u001b[39m\n\u001b[31m        <div style=\\\"font-size:1.5rem;margin-bottom:6px;\\\">📋</div>\u001b[39m\n\u001b[31m        <div style=\\\"font-family:var(--fnt-head);font-weight:700;font-size:.9rem;\\\">Upload Store List</div>\u001b[39m\n\u001b[31m        <div style=\\\"font-size:.68rem;color:var(--t3);margin-top:3px;font-family:var(--fnt-mono);\\\">Excel / CSV · stores, cities, contacts</div>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      <div id=\\\"storeTableArea\\\" style=\\\"display:none;\\\">\u001b[39m\n\u001b[31m        <!-- Column detection result -->\u001b[39m\n\u001b[31m        <div id=\\\"storeColInfo\\\" style=\\\"margin-top:10px;display:none;\\\"></div>\u001b[39m\n\u001b[31m        <div style=\\\"display:flex;justify-content:space-between;align-items:center;margin-top:10px;margin-bottom:6px;\\\">\u001b[39m\n\u001b[31m          <span style=\\\"font-family:var(--fnt-mono);font-size:.7rem;color:var(--sky);\\\" id=\\\"storeCount\\\">0 stores loaded</span>\u001b[39m\n\u001b[31m          <button class=\\\"btn-sm\\\" onclick=\\\"clearStores()\\\">Clear</button>\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m        <div class=\\\"store-table-wrap\\\">\u001b[39m\n\u001b[31m          <table class=\\\"store-table\\\" id=\\\"storeTable\\\"></table>\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m        <!-- Inline add-as-consignees UI instead of confirm() -->\u001b[39m\n\u001b[31m        <div id=\\\"storeAddPanel\\\" style=\\\"margin-top:10px;background:var(--sur3);border:1px solid var(--bdr2);border-radius:var(--r-sm);padding:10px 12px;display:none;\\\">\u001b[39m\n\u001b[31m          <div style=\\\"font-size:.75rem;color:var(--t1);margin-bottom:8px;font-weight:600;\\\" id=\\\"storeAddTitle\\\">Add stores as consignees?</div>\u001b[39m\n\u001b[31m          <div class=\\\"row2\\\">\u001b[39m\n\u001b[31m            <button class=\\\"btn btn-ghost\\\" style=\\\"font-size:.75rem;padding:8px;\\\" onclick=\\\"document.getElementById('storeAddPanel').style.display='none'\\\">Skip for now</button>\u001b[39m\n\u001b[31m            <button class=\\\"btn btn-orange\\\" style=\\\"font-size:.75rem;padding:8px;\\\" id=\\\"storeAddBtn\\\" onclick=\\\"addStoresToConsignees()\\\">✓ Add All as Consignees</button>\u001b[39m\n\u001b[31m          </div>\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m        <button class=\\\"btn btn-ghost\\\" style=\\\"margin-top:8px;font-size:.78rem;\\\" onclick=\\\"autoMatchDockets()\\\">\u001b[39m\n\u001b[31m          🔗 Auto-match Dockets to Stores\u001b[39m\n\u001b[31m        </button>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m    </div>·\u001b[39m\n\u001b[31m    <!-- 3. DOCKET SLIPS — AI OCR -->\u001b[39m\n\u001b[31m    <div class=\\\"card\\\">\u001b[39m\n\u001b[31m      <div class=\\\"card-title\\\"><span class=\\\"ct-dot\\\" style=\\\"background:var(--grn);\\\"></span>Docket Slips — AI OCR</div>·\u001b[39m\n\u001b[31m      <!-- Share target indicator -->\u001b[39m\n\u001b[31m      <div class=\\\"info-tag\\\" id=\\\"shareTargetInfo\\\" style=\\\"display:none;\\\">\u001b[39m\n\u001b[31m        <span>📤</span><span>Images shared from WhatsApp — <strong id=\\\"sharedCount\\\">0</strong> slips received</span>\u001b[39m\n\u001b[31m      </div>·\u001b[39m\n\u001b[31m      <label class=\\\"upload-zone\\\" id=\\\"slipZone\\\" for=\\\"slipFiles\\\" style=\\\"display:block;cursor:pointer;\\\">\u001b[39m\n\u001b[31m        <input type=\\\"file\\\" id=\\\"slipFiles\\\" accept=\\\"image/*\\\" multiple onchange=\\\"handleSlipFiles(this.files)\\\" style=\\\"display:none;\\\">\u001b[39m\n\u001b[31m        <div class=\\\"uz-icon\\\">🖼️</div>\u001b[39m\n\u001b[31m        <div class=\\\"uz-title\\\">Upload Courier Slips</div>\u001b[39m\n\u001b[31m        <div class=\\\"uz-sub\\\">Multiple images · Tap or drag · Camera capture supported</div>\u001b[39m\n\u001b[31m      </label>·\u001b[39m\n\u001b[31m      <!-- OCR Working indicator -->\u001b[39m\n\u001b[31m      <div class=\\\"ocr-working\\\" id=\\\"ocrWorking\\\">\u001b[39m\n\u001b[31m        <span class=\\\"spin\\\">⟳</span>\u001b[39m\n\u001b[31m        <span id=\\\"ocrWorkingText\\\">Running OCR...</span>\u001b[39m\n\u001b[31m      </div>·\u001b[39m\n\u001b[31m      <!-- AI Progress -->\u001b[39m\n\u001b[31m      <div class=\\\"ai-progress\\\" id=\\\"aiProgress\\\">\u001b[39m\n\u001b[31m        <div class=\\\"ai-prog-label\\\" id=\\\"aiProgLabel\\\">Initializing Transformers.js...</div>\u001b[39m\n\u001b[31m        <div class=\\\"ai-prog-bar\\\"><div class=\\\"ai-prog-fill\\\" id=\\\"aiProgFill\\\"></div></div>\u001b[39m\n\u001b[31m      </div>·\u001b[39m\n\u001b[31m      <!-- OCR Results -->\u001b[39m\n\u001b[31m      <div class=\\\"ocr-results\\\" id=\\\"ocrResults\\\">\u001b[39m\n\u001b[31m        <div class=\\\"sec-div\\\"><span>Detected Dockets</span></div>\u001b[39m\n\u001b[31m        <div id=\\\"ocrSummary\\\" style=\\\"display:none;font-size:.72rem;font-family:var(--fnt-mono);\u001b[39m\n\u001b[31m          padding:4px 8px 8px;gap:8px\\\"></div>\u001b[39m\n\u001b[31m        <div class=\\\"ocr-slip-list\\\" id=\\\"ocrSlipList\\\"></div>\u001b[39m\n\u001b[31m        <div class=\\\"btn-row\\\" style=\\\"margin-top:10px;\\\">\u001b[39m\n\u001b[31m          <button class=\\\"btn btn-ghost\\\" onclick=\\\"clearSlips()\\\">Clear All</button>\u001b[39m\n\u001b[31m          <button class=\\\"btn btn-grn\\\" onclick=\\\"applyAllDockets()\\\">✓ Apply All to Consignees</button>\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m    </div>·\u001b[39m\n\u001b[31m    <!-- 4. CONSIGNEES -->\u001b[39m\n\u001b[31m    <div class=\\\"card accent-left\\\">\u001b[39m\n\u001b[31m      <div class=\\\"card-title\\\"><span class=\\\"ct-dot\\\"></span>Consignees</div>\u001b[39m\n\u001b[31m      <div class=\\\"cons-list\\\" id=\\\"consList\\\"></div>\u001b[39m\n\u001b[31m      <button class=\\\"btn btn-ghost\\\" style=\\\"font-size:.8rem;\\\" onclick=\\\"addConsignee()\\\">+ Add Consignee</button>\u001b[39m\n\u001b[31m    </div>·\u001b[39m\n\u001b[31m    <!-- 5. REMARKS -->\u001b[39m\n\u001b[31m    <div class=\\\"card\\\">\u001b[39m\n\u001b[31m      <div class=\\\"card-title\\\"><span class=\\\"ct-dot\\\" style=\\\"background:var(--t3);\\\"></span>Remarks</div>\u001b[39m\n\u001b[31m      <div class=\\\"f\\\" style=\\\"margin-bottom:0;\\\">\u001b[39m\n\u001b[31m        <textarea id=\\\"n_remarks\\\" placeholder=\\\"Instructions, notes, special handling...\\\"></textarea>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m    </div>·\u001b[39m\n\u001b[31m    <!-- ACTIONS -->\u001b[39m\n\u001b[31m    <div class=\\\"btn-row\\\">\u001b[39m\n\u001b[31m      <button class=\\\"btn btn-ghost\\\" onclick=\\\"saveDraft()\\\">💾 Draft</button>\u001b[39m\n\u001b[31m      <button class=\\\"btn btn-orange\\\" onclick=\\\"finaliseDispatch()\\\">✓ Finalise & Log</button>\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m    <div style=\\\"height:16px;\\\"></div>\u001b[39m\n\u001b[31m  </div>·\u001b[39m\n\u001b[31m  <!-- ════════════ DASHBOARD ════════════ -->\u001b[39m\n\u001b[31m  <div class=\\\"pane\\\" id=\\\"pane-dashboard\\\">\u001b[39m\n\u001b[31m    <div class=\\\"dash-stats\\\">\u001b[39m\n\u001b[31m      <div class=\\\"stat-box grn\\\"><div class=\\\"stat-val\\\" id=\\\"st-delivered\\\">0</div><div class=\\\"stat-lbl\\\">Delivered</div></div>\u001b[39m\n\u001b[31m      <div class=\\\"stat-box ylw\\\"><div class=\\\"stat-val\\\" id=\\\"st-transit\\\">0</div><div class=\\\"stat-lbl\\\">In Transit</div></div>\u001b[39m\n\u001b[31m      <div class=\\\"stat-box sky\\\"><div class=\\\"stat-val\\\" id=\\\"st-booked\\\">0</div><div class=\\\"stat-lbl\\\">Booked</div></div>\u001b[39m\n\u001b[31m      <div class=\\\"stat-box red\\\"><div class=\\\"stat-val\\\" id=\\\"st-pending\\\">0</div><div class=\\\"stat-lbl\\\">Pending</div></div>\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m    <div id=\\\"dashFilters\\\" style=\\\"display:flex;gap:6px;flex-wrap:wrap;margin-bottom:12px;\\\">\u001b[39m\n\u001b[31m      <span class=\\\"btn-sm primary\\\" onclick=\\\"filterDash('all',this)\\\" style=\\\"cursor:pointer;\\\">All</span>\u001b[39m\n\u001b[31m      <span class=\\\"btn-sm\\\" onclick=\\\"filterDash('delivered',this)\\\" style=\\\"cursor:pointer;\\\">Delivered</span>\u001b[39m\n\u001b[31m      <span class=\\\"btn-sm\\\" onclick=\\\"filterDash('transit',this)\\\" style=\\\"cursor:pointer;\\\">In Transit</span>\u001b[39m\n\u001b[31m      <span class=\\\"btn-sm\\\" onclick=\\\"filterDash('booked',this)\\\" style=\\\"cursor:pointer;\\\">Booked</span>\u001b[39m\n\u001b[31m      <span class=\\\"btn-sm\\\" onclick=\\\"filterDash('pending',this)\\\" style=\\\"cursor:pointer;\\\">Pending</span>\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m    <div class=\\\"docket-grid\\\" id=\\\"dashGrid\\\">\u001b[39m\n\u001b[31m      <div class=\\\"empty\\\"><div class=\\\"e-icon\\\">📊</div><p>No dispatches yet.<br>Create one to see the dashboard.</p></div>\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m    <div style=\\\"display:flex;gap:8px;margin-top:10px;flex-wrap:wrap;\\\">\u001b[39m\n\u001b[31m      <button class=\\\"btn btn-ghost\\\" style=\\\"flex:2;font-size:.8rem;\\\" id=\\\"btnRefreshAll\\\" onclick=\\\"refreshAllTracking()\\\">\u001b[39m\n\u001b[31m        🔄 Refresh All Tracking\u001b[39m\n\u001b[31m      </button>\u001b[39m\n\u001b[31m      <button class=\\\"btn btn-ghost\\\" style=\\\"flex:1;font-size:.8rem;border-color:var(--sky);color:var(--sky);\\\"\u001b[39m\n\u001b[31m        onclick=\\\"shareSelectedReport()\\\">\u001b[39m\n\u001b[31m        📊 Share Selected\u001b[39m\n\u001b[31m      </button>\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m  </div>·\u001b[39m\n\u001b[31m  <!-- ════════════ TRACK ════════════ -->\u001b[39m\n\u001b[31m  <div class=\\\"pane\\\" id=\\\"pane-track\\\">·\u001b[39m\n\u001b[31m    <!-- Courier selector -->\u001b[39m\n\u001b[31m    <div class=\\\"card\\\" style=\\\"margin-bottom:10px;\\\">\u001b[39m\n\u001b[31m      <div class=\\\"card-title\\\"><span class=\\\"ct-dot\\\" style=\\\"background:var(--sky);\\\"></span>Manual Tracking</div>\u001b[39m\n\u001b[31m      <div style=\\\"font-size:.72rem;color:var(--t2);margin-bottom:10px;line-height:1.5;\\\">\u001b[39m\n\u001b[31m        Track any docket number — select from already dispatched dockets or enter new ones manually.\u001b[39m\n\u001b[31m        SkyKing supports up to <strong>10 dockets at once</strong>; others are tracked one at a time.\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      <div class=\\\"f\\\">\u001b[39m\n\u001b[31m        <label>Courier Company</label>\u001b[39m\n\u001b[31m        <select id=\\\"t_courier\\\" onchange=\\\"buildTrackDocketChips()\\\">\u001b[39m\n\u001b[31m          <option>SkyKing</option><option>DTDC</option><option>Delhivery</option>\u001b[39m\n\u001b[31m          <option>Blue Dart</option><option>FedEx</option><option>Ecom Express</option><option>India Post</option>\u001b[39m\n\u001b[31m        </select>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m    </div>·\u001b[39m\n\u001b[31m    <!-- Docket selector — from existing dispatches -->\u001b[39m\n\u001b[31m    <div class=\\\"card\\\" style=\\\"margin-bottom:10px;\\\">\u001b[39m\n\u001b[31m      <div class=\\\"card-title\\\"><span class=\\\"ct-dot\\\" style=\\\"background:var(--acc);\\\"></span>\u001b[39m\n\u001b[31m        Select from Dispatched Dockets\u001b[39m\n\u001b[31m        <span id=\\\"trackCourierLimit\\\" style=\\\"margin-left:auto;font-size:.65rem;color:var(--t3);font-weight:400;text-transform:none;letter-spacing:0;font-family:var(--fnt-mono);\\\"></span>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      <div id=\\\"trackDocketChips\\\" style=\\\"display:flex;flex-wrap:wrap;gap:6px;margin-bottom:10px;min-height:28px;\\\">\u001b[39m\n\u001b[31m        <span style=\\\"font-size:.72rem;color:var(--t3);font-family:var(--fnt-mono);\\\">No dispatched dockets yet</span>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      <div style=\\\"font-size:.68rem;color:var(--t3);font-family:var(--fnt-mono);margin-bottom:8px;\\\">\u001b[39m\n\u001b[31m        Tap a docket to add/remove from tracking list. Selected shown in orange.\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m    </div>·\u001b[39m\n\u001b[31m    <!-- Manual entry for new dockets -->\u001b[39m\n\u001b[31m    <div class=\\\"card\\\" style=\\\"margin-bottom:10px;\\\">\u001b[39m\n\u001b[31m      <div class=\\\"card-title\\\"><span class=\\\"ct-dot\\\" style=\\\"background:var(--grn);\\\"></span>Add New Docket Numbers</div>\u001b[39m\n\u001b[31m      <div class=\\\"f\\\" style=\\\"margin-bottom:6px;\\\">\u001b[39m\n\u001b[31m        <label>Enter docket numbers (comma or newline separated)</label>\u001b[39m\n\u001b[31m        <textarea id=\\\"t_manual_dockets\\\" placeholder=\\\"443005527&#10;443005528&#10;or: 443005527, 443005528\\\"\u001b[39m\n\u001b[31m          style=\\\"min-height:56px;font-family:var(--fnt-mono);font-size:.82rem;letter-spacing:.5px;\\\"></textarea>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      <button class=\\\"btn-sm primary\\\" onclick=\\\"addManualDockets()\\\" style=\\\"font-size:.72rem;\\\">+ Add to List</button>\u001b[39m\n\u001b[31m    </div>·\u001b[39m\n\u001b[31m    <!-- Selected dockets summary + track button -->\u001b[39m\n\u001b[31m    <div id=\\\"trackSelectedSummary\\\" style=\\\"display:none;\\\" class=\\\"card\\\" style=\\\"margin-bottom:10px;\\\">\u001b[39m\n\u001b[31m      <div style=\\\"display:flex;align-items:center;justify-content:space-between;margin-bottom:8px;\\\">\u001b[39m\n\u001b[31m        <div>\u001b[39m\n\u001b[31m          <span style=\\\"font-family:var(--fnt-mono);font-size:.72rem;color:var(--sky);\\\" id=\\\"trackSelectedCount\\\">0 selected</span>\u001b[39m\n\u001b[31m          <span style=\\\"font-family:var(--fnt-mono);font-size:.65rem;color:var(--t3);margin-left:8px;\\\" id=\\\"trackSelectedList\\\"></span>\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m        <button class=\\\"btn-sm\\\" onclick=\\\"clearTrackSelection()\\\">Clear</button>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      <div class=\\\"btn-row\\\">\u001b[39m\n\u001b[31m        <button class=\\\"btn btn-ghost\\\" onclick=\\\"openTrackOnCourierSite()\\\">🔗 Open Courier Site</button>\u001b[39m\n\u001b[31m        <button class=\\\"btn btn-orange\\\" onclick=\\\"trackSelected()\\\">🔍 Track All via Proxy</button>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m    </div>·\u001b[39m\n\u001b[31m    <div id=\\\"trackResultsArea\\\"></div>\u001b[39m\n\u001b[31m  </div>·\u001b[39m\n\u001b[31m  <!-- ════════════ HISTORY ════════════ -->\u001b[39m\n\u001b[31m  <div class=\\\"pane\\\" id=\\\"pane-history\\\">\u001b[39m\n\u001b[31m    <div id=\\\"historyList\\\">\u001b[39m\n\u001b[31m      <div class=\\\"empty\\\"><div class=\\\"e-icon\\\">📭</div><p>No dispatch history yet.</p></div>\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m  </div>·\u001b[39m\n\u001b[31m  <!-- ════════════ CONFIG ════════════ -->\u001b[39m\n\u001b[31m  <div class=\\\"pane\\\" id=\\\"pane-config\\\">·\u001b[39m\n\u001b[31m    <!-- SHEETS URL — the only thing you type manually in the PWA -->\u001b[39m\n\u001b[31m    <div class=\\\"card\\\" style=\\\"border-color:var(--acc);border-width:2px;\\\">\u001b[39m\n\u001b[31m      <div class=\\\"card-title\\\"><span class=\\\"ct-dot\\\"></span>Google Sheets — Master Config</div>\u001b[39m\n\u001b[31m      <div class=\\\"info-tag\\\" style=\\\"background:rgba(34,197,94,.07);border-color:rgba(34,197,94,.3);\\\">\u001b[39m\n\u001b[31m        <span>✅</span>\u001b[39m\n\u001b[31m        <span>URL is loaded automatically from <strong>config.json</strong> on the VPS — no manual entry needed. All other settings sync from your <strong>Google Sheet</strong>.</span>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      <div class=\\\"f\\\">\u001b[39m\n\u001b[31m        <label>Apps Script URL <span style=\\\"font-size:.65rem;color:var(--t3);font-weight:400;\\\">(auto-loaded from config.json)</span></label>\u001b[39m\n\u001b[31m        <input type=\\\"url\\\" id=\\\"cfg_sheets_url\\\"\u001b[39m\n\u001b[31m          placeholder=\\\"Auto-loaded from server...\\\"\u001b[39m\n\u001b[31m          oninput=\\\"onSheetsUrlChange()\\\" readonly\u001b[39m\n\u001b[31m          style=\\\"opacity:.7;cursor:default;\\\">\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      <div id=\\\"syncStatus\\\" class=\\\"info-tag\\\" style=\\\"display:none;\\\">\u001b[39m\n\u001b[31m        <span id=\\\"syncIcon\\\">⏳</span>\u001b[39m\n\u001b[31m        <span id=\\\"syncMsg\\\">Syncing from sheet...</span>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      <div class=\\\"btn-row\\\">\u001b[39m\n\u001b[31m        <button class=\\\"btn btn-ghost\\\" onclick=\\\"syncFromSheet()\\\">🔄 Sync Now</button>\u001b[39m\n\u001b[31m        <button class=\\\"btn btn-orange\\\" onclick=\\\"saveSheetsUrl()\\\">💾 Save & Sync</button>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m    </div>·\u001b[39m\n\u001b[31m    <!-- READ-ONLY display of what was loaded from sheet -->\u001b[39m\n\u001b[31m    <div class=\\\"card\\\" id=\\\"sheetConfigDisplay\\\" style=\\\"display:none;\\\">\u001b[39m\n\u001b[31m      <div class=\\\"card-title\\\"><span class=\\\"ct-dot\\\" style=\\\"background:var(--grn);\\\"></span>Config Loaded from Sheet\u001b[39m\n\u001b[31m        <span id=\\\"sheetSyncTime\\\" style=\\\"margin-left:auto;font-size:.62rem;color:var(--t3);font-family:var(--fnt-mono);font-weight:400;text-transform:none;letter-spacing:0;\\\"></span>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      <div id=\\\"sheetConfigRows\\\"></div>\u001b[39m\n\u001b[31m      <div class=\\\"info-tag\\\" style=\\\"margin-top:8px;\\\">\u001b[39m\n\u001b[31m        <span>✏️</span><span>To change any value, edit the <strong>Config sheet</strong> in Google Sheets, then tap Sync Now above.</span>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m    </div>·\u001b[39m\n\u001b[31m    <!-- Courier Master (from sheet) -->\u001b[39m\n\u001b[31m    <div class=\\\"card\\\">\u001b[39m\n\u001b[31m      <div class=\\\"card-title\\\"><span class=\\\"ct-dot\\\" style=\\\"background:var(--ylw);\\\"></span>Courier Master</div>\u001b[39m\n\u001b[31m      <div id=\\\"courierMasterList\\\"></div>\u001b[39m\n\u001b[31m      <button class=\\\"btn btn-ghost\\\" style=\\\"margin-top:8px;font-size:.78rem;\\\" onclick=\\\"openCourierMaster()\\\">✏️ Edit in Sheet / Override</button>\u001b[39m\n\u001b[31m    </div>·\u001b[39m\n\u001b[31m    <button class=\\\"btn\\\" style=\\\"background:var(--red-dim);color:var(--red);border:1px solid var(--red);margin-top:4px;\\\" onclick=\\\"clearAll()\\\">🗑️ Clear All Local Data</button>\u001b[39m\n\u001b[31m    <div style=\\\"height:20px;\\\"></div>\u001b[39m\n\u001b[31m  </div>··\u001b[39m\n\u001b[31m<!-- ════ ADDRESS REVIEW PANE ════ -->\u001b[39m\n\u001b[31m<div id=\\\"addrReviewPane\\\" style=\\\"display:none;position:fixed;inset:0;background:var(--bg);z-index:50;overflow-y:auto;padding:14px;\\\">\u001b[39m\n\u001b[31m  <div class=\\\"verify-hdr\\\">\u001b[39m\n\u001b[31m    <button class=\\\"btn-icon\\\" onclick=\\\"document.getElementById('addrReviewPane').style.display='none'\\\" style=\\\"font-size:1.1rem;\\\">←</button>\u001b[39m\n\u001b[31m    <div class=\\\"verify-title\\\">Review AI-Parsed Addresses</div>\u001b[39m\n\u001b[31m  </div>\u001b[39m\n\u001b[31m  <div style=\\\"background:rgba(56,189,248,.08);border:1px solid rgba(56,189,248,.3);border-radius:var(--r-sm);\u001b[39m\n\u001b[31m    padding:8px 12px;margin-bottom:12px;font-size:.72rem;color:var(--sky);\\\">\u001b[39m\n\u001b[31m    ✏️ Review each row — correct name and city if wrong. Raw address shown below each row for reference.\u001b[39m\n\u001b[31m    Corrections are saved and improve future AI parsing automatically.\u001b[39m\n\u001b[31m  </div>\u001b[39m\n\u001b[31m  <div class=\\\"verify-summary\\\" id=\\\"addrReviewSummary\\\"></div>\u001b[39m\n\u001b[31m  <div id=\\\"addrReviewList\\\"></div>\u001b[39m\n\u001b[31m  <div style=\\\"position:sticky;bottom:0;background:var(--bg);padding-top:10px;\u001b[39m\n\u001b[31m    border-top:1px solid var(--bdr);margin-top:8px;\\\">\u001b[39m\n\u001b[31m    <div style=\\\"display:flex;gap:8px;\\\">\u001b[39m\n\u001b[31m      <button class=\\\"btn btn-ghost\\\" onclick=\\\"backToMapping()\\\" style=\\\"flex:1;\\\">← Back to Mapping</button>\u001b[39m\n\u001b[31m      <button class=\\\"btn btn-orange\\\" onclick=\\\"confirmAddressReview()\\\" style=\\\"flex:2;\\\">✓ Confirm &amp; Use These Addresses</button>\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m    <div style=\\\"font-size:.65rem;color:var(--t3);text-align:center;margin-top:5px;font-family:var(--fnt-mono);\\\">\u001b[39m\n\u001b[31m      Edits you made above are preserved when you go back\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m  </div>\u001b[39m\n\u001b[31m</div>·\u001b[39m\n\u001b[31m<!-- ════ VERIFICATION SCREEN ════ -->\u001b[39m\n\u001b[31m<div id=\\\"verifyPane\\\">\u001b[39m\n\u001b[31m  <div class=\\\"verify-hdr\\\">\u001b[39m\n\u001b[31m    <button class=\\\"btn-icon\\\" onclick=\\\"closeVerify()\\\" style=\\\"font-size:1.1rem;\\\">←</button>\u001b[39m\n\u001b[31m    <div class=\\\"verify-title\\\">Verify Before Finalise</div>\u001b[39m\n\u001b[31m    <button class=\\\"btn-sm\\\" onclick=\\\"approveAll()\\\" style=\\\"white-space:nowrap;\\\">✓ Approve All</button>\u001b[39m\n\u001b[31m  </div>\u001b[39m\n\u001b[31m  <div class=\\\"verify-summary\\\" id=\\\"verifySummary\\\"></div>\u001b[39m\n\u001b[31m  <div id=\\\"verifyList\\\"></div>\u001b[39m\n\u001b[31m  <div style=\\\"position:sticky;bottom:0;background:var(--bg);padding-top:10px;\u001b[39m\n\u001b[31m    border-top:1px solid var(--bdr);margin-top:8px;\\\">\u001b[39m\n\u001b[31m    <div style=\\\"display:flex;gap:8px;flex-wrap:wrap;\\\">\u001b[39m\n\u001b[31m      <button class=\\\"btn btn-ghost\\\" onclick=\\\"closeVerify()\\\" style=\\\"flex:1;\\\">← Back to Edit</button>\u001b[39m\n\u001b[31m      <button class=\\\"btn btn-ghost\\\" onclick=\\\"exportCleanSheet()\\\" style=\\\"flex:1;border-color:var(--sky);color:var(--sky);\\\">\u001b[39m\n\u001b[31m        📥 Export Clean Sheet\u001b[39m\n\u001b[31m      </button>\u001b[39m\n\u001b[31m      <button class=\\\"btn btn-orange\\\" id=\\\"btnConfirmFinalise\\\" onclick=\\\"confirmAndFinalise()\\\" style=\\\"flex:2;\\\">\u001b[39m\n\u001b[31m        ✓ Finalise Dispatch\u001b[39m\n\u001b[31m      </button>\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m  </div>\u001b[39m\n\u001b[31m</div>·\u001b[39m\n\u001b[31m</div><!-- /app -->·\u001b[39m\n\u001b[31m<!-- ════ DISPATCH DETAIL SHEET ════ -->\u001b[39m\n\u001b[31m<div class=\\\"overlay\\\" id=\\\"detailOverlay\\\" onclick=\\\"closeOverlay('detailOverlay')\\\">\u001b[39m\n\u001b[31m  <div class=\\\"sheet\\\" onclick=\\\"event.stopPropagation()\\\">\u001b[39m\n\u001b[31m    <div class=\\\"sheet-handle\\\"></div>\u001b[39m\n\u001b[31m    <div id=\\\"detailContent\\\"></div>\u001b[39m\n\u001b[31m  </div>\u001b[39m\n\u001b[31m</div>·\u001b[39m\n\u001b[31m<!-- ════ COURIER MASTER EDITOR ════ -->\u001b[39m\n\u001b[31m<div class=\\\"overlay\\\" id=\\\"courierMasterOverlay\\\" onclick=\\\"closeOverlay('courierMasterOverlay')\\\">\u001b[39m\n\u001b[31m  <div class=\\\"sheet\\\" onclick=\\\"event.stopPropagation()\\\">\u001b[39m\n\u001b[31m    <div class=\\\"sheet-handle\\\"></div>\u001b[39m\n\u001b[31m    <div class=\\\"sheet-title\\\">✏️ Courier Master</div>\u001b[39m\n\u001b[31m    <div id=\\\"courierMasterEditor\\\"></div>\u001b[39m\n\u001b[31m  </div>\u001b[39m\n\u001b[31m</div>·\u001b[39m\n\u001b[31m<!-- ════ MANUAL TRACKING OVERLAY ════ -->\u001b[39m\n\u001b[31m<div class=\\\"overlay\\\" id=\\\"manualTrackOverlay\\\" onclick=\\\"closeOverlay('manualTrackOverlay')\\\" data-docket=\\\"\\\" data-dispatch-id=\\\"\\\">\u001b[39m\n\u001b[31m  <div class=\\\"sheet\\\" onclick=\\\"event.stopPropagation()\\\">\u001b[39m\n\u001b[31m    <div class=\\\"sheet-handle\\\"></div>\u001b[39m\n\u001b[31m    <div class=\\\"sheet-title\\\">📞 Manual Tracking Log</div>\u001b[39m\n\u001b[31m    <div style=\\\"background:var(--sur2);border:1px solid var(--bdr);border-radius:var(--r-sm);\u001b[39m\n\u001b[31m      padding:8px 12px;margin-bottom:12px;font-family:var(--fnt-mono);font-size:.82rem;color:var(--acc);\\\"\u001b[39m\n\u001b[31m      id=\\\"mtDocketDisplay\\\"></div>\u001b[39m\n\u001b[31m    <div class=\\\"row2\\\">\u001b[39m\n\u001b[31m      <div class=\\\"f\\\">\u001b[39m\n\u001b[31m        <label>Status Reported <span class=\\\"req\\\">*</span></label>\u001b[39m\n\u001b[31m        <select id=\\\"mtStatus\\\">\u001b[39m\n\u001b[31m          <option value=\\\"\\\">Select...</option>\u001b[39m\n\u001b[31m          <option value=\\\"delivered\\\">✅ Delivered</option>\u001b[39m\n\u001b[31m          <option value=\\\"not received\\\">❌ Not Received</option>\u001b[39m\n\u001b[31m          <option value=\\\"in transit\\\">🚚 In Transit / On the way</option>\u001b[39m\n\u001b[31m          <option value=\\\"will deliver tomorrow\\\">📅 Will Deliver Tomorrow</option>\u001b[39m\n\u001b[31m          <option value=\\\"refused\\\">🚫 Refused / Returned</option>\u001b[39m\n\u001b[31m          <option value=\\\"partial\\\">📦 Partial Delivery</option>\u001b[39m\n\u001b[31m        </select>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      <div class=\\\"f\\\">\u001b[39m\n\u001b[31m        <label>Called By (Staff) <span class=\\\"req\\\">*</span></label>\u001b[39m\n\u001b[31m        <input type=\\\"text\\\" id=\\\"mtCalledBy\\\" placeholder=\\\"Your name\\\" list=\\\"staffList\\\">\u001b[39m\n\u001b[31m        <datalist id=\\\"staffList\\\">\u001b[39m\n\u001b[31m          <option>Mukesh</option><option>Sachin</option><option>Shambhu</option>\u001b[39m\n\u001b[31m          <option>Umashankar</option><option>Narender</option><option>Rakesh</option>\u001b[39m\n\u001b[31m          <option>Gurpreet</option><option>Ashok</option>\u001b[39m\n\u001b[31m        </datalist>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m    <div class=\\\"row2\\\">\u001b[39m\n\u001b[31m      <div class=\\\"f\\\">\u001b[39m\n\u001b[31m        <label>Contact Number Used</label>\u001b[39m\n\u001b[31m        <input type=\\\"tel\\\" id=\\\"mtContactUsed\\\" placeholder=\\\"Mobile number called\\\">\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      <div class=\\\"f\\\">\u001b[39m\n\u001b[31m        <label>Person Who Confirmed</label>\u001b[39m\n\u001b[31m        <input type=\\\"text\\\" id=\\\"mtContactPerson\\\" placeholder=\\\"Name of person at store\\\">\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m    <div class=\\\"f\\\">\u001b[39m\n\u001b[31m      <label>Remarks</label>\u001b[39m\n\u001b[31m      <textarea id=\\\"mtRemarks\\\" placeholder=\\\"Any notes, delivery details, issues...\\\" style=\\\"min-height:60px;\\\"></textarea>\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m    <div class=\\\"f\\\">\u001b[39m\n\u001b[31m      <label>Call Date &amp; Time</label>\u001b[39m\n\u001b[31m      <input type=\\\"datetime-local\\\" id=\\\"mtCallTime\\\">\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m    <div class=\\\"btn-row\\\">\u001b[39m\n\u001b[31m      <button class=\\\"btn btn-ghost\\\" onclick=\\\"closeOverlay('manualTrackOverlay')\\\">Cancel</button>\u001b[39m\n\u001b[31m      <button class=\\\"btn btn-orange\\\" onclick=\\\"submitManualTracking()\\\">💾 Log &amp; Save</button>\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m  </div>\u001b[39m\n\u001b[31m</div>·\u001b[39m\n\u001b[31m<!-- ════ POD UPLOAD OVERLAY ════ -->\u001b[39m\n\u001b[31m<div class=\\\"overlay\\\" id=\\\"podOverlay\\\" onclick=\\\"closeOverlay('podOverlay')\\\" data-docket=\\\"\\\" data-dispatch-id=\\\"\\\">\u001b[39m\n\u001b[31m  <div class=\\\"sheet\\\" onclick=\\\"event.stopPropagation()\\\">\u001b[39m\n\u001b[31m    <div class=\\\"sheet-handle\\\"></div>\u001b[39m\n\u001b[31m    <div class=\\\"sheet-title\\\">📸 Upload Proof of Delivery</div>\u001b[39m\n\u001b[31m    <div class=\\\"f\\\">\u001b[39m\n\u001b[31m      <label>POD / Delivery Slip Photo</label>\u001b[39m\n\u001b[31m      <input type=\\\"file\\\" id=\\\"podFileInput\\\" accept=\\\"image/*\\\" capture=\\\"environment\\\"\u001b[39m\n\u001b[31m        onchange=\\\"previewPOD(this,'pod')\\\" style=\\\"font-size:.8rem;color:var(--t2);\\\">\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m    <div class=\\\"f\\\">\u001b[39m\n\u001b[31m      <label>Recipient / Receiver Photo (optional)</label>\u001b[39m\n\u001b[31m      <input type=\\\"file\\\" id=\\\"recipientFileInput\\\" accept=\\\"image/*\\\" capture=\\\"environment\\\"\u001b[39m\n\u001b[31m        onchange=\\\"previewPOD(this,'recip')\\\" style=\\\"font-size:.8rem;color:var(--t2);\\\">\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m    <div id=\\\"podPreview\\\" style=\\\"display:flex;gap:8px;flex-wrap:wrap;margin-bottom:12px;\\\"></div>\u001b[39m\n\u001b[31m    <div class=\\\"btn-row\\\">\u001b[39m\n\u001b[31m      <button class=\\\"btn btn-ghost\\\" onclick=\\\"closeOverlay('podOverlay')\\\">Cancel</button>\u001b[39m\n\u001b[31m      <button class=\\\"btn btn-orange\\\" onclick=\\\"uploadPODFiles()\\\">📤 Upload & Mark Delivered</button>\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m  </div>\u001b[39m\n\u001b[31m</div>·\u001b[39m\n\u001b[31m<!-- ════ ADMIN LOGIN OVERLAY ════ -->\u001b[39m\n\u001b[31m<div class=\\\"overlay\\\" id=\\\"adminLoginOverlay\\\" onclick=\\\"closeOverlay('adminLoginOverlay')\\\">\u001b[39m\n\u001b[31m  <div class=\\\"sheet\\\" onclick=\\\"event.stopPropagation()\\\" style=\\\"max-height:320px;\\\">\u001b[39m\n\u001b[31m    <div class=\\\"sheet-handle\\\"></div>\u001b[39m\n\u001b[31m    <div class=\\\"sheet-title\\\">🔐 Admin Access</div>\u001b[39m\n\u001b[31m    <div style=\\\"font-size:.78rem;color:var(--t2);margin-bottom:14px;\\\">Config settings require admin password.</div>\u001b[39m\n\u001b[31m    <div class=\\\"f\\\">\u001b[39m\n\u001b[31m      <label>Password</label>\u001b[39m\n\u001b[31m      <input type=\\\"password\\\" id=\\\"adminPwdInput\\\" placeholder=\\\"Enter admin password\\\"\u001b[39m\n\u001b[31m        onkeydown=\\\"if(event.key==='Enter')submitAdminLogin()\\\">\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m    <div class=\\\"btn-row\\\" style=\\\"margin-top:4px;\\\">\u001b[39m\n\u001b[31m      <button class=\\\"btn btn-ghost\\\" onclick=\\\"closeOverlay('adminLoginOverlay')\\\">Cancel</button>\u001b[39m\n\u001b[31m      <button class=\\\"btn btn-orange\\\" onclick=\\\"submitAdminLogin()\\\">🔓 Login</button>\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m  </div>\u001b[39m\n\u001b[31m</div>·\u001b[39m\n\u001b[31m<!-- ════ TOAST ════ -->\u001b[39m\n\u001b[31m<div class=\\\"toast\\\" id=\\\"toast\\\"></div>·\u001b[39m\n\u001b[31m<!-- ════ SCRIPTS ════ -->\u001b[39m\n\u001b[31m<!-- SheetJS for Excel/CSV parsing -->\u001b[39m\n\u001b[31m<script src=\\\"https://cdnjs.cloudflare.com/ajax/libs/xlsx/0.18.5/xlsx.full.min.js\\\"></script>·\u001b[39m\n\u001b[31m<script type=\\\"module\\\">\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31m// OCR — Transformers.js\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31mimport { pipeline, env } from 'https://cdn.jsdelivr.net/npm/@huggingface/transformers@3.5.2/dist/transformers.min.js';·\u001b[39m\n\u001b[31m// Cache models in browser\u001b[39m\n\u001b[31menv.allowLocalModels = false;\u001b[39m\n\u001b[31menv.useBrowserCache = true;·\u001b[39m\n\u001b[31mlet ocrPipeline = null;\u001b[39m\n\u001b[31mlet ocrLoading = false;·\u001b[39m\n\u001b[31mwindow.initOCR = async function() {\u001b[39m\n\u001b[31m  if (ocrPipeline || ocrLoading) return;\u001b[39m\n\u001b[31m  ocrLoading = true;\u001b[39m\n\u001b[31m  showAIProgress('Loading OCR model (first time ~120MB)...', 5);\u001b[39m\n\u001b[31m  try {\u001b[39m\n\u001b[31m    ocrPipeline = await pipeline('image-to-text', 'Xenova/trocr-base-printed', {\u001b[39m\n\u001b[31m      progress_callback: (p) => {\u001b[39m\n\u001b[31m        if (p.progress) {\u001b[39m\n\u001b[31m          showAIProgress(`Loading: ${p.file || 'model'} (${Math.round(p.progress)}%)`, p.progress);\u001b[39m\n\u001b[31m        }\u001b[39m\n\u001b[31m      }\u001b[39m\n\u001b[31m    });\u001b[39m\n\u001b[31m    showAIProgress('OCR model ready ✓', 100);\u001b[39m\n\u001b[31m    setTimeout(() => hideAIProgress(), 1500);\u001b[39m\n\u001b[31m    showToast('OCR ready ✓', 'ok');\u001b[39m\n\u001b[31m  } catch(e) {\u001b[39m\n\u001b[31m    console.warn('Transformers.js OCR failed:', e);\u001b[39m\n\u001b[31m    hideAIProgress();\u001b[39m\n\u001b[31m    showToast('OCR model load failed — will use Ollama fallback', 'info');\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m  ocrLoading = false;\u001b[39m\n\u001b[31m};·\u001b[39m\n\u001b[31mwindow.runOCROnImage = async function(imageData, filename) {\u001b[39m\n\u001b[31m  const cfg = window.appConfig;·\u001b[39m\n\u001b[31m  // Strategy 1: Transformers.js\u001b[39m\n\u001b[31m  if ((cfg.ocr_mode === 'transformers' || cfg.ocr_mode === 'both') && ocrPipeline) {\u001b[39m\n\u001b[31m    try {\u001b[39m\n\u001b[31m      const result = await ocrPipeline(imageData);\u001b[39m\n\u001b[31m      const text = result[0]?.generated_text || '';\u001b[39m\n\u001b[31m      return extractDocketFromText(text, 'transformers');\u001b[39m\n\u001b[31m    } catch(e) { /* fall through */ }\u001b[39m\n\u001b[31m  }·\u001b[39m\n\u001b[31m  // Strategy 2: Dispatch AI proxy → Ollama LLaVA (server-side, avoids CORS + port issues)\u001b[39m\n\u001b[31m  if (cfg.ocr_mode === 'ollama' || cfg.ocr_mode === 'both') {\u001b[39m\n\u001b[31m    try {\u001b[39m\n\u001b[31m      const resp = await fetch('/dispatch-ai/ocr', {\u001b[39m\n\u001b[31m        method: 'POST',\u001b[39m\n\u001b[31m        headers: { 'Content-Type': 'application/json' },\u001b[39m\n\u001b[31m        body: JSON.stringify({ imageBase64: imageData }),\u001b[39m\n\u001b[31m        signal: AbortSignal.timeout(180000)\u001b[39m\n\u001b[31m      });\u001b[39m\n\u001b[31m      const data = await resp.json();\u001b[39m\n\u001b[31m      if (data.success && data.docket) {\u001b[39m\n\u001b[31m        return { docket: data.docket, confidence: data.confidence, source: 'ollama' };\u001b[39m\n\u001b[31m      }\u001b[39m\n\u001b[31m    } catch(e) { /* fall through */ }\u001b[39m\n\u001b[31m  }·\u001b[39m\n\u001b[31m  return { docket: '', confidence: 0, source: 'failed' };\u001b[39m\n\u001b[31m};·\u001b[39m\n\u001b[31mfunction extractDocketFromText(text, source) {\u001b[39m\n\u001b[31m  // Pattern: typical courier docket = 8-15 digit numbers, sometimes with letters\u001b[39m\n\u001b[31m  const patterns = [\u001b[39m\n\u001b[31m    /\\\\b([A-Z]{0,3}\\\\d{8,15}[A-Z]{0,3})\\\\b/g,   // alphanumeric tracking\u001b[39m\n\u001b[31m    /\\\\b(\\\\d{9,15})\\\\b/g,                          // pure numeric\u001b[39m\n\u001b[31m    /\\\\b([A-Z]{2,4}\\\\d{6,12})\\\\b/g,               // letter prefix\u001b[39m\n\u001b[31m  ];\u001b[39m\n\u001b[31m  const candidates = [];\u001b[39m\n\u001b[31m  for (const pat of patterns) {\u001b[39m\n\u001b[31m    let m;\u001b[39m\n\u001b[31m    while ((m = pat.exec(text)) !== null) {\u001b[39m\n\u001b[31m      if (!candidates.includes(m[1])) candidates.push(m[1]);\u001b[39m\n\u001b[31m    }\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m  if (candidates.length === 0) return { docket: text.trim().slice(0, 20), confidence: 0.3, source };\u001b[39m\n\u001b[31m  // Pick longest candidate (usually most complete docket)\u001b[39m\n\u001b[31m  const best = candidates.sort((a, b) => b.length - a.length)[0];\u001b[39m\n\u001b[31m  const confidence = candidates.length === 1 ? 0.85 : 0.7;\u001b[39m\n\u001b[31m  return { docket: best, confidence, source };\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// Expose for non-module code\u001b[39m\n\u001b[31mwindow._ocrReady = () => ocrPipeline !== null;\u001b[39m\n\u001b[31m</script>·\u001b[39m\n\u001b[31m<script>\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31m// STATE & STORAGE\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31mlet dispatches = [];\u001b[39m\n\u001b[31mlet stores = [];\u001b[39m\n\u001b[31mlet slipOCRData = [];\u001b[39m\n\u001b[31mlet dashFilter = 'all';·\u001b[39m\n\u001b[31m// Only sheets_url is persisted locally — all other config comes from the sheet\u001b[39m\n\u001b[31mlet _trackingInProgress = false;\u001b[39m\n\u001b[31mwindow.appConfig = {\u001b[39m\n\u001b[31m  sheets_url:      'https://script.google.com/macros/s/AKfycbys8HrToDv2u_dRvSFnNqmO9bnyAO1s8oSprUydpVQnNQutq4tuUY71cVCH24urbvM9/exec',\u001b[39m\n\u001b[31m  webhook_url:     '',\u001b[39m\n\u001b[31m  proxy_url:       '',\u001b[39m\n\u001b[31m  ollama_url:      'http://srv1111289.hstgr.cloud:11434',\u001b[39m\n\u001b[31m  ocr_mode:        'both',\u001b[39m\n\u001b[31m  company:         '360 Degree Logical Marketing',\u001b[39m\n\u001b[31m  city:            'Chandigarh',\u001b[39m\n\u001b[31m  auto_log:        'true',\u001b[39m\n\u001b[31m  refresh_on_load: 'false',\u001b[39m\n\u001b[31m};·\u001b[39m\n\u001b[31mconst COURIER_MASTER = {\u001b[39m\n\u001b[31m  'SkyKing': {\u001b[39m\n\u001b[31m    trackUrl: 'https://skyking.co/track?cno={docket}',\u001b[39m\n\u001b[31m    trackBulkUrl: 'https://skyking.co/track',\u001b[39m\n\u001b[31m    apiProxy: '/track-proxy/skyking?cno={docket}',\u001b[39m\n\u001b[31m    maxBulk: 10\u001b[39m\n\u001b[31m  },\u001b[39m\n\u001b[31m  'DTDC': {\u001b[39m\n\u001b[31m    trackUrl: 'https://www.dtdc.in/tracking.asp',\u001b[39m\n\u001b[31m    queryParam: 'trkId',\u001b[39m\n\u001b[31m    maxBulk: 1\u001b[39m\n\u001b[31m  },\u001b[39m\n\u001b[31m  'Delhivery': {\u001b[39m\n\u001b[31m    trackUrl: 'https://www.delhivery.com/track/package/{docket}',\u001b[39m\n\u001b[31m    maxBulk: 1\u001b[39m\n\u001b[31m  },\u001b[39m\n\u001b[31m  'Blue Dart': {\u001b[39m\n\u001b[31m    trackUrl: 'https://www.bluedart.com/tracking',\u001b[39m\n\u001b[31m    maxBulk: 1\u001b[39m\n\u001b[31m  },\u001b[39m\n\u001b[31m  'FedEx': {\u001b[39m\n\u001b[31m    trackUrl: 'https://www.fedex.com/fedextrack/?trknbr={docket}',\u001b[39m\n\u001b[31m    maxBulk: 10\u001b[39m\n\u001b[31m  },\u001b[39m\n\u001b[31m  'Ecom Express': {\u001b[39m\n\u001b[31m    trackUrl: 'https://ecomexpress.in/tracking/?awb_field={docket}',\u001b[39m\n\u001b[31m    maxBulk: 1\u001b[39m\n\u001b[31m  },\u001b[39m\n\u001b[31m  'India Post': {\u001b[39m\n\u001b[31m    trackUrl: 'https://www.indiapost.gov.in/VAS/Pages/trackconsignment.aspx',\u001b[39m\n\u001b[31m    maxBulk: 1\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m};·\u001b[39m\n\u001b[31mfunction loadData() {\u001b[39m\n\u001b[31m  try {\u001b[39m\n\u001b[31m    dispatches = JSON.parse(localStorage.getItem('360lm_disp_v2') || '[]');\u001b[39m\n\u001b[31m    const saved = JSON.parse(localStorage.getItem('360lm_disp_sheets_url') || '{}');\u001b[39m\n\u001b[31m    if (saved.sheets_url) window.appConfig.sheets_url = saved.sheets_url;\u001b[39m\n\u001b[31m  } catch(e) {}\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31mfunction saveData() {\u001b[39m\n\u001b[31m  try {\u001b[39m\n\u001b[31m    localStorage.setItem('360lm_disp_v2', JSON.stringify(dispatches));\u001b[39m\n\u001b[31m  } catch(e) {\u001b[39m\n\u001b[31m    if (e.name === 'QuotaExceededError' || e.code === 22) {\u001b[39m\n\u001b[31m      showToast('Storage full — old dispatches cleared. Data is safe in Postgres.', 'err');\u001b[39m\n\u001b[31m      // Drop delivered dispatches older than 30 days to free space\u001b[39m\n\u001b[31m      const cutoff = Date.now() - 30 * 24 * 3600 * 1000;\u001b[39m\n\u001b[31m      dispatches = dispatches.filter(d =>\u001b[39m\n\u001b[31m        d.status !== 'draft' && (\u001b[39m\n\u001b[31m          d.status !== 'active' ||\u001b[39m\n\u001b[31m          new Date(d.createdAt || 0).getTime() > cutoff\u001b[39m\n\u001b[31m        ) ? true :\u001b[39m\n\u001b[31m        d.status === 'draft' || new Date(d.createdAt || 0).getTime() > cutoff\u001b[39m\n\u001b[31m      );\u001b[39m\n\u001b[31m      try { localStorage.setItem('360lm_disp_v2', JSON.stringify(dispatches)); } catch(_) {}\u001b[39m\n\u001b[31m    }\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// ── Postgres DB sync ──────────────────────────────────────────────────────\u001b[39m\n\u001b[31mconst _DB        = '/db';\u001b[39m\n\u001b[31m// PostgREST v12: Accept-Profile for reads, Content-Profile for writes\u001b[39m\n\u001b[31mconst _DHDR_READ  = { 'Accept-Profile': 'dispatch' };\u001b[39m\n\u001b[31mconst _DHDR_WRITE = { 'Content-Type': 'application/json', 'Content-Profile': 'dispatch', 'Accept-Profile': 'dispatch' };\u001b[39m\n\u001b[31mconst _DHDR_UPSERT = { ..._DHDR_WRITE, 'Prefer': 'resolution=merge-duplicates,return=minimal' };·\u001b[39m\n\u001b[31mfunction _dispRow(d) {\u001b[39m\n\u001b[31m  return {\u001b[39m\n\u001b[31m    dispatch_id:    d.id,\u001b[39m\n\u001b[31m    client:         d.client    || '',\u001b[39m\n\u001b[31m    job_ref:        d.jobref    || null,\u001b[39m\n\u001b[31m    courier:        d.courier   || '',\u001b[39m\n\u001b[31m    dispatch_date:  d.dispatchDate || null,\u001b[39m\n\u001b[31m    status:         d.status    || 'active',\u001b[39m\n\u001b[31m    total_count:    (d.consignees || []).length,\u001b[39m\n\u001b[31m    docketed_count: (d.consignees || []).filter(c => c.docket).length,\u001b[39m\n\u001b[31m    remarks:        d.remarks   || null,\u001b[39m\n\u001b[31m    created_by:     window._hubUser?.id || 'unknown',\u001b[39m\n\u001b[31m  };\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction _consRow(c, dispatchId, idx) {\u001b[39m\n\u001b[31m  return {\u001b[39m\n\u001b[31m    cons_id:            `${dispatchId}-${idx}`,\u001b[39m\n\u001b[31m    dispatch_id:        dispatchId,\u001b[39m\n\u001b[31m    seq_no:             idx,\u001b[39m\n\u001b[31m    name:               c.name          || '',\u001b[39m\n\u001b[31m    city:               c.city          || null,\u001b[39m\n\u001b[31m    address:            c.address       || null,\u001b[39m\n\u001b[31m    phone:              c.phone         || null,\u001b[39m\n\u001b[31m    pincode:            c.pincode       || null,\u001b[39m\n\u001b[31m    material:           c.material      || null,\u001b[39m\n\u001b[31m    qty:                c.qty           || null,\u001b[39m\n\u001b[31m    docket:             c.docket        || null,\u001b[39m\n\u001b[31m    status:             c.status        || 'pending',\u001b[39m\n\u001b[31m    last_event:         c.lastEvent     || null,\u001b[39m\n\u001b[31m    last_scan_location: c.lastScanLocation || null,\u001b[39m\n\u001b[31m    last_tracked_at:    c.lastTrackedAt || null,\u001b[39m\n\u001b[31m    manual_status:      c.manualStatus  || null,\u001b[39m\n\u001b[31m    manual_delivered:   !!c.manualDelivered,\u001b[39m\n\u001b[31m    manual_called_by:   c.manualCalledBy || null,\u001b[39m\n\u001b[31m    manual_remarks:     c.manualRemarks || null,\u001b[39m\n\u001b[31m    pod_uploaded:       !!c.podUploaded,\u001b[39m\n\u001b[31m  };\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31masync function syncDispatchToDB(d) {\u001b[39m\n\u001b[31m  if (!d || d.status === 'draft') return; // never sync drafts\u001b[39m\n\u001b[31m  try {\u001b[39m\n\u001b[31m    await fetch(`${_DB}/dispatches`, {\u001b[39m\n\u001b[31m      method: 'POST', headers: _DHDR_UPSERT,\u001b[39m\n\u001b[31m      body: JSON.stringify(_dispRow(d)),\u001b[39m\n\u001b[31m    });\u001b[39m\n\u001b[31m    const rows = (d.consignees || []).map((c, i) => _consRow(c, d.id, i));\u001b[39m\n\u001b[31m    if (rows.length) {\u001b[39m\n\u001b[31m      await fetch(`${_DB}/consignees`, {\u001b[39m\n\u001b[31m        method: 'POST', headers: _DHDR_UPSERT,\u001b[39m\n\u001b[31m        body: JSON.stringify(rows),\u001b[39m\n\u001b[31m      });\u001b[39m\n\u001b[31m    }\u001b[39m\n\u001b[31m  } catch(e) { console.warn('[dispatch-db] sync error:', e.message); }\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31masync function syncConsigneeToDB(c, dispatchId, idx) {\u001b[39m\n\u001b[31m  try {\u001b[39m\n\u001b[31m    await fetch(`${_DB}/consignees`, {\u001b[39m\n\u001b[31m      method: 'POST', headers: _DHDR_UPSERT,\u001b[39m\n\u001b[31m      body: JSON.stringify(_consRow(c, dispatchId, idx)),\u001b[39m\n\u001b[31m    });\u001b[39m\n\u001b[31m    // Log tracking event\u001b[39m\n\u001b[31m    if (c.lastEvent && c.status) {\u001b[39m\n\u001b[31m      fetch(`${_DB}/tracking_events`, {\u001b[39m\n\u001b[31m        method: 'POST', headers: _DHDR_WRITE,\u001b[39m\n\u001b[31m        body: JSON.stringify({\u001b[39m\n\u001b[31m          docket: c.docket, dispatch_id: dispatchId,\u001b[39m\n\u001b[31m          cons_id: `${dispatchId}-${idx}`,\u001b[39m\n\u001b[31m          status: c.status, event_text: c.lastEvent,\u001b[39m\n\u001b[31m          scan_location: c.lastScanLocation || null,\u001b[39m\n\u001b[31m        }),\u001b[39m\n\u001b[31m      }).catch(() => {});\u001b[39m\n\u001b[31m    }\u001b[39m\n\u001b[31m  } catch(e) { console.warn('[dispatch-db] consignee sync error:', e.message); }\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31masync function loadFromDB() {\u001b[39m\n\u001b[31m  try {\u001b[39m\n\u001b[31m    const [dResp, cResp] = await Promise.all([\u001b[39m\n\u001b[31m      fetch(`${_DB}/dispatches?order=created_at.desc&limit=100`, { headers: _DHDR_READ }),\u001b[39m\n\u001b[31m      fetch(`${_DB}/consignees?order=dispatch_id,seq_no`, { headers: _DHDR_READ }),\u001b[39m\n\u001b[31m    ]);\u001b[39m\n\u001b[31m    if (!dResp.ok) return;\u001b[39m\n\u001b[31m    const dbDispatches = await dResp.json();\u001b[39m\n\u001b[31m    const dbConsignees = cResp.ok ? await cResp.json() : [];\u001b[39m\n\u001b[31m    if (!dbDispatches.length) return;·\u001b[39m\n\u001b[31m    const dbIds = new Set(dbDispatches.map(d => d.dispatch_id));\u001b[39m\n\u001b[31m    const localOnly = dispatches.filter(d => !dbIds.has(d.id));·\u001b[39m\n\u001b[31m    const fromDB = dbDispatches.map(d => {\u001b[39m\n\u001b[31m      const cons = dbConsignees.filter(c => c.dispatch_id === d.dispatch_id);\u001b[39m\n\u001b[31m      return {\u001b[39m\n\u001b[31m        id:           d.dispatch_id,\u001b[39m\n\u001b[31m        client:       d.client,\u001b[39m\n\u001b[31m        jobref:       d.job_ref,\u001b[39m\n\u001b[31m        courier:      d.courier,\u001b[39m\n\u001b[31m        dispatchDate: d.dispatch_date,\u001b[39m\n\u001b[31m        status:       d.status,\u001b[39m\n\u001b[31m        remarks:      d.remarks,\u001b[39m\n\u001b[31m        createdAt:    d.created_at,\u001b[39m\n\u001b[31m        consignees:   cons.map(c => ({\u001b[39m\n\u001b[31m          name:              c.name,\u001b[39m\n\u001b[31m          city:              c.city,\u001b[39m\n\u001b[31m          address:           c.address,\u001b[39m\n\u001b[31m          phone:             c.phone,\u001b[39m\n\u001b[31m          pincode:           c.pincode,\u001b[39m\n\u001b[31m          material:          c.material,\u001b[39m\n\u001b[31m          qty:               c.qty,\u001b[39m\n\u001b[31m          docket:            c.docket,\u001b[39m\n\u001b[31m          status:            c.status,\u001b[39m\n\u001b[31m          lastEvent:         c.last_event,\u001b[39m\n\u001b[31m          lastScanLocation:  c.last_scan_location,\u001b[39m\n\u001b[31m          lastTrackedAt:     c.last_tracked_at,\u001b[39m\n\u001b[31m          manualStatus:      c.manual_status,\u001b[39m\n\u001b[31m          manualDelivered:   c.manual_delivered,\u001b[39m\n\u001b[31m          manualCalledBy:    c.manual_called_by,\u001b[39m\n\u001b[31m          manualRemarks:     c.manual_remarks,\u001b[39m\n\u001b[31m          podUploaded:       c.pod_uploaded,\u001b[39m\n\u001b[31m          _dbIdx:            c.seq_no,\u001b[39m\n\u001b[31m        })),\u001b[39m\n\u001b[31m      };\u001b[39m\n\u001b[31m    });·\u001b[39m\n\u001b[31m    dispatches = [...fromDB, ...localOnly];\u001b[39m\n\u001b[31m    saveData();\u001b[39m\n\u001b[31m    renderDashboard();\u001b[39m\n\u001b[31m    updateHistCount();\u001b[39m\n\u001b[31m  } catch(e) { console.warn('[dispatch-db] load error:', e.message); }\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31mfunction saveSheetsUrlLocal() {\u001b[39m\n\u001b[31m  localStorage.setItem('360lm_disp_sheets_url', JSON.stringify({ sheets_url: window.appConfig.sheets_url }));\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m/**\u001b[39m\n\u001b[31m * Fetch config.json from the VPS server (same origin).\u001b[39m\n\u001b[31m * This file contains the sheets_url — no manual entry needed.\u001b[39m\n\u001b[31m * Falls back to localStorage cache if fetch fails (offline).\u001b[39m\n\u001b[31m */\u001b[39m\n\u001b[31masync function loadServerConfig() {\u001b[39m\n\u001b[31m  try {\u001b[39m\n\u001b[31m    const resp = await fetch('./config.json', { cache: 'no-store' });\u001b[39m\n\u001b[31m    if (resp.ok) {\u001b[39m\n\u001b[31m      const cfg = await resp.json();\u001b[39m\n\u001b[31m      if (cfg.sheets_url) {\u001b[39m\n\u001b[31m        window.appConfig.sheets_url = cfg.sheets_url;\u001b[39m\n\u001b[31m        saveSheetsUrlLocal(); // cache it for offline use\u001b[39m\n\u001b[31m        document.getElementById('cfg_sheets_url').value = cfg.sheets_url;\u001b[39m\n\u001b[31m        return true;\u001b[39m\n\u001b[31m      }\u001b[39m\n\u001b[31m    }\u001b[39m\n\u001b[31m  } catch(e) {\u001b[39m\n\u001b[31m    console.log('config.json fetch failed, using cached URL:', e.message);\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m  return false;\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31m// INIT\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31m// ── Hub auth gate ─────────────────────────────────────────────────────────\u001b[39m\n\u001b[31mwindow._hubUser = null;\u001b[39m\n\u001b[31m(function() {\u001b[39m\n\u001b[31m  try {\u001b[39m\n\u001b[31m    const hubLogin = '/hub/?next=' + encodeURIComponent(location.pathname + location.search);\u001b[39m\n\u001b[31m    const s = JSON.parse(localStorage.getItem('lm360-session') || 'null');\u001b[39m\n\u001b[31m    if (!s || !s.empId || !s.name) { window.location.replace(hubLogin); return; }\u001b[39m\n\u001b[31m    if (Date.now() - new Date(s.loginAt).getTime() > 12 * 3600 * 1000) {\u001b[39m\n\u001b[31m      window.location.replace(hubLogin); return;\u001b[39m\n\u001b[31m    }\u001b[39m\n\u001b[31m    window._hubUser = { id: s.empId, name: s.name, role: s.role };\u001b[39m\n\u001b[31m  } catch(_) { window.location.replace('/hub/?next=' + encodeURIComponent(location.pathname + location.search)); }\u001b[39m\n\u001b[31m})();·\u001b[39m\n\u001b[31mwindow.addEventListener('load', async () => {\u001b[39m\n\u001b[31m  if (!window._hubUser) return; // should never reach here — gate redirected already\u001b[39m\n\u001b[31m  setTimeout(() => {\u001b[39m\n\u001b[31m    document.getElementById('splash').style.opacity = '0';\u001b[39m\n\u001b[31m    setTimeout(() => {\u001b[39m\n\u001b[31m      document.getElementById('splash').style.display = 'none';\u001b[39m\n\u001b[31m      document.getElementById('app').style.display = 'flex';\u001b[39m\n\u001b[31m    }, 450);\u001b[39m\n\u001b[31m  }, 1400);\u001b[39m\n\u001b[31m  loadData();\u001b[39m\n\u001b[31m  // Auto-authenticate config tab for admin users (no password needed)\u001b[39m\n\u001b[31m  if (['harish', 'pramod'].includes(window._hubUser?.id)) _adminAuthed = true;\u001b[39m\n\u001b[31m  setToday();\u001b[39m\n\u001b[31m  addConsignee();\u001b[39m\n\u001b[31m  updateHistCount();\u001b[39m\n\u001b[31m  setInterval(updateNetDot, 5000);\u001b[39m\n\u001b[31m  updateNetDot();\u001b[39m\n\u001b[31m  checkShareTarget();\u001b[39m\n\u001b[31m  setupDragDrop();·\u001b[39m\n\u001b[31m  // Step 1: fetch sheets_url from VPS config.json (auto, no user input)\u001b[39m\n\u001b[31m  await loadServerConfig();·\u001b[39m\n\u001b[31m  // Step 2: populate config tab field\u001b[39m\n\u001b[31m  document.getElementById('cfg_sheets_url').value = window.appConfig.sheets_url || '';·\u001b[39m\n\u001b[31m  // Step 3: load dispatches from Postgres in background (merge with local)\u001b[39m\n\u001b[31m  loadFromDB(); // non-blocking — updates UI when data arrives·\u001b[39m\n\u001b[31m  // Step 4: sync full config from Google Sheet silently\u001b[39m\n\u001b[31m  if (window.appConfig.sheets_url) {\u001b[39m\n\u001b[31m    syncFromSheet(true); // silent on startup\u001b[39m\n\u001b[31m  } else {\u001b[39m\n\u001b[31m    // Only show config tab if no URL found anywhere\u001b[39m\n\u001b[31m    showPane('config');\u001b[39m\n\u001b[31m    showToast('No config.json found on server', 'err');\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m  // Pre-warm OCR after delay\u001b[39m\n\u001b[31m  setTimeout(() => { if (typeof window.initOCR === 'function') window.initOCR(); }, 4000);\u001b[39m\n\u001b[31m});·\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31m// WEB SHARE TARGET\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31masync function checkShareTarget() {\u001b[39m\n\u001b[31m  // Check URL params for share target\u001b[39m\n\u001b[31m  const params = new URLSearchParams(window.location.search);\u001b[39m\n\u001b[31m  const action = params.get('action');\u001b[39m\n\u001b[31m  if (action !== 'share') return;·\u001b[39m\n\u001b[31m  // Check for shared files via Share Target API\u001b[39m\n\u001b[31m  if ('launchQueue' in window) {\u001b[39m\n\u001b[31m    window.launchQueue.setConsumer(async launchParams => {\u001b[39m\n\u001b[31m      if (!launchParams.files || launchParams.files.length === 0) return;\u001b[39m\n\u001b[31m      const files = await Promise.all(launchParams.files.map(f => f.getFile()));\u001b[39m\n\u001b[31m      const imageFiles = files.filter(f => f.type.startsWith('image/'));\u001b[39m\n\u001b[31m      if (imageFiles.length > 0) {\u001b[39m\n\u001b[31m        document.getElementById('shareTargetInfo').style.display = 'flex';\u001b[39m\n\u001b[31m        document.getElementById('sharedCount').textContent = imageFiles.length;\u001b[39m\n\u001b[31m        document.getElementById('shareBanner').classList.add('show');\u001b[39m\n\u001b[31m        setTimeout(() => document.getElementById('shareBanner').classList.remove('show'), 4000);\u001b[39m\n\u001b[31m        showPane('new');\u001b[39m\n\u001b[31m        await handleSlipFiles(imageFiles);\u001b[39m\n\u001b[31m      }\u001b[39m\n\u001b[31m    });\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction dismissShare() {\u001b[39m\n\u001b[31m  document.getElementById('shareBanner').classList.remove('show');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31m// DRAG & DROP\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31mfunction setupDragDrop() {\u001b[39m\n\u001b[31m  const zone = document.getElementById('slipZone');\u001b[39m\n\u001b[31m  zone.addEventListener('dragover', e => { e.preventDefault(); zone.classList.add('drag'); });\u001b[39m\n\u001b[31m  zone.addEventListener('dragleave', () => zone.classList.remove('drag'));\u001b[39m\n\u001b[31m  zone.addEventListener('drop', e => {\u001b[39m\n\u001b[31m    e.preventDefault();\u001b[39m\n\u001b[31m    zone.classList.remove('drag');\u001b[39m\n\u001b[31m    const files = Array.from(e.dataTransfer.files).filter(f => f.type.startsWith('image/'));\u001b[39m\n\u001b[31m    if (files.length) handleSlipFiles(files);\u001b[39m\n\u001b[31m  });\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31m// SLIP OCR HANDLER\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31masync function handleSlipFiles(files) {\u001b[39m\n\u001b[31m  const fileArr = Array.from(files);\u001b[39m\n\u001b[31m  if (!fileArr.length) return;\u001b[39m\n\u001b[31m  // If dockets already mapped from store list — allow upload but skip OCR extraction\u001b[39m\n\u001b[31m  const skipOCR = shouldSkipOCR();\u001b[39m\n\u001b[31m  if (skipOCR) {\u001b[39m\n\u001b[31m    showToast('Dockets already mapped — slips saved for reference only', 'info');\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m  showOCRWorking(true, `Processing ${fileArr.length} slip(s)...`);·\u001b[39m\n\u001b[31m  for (let i = 0; i < fileArr.length; i++) {\u001b[39m\n\u001b[31m    const file = fileArr[i];\u001b[39m\n\u001b[31m    showOCRWorking(true, `OCR: ${i+1}/${fileArr.length} — ${file.name}`);\u001b[39m\n\u001b[31m    const imageData = await readFileAsDataURL(file);\u001b[39m\n\u001b[31m    let result = { docket: '', confidence: 0, source: 'manual' };\u001b[39m\n\u001b[31m    if (!skipOCR) {\u001b[39m\n\u001b[31m      try {\u001b[39m\n\u001b[31m        result = await window.runOCROnImage(imageData, file.name);\u001b[39m\n\u001b[31m      } catch(e) {}\u001b[39m\n\u001b[31m    } else {\u001b[39m\n\u001b[31m      result = { docket: '', confidence: 1, source: 'skipped — docket pre-mapped' };\u001b[39m\n\u001b[31m    }\u001b[39m\n\u001b[31m    slipOCRData.push({ file, imageData, filename: file.name, ...result });\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m  showOCRWorking(false);\u001b[39m\n\u001b[31m  renderOCRResults();\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction readFileAsDataURL(file) {\u001b[39m\n\u001b[31m  return new Promise((res, rej) => {\u001b[39m\n\u001b[31m    const r = new FileReader();\u001b[39m\n\u001b[31m    r.onload = e => res(e.target.result);\u001b[39m\n\u001b[31m    r.onerror = rej;\u001b[39m\n\u001b[31m    r.readAsDataURL(file);\u001b[39m\n\u001b[31m  });\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction renderOCRResults() {\u001b[39m\n\u001b[31m  if (!slipOCRData.length) return;\u001b[39m\n\u001b[31m  const list = document.getElementById('ocrSlipList');\u001b[39m\n\u001b[31m  document.getElementById('ocrResults').classList.add('visible');·\u001b[39m\n\u001b[31m  const needReview = slipOCRData.filter(s => s.confidence < 0.65 && s.docket).length;\u001b[39m\n\u001b[31m  const autoOk     = slipOCRData.filter(s => s.confidence >= 0.88).length;\u001b[39m\n\u001b[31m  const summary    = document.getElementById('ocrSummary');\u001b[39m\n\u001b[31m  if (summary) {\u001b[39m\n\u001b[31m    summary.style.display = '';\u001b[39m\n\u001b[31m    summary.innerHTML =\u001b[39m\n\u001b[31m      `<span style=\\\"color:var(--grn)\\\">✓ ${autoOk} auto-accepted</span>` +\u001b[39m\n\u001b[31m      (needReview ? `&nbsp;·&nbsp;<span style=\\\"color:var(--ylw)\\\">⚠ ${needReview} need manual check</span>` : '');\u001b[39m\n\u001b[31m  }·\u001b[39m\n\u001b[31m  list.innerHTML = slipOCRData.map((s, i) => {\u001b[39m\n\u001b[31m    const autoAccepted = s.confidence >= 0.88 && s.docket;\u001b[39m\n\u001b[31m    const needsCheck   = s.confidence < 0.65  && s.docket;\u001b[39m\n\u001b[31m    const confClass = autoAccepted ? 'conf-high' : needsCheck ? 'conf-low' : 'conf-med';\u001b[39m\n\u001b[31m    const confLabel = autoAccepted\u001b[39m\n\u001b[31m      ? `✓ ${Math.round(s.confidence * 100)}% — auto`\u001b[39m\n\u001b[31m      : needsCheck\u001b[39m\n\u001b[31m        ? `⚠ ${Math.round(s.confidence * 100)}% — verify`\u001b[39m\n\u001b[31m        : `${Math.round(s.confidence * 100)}%`;\u001b[39m\n\u001b[31m    const borderStyle = needsCheck ? 'border:1px solid var(--ylw);' : '';\u001b[39m\n\u001b[31m    return `\u001b[39m\n\u001b[31m    <div class=\\\"ocr-slip\\\" id=\\\"ocrSlip_${i}\\\" style=\\\"${borderStyle}\\\">\u001b[39m\n\u001b[31m      <img src=\\\"${s.imageData}\\\" alt=\\\"slip ${i+1}\\\">\u001b[39m\n\u001b[31m      <div class=\\\"ocr-slip-body\\\">\u001b[39m\n\u001b[31m        <div class=\\\"ocr-slip-name\\\">${s.filename}</div>\u001b[39m\n\u001b[31m        <input type=\\\"text\\\" class=\\\"ocr-slip-docket\\\" id=\\\"ocrDocket_${i}\\\"\u001b[39m\n\u001b[31m          value=\\\"${s.docket}\\\" placeholder=\\\"Docket number\\\"\u001b[39m\n\u001b[31m          ${autoAccepted ? 'style=\\\"opacity:.6\\\"' : ''}\u001b[39m\n\u001b[31m          oninput=\\\"slipOCRData[${i}].docket=this.value;slipOCRData[${i}].confidence=0.5\\\">\u001b[39m\n\u001b[31m        <div class=\\\"ocr-slip-status\\\">\u001b[39m\n\u001b[31m          <span class=\\\"ocr-confidence ${confClass}\\\">${confLabel}</span>\u001b[39m\n\u001b[31m          <span style=\\\"color:var(--t4);font-family:var(--fnt-mono);font-size:.6rem;\\\">via ${s.source}</span>\u001b[39m\n\u001b[31m          <button class=\\\"ocr-ai-btn\\\" onclick=\\\"reOCR(${i})\\\">🔄 Re-OCR</button>\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m    </div>`;\u001b[39m\n\u001b[31m  }).join('');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31masync function reOCR(i) {\u001b[39m\n\u001b[31m  const s = slipOCRData[i];\u001b[39m\n\u001b[31m  showOCRWorking(true, `Re-running OCR on ${s.filename}...`);\u001b[39m\n\u001b[31m  try {\u001b[39m\n\u001b[31m    const result = await window.runOCROnImage(s.imageData, s.filename);\u001b[39m\n\u001b[31m    slipOCRData[i] = { ...s, ...result };\u001b[39m\n\u001b[31m    renderOCRResults();\u001b[39m\n\u001b[31m    showToast('Re-OCR complete', 'ok');\u001b[39m\n\u001b[31m  } catch(e) {\u001b[39m\n\u001b[31m    showToast('OCR failed', 'err');\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m  showOCRWorking(false);\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction clearSlips() {\u001b[39m\n\u001b[31m  slipOCRData = [];\u001b[39m\n\u001b[31m  document.getElementById('ocrResults').classList.remove('visible');\u001b[39m\n\u001b[31m  document.getElementById('ocrSlipList').innerHTML = '';\u001b[39m\n\u001b[31m  const sum = document.getElementById('ocrSummary');\u001b[39m\n\u001b[31m  if (sum) { sum.style.display = 'none'; sum.innerHTML = ''; }\u001b[39m\n\u001b[31m  document.getElementById('shareTargetInfo').style.display = 'none';\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction applyAllDockets() {\u001b[39m\n\u001b[31m  // Match each OCR result to a consignee by order or by name matching\u001b[39m\n\u001b[31m  const consList = document.getElementById('consList');\u001b[39m\n\u001b[31m  const cards = consList.querySelectorAll('.cons-card');\u001b[39m\n\u001b[31m  slipOCRData.forEach((s, i) => {\u001b[39m\n\u001b[31m    if (!s.docket) return;\u001b[39m\n\u001b[31m    const card = cards[i];\u001b[39m\n\u001b[31m    if (card) {\u001b[39m\n\u001b[31m      const docketInput = card.querySelector('.cons-docket-input');\u001b[39m\n\u001b[31m      if (docketInput) {\u001b[39m\n\u001b[31m        docketInput.value = s.docket;\u001b[39m\n\u001b[31m        showLinkedDocket(card, s.docket);\u001b[39m\n\u001b[31m      }\u001b[39m\n\u001b[31m    }\u001b[39m\n\u001b[31m  });\u001b[39m\n\u001b[31m  showToast(`Applied ${slipOCRData.filter(s=>s.docket).length} dockets ✓`, 'ok');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31m// STORE LIST — with column mapping UI\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31mlet _rawRows = [];      // raw rows from file, kept for re-mapping\u001b[39m\n\u001b[31mlet _rawHeaders = [];   // headers from file·\u001b[39m\n\u001b[31masync function handleStoreFile(input) {\u001b[39m\n\u001b[31m  const file = input.files[0];\u001b[39m\n\u001b[31m  if (!file) return;\u001b[39m\n\u001b[31m  showToast('Reading file...', 'info');·\u001b[39m\n\u001b[31m  const data = await readFileAsDataURL(file);\u001b[39m\n\u001b[31m  const base64 = data.split(',')[1];\u001b[39m\n\u001b[31m  const binary = atob(base64);\u001b[39m\n\u001b[31m  const bytes = new Uint8Array(binary.length);\u001b[39m\n\u001b[31m  for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);·\u001b[39m\n\u001b[31m  let rows = [];\u001b[39m\n\u001b[31m  try {\u001b[39m\n\u001b[31m    if (file.name.endsWith('.csv')) {\u001b[39m\n\u001b[31m      const text = new TextDecoder().decode(bytes);\u001b[39m\n\u001b[31m      rows = text.split('\\\\n').map(r => r.split(',').map(c => c.trim().replace(/^\\\"|\\\"$/g, '')));\u001b[39m\n\u001b[31m    } else {\u001b[39m\n\u001b[31m      const wb = XLSX.read(bytes, { type: 'array' });\u001b[39m\n\u001b[31m      const ws = wb.Sheets[wb.SheetNames[0]];\u001b[39m\n\u001b[31m      rows = XLSX.utils.sheet_to_json(ws, { header: 1 });\u001b[39m\n\u001b[31m    }\u001b[39m\n\u001b[31m  } catch(e) { showToast('File read error: ' + e.message, 'err'); return; }·\u001b[39m\n\u001b[31m  if (!rows.length || !rows[0]) { showToast('File appears empty', 'err'); return; }·\u001b[39m\n\u001b[31m  _rawHeaders = (rows[0] || []).map(h => (h || '').toString().trim());\u001b[39m\n\u001b[31m  _rawRows = rows.slice(1).filter(r => r && r.some(c => c !== '' && c != null));·\u001b[39m\n\u001b[31m  document.getElementById('storeTableArea').style.display = 'block';\u001b[39m\n\u001b[31m  renderMappingUI(file.name);\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// ── COLUMN MAPPING FIELDS ─────────────────────────────\u001b[39m\n\u001b[31mconst MAPPING_FIELDS = [\u001b[39m\n\u001b[31m  { key: 'name',    label: 'Store / Partner Name', required: true,\u001b[39m\n\u001b[31m    keywords: ['store','shop','name','outlet','consignee','destination','partner','dealer','client','retailer'] },\u001b[39m\n\u001b[31m  { key: 'city',    label: 'City / Location',       required: false,\u001b[39m\n\u001b[31m    keywords: ['city','town','district','location','area','region','place'] },\u001b[39m\n\u001b[31m  { key: 'phone',   label: 'Phone / Mobile',        required: false,\u001b[39m\n\u001b[31m    keywords: ['phone','mobile','contact','tel','number','mob'] },\u001b[39m\n\u001b[31m  { key: 'docket',  label: 'Docket / AWB No.',      required: false,\u001b[39m\n\u001b[31m    keywords: ['docket','awb','tracking','courier','dkt','consignment','cno','waybill'] },\u001b[39m\n\u001b[31m  { key: 'address', label: 'Address',               required: false,\u001b[39m\n\u001b[31m    keywords: ['address','addr','street','locality','area'] },\u001b[39m\n\u001b[31m  { key: 'pincode', label: 'Pincode / PIN',          required: false,\u001b[39m\n\u001b[31m    keywords: ['pin','pincode','postal','zip','code'] },\u001b[39m\n\u001b[31m];·\u001b[39m\n\u001b[31m// Saved mapping key based on column fingerprint (so same client file auto-maps)\u001b[39m\n\u001b[31mfunction getMappingKey() {\u001b[39m\n\u001b[31m  return 'colmap_' + _rawHeaders.join('|').toLowerCase().replace(/\\\\s+/g,'');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// ── Detect if a column is a combined/multiline address cell ─────────────\u001b[39m\n\u001b[31mfunction isCombinedColumn(colIdx) {\u001b[39m\n\u001b[31m  if (colIdx < 0) return false;\u001b[39m\n\u001b[31m  // Check first 3 data rows — if any cell contains newline or has 6+ digit number (pincode) = combined\u001b[39m\n\u001b[31m  const samples = _rawRows.slice(0, 3).map(r => (r[colIdx] || '').toString());\u001b[39m\n\u001b[31m  return samples.some(s => s.includes('\\\\n') || /\\\\b\\\\d{6}\\\\b/.test(s) || s.split(',').length >= 3);\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction renderMappingUI(filename) {\u001b[39m\n\u001b[31m  const saved = (() => { try { return JSON.parse(localStorage.getItem(getMappingKey()) || 'null'); } catch(e) { return null; } })();\u001b[39m\n\u001b[31m  const isSaved = !!saved;\u001b[39m\n\u001b[31m  const colInfoEl = document.getElementById('storeColInfo');\u001b[39m\n\u001b[31m  colInfoEl.style.display = 'block';·\u001b[39m\n\u001b[31m  const suggested = {};\u001b[39m\n\u001b[31m  MAPPING_FIELDS.forEach(f => {\u001b[39m\n\u001b[31m    const idx = findCol(_rawHeaders, f.keywords);\u001b[39m\n\u001b[31m    suggested[f.key] = saved ? (saved[f.key] ?? idx) : idx;\u001b[39m\n\u001b[31m  });·\u001b[39m\n\u001b[31m  // Detect if address column is combined\u001b[39m\n\u001b[31m  const addrIdx = suggested.address >= 0 ? suggested.address : findCol(_rawHeaders, ['address','addr']);\u001b[39m\n\u001b[31m  const looksCombo = isCombinedColumn(addrIdx) || isCombinedColumn(suggested.name);\u001b[39m\n\u001b[31m  const comboCol   = looksCombo ? (addrIdx >= 0 ? addrIdx : suggested.name) : -1;\u001b[39m\n\u001b[31m  const comboPreview = comboCol >= 0 && _rawRows[0] ? (_rawRows[0][comboCol] || '').toString().slice(0, 120) : '';·\u001b[39m\n\u001b[31m  colInfoEl.innerHTML = `\u001b[39m\n\u001b[31m    <div style=\\\"background:var(--sur2);border:1px solid var(--bdr);border-radius:var(--r-sm);padding:12px;\\\">\u001b[39m\n\u001b[31m      <div style=\\\"display:flex;align-items:center;justify-content:space-between;margin-bottom:10px;\\\">\u001b[39m\n\u001b[31m        <div style=\\\"font-family:var(--fnt-mono);font-size:.68rem;color:var(--sky);\\\">\u001b[39m\n\u001b[31m          📋 ${filename} &nbsp;·&nbsp; ${_rawRows.length} rows &nbsp;·&nbsp; ${_rawHeaders.length} columns\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m        ${isSaved ? '<span class=\\\"map-saved-badge\\\">✓ mapping remembered</span>' : ''}\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      <div style=\\\"font-size:.68rem;color:var(--t3);font-family:var(--fnt-mono);margin-bottom:10px;word-break:break-all;\\\">\u001b[39m\n\u001b[31m        Columns: <span style=\\\"color:var(--t2);\\\">${_rawHeaders.map((h,i)=>`[${i+1}] ${h||'?'}`).join(' · ')}</span>\u001b[39m\n\u001b[31m      </div>·\u001b[39m\n\u001b[31m      ${looksCombo ? `\u001b[39m\n\u001b[31m      <!-- COMBINED COLUMN DETECTED -->\u001b[39m\n\u001b[31m      <div style=\\\"background:rgba(56,189,248,.08);border:1px solid rgba(56,189,248,.3);\u001b[39m\n\u001b[31m        border-radius:var(--r-sm);padding:10px 12px;margin-bottom:10px;\\\">\u001b[39m\n\u001b[31m        <div style=\\\"font-size:.72rem;color:var(--sky);font-weight:600;margin-bottom:6px;\\\">\u001b[39m\n\u001b[31m          🤖 Combined Address Column Detected\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m        <div style=\\\"font-size:.68rem;color:var(--t2);font-family:var(--fnt-mono);\u001b[39m\n\u001b[31m          white-space:pre-wrap;background:var(--bg);padding:6px 8px;border-radius:4px;\u001b[39m\n\u001b[31m          margin-bottom:8px;max-height:80px;overflow:hidden;\\\">${comboPreview}</div>\u001b[39m\n\u001b[31m        <div style=\\\"font-size:.7rem;color:var(--t3);margin-bottom:8px;\\\">\u001b[39m\n\u001b[31m          AI will extract: Name · Address · City · Pincode · Phone from each cell.\u001b[39m\n\u001b[31m          Runs in browser (Transformers.js) with Ollama fallback.\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m        <div style=\\\"display:flex;align-items:center;gap:8px;\\\">\u001b[39m\n\u001b[31m          <label style=\\\"display:flex;align-items:center;gap:6px;font-size:.72rem;color:var(--t1);cursor:pointer;\\\">\u001b[39m\n\u001b[31m            <input type=\\\"checkbox\\\" id=\\\"useAIParse\\\" checked style=\\\"cursor:pointer;\\\">\u001b[39m\n\u001b[31m            Use AI to parse combined column\u001b[39m\n\u001b[31m          </label>\u001b[39m\n\u001b[31m          <select id=\\\"comboColSel\\\" style=\\\"background:var(--sur2);border:1px solid var(--bdr);\u001b[39m\n\u001b[31m            color:var(--t1);font-family:var(--fnt-mono);font-size:.72rem;padding:4px 8px;\u001b[39m\n\u001b[31m            border-radius:var(--r-sm);outline:none;\\\">\u001b[39m\n\u001b[31m            ${_rawHeaders.map((h,i)=>`<option value=\\\"${i}\\\" ${i===comboCol?'selected':''}>${h||'(col '+(i+1)+')'}</option>`).join('')}\u001b[39m\n\u001b[31m          </select>\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      ` : ''}·\u001b[39m\n\u001b[31m      <div id=\\\"manualMapSection\\\" style=\\\"${looksCombo ? 'display:none;' : ''}\\\">\u001b[39m\n\u001b[31m        <div style=\\\"font-size:.68rem;color:var(--t3);margin-bottom:6px;\\\">\u001b[39m\n\u001b[31m          ${looksCombo ? 'Manual mapping (used when AI parse is off, or for other columns):' : 'Map each field to your column:'}\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m        <table class=\\\"map-table\\\">\u001b[39m\n\u001b[31m          <thead><tr><th>Field</th><th>Your column</th><th>Preview (row 1)</th></tr></thead>\u001b[39m\n\u001b[31m          <tbody>\u001b[39m\n\u001b[31m            ${MAPPING_FIELDS.map(f => {\u001b[39m\n\u001b[31m              const selIdx = suggested[f.key] ?? -1;\u001b[39m\n\u001b[31m              const preview = selIdx >= 0 && _rawRows[0] ? ((_rawRows[0][selIdx]||'').toString().slice(0,40)) : '—';\u001b[39m\n\u001b[31m              return `<tr>\u001b[39m\n\u001b[31m                <td style=\\\"white-space:nowrap;color:var(--t1);font-weight:500;font-size:.75rem;\\\">\u001b[39m\n\u001b[31m                  ${f.label}${f.required ? '<span class=\\\"map-req\\\">*</span>' : ''}\u001b[39m\n\u001b[31m                </td>\u001b[39m\n\u001b[31m                <td>\u001b[39m\n\u001b[31m                  <select id=\\\"mapsel_${f.key}\\\" onchange=\\\"updateMappingPreview('${f.key}')\\\">\u001b[39m\n\u001b[31m                    ${_rawHeaders.map((h,i)=>\u001b[39m\n\u001b[31m                      `<option value=\\\"${i}\\\" ${i===selIdx?'selected':''}>${h||'(col '+(i+1)+')'}</option>`\u001b[39m\n\u001b[31m                    ).join('')}\u001b[39m\n\u001b[31m                    <option value=\\\"-1\\\" ${selIdx===-1?'selected':''}>— not in file —</option>\u001b[39m\n\u001b[31m                  </select>\u001b[39m\n\u001b[31m                </td>\u001b[39m\n\u001b[31m                <td id=\\\"mapprev_${f.key}\\\" style=\\\"font-family:var(--fnt-mono);font-size:.68rem;\u001b[39m\n\u001b[31m                  color:var(--t2);max-width:90px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;\\\">\u001b[39m\n\u001b[31m                  ${preview}\u001b[39m\n\u001b[31m                </td>\u001b[39m\n\u001b[31m              </tr>`;\u001b[39m\n\u001b[31m            }).join('')}\u001b[39m\n\u001b[31m          </tbody>\u001b[39m\n\u001b[31m        </table>\u001b[39m\n\u001b[31m      </div>·\u001b[39m\n\u001b[31m      <!-- Also map docket/courier no column separately — always shown -->\u001b[39m\n\u001b[31m      <div style=\\\"margin-top:10px;padding-top:10px;border-top:1px solid var(--bdr);\\\">\u001b[39m\n\u001b[31m        <div style=\\\"font-size:.68rem;color:var(--t3);margin-bottom:6px;font-family:var(--fnt-mono);\\\">\u001b[39m\n\u001b[31m          DOCKET / COURIER NO. COLUMN (required for tracking):\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m        <div style=\\\"display:flex;gap:8px;align-items:center;\\\">\u001b[39m\n\u001b[31m          <select id=\\\"docketColSel\\\" style=\\\"background:var(--sur2);border:1px solid var(--bdr);\u001b[39m\n\u001b[31m            color:var(--acc);font-family:var(--fnt-mono);font-size:.78rem;padding:6px 10px;\u001b[39m\n\u001b[31m            border-radius:var(--r-sm);outline:none;flex:1;\\\">\u001b[39m\n\u001b[31m            <option value=\\\"-1\\\">— Docket not in file —</option>\u001b[39m\n\u001b[31m            ${_rawHeaders.map((h,i)=>`<option value=\\\"${i}\\\" ${i===(suggested.docket??-1)?'selected':''}>${h||'(col '+(i+1)+')'}</option>`).join('')}\u001b[39m\n\u001b[31m          </select>\u001b[39m\n\u001b[31m          <span id=\\\"docketPrev\\\" style=\\\"font-family:var(--fnt-mono);font-size:.72rem;color:var(--acc);\\\">\u001b[39m\n\u001b[31m            ${suggested.docket >= 0 && _rawRows[0] ? (_rawRows[0][suggested.docket]||'') : ''}\u001b[39m\n\u001b[31m          </span>\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m      </div>·\u001b[39m\n\u001b[31m      <div class=\\\"btn-row\\\" style=\\\"margin-top:12px;\\\">\u001b[39m\n\u001b[31m        <button class=\\\"btn btn-ghost\\\" style=\\\"font-size:.78rem;\\\" onclick=\\\"clearStores()\\\">✕ Cancel</button>\u001b[39m\n\u001b[31m        <button class=\\\"btn btn-orange\\\" style=\\\"font-size:.78rem;\\\" onclick=\\\"applyMapping()\\\">✓ Apply Mapping</button>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m  `;\u001b[39m\n\u001b[31m  document.getElementById('storeTable').innerHTML = '';\u001b[39m\n\u001b[31m  document.getElementById('storeAddPanel').style.display = 'none';·\u001b[39m\n\u001b[31m  // Update docket preview on change\u001b[39m\n\u001b[31m  const docketSel = document.getElementById('docketColSel');\u001b[39m\n\u001b[31m  if (docketSel) docketSel.addEventListener('change', () => {\u001b[39m\n\u001b[31m    const idx = parseInt(docketSel.value);\u001b[39m\n\u001b[31m    const prev = document.getElementById('docketPrev');\u001b[39m\n\u001b[31m    if (prev) prev.textContent = idx >= 0 && _rawRows[0] ? (_rawRows[0][idx]||'') : '';\u001b[39m\n\u001b[31m  });·\u001b[39m\n\u001b[31m  // Toggle manual map opacity when AI checkbox changes\u001b[39m\n\u001b[31m  const aiChk = document.getElementById('useAIParse');\u001b[39m\n\u001b[31m  const manSec = document.getElementById('manualMapSection');\u001b[39m\n\u001b[31m  if (aiChk && manSec) {\u001b[39m\n\u001b[31m    aiChk.addEventListener('change', () => {\u001b[39m\n\u001b[31m      manSec.style.display = aiChk.checked ? 'none' : 'block';\u001b[39m\n\u001b[31m    });\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction updateMappingPreview(key) {\u001b[39m\n\u001b[31m  const sel = document.getElementById('mapsel_' + key);\u001b[39m\n\u001b[31m  if (!sel) return;\u001b[39m\n\u001b[31m  const idx = parseInt(sel.value);\u001b[39m\n\u001b[31m  const prev = document.getElementById('mapprev_' + key);\u001b[39m\n\u001b[31m  if (prev) prev.textContent = (idx >= 0 && _rawRows[0]) ? ((_rawRows[0][idx]||'').toString().slice(0,40)) : '—';\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// ── REGEX-FIRST ADDRESS PARSER ───────────────────────────────────────────\u001b[39m\n\u001b[31m// Improved Indian address parser\u001b[39m\n\u001b[31m// Handles formats seen in real BTL dispatch files\u001b[39m\n\u001b[31mfunction parseAddressRegex(raw) {\u001b[39m\n\u001b[31m  const text = raw.toString().replace(/\\\\r/g, '').trim();\u001b[39m\n\u001b[31m  const lines = text.split('\\\\n').map(l => l.trim()).filter(Boolean);\u001b[39m\n\u001b[31m  const result = { name:'', address:'', city:'', pincode:'', phone:'', confidence:0 };·\u001b[39m\n\u001b[31m  // ── PHONE: Indian mobile 6-9 prefix, 10 digits ──────────────────────────\u001b[39m\n\u001b[31m  // Also handles \\\"Phone : 9876543210\\\", \\\"M.No.-9876543210\\\", \\\"Mobile - 9876543210\\\"\u001b[39m\n\u001b[31m  const phoneMatch = text.match(/(?:Phone|Mobile|Mob|M\\\\.No\\\\.?|Ph\\\\.?)?\\\\s*[:\\\\-]?\\\\s*\\\\b([6-9]\\\\d{9})\\\\b/i);\u001b[39m\n\u001b[31m  if (phoneMatch) result.phone = phoneMatch[1];·\u001b[39m\n\u001b[31m  // ── PINCODE: 6-digit standalone number ──────────────────────────────────\u001b[39m\n\u001b[31m  // Must not be part of a longer number (e.g. not an account number)\u001b[39m\n\u001b[31m  const pinMatch = text.match(/(?<![\\\\d])([1-9]\\\\d{5})(?![\\\\d])/);\u001b[39m\n\u001b[31m  if (pinMatch) result.pincode = pinMatch[1];·\u001b[39m\n\u001b[31m  // ── CITY: the token immediately before the pincode ───────────────────────\u001b[39m\n\u001b[31m  const indianStates = ['rajasthan','gujarat','maharashtra','punjab','haryana',\u001b[39m\n\u001b[31m    'uttar pradesh','madhya pradesh','karnataka','tamil nadu','andhra pradesh',\u001b[39m\n\u001b[31m    'telangana','kerala','west bengal','bihar','odisha','jharkhand','assam',\u001b[39m\n\u001b[31m    'himachal pradesh','uttarakhand','chhattisgarh','goa','delhi','chandigarh',\u001b[39m\n\u001b[31m    'raj.','raj'];\u001b[39m\n\u001b[31m  if (result.pincode) {\u001b[39m\n\u001b[31m    const pinPos = text.indexOf(result.pincode);\u001b[39m\n\u001b[31m    const beforePin = text.slice(0, pinPos);\u001b[39m\n\u001b[31m    // Strategy 1: split by comma/newline, walk backwards skipping state names\u001b[39m\n\u001b[31m    const chunks = beforePin.split(/[,\\\\n]/).map(c => c.trim()).filter(Boolean);\u001b[39m\n\u001b[31m    for (let i = chunks.length - 1; i >= 0; i--) {\u001b[39m\n\u001b[31m      const chunk = chunks[i].replace(/\\\\d+/g,'').replace(/[()]/g,'').replace(/[.-]/g,'').trim();\u001b[39m\n\u001b[31m      const lower = chunk.toLowerCase();\u001b[39m\n\u001b[31m      if (!chunk || chunk.length < 2) continue;\u001b[39m\n\u001b[31m      if (indianStates.includes(lower)) continue;\u001b[39m\n\u001b[31m      // Skip chunks that look like addresses (contain numbers or long strings)\u001b[39m\n\u001b[31m      if (/\\\\d/.test(chunk)) continue;\u001b[39m\n\u001b[31m      result.city = chunk;\u001b[39m\n\u001b[31m      break;\u001b[39m\n\u001b[31m    }\u001b[39m\n\u001b[31m    // Strategy 2: if still no city, look for last capitalised word before pincode\u001b[39m\n\u001b[31m    // handles \\\"compound paota Jodhpur 342006\\\" → Jodhpur\u001b[39m\n\u001b[31m    if (!result.city) {\u001b[39m\n\u001b[31m      const wordsBefore = beforePin.replace(/[,\\\\n]/g,' ').trim().split(/\\\\s+/);\u001b[39m\n\u001b[31m      for (let i = wordsBefore.length - 1; i >= 0; i--) {\u001b[39m\n\u001b[31m        const w = wordsBefore[i].replace(/[^a-zA-Z]/g,'');\u001b[39m\n\u001b[31m        if (w.length < 3) continue;\u001b[39m\n\u001b[31m        if (indianStates.includes(w.toLowerCase())) continue;\u001b[39m\n\u001b[31m        // Prefer words that start with capital letter (likely a proper noun/city)\u001b[39m\n\u001b[31m        if (/^[A-Z]/.test(w)) { result.city = w; break; }\u001b[39m\n\u001b[31m      }\u001b[39m\n\u001b[31m    }\u001b[39m\n\u001b[31m  }·\u001b[39m\n\u001b[31m  // ── NAME: first meaningful line, extract shop name ───────────────────────\u001b[39m\n\u001b[31m  // Patterns:\u001b[39m\n\u001b[31m  //   \\\"Sanwar Mal Saini, C/o R G COMPUTERS\\\" → name = \\\"R G COMPUTERS\\\"\u001b[39m\n\u001b[31m  //   \\\"SANWAR MAL SAINI, C/o ADVANCE TECHNOLOGIES\\\" → name = \\\"ADVANCE TECHNOLOGIES\\\"\u001b[39m\n\u001b[31m  //   \\\"Sunil Vaishnav\\\" → name = \\\"Sunil Vaishnav\\\" (no C/o)\u001b[39m\n\u001b[31m  //   \\\"Baltej Singh, Professional computer care\\\" → name = \\\"Professional computer care\\\"\u001b[39m\n\u001b[31m  if (lines.length > 0) {\u001b[39m\n\u001b[31m    const firstLine = lines[0];\u001b[39m\n\u001b[31m    // Check for C/o or C/O pattern — shop name comes after it\u001b[39m\n\u001b[31m    const coMatch = firstLine.match(/C\\\\/?[Oo]\\\\.?\\\\s+(.+)/);\u001b[39m\n\u001b[31m    if (coMatch) {\u001b[39m\n\u001b[31m      result.name = coMatch[1].trim();\u001b[39m\n\u001b[31m    } else {\u001b[39m\n\u001b[31m      // Check if there is a comma — second part might be the shop name\u001b[39m\n\u001b[31m      const parts = firstLine.split(',').map(p => p.trim());\u001b[39m\n\u001b[31m      if (parts.length >= 2 && parts[1].length > 3) {\u001b[39m\n\u001b[31m        // Heuristic: if second part looks like a business name (has uppercase or >5 chars)\u001b[39m\n\u001b[31m        const hasUpperOrLong = /[A-Z]{2,}/.test(parts[1]) || parts[1].length > 8;\u001b[39m\n\u001b[31m        result.name = hasUpperOrLong ? parts[1] : parts[0];\u001b[39m\n\u001b[31m      } else {\u001b[39m\n\u001b[31m        result.name = parts[0];\u001b[39m\n\u001b[31m      }\u001b[39m\n\u001b[31m    }\u001b[39m\n\u001b[31m  }·\u001b[39m\n\u001b[31m  // ── ADDRESS: lines between name and phone/pincode line ───────────────────\u001b[39m\n\u001b[31m  const phoneLineIdx = lines.findIndex(l => result.phone && l.includes(result.phone));\u001b[39m\n\u001b[31m  const addrLines = lines.filter((l, i) => {\u001b[39m\n\u001b[31m    if (i === 0) return false;\u001b[39m\n\u001b[31m    if (i === phoneLineIdx) return false;\u001b[39m\n\u001b[31m    // Exclude lines that are just the phone number\u001b[39m\n\u001b[31m    if (/^[6-9]\\\\d{9}$/.test(l.replace(/\\\\s/g,''))) return false;\u001b[39m\n\u001b[31m    return true;\u001b[39m\n\u001b[31m  });\u001b[39m\n\u001b[31m  result.address = addrLines.join(', ').replace(/,\\\\s*,/g, ',').trim();·\u001b[39m\n\u001b[31m  // ── CONFIDENCE: simple score based on what we got ────────────────────────\u001b[39m\n\u001b[31m  let conf = 0;\u001b[39m\n\u001b[31m  if (result.name)    conf += 30;\u001b[39m\n\u001b[31m  if (result.city)    conf += 25;\u001b[39m\n\u001b[31m  if (result.pincode) conf += 25;\u001b[39m\n\u001b[31m  if (result.phone)   conf += 20;\u001b[39m\n\u001b[31m  result.confidence = conf;·\u001b[39m\n\u001b[31m  return result;\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// ── AI ADDRESS PARSER (Ollama fallback) ──────────────────────────────────\u001b[39m\n\u001b[31m// Fetches past corrections from localStorage to improve prompt accuracy\u001b[39m\n\u001b[31mfunction getPastCorrections() {\u001b[39m\n\u001b[31m  try {\u001b[39m\n\u001b[31m    const log = JSON.parse(localStorage.getItem('360lm_parse_corrections') || '[]');\u001b[39m\n\u001b[31m    return log.slice(-10); // last 10 corrections as examples\u001b[39m\n\u001b[31m  } catch(e) { return []; }\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction saveCorrectionLocal(entry) {\u001b[39m\n\u001b[31m  try {\u001b[39m\n\u001b[31m    const log = JSON.parse(localStorage.getItem('360lm_parse_corrections') || '[]');\u001b[39m\n\u001b[31m    log.push(entry);\u001b[39m\n\u001b[31m    // Keep only last 50\u001b[39m\n\u001b[31m    localStorage.setItem('360lm_parse_corrections', JSON.stringify(log.slice(-50)));\u001b[39m\n\u001b[31m  } catch(e) {}\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31masync function parseAddressWithOllama(raw) {\u001b[39m\n\u001b[31m  // Calls the server-side /dispatch-ai/address proxy (avoids CORS + port 11434 issues)\u001b[39m\n\u001b[31m  try {\u001b[39m\n\u001b[31m    const resp = await fetch('/dispatch-ai/address', {\u001b[39m\n\u001b[31m      method: 'POST',\u001b[39m\n\u001b[31m      headers: { 'Content-Type': 'application/json' },\u001b[39m\n\u001b[31m      body: JSON.stringify({ text: raw }),\u001b[39m\n\u001b[31m      signal: AbortSignal.timeout(120000)\u001b[39m\n\u001b[31m    });\u001b[39m\n\u001b[31m    const data = await resp.json();\u001b[39m\n\u001b[31m    if (data.success) return data;\u001b[39m\n\u001b[31m  } catch(e) {}\u001b[39m\n\u001b[31m  return null;\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31masync function parseAddressCell(raw) {\u001b[39m\n\u001b[31m  // Step 1: regex — always runs first, no network call\u001b[39m\n\u001b[31m  const regex = parseAddressRegex(raw);\u001b[39m\n\u001b[31m  if (regex.name) return { ...regex, source: 'regex' };·\u001b[39m\n\u001b[31m  // Step 2: Ollama only if regex got nothing AND Ollama is configured\u001b[39m\n\u001b[31m  // Uses a 3-second timeout to prevent browser hang\u001b[39m\n\u001b[31m  if (window.appConfig.ocr_mode !== 'transformers') {\u001b[39m\n\u001b[31m    try {\u001b[39m\n\u001b[31m      const ai = await Promise.race([\u001b[39m\n\u001b[31m        parseAddressWithOllama(raw),\u001b[39m\n\u001b[31m        new Promise((_, reject) => setTimeout(() => reject(new Error('timeout')), 120000))\u001b[39m\n\u001b[31m      ]);\u001b[39m\n\u001b[31m      if (ai && ai.name) return { ...ai, source: 'ollama' };\u001b[39m\n\u001b[31m    } catch(e) { /* timeout or error — continue to fallback */ }\u001b[39m\n\u001b[31m  }·\u001b[39m\n\u001b[31m  // Step 3: fallback — first line as name\u001b[39m\n\u001b[31m  const lines = raw.toString().split('\\\\n').map(l=>l.trim()).filter(Boolean);\u001b[39m\n\u001b[31m  return { name: lines[0]||raw.slice(0,40), address: raw, city:'', pincode:'', phone:'', source:'fallback' };\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31masync function applyMapping() {\u001b[39m\n\u001b[31m  const useAI   = document.getElementById('useAIParse')?.checked ?? false;\u001b[39m\n\u001b[31m  const comboSel = document.getElementById('comboColSel');\u001b[39m\n\u001b[31m  const comboIdx = comboSel ? parseInt(comboSel.value) : -1;\u001b[39m\n\u001b[31m  const docketIdx = parseInt(document.getElementById('docketColSel')?.value ?? -1);·\u001b[39m\n\u001b[31m  // Build manual mapping as fallback\u001b[39m\n\u001b[31m  const mapping = {};\u001b[39m\n\u001b[31m  MAPPING_FIELDS.forEach(f => {\u001b[39m\n\u001b[31m    const sel = document.getElementById('mapsel_' + f.key);\u001b[39m\n\u001b[31m    mapping[f.key] = sel ? parseInt(sel.value) : -1;\u001b[39m\n\u001b[31m  });\u001b[39m\n\u001b[31m  // Override docket from dedicated selector\u001b[39m\n\u001b[31m  mapping.docket = docketIdx;·\u001b[39m\n\u001b[31m  // Determine if we have a valid base for stores\u001b[39m\n\u001b[31m  const nameIdx = useAI && comboIdx >= 0 ? comboIdx : mapping.name;\u001b[39m\n\u001b[31m  if (nameIdx === -1 && !useAI) { showToast('Store Name column is required', 'err'); return; }·\u001b[39m\n\u001b[31m  // Save mapping\u001b[39m\n\u001b[31m  try { localStorage.setItem(getMappingKey(), JSON.stringify({...mapping, _useAI: useAI, _comboIdx: comboIdx})); } catch(e) {}·\u001b[39m\n\u001b[31m  if (useAI && comboIdx >= 0) {\u001b[39m\n\u001b[31m    // AI path — parse each combined cell\u001b[39m\n\u001b[31m    showToast('AI parsing addresses...', 'info');\u001b[39m\n\u001b[31m    showOCRWorking(true, `Parsing 0/${_rawRows.length} addresses...`);\u001b[39m\n\u001b[31m    stores = [];\u001b[39m\n\u001b[31m    for (let i = 0; i < _rawRows.length; i++) {\u001b[39m\n\u001b[31m      const r = _rawRows[i];\u001b[39m\n\u001b[31m      const raw = (r[comboIdx] || '').toString().trim();\u001b[39m\n\u001b[31m      if (!raw) continue;\u001b[39m\n\u001b[31m      showOCRWorking(true, `Parsing ${i+1}/${_rawRows.length} — ${raw.slice(0,30)}...`);\u001b[39m\n\u001b[31m      // Yield to browser every row to prevent UI freeze\u001b[39m\n\u001b[31m      await new Promise(r => setTimeout(r, 0));\u001b[39m\n\u001b[31m      const parsed = await parseAddressCell(raw);\u001b[39m\n\u001b[31m      stores.push({\u001b[39m\n\u001b[31m        id: i,\u001b[39m\n\u001b[31m        name:    parsed.name    || raw.slice(0,40),\u001b[39m\n\u001b[31m        city:    parsed.city    || '',\u001b[39m\n\u001b[31m        phone:   parsed.phone   || '',\u001b[39m\n\u001b[31m        address: parsed.address || raw,\u001b[39m\n\u001b[31m        pincode: parsed.pincode || '',\u001b[39m\n\u001b[31m        docket:  docketIdx >= 0 ? (r[docketIdx]||'').toString().trim() : '',\u001b[39m\n\u001b[31m        _parseSource: parsed.source,\u001b[39m\n\u001b[31m        _confidence:  parsed.confidence || 0,\u001b[39m\n\u001b[31m        _rawCell:     raw,\u001b[39m\n\u001b[31m        _origName:    parsed.name || '',\u001b[39m\n\u001b[31m        _origCity:    parsed.city || '',\u001b[39m\n\u001b[31m        matched: false\u001b[39m\n\u001b[31m      });\u001b[39m\n\u001b[31m    }\u001b[39m\n\u001b[31m    showOCRWorking(false);\u001b[39m\n\u001b[31m  } else {\u001b[39m\n\u001b[31m    // Manual mapping path\u001b[39m\n\u001b[31m    stores = _rawRows\u001b[39m\n\u001b[31m      .filter(r => r[nameIdx] && (r[nameIdx]+'').trim())\u001b[39m\n\u001b[31m      .map((r, i) => ({\u001b[39m\n\u001b[31m        id: i,\u001b[39m\n\u001b[31m        name:    mapping.name    >= 0 ? (r[mapping.name]   ||'').toString().trim() : '',\u001b[39m\n\u001b[31m        city:    mapping.city    >= 0 ? (r[mapping.city]   ||'').toString().trim() : '',\u001b[39m\n\u001b[31m        phone:   mapping.phone   >= 0 ? (r[mapping.phone]  ||'').toString().trim() : '',\u001b[39m\n\u001b[31m        docket:  mapping.docket  >= 0 ? (r[mapping.docket] ||'').toString().trim() : '',\u001b[39m\n\u001b[31m        address: mapping.address >= 0 ? (r[mapping.address]||'').toString().trim() : '',\u001b[39m\n\u001b[31m        pincode: mapping.pincode >= 0 ? (r[mapping.pincode]||'').toString().trim() : '',\u001b[39m\n\u001b[31m        matched: false\u001b[39m\n\u001b[31m      }));\u001b[39m\n\u001b[31m  }·\u001b[39m\n\u001b[31m  document.getElementById('storeCount').textContent = `${stores.length} stores loaded`;·\u001b[39m\n\u001b[31m  if (useAI && comboIdx >= 0) {\u001b[39m\n\u001b[31m    // AI path — show row-by-row review panel before adding consignees\u001b[39m\n\u001b[31m    showAddressReviewPanel();\u001b[39m\n\u001b[31m  } else {\u001b[39m\n\u001b[31m    // Manual mapping path — go straight to store table + add panel\u001b[39m\n\u001b[31m    renderStoreTable(mapping.docket >= 0 || docketIdx >= 0);\u001b[39m\n\u001b[31m    const addPanel = document.getElementById('storeAddPanel');\u001b[39m\n\u001b[31m    const addBtn   = document.getElementById('storeAddBtn');\u001b[39m\n\u001b[31m    const addTitle = document.getElementById('storeAddTitle');\u001b[39m\n\u001b[31m    const hasDockets = stores.some(s => s.docket);\u001b[39m\n\u001b[31m    addTitle.textContent = hasDockets\u001b[39m\n\u001b[31m      ? `${stores.filter(s=>s.docket).length} stores with dockets — add as consignees?`\u001b[39m\n\u001b[31m      : `Add ${stores.length} stores as consignees?`;\u001b[39m\n\u001b[31m    addBtn.textContent = `✓ Add ${stores.length} as Consignees`;\u001b[39m\n\u001b[31m    addPanel.style.display = 'block';\u001b[39m\n\u001b[31m    showToast(`${stores.length} stores mapped ✓`, 'ok');\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31m// ADDRESS REVIEW PANEL\u001b[39m\n\u001b[31m// Row-by-row review of AI-parsed addresses before consignees created\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31mfunction showAddressReviewPanel() {\u001b[39m\n\u001b[31m  const pane = document.getElementById('addrReviewPane');\u001b[39m\n\u001b[31m  if (!pane) return;\u001b[39m\n\u001b[31m  renderAddressReviewRows();\u001b[39m\n\u001b[31m  pane.style.display = 'block';\u001b[39m\n\u001b[31m  pane.scrollTop = 0;\u001b[39m\n\u001b[31m  showToast(`Review ${stores.length} parsed addresses`, 'info');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction renderAddressReviewRows() {\u001b[39m\n\u001b[31m  const list = document.getElementById('addrReviewList');\u001b[39m\n\u001b[31m  const summary = document.getElementById('addrReviewSummary');\u001b[39m\n\u001b[31m  if (!list) return;·\u001b[39m\n\u001b[31m  const highConf  = stores.filter(s => (s._confidence||0) >= 80).length;\u001b[39m\n\u001b[31m  const medConf   = stores.filter(s => (s._confidence||0) >= 50 && (s._confidence||0) < 80).length;\u001b[39m\n\u001b[31m  const lowConf   = stores.filter(s => (s._confidence||0) < 50).length;·\u001b[39m\n\u001b[31m  summary.innerHTML = `\u001b[39m\n\u001b[31m    <div class=\\\"vs-item\\\"><div class=\\\"vs-val\\\" style=\\\"color:var(--t1);\\\">${stores.length}</div><div class=\\\"vs-lbl\\\">Total</div></div>\u001b[39m\n\u001b[31m    <div class=\\\"vs-item\\\"><div class=\\\"vs-val\\\" style=\\\"color:var(--grn);\\\">${highConf}</div><div class=\\\"vs-lbl\\\">High conf</div></div>\u001b[39m\n\u001b[31m    <div class=\\\"vs-item\\\"><div class=\\\"vs-val\\\" style=\\\"color:var(--ylw);\\\">${medConf}</div><div class=\\\"vs-lbl\\\">Review</div></div>\u001b[39m\n\u001b[31m    <div class=\\\"vs-item\\\"><div class=\\\"vs-val\\\" style=\\\"color:var(--red);\\\">${lowConf}</div><div class=\\\"vs-lbl\\\">Check</div></div>\u001b[39m\n\u001b[31m    <div class=\\\"vs-item\\\" style=\\\"margin-left:auto;\\\">\u001b[39m\n\u001b[31m      <button class=\\\"btn-sm primary\\\" onclick=\\\"approveAllAddresses()\\\" style=\\\"font-size:.7rem;\\\">✓ Approve All</button>\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m  `;·\u001b[39m\n\u001b[31m  list.innerHTML = stores.map((s, i) => {\u001b[39m\n\u001b[31m    const conf = s._confidence || 0;\u001b[39m\n\u001b[31m    const confClass = conf >= 80 ? 'vf-ok' : conf >= 50 ? 'vf-nodocket' : 'vf-dupe';\u001b[39m\n\u001b[31m    const confLabel = conf >= 80 ? `${conf}% ✓` : conf >= 50 ? `${conf}% review` : `${conf}% check`;\u001b[39m\n\u001b[31m    const rowBorder = conf >= 80 ? 'var(--grn)' : conf >= 50 ? 'var(--ylw)' : 'var(--red)';·\u001b[39m\n\u001b[31m    return `\u001b[39m\n\u001b[31m    <div class=\\\"verify-row\\\" id=\\\"arow_${i}\\\" style=\\\"border-left:3px solid ${rowBorder};\\\">\u001b[39m\n\u001b[31m      <div class=\\\"verify-row-hdr\\\">\u001b[39m\n\u001b[31m        <span class=\\\"verify-num\\\">#${i+1}</span>\u001b[39m\n\u001b[31m        <span class=\\\"verify-store\\\" style=\\\"font-size:.78rem;color:var(--t2);font-weight:400;\u001b[39m\n\u001b[31m          font-family:var(--fnt-mono);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;flex:1;\\\"\u001b[39m\n\u001b[31m          title=\\\"${(s._rawCell||'').replace(/\\\"/g,\\\"'\\\")}\\\">\u001b[39m\n\u001b[31m          ${(s._rawCell||'').slice(0,50).split('\\\\n').join(' ↵ ')}       <span class=\\\"verify-flag ${confClass}\\\" style=\\\"flex-shrink:0;margin-right:6px;\\\">${confLabel} · ${s._parseSource||'?'}</span>\u001b[39m\n\u001b[31m        <button onclick=\\\"deleteReviewRow(${i})\\\"\u001b[39m\n\u001b[31m          style=\\\"background:none;border:1px solid rgba(239,68,68,.4);color:var(--red);\u001b[39m\n\u001b[31m          font-size:.65rem;padding:2px 8px;border-radius:6px;cursor:pointer;\u001b[39m\n\u001b[31m          font-family:var(--fnt-mono);flex-shrink:0;transition:background .2s;\\\"\u001b[39m\n\u001b[31m          onmouseover=\\\"this.style.background='rgba(239,68,68,.15)'\\\"\u001b[39m\n\u001b[31m          onmouseout=\\\"this.style.background='none'\\\">✕ Remove</button>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      <div class=\\\"verify-fields\\\" style=\\\"grid-template-columns:1fr 1fr 1fr 1fr 1fr;\\\">\u001b[39m\n\u001b[31m        <div class=\\\"verify-field\\\">\u001b[39m\n\u001b[31m          <label>Shop / Name <span style=\\\"color:var(--acc);\\\">*</span></label>\u001b[39m\n\u001b[31m          <input type=\\\"text\\\" value=\\\"${s.name||''}\\\" id=\\\"ar_name_${i}\\\"\u001b[39m\n\u001b[31m            oninput=\\\"updateAddrField(${i},'name',this)\\\"\u001b[39m\n\u001b[31m            style=\\\"${!s.name ? 'border-color:var(--red);' : ''}\\\">\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m        <div class=\\\"verify-field\\\">\u001b[39m\n\u001b[31m          <label>City</label>\u001b[39m\n\u001b[31m          <input type=\\\"text\\\" value=\\\"${s.city||''}\\\" id=\\\"ar_city_${i}\\\"\u001b[39m\n\u001b[31m            oninput=\\\"updateAddrField(${i},'city',this)\\\"\u001b[39m\n\u001b[31m            style=\\\"${!s.city ? 'border-color:var(--ylw);' : ''}\\\">\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m        <div class=\\\"verify-field\\\">\u001b[39m\n\u001b[31m          <label>Docket / Courier No.</label>\u001b[39m\n\u001b[31m          <input type=\\\"text\\\" value=\\\"${s.docket||''}\\\" id=\\\"ar_docket_${i}\\\"\u001b[39m\n\u001b[31m            oninput=\\\"updateAddrField(${i},'docket',this)\\\"\u001b[39m\n\u001b[31m            style=\\\"font-family:var(--fnt-mono);${!s.docket?'border-color:var(--ylw);':'color:var(--acc);'}\\\">\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m        <div class=\\\"verify-field\\\">\u001b[39m\n\u001b[31m          <label>Phone</label>\u001b[39m\n\u001b[31m          <input type=\\\"tel\\\" value=\\\"${s.phone||''}\\\" id=\\\"ar_phone_${i}\\\"\u001b[39m\n\u001b[31m            oninput=\\\"updateAddrField(${i},'phone',this)\\\">\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m        <div class=\\\"verify-field\\\">\u001b[39m\n\u001b[31m          <label>Pincode</label>\u001b[39m\n\u001b[31m          <input type=\\\"text\\\" value=\\\"${s.pincode||''}\\\" id=\\\"ar_pin_${i}\\\"\u001b[39m\n\u001b[31m            oninput=\\\"updateAddrField(${i},'pincode',this)\\\">\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      <div style=\\\"font-size:.65rem;color:var(--t3);font-family:var(--fnt-mono);\u001b[39m\n\u001b[31m        margin-top:4px;padding:4px 6px;background:var(--bg);border-radius:4px;word-break:break-all;\\\">\u001b[39m\n\u001b[31m        📋 ${(s._rawCell||'').split('\\\\n').join(' | ')}\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m    </div>`;\u001b[39m\n\u001b[31m  }).join('');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction updateAddrField(i, field, input) {\u001b[39m\n\u001b[31m  if (!stores[i]) return;\u001b[39m\n\u001b[31m  stores[i][field] = input.value.trim();\u001b[39m\n\u001b[31m  // Recompute confidence\u001b[39m\n\u001b[31m  let conf = 0;\u001b[39m\n\u001b[31m  if (stores[i].name)    conf += 30;\u001b[39m\n\u001b[31m  if (stores[i].city)    conf += 25;\u001b[39m\n\u001b[31m  if (stores[i].pincode) conf += 25;\u001b[39m\n\u001b[31m  if (stores[i].phone)   conf += 20;\u001b[39m\n\u001b[31m  stores[i]._confidence = conf;\u001b[39m\n\u001b[31m  input.classList.toggle('changed', true);\u001b[39m\n\u001b[31m  // Update border color on row\u001b[39m\n\u001b[31m  const row = document.getElementById('arow_' + i);\u001b[39m\n\u001b[31m  if (row) row.style.borderLeftColor = conf >= 80 ? 'var(--grn)' : conf >= 50 ? 'var(--ylw)' : 'var(--red)';\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction deleteReviewRow(i) {\u001b[39m\n\u001b[31m  // Mark as deleted — splice from stores array and re-render\u001b[39m\n\u001b[31m  stores.splice(i, 1);\u001b[39m\n\u001b[31m  // Re-assign IDs\u001b[39m\n\u001b[31m  stores.forEach((s, idx) => { s.id = idx; });\u001b[39m\n\u001b[31m  renderAddressReviewRows();\u001b[39m\n\u001b[31m  document.getElementById('storeCount').textContent = `${stores.length} stores loaded`;\u001b[39m\n\u001b[31m  showToast(`Row removed — ${stores.length} remaining`, 'info');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction backToMapping() {\u001b[39m\n\u001b[31m  // Hide review pane — store edits are preserved in stores[] array\u001b[39m\n\u001b[31m  document.getElementById('addrReviewPane').style.display = 'none';\u001b[39m\n\u001b[31m  // Show mapping UI again with current file\u001b[39m\n\u001b[31m  document.getElementById('storeTableArea').style.display = 'block';\u001b[39m\n\u001b[31m  document.getElementById('storeColInfo').style.display = 'block';\u001b[39m\n\u001b[31m  showToast('Your edits are preserved — re-apply mapping to reload', 'info');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction approveAllAddresses() {\u001b[39m\n\u001b[31m  stores.forEach((s, i) => { s._reviewed = true; });\u001b[39m\n\u001b[31m  document.getElementById('addrReviewPane').style.display = 'none';\u001b[39m\n\u001b[31m  applyReviewedStores();\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction confirmAddressReview() {\u001b[39m\n\u001b[31m  // Collect any pending edits, log corrections, close panel\u001b[39m\n\u001b[31m  const corrections = [];\u001b[39m\n\u001b[31m  stores.forEach((s, i) => {\u001b[39m\n\u001b[31m    const origName = s._origName || '';\u001b[39m\n\u001b[31m    const origCity = s._origCity || '';\u001b[39m\n\u001b[31m    if ((origName && origName !== s.name) || (origCity && origCity !== s.city)) {\u001b[39m\n\u001b[31m      corrections.push({ raw: s._rawCell||'', name: s.name, city: s.city, pincode: s.pincode });\u001b[39m\n\u001b[31m    }\u001b[39m\n\u001b[31m  });\u001b[39m\n\u001b[31m  // Save corrections locally for future Ollama prompts\u001b[39m\n\u001b[31m  corrections.forEach(c => saveCorrectionLocal(c));\u001b[39m\n\u001b[31m  // Log to Sheets asynchronously (fire and forget)\u001b[39m\n\u001b[31m  if (corrections.length > 0 && window.appConfig.webhook_url) {\u001b[39m\n\u001b[31m    logParseCorrectionsToSheets(corrections);\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m  document.getElementById('addrReviewPane').style.display = 'none';\u001b[39m\n\u001b[31m  applyReviewedStores();\u001b[39m\n\u001b[31m  showToast(`${corrections.length > 0 ? corrections.length + ' corrections logged · ' : ''}${stores.length} stores ready ✓`, 'ok');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// Clean street address: remove city, pincode, phone if they appear in address field\u001b[39m\n\u001b[31mfunction cleanAddress(addr, city, pincode, phone) {\u001b[39m\n\u001b[31m  if (!addr) return addr;\u001b[39m\n\u001b[31m  let s = addr;\u001b[39m\n\u001b[31m  // Remove phone\u001b[39m\n\u001b[31m  if (phone) s = s.replace(new RegExp(phone.replace(/[.*+?^${}()|[\\\\]\\\\\\\\]/g,'\\\\\\\\$&'), 'g'), '').trim();\u001b[39m\n\u001b[31m  // Remove pincode\u001b[39m\n\u001b[31m  if (pincode) s = s.replace(new RegExp('\\\\\\\\b' + pincode + '\\\\\\\\b', 'g'), '').trim();\u001b[39m\n\u001b[31m  // Remove city (case-insensitive)\u001b[39m\n\u001b[31m  if (city) s = s.replace(new RegExp('\\\\\\\\b' + city.replace(/[.*+?^${}()|[\\\\]\\\\\\\\]/g,'\\\\\\\\$&') + '\\\\\\\\b', 'ig'), '').trim();\u001b[39m\n\u001b[31m  // Remove state names and trailing noise\u001b[39m\n\u001b[31m  const noise = /\\\\b(rajasthan|gujarat|maharashtra|punjab|haryana|uttar pradesh|delhi|raj\\\\.)\\\\b/ig;\u001b[39m\n\u001b[31m  s = s.replace(noise, '');\u001b[39m\n\u001b[31m  // Remove \\\"Mobile :\\\", \\\"Phone :\\\", \\\"M.No.\\\" prefixes and dangling numbers\u001b[39m\n\u001b[31m  s = s.replace(/(?:Mobile|Phone|Mob|M\\\\.No\\\\.?|Ph\\\\.?)\\\\s*[:\\\\-]?\\\\s*/ig, '');\u001b[39m\n\u001b[31m  // Clean up multiple commas/spaces\u001b[39m\n\u001b[31m  s = s.replace(/,\\\\s*,/g, ',').replace(/\\\\s{2,}/g, ' ').replace(/^[\\\\s,]+|[\\\\s,]+$/g, '');\u001b[39m\n\u001b[31m  return s;\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction applyReviewedStores() {\u001b[39m\n\u001b[31m  // Clean address fields before loading stores\u001b[39m\n\u001b[31m  stores.forEach(s => {\u001b[39m\n\u001b[31m    s.address = cleanAddress(s.address, s.city, s.pincode, s.phone);\u001b[39m\n\u001b[31m  });\u001b[39m\n\u001b[31m  renderStoreTable(stores.some(s => s.docket));\u001b[39m\n\u001b[31m  const addPanel = document.getElementById('storeAddPanel');\u001b[39m\n\u001b[31m  const addTitle = document.getElementById('storeAddTitle');\u001b[39m\n\u001b[31m  const addBtn   = document.getElementById('storeAddBtn');\u001b[39m\n\u001b[31m  const hasDockets = stores.some(s => s.docket);\u001b[39m\n\u001b[31m  addTitle.textContent = hasDockets\u001b[39m\n\u001b[31m    ? `${stores.filter(s=>s.docket).length} stores with dockets — add as consignees?`\u001b[39m\n\u001b[31m    : `Add ${stores.length} stores as consignees?`;\u001b[39m\n\u001b[31m  addBtn.textContent = `✓ Add ${stores.length} as Consignees`;\u001b[39m\n\u001b[31m  addPanel.style.display = 'block';\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31masync function logParseCorrectionsToSheets(corrections) {\u001b[39m\n\u001b[31m  const url = window.appConfig.webhook_url;\u001b[39m\n\u001b[31m  if (!url) return;\u001b[39m\n\u001b[31m  try {\u001b[39m\n\u001b[31m    await fetch(url, {\u001b[39m\n\u001b[31m      method: 'POST', mode: 'no-cors',\u001b[39m\n\u001b[31m      headers: { 'Content-Type': 'application/json' },\u001b[39m\n\u001b[31m      body: JSON.stringify({\u001b[39m\n\u001b[31m        type: 'parse_correction',\u001b[39m\n\u001b[31m        corrections,\u001b[39m\n\u001b[31m        colFingerprint: getMappingKey(),\u001b[39m\n\u001b[31m        timestamp: new Date().toISOString()\u001b[39m\n\u001b[31m      })\u001b[39m\n\u001b[31m    });\u001b[39m\n\u001b[31m  } catch(e) {}\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction addStoresToConsignees() {\u001b[39m\n\u001b[31m  const consList = document.getElementById('consList');\u001b[39m\n\u001b[31m  consList.innerHTML = '';\u001b[39m\n\u001b[31m  stores.slice(0, 50).forEach(s => addConsignee(s));\u001b[39m\n\u001b[31m  document.getElementById('storeAddPanel').style.display = 'none';\u001b[39m\n\u001b[31m  showToast(`${Math.min(stores.length, 50)} consignees added ✓`, 'ok');\u001b[39m\n\u001b[31m  if (stores.some(s => s.docket)) setTimeout(() => autoCreateDispatchFromStoreList(), 600);\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction promptAddFromStores() { /* replaced by inline panel */ }··\u001b[39m\n\u001b[31mfunction findCol(headers, keywords) {\u001b[39m\n\u001b[31m  for (let i=0;i<headers.length;i++) {\u001b[39m\n\u001b[31m    const h = (headers[i]||'').toString().toLowerCase();\u001b[39m\n\u001b[31m    if (keywords.some(k => h.includes(k))) return i;\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m  return -1;\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction renderStoreTable(hasDocket) {\u001b[39m\n\u001b[31m  const t = document.getElementById('storeTable');\u001b[39m\n\u001b[31m  const cols = ['#','Store Name','City','Phone'];\u001b[39m\n\u001b[31m  if (hasDocket) cols.push('Docket','Status');\u001b[39m\n\u001b[31m  t.innerHTML = `<thead><tr>${cols.map(c=>`<th>${c}</th>`).join('')}</tr></thead>\u001b[39m\n\u001b[31m  <tbody>${stores.map((s,i) => {\u001b[39m\n\u001b[31m    const statusClass = s.docket ? (s.status === 'delivered' ? 'matched' : 'unmatched') : '';\u001b[39m\n\u001b[31m    const docketCell = hasDocket ? `<td class=\\\"${statusClass}\\\" style=\\\"font-family:var(--fnt-mono);\\\">${s.docket||'—'}</td>` : '';\u001b[39m\n\u001b[31m    const statusCell = hasDocket ? `<td>${s.status ? `<span class=\\\"status-pill sp-${s.status}\\\">${s.status}</span>` : '—'}</td>` : '';\u001b[39m\n\u001b[31m    return `<tr>\u001b[39m\n\u001b[31m      <td>${i+1}</td>\u001b[39m\n\u001b[31m      <td>${s.name}</td>\u001b[39m\n\u001b[31m      <td>${s.city}</td>\u001b[39m\n\u001b[31m      <td style=\\\"font-family:var(--fnt-mono);\\\">${s.phone||'—'}</td>\u001b[39m\n\u001b[31m      ${docketCell}${statusCell}\u001b[39m\n\u001b[31m    </tr>`;\u001b[39m\n\u001b[31m  }).join('')}</tbody>`;\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction clearStores() {\u001b[39m\n\u001b[31m  stores = [];\u001b[39m\n\u001b[31m  document.getElementById('storeTableArea').style.display = 'none';\u001b[39m\n\u001b[31m  document.getElementById('storeFile').value = '';\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction autoCreateDispatchFromStoreList() {\u001b[39m\n\u001b[31m  // Create/update dispatch with all stores that have dockets\u001b[39m\n\u001b[31m  const courier = document.getElementById('n_courier').value || 'SkyKing';\u001b[39m\n\u001b[31m  const storesWithDockets = stores.filter(s => s.docket);\u001b[39m\n\u001b[31m  if (!storesWithDockets.length) return;·\u001b[39m\n\u001b[31m  // Clear and rebuild consignee list from store data\u001b[39m\n\u001b[31m  const consList = document.getElementById('consList');\u001b[39m\n\u001b[31m  consList.innerHTML = '';\u001b[39m\n\u001b[31m  storesWithDockets.forEach(s => {\u001b[39m\n\u001b[31m    addConsignee(s);\u001b[39m\n\u001b[31m  });\u001b[39m\n\u001b[31m  showPane('new');\u001b[39m\n\u001b[31m  showToast(`${storesWithDockets.length} consignees added from store list`, 'ok');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction promptAddFromStores() {\u001b[39m\n\u001b[31m  // Replaced by inline storeAddPanel UI — no-op kept for safety\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction autoMatchDockets() {\u001b[39m\n\u001b[31m  // Match OCR dockets to stores by order\u001b[39m\n\u001b[31m  if (!slipOCRData.length) { showToast('Upload docket slip images first', 'err'); return; }\u001b[39m\n\u001b[31m  let matched = 0;\u001b[39m\n\u001b[31m  slipOCRData.forEach((slip, i) => {\u001b[39m\n\u001b[31m    if (i < stores.length && slip.docket) {\u001b[39m\n\u001b[31m      stores[i].docket = slip.docket;\u001b[39m\n\u001b[31m      matched++;\u001b[39m\n\u001b[31m    }\u001b[39m\n\u001b[31m  });\u001b[39m\n\u001b[31m  renderStoreTable(true);\u001b[39m\n\u001b[31m  showToast(`${matched} dockets matched to stores ✓`, 'ok');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31m// CONSIGNEES\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31mfunction addConsignee(storeData = null) {\u001b[39m\n\u001b[31m  const list = document.getElementById('consList');\u001b[39m\n\u001b[31m  const idx = list.children.length;\u001b[39m\n\u001b[31m  const card = document.createElement('div');\u001b[39m\n\u001b[31m  card.className = 'cons-card';\u001b[39m\n\u001b[31m  card.dataset.idx = idx;\u001b[39m\n\u001b[31m  const s = storeData || {};\u001b[39m\n\u001b[31m  card.innerHTML = `\u001b[39m\n\u001b[31m    <div class=\\\"cons-hdr\\\">\u001b[39m\n\u001b[31m      <span class=\\\"cons-num\\\">CONSIGNEE #${idx+1}</span>\u001b[39m\n\u001b[31m      <button class=\\\"cons-del\\\" onclick=\\\"this.closest('.cons-card').remove();renumberCons()\\\">✕</button>\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m    <div class=\\\"row2\\\">\u001b[39m\n\u001b[31m      <div class=\\\"f\\\">\u001b[39m\n\u001b[31m        <label>Store / Name <span class=\\\"req\\\">*</span></label>\u001b[39m\n\u001b[31m        <input type=\\\"text\\\" class=\\\"cons-name\\\" placeholder=\\\"Store name\\\" value=\\\"${s.name||''}\\\">\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      <div class=\\\"f\\\">\u001b[39m\n\u001b[31m        <label>City <span class=\\\"req\\\">*</span></label>\u001b[39m\n\u001b[31m        <input type=\\\"text\\\" class=\\\"cons-city\\\" placeholder=\\\"City\\\" value=\\\"${s.city||''}\\\">\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m    <div class=\\\"f\\\">\u001b[39m\n\u001b[31m      <label>Address</label>\u001b[39m\n\u001b[31m      <input type=\\\"text\\\" class=\\\"cons-addr\\\" placeholder=\\\"Full address\\\" value=\\\"${s.address||''}\\\">\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m    <div class=\\\"row2\\\">\u001b[39m\n\u001b[31m      <div class=\\\"f\\\">\u001b[39m\n\u001b[31m        <label>Phone</label>\u001b[39m\n\u001b[31m        <input type=\\\"tel\\\" class=\\\"cons-phone\\\" placeholder=\\\"Mobile\\\" value=\\\"${s.phone||''}\\\">\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      <div class=\\\"f\\\">\u001b[39m\n\u001b[31m        <label>Pincode</label>\u001b[39m\n\u001b[31m        <input type=\\\"text\\\" class=\\\"cons-pin\\\" placeholder=\\\"6-digit\\\" maxlength=\\\"6\\\">\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m    <div class=\\\"f\\\">\u001b[39m\n\u001b[31m      <label>Material / Contents <span class=\\\"req\\\">*</span></label>\u001b[39m\n\u001b[31m      <input type=\\\"text\\\" class=\\\"cons-material\\\" placeholder=\\\"e.g. Standees, Posters\\\">\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m    <div class=\\\"row2\\\">\u001b[39m\n\u001b[31m      <div class=\\\"f\\\">\u001b[39m\n\u001b[31m        <label>Qty</label>\u001b[39m\n\u001b[31m        <input type=\\\"number\\\" class=\\\"cons-qty\\\" placeholder=\\\"0\\\" min=\\\"0\\\">\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      <div class=\\\"f\\\">\u001b[39m\n\u001b[31m        <label>Docket No.</label>\u001b[39m\n\u001b[31m        <input type=\\\"text\\\" class=\\\"cons-docket-input\\\" placeholder=\\\"Docket #\\\"\u001b[39m\n\u001b[31m          style=\\\"font-family:var(--fnt-mono);\\\" value=\\\"${s.docket||''}\\\"\u001b[39m\n\u001b[31m          oninput=\\\"onDocketInput(this)\\\">\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m    ${s.docket ? `<div class=\\\"cons-docket-linked\\\"><span>🔗</span><span class=\\\"dkt\\\">${s.docket}</span></div>` : ''}\u001b[39m\n\u001b[31m  `;\u001b[39m\n\u001b[31m  list.appendChild(card);\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction onDocketInput(input) {\u001b[39m\n\u001b[31m  const card = input.closest('.cons-card');\u001b[39m\n\u001b[31m  let linked = card.querySelector('.cons-docket-linked');\u001b[39m\n\u001b[31m  if (input.value.trim()) {\u001b[39m\n\u001b[31m    if (!linked) {\u001b[39m\n\u001b[31m      linked = document.createElement('div');\u001b[39m\n\u001b[31m      linked.className = 'cons-docket-linked';\u001b[39m\n\u001b[31m      card.appendChild(linked);\u001b[39m\n\u001b[31m    }\u001b[39m\n\u001b[31m    linked.innerHTML = `<span>🔗</span><span class=\\\"dkt\\\">${input.value}</span>`;\u001b[39m\n\u001b[31m  } else if (linked) {\u001b[39m\n\u001b[31m    linked.remove();\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction showLinkedDocket(card, docket) {\u001b[39m\n\u001b[31m  const input = card.querySelector('.cons-docket-input');\u001b[39m\n\u001b[31m  if (input) { input.value = docket; onDocketInput(input); }\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction renumberCons() {\u001b[39m\n\u001b[31m  document.querySelectorAll('#consList .cons-card').forEach((c,i) => {\u001b[39m\n\u001b[31m    c.querySelector('.cons-num').textContent = `CONSIGNEE #${i+1}`;\u001b[39m\n\u001b[31m    c.dataset.idx = i;\u001b[39m\n\u001b[31m  });\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31m// JOB REFERENCE\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31mfunction onClientChange() {\u001b[39m\n\u001b[31m  const val = document.getElementById('n_client').value;\u001b[39m\n\u001b[31m  document.getElementById('customClientField').style.display = val === '__c' ? 'block' : 'none';\u001b[39m\n\u001b[31m  updatePill();\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31mfunction updatePill() {\u001b[39m\n\u001b[31m  const client = document.getElementById('n_client').value;\u001b[39m\n\u001b[31m  const job = document.getElementById('n_jobref').value.trim();\u001b[39m\n\u001b[31m  const cName = client === '__c' ? document.getElementById('n_customClient').value : client;\u001b[39m\n\u001b[31m  const pill = document.getElementById('jobPill');\u001b[39m\n\u001b[31m  if (cName && job) {\u001b[39m\n\u001b[31m    document.getElementById('jobPillTxt').textContent = `${cName} › ${job}`;\u001b[39m\n\u001b[31m    pill.style.display = 'inline-flex';\u001b[39m\n\u001b[31m  } else {\u001b[39m\n\u001b[31m    pill.style.display = 'none';\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31mfunction clearJob() {\u001b[39m\n\u001b[31m  document.getElementById('n_client').value = '';\u001b[39m\n\u001b[31m  document.getElementById('n_jobref').value = '';\u001b[39m\n\u001b[31m  document.getElementById('jobPill').style.display = 'none';\u001b[39m\n\u001b[31m  document.getElementById('customClientField').style.display = 'none';\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31m// GATHER / SAVE\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31mfunction gatherDispatch() {\u001b[39m\n\u001b[31m  const client = document.getElementById('n_client').value;\u001b[39m\n\u001b[31m  const clientName = client === '__c' ? document.getElementById('n_customClient').value : client;\u001b[39m\n\u001b[31m  const consignees = [];\u001b[39m\n\u001b[31m  document.querySelectorAll('#consList .cons-card').forEach(card => {\u001b[39m\n\u001b[31m    const docket = card.querySelector('.cons-docket-input')?.value.trim() || '';\u001b[39m\n\u001b[31m    consignees.push({\u001b[39m\n\u001b[31m      name: card.querySelector('.cons-name')?.value || '',\u001b[39m\n\u001b[31m      city: card.querySelector('.cons-city')?.value || '',\u001b[39m\n\u001b[31m      address: card.querySelector('.cons-addr')?.value || '',\u001b[39m\n\u001b[31m      phone: card.querySelector('.cons-phone')?.value || '',\u001b[39m\n\u001b[31m      pincode: card.querySelector('.cons-pin')?.value || '',\u001b[39m\n\u001b[31m      material: card.querySelector('.cons-material')?.value || '',\u001b[39m\n\u001b[31m      qty: card.querySelector('.cons-qty')?.value || '',\u001b[39m\n\u001b[31m      docket,\u001b[39m\n\u001b[31m      status: docket ? 'booked' : 'pending',\u001b[39m\n\u001b[31m      lastEvent: '',\u001b[39m\n\u001b[31m      lastUpdated: ''\u001b[39m\n\u001b[31m    });\u001b[39m\n\u001b[31m  });\u001b[39m\n\u001b[31m  return {\u001b[39m\n\u001b[31m    id: 'DSP-' + Date.now(),\u001b[39m\n\u001b[31m    mode: 'manual',\u001b[39m\n\u001b[31m    client: clientName,\u001b[39m\n\u001b[31m    jobref: document.getElementById('n_jobref').value,\u001b[39m\n\u001b[31m    courier: document.getElementById('n_courier').value,\u001b[39m\n\u001b[31m    dispatchDate: document.getElementById('n_date').value,\u001b[39m\n\u001b[31m    remarks: document.getElementById('n_remarks').value,\u001b[39m\n\u001b[31m    consignees,\u001b[39m\n\u001b[31m    slipCount: slipOCRData.length,\u001b[39m\n\u001b[31m    status: 'active',\u001b[39m\n\u001b[31m    createdAt: new Date().toISOString()\u001b[39m\n\u001b[31m  };\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction validate(d) {\u001b[39m\n\u001b[31m  if (!d.client) { showToast('Select a client', 'err'); return false; }\u001b[39m\n\u001b[31m  if (!d.courier) { showToast('Select courier', 'err'); return false; }\u001b[39m\n\u001b[31m  if (!d.consignees.length) { showToast('Add at least one consignee', 'err'); return false; }\u001b[39m\n\u001b[31m  for (let i = 0; i < d.consignees.length; i++) {\u001b[39m\n\u001b[31m    const c = d.consignees[i];\u001b[39m\n\u001b[31m    if (!c.name || !c.city) {\u001b[39m\n\u001b[31m      showToast(`Consignee #${i+1} (${c.name||'unnamed'}) — name & city required`, 'err');\u001b[39m\n\u001b[31m      // Scroll to and highlight the problem consignee card\u001b[39m\n\u001b[31m      const cards = document.querySelectorAll('#consList .cons-card');\u001b[39m\n\u001b[31m      if (cards[i]) {\u001b[39m\n\u001b[31m        cards[i].scrollIntoView({ behavior: 'smooth', block: 'center' });\u001b[39m\n\u001b[31m        cards[i].style.borderColor = 'var(--red)';\u001b[39m\n\u001b[31m        cards[i].style.boxShadow = '0 0 0 2px rgba(239,68,68,.3)';\u001b[39m\n\u001b[31m        setTimeout(() => {\u001b[39m\n\u001b[31m          if (cards[i]) { cards[i].style.borderColor = ''; cards[i].style.boxShadow = ''; }\u001b[39m\n\u001b[31m        }, 3000);\u001b[39m\n\u001b[31m      }\u001b[39m\n\u001b[31m      return false;\u001b[39m\n\u001b[31m    }\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m  return true;\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction saveDraft() {\u001b[39m\n\u001b[31m  const d = gatherDispatch();\u001b[39m\n\u001b[31m  d.status = 'draft';\u001b[39m\n\u001b[31m  dispatches.unshift(d);\u001b[39m\n\u001b[31m  saveData();\u001b[39m\n\u001b[31m  updateHistCount();\u001b[39m\n\u001b[31m  showToast('Draft saved ✓', 'ok');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// ── VERIFICATION SCREEN ─────────────────────────────────\u001b[39m\n\u001b[31m// Known docket lengths per courier for validation\u001b[39m\n\u001b[31mconst DOCKET_LENGTHS = {\u001b[39m\n\u001b[31m  'SkyKing': { min: 9, max: 12 },\u001b[39m\n\u001b[31m  'DTDC':    { min: 8, max: 15 },\u001b[39m\n\u001b[31m  'Delhivery': { min: 10, max: 18 },\u001b[39m\n\u001b[31m  'Blue Dart': { min: 10, max: 13 },\u001b[39m\n\u001b[31m  'FedEx':   { min: 10, max: 20 },\u001b[39m\n\u001b[31m  'Ecom Express': { min: 10, max: 14 },\u001b[39m\n\u001b[31m  'India Post': { min: 13, max: 22 },\u001b[39m\n\u001b[31m};·\u001b[39m\n\u001b[31mfunction finaliseDispatch() {\u001b[39m\n\u001b[31m  const d = gatherDispatch();\u001b[39m\n\u001b[31m  if (!validate(d)) return;\u001b[39m\n\u001b[31m  showVerifyScreen(d);\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction showVerifyScreen(d) {\u001b[39m\n\u001b[31m  const pane = document.getElementById('verifyPane');\u001b[39m\n\u001b[31m  const list = document.getElementById('verifyList');\u001b[39m\n\u001b[31m  const summary = document.getElementById('verifySummary');·\u001b[39m\n\u001b[31m  // Build flags\u001b[39m\n\u001b[31m  const docketsSeen = {};\u001b[39m\n\u001b[31m  const courier = d.courier;\u001b[39m\n\u001b[31m  const lenRule = DOCKET_LENGTHS[courier];·\u001b[39m\n\u001b[31m  d.consignees.forEach((c, i) => {\u001b[39m\n\u001b[31m    if (c.docket) docketsSeen[c.docket] = (docketsSeen[c.docket] || 0) + 1;\u001b[39m\n\u001b[31m  });·\u001b[39m\n\u001b[31m  const total     = d.consignees.length;\u001b[39m\n\u001b[31m  const withDock  = d.consignees.filter(c => c.docket).length;\u001b[39m\n\u001b[31m  const dupes     = Object.values(docketsSeen).filter(v => v > 1).length;\u001b[39m\n\u001b[31m  const noDock    = total - withDock;\u001b[39m\n\u001b[31m  const badLen    = lenRule ? d.consignees.filter(c => c.docket &&\u001b[39m\n\u001b[31m    (c.docket.length < lenRule.min || c.docket.length > lenRule.max)).length : 0;·\u001b[39m\n\u001b[31m  // Summary bar\u001b[39m\n\u001b[31m  summary.innerHTML = `\u001b[39m\n\u001b[31m    <div class=\\\"vs-item\\\"><div class=\\\"vs-val\\\" style=\\\"color:var(--t1);\\\">${total}</div><div class=\\\"vs-lbl\\\">Total</div></div>\u001b[39m\n\u001b[31m    <div class=\\\"vs-item\\\"><div class=\\\"vs-val\\\" style=\\\"color:var(--grn);\\\">${withDock}</div><div class=\\\"vs-lbl\\\">Docketed</div></div>\u001b[39m\n\u001b[31m    <div class=\\\"vs-item\\\"><div class=\\\"vs-val\\\" style=\\\"color:${noDock > 0 ? 'var(--ylw)' : 'var(--t3)'};\\\">${noDock}</div><div class=\\\"vs-lbl\\\">No Docket</div></div>\u001b[39m\n\u001b[31m    <div class=\\\"vs-item\\\"><div class=\\\"vs-val\\\" style=\\\"color:${dupes > 0 ? 'var(--red)' : 'var(--t3)'};\\\">${dupes}</div><div class=\\\"vs-lbl\\\">Duplicates</div></div>\u001b[39m\n\u001b[31m    ${badLen > 0 ? `<div class=\\\"vs-item\\\"><div class=\\\"vs-val\\\" style=\\\"color:var(--sky);\\\">${badLen}</div><div class=\\\"vs-lbl\\\">Short/Long</div></div>` : ''}\u001b[39m\n\u001b[31m    <div class=\\\"vs-item\\\" style=\\\"margin-left:auto;text-align:right;\\\">\u001b[39m\n\u001b[31m      <div class=\\\"vs-val\\\" style=\\\"font-size:.8rem;color:var(--t2);\\\">${d.courier||'—'}</div>\u001b[39m\n\u001b[31m      <div class=\\\"vs-lbl\\\">${d.client||'—'} ${d.jobref ? '› '+d.jobref : ''}</div>\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m  `;·\u001b[39m\n\u001b[31m  // Build consignee rows\u001b[39m\n\u001b[31m  list.innerHTML = d.consignees.map((c, i) => {\u001b[39m\n\u001b[31m    const isDupe   = c.docket && docketsSeen[c.docket] > 1;\u001b[39m\n\u001b[31m    const isNoDock = !c.docket;\u001b[39m\n\u001b[31m    const isShort  = lenRule && c.docket &&\u001b[39m\n\u001b[31m      (c.docket.length < lenRule.min || c.docket.length > lenRule.max);·\u001b[39m\n\u001b[31m    const rowClass = isDupe ? 'has-dupe' : (isNoDock ? 'no-docket' : 'ok');·\u001b[39m\n\u001b[31m    // Find matching OCR slip for thumbnail\u001b[39m\n\u001b[31m    const ocrMatch = slipOCRData[i];\u001b[39m\n\u001b[31m    const thumbHtml = ocrMatch?.imageData\u001b[39m\n\u001b[31m      ? `<img class=\\\"verify-thumb\\\" src=\\\"${ocrMatch.imageData}\\\" onclick=\\\"showSlipFull(${i})\\\" title=\\\"View slip\\\">`\u001b[39m\n\u001b[31m      : '';·\u001b[39m\n\u001b[31m    // Flags\u001b[39m\n\u001b[31m    let flags = '';\u001b[39m\n\u001b[31m    if (isDupe)   flags += `<span class=\\\"verify-flag vf-dupe\\\">⚠ Duplicate</span> `;\u001b[39m\n\u001b[31m    if (isNoDock) flags += `<span class=\\\"verify-flag vf-nodocket\\\">No Docket</span> `;\u001b[39m\n\u001b[31m    if (isShort)  flags += `<span class=\\\"verify-flag vf-short\\\">${c.docket.length} digits</span> `;\u001b[39m\n\u001b[31m    if (!isDupe && !isNoDock && !isShort) flags = `<span class=\\\"verify-flag vf-ok\\\">✓ OK</span>`;·\u001b[39m\n\u001b[31m    // OCR confidence note\u001b[39m\n\u001b[31m    const confNote = ocrMatch && ocrMatch.confidence\u001b[39m\n\u001b[31m      ? `<div class=\\\"ocr-correction-note\\\">OCR confidence: ${Math.round(ocrMatch.confidence*100)}% via ${ocrMatch.source||'—'}</div>` : '';·\u001b[39m\n\u001b[31m    // WhatsApp URL\u001b[39m\n\u001b[31m    const waMsg = encodeURIComponent(\u001b[39m\n\u001b[31m      `Dear ${c.name}, your material has been dispatched via ${d.courier}. Docket: ${c.docket||'TBD'}. Track: ${(COURIER_MASTER[d.courier]?.trackUrl||'').replace('{docket}', c.docket||'')}`\u001b[39m\n\u001b[31m    );\u001b[39m\n\u001b[31m    const waUrl = c.phone ? `https://wa.me/91${c.phone.replace(/\\\\D/g,'')}?text=${waMsg}` : '';·\u001b[39m\n\u001b[31m    return `\u001b[39m\n\u001b[31m    <div class=\\\"verify-row ${rowClass}\\\" id=\\\"vrow_${i}\\\">\u001b[39m\n\u001b[31m      <div class=\\\"verify-row-hdr\\\">\u001b[39m\n\u001b[31m        <span class=\\\"verify-num\\\">#${i+1}</span>\u001b[39m\n\u001b[31m        <span class=\\\"verify-store\\\">${c.name||'—'}</span>\u001b[39m\n\u001b[31m        ${flags}\u001b[39m\n\u001b[31m        ${thumbHtml}\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      <div class=\\\"verify-fields\\\" style=\\\"grid-template-columns:1fr 1fr 1fr;\\\">\u001b[39m\n\u001b[31m        <div class=\\\"verify-field\\\">\u001b[39m\n\u001b[31m          <label>Docket No. <span style=\\\"color:var(--acc);\\\">*</span></label>\u001b[39m\n\u001b[31m          <input type=\\\"text\\\" value=\\\"${c.docket||''}\\\" oninput=\\\"updateVerifyField(${i},'docket',this)\\\"\u001b[39m\n\u001b[31m            style=\\\"font-family:var(--fnt-mono);${isDupe||isShort?'border-color:var(--red);':!c.docket?'border-color:var(--ylw);':'color:var(--acc);'}\\\">\u001b[39m\n\u001b[31m          ${confNote}\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m        <div class=\\\"verify-field\\\">\u001b[39m\n\u001b[31m          <label>City</label>\u001b[39m\n\u001b[31m          <input type=\\\"text\\\" value=\\\"${c.city||''}\\\" oninput=\\\"updateVerifyField(${i},'city',this)\\\"\u001b[39m\n\u001b[31m            style=\\\"${!c.city ? 'border-color:var(--ylw);' : ''}\\\">\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m        <div class=\\\"verify-field\\\">\u001b[39m\n\u001b[31m          <label>Pincode</label>\u001b[39m\n\u001b[31m          <input type=\\\"text\\\" value=\\\"${c.pincode||''}\\\" oninput=\\\"updateVerifyField(${i},'pincode',this)\\\"\u001b[39m\n\u001b[31m            style=\\\"font-family:var(--fnt-mono);\\\">\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m        <div class=\\\"verify-field\\\">\u001b[39m\n\u001b[31m          <label>Phone</label>\u001b[39m\n\u001b[31m          <input type=\\\"tel\\\" value=\\\"${c.phone||''}\\\" oninput=\\\"updateVerifyField(${i},'phone',this)\\\">\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m        <div class=\\\"verify-field\\\">\u001b[39m\n\u001b[31m          <label>Material / Contents</label>\u001b[39m\n\u001b[31m          <input type=\\\"text\\\" value=\\\"${c.material||''}\\\" oninput=\\\"updateVerifyField(${i},'material',this)\\\">\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m        <div class=\\\"verify-field\\\">\u001b[39m\n\u001b[31m          <label>Street Address</label>\u001b[39m\n\u001b[31m          <input type=\\\"text\\\" value=\\\"${c.address||''}\\\" oninput=\\\"updateVerifyField(${i},'address',this)\\\"\u001b[39m\n\u001b[31m            placeholder=\\\"Street / locality only\\\">\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      <div class=\\\"verify-actions\\\">\u001b[39m\n\u001b[31m        ${c.docket && c.phone ? `<button class=\\\"btn-wa\\\" onclick=\\\"window.open('${waUrl}','_blank')\\\">📱 WhatsApp</button>` : ''}\u001b[39m\n\u001b[31m        <button class=\\\"btn-sm\\\" onclick=\\\"removeVerifyRow(${i})\\\">✕ Remove</button>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m    </div>`;\u001b[39m\n\u001b[31m  }).join('');·\u001b[39m\n\u001b[31m  // Store dispatch object for confirm\u001b[39m\n\u001b[31m  pane._pendingDispatch = d;\u001b[39m\n\u001b[31m  pane.style.display = 'block';\u001b[39m\n\u001b[31m  pane.scrollTop = 0;\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction updateVerifyField(idx, field, input) {\u001b[39m\n\u001b[31m  const pane = document.getElementById('verifyPane');\u001b[39m\n\u001b[31m  if (!pane._pendingDispatch) return;\u001b[39m\n\u001b[31m  const c = pane._pendingDispatch.consignees[idx];\u001b[39m\n\u001b[31m  if (!c) return;\u001b[39m\n\u001b[31m  c[field] = input.value.trim();\u001b[39m\n\u001b[31m  input.classList.toggle('changed', input.value.trim() !== '');\u001b[39m\n\u001b[31m  // Re-check dupe if docket changed\u001b[39m\n\u001b[31m  if (field === 'docket') refreshVerifyFlags(pane._pendingDispatch);\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction refreshVerifyFlags(d) {\u001b[39m\n\u001b[31m  const seen = {};\u001b[39m\n\u001b[31m  d.consignees.forEach(c => { if (c.docket) seen[c.docket] = (seen[c.docket]||0)+1; });\u001b[39m\n\u001b[31m  d.consignees.forEach((c, i) => {\u001b[39m\n\u001b[31m    const row = document.getElementById('vrow_' + i);\u001b[39m\n\u001b[31m    if (!row) return;\u001b[39m\n\u001b[31m    const isDupe = c.docket && seen[c.docket] > 1;\u001b[39m\n\u001b[31m    row.classList.toggle('has-dupe', isDupe);\u001b[39m\n\u001b[31m  });\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction removeVerifyRow(idx) {\u001b[39m\n\u001b[31m  const pane = document.getElementById('verifyPane');\u001b[39m\n\u001b[31m  if (!pane._pendingDispatch) return;\u001b[39m\n\u001b[31m  const el = document.getElementById('vrow_' + idx);\u001b[39m\n\u001b[31m  if (el) {\u001b[39m\n\u001b[31m    el.style.transition = 'opacity .25s';\u001b[39m\n\u001b[31m    el.style.opacity = '0';\u001b[39m\n\u001b[31m    setTimeout(() => {\u001b[39m\n\u001b[31m      pane._pendingDispatch.consignees.splice(idx, 1);\u001b[39m\n\u001b[31m      showVerifyScreen(pane._pendingDispatch); // re-render with updated indices\u001b[39m\n\u001b[31m    }, 250);\u001b[39m\n\u001b[31m  } else {\u001b[39m\n\u001b[31m    pane._pendingDispatch.consignees.splice(idx, 1);\u001b[39m\n\u001b[31m    showVerifyScreen(pane._pendingDispatch);\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction approveAll() {\u001b[39m\n\u001b[31m  showToast('All rows approved ✓', 'ok');\u001b[39m\n\u001b[31m  document.getElementById('btnConfirmFinalise').style.background = 'var(--grn)';\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction closeVerify() {\u001b[39m\n\u001b[31m  const pane = document.getElementById('verifyPane');\u001b[39m\n\u001b[31m  // If this dispatch was already finalised, warn before going back\u001b[39m\n\u001b[31m  if (_lastFinalisedId && pane._pendingDispatch?.id === _lastFinalisedId) {\u001b[39m\n\u001b[31m    if (!confirm(\u001b[39m\n\u001b[31m      'This dispatch has already been finalised.\\\\n\\\\n' +\u001b[39m\n\u001b[31m      'Going back will take you to the edit form — any changes will create a NEW dispatch record.\\\\n\\\\n' +\u001b[39m\n\u001b[31m      'Go back anyway?'\u001b[39m\n\u001b[31m    )) return;\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m  pane.style.display = 'none';\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction showSlipFull(idx) {\u001b[39m\n\u001b[31m  const s = slipOCRData[idx];\u001b[39m\n\u001b[31m  if (!s?.imageData) return;\u001b[39m\n\u001b[31m  const w = window.open('');\u001b[39m\n\u001b[31m  w.document.write(`<img src=\\\"${s.imageData}\\\" style=\\\"max-width:100%;height:auto;\\\">`);\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// Log OCR correction to sheets for future accuracy improvement\u001b[39m\n\u001b[31masync function logOCRCorrection(original, corrected, courier) {\u001b[39m\n\u001b[31m  if (!window.appConfig.webhook_url || original === corrected) return;\u001b[39m\n\u001b[31m  try {\u001b[39m\n\u001b[31m    await fetch(window.appConfig.webhook_url, {\u001b[39m\n\u001b[31m      method: 'POST', mode: 'no-cors',\u001b[39m\n\u001b[31m      headers: { 'Content-Type': 'application/json' },\u001b[39m\n\u001b[31m      body: JSON.stringify({\u001b[39m\n\u001b[31m        type: 'ocr_correction',\u001b[39m\n\u001b[31m        original, corrected, courier,\u001b[39m\n\u001b[31m        timestamp: new Date().toISOString()\u001b[39m\n\u001b[31m      })\u001b[39m\n\u001b[31m    });\u001b[39m\n\u001b[31m  } catch(e) {}\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mlet _lastFinalisedId = null; // track last finalised dispatch to warn on back·\u001b[39m\n\u001b[31masync function confirmAndFinalise() {\u001b[39m\n\u001b[31m  const pane = document.getElementById('verifyPane');\u001b[39m\n\u001b[31m  const d = pane._pendingDispatch;\u001b[39m\n\u001b[31m  if (!d) return;·\u001b[39m\n\u001b[31m  d.status = 'active';\u001b[39m\n\u001b[31m  d.finalisedAt = new Date().toISOString();·\u001b[39m\n\u001b[31m  // Log OCR corrections\u001b[39m\n\u001b[31m  slipOCRData.forEach((s, i) => {\u001b[39m\n\u001b[31m    const c = d.consignees[i];\u001b[39m\n\u001b[31m    if (c && s.docket && c.docket && s.docket !== c.docket) {\u001b[39m\n\u001b[31m      logOCRCorrection(s.docket, c.docket, d.courier);\u001b[39m\n\u001b[31m    }\u001b[39m\n\u001b[31m  });·\u001b[39m\n\u001b[31m  // Replace existing draft with same ID if present, else prepend\u001b[39m\n\u001b[31m  const existingIdx = dispatches.findIndex(x => x.id === d.id);\u001b[39m\n\u001b[31m  if (existingIdx >= 0) {\u001b[39m\n\u001b[31m    dispatches[existingIdx] = d;\u001b[39m\n\u001b[31m  } else {\u001b[39m\n\u001b[31m    dispatches.unshift(d);\u001b[39m\n\u001b[31m  }·\u001b[39m\n\u001b[31m  _lastFinalisedId = d.id;\u001b[39m\n\u001b[31m  saveData();\u001b[39m\n\u001b[31m  syncDispatchToDB(d); // push to Postgres (non-blocking)\u001b[39m\n\u001b[31m  updateHistCount();\u001b[39m\n\u001b[31m  if (window.appConfig.auto_log !== 'false' && window.appConfig.webhook_url) await logToSheets(d);\u001b[39m\n\u001b[31m  showToast('Dispatch finalised ✓', 'ok');\u001b[39m\n\u001b[31m  pane.style.display = 'none';\u001b[39m\n\u001b[31m  pane._pendingDispatch = null; // clear pending\u001b[39m\n\u001b[31m  resetForm();\u001b[39m\n\u001b[31m  showPane('dashboard');\u001b[39m\n\u001b[31m  renderDashboard();\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction resetForm() {\u001b[39m\n\u001b[31m  document.getElementById('n_client').value = '';\u001b[39m\n\u001b[31m  document.getElementById('n_jobref').value = '';\u001b[39m\n\u001b[31m  document.getElementById('n_courier').value = '';\u001b[39m\n\u001b[31m  document.getElementById('n_remarks').value = '';\u001b[39m\n\u001b[31m  document.getElementById('jobPill').style.display = 'none';\u001b[39m\n\u001b[31m  document.getElementById('customClientField').style.display = 'none';\u001b[39m\n\u001b[31m  document.getElementById('consList').innerHTML = '';\u001b[39m\n\u001b[31m  addConsignee();\u001b[39m\n\u001b[31m  clearSlips();\u001b[39m\n\u001b[31m  clearStores();\u001b[39m\n\u001b[31m  setToday();\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction newDispatch() { resetForm(); showPane('new'); }·\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31m// DASHBOARD\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31mfunction renderDashboard() {\u001b[39m\n\u001b[31m  // Exclude drafts from dashboard — only show active/finalised dispatches\u001b[39m\n\u001b[31m  const activeDis = dispatches.filter(d => d.status !== 'draft');\u001b[39m\n\u001b[31m  const allCons = activeDis.flatMap(d => d.consignees || []);\u001b[39m\n\u001b[31m  const countBy = (status) => allCons.filter(c => c.status === status).length;\u001b[39m\n\u001b[31m  document.getElementById('st-delivered').textContent = countBy('delivered');\u001b[39m\n\u001b[31m  document.getElementById('st-transit').textContent = countBy('transit') + countBy('in-transit');\u001b[39m\n\u001b[31m  document.getElementById('st-booked').textContent = countBy('booked');\u001b[39m\n\u001b[31m  document.getElementById('st-pending').textContent = allCons.filter(c => !c.docket || c.status === 'pending').length;·\u001b[39m\n\u001b[31m  const grid = document.getElementById('dashGrid');\u001b[39m\n\u001b[31m  // Flatten all consignees across dispatches\u001b[39m\n\u001b[31m  let items = activeDis.flatMap(d =>\u001b[39m\n\u001b[31m    (d.consignees || []).filter(c => c.docket).map(c => ({...c, dispatchId: d.id, courier: d.courier, client: d.client, jobref: d.jobref}))\u001b[39m\n\u001b[31m  );\u001b[39m\n\u001b[31m  if (dashFilter !== 'all') {\u001b[39m\n\u001b[31m    items = items.filter(c => {\u001b[39m\n\u001b[31m      if (dashFilter === 'delivered') return c.status === 'delivered';\u001b[39m\n\u001b[31m      if (dashFilter === 'transit') return c.status === 'transit' || c.status === 'in-transit';\u001b[39m\n\u001b[31m      if (dashFilter === 'booked') return c.status === 'booked';\u001b[39m\n\u001b[31m      if (dashFilter === 'pending') return !c.docket || c.status === 'pending';\u001b[39m\n\u001b[31m      return true;\u001b[39m\n\u001b[31m    });\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m  if (!items.length) {\u001b[39m\n\u001b[31m    grid.innerHTML = '<div class=\\\"empty\\\"><div class=\\\"e-icon\\\">📦</div><p>No docket-tracked consignees.<br>Add docket numbers to see tiles.</p></div>';\u001b[39m\n\u001b[31m    return;\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m  const progMap = { delivered:100, transit:60,'in-transit':60, booked:20, pending:5 };\u001b[39m\n\u001b[31m  const progClr = { delivered:'var(--grn)', transit:'var(--ylw)','in-transit':'var(--ylw)', booked:'var(--sky)', pending:'var(--t4)' };\u001b[39m\n\u001b[31m  grid.innerHTML = items.map(c => {\u001b[39m\n\u001b[31m    const sp = c.status || 'booked';\u001b[39m\n\u001b[31m    const prog = progMap[sp] || 20;\u001b[39m\n\u001b[31m    const clr = progClr[sp] || 'var(--t4)';\u001b[39m\n\u001b[31m    return `\u001b[39m\n\u001b[31m    <div class=\\\"docket-tile\\\" onclick=\\\"showDocketDetail('${c.docket}','${c.courier||''}','${c.dispatchId}')\\\">\u001b[39m\n\u001b[31m      <div class=\\\"docket-tile-hdr\\\">\u001b[39m\n\u001b[31m        <div class=\\\"docket-tile-left\\\">\u001b[39m\n\u001b[31m          <div class=\\\"docket-num\\\">${c.docket}</div>\u001b[39m\n\u001b[31m          <div class=\\\"docket-store\\\">${c.name} — ${c.city}</div>\u001b[39m\n\u001b[31m          <div class=\\\"docket-courier\\\">${c.courier||''} · ${c.client||''} ${c.jobref ? '› '+c.jobref : ''}</div>\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m        <span class=\\\"status-pill sp-${sp}\\\">${sp}</span>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      <div style=\\\"display:flex;gap:6px;margin-bottom:6px;align-items:center;\\\">\u001b[39m\n\u001b[31m        <input type=\\\"checkbox\\\" id=\\\"sel_${c.docket.replace(/[^a-z0-9]/gi,'_')}\\\"\u001b[39m\n\u001b[31m          onclick=\\\"event.stopPropagation();toggleTileSelect('${c.docket}')\\\"\u001b[39m\n\u001b[31m          style=\\\"width:14px;height:14px;cursor:pointer;accent-color:var(--acc);\\\">\u001b[39m\n\u001b[31m        <label style=\\\"font-size:.6rem;color:var(--t3);font-family:var(--fnt-mono);cursor:pointer;\\\"\u001b[39m\n\u001b[31m          onclick=\\\"event.stopPropagation();toggleTileSelect('${c.docket}')\\\">Select</label>\u001b[39m\n\u001b[31m        ${c.manualStatus ?\u001b[39m\n\u001b[31m          `<span style=\\\"font-size:.6rem;font-family:var(--fnt-mono);color:var(--grn);\\\">📞 ${c.manualStatus}</span>` : ''}\u001b[39m\n\u001b[31m        ${c.podUploaded ? '<span style=\\\"font-size:.6rem;color:var(--grn);font-family:var(--fnt-mono);\\\">✓ POD</span>' : ''}\u001b[39m\n\u001b[31m        <button onclick=\\\"event.stopPropagation();showManualTracking('${c.docket}','${c.dispatchId}','${c.name}','${c.phone||''}','${c.city||''}')\\\"\u001b[39m\n\u001b[31m          style=\\\"font-size:.6rem;background:rgba(56,189,248,.1);border:1px solid rgba(56,189,248,.3);\u001b[39m\n\u001b[31m          color:var(--sky);padding:2px 7px;border-radius:10px;cursor:pointer;font-family:var(--fnt-mono);\\\">📞 Call Log</button>\u001b[39m\n\u001b[31m        <button onclick=\\\"event.stopPropagation();showPODUpload('${c.docket}','${c.dispatchId}')\\\"\u001b[39m\n\u001b[31m          style=\\\"font-size:.6rem;background:none;border:1px solid var(--bdr);color:var(--t3);\u001b[39m\n\u001b[31m          padding:2px 7px;border-radius:10px;cursor:pointer;font-family:var(--fnt-mono);\\\">📸 POD</button>\u001b[39m\n\u001b[31m        ${c.manualDelivered ? `<button onclick=\\\"event.stopPropagation();resumeAutoTracking('${c.docket}','${c.dispatchId}')\\\"\u001b[39m\n\u001b[31m          style=\\\"font-size:.6rem;background:rgba(251,146,60,.1);border:1px solid rgba(251,146,60,.4);\u001b[39m\n\u001b[31m          color:#fb923c;padding:2px 7px;border-radius:10px;cursor:pointer;font-family:var(--fnt-mono);\\\">↩ Undo</button>` : ''}\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      <div class=\\\"docket-tile-footer\\\">\u001b[39m\n\u001b[31m        <div style=\\\"flex:1;min-width:0;\\\">\u001b[39m\n\u001b[31m          <span class=\\\"docket-last-event docket-evt-${c.docket}\\\" id=\\\"evt_${c.docket.replace(/[^a-z0-9]/gi,'_')}\\\">${c.lastEvent || 'No tracking yet'}</span>\u001b[39m\n\u001b[31m          <span class=\\\"track-spinner\\\" id=\\\"spin_${c.docket.replace(/[^a-z0-9]/gi,'_')}\\\" style=\\\"display:none;color:var(--ylw);font-family:var(--fnt-mono);font-size:.68rem;animation:blink 1s infinite;\\\">⏳ tracking...</span>\u001b[39m\n\u001b[31m          ${c.lastScanLocation && c.lastScanLocation !== c.lastEvent ?·\u001b[39m\n\u001b[31m            `<div style=\\\"font-size:.62rem;color:var(--t3);font-family:var(--fnt-mono);margin-top:2px;\\\">📍 ${c.lastScanLocation}</div>` : ''}\u001b[39m\n\u001b[31m          ${c.lastTrackedAt ?·\u001b[39m\n\u001b[31m            `<div style=\\\"font-size:.6rem;color:var(--t4);font-family:var(--fnt-mono);margin-top:1px;\\\">🕐 tracked ${new Date(c.lastTrackedAt).toLocaleTimeString('en-IN',{hour:'2-digit',minute:'2-digit'})}</div>` : ''}\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m        <button class=\\\"btn-track-live live-btn-${c.docket.replace(/[^a-z0-9]/gi,'_')}\\\" onclick=\\\"event.stopPropagation();liveTrackTile('${c.docket}','${c.courier||''}','${c.dispatchId}')\\\">\u001b[39m\n\u001b[31m          <span class=\\\"live-dot\\\"></span>LIVE\u001b[39m\n\u001b[31m        </button>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      <div class=\\\"docket-progress\\\">\u001b[39m\n\u001b[31m        <div class=\\\"docket-progress-fill\\\" style=\\\"width:${prog}%;background:${clr};\\\"></div>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m    </div>`;\u001b[39m\n\u001b[31m  }).join('');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction filterDash(f, el) {\u001b[39m\n\u001b[31m  dashFilter = f;\u001b[39m\n\u001b[31m  document.querySelectorAll('#dashFilters .btn-sm').forEach(b => b.classList.remove('primary'));\u001b[39m\n\u001b[31m  el.classList.add('primary');\u001b[39m\n\u001b[31m  renderDashboard();\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31masync function liveTrackTile(docket, courier, dispatchId) {\u001b[39m\n\u001b[31m  showToast(`Tracking ${docket}...`, 'info');\u001b[39m\n\u001b[31m  const result = await fetchTracking(docket, courier);\u001b[39m\n\u001b[31m  // Update status in dispatches\u001b[39m\n\u001b[31m  for (const d of dispatches) {\u001b[39m\n\u001b[31m    if (d.id !== dispatchId) continue;\u001b[39m\n\u001b[31m    for (const c of d.consignees) {\u001b[39m\n\u001b[31m      if (c.docket === docket) {\u001b[39m\n\u001b[31m        c.status = result.status || c.status;\u001b[39m\n\u001b[31m        c.lastEvent = result.lastEvent || c.lastEvent;\u001b[39m\n\u001b[31m        c.lastUpdated = new Date().toISOString();\u001b[39m\n\u001b[31m      }\u001b[39m\n\u001b[31m    }\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m  saveData();\u001b[39m\n\u001b[31m  renderDashboard();\u001b[39m\n\u001b[31m  if (result.status === 'failed') {\u001b[39m\n\u001b[31m    // Open courier site as fallback\u001b[39m\n\u001b[31m    openCourierTrackUrl(docket, courier);\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction showDocketDetail(docket, courier, dispatchId) {\u001b[39m\n\u001b[31m  const d = dispatches.find(x => x.id === dispatchId);\u001b[39m\n\u001b[31m  const c = d?.consignees?.find(x => x.docket === docket);\u001b[39m\n\u001b[31m  if (!c) return;\u001b[39m\n\u001b[31m  const content = document.getElementById('detailContent');\u001b[39m\n\u001b[31m  content.innerHTML = `\u001b[39m\n\u001b[31m    <div class=\\\"sheet-title\\\">${docket}</div>\u001b[39m\n\u001b[31m    <div class=\\\"info-tag\\\">📍 <strong>${c.name}</strong> — ${c.city}</div>\u001b[39m\n\u001b[31m    ${c.phone ? `<div class=\\\"info-tag\\\">📞 ${c.phone}</div>` : ''}\u001b[39m\n\u001b[31m    <div class=\\\"info-tag\\\">📦 ${c.material||'—'} ${c.qty ? '('+c.qty+' pcs)':''}</div>\u001b[39m\n\u001b[31m    <div class=\\\"info-tag\\\">🚚 ${courier||'—'} · <span class=\\\"status-pill sp-${c.status||'pending'}\\\">${c.status||'pending'}</span></div>\u001b[39m\n\u001b[31m    ${c.lastEvent ? `<div class=\\\"info-tag\\\">🕐 ${c.lastEvent}</div>` : ''}\u001b[39m\n\u001b[31m    <div class=\\\"sec-div\\\"><span>Tracking History</span></div>\u001b[39m\n\u001b[31m    <div id=\\\"docketTimeline\\\">\u001b[39m\n\u001b[31m      ${c.timeline?.length ?\u001b[39m\n\u001b[31m        c.timeline.map(e => `<div class=\\\"track-evt\\\"><span class=\\\"track-time\\\">${e.time||''}</span><div><div class=\\\"track-desc\\\">${e.desc||''}</div><div class=\\\"track-loc\\\">${e.loc||''}</div></div></div>`).join('') :\u001b[39m\n\u001b[31m        '<div style=\\\"color:var(--t3);font-size:.78rem;padding:10px 0;\\\">No timeline data. Click Live Track to fetch.</div>'}\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m    <div style=\\\"display:flex;gap:6px;flex-wrap:wrap;margin-top:14px;\\\">\u001b[39m\n\u001b[31m      <button class=\\\"btn btn-ghost\\\" style=\\\"flex:1;\\\" onclick=\\\"openCourierTrackUrl('${docket}','${courier}')\\\">🔗 Courier Site</button>\u001b[39m\n\u001b[31m      <button class=\\\"btn btn-orange\\\" style=\\\"flex:1;\\\" onclick=\\\"liveTrackTile('${docket}','${courier}','${dispatchId}');closeOverlay('detailOverlay')\\\">⚡ Live Track</button>\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m    <div style=\\\"display:flex;gap:6px;flex-wrap:wrap;margin-top:6px;\\\">\u001b[39m\n\u001b[31m      <button class=\\\"btn-wa\\\" style=\\\"flex:1;padding:8px;\\\" onclick=\\\"closeOverlay('detailOverlay');showPODUpload('${docket}','${dispatchId}')\\\">📸 Upload POD</button>\u001b[39m\n\u001b[31m      ${c.podUploaded ? '<span style=\\\"color:var(--grn);font-size:.72rem;font-family:var(--fnt-mono);padding:6px;\\\">✓ POD uploaded</span>' : ''}\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m  `;\u001b[39m\n\u001b[31m  openOverlay('detailOverlay');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31m// TRACKING ENGINE\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31mfunction _normaliseStatus(raw) {\u001b[39m\n\u001b[31m  const s = (raw || '').toLowerCase().trim();\u001b[39m\n\u001b[31m  if (!s || s === 'error') return null;\u001b[39m\n\u001b[31m  if (s.includes('undeliver') || s.includes('delivery_fail') ||\u001b[39m\n\u001b[31m      s.includes('failed_delivery') || s.includes('not deliver') ||\u001b[39m\n\u001b[31m      s.includes('return')  || s.includes('rto')) return 'pending';\u001b[39m\n\u001b[31m  if (s.includes('deliver'))                       return 'delivered';\u001b[39m\n\u001b[31m  if (s.includes('transit') || s.includes('hub') ||\u001b[39m\n\u001b[31m      s.includes('inbound') || s.includes('outbound') ||\u001b[39m\n\u001b[31m      s.includes('out for'))                       return 'transit';\u001b[39m\n\u001b[31m  if (s.includes('book') || s.includes('pick'))   return 'booked';\u001b[39m\n\u001b[31m  return s;\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31masync function _fetchWithRetry(url, opts, maxRetries = 1, retryDelayMs = 4000) {\u001b[39m\n\u001b[31m  for (let attempt = 0; attempt <= maxRetries; attempt++) {\u001b[39m\n\u001b[31m    try {\u001b[39m\n\u001b[31m      const resp = await fetch(url, opts);\u001b[39m\n\u001b[31m      if (resp.ok) return resp;\u001b[39m\n\u001b[31m    } catch(e) {\u001b[39m\n\u001b[31m      if (attempt === maxRetries) throw e;\u001b[39m\n\u001b[31m      await new Promise(r => setTimeout(r, retryDelayMs));\u001b[39m\n\u001b[31m    }\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m  return null;\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31masync function fetchTracking(docket, courier) {\u001b[39m\n\u001b[31m  // Strategy 1: VPS proxy (SkyKing + others that block direct fetch)\u001b[39m\n\u001b[31m  const proxyUrl = window.appConfig.proxy_url;\u001b[39m\n\u001b[31m  if (proxyUrl) {\u001b[39m\n\u001b[31m    try {\u001b[39m\n\u001b[31m      const url  = `${proxyUrl}?courier=${encodeURIComponent(courier)}&docket=${encodeURIComponent(docket)}`;\u001b[39m\n\u001b[31m      const resp = await _fetchWithRetry(url, { signal: AbortSignal.timeout(15000) });\u001b[39m\n\u001b[31m      if (resp) {\u001b[39m\n\u001b[31m        const data      = await resp.json();\u001b[39m\n\u001b[31m        const normStatus = _normaliseStatus(data.status);\u001b[39m\n\u001b[31m        if (normStatus) {\u001b[39m\n\u001b[31m          return { status: normStatus, lastEvent: data.lastEvent || '', timeline: data.timeline || [] };\u001b[39m\n\u001b[31m        }\u001b[39m\n\u001b[31m        openCourierTrackUrl(docket, courier);\u001b[39m\n\u001b[31m        return { status: 'transit', lastEvent: data.lastEvent || 'Check courier site', timeline: [] };\u001b[39m\n\u001b[31m      }\u001b[39m\n\u001b[31m    } catch(e) {}\u001b[39m\n\u001b[31m  }·\u001b[39m\n\u001b[31m  // Strategy 2: Delhivery direct API\u001b[39m\n\u001b[31m  if (courier === 'Delhivery') {\u001b[39m\n\u001b[31m    try {\u001b[39m\n\u001b[31m      const resp = await _fetchWithRetry(\u001b[39m\n\u001b[31m        `https://dlv-api.delhivery.com/v3/track?waybill=${docket}`,\u001b[39m\n\u001b[31m        { signal: AbortSignal.timeout(8000) }\u001b[39m\n\u001b[31m      );\u001b[39m\n\u001b[31m      if (resp) {\u001b[39m\n\u001b[31m        const data = await resp.json();\u001b[39m\n\u001b[31m        const pkg  = data.ShipmentData?.[0]?.Shipment;\u001b[39m\n\u001b[31m        if (pkg) {\u001b[39m\n\u001b[31m          const normStatus = _normaliseStatus(pkg.Status?.Status);\u001b[39m\n\u001b[31m          return { status: normStatus || 'transit', lastEvent: pkg.Status?.Instructions || '', timeline: [] };\u001b[39m\n\u001b[31m        }\u001b[39m\n\u001b[31m      }\u001b[39m\n\u001b[31m    } catch(e) {}\u001b[39m\n\u001b[31m  }·\u001b[39m\n\u001b[31m  return { status: 'failed', lastEvent: 'Open courier site to check', timeline: [] };\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction openCourierTrackUrl(docket, courier) {\u001b[39m\n\u001b[31m  const m = COURIER_MASTER[courier];\u001b[39m\n\u001b[31m  if (!m) return;\u001b[39m\n\u001b[31m  const url = m.trackUrl.replace('{docket}', docket);\u001b[39m\n\u001b[31m  window.open(url, '_blank');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// Track tab - multiple dockets\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31m// TRACK TAB — new logic\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31mlet _trackSelected = new Set(); // dockets currently selected for tracking·\u001b[39m\n\u001b[31m// Called when Track tab opens or courier changes\u001b[39m\n\u001b[31mfunction buildTrackDocketChips() {\u001b[39m\n\u001b[31m  const courier = document.getElementById('t_courier').value;\u001b[39m\n\u001b[31m  const m = COURIER_MASTER[courier];\u001b[39m\n\u001b[31m  const maxBulk = m?.maxBulk || 1;\u001b[39m\n\u001b[31m  const limitEl = document.getElementById('trackCourierLimit');\u001b[39m\n\u001b[31m  if (limitEl) limitEl.textContent = `Max ${maxBulk} at once for ${courier}`;·\u001b[39m\n\u001b[31m  // Gather all dockets from active dispatches for this courier\u001b[39m\n\u001b[31m  const activeDis = dispatches.filter(d => d.status !== 'draft');\u001b[39m\n\u001b[31m  const allDockets = activeDis.flatMap(d =>\u001b[39m\n\u001b[31m    (d.consignees || [])\u001b[39m\n\u001b[31m      .filter(c => c.docket && d.courier === courier)\u001b[39m\n\u001b[31m      .map(c => ({ docket: c.docket, name: c.name, city: c.city, status: c.status || 'booked' }))\u001b[39m\n\u001b[31m  );·\u001b[39m\n\u001b[31m  // Also include dockets from OTHER couriers as un-selectable reference\u001b[39m\n\u001b[31m  const otherDockets = activeDis.flatMap(d =>\u001b[39m\n\u001b[31m    (d.consignees || [])\u001b[39m\n\u001b[31m      .filter(c => c.docket && d.courier !== courier)\u001b[39m\n\u001b[31m      .map(c => ({ docket: c.docket, name: c.name, courier: d.courier }))\u001b[39m\n\u001b[31m  );·\u001b[39m\n\u001b[31m  const chips = document.getElementById('trackDocketChips');\u001b[39m\n\u001b[31m  if (!allDockets.length && !otherDockets.length) {\u001b[39m\n\u001b[31m    chips.innerHTML = '<span style=\\\"font-size:.72rem;color:var(--t3);font-family:var(--fnt-mono);\\\">No dispatched dockets yet — enter manually below</span>';\u001b[39m\n\u001b[31m    return;\u001b[39m\n\u001b[31m  }·\u001b[39m\n\u001b[31m  const spClr = { delivered:'var(--grn)', transit:'var(--ylw)', booked:'var(--sky)', pending:'var(--t4)' };·\u001b[39m\n\u001b[31m  chips.innerHTML = [\u001b[39m\n\u001b[31m    // This courier's dockets\u001b[39m\n\u001b[31m    ...allDockets.map(d => {\u001b[39m\n\u001b[31m      const sel = _trackSelected.has(d.docket);\u001b[39m\n\u001b[31m      const clr = spClr[d.status] || 'var(--t4)';\u001b[39m\n\u001b[31m      return `<button onclick=\\\"toggleTrackDocket('${d.docket}')\\\" id=\\\"chip_${d.docket}\\\"\u001b[39m\n\u001b[31m        style=\\\"font-family:var(--fnt-mono);font-size:.72rem;padding:4px 10px;border-radius:20px;\u001b[39m\n\u001b[31m        cursor:pointer;transition:all .2s;border:1px solid ${sel ? 'var(--acc)' : 'var(--bdr)'};\u001b[39m\n\u001b[31m        background:${sel ? 'rgba(249,115,22,.15)' : 'var(--sur2)'};\u001b[39m\n\u001b[31m        color:${sel ? 'var(--acc)' : 'var(--t2)'};\\\">\u001b[39m\n\u001b[31m        ${d.docket} <span style=\\\"font-size:.6rem;color:${clr};\\\">● ${d.status}</span>\u001b[39m\n\u001b[31m        ${d.city ? `<span style=\\\"font-size:.6rem;color:var(--t3);margin-left:4px;\\\">${d.city}</span>` : ''}\u001b[39m\n\u001b[31m      </button>`;\u001b[39m\n\u001b[31m    }),\u001b[39m\n\u001b[31m    // Other couriers — show greyed, click selects and switches courier\u001b[39m\n\u001b[31m    ...otherDockets.slice(0, 5).map(d =>\u001b[39m\n\u001b[31m      `<span title=\\\"${d.courier}\\\" style=\\\"font-family:var(--fnt-mono);font-size:.68rem;padding:3px 8px;\u001b[39m\n\u001b[31m        border-radius:20px;border:1px dashed var(--bdr);color:var(--t4);cursor:default;\\\">\u001b[39m\n\u001b[31m        ${d.docket} <span style=\\\"font-size:.55rem;\\\">${d.courier}</span>\u001b[39m\n\u001b[31m      </span>`\u001b[39m\n\u001b[31m    )\u001b[39m\n\u001b[31m  ].join('');·\u001b[39m\n\u001b[31m  updateTrackSummary();\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction toggleTrackDocket(docket) {\u001b[39m\n\u001b[31m  const courier = document.getElementById('t_courier').value;\u001b[39m\n\u001b[31m  const m = COURIER_MASTER[courier];\u001b[39m\n\u001b[31m  const maxBulk = m?.maxBulk || 1;·\u001b[39m\n\u001b[31m  if (_trackSelected.has(docket)) {\u001b[39m\n\u001b[31m    _trackSelected.delete(docket);\u001b[39m\n\u001b[31m  } else {\u001b[39m\n\u001b[31m    if (_trackSelected.size >= maxBulk) {\u001b[39m\n\u001b[31m      showToast(`${courier} supports max ${maxBulk} docket(s) at once`, 'err');\u001b[39m\n\u001b[31m      return;\u001b[39m\n\u001b[31m    }\u001b[39m\n\u001b[31m    _trackSelected.add(docket);\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m  buildTrackDocketChips(); // re-render chips with updated selection\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction addManualDockets() {\u001b[39m\n\u001b[31m  const raw = document.getElementById('t_manual_dockets').value;\u001b[39m\n\u001b[31m  const courier = document.getElementById('t_courier').value;\u001b[39m\n\u001b[31m  const m = COURIER_MASTER[courier];\u001b[39m\n\u001b[31m  const maxBulk = m?.maxBulk || 1;\u001b[39m\n\u001b[31m  const newDockets = raw.split(/[,\\\\n]/).map(d => d.trim()).filter(Boolean);\u001b[39m\n\u001b[31m  let added = 0;\u001b[39m\n\u001b[31m  for (const d of newDockets) {\u001b[39m\n\u001b[31m    if (_trackSelected.size >= maxBulk) {\u001b[39m\n\u001b[31m      showToast(`Max ${maxBulk} reached for ${courier}`, 'err');\u001b[39m\n\u001b[31m      break;\u001b[39m\n\u001b[31m    }\u001b[39m\n\u001b[31m    _trackSelected.add(d);\u001b[39m\n\u001b[31m    added++;\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m  document.getElementById('t_manual_dockets').value = '';\u001b[39m\n\u001b[31m  buildTrackDocketChips();\u001b[39m\n\u001b[31m  if (added) showToast(`${added} docket(s) added ✓`, 'ok');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction clearTrackSelection() {\u001b[39m\n\u001b[31m  _trackSelected.clear();\u001b[39m\n\u001b[31m  buildTrackDocketChips();\u001b[39m\n\u001b[31m  document.getElementById('trackResultsArea').innerHTML = '';\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction updateTrackSummary() {\u001b[39m\n\u001b[31m  const summary = document.getElementById('trackSelectedSummary');\u001b[39m\n\u001b[31m  const countEl = document.getElementById('trackSelectedCount');\u001b[39m\n\u001b[31m  const listEl  = document.getElementById('trackSelectedList');\u001b[39m\n\u001b[31m  if (!summary) return;\u001b[39m\n\u001b[31m  const arr = Array.from(_trackSelected);\u001b[39m\n\u001b[31m  summary.style.display = arr.length ? 'block' : 'none';\u001b[39m\n\u001b[31m  if (countEl) countEl.textContent = `${arr.length} docket(s) selected`;\u001b[39m\n\u001b[31m  if (listEl)  listEl.textContent  = arr.slice(0, 5).join(', ') + (arr.length > 5 ? `... +${arr.length-5}` : '');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction openTrackOnCourierSite() {\u001b[39m\n\u001b[31m  const courier = document.getElementById('t_courier').value;\u001b[39m\n\u001b[31m  const dockets = Array.from(_trackSelected);\u001b[39m\n\u001b[31m  if (!dockets.length) { showToast('Select dockets first', 'err'); return; }\u001b[39m\n\u001b[31m  const m = COURIER_MASTER[courier];\u001b[39m\n\u001b[31m  if (!m) return;\u001b[39m\n\u001b[31m  // SkyKing: open with all CNOs comma-separated in URL\u001b[39m\n\u001b[31m  let url;\u001b[39m\n\u001b[31m  if (courier === 'SkyKing') {\u001b[39m\n\u001b[31m    url = `https://skyking.co/track?cno=${dockets.join(',')}`;\u001b[39m\n\u001b[31m  } else if (dockets.length === 1) {\u001b[39m\n\u001b[31m    url = m.trackUrl.replace('{docket}', dockets[0]);\u001b[39m\n\u001b[31m  } else {\u001b[39m\n\u001b[31m    url = m.trackBulkUrl || m.trackUrl.replace('{docket}', dockets[0]);\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m  window.open(url, '_blank');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31masync function trackSelected() {\u001b[39m\n\u001b[31m  const courier = document.getElementById('t_courier').value;\u001b[39m\n\u001b[31m  const dockets = Array.from(_trackSelected);\u001b[39m\n\u001b[31m  if (!dockets.length) { showToast('Select at least one docket', 'err'); return; }·\u001b[39m\n\u001b[31m  const area = document.getElementById('trackResultsArea');\u001b[39m\n\u001b[31m  area.innerHTML = `<div class=\\\"card\\\" style=\\\"text-align:center;padding:20px;\\\">\u001b[39m\n\u001b[31m    <div style=\\\"font-size:1.5rem;margin-bottom:8px;\\\">⏳</div>\u001b[39m\n\u001b[31m    <div style=\\\"font-family:var(--fnt-mono);font-size:.78rem;color:var(--t2);\\\">Tracking ${dockets.length} docket(s) via ${courier}...</div>\u001b[39m\n\u001b[31m  </div>`;·\u001b[39m\n\u001b[31m  let resultsHTML = '';\u001b[39m\n\u001b[31m  for (const docket of dockets) {\u001b[39m\n\u001b[31m    const result = await fetchTracking(docket, courier);\u001b[39m\n\u001b[31m    const sp = (!result.status || result.status === 'failed') ? 'pending' : result.status;\u001b[39m\n\u001b[31m    const spClr = { delivered:'var(--grn)', transit:'var(--ylw)', booked:'var(--sky)', pending:'var(--t3)' };\u001b[39m\n\u001b[31m    const clr = spClr[sp] || 'var(--t3)';\u001b[39m\n\u001b[31m    // Update stored dispatch if this docket exists in our records\u001b[39m\n\u001b[31m    for (const d of dispatches) {\u001b[39m\n\u001b[31m      for (const c of (d.consignees||[])) {\u001b[39m\n\u001b[31m        if (c.docket === docket && result.status && result.status !== 'failed') {\u001b[39m\n\u001b[31m          c.status = result.status;\u001b[39m\n\u001b[31m          c.lastEvent = result.lastEvent;\u001b[39m\n\u001b[31m          c.lastTrackedAt = new Date().toISOString();\u001b[39m\n\u001b[31m        }\u001b[39m\n\u001b[31m      }\u001b[39m\n\u001b[31m    }\u001b[39m\n\u001b[31m    resultsHTML += `\u001b[39m\n\u001b[31m    <div style=\\\"background:var(--sur);border:1px solid var(--bdr);border-left:3px solid ${clr};\u001b[39m\n\u001b[31m      border-radius:var(--r-sm);padding:10px 12px;margin-bottom:8px;\\\">\u001b[39m\n\u001b[31m      <div style=\\\"display:flex;justify-content:space-between;align-items:center;margin-bottom:6px;\\\">\u001b[39m\n\u001b[31m        <span style=\\\"font-family:var(--fnt-mono);font-size:.9rem;color:var(--sky);\\\">${docket}</span>\u001b[39m\n\u001b[31m        <span class=\\\"status-pill sp-${sp}\\\">${sp}</span>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      <div style=\\\"font-size:.75rem;color:var(--t2);font-family:var(--fnt-mono);\\\">${result.lastEvent || '—'}</div>\u001b[39m\n\u001b[31m      ${result.lastScanLocation ? `<div style=\\\"font-size:.68rem;color:var(--t3);margin-top:3px;\\\">📍 ${result.lastScanLocation}</div>` : ''}\u001b[39m\n\u001b[31m      <div style=\\\"display:flex;gap:8px;margin-top:8px;\\\">\u001b[39m\n\u001b[31m        <button class=\\\"btn-sm\\\" onclick=\\\"openCourierTrackUrl('${docket}','${courier}')\\\">🔗 Open on ${courier}</button>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m    </div>`;\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m  saveData();\u001b[39m\n\u001b[31m  area.innerHTML = `<div style=\\\"margin-top:4px;\\\">${resultsHTML}</div>`;\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// Keep old names as aliases for compatibility\u001b[39m\n\u001b[31mfunction trackMultiple() { trackSelected(); }\u001b[39m\n\u001b[31mfunction openCourierSite() { openTrackOnCourierSite(); }·\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31m// HISTORY\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31mfunction renderHistory() {\u001b[39m\n\u001b[31m  const list = document.getElementById('historyList');\u001b[39m\n\u001b[31m  if (!dispatches.length) {\u001b[39m\n\u001b[31m    list.innerHTML = '<div class=\\\"empty\\\"><div class=\\\"e-icon\\\">📭</div><p>No dispatches yet.</p></div>';\u001b[39m\n\u001b[31m    return;\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m  list.innerHTML = dispatches.map(d => {\u001b[39m\n\u001b[31m    const cons = d.consignees?.length || 0;\u001b[39m\n\u001b[31m    const docketed = d.consignees?.filter(c=>c.docket).length || 0;\u001b[39m\n\u001b[31m    const clr = d.status === 'draft' ? 'var(--ylw)' : 'var(--grn)';\u001b[39m\n\u001b[31m    return `\u001b[39m\n\u001b[31m    <div class=\\\"docket-tile\\\" onclick=\\\"showDispatchDetail('${d.id}')\\\">\u001b[39m\n\u001b[31m      <div class=\\\"docket-tile-hdr\\\">\u001b[39m\n\u001b[31m        <div class=\\\"docket-tile-left\\\">\u001b[39m\n\u001b[31m          <div class=\\\"docket-num\\\" style=\\\"font-size:.78rem;\\\">${d.id}</div>\u001b[39m\n\u001b[31m          <div class=\\\"docket-store\\\">${d.client||'No client'} ${d.jobref ? '› '+d.jobref : ''}</div>\u001b[39m\n\u001b[31m          <div class=\\\"docket-courier\\\">📦 ${cons} consignees · ${docketed} docketed · ${d.courier||'—'} · ${d.dispatchDate||d.createdAt?.slice(0,10)||'—'}</div>\u001b[39m\n\u001b[31m        </div>\u001b[39m\n\u001b[31m        <span class=\\\"status-pill\\\" style=\\\"background:rgba(0,0,0,.2);color:${clr};\\\">${d.status||'active'}</span>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m    </div>`;\u001b[39m\n\u001b[31m  }).join('');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction showDispatchDetail(id) {\u001b[39m\n\u001b[31m  const d = dispatches.find(x => x.id === id);\u001b[39m\n\u001b[31m  if (!d) return;\u001b[39m\n\u001b[31m  const content = document.getElementById('detailContent');\u001b[39m\n\u001b[31m  content.innerHTML = `\u001b[39m\n\u001b[31m    <div class=\\\"sheet-title\\\">${d.client} ${d.jobref ? '› '+d.jobref : ''}</div>\u001b[39m\n\u001b[31m    <div class=\\\"info-tag\\\"><strong>${d.id}</strong> · ${d.courier||'—'} · ${d.dispatchDate||'—'}</div>\u001b[39m\n\u001b[31m    <div class=\\\"sec-div\\\"><span>Consignees (${d.consignees?.length||0})</span></div>\u001b[39m\n\u001b[31m    ${(d.consignees||[]).map((c,i) => `\u001b[39m\n\u001b[31m      <div style=\\\"background:var(--sur2);border:1px solid var(--bdr);border-radius:var(--r-sm);padding:10px;margin-bottom:6px;\\\">\u001b[39m\n\u001b[31m        <div style=\\\"font-weight:600;margin-bottom:4px;\\\">${i+1}. ${c.name} — ${c.city}</div>\u001b[39m\n\u001b[31m        ${c.docket ? `<div style=\\\"font-family:var(--fnt-mono);font-size:.78rem;color:var(--sky);\\\">🔖 ${c.docket}</div>` : ''}\u001b[39m\n\u001b[31m        ${c.material ? `<div style=\\\"font-size:.75rem;color:var(--t2);\\\">📦 ${c.material} ${c.qty ? '('+c.qty+')':''}</div>` : ''}\u001b[39m\n\u001b[31m        <span class=\\\"status-pill sp-${c.status||'pending'}\\\" style=\\\"margin-top:4px;display:inline-block;\\\">${c.status||'pending'}</span>\u001b[39m\n\u001b[31m      </div>`).join('')}\u001b[39m\n\u001b[31m    ${d.remarks ? `<div class=\\\"info-tag\\\">📝 ${d.remarks}</div>` : ''}\u001b[39m\n\u001b[31m    <div class=\\\"btn-row\\\" style=\\\"margin-top:14px;\\\">\u001b[39m\n\u001b[31m      <button class=\\\"btn btn-ghost\\\" onclick=\\\"deleteDispatch('${d.id}');closeOverlay('detailOverlay')\\\">🗑️ Delete</button>\u001b[39m\n\u001b[31m      <button class=\\\"btn btn-orange\\\" onclick=\\\"closeOverlay('detailOverlay');showPane('dashboard')\\\">📊 View in Dashboard</button>\u001b[39m\n\u001b[31m    </div>\u001b[39m\n\u001b[31m  `;\u001b[39m\n\u001b[31m  openOverlay('detailOverlay');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction deleteDispatch(id) {\u001b[39m\n\u001b[31m  if (!confirm('Delete this dispatch?')) return;\u001b[39m\n\u001b[31m  dispatches = dispatches.filter(d => d.id !== id);\u001b[39m\n\u001b[31m  saveData();\u001b[39m\n\u001b[31m  updateHistCount();\u001b[39m\n\u001b[31m  renderHistory();\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction updateHistCount() {\u001b[39m\n\u001b[31m  document.getElementById('histCount').textContent = dispatches.length;\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31m// GOOGLE SHEETS\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31masync function logToSheets(d) {\u001b[39m\n\u001b[31m  const url = window.appConfig.webhook_url;\u001b[39m\n\u001b[31m  if (!url) return;\u001b[39m\n\u001b[31m  try {\u001b[39m\n\u001b[31m    await fetch(url, {\u001b[39m\n\u001b[31m      method: 'POST', mode: 'no-cors',\u001b[39m\n\u001b[31m      headers: { 'Content-Type': 'application/json' },\u001b[39m\n\u001b[31m      body: JSON.stringify({\u001b[39m\n\u001b[31m        type: 'dispatch', id: d.id, client: d.client, jobref: d.jobref,\u001b[39m\n\u001b[31m        courier: d.courier, consignees: d.consignees?.length||0,\u001b[39m\n\u001b[31m        destinations: d.consignees?.map(c=>c.city).join(', ')||'',\u001b[39m\n\u001b[31m        dockets: d.consignees?.filter(c=>c.docket).map(c=>c.docket).join(', ')||'',\u001b[39m\n\u001b[31m        dispatchDate: d.dispatchDate, status: d.status,\u001b[39m\n\u001b[31m        timestamp: new Date().toISOString(), company: window.appConfig.company\u001b[39m\n\u001b[31m      })\u001b[39m\n\u001b[31m    });\u001b[39m\n\u001b[31m  } catch(e) {}\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31m// CONFIG — Google Sheet is the source of truth\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════·\u001b[39m\n\u001b[31mfunction onSheetsUrlChange() {\u001b[39m\n\u001b[31m  const url = document.getElementById('cfg_sheets_url').value.trim();\u001b[39m\n\u001b[31m  // Show sync button state\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction saveSheetsUrl() {\u001b[39m\n\u001b[31m  const url = document.getElementById('cfg_sheets_url').value.trim();\u001b[39m\n\u001b[31m  if (!url) { showToast('Enter the Apps Script URL first', 'err'); return; }\u001b[39m\n\u001b[31m  window.appConfig.sheets_url = url;\u001b[39m\n\u001b[31m  saveSheetsUrlLocal();\u001b[39m\n\u001b[31m  syncFromSheet(false);\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m/**\u001b[39m\n\u001b[31m * Fetch all config from Google Sheet and apply to appConfig.\u001b[39m\n\u001b[31m * Called on startup (silent=true) and manually (silent=false).\u001b[39m\n\u001b[31m */\u001b[39m\n\u001b[31masync function syncFromSheet(silent = false) {\u001b[39m\n\u001b[31m  const url = window.appConfig.sheets_url;\u001b[39m\n\u001b[31m  if (!url) { if (!silent) showToast('No Sheets URL configured', 'err'); return; }·\u001b[39m\n\u001b[31m  setSyncStatus('syncing', 'Syncing config from Google Sheet...');\u001b[39m\n\u001b[31m  try {\u001b[39m\n\u001b[31m    const resp = await fetch(url + '?action=pwa_config', {\u001b[39m\n\u001b[31m      method: 'GET',\u001b[39m\n\u001b[31m      redirect: 'follow'\u001b[39m\n\u001b[31m    });\u001b[39m\n\u001b[31m    if (!resp.ok) throw new Error('HTTP ' + resp.status);\u001b[39m\n\u001b[31m    const cfg = await resp.json();·\u001b[39m\n\u001b[31m    // Apply all values from sheet to appConfig\u001b[39m\n\u001b[31m    const keyMap = {\u001b[39m\n\u001b[31m      proxy_url:           'proxy_url',\u001b[39m\n\u001b[31m      webhook_url:         'webhook_url',\u001b[39m\n\u001b[31m      ollama_url:          'ollama_url',\u001b[39m\n\u001b[31m      ocr_mode:            'ocr_mode',\u001b[39m\n\u001b[31m      company:             'company',\u001b[39m\n\u001b[31m      city:                'city',\u001b[39m\n\u001b[31m      auto_log:            'auto_log',\u001b[39m\n\u001b[31m      refresh_on_load:     'refresh_on_load',\u001b[39m\n\u001b[31m      admin_password:      'admin_password',\u001b[39m\n\u001b[31m      n8n_dispatch_webhook:'n8n_dispatch_webhook',\u001b[39m\n\u001b[31m    };\u001b[39m\n\u001b[31m    Object.entries(keyMap).forEach(([sheetKey, cfgKey]) => {\u001b[39m\n\u001b[31m      if (cfg[sheetKey] !== undefined && cfg[sheetKey] !== '') {\u001b[39m\n\u001b[31m        window.appConfig[cfgKey] = cfg[sheetKey];\u001b[39m\n\u001b[31m      }\u001b[39m\n\u001b[31m    });·\u001b[39m\n\u001b[31m    // Apply courier master if provided\u001b[39m\n\u001b[31m    if (cfg._courier_master && Array.isArray(cfg._courier_master)) {\u001b[39m\n\u001b[31m      cfg._courier_master.forEach(c => {\u001b[39m\n\u001b[31m        if (c.name && c.trackUrl) {\u001b[39m\n\u001b[31m          COURIER_MASTER[c.name] = {\u001b[39m\n\u001b[31m            trackUrl: c.trackUrl,\u001b[39m\n\u001b[31m            trackBulkUrl: c.bulkUrl || '',\u001b[39m\n\u001b[31m            maxBulk: parseInt(c.maxBulk) || 1\u001b[39m\n\u001b[31m          };\u001b[39m\n\u001b[31m        }\u001b[39m\n\u001b[31m      });\u001b[39m\n\u001b[31m    }·\u001b[39m\n\u001b[31m    renderCourierMasterList();\u001b[39m\n\u001b[31m    renderSheetConfigDisplay(cfg);\u001b[39m\n\u001b[31m    setSyncStatus('ok', 'Synced from sheet · ' + new Date().toLocaleTimeString());\u001b[39m\n\u001b[31m    if (!silent) showToast('Config synced from sheet ✓', 'ok');·\u001b[39m\n\u001b[31m    // If refresh_on_load is true, trigger dashboard refresh\u001b[39m\n\u001b[31m    if (window.appConfig.refresh_on_load === 'true') {\u001b[39m\n\u001b[31m      setTimeout(refreshAllTracking, 2000);\u001b[39m\n\u001b[31m    }·\u001b[39m\n\u001b[31m  } catch(e) {\u001b[39m\n\u001b[31m    setSyncStatus('err', 'Sync failed: ' + e.message);\u001b[39m\n\u001b[31m    if (!silent) showToast('Sync failed — check URL', 'err');\u001b[39m\n\u001b[31m    console.error('Sheets sync error:', e);\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction setSyncStatus(state, msg) {\u001b[39m\n\u001b[31m  const el = document.getElementById('syncStatus');\u001b[39m\n\u001b[31m  const icon = document.getElementById('syncIcon');\u001b[39m\n\u001b[31m  const msgEl = document.getElementById('syncMsg');\u001b[39m\n\u001b[31m  el.style.display = 'flex';\u001b[39m\n\u001b[31m  icon.textContent = state === 'ok' ? '✅' : state === 'err' ? '❌' : '⏳';\u001b[39m\n\u001b[31m  msgEl.textContent = msg;\u001b[39m\n\u001b[31m  el.style.borderColor = state === 'ok' ? 'rgba(34,197,94,.3)' : state === 'err' ? 'rgba(239,68,68,.3)' : 'rgba(234,179,8,.3)';\u001b[39m\n\u001b[31m  el.style.background = state === 'ok' ? 'rgba(34,197,94,.05)' : state === 'err' ? 'rgba(239,68,68,.05)' : 'rgba(234,179,8,.05)';\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction renderSheetConfigDisplay(cfg) {\u001b[39m\n\u001b[31m  const display = document.getElementById('sheetConfigDisplay');\u001b[39m\n\u001b[31m  display.style.display = 'block';\u001b[39m\n\u001b[31m  document.getElementById('sheetSyncTime').textContent = 'Last sync: ' + new Date().toLocaleTimeString();\u001b[39m\n\u001b[31m  const showKeys = ['proxy_url','webhook_url','ollama_url','ocr_mode','company','city','auto_log'];\u001b[39m\n\u001b[31m  document.getElementById('sheetConfigRows').innerHTML = showKeys.map(k => {\u001b[39m\n\u001b[31m    const val = cfg[k] || window.appConfig[k] || '—';\u001b[39m\n\u001b[31m    const isEmpty = !cfg[k];\u001b[39m\n\u001b[31m    return `<div class=\\\"courier-master-row\\\">\u001b[39m\n\u001b[31m      <div class=\\\"cml\\\">\u001b[39m\n\u001b[31m        <div class=\\\"cml-name\\\" style=\\\"font-family:var(--fnt-mono);font-size:.75rem;\\\">${k}</div>\u001b[39m\n\u001b[31m        <div class=\\\"cml-url\\\" style=\\\"${isEmpty ? 'color:var(--red);' : ''}\\\">${isEmpty ? '⚠️ Not set in sheet' : val}</div>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m    </div>`;\u001b[39m\n\u001b[31m  }).join('');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m/**\u001b[39m\n\u001b[31m * Push current config changes back to the sheet.\u001b[39m\n\u001b[31m * Called when user edits config from PWA (rare — usually done in sheet directly).\u001b[39m\n\u001b[31m */\u001b[39m\n\u001b[31masync function pushConfigToSheet(updates) {\u001b[39m\n\u001b[31m  const url = window.appConfig.sheets_url;\u001b[39m\n\u001b[31m  if (!url) return;\u001b[39m\n\u001b[31m  try {\u001b[39m\n\u001b[31m    await fetch(url, {\u001b[39m\n\u001b[31m      method: 'POST', mode: 'no-cors',\u001b[39m\n\u001b[31m      headers: { 'Content-Type': 'application/json' },\u001b[39m\n\u001b[31m      body: JSON.stringify({ type: 'save_config', config: updates })\u001b[39m\n\u001b[31m    });\u001b[39m\n\u001b[31m  } catch(e) {}\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction clearAll() {\u001b[39m\n\u001b[31m  if (!confirm('Delete ALL dispatch data?')) return;\u001b[39m\n\u001b[31m  dispatches = [];\u001b[39m\n\u001b[31m  saveData();\u001b[39m\n\u001b[31m  updateHistCount();\u001b[39m\n\u001b[31m  renderDashboard();\u001b[39m\n\u001b[31m  renderHistory();\u001b[39m\n\u001b[31m  showToast('Cleared', 'ok');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction renderCourierMasterList() {\u001b[39m\n\u001b[31m  const el = document.getElementById('courierMasterList');\u001b[39m\n\u001b[31m  el.innerHTML = Object.entries(COURIER_MASTER).map(([name, info]) => `\u001b[39m\n\u001b[31m    <div class=\\\"courier-master-row\\\">\u001b[39m\n\u001b[31m      <div class=\\\"cml\\\">\u001b[39m\n\u001b[31m        <div class=\\\"cml-name\\\">${name}</div>\u001b[39m\n\u001b[31m        <div class=\\\"cml-url\\\">${info.trackUrl}</div>\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m      <button class=\\\"btn-sm\\\" onclick=\\\"window.open('${info.trackBulkUrl||info.trackUrl.replace('{docket}','')}','_blank')\\\">Open</button>\u001b[39m\n\u001b[31m    </div>`).join('');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction openCourierMaster() {\u001b[39m\n\u001b[31m  const ed = document.getElementById('courierMasterEditor');\u001b[39m\n\u001b[31m  ed.innerHTML = Object.entries(COURIER_MASTER).map(([name, info]) => `\u001b[39m\n\u001b[31m    <div style=\\\"margin-bottom:14px;\\\">\u001b[39m\n\u001b[31m      <div style=\\\"font-family:var(--fnt-head);font-size:.75rem;font-weight:700;color:var(--t2);text-transform:uppercase;letter-spacing:1.5px;margin-bottom:6px;\\\">${name}</div>\u001b[39m\n\u001b[31m      <div class=\\\"f\\\" style=\\\"margin-bottom:6px;\\\">\u001b[39m\n\u001b[31m        <label>Track URL (use {docket})</label>\u001b[39m\n\u001b[31m        <input type=\\\"text\\\" value=\\\"${info.trackUrl}\\\" id=\\\"cm_${name.replace(/\\\\s/g,'_')}\\\"\u001b[39m\n\u001b[31m          style=\\\"font-family:var(--fnt-mono);font-size:.75rem;\\\">\u001b[39m\n\u001b[31m      </div>\u001b[39m\n\u001b[31m    </div>`).join('') +\u001b[39m\n\u001b[31m    `<button class=\\\"btn btn-orange\\\" onclick=\\\"saveCourierMaster()\\\">Save Courier Master</button>`;\u001b[39m\n\u001b[31m  openOverlay('courierMasterOverlay');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction saveCourierMaster() {\u001b[39m\n\u001b[31m  Object.keys(COURIER_MASTER).forEach(name => {\u001b[39m\n\u001b[31m    const el = document.getElementById('cm_' + name.replace(/\\\\s/g,'_'));\u001b[39m\n\u001b[31m    if (el) COURIER_MASTER[name].trackUrl = el.value;\u001b[39m\n\u001b[31m  });\u001b[39m\n\u001b[31m  closeOverlay('courierMasterOverlay');\u001b[39m\n\u001b[31m  renderCourierMasterList();\u001b[39m\n\u001b[31m  showToast('Courier master saved ✓', 'ok');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31m// UI UTILS\u001b[39m\n\u001b[31m// ════════════════════════════════════════════════════════\u001b[39m\n\u001b[31m// ── ADMIN AUTH ───────────────────────────────────────────────────────────\u001b[39m\n\u001b[31mlet _adminAuthed = false;\u001b[39m\n\u001b[31mlet _lastAdminPane = null;·\u001b[39m\n\u001b[31mfunction showPane(name) {\u001b[39m\n\u001b[31m  // Config tab requires admin auth — re-ask if navigated away\u001b[39m\n\u001b[31m  if (name === 'config') {\u001b[39m\n\u001b[31m    if (!_adminAuthed) {\u001b[39m\n\u001b[31m      _lastAdminPane = 'config';\u001b[39m\n\u001b[31m      showAdminLogin();\u001b[39m\n\u001b[31m      return;\u001b[39m\n\u001b[31m    }\u001b[39m\n\u001b[31m  } else {\u001b[39m\n\u001b[31m    // User navigated away from config — revoke auth\u001b[39m\n\u001b[31m    if (_lastAdminPane === 'config') _adminAuthed = false;\u001b[39m\n\u001b[31m    _lastAdminPane = name;\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m  document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));\u001b[39m\n\u001b[31m  document.querySelectorAll('.pane').forEach(p => p.classList.remove('active'));\u001b[39m\n\u001b[31m  document.getElementById('tab-'+name).classList.add('active');\u001b[39m\n\u001b[31m  document.getElementById('pane-'+name).classList.add('active');\u001b[39m\n\u001b[31m  if (name === 'dashboard') renderDashboard();\u001b[39m\n\u001b[31m  if (name === 'history') renderHistory();\u001b[39m\n\u001b[31m  if (name === 'track') buildTrackDocketChips();\u001b[39m\n\u001b[31m  if (name === 'config') { document.getElementById('cfg_sheets_url').value = window.appConfig.sheets_url || ''; }\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction showAdminLogin() {\u001b[39m\n\u001b[31m  // Password replaced by hub identity — only harish/pramod can access config\u001b[39m\n\u001b[31m  if (['harish', 'pramod'].includes(window._hubUser?.id)) {\u001b[39m\n\u001b[31m    _adminAuthed = true;\u001b[39m\n\u001b[31m    showPane('config');\u001b[39m\n\u001b[31m  } else {\u001b[39m\n\u001b[31m    showToast('Config access restricted to admin users', 'err');\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31masync function submitAdminLogin() {\u001b[39m\n\u001b[31m  showAdminLogin(); // no-op shim — kept for any lingering HTML onclick refs\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31mfunction openOverlay(id) { document.getElementById(id).classList.add('open'); }\u001b[39m\n\u001b[31mfunction closeOverlay(id) { document.getElementById(id).classList.remove('open'); }·\u001b[39m\n\u001b[31mlet toastTimer;\u001b[39m\n\u001b[31mfunction showToast(msg, type='') {\u001b[39m\n\u001b[31m  const t = document.getElementById('toast');\u001b[39m\n\u001b[31m  t.textContent = msg;\u001b[39m\n\u001b[31m  t.className = 'toast show' + (type ? ' '+type : '');\u001b[39m\n\u001b[31m  clearTimeout(toastTimer);\u001b[39m\n\u001b[31m  toastTimer = setTimeout(() => t.classList.remove('show'), 2800);\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction showOCRWorking(show, msg='') {\u001b[39m\n\u001b[31m  const el = document.getElementById('ocrWorking');\u001b[39m\n\u001b[31m  el.classList.toggle('visible', show);\u001b[39m\n\u001b[31m  if (msg) document.getElementById('ocrWorkingText').textContent = msg;\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction showAIProgress(label, pct) {\u001b[39m\n\u001b[31m  const el = document.getElementById('aiProgress');\u001b[39m\n\u001b[31m  el.classList.add('visible');\u001b[39m\n\u001b[31m  document.getElementById('aiProgLabel').textContent = label;\u001b[39m\n\u001b[31m  document.getElementById('aiProgFill').style.width = pct + '%';\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31mfunction hideAIProgress() {\u001b[39m\n\u001b[31m  document.getElementById('aiProgress').classList.remove('visible');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction updateNetDot() {\u001b[39m\n\u001b[31m  document.getElementById('netDot').style.background = navigator.onLine ? 'var(--grn)' : 'var(--red)';\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction setToday() {\u001b[39m\n\u001b[31m  document.getElementById('n_date').value = new Date().toISOString().split('T')[0];\u001b[39m\n\u001b[31m}\u001b[39m\n\u001b[31m// ── EXPORT CLEAN SHEET ───────────────────────────────────────────────────\u001b[39m\n\u001b[31mfunction exportCleanSheet() {\u001b[39m\n\u001b[31m  const pane = document.getElementById('verifyPane');\u001b[39m\n\u001b[31m  const d = pane._pendingDispatch;\u001b[39m\n\u001b[31m  if (!d || !d.consignees?.length) { showToast('No data to export', 'err'); return; }·\u001b[39m\n\u001b[31m  // Build CSV\u001b[39m\n\u001b[31m  const headers = ['S.No.','Store Name','City','Pincode','Phone','Address','Docket No.','Material','Courier','Status'];\u001b[39m\n\u001b[31m  const rows = d.consignees.map((c, i) => [\u001b[39m\n\u001b[31m    i + 1,\u001b[39m\n\u001b[31m    `\\\"${(c.name||'').replace(/\\\"/g,'\\\"\\\"')}\\\"`,\u001b[39m\n\u001b[31m    `\\\"${(c.city||'').replace(/\\\"/g,'\\\"\\\"')}\\\"`,\u001b[39m\n\u001b[31m    c.pincode || '',\u001b[39m\n\u001b[31m    c.phone || '',\u001b[39m\n\u001b[31m    `\\\"${(c.address||'').replace(/\\\"/g,'\\\"\\\"')}\\\"`,\u001b[39m\n\u001b[31m    c.docket || '',\u001b[39m\n\u001b[31m    `\\\"${(c.material||'').replace(/\\\"/g,'\\\"\\\"')}\\\"`,\u001b[39m\n\u001b[31m    d.courier || '',\u001b[39m\n\u001b[31m    c.status || 'pending'\u001b[39m\n\u001b[31m  ]);·\u001b[39m\n\u001b[31m  const csv = [headers.join(','), ...rows.map(r => r.join(','))].join('\\\\n');\u001b[39m\n\u001b[31m  const blob = new Blob([csv], { type: 'text/csv' });\u001b[39m\n\u001b[31m  const url = URL.createObjectURL(blob);\u001b[39m\n\u001b[31m  const a = document.createElement('a');\u001b[39m\n\u001b[31m  a.href = url;\u001b[39m\n\u001b[31m  a.download = `360LM_Dispatch_${d.client||'export'}_${d.dispatchDate||new Date().toISOString().slice(0,10)}_clean.csv`;\u001b[39m\n\u001b[31m  a.click();\u001b[39m\n\u001b[31m  URL.revokeObjectURL(url);\u001b[39m\n\u001b[31m  showToast('Clean sheet exported ✓', 'ok');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// ── DOCKET SLIP UPLOAD — SKIP OCR IF DOCKET ALREADY MAPPED ───────────────\u001b[39m\n\u001b[31mfunction shouldSkipOCR() {\u001b[39m\n\u001b[31m  // If any consignee already has a docket (from store list mapping), skip OCR extraction\u001b[39m\n\u001b[31m  const consList = document.getElementById('consList');\u001b[39m\n\u001b[31m  if (!consList) return false;\u001b[39m\n\u001b[31m  const cards = consList.querySelectorAll('.cons-card');\u001b[39m\n\u001b[31m  let hasAnyDocket = false;\u001b[39m\n\u001b[31m  cards.forEach(card => {\u001b[39m\n\u001b[31m    const d = card.querySelector('.cons-docket-input')?.value?.trim();\u001b[39m\n\u001b[31m    if (d) hasAnyDocket = true;\u001b[39m\n\u001b[31m  });\u001b[39m\n\u001b[31m  return hasAnyDocket;\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// ─── TRACKING UPDATE TO SHEETS ──────────────────────────────────────────\u001b[39m\n\u001b[31masync function logTrackingUpdate(docket, status, lastEvent, location) {\u001b[39m\n\u001b[31m  const url = window.appConfig.webhook_url;\u001b[39m\n\u001b[31m  if (!url) return;\u001b[39m\n\u001b[31m  try {\u001b[39m\n\u001b[31m    await fetch(url, {\u001b[39m\n\u001b[31m      method: 'POST', mode: 'no-cors',\u001b[39m\n\u001b[31m      headers: { 'Content-Type': 'application/json' },\u001b[39m\n\u001b[31m      body: JSON.stringify({\u001b[39m\n\u001b[31m        type: 'tracking_update',\u001b[39m\n\u001b[31m        docket, status, lastEvent, location,\u001b[39m\n\u001b[31m        timestamp: new Date().toISOString()\u001b[39m\n\u001b[31m      })\u001b[39m\n\u001b[31m    });\u001b[39m\n\u001b[31m  } catch(e) {}\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// ─── POD / RECIPIENT PHOTO UPLOAD ───────────────────────────────────────\u001b[39m\n\u001b[31m// Stores base64 locally, pushes to Sheets webhook (Apps Script saves to Drive)\u001b[39m\n\u001b[31mfunction showPODUpload(docket, dispatchId) {\u001b[39m\n\u001b[31m  const overlay = document.getElementById('podOverlay');\u001b[39m\n\u001b[31m  overlay.dataset.docket = docket;\u001b[39m\n\u001b[31m  overlay.dataset.dispatchId = dispatchId;\u001b[39m\n\u001b[31m  document.getElementById('podPreview').innerHTML = '';\u001b[39m\n\u001b[31m  document.getElementById('podFileInput').value = '';\u001b[39m\n\u001b[31m  document.getElementById('recipientFileInput').value = '';\u001b[39m\n\u001b[31m  openOverlay('podOverlay');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31masync function uploadPODFiles() {\u001b[39m\n\u001b[31m  const overlay = document.getElementById('podOverlay');\u001b[39m\n\u001b[31m  const docket = overlay.dataset.docket;\u001b[39m\n\u001b[31m  const dispatchId = overlay.dataset.dispatchId;\u001b[39m\n\u001b[31m  const podFile = document.getElementById('podFileInput').files[0];\u001b[39m\n\u001b[31m  const recipFile = document.getElementById('recipientFileInput').files[0];\u001b[39m\n\u001b[31m  if (!podFile && !recipFile) { showToast('Select at least one photo', 'err'); return; }·\u001b[39m\n\u001b[31m  showToast('Uploading...', 'info');\u001b[39m\n\u001b[31m  const toBase64 = f => new Promise((res,rej) => {\u001b[39m\n\u001b[31m    const r = new FileReader(); r.onload = e => res(e.target.result); r.onerror = rej; r.readAsDataURL(f);\u001b[39m\n\u001b[31m  });·\u001b[39m\n\u001b[31m  const payload = {\u001b[39m\n\u001b[31m    type: 'pod_upload',\u001b[39m\n\u001b[31m    docket, dispatchId,\u001b[39m\n\u001b[31m    timestamp: new Date().toISOString()\u001b[39m\n\u001b[31m  };\u001b[39m\n\u001b[31m  if (podFile)   payload.pod_image = await toBase64(podFile);\u001b[39m\n\u001b[31m  if (recipFile) payload.recipient_image = await toBase64(recipFile);·\u001b[39m\n\u001b[31m  // Update local store\u001b[39m\n\u001b[31m  for (const d of dispatches) {\u001b[39m\n\u001b[31m    if (d.id !== dispatchId) continue;\u001b[39m\n\u001b[31m    for (const c of d.consignees) {\u001b[39m\n\u001b[31m      if (c.docket === docket) {\u001b[39m\n\u001b[31m        c.podUploaded = true;\u001b[39m\n\u001b[31m        c.status = 'delivered';\u001b[39m\n\u001b[31m      }\u001b[39m\n\u001b[31m    }\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m  saveData();·\u001b[39m\n\u001b[31m  // Push to Sheets (Apps Script saves images to Drive, stores URL in sheet)\u001b[39m\n\u001b[31m  if (window.appConfig.webhook_url) {\u001b[39m\n\u001b[31m    try {\u001b[39m\n\u001b[31m      await fetch(window.appConfig.webhook_url, {\u001b[39m\n\u001b[31m        method: 'POST', mode: 'no-cors',\u001b[39m\n\u001b[31m        headers: { 'Content-Type': 'application/json' },\u001b[39m\n\u001b[31m        body: JSON.stringify(payload)\u001b[39m\n\u001b[31m      });\u001b[39m\n\u001b[31m    } catch(e) {}\u001b[39m\n\u001b[31m  }·\u001b[39m\n\u001b[31m  closeOverlay('podOverlay');\u001b[39m\n\u001b[31m  renderDashboard();\u001b[39m\n\u001b[31m  showToast('POD uploaded ✓ — marked as delivered', 'ok');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// ─── WHATSAPP DISPATCH TABLE SHARE ──────────────────────────────────────\u001b[39m\n\u001b[31mfunction shareDispatchToWhatsApp(dispatchId) {\u001b[39m\n\u001b[31m  const d = dispatches.find(x => x.id === dispatchId);\u001b[39m\n\u001b[31m  if (!d) return;\u001b[39m\n\u001b[31m  const lines = [\u001b[39m\n\u001b[31m    `📦 *Dispatch Update — ${d.client} ${d.jobref ? '› '+d.jobref : ''}*`,\u001b[39m\n\u001b[31m    `🚚 Courier: ${d.courier} | Date: ${d.dispatchDate || d.createdAt?.slice(0,10)}`,\u001b[39m\n\u001b[31m    ``,\u001b[39m\n\u001b[31m    ...d.consignees.filter(c => c.docket).map((c,i) =>\u001b[39m\n\u001b[31m      `${i+1}. *${c.name}* — ${c.city}\\\\n   Docket: ${c.docket} | Status: ${c.status||'booked'}`\u001b[39m\n\u001b[31m    ),\u001b[39m\n\u001b[31m    ``,\u001b[39m\n\u001b[31m    `_360 Degree Logical Marketing_`\u001b[39m\n\u001b[31m  ];\u001b[39m\n\u001b[31m  const msg = encodeURIComponent(lines.join('\\\\n'));\u001b[39m\n\u001b[31m  window.open('https://wa.me/?text=' + msg, '_blank');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// Trigger n8n webhook for automated WhatsApp/email share\u001b[39m\n\u001b[31masync function triggerN8nShare(dispatchId, channel) {\u001b[39m\n\u001b[31m  const d = dispatches.find(x => x.id === dispatchId);\u001b[39m\n\u001b[31m  if (!d) return;\u001b[39m\n\u001b[31m  const n8nUrl = window.appConfig.n8n_dispatch_webhook;\u001b[39m\n\u001b[31m  if (!n8nUrl) { showToast('n8n webhook not configured in Config sheet', 'err'); return; }\u001b[39m\n\u001b[31m  try {\u001b[39m\n\u001b[31m    await fetch(n8nUrl, {\u001b[39m\n\u001b[31m      method: 'POST',\u001b[39m\n\u001b[31m      headers: { 'Content-Type': 'application/json' },\u001b[39m\n\u001b[31m      body: JSON.stringify({\u001b[39m\n\u001b[31m        channel, // 'whatsapp' or 'email'\u001b[39m\n\u001b[31m        dispatch: d,\u001b[39m\n\u001b[31m        timestamp: new Date().toISOString()\u001b[39m\n\u001b[31m      })\u001b[39m\n\u001b[31m    });\u001b[39m\n\u001b[31m    showToast(`Sent to n8n for ${channel} delivery ✓`, 'ok');\u001b[39m\n\u001b[31m  } catch(e) {\u001b[39m\n\u001b[31m    showToast('n8n webhook failed', 'err');\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction previewPOD(input, type) {\u001b[39m\n\u001b[31m  const f = input.files[0];\u001b[39m\n\u001b[31m  if (!f) return;\u001b[39m\n\u001b[31m  const r = new FileReader();\u001b[39m\n\u001b[31m  r.onload = e => {\u001b[39m\n\u001b[31m    const prev = document.getElementById('podPreview');\u001b[39m\n\u001b[31m    const existing = document.getElementById('pod_prev_' + type);\u001b[39m\n\u001b[31m    if (existing) existing.remove();\u001b[39m\n\u001b[31m    const img = document.createElement('img');\u001b[39m\n\u001b[31m    img.id = 'pod_prev_' + type;\u001b[39m\n\u001b[31m    img.src = e.target.result;\u001b[39m\n\u001b[31m    img.style.cssText = 'width:80px;height:80px;object-fit:cover;border-radius:6px;border:1px solid var(--bdr);';\u001b[39m\n\u001b[31m    prev.appendChild(img);\u001b[39m\n\u001b[31m  };\u001b[39m\n\u001b[31m  r.readAsDataURL(f);\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// ─── MANUAL TRACKING ─────────────────────────────────────────────────────\u001b[39m\n\u001b[31mfunction showManualTracking(docket, dispatchId, storeName, phone, city) {\u001b[39m\n\u001b[31m  const ov = document.getElementById('manualTrackOverlay');\u001b[39m\n\u001b[31m  ov.dataset.docket = docket;\u001b[39m\n\u001b[31m  ov.dataset.dispatchId = dispatchId;\u001b[39m\n\u001b[31m  ov.dataset.storeName = storeName;\u001b[39m\n\u001b[31m  ov.dataset.city = city;·\u001b[39m\n\u001b[31m  document.getElementById('mtDocketDisplay').textContent =\u001b[39m\n\u001b[31m    docket + '  ·  ' + storeName + '  ·  ' + city;\u001b[39m\n\u001b[31m  document.getElementById('mtContactUsed').value = phone || '';\u001b[39m\n\u001b[31m  document.getElementById('mtContactPerson').value = '';\u001b[39m\n\u001b[31m  document.getElementById('mtRemarks').value = '';\u001b[39m\n\u001b[31m  document.getElementById('mtStatus').value = '';\u001b[39m\n\u001b[31m  document.getElementById('mtCalledBy').value = '';·\u001b[39m\n\u001b[31m  // Set current datetime\u001b[39m\n\u001b[31m  const now = new Date();\u001b[39m\n\u001b[31m  const pad = n => String(n).padStart(2,'0');\u001b[39m\n\u001b[31m  document.getElementById('mtCallTime').value =\u001b[39m\n\u001b[31m    `${now.getFullYear()}-${pad(now.getMonth()+1)}-${pad(now.getDate())}T${pad(now.getHours())}:${pad(now.getMinutes())}`;·\u001b[39m\n\u001b[31m  openOverlay('manualTrackOverlay');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31masync function submitManualTracking() {\u001b[39m\n\u001b[31m  const ov = document.getElementById('manualTrackOverlay');\u001b[39m\n\u001b[31m  const docket     = ov.dataset.docket;\u001b[39m\n\u001b[31m  const dispatchId = ov.dataset.dispatchId;\u001b[39m\n\u001b[31m  const status     = document.getElementById('mtStatus').value;\u001b[39m\n\u001b[31m  const calledBy   = document.getElementById('mtCalledBy').value.trim();·\u001b[39m\n\u001b[31m  if (!status)    { showToast('Select a status', 'err'); return; }\u001b[39m\n\u001b[31m  if (!calledBy)  { showToast('Enter your name', 'err'); return; }·\u001b[39m\n\u001b[31m  const payload = {\u001b[39m\n\u001b[31m    type:          'manual_tracking',\u001b[39m\n\u001b[31m    docket,        dispatchId,\u001b[39m\n\u001b[31m    storeName:     ov.dataset.storeName,\u001b[39m\n\u001b[31m    city:          ov.dataset.city,\u001b[39m\n\u001b[31m    manualStatus:  status,\u001b[39m\n\u001b[31m    calledBy,\u001b[39m\n\u001b[31m    contactUsed:   document.getElementById('mtContactUsed').value.trim(),\u001b[39m\n\u001b[31m    contactPerson: document.getElementById('mtContactPerson').value.trim(),\u001b[39m\n\u001b[31m    remarks:       document.getElementById('mtRemarks').value.trim(),\u001b[39m\n\u001b[31m    callTime:      document.getElementById('mtCallTime').value || new Date().toISOString(),\u001b[39m\n\u001b[31m    timestamp:     new Date().toISOString()\u001b[39m\n\u001b[31m  };·\u001b[39m\n\u001b[31m  // Update local dispatch record\u001b[39m\n\u001b[31m  for (const d of dispatches) {\u001b[39m\n\u001b[31m    if (d.id !== dispatchId) continue;\u001b[39m\n\u001b[31m    for (const c of d.consignees) {\u001b[39m\n\u001b[31m      if (c.docket === docket) {\u001b[39m\n\u001b[31m        c.manualStatus    = status;\u001b[39m\n\u001b[31m        c.manualCalledBy  = calledBy;\u001b[39m\n\u001b[31m        c.manualTime      = payload.callTime;\u001b[39m\n\u001b[31m        c.manualRemarks   = payload.remarks;\u001b[39m\n\u001b[31m        // If confirmed delivered — flag so dashboard shows manual badge\u001b[39m\n\u001b[31m        // (not permanently locked — use resumeAutoTracking() to re-enable)\u001b[39m\n\u001b[31m        if (status === 'delivered') {\u001b[39m\n\u001b[31m          c.status = 'delivered';\u001b[39m\n\u001b[31m          c.manualDelivered = true;\u001b[39m\n\u001b[31m        } else {\u001b[39m\n\u001b[31m          // Non-delivered manual update: clear any previous lock\u001b[39m\n\u001b[31m          c.manualDelivered = false;\u001b[39m\n\u001b[31m        }\u001b[39m\n\u001b[31m      }\u001b[39m\n\u001b[31m    }\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m  saveData();\u001b[39m\n\u001b[31m  renderDashboard();\u001b[39m\n\u001b[31m  // Sync updated consignee to Postgres\u001b[39m\n\u001b[31m  const _d = dispatches.find(x => x.id === dispatchId);\u001b[39m\n\u001b[31m  if (_d) {\u001b[39m\n\u001b[31m    const _idx = _d.consignees.findIndex(c => c.docket === docket);\u001b[39m\n\u001b[31m    if (_idx >= 0) syncConsigneeToDB(_d.consignees[_idx], dispatchId, _idx);\u001b[39m\n\u001b[31m  }·\u001b[39m\n\u001b[31m  // Log to Sheets\u001b[39m\n\u001b[31m  if (window.appConfig.webhook_url) {\u001b[39m\n\u001b[31m    try {\u001b[39m\n\u001b[31m      await fetch(window.appConfig.webhook_url, {\u001b[39m\n\u001b[31m        method: 'POST', mode: 'no-cors',\u001b[39m\n\u001b[31m        headers: { 'Content-Type': 'application/json' },\u001b[39m\n\u001b[31m        body: JSON.stringify(payload)\u001b[39m\n\u001b[31m      });\u001b[39m\n\u001b[31m    } catch(e) {}\u001b[39m\n\u001b[31m  }·\u001b[39m\n\u001b[31m  closeOverlay('manualTrackOverlay');\u001b[39m\n\u001b[31m  showToast('Manual tracking logged ✓', 'ok');\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// ─── TILE SELECTION + PARTIAL REPORT SHARE ───────────────────────────────\u001b[39m\n\u001b[31mlet _selectedDockets = new Set();·\u001b[39m\n\u001b[31mfunction toggleTileSelect(docket) {\u001b[39m\n\u001b[31m  if (_selectedDockets.has(docket)) _selectedDockets.delete(docket);\u001b[39m\n\u001b[31m  else _selectedDockets.add(docket);\u001b[39m\n\u001b[31m  // Update checkbox visual\u001b[39m\n\u001b[31m  const chk = document.getElementById('sel_' + docket.replace(/[^a-z0-9]/gi,'_'));\u001b[39m\n\u001b[31m  if (chk) chk.checked = _selectedDockets.has(docket);\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31mfunction shareSelectedReport() {\u001b[39m\n\u001b[31m  if (_selectedDockets.size === 0) {\u001b[39m\n\u001b[31m    showToast('Select at least one consignee tile first', 'err');\u001b[39m\n\u001b[31m    return;\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m  const activeDis = dispatches.filter(d => d.status !== 'draft');\u001b[39m\n\u001b[31m  const allItems  = activeDis.flatMap(d =>\u001b[39m\n\u001b[31m    (d.consignees||[]).map(c => Object.assign({}, c, {courier:d.courier, client:d.client, jobref:d.jobref, dispatchId:d.id}))\u001b[39m\n\u001b[31m  );\u001b[39m\n\u001b[31m  const selected = allItems.filter(c => _selectedDockets.has(c.docket));\u001b[39m\n\u001b[31m  if (!selected.length) { showToast('Selected dockets not found', 'err'); return; }·\u001b[39m\n\u001b[31m  const NL = '\\\\n';\u001b[39m\n\u001b[31m  const statusEmoji = {\u001b[39m\n\u001b[31m    delivered:'\\\\u2705', transit:'\\\\uD83D\\\\uDE9A', 'in-transit':'\\\\uD83D\\\\uDE9A',\u001b[39m\n\u001b[31m    booked:'\\\\uD83D\\\\uDCEC', pending:'\\\\u23F3', 'not received':'\\\\u274C', forwarded:'\\\\u27A1'\u001b[39m\n\u001b[31m  };·\u001b[39m\n\u001b[31m  const header = '\\\\uD83D\\\\uDCE6 *Dispatch Status Report*' + NL +\u001b[39m\n\u001b[31m    (selected[0].client || '') + ' ' + (selected[0].jobref ? '> ' + selected[0].jobref : '') + NL +\u001b[39m\n\u001b[31m    new Date().toLocaleDateString('en-IN', {day:'2-digit',month:'short',year:'numeric'}) + NL + NL;·\u001b[39m\n\u001b[31m  const rows = selected.map(function(c, i) {\u001b[39m\n\u001b[31m    const finalStatus = c.manualStatus === 'delivered' ? 'delivered' : (c.status || 'booked');\u001b[39m\n\u001b[31m    const emoji  = statusEmoji[finalStatus] || '\\\\uD83D\\\\uDCE6';\u001b[39m\n\u001b[31m    const source = c.manualStatus ? '(manual)' : '(auto)';\u001b[39m\n\u001b[31m    const remark = c.manualRemarks ? (NL + '   Note: ' + c.manualRemarks) : '';\u001b[39m\n\u001b[31m    return (i+1) + '. *' + c.name + '* - ' + c.city + NL +\u001b[39m\n\u001b[31m      '   ' + emoji + ' ' + finalStatus + ' ' + source + NL +\u001b[39m\n\u001b[31m      '   Docket: ' + c.docket + remark;\u001b[39m\n\u001b[31m  }).join(NL + NL);·\u001b[39m\n\u001b[31m  const footer = NL + NL + '_360 Degree Logical Marketing_';\u001b[39m\n\u001b[31m  window.open('https://wa.me/?text=' + encodeURIComponent(header + rows + footer), '_blank');\u001b[39m\n\u001b[31m  showToast('Report for ' + selected.length + ' consignees ready', 'ok');\u001b[39m\n\u001b[31m}··\u001b[39m\n\u001b[31m// ── resumeAutoTracking: undo a manual-delivered lock ──────────────────────\u001b[39m\n\u001b[31mfunction resumeAutoTracking(docket, dispatchId) {\u001b[39m\n\u001b[31m  for (const d of dispatches) {\u001b[39m\n\u001b[31m    if (d.id !== dispatchId) continue;\u001b[39m\n\u001b[31m    for (const c of d.consignees) {\u001b[39m\n\u001b[31m      if (c.docket === docket) {\u001b[39m\n\u001b[31m        c.manualDelivered = false;\u001b[39m\n\u001b[31m        c.status          = 'transit';\u001b[39m\n\u001b[31m        c.manualStatus    = '';\u001b[39m\n\u001b[31m      }\u001b[39m\n\u001b[31m    }\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m  saveData();\u001b[39m\n\u001b[31m  renderDashboard();\u001b[39m\n\u001b[31m  showToast(`Auto-tracking resumed for ${docket}`, 'ok');\u001b[39m\n\u001b[31m  const _rd = dispatches.find(x => x.id === dispatchId);\u001b[39m\n\u001b[31m  if (_rd) {\u001b[39m\n\u001b[31m    const _ri = _rd.consignees.findIndex(c => c.docket === docket);\u001b[39m\n\u001b[31m    if (_ri >= 0) syncConsigneeToDB(_rd.consignees[_ri], dispatchId, _ri);\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m// ── refreshAllTracking (consolidated, try/finally for safety) ─────────────\u001b[39m\n\u001b[31masync function refreshAllTracking() {\u001b[39m\n\u001b[31m  const activeDis  = dispatches.filter(d => d.status !== 'draft');\u001b[39m\n\u001b[31m  const allCons    = activeDis.flatMap(d =>\u001b[39m\n\u001b[31m    (d.consignees || []).map(c => ({ ...c, dispatchId: d.id, courier: d.courier }))\u001b[39m\n\u001b[31m  );\u001b[39m\n\u001b[31m  const withDockets = allCons.filter(c =>\u001b[39m\n\u001b[31m    c.docket && c.status !== 'delivered' && !c.manualDelivered\u001b[39m\n\u001b[31m  );\u001b[39m\n\u001b[31m  if (!withDockets.length) { showToast('No active dockets to refresh', 'info'); return; }·\u001b[39m\n\u001b[31m  showToast(`⏳ Tracking ${withDockets.length} dockets...`, 'info');\u001b[39m\n\u001b[31m  withDockets.forEach(c => {\u001b[39m\n\u001b[31m    const id    = c.docket.replace(/[^a-z0-9]/gi, '_');\u001b[39m\n\u001b[31m    const evtEl = document.getElementById('evt_'  + id);\u001b[39m\n\u001b[31m    const spnEl = document.getElementById('spin_' + id);\u001b[39m\n\u001b[31m    if (evtEl) evtEl.style.display = 'none';\u001b[39m\n\u001b[31m    if (spnEl) spnEl.style.display = 'inline';\u001b[39m\n\u001b[31m  });·\u001b[39m\n\u001b[31m  let done = 0;\u001b[39m\n\u001b[31m  try {\u001b[39m\n\u001b[31m    for (const c of withDockets) {\u001b[39m\n\u001b[31m      const result = await fetchTracking(c.docket, c.courier || '');\u001b[39m\n\u001b[31m      const id    = c.docket.replace(/[^a-z0-9]/gi, '_');·\u001b[39m\n\u001b[31m      // Batch-write result into state (never touch DOM here)\u001b[39m\n\u001b[31m      if (result.status && result.status !== 'failed') {\u001b[39m\n\u001b[31m        for (const d of dispatches) {\u001b[39m\n\u001b[31m          if (d.id !== c.dispatchId) continue;\u001b[39m\n\u001b[31m          for (const con of d.consignees) {\u001b[39m\n\u001b[31m            if (con.docket === c.docket && !con.manualDelivered) {\u001b[39m\n\u001b[31m              con.status           = result.status;\u001b[39m\n\u001b[31m              con.lastEvent        = result.lastEvent || con.lastEvent;\u001b[39m\n\u001b[31m              con.lastScanLocation = result.lastScanLocation || '';\u001b[39m\n\u001b[31m              con.lastTrackedAt    = new Date().toISOString();\u001b[39m\n\u001b[31m            }\u001b[39m\n\u001b[31m          }\u001b[39m\n\u001b[31m        }\u001b[39m\n\u001b[31m      }·\u001b[39m\n\u001b[31m      // Update tile spinner/event text in DOM (cosmetic only)\u001b[39m\n\u001b[31m      const spnEl = document.getElementById('spin_' + id);\u001b[39m\n\u001b[31m      const evtEl = document.getElementById('evt_'  + id);\u001b[39m\n\u001b[31m      if (spnEl) spnEl.style.display = 'none';\u001b[39m\n\u001b[31m      if (evtEl) { evtEl.style.display = 'inline'; evtEl.textContent = result.lastEvent || 'Checked'; }·\u001b[39m\n\u001b[31m      if (result.status && result.status !== 'failed' && window.appConfig.webhook_url) {\u001b[39m\n\u001b[31m        logTrackingUpdate(c.docket, result.status, result.lastEvent || '', result.lastScanLocation || '');\u001b[39m\n\u001b[31m      }\u001b[39m\n\u001b[31m      done++;\u001b[39m\n\u001b[31m    }\u001b[39m\n\u001b[31m    showToast(`✓ ${done} dockets refreshed`, 'ok');\u001b[39m\n\u001b[31m  } finally {\u001b[39m\n\u001b[31m    // Always persist and re-render — even if loop throws mid-way\u001b[39m\n\u001b[31m    saveData();\u001b[39m\n\u001b[31m    renderDashboard();\u001b[39m\n\u001b[31m    // Push updated consignee states to Postgres (fire-and-forget)\u001b[39m\n\u001b[31m    dispatches.filter(d => d.status !== 'draft').forEach(d => syncDispatchToDB(d));\u001b[39m\n\u001b[31m  }\u001b[39m\n\u001b[31m}·\u001b[39m\n\u001b[31m</script>\u001b[39m\n\u001b[31m</body>\u001b[39m\n\u001b[31m</html>\"\u001b[39m\n\n  51 |\n  52 |   test('hub auth gate present', () => {\n> 53 |     expect(html).toContain(\"window.location.replace('/hub/')\");\n     |                  ^\n  54 |     expect(html).toContain('_hubUser');\n  55 |   });\n  56 | });\n    at /var/www/360lm/tests/dispatch.spec.js:53:18"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 1,
                      "startTime": "2026-07-12T13:17:42.599Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/dispatch-Dispatch-—-HTML-Structure-hub-auth-gate-present-desktop-chrome-retry1/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/dispatch.spec.js",
                        "column": 18,
                        "line": 53
                      }
                    }
                  ],
                  "status": "unexpected"
                }
              ],
              "id": "3510226c3ce1e611dacb-779d266fdd2a744a1b06",
              "file": "dispatch.spec.js",
              "line": 52,
              "column": 3
            }
          ]
        }
      ]
    },
    {
      "title": "expense.spec.js",
      "file": "expense.spec.js",
      "column": 0,
      "line": 0,
      "specs": [],
      "suites": [
        {
          "title": "Expenses — Smoke",
          "file": "expense.spec.js",
          "line": 9,
          "column": 6,
          "specs": [
            {
              "title": "page or hub loads",
              "ok": true,
              "tags": [],
              "tests": [
                {
                  "timeout": 30000,
                  "annotations": [],
                  "expectedStatus": "passed",
                  "projectId": "android-chrome",
                  "projectName": "android-chrome",
                  "results": [
                    {
                      "workerIndex": 2,
                      "parallelIndex": 0,
                      "status": "passed",
                      "duration": 1206,
                      "errors": [],
                      "stdout": [],
                      "stderr": [],
                      "retry": 0,
                      "startTime": "2026-07-12T12:57:28.145Z",
                      "annotations": [],
                      "attachments": []
                    }
                  ],
                  "status": "expected"
                }
              ],
              "id": "2399d47327fc24358745-3bf1fb454096f73d81f8",
              "file": "expense.spec.js",
              "line": 10,
              "column": 3
            },
            {
              "title": "manifest.json returns 200",
              "ok": false,
              "tags": [],
              "tests": [
                {
                  "timeout": 30000,
                  "annotations": [],
                  "expectedStatus": "passed",
                  "projectId": "android-chrome",
                  "projectName": "android-chrome",
                  "results": [
                    {
                      "workerIndex": 2,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 244,
                      "error": {
                        "message": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoBe\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m) // Object.is equality\u001b[22m\n\nExpected: \u001b[32m200\u001b[39m\nReceived: \u001b[31m404\u001b[39m",
                        "stack": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoBe\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m) // Object.is equality\u001b[22m\n\nExpected: \u001b[32m200\u001b[39m\nReceived: \u001b[31m404\u001b[39m\n    at /var/www/360lm/tests/expense.spec.js:19:28",
                        "location": {
                          "file": "/var/www/360lm/tests/expense.spec.js",
                          "column": 28,
                          "line": 19
                        },
                        "snippet": "  17 |   test('manifest.json returns 200', async ({ page }) => {\n  18 |     const resp = await page.goto(BASE_URL + 'manifest.json');\n> 19 |     expect(resp?.status()).toBe(200);\n     |                            ^\n  20 |   });\n  21 |\n  22 |   test('sw.js is versioned', async ({ page }) => {"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/expense.spec.js",
                            "column": 28,
                            "line": 19
                          },
                          "message": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoBe\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m) // Object.is equality\u001b[22m\n\nExpected: \u001b[32m200\u001b[39m\nReceived: \u001b[31m404\u001b[39m\n\n  17 |   test('manifest.json returns 200', async ({ page }) => {\n  18 |     const resp = await page.goto(BASE_URL + 'manifest.json');\n> 19 |     expect(resp?.status()).toBe(200);\n     |                            ^\n  20 |   });\n  21 |\n  22 |   test('sw.js is versioned', async ({ page }) => {\n    at /var/www/360lm/tests/expense.spec.js:19:28"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 0,
                      "startTime": "2026-07-12T12:57:29.577Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "screenshot",
                          "contentType": "image/png",
                          "path": "/var/www/360lm/test-results/expense-Expenses-—-Smoke-manifest-json-returns-200-android-chrome/test-failed-1.png"
                        },
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/expense-Expenses-—-Smoke-manifest-json-returns-200-android-chrome/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/expense.spec.js",
                        "column": 28,
                        "line": 19
                      }
                    },
                    {
                      "workerIndex": 3,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 398,
                      "error": {
                        "message": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoBe\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m) // Object.is equality\u001b[22m\n\nExpected: \u001b[32m200\u001b[39m\nReceived: \u001b[31m404\u001b[39m",
                        "stack": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoBe\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m) // Object.is equality\u001b[22m\n\nExpected: \u001b[32m200\u001b[39m\nReceived: \u001b[31m404\u001b[39m\n    at /var/www/360lm/tests/expense.spec.js:19:28",
                        "location": {
                          "file": "/var/www/360lm/tests/expense.spec.js",
                          "column": 28,
                          "line": 19
                        },
                        "snippet": "  17 |   test('manifest.json returns 200', async ({ page }) => {\n  18 |     const resp = await page.goto(BASE_URL + 'manifest.json');\n> 19 |     expect(resp?.status()).toBe(200);\n     |                            ^\n  20 |   });\n  21 |\n  22 |   test('sw.js is versioned', async ({ page }) => {"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/expense.spec.js",
                            "column": 28,
                            "line": 19
                          },
                          "message": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoBe\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m) // Object.is equality\u001b[22m\n\nExpected: \u001b[32m200\u001b[39m\nReceived: \u001b[31m404\u001b[39m\n\n  17 |   test('manifest.json returns 200', async ({ page }) => {\n  18 |     const resp = await page.goto(BASE_URL + 'manifest.json');\n> 19 |     expect(resp?.status()).toBe(200);\n     |                            ^\n  20 |   });\n  21 |\n  22 |   test('sw.js is versioned', async ({ page }) => {\n    at /var/www/360lm/tests/expense.spec.js:19:28"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 1,
                      "startTime": "2026-07-12T12:57:30.931Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "screenshot",
                          "contentType": "image/png",
                          "path": "/var/www/360lm/test-results/expense-Expenses-—-Smoke-manifest-json-returns-200-android-chrome-retry1/test-failed-1.png"
                        },
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/expense-Expenses-—-Smoke-manifest-json-returns-200-android-chrome-retry1/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/expense.spec.js",
                        "column": 28,
                        "line": 19
                      }
                    }
                  ],
                  "status": "unexpected"
                }
              ],
              "id": "2399d47327fc24358745-d5316b6e995b7790fe42",
              "file": "expense.spec.js",
              "line": 17,
              "column": 3
            },
            {
              "title": "sw.js is versioned",
              "ok": false,
              "tags": [],
              "tests": [
                {
                  "timeout": 30000,
                  "annotations": [],
                  "expectedStatus": "passed",
                  "projectId": "android-chrome",
                  "projectName": "android-chrome",
                  "results": [
                    {
                      "workerIndex": 4,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 433,
                      "error": {
                        "message": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoMatch\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m)\u001b[22m\n\nExpected pattern: \u001b[32m/360exp-v\\d+/\u001b[39m\nReceived string:  \u001b[31m\"<!DOCTYPE HTML PUBLIC \\\"-//W3C//DTD HTML 4.01//EN\\\" \\\"http://www.w3.org/TR/html4/strict.dtd\\\">\u001b[39m\n\u001b[31m<html><head>\u001b[39m\n\u001b[31m<title>404 Not Found</title>\u001b[39m\n\u001b[31m</head><body>\u001b[39m\n\u001b[31m<h1>Not Found</h1>\u001b[39m\n\u001b[31m<p>The requested URL was not found on this server.</p>\u001b[39m\n\u001b[31m</body></html>\u001b[39m\n\u001b[31m\"\u001b[39m",
                        "stack": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoMatch\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m)\u001b[22m\n\nExpected pattern: \u001b[32m/360exp-v\\d+/\u001b[39m\nReceived string:  \u001b[31m\"<!DOCTYPE HTML PUBLIC \\\"-//W3C//DTD HTML 4.01//EN\\\" \\\"http://www.w3.org/TR/html4/strict.dtd\\\">\u001b[39m\n\u001b[31m<html><head>\u001b[39m\n\u001b[31m<title>404 Not Found</title>\u001b[39m\n\u001b[31m</head><body>\u001b[39m\n\u001b[31m<h1>Not Found</h1>\u001b[39m\n\u001b[31m<p>The requested URL was not found on this server.</p>\u001b[39m\n\u001b[31m</body></html>\u001b[39m\n\u001b[31m\"\u001b[39m\n    at /var/www/360lm/tests/expense.spec.js:25:18",
                        "location": {
                          "file": "/var/www/360lm/tests/expense.spec.js",
                          "column": 18,
                          "line": 25
                        },
                        "snippet": "  23 |     const resp = await page.goto(BASE_URL + 'sw.js');\n  24 |     const text = await resp?.text() || '';\n> 25 |     expect(text).toMatch(/360exp-v\\d+/);\n     |                  ^\n  26 |   });\n  27 | });\n  28 |"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/expense.spec.js",
                            "column": 18,
                            "line": 25
                          },
                          "message": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoMatch\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m)\u001b[22m\n\nExpected pattern: \u001b[32m/360exp-v\\d+/\u001b[39m\nReceived string:  \u001b[31m\"<!DOCTYPE HTML PUBLIC \\\"-//W3C//DTD HTML 4.01//EN\\\" \\\"http://www.w3.org/TR/html4/strict.dtd\\\">\u001b[39m\n\u001b[31m<html><head>\u001b[39m\n\u001b[31m<title>404 Not Found</title>\u001b[39m\n\u001b[31m</head><body>\u001b[39m\n\u001b[31m<h1>Not Found</h1>\u001b[39m\n\u001b[31m<p>The requested URL was not found on this server.</p>\u001b[39m\n\u001b[31m</body></html>\u001b[39m\n\u001b[31m\"\u001b[39m\n\n  23 |     const resp = await page.goto(BASE_URL + 'sw.js');\n  24 |     const text = await resp?.text() || '';\n> 25 |     expect(text).toMatch(/360exp-v\\d+/);\n     |                  ^\n  26 |   });\n  27 | });\n  28 |\n    at /var/www/360lm/tests/expense.spec.js:25:18"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 0,
                      "startTime": "2026-07-12T12:57:32.830Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "screenshot",
                          "contentType": "image/png",
                          "path": "/var/www/360lm/test-results/expense-Expenses-—-Smoke-sw-js-is-versioned-android-chrome/test-failed-1.png"
                        },
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/expense-Expenses-—-Smoke-sw-js-is-versioned-android-chrome/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/expense.spec.js",
                        "column": 18,
                        "line": 25
                      }
                    },
                    {
                      "workerIndex": 5,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 464,
                      "error": {
                        "message": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoMatch\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m)\u001b[22m\n\nExpected pattern: \u001b[32m/360exp-v\\d+/\u001b[39m\nReceived string:  \u001b[31m\"<!DOCTYPE HTML PUBLIC \\\"-//W3C//DTD HTML 4.01//EN\\\" \\\"http://www.w3.org/TR/html4/strict.dtd\\\">\u001b[39m\n\u001b[31m<html><head>\u001b[39m\n\u001b[31m<title>404 Not Found</title>\u001b[39m\n\u001b[31m</head><body>\u001b[39m\n\u001b[31m<h1>Not Found</h1>\u001b[39m\n\u001b[31m<p>The requested URL was not found on this server.</p>\u001b[39m\n\u001b[31m</body></html>\u001b[39m\n\u001b[31m\"\u001b[39m",
                        "stack": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoMatch\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m)\u001b[22m\n\nExpected pattern: \u001b[32m/360exp-v\\d+/\u001b[39m\nReceived string:  \u001b[31m\"<!DOCTYPE HTML PUBLIC \\\"-//W3C//DTD HTML 4.01//EN\\\" \\\"http://www.w3.org/TR/html4/strict.dtd\\\">\u001b[39m\n\u001b[31m<html><head>\u001b[39m\n\u001b[31m<title>404 Not Found</title>\u001b[39m\n\u001b[31m</head><body>\u001b[39m\n\u001b[31m<h1>Not Found</h1>\u001b[39m\n\u001b[31m<p>The requested URL was not found on this server.</p>\u001b[39m\n\u001b[31m</body></html>\u001b[39m\n\u001b[31m\"\u001b[39m\n    at /var/www/360lm/tests/expense.spec.js:25:18",
                        "location": {
                          "file": "/var/www/360lm/tests/expense.spec.js",
                          "column": 18,
                          "line": 25
                        },
                        "snippet": "  23 |     const resp = await page.goto(BASE_URL + 'sw.js');\n  24 |     const text = await resp?.text() || '';\n> 25 |     expect(text).toMatch(/360exp-v\\d+/);\n     |                  ^\n  26 |   });\n  27 | });\n  28 |"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/expense.spec.js",
                            "column": 18,
                            "line": 25
                          },
                          "message": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoMatch\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m)\u001b[22m\n\nExpected pattern: \u001b[32m/360exp-v\\d+/\u001b[39m\nReceived string:  \u001b[31m\"<!DOCTYPE HTML PUBLIC \\\"-//W3C//DTD HTML 4.01//EN\\\" \\\"http://www.w3.org/TR/html4/strict.dtd\\\">\u001b[39m\n\u001b[31m<html><head>\u001b[39m\n\u001b[31m<title>404 Not Found</title>\u001b[39m\n\u001b[31m</head><body>\u001b[39m\n\u001b[31m<h1>Not Found</h1>\u001b[39m\n\u001b[31m<p>The requested URL was not found on this server.</p>\u001b[39m\n\u001b[31m</body></html>\u001b[39m\n\u001b[31m\"\u001b[39m\n\n  23 |     const resp = await page.goto(BASE_URL + 'sw.js');\n  24 |     const text = await resp?.text() || '';\n> 25 |     expect(text).toMatch(/360exp-v\\d+/);\n     |                  ^\n  26 |   });\n  27 | });\n  28 |\n    at /var/www/360lm/tests/expense.spec.js:25:18"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 1,
                      "startTime": "2026-07-12T12:57:34.440Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "screenshot",
                          "contentType": "image/png",
                          "path": "/var/www/360lm/test-results/expense-Expenses-—-Smoke-sw-js-is-versioned-android-chrome-retry1/test-failed-1.png"
                        },
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/expense-Expenses-—-Smoke-sw-js-is-versioned-android-chrome-retry1/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/expense.spec.js",
                        "column": 18,
                        "line": 25
                      }
                    }
                  ],
                  "status": "unexpected"
                }
              ],
              "id": "2399d47327fc24358745-22d3fda65423125f291f",
              "file": "expense.spec.js",
              "line": 22,
              "column": 3
            },
            {
              "title": "page or hub loads",
              "ok": true,
              "tags": [],
              "tests": [
                {
                  "timeout": 30000,
                  "annotations": [],
                  "expectedStatus": "passed",
                  "projectId": "desktop-chrome",
                  "projectName": "desktop-chrome",
                  "results": [
                    {
                      "workerIndex": 178,
                      "parallelIndex": 0,
                      "status": "passed",
                      "duration": 1102,
                      "errors": [],
                      "stdout": [],
                      "stderr": [],
                      "retry": 0,
                      "startTime": "2026-07-12T13:17:43.604Z",
                      "annotations": [],
                      "attachments": []
                    }
                  ],
                  "status": "expected"
                }
              ],
              "id": "2399d47327fc24358745-7d1f2eea2388ccca6d02",
              "file": "expense.spec.js",
              "line": 10,
              "column": 3
            },
            {
              "title": "manifest.json returns 200",
              "ok": false,
              "tags": [],
              "tests": [
                {
                  "timeout": 30000,
                  "annotations": [],
                  "expectedStatus": "passed",
                  "projectId": "desktop-chrome",
                  "projectName": "desktop-chrome",
                  "results": [
                    {
                      "workerIndex": 178,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 244,
                      "error": {
                        "message": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoBe\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m) // Object.is equality\u001b[22m\n\nExpected: \u001b[32m200\u001b[39m\nReceived: \u001b[31m404\u001b[39m",
                        "stack": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoBe\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m) // Object.is equality\u001b[22m\n\nExpected: \u001b[32m200\u001b[39m\nReceived: \u001b[31m404\u001b[39m\n    at /var/www/360lm/tests/expense.spec.js:19:28",
                        "location": {
                          "file": "/var/www/360lm/tests/expense.spec.js",
                          "column": 28,
                          "line": 19
                        },
                        "snippet": "  17 |   test('manifest.json returns 200', async ({ page }) => {\n  18 |     const resp = await page.goto(BASE_URL + 'manifest.json');\n> 19 |     expect(resp?.status()).toBe(200);\n     |                            ^\n  20 |   });\n  21 |\n  22 |   test('sw.js is versioned', async ({ page }) => {"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/expense.spec.js",
                            "column": 28,
                            "line": 19
                          },
                          "message": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoBe\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m) // Object.is equality\u001b[22m\n\nExpected: \u001b[32m200\u001b[39m\nReceived: \u001b[31m404\u001b[39m\n\n  17 |   test('manifest.json returns 200', async ({ page }) => {\n  18 |     const resp = await page.goto(BASE_URL + 'manifest.json');\n> 19 |     expect(resp?.status()).toBe(200);\n     |                            ^\n  20 |   });\n  21 |\n  22 |   test('sw.js is versioned', async ({ page }) => {\n    at /var/www/360lm/tests/expense.spec.js:19:28"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 0,
                      "startTime": "2026-07-12T13:17:44.884Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "screenshot",
                          "contentType": "image/png",
                          "path": "/var/www/360lm/test-results/expense-Expenses-—-Smoke-manifest-json-returns-200-desktop-chrome/test-failed-1.png"
                        },
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/expense-Expenses-—-Smoke-manifest-json-returns-200-desktop-chrome/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/expense.spec.js",
                        "column": 28,
                        "line": 19
                      }
                    },
                    {
                      "workerIndex": 179,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 293,
                      "error": {
                        "message": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoBe\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m) // Object.is equality\u001b[22m\n\nExpected: \u001b[32m200\u001b[39m\nReceived: \u001b[31m404\u001b[39m",
                        "stack": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoBe\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m) // Object.is equality\u001b[22m\n\nExpected: \u001b[32m200\u001b[39m\nReceived: \u001b[31m404\u001b[39m\n    at /var/www/360lm/tests/expense.spec.js:19:28",
                        "location": {
                          "file": "/var/www/360lm/tests/expense.spec.js",
                          "column": 28,
                          "line": 19
                        },
                        "snippet": "  17 |   test('manifest.json returns 200', async ({ page }) => {\n  18 |     const resp = await page.goto(BASE_URL + 'manifest.json');\n> 19 |     expect(resp?.status()).toBe(200);\n     |                            ^\n  20 |   });\n  21 |\n  22 |   test('sw.js is versioned', async ({ page }) => {"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/expense.spec.js",
                            "column": 28,
                            "line": 19
                          },
                          "message": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoBe\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m) // Object.is equality\u001b[22m\n\nExpected: \u001b[32m200\u001b[39m\nReceived: \u001b[31m404\u001b[39m\n\n  17 |   test('manifest.json returns 200', async ({ page }) => {\n  18 |     const resp = await page.goto(BASE_URL + 'manifest.json');\n> 19 |     expect(resp?.status()).toBe(200);\n     |                            ^\n  20 |   });\n  21 |\n  22 |   test('sw.js is versioned', async ({ page }) => {\n    at /var/www/360lm/tests/expense.spec.js:19:28"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 1,
                      "startTime": "2026-07-12T13:17:45.973Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "screenshot",
                          "contentType": "image/png",
                          "path": "/var/www/360lm/test-results/expense-Expenses-—-Smoke-manifest-json-returns-200-desktop-chrome-retry1/test-failed-1.png"
                        },
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/expense-Expenses-—-Smoke-manifest-json-returns-200-desktop-chrome-retry1/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/expense.spec.js",
                        "column": 28,
                        "line": 19
                      }
                    }
                  ],
                  "status": "unexpected"
                }
              ],
              "id": "2399d47327fc24358745-1da934617a4d1cda8788",
              "file": "expense.spec.js",
              "line": 17,
              "column": 3
            },
            {
              "title": "sw.js is versioned",
              "ok": false,
              "tags": [],
              "tests": [
                {
                  "timeout": 30000,
                  "annotations": [],
                  "expectedStatus": "passed",
                  "projectId": "desktop-chrome",
                  "projectName": "desktop-chrome",
                  "results": [
                    {
                      "workerIndex": 180,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 582,
                      "error": {
                        "message": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoMatch\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m)\u001b[22m\n\nExpected pattern: \u001b[32m/360exp-v\\d+/\u001b[39m\nReceived string:  \u001b[31m\"<!DOCTYPE HTML PUBLIC \\\"-//W3C//DTD HTML 4.01//EN\\\" \\\"http://www.w3.org/TR/html4/strict.dtd\\\">\u001b[39m\n\u001b[31m<html><head>\u001b[39m\n\u001b[31m<title>404 Not Found</title>\u001b[39m\n\u001b[31m</head><body>\u001b[39m\n\u001b[31m<h1>Not Found</h1>\u001b[39m\n\u001b[31m<p>The requested URL was not found on this server.</p>\u001b[39m\n\u001b[31m</body></html>\u001b[39m\n\u001b[31m\"\u001b[39m",
                        "stack": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoMatch\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m)\u001b[22m\n\nExpected pattern: \u001b[32m/360exp-v\\d+/\u001b[39m\nReceived string:  \u001b[31m\"<!DOCTYPE HTML PUBLIC \\\"-//W3C//DTD HTML 4.01//EN\\\" \\\"http://www.w3.org/TR/html4/strict.dtd\\\">\u001b[39m\n\u001b[31m<html><head>\u001b[39m\n\u001b[31m<title>404 Not Found</title>\u001b[39m\n\u001b[31m</head><body>\u001b[39m\n\u001b[31m<h1>Not Found</h1>\u001b[39m\n\u001b[31m<p>The requested URL was not found on this server.</p>\u001b[39m\n\u001b[31m</body></html>\u001b[39m\n\u001b[31m\"\u001b[39m\n    at /var/www/360lm/tests/expense.spec.js:25:18",
                        "location": {
                          "file": "/var/www/360lm/tests/expense.spec.js",
                          "column": 18,
                          "line": 25
                        },
                        "snippet": "  23 |     const resp = await page.goto(BASE_URL + 'sw.js');\n  24 |     const text = await resp?.text() || '';\n> 25 |     expect(text).toMatch(/360exp-v\\d+/);\n     |                  ^\n  26 |   });\n  27 | });\n  28 |"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/expense.spec.js",
                            "column": 18,
                            "line": 25
                          },
                          "message": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoMatch\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m)\u001b[22m\n\nExpected pattern: \u001b[32m/360exp-v\\d+/\u001b[39m\nReceived string:  \u001b[31m\"<!DOCTYPE HTML PUBLIC \\\"-//W3C//DTD HTML 4.01//EN\\\" \\\"http://www.w3.org/TR/html4/strict.dtd\\\">\u001b[39m\n\u001b[31m<html><head>\u001b[39m\n\u001b[31m<title>404 Not Found</title>\u001b[39m\n\u001b[31m</head><body>\u001b[39m\n\u001b[31m<h1>Not Found</h1>\u001b[39m\n\u001b[31m<p>The requested URL was not found on this server.</p>\u001b[39m\n\u001b[31m</body></html>\u001b[39m\n\u001b[31m\"\u001b[39m\n\n  23 |     const resp = await page.goto(BASE_URL + 'sw.js');\n  24 |     const text = await resp?.text() || '';\n> 25 |     expect(text).toMatch(/360exp-v\\d+/);\n     |                  ^\n  26 |   });\n  27 | });\n  28 |\n    at /var/www/360lm/tests/expense.spec.js:25:18"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 0,
                      "startTime": "2026-07-12T13:17:47.280Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "screenshot",
                          "contentType": "image/png",
                          "path": "/var/www/360lm/test-results/expense-Expenses-—-Smoke-sw-js-is-versioned-desktop-chrome/test-failed-1.png"
                        },
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/expense-Expenses-—-Smoke-sw-js-is-versioned-desktop-chrome/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/expense.spec.js",
                        "column": 18,
                        "line": 25
                      }
                    },
                    {
                      "workerIndex": 181,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 363,
                      "error": {
                        "message": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoMatch\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m)\u001b[22m\n\nExpected pattern: \u001b[32m/360exp-v\\d+/\u001b[39m\nReceived string:  \u001b[31m\"<!DOCTYPE HTML PUBLIC \\\"-//W3C//DTD HTML 4.01//EN\\\" \\\"http://www.w3.org/TR/html4/strict.dtd\\\">\u001b[39m\n\u001b[31m<html><head>\u001b[39m\n\u001b[31m<title>404 Not Found</title>\u001b[39m\n\u001b[31m</head><body>\u001b[39m\n\u001b[31m<h1>Not Found</h1>\u001b[39m\n\u001b[31m<p>The requested URL was not found on this server.</p>\u001b[39m\n\u001b[31m</body></html>\u001b[39m\n\u001b[31m\"\u001b[39m",
                        "stack": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoMatch\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m)\u001b[22m\n\nExpected pattern: \u001b[32m/360exp-v\\d+/\u001b[39m\nReceived string:  \u001b[31m\"<!DOCTYPE HTML PUBLIC \\\"-//W3C//DTD HTML 4.01//EN\\\" \\\"http://www.w3.org/TR/html4/strict.dtd\\\">\u001b[39m\n\u001b[31m<html><head>\u001b[39m\n\u001b[31m<title>404 Not Found</title>\u001b[39m\n\u001b[31m</head><body>\u001b[39m\n\u001b[31m<h1>Not Found</h1>\u001b[39m\n\u001b[31m<p>The requested URL was not found on this server.</p>\u001b[39m\n\u001b[31m</body></html>\u001b[39m\n\u001b[31m\"\u001b[39m\n    at /var/www/360lm/tests/expense.spec.js:25:18",
                        "location": {
                          "file": "/var/www/360lm/tests/expense.spec.js",
                          "column": 18,
                          "line": 25
                        },
                        "snippet": "  23 |     const resp = await page.goto(BASE_URL + 'sw.js');\n  24 |     const text = await resp?.text() || '';\n> 25 |     expect(text).toMatch(/360exp-v\\d+/);\n     |                  ^\n  26 |   });\n  27 | });\n  28 |"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/expense.spec.js",
                            "column": 18,
                            "line": 25
                          },
                          "message": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoMatch\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m)\u001b[22m\n\nExpected pattern: \u001b[32m/360exp-v\\d+/\u001b[39m\nReceived string:  \u001b[31m\"<!DOCTYPE HTML PUBLIC \\\"-//W3C//DTD HTML 4.01//EN\\\" \\\"http://www.w3.org/TR/html4/strict.dtd\\\">\u001b[39m\n\u001b[31m<html><head>\u001b[39m\n\u001b[31m<title>404 Not Found</title>\u001b[39m\n\u001b[31m</head><body>\u001b[39m\n\u001b[31m<h1>Not Found</h1>\u001b[39m\n\u001b[31m<p>The requested URL was not found on this server.</p>\u001b[39m\n\u001b[31m</body></html>\u001b[39m\n\u001b[31m\"\u001b[39m\n\n  23 |     const resp = await page.goto(BASE_URL + 'sw.js');\n  24 |     const text = await resp?.text() || '';\n> 25 |     expect(text).toMatch(/360exp-v\\d+/);\n     |                  ^\n  26 |   });\n  27 | });\n  28 |\n    at /var/www/360lm/tests/expense.spec.js:25:18"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 1,
                      "startTime": "2026-07-12T13:17:49.143Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "screenshot",
                          "contentType": "image/png",
                          "path": "/var/www/360lm/test-results/expense-Expenses-—-Smoke-sw-js-is-versioned-desktop-chrome-retry1/test-failed-1.png"
                        },
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/expense-Expenses-—-Smoke-sw-js-is-versioned-desktop-chrome-retry1/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/expense.spec.js",
                        "column": 18,
                        "line": 25
                      }
                    }
                  ],
                  "status": "unexpected"
                }
              ],
              "id": "2399d47327fc24358745-d7fecb204bf6e208d449",
              "file": "expense.spec.js",
              "line": 22,
              "column": 3
            }
          ]
        },
        {
          "title": "Expenses — HTML Structure",
          "file": "expense.spec.js",
          "line": 29,
          "column": 6,
          "specs": [
            {
              "title": "all screens present",
              "ok": false,
              "tags": [],
              "tests": [
                {
                  "timeout": 30000,
                  "annotations": [],
                  "expectedStatus": "passed",
                  "projectId": "android-chrome",
                  "projectName": "android-chrome",
                  "results": [
                    {
                      "workerIndex": 6,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 12,
                      "error": {
                        "message": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoContain\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m) // indexOf\u001b[22m\n\nExpected substring: \u001b[32m\"id=\\\"screen-login\\\"\"\u001b[39m\nReceived string:    \u001b[31m\"<!DOCTYPE HTML PUBLIC \\\"-//W3C//DTD HTML 4.01//EN\\\" \\\"http://www.w3.org/TR/html4/strict.dtd\\\">\u001b[39m\n\u001b[31m<html><head>\u001b[39m\n\u001b[31m<title>404 Not Found</title>\u001b[39m\n\u001b[31m</head><body>\u001b[39m\n\u001b[31m<h1>Not Found</h1>\u001b[39m\n\u001b[31m<p>The requested URL was not found on this server.</p>\u001b[39m\n\u001b[31m</body></html>\u001b[39m\n\u001b[31m\"\u001b[39m",
                        "stack": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoContain\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m) // indexOf\u001b[22m\n\nExpected substring: \u001b[32m\"id=\\\"screen-login\\\"\"\u001b[39m\nReceived string:    \u001b[31m\"<!DOCTYPE HTML PUBLIC \\\"-//W3C//DTD HTML 4.01//EN\\\" \\\"http://www.w3.org/TR/html4/strict.dtd\\\">\u001b[39m\n\u001b[31m<html><head>\u001b[39m\n\u001b[31m<title>404 Not Found</title>\u001b[39m\n\u001b[31m</head><body>\u001b[39m\n\u001b[31m<h1>Not Found</h1>\u001b[39m\n\u001b[31m<p>The requested URL was not found on this server.</p>\u001b[39m\n\u001b[31m</body></html>\u001b[39m\n\u001b[31m\"\u001b[39m\n    at /var/www/360lm/tests/expense.spec.js:40:18",
                        "location": {
                          "file": "/var/www/360lm/tests/expense.spec.js",
                          "column": 18,
                          "line": 40
                        },
                        "snippet": "  38 |\n  39 |   test('all screens present', () => {\n> 40 |     expect(html).toContain('id=\"screen-login\"');\n     |                  ^\n  41 |     expect(html).toContain('id=\"screen-dash\"');\n  42 |     expect(html).toContain('id=\"screen-capture\"');\n  43 |     expect(html).toContain('id=\"screen-actions\"');"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/expense.spec.js",
                            "column": 18,
                            "line": 40
                          },
                          "message": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoContain\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m) // indexOf\u001b[22m\n\nExpected substring: \u001b[32m\"id=\\\"screen-login\\\"\"\u001b[39m\nReceived string:    \u001b[31m\"<!DOCTYPE HTML PUBLIC \\\"-//W3C//DTD HTML 4.01//EN\\\" \\\"http://www.w3.org/TR/html4/strict.dtd\\\">\u001b[39m\n\u001b[31m<html><head>\u001b[39m\n\u001b[31m<title>404 Not Found</title>\u001b[39m\n\u001b[31m</head><body>\u001b[39m\n\u001b[31m<h1>Not Found</h1>\u001b[39m\n\u001b[31m<p>The requested URL was not found on this server.</p>\u001b[39m\n\u001b[31m</body></html>\u001b[39m\n\u001b[31m\"\u001b[39m\n\n  38 |\n  39 |   test('all screens present', () => {\n> 40 |     expect(html).toContain('id=\"screen-login\"');\n     |                  ^\n  41 |     expect(html).toContain('id=\"screen-dash\"');\n  42 |     expect(html).toContain('id=\"screen-capture\"');\n  43 |     expect(html).toContain('id=\"screen-actions\"');\n    at /var/www/360lm/tests/expense.spec.js:40:18"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 0,
                      "startTime": "2026-07-12T12:57:36.003Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/expense-Expenses-—-HTML-Structure-all-screens-present-android-chrome/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/expense.spec.js",
                        "column": 18,
                        "line": 40
                      }
                    },
                    {
                      "workerIndex": 7,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 11,
                      "error": {
                        "message": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoContain\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m) // indexOf\u001b[22m\n\nExpected substring: \u001b[32m\"id=\\\"screen-login\\\"\"\u001b[39m\nReceived string:    \u001b[31m\"<!DOCTYPE HTML PUBLIC \\\"-//W3C//DTD HTML 4.01//EN\\\" \\\"http://www.w3.org/TR/html4/strict.dtd\\\">\u001b[39m\n\u001b[31m<html><head>\u001b[39m\n\u001b[31m<title>404 Not Found</title>\u001b[39m\n\u001b[31m</head><body>\u001b[39m\n\u001b[31m<h1>Not Found</h1>\u001b[39m\n\u001b[31m<p>The requested URL was not found on this server.</p>\u001b[39m\n\u001b[31m</body></html>\u001b[39m\n\u001b[31m\"\u001b[39m",
                        "stack": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoContain\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m) // indexOf\u001b[22m\n\nExpected substring: \u001b[32m\"id=\\\"screen-login\\\"\"\u001b[39m\nReceived string:    \u001b[31m\"<!DOCTYPE HTML PUBLIC \\\"-//W3C//DTD HTML 4.01//EN\\\" \\\"http://www.w3.org/TR/html4/strict.dtd\\\">\u001b[39m\n\u001b[31m<html><head>\u001b[39m\n\u001b[31m<title>404 Not Found</title>\u001b[39m\n\u001b[31m</head><body>\u001b[39m\n\u001b[31m<h1>Not Found</h1>\u001b[39m\n\u001b[31m<p>The requested URL was not found on this server.</p>\u001b[39m\n\u001b[31m</body></html>\u001b[39m\n\u001b[31m\"\u001b[39m\n    at /var/www/360lm/tests/expense.spec.js:40:18",
                        "location": {
                          "file": "/var/www/360lm/tests/expense.spec.js",
                          "column": 18,
                          "line": 40
                        },
                        "snippet": "  38 |\n  39 |   test('all screens present', () => {\n> 40 |     expect(html).toContain('id=\"screen-login\"');\n     |                  ^\n  41 |     expect(html).toContain('id=\"screen-dash\"');\n  42 |     expect(html).toContain('id=\"screen-capture\"');\n  43 |     expect(html).toContain('id=\"screen-actions\"');"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/expense.spec.js",
                            "column": 18,
                            "line": 40
                          },
                          "message": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoContain\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m) // indexOf\u001b[22m\n\nExpected substring: \u001b[32m\"id=\\\"screen-login\\\"\"\u001b[39m\nReceived string:    \u001b[31m\"<!DOCTYPE HTML PUBLIC \\\"-//W3C//DTD HTML 4.01//EN\\\" \\\"http://www.w3.org/TR/html4/strict.dtd\\\">\u001b[39m\n\u001b[31m<html><head>\u001b[39m\n\u001b[31m<title>404 Not Found</title>\u001b[39m\n\u001b[31m</head><body>\u001b[39m\n\u001b[31m<h1>Not Found</h1>\u001b[39m\n\u001b[31m<p>The requested URL was not found on this server.</p>\u001b[39m\n\u001b[31m</body></html>\u001b[39m\n\u001b[31m\"\u001b[39m\n\n  38 |\n  39 |   test('all screens present', () => {\n> 40 |     expect(html).toContain('id=\"screen-login\"');\n     |                  ^\n  41 |     expect(html).toContain('id=\"screen-dash\"');\n  42 |     expect(html).toContain('id=\"screen-capture\"');\n  43 |     expect(html).toContain('id=\"screen-actions\"');\n    at /var/www/360lm/tests/expense.spec.js:40:18"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 1,
                      "startTime": "2026-07-12T12:57:37.103Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/expense-Expenses-—-HTML-Structure-all-screens-present-android-chrome-retry1/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/expense.spec.js",
                        "column": 18,
                        "line": 40
                      }
                    }
                  ],
                  "status": "unexpected"
                }
              ],
              "id": "2399d47327fc24358745-cf961a4a22ce7b4adf00",
              "file": "expense.spec.js",
              "line": 39,
              "column": 3
            },
            {
              "title": "key functions present in source",
              "ok": false,
              "tags": [],
              "tests": [
                {
                  "timeout": 30000,
                  "annotations": [],
                  "expectedStatus": "passed",
                  "projectId": "android-chrome",
                  "projectName": "android-chrome",
                  "results": [
                    {
                      "workerIndex": 8,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 15,
                      "error": {
                        "message": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoContain\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m) // indexOf\u001b[22m\n\nExpected substring: \u001b[32m\"function goToHub(\"\u001b[39m\nReceived string:    \u001b[31m\"<!DOCTYPE HTML PUBLIC \\\"-//W3C//DTD HTML 4.01//EN\\\" \\\"http://www.w3.org/TR/html4/strict.dtd\\\">\u001b[39m\n\u001b[31m<html><head>\u001b[39m\n\u001b[31m<title>404 Not Found</title>\u001b[39m\n\u001b[31m</head><body>\u001b[39m\n\u001b[31m<h1>Not Found</h1>\u001b[39m\n\u001b[31m<p>The requested URL was not found on this server.</p>\u001b[39m\n\u001b[31m</body></html>\u001b[39m\n\u001b[31m\"\u001b[39m",
                        "stack": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoContain\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m) // indexOf\u001b[22m\n\nExpected substring: \u001b[32m\"function goToHub(\"\u001b[39m\nReceived string:    \u001b[31m\"<!DOCTYPE HTML PUBLIC \\\"-//W3C//DTD HTML 4.01//EN\\\" \\\"http://www.w3.org/TR/html4/strict.dtd\\\">\u001b[39m\n\u001b[31m<html><head>\u001b[39m\n\u001b[31m<title>404 Not Found</title>\u001b[39m\n\u001b[31m</head><body>\u001b[39m\n\u001b[31m<h1>Not Found</h1>\u001b[39m\n\u001b[31m<p>The requested URL was not found on this server.</p>\u001b[39m\n\u001b[31m</body></html>\u001b[39m\n\u001b[31m\"\u001b[39m\n    at /var/www/360lm/tests/expense.spec.js:51:18",
                        "location": {
                          "file": "/var/www/360lm/tests/expense.spec.js",
                          "column": 18,
                          "line": 51
                        },
                        "snippet": "  49 |\n  50 |   test('key functions present in source', () => {\n> 51 |     expect(html).toContain('function goToHub(');\n     |                  ^\n  52 |     expect(html).toContain('function submitSheet(');\n  53 |     expect(html).toContain('function loadApprovals(');\n  54 |     expect(html).toContain('function openCaptureOptsModal(');"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/expense.spec.js",
                            "column": 18,
                            "line": 51
                          },
                          "message": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoContain\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m) // indexOf\u001b[22m\n\nExpected substring: \u001b[32m\"function goToHub(\"\u001b[39m\nReceived string:    \u001b[31m\"<!DOCTYPE HTML PUBLIC \\\"-//W3C//DTD HTML 4.01//EN\\\" \\\"http://www.w3.org/TR/html4/strict.dtd\\\">\u001b[39m\n\u001b[31m<html><head>\u001b[39m\n\u001b[31m<title>404 Not Found</title>\u001b[39m\n\u001b[31m</head><body>\u001b[39m\n\u001b[31m<h1>Not Found</h1>\u001b[39m\n\u001b[31m<p>The requested URL was not found on this server.</p>\u001b[39m\n\u001b[31m</body></html>\u001b[39m\n\u001b[31m\"\u001b[39m\n\n  49 |\n  50 |   test('key functions present in source', () => {\n> 51 |     expect(html).toContain('function goToHub(');\n     |                  ^\n  52 |     expect(html).toContain('function submitSheet(');\n  53 |     expect(html).toContain('function loadApprovals(');\n  54 |     expect(html).toContain('function openCaptureOptsModal(');\n    at /var/www/360lm/tests/expense.spec.js:51:18"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 0,
                      "startTime": "2026-07-12T12:57:38.240Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/expense-Expenses-—-HTML-St-a9fb7-functions-present-in-source-android-chrome/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/expense.spec.js",
                        "column": 18,
                        "line": 51
                      }
                    },
                    {
                      "workerIndex": 9,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 14,
                      "error": {
                        "message": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoContain\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m) // indexOf\u001b[22m\n\nExpected substring: \u001b[32m\"function goToHub(\"\u001b[39m\nReceived string:    \u001b[31m\"<!DOCTYPE HTML PUBLIC \\\"-//W3C//DTD HTML 4.01//EN\\\" \\\"http://www.w3.org/TR/html4/strict.dtd\\\">\u001b[39m\n\u001b[31m<html><head>\u001b[39m\n\u001b[31m<title>404 Not Found</title>\u001b[39m\n\u001b[31m</head><body>\u001b[39m\n\u001b[31m<h1>Not Found</h1>\u001b[39m\n\u001b[31m<p>The requested URL was not found on this server.</p>\u001b[39m\n\u001b[31m</body></html>\u001b[39m\n\u001b[31m\"\u001b[39m",
                        "stack": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoContain\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m) // indexOf\u001b[22m\n\nExpected substring: \u001b[32m\"function goToHub(\"\u001b[39m\nReceived string:    \u001b[31m\"<!DOCTYPE HTML PUBLIC \\\"-//W3C//DTD HTML 4.01//EN\\\" \\\"http://www.w3.org/TR/html4/strict.dtd\\\">\u001b[39m\n\u001b[31m<html><head>\u001b[39m\n\u001b[31m<title>404 Not Found</title>\u001b[39m\n\u001b[31m</head><body>\u001b[39m\n\u001b[31m<h1>Not Found</h1>\u001b[39m\n\u001b[31m<p>The requested URL was not found on this server.</p>\u001b[39m\n\u001b[31m</body></html>\u001b[39m\n\u001b[31m\"\u001b[39m\n    at /var/www/360lm/tests/expense.spec.js:51:18",
                        "location": {
                          "file": "/var/www/360lm/tests/expense.spec.js",
                          "column": 18,
                          "line": 51
                        },
                        "snippet": "  49 |\n  50 |   test('key functions present in source', () => {\n> 51 |     expect(html).toContain('function goToHub(');\n     |                  ^\n  52 |     expect(html).toContain('function submitSheet(');\n  53 |     expect(html).toContain('function loadApprovals(');\n  54 |     expect(html).toContain('function openCaptureOptsModal(');"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/expense.spec.js",
                            "column": 18,
                            "line": 51
                          },
                          "message": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoContain\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m) // indexOf\u001b[22m\n\nExpected substring: \u001b[32m\"function goToHub(\"\u001b[39m\nReceived string:    \u001b[31m\"<!DOCTYPE HTML PUBLIC \\\"-//W3C//DTD HTML 4.01//EN\\\" \\\"http://www.w3.org/TR/html4/strict.dtd\\\">\u001b[39m\n\u001b[31m<html><head>\u001b[39m\n\u001b[31m<title>404 Not Found</title>\u001b[39m\n\u001b[31m</head><body>\u001b[39m\n\u001b[31m<h1>Not Found</h1>\u001b[39m\n\u001b[31m<p>The requested URL was not found on this server.</p>\u001b[39m\n\u001b[31m</body></html>\u001b[39m\n\u001b[31m\"\u001b[39m\n\n  49 |\n  50 |   test('key functions present in source', () => {\n> 51 |     expect(html).toContain('function goToHub(');\n     |                  ^\n  52 |     expect(html).toContain('function submitSheet(');\n  53 |     expect(html).toContain('function loadApprovals(');\n  54 |     expect(html).toContain('function openCaptureOptsModal(');\n    at /var/www/360lm/tests/expense.spec.js:51:18"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 1,
                      "startTime": "2026-07-12T12:57:39.297Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/expense-Expenses-—-HTML-St-a9fb7-functions-present-in-source-android-chrome-retry1/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/expense.spec.js",
                        "column": 18,
                        "line": 51
                      }
                    }
                  ],
                  "status": "unexpected"
                }
              ],
              "id": "2399d47327fc24358745-1e43735bf45a18708dc4",
              "file": "expense.spec.js",
              "line": 50,
              "column": 3
            },
            {
              "title": "hub navigation is present",
              "ok": false,
              "tags": [],
              "tests": [
                {
                  "timeout": 30000,
                  "annotations": [],
                  "expectedStatus": "passed",
                  "projectId": "android-chrome",
                  "projectName": "android-chrome",
                  "results": [
                    {
                      "workerIndex": 10,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 14,
                      "error": {
                        "message": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoContain\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m) // indexOf\u001b[22m\n\nExpected substring: \u001b[32m\"goToHub\"\u001b[39m\nReceived string:    \u001b[31m\"<!DOCTYPE HTML PUBLIC \\\"-//W3C//DTD HTML 4.01//EN\\\" \\\"http://www.w3.org/TR/html4/strict.dtd\\\">\u001b[39m\n\u001b[31m<html><head>\u001b[39m\n\u001b[31m<title>404 Not Found</title>\u001b[39m\n\u001b[31m</head><body>\u001b[39m\n\u001b[31m<h1>Not Found</h1>\u001b[39m\n\u001b[31m<p>The requested URL was not found on this server.</p>\u001b[39m\n\u001b[31m</body></html>\u001b[39m\n\u001b[31m\"\u001b[39m",
                        "stack": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoContain\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m) // indexOf\u001b[22m\n\nExpected substring: \u001b[32m\"goToHub\"\u001b[39m\nReceived string:    \u001b[31m\"<!DOCTYPE HTML PUBLIC \\\"-//W3C//DTD HTML 4.01//EN\\\" \\\"http://www.w3.org/TR/html4/strict.dtd\\\">\u001b[39m\n\u001b[31m<html><head>\u001b[39m\n\u001b[31m<title>404 Not Found</title>\u001b[39m\n\u001b[31m</head><body>\u001b[39m\n\u001b[31m<h1>Not Found</h1>\u001b[39m\n\u001b[31m<p>The requested URL was not found on this server.</p>\u001b[39m\n\u001b[31m</body></html>\u001b[39m\n\u001b[31m\"\u001b[39m\n    at /var/www/360lm/tests/expense.spec.js:60:18",
                        "location": {
                          "file": "/var/www/360lm/tests/expense.spec.js",
                          "column": 18,
                          "line": 60
                        },
                        "snippet": "  58 |\n  59 |   test('hub navigation is present', () => {\n> 60 |     expect(html).toContain(\"goToHub\");\n     |                  ^\n  61 |     expect(html).toContain('🏡');\n  62 |   });\n  63 |"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/expense.spec.js",
                            "column": 18,
                            "line": 60
                          },
                          "message": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoContain\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m) // indexOf\u001b[22m\n\nExpected substring: \u001b[32m\"goToHub\"\u001b[39m\nReceived string:    \u001b[31m\"<!DOCTYPE HTML PUBLIC \\\"-//W3C//DTD HTML 4.01//EN\\\" \\\"http://www.w3.org/TR/html4/strict.dtd\\\">\u001b[39m\n\u001b[31m<html><head>\u001b[39m\n\u001b[31m<title>404 Not Found</title>\u001b[39m\n\u001b[31m</head><body>\u001b[39m\n\u001b[31m<h1>Not Found</h1>\u001b[39m\n\u001b[31m<p>The requested URL was not found on this server.</p>\u001b[39m\n\u001b[31m</body></html>\u001b[39m\n\u001b[31m\"\u001b[39m\n\n  58 |\n  59 |   test('hub navigation is present', () => {\n> 60 |     expect(html).toContain(\"goToHub\");\n     |                  ^\n  61 |     expect(html).toContain('🏡');\n  62 |   });\n  63 |\n    at /var/www/360lm/tests/expense.spec.js:60:18"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 0,
                      "startTime": "2026-07-12T12:57:40.400Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/expense-Expenses-—-HTML-Structure-hub-navigation-is-present-android-chrome/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/expense.spec.js",
                        "column": 18,
                        "line": 60
                      }
                    },
                    {
                      "workerIndex": 11,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 14,
                      "error": {
                        "message": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoContain\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m) // indexOf\u001b[22m\n\nExpected substring: \u001b[32m\"goToHub\"\u001b[39m\nReceived string:    \u001b[31m\"<!DOCTYPE HTML PUBLIC \\\"-//W3C//DTD HTML 4.01//EN\\\" \\\"http://www.w3.org/TR/html4/strict.dtd\\\">\u001b[39m\n\u001b[31m<html><head>\u001b[39m\n\u001b[31m<title>404 Not Found</title>\u001b[39m\n\u001b[31m</head><body>\u001b[39m\n\u001b[31m<h1>Not Found</h1>\u001b[39m\n\u001b[31m<p>The requested URL was not found on this server.</p>\u001b[39m\n\u001b[31m</body></html>\u001b[39m\n\u001b[31m\"\u001b[39m",
                        "stack": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoContain\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m) // indexOf\u001b[22m\n\nExpected substring: \u001b[32m\"goToHub\"\u001b[39m\nReceived string:    \u001b[31m\"<!DOCTYPE HTML PUBLIC \\\"-//W3C//DTD HTML 4.01//EN\\\" \\\"http://www.w3.org/TR/html4/strict.dtd\\\">\u001b[39m\n\u001b[31m<html><head>\u001b[39m\n\u001b[31m<title>404 Not Found</title>\u001b[39m\n\u001b[31m</head><body>\u001b[39m\n\u001b[31m<h1>Not Found</h1>\u001b[39m\n\u001b[31m<p>The requested URL was not found on this server.</p>\u001b[39m\n\u001b[31m</body></html>\u001b[39m\n\u001b[31m\"\u001b[39m\n    at /var/www/360lm/tests/expense.spec.js:60:18",
                        "location": {
                          "file": "/var/www/360lm/tests/expense.spec.js",
                          "column": 18,
                          "line": 60
                        },
                        "snippet": "  58 |\n  59 |   test('hub navigation is present', () => {\n> 60 |     expect(html).toContain(\"goToHub\");\n     |                  ^\n  61 |     expect(html).toContain('🏡');\n  62 |   });\n  63 |"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/expense.spec.js",
                            "column": 18,
                            "line": 60
                          },
                          "message": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoContain\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m) // indexOf\u001b[22m\n\nExpected substring: \u001b[32m\"goToHub\"\u001b[39m\nReceived string:    \u001b[31m\"<!DOCTYPE HTML PUBLIC \\\"-//W3C//DTD HTML 4.01//EN\\\" \\\"http://www.w3.org/TR/html4/strict.dtd\\\">\u001b[39m\n\u001b[31m<html><head>\u001b[39m\n\u001b[31m<title>404 Not Found</title>\u001b[39m\n\u001b[31m</head><body>\u001b[39m\n\u001b[31m<h1>Not Found</h1>\u001b[39m\n\u001b[31m<p>The requested URL was not found on this server.</p>\u001b[39m\n\u001b[31m</body></html>\u001b[39m\n\u001b[31m\"\u001b[39m\n\n  58 |\n  59 |   test('hub navigation is present', () => {\n> 60 |     expect(html).toContain(\"goToHub\");\n     |                  ^\n  61 |     expect(html).toContain('🏡');\n  62 |   });\n  63 |\n    at /var/www/360lm/tests/expense.spec.js:60:18"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 1,
                      "startTime": "2026-07-12T12:57:41.698Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/expense-Expenses-—-HTML-Structure-hub-navigation-is-present-android-chrome-retry1/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/expense.spec.js",
                        "column": 18,
                        "line": 60
                      }
                    }
                  ],
                  "status": "unexpected"
                }
              ],
              "id": "2399d47327fc24358745-fa4747e31770727c09ff",
              "file": "expense.spec.js",
              "line": 59,
              "column": 3
            },
            {
              "title": "push notification support present",
              "ok": false,
              "tags": [],
              "tests": [
                {
                  "timeout": 30000,
                  "annotations": [],
                  "expectedStatus": "passed",
                  "projectId": "android-chrome",
                  "projectName": "android-chrome",
                  "results": [
                    {
                      "workerIndex": 12,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 18,
                      "error": {
                        "message": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoContain\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m) // indexOf\u001b[22m\n\nExpected substring: \u001b[32m\"subscribePush(\"\u001b[39m\nReceived string:    \u001b[31m\"<!DOCTYPE HTML PUBLIC \\\"-//W3C//DTD HTML 4.01//EN\\\" \\\"http://www.w3.org/TR/html4/strict.dtd\\\">\u001b[39m\n\u001b[31m<html><head>\u001b[39m\n\u001b[31m<title>404 Not Found</title>\u001b[39m\n\u001b[31m</head><body>\u001b[39m\n\u001b[31m<h1>Not Found</h1>\u001b[39m\n\u001b[31m<p>The requested URL was not found on this server.</p>\u001b[39m\n\u001b[31m</body></html>\u001b[39m\n\u001b[31m\"\u001b[39m",
                        "stack": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoContain\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m) // indexOf\u001b[22m\n\nExpected substring: \u001b[32m\"subscribePush(\"\u001b[39m\nReceived string:    \u001b[31m\"<!DOCTYPE HTML PUBLIC \\\"-//W3C//DTD HTML 4.01//EN\\\" \\\"http://www.w3.org/TR/html4/strict.dtd\\\">\u001b[39m\n\u001b[31m<html><head>\u001b[39m\n\u001b[31m<title>404 Not Found</title>\u001b[39m\n\u001b[31m</head><body>\u001b[39m\n\u001b[31m<h1>Not Found</h1>\u001b[39m\n\u001b[31m<p>The requested URL was not found on this server.</p>\u001b[39m\n\u001b[31m</body></html>\u001b[39m\n\u001b[31m\"\u001b[39m\n    at /var/www/360lm/tests/expense.spec.js:65:18",
                        "location": {
                          "file": "/var/www/360lm/tests/expense.spec.js",
                          "column": 18,
                          "line": 65
                        },
                        "snippet": "  63 |\n  64 |   test('push notification support present', () => {\n> 65 |     expect(html).toContain('subscribePush(');\n     |                  ^\n  66 |     expect(html).toContain('notifyUser(');\n  67 |   });\n  68 |"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/expense.spec.js",
                            "column": 18,
                            "line": 65
                          },
                          "message": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoContain\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m) // indexOf\u001b[22m\n\nExpected substring: \u001b[32m\"subscribePush(\"\u001b[39m\nReceived string:    \u001b[31m\"<!DOCTYPE HTML PUBLIC \\\"-//W3C//DTD HTML 4.01//EN\\\" \\\"http://www.w3.org/TR/html4/strict.dtd\\\">\u001b[39m\n\u001b[31m<html><head>\u001b[39m\n\u001b[31m<title>404 Not Found</title>\u001b[39m\n\u001b[31m</head><body>\u001b[39m\n\u001b[31m<h1>Not Found</h1>\u001b[39m\n\u001b[31m<p>The requested URL was not found on this server.</p>\u001b[39m\n\u001b[31m</body></html>\u001b[39m\n\u001b[31m\"\u001b[39m\n\n  63 |\n  64 |   test('push notification support present', () => {\n> 65 |     expect(html).toContain('subscribePush(');\n     |                  ^\n  66 |     expect(html).toContain('notifyUser(');\n  67 |   });\n  68 |\n    at /var/www/360lm/tests/expense.spec.js:65:18"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 0,
                      "startTime": "2026-07-12T12:57:42.788Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/expense-Expenses-—-HTML-St-1d830-otification-support-present-android-chrome/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/expense.spec.js",
                        "column": 18,
                        "line": 65
                      }
                    },
                    {
                      "workerIndex": 13,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 10,
                      "error": {
                        "message": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoContain\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m) // indexOf\u001b[22m\n\nExpected substring: \u001b[32m\"subscribePush(\"\u001b[39m\nReceived string:    \u001b[31m\"<!DOCTYPE HTML PUBLIC \\\"-//W3C//DTD HTML 4.01//EN\\\" \\\"http://www.w3.org/TR/html4/strict.dtd\\\">\u001b[39m\n\u001b[31m<html><head>\u001b[39m\n\u001b[31m<title>404 Not Found</title>\u001b[39m\n\u001b[31m</head><body>\u001b[39m\n\u001b[31m<h1>Not Found</h1>\u001b[39m\n\u001b[31m<p>The requested URL was not found on this server.</p>\u001b[39m\n\u001b[31m</body></html>\u001b[39m\n\u001b[31m\"\u001b[39m",
                        "stack": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoContain\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m) // indexOf\u001b[22m\n\nExpected substring: \u001b[32m\"subscribePush(\"\u001b[39m\nReceived string:    \u001b[31m\"<!DOCTYPE HTML PUBLIC \\\"-//W3C//DTD HTML 4.01//EN\\\" \\\"http://www.w3.org/TR/html4/strict.dtd\\\">\u001b[39m\n\u001b[31m<html><head>\u001b[39m\n\u001b[31m<title>404 Not Found</title>\u001b[39m\n\u001b[31m</head><body>\u001b[39m\n\u001b[31m<h1>Not Found</h1>\u001b[39m\n\u001b[31m<p>The requested URL was not found on this server.</p>\u001b[39m\n\u001b[31m</body></html>\u001b[39m\n\u001b[31m\"\u001b[39m\n    at /var/www/360lm/tests/expense.spec.js:65:18",
                        "location": {
                          "file": "/var/www/360lm/tests/expense.spec.js",
                          "column": 18,
                          "line": 65
                        },
                        "snippet": "  63 |\n  64 |   test('push notification support present', () => {\n> 65 |     expect(html).toContain('subscribePush(');\n     |                  ^\n  66 |     expect(html).toContain('notifyUser(');\n  67 |   });\n  68 |"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/expense.spec.js",
                            "column": 18,
                            "line": 65
                          },
                          "message": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoContain\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m) // indexOf\u001b[22m\n\nExpected substring: \u001b[32m\"subscribePush(\"\u001b[39m\nReceived string:    \u001b[31m\"<!DOCTYPE HTML PUBLIC \\\"-//W3C//DTD HTML 4.01//EN\\\" \\\"http://www.w3.org/TR/html4/strict.dtd\\\">\u001b[39m\n\u001b[31m<html><head>\u001b[39m\n\u001b[31m<title>404 Not Found</title>\u001b[39m\n\u001b[31m</head><body>\u001b[39m\n\u001b[31m<h1>Not Found</h1>\u001b[39m\n\u001b[31m<p>The requested URL was not found on this server.</p>\u001b[39m\n\u001b[31m</body></html>\u001b[39m\n\u001b[31m\"\u001b[39m\n\n  63 |\n  64 |   test('push notification support present', () => {\n> 65 |     expect(html).toContain('subscribePush(');\n     |                  ^\n  66 |     expect(html).toContain('notifyUser(');\n  67 |   });\n  68 |\n    at /var/www/360lm/tests/expense.spec.js:65:18"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 1,
                      "startTime": "2026-07-12T12:57:43.878Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/expense-Expenses-—-HTML-St-1d830-otification-support-present-android-chrome-retry1/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/expense.spec.js",
                        "column": 18,
                        "line": 65
                      }
                    }
                  ],
                  "status": "unexpected"
                }
              ],
              "id": "2399d47327fc24358745-6626a912c1ac516dab54",
              "file": "expense.spec.js",
              "line": 64,
              "column": 3
            },
            {
              "title": "custodian advances integration present",
              "ok": false,
              "tags": [],
              "tests": [
                {
                  "timeout": 30000,
                  "annotations": [],
                  "expectedStatus": "passed",
                  "projectId": "android-chrome",
                  "projectName": "android-chrome",
                  "results": [
                    {
                      "workerIndex": 14,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 18,
                      "error": {
                        "message": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoContain\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m) // indexOf\u001b[22m\n\nExpected substring: \u001b[32m\"loadAdvancesFromCustodian(\"\u001b[39m\nReceived string:    \u001b[31m\"<!DOCTYPE HTML PUBLIC \\\"-//W3C//DTD HTML 4.01//EN\\\" \\\"http://www.w3.org/TR/html4/strict.dtd\\\">\u001b[39m\n\u001b[31m<html><head>\u001b[39m\n\u001b[31m<title>404 Not Found</title>\u001b[39m\n\u001b[31m</head><body>\u001b[39m\n\u001b[31m<h1>Not Found</h1>\u001b[39m\n\u001b[31m<p>The requested URL was not found on this server.</p>\u001b[39m\n\u001b[31m</body></html>\u001b[39m\n\u001b[31m\"\u001b[39m",
                        "stack": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoContain\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m) // indexOf\u001b[22m\n\nExpected substring: \u001b[32m\"loadAdvancesFromCustodian(\"\u001b[39m\nReceived string:    \u001b[31m\"<!DOCTYPE HTML PUBLIC \\\"-//W3C//DTD HTML 4.01//EN\\\" \\\"http://www.w3.org/TR/html4/strict.dtd\\\">\u001b[39m\n\u001b[31m<html><head>\u001b[39m\n\u001b[31m<title>404 Not Found</title>\u001b[39m\n\u001b[31m</head><body>\u001b[39m\n\u001b[31m<h1>Not Found</h1>\u001b[39m\n\u001b[31m<p>The requested URL was not found on this server.</p>\u001b[39m\n\u001b[31m</body></html>\u001b[39m\n\u001b[31m\"\u001b[39m\n    at /var/www/360lm/tests/expense.spec.js:70:18",
                        "location": {
                          "file": "/var/www/360lm/tests/expense.spec.js",
                          "column": 18,
                          "line": 70
                        },
                        "snippet": "  68 |\n  69 |   test('custodian advances integration present', () => {\n> 70 |     expect(html).toContain('loadAdvancesFromCustodian(');\n     |                  ^\n  71 |     expect(html).toContain('renderAdvanceDashboardTiles(');\n  72 |   });\n  73 |"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/expense.spec.js",
                            "column": 18,
                            "line": 70
                          },
                          "message": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoContain\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m) // indexOf\u001b[22m\n\nExpected substring: \u001b[32m\"loadAdvancesFromCustodian(\"\u001b[39m\nReceived string:    \u001b[31m\"<!DOCTYPE HTML PUBLIC \\\"-//W3C//DTD HTML 4.01//EN\\\" \\\"http://www.w3.org/TR/html4/strict.dtd\\\">\u001b[39m\n\u001b[31m<html><head>\u001b[39m\n\u001b[31m<title>404 Not Found</title>\u001b[39m\n\u001b[31m</head><body>\u001b[39m\n\u001b[31m<h1>Not Found</h1>\u001b[39m\n\u001b[31m<p>The requested URL was not found on this server.</p>\u001b[39m\n\u001b[31m</body></html>\u001b[39m\n\u001b[31m\"\u001b[39m\n\n  68 |\n  69 |   test('custodian advances integration present', () => {\n> 70 |     expect(html).toContain('loadAdvancesFromCustodian(');\n     |                  ^\n  71 |     expect(html).toContain('renderAdvanceDashboardTiles(');\n  72 |   });\n  73 |\n    at /var/www/360lm/tests/expense.spec.js:70:18"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 0,
                      "startTime": "2026-07-12T12:57:45.475Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/expense-Expenses-—-HTML-St-dbba9-dvances-integration-present-android-chrome/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/expense.spec.js",
                        "column": 18,
                        "line": 70
                      }
                    },
                    {
                      "workerIndex": 15,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 24,
                      "error": {
                        "message": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoContain\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m) // indexOf\u001b[22m\n\nExpected substring: \u001b[32m\"loadAdvancesFromCustodian(\"\u001b[39m\nReceived string:    \u001b[31m\"<!DOCTYPE HTML PUBLIC \\\"-//W3C//DTD HTML 4.01//EN\\\" \\\"http://www.w3.org/TR/html4/strict.dtd\\\">\u001b[39m\n\u001b[31m<html><head>\u001b[39m\n\u001b[31m<title>404 Not Found</title>\u001b[39m\n\u001b[31m</head><body>\u001b[39m\n\u001b[31m<h1>Not Found</h1>\u001b[39m\n\u001b[31m<p>The requested URL was not found on this server.</p>\u001b[39m\n\u001b[31m</body></html>\u001b[39m\n\u001b[31m\"\u001b[39m",
                        "stack": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoContain\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m) // indexOf\u001b[22m\n\nExpected substring: \u001b[32m\"loadAdvancesFromCustodian(\"\u001b[39m\nReceived string:    \u001b[31m\"<!DOCTYPE HTML PUBLIC \\\"-//W3C//DTD HTML 4.01//EN\\\" \\\"http://www.w3.org/TR/html4/strict.dtd\\\">\u001b[39m\n\u001b[31m<html><head>\u001b[39m\n\u001b[31m<title>404 Not Found</title>\u001b[39m\n\u001b[31m</head><body>\u001b[39m\n\u001b[31m<h1>Not Found</h1>\u001b[39m\n\u001b[31m<p>The requested URL was not found on this server.</p>\u001b[39m\n\u001b[31m</body></html>\u001b[39m\n\u001b[31m\"\u001b[39m\n    at /var/www/360lm/tests/expense.spec.js:70:18",
                        "location": {
                          "file": "/var/www/360lm/tests/expense.spec.js",
                          "column": 18,
                          "line": 70
                        },
                        "snippet": "  68 |\n  69 |   test('custodian advances integration present', () => {\n> 70 |     expect(html).toContain('loadAdvancesFromCustodian(');\n     |                  ^\n  71 |     expect(html).toContain('renderAdvanceDashboardTiles(');\n  72 |   });\n  73 |"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/expense.spec.js",
                            "column": 18,
                            "line": 70
                          },
                          "message": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoContain\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m) // indexOf\u001b[22m\n\nExpected substring: \u001b[32m\"loadAdvancesFromCustodian(\"\u001b[39m\nReceived string:    \u001b[31m\"<!DOCTYPE HTML PUBLIC \\\"-//W3C//DTD HTML 4.01//EN\\\" \\\"http://www.w3.org/TR/html4/strict.dtd\\\">\u001b[39m\n\u001b[31m<html><head>\u001b[39m\n\u001b[31m<title>404 Not Found</title>\u001b[39m\n\u001b[31m</head><body>\u001b[39m\n\u001b[31m<h1>Not Found</h1>\u001b[39m\n\u001b[31m<p>The requested URL was not found on this server.</p>\u001b[39m\n\u001b[31m</body></html>\u001b[39m\n\u001b[31m\"\u001b[39m\n\n  68 |\n  69 |   test('custodian advances integration present', () => {\n> 70 |     expect(html).toContain('loadAdvancesFromCustodian(');\n     |                  ^\n  71 |     expect(html).toContain('renderAdvanceDashboardTiles(');\n  72 |   });\n  73 |\n    at /var/www/360lm/tests/expense.spec.js:70:18"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 1,
                      "startTime": "2026-07-12T12:57:46.742Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/expense-Expenses-—-HTML-St-dbba9-dvances-integration-present-android-chrome-retry1/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/expense.spec.js",
                        "column": 18,
                        "line": 70
                      }
                    }
                  ],
                  "status": "unexpected"
                }
              ],
              "id": "2399d47327fc24358745-ea60ed3da9da31148844",
              "file": "expense.spec.js",
              "line": 69,
              "column": 3
            },
            {
              "title": "impress screen present for Impress Reports",
              "ok": false,
              "tags": [],
              "tests": [
                {
                  "timeout": 30000,
                  "annotations": [],
                  "expectedStatus": "passed",
                  "projectId": "android-chrome",
                  "projectName": "android-chrome",
                  "results": [
                    {
                      "workerIndex": 16,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 10,
                      "error": {
                        "message": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoContain\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m) // indexOf\u001b[22m\n\nExpected substring: \u001b[32m\"id=\\\"screen-impress\\\"\"\u001b[39m\nReceived string:    \u001b[31m\"<!DOCTYPE HTML PUBLIC \\\"-//W3C//DTD HTML 4.01//EN\\\" \\\"http://www.w3.org/TR/html4/strict.dtd\\\">\u001b[39m\n\u001b[31m<html><head>\u001b[39m\n\u001b[31m<title>404 Not Found</title>\u001b[39m\n\u001b[31m</head><body>\u001b[39m\n\u001b[31m<h1>Not Found</h1>\u001b[39m\n\u001b[31m<p>The requested URL was not found on this server.</p>\u001b[39m\n\u001b[31m</body></html>\u001b[39m\n\u001b[31m\"\u001b[39m",
                        "stack": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoContain\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m) // indexOf\u001b[22m\n\nExpected substring: \u001b[32m\"id=\\\"screen-impress\\\"\"\u001b[39m\nReceived string:    \u001b[31m\"<!DOCTYPE HTML PUBLIC \\\"-//W3C//DTD HTML 4.01//EN\\\" \\\"http://www.w3.org/TR/html4/strict.dtd\\\">\u001b[39m\n\u001b[31m<html><head>\u001b[39m\n\u001b[31m<title>404 Not Found</title>\u001b[39m\n\u001b[31m</head><body>\u001b[39m\n\u001b[31m<h1>Not Found</h1>\u001b[39m\n\u001b[31m<p>The requested URL was not found on this server.</p>\u001b[39m\n\u001b[31m</body></html>\u001b[39m\n\u001b[31m\"\u001b[39m\n    at /var/www/360lm/tests/expense.spec.js:75:18",
                        "location": {
                          "file": "/var/www/360lm/tests/expense.spec.js",
                          "column": 18,
                          "line": 75
                        },
                        "snippet": "  73 |\n  74 |   test('impress screen present for Impress Reports', () => {\n> 75 |     expect(html).toContain('id=\"screen-impress\"');\n     |                  ^\n  76 |   });\n  77 | });\n  78 |"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/expense.spec.js",
                            "column": 18,
                            "line": 75
                          },
                          "message": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoContain\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m) // indexOf\u001b[22m\n\nExpected substring: \u001b[32m\"id=\\\"screen-impress\\\"\"\u001b[39m\nReceived string:    \u001b[31m\"<!DOCTYPE HTML PUBLIC \\\"-//W3C//DTD HTML 4.01//EN\\\" \\\"http://www.w3.org/TR/html4/strict.dtd\\\">\u001b[39m\n\u001b[31m<html><head>\u001b[39m\n\u001b[31m<title>404 Not Found</title>\u001b[39m\n\u001b[31m</head><body>\u001b[39m\n\u001b[31m<h1>Not Found</h1>\u001b[39m\n\u001b[31m<p>The requested URL was not found on this server.</p>\u001b[39m\n\u001b[31m</body></html>\u001b[39m\n\u001b[31m\"\u001b[39m\n\n  73 |\n  74 |   test('impress screen present for Impress Reports', () => {\n> 75 |     expect(html).toContain('id=\"screen-impress\"');\n     |                  ^\n  76 |   });\n  77 | });\n  78 |\n    at /var/www/360lm/tests/expense.spec.js:75:18"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 0,
                      "startTime": "2026-07-12T12:57:47.925Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/expense-Expenses-—-HTML-St-135f5-present-for-Impress-Reports-android-chrome/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/expense.spec.js",
                        "column": 18,
                        "line": 75
                      }
                    },
                    {
                      "workerIndex": 17,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 15,
                      "error": {
                        "message": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoContain\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m) // indexOf\u001b[22m\n\nExpected substring: \u001b[32m\"id=\\\"screen-impress\\\"\"\u001b[39m\nReceived string:    \u001b[31m\"<!DOCTYPE HTML PUBLIC \\\"-//W3C//DTD HTML 4.01//EN\\\" \\\"http://www.w3.org/TR/html4/strict.dtd\\\">\u001b[39m\n\u001b[31m<html><head>\u001b[39m\n\u001b[31m<title>404 Not Found</title>\u001b[39m\n\u001b[31m</head><body>\u001b[39m\n\u001b[31m<h1>Not Found</h1>\u001b[39m\n\u001b[31m<p>The requested URL was not found on this server.</p>\u001b[39m\n\u001b[31m</body></html>\u001b[39m\n\u001b[31m\"\u001b[39m",
                        "stack": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoContain\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m) // indexOf\u001b[22m\n\nExpected substring: \u001b[32m\"id=\\\"screen-impress\\\"\"\u001b[39m\nReceived string:    \u001b[31m\"<!DOCTYPE HTML PUBLIC \\\"-//W3C//DTD HTML 4.01//EN\\\" \\\"http://www.w3.org/TR/html4/strict.dtd\\\">\u001b[39m\n\u001b[31m<html><head>\u001b[39m\n\u001b[31m<title>404 Not Found</title>\u001b[39m\n\u001b[31m</head><body>\u001b[39m\n\u001b[31m<h1>Not Found</h1>\u001b[39m\n\u001b[31m<p>The requested URL was not found on this server.</p>\u001b[39m\n\u001b[31m</body></html>\u001b[39m\n\u001b[31m\"\u001b[39m\n    at /var/www/360lm/tests/expense.spec.js:75:18",
                        "location": {
                          "file": "/var/www/360lm/tests/expense.spec.js",
                          "column": 18,
                          "line": 75
                        },
                        "snippet": "  73 |\n  74 |   test('impress screen present for Impress Reports', () => {\n> 75 |     expect(html).toContain('id=\"screen-impress\"');\n     |                  ^\n  76 |   });\n  77 | });\n  78 |"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/expense.spec.js",
                            "column": 18,
                            "line": 75
                          },
                          "message": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoContain\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m) // indexOf\u001b[22m\n\nExpected substring: \u001b[32m\"id=\\\"screen-impress\\\"\"\u001b[39m\nReceived string:    \u001b[31m\"<!DOCTYPE HTML PUBLIC \\\"-//W3C//DTD HTML 4.01//EN\\\" \\\"http://www.w3.org/TR/html4/strict.dtd\\\">\u001b[39m\n\u001b[31m<html><head>\u001b[39m\n\u001b[31m<title>404 Not Found</title>\u001b[39m\n\u001b[31m</head><body>\u001b[39m\n\u001b[31m<h1>Not Found</h1>\u001b[39m\n\u001b[31m<p>The requested URL was not found on this server.</p>\u001b[39m\n\u001b[31m</body></html>\u001b[39m\n\u001b[31m\"\u001b[39m\n\n  73 |\n  74 |   test('impress screen present for Impress Reports', () => {\n> 75 |     expect(html).toContain('id=\"screen-impress\"');\n     |                  ^\n  76 |   });\n  77 | });\n  78 |\n    at /var/www/360lm/tests/expense.spec.js:75:18"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 1,
                      "startTime": "2026-07-12T12:57:49.047Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/expense-Expenses-—-HTML-St-135f5-present-for-Impress-Reports-android-chrome-retry1/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/expense.spec.js",
                        "column": 18,
                        "line": 75
                      }
                    }
                  ],
                  "status": "unexpected"
                }
              ],
              "id": "2399d47327fc24358745-4f6fe6df41f7cbbc8293",
              "file": "expense.spec.js",
              "line": 74,
              "column": 3
            },
            {
              "title": "all screens present",
              "ok": false,
              "tags": [],
              "tests": [
                {
                  "timeout": 30000,
                  "annotations": [],
                  "expectedStatus": "passed",
                  "projectId": "desktop-chrome",
                  "projectName": "desktop-chrome",
                  "results": [
                    {
                      "workerIndex": 182,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 10,
                      "error": {
                        "message": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoContain\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m) // indexOf\u001b[22m\n\nExpected substring: \u001b[32m\"id=\\\"screen-login\\\"\"\u001b[39m\nReceived string:    \u001b[31m\"<!DOCTYPE HTML PUBLIC \\\"-//W3C//DTD HTML 4.01//EN\\\" \\\"http://www.w3.org/TR/html4/strict.dtd\\\">\u001b[39m\n\u001b[31m<html><head>\u001b[39m\n\u001b[31m<title>404 Not Found</title>\u001b[39m\n\u001b[31m</head><body>\u001b[39m\n\u001b[31m<h1>Not Found</h1>\u001b[39m\n\u001b[31m<p>The requested URL was not found on this server.</p>\u001b[39m\n\u001b[31m</body></html>\u001b[39m\n\u001b[31m\"\u001b[39m",
                        "stack": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoContain\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m) // indexOf\u001b[22m\n\nExpected substring: \u001b[32m\"id=\\\"screen-login\\\"\"\u001b[39m\nReceived string:    \u001b[31m\"<!DOCTYPE HTML PUBLIC \\\"-//W3C//DTD HTML 4.01//EN\\\" \\\"http://www.w3.org/TR/html4/strict.dtd\\\">\u001b[39m\n\u001b[31m<html><head>\u001b[39m\n\u001b[31m<title>404 Not Found</title>\u001b[39m\n\u001b[31m</head><body>\u001b[39m\n\u001b[31m<h1>Not Found</h1>\u001b[39m\n\u001b[31m<p>The requested URL was not found on this server.</p>\u001b[39m\n\u001b[31m</body></html>\u001b[39m\n\u001b[31m\"\u001b[39m\n    at /var/www/360lm/tests/expense.spec.js:40:18",
                        "location": {
                          "file": "/var/www/360lm/tests/expense.spec.js",
                          "column": 18,
                          "line": 40
                        },
                        "snippet": "  38 |\n  39 |   test('all screens present', () => {\n> 40 |     expect(html).toContain('id=\"screen-login\"');\n     |                  ^\n  41 |     expect(html).toContain('id=\"screen-dash\"');\n  42 |     expect(html).toContain('id=\"screen-capture\"');\n  43 |     expect(html).toContain('id=\"screen-actions\"');"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/expense.spec.js",
                            "column": 18,
                            "line": 40
                          },
                          "message": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoContain\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m) // indexOf\u001b[22m\n\nExpected substring: \u001b[32m\"id=\\\"screen-login\\\"\"\u001b[39m\nReceived string:    \u001b[31m\"<!DOCTYPE HTML PUBLIC \\\"-//W3C//DTD HTML 4.01//EN\\\" \\\"http://www.w3.org/TR/html4/strict.dtd\\\">\u001b[39m\n\u001b[31m<html><head>\u001b[39m\n\u001b[31m<title>404 Not Found</title>\u001b[39m\n\u001b[31m</head><body>\u001b[39m\n\u001b[31m<h1>Not Found</h1>\u001b[39m\n\u001b[31m<p>The requested URL was not found on this server.</p>\u001b[39m\n\u001b[31m</body></html>\u001b[39m\n\u001b[31m\"\u001b[39m\n\n  38 |\n  39 |   test('all screens present', () => {\n> 40 |     expect(html).toContain('id=\"screen-login\"');\n     |                  ^\n  41 |     expect(html).toContain('id=\"screen-dash\"');\n  42 |     expect(html).toContain('id=\"screen-capture\"');\n  43 |     expect(html).toContain('id=\"screen-actions\"');\n    at /var/www/360lm/tests/expense.spec.js:40:18"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 0,
                      "startTime": "2026-07-12T13:17:50.747Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/expense-Expenses-—-HTML-Structure-all-screens-present-desktop-chrome/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/expense.spec.js",
                        "column": 18,
                        "line": 40
                      }
                    },
                    {
                      "workerIndex": 183,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 11,
                      "error": {
                        "message": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoContain\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m) // indexOf\u001b[22m\n\nExpected substring: \u001b[32m\"id=\\\"screen-login\\\"\"\u001b[39m\nReceived string:    \u001b[31m\"<!DOCTYPE HTML PUBLIC \\\"-//W3C//DTD HTML 4.01//EN\\\" \\\"http://www.w3.org/TR/html4/strict.dtd\\\">\u001b[39m\n\u001b[31m<html><head>\u001b[39m\n\u001b[31m<title>404 Not Found</title>\u001b[39m\n\u001b[31m</head><body>\u001b[39m\n\u001b[31m<h1>Not Found</h1>\u001b[39m\n\u001b[31m<p>The requested URL was not found on this server.</p>\u001b[39m\n\u001b[31m</body></html>\u001b[39m\n\u001b[31m\"\u001b[39m",
                        "stack": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoContain\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m) // indexOf\u001b[22m\n\nExpected substring: \u001b[32m\"id=\\\"screen-login\\\"\"\u001b[39m\nReceived string:    \u001b[31m\"<!DOCTYPE HTML PUBLIC \\\"-//W3C//DTD HTML 4.01//EN\\\" \\\"http://www.w3.org/TR/html4/strict.dtd\\\">\u001b[39m\n\u001b[31m<html><head>\u001b[39m\n\u001b[31m<title>404 Not Found</title>\u001b[39m\n\u001b[31m</head><body>\u001b[39m\n\u001b[31m<h1>Not Found</h1>\u001b[39m\n\u001b[31m<p>The requested URL was not found on this server.</p>\u001b[39m\n\u001b[31m</body></html>\u001b[39m\n\u001b[31m\"\u001b[39m\n    at /var/www/360lm/tests/expense.spec.js:40:18",
                        "location": {
                          "file": "/var/www/360lm/tests/expense.spec.js",
                          "column": 18,
                          "line": 40
                        },
                        "snippet": "  38 |\n  39 |   test('all screens present', () => {\n> 40 |     expect(html).toContain('id=\"screen-login\"');\n     |                  ^\n  41 |     expect(html).toContain('id=\"screen-dash\"');\n  42 |     expect(html).toContain('id=\"screen-capture\"');\n  43 |     expect(html).toContain('id=\"screen-actions\"');"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/expense.spec.js",
                            "column": 18,
                            "line": 40
                          },
                          "message": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoContain\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m) // indexOf\u001b[22m\n\nExpected substring: \u001b[32m\"id=\\\"screen-login\\\"\"\u001b[39m\nReceived string:    \u001b[31m\"<!DOCTYPE HTML PUBLIC \\\"-//W3C//DTD HTML 4.01//EN\\\" \\\"http://www.w3.org/TR/html4/strict.dtd\\\">\u001b[39m\n\u001b[31m<html><head>\u001b[39m\n\u001b[31m<title>404 Not Found</title>\u001b[39m\n\u001b[31m</head><body>\u001b[39m\n\u001b[31m<h1>Not Found</h1>\u001b[39m\n\u001b[31m<p>The requested URL was not found on this server.</p>\u001b[39m\n\u001b[31m</body></html>\u001b[39m\n\u001b[31m\"\u001b[39m\n\n  38 |\n  39 |   test('all screens present', () => {\n> 40 |     expect(html).toContain('id=\"screen-login\"');\n     |                  ^\n  41 |     expect(html).toContain('id=\"screen-dash\"');\n  42 |     expect(html).toContain('id=\"screen-capture\"');\n  43 |     expect(html).toContain('id=\"screen-actions\"');\n    at /var/www/360lm/tests/expense.spec.js:40:18"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 1,
                      "startTime": "2026-07-12T13:17:51.658Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/expense-Expenses-—-HTML-Structure-all-screens-present-desktop-chrome-retry1/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/expense.spec.js",
                        "column": 18,
                        "line": 40
                      }
                    }
                  ],
                  "status": "unexpected"
                }
              ],
              "id": "2399d47327fc24358745-543108dd5b722e4b30e7",
              "file": "expense.spec.js",
              "line": 39,
              "column": 3
            },
            {
              "title": "key functions present in source",
              "ok": false,
              "tags": [],
              "tests": [
                {
                  "timeout": 30000,
                  "annotations": [],
                  "expectedStatus": "passed",
                  "projectId": "desktop-chrome",
                  "projectName": "desktop-chrome",
                  "results": [
                    {
                      "workerIndex": 184,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 10,
                      "error": {
                        "message": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoContain\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m) // indexOf\u001b[22m\n\nExpected substring: \u001b[32m\"function goToHub(\"\u001b[39m\nReceived string:    \u001b[31m\"<!DOCTYPE HTML PUBLIC \\\"-//W3C//DTD HTML 4.01//EN\\\" \\\"http://www.w3.org/TR/html4/strict.dtd\\\">\u001b[39m\n\u001b[31m<html><head>\u001b[39m\n\u001b[31m<title>404 Not Found</title>\u001b[39m\n\u001b[31m</head><body>\u001b[39m\n\u001b[31m<h1>Not Found</h1>\u001b[39m\n\u001b[31m<p>The requested URL was not found on this server.</p>\u001b[39m\n\u001b[31m</body></html>\u001b[39m\n\u001b[31m\"\u001b[39m",
                        "stack": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoContain\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m) // indexOf\u001b[22m\n\nExpected substring: \u001b[32m\"function goToHub(\"\u001b[39m\nReceived string:    \u001b[31m\"<!DOCTYPE HTML PUBLIC \\\"-//W3C//DTD HTML 4.01//EN\\\" \\\"http://www.w3.org/TR/html4/strict.dtd\\\">\u001b[39m\n\u001b[31m<html><head>\u001b[39m\n\u001b[31m<title>404 Not Found</title>\u001b[39m\n\u001b[31m</head><body>\u001b[39m\n\u001b[31m<h1>Not Found</h1>\u001b[39m\n\u001b[31m<p>The requested URL was not found on this server.</p>\u001b[39m\n\u001b[31m</body></html>\u001b[39m\n\u001b[31m\"\u001b[39m\n    at /var/www/360lm/tests/expense.spec.js:51:18",
                        "location": {
                          "file": "/var/www/360lm/tests/expense.spec.js",
                          "column": 18,
                          "line": 51
                        },
                        "snippet": "  49 |\n  50 |   test('key functions present in source', () => {\n> 51 |     expect(html).toContain('function goToHub(');\n     |                  ^\n  52 |     expect(html).toContain('function submitSheet(');\n  53 |     expect(html).toContain('function loadApprovals(');\n  54 |     expect(html).toContain('function openCaptureOptsModal(');"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/expense.spec.js",
                            "column": 18,
                            "line": 51
                          },
                          "message": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoContain\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m) // indexOf\u001b[22m\n\nExpected substring: \u001b[32m\"function goToHub(\"\u001b[39m\nReceived string:    \u001b[31m\"<!DOCTYPE HTML PUBLIC \\\"-//W3C//DTD HTML 4.01//EN\\\" \\\"http://www.w3.org/TR/html4/strict.dtd\\\">\u001b[39m\n\u001b[31m<html><head>\u001b[39m\n\u001b[31m<title>404 Not Found</title>\u001b[39m\n\u001b[31m</head><body>\u001b[39m\n\u001b[31m<h1>Not Found</h1>\u001b[39m\n\u001b[31m<p>The requested URL was not found on this server.</p>\u001b[39m\n\u001b[31m</body></html>\u001b[39m\n\u001b[31m\"\u001b[39m\n\n  49 |\n  50 |   test('key functions present in source', () => {\n> 51 |     expect(html).toContain('function goToHub(');\n     |                  ^\n  52 |     expect(html).toContain('function submitSheet(');\n  53 |     expect(html).toContain('function loadApprovals(');\n  54 |     expect(html).toContain('function openCaptureOptsModal(');\n    at /var/www/360lm/tests/expense.spec.js:51:18"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 0,
                      "startTime": "2026-07-12T13:17:52.688Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/expense-Expenses-—-HTML-St-a9fb7-functions-present-in-source-desktop-chrome/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/expense.spec.js",
                        "column": 18,
                        "line": 51
                      }
                    },
                    {
                      "workerIndex": 185,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 19,
                      "error": {
                        "message": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoContain\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m) // indexOf\u001b[22m\n\nExpected substring: \u001b[32m\"function goToHub(\"\u001b[39m\nReceived string:    \u001b[31m\"<!DOCTYPE HTML PUBLIC \\\"-//W3C//DTD HTML 4.01//EN\\\" \\\"http://www.w3.org/TR/html4/strict.dtd\\\">\u001b[39m\n\u001b[31m<html><head>\u001b[39m\n\u001b[31m<title>404 Not Found</title>\u001b[39m\n\u001b[31m</head><body>\u001b[39m\n\u001b[31m<h1>Not Found</h1>\u001b[39m\n\u001b[31m<p>The requested URL was not found on this server.</p>\u001b[39m\n\u001b[31m</body></html>\u001b[39m\n\u001b[31m\"\u001b[39m",
                        "stack": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoContain\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m) // indexOf\u001b[22m\n\nExpected substring: \u001b[32m\"function goToHub(\"\u001b[39m\nReceived string:    \u001b[31m\"<!DOCTYPE HTML PUBLIC \\\"-//W3C//DTD HTML 4.01//EN\\\" \\\"http://www.w3.org/TR/html4/strict.dtd\\\">\u001b[39m\n\u001b[31m<html><head>\u001b[39m\n\u001b[31m<title>404 Not Found</title>\u001b[39m\n\u001b[31m</head><body>\u001b[39m\n\u001b[31m<h1>Not Found</h1>\u001b[39m\n\u001b[31m<p>The requested URL was not found on this server.</p>\u001b[39m\n\u001b[31m</body></html>\u001b[39m\n\u001b[31m\"\u001b[39m\n    at /var/www/360lm/tests/expense.spec.js:51:18",
                        "location": {
                          "file": "/var/www/360lm/tests/expense.spec.js",
                          "column": 18,
                          "line": 51
                        },
                        "snippet": "  49 |\n  50 |   test('key functions present in source', () => {\n> 51 |     expect(html).toContain('function goToHub(');\n     |                  ^\n  52 |     expect(html).toContain('function submitSheet(');\n  53 |     expect(html).toContain('function loadApprovals(');\n  54 |     expect(html).toContain('function openCaptureOptsModal(');"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/expense.spec.js",
                            "column": 18,
                            "line": 51
                          },
                          "message": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoContain\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m) // indexOf\u001b[22m\n\nExpected substring: \u001b[32m\"function goToHub(\"\u001b[39m\nReceived string:    \u001b[31m\"<!DOCTYPE HTML PUBLIC \\\"-//W3C//DTD HTML 4.01//EN\\\" \\\"http://www.w3.org/TR/html4/strict.dtd\\\">\u001b[39m\n\u001b[31m<html><head>\u001b[39m\n\u001b[31m<title>404 Not Found</title>\u001b[39m\n\u001b[31m</head><body>\u001b[39m\n\u001b[31m<h1>Not Found</h1>\u001b[39m\n\u001b[31m<p>The requested URL was not found on this server.</p>\u001b[39m\n\u001b[31m</body></html>\u001b[39m\n\u001b[31m\"\u001b[39m\n\n  49 |\n  50 |   test('key functions present in source', () => {\n> 51 |     expect(html).toContain('function goToHub(');\n     |                  ^\n  52 |     expect(html).toContain('function submitSheet(');\n  53 |     expect(html).toContain('function loadApprovals(');\n  54 |     expect(html).toContain('function openCaptureOptsModal(');\n    at /var/www/360lm/tests/expense.spec.js:51:18"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 1,
                      "startTime": "2026-07-12T13:17:53.661Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/expense-Expenses-—-HTML-St-a9fb7-functions-present-in-source-desktop-chrome-retry1/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/expense.spec.js",
                        "column": 18,
                        "line": 51
                      }
                    }
                  ],
                  "status": "unexpected"
                }
              ],
              "id": "2399d47327fc24358745-4af52f23743a9ad7d894",
              "file": "expense.spec.js",
              "line": 50,
              "column": 3
            },
            {
              "title": "hub navigation is present",
              "ok": false,
              "tags": [],
              "tests": [
                {
                  "timeout": 30000,
                  "annotations": [],
                  "expectedStatus": "passed",
                  "projectId": "desktop-chrome",
                  "projectName": "desktop-chrome",
                  "results": [
                    {
                      "workerIndex": 186,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 10,
                      "error": {
                        "message": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoContain\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m) // indexOf\u001b[22m\n\nExpected substring: \u001b[32m\"goToHub\"\u001b[39m\nReceived string:    \u001b[31m\"<!DOCTYPE HTML PUBLIC \\\"-//W3C//DTD HTML 4.01//EN\\\" \\\"http://www.w3.org/TR/html4/strict.dtd\\\">\u001b[39m\n\u001b[31m<html><head>\u001b[39m\n\u001b[31m<title>404 Not Found</title>\u001b[39m\n\u001b[31m</head><body>\u001b[39m\n\u001b[31m<h1>Not Found</h1>\u001b[39m\n\u001b[31m<p>The requested URL was not found on this server.</p>\u001b[39m\n\u001b[31m</body></html>\u001b[39m\n\u001b[31m\"\u001b[39m",
                        "stack": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoContain\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m) // indexOf\u001b[22m\n\nExpected substring: \u001b[32m\"goToHub\"\u001b[39m\nReceived string:    \u001b[31m\"<!DOCTYPE HTML PUBLIC \\\"-//W3C//DTD HTML 4.01//EN\\\" \\\"http://www.w3.org/TR/html4/strict.dtd\\\">\u001b[39m\n\u001b[31m<html><head>\u001b[39m\n\u001b[31m<title>404 Not Found</title>\u001b[39m\n\u001b[31m</head><body>\u001b[39m\n\u001b[31m<h1>Not Found</h1>\u001b[39m\n\u001b[31m<p>The requested URL was not found on this server.</p>\u001b[39m\n\u001b[31m</body></html>\u001b[39m\n\u001b[31m\"\u001b[39m\n    at /var/www/360lm/tests/expense.spec.js:60:18",
                        "location": {
                          "file": "/var/www/360lm/tests/expense.spec.js",
                          "column": 18,
                          "line": 60
                        },
                        "snippet": "  58 |\n  59 |   test('hub navigation is present', () => {\n> 60 |     expect(html).toContain(\"goToHub\");\n     |                  ^\n  61 |     expect(html).toContain('🏡');\n  62 |   });\n  63 |"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/expense.spec.js",
                            "column": 18,
                            "line": 60
                          },
                          "message": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoContain\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m) // indexOf\u001b[22m\n\nExpected substring: \u001b[32m\"goToHub\"\u001b[39m\nReceived string:    \u001b[31m\"<!DOCTYPE HTML PUBLIC \\\"-//W3C//DTD HTML 4.01//EN\\\" \\\"http://www.w3.org/TR/html4/strict.dtd\\\">\u001b[39m\n\u001b[31m<html><head>\u001b[39m\n\u001b[31m<title>404 Not Found</title>\u001b[39m\n\u001b[31m</head><body>\u001b[39m\n\u001b[31m<h1>Not Found</h1>\u001b[39m\n\u001b[31m<p>The requested URL was not found on this server.</p>\u001b[39m\n\u001b[31m</body></html>\u001b[39m\n\u001b[31m\"\u001b[39m\n\n  58 |\n  59 |   test('hub navigation is present', () => {\n> 60 |     expect(html).toContain(\"goToHub\");\n     |                  ^\n  61 |     expect(html).toContain('🏡');\n  62 |   });\n  63 |\n    at /var/www/360lm/tests/expense.spec.js:60:18"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 0,
                      "startTime": "2026-07-12T13:17:54.628Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/expense-Expenses-—-HTML-Structure-hub-navigation-is-present-desktop-chrome/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/expense.spec.js",
                        "column": 18,
                        "line": 60
                      }
                    },
                    {
                      "workerIndex": 187,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 13,
                      "error": {
                        "message": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoContain\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m) // indexOf\u001b[22m\n\nExpected substring: \u001b[32m\"goToHub\"\u001b[39m\nReceived string:    \u001b[31m\"<!DOCTYPE HTML PUBLIC \\\"-//W3C//DTD HTML 4.01//EN\\\" \\\"http://www.w3.org/TR/html4/strict.dtd\\\">\u001b[39m\n\u001b[31m<html><head>\u001b[39m\n\u001b[31m<title>404 Not Found</title>\u001b[39m\n\u001b[31m</head><body>\u001b[39m\n\u001b[31m<h1>Not Found</h1>\u001b[39m\n\u001b[31m<p>The requested URL was not found on this server.</p>\u001b[39m\n\u001b[31m</body></html>\u001b[39m\n\u001b[31m\"\u001b[39m",
                        "stack": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoContain\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m) // indexOf\u001b[22m\n\nExpected substring: \u001b[32m\"goToHub\"\u001b[39m\nReceived string:    \u001b[31m\"<!DOCTYPE HTML PUBLIC \\\"-//W3C//DTD HTML 4.01//EN\\\" \\\"http://www.w3.org/TR/html4/strict.dtd\\\">\u001b[39m\n\u001b[31m<html><head>\u001b[39m\n\u001b[31m<title>404 Not Found</title>\u001b[39m\n\u001b[31m</head><body>\u001b[39m\n\u001b[31m<h1>Not Found</h1>\u001b[39m\n\u001b[31m<p>The requested URL was not found on this server.</p>\u001b[39m\n\u001b[31m</body></html>\u001b[39m\n\u001b[31m\"\u001b[39m\n    at /var/www/360lm/tests/expense.spec.js:60:18",
                        "location": {
                          "file": "/var/www/360lm/tests/expense.spec.js",
                          "column": 18,
                          "line": 60
                        },
                        "snippet": "  58 |\n  59 |   test('hub navigation is present', () => {\n> 60 |     expect(html).toContain(\"goToHub\");\n     |                  ^\n  61 |     expect(html).toContain('🏡');\n  62 |   });\n  63 |"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/expense.spec.js",
                            "column": 18,
                            "line": 60
                          },
                          "message": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoContain\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m) // indexOf\u001b[22m\n\nExpected substring: \u001b[32m\"goToHub\"\u001b[39m\nReceived string:    \u001b[31m\"<!DOCTYPE HTML PUBLIC \\\"-//W3C//DTD HTML 4.01//EN\\\" \\\"http://www.w3.org/TR/html4/strict.dtd\\\">\u001b[39m\n\u001b[31m<html><head>\u001b[39m\n\u001b[31m<title>404 Not Found</title>\u001b[39m\n\u001b[31m</head><body>\u001b[39m\n\u001b[31m<h1>Not Found</h1>\u001b[39m\n\u001b[31m<p>The requested URL was not found on this server.</p>\u001b[39m\n\u001b[31m</body></html>\u001b[39m\n\u001b[31m\"\u001b[39m\n\n  58 |\n  59 |   test('hub navigation is present', () => {\n> 60 |     expect(html).toContain(\"goToHub\");\n     |                  ^\n  61 |     expect(html).toContain('🏡');\n  62 |   });\n  63 |\n    at /var/www/360lm/tests/expense.spec.js:60:18"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 1,
                      "startTime": "2026-07-12T13:17:55.639Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/expense-Expenses-—-HTML-Structure-hub-navigation-is-present-desktop-chrome-retry1/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/expense.spec.js",
                        "column": 18,
                        "line": 60
                      }
                    }
                  ],
                  "status": "unexpected"
                }
              ],
              "id": "2399d47327fc24358745-7e3373e789c02c52b233",
              "file": "expense.spec.js",
              "line": 59,
              "column": 3
            },
            {
              "title": "push notification support present",
              "ok": false,
              "tags": [],
              "tests": [
                {
                  "timeout": 30000,
                  "annotations": [],
                  "expectedStatus": "passed",
                  "projectId": "desktop-chrome",
                  "projectName": "desktop-chrome",
                  "results": [
                    {
                      "workerIndex": 188,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 10,
                      "error": {
                        "message": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoContain\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m) // indexOf\u001b[22m\n\nExpected substring: \u001b[32m\"subscribePush(\"\u001b[39m\nReceived string:    \u001b[31m\"<!DOCTYPE HTML PUBLIC \\\"-//W3C//DTD HTML 4.01//EN\\\" \\\"http://www.w3.org/TR/html4/strict.dtd\\\">\u001b[39m\n\u001b[31m<html><head>\u001b[39m\n\u001b[31m<title>404 Not Found</title>\u001b[39m\n\u001b[31m</head><body>\u001b[39m\n\u001b[31m<h1>Not Found</h1>\u001b[39m\n\u001b[31m<p>The requested URL was not found on this server.</p>\u001b[39m\n\u001b[31m</body></html>\u001b[39m\n\u001b[31m\"\u001b[39m",
                        "stack": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoContain\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m) // indexOf\u001b[22m\n\nExpected substring: \u001b[32m\"subscribePush(\"\u001b[39m\nReceived string:    \u001b[31m\"<!DOCTYPE HTML PUBLIC \\\"-//W3C//DTD HTML 4.01//EN\\\" \\\"http://www.w3.org/TR/html4/strict.dtd\\\">\u001b[39m\n\u001b[31m<html><head>\u001b[39m\n\u001b[31m<title>404 Not Found</title>\u001b[39m\n\u001b[31m</head><body>\u001b[39m\n\u001b[31m<h1>Not Found</h1>\u001b[39m\n\u001b[31m<p>The requested URL was not found on this server.</p>\u001b[39m\n\u001b[31m</body></html>\u001b[39m\n\u001b[31m\"\u001b[39m\n    at /var/www/360lm/tests/expense.spec.js:65:18",
                        "location": {
                          "file": "/var/www/360lm/tests/expense.spec.js",
                          "column": 18,
                          "line": 65
                        },
                        "snippet": "  63 |\n  64 |   test('push notification support present', () => {\n> 65 |     expect(html).toContain('subscribePush(');\n     |                  ^\n  66 |     expect(html).toContain('notifyUser(');\n  67 |   });\n  68 |"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/expense.spec.js",
                            "column": 18,
                            "line": 65
                          },
                          "message": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoContain\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m) // indexOf\u001b[22m\n\nExpected substring: \u001b[32m\"subscribePush(\"\u001b[39m\nReceived string:    \u001b[31m\"<!DOCTYPE HTML PUBLIC \\\"-//W3C//DTD HTML 4.01//EN\\\" \\\"http://www.w3.org/TR/html4/strict.dtd\\\">\u001b[39m\n\u001b[31m<html><head>\u001b[39m\n\u001b[31m<title>404 Not Found</title>\u001b[39m\n\u001b[31m</head><body>\u001b[39m\n\u001b[31m<h1>Not Found</h1>\u001b[39m\n\u001b[31m<p>The requested URL was not found on this server.</p>\u001b[39m\n\u001b[31m</body></html>\u001b[39m\n\u001b[31m\"\u001b[39m\n\n  63 |\n  64 |   test('push notification support present', () => {\n> 65 |     expect(html).toContain('subscribePush(');\n     |                  ^\n  66 |     expect(html).toContain('notifyUser(');\n  67 |   });\n  68 |\n    at /var/www/360lm/tests/expense.spec.js:65:18"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 0,
                      "startTime": "2026-07-12T13:17:56.646Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/expense-Expenses-—-HTML-St-1d830-otification-support-present-desktop-chrome/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/expense.spec.js",
                        "column": 18,
                        "line": 65
                      }
                    },
                    {
                      "workerIndex": 189,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 19,
                      "error": {
                        "message": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoContain\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m) // indexOf\u001b[22m\n\nExpected substring: \u001b[32m\"subscribePush(\"\u001b[39m\nReceived string:    \u001b[31m\"<!DOCTYPE HTML PUBLIC \\\"-//W3C//DTD HTML 4.01//EN\\\" \\\"http://www.w3.org/TR/html4/strict.dtd\\\">\u001b[39m\n\u001b[31m<html><head>\u001b[39m\n\u001b[31m<title>404 Not Found</title>\u001b[39m\n\u001b[31m</head><body>\u001b[39m\n\u001b[31m<h1>Not Found</h1>\u001b[39m\n\u001b[31m<p>The requested URL was not found on this server.</p>\u001b[39m\n\u001b[31m</body></html>\u001b[39m\n\u001b[31m\"\u001b[39m",
                        "stack": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoContain\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m) // indexOf\u001b[22m\n\nExpected substring: \u001b[32m\"subscribePush(\"\u001b[39m\nReceived string:    \u001b[31m\"<!DOCTYPE HTML PUBLIC \\\"-//W3C//DTD HTML 4.01//EN\\\" \\\"http://www.w3.org/TR/html4/strict.dtd\\\">\u001b[39m\n\u001b[31m<html><head>\u001b[39m\n\u001b[31m<title>404 Not Found</title>\u001b[39m\n\u001b[31m</head><body>\u001b[39m\n\u001b[31m<h1>Not Found</h1>\u001b[39m\n\u001b[31m<p>The requested URL was not found on this server.</p>\u001b[39m\n\u001b[31m</body></html>\u001b[39m\n\u001b[31m\"\u001b[39m\n    at /var/www/360lm/tests/expense.spec.js:65:18",
                        "location": {
                          "file": "/var/www/360lm/tests/expense.spec.js",
                          "column": 18,
                          "line": 65
                        },
                        "snippet": "  63 |\n  64 |   test('push notification support present', () => {\n> 65 |     expect(html).toContain('subscribePush(');\n     |                  ^\n  66 |     expect(html).toContain('notifyUser(');\n  67 |   });\n  68 |"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/expense.spec.js",
                            "column": 18,
                            "line": 65
                          },
                          "message": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoContain\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m) // indexOf\u001b[22m\n\nExpected substring: \u001b[32m\"subscribePush(\"\u001b[39m\nReceived string:    \u001b[31m\"<!DOCTYPE HTML PUBLIC \\\"-//W3C//DTD HTML 4.01//EN\\\" \\\"http://www.w3.org/TR/html4/strict.dtd\\\">\u001b[39m\n\u001b[31m<html><head>\u001b[39m\n\u001b[31m<title>404 Not Found</title>\u001b[39m\n\u001b[31m</head><body>\u001b[39m\n\u001b[31m<h1>Not Found</h1>\u001b[39m\n\u001b[31m<p>The requested URL was not found on this server.</p>\u001b[39m\n\u001b[31m</body></html>\u001b[39m\n\u001b[31m\"\u001b[39m\n\n  63 |\n  64 |   test('push notification support present', () => {\n> 65 |     expect(html).toContain('subscribePush(');\n     |                  ^\n  66 |     expect(html).toContain('notifyUser(');\n  67 |   });\n  68 |\n    at /var/www/360lm/tests/expense.spec.js:65:18"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 1,
                      "startTime": "2026-07-12T13:17:57.564Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/expense-Expenses-—-HTML-St-1d830-otification-support-present-desktop-chrome-retry1/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/expense.spec.js",
                        "column": 18,
                        "line": 65
                      }
                    }
                  ],
                  "status": "unexpected"
                }
              ],
              "id": "2399d47327fc24358745-efdd0c31fb75f3eb1693",
              "file": "expense.spec.js",
              "line": 64,
              "column": 3
            },
            {
              "title": "custodian advances integration present",
              "ok": false,
              "tags": [],
              "tests": [
                {
                  "timeout": 30000,
                  "annotations": [],
                  "expectedStatus": "passed",
                  "projectId": "desktop-chrome",
                  "projectName": "desktop-chrome",
                  "results": [
                    {
                      "workerIndex": 190,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 18,
                      "error": {
                        "message": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoContain\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m) // indexOf\u001b[22m\n\nExpected substring: \u001b[32m\"loadAdvancesFromCustodian(\"\u001b[39m\nReceived string:    \u001b[31m\"<!DOCTYPE HTML PUBLIC \\\"-//W3C//DTD HTML 4.01//EN\\\" \\\"http://www.w3.org/TR/html4/strict.dtd\\\">\u001b[39m\n\u001b[31m<html><head>\u001b[39m\n\u001b[31m<title>404 Not Found</title>\u001b[39m\n\u001b[31m</head><body>\u001b[39m\n\u001b[31m<h1>Not Found</h1>\u001b[39m\n\u001b[31m<p>The requested URL was not found on this server.</p>\u001b[39m\n\u001b[31m</body></html>\u001b[39m\n\u001b[31m\"\u001b[39m",
                        "stack": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoContain\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m) // indexOf\u001b[22m\n\nExpected substring: \u001b[32m\"loadAdvancesFromCustodian(\"\u001b[39m\nReceived string:    \u001b[31m\"<!DOCTYPE HTML PUBLIC \\\"-//W3C//DTD HTML 4.01//EN\\\" \\\"http://www.w3.org/TR/html4/strict.dtd\\\">\u001b[39m\n\u001b[31m<html><head>\u001b[39m\n\u001b[31m<title>404 Not Found</title>\u001b[39m\n\u001b[31m</head><body>\u001b[39m\n\u001b[31m<h1>Not Found</h1>\u001b[39m\n\u001b[31m<p>The requested URL was not found on this server.</p>\u001b[39m\n\u001b[31m</body></html>\u001b[39m\n\u001b[31m\"\u001b[39m\n    at /var/www/360lm/tests/expense.spec.js:70:18",
                        "location": {
                          "file": "/var/www/360lm/tests/expense.spec.js",
                          "column": 18,
                          "line": 70
                        },
                        "snippet": "  68 |\n  69 |   test('custodian advances integration present', () => {\n> 70 |     expect(html).toContain('loadAdvancesFromCustodian(');\n     |                  ^\n  71 |     expect(html).toContain('renderAdvanceDashboardTiles(');\n  72 |   });\n  73 |"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/expense.spec.js",
                            "column": 18,
                            "line": 70
                          },
                          "message": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoContain\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m) // indexOf\u001b[22m\n\nExpected substring: \u001b[32m\"loadAdvancesFromCustodian(\"\u001b[39m\nReceived string:    \u001b[31m\"<!DOCTYPE HTML PUBLIC \\\"-//W3C//DTD HTML 4.01//EN\\\" \\\"http://www.w3.org/TR/html4/strict.dtd\\\">\u001b[39m\n\u001b[31m<html><head>\u001b[39m\n\u001b[31m<title>404 Not Found</title>\u001b[39m\n\u001b[31m</head><body>\u001b[39m\n\u001b[31m<h1>Not Found</h1>\u001b[39m\n\u001b[31m<p>The requested URL was not found on this server.</p>\u001b[39m\n\u001b[31m</body></html>\u001b[39m\n\u001b[31m\"\u001b[39m\n\n  68 |\n  69 |   test('custodian advances integration present', () => {\n> 70 |     expect(html).toContain('loadAdvancesFromCustodian(');\n     |                  ^\n  71 |     expect(html).toContain('renderAdvanceDashboardTiles(');\n  72 |   });\n  73 |\n    at /var/www/360lm/tests/expense.spec.js:70:18"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 0,
                      "startTime": "2026-07-12T13:17:58.605Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/expense-Expenses-—-HTML-St-dbba9-dvances-integration-present-desktop-chrome/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/expense.spec.js",
                        "column": 18,
                        "line": 70
                      }
                    },
                    {
                      "workerIndex": 191,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 13,
                      "error": {
                        "message": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoContain\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m) // indexOf\u001b[22m\n\nExpected substring: \u001b[32m\"loadAdvancesFromCustodian(\"\u001b[39m\nReceived string:    \u001b[31m\"<!DOCTYPE HTML PUBLIC \\\"-//W3C//DTD HTML 4.01//EN\\\" \\\"http://www.w3.org/TR/html4/strict.dtd\\\">\u001b[39m\n\u001b[31m<html><head>\u001b[39m\n\u001b[31m<title>404 Not Found</title>\u001b[39m\n\u001b[31m</head><body>\u001b[39m\n\u001b[31m<h1>Not Found</h1>\u001b[39m\n\u001b[31m<p>The requested URL was not found on this server.</p>\u001b[39m\n\u001b[31m</body></html>\u001b[39m\n\u001b[31m\"\u001b[39m",
                        "stack": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoContain\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m) // indexOf\u001b[22m\n\nExpected substring: \u001b[32m\"loadAdvancesFromCustodian(\"\u001b[39m\nReceived string:    \u001b[31m\"<!DOCTYPE HTML PUBLIC \\\"-//W3C//DTD HTML 4.01//EN\\\" \\\"http://www.w3.org/TR/html4/strict.dtd\\\">\u001b[39m\n\u001b[31m<html><head>\u001b[39m\n\u001b[31m<title>404 Not Found</title>\u001b[39m\n\u001b[31m</head><body>\u001b[39m\n\u001b[31m<h1>Not Found</h1>\u001b[39m\n\u001b[31m<p>The requested URL was not found on this server.</p>\u001b[39m\n\u001b[31m</body></html>\u001b[39m\n\u001b[31m\"\u001b[39m\n    at /var/www/360lm/tests/expense.spec.js:70:18",
                        "location": {
                          "file": "/var/www/360lm/tests/expense.spec.js",
                          "column": 18,
                          "line": 70
                        },
                        "snippet": "  68 |\n  69 |   test('custodian advances integration present', () => {\n> 70 |     expect(html).toContain('loadAdvancesFromCustodian(');\n     |                  ^\n  71 |     expect(html).toContain('renderAdvanceDashboardTiles(');\n  72 |   });\n  73 |"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/expense.spec.js",
                            "column": 18,
                            "line": 70
                          },
                          "message": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoContain\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m) // indexOf\u001b[22m\n\nExpected substring: \u001b[32m\"loadAdvancesFromCustodian(\"\u001b[39m\nReceived string:    \u001b[31m\"<!DOCTYPE HTML PUBLIC \\\"-//W3C//DTD HTML 4.01//EN\\\" \\\"http://www.w3.org/TR/html4/strict.dtd\\\">\u001b[39m\n\u001b[31m<html><head>\u001b[39m\n\u001b[31m<title>404 Not Found</title>\u001b[39m\n\u001b[31m</head><body>\u001b[39m\n\u001b[31m<h1>Not Found</h1>\u001b[39m\n\u001b[31m<p>The requested URL was not found on this server.</p>\u001b[39m\n\u001b[31m</body></html>\u001b[39m\n\u001b[31m\"\u001b[39m\n\n  68 |\n  69 |   test('custodian advances integration present', () => {\n> 70 |     expect(html).toContain('loadAdvancesFromCustodian(');\n     |                  ^\n  71 |     expect(html).toContain('renderAdvanceDashboardTiles(');\n  72 |   });\n  73 |\n    at /var/www/360lm/tests/expense.spec.js:70:18"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 1,
                      "startTime": "2026-07-12T13:17:59.647Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/expense-Expenses-—-HTML-St-dbba9-dvances-integration-present-desktop-chrome-retry1/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/expense.spec.js",
                        "column": 18,
                        "line": 70
                      }
                    }
                  ],
                  "status": "unexpected"
                }
              ],
              "id": "2399d47327fc24358745-14bcda3901c84ce603fd",
              "file": "expense.spec.js",
              "line": 69,
              "column": 3
            },
            {
              "title": "impress screen present for Impress Reports",
              "ok": false,
              "tags": [],
              "tests": [
                {
                  "timeout": 30000,
                  "annotations": [],
                  "expectedStatus": "passed",
                  "projectId": "desktop-chrome",
                  "projectName": "desktop-chrome",
                  "results": [
                    {
                      "workerIndex": 192,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 26,
                      "error": {
                        "message": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoContain\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m) // indexOf\u001b[22m\n\nExpected substring: \u001b[32m\"id=\\\"screen-impress\\\"\"\u001b[39m\nReceived string:    \u001b[31m\"<!DOCTYPE HTML PUBLIC \\\"-//W3C//DTD HTML 4.01//EN\\\" \\\"http://www.w3.org/TR/html4/strict.dtd\\\">\u001b[39m\n\u001b[31m<html><head>\u001b[39m\n\u001b[31m<title>404 Not Found</title>\u001b[39m\n\u001b[31m</head><body>\u001b[39m\n\u001b[31m<h1>Not Found</h1>\u001b[39m\n\u001b[31m<p>The requested URL was not found on this server.</p>\u001b[39m\n\u001b[31m</body></html>\u001b[39m\n\u001b[31m\"\u001b[39m",
                        "stack": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoContain\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m) // indexOf\u001b[22m\n\nExpected substring: \u001b[32m\"id=\\\"screen-impress\\\"\"\u001b[39m\nReceived string:    \u001b[31m\"<!DOCTYPE HTML PUBLIC \\\"-//W3C//DTD HTML 4.01//EN\\\" \\\"http://www.w3.org/TR/html4/strict.dtd\\\">\u001b[39m\n\u001b[31m<html><head>\u001b[39m\n\u001b[31m<title>404 Not Found</title>\u001b[39m\n\u001b[31m</head><body>\u001b[39m\n\u001b[31m<h1>Not Found</h1>\u001b[39m\n\u001b[31m<p>The requested URL was not found on this server.</p>\u001b[39m\n\u001b[31m</body></html>\u001b[39m\n\u001b[31m\"\u001b[39m\n    at /var/www/360lm/tests/expense.spec.js:75:18",
                        "location": {
                          "file": "/var/www/360lm/tests/expense.spec.js",
                          "column": 18,
                          "line": 75
                        },
                        "snippet": "  73 |\n  74 |   test('impress screen present for Impress Reports', () => {\n> 75 |     expect(html).toContain('id=\"screen-impress\"');\n     |                  ^\n  76 |   });\n  77 | });\n  78 |"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/expense.spec.js",
                            "column": 18,
                            "line": 75
                          },
                          "message": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoContain\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m) // indexOf\u001b[22m\n\nExpected substring: \u001b[32m\"id=\\\"screen-impress\\\"\"\u001b[39m\nReceived string:    \u001b[31m\"<!DOCTYPE HTML PUBLIC \\\"-//W3C//DTD HTML 4.01//EN\\\" \\\"http://www.w3.org/TR/html4/strict.dtd\\\">\u001b[39m\n\u001b[31m<html><head>\u001b[39m\n\u001b[31m<title>404 Not Found</title>\u001b[39m\n\u001b[31m</head><body>\u001b[39m\n\u001b[31m<h1>Not Found</h1>\u001b[39m\n\u001b[31m<p>The requested URL was not found on this server.</p>\u001b[39m\n\u001b[31m</body></html>\u001b[39m\n\u001b[31m\"\u001b[39m\n\n  73 |\n  74 |   test('impress screen present for Impress Reports', () => {\n> 75 |     expect(html).toContain('id=\"screen-impress\"');\n     |                  ^\n  76 |   });\n  77 | });\n  78 |\n    at /var/www/360lm/tests/expense.spec.js:75:18"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 0,
                      "startTime": "2026-07-12T13:18:00.841Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/expense-Expenses-—-HTML-St-135f5-present-for-Impress-Reports-desktop-chrome/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/expense.spec.js",
                        "column": 18,
                        "line": 75
                      }
                    },
                    {
                      "workerIndex": 193,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 15,
                      "error": {
                        "message": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoContain\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m) // indexOf\u001b[22m\n\nExpected substring: \u001b[32m\"id=\\\"screen-impress\\\"\"\u001b[39m\nReceived string:    \u001b[31m\"<!DOCTYPE HTML PUBLIC \\\"-//W3C//DTD HTML 4.01//EN\\\" \\\"http://www.w3.org/TR/html4/strict.dtd\\\">\u001b[39m\n\u001b[31m<html><head>\u001b[39m\n\u001b[31m<title>404 Not Found</title>\u001b[39m\n\u001b[31m</head><body>\u001b[39m\n\u001b[31m<h1>Not Found</h1>\u001b[39m\n\u001b[31m<p>The requested URL was not found on this server.</p>\u001b[39m\n\u001b[31m</body></html>\u001b[39m\n\u001b[31m\"\u001b[39m",
                        "stack": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoContain\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m) // indexOf\u001b[22m\n\nExpected substring: \u001b[32m\"id=\\\"screen-impress\\\"\"\u001b[39m\nReceived string:    \u001b[31m\"<!DOCTYPE HTML PUBLIC \\\"-//W3C//DTD HTML 4.01//EN\\\" \\\"http://www.w3.org/TR/html4/strict.dtd\\\">\u001b[39m\n\u001b[31m<html><head>\u001b[39m\n\u001b[31m<title>404 Not Found</title>\u001b[39m\n\u001b[31m</head><body>\u001b[39m\n\u001b[31m<h1>Not Found</h1>\u001b[39m\n\u001b[31m<p>The requested URL was not found on this server.</p>\u001b[39m\n\u001b[31m</body></html>\u001b[39m\n\u001b[31m\"\u001b[39m\n    at /var/www/360lm/tests/expense.spec.js:75:18",
                        "location": {
                          "file": "/var/www/360lm/tests/expense.spec.js",
                          "column": 18,
                          "line": 75
                        },
                        "snippet": "  73 |\n  74 |   test('impress screen present for Impress Reports', () => {\n> 75 |     expect(html).toContain('id=\"screen-impress\"');\n     |                  ^\n  76 |   });\n  77 | });\n  78 |"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/expense.spec.js",
                            "column": 18,
                            "line": 75
                          },
                          "message": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoContain\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m) // indexOf\u001b[22m\n\nExpected substring: \u001b[32m\"id=\\\"screen-impress\\\"\"\u001b[39m\nReceived string:    \u001b[31m\"<!DOCTYPE HTML PUBLIC \\\"-//W3C//DTD HTML 4.01//EN\\\" \\\"http://www.w3.org/TR/html4/strict.dtd\\\">\u001b[39m\n\u001b[31m<html><head>\u001b[39m\n\u001b[31m<title>404 Not Found</title>\u001b[39m\n\u001b[31m</head><body>\u001b[39m\n\u001b[31m<h1>Not Found</h1>\u001b[39m\n\u001b[31m<p>The requested URL was not found on this server.</p>\u001b[39m\n\u001b[31m</body></html>\u001b[39m\n\u001b[31m\"\u001b[39m\n\n  73 |\n  74 |   test('impress screen present for Impress Reports', () => {\n> 75 |     expect(html).toContain('id=\"screen-impress\"');\n     |                  ^\n  76 |   });\n  77 | });\n  78 |\n    at /var/www/360lm/tests/expense.spec.js:75:18"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 1,
                      "startTime": "2026-07-12T13:18:03.913Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/expense-Expenses-—-HTML-St-135f5-present-for-Impress-Reports-desktop-chrome-retry1/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/expense.spec.js",
                        "column": 18,
                        "line": 75
                      }
                    }
                  ],
                  "status": "unexpected"
                }
              ],
              "id": "2399d47327fc24358745-e4d36895cc6108ba501b",
              "file": "expense.spec.js",
              "line": 74,
              "column": 3
            }
          ]
        }
      ]
    },
    {
      "title": "hr.spec.js",
      "file": "hr.spec.js",
      "column": 0,
      "line": 0,
      "specs": [],
      "suites": [
        {
          "title": "HR Management — Access Control",
          "file": "hr.spec.js",
          "line": 17,
          "column": 6,
          "specs": [
            {
              "title": "no session → redirects to /hub/",
              "ok": false,
              "tags": [],
              "tests": [
                {
                  "timeout": 30000,
                  "annotations": [],
                  "expectedStatus": "passed",
                  "projectId": "android-chrome",
                  "projectName": "android-chrome",
                  "results": [
                    {
                      "workerIndex": 18,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 6531,
                      "error": {
                        "message": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mpage\u001b[39m\u001b[2m).\u001b[22mtoHaveURL\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m)\u001b[22m failed\n\nExpected pattern: \u001b[32m/\\/hub\\//\u001b[39m\nReceived string:  \u001b[31m\"https://srv1111289.hstgr.cloud/hr/\"\u001b[39m\nTimeout: 6000ms\n\nCall log:\n\u001b[2m  - Expect \"toHaveURL\" with timeout 6000ms\u001b[22m\n\u001b[2m    16 × unexpected value \"https://srv1111289.hstgr.cloud/hr/\"\u001b[22m\n",
                        "stack": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mpage\u001b[39m\u001b[2m).\u001b[22mtoHaveURL\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m)\u001b[22m failed\n\nExpected pattern: \u001b[32m/\\/hub\\//\u001b[39m\nReceived string:  \u001b[31m\"https://srv1111289.hstgr.cloud/hr/\"\u001b[39m\nTimeout: 6000ms\n\nCall log:\n\u001b[2m  - Expect \"toHaveURL\" with timeout 6000ms\u001b[22m\n\u001b[2m    16 × unexpected value \"https://srv1111289.hstgr.cloud/hr/\"\u001b[22m\n\n    at /var/www/360lm/tests/hr.spec.js:20:24",
                        "location": {
                          "file": "/var/www/360lm/tests/hr.spec.js",
                          "column": 24,
                          "line": 20
                        },
                        "snippet": "  18 |   test('no session → redirects to /hub/', async ({ page }) => {\n  19 |     await page.goto(BASE_URL);\n> 20 |     await expect(page).toHaveURL(/\\/hub\\//, { timeout: 6000 });\n     |                        ^\n  21 |   });\n  22 |\n  23 |   test('with valid session → home screen loads', async ({ page }) => {"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/hr.spec.js",
                            "column": 24,
                            "line": 20
                          },
                          "message": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mpage\u001b[39m\u001b[2m).\u001b[22mtoHaveURL\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m)\u001b[22m failed\n\nExpected pattern: \u001b[32m/\\/hub\\//\u001b[39m\nReceived string:  \u001b[31m\"https://srv1111289.hstgr.cloud/hr/\"\u001b[39m\nTimeout: 6000ms\n\nCall log:\n\u001b[2m  - Expect \"toHaveURL\" with timeout 6000ms\u001b[22m\n\u001b[2m    16 × unexpected value \"https://srv1111289.hstgr.cloud/hr/\"\u001b[22m\n\n\n  18 |   test('no session → redirects to /hub/', async ({ page }) => {\n  19 |     await page.goto(BASE_URL);\n> 20 |     await expect(page).toHaveURL(/\\/hub\\//, { timeout: 6000 });\n     |                        ^\n  21 |   });\n  22 |\n  23 |   test('with valid session → home screen loads', async ({ page }) => {\n    at /var/www/360lm/tests/hr.spec.js:20:24"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 0,
                      "startTime": "2026-07-12T12:57:50.206Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "screenshot",
                          "contentType": "image/png",
                          "path": "/var/www/360lm/test-results/hr-HR-Management-—-Access--6659e-session-→-redirects-to-hub--android-chrome/test-failed-1.png"
                        },
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/hr-HR-Management-—-Access--6659e-session-→-redirects-to-hub--android-chrome/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/hr.spec.js",
                        "column": 24,
                        "line": 20
                      }
                    },
                    {
                      "workerIndex": 19,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 6944,
                      "error": {
                        "message": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mpage\u001b[39m\u001b[2m).\u001b[22mtoHaveURL\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m)\u001b[22m failed\n\nExpected pattern: \u001b[32m/\\/hub\\//\u001b[39m\nReceived string:  \u001b[31m\"https://srv1111289.hstgr.cloud/hr/\"\u001b[39m\nTimeout: 6000ms\n\nCall log:\n\u001b[2m  - Expect \"toHaveURL\" with timeout 6000ms\u001b[22m\n\u001b[2m    15 × unexpected value \"https://srv1111289.hstgr.cloud/hr/\"\u001b[22m\n",
                        "stack": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mpage\u001b[39m\u001b[2m).\u001b[22mtoHaveURL\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m)\u001b[22m failed\n\nExpected pattern: \u001b[32m/\\/hub\\//\u001b[39m\nReceived string:  \u001b[31m\"https://srv1111289.hstgr.cloud/hr/\"\u001b[39m\nTimeout: 6000ms\n\nCall log:\n\u001b[2m  - Expect \"toHaveURL\" with timeout 6000ms\u001b[22m\n\u001b[2m    15 × unexpected value \"https://srv1111289.hstgr.cloud/hr/\"\u001b[22m\n\n    at /var/www/360lm/tests/hr.spec.js:20:24",
                        "location": {
                          "file": "/var/www/360lm/tests/hr.spec.js",
                          "column": 24,
                          "line": 20
                        },
                        "snippet": "  18 |   test('no session → redirects to /hub/', async ({ page }) => {\n  19 |     await page.goto(BASE_URL);\n> 20 |     await expect(page).toHaveURL(/\\/hub\\//, { timeout: 6000 });\n     |                        ^\n  21 |   });\n  22 |\n  23 |   test('with valid session → home screen loads', async ({ page }) => {"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/hr.spec.js",
                            "column": 24,
                            "line": 20
                          },
                          "message": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mpage\u001b[39m\u001b[2m).\u001b[22mtoHaveURL\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m)\u001b[22m failed\n\nExpected pattern: \u001b[32m/\\/hub\\//\u001b[39m\nReceived string:  \u001b[31m\"https://srv1111289.hstgr.cloud/hr/\"\u001b[39m\nTimeout: 6000ms\n\nCall log:\n\u001b[2m  - Expect \"toHaveURL\" with timeout 6000ms\u001b[22m\n\u001b[2m    15 × unexpected value \"https://srv1111289.hstgr.cloud/hr/\"\u001b[22m\n\n\n  18 |   test('no session → redirects to /hub/', async ({ page }) => {\n  19 |     await page.goto(BASE_URL);\n> 20 |     await expect(page).toHaveURL(/\\/hub\\//, { timeout: 6000 });\n     |                        ^\n  21 |   });\n  22 |\n  23 |   test('with valid session → home screen loads', async ({ page }) => {\n    at /var/www/360lm/tests/hr.spec.js:20:24"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 1,
                      "startTime": "2026-07-12T12:57:59.993Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "screenshot",
                          "contentType": "image/png",
                          "path": "/var/www/360lm/test-results/hr-HR-Management-—-Access--6659e-session-→-redirects-to-hub--android-chrome-retry1/test-failed-1.png"
                        },
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/hr-HR-Management-—-Access--6659e-session-→-redirects-to-hub--android-chrome-retry1/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/hr.spec.js",
                        "column": 24,
                        "line": 20
                      }
                    }
                  ],
                  "status": "unexpected"
                }
              ],
              "id": "98264c29bbadd0584463-93f8841e1f49eb7621a4",
              "file": "hr.spec.js",
              "line": 18,
              "column": 3
            },
            {
              "title": "with valid session → home screen loads",
              "ok": false,
              "tags": [],
              "tests": [
                {
                  "timeout": 30000,
                  "annotations": [],
                  "expectedStatus": "passed",
                  "projectId": "android-chrome",
                  "projectName": "android-chrome",
                  "results": [
                    {
                      "workerIndex": 20,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 8275,
                      "error": {
                        "message": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mlocator\u001b[39m\u001b[2m).\u001b[22mtoBeVisible\u001b[2m(\u001b[22m\u001b[2m)\u001b[22m failed\n\nLocator: locator('#scr-home.active')\nExpected: visible\nTimeout: 8000ms\nError: element(s) not found\n\nCall log:\n\u001b[2m  - Expect \"toBeVisible\" with timeout 8000ms\u001b[22m\n\u001b[2m  - waiting for locator('#scr-home.active')\u001b[22m\n",
                        "stack": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mlocator\u001b[39m\u001b[2m).\u001b[22mtoBeVisible\u001b[2m(\u001b[22m\u001b[2m)\u001b[22m failed\n\nLocator: locator('#scr-home.active')\nExpected: visible\nTimeout: 8000ms\nError: element(s) not found\n\nCall log:\n\u001b[2m  - Expect \"toBeVisible\" with timeout 8000ms\u001b[22m\n\u001b[2m  - waiting for locator('#scr-home.active')\u001b[22m\n\n    at /var/www/360lm/tests/hr.spec.js:26:52",
                        "location": {
                          "file": "/var/www/360lm/tests/hr.spec.js",
                          "column": 52,
                          "line": 26
                        },
                        "snippet": "  24 |     await injectSession(page);\n  25 |     await page.goto(BASE_URL);\n> 26 |     await expect(page.locator('#scr-home.active')).toBeVisible({ timeout: 8000 });\n     |                                                    ^\n  27 |   });\n  28 | });\n  29 |"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/hr.spec.js",
                            "column": 52,
                            "line": 26
                          },
                          "message": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mlocator\u001b[39m\u001b[2m).\u001b[22mtoBeVisible\u001b[2m(\u001b[22m\u001b[2m)\u001b[22m failed\n\nLocator: locator('#scr-home.active')\nExpected: visible\nTimeout: 8000ms\nError: element(s) not found\n\nCall log:\n\u001b[2m  - Expect \"toBeVisible\" with timeout 8000ms\u001b[22m\n\u001b[2m  - waiting for locator('#scr-home.active')\u001b[22m\n\n\n  24 |     await injectSession(page);\n  25 |     await page.goto(BASE_URL);\n> 26 |     await expect(page.locator('#scr-home.active')).toBeVisible({ timeout: 8000 });\n     |                                                    ^\n  27 |   });\n  28 | });\n  29 |\n    at /var/www/360lm/tests/hr.spec.js:26:52"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 0,
                      "startTime": "2026-07-12T12:58:08.560Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "screenshot",
                          "contentType": "image/png",
                          "path": "/var/www/360lm/test-results/hr-HR-Management-—-Access--f09fc-session-→-home-screen-loads-android-chrome/test-failed-1.png"
                        },
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/hr-HR-Management-—-Access--f09fc-session-→-home-screen-loads-android-chrome/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/hr.spec.js",
                        "column": 52,
                        "line": 26
                      }
                    },
                    {
                      "workerIndex": 21,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 8309,
                      "error": {
                        "message": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mlocator\u001b[39m\u001b[2m).\u001b[22mtoBeVisible\u001b[2m(\u001b[22m\u001b[2m)\u001b[22m failed\n\nLocator: locator('#scr-home.active')\nExpected: visible\nTimeout: 8000ms\nError: element(s) not found\n\nCall log:\n\u001b[2m  - Expect \"toBeVisible\" with timeout 8000ms\u001b[22m\n\u001b[2m  - waiting for locator('#scr-home.active')\u001b[22m\n",
                        "stack": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mlocator\u001b[39m\u001b[2m).\u001b[22mtoBeVisible\u001b[2m(\u001b[22m\u001b[2m)\u001b[22m failed\n\nLocator: locator('#scr-home.active')\nExpected: visible\nTimeout: 8000ms\nError: element(s) not found\n\nCall log:\n\u001b[2m  - Expect \"toBeVisible\" with timeout 8000ms\u001b[22m\n\u001b[2m  - waiting for locator('#scr-home.active')\u001b[22m\n\n    at /var/www/360lm/tests/hr.spec.js:26:52",
                        "location": {
                          "file": "/var/www/360lm/tests/hr.spec.js",
                          "column": 52,
                          "line": 26
                        },
                        "snippet": "  24 |     await injectSession(page);\n  25 |     await page.goto(BASE_URL);\n> 26 |     await expect(page.locator('#scr-home.active')).toBeVisible({ timeout: 8000 });\n     |                                                    ^\n  27 |   });\n  28 | });\n  29 |"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/hr.spec.js",
                            "column": 52,
                            "line": 26
                          },
                          "message": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mlocator\u001b[39m\u001b[2m).\u001b[22mtoBeVisible\u001b[2m(\u001b[22m\u001b[2m)\u001b[22m failed\n\nLocator: locator('#scr-home.active')\nExpected: visible\nTimeout: 8000ms\nError: element(s) not found\n\nCall log:\n\u001b[2m  - Expect \"toBeVisible\" with timeout 8000ms\u001b[22m\n\u001b[2m  - waiting for locator('#scr-home.active')\u001b[22m\n\n\n  24 |     await injectSession(page);\n  25 |     await page.goto(BASE_URL);\n> 26 |     await expect(page.locator('#scr-home.active')).toBeVisible({ timeout: 8000 });\n     |                                                    ^\n  27 |   });\n  28 | });\n  29 |\n    at /var/www/360lm/tests/hr.spec.js:26:52"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 1,
                      "startTime": "2026-07-12T12:58:17.868Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "screenshot",
                          "contentType": "image/png",
                          "path": "/var/www/360lm/test-results/hr-HR-Management-—-Access--f09fc-session-→-home-screen-loads-android-chrome-retry1/test-failed-1.png"
                        },
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/hr-HR-Management-—-Access--f09fc-session-→-home-screen-loads-android-chrome-retry1/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/hr.spec.js",
                        "column": 52,
                        "line": 26
                      }
                    }
                  ],
                  "status": "unexpected"
                }
              ],
              "id": "98264c29bbadd0584463-a154d0175ca310e5fb89",
              "file": "hr.spec.js",
              "line": 23,
              "column": 3
            },
            {
              "title": "no session → redirects to /hub/",
              "ok": false,
              "tags": [],
              "tests": [
                {
                  "timeout": 30000,
                  "annotations": [],
                  "expectedStatus": "passed",
                  "projectId": "desktop-chrome",
                  "projectName": "desktop-chrome",
                  "results": [
                    {
                      "workerIndex": 194,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 6361,
                      "error": {
                        "message": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mpage\u001b[39m\u001b[2m).\u001b[22mtoHaveURL\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m)\u001b[22m failed\n\nExpected pattern: \u001b[32m/\\/hub\\//\u001b[39m\nReceived string:  \u001b[31m\"https://srv1111289.hstgr.cloud/hr/\"\u001b[39m\nTimeout: 6000ms\n\nCall log:\n\u001b[2m  - Expect \"toHaveURL\" with timeout 6000ms\u001b[22m\n\u001b[2m    16 × unexpected value \"https://srv1111289.hstgr.cloud/hr/\"\u001b[22m\n",
                        "stack": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mpage\u001b[39m\u001b[2m).\u001b[22mtoHaveURL\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m)\u001b[22m failed\n\nExpected pattern: \u001b[32m/\\/hub\\//\u001b[39m\nReceived string:  \u001b[31m\"https://srv1111289.hstgr.cloud/hr/\"\u001b[39m\nTimeout: 6000ms\n\nCall log:\n\u001b[2m  - Expect \"toHaveURL\" with timeout 6000ms\u001b[22m\n\u001b[2m    16 × unexpected value \"https://srv1111289.hstgr.cloud/hr/\"\u001b[22m\n\n    at /var/www/360lm/tests/hr.spec.js:20:24",
                        "location": {
                          "file": "/var/www/360lm/tests/hr.spec.js",
                          "column": 24,
                          "line": 20
                        },
                        "snippet": "  18 |   test('no session → redirects to /hub/', async ({ page }) => {\n  19 |     await page.goto(BASE_URL);\n> 20 |     await expect(page).toHaveURL(/\\/hub\\//, { timeout: 6000 });\n     |                        ^\n  21 |   });\n  22 |\n  23 |   test('with valid session → home screen loads', async ({ page }) => {"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/hr.spec.js",
                            "column": 24,
                            "line": 20
                          },
                          "message": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mpage\u001b[39m\u001b[2m).\u001b[22mtoHaveURL\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m)\u001b[22m failed\n\nExpected pattern: \u001b[32m/\\/hub\\//\u001b[39m\nReceived string:  \u001b[31m\"https://srv1111289.hstgr.cloud/hr/\"\u001b[39m\nTimeout: 6000ms\n\nCall log:\n\u001b[2m  - Expect \"toHaveURL\" with timeout 6000ms\u001b[22m\n\u001b[2m    16 × unexpected value \"https://srv1111289.hstgr.cloud/hr/\"\u001b[22m\n\n\n  18 |   test('no session → redirects to /hub/', async ({ page }) => {\n  19 |     await page.goto(BASE_URL);\n> 20 |     await expect(page).toHaveURL(/\\/hub\\//, { timeout: 6000 });\n     |                        ^\n  21 |   });\n  22 |\n  23 |   test('with valid session → home screen loads', async ({ page }) => {\n    at /var/www/360lm/tests/hr.spec.js:20:24"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 0,
                      "startTime": "2026-07-12T13:18:05.481Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "screenshot",
                          "contentType": "image/png",
                          "path": "/var/www/360lm/test-results/hr-HR-Management-—-Access--6659e-session-→-redirects-to-hub--desktop-chrome/test-failed-1.png"
                        },
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/hr-HR-Management-—-Access--6659e-session-→-redirects-to-hub--desktop-chrome/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/hr.spec.js",
                        "column": 24,
                        "line": 20
                      }
                    },
                    {
                      "workerIndex": 195,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 6348,
                      "error": {
                        "message": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mpage\u001b[39m\u001b[2m).\u001b[22mtoHaveURL\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m)\u001b[22m failed\n\nExpected pattern: \u001b[32m/\\/hub\\//\u001b[39m\nReceived string:  \u001b[31m\"https://srv1111289.hstgr.cloud/hr/\"\u001b[39m\nTimeout: 6000ms\n\nCall log:\n\u001b[2m  - Expect \"toHaveURL\" with timeout 6000ms\u001b[22m\n\u001b[2m    16 × unexpected value \"https://srv1111289.hstgr.cloud/hr/\"\u001b[22m\n",
                        "stack": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mpage\u001b[39m\u001b[2m).\u001b[22mtoHaveURL\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m)\u001b[22m failed\n\nExpected pattern: \u001b[32m/\\/hub\\//\u001b[39m\nReceived string:  \u001b[31m\"https://srv1111289.hstgr.cloud/hr/\"\u001b[39m\nTimeout: 6000ms\n\nCall log:\n\u001b[2m  - Expect \"toHaveURL\" with timeout 6000ms\u001b[22m\n\u001b[2m    16 × unexpected value \"https://srv1111289.hstgr.cloud/hr/\"\u001b[22m\n\n    at /var/www/360lm/tests/hr.spec.js:20:24",
                        "location": {
                          "file": "/var/www/360lm/tests/hr.spec.js",
                          "column": 24,
                          "line": 20
                        },
                        "snippet": "  18 |   test('no session → redirects to /hub/', async ({ page }) => {\n  19 |     await page.goto(BASE_URL);\n> 20 |     await expect(page).toHaveURL(/\\/hub\\//, { timeout: 6000 });\n     |                        ^\n  21 |   });\n  22 |\n  23 |   test('with valid session → home screen loads', async ({ page }) => {"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/hr.spec.js",
                            "column": 24,
                            "line": 20
                          },
                          "message": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mpage\u001b[39m\u001b[2m).\u001b[22mtoHaveURL\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m)\u001b[22m failed\n\nExpected pattern: \u001b[32m/\\/hub\\//\u001b[39m\nReceived string:  \u001b[31m\"https://srv1111289.hstgr.cloud/hr/\"\u001b[39m\nTimeout: 6000ms\n\nCall log:\n\u001b[2m  - Expect \"toHaveURL\" with timeout 6000ms\u001b[22m\n\u001b[2m    16 × unexpected value \"https://srv1111289.hstgr.cloud/hr/\"\u001b[22m\n\n\n  18 |   test('no session → redirects to /hub/', async ({ page }) => {\n  19 |     await page.goto(BASE_URL);\n> 20 |     await expect(page).toHaveURL(/\\/hub\\//, { timeout: 6000 });\n     |                        ^\n  21 |   });\n  22 |\n  23 |   test('with valid session → home screen loads', async ({ page }) => {\n    at /var/www/360lm/tests/hr.spec.js:20:24"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 1,
                      "startTime": "2026-07-12T13:18:13.062Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "screenshot",
                          "contentType": "image/png",
                          "path": "/var/www/360lm/test-results/hr-HR-Management-—-Access--6659e-session-→-redirects-to-hub--desktop-chrome-retry1/test-failed-1.png"
                        },
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/hr-HR-Management-—-Access--6659e-session-→-redirects-to-hub--desktop-chrome-retry1/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/hr.spec.js",
                        "column": 24,
                        "line": 20
                      }
                    }
                  ],
                  "status": "unexpected"
                }
              ],
              "id": "98264c29bbadd0584463-98f1919840e230f3f0a8",
              "file": "hr.spec.js",
              "line": 18,
              "column": 3
            },
            {
              "title": "with valid session → home screen loads",
              "ok": false,
              "tags": [],
              "tests": [
                {
                  "timeout": 30000,
                  "annotations": [],
                  "expectedStatus": "passed",
                  "projectId": "desktop-chrome",
                  "projectName": "desktop-chrome",
                  "results": [
                    {
                      "workerIndex": 196,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 8341,
                      "error": {
                        "message": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mlocator\u001b[39m\u001b[2m).\u001b[22mtoBeVisible\u001b[2m(\u001b[22m\u001b[2m)\u001b[22m failed\n\nLocator: locator('#scr-home.active')\nExpected: visible\nTimeout: 8000ms\nError: element(s) not found\n\nCall log:\n\u001b[2m  - Expect \"toBeVisible\" with timeout 8000ms\u001b[22m\n\u001b[2m  - waiting for locator('#scr-home.active')\u001b[22m\n",
                        "stack": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mlocator\u001b[39m\u001b[2m).\u001b[22mtoBeVisible\u001b[2m(\u001b[22m\u001b[2m)\u001b[22m failed\n\nLocator: locator('#scr-home.active')\nExpected: visible\nTimeout: 8000ms\nError: element(s) not found\n\nCall log:\n\u001b[2m  - Expect \"toBeVisible\" with timeout 8000ms\u001b[22m\n\u001b[2m  - waiting for locator('#scr-home.active')\u001b[22m\n\n    at /var/www/360lm/tests/hr.spec.js:26:52",
                        "location": {
                          "file": "/var/www/360lm/tests/hr.spec.js",
                          "column": 52,
                          "line": 26
                        },
                        "snippet": "  24 |     await injectSession(page);\n  25 |     await page.goto(BASE_URL);\n> 26 |     await expect(page.locator('#scr-home.active')).toBeVisible({ timeout: 8000 });\n     |                                                    ^\n  27 |   });\n  28 | });\n  29 |"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/hr.spec.js",
                            "column": 52,
                            "line": 26
                          },
                          "message": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mlocator\u001b[39m\u001b[2m).\u001b[22mtoBeVisible\u001b[2m(\u001b[22m\u001b[2m)\u001b[22m failed\n\nLocator: locator('#scr-home.active')\nExpected: visible\nTimeout: 8000ms\nError: element(s) not found\n\nCall log:\n\u001b[2m  - Expect \"toBeVisible\" with timeout 8000ms\u001b[22m\n\u001b[2m  - waiting for locator('#scr-home.active')\u001b[22m\n\n\n  24 |     await injectSession(page);\n  25 |     await page.goto(BASE_URL);\n> 26 |     await expect(page.locator('#scr-home.active')).toBeVisible({ timeout: 8000 });\n     |                                                    ^\n  27 |   });\n  28 | });\n  29 |\n    at /var/www/360lm/tests/hr.spec.js:26:52"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 0,
                      "startTime": "2026-07-12T13:18:20.605Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "screenshot",
                          "contentType": "image/png",
                          "path": "/var/www/360lm/test-results/hr-HR-Management-—-Access--f09fc-session-→-home-screen-loads-desktop-chrome/test-failed-1.png"
                        },
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/hr-HR-Management-—-Access--f09fc-session-→-home-screen-loads-desktop-chrome/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/hr.spec.js",
                        "column": 52,
                        "line": 26
                      }
                    },
                    {
                      "workerIndex": 197,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 8301,
                      "error": {
                        "message": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mlocator\u001b[39m\u001b[2m).\u001b[22mtoBeVisible\u001b[2m(\u001b[22m\u001b[2m)\u001b[22m failed\n\nLocator: locator('#scr-home.active')\nExpected: visible\nTimeout: 8000ms\nError: element(s) not found\n\nCall log:\n\u001b[2m  - Expect \"toBeVisible\" with timeout 8000ms\u001b[22m\n\u001b[2m  - waiting for locator('#scr-home.active')\u001b[22m\n",
                        "stack": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mlocator\u001b[39m\u001b[2m).\u001b[22mtoBeVisible\u001b[2m(\u001b[22m\u001b[2m)\u001b[22m failed\n\nLocator: locator('#scr-home.active')\nExpected: visible\nTimeout: 8000ms\nError: element(s) not found\n\nCall log:\n\u001b[2m  - Expect \"toBeVisible\" with timeout 8000ms\u001b[22m\n\u001b[2m  - waiting for locator('#scr-home.active')\u001b[22m\n\n    at /var/www/360lm/tests/hr.spec.js:26:52",
                        "location": {
                          "file": "/var/www/360lm/tests/hr.spec.js",
                          "column": 52,
                          "line": 26
                        },
                        "snippet": "  24 |     await injectSession(page);\n  25 |     await page.goto(BASE_URL);\n> 26 |     await expect(page.locator('#scr-home.active')).toBeVisible({ timeout: 8000 });\n     |                                                    ^\n  27 |   });\n  28 | });\n  29 |"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/hr.spec.js",
                            "column": 52,
                            "line": 26
                          },
                          "message": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mlocator\u001b[39m\u001b[2m).\u001b[22mtoBeVisible\u001b[2m(\u001b[22m\u001b[2m)\u001b[22m failed\n\nLocator: locator('#scr-home.active')\nExpected: visible\nTimeout: 8000ms\nError: element(s) not found\n\nCall log:\n\u001b[2m  - Expect \"toBeVisible\" with timeout 8000ms\u001b[22m\n\u001b[2m  - waiting for locator('#scr-home.active')\u001b[22m\n\n\n  24 |     await injectSession(page);\n  25 |     await page.goto(BASE_URL);\n> 26 |     await expect(page.locator('#scr-home.active')).toBeVisible({ timeout: 8000 });\n     |                                                    ^\n  27 |   });\n  28 | });\n  29 |\n    at /var/www/360lm/tests/hr.spec.js:26:52"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 1,
                      "startTime": "2026-07-12T13:18:30.092Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "screenshot",
                          "contentType": "image/png",
                          "path": "/var/www/360lm/test-results/hr-HR-Management-—-Access--f09fc-session-→-home-screen-loads-desktop-chrome-retry1/test-failed-1.png"
                        },
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/hr-HR-Management-—-Access--f09fc-session-→-home-screen-loads-desktop-chrome-retry1/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/hr.spec.js",
                        "column": 52,
                        "line": 26
                      }
                    }
                  ],
                  "status": "unexpected"
                }
              ],
              "id": "98264c29bbadd0584463-d1caa549fb073e2fb62d",
              "file": "hr.spec.js",
              "line": 23,
              "column": 3
            }
          ]
        },
        {
          "title": "HR Management — User Bar: Topbar Screens",
          "file": "hr.spec.js",
          "line": 30,
          "column": 6,
          "specs": [
            {
              "title": "home topbar shows user name",
              "ok": false,
              "tags": [],
              "tests": [
                {
                  "timeout": 30000,
                  "annotations": [],
                  "expectedStatus": "passed",
                  "projectId": "android-chrome",
                  "projectName": "android-chrome",
                  "results": [
                    {
                      "workerIndex": 22,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 8249,
                      "error": {
                        "message": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mlocator\u001b[39m\u001b[2m).\u001b[22mtoHaveText\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m)\u001b[22m failed\n\nLocator: locator('#scr-home .topbar .topbar-user')\nExpected pattern: \u001b[32m/Harish/\u001b[39m\nTimeout: 8000ms\nError: element(s) not found\n\nCall log:\n\u001b[2m  - Expect \"toHaveText\" with timeout 8000ms\u001b[22m\n\u001b[2m  - waiting for locator('#scr-home .topbar .topbar-user')\u001b[22m\n",
                        "stack": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mlocator\u001b[39m\u001b[2m).\u001b[22mtoHaveText\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m)\u001b[22m failed\n\nLocator: locator('#scr-home .topbar .topbar-user')\nExpected pattern: \u001b[32m/Harish/\u001b[39m\nTimeout: 8000ms\nError: element(s) not found\n\nCall log:\n\u001b[2m  - Expect \"toHaveText\" with timeout 8000ms\u001b[22m\n\u001b[2m  - waiting for locator('#scr-home .topbar .topbar-user')\u001b[22m\n\n    at /var/www/360lm/tests/hr.spec.js:37:66",
                        "location": {
                          "file": "/var/www/360lm/tests/hr.spec.js",
                          "column": 66,
                          "line": 37
                        },
                        "snippet": "  35 |   test('home topbar shows user name', async ({ page }) => {\n  36 |     await page.goto(BASE_URL);\n> 37 |     await expect(page.locator('#scr-home .topbar .topbar-user')).toHaveText(/Harish/, { timeout: 8000 });\n     |                                                                  ^\n  38 |   });\n  39 |\n  40 |   test('home topbar has Logout button', async ({ page }) => {"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/hr.spec.js",
                            "column": 66,
                            "line": 37
                          },
                          "message": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mlocator\u001b[39m\u001b[2m).\u001b[22mtoHaveText\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m)\u001b[22m failed\n\nLocator: locator('#scr-home .topbar .topbar-user')\nExpected pattern: \u001b[32m/Harish/\u001b[39m\nTimeout: 8000ms\nError: element(s) not found\n\nCall log:\n\u001b[2m  - Expect \"toHaveText\" with timeout 8000ms\u001b[22m\n\u001b[2m  - waiting for locator('#scr-home .topbar .topbar-user')\u001b[22m\n\n\n  35 |   test('home topbar shows user name', async ({ page }) => {\n  36 |     await page.goto(BASE_URL);\n> 37 |     await expect(page.locator('#scr-home .topbar .topbar-user')).toHaveText(/Harish/, { timeout: 8000 });\n     |                                                                  ^\n  38 |   });\n  39 |\n  40 |   test('home topbar has Logout button', async ({ page }) => {\n    at /var/www/360lm/tests/hr.spec.js:37:66"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 0,
                      "startTime": "2026-07-12T12:58:27.346Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "screenshot",
                          "contentType": "image/png",
                          "path": "/var/www/360lm/test-results/hr-HR-Management-—-User-Ba-38556-home-topbar-shows-user-name-android-chrome/test-failed-1.png"
                        },
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/hr-HR-Management-—-User-Ba-38556-home-topbar-shows-user-name-android-chrome/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/hr.spec.js",
                        "column": 66,
                        "line": 37
                      }
                    },
                    {
                      "workerIndex": 23,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 8265,
                      "error": {
                        "message": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mlocator\u001b[39m\u001b[2m).\u001b[22mtoHaveText\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m)\u001b[22m failed\n\nLocator: locator('#scr-home .topbar .topbar-user')\nExpected pattern: \u001b[32m/Harish/\u001b[39m\nTimeout: 8000ms\nError: element(s) not found\n\nCall log:\n\u001b[2m  - Expect \"toHaveText\" with timeout 8000ms\u001b[22m\n\u001b[2m  - waiting for locator('#scr-home .topbar .topbar-user')\u001b[22m\n",
                        "stack": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mlocator\u001b[39m\u001b[2m).\u001b[22mtoHaveText\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m)\u001b[22m failed\n\nLocator: locator('#scr-home .topbar .topbar-user')\nExpected pattern: \u001b[32m/Harish/\u001b[39m\nTimeout: 8000ms\nError: element(s) not found\n\nCall log:\n\u001b[2m  - Expect \"toHaveText\" with timeout 8000ms\u001b[22m\n\u001b[2m  - waiting for locator('#scr-home .topbar .topbar-user')\u001b[22m\n\n    at /var/www/360lm/tests/hr.spec.js:37:66",
                        "location": {
                          "file": "/var/www/360lm/tests/hr.spec.js",
                          "column": 66,
                          "line": 37
                        },
                        "snippet": "  35 |   test('home topbar shows user name', async ({ page }) => {\n  36 |     await page.goto(BASE_URL);\n> 37 |     await expect(page.locator('#scr-home .topbar .topbar-user')).toHaveText(/Harish/, { timeout: 8000 });\n     |                                                                  ^\n  38 |   });\n  39 |\n  40 |   test('home topbar has Logout button', async ({ page }) => {"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/hr.spec.js",
                            "column": 66,
                            "line": 37
                          },
                          "message": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mlocator\u001b[39m\u001b[2m).\u001b[22mtoHaveText\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m)\u001b[22m failed\n\nLocator: locator('#scr-home .topbar .topbar-user')\nExpected pattern: \u001b[32m/Harish/\u001b[39m\nTimeout: 8000ms\nError: element(s) not found\n\nCall log:\n\u001b[2m  - Expect \"toHaveText\" with timeout 8000ms\u001b[22m\n\u001b[2m  - waiting for locator('#scr-home .topbar .topbar-user')\u001b[22m\n\n\n  35 |   test('home topbar shows user name', async ({ page }) => {\n  36 |     await page.goto(BASE_URL);\n> 37 |     await expect(page.locator('#scr-home .topbar .topbar-user')).toHaveText(/Harish/, { timeout: 8000 });\n     |                                                                  ^\n  38 |   });\n  39 |\n  40 |   test('home topbar has Logout button', async ({ page }) => {\n    at /var/www/360lm/tests/hr.spec.js:37:66"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 1,
                      "startTime": "2026-07-12T12:58:36.607Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "screenshot",
                          "contentType": "image/png",
                          "path": "/var/www/360lm/test-results/hr-HR-Management-—-User-Ba-38556-home-topbar-shows-user-name-android-chrome-retry1/test-failed-1.png"
                        },
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/hr-HR-Management-—-User-Ba-38556-home-topbar-shows-user-name-android-chrome-retry1/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/hr.spec.js",
                        "column": 66,
                        "line": 37
                      }
                    }
                  ],
                  "status": "unexpected"
                }
              ],
              "id": "98264c29bbadd0584463-4dd8bc3fa551f6f2fc0b",
              "file": "hr.spec.js",
              "line": 35,
              "column": 3
            },
            {
              "title": "home topbar has Logout button",
              "ok": false,
              "tags": [],
              "tests": [
                {
                  "timeout": 30000,
                  "annotations": [],
                  "expectedStatus": "passed",
                  "projectId": "android-chrome",
                  "projectName": "android-chrome",
                  "results": [
                    {
                      "workerIndex": 24,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 8243,
                      "error": {
                        "message": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mlocator\u001b[39m\u001b[2m).\u001b[22mtoBeVisible\u001b[2m(\u001b[22m\u001b[2m)\u001b[22m failed\n\nLocator: locator('#scr-home .topbar .hub-logout-btn')\nExpected: visible\nTimeout: 8000ms\nError: element(s) not found\n\nCall log:\n\u001b[2m  - Expect \"toBeVisible\" with timeout 8000ms\u001b[22m\n\u001b[2m  - waiting for locator('#scr-home .topbar .hub-logout-btn')\u001b[22m\n",
                        "stack": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mlocator\u001b[39m\u001b[2m).\u001b[22mtoBeVisible\u001b[2m(\u001b[22m\u001b[2m)\u001b[22m failed\n\nLocator: locator('#scr-home .topbar .hub-logout-btn')\nExpected: visible\nTimeout: 8000ms\nError: element(s) not found\n\nCall log:\n\u001b[2m  - Expect \"toBeVisible\" with timeout 8000ms\u001b[22m\n\u001b[2m  - waiting for locator('#scr-home .topbar .hub-logout-btn')\u001b[22m\n\n    at /var/www/360lm/tests/hr.spec.js:42:69",
                        "location": {
                          "file": "/var/www/360lm/tests/hr.spec.js",
                          "column": 69,
                          "line": 42
                        },
                        "snippet": "  40 |   test('home topbar has Logout button', async ({ page }) => {\n  41 |     await page.goto(BASE_URL);\n> 42 |     await expect(page.locator('#scr-home .topbar .hub-logout-btn')).toBeVisible({ timeout: 8000 });\n     |                                                                     ^\n  43 |   });\n  44 |\n  45 |   test('logout confirm=dismiss — stays on page', async ({ page }) => {"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/hr.spec.js",
                            "column": 69,
                            "line": 42
                          },
                          "message": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mlocator\u001b[39m\u001b[2m).\u001b[22mtoBeVisible\u001b[2m(\u001b[22m\u001b[2m)\u001b[22m failed\n\nLocator: locator('#scr-home .topbar .hub-logout-btn')\nExpected: visible\nTimeout: 8000ms\nError: element(s) not found\n\nCall log:\n\u001b[2m  - Expect \"toBeVisible\" with timeout 8000ms\u001b[22m\n\u001b[2m  - waiting for locator('#scr-home .topbar .hub-logout-btn')\u001b[22m\n\n\n  40 |   test('home topbar has Logout button', async ({ page }) => {\n  41 |     await page.goto(BASE_URL);\n> 42 |     await expect(page.locator('#scr-home .topbar .hub-logout-btn')).toBeVisible({ timeout: 8000 });\n     |                                                                     ^\n  43 |   });\n  44 |\n  45 |   test('logout confirm=dismiss — stays on page', async ({ page }) => {\n    at /var/www/360lm/tests/hr.spec.js:42:69"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 0,
                      "startTime": "2026-07-12T12:58:45.798Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "screenshot",
                          "contentType": "image/png",
                          "path": "/var/www/360lm/test-results/hr-HR-Management-—-User-Ba-939ed-me-topbar-has-Logout-button-android-chrome/test-failed-1.png"
                        },
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/hr-HR-Management-—-User-Ba-939ed-me-topbar-has-Logout-button-android-chrome/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/hr.spec.js",
                        "column": 69,
                        "line": 42
                      }
                    },
                    {
                      "workerIndex": 25,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 8280,
                      "error": {
                        "message": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mlocator\u001b[39m\u001b[2m).\u001b[22mtoBeVisible\u001b[2m(\u001b[22m\u001b[2m)\u001b[22m failed\n\nLocator: locator('#scr-home .topbar .hub-logout-btn')\nExpected: visible\nTimeout: 8000ms\nError: element(s) not found\n\nCall log:\n\u001b[2m  - Expect \"toBeVisible\" with timeout 8000ms\u001b[22m\n\u001b[2m  - waiting for locator('#scr-home .topbar .hub-logout-btn')\u001b[22m\n",
                        "stack": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mlocator\u001b[39m\u001b[2m).\u001b[22mtoBeVisible\u001b[2m(\u001b[22m\u001b[2m)\u001b[22m failed\n\nLocator: locator('#scr-home .topbar .hub-logout-btn')\nExpected: visible\nTimeout: 8000ms\nError: element(s) not found\n\nCall log:\n\u001b[2m  - Expect \"toBeVisible\" with timeout 8000ms\u001b[22m\n\u001b[2m  - waiting for locator('#scr-home .topbar .hub-logout-btn')\u001b[22m\n\n    at /var/www/360lm/tests/hr.spec.js:42:69",
                        "location": {
                          "file": "/var/www/360lm/tests/hr.spec.js",
                          "column": 69,
                          "line": 42
                        },
                        "snippet": "  40 |   test('home topbar has Logout button', async ({ page }) => {\n  41 |     await page.goto(BASE_URL);\n> 42 |     await expect(page.locator('#scr-home .topbar .hub-logout-btn')).toBeVisible({ timeout: 8000 });\n     |                                                                     ^\n  43 |   });\n  44 |\n  45 |   test('logout confirm=dismiss — stays on page', async ({ page }) => {"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/hr.spec.js",
                            "column": 69,
                            "line": 42
                          },
                          "message": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mlocator\u001b[39m\u001b[2m).\u001b[22mtoBeVisible\u001b[2m(\u001b[22m\u001b[2m)\u001b[22m failed\n\nLocator: locator('#scr-home .topbar .hub-logout-btn')\nExpected: visible\nTimeout: 8000ms\nError: element(s) not found\n\nCall log:\n\u001b[2m  - Expect \"toBeVisible\" with timeout 8000ms\u001b[22m\n\u001b[2m  - waiting for locator('#scr-home .topbar .hub-logout-btn')\u001b[22m\n\n\n  40 |   test('home topbar has Logout button', async ({ page }) => {\n  41 |     await page.goto(BASE_URL);\n> 42 |     await expect(page.locator('#scr-home .topbar .hub-logout-btn')).toBeVisible({ timeout: 8000 });\n     |                                                                     ^\n  43 |   });\n  44 |\n  45 |   test('logout confirm=dismiss — stays on page', async ({ page }) => {\n    at /var/www/360lm/tests/hr.spec.js:42:69"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 1,
                      "startTime": "2026-07-12T12:58:55.014Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "screenshot",
                          "contentType": "image/png",
                          "path": "/var/www/360lm/test-results/hr-HR-Management-—-User-Ba-939ed-me-topbar-has-Logout-button-android-chrome-retry1/test-failed-1.png"
                        },
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/hr-HR-Management-—-User-Ba-939ed-me-topbar-has-Logout-button-android-chrome-retry1/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/hr.spec.js",
                        "column": 69,
                        "line": 42
                      }
                    }
                  ],
                  "status": "unexpected"
                }
              ],
              "id": "98264c29bbadd0584463-0f0747cbdccb5743bbf5",
              "file": "hr.spec.js",
              "line": 40,
              "column": 3
            },
            {
              "title": "logout confirm=dismiss — stays on page",
              "ok": false,
              "tags": [],
              "tests": [
                {
                  "timeout": 30000,
                  "annotations": [],
                  "expectedStatus": "passed",
                  "projectId": "android-chrome",
                  "projectName": "android-chrome",
                  "results": [
                    {
                      "workerIndex": 26,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 8242,
                      "error": {
                        "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#scr-home.active') to be visible\u001b[22m\n",
                        "stack": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#scr-home.active') to be visible\u001b[22m\n\n    at /var/www/360lm/tests/hr.spec.js:47:16",
                        "location": {
                          "file": "/var/www/360lm/tests/hr.spec.js",
                          "column": 16,
                          "line": 47
                        },
                        "snippet": "  45 |   test('logout confirm=dismiss — stays on page', async ({ page }) => {\n  46 |     await page.goto(BASE_URL);\n> 47 |     await page.waitForSelector('#scr-home.active', { timeout: 8000 });\n     |                ^\n  48 |     page.on('dialog', d => d.dismiss());\n  49 |     await page.locator('#scr-home .topbar .hub-logout-btn').click();\n  50 |     await expect(page).toHaveURL(BASE_URL);"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/hr.spec.js",
                            "column": 16,
                            "line": 47
                          },
                          "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#scr-home.active') to be visible\u001b[22m\n\n\n  45 |   test('logout confirm=dismiss — stays on page', async ({ page }) => {\n  46 |     await page.goto(BASE_URL);\n> 47 |     await page.waitForSelector('#scr-home.active', { timeout: 8000 });\n     |                ^\n  48 |     page.on('dialog', d => d.dismiss());\n  49 |     await page.locator('#scr-home .topbar .hub-logout-btn').click();\n  50 |     await expect(page).toHaveURL(BASE_URL);\n    at /var/www/360lm/tests/hr.spec.js:47:16"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 0,
                      "startTime": "2026-07-12T12:59:04.386Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "screenshot",
                          "contentType": "image/png",
                          "path": "/var/www/360lm/test-results/hr-HR-Management-—-User-Ba-7a41f-irm-dismiss-—-stays-on-page-android-chrome/test-failed-1.png"
                        },
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/hr-HR-Management-—-User-Ba-7a41f-irm-dismiss-—-stays-on-page-android-chrome/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/hr.spec.js",
                        "column": 16,
                        "line": 47
                      }
                    },
                    {
                      "workerIndex": 27,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 8260,
                      "error": {
                        "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#scr-home.active') to be visible\u001b[22m\n",
                        "stack": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#scr-home.active') to be visible\u001b[22m\n\n    at /var/www/360lm/tests/hr.spec.js:47:16",
                        "location": {
                          "file": "/var/www/360lm/tests/hr.spec.js",
                          "column": 16,
                          "line": 47
                        },
                        "snippet": "  45 |   test('logout confirm=dismiss — stays on page', async ({ page }) => {\n  46 |     await page.goto(BASE_URL);\n> 47 |     await page.waitForSelector('#scr-home.active', { timeout: 8000 });\n     |                ^\n  48 |     page.on('dialog', d => d.dismiss());\n  49 |     await page.locator('#scr-home .topbar .hub-logout-btn').click();\n  50 |     await expect(page).toHaveURL(BASE_URL);"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/hr.spec.js",
                            "column": 16,
                            "line": 47
                          },
                          "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#scr-home.active') to be visible\u001b[22m\n\n\n  45 |   test('logout confirm=dismiss — stays on page', async ({ page }) => {\n  46 |     await page.goto(BASE_URL);\n> 47 |     await page.waitForSelector('#scr-home.active', { timeout: 8000 });\n     |                ^\n  48 |     page.on('dialog', d => d.dismiss());\n  49 |     await page.locator('#scr-home .topbar .hub-logout-btn').click();\n  50 |     await expect(page).toHaveURL(BASE_URL);\n    at /var/www/360lm/tests/hr.spec.js:47:16"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 1,
                      "startTime": "2026-07-12T12:59:13.579Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "screenshot",
                          "contentType": "image/png",
                          "path": "/var/www/360lm/test-results/hr-HR-Management-—-User-Ba-7a41f-irm-dismiss-—-stays-on-page-android-chrome-retry1/test-failed-1.png"
                        },
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/hr-HR-Management-—-User-Ba-7a41f-irm-dismiss-—-stays-on-page-android-chrome-retry1/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/hr.spec.js",
                        "column": 16,
                        "line": 47
                      }
                    }
                  ],
                  "status": "unexpected"
                }
              ],
              "id": "98264c29bbadd0584463-0efbece8363a26d7b996",
              "file": "hr.spec.js",
              "line": 45,
              "column": 3
            },
            {
              "title": "logout confirm=OK → redirects to /hub/",
              "ok": false,
              "tags": [],
              "tests": [
                {
                  "timeout": 30000,
                  "annotations": [],
                  "expectedStatus": "passed",
                  "projectId": "android-chrome",
                  "projectName": "android-chrome",
                  "results": [
                    {
                      "workerIndex": 28,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 8260,
                      "error": {
                        "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#scr-home.active') to be visible\u001b[22m\n",
                        "stack": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#scr-home.active') to be visible\u001b[22m\n\n    at /var/www/360lm/tests/hr.spec.js:55:16",
                        "location": {
                          "file": "/var/www/360lm/tests/hr.spec.js",
                          "column": 16,
                          "line": 55
                        },
                        "snippet": "  53 |   test('logout confirm=OK → redirects to /hub/', async ({ page }) => {\n  54 |     await page.goto(BASE_URL);\n> 55 |     await page.waitForSelector('#scr-home.active', { timeout: 8000 });\n     |                ^\n  56 |     page.on('dialog', d => d.accept());\n  57 |     await page.locator('#scr-home .topbar .hub-logout-btn').click();\n  58 |     await expect(page).toHaveURL(/\\/hub\\//, { timeout: 6000 });"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/hr.spec.js",
                            "column": 16,
                            "line": 55
                          },
                          "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#scr-home.active') to be visible\u001b[22m\n\n\n  53 |   test('logout confirm=OK → redirects to /hub/', async ({ page }) => {\n  54 |     await page.goto(BASE_URL);\n> 55 |     await page.waitForSelector('#scr-home.active', { timeout: 8000 });\n     |                ^\n  56 |     page.on('dialog', d => d.accept());\n  57 |     await page.locator('#scr-home .topbar .hub-logout-btn').click();\n  58 |     await expect(page).toHaveURL(/\\/hub\\//, { timeout: 6000 });\n    at /var/www/360lm/tests/hr.spec.js:55:16"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 0,
                      "startTime": "2026-07-12T12:59:22.802Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "screenshot",
                          "contentType": "image/png",
                          "path": "/var/www/360lm/test-results/hr-HR-Management-—-User-Ba-b5315-firm-OK-→-redirects-to-hub--android-chrome/test-failed-1.png"
                        },
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/hr-HR-Management-—-User-Ba-b5315-firm-OK-→-redirects-to-hub--android-chrome/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/hr.spec.js",
                        "column": 16,
                        "line": 55
                      }
                    },
                    {
                      "workerIndex": 29,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 8291,
                      "error": {
                        "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#scr-home.active') to be visible\u001b[22m\n",
                        "stack": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#scr-home.active') to be visible\u001b[22m\n\n    at /var/www/360lm/tests/hr.spec.js:55:16",
                        "location": {
                          "file": "/var/www/360lm/tests/hr.spec.js",
                          "column": 16,
                          "line": 55
                        },
                        "snippet": "  53 |   test('logout confirm=OK → redirects to /hub/', async ({ page }) => {\n  54 |     await page.goto(BASE_URL);\n> 55 |     await page.waitForSelector('#scr-home.active', { timeout: 8000 });\n     |                ^\n  56 |     page.on('dialog', d => d.accept());\n  57 |     await page.locator('#scr-home .topbar .hub-logout-btn').click();\n  58 |     await expect(page).toHaveURL(/\\/hub\\//, { timeout: 6000 });"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/hr.spec.js",
                            "column": 16,
                            "line": 55
                          },
                          "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#scr-home.active') to be visible\u001b[22m\n\n\n  53 |   test('logout confirm=OK → redirects to /hub/', async ({ page }) => {\n  54 |     await page.goto(BASE_URL);\n> 55 |     await page.waitForSelector('#scr-home.active', { timeout: 8000 });\n     |                ^\n  56 |     page.on('dialog', d => d.accept());\n  57 |     await page.locator('#scr-home .topbar .hub-logout-btn').click();\n  58 |     await expect(page).toHaveURL(/\\/hub\\//, { timeout: 6000 });\n    at /var/www/360lm/tests/hr.spec.js:55:16"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 1,
                      "startTime": "2026-07-12T12:59:32.054Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "screenshot",
                          "contentType": "image/png",
                          "path": "/var/www/360lm/test-results/hr-HR-Management-—-User-Ba-b5315-firm-OK-→-redirects-to-hub--android-chrome-retry1/test-failed-1.png"
                        },
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/hr-HR-Management-—-User-Ba-b5315-firm-OK-→-redirects-to-hub--android-chrome-retry1/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/hr.spec.js",
                        "column": 16,
                        "line": 55
                      }
                    }
                  ],
                  "status": "unexpected"
                }
              ],
              "id": "98264c29bbadd0584463-93ab81e3991c7861b699",
              "file": "hr.spec.js",
              "line": 53,
              "column": 3
            },
            {
              "title": "home topbar shows user name",
              "ok": false,
              "tags": [],
              "tests": [
                {
                  "timeout": 30000,
                  "annotations": [],
                  "expectedStatus": "passed",
                  "projectId": "desktop-chrome",
                  "projectName": "desktop-chrome",
                  "results": [
                    {
                      "workerIndex": 198,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 8325,
                      "error": {
                        "message": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mlocator\u001b[39m\u001b[2m).\u001b[22mtoHaveText\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m)\u001b[22m failed\n\nLocator: locator('#scr-home .topbar .topbar-user')\nExpected pattern: \u001b[32m/Harish/\u001b[39m\nTimeout: 8000ms\nError: element(s) not found\n\nCall log:\n\u001b[2m  - Expect \"toHaveText\" with timeout 8000ms\u001b[22m\n\u001b[2m  - waiting for locator('#scr-home .topbar .topbar-user')\u001b[22m\n",
                        "stack": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mlocator\u001b[39m\u001b[2m).\u001b[22mtoHaveText\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m)\u001b[22m failed\n\nLocator: locator('#scr-home .topbar .topbar-user')\nExpected pattern: \u001b[32m/Harish/\u001b[39m\nTimeout: 8000ms\nError: element(s) not found\n\nCall log:\n\u001b[2m  - Expect \"toHaveText\" with timeout 8000ms\u001b[22m\n\u001b[2m  - waiting for locator('#scr-home .topbar .topbar-user')\u001b[22m\n\n    at /var/www/360lm/tests/hr.spec.js:37:66",
                        "location": {
                          "file": "/var/www/360lm/tests/hr.spec.js",
                          "column": 66,
                          "line": 37
                        },
                        "snippet": "  35 |   test('home topbar shows user name', async ({ page }) => {\n  36 |     await page.goto(BASE_URL);\n> 37 |     await expect(page.locator('#scr-home .topbar .topbar-user')).toHaveText(/Harish/, { timeout: 8000 });\n     |                                                                  ^\n  38 |   });\n  39 |\n  40 |   test('home topbar has Logout button', async ({ page }) => {"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/hr.spec.js",
                            "column": 66,
                            "line": 37
                          },
                          "message": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mlocator\u001b[39m\u001b[2m).\u001b[22mtoHaveText\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m)\u001b[22m failed\n\nLocator: locator('#scr-home .topbar .topbar-user')\nExpected pattern: \u001b[32m/Harish/\u001b[39m\nTimeout: 8000ms\nError: element(s) not found\n\nCall log:\n\u001b[2m  - Expect \"toHaveText\" with timeout 8000ms\u001b[22m\n\u001b[2m  - waiting for locator('#scr-home .topbar .topbar-user')\u001b[22m\n\n\n  35 |   test('home topbar shows user name', async ({ page }) => {\n  36 |     await page.goto(BASE_URL);\n> 37 |     await expect(page.locator('#scr-home .topbar .topbar-user')).toHaveText(/Harish/, { timeout: 8000 });\n     |                                                                  ^\n  38 |   });\n  39 |\n  40 |   test('home topbar has Logout button', async ({ page }) => {\n    at /var/www/360lm/tests/hr.spec.js:37:66"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 0,
                      "startTime": "2026-07-12T13:18:39.592Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "screenshot",
                          "contentType": "image/png",
                          "path": "/var/www/360lm/test-results/hr-HR-Management-—-User-Ba-38556-home-topbar-shows-user-name-desktop-chrome/test-failed-1.png"
                        },
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/hr-HR-Management-—-User-Ba-38556-home-topbar-shows-user-name-desktop-chrome/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/hr.spec.js",
                        "column": 66,
                        "line": 37
                      }
                    },
                    {
                      "workerIndex": 199,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 8369,
                      "error": {
                        "message": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mlocator\u001b[39m\u001b[2m).\u001b[22mtoHaveText\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m)\u001b[22m failed\n\nLocator: locator('#scr-home .topbar .topbar-user')\nExpected pattern: \u001b[32m/Harish/\u001b[39m\nTimeout: 8000ms\nError: element(s) not found\n\nCall log:\n\u001b[2m  - Expect \"toHaveText\" with timeout 8000ms\u001b[22m\n\u001b[2m  - waiting for locator('#scr-home .topbar .topbar-user')\u001b[22m\n",
                        "stack": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mlocator\u001b[39m\u001b[2m).\u001b[22mtoHaveText\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m)\u001b[22m failed\n\nLocator: locator('#scr-home .topbar .topbar-user')\nExpected pattern: \u001b[32m/Harish/\u001b[39m\nTimeout: 8000ms\nError: element(s) not found\n\nCall log:\n\u001b[2m  - Expect \"toHaveText\" with timeout 8000ms\u001b[22m\n\u001b[2m  - waiting for locator('#scr-home .topbar .topbar-user')\u001b[22m\n\n    at /var/www/360lm/tests/hr.spec.js:37:66",
                        "location": {
                          "file": "/var/www/360lm/tests/hr.spec.js",
                          "column": 66,
                          "line": 37
                        },
                        "snippet": "  35 |   test('home topbar shows user name', async ({ page }) => {\n  36 |     await page.goto(BASE_URL);\n> 37 |     await expect(page.locator('#scr-home .topbar .topbar-user')).toHaveText(/Harish/, { timeout: 8000 });\n     |                                                                  ^\n  38 |   });\n  39 |\n  40 |   test('home topbar has Logout button', async ({ page }) => {"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/hr.spec.js",
                            "column": 66,
                            "line": 37
                          },
                          "message": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mlocator\u001b[39m\u001b[2m).\u001b[22mtoHaveText\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m)\u001b[22m failed\n\nLocator: locator('#scr-home .topbar .topbar-user')\nExpected pattern: \u001b[32m/Harish/\u001b[39m\nTimeout: 8000ms\nError: element(s) not found\n\nCall log:\n\u001b[2m  - Expect \"toHaveText\" with timeout 8000ms\u001b[22m\n\u001b[2m  - waiting for locator('#scr-home .topbar .topbar-user')\u001b[22m\n\n\n  35 |   test('home topbar shows user name', async ({ page }) => {\n  36 |     await page.goto(BASE_URL);\n> 37 |     await expect(page.locator('#scr-home .topbar .topbar-user')).toHaveText(/Harish/, { timeout: 8000 });\n     |                                                                  ^\n  38 |   });\n  39 |\n  40 |   test('home topbar has Logout button', async ({ page }) => {\n    at /var/www/360lm/tests/hr.spec.js:37:66"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 1,
                      "startTime": "2026-07-12T13:18:49.141Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "screenshot",
                          "contentType": "image/png",
                          "path": "/var/www/360lm/test-results/hr-HR-Management-—-User-Ba-38556-home-topbar-shows-user-name-desktop-chrome-retry1/test-failed-1.png"
                        },
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/hr-HR-Management-—-User-Ba-38556-home-topbar-shows-user-name-desktop-chrome-retry1/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/hr.spec.js",
                        "column": 66,
                        "line": 37
                      }
                    }
                  ],
                  "status": "unexpected"
                }
              ],
              "id": "98264c29bbadd0584463-7d3c1892ec14e8a7f204",
              "file": "hr.spec.js",
              "line": 35,
              "column": 3
            },
            {
              "title": "home topbar has Logout button",
              "ok": false,
              "tags": [],
              "tests": [
                {
                  "timeout": 30000,
                  "annotations": [],
                  "expectedStatus": "passed",
                  "projectId": "desktop-chrome",
                  "projectName": "desktop-chrome",
                  "results": [
                    {
                      "workerIndex": 200,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 8297,
                      "error": {
                        "message": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mlocator\u001b[39m\u001b[2m).\u001b[22mtoBeVisible\u001b[2m(\u001b[22m\u001b[2m)\u001b[22m failed\n\nLocator: locator('#scr-home .topbar .hub-logout-btn')\nExpected: visible\nTimeout: 8000ms\nError: element(s) not found\n\nCall log:\n\u001b[2m  - Expect \"toBeVisible\" with timeout 8000ms\u001b[22m\n\u001b[2m  - waiting for locator('#scr-home .topbar .hub-logout-btn')\u001b[22m\n",
                        "stack": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mlocator\u001b[39m\u001b[2m).\u001b[22mtoBeVisible\u001b[2m(\u001b[22m\u001b[2m)\u001b[22m failed\n\nLocator: locator('#scr-home .topbar .hub-logout-btn')\nExpected: visible\nTimeout: 8000ms\nError: element(s) not found\n\nCall log:\n\u001b[2m  - Expect \"toBeVisible\" with timeout 8000ms\u001b[22m\n\u001b[2m  - waiting for locator('#scr-home .topbar .hub-logout-btn')\u001b[22m\n\n    at /var/www/360lm/tests/hr.spec.js:42:69",
                        "location": {
                          "file": "/var/www/360lm/tests/hr.spec.js",
                          "column": 69,
                          "line": 42
                        },
                        "snippet": "  40 |   test('home topbar has Logout button', async ({ page }) => {\n  41 |     await page.goto(BASE_URL);\n> 42 |     await expect(page.locator('#scr-home .topbar .hub-logout-btn')).toBeVisible({ timeout: 8000 });\n     |                                                                     ^\n  43 |   });\n  44 |\n  45 |   test('logout confirm=dismiss — stays on page', async ({ page }) => {"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/hr.spec.js",
                            "column": 69,
                            "line": 42
                          },
                          "message": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mlocator\u001b[39m\u001b[2m).\u001b[22mtoBeVisible\u001b[2m(\u001b[22m\u001b[2m)\u001b[22m failed\n\nLocator: locator('#scr-home .topbar .hub-logout-btn')\nExpected: visible\nTimeout: 8000ms\nError: element(s) not found\n\nCall log:\n\u001b[2m  - Expect \"toBeVisible\" with timeout 8000ms\u001b[22m\n\u001b[2m  - waiting for locator('#scr-home .topbar .hub-logout-btn')\u001b[22m\n\n\n  40 |   test('home topbar has Logout button', async ({ page }) => {\n  41 |     await page.goto(BASE_URL);\n> 42 |     await expect(page.locator('#scr-home .topbar .hub-logout-btn')).toBeVisible({ timeout: 8000 });\n     |                                                                     ^\n  43 |   });\n  44 |\n  45 |   test('logout confirm=dismiss — stays on page', async ({ page }) => {\n    at /var/www/360lm/tests/hr.spec.js:42:69"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 0,
                      "startTime": "2026-07-12T13:18:58.644Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "screenshot",
                          "contentType": "image/png",
                          "path": "/var/www/360lm/test-results/hr-HR-Management-—-User-Ba-939ed-me-topbar-has-Logout-button-desktop-chrome/test-failed-1.png"
                        },
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/hr-HR-Management-—-User-Ba-939ed-me-topbar-has-Logout-button-desktop-chrome/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/hr.spec.js",
                        "column": 69,
                        "line": 42
                      }
                    },
                    {
                      "workerIndex": 201,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 8345,
                      "error": {
                        "message": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mlocator\u001b[39m\u001b[2m).\u001b[22mtoBeVisible\u001b[2m(\u001b[22m\u001b[2m)\u001b[22m failed\n\nLocator: locator('#scr-home .topbar .hub-logout-btn')\nExpected: visible\nTimeout: 8000ms\nError: element(s) not found\n\nCall log:\n\u001b[2m  - Expect \"toBeVisible\" with timeout 8000ms\u001b[22m\n\u001b[2m  - waiting for locator('#scr-home .topbar .hub-logout-btn')\u001b[22m\n",
                        "stack": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mlocator\u001b[39m\u001b[2m).\u001b[22mtoBeVisible\u001b[2m(\u001b[22m\u001b[2m)\u001b[22m failed\n\nLocator: locator('#scr-home .topbar .hub-logout-btn')\nExpected: visible\nTimeout: 8000ms\nError: element(s) not found\n\nCall log:\n\u001b[2m  - Expect \"toBeVisible\" with timeout 8000ms\u001b[22m\n\u001b[2m  - waiting for locator('#scr-home .topbar .hub-logout-btn')\u001b[22m\n\n    at /var/www/360lm/tests/hr.spec.js:42:69",
                        "location": {
                          "file": "/var/www/360lm/tests/hr.spec.js",
                          "column": 69,
                          "line": 42
                        },
                        "snippet": "  40 |   test('home topbar has Logout button', async ({ page }) => {\n  41 |     await page.goto(BASE_URL);\n> 42 |     await expect(page.locator('#scr-home .topbar .hub-logout-btn')).toBeVisible({ timeout: 8000 });\n     |                                                                     ^\n  43 |   });\n  44 |\n  45 |   test('logout confirm=dismiss — stays on page', async ({ page }) => {"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/hr.spec.js",
                            "column": 69,
                            "line": 42
                          },
                          "message": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mlocator\u001b[39m\u001b[2m).\u001b[22mtoBeVisible\u001b[2m(\u001b[22m\u001b[2m)\u001b[22m failed\n\nLocator: locator('#scr-home .topbar .hub-logout-btn')\nExpected: visible\nTimeout: 8000ms\nError: element(s) not found\n\nCall log:\n\u001b[2m  - Expect \"toBeVisible\" with timeout 8000ms\u001b[22m\n\u001b[2m  - waiting for locator('#scr-home .topbar .hub-logout-btn')\u001b[22m\n\n\n  40 |   test('home topbar has Logout button', async ({ page }) => {\n  41 |     await page.goto(BASE_URL);\n> 42 |     await expect(page.locator('#scr-home .topbar .hub-logout-btn')).toBeVisible({ timeout: 8000 });\n     |                                                                     ^\n  43 |   });\n  44 |\n  45 |   test('logout confirm=dismiss — stays on page', async ({ page }) => {\n    at /var/www/360lm/tests/hr.spec.js:42:69"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 1,
                      "startTime": "2026-07-12T13:19:08.034Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "screenshot",
                          "contentType": "image/png",
                          "path": "/var/www/360lm/test-results/hr-HR-Management-—-User-Ba-939ed-me-topbar-has-Logout-button-desktop-chrome-retry1/test-failed-1.png"
                        },
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/hr-HR-Management-—-User-Ba-939ed-me-topbar-has-Logout-button-desktop-chrome-retry1/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/hr.spec.js",
                        "column": 69,
                        "line": 42
                      }
                    }
                  ],
                  "status": "unexpected"
                }
              ],
              "id": "98264c29bbadd0584463-6cf2e2d98d186fc640e2",
              "file": "hr.spec.js",
              "line": 40,
              "column": 3
            },
            {
              "title": "logout confirm=dismiss — stays on page",
              "ok": false,
              "tags": [],
              "tests": [
                {
                  "timeout": 30000,
                  "annotations": [],
                  "expectedStatus": "passed",
                  "projectId": "desktop-chrome",
                  "projectName": "desktop-chrome",
                  "results": [
                    {
                      "workerIndex": 202,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 8306,
                      "error": {
                        "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#scr-home.active') to be visible\u001b[22m\n",
                        "stack": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#scr-home.active') to be visible\u001b[22m\n\n    at /var/www/360lm/tests/hr.spec.js:47:16",
                        "location": {
                          "file": "/var/www/360lm/tests/hr.spec.js",
                          "column": 16,
                          "line": 47
                        },
                        "snippet": "  45 |   test('logout confirm=dismiss — stays on page', async ({ page }) => {\n  46 |     await page.goto(BASE_URL);\n> 47 |     await page.waitForSelector('#scr-home.active', { timeout: 8000 });\n     |                ^\n  48 |     page.on('dialog', d => d.dismiss());\n  49 |     await page.locator('#scr-home .topbar .hub-logout-btn').click();\n  50 |     await expect(page).toHaveURL(BASE_URL);"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/hr.spec.js",
                            "column": 16,
                            "line": 47
                          },
                          "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#scr-home.active') to be visible\u001b[22m\n\n\n  45 |   test('logout confirm=dismiss — stays on page', async ({ page }) => {\n  46 |     await page.goto(BASE_URL);\n> 47 |     await page.waitForSelector('#scr-home.active', { timeout: 8000 });\n     |                ^\n  48 |     page.on('dialog', d => d.dismiss());\n  49 |     await page.locator('#scr-home .topbar .hub-logout-btn').click();\n  50 |     await expect(page).toHaveURL(BASE_URL);\n    at /var/www/360lm/tests/hr.spec.js:47:16"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 0,
                      "startTime": "2026-07-12T13:19:17.573Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "screenshot",
                          "contentType": "image/png",
                          "path": "/var/www/360lm/test-results/hr-HR-Management-—-User-Ba-7a41f-irm-dismiss-—-stays-on-page-desktop-chrome/test-failed-1.png"
                        },
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/hr-HR-Management-—-User-Ba-7a41f-irm-dismiss-—-stays-on-page-desktop-chrome/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/hr.spec.js",
                        "column": 16,
                        "line": 47
                      }
                    },
                    {
                      "workerIndex": 203,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 8372,
                      "error": {
                        "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#scr-home.active') to be visible\u001b[22m\n",
                        "stack": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#scr-home.active') to be visible\u001b[22m\n\n    at /var/www/360lm/tests/hr.spec.js:47:16",
                        "location": {
                          "file": "/var/www/360lm/tests/hr.spec.js",
                          "column": 16,
                          "line": 47
                        },
                        "snippet": "  45 |   test('logout confirm=dismiss — stays on page', async ({ page }) => {\n  46 |     await page.goto(BASE_URL);\n> 47 |     await page.waitForSelector('#scr-home.active', { timeout: 8000 });\n     |                ^\n  48 |     page.on('dialog', d => d.dismiss());\n  49 |     await page.locator('#scr-home .topbar .hub-logout-btn').click();\n  50 |     await expect(page).toHaveURL(BASE_URL);"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/hr.spec.js",
                            "column": 16,
                            "line": 47
                          },
                          "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#scr-home.active') to be visible\u001b[22m\n\n\n  45 |   test('logout confirm=dismiss — stays on page', async ({ page }) => {\n  46 |     await page.goto(BASE_URL);\n> 47 |     await page.waitForSelector('#scr-home.active', { timeout: 8000 });\n     |                ^\n  48 |     page.on('dialog', d => d.dismiss());\n  49 |     await page.locator('#scr-home .topbar .hub-logout-btn').click();\n  50 |     await expect(page).toHaveURL(BASE_URL);\n    at /var/www/360lm/tests/hr.spec.js:47:16"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 1,
                      "startTime": "2026-07-12T13:19:27.116Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "screenshot",
                          "contentType": "image/png",
                          "path": "/var/www/360lm/test-results/hr-HR-Management-—-User-Ba-7a41f-irm-dismiss-—-stays-on-page-desktop-chrome-retry1/test-failed-1.png"
                        },
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/hr-HR-Management-—-User-Ba-7a41f-irm-dismiss-—-stays-on-page-desktop-chrome-retry1/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/hr.spec.js",
                        "column": 16,
                        "line": 47
                      }
                    }
                  ],
                  "status": "unexpected"
                }
              ],
              "id": "98264c29bbadd0584463-6723e85d05fa5b06bc6e",
              "file": "hr.spec.js",
              "line": 45,
              "column": 3
            },
            {
              "title": "logout confirm=OK → redirects to /hub/",
              "ok": false,
              "tags": [],
              "tests": [
                {
                  "timeout": 30000,
                  "annotations": [],
                  "expectedStatus": "passed",
                  "projectId": "desktop-chrome",
                  "projectName": "desktop-chrome",
                  "results": [
                    {
                      "workerIndex": 204,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 8328,
                      "error": {
                        "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#scr-home.active') to be visible\u001b[22m\n",
                        "stack": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#scr-home.active') to be visible\u001b[22m\n\n    at /var/www/360lm/tests/hr.spec.js:55:16",
                        "location": {
                          "file": "/var/www/360lm/tests/hr.spec.js",
                          "column": 16,
                          "line": 55
                        },
                        "snippet": "  53 |   test('logout confirm=OK → redirects to /hub/', async ({ page }) => {\n  54 |     await page.goto(BASE_URL);\n> 55 |     await page.waitForSelector('#scr-home.active', { timeout: 8000 });\n     |                ^\n  56 |     page.on('dialog', d => d.accept());\n  57 |     await page.locator('#scr-home .topbar .hub-logout-btn').click();\n  58 |     await expect(page).toHaveURL(/\\/hub\\//, { timeout: 6000 });"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/hr.spec.js",
                            "column": 16,
                            "line": 55
                          },
                          "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#scr-home.active') to be visible\u001b[22m\n\n\n  53 |   test('logout confirm=OK → redirects to /hub/', async ({ page }) => {\n  54 |     await page.goto(BASE_URL);\n> 55 |     await page.waitForSelector('#scr-home.active', { timeout: 8000 });\n     |                ^\n  56 |     page.on('dialog', d => d.accept());\n  57 |     await page.locator('#scr-home .topbar .hub-logout-btn').click();\n  58 |     await expect(page).toHaveURL(/\\/hub\\//, { timeout: 6000 });\n    at /var/www/360lm/tests/hr.spec.js:55:16"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 0,
                      "startTime": "2026-07-12T13:19:36.691Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "screenshot",
                          "contentType": "image/png",
                          "path": "/var/www/360lm/test-results/hr-HR-Management-—-User-Ba-b5315-firm-OK-→-redirects-to-hub--desktop-chrome/test-failed-1.png"
                        },
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/hr-HR-Management-—-User-Ba-b5315-firm-OK-→-redirects-to-hub--desktop-chrome/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/hr.spec.js",
                        "column": 16,
                        "line": 55
                      }
                    },
                    {
                      "workerIndex": 205,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 8303,
                      "error": {
                        "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#scr-home.active') to be visible\u001b[22m\n",
                        "stack": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#scr-home.active') to be visible\u001b[22m\n\n    at /var/www/360lm/tests/hr.spec.js:55:16",
                        "location": {
                          "file": "/var/www/360lm/tests/hr.spec.js",
                          "column": 16,
                          "line": 55
                        },
                        "snippet": "  53 |   test('logout confirm=OK → redirects to /hub/', async ({ page }) => {\n  54 |     await page.goto(BASE_URL);\n> 55 |     await page.waitForSelector('#scr-home.active', { timeout: 8000 });\n     |                ^\n  56 |     page.on('dialog', d => d.accept());\n  57 |     await page.locator('#scr-home .topbar .hub-logout-btn').click();\n  58 |     await expect(page).toHaveURL(/\\/hub\\//, { timeout: 6000 });"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/hr.spec.js",
                            "column": 16,
                            "line": 55
                          },
                          "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#scr-home.active') to be visible\u001b[22m\n\n\n  53 |   test('logout confirm=OK → redirects to /hub/', async ({ page }) => {\n  54 |     await page.goto(BASE_URL);\n> 55 |     await page.waitForSelector('#scr-home.active', { timeout: 8000 });\n     |                ^\n  56 |     page.on('dialog', d => d.accept());\n  57 |     await page.locator('#scr-home .topbar .hub-logout-btn').click();\n  58 |     await expect(page).toHaveURL(/\\/hub\\//, { timeout: 6000 });\n    at /var/www/360lm/tests/hr.spec.js:55:16"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 1,
                      "startTime": "2026-07-12T13:19:46.118Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "screenshot",
                          "contentType": "image/png",
                          "path": "/var/www/360lm/test-results/hr-HR-Management-—-User-Ba-b5315-firm-OK-→-redirects-to-hub--desktop-chrome-retry1/test-failed-1.png"
                        },
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/hr-HR-Management-—-User-Ba-b5315-firm-OK-→-redirects-to-hub--desktop-chrome-retry1/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/hr.spec.js",
                        "column": 16,
                        "line": 55
                      }
                    }
                  ],
                  "status": "unexpected"
                }
              ],
              "id": "98264c29bbadd0584463-6bcb167a520971d057aa",
              "file": "hr.spec.js",
              "line": 53,
              "column": 3
            }
          ]
        },
        {
          "title": "HR Management — User Bar: No-Topbar Screens (injected)",
          "file": "hr.spec.js",
          "line": 62,
          "column": 6,
          "specs": [
            {
              "title": "scr-attendance gets a topbar prepended with Hub + Logout",
              "ok": false,
              "tags": [],
              "tests": [
                {
                  "timeout": 30000,
                  "annotations": [],
                  "expectedStatus": "passed",
                  "projectId": "android-chrome",
                  "projectName": "android-chrome",
                  "results": [
                    {
                      "workerIndex": 30,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 8246,
                      "error": {
                        "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#scr-home.active') to be visible\u001b[22m\n",
                        "stack": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#scr-home.active') to be visible\u001b[22m\n\n    at /var/www/360lm/tests/hr.spec.js:69:16",
                        "location": {
                          "file": "/var/www/360lm/tests/hr.spec.js",
                          "column": 16,
                          "line": 69
                        },
                        "snippet": "  67 |   test('scr-attendance gets a topbar prepended with Hub + Logout', async ({ page }) => {\n  68 |     await page.goto(BASE_URL);\n> 69 |     await page.waitForSelector('#scr-home.active', { timeout: 8000 });\n     |                ^\n  70 |     // Navigate to attendance via bottom nav\n  71 |     await page.locator('.bnav-item[onclick*=\"attendance\"], .bnav-item:has-text(\"Attendance\")').first().click();\n  72 |     await expect(page.locator('#scr-attendance.active')).toBeVisible({ timeout: 5000 });"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/hr.spec.js",
                            "column": 16,
                            "line": 69
                          },
                          "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#scr-home.active') to be visible\u001b[22m\n\n\n  67 |   test('scr-attendance gets a topbar prepended with Hub + Logout', async ({ page }) => {\n  68 |     await page.goto(BASE_URL);\n> 69 |     await page.waitForSelector('#scr-home.active', { timeout: 8000 });\n     |                ^\n  70 |     // Navigate to attendance via bottom nav\n  71 |     await page.locator('.bnav-item[onclick*=\"attendance\"], .bnav-item:has-text(\"Attendance\")').first().click();\n  72 |     await expect(page.locator('#scr-attendance.active')).toBeVisible({ timeout: 5000 });\n    at /var/www/360lm/tests/hr.spec.js:69:16"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 0,
                      "startTime": "2026-07-12T12:59:41.384Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "screenshot",
                          "contentType": "image/png",
                          "path": "/var/www/360lm/test-results/hr-HR-Management-—-User-Ba-1c19d-r-prepended-with-Hub-Logout-android-chrome/test-failed-1.png"
                        },
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/hr-HR-Management-—-User-Ba-1c19d-r-prepended-with-Hub-Logout-android-chrome/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/hr.spec.js",
                        "column": 16,
                        "line": 69
                      }
                    },
                    {
                      "workerIndex": 31,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 8246,
                      "error": {
                        "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#scr-home.active') to be visible\u001b[22m\n",
                        "stack": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#scr-home.active') to be visible\u001b[22m\n\n    at /var/www/360lm/tests/hr.spec.js:69:16",
                        "location": {
                          "file": "/var/www/360lm/tests/hr.spec.js",
                          "column": 16,
                          "line": 69
                        },
                        "snippet": "  67 |   test('scr-attendance gets a topbar prepended with Hub + Logout', async ({ page }) => {\n  68 |     await page.goto(BASE_URL);\n> 69 |     await page.waitForSelector('#scr-home.active', { timeout: 8000 });\n     |                ^\n  70 |     // Navigate to attendance via bottom nav\n  71 |     await page.locator('.bnav-item[onclick*=\"attendance\"], .bnav-item:has-text(\"Attendance\")').first().click();\n  72 |     await expect(page.locator('#scr-attendance.active')).toBeVisible({ timeout: 5000 });"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/hr.spec.js",
                            "column": 16,
                            "line": 69
                          },
                          "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#scr-home.active') to be visible\u001b[22m\n\n\n  67 |   test('scr-attendance gets a topbar prepended with Hub + Logout', async ({ page }) => {\n  68 |     await page.goto(BASE_URL);\n> 69 |     await page.waitForSelector('#scr-home.active', { timeout: 8000 });\n     |                ^\n  70 |     // Navigate to attendance via bottom nav\n  71 |     await page.locator('.bnav-item[onclick*=\"attendance\"], .bnav-item:has-text(\"Attendance\")').first().click();\n  72 |     await expect(page.locator('#scr-attendance.active')).toBeVisible({ timeout: 5000 });\n    at /var/www/360lm/tests/hr.spec.js:69:16"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 1,
                      "startTime": "2026-07-12T12:59:50.597Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "screenshot",
                          "contentType": "image/png",
                          "path": "/var/www/360lm/test-results/hr-HR-Management-—-User-Ba-1c19d-r-prepended-with-Hub-Logout-android-chrome-retry1/test-failed-1.png"
                        },
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/hr-HR-Management-—-User-Ba-1c19d-r-prepended-with-Hub-Logout-android-chrome-retry1/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/hr.spec.js",
                        "column": 16,
                        "line": 69
                      }
                    }
                  ],
                  "status": "unexpected"
                }
              ],
              "id": "98264c29bbadd0584463-7eec289a0151f0a85b50",
              "file": "hr.spec.js",
              "line": 67,
              "column": 3
            },
            {
              "title": "scr-attendance topbar Hub button navigates to /hub/",
              "ok": false,
              "tags": [],
              "tests": [
                {
                  "timeout": 30000,
                  "annotations": [],
                  "expectedStatus": "passed",
                  "projectId": "android-chrome",
                  "projectName": "android-chrome",
                  "results": [
                    {
                      "workerIndex": 32,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 8462,
                      "error": {
                        "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#scr-home.active') to be visible\u001b[22m\n",
                        "stack": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#scr-home.active') to be visible\u001b[22m\n\n    at /var/www/360lm/tests/hr.spec.js:80:16",
                        "location": {
                          "file": "/var/www/360lm/tests/hr.spec.js",
                          "column": 16,
                          "line": 80
                        },
                        "snippet": "  78 |   test('scr-attendance topbar Hub button navigates to /hub/', async ({ page }) => {\n  79 |     await page.goto(BASE_URL);\n> 80 |     await page.waitForSelector('#scr-home.active', { timeout: 8000 });\n     |                ^\n  81 |     await page.locator('.bnav-item[onclick*=\"attendance\"], .bnav-item:has-text(\"Attendance\")').first().click();\n  82 |     await expect(page.locator('#scr-attendance.active')).toBeVisible({ timeout: 5000 });\n  83 |     await page.locator('#scr-attendance .topbar .hub-btn').click();"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/hr.spec.js",
                            "column": 16,
                            "line": 80
                          },
                          "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#scr-home.active') to be visible\u001b[22m\n\n\n  78 |   test('scr-attendance topbar Hub button navigates to /hub/', async ({ page }) => {\n  79 |     await page.goto(BASE_URL);\n> 80 |     await page.waitForSelector('#scr-home.active', { timeout: 8000 });\n     |                ^\n  81 |     await page.locator('.bnav-item[onclick*=\"attendance\"], .bnav-item:has-text(\"Attendance\")').first().click();\n  82 |     await expect(page.locator('#scr-attendance.active')).toBeVisible({ timeout: 5000 });\n  83 |     await page.locator('#scr-attendance .topbar .hub-btn').click();\n    at /var/www/360lm/tests/hr.spec.js:80:16"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 0,
                      "startTime": "2026-07-12T12:59:59.876Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "screenshot",
                          "contentType": "image/png",
                          "path": "/var/www/360lm/test-results/hr-HR-Management-—-User-Ba-1a182-ub-button-navigates-to-hub--android-chrome/test-failed-1.png"
                        },
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/hr-HR-Management-—-User-Ba-1a182-ub-button-navigates-to-hub--android-chrome/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/hr.spec.js",
                        "column": 16,
                        "line": 80
                      }
                    },
                    {
                      "workerIndex": 33,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 8292,
                      "error": {
                        "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#scr-home.active') to be visible\u001b[22m\n",
                        "stack": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#scr-home.active') to be visible\u001b[22m\n\n    at /var/www/360lm/tests/hr.spec.js:80:16",
                        "location": {
                          "file": "/var/www/360lm/tests/hr.spec.js",
                          "column": 16,
                          "line": 80
                        },
                        "snippet": "  78 |   test('scr-attendance topbar Hub button navigates to /hub/', async ({ page }) => {\n  79 |     await page.goto(BASE_URL);\n> 80 |     await page.waitForSelector('#scr-home.active', { timeout: 8000 });\n     |                ^\n  81 |     await page.locator('.bnav-item[onclick*=\"attendance\"], .bnav-item:has-text(\"Attendance\")').first().click();\n  82 |     await expect(page.locator('#scr-attendance.active')).toBeVisible({ timeout: 5000 });\n  83 |     await page.locator('#scr-attendance .topbar .hub-btn').click();"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/hr.spec.js",
                            "column": 16,
                            "line": 80
                          },
                          "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#scr-home.active') to be visible\u001b[22m\n\n\n  78 |   test('scr-attendance topbar Hub button navigates to /hub/', async ({ page }) => {\n  79 |     await page.goto(BASE_URL);\n> 80 |     await page.waitForSelector('#scr-home.active', { timeout: 8000 });\n     |                ^\n  81 |     await page.locator('.bnav-item[onclick*=\"attendance\"], .bnav-item:has-text(\"Attendance\")').first().click();\n  82 |     await expect(page.locator('#scr-attendance.active')).toBeVisible({ timeout: 5000 });\n  83 |     await page.locator('#scr-attendance .topbar .hub-btn').click();\n    at /var/www/360lm/tests/hr.spec.js:80:16"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 1,
                      "startTime": "2026-07-12T13:00:09.807Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "screenshot",
                          "contentType": "image/png",
                          "path": "/var/www/360lm/test-results/hr-HR-Management-—-User-Ba-1a182-ub-button-navigates-to-hub--android-chrome-retry1/test-failed-1.png"
                        },
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/hr-HR-Management-—-User-Ba-1a182-ub-button-navigates-to-hub--android-chrome-retry1/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/hr.spec.js",
                        "column": 16,
                        "line": 80
                      }
                    }
                  ],
                  "status": "unexpected"
                }
              ],
              "id": "98264c29bbadd0584463-1c46bc3e940f14e12a43",
              "file": "hr.spec.js",
              "line": 78,
              "column": 3
            },
            {
              "title": "scr-attendance gets a topbar prepended with Hub + Logout",
              "ok": false,
              "tags": [],
              "tests": [
                {
                  "timeout": 30000,
                  "annotations": [],
                  "expectedStatus": "passed",
                  "projectId": "desktop-chrome",
                  "projectName": "desktop-chrome",
                  "results": [
                    {
                      "workerIndex": 206,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 8323,
                      "error": {
                        "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#scr-home.active') to be visible\u001b[22m\n",
                        "stack": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#scr-home.active') to be visible\u001b[22m\n\n    at /var/www/360lm/tests/hr.spec.js:69:16",
                        "location": {
                          "file": "/var/www/360lm/tests/hr.spec.js",
                          "column": 16,
                          "line": 69
                        },
                        "snippet": "  67 |   test('scr-attendance gets a topbar prepended with Hub + Logout', async ({ page }) => {\n  68 |     await page.goto(BASE_URL);\n> 69 |     await page.waitForSelector('#scr-home.active', { timeout: 8000 });\n     |                ^\n  70 |     // Navigate to attendance via bottom nav\n  71 |     await page.locator('.bnav-item[onclick*=\"attendance\"], .bnav-item:has-text(\"Attendance\")').first().click();\n  72 |     await expect(page.locator('#scr-attendance.active')).toBeVisible({ timeout: 5000 });"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/hr.spec.js",
                            "column": 16,
                            "line": 69
                          },
                          "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#scr-home.active') to be visible\u001b[22m\n\n\n  67 |   test('scr-attendance gets a topbar prepended with Hub + Logout', async ({ page }) => {\n  68 |     await page.goto(BASE_URL);\n> 69 |     await page.waitForSelector('#scr-home.active', { timeout: 8000 });\n     |                ^\n  70 |     // Navigate to attendance via bottom nav\n  71 |     await page.locator('.bnav-item[onclick*=\"attendance\"], .bnav-item:has-text(\"Attendance\")').first().click();\n  72 |     await expect(page.locator('#scr-attendance.active')).toBeVisible({ timeout: 5000 });\n    at /var/www/360lm/tests/hr.spec.js:69:16"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 0,
                      "startTime": "2026-07-12T13:19:55.555Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "screenshot",
                          "contentType": "image/png",
                          "path": "/var/www/360lm/test-results/hr-HR-Management-—-User-Ba-1c19d-r-prepended-with-Hub-Logout-desktop-chrome/test-failed-1.png"
                        },
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/hr-HR-Management-—-User-Ba-1c19d-r-prepended-with-Hub-Logout-desktop-chrome/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/hr.spec.js",
                        "column": 16,
                        "line": 69
                      }
                    },
                    {
                      "workerIndex": 207,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 8537,
                      "error": {
                        "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#scr-home.active') to be visible\u001b[22m\n",
                        "stack": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#scr-home.active') to be visible\u001b[22m\n\n    at /var/www/360lm/tests/hr.spec.js:69:16",
                        "location": {
                          "file": "/var/www/360lm/tests/hr.spec.js",
                          "column": 16,
                          "line": 69
                        },
                        "snippet": "  67 |   test('scr-attendance gets a topbar prepended with Hub + Logout', async ({ page }) => {\n  68 |     await page.goto(BASE_URL);\n> 69 |     await page.waitForSelector('#scr-home.active', { timeout: 8000 });\n     |                ^\n  70 |     // Navigate to attendance via bottom nav\n  71 |     await page.locator('.bnav-item[onclick*=\"attendance\"], .bnav-item:has-text(\"Attendance\")').first().click();\n  72 |     await expect(page.locator('#scr-attendance.active')).toBeVisible({ timeout: 5000 });"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/hr.spec.js",
                            "column": 16,
                            "line": 69
                          },
                          "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#scr-home.active') to be visible\u001b[22m\n\n\n  67 |   test('scr-attendance gets a topbar prepended with Hub + Logout', async ({ page }) => {\n  68 |     await page.goto(BASE_URL);\n> 69 |     await page.waitForSelector('#scr-home.active', { timeout: 8000 });\n     |                ^\n  70 |     // Navigate to attendance via bottom nav\n  71 |     await page.locator('.bnav-item[onclick*=\"attendance\"], .bnav-item:has-text(\"Attendance\")').first().click();\n  72 |     await expect(page.locator('#scr-attendance.active')).toBeVisible({ timeout: 5000 });\n    at /var/www/360lm/tests/hr.spec.js:69:16"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 1,
                      "startTime": "2026-07-12T13:20:05.506Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "screenshot",
                          "contentType": "image/png",
                          "path": "/var/www/360lm/test-results/hr-HR-Management-—-User-Ba-1c19d-r-prepended-with-Hub-Logout-desktop-chrome-retry1/test-failed-1.png"
                        },
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/hr-HR-Management-—-User-Ba-1c19d-r-prepended-with-Hub-Logout-desktop-chrome-retry1/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/hr.spec.js",
                        "column": 16,
                        "line": 69
                      }
                    }
                  ],
                  "status": "unexpected"
                }
              ],
              "id": "98264c29bbadd0584463-f4a105b5880391714503",
              "file": "hr.spec.js",
              "line": 67,
              "column": 3
            },
            {
              "title": "scr-attendance topbar Hub button navigates to /hub/",
              "ok": false,
              "tags": [],
              "tests": [
                {
                  "timeout": 30000,
                  "annotations": [],
                  "expectedStatus": "passed",
                  "projectId": "desktop-chrome",
                  "projectName": "desktop-chrome",
                  "results": [
                    {
                      "workerIndex": 208,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 8329,
                      "error": {
                        "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#scr-home.active') to be visible\u001b[22m\n",
                        "stack": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#scr-home.active') to be visible\u001b[22m\n\n    at /var/www/360lm/tests/hr.spec.js:80:16",
                        "location": {
                          "file": "/var/www/360lm/tests/hr.spec.js",
                          "column": 16,
                          "line": 80
                        },
                        "snippet": "  78 |   test('scr-attendance topbar Hub button navigates to /hub/', async ({ page }) => {\n  79 |     await page.goto(BASE_URL);\n> 80 |     await page.waitForSelector('#scr-home.active', { timeout: 8000 });\n     |                ^\n  81 |     await page.locator('.bnav-item[onclick*=\"attendance\"], .bnav-item:has-text(\"Attendance\")').first().click();\n  82 |     await expect(page.locator('#scr-attendance.active')).toBeVisible({ timeout: 5000 });\n  83 |     await page.locator('#scr-attendance .topbar .hub-btn').click();"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/hr.spec.js",
                            "column": 16,
                            "line": 80
                          },
                          "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#scr-home.active') to be visible\u001b[22m\n\n\n  78 |   test('scr-attendance topbar Hub button navigates to /hub/', async ({ page }) => {\n  79 |     await page.goto(BASE_URL);\n> 80 |     await page.waitForSelector('#scr-home.active', { timeout: 8000 });\n     |                ^\n  81 |     await page.locator('.bnav-item[onclick*=\"attendance\"], .bnav-item:has-text(\"Attendance\")').first().click();\n  82 |     await expect(page.locator('#scr-attendance.active')).toBeVisible({ timeout: 5000 });\n  83 |     await page.locator('#scr-attendance .topbar .hub-btn').click();\n    at /var/www/360lm/tests/hr.spec.js:80:16"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 0,
                      "startTime": "2026-07-12T13:20:15.550Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "screenshot",
                          "contentType": "image/png",
                          "path": "/var/www/360lm/test-results/hr-HR-Management-—-User-Ba-1a182-ub-button-navigates-to-hub--desktop-chrome/test-failed-1.png"
                        },
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/hr-HR-Management-—-User-Ba-1a182-ub-button-navigates-to-hub--desktop-chrome/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/hr.spec.js",
                        "column": 16,
                        "line": 80
                      }
                    },
                    {
                      "workerIndex": 209,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 8275,
                      "error": {
                        "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#scr-home.active') to be visible\u001b[22m\n",
                        "stack": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#scr-home.active') to be visible\u001b[22m\n\n    at /var/www/360lm/tests/hr.spec.js:80:16",
                        "location": {
                          "file": "/var/www/360lm/tests/hr.spec.js",
                          "column": 16,
                          "line": 80
                        },
                        "snippet": "  78 |   test('scr-attendance topbar Hub button navigates to /hub/', async ({ page }) => {\n  79 |     await page.goto(BASE_URL);\n> 80 |     await page.waitForSelector('#scr-home.active', { timeout: 8000 });\n     |                ^\n  81 |     await page.locator('.bnav-item[onclick*=\"attendance\"], .bnav-item:has-text(\"Attendance\")').first().click();\n  82 |     await expect(page.locator('#scr-attendance.active')).toBeVisible({ timeout: 5000 });\n  83 |     await page.locator('#scr-attendance .topbar .hub-btn').click();"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/hr.spec.js",
                            "column": 16,
                            "line": 80
                          },
                          "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#scr-home.active') to be visible\u001b[22m\n\n\n  78 |   test('scr-attendance topbar Hub button navigates to /hub/', async ({ page }) => {\n  79 |     await page.goto(BASE_URL);\n> 80 |     await page.waitForSelector('#scr-home.active', { timeout: 8000 });\n     |                ^\n  81 |     await page.locator('.bnav-item[onclick*=\"attendance\"], .bnav-item:has-text(\"Attendance\")').first().click();\n  82 |     await expect(page.locator('#scr-attendance.active')).toBeVisible({ timeout: 5000 });\n  83 |     await page.locator('#scr-attendance .topbar .hub-btn').click();\n    at /var/www/360lm/tests/hr.spec.js:80:16"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 1,
                      "startTime": "2026-07-12T13:20:24.912Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "screenshot",
                          "contentType": "image/png",
                          "path": "/var/www/360lm/test-results/hr-HR-Management-—-User-Ba-1a182-ub-button-navigates-to-hub--desktop-chrome-retry1/test-failed-1.png"
                        },
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/hr-HR-Management-—-User-Ba-1a182-ub-button-navigates-to-hub--desktop-chrome-retry1/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/hr.spec.js",
                        "column": 16,
                        "line": 80
                      }
                    }
                  ],
                  "status": "unexpected"
                }
              ],
              "id": "98264c29bbadd0584463-ed0ffc73d9c06bd3b6a1",
              "file": "hr.spec.js",
              "line": 78,
              "column": 3
            }
          ]
        },
        {
          "title": "HR Management — User Bar: Employee Role",
          "file": "hr.spec.js",
          "line": 88,
          "column": 6,
          "specs": [
            {
              "title": "employee home topbar shows their name",
              "ok": false,
              "tags": [],
              "tests": [
                {
                  "timeout": 30000,
                  "annotations": [],
                  "expectedStatus": "passed",
                  "projectId": "android-chrome",
                  "projectName": "android-chrome",
                  "results": [
                    {
                      "workerIndex": 34,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 8272,
                      "error": {
                        "message": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mlocator\u001b[39m\u001b[2m).\u001b[22mtoHaveText\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m)\u001b[22m failed\n\nLocator: locator('#scr-home .topbar .topbar-user')\nExpected pattern: \u001b[32m/Mukesh/\u001b[39m\nTimeout: 8000ms\nError: element(s) not found\n\nCall log:\n\u001b[2m  - Expect \"toHaveText\" with timeout 8000ms\u001b[22m\n\u001b[2m  - waiting for locator('#scr-home .topbar .topbar-user')\u001b[22m\n",
                        "stack": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mlocator\u001b[39m\u001b[2m).\u001b[22mtoHaveText\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m)\u001b[22m failed\n\nLocator: locator('#scr-home .topbar .topbar-user')\nExpected pattern: \u001b[32m/Mukesh/\u001b[39m\nTimeout: 8000ms\nError: element(s) not found\n\nCall log:\n\u001b[2m  - Expect \"toHaveText\" with timeout 8000ms\u001b[22m\n\u001b[2m  - waiting for locator('#scr-home .topbar .topbar-user')\u001b[22m\n\n    at /var/www/360lm/tests/hr.spec.js:95:66",
                        "location": {
                          "file": "/var/www/360lm/tests/hr.spec.js",
                          "column": 66,
                          "line": 95
                        },
                        "snippet": "  93 |   test('employee home topbar shows their name', async ({ page }) => {\n  94 |     await page.goto(BASE_URL);\n> 95 |     await expect(page.locator('#scr-home .topbar .topbar-user')).toHaveText(/Mukesh/, { timeout: 8000 });\n     |                                                                  ^\n  96 |   });\n  97 |\n  98 |   test('employee home topbar has Logout button', async ({ page }) => {"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/hr.spec.js",
                            "column": 66,
                            "line": 95
                          },
                          "message": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mlocator\u001b[39m\u001b[2m).\u001b[22mtoHaveText\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m)\u001b[22m failed\n\nLocator: locator('#scr-home .topbar .topbar-user')\nExpected pattern: \u001b[32m/Mukesh/\u001b[39m\nTimeout: 8000ms\nError: element(s) not found\n\nCall log:\n\u001b[2m  - Expect \"toHaveText\" with timeout 8000ms\u001b[22m\n\u001b[2m  - waiting for locator('#scr-home .topbar .topbar-user')\u001b[22m\n\n\n  93 |   test('employee home topbar shows their name', async ({ page }) => {\n  94 |     await page.goto(BASE_URL);\n> 95 |     await expect(page.locator('#scr-home .topbar .topbar-user')).toHaveText(/Mukesh/, { timeout: 8000 });\n     |                                                                  ^\n  96 |   });\n  97 |\n  98 |   test('employee home topbar has Logout button', async ({ page }) => {\n    at /var/www/360lm/tests/hr.spec.js:95:66"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 0,
                      "startTime": "2026-07-12T13:00:19.165Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "screenshot",
                          "contentType": "image/png",
                          "path": "/var/www/360lm/test-results/hr-HR-Management-—-User-Ba-977b0-ome-topbar-shows-their-name-android-chrome/test-failed-1.png"
                        },
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/hr-HR-Management-—-User-Ba-977b0-ome-topbar-shows-their-name-android-chrome/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/hr.spec.js",
                        "column": 66,
                        "line": 95
                      }
                    },
                    {
                      "workerIndex": 35,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 8298,
                      "error": {
                        "message": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mlocator\u001b[39m\u001b[2m).\u001b[22mtoHaveText\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m)\u001b[22m failed\n\nLocator: locator('#scr-home .topbar .topbar-user')\nExpected pattern: \u001b[32m/Mukesh/\u001b[39m\nTimeout: 8000ms\nError: element(s) not found\n\nCall log:\n\u001b[2m  - Expect \"toHaveText\" with timeout 8000ms\u001b[22m\n\u001b[2m  - waiting for locator('#scr-home .topbar .topbar-user')\u001b[22m\n",
                        "stack": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mlocator\u001b[39m\u001b[2m).\u001b[22mtoHaveText\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m)\u001b[22m failed\n\nLocator: locator('#scr-home .topbar .topbar-user')\nExpected pattern: \u001b[32m/Mukesh/\u001b[39m\nTimeout: 8000ms\nError: element(s) not found\n\nCall log:\n\u001b[2m  - Expect \"toHaveText\" with timeout 8000ms\u001b[22m\n\u001b[2m  - waiting for locator('#scr-home .topbar .topbar-user')\u001b[22m\n\n    at /var/www/360lm/tests/hr.spec.js:95:66",
                        "location": {
                          "file": "/var/www/360lm/tests/hr.spec.js",
                          "column": 66,
                          "line": 95
                        },
                        "snippet": "  93 |   test('employee home topbar shows their name', async ({ page }) => {\n  94 |     await page.goto(BASE_URL);\n> 95 |     await expect(page.locator('#scr-home .topbar .topbar-user')).toHaveText(/Mukesh/, { timeout: 8000 });\n     |                                                                  ^\n  96 |   });\n  97 |\n  98 |   test('employee home topbar has Logout button', async ({ page }) => {"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/hr.spec.js",
                            "column": 66,
                            "line": 95
                          },
                          "message": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mlocator\u001b[39m\u001b[2m).\u001b[22mtoHaveText\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m)\u001b[22m failed\n\nLocator: locator('#scr-home .topbar .topbar-user')\nExpected pattern: \u001b[32m/Mukesh/\u001b[39m\nTimeout: 8000ms\nError: element(s) not found\n\nCall log:\n\u001b[2m  - Expect \"toHaveText\" with timeout 8000ms\u001b[22m\n\u001b[2m  - waiting for locator('#scr-home .topbar .topbar-user')\u001b[22m\n\n\n  93 |   test('employee home topbar shows their name', async ({ page }) => {\n  94 |     await page.goto(BASE_URL);\n> 95 |     await expect(page.locator('#scr-home .topbar .topbar-user')).toHaveText(/Mukesh/, { timeout: 8000 });\n     |                                                                  ^\n  96 |   });\n  97 |\n  98 |   test('employee home topbar has Logout button', async ({ page }) => {\n    at /var/www/360lm/tests/hr.spec.js:95:66"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 1,
                      "startTime": "2026-07-12T13:00:28.442Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "screenshot",
                          "contentType": "image/png",
                          "path": "/var/www/360lm/test-results/hr-HR-Management-—-User-Ba-977b0-ome-topbar-shows-their-name-android-chrome-retry1/test-failed-1.png"
                        },
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/hr-HR-Management-—-User-Ba-977b0-ome-topbar-shows-their-name-android-chrome-retry1/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/hr.spec.js",
                        "column": 66,
                        "line": 95
                      }
                    }
                  ],
                  "status": "unexpected"
                }
              ],
              "id": "98264c29bbadd0584463-df62e39b02edf42734c3",
              "file": "hr.spec.js",
              "line": 93,
              "column": 3
            },
            {
              "title": "employee home topbar has Logout button",
              "ok": false,
              "tags": [],
              "tests": [
                {
                  "timeout": 30000,
                  "annotations": [],
                  "expectedStatus": "passed",
                  "projectId": "android-chrome",
                  "projectName": "android-chrome",
                  "results": [
                    {
                      "workerIndex": 36,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 8257,
                      "error": {
                        "message": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mlocator\u001b[39m\u001b[2m).\u001b[22mtoBeVisible\u001b[2m(\u001b[22m\u001b[2m)\u001b[22m failed\n\nLocator: locator('#scr-home .topbar .hub-logout-btn')\nExpected: visible\nTimeout: 8000ms\nError: element(s) not found\n\nCall log:\n\u001b[2m  - Expect \"toBeVisible\" with timeout 8000ms\u001b[22m\n\u001b[2m  - waiting for locator('#scr-home .topbar .hub-logout-btn')\u001b[22m\n",
                        "stack": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mlocator\u001b[39m\u001b[2m).\u001b[22mtoBeVisible\u001b[2m(\u001b[22m\u001b[2m)\u001b[22m failed\n\nLocator: locator('#scr-home .topbar .hub-logout-btn')\nExpected: visible\nTimeout: 8000ms\nError: element(s) not found\n\nCall log:\n\u001b[2m  - Expect \"toBeVisible\" with timeout 8000ms\u001b[22m\n\u001b[2m  - waiting for locator('#scr-home .topbar .hub-logout-btn')\u001b[22m\n\n    at /var/www/360lm/tests/hr.spec.js:100:69",
                        "location": {
                          "file": "/var/www/360lm/tests/hr.spec.js",
                          "column": 69,
                          "line": 100
                        },
                        "snippet": "   98 |   test('employee home topbar has Logout button', async ({ page }) => {\n   99 |     await page.goto(BASE_URL);\n> 100 |     await expect(page.locator('#scr-home .topbar .hub-logout-btn')).toBeVisible({ timeout: 8000 });\n      |                                                                     ^\n  101 |   });\n  102 | });\n  103 |"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/hr.spec.js",
                            "column": 69,
                            "line": 100
                          },
                          "message": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mlocator\u001b[39m\u001b[2m).\u001b[22mtoBeVisible\u001b[2m(\u001b[22m\u001b[2m)\u001b[22m failed\n\nLocator: locator('#scr-home .topbar .hub-logout-btn')\nExpected: visible\nTimeout: 8000ms\nError: element(s) not found\n\nCall log:\n\u001b[2m  - Expect \"toBeVisible\" with timeout 8000ms\u001b[22m\n\u001b[2m  - waiting for locator('#scr-home .topbar .hub-logout-btn')\u001b[22m\n\n\n   98 |   test('employee home topbar has Logout button', async ({ page }) => {\n   99 |     await page.goto(BASE_URL);\n> 100 |     await expect(page.locator('#scr-home .topbar .hub-logout-btn')).toBeVisible({ timeout: 8000 });\n      |                                                                     ^\n  101 |   });\n  102 | });\n  103 |\n    at /var/www/360lm/tests/hr.spec.js:100:69"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 0,
                      "startTime": "2026-07-12T13:00:37.821Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "screenshot",
                          "contentType": "image/png",
                          "path": "/var/www/360lm/test-results/hr-HR-Management-—-User-Ba-b7c29-me-topbar-has-Logout-button-android-chrome/test-failed-1.png"
                        },
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/hr-HR-Management-—-User-Ba-b7c29-me-topbar-has-Logout-button-android-chrome/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/hr.spec.js",
                        "column": 69,
                        "line": 100
                      }
                    },
                    {
                      "workerIndex": 37,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 8230,
                      "error": {
                        "message": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mlocator\u001b[39m\u001b[2m).\u001b[22mtoBeVisible\u001b[2m(\u001b[22m\u001b[2m)\u001b[22m failed\n\nLocator: locator('#scr-home .topbar .hub-logout-btn')\nExpected: visible\nTimeout: 8000ms\nError: element(s) not found\n\nCall log:\n\u001b[2m  - Expect \"toBeVisible\" with timeout 8000ms\u001b[22m\n\u001b[2m  - waiting for locator('#scr-home .topbar .hub-logout-btn')\u001b[22m\n",
                        "stack": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mlocator\u001b[39m\u001b[2m).\u001b[22mtoBeVisible\u001b[2m(\u001b[22m\u001b[2m)\u001b[22m failed\n\nLocator: locator('#scr-home .topbar .hub-logout-btn')\nExpected: visible\nTimeout: 8000ms\nError: element(s) not found\n\nCall log:\n\u001b[2m  - Expect \"toBeVisible\" with timeout 8000ms\u001b[22m\n\u001b[2m  - waiting for locator('#scr-home .topbar .hub-logout-btn')\u001b[22m\n\n    at /var/www/360lm/tests/hr.spec.js:100:69",
                        "location": {
                          "file": "/var/www/360lm/tests/hr.spec.js",
                          "column": 69,
                          "line": 100
                        },
                        "snippet": "   98 |   test('employee home topbar has Logout button', async ({ page }) => {\n   99 |     await page.goto(BASE_URL);\n> 100 |     await expect(page.locator('#scr-home .topbar .hub-logout-btn')).toBeVisible({ timeout: 8000 });\n      |                                                                     ^\n  101 |   });\n  102 | });\n  103 |"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/hr.spec.js",
                            "column": 69,
                            "line": 100
                          },
                          "message": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mlocator\u001b[39m\u001b[2m).\u001b[22mtoBeVisible\u001b[2m(\u001b[22m\u001b[2m)\u001b[22m failed\n\nLocator: locator('#scr-home .topbar .hub-logout-btn')\nExpected: visible\nTimeout: 8000ms\nError: element(s) not found\n\nCall log:\n\u001b[2m  - Expect \"toBeVisible\" with timeout 8000ms\u001b[22m\n\u001b[2m  - waiting for locator('#scr-home .topbar .hub-logout-btn')\u001b[22m\n\n\n   98 |   test('employee home topbar has Logout button', async ({ page }) => {\n   99 |     await page.goto(BASE_URL);\n> 100 |     await expect(page.locator('#scr-home .topbar .hub-logout-btn')).toBeVisible({ timeout: 8000 });\n      |                                                                     ^\n  101 |   });\n  102 | });\n  103 |\n    at /var/www/360lm/tests/hr.spec.js:100:69"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 1,
                      "startTime": "2026-07-12T13:00:47.142Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "screenshot",
                          "contentType": "image/png",
                          "path": "/var/www/360lm/test-results/hr-HR-Management-—-User-Ba-b7c29-me-topbar-has-Logout-button-android-chrome-retry1/test-failed-1.png"
                        },
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/hr-HR-Management-—-User-Ba-b7c29-me-topbar-has-Logout-button-android-chrome-retry1/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/hr.spec.js",
                        "column": 69,
                        "line": 100
                      }
                    }
                  ],
                  "status": "unexpected"
                }
              ],
              "id": "98264c29bbadd0584463-4882593ca53c335695b3",
              "file": "hr.spec.js",
              "line": 98,
              "column": 3
            },
            {
              "title": "employee home topbar shows their name",
              "ok": false,
              "tags": [],
              "tests": [
                {
                  "timeout": 30000,
                  "annotations": [],
                  "expectedStatus": "passed",
                  "projectId": "desktop-chrome",
                  "projectName": "desktop-chrome",
                  "results": [
                    {
                      "workerIndex": 210,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 8272,
                      "error": {
                        "message": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mlocator\u001b[39m\u001b[2m).\u001b[22mtoHaveText\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m)\u001b[22m failed\n\nLocator: locator('#scr-home .topbar .topbar-user')\nExpected pattern: \u001b[32m/Mukesh/\u001b[39m\nTimeout: 8000ms\nError: element(s) not found\n\nCall log:\n\u001b[2m  - Expect \"toHaveText\" with timeout 8000ms\u001b[22m\n\u001b[2m  - waiting for locator('#scr-home .topbar .topbar-user')\u001b[22m\n",
                        "stack": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mlocator\u001b[39m\u001b[2m).\u001b[22mtoHaveText\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m)\u001b[22m failed\n\nLocator: locator('#scr-home .topbar .topbar-user')\nExpected pattern: \u001b[32m/Mukesh/\u001b[39m\nTimeout: 8000ms\nError: element(s) not found\n\nCall log:\n\u001b[2m  - Expect \"toHaveText\" with timeout 8000ms\u001b[22m\n\u001b[2m  - waiting for locator('#scr-home .topbar .topbar-user')\u001b[22m\n\n    at /var/www/360lm/tests/hr.spec.js:95:66",
                        "location": {
                          "file": "/var/www/360lm/tests/hr.spec.js",
                          "column": 66,
                          "line": 95
                        },
                        "snippet": "  93 |   test('employee home topbar shows their name', async ({ page }) => {\n  94 |     await page.goto(BASE_URL);\n> 95 |     await expect(page.locator('#scr-home .topbar .topbar-user')).toHaveText(/Mukesh/, { timeout: 8000 });\n     |                                                                  ^\n  96 |   });\n  97 |\n  98 |   test('employee home topbar has Logout button', async ({ page }) => {"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/hr.spec.js",
                            "column": 66,
                            "line": 95
                          },
                          "message": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mlocator\u001b[39m\u001b[2m).\u001b[22mtoHaveText\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m)\u001b[22m failed\n\nLocator: locator('#scr-home .topbar .topbar-user')\nExpected pattern: \u001b[32m/Mukesh/\u001b[39m\nTimeout: 8000ms\nError: element(s) not found\n\nCall log:\n\u001b[2m  - Expect \"toHaveText\" with timeout 8000ms\u001b[22m\n\u001b[2m  - waiting for locator('#scr-home .topbar .topbar-user')\u001b[22m\n\n\n  93 |   test('employee home topbar shows their name', async ({ page }) => {\n  94 |     await page.goto(BASE_URL);\n> 95 |     await expect(page.locator('#scr-home .topbar .topbar-user')).toHaveText(/Mukesh/, { timeout: 8000 });\n     |                                                                  ^\n  96 |   });\n  97 |\n  98 |   test('employee home topbar has Logout button', async ({ page }) => {\n    at /var/www/360lm/tests/hr.spec.js:95:66"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 0,
                      "startTime": "2026-07-12T13:20:34.268Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "screenshot",
                          "contentType": "image/png",
                          "path": "/var/www/360lm/test-results/hr-HR-Management-—-User-Ba-977b0-ome-topbar-shows-their-name-desktop-chrome/test-failed-1.png"
                        },
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/hr-HR-Management-—-User-Ba-977b0-ome-topbar-shows-their-name-desktop-chrome/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/hr.spec.js",
                        "column": 66,
                        "line": 95
                      }
                    },
                    {
                      "workerIndex": 211,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 8325,
                      "error": {
                        "message": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mlocator\u001b[39m\u001b[2m).\u001b[22mtoHaveText\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m)\u001b[22m failed\n\nLocator: locator('#scr-home .topbar .topbar-user')\nExpected pattern: \u001b[32m/Mukesh/\u001b[39m\nTimeout: 8000ms\nError: element(s) not found\n\nCall log:\n\u001b[2m  - Expect \"toHaveText\" with timeout 8000ms\u001b[22m\n\u001b[2m  - waiting for locator('#scr-home .topbar .topbar-user')\u001b[22m\n",
                        "stack": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mlocator\u001b[39m\u001b[2m).\u001b[22mtoHaveText\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m)\u001b[22m failed\n\nLocator: locator('#scr-home .topbar .topbar-user')\nExpected pattern: \u001b[32m/Mukesh/\u001b[39m\nTimeout: 8000ms\nError: element(s) not found\n\nCall log:\n\u001b[2m  - Expect \"toHaveText\" with timeout 8000ms\u001b[22m\n\u001b[2m  - waiting for locator('#scr-home .topbar .topbar-user')\u001b[22m\n\n    at /var/www/360lm/tests/hr.spec.js:95:66",
                        "location": {
                          "file": "/var/www/360lm/tests/hr.spec.js",
                          "column": 66,
                          "line": 95
                        },
                        "snippet": "  93 |   test('employee home topbar shows their name', async ({ page }) => {\n  94 |     await page.goto(BASE_URL);\n> 95 |     await expect(page.locator('#scr-home .topbar .topbar-user')).toHaveText(/Mukesh/, { timeout: 8000 });\n     |                                                                  ^\n  96 |   });\n  97 |\n  98 |   test('employee home topbar has Logout button', async ({ page }) => {"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/hr.spec.js",
                            "column": 66,
                            "line": 95
                          },
                          "message": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mlocator\u001b[39m\u001b[2m).\u001b[22mtoHaveText\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m)\u001b[22m failed\n\nLocator: locator('#scr-home .topbar .topbar-user')\nExpected pattern: \u001b[32m/Mukesh/\u001b[39m\nTimeout: 8000ms\nError: element(s) not found\n\nCall log:\n\u001b[2m  - Expect \"toHaveText\" with timeout 8000ms\u001b[22m\n\u001b[2m  - waiting for locator('#scr-home .topbar .topbar-user')\u001b[22m\n\n\n  93 |   test('employee home topbar shows their name', async ({ page }) => {\n  94 |     await page.goto(BASE_URL);\n> 95 |     await expect(page.locator('#scr-home .topbar .topbar-user')).toHaveText(/Mukesh/, { timeout: 8000 });\n     |                                                                  ^\n  96 |   });\n  97 |\n  98 |   test('employee home topbar has Logout button', async ({ page }) => {\n    at /var/www/360lm/tests/hr.spec.js:95:66"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 1,
                      "startTime": "2026-07-12T13:20:43.594Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "screenshot",
                          "contentType": "image/png",
                          "path": "/var/www/360lm/test-results/hr-HR-Management-—-User-Ba-977b0-ome-topbar-shows-their-name-desktop-chrome-retry1/test-failed-1.png"
                        },
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/hr-HR-Management-—-User-Ba-977b0-ome-topbar-shows-their-name-desktop-chrome-retry1/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/hr.spec.js",
                        "column": 66,
                        "line": 95
                      }
                    }
                  ],
                  "status": "unexpected"
                }
              ],
              "id": "98264c29bbadd0584463-acfff3368cdaf76cede0",
              "file": "hr.spec.js",
              "line": 93,
              "column": 3
            },
            {
              "title": "employee home topbar has Logout button",
              "ok": false,
              "tags": [],
              "tests": [
                {
                  "timeout": 30000,
                  "annotations": [],
                  "expectedStatus": "passed",
                  "projectId": "desktop-chrome",
                  "projectName": "desktop-chrome",
                  "results": [
                    {
                      "workerIndex": 212,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 8304,
                      "error": {
                        "message": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mlocator\u001b[39m\u001b[2m).\u001b[22mtoBeVisible\u001b[2m(\u001b[22m\u001b[2m)\u001b[22m failed\n\nLocator: locator('#scr-home .topbar .hub-logout-btn')\nExpected: visible\nTimeout: 8000ms\nError: element(s) not found\n\nCall log:\n\u001b[2m  - Expect \"toBeVisible\" with timeout 8000ms\u001b[22m\n\u001b[2m  - waiting for locator('#scr-home .topbar .hub-logout-btn')\u001b[22m\n",
                        "stack": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mlocator\u001b[39m\u001b[2m).\u001b[22mtoBeVisible\u001b[2m(\u001b[22m\u001b[2m)\u001b[22m failed\n\nLocator: locator('#scr-home .topbar .hub-logout-btn')\nExpected: visible\nTimeout: 8000ms\nError: element(s) not found\n\nCall log:\n\u001b[2m  - Expect \"toBeVisible\" with timeout 8000ms\u001b[22m\n\u001b[2m  - waiting for locator('#scr-home .topbar .hub-logout-btn')\u001b[22m\n\n    at /var/www/360lm/tests/hr.spec.js:100:69",
                        "location": {
                          "file": "/var/www/360lm/tests/hr.spec.js",
                          "column": 69,
                          "line": 100
                        },
                        "snippet": "   98 |   test('employee home topbar has Logout button', async ({ page }) => {\n   99 |     await page.goto(BASE_URL);\n> 100 |     await expect(page.locator('#scr-home .topbar .hub-logout-btn')).toBeVisible({ timeout: 8000 });\n      |                                                                     ^\n  101 |   });\n  102 | });\n  103 |"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/hr.spec.js",
                            "column": 69,
                            "line": 100
                          },
                          "message": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mlocator\u001b[39m\u001b[2m).\u001b[22mtoBeVisible\u001b[2m(\u001b[22m\u001b[2m)\u001b[22m failed\n\nLocator: locator('#scr-home .topbar .hub-logout-btn')\nExpected: visible\nTimeout: 8000ms\nError: element(s) not found\n\nCall log:\n\u001b[2m  - Expect \"toBeVisible\" with timeout 8000ms\u001b[22m\n\u001b[2m  - waiting for locator('#scr-home .topbar .hub-logout-btn')\u001b[22m\n\n\n   98 |   test('employee home topbar has Logout button', async ({ page }) => {\n   99 |     await page.goto(BASE_URL);\n> 100 |     await expect(page.locator('#scr-home .topbar .hub-logout-btn')).toBeVisible({ timeout: 8000 });\n      |                                                                     ^\n  101 |   });\n  102 | });\n  103 |\n    at /var/www/360lm/tests/hr.spec.js:100:69"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 0,
                      "startTime": "2026-07-12T13:20:52.925Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "screenshot",
                          "contentType": "image/png",
                          "path": "/var/www/360lm/test-results/hr-HR-Management-—-User-Ba-b7c29-me-topbar-has-Logout-button-desktop-chrome/test-failed-1.png"
                        },
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/hr-HR-Management-—-User-Ba-b7c29-me-topbar-has-Logout-button-desktop-chrome/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/hr.spec.js",
                        "column": 69,
                        "line": 100
                      }
                    },
                    {
                      "workerIndex": 213,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 8346,
                      "error": {
                        "message": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mlocator\u001b[39m\u001b[2m).\u001b[22mtoBeVisible\u001b[2m(\u001b[22m\u001b[2m)\u001b[22m failed\n\nLocator: locator('#scr-home .topbar .hub-logout-btn')\nExpected: visible\nTimeout: 8000ms\nError: element(s) not found\n\nCall log:\n\u001b[2m  - Expect \"toBeVisible\" with timeout 8000ms\u001b[22m\n\u001b[2m  - waiting for locator('#scr-home .topbar .hub-logout-btn')\u001b[22m\n",
                        "stack": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mlocator\u001b[39m\u001b[2m).\u001b[22mtoBeVisible\u001b[2m(\u001b[22m\u001b[2m)\u001b[22m failed\n\nLocator: locator('#scr-home .topbar .hub-logout-btn')\nExpected: visible\nTimeout: 8000ms\nError: element(s) not found\n\nCall log:\n\u001b[2m  - Expect \"toBeVisible\" with timeout 8000ms\u001b[22m\n\u001b[2m  - waiting for locator('#scr-home .topbar .hub-logout-btn')\u001b[22m\n\n    at /var/www/360lm/tests/hr.spec.js:100:69",
                        "location": {
                          "file": "/var/www/360lm/tests/hr.spec.js",
                          "column": 69,
                          "line": 100
                        },
                        "snippet": "   98 |   test('employee home topbar has Logout button', async ({ page }) => {\n   99 |     await page.goto(BASE_URL);\n> 100 |     await expect(page.locator('#scr-home .topbar .hub-logout-btn')).toBeVisible({ timeout: 8000 });\n      |                                                                     ^\n  101 |   });\n  102 | });\n  103 |"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/hr.spec.js",
                            "column": 69,
                            "line": 100
                          },
                          "message": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mlocator\u001b[39m\u001b[2m).\u001b[22mtoBeVisible\u001b[2m(\u001b[22m\u001b[2m)\u001b[22m failed\n\nLocator: locator('#scr-home .topbar .hub-logout-btn')\nExpected: visible\nTimeout: 8000ms\nError: element(s) not found\n\nCall log:\n\u001b[2m  - Expect \"toBeVisible\" with timeout 8000ms\u001b[22m\n\u001b[2m  - waiting for locator('#scr-home .topbar .hub-logout-btn')\u001b[22m\n\n\n   98 |   test('employee home topbar has Logout button', async ({ page }) => {\n   99 |     await page.goto(BASE_URL);\n> 100 |     await expect(page.locator('#scr-home .topbar .hub-logout-btn')).toBeVisible({ timeout: 8000 });\n      |                                                                     ^\n  101 |   });\n  102 | });\n  103 |\n    at /var/www/360lm/tests/hr.spec.js:100:69"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 1,
                      "startTime": "2026-07-12T13:21:03.160Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "screenshot",
                          "contentType": "image/png",
                          "path": "/var/www/360lm/test-results/hr-HR-Management-—-User-Ba-b7c29-me-topbar-has-Logout-button-desktop-chrome-retry1/test-failed-1.png"
                        },
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/hr-HR-Management-—-User-Ba-b7c29-me-topbar-has-Logout-button-desktop-chrome-retry1/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/hr.spec.js",
                        "column": 69,
                        "line": 100
                      }
                    }
                  ],
                  "status": "unexpected"
                }
              ],
              "id": "98264c29bbadd0584463-9072c0c4372ea082848b",
              "file": "hr.spec.js",
              "line": 98,
              "column": 3
            }
          ]
        },
        {
          "title": "HR Management — Existing Functionality Unchanged",
          "file": "hr.spec.js",
          "line": 104,
          "column": 6,
          "specs": [
            {
              "title": "bottom nav renders correct items for admin",
              "ok": false,
              "tags": [],
              "tests": [
                {
                  "timeout": 30000,
                  "annotations": [],
                  "expectedStatus": "passed",
                  "projectId": "android-chrome",
                  "projectName": "android-chrome",
                  "results": [
                    {
                      "workerIndex": 38,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 8268,
                      "error": {
                        "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#scr-home.active') to be visible\u001b[22m\n",
                        "stack": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#scr-home.active') to be visible\u001b[22m\n\n    at /var/www/360lm/tests/hr.spec.js:111:16",
                        "location": {
                          "file": "/var/www/360lm/tests/hr.spec.js",
                          "column": 16,
                          "line": 111
                        },
                        "snippet": "  109 |   test('bottom nav renders correct items for admin', async ({ page }) => {\n  110 |     await page.goto(BASE_URL);\n> 111 |     await page.waitForSelector('#scr-home.active', { timeout: 8000 });\n      |                ^\n  112 |     const bnavCount = await page.locator('.bnav-item').count();\n  113 |     expect(bnavCount).toBeGreaterThanOrEqual(3);\n  114 |   });"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/hr.spec.js",
                            "column": 16,
                            "line": 111
                          },
                          "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#scr-home.active') to be visible\u001b[22m\n\n\n  109 |   test('bottom nav renders correct items for admin', async ({ page }) => {\n  110 |     await page.goto(BASE_URL);\n> 111 |     await page.waitForSelector('#scr-home.active', { timeout: 8000 });\n      |                ^\n  112 |     const bnavCount = await page.locator('.bnav-item').count();\n  113 |     expect(bnavCount).toBeGreaterThanOrEqual(3);\n  114 |   });\n    at /var/www/360lm/tests/hr.spec.js:111:16"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 0,
                      "startTime": "2026-07-12T13:00:56.347Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "screenshot",
                          "contentType": "image/png",
                          "path": "/var/www/360lm/test-results/hr-HR-Management-—-Existin-ee07e-ers-correct-items-for-admin-android-chrome/test-failed-1.png"
                        },
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/hr-HR-Management-—-Existin-ee07e-ers-correct-items-for-admin-android-chrome/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/hr.spec.js",
                        "column": 16,
                        "line": 111
                      }
                    },
                    {
                      "workerIndex": 39,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 8292,
                      "error": {
                        "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#scr-home.active') to be visible\u001b[22m\n",
                        "stack": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#scr-home.active') to be visible\u001b[22m\n\n    at /var/www/360lm/tests/hr.spec.js:111:16",
                        "location": {
                          "file": "/var/www/360lm/tests/hr.spec.js",
                          "column": 16,
                          "line": 111
                        },
                        "snippet": "  109 |   test('bottom nav renders correct items for admin', async ({ page }) => {\n  110 |     await page.goto(BASE_URL);\n> 111 |     await page.waitForSelector('#scr-home.active', { timeout: 8000 });\n      |                ^\n  112 |     const bnavCount = await page.locator('.bnav-item').count();\n  113 |     expect(bnavCount).toBeGreaterThanOrEqual(3);\n  114 |   });"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/hr.spec.js",
                            "column": 16,
                            "line": 111
                          },
                          "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#scr-home.active') to be visible\u001b[22m\n\n\n  109 |   test('bottom nav renders correct items for admin', async ({ page }) => {\n  110 |     await page.goto(BASE_URL);\n> 111 |     await page.waitForSelector('#scr-home.active', { timeout: 8000 });\n      |                ^\n  112 |     const bnavCount = await page.locator('.bnav-item').count();\n  113 |     expect(bnavCount).toBeGreaterThanOrEqual(3);\n  114 |   });\n    at /var/www/360lm/tests/hr.spec.js:111:16"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 1,
                      "startTime": "2026-07-12T13:01:05.691Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "screenshot",
                          "contentType": "image/png",
                          "path": "/var/www/360lm/test-results/hr-HR-Management-—-Existin-ee07e-ers-correct-items-for-admin-android-chrome-retry1/test-failed-1.png"
                        },
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/hr-HR-Management-—-Existin-ee07e-ers-correct-items-for-admin-android-chrome-retry1/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/hr.spec.js",
                        "column": 16,
                        "line": 111
                      }
                    }
                  ],
                  "status": "unexpected"
                }
              ],
              "id": "98264c29bbadd0584463-2ce28d43100e8a510f28",
              "file": "hr.spec.js",
              "line": 109,
              "column": 3
            },
            {
              "title": "home content area is populated",
              "ok": false,
              "tags": [],
              "tests": [
                {
                  "timeout": 30000,
                  "annotations": [],
                  "expectedStatus": "passed",
                  "projectId": "android-chrome",
                  "projectName": "android-chrome",
                  "results": [
                    {
                      "workerIndex": 40,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 8245,
                      "error": {
                        "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#scr-home.active') to be visible\u001b[22m\n",
                        "stack": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#scr-home.active') to be visible\u001b[22m\n\n    at /var/www/360lm/tests/hr.spec.js:118:16",
                        "location": {
                          "file": "/var/www/360lm/tests/hr.spec.js",
                          "column": 16,
                          "line": 118
                        },
                        "snippet": "  116 |   test('home content area is populated', async ({ page }) => {\n  117 |     await page.goto(BASE_URL);\n> 118 |     await page.waitForSelector('#scr-home.active', { timeout: 8000 });\n      |                ^\n  119 |     await expect(page.locator('#home-content')).toBeVisible();\n  120 |   });\n  121 |"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/hr.spec.js",
                            "column": 16,
                            "line": 118
                          },
                          "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#scr-home.active') to be visible\u001b[22m\n\n\n  116 |   test('home content area is populated', async ({ page }) => {\n  117 |     await page.goto(BASE_URL);\n> 118 |     await page.waitForSelector('#scr-home.active', { timeout: 8000 });\n      |                ^\n  119 |     await expect(page.locator('#home-content')).toBeVisible();\n  120 |   });\n  121 |\n    at /var/www/360lm/tests/hr.spec.js:118:16"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 0,
                      "startTime": "2026-07-12T13:01:15.034Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "screenshot",
                          "contentType": "image/png",
                          "path": "/var/www/360lm/test-results/hr-HR-Management-—-Existin-7f588-e-content-area-is-populated-android-chrome/test-failed-1.png"
                        },
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/hr-HR-Management-—-Existin-7f588-e-content-area-is-populated-android-chrome/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/hr.spec.js",
                        "column": 16,
                        "line": 118
                      }
                    },
                    {
                      "workerIndex": 41,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 8244,
                      "error": {
                        "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#scr-home.active') to be visible\u001b[22m\n",
                        "stack": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#scr-home.active') to be visible\u001b[22m\n\n    at /var/www/360lm/tests/hr.spec.js:118:16",
                        "location": {
                          "file": "/var/www/360lm/tests/hr.spec.js",
                          "column": 16,
                          "line": 118
                        },
                        "snippet": "  116 |   test('home content area is populated', async ({ page }) => {\n  117 |     await page.goto(BASE_URL);\n> 118 |     await page.waitForSelector('#scr-home.active', { timeout: 8000 });\n      |                ^\n  119 |     await expect(page.locator('#home-content')).toBeVisible();\n  120 |   });\n  121 |"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/hr.spec.js",
                            "column": 16,
                            "line": 118
                          },
                          "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#scr-home.active') to be visible\u001b[22m\n\n\n  116 |   test('home content area is populated', async ({ page }) => {\n  117 |     await page.goto(BASE_URL);\n> 118 |     await page.waitForSelector('#scr-home.active', { timeout: 8000 });\n      |                ^\n  119 |     await expect(page.locator('#home-content')).toBeVisible();\n  120 |   });\n  121 |\n    at /var/www/360lm/tests/hr.spec.js:118:16"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 1,
                      "startTime": "2026-07-12T13:01:24.183Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "screenshot",
                          "contentType": "image/png",
                          "path": "/var/www/360lm/test-results/hr-HR-Management-—-Existin-7f588-e-content-area-is-populated-android-chrome-retry1/test-failed-1.png"
                        },
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/hr-HR-Management-—-Existin-7f588-e-content-area-is-populated-android-chrome-retry1/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/hr.spec.js",
                        "column": 16,
                        "line": 118
                      }
                    }
                  ],
                  "status": "unexpected"
                }
              ],
              "id": "98264c29bbadd0584463-876327021cf36189da99",
              "file": "hr.spec.js",
              "line": 116,
              "column": 3
            },
            {
              "title": "existing Hub button still works on home topbar",
              "ok": false,
              "tags": [],
              "tests": [
                {
                  "timeout": 30000,
                  "annotations": [],
                  "expectedStatus": "passed",
                  "projectId": "android-chrome",
                  "projectName": "android-chrome",
                  "results": [
                    {
                      "workerIndex": 42,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 8241,
                      "error": {
                        "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#scr-home.active') to be visible\u001b[22m\n",
                        "stack": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#scr-home.active') to be visible\u001b[22m\n\n    at /var/www/360lm/tests/hr.spec.js:124:16",
                        "location": {
                          "file": "/var/www/360lm/tests/hr.spec.js",
                          "column": 16,
                          "line": 124
                        },
                        "snippet": "  122 |   test('existing Hub button still works on home topbar', async ({ page }) => {\n  123 |     await page.goto(BASE_URL);\n> 124 |     await page.waitForSelector('#scr-home.active', { timeout: 8000 });\n      |                ^\n  125 |     // Both existing hub-btn and new logout-btn should coexist in topbar\n  126 |     await expect(page.locator('#scr-home .topbar .hub-btn')).toBeVisible();\n  127 |   });"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/hr.spec.js",
                            "column": 16,
                            "line": 124
                          },
                          "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#scr-home.active') to be visible\u001b[22m\n\n\n  122 |   test('existing Hub button still works on home topbar', async ({ page }) => {\n  123 |     await page.goto(BASE_URL);\n> 124 |     await page.waitForSelector('#scr-home.active', { timeout: 8000 });\n      |                ^\n  125 |     // Both existing hub-btn and new logout-btn should coexist in topbar\n  126 |     await expect(page.locator('#scr-home .topbar .hub-btn')).toBeVisible();\n  127 |   });\n    at /var/www/360lm/tests/hr.spec.js:124:16"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 0,
                      "startTime": "2026-07-12T13:01:33.379Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "screenshot",
                          "contentType": "image/png",
                          "path": "/var/www/360lm/test-results/hr-HR-Management-—-Existin-521c3--still-works-on-home-topbar-android-chrome/test-failed-1.png"
                        },
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/hr-HR-Management-—-Existin-521c3--still-works-on-home-topbar-android-chrome/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/hr.spec.js",
                        "column": 16,
                        "line": 124
                      }
                    },
                    {
                      "workerIndex": 43,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 8260,
                      "error": {
                        "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#scr-home.active') to be visible\u001b[22m\n",
                        "stack": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#scr-home.active') to be visible\u001b[22m\n\n    at /var/www/360lm/tests/hr.spec.js:124:16",
                        "location": {
                          "file": "/var/www/360lm/tests/hr.spec.js",
                          "column": 16,
                          "line": 124
                        },
                        "snippet": "  122 |   test('existing Hub button still works on home topbar', async ({ page }) => {\n  123 |     await page.goto(BASE_URL);\n> 124 |     await page.waitForSelector('#scr-home.active', { timeout: 8000 });\n      |                ^\n  125 |     // Both existing hub-btn and new logout-btn should coexist in topbar\n  126 |     await expect(page.locator('#scr-home .topbar .hub-btn')).toBeVisible();\n  127 |   });"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/hr.spec.js",
                            "column": 16,
                            "line": 124
                          },
                          "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#scr-home.active') to be visible\u001b[22m\n\n\n  122 |   test('existing Hub button still works on home topbar', async ({ page }) => {\n  123 |     await page.goto(BASE_URL);\n> 124 |     await page.waitForSelector('#scr-home.active', { timeout: 8000 });\n      |                ^\n  125 |     // Both existing hub-btn and new logout-btn should coexist in topbar\n  126 |     await expect(page.locator('#scr-home .topbar .hub-btn')).toBeVisible();\n  127 |   });\n    at /var/www/360lm/tests/hr.spec.js:124:16"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 1,
                      "startTime": "2026-07-12T13:01:42.632Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "screenshot",
                          "contentType": "image/png",
                          "path": "/var/www/360lm/test-results/hr-HR-Management-—-Existin-521c3--still-works-on-home-topbar-android-chrome-retry1/test-failed-1.png"
                        },
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/hr-HR-Management-—-Existin-521c3--still-works-on-home-topbar-android-chrome-retry1/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/hr.spec.js",
                        "column": 16,
                        "line": 124
                      }
                    }
                  ],
                  "status": "unexpected"
                }
              ],
              "id": "98264c29bbadd0584463-0ad0523966278d674a7c",
              "file": "hr.spec.js",
              "line": 122,
              "column": 3
            },
            {
              "title": "bottom nav renders correct items for admin",
              "ok": false,
              "tags": [],
              "tests": [
                {
                  "timeout": 30000,
                  "annotations": [],
                  "expectedStatus": "passed",
                  "projectId": "desktop-chrome",
                  "projectName": "desktop-chrome",
                  "results": [
                    {
                      "workerIndex": 214,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 8265,
                      "error": {
                        "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#scr-home.active') to be visible\u001b[22m\n",
                        "stack": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#scr-home.active') to be visible\u001b[22m\n\n    at /var/www/360lm/tests/hr.spec.js:111:16",
                        "location": {
                          "file": "/var/www/360lm/tests/hr.spec.js",
                          "column": 16,
                          "line": 111
                        },
                        "snippet": "  109 |   test('bottom nav renders correct items for admin', async ({ page }) => {\n  110 |     await page.goto(BASE_URL);\n> 111 |     await page.waitForSelector('#scr-home.active', { timeout: 8000 });\n      |                ^\n  112 |     const bnavCount = await page.locator('.bnav-item').count();\n  113 |     expect(bnavCount).toBeGreaterThanOrEqual(3);\n  114 |   });"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/hr.spec.js",
                            "column": 16,
                            "line": 111
                          },
                          "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#scr-home.active') to be visible\u001b[22m\n\n\n  109 |   test('bottom nav renders correct items for admin', async ({ page }) => {\n  110 |     await page.goto(BASE_URL);\n> 111 |     await page.waitForSelector('#scr-home.active', { timeout: 8000 });\n      |                ^\n  112 |     const bnavCount = await page.locator('.bnav-item').count();\n  113 |     expect(bnavCount).toBeGreaterThanOrEqual(3);\n  114 |   });\n    at /var/www/360lm/tests/hr.spec.js:111:16"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 0,
                      "startTime": "2026-07-12T13:21:12.811Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "screenshot",
                          "contentType": "image/png",
                          "path": "/var/www/360lm/test-results/hr-HR-Management-—-Existin-ee07e-ers-correct-items-for-admin-desktop-chrome/test-failed-1.png"
                        },
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/hr-HR-Management-—-Existin-ee07e-ers-correct-items-for-admin-desktop-chrome/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/hr.spec.js",
                        "column": 16,
                        "line": 111
                      }
                    },
                    {
                      "workerIndex": 215,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 8247,
                      "error": {
                        "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#scr-home.active') to be visible\u001b[22m\n",
                        "stack": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#scr-home.active') to be visible\u001b[22m\n\n    at /var/www/360lm/tests/hr.spec.js:111:16",
                        "location": {
                          "file": "/var/www/360lm/tests/hr.spec.js",
                          "column": 16,
                          "line": 111
                        },
                        "snippet": "  109 |   test('bottom nav renders correct items for admin', async ({ page }) => {\n  110 |     await page.goto(BASE_URL);\n> 111 |     await page.waitForSelector('#scr-home.active', { timeout: 8000 });\n      |                ^\n  112 |     const bnavCount = await page.locator('.bnav-item').count();\n  113 |     expect(bnavCount).toBeGreaterThanOrEqual(3);\n  114 |   });"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/hr.spec.js",
                            "column": 16,
                            "line": 111
                          },
                          "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#scr-home.active') to be visible\u001b[22m\n\n\n  109 |   test('bottom nav renders correct items for admin', async ({ page }) => {\n  110 |     await page.goto(BASE_URL);\n> 111 |     await page.waitForSelector('#scr-home.active', { timeout: 8000 });\n      |                ^\n  112 |     const bnavCount = await page.locator('.bnav-item').count();\n  113 |     expect(bnavCount).toBeGreaterThanOrEqual(3);\n  114 |   });\n    at /var/www/360lm/tests/hr.spec.js:111:16"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 1,
                      "startTime": "2026-07-12T13:21:22.156Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "screenshot",
                          "contentType": "image/png",
                          "path": "/var/www/360lm/test-results/hr-HR-Management-—-Existin-ee07e-ers-correct-items-for-admin-desktop-chrome-retry1/test-failed-1.png"
                        },
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/hr-HR-Management-—-Existin-ee07e-ers-correct-items-for-admin-desktop-chrome-retry1/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/hr.spec.js",
                        "column": 16,
                        "line": 111
                      }
                    }
                  ],
                  "status": "unexpected"
                }
              ],
              "id": "98264c29bbadd0584463-5b5a17f7b4f7cbc4c543",
              "file": "hr.spec.js",
              "line": 109,
              "column": 3
            },
            {
              "title": "home content area is populated",
              "ok": false,
              "tags": [],
              "tests": [
                {
                  "timeout": 30000,
                  "annotations": [],
                  "expectedStatus": "passed",
                  "projectId": "desktop-chrome",
                  "projectName": "desktop-chrome",
                  "results": [
                    {
                      "workerIndex": 216,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 8274,
                      "error": {
                        "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#scr-home.active') to be visible\u001b[22m\n",
                        "stack": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#scr-home.active') to be visible\u001b[22m\n\n    at /var/www/360lm/tests/hr.spec.js:118:16",
                        "location": {
                          "file": "/var/www/360lm/tests/hr.spec.js",
                          "column": 16,
                          "line": 118
                        },
                        "snippet": "  116 |   test('home content area is populated', async ({ page }) => {\n  117 |     await page.goto(BASE_URL);\n> 118 |     await page.waitForSelector('#scr-home.active', { timeout: 8000 });\n      |                ^\n  119 |     await expect(page.locator('#home-content')).toBeVisible();\n  120 |   });\n  121 |"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/hr.spec.js",
                            "column": 16,
                            "line": 118
                          },
                          "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#scr-home.active') to be visible\u001b[22m\n\n\n  116 |   test('home content area is populated', async ({ page }) => {\n  117 |     await page.goto(BASE_URL);\n> 118 |     await page.waitForSelector('#scr-home.active', { timeout: 8000 });\n      |                ^\n  119 |     await expect(page.locator('#home-content')).toBeVisible();\n  120 |   });\n  121 |\n    at /var/www/360lm/tests/hr.spec.js:118:16"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 0,
                      "startTime": "2026-07-12T13:21:31.455Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "screenshot",
                          "contentType": "image/png",
                          "path": "/var/www/360lm/test-results/hr-HR-Management-—-Existin-7f588-e-content-area-is-populated-desktop-chrome/test-failed-1.png"
                        },
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/hr-HR-Management-—-Existin-7f588-e-content-area-is-populated-desktop-chrome/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/hr.spec.js",
                        "column": 16,
                        "line": 118
                      }
                    },
                    {
                      "workerIndex": 217,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 8329,
                      "error": {
                        "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#scr-home.active') to be visible\u001b[22m\n",
                        "stack": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#scr-home.active') to be visible\u001b[22m\n\n    at /var/www/360lm/tests/hr.spec.js:118:16",
                        "location": {
                          "file": "/var/www/360lm/tests/hr.spec.js",
                          "column": 16,
                          "line": 118
                        },
                        "snippet": "  116 |   test('home content area is populated', async ({ page }) => {\n  117 |     await page.goto(BASE_URL);\n> 118 |     await page.waitForSelector('#scr-home.active', { timeout: 8000 });\n      |                ^\n  119 |     await expect(page.locator('#home-content')).toBeVisible();\n  120 |   });\n  121 |"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/hr.spec.js",
                            "column": 16,
                            "line": 118
                          },
                          "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#scr-home.active') to be visible\u001b[22m\n\n\n  116 |   test('home content area is populated', async ({ page }) => {\n  117 |     await page.goto(BASE_URL);\n> 118 |     await page.waitForSelector('#scr-home.active', { timeout: 8000 });\n      |                ^\n  119 |     await expect(page.locator('#home-content')).toBeVisible();\n  120 |   });\n  121 |\n    at /var/www/360lm/tests/hr.spec.js:118:16"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 1,
                      "startTime": "2026-07-12T13:21:40.785Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "screenshot",
                          "contentType": "image/png",
                          "path": "/var/www/360lm/test-results/hr-HR-Management-—-Existin-7f588-e-content-area-is-populated-desktop-chrome-retry1/test-failed-1.png"
                        },
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/hr-HR-Management-—-Existin-7f588-e-content-area-is-populated-desktop-chrome-retry1/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/hr.spec.js",
                        "column": 16,
                        "line": 118
                      }
                    }
                  ],
                  "status": "unexpected"
                }
              ],
              "id": "98264c29bbadd0584463-ea2223456924354025c1",
              "file": "hr.spec.js",
              "line": 116,
              "column": 3
            },
            {
              "title": "existing Hub button still works on home topbar",
              "ok": false,
              "tags": [],
              "tests": [
                {
                  "timeout": 30000,
                  "annotations": [],
                  "expectedStatus": "passed",
                  "projectId": "desktop-chrome",
                  "projectName": "desktop-chrome",
                  "results": [
                    {
                      "workerIndex": 218,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 8314,
                      "error": {
                        "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#scr-home.active') to be visible\u001b[22m\n",
                        "stack": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#scr-home.active') to be visible\u001b[22m\n\n    at /var/www/360lm/tests/hr.spec.js:124:16",
                        "location": {
                          "file": "/var/www/360lm/tests/hr.spec.js",
                          "column": 16,
                          "line": 124
                        },
                        "snippet": "  122 |   test('existing Hub button still works on home topbar', async ({ page }) => {\n  123 |     await page.goto(BASE_URL);\n> 124 |     await page.waitForSelector('#scr-home.active', { timeout: 8000 });\n      |                ^\n  125 |     // Both existing hub-btn and new logout-btn should coexist in topbar\n  126 |     await expect(page.locator('#scr-home .topbar .hub-btn')).toBeVisible();\n  127 |   });"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/hr.spec.js",
                            "column": 16,
                            "line": 124
                          },
                          "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#scr-home.active') to be visible\u001b[22m\n\n\n  122 |   test('existing Hub button still works on home topbar', async ({ page }) => {\n  123 |     await page.goto(BASE_URL);\n> 124 |     await page.waitForSelector('#scr-home.active', { timeout: 8000 });\n      |                ^\n  125 |     // Both existing hub-btn and new logout-btn should coexist in topbar\n  126 |     await expect(page.locator('#scr-home .topbar .hub-btn')).toBeVisible();\n  127 |   });\n    at /var/www/360lm/tests/hr.spec.js:124:16"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 0,
                      "startTime": "2026-07-12T13:21:50.326Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "screenshot",
                          "contentType": "image/png",
                          "path": "/var/www/360lm/test-results/hr-HR-Management-—-Existin-521c3--still-works-on-home-topbar-desktop-chrome/test-failed-1.png"
                        },
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/hr-HR-Management-—-Existin-521c3--still-works-on-home-topbar-desktop-chrome/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/hr.spec.js",
                        "column": 16,
                        "line": 124
                      }
                    },
                    {
                      "workerIndex": 219,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 8291,
                      "error": {
                        "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#scr-home.active') to be visible\u001b[22m\n",
                        "stack": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#scr-home.active') to be visible\u001b[22m\n\n    at /var/www/360lm/tests/hr.spec.js:124:16",
                        "location": {
                          "file": "/var/www/360lm/tests/hr.spec.js",
                          "column": 16,
                          "line": 124
                        },
                        "snippet": "  122 |   test('existing Hub button still works on home topbar', async ({ page }) => {\n  123 |     await page.goto(BASE_URL);\n> 124 |     await page.waitForSelector('#scr-home.active', { timeout: 8000 });\n      |                ^\n  125 |     // Both existing hub-btn and new logout-btn should coexist in topbar\n  126 |     await expect(page.locator('#scr-home .topbar .hub-btn')).toBeVisible();\n  127 |   });"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/hr.spec.js",
                            "column": 16,
                            "line": 124
                          },
                          "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#scr-home.active') to be visible\u001b[22m\n\n\n  122 |   test('existing Hub button still works on home topbar', async ({ page }) => {\n  123 |     await page.goto(BASE_URL);\n> 124 |     await page.waitForSelector('#scr-home.active', { timeout: 8000 });\n      |                ^\n  125 |     // Both existing hub-btn and new logout-btn should coexist in topbar\n  126 |     await expect(page.locator('#scr-home .topbar .hub-btn')).toBeVisible();\n  127 |   });\n    at /var/www/360lm/tests/hr.spec.js:124:16"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 1,
                      "startTime": "2026-07-12T13:21:59.575Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "screenshot",
                          "contentType": "image/png",
                          "path": "/var/www/360lm/test-results/hr-HR-Management-—-Existin-521c3--still-works-on-home-topbar-desktop-chrome-retry1/test-failed-1.png"
                        },
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/hr-HR-Management-—-Existin-521c3--still-works-on-home-topbar-desktop-chrome-retry1/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/hr.spec.js",
                        "column": 16,
                        "line": 124
                      }
                    }
                  ],
                  "status": "unexpected"
                }
              ],
              "id": "98264c29bbadd0584463-312c714c805cfe58aacf",
              "file": "hr.spec.js",
              "line": 122,
              "column": 3
            }
          ]
        },
        {
          "title": "HR Management — Salary Advance Recovery",
          "file": "hr.spec.js",
          "line": 137,
          "column": 6,
          "specs": [
            {
              "title": "get_employee_advances_for_salary RPC is reachable (hr profile)",
              "ok": false,
              "tags": [],
              "tests": [
                {
                  "timeout": 30000,
                  "annotations": [],
                  "expectedStatus": "passed",
                  "projectId": "android-chrome",
                  "projectName": "android-chrome",
                  "results": [
                    {
                      "workerIndex": 44,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 78,
                      "error": {
                        "message": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoBeTruthy\u001b[2m()\u001b[22m\n\nReceived: \u001b[31mfalse\u001b[39m",
                        "stack": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoBeTruthy\u001b[2m()\u001b[22m\n\nReceived: \u001b[31mfalse\u001b[39m\n    at /var/www/360lm/tests/hr.spec.js:142:20",
                        "location": {
                          "file": "/var/www/360lm/tests/hr.spec.js",
                          "column": 20,
                          "line": 142
                        },
                        "snippet": "  140 |       headers: HR_HEADERS, data: { p_employee_id: 'mukesh' }\n  141 |     });\n> 142 |     expect(r.ok()).toBeTruthy();\n      |                    ^\n  143 |     expect(Array.isArray(await r.json())).toBe(true);\n  144 |   });\n  145 |"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/hr.spec.js",
                            "column": 20,
                            "line": 142
                          },
                          "message": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoBeTruthy\u001b[2m()\u001b[22m\n\nReceived: \u001b[31mfalse\u001b[39m\n\n  140 |       headers: HR_HEADERS, data: { p_employee_id: 'mukesh' }\n  141 |     });\n> 142 |     expect(r.ok()).toBeTruthy();\n      |                    ^\n  143 |     expect(Array.isArray(await r.json())).toBe(true);\n  144 |   });\n  145 |\n    at /var/www/360lm/tests/hr.spec.js:142:20"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 0,
                      "startTime": "2026-07-12T13:01:51.962Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/hr-HR-Management-—-Salary--c1708-PC-is-reachable-hr-profile--android-chrome/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/hr.spec.js",
                        "column": 20,
                        "line": 142
                      }
                    },
                    {
                      "workerIndex": 45,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 68,
                      "error": {
                        "message": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoBeTruthy\u001b[2m()\u001b[22m\n\nReceived: \u001b[31mfalse\u001b[39m",
                        "stack": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoBeTruthy\u001b[2m()\u001b[22m\n\nReceived: \u001b[31mfalse\u001b[39m\n    at /var/www/360lm/tests/hr.spec.js:142:20",
                        "location": {
                          "file": "/var/www/360lm/tests/hr.spec.js",
                          "column": 20,
                          "line": 142
                        },
                        "snippet": "  140 |       headers: HR_HEADERS, data: { p_employee_id: 'mukesh' }\n  141 |     });\n> 142 |     expect(r.ok()).toBeTruthy();\n      |                    ^\n  143 |     expect(Array.isArray(await r.json())).toBe(true);\n  144 |   });\n  145 |"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/hr.spec.js",
                            "column": 20,
                            "line": 142
                          },
                          "message": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoBeTruthy\u001b[2m()\u001b[22m\n\nReceived: \u001b[31mfalse\u001b[39m\n\n  140 |       headers: HR_HEADERS, data: { p_employee_id: 'mukesh' }\n  141 |     });\n> 142 |     expect(r.ok()).toBeTruthy();\n      |                    ^\n  143 |     expect(Array.isArray(await r.json())).toBe(true);\n  144 |   });\n  145 |\n    at /var/www/360lm/tests/hr.spec.js:142:20"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 1,
                      "startTime": "2026-07-12T13:01:52.926Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/hr-HR-Management-—-Salary--c1708-PC-is-reachable-hr-profile--android-chrome-retry1/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/hr.spec.js",
                        "column": 20,
                        "line": 142
                      }
                    }
                  ],
                  "status": "unexpected"
                }
              ],
              "id": "98264c29bbadd0584463-0e9178349aba126ae32d",
              "file": "hr.spec.js",
              "line": 138,
              "column": 3
            },
            {
              "title": "pay_salary_slip guards a non-existent slip (mutates nothing)",
              "ok": false,
              "tags": [],
              "tests": [
                {
                  "timeout": 30000,
                  "annotations": [],
                  "expectedStatus": "passed",
                  "projectId": "android-chrome",
                  "projectName": "android-chrome",
                  "results": [
                    {
                      "workerIndex": 46,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 75,
                      "error": {
                        "message": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoBeTruthy\u001b[2m()\u001b[22m\n\nReceived: \u001b[31mfalse\u001b[39m",
                        "stack": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoBeTruthy\u001b[2m()\u001b[22m\n\nReceived: \u001b[31mfalse\u001b[39m\n    at /var/www/360lm/tests/hr.spec.js:152:20",
                        "location": {
                          "file": "/var/www/360lm/tests/hr.spec.js",
                          "column": 20,
                          "line": 152
                        },
                        "snippet": "  150 |               p_reference:null, p_paid_by:'pramod', p_recoveries:[] }\n  151 |     });\n> 152 |     expect(r.ok()).toBeTruthy();\n      |                    ^\n  153 |     const d = await r.json();\n  154 |     expect(d.ok).toBe(false);\n  155 |     expect(d.error).toMatch(/not found/i);"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/hr.spec.js",
                            "column": 20,
                            "line": 152
                          },
                          "message": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoBeTruthy\u001b[2m()\u001b[22m\n\nReceived: \u001b[31mfalse\u001b[39m\n\n  150 |               p_reference:null, p_paid_by:'pramod', p_recoveries:[] }\n  151 |     });\n> 152 |     expect(r.ok()).toBeTruthy();\n      |                    ^\n  153 |     const d = await r.json();\n  154 |     expect(d.ok).toBe(false);\n  155 |     expect(d.error).toMatch(/not found/i);\n    at /var/www/360lm/tests/hr.spec.js:152:20"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 0,
                      "startTime": "2026-07-12T13:01:53.912Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/hr-HR-Management-—-Salary--68164-stent-slip-mutates-nothing--android-chrome/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/hr.spec.js",
                        "column": 20,
                        "line": 152
                      }
                    },
                    {
                      "workerIndex": 47,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 61,
                      "error": {
                        "message": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoBeTruthy\u001b[2m()\u001b[22m\n\nReceived: \u001b[31mfalse\u001b[39m",
                        "stack": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoBeTruthy\u001b[2m()\u001b[22m\n\nReceived: \u001b[31mfalse\u001b[39m\n    at /var/www/360lm/tests/hr.spec.js:152:20",
                        "location": {
                          "file": "/var/www/360lm/tests/hr.spec.js",
                          "column": 20,
                          "line": 152
                        },
                        "snippet": "  150 |               p_reference:null, p_paid_by:'pramod', p_recoveries:[] }\n  151 |     });\n> 152 |     expect(r.ok()).toBeTruthy();\n      |                    ^\n  153 |     const d = await r.json();\n  154 |     expect(d.ok).toBe(false);\n  155 |     expect(d.error).toMatch(/not found/i);"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/hr.spec.js",
                            "column": 20,
                            "line": 152
                          },
                          "message": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoBeTruthy\u001b[2m()\u001b[22m\n\nReceived: \u001b[31mfalse\u001b[39m\n\n  150 |               p_reference:null, p_paid_by:'pramod', p_recoveries:[] }\n  151 |     });\n> 152 |     expect(r.ok()).toBeTruthy();\n      |                    ^\n  153 |     const d = await r.json();\n  154 |     expect(d.ok).toBe(false);\n  155 |     expect(d.error).toMatch(/not found/i);\n    at /var/www/360lm/tests/hr.spec.js:152:20"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 1,
                      "startTime": "2026-07-12T13:01:54.944Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/hr-HR-Management-—-Salary--68164-stent-slip-mutates-nothing--android-chrome-retry1/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/hr.spec.js",
                        "column": 20,
                        "line": 152
                      }
                    }
                  ],
                  "status": "unexpected"
                }
              ],
              "id": "98264c29bbadd0584463-b3b803d3fed16d1428bd",
              "file": "hr.spec.js",
              "line": 146,
              "column": 3
            },
            {
              "title": "pay modal markup includes advance-recovery section",
              "ok": false,
              "tags": [],
              "tests": [
                {
                  "timeout": 30000,
                  "annotations": [],
                  "expectedStatus": "passed",
                  "projectId": "android-chrome",
                  "projectName": "android-chrome",
                  "results": [
                    {
                      "workerIndex": 48,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 8275,
                      "error": {
                        "message": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mlocator\u001b[39m\u001b[2m).\u001b[22mtoBeVisible\u001b[2m(\u001b[22m\u001b[2m)\u001b[22m failed\n\nLocator: locator('#scr-home.active')\nExpected: visible\nTimeout: 8000ms\nError: element(s) not found\n\nCall log:\n\u001b[2m  - Expect \"toBeVisible\" with timeout 8000ms\u001b[22m\n\u001b[2m  - waiting for locator('#scr-home.active')\u001b[22m\n",
                        "stack": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mlocator\u001b[39m\u001b[2m).\u001b[22mtoBeVisible\u001b[2m(\u001b[22m\u001b[2m)\u001b[22m failed\n\nLocator: locator('#scr-home.active')\nExpected: visible\nTimeout: 8000ms\nError: element(s) not found\n\nCall log:\n\u001b[2m  - Expect \"toBeVisible\" with timeout 8000ms\u001b[22m\n\u001b[2m  - waiting for locator('#scr-home.active')\u001b[22m\n\n    at /var/www/360lm/tests/hr.spec.js:161:52",
                        "location": {
                          "file": "/var/www/360lm/tests/hr.spec.js",
                          "column": 52,
                          "line": 161
                        },
                        "snippet": "  159 |     await injectSession(page);\n  160 |     await page.goto(BASE_URL);\n> 161 |     await expect(page.locator('#scr-home.active')).toBeVisible({ timeout: 8000 });\n      |                                                    ^\n  162 |     // Section is hidden until a slip is paid, but must exist in the DOM\n  163 |     await expect(page.locator('#pay-adv-section')).toHaveCount(1);\n  164 |     await expect(page.locator('#pay-adv-list')).toHaveCount(1);"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/hr.spec.js",
                            "column": 52,
                            "line": 161
                          },
                          "message": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mlocator\u001b[39m\u001b[2m).\u001b[22mtoBeVisible\u001b[2m(\u001b[22m\u001b[2m)\u001b[22m failed\n\nLocator: locator('#scr-home.active')\nExpected: visible\nTimeout: 8000ms\nError: element(s) not found\n\nCall log:\n\u001b[2m  - Expect \"toBeVisible\" with timeout 8000ms\u001b[22m\n\u001b[2m  - waiting for locator('#scr-home.active')\u001b[22m\n\n\n  159 |     await injectSession(page);\n  160 |     await page.goto(BASE_URL);\n> 161 |     await expect(page.locator('#scr-home.active')).toBeVisible({ timeout: 8000 });\n      |                                                    ^\n  162 |     // Section is hidden until a slip is paid, but must exist in the DOM\n  163 |     await expect(page.locator('#pay-adv-section')).toHaveCount(1);\n  164 |     await expect(page.locator('#pay-adv-list')).toHaveCount(1);\n    at /var/www/360lm/tests/hr.spec.js:161:52"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 0,
                      "startTime": "2026-07-12T13:01:55.788Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "screenshot",
                          "contentType": "image/png",
                          "path": "/var/www/360lm/test-results/hr-HR-Management-—-Salary--024cf-es-advance-recovery-section-android-chrome/test-failed-1.png"
                        },
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/hr-HR-Management-—-Salary--024cf-es-advance-recovery-section-android-chrome/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/hr.spec.js",
                        "column": 52,
                        "line": 161
                      }
                    },
                    {
                      "workerIndex": 49,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 8266,
                      "error": {
                        "message": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mlocator\u001b[39m\u001b[2m).\u001b[22mtoBeVisible\u001b[2m(\u001b[22m\u001b[2m)\u001b[22m failed\n\nLocator: locator('#scr-home.active')\nExpected: visible\nTimeout: 8000ms\nError: element(s) not found\n\nCall log:\n\u001b[2m  - Expect \"toBeVisible\" with timeout 8000ms\u001b[22m\n\u001b[2m  - waiting for locator('#scr-home.active')\u001b[22m\n",
                        "stack": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mlocator\u001b[39m\u001b[2m).\u001b[22mtoBeVisible\u001b[2m(\u001b[22m\u001b[2m)\u001b[22m failed\n\nLocator: locator('#scr-home.active')\nExpected: visible\nTimeout: 8000ms\nError: element(s) not found\n\nCall log:\n\u001b[2m  - Expect \"toBeVisible\" with timeout 8000ms\u001b[22m\n\u001b[2m  - waiting for locator('#scr-home.active')\u001b[22m\n\n    at /var/www/360lm/tests/hr.spec.js:161:52",
                        "location": {
                          "file": "/var/www/360lm/tests/hr.spec.js",
                          "column": 52,
                          "line": 161
                        },
                        "snippet": "  159 |     await injectSession(page);\n  160 |     await page.goto(BASE_URL);\n> 161 |     await expect(page.locator('#scr-home.active')).toBeVisible({ timeout: 8000 });\n      |                                                    ^\n  162 |     // Section is hidden until a slip is paid, but must exist in the DOM\n  163 |     await expect(page.locator('#pay-adv-section')).toHaveCount(1);\n  164 |     await expect(page.locator('#pay-adv-list')).toHaveCount(1);"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/hr.spec.js",
                            "column": 52,
                            "line": 161
                          },
                          "message": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mlocator\u001b[39m\u001b[2m).\u001b[22mtoBeVisible\u001b[2m(\u001b[22m\u001b[2m)\u001b[22m failed\n\nLocator: locator('#scr-home.active')\nExpected: visible\nTimeout: 8000ms\nError: element(s) not found\n\nCall log:\n\u001b[2m  - Expect \"toBeVisible\" with timeout 8000ms\u001b[22m\n\u001b[2m  - waiting for locator('#scr-home.active')\u001b[22m\n\n\n  159 |     await injectSession(page);\n  160 |     await page.goto(BASE_URL);\n> 161 |     await expect(page.locator('#scr-home.active')).toBeVisible({ timeout: 8000 });\n      |                                                    ^\n  162 |     // Section is hidden until a slip is paid, but must exist in the DOM\n  163 |     await expect(page.locator('#pay-adv-section')).toHaveCount(1);\n  164 |     await expect(page.locator('#pay-adv-list')).toHaveCount(1);\n    at /var/www/360lm/tests/hr.spec.js:161:52"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 1,
                      "startTime": "2026-07-12T13:02:05.282Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "screenshot",
                          "contentType": "image/png",
                          "path": "/var/www/360lm/test-results/hr-HR-Management-—-Salary--024cf-es-advance-recovery-section-android-chrome-retry1/test-failed-1.png"
                        },
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/hr-HR-Management-—-Salary--024cf-es-advance-recovery-section-android-chrome-retry1/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/hr.spec.js",
                        "column": 52,
                        "line": 161
                      }
                    }
                  ],
                  "status": "unexpected"
                }
              ],
              "id": "98264c29bbadd0584463-a012dbe1416c4e1b261a",
              "file": "hr.spec.js",
              "line": 158,
              "column": 3
            },
            {
              "title": "service worker version is current",
              "ok": false,
              "tags": [],
              "tests": [
                {
                  "timeout": 30000,
                  "annotations": [],
                  "expectedStatus": "passed",
                  "projectId": "android-chrome",
                  "projectName": "android-chrome",
                  "results": [
                    {
                      "workerIndex": 50,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 319,
                      "error": {
                        "message": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoMatch\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m)\u001b[22m\n\nExpected pattern: \u001b[32m/360hr-v\\d+/\u001b[39m\nReceived string:  \u001b[31m\"<!DOCTYPE HTML PUBLIC \\\"-//W3C//DTD HTML 4.01//EN\\\" \\\"http://www.w3.org/TR/html4/strict.dtd\\\">\u001b[39m\n\u001b[31m<html><head>\u001b[39m\n\u001b[31m<title>404 Not Found</title>\u001b[39m\n\u001b[31m</head><body>\u001b[39m\n\u001b[31m<h1>Not Found</h1>\u001b[39m\n\u001b[31m<p>The requested URL was not found on this server.</p>\u001b[39m\n\u001b[31m</body></html>\u001b[39m\n\u001b[31m\"\u001b[39m",
                        "stack": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoMatch\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m)\u001b[22m\n\nExpected pattern: \u001b[32m/360hr-v\\d+/\u001b[39m\nReceived string:  \u001b[31m\"<!DOCTYPE HTML PUBLIC \\\"-//W3C//DTD HTML 4.01//EN\\\" \\\"http://www.w3.org/TR/html4/strict.dtd\\\">\u001b[39m\n\u001b[31m<html><head>\u001b[39m\n\u001b[31m<title>404 Not Found</title>\u001b[39m\n\u001b[31m</head><body>\u001b[39m\n\u001b[31m<h1>Not Found</h1>\u001b[39m\n\u001b[31m<p>The requested URL was not found on this server.</p>\u001b[39m\n\u001b[31m</body></html>\u001b[39m\n\u001b[31m\"\u001b[39m\n    at /var/www/360lm/tests/hr.spec.js:170:33",
                        "location": {
                          "file": "/var/www/360lm/tests/hr.spec.js",
                          "column": 33,
                          "line": 170
                        },
                        "snippet": "  168 |   test('service worker version is current', async ({ page }) => {\n  169 |     const resp = await page.goto(BASE_URL + 'sw.js');\n> 170 |     expect((await resp.text())).toMatch(/360hr-v\\d+/);\n      |                                 ^\n  171 |   });\n  172 | });\n  173 |"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/hr.spec.js",
                            "column": 33,
                            "line": 170
                          },
                          "message": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoMatch\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m)\u001b[22m\n\nExpected pattern: \u001b[32m/360hr-v\\d+/\u001b[39m\nReceived string:  \u001b[31m\"<!DOCTYPE HTML PUBLIC \\\"-//W3C//DTD HTML 4.01//EN\\\" \\\"http://www.w3.org/TR/html4/strict.dtd\\\">\u001b[39m\n\u001b[31m<html><head>\u001b[39m\n\u001b[31m<title>404 Not Found</title>\u001b[39m\n\u001b[31m</head><body>\u001b[39m\n\u001b[31m<h1>Not Found</h1>\u001b[39m\n\u001b[31m<p>The requested URL was not found on this server.</p>\u001b[39m\n\u001b[31m</body></html>\u001b[39m\n\u001b[31m\"\u001b[39m\n\n  168 |   test('service worker version is current', async ({ page }) => {\n  169 |     const resp = await page.goto(BASE_URL + 'sw.js');\n> 170 |     expect((await resp.text())).toMatch(/360hr-v\\d+/);\n      |                                 ^\n  171 |   });\n  172 | });\n  173 |\n    at /var/www/360lm/tests/hr.spec.js:170:33"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 0,
                      "startTime": "2026-07-12T13:02:14.556Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "screenshot",
                          "contentType": "image/png",
                          "path": "/var/www/360lm/test-results/hr-HR-Management-—-Salary--bcdbd-e-worker-version-is-current-android-chrome/test-failed-1.png"
                        },
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/hr-HR-Management-—-Salary--bcdbd-e-worker-version-is-current-android-chrome/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/hr.spec.js",
                        "column": 33,
                        "line": 170
                      }
                    },
                    {
                      "workerIndex": 51,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 283,
                      "error": {
                        "message": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoMatch\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m)\u001b[22m\n\nExpected pattern: \u001b[32m/360hr-v\\d+/\u001b[39m\nReceived string:  \u001b[31m\"<!DOCTYPE HTML PUBLIC \\\"-//W3C//DTD HTML 4.01//EN\\\" \\\"http://www.w3.org/TR/html4/strict.dtd\\\">\u001b[39m\n\u001b[31m<html><head>\u001b[39m\n\u001b[31m<title>404 Not Found</title>\u001b[39m\n\u001b[31m</head><body>\u001b[39m\n\u001b[31m<h1>Not Found</h1>\u001b[39m\n\u001b[31m<p>The requested URL was not found on this server.</p>\u001b[39m\n\u001b[31m</body></html>\u001b[39m\n\u001b[31m\"\u001b[39m",
                        "stack": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoMatch\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m)\u001b[22m\n\nExpected pattern: \u001b[32m/360hr-v\\d+/\u001b[39m\nReceived string:  \u001b[31m\"<!DOCTYPE HTML PUBLIC \\\"-//W3C//DTD HTML 4.01//EN\\\" \\\"http://www.w3.org/TR/html4/strict.dtd\\\">\u001b[39m\n\u001b[31m<html><head>\u001b[39m\n\u001b[31m<title>404 Not Found</title>\u001b[39m\n\u001b[31m</head><body>\u001b[39m\n\u001b[31m<h1>Not Found</h1>\u001b[39m\n\u001b[31m<p>The requested URL was not found on this server.</p>\u001b[39m\n\u001b[31m</body></html>\u001b[39m\n\u001b[31m\"\u001b[39m\n    at /var/www/360lm/tests/hr.spec.js:170:33",
                        "location": {
                          "file": "/var/www/360lm/tests/hr.spec.js",
                          "column": 33,
                          "line": 170
                        },
                        "snippet": "  168 |   test('service worker version is current', async ({ page }) => {\n  169 |     const resp = await page.goto(BASE_URL + 'sw.js');\n> 170 |     expect((await resp.text())).toMatch(/360hr-v\\d+/);\n      |                                 ^\n  171 |   });\n  172 | });\n  173 |"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/hr.spec.js",
                            "column": 33,
                            "line": 170
                          },
                          "message": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoMatch\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m)\u001b[22m\n\nExpected pattern: \u001b[32m/360hr-v\\d+/\u001b[39m\nReceived string:  \u001b[31m\"<!DOCTYPE HTML PUBLIC \\\"-//W3C//DTD HTML 4.01//EN\\\" \\\"http://www.w3.org/TR/html4/strict.dtd\\\">\u001b[39m\n\u001b[31m<html><head>\u001b[39m\n\u001b[31m<title>404 Not Found</title>\u001b[39m\n\u001b[31m</head><body>\u001b[39m\n\u001b[31m<h1>Not Found</h1>\u001b[39m\n\u001b[31m<p>The requested URL was not found on this server.</p>\u001b[39m\n\u001b[31m</body></html>\u001b[39m\n\u001b[31m\"\u001b[39m\n\n  168 |   test('service worker version is current', async ({ page }) => {\n  169 |     const resp = await page.goto(BASE_URL + 'sw.js');\n> 170 |     expect((await resp.text())).toMatch(/360hr-v\\d+/);\n      |                                 ^\n  171 |   });\n  172 | });\n  173 |\n    at /var/www/360lm/tests/hr.spec.js:170:33"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 1,
                      "startTime": "2026-07-12T13:02:15.880Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "screenshot",
                          "contentType": "image/png",
                          "path": "/var/www/360lm/test-results/hr-HR-Management-—-Salary--bcdbd-e-worker-version-is-current-android-chrome-retry1/test-failed-1.png"
                        },
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/hr-HR-Management-—-Salary--bcdbd-e-worker-version-is-current-android-chrome-retry1/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/hr.spec.js",
                        "column": 33,
                        "line": 170
                      }
                    }
                  ],
                  "status": "unexpected"
                }
              ],
              "id": "98264c29bbadd0584463-77c829ea4e6f07a517ae",
              "file": "hr.spec.js",
              "line": 168,
              "column": 3
            },
            {
              "title": "get_employee_advances_for_salary RPC is reachable (hr profile)",
              "ok": false,
              "tags": [],
              "tests": [
                {
                  "timeout": 30000,
                  "annotations": [],
                  "expectedStatus": "passed",
                  "projectId": "desktop-chrome",
                  "projectName": "desktop-chrome",
                  "results": [
                    {
                      "workerIndex": 220,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 99,
                      "error": {
                        "message": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoBeTruthy\u001b[2m()\u001b[22m\n\nReceived: \u001b[31mfalse\u001b[39m",
                        "stack": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoBeTruthy\u001b[2m()\u001b[22m\n\nReceived: \u001b[31mfalse\u001b[39m\n    at /var/www/360lm/tests/hr.spec.js:142:20",
                        "location": {
                          "file": "/var/www/360lm/tests/hr.spec.js",
                          "column": 20,
                          "line": 142
                        },
                        "snippet": "  140 |       headers: HR_HEADERS, data: { p_employee_id: 'mukesh' }\n  141 |     });\n> 142 |     expect(r.ok()).toBeTruthy();\n      |                    ^\n  143 |     expect(Array.isArray(await r.json())).toBe(true);\n  144 |   });\n  145 |"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/hr.spec.js",
                            "column": 20,
                            "line": 142
                          },
                          "message": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoBeTruthy\u001b[2m()\u001b[22m\n\nReceived: \u001b[31mfalse\u001b[39m\n\n  140 |       headers: HR_HEADERS, data: { p_employee_id: 'mukesh' }\n  141 |     });\n> 142 |     expect(r.ok()).toBeTruthy();\n      |                    ^\n  143 |     expect(Array.isArray(await r.json())).toBe(true);\n  144 |   });\n  145 |\n    at /var/www/360lm/tests/hr.spec.js:142:20"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 0,
                      "startTime": "2026-07-12T13:22:08.974Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/hr-HR-Management-—-Salary--c1708-PC-is-reachable-hr-profile--desktop-chrome/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/hr.spec.js",
                        "column": 20,
                        "line": 142
                      }
                    },
                    {
                      "workerIndex": 221,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 80,
                      "error": {
                        "message": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoBeTruthy\u001b[2m()\u001b[22m\n\nReceived: \u001b[31mfalse\u001b[39m",
                        "stack": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoBeTruthy\u001b[2m()\u001b[22m\n\nReceived: \u001b[31mfalse\u001b[39m\n    at /var/www/360lm/tests/hr.spec.js:142:20",
                        "location": {
                          "file": "/var/www/360lm/tests/hr.spec.js",
                          "column": 20,
                          "line": 142
                        },
                        "snippet": "  140 |       headers: HR_HEADERS, data: { p_employee_id: 'mukesh' }\n  141 |     });\n> 142 |     expect(r.ok()).toBeTruthy();\n      |                    ^\n  143 |     expect(Array.isArray(await r.json())).toBe(true);\n  144 |   });\n  145 |"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/hr.spec.js",
                            "column": 20,
                            "line": 142
                          },
                          "message": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoBeTruthy\u001b[2m()\u001b[22m\n\nReceived: \u001b[31mfalse\u001b[39m\n\n  140 |       headers: HR_HEADERS, data: { p_employee_id: 'mukesh' }\n  141 |     });\n> 142 |     expect(r.ok()).toBeTruthy();\n      |                    ^\n  143 |     expect(Array.isArray(await r.json())).toBe(true);\n  144 |   });\n  145 |\n    at /var/www/360lm/tests/hr.spec.js:142:20"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 1,
                      "startTime": "2026-07-12T13:22:10.052Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/hr-HR-Management-—-Salary--c1708-PC-is-reachable-hr-profile--desktop-chrome-retry1/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/hr.spec.js",
                        "column": 20,
                        "line": 142
                      }
                    }
                  ],
                  "status": "unexpected"
                }
              ],
              "id": "98264c29bbadd0584463-c70fe4e5d35a47606af2",
              "file": "hr.spec.js",
              "line": 138,
              "column": 3
            },
            {
              "title": "pay_salary_slip guards a non-existent slip (mutates nothing)",
              "ok": false,
              "tags": [],
              "tests": [
                {
                  "timeout": 30000,
                  "annotations": [],
                  "expectedStatus": "passed",
                  "projectId": "desktop-chrome",
                  "projectName": "desktop-chrome",
                  "results": [
                    {
                      "workerIndex": 222,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 77,
                      "error": {
                        "message": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoBeTruthy\u001b[2m()\u001b[22m\n\nReceived: \u001b[31mfalse\u001b[39m",
                        "stack": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoBeTruthy\u001b[2m()\u001b[22m\n\nReceived: \u001b[31mfalse\u001b[39m\n    at /var/www/360lm/tests/hr.spec.js:152:20",
                        "location": {
                          "file": "/var/www/360lm/tests/hr.spec.js",
                          "column": 20,
                          "line": 152
                        },
                        "snippet": "  150 |               p_reference:null, p_paid_by:'pramod', p_recoveries:[] }\n  151 |     });\n> 152 |     expect(r.ok()).toBeTruthy();\n      |                    ^\n  153 |     const d = await r.json();\n  154 |     expect(d.ok).toBe(false);\n  155 |     expect(d.error).toMatch(/not found/i);"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/hr.spec.js",
                            "column": 20,
                            "line": 152
                          },
                          "message": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoBeTruthy\u001b[2m()\u001b[22m\n\nReceived: \u001b[31mfalse\u001b[39m\n\n  150 |               p_reference:null, p_paid_by:'pramod', p_recoveries:[] }\n  151 |     });\n> 152 |     expect(r.ok()).toBeTruthy();\n      |                    ^\n  153 |     const d = await r.json();\n  154 |     expect(d.ok).toBe(false);\n  155 |     expect(d.error).toMatch(/not found/i);\n    at /var/www/360lm/tests/hr.spec.js:152:20"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 0,
                      "startTime": "2026-07-12T13:22:11.107Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/hr-HR-Management-—-Salary--68164-stent-slip-mutates-nothing--desktop-chrome/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/hr.spec.js",
                        "column": 20,
                        "line": 152
                      }
                    },
                    {
                      "workerIndex": 223,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 82,
                      "error": {
                        "message": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoBeTruthy\u001b[2m()\u001b[22m\n\nReceived: \u001b[31mfalse\u001b[39m",
                        "stack": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoBeTruthy\u001b[2m()\u001b[22m\n\nReceived: \u001b[31mfalse\u001b[39m\n    at /var/www/360lm/tests/hr.spec.js:152:20",
                        "location": {
                          "file": "/var/www/360lm/tests/hr.spec.js",
                          "column": 20,
                          "line": 152
                        },
                        "snippet": "  150 |               p_reference:null, p_paid_by:'pramod', p_recoveries:[] }\n  151 |     });\n> 152 |     expect(r.ok()).toBeTruthy();\n      |                    ^\n  153 |     const d = await r.json();\n  154 |     expect(d.ok).toBe(false);\n  155 |     expect(d.error).toMatch(/not found/i);"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/hr.spec.js",
                            "column": 20,
                            "line": 152
                          },
                          "message": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoBeTruthy\u001b[2m()\u001b[22m\n\nReceived: \u001b[31mfalse\u001b[39m\n\n  150 |               p_reference:null, p_paid_by:'pramod', p_recoveries:[] }\n  151 |     });\n> 152 |     expect(r.ok()).toBeTruthy();\n      |                    ^\n  153 |     const d = await r.json();\n  154 |     expect(d.ok).toBe(false);\n  155 |     expect(d.error).toMatch(/not found/i);\n    at /var/www/360lm/tests/hr.spec.js:152:20"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 1,
                      "startTime": "2026-07-12T13:22:12.169Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/hr-HR-Management-—-Salary--68164-stent-slip-mutates-nothing--desktop-chrome-retry1/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/hr.spec.js",
                        "column": 20,
                        "line": 152
                      }
                    }
                  ],
                  "status": "unexpected"
                }
              ],
              "id": "98264c29bbadd0584463-028fbd1b5548f7845057",
              "file": "hr.spec.js",
              "line": 146,
              "column": 3
            },
            {
              "title": "pay modal markup includes advance-recovery section",
              "ok": false,
              "tags": [],
              "tests": [
                {
                  "timeout": 30000,
                  "annotations": [],
                  "expectedStatus": "passed",
                  "projectId": "desktop-chrome",
                  "projectName": "desktop-chrome",
                  "results": [
                    {
                      "workerIndex": 224,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 8360,
                      "error": {
                        "message": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mlocator\u001b[39m\u001b[2m).\u001b[22mtoBeVisible\u001b[2m(\u001b[22m\u001b[2m)\u001b[22m failed\n\nLocator: locator('#scr-home.active')\nExpected: visible\nTimeout: 8000ms\nError: element(s) not found\n\nCall log:\n\u001b[2m  - Expect \"toBeVisible\" with timeout 8000ms\u001b[22m\n\u001b[2m  - waiting for locator('#scr-home.active')\u001b[22m\n",
                        "stack": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mlocator\u001b[39m\u001b[2m).\u001b[22mtoBeVisible\u001b[2m(\u001b[22m\u001b[2m)\u001b[22m failed\n\nLocator: locator('#scr-home.active')\nExpected: visible\nTimeout: 8000ms\nError: element(s) not found\n\nCall log:\n\u001b[2m  - Expect \"toBeVisible\" with timeout 8000ms\u001b[22m\n\u001b[2m  - waiting for locator('#scr-home.active')\u001b[22m\n\n    at /var/www/360lm/tests/hr.spec.js:161:52",
                        "location": {
                          "file": "/var/www/360lm/tests/hr.spec.js",
                          "column": 52,
                          "line": 161
                        },
                        "snippet": "  159 |     await injectSession(page);\n  160 |     await page.goto(BASE_URL);\n> 161 |     await expect(page.locator('#scr-home.active')).toBeVisible({ timeout: 8000 });\n      |                                                    ^\n  162 |     // Section is hidden until a slip is paid, but must exist in the DOM\n  163 |     await expect(page.locator('#pay-adv-section')).toHaveCount(1);\n  164 |     await expect(page.locator('#pay-adv-list')).toHaveCount(1);"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/hr.spec.js",
                            "column": 52,
                            "line": 161
                          },
                          "message": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mlocator\u001b[39m\u001b[2m).\u001b[22mtoBeVisible\u001b[2m(\u001b[22m\u001b[2m)\u001b[22m failed\n\nLocator: locator('#scr-home.active')\nExpected: visible\nTimeout: 8000ms\nError: element(s) not found\n\nCall log:\n\u001b[2m  - Expect \"toBeVisible\" with timeout 8000ms\u001b[22m\n\u001b[2m  - waiting for locator('#scr-home.active')\u001b[22m\n\n\n  159 |     await injectSession(page);\n  160 |     await page.goto(BASE_URL);\n> 161 |     await expect(page.locator('#scr-home.active')).toBeVisible({ timeout: 8000 });\n      |                                                    ^\n  162 |     // Section is hidden until a slip is paid, but must exist in the DOM\n  163 |     await expect(page.locator('#pay-adv-section')).toHaveCount(1);\n  164 |     await expect(page.locator('#pay-adv-list')).toHaveCount(1);\n    at /var/www/360lm/tests/hr.spec.js:161:52"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 0,
                      "startTime": "2026-07-12T13:22:13.351Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "screenshot",
                          "contentType": "image/png",
                          "path": "/var/www/360lm/test-results/hr-HR-Management-—-Salary--024cf-es-advance-recovery-section-desktop-chrome/test-failed-1.png"
                        },
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/hr-HR-Management-—-Salary--024cf-es-advance-recovery-section-desktop-chrome/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/hr.spec.js",
                        "column": 52,
                        "line": 161
                      }
                    },
                    {
                      "workerIndex": 225,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 8292,
                      "error": {
                        "message": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mlocator\u001b[39m\u001b[2m).\u001b[22mtoBeVisible\u001b[2m(\u001b[22m\u001b[2m)\u001b[22m failed\n\nLocator: locator('#scr-home.active')\nExpected: visible\nTimeout: 8000ms\nError: element(s) not found\n\nCall log:\n\u001b[2m  - Expect \"toBeVisible\" with timeout 8000ms\u001b[22m\n\u001b[2m  - waiting for locator('#scr-home.active')\u001b[22m\n",
                        "stack": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mlocator\u001b[39m\u001b[2m).\u001b[22mtoBeVisible\u001b[2m(\u001b[22m\u001b[2m)\u001b[22m failed\n\nLocator: locator('#scr-home.active')\nExpected: visible\nTimeout: 8000ms\nError: element(s) not found\n\nCall log:\n\u001b[2m  - Expect \"toBeVisible\" with timeout 8000ms\u001b[22m\n\u001b[2m  - waiting for locator('#scr-home.active')\u001b[22m\n\n    at /var/www/360lm/tests/hr.spec.js:161:52",
                        "location": {
                          "file": "/var/www/360lm/tests/hr.spec.js",
                          "column": 52,
                          "line": 161
                        },
                        "snippet": "  159 |     await injectSession(page);\n  160 |     await page.goto(BASE_URL);\n> 161 |     await expect(page.locator('#scr-home.active')).toBeVisible({ timeout: 8000 });\n      |                                                    ^\n  162 |     // Section is hidden until a slip is paid, but must exist in the DOM\n  163 |     await expect(page.locator('#pay-adv-section')).toHaveCount(1);\n  164 |     await expect(page.locator('#pay-adv-list')).toHaveCount(1);"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/hr.spec.js",
                            "column": 52,
                            "line": 161
                          },
                          "message": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mlocator\u001b[39m\u001b[2m).\u001b[22mtoBeVisible\u001b[2m(\u001b[22m\u001b[2m)\u001b[22m failed\n\nLocator: locator('#scr-home.active')\nExpected: visible\nTimeout: 8000ms\nError: element(s) not found\n\nCall log:\n\u001b[2m  - Expect \"toBeVisible\" with timeout 8000ms\u001b[22m\n\u001b[2m  - waiting for locator('#scr-home.active')\u001b[22m\n\n\n  159 |     await injectSession(page);\n  160 |     await page.goto(BASE_URL);\n> 161 |     await expect(page.locator('#scr-home.active')).toBeVisible({ timeout: 8000 });\n      |                                                    ^\n  162 |     // Section is hidden until a slip is paid, but must exist in the DOM\n  163 |     await expect(page.locator('#pay-adv-section')).toHaveCount(1);\n  164 |     await expect(page.locator('#pay-adv-list')).toHaveCount(1);\n    at /var/www/360lm/tests/hr.spec.js:161:52"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 1,
                      "startTime": "2026-07-12T13:22:23.019Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "screenshot",
                          "contentType": "image/png",
                          "path": "/var/www/360lm/test-results/hr-HR-Management-—-Salary--024cf-es-advance-recovery-section-desktop-chrome-retry1/test-failed-1.png"
                        },
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/hr-HR-Management-—-Salary--024cf-es-advance-recovery-section-desktop-chrome-retry1/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/hr.spec.js",
                        "column": 52,
                        "line": 161
                      }
                    }
                  ],
                  "status": "unexpected"
                }
              ],
              "id": "98264c29bbadd0584463-8039da32a6f33158e356",
              "file": "hr.spec.js",
              "line": 158,
              "column": 3
            },
            {
              "title": "service worker version is current",
              "ok": false,
              "tags": [],
              "tests": [
                {
                  "timeout": 30000,
                  "annotations": [],
                  "expectedStatus": "passed",
                  "projectId": "desktop-chrome",
                  "projectName": "desktop-chrome",
                  "results": [
                    {
                      "workerIndex": 226,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 271,
                      "error": {
                        "message": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoMatch\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m)\u001b[22m\n\nExpected pattern: \u001b[32m/360hr-v\\d+/\u001b[39m\nReceived string:  \u001b[31m\"<!DOCTYPE HTML PUBLIC \\\"-//W3C//DTD HTML 4.01//EN\\\" \\\"http://www.w3.org/TR/html4/strict.dtd\\\">\u001b[39m\n\u001b[31m<html><head>\u001b[39m\n\u001b[31m<title>404 Not Found</title>\u001b[39m\n\u001b[31m</head><body>\u001b[39m\n\u001b[31m<h1>Not Found</h1>\u001b[39m\n\u001b[31m<p>The requested URL was not found on this server.</p>\u001b[39m\n\u001b[31m</body></html>\u001b[39m\n\u001b[31m\"\u001b[39m",
                        "stack": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoMatch\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m)\u001b[22m\n\nExpected pattern: \u001b[32m/360hr-v\\d+/\u001b[39m\nReceived string:  \u001b[31m\"<!DOCTYPE HTML PUBLIC \\\"-//W3C//DTD HTML 4.01//EN\\\" \\\"http://www.w3.org/TR/html4/strict.dtd\\\">\u001b[39m\n\u001b[31m<html><head>\u001b[39m\n\u001b[31m<title>404 Not Found</title>\u001b[39m\n\u001b[31m</head><body>\u001b[39m\n\u001b[31m<h1>Not Found</h1>\u001b[39m\n\u001b[31m<p>The requested URL was not found on this server.</p>\u001b[39m\n\u001b[31m</body></html>\u001b[39m\n\u001b[31m\"\u001b[39m\n    at /var/www/360lm/tests/hr.spec.js:170:33",
                        "location": {
                          "file": "/var/www/360lm/tests/hr.spec.js",
                          "column": 33,
                          "line": 170
                        },
                        "snippet": "  168 |   test('service worker version is current', async ({ page }) => {\n  169 |     const resp = await page.goto(BASE_URL + 'sw.js');\n> 170 |     expect((await resp.text())).toMatch(/360hr-v\\d+/);\n      |                                 ^\n  171 |   });\n  172 | });\n  173 |"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/hr.spec.js",
                            "column": 33,
                            "line": 170
                          },
                          "message": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoMatch\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m)\u001b[22m\n\nExpected pattern: \u001b[32m/360hr-v\\d+/\u001b[39m\nReceived string:  \u001b[31m\"<!DOCTYPE HTML PUBLIC \\\"-//W3C//DTD HTML 4.01//EN\\\" \\\"http://www.w3.org/TR/html4/strict.dtd\\\">\u001b[39m\n\u001b[31m<html><head>\u001b[39m\n\u001b[31m<title>404 Not Found</title>\u001b[39m\n\u001b[31m</head><body>\u001b[39m\n\u001b[31m<h1>Not Found</h1>\u001b[39m\n\u001b[31m<p>The requested URL was not found on this server.</p>\u001b[39m\n\u001b[31m</body></html>\u001b[39m\n\u001b[31m\"\u001b[39m\n\n  168 |   test('service worker version is current', async ({ page }) => {\n  169 |     const resp = await page.goto(BASE_URL + 'sw.js');\n> 170 |     expect((await resp.text())).toMatch(/360hr-v\\d+/);\n      |                                 ^\n  171 |   });\n  172 | });\n  173 |\n    at /var/www/360lm/tests/hr.spec.js:170:33"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 0,
                      "startTime": "2026-07-12T13:22:32.443Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "screenshot",
                          "contentType": "image/png",
                          "path": "/var/www/360lm/test-results/hr-HR-Management-—-Salary--bcdbd-e-worker-version-is-current-desktop-chrome/test-failed-1.png"
                        },
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/hr-HR-Management-—-Salary--bcdbd-e-worker-version-is-current-desktop-chrome/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/hr.spec.js",
                        "column": 33,
                        "line": 170
                      }
                    },
                    {
                      "workerIndex": 227,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 317,
                      "error": {
                        "message": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoMatch\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m)\u001b[22m\n\nExpected pattern: \u001b[32m/360hr-v\\d+/\u001b[39m\nReceived string:  \u001b[31m\"<!DOCTYPE HTML PUBLIC \\\"-//W3C//DTD HTML 4.01//EN\\\" \\\"http://www.w3.org/TR/html4/strict.dtd\\\">\u001b[39m\n\u001b[31m<html><head>\u001b[39m\n\u001b[31m<title>404 Not Found</title>\u001b[39m\n\u001b[31m</head><body>\u001b[39m\n\u001b[31m<h1>Not Found</h1>\u001b[39m\n\u001b[31m<p>The requested URL was not found on this server.</p>\u001b[39m\n\u001b[31m</body></html>\u001b[39m\n\u001b[31m\"\u001b[39m",
                        "stack": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoMatch\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m)\u001b[22m\n\nExpected pattern: \u001b[32m/360hr-v\\d+/\u001b[39m\nReceived string:  \u001b[31m\"<!DOCTYPE HTML PUBLIC \\\"-//W3C//DTD HTML 4.01//EN\\\" \\\"http://www.w3.org/TR/html4/strict.dtd\\\">\u001b[39m\n\u001b[31m<html><head>\u001b[39m\n\u001b[31m<title>404 Not Found</title>\u001b[39m\n\u001b[31m</head><body>\u001b[39m\n\u001b[31m<h1>Not Found</h1>\u001b[39m\n\u001b[31m<p>The requested URL was not found on this server.</p>\u001b[39m\n\u001b[31m</body></html>\u001b[39m\n\u001b[31m\"\u001b[39m\n    at /var/www/360lm/tests/hr.spec.js:170:33",
                        "location": {
                          "file": "/var/www/360lm/tests/hr.spec.js",
                          "column": 33,
                          "line": 170
                        },
                        "snippet": "  168 |   test('service worker version is current', async ({ page }) => {\n  169 |     const resp = await page.goto(BASE_URL + 'sw.js');\n> 170 |     expect((await resp.text())).toMatch(/360hr-v\\d+/);\n      |                                 ^\n  171 |   });\n  172 | });\n  173 |"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/hr.spec.js",
                            "column": 33,
                            "line": 170
                          },
                          "message": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoMatch\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m)\u001b[22m\n\nExpected pattern: \u001b[32m/360hr-v\\d+/\u001b[39m\nReceived string:  \u001b[31m\"<!DOCTYPE HTML PUBLIC \\\"-//W3C//DTD HTML 4.01//EN\\\" \\\"http://www.w3.org/TR/html4/strict.dtd\\\">\u001b[39m\n\u001b[31m<html><head>\u001b[39m\n\u001b[31m<title>404 Not Found</title>\u001b[39m\n\u001b[31m</head><body>\u001b[39m\n\u001b[31m<h1>Not Found</h1>\u001b[39m\n\u001b[31m<p>The requested URL was not found on this server.</p>\u001b[39m\n\u001b[31m</body></html>\u001b[39m\n\u001b[31m\"\u001b[39m\n\n  168 |   test('service worker version is current', async ({ page }) => {\n  169 |     const resp = await page.goto(BASE_URL + 'sw.js');\n> 170 |     expect((await resp.text())).toMatch(/360hr-v\\d+/);\n      |                                 ^\n  171 |   });\n  172 | });\n  173 |\n    at /var/www/360lm/tests/hr.spec.js:170:33"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 1,
                      "startTime": "2026-07-12T13:22:33.688Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "screenshot",
                          "contentType": "image/png",
                          "path": "/var/www/360lm/test-results/hr-HR-Management-—-Salary--bcdbd-e-worker-version-is-current-desktop-chrome-retry1/test-failed-1.png"
                        },
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/hr-HR-Management-—-Salary--bcdbd-e-worker-version-is-current-desktop-chrome-retry1/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/hr.spec.js",
                        "column": 33,
                        "line": 170
                      }
                    }
                  ],
                  "status": "unexpected"
                }
              ],
              "id": "98264c29bbadd0584463-1812ca435c76bc65f915",
              "file": "hr.spec.js",
              "line": 168,
              "column": 3
            }
          ]
        },
        {
          "title": "HR Management — Employee read path (web_anon)",
          "file": "hr.spec.js",
          "line": 178,
          "column": 6,
          "specs": [
            {
              "title": "GET /db/employees (expense profile) returns rows for web_anon [BUG-1]",
              "ok": true,
              "tags": [],
              "tests": [
                {
                  "timeout": 30000,
                  "annotations": [],
                  "expectedStatus": "passed",
                  "projectId": "android-chrome",
                  "projectName": "android-chrome",
                  "results": [
                    {
                      "workerIndex": 52,
                      "parallelIndex": 0,
                      "status": "passed",
                      "duration": 82,
                      "errors": [],
                      "stdout": [],
                      "stderr": [],
                      "retry": 0,
                      "startTime": "2026-07-12T13:02:17.189Z",
                      "annotations": [],
                      "attachments": []
                    }
                  ],
                  "status": "expected"
                }
              ],
              "id": "98264c29bbadd0584463-7621dadd18dd4b6b11e0",
              "file": "hr.spec.js",
              "line": 179,
              "column": 3
            },
            {
              "title": "generate_salary_slip runs as web_anon without permission error [BUG-3]",
              "ok": false,
              "tags": [],
              "tests": [
                {
                  "timeout": 30000,
                  "annotations": [],
                  "expectedStatus": "passed",
                  "projectId": "android-chrome",
                  "projectName": "android-chrome",
                  "results": [
                    {
                      "workerIndex": 52,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 19,
                      "error": {
                        "message": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoMatch\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m)\u001b[22m\n\nExpected pattern: \u001b[32m/salary structure|slip_id/i\u001b[39m\nReceived string:  \u001b[31m\"{\\\"code\\\":\\\"PGRST106\\\",\\\"details\\\":null,\\\"hint\\\":null,\\\"message\\\":\\\"The schema must be one of the following: hub, expense, sales, custodian, activity, recce, counters\\\"}\"\u001b[39m",
                        "stack": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoMatch\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m)\u001b[22m\n\nExpected pattern: \u001b[32m/salary structure|slip_id/i\u001b[39m\nReceived string:  \u001b[31m\"{\\\"code\\\":\\\"PGRST106\\\",\\\"details\\\":null,\\\"hint\\\":null,\\\"message\\\":\\\"The schema must be one of the following: hub, expense, sales, custodian, activity, recce, counters\\\"}\"\u001b[39m\n    at /var/www/360lm/tests/hr.spec.js:202:17",
                        "location": {
                          "file": "/var/www/360lm/tests/hr.spec.js",
                          "column": 17,
                          "line": 202
                        },
                        "snippet": "  200 |     expect(msg).not.toMatch(/42501/);\n  201 |     // It should reach business logic (no structure) — proving the employee read worked\n> 202 |     expect(msg).toMatch(/salary structure|slip_id/i);\n      |                 ^\n  203 |   });\n  204 | });\n  205 |"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/hr.spec.js",
                            "column": 17,
                            "line": 202
                          },
                          "message": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoMatch\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m)\u001b[22m\n\nExpected pattern: \u001b[32m/salary structure|slip_id/i\u001b[39m\nReceived string:  \u001b[31m\"{\\\"code\\\":\\\"PGRST106\\\",\\\"details\\\":null,\\\"hint\\\":null,\\\"message\\\":\\\"The schema must be one of the following: hub, expense, sales, custodian, activity, recce, counters\\\"}\"\u001b[39m\n\n  200 |     expect(msg).not.toMatch(/42501/);\n  201 |     // It should reach business logic (no structure) — proving the employee read worked\n> 202 |     expect(msg).toMatch(/salary structure|slip_id/i);\n      |                 ^\n  203 |   });\n  204 | });\n  205 |\n    at /var/www/360lm/tests/hr.spec.js:202:17"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 0,
                      "startTime": "2026-07-12T13:02:17.316Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/hr-HR-Management-—-Employe-8fb1e-out-permission-error-BUG-3--android-chrome/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/hr.spec.js",
                        "column": 17,
                        "line": 202
                      }
                    },
                    {
                      "workerIndex": 53,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 76,
                      "error": {
                        "message": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoMatch\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m)\u001b[22m\n\nExpected pattern: \u001b[32m/salary structure|slip_id/i\u001b[39m\nReceived string:  \u001b[31m\"{\\\"code\\\":\\\"PGRST106\\\",\\\"details\\\":null,\\\"hint\\\":null,\\\"message\\\":\\\"The schema must be one of the following: hub, expense, sales, custodian, activity, recce, counters\\\"}\"\u001b[39m",
                        "stack": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoMatch\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m)\u001b[22m\n\nExpected pattern: \u001b[32m/salary structure|slip_id/i\u001b[39m\nReceived string:  \u001b[31m\"{\\\"code\\\":\\\"PGRST106\\\",\\\"details\\\":null,\\\"hint\\\":null,\\\"message\\\":\\\"The schema must be one of the following: hub, expense, sales, custodian, activity, recce, counters\\\"}\"\u001b[39m\n    at /var/www/360lm/tests/hr.spec.js:202:17",
                        "location": {
                          "file": "/var/www/360lm/tests/hr.spec.js",
                          "column": 17,
                          "line": 202
                        },
                        "snippet": "  200 |     expect(msg).not.toMatch(/42501/);\n  201 |     // It should reach business logic (no structure) — proving the employee read worked\n> 202 |     expect(msg).toMatch(/salary structure|slip_id/i);\n      |                 ^\n  203 |   });\n  204 | });\n  205 |"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/hr.spec.js",
                            "column": 17,
                            "line": 202
                          },
                          "message": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoMatch\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m)\u001b[22m\n\nExpected pattern: \u001b[32m/salary structure|slip_id/i\u001b[39m\nReceived string:  \u001b[31m\"{\\\"code\\\":\\\"PGRST106\\\",\\\"details\\\":null,\\\"hint\\\":null,\\\"message\\\":\\\"The schema must be one of the following: hub, expense, sales, custodian, activity, recce, counters\\\"}\"\u001b[39m\n\n  200 |     expect(msg).not.toMatch(/42501/);\n  201 |     // It should reach business logic (no structure) — proving the employee read worked\n> 202 |     expect(msg).toMatch(/salary structure|slip_id/i);\n      |                 ^\n  203 |   });\n  204 | });\n  205 |\n    at /var/www/360lm/tests/hr.spec.js:202:17"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 1,
                      "startTime": "2026-07-12T13:02:18.128Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/hr-HR-Management-—-Employe-8fb1e-out-permission-error-BUG-3--android-chrome-retry1/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/hr.spec.js",
                        "column": 17,
                        "line": 202
                      }
                    }
                  ],
                  "status": "unexpected"
                }
              ],
              "id": "98264c29bbadd0584463-090e0d9032f49fc96ef7",
              "file": "hr.spec.js",
              "line": 189,
              "column": 3
            },
            {
              "title": "GET /db/employees (expense profile) returns rows for web_anon [BUG-1]",
              "ok": true,
              "tags": [],
              "tests": [
                {
                  "timeout": 30000,
                  "annotations": [],
                  "expectedStatus": "passed",
                  "projectId": "desktop-chrome",
                  "projectName": "desktop-chrome",
                  "results": [
                    {
                      "workerIndex": 228,
                      "parallelIndex": 0,
                      "status": "passed",
                      "duration": 91,
                      "errors": [],
                      "stdout": [],
                      "stderr": [],
                      "retry": 0,
                      "startTime": "2026-07-12T13:22:35.025Z",
                      "annotations": [],
                      "attachments": []
                    }
                  ],
                  "status": "expected"
                }
              ],
              "id": "98264c29bbadd0584463-005ecd07d98551964620",
              "file": "hr.spec.js",
              "line": 179,
              "column": 3
            },
            {
              "title": "generate_salary_slip runs as web_anon without permission error [BUG-3]",
              "ok": false,
              "tags": [],
              "tests": [
                {
                  "timeout": 30000,
                  "annotations": [],
                  "expectedStatus": "passed",
                  "projectId": "desktop-chrome",
                  "projectName": "desktop-chrome",
                  "results": [
                    {
                      "workerIndex": 228,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 29,
                      "error": {
                        "message": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoMatch\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m)\u001b[22m\n\nExpected pattern: \u001b[32m/salary structure|slip_id/i\u001b[39m\nReceived string:  \u001b[31m\"{\\\"code\\\":\\\"PGRST106\\\",\\\"details\\\":null,\\\"hint\\\":null,\\\"message\\\":\\\"The schema must be one of the following: hub, expense, sales, custodian, activity, recce, counters\\\"}\"\u001b[39m",
                        "stack": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoMatch\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m)\u001b[22m\n\nExpected pattern: \u001b[32m/salary structure|slip_id/i\u001b[39m\nReceived string:  \u001b[31m\"{\\\"code\\\":\\\"PGRST106\\\",\\\"details\\\":null,\\\"hint\\\":null,\\\"message\\\":\\\"The schema must be one of the following: hub, expense, sales, custodian, activity, recce, counters\\\"}\"\u001b[39m\n    at /var/www/360lm/tests/hr.spec.js:202:17",
                        "location": {
                          "file": "/var/www/360lm/tests/hr.spec.js",
                          "column": 17,
                          "line": 202
                        },
                        "snippet": "  200 |     expect(msg).not.toMatch(/42501/);\n  201 |     // It should reach business logic (no structure) — proving the employee read worked\n> 202 |     expect(msg).toMatch(/salary structure|slip_id/i);\n      |                 ^\n  203 |   });\n  204 | });\n  205 |"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/hr.spec.js",
                            "column": 17,
                            "line": 202
                          },
                          "message": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoMatch\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m)\u001b[22m\n\nExpected pattern: \u001b[32m/salary structure|slip_id/i\u001b[39m\nReceived string:  \u001b[31m\"{\\\"code\\\":\\\"PGRST106\\\",\\\"details\\\":null,\\\"hint\\\":null,\\\"message\\\":\\\"The schema must be one of the following: hub, expense, sales, custodian, activity, recce, counters\\\"}\"\u001b[39m\n\n  200 |     expect(msg).not.toMatch(/42501/);\n  201 |     // It should reach business logic (no structure) — proving the employee read worked\n> 202 |     expect(msg).toMatch(/salary structure|slip_id/i);\n      |                 ^\n  203 |   });\n  204 | });\n  205 |\n    at /var/www/360lm/tests/hr.spec.js:202:17"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 0,
                      "startTime": "2026-07-12T13:22:35.172Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/hr-HR-Management-—-Employe-8fb1e-out-permission-error-BUG-3--desktop-chrome/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/hr.spec.js",
                        "column": 17,
                        "line": 202
                      }
                    },
                    {
                      "workerIndex": 229,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 59,
                      "error": {
                        "message": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoMatch\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m)\u001b[22m\n\nExpected pattern: \u001b[32m/salary structure|slip_id/i\u001b[39m\nReceived string:  \u001b[31m\"{\\\"code\\\":\\\"PGRST106\\\",\\\"details\\\":null,\\\"hint\\\":null,\\\"message\\\":\\\"The schema must be one of the following: hub, expense, sales, custodian, activity, recce, counters\\\"}\"\u001b[39m",
                        "stack": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoMatch\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m)\u001b[22m\n\nExpected pattern: \u001b[32m/salary structure|slip_id/i\u001b[39m\nReceived string:  \u001b[31m\"{\\\"code\\\":\\\"PGRST106\\\",\\\"details\\\":null,\\\"hint\\\":null,\\\"message\\\":\\\"The schema must be one of the following: hub, expense, sales, custodian, activity, recce, counters\\\"}\"\u001b[39m\n    at /var/www/360lm/tests/hr.spec.js:202:17",
                        "location": {
                          "file": "/var/www/360lm/tests/hr.spec.js",
                          "column": 17,
                          "line": 202
                        },
                        "snippet": "  200 |     expect(msg).not.toMatch(/42501/);\n  201 |     // It should reach business logic (no structure) — proving the employee read worked\n> 202 |     expect(msg).toMatch(/salary structure|slip_id/i);\n      |                 ^\n  203 |   });\n  204 | });\n  205 |"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/hr.spec.js",
                            "column": 17,
                            "line": 202
                          },
                          "message": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoMatch\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m)\u001b[22m\n\nExpected pattern: \u001b[32m/salary structure|slip_id/i\u001b[39m\nReceived string:  \u001b[31m\"{\\\"code\\\":\\\"PGRST106\\\",\\\"details\\\":null,\\\"hint\\\":null,\\\"message\\\":\\\"The schema must be one of the following: hub, expense, sales, custodian, activity, recce, counters\\\"}\"\u001b[39m\n\n  200 |     expect(msg).not.toMatch(/42501/);\n  201 |     // It should reach business logic (no structure) — proving the employee read worked\n> 202 |     expect(msg).toMatch(/salary structure|slip_id/i);\n      |                 ^\n  203 |   });\n  204 | });\n  205 |\n    at /var/www/360lm/tests/hr.spec.js:202:17"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 1,
                      "startTime": "2026-07-12T13:22:36.059Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/hr-HR-Management-—-Employe-8fb1e-out-permission-error-BUG-3--desktop-chrome-retry1/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/hr.spec.js",
                        "column": 17,
                        "line": 202
                      }
                    }
                  ],
                  "status": "unexpected"
                }
              ],
              "id": "98264c29bbadd0584463-e612f8208002c2c0a394",
              "file": "hr.spec.js",
              "line": 189,
              "column": 3
            }
          ]
        }
      ]
    },
    {
      "title": "printing.spec.js",
      "file": "printing.spec.js",
      "column": 0,
      "line": 0,
      "specs": [],
      "suites": [
        {
          "title": "Print Nest — Page Load",
          "file": "printing.spec.js",
          "line": 20,
          "column": 6,
          "specs": [
            {
              "title": "no session → redirects to hub",
              "ok": false,
              "tags": [],
              "tests": [
                {
                  "timeout": 30000,
                  "annotations": [],
                  "expectedStatus": "passed",
                  "projectId": "android-chrome",
                  "projectName": "android-chrome",
                  "results": [
                    {
                      "workerIndex": 54,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 5235,
                      "error": {
                        "message": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mpage\u001b[39m\u001b[2m).\u001b[22mtoHaveURL\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m)\u001b[22m failed\n\nExpected pattern: \u001b[32m/\\/hub\\//\u001b[39m\nReceived string:  \u001b[31m\"https://srv1111289.hstgr.cloud/printing/\"\u001b[39m\nTimeout: 5000ms\n\nCall log:\n\u001b[2m  - Expect \"toHaveURL\" with timeout 5000ms\u001b[22m\n\u001b[2m    14 × unexpected value \"https://srv1111289.hstgr.cloud/printing/\"\u001b[22m\n",
                        "stack": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mpage\u001b[39m\u001b[2m).\u001b[22mtoHaveURL\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m)\u001b[22m failed\n\nExpected pattern: \u001b[32m/\\/hub\\//\u001b[39m\nReceived string:  \u001b[31m\"https://srv1111289.hstgr.cloud/printing/\"\u001b[39m\nTimeout: 5000ms\n\nCall log:\n\u001b[2m  - Expect \"toHaveURL\" with timeout 5000ms\u001b[22m\n\u001b[2m    14 × unexpected value \"https://srv1111289.hstgr.cloud/printing/\"\u001b[22m\n\n    at /var/www/360lm/tests/printing.spec.js:23:24",
                        "location": {
                          "file": "/var/www/360lm/tests/printing.spec.js",
                          "column": 24,
                          "line": 23
                        },
                        "snippet": "  21 |   test('no session → redirects to hub', async ({ page }) => {\n  22 |     await page.goto(BASE_URL);\n> 23 |     await expect(page).toHaveURL(/\\/hub\\//);\n     |                        ^\n  24 |   });\n  25 |\n  26 |   test('with session → dashboard visible', async ({ page }) => {"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/printing.spec.js",
                            "column": 24,
                            "line": 23
                          },
                          "message": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mpage\u001b[39m\u001b[2m).\u001b[22mtoHaveURL\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m)\u001b[22m failed\n\nExpected pattern: \u001b[32m/\\/hub\\//\u001b[39m\nReceived string:  \u001b[31m\"https://srv1111289.hstgr.cloud/printing/\"\u001b[39m\nTimeout: 5000ms\n\nCall log:\n\u001b[2m  - Expect \"toHaveURL\" with timeout 5000ms\u001b[22m\n\u001b[2m    14 × unexpected value \"https://srv1111289.hstgr.cloud/printing/\"\u001b[22m\n\n\n  21 |   test('no session → redirects to hub', async ({ page }) => {\n  22 |     await page.goto(BASE_URL);\n> 23 |     await expect(page).toHaveURL(/\\/hub\\//);\n     |                        ^\n  24 |   });\n  25 |\n  26 |   test('with session → dashboard visible', async ({ page }) => {\n    at /var/www/360lm/tests/printing.spec.js:23:24"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 0,
                      "startTime": "2026-07-12T13:02:19.050Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "screenshot",
                          "contentType": "image/png",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-Page-8a6d6--session-→-redirects-to-hub-android-chrome/test-failed-1.png"
                        },
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-Page-8a6d6--session-→-redirects-to-hub-android-chrome/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/printing.spec.js",
                        "column": 24,
                        "line": 23
                      }
                    },
                    {
                      "workerIndex": 55,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 5251,
                      "error": {
                        "message": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mpage\u001b[39m\u001b[2m).\u001b[22mtoHaveURL\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m)\u001b[22m failed\n\nExpected pattern: \u001b[32m/\\/hub\\//\u001b[39m\nReceived string:  \u001b[31m\"https://srv1111289.hstgr.cloud/printing/\"\u001b[39m\nTimeout: 5000ms\n\nCall log:\n\u001b[2m  - Expect \"toHaveURL\" with timeout 5000ms\u001b[22m\n\u001b[2m    14 × unexpected value \"https://srv1111289.hstgr.cloud/printing/\"\u001b[22m\n",
                        "stack": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mpage\u001b[39m\u001b[2m).\u001b[22mtoHaveURL\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m)\u001b[22m failed\n\nExpected pattern: \u001b[32m/\\/hub\\//\u001b[39m\nReceived string:  \u001b[31m\"https://srv1111289.hstgr.cloud/printing/\"\u001b[39m\nTimeout: 5000ms\n\nCall log:\n\u001b[2m  - Expect \"toHaveURL\" with timeout 5000ms\u001b[22m\n\u001b[2m    14 × unexpected value \"https://srv1111289.hstgr.cloud/printing/\"\u001b[22m\n\n    at /var/www/360lm/tests/printing.spec.js:23:24",
                        "location": {
                          "file": "/var/www/360lm/tests/printing.spec.js",
                          "column": 24,
                          "line": 23
                        },
                        "snippet": "  21 |   test('no session → redirects to hub', async ({ page }) => {\n  22 |     await page.goto(BASE_URL);\n> 23 |     await expect(page).toHaveURL(/\\/hub\\//);\n     |                        ^\n  24 |   });\n  25 |\n  26 |   test('with session → dashboard visible', async ({ page }) => {"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/printing.spec.js",
                            "column": 24,
                            "line": 23
                          },
                          "message": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mpage\u001b[39m\u001b[2m).\u001b[22mtoHaveURL\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m)\u001b[22m failed\n\nExpected pattern: \u001b[32m/\\/hub\\//\u001b[39m\nReceived string:  \u001b[31m\"https://srv1111289.hstgr.cloud/printing/\"\u001b[39m\nTimeout: 5000ms\n\nCall log:\n\u001b[2m  - Expect \"toHaveURL\" with timeout 5000ms\u001b[22m\n\u001b[2m    14 × unexpected value \"https://srv1111289.hstgr.cloud/printing/\"\u001b[22m\n\n\n  21 |   test('no session → redirects to hub', async ({ page }) => {\n  22 |     await page.goto(BASE_URL);\n> 23 |     await expect(page).toHaveURL(/\\/hub\\//);\n     |                        ^\n  24 |   });\n  25 |\n  26 |   test('with session → dashboard visible', async ({ page }) => {\n    at /var/www/360lm/tests/printing.spec.js:23:24"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 1,
                      "startTime": "2026-07-12T13:02:25.267Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "screenshot",
                          "contentType": "image/png",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-Page-8a6d6--session-→-redirects-to-hub-android-chrome-retry1/test-failed-1.png"
                        },
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-Page-8a6d6--session-→-redirects-to-hub-android-chrome-retry1/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/printing.spec.js",
                        "column": 24,
                        "line": 23
                      }
                    }
                  ],
                  "status": "unexpected"
                }
              ],
              "id": "84f3bc84c047e9f9c0b8-e9a03ed15400c0adb24e",
              "file": "printing.spec.js",
              "line": 21,
              "column": 3
            },
            {
              "title": "with session → dashboard visible",
              "ok": false,
              "tags": [],
              "tests": [
                {
                  "timeout": 30000,
                  "annotations": [],
                  "expectedStatus": "passed",
                  "projectId": "android-chrome",
                  "projectName": "android-chrome",
                  "results": [
                    {
                      "workerIndex": 56,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 8256,
                      "error": {
                        "message": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mlocator\u001b[39m\u001b[2m).\u001b[22mtoBeVisible\u001b[2m(\u001b[22m\u001b[2m)\u001b[22m failed\n\nLocator: locator('#screen-dash.active')\nExpected: visible\nTimeout: 8000ms\nError: element(s) not found\n\nCall log:\n\u001b[2m  - Expect \"toBeVisible\" with timeout 8000ms\u001b[22m\n\u001b[2m  - waiting for locator('#screen-dash.active')\u001b[22m\n",
                        "stack": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mlocator\u001b[39m\u001b[2m).\u001b[22mtoBeVisible\u001b[2m(\u001b[22m\u001b[2m)\u001b[22m failed\n\nLocator: locator('#screen-dash.active')\nExpected: visible\nTimeout: 8000ms\nError: element(s) not found\n\nCall log:\n\u001b[2m  - Expect \"toBeVisible\" with timeout 8000ms\u001b[22m\n\u001b[2m  - waiting for locator('#screen-dash.active')\u001b[22m\n\n    at /var/www/360lm/tests/printing.spec.js:29:55",
                        "location": {
                          "file": "/var/www/360lm/tests/printing.spec.js",
                          "column": 55,
                          "line": 29
                        },
                        "snippet": "  27 |     await injectSession(page);\n  28 |     await page.goto(BASE_URL);\n> 29 |     await expect(page.locator('#screen-dash.active')).toBeVisible({ timeout: 8000 });\n     |                                                       ^\n  30 |   });\n  31 |\n  32 |   test('manifest.json reachable', async ({ page }) => {"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/printing.spec.js",
                            "column": 55,
                            "line": 29
                          },
                          "message": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mlocator\u001b[39m\u001b[2m).\u001b[22mtoBeVisible\u001b[2m(\u001b[22m\u001b[2m)\u001b[22m failed\n\nLocator: locator('#screen-dash.active')\nExpected: visible\nTimeout: 8000ms\nError: element(s) not found\n\nCall log:\n\u001b[2m  - Expect \"toBeVisible\" with timeout 8000ms\u001b[22m\n\u001b[2m  - waiting for locator('#screen-dash.active')\u001b[22m\n\n\n  27 |     await injectSession(page);\n  28 |     await page.goto(BASE_URL);\n> 29 |     await expect(page.locator('#screen-dash.active')).toBeVisible({ timeout: 8000 });\n     |                                                       ^\n  30 |   });\n  31 |\n  32 |   test('manifest.json reachable', async ({ page }) => {\n    at /var/www/360lm/tests/printing.spec.js:29:55"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 0,
                      "startTime": "2026-07-12T13:02:31.481Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "screenshot",
                          "contentType": "image/png",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-Page-bcc0c-session-→-dashboard-visible-android-chrome/test-failed-1.png"
                        },
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-Page-bcc0c-session-→-dashboard-visible-android-chrome/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/printing.spec.js",
                        "column": 55,
                        "line": 29
                      }
                    },
                    {
                      "workerIndex": 57,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 8286,
                      "error": {
                        "message": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mlocator\u001b[39m\u001b[2m).\u001b[22mtoBeVisible\u001b[2m(\u001b[22m\u001b[2m)\u001b[22m failed\n\nLocator: locator('#screen-dash.active')\nExpected: visible\nTimeout: 8000ms\nError: element(s) not found\n\nCall log:\n\u001b[2m  - Expect \"toBeVisible\" with timeout 8000ms\u001b[22m\n\u001b[2m  - waiting for locator('#screen-dash.active')\u001b[22m\n",
                        "stack": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mlocator\u001b[39m\u001b[2m).\u001b[22mtoBeVisible\u001b[2m(\u001b[22m\u001b[2m)\u001b[22m failed\n\nLocator: locator('#screen-dash.active')\nExpected: visible\nTimeout: 8000ms\nError: element(s) not found\n\nCall log:\n\u001b[2m  - Expect \"toBeVisible\" with timeout 8000ms\u001b[22m\n\u001b[2m  - waiting for locator('#screen-dash.active')\u001b[22m\n\n    at /var/www/360lm/tests/printing.spec.js:29:55",
                        "location": {
                          "file": "/var/www/360lm/tests/printing.spec.js",
                          "column": 55,
                          "line": 29
                        },
                        "snippet": "  27 |     await injectSession(page);\n  28 |     await page.goto(BASE_URL);\n> 29 |     await expect(page.locator('#screen-dash.active')).toBeVisible({ timeout: 8000 });\n     |                                                       ^\n  30 |   });\n  31 |\n  32 |   test('manifest.json reachable', async ({ page }) => {"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/printing.spec.js",
                            "column": 55,
                            "line": 29
                          },
                          "message": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mlocator\u001b[39m\u001b[2m).\u001b[22mtoBeVisible\u001b[2m(\u001b[22m\u001b[2m)\u001b[22m failed\n\nLocator: locator('#screen-dash.active')\nExpected: visible\nTimeout: 8000ms\nError: element(s) not found\n\nCall log:\n\u001b[2m  - Expect \"toBeVisible\" with timeout 8000ms\u001b[22m\n\u001b[2m  - waiting for locator('#screen-dash.active')\u001b[22m\n\n\n  27 |     await injectSession(page);\n  28 |     await page.goto(BASE_URL);\n> 29 |     await expect(page.locator('#screen-dash.active')).toBeVisible({ timeout: 8000 });\n     |                                                       ^\n  30 |   });\n  31 |\n  32 |   test('manifest.json reachable', async ({ page }) => {\n    at /var/www/360lm/tests/printing.spec.js:29:55"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 1,
                      "startTime": "2026-07-12T13:02:40.749Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "screenshot",
                          "contentType": "image/png",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-Page-bcc0c-session-→-dashboard-visible-android-chrome-retry1/test-failed-1.png"
                        },
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-Page-bcc0c-session-→-dashboard-visible-android-chrome-retry1/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/printing.spec.js",
                        "column": 55,
                        "line": 29
                      }
                    }
                  ],
                  "status": "unexpected"
                }
              ],
              "id": "84f3bc84c047e9f9c0b8-7006444a9ba7177df2b0",
              "file": "printing.spec.js",
              "line": 26,
              "column": 3
            },
            {
              "title": "manifest.json reachable",
              "ok": false,
              "tags": [],
              "tests": [
                {
                  "timeout": 30000,
                  "annotations": [],
                  "expectedStatus": "passed",
                  "projectId": "android-chrome",
                  "projectName": "android-chrome",
                  "results": [
                    {
                      "workerIndex": 58,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 291,
                      "error": {
                        "message": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoBe\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m) // Object.is equality\u001b[22m\n\nExpected: \u001b[32m200\u001b[39m\nReceived: \u001b[31m404\u001b[39m",
                        "stack": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoBe\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m) // Object.is equality\u001b[22m\n\nExpected: \u001b[32m200\u001b[39m\nReceived: \u001b[31m404\u001b[39m\n    at /var/www/360lm/tests/printing.spec.js:34:24",
                        "location": {
                          "file": "/var/www/360lm/tests/printing.spec.js",
                          "column": 24,
                          "line": 34
                        },
                        "snippet": "  32 |   test('manifest.json reachable', async ({ page }) => {\n  33 |     const r = await page.request.get(BASE_URL + 'manifest.json');\n> 34 |     expect(r.status()).toBe(200);\n     |                        ^\n  35 |     const json = await r.json();\n  36 |     expect(json.name).toContain('Print Nest');\n  37 |     expect(json.start_url).toBe('/printing/');"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/printing.spec.js",
                            "column": 24,
                            "line": 34
                          },
                          "message": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoBe\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m) // Object.is equality\u001b[22m\n\nExpected: \u001b[32m200\u001b[39m\nReceived: \u001b[31m404\u001b[39m\n\n  32 |   test('manifest.json reachable', async ({ page }) => {\n  33 |     const r = await page.request.get(BASE_URL + 'manifest.json');\n> 34 |     expect(r.status()).toBe(200);\n     |                        ^\n  35 |     const json = await r.json();\n  36 |     expect(json.name).toContain('Print Nest');\n  37 |     expect(json.start_url).toBe('/printing/');\n    at /var/www/360lm/tests/printing.spec.js:34:24"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 0,
                      "startTime": "2026-07-12T13:02:50.172Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "screenshot",
                          "contentType": "image/png",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-Page-Load-manifest-json-reachable-android-chrome/test-failed-1.png"
                        },
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-Page-Load-manifest-json-reachable-android-chrome/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/printing.spec.js",
                        "column": 24,
                        "line": 34
                      }
                    },
                    {
                      "workerIndex": 59,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 277,
                      "error": {
                        "message": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoBe\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m) // Object.is equality\u001b[22m\n\nExpected: \u001b[32m200\u001b[39m\nReceived: \u001b[31m404\u001b[39m",
                        "stack": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoBe\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m) // Object.is equality\u001b[22m\n\nExpected: \u001b[32m200\u001b[39m\nReceived: \u001b[31m404\u001b[39m\n    at /var/www/360lm/tests/printing.spec.js:34:24",
                        "location": {
                          "file": "/var/www/360lm/tests/printing.spec.js",
                          "column": 24,
                          "line": 34
                        },
                        "snippet": "  32 |   test('manifest.json reachable', async ({ page }) => {\n  33 |     const r = await page.request.get(BASE_URL + 'manifest.json');\n> 34 |     expect(r.status()).toBe(200);\n     |                        ^\n  35 |     const json = await r.json();\n  36 |     expect(json.name).toContain('Print Nest');\n  37 |     expect(json.start_url).toBe('/printing/');"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/printing.spec.js",
                            "column": 24,
                            "line": 34
                          },
                          "message": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoBe\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m) // Object.is equality\u001b[22m\n\nExpected: \u001b[32m200\u001b[39m\nReceived: \u001b[31m404\u001b[39m\n\n  32 |   test('manifest.json reachable', async ({ page }) => {\n  33 |     const r = await page.request.get(BASE_URL + 'manifest.json');\n> 34 |     expect(r.status()).toBe(200);\n     |                        ^\n  35 |     const json = await r.json();\n  36 |     expect(json.name).toContain('Print Nest');\n  37 |     expect(json.start_url).toBe('/printing/');\n    at /var/www/360lm/tests/printing.spec.js:34:24"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 1,
                      "startTime": "2026-07-12T13:02:51.574Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "screenshot",
                          "contentType": "image/png",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-Page-Load-manifest-json-reachable-android-chrome-retry1/test-failed-1.png"
                        },
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-Page-Load-manifest-json-reachable-android-chrome-retry1/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/printing.spec.js",
                        "column": 24,
                        "line": 34
                      }
                    }
                  ],
                  "status": "unexpected"
                }
              ],
              "id": "84f3bc84c047e9f9c0b8-c5d807eb50919c436141",
              "file": "printing.spec.js",
              "line": 32,
              "column": 3
            },
            {
              "title": "service worker registers",
              "ok": false,
              "tags": [],
              "tests": [
                {
                  "timeout": 30000,
                  "annotations": [],
                  "expectedStatus": "passed",
                  "projectId": "android-chrome",
                  "projectName": "android-chrome",
                  "results": [
                    {
                      "workerIndex": 60,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 8322,
                      "error": {
                        "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n",
                        "stack": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n    at /var/www/360lm/tests/printing.spec.js:43:16",
                        "location": {
                          "file": "/var/www/360lm/tests/printing.spec.js",
                          "column": 16,
                          "line": 43
                        },
                        "snippet": "  41 |     await injectSession(page);\n  42 |     await page.goto(BASE_URL);\n> 43 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n     |                ^\n  44 |     const swRegistered = await page.evaluate(async () => {\n  45 |       const regs = await navigator.serviceWorker.getRegistrations();\n  46 |       return regs.some(r => r.scope.includes('/printing/'));"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/printing.spec.js",
                            "column": 16,
                            "line": 43
                          },
                          "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n\n  41 |     await injectSession(page);\n  42 |     await page.goto(BASE_URL);\n> 43 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n     |                ^\n  44 |     const swRegistered = await page.evaluate(async () => {\n  45 |       const regs = await navigator.serviceWorker.getRegistrations();\n  46 |       return regs.some(r => r.scope.includes('/printing/'));\n    at /var/www/360lm/tests/printing.spec.js:43:16"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 0,
                      "startTime": "2026-07-12T13:02:52.875Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "screenshot",
                          "contentType": "image/png",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-Page-Load-service-worker-registers-android-chrome/test-failed-1.png"
                        },
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-Page-Load-service-worker-registers-android-chrome/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/printing.spec.js",
                        "column": 16,
                        "line": 43
                      }
                    },
                    {
                      "workerIndex": 61,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 8286,
                      "error": {
                        "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n",
                        "stack": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n    at /var/www/360lm/tests/printing.spec.js:43:16",
                        "location": {
                          "file": "/var/www/360lm/tests/printing.spec.js",
                          "column": 16,
                          "line": 43
                        },
                        "snippet": "  41 |     await injectSession(page);\n  42 |     await page.goto(BASE_URL);\n> 43 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n     |                ^\n  44 |     const swRegistered = await page.evaluate(async () => {\n  45 |       const regs = await navigator.serviceWorker.getRegistrations();\n  46 |       return regs.some(r => r.scope.includes('/printing/'));"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/printing.spec.js",
                            "column": 16,
                            "line": 43
                          },
                          "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n\n  41 |     await injectSession(page);\n  42 |     await page.goto(BASE_URL);\n> 43 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n     |                ^\n  44 |     const swRegistered = await page.evaluate(async () => {\n  45 |       const regs = await navigator.serviceWorker.getRegistrations();\n  46 |       return regs.some(r => r.scope.includes('/printing/'));\n    at /var/www/360lm/tests/printing.spec.js:43:16"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 1,
                      "startTime": "2026-07-12T13:03:03.050Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "screenshot",
                          "contentType": "image/png",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-Page-Load-service-worker-registers-android-chrome-retry1/test-failed-1.png"
                        },
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-Page-Load-service-worker-registers-android-chrome-retry1/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/printing.spec.js",
                        "column": 16,
                        "line": 43
                      }
                    }
                  ],
                  "status": "unexpected"
                }
              ],
              "id": "84f3bc84c047e9f9c0b8-1b08180ed543df04f851",
              "file": "printing.spec.js",
              "line": 40,
              "column": 3
            },
            {
              "title": "no session → redirects to hub",
              "ok": false,
              "tags": [],
              "tests": [
                {
                  "timeout": 30000,
                  "annotations": [],
                  "expectedStatus": "passed",
                  "projectId": "desktop-chrome",
                  "projectName": "desktop-chrome",
                  "results": [
                    {
                      "workerIndex": 230,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 5267,
                      "error": {
                        "message": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mpage\u001b[39m\u001b[2m).\u001b[22mtoHaveURL\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m)\u001b[22m failed\n\nExpected pattern: \u001b[32m/\\/hub\\//\u001b[39m\nReceived string:  \u001b[31m\"https://srv1111289.hstgr.cloud/printing/\"\u001b[39m\nTimeout: 5000ms\n\nCall log:\n\u001b[2m  - Expect \"toHaveURL\" with timeout 5000ms\u001b[22m\n\u001b[2m    14 × unexpected value \"https://srv1111289.hstgr.cloud/printing/\"\u001b[22m\n",
                        "stack": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mpage\u001b[39m\u001b[2m).\u001b[22mtoHaveURL\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m)\u001b[22m failed\n\nExpected pattern: \u001b[32m/\\/hub\\//\u001b[39m\nReceived string:  \u001b[31m\"https://srv1111289.hstgr.cloud/printing/\"\u001b[39m\nTimeout: 5000ms\n\nCall log:\n\u001b[2m  - Expect \"toHaveURL\" with timeout 5000ms\u001b[22m\n\u001b[2m    14 × unexpected value \"https://srv1111289.hstgr.cloud/printing/\"\u001b[22m\n\n    at /var/www/360lm/tests/printing.spec.js:23:24",
                        "location": {
                          "file": "/var/www/360lm/tests/printing.spec.js",
                          "column": 24,
                          "line": 23
                        },
                        "snippet": "  21 |   test('no session → redirects to hub', async ({ page }) => {\n  22 |     await page.goto(BASE_URL);\n> 23 |     await expect(page).toHaveURL(/\\/hub\\//);\n     |                        ^\n  24 |   });\n  25 |\n  26 |   test('with session → dashboard visible', async ({ page }) => {"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/printing.spec.js",
                            "column": 24,
                            "line": 23
                          },
                          "message": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mpage\u001b[39m\u001b[2m).\u001b[22mtoHaveURL\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m)\u001b[22m failed\n\nExpected pattern: \u001b[32m/\\/hub\\//\u001b[39m\nReceived string:  \u001b[31m\"https://srv1111289.hstgr.cloud/printing/\"\u001b[39m\nTimeout: 5000ms\n\nCall log:\n\u001b[2m  - Expect \"toHaveURL\" with timeout 5000ms\u001b[22m\n\u001b[2m    14 × unexpected value \"https://srv1111289.hstgr.cloud/printing/\"\u001b[22m\n\n\n  21 |   test('no session → redirects to hub', async ({ page }) => {\n  22 |     await page.goto(BASE_URL);\n> 23 |     await expect(page).toHaveURL(/\\/hub\\//);\n     |                        ^\n  24 |   });\n  25 |\n  26 |   test('with session → dashboard visible', async ({ page }) => {\n    at /var/www/360lm/tests/printing.spec.js:23:24"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 0,
                      "startTime": "2026-07-12T13:22:36.990Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "screenshot",
                          "contentType": "image/png",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-Page-8a6d6--session-→-redirects-to-hub-desktop-chrome/test-failed-1.png"
                        },
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-Page-8a6d6--session-→-redirects-to-hub-desktop-chrome/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/printing.spec.js",
                        "column": 24,
                        "line": 23
                      }
                    },
                    {
                      "workerIndex": 231,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 5296,
                      "error": {
                        "message": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mpage\u001b[39m\u001b[2m).\u001b[22mtoHaveURL\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m)\u001b[22m failed\n\nExpected pattern: \u001b[32m/\\/hub\\//\u001b[39m\nReceived string:  \u001b[31m\"https://srv1111289.hstgr.cloud/printing/\"\u001b[39m\nTimeout: 5000ms\n\nCall log:\n\u001b[2m  - Expect \"toHaveURL\" with timeout 5000ms\u001b[22m\n\u001b[2m    14 × unexpected value \"https://srv1111289.hstgr.cloud/printing/\"\u001b[22m\n",
                        "stack": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mpage\u001b[39m\u001b[2m).\u001b[22mtoHaveURL\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m)\u001b[22m failed\n\nExpected pattern: \u001b[32m/\\/hub\\//\u001b[39m\nReceived string:  \u001b[31m\"https://srv1111289.hstgr.cloud/printing/\"\u001b[39m\nTimeout: 5000ms\n\nCall log:\n\u001b[2m  - Expect \"toHaveURL\" with timeout 5000ms\u001b[22m\n\u001b[2m    14 × unexpected value \"https://srv1111289.hstgr.cloud/printing/\"\u001b[22m\n\n    at /var/www/360lm/tests/printing.spec.js:23:24",
                        "location": {
                          "file": "/var/www/360lm/tests/printing.spec.js",
                          "column": 24,
                          "line": 23
                        },
                        "snippet": "  21 |   test('no session → redirects to hub', async ({ page }) => {\n  22 |     await page.goto(BASE_URL);\n> 23 |     await expect(page).toHaveURL(/\\/hub\\//);\n     |                        ^\n  24 |   });\n  25 |\n  26 |   test('with session → dashboard visible', async ({ page }) => {"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/printing.spec.js",
                            "column": 24,
                            "line": 23
                          },
                          "message": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mpage\u001b[39m\u001b[2m).\u001b[22mtoHaveURL\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m)\u001b[22m failed\n\nExpected pattern: \u001b[32m/\\/hub\\//\u001b[39m\nReceived string:  \u001b[31m\"https://srv1111289.hstgr.cloud/printing/\"\u001b[39m\nTimeout: 5000ms\n\nCall log:\n\u001b[2m  - Expect \"toHaveURL\" with timeout 5000ms\u001b[22m\n\u001b[2m    14 × unexpected value \"https://srv1111289.hstgr.cloud/printing/\"\u001b[22m\n\n\n  21 |   test('no session → redirects to hub', async ({ page }) => {\n  22 |     await page.goto(BASE_URL);\n> 23 |     await expect(page).toHaveURL(/\\/hub\\//);\n     |                        ^\n  24 |   });\n  25 |\n  26 |   test('with session → dashboard visible', async ({ page }) => {\n    at /var/www/360lm/tests/printing.spec.js:23:24"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 1,
                      "startTime": "2026-07-12T13:22:43.365Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "screenshot",
                          "contentType": "image/png",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-Page-8a6d6--session-→-redirects-to-hub-desktop-chrome-retry1/test-failed-1.png"
                        },
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-Page-8a6d6--session-→-redirects-to-hub-desktop-chrome-retry1/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/printing.spec.js",
                        "column": 24,
                        "line": 23
                      }
                    }
                  ],
                  "status": "unexpected"
                }
              ],
              "id": "84f3bc84c047e9f9c0b8-599665ed77b1c5a0d64d",
              "file": "printing.spec.js",
              "line": 21,
              "column": 3
            },
            {
              "title": "with session → dashboard visible",
              "ok": false,
              "tags": [],
              "tests": [
                {
                  "timeout": 30000,
                  "annotations": [],
                  "expectedStatus": "passed",
                  "projectId": "desktop-chrome",
                  "projectName": "desktop-chrome",
                  "results": [
                    {
                      "workerIndex": 232,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 8299,
                      "error": {
                        "message": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mlocator\u001b[39m\u001b[2m).\u001b[22mtoBeVisible\u001b[2m(\u001b[22m\u001b[2m)\u001b[22m failed\n\nLocator: locator('#screen-dash.active')\nExpected: visible\nTimeout: 8000ms\nError: element(s) not found\n\nCall log:\n\u001b[2m  - Expect \"toBeVisible\" with timeout 8000ms\u001b[22m\n\u001b[2m  - waiting for locator('#screen-dash.active')\u001b[22m\n",
                        "stack": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mlocator\u001b[39m\u001b[2m).\u001b[22mtoBeVisible\u001b[2m(\u001b[22m\u001b[2m)\u001b[22m failed\n\nLocator: locator('#screen-dash.active')\nExpected: visible\nTimeout: 8000ms\nError: element(s) not found\n\nCall log:\n\u001b[2m  - Expect \"toBeVisible\" with timeout 8000ms\u001b[22m\n\u001b[2m  - waiting for locator('#screen-dash.active')\u001b[22m\n\n    at /var/www/360lm/tests/printing.spec.js:29:55",
                        "location": {
                          "file": "/var/www/360lm/tests/printing.spec.js",
                          "column": 55,
                          "line": 29
                        },
                        "snippet": "  27 |     await injectSession(page);\n  28 |     await page.goto(BASE_URL);\n> 29 |     await expect(page.locator('#screen-dash.active')).toBeVisible({ timeout: 8000 });\n     |                                                       ^\n  30 |   });\n  31 |\n  32 |   test('manifest.json reachable', async ({ page }) => {"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/printing.spec.js",
                            "column": 55,
                            "line": 29
                          },
                          "message": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mlocator\u001b[39m\u001b[2m).\u001b[22mtoBeVisible\u001b[2m(\u001b[22m\u001b[2m)\u001b[22m failed\n\nLocator: locator('#screen-dash.active')\nExpected: visible\nTimeout: 8000ms\nError: element(s) not found\n\nCall log:\n\u001b[2m  - Expect \"toBeVisible\" with timeout 8000ms\u001b[22m\n\u001b[2m  - waiting for locator('#screen-dash.active')\u001b[22m\n\n\n  27 |     await injectSession(page);\n  28 |     await page.goto(BASE_URL);\n> 29 |     await expect(page.locator('#screen-dash.active')).toBeVisible({ timeout: 8000 });\n     |                                                       ^\n  30 |   });\n  31 |\n  32 |   test('manifest.json reachable', async ({ page }) => {\n    at /var/www/360lm/tests/printing.spec.js:29:55"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 0,
                      "startTime": "2026-07-12T13:22:49.958Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "screenshot",
                          "contentType": "image/png",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-Page-bcc0c-session-→-dashboard-visible-desktop-chrome/test-failed-1.png"
                        },
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-Page-bcc0c-session-→-dashboard-visible-desktop-chrome/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/printing.spec.js",
                        "column": 55,
                        "line": 29
                      }
                    },
                    {
                      "workerIndex": 233,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 8291,
                      "error": {
                        "message": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mlocator\u001b[39m\u001b[2m).\u001b[22mtoBeVisible\u001b[2m(\u001b[22m\u001b[2m)\u001b[22m failed\n\nLocator: locator('#screen-dash.active')\nExpected: visible\nTimeout: 8000ms\nError: element(s) not found\n\nCall log:\n\u001b[2m  - Expect \"toBeVisible\" with timeout 8000ms\u001b[22m\n\u001b[2m  - waiting for locator('#screen-dash.active')\u001b[22m\n",
                        "stack": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mlocator\u001b[39m\u001b[2m).\u001b[22mtoBeVisible\u001b[2m(\u001b[22m\u001b[2m)\u001b[22m failed\n\nLocator: locator('#screen-dash.active')\nExpected: visible\nTimeout: 8000ms\nError: element(s) not found\n\nCall log:\n\u001b[2m  - Expect \"toBeVisible\" with timeout 8000ms\u001b[22m\n\u001b[2m  - waiting for locator('#screen-dash.active')\u001b[22m\n\n    at /var/www/360lm/tests/printing.spec.js:29:55",
                        "location": {
                          "file": "/var/www/360lm/tests/printing.spec.js",
                          "column": 55,
                          "line": 29
                        },
                        "snippet": "  27 |     await injectSession(page);\n  28 |     await page.goto(BASE_URL);\n> 29 |     await expect(page.locator('#screen-dash.active')).toBeVisible({ timeout: 8000 });\n     |                                                       ^\n  30 |   });\n  31 |\n  32 |   test('manifest.json reachable', async ({ page }) => {"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/printing.spec.js",
                            "column": 55,
                            "line": 29
                          },
                          "message": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mlocator\u001b[39m\u001b[2m).\u001b[22mtoBeVisible\u001b[2m(\u001b[22m\u001b[2m)\u001b[22m failed\n\nLocator: locator('#screen-dash.active')\nExpected: visible\nTimeout: 8000ms\nError: element(s) not found\n\nCall log:\n\u001b[2m  - Expect \"toBeVisible\" with timeout 8000ms\u001b[22m\n\u001b[2m  - waiting for locator('#screen-dash.active')\u001b[22m\n\n\n  27 |     await injectSession(page);\n  28 |     await page.goto(BASE_URL);\n> 29 |     await expect(page.locator('#screen-dash.active')).toBeVisible({ timeout: 8000 });\n     |                                                       ^\n  30 |   });\n  31 |\n  32 |   test('manifest.json reachable', async ({ page }) => {\n    at /var/www/360lm/tests/printing.spec.js:29:55"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 1,
                      "startTime": "2026-07-12T13:22:59.450Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "screenshot",
                          "contentType": "image/png",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-Page-bcc0c-session-→-dashboard-visible-desktop-chrome-retry1/test-failed-1.png"
                        },
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-Page-bcc0c-session-→-dashboard-visible-desktop-chrome-retry1/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/printing.spec.js",
                        "column": 55,
                        "line": 29
                      }
                    }
                  ],
                  "status": "unexpected"
                }
              ],
              "id": "84f3bc84c047e9f9c0b8-517b817b11b2ee7ec7ee",
              "file": "printing.spec.js",
              "line": 26,
              "column": 3
            },
            {
              "title": "manifest.json reachable",
              "ok": false,
              "tags": [],
              "tests": [
                {
                  "timeout": 30000,
                  "annotations": [],
                  "expectedStatus": "passed",
                  "projectId": "desktop-chrome",
                  "projectName": "desktop-chrome",
                  "results": [
                    {
                      "workerIndex": 234,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 279,
                      "error": {
                        "message": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoBe\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m) // Object.is equality\u001b[22m\n\nExpected: \u001b[32m200\u001b[39m\nReceived: \u001b[31m404\u001b[39m",
                        "stack": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoBe\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m) // Object.is equality\u001b[22m\n\nExpected: \u001b[32m200\u001b[39m\nReceived: \u001b[31m404\u001b[39m\n    at /var/www/360lm/tests/printing.spec.js:34:24",
                        "location": {
                          "file": "/var/www/360lm/tests/printing.spec.js",
                          "column": 24,
                          "line": 34
                        },
                        "snippet": "  32 |   test('manifest.json reachable', async ({ page }) => {\n  33 |     const r = await page.request.get(BASE_URL + 'manifest.json');\n> 34 |     expect(r.status()).toBe(200);\n     |                        ^\n  35 |     const json = await r.json();\n  36 |     expect(json.name).toContain('Print Nest');\n  37 |     expect(json.start_url).toBe('/printing/');"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/printing.spec.js",
                            "column": 24,
                            "line": 34
                          },
                          "message": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoBe\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m) // Object.is equality\u001b[22m\n\nExpected: \u001b[32m200\u001b[39m\nReceived: \u001b[31m404\u001b[39m\n\n  32 |   test('manifest.json reachable', async ({ page }) => {\n  33 |     const r = await page.request.get(BASE_URL + 'manifest.json');\n> 34 |     expect(r.status()).toBe(200);\n     |                        ^\n  35 |     const json = await r.json();\n  36 |     expect(json.name).toContain('Print Nest');\n  37 |     expect(json.start_url).toBe('/printing/');\n    at /var/www/360lm/tests/printing.spec.js:34:24"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 0,
                      "startTime": "2026-07-12T13:23:08.933Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "screenshot",
                          "contentType": "image/png",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-Page-Load-manifest-json-reachable-desktop-chrome/test-failed-1.png"
                        },
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-Page-Load-manifest-json-reachable-desktop-chrome/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/printing.spec.js",
                        "column": 24,
                        "line": 34
                      }
                    },
                    {
                      "workerIndex": 235,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 326,
                      "error": {
                        "message": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoBe\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m) // Object.is equality\u001b[22m\n\nExpected: \u001b[32m200\u001b[39m\nReceived: \u001b[31m404\u001b[39m",
                        "stack": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoBe\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m) // Object.is equality\u001b[22m\n\nExpected: \u001b[32m200\u001b[39m\nReceived: \u001b[31m404\u001b[39m\n    at /var/www/360lm/tests/printing.spec.js:34:24",
                        "location": {
                          "file": "/var/www/360lm/tests/printing.spec.js",
                          "column": 24,
                          "line": 34
                        },
                        "snippet": "  32 |   test('manifest.json reachable', async ({ page }) => {\n  33 |     const r = await page.request.get(BASE_URL + 'manifest.json');\n> 34 |     expect(r.status()).toBe(200);\n     |                        ^\n  35 |     const json = await r.json();\n  36 |     expect(json.name).toContain('Print Nest');\n  37 |     expect(json.start_url).toBe('/printing/');"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/printing.spec.js",
                            "column": 24,
                            "line": 34
                          },
                          "message": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoBe\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m) // Object.is equality\u001b[22m\n\nExpected: \u001b[32m200\u001b[39m\nReceived: \u001b[31m404\u001b[39m\n\n  32 |   test('manifest.json reachable', async ({ page }) => {\n  33 |     const r = await page.request.get(BASE_URL + 'manifest.json');\n> 34 |     expect(r.status()).toBe(200);\n     |                        ^\n  35 |     const json = await r.json();\n  36 |     expect(json.name).toContain('Print Nest');\n  37 |     expect(json.start_url).toBe('/printing/');\n    at /var/www/360lm/tests/printing.spec.js:34:24"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 1,
                      "startTime": "2026-07-12T13:23:10.381Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "screenshot",
                          "contentType": "image/png",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-Page-Load-manifest-json-reachable-desktop-chrome-retry1/test-failed-1.png"
                        },
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-Page-Load-manifest-json-reachable-desktop-chrome-retry1/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/printing.spec.js",
                        "column": 24,
                        "line": 34
                      }
                    }
                  ],
                  "status": "unexpected"
                }
              ],
              "id": "84f3bc84c047e9f9c0b8-31d2c9edd77ee2f30204",
              "file": "printing.spec.js",
              "line": 32,
              "column": 3
            },
            {
              "title": "service worker registers",
              "ok": false,
              "tags": [],
              "tests": [
                {
                  "timeout": 30000,
                  "annotations": [],
                  "expectedStatus": "passed",
                  "projectId": "desktop-chrome",
                  "projectName": "desktop-chrome",
                  "results": [
                    {
                      "workerIndex": 236,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 8282,
                      "error": {
                        "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n",
                        "stack": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n    at /var/www/360lm/tests/printing.spec.js:43:16",
                        "location": {
                          "file": "/var/www/360lm/tests/printing.spec.js",
                          "column": 16,
                          "line": 43
                        },
                        "snippet": "  41 |     await injectSession(page);\n  42 |     await page.goto(BASE_URL);\n> 43 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n     |                ^\n  44 |     const swRegistered = await page.evaluate(async () => {\n  45 |       const regs = await navigator.serviceWorker.getRegistrations();\n  46 |       return regs.some(r => r.scope.includes('/printing/'));"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/printing.spec.js",
                            "column": 16,
                            "line": 43
                          },
                          "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n\n  41 |     await injectSession(page);\n  42 |     await page.goto(BASE_URL);\n> 43 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n     |                ^\n  44 |     const swRegistered = await page.evaluate(async () => {\n  45 |       const regs = await navigator.serviceWorker.getRegistrations();\n  46 |       return regs.some(r => r.scope.includes('/printing/'));\n    at /var/www/360lm/tests/printing.spec.js:43:16"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 0,
                      "startTime": "2026-07-12T13:23:11.819Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "screenshot",
                          "contentType": "image/png",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-Page-Load-service-worker-registers-desktop-chrome/test-failed-1.png"
                        },
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-Page-Load-service-worker-registers-desktop-chrome/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/printing.spec.js",
                        "column": 16,
                        "line": 43
                      }
                    },
                    {
                      "workerIndex": 237,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 8309,
                      "error": {
                        "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n",
                        "stack": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n    at /var/www/360lm/tests/printing.spec.js:43:16",
                        "location": {
                          "file": "/var/www/360lm/tests/printing.spec.js",
                          "column": 16,
                          "line": 43
                        },
                        "snippet": "  41 |     await injectSession(page);\n  42 |     await page.goto(BASE_URL);\n> 43 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n     |                ^\n  44 |     const swRegistered = await page.evaluate(async () => {\n  45 |       const regs = await navigator.serviceWorker.getRegistrations();\n  46 |       return regs.some(r => r.scope.includes('/printing/'));"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/printing.spec.js",
                            "column": 16,
                            "line": 43
                          },
                          "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n\n  41 |     await injectSession(page);\n  42 |     await page.goto(BASE_URL);\n> 43 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n     |                ^\n  44 |     const swRegistered = await page.evaluate(async () => {\n  45 |       const regs = await navigator.serviceWorker.getRegistrations();\n  46 |       return regs.some(r => r.scope.includes('/printing/'));\n    at /var/www/360lm/tests/printing.spec.js:43:16"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 1,
                      "startTime": "2026-07-12T13:23:21.337Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "screenshot",
                          "contentType": "image/png",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-Page-Load-service-worker-registers-desktop-chrome-retry1/test-failed-1.png"
                        },
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-Page-Load-service-worker-registers-desktop-chrome-retry1/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/printing.spec.js",
                        "column": 16,
                        "line": 43
                      }
                    }
                  ],
                  "status": "unexpected"
                }
              ],
              "id": "84f3bc84c047e9f9c0b8-b63a0ee0f09ecbe80cab",
              "file": "printing.spec.js",
              "line": 40,
              "column": 3
            }
          ]
        },
        {
          "title": "Print Nest — Dashboard",
          "file": "printing.spec.js",
          "line": 52,
          "column": 6,
          "specs": [
            {
              "title": "stat boxes visible",
              "ok": false,
              "tags": [],
              "tests": [
                {
                  "timeout": 30000,
                  "annotations": [],
                  "expectedStatus": "passed",
                  "projectId": "android-chrome",
                  "projectName": "android-chrome",
                  "results": [
                    {
                      "workerIndex": 62,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 8296,
                      "error": {
                        "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n",
                        "stack": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n    at /var/www/360lm/tests/printing.spec.js:57:16",
                        "location": {
                          "file": "/var/www/360lm/tests/printing.spec.js",
                          "column": 16,
                          "line": 57
                        },
                        "snippet": "  55 |   test('stat boxes visible', async ({ page }) => {\n  56 |     await page.goto(BASE_URL);\n> 57 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n     |                ^\n  58 |     await expect(page.locator('#stat-runs')).toBeVisible();\n  59 |     await expect(page.locator('#stat-sqft')).toBeVisible();\n  60 |     await expect(page.locator('#stat-stock')).toBeVisible();"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/printing.spec.js",
                            "column": 16,
                            "line": 57
                          },
                          "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n\n  55 |   test('stat boxes visible', async ({ page }) => {\n  56 |     await page.goto(BASE_URL);\n> 57 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n     |                ^\n  58 |     await expect(page.locator('#stat-runs')).toBeVisible();\n  59 |     await expect(page.locator('#stat-sqft')).toBeVisible();\n  60 |     await expect(page.locator('#stat-stock')).toBeVisible();\n    at /var/www/360lm/tests/printing.spec.js:57:16"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 0,
                      "startTime": "2026-07-12T13:03:12.587Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "screenshot",
                          "contentType": "image/png",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-Dashboard-stat-boxes-visible-android-chrome/test-failed-1.png"
                        },
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-Dashboard-stat-boxes-visible-android-chrome/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/printing.spec.js",
                        "column": 16,
                        "line": 57
                      }
                    },
                    {
                      "workerIndex": 63,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 8253,
                      "error": {
                        "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n",
                        "stack": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n    at /var/www/360lm/tests/printing.spec.js:57:16",
                        "location": {
                          "file": "/var/www/360lm/tests/printing.spec.js",
                          "column": 16,
                          "line": 57
                        },
                        "snippet": "  55 |   test('stat boxes visible', async ({ page }) => {\n  56 |     await page.goto(BASE_URL);\n> 57 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n     |                ^\n  58 |     await expect(page.locator('#stat-runs')).toBeVisible();\n  59 |     await expect(page.locator('#stat-sqft')).toBeVisible();\n  60 |     await expect(page.locator('#stat-stock')).toBeVisible();"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/printing.spec.js",
                            "column": 16,
                            "line": 57
                          },
                          "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n\n  55 |   test('stat boxes visible', async ({ page }) => {\n  56 |     await page.goto(BASE_URL);\n> 57 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n     |                ^\n  58 |     await expect(page.locator('#stat-runs')).toBeVisible();\n  59 |     await expect(page.locator('#stat-sqft')).toBeVisible();\n  60 |     await expect(page.locator('#stat-stock')).toBeVisible();\n    at /var/www/360lm/tests/printing.spec.js:57:16"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 1,
                      "startTime": "2026-07-12T13:03:21.887Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "screenshot",
                          "contentType": "image/png",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-Dashboard-stat-boxes-visible-android-chrome-retry1/test-failed-1.png"
                        },
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-Dashboard-stat-boxes-visible-android-chrome-retry1/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/printing.spec.js",
                        "column": 16,
                        "line": 57
                      }
                    }
                  ],
                  "status": "unexpected"
                }
              ],
              "id": "84f3bc84c047e9f9c0b8-fa405ea4ee770d0637ea",
              "file": "printing.spec.js",
              "line": 55,
              "column": 3
            },
            {
              "title": "user name shown in header",
              "ok": false,
              "tags": [],
              "tests": [
                {
                  "timeout": 30000,
                  "annotations": [],
                  "expectedStatus": "passed",
                  "projectId": "android-chrome",
                  "projectName": "android-chrome",
                  "results": [
                    {
                      "workerIndex": 64,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 8286,
                      "error": {
                        "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n",
                        "stack": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n    at /var/www/360lm/tests/printing.spec.js:65:16",
                        "location": {
                          "file": "/var/www/360lm/tests/printing.spec.js",
                          "column": 16,
                          "line": 65
                        },
                        "snippet": "  63 |   test('user name shown in header', async ({ page }) => {\n  64 |     await page.goto(BASE_URL);\n> 65 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n     |                ^\n  66 |     await expect(page.locator('#dash-user')).toHaveText(/Test Printer/);\n  67 |   });\n  68 |"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/printing.spec.js",
                            "column": 16,
                            "line": 65
                          },
                          "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n\n  63 |   test('user name shown in header', async ({ page }) => {\n  64 |     await page.goto(BASE_URL);\n> 65 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n     |                ^\n  66 |     await expect(page.locator('#dash-user')).toHaveText(/Test Printer/);\n  67 |   });\n  68 |\n    at /var/www/360lm/tests/printing.spec.js:65:16"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 0,
                      "startTime": "2026-07-12T13:03:31.169Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "screenshot",
                          "contentType": "image/png",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-Dashboard-user-name-shown-in-header-android-chrome/test-failed-1.png"
                        },
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-Dashboard-user-name-shown-in-header-android-chrome/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/printing.spec.js",
                        "column": 16,
                        "line": 65
                      }
                    },
                    {
                      "workerIndex": 65,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 8298,
                      "error": {
                        "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n",
                        "stack": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n    at /var/www/360lm/tests/printing.spec.js:65:16",
                        "location": {
                          "file": "/var/www/360lm/tests/printing.spec.js",
                          "column": 16,
                          "line": 65
                        },
                        "snippet": "  63 |   test('user name shown in header', async ({ page }) => {\n  64 |     await page.goto(BASE_URL);\n> 65 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n     |                ^\n  66 |     await expect(page.locator('#dash-user')).toHaveText(/Test Printer/);\n  67 |   });\n  68 |"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/printing.spec.js",
                            "column": 16,
                            "line": 65
                          },
                          "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n\n  63 |   test('user name shown in header', async ({ page }) => {\n  64 |     await page.goto(BASE_URL);\n> 65 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n     |                ^\n  66 |     await expect(page.locator('#dash-user')).toHaveText(/Test Printer/);\n  67 |   });\n  68 |\n    at /var/www/360lm/tests/printing.spec.js:65:16"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 1,
                      "startTime": "2026-07-12T13:03:40.517Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "screenshot",
                          "contentType": "image/png",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-Dashboard-user-name-shown-in-header-android-chrome-retry1/test-failed-1.png"
                        },
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-Dashboard-user-name-shown-in-header-android-chrome-retry1/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/printing.spec.js",
                        "column": 16,
                        "line": 65
                      }
                    }
                  ],
                  "status": "unexpected"
                }
              ],
              "id": "84f3bc84c047e9f9c0b8-e9f3cdfe74b455808006",
              "file": "printing.spec.js",
              "line": 63,
              "column": 3
            },
            {
              "title": "Start New Nesting button navigates to Add screen",
              "ok": false,
              "tags": [],
              "tests": [
                {
                  "timeout": 30000,
                  "annotations": [],
                  "expectedStatus": "passed",
                  "projectId": "android-chrome",
                  "projectName": "android-chrome",
                  "results": [
                    {
                      "workerIndex": 66,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 8239,
                      "error": {
                        "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n",
                        "stack": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n    at /var/www/360lm/tests/printing.spec.js:71:16",
                        "location": {
                          "file": "/var/www/360lm/tests/printing.spec.js",
                          "column": 16,
                          "line": 71
                        },
                        "snippet": "  69 |   test('Start New Nesting button navigates to Add screen', async ({ page }) => {\n  70 |     await page.goto(BASE_URL);\n> 71 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n     |                ^\n  72 |     await page.locator('text=Start New Nesting').click();\n  73 |     await expect(page.locator('#screen-add.active')).toBeVisible({ timeout: 4000 });\n  74 |   });"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/printing.spec.js",
                            "column": 16,
                            "line": 71
                          },
                          "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n\n  69 |   test('Start New Nesting button navigates to Add screen', async ({ page }) => {\n  70 |     await page.goto(BASE_URL);\n> 71 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n     |                ^\n  72 |     await page.locator('text=Start New Nesting').click();\n  73 |     await expect(page.locator('#screen-add.active')).toBeVisible({ timeout: 4000 });\n  74 |   });\n    at /var/www/360lm/tests/printing.spec.js:71:16"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 0,
                      "startTime": "2026-07-12T13:03:49.864Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "screenshot",
                          "contentType": "image/png",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-Dash-b1c8b-ton-navigates-to-Add-screen-android-chrome/test-failed-1.png"
                        },
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-Dash-b1c8b-ton-navigates-to-Add-screen-android-chrome/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/printing.spec.js",
                        "column": 16,
                        "line": 71
                      }
                    },
                    {
                      "workerIndex": 67,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 8247,
                      "error": {
                        "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n",
                        "stack": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n    at /var/www/360lm/tests/printing.spec.js:71:16",
                        "location": {
                          "file": "/var/www/360lm/tests/printing.spec.js",
                          "column": 16,
                          "line": 71
                        },
                        "snippet": "  69 |   test('Start New Nesting button navigates to Add screen', async ({ page }) => {\n  70 |     await page.goto(BASE_URL);\n> 71 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n     |                ^\n  72 |     await page.locator('text=Start New Nesting').click();\n  73 |     await expect(page.locator('#screen-add.active')).toBeVisible({ timeout: 4000 });\n  74 |   });"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/printing.spec.js",
                            "column": 16,
                            "line": 71
                          },
                          "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n\n  69 |   test('Start New Nesting button navigates to Add screen', async ({ page }) => {\n  70 |     await page.goto(BASE_URL);\n> 71 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n     |                ^\n  72 |     await page.locator('text=Start New Nesting').click();\n  73 |     await expect(page.locator('#screen-add.active')).toBeVisible({ timeout: 4000 });\n  74 |   });\n    at /var/www/360lm/tests/printing.spec.js:71:16"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 1,
                      "startTime": "2026-07-12T13:03:59.147Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "screenshot",
                          "contentType": "image/png",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-Dash-b1c8b-ton-navigates-to-Add-screen-android-chrome-retry1/test-failed-1.png"
                        },
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-Dash-b1c8b-ton-navigates-to-Add-screen-android-chrome-retry1/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/printing.spec.js",
                        "column": 16,
                        "line": 71
                      }
                    }
                  ],
                  "status": "unexpected"
                }
              ],
              "id": "84f3bc84c047e9f9c0b8-47f5bb5b71ebdce00a51",
              "file": "printing.spec.js",
              "line": 69,
              "column": 3
            },
            {
              "title": "bottom nav Runs tab loads runs screen",
              "ok": false,
              "tags": [],
              "tests": [
                {
                  "timeout": 30000,
                  "annotations": [],
                  "expectedStatus": "passed",
                  "projectId": "android-chrome",
                  "projectName": "android-chrome",
                  "results": [
                    {
                      "workerIndex": 68,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 8280,
                      "error": {
                        "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n",
                        "stack": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n    at /var/www/360lm/tests/printing.spec.js:78:16",
                        "location": {
                          "file": "/var/www/360lm/tests/printing.spec.js",
                          "column": 16,
                          "line": 78
                        },
                        "snippet": "  76 |   test('bottom nav Runs tab loads runs screen', async ({ page }) => {\n  77 |     await page.goto(BASE_URL);\n> 78 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n     |                ^\n  79 |     await page.locator('#screen-dash .nav-btn', { hasText: 'Runs' }).click();\n  80 |     await expect(page.locator('#screen-runs.active')).toBeVisible({ timeout: 4000 });\n  81 |   });"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/printing.spec.js",
                            "column": 16,
                            "line": 78
                          },
                          "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n\n  76 |   test('bottom nav Runs tab loads runs screen', async ({ page }) => {\n  77 |     await page.goto(BASE_URL);\n> 78 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n     |                ^\n  79 |     await page.locator('#screen-dash .nav-btn', { hasText: 'Runs' }).click();\n  80 |     await expect(page.locator('#screen-runs.active')).toBeVisible({ timeout: 4000 });\n  81 |   });\n    at /var/www/360lm/tests/printing.spec.js:78:16"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 0,
                      "startTime": "2026-07-12T13:04:08.462Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "screenshot",
                          "contentType": "image/png",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-Dash-9c7d4--Runs-tab-loads-runs-screen-android-chrome/test-failed-1.png"
                        },
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-Dash-9c7d4--Runs-tab-loads-runs-screen-android-chrome/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/printing.spec.js",
                        "column": 16,
                        "line": 78
                      }
                    },
                    {
                      "workerIndex": 69,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 8261,
                      "error": {
                        "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n",
                        "stack": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n    at /var/www/360lm/tests/printing.spec.js:78:16",
                        "location": {
                          "file": "/var/www/360lm/tests/printing.spec.js",
                          "column": 16,
                          "line": 78
                        },
                        "snippet": "  76 |   test('bottom nav Runs tab loads runs screen', async ({ page }) => {\n  77 |     await page.goto(BASE_URL);\n> 78 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n     |                ^\n  79 |     await page.locator('#screen-dash .nav-btn', { hasText: 'Runs' }).click();\n  80 |     await expect(page.locator('#screen-runs.active')).toBeVisible({ timeout: 4000 });\n  81 |   });"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/printing.spec.js",
                            "column": 16,
                            "line": 78
                          },
                          "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n\n  76 |   test('bottom nav Runs tab loads runs screen', async ({ page }) => {\n  77 |     await page.goto(BASE_URL);\n> 78 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n     |                ^\n  79 |     await page.locator('#screen-dash .nav-btn', { hasText: 'Runs' }).click();\n  80 |     await expect(page.locator('#screen-runs.active')).toBeVisible({ timeout: 4000 });\n  81 |   });\n    at /var/www/360lm/tests/printing.spec.js:78:16"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 1,
                      "startTime": "2026-07-12T13:04:17.895Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "screenshot",
                          "contentType": "image/png",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-Dash-9c7d4--Runs-tab-loads-runs-screen-android-chrome-retry1/test-failed-1.png"
                        },
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-Dash-9c7d4--Runs-tab-loads-runs-screen-android-chrome-retry1/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/printing.spec.js",
                        "column": 16,
                        "line": 78
                      }
                    }
                  ],
                  "status": "unexpected"
                }
              ],
              "id": "84f3bc84c047e9f9c0b8-7a666b0a97a452261553",
              "file": "printing.spec.js",
              "line": 76,
              "column": 3
            },
            {
              "title": "bottom nav Stock tab loads stock screen",
              "ok": false,
              "tags": [],
              "tests": [
                {
                  "timeout": 30000,
                  "annotations": [],
                  "expectedStatus": "passed",
                  "projectId": "android-chrome",
                  "projectName": "android-chrome",
                  "results": [
                    {
                      "workerIndex": 70,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 8260,
                      "error": {
                        "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n",
                        "stack": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n    at /var/www/360lm/tests/printing.spec.js:85:16",
                        "location": {
                          "file": "/var/www/360lm/tests/printing.spec.js",
                          "column": 16,
                          "line": 85
                        },
                        "snippet": "  83 |   test('bottom nav Stock tab loads stock screen', async ({ page }) => {\n  84 |     await page.goto(BASE_URL);\n> 85 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n     |                ^\n  86 |     await page.locator('#screen-dash .nav-btn', { hasText: 'Stock' }).click();\n  87 |     await expect(page.locator('#screen-stock.active')).toBeVisible({ timeout: 4000 });\n  88 |   });"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/printing.spec.js",
                            "column": 16,
                            "line": 85
                          },
                          "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n\n  83 |   test('bottom nav Stock tab loads stock screen', async ({ page }) => {\n  84 |     await page.goto(BASE_URL);\n> 85 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n     |                ^\n  86 |     await page.locator('#screen-dash .nav-btn', { hasText: 'Stock' }).click();\n  87 |     await expect(page.locator('#screen-stock.active')).toBeVisible({ timeout: 4000 });\n  88 |   });\n    at /var/www/360lm/tests/printing.spec.js:85:16"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 0,
                      "startTime": "2026-07-12T13:04:27.157Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "screenshot",
                          "contentType": "image/png",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-Dash-bd6d4-tock-tab-loads-stock-screen-android-chrome/test-failed-1.png"
                        },
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-Dash-bd6d4-tock-tab-loads-stock-screen-android-chrome/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/printing.spec.js",
                        "column": 16,
                        "line": 85
                      }
                    },
                    {
                      "workerIndex": 71,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 8279,
                      "error": {
                        "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n",
                        "stack": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n    at /var/www/360lm/tests/printing.spec.js:85:16",
                        "location": {
                          "file": "/var/www/360lm/tests/printing.spec.js",
                          "column": 16,
                          "line": 85
                        },
                        "snippet": "  83 |   test('bottom nav Stock tab loads stock screen', async ({ page }) => {\n  84 |     await page.goto(BASE_URL);\n> 85 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n     |                ^\n  86 |     await page.locator('#screen-dash .nav-btn', { hasText: 'Stock' }).click();\n  87 |     await expect(page.locator('#screen-stock.active')).toBeVisible({ timeout: 4000 });\n  88 |   });"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/printing.spec.js",
                            "column": 16,
                            "line": 85
                          },
                          "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n\n  83 |   test('bottom nav Stock tab loads stock screen', async ({ page }) => {\n  84 |     await page.goto(BASE_URL);\n> 85 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n     |                ^\n  86 |     await page.locator('#screen-dash .nav-btn', { hasText: 'Stock' }).click();\n  87 |     await expect(page.locator('#screen-stock.active')).toBeVisible({ timeout: 4000 });\n  88 |   });\n    at /var/www/360lm/tests/printing.spec.js:85:16"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 1,
                      "startTime": "2026-07-12T13:04:36.390Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "screenshot",
                          "contentType": "image/png",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-Dash-bd6d4-tock-tab-loads-stock-screen-android-chrome-retry1/test-failed-1.png"
                        },
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-Dash-bd6d4-tock-tab-loads-stock-screen-android-chrome-retry1/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/printing.spec.js",
                        "column": 16,
                        "line": 85
                      }
                    }
                  ],
                  "status": "unexpected"
                }
              ],
              "id": "84f3bc84c047e9f9c0b8-cc32a17fcdcd303b5530",
              "file": "printing.spec.js",
              "line": 83,
              "column": 3
            },
            {
              "title": "stat boxes visible",
              "ok": false,
              "tags": [],
              "tests": [
                {
                  "timeout": 30000,
                  "annotations": [],
                  "expectedStatus": "passed",
                  "projectId": "desktop-chrome",
                  "projectName": "desktop-chrome",
                  "results": [
                    {
                      "workerIndex": 238,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 8268,
                      "error": {
                        "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n",
                        "stack": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n    at /var/www/360lm/tests/printing.spec.js:57:16",
                        "location": {
                          "file": "/var/www/360lm/tests/printing.spec.js",
                          "column": 16,
                          "line": 57
                        },
                        "snippet": "  55 |   test('stat boxes visible', async ({ page }) => {\n  56 |     await page.goto(BASE_URL);\n> 57 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n     |                ^\n  58 |     await expect(page.locator('#stat-runs')).toBeVisible();\n  59 |     await expect(page.locator('#stat-sqft')).toBeVisible();\n  60 |     await expect(page.locator('#stat-stock')).toBeVisible();"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/printing.spec.js",
                            "column": 16,
                            "line": 57
                          },
                          "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n\n  55 |   test('stat boxes visible', async ({ page }) => {\n  56 |     await page.goto(BASE_URL);\n> 57 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n     |                ^\n  58 |     await expect(page.locator('#stat-runs')).toBeVisible();\n  59 |     await expect(page.locator('#stat-sqft')).toBeVisible();\n  60 |     await expect(page.locator('#stat-stock')).toBeVisible();\n    at /var/www/360lm/tests/printing.spec.js:57:16"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 0,
                      "startTime": "2026-07-12T13:23:30.892Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "screenshot",
                          "contentType": "image/png",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-Dashboard-stat-boxes-visible-desktop-chrome/test-failed-1.png"
                        },
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-Dashboard-stat-boxes-visible-desktop-chrome/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/printing.spec.js",
                        "column": 16,
                        "line": 57
                      }
                    },
                    {
                      "workerIndex": 239,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 8320,
                      "error": {
                        "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n",
                        "stack": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n    at /var/www/360lm/tests/printing.spec.js:57:16",
                        "location": {
                          "file": "/var/www/360lm/tests/printing.spec.js",
                          "column": 16,
                          "line": 57
                        },
                        "snippet": "  55 |   test('stat boxes visible', async ({ page }) => {\n  56 |     await page.goto(BASE_URL);\n> 57 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n     |                ^\n  58 |     await expect(page.locator('#stat-runs')).toBeVisible();\n  59 |     await expect(page.locator('#stat-sqft')).toBeVisible();\n  60 |     await expect(page.locator('#stat-stock')).toBeVisible();"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/printing.spec.js",
                            "column": 16,
                            "line": 57
                          },
                          "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n\n  55 |   test('stat boxes visible', async ({ page }) => {\n  56 |     await page.goto(BASE_URL);\n> 57 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n     |                ^\n  58 |     await expect(page.locator('#stat-runs')).toBeVisible();\n  59 |     await expect(page.locator('#stat-sqft')).toBeVisible();\n  60 |     await expect(page.locator('#stat-stock')).toBeVisible();\n    at /var/www/360lm/tests/printing.spec.js:57:16"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 1,
                      "startTime": "2026-07-12T13:23:40.347Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "screenshot",
                          "contentType": "image/png",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-Dashboard-stat-boxes-visible-desktop-chrome-retry1/test-failed-1.png"
                        },
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-Dashboard-stat-boxes-visible-desktop-chrome-retry1/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/printing.spec.js",
                        "column": 16,
                        "line": 57
                      }
                    }
                  ],
                  "status": "unexpected"
                }
              ],
              "id": "84f3bc84c047e9f9c0b8-e1dba80098461a38f898",
              "file": "printing.spec.js",
              "line": 55,
              "column": 3
            },
            {
              "title": "user name shown in header",
              "ok": false,
              "tags": [],
              "tests": [
                {
                  "timeout": 30000,
                  "annotations": [],
                  "expectedStatus": "passed",
                  "projectId": "desktop-chrome",
                  "projectName": "desktop-chrome",
                  "results": [
                    {
                      "workerIndex": 240,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 8370,
                      "error": {
                        "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n",
                        "stack": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n    at /var/www/360lm/tests/printing.spec.js:65:16",
                        "location": {
                          "file": "/var/www/360lm/tests/printing.spec.js",
                          "column": 16,
                          "line": 65
                        },
                        "snippet": "  63 |   test('user name shown in header', async ({ page }) => {\n  64 |     await page.goto(BASE_URL);\n> 65 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n     |                ^\n  66 |     await expect(page.locator('#dash-user')).toHaveText(/Test Printer/);\n  67 |   });\n  68 |"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/printing.spec.js",
                            "column": 16,
                            "line": 65
                          },
                          "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n\n  63 |   test('user name shown in header', async ({ page }) => {\n  64 |     await page.goto(BASE_URL);\n> 65 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n     |                ^\n  66 |     await expect(page.locator('#dash-user')).toHaveText(/Test Printer/);\n  67 |   });\n  68 |\n    at /var/www/360lm/tests/printing.spec.js:65:16"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 0,
                      "startTime": "2026-07-12T13:23:49.826Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "screenshot",
                          "contentType": "image/png",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-Dashboard-user-name-shown-in-header-desktop-chrome/test-failed-1.png"
                        },
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-Dashboard-user-name-shown-in-header-desktop-chrome/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/printing.spec.js",
                        "column": 16,
                        "line": 65
                      }
                    },
                    {
                      "workerIndex": 241,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 8265,
                      "error": {
                        "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n",
                        "stack": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n    at /var/www/360lm/tests/printing.spec.js:65:16",
                        "location": {
                          "file": "/var/www/360lm/tests/printing.spec.js",
                          "column": 16,
                          "line": 65
                        },
                        "snippet": "  63 |   test('user name shown in header', async ({ page }) => {\n  64 |     await page.goto(BASE_URL);\n> 65 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n     |                ^\n  66 |     await expect(page.locator('#dash-user')).toHaveText(/Test Printer/);\n  67 |   });\n  68 |"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/printing.spec.js",
                            "column": 16,
                            "line": 65
                          },
                          "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n\n  63 |   test('user name shown in header', async ({ page }) => {\n  64 |     await page.goto(BASE_URL);\n> 65 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n     |                ^\n  66 |     await expect(page.locator('#dash-user')).toHaveText(/Test Printer/);\n  67 |   });\n  68 |\n    at /var/www/360lm/tests/printing.spec.js:65:16"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 1,
                      "startTime": "2026-07-12T13:23:59.403Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "screenshot",
                          "contentType": "image/png",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-Dashboard-user-name-shown-in-header-desktop-chrome-retry1/test-failed-1.png"
                        },
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-Dashboard-user-name-shown-in-header-desktop-chrome-retry1/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/printing.spec.js",
                        "column": 16,
                        "line": 65
                      }
                    }
                  ],
                  "status": "unexpected"
                }
              ],
              "id": "84f3bc84c047e9f9c0b8-9ca22474619662f7c0bc",
              "file": "printing.spec.js",
              "line": 63,
              "column": 3
            },
            {
              "title": "Start New Nesting button navigates to Add screen",
              "ok": false,
              "tags": [],
              "tests": [
                {
                  "timeout": 30000,
                  "annotations": [],
                  "expectedStatus": "passed",
                  "projectId": "desktop-chrome",
                  "projectName": "desktop-chrome",
                  "results": [
                    {
                      "workerIndex": 242,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 8305,
                      "error": {
                        "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n",
                        "stack": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n    at /var/www/360lm/tests/printing.spec.js:71:16",
                        "location": {
                          "file": "/var/www/360lm/tests/printing.spec.js",
                          "column": 16,
                          "line": 71
                        },
                        "snippet": "  69 |   test('Start New Nesting button navigates to Add screen', async ({ page }) => {\n  70 |     await page.goto(BASE_URL);\n> 71 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n     |                ^\n  72 |     await page.locator('text=Start New Nesting').click();\n  73 |     await expect(page.locator('#screen-add.active')).toBeVisible({ timeout: 4000 });\n  74 |   });"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/printing.spec.js",
                            "column": 16,
                            "line": 71
                          },
                          "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n\n  69 |   test('Start New Nesting button navigates to Add screen', async ({ page }) => {\n  70 |     await page.goto(BASE_URL);\n> 71 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n     |                ^\n  72 |     await page.locator('text=Start New Nesting').click();\n  73 |     await expect(page.locator('#screen-add.active')).toBeVisible({ timeout: 4000 });\n  74 |   });\n    at /var/www/360lm/tests/printing.spec.js:71:16"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 0,
                      "startTime": "2026-07-12T13:24:08.749Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "screenshot",
                          "contentType": "image/png",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-Dash-b1c8b-ton-navigates-to-Add-screen-desktop-chrome/test-failed-1.png"
                        },
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-Dash-b1c8b-ton-navigates-to-Add-screen-desktop-chrome/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/printing.spec.js",
                        "column": 16,
                        "line": 71
                      }
                    },
                    {
                      "workerIndex": 243,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 8250,
                      "error": {
                        "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n",
                        "stack": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n    at /var/www/360lm/tests/printing.spec.js:71:16",
                        "location": {
                          "file": "/var/www/360lm/tests/printing.spec.js",
                          "column": 16,
                          "line": 71
                        },
                        "snippet": "  69 |   test('Start New Nesting button navigates to Add screen', async ({ page }) => {\n  70 |     await page.goto(BASE_URL);\n> 71 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n     |                ^\n  72 |     await page.locator('text=Start New Nesting').click();\n  73 |     await expect(page.locator('#screen-add.active')).toBeVisible({ timeout: 4000 });\n  74 |   });"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/printing.spec.js",
                            "column": 16,
                            "line": 71
                          },
                          "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n\n  69 |   test('Start New Nesting button navigates to Add screen', async ({ page }) => {\n  70 |     await page.goto(BASE_URL);\n> 71 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n     |                ^\n  72 |     await page.locator('text=Start New Nesting').click();\n  73 |     await expect(page.locator('#screen-add.active')).toBeVisible({ timeout: 4000 });\n  74 |   });\n    at /var/www/360lm/tests/printing.spec.js:71:16"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 1,
                      "startTime": "2026-07-12T13:24:18.087Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "screenshot",
                          "contentType": "image/png",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-Dash-b1c8b-ton-navigates-to-Add-screen-desktop-chrome-retry1/test-failed-1.png"
                        },
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-Dash-b1c8b-ton-navigates-to-Add-screen-desktop-chrome-retry1/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/printing.spec.js",
                        "column": 16,
                        "line": 71
                      }
                    }
                  ],
                  "status": "unexpected"
                }
              ],
              "id": "84f3bc84c047e9f9c0b8-44fb3cd490b0d388c929",
              "file": "printing.spec.js",
              "line": 69,
              "column": 3
            },
            {
              "title": "bottom nav Runs tab loads runs screen",
              "ok": false,
              "tags": [],
              "tests": [
                {
                  "timeout": 30000,
                  "annotations": [],
                  "expectedStatus": "passed",
                  "projectId": "desktop-chrome",
                  "projectName": "desktop-chrome",
                  "results": [
                    {
                      "workerIndex": 244,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 8234,
                      "error": {
                        "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n",
                        "stack": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n    at /var/www/360lm/tests/printing.spec.js:78:16",
                        "location": {
                          "file": "/var/www/360lm/tests/printing.spec.js",
                          "column": 16,
                          "line": 78
                        },
                        "snippet": "  76 |   test('bottom nav Runs tab loads runs screen', async ({ page }) => {\n  77 |     await page.goto(BASE_URL);\n> 78 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n     |                ^\n  79 |     await page.locator('#screen-dash .nav-btn', { hasText: 'Runs' }).click();\n  80 |     await expect(page.locator('#screen-runs.active')).toBeVisible({ timeout: 4000 });\n  81 |   });"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/printing.spec.js",
                            "column": 16,
                            "line": 78
                          },
                          "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n\n  76 |   test('bottom nav Runs tab loads runs screen', async ({ page }) => {\n  77 |     await page.goto(BASE_URL);\n> 78 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n     |                ^\n  79 |     await page.locator('#screen-dash .nav-btn', { hasText: 'Runs' }).click();\n  80 |     await expect(page.locator('#screen-runs.active')).toBeVisible({ timeout: 4000 });\n  81 |   });\n    at /var/www/360lm/tests/printing.spec.js:78:16"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 0,
                      "startTime": "2026-07-12T13:24:27.471Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "screenshot",
                          "contentType": "image/png",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-Dash-9c7d4--Runs-tab-loads-runs-screen-desktop-chrome/test-failed-1.png"
                        },
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-Dash-9c7d4--Runs-tab-loads-runs-screen-desktop-chrome/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/printing.spec.js",
                        "column": 16,
                        "line": 78
                      }
                    },
                    {
                      "workerIndex": 245,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 8278,
                      "error": {
                        "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n",
                        "stack": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n    at /var/www/360lm/tests/printing.spec.js:78:16",
                        "location": {
                          "file": "/var/www/360lm/tests/printing.spec.js",
                          "column": 16,
                          "line": 78
                        },
                        "snippet": "  76 |   test('bottom nav Runs tab loads runs screen', async ({ page }) => {\n  77 |     await page.goto(BASE_URL);\n> 78 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n     |                ^\n  79 |     await page.locator('#screen-dash .nav-btn', { hasText: 'Runs' }).click();\n  80 |     await expect(page.locator('#screen-runs.active')).toBeVisible({ timeout: 4000 });\n  81 |   });"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/printing.spec.js",
                            "column": 16,
                            "line": 78
                          },
                          "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n\n  76 |   test('bottom nav Runs tab loads runs screen', async ({ page }) => {\n  77 |     await page.goto(BASE_URL);\n> 78 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n     |                ^\n  79 |     await page.locator('#screen-dash .nav-btn', { hasText: 'Runs' }).click();\n  80 |     await expect(page.locator('#screen-runs.active')).toBeVisible({ timeout: 4000 });\n  81 |   });\n    at /var/www/360lm/tests/printing.spec.js:78:16"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 1,
                      "startTime": "2026-07-12T13:24:36.718Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "screenshot",
                          "contentType": "image/png",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-Dash-9c7d4--Runs-tab-loads-runs-screen-desktop-chrome-retry1/test-failed-1.png"
                        },
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-Dash-9c7d4--Runs-tab-loads-runs-screen-desktop-chrome-retry1/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/printing.spec.js",
                        "column": 16,
                        "line": 78
                      }
                    }
                  ],
                  "status": "unexpected"
                }
              ],
              "id": "84f3bc84c047e9f9c0b8-d4ac55b6502fc643652d",
              "file": "printing.spec.js",
              "line": 76,
              "column": 3
            },
            {
              "title": "bottom nav Stock tab loads stock screen",
              "ok": false,
              "tags": [],
              "tests": [
                {
                  "timeout": 30000,
                  "annotations": [],
                  "expectedStatus": "passed",
                  "projectId": "desktop-chrome",
                  "projectName": "desktop-chrome",
                  "results": [
                    {
                      "workerIndex": 246,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 8252,
                      "error": {
                        "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n",
                        "stack": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n    at /var/www/360lm/tests/printing.spec.js:85:16",
                        "location": {
                          "file": "/var/www/360lm/tests/printing.spec.js",
                          "column": 16,
                          "line": 85
                        },
                        "snippet": "  83 |   test('bottom nav Stock tab loads stock screen', async ({ page }) => {\n  84 |     await page.goto(BASE_URL);\n> 85 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n     |                ^\n  86 |     await page.locator('#screen-dash .nav-btn', { hasText: 'Stock' }).click();\n  87 |     await expect(page.locator('#screen-stock.active')).toBeVisible({ timeout: 4000 });\n  88 |   });"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/printing.spec.js",
                            "column": 16,
                            "line": 85
                          },
                          "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n\n  83 |   test('bottom nav Stock tab loads stock screen', async ({ page }) => {\n  84 |     await page.goto(BASE_URL);\n> 85 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n     |                ^\n  86 |     await page.locator('#screen-dash .nav-btn', { hasText: 'Stock' }).click();\n  87 |     await expect(page.locator('#screen-stock.active')).toBeVisible({ timeout: 4000 });\n  88 |   });\n    at /var/www/360lm/tests/printing.spec.js:85:16"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 0,
                      "startTime": "2026-07-12T13:24:46.061Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "screenshot",
                          "contentType": "image/png",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-Dash-bd6d4-tock-tab-loads-stock-screen-desktop-chrome/test-failed-1.png"
                        },
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-Dash-bd6d4-tock-tab-loads-stock-screen-desktop-chrome/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/printing.spec.js",
                        "column": 16,
                        "line": 85
                      }
                    },
                    {
                      "workerIndex": 247,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 8399,
                      "error": {
                        "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n",
                        "stack": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n    at /var/www/360lm/tests/printing.spec.js:85:16",
                        "location": {
                          "file": "/var/www/360lm/tests/printing.spec.js",
                          "column": 16,
                          "line": 85
                        },
                        "snippet": "  83 |   test('bottom nav Stock tab loads stock screen', async ({ page }) => {\n  84 |     await page.goto(BASE_URL);\n> 85 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n     |                ^\n  86 |     await page.locator('#screen-dash .nav-btn', { hasText: 'Stock' }).click();\n  87 |     await expect(page.locator('#screen-stock.active')).toBeVisible({ timeout: 4000 });\n  88 |   });"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/printing.spec.js",
                            "column": 16,
                            "line": 85
                          },
                          "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n\n  83 |   test('bottom nav Stock tab loads stock screen', async ({ page }) => {\n  84 |     await page.goto(BASE_URL);\n> 85 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n     |                ^\n  86 |     await page.locator('#screen-dash .nav-btn', { hasText: 'Stock' }).click();\n  87 |     await expect(page.locator('#screen-stock.active')).toBeVisible({ timeout: 4000 });\n  88 |   });\n    at /var/www/360lm/tests/printing.spec.js:85:16"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 1,
                      "startTime": "2026-07-12T13:24:55.306Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "screenshot",
                          "contentType": "image/png",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-Dash-bd6d4-tock-tab-loads-stock-screen-desktop-chrome-retry1/test-failed-1.png"
                        },
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-Dash-bd6d4-tock-tab-loads-stock-screen-desktop-chrome-retry1/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/printing.spec.js",
                        "column": 16,
                        "line": 85
                      }
                    }
                  ],
                  "status": "unexpected"
                }
              ],
              "id": "84f3bc84c047e9f9c0b8-1a95200c08fd862f955b",
              "file": "printing.spec.js",
              "line": 83,
              "column": 3
            }
          ]
        },
        {
          "title": "Print Nest — Add Items (Manual)",
          "file": "printing.spec.js",
          "line": 91,
          "column": 6,
          "specs": [
            {
              "title": "Compute button disabled with no items",
              "ok": false,
              "tags": [],
              "tests": [
                {
                  "timeout": 30000,
                  "annotations": [],
                  "expectedStatus": "passed",
                  "projectId": "android-chrome",
                  "projectName": "android-chrome",
                  "results": [
                    {
                      "workerIndex": 72,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 8259,
                      "error": {
                        "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n",
                        "stack": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n    at /var/www/360lm/tests/printing.spec.js:95:16",
                        "location": {
                          "file": "/var/www/360lm/tests/printing.spec.js",
                          "column": 16,
                          "line": 95
                        },
                        "snippet": "  93 |     await injectSession(page);\n  94 |     await page.goto(BASE_URL);\n> 95 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n     |                ^\n  96 |     await page.locator('text=Start New Nesting').click();\n  97 |     await page.waitForSelector('#screen-add.active', { timeout: 4000 });\n  98 |     await page.locator('#tab-m-btn').click();"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/printing.spec.js",
                            "column": 16,
                            "line": 95
                          },
                          "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n\n  93 |     await injectSession(page);\n  94 |     await page.goto(BASE_URL);\n> 95 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n     |                ^\n  96 |     await page.locator('text=Start New Nesting').click();\n  97 |     await page.waitForSelector('#screen-add.active', { timeout: 4000 });\n  98 |     await page.locator('#tab-m-btn').click();\n    at /var/www/360lm/tests/printing.spec.js:95:16"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 0,
                      "startTime": "2026-07-12T13:04:45.747Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "screenshot",
                          "contentType": "image/png",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-Add--923a8-tton-disabled-with-no-items-android-chrome/test-failed-1.png"
                        },
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-Add--923a8-tton-disabled-with-no-items-android-chrome/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/printing.spec.js",
                        "column": 16,
                        "line": 95
                      }
                    },
                    {
                      "workerIndex": 73,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 8371,
                      "error": {
                        "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n",
                        "stack": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n    at /var/www/360lm/tests/printing.spec.js:95:16",
                        "location": {
                          "file": "/var/www/360lm/tests/printing.spec.js",
                          "column": 16,
                          "line": 95
                        },
                        "snippet": "  93 |     await injectSession(page);\n  94 |     await page.goto(BASE_URL);\n> 95 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n     |                ^\n  96 |     await page.locator('text=Start New Nesting').click();\n  97 |     await page.waitForSelector('#screen-add.active', { timeout: 4000 });\n  98 |     await page.locator('#tab-m-btn').click();"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/printing.spec.js",
                            "column": 16,
                            "line": 95
                          },
                          "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n\n  93 |     await injectSession(page);\n  94 |     await page.goto(BASE_URL);\n> 95 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n     |                ^\n  96 |     await page.locator('text=Start New Nesting').click();\n  97 |     await page.waitForSelector('#screen-add.active', { timeout: 4000 });\n  98 |     await page.locator('#tab-m-btn').click();\n    at /var/www/360lm/tests/printing.spec.js:95:16"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 1,
                      "startTime": "2026-07-12T13:04:55.006Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "screenshot",
                          "contentType": "image/png",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-Add--923a8-tton-disabled-with-no-items-android-chrome-retry1/test-failed-1.png"
                        },
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-Add--923a8-tton-disabled-with-no-items-android-chrome-retry1/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/printing.spec.js",
                        "column": 16,
                        "line": 95
                      }
                    }
                  ],
                  "status": "unexpected"
                }
              ],
              "id": "84f3bc84c047e9f9c0b8-e179d9dfe158529b0fb2",
              "file": "printing.spec.js",
              "line": 102,
              "column": 3
            },
            {
              "title": "empty form shows validation toast",
              "ok": false,
              "tags": [],
              "tests": [
                {
                  "timeout": 30000,
                  "annotations": [],
                  "expectedStatus": "passed",
                  "projectId": "android-chrome",
                  "projectName": "android-chrome",
                  "results": [
                    {
                      "workerIndex": 74,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 8326,
                      "error": {
                        "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n",
                        "stack": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n    at /var/www/360lm/tests/printing.spec.js:95:16",
                        "location": {
                          "file": "/var/www/360lm/tests/printing.spec.js",
                          "column": 16,
                          "line": 95
                        },
                        "snippet": "  93 |     await injectSession(page);\n  94 |     await page.goto(BASE_URL);\n> 95 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n     |                ^\n  96 |     await page.locator('text=Start New Nesting').click();\n  97 |     await page.waitForSelector('#screen-add.active', { timeout: 4000 });\n  98 |     await page.locator('#tab-m-btn').click();"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/printing.spec.js",
                            "column": 16,
                            "line": 95
                          },
                          "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n\n  93 |     await injectSession(page);\n  94 |     await page.goto(BASE_URL);\n> 95 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n     |                ^\n  96 |     await page.locator('text=Start New Nesting').click();\n  97 |     await page.waitForSelector('#screen-add.active', { timeout: 4000 });\n  98 |     await page.locator('#tab-m-btn').click();\n    at /var/www/360lm/tests/printing.spec.js:95:16"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 0,
                      "startTime": "2026-07-12T13:05:04.942Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "screenshot",
                          "contentType": "image/png",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-Add--968cc-form-shows-validation-toast-android-chrome/test-failed-1.png"
                        },
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-Add--968cc-form-shows-validation-toast-android-chrome/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/printing.spec.js",
                        "column": 16,
                        "line": 95
                      }
                    },
                    {
                      "workerIndex": 75,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 8252,
                      "error": {
                        "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n",
                        "stack": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n    at /var/www/360lm/tests/printing.spec.js:95:16",
                        "location": {
                          "file": "/var/www/360lm/tests/printing.spec.js",
                          "column": 16,
                          "line": 95
                        },
                        "snippet": "  93 |     await injectSession(page);\n  94 |     await page.goto(BASE_URL);\n> 95 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n     |                ^\n  96 |     await page.locator('text=Start New Nesting').click();\n  97 |     await page.waitForSelector('#screen-add.active', { timeout: 4000 });\n  98 |     await page.locator('#tab-m-btn').click();"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/printing.spec.js",
                            "column": 16,
                            "line": 95
                          },
                          "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n\n  93 |     await injectSession(page);\n  94 |     await page.goto(BASE_URL);\n> 95 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n     |                ^\n  96 |     await page.locator('text=Start New Nesting').click();\n  97 |     await page.waitForSelector('#screen-add.active', { timeout: 4000 });\n  98 |     await page.locator('#tab-m-btn').click();\n    at /var/www/360lm/tests/printing.spec.js:95:16"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 1,
                      "startTime": "2026-07-12T13:05:14.324Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "screenshot",
                          "contentType": "image/png",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-Add--968cc-form-shows-validation-toast-android-chrome-retry1/test-failed-1.png"
                        },
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-Add--968cc-form-shows-validation-toast-android-chrome-retry1/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/printing.spec.js",
                        "column": 16,
                        "line": 95
                      }
                    }
                  ],
                  "status": "unexpected"
                }
              ],
              "id": "84f3bc84c047e9f9c0b8-d4910943ff27bc3b36a2",
              "file": "printing.spec.js",
              "line": 106,
              "column": 3
            },
            {
              "title": "valid item adds to list and enables Compute",
              "ok": false,
              "tags": [],
              "tests": [
                {
                  "timeout": 30000,
                  "annotations": [],
                  "expectedStatus": "passed",
                  "projectId": "android-chrome",
                  "projectName": "android-chrome",
                  "results": [
                    {
                      "workerIndex": 76,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 8238,
                      "error": {
                        "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n",
                        "stack": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n    at /var/www/360lm/tests/printing.spec.js:95:16",
                        "location": {
                          "file": "/var/www/360lm/tests/printing.spec.js",
                          "column": 16,
                          "line": 95
                        },
                        "snippet": "  93 |     await injectSession(page);\n  94 |     await page.goto(BASE_URL);\n> 95 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n     |                ^\n  96 |     await page.locator('text=Start New Nesting').click();\n  97 |     await page.waitForSelector('#screen-add.active', { timeout: 4000 });\n  98 |     await page.locator('#tab-m-btn').click();"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/printing.spec.js",
                            "column": 16,
                            "line": 95
                          },
                          "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n\n  93 |     await injectSession(page);\n  94 |     await page.goto(BASE_URL);\n> 95 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n     |                ^\n  96 |     await page.locator('text=Start New Nesting').click();\n  97 |     await page.waitForSelector('#screen-add.active', { timeout: 4000 });\n  98 |     await page.locator('#tab-m-btn').click();\n    at /var/www/360lm/tests/printing.spec.js:95:16"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 0,
                      "startTime": "2026-07-12T13:05:23.538Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "screenshot",
                          "contentType": "image/png",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-Add--8f6f3-to-list-and-enables-Compute-android-chrome/test-failed-1.png"
                        },
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-Add--8f6f3-to-list-and-enables-Compute-android-chrome/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/printing.spec.js",
                        "column": 16,
                        "line": 95
                      }
                    },
                    {
                      "workerIndex": 77,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 8243,
                      "error": {
                        "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n",
                        "stack": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n    at /var/www/360lm/tests/printing.spec.js:95:16",
                        "location": {
                          "file": "/var/www/360lm/tests/printing.spec.js",
                          "column": 16,
                          "line": 95
                        },
                        "snippet": "  93 |     await injectSession(page);\n  94 |     await page.goto(BASE_URL);\n> 95 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n     |                ^\n  96 |     await page.locator('text=Start New Nesting').click();\n  97 |     await page.waitForSelector('#screen-add.active', { timeout: 4000 });\n  98 |     await page.locator('#tab-m-btn').click();"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/printing.spec.js",
                            "column": 16,
                            "line": 95
                          },
                          "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n\n  93 |     await injectSession(page);\n  94 |     await page.goto(BASE_URL);\n> 95 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n     |                ^\n  96 |     await page.locator('text=Start New Nesting').click();\n  97 |     await page.waitForSelector('#screen-add.active', { timeout: 4000 });\n  98 |     await page.locator('#tab-m-btn').click();\n    at /var/www/360lm/tests/printing.spec.js:95:16"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 1,
                      "startTime": "2026-07-12T13:05:32.865Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "screenshot",
                          "contentType": "image/png",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-Add--8f6f3-to-list-and-enables-Compute-android-chrome-retry1/test-failed-1.png"
                        },
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-Add--8f6f3-to-list-and-enables-Compute-android-chrome-retry1/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/printing.spec.js",
                        "column": 16,
                        "line": 95
                      }
                    }
                  ],
                  "status": "unexpected"
                }
              ],
              "id": "84f3bc84c047e9f9c0b8-54e05ee6fecacfc0cee7",
              "file": "printing.spec.js",
              "line": 111,
              "column": 3
            },
            {
              "title": "removing item disables Compute again",
              "ok": false,
              "tags": [],
              "tests": [
                {
                  "timeout": 30000,
                  "annotations": [],
                  "expectedStatus": "passed",
                  "projectId": "android-chrome",
                  "projectName": "android-chrome",
                  "results": [
                    {
                      "workerIndex": 78,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 8276,
                      "error": {
                        "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n",
                        "stack": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n    at /var/www/360lm/tests/printing.spec.js:95:16",
                        "location": {
                          "file": "/var/www/360lm/tests/printing.spec.js",
                          "column": 16,
                          "line": 95
                        },
                        "snippet": "  93 |     await injectSession(page);\n  94 |     await page.goto(BASE_URL);\n> 95 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n     |                ^\n  96 |     await page.locator('text=Start New Nesting').click();\n  97 |     await page.waitForSelector('#screen-add.active', { timeout: 4000 });\n  98 |     await page.locator('#tab-m-btn').click();"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/printing.spec.js",
                            "column": 16,
                            "line": 95
                          },
                          "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n\n  93 |     await injectSession(page);\n  94 |     await page.goto(BASE_URL);\n> 95 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n     |                ^\n  96 |     await page.locator('text=Start New Nesting').click();\n  97 |     await page.waitForSelector('#screen-add.active', { timeout: 4000 });\n  98 |     await page.locator('#tab-m-btn').click();\n    at /var/www/360lm/tests/printing.spec.js:95:16"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 0,
                      "startTime": "2026-07-12T13:05:42.109Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "screenshot",
                          "contentType": "image/png",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-Add--592c7-item-disables-Compute-again-android-chrome/test-failed-1.png"
                        },
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-Add--592c7-item-disables-Compute-again-android-chrome/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/printing.spec.js",
                        "column": 16,
                        "line": 95
                      }
                    },
                    {
                      "workerIndex": 79,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 8253,
                      "error": {
                        "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n",
                        "stack": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n    at /var/www/360lm/tests/printing.spec.js:95:16",
                        "location": {
                          "file": "/var/www/360lm/tests/printing.spec.js",
                          "column": 16,
                          "line": 95
                        },
                        "snippet": "  93 |     await injectSession(page);\n  94 |     await page.goto(BASE_URL);\n> 95 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n     |                ^\n  96 |     await page.locator('text=Start New Nesting').click();\n  97 |     await page.waitForSelector('#screen-add.active', { timeout: 4000 });\n  98 |     await page.locator('#tab-m-btn').click();"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/printing.spec.js",
                            "column": 16,
                            "line": 95
                          },
                          "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n\n  93 |     await injectSession(page);\n  94 |     await page.goto(BASE_URL);\n> 95 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n     |                ^\n  96 |     await page.locator('text=Start New Nesting').click();\n  97 |     await page.waitForSelector('#screen-add.active', { timeout: 4000 });\n  98 |     await page.locator('#tab-m-btn').click();\n    at /var/www/360lm/tests/printing.spec.js:95:16"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 1,
                      "startTime": "2026-07-12T13:05:51.534Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "screenshot",
                          "contentType": "image/png",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-Add--592c7-item-disables-Compute-again-android-chrome-retry1/test-failed-1.png"
                        },
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-Add--592c7-item-disables-Compute-again-android-chrome-retry1/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/printing.spec.js",
                        "column": 16,
                        "line": 95
                      }
                    }
                  ],
                  "status": "unexpected"
                }
              ],
              "id": "84f3bc84c047e9f9c0b8-399d972b0fcd44fa6fe8",
              "file": "printing.spec.js",
              "line": 128,
              "column": 3
            },
            {
              "title": "multiple items accumulate in list",
              "ok": false,
              "tags": [],
              "tests": [
                {
                  "timeout": 30000,
                  "annotations": [],
                  "expectedStatus": "passed",
                  "projectId": "android-chrome",
                  "projectName": "android-chrome",
                  "results": [
                    {
                      "workerIndex": 80,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 8650,
                      "error": {
                        "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n",
                        "stack": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n    at /var/www/360lm/tests/printing.spec.js:95:16",
                        "location": {
                          "file": "/var/www/360lm/tests/printing.spec.js",
                          "column": 16,
                          "line": 95
                        },
                        "snippet": "  93 |     await injectSession(page);\n  94 |     await page.goto(BASE_URL);\n> 95 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n     |                ^\n  96 |     await page.locator('text=Start New Nesting').click();\n  97 |     await page.waitForSelector('#screen-add.active', { timeout: 4000 });\n  98 |     await page.locator('#tab-m-btn').click();"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/printing.spec.js",
                            "column": 16,
                            "line": 95
                          },
                          "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n\n  93 |     await injectSession(page);\n  94 |     await page.goto(BASE_URL);\n> 95 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n     |                ^\n  96 |     await page.locator('text=Start New Nesting').click();\n  97 |     await page.waitForSelector('#screen-add.active', { timeout: 4000 });\n  98 |     await page.locator('#tab-m-btn').click();\n    at /var/www/360lm/tests/printing.spec.js:95:16"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 0,
                      "startTime": "2026-07-12T13:06:01.036Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "screenshot",
                          "contentType": "image/png",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-Add--5c572-le-items-accumulate-in-list-android-chrome/test-failed-1.png"
                        },
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-Add--5c572-le-items-accumulate-in-list-android-chrome/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/printing.spec.js",
                        "column": 16,
                        "line": 95
                      }
                    },
                    {
                      "workerIndex": 81,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 8300,
                      "error": {
                        "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n",
                        "stack": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n    at /var/www/360lm/tests/printing.spec.js:95:16",
                        "location": {
                          "file": "/var/www/360lm/tests/printing.spec.js",
                          "column": 16,
                          "line": 95
                        },
                        "snippet": "  93 |     await injectSession(page);\n  94 |     await page.goto(BASE_URL);\n> 95 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n     |                ^\n  96 |     await page.locator('text=Start New Nesting').click();\n  97 |     await page.waitForSelector('#screen-add.active', { timeout: 4000 });\n  98 |     await page.locator('#tab-m-btn').click();"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/printing.spec.js",
                            "column": 16,
                            "line": 95
                          },
                          "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n\n  93 |     await injectSession(page);\n  94 |     await page.goto(BASE_URL);\n> 95 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n     |                ^\n  96 |     await page.locator('text=Start New Nesting').click();\n  97 |     await page.waitForSelector('#screen-add.active', { timeout: 4000 });\n  98 |     await page.locator('#tab-m-btn').click();\n    at /var/www/360lm/tests/printing.spec.js:95:16"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 1,
                      "startTime": "2026-07-12T13:06:10.738Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "screenshot",
                          "contentType": "image/png",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-Add--5c572-le-items-accumulate-in-list-android-chrome-retry1/test-failed-1.png"
                        },
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-Add--5c572-le-items-accumulate-in-list-android-chrome-retry1/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/printing.spec.js",
                        "column": 16,
                        "line": 95
                      }
                    }
                  ],
                  "status": "unexpected"
                }
              ],
              "id": "84f3bc84c047e9f9c0b8-622332b7a31b39260603",
              "file": "printing.spec.js",
              "line": 139,
              "column": 3
            },
            {
              "title": "bleed margins section expands",
              "ok": false,
              "tags": [],
              "tests": [
                {
                  "timeout": 30000,
                  "annotations": [],
                  "expectedStatus": "passed",
                  "projectId": "android-chrome",
                  "projectName": "android-chrome",
                  "results": [
                    {
                      "workerIndex": 82,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 8257,
                      "error": {
                        "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n",
                        "stack": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n    at /var/www/360lm/tests/printing.spec.js:95:16",
                        "location": {
                          "file": "/var/www/360lm/tests/printing.spec.js",
                          "column": 16,
                          "line": 95
                        },
                        "snippet": "  93 |     await injectSession(page);\n  94 |     await page.goto(BASE_URL);\n> 95 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n     |                ^\n  96 |     await page.locator('text=Start New Nesting').click();\n  97 |     await page.waitForSelector('#screen-add.active', { timeout: 4000 });\n  98 |     await page.locator('#tab-m-btn').click();"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/printing.spec.js",
                            "column": 16,
                            "line": 95
                          },
                          "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n\n  93 |     await injectSession(page);\n  94 |     await page.goto(BASE_URL);\n> 95 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n     |                ^\n  96 |     await page.locator('text=Start New Nesting').click();\n  97 |     await page.waitForSelector('#screen-add.active', { timeout: 4000 });\n  98 |     await page.locator('#tab-m-btn').click();\n    at /var/www/360lm/tests/printing.spec.js:95:16"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 0,
                      "startTime": "2026-07-12T13:06:20.097Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "screenshot",
                          "contentType": "image/png",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-Add--1cebd-eed-margins-section-expands-android-chrome/test-failed-1.png"
                        },
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-Add--1cebd-eed-margins-section-expands-android-chrome/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/printing.spec.js",
                        "column": 16,
                        "line": 95
                      }
                    },
                    {
                      "workerIndex": 83,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 8300,
                      "error": {
                        "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n",
                        "stack": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n    at /var/www/360lm/tests/printing.spec.js:95:16",
                        "location": {
                          "file": "/var/www/360lm/tests/printing.spec.js",
                          "column": 16,
                          "line": 95
                        },
                        "snippet": "  93 |     await injectSession(page);\n  94 |     await page.goto(BASE_URL);\n> 95 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n     |                ^\n  96 |     await page.locator('text=Start New Nesting').click();\n  97 |     await page.waitForSelector('#screen-add.active', { timeout: 4000 });\n  98 |     await page.locator('#tab-m-btn').click();"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/printing.spec.js",
                            "column": 16,
                            "line": 95
                          },
                          "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n\n  93 |     await injectSession(page);\n  94 |     await page.goto(BASE_URL);\n> 95 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n     |                ^\n  96 |     await page.locator('text=Start New Nesting').click();\n  97 |     await page.waitForSelector('#screen-add.active', { timeout: 4000 });\n  98 |     await page.locator('#tab-m-btn').click();\n    at /var/www/360lm/tests/printing.spec.js:95:16"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 1,
                      "startTime": "2026-07-12T13:06:29.420Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "screenshot",
                          "contentType": "image/png",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-Add--1cebd-eed-margins-section-expands-android-chrome-retry1/test-failed-1.png"
                        },
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-Add--1cebd-eed-margins-section-expands-android-chrome-retry1/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/printing.spec.js",
                        "column": 16,
                        "line": 95
                      }
                    }
                  ],
                  "status": "unexpected"
                }
              ],
              "id": "84f3bc84c047e9f9c0b8-b89251c9b0cdd647b45e",
              "file": "printing.spec.js",
              "line": 151,
              "column": 3
            },
            {
              "title": "Compute button disabled with no items",
              "ok": false,
              "tags": [],
              "tests": [
                {
                  "timeout": 30000,
                  "annotations": [],
                  "expectedStatus": "passed",
                  "projectId": "desktop-chrome",
                  "projectName": "desktop-chrome",
                  "results": [
                    {
                      "workerIndex": 248,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 8416,
                      "error": {
                        "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n",
                        "stack": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n    at /var/www/360lm/tests/printing.spec.js:95:16",
                        "location": {
                          "file": "/var/www/360lm/tests/printing.spec.js",
                          "column": 16,
                          "line": 95
                        },
                        "snippet": "  93 |     await injectSession(page);\n  94 |     await page.goto(BASE_URL);\n> 95 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n     |                ^\n  96 |     await page.locator('text=Start New Nesting').click();\n  97 |     await page.waitForSelector('#screen-add.active', { timeout: 4000 });\n  98 |     await page.locator('#tab-m-btn').click();"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/printing.spec.js",
                            "column": 16,
                            "line": 95
                          },
                          "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n\n  93 |     await injectSession(page);\n  94 |     await page.goto(BASE_URL);\n> 95 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n     |                ^\n  96 |     await page.locator('text=Start New Nesting').click();\n  97 |     await page.waitForSelector('#screen-add.active', { timeout: 4000 });\n  98 |     await page.locator('#tab-m-btn').click();\n    at /var/www/360lm/tests/printing.spec.js:95:16"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 0,
                      "startTime": "2026-07-12T13:25:05.329Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "screenshot",
                          "contentType": "image/png",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-Add--923a8-tton-disabled-with-no-items-desktop-chrome/test-failed-1.png"
                        },
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-Add--923a8-tton-disabled-with-no-items-desktop-chrome/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/printing.spec.js",
                        "column": 16,
                        "line": 95
                      }
                    },
                    {
                      "workerIndex": 249,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 8277,
                      "error": {
                        "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n",
                        "stack": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n    at /var/www/360lm/tests/printing.spec.js:95:16",
                        "location": {
                          "file": "/var/www/360lm/tests/printing.spec.js",
                          "column": 16,
                          "line": 95
                        },
                        "snippet": "  93 |     await injectSession(page);\n  94 |     await page.goto(BASE_URL);\n> 95 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n     |                ^\n  96 |     await page.locator('text=Start New Nesting').click();\n  97 |     await page.waitForSelector('#screen-add.active', { timeout: 4000 });\n  98 |     await page.locator('#tab-m-btn').click();"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/printing.spec.js",
                            "column": 16,
                            "line": 95
                          },
                          "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n\n  93 |     await injectSession(page);\n  94 |     await page.goto(BASE_URL);\n> 95 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n     |                ^\n  96 |     await page.locator('text=Start New Nesting').click();\n  97 |     await page.waitForSelector('#screen-add.active', { timeout: 4000 });\n  98 |     await page.locator('#tab-m-btn').click();\n    at /var/www/360lm/tests/printing.spec.js:95:16"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 1,
                      "startTime": "2026-07-12T13:25:14.774Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "screenshot",
                          "contentType": "image/png",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-Add--923a8-tton-disabled-with-no-items-desktop-chrome-retry1/test-failed-1.png"
                        },
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-Add--923a8-tton-disabled-with-no-items-desktop-chrome-retry1/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/printing.spec.js",
                        "column": 16,
                        "line": 95
                      }
                    }
                  ],
                  "status": "unexpected"
                }
              ],
              "id": "84f3bc84c047e9f9c0b8-c2394bb0a2591953ace2",
              "file": "printing.spec.js",
              "line": 102,
              "column": 3
            },
            {
              "title": "empty form shows validation toast",
              "ok": false,
              "tags": [],
              "tests": [
                {
                  "timeout": 30000,
                  "annotations": [],
                  "expectedStatus": "passed",
                  "projectId": "desktop-chrome",
                  "projectName": "desktop-chrome",
                  "results": [
                    {
                      "workerIndex": 250,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 8267,
                      "error": {
                        "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n",
                        "stack": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n    at /var/www/360lm/tests/printing.spec.js:95:16",
                        "location": {
                          "file": "/var/www/360lm/tests/printing.spec.js",
                          "column": 16,
                          "line": 95
                        },
                        "snippet": "  93 |     await injectSession(page);\n  94 |     await page.goto(BASE_URL);\n> 95 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n     |                ^\n  96 |     await page.locator('text=Start New Nesting').click();\n  97 |     await page.waitForSelector('#screen-add.active', { timeout: 4000 });\n  98 |     await page.locator('#tab-m-btn').click();"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/printing.spec.js",
                            "column": 16,
                            "line": 95
                          },
                          "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n\n  93 |     await injectSession(page);\n  94 |     await page.goto(BASE_URL);\n> 95 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n     |                ^\n  96 |     await page.locator('text=Start New Nesting').click();\n  97 |     await page.waitForSelector('#screen-add.active', { timeout: 4000 });\n  98 |     await page.locator('#tab-m-btn').click();\n    at /var/www/360lm/tests/printing.spec.js:95:16"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 0,
                      "startTime": "2026-07-12T13:25:24.015Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "screenshot",
                          "contentType": "image/png",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-Add--968cc-form-shows-validation-toast-desktop-chrome/test-failed-1.png"
                        },
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-Add--968cc-form-shows-validation-toast-desktop-chrome/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/printing.spec.js",
                        "column": 16,
                        "line": 95
                      }
                    },
                    {
                      "workerIndex": 251,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 8500,
                      "error": {
                        "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n",
                        "stack": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n    at /var/www/360lm/tests/printing.spec.js:95:16",
                        "location": {
                          "file": "/var/www/360lm/tests/printing.spec.js",
                          "column": 16,
                          "line": 95
                        },
                        "snippet": "  93 |     await injectSession(page);\n  94 |     await page.goto(BASE_URL);\n> 95 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n     |                ^\n  96 |     await page.locator('text=Start New Nesting').click();\n  97 |     await page.waitForSelector('#screen-add.active', { timeout: 4000 });\n  98 |     await page.locator('#tab-m-btn').click();"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/printing.spec.js",
                            "column": 16,
                            "line": 95
                          },
                          "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n\n  93 |     await injectSession(page);\n  94 |     await page.goto(BASE_URL);\n> 95 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n     |                ^\n  96 |     await page.locator('text=Start New Nesting').click();\n  97 |     await page.waitForSelector('#screen-add.active', { timeout: 4000 });\n  98 |     await page.locator('#tab-m-btn').click();\n    at /var/www/360lm/tests/printing.spec.js:95:16"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 1,
                      "startTime": "2026-07-12T13:25:33.729Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "screenshot",
                          "contentType": "image/png",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-Add--968cc-form-shows-validation-toast-desktop-chrome-retry1/test-failed-1.png"
                        },
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-Add--968cc-form-shows-validation-toast-desktop-chrome-retry1/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/printing.spec.js",
                        "column": 16,
                        "line": 95
                      }
                    }
                  ],
                  "status": "unexpected"
                }
              ],
              "id": "84f3bc84c047e9f9c0b8-a2c2aed7551e2edd1e70",
              "file": "printing.spec.js",
              "line": 106,
              "column": 3
            },
            {
              "title": "valid item adds to list and enables Compute",
              "ok": false,
              "tags": [],
              "tests": [
                {
                  "timeout": 30000,
                  "annotations": [],
                  "expectedStatus": "passed",
                  "projectId": "desktop-chrome",
                  "projectName": "desktop-chrome",
                  "results": [
                    {
                      "workerIndex": 252,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 8272,
                      "error": {
                        "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n",
                        "stack": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n    at /var/www/360lm/tests/printing.spec.js:95:16",
                        "location": {
                          "file": "/var/www/360lm/tests/printing.spec.js",
                          "column": 16,
                          "line": 95
                        },
                        "snippet": "  93 |     await injectSession(page);\n  94 |     await page.goto(BASE_URL);\n> 95 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n     |                ^\n  96 |     await page.locator('text=Start New Nesting').click();\n  97 |     await page.waitForSelector('#screen-add.active', { timeout: 4000 });\n  98 |     await page.locator('#tab-m-btn').click();"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/printing.spec.js",
                            "column": 16,
                            "line": 95
                          },
                          "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n\n  93 |     await injectSession(page);\n  94 |     await page.goto(BASE_URL);\n> 95 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n     |                ^\n  96 |     await page.locator('text=Start New Nesting').click();\n  97 |     await page.waitForSelector('#screen-add.active', { timeout: 4000 });\n  98 |     await page.locator('#tab-m-btn').click();\n    at /var/www/360lm/tests/printing.spec.js:95:16"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 0,
                      "startTime": "2026-07-12T13:25:43.464Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "screenshot",
                          "contentType": "image/png",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-Add--8f6f3-to-list-and-enables-Compute-desktop-chrome/test-failed-1.png"
                        },
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-Add--8f6f3-to-list-and-enables-Compute-desktop-chrome/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/printing.spec.js",
                        "column": 16,
                        "line": 95
                      }
                    },
                    {
                      "workerIndex": 253,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 8499,
                      "error": {
                        "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n",
                        "stack": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n    at /var/www/360lm/tests/printing.spec.js:95:16",
                        "location": {
                          "file": "/var/www/360lm/tests/printing.spec.js",
                          "column": 16,
                          "line": 95
                        },
                        "snippet": "  93 |     await injectSession(page);\n  94 |     await page.goto(BASE_URL);\n> 95 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n     |                ^\n  96 |     await page.locator('text=Start New Nesting').click();\n  97 |     await page.waitForSelector('#screen-add.active', { timeout: 4000 });\n  98 |     await page.locator('#tab-m-btn').click();"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/printing.spec.js",
                            "column": 16,
                            "line": 95
                          },
                          "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n\n  93 |     await injectSession(page);\n  94 |     await page.goto(BASE_URL);\n> 95 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n     |                ^\n  96 |     await page.locator('text=Start New Nesting').click();\n  97 |     await page.waitForSelector('#screen-add.active', { timeout: 4000 });\n  98 |     await page.locator('#tab-m-btn').click();\n    at /var/www/360lm/tests/printing.spec.js:95:16"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 1,
                      "startTime": "2026-07-12T13:25:52.812Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "screenshot",
                          "contentType": "image/png",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-Add--8f6f3-to-list-and-enables-Compute-desktop-chrome-retry1/test-failed-1.png"
                        },
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-Add--8f6f3-to-list-and-enables-Compute-desktop-chrome-retry1/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/printing.spec.js",
                        "column": 16,
                        "line": 95
                      }
                    }
                  ],
                  "status": "unexpected"
                }
              ],
              "id": "84f3bc84c047e9f9c0b8-c4a183df5d2e20dec15e",
              "file": "printing.spec.js",
              "line": 111,
              "column": 3
            },
            {
              "title": "removing item disables Compute again",
              "ok": false,
              "tags": [],
              "tests": [
                {
                  "timeout": 30000,
                  "annotations": [],
                  "expectedStatus": "passed",
                  "projectId": "desktop-chrome",
                  "projectName": "desktop-chrome",
                  "results": [
                    {
                      "workerIndex": 254,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 8512,
                      "error": {
                        "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n",
                        "stack": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n    at /var/www/360lm/tests/printing.spec.js:95:16",
                        "location": {
                          "file": "/var/www/360lm/tests/printing.spec.js",
                          "column": 16,
                          "line": 95
                        },
                        "snippet": "  93 |     await injectSession(page);\n  94 |     await page.goto(BASE_URL);\n> 95 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n     |                ^\n  96 |     await page.locator('text=Start New Nesting').click();\n  97 |     await page.waitForSelector('#screen-add.active', { timeout: 4000 });\n  98 |     await page.locator('#tab-m-btn').click();"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/printing.spec.js",
                            "column": 16,
                            "line": 95
                          },
                          "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n\n  93 |     await injectSession(page);\n  94 |     await page.goto(BASE_URL);\n> 95 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n     |                ^\n  96 |     await page.locator('text=Start New Nesting').click();\n  97 |     await page.waitForSelector('#screen-add.active', { timeout: 4000 });\n  98 |     await page.locator('#tab-m-btn').click();\n    at /var/www/360lm/tests/printing.spec.js:95:16"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 0,
                      "startTime": "2026-07-12T13:26:02.980Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "screenshot",
                          "contentType": "image/png",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-Add--592c7-item-disables-Compute-again-desktop-chrome/test-failed-1.png"
                        },
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-Add--592c7-item-disables-Compute-again-desktop-chrome/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/printing.spec.js",
                        "column": 16,
                        "line": 95
                      }
                    },
                    {
                      "workerIndex": 255,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 8413,
                      "error": {
                        "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n",
                        "stack": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n    at /var/www/360lm/tests/printing.spec.js:95:16",
                        "location": {
                          "file": "/var/www/360lm/tests/printing.spec.js",
                          "column": 16,
                          "line": 95
                        },
                        "snippet": "  93 |     await injectSession(page);\n  94 |     await page.goto(BASE_URL);\n> 95 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n     |                ^\n  96 |     await page.locator('text=Start New Nesting').click();\n  97 |     await page.waitForSelector('#screen-add.active', { timeout: 4000 });\n  98 |     await page.locator('#tab-m-btn').click();"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/printing.spec.js",
                            "column": 16,
                            "line": 95
                          },
                          "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n\n  93 |     await injectSession(page);\n  94 |     await page.goto(BASE_URL);\n> 95 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n     |                ^\n  96 |     await page.locator('text=Start New Nesting').click();\n  97 |     await page.waitForSelector('#screen-add.active', { timeout: 4000 });\n  98 |     await page.locator('#tab-m-btn').click();\n    at /var/www/360lm/tests/printing.spec.js:95:16"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 1,
                      "startTime": "2026-07-12T13:26:12.731Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "screenshot",
                          "contentType": "image/png",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-Add--592c7-item-disables-Compute-again-desktop-chrome-retry1/test-failed-1.png"
                        },
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-Add--592c7-item-disables-Compute-again-desktop-chrome-retry1/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/printing.spec.js",
                        "column": 16,
                        "line": 95
                      }
                    }
                  ],
                  "status": "unexpected"
                }
              ],
              "id": "84f3bc84c047e9f9c0b8-4550ef4778ee5e898c37",
              "file": "printing.spec.js",
              "line": 128,
              "column": 3
            },
            {
              "title": "multiple items accumulate in list",
              "ok": false,
              "tags": [],
              "tests": [
                {
                  "timeout": 30000,
                  "annotations": [],
                  "expectedStatus": "passed",
                  "projectId": "desktop-chrome",
                  "projectName": "desktop-chrome",
                  "results": [
                    {
                      "workerIndex": 256,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 8285,
                      "error": {
                        "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n",
                        "stack": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n    at /var/www/360lm/tests/printing.spec.js:95:16",
                        "location": {
                          "file": "/var/www/360lm/tests/printing.spec.js",
                          "column": 16,
                          "line": 95
                        },
                        "snippet": "  93 |     await injectSession(page);\n  94 |     await page.goto(BASE_URL);\n> 95 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n     |                ^\n  96 |     await page.locator('text=Start New Nesting').click();\n  97 |     await page.waitForSelector('#screen-add.active', { timeout: 4000 });\n  98 |     await page.locator('#tab-m-btn').click();"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/printing.spec.js",
                            "column": 16,
                            "line": 95
                          },
                          "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n\n  93 |     await injectSession(page);\n  94 |     await page.goto(BASE_URL);\n> 95 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n     |                ^\n  96 |     await page.locator('text=Start New Nesting').click();\n  97 |     await page.waitForSelector('#screen-add.active', { timeout: 4000 });\n  98 |     await page.locator('#tab-m-btn').click();\n    at /var/www/360lm/tests/printing.spec.js:95:16"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 0,
                      "startTime": "2026-07-12T13:26:22.178Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "screenshot",
                          "contentType": "image/png",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-Add--5c572-le-items-accumulate-in-list-desktop-chrome/test-failed-1.png"
                        },
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-Add--5c572-le-items-accumulate-in-list-desktop-chrome/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/printing.spec.js",
                        "column": 16,
                        "line": 95
                      }
                    },
                    {
                      "workerIndex": 257,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 8309,
                      "error": {
                        "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n",
                        "stack": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n    at /var/www/360lm/tests/printing.spec.js:95:16",
                        "location": {
                          "file": "/var/www/360lm/tests/printing.spec.js",
                          "column": 16,
                          "line": 95
                        },
                        "snippet": "  93 |     await injectSession(page);\n  94 |     await page.goto(BASE_URL);\n> 95 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n     |                ^\n  96 |     await page.locator('text=Start New Nesting').click();\n  97 |     await page.waitForSelector('#screen-add.active', { timeout: 4000 });\n  98 |     await page.locator('#tab-m-btn').click();"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/printing.spec.js",
                            "column": 16,
                            "line": 95
                          },
                          "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n\n  93 |     await injectSession(page);\n  94 |     await page.goto(BASE_URL);\n> 95 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n     |                ^\n  96 |     await page.locator('text=Start New Nesting').click();\n  97 |     await page.waitForSelector('#screen-add.active', { timeout: 4000 });\n  98 |     await page.locator('#tab-m-btn').click();\n    at /var/www/360lm/tests/printing.spec.js:95:16"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 1,
                      "startTime": "2026-07-12T13:26:31.669Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "screenshot",
                          "contentType": "image/png",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-Add--5c572-le-items-accumulate-in-list-desktop-chrome-retry1/test-failed-1.png"
                        },
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-Add--5c572-le-items-accumulate-in-list-desktop-chrome-retry1/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/printing.spec.js",
                        "column": 16,
                        "line": 95
                      }
                    }
                  ],
                  "status": "unexpected"
                }
              ],
              "id": "84f3bc84c047e9f9c0b8-72cae6b844b178f8b174",
              "file": "printing.spec.js",
              "line": 139,
              "column": 3
            },
            {
              "title": "bleed margins section expands",
              "ok": false,
              "tags": [],
              "tests": [
                {
                  "timeout": 30000,
                  "annotations": [],
                  "expectedStatus": "passed",
                  "projectId": "desktop-chrome",
                  "projectName": "desktop-chrome",
                  "results": [
                    {
                      "workerIndex": 258,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 8346,
                      "error": {
                        "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n",
                        "stack": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n    at /var/www/360lm/tests/printing.spec.js:95:16",
                        "location": {
                          "file": "/var/www/360lm/tests/printing.spec.js",
                          "column": 16,
                          "line": 95
                        },
                        "snippet": "  93 |     await injectSession(page);\n  94 |     await page.goto(BASE_URL);\n> 95 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n     |                ^\n  96 |     await page.locator('text=Start New Nesting').click();\n  97 |     await page.waitForSelector('#screen-add.active', { timeout: 4000 });\n  98 |     await page.locator('#tab-m-btn').click();"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/printing.spec.js",
                            "column": 16,
                            "line": 95
                          },
                          "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n\n  93 |     await injectSession(page);\n  94 |     await page.goto(BASE_URL);\n> 95 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n     |                ^\n  96 |     await page.locator('text=Start New Nesting').click();\n  97 |     await page.waitForSelector('#screen-add.active', { timeout: 4000 });\n  98 |     await page.locator('#tab-m-btn').click();\n    at /var/www/360lm/tests/printing.spec.js:95:16"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 0,
                      "startTime": "2026-07-12T13:26:40.990Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "screenshot",
                          "contentType": "image/png",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-Add--1cebd-eed-margins-section-expands-desktop-chrome/test-failed-1.png"
                        },
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-Add--1cebd-eed-margins-section-expands-desktop-chrome/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/printing.spec.js",
                        "column": 16,
                        "line": 95
                      }
                    },
                    {
                      "workerIndex": 259,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 8306,
                      "error": {
                        "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n",
                        "stack": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n    at /var/www/360lm/tests/printing.spec.js:95:16",
                        "location": {
                          "file": "/var/www/360lm/tests/printing.spec.js",
                          "column": 16,
                          "line": 95
                        },
                        "snippet": "  93 |     await injectSession(page);\n  94 |     await page.goto(BASE_URL);\n> 95 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n     |                ^\n  96 |     await page.locator('text=Start New Nesting').click();\n  97 |     await page.waitForSelector('#screen-add.active', { timeout: 4000 });\n  98 |     await page.locator('#tab-m-btn').click();"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/printing.spec.js",
                            "column": 16,
                            "line": 95
                          },
                          "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n\n  93 |     await injectSession(page);\n  94 |     await page.goto(BASE_URL);\n> 95 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n     |                ^\n  96 |     await page.locator('text=Start New Nesting').click();\n  97 |     await page.waitForSelector('#screen-add.active', { timeout: 4000 });\n  98 |     await page.locator('#tab-m-btn').click();\n    at /var/www/360lm/tests/printing.spec.js:95:16"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 1,
                      "startTime": "2026-07-12T13:26:50.460Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "screenshot",
                          "contentType": "image/png",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-Add--1cebd-eed-margins-section-expands-desktop-chrome-retry1/test-failed-1.png"
                        },
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-Add--1cebd-eed-margins-section-expands-desktop-chrome-retry1/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/printing.spec.js",
                        "column": 16,
                        "line": 95
                      }
                    }
                  ],
                  "status": "unexpected"
                }
              ],
              "id": "84f3bc84c047e9f9c0b8-2e472695fc5bf56ec57f",
              "file": "printing.spec.js",
              "line": 151,
              "column": 3
            }
          ]
        },
        {
          "title": "Print Nest — Nesting Engine",
          "file": "printing.spec.js",
          "line": 158,
          "column": 6,
          "specs": [
            {
              "title": "3×6 Flex → 1-roll 3ft, 18 sqft/pc (narrow=3 → 3ft roll)",
              "ok": false,
              "tags": [],
              "tests": [
                {
                  "timeout": 30000,
                  "annotations": [],
                  "expectedStatus": "passed",
                  "projectId": "android-chrome",
                  "projectName": "android-chrome",
                  "results": [
                    {
                      "workerIndex": 84,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 8266,
                      "error": {
                        "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n",
                        "stack": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n    at /var/www/360lm/tests/printing.spec.js:162:16",
                        "location": {
                          "file": "/var/www/360lm/tests/printing.spec.js",
                          "column": 16,
                          "line": 162
                        },
                        "snippet": "  160 |     await injectSession(page);\n  161 |     await page.goto(BASE_URL);\n> 162 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n      |                ^\n  163 |     await page.locator('text=Start New Nesting').click();\n  164 |     await page.waitForSelector('#screen-add.active', { timeout: 4000 });\n  165 |   });"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/printing.spec.js",
                            "column": 16,
                            "line": 162
                          },
                          "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n\n  160 |     await injectSession(page);\n  161 |     await page.goto(BASE_URL);\n> 162 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n      |                ^\n  163 |     await page.locator('text=Start New Nesting').click();\n  164 |     await page.waitForSelector('#screen-add.active', { timeout: 4000 });\n  165 |   });\n    at /var/www/360lm/tests/printing.spec.js:162:16"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 0,
                      "startTime": "2026-07-12T13:06:38.760Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "screenshot",
                          "contentType": "image/png",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-Nest-9e0e4-qft-pc-narrow-3-→-3ft-roll--android-chrome/test-failed-1.png"
                        },
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-Nest-9e0e4-qft-pc-narrow-3-→-3ft-roll--android-chrome/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/printing.spec.js",
                        "column": 16,
                        "line": 162
                      }
                    },
                    {
                      "workerIndex": 85,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 8250,
                      "error": {
                        "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n",
                        "stack": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n    at /var/www/360lm/tests/printing.spec.js:162:16",
                        "location": {
                          "file": "/var/www/360lm/tests/printing.spec.js",
                          "column": 16,
                          "line": 162
                        },
                        "snippet": "  160 |     await injectSession(page);\n  161 |     await page.goto(BASE_URL);\n> 162 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n      |                ^\n  163 |     await page.locator('text=Start New Nesting').click();\n  164 |     await page.waitForSelector('#screen-add.active', { timeout: 4000 });\n  165 |   });"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/printing.spec.js",
                            "column": 16,
                            "line": 162
                          },
                          "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n\n  160 |     await injectSession(page);\n  161 |     await page.goto(BASE_URL);\n> 162 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n      |                ^\n  163 |     await page.locator('text=Start New Nesting').click();\n  164 |     await page.waitForSelector('#screen-add.active', { timeout: 4000 });\n  165 |   });\n    at /var/www/360lm/tests/printing.spec.js:162:16"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 1,
                      "startTime": "2026-07-12T13:06:48.058Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "screenshot",
                          "contentType": "image/png",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-Nest-9e0e4-qft-pc-narrow-3-→-3ft-roll--android-chrome-retry1/test-failed-1.png"
                        },
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-Nest-9e0e4-qft-pc-narrow-3-→-3ft-roll--android-chrome-retry1/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/printing.spec.js",
                        "column": 16,
                        "line": 162
                      }
                    }
                  ],
                  "status": "unexpected"
                }
              ],
              "id": "84f3bc84c047e9f9c0b8-5cafecf53aee7e1b2b16",
              "file": "printing.spec.js",
              "line": 178,
              "column": 3
            },
            {
              "title": "7ft wide Flex → 2-roll (3+4ft) is available",
              "ok": false,
              "tags": [],
              "tests": [
                {
                  "timeout": 30000,
                  "annotations": [],
                  "expectedStatus": "passed",
                  "projectId": "android-chrome",
                  "projectName": "android-chrome",
                  "results": [
                    {
                      "workerIndex": 86,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 8275,
                      "error": {
                        "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n",
                        "stack": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n    at /var/www/360lm/tests/printing.spec.js:162:16",
                        "location": {
                          "file": "/var/www/360lm/tests/printing.spec.js",
                          "column": 16,
                          "line": 162
                        },
                        "snippet": "  160 |     await injectSession(page);\n  161 |     await page.goto(BASE_URL);\n> 162 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n      |                ^\n  163 |     await page.locator('text=Start New Nesting').click();\n  164 |     await page.waitForSelector('#screen-add.active', { timeout: 4000 });\n  165 |   });"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/printing.spec.js",
                            "column": 16,
                            "line": 162
                          },
                          "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n\n  160 |     await injectSession(page);\n  161 |     await page.goto(BASE_URL);\n> 162 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n      |                ^\n  163 |     await page.locator('text=Start New Nesting').click();\n  164 |     await page.waitForSelector('#screen-add.active', { timeout: 4000 });\n  165 |   });\n    at /var/www/360lm/tests/printing.spec.js:162:16"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 0,
                      "startTime": "2026-07-12T13:06:57.400Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "screenshot",
                          "contentType": "image/png",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-Nest-a3328-→-2-roll-3-4ft-is-available-android-chrome/test-failed-1.png"
                        },
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-Nest-a3328-→-2-roll-3-4ft-is-available-android-chrome/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/printing.spec.js",
                        "column": 16,
                        "line": 162
                      }
                    },
                    {
                      "workerIndex": 87,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 8307,
                      "error": {
                        "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n",
                        "stack": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n    at /var/www/360lm/tests/printing.spec.js:162:16",
                        "location": {
                          "file": "/var/www/360lm/tests/printing.spec.js",
                          "column": 16,
                          "line": 162
                        },
                        "snippet": "  160 |     await injectSession(page);\n  161 |     await page.goto(BASE_URL);\n> 162 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n      |                ^\n  163 |     await page.locator('text=Start New Nesting').click();\n  164 |     await page.waitForSelector('#screen-add.active', { timeout: 4000 });\n  165 |   });"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/printing.spec.js",
                            "column": 16,
                            "line": 162
                          },
                          "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n\n  160 |     await injectSession(page);\n  161 |     await page.goto(BASE_URL);\n> 162 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n      |                ^\n  163 |     await page.locator('text=Start New Nesting').click();\n  164 |     await page.waitForSelector('#screen-add.active', { timeout: 4000 });\n  165 |   });\n    at /var/www/360lm/tests/printing.spec.js:162:16"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 1,
                      "startTime": "2026-07-12T13:07:06.980Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "screenshot",
                          "contentType": "image/png",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-Nest-a3328-→-2-roll-3-4ft-is-available-android-chrome-retry1/test-failed-1.png"
                        },
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-Nest-a3328-→-2-roll-3-4ft-is-available-android-chrome-retry1/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/printing.spec.js",
                        "column": 16,
                        "line": 162
                      }
                    }
                  ],
                  "status": "unexpected"
                }
              ],
              "id": "84f3bc84c047e9f9c0b8-70ff6019d0a5c87828da",
              "file": "printing.spec.js",
              "line": 193,
              "column": 3
            },
            {
              "title": "item >5ft shows outsource/join buttons",
              "ok": false,
              "tags": [],
              "tests": [
                {
                  "timeout": 30000,
                  "annotations": [],
                  "expectedStatus": "passed",
                  "projectId": "android-chrome",
                  "projectName": "android-chrome",
                  "results": [
                    {
                      "workerIndex": 88,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 8242,
                      "error": {
                        "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n",
                        "stack": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n    at /var/www/360lm/tests/printing.spec.js:162:16",
                        "location": {
                          "file": "/var/www/360lm/tests/printing.spec.js",
                          "column": 16,
                          "line": 162
                        },
                        "snippet": "  160 |     await injectSession(page);\n  161 |     await page.goto(BASE_URL);\n> 162 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n      |                ^\n  163 |     await page.locator('text=Start New Nesting').click();\n  164 |     await page.waitForSelector('#screen-add.active', { timeout: 4000 });\n  165 |   });"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/printing.spec.js",
                            "column": 16,
                            "line": 162
                          },
                          "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n\n  160 |     await injectSession(page);\n  161 |     await page.goto(BASE_URL);\n> 162 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n      |                ^\n  163 |     await page.locator('text=Start New Nesting').click();\n  164 |     await page.waitForSelector('#screen-add.active', { timeout: 4000 });\n  165 |   });\n    at /var/www/360lm/tests/printing.spec.js:162:16"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 0,
                      "startTime": "2026-07-12T13:07:16.404Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "screenshot",
                          "contentType": "image/png",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-Nest-642e7-hows-outsource-join-buttons-android-chrome/test-failed-1.png"
                        },
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-Nest-642e7-hows-outsource-join-buttons-android-chrome/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/printing.spec.js",
                        "column": 16,
                        "line": 162
                      }
                    },
                    {
                      "workerIndex": 89,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 8353,
                      "error": {
                        "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n",
                        "stack": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n    at /var/www/360lm/tests/printing.spec.js:162:16",
                        "location": {
                          "file": "/var/www/360lm/tests/printing.spec.js",
                          "column": 16,
                          "line": 162
                        },
                        "snippet": "  160 |     await injectSession(page);\n  161 |     await page.goto(BASE_URL);\n> 162 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n      |                ^\n  163 |     await page.locator('text=Start New Nesting').click();\n  164 |     await page.waitForSelector('#screen-add.active', { timeout: 4000 });\n  165 |   });"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/printing.spec.js",
                            "column": 16,
                            "line": 162
                          },
                          "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n\n  160 |     await injectSession(page);\n  161 |     await page.goto(BASE_URL);\n> 162 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n      |                ^\n  163 |     await page.locator('text=Start New Nesting').click();\n  164 |     await page.waitForSelector('#screen-add.active', { timeout: 4000 });\n  165 |   });\n    at /var/www/360lm/tests/printing.spec.js:162:16"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 1,
                      "startTime": "2026-07-12T13:07:25.623Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "screenshot",
                          "contentType": "image/png",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-Nest-642e7-hows-outsource-join-buttons-android-chrome-retry1/test-failed-1.png"
                        },
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-Nest-642e7-hows-outsource-join-buttons-android-chrome-retry1/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/printing.spec.js",
                        "column": 16,
                        "line": 162
                      }
                    }
                  ],
                  "status": "unexpected"
                }
              ],
              "id": "84f3bc84c047e9f9c0b8-7e40f609da5de5265042",
              "file": "printing.spec.js",
              "line": 207,
              "column": 3
            },
            {
              "title": "gang plan groups same roll-width items together",
              "ok": false,
              "tags": [],
              "tests": [
                {
                  "timeout": 30000,
                  "annotations": [],
                  "expectedStatus": "passed",
                  "projectId": "android-chrome",
                  "projectName": "android-chrome",
                  "results": [
                    {
                      "workerIndex": 90,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 8303,
                      "error": {
                        "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n",
                        "stack": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n    at /var/www/360lm/tests/printing.spec.js:162:16",
                        "location": {
                          "file": "/var/www/360lm/tests/printing.spec.js",
                          "column": 16,
                          "line": 162
                        },
                        "snippet": "  160 |     await injectSession(page);\n  161 |     await page.goto(BASE_URL);\n> 162 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n      |                ^\n  163 |     await page.locator('text=Start New Nesting').click();\n  164 |     await page.waitForSelector('#screen-add.active', { timeout: 4000 });\n  165 |   });"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/printing.spec.js",
                            "column": 16,
                            "line": 162
                          },
                          "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n\n  160 |     await injectSession(page);\n  161 |     await page.goto(BASE_URL);\n> 162 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n      |                ^\n  163 |     await page.locator('text=Start New Nesting').click();\n  164 |     await page.waitForSelector('#screen-add.active', { timeout: 4000 });\n  165 |   });\n    at /var/www/360lm/tests/printing.spec.js:162:16"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 0,
                      "startTime": "2026-07-12T13:07:35.027Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "screenshot",
                          "contentType": "image/png",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-Nest-bb2e2-e-roll-width-items-together-android-chrome/test-failed-1.png"
                        },
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-Nest-bb2e2-e-roll-width-items-together-android-chrome/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/printing.spec.js",
                        "column": 16,
                        "line": 162
                      }
                    },
                    {
                      "workerIndex": 91,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 8287,
                      "error": {
                        "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n",
                        "stack": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n    at /var/www/360lm/tests/printing.spec.js:162:16",
                        "location": {
                          "file": "/var/www/360lm/tests/printing.spec.js",
                          "column": 16,
                          "line": 162
                        },
                        "snippet": "  160 |     await injectSession(page);\n  161 |     await page.goto(BASE_URL);\n> 162 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n      |                ^\n  163 |     await page.locator('text=Start New Nesting').click();\n  164 |     await page.waitForSelector('#screen-add.active', { timeout: 4000 });\n  165 |   });"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/printing.spec.js",
                            "column": 16,
                            "line": 162
                          },
                          "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n\n  160 |     await injectSession(page);\n  161 |     await page.goto(BASE_URL);\n> 162 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n      |                ^\n  163 |     await page.locator('text=Start New Nesting').click();\n  164 |     await page.waitForSelector('#screen-add.active', { timeout: 4000 });\n  165 |   });\n    at /var/www/360lm/tests/printing.spec.js:162:16"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 1,
                      "startTime": "2026-07-12T13:07:44.398Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "screenshot",
                          "contentType": "image/png",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-Nest-bb2e2-e-roll-width-items-together-android-chrome-retry1/test-failed-1.png"
                        },
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-Nest-bb2e2-e-roll-width-items-together-android-chrome-retry1/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/printing.spec.js",
                        "column": 16,
                        "line": 162
                      }
                    }
                  ],
                  "status": "unexpected"
                }
              ],
              "id": "84f3bc84c047e9f9c0b8-6e27e03e92e572b90617",
              "file": "printing.spec.js",
              "line": 220,
              "column": 3
            },
            {
              "title": "Flex and Vinyl items split into separate groups",
              "ok": false,
              "tags": [],
              "tests": [
                {
                  "timeout": 30000,
                  "annotations": [],
                  "expectedStatus": "passed",
                  "projectId": "android-chrome",
                  "projectName": "android-chrome",
                  "results": [
                    {
                      "workerIndex": 92,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 8412,
                      "error": {
                        "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n",
                        "stack": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n    at /var/www/360lm/tests/printing.spec.js:162:16",
                        "location": {
                          "file": "/var/www/360lm/tests/printing.spec.js",
                          "column": 16,
                          "line": 162
                        },
                        "snippet": "  160 |     await injectSession(page);\n  161 |     await page.goto(BASE_URL);\n> 162 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n      |                ^\n  163 |     await page.locator('text=Start New Nesting').click();\n  164 |     await page.waitForSelector('#screen-add.active', { timeout: 4000 });\n  165 |   });"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/printing.spec.js",
                            "column": 16,
                            "line": 162
                          },
                          "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n\n  160 |     await injectSession(page);\n  161 |     await page.goto(BASE_URL);\n> 162 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n      |                ^\n  163 |     await page.locator('text=Start New Nesting').click();\n  164 |     await page.waitForSelector('#screen-add.active', { timeout: 4000 });\n  165 |   });\n    at /var/www/360lm/tests/printing.spec.js:162:16"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 0,
                      "startTime": "2026-07-12T13:07:53.926Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "screenshot",
                          "contentType": "image/png",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-Nest-bfd73--split-into-separate-groups-android-chrome/test-failed-1.png"
                        },
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-Nest-bfd73--split-into-separate-groups-android-chrome/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/printing.spec.js",
                        "column": 16,
                        "line": 162
                      }
                    },
                    {
                      "workerIndex": 93,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 8760,
                      "error": {
                        "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n",
                        "stack": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n    at /var/www/360lm/tests/printing.spec.js:162:16",
                        "location": {
                          "file": "/var/www/360lm/tests/printing.spec.js",
                          "column": 16,
                          "line": 162
                        },
                        "snippet": "  160 |     await injectSession(page);\n  161 |     await page.goto(BASE_URL);\n> 162 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n      |                ^\n  163 |     await page.locator('text=Start New Nesting').click();\n  164 |     await page.waitForSelector('#screen-add.active', { timeout: 4000 });\n  165 |   });"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/printing.spec.js",
                            "column": 16,
                            "line": 162
                          },
                          "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n\n  160 |     await injectSession(page);\n  161 |     await page.goto(BASE_URL);\n> 162 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n      |                ^\n  163 |     await page.locator('text=Start New Nesting').click();\n  164 |     await page.waitForSelector('#screen-add.active', { timeout: 4000 });\n  165 |   });\n    at /var/www/360lm/tests/printing.spec.js:162:16"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 1,
                      "startTime": "2026-07-12T13:08:04.060Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "screenshot",
                          "contentType": "image/png",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-Nest-bfd73--split-into-separate-groups-android-chrome-retry1/test-failed-1.png"
                        },
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-Nest-bfd73--split-into-separate-groups-android-chrome-retry1/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/printing.spec.js",
                        "column": 16,
                        "line": 162
                      }
                    }
                  ],
                  "status": "unexpected"
                }
              ],
              "id": "84f3bc84c047e9f9c0b8-73997ee3ee4043d443ea",
              "file": "printing.spec.js",
              "line": 234,
              "column": 3
            },
            {
              "title": "Save button opens save modal",
              "ok": false,
              "tags": [],
              "tests": [
                {
                  "timeout": 30000,
                  "annotations": [],
                  "expectedStatus": "passed",
                  "projectId": "android-chrome",
                  "projectName": "android-chrome",
                  "results": [
                    {
                      "workerIndex": 94,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 8871,
                      "error": {
                        "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n",
                        "stack": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n    at /var/www/360lm/tests/printing.spec.js:162:16",
                        "location": {
                          "file": "/var/www/360lm/tests/printing.spec.js",
                          "column": 16,
                          "line": 162
                        },
                        "snippet": "  160 |     await injectSession(page);\n  161 |     await page.goto(BASE_URL);\n> 162 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n      |                ^\n  163 |     await page.locator('text=Start New Nesting').click();\n  164 |     await page.waitForSelector('#screen-add.active', { timeout: 4000 });\n  165 |   });"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/printing.spec.js",
                            "column": 16,
                            "line": 162
                          },
                          "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n\n  160 |     await injectSession(page);\n  161 |     await page.goto(BASE_URL);\n> 162 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n      |                ^\n  163 |     await page.locator('text=Start New Nesting').click();\n  164 |     await page.waitForSelector('#screen-add.active', { timeout: 4000 });\n  165 |   });\n    at /var/www/360lm/tests/printing.spec.js:162:16"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 0,
                      "startTime": "2026-07-12T13:08:14.737Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "screenshot",
                          "contentType": "image/png",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-Nest-449f8-ave-button-opens-save-modal-android-chrome/test-failed-1.png"
                        },
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-Nest-449f8-ave-button-opens-save-modal-android-chrome/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/printing.spec.js",
                        "column": 16,
                        "line": 162
                      }
                    },
                    {
                      "workerIndex": 95,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 8331,
                      "error": {
                        "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n",
                        "stack": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n    at /var/www/360lm/tests/printing.spec.js:162:16",
                        "location": {
                          "file": "/var/www/360lm/tests/printing.spec.js",
                          "column": 16,
                          "line": 162
                        },
                        "snippet": "  160 |     await injectSession(page);\n  161 |     await page.goto(BASE_URL);\n> 162 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n      |                ^\n  163 |     await page.locator('text=Start New Nesting').click();\n  164 |     await page.waitForSelector('#screen-add.active', { timeout: 4000 });\n  165 |   });"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/printing.spec.js",
                            "column": 16,
                            "line": 162
                          },
                          "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n\n  160 |     await injectSession(page);\n  161 |     await page.goto(BASE_URL);\n> 162 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n      |                ^\n  163 |     await page.locator('text=Start New Nesting').click();\n  164 |     await page.waitForSelector('#screen-add.active', { timeout: 4000 });\n  165 |   });\n    at /var/www/360lm/tests/printing.spec.js:162:16"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 1,
                      "startTime": "2026-07-12T13:08:25.383Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "screenshot",
                          "contentType": "image/png",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-Nest-449f8-ave-button-opens-save-modal-android-chrome-retry1/test-failed-1.png"
                        },
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-Nest-449f8-ave-button-opens-save-modal-android-chrome-retry1/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/printing.spec.js",
                        "column": 16,
                        "line": 162
                      }
                    }
                  ],
                  "status": "unexpected"
                }
              ],
              "id": "84f3bc84c047e9f9c0b8-1c6758088a97dceed358",
              "file": "printing.spec.js",
              "line": 242,
              "column": 3
            },
            {
              "title": "AI review button visible after computing nest",
              "ok": false,
              "tags": [],
              "tests": [
                {
                  "timeout": 30000,
                  "annotations": [],
                  "expectedStatus": "passed",
                  "projectId": "android-chrome",
                  "projectName": "android-chrome",
                  "results": [
                    {
                      "workerIndex": 96,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 8286,
                      "error": {
                        "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n",
                        "stack": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n    at /var/www/360lm/tests/printing.spec.js:162:16",
                        "location": {
                          "file": "/var/www/360lm/tests/printing.spec.js",
                          "column": 16,
                          "line": 162
                        },
                        "snippet": "  160 |     await injectSession(page);\n  161 |     await page.goto(BASE_URL);\n> 162 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n      |                ^\n  163 |     await page.locator('text=Start New Nesting').click();\n  164 |     await page.waitForSelector('#screen-add.active', { timeout: 4000 });\n  165 |   });"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/printing.spec.js",
                            "column": 16,
                            "line": 162
                          },
                          "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n\n  160 |     await injectSession(page);\n  161 |     await page.goto(BASE_URL);\n> 162 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n      |                ^\n  163 |     await page.locator('text=Start New Nesting').click();\n  164 |     await page.waitForSelector('#screen-add.active', { timeout: 4000 });\n  165 |   });\n    at /var/www/360lm/tests/printing.spec.js:162:16"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 0,
                      "startTime": "2026-07-12T13:08:34.969Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "screenshot",
                          "contentType": "image/png",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-Nest-62185-isible-after-computing-nest-android-chrome/test-failed-1.png"
                        },
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-Nest-62185-isible-after-computing-nest-android-chrome/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/printing.spec.js",
                        "column": 16,
                        "line": 162
                      }
                    },
                    {
                      "workerIndex": 97,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 8308,
                      "error": {
                        "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n",
                        "stack": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n    at /var/www/360lm/tests/printing.spec.js:162:16",
                        "location": {
                          "file": "/var/www/360lm/tests/printing.spec.js",
                          "column": 16,
                          "line": 162
                        },
                        "snippet": "  160 |     await injectSession(page);\n  161 |     await page.goto(BASE_URL);\n> 162 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n      |                ^\n  163 |     await page.locator('text=Start New Nesting').click();\n  164 |     await page.waitForSelector('#screen-add.active', { timeout: 4000 });\n  165 |   });"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/printing.spec.js",
                            "column": 16,
                            "line": 162
                          },
                          "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n\n  160 |     await injectSession(page);\n  161 |     await page.goto(BASE_URL);\n> 162 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n      |                ^\n  163 |     await page.locator('text=Start New Nesting').click();\n  164 |     await page.waitForSelector('#screen-add.active', { timeout: 4000 });\n  165 |   });\n    at /var/www/360lm/tests/printing.spec.js:162:16"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 1,
                      "startTime": "2026-07-12T13:08:44.297Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "screenshot",
                          "contentType": "image/png",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-Nest-62185-isible-after-computing-nest-android-chrome-retry1/test-failed-1.png"
                        },
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-Nest-62185-isible-after-computing-nest-android-chrome-retry1/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/printing.spec.js",
                        "column": 16,
                        "line": 162
                      }
                    }
                  ],
                  "status": "unexpected"
                }
              ],
              "id": "84f3bc84c047e9f9c0b8-9e107b68e8911f92ed23",
              "file": "printing.spec.js",
              "line": 250,
              "column": 3
            },
            {
              "title": "AI review button opens chat modal",
              "ok": false,
              "tags": [],
              "tests": [
                {
                  "timeout": 30000,
                  "annotations": [],
                  "expectedStatus": "passed",
                  "projectId": "android-chrome",
                  "projectName": "android-chrome",
                  "results": [
                    {
                      "workerIndex": 98,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 8796,
                      "error": {
                        "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n",
                        "stack": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n    at /var/www/360lm/tests/printing.spec.js:162:16",
                        "location": {
                          "file": "/var/www/360lm/tests/printing.spec.js",
                          "column": 16,
                          "line": 162
                        },
                        "snippet": "  160 |     await injectSession(page);\n  161 |     await page.goto(BASE_URL);\n> 162 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n      |                ^\n  163 |     await page.locator('text=Start New Nesting').click();\n  164 |     await page.waitForSelector('#screen-add.active', { timeout: 4000 });\n  165 |   });"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/printing.spec.js",
                            "column": 16,
                            "line": 162
                          },
                          "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n\n  160 |     await injectSession(page);\n  161 |     await page.goto(BASE_URL);\n> 162 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n      |                ^\n  163 |     await page.locator('text=Start New Nesting').click();\n  164 |     await page.waitForSelector('#screen-add.active', { timeout: 4000 });\n  165 |   });\n    at /var/www/360lm/tests/printing.spec.js:162:16"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 0,
                      "startTime": "2026-07-12T13:08:53.743Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "screenshot",
                          "contentType": "image/png",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-Nest-0f364-iew-button-opens-chat-modal-android-chrome/test-failed-1.png"
                        },
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-Nest-0f364-iew-button-opens-chat-modal-android-chrome/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/printing.spec.js",
                        "column": 16,
                        "line": 162
                      }
                    },
                    {
                      "workerIndex": 99,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 8594,
                      "error": {
                        "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n",
                        "stack": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n    at /var/www/360lm/tests/printing.spec.js:162:16",
                        "location": {
                          "file": "/var/www/360lm/tests/printing.spec.js",
                          "column": 16,
                          "line": 162
                        },
                        "snippet": "  160 |     await injectSession(page);\n  161 |     await page.goto(BASE_URL);\n> 162 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n      |                ^\n  163 |     await page.locator('text=Start New Nesting').click();\n  164 |     await page.waitForSelector('#screen-add.active', { timeout: 4000 });\n  165 |   });"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/printing.spec.js",
                            "column": 16,
                            "line": 162
                          },
                          "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n\n  160 |     await injectSession(page);\n  161 |     await page.goto(BASE_URL);\n> 162 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n      |                ^\n  163 |     await page.locator('text=Start New Nesting').click();\n  164 |     await page.waitForSelector('#screen-add.active', { timeout: 4000 });\n  165 |   });\n    at /var/www/360lm/tests/printing.spec.js:162:16"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 1,
                      "startTime": "2026-07-12T13:09:05.563Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "screenshot",
                          "contentType": "image/png",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-Nest-0f364-iew-button-opens-chat-modal-android-chrome-retry1/test-failed-1.png"
                        },
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-Nest-0f364-iew-button-opens-chat-modal-android-chrome-retry1/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/printing.spec.js",
                        "column": 16,
                        "line": 162
                      }
                    }
                  ],
                  "status": "unexpected"
                }
              ],
              "id": "84f3bc84c047e9f9c0b8-189c1833ae2688412e71",
              "file": "printing.spec.js",
              "line": 258,
              "column": 3
            },
            {
              "title": "Copy plan button visible on nest screen",
              "ok": false,
              "tags": [],
              "tests": [
                {
                  "timeout": 30000,
                  "annotations": [],
                  "expectedStatus": "passed",
                  "projectId": "android-chrome",
                  "projectName": "android-chrome",
                  "results": [
                    {
                      "workerIndex": 100,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 8280,
                      "error": {
                        "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n",
                        "stack": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n    at /var/www/360lm/tests/printing.spec.js:162:16",
                        "location": {
                          "file": "/var/www/360lm/tests/printing.spec.js",
                          "column": 16,
                          "line": 162
                        },
                        "snippet": "  160 |     await injectSession(page);\n  161 |     await page.goto(BASE_URL);\n> 162 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n      |                ^\n  163 |     await page.locator('text=Start New Nesting').click();\n  164 |     await page.waitForSelector('#screen-add.active', { timeout: 4000 });\n  165 |   });"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/printing.spec.js",
                            "column": 16,
                            "line": 162
                          },
                          "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n\n  160 |     await injectSession(page);\n  161 |     await page.goto(BASE_URL);\n> 162 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n      |                ^\n  163 |     await page.locator('text=Start New Nesting').click();\n  164 |     await page.waitForSelector('#screen-add.active', { timeout: 4000 });\n  165 |   });\n    at /var/www/360lm/tests/printing.spec.js:162:16"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 0,
                      "startTime": "2026-07-12T13:09:15.667Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "screenshot",
                          "contentType": "image/png",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-Nest-cbdfe-tton-visible-on-nest-screen-android-chrome/test-failed-1.png"
                        },
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-Nest-cbdfe-tton-visible-on-nest-screen-android-chrome/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/printing.spec.js",
                        "column": 16,
                        "line": 162
                      }
                    },
                    {
                      "workerIndex": 101,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 8286,
                      "error": {
                        "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n",
                        "stack": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n    at /var/www/360lm/tests/printing.spec.js:162:16",
                        "location": {
                          "file": "/var/www/360lm/tests/printing.spec.js",
                          "column": 16,
                          "line": 162
                        },
                        "snippet": "  160 |     await injectSession(page);\n  161 |     await page.goto(BASE_URL);\n> 162 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n      |                ^\n  163 |     await page.locator('text=Start New Nesting').click();\n  164 |     await page.waitForSelector('#screen-add.active', { timeout: 4000 });\n  165 |   });"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/printing.spec.js",
                            "column": 16,
                            "line": 162
                          },
                          "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n\n  160 |     await injectSession(page);\n  161 |     await page.goto(BASE_URL);\n> 162 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n      |                ^\n  163 |     await page.locator('text=Start New Nesting').click();\n  164 |     await page.waitForSelector('#screen-add.active', { timeout: 4000 });\n  165 |   });\n    at /var/www/360lm/tests/printing.spec.js:162:16"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 1,
                      "startTime": "2026-07-12T13:09:25.130Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "screenshot",
                          "contentType": "image/png",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-Nest-cbdfe-tton-visible-on-nest-screen-android-chrome-retry1/test-failed-1.png"
                        },
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-Nest-cbdfe-tton-visible-on-nest-screen-android-chrome-retry1/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/printing.spec.js",
                        "column": 16,
                        "line": 162
                      }
                    }
                  ],
                  "status": "unexpected"
                }
              ],
              "id": "84f3bc84c047e9f9c0b8-393eaf8c973ebe982a39",
              "file": "printing.spec.js",
              "line": 268,
              "column": 3
            },
            {
              "title": "Copy plan writes text to clipboard and shows toast",
              "ok": false,
              "tags": [],
              "tests": [
                {
                  "timeout": 30000,
                  "annotations": [],
                  "expectedStatus": "passed",
                  "projectId": "android-chrome",
                  "projectName": "android-chrome",
                  "results": [
                    {
                      "workerIndex": 102,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 8336,
                      "error": {
                        "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n",
                        "stack": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n    at /var/www/360lm/tests/printing.spec.js:162:16",
                        "location": {
                          "file": "/var/www/360lm/tests/printing.spec.js",
                          "column": 16,
                          "line": 162
                        },
                        "snippet": "  160 |     await injectSession(page);\n  161 |     await page.goto(BASE_URL);\n> 162 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n      |                ^\n  163 |     await page.locator('text=Start New Nesting').click();\n  164 |     await page.waitForSelector('#screen-add.active', { timeout: 4000 });\n  165 |   });"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/printing.spec.js",
                            "column": 16,
                            "line": 162
                          },
                          "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n\n  160 |     await injectSession(page);\n  161 |     await page.goto(BASE_URL);\n> 162 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n      |                ^\n  163 |     await page.locator('text=Start New Nesting').click();\n  164 |     await page.waitForSelector('#screen-add.active', { timeout: 4000 });\n  165 |   });\n    at /var/www/360lm/tests/printing.spec.js:162:16"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 0,
                      "startTime": "2026-07-12T13:09:34.595Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "screenshot",
                          "contentType": "image/png",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-Nest-25769-o-clipboard-and-shows-toast-android-chrome/test-failed-1.png"
                        },
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-Nest-25769-o-clipboard-and-shows-toast-android-chrome/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/printing.spec.js",
                        "column": 16,
                        "line": 162
                      }
                    },
                    {
                      "workerIndex": 103,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 8383,
                      "error": {
                        "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n",
                        "stack": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n    at /var/www/360lm/tests/printing.spec.js:162:16",
                        "location": {
                          "file": "/var/www/360lm/tests/printing.spec.js",
                          "column": 16,
                          "line": 162
                        },
                        "snippet": "  160 |     await injectSession(page);\n  161 |     await page.goto(BASE_URL);\n> 162 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n      |                ^\n  163 |     await page.locator('text=Start New Nesting').click();\n  164 |     await page.waitForSelector('#screen-add.active', { timeout: 4000 });\n  165 |   });"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/printing.spec.js",
                            "column": 16,
                            "line": 162
                          },
                          "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n\n  160 |     await injectSession(page);\n  161 |     await page.goto(BASE_URL);\n> 162 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n      |                ^\n  163 |     await page.locator('text=Start New Nesting').click();\n  164 |     await page.waitForSelector('#screen-add.active', { timeout: 4000 });\n  165 |   });\n    at /var/www/360lm/tests/printing.spec.js:162:16"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 1,
                      "startTime": "2026-07-12T13:09:44.194Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "screenshot",
                          "contentType": "image/png",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-Nest-25769-o-clipboard-and-shows-toast-android-chrome-retry1/test-failed-1.png"
                        },
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-Nest-25769-o-clipboard-and-shows-toast-android-chrome-retry1/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/printing.spec.js",
                        "column": 16,
                        "line": 162
                      }
                    }
                  ],
                  "status": "unexpected"
                }
              ],
              "id": "84f3bc84c047e9f9c0b8-09429b972abab1567049",
              "file": "printing.spec.js",
              "line": 275,
              "column": 3
            },
            {
              "title": "3×6 Flex → 1-roll 3ft, 18 sqft/pc (narrow=3 → 3ft roll)",
              "ok": false,
              "tags": [],
              "tests": [
                {
                  "timeout": 30000,
                  "annotations": [],
                  "expectedStatus": "passed",
                  "projectId": "desktop-chrome",
                  "projectName": "desktop-chrome",
                  "results": [
                    {
                      "workerIndex": 260,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 8269,
                      "error": {
                        "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n",
                        "stack": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n    at /var/www/360lm/tests/printing.spec.js:162:16",
                        "location": {
                          "file": "/var/www/360lm/tests/printing.spec.js",
                          "column": 16,
                          "line": 162
                        },
                        "snippet": "  160 |     await injectSession(page);\n  161 |     await page.goto(BASE_URL);\n> 162 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n      |                ^\n  163 |     await page.locator('text=Start New Nesting').click();\n  164 |     await page.waitForSelector('#screen-add.active', { timeout: 4000 });\n  165 |   });"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/printing.spec.js",
                            "column": 16,
                            "line": 162
                          },
                          "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n\n  160 |     await injectSession(page);\n  161 |     await page.goto(BASE_URL);\n> 162 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n      |                ^\n  163 |     await page.locator('text=Start New Nesting').click();\n  164 |     await page.waitForSelector('#screen-add.active', { timeout: 4000 });\n  165 |   });\n    at /var/www/360lm/tests/printing.spec.js:162:16"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 0,
                      "startTime": "2026-07-12T13:26:59.969Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "screenshot",
                          "contentType": "image/png",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-Nest-9e0e4-qft-pc-narrow-3-→-3ft-roll--desktop-chrome/test-failed-1.png"
                        },
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-Nest-9e0e4-qft-pc-narrow-3-→-3ft-roll--desktop-chrome/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/printing.spec.js",
                        "column": 16,
                        "line": 162
                      }
                    },
                    {
                      "workerIndex": 261,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 8364,
                      "error": {
                        "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n",
                        "stack": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n    at /var/www/360lm/tests/printing.spec.js:162:16",
                        "location": {
                          "file": "/var/www/360lm/tests/printing.spec.js",
                          "column": 16,
                          "line": 162
                        },
                        "snippet": "  160 |     await injectSession(page);\n  161 |     await page.goto(BASE_URL);\n> 162 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n      |                ^\n  163 |     await page.locator('text=Start New Nesting').click();\n  164 |     await page.waitForSelector('#screen-add.active', { timeout: 4000 });\n  165 |   });"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/printing.spec.js",
                            "column": 16,
                            "line": 162
                          },
                          "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n\n  160 |     await injectSession(page);\n  161 |     await page.goto(BASE_URL);\n> 162 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n      |                ^\n  163 |     await page.locator('text=Start New Nesting').click();\n  164 |     await page.waitForSelector('#screen-add.active', { timeout: 4000 });\n  165 |   });\n    at /var/www/360lm/tests/printing.spec.js:162:16"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 1,
                      "startTime": "2026-07-12T13:27:09.382Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "screenshot",
                          "contentType": "image/png",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-Nest-9e0e4-qft-pc-narrow-3-→-3ft-roll--desktop-chrome-retry1/test-failed-1.png"
                        },
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-Nest-9e0e4-qft-pc-narrow-3-→-3ft-roll--desktop-chrome-retry1/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/printing.spec.js",
                        "column": 16,
                        "line": 162
                      }
                    }
                  ],
                  "status": "unexpected"
                }
              ],
              "id": "84f3bc84c047e9f9c0b8-47c53cb59920ccacabce",
              "file": "printing.spec.js",
              "line": 178,
              "column": 3
            },
            {
              "title": "7ft wide Flex → 2-roll (3+4ft) is available",
              "ok": false,
              "tags": [],
              "tests": [
                {
                  "timeout": 30000,
                  "annotations": [],
                  "expectedStatus": "passed",
                  "projectId": "desktop-chrome",
                  "projectName": "desktop-chrome",
                  "results": [
                    {
                      "workerIndex": 262,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 8306,
                      "error": {
                        "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n",
                        "stack": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n    at /var/www/360lm/tests/printing.spec.js:162:16",
                        "location": {
                          "file": "/var/www/360lm/tests/printing.spec.js",
                          "column": 16,
                          "line": 162
                        },
                        "snippet": "  160 |     await injectSession(page);\n  161 |     await page.goto(BASE_URL);\n> 162 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n      |                ^\n  163 |     await page.locator('text=Start New Nesting').click();\n  164 |     await page.waitForSelector('#screen-add.active', { timeout: 4000 });\n  165 |   });"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/printing.spec.js",
                            "column": 16,
                            "line": 162
                          },
                          "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n\n  160 |     await injectSession(page);\n  161 |     await page.goto(BASE_URL);\n> 162 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n      |                ^\n  163 |     await page.locator('text=Start New Nesting').click();\n  164 |     await page.waitForSelector('#screen-add.active', { timeout: 4000 });\n  165 |   });\n    at /var/www/360lm/tests/printing.spec.js:162:16"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 0,
                      "startTime": "2026-07-12T13:27:18.873Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "screenshot",
                          "contentType": "image/png",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-Nest-a3328-→-2-roll-3-4ft-is-available-desktop-chrome/test-failed-1.png"
                        },
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-Nest-a3328-→-2-roll-3-4ft-is-available-desktop-chrome/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/printing.spec.js",
                        "column": 16,
                        "line": 162
                      }
                    },
                    {
                      "workerIndex": 263,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 8297,
                      "error": {
                        "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n",
                        "stack": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n    at /var/www/360lm/tests/printing.spec.js:162:16",
                        "location": {
                          "file": "/var/www/360lm/tests/printing.spec.js",
                          "column": 16,
                          "line": 162
                        },
                        "snippet": "  160 |     await injectSession(page);\n  161 |     await page.goto(BASE_URL);\n> 162 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n      |                ^\n  163 |     await page.locator('text=Start New Nesting').click();\n  164 |     await page.waitForSelector('#screen-add.active', { timeout: 4000 });\n  165 |   });"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/printing.spec.js",
                            "column": 16,
                            "line": 162
                          },
                          "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n\n  160 |     await injectSession(page);\n  161 |     await page.goto(BASE_URL);\n> 162 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n      |                ^\n  163 |     await page.locator('text=Start New Nesting').click();\n  164 |     await page.waitForSelector('#screen-add.active', { timeout: 4000 });\n  165 |   });\n    at /var/www/360lm/tests/printing.spec.js:162:16"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 1,
                      "startTime": "2026-07-12T13:27:28.273Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "screenshot",
                          "contentType": "image/png",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-Nest-a3328-→-2-roll-3-4ft-is-available-desktop-chrome-retry1/test-failed-1.png"
                        },
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-Nest-a3328-→-2-roll-3-4ft-is-available-desktop-chrome-retry1/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/printing.spec.js",
                        "column": 16,
                        "line": 162
                      }
                    }
                  ],
                  "status": "unexpected"
                }
              ],
              "id": "84f3bc84c047e9f9c0b8-5db5e7122f08c65c6e5d",
              "file": "printing.spec.js",
              "line": 193,
              "column": 3
            },
            {
              "title": "item >5ft shows outsource/join buttons",
              "ok": false,
              "tags": [],
              "tests": [
                {
                  "timeout": 30000,
                  "annotations": [],
                  "expectedStatus": "passed",
                  "projectId": "desktop-chrome",
                  "projectName": "desktop-chrome",
                  "results": [
                    {
                      "workerIndex": 264,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 8274,
                      "error": {
                        "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n",
                        "stack": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n    at /var/www/360lm/tests/printing.spec.js:162:16",
                        "location": {
                          "file": "/var/www/360lm/tests/printing.spec.js",
                          "column": 16,
                          "line": 162
                        },
                        "snippet": "  160 |     await injectSession(page);\n  161 |     await page.goto(BASE_URL);\n> 162 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n      |                ^\n  163 |     await page.locator('text=Start New Nesting').click();\n  164 |     await page.waitForSelector('#screen-add.active', { timeout: 4000 });\n  165 |   });"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/printing.spec.js",
                            "column": 16,
                            "line": 162
                          },
                          "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n\n  160 |     await injectSession(page);\n  161 |     await page.goto(BASE_URL);\n> 162 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n      |                ^\n  163 |     await page.locator('text=Start New Nesting').click();\n  164 |     await page.waitForSelector('#screen-add.active', { timeout: 4000 });\n  165 |   });\n    at /var/www/360lm/tests/printing.spec.js:162:16"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 0,
                      "startTime": "2026-07-12T13:27:37.729Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "screenshot",
                          "contentType": "image/png",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-Nest-642e7-hows-outsource-join-buttons-desktop-chrome/test-failed-1.png"
                        },
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-Nest-642e7-hows-outsource-join-buttons-desktop-chrome/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/printing.spec.js",
                        "column": 16,
                        "line": 162
                      }
                    },
                    {
                      "workerIndex": 265,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 8288,
                      "error": {
                        "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n",
                        "stack": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n    at /var/www/360lm/tests/printing.spec.js:162:16",
                        "location": {
                          "file": "/var/www/360lm/tests/printing.spec.js",
                          "column": 16,
                          "line": 162
                        },
                        "snippet": "  160 |     await injectSession(page);\n  161 |     await page.goto(BASE_URL);\n> 162 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n      |                ^\n  163 |     await page.locator('text=Start New Nesting').click();\n  164 |     await page.waitForSelector('#screen-add.active', { timeout: 4000 });\n  165 |   });"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/printing.spec.js",
                            "column": 16,
                            "line": 162
                          },
                          "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n\n  160 |     await injectSession(page);\n  161 |     await page.goto(BASE_URL);\n> 162 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n      |                ^\n  163 |     await page.locator('text=Start New Nesting').click();\n  164 |     await page.waitForSelector('#screen-add.active', { timeout: 4000 });\n  165 |   });\n    at /var/www/360lm/tests/printing.spec.js:162:16"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 1,
                      "startTime": "2026-07-12T13:27:47.115Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "screenshot",
                          "contentType": "image/png",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-Nest-642e7-hows-outsource-join-buttons-desktop-chrome-retry1/test-failed-1.png"
                        },
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-Nest-642e7-hows-outsource-join-buttons-desktop-chrome-retry1/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/printing.spec.js",
                        "column": 16,
                        "line": 162
                      }
                    }
                  ],
                  "status": "unexpected"
                }
              ],
              "id": "84f3bc84c047e9f9c0b8-8c7c13efddd6faf596bd",
              "file": "printing.spec.js",
              "line": 207,
              "column": 3
            },
            {
              "title": "gang plan groups same roll-width items together",
              "ok": false,
              "tags": [],
              "tests": [
                {
                  "timeout": 30000,
                  "annotations": [],
                  "expectedStatus": "passed",
                  "projectId": "desktop-chrome",
                  "projectName": "desktop-chrome",
                  "results": [
                    {
                      "workerIndex": 266,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 8311,
                      "error": {
                        "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n",
                        "stack": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n    at /var/www/360lm/tests/printing.spec.js:162:16",
                        "location": {
                          "file": "/var/www/360lm/tests/printing.spec.js",
                          "column": 16,
                          "line": 162
                        },
                        "snippet": "  160 |     await injectSession(page);\n  161 |     await page.goto(BASE_URL);\n> 162 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n      |                ^\n  163 |     await page.locator('text=Start New Nesting').click();\n  164 |     await page.waitForSelector('#screen-add.active', { timeout: 4000 });\n  165 |   });"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/printing.spec.js",
                            "column": 16,
                            "line": 162
                          },
                          "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n\n  160 |     await injectSession(page);\n  161 |     await page.goto(BASE_URL);\n> 162 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n      |                ^\n  163 |     await page.locator('text=Start New Nesting').click();\n  164 |     await page.waitForSelector('#screen-add.active', { timeout: 4000 });\n  165 |   });\n    at /var/www/360lm/tests/printing.spec.js:162:16"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 0,
                      "startTime": "2026-07-12T13:27:56.503Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "screenshot",
                          "contentType": "image/png",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-Nest-bb2e2-e-roll-width-items-together-desktop-chrome/test-failed-1.png"
                        },
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-Nest-bb2e2-e-roll-width-items-together-desktop-chrome/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/printing.spec.js",
                        "column": 16,
                        "line": 162
                      }
                    },
                    {
                      "workerIndex": 267,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 8318,
                      "error": {
                        "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n",
                        "stack": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n    at /var/www/360lm/tests/printing.spec.js:162:16",
                        "location": {
                          "file": "/var/www/360lm/tests/printing.spec.js",
                          "column": 16,
                          "line": 162
                        },
                        "snippet": "  160 |     await injectSession(page);\n  161 |     await page.goto(BASE_URL);\n> 162 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n      |                ^\n  163 |     await page.locator('text=Start New Nesting').click();\n  164 |     await page.waitForSelector('#screen-add.active', { timeout: 4000 });\n  165 |   });"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/printing.spec.js",
                            "column": 16,
                            "line": 162
                          },
                          "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n\n  160 |     await injectSession(page);\n  161 |     await page.goto(BASE_URL);\n> 162 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n      |                ^\n  163 |     await page.locator('text=Start New Nesting').click();\n  164 |     await page.waitForSelector('#screen-add.active', { timeout: 4000 });\n  165 |   });\n    at /var/www/360lm/tests/printing.spec.js:162:16"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 1,
                      "startTime": "2026-07-12T13:28:06.172Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "screenshot",
                          "contentType": "image/png",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-Nest-bb2e2-e-roll-width-items-together-desktop-chrome-retry1/test-failed-1.png"
                        },
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-Nest-bb2e2-e-roll-width-items-together-desktop-chrome-retry1/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/printing.spec.js",
                        "column": 16,
                        "line": 162
                      }
                    }
                  ],
                  "status": "unexpected"
                }
              ],
              "id": "84f3bc84c047e9f9c0b8-484a882ae5569bbd49d4",
              "file": "printing.spec.js",
              "line": 220,
              "column": 3
            },
            {
              "title": "Flex and Vinyl items split into separate groups",
              "ok": false,
              "tags": [],
              "tests": [
                {
                  "timeout": 30000,
                  "annotations": [],
                  "expectedStatus": "passed",
                  "projectId": "desktop-chrome",
                  "projectName": "desktop-chrome",
                  "results": [
                    {
                      "workerIndex": 268,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 8284,
                      "error": {
                        "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n",
                        "stack": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n    at /var/www/360lm/tests/printing.spec.js:162:16",
                        "location": {
                          "file": "/var/www/360lm/tests/printing.spec.js",
                          "column": 16,
                          "line": 162
                        },
                        "snippet": "  160 |     await injectSession(page);\n  161 |     await page.goto(BASE_URL);\n> 162 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n      |                ^\n  163 |     await page.locator('text=Start New Nesting').click();\n  164 |     await page.waitForSelector('#screen-add.active', { timeout: 4000 });\n  165 |   });"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/printing.spec.js",
                            "column": 16,
                            "line": 162
                          },
                          "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n\n  160 |     await injectSession(page);\n  161 |     await page.goto(BASE_URL);\n> 162 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n      |                ^\n  163 |     await page.locator('text=Start New Nesting').click();\n  164 |     await page.waitForSelector('#screen-add.active', { timeout: 4000 });\n  165 |   });\n    at /var/www/360lm/tests/printing.spec.js:162:16"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 0,
                      "startTime": "2026-07-12T13:28:15.646Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "screenshot",
                          "contentType": "image/png",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-Nest-bfd73--split-into-separate-groups-desktop-chrome/test-failed-1.png"
                        },
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-Nest-bfd73--split-into-separate-groups-desktop-chrome/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/printing.spec.js",
                        "column": 16,
                        "line": 162
                      }
                    },
                    {
                      "workerIndex": 269,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 8260,
                      "error": {
                        "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n",
                        "stack": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n    at /var/www/360lm/tests/printing.spec.js:162:16",
                        "location": {
                          "file": "/var/www/360lm/tests/printing.spec.js",
                          "column": 16,
                          "line": 162
                        },
                        "snippet": "  160 |     await injectSession(page);\n  161 |     await page.goto(BASE_URL);\n> 162 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n      |                ^\n  163 |     await page.locator('text=Start New Nesting').click();\n  164 |     await page.waitForSelector('#screen-add.active', { timeout: 4000 });\n  165 |   });"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/printing.spec.js",
                            "column": 16,
                            "line": 162
                          },
                          "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n\n  160 |     await injectSession(page);\n  161 |     await page.goto(BASE_URL);\n> 162 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n      |                ^\n  163 |     await page.locator('text=Start New Nesting').click();\n  164 |     await page.waitForSelector('#screen-add.active', { timeout: 4000 });\n  165 |   });\n    at /var/www/360lm/tests/printing.spec.js:162:16"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 1,
                      "startTime": "2026-07-12T13:28:25.034Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "screenshot",
                          "contentType": "image/png",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-Nest-bfd73--split-into-separate-groups-desktop-chrome-retry1/test-failed-1.png"
                        },
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-Nest-bfd73--split-into-separate-groups-desktop-chrome-retry1/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/printing.spec.js",
                        "column": 16,
                        "line": 162
                      }
                    }
                  ],
                  "status": "unexpected"
                }
              ],
              "id": "84f3bc84c047e9f9c0b8-4419a71daf3798747a1f",
              "file": "printing.spec.js",
              "line": 234,
              "column": 3
            },
            {
              "title": "Save button opens save modal",
              "ok": false,
              "tags": [],
              "tests": [
                {
                  "timeout": 30000,
                  "annotations": [],
                  "expectedStatus": "passed",
                  "projectId": "desktop-chrome",
                  "projectName": "desktop-chrome",
                  "results": [
                    {
                      "workerIndex": 270,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 8320,
                      "error": {
                        "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n",
                        "stack": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n    at /var/www/360lm/tests/printing.spec.js:162:16",
                        "location": {
                          "file": "/var/www/360lm/tests/printing.spec.js",
                          "column": 16,
                          "line": 162
                        },
                        "snippet": "  160 |     await injectSession(page);\n  161 |     await page.goto(BASE_URL);\n> 162 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n      |                ^\n  163 |     await page.locator('text=Start New Nesting').click();\n  164 |     await page.waitForSelector('#screen-add.active', { timeout: 4000 });\n  165 |   });"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/printing.spec.js",
                            "column": 16,
                            "line": 162
                          },
                          "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n\n  160 |     await injectSession(page);\n  161 |     await page.goto(BASE_URL);\n> 162 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n      |                ^\n  163 |     await page.locator('text=Start New Nesting').click();\n  164 |     await page.waitForSelector('#screen-add.active', { timeout: 4000 });\n  165 |   });\n    at /var/www/360lm/tests/printing.spec.js:162:16"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 0,
                      "startTime": "2026-07-12T13:28:34.543Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "screenshot",
                          "contentType": "image/png",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-Nest-449f8-ave-button-opens-save-modal-desktop-chrome/test-failed-1.png"
                        },
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-Nest-449f8-ave-button-opens-save-modal-desktop-chrome/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/printing.spec.js",
                        "column": 16,
                        "line": 162
                      }
                    },
                    {
                      "workerIndex": 271,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 8342,
                      "error": {
                        "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n",
                        "stack": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n    at /var/www/360lm/tests/printing.spec.js:162:16",
                        "location": {
                          "file": "/var/www/360lm/tests/printing.spec.js",
                          "column": 16,
                          "line": 162
                        },
                        "snippet": "  160 |     await injectSession(page);\n  161 |     await page.goto(BASE_URL);\n> 162 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n      |                ^\n  163 |     await page.locator('text=Start New Nesting').click();\n  164 |     await page.waitForSelector('#screen-add.active', { timeout: 4000 });\n  165 |   });"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/printing.spec.js",
                            "column": 16,
                            "line": 162
                          },
                          "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n\n  160 |     await injectSession(page);\n  161 |     await page.goto(BASE_URL);\n> 162 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n      |                ^\n  163 |     await page.locator('text=Start New Nesting').click();\n  164 |     await page.waitForSelector('#screen-add.active', { timeout: 4000 });\n  165 |   });\n    at /var/www/360lm/tests/printing.spec.js:162:16"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 1,
                      "startTime": "2026-07-12T13:28:44.082Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "screenshot",
                          "contentType": "image/png",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-Nest-449f8-ave-button-opens-save-modal-desktop-chrome-retry1/test-failed-1.png"
                        },
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-Nest-449f8-ave-button-opens-save-modal-desktop-chrome-retry1/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/printing.spec.js",
                        "column": 16,
                        "line": 162
                      }
                    }
                  ],
                  "status": "unexpected"
                }
              ],
              "id": "84f3bc84c047e9f9c0b8-46cfaee8b74a0d61cc70",
              "file": "printing.spec.js",
              "line": 242,
              "column": 3
            },
            {
              "title": "AI review button visible after computing nest",
              "ok": false,
              "tags": [],
              "tests": [
                {
                  "timeout": 30000,
                  "annotations": [],
                  "expectedStatus": "passed",
                  "projectId": "desktop-chrome",
                  "projectName": "desktop-chrome",
                  "results": [
                    {
                      "workerIndex": 272,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 8341,
                      "error": {
                        "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n",
                        "stack": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n    at /var/www/360lm/tests/printing.spec.js:162:16",
                        "location": {
                          "file": "/var/www/360lm/tests/printing.spec.js",
                          "column": 16,
                          "line": 162
                        },
                        "snippet": "  160 |     await injectSession(page);\n  161 |     await page.goto(BASE_URL);\n> 162 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n      |                ^\n  163 |     await page.locator('text=Start New Nesting').click();\n  164 |     await page.waitForSelector('#screen-add.active', { timeout: 4000 });\n  165 |   });"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/printing.spec.js",
                            "column": 16,
                            "line": 162
                          },
                          "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n\n  160 |     await injectSession(page);\n  161 |     await page.goto(BASE_URL);\n> 162 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n      |                ^\n  163 |     await page.locator('text=Start New Nesting').click();\n  164 |     await page.waitForSelector('#screen-add.active', { timeout: 4000 });\n  165 |   });\n    at /var/www/360lm/tests/printing.spec.js:162:16"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 0,
                      "startTime": "2026-07-12T13:28:53.610Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "screenshot",
                          "contentType": "image/png",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-Nest-62185-isible-after-computing-nest-desktop-chrome/test-failed-1.png"
                        },
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-Nest-62185-isible-after-computing-nest-desktop-chrome/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/printing.spec.js",
                        "column": 16,
                        "line": 162
                      }
                    },
                    {
                      "workerIndex": 273,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 8291,
                      "error": {
                        "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n",
                        "stack": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n    at /var/www/360lm/tests/printing.spec.js:162:16",
                        "location": {
                          "file": "/var/www/360lm/tests/printing.spec.js",
                          "column": 16,
                          "line": 162
                        },
                        "snippet": "  160 |     await injectSession(page);\n  161 |     await page.goto(BASE_URL);\n> 162 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n      |                ^\n  163 |     await page.locator('text=Start New Nesting').click();\n  164 |     await page.waitForSelector('#screen-add.active', { timeout: 4000 });\n  165 |   });"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/printing.spec.js",
                            "column": 16,
                            "line": 162
                          },
                          "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n\n  160 |     await injectSession(page);\n  161 |     await page.goto(BASE_URL);\n> 162 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n      |                ^\n  163 |     await page.locator('text=Start New Nesting').click();\n  164 |     await page.waitForSelector('#screen-add.active', { timeout: 4000 });\n  165 |   });\n    at /var/www/360lm/tests/printing.spec.js:162:16"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 1,
                      "startTime": "2026-07-12T13:29:03.365Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "screenshot",
                          "contentType": "image/png",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-Nest-62185-isible-after-computing-nest-desktop-chrome-retry1/test-failed-1.png"
                        },
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-Nest-62185-isible-after-computing-nest-desktop-chrome-retry1/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/printing.spec.js",
                        "column": 16,
                        "line": 162
                      }
                    }
                  ],
                  "status": "unexpected"
                }
              ],
              "id": "84f3bc84c047e9f9c0b8-2b6272c63008b807964a",
              "file": "printing.spec.js",
              "line": 250,
              "column": 3
            },
            {
              "title": "AI review button opens chat modal",
              "ok": false,
              "tags": [],
              "tests": [
                {
                  "timeout": 30000,
                  "annotations": [],
                  "expectedStatus": "passed",
                  "projectId": "desktop-chrome",
                  "projectName": "desktop-chrome",
                  "results": [
                    {
                      "workerIndex": 274,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 8293,
                      "error": {
                        "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n",
                        "stack": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n    at /var/www/360lm/tests/printing.spec.js:162:16",
                        "location": {
                          "file": "/var/www/360lm/tests/printing.spec.js",
                          "column": 16,
                          "line": 162
                        },
                        "snippet": "  160 |     await injectSession(page);\n  161 |     await page.goto(BASE_URL);\n> 162 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n      |                ^\n  163 |     await page.locator('text=Start New Nesting').click();\n  164 |     await page.waitForSelector('#screen-add.active', { timeout: 4000 });\n  165 |   });"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/printing.spec.js",
                            "column": 16,
                            "line": 162
                          },
                          "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n\n  160 |     await injectSession(page);\n  161 |     await page.goto(BASE_URL);\n> 162 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n      |                ^\n  163 |     await page.locator('text=Start New Nesting').click();\n  164 |     await page.waitForSelector('#screen-add.active', { timeout: 4000 });\n  165 |   });\n    at /var/www/360lm/tests/printing.spec.js:162:16"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 0,
                      "startTime": "2026-07-12T13:29:12.781Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "screenshot",
                          "contentType": "image/png",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-Nest-0f364-iew-button-opens-chat-modal-desktop-chrome/test-failed-1.png"
                        },
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-Nest-0f364-iew-button-opens-chat-modal-desktop-chrome/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/printing.spec.js",
                        "column": 16,
                        "line": 162
                      }
                    },
                    {
                      "workerIndex": 275,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 8286,
                      "error": {
                        "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n",
                        "stack": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n    at /var/www/360lm/tests/printing.spec.js:162:16",
                        "location": {
                          "file": "/var/www/360lm/tests/printing.spec.js",
                          "column": 16,
                          "line": 162
                        },
                        "snippet": "  160 |     await injectSession(page);\n  161 |     await page.goto(BASE_URL);\n> 162 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n      |                ^\n  163 |     await page.locator('text=Start New Nesting').click();\n  164 |     await page.waitForSelector('#screen-add.active', { timeout: 4000 });\n  165 |   });"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/printing.spec.js",
                            "column": 16,
                            "line": 162
                          },
                          "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n\n  160 |     await injectSession(page);\n  161 |     await page.goto(BASE_URL);\n> 162 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n      |                ^\n  163 |     await page.locator('text=Start New Nesting').click();\n  164 |     await page.waitForSelector('#screen-add.active', { timeout: 4000 });\n  165 |   });\n    at /var/www/360lm/tests/printing.spec.js:162:16"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 1,
                      "startTime": "2026-07-12T13:29:22.260Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "screenshot",
                          "contentType": "image/png",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-Nest-0f364-iew-button-opens-chat-modal-desktop-chrome-retry1/test-failed-1.png"
                        },
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-Nest-0f364-iew-button-opens-chat-modal-desktop-chrome-retry1/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/printing.spec.js",
                        "column": 16,
                        "line": 162
                      }
                    }
                  ],
                  "status": "unexpected"
                }
              ],
              "id": "84f3bc84c047e9f9c0b8-0f5371b95d2c82a0779f",
              "file": "printing.spec.js",
              "line": 258,
              "column": 3
            },
            {
              "title": "Copy plan button visible on nest screen",
              "ok": false,
              "tags": [],
              "tests": [
                {
                  "timeout": 30000,
                  "annotations": [],
                  "expectedStatus": "passed",
                  "projectId": "desktop-chrome",
                  "projectName": "desktop-chrome",
                  "results": [
                    {
                      "workerIndex": 276,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 8331,
                      "error": {
                        "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n",
                        "stack": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n    at /var/www/360lm/tests/printing.spec.js:162:16",
                        "location": {
                          "file": "/var/www/360lm/tests/printing.spec.js",
                          "column": 16,
                          "line": 162
                        },
                        "snippet": "  160 |     await injectSession(page);\n  161 |     await page.goto(BASE_URL);\n> 162 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n      |                ^\n  163 |     await page.locator('text=Start New Nesting').click();\n  164 |     await page.waitForSelector('#screen-add.active', { timeout: 4000 });\n  165 |   });"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/printing.spec.js",
                            "column": 16,
                            "line": 162
                          },
                          "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n\n  160 |     await injectSession(page);\n  161 |     await page.goto(BASE_URL);\n> 162 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n      |                ^\n  163 |     await page.locator('text=Start New Nesting').click();\n  164 |     await page.waitForSelector('#screen-add.active', { timeout: 4000 });\n  165 |   });\n    at /var/www/360lm/tests/printing.spec.js:162:16"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 0,
                      "startTime": "2026-07-12T13:29:31.634Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "screenshot",
                          "contentType": "image/png",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-Nest-cbdfe-tton-visible-on-nest-screen-desktop-chrome/test-failed-1.png"
                        },
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-Nest-cbdfe-tton-visible-on-nest-screen-desktop-chrome/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/printing.spec.js",
                        "column": 16,
                        "line": 162
                      }
                    },
                    {
                      "workerIndex": 277,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 8369,
                      "error": {
                        "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n",
                        "stack": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n    at /var/www/360lm/tests/printing.spec.js:162:16",
                        "location": {
                          "file": "/var/www/360lm/tests/printing.spec.js",
                          "column": 16,
                          "line": 162
                        },
                        "snippet": "  160 |     await injectSession(page);\n  161 |     await page.goto(BASE_URL);\n> 162 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n      |                ^\n  163 |     await page.locator('text=Start New Nesting').click();\n  164 |     await page.waitForSelector('#screen-add.active', { timeout: 4000 });\n  165 |   });"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/printing.spec.js",
                            "column": 16,
                            "line": 162
                          },
                          "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n\n  160 |     await injectSession(page);\n  161 |     await page.goto(BASE_URL);\n> 162 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n      |                ^\n  163 |     await page.locator('text=Start New Nesting').click();\n  164 |     await page.waitForSelector('#screen-add.active', { timeout: 4000 });\n  165 |   });\n    at /var/www/360lm/tests/printing.spec.js:162:16"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 1,
                      "startTime": "2026-07-12T13:29:41.111Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "screenshot",
                          "contentType": "image/png",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-Nest-cbdfe-tton-visible-on-nest-screen-desktop-chrome-retry1/test-failed-1.png"
                        },
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-Nest-cbdfe-tton-visible-on-nest-screen-desktop-chrome-retry1/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/printing.spec.js",
                        "column": 16,
                        "line": 162
                      }
                    }
                  ],
                  "status": "unexpected"
                }
              ],
              "id": "84f3bc84c047e9f9c0b8-4d08a127616c207c0cef",
              "file": "printing.spec.js",
              "line": 268,
              "column": 3
            },
            {
              "title": "Copy plan writes text to clipboard and shows toast",
              "ok": false,
              "tags": [],
              "tests": [
                {
                  "timeout": 30000,
                  "annotations": [],
                  "expectedStatus": "passed",
                  "projectId": "desktop-chrome",
                  "projectName": "desktop-chrome",
                  "results": [
                    {
                      "workerIndex": 278,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 8339,
                      "error": {
                        "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n",
                        "stack": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n    at /var/www/360lm/tests/printing.spec.js:162:16",
                        "location": {
                          "file": "/var/www/360lm/tests/printing.spec.js",
                          "column": 16,
                          "line": 162
                        },
                        "snippet": "  160 |     await injectSession(page);\n  161 |     await page.goto(BASE_URL);\n> 162 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n      |                ^\n  163 |     await page.locator('text=Start New Nesting').click();\n  164 |     await page.waitForSelector('#screen-add.active', { timeout: 4000 });\n  165 |   });"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/printing.spec.js",
                            "column": 16,
                            "line": 162
                          },
                          "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n\n  160 |     await injectSession(page);\n  161 |     await page.goto(BASE_URL);\n> 162 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n      |                ^\n  163 |     await page.locator('text=Start New Nesting').click();\n  164 |     await page.waitForSelector('#screen-add.active', { timeout: 4000 });\n  165 |   });\n    at /var/www/360lm/tests/printing.spec.js:162:16"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 0,
                      "startTime": "2026-07-12T13:29:50.650Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "screenshot",
                          "contentType": "image/png",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-Nest-25769-o-clipboard-and-shows-toast-desktop-chrome/test-failed-1.png"
                        },
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-Nest-25769-o-clipboard-and-shows-toast-desktop-chrome/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/printing.spec.js",
                        "column": 16,
                        "line": 162
                      }
                    },
                    {
                      "workerIndex": 279,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 8558,
                      "error": {
                        "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n",
                        "stack": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n    at /var/www/360lm/tests/printing.spec.js:162:16",
                        "location": {
                          "file": "/var/www/360lm/tests/printing.spec.js",
                          "column": 16,
                          "line": 162
                        },
                        "snippet": "  160 |     await injectSession(page);\n  161 |     await page.goto(BASE_URL);\n> 162 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n      |                ^\n  163 |     await page.locator('text=Start New Nesting').click();\n  164 |     await page.waitForSelector('#screen-add.active', { timeout: 4000 });\n  165 |   });"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/printing.spec.js",
                            "column": 16,
                            "line": 162
                          },
                          "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n\n  160 |     await injectSession(page);\n  161 |     await page.goto(BASE_URL);\n> 162 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n      |                ^\n  163 |     await page.locator('text=Start New Nesting').click();\n  164 |     await page.waitForSelector('#screen-add.active', { timeout: 4000 });\n  165 |   });\n    at /var/www/360lm/tests/printing.spec.js:162:16"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 1,
                      "startTime": "2026-07-12T13:30:00.155Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "screenshot",
                          "contentType": "image/png",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-Nest-25769-o-clipboard-and-shows-toast-desktop-chrome-retry1/test-failed-1.png"
                        },
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-Nest-25769-o-clipboard-and-shows-toast-desktop-chrome-retry1/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/printing.spec.js",
                        "column": 16,
                        "line": 162
                      }
                    }
                  ],
                  "status": "unexpected"
                }
              ],
              "id": "84f3bc84c047e9f9c0b8-0adb747481eb55b6290d",
              "file": "printing.spec.js",
              "line": 275,
              "column": 3
            }
          ]
        },
        {
          "title": "Print Nest — Excel/AI Tab",
          "file": "printing.spec.js",
          "line": 291,
          "column": 6,
          "specs": [
            {
              "title": "Excel tab is active by default on add screen",
              "ok": false,
              "tags": [],
              "tests": [
                {
                  "timeout": 30000,
                  "annotations": [],
                  "expectedStatus": "passed",
                  "projectId": "android-chrome",
                  "projectName": "android-chrome",
                  "results": [
                    {
                      "workerIndex": 104,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 8613,
                      "error": {
                        "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n",
                        "stack": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n    at /var/www/360lm/tests/printing.spec.js:295:16",
                        "location": {
                          "file": "/var/www/360lm/tests/printing.spec.js",
                          "column": 16,
                          "line": 295
                        },
                        "snippet": "  293 |     await injectSession(page);\n  294 |     await page.goto(BASE_URL);\n> 295 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n      |                ^\n  296 |     await page.locator('text=Start New Nesting').click();\n  297 |     await page.waitForSelector('#screen-add.active', { timeout: 4000 });\n  298 |   });"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/printing.spec.js",
                            "column": 16,
                            "line": 295
                          },
                          "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n\n  293 |     await injectSession(page);\n  294 |     await page.goto(BASE_URL);\n> 295 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n      |                ^\n  296 |     await page.locator('text=Start New Nesting').click();\n  297 |     await page.waitForSelector('#screen-add.active', { timeout: 4000 });\n  298 |   });\n    at /var/www/360lm/tests/printing.spec.js:295:16"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 0,
                      "startTime": "2026-07-12T13:09:53.864Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "screenshot",
                          "contentType": "image/png",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-Exce-424b8-ve-by-default-on-add-screen-android-chrome/test-failed-1.png"
                        },
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-Exce-424b8-ve-by-default-on-add-screen-android-chrome/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/printing.spec.js",
                        "column": 16,
                        "line": 295
                      }
                    },
                    {
                      "workerIndex": 105,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 8482,
                      "error": {
                        "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n",
                        "stack": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n    at /var/www/360lm/tests/printing.spec.js:295:16",
                        "location": {
                          "file": "/var/www/360lm/tests/printing.spec.js",
                          "column": 16,
                          "line": 295
                        },
                        "snippet": "  293 |     await injectSession(page);\n  294 |     await page.goto(BASE_URL);\n> 295 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n      |                ^\n  296 |     await page.locator('text=Start New Nesting').click();\n  297 |     await page.waitForSelector('#screen-add.active', { timeout: 4000 });\n  298 |   });"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/printing.spec.js",
                            "column": 16,
                            "line": 295
                          },
                          "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n\n  293 |     await injectSession(page);\n  294 |     await page.goto(BASE_URL);\n> 295 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n      |                ^\n  296 |     await page.locator('text=Start New Nesting').click();\n  297 |     await page.waitForSelector('#screen-add.active', { timeout: 4000 });\n  298 |   });\n    at /var/www/360lm/tests/printing.spec.js:295:16"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 1,
                      "startTime": "2026-07-12T13:10:04.614Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "screenshot",
                          "contentType": "image/png",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-Exce-424b8-ve-by-default-on-add-screen-android-chrome-retry1/test-failed-1.png"
                        },
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-Exce-424b8-ve-by-default-on-add-screen-android-chrome-retry1/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/printing.spec.js",
                        "column": 16,
                        "line": 295
                      }
                    }
                  ],
                  "status": "unexpected"
                }
              ],
              "id": "84f3bc84c047e9f9c0b8-472a3a2ce3bb0bf75b89",
              "file": "printing.spec.js",
              "line": 300,
              "column": 3
            },
            {
              "title": "Excel/AI tab shows upload area",
              "ok": false,
              "tags": [],
              "tests": [
                {
                  "timeout": 30000,
                  "annotations": [],
                  "expectedStatus": "passed",
                  "projectId": "android-chrome",
                  "projectName": "android-chrome",
                  "results": [
                    {
                      "workerIndex": 106,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 8403,
                      "error": {
                        "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n",
                        "stack": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n    at /var/www/360lm/tests/printing.spec.js:295:16",
                        "location": {
                          "file": "/var/www/360lm/tests/printing.spec.js",
                          "column": 16,
                          "line": 295
                        },
                        "snippet": "  293 |     await injectSession(page);\n  294 |     await page.goto(BASE_URL);\n> 295 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n      |                ^\n  296 |     await page.locator('text=Start New Nesting').click();\n  297 |     await page.waitForSelector('#screen-add.active', { timeout: 4000 });\n  298 |   });"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/printing.spec.js",
                            "column": 16,
                            "line": 295
                          },
                          "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n\n  293 |     await injectSession(page);\n  294 |     await page.goto(BASE_URL);\n> 295 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n      |                ^\n  296 |     await page.locator('text=Start New Nesting').click();\n  297 |     await page.waitForSelector('#screen-add.active', { timeout: 4000 });\n  298 |   });\n    at /var/www/360lm/tests/printing.spec.js:295:16"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 0,
                      "startTime": "2026-07-12T13:10:14.524Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "screenshot",
                          "contentType": "image/png",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-Exce-d28a5-el-AI-tab-shows-upload-area-android-chrome/test-failed-1.png"
                        },
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-Exce-d28a5-el-AI-tab-shows-upload-area-android-chrome/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/printing.spec.js",
                        "column": 16,
                        "line": 295
                      }
                    },
                    {
                      "workerIndex": 107,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 8305,
                      "error": {
                        "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n",
                        "stack": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n    at /var/www/360lm/tests/printing.spec.js:295:16",
                        "location": {
                          "file": "/var/www/360lm/tests/printing.spec.js",
                          "column": 16,
                          "line": 295
                        },
                        "snippet": "  293 |     await injectSession(page);\n  294 |     await page.goto(BASE_URL);\n> 295 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n      |                ^\n  296 |     await page.locator('text=Start New Nesting').click();\n  297 |     await page.waitForSelector('#screen-add.active', { timeout: 4000 });\n  298 |   });"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/printing.spec.js",
                            "column": 16,
                            "line": 295
                          },
                          "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n\n  293 |     await injectSession(page);\n  294 |     await page.goto(BASE_URL);\n> 295 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n      |                ^\n  296 |     await page.locator('text=Start New Nesting').click();\n  297 |     await page.waitForSelector('#screen-add.active', { timeout: 4000 });\n  298 |   });\n    at /var/www/360lm/tests/printing.spec.js:295:16"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 1,
                      "startTime": "2026-07-12T13:10:24.076Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "screenshot",
                          "contentType": "image/png",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-Exce-d28a5-el-AI-tab-shows-upload-area-android-chrome-retry1/test-failed-1.png"
                        },
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-Exce-d28a5-el-AI-tab-shows-upload-area-android-chrome-retry1/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/printing.spec.js",
                        "column": 16,
                        "line": 295
                      }
                    }
                  ],
                  "status": "unexpected"
                }
              ],
              "id": "84f3bc84c047e9f9c0b8-a1bf996ecafcd8c382c6",
              "file": "printing.spec.js",
              "line": 307,
              "column": 3
            },
            {
              "title": "Layout button present on nest screen",
              "ok": false,
              "tags": [],
              "tests": [
                {
                  "timeout": 30000,
                  "annotations": [],
                  "expectedStatus": "passed",
                  "projectId": "android-chrome",
                  "projectName": "android-chrome",
                  "results": [
                    {
                      "workerIndex": 108,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 8334,
                      "error": {
                        "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n",
                        "stack": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n    at /var/www/360lm/tests/printing.spec.js:295:16",
                        "location": {
                          "file": "/var/www/360lm/tests/printing.spec.js",
                          "column": 16,
                          "line": 295
                        },
                        "snippet": "  293 |     await injectSession(page);\n  294 |     await page.goto(BASE_URL);\n> 295 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n      |                ^\n  296 |     await page.locator('text=Start New Nesting').click();\n  297 |     await page.waitForSelector('#screen-add.active', { timeout: 4000 });\n  298 |   });"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/printing.spec.js",
                            "column": 16,
                            "line": 295
                          },
                          "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n\n  293 |     await injectSession(page);\n  294 |     await page.goto(BASE_URL);\n> 295 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n      |                ^\n  296 |     await page.locator('text=Start New Nesting').click();\n  297 |     await page.waitForSelector('#screen-add.active', { timeout: 4000 });\n  298 |   });\n    at /var/www/360lm/tests/printing.spec.js:295:16"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 0,
                      "startTime": "2026-07-12T13:10:33.580Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "screenshot",
                          "contentType": "image/png",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-Exce-99749-tton-present-on-nest-screen-android-chrome/test-failed-1.png"
                        },
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-Exce-99749-tton-present-on-nest-screen-android-chrome/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/printing.spec.js",
                        "column": 16,
                        "line": 295
                      }
                    },
                    {
                      "workerIndex": 109,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 8355,
                      "error": {
                        "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n",
                        "stack": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n    at /var/www/360lm/tests/printing.spec.js:295:16",
                        "location": {
                          "file": "/var/www/360lm/tests/printing.spec.js",
                          "column": 16,
                          "line": 295
                        },
                        "snippet": "  293 |     await injectSession(page);\n  294 |     await page.goto(BASE_URL);\n> 295 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n      |                ^\n  296 |     await page.locator('text=Start New Nesting').click();\n  297 |     await page.waitForSelector('#screen-add.active', { timeout: 4000 });\n  298 |   });"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/printing.spec.js",
                            "column": 16,
                            "line": 295
                          },
                          "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n\n  293 |     await injectSession(page);\n  294 |     await page.goto(BASE_URL);\n> 295 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n      |                ^\n  296 |     await page.locator('text=Start New Nesting').click();\n  297 |     await page.waitForSelector('#screen-add.active', { timeout: 4000 });\n  298 |   });\n    at /var/www/360lm/tests/printing.spec.js:295:16"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 1,
                      "startTime": "2026-07-12T13:10:43.091Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "screenshot",
                          "contentType": "image/png",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-Exce-99749-tton-present-on-nest-screen-android-chrome-retry1/test-failed-1.png"
                        },
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-Exce-99749-tton-present-on-nest-screen-android-chrome-retry1/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/printing.spec.js",
                        "column": 16,
                        "line": 295
                      }
                    }
                  ],
                  "status": "unexpected"
                }
              ],
              "id": "84f3bc84c047e9f9c0b8-f533009b19f0a1a2749a",
              "file": "printing.spec.js",
              "line": 312,
              "column": 3
            },
            {
              "title": "Compute button pulses when items exist",
              "ok": false,
              "tags": [],
              "tests": [
                {
                  "timeout": 30000,
                  "annotations": [],
                  "expectedStatus": "passed",
                  "projectId": "android-chrome",
                  "projectName": "android-chrome",
                  "results": [
                    {
                      "workerIndex": 110,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 8358,
                      "error": {
                        "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n",
                        "stack": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n    at /var/www/360lm/tests/printing.spec.js:295:16",
                        "location": {
                          "file": "/var/www/360lm/tests/printing.spec.js",
                          "column": 16,
                          "line": 295
                        },
                        "snippet": "  293 |     await injectSession(page);\n  294 |     await page.goto(BASE_URL);\n> 295 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n      |                ^\n  296 |     await page.locator('text=Start New Nesting').click();\n  297 |     await page.waitForSelector('#screen-add.active', { timeout: 4000 });\n  298 |   });"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/printing.spec.js",
                            "column": 16,
                            "line": 295
                          },
                          "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n\n  293 |     await injectSession(page);\n  294 |     await page.goto(BASE_URL);\n> 295 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n      |                ^\n  296 |     await page.locator('text=Start New Nesting').click();\n  297 |     await page.waitForSelector('#screen-add.active', { timeout: 4000 });\n  298 |   });\n    at /var/www/360lm/tests/printing.spec.js:295:16"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 0,
                      "startTime": "2026-07-12T13:10:52.596Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "screenshot",
                          "contentType": "image/png",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-Exce-5a943-ton-pulses-when-items-exist-android-chrome/test-failed-1.png"
                        },
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-Exce-5a943-ton-pulses-when-items-exist-android-chrome/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/printing.spec.js",
                        "column": 16,
                        "line": 295
                      }
                    },
                    {
                      "workerIndex": 111,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 8308,
                      "error": {
                        "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n",
                        "stack": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n    at /var/www/360lm/tests/printing.spec.js:295:16",
                        "location": {
                          "file": "/var/www/360lm/tests/printing.spec.js",
                          "column": 16,
                          "line": 295
                        },
                        "snippet": "  293 |     await injectSession(page);\n  294 |     await page.goto(BASE_URL);\n> 295 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n      |                ^\n  296 |     await page.locator('text=Start New Nesting').click();\n  297 |     await page.waitForSelector('#screen-add.active', { timeout: 4000 });\n  298 |   });"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/printing.spec.js",
                            "column": 16,
                            "line": 295
                          },
                          "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n\n  293 |     await injectSession(page);\n  294 |     await page.goto(BASE_URL);\n> 295 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n      |                ^\n  296 |     await page.locator('text=Start New Nesting').click();\n  297 |     await page.waitForSelector('#screen-add.active', { timeout: 4000 });\n  298 |   });\n    at /var/www/360lm/tests/printing.spec.js:295:16"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 1,
                      "startTime": "2026-07-12T13:11:03.082Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "screenshot",
                          "contentType": "image/png",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-Exce-5a943-ton-pulses-when-items-exist-android-chrome-retry1/test-failed-1.png"
                        },
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-Exce-5a943-ton-pulses-when-items-exist-android-chrome-retry1/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/printing.spec.js",
                        "column": 16,
                        "line": 295
                      }
                    }
                  ],
                  "status": "unexpected"
                }
              ],
              "id": "84f3bc84c047e9f9c0b8-164ab48d656211accbec",
              "file": "printing.spec.js",
              "line": 325,
              "column": 3
            },
            {
              "title": "Excel tab is active by default on add screen",
              "ok": false,
              "tags": [],
              "tests": [
                {
                  "timeout": 30000,
                  "annotations": [],
                  "expectedStatus": "passed",
                  "projectId": "desktop-chrome",
                  "projectName": "desktop-chrome",
                  "results": [
                    {
                      "workerIndex": 280,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 8498,
                      "error": {
                        "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n",
                        "stack": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n    at /var/www/360lm/tests/printing.spec.js:295:16",
                        "location": {
                          "file": "/var/www/360lm/tests/printing.spec.js",
                          "column": 16,
                          "line": 295
                        },
                        "snippet": "  293 |     await injectSession(page);\n  294 |     await page.goto(BASE_URL);\n> 295 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n      |                ^\n  296 |     await page.locator('text=Start New Nesting').click();\n  297 |     await page.waitForSelector('#screen-add.active', { timeout: 4000 });\n  298 |   });"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/printing.spec.js",
                            "column": 16,
                            "line": 295
                          },
                          "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n\n  293 |     await injectSession(page);\n  294 |     await page.goto(BASE_URL);\n> 295 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n      |                ^\n  296 |     await page.locator('text=Start New Nesting').click();\n  297 |     await page.waitForSelector('#screen-add.active', { timeout: 4000 });\n  298 |   });\n    at /var/www/360lm/tests/printing.spec.js:295:16"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 0,
                      "startTime": "2026-07-12T13:30:10.402Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "screenshot",
                          "contentType": "image/png",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-Exce-424b8-ve-by-default-on-add-screen-desktop-chrome/test-failed-1.png"
                        },
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-Exce-424b8-ve-by-default-on-add-screen-desktop-chrome/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/printing.spec.js",
                        "column": 16,
                        "line": 295
                      }
                    },
                    {
                      "workerIndex": 281,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 8306,
                      "error": {
                        "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n",
                        "stack": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n    at /var/www/360lm/tests/printing.spec.js:295:16",
                        "location": {
                          "file": "/var/www/360lm/tests/printing.spec.js",
                          "column": 16,
                          "line": 295
                        },
                        "snippet": "  293 |     await injectSession(page);\n  294 |     await page.goto(BASE_URL);\n> 295 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n      |                ^\n  296 |     await page.locator('text=Start New Nesting').click();\n  297 |     await page.waitForSelector('#screen-add.active', { timeout: 4000 });\n  298 |   });"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/printing.spec.js",
                            "column": 16,
                            "line": 295
                          },
                          "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n\n  293 |     await injectSession(page);\n  294 |     await page.goto(BASE_URL);\n> 295 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n      |                ^\n  296 |     await page.locator('text=Start New Nesting').click();\n  297 |     await page.waitForSelector('#screen-add.active', { timeout: 4000 });\n  298 |   });\n    at /var/www/360lm/tests/printing.spec.js:295:16"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 1,
                      "startTime": "2026-07-12T13:30:20.100Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "screenshot",
                          "contentType": "image/png",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-Exce-424b8-ve-by-default-on-add-screen-desktop-chrome-retry1/test-failed-1.png"
                        },
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-Exce-424b8-ve-by-default-on-add-screen-desktop-chrome-retry1/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/printing.spec.js",
                        "column": 16,
                        "line": 295
                      }
                    }
                  ],
                  "status": "unexpected"
                }
              ],
              "id": "84f3bc84c047e9f9c0b8-5d797116170cf6ab836e",
              "file": "printing.spec.js",
              "line": 300,
              "column": 3
            },
            {
              "title": "Excel/AI tab shows upload area",
              "ok": false,
              "tags": [],
              "tests": [
                {
                  "timeout": 30000,
                  "annotations": [],
                  "expectedStatus": "passed",
                  "projectId": "desktop-chrome",
                  "projectName": "desktop-chrome",
                  "results": [
                    {
                      "workerIndex": 282,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 8342,
                      "error": {
                        "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n",
                        "stack": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n    at /var/www/360lm/tests/printing.spec.js:295:16",
                        "location": {
                          "file": "/var/www/360lm/tests/printing.spec.js",
                          "column": 16,
                          "line": 295
                        },
                        "snippet": "  293 |     await injectSession(page);\n  294 |     await page.goto(BASE_URL);\n> 295 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n      |                ^\n  296 |     await page.locator('text=Start New Nesting').click();\n  297 |     await page.waitForSelector('#screen-add.active', { timeout: 4000 });\n  298 |   });"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/printing.spec.js",
                            "column": 16,
                            "line": 295
                          },
                          "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n\n  293 |     await injectSession(page);\n  294 |     await page.goto(BASE_URL);\n> 295 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n      |                ^\n  296 |     await page.locator('text=Start New Nesting').click();\n  297 |     await page.waitForSelector('#screen-add.active', { timeout: 4000 });\n  298 |   });\n    at /var/www/360lm/tests/printing.spec.js:295:16"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 0,
                      "startTime": "2026-07-12T13:30:29.650Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "screenshot",
                          "contentType": "image/png",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-Exce-d28a5-el-AI-tab-shows-upload-area-desktop-chrome/test-failed-1.png"
                        },
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-Exce-d28a5-el-AI-tab-shows-upload-area-desktop-chrome/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/printing.spec.js",
                        "column": 16,
                        "line": 295
                      }
                    },
                    {
                      "workerIndex": 283,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 8334,
                      "error": {
                        "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n",
                        "stack": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n    at /var/www/360lm/tests/printing.spec.js:295:16",
                        "location": {
                          "file": "/var/www/360lm/tests/printing.spec.js",
                          "column": 16,
                          "line": 295
                        },
                        "snippet": "  293 |     await injectSession(page);\n  294 |     await page.goto(BASE_URL);\n> 295 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n      |                ^\n  296 |     await page.locator('text=Start New Nesting').click();\n  297 |     await page.waitForSelector('#screen-add.active', { timeout: 4000 });\n  298 |   });"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/printing.spec.js",
                            "column": 16,
                            "line": 295
                          },
                          "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n\n  293 |     await injectSession(page);\n  294 |     await page.goto(BASE_URL);\n> 295 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n      |                ^\n  296 |     await page.locator('text=Start New Nesting').click();\n  297 |     await page.waitForSelector('#screen-add.active', { timeout: 4000 });\n  298 |   });\n    at /var/www/360lm/tests/printing.spec.js:295:16"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 1,
                      "startTime": "2026-07-12T13:30:39.243Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "screenshot",
                          "contentType": "image/png",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-Exce-d28a5-el-AI-tab-shows-upload-area-desktop-chrome-retry1/test-failed-1.png"
                        },
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-Exce-d28a5-el-AI-tab-shows-upload-area-desktop-chrome-retry1/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/printing.spec.js",
                        "column": 16,
                        "line": 295
                      }
                    }
                  ],
                  "status": "unexpected"
                }
              ],
              "id": "84f3bc84c047e9f9c0b8-5f8c798e8c9d22dd5e2e",
              "file": "printing.spec.js",
              "line": 307,
              "column": 3
            },
            {
              "title": "Layout button present on nest screen",
              "ok": false,
              "tags": [],
              "tests": [
                {
                  "timeout": 30000,
                  "annotations": [],
                  "expectedStatus": "passed",
                  "projectId": "desktop-chrome",
                  "projectName": "desktop-chrome",
                  "results": [
                    {
                      "workerIndex": 284,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 8287,
                      "error": {
                        "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n",
                        "stack": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n    at /var/www/360lm/tests/printing.spec.js:295:16",
                        "location": {
                          "file": "/var/www/360lm/tests/printing.spec.js",
                          "column": 16,
                          "line": 295
                        },
                        "snippet": "  293 |     await injectSession(page);\n  294 |     await page.goto(BASE_URL);\n> 295 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n      |                ^\n  296 |     await page.locator('text=Start New Nesting').click();\n  297 |     await page.waitForSelector('#screen-add.active', { timeout: 4000 });\n  298 |   });"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/printing.spec.js",
                            "column": 16,
                            "line": 295
                          },
                          "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n\n  293 |     await injectSession(page);\n  294 |     await page.goto(BASE_URL);\n> 295 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n      |                ^\n  296 |     await page.locator('text=Start New Nesting').click();\n  297 |     await page.waitForSelector('#screen-add.active', { timeout: 4000 });\n  298 |   });\n    at /var/www/360lm/tests/printing.spec.js:295:16"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 0,
                      "startTime": "2026-07-12T13:30:48.657Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "screenshot",
                          "contentType": "image/png",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-Exce-99749-tton-present-on-nest-screen-desktop-chrome/test-failed-1.png"
                        },
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-Exce-99749-tton-present-on-nest-screen-desktop-chrome/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/printing.spec.js",
                        "column": 16,
                        "line": 295
                      }
                    },
                    {
                      "workerIndex": 285,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 8590,
                      "error": {
                        "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n",
                        "stack": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n    at /var/www/360lm/tests/printing.spec.js:295:16",
                        "location": {
                          "file": "/var/www/360lm/tests/printing.spec.js",
                          "column": 16,
                          "line": 295
                        },
                        "snippet": "  293 |     await injectSession(page);\n  294 |     await page.goto(BASE_URL);\n> 295 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n      |                ^\n  296 |     await page.locator('text=Start New Nesting').click();\n  297 |     await page.waitForSelector('#screen-add.active', { timeout: 4000 });\n  298 |   });"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/printing.spec.js",
                            "column": 16,
                            "line": 295
                          },
                          "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n\n  293 |     await injectSession(page);\n  294 |     await page.goto(BASE_URL);\n> 295 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n      |                ^\n  296 |     await page.locator('text=Start New Nesting').click();\n  297 |     await page.waitForSelector('#screen-add.active', { timeout: 4000 });\n  298 |   });\n    at /var/www/360lm/tests/printing.spec.js:295:16"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 1,
                      "startTime": "2026-07-12T13:30:58.065Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "screenshot",
                          "contentType": "image/png",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-Exce-99749-tton-present-on-nest-screen-desktop-chrome-retry1/test-failed-1.png"
                        },
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-Exce-99749-tton-present-on-nest-screen-desktop-chrome-retry1/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/printing.spec.js",
                        "column": 16,
                        "line": 295
                      }
                    }
                  ],
                  "status": "unexpected"
                }
              ],
              "id": "84f3bc84c047e9f9c0b8-179fbaafd55252cf489b",
              "file": "printing.spec.js",
              "line": 312,
              "column": 3
            },
            {
              "title": "Compute button pulses when items exist",
              "ok": false,
              "tags": [],
              "tests": [
                {
                  "timeout": 30000,
                  "annotations": [],
                  "expectedStatus": "passed",
                  "projectId": "desktop-chrome",
                  "projectName": "desktop-chrome",
                  "results": [
                    {
                      "workerIndex": 286,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 8449,
                      "error": {
                        "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n",
                        "stack": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n    at /var/www/360lm/tests/printing.spec.js:295:16",
                        "location": {
                          "file": "/var/www/360lm/tests/printing.spec.js",
                          "column": 16,
                          "line": 295
                        },
                        "snippet": "  293 |     await injectSession(page);\n  294 |     await page.goto(BASE_URL);\n> 295 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n      |                ^\n  296 |     await page.locator('text=Start New Nesting').click();\n  297 |     await page.waitForSelector('#screen-add.active', { timeout: 4000 });\n  298 |   });"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/printing.spec.js",
                            "column": 16,
                            "line": 295
                          },
                          "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n\n  293 |     await injectSession(page);\n  294 |     await page.goto(BASE_URL);\n> 295 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n      |                ^\n  296 |     await page.locator('text=Start New Nesting').click();\n  297 |     await page.waitForSelector('#screen-add.active', { timeout: 4000 });\n  298 |   });\n    at /var/www/360lm/tests/printing.spec.js:295:16"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 0,
                      "startTime": "2026-07-12T13:31:07.922Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "screenshot",
                          "contentType": "image/png",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-Exce-5a943-ton-pulses-when-items-exist-desktop-chrome/test-failed-1.png"
                        },
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-Exce-5a943-ton-pulses-when-items-exist-desktop-chrome/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/printing.spec.js",
                        "column": 16,
                        "line": 295
                      }
                    },
                    {
                      "workerIndex": 287,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 8350,
                      "error": {
                        "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n",
                        "stack": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n    at /var/www/360lm/tests/printing.spec.js:295:16",
                        "location": {
                          "file": "/var/www/360lm/tests/printing.spec.js",
                          "column": 16,
                          "line": 295
                        },
                        "snippet": "  293 |     await injectSession(page);\n  294 |     await page.goto(BASE_URL);\n> 295 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n      |                ^\n  296 |     await page.locator('text=Start New Nesting').click();\n  297 |     await page.waitForSelector('#screen-add.active', { timeout: 4000 });\n  298 |   });"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/printing.spec.js",
                            "column": 16,
                            "line": 295
                          },
                          "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n\n  293 |     await injectSession(page);\n  294 |     await page.goto(BASE_URL);\n> 295 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n      |                ^\n  296 |     await page.locator('text=Start New Nesting').click();\n  297 |     await page.waitForSelector('#screen-add.active', { timeout: 4000 });\n  298 |   });\n    at /var/www/360lm/tests/printing.spec.js:295:16"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 1,
                      "startTime": "2026-07-12T13:31:17.431Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "screenshot",
                          "contentType": "image/png",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-Exce-5a943-ton-pulses-when-items-exist-desktop-chrome-retry1/test-failed-1.png"
                        },
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-Exce-5a943-ton-pulses-when-items-exist-desktop-chrome-retry1/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/printing.spec.js",
                        "column": 16,
                        "line": 295
                      }
                    }
                  ],
                  "status": "unexpected"
                }
              ],
              "id": "84f3bc84c047e9f9c0b8-6aff064d370b9278838f",
              "file": "printing.spec.js",
              "line": 325,
              "column": 3
            }
          ]
        },
        {
          "title": "Print Nest — 2D Lane Nesting",
          "file": "printing.spec.js",
          "line": 337,
          "column": 6,
          "specs": [
            {
              "title": "demo button loads 4 items",
              "ok": false,
              "tags": [],
              "tests": [
                {
                  "timeout": 30000,
                  "annotations": [],
                  "expectedStatus": "passed",
                  "projectId": "android-chrome",
                  "projectName": "android-chrome",
                  "results": [
                    {
                      "workerIndex": 112,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 8329,
                      "error": {
                        "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n",
                        "stack": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n    at /var/www/360lm/tests/printing.spec.js:341:16",
                        "location": {
                          "file": "/var/www/360lm/tests/printing.spec.js",
                          "column": 16,
                          "line": 341
                        },
                        "snippet": "  339 |     await injectSession(page);\n  340 |     await page.goto(BASE_URL);\n> 341 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n      |                ^\n  342 |     await page.locator('text=Start New Nesting').click();\n  343 |     await page.waitForSelector('#screen-add.active', { timeout: 4000 });\n  344 |   });"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/printing.spec.js",
                            "column": 16,
                            "line": 341
                          },
                          "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n\n  339 |     await injectSession(page);\n  340 |     await page.goto(BASE_URL);\n> 341 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n      |                ^\n  342 |     await page.locator('text=Start New Nesting').click();\n  343 |     await page.waitForSelector('#screen-add.active', { timeout: 4000 });\n  344 |   });\n    at /var/www/360lm/tests/printing.spec.js:341:16"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 0,
                      "startTime": "2026-07-12T13:11:12.863Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "screenshot",
                          "contentType": "image/png",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-2D-L-d8667-g-demo-button-loads-4-items-android-chrome/test-failed-1.png"
                        },
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-2D-L-d8667-g-demo-button-loads-4-items-android-chrome/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/printing.spec.js",
                        "column": 16,
                        "line": 341
                      }
                    },
                    {
                      "workerIndex": 113,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 8293,
                      "error": {
                        "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n",
                        "stack": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n    at /var/www/360lm/tests/printing.spec.js:341:16",
                        "location": {
                          "file": "/var/www/360lm/tests/printing.spec.js",
                          "column": 16,
                          "line": 341
                        },
                        "snippet": "  339 |     await injectSession(page);\n  340 |     await page.goto(BASE_URL);\n> 341 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n      |                ^\n  342 |     await page.locator('text=Start New Nesting').click();\n  343 |     await page.waitForSelector('#screen-add.active', { timeout: 4000 });\n  344 |   });"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/printing.spec.js",
                            "column": 16,
                            "line": 341
                          },
                          "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n\n  339 |     await injectSession(page);\n  340 |     await page.goto(BASE_URL);\n> 341 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n      |                ^\n  342 |     await page.locator('text=Start New Nesting').click();\n  343 |     await page.waitForSelector('#screen-add.active', { timeout: 4000 });\n  344 |   });\n    at /var/www/360lm/tests/printing.spec.js:341:16"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 1,
                      "startTime": "2026-07-12T13:11:22.580Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "screenshot",
                          "contentType": "image/png",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-2D-L-d8667-g-demo-button-loads-4-items-android-chrome-retry1/test-failed-1.png"
                        },
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-2D-L-d8667-g-demo-button-loads-4-items-android-chrome-retry1/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/printing.spec.js",
                        "column": 16,
                        "line": 341
                      }
                    }
                  ],
                  "status": "unexpected"
                }
              ],
              "id": "84f3bc84c047e9f9c0b8-30dfdd4a3bca0080ff75",
              "file": "printing.spec.js",
              "line": 357,
              "column": 3
            },
            {
              "title": "inline SVG visual rendered on nest screen after compute",
              "ok": false,
              "tags": [],
              "tests": [
                {
                  "timeout": 30000,
                  "annotations": [],
                  "expectedStatus": "passed",
                  "projectId": "android-chrome",
                  "projectName": "android-chrome",
                  "results": [
                    {
                      "workerIndex": 114,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 8544,
                      "error": {
                        "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n",
                        "stack": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n    at /var/www/360lm/tests/printing.spec.js:341:16",
                        "location": {
                          "file": "/var/www/360lm/tests/printing.spec.js",
                          "column": 16,
                          "line": 341
                        },
                        "snippet": "  339 |     await injectSession(page);\n  340 |     await page.goto(BASE_URL);\n> 341 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n      |                ^\n  342 |     await page.locator('text=Start New Nesting').click();\n  343 |     await page.waitForSelector('#screen-add.active', { timeout: 4000 });\n  344 |   });"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/printing.spec.js",
                            "column": 16,
                            "line": 341
                          },
                          "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n\n  339 |     await injectSession(page);\n  340 |     await page.goto(BASE_URL);\n> 341 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n      |                ^\n  342 |     await page.locator('text=Start New Nesting').click();\n  343 |     await page.waitForSelector('#screen-add.active', { timeout: 4000 });\n  344 |   });\n    at /var/www/360lm/tests/printing.spec.js:341:16"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 0,
                      "startTime": "2026-07-12T13:11:31.928Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "screenshot",
                          "contentType": "image/png",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-2D-L-2a29b-n-nest-screen-after-compute-android-chrome/test-failed-1.png"
                        },
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-2D-L-2a29b-n-nest-screen-after-compute-android-chrome/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/printing.spec.js",
                        "column": 16,
                        "line": 341
                      }
                    },
                    {
                      "workerIndex": 115,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 8435,
                      "error": {
                        "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n",
                        "stack": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n    at /var/www/360lm/tests/printing.spec.js:341:16",
                        "location": {
                          "file": "/var/www/360lm/tests/printing.spec.js",
                          "column": 16,
                          "line": 341
                        },
                        "snippet": "  339 |     await injectSession(page);\n  340 |     await page.goto(BASE_URL);\n> 341 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n      |                ^\n  342 |     await page.locator('text=Start New Nesting').click();\n  343 |     await page.waitForSelector('#screen-add.active', { timeout: 4000 });\n  344 |   });"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/printing.spec.js",
                            "column": 16,
                            "line": 341
                          },
                          "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n\n  339 |     await injectSession(page);\n  340 |     await page.goto(BASE_URL);\n> 341 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n      |                ^\n  342 |     await page.locator('text=Start New Nesting').click();\n  343 |     await page.waitForSelector('#screen-add.active', { timeout: 4000 });\n  344 |   });\n    at /var/www/360lm/tests/printing.spec.js:341:16"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 1,
                      "startTime": "2026-07-12T13:11:41.848Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "screenshot",
                          "contentType": "image/png",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-2D-L-2a29b-n-nest-screen-after-compute-android-chrome-retry1/test-failed-1.png"
                        },
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-2D-L-2a29b-n-nest-screen-after-compute-android-chrome-retry1/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/printing.spec.js",
                        "column": 16,
                        "line": 341
                      }
                    }
                  ],
                  "status": "unexpected"
                }
              ],
              "id": "84f3bc84c047e9f9c0b8-1535e75418eb1ef8e7eb",
              "file": "printing.spec.js",
              "line": 363,
              "column": 3
            },
            {
              "title": "2-lane nesting: 2+3ft items on 5ft roll show savings in summary",
              "ok": false,
              "tags": [],
              "tests": [
                {
                  "timeout": 30000,
                  "annotations": [],
                  "expectedStatus": "passed",
                  "projectId": "android-chrome",
                  "projectName": "android-chrome",
                  "results": [
                    {
                      "workerIndex": 116,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 8395,
                      "error": {
                        "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n",
                        "stack": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n    at /var/www/360lm/tests/printing.spec.js:341:16",
                        "location": {
                          "file": "/var/www/360lm/tests/printing.spec.js",
                          "column": 16,
                          "line": 341
                        },
                        "snippet": "  339 |     await injectSession(page);\n  340 |     await page.goto(BASE_URL);\n> 341 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n      |                ^\n  342 |     await page.locator('text=Start New Nesting').click();\n  343 |     await page.waitForSelector('#screen-add.active', { timeout: 4000 });\n  344 |   });"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/printing.spec.js",
                            "column": 16,
                            "line": 341
                          },
                          "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n\n  339 |     await injectSession(page);\n  340 |     await page.goto(BASE_URL);\n> 341 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n      |                ^\n  342 |     await page.locator('text=Start New Nesting').click();\n  343 |     await page.waitForSelector('#screen-add.active', { timeout: 4000 });\n  344 |   });\n    at /var/www/360lm/tests/printing.spec.js:341:16"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 0,
                      "startTime": "2026-07-12T13:11:51.685Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "screenshot",
                          "contentType": "image/png",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-2D-L-11917-oll-show-savings-in-summary-android-chrome/test-failed-1.png"
                        },
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-2D-L-11917-oll-show-savings-in-summary-android-chrome/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/printing.spec.js",
                        "column": 16,
                        "line": 341
                      }
                    },
                    {
                      "workerIndex": 117,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 8631,
                      "error": {
                        "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n",
                        "stack": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n    at /var/www/360lm/tests/printing.spec.js:341:16",
                        "location": {
                          "file": "/var/www/360lm/tests/printing.spec.js",
                          "column": 16,
                          "line": 341
                        },
                        "snippet": "  339 |     await injectSession(page);\n  340 |     await page.goto(BASE_URL);\n> 341 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n      |                ^\n  342 |     await page.locator('text=Start New Nesting').click();\n  343 |     await page.waitForSelector('#screen-add.active', { timeout: 4000 });\n  344 |   });"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/printing.spec.js",
                            "column": 16,
                            "line": 341
                          },
                          "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n\n  339 |     await injectSession(page);\n  340 |     await page.goto(BASE_URL);\n> 341 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n      |                ^\n  342 |     await page.locator('text=Start New Nesting').click();\n  343 |     await page.waitForSelector('#screen-add.active', { timeout: 4000 });\n  344 |   });\n    at /var/www/360lm/tests/printing.spec.js:341:16"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 1,
                      "startTime": "2026-07-12T13:12:02.888Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "screenshot",
                          "contentType": "image/png",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-2D-L-11917-oll-show-savings-in-summary-android-chrome-retry1/test-failed-1.png"
                        },
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-2D-L-11917-oll-show-savings-in-summary-android-chrome-retry1/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/printing.spec.js",
                        "column": 16,
                        "line": 341
                      }
                    }
                  ],
                  "status": "unexpected"
                }
              ],
              "id": "84f3bc84c047e9f9c0b8-d50109f81859a164bbe5",
              "file": "printing.spec.js",
              "line": 370,
              "column": 3
            },
            {
              "title": "2-lane: waste rectangle present in SVG for shorter lane",
              "ok": false,
              "tags": [],
              "tests": [
                {
                  "timeout": 30000,
                  "annotations": [],
                  "expectedStatus": "passed",
                  "projectId": "android-chrome",
                  "projectName": "android-chrome",
                  "results": [
                    {
                      "workerIndex": 118,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 8317,
                      "error": {
                        "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n",
                        "stack": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n    at /var/www/360lm/tests/printing.spec.js:341:16",
                        "location": {
                          "file": "/var/www/360lm/tests/printing.spec.js",
                          "column": 16,
                          "line": 341
                        },
                        "snippet": "  339 |     await injectSession(page);\n  340 |     await page.goto(BASE_URL);\n> 341 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n      |                ^\n  342 |     await page.locator('text=Start New Nesting').click();\n  343 |     await page.waitForSelector('#screen-add.active', { timeout: 4000 });\n  344 |   });"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/printing.spec.js",
                            "column": 16,
                            "line": 341
                          },
                          "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n\n  339 |     await injectSession(page);\n  340 |     await page.goto(BASE_URL);\n> 341 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n      |                ^\n  342 |     await page.locator('text=Start New Nesting').click();\n  343 |     await page.waitForSelector('#screen-add.active', { timeout: 4000 });\n  344 |   });\n    at /var/www/360lm/tests/printing.spec.js:341:16"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 0,
                      "startTime": "2026-07-12T13:12:13.058Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "screenshot",
                          "contentType": "image/png",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-2D-L-b0897-ent-in-SVG-for-shorter-lane-android-chrome/test-failed-1.png"
                        },
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-2D-L-b0897-ent-in-SVG-for-shorter-lane-android-chrome/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/printing.spec.js",
                        "column": 16,
                        "line": 341
                      }
                    },
                    {
                      "workerIndex": 119,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 8270,
                      "error": {
                        "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n",
                        "stack": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n    at /var/www/360lm/tests/printing.spec.js:341:16",
                        "location": {
                          "file": "/var/www/360lm/tests/printing.spec.js",
                          "column": 16,
                          "line": 341
                        },
                        "snippet": "  339 |     await injectSession(page);\n  340 |     await page.goto(BASE_URL);\n> 341 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n      |                ^\n  342 |     await page.locator('text=Start New Nesting').click();\n  343 |     await page.waitForSelector('#screen-add.active', { timeout: 4000 });\n  344 |   });"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/printing.spec.js",
                            "column": 16,
                            "line": 341
                          },
                          "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n\n  339 |     await injectSession(page);\n  340 |     await page.goto(BASE_URL);\n> 341 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n      |                ^\n  342 |     await page.locator('text=Start New Nesting').click();\n  343 |     await page.waitForSelector('#screen-add.active', { timeout: 4000 });\n  344 |   });\n    at /var/www/360lm/tests/printing.spec.js:341:16"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 1,
                      "startTime": "2026-07-12T13:12:22.385Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "screenshot",
                          "contentType": "image/png",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-2D-L-b0897-ent-in-SVG-for-shorter-lane-android-chrome-retry1/test-failed-1.png"
                        },
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-2D-L-b0897-ent-in-SVG-for-shorter-lane-android-chrome-retry1/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/printing.spec.js",
                        "column": 16,
                        "line": 341
                      }
                    }
                  ],
                  "status": "unexpected"
                }
              ],
              "id": "84f3bc84c047e9f9c0b8-bbfb133f464368871f54",
              "file": "printing.spec.js",
              "line": 381,
              "column": 3
            },
            {
              "title": "linear group (same-width items) shows no lane savings",
              "ok": false,
              "tags": [],
              "tests": [
                {
                  "timeout": 30000,
                  "annotations": [],
                  "expectedStatus": "passed",
                  "projectId": "android-chrome",
                  "projectName": "android-chrome",
                  "results": [
                    {
                      "workerIndex": 120,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 8407,
                      "error": {
                        "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n",
                        "stack": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n    at /var/www/360lm/tests/printing.spec.js:341:16",
                        "location": {
                          "file": "/var/www/360lm/tests/printing.spec.js",
                          "column": 16,
                          "line": 341
                        },
                        "snippet": "  339 |     await injectSession(page);\n  340 |     await page.goto(BASE_URL);\n> 341 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n      |                ^\n  342 |     await page.locator('text=Start New Nesting').click();\n  343 |     await page.waitForSelector('#screen-add.active', { timeout: 4000 });\n  344 |   });"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/printing.spec.js",
                            "column": 16,
                            "line": 341
                          },
                          "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n\n  339 |     await injectSession(page);\n  340 |     await page.goto(BASE_URL);\n> 341 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n      |                ^\n  342 |     await page.locator('text=Start New Nesting').click();\n  343 |     await page.waitForSelector('#screen-add.active', { timeout: 4000 });\n  344 |   });\n    at /var/www/360lm/tests/printing.spec.js:341:16"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 0,
                      "startTime": "2026-07-12T13:12:31.758Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "screenshot",
                          "contentType": "image/png",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-2D-L-d6af8-items-shows-no-lane-savings-android-chrome/test-failed-1.png"
                        },
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-2D-L-d6af8-items-shows-no-lane-savings-android-chrome/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/printing.spec.js",
                        "column": 16,
                        "line": 341
                      }
                    },
                    {
                      "workerIndex": 121,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 8374,
                      "error": {
                        "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n",
                        "stack": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n    at /var/www/360lm/tests/printing.spec.js:341:16",
                        "location": {
                          "file": "/var/www/360lm/tests/printing.spec.js",
                          "column": 16,
                          "line": 341
                        },
                        "snippet": "  339 |     await injectSession(page);\n  340 |     await page.goto(BASE_URL);\n> 341 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n      |                ^\n  342 |     await page.locator('text=Start New Nesting').click();\n  343 |     await page.waitForSelector('#screen-add.active', { timeout: 4000 });\n  344 |   });"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/printing.spec.js",
                            "column": 16,
                            "line": 341
                          },
                          "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n\n  339 |     await injectSession(page);\n  340 |     await page.goto(BASE_URL);\n> 341 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n      |                ^\n  342 |     await page.locator('text=Start New Nesting').click();\n  343 |     await page.waitForSelector('#screen-add.active', { timeout: 4000 });\n  344 |   });\n    at /var/www/360lm/tests/printing.spec.js:341:16"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 1,
                      "startTime": "2026-07-12T13:12:41.556Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "screenshot",
                          "contentType": "image/png",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-2D-L-d6af8-items-shows-no-lane-savings-android-chrome-retry1/test-failed-1.png"
                        },
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-2D-L-d6af8-items-shows-no-lane-savings-android-chrome-retry1/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/printing.spec.js",
                        "column": 16,
                        "line": 341
                      }
                    }
                  ],
                  "status": "unexpected"
                }
              ],
              "id": "84f3bc84c047e9f9c0b8-b3c0008d24b96e8d8ef7",
              "file": "printing.spec.js",
              "line": 392,
              "column": 3
            },
            {
              "title": "hover popup present in DOM",
              "ok": false,
              "tags": [],
              "tests": [
                {
                  "timeout": 30000,
                  "annotations": [],
                  "expectedStatus": "passed",
                  "projectId": "android-chrome",
                  "projectName": "android-chrome",
                  "results": [
                    {
                      "workerIndex": 122,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 8296,
                      "error": {
                        "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n",
                        "stack": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n    at /var/www/360lm/tests/printing.spec.js:341:16",
                        "location": {
                          "file": "/var/www/360lm/tests/printing.spec.js",
                          "column": 16,
                          "line": 341
                        },
                        "snippet": "  339 |     await injectSession(page);\n  340 |     await page.goto(BASE_URL);\n> 341 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n      |                ^\n  342 |     await page.locator('text=Start New Nesting').click();\n  343 |     await page.waitForSelector('#screen-add.active', { timeout: 4000 });\n  344 |   });"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/printing.spec.js",
                            "column": 16,
                            "line": 341
                          },
                          "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n\n  339 |     await injectSession(page);\n  340 |     await page.goto(BASE_URL);\n> 341 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n      |                ^\n  342 |     await page.locator('text=Start New Nesting').click();\n  343 |     await page.waitForSelector('#screen-add.active', { timeout: 4000 });\n  344 |   });\n    at /var/www/360lm/tests/printing.spec.js:341:16"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 0,
                      "startTime": "2026-07-12T13:12:51.310Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "screenshot",
                          "contentType": "image/png",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-2D-L-8d66f--hover-popup-present-in-DOM-android-chrome/test-failed-1.png"
                        },
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-2D-L-8d66f--hover-popup-present-in-DOM-android-chrome/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/printing.spec.js",
                        "column": 16,
                        "line": 341
                      }
                    },
                    {
                      "workerIndex": 123,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 8632,
                      "error": {
                        "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n",
                        "stack": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n    at /var/www/360lm/tests/printing.spec.js:341:16",
                        "location": {
                          "file": "/var/www/360lm/tests/printing.spec.js",
                          "column": 16,
                          "line": 341
                        },
                        "snippet": "  339 |     await injectSession(page);\n  340 |     await page.goto(BASE_URL);\n> 341 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n      |                ^\n  342 |     await page.locator('text=Start New Nesting').click();\n  343 |     await page.waitForSelector('#screen-add.active', { timeout: 4000 });\n  344 |   });"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/printing.spec.js",
                            "column": 16,
                            "line": 341
                          },
                          "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n\n  339 |     await injectSession(page);\n  340 |     await page.goto(BASE_URL);\n> 341 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n      |                ^\n  342 |     await page.locator('text=Start New Nesting').click();\n  343 |     await page.waitForSelector('#screen-add.active', { timeout: 4000 });\n  344 |   });\n    at /var/www/360lm/tests/printing.spec.js:341:16"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 1,
                      "startTime": "2026-07-12T13:13:00.988Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "screenshot",
                          "contentType": "image/png",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-2D-L-8d66f--hover-popup-present-in-DOM-android-chrome-retry1/test-failed-1.png"
                        },
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-2D-L-8d66f--hover-popup-present-in-DOM-android-chrome-retry1/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/printing.spec.js",
                        "column": 16,
                        "line": 341
                      }
                    }
                  ],
                  "status": "unexpected"
                }
              ],
              "id": "84f3bc84c047e9f9c0b8-545e7fdfc45fbf178d83",
              "file": "printing.spec.js",
              "line": 402,
              "column": 3
            },
            {
              "title": "text plan toggle button shows/hides details",
              "ok": false,
              "tags": [],
              "tests": [
                {
                  "timeout": 30000,
                  "annotations": [],
                  "expectedStatus": "passed",
                  "projectId": "android-chrome",
                  "projectName": "android-chrome",
                  "results": [
                    {
                      "workerIndex": 124,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 8309,
                      "error": {
                        "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n",
                        "stack": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n    at /var/www/360lm/tests/printing.spec.js:341:16",
                        "location": {
                          "file": "/var/www/360lm/tests/printing.spec.js",
                          "column": 16,
                          "line": 341
                        },
                        "snippet": "  339 |     await injectSession(page);\n  340 |     await page.goto(BASE_URL);\n> 341 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n      |                ^\n  342 |     await page.locator('text=Start New Nesting').click();\n  343 |     await page.waitForSelector('#screen-add.active', { timeout: 4000 });\n  344 |   });"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/printing.spec.js",
                            "column": 16,
                            "line": 341
                          },
                          "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n\n  339 |     await injectSession(page);\n  340 |     await page.goto(BASE_URL);\n> 341 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n      |                ^\n  342 |     await page.locator('text=Start New Nesting').click();\n  343 |     await page.waitForSelector('#screen-add.active', { timeout: 4000 });\n  344 |   });\n    at /var/www/360lm/tests/printing.spec.js:341:16"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 0,
                      "startTime": "2026-07-12T13:13:10.865Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "screenshot",
                          "contentType": "image/png",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-2D-L-562f8--button-shows-hides-details-android-chrome/test-failed-1.png"
                        },
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-2D-L-562f8--button-shows-hides-details-android-chrome/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/printing.spec.js",
                        "column": 16,
                        "line": 341
                      }
                    },
                    {
                      "workerIndex": 125,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 8266,
                      "error": {
                        "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n",
                        "stack": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n    at /var/www/360lm/tests/printing.spec.js:341:16",
                        "location": {
                          "file": "/var/www/360lm/tests/printing.spec.js",
                          "column": 16,
                          "line": 341
                        },
                        "snippet": "  339 |     await injectSession(page);\n  340 |     await page.goto(BASE_URL);\n> 341 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n      |                ^\n  342 |     await page.locator('text=Start New Nesting').click();\n  343 |     await page.waitForSelector('#screen-add.active', { timeout: 4000 });\n  344 |   });"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/printing.spec.js",
                            "column": 16,
                            "line": 341
                          },
                          "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n\n  339 |     await injectSession(page);\n  340 |     await page.goto(BASE_URL);\n> 341 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n      |                ^\n  342 |     await page.locator('text=Start New Nesting').click();\n  343 |     await page.waitForSelector('#screen-add.active', { timeout: 4000 });\n  344 |   });\n    at /var/www/360lm/tests/printing.spec.js:341:16"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 1,
                      "startTime": "2026-07-12T13:13:20.377Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "screenshot",
                          "contentType": "image/png",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-2D-L-562f8--button-shows-hides-details-android-chrome-retry1/test-failed-1.png"
                        },
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-2D-L-562f8--button-shows-hides-details-android-chrome-retry1/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/printing.spec.js",
                        "column": 16,
                        "line": 341
                      }
                    }
                  ],
                  "status": "unexpected"
                }
              ],
              "id": "84f3bc84c047e9f9c0b8-093260cd31123b5f69b0",
              "file": "printing.spec.js",
              "line": 409,
              "column": 3
            },
            {
              "title": "demo items produce 2-lane nesting for both Flex and Vinyl",
              "ok": false,
              "tags": [],
              "tests": [
                {
                  "timeout": 30000,
                  "annotations": [],
                  "expectedStatus": "passed",
                  "projectId": "android-chrome",
                  "projectName": "android-chrome",
                  "results": [
                    {
                      "workerIndex": 126,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 8337,
                      "error": {
                        "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n",
                        "stack": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n    at /var/www/360lm/tests/printing.spec.js:341:16",
                        "location": {
                          "file": "/var/www/360lm/tests/printing.spec.js",
                          "column": 16,
                          "line": 341
                        },
                        "snippet": "  339 |     await injectSession(page);\n  340 |     await page.goto(BASE_URL);\n> 341 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n      |                ^\n  342 |     await page.locator('text=Start New Nesting').click();\n  343 |     await page.waitForSelector('#screen-add.active', { timeout: 4000 });\n  344 |   });"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/printing.spec.js",
                            "column": 16,
                            "line": 341
                          },
                          "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n\n  339 |     await injectSession(page);\n  340 |     await page.goto(BASE_URL);\n> 341 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n      |                ^\n  342 |     await page.locator('text=Start New Nesting').click();\n  343 |     await page.waitForSelector('#screen-add.active', { timeout: 4000 });\n  344 |   });\n    at /var/www/360lm/tests/printing.spec.js:341:16"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 0,
                      "startTime": "2026-07-12T13:13:29.801Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "screenshot",
                          "contentType": "image/png",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-2D-L-32b33-ing-for-both-Flex-and-Vinyl-android-chrome/test-failed-1.png"
                        },
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-2D-L-32b33-ing-for-both-Flex-and-Vinyl-android-chrome/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/printing.spec.js",
                        "column": 16,
                        "line": 341
                      }
                    },
                    {
                      "workerIndex": 127,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 8284,
                      "error": {
                        "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n",
                        "stack": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n    at /var/www/360lm/tests/printing.spec.js:341:16",
                        "location": {
                          "file": "/var/www/360lm/tests/printing.spec.js",
                          "column": 16,
                          "line": 341
                        },
                        "snippet": "  339 |     await injectSession(page);\n  340 |     await page.goto(BASE_URL);\n> 341 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n      |                ^\n  342 |     await page.locator('text=Start New Nesting').click();\n  343 |     await page.waitForSelector('#screen-add.active', { timeout: 4000 });\n  344 |   });"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/printing.spec.js",
                            "column": 16,
                            "line": 341
                          },
                          "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n\n  339 |     await injectSession(page);\n  340 |     await page.goto(BASE_URL);\n> 341 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n      |                ^\n  342 |     await page.locator('text=Start New Nesting').click();\n  343 |     await page.waitForSelector('#screen-add.active', { timeout: 4000 });\n  344 |   });\n    at /var/www/360lm/tests/printing.spec.js:341:16"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 1,
                      "startTime": "2026-07-12T13:13:39.364Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "screenshot",
                          "contentType": "image/png",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-2D-L-32b33-ing-for-both-Flex-and-Vinyl-android-chrome-retry1/test-failed-1.png"
                        },
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-2D-L-32b33-ing-for-both-Flex-and-Vinyl-android-chrome-retry1/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/printing.spec.js",
                        "column": 16,
                        "line": 341
                      }
                    }
                  ],
                  "status": "unexpected"
                }
              ],
              "id": "84f3bc84c047e9f9c0b8-6d30c45862d58ba37083",
              "file": "printing.spec.js",
              "line": 420,
              "column": 3
            },
            {
              "title": "demo button loads 4 items",
              "ok": false,
              "tags": [],
              "tests": [
                {
                  "timeout": 30000,
                  "annotations": [],
                  "expectedStatus": "passed",
                  "projectId": "desktop-chrome",
                  "projectName": "desktop-chrome",
                  "results": [
                    {
                      "workerIndex": 288,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 8331,
                      "error": {
                        "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n",
                        "stack": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n    at /var/www/360lm/tests/printing.spec.js:341:16",
                        "location": {
                          "file": "/var/www/360lm/tests/printing.spec.js",
                          "column": 16,
                          "line": 341
                        },
                        "snippet": "  339 |     await injectSession(page);\n  340 |     await page.goto(BASE_URL);\n> 341 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n      |                ^\n  342 |     await page.locator('text=Start New Nesting').click();\n  343 |     await page.waitForSelector('#screen-add.active', { timeout: 4000 });\n  344 |   });"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/printing.spec.js",
                            "column": 16,
                            "line": 341
                          },
                          "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n\n  339 |     await injectSession(page);\n  340 |     await page.goto(BASE_URL);\n> 341 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n      |                ^\n  342 |     await page.locator('text=Start New Nesting').click();\n  343 |     await page.waitForSelector('#screen-add.active', { timeout: 4000 });\n  344 |   });\n    at /var/www/360lm/tests/printing.spec.js:341:16"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 0,
                      "startTime": "2026-07-12T13:31:26.853Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "screenshot",
                          "contentType": "image/png",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-2D-L-d8667-g-demo-button-loads-4-items-desktop-chrome/test-failed-1.png"
                        },
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-2D-L-d8667-g-demo-button-loads-4-items-desktop-chrome/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/printing.spec.js",
                        "column": 16,
                        "line": 341
                      }
                    },
                    {
                      "workerIndex": 289,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 8302,
                      "error": {
                        "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n",
                        "stack": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n    at /var/www/360lm/tests/printing.spec.js:341:16",
                        "location": {
                          "file": "/var/www/360lm/tests/printing.spec.js",
                          "column": 16,
                          "line": 341
                        },
                        "snippet": "  339 |     await injectSession(page);\n  340 |     await page.goto(BASE_URL);\n> 341 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n      |                ^\n  342 |     await page.locator('text=Start New Nesting').click();\n  343 |     await page.waitForSelector('#screen-add.active', { timeout: 4000 });\n  344 |   });"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/printing.spec.js",
                            "column": 16,
                            "line": 341
                          },
                          "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n\n  339 |     await injectSession(page);\n  340 |     await page.goto(BASE_URL);\n> 341 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n      |                ^\n  342 |     await page.locator('text=Start New Nesting').click();\n  343 |     await page.waitForSelector('#screen-add.active', { timeout: 4000 });\n  344 |   });\n    at /var/www/360lm/tests/printing.spec.js:341:16"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 1,
                      "startTime": "2026-07-12T13:31:36.368Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "screenshot",
                          "contentType": "image/png",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-2D-L-d8667-g-demo-button-loads-4-items-desktop-chrome-retry1/test-failed-1.png"
                        },
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-2D-L-d8667-g-demo-button-loads-4-items-desktop-chrome-retry1/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/printing.spec.js",
                        "column": 16,
                        "line": 341
                      }
                    }
                  ],
                  "status": "unexpected"
                }
              ],
              "id": "84f3bc84c047e9f9c0b8-e01bd768b2389a308143",
              "file": "printing.spec.js",
              "line": 357,
              "column": 3
            },
            {
              "title": "inline SVG visual rendered on nest screen after compute",
              "ok": false,
              "tags": [],
              "tests": [
                {
                  "timeout": 30000,
                  "annotations": [],
                  "expectedStatus": "passed",
                  "projectId": "desktop-chrome",
                  "projectName": "desktop-chrome",
                  "results": [
                    {
                      "workerIndex": 290,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 8349,
                      "error": {
                        "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n",
                        "stack": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n    at /var/www/360lm/tests/printing.spec.js:341:16",
                        "location": {
                          "file": "/var/www/360lm/tests/printing.spec.js",
                          "column": 16,
                          "line": 341
                        },
                        "snippet": "  339 |     await injectSession(page);\n  340 |     await page.goto(BASE_URL);\n> 341 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n      |                ^\n  342 |     await page.locator('text=Start New Nesting').click();\n  343 |     await page.waitForSelector('#screen-add.active', { timeout: 4000 });\n  344 |   });"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/printing.spec.js",
                            "column": 16,
                            "line": 341
                          },
                          "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n\n  339 |     await injectSession(page);\n  340 |     await page.goto(BASE_URL);\n> 341 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n      |                ^\n  342 |     await page.locator('text=Start New Nesting').click();\n  343 |     await page.waitForSelector('#screen-add.active', { timeout: 4000 });\n  344 |   });\n    at /var/www/360lm/tests/printing.spec.js:341:16"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 0,
                      "startTime": "2026-07-12T13:31:45.843Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "screenshot",
                          "contentType": "image/png",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-2D-L-2a29b-n-nest-screen-after-compute-desktop-chrome/test-failed-1.png"
                        },
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-2D-L-2a29b-n-nest-screen-after-compute-desktop-chrome/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/printing.spec.js",
                        "column": 16,
                        "line": 341
                      }
                    },
                    {
                      "workerIndex": 291,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 8319,
                      "error": {
                        "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n",
                        "stack": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n    at /var/www/360lm/tests/printing.spec.js:341:16",
                        "location": {
                          "file": "/var/www/360lm/tests/printing.spec.js",
                          "column": 16,
                          "line": 341
                        },
                        "snippet": "  339 |     await injectSession(page);\n  340 |     await page.goto(BASE_URL);\n> 341 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n      |                ^\n  342 |     await page.locator('text=Start New Nesting').click();\n  343 |     await page.waitForSelector('#screen-add.active', { timeout: 4000 });\n  344 |   });"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/printing.spec.js",
                            "column": 16,
                            "line": 341
                          },
                          "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n\n  339 |     await injectSession(page);\n  340 |     await page.goto(BASE_URL);\n> 341 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n      |                ^\n  342 |     await page.locator('text=Start New Nesting').click();\n  343 |     await page.waitForSelector('#screen-add.active', { timeout: 4000 });\n  344 |   });\n    at /var/www/360lm/tests/printing.spec.js:341:16"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 1,
                      "startTime": "2026-07-12T13:31:55.337Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "screenshot",
                          "contentType": "image/png",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-2D-L-2a29b-n-nest-screen-after-compute-desktop-chrome-retry1/test-failed-1.png"
                        },
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-2D-L-2a29b-n-nest-screen-after-compute-desktop-chrome-retry1/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/printing.spec.js",
                        "column": 16,
                        "line": 341
                      }
                    }
                  ],
                  "status": "unexpected"
                }
              ],
              "id": "84f3bc84c047e9f9c0b8-0ca335967ff9529d5547",
              "file": "printing.spec.js",
              "line": 363,
              "column": 3
            },
            {
              "title": "2-lane nesting: 2+3ft items on 5ft roll show savings in summary",
              "ok": false,
              "tags": [],
              "tests": [
                {
                  "timeout": 30000,
                  "annotations": [],
                  "expectedStatus": "passed",
                  "projectId": "desktop-chrome",
                  "projectName": "desktop-chrome",
                  "results": [
                    {
                      "workerIndex": 292,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 8366,
                      "error": {
                        "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n",
                        "stack": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n    at /var/www/360lm/tests/printing.spec.js:341:16",
                        "location": {
                          "file": "/var/www/360lm/tests/printing.spec.js",
                          "column": 16,
                          "line": 341
                        },
                        "snippet": "  339 |     await injectSession(page);\n  340 |     await page.goto(BASE_URL);\n> 341 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n      |                ^\n  342 |     await page.locator('text=Start New Nesting').click();\n  343 |     await page.waitForSelector('#screen-add.active', { timeout: 4000 });\n  344 |   });"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/printing.spec.js",
                            "column": 16,
                            "line": 341
                          },
                          "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n\n  339 |     await injectSession(page);\n  340 |     await page.goto(BASE_URL);\n> 341 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n      |                ^\n  342 |     await page.locator('text=Start New Nesting').click();\n  343 |     await page.waitForSelector('#screen-add.active', { timeout: 4000 });\n  344 |   });\n    at /var/www/360lm/tests/printing.spec.js:341:16"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 0,
                      "startTime": "2026-07-12T13:32:05.148Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "screenshot",
                          "contentType": "image/png",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-2D-L-11917-oll-show-savings-in-summary-desktop-chrome/test-failed-1.png"
                        },
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-2D-L-11917-oll-show-savings-in-summary-desktop-chrome/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/printing.spec.js",
                        "column": 16,
                        "line": 341
                      }
                    },
                    {
                      "workerIndex": 293,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 8320,
                      "error": {
                        "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n",
                        "stack": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n    at /var/www/360lm/tests/printing.spec.js:341:16",
                        "location": {
                          "file": "/var/www/360lm/tests/printing.spec.js",
                          "column": 16,
                          "line": 341
                        },
                        "snippet": "  339 |     await injectSession(page);\n  340 |     await page.goto(BASE_URL);\n> 341 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n      |                ^\n  342 |     await page.locator('text=Start New Nesting').click();\n  343 |     await page.waitForSelector('#screen-add.active', { timeout: 4000 });\n  344 |   });"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/printing.spec.js",
                            "column": 16,
                            "line": 341
                          },
                          "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n\n  339 |     await injectSession(page);\n  340 |     await page.goto(BASE_URL);\n> 341 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n      |                ^\n  342 |     await page.locator('text=Start New Nesting').click();\n  343 |     await page.waitForSelector('#screen-add.active', { timeout: 4000 });\n  344 |   });\n    at /var/www/360lm/tests/printing.spec.js:341:16"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 1,
                      "startTime": "2026-07-12T13:32:14.625Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "screenshot",
                          "contentType": "image/png",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-2D-L-11917-oll-show-savings-in-summary-desktop-chrome-retry1/test-failed-1.png"
                        },
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-2D-L-11917-oll-show-savings-in-summary-desktop-chrome-retry1/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/printing.spec.js",
                        "column": 16,
                        "line": 341
                      }
                    }
                  ],
                  "status": "unexpected"
                }
              ],
              "id": "84f3bc84c047e9f9c0b8-e5ed090ec3c7fbef93cf",
              "file": "printing.spec.js",
              "line": 370,
              "column": 3
            },
            {
              "title": "2-lane: waste rectangle present in SVG for shorter lane",
              "ok": false,
              "tags": [],
              "tests": [
                {
                  "timeout": 30000,
                  "annotations": [],
                  "expectedStatus": "passed",
                  "projectId": "desktop-chrome",
                  "projectName": "desktop-chrome",
                  "results": [
                    {
                      "workerIndex": 294,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 8258,
                      "error": {
                        "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n",
                        "stack": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n    at /var/www/360lm/tests/printing.spec.js:341:16",
                        "location": {
                          "file": "/var/www/360lm/tests/printing.spec.js",
                          "column": 16,
                          "line": 341
                        },
                        "snippet": "  339 |     await injectSession(page);\n  340 |     await page.goto(BASE_URL);\n> 341 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n      |                ^\n  342 |     await page.locator('text=Start New Nesting').click();\n  343 |     await page.waitForSelector('#screen-add.active', { timeout: 4000 });\n  344 |   });"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/printing.spec.js",
                            "column": 16,
                            "line": 341
                          },
                          "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n\n  339 |     await injectSession(page);\n  340 |     await page.goto(BASE_URL);\n> 341 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n      |                ^\n  342 |     await page.locator('text=Start New Nesting').click();\n  343 |     await page.waitForSelector('#screen-add.active', { timeout: 4000 });\n  344 |   });\n    at /var/www/360lm/tests/printing.spec.js:341:16"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 0,
                      "startTime": "2026-07-12T13:32:24.116Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "screenshot",
                          "contentType": "image/png",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-2D-L-b0897-ent-in-SVG-for-shorter-lane-desktop-chrome/test-failed-1.png"
                        },
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-2D-L-b0897-ent-in-SVG-for-shorter-lane-desktop-chrome/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/printing.spec.js",
                        "column": 16,
                        "line": 341
                      }
                    },
                    {
                      "workerIndex": 295,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 8278,
                      "error": {
                        "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n",
                        "stack": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n    at /var/www/360lm/tests/printing.spec.js:341:16",
                        "location": {
                          "file": "/var/www/360lm/tests/printing.spec.js",
                          "column": 16,
                          "line": 341
                        },
                        "snippet": "  339 |     await injectSession(page);\n  340 |     await page.goto(BASE_URL);\n> 341 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n      |                ^\n  342 |     await page.locator('text=Start New Nesting').click();\n  343 |     await page.waitForSelector('#screen-add.active', { timeout: 4000 });\n  344 |   });"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/printing.spec.js",
                            "column": 16,
                            "line": 341
                          },
                          "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n\n  339 |     await injectSession(page);\n  340 |     await page.goto(BASE_URL);\n> 341 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n      |                ^\n  342 |     await page.locator('text=Start New Nesting').click();\n  343 |     await page.waitForSelector('#screen-add.active', { timeout: 4000 });\n  344 |   });\n    at /var/www/360lm/tests/printing.spec.js:341:16"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 1,
                      "startTime": "2026-07-12T13:32:33.301Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "screenshot",
                          "contentType": "image/png",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-2D-L-b0897-ent-in-SVG-for-shorter-lane-desktop-chrome-retry1/test-failed-1.png"
                        },
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-2D-L-b0897-ent-in-SVG-for-shorter-lane-desktop-chrome-retry1/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/printing.spec.js",
                        "column": 16,
                        "line": 341
                      }
                    }
                  ],
                  "status": "unexpected"
                }
              ],
              "id": "84f3bc84c047e9f9c0b8-a3c1f9fda5e8449d76d9",
              "file": "printing.spec.js",
              "line": 381,
              "column": 3
            },
            {
              "title": "linear group (same-width items) shows no lane savings",
              "ok": false,
              "tags": [],
              "tests": [
                {
                  "timeout": 30000,
                  "annotations": [],
                  "expectedStatus": "passed",
                  "projectId": "desktop-chrome",
                  "projectName": "desktop-chrome",
                  "results": [
                    {
                      "workerIndex": 296,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 8315,
                      "error": {
                        "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n",
                        "stack": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n    at /var/www/360lm/tests/printing.spec.js:341:16",
                        "location": {
                          "file": "/var/www/360lm/tests/printing.spec.js",
                          "column": 16,
                          "line": 341
                        },
                        "snippet": "  339 |     await injectSession(page);\n  340 |     await page.goto(BASE_URL);\n> 341 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n      |                ^\n  342 |     await page.locator('text=Start New Nesting').click();\n  343 |     await page.waitForSelector('#screen-add.active', { timeout: 4000 });\n  344 |   });"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/printing.spec.js",
                            "column": 16,
                            "line": 341
                          },
                          "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n\n  339 |     await injectSession(page);\n  340 |     await page.goto(BASE_URL);\n> 341 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n      |                ^\n  342 |     await page.locator('text=Start New Nesting').click();\n  343 |     await page.waitForSelector('#screen-add.active', { timeout: 4000 });\n  344 |   });\n    at /var/www/360lm/tests/printing.spec.js:341:16"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 0,
                      "startTime": "2026-07-12T13:32:42.700Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "screenshot",
                          "contentType": "image/png",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-2D-L-d6af8-items-shows-no-lane-savings-desktop-chrome/test-failed-1.png"
                        },
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-2D-L-d6af8-items-shows-no-lane-savings-desktop-chrome/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/printing.spec.js",
                        "column": 16,
                        "line": 341
                      }
                    },
                    {
                      "workerIndex": 297,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 8353,
                      "error": {
                        "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n",
                        "stack": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n    at /var/www/360lm/tests/printing.spec.js:341:16",
                        "location": {
                          "file": "/var/www/360lm/tests/printing.spec.js",
                          "column": 16,
                          "line": 341
                        },
                        "snippet": "  339 |     await injectSession(page);\n  340 |     await page.goto(BASE_URL);\n> 341 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n      |                ^\n  342 |     await page.locator('text=Start New Nesting').click();\n  343 |     await page.waitForSelector('#screen-add.active', { timeout: 4000 });\n  344 |   });"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/printing.spec.js",
                            "column": 16,
                            "line": 341
                          },
                          "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n\n  339 |     await injectSession(page);\n  340 |     await page.goto(BASE_URL);\n> 341 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n      |                ^\n  342 |     await page.locator('text=Start New Nesting').click();\n  343 |     await page.waitForSelector('#screen-add.active', { timeout: 4000 });\n  344 |   });\n    at /var/www/360lm/tests/printing.spec.js:341:16"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 1,
                      "startTime": "2026-07-12T13:32:52.107Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "screenshot",
                          "contentType": "image/png",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-2D-L-d6af8-items-shows-no-lane-savings-desktop-chrome-retry1/test-failed-1.png"
                        },
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-2D-L-d6af8-items-shows-no-lane-savings-desktop-chrome-retry1/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/printing.spec.js",
                        "column": 16,
                        "line": 341
                      }
                    }
                  ],
                  "status": "unexpected"
                }
              ],
              "id": "84f3bc84c047e9f9c0b8-6d46fcb4997f61ac8173",
              "file": "printing.spec.js",
              "line": 392,
              "column": 3
            },
            {
              "title": "hover popup present in DOM",
              "ok": false,
              "tags": [],
              "tests": [
                {
                  "timeout": 30000,
                  "annotations": [],
                  "expectedStatus": "passed",
                  "projectId": "desktop-chrome",
                  "projectName": "desktop-chrome",
                  "results": [
                    {
                      "workerIndex": 298,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 8311,
                      "error": {
                        "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n",
                        "stack": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n    at /var/www/360lm/tests/printing.spec.js:341:16",
                        "location": {
                          "file": "/var/www/360lm/tests/printing.spec.js",
                          "column": 16,
                          "line": 341
                        },
                        "snippet": "  339 |     await injectSession(page);\n  340 |     await page.goto(BASE_URL);\n> 341 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n      |                ^\n  342 |     await page.locator('text=Start New Nesting').click();\n  343 |     await page.waitForSelector('#screen-add.active', { timeout: 4000 });\n  344 |   });"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/printing.spec.js",
                            "column": 16,
                            "line": 341
                          },
                          "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n\n  339 |     await injectSession(page);\n  340 |     await page.goto(BASE_URL);\n> 341 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n      |                ^\n  342 |     await page.locator('text=Start New Nesting').click();\n  343 |     await page.waitForSelector('#screen-add.active', { timeout: 4000 });\n  344 |   });\n    at /var/www/360lm/tests/printing.spec.js:341:16"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 0,
                      "startTime": "2026-07-12T13:33:02.582Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "screenshot",
                          "contentType": "image/png",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-2D-L-8d66f--hover-popup-present-in-DOM-desktop-chrome/test-failed-1.png"
                        },
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-2D-L-8d66f--hover-popup-present-in-DOM-desktop-chrome/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/printing.spec.js",
                        "column": 16,
                        "line": 341
                      }
                    },
                    {
                      "workerIndex": 299,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 8387,
                      "error": {
                        "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n",
                        "stack": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n    at /var/www/360lm/tests/printing.spec.js:341:16",
                        "location": {
                          "file": "/var/www/360lm/tests/printing.spec.js",
                          "column": 16,
                          "line": 341
                        },
                        "snippet": "  339 |     await injectSession(page);\n  340 |     await page.goto(BASE_URL);\n> 341 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n      |                ^\n  342 |     await page.locator('text=Start New Nesting').click();\n  343 |     await page.waitForSelector('#screen-add.active', { timeout: 4000 });\n  344 |   });"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/printing.spec.js",
                            "column": 16,
                            "line": 341
                          },
                          "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n\n  339 |     await injectSession(page);\n  340 |     await page.goto(BASE_URL);\n> 341 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n      |                ^\n  342 |     await page.locator('text=Start New Nesting').click();\n  343 |     await page.waitForSelector('#screen-add.active', { timeout: 4000 });\n  344 |   });\n    at /var/www/360lm/tests/printing.spec.js:341:16"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 1,
                      "startTime": "2026-07-12T13:33:12.180Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "screenshot",
                          "contentType": "image/png",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-2D-L-8d66f--hover-popup-present-in-DOM-desktop-chrome-retry1/test-failed-1.png"
                        },
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-2D-L-8d66f--hover-popup-present-in-DOM-desktop-chrome-retry1/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/printing.spec.js",
                        "column": 16,
                        "line": 341
                      }
                    }
                  ],
                  "status": "unexpected"
                }
              ],
              "id": "84f3bc84c047e9f9c0b8-f18abd3cf660ab61f5bb",
              "file": "printing.spec.js",
              "line": 402,
              "column": 3
            },
            {
              "title": "text plan toggle button shows/hides details",
              "ok": false,
              "tags": [],
              "tests": [
                {
                  "timeout": 30000,
                  "annotations": [],
                  "expectedStatus": "passed",
                  "projectId": "desktop-chrome",
                  "projectName": "desktop-chrome",
                  "results": [
                    {
                      "workerIndex": 300,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 8292,
                      "error": {
                        "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n",
                        "stack": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n    at /var/www/360lm/tests/printing.spec.js:341:16",
                        "location": {
                          "file": "/var/www/360lm/tests/printing.spec.js",
                          "column": 16,
                          "line": 341
                        },
                        "snippet": "  339 |     await injectSession(page);\n  340 |     await page.goto(BASE_URL);\n> 341 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n      |                ^\n  342 |     await page.locator('text=Start New Nesting').click();\n  343 |     await page.waitForSelector('#screen-add.active', { timeout: 4000 });\n  344 |   });"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/printing.spec.js",
                            "column": 16,
                            "line": 341
                          },
                          "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n\n  339 |     await injectSession(page);\n  340 |     await page.goto(BASE_URL);\n> 341 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n      |                ^\n  342 |     await page.locator('text=Start New Nesting').click();\n  343 |     await page.waitForSelector('#screen-add.active', { timeout: 4000 });\n  344 |   });\n    at /var/www/360lm/tests/printing.spec.js:341:16"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 0,
                      "startTime": "2026-07-12T13:33:21.622Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "screenshot",
                          "contentType": "image/png",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-2D-L-562f8--button-shows-hides-details-desktop-chrome/test-failed-1.png"
                        },
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-2D-L-562f8--button-shows-hides-details-desktop-chrome/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/printing.spec.js",
                        "column": 16,
                        "line": 341
                      }
                    },
                    {
                      "workerIndex": 301,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 8332,
                      "error": {
                        "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n",
                        "stack": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n    at /var/www/360lm/tests/printing.spec.js:341:16",
                        "location": {
                          "file": "/var/www/360lm/tests/printing.spec.js",
                          "column": 16,
                          "line": 341
                        },
                        "snippet": "  339 |     await injectSession(page);\n  340 |     await page.goto(BASE_URL);\n> 341 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n      |                ^\n  342 |     await page.locator('text=Start New Nesting').click();\n  343 |     await page.waitForSelector('#screen-add.active', { timeout: 4000 });\n  344 |   });"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/printing.spec.js",
                            "column": 16,
                            "line": 341
                          },
                          "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n\n  339 |     await injectSession(page);\n  340 |     await page.goto(BASE_URL);\n> 341 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n      |                ^\n  342 |     await page.locator('text=Start New Nesting').click();\n  343 |     await page.waitForSelector('#screen-add.active', { timeout: 4000 });\n  344 |   });\n    at /var/www/360lm/tests/printing.spec.js:341:16"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 1,
                      "startTime": "2026-07-12T13:33:31.121Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "screenshot",
                          "contentType": "image/png",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-2D-L-562f8--button-shows-hides-details-desktop-chrome-retry1/test-failed-1.png"
                        },
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-2D-L-562f8--button-shows-hides-details-desktop-chrome-retry1/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/printing.spec.js",
                        "column": 16,
                        "line": 341
                      }
                    }
                  ],
                  "status": "unexpected"
                }
              ],
              "id": "84f3bc84c047e9f9c0b8-028251705e43e78d29c5",
              "file": "printing.spec.js",
              "line": 409,
              "column": 3
            },
            {
              "title": "demo items produce 2-lane nesting for both Flex and Vinyl",
              "ok": false,
              "tags": [],
              "tests": [
                {
                  "timeout": 30000,
                  "annotations": [],
                  "expectedStatus": "passed",
                  "projectId": "desktop-chrome",
                  "projectName": "desktop-chrome",
                  "results": [
                    {
                      "workerIndex": 302,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 8275,
                      "error": {
                        "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n",
                        "stack": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n    at /var/www/360lm/tests/printing.spec.js:341:16",
                        "location": {
                          "file": "/var/www/360lm/tests/printing.spec.js",
                          "column": 16,
                          "line": 341
                        },
                        "snippet": "  339 |     await injectSession(page);\n  340 |     await page.goto(BASE_URL);\n> 341 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n      |                ^\n  342 |     await page.locator('text=Start New Nesting').click();\n  343 |     await page.waitForSelector('#screen-add.active', { timeout: 4000 });\n  344 |   });"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/printing.spec.js",
                            "column": 16,
                            "line": 341
                          },
                          "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n\n  339 |     await injectSession(page);\n  340 |     await page.goto(BASE_URL);\n> 341 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n      |                ^\n  342 |     await page.locator('text=Start New Nesting').click();\n  343 |     await page.waitForSelector('#screen-add.active', { timeout: 4000 });\n  344 |   });\n    at /var/www/360lm/tests/printing.spec.js:341:16"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 0,
                      "startTime": "2026-07-12T13:33:40.511Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "screenshot",
                          "contentType": "image/png",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-2D-L-32b33-ing-for-both-Flex-and-Vinyl-desktop-chrome/test-failed-1.png"
                        },
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-2D-L-32b33-ing-for-both-Flex-and-Vinyl-desktop-chrome/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/printing.spec.js",
                        "column": 16,
                        "line": 341
                      }
                    },
                    {
                      "workerIndex": 303,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 8260,
                      "error": {
                        "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n",
                        "stack": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n    at /var/www/360lm/tests/printing.spec.js:341:16",
                        "location": {
                          "file": "/var/www/360lm/tests/printing.spec.js",
                          "column": 16,
                          "line": 341
                        },
                        "snippet": "  339 |     await injectSession(page);\n  340 |     await page.goto(BASE_URL);\n> 341 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n      |                ^\n  342 |     await page.locator('text=Start New Nesting').click();\n  343 |     await page.waitForSelector('#screen-add.active', { timeout: 4000 });\n  344 |   });"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/printing.spec.js",
                            "column": 16,
                            "line": 341
                          },
                          "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n\n  339 |     await injectSession(page);\n  340 |     await page.goto(BASE_URL);\n> 341 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n      |                ^\n  342 |     await page.locator('text=Start New Nesting').click();\n  343 |     await page.waitForSelector('#screen-add.active', { timeout: 4000 });\n  344 |   });\n    at /var/www/360lm/tests/printing.spec.js:341:16"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 1,
                      "startTime": "2026-07-12T13:33:49.761Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "screenshot",
                          "contentType": "image/png",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-2D-L-32b33-ing-for-both-Flex-and-Vinyl-desktop-chrome-retry1/test-failed-1.png"
                        },
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-2D-L-32b33-ing-for-both-Flex-and-Vinyl-desktop-chrome-retry1/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/printing.spec.js",
                        "column": 16,
                        "line": 341
                      }
                    }
                  ],
                  "status": "unexpected"
                }
              ],
              "id": "84f3bc84c047e9f9c0b8-a7e7ae131d8b42c53bce",
              "file": "printing.spec.js",
              "line": 420,
              "column": 3
            }
          ]
        },
        {
          "title": "Print Nest — Runs Screen",
          "file": "printing.spec.js",
          "line": 434,
          "column": 6,
          "specs": [
            {
              "title": "runs screen renders (empty or list)",
              "ok": false,
              "tags": [],
              "tests": [
                {
                  "timeout": 30000,
                  "annotations": [],
                  "expectedStatus": "passed",
                  "projectId": "android-chrome",
                  "projectName": "android-chrome",
                  "results": [
                    {
                      "workerIndex": 128,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 8265,
                      "error": {
                        "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n",
                        "stack": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n    at /var/www/360lm/tests/printing.spec.js:438:16",
                        "location": {
                          "file": "/var/www/360lm/tests/printing.spec.js",
                          "column": 16,
                          "line": 438
                        },
                        "snippet": "  436 |     await injectSession(page);\n  437 |     await page.goto(BASE_URL);\n> 438 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n      |                ^\n  439 |     await page.locator('#screen-dash .nav-btn', { hasText: 'Runs' }).click();\n  440 |     await page.waitForSelector('#screen-runs.active', { timeout: 4000 });\n  441 |   });"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/printing.spec.js",
                            "column": 16,
                            "line": 438
                          },
                          "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n\n  436 |     await injectSession(page);\n  437 |     await page.goto(BASE_URL);\n> 438 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n      |                ^\n  439 |     await page.locator('#screen-dash .nav-btn', { hasText: 'Runs' }).click();\n  440 |     await page.waitForSelector('#screen-runs.active', { timeout: 4000 });\n  441 |   });\n    at /var/www/360lm/tests/printing.spec.js:438:16"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 0,
                      "startTime": "2026-07-12T13:13:48.664Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "screenshot",
                          "contentType": "image/png",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-Runs-cdde4-reen-renders-empty-or-list--android-chrome/test-failed-1.png"
                        },
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-Runs-cdde4-reen-renders-empty-or-list--android-chrome/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/printing.spec.js",
                        "column": 16,
                        "line": 438
                      }
                    },
                    {
                      "workerIndex": 129,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 8317,
                      "error": {
                        "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n",
                        "stack": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n    at /var/www/360lm/tests/printing.spec.js:438:16",
                        "location": {
                          "file": "/var/www/360lm/tests/printing.spec.js",
                          "column": 16,
                          "line": 438
                        },
                        "snippet": "  436 |     await injectSession(page);\n  437 |     await page.goto(BASE_URL);\n> 438 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n      |                ^\n  439 |     await page.locator('#screen-dash .nav-btn', { hasText: 'Runs' }).click();\n  440 |     await page.waitForSelector('#screen-runs.active', { timeout: 4000 });\n  441 |   });"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/printing.spec.js",
                            "column": 16,
                            "line": 438
                          },
                          "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n\n  436 |     await injectSession(page);\n  437 |     await page.goto(BASE_URL);\n> 438 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n      |                ^\n  439 |     await page.locator('#screen-dash .nav-btn', { hasText: 'Runs' }).click();\n  440 |     await page.waitForSelector('#screen-runs.active', { timeout: 4000 });\n  441 |   });\n    at /var/www/360lm/tests/printing.spec.js:438:16"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 1,
                      "startTime": "2026-07-12T13:13:58.138Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "screenshot",
                          "contentType": "image/png",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-Runs-cdde4-reen-renders-empty-or-list--android-chrome-retry1/test-failed-1.png"
                        },
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-Runs-cdde4-reen-renders-empty-or-list--android-chrome-retry1/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/printing.spec.js",
                        "column": 16,
                        "line": 438
                      }
                    }
                  ],
                  "status": "unexpected"
                }
              ],
              "id": "84f3bc84c047e9f9c0b8-3b25031229719607eb5b",
              "file": "printing.spec.js",
              "line": 443,
              "column": 3
            },
            {
              "title": "filter bar shows all 5 filter buttons",
              "ok": false,
              "tags": [],
              "tests": [
                {
                  "timeout": 30000,
                  "annotations": [],
                  "expectedStatus": "passed",
                  "projectId": "android-chrome",
                  "projectName": "android-chrome",
                  "results": [
                    {
                      "workerIndex": 130,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 8320,
                      "error": {
                        "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n",
                        "stack": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n    at /var/www/360lm/tests/printing.spec.js:438:16",
                        "location": {
                          "file": "/var/www/360lm/tests/printing.spec.js",
                          "column": 16,
                          "line": 438
                        },
                        "snippet": "  436 |     await injectSession(page);\n  437 |     await page.goto(BASE_URL);\n> 438 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n      |                ^\n  439 |     await page.locator('#screen-dash .nav-btn', { hasText: 'Runs' }).click();\n  440 |     await page.waitForSelector('#screen-runs.active', { timeout: 4000 });\n  441 |   });"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/printing.spec.js",
                            "column": 16,
                            "line": 438
                          },
                          "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n\n  436 |     await injectSession(page);\n  437 |     await page.goto(BASE_URL);\n> 438 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n      |                ^\n  439 |     await page.locator('#screen-dash .nav-btn', { hasText: 'Runs' }).click();\n  440 |     await page.waitForSelector('#screen-runs.active', { timeout: 4000 });\n  441 |   });\n    at /var/www/360lm/tests/printing.spec.js:438:16"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 0,
                      "startTime": "2026-07-12T13:14:07.640Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "screenshot",
                          "contentType": "image/png",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-Runs-7d012--shows-all-5-filter-buttons-android-chrome/test-failed-1.png"
                        },
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-Runs-7d012--shows-all-5-filter-buttons-android-chrome/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/printing.spec.js",
                        "column": 16,
                        "line": 438
                      }
                    },
                    {
                      "workerIndex": 131,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 8263,
                      "error": {
                        "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n",
                        "stack": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n    at /var/www/360lm/tests/printing.spec.js:438:16",
                        "location": {
                          "file": "/var/www/360lm/tests/printing.spec.js",
                          "column": 16,
                          "line": 438
                        },
                        "snippet": "  436 |     await injectSession(page);\n  437 |     await page.goto(BASE_URL);\n> 438 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n      |                ^\n  439 |     await page.locator('#screen-dash .nav-btn', { hasText: 'Runs' }).click();\n  440 |     await page.waitForSelector('#screen-runs.active', { timeout: 4000 });\n  441 |   });"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/printing.spec.js",
                            "column": 16,
                            "line": 438
                          },
                          "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n\n  436 |     await injectSession(page);\n  437 |     await page.goto(BASE_URL);\n> 438 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n      |                ^\n  439 |     await page.locator('#screen-dash .nav-btn', { hasText: 'Runs' }).click();\n  440 |     await page.waitForSelector('#screen-runs.active', { timeout: 4000 });\n  441 |   });\n    at /var/www/360lm/tests/printing.spec.js:438:16"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 1,
                      "startTime": "2026-07-12T13:14:17.184Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "screenshot",
                          "contentType": "image/png",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-Runs-7d012--shows-all-5-filter-buttons-android-chrome-retry1/test-failed-1.png"
                        },
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-Runs-7d012--shows-all-5-filter-buttons-android-chrome-retry1/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/printing.spec.js",
                        "column": 16,
                        "line": 438
                      }
                    }
                  ],
                  "status": "unexpected"
                }
              ],
              "id": "84f3bc84c047e9f9c0b8-56b7969aaceb925add18",
              "file": "printing.spec.js",
              "line": 450,
              "column": 3
            },
            {
              "title": "\"All\" filter is active by default",
              "ok": false,
              "tags": [],
              "tests": [
                {
                  "timeout": 30000,
                  "annotations": [],
                  "expectedStatus": "passed",
                  "projectId": "android-chrome",
                  "projectName": "android-chrome",
                  "results": [
                    {
                      "workerIndex": 132,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 8326,
                      "error": {
                        "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n",
                        "stack": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n    at /var/www/360lm/tests/printing.spec.js:438:16",
                        "location": {
                          "file": "/var/www/360lm/tests/printing.spec.js",
                          "column": 16,
                          "line": 438
                        },
                        "snippet": "  436 |     await injectSession(page);\n  437 |     await page.goto(BASE_URL);\n> 438 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n      |                ^\n  439 |     await page.locator('#screen-dash .nav-btn', { hasText: 'Runs' }).click();\n  440 |     await page.waitForSelector('#screen-runs.active', { timeout: 4000 });\n  441 |   });"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/printing.spec.js",
                            "column": 16,
                            "line": 438
                          },
                          "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n\n  436 |     await injectSession(page);\n  437 |     await page.goto(BASE_URL);\n> 438 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n      |                ^\n  439 |     await page.locator('#screen-dash .nav-btn', { hasText: 'Runs' }).click();\n  440 |     await page.waitForSelector('#screen-runs.active', { timeout: 4000 });\n  441 |   });\n    at /var/www/360lm/tests/printing.spec.js:438:16"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 0,
                      "startTime": "2026-07-12T13:14:26.528Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "screenshot",
                          "contentType": "image/png",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-Runs-2ce68-filter-is-active-by-default-android-chrome/test-failed-1.png"
                        },
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-Runs-2ce68-filter-is-active-by-default-android-chrome/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/printing.spec.js",
                        "column": 16,
                        "line": 438
                      }
                    },
                    {
                      "workerIndex": 133,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 8307,
                      "error": {
                        "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n",
                        "stack": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n    at /var/www/360lm/tests/printing.spec.js:438:16",
                        "location": {
                          "file": "/var/www/360lm/tests/printing.spec.js",
                          "column": 16,
                          "line": 438
                        },
                        "snippet": "  436 |     await injectSession(page);\n  437 |     await page.goto(BASE_URL);\n> 438 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n      |                ^\n  439 |     await page.locator('#screen-dash .nav-btn', { hasText: 'Runs' }).click();\n  440 |     await page.waitForSelector('#screen-runs.active', { timeout: 4000 });\n  441 |   });"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/printing.spec.js",
                            "column": 16,
                            "line": 438
                          },
                          "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n\n  436 |     await injectSession(page);\n  437 |     await page.goto(BASE_URL);\n> 438 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n      |                ^\n  439 |     await page.locator('#screen-dash .nav-btn', { hasText: 'Runs' }).click();\n  440 |     await page.waitForSelector('#screen-runs.active', { timeout: 4000 });\n  441 |   });\n    at /var/www/360lm/tests/printing.spec.js:438:16"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 1,
                      "startTime": "2026-07-12T13:14:35.934Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "screenshot",
                          "contentType": "image/png",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-Runs-2ce68-filter-is-active-by-default-android-chrome-retry1/test-failed-1.png"
                        },
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-Runs-2ce68-filter-is-active-by-default-android-chrome-retry1/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/printing.spec.js",
                        "column": 16,
                        "line": 438
                      }
                    }
                  ],
                  "status": "unexpected"
                }
              ],
              "id": "84f3bc84c047e9f9c0b8-c9cb08940ea8b6185118",
              "file": "printing.spec.js",
              "line": 460,
              "column": 3
            },
            {
              "title": "clicking a filter button makes it active and deactivates others",
              "ok": false,
              "tags": [],
              "tests": [
                {
                  "timeout": 30000,
                  "annotations": [],
                  "expectedStatus": "passed",
                  "projectId": "android-chrome",
                  "projectName": "android-chrome",
                  "results": [
                    {
                      "workerIndex": 134,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 8462,
                      "error": {
                        "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n",
                        "stack": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n    at /var/www/360lm/tests/printing.spec.js:438:16",
                        "location": {
                          "file": "/var/www/360lm/tests/printing.spec.js",
                          "column": 16,
                          "line": 438
                        },
                        "snippet": "  436 |     await injectSession(page);\n  437 |     await page.goto(BASE_URL);\n> 438 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n      |                ^\n  439 |     await page.locator('#screen-dash .nav-btn', { hasText: 'Runs' }).click();\n  440 |     await page.waitForSelector('#screen-runs.active', { timeout: 4000 });\n  441 |   });"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/printing.spec.js",
                            "column": 16,
                            "line": 438
                          },
                          "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n\n  436 |     await injectSession(page);\n  437 |     await page.goto(BASE_URL);\n> 438 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n      |                ^\n  439 |     await page.locator('#screen-dash .nav-btn', { hasText: 'Runs' }).click();\n  440 |     await page.waitForSelector('#screen-runs.active', { timeout: 4000 });\n  441 |   });\n    at /var/www/360lm/tests/printing.spec.js:438:16"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 0,
                      "startTime": "2026-07-12T13:14:45.355Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "screenshot",
                          "contentType": "image/png",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-Runs-0c5e5-tive-and-deactivates-others-android-chrome/test-failed-1.png"
                        },
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-Runs-0c5e5-tive-and-deactivates-others-android-chrome/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/printing.spec.js",
                        "column": 16,
                        "line": 438
                      }
                    },
                    {
                      "workerIndex": 135,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 8848,
                      "error": {
                        "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n",
                        "stack": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n    at /var/www/360lm/tests/printing.spec.js:438:16",
                        "location": {
                          "file": "/var/www/360lm/tests/printing.spec.js",
                          "column": 16,
                          "line": 438
                        },
                        "snippet": "  436 |     await injectSession(page);\n  437 |     await page.goto(BASE_URL);\n> 438 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n      |                ^\n  439 |     await page.locator('#screen-dash .nav-btn', { hasText: 'Runs' }).click();\n  440 |     await page.waitForSelector('#screen-runs.active', { timeout: 4000 });\n  441 |   });"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/printing.spec.js",
                            "column": 16,
                            "line": 438
                          },
                          "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n\n  436 |     await injectSession(page);\n  437 |     await page.goto(BASE_URL);\n> 438 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n      |                ^\n  439 |     await page.locator('#screen-dash .nav-btn', { hasText: 'Runs' }).click();\n  440 |     await page.waitForSelector('#screen-runs.active', { timeout: 4000 });\n  441 |   });\n    at /var/www/360lm/tests/printing.spec.js:438:16"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 1,
                      "startTime": "2026-07-12T13:14:55.751Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "screenshot",
                          "contentType": "image/png",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-Runs-0c5e5-tive-and-deactivates-others-android-chrome-retry1/test-failed-1.png"
                        },
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-Runs-0c5e5-tive-and-deactivates-others-android-chrome-retry1/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/printing.spec.js",
                        "column": 16,
                        "line": 438
                      }
                    }
                  ],
                  "status": "unexpected"
                }
              ],
              "id": "84f3bc84c047e9f9c0b8-435850aac22d8ef6d082",
              "file": "printing.spec.js",
              "line": 464,
              "column": 3
            },
            {
              "title": "clicking Planned filter reloads runs-body",
              "ok": false,
              "tags": [],
              "tests": [
                {
                  "timeout": 30000,
                  "annotations": [],
                  "expectedStatus": "passed",
                  "projectId": "android-chrome",
                  "projectName": "android-chrome",
                  "results": [
                    {
                      "workerIndex": 136,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 8533,
                      "error": {
                        "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n",
                        "stack": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n    at /var/www/360lm/tests/printing.spec.js:438:16",
                        "location": {
                          "file": "/var/www/360lm/tests/printing.spec.js",
                          "column": 16,
                          "line": 438
                        },
                        "snippet": "  436 |     await injectSession(page);\n  437 |     await page.goto(BASE_URL);\n> 438 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n      |                ^\n  439 |     await page.locator('#screen-dash .nav-btn', { hasText: 'Runs' }).click();\n  440 |     await page.waitForSelector('#screen-runs.active', { timeout: 4000 });\n  441 |   });"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/printing.spec.js",
                            "column": 16,
                            "line": 438
                          },
                          "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n\n  436 |     await injectSession(page);\n  437 |     await page.goto(BASE_URL);\n> 438 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n      |                ^\n  439 |     await page.locator('#screen-dash .nav-btn', { hasText: 'Runs' }).click();\n  440 |     await page.waitForSelector('#screen-runs.active', { timeout: 4000 });\n  441 |   });\n    at /var/www/360lm/tests/printing.spec.js:438:16"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 0,
                      "startTime": "2026-07-12T13:15:06.599Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "screenshot",
                          "contentType": "image/png",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-Runs-a8819-ed-filter-reloads-runs-body-android-chrome/test-failed-1.png"
                        },
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-Runs-a8819-ed-filter-reloads-runs-body-android-chrome/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/printing.spec.js",
                        "column": 16,
                        "line": 438
                      }
                    },
                    {
                      "workerIndex": 137,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 8247,
                      "error": {
                        "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n",
                        "stack": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n    at /var/www/360lm/tests/printing.spec.js:438:16",
                        "location": {
                          "file": "/var/www/360lm/tests/printing.spec.js",
                          "column": 16,
                          "line": 438
                        },
                        "snippet": "  436 |     await injectSession(page);\n  437 |     await page.goto(BASE_URL);\n> 438 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n      |                ^\n  439 |     await page.locator('#screen-dash .nav-btn', { hasText: 'Runs' }).click();\n  440 |     await page.waitForSelector('#screen-runs.active', { timeout: 4000 });\n  441 |   });"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/printing.spec.js",
                            "column": 16,
                            "line": 438
                          },
                          "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n\n  436 |     await injectSession(page);\n  437 |     await page.goto(BASE_URL);\n> 438 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n      |                ^\n  439 |     await page.locator('#screen-dash .nav-btn', { hasText: 'Runs' }).click();\n  440 |     await page.waitForSelector('#screen-runs.active', { timeout: 4000 });\n  441 |   });\n    at /var/www/360lm/tests/printing.spec.js:438:16"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 1,
                      "startTime": "2026-07-12T13:15:16.306Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "screenshot",
                          "contentType": "image/png",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-Runs-a8819-ed-filter-reloads-runs-body-android-chrome-retry1/test-failed-1.png"
                        },
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-Runs-a8819-ed-filter-reloads-runs-body-android-chrome-retry1/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/printing.spec.js",
                        "column": 16,
                        "line": 438
                      }
                    }
                  ],
                  "status": "unexpected"
                }
              ],
              "id": "84f3bc84c047e9f9c0b8-27b361306b2c7060ae14",
              "file": "printing.spec.js",
              "line": 471,
              "column": 3
            },
            {
              "title": "runs screen renders (empty or list)",
              "ok": false,
              "tags": [],
              "tests": [
                {
                  "timeout": 30000,
                  "annotations": [],
                  "expectedStatus": "passed",
                  "projectId": "desktop-chrome",
                  "projectName": "desktop-chrome",
                  "results": [
                    {
                      "workerIndex": 304,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 8254,
                      "error": {
                        "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n",
                        "stack": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n    at /var/www/360lm/tests/printing.spec.js:438:16",
                        "location": {
                          "file": "/var/www/360lm/tests/printing.spec.js",
                          "column": 16,
                          "line": 438
                        },
                        "snippet": "  436 |     await injectSession(page);\n  437 |     await page.goto(BASE_URL);\n> 438 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n      |                ^\n  439 |     await page.locator('#screen-dash .nav-btn', { hasText: 'Runs' }).click();\n  440 |     await page.waitForSelector('#screen-runs.active', { timeout: 4000 });\n  441 |   });"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/printing.spec.js",
                            "column": 16,
                            "line": 438
                          },
                          "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n\n  436 |     await injectSession(page);\n  437 |     await page.goto(BASE_URL);\n> 438 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n      |                ^\n  439 |     await page.locator('#screen-dash .nav-btn', { hasText: 'Runs' }).click();\n  440 |     await page.waitForSelector('#screen-runs.active', { timeout: 4000 });\n  441 |   });\n    at /var/www/360lm/tests/printing.spec.js:438:16"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 0,
                      "startTime": "2026-07-12T13:33:59.026Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "screenshot",
                          "contentType": "image/png",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-Runs-cdde4-reen-renders-empty-or-list--desktop-chrome/test-failed-1.png"
                        },
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-Runs-cdde4-reen-renders-empty-or-list--desktop-chrome/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/printing.spec.js",
                        "column": 16,
                        "line": 438
                      }
                    },
                    {
                      "workerIndex": 305,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 8234,
                      "error": {
                        "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n",
                        "stack": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n    at /var/www/360lm/tests/printing.spec.js:438:16",
                        "location": {
                          "file": "/var/www/360lm/tests/printing.spec.js",
                          "column": 16,
                          "line": 438
                        },
                        "snippet": "  436 |     await injectSession(page);\n  437 |     await page.goto(BASE_URL);\n> 438 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n      |                ^\n  439 |     await page.locator('#screen-dash .nav-btn', { hasText: 'Runs' }).click();\n  440 |     await page.waitForSelector('#screen-runs.active', { timeout: 4000 });\n  441 |   });"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/printing.spec.js",
                            "column": 16,
                            "line": 438
                          },
                          "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n\n  436 |     await injectSession(page);\n  437 |     await page.goto(BASE_URL);\n> 438 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n      |                ^\n  439 |     await page.locator('#screen-dash .nav-btn', { hasText: 'Runs' }).click();\n  440 |     await page.waitForSelector('#screen-runs.active', { timeout: 4000 });\n  441 |   });\n    at /var/www/360lm/tests/printing.spec.js:438:16"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 1,
                      "startTime": "2026-07-12T13:34:08.274Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "screenshot",
                          "contentType": "image/png",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-Runs-cdde4-reen-renders-empty-or-list--desktop-chrome-retry1/test-failed-1.png"
                        },
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-Runs-cdde4-reen-renders-empty-or-list--desktop-chrome-retry1/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/printing.spec.js",
                        "column": 16,
                        "line": 438
                      }
                    }
                  ],
                  "status": "unexpected"
                }
              ],
              "id": "84f3bc84c047e9f9c0b8-7abf91c4d08d19b26e24",
              "file": "printing.spec.js",
              "line": 443,
              "column": 3
            },
            {
              "title": "filter bar shows all 5 filter buttons",
              "ok": false,
              "tags": [],
              "tests": [
                {
                  "timeout": 30000,
                  "annotations": [],
                  "expectedStatus": "passed",
                  "projectId": "desktop-chrome",
                  "projectName": "desktop-chrome",
                  "results": [
                    {
                      "workerIndex": 306,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 8267,
                      "error": {
                        "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n",
                        "stack": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n    at /var/www/360lm/tests/printing.spec.js:438:16",
                        "location": {
                          "file": "/var/www/360lm/tests/printing.spec.js",
                          "column": 16,
                          "line": 438
                        },
                        "snippet": "  436 |     await injectSession(page);\n  437 |     await page.goto(BASE_URL);\n> 438 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n      |                ^\n  439 |     await page.locator('#screen-dash .nav-btn', { hasText: 'Runs' }).click();\n  440 |     await page.waitForSelector('#screen-runs.active', { timeout: 4000 });\n  441 |   });"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/printing.spec.js",
                            "column": 16,
                            "line": 438
                          },
                          "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n\n  436 |     await injectSession(page);\n  437 |     await page.goto(BASE_URL);\n> 438 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n      |                ^\n  439 |     await page.locator('#screen-dash .nav-btn', { hasText: 'Runs' }).click();\n  440 |     await page.waitForSelector('#screen-runs.active', { timeout: 4000 });\n  441 |   });\n    at /var/www/360lm/tests/printing.spec.js:438:16"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 0,
                      "startTime": "2026-07-12T13:34:17.558Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "screenshot",
                          "contentType": "image/png",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-Runs-7d012--shows-all-5-filter-buttons-desktop-chrome/test-failed-1.png"
                        },
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-Runs-7d012--shows-all-5-filter-buttons-desktop-chrome/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/printing.spec.js",
                        "column": 16,
                        "line": 438
                      }
                    },
                    {
                      "workerIndex": 307,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 8319,
                      "error": {
                        "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n",
                        "stack": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n    at /var/www/360lm/tests/printing.spec.js:438:16",
                        "location": {
                          "file": "/var/www/360lm/tests/printing.spec.js",
                          "column": 16,
                          "line": 438
                        },
                        "snippet": "  436 |     await injectSession(page);\n  437 |     await page.goto(BASE_URL);\n> 438 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n      |                ^\n  439 |     await page.locator('#screen-dash .nav-btn', { hasText: 'Runs' }).click();\n  440 |     await page.waitForSelector('#screen-runs.active', { timeout: 4000 });\n  441 |   });"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/printing.spec.js",
                            "column": 16,
                            "line": 438
                          },
                          "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n\n  436 |     await injectSession(page);\n  437 |     await page.goto(BASE_URL);\n> 438 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n      |                ^\n  439 |     await page.locator('#screen-dash .nav-btn', { hasText: 'Runs' }).click();\n  440 |     await page.waitForSelector('#screen-runs.active', { timeout: 4000 });\n  441 |   });\n    at /var/www/360lm/tests/printing.spec.js:438:16"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 1,
                      "startTime": "2026-07-12T13:34:26.814Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "screenshot",
                          "contentType": "image/png",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-Runs-7d012--shows-all-5-filter-buttons-desktop-chrome-retry1/test-failed-1.png"
                        },
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-Runs-7d012--shows-all-5-filter-buttons-desktop-chrome-retry1/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/printing.spec.js",
                        "column": 16,
                        "line": 438
                      }
                    }
                  ],
                  "status": "unexpected"
                }
              ],
              "id": "84f3bc84c047e9f9c0b8-6103064ec5e5d704465f",
              "file": "printing.spec.js",
              "line": 450,
              "column": 3
            },
            {
              "title": "\"All\" filter is active by default",
              "ok": false,
              "tags": [],
              "tests": [
                {
                  "timeout": 30000,
                  "annotations": [],
                  "expectedStatus": "passed",
                  "projectId": "desktop-chrome",
                  "projectName": "desktop-chrome",
                  "results": [
                    {
                      "workerIndex": 308,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 8239,
                      "error": {
                        "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n",
                        "stack": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n    at /var/www/360lm/tests/printing.spec.js:438:16",
                        "location": {
                          "file": "/var/www/360lm/tests/printing.spec.js",
                          "column": 16,
                          "line": 438
                        },
                        "snippet": "  436 |     await injectSession(page);\n  437 |     await page.goto(BASE_URL);\n> 438 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n      |                ^\n  439 |     await page.locator('#screen-dash .nav-btn', { hasText: 'Runs' }).click();\n  440 |     await page.waitForSelector('#screen-runs.active', { timeout: 4000 });\n  441 |   });"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/printing.spec.js",
                            "column": 16,
                            "line": 438
                          },
                          "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n\n  436 |     await injectSession(page);\n  437 |     await page.goto(BASE_URL);\n> 438 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n      |                ^\n  439 |     await page.locator('#screen-dash .nav-btn', { hasText: 'Runs' }).click();\n  440 |     await page.waitForSelector('#screen-runs.active', { timeout: 4000 });\n  441 |   });\n    at /var/www/360lm/tests/printing.spec.js:438:16"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 0,
                      "startTime": "2026-07-12T13:34:36.198Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "screenshot",
                          "contentType": "image/png",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-Runs-2ce68-filter-is-active-by-default-desktop-chrome/test-failed-1.png"
                        },
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-Runs-2ce68-filter-is-active-by-default-desktop-chrome/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/printing.spec.js",
                        "column": 16,
                        "line": 438
                      }
                    },
                    {
                      "workerIndex": 309,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 8259,
                      "error": {
                        "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n",
                        "stack": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n    at /var/www/360lm/tests/printing.spec.js:438:16",
                        "location": {
                          "file": "/var/www/360lm/tests/printing.spec.js",
                          "column": 16,
                          "line": 438
                        },
                        "snippet": "  436 |     await injectSession(page);\n  437 |     await page.goto(BASE_URL);\n> 438 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n      |                ^\n  439 |     await page.locator('#screen-dash .nav-btn', { hasText: 'Runs' }).click();\n  440 |     await page.waitForSelector('#screen-runs.active', { timeout: 4000 });\n  441 |   });"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/printing.spec.js",
                            "column": 16,
                            "line": 438
                          },
                          "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n\n  436 |     await injectSession(page);\n  437 |     await page.goto(BASE_URL);\n> 438 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n      |                ^\n  439 |     await page.locator('#screen-dash .nav-btn', { hasText: 'Runs' }).click();\n  440 |     await page.waitForSelector('#screen-runs.active', { timeout: 4000 });\n  441 |   });\n    at /var/www/360lm/tests/printing.spec.js:438:16"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 1,
                      "startTime": "2026-07-12T13:34:45.511Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "screenshot",
                          "contentType": "image/png",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-Runs-2ce68-filter-is-active-by-default-desktop-chrome-retry1/test-failed-1.png"
                        },
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-Runs-2ce68-filter-is-active-by-default-desktop-chrome-retry1/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/printing.spec.js",
                        "column": 16,
                        "line": 438
                      }
                    }
                  ],
                  "status": "unexpected"
                }
              ],
              "id": "84f3bc84c047e9f9c0b8-47f2c1728fbdc72349f8",
              "file": "printing.spec.js",
              "line": 460,
              "column": 3
            },
            {
              "title": "clicking a filter button makes it active and deactivates others",
              "ok": false,
              "tags": [],
              "tests": [
                {
                  "timeout": 30000,
                  "annotations": [],
                  "expectedStatus": "passed",
                  "projectId": "desktop-chrome",
                  "projectName": "desktop-chrome",
                  "results": [
                    {
                      "workerIndex": 310,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 8314,
                      "error": {
                        "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n",
                        "stack": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n    at /var/www/360lm/tests/printing.spec.js:438:16",
                        "location": {
                          "file": "/var/www/360lm/tests/printing.spec.js",
                          "column": 16,
                          "line": 438
                        },
                        "snippet": "  436 |     await injectSession(page);\n  437 |     await page.goto(BASE_URL);\n> 438 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n      |                ^\n  439 |     await page.locator('#screen-dash .nav-btn', { hasText: 'Runs' }).click();\n  440 |     await page.waitForSelector('#screen-runs.active', { timeout: 4000 });\n  441 |   });"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/printing.spec.js",
                            "column": 16,
                            "line": 438
                          },
                          "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n\n  436 |     await injectSession(page);\n  437 |     await page.goto(BASE_URL);\n> 438 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n      |                ^\n  439 |     await page.locator('#screen-dash .nav-btn', { hasText: 'Runs' }).click();\n  440 |     await page.waitForSelector('#screen-runs.active', { timeout: 4000 });\n  441 |   });\n    at /var/www/360lm/tests/printing.spec.js:438:16"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 0,
                      "startTime": "2026-07-12T13:34:54.749Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "screenshot",
                          "contentType": "image/png",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-Runs-0c5e5-tive-and-deactivates-others-desktop-chrome/test-failed-1.png"
                        },
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-Runs-0c5e5-tive-and-deactivates-others-desktop-chrome/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/printing.spec.js",
                        "column": 16,
                        "line": 438
                      }
                    },
                    {
                      "workerIndex": 311,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 8284,
                      "error": {
                        "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n",
                        "stack": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n    at /var/www/360lm/tests/printing.spec.js:438:16",
                        "location": {
                          "file": "/var/www/360lm/tests/printing.spec.js",
                          "column": 16,
                          "line": 438
                        },
                        "snippet": "  436 |     await injectSession(page);\n  437 |     await page.goto(BASE_URL);\n> 438 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n      |                ^\n  439 |     await page.locator('#screen-dash .nav-btn', { hasText: 'Runs' }).click();\n  440 |     await page.waitForSelector('#screen-runs.active', { timeout: 4000 });\n  441 |   });"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/printing.spec.js",
                            "column": 16,
                            "line": 438
                          },
                          "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n\n  436 |     await injectSession(page);\n  437 |     await page.goto(BASE_URL);\n> 438 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n      |                ^\n  439 |     await page.locator('#screen-dash .nav-btn', { hasText: 'Runs' }).click();\n  440 |     await page.waitForSelector('#screen-runs.active', { timeout: 4000 });\n  441 |   });\n    at /var/www/360lm/tests/printing.spec.js:438:16"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 1,
                      "startTime": "2026-07-12T13:35:04.425Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "screenshot",
                          "contentType": "image/png",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-Runs-0c5e5-tive-and-deactivates-others-desktop-chrome-retry1/test-failed-1.png"
                        },
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-Runs-0c5e5-tive-and-deactivates-others-desktop-chrome-retry1/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/printing.spec.js",
                        "column": 16,
                        "line": 438
                      }
                    }
                  ],
                  "status": "unexpected"
                }
              ],
              "id": "84f3bc84c047e9f9c0b8-2429fa665e9ea707305d",
              "file": "printing.spec.js",
              "line": 464,
              "column": 3
            },
            {
              "title": "clicking Planned filter reloads runs-body",
              "ok": false,
              "tags": [],
              "tests": [
                {
                  "timeout": 30000,
                  "annotations": [],
                  "expectedStatus": "passed",
                  "projectId": "desktop-chrome",
                  "projectName": "desktop-chrome",
                  "results": [
                    {
                      "workerIndex": 312,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 8275,
                      "error": {
                        "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n",
                        "stack": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n    at /var/www/360lm/tests/printing.spec.js:438:16",
                        "location": {
                          "file": "/var/www/360lm/tests/printing.spec.js",
                          "column": 16,
                          "line": 438
                        },
                        "snippet": "  436 |     await injectSession(page);\n  437 |     await page.goto(BASE_URL);\n> 438 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n      |                ^\n  439 |     await page.locator('#screen-dash .nav-btn', { hasText: 'Runs' }).click();\n  440 |     await page.waitForSelector('#screen-runs.active', { timeout: 4000 });\n  441 |   });"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/printing.spec.js",
                            "column": 16,
                            "line": 438
                          },
                          "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n\n  436 |     await injectSession(page);\n  437 |     await page.goto(BASE_URL);\n> 438 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n      |                ^\n  439 |     await page.locator('#screen-dash .nav-btn', { hasText: 'Runs' }).click();\n  440 |     await page.waitForSelector('#screen-runs.active', { timeout: 4000 });\n  441 |   });\n    at /var/www/360lm/tests/printing.spec.js:438:16"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 0,
                      "startTime": "2026-07-12T13:35:13.686Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "screenshot",
                          "contentType": "image/png",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-Runs-a8819-ed-filter-reloads-runs-body-desktop-chrome/test-failed-1.png"
                        },
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-Runs-a8819-ed-filter-reloads-runs-body-desktop-chrome/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/printing.spec.js",
                        "column": 16,
                        "line": 438
                      }
                    },
                    {
                      "workerIndex": 313,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 8249,
                      "error": {
                        "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n",
                        "stack": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n    at /var/www/360lm/tests/printing.spec.js:438:16",
                        "location": {
                          "file": "/var/www/360lm/tests/printing.spec.js",
                          "column": 16,
                          "line": 438
                        },
                        "snippet": "  436 |     await injectSession(page);\n  437 |     await page.goto(BASE_URL);\n> 438 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n      |                ^\n  439 |     await page.locator('#screen-dash .nav-btn', { hasText: 'Runs' }).click();\n  440 |     await page.waitForSelector('#screen-runs.active', { timeout: 4000 });\n  441 |   });"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/printing.spec.js",
                            "column": 16,
                            "line": 438
                          },
                          "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n\n  436 |     await injectSession(page);\n  437 |     await page.goto(BASE_URL);\n> 438 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n      |                ^\n  439 |     await page.locator('#screen-dash .nav-btn', { hasText: 'Runs' }).click();\n  440 |     await page.waitForSelector('#screen-runs.active', { timeout: 4000 });\n  441 |   });\n    at /var/www/360lm/tests/printing.spec.js:438:16"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 1,
                      "startTime": "2026-07-12T13:35:22.875Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "screenshot",
                          "contentType": "image/png",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-Runs-a8819-ed-filter-reloads-runs-body-desktop-chrome-retry1/test-failed-1.png"
                        },
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-Runs-a8819-ed-filter-reloads-runs-body-desktop-chrome-retry1/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/printing.spec.js",
                        "column": 16,
                        "line": 438
                      }
                    }
                  ],
                  "status": "unexpected"
                }
              ],
              "id": "84f3bc84c047e9f9c0b8-4ed10d688b2cc21447f5",
              "file": "printing.spec.js",
              "line": 471,
              "column": 3
            }
          ]
        },
        {
          "title": "Print Nest — Cancel Run UI",
          "file": "printing.spec.js",
          "line": 481,
          "column": 6,
          "specs": [
            {
              "title": "cancel button element exists in run modal",
              "ok": false,
              "tags": [],
              "tests": [
                {
                  "timeout": 30000,
                  "annotations": [],
                  "expectedStatus": "passed",
                  "projectId": "android-chrome",
                  "projectName": "android-chrome",
                  "results": [
                    {
                      "workerIndex": 138,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 8285,
                      "error": {
                        "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n",
                        "stack": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n    at /var/www/360lm/tests/printing.spec.js:486:16",
                        "location": {
                          "file": "/var/www/360lm/tests/printing.spec.js",
                          "column": 16,
                          "line": 486
                        },
                        "snippet": "  484 |     await injectSession(page);\n  485 |     await page.goto(BASE_URL);\n> 486 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n      |                ^\n  487 |   });\n  488 |\n  489 |   test('cancel button element exists in run modal', async ({ page }) => {"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/printing.spec.js",
                            "column": 16,
                            "line": 486
                          },
                          "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n\n  484 |     await injectSession(page);\n  485 |     await page.goto(BASE_URL);\n> 486 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n      |                ^\n  487 |   });\n  488 |\n  489 |   test('cancel button element exists in run modal', async ({ page }) => {\n    at /var/www/360lm/tests/printing.spec.js:486:16"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 0,
                      "startTime": "2026-07-12T13:15:25.583Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "screenshot",
                          "contentType": "image/png",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-Canc-219fc-element-exists-in-run-modal-android-chrome/test-failed-1.png"
                        },
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-Canc-219fc-element-exists-in-run-modal-android-chrome/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/printing.spec.js",
                        "column": 16,
                        "line": 486
                      }
                    },
                    {
                      "workerIndex": 139,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 8293,
                      "error": {
                        "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n",
                        "stack": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n    at /var/www/360lm/tests/printing.spec.js:486:16",
                        "location": {
                          "file": "/var/www/360lm/tests/printing.spec.js",
                          "column": 16,
                          "line": 486
                        },
                        "snippet": "  484 |     await injectSession(page);\n  485 |     await page.goto(BASE_URL);\n> 486 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n      |                ^\n  487 |   });\n  488 |\n  489 |   test('cancel button element exists in run modal', async ({ page }) => {"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/printing.spec.js",
                            "column": 16,
                            "line": 486
                          },
                          "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n\n  484 |     await injectSession(page);\n  485 |     await page.goto(BASE_URL);\n> 486 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n      |                ^\n  487 |   });\n  488 |\n  489 |   test('cancel button element exists in run modal', async ({ page }) => {\n    at /var/www/360lm/tests/printing.spec.js:486:16"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 1,
                      "startTime": "2026-07-12T13:15:34.948Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "screenshot",
                          "contentType": "image/png",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-Canc-219fc-element-exists-in-run-modal-android-chrome-retry1/test-failed-1.png"
                        },
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-Canc-219fc-element-exists-in-run-modal-android-chrome-retry1/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/printing.spec.js",
                        "column": 16,
                        "line": 486
                      }
                    }
                  ],
                  "status": "unexpected"
                }
              ],
              "id": "84f3bc84c047e9f9c0b8-8a867334f327fe45ec88",
              "file": "printing.spec.js",
              "line": 489,
              "column": 3
            },
            {
              "title": "cancel button starts hidden when modal is closed",
              "ok": false,
              "tags": [],
              "tests": [
                {
                  "timeout": 30000,
                  "annotations": [],
                  "expectedStatus": "passed",
                  "projectId": "android-chrome",
                  "projectName": "android-chrome",
                  "results": [
                    {
                      "workerIndex": 140,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 8296,
                      "error": {
                        "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n",
                        "stack": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n    at /var/www/360lm/tests/printing.spec.js:486:16",
                        "location": {
                          "file": "/var/www/360lm/tests/printing.spec.js",
                          "column": 16,
                          "line": 486
                        },
                        "snippet": "  484 |     await injectSession(page);\n  485 |     await page.goto(BASE_URL);\n> 486 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n      |                ^\n  487 |   });\n  488 |\n  489 |   test('cancel button element exists in run modal', async ({ page }) => {"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/printing.spec.js",
                            "column": 16,
                            "line": 486
                          },
                          "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n\n  484 |     await injectSession(page);\n  485 |     await page.goto(BASE_URL);\n> 486 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n      |                ^\n  487 |   });\n  488 |\n  489 |   test('cancel button element exists in run modal', async ({ page }) => {\n    at /var/www/360lm/tests/printing.spec.js:486:16"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 0,
                      "startTime": "2026-07-12T13:15:44.482Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "screenshot",
                          "contentType": "image/png",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-Canc-71b4a-hidden-when-modal-is-closed-android-chrome/test-failed-1.png"
                        },
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-Canc-71b4a-hidden-when-modal-is-closed-android-chrome/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/printing.spec.js",
                        "column": 16,
                        "line": 486
                      }
                    },
                    {
                      "workerIndex": 141,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 8536,
                      "error": {
                        "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n",
                        "stack": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n    at /var/www/360lm/tests/printing.spec.js:486:16",
                        "location": {
                          "file": "/var/www/360lm/tests/printing.spec.js",
                          "column": 16,
                          "line": 486
                        },
                        "snippet": "  484 |     await injectSession(page);\n  485 |     await page.goto(BASE_URL);\n> 486 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n      |                ^\n  487 |   });\n  488 |\n  489 |   test('cancel button element exists in run modal', async ({ page }) => {"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/printing.spec.js",
                            "column": 16,
                            "line": 486
                          },
                          "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n\n  484 |     await injectSession(page);\n  485 |     await page.goto(BASE_URL);\n> 486 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n      |                ^\n  487 |   });\n  488 |\n  489 |   test('cancel button element exists in run modal', async ({ page }) => {\n    at /var/www/360lm/tests/printing.spec.js:486:16"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 1,
                      "startTime": "2026-07-12T13:15:53.864Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "screenshot",
                          "contentType": "image/png",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-Canc-71b4a-hidden-when-modal-is-closed-android-chrome-retry1/test-failed-1.png"
                        },
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-Canc-71b4a-hidden-when-modal-is-closed-android-chrome-retry1/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/printing.spec.js",
                        "column": 16,
                        "line": 486
                      }
                    }
                  ],
                  "status": "unexpected"
                }
              ],
              "id": "84f3bc84c047e9f9c0b8-ab43af209b59593bf292",
              "file": "printing.spec.js",
              "line": 494,
              "column": 3
            },
            {
              "title": "cancel button shown for planned run, hidden for terminal status",
              "ok": false,
              "tags": [],
              "tests": [
                {
                  "timeout": 30000,
                  "annotations": [],
                  "expectedStatus": "passed",
                  "projectId": "android-chrome",
                  "projectName": "android-chrome",
                  "results": [
                    {
                      "workerIndex": 142,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 8314,
                      "error": {
                        "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n",
                        "stack": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n    at /var/www/360lm/tests/printing.spec.js:486:16",
                        "location": {
                          "file": "/var/www/360lm/tests/printing.spec.js",
                          "column": 16,
                          "line": 486
                        },
                        "snippet": "  484 |     await injectSession(page);\n  485 |     await page.goto(BASE_URL);\n> 486 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n      |                ^\n  487 |   });\n  488 |\n  489 |   test('cancel button element exists in run modal', async ({ page }) => {"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/printing.spec.js",
                            "column": 16,
                            "line": 486
                          },
                          "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n\n  484 |     await injectSession(page);\n  485 |     await page.goto(BASE_URL);\n> 486 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n      |                ^\n  487 |   });\n  488 |\n  489 |   test('cancel button element exists in run modal', async ({ page }) => {\n    at /var/www/360lm/tests/printing.spec.js:486:16"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 0,
                      "startTime": "2026-07-12T13:16:04.431Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "screenshot",
                          "contentType": "image/png",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-Canc-c87ef--hidden-for-terminal-status-android-chrome/test-failed-1.png"
                        },
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-Canc-c87ef--hidden-for-terminal-status-android-chrome/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/printing.spec.js",
                        "column": 16,
                        "line": 486
                      }
                    },
                    {
                      "workerIndex": 143,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 8303,
                      "error": {
                        "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n",
                        "stack": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n    at /var/www/360lm/tests/printing.spec.js:486:16",
                        "location": {
                          "file": "/var/www/360lm/tests/printing.spec.js",
                          "column": 16,
                          "line": 486
                        },
                        "snippet": "  484 |     await injectSession(page);\n  485 |     await page.goto(BASE_URL);\n> 486 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n      |                ^\n  487 |   });\n  488 |\n  489 |   test('cancel button element exists in run modal', async ({ page }) => {"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/printing.spec.js",
                            "column": 16,
                            "line": 486
                          },
                          "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n\n  484 |     await injectSession(page);\n  485 |     await page.goto(BASE_URL);\n> 486 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n      |                ^\n  487 |   });\n  488 |\n  489 |   test('cancel button element exists in run modal', async ({ page }) => {\n    at /var/www/360lm/tests/printing.spec.js:486:16"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 1,
                      "startTime": "2026-07-12T13:16:13.906Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "screenshot",
                          "contentType": "image/png",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-Canc-c87ef--hidden-for-terminal-status-android-chrome-retry1/test-failed-1.png"
                        },
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-Canc-c87ef--hidden-for-terminal-status-android-chrome-retry1/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/printing.spec.js",
                        "column": 16,
                        "line": 486
                      }
                    }
                  ],
                  "status": "unexpected"
                }
              ],
              "id": "84f3bc84c047e9f9c0b8-5fdd7cfaffff81cfc942",
              "file": "printing.spec.js",
              "line": 499,
              "column": 3
            },
            {
              "title": "cancel button element exists in run modal",
              "ok": false,
              "tags": [],
              "tests": [
                {
                  "timeout": 30000,
                  "annotations": [],
                  "expectedStatus": "passed",
                  "projectId": "desktop-chrome",
                  "projectName": "desktop-chrome",
                  "results": [
                    {
                      "workerIndex": 314,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 8286,
                      "error": {
                        "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n",
                        "stack": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n    at /var/www/360lm/tests/printing.spec.js:486:16",
                        "location": {
                          "file": "/var/www/360lm/tests/printing.spec.js",
                          "column": 16,
                          "line": 486
                        },
                        "snippet": "  484 |     await injectSession(page);\n  485 |     await page.goto(BASE_URL);\n> 486 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n      |                ^\n  487 |   });\n  488 |\n  489 |   test('cancel button element exists in run modal', async ({ page }) => {"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/printing.spec.js",
                            "column": 16,
                            "line": 486
                          },
                          "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n\n  484 |     await injectSession(page);\n  485 |     await page.goto(BASE_URL);\n> 486 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n      |                ^\n  487 |   });\n  488 |\n  489 |   test('cancel button element exists in run modal', async ({ page }) => {\n    at /var/www/360lm/tests/printing.spec.js:486:16"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 0,
                      "startTime": "2026-07-12T13:35:32.077Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "screenshot",
                          "contentType": "image/png",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-Canc-219fc-element-exists-in-run-modal-desktop-chrome/test-failed-1.png"
                        },
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-Canc-219fc-element-exists-in-run-modal-desktop-chrome/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/printing.spec.js",
                        "column": 16,
                        "line": 486
                      }
                    },
                    {
                      "workerIndex": 315,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 8239,
                      "error": {
                        "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n",
                        "stack": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n    at /var/www/360lm/tests/printing.spec.js:486:16",
                        "location": {
                          "file": "/var/www/360lm/tests/printing.spec.js",
                          "column": 16,
                          "line": 486
                        },
                        "snippet": "  484 |     await injectSession(page);\n  485 |     await page.goto(BASE_URL);\n> 486 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n      |                ^\n  487 |   });\n  488 |\n  489 |   test('cancel button element exists in run modal', async ({ page }) => {"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/printing.spec.js",
                            "column": 16,
                            "line": 486
                          },
                          "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n\n  484 |     await injectSession(page);\n  485 |     await page.goto(BASE_URL);\n> 486 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n      |                ^\n  487 |   });\n  488 |\n  489 |   test('cancel button element exists in run modal', async ({ page }) => {\n    at /var/www/360lm/tests/printing.spec.js:486:16"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 1,
                      "startTime": "2026-07-12T13:35:41.451Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "screenshot",
                          "contentType": "image/png",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-Canc-219fc-element-exists-in-run-modal-desktop-chrome-retry1/test-failed-1.png"
                        },
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-Canc-219fc-element-exists-in-run-modal-desktop-chrome-retry1/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/printing.spec.js",
                        "column": 16,
                        "line": 486
                      }
                    }
                  ],
                  "status": "unexpected"
                }
              ],
              "id": "84f3bc84c047e9f9c0b8-749e66b94839ef221205",
              "file": "printing.spec.js",
              "line": 489,
              "column": 3
            },
            {
              "title": "cancel button starts hidden when modal is closed",
              "ok": false,
              "tags": [],
              "tests": [
                {
                  "timeout": 30000,
                  "annotations": [],
                  "expectedStatus": "passed",
                  "projectId": "desktop-chrome",
                  "projectName": "desktop-chrome",
                  "results": [
                    {
                      "workerIndex": 316,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 8290,
                      "error": {
                        "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n",
                        "stack": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n    at /var/www/360lm/tests/printing.spec.js:486:16",
                        "location": {
                          "file": "/var/www/360lm/tests/printing.spec.js",
                          "column": 16,
                          "line": 486
                        },
                        "snippet": "  484 |     await injectSession(page);\n  485 |     await page.goto(BASE_URL);\n> 486 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n      |                ^\n  487 |   });\n  488 |\n  489 |   test('cancel button element exists in run modal', async ({ page }) => {"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/printing.spec.js",
                            "column": 16,
                            "line": 486
                          },
                          "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n\n  484 |     await injectSession(page);\n  485 |     await page.goto(BASE_URL);\n> 486 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n      |                ^\n  487 |   });\n  488 |\n  489 |   test('cancel button element exists in run modal', async ({ page }) => {\n    at /var/www/360lm/tests/printing.spec.js:486:16"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 0,
                      "startTime": "2026-07-12T13:35:50.746Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "screenshot",
                          "contentType": "image/png",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-Canc-71b4a-hidden-when-modal-is-closed-desktop-chrome/test-failed-1.png"
                        },
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-Canc-71b4a-hidden-when-modal-is-closed-desktop-chrome/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/printing.spec.js",
                        "column": 16,
                        "line": 486
                      }
                    },
                    {
                      "workerIndex": 317,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 8312,
                      "error": {
                        "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n",
                        "stack": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n    at /var/www/360lm/tests/printing.spec.js:486:16",
                        "location": {
                          "file": "/var/www/360lm/tests/printing.spec.js",
                          "column": 16,
                          "line": 486
                        },
                        "snippet": "  484 |     await injectSession(page);\n  485 |     await page.goto(BASE_URL);\n> 486 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n      |                ^\n  487 |   });\n  488 |\n  489 |   test('cancel button element exists in run modal', async ({ page }) => {"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/printing.spec.js",
                            "column": 16,
                            "line": 486
                          },
                          "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n\n  484 |     await injectSession(page);\n  485 |     await page.goto(BASE_URL);\n> 486 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n      |                ^\n  487 |   });\n  488 |\n  489 |   test('cancel button element exists in run modal', async ({ page }) => {\n    at /var/www/360lm/tests/printing.spec.js:486:16"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 1,
                      "startTime": "2026-07-12T13:36:00.052Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "screenshot",
                          "contentType": "image/png",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-Canc-71b4a-hidden-when-modal-is-closed-desktop-chrome-retry1/test-failed-1.png"
                        },
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-Canc-71b4a-hidden-when-modal-is-closed-desktop-chrome-retry1/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/printing.spec.js",
                        "column": 16,
                        "line": 486
                      }
                    }
                  ],
                  "status": "unexpected"
                }
              ],
              "id": "84f3bc84c047e9f9c0b8-3250c4f16befeecadab2",
              "file": "printing.spec.js",
              "line": 494,
              "column": 3
            },
            {
              "title": "cancel button shown for planned run, hidden for terminal status",
              "ok": false,
              "tags": [],
              "tests": [
                {
                  "timeout": 30000,
                  "annotations": [],
                  "expectedStatus": "passed",
                  "projectId": "desktop-chrome",
                  "projectName": "desktop-chrome",
                  "results": [
                    {
                      "workerIndex": 318,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 8278,
                      "error": {
                        "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n",
                        "stack": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n    at /var/www/360lm/tests/printing.spec.js:486:16",
                        "location": {
                          "file": "/var/www/360lm/tests/printing.spec.js",
                          "column": 16,
                          "line": 486
                        },
                        "snippet": "  484 |     await injectSession(page);\n  485 |     await page.goto(BASE_URL);\n> 486 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n      |                ^\n  487 |   });\n  488 |\n  489 |   test('cancel button element exists in run modal', async ({ page }) => {"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/printing.spec.js",
                            "column": 16,
                            "line": 486
                          },
                          "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n\n  484 |     await injectSession(page);\n  485 |     await page.goto(BASE_URL);\n> 486 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n      |                ^\n  487 |   });\n  488 |\n  489 |   test('cancel button element exists in run modal', async ({ page }) => {\n    at /var/www/360lm/tests/printing.spec.js:486:16"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 0,
                      "startTime": "2026-07-12T13:36:09.456Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "screenshot",
                          "contentType": "image/png",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-Canc-c87ef--hidden-for-terminal-status-desktop-chrome/test-failed-1.png"
                        },
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-Canc-c87ef--hidden-for-terminal-status-desktop-chrome/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/printing.spec.js",
                        "column": 16,
                        "line": 486
                      }
                    },
                    {
                      "workerIndex": 319,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 8247,
                      "error": {
                        "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n",
                        "stack": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n    at /var/www/360lm/tests/printing.spec.js:486:16",
                        "location": {
                          "file": "/var/www/360lm/tests/printing.spec.js",
                          "column": 16,
                          "line": 486
                        },
                        "snippet": "  484 |     await injectSession(page);\n  485 |     await page.goto(BASE_URL);\n> 486 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n      |                ^\n  487 |   });\n  488 |\n  489 |   test('cancel button element exists in run modal', async ({ page }) => {"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/printing.spec.js",
                            "column": 16,
                            "line": 486
                          },
                          "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n\n  484 |     await injectSession(page);\n  485 |     await page.goto(BASE_URL);\n> 486 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n      |                ^\n  487 |   });\n  488 |\n  489 |   test('cancel button element exists in run modal', async ({ page }) => {\n    at /var/www/360lm/tests/printing.spec.js:486:16"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 1,
                      "startTime": "2026-07-12T13:36:18.786Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "screenshot",
                          "contentType": "image/png",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-Canc-c87ef--hidden-for-terminal-status-desktop-chrome-retry1/test-failed-1.png"
                        },
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-Canc-c87ef--hidden-for-terminal-status-desktop-chrome-retry1/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/printing.spec.js",
                        "column": 16,
                        "line": 486
                      }
                    }
                  ],
                  "status": "unexpected"
                }
              ],
              "id": "84f3bc84c047e9f9c0b8-081a53b0845897d33401",
              "file": "printing.spec.js",
              "line": 499,
              "column": 3
            }
          ]
        },
        {
          "title": "Print Nest — Stock Screen",
          "file": "printing.spec.js",
          "line": 517,
          "column": 6,
          "specs": [
            {
              "title": "stock screen loads without error",
              "ok": false,
              "tags": [],
              "tests": [
                {
                  "timeout": 30000,
                  "annotations": [],
                  "expectedStatus": "passed",
                  "projectId": "android-chrome",
                  "projectName": "android-chrome",
                  "results": [
                    {
                      "workerIndex": 144,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 8277,
                      "error": {
                        "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n",
                        "stack": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n    at /var/www/360lm/tests/printing.spec.js:521:16",
                        "location": {
                          "file": "/var/www/360lm/tests/printing.spec.js",
                          "column": 16,
                          "line": 521
                        },
                        "snippet": "  519 |     await injectSession(page);\n  520 |     await page.goto(BASE_URL);\n> 521 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n      |                ^\n  522 |     await page.locator('#screen-dash .nav-btn', { hasText: 'Stock' }).click();\n  523 |     await page.waitForSelector('#screen-stock.active', { timeout: 4000 });\n  524 |   });"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/printing.spec.js",
                            "column": 16,
                            "line": 521
                          },
                          "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n\n  519 |     await injectSession(page);\n  520 |     await page.goto(BASE_URL);\n> 521 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n      |                ^\n  522 |     await page.locator('#screen-dash .nav-btn', { hasText: 'Stock' }).click();\n  523 |     await page.waitForSelector('#screen-stock.active', { timeout: 4000 });\n  524 |   });\n    at /var/www/360lm/tests/printing.spec.js:521:16"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 0,
                      "startTime": "2026-07-12T13:16:23.290Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "screenshot",
                          "contentType": "image/png",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-Stoc-16435--screen-loads-without-error-android-chrome/test-failed-1.png"
                        },
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-Stoc-16435--screen-loads-without-error-android-chrome/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/printing.spec.js",
                        "column": 16,
                        "line": 521
                      }
                    },
                    {
                      "workerIndex": 145,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 8282,
                      "error": {
                        "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n",
                        "stack": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n    at /var/www/360lm/tests/printing.spec.js:521:16",
                        "location": {
                          "file": "/var/www/360lm/tests/printing.spec.js",
                          "column": 16,
                          "line": 521
                        },
                        "snippet": "  519 |     await injectSession(page);\n  520 |     await page.goto(BASE_URL);\n> 521 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n      |                ^\n  522 |     await page.locator('#screen-dash .nav-btn', { hasText: 'Stock' }).click();\n  523 |     await page.waitForSelector('#screen-stock.active', { timeout: 4000 });\n  524 |   });"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/printing.spec.js",
                            "column": 16,
                            "line": 521
                          },
                          "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n\n  519 |     await injectSession(page);\n  520 |     await page.goto(BASE_URL);\n> 521 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n      |                ^\n  522 |     await page.locator('#screen-dash .nav-btn', { hasText: 'Stock' }).click();\n  523 |     await page.waitForSelector('#screen-stock.active', { timeout: 4000 });\n  524 |   });\n    at /var/www/360lm/tests/printing.spec.js:521:16"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 1,
                      "startTime": "2026-07-12T13:16:33.037Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "screenshot",
                          "contentType": "image/png",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-Stoc-16435--screen-loads-without-error-android-chrome-retry1/test-failed-1.png"
                        },
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-Stoc-16435--screen-loads-without-error-android-chrome-retry1/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/printing.spec.js",
                        "column": 16,
                        "line": 521
                      }
                    }
                  ],
                  "status": "unexpected"
                }
              ],
              "id": "84f3bc84c047e9f9c0b8-13941d521046b534329f",
              "file": "printing.spec.js",
              "line": 526,
              "column": 3
            },
            {
              "title": "stock cards render with name and quantity",
              "ok": false,
              "tags": [],
              "tests": [
                {
                  "timeout": 30000,
                  "annotations": [],
                  "expectedStatus": "passed",
                  "projectId": "android-chrome",
                  "projectName": "android-chrome",
                  "results": [
                    {
                      "workerIndex": 146,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 8336,
                      "error": {
                        "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n",
                        "stack": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n    at /var/www/360lm/tests/printing.spec.js:521:16",
                        "location": {
                          "file": "/var/www/360lm/tests/printing.spec.js",
                          "column": 16,
                          "line": 521
                        },
                        "snippet": "  519 |     await injectSession(page);\n  520 |     await page.goto(BASE_URL);\n> 521 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n      |                ^\n  522 |     await page.locator('#screen-dash .nav-btn', { hasText: 'Stock' }).click();\n  523 |     await page.waitForSelector('#screen-stock.active', { timeout: 4000 });\n  524 |   });"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/printing.spec.js",
                            "column": 16,
                            "line": 521
                          },
                          "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n\n  519 |     await injectSession(page);\n  520 |     await page.goto(BASE_URL);\n> 521 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n      |                ^\n  522 |     await page.locator('#screen-dash .nav-btn', { hasText: 'Stock' }).click();\n  523 |     await page.waitForSelector('#screen-stock.active', { timeout: 4000 });\n  524 |   });\n    at /var/www/360lm/tests/printing.spec.js:521:16"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 0,
                      "startTime": "2026-07-12T13:16:42.564Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "screenshot",
                          "contentType": "image/png",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-Stoc-8d0f0-nder-with-name-and-quantity-android-chrome/test-failed-1.png"
                        },
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-Stoc-8d0f0-nder-with-name-and-quantity-android-chrome/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/printing.spec.js",
                        "column": 16,
                        "line": 521
                      }
                    },
                    {
                      "workerIndex": 147,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 8355,
                      "error": {
                        "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n",
                        "stack": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n    at /var/www/360lm/tests/printing.spec.js:521:16",
                        "location": {
                          "file": "/var/www/360lm/tests/printing.spec.js",
                          "column": 16,
                          "line": 521
                        },
                        "snippet": "  519 |     await injectSession(page);\n  520 |     await page.goto(BASE_URL);\n> 521 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n      |                ^\n  522 |     await page.locator('#screen-dash .nav-btn', { hasText: 'Stock' }).click();\n  523 |     await page.waitForSelector('#screen-stock.active', { timeout: 4000 });\n  524 |   });"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/printing.spec.js",
                            "column": 16,
                            "line": 521
                          },
                          "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n\n  519 |     await injectSession(page);\n  520 |     await page.goto(BASE_URL);\n> 521 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n      |                ^\n  522 |     await page.locator('#screen-dash .nav-btn', { hasText: 'Stock' }).click();\n  523 |     await page.waitForSelector('#screen-stock.active', { timeout: 4000 });\n  524 |   });\n    at /var/www/360lm/tests/printing.spec.js:521:16"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 1,
                      "startTime": "2026-07-12T13:16:52.061Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "screenshot",
                          "contentType": "image/png",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-Stoc-8d0f0-nder-with-name-and-quantity-android-chrome-retry1/test-failed-1.png"
                        },
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-Stoc-8d0f0-nder-with-name-and-quantity-android-chrome-retry1/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/printing.spec.js",
                        "column": 16,
                        "line": 521
                      }
                    }
                  ],
                  "status": "unexpected"
                }
              ],
              "id": "84f3bc84c047e9f9c0b8-b8e987c94e632bdbfbb1",
              "file": "printing.spec.js",
              "line": 533,
              "column": 3
            },
            {
              "title": "stock screen loads without error",
              "ok": false,
              "tags": [],
              "tests": [
                {
                  "timeout": 30000,
                  "annotations": [],
                  "expectedStatus": "passed",
                  "projectId": "desktop-chrome",
                  "projectName": "desktop-chrome",
                  "results": [
                    {
                      "workerIndex": 320,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 8253,
                      "error": {
                        "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n",
                        "stack": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n    at /var/www/360lm/tests/printing.spec.js:521:16",
                        "location": {
                          "file": "/var/www/360lm/tests/printing.spec.js",
                          "column": 16,
                          "line": 521
                        },
                        "snippet": "  519 |     await injectSession(page);\n  520 |     await page.goto(BASE_URL);\n> 521 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n      |                ^\n  522 |     await page.locator('#screen-dash .nav-btn', { hasText: 'Stock' }).click();\n  523 |     await page.waitForSelector('#screen-stock.active', { timeout: 4000 });\n  524 |   });"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/printing.spec.js",
                            "column": 16,
                            "line": 521
                          },
                          "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n\n  519 |     await injectSession(page);\n  520 |     await page.goto(BASE_URL);\n> 521 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n      |                ^\n  522 |     await page.locator('#screen-dash .nav-btn', { hasText: 'Stock' }).click();\n  523 |     await page.waitForSelector('#screen-stock.active', { timeout: 4000 });\n  524 |   });\n    at /var/www/360lm/tests/printing.spec.js:521:16"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 0,
                      "startTime": "2026-07-12T13:36:28.028Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "screenshot",
                          "contentType": "image/png",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-Stoc-16435--screen-loads-without-error-desktop-chrome/test-failed-1.png"
                        },
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-Stoc-16435--screen-loads-without-error-desktop-chrome/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/printing.spec.js",
                        "column": 16,
                        "line": 521
                      }
                    },
                    {
                      "workerIndex": 321,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 8283,
                      "error": {
                        "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n",
                        "stack": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n    at /var/www/360lm/tests/printing.spec.js:521:16",
                        "location": {
                          "file": "/var/www/360lm/tests/printing.spec.js",
                          "column": 16,
                          "line": 521
                        },
                        "snippet": "  519 |     await injectSession(page);\n  520 |     await page.goto(BASE_URL);\n> 521 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n      |                ^\n  522 |     await page.locator('#screen-dash .nav-btn', { hasText: 'Stock' }).click();\n  523 |     await page.waitForSelector('#screen-stock.active', { timeout: 4000 });\n  524 |   });"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/printing.spec.js",
                            "column": 16,
                            "line": 521
                          },
                          "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n\n  519 |     await injectSession(page);\n  520 |     await page.goto(BASE_URL);\n> 521 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n      |                ^\n  522 |     await page.locator('#screen-dash .nav-btn', { hasText: 'Stock' }).click();\n  523 |     await page.waitForSelector('#screen-stock.active', { timeout: 4000 });\n  524 |   });\n    at /var/www/360lm/tests/printing.spec.js:521:16"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 1,
                      "startTime": "2026-07-12T13:36:37.193Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "screenshot",
                          "contentType": "image/png",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-Stoc-16435--screen-loads-without-error-desktop-chrome-retry1/test-failed-1.png"
                        },
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-Stoc-16435--screen-loads-without-error-desktop-chrome-retry1/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/printing.spec.js",
                        "column": 16,
                        "line": 521
                      }
                    }
                  ],
                  "status": "unexpected"
                }
              ],
              "id": "84f3bc84c047e9f9c0b8-37ecc299bb497c26af4f",
              "file": "printing.spec.js",
              "line": 526,
              "column": 3
            },
            {
              "title": "stock cards render with name and quantity",
              "ok": false,
              "tags": [],
              "tests": [
                {
                  "timeout": 30000,
                  "annotations": [],
                  "expectedStatus": "passed",
                  "projectId": "desktop-chrome",
                  "projectName": "desktop-chrome",
                  "results": [
                    {
                      "workerIndex": 322,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 8421,
                      "error": {
                        "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n",
                        "stack": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n    at /var/www/360lm/tests/printing.spec.js:521:16",
                        "location": {
                          "file": "/var/www/360lm/tests/printing.spec.js",
                          "column": 16,
                          "line": 521
                        },
                        "snippet": "  519 |     await injectSession(page);\n  520 |     await page.goto(BASE_URL);\n> 521 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n      |                ^\n  522 |     await page.locator('#screen-dash .nav-btn', { hasText: 'Stock' }).click();\n  523 |     await page.waitForSelector('#screen-stock.active', { timeout: 4000 });\n  524 |   });"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/printing.spec.js",
                            "column": 16,
                            "line": 521
                          },
                          "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n\n  519 |     await injectSession(page);\n  520 |     await page.goto(BASE_URL);\n> 521 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n      |                ^\n  522 |     await page.locator('#screen-dash .nav-btn', { hasText: 'Stock' }).click();\n  523 |     await page.waitForSelector('#screen-stock.active', { timeout: 4000 });\n  524 |   });\n    at /var/www/360lm/tests/printing.spec.js:521:16"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 0,
                      "startTime": "2026-07-12T13:36:46.511Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "screenshot",
                          "contentType": "image/png",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-Stoc-8d0f0-nder-with-name-and-quantity-desktop-chrome/test-failed-1.png"
                        },
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-Stoc-8d0f0-nder-with-name-and-quantity-desktop-chrome/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/printing.spec.js",
                        "column": 16,
                        "line": 521
                      }
                    },
                    {
                      "workerIndex": 323,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 8533,
                      "error": {
                        "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n",
                        "stack": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n    at /var/www/360lm/tests/printing.spec.js:521:16",
                        "location": {
                          "file": "/var/www/360lm/tests/printing.spec.js",
                          "column": 16,
                          "line": 521
                        },
                        "snippet": "  519 |     await injectSession(page);\n  520 |     await page.goto(BASE_URL);\n> 521 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n      |                ^\n  522 |     await page.locator('#screen-dash .nav-btn', { hasText: 'Stock' }).click();\n  523 |     await page.waitForSelector('#screen-stock.active', { timeout: 4000 });\n  524 |   });"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/printing.spec.js",
                            "column": 16,
                            "line": 521
                          },
                          "message": "TimeoutError: page.waitForSelector: Timeout 8000ms exceeded.\nCall log:\n\u001b[2m  - waiting for locator('#screen-dash.active') to be visible\u001b[22m\n\n\n  519 |     await injectSession(page);\n  520 |     await page.goto(BASE_URL);\n> 521 |     await page.waitForSelector('#screen-dash.active', { timeout: 8000 });\n      |                ^\n  522 |     await page.locator('#screen-dash .nav-btn', { hasText: 'Stock' }).click();\n  523 |     await page.waitForSelector('#screen-stock.active', { timeout: 4000 });\n  524 |   });\n    at /var/www/360lm/tests/printing.spec.js:521:16"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 1,
                      "startTime": "2026-07-12T13:36:56.267Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "screenshot",
                          "contentType": "image/png",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-Stoc-8d0f0-nder-with-name-and-quantity-desktop-chrome-retry1/test-failed-1.png"
                        },
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/printing-Print-Nest-—-Stoc-8d0f0-nder-with-name-and-quantity-desktop-chrome-retry1/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/printing.spec.js",
                        "column": 16,
                        "line": 521
                      }
                    }
                  ],
                  "status": "unexpected"
                }
              ],
              "id": "84f3bc84c047e9f9c0b8-b46fc8519fc0281dc3ee",
              "file": "printing.spec.js",
              "line": 533,
              "column": 3
            }
          ]
        }
      ]
    },
    {
      "title": "production.spec.js",
      "file": "production.spec.js",
      "column": 0,
      "line": 0,
      "specs": [],
      "suites": [
        {
          "title": "Production — Smoke",
          "file": "production.spec.js",
          "line": 9,
          "column": 6,
          "specs": [
            {
              "title": "page or hub loads",
              "ok": true,
              "tags": [],
              "tests": [
                {
                  "timeout": 30000,
                  "annotations": [],
                  "expectedStatus": "passed",
                  "projectId": "android-chrome",
                  "projectName": "android-chrome",
                  "results": [
                    {
                      "workerIndex": 148,
                      "parallelIndex": 0,
                      "status": "passed",
                      "duration": 1310,
                      "errors": [],
                      "stdout": [],
                      "stderr": [],
                      "retry": 0,
                      "startTime": "2026-07-12T13:17:02.242Z",
                      "annotations": [],
                      "attachments": []
                    }
                  ],
                  "status": "expected"
                }
              ],
              "id": "e03a4b7a6378dd18a075-08ab67d2b70bac9026c0",
              "file": "production.spec.js",
              "line": 10,
              "column": 3
            },
            {
              "title": "manifest.json returns 200",
              "ok": false,
              "tags": [],
              "tests": [
                {
                  "timeout": 30000,
                  "annotations": [],
                  "expectedStatus": "passed",
                  "projectId": "android-chrome",
                  "projectName": "android-chrome",
                  "results": [
                    {
                      "workerIndex": 148,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 348,
                      "error": {
                        "message": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoBe\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m) // Object.is equality\u001b[22m\n\nExpected: \u001b[32m200\u001b[39m\nReceived: \u001b[31m404\u001b[39m",
                        "stack": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoBe\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m) // Object.is equality\u001b[22m\n\nExpected: \u001b[32m200\u001b[39m\nReceived: \u001b[31m404\u001b[39m\n    at /var/www/360lm/tests/production.spec.js:19:28",
                        "location": {
                          "file": "/var/www/360lm/tests/production.spec.js",
                          "column": 28,
                          "line": 19
                        },
                        "snippet": "  17 |   test('manifest.json returns 200', async ({ page }) => {\n  18 |     const resp = await page.goto(BASE_URL + 'manifest.json');\n> 19 |     expect(resp?.status()).toBe(200);\n     |                            ^\n  20 |   });\n  21 |\n  22 |   test('sw.js is versioned', async ({ page }) => {"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/production.spec.js",
                            "column": 28,
                            "line": 19
                          },
                          "message": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoBe\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m) // Object.is equality\u001b[22m\n\nExpected: \u001b[32m200\u001b[39m\nReceived: \u001b[31m404\u001b[39m\n\n  17 |   test('manifest.json returns 200', async ({ page }) => {\n  18 |     const resp = await page.goto(BASE_URL + 'manifest.json');\n> 19 |     expect(resp?.status()).toBe(200);\n     |                            ^\n  20 |   });\n  21 |\n  22 |   test('sw.js is versioned', async ({ page }) => {\n    at /var/www/360lm/tests/production.spec.js:19:28"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 0,
                      "startTime": "2026-07-12T13:17:03.837Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "screenshot",
                          "contentType": "image/png",
                          "path": "/var/www/360lm/test-results/production-Production-—-Smoke-manifest-json-returns-200-android-chrome/test-failed-1.png"
                        },
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/production-Production-—-Smoke-manifest-json-returns-200-android-chrome/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/production.spec.js",
                        "column": 28,
                        "line": 19
                      }
                    },
                    {
                      "workerIndex": 149,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 344,
                      "error": {
                        "message": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoBe\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m) // Object.is equality\u001b[22m\n\nExpected: \u001b[32m200\u001b[39m\nReceived: \u001b[31m404\u001b[39m",
                        "stack": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoBe\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m) // Object.is equality\u001b[22m\n\nExpected: \u001b[32m200\u001b[39m\nReceived: \u001b[31m404\u001b[39m\n    at /var/www/360lm/tests/production.spec.js:19:28",
                        "location": {
                          "file": "/var/www/360lm/tests/production.spec.js",
                          "column": 28,
                          "line": 19
                        },
                        "snippet": "  17 |   test('manifest.json returns 200', async ({ page }) => {\n  18 |     const resp = await page.goto(BASE_URL + 'manifest.json');\n> 19 |     expect(resp?.status()).toBe(200);\n     |                            ^\n  20 |   });\n  21 |\n  22 |   test('sw.js is versioned', async ({ page }) => {"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/production.spec.js",
                            "column": 28,
                            "line": 19
                          },
                          "message": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoBe\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m) // Object.is equality\u001b[22m\n\nExpected: \u001b[32m200\u001b[39m\nReceived: \u001b[31m404\u001b[39m\n\n  17 |   test('manifest.json returns 200', async ({ page }) => {\n  18 |     const resp = await page.goto(BASE_URL + 'manifest.json');\n> 19 |     expect(resp?.status()).toBe(200);\n     |                            ^\n  20 |   });\n  21 |\n  22 |   test('sw.js is versioned', async ({ page }) => {\n    at /var/www/360lm/tests/production.spec.js:19:28"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 1,
                      "startTime": "2026-07-12T13:17:05.371Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "screenshot",
                          "contentType": "image/png",
                          "path": "/var/www/360lm/test-results/production-Production-—-Smoke-manifest-json-returns-200-android-chrome-retry1/test-failed-1.png"
                        },
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/production-Production-—-Smoke-manifest-json-returns-200-android-chrome-retry1/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/production.spec.js",
                        "column": 28,
                        "line": 19
                      }
                    }
                  ],
                  "status": "unexpected"
                }
              ],
              "id": "e03a4b7a6378dd18a075-16d4c3afa38c0afcaa8a",
              "file": "production.spec.js",
              "line": 17,
              "column": 3
            },
            {
              "title": "sw.js is versioned",
              "ok": false,
              "tags": [],
              "tests": [
                {
                  "timeout": 30000,
                  "annotations": [],
                  "expectedStatus": "passed",
                  "projectId": "android-chrome",
                  "projectName": "android-chrome",
                  "results": [
                    {
                      "workerIndex": 150,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 368,
                      "error": {
                        "message": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoContain\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m) // indexOf\u001b[22m\n\nExpected substring: \u001b[32m\"360lm-prod-v\"\u001b[39m\nReceived string:    \u001b[31m\"<!DOCTYPE HTML PUBLIC \\\"-//W3C//DTD HTML 4.01//EN\\\" \\\"http://www.w3.org/TR/html4/strict.dtd\\\">\u001b[39m\n\u001b[31m<html><head>\u001b[39m\n\u001b[31m<title>404 Not Found</title>\u001b[39m\n\u001b[31m</head><body>\u001b[39m\n\u001b[31m<h1>Not Found</h1>\u001b[39m\n\u001b[31m<p>The requested URL was not found on this server.</p>\u001b[39m\n\u001b[31m</body></html>\u001b[39m\n\u001b[31m\"\u001b[39m",
                        "stack": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoContain\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m) // indexOf\u001b[22m\n\nExpected substring: \u001b[32m\"360lm-prod-v\"\u001b[39m\nReceived string:    \u001b[31m\"<!DOCTYPE HTML PUBLIC \\\"-//W3C//DTD HTML 4.01//EN\\\" \\\"http://www.w3.org/TR/html4/strict.dtd\\\">\u001b[39m\n\u001b[31m<html><head>\u001b[39m\n\u001b[31m<title>404 Not Found</title>\u001b[39m\n\u001b[31m</head><body>\u001b[39m\n\u001b[31m<h1>Not Found</h1>\u001b[39m\n\u001b[31m<p>The requested URL was not found on this server.</p>\u001b[39m\n\u001b[31m</body></html>\u001b[39m\n\u001b[31m\"\u001b[39m\n    at /var/www/360lm/tests/production.spec.js:25:18",
                        "location": {
                          "file": "/var/www/360lm/tests/production.spec.js",
                          "column": 18,
                          "line": 25
                        },
                        "snippet": "  23 |     const resp = await page.goto(BASE_URL + 'sw.js');\n  24 |     const text = await resp?.text() || '';\n> 25 |     expect(text).toContain('360lm-prod-v');\n     |                  ^\n  26 |   });\n  27 | });\n  28 |"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/production.spec.js",
                            "column": 18,
                            "line": 25
                          },
                          "message": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoContain\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m) // indexOf\u001b[22m\n\nExpected substring: \u001b[32m\"360lm-prod-v\"\u001b[39m\nReceived string:    \u001b[31m\"<!DOCTYPE HTML PUBLIC \\\"-//W3C//DTD HTML 4.01//EN\\\" \\\"http://www.w3.org/TR/html4/strict.dtd\\\">\u001b[39m\n\u001b[31m<html><head>\u001b[39m\n\u001b[31m<title>404 Not Found</title>\u001b[39m\n\u001b[31m</head><body>\u001b[39m\n\u001b[31m<h1>Not Found</h1>\u001b[39m\n\u001b[31m<p>The requested URL was not found on this server.</p>\u001b[39m\n\u001b[31m</body></html>\u001b[39m\n\u001b[31m\"\u001b[39m\n\n  23 |     const resp = await page.goto(BASE_URL + 'sw.js');\n  24 |     const text = await resp?.text() || '';\n> 25 |     expect(text).toContain('360lm-prod-v');\n     |                  ^\n  26 |   });\n  27 | });\n  28 |\n    at /var/www/360lm/tests/production.spec.js:25:18"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 0,
                      "startTime": "2026-07-12T13:17:06.943Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "screenshot",
                          "contentType": "image/png",
                          "path": "/var/www/360lm/test-results/production-Production-—-Smoke-sw-js-is-versioned-android-chrome/test-failed-1.png"
                        },
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/production-Production-—-Smoke-sw-js-is-versioned-android-chrome/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/production.spec.js",
                        "column": 18,
                        "line": 25
                      }
                    },
                    {
                      "workerIndex": 151,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 325,
                      "error": {
                        "message": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoContain\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m) // indexOf\u001b[22m\n\nExpected substring: \u001b[32m\"360lm-prod-v\"\u001b[39m\nReceived string:    \u001b[31m\"<!DOCTYPE HTML PUBLIC \\\"-//W3C//DTD HTML 4.01//EN\\\" \\\"http://www.w3.org/TR/html4/strict.dtd\\\">\u001b[39m\n\u001b[31m<html><head>\u001b[39m\n\u001b[31m<title>404 Not Found</title>\u001b[39m\n\u001b[31m</head><body>\u001b[39m\n\u001b[31m<h1>Not Found</h1>\u001b[39m\n\u001b[31m<p>The requested URL was not found on this server.</p>\u001b[39m\n\u001b[31m</body></html>\u001b[39m\n\u001b[31m\"\u001b[39m",
                        "stack": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoContain\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m) // indexOf\u001b[22m\n\nExpected substring: \u001b[32m\"360lm-prod-v\"\u001b[39m\nReceived string:    \u001b[31m\"<!DOCTYPE HTML PUBLIC \\\"-//W3C//DTD HTML 4.01//EN\\\" \\\"http://www.w3.org/TR/html4/strict.dtd\\\">\u001b[39m\n\u001b[31m<html><head>\u001b[39m\n\u001b[31m<title>404 Not Found</title>\u001b[39m\n\u001b[31m</head><body>\u001b[39m\n\u001b[31m<h1>Not Found</h1>\u001b[39m\n\u001b[31m<p>The requested URL was not found on this server.</p>\u001b[39m\n\u001b[31m</body></html>\u001b[39m\n\u001b[31m\"\u001b[39m\n    at /var/www/360lm/tests/production.spec.js:25:18",
                        "location": {
                          "file": "/var/www/360lm/tests/production.spec.js",
                          "column": 18,
                          "line": 25
                        },
                        "snippet": "  23 |     const resp = await page.goto(BASE_URL + 'sw.js');\n  24 |     const text = await resp?.text() || '';\n> 25 |     expect(text).toContain('360lm-prod-v');\n     |                  ^\n  26 |   });\n  27 | });\n  28 |"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/production.spec.js",
                            "column": 18,
                            "line": 25
                          },
                          "message": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoContain\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m) // indexOf\u001b[22m\n\nExpected substring: \u001b[32m\"360lm-prod-v\"\u001b[39m\nReceived string:    \u001b[31m\"<!DOCTYPE HTML PUBLIC \\\"-//W3C//DTD HTML 4.01//EN\\\" \\\"http://www.w3.org/TR/html4/strict.dtd\\\">\u001b[39m\n\u001b[31m<html><head>\u001b[39m\n\u001b[31m<title>404 Not Found</title>\u001b[39m\n\u001b[31m</head><body>\u001b[39m\n\u001b[31m<h1>Not Found</h1>\u001b[39m\n\u001b[31m<p>The requested URL was not found on this server.</p>\u001b[39m\n\u001b[31m</body></html>\u001b[39m\n\u001b[31m\"\u001b[39m\n\n  23 |     const resp = await page.goto(BASE_URL + 'sw.js');\n  24 |     const text = await resp?.text() || '';\n> 25 |     expect(text).toContain('360lm-prod-v');\n     |                  ^\n  26 |   });\n  27 | });\n  28 |\n    at /var/www/360lm/tests/production.spec.js:25:18"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 1,
                      "startTime": "2026-07-12T13:17:08.501Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "screenshot",
                          "contentType": "image/png",
                          "path": "/var/www/360lm/test-results/production-Production-—-Smoke-sw-js-is-versioned-android-chrome-retry1/test-failed-1.png"
                        },
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/production-Production-—-Smoke-sw-js-is-versioned-android-chrome-retry1/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/production.spec.js",
                        "column": 18,
                        "line": 25
                      }
                    }
                  ],
                  "status": "unexpected"
                }
              ],
              "id": "e03a4b7a6378dd18a075-479a33c51e1e0ed8ae0d",
              "file": "production.spec.js",
              "line": 22,
              "column": 3
            },
            {
              "title": "page or hub loads",
              "ok": true,
              "tags": [],
              "tests": [
                {
                  "timeout": 30000,
                  "annotations": [],
                  "expectedStatus": "passed",
                  "projectId": "desktop-chrome",
                  "projectName": "desktop-chrome",
                  "results": [
                    {
                      "workerIndex": 324,
                      "parallelIndex": 0,
                      "status": "passed",
                      "duration": 1064,
                      "errors": [],
                      "stdout": [],
                      "stderr": [],
                      "retry": 0,
                      "startTime": "2026-07-12T13:37:06.133Z",
                      "annotations": [],
                      "attachments": []
                    }
                  ],
                  "status": "expected"
                }
              ],
              "id": "e03a4b7a6378dd18a075-a5a5a553947fd21ca24d",
              "file": "production.spec.js",
              "line": 10,
              "column": 3
            },
            {
              "title": "manifest.json returns 200",
              "ok": false,
              "tags": [],
              "tests": [
                {
                  "timeout": 30000,
                  "annotations": [],
                  "expectedStatus": "passed",
                  "projectId": "desktop-chrome",
                  "projectName": "desktop-chrome",
                  "results": [
                    {
                      "workerIndex": 324,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 227,
                      "error": {
                        "message": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoBe\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m) // Object.is equality\u001b[22m\n\nExpected: \u001b[32m200\u001b[39m\nReceived: \u001b[31m404\u001b[39m",
                        "stack": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoBe\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m) // Object.is equality\u001b[22m\n\nExpected: \u001b[32m200\u001b[39m\nReceived: \u001b[31m404\u001b[39m\n    at /var/www/360lm/tests/production.spec.js:19:28",
                        "location": {
                          "file": "/var/www/360lm/tests/production.spec.js",
                          "column": 28,
                          "line": 19
                        },
                        "snippet": "  17 |   test('manifest.json returns 200', async ({ page }) => {\n  18 |     const resp = await page.goto(BASE_URL + 'manifest.json');\n> 19 |     expect(resp?.status()).toBe(200);\n     |                            ^\n  20 |   });\n  21 |\n  22 |   test('sw.js is versioned', async ({ page }) => {"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/production.spec.js",
                            "column": 28,
                            "line": 19
                          },
                          "message": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoBe\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m) // Object.is equality\u001b[22m\n\nExpected: \u001b[32m200\u001b[39m\nReceived: \u001b[31m404\u001b[39m\n\n  17 |   test('manifest.json returns 200', async ({ page }) => {\n  18 |     const resp = await page.goto(BASE_URL + 'manifest.json');\n> 19 |     expect(resp?.status()).toBe(200);\n     |                            ^\n  20 |   });\n  21 |\n  22 |   test('sw.js is versioned', async ({ page }) => {\n    at /var/www/360lm/tests/production.spec.js:19:28"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 0,
                      "startTime": "2026-07-12T13:37:07.377Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "screenshot",
                          "contentType": "image/png",
                          "path": "/var/www/360lm/test-results/production-Production-—-Smoke-manifest-json-returns-200-desktop-chrome/test-failed-1.png"
                        },
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/production-Production-—-Smoke-manifest-json-returns-200-desktop-chrome/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/production.spec.js",
                        "column": 28,
                        "line": 19
                      }
                    },
                    {
                      "workerIndex": 325,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 328,
                      "error": {
                        "message": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoBe\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m) // Object.is equality\u001b[22m\n\nExpected: \u001b[32m200\u001b[39m\nReceived: \u001b[31m404\u001b[39m",
                        "stack": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoBe\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m) // Object.is equality\u001b[22m\n\nExpected: \u001b[32m200\u001b[39m\nReceived: \u001b[31m404\u001b[39m\n    at /var/www/360lm/tests/production.spec.js:19:28",
                        "location": {
                          "file": "/var/www/360lm/tests/production.spec.js",
                          "column": 28,
                          "line": 19
                        },
                        "snippet": "  17 |   test('manifest.json returns 200', async ({ page }) => {\n  18 |     const resp = await page.goto(BASE_URL + 'manifest.json');\n> 19 |     expect(resp?.status()).toBe(200);\n     |                            ^\n  20 |   });\n  21 |\n  22 |   test('sw.js is versioned', async ({ page }) => {"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/production.spec.js",
                            "column": 28,
                            "line": 19
                          },
                          "message": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoBe\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m) // Object.is equality\u001b[22m\n\nExpected: \u001b[32m200\u001b[39m\nReceived: \u001b[31m404\u001b[39m\n\n  17 |   test('manifest.json returns 200', async ({ page }) => {\n  18 |     const resp = await page.goto(BASE_URL + 'manifest.json');\n> 19 |     expect(resp?.status()).toBe(200);\n     |                            ^\n  20 |   });\n  21 |\n  22 |   test('sw.js is versioned', async ({ page }) => {\n    at /var/www/360lm/tests/production.spec.js:19:28"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 1,
                      "startTime": "2026-07-12T13:37:08.418Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "screenshot",
                          "contentType": "image/png",
                          "path": "/var/www/360lm/test-results/production-Production-—-Smoke-manifest-json-returns-200-desktop-chrome-retry1/test-failed-1.png"
                        },
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/production-Production-—-Smoke-manifest-json-returns-200-desktop-chrome-retry1/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/production.spec.js",
                        "column": 28,
                        "line": 19
                      }
                    }
                  ],
                  "status": "unexpected"
                }
              ],
              "id": "e03a4b7a6378dd18a075-629b90f96f7227a10615",
              "file": "production.spec.js",
              "line": 17,
              "column": 3
            },
            {
              "title": "sw.js is versioned",
              "ok": false,
              "tags": [],
              "tests": [
                {
                  "timeout": 30000,
                  "annotations": [],
                  "expectedStatus": "passed",
                  "projectId": "desktop-chrome",
                  "projectName": "desktop-chrome",
                  "results": [
                    {
                      "workerIndex": 326,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 286,
                      "error": {
                        "message": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoContain\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m) // indexOf\u001b[22m\n\nExpected substring: \u001b[32m\"360lm-prod-v\"\u001b[39m\nReceived string:    \u001b[31m\"<!DOCTYPE HTML PUBLIC \\\"-//W3C//DTD HTML 4.01//EN\\\" \\\"http://www.w3.org/TR/html4/strict.dtd\\\">\u001b[39m\n\u001b[31m<html><head>\u001b[39m\n\u001b[31m<title>404 Not Found</title>\u001b[39m\n\u001b[31m</head><body>\u001b[39m\n\u001b[31m<h1>Not Found</h1>\u001b[39m\n\u001b[31m<p>The requested URL was not found on this server.</p>\u001b[39m\n\u001b[31m</body></html>\u001b[39m\n\u001b[31m\"\u001b[39m",
                        "stack": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoContain\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m) // indexOf\u001b[22m\n\nExpected substring: \u001b[32m\"360lm-prod-v\"\u001b[39m\nReceived string:    \u001b[31m\"<!DOCTYPE HTML PUBLIC \\\"-//W3C//DTD HTML 4.01//EN\\\" \\\"http://www.w3.org/TR/html4/strict.dtd\\\">\u001b[39m\n\u001b[31m<html><head>\u001b[39m\n\u001b[31m<title>404 Not Found</title>\u001b[39m\n\u001b[31m</head><body>\u001b[39m\n\u001b[31m<h1>Not Found</h1>\u001b[39m\n\u001b[31m<p>The requested URL was not found on this server.</p>\u001b[39m\n\u001b[31m</body></html>\u001b[39m\n\u001b[31m\"\u001b[39m\n    at /var/www/360lm/tests/production.spec.js:25:18",
                        "location": {
                          "file": "/var/www/360lm/tests/production.spec.js",
                          "column": 18,
                          "line": 25
                        },
                        "snippet": "  23 |     const resp = await page.goto(BASE_URL + 'sw.js');\n  24 |     const text = await resp?.text() || '';\n> 25 |     expect(text).toContain('360lm-prod-v');\n     |                  ^\n  26 |   });\n  27 | });\n  28 |"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/production.spec.js",
                            "column": 18,
                            "line": 25
                          },
                          "message": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoContain\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m) // indexOf\u001b[22m\n\nExpected substring: \u001b[32m\"360lm-prod-v\"\u001b[39m\nReceived string:    \u001b[31m\"<!DOCTYPE HTML PUBLIC \\\"-//W3C//DTD HTML 4.01//EN\\\" \\\"http://www.w3.org/TR/html4/strict.dtd\\\">\u001b[39m\n\u001b[31m<html><head>\u001b[39m\n\u001b[31m<title>404 Not Found</title>\u001b[39m\n\u001b[31m</head><body>\u001b[39m\n\u001b[31m<h1>Not Found</h1>\u001b[39m\n\u001b[31m<p>The requested URL was not found on this server.</p>\u001b[39m\n\u001b[31m</body></html>\u001b[39m\n\u001b[31m\"\u001b[39m\n\n  23 |     const resp = await page.goto(BASE_URL + 'sw.js');\n  24 |     const text = await resp?.text() || '';\n> 25 |     expect(text).toContain('360lm-prod-v');\n     |                  ^\n  26 |   });\n  27 | });\n  28 |\n    at /var/www/360lm/tests/production.spec.js:25:18"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 0,
                      "startTime": "2026-07-12T13:37:09.782Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "screenshot",
                          "contentType": "image/png",
                          "path": "/var/www/360lm/test-results/production-Production-—-Smoke-sw-js-is-versioned-desktop-chrome/test-failed-1.png"
                        },
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/production-Production-—-Smoke-sw-js-is-versioned-desktop-chrome/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/production.spec.js",
                        "column": 18,
                        "line": 25
                      }
                    },
                    {
                      "workerIndex": 327,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 296,
                      "error": {
                        "message": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoContain\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m) // indexOf\u001b[22m\n\nExpected substring: \u001b[32m\"360lm-prod-v\"\u001b[39m\nReceived string:    \u001b[31m\"<!DOCTYPE HTML PUBLIC \\\"-//W3C//DTD HTML 4.01//EN\\\" \\\"http://www.w3.org/TR/html4/strict.dtd\\\">\u001b[39m\n\u001b[31m<html><head>\u001b[39m\n\u001b[31m<title>404 Not Found</title>\u001b[39m\n\u001b[31m</head><body>\u001b[39m\n\u001b[31m<h1>Not Found</h1>\u001b[39m\n\u001b[31m<p>The requested URL was not found on this server.</p>\u001b[39m\n\u001b[31m</body></html>\u001b[39m\n\u001b[31m\"\u001b[39m",
                        "stack": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoContain\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m) // indexOf\u001b[22m\n\nExpected substring: \u001b[32m\"360lm-prod-v\"\u001b[39m\nReceived string:    \u001b[31m\"<!DOCTYPE HTML PUBLIC \\\"-//W3C//DTD HTML 4.01//EN\\\" \\\"http://www.w3.org/TR/html4/strict.dtd\\\">\u001b[39m\n\u001b[31m<html><head>\u001b[39m\n\u001b[31m<title>404 Not Found</title>\u001b[39m\n\u001b[31m</head><body>\u001b[39m\n\u001b[31m<h1>Not Found</h1>\u001b[39m\n\u001b[31m<p>The requested URL was not found on this server.</p>\u001b[39m\n\u001b[31m</body></html>\u001b[39m\n\u001b[31m\"\u001b[39m\n    at /var/www/360lm/tests/production.spec.js:25:18",
                        "location": {
                          "file": "/var/www/360lm/tests/production.spec.js",
                          "column": 18,
                          "line": 25
                        },
                        "snippet": "  23 |     const resp = await page.goto(BASE_URL + 'sw.js');\n  24 |     const text = await resp?.text() || '';\n> 25 |     expect(text).toContain('360lm-prod-v');\n     |                  ^\n  26 |   });\n  27 | });\n  28 |"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/production.spec.js",
                            "column": 18,
                            "line": 25
                          },
                          "message": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoContain\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m) // indexOf\u001b[22m\n\nExpected substring: \u001b[32m\"360lm-prod-v\"\u001b[39m\nReceived string:    \u001b[31m\"<!DOCTYPE HTML PUBLIC \\\"-//W3C//DTD HTML 4.01//EN\\\" \\\"http://www.w3.org/TR/html4/strict.dtd\\\">\u001b[39m\n\u001b[31m<html><head>\u001b[39m\n\u001b[31m<title>404 Not Found</title>\u001b[39m\n\u001b[31m</head><body>\u001b[39m\n\u001b[31m<h1>Not Found</h1>\u001b[39m\n\u001b[31m<p>The requested URL was not found on this server.</p>\u001b[39m\n\u001b[31m</body></html>\u001b[39m\n\u001b[31m\"\u001b[39m\n\n  23 |     const resp = await page.goto(BASE_URL + 'sw.js');\n  24 |     const text = await resp?.text() || '';\n> 25 |     expect(text).toContain('360lm-prod-v');\n     |                  ^\n  26 |   });\n  27 | });\n  28 |\n    at /var/www/360lm/tests/production.spec.js:25:18"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 1,
                      "startTime": "2026-07-12T13:37:11.010Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "screenshot",
                          "contentType": "image/png",
                          "path": "/var/www/360lm/test-results/production-Production-—-Smoke-sw-js-is-versioned-desktop-chrome-retry1/test-failed-1.png"
                        },
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/production-Production-—-Smoke-sw-js-is-versioned-desktop-chrome-retry1/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/production.spec.js",
                        "column": 18,
                        "line": 25
                      }
                    }
                  ],
                  "status": "unexpected"
                }
              ],
              "id": "e03a4b7a6378dd18a075-e071e46e4761707c8728",
              "file": "production.spec.js",
              "line": 22,
              "column": 3
            }
          ]
        },
        {
          "title": "Production — HTML Structure",
          "file": "production.spec.js",
          "line": 29,
          "column": 6,
          "specs": [
            {
              "title": "all screens present",
              "ok": false,
              "tags": [],
              "tests": [
                {
                  "timeout": 30000,
                  "annotations": [],
                  "expectedStatus": "passed",
                  "projectId": "android-chrome",
                  "projectName": "android-chrome",
                  "results": [
                    {
                      "workerIndex": 152,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 9,
                      "error": {
                        "message": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoContain\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m) // indexOf\u001b[22m\n\nExpected substring: \u001b[32m\"id=\\\"screen-splash\\\"\"\u001b[39m\nReceived string:    \u001b[31m\"<!DOCTYPE HTML PUBLIC \\\"-//W3C//DTD HTML 4.01//EN\\\" \\\"http://www.w3.org/TR/html4/strict.dtd\\\">\u001b[39m\n\u001b[31m<html><head>\u001b[39m\n\u001b[31m<title>404 Not Found</title>\u001b[39m\n\u001b[31m</head><body>\u001b[39m\n\u001b[31m<h1>Not Found</h1>\u001b[39m\n\u001b[31m<p>The requested URL was not found on this server.</p>\u001b[39m\n\u001b[31m</body></html>\u001b[39m\n\u001b[31m\"\u001b[39m",
                        "stack": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoContain\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m) // indexOf\u001b[22m\n\nExpected substring: \u001b[32m\"id=\\\"screen-splash\\\"\"\u001b[39m\nReceived string:    \u001b[31m\"<!DOCTYPE HTML PUBLIC \\\"-//W3C//DTD HTML 4.01//EN\\\" \\\"http://www.w3.org/TR/html4/strict.dtd\\\">\u001b[39m\n\u001b[31m<html><head>\u001b[39m\n\u001b[31m<title>404 Not Found</title>\u001b[39m\n\u001b[31m</head><body>\u001b[39m\n\u001b[31m<h1>Not Found</h1>\u001b[39m\n\u001b[31m<p>The requested URL was not found on this server.</p>\u001b[39m\n\u001b[31m</body></html>\u001b[39m\n\u001b[31m\"\u001b[39m\n    at /var/www/360lm/tests/production.spec.js:40:18",
                        "location": {
                          "file": "/var/www/360lm/tests/production.spec.js",
                          "column": 18,
                          "line": 40
                        },
                        "snippet": "  38 |\n  39 |   test('all screens present', () => {\n> 40 |     expect(html).toContain('id=\"screen-splash\"');\n     |                  ^\n  41 |     expect(html).toContain('id=\"screen-dash\"');\n  42 |     expect(html).toContain('id=\"screen-new\"');\n  43 |     expect(html).toContain('id=\"screen-detail\"');"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/production.spec.js",
                            "column": 18,
                            "line": 40
                          },
                          "message": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoContain\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m) // indexOf\u001b[22m\n\nExpected substring: \u001b[32m\"id=\\\"screen-splash\\\"\"\u001b[39m\nReceived string:    \u001b[31m\"<!DOCTYPE HTML PUBLIC \\\"-//W3C//DTD HTML 4.01//EN\\\" \\\"http://www.w3.org/TR/html4/strict.dtd\\\">\u001b[39m\n\u001b[31m<html><head>\u001b[39m\n\u001b[31m<title>404 Not Found</title>\u001b[39m\n\u001b[31m</head><body>\u001b[39m\n\u001b[31m<h1>Not Found</h1>\u001b[39m\n\u001b[31m<p>The requested URL was not found on this server.</p>\u001b[39m\n\u001b[31m</body></html>\u001b[39m\n\u001b[31m\"\u001b[39m\n\n  38 |\n  39 |   test('all screens present', () => {\n> 40 |     expect(html).toContain('id=\"screen-splash\"');\n     |                  ^\n  41 |     expect(html).toContain('id=\"screen-dash\"');\n  42 |     expect(html).toContain('id=\"screen-new\"');\n  43 |     expect(html).toContain('id=\"screen-detail\"');\n    at /var/www/360lm/tests/production.spec.js:40:18"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 0,
                      "startTime": "2026-07-12T13:17:10.005Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/production-Production-—-HTML-Structure-all-screens-present-android-chrome/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/production.spec.js",
                        "column": 18,
                        "line": 40
                      }
                    },
                    {
                      "workerIndex": 153,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 11,
                      "error": {
                        "message": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoContain\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m) // indexOf\u001b[22m\n\nExpected substring: \u001b[32m\"id=\\\"screen-splash\\\"\"\u001b[39m\nReceived string:    \u001b[31m\"<!DOCTYPE HTML PUBLIC \\\"-//W3C//DTD HTML 4.01//EN\\\" \\\"http://www.w3.org/TR/html4/strict.dtd\\\">\u001b[39m\n\u001b[31m<html><head>\u001b[39m\n\u001b[31m<title>404 Not Found</title>\u001b[39m\n\u001b[31m</head><body>\u001b[39m\n\u001b[31m<h1>Not Found</h1>\u001b[39m\n\u001b[31m<p>The requested URL was not found on this server.</p>\u001b[39m\n\u001b[31m</body></html>\u001b[39m\n\u001b[31m\"\u001b[39m",
                        "stack": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoContain\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m) // indexOf\u001b[22m\n\nExpected substring: \u001b[32m\"id=\\\"screen-splash\\\"\"\u001b[39m\nReceived string:    \u001b[31m\"<!DOCTYPE HTML PUBLIC \\\"-//W3C//DTD HTML 4.01//EN\\\" \\\"http://www.w3.org/TR/html4/strict.dtd\\\">\u001b[39m\n\u001b[31m<html><head>\u001b[39m\n\u001b[31m<title>404 Not Found</title>\u001b[39m\n\u001b[31m</head><body>\u001b[39m\n\u001b[31m<h1>Not Found</h1>\u001b[39m\n\u001b[31m<p>The requested URL was not found on this server.</p>\u001b[39m\n\u001b[31m</body></html>\u001b[39m\n\u001b[31m\"\u001b[39m\n    at /var/www/360lm/tests/production.spec.js:40:18",
                        "location": {
                          "file": "/var/www/360lm/tests/production.spec.js",
                          "column": 18,
                          "line": 40
                        },
                        "snippet": "  38 |\n  39 |   test('all screens present', () => {\n> 40 |     expect(html).toContain('id=\"screen-splash\"');\n     |                  ^\n  41 |     expect(html).toContain('id=\"screen-dash\"');\n  42 |     expect(html).toContain('id=\"screen-new\"');\n  43 |     expect(html).toContain('id=\"screen-detail\"');"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/production.spec.js",
                            "column": 18,
                            "line": 40
                          },
                          "message": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoContain\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m) // indexOf\u001b[22m\n\nExpected substring: \u001b[32m\"id=\\\"screen-splash\\\"\"\u001b[39m\nReceived string:    \u001b[31m\"<!DOCTYPE HTML PUBLIC \\\"-//W3C//DTD HTML 4.01//EN\\\" \\\"http://www.w3.org/TR/html4/strict.dtd\\\">\u001b[39m\n\u001b[31m<html><head>\u001b[39m\n\u001b[31m<title>404 Not Found</title>\u001b[39m\n\u001b[31m</head><body>\u001b[39m\n\u001b[31m<h1>Not Found</h1>\u001b[39m\n\u001b[31m<p>The requested URL was not found on this server.</p>\u001b[39m\n\u001b[31m</body></html>\u001b[39m\n\u001b[31m\"\u001b[39m\n\n  38 |\n  39 |   test('all screens present', () => {\n> 40 |     expect(html).toContain('id=\"screen-splash\"');\n     |                  ^\n  41 |     expect(html).toContain('id=\"screen-dash\"');\n  42 |     expect(html).toContain('id=\"screen-new\"');\n  43 |     expect(html).toContain('id=\"screen-detail\"');\n    at /var/www/360lm/tests/production.spec.js:40:18"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 1,
                      "startTime": "2026-07-12T13:17:11.176Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/production-Production-—-HTML-Structure-all-screens-present-android-chrome-retry1/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/production.spec.js",
                        "column": 18,
                        "line": 40
                      }
                    }
                  ],
                  "status": "unexpected"
                }
              ],
              "id": "e03a4b7a6378dd18a075-751b1956f8285092c40a",
              "file": "production.spec.js",
              "line": 39,
              "column": 3
            },
            {
              "title": "hub auth gate and navigation present",
              "ok": false,
              "tags": [],
              "tests": [
                {
                  "timeout": 30000,
                  "annotations": [],
                  "expectedStatus": "passed",
                  "projectId": "android-chrome",
                  "projectName": "android-chrome",
                  "results": [
                    {
                      "workerIndex": 154,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 13,
                      "error": {
                        "message": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoContain\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m) // indexOf\u001b[22m\n\nExpected substring: \u001b[32m\"window.location.replace('/hub/')\"\u001b[39m\nReceived string:    \u001b[31m\"<!DOCTYPE HTML PUBLIC \\\"-//W3C//DTD HTML 4.01//EN\\\" \\\"http://www.w3.org/TR/html4/strict.dtd\\\">\u001b[39m\n\u001b[31m<html><head>\u001b[39m\n\u001b[31m<title>404 Not Found</title>\u001b[39m\n\u001b[31m</head><body>\u001b[39m\n\u001b[31m<h1>Not Found</h1>\u001b[39m\n\u001b[31m<p>The requested URL was not found on this server.</p>\u001b[39m\n\u001b[31m</body></html>\u001b[39m\n\u001b[31m\"\u001b[39m",
                        "stack": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoContain\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m) // indexOf\u001b[22m\n\nExpected substring: \u001b[32m\"window.location.replace('/hub/')\"\u001b[39m\nReceived string:    \u001b[31m\"<!DOCTYPE HTML PUBLIC \\\"-//W3C//DTD HTML 4.01//EN\\\" \\\"http://www.w3.org/TR/html4/strict.dtd\\\">\u001b[39m\n\u001b[31m<html><head>\u001b[39m\n\u001b[31m<title>404 Not Found</title>\u001b[39m\n\u001b[31m</head><body>\u001b[39m\n\u001b[31m<h1>Not Found</h1>\u001b[39m\n\u001b[31m<p>The requested URL was not found on this server.</p>\u001b[39m\n\u001b[31m</body></html>\u001b[39m\n\u001b[31m\"\u001b[39m\n    at /var/www/360lm/tests/production.spec.js:48:18",
                        "location": {
                          "file": "/var/www/360lm/tests/production.spec.js",
                          "column": 18,
                          "line": 48
                        },
                        "snippet": "  46 |\n  47 |   test('hub auth gate and navigation present', () => {\n> 48 |     expect(html).toContain(\"window.location.replace('/hub/')\");\n     |                  ^\n  49 |     expect(html).toContain('btn-hub');\n  50 |     expect(html).toContain('🏡 Hub');\n  51 |   });"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/production.spec.js",
                            "column": 18,
                            "line": 48
                          },
                          "message": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoContain\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m) // indexOf\u001b[22m\n\nExpected substring: \u001b[32m\"window.location.replace('/hub/')\"\u001b[39m\nReceived string:    \u001b[31m\"<!DOCTYPE HTML PUBLIC \\\"-//W3C//DTD HTML 4.01//EN\\\" \\\"http://www.w3.org/TR/html4/strict.dtd\\\">\u001b[39m\n\u001b[31m<html><head>\u001b[39m\n\u001b[31m<title>404 Not Found</title>\u001b[39m\n\u001b[31m</head><body>\u001b[39m\n\u001b[31m<h1>Not Found</h1>\u001b[39m\n\u001b[31m<p>The requested URL was not found on this server.</p>\u001b[39m\n\u001b[31m</body></html>\u001b[39m\n\u001b[31m\"\u001b[39m\n\n  46 |\n  47 |   test('hub auth gate and navigation present', () => {\n> 48 |     expect(html).toContain(\"window.location.replace('/hub/')\");\n     |                  ^\n  49 |     expect(html).toContain('btn-hub');\n  50 |     expect(html).toContain('🏡 Hub');\n  51 |   });\n    at /var/www/360lm/tests/production.spec.js:48:18"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 0,
                      "startTime": "2026-07-12T13:17:12.229Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/production-Production-—-HT-2ba61-gate-and-navigation-present-android-chrome/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/production.spec.js",
                        "column": 18,
                        "line": 48
                      }
                    },
                    {
                      "workerIndex": 155,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 14,
                      "error": {
                        "message": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoContain\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m) // indexOf\u001b[22m\n\nExpected substring: \u001b[32m\"window.location.replace('/hub/')\"\u001b[39m\nReceived string:    \u001b[31m\"<!DOCTYPE HTML PUBLIC \\\"-//W3C//DTD HTML 4.01//EN\\\" \\\"http://www.w3.org/TR/html4/strict.dtd\\\">\u001b[39m\n\u001b[31m<html><head>\u001b[39m\n\u001b[31m<title>404 Not Found</title>\u001b[39m\n\u001b[31m</head><body>\u001b[39m\n\u001b[31m<h1>Not Found</h1>\u001b[39m\n\u001b[31m<p>The requested URL was not found on this server.</p>\u001b[39m\n\u001b[31m</body></html>\u001b[39m\n\u001b[31m\"\u001b[39m",
                        "stack": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoContain\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m) // indexOf\u001b[22m\n\nExpected substring: \u001b[32m\"window.location.replace('/hub/')\"\u001b[39m\nReceived string:    \u001b[31m\"<!DOCTYPE HTML PUBLIC \\\"-//W3C//DTD HTML 4.01//EN\\\" \\\"http://www.w3.org/TR/html4/strict.dtd\\\">\u001b[39m\n\u001b[31m<html><head>\u001b[39m\n\u001b[31m<title>404 Not Found</title>\u001b[39m\n\u001b[31m</head><body>\u001b[39m\n\u001b[31m<h1>Not Found</h1>\u001b[39m\n\u001b[31m<p>The requested URL was not found on this server.</p>\u001b[39m\n\u001b[31m</body></html>\u001b[39m\n\u001b[31m\"\u001b[39m\n    at /var/www/360lm/tests/production.spec.js:48:18",
                        "location": {
                          "file": "/var/www/360lm/tests/production.spec.js",
                          "column": 18,
                          "line": 48
                        },
                        "snippet": "  46 |\n  47 |   test('hub auth gate and navigation present', () => {\n> 48 |     expect(html).toContain(\"window.location.replace('/hub/')\");\n     |                  ^\n  49 |     expect(html).toContain('btn-hub');\n  50 |     expect(html).toContain('🏡 Hub');\n  51 |   });"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/production.spec.js",
                            "column": 18,
                            "line": 48
                          },
                          "message": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoContain\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m) // indexOf\u001b[22m\n\nExpected substring: \u001b[32m\"window.location.replace('/hub/')\"\u001b[39m\nReceived string:    \u001b[31m\"<!DOCTYPE HTML PUBLIC \\\"-//W3C//DTD HTML 4.01//EN\\\" \\\"http://www.w3.org/TR/html4/strict.dtd\\\">\u001b[39m\n\u001b[31m<html><head>\u001b[39m\n\u001b[31m<title>404 Not Found</title>\u001b[39m\n\u001b[31m</head><body>\u001b[39m\n\u001b[31m<h1>Not Found</h1>\u001b[39m\n\u001b[31m<p>The requested URL was not found on this server.</p>\u001b[39m\n\u001b[31m</body></html>\u001b[39m\n\u001b[31m\"\u001b[39m\n\n  46 |\n  47 |   test('hub auth gate and navigation present', () => {\n> 48 |     expect(html).toContain(\"window.location.replace('/hub/')\");\n     |                  ^\n  49 |     expect(html).toContain('btn-hub');\n  50 |     expect(html).toContain('🏡 Hub');\n  51 |   });\n    at /var/www/360lm/tests/production.spec.js:48:18"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 1,
                      "startTime": "2026-07-12T13:17:13.303Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/production-Production-—-HT-2ba61-gate-and-navigation-present-android-chrome-retry1/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/production.spec.js",
                        "column": 18,
                        "line": 48
                      }
                    }
                  ],
                  "status": "unexpected"
                }
              ],
              "id": "e03a4b7a6378dd18a075-ff61111594cce2e575f7",
              "file": "production.spec.js",
              "line": 47,
              "column": 3
            },
            {
              "title": "all screens present",
              "ok": false,
              "tags": [],
              "tests": [
                {
                  "timeout": 30000,
                  "annotations": [],
                  "expectedStatus": "passed",
                  "projectId": "desktop-chrome",
                  "projectName": "desktop-chrome",
                  "results": [
                    {
                      "workerIndex": 328,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 11,
                      "error": {
                        "message": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoContain\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m) // indexOf\u001b[22m\n\nExpected substring: \u001b[32m\"id=\\\"screen-splash\\\"\"\u001b[39m\nReceived string:    \u001b[31m\"<!DOCTYPE HTML PUBLIC \\\"-//W3C//DTD HTML 4.01//EN\\\" \\\"http://www.w3.org/TR/html4/strict.dtd\\\">\u001b[39m\n\u001b[31m<html><head>\u001b[39m\n\u001b[31m<title>404 Not Found</title>\u001b[39m\n\u001b[31m</head><body>\u001b[39m\n\u001b[31m<h1>Not Found</h1>\u001b[39m\n\u001b[31m<p>The requested URL was not found on this server.</p>\u001b[39m\n\u001b[31m</body></html>\u001b[39m\n\u001b[31m\"\u001b[39m",
                        "stack": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoContain\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m) // indexOf\u001b[22m\n\nExpected substring: \u001b[32m\"id=\\\"screen-splash\\\"\"\u001b[39m\nReceived string:    \u001b[31m\"<!DOCTYPE HTML PUBLIC \\\"-//W3C//DTD HTML 4.01//EN\\\" \\\"http://www.w3.org/TR/html4/strict.dtd\\\">\u001b[39m\n\u001b[31m<html><head>\u001b[39m\n\u001b[31m<title>404 Not Found</title>\u001b[39m\n\u001b[31m</head><body>\u001b[39m\n\u001b[31m<h1>Not Found</h1>\u001b[39m\n\u001b[31m<p>The requested URL was not found on this server.</p>\u001b[39m\n\u001b[31m</body></html>\u001b[39m\n\u001b[31m\"\u001b[39m\n    at /var/www/360lm/tests/production.spec.js:40:18",
                        "location": {
                          "file": "/var/www/360lm/tests/production.spec.js",
                          "column": 18,
                          "line": 40
                        },
                        "snippet": "  38 |\n  39 |   test('all screens present', () => {\n> 40 |     expect(html).toContain('id=\"screen-splash\"');\n     |                  ^\n  41 |     expect(html).toContain('id=\"screen-dash\"');\n  42 |     expect(html).toContain('id=\"screen-new\"');\n  43 |     expect(html).toContain('id=\"screen-detail\"');"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/production.spec.js",
                            "column": 18,
                            "line": 40
                          },
                          "message": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoContain\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m) // indexOf\u001b[22m\n\nExpected substring: \u001b[32m\"id=\\\"screen-splash\\\"\"\u001b[39m\nReceived string:    \u001b[31m\"<!DOCTYPE HTML PUBLIC \\\"-//W3C//DTD HTML 4.01//EN\\\" \\\"http://www.w3.org/TR/html4/strict.dtd\\\">\u001b[39m\n\u001b[31m<html><head>\u001b[39m\n\u001b[31m<title>404 Not Found</title>\u001b[39m\n\u001b[31m</head><body>\u001b[39m\n\u001b[31m<h1>Not Found</h1>\u001b[39m\n\u001b[31m<p>The requested URL was not found on this server.</p>\u001b[39m\n\u001b[31m</body></html>\u001b[39m\n\u001b[31m\"\u001b[39m\n\n  38 |\n  39 |   test('all screens present', () => {\n> 40 |     expect(html).toContain('id=\"screen-splash\"');\n     |                  ^\n  41 |     expect(html).toContain('id=\"screen-dash\"');\n  42 |     expect(html).toContain('id=\"screen-new\"');\n  43 |     expect(html).toContain('id=\"screen-detail\"');\n    at /var/www/360lm/tests/production.spec.js:40:18"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 0,
                      "startTime": "2026-07-12T13:37:12.326Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/production-Production-—-HTML-Structure-all-screens-present-desktop-chrome/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/production.spec.js",
                        "column": 18,
                        "line": 40
                      }
                    },
                    {
                      "workerIndex": 329,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 10,
                      "error": {
                        "message": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoContain\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m) // indexOf\u001b[22m\n\nExpected substring: \u001b[32m\"id=\\\"screen-splash\\\"\"\u001b[39m\nReceived string:    \u001b[31m\"<!DOCTYPE HTML PUBLIC \\\"-//W3C//DTD HTML 4.01//EN\\\" \\\"http://www.w3.org/TR/html4/strict.dtd\\\">\u001b[39m\n\u001b[31m<html><head>\u001b[39m\n\u001b[31m<title>404 Not Found</title>\u001b[39m\n\u001b[31m</head><body>\u001b[39m\n\u001b[31m<h1>Not Found</h1>\u001b[39m\n\u001b[31m<p>The requested URL was not found on this server.</p>\u001b[39m\n\u001b[31m</body></html>\u001b[39m\n\u001b[31m\"\u001b[39m",
                        "stack": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoContain\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m) // indexOf\u001b[22m\n\nExpected substring: \u001b[32m\"id=\\\"screen-splash\\\"\"\u001b[39m\nReceived string:    \u001b[31m\"<!DOCTYPE HTML PUBLIC \\\"-//W3C//DTD HTML 4.01//EN\\\" \\\"http://www.w3.org/TR/html4/strict.dtd\\\">\u001b[39m\n\u001b[31m<html><head>\u001b[39m\n\u001b[31m<title>404 Not Found</title>\u001b[39m\n\u001b[31m</head><body>\u001b[39m\n\u001b[31m<h1>Not Found</h1>\u001b[39m\n\u001b[31m<p>The requested URL was not found on this server.</p>\u001b[39m\n\u001b[31m</body></html>\u001b[39m\n\u001b[31m\"\u001b[39m\n    at /var/www/360lm/tests/production.spec.js:40:18",
                        "location": {
                          "file": "/var/www/360lm/tests/production.spec.js",
                          "column": 18,
                          "line": 40
                        },
                        "snippet": "  38 |\n  39 |   test('all screens present', () => {\n> 40 |     expect(html).toContain('id=\"screen-splash\"');\n     |                  ^\n  41 |     expect(html).toContain('id=\"screen-dash\"');\n  42 |     expect(html).toContain('id=\"screen-new\"');\n  43 |     expect(html).toContain('id=\"screen-detail\"');"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/production.spec.js",
                            "column": 18,
                            "line": 40
                          },
                          "message": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoContain\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m) // indexOf\u001b[22m\n\nExpected substring: \u001b[32m\"id=\\\"screen-splash\\\"\"\u001b[39m\nReceived string:    \u001b[31m\"<!DOCTYPE HTML PUBLIC \\\"-//W3C//DTD HTML 4.01//EN\\\" \\\"http://www.w3.org/TR/html4/strict.dtd\\\">\u001b[39m\n\u001b[31m<html><head>\u001b[39m\n\u001b[31m<title>404 Not Found</title>\u001b[39m\n\u001b[31m</head><body>\u001b[39m\n\u001b[31m<h1>Not Found</h1>\u001b[39m\n\u001b[31m<p>The requested URL was not found on this server.</p>\u001b[39m\n\u001b[31m</body></html>\u001b[39m\n\u001b[31m\"\u001b[39m\n\n  38 |\n  39 |   test('all screens present', () => {\n> 40 |     expect(html).toContain('id=\"screen-splash\"');\n     |                  ^\n  41 |     expect(html).toContain('id=\"screen-dash\"');\n  42 |     expect(html).toContain('id=\"screen-new\"');\n  43 |     expect(html).toContain('id=\"screen-detail\"');\n    at /var/www/360lm/tests/production.spec.js:40:18"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 1,
                      "startTime": "2026-07-12T13:37:13.289Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/production-Production-—-HTML-Structure-all-screens-present-desktop-chrome-retry1/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/production.spec.js",
                        "column": 18,
                        "line": 40
                      }
                    }
                  ],
                  "status": "unexpected"
                }
              ],
              "id": "e03a4b7a6378dd18a075-aef8b5e752aa2910905d",
              "file": "production.spec.js",
              "line": 39,
              "column": 3
            },
            {
              "title": "hub auth gate and navigation present",
              "ok": false,
              "tags": [],
              "tests": [
                {
                  "timeout": 30000,
                  "annotations": [],
                  "expectedStatus": "passed",
                  "projectId": "desktop-chrome",
                  "projectName": "desktop-chrome",
                  "results": [
                    {
                      "workerIndex": 330,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 12,
                      "error": {
                        "message": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoContain\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m) // indexOf\u001b[22m\n\nExpected substring: \u001b[32m\"window.location.replace('/hub/')\"\u001b[39m\nReceived string:    \u001b[31m\"<!DOCTYPE HTML PUBLIC \\\"-//W3C//DTD HTML 4.01//EN\\\" \\\"http://www.w3.org/TR/html4/strict.dtd\\\">\u001b[39m\n\u001b[31m<html><head>\u001b[39m\n\u001b[31m<title>404 Not Found</title>\u001b[39m\n\u001b[31m</head><body>\u001b[39m\n\u001b[31m<h1>Not Found</h1>\u001b[39m\n\u001b[31m<p>The requested URL was not found on this server.</p>\u001b[39m\n\u001b[31m</body></html>\u001b[39m\n\u001b[31m\"\u001b[39m",
                        "stack": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoContain\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m) // indexOf\u001b[22m\n\nExpected substring: \u001b[32m\"window.location.replace('/hub/')\"\u001b[39m\nReceived string:    \u001b[31m\"<!DOCTYPE HTML PUBLIC \\\"-//W3C//DTD HTML 4.01//EN\\\" \\\"http://www.w3.org/TR/html4/strict.dtd\\\">\u001b[39m\n\u001b[31m<html><head>\u001b[39m\n\u001b[31m<title>404 Not Found</title>\u001b[39m\n\u001b[31m</head><body>\u001b[39m\n\u001b[31m<h1>Not Found</h1>\u001b[39m\n\u001b[31m<p>The requested URL was not found on this server.</p>\u001b[39m\n\u001b[31m</body></html>\u001b[39m\n\u001b[31m\"\u001b[39m\n    at /var/www/360lm/tests/production.spec.js:48:18",
                        "location": {
                          "file": "/var/www/360lm/tests/production.spec.js",
                          "column": 18,
                          "line": 48
                        },
                        "snippet": "  46 |\n  47 |   test('hub auth gate and navigation present', () => {\n> 48 |     expect(html).toContain(\"window.location.replace('/hub/')\");\n     |                  ^\n  49 |     expect(html).toContain('btn-hub');\n  50 |     expect(html).toContain('🏡 Hub');\n  51 |   });"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/production.spec.js",
                            "column": 18,
                            "line": 48
                          },
                          "message": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoContain\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m) // indexOf\u001b[22m\n\nExpected substring: \u001b[32m\"window.location.replace('/hub/')\"\u001b[39m\nReceived string:    \u001b[31m\"<!DOCTYPE HTML PUBLIC \\\"-//W3C//DTD HTML 4.01//EN\\\" \\\"http://www.w3.org/TR/html4/strict.dtd\\\">\u001b[39m\n\u001b[31m<html><head>\u001b[39m\n\u001b[31m<title>404 Not Found</title>\u001b[39m\n\u001b[31m</head><body>\u001b[39m\n\u001b[31m<h1>Not Found</h1>\u001b[39m\n\u001b[31m<p>The requested URL was not found on this server.</p>\u001b[39m\n\u001b[31m</body></html>\u001b[39m\n\u001b[31m\"\u001b[39m\n\n  46 |\n  47 |   test('hub auth gate and navigation present', () => {\n> 48 |     expect(html).toContain(\"window.location.replace('/hub/')\");\n     |                  ^\n  49 |     expect(html).toContain('btn-hub');\n  50 |     expect(html).toContain('🏡 Hub');\n  51 |   });\n    at /var/www/360lm/tests/production.spec.js:48:18"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 0,
                      "startTime": "2026-07-12T13:37:14.232Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/production-Production-—-HT-2ba61-gate-and-navigation-present-desktop-chrome/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/production.spec.js",
                        "column": 18,
                        "line": 48
                      }
                    },
                    {
                      "workerIndex": 331,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 15,
                      "error": {
                        "message": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoContain\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m) // indexOf\u001b[22m\n\nExpected substring: \u001b[32m\"window.location.replace('/hub/')\"\u001b[39m\nReceived string:    \u001b[31m\"<!DOCTYPE HTML PUBLIC \\\"-//W3C//DTD HTML 4.01//EN\\\" \\\"http://www.w3.org/TR/html4/strict.dtd\\\">\u001b[39m\n\u001b[31m<html><head>\u001b[39m\n\u001b[31m<title>404 Not Found</title>\u001b[39m\n\u001b[31m</head><body>\u001b[39m\n\u001b[31m<h1>Not Found</h1>\u001b[39m\n\u001b[31m<p>The requested URL was not found on this server.</p>\u001b[39m\n\u001b[31m</body></html>\u001b[39m\n\u001b[31m\"\u001b[39m",
                        "stack": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoContain\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m) // indexOf\u001b[22m\n\nExpected substring: \u001b[32m\"window.location.replace('/hub/')\"\u001b[39m\nReceived string:    \u001b[31m\"<!DOCTYPE HTML PUBLIC \\\"-//W3C//DTD HTML 4.01//EN\\\" \\\"http://www.w3.org/TR/html4/strict.dtd\\\">\u001b[39m\n\u001b[31m<html><head>\u001b[39m\n\u001b[31m<title>404 Not Found</title>\u001b[39m\n\u001b[31m</head><body>\u001b[39m\n\u001b[31m<h1>Not Found</h1>\u001b[39m\n\u001b[31m<p>The requested URL was not found on this server.</p>\u001b[39m\n\u001b[31m</body></html>\u001b[39m\n\u001b[31m\"\u001b[39m\n    at /var/www/360lm/tests/production.spec.js:48:18",
                        "location": {
                          "file": "/var/www/360lm/tests/production.spec.js",
                          "column": 18,
                          "line": 48
                        },
                        "snippet": "  46 |\n  47 |   test('hub auth gate and navigation present', () => {\n> 48 |     expect(html).toContain(\"window.location.replace('/hub/')\");\n     |                  ^\n  49 |     expect(html).toContain('btn-hub');\n  50 |     expect(html).toContain('🏡 Hub');\n  51 |   });"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/production.spec.js",
                            "column": 18,
                            "line": 48
                          },
                          "message": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoContain\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m) // indexOf\u001b[22m\n\nExpected substring: \u001b[32m\"window.location.replace('/hub/')\"\u001b[39m\nReceived string:    \u001b[31m\"<!DOCTYPE HTML PUBLIC \\\"-//W3C//DTD HTML 4.01//EN\\\" \\\"http://www.w3.org/TR/html4/strict.dtd\\\">\u001b[39m\n\u001b[31m<html><head>\u001b[39m\n\u001b[31m<title>404 Not Found</title>\u001b[39m\n\u001b[31m</head><body>\u001b[39m\n\u001b[31m<h1>Not Found</h1>\u001b[39m\n\u001b[31m<p>The requested URL was not found on this server.</p>\u001b[39m\n\u001b[31m</body></html>\u001b[39m\n\u001b[31m\"\u001b[39m\n\n  46 |\n  47 |   test('hub auth gate and navigation present', () => {\n> 48 |     expect(html).toContain(\"window.location.replace('/hub/')\");\n     |                  ^\n  49 |     expect(html).toContain('btn-hub');\n  50 |     expect(html).toContain('🏡 Hub');\n  51 |   });\n    at /var/www/360lm/tests/production.spec.js:48:18"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 1,
                      "startTime": "2026-07-12T13:37:15.149Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/production-Production-—-HT-2ba61-gate-and-navigation-present-desktop-chrome-retry1/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/production.spec.js",
                        "column": 18,
                        "line": 48
                      }
                    }
                  ],
                  "status": "unexpected"
                }
              ],
              "id": "e03a4b7a6378dd18a075-6f38512f94362937af87",
              "file": "production.spec.js",
              "line": 47,
              "column": 3
            }
          ]
        }
      ]
    },
    {
      "title": "vehicle.spec.js",
      "file": "vehicle.spec.js",
      "column": 0,
      "line": 0,
      "specs": [],
      "suites": [
        {
          "title": "Vehicle Log — Smoke",
          "file": "vehicle.spec.js",
          "line": 9,
          "column": 6,
          "specs": [
            {
              "title": "page or hub loads",
              "ok": true,
              "tags": [],
              "tests": [
                {
                  "timeout": 30000,
                  "annotations": [],
                  "expectedStatus": "passed",
                  "projectId": "android-chrome",
                  "projectName": "android-chrome",
                  "results": [
                    {
                      "workerIndex": 156,
                      "parallelIndex": 0,
                      "status": "passed",
                      "duration": 1073,
                      "errors": [],
                      "stdout": [],
                      "stderr": [],
                      "retry": 0,
                      "startTime": "2026-07-12T13:17:14.316Z",
                      "annotations": [],
                      "attachments": []
                    }
                  ],
                  "status": "expected"
                }
              ],
              "id": "084e7535606ab6e45f08-c69be7604bfc5bd3eecf",
              "file": "vehicle.spec.js",
              "line": 10,
              "column": 3
            },
            {
              "title": "manifest.json returns 200",
              "ok": false,
              "tags": [],
              "tests": [
                {
                  "timeout": 30000,
                  "annotations": [],
                  "expectedStatus": "passed",
                  "projectId": "android-chrome",
                  "projectName": "android-chrome",
                  "results": [
                    {
                      "workerIndex": 156,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 236,
                      "error": {
                        "message": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoBe\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m) // Object.is equality\u001b[22m\n\nExpected: \u001b[32m200\u001b[39m\nReceived: \u001b[31m404\u001b[39m",
                        "stack": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoBe\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m) // Object.is equality\u001b[22m\n\nExpected: \u001b[32m200\u001b[39m\nReceived: \u001b[31m404\u001b[39m\n    at /var/www/360lm/tests/vehicle.spec.js:19:28",
                        "location": {
                          "file": "/var/www/360lm/tests/vehicle.spec.js",
                          "column": 28,
                          "line": 19
                        },
                        "snippet": "  17 |   test('manifest.json returns 200', async ({ page }) => {\n  18 |     const resp = await page.goto(BASE_URL + 'manifest.json');\n> 19 |     expect(resp?.status()).toBe(200);\n     |                            ^\n  20 |   });\n  21 |\n  22 |   test('sw.js contains current vehicle version', async ({ page }) => {"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/vehicle.spec.js",
                            "column": 28,
                            "line": 19
                          },
                          "message": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoBe\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m) // Object.is equality\u001b[22m\n\nExpected: \u001b[32m200\u001b[39m\nReceived: \u001b[31m404\u001b[39m\n\n  17 |   test('manifest.json returns 200', async ({ page }) => {\n  18 |     const resp = await page.goto(BASE_URL + 'manifest.json');\n> 19 |     expect(resp?.status()).toBe(200);\n     |                            ^\n  20 |   });\n  21 |\n  22 |   test('sw.js contains current vehicle version', async ({ page }) => {\n    at /var/www/360lm/tests/vehicle.spec.js:19:28"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 0,
                      "startTime": "2026-07-12T13:17:15.569Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "screenshot",
                          "contentType": "image/png",
                          "path": "/var/www/360lm/test-results/vehicle-Vehicle-Log-—-Smoke-manifest-json-returns-200-android-chrome/test-failed-1.png"
                        },
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/vehicle-Vehicle-Log-—-Smoke-manifest-json-returns-200-android-chrome/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/vehicle.spec.js",
                        "column": 28,
                        "line": 19
                      }
                    },
                    {
                      "workerIndex": 157,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 351,
                      "error": {
                        "message": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoBe\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m) // Object.is equality\u001b[22m\n\nExpected: \u001b[32m200\u001b[39m\nReceived: \u001b[31m404\u001b[39m",
                        "stack": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoBe\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m) // Object.is equality\u001b[22m\n\nExpected: \u001b[32m200\u001b[39m\nReceived: \u001b[31m404\u001b[39m\n    at /var/www/360lm/tests/vehicle.spec.js:19:28",
                        "location": {
                          "file": "/var/www/360lm/tests/vehicle.spec.js",
                          "column": 28,
                          "line": 19
                        },
                        "snippet": "  17 |   test('manifest.json returns 200', async ({ page }) => {\n  18 |     const resp = await page.goto(BASE_URL + 'manifest.json');\n> 19 |     expect(resp?.status()).toBe(200);\n     |                            ^\n  20 |   });\n  21 |\n  22 |   test('sw.js contains current vehicle version', async ({ page }) => {"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/vehicle.spec.js",
                            "column": 28,
                            "line": 19
                          },
                          "message": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoBe\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m) // Object.is equality\u001b[22m\n\nExpected: \u001b[32m200\u001b[39m\nReceived: \u001b[31m404\u001b[39m\n\n  17 |   test('manifest.json returns 200', async ({ page }) => {\n  18 |     const resp = await page.goto(BASE_URL + 'manifest.json');\n> 19 |     expect(resp?.status()).toBe(200);\n     |                            ^\n  20 |   });\n  21 |\n  22 |   test('sw.js contains current vehicle version', async ({ page }) => {\n    at /var/www/360lm/tests/vehicle.spec.js:19:28"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 1,
                      "startTime": "2026-07-12T13:17:16.761Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "screenshot",
                          "contentType": "image/png",
                          "path": "/var/www/360lm/test-results/vehicle-Vehicle-Log-—-Smoke-manifest-json-returns-200-android-chrome-retry1/test-failed-1.png"
                        },
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/vehicle-Vehicle-Log-—-Smoke-manifest-json-returns-200-android-chrome-retry1/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/vehicle.spec.js",
                        "column": 28,
                        "line": 19
                      }
                    }
                  ],
                  "status": "unexpected"
                }
              ],
              "id": "084e7535606ab6e45f08-0af762c479f72f5c2368",
              "file": "vehicle.spec.js",
              "line": 17,
              "column": 3
            },
            {
              "title": "sw.js contains current vehicle version",
              "ok": false,
              "tags": [],
              "tests": [
                {
                  "timeout": 30000,
                  "annotations": [],
                  "expectedStatus": "passed",
                  "projectId": "android-chrome",
                  "projectName": "android-chrome",
                  "results": [
                    {
                      "workerIndex": 158,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 368,
                      "error": {
                        "message": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoMatch\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m)\u001b[22m\n\nExpected pattern: \u001b[32m/vehicle-v\\d+/\u001b[39m\nReceived string:  \u001b[31m\"<!DOCTYPE HTML PUBLIC \\\"-//W3C//DTD HTML 4.01//EN\\\" \\\"http://www.w3.org/TR/html4/strict.dtd\\\">\u001b[39m\n\u001b[31m<html><head>\u001b[39m\n\u001b[31m<title>404 Not Found</title>\u001b[39m\n\u001b[31m</head><body>\u001b[39m\n\u001b[31m<h1>Not Found</h1>\u001b[39m\n\u001b[31m<p>The requested URL was not found on this server.</p>\u001b[39m\n\u001b[31m</body></html>\u001b[39m\n\u001b[31m\"\u001b[39m",
                        "stack": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoMatch\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m)\u001b[22m\n\nExpected pattern: \u001b[32m/vehicle-v\\d+/\u001b[39m\nReceived string:  \u001b[31m\"<!DOCTYPE HTML PUBLIC \\\"-//W3C//DTD HTML 4.01//EN\\\" \\\"http://www.w3.org/TR/html4/strict.dtd\\\">\u001b[39m\n\u001b[31m<html><head>\u001b[39m\n\u001b[31m<title>404 Not Found</title>\u001b[39m\n\u001b[31m</head><body>\u001b[39m\n\u001b[31m<h1>Not Found</h1>\u001b[39m\n\u001b[31m<p>The requested URL was not found on this server.</p>\u001b[39m\n\u001b[31m</body></html>\u001b[39m\n\u001b[31m\"\u001b[39m\n    at /var/www/360lm/tests/vehicle.spec.js:25:18",
                        "location": {
                          "file": "/var/www/360lm/tests/vehicle.spec.js",
                          "column": 18,
                          "line": 25
                        },
                        "snippet": "  23 |     const resp = await page.goto(BASE_URL + 'sw.js');\n  24 |     const text = await resp?.text() || '';\n> 25 |     expect(text).toMatch(/vehicle-v\\d+/);\n     |                  ^\n  26 |   });\n  27 | });\n  28 |"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/vehicle.spec.js",
                            "column": 18,
                            "line": 25
                          },
                          "message": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoMatch\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m)\u001b[22m\n\nExpected pattern: \u001b[32m/vehicle-v\\d+/\u001b[39m\nReceived string:  \u001b[31m\"<!DOCTYPE HTML PUBLIC \\\"-//W3C//DTD HTML 4.01//EN\\\" \\\"http://www.w3.org/TR/html4/strict.dtd\\\">\u001b[39m\n\u001b[31m<html><head>\u001b[39m\n\u001b[31m<title>404 Not Found</title>\u001b[39m\n\u001b[31m</head><body>\u001b[39m\n\u001b[31m<h1>Not Found</h1>\u001b[39m\n\u001b[31m<p>The requested URL was not found on this server.</p>\u001b[39m\n\u001b[31m</body></html>\u001b[39m\n\u001b[31m\"\u001b[39m\n\n  23 |     const resp = await page.goto(BASE_URL + 'sw.js');\n  24 |     const text = await resp?.text() || '';\n> 25 |     expect(text).toMatch(/vehicle-v\\d+/);\n     |                  ^\n  26 |   });\n  27 | });\n  28 |\n    at /var/www/360lm/tests/vehicle.spec.js:25:18"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 0,
                      "startTime": "2026-07-12T13:17:18.182Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "screenshot",
                          "contentType": "image/png",
                          "path": "/var/www/360lm/test-results/vehicle-Vehicle-Log-—-Smok-beee2-ins-current-vehicle-version-android-chrome/test-failed-1.png"
                        },
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/vehicle-Vehicle-Log-—-Smok-beee2-ins-current-vehicle-version-android-chrome/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/vehicle.spec.js",
                        "column": 18,
                        "line": 25
                      }
                    },
                    {
                      "workerIndex": 159,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 339,
                      "error": {
                        "message": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoMatch\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m)\u001b[22m\n\nExpected pattern: \u001b[32m/vehicle-v\\d+/\u001b[39m\nReceived string:  \u001b[31m\"<!DOCTYPE HTML PUBLIC \\\"-//W3C//DTD HTML 4.01//EN\\\" \\\"http://www.w3.org/TR/html4/strict.dtd\\\">\u001b[39m\n\u001b[31m<html><head>\u001b[39m\n\u001b[31m<title>404 Not Found</title>\u001b[39m\n\u001b[31m</head><body>\u001b[39m\n\u001b[31m<h1>Not Found</h1>\u001b[39m\n\u001b[31m<p>The requested URL was not found on this server.</p>\u001b[39m\n\u001b[31m</body></html>\u001b[39m\n\u001b[31m\"\u001b[39m",
                        "stack": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoMatch\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m)\u001b[22m\n\nExpected pattern: \u001b[32m/vehicle-v\\d+/\u001b[39m\nReceived string:  \u001b[31m\"<!DOCTYPE HTML PUBLIC \\\"-//W3C//DTD HTML 4.01//EN\\\" \\\"http://www.w3.org/TR/html4/strict.dtd\\\">\u001b[39m\n\u001b[31m<html><head>\u001b[39m\n\u001b[31m<title>404 Not Found</title>\u001b[39m\n\u001b[31m</head><body>\u001b[39m\n\u001b[31m<h1>Not Found</h1>\u001b[39m\n\u001b[31m<p>The requested URL was not found on this server.</p>\u001b[39m\n\u001b[31m</body></html>\u001b[39m\n\u001b[31m\"\u001b[39m\n    at /var/www/360lm/tests/vehicle.spec.js:25:18",
                        "location": {
                          "file": "/var/www/360lm/tests/vehicle.spec.js",
                          "column": 18,
                          "line": 25
                        },
                        "snippet": "  23 |     const resp = await page.goto(BASE_URL + 'sw.js');\n  24 |     const text = await resp?.text() || '';\n> 25 |     expect(text).toMatch(/vehicle-v\\d+/);\n     |                  ^\n  26 |   });\n  27 | });\n  28 |"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/vehicle.spec.js",
                            "column": 18,
                            "line": 25
                          },
                          "message": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoMatch\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m)\u001b[22m\n\nExpected pattern: \u001b[32m/vehicle-v\\d+/\u001b[39m\nReceived string:  \u001b[31m\"<!DOCTYPE HTML PUBLIC \\\"-//W3C//DTD HTML 4.01//EN\\\" \\\"http://www.w3.org/TR/html4/strict.dtd\\\">\u001b[39m\n\u001b[31m<html><head>\u001b[39m\n\u001b[31m<title>404 Not Found</title>\u001b[39m\n\u001b[31m</head><body>\u001b[39m\n\u001b[31m<h1>Not Found</h1>\u001b[39m\n\u001b[31m<p>The requested URL was not found on this server.</p>\u001b[39m\n\u001b[31m</body></html>\u001b[39m\n\u001b[31m\"\u001b[39m\n\n  23 |     const resp = await page.goto(BASE_URL + 'sw.js');\n  24 |     const text = await resp?.text() || '';\n> 25 |     expect(text).toMatch(/vehicle-v\\d+/);\n     |                  ^\n  26 |   });\n  27 | });\n  28 |\n    at /var/www/360lm/tests/vehicle.spec.js:25:18"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 1,
                      "startTime": "2026-07-12T13:17:19.566Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "screenshot",
                          "contentType": "image/png",
                          "path": "/var/www/360lm/test-results/vehicle-Vehicle-Log-—-Smok-beee2-ins-current-vehicle-version-android-chrome-retry1/test-failed-1.png"
                        },
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/vehicle-Vehicle-Log-—-Smok-beee2-ins-current-vehicle-version-android-chrome-retry1/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/vehicle.spec.js",
                        "column": 18,
                        "line": 25
                      }
                    }
                  ],
                  "status": "unexpected"
                }
              ],
              "id": "084e7535606ab6e45f08-4c6f0c5164abc99757d6",
              "file": "vehicle.spec.js",
              "line": 22,
              "column": 3
            },
            {
              "title": "page or hub loads",
              "ok": true,
              "tags": [],
              "tests": [
                {
                  "timeout": 30000,
                  "annotations": [],
                  "expectedStatus": "passed",
                  "projectId": "desktop-chrome",
                  "projectName": "desktop-chrome",
                  "results": [
                    {
                      "workerIndex": 332,
                      "parallelIndex": 0,
                      "status": "passed",
                      "duration": 1035,
                      "errors": [],
                      "stdout": [],
                      "stderr": [],
                      "retry": 0,
                      "startTime": "2026-07-12T13:37:16.101Z",
                      "annotations": [],
                      "attachments": []
                    }
                  ],
                  "status": "expected"
                }
              ],
              "id": "084e7535606ab6e45f08-f19cecc0003599b5d468",
              "file": "vehicle.spec.js",
              "line": 10,
              "column": 3
            },
            {
              "title": "manifest.json returns 200",
              "ok": false,
              "tags": [],
              "tests": [
                {
                  "timeout": 30000,
                  "annotations": [],
                  "expectedStatus": "passed",
                  "projectId": "desktop-chrome",
                  "projectName": "desktop-chrome",
                  "results": [
                    {
                      "workerIndex": 332,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 202,
                      "error": {
                        "message": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoBe\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m) // Object.is equality\u001b[22m\n\nExpected: \u001b[32m200\u001b[39m\nReceived: \u001b[31m404\u001b[39m",
                        "stack": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoBe\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m) // Object.is equality\u001b[22m\n\nExpected: \u001b[32m200\u001b[39m\nReceived: \u001b[31m404\u001b[39m\n    at /var/www/360lm/tests/vehicle.spec.js:19:28",
                        "location": {
                          "file": "/var/www/360lm/tests/vehicle.spec.js",
                          "column": 28,
                          "line": 19
                        },
                        "snippet": "  17 |   test('manifest.json returns 200', async ({ page }) => {\n  18 |     const resp = await page.goto(BASE_URL + 'manifest.json');\n> 19 |     expect(resp?.status()).toBe(200);\n     |                            ^\n  20 |   });\n  21 |\n  22 |   test('sw.js contains current vehicle version', async ({ page }) => {"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/vehicle.spec.js",
                            "column": 28,
                            "line": 19
                          },
                          "message": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoBe\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m) // Object.is equality\u001b[22m\n\nExpected: \u001b[32m200\u001b[39m\nReceived: \u001b[31m404\u001b[39m\n\n  17 |   test('manifest.json returns 200', async ({ page }) => {\n  18 |     const resp = await page.goto(BASE_URL + 'manifest.json');\n> 19 |     expect(resp?.status()).toBe(200);\n     |                            ^\n  20 |   });\n  21 |\n  22 |   test('sw.js contains current vehicle version', async ({ page }) => {\n    at /var/www/360lm/tests/vehicle.spec.js:19:28"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 0,
                      "startTime": "2026-07-12T13:37:17.307Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "screenshot",
                          "contentType": "image/png",
                          "path": "/var/www/360lm/test-results/vehicle-Vehicle-Log-—-Smoke-manifest-json-returns-200-desktop-chrome/test-failed-1.png"
                        },
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/vehicle-Vehicle-Log-—-Smoke-manifest-json-returns-200-desktop-chrome/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/vehicle.spec.js",
                        "column": 28,
                        "line": 19
                      }
                    },
                    {
                      "workerIndex": 333,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 275,
                      "error": {
                        "message": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoBe\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m) // Object.is equality\u001b[22m\n\nExpected: \u001b[32m200\u001b[39m\nReceived: \u001b[31m404\u001b[39m",
                        "stack": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoBe\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m) // Object.is equality\u001b[22m\n\nExpected: \u001b[32m200\u001b[39m\nReceived: \u001b[31m404\u001b[39m\n    at /var/www/360lm/tests/vehicle.spec.js:19:28",
                        "location": {
                          "file": "/var/www/360lm/tests/vehicle.spec.js",
                          "column": 28,
                          "line": 19
                        },
                        "snippet": "  17 |   test('manifest.json returns 200', async ({ page }) => {\n  18 |     const resp = await page.goto(BASE_URL + 'manifest.json');\n> 19 |     expect(resp?.status()).toBe(200);\n     |                            ^\n  20 |   });\n  21 |\n  22 |   test('sw.js contains current vehicle version', async ({ page }) => {"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/vehicle.spec.js",
                            "column": 28,
                            "line": 19
                          },
                          "message": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoBe\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m) // Object.is equality\u001b[22m\n\nExpected: \u001b[32m200\u001b[39m\nReceived: \u001b[31m404\u001b[39m\n\n  17 |   test('manifest.json returns 200', async ({ page }) => {\n  18 |     const resp = await page.goto(BASE_URL + 'manifest.json');\n> 19 |     expect(resp?.status()).toBe(200);\n     |                            ^\n  20 |   });\n  21 |\n  22 |   test('sw.js contains current vehicle version', async ({ page }) => {\n    at /var/www/360lm/tests/vehicle.spec.js:19:28"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 1,
                      "startTime": "2026-07-12T13:37:18.314Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "screenshot",
                          "contentType": "image/png",
                          "path": "/var/www/360lm/test-results/vehicle-Vehicle-Log-—-Smoke-manifest-json-returns-200-desktop-chrome-retry1/test-failed-1.png"
                        },
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/vehicle-Vehicle-Log-—-Smoke-manifest-json-returns-200-desktop-chrome-retry1/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/vehicle.spec.js",
                        "column": 28,
                        "line": 19
                      }
                    }
                  ],
                  "status": "unexpected"
                }
              ],
              "id": "084e7535606ab6e45f08-6d3329d2335f2eb6f7a3",
              "file": "vehicle.spec.js",
              "line": 17,
              "column": 3
            },
            {
              "title": "sw.js contains current vehicle version",
              "ok": false,
              "tags": [],
              "tests": [
                {
                  "timeout": 30000,
                  "annotations": [],
                  "expectedStatus": "passed",
                  "projectId": "desktop-chrome",
                  "projectName": "desktop-chrome",
                  "results": [
                    {
                      "workerIndex": 334,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 296,
                      "error": {
                        "message": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoMatch\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m)\u001b[22m\n\nExpected pattern: \u001b[32m/vehicle-v\\d+/\u001b[39m\nReceived string:  \u001b[31m\"<!DOCTYPE HTML PUBLIC \\\"-//W3C//DTD HTML 4.01//EN\\\" \\\"http://www.w3.org/TR/html4/strict.dtd\\\">\u001b[39m\n\u001b[31m<html><head>\u001b[39m\n\u001b[31m<title>404 Not Found</title>\u001b[39m\n\u001b[31m</head><body>\u001b[39m\n\u001b[31m<h1>Not Found</h1>\u001b[39m\n\u001b[31m<p>The requested URL was not found on this server.</p>\u001b[39m\n\u001b[31m</body></html>\u001b[39m\n\u001b[31m\"\u001b[39m",
                        "stack": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoMatch\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m)\u001b[22m\n\nExpected pattern: \u001b[32m/vehicle-v\\d+/\u001b[39m\nReceived string:  \u001b[31m\"<!DOCTYPE HTML PUBLIC \\\"-//W3C//DTD HTML 4.01//EN\\\" \\\"http://www.w3.org/TR/html4/strict.dtd\\\">\u001b[39m\n\u001b[31m<html><head>\u001b[39m\n\u001b[31m<title>404 Not Found</title>\u001b[39m\n\u001b[31m</head><body>\u001b[39m\n\u001b[31m<h1>Not Found</h1>\u001b[39m\n\u001b[31m<p>The requested URL was not found on this server.</p>\u001b[39m\n\u001b[31m</body></html>\u001b[39m\n\u001b[31m\"\u001b[39m\n    at /var/www/360lm/tests/vehicle.spec.js:25:18",
                        "location": {
                          "file": "/var/www/360lm/tests/vehicle.spec.js",
                          "column": 18,
                          "line": 25
                        },
                        "snippet": "  23 |     const resp = await page.goto(BASE_URL + 'sw.js');\n  24 |     const text = await resp?.text() || '';\n> 25 |     expect(text).toMatch(/vehicle-v\\d+/);\n     |                  ^\n  26 |   });\n  27 | });\n  28 |"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/vehicle.spec.js",
                            "column": 18,
                            "line": 25
                          },
                          "message": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoMatch\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m)\u001b[22m\n\nExpected pattern: \u001b[32m/vehicle-v\\d+/\u001b[39m\nReceived string:  \u001b[31m\"<!DOCTYPE HTML PUBLIC \\\"-//W3C//DTD HTML 4.01//EN\\\" \\\"http://www.w3.org/TR/html4/strict.dtd\\\">\u001b[39m\n\u001b[31m<html><head>\u001b[39m\n\u001b[31m<title>404 Not Found</title>\u001b[39m\n\u001b[31m</head><body>\u001b[39m\n\u001b[31m<h1>Not Found</h1>\u001b[39m\n\u001b[31m<p>The requested URL was not found on this server.</p>\u001b[39m\n\u001b[31m</body></html>\u001b[39m\n\u001b[31m\"\u001b[39m\n\n  23 |     const resp = await page.goto(BASE_URL + 'sw.js');\n  24 |     const text = await resp?.text() || '';\n> 25 |     expect(text).toMatch(/vehicle-v\\d+/);\n     |                  ^\n  26 |   });\n  27 | });\n  28 |\n    at /var/www/360lm/tests/vehicle.spec.js:25:18"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 0,
                      "startTime": "2026-07-12T13:37:19.557Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "screenshot",
                          "contentType": "image/png",
                          "path": "/var/www/360lm/test-results/vehicle-Vehicle-Log-—-Smok-beee2-ins-current-vehicle-version-desktop-chrome/test-failed-1.png"
                        },
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/vehicle-Vehicle-Log-—-Smok-beee2-ins-current-vehicle-version-desktop-chrome/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/vehicle.spec.js",
                        "column": 18,
                        "line": 25
                      }
                    },
                    {
                      "workerIndex": 335,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 327,
                      "error": {
                        "message": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoMatch\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m)\u001b[22m\n\nExpected pattern: \u001b[32m/vehicle-v\\d+/\u001b[39m\nReceived string:  \u001b[31m\"<!DOCTYPE HTML PUBLIC \\\"-//W3C//DTD HTML 4.01//EN\\\" \\\"http://www.w3.org/TR/html4/strict.dtd\\\">\u001b[39m\n\u001b[31m<html><head>\u001b[39m\n\u001b[31m<title>404 Not Found</title>\u001b[39m\n\u001b[31m</head><body>\u001b[39m\n\u001b[31m<h1>Not Found</h1>\u001b[39m\n\u001b[31m<p>The requested URL was not found on this server.</p>\u001b[39m\n\u001b[31m</body></html>\u001b[39m\n\u001b[31m\"\u001b[39m",
                        "stack": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoMatch\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m)\u001b[22m\n\nExpected pattern: \u001b[32m/vehicle-v\\d+/\u001b[39m\nReceived string:  \u001b[31m\"<!DOCTYPE HTML PUBLIC \\\"-//W3C//DTD HTML 4.01//EN\\\" \\\"http://www.w3.org/TR/html4/strict.dtd\\\">\u001b[39m\n\u001b[31m<html><head>\u001b[39m\n\u001b[31m<title>404 Not Found</title>\u001b[39m\n\u001b[31m</head><body>\u001b[39m\n\u001b[31m<h1>Not Found</h1>\u001b[39m\n\u001b[31m<p>The requested URL was not found on this server.</p>\u001b[39m\n\u001b[31m</body></html>\u001b[39m\n\u001b[31m\"\u001b[39m\n    at /var/www/360lm/tests/vehicle.spec.js:25:18",
                        "location": {
                          "file": "/var/www/360lm/tests/vehicle.spec.js",
                          "column": 18,
                          "line": 25
                        },
                        "snippet": "  23 |     const resp = await page.goto(BASE_URL + 'sw.js');\n  24 |     const text = await resp?.text() || '';\n> 25 |     expect(text).toMatch(/vehicle-v\\d+/);\n     |                  ^\n  26 |   });\n  27 | });\n  28 |"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/vehicle.spec.js",
                            "column": 18,
                            "line": 25
                          },
                          "message": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoMatch\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m)\u001b[22m\n\nExpected pattern: \u001b[32m/vehicle-v\\d+/\u001b[39m\nReceived string:  \u001b[31m\"<!DOCTYPE HTML PUBLIC \\\"-//W3C//DTD HTML 4.01//EN\\\" \\\"http://www.w3.org/TR/html4/strict.dtd\\\">\u001b[39m\n\u001b[31m<html><head>\u001b[39m\n\u001b[31m<title>404 Not Found</title>\u001b[39m\n\u001b[31m</head><body>\u001b[39m\n\u001b[31m<h1>Not Found</h1>\u001b[39m\n\u001b[31m<p>The requested URL was not found on this server.</p>\u001b[39m\n\u001b[31m</body></html>\u001b[39m\n\u001b[31m\"\u001b[39m\n\n  23 |     const resp = await page.goto(BASE_URL + 'sw.js');\n  24 |     const text = await resp?.text() || '';\n> 25 |     expect(text).toMatch(/vehicle-v\\d+/);\n     |                  ^\n  26 |   });\n  27 | });\n  28 |\n    at /var/www/360lm/tests/vehicle.spec.js:25:18"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 1,
                      "startTime": "2026-07-12T13:37:20.794Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "screenshot",
                          "contentType": "image/png",
                          "path": "/var/www/360lm/test-results/vehicle-Vehicle-Log-—-Smok-beee2-ins-current-vehicle-version-desktop-chrome-retry1/test-failed-1.png"
                        },
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/vehicle-Vehicle-Log-—-Smok-beee2-ins-current-vehicle-version-desktop-chrome-retry1/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/vehicle.spec.js",
                        "column": 18,
                        "line": 25
                      }
                    }
                  ],
                  "status": "unexpected"
                }
              ],
              "id": "084e7535606ab6e45f08-b4f1974d25bcf1fbe34d",
              "file": "vehicle.spec.js",
              "line": 22,
              "column": 3
            }
          ]
        },
        {
          "title": "Vehicle Log — HTML Structure (v18)",
          "file": "vehicle.spec.js",
          "line": 29,
          "column": 6,
          "specs": [
            {
              "title": "edit+delegate modals and edit-log screen present",
              "ok": false,
              "tags": [],
              "tests": [
                {
                  "timeout": 30000,
                  "annotations": [],
                  "expectedStatus": "passed",
                  "projectId": "android-chrome",
                  "projectName": "android-chrome",
                  "results": [
                    {
                      "workerIndex": 160,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 70,
                      "error": {
                        "message": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoContain\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m) // indexOf\u001b[22m\n\nExpected substring: \u001b[32m\"id=\\\"modal-edit-entry\\\"\"\u001b[39m\nReceived string:    \u001b[31m\"<!DOCTYPE HTML PUBLIC \\\"-//W3C//DTD HTML 4.01//EN\\\" \\\"http://www.w3.org/TR/html4/strict.dtd\\\">\u001b[39m\n\u001b[31m<html><head>\u001b[39m\n\u001b[31m<title>404 Not Found</title>\u001b[39m\n\u001b[31m</head><body>\u001b[39m\n\u001b[31m<h1>Not Found</h1>\u001b[39m\n\u001b[31m<p>The requested URL was not found on this server.</p>\u001b[39m\n\u001b[31m</body></html>\u001b[39m\n\u001b[31m\"\u001b[39m",
                        "stack": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoContain\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m) // indexOf\u001b[22m\n\nExpected substring: \u001b[32m\"id=\\\"modal-edit-entry\\\"\"\u001b[39m\nReceived string:    \u001b[31m\"<!DOCTYPE HTML PUBLIC \\\"-//W3C//DTD HTML 4.01//EN\\\" \\\"http://www.w3.org/TR/html4/strict.dtd\\\">\u001b[39m\n\u001b[31m<html><head>\u001b[39m\n\u001b[31m<title>404 Not Found</title>\u001b[39m\n\u001b[31m</head><body>\u001b[39m\n\u001b[31m<h1>Not Found</h1>\u001b[39m\n\u001b[31m<p>The requested URL was not found on this server.</p>\u001b[39m\n\u001b[31m</body></html>\u001b[39m\n\u001b[31m\"\u001b[39m\n    at /var/www/360lm/tests/vehicle.spec.js:35:18",
                        "location": {
                          "file": "/var/www/360lm/tests/vehicle.spec.js",
                          "column": 18,
                          "line": 35
                        },
                        "snippet": "  33 |     const html = await resp.text();\n  34 |\n> 35 |     expect(html).toContain('id=\"modal-edit-entry\"');\n     |                  ^\n  36 |     expect(html).toContain('id=\"edit-step-form\"');\n  37 |     expect(html).toContain('id=\"edit-step-pin\"');\n  38 |     expect(html).toContain('id=\"edit-step-cc\"');"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/vehicle.spec.js",
                            "column": 18,
                            "line": 35
                          },
                          "message": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoContain\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m) // indexOf\u001b[22m\n\nExpected substring: \u001b[32m\"id=\\\"modal-edit-entry\\\"\"\u001b[39m\nReceived string:    \u001b[31m\"<!DOCTYPE HTML PUBLIC \\\"-//W3C//DTD HTML 4.01//EN\\\" \\\"http://www.w3.org/TR/html4/strict.dtd\\\">\u001b[39m\n\u001b[31m<html><head>\u001b[39m\n\u001b[31m<title>404 Not Found</title>\u001b[39m\n\u001b[31m</head><body>\u001b[39m\n\u001b[31m<h1>Not Found</h1>\u001b[39m\n\u001b[31m<p>The requested URL was not found on this server.</p>\u001b[39m\n\u001b[31m</body></html>\u001b[39m\n\u001b[31m\"\u001b[39m\n\n  33 |     const html = await resp.text();\n  34 |\n> 35 |     expect(html).toContain('id=\"modal-edit-entry\"');\n     |                  ^\n  36 |     expect(html).toContain('id=\"edit-step-form\"');\n  37 |     expect(html).toContain('id=\"edit-step-pin\"');\n  38 |     expect(html).toContain('id=\"edit-step-cc\"');\n    at /var/www/360lm/tests/vehicle.spec.js:35:18"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 0,
                      "startTime": "2026-07-12T13:17:20.870Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/vehicle-Vehicle-Log-—-HTML-f92da-and-edit-log-screen-present-android-chrome/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/vehicle.spec.js",
                        "column": 18,
                        "line": 35
                      }
                    },
                    {
                      "workerIndex": 161,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 56,
                      "error": {
                        "message": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoContain\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m) // indexOf\u001b[22m\n\nExpected substring: \u001b[32m\"id=\\\"modal-edit-entry\\\"\"\u001b[39m\nReceived string:    \u001b[31m\"<!DOCTYPE HTML PUBLIC \\\"-//W3C//DTD HTML 4.01//EN\\\" \\\"http://www.w3.org/TR/html4/strict.dtd\\\">\u001b[39m\n\u001b[31m<html><head>\u001b[39m\n\u001b[31m<title>404 Not Found</title>\u001b[39m\n\u001b[31m</head><body>\u001b[39m\n\u001b[31m<h1>Not Found</h1>\u001b[39m\n\u001b[31m<p>The requested URL was not found on this server.</p>\u001b[39m\n\u001b[31m</body></html>\u001b[39m\n\u001b[31m\"\u001b[39m",
                        "stack": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoContain\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m) // indexOf\u001b[22m\n\nExpected substring: \u001b[32m\"id=\\\"modal-edit-entry\\\"\"\u001b[39m\nReceived string:    \u001b[31m\"<!DOCTYPE HTML PUBLIC \\\"-//W3C//DTD HTML 4.01//EN\\\" \\\"http://www.w3.org/TR/html4/strict.dtd\\\">\u001b[39m\n\u001b[31m<html><head>\u001b[39m\n\u001b[31m<title>404 Not Found</title>\u001b[39m\n\u001b[31m</head><body>\u001b[39m\n\u001b[31m<h1>Not Found</h1>\u001b[39m\n\u001b[31m<p>The requested URL was not found on this server.</p>\u001b[39m\n\u001b[31m</body></html>\u001b[39m\n\u001b[31m\"\u001b[39m\n    at /var/www/360lm/tests/vehicle.spec.js:35:18",
                        "location": {
                          "file": "/var/www/360lm/tests/vehicle.spec.js",
                          "column": 18,
                          "line": 35
                        },
                        "snippet": "  33 |     const html = await resp.text();\n  34 |\n> 35 |     expect(html).toContain('id=\"modal-edit-entry\"');\n     |                  ^\n  36 |     expect(html).toContain('id=\"edit-step-form\"');\n  37 |     expect(html).toContain('id=\"edit-step-pin\"');\n  38 |     expect(html).toContain('id=\"edit-step-cc\"');"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/vehicle.spec.js",
                            "column": 18,
                            "line": 35
                          },
                          "message": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoContain\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m) // indexOf\u001b[22m\n\nExpected substring: \u001b[32m\"id=\\\"modal-edit-entry\\\"\"\u001b[39m\nReceived string:    \u001b[31m\"<!DOCTYPE HTML PUBLIC \\\"-//W3C//DTD HTML 4.01//EN\\\" \\\"http://www.w3.org/TR/html4/strict.dtd\\\">\u001b[39m\n\u001b[31m<html><head>\u001b[39m\n\u001b[31m<title>404 Not Found</title>\u001b[39m\n\u001b[31m</head><body>\u001b[39m\n\u001b[31m<h1>Not Found</h1>\u001b[39m\n\u001b[31m<p>The requested URL was not found on this server.</p>\u001b[39m\n\u001b[31m</body></html>\u001b[39m\n\u001b[31m\"\u001b[39m\n\n  33 |     const html = await resp.text();\n  34 |\n> 35 |     expect(html).toContain('id=\"modal-edit-entry\"');\n     |                  ^\n  36 |     expect(html).toContain('id=\"edit-step-form\"');\n  37 |     expect(html).toContain('id=\"edit-step-pin\"');\n  38 |     expect(html).toContain('id=\"edit-step-cc\"');\n    at /var/www/360lm/tests/vehicle.spec.js:35:18"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 1,
                      "startTime": "2026-07-12T13:17:21.846Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/vehicle-Vehicle-Log-—-HTML-f92da-and-edit-log-screen-present-android-chrome-retry1/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/vehicle.spec.js",
                        "column": 18,
                        "line": 35
                      }
                    }
                  ],
                  "status": "unexpected"
                }
              ],
              "id": "084e7535606ab6e45f08-61ff6582f0f0b6314fd2",
              "file": "vehicle.spec.js",
              "line": 30,
              "column": 3
            },
            {
              "title": "canBypassPhoto and canEditEntry functions present in JS",
              "ok": false,
              "tags": [],
              "tests": [
                {
                  "timeout": 30000,
                  "annotations": [],
                  "expectedStatus": "passed",
                  "projectId": "android-chrome",
                  "projectName": "android-chrome",
                  "results": [
                    {
                      "workerIndex": 162,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 96,
                      "error": {
                        "message": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoContain\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m) // indexOf\u001b[22m\n\nExpected substring: \u001b[32m\"function canBypassPhoto\"\u001b[39m\nReceived string:    \u001b[31m\"<!DOCTYPE HTML PUBLIC \\\"-//W3C//DTD HTML 4.01//EN\\\" \\\"http://www.w3.org/TR/html4/strict.dtd\\\">\u001b[39m\n\u001b[31m<html><head>\u001b[39m\n\u001b[31m<title>404 Not Found</title>\u001b[39m\n\u001b[31m</head><body>\u001b[39m\n\u001b[31m<h1>Not Found</h1>\u001b[39m\n\u001b[31m<p>The requested URL was not found on this server.</p>\u001b[39m\n\u001b[31m</body></html>\u001b[39m\n\u001b[31m\"\u001b[39m",
                        "stack": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoContain\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m) // indexOf\u001b[22m\n\nExpected substring: \u001b[32m\"function canBypassPhoto\"\u001b[39m\nReceived string:    \u001b[31m\"<!DOCTYPE HTML PUBLIC \\\"-//W3C//DTD HTML 4.01//EN\\\" \\\"http://www.w3.org/TR/html4/strict.dtd\\\">\u001b[39m\n\u001b[31m<html><head>\u001b[39m\n\u001b[31m<title>404 Not Found</title>\u001b[39m\n\u001b[31m</head><body>\u001b[39m\n\u001b[31m<h1>Not Found</h1>\u001b[39m\n\u001b[31m<p>The requested URL was not found on this server.</p>\u001b[39m\n\u001b[31m</body></html>\u001b[39m\n\u001b[31m\"\u001b[39m\n    at /var/www/360lm/tests/vehicle.spec.js:55:18",
                        "location": {
                          "file": "/var/www/360lm/tests/vehicle.spec.js",
                          "column": 18,
                          "line": 55
                        },
                        "snippet": "  53 |     const html = await resp.text();\n  54 |\n> 55 |     expect(html).toContain('function canBypassPhoto');\n     |                  ^\n  56 |     expect(html).toContain('function canEditEntry');\n  57 |     expect(html).toContain('function loadUserPermissions');\n  58 |     expect(html).toContain('function openDelegateModal');"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/vehicle.spec.js",
                            "column": 18,
                            "line": 55
                          },
                          "message": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoContain\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m) // indexOf\u001b[22m\n\nExpected substring: \u001b[32m\"function canBypassPhoto\"\u001b[39m\nReceived string:    \u001b[31m\"<!DOCTYPE HTML PUBLIC \\\"-//W3C//DTD HTML 4.01//EN\\\" \\\"http://www.w3.org/TR/html4/strict.dtd\\\">\u001b[39m\n\u001b[31m<html><head>\u001b[39m\n\u001b[31m<title>404 Not Found</title>\u001b[39m\n\u001b[31m</head><body>\u001b[39m\n\u001b[31m<h1>Not Found</h1>\u001b[39m\n\u001b[31m<p>The requested URL was not found on this server.</p>\u001b[39m\n\u001b[31m</body></html>\u001b[39m\n\u001b[31m\"\u001b[39m\n\n  53 |     const html = await resp.text();\n  54 |\n> 55 |     expect(html).toContain('function canBypassPhoto');\n     |                  ^\n  56 |     expect(html).toContain('function canEditEntry');\n  57 |     expect(html).toContain('function loadUserPermissions');\n  58 |     expect(html).toContain('function openDelegateModal');\n    at /var/www/360lm/tests/vehicle.spec.js:55:18"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 0,
                      "startTime": "2026-07-12T13:17:22.897Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/vehicle-Vehicle-Log-—-HTML-be541-try-functions-present-in-JS-android-chrome/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/vehicle.spec.js",
                        "column": 18,
                        "line": 55
                      }
                    },
                    {
                      "workerIndex": 163,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 70,
                      "error": {
                        "message": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoContain\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m) // indexOf\u001b[22m\n\nExpected substring: \u001b[32m\"function canBypassPhoto\"\u001b[39m\nReceived string:    \u001b[31m\"<!DOCTYPE HTML PUBLIC \\\"-//W3C//DTD HTML 4.01//EN\\\" \\\"http://www.w3.org/TR/html4/strict.dtd\\\">\u001b[39m\n\u001b[31m<html><head>\u001b[39m\n\u001b[31m<title>404 Not Found</title>\u001b[39m\n\u001b[31m</head><body>\u001b[39m\n\u001b[31m<h1>Not Found</h1>\u001b[39m\n\u001b[31m<p>The requested URL was not found on this server.</p>\u001b[39m\n\u001b[31m</body></html>\u001b[39m\n\u001b[31m\"\u001b[39m",
                        "stack": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoContain\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m) // indexOf\u001b[22m\n\nExpected substring: \u001b[32m\"function canBypassPhoto\"\u001b[39m\nReceived string:    \u001b[31m\"<!DOCTYPE HTML PUBLIC \\\"-//W3C//DTD HTML 4.01//EN\\\" \\\"http://www.w3.org/TR/html4/strict.dtd\\\">\u001b[39m\n\u001b[31m<html><head>\u001b[39m\n\u001b[31m<title>404 Not Found</title>\u001b[39m\n\u001b[31m</head><body>\u001b[39m\n\u001b[31m<h1>Not Found</h1>\u001b[39m\n\u001b[31m<p>The requested URL was not found on this server.</p>\u001b[39m\n\u001b[31m</body></html>\u001b[39m\n\u001b[31m\"\u001b[39m\n    at /var/www/360lm/tests/vehicle.spec.js:55:18",
                        "location": {
                          "file": "/var/www/360lm/tests/vehicle.spec.js",
                          "column": 18,
                          "line": 55
                        },
                        "snippet": "  53 |     const html = await resp.text();\n  54 |\n> 55 |     expect(html).toContain('function canBypassPhoto');\n     |                  ^\n  56 |     expect(html).toContain('function canEditEntry');\n  57 |     expect(html).toContain('function loadUserPermissions');\n  58 |     expect(html).toContain('function openDelegateModal');"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/vehicle.spec.js",
                            "column": 18,
                            "line": 55
                          },
                          "message": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoContain\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m) // indexOf\u001b[22m\n\nExpected substring: \u001b[32m\"function canBypassPhoto\"\u001b[39m\nReceived string:    \u001b[31m\"<!DOCTYPE HTML PUBLIC \\\"-//W3C//DTD HTML 4.01//EN\\\" \\\"http://www.w3.org/TR/html4/strict.dtd\\\">\u001b[39m\n\u001b[31m<html><head>\u001b[39m\n\u001b[31m<title>404 Not Found</title>\u001b[39m\n\u001b[31m</head><body>\u001b[39m\n\u001b[31m<h1>Not Found</h1>\u001b[39m\n\u001b[31m<p>The requested URL was not found on this server.</p>\u001b[39m\n\u001b[31m</body></html>\u001b[39m\n\u001b[31m\"\u001b[39m\n\n  53 |     const html = await resp.text();\n  54 |\n> 55 |     expect(html).toContain('function canBypassPhoto');\n     |                  ^\n  56 |     expect(html).toContain('function canEditEntry');\n  57 |     expect(html).toContain('function loadUserPermissions');\n  58 |     expect(html).toContain('function openDelegateModal');\n    at /var/www/360lm/tests/vehicle.spec.js:55:18"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 1,
                      "startTime": "2026-07-12T13:17:23.975Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/vehicle-Vehicle-Log-—-HTML-be541-try-functions-present-in-JS-android-chrome-retry1/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/vehicle.spec.js",
                        "column": 18,
                        "line": 55
                      }
                    }
                  ],
                  "status": "unexpected"
                }
              ],
              "id": "084e7535606ab6e45f08-e3848d43d66a83c9c8de",
              "file": "vehicle.spec.js",
              "line": 50,
              "column": 3
            },
            {
              "title": "photo bypass guards present (4 locations)",
              "ok": false,
              "tags": [],
              "tests": [
                {
                  "timeout": 30000,
                  "annotations": [],
                  "expectedStatus": "passed",
                  "projectId": "android-chrome",
                  "projectName": "android-chrome",
                  "results": [
                    {
                      "workerIndex": 164,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 60,
                      "error": {
                        "message": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoBeGreaterThanOrEqual\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m)\u001b[22m\n\nExpected: >= \u001b[32m5\u001b[39m\nReceived:    \u001b[31m0\u001b[39m",
                        "stack": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoBeGreaterThanOrEqual\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m)\u001b[22m\n\nExpected: >= \u001b[32m5\u001b[39m\nReceived:    \u001b[31m0\u001b[39m\n    at /var/www/360lm/tests/vehicle.spec.js:70:21",
                        "location": {
                          "file": "/var/www/360lm/tests/vehicle.spec.js",
                          "column": 21,
                          "line": 70
                        },
                        "snippet": "  68 |     const matches = (html.match(/canBypassPhoto\\(\\)/g) || []).length;\n  69 |     // 4 validation points + 1 function definition = at least 5 occurrences\n> 70 |     expect(matches).toBeGreaterThanOrEqual(5);\n     |                     ^\n  71 |\n  72 |     await ctx.dispose();\n  73 |   });"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/vehicle.spec.js",
                            "column": 21,
                            "line": 70
                          },
                          "message": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoBeGreaterThanOrEqual\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m)\u001b[22m\n\nExpected: >= \u001b[32m5\u001b[39m\nReceived:    \u001b[31m0\u001b[39m\n\n  68 |     const matches = (html.match(/canBypassPhoto\\(\\)/g) || []).length;\n  69 |     // 4 validation points + 1 function definition = at least 5 occurrences\n> 70 |     expect(matches).toBeGreaterThanOrEqual(5);\n     |                     ^\n  71 |\n  72 |     await ctx.dispose();\n  73 |   });\n    at /var/www/360lm/tests/vehicle.spec.js:70:21"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 0,
                      "startTime": "2026-07-12T13:17:24.883Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/vehicle-Vehicle-Log-—-HTML-56323-guards-present-4-locations--android-chrome/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/vehicle.spec.js",
                        "column": 21,
                        "line": 70
                      }
                    },
                    {
                      "workerIndex": 165,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 81,
                      "error": {
                        "message": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoBeGreaterThanOrEqual\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m)\u001b[22m\n\nExpected: >= \u001b[32m5\u001b[39m\nReceived:    \u001b[31m0\u001b[39m",
                        "stack": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoBeGreaterThanOrEqual\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m)\u001b[22m\n\nExpected: >= \u001b[32m5\u001b[39m\nReceived:    \u001b[31m0\u001b[39m\n    at /var/www/360lm/tests/vehicle.spec.js:70:21",
                        "location": {
                          "file": "/var/www/360lm/tests/vehicle.spec.js",
                          "column": 21,
                          "line": 70
                        },
                        "snippet": "  68 |     const matches = (html.match(/canBypassPhoto\\(\\)/g) || []).length;\n  69 |     // 4 validation points + 1 function definition = at least 5 occurrences\n> 70 |     expect(matches).toBeGreaterThanOrEqual(5);\n     |                     ^\n  71 |\n  72 |     await ctx.dispose();\n  73 |   });"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/vehicle.spec.js",
                            "column": 21,
                            "line": 70
                          },
                          "message": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoBeGreaterThanOrEqual\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m)\u001b[22m\n\nExpected: >= \u001b[32m5\u001b[39m\nReceived:    \u001b[31m0\u001b[39m\n\n  68 |     const matches = (html.match(/canBypassPhoto\\(\\)/g) || []).length;\n  69 |     // 4 validation points + 1 function definition = at least 5 occurrences\n> 70 |     expect(matches).toBeGreaterThanOrEqual(5);\n     |                     ^\n  71 |\n  72 |     await ctx.dispose();\n  73 |   });\n    at /var/www/360lm/tests/vehicle.spec.js:70:21"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 1,
                      "startTime": "2026-07-12T13:17:25.802Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/vehicle-Vehicle-Log-—-HTML-56323-guards-present-4-locations--android-chrome-retry1/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/vehicle.spec.js",
                        "column": 21,
                        "line": 70
                      }
                    }
                  ],
                  "status": "unexpected"
                }
              ],
              "id": "084e7535606ab6e45f08-9b6dda5ff14592fbe0ed",
              "file": "vehicle.spec.js",
              "line": 64,
              "column": 3
            },
            {
              "title": "edit+delegate modals and edit-log screen present",
              "ok": false,
              "tags": [],
              "tests": [
                {
                  "timeout": 30000,
                  "annotations": [],
                  "expectedStatus": "passed",
                  "projectId": "desktop-chrome",
                  "projectName": "desktop-chrome",
                  "results": [
                    {
                      "workerIndex": 336,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 51,
                      "error": {
                        "message": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoContain\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m) // indexOf\u001b[22m\n\nExpected substring: \u001b[32m\"id=\\\"modal-edit-entry\\\"\"\u001b[39m\nReceived string:    \u001b[31m\"<!DOCTYPE HTML PUBLIC \\\"-//W3C//DTD HTML 4.01//EN\\\" \\\"http://www.w3.org/TR/html4/strict.dtd\\\">\u001b[39m\n\u001b[31m<html><head>\u001b[39m\n\u001b[31m<title>404 Not Found</title>\u001b[39m\n\u001b[31m</head><body>\u001b[39m\n\u001b[31m<h1>Not Found</h1>\u001b[39m\n\u001b[31m<p>The requested URL was not found on this server.</p>\u001b[39m\n\u001b[31m</body></html>\u001b[39m\n\u001b[31m\"\u001b[39m",
                        "stack": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoContain\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m) // indexOf\u001b[22m\n\nExpected substring: \u001b[32m\"id=\\\"modal-edit-entry\\\"\"\u001b[39m\nReceived string:    \u001b[31m\"<!DOCTYPE HTML PUBLIC \\\"-//W3C//DTD HTML 4.01//EN\\\" \\\"http://www.w3.org/TR/html4/strict.dtd\\\">\u001b[39m\n\u001b[31m<html><head>\u001b[39m\n\u001b[31m<title>404 Not Found</title>\u001b[39m\n\u001b[31m</head><body>\u001b[39m\n\u001b[31m<h1>Not Found</h1>\u001b[39m\n\u001b[31m<p>The requested URL was not found on this server.</p>\u001b[39m\n\u001b[31m</body></html>\u001b[39m\n\u001b[31m\"\u001b[39m\n    at /var/www/360lm/tests/vehicle.spec.js:35:18",
                        "location": {
                          "file": "/var/www/360lm/tests/vehicle.spec.js",
                          "column": 18,
                          "line": 35
                        },
                        "snippet": "  33 |     const html = await resp.text();\n  34 |\n> 35 |     expect(html).toContain('id=\"modal-edit-entry\"');\n     |                  ^\n  36 |     expect(html).toContain('id=\"edit-step-form\"');\n  37 |     expect(html).toContain('id=\"edit-step-pin\"');\n  38 |     expect(html).toContain('id=\"edit-step-cc\"');"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/vehicle.spec.js",
                            "column": 18,
                            "line": 35
                          },
                          "message": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoContain\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m) // indexOf\u001b[22m\n\nExpected substring: \u001b[32m\"id=\\\"modal-edit-entry\\\"\"\u001b[39m\nReceived string:    \u001b[31m\"<!DOCTYPE HTML PUBLIC \\\"-//W3C//DTD HTML 4.01//EN\\\" \\\"http://www.w3.org/TR/html4/strict.dtd\\\">\u001b[39m\n\u001b[31m<html><head>\u001b[39m\n\u001b[31m<title>404 Not Found</title>\u001b[39m\n\u001b[31m</head><body>\u001b[39m\n\u001b[31m<h1>Not Found</h1>\u001b[39m\n\u001b[31m<p>The requested URL was not found on this server.</p>\u001b[39m\n\u001b[31m</body></html>\u001b[39m\n\u001b[31m\"\u001b[39m\n\n  33 |     const html = await resp.text();\n  34 |\n> 35 |     expect(html).toContain('id=\"modal-edit-entry\"');\n     |                  ^\n  36 |     expect(html).toContain('id=\"edit-step-form\"');\n  37 |     expect(html).toContain('id=\"edit-step-pin\"');\n  38 |     expect(html).toContain('id=\"edit-step-cc\"');\n    at /var/www/360lm/tests/vehicle.spec.js:35:18"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 0,
                      "startTime": "2026-07-12T13:37:22.153Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/vehicle-Vehicle-Log-—-HTML-f92da-and-edit-log-screen-present-desktop-chrome/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/vehicle.spec.js",
                        "column": 18,
                        "line": 35
                      }
                    },
                    {
                      "workerIndex": 337,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 55,
                      "error": {
                        "message": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoContain\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m) // indexOf\u001b[22m\n\nExpected substring: \u001b[32m\"id=\\\"modal-edit-entry\\\"\"\u001b[39m\nReceived string:    \u001b[31m\"<!DOCTYPE HTML PUBLIC \\\"-//W3C//DTD HTML 4.01//EN\\\" \\\"http://www.w3.org/TR/html4/strict.dtd\\\">\u001b[39m\n\u001b[31m<html><head>\u001b[39m\n\u001b[31m<title>404 Not Found</title>\u001b[39m\n\u001b[31m</head><body>\u001b[39m\n\u001b[31m<h1>Not Found</h1>\u001b[39m\n\u001b[31m<p>The requested URL was not found on this server.</p>\u001b[39m\n\u001b[31m</body></html>\u001b[39m\n\u001b[31m\"\u001b[39m",
                        "stack": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoContain\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m) // indexOf\u001b[22m\n\nExpected substring: \u001b[32m\"id=\\\"modal-edit-entry\\\"\"\u001b[39m\nReceived string:    \u001b[31m\"<!DOCTYPE HTML PUBLIC \\\"-//W3C//DTD HTML 4.01//EN\\\" \\\"http://www.w3.org/TR/html4/strict.dtd\\\">\u001b[39m\n\u001b[31m<html><head>\u001b[39m\n\u001b[31m<title>404 Not Found</title>\u001b[39m\n\u001b[31m</head><body>\u001b[39m\n\u001b[31m<h1>Not Found</h1>\u001b[39m\n\u001b[31m<p>The requested URL was not found on this server.</p>\u001b[39m\n\u001b[31m</body></html>\u001b[39m\n\u001b[31m\"\u001b[39m\n    at /var/www/360lm/tests/vehicle.spec.js:35:18",
                        "location": {
                          "file": "/var/www/360lm/tests/vehicle.spec.js",
                          "column": 18,
                          "line": 35
                        },
                        "snippet": "  33 |     const html = await resp.text();\n  34 |\n> 35 |     expect(html).toContain('id=\"modal-edit-entry\"');\n     |                  ^\n  36 |     expect(html).toContain('id=\"edit-step-form\"');\n  37 |     expect(html).toContain('id=\"edit-step-pin\"');\n  38 |     expect(html).toContain('id=\"edit-step-cc\"');"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/vehicle.spec.js",
                            "column": 18,
                            "line": 35
                          },
                          "message": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoContain\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m) // indexOf\u001b[22m\n\nExpected substring: \u001b[32m\"id=\\\"modal-edit-entry\\\"\"\u001b[39m\nReceived string:    \u001b[31m\"<!DOCTYPE HTML PUBLIC \\\"-//W3C//DTD HTML 4.01//EN\\\" \\\"http://www.w3.org/TR/html4/strict.dtd\\\">\u001b[39m\n\u001b[31m<html><head>\u001b[39m\n\u001b[31m<title>404 Not Found</title>\u001b[39m\n\u001b[31m</head><body>\u001b[39m\n\u001b[31m<h1>Not Found</h1>\u001b[39m\n\u001b[31m<p>The requested URL was not found on this server.</p>\u001b[39m\n\u001b[31m</body></html>\u001b[39m\n\u001b[31m\"\u001b[39m\n\n  33 |     const html = await resp.text();\n  34 |\n> 35 |     expect(html).toContain('id=\"modal-edit-entry\"');\n     |                  ^\n  36 |     expect(html).toContain('id=\"edit-step-form\"');\n  37 |     expect(html).toContain('id=\"edit-step-pin\"');\n  38 |     expect(html).toContain('id=\"edit-step-cc\"');\n    at /var/www/360lm/tests/vehicle.spec.js:35:18"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 1,
                      "startTime": "2026-07-12T13:37:23.074Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/vehicle-Vehicle-Log-—-HTML-f92da-and-edit-log-screen-present-desktop-chrome-retry1/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/vehicle.spec.js",
                        "column": 18,
                        "line": 35
                      }
                    }
                  ],
                  "status": "unexpected"
                }
              ],
              "id": "084e7535606ab6e45f08-eecfce8c5bbdcfc30b12",
              "file": "vehicle.spec.js",
              "line": 30,
              "column": 3
            },
            {
              "title": "canBypassPhoto and canEditEntry functions present in JS",
              "ok": false,
              "tags": [],
              "tests": [
                {
                  "timeout": 30000,
                  "annotations": [],
                  "expectedStatus": "passed",
                  "projectId": "desktop-chrome",
                  "projectName": "desktop-chrome",
                  "results": [
                    {
                      "workerIndex": 338,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 62,
                      "error": {
                        "message": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoContain\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m) // indexOf\u001b[22m\n\nExpected substring: \u001b[32m\"function canBypassPhoto\"\u001b[39m\nReceived string:    \u001b[31m\"<!DOCTYPE HTML PUBLIC \\\"-//W3C//DTD HTML 4.01//EN\\\" \\\"http://www.w3.org/TR/html4/strict.dtd\\\">\u001b[39m\n\u001b[31m<html><head>\u001b[39m\n\u001b[31m<title>404 Not Found</title>\u001b[39m\n\u001b[31m</head><body>\u001b[39m\n\u001b[31m<h1>Not Found</h1>\u001b[39m\n\u001b[31m<p>The requested URL was not found on this server.</p>\u001b[39m\n\u001b[31m</body></html>\u001b[39m\n\u001b[31m\"\u001b[39m",
                        "stack": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoContain\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m) // indexOf\u001b[22m\n\nExpected substring: \u001b[32m\"function canBypassPhoto\"\u001b[39m\nReceived string:    \u001b[31m\"<!DOCTYPE HTML PUBLIC \\\"-//W3C//DTD HTML 4.01//EN\\\" \\\"http://www.w3.org/TR/html4/strict.dtd\\\">\u001b[39m\n\u001b[31m<html><head>\u001b[39m\n\u001b[31m<title>404 Not Found</title>\u001b[39m\n\u001b[31m</head><body>\u001b[39m\n\u001b[31m<h1>Not Found</h1>\u001b[39m\n\u001b[31m<p>The requested URL was not found on this server.</p>\u001b[39m\n\u001b[31m</body></html>\u001b[39m\n\u001b[31m\"\u001b[39m\n    at /var/www/360lm/tests/vehicle.spec.js:55:18",
                        "location": {
                          "file": "/var/www/360lm/tests/vehicle.spec.js",
                          "column": 18,
                          "line": 55
                        },
                        "snippet": "  53 |     const html = await resp.text();\n  54 |\n> 55 |     expect(html).toContain('function canBypassPhoto');\n     |                  ^\n  56 |     expect(html).toContain('function canEditEntry');\n  57 |     expect(html).toContain('function loadUserPermissions');\n  58 |     expect(html).toContain('function openDelegateModal');"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/vehicle.spec.js",
                            "column": 18,
                            "line": 55
                          },
                          "message": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoContain\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m) // indexOf\u001b[22m\n\nExpected substring: \u001b[32m\"function canBypassPhoto\"\u001b[39m\nReceived string:    \u001b[31m\"<!DOCTYPE HTML PUBLIC \\\"-//W3C//DTD HTML 4.01//EN\\\" \\\"http://www.w3.org/TR/html4/strict.dtd\\\">\u001b[39m\n\u001b[31m<html><head>\u001b[39m\n\u001b[31m<title>404 Not Found</title>\u001b[39m\n\u001b[31m</head><body>\u001b[39m\n\u001b[31m<h1>Not Found</h1>\u001b[39m\n\u001b[31m<p>The requested URL was not found on this server.</p>\u001b[39m\n\u001b[31m</body></html>\u001b[39m\n\u001b[31m\"\u001b[39m\n\n  53 |     const html = await resp.text();\n  54 |\n> 55 |     expect(html).toContain('function canBypassPhoto');\n     |                  ^\n  56 |     expect(html).toContain('function canEditEntry');\n  57 |     expect(html).toContain('function loadUserPermissions');\n  58 |     expect(html).toContain('function openDelegateModal');\n    at /var/www/360lm/tests/vehicle.spec.js:55:18"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 0,
                      "startTime": "2026-07-12T13:37:23.888Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/vehicle-Vehicle-Log-—-HTML-be541-try-functions-present-in-JS-desktop-chrome/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/vehicle.spec.js",
                        "column": 18,
                        "line": 55
                      }
                    },
                    {
                      "workerIndex": 339,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 83,
                      "error": {
                        "message": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoContain\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m) // indexOf\u001b[22m\n\nExpected substring: \u001b[32m\"function canBypassPhoto\"\u001b[39m\nReceived string:    \u001b[31m\"<!DOCTYPE HTML PUBLIC \\\"-//W3C//DTD HTML 4.01//EN\\\" \\\"http://www.w3.org/TR/html4/strict.dtd\\\">\u001b[39m\n\u001b[31m<html><head>\u001b[39m\n\u001b[31m<title>404 Not Found</title>\u001b[39m\n\u001b[31m</head><body>\u001b[39m\n\u001b[31m<h1>Not Found</h1>\u001b[39m\n\u001b[31m<p>The requested URL was not found on this server.</p>\u001b[39m\n\u001b[31m</body></html>\u001b[39m\n\u001b[31m\"\u001b[39m",
                        "stack": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoContain\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m) // indexOf\u001b[22m\n\nExpected substring: \u001b[32m\"function canBypassPhoto\"\u001b[39m\nReceived string:    \u001b[31m\"<!DOCTYPE HTML PUBLIC \\\"-//W3C//DTD HTML 4.01//EN\\\" \\\"http://www.w3.org/TR/html4/strict.dtd\\\">\u001b[39m\n\u001b[31m<html><head>\u001b[39m\n\u001b[31m<title>404 Not Found</title>\u001b[39m\n\u001b[31m</head><body>\u001b[39m\n\u001b[31m<h1>Not Found</h1>\u001b[39m\n\u001b[31m<p>The requested URL was not found on this server.</p>\u001b[39m\n\u001b[31m</body></html>\u001b[39m\n\u001b[31m\"\u001b[39m\n    at /var/www/360lm/tests/vehicle.spec.js:55:18",
                        "location": {
                          "file": "/var/www/360lm/tests/vehicle.spec.js",
                          "column": 18,
                          "line": 55
                        },
                        "snippet": "  53 |     const html = await resp.text();\n  54 |\n> 55 |     expect(html).toContain('function canBypassPhoto');\n     |                  ^\n  56 |     expect(html).toContain('function canEditEntry');\n  57 |     expect(html).toContain('function loadUserPermissions');\n  58 |     expect(html).toContain('function openDelegateModal');"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/vehicle.spec.js",
                            "column": 18,
                            "line": 55
                          },
                          "message": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoContain\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m) // indexOf\u001b[22m\n\nExpected substring: \u001b[32m\"function canBypassPhoto\"\u001b[39m\nReceived string:    \u001b[31m\"<!DOCTYPE HTML PUBLIC \\\"-//W3C//DTD HTML 4.01//EN\\\" \\\"http://www.w3.org/TR/html4/strict.dtd\\\">\u001b[39m\n\u001b[31m<html><head>\u001b[39m\n\u001b[31m<title>404 Not Found</title>\u001b[39m\n\u001b[31m</head><body>\u001b[39m\n\u001b[31m<h1>Not Found</h1>\u001b[39m\n\u001b[31m<p>The requested URL was not found on this server.</p>\u001b[39m\n\u001b[31m</body></html>\u001b[39m\n\u001b[31m\"\u001b[39m\n\n  53 |     const html = await resp.text();\n  54 |\n> 55 |     expect(html).toContain('function canBypassPhoto');\n     |                  ^\n  56 |     expect(html).toContain('function canEditEntry');\n  57 |     expect(html).toContain('function loadUserPermissions');\n  58 |     expect(html).toContain('function openDelegateModal');\n    at /var/www/360lm/tests/vehicle.spec.js:55:18"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 1,
                      "startTime": "2026-07-12T13:37:24.775Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/vehicle-Vehicle-Log-—-HTML-be541-try-functions-present-in-JS-desktop-chrome-retry1/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/vehicle.spec.js",
                        "column": 18,
                        "line": 55
                      }
                    }
                  ],
                  "status": "unexpected"
                }
              ],
              "id": "084e7535606ab6e45f08-1da678b2208dbba484d5",
              "file": "vehicle.spec.js",
              "line": 50,
              "column": 3
            },
            {
              "title": "photo bypass guards present (4 locations)",
              "ok": false,
              "tags": [],
              "tests": [
                {
                  "timeout": 30000,
                  "annotations": [],
                  "expectedStatus": "passed",
                  "projectId": "desktop-chrome",
                  "projectName": "desktop-chrome",
                  "results": [
                    {
                      "workerIndex": 340,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 64,
                      "error": {
                        "message": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoBeGreaterThanOrEqual\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m)\u001b[22m\n\nExpected: >= \u001b[32m5\u001b[39m\nReceived:    \u001b[31m0\u001b[39m",
                        "stack": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoBeGreaterThanOrEqual\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m)\u001b[22m\n\nExpected: >= \u001b[32m5\u001b[39m\nReceived:    \u001b[31m0\u001b[39m\n    at /var/www/360lm/tests/vehicle.spec.js:70:21",
                        "location": {
                          "file": "/var/www/360lm/tests/vehicle.spec.js",
                          "column": 21,
                          "line": 70
                        },
                        "snippet": "  68 |     const matches = (html.match(/canBypassPhoto\\(\\)/g) || []).length;\n  69 |     // 4 validation points + 1 function definition = at least 5 occurrences\n> 70 |     expect(matches).toBeGreaterThanOrEqual(5);\n     |                     ^\n  71 |\n  72 |     await ctx.dispose();\n  73 |   });"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/vehicle.spec.js",
                            "column": 21,
                            "line": 70
                          },
                          "message": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoBeGreaterThanOrEqual\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m)\u001b[22m\n\nExpected: >= \u001b[32m5\u001b[39m\nReceived:    \u001b[31m0\u001b[39m\n\n  68 |     const matches = (html.match(/canBypassPhoto\\(\\)/g) || []).length;\n  69 |     // 4 validation points + 1 function definition = at least 5 occurrences\n> 70 |     expect(matches).toBeGreaterThanOrEqual(5);\n     |                     ^\n  71 |\n  72 |     await ctx.dispose();\n  73 |   });\n    at /var/www/360lm/tests/vehicle.spec.js:70:21"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 0,
                      "startTime": "2026-07-12T13:37:25.651Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/vehicle-Vehicle-Log-—-HTML-56323-guards-present-4-locations--desktop-chrome/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/vehicle.spec.js",
                        "column": 21,
                        "line": 70
                      }
                    },
                    {
                      "workerIndex": 341,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 53,
                      "error": {
                        "message": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoBeGreaterThanOrEqual\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m)\u001b[22m\n\nExpected: >= \u001b[32m5\u001b[39m\nReceived:    \u001b[31m0\u001b[39m",
                        "stack": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoBeGreaterThanOrEqual\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m)\u001b[22m\n\nExpected: >= \u001b[32m5\u001b[39m\nReceived:    \u001b[31m0\u001b[39m\n    at /var/www/360lm/tests/vehicle.spec.js:70:21",
                        "location": {
                          "file": "/var/www/360lm/tests/vehicle.spec.js",
                          "column": 21,
                          "line": 70
                        },
                        "snippet": "  68 |     const matches = (html.match(/canBypassPhoto\\(\\)/g) || []).length;\n  69 |     // 4 validation points + 1 function definition = at least 5 occurrences\n> 70 |     expect(matches).toBeGreaterThanOrEqual(5);\n     |                     ^\n  71 |\n  72 |     await ctx.dispose();\n  73 |   });"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/vehicle.spec.js",
                            "column": 21,
                            "line": 70
                          },
                          "message": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoBeGreaterThanOrEqual\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m)\u001b[22m\n\nExpected: >= \u001b[32m5\u001b[39m\nReceived:    \u001b[31m0\u001b[39m\n\n  68 |     const matches = (html.match(/canBypassPhoto\\(\\)/g) || []).length;\n  69 |     // 4 validation points + 1 function definition = at least 5 occurrences\n> 70 |     expect(matches).toBeGreaterThanOrEqual(5);\n     |                     ^\n  71 |\n  72 |     await ctx.dispose();\n  73 |   });\n    at /var/www/360lm/tests/vehicle.spec.js:70:21"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 1,
                      "startTime": "2026-07-12T13:37:26.674Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/vehicle-Vehicle-Log-—-HTML-56323-guards-present-4-locations--desktop-chrome-retry1/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/vehicle.spec.js",
                        "column": 21,
                        "line": 70
                      }
                    }
                  ],
                  "status": "unexpected"
                }
              ],
              "id": "084e7535606ab6e45f08-d807e8ea32c68934afb9",
              "file": "vehicle.spec.js",
              "line": 64,
              "column": 3
            }
          ]
        },
        {
          "title": "Vehicle Log — HTML Structure (v22)",
          "file": "vehicle.spec.js",
          "line": 76,
          "column": 6,
          "specs": [
            {
              "title": "mileage segment functions and retro-switch present",
              "ok": false,
              "tags": [],
              "tests": [
                {
                  "timeout": 30000,
                  "annotations": [],
                  "expectedStatus": "passed",
                  "projectId": "android-chrome",
                  "projectName": "android-chrome",
                  "results": [
                    {
                      "workerIndex": 166,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 61,
                      "error": {
                        "message": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoContain\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m) // indexOf\u001b[22m\n\nExpected substring: \u001b[32m\"function calcSegmentMileage\"\u001b[39m\nReceived string:    \u001b[31m\"<!DOCTYPE HTML PUBLIC \\\"-//W3C//DTD HTML 4.01//EN\\\" \\\"http://www.w3.org/TR/html4/strict.dtd\\\">\u001b[39m\n\u001b[31m<html><head>\u001b[39m\n\u001b[31m<title>404 Not Found</title>\u001b[39m\n\u001b[31m</head><body>\u001b[39m\n\u001b[31m<h1>Not Found</h1>\u001b[39m\n\u001b[31m<p>The requested URL was not found on this server.</p>\u001b[39m\n\u001b[31m</body></html>\u001b[39m\n\u001b[31m\"\u001b[39m",
                        "stack": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoContain\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m) // indexOf\u001b[22m\n\nExpected substring: \u001b[32m\"function calcSegmentMileage\"\u001b[39m\nReceived string:    \u001b[31m\"<!DOCTYPE HTML PUBLIC \\\"-//W3C//DTD HTML 4.01//EN\\\" \\\"http://www.w3.org/TR/html4/strict.dtd\\\">\u001b[39m\n\u001b[31m<html><head>\u001b[39m\n\u001b[31m<title>404 Not Found</title>\u001b[39m\n\u001b[31m</head><body>\u001b[39m\n\u001b[31m<h1>Not Found</h1>\u001b[39m\n\u001b[31m<p>The requested URL was not found on this server.</p>\u001b[39m\n\u001b[31m</body></html>\u001b[39m\n\u001b[31m\"\u001b[39m\n    at /var/www/360lm/tests/vehicle.spec.js:82:18",
                        "location": {
                          "file": "/var/www/360lm/tests/vehicle.spec.js",
                          "column": 18,
                          "line": 82
                        },
                        "snippet": "  80 |     const html = await resp.text();\n  81 |\n> 82 |     expect(html).toContain('function calcSegmentMileage');\n     |                  ^\n  83 |     expect(html).toContain('function applyMileage');\n  84 |     expect(html).toContain('function openRetroSwitch');\n  85 |     expect(html).toContain('function saveRetroSwitch');"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/vehicle.spec.js",
                            "column": 18,
                            "line": 82
                          },
                          "message": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoContain\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m) // indexOf\u001b[22m\n\nExpected substring: \u001b[32m\"function calcSegmentMileage\"\u001b[39m\nReceived string:    \u001b[31m\"<!DOCTYPE HTML PUBLIC \\\"-//W3C//DTD HTML 4.01//EN\\\" \\\"http://www.w3.org/TR/html4/strict.dtd\\\">\u001b[39m\n\u001b[31m<html><head>\u001b[39m\n\u001b[31m<title>404 Not Found</title>\u001b[39m\n\u001b[31m</head><body>\u001b[39m\n\u001b[31m<h1>Not Found</h1>\u001b[39m\n\u001b[31m<p>The requested URL was not found on this server.</p>\u001b[39m\n\u001b[31m</body></html>\u001b[39m\n\u001b[31m\"\u001b[39m\n\n  80 |     const html = await resp.text();\n  81 |\n> 82 |     expect(html).toContain('function calcSegmentMileage');\n     |                  ^\n  83 |     expect(html).toContain('function applyMileage');\n  84 |     expect(html).toContain('function openRetroSwitch');\n  85 |     expect(html).toContain('function saveRetroSwitch');\n    at /var/www/360lm/tests/vehicle.spec.js:82:18"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 0,
                      "startTime": "2026-07-12T13:17:27.027Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/vehicle-Vehicle-Log-—-HTML-94be2-ns-and-retro-switch-present-android-chrome/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/vehicle.spec.js",
                        "column": 18,
                        "line": 82
                      }
                    },
                    {
                      "workerIndex": 167,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 60,
                      "error": {
                        "message": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoContain\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m) // indexOf\u001b[22m\n\nExpected substring: \u001b[32m\"function calcSegmentMileage\"\u001b[39m\nReceived string:    \u001b[31m\"<!DOCTYPE HTML PUBLIC \\\"-//W3C//DTD HTML 4.01//EN\\\" \\\"http://www.w3.org/TR/html4/strict.dtd\\\">\u001b[39m\n\u001b[31m<html><head>\u001b[39m\n\u001b[31m<title>404 Not Found</title>\u001b[39m\n\u001b[31m</head><body>\u001b[39m\n\u001b[31m<h1>Not Found</h1>\u001b[39m\n\u001b[31m<p>The requested URL was not found on this server.</p>\u001b[39m\n\u001b[31m</body></html>\u001b[39m\n\u001b[31m\"\u001b[39m",
                        "stack": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoContain\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m) // indexOf\u001b[22m\n\nExpected substring: \u001b[32m\"function calcSegmentMileage\"\u001b[39m\nReceived string:    \u001b[31m\"<!DOCTYPE HTML PUBLIC \\\"-//W3C//DTD HTML 4.01//EN\\\" \\\"http://www.w3.org/TR/html4/strict.dtd\\\">\u001b[39m\n\u001b[31m<html><head>\u001b[39m\n\u001b[31m<title>404 Not Found</title>\u001b[39m\n\u001b[31m</head><body>\u001b[39m\n\u001b[31m<h1>Not Found</h1>\u001b[39m\n\u001b[31m<p>The requested URL was not found on this server.</p>\u001b[39m\n\u001b[31m</body></html>\u001b[39m\n\u001b[31m\"\u001b[39m\n    at /var/www/360lm/tests/vehicle.spec.js:82:18",
                        "location": {
                          "file": "/var/www/360lm/tests/vehicle.spec.js",
                          "column": 18,
                          "line": 82
                        },
                        "snippet": "  80 |     const html = await resp.text();\n  81 |\n> 82 |     expect(html).toContain('function calcSegmentMileage');\n     |                  ^\n  83 |     expect(html).toContain('function applyMileage');\n  84 |     expect(html).toContain('function openRetroSwitch');\n  85 |     expect(html).toContain('function saveRetroSwitch');"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/vehicle.spec.js",
                            "column": 18,
                            "line": 82
                          },
                          "message": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoContain\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m) // indexOf\u001b[22m\n\nExpected substring: \u001b[32m\"function calcSegmentMileage\"\u001b[39m\nReceived string:    \u001b[31m\"<!DOCTYPE HTML PUBLIC \\\"-//W3C//DTD HTML 4.01//EN\\\" \\\"http://www.w3.org/TR/html4/strict.dtd\\\">\u001b[39m\n\u001b[31m<html><head>\u001b[39m\n\u001b[31m<title>404 Not Found</title>\u001b[39m\n\u001b[31m</head><body>\u001b[39m\n\u001b[31m<h1>Not Found</h1>\u001b[39m\n\u001b[31m<p>The requested URL was not found on this server.</p>\u001b[39m\n\u001b[31m</body></html>\u001b[39m\n\u001b[31m\"\u001b[39m\n\n  80 |     const html = await resp.text();\n  81 |\n> 82 |     expect(html).toContain('function calcSegmentMileage');\n     |                  ^\n  83 |     expect(html).toContain('function applyMileage');\n  84 |     expect(html).toContain('function openRetroSwitch');\n  85 |     expect(html).toContain('function saveRetroSwitch');\n    at /var/www/360lm/tests/vehicle.spec.js:82:18"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 1,
                      "startTime": "2026-07-12T13:17:27.998Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/vehicle-Vehicle-Log-—-HTML-94be2-ns-and-retro-switch-present-android-chrome-retry1/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/vehicle.spec.js",
                        "column": 18,
                        "line": 82
                      }
                    }
                  ],
                  "status": "unexpected"
                }
              ],
              "id": "084e7535606ab6e45f08-e81fe7704c34b9bba4d8",
              "file": "vehicle.spec.js",
              "line": 77,
              "column": 3
            },
            {
              "title": "mileage segment functions and retro-switch present",
              "ok": false,
              "tags": [],
              "tests": [
                {
                  "timeout": 30000,
                  "annotations": [],
                  "expectedStatus": "passed",
                  "projectId": "desktop-chrome",
                  "projectName": "desktop-chrome",
                  "results": [
                    {
                      "workerIndex": 342,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 58,
                      "error": {
                        "message": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoContain\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m) // indexOf\u001b[22m\n\nExpected substring: \u001b[32m\"function calcSegmentMileage\"\u001b[39m\nReceived string:    \u001b[31m\"<!DOCTYPE HTML PUBLIC \\\"-//W3C//DTD HTML 4.01//EN\\\" \\\"http://www.w3.org/TR/html4/strict.dtd\\\">\u001b[39m\n\u001b[31m<html><head>\u001b[39m\n\u001b[31m<title>404 Not Found</title>\u001b[39m\n\u001b[31m</head><body>\u001b[39m\n\u001b[31m<h1>Not Found</h1>\u001b[39m\n\u001b[31m<p>The requested URL was not found on this server.</p>\u001b[39m\n\u001b[31m</body></html>\u001b[39m\n\u001b[31m\"\u001b[39m",
                        "stack": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoContain\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m) // indexOf\u001b[22m\n\nExpected substring: \u001b[32m\"function calcSegmentMileage\"\u001b[39m\nReceived string:    \u001b[31m\"<!DOCTYPE HTML PUBLIC \\\"-//W3C//DTD HTML 4.01//EN\\\" \\\"http://www.w3.org/TR/html4/strict.dtd\\\">\u001b[39m\n\u001b[31m<html><head>\u001b[39m\n\u001b[31m<title>404 Not Found</title>\u001b[39m\n\u001b[31m</head><body>\u001b[39m\n\u001b[31m<h1>Not Found</h1>\u001b[39m\n\u001b[31m<p>The requested URL was not found on this server.</p>\u001b[39m\n\u001b[31m</body></html>\u001b[39m\n\u001b[31m\"\u001b[39m\n    at /var/www/360lm/tests/vehicle.spec.js:82:18",
                        "location": {
                          "file": "/var/www/360lm/tests/vehicle.spec.js",
                          "column": 18,
                          "line": 82
                        },
                        "snippet": "  80 |     const html = await resp.text();\n  81 |\n> 82 |     expect(html).toContain('function calcSegmentMileage');\n     |                  ^\n  83 |     expect(html).toContain('function applyMileage');\n  84 |     expect(html).toContain('function openRetroSwitch');\n  85 |     expect(html).toContain('function saveRetroSwitch');"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/vehicle.spec.js",
                            "column": 18,
                            "line": 82
                          },
                          "message": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoContain\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m) // indexOf\u001b[22m\n\nExpected substring: \u001b[32m\"function calcSegmentMileage\"\u001b[39m\nReceived string:    \u001b[31m\"<!DOCTYPE HTML PUBLIC \\\"-//W3C//DTD HTML 4.01//EN\\\" \\\"http://www.w3.org/TR/html4/strict.dtd\\\">\u001b[39m\n\u001b[31m<html><head>\u001b[39m\n\u001b[31m<title>404 Not Found</title>\u001b[39m\n\u001b[31m</head><body>\u001b[39m\n\u001b[31m<h1>Not Found</h1>\u001b[39m\n\u001b[31m<p>The requested URL was not found on this server.</p>\u001b[39m\n\u001b[31m</body></html>\u001b[39m\n\u001b[31m\"\u001b[39m\n\n  80 |     const html = await resp.text();\n  81 |\n> 82 |     expect(html).toContain('function calcSegmentMileage');\n     |                  ^\n  83 |     expect(html).toContain('function applyMileage');\n  84 |     expect(html).toContain('function openRetroSwitch');\n  85 |     expect(html).toContain('function saveRetroSwitch');\n    at /var/www/360lm/tests/vehicle.spec.js:82:18"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 0,
                      "startTime": "2026-07-12T13:37:27.575Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/vehicle-Vehicle-Log-—-HTML-94be2-ns-and-retro-switch-present-desktop-chrome/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/vehicle.spec.js",
                        "column": 18,
                        "line": 82
                      }
                    },
                    {
                      "workerIndex": 343,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 62,
                      "error": {
                        "message": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoContain\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m) // indexOf\u001b[22m\n\nExpected substring: \u001b[32m\"function calcSegmentMileage\"\u001b[39m\nReceived string:    \u001b[31m\"<!DOCTYPE HTML PUBLIC \\\"-//W3C//DTD HTML 4.01//EN\\\" \\\"http://www.w3.org/TR/html4/strict.dtd\\\">\u001b[39m\n\u001b[31m<html><head>\u001b[39m\n\u001b[31m<title>404 Not Found</title>\u001b[39m\n\u001b[31m</head><body>\u001b[39m\n\u001b[31m<h1>Not Found</h1>\u001b[39m\n\u001b[31m<p>The requested URL was not found on this server.</p>\u001b[39m\n\u001b[31m</body></html>\u001b[39m\n\u001b[31m\"\u001b[39m",
                        "stack": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoContain\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m) // indexOf\u001b[22m\n\nExpected substring: \u001b[32m\"function calcSegmentMileage\"\u001b[39m\nReceived string:    \u001b[31m\"<!DOCTYPE HTML PUBLIC \\\"-//W3C//DTD HTML 4.01//EN\\\" \\\"http://www.w3.org/TR/html4/strict.dtd\\\">\u001b[39m\n\u001b[31m<html><head>\u001b[39m\n\u001b[31m<title>404 Not Found</title>\u001b[39m\n\u001b[31m</head><body>\u001b[39m\n\u001b[31m<h1>Not Found</h1>\u001b[39m\n\u001b[31m<p>The requested URL was not found on this server.</p>\u001b[39m\n\u001b[31m</body></html>\u001b[39m\n\u001b[31m\"\u001b[39m\n    at /var/www/360lm/tests/vehicle.spec.js:82:18",
                        "location": {
                          "file": "/var/www/360lm/tests/vehicle.spec.js",
                          "column": 18,
                          "line": 82
                        },
                        "snippet": "  80 |     const html = await resp.text();\n  81 |\n> 82 |     expect(html).toContain('function calcSegmentMileage');\n     |                  ^\n  83 |     expect(html).toContain('function applyMileage');\n  84 |     expect(html).toContain('function openRetroSwitch');\n  85 |     expect(html).toContain('function saveRetroSwitch');"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/vehicle.spec.js",
                            "column": 18,
                            "line": 82
                          },
                          "message": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoContain\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m) // indexOf\u001b[22m\n\nExpected substring: \u001b[32m\"function calcSegmentMileage\"\u001b[39m\nReceived string:    \u001b[31m\"<!DOCTYPE HTML PUBLIC \\\"-//W3C//DTD HTML 4.01//EN\\\" \\\"http://www.w3.org/TR/html4/strict.dtd\\\">\u001b[39m\n\u001b[31m<html><head>\u001b[39m\n\u001b[31m<title>404 Not Found</title>\u001b[39m\n\u001b[31m</head><body>\u001b[39m\n\u001b[31m<h1>Not Found</h1>\u001b[39m\n\u001b[31m<p>The requested URL was not found on this server.</p>\u001b[39m\n\u001b[31m</body></html>\u001b[39m\n\u001b[31m\"\u001b[39m\n\n  80 |     const html = await resp.text();\n  81 |\n> 82 |     expect(html).toContain('function calcSegmentMileage');\n     |                  ^\n  83 |     expect(html).toContain('function applyMileage');\n  84 |     expect(html).toContain('function openRetroSwitch');\n  85 |     expect(html).toContain('function saveRetroSwitch');\n    at /var/www/360lm/tests/vehicle.spec.js:82:18"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 1,
                      "startTime": "2026-07-12T13:37:28.506Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/vehicle-Vehicle-Log-—-HTML-94be2-ns-and-retro-switch-present-desktop-chrome-retry1/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/vehicle.spec.js",
                        "column": 18,
                        "line": 82
                      }
                    }
                  ],
                  "status": "unexpected"
                }
              ],
              "id": "084e7535606ab6e45f08-4ad5358a0f338a97379f",
              "file": "vehicle.spec.js",
              "line": 77,
              "column": 3
            }
          ]
        },
        {
          "title": "Vehicle Log — HTML Structure (v25)",
          "file": "vehicle.spec.js",
          "line": 93,
          "column": 6,
          "specs": [
            {
              "title": "fuel bill photo and CC slip photo elements present",
              "ok": false,
              "tags": [],
              "tests": [
                {
                  "timeout": 30000,
                  "annotations": [],
                  "expectedStatus": "passed",
                  "projectId": "android-chrome",
                  "projectName": "android-chrome",
                  "results": [
                    {
                      "workerIndex": 168,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 60,
                      "error": {
                        "message": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoContain\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m) // indexOf\u001b[22m\n\nExpected substring: \u001b[32m\"id=\\\"fuel-bill-preview\\\"\"\u001b[39m\nReceived string:    \u001b[31m\"<!DOCTYPE HTML PUBLIC \\\"-//W3C//DTD HTML 4.01//EN\\\" \\\"http://www.w3.org/TR/html4/strict.dtd\\\">\u001b[39m\n\u001b[31m<html><head>\u001b[39m\n\u001b[31m<title>404 Not Found</title>\u001b[39m\n\u001b[31m</head><body>\u001b[39m\n\u001b[31m<h1>Not Found</h1>\u001b[39m\n\u001b[31m<p>The requested URL was not found on this server.</p>\u001b[39m\n\u001b[31m</body></html>\u001b[39m\n\u001b[31m\"\u001b[39m",
                        "stack": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoContain\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m) // indexOf\u001b[22m\n\nExpected substring: \u001b[32m\"id=\\\"fuel-bill-preview\\\"\"\u001b[39m\nReceived string:    \u001b[31m\"<!DOCTYPE HTML PUBLIC \\\"-//W3C//DTD HTML 4.01//EN\\\" \\\"http://www.w3.org/TR/html4/strict.dtd\\\">\u001b[39m\n\u001b[31m<html><head>\u001b[39m\n\u001b[31m<title>404 Not Found</title>\u001b[39m\n\u001b[31m</head><body>\u001b[39m\n\u001b[31m<h1>Not Found</h1>\u001b[39m\n\u001b[31m<p>The requested URL was not found on this server.</p>\u001b[39m\n\u001b[31m</body></html>\u001b[39m\n\u001b[31m\"\u001b[39m\n    at /var/www/360lm/tests/vehicle.spec.js:99:18",
                        "location": {
                          "file": "/var/www/360lm/tests/vehicle.spec.js",
                          "column": 18,
                          "line": 99
                        },
                        "snippet": "   97 |     const html = await resp.text();\n   98 |\n>  99 |     expect(html).toContain('id=\"fuel-bill-preview\"');\n      |                  ^\n  100 |     expect(html).toContain('id=\"fuel-bill-photo-url\"');\n  101 |     expect(html).toContain('id=\"cc-slip-row\"');\n  102 |     expect(html).toContain('id=\"cc-slip-preview\"');"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/vehicle.spec.js",
                            "column": 18,
                            "line": 99
                          },
                          "message": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoContain\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m) // indexOf\u001b[22m\n\nExpected substring: \u001b[32m\"id=\\\"fuel-bill-preview\\\"\"\u001b[39m\nReceived string:    \u001b[31m\"<!DOCTYPE HTML PUBLIC \\\"-//W3C//DTD HTML 4.01//EN\\\" \\\"http://www.w3.org/TR/html4/strict.dtd\\\">\u001b[39m\n\u001b[31m<html><head>\u001b[39m\n\u001b[31m<title>404 Not Found</title>\u001b[39m\n\u001b[31m</head><body>\u001b[39m\n\u001b[31m<h1>Not Found</h1>\u001b[39m\n\u001b[31m<p>The requested URL was not found on this server.</p>\u001b[39m\n\u001b[31m</body></html>\u001b[39m\n\u001b[31m\"\u001b[39m\n\n   97 |     const html = await resp.text();\n   98 |\n>  99 |     expect(html).toContain('id=\"fuel-bill-preview\"');\n      |                  ^\n  100 |     expect(html).toContain('id=\"fuel-bill-photo-url\"');\n  101 |     expect(html).toContain('id=\"cc-slip-row\"');\n  102 |     expect(html).toContain('id=\"cc-slip-preview\"');\n    at /var/www/360lm/tests/vehicle.spec.js:99:18"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 0,
                      "startTime": "2026-07-12T13:17:29.059Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/vehicle-Vehicle-Log-—-HTML-5020b-slip-photo-elements-present-android-chrome/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/vehicle.spec.js",
                        "column": 18,
                        "line": 99
                      }
                    },
                    {
                      "workerIndex": 169,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 85,
                      "error": {
                        "message": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoContain\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m) // indexOf\u001b[22m\n\nExpected substring: \u001b[32m\"id=\\\"fuel-bill-preview\\\"\"\u001b[39m\nReceived string:    \u001b[31m\"<!DOCTYPE HTML PUBLIC \\\"-//W3C//DTD HTML 4.01//EN\\\" \\\"http://www.w3.org/TR/html4/strict.dtd\\\">\u001b[39m\n\u001b[31m<html><head>\u001b[39m\n\u001b[31m<title>404 Not Found</title>\u001b[39m\n\u001b[31m</head><body>\u001b[39m\n\u001b[31m<h1>Not Found</h1>\u001b[39m\n\u001b[31m<p>The requested URL was not found on this server.</p>\u001b[39m\n\u001b[31m</body></html>\u001b[39m\n\u001b[31m\"\u001b[39m",
                        "stack": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoContain\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m) // indexOf\u001b[22m\n\nExpected substring: \u001b[32m\"id=\\\"fuel-bill-preview\\\"\"\u001b[39m\nReceived string:    \u001b[31m\"<!DOCTYPE HTML PUBLIC \\\"-//W3C//DTD HTML 4.01//EN\\\" \\\"http://www.w3.org/TR/html4/strict.dtd\\\">\u001b[39m\n\u001b[31m<html><head>\u001b[39m\n\u001b[31m<title>404 Not Found</title>\u001b[39m\n\u001b[31m</head><body>\u001b[39m\n\u001b[31m<h1>Not Found</h1>\u001b[39m\n\u001b[31m<p>The requested URL was not found on this server.</p>\u001b[39m\n\u001b[31m</body></html>\u001b[39m\n\u001b[31m\"\u001b[39m\n    at /var/www/360lm/tests/vehicle.spec.js:99:18",
                        "location": {
                          "file": "/var/www/360lm/tests/vehicle.spec.js",
                          "column": 18,
                          "line": 99
                        },
                        "snippet": "   97 |     const html = await resp.text();\n   98 |\n>  99 |     expect(html).toContain('id=\"fuel-bill-preview\"');\n      |                  ^\n  100 |     expect(html).toContain('id=\"fuel-bill-photo-url\"');\n  101 |     expect(html).toContain('id=\"cc-slip-row\"');\n  102 |     expect(html).toContain('id=\"cc-slip-preview\"');"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/vehicle.spec.js",
                            "column": 18,
                            "line": 99
                          },
                          "message": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoContain\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m) // indexOf\u001b[22m\n\nExpected substring: \u001b[32m\"id=\\\"fuel-bill-preview\\\"\"\u001b[39m\nReceived string:    \u001b[31m\"<!DOCTYPE HTML PUBLIC \\\"-//W3C//DTD HTML 4.01//EN\\\" \\\"http://www.w3.org/TR/html4/strict.dtd\\\">\u001b[39m\n\u001b[31m<html><head>\u001b[39m\n\u001b[31m<title>404 Not Found</title>\u001b[39m\n\u001b[31m</head><body>\u001b[39m\n\u001b[31m<h1>Not Found</h1>\u001b[39m\n\u001b[31m<p>The requested URL was not found on this server.</p>\u001b[39m\n\u001b[31m</body></html>\u001b[39m\n\u001b[31m\"\u001b[39m\n\n   97 |     const html = await resp.text();\n   98 |\n>  99 |     expect(html).toContain('id=\"fuel-bill-preview\"');\n      |                  ^\n  100 |     expect(html).toContain('id=\"fuel-bill-photo-url\"');\n  101 |     expect(html).toContain('id=\"cc-slip-row\"');\n  102 |     expect(html).toContain('id=\"cc-slip-preview\"');\n    at /var/www/360lm/tests/vehicle.spec.js:99:18"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 1,
                      "startTime": "2026-07-12T13:17:29.936Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/vehicle-Vehicle-Log-—-HTML-5020b-slip-photo-elements-present-android-chrome-retry1/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/vehicle.spec.js",
                        "column": 18,
                        "line": 99
                      }
                    }
                  ],
                  "status": "unexpected"
                }
              ],
              "id": "084e7535606ab6e45f08-4ccd65b60ead57af1fd6",
              "file": "vehicle.spec.js",
              "line": 94,
              "column": 3
            },
            {
              "title": "fuel bill and CC slip handler functions present",
              "ok": false,
              "tags": [],
              "tests": [
                {
                  "timeout": 30000,
                  "annotations": [],
                  "expectedStatus": "passed",
                  "projectId": "android-chrome",
                  "projectName": "android-chrome",
                  "results": [
                    {
                      "workerIndex": 170,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 79,
                      "error": {
                        "message": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoContain\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m) // indexOf\u001b[22m\n\nExpected substring: \u001b[32m\"function handleFuelBillPhoto\"\u001b[39m\nReceived string:    \u001b[31m\"<!DOCTYPE HTML PUBLIC \\\"-//W3C//DTD HTML 4.01//EN\\\" \\\"http://www.w3.org/TR/html4/strict.dtd\\\">\u001b[39m\n\u001b[31m<html><head>\u001b[39m\n\u001b[31m<title>404 Not Found</title>\u001b[39m\n\u001b[31m</head><body>\u001b[39m\n\u001b[31m<h1>Not Found</h1>\u001b[39m\n\u001b[31m<p>The requested URL was not found on this server.</p>\u001b[39m\n\u001b[31m</body></html>\u001b[39m\n\u001b[31m\"\u001b[39m",
                        "stack": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoContain\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m) // indexOf\u001b[22m\n\nExpected substring: \u001b[32m\"function handleFuelBillPhoto\"\u001b[39m\nReceived string:    \u001b[31m\"<!DOCTYPE HTML PUBLIC \\\"-//W3C//DTD HTML 4.01//EN\\\" \\\"http://www.w3.org/TR/html4/strict.dtd\\\">\u001b[39m\n\u001b[31m<html><head>\u001b[39m\n\u001b[31m<title>404 Not Found</title>\u001b[39m\n\u001b[31m</head><body>\u001b[39m\n\u001b[31m<h1>Not Found</h1>\u001b[39m\n\u001b[31m<p>The requested URL was not found on this server.</p>\u001b[39m\n\u001b[31m</body></html>\u001b[39m\n\u001b[31m\"\u001b[39m\n    at /var/www/360lm/tests/vehicle.spec.js:113:18",
                        "location": {
                          "file": "/var/www/360lm/tests/vehicle.spec.js",
                          "column": 18,
                          "line": 113
                        },
                        "snippet": "  111 |     const html = await resp.text();\n  112 |\n> 113 |     expect(html).toContain('function handleFuelBillPhoto');\n      |                  ^\n  114 |     expect(html).toContain('function handleCCSlipPhoto');\n  115 |     expect(html).toContain('function retakeFuelBillPhoto');\n  116 |     expect(html).toContain('function retakeCCSlipPhoto');"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/vehicle.spec.js",
                            "column": 18,
                            "line": 113
                          },
                          "message": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoContain\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m) // indexOf\u001b[22m\n\nExpected substring: \u001b[32m\"function handleFuelBillPhoto\"\u001b[39m\nReceived string:    \u001b[31m\"<!DOCTYPE HTML PUBLIC \\\"-//W3C//DTD HTML 4.01//EN\\\" \\\"http://www.w3.org/TR/html4/strict.dtd\\\">\u001b[39m\n\u001b[31m<html><head>\u001b[39m\n\u001b[31m<title>404 Not Found</title>\u001b[39m\n\u001b[31m</head><body>\u001b[39m\n\u001b[31m<h1>Not Found</h1>\u001b[39m\n\u001b[31m<p>The requested URL was not found on this server.</p>\u001b[39m\n\u001b[31m</body></html>\u001b[39m\n\u001b[31m\"\u001b[39m\n\n  111 |     const html = await resp.text();\n  112 |\n> 113 |     expect(html).toContain('function handleFuelBillPhoto');\n      |                  ^\n  114 |     expect(html).toContain('function handleCCSlipPhoto');\n  115 |     expect(html).toContain('function retakeFuelBillPhoto');\n  116 |     expect(html).toContain('function retakeCCSlipPhoto');\n    at /var/www/360lm/tests/vehicle.spec.js:113:18"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 0,
                      "startTime": "2026-07-12T13:17:30.923Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/vehicle-Vehicle-Log-—-HTML-59590-p-handler-functions-present-android-chrome/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/vehicle.spec.js",
                        "column": 18,
                        "line": 113
                      }
                    },
                    {
                      "workerIndex": 171,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 83,
                      "error": {
                        "message": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoContain\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m) // indexOf\u001b[22m\n\nExpected substring: \u001b[32m\"function handleFuelBillPhoto\"\u001b[39m\nReceived string:    \u001b[31m\"<!DOCTYPE HTML PUBLIC \\\"-//W3C//DTD HTML 4.01//EN\\\" \\\"http://www.w3.org/TR/html4/strict.dtd\\\">\u001b[39m\n\u001b[31m<html><head>\u001b[39m\n\u001b[31m<title>404 Not Found</title>\u001b[39m\n\u001b[31m</head><body>\u001b[39m\n\u001b[31m<h1>Not Found</h1>\u001b[39m\n\u001b[31m<p>The requested URL was not found on this server.</p>\u001b[39m\n\u001b[31m</body></html>\u001b[39m\n\u001b[31m\"\u001b[39m",
                        "stack": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoContain\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m) // indexOf\u001b[22m\n\nExpected substring: \u001b[32m\"function handleFuelBillPhoto\"\u001b[39m\nReceived string:    \u001b[31m\"<!DOCTYPE HTML PUBLIC \\\"-//W3C//DTD HTML 4.01//EN\\\" \\\"http://www.w3.org/TR/html4/strict.dtd\\\">\u001b[39m\n\u001b[31m<html><head>\u001b[39m\n\u001b[31m<title>404 Not Found</title>\u001b[39m\n\u001b[31m</head><body>\u001b[39m\n\u001b[31m<h1>Not Found</h1>\u001b[39m\n\u001b[31m<p>The requested URL was not found on this server.</p>\u001b[39m\n\u001b[31m</body></html>\u001b[39m\n\u001b[31m\"\u001b[39m\n    at /var/www/360lm/tests/vehicle.spec.js:113:18",
                        "location": {
                          "file": "/var/www/360lm/tests/vehicle.spec.js",
                          "column": 18,
                          "line": 113
                        },
                        "snippet": "  111 |     const html = await resp.text();\n  112 |\n> 113 |     expect(html).toContain('function handleFuelBillPhoto');\n      |                  ^\n  114 |     expect(html).toContain('function handleCCSlipPhoto');\n  115 |     expect(html).toContain('function retakeFuelBillPhoto');\n  116 |     expect(html).toContain('function retakeCCSlipPhoto');"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/vehicle.spec.js",
                            "column": 18,
                            "line": 113
                          },
                          "message": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoContain\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m) // indexOf\u001b[22m\n\nExpected substring: \u001b[32m\"function handleFuelBillPhoto\"\u001b[39m\nReceived string:    \u001b[31m\"<!DOCTYPE HTML PUBLIC \\\"-//W3C//DTD HTML 4.01//EN\\\" \\\"http://www.w3.org/TR/html4/strict.dtd\\\">\u001b[39m\n\u001b[31m<html><head>\u001b[39m\n\u001b[31m<title>404 Not Found</title>\u001b[39m\n\u001b[31m</head><body>\u001b[39m\n\u001b[31m<h1>Not Found</h1>\u001b[39m\n\u001b[31m<p>The requested URL was not found on this server.</p>\u001b[39m\n\u001b[31m</body></html>\u001b[39m\n\u001b[31m\"\u001b[39m\n\n  111 |     const html = await resp.text();\n  112 |\n> 113 |     expect(html).toContain('function handleFuelBillPhoto');\n      |                  ^\n  114 |     expect(html).toContain('function handleCCSlipPhoto');\n  115 |     expect(html).toContain('function retakeFuelBillPhoto');\n  116 |     expect(html).toContain('function retakeCCSlipPhoto');\n    at /var/www/360lm/tests/vehicle.spec.js:113:18"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 1,
                      "startTime": "2026-07-12T13:17:32.110Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/vehicle-Vehicle-Log-—-HTML-59590-p-handler-functions-present-android-chrome-retry1/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/vehicle.spec.js",
                        "column": 18,
                        "line": 113
                      }
                    }
                  ],
                  "status": "unexpected"
                }
              ],
              "id": "084e7535606ab6e45f08-6be3e9e521885024edcc",
              "file": "vehicle.spec.js",
              "line": 108,
              "column": 3
            },
            {
              "title": "fuel bill photo and CC slip photo elements present",
              "ok": false,
              "tags": [],
              "tests": [
                {
                  "timeout": 30000,
                  "annotations": [],
                  "expectedStatus": "passed",
                  "projectId": "desktop-chrome",
                  "projectName": "desktop-chrome",
                  "results": [
                    {
                      "workerIndex": 344,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 55,
                      "error": {
                        "message": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoContain\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m) // indexOf\u001b[22m\n\nExpected substring: \u001b[32m\"id=\\\"fuel-bill-preview\\\"\"\u001b[39m\nReceived string:    \u001b[31m\"<!DOCTYPE HTML PUBLIC \\\"-//W3C//DTD HTML 4.01//EN\\\" \\\"http://www.w3.org/TR/html4/strict.dtd\\\">\u001b[39m\n\u001b[31m<html><head>\u001b[39m\n\u001b[31m<title>404 Not Found</title>\u001b[39m\n\u001b[31m</head><body>\u001b[39m\n\u001b[31m<h1>Not Found</h1>\u001b[39m\n\u001b[31m<p>The requested URL was not found on this server.</p>\u001b[39m\n\u001b[31m</body></html>\u001b[39m\n\u001b[31m\"\u001b[39m",
                        "stack": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoContain\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m) // indexOf\u001b[22m\n\nExpected substring: \u001b[32m\"id=\\\"fuel-bill-preview\\\"\"\u001b[39m\nReceived string:    \u001b[31m\"<!DOCTYPE HTML PUBLIC \\\"-//W3C//DTD HTML 4.01//EN\\\" \\\"http://www.w3.org/TR/html4/strict.dtd\\\">\u001b[39m\n\u001b[31m<html><head>\u001b[39m\n\u001b[31m<title>404 Not Found</title>\u001b[39m\n\u001b[31m</head><body>\u001b[39m\n\u001b[31m<h1>Not Found</h1>\u001b[39m\n\u001b[31m<p>The requested URL was not found on this server.</p>\u001b[39m\n\u001b[31m</body></html>\u001b[39m\n\u001b[31m\"\u001b[39m\n    at /var/www/360lm/tests/vehicle.spec.js:99:18",
                        "location": {
                          "file": "/var/www/360lm/tests/vehicle.spec.js",
                          "column": 18,
                          "line": 99
                        },
                        "snippet": "   97 |     const html = await resp.text();\n   98 |\n>  99 |     expect(html).toContain('id=\"fuel-bill-preview\"');\n      |                  ^\n  100 |     expect(html).toContain('id=\"fuel-bill-photo-url\"');\n  101 |     expect(html).toContain('id=\"cc-slip-row\"');\n  102 |     expect(html).toContain('id=\"cc-slip-preview\"');"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/vehicle.spec.js",
                            "column": 18,
                            "line": 99
                          },
                          "message": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoContain\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m) // indexOf\u001b[22m\n\nExpected substring: \u001b[32m\"id=\\\"fuel-bill-preview\\\"\"\u001b[39m\nReceived string:    \u001b[31m\"<!DOCTYPE HTML PUBLIC \\\"-//W3C//DTD HTML 4.01//EN\\\" \\\"http://www.w3.org/TR/html4/strict.dtd\\\">\u001b[39m\n\u001b[31m<html><head>\u001b[39m\n\u001b[31m<title>404 Not Found</title>\u001b[39m\n\u001b[31m</head><body>\u001b[39m\n\u001b[31m<h1>Not Found</h1>\u001b[39m\n\u001b[31m<p>The requested URL was not found on this server.</p>\u001b[39m\n\u001b[31m</body></html>\u001b[39m\n\u001b[31m\"\u001b[39m\n\n   97 |     const html = await resp.text();\n   98 |\n>  99 |     expect(html).toContain('id=\"fuel-bill-preview\"');\n      |                  ^\n  100 |     expect(html).toContain('id=\"fuel-bill-photo-url\"');\n  101 |     expect(html).toContain('id=\"cc-slip-row\"');\n  102 |     expect(html).toContain('id=\"cc-slip-preview\"');\n    at /var/www/360lm/tests/vehicle.spec.js:99:18"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 0,
                      "startTime": "2026-07-12T13:37:29.435Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/vehicle-Vehicle-Log-—-HTML-5020b-slip-photo-elements-present-desktop-chrome/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/vehicle.spec.js",
                        "column": 18,
                        "line": 99
                      }
                    },
                    {
                      "workerIndex": 345,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 61,
                      "error": {
                        "message": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoContain\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m) // indexOf\u001b[22m\n\nExpected substring: \u001b[32m\"id=\\\"fuel-bill-preview\\\"\"\u001b[39m\nReceived string:    \u001b[31m\"<!DOCTYPE HTML PUBLIC \\\"-//W3C//DTD HTML 4.01//EN\\\" \\\"http://www.w3.org/TR/html4/strict.dtd\\\">\u001b[39m\n\u001b[31m<html><head>\u001b[39m\n\u001b[31m<title>404 Not Found</title>\u001b[39m\n\u001b[31m</head><body>\u001b[39m\n\u001b[31m<h1>Not Found</h1>\u001b[39m\n\u001b[31m<p>The requested URL was not found on this server.</p>\u001b[39m\n\u001b[31m</body></html>\u001b[39m\n\u001b[31m\"\u001b[39m",
                        "stack": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoContain\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m) // indexOf\u001b[22m\n\nExpected substring: \u001b[32m\"id=\\\"fuel-bill-preview\\\"\"\u001b[39m\nReceived string:    \u001b[31m\"<!DOCTYPE HTML PUBLIC \\\"-//W3C//DTD HTML 4.01//EN\\\" \\\"http://www.w3.org/TR/html4/strict.dtd\\\">\u001b[39m\n\u001b[31m<html><head>\u001b[39m\n\u001b[31m<title>404 Not Found</title>\u001b[39m\n\u001b[31m</head><body>\u001b[39m\n\u001b[31m<h1>Not Found</h1>\u001b[39m\n\u001b[31m<p>The requested URL was not found on this server.</p>\u001b[39m\n\u001b[31m</body></html>\u001b[39m\n\u001b[31m\"\u001b[39m\n    at /var/www/360lm/tests/vehicle.spec.js:99:18",
                        "location": {
                          "file": "/var/www/360lm/tests/vehicle.spec.js",
                          "column": 18,
                          "line": 99
                        },
                        "snippet": "   97 |     const html = await resp.text();\n   98 |\n>  99 |     expect(html).toContain('id=\"fuel-bill-preview\"');\n      |                  ^\n  100 |     expect(html).toContain('id=\"fuel-bill-photo-url\"');\n  101 |     expect(html).toContain('id=\"cc-slip-row\"');\n  102 |     expect(html).toContain('id=\"cc-slip-preview\"');"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/vehicle.spec.js",
                            "column": 18,
                            "line": 99
                          },
                          "message": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoContain\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m) // indexOf\u001b[22m\n\nExpected substring: \u001b[32m\"id=\\\"fuel-bill-preview\\\"\"\u001b[39m\nReceived string:    \u001b[31m\"<!DOCTYPE HTML PUBLIC \\\"-//W3C//DTD HTML 4.01//EN\\\" \\\"http://www.w3.org/TR/html4/strict.dtd\\\">\u001b[39m\n\u001b[31m<html><head>\u001b[39m\n\u001b[31m<title>404 Not Found</title>\u001b[39m\n\u001b[31m</head><body>\u001b[39m\n\u001b[31m<h1>Not Found</h1>\u001b[39m\n\u001b[31m<p>The requested URL was not found on this server.</p>\u001b[39m\n\u001b[31m</body></html>\u001b[39m\n\u001b[31m\"\u001b[39m\n\n   97 |     const html = await resp.text();\n   98 |\n>  99 |     expect(html).toContain('id=\"fuel-bill-preview\"');\n      |                  ^\n  100 |     expect(html).toContain('id=\"fuel-bill-photo-url\"');\n  101 |     expect(html).toContain('id=\"cc-slip-row\"');\n  102 |     expect(html).toContain('id=\"cc-slip-preview\"');\n    at /var/www/360lm/tests/vehicle.spec.js:99:18"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 1,
                      "startTime": "2026-07-12T13:37:30.250Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/vehicle-Vehicle-Log-—-HTML-5020b-slip-photo-elements-present-desktop-chrome-retry1/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/vehicle.spec.js",
                        "column": 18,
                        "line": 99
                      }
                    }
                  ],
                  "status": "unexpected"
                }
              ],
              "id": "084e7535606ab6e45f08-ce93d65c65ac5105f143",
              "file": "vehicle.spec.js",
              "line": 94,
              "column": 3
            },
            {
              "title": "fuel bill and CC slip handler functions present",
              "ok": false,
              "tags": [],
              "tests": [
                {
                  "timeout": 30000,
                  "annotations": [],
                  "expectedStatus": "passed",
                  "projectId": "desktop-chrome",
                  "projectName": "desktop-chrome",
                  "results": [
                    {
                      "workerIndex": 346,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 57,
                      "error": {
                        "message": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoContain\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m) // indexOf\u001b[22m\n\nExpected substring: \u001b[32m\"function handleFuelBillPhoto\"\u001b[39m\nReceived string:    \u001b[31m\"<!DOCTYPE HTML PUBLIC \\\"-//W3C//DTD HTML 4.01//EN\\\" \\\"http://www.w3.org/TR/html4/strict.dtd\\\">\u001b[39m\n\u001b[31m<html><head>\u001b[39m\n\u001b[31m<title>404 Not Found</title>\u001b[39m\n\u001b[31m</head><body>\u001b[39m\n\u001b[31m<h1>Not Found</h1>\u001b[39m\n\u001b[31m<p>The requested URL was not found on this server.</p>\u001b[39m\n\u001b[31m</body></html>\u001b[39m\n\u001b[31m\"\u001b[39m",
                        "stack": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoContain\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m) // indexOf\u001b[22m\n\nExpected substring: \u001b[32m\"function handleFuelBillPhoto\"\u001b[39m\nReceived string:    \u001b[31m\"<!DOCTYPE HTML PUBLIC \\\"-//W3C//DTD HTML 4.01//EN\\\" \\\"http://www.w3.org/TR/html4/strict.dtd\\\">\u001b[39m\n\u001b[31m<html><head>\u001b[39m\n\u001b[31m<title>404 Not Found</title>\u001b[39m\n\u001b[31m</head><body>\u001b[39m\n\u001b[31m<h1>Not Found</h1>\u001b[39m\n\u001b[31m<p>The requested URL was not found on this server.</p>\u001b[39m\n\u001b[31m</body></html>\u001b[39m\n\u001b[31m\"\u001b[39m\n    at /var/www/360lm/tests/vehicle.spec.js:113:18",
                        "location": {
                          "file": "/var/www/360lm/tests/vehicle.spec.js",
                          "column": 18,
                          "line": 113
                        },
                        "snippet": "  111 |     const html = await resp.text();\n  112 |\n> 113 |     expect(html).toContain('function handleFuelBillPhoto');\n      |                  ^\n  114 |     expect(html).toContain('function handleCCSlipPhoto');\n  115 |     expect(html).toContain('function retakeFuelBillPhoto');\n  116 |     expect(html).toContain('function retakeCCSlipPhoto');"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/vehicle.spec.js",
                            "column": 18,
                            "line": 113
                          },
                          "message": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoContain\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m) // indexOf\u001b[22m\n\nExpected substring: \u001b[32m\"function handleFuelBillPhoto\"\u001b[39m\nReceived string:    \u001b[31m\"<!DOCTYPE HTML PUBLIC \\\"-//W3C//DTD HTML 4.01//EN\\\" \\\"http://www.w3.org/TR/html4/strict.dtd\\\">\u001b[39m\n\u001b[31m<html><head>\u001b[39m\n\u001b[31m<title>404 Not Found</title>\u001b[39m\n\u001b[31m</head><body>\u001b[39m\n\u001b[31m<h1>Not Found</h1>\u001b[39m\n\u001b[31m<p>The requested URL was not found on this server.</p>\u001b[39m\n\u001b[31m</body></html>\u001b[39m\n\u001b[31m\"\u001b[39m\n\n  111 |     const html = await resp.text();\n  112 |\n> 113 |     expect(html).toContain('function handleFuelBillPhoto');\n      |                  ^\n  114 |     expect(html).toContain('function handleCCSlipPhoto');\n  115 |     expect(html).toContain('function retakeFuelBillPhoto');\n  116 |     expect(html).toContain('function retakeCCSlipPhoto');\n    at /var/www/360lm/tests/vehicle.spec.js:113:18"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 0,
                      "startTime": "2026-07-12T13:37:31.182Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/vehicle-Vehicle-Log-—-HTML-59590-p-handler-functions-present-desktop-chrome/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/vehicle.spec.js",
                        "column": 18,
                        "line": 113
                      }
                    },
                    {
                      "workerIndex": 347,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 62,
                      "error": {
                        "message": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoContain\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m) // indexOf\u001b[22m\n\nExpected substring: \u001b[32m\"function handleFuelBillPhoto\"\u001b[39m\nReceived string:    \u001b[31m\"<!DOCTYPE HTML PUBLIC \\\"-//W3C//DTD HTML 4.01//EN\\\" \\\"http://www.w3.org/TR/html4/strict.dtd\\\">\u001b[39m\n\u001b[31m<html><head>\u001b[39m\n\u001b[31m<title>404 Not Found</title>\u001b[39m\n\u001b[31m</head><body>\u001b[39m\n\u001b[31m<h1>Not Found</h1>\u001b[39m\n\u001b[31m<p>The requested URL was not found on this server.</p>\u001b[39m\n\u001b[31m</body></html>\u001b[39m\n\u001b[31m\"\u001b[39m",
                        "stack": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoContain\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m) // indexOf\u001b[22m\n\nExpected substring: \u001b[32m\"function handleFuelBillPhoto\"\u001b[39m\nReceived string:    \u001b[31m\"<!DOCTYPE HTML PUBLIC \\\"-//W3C//DTD HTML 4.01//EN\\\" \\\"http://www.w3.org/TR/html4/strict.dtd\\\">\u001b[39m\n\u001b[31m<html><head>\u001b[39m\n\u001b[31m<title>404 Not Found</title>\u001b[39m\n\u001b[31m</head><body>\u001b[39m\n\u001b[31m<h1>Not Found</h1>\u001b[39m\n\u001b[31m<p>The requested URL was not found on this server.</p>\u001b[39m\n\u001b[31m</body></html>\u001b[39m\n\u001b[31m\"\u001b[39m\n    at /var/www/360lm/tests/vehicle.spec.js:113:18",
                        "location": {
                          "file": "/var/www/360lm/tests/vehicle.spec.js",
                          "column": 18,
                          "line": 113
                        },
                        "snippet": "  111 |     const html = await resp.text();\n  112 |\n> 113 |     expect(html).toContain('function handleFuelBillPhoto');\n      |                  ^\n  114 |     expect(html).toContain('function handleCCSlipPhoto');\n  115 |     expect(html).toContain('function retakeFuelBillPhoto');\n  116 |     expect(html).toContain('function retakeCCSlipPhoto');"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/vehicle.spec.js",
                            "column": 18,
                            "line": 113
                          },
                          "message": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoContain\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m) // indexOf\u001b[22m\n\nExpected substring: \u001b[32m\"function handleFuelBillPhoto\"\u001b[39m\nReceived string:    \u001b[31m\"<!DOCTYPE HTML PUBLIC \\\"-//W3C//DTD HTML 4.01//EN\\\" \\\"http://www.w3.org/TR/html4/strict.dtd\\\">\u001b[39m\n\u001b[31m<html><head>\u001b[39m\n\u001b[31m<title>404 Not Found</title>\u001b[39m\n\u001b[31m</head><body>\u001b[39m\n\u001b[31m<h1>Not Found</h1>\u001b[39m\n\u001b[31m<p>The requested URL was not found on this server.</p>\u001b[39m\n\u001b[31m</body></html>\u001b[39m\n\u001b[31m\"\u001b[39m\n\n  111 |     const html = await resp.text();\n  112 |\n> 113 |     expect(html).toContain('function handleFuelBillPhoto');\n      |                  ^\n  114 |     expect(html).toContain('function handleCCSlipPhoto');\n  115 |     expect(html).toContain('function retakeFuelBillPhoto');\n  116 |     expect(html).toContain('function retakeCCSlipPhoto');\n    at /var/www/360lm/tests/vehicle.spec.js:113:18"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 1,
                      "startTime": "2026-07-12T13:37:32.117Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/vehicle-Vehicle-Log-—-HTML-59590-p-handler-functions-present-desktop-chrome-retry1/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/vehicle.spec.js",
                        "column": 18,
                        "line": 113
                      }
                    }
                  ],
                  "status": "unexpected"
                }
              ],
              "id": "084e7535606ab6e45f08-c8df628ad737ecf34fd7",
              "file": "vehicle.spec.js",
              "line": 108,
              "column": 3
            }
          ]
        },
        {
          "title": "Vehicle Log — Hub Navigation",
          "file": "vehicle.spec.js",
          "line": 122,
          "column": 6,
          "specs": [
            {
              "title": "dashboard header has Hub home button",
              "ok": false,
              "tags": [],
              "tests": [
                {
                  "timeout": 30000,
                  "annotations": [],
                  "expectedStatus": "passed",
                  "projectId": "android-chrome",
                  "projectName": "android-chrome",
                  "results": [
                    {
                      "workerIndex": 172,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 80,
                      "error": {
                        "message": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoContain\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m) // indexOf\u001b[22m\n\nExpected substring: \u001b[32m\"window.location.href='/hub/'\"\u001b[39m\nReceived string:    \u001b[31m\"<!DOCTYPE HTML PUBLIC \\\"-//W3C//DTD HTML 4.01//EN\\\" \\\"http://www.w3.org/TR/html4/strict.dtd\\\">\u001b[39m\n\u001b[31m<html><head>\u001b[39m\n\u001b[31m<title>404 Not Found</title>\u001b[39m\n\u001b[31m</head><body>\u001b[39m\n\u001b[31m<h1>Not Found</h1>\u001b[39m\n\u001b[31m<p>The requested URL was not found on this server.</p>\u001b[39m\n\u001b[31m</body></html>\u001b[39m\n\u001b[31m\"\u001b[39m",
                        "stack": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoContain\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m) // indexOf\u001b[22m\n\nExpected substring: \u001b[32m\"window.location.href='/hub/'\"\u001b[39m\nReceived string:    \u001b[31m\"<!DOCTYPE HTML PUBLIC \\\"-//W3C//DTD HTML 4.01//EN\\\" \\\"http://www.w3.org/TR/html4/strict.dtd\\\">\u001b[39m\n\u001b[31m<html><head>\u001b[39m\n\u001b[31m<title>404 Not Found</title>\u001b[39m\n\u001b[31m</head><body>\u001b[39m\n\u001b[31m<h1>Not Found</h1>\u001b[39m\n\u001b[31m<p>The requested URL was not found on this server.</p>\u001b[39m\n\u001b[31m</body></html>\u001b[39m\n\u001b[31m\"\u001b[39m\n    at /var/www/360lm/tests/vehicle.spec.js:128:18",
                        "location": {
                          "file": "/var/www/360lm/tests/vehicle.spec.js",
                          "column": 18,
                          "line": 128
                        },
                        "snippet": "  126 |     const html = await resp.text();\n  127 |     await ctx.dispose();\n> 128 |     expect(html).toContain(\"window.location.href='/hub/'\");\n      |                  ^\n  129 |     expect(html).toContain('🏡');\n  130 |   });\n  131 |"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/vehicle.spec.js",
                            "column": 18,
                            "line": 128
                          },
                          "message": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoContain\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m) // indexOf\u001b[22m\n\nExpected substring: \u001b[32m\"window.location.href='/hub/'\"\u001b[39m\nReceived string:    \u001b[31m\"<!DOCTYPE HTML PUBLIC \\\"-//W3C//DTD HTML 4.01//EN\\\" \\\"http://www.w3.org/TR/html4/strict.dtd\\\">\u001b[39m\n\u001b[31m<html><head>\u001b[39m\n\u001b[31m<title>404 Not Found</title>\u001b[39m\n\u001b[31m</head><body>\u001b[39m\n\u001b[31m<h1>Not Found</h1>\u001b[39m\n\u001b[31m<p>The requested URL was not found on this server.</p>\u001b[39m\n\u001b[31m</body></html>\u001b[39m\n\u001b[31m\"\u001b[39m\n\n  126 |     const html = await resp.text();\n  127 |     await ctx.dispose();\n> 128 |     expect(html).toContain(\"window.location.href='/hub/'\");\n      |                  ^\n  129 |     expect(html).toContain('🏡');\n  130 |   });\n  131 |\n    at /var/www/360lm/tests/vehicle.spec.js:128:18"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 0,
                      "startTime": "2026-07-12T13:17:33.198Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/vehicle-Vehicle-Log-—-Hub--07118--header-has-Hub-home-button-android-chrome/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/vehicle.spec.js",
                        "column": 18,
                        "line": 128
                      }
                    },
                    {
                      "workerIndex": 173,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 71,
                      "error": {
                        "message": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoContain\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m) // indexOf\u001b[22m\n\nExpected substring: \u001b[32m\"window.location.href='/hub/'\"\u001b[39m\nReceived string:    \u001b[31m\"<!DOCTYPE HTML PUBLIC \\\"-//W3C//DTD HTML 4.01//EN\\\" \\\"http://www.w3.org/TR/html4/strict.dtd\\\">\u001b[39m\n\u001b[31m<html><head>\u001b[39m\n\u001b[31m<title>404 Not Found</title>\u001b[39m\n\u001b[31m</head><body>\u001b[39m\n\u001b[31m<h1>Not Found</h1>\u001b[39m\n\u001b[31m<p>The requested URL was not found on this server.</p>\u001b[39m\n\u001b[31m</body></html>\u001b[39m\n\u001b[31m\"\u001b[39m",
                        "stack": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoContain\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m) // indexOf\u001b[22m\n\nExpected substring: \u001b[32m\"window.location.href='/hub/'\"\u001b[39m\nReceived string:    \u001b[31m\"<!DOCTYPE HTML PUBLIC \\\"-//W3C//DTD HTML 4.01//EN\\\" \\\"http://www.w3.org/TR/html4/strict.dtd\\\">\u001b[39m\n\u001b[31m<html><head>\u001b[39m\n\u001b[31m<title>404 Not Found</title>\u001b[39m\n\u001b[31m</head><body>\u001b[39m\n\u001b[31m<h1>Not Found</h1>\u001b[39m\n\u001b[31m<p>The requested URL was not found on this server.</p>\u001b[39m\n\u001b[31m</body></html>\u001b[39m\n\u001b[31m\"\u001b[39m\n    at /var/www/360lm/tests/vehicle.spec.js:128:18",
                        "location": {
                          "file": "/var/www/360lm/tests/vehicle.spec.js",
                          "column": 18,
                          "line": 128
                        },
                        "snippet": "  126 |     const html = await resp.text();\n  127 |     await ctx.dispose();\n> 128 |     expect(html).toContain(\"window.location.href='/hub/'\");\n      |                  ^\n  129 |     expect(html).toContain('🏡');\n  130 |   });\n  131 |"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/vehicle.spec.js",
                            "column": 18,
                            "line": 128
                          },
                          "message": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoContain\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m) // indexOf\u001b[22m\n\nExpected substring: \u001b[32m\"window.location.href='/hub/'\"\u001b[39m\nReceived string:    \u001b[31m\"<!DOCTYPE HTML PUBLIC \\\"-//W3C//DTD HTML 4.01//EN\\\" \\\"http://www.w3.org/TR/html4/strict.dtd\\\">\u001b[39m\n\u001b[31m<html><head>\u001b[39m\n\u001b[31m<title>404 Not Found</title>\u001b[39m\n\u001b[31m</head><body>\u001b[39m\n\u001b[31m<h1>Not Found</h1>\u001b[39m\n\u001b[31m<p>The requested URL was not found on this server.</p>\u001b[39m\n\u001b[31m</body></html>\u001b[39m\n\u001b[31m\"\u001b[39m\n\n  126 |     const html = await resp.text();\n  127 |     await ctx.dispose();\n> 128 |     expect(html).toContain(\"window.location.href='/hub/'\");\n      |                  ^\n  129 |     expect(html).toContain('🏡');\n  130 |   });\n  131 |\n    at /var/www/360lm/tests/vehicle.spec.js:128:18"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 1,
                      "startTime": "2026-07-12T13:17:34.214Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/vehicle-Vehicle-Log-—-Hub--07118--header-has-Hub-home-button-android-chrome-retry1/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/vehicle.spec.js",
                        "column": 18,
                        "line": 128
                      }
                    }
                  ],
                  "status": "unexpected"
                }
              ],
              "id": "084e7535606ab6e45f08-c65046058d915619ea54",
              "file": "vehicle.spec.js",
              "line": 123,
              "column": 3
            },
            {
              "title": "sw.js version is v30 or later",
              "ok": false,
              "tags": [],
              "tests": [
                {
                  "timeout": 30000,
                  "annotations": [],
                  "expectedStatus": "passed",
                  "projectId": "android-chrome",
                  "projectName": "android-chrome",
                  "results": [
                    {
                      "workerIndex": 174,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 77,
                      "error": {
                        "message": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoMatch\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m)\u001b[22m\n\nExpected pattern: \u001b[32m/vehicle-v[3-9]\\d*/\u001b[39m\nReceived string:  \u001b[31m\"<!DOCTYPE HTML PUBLIC \\\"-//W3C//DTD HTML 4.01//EN\\\" \\\"http://www.w3.org/TR/html4/strict.dtd\\\">\u001b[39m\n\u001b[31m<html><head>\u001b[39m\n\u001b[31m<title>404 Not Found</title>\u001b[39m\n\u001b[31m</head><body>\u001b[39m\n\u001b[31m<h1>Not Found</h1>\u001b[39m\n\u001b[31m<p>The requested URL was not found on this server.</p>\u001b[39m\n\u001b[31m</body></html>\u001b[39m\n\u001b[31m\"\u001b[39m",
                        "stack": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoMatch\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m)\u001b[22m\n\nExpected pattern: \u001b[32m/vehicle-v[3-9]\\d*/\u001b[39m\nReceived string:  \u001b[31m\"<!DOCTYPE HTML PUBLIC \\\"-//W3C//DTD HTML 4.01//EN\\\" \\\"http://www.w3.org/TR/html4/strict.dtd\\\">\u001b[39m\n\u001b[31m<html><head>\u001b[39m\n\u001b[31m<title>404 Not Found</title>\u001b[39m\n\u001b[31m</head><body>\u001b[39m\n\u001b[31m<h1>Not Found</h1>\u001b[39m\n\u001b[31m<p>The requested URL was not found on this server.</p>\u001b[39m\n\u001b[31m</body></html>\u001b[39m\n\u001b[31m\"\u001b[39m\n    at /var/www/360lm/tests/vehicle.spec.js:137:18",
                        "location": {
                          "file": "/var/www/360lm/tests/vehicle.spec.js",
                          "column": 18,
                          "line": 137
                        },
                        "snippet": "  135 |     const text = await resp.text();\n  136 |     await ctx.dispose();\n> 137 |     expect(text).toMatch(/vehicle-v[3-9]\\d*/);\n      |                  ^\n  138 |   });\n  139 | });\n  140 |"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/vehicle.spec.js",
                            "column": 18,
                            "line": 137
                          },
                          "message": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoMatch\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m)\u001b[22m\n\nExpected pattern: \u001b[32m/vehicle-v[3-9]\\d*/\u001b[39m\nReceived string:  \u001b[31m\"<!DOCTYPE HTML PUBLIC \\\"-//W3C//DTD HTML 4.01//EN\\\" \\\"http://www.w3.org/TR/html4/strict.dtd\\\">\u001b[39m\n\u001b[31m<html><head>\u001b[39m\n\u001b[31m<title>404 Not Found</title>\u001b[39m\n\u001b[31m</head><body>\u001b[39m\n\u001b[31m<h1>Not Found</h1>\u001b[39m\n\u001b[31m<p>The requested URL was not found on this server.</p>\u001b[39m\n\u001b[31m</body></html>\u001b[39m\n\u001b[31m\"\u001b[39m\n\n  135 |     const text = await resp.text();\n  136 |     await ctx.dispose();\n> 137 |     expect(text).toMatch(/vehicle-v[3-9]\\d*/);\n      |                  ^\n  138 |   });\n  139 | });\n  140 |\n    at /var/www/360lm/tests/vehicle.spec.js:137:18"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 0,
                      "startTime": "2026-07-12T13:17:35.186Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/vehicle-Vehicle-Log-—-Hub--8adc1--js-version-is-v30-or-later-android-chrome/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/vehicle.spec.js",
                        "column": 18,
                        "line": 137
                      }
                    },
                    {
                      "workerIndex": 175,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 68,
                      "error": {
                        "message": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoMatch\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m)\u001b[22m\n\nExpected pattern: \u001b[32m/vehicle-v[3-9]\\d*/\u001b[39m\nReceived string:  \u001b[31m\"<!DOCTYPE HTML PUBLIC \\\"-//W3C//DTD HTML 4.01//EN\\\" \\\"http://www.w3.org/TR/html4/strict.dtd\\\">\u001b[39m\n\u001b[31m<html><head>\u001b[39m\n\u001b[31m<title>404 Not Found</title>\u001b[39m\n\u001b[31m</head><body>\u001b[39m\n\u001b[31m<h1>Not Found</h1>\u001b[39m\n\u001b[31m<p>The requested URL was not found on this server.</p>\u001b[39m\n\u001b[31m</body></html>\u001b[39m\n\u001b[31m\"\u001b[39m",
                        "stack": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoMatch\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m)\u001b[22m\n\nExpected pattern: \u001b[32m/vehicle-v[3-9]\\d*/\u001b[39m\nReceived string:  \u001b[31m\"<!DOCTYPE HTML PUBLIC \\\"-//W3C//DTD HTML 4.01//EN\\\" \\\"http://www.w3.org/TR/html4/strict.dtd\\\">\u001b[39m\n\u001b[31m<html><head>\u001b[39m\n\u001b[31m<title>404 Not Found</title>\u001b[39m\n\u001b[31m</head><body>\u001b[39m\n\u001b[31m<h1>Not Found</h1>\u001b[39m\n\u001b[31m<p>The requested URL was not found on this server.</p>\u001b[39m\n\u001b[31m</body></html>\u001b[39m\n\u001b[31m\"\u001b[39m\n    at /var/www/360lm/tests/vehicle.spec.js:137:18",
                        "location": {
                          "file": "/var/www/360lm/tests/vehicle.spec.js",
                          "column": 18,
                          "line": 137
                        },
                        "snippet": "  135 |     const text = await resp.text();\n  136 |     await ctx.dispose();\n> 137 |     expect(text).toMatch(/vehicle-v[3-9]\\d*/);\n      |                  ^\n  138 |   });\n  139 | });\n  140 |"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/vehicle.spec.js",
                            "column": 18,
                            "line": 137
                          },
                          "message": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoMatch\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m)\u001b[22m\n\nExpected pattern: \u001b[32m/vehicle-v[3-9]\\d*/\u001b[39m\nReceived string:  \u001b[31m\"<!DOCTYPE HTML PUBLIC \\\"-//W3C//DTD HTML 4.01//EN\\\" \\\"http://www.w3.org/TR/html4/strict.dtd\\\">\u001b[39m\n\u001b[31m<html><head>\u001b[39m\n\u001b[31m<title>404 Not Found</title>\u001b[39m\n\u001b[31m</head><body>\u001b[39m\n\u001b[31m<h1>Not Found</h1>\u001b[39m\n\u001b[31m<p>The requested URL was not found on this server.</p>\u001b[39m\n\u001b[31m</body></html>\u001b[39m\n\u001b[31m\"\u001b[39m\n\n  135 |     const text = await resp.text();\n  136 |     await ctx.dispose();\n> 137 |     expect(text).toMatch(/vehicle-v[3-9]\\d*/);\n      |                  ^\n  138 |   });\n  139 | });\n  140 |\n    at /var/www/360lm/tests/vehicle.spec.js:137:18"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 1,
                      "startTime": "2026-07-12T13:17:36.137Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/vehicle-Vehicle-Log-—-Hub--8adc1--js-version-is-v30-or-later-android-chrome-retry1/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/vehicle.spec.js",
                        "column": 18,
                        "line": 137
                      }
                    }
                  ],
                  "status": "unexpected"
                }
              ],
              "id": "084e7535606ab6e45f08-6f06cad994967ee91b8e",
              "file": "vehicle.spec.js",
              "line": 132,
              "column": 3
            },
            {
              "title": "dashboard header has Hub home button",
              "ok": false,
              "tags": [],
              "tests": [
                {
                  "timeout": 30000,
                  "annotations": [],
                  "expectedStatus": "passed",
                  "projectId": "desktop-chrome",
                  "projectName": "desktop-chrome",
                  "results": [
                    {
                      "workerIndex": 348,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 86,
                      "error": {
                        "message": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoContain\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m) // indexOf\u001b[22m\n\nExpected substring: \u001b[32m\"window.location.href='/hub/'\"\u001b[39m\nReceived string:    \u001b[31m\"<!DOCTYPE HTML PUBLIC \\\"-//W3C//DTD HTML 4.01//EN\\\" \\\"http://www.w3.org/TR/html4/strict.dtd\\\">\u001b[39m\n\u001b[31m<html><head>\u001b[39m\n\u001b[31m<title>404 Not Found</title>\u001b[39m\n\u001b[31m</head><body>\u001b[39m\n\u001b[31m<h1>Not Found</h1>\u001b[39m\n\u001b[31m<p>The requested URL was not found on this server.</p>\u001b[39m\n\u001b[31m</body></html>\u001b[39m\n\u001b[31m\"\u001b[39m",
                        "stack": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoContain\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m) // indexOf\u001b[22m\n\nExpected substring: \u001b[32m\"window.location.href='/hub/'\"\u001b[39m\nReceived string:    \u001b[31m\"<!DOCTYPE HTML PUBLIC \\\"-//W3C//DTD HTML 4.01//EN\\\" \\\"http://www.w3.org/TR/html4/strict.dtd\\\">\u001b[39m\n\u001b[31m<html><head>\u001b[39m\n\u001b[31m<title>404 Not Found</title>\u001b[39m\n\u001b[31m</head><body>\u001b[39m\n\u001b[31m<h1>Not Found</h1>\u001b[39m\n\u001b[31m<p>The requested URL was not found on this server.</p>\u001b[39m\n\u001b[31m</body></html>\u001b[39m\n\u001b[31m\"\u001b[39m\n    at /var/www/360lm/tests/vehicle.spec.js:128:18",
                        "location": {
                          "file": "/var/www/360lm/tests/vehicle.spec.js",
                          "column": 18,
                          "line": 128
                        },
                        "snippet": "  126 |     const html = await resp.text();\n  127 |     await ctx.dispose();\n> 128 |     expect(html).toContain(\"window.location.href='/hub/'\");\n      |                  ^\n  129 |     expect(html).toContain('🏡');\n  130 |   });\n  131 |"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/vehicle.spec.js",
                            "column": 18,
                            "line": 128
                          },
                          "message": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoContain\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m) // indexOf\u001b[22m\n\nExpected substring: \u001b[32m\"window.location.href='/hub/'\"\u001b[39m\nReceived string:    \u001b[31m\"<!DOCTYPE HTML PUBLIC \\\"-//W3C//DTD HTML 4.01//EN\\\" \\\"http://www.w3.org/TR/html4/strict.dtd\\\">\u001b[39m\n\u001b[31m<html><head>\u001b[39m\n\u001b[31m<title>404 Not Found</title>\u001b[39m\n\u001b[31m</head><body>\u001b[39m\n\u001b[31m<h1>Not Found</h1>\u001b[39m\n\u001b[31m<p>The requested URL was not found on this server.</p>\u001b[39m\n\u001b[31m</body></html>\u001b[39m\n\u001b[31m\"\u001b[39m\n\n  126 |     const html = await resp.text();\n  127 |     await ctx.dispose();\n> 128 |     expect(html).toContain(\"window.location.href='/hub/'\");\n      |                  ^\n  129 |     expect(html).toContain('🏡');\n  130 |   });\n  131 |\n    at /var/www/360lm/tests/vehicle.spec.js:128:18"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 0,
                      "startTime": "2026-07-12T13:37:32.956Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/vehicle-Vehicle-Log-—-Hub--07118--header-has-Hub-home-button-desktop-chrome/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/vehicle.spec.js",
                        "column": 18,
                        "line": 128
                      }
                    },
                    {
                      "workerIndex": 349,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 54,
                      "error": {
                        "message": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoContain\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m) // indexOf\u001b[22m\n\nExpected substring: \u001b[32m\"window.location.href='/hub/'\"\u001b[39m\nReceived string:    \u001b[31m\"<!DOCTYPE HTML PUBLIC \\\"-//W3C//DTD HTML 4.01//EN\\\" \\\"http://www.w3.org/TR/html4/strict.dtd\\\">\u001b[39m\n\u001b[31m<html><head>\u001b[39m\n\u001b[31m<title>404 Not Found</title>\u001b[39m\n\u001b[31m</head><body>\u001b[39m\n\u001b[31m<h1>Not Found</h1>\u001b[39m\n\u001b[31m<p>The requested URL was not found on this server.</p>\u001b[39m\n\u001b[31m</body></html>\u001b[39m\n\u001b[31m\"\u001b[39m",
                        "stack": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoContain\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m) // indexOf\u001b[22m\n\nExpected substring: \u001b[32m\"window.location.href='/hub/'\"\u001b[39m\nReceived string:    \u001b[31m\"<!DOCTYPE HTML PUBLIC \\\"-//W3C//DTD HTML 4.01//EN\\\" \\\"http://www.w3.org/TR/html4/strict.dtd\\\">\u001b[39m\n\u001b[31m<html><head>\u001b[39m\n\u001b[31m<title>404 Not Found</title>\u001b[39m\n\u001b[31m</head><body>\u001b[39m\n\u001b[31m<h1>Not Found</h1>\u001b[39m\n\u001b[31m<p>The requested URL was not found on this server.</p>\u001b[39m\n\u001b[31m</body></html>\u001b[39m\n\u001b[31m\"\u001b[39m\n    at /var/www/360lm/tests/vehicle.spec.js:128:18",
                        "location": {
                          "file": "/var/www/360lm/tests/vehicle.spec.js",
                          "column": 18,
                          "line": 128
                        },
                        "snippet": "  126 |     const html = await resp.text();\n  127 |     await ctx.dispose();\n> 128 |     expect(html).toContain(\"window.location.href='/hub/'\");\n      |                  ^\n  129 |     expect(html).toContain('🏡');\n  130 |   });\n  131 |"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/vehicle.spec.js",
                            "column": 18,
                            "line": 128
                          },
                          "message": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoContain\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m) // indexOf\u001b[22m\n\nExpected substring: \u001b[32m\"window.location.href='/hub/'\"\u001b[39m\nReceived string:    \u001b[31m\"<!DOCTYPE HTML PUBLIC \\\"-//W3C//DTD HTML 4.01//EN\\\" \\\"http://www.w3.org/TR/html4/strict.dtd\\\">\u001b[39m\n\u001b[31m<html><head>\u001b[39m\n\u001b[31m<title>404 Not Found</title>\u001b[39m\n\u001b[31m</head><body>\u001b[39m\n\u001b[31m<h1>Not Found</h1>\u001b[39m\n\u001b[31m<p>The requested URL was not found on this server.</p>\u001b[39m\n\u001b[31m</body></html>\u001b[39m\n\u001b[31m\"\u001b[39m\n\n  126 |     const html = await resp.text();\n  127 |     await ctx.dispose();\n> 128 |     expect(html).toContain(\"window.location.href='/hub/'\");\n      |                  ^\n  129 |     expect(html).toContain('🏡');\n  130 |   });\n  131 |\n    at /var/www/360lm/tests/vehicle.spec.js:128:18"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 1,
                      "startTime": "2026-07-12T13:37:33.855Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/vehicle-Vehicle-Log-—-Hub--07118--header-has-Hub-home-button-desktop-chrome-retry1/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/vehicle.spec.js",
                        "column": 18,
                        "line": 128
                      }
                    }
                  ],
                  "status": "unexpected"
                }
              ],
              "id": "084e7535606ab6e45f08-f4a712a3829d53a9aed5",
              "file": "vehicle.spec.js",
              "line": 123,
              "column": 3
            },
            {
              "title": "sw.js version is v30 or later",
              "ok": false,
              "tags": [],
              "tests": [
                {
                  "timeout": 30000,
                  "annotations": [],
                  "expectedStatus": "passed",
                  "projectId": "desktop-chrome",
                  "projectName": "desktop-chrome",
                  "results": [
                    {
                      "workerIndex": 350,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 56,
                      "error": {
                        "message": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoMatch\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m)\u001b[22m\n\nExpected pattern: \u001b[32m/vehicle-v[3-9]\\d*/\u001b[39m\nReceived string:  \u001b[31m\"<!DOCTYPE HTML PUBLIC \\\"-//W3C//DTD HTML 4.01//EN\\\" \\\"http://www.w3.org/TR/html4/strict.dtd\\\">\u001b[39m\n\u001b[31m<html><head>\u001b[39m\n\u001b[31m<title>404 Not Found</title>\u001b[39m\n\u001b[31m</head><body>\u001b[39m\n\u001b[31m<h1>Not Found</h1>\u001b[39m\n\u001b[31m<p>The requested URL was not found on this server.</p>\u001b[39m\n\u001b[31m</body></html>\u001b[39m\n\u001b[31m\"\u001b[39m",
                        "stack": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoMatch\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m)\u001b[22m\n\nExpected pattern: \u001b[32m/vehicle-v[3-9]\\d*/\u001b[39m\nReceived string:  \u001b[31m\"<!DOCTYPE HTML PUBLIC \\\"-//W3C//DTD HTML 4.01//EN\\\" \\\"http://www.w3.org/TR/html4/strict.dtd\\\">\u001b[39m\n\u001b[31m<html><head>\u001b[39m\n\u001b[31m<title>404 Not Found</title>\u001b[39m\n\u001b[31m</head><body>\u001b[39m\n\u001b[31m<h1>Not Found</h1>\u001b[39m\n\u001b[31m<p>The requested URL was not found on this server.</p>\u001b[39m\n\u001b[31m</body></html>\u001b[39m\n\u001b[31m\"\u001b[39m\n    at /var/www/360lm/tests/vehicle.spec.js:137:18",
                        "location": {
                          "file": "/var/www/360lm/tests/vehicle.spec.js",
                          "column": 18,
                          "line": 137
                        },
                        "snippet": "  135 |     const text = await resp.text();\n  136 |     await ctx.dispose();\n> 137 |     expect(text).toMatch(/vehicle-v[3-9]\\d*/);\n      |                  ^\n  138 |   });\n  139 | });\n  140 |"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/vehicle.spec.js",
                            "column": 18,
                            "line": 137
                          },
                          "message": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoMatch\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m)\u001b[22m\n\nExpected pattern: \u001b[32m/vehicle-v[3-9]\\d*/\u001b[39m\nReceived string:  \u001b[31m\"<!DOCTYPE HTML PUBLIC \\\"-//W3C//DTD HTML 4.01//EN\\\" \\\"http://www.w3.org/TR/html4/strict.dtd\\\">\u001b[39m\n\u001b[31m<html><head>\u001b[39m\n\u001b[31m<title>404 Not Found</title>\u001b[39m\n\u001b[31m</head><body>\u001b[39m\n\u001b[31m<h1>Not Found</h1>\u001b[39m\n\u001b[31m<p>The requested URL was not found on this server.</p>\u001b[39m\n\u001b[31m</body></html>\u001b[39m\n\u001b[31m\"\u001b[39m\n\n  135 |     const text = await resp.text();\n  136 |     await ctx.dispose();\n> 137 |     expect(text).toMatch(/vehicle-v[3-9]\\d*/);\n      |                  ^\n  138 |   });\n  139 | });\n  140 |\n    at /var/www/360lm/tests/vehicle.spec.js:137:18"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 0,
                      "startTime": "2026-07-12T13:37:34.658Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/vehicle-Vehicle-Log-—-Hub--8adc1--js-version-is-v30-or-later-desktop-chrome/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/vehicle.spec.js",
                        "column": 18,
                        "line": 137
                      }
                    },
                    {
                      "workerIndex": 351,
                      "parallelIndex": 0,
                      "status": "failed",
                      "duration": 54,
                      "error": {
                        "message": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoMatch\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m)\u001b[22m\n\nExpected pattern: \u001b[32m/vehicle-v[3-9]\\d*/\u001b[39m\nReceived string:  \u001b[31m\"<!DOCTYPE HTML PUBLIC \\\"-//W3C//DTD HTML 4.01//EN\\\" \\\"http://www.w3.org/TR/html4/strict.dtd\\\">\u001b[39m\n\u001b[31m<html><head>\u001b[39m\n\u001b[31m<title>404 Not Found</title>\u001b[39m\n\u001b[31m</head><body>\u001b[39m\n\u001b[31m<h1>Not Found</h1>\u001b[39m\n\u001b[31m<p>The requested URL was not found on this server.</p>\u001b[39m\n\u001b[31m</body></html>\u001b[39m\n\u001b[31m\"\u001b[39m",
                        "stack": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoMatch\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m)\u001b[22m\n\nExpected pattern: \u001b[32m/vehicle-v[3-9]\\d*/\u001b[39m\nReceived string:  \u001b[31m\"<!DOCTYPE HTML PUBLIC \\\"-//W3C//DTD HTML 4.01//EN\\\" \\\"http://www.w3.org/TR/html4/strict.dtd\\\">\u001b[39m\n\u001b[31m<html><head>\u001b[39m\n\u001b[31m<title>404 Not Found</title>\u001b[39m\n\u001b[31m</head><body>\u001b[39m\n\u001b[31m<h1>Not Found</h1>\u001b[39m\n\u001b[31m<p>The requested URL was not found on this server.</p>\u001b[39m\n\u001b[31m</body></html>\u001b[39m\n\u001b[31m\"\u001b[39m\n    at /var/www/360lm/tests/vehicle.spec.js:137:18",
                        "location": {
                          "file": "/var/www/360lm/tests/vehicle.spec.js",
                          "column": 18,
                          "line": 137
                        },
                        "snippet": "  135 |     const text = await resp.text();\n  136 |     await ctx.dispose();\n> 137 |     expect(text).toMatch(/vehicle-v[3-9]\\d*/);\n      |                  ^\n  138 |   });\n  139 | });\n  140 |"
                      },
                      "errors": [
                        {
                          "location": {
                            "file": "/var/www/360lm/tests/vehicle.spec.js",
                            "column": 18,
                            "line": 137
                          },
                          "message": "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoMatch\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m)\u001b[22m\n\nExpected pattern: \u001b[32m/vehicle-v[3-9]\\d*/\u001b[39m\nReceived string:  \u001b[31m\"<!DOCTYPE HTML PUBLIC \\\"-//W3C//DTD HTML 4.01//EN\\\" \\\"http://www.w3.org/TR/html4/strict.dtd\\\">\u001b[39m\n\u001b[31m<html><head>\u001b[39m\n\u001b[31m<title>404 Not Found</title>\u001b[39m\n\u001b[31m</head><body>\u001b[39m\n\u001b[31m<h1>Not Found</h1>\u001b[39m\n\u001b[31m<p>The requested URL was not found on this server.</p>\u001b[39m\n\u001b[31m</body></html>\u001b[39m\n\u001b[31m\"\u001b[39m\n\n  135 |     const text = await resp.text();\n  136 |     await ctx.dispose();\n> 137 |     expect(text).toMatch(/vehicle-v[3-9]\\d*/);\n      |                  ^\n  138 |   });\n  139 | });\n  140 |\n    at /var/www/360lm/tests/vehicle.spec.js:137:18"
                        }
                      ],
                      "stdout": [],
                      "stderr": [],
                      "retry": 1,
                      "startTime": "2026-07-12T13:37:35.429Z",
                      "annotations": [],
                      "attachments": [
                        {
                          "name": "error-context",
                          "contentType": "text/markdown",
                          "path": "/var/www/360lm/test-results/vehicle-Vehicle-Log-—-Hub--8adc1--js-version-is-v30-or-later-desktop-chrome-retry1/error-context.md"
                        }
                      ],
                      "errorLocation": {
                        "file": "/var/www/360lm/tests/vehicle.spec.js",
                        "column": 18,
                        "line": 137
                      }
                    }
                  ],
                  "status": "unexpected"
                }
              ],
              "id": "084e7535606ab6e45f08-a6266891ef4a68c85905",
              "file": "vehicle.spec.js",
              "line": 132,
              "column": 3
            }
          ]
        }
      ]
    }
  ],
  "errors": [],
  "stats": {
    "startTime": "2026-07-12T12:57:20.259Z",
    "duration": 2415290.639,
    "expected": 60,
    "skipped": 0,
    "unexpected": 176,
    "flaky": 0
  }
}
