strip more seamless and margin improvements

This commit is contained in:
Vincent van der Wal
2026-08-01 11:38:02 +02:00
parent 99c1a89537
commit a0379a1fd6
13 changed files with 706 additions and 188 deletions
@@ -5,8 +5,14 @@
import { getTempStyle } from '../../utils/colors';
import { getWeatherIconName } from '../../utils/weather-codes';
import { precipIsSignificant, windIsSignificant } from './significance';
import { type FetchedDaily, type WeatherUnits } from './types';
import {
getSunshineColor,
getSunshinePercent,
precipIsSignificant,
sunIsSignificant,
windIsSignificant
} from './significance';
import { type FetchedDaily, type WeatherUnits, getWindArrowRotation } from './types';
interface Props {
daily: FetchedDaily | null;
@@ -30,9 +36,11 @@
onExtendPast
}: Props = $props();
// ─── Scroll-driven collapse (full → compact, mobile only) ───────────────────
// ─── Scroll-driven collapse (full cards → compact strip) ───────────────────
// All sizing derives from a single registered custom property `--strip-p`
// (0 = full cards, 1 = compact strip):
// (0 = full cards, 1 = compact strip). Mobile and desktop share the exact
// same mechanics — desktop just uses larger "full" tunables and collapses
// into a bar that matches the topbar height.
//
// * Browsers with CSS scroll-driven animations (Chrome/Edge 115+, Safari 26+)
// scrub `--strip-p` natively from a view-timeline on the sentinel below —
@@ -40,36 +48,37 @@
// * Everything else (Firefox, older Safari) snaps between the two states
// with a short CSS transition instead: an IntersectionObserver on the same
// sentinel toggles `.compact`. No per-frame scroll handler anywhere.
//
// On md+ the strip is pinned to the compact state (no animation) and docks
// under the topbar as a slim always-sticky day picker.
let sentinelEl = $state<HTMLDivElement>();
let stripScrollEl = $state<HTMLDivElement>();
let daysWrapEl = $state<HTMLDivElement>();
let needsSnapFallback = $state(false);
let compact = $state(false);
let stuck = $state(false);
onMount(() => {
const scrubSupported =
CSS.supports('animation-timeline: view()') && CSS.supports('timeline-scope: none');
if (scrubSupported || !sentinelEl) return;
needsSnapFallback = true;
// Collapse once more than half of the sentinel band has scrolled past the
// top; expands again on the same boundary (the transition smooths both).
// `stuck` fires the moment the strip pins (turns the bar opaque before any
// content can slide under it); `compact` snaps the cells once more than
// half of the sentinel band has scrolled past. The transitions smooth both.
const io = new IntersectionObserver(
(entries) => {
const e = entries[entries.length - 1];
stuck = e.intersectionRatio < 0.97;
compact = e.intersectionRatio < 0.5;
},
{ threshold: [0.25, 0.5, 0.75] }
{ threshold: [0.25, 0.5, 0.75, 0.97] }
);
io.observe(sentinelEl);
return () => io.disconnect();
});
// Start scrolled so the "Past" button sits just off the left edge (revealed by
// scrolling left), with the first day flush to the content edge — same as the
// desktop cards. Runs once per dataset.
// scrolling left), with the first day flush to the content edge. The days
// group has min-width:100%, so the row always overflows by exactly the past
// button — this works at every viewport size. Runs once per dataset.
let scrolledForRef: FetchedDaily | null = null;
$effect(() => {
const d = daily;
@@ -77,10 +86,18 @@
scrolledForRef = d;
const scroll = stripScrollEl;
const wrap = daysWrapEl;
requestAnimationFrame(() => {
// 12px = the strip's px-3 left padding, so the first day lands on the edge
// the delta form is self-correcting (a no-op once right), so apply after
// layout and once more after late-loading CSS/fonts settle — otherwise a
// sliver of the past button can stay visible on first paint
const apply = () => {
const padLeft = parseFloat(getComputedStyle(scroll).paddingLeft) || 0;
const firstCell = wrap.querySelector('.strip-cell') ?? wrap;
scroll.scrollLeft +=
wrap.getBoundingClientRect().left - scroll.getBoundingClientRect().left - 12;
firstCell.getBoundingClientRect().left - scroll.getBoundingClientRect().left - padLeft;
};
requestAnimationFrame(() => {
apply();
setTimeout(apply, 250);
});
});
</script>
@@ -96,26 +113,33 @@
class="daystrip sticky -top-3 z-30 -mx-3 lg:-top-6 lg:-mx-8"
class:js-snap={needsSnapFallback}
class:compact
class:stuck
>
<div class="strip-row flex overflow-x-auto px-3 py-2 md:py-1 lg:px-8" bind:this={stripScrollEl}>
<div class="strip-row flex overflow-x-auto px-3 lg:px-8" bind:this={stripScrollEl}>
{#if daily}
{#if canExtendPast && onExtendPast}
<!-- starts scrolled out of view (revealed by scrolling left); styled to
match the 15-days button on the other end -->
<button
type="button"
class="strip-side flex shrink-0 flex-col items-center justify-center rounded-xl border border-dashed border-border/70 text-muted-foreground"
class="strip-side flex shrink-0 cursor-pointer flex-col items-center justify-center gap-0.5 rounded-xl border border-dashed border-primary/50 bg-primary/5 text-primary transition-colors hover:border-primary hover:bg-primary/10"
onclick={onExtendPast}
aria-label="Load recent past days"
aria-label="Load the past 3 days"
title="Load the past 3 days"
>
<svg
class="h-5 w-5 md:h-4 md:w-4"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
stroke-width="1.75"
>
<path stroke-linecap="round" stroke-linejoin="round" d="M15 6l-6 6 6 6" />
</svg>
<span class="text-[9px] leading-tight font-semibold">Past</span>
<span class="inline-flex items-baseline gap-0.5">
<svg
class="h-3 w-3 translate-y-px"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
stroke-width="2.5"
>
<path stroke-linecap="round" stroke-linejoin="round" d="M15 5l-7 7 7 7" />
</svg>
<span class="text-sm leading-none font-extrabold">3</span>
</span>
<span class="text-[9px] leading-tight font-semibold">past</span>
</button>
{/if}
@@ -129,13 +153,23 @@
{@const nightCode = daily.nightCodes?.[index] ?? wCode}
{@const precipSum = daily.daily.precipitation_sum[index]}
{@const windMax = daily.daily.windspeed_10m_max[index]}
{@const gustMax = daily.daily.windgusts_10m_max[index]}
{@const windDir = daily.daily.winddirection_10m_dominant[index]}
{@const sunDuration = daily.daily.sunshine_duration[index]}
{@const daylightSec = Math.max(
0,
(daily.daily.sunset[index] ?? 0) - (daily.daily.sunrise[index] ?? 0)
)}
{@const sunColor = getSunshineColor(sunDuration, daylightSec)}
{@const sunPct = getSunshinePercent(sunDuration, daylightSec)}
{@const lowSun = !sunIsSignificant(sunDuration, daylightSec)}
{@const maxStyle = getTempStyle(tempMax, String(units.temperature_unit))}
{@const lowPrecip = !precipIsSignificant(precipSum, String(units.precipitation_unit))}
{@const lowWind = !windIsSignificant(windMax, null, String(units.wind_speed_unit))}
{@const lowWind = !windIsSignificant(windMax, gustMax, String(units.wind_speed_unit))}
{#if tempMax != null && !isNaN(tempMax) && !(tempMax === 0 && tempMin === 0)}
<button
type="button"
class="strip-cell flex shrink-0 cursor-pointer flex-col items-center justify-start gap-0.5 rounded-xl border px-1 pb-1.5 md:gap-0 md:pb-0 {selected
class="strip-cell flex shrink-0 cursor-pointer flex-col items-center justify-start gap-0.5 rounded-xl border px-1 {selected
? 'border-primary bg-primary/10 ring-1 ring-primary/50'
: 'border-border/60 bg-card'}"
aria-pressed={selected}
@@ -144,7 +178,7 @@
<!-- weekday drifts to the left edge and the date fades in at the
right edge as the cards collapse (flex spacers driven by --rel) -->
<span
class="dow-row flex w-full items-baseline px-0.5 text-[11px] font-semibold tracking-wide whitespace-nowrap md:text-[10px] {selected
class="dow-row flex w-full items-baseline px-0.5 font-semibold tracking-wide whitespace-nowrap {selected
? 'text-primary'
: ''}"
>
@@ -158,24 +192,27 @@
</span>
<span
class="rel-label overflow-hidden text-[9px] leading-[1.25] whitespace-nowrap text-muted-foreground"
class="rel-label overflow-hidden leading-[1.25] whitespace-nowrap text-muted-foreground"
>
{getRelativeDayLabel(time, daily.timezone)}
</span>
<div class="icon-wrap relative">
<svg class="day-icon fill-foreground">
<use
xlink:href="/images/weather-icons/{getWeatherIconName(
dayCode,
true
)}.svg#Layer_1"
></use>
</svg>
<!-- night companion icon, present in the full view, fades as it collapses -->
<svg
class="night-badge absolute -right-1 -bottom-0.5 rounded-full bg-card fill-foreground/60 p-px ring-1 ring-border/60"
>
<!-- Two centered rows: day + night icons, then day + night temps.
Both stay perfectly centered when compact (the night icon melts
away and the day icon re-centers on its own); when full, small
k-driven nudges line each temp up under its icon. -->
<div class="flex w-full items-center justify-center">
<div class="icon-wrap">
<svg class="day-icon fill-foreground">
<use
xlink:href="/images/weather-icons/{getWeatherIconName(
dayCode,
true
)}.svg#Layer_1"
></use>
</svg>
</div>
<svg class="night-icon self-end fill-foreground/60">
<use
xlink:href="/images/weather-icons/{getWeatherIconName(
nightCode,
@@ -185,9 +222,9 @@
</svg>
</div>
<div class="flex items-baseline gap-1 leading-none">
<div class="flex w-full items-baseline justify-center gap-0.5 leading-none">
<span
class="temp-max rounded-md py-0.5 font-extrabold tabular-nums md:py-px"
class="temp-max rounded-md font-extrabold tabular-nums"
style="background-color:{maxStyle.bg};color:{maxStyle.fg}"
>
{tempMax.toFixed(0)}°
@@ -200,40 +237,90 @@
<div
class="detail-row flex w-full flex-col items-center gap-0.5 overflow-hidden text-[10px] tabular-nums text-muted-foreground"
>
<span class="inline-flex items-center gap-1 {lowPrecip ? 'opacity-40' : ''}">
<svg class="fill-sky-500" width="13" height="13">
<use xlink:href="/images/weather-icons/wi-raindrop.svg#Layer_1"></use>
<!-- sunshine bar (desktop full cards only, like the old day cards) -->
<div
class="hidden w-full items-center gap-1 px-0.5 md:my-1 md:flex {lowSun
? 'opacity-45'
: ''}"
>
<svg class="shrink-0" width="14" height="14" style="fill:{sunColor}">
<use xlink:href="/images/weather-icons/wi-day-sunny.svg#Layer_1"></use>
</svg>
{Number(precipSum ?? 0).toFixed(precipSum >= 10 ? 0 : 1)}
</span>
<span class="inline-flex items-center gap-1 {lowWind ? 'opacity-40' : ''}">
<svg class="fill-muted-foreground" width="13" height="13">
<use xlink:href="/images/weather-icons/wi-strong-wind.svg#Layer_1"></use>
</svg>
{windMax?.toFixed(0) ?? '-'}
</span>
<div class="h-1 min-w-0 flex-1 overflow-hidden rounded-full bg-muted">
<div
class="h-full rounded-full"
style="width:{sunPct}%;background-color:{sunColor}"
></div>
</div>
<span class="font-medium">
{Number((sunDuration ?? 0) / 3600).toFixed(0)}h
</span>
</div>
<!-- precip + wind: stacked on mobile, one row on md+ (old card style) -->
<div
class="flex w-full flex-col items-center gap-0.5 md:flex-row md:justify-center md:gap-2"
>
<span class="inline-flex items-center gap-1 {lowPrecip ? 'opacity-40' : ''}">
<svg class="fill-sky-500" width="13" height="13">
<use xlink:href="/images/weather-icons/wi-raindrop.svg#Layer_1"></use>
</svg>
{Number(precipSum ?? 0).toFixed(precipSum >= 10 ? 0 : 1)}
</span>
<span class="inline-flex items-center gap-1 {lowWind ? 'opacity-40' : ''}">
{#if windDir != null && !isNaN(windDir)}
<span
class="hidden shrink-0 md:inline-flex md:-mr-1"
style="transform: {getWindArrowRotation(windDir)}"
>
<svg class="fill-muted-foreground" width="20" height="20">
<use xlink:href="/images/weather-icons/wi-direction-down.svg#Layer_1"
></use>
</svg>
</span>
<svg class="fill-muted-foreground md:hidden" width="13" height="13">
<use xlink:href="/images/weather-icons/wi-strong-wind.svg#Layer_1"></use>
</svg>
{:else}
<svg class="fill-muted-foreground" width="13" height="13">
<use xlink:href="/images/weather-icons/wi-strong-wind.svg#Layer_1"></use>
</svg>
{/if}
<span class="whitespace-nowrap"
>{windMax?.toFixed(0) ?? '-'}<span class="hidden opacity-70 md:inline"
>-{gustMax?.toFixed(0) ?? '-'}</span
></span
>
</span>
</div>
</div>
</button>
{/if}
{/each}
{#if canExtend && onExtend}
<!-- shows the RESULTING range, not an increment: tapping switches the
whole forecast from 7 to 15 days -->
<button
type="button"
class="strip-side flex shrink-0 flex-col items-center justify-center rounded-xl border border-dashed border-border/70 text-muted-foreground"
class="strip-side flex shrink-0 cursor-pointer flex-col items-center justify-center gap-0.5 rounded-xl border border-dashed border-primary/50 bg-primary/5 text-primary transition-colors hover:border-primary hover:bg-primary/10"
onclick={onExtend}
aria-label="Load the longer-range forecast"
aria-label="Show the full 15-day forecast"
title="Show the full 15-day forecast"
>
<svg
class="h-5 w-5 md:h-4 md:w-4"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
stroke-width="1.75"
>
<path stroke-linecap="round" stroke-linejoin="round" d="M12 5v14m-7-7h14" />
</svg>
<span class="text-[9px] leading-tight font-semibold">15d</span>
<span class="inline-flex items-baseline gap-0.5">
<span class="text-sm leading-none font-extrabold">15</span>
<svg
class="h-3 w-3 translate-y-px"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
stroke-width="2.5"
>
<path stroke-linecap="round" stroke-linejoin="round" d="M9 5l7 7-7 7" />
</svg>
</span>
<span class="text-[9px] leading-tight font-semibold">days</span>
</button>
{/if}
</div>
@@ -253,21 +340,90 @@
initial-value: 0;
}
/* 1 as soon as the strip pins — drives the bar chrome independently of the
collapse progress so nothing ever shows through a still-expanding bar. */
@property --stuck {
syntax: '<number>';
inherits: true;
initial-value: 0;
}
/* Tunables, shared by the strip and its sentinel: the collapse plays out
over exactly the cell-height difference (see .daystrip height below). */
over exactly the bar-height difference (see .daystrip height below). */
.sentinel,
.daystrip {
--cell-w-full: 76px;
--cell-w-min: 56px;
--cell-w-min: 58px;
--cell-h-full: 140px;
--cell-h-min: 62px;
--cell-h-min: 64px;
--icon-full: 44px;
--icon-min: 21px;
--pt-full: 7px;
--pt-min: 2px;
--gap-full: 8px;
--gap-min: 4px;
--collapse: calc(var(--cell-h-full) - var(--cell-h-min));
/* bar (strip-row) vertical padding */
--pad-full: 8px;
--pad-min: 8px;
/* fonts */
--dow-font-full: 11px;
--dow-font-min: 11px;
--rel-font-full: 9px;
--rel-font-min: 9px;
--tmax-font-full: 12px;
--tmax-font-min: 11px;
--tmin-font-full: 11px;
--tmin-font-min: 10px;
--tmax-padx-full: 6px;
--tmax-padx-min: 4px;
/* full state only: nudge each temp to sit under "its" icon */
--tmax-nudge: 0px;
--tmin-nudge: 5px;
/* cell bottom padding (roomy when full, tight when compact) */
--pb-full: 6px;
--pb-min: 6px;
/* extra scrub distance beyond the height difference — slows the collapse
down; the surplus just slides content under the (opaque) bar */
--collapse-extra: 0px;
--collapse: calc(
var(--cell-h-full) + 2 * var(--pad-full) - var(--cell-h-min) - 2 * var(--pad-min) +
var(--collapse-extra)
);
}
/* md+: same collapse, but the full state keeps the old desktop day-card
proportions, plays out over a longer scroll distance, and the compact
bar docks under the topbar (slightly taller than its 56px). */
@media (min-width: 768px) {
.sentinel,
.daystrip {
--cell-w-full: 120px;
--cell-h-full: 208px;
--cell-w-min: 64px;
--cell-h-min: 56px;
--icon-full: 80px;
--icon-min: 22px;
--pt-full: 10px;
--gap-full: 10px;
--gap-min: 6px;
--pad-min: 4px;
--dow-font-full: 13px;
--dow-font-min: 10px;
--rel-font-full: 11px;
--tmax-font-full: 17px;
--tmin-font-full: 15px;
--tmax-padx-full: 12px;
--tmax-nudge: 0px;
--tmin-nudge: 8px;
--pb-full: 10px;
--pb-min: 2px;
--collapse-extra: 60px;
}
.daystrip {
/* breathing room between the full cards and the table */
margin-bottom: 14px;
}
}
/* Invisible collapse band: the scroll distance over which the collapse
@@ -280,6 +436,7 @@
.daystrip {
--strip-p: 0;
--stuck: 0;
/* progress-derived (--k is the "fullness": 1 when full, 0 when compact) */
--k: calc(1 - var(--strip-p));
@@ -288,11 +445,13 @@
--icon: calc(var(--icon-min) + (var(--icon-full) - var(--icon-min)) * var(--k));
--pt: calc(var(--pt-min) + (var(--pt-full) - var(--pt-min)) * var(--k));
--gap: calc(var(--gap-min) + (var(--gap-full) - var(--gap-min)) * var(--k));
--pad: calc(var(--pad-min) + (var(--pad-full) - var(--pad-min)) * var(--k));
--rel: clamp(0, calc(1 - var(--strip-p) * 2), 1);
--detail: clamp(0, calc(1 - var(--strip-p) * 1.6), 1);
/* bar background turns opaque almost as soon as the strip sticks, so
content never shows through it */
--chrome: clamp(0, calc(var(--strip-p) * 6), 1);
/* bar background turns opaque the moment the strip sticks (via --stuck),
so content never shows through it — even while the cells are still
large and the collapse has barely started */
--chrome: clamp(0, calc(var(--strip-p) * 6 + var(--stuck)), 1);
/* The sticky box keeps a CONSTANT height — only its contents shrink.
The collapse therefore never resizes the document (the large table
@@ -300,7 +459,7 @@
of scroll jank on mobile Chromium), and because the collapse distance
equals the height difference, the table slides up under the shrinking
bar in exact sync. The empty lower part is click-through. */
height: calc(var(--cell-h-full) + 16px);
height: calc(var(--cell-h-full) + 2 * var(--pad-full));
pointer-events: none;
contain: layout style;
/* own compositor layer: per-frame repaints stay isolated to the strip */
@@ -313,6 +472,7 @@
.strip-row {
pointer-events: auto;
gap: var(--gap);
padding-block: var(--pad);
background: color-mix(
in oklab,
var(--color-background) calc(var(--chrome) * 100%),
@@ -330,9 +490,13 @@
view-timeline: --daystrip-sentinel block;
}
.daystrip {
animation: strip-collapse linear both;
animation-timeline: --daystrip-sentinel;
animation-range: exit 0% exit 100%;
animation:
strip-collapse linear both,
strip-stuck linear both;
animation-timeline: --daystrip-sentinel, --daystrip-sentinel;
animation-range:
exit 0% exit 100%,
exit 0% exit 3%;
}
}
@keyframes strip-collapse {
@@ -340,39 +504,57 @@
--strip-p: 1;
}
}
@keyframes strip-stuck {
to {
--stuck: 1;
}
}
/* Snap fallback: ease between the two end states instead of scrubbing.
(Browsers too old to register --strip-p simply switch instantly.) */
.daystrip.js-snap {
transition: --strip-p 0.28s ease;
transition:
--strip-p 0.28s ease,
--stuck 0.15s ease;
}
.daystrip.js-snap.compact {
--strip-p: 1;
}
.daystrip.js-snap.stuck {
--stuck: 1;
}
@media (min-width: 768px) {
/* larger cards need a touch longer to feel smooth */
.daystrip.js-snap {
transition:
--strip-p 0.55s ease,
--stuck 0.15s ease;
}
}
@media (prefers-reduced-motion: reduce) {
.daystrip.js-snap {
transition: none;
}
}
/* md+: no collapse — the strip is a slim, always-compact day picker that
docks under the topbar, matching its h-14 (56px) height exactly. */
@media (min-width: 768px) {
.sentinel,
.daystrip {
--cell-h-min: 48px;
--icon-min: 20px;
--gap-min: 6px;
}
.daystrip {
--strip-p: 1;
animation: none;
height: 56px;
}
}
.strip-days {
gap: var(--gap);
/* fill the row so it overflows by exactly the past button, which starts
scrolled out of view and is revealed by scrolling left — even when the
day cells alone wouldn't overflow (wide desktop viewports) */
min-width: 100%;
}
/* Scroll containers clip at the PADDING edge, so a parked past button would
always leak a sliver across the row's left padding. Extending the days
group's left edge (only while the past button exists) moves max-scroll so
the button parks fully beyond the clip edge. */
.strip-side + .strip-days {
padding-left: calc(12px - var(--gap-min));
}
@media (min-width: 1024px) {
.strip-side + .strip-days {
padding-left: calc(32px - var(--gap-min));
}
}
.strip-cell,
.strip-side {
@@ -380,11 +562,32 @@
/* size tracks the collapse exactly; only the tap highlight eases */
transition:
border-color 0.15s,
background-color 0.15s;
background-color 0.15s,
translate 0.2s ease-out,
scale 0.2s ease-out,
box-shadow 0.2s ease-out;
}
/* The old day-card lift: hover raises the card slightly, the selected day a
touch more. Scaled by --k so the effect melts away as the strip compacts
(and never disturbs the slim bar); hover only where hover exists. */
.strip-cell[aria-pressed='true'] {
z-index: 10;
translate: 0 calc(-4px * var(--k));
scale: calc(1 + 0.04 * var(--k));
box-shadow: 0 5px 14px -4px rgba(0, 0, 0, calc(0.4 * var(--k)));
}
@media (hover: hover) {
.strip-cell:hover:not([aria-pressed='true']) {
z-index: 10;
translate: 0 calc(-3px * var(--k));
scale: calc(1 + 0.02 * var(--k));
box-shadow: 0 4px 10px -4px rgba(0, 0, 0, calc(0.3 * var(--k)));
}
}
.strip-cell {
width: var(--cell-w);
padding-top: var(--pt);
padding-bottom: calc(var(--pb-min) + (var(--pb-full) - var(--pb-min)) * var(--k));
}
/* Side buttons keep the compact width in BOTH states, so almost nothing to
the left of the first day changes size during the collapse — the first
@@ -402,14 +605,24 @@
width: var(--icon);
height: var(--icon);
}
.night-badge {
width: calc(var(--icon) * 0.44);
height: calc(var(--icon) * 0.44);
/* The night icon sits beside the day icon (slightly low, like a companion)
and melts away completely when compact so the day icon re-centers. */
.night-icon {
display: block;
width: calc(var(--icon) * 0.42 * var(--rel));
height: calc(var(--icon) * 0.42 * var(--rel));
opacity: var(--rel);
margin-left: calc(-4px * var(--rel));
margin-bottom: calc(6px * var(--rel));
}
/* weekday centered when full, pushed to the edges when compact */
.dow-row {
font-size: calc(var(--dow-font-min) + (var(--dow-font-full) - var(--dow-font-min)) * var(--k));
}
.dow-spacer {
flex-grow: var(--rel);
/* keep some outer share when compact so weekday + date sit near-centered
with a modest gap instead of being pushed to the cell edges */
flex-grow: calc(0.6 + 0.4 * var(--rel));
}
.dow-mid {
flex-grow: calc(1 - var(--rel));
@@ -421,19 +634,31 @@
opacity: calc(1 - var(--rel));
}
.rel-label {
font-size: calc(var(--rel-font-min) + (var(--rel-font-full) - var(--rel-font-min)) * var(--k));
opacity: var(--rel);
max-height: calc(14px * var(--rel));
}
.detail-row {
opacity: var(--detail);
max-height: calc(42px * var(--detail));
max-height: calc(52px * var(--detail));
}
.temp-max {
font-size: calc(11px + 1px * var(--k));
padding-inline: calc(3px + 3px * var(--k));
font-size: calc(
var(--tmax-font-min) + (var(--tmax-font-full) - var(--tmax-font-min)) * var(--k)
);
padding-inline: calc(
var(--tmax-padx-min) + (var(--tmax-padx-full) - var(--tmax-padx-min)) * var(--k)
);
padding-block: calc(2px + 1px * var(--k));
/* full: line up under the day icon */
transform: translateX(calc(var(--tmax-nudge) * var(--k)));
}
.temp-min {
font-size: calc(10px + 1px * var(--k));
font-size: calc(
var(--tmin-font-min) + (var(--tmin-font-full) - var(--tmin-font-min)) * var(--k)
);
/* full: line up under the night icon */
transform: translateX(calc(var(--tmin-nudge) * var(--k)));
}
.daystrip :global(.overflow-x-auto) {