{
  "version": 3,
  "sources": ["relative_time.js"],
  "sourcesContent": ["/**\n * static/js/relative_time.js \u2014 keeps a server-rendered relative timestamp\n * honest for as long as the page stays open.\n *\n * A relative age (\"2 hours ago\") is only true at the instant it is\n * rendered. The map's field-observation rows carry one, and the map is a\n * page that stays open for hours: opened at the car park, put in a pocket,\n * read again on the ridge. By then the server's answer is four hours out of\n * date and nothing on the page has re-rendered it \u2014 the list endpoint is\n * network-only (see sw.js's classification), so away from signal there is\n * no re-render to be had. The arithmetic, though, needs no network at all,\n * which is why it happens here.\n *\n * Markup contract \u2014 any surface, not just observations:\n *\n *     <time datetime=\"2026-08-11T05:29:00+00:00\" data-relative-time>\n *       2 hours ago\n *     </time>\n *\n * The server renders the age in the element's text (``naturaltime``) and\n * the instant in ``datetime``. This module recomputes the text from\n * ``datetime``; a browser that never runs it keeps the server's answer,\n * which is right at page load and stale only afterwards. The surrounding\n * copy \u2014 \"Martigny-Verbier \u00B7 reported \u2026\" \u2014 stays in the template, so a\n * translator sees a whole sentence and this module owns no prose.\n *\n * That is also what keeps the module out of the i18n trap (docs/i18n.md):\n * the age itself is formatted by ``Intl.RelativeTimeFormat`` against\n * ``<html lang>``, so the words come from the platform's CLDR data rather\n * than from English literals hard-coded here.\n *\n * WHEN IT RECOMPUTES. The interval is the least important of the four:\n *\n *   - a 60s interval, for a panel someone is looking at;\n *   - ``visibilitychange`` and ``pageshow``, which is the case that\n *     actually matters. A backgrounded tab has its timers throttled, and a\n *     phone that slept for four hours may have had the page frozen\n *     outright \u2014 no timer fired at all. Both fire on the way back, before\n *     the user can read a stale row;\n *   - ``htmx:afterSwap``, because the rows arrive by HTMX after this\n *     module's first pass and would otherwise wait up to a minute for\n *     their first correction. (They are correct as rendered, so this is\n *     about the swap-then-sleep case, not the swap itself.)\n *\n * Exposes ``window.pwaRelativeTime.refresh(root)`` so a caller that\n * injects rows by some other route can ask for a pass directly.\n */\n\n(function () {\n  'use strict';\n\n  /** Elements this module owns. `datetime` is required: without an instant\n   * there is nothing to recompute from, and overwriting the server's text\n   * would lose the only age the row has.\n   * @type {string}\n   */\n  const SELECTOR = 'time[data-relative-time][datetime]';\n\n  /** How often to recompute while the page is visible. A minute is the\n   * coarsest tick that still keeps a sub-hour age (the common case for a\n   * report just filed) from reading visibly wrong.\n   * @type {number}\n   */\n  const TICK_MS = 60000;\n\n  /** Unit ladder, smallest first: how many of each unit make up the next\n   * one. `Infinity` terminates it \u2014 years are the last unit we name.\n   * @type {Array<[Intl.RelativeTimeFormatUnit, number]>}\n   */\n  const UNITS = [\n    ['second', 60],\n    ['minute', 60],\n    ['hour', 24],\n    ['day', 7],\n    // 30.4 days per month / 7 per week. Approximate on purpose: this is a\n    // human-readable age, not a duration anyone measures against.\n    ['week', 4.35],\n    ['month', 12],\n    ['year', Infinity],\n  ];\n\n  /** The formatter, or null where `Intl.RelativeTimeFormat` is missing.\n   * Null means every element keeps its server-rendered text \u2014 the same\n   * outcome as this file failing to load, which is the point of rendering\n   * the age server-side in the first place.\n   * @type {Intl.RelativeTimeFormat | null}\n   */\n  const FORMATTER = (function () {\n    if (typeof Intl === 'undefined' || !Intl.RelativeTimeFormat) return null;\n    const lang = document.documentElement.getAttribute('lang');\n    try {\n      // `numeric: 'always'` \u2014 \"1 day ago\", not \"yesterday\". The template's\n      // sentence is \"reported <age>\", and only the numeric form completes\n      // it in every language the way the server's own naturaltime does.\n      return new Intl.RelativeTimeFormat(lang || undefined, { numeric: 'always' });\n    } catch (_err) {\n      // An unparseable lang tag is not worth losing the feature over.\n      return new Intl.RelativeTimeFormat(undefined, { numeric: 'always' });\n    }\n  })();\n\n  /** Format a signed millisecond offset as a relative age.\n   *\n   * Negative is the past (\"2 hours ago\"); positive the future (\"in 3\n   * minutes\"), which a report can legitimately be by up to the server's\n   * five-minute clock-skew tolerance.\n   *\n   * @param {number} deltaMs Instant minus now, in milliseconds.\n   * @returns {string} The formatted age.\n   */\n  function format(deltaMs) {\n    let value = deltaMs / 1000;\n    for (let i = 0; i < UNITS.length; i += 1) {\n      const [unit, perNext] = UNITS[i];\n      if (Math.abs(value) < perNext || perNext === Infinity) {\n        // Round away from zero, so the first second of a report's life\n        // reads \"1 second ago\" rather than the meaningless \"0 seconds ago\".\n        const rounded = Math.round(value) || (value < 0 ? -1 : 1);\n        return FORMATTER.format(rounded, unit);\n      }\n      value /= perNext;\n    }\n    // Unreachable: the ladder ends at Infinity. Kept so the function has\n    // one return type whatever a future edit does to UNITS.\n    return FORMATTER.format(Math.round(value), 'year');\n  }\n\n  /** Recompute every relative timestamp under `root`.\n   *\n   * @param {ParentNode} [root] Subtree to scan. Defaults to the document.\n   * @returns {void}\n   */\n  function refresh(root) {\n    if (!FORMATTER) return;\n    const scope = root || document;\n    const now = Date.now();\n    scope.querySelectorAll(SELECTOR).forEach(function (el) {\n      const instant = Date.parse(el.getAttribute('datetime'));\n      // An unparseable datetime leaves the server's text alone rather than\n      // replacing it with \"NaN years ago\".\n      if (Number.isNaN(instant)) return;\n      el.textContent = format(instant - now);\n    });\n  }\n\n  refresh();\n\n  setInterval(refresh, TICK_MS);\n\n  // The page coming back into view \u2014 from another tab, from a locked\n  // screen, or out of the bfcache after a device slept through the whole\n  // interval. `pageshow` covers the restore that fires no visibilitychange.\n  document.addEventListener('visibilitychange', function () {\n    if (!document.hidden) refresh();\n  });\n  window.addEventListener('pageshow', function () {\n    refresh();\n  });\n\n  // Rows swapped in by HTMX after this module's first pass.\n  document.addEventListener('htmx:afterSwap', function (event) {\n    const target = event.detail && event.detail.target;\n    refresh(target && target.querySelectorAll ? target : undefined);\n  });\n\n  window.pwaRelativeTime = { refresh: refresh, format: format };\n})();\n"],
  "mappings": "CAgDC,UAAY,CACX,aAOA,MAAMA,EAAW,qCAaXC,EAAQ,CACZ,CAAC,SAAU,EAAE,EACb,CAAC,SAAU,EAAE,EACb,CAAC,OAAQ,EAAE,EACX,CAAC,MAAO,CAAC,EAGT,CAAC,OAAQ,IAAI,EACb,CAAC,QAAS,EAAE,EACZ,CAAC,OAAQ,GAAQ,CACnB,EAQMC,GAAa,UAAY,CAC7B,GAAI,OAAO,KAAS,KAAe,CAAC,KAAK,mBAAoB,OAAO,KACpE,MAAMC,EAAO,SAAS,gBAAgB,aAAa,MAAM,EACzD,GAAI,CAIF,OAAO,IAAI,KAAK,mBAAmBA,GAAQ,OAAW,CAAE,QAAS,QAAS,CAAC,CAC7E,MAAe,CAEb,OAAO,IAAI,KAAK,mBAAmB,OAAW,CAAE,QAAS,QAAS,CAAC,CACrE,CACF,GAAG,EAWH,SAASC,EAAOC,EAAS,CACvB,IAAIC,EAAQD,EAAU,IACtB,QAASE,EAAI,EAAGA,EAAIN,EAAM,OAAQM,GAAK,EAAG,CACxC,KAAM,CAACC,EAAMC,CAAO,EAAIR,EAAMM,CAAC,EAC/B,GAAI,KAAK,IAAID,CAAK,EAAIG,GAAWA,IAAY,IAAU,CAGrD,MAAMC,EAAU,KAAK,MAAMJ,CAAK,IAAMA,EAAQ,EAAI,GAAK,GACvD,OAAOJ,EAAU,OAAOQ,EAASF,CAAI,CACvC,CACAF,GAASG,CACX,CAGA,OAAOP,EAAU,OAAO,KAAK,MAAMI,CAAK,EAAG,MAAM,CACnD,CAOA,SAASK,EAAQC,EAAM,CACrB,GAAI,CAACV,EAAW,OAChB,MAAMW,EAAQD,GAAQ,SAChBE,EAAM,KAAK,IAAI,EACrBD,EAAM,iBAAiBb,CAAQ,EAAE,QAAQ,SAAUe,EAAI,CACrD,MAAMC,EAAU,KAAK,MAAMD,EAAG,aAAa,UAAU,CAAC,EAGlD,OAAO,MAAMC,CAAO,IACxBD,EAAG,YAAcX,EAAOY,EAAUF,CAAG,EACvC,CAAC,CACH,CAEAH,EAAQ,EAER,YAAYA,EAAS,GAAO,EAK5B,SAAS,iBAAiB,mBAAoB,UAAY,CACnD,SAAS,QAAQA,EAAQ,CAChC,CAAC,EACD,OAAO,iBAAiB,WAAY,UAAY,CAC9CA,EAAQ,CACV,CAAC,EAGD,SAAS,iBAAiB,iBAAkB,SAAUM,EAAO,CAC3D,MAAMC,EAASD,EAAM,QAAUA,EAAM,OAAO,OAC5CN,EAAQO,GAAUA,EAAO,iBAAmBA,EAAS,MAAS,CAChE,CAAC,EAED,OAAO,gBAAkB,CAAE,QAASP,EAAS,OAAQP,CAAO,CAC9D,GAAG",
  "names": ["SELECTOR", "UNITS", "FORMATTER", "lang", "format", "deltaMs", "value", "i", "unit", "perNext", "rounded", "refresh", "root", "scope", "now", "el", "instant", "event", "target"]
}
