{
  "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) Freshness on demand \u2014 pressing that symbol opens the network menu\n *       (``includes/_connection_panel.html``), anchored beneath it in the\n *       header, which shows how long ago this device last reached the\n *       server, explains the current state, carries the \"Offline mode\"\n *       switch, 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 * Two clocks (SNOW-482)\n * ----------------------\n * The banner conflated two different facts into one in-memory value:\n * when the *device last synced* with the server, and when the *server\n * generated the data* it last returned. These are now tracked and\n * persisted separately:\n *\n *   - ``syncLastAt`` \u2014 wall-clock time of the most recent *successful*\n *     (2xx) same-origin response that is neither cache-served\n *     (``X-SW-Cache`` absent) nor a synthesized service-worker fallback\n *     (a non-empty resolved URL \u2014 see SNOW-490: an offline cache miss\n *     in ``static/js/sw.js`` resolves to a synthesized 504 with\n *     ``url === ''``, which must not be mistaken for a real\n *     round-trip). Persisted to IndexedDB ``meta:app`` under key\n *     ``sync.last_at``.\n * It is read back from ``meta:app`` on init, before the first\n * ``renderConnectionUi`` call, so a cold offline launch shows the real\n * last-known value rather than resetting to blank.\n *\n * SNOW-615: there was a second clock here, ``freshnessLastGeneratedAt``,\n * tracking the newest ``X-Data-Generated-At`` header. It was declared,\n * assigned on every qualifying response, persisted, and hydrated on every\n * page boot \u2014 and never read. The panel has one ``data-role=\"synced-at\"``\n * cell, filled from the clock above; the template's own comment said so\n * while this header claimed both reached the UI. Deleted rather than\n * wired in: nothing had asked for a second timestamp in three tickets'\n * worth of work, and a write-only clock costs a put per response and a\n * read per boot to misdirect the next reader.\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`` and the freshness ticker in\n *     step, 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 * 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 * 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: the \"Get ready for offline\" block \u2014 the block itself,\n  // the list the area rows are cloned into, the row <template>, the\n  // offline note, the one press and its two outcome lines, and the way to\n  // `/offline/` a press falls back to on a page with no map.\n  const NETWORK_SYNC_SELECTOR = '[data-network-sync]';\n  const NETWORK_SYNC_LIST_SELECTOR = '[data-network-sync-list]';\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  const NETWORK_DOWNLOADS_SELECTOR = '[data-network-downloads]';\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-482: the meta:app key the last-sync clock is persisted under.\n  // A sibling ``freshness.last_generated_at`` key went with the write-only\n  // clock SNOW-615 removed. Rows already written under it on devices in\n  // the field are simply never read again \u2014 harmless, and cheaper than a\n  // migration to delete one small row.\n  const SYNC_LAST_AT_KEY = 'sync.last_at';\n\n  // SNOW-482: cadence at which the panel re-renders its relative\n  // \"last synced\" phrase while OPEN, so it counts up live rather than\n  // freezing at the value captured when the user pressed the symbol.\n  const FRESHNESS_TICK_MS = 30000;\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  // In-memory copy of the last-sync clock, hydrated from IndexedDB on\n  // init (see hydratePersistedClocks) and kept in sync with the persisted\n  // copy on every qualifying response.\n  let syncLastAt = null;\n\n  /**\n   * Coerce a Date / ISO string to a valid Date, or null on failure so\n   * callers can fall back rather than render \"Invalid Date\".\n   *\n   * @param {string | Date | null} value\n   * @returns {Date | null}\n   */\n  function toDate(value) {\n    if (!value) return null;\n    const d = value instanceof Date ? value : new Date(value);\n    return Number.isNaN(d.valueOf()) ? null : d;\n  }\n\n  /**\n   * Format the sync clock as a naturalistic relative phrase \u2014 e.g.\n   * \"6 minutes ago\", \"2 hours ago\", \"now\" \u2014 using the largest sensible\n   * unit. Locale-aware via ``Intl.RelativeTimeFormat`` so it localises\n   * for free once the UI gains other languages.\n   *\n   * @param {string | Date | null} value\n   * @returns {string}\n   */\n  function formatRelative(value) {\n    const d = toDate(value);\n    if (!d) return '';\n    const rtf = new Intl.RelativeTimeFormat(undefined, { numeric: 'auto' });\n    const diffSeconds = Math.round((d.getTime() - Date.now()) / 1000);\n    const units = [\n      ['day', 86400],\n      ['hour', 3600],\n      ['minute', 60],\n    ];\n    for (const [unit, secs] of units) {\n      if (Math.abs(diffSeconds) >= secs) {\n        return rtf.format(Math.round(diffSeconds / secs), unit);\n      }\n    }\n    return rtf.format(Math.round(diffSeconds), 'second');\n  }\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 two 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   * Fill the panel's \"last synced\" span with the sync clock as a relative\n   * phrase, degrading to an em dash until the first sync is known.\n   *\n   * @returns {void}\n   */\n  function renderFreshnessCells() {\n    const syncedCell = document.querySelector('[data-role=\"synced-at\"]');\n    if (syncedCell) syncedCell.textContent = formatRelative(syncLastAt) || '\u2014';\n    renderDownloadsAge();\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 \u2014 so the line hid itself on\n   * exactly the oldest installs, which are the ones most likely to be\n   * holding content from weeks ago. Such a record predates ``contentAt``\n   * entirely, so it takes the \"no bulletins saved yet\" sentence, which is\n   * true of it.\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. It is also not loaded outside the map and ``/offline/``, so\n   * it cannot be relied on here at all. The precedence rule is mirrored\n   * rather than shared: an ARRAY at the new key wins even when empty\n   * (\"already migrated, nothing left\"), and only an absent or\n   * non-array value falls through to the legacy row. The migration\n   * itself still happens where it belongs, on the next map or\n   * ``/offline/`` load.\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 line last said.\n      return { value: [] };\n    }\n  }\n\n  /**\n   * SNOW-928: the OTHER age \u2014 how old the content inside this device's\n   * downloaded areas is.\n   *\n   * The summary line above answers \"is this app reaching the server\",\n   * which is about right now. SNOW-923's permanent/perishable split poses\n   * the same question one time-scale out: the tiles are a fixed grid over\n   * fixed ground and never go stale, and the bulletins inside the same\n   * boundary are new every day. A user who topped up on Tuesday and reads\n   * a Friday page offline has a working app showing Tuesday's danger\n   * ratings, and nothing anywhere told them so.\n   *\n   * The OLDEST refresh across every area, because the worst one is what\n   * decides whether the reader is holding current data. An area that has\n   * never had content at all is not a very old one \u2014 it is a map with\n   * nothing on it \u2014 so it takes its own sentence rather than an\n   * arbitrarily distant date.\n   *\n   * Reads ``downloadsOldestContentAt``, resolved once at boot and again\n   * whenever the panel opens, not on the 30s tick: the underlying records\n   * change only when a download runs, and re-opening IndexedDB twice a\n   * minute for a value that has not moved is waste on the device least\n   * able to afford it. The PHRASE is re-rendered on the tick, which is\n   * what makes it count up.\n   *\n   * @returns {void}\n   */\n  function renderDownloadsAge() {\n    const line = document.querySelector('[data-role=\"downloads-age\"]');\n    if (!line) return;\n    // No areas at all: nothing to say, and a line reading \"\u2014\" invites the\n    // reader to wonder what is missing. The same posture the reconnect\n    // button takes for a state it does not apply to.\n    if (downloadsAreaCount === 0) {\n      line.classList.add('hidden');\n      return;\n    }\n    line.classList.remove('hidden');\n    const never = downloadsOldestContentAt === null;\n    toggleRole('downloads-updated', !never);\n    toggleRole('downloads-empty', never);\n    if (never) return;\n    const cell = document.querySelector('[data-role=\"downloads-at\"]');\n    if (cell) cell.textContent = formatRelative(downloadsOldestContentAt) || '\u2014';\n  }\n\n  /**\n   * Re-read what this device's downloaded areas hold, and repaint the line.\n   *\n   * Never throws: a device whose IndexedDB will not answer keeps whatever\n   * the line last said, which is the pre-SNOW-928 behaviour of saying\n   * nothing rather than a wrong thing.\n   *\n   * @returns {Promise<void>}\n   */\n  async function refreshDownloadsAge() {\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      let count = 0;\n      let oldest = null;\n      let anyMissing = false;\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 is what decides how\n        // it is identified and named \u2014 a region is keyed by `region_id`\n        // and a custom area IS its bucket id \u2014 so the index carries that\n        // rather than each entry being sniffed for a field.\n        const isRegion = index === 0;\n        list.forEach((entry) => {\n          if (!entry) return;\n          count += 1;\n          const identified = syncableArea(entry, isRegion);\n          if (identified) areas.push(identified);\n          const at = toDate(entry.contentAt);\n          // Absent means never fetched, which is every area downloaded\n          // before SNOW-924 and every one whose content phase fell short.\n          // It is not \"very old\"; it is a different sentence.\n          if (!at) {\n            anyMissing = true;\n            return;\n          }\n          if (oldest === null || at < oldest) oldest = at;\n        });\n      });\n      downloadsAreaCount = count;\n      downloadsOldestContentAt = anyMissing ? null : oldest;\n      downloadsAreas = areas;\n    } catch (_err) {\n      // Best-effort, per the docstring.\n    }\n    renderDownloadsAge();\n    renderDownloadsSync();\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-1141) when its bulletins were fetched.\n   *\n   * The id is the PINNED BUCKET id in both cases, which is what every\n   * consumer of a sync request expects. A custom area already carries it\n   * (`entry.id`); a region carries its `region_id`, and the bucket-id\n   * format belongs to `basemap_download_core.js`'s `areaIdForRegion`,\n   * which this module deliberately does not load \u2014 so the region id\n   * travels as itself and `/offline/` resolves it there, where that\n   * function is. Nothing here assembles the format by hand.\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, contentAt: string|null}|null}\n   *   Null for an entry with no id at all \u2014 a half-written record is not something to offer a\n   *   control 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 {\n        id: entry.region_id,\n        label: entry.name || entry.region_id,\n        contentAt: entry.contentAt || null,\n      };\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, contentAt: entry.contentAt || null };\n  }\n\n  /**\n   * SNOW-951/1141: paint the area rows from what this device holds.\n   *\n   * Rebuilt rather than patched, on every panel open, because the list\n   * itself moves \u2014 an area downloaded or removed in another tab is a\n   * different list, and reconciling row by row would be more code than\n   * cloning three or four <li>s. What a press in flight has learned\n   * survives the rebuild in `readyRun`, which is why it lives outside the\n   * DOM.\n   *\n   * HIDDEN with nothing downloaded, the same posture the staleness line\n   * above takes: a reader with no areas has nothing to get ready, and a\n   * heading over an empty list reads as a fault rather than as an answer.\n   *\n   * @returns {void}\n   */\n  function renderDownloadsSync() {\n    const block = document.querySelector(NETWORK_SYNC_SELECTOR);\n    if (!block) return;\n    const list = block.querySelector(NETWORK_SYNC_LIST_SELECTOR);\n    const template = block.querySelector(NETWORK_SYNC_ROW_SELECTOR);\n    if (!list || !template) return;\n    if (downloadsAreas.length === 0) {\n      block.classList.add('hidden');\n      list.textContent = '';\n      return;\n    }\n    block.classList.remove('hidden');\n    list.textContent = '';\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      // the row carries the id the press's outcomes are keyed by.\n      if (item) item.setAttribute('data-area-id', area.id);\n      const label = row.querySelector('[data-sync-label]');\n      if (label) label.textContent = area.label;\n      list.appendChild(row);\n    }\n    paintReady();\n  }\n\n  /**\n   * SNOW-1141: what a row says before any press \u2014 how old its bulletins\n   * are, read off the record's `contentAt` with SNOW-1139's stamp rule.\n   * Nothing about the tiles: this module cannot see the area's bucket, so\n   * it makes no claim about them until a press has asked.\n   *\n   * @param {{contentAt: string|null}} area\n   * @returns {'today'|'earlier'|'none'}\n   */\n  function recordStatus(area) {\n    if (!toDate(area.contentAt)) return 'none';\n    const stamp = window.pwaCachedStampCore;\n    if (stamp && stamp.isBeforeToday(area.contentAt)) return 'earlier';\n    return 'today';\n  }\n\n  /**\n   * SNOW-1141: paint the press, its outcome and every row's status, and\n   * refuse the press while the app is not using the network.\n   *\n   * Every fixed word is rendered in the template and toggled here; the\n   * sentences that carry a number or a day 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 areaId = row.getAttribute('data-area-id') || '';\n      const area = byId.get(areaId);\n      if (!area) return;\n      const learned = readyRun ? readyRun.outcomes.get(areaId) : undefined;\n      const status = learned || recordStatus(area);\n      row.querySelectorAll('[data-area-status]').forEach((span) => {\n        span.classList.toggle('hidden', span.getAttribute('data-area-status') !== status);\n      });\n      if (status === 'earlier') {\n        const earlier = row.querySelector('[data-area-status=\"earlier\"]');\n        const stamp = window.pwaCachedStampCore;\n        if (earlier && stamp && window.pwaStrings) {\n          earlier.textContent = window.pwaStrings.interpolate(strings['area-bulletins-from'], {\n            day: stamp.formatDay(area.contentAt),\n          });\n        }\n      }\n    });\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, which is the only\n   * place ``syncArea`` exists at all), so the unresolved id is handed on\n   * untouched everywhere else and ``/offline/`` resolves it there.\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-951/1141: hand the press to ``/offline/``, which can run it.\n   *\n   * ``?sync=all`` \u2014 that page drains the queue, brings every area it can\n   * up to date and re-runs its report over the result. The href comes off\n   * the panel's own downloads link rather than being written here: one\n   * statement of where that page lives.\n   *\n   * @returns {void}\n   */\n  function navigateToOfflineSync() {\n    const link = document.querySelector(NETWORK_DOWNLOADS_SELECTOR);\n    const href = link ? link.getAttribute('href') : '';\n    if (!href) return;\n    try {\n      const url = new URL(href, self.location.href);\n      url.searchParams.set('sync', 'all');\n      self.location.assign(url.pathname + url.search + url.hash);\n    } catch (_err) {\n      // A URL this runtime will not build is not a navigation to make.\n    }\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: \"Get ready for offline\" \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 menu it was made from \u2014 the\n   * menu stays open, because the result is the answer to the question the\n   * press asked. Everywhere else it navigates to ``/offline/?sync=all``;\n   * see the panel's own comment for why a 140KB map-only module is not\n   * lazily loaded into a bulletin page instead.\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      navigateToOfflineSync();\n      return;\n    }\n    const run = {\n      running: true,\n      step: /** @type {Object|null} */ (null),\n      outcomes: new Map(),\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) =>\n          core.areaOutcome(await bridge.syncArea(resolveSyncAreaId(areaId))),\n        onStep: (step) => {\n          run.step = step;\n          if (step.phase === 'area') run.outcomes.set(step.id, 'running');\n          if (step.phase === 'done') run.outcomes.set(step.id, step.outcome);\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        // The staleness line and each row's record have just moved, and\n        // they are read from the records rather than told an answer.\n        refreshDownloadsAge();\n        paintReady();\n      });\n  }\n\n  /**\n   * SNOW-1141: \"Download again\" on a tiles-gone row.\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-482: re-render the \"last synced\" phrase on a timer while the panel\n  // is OPEN, so the phrase counts up (\"6 minutes ago\" \u2192 \"7 minutes ago\")\n  // rather than freezing at the value it had when the symbol was pressed.\n  // Started and cleared from the <details>'s ``toggle`` event, which fires\n  // however the panel was closed \u2014 the symbol, the \"\u00D7\", Escape or a click\n  // outside \u2014 and it also self-clears if it wakes to find the panel gone.\n  let freshnessTicker = null;\n\n  // SNOW-928: how many areas this device holds, and the oldest content\n  // refresh among them \u2014 `null` when at least one holds no content at all,\n  // which is a different sentence rather than a very old date. Resolved\n  // from IndexedDB at boot and on every panel open; see\n  // `refreshDownloadsAge`.\n  let downloadsAreaCount = 0;\n  let downloadsOldestContentAt = null;\n\n  // SNOW-951: the same read's OTHER answer \u2014 `{id, label, contentAt}` per\n  // area, in the order the two stores yield them, for the area rows.\n  let downloadsAreas = [];\n\n  // SNOW-1141: the current \"Get ready\" press \u2014 `running`, the step it is\n  // on, what each area has come to so far (by id: `'running'`, `'ready'`,\n  // `'partial'` or `'absent'`) and, once settled, its result. Module\n  // state rather than DOM state because the rows are rebuilt from the\n  // <template> on every panel open, and a press still in flight must\n  // survive that. A SETTLED press is cleared on the next open\n  // (`syncPanelState`): its answer belongs to the moment it was given, and\n  // a \"ready\" that outlived it would be the session-long claim SNOW-951's\n  // \"Synced\" made.\n  /** @type {{running: boolean, step: Object|null, outcomes: Map<string, string>, result: Object|null}|null} */\n  let readyRun = null;\n\n  /**\n   * Start the freshness re-render timer, if not already running.\n   *\n   * @returns {void}\n   */\n  function startFreshnessTicker() {\n    if (freshnessTicker !== null) return;\n    freshnessTicker = window.setInterval(() => {\n      if (!panelIsOpen()) {\n        stopFreshnessTicker();\n        return;\n      }\n      renderFreshnessCells();\n    }, FRESHNESS_TICK_MS);\n  }\n\n  /**\n   * Stop the freshness re-render timer, if running.\n   *\n   * @returns {void}\n   */\n  function stopFreshnessTicker() {\n    if (freshnessTicker === null) return;\n    window.clearInterval(freshnessTicker);\n    freshnessTicker = null;\n  }\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``, fill the freshness cell, and run the re-render ticker\n   * only while the panel is on screen.\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      renderFreshnessCells();\n      // SNOW-928: re-read what the downloads hold, on every open rather\n      // than on the 30s tick. The records move only when a download runs \u2014\n      // which can have happened in another tab since this page loaded \u2014\n      // and re-opening IndexedDB twice a minute for a value that has not\n      // moved is waste on the device least able to afford it. The PHRASE\n      // still counts up on the tick.\n      refreshDownloadsAge();\n      startFreshnessTicker();\n    } else {\n      stopFreshnessTicker();\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    if (panelIsOpen()) renderFreshnessCells();\n  }\n\n  /**\n   * SNOW-742/748: show the message, explanation and CTA label that match the\n   * current state. 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. They do NOT share the explanation: the latched copy asserts\n   * there is no usable connection, which is exactly what is false when the\n   * 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    toggleRole('online-explainer', auto && reaching);\n    toggleRole('offline-explainer', auto && !reaching);\n    toggleRole('latched-explainer', latched);\n    toggleRole('forced-explainer', forced);\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': 'Getting ready\u2026',\n    'ready-progress': 'Getting ready\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    'area-bulletins-from': 'Bulletins from %(day)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-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). ``syncLastAt`` / ``sync.last_at`` /\n   * 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      syncLastAt = now;\n      persistMeta(SYNC_LAST_AT_KEY, now.toISOString());\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 an open panel's clock updates live when a\n    // fresh(er) response arrives, and so the symbol keeps reporting the\n    // failure a 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-482: read the persisted clock back from ``meta:app`` before\n   * the first ``renderConnectionUi`` call, so a cold offline launch shows the\n   * real last-known value instead of resetting to blank. Guarded on\n   * ``window.pwaDb`` presence (it loads before this script \u2014 see\n   * ``base.html``) and never throws \u2014 a read failure just leaves the\n   * clocks unset, same as before this ticket.\n   */\n  async function hydratePersistedClocks() {\n    if (!window.pwaDb || typeof window.pwaDb.get !== 'function') return;\n    try {\n      const syncRow = await window.pwaDb.get('meta:app', SYNC_LAST_AT_KEY);\n      if (syncRow && syncRow.value) {\n        const parsed = new Date(syncRow.value);\n        if (!Number.isNaN(parsed.valueOf())) syncLastAt = parsed;\n      }\n    } catch (_err) {\n      // Best-effort \u2014 the panel falls back to \"no data yet\" copy.\n    }\n    // SNOW-742: and the network mode, which is re-asserted to the worker by\n    // ``init`` below. Read separately from the clock above so one failing row\n    // can't cost the other.\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 hydratePersistedClocks();\n    // SNOW-928: and what this device's downloads hold, so the panel has\n    // the second age from the first press rather than from the second.\n    // Not awaited: the line is hidden until it resolves, and holding the\n    // mode re-assertion below behind an IndexedDB read would leave a\n    // latched worker in 'auto' for the length of it.\n    refreshDownloadsAge();\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  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", "NETWORK_SYNC_LIST_SELECTOR", "NETWORK_SYNC_ROW_SELECTOR", "NETWORK_SYNC_OFFLINE_SELECTOR", "NETWORK_READY_SELECTOR", "NETWORK_READY_SUMMARY_SELECTOR", "NETWORK_READY_QUEUE_SELECTOR", "MUTATION_STORE", "NETWORK_DOWNLOADS_SELECTOR", "TRAFFIC_UP_SELECTOR", "TRAFFIC_DOWN_SELECTOR", "TRAFFIC_PULSE_MS", "NETWORK_SWITCH_ID", "SYNC_LAST_AT_KEY", "FRESHNESS_TICK_MS", "STATIC_ASSET_EXTENSIONS", "SYNC_LOG_EXCLUDED_PATHS", "syncLastAt", "toDate", "value", "d", "formatRelative", "rtf", "diffSeconds", "units", "unit", "secs", "toggleRole", "role", "shown", "el", "renderFreshnessCells", "syncedCell", "renderDownloadsAge", "readCustomAreaRecords", "row", "legacyRow", "legacy", "line", "downloadsAreaCount", "never", "downloadsOldestContentAt", "cell", "refreshDownloadsAge", "rows", "count", "oldest", "anyMissing", "areas", "index", "list", "isRegion", "entry", "identified", "syncableArea", "at", "downloadsAreas", "renderDownloadsSync", "strings", "networkStrings", "label", "block", "template", "area", "item", "paintReady", "recordStatus", "stamp", "online", "effectiveOnline", "running", "readyRun", "core", "note", "button", "span", "state", "progress", "described", "summary", "queue", "byId", "areaId", "status", "earlier", "resolveSyncAreaId", "navigateToOfflineSync", "link", "href", "url", "waitingMutations", "db", "pressReady", "bridge", "run", "step", "result", "pressRedownload", "event", "manager", "disclosure", "panelDisclosure", "freshnessTicker", "startFreshnessTicker", "panelIsOpen", "stopFreshnessTicker", "syncPanelState", "open", "indicator", "renderConnectionUi", "reaching", "renderNetworkUi", "renderNetworkCopy", "auto", "forced", "latched", "cta", "input", "networkLockoutWarning", "warning", "confirm", "NETWORK_STRING_FALLBACKS", "coerceNetworkMode", "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", "absorbFreshness", "getHeader", "responseUrl", "now", "cacheHit", "successful", "sameOrigin", "wrapFetch", "original", "args", "pulsing", "response", "name", "err", "wrapHtmx", "evt", "xhr", "bindConnectionEvents", "hydratePersistedClocks", "syncRow", "modeRow", "init"]
}
