{
  "version": 3,
  "sources": ["pwa_offline.js"],
  "sourcesContent": ["/*\n * static/js/pwa_offline.js \u2014 Connection-state + freshness UI (SNOW-377).\n *\n * Ships spec \u00A73.5, \u00A710.1, \u00A710.2, \u00A710.4, \u00A710.7 non-negotiables:\n *\n *   (1) Permanent connectivity symbol \u2014 the ``[data-network-indicator]``\n *       <summary> in ``includes/nav.html``, painted \"using the network\" or\n *       \"not using the network\" on every state change. Not using the\n *       network means ``navigator.onLine === false``, OR a fetch just\n *       failed (an ``AbortError`` is a caller cancelling its own request,\n *       not a connectivity failure, so it is excluded), OR the service\n *       worker is in an offline mode.\n *   (2) Readiness on demand \u2014 pressing that symbol opens the network menu\n *       (``includes/_connection_panel.html``), anchored beneath it in the\n *       header, which answers per downloaded area whether it will work\n *       offline (SNOW-1415), carries one Sync button, names the network\n *       state in one line, and offers the way back to the network.\n *   (3) Network-required buttons \u2014 any element carrying\n *       ``data-network-required`` is set ``disabled`` when offline and\n *       re-enabled when back online. Non-button elements get an\n *       ``aria-disabled=\"true\"`` + ``pointer-events: none`` fallback.\n *   (4) Traffic arrows (SNOW-921) \u2014 the ``[data-traffic-arrow]`` pair\n *       beside that symbol, lit for ~450ms per edge: up when a request\n *       goes out, down when a response comes back.\n *\n * The banner is gone (SNOW-748)\n * ------------------------------\n * (1) and (2) used to be one surface: ``includes/_offline_banner.html``, a\n * full-width strip above the nav that this module revealed when the app was\n * not reaching the server and hid again when it was. It has been deleted.\n * The header now carries a PERMANENT symbol, which is a stronger guarantee\n * than the strip ever gave \u2014 the strip said nothing at all while the app was\n * healthy, so a user only ever learned where to look by losing their\n * connection \u2014 and the strip's own content (the \"last synced\" phrase, the\n * per-state explanation, the reconnect button) moved into the panel behind a\n * press on that symbol.\n *\n * The freshness timestamp is therefore one interaction further away than it\n * was. That is deliberate and is recorded as such in docs/offline-first.md:\n * an always-visible state indicator plus a one-press timestamp beats an\n * indicator that only exists in the failure case.\n *\n * No clocks (SNOW-1415)\n * ----------------------\n * SNOW-482 put a \"last synced\" clock in the panel, and SNOW-928 a second\n * age for the downloads' content. SNOW-1415 removed both, with the\n * ``sync.last_at`` row behind the first: a reader about to lose signal asks\n * whether each area will work, not when the device last reached the\n * server, and the per-area rows answer that (see \"The area rows\" below).\n *\n * Qualifying requests \u2014 ``/api/*`` and other non-static-asset\n * same-origin responses (HTML partials/navigations included) \u2014 also\n * append a row to the ``log:sync`` IndexedDB store (SNOW-482), read\n * out by the manage-page sync-log panel behind the ``sync_log`` waffle\n * flag.\n *\n * Network mode (SNOW-742)\n * ------------------------\n * The UI used to key entirely off ``navigator.onLine`` plus \"did a fetch just\n * fail\". Neither can see the state this ticket added: the service worker has\n * LATCHED offline \u2014 stopped calling the network at all after three\n * consecutive read timeouts \u2014 while ``navigator.onLine`` is still true. That\n * combination is the Underground exactly: the radio is attached, so the\n * platform reports online; there is no route, so nothing completes.\n *\n * So the panel has four states rather than one, and they make different\n * promises. \"Online \u2014 last synced\" means the app is using the network.\n * \"Offline \u2014 last synced\" means requests are still going out and the app will\n * update as soon as one lands. \"Offline mode \u2014 last synced\" means it has\n * stopped asking and is serving downloaded data only. Showing the second\n * while the third is true would be a lie about avalanche data, which is why\n * every predicate below tests the mode and not ``onLine`` alone.\n *\n * The worker owns the mode; ``networkMode`` here is a mirror, kept in step by\n * the ``network-mode`` message in both directions. It is persisted to\n * ``meta:app`` under ``network.mode`` and re-asserted to the worker on boot \u2014\n * a worker terminated while idle comes back in ``'auto'`` having forgotten the\n * latch, and re-asserting is what restores it without putting an IndexedDB\n * read on the worker's own fetch path.\n *\n * The symbol and the switch (SNOW-748)\n * -------------------------------------\n * SNOW-742 put the user's own way into offline mode inside the banner, which\n * revealed only once the connection had already failed, so the control for \"I\n * have signal and am about to lose it\" was unreachable in exactly that case.\n * It moved to ``includes/nav.html``, and the model it follows there is a\n * phone's aeroplane mode: a symbol in the status bar, a switch in the\n * settings. Two elements, both painted by this module (as ``mutation_queue.js``\n * paints the sync badge beside them):\n *\n *   * ``[data-network-indicator]`` \u2014 the header symbol. ALWAYS rendered,\n *     never hidden; this module swaps its glyph, its colour, its accessible\n *     name and its ``data-network-state`` between the two states. It is a\n *     disclosure \u2014 the ``<summary>`` of a ``<details data-network-panel>``,\n *     like the two dropdowns beside it \u2014 so pressing it opens the panel and\n *     pressing it again closes it, and it never changes the network mode.\n *     Opening and closing are the BROWSER's; this module follows the\n *     ``toggle`` event to keep ``aria-expanded`` in step and re-read the\n *     areas, and nav.html's own script adds outside-click, Escape and the\n *     panel's close control. That is why nothing here binds a click on the\n *     symbol, and why the panel needs no help from ``overlays.js``.\n *   * ``[data-network-toggle]`` \u2014 the \"Offline mode\" row, an\n *     ``includes/_switch.html`` checkbox. Revealed here, its ``checked``\n *     state painted here, its ``change`` event bound here.\n *\n * The two are found independently, and each is optional \u2014 a page served from\n * a shell cached before either ticket renders one and not the other, so\n * neither may assume the other is on the page.\n *\n * SNOW-921 moved the row out of the subscriber menu and into the panel the\n * symbol discloses, which is where the state it sets is already reported. The\n * consequence worth knowing here: the row is no longer signed-in only. It was\n * only ever so because the account dropdown was the only menu available to put\n * it in \u2014 the mode itself is a ``meta:app`` row and a service-worker flag, and\n * has never had anything to do with an account. Nothing in this module changes\n * for that; the selectors are unchanged and the row was already optional.\n *\n * SNOW-1415 moved the row again, out of the panel and onto the /offline/\n * debug page (``public/offline.html``). This module still binds it, by id,\n * on whichever page renders it \u2014 it loads everywhere.\n *\n * The two do NOT paint the same predicate, and that is the point. The symbol\n * answers \"is this app reaching the server\", so a dead interface turns it\n * struck-through even in ``'auto'``. The switch answers \"did you ask for\n * offline mode\", so a merely-struggling connection must leave it off \u2014 a\n * switch that flicks itself on when the lift goes over a ridge is reporting\n * someone else's decision as the user's.\n *\n * That move needed a third mode. The worker's ``'offline'`` is an auto-latch\n * and is probed back to ``'auto'`` within thirty seconds of a route\n * reappearing \u2014 correct for a latch, and the exact opposite of what a user who\n * pressed the switch while online asked for. So a user's request is\n * ``'offline-forced'``, which the worker never probes. The three values are\n * ``'auto'``, ``'offline'`` (auto-latched) and ``'offline-forced'`` (the\n * user's), and the comparisons below are NOT interchangeable \u2014 see the\n * ``online`` listener in particular.\n *\n * One connectivity answer, not two (SNOW-748)\n * -------------------------------------------\n * The control shipped with a hole underneath it: ``snowdesk:connectivity-changed``\n * carried ``navigator.onLine`` alone and fired only on an interface\n * transition, so forcing offline mode changed nothing anybody downstream could\n * see. The map's layers menu kept its green sync dots and the basemap download\n * controls stayed enabled \u2014 the app said it was offline in the header while\n * offering to spend the connection.\n *\n * So the broadcast now carries the EFFECTIVE value (``effectiveOnline()`` \u2014\n * interface up AND ``networkMode === 'auto'``), and it fires on every mode\n * change as well as every interface event. ``window.pwaConnectivity.isOnline()``\n * exposes the same value for the consumers that re-read the state when they\n * repaint, so no surface has to derive it for itself. The worker enforces the\n * matching half \u2014 see ``_warmCache``'s forced-mode guard in ``static/js/sw.js``,\n * which refuses a download the UI somehow still dispatches.\n *\n * Every user-facing string for all four states is rendered by\n * ``includes/_connection_panel.html`` and\n * toggled here by ``hidden``, and the symbol's two accessible names by\n * ``includes/nav.html``. Setting any of that text from JavaScript would ship\n * English to every locale \u2014 ``makemessages`` never scans ``static/js`` \u2014\n * which is what ``bin/i18n-lint`` fails on. The menu switch needs no string\n * from here at all: its label is fixed (\"Offline mode\").\n *\n * The traffic arrows (SNOW-921)\n * ------------------------------\n * The header said whether the app COULD reach the server and when it last\n * DID, and nothing at all about whether anything was moving right now. A\n * pan over a downloaded region and a pan spending a roaming connection\n * looked identical.\n *\n * So: two arrows beside the glyph, one lit per edge \u2014 ``pulseTraffic('up')``\n * from the fetch wrapper's entry and htmx's ``beforeRequest``,\n * ``pulseTraffic('down')`` from a resolved response and ``afterOnLoad``. A\n * failed fetch pulses neither on the way back, because nothing came back.\n *\n * Approximate ON PURPOSE, and the ticket says so: they answer \"is anything\n * moving\", not \"how much\". There is no counter, no queue depth and no byte\n * total. The one thing they are careful about is what they DON'T show \u2014\n * ``shouldPulseFor`` applies the sync log's own exclusions, so the pair does\n * not blink at ``/api/telemetry``'s 30-second flush on an idle tab, which\n * would turn an activity lamp into a metronome. Cross-origin requests always\n * pulse, because basemap tiles are the bulkiest thing this app fetches and\n * telling a cached pan from a paid one is the whole point.\n *\n * They are painted by a data attribute, never a class string built here:\n * ``src/css/main.css``'s ``[data-traffic-arrow]`` rules own the rest state,\n * the lit state and the transition, and ``includes/nav.html`` marks the pair\n * ``aria-hidden`` \u2014 a screen reader gets the symbol's accessible name and the\n * panel's summary line, which say the same things in words.\n *\n * The area rows (SNOW-1415)\n * --------------------------\n * One row per downloaded area, each answering \"will this work offline?\"\n * with the verdict /offline/'s panels give: `offline_journey_core.js`'s\n * `stagesFromRecord` and `verdictFor`, over the stored record alone. That\n * core is loaded globally by base.html for this. The tile-capable pages\n * (the map and /offline/) stamp what they verified on each record, so a\n * bulletin page can answer without reading a tile. The header symbol\n * carries the device's overall answer as a small mark: ready, syncing or\n * needs attention, and nothing at all with no areas or no signal.\n *\n * Sync runs `offline_ready_core.js` in place on the map page and navigates\n * to ``/map/?sync=all`` everywhere else; the map runs it on arrival through\n * ``window.pwaConnectionPanel.syncAll``.\n *\n * Deferred to SNOW-375 / follow-ups:\n *   * Pull-to-refresh explicit-network path.\n *   * \"Updated HH:MM\" post-refresh toast.\n */\n\n(function () {\n  'use strict';\n\n  // SNOW-748: the connection-status panel, disclosed by the header symbol.\n  // Replaces the ``pwa-offline-banner`` strip this module used to reveal, and\n  // the bottom-centred toast that briefly stood in for it.\n  const PANEL_ID = 'pwa-connection-panel';\n  // The <details> the panel and the symbol live in (``includes/nav.html``).\n  // It owns open/closed; this module only reads it and follows its ``toggle``.\n  const PANEL_SELECTOR = '[data-network-panel]';\n  // The panel's one CTA \u2014 the way back to the network.\n  const PANEL_CTA_SELECTOR = `#${PANEL_ID} [data-network-reconnect]`;\n  const NETWORK_ATTR = 'data-network-required';\n\n  // SNOW-742: the meta:app key the network mode is persisted under, and the\n  // mode itself as this page last heard it from the worker.\n  //\n  // The worker owns the mode; this is a mirror, for two jobs. It decides which\n  // panel variant to show, and it is re-asserted to the worker on boot \u2014 a\n  // worker that was terminated while idle comes back in 'auto' with no memory\n  // of the latch, and re-asserting is what restores it.\n  //\n  // SNOW-748: that re-assert is now the FAST path rather than the only one.\n  // The worker recovers a user-FORCED mode from this same row by itself (see\n  // sw.js's ``_hydrateNetworkMode``), because a worker recycled with no page\n  // open had nothing to push it and silently went back on the network. An\n  // auto-latch is still restored from here alone, and deliberately \u2014 see that\n  // function for why a stale latch is not worth reinstating.\n  //\n  // SNOW-748: three values, not two \u2014 ``'auto'``, ``'offline'`` (the worker\n  // latched itself after three read timeouts) and ``'offline-forced'`` (the\n  // user switched on \"Offline mode\" in the network menu). Only the middle one\n  // is ever probed, which\n  // is why the user's choice survives being online.\n  const NETWORK_MODE_KEY = 'network.mode';\n  let networkMode = 'auto';\n\n  // SNOW-922 review: which press of the \"Offline mode\" switch is the live\n  // one. The ON direction awaits the worker before it commits, so a result\n  // can land after the user has already changed their mind; the handler\n  // compares the ticket it took against this before acting on anything it\n  // learned. See ``bindNetworkModeControls``.\n  let networkSwitchPress = 0;\n\n  // SNOW-748: the two nav surfaces in includes/nav.html \u2014 the header symbol\n  // (every viewer) and the \"Offline mode\" row in the subscriber menu\n  // (signed-in only). Looked up separately on every paint, because whether\n  // either exists depends on the page and on who is reading it.\n  const NETWORK_INDICATOR_SELECTOR = '[data-network-indicator]';\n  const NETWORK_TOGGLE_SELECTOR = '[data-network-toggle]';\n\n  // SNOW-951/1141/1415: the area block \u2014 the block itself (which carries\n  // the map's url for a press off the map), the list the area rows are\n  // cloned into, the row <template>, the offline note, the one press and\n  // its two outcome lines.\n  const NETWORK_SYNC_SELECTOR = '[data-network-sync]';\n  // SNOW-1415: the shape of the map url the Sync block carries \u2014 a path\n  // from the root, letters, digits, '-', '_' and '/' only, never '//'.\n  const SAME_SITE_PATH = /^\\/(?!\\/)[A-Za-z0-9_\\-\\/]*$/;\n  const NETWORK_SYNC_LIST_SELECTOR = '[data-network-sync-list]';\n  // SNOW-1415: the \"No offline areas yet\" callout, shown with no areas.\n  const NETWORK_EMPTY_SELECTOR = '[data-network-empty]';\n  const NETWORK_SYNC_ROW_SELECTOR = '[data-network-sync-row]';\n  const NETWORK_SYNC_OFFLINE_SELECTOR = '[data-network-sync-offline]';\n  const NETWORK_READY_SELECTOR = '[data-network-ready]';\n  const NETWORK_READY_SUMMARY_SELECTOR = '[data-network-ready-summary]';\n  const NETWORK_READY_QUEUE_SELECTOR = '[data-network-ready-queue]';\n  // SNOW-1141: the store the drain reads, for the \"still waiting\" count.\n  const MUTATION_STORE = 'queue:mutations';\n\n  // SNOW-921: the traffic arrows beside the header glyph \u2014 up for a request\n  // going out, down for a response coming back (``includes/nav.html``).\n  const TRAFFIC_UP_SELECTOR = '[data-traffic-arrow=\"up\"]';\n  const TRAFFIC_DOWN_SELECTOR = '[data-traffic-arrow=\"down\"]';\n  // How long an arrow stays lit after the edge that lit it. Long enough for\n  // a 40ms round trip to register as a deliberate blink rather than a\n  // flicker, short enough that the pair is dark again by the time a user\n  // looks up from a finished page. A second edge inside the window restarts\n  // the clock rather than queueing, so a burst of tile fetches holds the\n  // arrow on \u2014 see ``pulseTraffic``.\n  const TRAFFIC_PULSE_MS = 450;\n  // The switch input inside that row. ``includes/_switch.html`` renders only\n  // an ``id`` \u2014 SNOW-645 removed its raw attribute passthrough as a SAST\n  // finding \u2014 so its callers select it by id, as the downloads sheet does.\n  const NETWORK_SWITCH_ID = 'nav-offline-mode';\n\n  // SNOW-1415: the panel's \"last synced\" clock (SNOW-482) is gone, and\n  // with it the ``sync.last_at`` row it was persisted under. Rows already\n  // written on devices in the field are simply never read again \u2014\n  // harmless, and cheaper than a migration to delete one small row.\n\n  // Extensions treated as static-asset requests for the purposes of the\n  // sync log \u2014 mirrors static/js/sw.js's STATIC_SHELL_EXTENSIONS. These\n  // never represent a meaningful \"sync\" from the user's point of view.\n  const STATIC_ASSET_EXTENSIONS = new Set([\n    '.css',\n    '.js',\n    '.svg',\n    '.png',\n    '.jpg',\n    '.jpeg',\n    '.webp',\n    '.ico',\n    '.woff',\n    '.woff2',\n    '.webmanifest',\n  ]);\n\n  // Same-origin paths that are the page talking to itself rather than\n  // fetching anything the user asked for, so they never belong in the\n  // sync log. static/js/telemetry.js flushes its buffer on a 30s cadence\n  // and on every lifecycle event, which swamped the panel with rows\n  // describing nothing the reader did. Listed with and without the\n  // trailing slash because the receiver is mounted without one and\n  // APPEND_SLASH can resolve either spelling.\n  const SYNC_LOG_EXCLUDED_PATHS = new Set(['/api/telemetry', '/api/telemetry/']);\n\n  /**\n   * Show or hide every element carrying ``data-role=\"<role>\"``.\n   *\n   * Document-scoped rather than scoped to one root, because SNOW-748 split\n   * the roles across two surfaces \u2014 the panel owns the messages, the\n   * explanations and the CTA's two labels, the header symbol owns its glyph\n   * pair and its accessible names \u2014 and both are painted from the same\n   * state in the same pass. Each role is unique in the document.\n   *\n   * @param {string} role\n   * @param {boolean} shown\n   * @returns {void}\n   */\n  function toggleRole(role, shown) {\n    document.querySelectorAll(`[data-role=\"${role}\"]`).forEach((el) => {\n      el.classList.toggle('hidden', !shown);\n    });\n  }\n\n  /**\n   * The stored custom areas, legacy row included (SNOW-928 review).\n   *\n   * A device that still holds its one custom download under the\n   * pre-SNOW-635 ``basemap.customArea`` key, and has not yet loaded a page\n   * that triggers the lazy migration, has a real download that a direct\n   * read of ``basemap.customAreas`` cannot see.\n   *\n   * READ-ONLY, and deliberately not a call to\n   * ``pwaBasemapAreas.readCustomAreas()``. That function MIGRATES as a\n   * side effect of reading, and this module loads on every page in the\n   * site \u2014 including the Django admin \u2014 so routing this read through it\n   * would fire the migration from pages that have nothing to do with\n   * downloads. The precedence rule is mirrored rather than shared: an\n   * ARRAY at the new key wins even when empty (\"already migrated, nothing\n   * left\"), and only an absent or non-array value falls through to the\n   * legacy row.\n   *\n   * @returns {Promise<{value: Array<Object>}>} Shaped like a ``meta:app``\n   *   row so the caller can treat both reads alike.\n   */\n  async function readCustomAreaRecords() {\n    try {\n      const row = await window.pwaDb.get('meta:app', 'basemap.customAreas');\n      if (Array.isArray(row && row.value)) return { value: row.value };\n      const legacyRow = await window.pwaDb.get('meta:app', 'basemap.customArea');\n      const legacy = legacyRow && legacyRow.value;\n      if (!legacy || !Array.isArray(legacy.bbox)) return { value: [] };\n      return { value: [legacy] };\n    } catch (_err) {\n      // Best-effort, like every other read here \u2014 the caller keeps\n      // whatever the rows last said.\n      return { value: [] };\n    }\n  }\n\n  /**\n   * SNOW-1415: re-read the areas this device holds, and repaint the rows\n   * and the header symbol from them.\n   *\n   * Read on boot, on every panel open and after each area of a sync\n   * settles \u2014 the records move only when a sync or a download runs, which\n   * can happen in another tab. Never throws: a device whose IndexedDB will\n   * not answer keeps whatever the panel last said.\n   *\n   * @returns {Promise<void>}\n   */\n  async function refreshAreas() {\n    if (!window.pwaDb || typeof window.pwaDb.get !== 'function') return;\n    try {\n      const rows = await Promise.all([\n        window.pwaDb.get('meta:app', 'basemap.regions'),\n        readCustomAreaRecords(),\n      ]);\n      const areas = [];\n      rows.forEach((row, index) => {\n        const list = (row && row.value) || [];\n        if (!Array.isArray(list)) return;\n        // The two stores in the order they were read above: regions, then\n        // custom areas. Which one an entry came from decides how it is\n        // identified and named, so the index carries that rather than each\n        // entry being sniffed for a field.\n        const isRegion = index === 0;\n        list.forEach((entry) => {\n          if (!entry) return;\n          const identified = syncableArea(entry, isRegion);\n          if (identified) areas.push(identified);\n        });\n      });\n      downloadsAreas = areas;\n    } catch (_err) {\n      // Best-effort, per the docstring.\n    }\n    renderAreaRows();\n  }\n\n  /**\n   * SNOW-951: one stored record as the area rows need it \u2014 an id to act\n   * on, a name to show and (SNOW-1415) the record itself, which the row's\n   * verdict is read from.\n   *\n   * The id is the PINNED BUCKET id for a custom area (`entry.id`) and the\n   * `region_id` for a region. The bucket-id format belongs to\n   * `basemap_download_core.js`'s `areaIdForRegion`, which this module\n   * deliberately does not load, so the region id travels as itself and the\n   * map resolves it there (`resolveSyncAreaId`).\n   *\n   * @param {Object} entry A `basemap.regions` or `basemap.customAreas` row.\n   * @param {boolean} isRegion Which store it came out of.\n   * @returns {{id: string, label: string, record: Object}|null} Null for an\n   *   entry with no id at all \u2014 a half-written record is not something to\n   *   answer for.\n   */\n  function syncableArea(entry, isRegion) {\n    const strings = networkStrings();\n    if (isRegion) {\n      if (!entry.region_id) return null;\n      // The stored name, or the region id itself \u2014 the same fallback\n      // `basemap_downloaded_areas.js` applies, so the two surfaces cannot\n      // name one area differently.\n      return { id: entry.region_id, label: entry.name || entry.region_id, record: entry };\n    }\n    if (!entry.id) return null;\n    let label = entry.name || '';\n    if (!label && Number.isFinite(entry.ordinal)) {\n      label = window.pwaStrings\n        ? window.pwaStrings.interpolate(strings['area-default-name'], { n: entry.ordinal })\n        : '';\n    }\n    return { id: entry.id, label: label || entry.id, record: entry };\n  }\n\n  /**\n   * Whether this page was rendered for a signed-in reader \u2014 base.html's\n   * `pwa-user-id`, empty when signed out. A signed-out reader has no\n   * things of their own, so their areas get no things stage.\n   *\n   * @returns {boolean}\n   */\n  function signedIn() {\n    return !!currentPrincipal();\n  }\n\n  /**\n   * The account this page was rendered for \u2014 base.html's `pwa-user-id`,\n   * '' when signed out. A stamped things answer is only read back to the\n   * account it was stamped for.\n   *\n   * @returns {string}\n   */\n  function currentPrincipal() {\n    const meta = document.querySelector('meta[name=\"pwa-user-id\"]');\n    return (meta && meta.getAttribute('content')) || '';\n  }\n\n  /**\n   * SNOW-1415: one area's answer, from its record alone.\n   *\n   * The stages come from `offline_journey_core.js`'s `stagesFromRecord`\n   * and the verdict from its `verdictFor`, the same verdict /offline/'s\n   * panels give, so the two surfaces cannot disagree about one area.\n   * A \"mostly\" (`refresh`) names its first No: the bulletins and weather,\n   * then the reader's things.\n   *\n   * One rule is the panel's own: an area no tile-capable page has checked\n   * (an unknown map) reads \"Not checked yet\" whatever its content age.\n   * `verdictFor` would let a stale bulletin outrank the unknown map and\n   * say \"Mostly\", which claims the map is there when nothing has looked.\n   *\n   * @param {{record: Object}} area\n   * @returns {{verdict: 'ready'|'refresh'|'open-map'|'unknown', reason: string}}\n   */\n  function areaVerdict(area) {\n    const journey = window.pwaOfflineJourneyCore;\n    if (!journey) return { verdict: 'unknown', reason: '' };\n    const stages = journey.stagesFromRecord(area.record, new Date(), {\n      signedIn: signedIn(),\n      principal: currentPrincipal(),\n    });\n    if (stages.map.status === 'unknown') return { verdict: 'unknown', reason: '' };\n    const verdict = journey.verdictFor(stages);\n    let reason = '';\n    if (verdict === 'refresh') {\n      reason = stages.conditions.status === 'no' ? 'conditions' : 'things';\n    }\n    return { verdict: verdict, reason: reason };\n  }\n\n  /**\n   * SNOW-1415: the whole device's answer, for the header symbol.\n   *\n   *   - `''` \u2014 no areas: the symbol stays the plain signal glyph.\n   *   - `syncing` \u2014 a sync is running.\n   *   - `ready` \u2014 every area answers Yes.\n   *   - `attention` \u2014 any area does not.\n   *\n   * @returns {''|'syncing'|'ready'|'attention'}\n   */\n  function overallReadiness() {\n    if (downloadsAreas.length === 0) return '';\n    if (readyRun && readyRun.running) return 'syncing';\n    return downloadsAreas.every((area) => areaVerdict(area).verdict === 'ready')\n      ? 'ready'\n      : 'attention';\n  }\n\n  /**\n   * SNOW-1415: paint the header symbol's readiness mark.\n   *\n   * No signal is the signal glyph's own answer (struck through), so the\n   * mark is hidden whenever the app is not reaching the server. The state\n   * is also written to `data-offline-readiness` so a test, or a reader in\n   * DevTools, can ask the symbol what it is showing.\n   *\n   * @returns {void}\n   */\n  function paintReadiness() {\n    const reaching = effectiveOnline();\n    const state = reaching ? overallReadiness() : '';\n    const indicator = document.querySelector(NETWORK_INDICATOR_SELECTOR);\n    if (indicator) indicator.setAttribute('data-offline-readiness', state || 'none');\n    for (const name of ['ready', 'syncing', 'attention']) {\n      toggleRole(`readiness-${name}`, state === name);\n    }\n  }\n\n  /**\n   * SNOW-1415: one row per area, cloned from the panel's <template>.\n   *\n   * Rebuilt rather than patched on every read, because the list itself\n   * moves \u2014 an area downloaded or removed in another tab is a different\n   * list. What a sync in flight has learned survives the rebuild in\n   * `readyRun`, which is why it lives outside the DOM.\n   *\n   * HIDDEN with nothing downloaded: a reader with no areas has nothing to\n   * answer for, and a heading over an empty list reads as a fault.\n   *\n   * @returns {void}\n   */\n  function renderAreaRows() {\n    const block = document.querySelector(NETWORK_SYNC_SELECTOR);\n    const list = block ? block.querySelector(NETWORK_SYNC_LIST_SELECTOR) : null;\n    const template = block ? block.querySelector(NETWORK_SYNC_ROW_SELECTOR) : null;\n    if (block && list && template) {\n      list.textContent = '';\n      block.classList.toggle('hidden', downloadsAreas.length === 0);\n      document\n        .querySelector(NETWORK_EMPTY_SELECTOR)\n        ?.classList.toggle('hidden', downloadsAreas.length > 0);\n      for (const area of downloadsAreas) {\n        const row = /** @type {DocumentFragment} */ (template.content.cloneNode(true));\n        const item = row.querySelector('li');\n        // The stamp-on-clone pattern every row on the downloads sheet uses.\n        if (item) item.setAttribute('data-area-id', area.id);\n        const label = row.querySelector('[data-area-label]');\n        if (label) label.textContent = area.label;\n        list.appendChild(row);\n      }\n    }\n    paintReady();\n  }\n\n  /**\n   * SNOW-1141/1415: paint the Sync button, its outcome lines and every\n   * row's answer, and refuse the press while the app is not using the\n   * network.\n   *\n   * Every fixed word is rendered in the template and toggled here; the\n   * sentences that carry a number come from\n   * ``includes/_network_mode_strings.html`` (docs/i18n.md). The refusal is\n   * a DISABLED control plus a sentence rather than a hidden one: a control\n   * that vanishes offline reads as a bug.\n   *\n   * @returns {void}\n   */\n  function paintReady() {\n    const online = effectiveOnline();\n    const running = !!(readyRun && readyRun.running);\n    const strings = networkStrings();\n    const core = window.pwaOfflineReadyCore;\n    const note = document.querySelector(NETWORK_SYNC_OFFLINE_SELECTOR);\n    if (note) note.classList.toggle('hidden', online || downloadsAreas.length === 0);\n\n    const button = /** @type {HTMLButtonElement|null} */ (\n      document.querySelector(NETWORK_READY_SELECTOR)\n    );\n    if (button) {\n      button.disabled = running || !online;\n      button.querySelectorAll('[data-ready-state]').forEach((span) => {\n        const state = span.getAttribute('data-ready-state');\n        span.classList.toggle('hidden', state !== (running ? 'running' : 'idle'));\n      });\n      const progress = button.querySelector('[data-ready-progress]');\n      if (progress && running && core) {\n        progress.textContent = core.progressText(readyRun.step, strings);\n      }\n    }\n\n    const described =\n      readyRun && readyRun.result && core\n        ? core.describe(readyRun.result, strings)\n        : { summary: '', queue: '' };\n    const summary = document.querySelector(NETWORK_READY_SUMMARY_SELECTOR);\n    if (summary) {\n      summary.textContent = described.summary;\n      summary.classList.toggle('hidden', !described.summary);\n    }\n    const queue = document.querySelector(NETWORK_READY_QUEUE_SELECTOR);\n    if (queue) {\n      queue.textContent = described.queue;\n      queue.classList.toggle('hidden', !described.queue);\n    }\n\n    const byId = new Map(downloadsAreas.map((area) => [area.id, area]));\n    document.querySelectorAll(`${NETWORK_SYNC_LIST_SELECTOR} [data-area-id]`).forEach((row) => {\n      const area = byId.get(row.getAttribute('data-area-id') || '');\n      if (!area) return;\n      const inFlight = !!(running && readyRun.current === area.id);\n      const answer = inFlight ? { verdict: 'running', reason: '' } : areaVerdict(area);\n      row.setAttribute('data-area-verdict', answer.verdict);\n      // The dot and the line under the name, each shown for its verdict.\n      row.querySelectorAll('[data-area-status], [data-area-dot]').forEach((span) => {\n        const verdict = span.getAttribute('data-area-status') || span.getAttribute('data-area-dot');\n        span.classList.toggle('hidden', verdict !== answer.verdict);\n      });\n      row.querySelectorAll('[data-area-reason]').forEach((span) => {\n        span.classList.toggle('hidden', span.getAttribute('data-area-reason') !== answer.reason);\n      });\n    });\n    paintReadiness();\n  }\n\n  /**\n   * SNOW-951: the pinned bucket id one row's area id names.\n   *\n   * A custom area IS its bucket id; a region carries its region id, and\n   * turning one into the other belongs to\n   * ``basemap_download_core.js``'s ``areaIdForRegion`` \u2014 the format is\n   * deliberately never assembled by hand anywhere in the tree. That core\n   * is loaded exactly where this matters (the map page, the only place\n   * ``syncArea`` exists at all).\n   *\n   * @param {string} areaId As stamped on the row.\n   * @returns {string}\n   */\n  function resolveSyncAreaId(areaId) {\n    const core = self.pwaBasemapDownloadCore;\n    if (!core || typeof core.areaIdForRegion !== 'function') return areaId;\n    if (typeof core.isCustomAreaId === 'function' && core.isCustomAreaId(areaId)) return areaId;\n    return core.areaIdForRegion(areaId);\n  }\n\n  /**\n   * SNOW-1415: hand the press to the map, which can run it.\n   *\n   * ``/map/?sync=all`` \u2014 the map's downloads module runs the same sync\n   * once it is ready, opens this panel so the rows show the result, and\n   * strips the parameter (`map_basemap_downloads.js`). Only the map can\n   * mend tiles, so only the map can answer for them. The map's url is the\n   * block's own ``data-map-url``, rendered from the url conf.\n   *\n   * @returns {void}\n   */\n  function navigateToMapSync() {\n    const block = document.querySelector(NETWORK_SYNC_SELECTOR);\n    const href = block ? block.getAttribute('data-map-url') : '';\n    // Only a plain same-site path, as `{% url %}` renders it: the attribute\n    // is page text, and nothing but a path of ours is a place to send the\n    // reader (no scheme, no host, no `//`).\n    if (!href || !SAME_SITE_PATH.test(href)) return;\n    self.location.assign(href + '?sync=all');\n  }\n\n  /**\n   * SNOW-1141: how many queued edits are still waiting to send.\n   *\n   * @returns {Promise<number>}\n   */\n  async function waitingMutations() {\n    const db = window.pwaDb;\n    const core = window.pwaOfflineReadyCore;\n    if (!db || typeof db.getAll !== 'function' || !core) return 0;\n    try {\n      return core.waitingCount(await db.getAll(MUTATION_STORE));\n    } catch (_err) {\n      return 0;\n    }\n  }\n\n  /**\n   * SNOW-1141/1415: Sync \u2014 the queue, then every area.\n   *\n   * On the MAP page ``window.pwaBasemapDownloads.syncArea`` exists, so the\n   * press runs in place and reports in the panel it was made from \u2014 the\n   * panel stays open, because the result is the answer to the question the\n   * press asked. Everywhere else it navigates to ``/map/?sync=all``.\n   *\n   * Each area's row re-reads its record as soon as that area settles,\n   * because the sync has just stamped what it verified there.\n   *\n   * @returns {void}\n   */\n  function pressReady() {\n    // The MODE, not the radio \u2014 a forced offline mode leaves\n    // `navigator.onLine` true, and a sync is network use. The button is\n    // disabled in that state; this is the guard for a press that raced\n    // the repaint.\n    if (!effectiveOnline()) return;\n    if (readyRun && readyRun.running) return;\n    const core = window.pwaOfflineReadyCore;\n    const bridge = window.pwaBasemapDownloads;\n    if (!core || !bridge || typeof bridge.syncArea !== 'function') {\n      navigateToMapSync();\n      return;\n    }\n    const run = {\n      running: true,\n      step: /** @type {Object|null} */ (null),\n      current: '',\n      result: /** @type {Object|null} */ (null),\n    };\n    readyRun = run;\n    paintReady();\n    const queue = window.pwaMutationQueue;\n    core\n      .run({\n        drain: () => (queue && typeof queue.drain === 'function' ? queue.drain() : undefined),\n        waiting: waitingMutations,\n        areas: downloadsAreas.map((area) => area.id),\n        syncOne: async (areaId, onStage) =>\n          core.areaOutcome(await bridge.syncArea(resolveSyncAreaId(areaId), onStage)),\n        onStep: (step) => {\n          run.step = step;\n          if (step.phase === 'area') run.current = step.id;\n          if (step.phase === 'done') {\n            run.current = '';\n            if (readyRun === run) refreshAreas();\n            return;\n          }\n          if (readyRun === run) paintReady();\n        },\n      })\n      .then((result) => {\n        run.result = result;\n      })\n      .catch(() => {\n        run.result = { total: downloadsAreas.length, ready: 0, waiting: 0, outcomes: [] };\n      })\n      .finally(() => {\n        run.running = false;\n        if (readyRun !== run) return;\n        refreshAreas();\n      });\n  }\n\n  /**\n   * SNOW-1415: run Sync on arrival \u2014 the map's ``?sync=all``, handed here\n   * by `map_basemap_downloads.js` once its downloads module is ready.\n   *\n   * Opens the panel first, so the rows show the run as it goes, and reads\n   * the areas before pressing, because a press with no areas read yet\n   * would run over nothing.\n   *\n   * @returns {Promise<void>}\n   */\n  async function syncAll() {\n    const disclosure = panelDisclosure();\n    if (disclosure && !disclosure.open) {\n      // `toggle` fires a task later, and its handler clears a SETTLED\n      // press \u2014 so the press waits for it rather than racing it.\n      const opened = new Promise((resolve) => {\n        disclosure.addEventListener('toggle', () => resolve(undefined), { once: true });\n      });\n      disclosure.open = true;\n      await opened;\n    }\n    await refreshAreas();\n    pressReady();\n  }\n\n  /**\n   * SNOW-1141: \"Download again\" on a row whose map is missing.\n   *\n   * On the map page the Manage downloads sheet is right here, so it opens\n   * in place rather than reloading the map; everywhere else the link's own\n   * href (``/map/?panel=downloads``) is followed.\n   *\n   * @param {MouseEvent} event\n   * @returns {void}\n   */\n  function pressRedownload(event) {\n    const manager = window.pwaDownloadsManager;\n    if (!manager || typeof manager.open !== 'function') return;\n    event.preventDefault();\n    const disclosure = panelDisclosure();\n    if (disclosure) disclosure.open = false;\n    manager.open();\n  }\n\n  // SNOW-951/1415: `{id, label, record}` per area, in the order the two\n  // stores yield them, for the rows and the header symbol.\n  let downloadsAreas = [];\n\n  // SNOW-1141: the current Sync press \u2014 `running`, the step it is on, the\n  // area it is on now and, once settled, its result. Module state rather\n  // than DOM state because the rows are rebuilt from the <template> on\n  // every read, and a press still in flight must survive that. A SETTLED\n  // press's summary is cleared on the next open (`syncPanelState`): its\n  // answer belongs to the moment it was given.\n  /** @type {{running: boolean, step: Object|null, current: string, result: Object|null}|null} */\n  let readyRun = null;\n\n  /**\n   * The <details> carrying the symbol and its panel, or null on a page that\n   * renders neither.\n   *\n   * @returns {HTMLDetailsElement|null}\n   */\n  function panelDisclosure() {\n    return document.querySelector(PANEL_SELECTOR);\n  }\n\n  /**\n   * Whether the connection-status panel is currently open.\n   *\n   * Read from the <details>, which is the only thing that knows: the panel\n   * opens and closes natively, and this module never toggles it.\n   *\n   * @returns {boolean}\n   */\n  function panelIsOpen() {\n    const disclosure = panelDisclosure();\n    return !!disclosure && disclosure.open;\n  }\n\n  /**\n   * Follow the disclosure's own open/closed state: paint the symbol's\n   * ``aria-expanded``, and re-read the areas each time the panel opens.\n   *\n   * Called from the ``toggle`` event, so it lands however the panel was\n   * closed \u2014 the symbol again, the \"\u00D7\", Escape, or a click outside \u2014 without\n   * this module having to know about any of those mechanisms.\n   *\n   * @returns {void}\n   */\n  function syncPanelState() {\n    const open = panelIsOpen();\n    const indicator = document.querySelector(NETWORK_INDICATOR_SELECTOR);\n    if (indicator) indicator.setAttribute('aria-expanded', open ? 'true' : 'false');\n    if (open) {\n      // SNOW-1141: a settled press's answer is not carried into a new\n      // opening of the menu; one still running is.\n      if (readyRun && !readyRun.running) readyRun = null;\n      // Re-read what the downloads hold on every open: the records move\n      // only when a sync or a download runs, which can have happened in\n      // another tab since this page loaded.\n      refreshAreas();\n    }\n  }\n\n  /**\n   * Repaint every surface this module owns for the current connection state.\n   * Idempotent \u2014 safe to call on every online/offline transition, every mode\n   * change and every qualifying response.\n   *\n   * ``online`` is \"is the app reaching the server\", which is NOT\n   * ``navigator.onLine``: a fetch that has just failed passes ``false`` while\n   * the platform still reports online, because a radio attached to no route\n   * reports online throughout (the Underground). The offline modes are folded\n   * in here rather than at each call site.\n   *\n   * The panel is repainted whether or not it is open, so pressing the symbol\n   * never shows a frame of the previous state's copy. Its VISIBILITY is not\n   * touched: the <details> opens and closes on the user's press alone.\n   *\n   * @param {boolean} online\n   * @returns {void}\n   */\n  function renderConnectionUi(online) {\n    const reaching = online && networkMode === 'auto';\n    renderNetworkUi(reaching);\n    renderNetworkCopy(reaching);\n    // SNOW-951/1141: and the press's own refusal, which follows the same\n    // effective-online reading the rest of this function does. Only its\n    // STATE, not the list: which areas exist has not changed, and\n    // re-reading IndexedDB on every interface blink would be waste on the\n    // device least able to afford it.\n    paintReady();\n  }\n\n  /**\n   * SNOW-742/748: show the state line and CTA label that match the current\n   * state. SNOW-1415: no explanation under the line (Hugo, in the PR). Every variant of each is rendered server-side by\n   * ``includes/_connection_panel.html`` and\n   * toggled here, so no user-facing string is ever built in JavaScript\n   * (docs/i18n.md).\n   *\n   * Four states. ``reaching`` distinguishes the first two \u2014 the app is using\n   * the network, or it is trying and failing \u2014 and the mode distinguishes the\n   * last two, which agree on more than they differ on. Both offline modes\n   * share the summary line: it answers \"is this app contacting the server\",\n   * and the answer is no either way. They share the CTA, because the way out\n   * is the same, under different labels: \"Try reconnecting\" asserts a\n   * repair, which is false when the user chose the mode while online.\n   *\n   * @param {boolean} reaching \u2014 the app is using the network.\n   * @returns {void}\n   */\n  function renderNetworkCopy(reaching) {\n    const auto = networkMode === 'auto';\n    const forced = networkMode === 'offline-forced';\n    const latched = networkMode === 'offline';\n    toggleRole('online-message', auto && reaching);\n    toggleRole('offline-message', auto && !reaching);\n    toggleRole('latched-message', !auto);\n    // Same button, two labels: \"try reconnecting\" reads as a repair, which is\n    // the wrong promise when nothing is broken and the user simply chose this.\n    toggleRole('reconnect-label', latched);\n    toggleRole('resume-label', forced);\n    // The way back to normal operation, offered only where it does something\n    // \u2014 under either offline mode, and not while the app is merely struggling\n    // and still trying on its own, nor while it is succeeding.\n    const cta = document.querySelector(PANEL_CTA_SELECTOR);\n    if (cta) cta.classList.toggle('hidden', auto);\n  }\n\n  /**\n   * SNOW-748: paint the two nav surfaces this mode owns\n   * (``includes/nav.html``).\n   *\n   * The header symbol is PERMANENT \u2014 it is never hidden, and this function\n   * only ever changes what it says. That is the change SNOW-748's rework\n   * made: an earlier pass hid it in ``'auto'``, on the phone's-aeroplane-glyph\n   * model, which meant the one element telling a user whether their avalanche\n   * data was live existed only once it was not. Two glyphs, two colours and\n   * two accessible names are rendered in the template and swapped here; the\n   * same bit is written to ``data-network-state`` so a reader (or a test) can\n   * ask the symbol what it is showing.\n   *\n   * The menu switch is revealed here rather than rendered visible, the same\n   * contract ``mutation_queue.js`` has with the sync badge beside it: a\n   * control that only works because a script is running must not be on screen\n   * when that script is not.\n   *\n   * The two paint DIFFERENT predicates. The symbol takes ``reaching`` \u2014 a\n   * dead interface strikes it through even in ``'auto'``, which is the whole\n   * point of a permanent indicator. The switch takes the MODE alone: it is\n   * the user's own setting, and flicking itself on because a request timed\n   * out would report the worker's decision as theirs.\n   *\n   * Each element is guarded on its own, not behind a shared early return: the\n   * row renders only for a signed-in user, so an anonymous page has the symbol\n   * and no row, and the symbol must still be painted.\n   *\n   * @param {boolean} reaching \u2014 the app is using the network.\n   * @returns {void}\n   */\n  function renderNetworkUi(reaching) {\n    const indicator = document.querySelector(NETWORK_INDICATOR_SELECTOR);\n    if (indicator) indicator.setAttribute('data-network-state', reaching ? 'online' : 'offline');\n    toggleRole('network-online-icon', reaching);\n    toggleRole('network-offline-icon', !reaching);\n    toggleRole('network-name-online', reaching);\n    toggleRole('network-name-offline', !reaching);\n    const row = document.querySelector(NETWORK_TOGGLE_SELECTOR);\n    if (row) {\n      row.classList.remove('hidden');\n      row.classList.add('flex');\n    }\n    const input = document.getElementById(NETWORK_SWITCH_ID);\n    // ``checked`` on a real checkbox, not an ``aria-checked`` attribute on a\n    // button: ``includes/_switch.html`` is an <input role=\"switch\">, so the\n    // announced state and the drawn track both follow the property for free.\n    // Assigning it fires no ``change`` event, so this cannot loop back into\n    // the handler that called it.\n    if (input) input.checked = networkMode !== 'auto';\n  }\n\n  /**\n   * SNOW-922: the lock-out warning, as one ``window.confirm`` body.\n   *\n   * Read from ``includes/_network_mode_strings.html`` rather than written\n   * inline, because ``makemessages`` never sees a JS literal (docs/i18n.md).\n   * Read lazily rather than at load: this module is deferred and the template\n   * is markup, so it is always there by the time a user presses the switch,\n   * and reading it per press costs nothing measurable against a dialogue.\n   *\n   * The blank line between the two halves is joined HERE, not in the\n   * template: ``pwaStrings.read`` collapses whitespace on read (so the\n   * formatter's wrapping cannot reach the dialogue), which would flatten a\n   * paragraph break written there into a single space.\n   *\n   * @returns {string}\n   */\n  function networkLockoutWarning() {\n    const strings = networkStrings();\n    const warning =\n      strings['lockout-warning'] ||\n      'Snowdesk has not been saved on this device yet, so switching Offline mode on will ' +\n        'stop it opening at all until you switch it off again.';\n    const confirm = strings['lockout-confirm'] || 'Switch it on anyway?';\n    return warning + '\\n\\n' + confirm;\n  }\n\n  // SNOW-922/951: the English every string below falls back to, per\n  // docs/i18n.md \u2014 a page served by a shell cached before the template\n  // existed has nothing to read.\n  //\n  // `var NAME = {` at module scope is not a stylistic choice in a file of\n  // `const`s: `tests/test_js_strings_are_translatable.py` follows a named\n  // fallback object only in that form, and a reader it cannot see reads as\n  // a template nothing consumes. See that module's own note on the shape.\n  var NETWORK_STRING_FALLBACKS = {\n    'lockout-warning':\n      'Snowdesk has not been saved on this device yet, so switching Offline mode on ' +\n      'will stop it opening at all until you switch it off again.',\n    'lockout-confirm': 'Switch it on anyway?',\n    'area-default-name': 'Custom area %(n)s',\n    'ready-running': 'Syncing\u2026',\n    'ready-progress': 'Syncing\u2026 (%(n)s of %(total)s)',\n    'ready-all': 'Ready \u2014 %(total)s areas up to date',\n    'ready-all-one': 'Ready \u2014 1 area up to date',\n    'ready-some': '%(ready)s of %(total)s ready',\n    'queue-waiting': '%(n)s changes still waiting to send',\n    'queue-waiting-one': '1 change still waiting to send',\n    // SNOW-1415: what one area's sync is fetching now (`progressText`).\n    'sync-stage-map': 'Checking the map\u2026 (%(n)s of %(total)s)',\n    'sync-stage-bulletins': 'Saving bulletins\u2026 (%(n)s of %(total)s)',\n    'sync-stage-weather': 'Saving weather\u2026 (%(n)s of %(total)s)',\n    'sync-stage-things': 'Saving your trips and pins\u2026 (%(n)s of %(total)s)',\n  };\n\n  /**\n   * SNOW-922/951: every string this module renders, from\n   * ``includes/_network_mode_strings.html``.\n   *\n   * One reader for the two callers \u2014 the lock-out dialogue and the\n   * \"Get ready for offline\" block \u2014 because ``pwaStrings.read`` takes the whole fallback map at\n   * once and two partial reads of the same template would each have to\n   * carry the other's keys or silently lose them.\n   *\n   * Read lazily rather than at load: this module is deferred and the\n   * template is markup, so it is always there by the time either caller\n   * runs, and re-reading it costs nothing measurable against a dialogue\n   * or an IndexedDB round trip.\n   *\n   * The English literals stay as the JS-side fallback, per docs/i18n.md\n   * \u2014 a page rendered by an older cached shell has no template to read.\n   *\n   * @returns {Object<string, string>}\n   */\n  function networkStrings() {\n    if (!window.pwaStrings) return NETWORK_STRING_FALLBACKS;\n    return window.pwaStrings.read('network-mode-strings-template', NETWORK_STRING_FALLBACKS);\n  }\n\n  /**\n   * SNOW-748: narrow an arbitrary value to one of the three known modes.\n   *\n   * Used on both inbound paths \u2014 the worker's announcement and the persisted\n   * ``meta:app`` row \u2014 because both can carry a value written by a different\n   * version of the code than the one reading it: a shell cached before this\n   * ticket, or a row written by one after it. An unrecognised value becomes\n   * ``'auto'``, which is the only mode that claims nothing.\n   *\n   * @param {*} value\n   * @returns {'auto'|'offline'|'offline-forced'}\n   */\n  function coerceNetworkMode(value) {\n    // SNOW-922: one narrowing rule, owned by pwa_network_mode.js, which\n    // static/offline.html also reads it from. The inline fallback stays for\n    // the load-order case only \u2014 this module is deferred behind that one in\n    // base.html, so in practice the delegate is always there.\n    if (window.pwaNetworkMode) return window.pwaNetworkMode.coerce(value);\n    if (value === 'offline' || value === 'offline-forced') return value;\n    return 'auto';\n  }\n\n  /**\n   * Ask the service worker to change mode, and persist the request so a\n   * restarted worker can be told about it again on the next boot.\n   *\n   * @param {'auto'|'offline'|'offline-forced'} mode\n   */\n  function requestNetworkMode(mode) {\n    networkMode = mode;\n    // SNOW-922: the persist-then-announce pair is pwa_network_mode.js's,\n    // because static/offline.html has to perform exactly the same pair and\n    // two copies of it would drift. The ORDER is the part that matters \u2014\n    // announcing first leaves a window in which the worker holds the new mode\n    // and the disk the old one, and an idle worker recycled inside it comes\n    // back in the mode the user just left. `set` persists first, and the\n    // module's storage adapter prefers `window.pwaDb` where it is loaded, so\n    // an app-side write still goes through the real database layer.\n    if (window.pwaNetworkMode) {\n      window.pwaNetworkMode.set(mode);\n    } else {\n      persistMeta(NETWORK_MODE_KEY, mode);\n    }\n    renderConnectionUi(navigator.onLine);\n    // SNOW-748: the page's own mode changes are broadcast too, not just the\n    // worker's. The toggle's click lands here, and it must not wait for the\n    // worker's echo \u2014 that echo needs a controller, and a page loaded before\n    // the worker activated has none.\n    syncNetworkRequired(effectiveOnline());\n    broadcastConnectivity();\n    // SNOW-922: only on the fallback path \u2014 `pwaNetworkMode.set` above has\n    // already announced, in the right order relative to its own write.\n    if (window.pwaNetworkMode) return;\n    try {\n      navigator.serviceWorker?.controller?.postMessage({ type: 'network-mode', mode });\n    } catch (_err) {\n      // No controller yet (first load before activation), or messaging\n      // unavailable. The persisted row is re-asserted on the next boot, so the\n      // user's choice is not lost \u2014 it just takes effect a load later.\n    }\n  }\n\n  /**\n   * Follow the panel's disclosure, bind its CTA and the menu's \"Offline mode\"\n   * switch; then listen for the worker announcing a mode change it made on\n   * its own (the latch tripping, or a probe finding a route again).\n   */\n  function bindNetworkModeControls() {\n    // SNOW-748: the header symbol is the <summary> of a native disclosure, so\n    // the press that opens and closes the panel is the browser's and nothing\n    // is bound to it here \u2014 which is also what stops this module ever\n    // changing the network mode from the status area, the misread an earlier\n    // pass invited and ``aria-expanded`` (rather than ``aria-pressed``)\n    // promises against.\n    //\n    // ``toggle`` is the one event every close arrives on: the symbol, the\n    // panel's \"\u00D7\", Escape and a click outside (the last three from nav.html's\n    // shared disclosure script) all set ``open``, and this fires for each.\n    panelDisclosure()?.addEventListener('toggle', () => {\n      syncPanelState();\n    });\n    // The way back to the network, and the only one an anonymous reader has:\n    // the \"Offline mode\" switch below lives in the subscriber menu, so a\n    // signed-out user who gets auto-latched can leave that state only here.\n    document.querySelector(PANEL_CTA_SELECTOR)?.addEventListener('click', () => {\n      requestNetworkMode('auto');\n    });\n    // SNOW-1141: the one press, and a tiles-gone row's \"Download again\".\n    // Delegated on the block, because every row is rebuilt on each panel\n    // open and a listener bound to a link would go with the link it was\n    // bound to \u2014 the same reason the downloads sheet and the audit report\n    // both delegate.\n    document.querySelector(NETWORK_SYNC_SELECTOR)?.addEventListener('click', (event) => {\n      const target = /** @type {HTMLElement} */ (event.target);\n      if (!target || !target.closest) return;\n      if (target.closest(NETWORK_READY_SELECTOR)) {\n        pressReady();\n        return;\n      }\n      if (target.closest('[data-area-redownload]')) pressRedownload(event);\n    });\n    // SNOW-1415: the empty state's \"Download an area\" opens the same sheet.\n    document.querySelector(NETWORK_EMPTY_SELECTOR)?.addEventListener('click', (event) => {\n      const target = /** @type {HTMLElement} */ (event.target);\n      if (target && target.closest && target.closest('a')) pressRedownload(event);\n    });\n    // SNOW-748: the menu's \"Offline mode\" switch. Its two directions are not\n    // symmetrical \u2014 going offline asks for ``'offline-forced'`` (a choice,\n    // never probed), while coming back always asks for plain ``'auto'``\n    // whichever offline mode it is leaving, because \"use the network again\"\n    // means the same thing either way.\n    //\n    // ``change``, not ``click``: the control is a real checkbox, so a keyboard\n    // Space and a click on the <label> both arrive here, and the browser has\n    // already flipped ``checked`` by the time it does.\n    document.getElementById(NETWORK_SWITCH_ID)?.addEventListener('change', (event) => {\n      const input = event.target;\n      // SNOW-922 review: every press takes a ticket, and only the newest one\n      // may act on what it learns. The ON path awaits the worker for up to\n      // three seconds, and a user who flips back OFF inside that window used\n      // to get the mode set anyway when the stale callback landed \u2014 their\n      // last action silently losing to their previous one, which on this\n      // particular switch means the app stops calling the server against\n      // their stated wish. A press that has been superseded now does nothing\n      // at all: not the mode, not the dialogue, not the switch's position.\n      //\n      // Deliberately NOT `input.disabled` for the duration. OFF is the\n      // recovery direction \u2014 it is how someone who has just stranded\n      // themselves gets back \u2014 and a control that ignores it for three\n      // seconds is the fault this ticket exists to remove, in miniature.\n      const press = (networkSwitchPress += 1);\n      if (!input.checked) {\n        requestNetworkMode('auto');\n        return;\n      }\n      // SNOW-922: switching ON is the move that can strand the user, so it\n      // asks the worker whether the app would in fact open first. Under\n      // 'offline-forced' every navigation is answered from cache, so a device\n      // with no shell page cached for this account reaches offline.html and\n      // nothing else \u2014 and until this ticket the only control that ended that\n      // state was this switch, inside the app that would not open.\n      //\n      // `canOpenOffline` answers false on any doubt (no worker, no reply\n      // inside its budget), so the worst case is one extra press for someone\n      // whose map really is saved. That is much the cheaper way to be wrong,\n      // and the confirmation names the consequence rather than warning\n      // vaguely: someone who genuinely wants aeroplane mode on a fresh device\n      // can still have it, having been told what it costs.\n      const proceed = window.pwaNetworkMode\n        ? window.pwaNetworkMode.canOpenOffline().then((canOpen) => {\n            // Checked before the dialogue as well as after it: a warning\n            // about a press the user has already taken back is a question\n            // with no right answer.\n            if (press !== networkSwitchPress) return false;\n            if (canOpen) return true;\n            return window.confirm(networkLockoutWarning());\n          })\n        : Promise.resolve(true);\n      proceed.then((confirmed) => {\n        if (press !== networkSwitchPress) return;\n        if (!confirmed) {\n          // Put the switch back where the user left it. Assigning `checked`\n          // fires no `change`, so this cannot re-enter.\n          input.checked = false;\n          renderConnectionUi(navigator.onLine);\n          return;\n        }\n        requestNetworkMode('offline-forced');\n      });\n    });\n    // ``navigator.serviceWorker``'s message queue is disabled until something\n    // enables it \u2014 setting ``onmessage``, or calling this. An\n    // ``addEventListener`` listener alone does NOT enable it, so a message the\n    // worker posts before the queue opens is simply never delivered. In\n    // practice the queue opens at document load and sw_register.js has always\n    // relied on that, but the latch can trip during the initial tile burst,\n    // which is close enough to that boundary to be worth removing the\n    // question. Idempotent, and a no-op once already enabled.\n    try {\n      navigator.serviceWorker?.startMessages?.();\n    } catch (_err) {\n      // Not available (or no container at all) \u2014 fall back to the implicit\n      // load-time enablement, which is what the rest of the app already uses.\n    }\n    navigator.serviceWorker?.addEventListener('message', (event) => {\n      if (!event.data || event.data.type !== 'network-mode') return;\n      // SNOW-748: coerced to the three known values rather than trusted, so a\n      // message from an older or newer worker cannot put an unrenderable\n      // string in the mirror. Anything unrecognised means 'auto' \u2014 the mode\n      // that promises least.\n      networkMode = coerceNetworkMode(event.data.mode);\n      persistMeta(NETWORK_MODE_KEY, networkMode);\n      renderConnectionUi(navigator.onLine);\n      syncNetworkRequired(effectiveOnline());\n      // SNOW-748: a mode change IS a connectivity change for everything that\n      // gates on the broadcast \u2014 the worker latching, a probe lifting it, or\n      // the user pressing the toggle all change whether the network is in\n      // use. Without this the event only ever fired on an interface\n      // transition, so a forced mode left every consumer believing the\n      // network was there.\n      broadcastConnectivity();\n    });\n  }\n\n  /**\n   * Toggle the disabled state of every element carrying\n   * ``data-network-required``. Direct matches use the native\n   * ``disabled`` property where available; matched containers (forms)\n   * cascade the disabled state to their submit-capable children so a\n   * ``<form data-network-required>`` blocks its Submit / Add / Delete\n   * buttons without having to tag each one individually.\n   *\n   * Non-disable-able elements (``<a>``, ``<div>``) fall back to\n   * ``aria-disabled=\"true\"`` + ``pointer-events: none``.\n   *\n   * @param {boolean} online\n   */\n  function syncNetworkRequired(online) {\n    const nodes = document.querySelectorAll(`[${NETWORK_ATTR}]`);\n    nodes.forEach((node) => applyDisabled(node, !online));\n    document\n      .querySelectorAll(\n        `[${NETWORK_ATTR}] button, [${NETWORK_ATTR}] input[type=\"submit\"], [${NETWORK_ATTR}] input[type=\"button\"]`,\n      )\n      .forEach((child) => applyDisabled(child, !online));\n  }\n\n  /**\n   * Apply / remove the disabled state on a single element without\n   * clobbering a pre-existing ``disabled`` attribute set for other\n   * reasons (form validity, etc.). We tag our own mutations with\n   * ``data-was-disabled-offline`` so the reverse transition only\n   * un-disables what we disabled.\n   *\n   * @param {Element} node\n   * @param {boolean} disabled\n   */\n  function applyDisabled(node, disabled) {\n    if (disabled) {\n      if ('disabled' in node && !node.disabled) {\n        node.disabled = true;\n        node.setAttribute('data-was-disabled-offline', '1');\n      }\n      node.setAttribute('aria-disabled', 'true');\n      node.style.pointerEvents = 'none';\n    } else {\n      if (\n        'disabled' in node &&\n        node.getAttribute('data-was-disabled-offline') === '1'\n      ) {\n        node.disabled = false;\n      }\n      node.removeAttribute('aria-disabled');\n      node.style.removeProperty('pointer-events');\n      node.removeAttribute('data-was-disabled-offline');\n    }\n  }\n\n  /**\n   * Best-effort persistence of one ``meta:app`` key. Never throws \u2014\n   * IndexedDB unavailability (private mode, Reset Required) must not\n   * break the in-memory state update.\n   *\n   * @param {string} key\n   * @param {string} value\n   */\n  function persistMeta(key, value) {\n    if (!window.pwaDb || typeof window.pwaDb.put !== 'function') return;\n    try {\n      window.pwaDb.put('meta:app', { key, value }).catch(() => {});\n    } catch (_err) {\n      // Ignore \u2014 persistence is best-effort.\n    }\n  }\n\n  // SNOW-921: the live timer per direction, so a second request inside the\n  // window extends the existing pulse instead of stacking a second one that\n  // would put the arrow out early.\n  const trafficTimers = { up: null, down: null };\n\n  /**\n   * SNOW-921: light one traffic arrow for ``TRAFFIC_PULSE_MS``.\n   *\n   * Deliberately approximate. The pair answers \"is anything moving\", not\n   * \"how much\" \u2014 the ticket says so \u2014 so there is no counting, no queue\n   * depth and no byte total anywhere in here. It is a modem's activity\n   * lamp: an edge lights it, a beat of silence puts it out, and a burst\n   * holds it on because each edge restarts the timer.\n   *\n   * The arrow is painted by a data attribute, not by a class this module\n   * builds. ``src/css/main.css``'s ``[data-traffic-arrow]`` rules own the\n   * rest state, the lit state and the transition between them, which keeps\n   * every colour and duration in the one file the design system asks for\n   * them to live in \u2014 and means this function cannot introduce a raw\n   * Tailwind palette utility for ``bin/ds-lint`` to find.\n   *\n   * A no-op on a page that renders no arrows, which is any page served\n   * from a shell cached before this ticket.\n   *\n   * @param {'up'|'down'} direction\n   * @returns {void}\n   */\n  function pulseTraffic(direction) {\n    const selector = direction === 'up' ? TRAFFIC_UP_SELECTOR : TRAFFIC_DOWN_SELECTOR;\n    const arrow = document.querySelector(selector);\n    if (!arrow) return;\n    arrow.setAttribute('data-active', '');\n    if (trafficTimers[direction] !== null) window.clearTimeout(trafficTimers[direction]);\n    trafficTimers[direction] = window.setTimeout(() => {\n      trafficTimers[direction] = null;\n      arrow.removeAttribute('data-active');\n    }, TRAFFIC_PULSE_MS);\n  }\n\n  /**\n   * SNOW-921: the URL a ``fetch`` call is for, as a string.\n   *\n   * ``fetch`` takes a string, a ``URL`` or a ``Request``, and the traffic\n   * arrows are the only caller that needs to know which \u2014 everything else\n   * in this module reads the RESPONSE. Returns an empty string for\n   * anything unrecognised, which ``shouldPulseFor`` treats as \"pulse\n   * anyway\": an unidentifiable request is still a request, and guessing\n   * wrong here costs one blink.\n   *\n   * @param {*} input \u2014 the first argument passed to ``fetch``.\n   * @returns {string}\n   */\n  function requestUrlOf(input) {\n    if (typeof input === 'string') return input;\n    if (input && typeof input.url === 'string') return input.url;\n    if (input && typeof input.href === 'string') return input.href;\n    return '';\n  }\n\n  /**\n   * SNOW-921: whether a request is worth lighting an arrow for.\n   *\n   * The same exclusions the sync log applies, and for the same reason:\n   * ``static/js/telemetry.js`` flushes its buffer every 30 seconds and on\n   * every lifecycle event, so without this the arrows would blink at the\n   * page talking to itself, for ever, on an idle tab \u2014 which is precisely\n   * the impression an activity lamp must not give. Static assets go the\n   * same way; a cold boot pulling forty of them says nothing a user\n   * wanted to know.\n   *\n   * CROSS-ORIGIN requests always pulse, and that is the interesting half.\n   * Basemap tiles are a different origin, they are the bulkiest thing this\n   * app fetches, and a pan that is quietly serving everything from a\n   * pinned bucket looks exactly like one that is spending a roaming\n   * connection. The arrows are the only surface that can tell those apart.\n   *\n   * @param {string} rawUrl \u2014 as returned by ``requestUrlOf``.\n   * @returns {boolean}\n   */\n  function shouldPulseFor(rawUrl) {\n    if (!rawUrl) return true;\n    try {\n      const parsed = new URL(rawUrl, window.location.href);\n      if (parsed.origin !== window.location.origin) return true;\n      return isLoggableSyncPath(parsed.pathname);\n    } catch (_err) {\n      // Unparseable \u2014 treat it as traffic. One stray blink is a cheaper\n      // failure than a dark lamp on a page that is busy.\n      return true;\n    }\n  }\n\n  /**\n   * Whether a same-origin pathname is a static-asset request that\n   * should never be recorded as a \"sync\" (CSS/JS/images/fonts/the\n   * webmanifest, and anything under ``/static/``).\n   *\n   * @param {string} pathname\n   * @returns {boolean}\n   */\n  function isStaticAssetPath(pathname) {\n    if (pathname.startsWith('/static/')) return true;\n    const dot = pathname.lastIndexOf('.');\n    if (dot === -1) return false;\n    return STATIC_ASSET_EXTENSIONS.has(pathname.slice(dot).toLowerCase());\n  }\n\n  /**\n   * Whether a same-origin pathname is worth recording as a \"sync\" \u2014 a\n   * request the reader would recognise as the app fetching something,\n   * rather than a static asset or the page's own background chatter.\n   *\n   * @param {string} pathname\n   * @returns {boolean}\n   */\n  function isLoggableSyncPath(pathname) {\n    if (SYNC_LOG_EXCLUDED_PATHS.has(pathname)) return false;\n    return !isStaticAssetPath(pathname);\n  }\n\n  /**\n   * Best-effort append to the ``log:sync`` IndexedDB store. Never\n   * throws.\n   *\n   * @param {Date} at\n   * @param {string} pathname\n   */\n  function appendSyncLogEntry(at, pathname) {\n    if (!window.pwaDb || typeof window.pwaDb.appendSyncLog !== 'function') return;\n    try {\n      window.pwaDb.appendSyncLog({ at: at.toISOString(), path: pathname }).catch(() => {});\n    } catch (_err) {\n      // Ignore \u2014 the sync log is a diagnostic nice-to-have.\n    }\n  }\n\n  /**\n   * Called whenever a response header set + resolved URL + status is\n   * available (fetch or HTMX). The ``log:sync`` row (for paths ``isLoggableSyncPath`` accepts) only\n   * advance for a response that is: successful (2xx status), un-stamped\n   * (no ``X-SW-Cache`` header \u2014 i.e. neither a Cache-Storage replay\n   * (``X-SW-Cache: hit``) nor a synthesized offline fallback\n   * (``X-SW-Cache: miss``) served by ``static/js/sw.js``), and\n   * same-origin with a *non-empty* resolved URL. The empty-URL check matters because a synthesized\n   * service-worker fallback (offline cache miss \u2014 see SNOW-490) has\n   * ``url === ''``, which would otherwise resolve against\n   * ``location.href`` and be misclassified as a same-origin success.\n   * ``X-Data-Generated-At`` is always absorbed regardless of cache-hit\n   * status \u2014 a cache-served response still tells the user how old the\n   * data it's showing is.\n   *\n   * That same qualifying-response condition doubles as the symbol's\n   * ``online``-independent recovery path \u2014 see the call to\n   * ``renderConnectionUi(true)`` below.\n   *\n   * @param {(name: string) => string | null} getHeader\n   * @param {string} responseUrl\n   * @param {number} status\n   */\n  function absorbFreshness(getHeader, responseUrl, status) {\n    const now = new Date();\n    const cacheHit = !!getHeader('X-SW-Cache');\n    const successful = status >= 200 && status < 300;\n\n    let sameOrigin = false;\n    let pathname = '';\n    if (responseUrl) {\n      try {\n        const parsed = new URL(responseUrl, window.location.href);\n        sameOrigin = parsed.origin === window.location.origin;\n        pathname = parsed.pathname;\n      } catch (_err) {\n        sameOrigin = false;\n      }\n    }\n\n    if (successful && !cacheHit && sameOrigin) {\n      if (isLoggableSyncPath(pathname)) {\n        appendSyncLogEntry(now, pathname);\n      }\n      // A real same-origin round-trip proves the connection works, so it\n      // is also the symbol's recovery path. Without this the only way back\n      // is the ``online`` event, which never fires when connectivity never\n      // actually changed \u2014 a single failed request on an online page would\n      // pin the symbol struck-through for the life of that page.\n      if (navigator.onLine) renderConnectionUi(true);\n    }\n\n    // Repaint while offline so the symbol keeps reporting the failure a\n    // successful response has not yet cleared.\n    if (!navigator.onLine) renderConnectionUi(false);\n  }\n\n  /**\n   * Wrap ``window.fetch`` so every response participates in the\n   * freshness ledger and every network failure flips the symbol. An\n   * ``AbortError`` is excluded \u2014 see the ``catch`` below.\n   */\n  function wrapFetch() {\n    if (typeof window.fetch !== 'function') return;\n    const original = window.fetch.bind(window);\n    window.fetch = async function (...args) {\n      // SNOW-921: the OUTBOUND edge, read from the request rather than the\n      // response, because it is the only place the request is still the\n      // only thing that has happened. Wrapped in its own try: an activity\n      // lamp must never be able to stop a fetch.\n      let pulsing = false;\n      try {\n        pulsing = shouldPulseFor(requestUrlOf(args[0]));\n        if (pulsing) pulseTraffic('up');\n      } catch (_err) {\n        // Ignore \u2014 the arrows are decoration.\n      }\n      try {\n        const response = await original(...args);\n        // The INBOUND edge. Gated on the same decision the outbound one\n        // took, so a request that did not light the up arrow cannot light\n        // the down one and leave the pair looking like traffic arrived\n        // from nowhere.\n        try {\n          if (pulsing) pulseTraffic('down');\n        } catch (_err) {\n          // Ignore.\n        }\n        try {\n          absorbFreshness(\n            (name) => response.headers.get(name),\n            response.url,\n            response.status,\n          );\n        } catch (_err) {\n          // Never break the caller on a header quirk.\n        }\n        return response;\n      } catch (err) {\n        // An abort is the caller cancelling its own request, not a\n        // connectivity failure, so it must not strike the symbol through.\n        // The sign-in page hits this on every visit: the WebAuthn\n        // conditional ceremony starts on email-input focus\n        // (static/js/passkey.js) and is aborted on the first keystroke,\n        // which on a fully-online page would otherwise leave the symbol\n        // reporting offline \u2014 nothing would fire ``online`` to clear it.\n        if (err && err.name === 'AbortError') throw err;\n        // Network failure \u2014 repaint as offline. Rethrow so callers can\n        // still handle the failure themselves.\n        //\n        // SNOW-921: deliberately NO down pulse here. Nothing came back, and\n        // an arrow that lit on a failure would say the opposite of what the\n        // struck-through glyph this line paints is about to say. The up\n        // arrow stays lit for its window and goes out on its own, which is\n        // exactly the right reading: something went out, nothing returned.\n        renderConnectionUi(false);\n        throw err;\n      }\n    };\n  }\n\n  /**\n   * Hook the HTMX post-response event so XHR-driven traffic also feeds\n   * the freshness ledger, and the ``htmx:sendError`` event so a failed\n   * mutation reaches the symbol immediately.\n   */\n  function wrapHtmx() {\n    // SNOW-921: htmx drives XHR, not fetch, so the wrapper above never sees\n    // it. Its two edges are these events \u2014 and unlike the fetch path there\n    // is no filtering to do: htmx only ever requests same-origin fragment\n    // endpoints a user's own interaction asked for, which is the definition\n    // of traffic worth showing.\n    document.body?.addEventListener('htmx:beforeRequest', () => {\n      pulseTraffic('up');\n    });\n    document.body?.addEventListener('htmx:afterOnLoad', (evt) => {\n      pulseTraffic('down');\n      const xhr = evt?.detail?.xhr;\n      if (!xhr || typeof xhr.getResponseHeader !== 'function') return;\n      try {\n        absorbFreshness(\n          (name) => xhr.getResponseHeader(name),\n          xhr.responseURL || '',\n          xhr.status,\n        );\n      } catch (_err) {\n        // Ignore.\n      }\n    });\n    // No ``AbortError`` guard needed here, unlike the fetch wrapper:\n    // htmx raises ``htmx:sendError`` from ``xhr.onerror`` only, and\n    // routes a cancelled request to a separate ``htmx:sendAbort`` event\n    // we do not listen for. The recovery path is the ``afterOnLoad``\n    // handler above, whose ``absorbFreshness`` call repaints as online\n    // on the next successful same-origin response.\n    document.body?.addEventListener('htmx:sendError', () => {\n      renderConnectionUi(false);\n    });\n  }\n\n  /**\n   * SNOW-748: whether the app is using the network AT ALL \u2014 the interface is\n   * up *and* no offline mode is in force.\n   *\n   * This is the question every consumer of the broadcast below was really\n   * asking, and until this ticket ``navigator.onLine`` was the only answer\n   * available to them. It is the wrong one under a forced mode by\n   * construction: the user presses the toggle precisely when they have a\n   * connection and do not want it spent, so ``onLine`` stays true while the\n   * worker refuses every read. The layers menu went on painting green dots and\n   * the basemap download controls went on offering downloads the worker would\n   * (SNOW-748) refuse.\n   *\n   * @returns {boolean}\n   */\n  function effectiveOnline() {\n    return navigator.onLine !== false && networkMode === 'auto';\n  }\n\n  /**\n   * Broadcast the connection state to any listener that needs to react\n   * beyond the blunt ``data-network-required`` disable \u2014 chiefly the map's\n   * layers menu (map_layer_sync_status.js), which gates each row against\n   * *cache* state (offline + uncached \u27F9 disabled + red dot) rather than\n   * disabling everything wholesale, and the basemap download controls. A\n   * single event keeps every consumer off its own ``navigator.onLine`` poll\n   * and in lockstep with the header symbol.\n   *\n   * SNOW-748: takes no argument. It carries ``effectiveOnline()``, and the\n   * callers that used to pass a literal ``true``/``false`` were the bug \u2014 the\n   * ``online`` listener passing ``true`` re-enabled the whole UI the first\n   * time the radio blinked under a mode the user had chosen. There is one\n   * answer to \"is this app using the network\", and it is computed here rather\n   * than at four call sites that can each get it wrong.\n   */\n  function broadcastConnectivity() {\n    document.dispatchEvent(\n      new CustomEvent('snowdesk:connectivity-changed', {\n        detail: { online: effectiveOnline() },\n      }),\n    );\n  }\n\n  /**\n   * Bind ``online`` / ``offline`` events on window so the symbol, the panel\n   * and network-required elements track the connection state without\n   * requiring a page reload.\n   */\n  function bindConnectionEvents() {\n    window.addEventListener('online', () => {\n      // SNOW-742: an interface coming back is the strongest signal there is\n      // that the latch should lift, and it beats waiting out the worker's own\n      // backoff (up to five minutes). If the route is still dead, three\n      // bounded reads re-latch within about nine seconds.\n      //\n      // SNOW-748: ``=== 'offline'`` EXACTLY, and this is the single most\n      // important comparison in the file. An auto-latch is the worker's guess\n      // that there is no route, and an ``online`` event is better evidence, so\n      // lifting it is right. A forced mode is not a guess about the network at\n      // all \u2014 the user chose it, very often while online \u2014 so an interface\n      // event must not overrule it. Widening this to ``!== 'auto'`` would undo\n      // the user's choice the first time the radio blinked, which is the bug\n      // this ticket exists to fix, moved one file across.\n      if (networkMode === 'offline') requestNetworkMode('auto');\n      renderConnectionUi(true);\n      // SNOW-748: the effective value, not a literal ``true``. Under a forced\n      // mode (which the branch above deliberately leaves alone) the network is\n      // still not being used, and telling the app otherwise here is what let\n      // the download controls come back to life the moment the radio blinked.\n      syncNetworkRequired(effectiveOnline());\n      broadcastConnectivity();\n    });\n    window.addEventListener('offline', () => {\n      renderConnectionUi(false);\n      syncNetworkRequired(false);\n      broadcastConnectivity();\n    });\n  }\n\n  /**\n   * SNOW-742: read the persisted network mode back from ``meta:app``\n   * before the first ``renderConnectionUi`` call. Guarded on\n   * ``window.pwaDb`` presence (it loads before this script \u2014 see\n   * ``base.html``) and never throws.\n   */\n  async function hydratePersistedMode() {\n    if (!window.pwaDb || typeof window.pwaDb.get !== 'function') return;\n    // Re-asserted to the worker by ``init`` below.\n    //\n    // SNOW-748: WHICH offline mode is preserved, not just the fact of one. A\n    // forced mode read back as a latch would be probed away within thirty\n    // seconds of the next page load \u2014 the user's choice surviving a reload is\n    // most of what makes it a setting rather than a gesture.\n    try {\n      const modeRow = await window.pwaDb.get('meta:app', NETWORK_MODE_KEY);\n      if (modeRow) networkMode = coerceNetworkMode(modeRow.value);\n    } catch (_err) {\n      // Best-effort \u2014 an unread mode simply starts in 'auto', and the latch\n      // re-trips within about nine seconds if the radio really is dead.\n    }\n  }\n\n  /**\n   * Prime the initial state. If the page loaded while offline (unlikely\n   * via the browser \u2014 offline navigations normally show the browser's\n   * own error page \u2014 but possible via the SW cache), we want the symbol\n   * painted offline immediately, and the panel holding the persisted clock\n   * rather than a blank the moment the user presses it.\n   */\n  async function init() {\n    bindConnectionEvents();\n    wrapFetch();\n    wrapHtmx();\n    // SNOW-742: bound BEFORE the IndexedDB read below, not after. The worker\n    // can latch during the page's own initial request burst, and a listener\n    // attached behind an await would miss the announcement \u2014 leaving the app\n    // latched with the panel still claiming it is merely struggling.\n    bindNetworkModeControls();\n    await hydratePersistedMode();\n    // SNOW-1415: and what this device's downloads hold, so the rows and\n    // the header symbol answer from the first paint. Not awaited: holding\n    // the mode re-assertion below behind an IndexedDB read would leave a\n    // latched worker in 'auto' for the length of it.\n    refreshAreas();\n    // SNOW-742: re-assert the persisted mode to the worker. A worker\n    // terminated while idle comes back in 'auto' having forgotten the latch;\n    // this is what restores it, and it is why the worker never has to read\n    // IndexedDB on its own fetch path.\n    //\n    // SNOW-748: either offline mode is re-asserted, as ITSELF. Sending\n    // 'offline' for a forced mode would hand the worker a latch, which\n    // schedules the probe that ends it.\n    if (networkMode !== 'auto') requestNetworkMode(networkMode);\n    renderConnectionUi(navigator.onLine);\n    syncNetworkRequired(effectiveOnline());\n    // Prime consumers with the initial state so a page that loaded offline\n    // (via the SW cache), or one that booted straight back into a persisted\n    // forced mode, gets its cache-aware gating applied at boot rather than\n    // only on the next transition.\n    broadcastConnectivity();\n  }\n\n  // SNOW-748: the READ half of the broadcast above, for the consumers that\n  // re-render on ``snowdesk:connectivity-changed`` but then decide what to\n  // paint by reading ``navigator.onLine`` again \u2014 the two basemap download\n  // controls, the downloads sheet and the layers menu's sync dots. The event\n  // is still what tells them to re-render; this is what they ask when they\n  // do, so the answer is the same one the symbol and the worker are acting\n  // on. Assigned synchronously at script evaluation, before ``init``'s first\n  // await, so a consumer that runs early gets a real answer rather than the\n  // ``navigator.onLine`` fallback.\n  //\n  // Frozen, like the other ``window.pwa*`` surfaces (docs/offline-map.md): the\n  // mode itself is owned by the service worker, and nothing on the page may\n  // assert connectivity by assignment.\n  // SNOW-1415: the map's ``?sync=all`` arrival hands the press here\n  // (``map_basemap_downloads.js``), because this module owns the panel the\n  // result is shown in. Frozen, like the other ``window.pwa*`` surfaces.\n  window.pwaConnectionPanel = Object.freeze({\n    syncAll: () => syncAll(),\n  });\n\n  window.pwaConnectivity = Object.freeze({\n    /**\n     * True when the app is using the network: the interface is up and no\n     * offline mode \u2014 latched or user-forced \u2014 is in force.\n     *\n     * @returns {boolean}\n     */\n    isOnline: () => effectiveOnline(),\n  });\n\n  init();\n})();\n"],
  "mappings": "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",
  "names": ["PANEL_ID", "PANEL_SELECTOR", "PANEL_CTA_SELECTOR", "NETWORK_ATTR", "NETWORK_MODE_KEY", "networkMode", "networkSwitchPress", "NETWORK_INDICATOR_SELECTOR", "NETWORK_TOGGLE_SELECTOR", "NETWORK_SYNC_SELECTOR", "SAME_SITE_PATH", "NETWORK_SYNC_LIST_SELECTOR", "NETWORK_EMPTY_SELECTOR", "NETWORK_SYNC_ROW_SELECTOR", "NETWORK_SYNC_OFFLINE_SELECTOR", "NETWORK_READY_SELECTOR", "NETWORK_READY_SUMMARY_SELECTOR", "NETWORK_READY_QUEUE_SELECTOR", "MUTATION_STORE", "TRAFFIC_UP_SELECTOR", "TRAFFIC_DOWN_SELECTOR", "TRAFFIC_PULSE_MS", "NETWORK_SWITCH_ID", "STATIC_ASSET_EXTENSIONS", "SYNC_LOG_EXCLUDED_PATHS", "toggleRole", "role", "shown", "el", "readCustomAreaRecords", "row", "legacyRow", "legacy", "refreshAreas", "rows", "areas", "index", "list", "isRegion", "entry", "identified", "syncableArea", "downloadsAreas", "renderAreaRows", "strings", "networkStrings", "label", "signedIn", "currentPrincipal", "meta", "areaVerdict", "area", "journey", "stages", "verdict", "reason", "overallReadiness", "readyRun", "paintReadiness", "state", "effectiveOnline", "indicator", "name", "block", "template", "item", "paintReady", "online", "running", "core", "note", "button", "span", "progress", "described", "summary", "queue", "byId", "answer", "resolveSyncAreaId", "areaId", "navigateToMapSync", "href", "waitingMutations", "db", "pressReady", "bridge", "run", "onStage", "step", "result", "syncAll", "disclosure", "panelDisclosure", "opened", "resolve", "pressRedownload", "event", "manager", "panelIsOpen", "syncPanelState", "open", "renderConnectionUi", "reaching", "renderNetworkUi", "renderNetworkCopy", "auto", "forced", "latched", "cta", "input", "networkLockoutWarning", "warning", "confirm", "NETWORK_STRING_FALLBACKS", "coerceNetworkMode", "value", "requestNetworkMode", "mode", "persistMeta", "syncNetworkRequired", "broadcastConnectivity", "bindNetworkModeControls", "target", "press", "canOpen", "confirmed", "node", "applyDisabled", "child", "disabled", "key", "trafficTimers", "pulseTraffic", "direction", "selector", "arrow", "requestUrlOf", "shouldPulseFor", "rawUrl", "parsed", "isLoggableSyncPath", "isStaticAssetPath", "pathname", "dot", "appendSyncLogEntry", "at", "absorbFreshness", "getHeader", "responseUrl", "status", "now", "cacheHit", "successful", "sameOrigin", "wrapFetch", "original", "args", "pulsing", "response", "err", "wrapHtmx", "evt", "xhr", "bindConnectionEvents", "hydratePersistedMode", "modeRow", "init"]
}
