diff --git a/static/app.js b/static/app.js
index 8ea1965..19fc983 100644
--- a/static/app.js
+++ b/static/app.js
@@ -170,10 +170,7 @@ function render(data) {
${IC.calendar}
-
- Check historical data
- The last 2 years, graded day by day
-
+ Check historical data
→
${data.target_date} vs a typical day — tap a metric for the full breakdown
@@ -263,6 +260,14 @@ window.Thermograph.onUnitChange(() => {
if (recentData) render(recentData);
});
+// Rebuild the chart when the window is resized so its drawing width keeps
+// tracking the container (debounced — resize fires continuously while dragging).
+let resizeTimer = null;
+window.addEventListener("resize", () => {
+ clearTimeout(resizeTimer);
+ resizeTimer = setTimeout(() => { if (recentData) renderSeries(); }, 160);
+});
+
function renderSeries() {
const host = document.getElementById("series");
if (!host || !recentData) return;
@@ -369,9 +374,13 @@ function chartPalette() {
};
}
-const W = 720, H = 340;
+// W/PW are recomputed per build from the container width (see buildChart), so a
+// wide monitor gets a genuinely wider drawing — more room between days — instead
+// of the same 720-unit canvas magnified.
+let W = 720;
+const H = 340;
const PL = 46, PR = 62, plotTop = 48, plotBot = 288, xLabY = 308;
-const PW = W - PL - PR;
+let PW = W - PL - PR;
const ord = (n) => {
const r = Math.round(n), s = ["th", "st", "nd", "rd"], v = r % 100;
return r + (s[(v - 20) % 10] || s[v] || s[0]);
@@ -397,6 +406,11 @@ let chartMetric = localStorage.getItem("thermograph:chartMetric") || "tmax";
function buildChart(data) {
const wrap = document.getElementById("chart-wrap");
const C = chartPalette();
+ // Drawing width tracks the on-screen box at a steady ~1.5 CSS px per SVG unit,
+ // so text renders the same size everywhere while wide monitors gain plot room.
+ // Phones keep the 720-unit floor (the SVG just scales down).
+ W = Math.max(720, Math.min(1400, Math.round((wrap.clientWidth || 720) / 1.5)));
+ PW = W - PL - PR;
lastGraded = data; // the series currently drawn (for PNG export)
chartDays = [...data.recent].reverse(); // oldest -> newest
const days = chartDays, n = days.length;
diff --git a/static/calendar.js b/static/calendar.js
index aba08fd..70e870c 100644
--- a/static/calendar.js
+++ b/static/calendar.js
@@ -412,6 +412,13 @@ function markRangeDirty() { refreshBtn.hidden = false; }
startInput.addEventListener("change", markRangeDirty);
endInput.addEventListener("change", markRangeDirty);
+// Clicking anywhere in a From/To field opens the native month/year picker.
+// Desktop Chrome otherwise only opens it from the tiny calendar glyph and just
+// focuses a segment. showPicker isn't everywhere (Safari/Firefox) — ignore there.
+for (const el of [startInput, endInput]) {
+ el.addEventListener("click", () => { try { el.showPicker(); } catch (e) {} });
+}
+
// Confirm: read the pickers into the active range and (re)load.
function applyRange() {
let s = startInput.value ? monthStart(startInput.value) : null;
diff --git a/static/compare.js b/static/compare.js
index 6374e89..15b719e 100644
--- a/static/compare.js
+++ b/static/compare.js
@@ -383,6 +383,11 @@ basisToggle.addEventListener("click", (e) => {
startInput.max = endInput.max = `${new Date().getFullYear()}-12`;
startInput.addEventListener("change", renderAll);
endInput.addEventListener("change", renderAll);
+// Clicking anywhere in a From/To field opens the native month/year picker
+// (mirrors the Calendar range; showPicker is missing in some browsers — ignore).
+for (const el of [startInput, endInput]) {
+ el.addEventListener("click", () => { try { el.showPicker(); } catch (e) {} });
+}
// ---- init ----
(function restore() {
diff --git a/static/style.css b/static/style.css
index c7345c1..f2584e3 100644
--- a/static/style.css
+++ b/static/style.css
@@ -58,7 +58,10 @@ header {
border-bottom: 1px solid var(--border);
background: var(--surface);
}
-.brand { display: flex; align-items: center; gap: 14px; }
+/* Header content shares the main column's width so the logo and tabs line up
+ with the content edges instead of hugging the monitor corners. (1152 = main's
+ 1200 max minus its 24px side paddings; the header carries its own padding.) */
+.brand { display: flex; align-items: center; gap: 14px; max-width: 1152px; margin: 0 auto; }
/* The title block (logo's neighbour) grows and may shrink, so the unit toggle is
pushed to the top-right on the logo's row and the long tagline wraps inside it
instead of bumping the toggle onto its own line. */
@@ -79,6 +82,22 @@ header.compact h1 { font-size: 18px; }
main { max-width: 1200px; margin: 0 auto; padding: 20px 24px 60px; }
+/* Larger monitors: widen the content column in steps (1440p, 2K, 4K) so pages
+ use the screen instead of a 1200px strip. Each page's own grids (normals,
+ calendar months, day cards) flow into the extra room. */
+@media (min-width: 1680px) {
+ main { max-width: 1440px; }
+ .brand { max-width: 1392px; }
+}
+@media (min-width: 2400px) {
+ main { max-width: 1640px; }
+ .brand { max-width: 1592px; }
+}
+@media (min-width: 3400px) {
+ main { max-width: 1880px; }
+ .brand { max-width: 1832px; }
+}
+
.controls { margin-bottom: 18px; display: flex; flex-wrap: wrap; gap: 16px; align-items: flex-start; }
.search { position: relative; flex: 1 1 340px; display: flex; gap: 8px; }
.search input {
@@ -159,24 +178,23 @@ main { max-width: 1200px; margin: 0 auto; padding: 20px 24px 60px; }
.wx { width: 1.15em; height: 1.15em; vertical-align: -0.2em; }
.share button:hover, .share .share-btn:hover { border-color: var(--accent); }
-/* Prominent call-to-action into the 2-year calendar — the primary next step,
- so it reads unmistakably as a tappable button (full width, big touch target). */
+/* Call-to-action into the 2-year calendar — a compact button-sized link, so the
+ graded results stay the page's focal point. */
.cta-cal {
- display: flex; align-items: center; gap: 13px;
- width: 100%; margin: 0 0 20px; padding: 14px 16px; min-height: 60px;
- border: none; border-radius: 13px; text-decoration: none;
+ display: inline-flex; align-items: center; gap: 9px;
+ margin: 0 0 20px; padding: 10px 16px; min-height: 44px;
+ border: none; border-radius: 10px; text-decoration: none;
+ font-size: 14px; font-weight: 700;
color: #1a1206; background: linear-gradient(135deg, #f79556, #ea722c);
- box-shadow: 0 4px 16px rgba(240, 128, 60, .3);
+ box-shadow: 0 2px 8px rgba(240, 128, 60, .25);
transition: transform .12s ease, box-shadow .12s ease, filter .12s ease;
}
-.cta-cal:hover { transform: translateY(-1px); box-shadow: 0 7px 22px rgba(240, 128, 60, .42); filter: brightness(1.04); }
-.cta-cal:active { transform: translateY(0); box-shadow: 0 3px 10px rgba(240, 128, 60, .34); }
+.cta-cal .ic-lg { width: 18px; height: 18px; }
+.cta-cal:hover { transform: translateY(-1px); box-shadow: 0 4px 12px rgba(240, 128, 60, .35); filter: brightness(1.04); }
+.cta-cal:active { transform: translateY(0); box-shadow: 0 2px 6px rgba(240, 128, 60, .3); }
.cta-cal:focus-visible { outline: 3px solid var(--text); outline-offset: 2px; }
-.cta-cal-icon { font-size: 26px; line-height: 1; flex-shrink: 0; }
-.cta-cal-text { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
-.cta-cal-title { font-size: 16px; font-weight: 800; letter-spacing: .01em; }
-.cta-cal-sub { font-size: 12.5px; font-weight: 600; opacity: .8; }
-.cta-cal-arrow { margin-left: auto; font-size: 22px; font-weight: 800; flex-shrink: 0; }
+.cta-cal-icon { line-height: 1; flex-shrink: 0; display: inline-flex; }
+.cta-cal-arrow { font-size: 16px; font-weight: 800; flex-shrink: 0; }
#chart-wrap { position: relative; margin-bottom: 22px; }
#chart-wrap svg { width: 100%; height: auto; display: block; border-radius: 10px; touch-action: pan-y; }
@@ -332,7 +350,7 @@ main { max-width: 1200px; margin: 0 auto; padding: 20px 24px 60px; }
.range-refresh:hover { filter: brightness(1.05); }
/* Time-filter panel: month/year range + season + year checklists, stacked in a
- tidy column so nothing balloons on wide screens or leaves gaps on phones. */
+ tidy column on phones. */
.cal-filters {
display: flex; flex-direction: column; gap: 16px;
max-width: 560px; margin: 0 0 18px; padding: 14px 16px;
@@ -340,6 +358,25 @@ main { max-width: 1200px; margin: 0 auto; padding: 20px 24px 60px; }
}
/* Season + year dropdowns fill the row evenly — no dead space to their right. */
.cal-seasonyear { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }
+/* Desktop: the panel spans the content width as one row — the date range on the
+ left, the season/year/weather dropdowns filling the rest — instead of a narrow
+ stacked box with dead space beside it. */
+@media (min-width: 960px) {
+ .cal-filters { max-width: none; flex-direction: row; align-items: flex-start; gap: 14px 28px; }
+ .cal-range { flex: 0 1 400px; }
+ .cal-seasonyear { flex: 1 1 500px; grid-template-columns: 1fr 1fr 1.6fr; align-content: start; }
+ /* A head row mirroring the Range label, so both halves read as label + controls. */
+ .cal-seasonyear::before {
+ content: "Filter by"; grid-column: 1 / -1;
+ font-size: 11px; text-transform: uppercase; letter-spacing: .04em;
+ color: var(--muted); font-weight: 700; line-height: 1.45;
+ }
+ /* Weather joins the same row as Seasons + Years (it only spans the full width
+ in the stacked phone layout — the extra .cal-seasonyear qualifier outranks
+ that base rule); its panel keeps a usable two-column width, anchored right. */
+ .cal-seasonyear .cal-weather { grid-column: auto; }
+ .cal-seasonyear .cal-weather .cal-dd-panel { left: auto; right: 0; min-width: 420px; }
+}
.cal-filter-group { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; }
.cal-filter-label {
font-size: 11px; text-transform: uppercase; letter-spacing: .04em;
@@ -370,11 +407,14 @@ main { max-width: 1200px; margin: 0 auto; padding: 20px 24px 60px; }
border-radius: 999px;
}
-/* Months stacked vertically (one per row), with roomier cells. */
-.calendar { display: flex; flex-direction: column; gap: 18px; margin-bottom: 22px; align-items: stretch; }
-/* Each month fills the available width (capped on wide desktops so the squares
- don't balloon); the 7 columns split it evenly and cells stay square. */
-.cal-month { width: 100%; max-width: 560px; }
+/* Months flow into as many columns as fit (one on phones, several on monitors),
+ so a multi-year span reads as a year-at-a-glance wall instead of one long
+ thin strip. Cells stay square via aspect-ratio. */
+.calendar {
+ display: grid; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
+ gap: 20px 24px; margin-bottom: 22px; align-items: start;
+}
+.cal-month { width: 100%; }
.cal-month h4 { margin: 0 0 6px; font-size: 14px; color: var(--text); font-weight: 700; }
.cal-dows, .cal-grid { display: grid; grid-template-columns: repeat(7, 1fr); gap: 5px; }
.cal-dows { margin-bottom: 3px; }
@@ -444,9 +484,15 @@ main { max-width: 1200px; margin: 0 auto; padding: 20px 24px 60px; }
.day-step:hover:not(:disabled) { border-color: var(--accent); }
.day-step:disabled { opacity: .4; cursor: default; }
+/* Metric cards: one column on phones, side by side on monitors so the page
+ doesn't run as a single long strip of full-width ladders. */
+#day-body { display: grid; grid-template-columns: 1fr; gap: 16px; align-items: start; }
+@media (min-width: 1000px) { #day-body { grid-template-columns: repeat(2, 1fr); } }
+@media (min-width: 3400px) { #day-body { grid-template-columns: repeat(3, 1fr); } }
+
.ladder-card {
background: var(--surface); border: 1px solid var(--border);
- border-radius: 13px; padding: 16px 16px 12px; margin-bottom: 16px;
+ border-radius: 13px; padding: 16px 16px 12px;
}
.ladder-head { display: flex; align-items: baseline; justify-content: space-between; gap: 12px; flex-wrap: wrap; }
.ladder-head h3 { margin: 0; font-size: 15px; }
@@ -787,7 +833,7 @@ main { max-width: 1200px; margin: 0 auto; padding: 20px 24px 60px; }
"Show count" is ticked, in that category's own status color) above a thin status-
colored line rising from a shared baseline (height ∝ share). Every category draws
a line — including the neutral "Normal" center of the temperature scale. */
-.cal-totals { max-width: 560px; margin: 0 0 18px; }
+.cal-totals { max-width: 720px; margin: 0 0 18px; }
/* Header row: the "N days shown — by X" title, with the share/count toggle at the
far end. Wraps to its own line on narrow screens rather than crushing the title. */
.ct-head { display: flex; align-items: baseline; justify-content: space-between; gap: 10px 16px; flex-wrap: wrap; }
@@ -911,9 +957,11 @@ main { max-width: 1200px; margin: 0 auto; padding: 20px 24px 60px; }
.cmp-bar { display: flex; height: 14px; border-radius: 7px; overflow: hidden; margin: 12px 0 10px; background: var(--surface-2); }
.cmp-seg { display: block; height: 100%; }
-.cmp-below { background: var(--cold); }
-.cmp-comfort { background: var(--normal); }
-.cmp-above { background: var(--hot); }
+/* Scoped to the bar segments — .cmp-comfort is also the comfort-slider param's
+ class, which must not inherit the green fill. */
+.cmp-seg.cmp-below { background: var(--cold); }
+.cmp-seg.cmp-comfort { background: var(--normal); }
+.cmp-seg.cmp-above { background: var(--hot); }
.cmp-stats { display: grid; grid-template-columns: repeat(3, 1fr); gap: 10px; }
.cmp-stat { text-align: center; padding: 8px 4px; border-radius: 8px; background: var(--surface-2); }