Day page: stop metric cards overflowing the viewport on phones

The #day-body card grid used bare 1fr columns, whose minimum track size is
the widest card's min-content. The humidity ladder's nowrap value ranges
("17.3 g/m³–19.9 g/m³") pushed that minimum past the viewport on phones —
the page scrolled sideways 21px at 390px wide and 51px at 360px — so the
whole Day view sat off-center.

- Size #day-body columns with minmax(0, 1fr) so card content can never
  widen the page canvas.
- Collapse the unit repeated on both range bounds ("17.3–19.9 g/m³") and
  drop it from the observed ◀ marker; the unit still reads from the card
  summary, median, and historical range.
- Let .ladder-val wrap as a last resort instead of nowrap-widening the row.
- Rebalance the mobile ladder tracks (pct 54→46, marker 48→42) so the
  longest range stays on one line down to ~375px screens; narrower than
  that it wraps cleanly at the unit.
This commit is contained in:
Emi Griffith 2026-07-11 14:13:05 -07:00
parent 431457132a
commit e5b1bb0fc2
2 changed files with 35 additions and 10 deletions

View file

@ -132,6 +132,22 @@ function render(data) {
].join(""); ].join("");
} }
// Collapse a space-separated unit shared by both bounds ("17.3 g/m³19.9 g/m³"
// → "17.319.9 g/m³") so ranges fit the narrow value column on phones. Attached
// units (76°, 0.25") pass through untouched.
function fmtRange(fmt, lo, hi) {
const a = fmt(lo), b = fmt(hi);
const i = a.lastIndexOf(" ");
if (i > 0 && b.endsWith(a.slice(i))) return `${a.slice(0, i)}${b}`;
return `${a}${b}`;
}
// The unit-less value for the ◀ marker — the unit is already on the range beside
// it and in the card's summary, and dropping it keeps the marker on one line.
const bareVal = (fmt, v) => {
const s = fmt(v), i = s.lastIndexOf(" ");
return i > 0 ? s.slice(0, i) : s;
};
// One metric card: an observed summary line + the tier ladder (highest tier on // One metric card: an observed summary line + the tier ladder (highest tier on
// top). The tier the observed value falls into is highlighted with its value. // top). The tier the observed value falls into is highlighted with its value.
function ladderCard(title, m, fmt, kind) { function ladderCard(title, m, fmt, kind) {
@ -153,7 +169,7 @@ function ladderCard(title, m, fmt, kind) {
? `median ${fmt(m.ladder.median)}` ? `median ${fmt(m.ladder.median)}`
: `${m.ladder.rain_days.toLocaleString()} rain days · dry ${m.ladder.dry_pct}%`; : `${m.ladder.rain_days.toLocaleString()} rain days · dry ${m.ladder.dry_pct}%`;
const note = `<span class="ladder-note-left">${noteLeft}</span>` + const note = `<span class="ladder-note-left">${noteLeft}</span>` +
`<span class="ladder-range">Historical Range ${fmt(m.ladder.min)}${fmt(m.ladder.max)}</span>`; `<span class="ladder-range">Historical Range ${fmtRange(fmt, m.ladder.min, m.ladder.max)}</span>`;
const rows = m.ladder.tiers.map((t) => { const rows = m.ladder.tiers.map((t) => {
const active = t.c === activeClass ? " active" : ""; const active = t.c === activeClass ? " active" : "";
@ -163,9 +179,9 @@ function ladderCard(title, m, fmt, kind) {
if (t.c === "dry") { val = t.range; pct = ""; } if (t.c === "dry") { val = t.range; pct = ""; }
else if (t.hi == null) val = `> ${fmt(t.lo)}`; // top tier: strictly beyond p99 else if (t.hi == null) val = `> ${fmt(t.lo)}`; // top tier: strictly beyond p99
else if (t.lo == null) val = `< ${fmt(t.hi)}`; // bottom tier: strictly below p1 else if (t.lo == null) val = `< ${fmt(t.hi)}`; // bottom tier: strictly below p1
else val = `${fmt(t.lo)}${fmt(t.hi)}`; else val = fmtRange(fmt, t.lo, t.hi);
const marker = t.c === activeClass && obs const marker = t.c === activeClass && obs
? `<span class="ladder-mark">◀ ${fmt(obs.value)}</span>` : ""; ? `<span class="ladder-mark">◀ ${bareVal(fmt, obs.value)}</span>` : "";
return `<div class="ladder-row${active}"> return `<div class="ladder-row${active}">
<span class="ladder-sw" style="background:${tierColor(t.c)}"></span> <span class="ladder-sw" style="background:${tierColor(t.c)}"></span>
<span class="ladder-label" style="--cat:${tierColor(t.c)}">${t.label}</span> <span class="ladder-label" style="--cat:${tierColor(t.c)}">${t.label}</span>

View file

@ -459,10 +459,12 @@ main { max-width: 1200px; margin: 0 auto; padding: 20px 24px 60px; }
.day-step:disabled { opacity: .4; cursor: default; } .day-step:disabled { opacity: .4; cursor: default; }
/* Metric cards: one column on phones, side by side on monitors so the page /* 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. */ doesn't run as a single long strip of full-width ladders. minmax(0, 1fr)
#day-body { display: grid; grid-template-columns: 1fr; gap: 16px; align-items: start; } (not bare 1fr, whose min is the content) so one wide ladder row can't inflate
@media (min-width: 1000px) { #day-body { grid-template-columns: repeat(2, 1fr); } } the shared column track past the viewport and push the whole page off-center. */
@media (min-width: 3400px) { #day-body { grid-template-columns: repeat(3, 1fr); } } #day-body { display: grid; grid-template-columns: minmax(0, 1fr); gap: 16px; align-items: start; }
@media (min-width: 1000px) { #day-body { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
@media (min-width: 3400px) { #day-body { grid-template-columns: repeat(3, minmax(0, 1fr)); } }
.ladder-card { .ladder-card {
background: var(--surface); border: 1px solid var(--border); background: var(--surface); border: 1px solid var(--border);
@ -502,10 +504,14 @@ main { max-width: 1200px; margin: 0 auto; padding: 20px 24px 60px; }
.ladder-sw { width: 14px; height: 14px; border-radius: 4px; } .ladder-sw { width: 14px; height: 14px; border-radius: 4px; }
.ladder-label { font-weight: 700; color: color-mix(in oklab, var(--cat, var(--text)), var(--text) 22%); } .ladder-label { font-weight: 700; color: color-mix(in oklab, var(--cat, var(--text)), var(--text) 22%); }
.ladder-pct { color: var(--muted); font-size: 12px; } .ladder-pct { color: var(--muted); font-size: 12px; }
.ladder-val { justify-self: end; font-variant-numeric: tabular-nums; white-space: nowrap; } /* Value ranges may wrap (long unit strings like "8.1 g/m³12.9 g/m³" on narrow
phones) nowrap here would widen the row past the card and scroll the page. */
.ladder-val { justify-self: end; text-align: right; font-variant-numeric: tabular-nums; }
/* Observed marker: stays in the rightmost column but reads from its left edge, /* Observed marker: stays in the rightmost column but reads from its left edge,
so the arrow sits right beside the value range it points at. */ so the arrow sits right beside the value range it points at. */
.ladder-mk { justify-self: start; min-width: 0; } .ladder-mk { justify-self: start; min-width: 0; }
/* Markers are unit-less ("◀ 11.5") so nowrap can't push them past the card
the widest (precip, 0.25") bleeds a few px into the row padding at most. */
.ladder-mark { color: var(--accent); font-weight: 800; white-space: nowrap; font-size: 12.5px; } .ladder-mark { color: var(--accent); font-weight: 800; white-space: nowrap; font-size: 12.5px; }
@ -857,10 +863,13 @@ main { max-width: 1200px; margin: 0 auto; padding: 20px 24px 60px; }
#cal-tip .tt-type { font-size: 13.5px; margin: 1px 0 5px; padding-bottom: 5px; } #cal-tip .tt-type { font-size: 13.5px; margin: 1px 0 5px; padding-bottom: 5px; }
#cal-tip .tt-grid { column-gap: 8px; row-gap: 2px; } #cal-tip .tt-grid { column-gap: 8px; row-gap: 2px; }
/* Day ladder on mobile: tighten the fixed tracks but keep every column (including /* Day ladder on mobile: tighten the fixed tracks but keep every column (including
the percentile range) so the ranges still line up down the ladder. */ the percentile range) so the ranges still line up down the ladder. Slack moved
from the pct/marker tracks to the value column keeps the longest range
("17.319.9 g/m³", ~88px) on one line down to ~375px screens; narrower than
that it wraps at the unit. The label track can't give ("Above Normal" 89px). */
.day-nav { width: 100%; } .day-nav { width: 100%; }
.day-date { flex: 1; } .day-date { flex: 1; }
.ladder-row { grid-template-columns: 13px 90px 54px 1fr 48px; column-gap: 8px; font-size: 12.5px; } .ladder-row { grid-template-columns: 13px 90px 46px 1fr 42px; column-gap: 8px; font-size: 12.5px; }
header { padding: 14px 16px; } header { padding: 14px 16px; }
h1 { font-size: 19px; } h1 { font-size: 19px; }