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("");
}
// 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
// top). The tier the observed value falls into is highlighted with its value.
function ladderCard(title, m, fmt, kind) {
@ -153,7 +169,7 @@ function ladderCard(title, m, fmt, kind) {
? `median ${fmt(m.ladder.median)}`
: `${m.ladder.rain_days.toLocaleString()} rain days · dry ${m.ladder.dry_pct}%`;
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 active = t.c === activeClass ? " active" : "";
@ -163,9 +179,9 @@ function ladderCard(title, m, fmt, kind) {
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.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
? `<span class="ladder-mark">◀ ${fmt(obs.value)}</span>` : "";
? `<span class="ladder-mark">◀ ${bareVal(fmt, obs.value)}</span>` : "";
return `<div class="ladder-row${active}">
<span class="ladder-sw" style="background:${tierColor(t.c)}"></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; }
/* 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); } }
doesn't run as a single long strip of full-width ladders. minmax(0, 1fr)
(not bare 1fr, whose min is the content) so one wide ladder row can't inflate
the shared column track past the viewport and push the whole page off-center. */
#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 {
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-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-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,
so the arrow sits right beside the value range it points at. */
.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; }
@ -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-grid { column-gap: 8px; row-gap: 2px; }
/* 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-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; }
h1 { font-size: 19px; }