{
  "version": 3,
  "sources": ["map_overlay_bounds.js"],
  "sourcesContent": ["/*\n * static/js/map_overlay_bounds.js \u2014 how far down, and how far right, a map\n * overlay may reach (SNOW-658).\n *\n * The layers menu worked this out first. SNOW-511 capped its height so its\n * top rows could not slide behind the nav and the conditional off-season\n * banner; SNOW-656 then dropped its lower edge to just above the season\n * scrubber, because the CSS baseline it inherited was a function of the\n * roundel column's height and left a third of the map unused. Both halves\n * are MEASURED rather than declared, because everything they measure moves:\n * the banner is conditional, the safe-area insets vary by device, and the\n * column's height varies with the collapsible group.\n *\n * The three UGC sheets (#map-downloads-sheet, #favourite-sheet,\n * #report-sheet) need the same answer and had none: includes/_overlay_sheet.html\n * hard-codes a viewport corner (`sm:bottom-4 sm:right-4`), which knows\n * nothing about the scrubber below it or the roundel column beside it, so on\n * desktop a sheet opened over both. This module is that arithmetic, extracted\n * once, plus the horizontal inset the menu never needed (the menu opens from\n * INSIDE the column; a sheet has to clear it).\n *\n * ## Two coordinate bases, one geometry\n *\n * `compute()` deliberately returns VIEWPORT coordinates and leaves the\n * translation to each caller, because the two callers do not share a base:\n *\n *   - the layers menu is `position: absolute` inside #map-controls-br (it was\n *     inside #basemap-pill until SNOW-664 moved that pill into the clipped\n *     collapsible group), so its `bottom` is measured from the stack \u2014\n *     `stackBottom - floorY`;\n *   - a sheet is `position: fixed`, so its `bottom` is measured from the\n *     viewport \u2014 `window.innerHeight - floorY`.\n *\n * Returning a ready-made `bottom` would have to pick one of those and be\n * wrong for the other caller.\n *\n * ## Desktop only \u2014 and the mobile silence is the design, not a gap\n *\n * Below Tailwind's `sm` breakpoint (640px) this module writes NOTHING, and\n * clears anything a previous desktop-width pass wrote. That is deliberate\n * twice over.\n *\n * First, because there is nothing to compute: below `sm` the three panels\n * are full-width and docked to the bottom edge, covering the map, and that\n * IS the intended mobile behaviour \u2014 a docked sheet is the right shape on a\n * phone (the list is what is being read, there is no room to show it beside\n * anything, and the bottom edge is where the thumb is). A panel that had to\n * clear the scrubber and the roundel column on a 375px screen would be a\n * small box floating in a map the user can barely see. The two layouts and\n * the reasoning are written up in \u00A73.7 of\n * docs/map-page-functional-spec.md, and in includes/_overlay_sheet.html's\n * own header beside the classes that draw them; expect \"the panel fills the\n * screen on my phone\" as a report, and read it as working.\n *\n * Second, because the mobile layout is left entirely to CSS on purpose:\n * inline styles beat the stylesheet, so a stranded value from before a\n * resize would silently pin a mobile sheet to a desktop geometry. Writing\n * nothing on a phone means the sheet's own classes are always the whole\n * answer there. Do not \"complete\" this module by giving it a mobile branch.\n *\n * ## What it does not do\n *\n * Positioning is recomputed on open and on resize, matching the menu. A\n * scrubber that appears or disappears WITHOUT a resize (it is server-rendered\n * and permanent on the map page today) would leave an open overlay on a stale\n * floor; the menu has always had that gap and this module does not close it,\n * rather than growing a MutationObserver for a case that cannot currently\n * arise.\n */\n\n(function mapOverlayBoundsInit() {\n  'use strict';\n\n  // The clearance the layers menu already used above #map's top edge and\n  // below its own floor (SNOW-511/SNOW-656) \u2014 kept identical so extracting\n  // the arithmetic changes no number the menu writes.\n  var TOP_GAP = 8;\n  var BOTTOM_GAP = 8;\n\n  // The gap between a sheet's right edge and the roundel column it must\n  // clear. Matches the column's own inter-roundel gap (.map-controls-br's\n  // `gap: 8px`, static/css/map.css), so the sheet reads as part of the same\n  // rhythm rather than at an arbitrary distance from it.\n  var SIDE_GAP = 8;\n\n  // Tailwind's `sm` breakpoint \u2014 the width at which includes/_overlay_sheet.html\n  // stops being a full-width bottom dock and becomes a floating card. Stated\n  // once here rather than compared against `window.innerWidth` at each call\n  // site, so there is one definition of \"desktop\" to keep in step with the CSS.\n  var DESKTOP_QUERY = '(min-width: 640px)';\n\n  /**\n   * Every sheet that has been positioned at least once.\n   *\n   * Held so the resize listener below can re-place them all \u2014 including\n   * clearing them on the way down to a phone width \u2014 without each caller\n   * wiring a listener of its own.\n   *\n   * @type {Set<HTMLElement>}\n   */\n  var tracked = new Set();\n\n  /**\n   * Whether the viewport is wide enough for the floating-card layout.\n   *\n   * @returns {boolean} False when `matchMedia` is unavailable \u2014 a browser\n   *   that cannot answer is treated as the mobile shape, which is the one\n   *   that needs no JS to be correct.\n   */\n  function isDesktop() {\n    if (typeof window.matchMedia !== 'function') return false;\n    return window.matchMedia(DESKTOP_QUERY).matches;\n  }\n\n  /**\n   * The room a map overlay may occupy, in viewport coordinates.\n   *\n   * @returns {{floorY: number, maxHeight: number, controlsLeft: number|null}|null}\n   *   `floorY` \u2014 the lowest y an overlay's bottom edge may reach.\n   *   `maxHeight` \u2014 the height that floor leaves below #map's top edge.\n   *   `controlsLeft` \u2014 the left edge of the bottom-right roundel column, or\n   *   null when it has no box (absent, or a display:none ancestor).\n   *   Null overall when there is no #map on the page, which is the one case\n   *   where a caller should leave its CSS alone entirely.\n   */\n  function compute() {\n    var mapEl = document.getElementById('map');\n    if (!mapEl) return null;\n    var mapBox = mapEl.getBoundingClientRect();\n\n    // Stop above the bottom-left stack's last line \u2014 the date control, and\n    // the season scrubber beside it when the season is showing. That line is\n    // the lowest chrome on the map and it holds the baseline whatever comes\n    // and goes above it, so it is the floor every sheet has to clear.\n    //\n    // SNOW-794: this used to measure #season-scrubber, which was then a\n    // full-width bar across the foot. Two things broke that. The scrubber\n    // now shares a line rather than owning one, so it is no longer the\n    // lowest thing; and it is absent entirely off-season and below 640px,\n    // where its rect reads all zeros and put the floor at -8, sending every\n    // sheet on the map to the top of the viewport. Measuring the row fixes\n    // both \u2014 it is always present, and it is always the bottom.\n    var bottomRow = document.getElementById('map-date-row');\n    var rowBox = bottomRow ? bottomRow.getBoundingClientRect() : null;\n    var floorY = (rowBox && rowBox.height > 0 ? rowBox.top : mapBox.bottom) - BOTTOM_GAP;\n\n    var controls = document.getElementById('map-controls-br');\n    var controlsBox = controls ? controls.getBoundingClientRect() : null;\n\n    return {\n      floorY: floorY,\n      maxHeight: Math.max(0, Math.round(floorY - mapBox.top - TOP_GAP)),\n      controlsLeft: controlsBox && controlsBox.width > 0 ? controlsBox.left : null,\n    };\n  }\n\n  /**\n   * Drop every inline value this module writes, restoring the stylesheet.\n   *\n   * @param {HTMLElement} el\n   * @returns {void}\n   */\n  function clear(el) {\n    el.style.top = '';\n    el.style.bottom = '';\n    el.style.right = '';\n    el.style.maxHeight = '';\n  }\n\n  /**\n   * Place one fixed-position sheet inside the map's usable area.\n   *\n   * Opt-in per element rather than a sweep over `[data-overlay]`: the same\n   * partial renders inline (`static=True`) in the component library, where a\n   * fixed-position geometry would be meaningless.\n   *\n   * @param {HTMLElement} el The sheet container.\n   * @returns {void}\n   */\n  function positionSheet(el) {\n    if (!el) return;\n    tracked.add(el);\n\n    if (!isDesktop()) {\n      clear(el);\n      return;\n    }\n\n    var bounds = compute();\n    if (!bounds) {\n      clear(el);\n      return;\n    }\n\n    // A selection sheet docks down the LEFT side (2026-10-21,\n    // includes/_overlay_sheet.html's `side`): under the search pill it was\n    // picked from, on the same floor, with its `left` left to the partial's\n    // inset. Its top is the pill row's bottom plus the column's gap, the\n    // shelf the route panel sits on too; with no row to measure it takes\n    // the full height a right-hand sheet would.\n    if (el.dataset.sheetSide === 'left') {\n      var cluster = document.getElementById('map-utility-cluster');\n      var clusterBox = cluster ? cluster.getBoundingClientRect() : null;\n      var top =\n        clusterBox && clusterBox.height > 0\n          ? clusterBox.bottom + SIDE_GAP\n          : bounds.floorY - bounds.maxHeight;\n      // Hung from that top rather than stood on the floor, so a short\n      // sheet stays a short card under the pill; the floor caps its\n      // height instead.\n      el.style.top = Math.round(top) + 'px';\n      el.style.bottom = 'auto';\n      el.style.right = '';\n      el.style.maxHeight = Math.max(0, Math.round(bounds.floorY - top)) + 'px';\n      return;\n    }\n\n    // `bottom` and `right` are both measured from the viewport, the sheet\n    // being `position: fixed`.\n    el.style.bottom = Math.round(window.innerHeight - bounds.floorY) + 'px';\n    el.style.maxHeight = bounds.maxHeight + 'px';\n    if (bounds.controlsLeft === null) {\n      // No column to clear \u2014 leave the partial's own `sm:right-4`.\n      el.style.right = '';\n    } else {\n      el.style.right =\n        Math.round(window.innerWidth - bounds.controlsLeft + SIDE_GAP) + 'px';\n    }\n  }\n\n  // Same reason the menu re-places itself on resize: an orientation flip, a\n  // mobile URL-bar show/hide or a desktop resize all move the floor, the cap\n  // and the column. Every tracked sheet is re-placed, open or not \u2014 the\n  // measurement reads the map chrome rather than the sheet, so a hidden sheet\n  // is placed just as correctly, and this is also what clears a desktop\n  // geometry off a sheet on the way down to a phone width.\n  window.addEventListener('resize', function () {\n    tracked.forEach(function (el) {\n      positionSheet(el);\n    });\n  });\n\n  window.pwaOverlayBounds = Object.freeze({\n    compute: compute,\n    isDesktop: isDesktop,\n    positionSheet: positionSheet,\n  });\n})();\n"],
  "mappings": "CAsEC,UAAgC,CAC/B,aAKA,IAAIA,EAAU,EACVC,EAAa,EAMbC,EAAW,EAMXC,EAAgB,qBAWhBC,EAAU,IAAI,IASlB,SAASC,GAAY,CACnB,OAAI,OAAO,OAAO,YAAe,WAAmB,GAC7C,OAAO,WAAWF,CAAa,EAAE,OAC1C,CAaA,SAASG,GAAU,CACjB,IAAIC,EAAQ,SAAS,eAAe,KAAK,EACzC,GAAI,CAACA,EAAO,OAAO,KACnB,IAAIC,EAASD,EAAM,sBAAsB,EAcrCE,EAAY,SAAS,eAAe,cAAc,EAClDC,EAASD,EAAYA,EAAU,sBAAsB,EAAI,KACzDE,GAAUD,GAAUA,EAAO,OAAS,EAAIA,EAAO,IAAMF,EAAO,QAAUP,EAEtEW,EAAW,SAAS,eAAe,iBAAiB,EACpDC,EAAcD,EAAWA,EAAS,sBAAsB,EAAI,KAEhE,MAAO,CACL,OAAQD,EACR,UAAW,KAAK,IAAI,EAAG,KAAK,MAAMA,EAASH,EAAO,IAAMR,CAAO,CAAC,EAChE,aAAca,GAAeA,EAAY,MAAQ,EAAIA,EAAY,KAAO,IAC1E,CACF,CAQA,SAASC,EAAMC,EAAI,CACjBA,EAAG,MAAM,IAAM,GACfA,EAAG,MAAM,OAAS,GAClBA,EAAG,MAAM,MAAQ,GACjBA,EAAG,MAAM,UAAY,EACvB,CAYA,SAASC,EAAcD,EAAI,CACzB,GAAKA,EAGL,IAFAX,EAAQ,IAAIW,CAAE,EAEV,CAACV,EAAU,EAAG,CAChBS,EAAMC,CAAE,EACR,MACF,CAEA,IAAIE,EAASX,EAAQ,EACrB,GAAI,CAACW,EAAQ,CACXH,EAAMC,CAAE,EACR,MACF,CAQA,GAAIA,EAAG,QAAQ,YAAc,OAAQ,CACnC,IAAIG,EAAU,SAAS,eAAe,qBAAqB,EACvDC,EAAaD,EAAUA,EAAQ,sBAAsB,EAAI,KACzDE,EACFD,GAAcA,EAAW,OAAS,EAC9BA,EAAW,OAASjB,EACpBe,EAAO,OAASA,EAAO,UAI7BF,EAAG,MAAM,IAAM,KAAK,MAAMK,CAAG,EAAI,KACjCL,EAAG,MAAM,OAAS,OAClBA,EAAG,MAAM,MAAQ,GACjBA,EAAG,MAAM,UAAY,KAAK,IAAI,EAAG,KAAK,MAAME,EAAO,OAASG,CAAG,CAAC,EAAI,KACpE,MACF,CAIAL,EAAG,MAAM,OAAS,KAAK,MAAM,OAAO,YAAcE,EAAO,MAAM,EAAI,KACnEF,EAAG,MAAM,UAAYE,EAAO,UAAY,KACpCA,EAAO,eAAiB,KAE1BF,EAAG,MAAM,MAAQ,GAEjBA,EAAG,MAAM,MACP,KAAK,MAAM,OAAO,WAAaE,EAAO,aAAef,CAAQ,EAAI,KAEvE,CAQA,OAAO,iBAAiB,SAAU,UAAY,CAC5CE,EAAQ,QAAQ,SAAUW,EAAI,CAC5BC,EAAcD,CAAE,CAClB,CAAC,CACH,CAAC,EAED,OAAO,iBAAmB,OAAO,OAAO,CACtC,QAAST,EACT,UAAWD,EACX,cAAeW,CACjB,CAAC,CACH,GAAG",
  "names": ["TOP_GAP", "BOTTOM_GAP", "SIDE_GAP", "DESKTOP_QUERY", "tracked", "isDesktop", "compute", "mapEl", "mapBox", "bottomRow", "rowBox", "floorY", "controls", "controlsBox", "clear", "el", "positionSheet", "bounds", "cluster", "clusterBox", "top"]
}
