diff --git a/static/calendar.js b/static/calendar.js
index d5d7a9a..e104be0 100644
--- a/static/calendar.js
+++ b/static/calendar.js
@@ -144,19 +144,27 @@ initFindButton(findBtn, "Find a location",
() => selected, (lat, lon) => selectLocation(lat, lon));
// ---- metric toggle ----
-// Reflect the restored metric on the toggle (the HTML defaults to High).
-document.querySelectorAll("#metric-toggle button").forEach((b) =>
- b.classList.toggle("active", b.dataset.metric === metric));
-
-document.getElementById("metric-toggle").addEventListener("click", (e) => {
- const btn = e.target.closest("button[data-metric]");
- if (!btn) return;
- metric = btn.dataset.metric;
+// An inline selector (above the grid) plus a synced copy in the filter sheet; the sheet
+// copy mirrors the inline 8 buttons (cloned). Either drives the shared `metric`.
+document.getElementById("metric-toggle-sheet").innerHTML =
+ document.getElementById("metric-toggle").innerHTML;
+const syncColorButtons = () =>
+ document.querySelectorAll("#metric-toggle button, #metric-toggle-sheet button")
+ .forEach((b) => b.classList.toggle("active", b.dataset.metric === metric));
+function setColorMetric(m) {
+ metric = m;
try { localStorage.setItem("thermograph:calMetric", metric); } catch (err) {}
- document.querySelectorAll("#metric-toggle button").forEach((b) => b.classList.toggle("active", b === btn));
+ syncColorButtons();
renderCalendar();
renderKey();
-});
+}
+syncColorButtons(); // reflect the restored metric on both (the HTML defaults to High)
+for (const id of ["metric-toggle", "metric-toggle-sheet"]) {
+ document.getElementById(id).addEventListener("click", (e) => {
+ const btn = e.target.closest("button[data-metric]");
+ if (btn) setColorMetric(btn.dataset.metric);
+ });
+}
// ---- category totals: share vs. count toggle ----
// The strip normally prints each category's share; ticking "Show count" swaps every
@@ -290,9 +298,7 @@ filtersEl.addEventListener("change", (e) => {
// Wire the per-season "Months" expand toggles (delegated on the stable container),
// and the mobile floating pill that slides the whole filter panel up as a sheet.
initSeasonExpand(seasonYearEl);
-// On phones the "Color the calendar by" metric selector rides the filter sheet too.
-const calSheet = initFilterSheet({ panel: filtersEl, label: "Filters",
- sheetOnly: [document.getElementById("metric-block")] });
+const calSheet = initFilterSheet({ panel: filtersEl, label: "Filters" });
calSheet.setActive(checkedMonths.size < 12);
// Close any open filter dropdown when tapping outside it (native stays
diff --git a/static/compare.html b/static/compare.html
index 45f7d86..d0b5d30 100644
--- a/static/compare.html
+++ b/static/compare.html
@@ -81,10 +81,9 @@
Inner handles set your comfort range; outer handles set the tolerance range. Days in tolerance still count toward the score, scaled by how far past comfort they fall.
-
-
-
-
+
+
Judge each day by its
@@ -93,7 +92,9 @@
+
+
Time of year
@@ -110,6 +111,13 @@
Editing the range arms Load — tap it to reload.
+
+
+
+ Distribution by
+
+
diff --git a/static/compare.js b/static/compare.js
index 539cc24..ebc811e 100644
--- a/static/compare.js
+++ b/static/compare.js
@@ -196,6 +196,9 @@ const startInput = document.getElementById("cmp-start");
const endInput = document.getElementById("cmp-end");
const distSection = document.getElementById("cmp-dist");
const distToggle = document.getElementById("cmp-dist-metric");
+const distToggleSheet = document.getElementById("cmp-dist-metric-sheet");
+// The sheet copy mirrors the inline selector's 8 buttons (cloned) and stays in sync.
+distToggleSheet.innerHTML = distToggle.innerHTML;
const distBody = document.getElementById("cmp-dist-body");
@@ -602,15 +605,23 @@ thiInput.addEventListener("input", () => { thi = Math.max(+thiInput.value, hi);
// (its per-category averages + ranges are in the active unit).
onUnitChange(() => { syncSlicer(); renderResults(); renderDist(); });
-// Distribution: its own metric selector + share/count toggle, re-rendering in place.
-distToggle.addEventListener("click", (e) => {
- const btn = e.target.closest("button[data-metric]");
- if (!btn) return;
- distMetric = btn.dataset.metric;
+// Distribution metric: an inline selector (by the chart) plus a synced copy in the
+// filter sheet. Either one drives the shared distMetric; both reflect the active state.
+const syncDistButtons = () =>
+ document.querySelectorAll("#cmp-dist-metric button, #cmp-dist-metric-sheet button")
+ .forEach((b) => b.classList.toggle("active", b.dataset.metric === distMetric));
+function setDistMetric(m) {
+ distMetric = m;
lsSet(CMP.distMetric, distMetric);
- distToggle.querySelectorAll("button").forEach((b) => b.classList.toggle("active", b === btn));
+ syncDistButtons();
renderDist();
-});
+}
+for (const t of [distToggle, distToggleSheet]) {
+ t.addEventListener("click", (e) => {
+ const btn = e.target.closest("button[data-metric]");
+ if (btn) setDistMetric(btn.dataset.metric);
+ });
+}
distSection.addEventListener("change", (e) => {
if (!e.target.closest("#cmp-dist-count")) return;
distCount = e.target.checked;
@@ -678,13 +689,11 @@ clickOpensPicker(startInput, endInput); // whole-field tap opens the month pic
syncSlicer();
basisToggle.querySelectorAll("button").forEach((b) => b.classList.toggle("active", b.dataset.basis === basis));
- distToggle.querySelectorAll("button").forEach((b) => b.classList.toggle("active", b.dataset.metric === distMetric));
+ syncDistButtons(); // active state on both the inline + sheet distribution selectors
document.getElementById("cmp-dist-count").checked = distCount;
startInput.value = range.start; endInput.value = range.end;
buildSeasonFilter();
- // On phones the "Distribution by" metric selector rides the filter sheet too.
- fabSheet = initFilterSheet({ panel: params, label: "Filters",
- sheetOnly: [document.querySelector(".cmp-dist-controls")] });
+ fabSheet = initFilterSheet({ panel: params, label: "Filters" });
fabSheet.setActive(monthsActive());
// A shared link's locations win; else the last-used set; else seed from the spot
diff --git a/static/filtersheet.js b/static/filtersheet.js
index 67fac21..da9bccf 100644
--- a/static/filtersheet.js
+++ b/static/filtersheet.js
@@ -4,16 +4,14 @@
// phone breakpoint, so the panel stays inline exactly as before (all styling lives in
// the @media(max-width:640px) block in style.css). Shared by compare + calendar.
//
-// initFilterSheet({ panel, label, sheetOnly })
-// panel — the filter panel element (becomes the sheet on mobile)
-// label — text on the pill and the sheet header (e.g. "Filters")
-// sheetOnly — elements that live inline on desktop but move INTO the sheet on
-// mobile (e.g. a metric selector); restored to their slot on desktop
+// initFilterSheet({ panel, label })
+// panel — the filter panel element (becomes the sheet on mobile)
+// label — text on the pill and the sheet header (e.g. "Filters")
// returns { setActive(bool), close() }
const FUNNEL = ``;
-export function initFilterSheet({ panel, label = "Filters", sheetOnly = [] }) {
+export function initFilterSheet({ panel, label = "Filters" }) {
if (!panel) return { setActive() {}, close() {} };
// The pill lives in so it floats over the whole page regardless of scroll.
@@ -65,23 +63,6 @@ export function initFilterSheet({ panel, label = "Filters", sheetOnly = [] }) {
new MutationObserver(reflect).observe(panel, { attributes: true, attributeFilter: ["hidden"] });
reflect();
- // Relocate `sheetOnly` controls into the sheet on phones and back to their inline
- // slot on desktop, at the same 640px breakpoint the CSS uses. Each is remembered by
- // its original parent + next sibling; reparenting preserves their event listeners.
- const mq = window.matchMedia("(max-width: 640px)");
- const homes = sheetOnly.filter(Boolean).map((el) => ({ el, parent: el.parentNode, next: el.nextSibling }));
- function placeExtras() {
- for (const h of homes) {
- if (mq.matches) {
- if (h.el.parentNode !== panel) panel.append(h.el); // append after the grip + existing controls
- } else if (h.el.parentNode === panel) {
- h.parent.insertBefore(h.el, h.next); // restore (next === null → append)
- }
- }
- }
- mq.addEventListener("change", placeExtras);
- placeExtras();
-
return {
// Show a dot on the pill when a filter subset is active (so the applied filter is
// visible even with the sheet closed).
diff --git a/static/style.css b/static/style.css
index 19ce72c..a9369cd 100644
--- a/static/style.css
+++ b/static/style.css
@@ -1009,6 +1009,12 @@ main { max-width: 1200px; margin: 0 auto; padding: 20px 24px 60px; }
border: 1px solid var(--border); border-radius: 12px; background: var(--surface);
}
.cmp-comfort-wrap[hidden] { display: none; }
+/* The basis toggle now rides the comfort card (a scoring control, above the results). */
+.cmp-comfort-wrap .cmp-basis-block { margin-top: 12px; }
+/* Sheet-only duplicate metric selectors: hidden on desktop (the inline copy shows),
+ revealed inside the filter sheet on phones (see the max-width:640px block). */
+.cmp-dist-metric-block, .cal-metric-sheet { display: none; }
+.cal-metric-sheet .cal-filter-label { display: block; margin: 0 0 8px; }
.cmp-range-labels { display: flex; flex-wrap: wrap; gap: 4px 18px; margin: 0 0 6px; font-size: 13px; color: var(--muted); }
.cmp-rl b { color: var(--text); font-size: 15px; margin-left: 2px; }
.cmp-rl-comfort b { color: var(--accent); }
@@ -1157,6 +1163,8 @@ main { max-width: 1200px; margin: 0 auto; padding: 20px 24px 60px; }
.cmp-params { grid-template-columns: 1fr; }
.cmp-basis-block .metric-toggle button { flex: 1 0 22%; padding: 10px 4px; }
+ /* Reveal the synced metric selectors inside the filter sheet on phones. */
+ .cmp-dist-metric-block, .cal-metric-sheet { display: block; }
/* --- filters reachable from anywhere: a floating pill slides the page's filter
panel up as a bottom sheet (compare #cmp-params, calendar #cal-filters) --- */