All checks were successful
Sync infra to hosts / sync-beta (push) Has been skipped
Sync infra to hosts / sync-prod (push) Has been skipped
Sync infra to hosts / sync-dev (push) Successful in 11s
secrets-guard / encrypted (push) Successful in 17s
shell-lint / shellcheck (push) Successful in 15s
Build + push images (Forgejo registry) / build-push (frontend) (push) Successful in 1m5s
Build + push images (Forgejo registry) / build-push (backend) (push) Successful in 1m8s
Deploy / deploy (backend) (push) Successful in 1m29s
Deploy / deploy (frontend) (push) Successful in 1m38s
secrets-guard / encrypted (pull_request) Successful in 8s
PR build (required check) / changes (pull_request) Successful in 10s
shell-lint / shellcheck (pull_request) Successful in 11s
PR build (required check) / validate-observability (pull_request) Has been skipped
PR build (required check) / build-frontend (pull_request) Successful in 59s
PR build (required check) / build-backend (pull_request) Successful in 1m22s
PR build (required check) / gate (pull_request) Successful in 1s
2134 lines
110 KiB
CSS
2134 lines
110 KiB
CSS
:root {
|
||
--bg: #0f1216;
|
||
--surface: #171b21;
|
||
--surface-2: #1e242c;
|
||
--border: #2a323c;
|
||
--text: #e7ecf2;
|
||
--muted: #9aa6b2;
|
||
--accent: #f0803c;
|
||
/* Discord's brand blurple. Fixed in both schemes on purpose: it identifies the
|
||
provider, so it is their colour rather than one of ours to re-theme. */
|
||
--discord: #5865f2;
|
||
|
||
/* temperature grades: 9-step diverging cold -> green -> hot (colorblind-safe).
|
||
rec-* are the "Near Record" danger tiers — deliberately dark/saturated. */
|
||
--rec-cold: #0a2f6b;
|
||
--very-cold: #2166ac;
|
||
--cold: #4393c3;
|
||
--cool: #92c5de;
|
||
--normal: #4a9d5b; /* middle of the diverging temp scale = green */
|
||
--warm: #f6c18a;
|
||
--hot: #ef9351;
|
||
--very-hot: #dd6b52;
|
||
--rec-hot: #8f0e20;
|
||
|
||
/* precipitation: "dry" + 9 rain-intensity tiers, light green -> teal -> dark navy */
|
||
--dry: #c9a24a;
|
||
--wet-1: #d9f0d3;
|
||
--wet-2: #b7e2b1;
|
||
--wet-3: #8fd18f;
|
||
--wet-4: #5cba9f;
|
||
--wet-5: #35a1c0; /* middle of the rain scale */
|
||
--wet-6: #2b8cbe;
|
||
--wet-7: #226bb3;
|
||
--wet-8: #164a97;
|
||
--wet-9: #08306b;
|
||
}
|
||
|
||
@media (prefers-color-scheme: light) {
|
||
:root {
|
||
--bg: #f4f6f9;
|
||
--surface: #ffffff;
|
||
--surface-2: #eef1f5;
|
||
--border: #dde3ea;
|
||
--text: #1a2029;
|
||
--muted: #5b6673;
|
||
}
|
||
}
|
||
|
||
* { box-sizing: border-box; }
|
||
|
||
body {
|
||
margin: 0;
|
||
font-family: "Inter", system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
|
||
background: var(--bg);
|
||
color: var(--text);
|
||
line-height: 1.45;
|
||
}
|
||
|
||
header {
|
||
padding: 18px 24px;
|
||
border-bottom: 1px solid var(--border);
|
||
background: var(--surface);
|
||
}
|
||
/* 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. */
|
||
.brand > div:not(.unit-toggle):not(.acct) { flex: 1 1 0; min-width: 0; }
|
||
.logo {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
flex-shrink: 0;
|
||
position: relative;
|
||
top: -2px; /* optical: tile centers on the wordmark's cap height */
|
||
}
|
||
.logo svg { width: 28px; height: 28px; display: block; }
|
||
h1 { margin: 0; font-size: 22px; letter-spacing: -0.02em; }
|
||
/* Brand wordmark: monospace stack (instrument/data feel, no webfonts) beside
|
||
the mark, larger than the old Inter treatment. The mark lives INSIDE the h1
|
||
(and inside its home link on server-rendered pages) so glyph and wordmark
|
||
are one lockup, aligned to each other on every page regardless of tagline.
|
||
|
||
Matched on .site-name as well as h1: the homepage renders the brand as a <p>
|
||
so its hero headline can own the page's only h1. Without the class here the
|
||
lockup falls back to block layout, which baseline-aligns the mark instead of
|
||
centring it (it rides ~8px high) and drops the monospace wordmark. */
|
||
.brand h1,
|
||
.brand .site-name {
|
||
font-family: ui-monospace, "SF Mono", "Cascadia Code", Menlo, Consolas, "Liberation Mono", monospace;
|
||
font-weight: 700;
|
||
font-size: 26px;
|
||
letter-spacing: -0.04em;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 10px;
|
||
}
|
||
/* The whole lockup is the home link on every page, so it inherits the wordmark's
|
||
colour rather than looking like body copy that happens to be a link. */
|
||
.brand h1 a,
|
||
.brand .site-name a {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 10px;
|
||
color: var(--text);
|
||
text-decoration: none;
|
||
}
|
||
.brand h1 a:hover,
|
||
.brand .site-name a:hover { color: var(--accent); }
|
||
.tag { margin: 2px 0 0; color: var(--muted); font-size: 13px; }
|
||
/* Compact header (e.g. Calendar): no tagline, roughly half the height.
|
||
Only the vertical padding is trimmed so the responsive side padding stands. */
|
||
header.compact { padding-top: 9px; padding-bottom: 9px; }
|
||
header.compact .logo svg { width: 22px; height: 22px; }
|
||
header.compact h1,
|
||
header.compact .site-name { font-size: 21px; }
|
||
|
||
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; }
|
||
/* ---- shared control recipes ----
|
||
One home for the accent-button and text-input look every view reuses. The
|
||
16px input font is load-bearing (below it iOS zooms on focus); buttons keep
|
||
their ≥44px touch targets in the per-component rules, which carry only each
|
||
control's differences from these. */
|
||
.date-row button, .range-refresh, .find-btn, .mp-search button, .mp-confirm {
|
||
border-radius: 10px; border: none; cursor: pointer;
|
||
background: var(--accent); color: #1a1206; font-weight: 600;
|
||
}
|
||
.date-row input, .cal-range input, .day-date input, .mp-search input {
|
||
padding: 9px 12px; border-radius: 10px; border: 1px solid var(--border);
|
||
background: var(--surface); color: var(--text); font-size: 16px;
|
||
}
|
||
.date-row button { padding: 11px 18px; }
|
||
|
||
.date-row { display: flex; align-items: center; gap: 16px; flex-wrap: wrap; }
|
||
.date-row label { font-size: 13px; color: var(--muted); display: flex; flex-direction: column; gap: 4px; }
|
||
/* "Today" quick-reset chip beside the target date. Outlined when the target is a
|
||
past date (a clickable "jump to today"); filled accent while already on today. */
|
||
.date-row .today-chip {
|
||
align-self: flex-end; padding: 9px 14px; border-radius: 10px; min-height: 40px;
|
||
font-size: 13px; font-weight: 600; cursor: pointer;
|
||
background: transparent; color: var(--muted); border: 1px solid var(--border);
|
||
transition: background .12s ease, color .12s ease, border-color .12s ease;
|
||
}
|
||
.date-row .today-chip:hover { border-color: var(--accent); color: var(--text); }
|
||
.date-row .today-chip.active {
|
||
background: var(--accent); color: #1a1206; border-color: var(--accent); cursor: default;
|
||
}
|
||
|
||
.hint { color: var(--muted); font-size: 12.5px; max-width: 220px; }
|
||
|
||
.panel {
|
||
background: var(--surface); border: 1px solid var(--border); border-radius: 14px;
|
||
padding: 20px;
|
||
}
|
||
|
||
.placeholder p { color: var(--text); }
|
||
.muted { color: var(--muted); font-size: 13.5px; }
|
||
|
||
/* --- results --- */
|
||
.loc-head { display: flex; justify-content: space-between; align-items: flex-start; gap: 12px; margin-bottom: 16px; }
|
||
.loc-head h2 { margin: 0 0 2px; font-size: 18px; }
|
||
.loc-head .meta { color: var(--muted); font-size: 12.5px; margin: 0; }
|
||
/* Context stacked top-down, one fact per line, instead of dot-joined tags. */
|
||
.loc-meta { list-style: none; margin: 4px 0 0; padding: 0; display: flex; flex-direction: column; gap: 2px; }
|
||
.loc-meta li { color: var(--muted); font-size: 12.5px; line-height: 1.35; }
|
||
.share { display: flex; gap: 6px; flex-shrink: 0; flex-wrap: wrap; }
|
||
.share button {
|
||
padding: 6px 11px; border-radius: 8px; border: 1px solid var(--border);
|
||
background: var(--surface-2); color: var(--text); font-size: 12px; cursor: pointer;
|
||
white-space: nowrap; text-decoration: none; display: inline-flex; align-items: center; gap: 5px;
|
||
}
|
||
|
||
/* Inline monochrome icons (currentColor) used in place of emoji. */
|
||
.ic { width: 1em; height: 1em; vertical-align: -0.14em; flex-shrink: 0; }
|
||
.ic-lg { width: 26px; height: 26px; }
|
||
.wx { width: 1.15em; height: 1.15em; vertical-align: -0.2em; }
|
||
.share button:hover { border-color: var(--accent); }
|
||
|
||
/* 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: 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 2px 8px rgba(240, 128, 60, .25);
|
||
transition: transform .12s ease, box-shadow .12s ease, filter .12s ease;
|
||
}
|
||
.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 { 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; }
|
||
/* High / Low / Weather metric toggle above the trend chart; active tab takes the
|
||
metric's own color so the control matches the chart it drives. */
|
||
.chart-toggle {
|
||
display: flex; flex-wrap: wrap; border: 1px solid var(--border); border-radius: 10px;
|
||
overflow: hidden; margin-bottom: 10px;
|
||
}
|
||
.chart-toggle button {
|
||
padding: 8px 18px; background: var(--surface); color: var(--text); border: none;
|
||
cursor: pointer; font-size: 13.5px; font-weight: 600; min-height: 40px;
|
||
}
|
||
.chart-toggle button + button { border-left: 1px solid var(--border); }
|
||
/* Beyond phones the switch shrinks to its tabs and sits centered over the chart
|
||
(phones keep the full-width 4-per-row grid from the 640px rules). */
|
||
@media (min-width: 641px) {
|
||
.chart-toggle { width: fit-content; margin-left: auto; margin-right: auto; }
|
||
}
|
||
.chart-toggle button.active[data-metric="tmax"] { background: var(--very-hot); color: #1a1206; }
|
||
.chart-toggle button.active[data-metric="tmin"] { background: var(--cold); color: #10222f; }
|
||
.chart-toggle button.active[data-metric="feels"] { background: var(--accent); color: #1a1206; }
|
||
.chart-toggle button.active[data-metric="humid"] { background: #2ea9bf; color: #04222a; }
|
||
.chart-toggle button.active[data-metric="wind"] { background: #5aa9a3; color: #08201e; }
|
||
.chart-toggle button.active[data-metric="gust"] { background: #8f86d8; color: #100c26; }
|
||
.chart-toggle button.active[data-metric="precip"] { background: var(--wet-6); color: #fff; }
|
||
.chart-toggle button.active[data-metric="dry"] { background: #c9a24a; color: #241a04; }
|
||
.chart-legend {
|
||
display: flex; flex-wrap: wrap; gap: 14px; font-size: 12px; color: var(--muted); margin-bottom: 8px;
|
||
}
|
||
.chart-legend span { display: inline-flex; align-items: center; gap: 6px; }
|
||
.chart-legend i { width: 12px; height: 12px; border-radius: 3px; display: inline-block; }
|
||
.chart-legend i.swatch-band { border: 1px solid; border-radius: 3px; }
|
||
.chart-legend .legend-note { font-style: italic; opacity: .85; }
|
||
.chart-tip {
|
||
position: absolute; z-index: 10; pointer-events: none;
|
||
background: var(--surface); border: 1px solid var(--border); border-radius: 9px;
|
||
padding: 8px 10px; font-size: 12px; line-height: 1.5; min-width: 148px;
|
||
box-shadow: 0 8px 24px rgba(0,0,0,.35);
|
||
}
|
||
.chart-tip .tt-date { font-weight: 700; margin-bottom: 3px; }
|
||
.chart-tip .tt-grade { color: var(--muted); }
|
||
.chart-tip .tt-norm { color: var(--muted); margin-top: 3px; border-top: 1px solid var(--border); padding-top: 3px; }
|
||
|
||
/* Flex (not fixed 3-col) so 7 cards fill each row evenly — no lonely trailing card. */
|
||
.normals {
|
||
display: flex; flex-wrap: wrap; gap: 10px; margin-bottom: 22px;
|
||
}
|
||
.normal-card {
|
||
flex: 1 1 130px;
|
||
background: var(--surface-2); border: 1px solid var(--border);
|
||
border-radius: 11px; padding: 12px 14px;
|
||
text-decoration: none; color: var(--text); cursor: pointer;
|
||
transition: border-color .12s ease, background .12s ease;
|
||
}
|
||
.normal-card:hover { border-color: var(--accent); background: var(--surface); }
|
||
/* Title on the left, grade status pinned to the top-right corner of the card. */
|
||
.normal-card .nc-head { display: flex; justify-content: space-between; align-items: baseline; gap: 8px; margin-bottom: 6px; }
|
||
.normal-card h3 { margin: 0; font-size: 12px; text-transform: uppercase; letter-spacing: .04em; color: var(--muted); }
|
||
/* Big value is the day's own reading, tinted by its grade (lifted toward the text
|
||
color so dark tiers stay legible); the line under it shows the normal median. */
|
||
.normal-card .big { font-size: 22px; font-weight: 800; color: color-mix(in oklab, var(--cat, var(--text)), var(--text) 15%); }
|
||
.normal-card .range { font-size: 12px; color: var(--muted); margin-top: 2px; }
|
||
.normal-card .nc-grade { font-size: 11px; font-weight: 700; text-align: right; line-height: 1.2; color: color-mix(in oklab, var(--cat, var(--muted)), var(--text) 28%); }
|
||
|
||
.section-title {
|
||
font-size: 12px; text-transform: uppercase; letter-spacing: .05em;
|
||
color: var(--muted); margin: 4px 0 12px;
|
||
}
|
||
|
||
/* Plain-language "what to expect" insights (home results + calendar). A soft
|
||
card of friendly climatology sentences with a leading emoji per line. */
|
||
.insights, .cal-insights {
|
||
background: var(--surface-2); border: 1px solid var(--border);
|
||
border-radius: 12px; padding: 12px 14px; margin: 4px 0 16px;
|
||
border-left: 3px solid var(--accent);
|
||
}
|
||
.insight-lead {
|
||
font-size: 12px; text-transform: uppercase; letter-spacing: .05em;
|
||
color: var(--muted); margin: 0 0 8px;
|
||
}
|
||
.insight-list { list-style: none; margin: 0; padding: 0; display: grid; gap: 8px; }
|
||
.insight-list li {
|
||
display: flex; align-items: baseline; gap: 8px;
|
||
font-size: 15px; line-height: 1.45; color: var(--text);
|
||
}
|
||
.insight-list li b { color: var(--text); font-weight: 700; }
|
||
.ins-emoji { flex-shrink: 0; font-size: 16px; line-height: 1; }
|
||
/* Tint the rain line's accent stripe to the mid rain-scale blue. */
|
||
.insight-list li.ins-rain { border-left: 2px solid var(--wet-5); padding-left: 8px; margin-left: -2px; }
|
||
|
||
/* Wide screens: let the insight sentences flow in two columns. */
|
||
@media (min-width: 900px) {
|
||
.insight-list { grid-template-columns: 1fr 1fr; gap: 8px 20px; }
|
||
}
|
||
|
||
/* grade-scale key (low -> high, relative categories) — swatch + name only;
|
||
the percentile figures live on the full guide, not here. */
|
||
.tier-key { display: flex; flex-wrap: wrap; gap: 6px 14px; margin: -2px 0 8px; }
|
||
.tk-seg { display: inline-flex; align-items: center; gap: 6px; font-size: 12px; }
|
||
.tk-swatch { width: 14px; height: 10px; border-radius: 3px; flex-shrink: 0; }
|
||
.tk-label { font-weight: 600; }
|
||
.tk-note { margin: 0 0 16px; font-size: 12.5px; }
|
||
|
||
/* --- header °F/°C unit toggle (sits at the top-right, left of the tabs) --- */
|
||
.unit-toggle {
|
||
margin-left: auto; align-self: center; flex-shrink: 0; display: inline-flex; gap: 2px;
|
||
background: var(--surface-2); border: 1px solid var(--border);
|
||
border-radius: 11px; padding: 3px;
|
||
}
|
||
.unit-toggle button {
|
||
border: 0; background: transparent; color: var(--muted); cursor: pointer;
|
||
font-size: 14px; font-weight: 700; padding: 8px 12px; border-radius: 8px;
|
||
min-height: 38px; min-width: 44px; line-height: 1;
|
||
}
|
||
.unit-toggle button.active { background: var(--accent); color: #fff; }
|
||
.unit-toggle button:hover:not(.active) { color: var(--text); }
|
||
/* The view switcher already claims the right edge; with the toggle to its left,
|
||
drop its auto-margin so the two sit together in the corner. */
|
||
.unit-toggle + .view-nav { margin-left: 8px; }
|
||
|
||
/* --- account entry (header) + auth modal --- */
|
||
.acct { align-self: center; flex-shrink: 0; position: relative; }
|
||
.acct-btn {
|
||
display: inline-flex; align-items: center; gap: 7px; cursor: pointer;
|
||
background: var(--surface-2); border: 1px solid var(--border); color: var(--text);
|
||
border-radius: 11px; padding: 8px 12px; min-height: 40px; font-size: 14px; font-weight: 600;
|
||
}
|
||
.acct-btn:hover { border-color: var(--accent); }
|
||
.acct-btn svg { width: 18px; height: 18px; flex-shrink: 0; }
|
||
.acct-name { max-width: 16ch; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||
.acct-menu { position: relative; }
|
||
.acct-pop {
|
||
position: absolute; right: 0; top: calc(100% + 6px); z-index: 1100; min-width: 180px;
|
||
display: flex; flex-direction: column; padding: 6px;
|
||
background: var(--surface); border: 1px solid var(--border); border-radius: 12px;
|
||
box-shadow: 0 16px 40px rgba(0, 0, 0, .45);
|
||
}
|
||
.acct-pop[hidden] { display: none; }
|
||
.acct-pop-link {
|
||
display: block; text-align: left; padding: 10px 12px; border-radius: 8px; min-height: 40px;
|
||
background: transparent; border: 0; color: var(--text); cursor: pointer;
|
||
font: inherit; font-size: 14px; text-decoration: none;
|
||
}
|
||
.acct-pop-link:hover { background: var(--surface-2); }
|
||
|
||
/* Notification bell + dropdown. */
|
||
.acct { display: inline-flex; align-items: center; gap: 8px; }
|
||
.notif { position: relative; }
|
||
.notif-btn { position: relative; padding: 8px; min-width: 40px; justify-content: center; }
|
||
.notif-badge {
|
||
position: absolute; top: -4px; right: -4px; min-width: 18px; height: 18px; padding: 0 4px;
|
||
display: inline-flex; align-items: center; justify-content: center;
|
||
background: var(--accent); color: #1a1206; font-size: 11px; font-weight: 800;
|
||
border-radius: 999px; line-height: 1;
|
||
}
|
||
.notif-badge[hidden] { display: none; }
|
||
.notif-pop {
|
||
position: absolute; right: 0; top: calc(100% + 6px); z-index: 1100;
|
||
width: min(360px, calc(100vw - 24px));
|
||
display: flex; flex-direction: column;
|
||
background: var(--surface); border: 1px solid var(--border); border-radius: 12px;
|
||
box-shadow: 0 16px 40px rgba(0, 0, 0, .45); overflow: hidden;
|
||
}
|
||
.notif-pop[hidden] { display: none; }
|
||
.notif-head {
|
||
display: flex; align-items: center; justify-content: space-between;
|
||
padding: 12px 14px; border-bottom: 1px solid var(--border); font-weight: 700; font-size: 14px;
|
||
}
|
||
.notif-readall {
|
||
background: none; border: 0; color: var(--accent); cursor: pointer;
|
||
font: inherit; font-size: 12px; font-weight: 600;
|
||
}
|
||
.notif-list { list-style: none; margin: 0; padding: 4px; max-height: 60vh; overflow-y: auto; }
|
||
.notif-empty { padding: 20px 14px; text-align: center; font-size: 13px; }
|
||
.notif-item { padding: 10px 12px; border-radius: 9px; cursor: default; }
|
||
.notif-item.is-unread { cursor: pointer; background: color-mix(in srgb, var(--accent) 10%, transparent); }
|
||
.notif-item.is-unread:hover { background: color-mix(in srgb, var(--accent) 16%, transparent); }
|
||
.notif-item + .notif-item { margin-top: 2px; }
|
||
.notif-item-title { font-weight: 700; font-size: 13px; display: flex; align-items: center; gap: 6px; }
|
||
.notif-item.is-unread .notif-item-title::before {
|
||
content: ""; width: 7px; height: 7px; border-radius: 50%; background: var(--accent); flex-shrink: 0;
|
||
}
|
||
.notif-item-body { font-size: 12px; color: var(--muted); margin-top: 3px; }
|
||
.notif-item-time { font-size: 11px; margin-top: 3px; }
|
||
.notif-manage {
|
||
display: block; padding: 10px 14px; border-top: 1px solid var(--border);
|
||
font-size: 13px; font-weight: 600; color: var(--accent); text-decoration: none;
|
||
}
|
||
.notif-manage:hover { background: var(--surface-2); }
|
||
|
||
/* Auth modal reuses .mp-overlay/.mp-modal chrome; this styles its form body. */
|
||
.acct-modal { max-width: 400px; }
|
||
.acct-form { display: flex; flex-direction: column; gap: 14px; padding: 16px; }
|
||
.acct-form label { display: flex; flex-direction: column; gap: 5px; font-size: 13px; color: var(--muted); }
|
||
.acct-form input {
|
||
padding: 11px 14px; border-radius: 10px; border: 1px solid var(--border);
|
||
background: var(--bg); color: var(--text); font-size: 16px;
|
||
}
|
||
.acct-form input:focus { outline: none; border-color: var(--accent); }
|
||
.acct-submit {
|
||
margin-top: 2px; padding: 12px 16px; min-height: 44px; border: none; border-radius: 10px;
|
||
background: var(--accent); color: #1a1206; font-weight: 700; font-size: 15px; cursor: pointer;
|
||
}
|
||
.acct-submit:hover { filter: brightness(1.05); }
|
||
.acct-submit:disabled { opacity: .6; cursor: progress; }
|
||
.acct-error {
|
||
margin: 0; padding: 9px 12px; border-radius: 8px; font-size: 13px;
|
||
background: color-mix(in srgb, var(--rec-hot, #d64545) 20%, transparent);
|
||
color: var(--text); border: 1px solid var(--rec-hot, #d64545);
|
||
}
|
||
.acct-error[hidden] { display: none; }
|
||
/* "or / Continue with Discord" under the submit button. Hidden unless the server
|
||
reports Discord configured (account.js sets [hidden]). */
|
||
.acct-alt { display: flex; flex-direction: column; gap: 10px; }
|
||
.acct-alt[hidden] { display: none; }
|
||
.acct-or {
|
||
display: flex; align-items: center; gap: 10px;
|
||
font-size: 12px; color: var(--muted); text-transform: uppercase; letter-spacing: .06em;
|
||
}
|
||
.acct-or::before, .acct-or::after {
|
||
content: ""; flex: 1; height: 1px; background: var(--border);
|
||
}
|
||
.acct-oauth-row { display: flex; flex-direction: column; gap: 8px; }
|
||
.acct-oauth-login {
|
||
display: flex; align-items: center; justify-content: center; gap: 9px;
|
||
padding: 12px 16px; min-height: 44px; border-radius: 10px;
|
||
font-weight: 700; font-size: 15px; text-decoration: none;
|
||
border: 1px solid transparent;
|
||
}
|
||
.acct-oauth-login:hover { filter: brightness(1.08); }
|
||
.acct-oauth-login svg { width: 20px; height: 20px; flex: none; }
|
||
.acct-oauth-login.is-discord {
|
||
background: var(--discord); color: #fff;
|
||
}
|
||
.acct-oauth-login.is-discord svg { height: 15px; }
|
||
/* Google's branding terms require their mark on white (or their own grey), with a
|
||
visible border — so this one button is deliberately light in both schemes and
|
||
does not follow the surface tokens. */
|
||
.acct-oauth-login.is-google {
|
||
background: #fff; color: #1f1f1f; border-color: #747775;
|
||
}
|
||
.acct-oauth-login.is-google:hover { filter: none; background: #f2f2f2; }
|
||
|
||
.acct-pop-note { display: block; padding: 9px 12px; font-size: 13px; }
|
||
.acct-switch { margin: 0; font-size: 13px; color: var(--muted); text-align: center; }
|
||
.acct-switch button {
|
||
background: none; border: 0; color: var(--accent); cursor: pointer;
|
||
font: inherit; font-weight: 600; padding: 0; text-decoration: underline;
|
||
}
|
||
|
||
/* One-off status toast (e.g. "email verified") — neutral for both success and
|
||
failure; the text says which, not the color. */
|
||
.acct-toast {
|
||
position: fixed; left: 50%; bottom: 24px; z-index: 1200;
|
||
transform: translateX(-50%);
|
||
max-width: calc(100vw - 32px); padding: 12px 18px; border-radius: 10px;
|
||
background: var(--surface); border: 1px solid var(--border); color: var(--text);
|
||
font-size: 14px; box-shadow: 0 12px 30px rgba(0, 0, 0, .4);
|
||
}
|
||
.acct-toast[hidden] { display: none; }
|
||
|
||
/* --- alerts page (subscriptions) --- */
|
||
.alerts-loading { color: var(--muted); }
|
||
.alerts-gate {
|
||
max-width: 480px; margin: 24px auto; padding: 24px; text-align: center;
|
||
display: flex; flex-direction: column; gap: 12px; align-items: center;
|
||
}
|
||
.alerts-gate h2 { margin: 0; }
|
||
.alerts-gate { max-width: 520px; }
|
||
.alerts-lede { margin: 0; }
|
||
/* The "what are alerts" steps read as a left-aligned list inside the centered card. */
|
||
.alerts-explain {
|
||
text-align: left; align-self: stretch; margin: 2px 0; padding-left: 24px;
|
||
display: flex; flex-direction: column; gap: 10px; line-height: 1.5;
|
||
}
|
||
.alerts-explain li { padding-left: 4px; }
|
||
.alerts-eg { margin: 0; font-style: italic; }
|
||
.alerts-gate-note { margin: 0; font-size: 13px; }
|
||
.alerts-gate .acct-submit { max-width: 280px; }
|
||
.alerts-toolbar {
|
||
display: flex; align-items: center; gap: 14px; flex-wrap: wrap; margin-bottom: 16px;
|
||
}
|
||
.alerts-toolbar .find-btn { padding: 11px 18px; min-height: 44px; }
|
||
.alerts-note { margin: 0; font-size: 13px; }
|
||
|
||
/* Per-device push toggle, above the account-wide alert list. */
|
||
.push-bar {
|
||
border: 1px solid var(--border); background: var(--surface); border-radius: 14px;
|
||
padding: 12px 16px; margin-bottom: 16px;
|
||
}
|
||
.push-row {
|
||
display: flex; align-items: center; justify-content: space-between; gap: 12px; flex-wrap: wrap;
|
||
}
|
||
.push-status { font-size: 14px; font-weight: 600; }
|
||
.push-actions { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
|
||
.push-toggle { padding: 9px 16px; min-height: 44px; }
|
||
.push-test-btn {
|
||
min-height: 44px; padding: 9px 14px; border-radius: 10px; cursor: pointer;
|
||
border: 1px solid var(--border); background: transparent; color: var(--text); font-size: 14px;
|
||
}
|
||
.push-test-btn:hover { border-color: var(--accent); }
|
||
.push-note { margin: 0; }
|
||
|
||
.sub-list { list-style: none; margin: 0; padding: 0; display: grid; gap: 14px; }
|
||
@media (min-width: 720px) { .sub-list { grid-template-columns: repeat(auto-fill, minmax(340px, 1fr)); } }
|
||
.sub-empty { padding: 20px; border: 1px dashed var(--border); border-radius: 12px; text-align: center; }
|
||
.sub-card {
|
||
border: 1px solid var(--border); background: var(--surface); border-radius: 14px;
|
||
padding: 16px; display: flex; flex-direction: column; gap: 12px;
|
||
}
|
||
.sub-card.is-paused { opacity: .62; }
|
||
.sub-card.is-busy { pointer-events: none; opacity: .6; }
|
||
.sub-top { display: flex; align-items: flex-start; justify-content: space-between; gap: 10px; }
|
||
.sub-title { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; min-width: 0; }
|
||
.sub-name { font-weight: 700; font-size: 15px; }
|
||
.sub-kind {
|
||
font-size: 11px; font-weight: 700; text-transform: uppercase; letter-spacing: .04em;
|
||
padding: 3px 8px; border-radius: 999px; white-space: nowrap;
|
||
}
|
||
.sub-kind-observed { background: color-mix(in srgb, var(--accent) 20%, transparent); color: var(--accent); }
|
||
.sub-kind-forecast { background: var(--surface-2); color: var(--muted); border: 1px solid var(--border); }
|
||
.sub-remove {
|
||
flex-shrink: 0; border: 1px solid var(--border); background: transparent; color: var(--muted);
|
||
cursor: pointer; border-radius: 9px; min-width: 40px; min-height: 40px;
|
||
display: inline-flex; align-items: center; justify-content: center;
|
||
}
|
||
.sub-remove svg { width: 17px; height: 17px; }
|
||
.sub-remove:hover { border-color: var(--rec-hot, #d64545); color: var(--rec-hot, #d64545); }
|
||
.sub-metrics { display: flex; flex-wrap: wrap; gap: 6px; }
|
||
.sub-metric {
|
||
font-size: 12px; padding: 3px 9px; border-radius: 999px;
|
||
background: var(--surface-2); color: var(--text); border: 1px solid var(--border);
|
||
}
|
||
.sub-controls { display: flex; flex-wrap: wrap; align-items: center; gap: 14px; justify-content: space-between; }
|
||
.sub-thr { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; font-size: 13px; color: var(--muted); }
|
||
.sub-active-lbl { display: inline-flex; align-items: center; gap: 7px; font-size: 14px; cursor: pointer; }
|
||
.sub-active-lbl input { width: 18px; height: 18px; accent-color: var(--accent); }
|
||
|
||
/* Segmented control shared by the threshold + kind pickers. */
|
||
.seg { display: inline-flex; gap: 2px; background: var(--surface-2); border: 1px solid var(--border); border-radius: 10px; padding: 3px; }
|
||
.seg button {
|
||
border: 0; background: transparent; color: var(--muted); cursor: pointer;
|
||
font: inherit; font-weight: 700; font-size: 13px; padding: 7px 11px; border-radius: 7px;
|
||
min-height: 38px; min-width: 40px; line-height: 1;
|
||
}
|
||
.seg button.active { background: var(--accent); color: #1a1206; }
|
||
.seg button:hover:not(.active) { color: var(--text); }
|
||
|
||
/* Alert editor modal (reuses .mp-overlay / .mp-modal chrome). */
|
||
.sub-editor { max-width: 460px; }
|
||
.sub-form { display: flex; flex-direction: column; gap: 16px; padding: 16px; overflow-y: auto; }
|
||
.sub-form-loc { margin: 0; font-weight: 700; font-size: 15px; }
|
||
.sub-field { border: 1px solid var(--border); border-radius: 12px; padding: 12px 14px; margin: 0; display: flex; flex-direction: column; gap: 10px; }
|
||
.sub-field legend { padding: 0 6px; font-size: 13px; color: var(--muted); }
|
||
.sub-kind-help { margin: 0; font-size: 12px; }
|
||
.metric-checks { display: grid; grid-template-columns: 1fr 1fr; gap: 8px 14px; }
|
||
.metric-check { display: inline-flex; align-items: center; gap: 8px; font-size: 14px; cursor: pointer; min-height: 32px; }
|
||
.metric-check input { width: 18px; height: 18px; accent-color: var(--accent); flex-shrink: 0; }
|
||
.thr-range { width: 100%; accent-color: var(--accent); height: 28px; }
|
||
.two-sided-lbl { display: flex; align-items: center; gap: 8px; font-size: 13px; color: var(--muted); cursor: pointer; }
|
||
.two-sided-lbl input { width: 18px; height: 18px; accent-color: var(--accent); flex-shrink: 0; }
|
||
.sub-form-error {
|
||
margin: 0; padding: 9px 12px; border-radius: 8px; font-size: 13px;
|
||
background: color-mix(in srgb, var(--rec-hot, #d64545) 20%, transparent);
|
||
color: var(--text); border: 1px solid var(--rec-hot, #d64545);
|
||
}
|
||
.sub-form-error[hidden] { display: none; }
|
||
@media (max-width: 640px) { .metric-checks { grid-template-columns: 1fr; } }
|
||
|
||
/* --- shared header view switcher (Map · Calendar · Day) --- */
|
||
/* The bar is split into equal, fully-clickable segments (one per page) with a
|
||
clear divider between them, so the whole width is a set of obvious tabs. */
|
||
.view-nav {
|
||
margin-left: auto; align-self: center; display: inline-flex; gap: 0;
|
||
background: var(--surface-2); border: 1px solid var(--border);
|
||
border-radius: 11px; padding: 3px;
|
||
}
|
||
.view-nav a {
|
||
flex: 1; color: var(--muted); text-decoration: none; font-size: 14px; font-weight: 600;
|
||
padding: 8px 18px; border-radius: 8px; white-space: nowrap; position: relative;
|
||
display: inline-flex; align-items: center; justify-content: center; min-height: 38px;
|
||
}
|
||
/* Divider line between adjacent tabs. Hidden around the highlighted (active) tab
|
||
so its pill reads clean against the bar. */
|
||
.view-nav a + a::before {
|
||
content: ""; position: absolute; left: 0; top: 16%; bottom: 16%;
|
||
width: 2px; border-radius: 2px; background: var(--border);
|
||
}
|
||
.view-nav a.active::before,
|
||
.view-nav a.active + a::before { background: transparent; }
|
||
.view-nav a:hover { color: var(--text); }
|
||
.view-nav a.active { background: var(--accent); color: #fff; }
|
||
|
||
/* Hamburger wrapper. On desktop the menu and its panel are transparent
|
||
(display:contents), so the nav pills plus the injected °F/°C toggle, bell and
|
||
account render inline in the header row and the hamburger summary is hidden;
|
||
on phones (the 640px block) the panel becomes a dropdown behind the hamburger. */
|
||
.nav-menu { display: contents; }
|
||
.nav-panel { display: contents; }
|
||
/* A closed <details> hides its non-summary content; force the panel visible on
|
||
desktop (the 640px block re-hides it behind the hamburger on phones). */
|
||
.nav-menu > .view-nav,
|
||
.nav-panel > .view-nav { display: inline-flex; }
|
||
.nav-menu::details-content { display: contents; }
|
||
/* The controls are injected into the panel after the nav in DOM order; restore
|
||
the header's visual order — °F/°C · account · tabs — on desktop. On phones the
|
||
640px block resets these so the dropdown stacks nav → °F/°C → account. */
|
||
.nav-panel > .unit-toggle { order: 1; }
|
||
.nav-panel > .acct { order: 2; }
|
||
.nav-panel > .view-nav { order: 3; }
|
||
.nav-toggle { display: none; list-style: none; cursor: pointer; }
|
||
.nav-toggle::-webkit-details-marker { display: none; }
|
||
.nav-toggle svg { display: block; }
|
||
|
||
/* --- calendar subpage --- */
|
||
.metric-toggle {
|
||
display: flex; flex-wrap: wrap; border: 1px solid var(--border); border-radius: 10px; overflow: hidden;
|
||
}
|
||
.metric-toggle button {
|
||
padding: 10px 16px; background: var(--surface); color: var(--text);
|
||
border: none; cursor: pointer; font-size: 14px; min-height: 44px; white-space: nowrap;
|
||
}
|
||
.metric-toggle button + button { border-left: 1px solid var(--border); }
|
||
.metric-toggle button.active { background: var(--accent); color: #1a1206; font-weight: 600; }
|
||
|
||
/* Metric selector block — sits below the time filters, labeling the grid it colors. */
|
||
.metric-block { margin: 0 0 18px; }
|
||
.metric-block .cal-filter-label { display: block; margin: 0 0 8px; }
|
||
/* Beyond phones the selector shrinks to its tabs and sits centered over the
|
||
month grid (scoped under .metric-block — the Compare page's basis toggle
|
||
shares .metric-toggle and must keep filling its param cell). */
|
||
@media (min-width: 641px) {
|
||
.metric-block { display: flex; flex-direction: column; align-items: center; }
|
||
.metric-block .cal-filter-label { text-align: center; }
|
||
}
|
||
|
||
/* Month/year range picker (calendar). 16px inputs avoid iOS zoom-on-focus. */
|
||
.cal-range { display: grid; grid-template-columns: 1fr 1fr; gap: 8px 12px; }
|
||
/* Range label + "up to 3 years" hint share one row spanning both input columns. */
|
||
.cal-range-head {
|
||
grid-column: 1 / -1;
|
||
display: flex; align-items: baseline; justify-content: space-between; gap: 10px;
|
||
}
|
||
.cal-range label {
|
||
display: flex; flex-direction: column; gap: 4px; font-size: 13px; color: var(--muted);
|
||
}
|
||
.cal-range input { width: 100%; padding: 8px 10px; border-radius: 8px; min-height: 40px; }
|
||
.cal-range .hint { margin: 0; flex: 0 0 auto; max-width: none; font-size: 12px; color: var(--muted); }
|
||
/* Appears under the pickers once the user edits a date; no request is made until
|
||
it's tapped. Spans both columns so it reads as a clear confirm step. */
|
||
.range-refresh {
|
||
grid-column: 1 / -1; margin-top: 2px;
|
||
padding: 10px 16px; min-height: 44px; border-radius: 9px;
|
||
font-size: 15px; font-weight: 700; border: 1px solid var(--accent);
|
||
}
|
||
.range-refresh:hover { filter: brightness(1.05); }
|
||
|
||
/* Time-filter panel: month/year range + season + year checklists, stacked in a
|
||
tidy column on phones. */
|
||
.cal-filters {
|
||
display: flex; flex-direction: column; gap: 16px;
|
||
max-width: 560px; margin: 0 0 18px; padding: 14px 16px;
|
||
border: 1px solid var(--border); border-radius: 12px; background: var(--surface);
|
||
}
|
||
/* 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-label {
|
||
font-size: 11px; text-transform: uppercase; letter-spacing: .04em;
|
||
color: var(--muted); font-weight: 700; margin-right: 2px;
|
||
}
|
||
|
||
.cal-head { margin-bottom: 16px; }
|
||
.cal-head h2 { margin: 0 0 2px; font-size: 18px; }
|
||
.cal-head .meta { color: var(--muted); font-size: 12.5px; margin: 0; }
|
||
/* "loading N more blocks…" note while a long span streams in chunk by chunk. */
|
||
.cal-loading { color: var(--accent); font-weight: 600; }
|
||
/* Nudge to interact with the grid — wording covers both tap (mobile) and click. */
|
||
.cal-hint {
|
||
display: inline-flex; align-items: center; gap: 6px;
|
||
margin: 10px 0 0; padding: 6px 11px; font-size: 13px; font-weight: 600;
|
||
color: var(--accent); background: color-mix(in oklab, var(--accent), transparent 88%);
|
||
border: 1px solid color-mix(in oklab, var(--accent), transparent 70%);
|
||
border-radius: 999px;
|
||
}
|
||
|
||
/* 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; }
|
||
.cal-dows span { font-size: 11px; color: var(--muted); text-align: center; line-height: 1; }
|
||
.cal-cell { position: relative; aspect-ratio: 1; border-radius: 6px; background: var(--surface-2); }
|
||
.cal-cell.empty { background: transparent; }
|
||
.cal-cell.filled { cursor: pointer; }
|
||
/* Day-of-month number, corner-anchored. White with a dark halo so it reads on any
|
||
tier color; on transparent (no-record) tiles fall back to a plain muted number. */
|
||
.cal-daynum {
|
||
position: absolute; top: 2px; left: 4px; font-size: 10px; font-weight: 700;
|
||
line-height: 1; color: #fff; pointer-events: none;
|
||
text-shadow: 0 0 2px rgba(0, 0, 0, .9), 0 1px 1px rgba(0, 0, 0, .7);
|
||
}
|
||
.cal-cell.empty .cal-daynum { color: var(--muted); text-shadow: none; }
|
||
.cal-cell.filled:hover { outline: 2px solid var(--text); outline-offset: 1px; }
|
||
|
||
/* Fixed positioning so the tooltip tracks the viewport (clientX/Y) even when the
|
||
page is scrolled far down the calendar. */
|
||
#cal-tip { position: fixed; max-width: calc(100vw - 20px); }
|
||
/* Mini grid of the day's stats: metric name · value(+pct) · category. */
|
||
#cal-tip .tt-grid {
|
||
display: grid; grid-template-columns: auto auto minmax(0, 1fr);
|
||
column-gap: 12px; row-gap: 3px; align-items: baseline;
|
||
}
|
||
#cal-tip .tt-name { font-weight: 700; }
|
||
#cal-tip .tt-val { white-space: nowrap; }
|
||
#cal-tip .tt-pct { color: var(--muted); }
|
||
/* Dry streak has no percentile, so its text spans the value + category columns. */
|
||
#cal-tip .tt-span2 { grid-column: 2 / -1; }
|
||
/* The stat currently being viewed is bolded and separated from the rest, so it's
|
||
easy to match a cell's color to the stat it represents. The border spans all
|
||
three columns because it's set on every cell of the active row. */
|
||
#cal-tip .tt-name.tt-r-active, #cal-tip .tt-val.tt-r-active { font-weight: 800; }
|
||
#cal-tip .tt-r-active { padding-bottom: 5px; border-bottom: 1px solid var(--border); }
|
||
/* Category (grade) text, right-aligned, is tinted by --cat — the color that
|
||
represents that day's value for the metric. The active calendar metric gets it
|
||
bold and near full saturation; the others are lifted toward --text so they stay
|
||
readable and read as secondary. color-mix keeps every tier legible on both. */
|
||
#cal-tip .tt-cat {
|
||
justify-self: end; text-align: right; font-weight: 600;
|
||
color: color-mix(in oklab, var(--cat, var(--muted)), var(--text) 50%);
|
||
}
|
||
#cal-tip .tt-cat-active {
|
||
font-weight: 800;
|
||
color: color-mix(in oklab, var(--cat, var(--muted)), var(--text) 12%);
|
||
}
|
||
/* Plain-language "what the day was like" summary title, sits just under the date. */
|
||
#cal-tip .tt-type {
|
||
font-size: 15px; font-weight: 800; margin: 2px 0 6px;
|
||
padding-bottom: 6px; border-bottom: 1px solid var(--border);
|
||
}
|
||
|
||
/* --- single-day detail subpage --- */
|
||
.day-weather { margin: 2px 0 4px; font-size: 16px; font-weight: 800; }
|
||
.day-nav { display: flex; align-items: flex-end; gap: 8px; }
|
||
.day-date { font-size: 13px; color: var(--muted); display: flex; flex-direction: column; gap: 4px; }
|
||
.day-step {
|
||
min-width: 44px; min-height: 44px; padding: 0 12px; border-radius: 10px;
|
||
border: 1px solid var(--border); background: var(--surface-2); color: var(--text);
|
||
font-size: 22px; line-height: 1; cursor: pointer;
|
||
}
|
||
.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. 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);
|
||
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; }
|
||
.ladder-summary { display: inline-flex; align-items: baseline; gap: 8px; }
|
||
.day-obs {
|
||
font-size: 20px; font-weight: 800;
|
||
color: color-mix(in oklab, var(--cat, var(--text)), var(--text) 12%);
|
||
}
|
||
.day-obs-meta { font-size: 12.5px; color: var(--muted); }
|
||
.ladder-note {
|
||
display: flex; justify-content: space-between; align-items: baseline; gap: 12px;
|
||
margin: 4px 0 12px; font-size: 12px; color: var(--muted);
|
||
}
|
||
.ladder-note-left { min-width: 0; }
|
||
/* Historical range: bold, pinned to the right edge of the card. */
|
||
.ladder-range { font-weight: 700; color: var(--text); white-space: nowrap; }
|
||
|
||
/* Tier ladder: swatch · label · percentile range · value range · observed marker.
|
||
Fixed column widths (not auto) so every row shares the same tracks — the
|
||
percentile ranges and value ranges line up down the whole ladder. The negative
|
||
margin cancels the row's own padding so the swatches sit on the same left edge
|
||
as the card title and the subheader note above. */
|
||
.ladder { display: flex; flex-direction: column; gap: 3px; }
|
||
.ladder-row {
|
||
display: grid; grid-template-columns: 14px 128px 64px 1fr 60px;
|
||
align-items: center; column-gap: 10px; padding: 6px 8px; margin: 0 -8px;
|
||
border-radius: 8px; font-size: 13px;
|
||
}
|
||
.ladder-row.active {
|
||
background: color-mix(in oklab, var(--accent), transparent 86%);
|
||
outline: 1px solid color-mix(in oklab, var(--accent), transparent 55%);
|
||
}
|
||
.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; }
|
||
/* 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; }
|
||
|
||
|
||
/* Compact "graded days" table: a ROW per metric, a COLUMN per day. Wide runs
|
||
scroll horizontally in their own container; the metric-label column is pinned. */
|
||
.rd-scroll { position: relative; overflow-x: auto; margin: 0 0 6px; padding-bottom: 4px; }
|
||
.rd-grid { display: grid; gap: 3px; align-items: stretch; min-width: min-content; }
|
||
/* Simple scroll-orientation cue for the timeline: oldest at left, newest at right. */
|
||
.rd-orient {
|
||
display: flex; justify-content: space-between; margin: 0 0 8px;
|
||
font-size: 11.5px; font-weight: 600; letter-spacing: .03em; color: var(--muted);
|
||
}
|
||
/* Forecast (future) columns: accent-tinted headers. The .rd-today column is the
|
||
graded target day (the orange chart marker), flagged with an accent left edge;
|
||
forecast columns sit to the right of today. */
|
||
.rd-colh.rd-fc b { color: var(--accent); }
|
||
.rd-colh.rd-fc span { color: color-mix(in oklab, var(--accent), var(--muted) 45%); }
|
||
.rd-colh.rd-today b { color: var(--accent); }
|
||
.rd-c.rd-fc { opacity: .9; }
|
||
.rd-colh.rd-today,
|
||
.rd-c.rd-today { box-shadow: inset 2px 0 0 var(--accent); }
|
||
.rd-corner {
|
||
position: sticky; left: 0; z-index: 3; background: var(--surface);
|
||
}
|
||
.rd-mlabel {
|
||
position: sticky; left: 0; z-index: 2; background: var(--surface);
|
||
display: flex; align-items: center; gap: 4px; padding-right: 6px;
|
||
font-size: 11px; font-weight: 700; text-transform: uppercase; letter-spacing: .03em;
|
||
color: var(--muted);
|
||
}
|
||
/* Wind/rain cells are bare numbers to keep the grid narrow, so the row label
|
||
carries the unit — quieter than the label itself, and lowercase since "km/h"
|
||
and "mm" are unit symbols, not words. */
|
||
.rd-unit {
|
||
font-weight: 500; text-transform: none; letter-spacing: 0;
|
||
font-size: 10px; opacity: .75;
|
||
}
|
||
.rd-colh {
|
||
display: flex; flex-direction: column; align-items: center; justify-content: flex-end;
|
||
gap: 0; padding-bottom: 3px; font-size: 10px; color: var(--muted);
|
||
line-height: 1.15; white-space: nowrap; cursor: pointer;
|
||
}
|
||
.rd-colh b { color: var(--text); font-size: 11.5px; font-weight: 700; }
|
||
.rd-c {
|
||
display: flex; align-items: center; justify-content: center;
|
||
min-height: 30px; border-radius: 6px; font-size: 12px; font-weight: 700;
|
||
font-variant-numeric: tabular-nums; cursor: pointer;
|
||
background: color-mix(in oklab, var(--c, var(--surface-2)), transparent 74%);
|
||
color: color-mix(in oklab, var(--c, var(--text)), var(--text) 28%);
|
||
}
|
||
.rd-c[data-date]:hover { filter: brightness(1.18); }
|
||
|
||
/* grade colors */
|
||
.g-rec-cold { color: var(--rec-cold); } .m-rec-cold { background: var(--rec-cold); }
|
||
.g-very-cold { color: var(--very-cold); } .m-very-cold { background: var(--very-cold); }
|
||
.g-cold { color: var(--cold); } .m-cold { background: var(--cold); }
|
||
.g-cool { color: var(--cool); } .m-cool { background: var(--cool); }
|
||
.g-normal { color: var(--normal); } .m-normal { background: var(--normal); }
|
||
.g-warm { color: var(--warm); } .m-warm { background: var(--warm); }
|
||
.g-hot { color: var(--hot); } .m-hot { background: var(--hot); }
|
||
.g-very-hot { color: var(--very-hot); } .m-very-hot { background: var(--very-hot); }
|
||
.g-rec-hot { color: var(--rec-hot); } .m-rec-hot { background: var(--rec-hot); }
|
||
.g-dry { color: var(--dry); } .m-dry { background: var(--dry); }
|
||
.g-wet-1 { color: var(--wet-1); } .m-wet-1 { background: var(--wet-1); }
|
||
.g-wet-2 { color: var(--wet-2); } .m-wet-2 { background: var(--wet-2); }
|
||
.g-wet-3 { color: var(--wet-3); } .m-wet-3 { background: var(--wet-3); }
|
||
.g-wet-4 { color: var(--wet-4); } .m-wet-4 { background: var(--wet-4); }
|
||
.g-wet-5 { color: var(--wet-5); } .m-wet-5 { background: var(--wet-5); }
|
||
.g-wet-6 { color: var(--wet-6); } .m-wet-6 { background: var(--wet-6); }
|
||
.g-wet-7 { color: var(--wet-7); } .m-wet-7 { background: var(--wet-7); }
|
||
.g-wet-8 { color: var(--wet-8); } .m-wet-8 { background: var(--wet-8); }
|
||
.g-wet-9 { color: var(--wet-9); } .m-wet-9 { background: var(--wet-9); }
|
||
|
||
/* "Near Record" danger tiers: fill the whole grade cell to make it pop. */
|
||
.error { color: #e06a6a; font-size: 14px; }
|
||
.spinner { color: var(--muted); font-size: 14px; }
|
||
/* --- phones / narrow screens --- */
|
||
|
||
/* --- Find button + current-place label (opens the modal map picker) --- */
|
||
.find-bar { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; flex: 1 1 340px; }
|
||
.find-btn {
|
||
display: inline-flex; align-items: center; gap: 8px;
|
||
padding: 11px 18px; font-weight: 700; font-size: 15px; min-height: 44px;
|
||
}
|
||
.find-btn svg { width: 18px; height: 18px; flex-shrink: 0; }
|
||
.find-btn:hover { filter: brightness(1.05); }
|
||
.loc-label { color: var(--text); font-weight: 600; font-size: 14px; }
|
||
|
||
/* Weekly panel is now full width (the map lives in the picker, not inline). */
|
||
|
||
/* Link styling for the compact legend notes + guide page. */
|
||
.tk-note a, .hint a, .guide-back a { color: var(--accent); text-decoration: none; font-weight: 600; }
|
||
.tk-note a:hover, .hint a:hover, .guide-back a:hover { text-decoration: underline; }
|
||
|
||
/* --- shared modal location picker --- */
|
||
.mp-overlay {
|
||
position: fixed; inset: 0; z-index: 1000;
|
||
background: rgba(0, 0, 0, .55);
|
||
display: flex; align-items: center; justify-content: center; padding: 16px;
|
||
}
|
||
.mp-overlay[hidden] { display: none; }
|
||
.mp-modal {
|
||
width: 100%; max-width: 560px; max-height: 92vh;
|
||
display: flex; flex-direction: column;
|
||
background: var(--surface); border: 1px solid var(--border);
|
||
border-radius: 16px; overflow: hidden; box-shadow: 0 24px 60px rgba(0, 0, 0, .5);
|
||
}
|
||
.mp-head {
|
||
display: flex; align-items: center; justify-content: space-between;
|
||
padding: 14px 16px; border-bottom: 1px solid var(--border);
|
||
}
|
||
.mp-head h2 { margin: 0; font-size: 16px; }
|
||
.mp-close {
|
||
border: none; background: transparent; color: var(--muted); cursor: pointer;
|
||
font-size: 26px; line-height: 1; padding: 0 4px; min-width: 40px; min-height: 40px;
|
||
}
|
||
.mp-close:hover { color: var(--text); }
|
||
.mp-search { position: relative; display: flex; gap: 8px; padding: 12px 16px 8px; }
|
||
.mp-search input { flex: 1; min-width: 0; padding: 11px 14px; background: var(--bg); }
|
||
.mp-search button { padding: 11px 16px; min-height: 44px; }
|
||
.mp-sug {
|
||
position: absolute; top: 100%; left: 16px; right: 16px; z-index: 5;
|
||
list-style: none; margin: 4px 0 0; padding: 4px; max-height: 240px; overflow-y: auto;
|
||
background: var(--surface); border: 1px solid var(--border); border-radius: 10px;
|
||
box-shadow: 0 12px 30px rgba(0, 0, 0, .4);
|
||
}
|
||
.mp-sug li { padding: 10px 12px; border-radius: 7px; cursor: pointer; font-size: 14px; }
|
||
.mp-sug li:hover, .mp-sug li.active { background: var(--surface-2); }
|
||
.mp-sug .sub { color: var(--muted); font-size: 12px; }
|
||
.mp-map {
|
||
flex: 1 1 auto; min-height: 300px; margin: 4px 16px; z-index: 0;
|
||
border-radius: 12px; overflow: hidden; border: 1px solid var(--border);
|
||
box-shadow: inset 0 0 0 1px rgba(0, 0, 0, .25), 0 6px 18px rgba(0, 0, 0, .25);
|
||
}
|
||
.mp-map .leaflet-container { background: var(--surface-2); }
|
||
/* Base map only (roads/land/water): deepen the roads and push saturation. The
|
||
labels layer (.mp-labels) is left unfiltered so city names keep full contrast. */
|
||
.mp-map .mp-base { filter: saturate(1.3) contrast(1.12) brightness(0.92); }
|
||
/* Branded accent teardrop pin (an L.divIcon wrapping the Find-button glyph),
|
||
replacing Leaflet's default blue raster marker. */
|
||
.mp-pin { background: none; border: none; filter: drop-shadow(0 3px 4px rgba(0, 0, 0, .45)); }
|
||
.mp-pin svg { width: 32px; height: 32px; display: block; }
|
||
.mp-pin path { fill: var(--accent); stroke: rgba(0, 0, 0, .35); stroke-width: 1.25; }
|
||
.mp-pin circle { fill: #fff; stroke: none; }
|
||
/* Theme the Leaflet zoom buttons + attribution to sit in the app's surfaces
|
||
instead of Leaflet's stock white chrome. */
|
||
.mp-map .leaflet-bar { border: none; box-shadow: 0 2px 8px rgba(0, 0, 0, .3); }
|
||
.mp-map .leaflet-bar a {
|
||
background: var(--surface); color: var(--text); border-bottom-color: var(--border);
|
||
}
|
||
.mp-map .leaflet-bar a:hover { background: var(--surface-2); color: var(--accent); }
|
||
.mp-map .leaflet-control-attribution {
|
||
background: color-mix(in srgb, var(--surface) 80%, transparent);
|
||
color: var(--muted); backdrop-filter: blur(2px); border-top-left-radius: 6px;
|
||
}
|
||
.mp-map .leaflet-control-attribution a { color: var(--accent); }
|
||
.mp-foot {
|
||
display: flex; align-items: center; justify-content: space-between; gap: 12px;
|
||
padding: 12px 16px 16px; flex-wrap: wrap;
|
||
}
|
||
.mp-hint { color: var(--muted); font-size: 12.5px; flex: 1 1 auto; min-width: 0; }
|
||
.mp-confirm { padding: 11px 18px; min-height: 44px; font-weight: 700; }
|
||
.mp-confirm:disabled { opacity: .45; cursor: default; }
|
||
|
||
/* --- calendar Season/Year filters as compact dropdowns (native <details>) --- */
|
||
.cal-dd { position: relative; min-width: 0; }
|
||
.cal-dd summary {
|
||
display: flex; align-items: center; gap: 8px; cursor: pointer; user-select: none;
|
||
list-style: none; padding: 8px 12px; min-height: 40px;
|
||
border: 1px solid var(--border); border-radius: 9px; background: var(--surface);
|
||
}
|
||
.cal-dd summary::-webkit-details-marker { display: none; }
|
||
.cal-dd summary .cal-filter-label { flex: 0 0 auto; }
|
||
.cal-dd[open] summary { border-color: var(--accent); }
|
||
/* Summary value truncates rather than wrapping to a second line in the narrow cell. */
|
||
.cal-dd-sum {
|
||
flex: 1 1 auto; min-width: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
|
||
font-size: 13px; color: var(--text); font-weight: 600;
|
||
}
|
||
/* Push the caret to the right edge so summaries read cleanly at full width. */
|
||
.cal-dd-caret { margin-left: auto; color: var(--muted); font-size: 11px; transition: transform .15s ease; }
|
||
.cal-dd[open] .cal-dd-caret { transform: rotate(180deg); }
|
||
.cal-dd-panel {
|
||
position: absolute; top: calc(100% + 6px); left: 0; z-index: 20; min-width: 168px;
|
||
display: flex; flex-direction: column; gap: 2px; padding: 6px;
|
||
background: var(--surface); border: 1px solid var(--border); border-radius: 10px;
|
||
box-shadow: 0 12px 30px rgba(0, 0, 0, .35);
|
||
}
|
||
.cal-dd-opt {
|
||
display: flex; align-items: center; gap: 9px; cursor: pointer;
|
||
font-size: 14px; padding: 8px 10px; border-radius: 7px; min-height: 40px;
|
||
}
|
||
.cal-dd-opt:hover { background: var(--surface-2); }
|
||
.cal-dd-opt input { accent-color: var(--accent); width: 17px; height: 17px; margin: 0; }
|
||
/* Group headings inside a dropdown panel (the weather filter's Feel / Sky halves). */
|
||
.cal-dd-group {
|
||
font-size: 11px; text-transform: uppercase; letter-spacing: .04em;
|
||
color: var(--muted); font-weight: 700; padding: 8px 10px 2px;
|
||
}
|
||
/* The weather filter spans the full filter row beneath Seasons + Years, and its
|
||
panel is two columns so 13 checkboxes don't make a skyscraper on phones. */
|
||
.cal-weather { grid-column: 1 / -1; }
|
||
.cal-weather .cal-dd-panel {
|
||
left: 0; right: 0; display: grid; grid-template-columns: 1fr 1fr; column-gap: 6px;
|
||
}
|
||
.cal-weather .cal-dd-group { grid-column: 1 / -1; }
|
||
|
||
/* --- season-primary time filter (shared: calendar Seasons dropdown + compare) ---
|
||
Seasons are the primary, color-coded selector; each expands to its three months
|
||
(the nested suboption). One month Set backs both; a season checkbox is checked when
|
||
all three of its months are on, indeterminate (native dash) when one or two are. */
|
||
:root {
|
||
--season-winter: #5b9bd5; --season-spring: #6fbf73;
|
||
--season-summer: #e6a52c; --season-fall: #c56a3a;
|
||
}
|
||
.cal-season-panel { min-width: 212px; max-height: 320px; overflow-y: auto; gap: 4px; }
|
||
.season-row { border-radius: 8px; }
|
||
.season-row.expanded { background: color-mix(in oklab, var(--season) 12%, transparent); }
|
||
.season-head { display: flex; align-items: center; gap: 8px; }
|
||
.season-check {
|
||
display: flex; align-items: center; gap: 9px; flex: 1 1 auto; cursor: pointer;
|
||
padding: 8px 10px; border-radius: 7px; min-height: 40px; font-size: 14px;
|
||
}
|
||
.season-check:hover { background: var(--surface-2); }
|
||
.season-check input { accent-color: var(--season); width: 17px; height: 17px; margin: 0; flex: 0 0 auto; }
|
||
.season-sw {
|
||
width: 12px; height: 12px; border-radius: 3px; background: var(--season); flex: 0 0 auto;
|
||
box-shadow: inset 0 0 0 1px rgba(0, 0, 0, .25);
|
||
}
|
||
.season-name { font-weight: 600; }
|
||
/* "Full year" select-all, above the four seasons — a divider separates it, and its
|
||
swatch runs the four season colors so it reads as "everything". */
|
||
.season-allyear { --season: var(--accent); border-bottom: 1px solid var(--border); border-radius: 0; margin-bottom: 2px; }
|
||
.season-allyear .allyear-sw {
|
||
background: linear-gradient(90deg, var(--season-winter), var(--season-spring), var(--season-summer), var(--season-fall));
|
||
box-shadow: none;
|
||
}
|
||
.season-expand {
|
||
flex: 0 0 auto; display: inline-flex; align-items: center; gap: 4px; cursor: pointer;
|
||
background: transparent; border: 1px solid var(--border); border-radius: 7px;
|
||
color: var(--muted); font-size: 12px; padding: 6px 9px; min-height: 34px;
|
||
}
|
||
.season-expand:hover { color: var(--text); border-color: var(--season); }
|
||
.season-caret { font-size: 10px; transition: transform .15s ease; }
|
||
.season-expand[aria-expanded="true"] .season-caret { transform: rotate(180deg); }
|
||
.season-months {
|
||
display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 2px;
|
||
padding: 2px 4px 6px; margin: 0 0 4px 12px; border-left: 2px solid var(--season);
|
||
}
|
||
.season-months .month-opt { font-size: 13px; padding: 7px 4px; min-height: 38px; justify-content: flex-start; }
|
||
.season-months .month-opt input { accent-color: var(--season); }
|
||
.cmp-season-dd .cal-dd { display: block; }
|
||
|
||
/* Mobile filter sheet — the pill, scrim and grip stay hidden above the phone
|
||
breakpoint (switched on inside the max-width:640px block); desktop keeps the filter
|
||
panel inline. */
|
||
.filter-fab, .filter-scrim, .filter-sheet-grip { display: none; }
|
||
|
||
/* Category totals above the grid: a compact distribution strip. Each category is a
|
||
column captioned with its name over its value (share by default, raw count when
|
||
"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: 960px; margin: 0 auto 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; }
|
||
.ct-head .section-title { margin-bottom: 4px; }
|
||
.ct-count-toggle {
|
||
display: inline-flex; align-items: center; gap: 6px; flex-shrink: 0;
|
||
font-size: 12.5px; color: var(--muted); cursor: pointer; user-select: none;
|
||
margin-bottom: 4px;
|
||
}
|
||
.ct-count-toggle input { accent-color: var(--accent); width: 15px; height: 15px; margin: 0; cursor: pointer; }
|
||
/* Connected category bar chart: one bar per tier, low→high, sitting on a shared
|
||
baseline and nearly touching so they read as one chart. The tier's average value
|
||
sits above its bar, the min–max range inside it, and the share + name below.
|
||
Bar height encodes frequency (within the metric's scale group). */
|
||
/* Wraps the unit label + the scrolling strip. The label sits outside .ct-strip's
|
||
overflow so it stays put when a wide (precip) strip scrolls. */
|
||
.ct-plot { position: relative; }
|
||
/* The metric's unit, shown once at the top-right of the strip — the values on the
|
||
bars are bare so they don't clip a narrow phone column. */
|
||
.ct-unit {
|
||
text-align: right; margin: 6px 2px -2px; height: 1em;
|
||
font-size: 11px; font-weight: 700; line-height: 1;
|
||
color: var(--muted); font-variant-numeric: tabular-nums;
|
||
}
|
||
.ct-strip {
|
||
display: flex; align-items: flex-end; gap: 1px; margin: 10px 0 6px; overflow-x: auto;
|
||
}
|
||
.ct-col {
|
||
flex: 1 1 0; min-width: 38px;
|
||
display: flex; flex-direction: column; justify-content: flex-end; align-items: stretch;
|
||
text-align: center;
|
||
}
|
||
/* Average value, pinned just above each bar's top. */
|
||
.ct-top {
|
||
min-height: 1.25em; margin-bottom: 3px; font-size: 10.5px; font-weight: 700; line-height: 1;
|
||
color: color-mix(in oklab, var(--c, var(--muted)), var(--text) 45%);
|
||
white-space: nowrap; overflow: hidden;
|
||
}
|
||
.ct-bar {
|
||
width: 100%; background: var(--c); border-radius: 4px 4px 0 0;
|
||
display: flex; align-items: center; justify-content: center;
|
||
}
|
||
.ct-bar-0 { height: 2px; opacity: .35; }
|
||
/* The tier's low–high span, directly under the average and above the bar. It used
|
||
to sit inside the bar as a dark pill; a column is ~38px on a phone, so it clipped
|
||
there however small the type got. Out here it gets the whole column width, and
|
||
needs no scrim to stay legible over nine different tier colours. */
|
||
.ct-range {
|
||
min-height: 1.1em; margin-bottom: 3px;
|
||
font-size: 9px; font-weight: 600; line-height: 1;
|
||
font-variant-numeric: tabular-nums;
|
||
color: var(--muted); opacity: .9;
|
||
white-space: nowrap; overflow: hidden;
|
||
}
|
||
/* Share + category name, below the bar. */
|
||
.ct-cap { display: flex; flex-direction: column; align-items: center; gap: 1px; margin-top: 5px; }
|
||
.ct-num {
|
||
font-size: 12px; font-weight: 700; line-height: 1;
|
||
color: color-mix(in oklab, var(--c, var(--muted)), var(--text) 40%);
|
||
}
|
||
.ct-num-zero { font-weight: 600; opacity: .38; }
|
||
.ct-label {
|
||
min-height: 2.2em; font-size: 10px; line-height: 1.1; font-weight: 600;
|
||
color: var(--muted); overflow: hidden;
|
||
/* The strip's columns sit 1px apart, so long neighbouring names (e.g. "Very
|
||
Heavy" next to "Extreme") ran into each other and read as one word. */
|
||
padding: 0 2px;
|
||
}
|
||
|
||
/* Days filtered out by the weather filter fade back so matches pop. */
|
||
.cal-cell.dim { opacity: .15; }
|
||
|
||
/* --- guide / legend page --- */
|
||
.guide { max-width: 760px; }
|
||
.guide-block { margin: 0 0 26px; }
|
||
.guide-block h2 { font-size: 16px; margin: 0 0 10px; }
|
||
.guide-block p { font-size: 14px; margin: 0 0 10px; }
|
||
.guide-scale { display: flex; flex-wrap: wrap; gap: 8px 18px; }
|
||
.guide-seg { display: inline-flex; align-items: center; gap: 8px; font-size: 13.5px; }
|
||
.guide-sw { width: 22px; height: 14px; border-radius: 4px; flex-shrink: 0; }
|
||
.guide-lbl { font-weight: 600; }
|
||
.guide-rng { color: var(--muted); font-size: 12px; }
|
||
.guide-metrics { display: grid; grid-template-columns: max-content 1fr; gap: 8px 18px; margin: 0; }
|
||
.guide-metrics dt { font-weight: 700; color: var(--text); }
|
||
.guide-metrics dd { margin: 0; color: var(--muted); font-size: 14px; }
|
||
.guide-back { margin-top: 8px; }
|
||
|
||
|
||
/* ===== Compare view ===== */
|
||
.cmp-locs { flex: 1 1 340px; }
|
||
.cmp-loc-list { list-style: none; margin: 8px 0 12px; padding: 0; display: flex; flex-wrap: wrap; gap: 8px; }
|
||
.cmp-loc-list:empty { margin: 0; }
|
||
.cmp-chip {
|
||
display: inline-flex; align-items: center; gap: 6px;
|
||
background: var(--surface-2); border: 1px solid var(--border);
|
||
border-radius: 999px; padding: 5px 5px 5px 14px; font-size: 14px;
|
||
}
|
||
.cmp-chip.loading { color: var(--muted); }
|
||
.cmp-chip.pending { color: var(--muted); border-style: dashed; }
|
||
.cmp-chip.error { border-color: var(--very-hot); color: var(--very-hot); }
|
||
|
||
/* Add-location + Refresh sit together; Refresh only shows while changes are pending. */
|
||
.cmp-loc-actions { display: flex; flex-wrap: wrap; gap: 10px; align-items: center; }
|
||
.cmp-refresh-btn {
|
||
padding: 11px 18px; border-radius: 10px; cursor: pointer;
|
||
font-weight: 700; font-size: 15px; min-height: 44px;
|
||
background: var(--accent); color: #1a1206; border: 1px solid var(--accent);
|
||
}
|
||
.cmp-refresh-btn[hidden] { display: none; }
|
||
.cmp-refresh-btn:hover { filter: brightness(1.05); }
|
||
.cmp-chip-name {
|
||
font-weight: 600; max-width: 44vw;
|
||
overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
|
||
}
|
||
.cmp-chip-x {
|
||
border: none; background: transparent; color: var(--muted); cursor: pointer;
|
||
font-size: 20px; line-height: 1; width: 30px; height: 30px; border-radius: 999px;
|
||
}
|
||
.cmp-chip-x:hover { color: var(--text); background: var(--border); }
|
||
|
||
/* Parameter panel: comfort target, band, judged temperature, date range. */
|
||
.cmp-params {
|
||
display: grid; grid-template-columns: repeat(auto-fit, minmax(230px, 1fr));
|
||
gap: 18px 24px; margin: 0 0 22px; padding: 16px;
|
||
border: 1px solid var(--border); border-radius: 12px; background: var(--surface);
|
||
}
|
||
.cmp-params[hidden] { display: none; }
|
||
.cmp-param > label { display: block; font-size: 13px; color: var(--muted); margin: 0 0 8px; }
|
||
.cmp-param > label b { color: var(--text); font-size: 15px; }
|
||
.cmp-param input[type="range"] {
|
||
width: 100%; height: 44px; margin: 0; accent-color: var(--accent); background: transparent;
|
||
}
|
||
.cmp-param .hint { display: block; margin: 4px 0 0; font-size: 12px; color: var(--muted); max-width: none; }
|
||
.cmp-basis-block .metric-toggle { margin-top: 2px; }
|
||
.cmp-basis-block .metric-toggle button { flex: 1 0 auto; padding: 10px 14px; }
|
||
.cmp-range { align-content: start; }
|
||
|
||
/* Ranked result cards. */
|
||
.cmp-results { display: grid; gap: 14px; }
|
||
.cmp-card { border: 1px solid var(--border); border-radius: 12px; background: var(--surface); padding: 16px 18px; }
|
||
.cmp-card-top { display: flex; align-items: center; gap: 12px; }
|
||
/* Rank badge: a filled disc — accent for the winner, muted for the rest. */
|
||
.cmp-rank {
|
||
flex: 0 0 auto; width: 26px; height: 26px; border-radius: 50%;
|
||
display: grid; place-items: center; font-size: 13px; font-weight: 800;
|
||
background: var(--surface-2); color: var(--muted);
|
||
}
|
||
.cmp-card-name { flex: 1 1 auto; min-width: 0; }
|
||
/* Name hierarchy: the lead segment bold, the rest (city · region · country) muted
|
||
beneath — both truncate rather than wrap so the card head stays one tidy block. */
|
||
.cmp-name { display: flex; flex-direction: column; gap: 1px; min-width: 0; }
|
||
.cmp-name b { font-size: 16px; font-weight: 700; line-height: 1.2; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||
.cmp-name-sub { font-size: 12.5px; color: var(--muted); font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||
.cmp-daycount { font-size: 12px; color: var(--muted); }
|
||
.cmp-big { flex: 0 0 auto; text-align: right; }
|
||
.cmp-big b { display: block; font-size: 22px; color: var(--normal); line-height: 1.1; }
|
||
.cmp-big span { font-size: 10.5px; color: var(--muted); text-transform: uppercase; letter-spacing: .04em; }
|
||
|
||
/* Winner: accent frame + badge, and a "Best match" flag on the subline. */
|
||
.cmp-win { border-color: var(--accent); box-shadow: inset 0 0 0 1px var(--accent); }
|
||
.cmp-win .cmp-rank { background: var(--accent); color: #1a1206; }
|
||
.cmp-best { color: var(--accent); font-weight: 700; }
|
||
|
||
/* Diverging comfort bar: a fixed center marks the comfort zone; colder days grow
|
||
left of it, warmer days grow right. Both halves share one 0–100% scale, so a
|
||
cold-skewed place and a hot-skewed one mirror each other and compare at a glance. */
|
||
/* Full-width day-distribution bar: five temperature buckets, cold to hot, together
|
||
100% of the days, in the site's own temperature colors. min-width keeps a thin
|
||
sliver visible; the key below spells out every color and share. */
|
||
.cmp-bar { display: flex; height: 20px; border-radius: 6px; overflow: hidden; margin: 14px 0 10px; background: var(--surface-2); }
|
||
.cmp-seg { height: 100%; min-width: 2px; }
|
||
.cmp-seg-vcold { background: var(--very-cold); }
|
||
.cmp-seg-cool { background: var(--cool); }
|
||
.cmp-seg-comf { background: var(--normal); }
|
||
.cmp-seg-warm { background: var(--warm); }
|
||
.cmp-seg-vhot { background: var(--very-hot); }
|
||
|
||
/* Key under the bar: swatch + plain name + share for every bucket the place has, in
|
||
the same cold-to-hot order as the bar — so a 1% span is named as clearly as a 60% one. */
|
||
.cmp-key { display: flex; flex-wrap: wrap; gap: 6px 14px; font-size: 12.5px; color: var(--muted); }
|
||
.cmp-key-item { display: inline-flex; align-items: center; gap: 6px; }
|
||
.cmp-key-item b { color: var(--text); font-weight: 700; }
|
||
.cmp-sw { width: 12px; height: 12px; border-radius: 3px; flex: 0 0 auto; }
|
||
/* Desktop: spread the labels across the bar's width, each centred under its own
|
||
segment; spreadKeys() then de-collides them and keeps the run inside the edges. */
|
||
@media (min-width: 641px) {
|
||
.cmp-key { position: relative; display: block; height: 18px; }
|
||
.cmp-key-item { position: absolute; transform: translateX(-50%); white-space: nowrap; }
|
||
}
|
||
|
||
.cmp-bands { margin: 8px 0 0; font-size: 12.5px; color: var(--muted); }
|
||
.cmp-bands b { color: var(--text); }
|
||
|
||
/* Baseline strip: every place's typical basis temperature on one shared °F axis,
|
||
with the comfort range shaded, so the places' baselines line up side by side. */
|
||
.cmp-baseline { margin: 0 0 18px; padding: 14px 16px; border: 1px solid var(--border); border-radius: 12px; background: var(--surface); }
|
||
.cmp-baseline[hidden] { display: none; }
|
||
.cmp-bl-head { font-size: 14px; font-weight: 700; }
|
||
.cmp-bl-head .hint { display: block; font-weight: 400; font-size: 11.5px; color: var(--muted); margin: 2px 0 12px; max-width: none; }
|
||
.cmp-bl-rows { display: grid; gap: 10px; }
|
||
.cmp-bl-row { display: grid; grid-template-columns: 120px 1fr; align-items: center; gap: 12px; }
|
||
.cmp-bl-name { font-size: 13px; font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||
.cmp-bl-track { position: relative; height: 26px; }
|
||
.cmp-bl-tol { position: absolute; top: 0; bottom: 0; border-radius: 3px; background: color-mix(in srgb, var(--normal) 9%, transparent); }
|
||
.cmp-bl-band {
|
||
position: absolute; top: 0; bottom: 0; border-radius: 3px;
|
||
background: color-mix(in srgb, var(--normal) 20%, transparent);
|
||
outline: 1px solid color-mix(in srgb, var(--normal) 50%, transparent); outline-offset: -1px;
|
||
}
|
||
.cmp-bl-spread { position: absolute; top: 17px; height: 4px; transform: translateY(-50%); border-radius: 2px; opacity: .6; }
|
||
.cmp-bl-dot { position: absolute; top: 17px; width: 12px; height: 12px; border-radius: 50%; transform: translate(-50%, -50%); border: 2px solid var(--surface); }
|
||
.cmp-bl-val { position: absolute; top: 1px; transform: translateX(-50%); font-size: 10.5px; font-weight: 700; color: var(--text); white-space: nowrap; }
|
||
.cmp-bl-spread.cmp-below, .cmp-bl-dot.cmp-below { background: var(--cold); }
|
||
.cmp-bl-spread.cmp-comfort, .cmp-bl-dot.cmp-comfort { background: var(--normal); }
|
||
.cmp-bl-spread.cmp-above, .cmp-bl-dot.cmp-above { background: var(--hot); }
|
||
.cmp-bl-axis { display: flex; justify-content: space-between; margin: 8px 0 0 132px; font-size: 11px; color: var(--muted); }
|
||
|
||
/* Loading affordances: a spinning ring (chips + header) and shimmering skeleton
|
||
cards, so a still-loading place is obvious and the results don't pop in cold. */
|
||
@keyframes cmp-spin { to { transform: rotate(360deg); } }
|
||
@keyframes cmp-shimmer { 100% { background-position: -200% 0; } }
|
||
.cmp-spinner {
|
||
display: inline-block; width: 14px; height: 14px; border-radius: 50%; box-sizing: border-box;
|
||
border: 2px solid var(--border); border-top-color: var(--accent);
|
||
animation: cmp-spin .7s linear infinite; vertical-align: -2px;
|
||
}
|
||
.cmp-spinner-chip { width: 12px; height: 12px; }
|
||
.cmp-chip-dot { display: inline-block; width: 7px; height: 7px; border-radius: 50%; background: var(--muted); opacity: .55; }
|
||
.cmp-loading { display: flex; align-items: center; gap: 8px; margin: 0 0 14px; color: var(--muted); font-size: 14px; }
|
||
.cmp-loading-inline { display: inline-flex; align-items: center; gap: 6px; }
|
||
.cmp-loading-inline .cmp-spinner { width: 12px; height: 12px; }
|
||
|
||
.cmp-skel-top { display: flex; align-items: center; gap: 12px; }
|
||
.cmp-skel-badge, .cmp-skel-name, .cmp-skel-big, .cmp-skel-bar {
|
||
display: block; border-radius: 6px;
|
||
background: linear-gradient(90deg, var(--surface-2) 25%, var(--border) 37%, var(--surface-2) 63%);
|
||
background-size: 200% 100%; animation: cmp-shimmer 1.3s ease-in-out infinite;
|
||
}
|
||
.cmp-skel-badge { width: 26px; height: 26px; border-radius: 50%; flex: 0 0 auto; }
|
||
.cmp-skel-name { flex: 1 1 auto; height: 16px; }
|
||
.cmp-skel-big { width: 56px; height: 22px; flex: 0 0 auto; }
|
||
.cmp-skel-bar { height: 16px; margin-top: 16px; }
|
||
@media (prefers-reduced-motion: reduce) {
|
||
.cmp-spinner { animation-duration: 1.4s; }
|
||
.cmp-skel-badge, .cmp-skel-name, .cmp-skel-big, .cmp-skel-bar { animation: none; }
|
||
}
|
||
|
||
/* Comfort vs tolerance shown as two separate labels, and a temperature marker above
|
||
each of the four handles (the band borders). */
|
||
/* Comfort/tolerance panel — pulled out of the params grid so it stays inline above
|
||
the results (and out of the mobile filter sheet); a full-width bordered card. */
|
||
.cmp-comfort-wrap {
|
||
margin: 0 0 18px; padding: 14px 16px 8px;
|
||
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); }
|
||
.cmp-ticks { position: relative; height: 17px; }
|
||
.cmp-tick { position: absolute; transform: translateX(-50%); font-size: 11px; font-weight: 700; color: var(--text); white-space: nowrap; }
|
||
.cmp-tick.tol { color: var(--muted); font-weight: 600; }
|
||
|
||
/* Dual-handle comfort slicer: two range inputs overlaid on a shared track, with a
|
||
fill marking the selected span. Only the thumbs take pointer events, so both
|
||
handles stay grabbable even where the inputs overlap. */
|
||
.dual-range { position: relative; height: 44px; }
|
||
.dual-range .dual-track {
|
||
position: absolute; left: 0; right: 0; top: 50%; transform: translateY(-50%);
|
||
height: 6px; border-radius: 3px; background: var(--surface-2);
|
||
}
|
||
.dual-range .dual-fill { position: absolute; top: 0; bottom: 0; border-radius: 3px; background: var(--accent); }
|
||
.dual-range input[type="range"] {
|
||
position: absolute; left: 0; top: 0; width: 100%; height: 100%; margin: 0;
|
||
-webkit-appearance: none; appearance: none; background: transparent; pointer-events: none;
|
||
}
|
||
.dual-range input[type="range"]::-webkit-slider-thumb {
|
||
-webkit-appearance: none; appearance: none; pointer-events: auto;
|
||
width: 22px; height: 22px; border-radius: 50%; background: var(--accent);
|
||
border: 2px solid var(--surface); box-shadow: 0 1px 3px rgba(0, 0, 0, .35); cursor: pointer;
|
||
}
|
||
.dual-range input[type="range"]::-moz-range-thumb {
|
||
pointer-events: auto; width: 22px; height: 22px; border-radius: 50%; background: var(--accent);
|
||
border: 2px solid var(--surface); box-shadow: 0 1px 3px rgba(0, 0, 0, .35); cursor: pointer;
|
||
}
|
||
.dual-range input[type="range"]::-webkit-slider-runnable-track { background: transparent; border: none; }
|
||
.dual-range input[type="range"]::-moz-range-track { background: transparent; border: none; }
|
||
|
||
/* Quad-handle variant: an outer tolerance range around the inner comfort range on
|
||
the same track. The tolerance fill sits behind the comfort fill; tolerance thumbs
|
||
are hollow (a ring) so they read as secondary, and the comfort thumbs stack above
|
||
them so the inner handles always win where two thumbs overlap. */
|
||
.quad-range .dual-fill-tol { background: color-mix(in srgb, var(--accent) 28%, transparent); }
|
||
.quad-range #cmp-lo, .quad-range #cmp-hi { z-index: 2; }
|
||
.quad-range input.thumb-tol::-webkit-slider-thumb { width: 18px; height: 18px; background: var(--surface); border: 2px solid var(--accent); }
|
||
.quad-range input.thumb-tol::-moz-range-thumb { width: 18px; height: 18px; background: var(--surface); border: 2px solid var(--accent); }
|
||
|
||
/* Climate distribution below the ranked cards: a metric toggle + share/count toggle,
|
||
then one calendar-style distribution strip (.ct-strip, shared) per location. */
|
||
.cmp-dist { margin: 24px 0 8px; }
|
||
.cmp-dist[hidden] { display: none; }
|
||
.cmp-dist .ct-head { align-items: center; }
|
||
.cmp-dist-controls { display: flex; align-items: center; flex-wrap: wrap; gap: 8px 12px; }
|
||
.cmp-dist-controls .cal-filter-label { margin: 0; }
|
||
.cmp-dist .metric-toggle { margin: 0; }
|
||
/* minmax(0,1fr) + the row's min-width:0 let the track shrink below the strip's
|
||
min-content, so the connected bar chart scrolls inside its own .ct-strip
|
||
(overflow-x:auto) instead of widening the whole page on a phone. */
|
||
.cmp-dist-body { display: grid; grid-template-columns: minmax(0, 1fr); gap: 12px; margin-top: 14px; }
|
||
.cmp-dist-row {
|
||
min-width: 0;
|
||
border: 1px solid var(--border); border-radius: 12px; background: var(--surface); padding: 12px 16px 6px;
|
||
}
|
||
.cmp-dist-name { display: flex; align-items: baseline; gap: 8px; font-size: 14px; font-weight: 700; }
|
||
.cmp-dist-days { font-size: 12px; font-weight: 400; color: var(--muted); }
|
||
|
||
/* --- phones / narrow screens — every mobile override lives in this one
|
||
block (they used to be scattered across four) --- */
|
||
@media (max-width: 640px) {
|
||
.calendar { gap: 16px; }
|
||
.metric-toggle { width: 100%; }
|
||
/* 7 metrics wrap to ~4 per row on a phone instead of overflowing one line. */
|
||
.metric-toggle button { flex: 1 0 25%; padding: 11px 4px; font-size: 12px; }
|
||
/* Compress the day tooltip so it fits narrow phone screens without cutting off. */
|
||
#cal-tip { padding: 7px 9px; min-width: 0; font-size: 11.5px; line-height: 1.4; }
|
||
#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. Slack moved
|
||
from the pct/marker tracks to the value column keeps the longest range
|
||
("17.3–19.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 46px 1fr 42px; column-gap: 8px; font-size: 12.5px; }
|
||
}
|
||
|
||
/* The full inline header (logo + name + tagline + °F/°C + Sign in + six nav tabs)
|
||
only fits comfortably on a wide screen; squeeze it and the tagline crushes the
|
||
title into a one-word-per-line sliver. So: hide the tagline below 1200px, and
|
||
below 1080px fold the nav, °F/°C and account into a hamburger dropdown, leaving a
|
||
clean compact header (logo + name + hamburger). The body keeps its ≤640px rules. */
|
||
@media (max-width: 1199px) { .tag { display: none; } }
|
||
|
||
@media (max-width: 1080px) {
|
||
header { padding: 14px 16px; }
|
||
h1 { font-size: 19px; }
|
||
|
||
/* One compact row: logo + name on the left, the hamburger flush-right. The nav,
|
||
°F/°C toggle, bell and account all fold into the hamburger dropdown below, so
|
||
the header never grows a second control-bar row. */
|
||
/* The hamburger is fixed (out of flow) — reserve its corner so the lockup
|
||
can't slide beneath it. */
|
||
.brand { flex-wrap: nowrap; align-items: center; gap: 10px; padding-right: 56px; }
|
||
.brand > div:not(.unit-toggle):not(.acct) { flex: 1 1 auto; min-width: 0; }
|
||
.logo { align-self: center; margin-top: 0; }
|
||
|
||
/* A real box again (not display:contents) so it anchors the absolute dropdown
|
||
and holds the hamburger — fixed to the viewport's top-right so nav follows
|
||
the user down the page. Below the filter-sheet scrim (950) and the map
|
||
modal (1000) so overlays still cover it. */
|
||
.nav-menu { display: inline-flex; position: fixed; top: 10px; right: 14px; z-index: 940; }
|
||
.nav-toggle {
|
||
display: inline-flex; align-items: center; justify-content: center;
|
||
width: 44px; height: 44px; border: 1px solid var(--border); border-radius: 12px;
|
||
background: var(--surface-2); color: var(--text);
|
||
box-shadow: 0 2px 10px rgba(0, 0, 0, .25);
|
||
}
|
||
.nav-menu[open] .nav-toggle { border-color: var(--accent); color: var(--accent); }
|
||
|
||
/* The panel is the dropdown: right-anchored and clamped to the viewport so it
|
||
never runs off-screen (this also fixes the SEO/city pages, whose only panel
|
||
item is the nav). Contents stack nav → °F/°C → account top-to-bottom. */
|
||
.nav-panel {
|
||
display: flex; flex-direction: column; align-items: stretch; gap: 6px;
|
||
position: absolute; right: 0; top: calc(100% + 8px); z-index: 60;
|
||
margin: 0; padding: 8px; width: min(208px, calc(100vw - 32px));
|
||
background: var(--surface); border: 1px solid var(--border); border-radius: 14px;
|
||
box-shadow: 0 16px 40px rgba(0, 0, 0, .45);
|
||
}
|
||
.nav-menu:not([open]) .nav-panel { display: none; }
|
||
/* Reset the desktop cluster order — the dropdown wants plain DOM order. */
|
||
.nav-panel > .unit-toggle, .nav-panel > .acct, .nav-panel > .view-nav { order: 0; }
|
||
|
||
/* Nav links: a compact, left-aligned list with an inset hairline between each
|
||
entry (suppressed around the active pill so nothing crosses it). The whole
|
||
group is hairline-separated from the controls below — the :not(:last-child)
|
||
keeps SEO pages, which have nav only, divider-free. */
|
||
.view-nav {
|
||
display: flex; flex-direction: column; align-items: stretch; align-self: stretch;
|
||
gap: 0; margin: 0; width: auto;
|
||
background: transparent; border: 0; border-radius: 0; padding: 0;
|
||
}
|
||
.nav-panel > .view-nav:not(:last-child) {
|
||
padding-bottom: 6px; margin-bottom: 4px; border-bottom: 1px solid var(--border);
|
||
}
|
||
.nav-panel .view-nav a {
|
||
min-height: 40px; padding: 8px 12px; justify-content: flex-start;
|
||
border-radius: 8px; position: relative;
|
||
}
|
||
.nav-panel .view-nav a + a::before { display: none; }
|
||
.nav-panel .view-nav a:not(:last-child)::after {
|
||
content: ""; position: absolute; left: 12px; right: 12px; bottom: 0;
|
||
height: 1px; background: var(--border);
|
||
}
|
||
.nav-panel .view-nav a.active::after,
|
||
.nav-panel .view-nav a:has(+ a.active)::after { display: none; }
|
||
|
||
/* °F/°C toggle: full-width segmented control inside the dropdown. */
|
||
.nav-panel > .unit-toggle { margin: 0; width: 100%; align-self: stretch; }
|
||
.nav-panel > .unit-toggle button { flex: 1; }
|
||
|
||
/* Account row (bell + account/Sign in): a compact chip that hugs its label and
|
||
sits beside the bell, centered in the dropdown, rather than a full-width bar.
|
||
The 40px min-height stays for the tap target; padding and truncation tighten
|
||
so the long email no longer dominates. Popovers open below, clamped to the
|
||
viewport so the notification list never overflows. */
|
||
.nav-panel > .acct { margin: 0; width: 100%; align-self: stretch; justify-content: center; }
|
||
.nav-panel > .acct .acct-btn:not(.notif-btn) { padding: 8px 10px; }
|
||
.nav-panel > .acct .acct-name { max-width: 12ch; }
|
||
/* The bell's notification list would anchor to the small bell button and spill
|
||
off the left edge; drop .notif's positioning context so it anchors to the
|
||
panel instead — dropping below the menu, right-aligned and within the
|
||
viewport. The account menu already fits; just clamp its width. */
|
||
.nav-panel .notif { position: static; }
|
||
.nav-panel .notif-pop { right: 0; width: min(340px, calc(100vw - 24px)); }
|
||
.acct-pop { max-width: calc(100vw - 40px); }
|
||
}
|
||
|
||
@media (max-width: 640px) {
|
||
main { padding: 14px 14px 40px; }
|
||
.controls { gap: 12px; margin-bottom: 14px; }
|
||
|
||
/* Map + panel stack (from the 900px rule); shrink the map so results are
|
||
reachable with less scrolling on a phone. */
|
||
.panel { padding: 16px; }
|
||
|
||
/* Give buttons a comfortable touch target (~44px). */
|
||
.date-row button { padding: 12px 16px; min-height: 44px; }
|
||
.share button { padding: 9px 12px; font-size: 12.5px; min-height: 38px; }
|
||
|
||
.date-row { gap: 10px; width: 100%; }
|
||
.hint { max-width: none; flex-basis: 100%; }
|
||
|
||
.loc-head { flex-wrap: wrap; }
|
||
.share { flex-wrap: wrap; }
|
||
|
||
/* Keep the normal cards legible when very narrow. */
|
||
.normals { gap: 8px; }
|
||
.normal-card { padding: 10px 11px; }
|
||
.normal-card .big { font-size: 19px; }
|
||
|
||
/* Compact table tuned for phone width. */
|
||
/* Chart metric toggle: force an even 4-per-row grid so the buttons line up
|
||
(without this the flex-wrap sizes each to its label and the rows go ragged). */
|
||
.chart-toggle { width: 100%; }
|
||
.chart-toggle button { flex: 1 0 25%; padding: 9px 4px; font-size: 12px; }
|
||
.rd-c { min-height: 30px; font-size: 11px; border-radius: 5px; }
|
||
.rd-mlabel { font-size: 10px; padding-right: 4px; }
|
||
.rd-colh { font-size: 9px; }
|
||
.rd-colh b { font-size: 10.5px; }
|
||
|
||
.mp-overlay { padding: 0; }
|
||
/* Full-screen sheet. Size to the *dynamic* viewport (dvh) — plain 100vh on
|
||
iOS Safari is the large-viewport height (toolbars retracted), so with the
|
||
chrome showing the footer's "Use this location" button falls behind it and
|
||
gets cut off. dvh tracks the currently-visible height, and the flex map
|
||
gives up the space so the footer stays on-screen. vh kept as a fallback. */
|
||
.mp-modal {
|
||
max-width: none; border-radius: 0; border: none;
|
||
max-height: 100vh; height: 100vh;
|
||
max-height: 100dvh; height: 100dvh;
|
||
}
|
||
/* Clear the home-indicator inset once the bottom toolbar retracts. */
|
||
.mp-foot { padding-bottom: calc(16px + env(safe-area-inset-bottom)); }
|
||
.find-bar { width: 100%; }
|
||
.guide-metrics { grid-template-columns: 1fr; gap: 2px 0; }
|
||
.guide-metrics dt { margin-top: 10px; }
|
||
|
||
/* Tighter distribution columns so a 9-tier temp strip fits a phone without
|
||
scrolling (precip's 10th column still scrolls inside its own .ct-strip).
|
||
The low–high line shrinks to match: at 34px a column has room for "17–62"
|
||
comfortably, and for precip's ".04–.46" once the leading zero is dropped. */
|
||
.ct-col { min-width: 34px; }
|
||
.ct-range { font-size: 8px; }
|
||
|
||
.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) --- */
|
||
.filter-fab {
|
||
display: none; position: fixed; z-index: 900;
|
||
left: 50%; transform: translateX(-50%);
|
||
bottom: calc(14px + env(safe-area-inset-bottom));
|
||
align-items: center; gap: 7px; padding: 9px 16px; min-height: 40px;
|
||
background: color-mix(in srgb, var(--surface) 92%, transparent);
|
||
color: var(--text); border: 1px solid var(--border); border-radius: 999px;
|
||
box-shadow: 0 6px 20px rgba(0, 0, 0, .38); backdrop-filter: blur(6px);
|
||
font-size: 13px; font-weight: 600; cursor: pointer;
|
||
}
|
||
.filter-fab.ready { display: inline-flex; }
|
||
.filter-fab .ff-ic { width: 16px; height: 16px; }
|
||
.filter-fab .ff-dot {
|
||
width: 8px; height: 8px; border-radius: 50%; background: var(--accent);
|
||
box-shadow: 0 0 0 2px var(--surface);
|
||
}
|
||
.filter-scrim { display: block; position: fixed; inset: 0; z-index: 950; background: rgba(0, 0, 0, .5); }
|
||
.filter-scrim[hidden] { display: none; }
|
||
|
||
/* The filter panel becomes a bottom sheet, off-screen until the pill opens it.
|
||
id selectors keep these above the pages' own class-based mobile rules. */
|
||
#cmp-params, #cal-filters {
|
||
position: fixed; left: 0; right: 0; bottom: 0; z-index: 960;
|
||
max-width: none; margin: 0; border-radius: 16px 16px 0 0;
|
||
max-height: 85dvh; overflow-y: auto; transform: translateY(110%);
|
||
padding: 16px 16px calc(18px + env(safe-area-inset-bottom));
|
||
box-shadow: 0 -12px 40px rgba(0, 0, 0, .5);
|
||
}
|
||
#cmp-params.open, #cal-filters.open { transform: translateY(0); }
|
||
|
||
.filter-sheet-grip {
|
||
display: flex; align-items: center; gap: 10px; position: relative;
|
||
padding: 6px 2px 10px; margin-bottom: 10px; border-bottom: 1px solid var(--border);
|
||
}
|
||
.filter-sheet-grip .fsg-bar {
|
||
position: absolute; top: -6px; left: 50%; transform: translateX(-50%);
|
||
width: 36px; height: 4px; border-radius: 2px; background: var(--border);
|
||
}
|
||
.filter-sheet-grip .fsg-title { font-weight: 700; font-size: 15px; }
|
||
.filter-sheet-grip .fsg-close {
|
||
margin-left: auto; background: transparent; border: none; color: var(--muted);
|
||
font-size: 20px; line-height: 1; min-width: 40px; min-height: 40px; cursor: pointer;
|
||
}
|
||
/* Room so the pill never covers the last bit of content (only on pages with it). */
|
||
body.has-filter-sheet main { padding-bottom: 76px; }
|
||
.cmp-big b { font-size: 20px; }
|
||
/* Baseline strip: tighten the name gutter and shrink text so the shared axis
|
||
still fits a phone; keep the axis ticks aligned to the narrower track. */
|
||
.cmp-bl-row { grid-template-columns: 84px 1fr; gap: 8px; }
|
||
.cmp-bl-name { font-size: 12px; }
|
||
.cmp-bl-axis { margin-left: 92px; }
|
||
.cmp-key { font-size: 11.5px; gap: 5px 12px; }
|
||
}
|
||
|
||
/* Slide the filter sheet only when motion is welcome (phones). */
|
||
@media (max-width: 640px) and (prefers-reduced-motion: no-preference) {
|
||
#cmp-params, #cal-filters { transition: transform .28s ease; }
|
||
}
|
||
|
||
/* ============================================================
|
||
Server-rendered content pages (climate hub / city / month /
|
||
records / glossary / about). Text-first, minimal JS.
|
||
============================================================ */
|
||
/* Link colour/decoration for the brand now lives with the lockup rule at the top
|
||
of this file, so it covers the static pages' <h1> form too. */
|
||
.site-name { font-size: 20px; margin: 0; }
|
||
main.content { max-width: 880px; margin: 0 auto; padding: 8px 20px 56px; line-height: 1.6; }
|
||
main.content h1 { font-size: clamp(26px, 4vw, 36px); margin: 8px 0 4px; }
|
||
main.content h2 { font-size: 22px; margin: 32px 0 10px; }
|
||
main.content p { margin: 10px 0; }
|
||
main.content a { color: var(--accent); }
|
||
.content .lede { font-size: 18px; color: var(--text); }
|
||
.breadcrumb { font-size: 13px; color: var(--muted); margin: 4px 0 12px; }
|
||
.breadcrumb a { color: var(--muted); text-decoration: none; }
|
||
.breadcrumb a:hover { color: var(--accent); }
|
||
.breadcrumb .sep { opacity: .5; }
|
||
|
||
.content .cta {
|
||
display: inline-block; margin-top: 6px; padding: 11px 18px; border-radius: 10px;
|
||
background: var(--accent); color: #1a1206; font-weight: 700; text-decoration: none;
|
||
}
|
||
.content .cta:hover { filter: brightness(1.05); }
|
||
|
||
.today-cards { display: grid; grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); gap: 10px; margin: 12px 0; }
|
||
.today-card {
|
||
display: flex; flex-direction: column; gap: 3px; padding: 12px 14px;
|
||
background: var(--surface); border: 1px solid var(--border); border-left: 5px solid var(--border);
|
||
border-radius: 10px;
|
||
}
|
||
.tc-label { font-size: 12px; color: var(--muted); text-transform: uppercase; letter-spacing: .03em; }
|
||
.tc-value { font-size: 17px; font-weight: 700; }
|
||
.tc-grade { font-size: 12px; color: var(--muted); }
|
||
|
||
.table-wrap { overflow-x: auto; }
|
||
table.normals-table { border-collapse: collapse; width: 100%; margin: 8px 0; font-size: 15px; table-layout: fixed; }
|
||
/* Records as color-accented cards: a grid on desktop, single-column (high/low
|
||
stacked vertically) on mobile — no horizontal scroll. */
|
||
.records-grid { display: grid; gap: 12px; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); margin: 14px 0; }
|
||
.record-card { border: 1px solid var(--border); background: var(--surface); border-radius: 12px; padding: 12px 14px; }
|
||
.rc-metric { margin: 0 0 8px; font-size: 16px; }
|
||
.rc-row { display: flex; align-items: baseline; gap: 8px; padding: 6px 0 6px 10px; border-left: 4px solid var(--border); }
|
||
.rc-row + .rc-row { margin-top: 4px; }
|
||
.rc-high { border-left-color: var(--very-hot); }
|
||
.rc-low { border-left-color: var(--very-cold); }
|
||
.rc-tag { font-size: 11px; font-weight: 700; text-transform: uppercase; letter-spacing: .03em; min-width: 62px; flex-shrink: 0; }
|
||
.rc-high .rc-tag { color: var(--very-hot); }
|
||
.rc-low .rc-tag { color: var(--very-cold); }
|
||
.rc-val { font-weight: 700; font-size: 15px; }
|
||
.rc-date { color: var(--muted); font-size: 13px; margin-left: auto; white-space: nowrap; padding-left: 6px; }
|
||
.records-note { font-size: 13px; margin-top: 12px; }
|
||
@media (max-width: 640px) { .records-grid { grid-template-columns: 1fr; } }
|
||
table.normals-table th:first-child, table.normals-table td:first-child { width: 26%; }
|
||
table.normals-table th, table.normals-table td { padding: 8px 12px; text-align: left; border-bottom: 1px solid var(--border); white-space: nowrap; }
|
||
table.normals-table thead th { color: var(--muted); font-weight: 600; font-size: 13px; }
|
||
table.normals-table td a { text-decoration: none; }
|
||
table.normals-table tbody tr:hover { background: var(--surface-2); }
|
||
/* On phones, tighten the climate tables so wide values (3-digit °F with the °C in
|
||
parens) never collide — the records tables are already 3 columns (date folded
|
||
under each value), and this keeps the city's 4-column normals table roomy too. */
|
||
/* Month/season records tables: value+date (and season name+span) stack within a
|
||
cell, so top-align keeps the two lines tidy instead of vertically centred. */
|
||
table.records-table td { vertical-align: top; }
|
||
table.records-table td:first-child { white-space: normal; }
|
||
|
||
@media (max-width: 560px) {
|
||
table.normals-table { font-size: 13px; }
|
||
table.normals-table th, table.normals-table td { padding: 7px 8px; }
|
||
.rec-on { font-size: 10.5px; }
|
||
/* Stack the "(Dec–Feb)" span under the season name so it can't overflow the
|
||
narrow first column and slide under the tinted value cell. */
|
||
.season-span { display: block; }
|
||
}
|
||
|
||
.climate-records ul { margin: 8px 0; padding-left: 20px; }
|
||
.climate-records li { margin: 4px 0; }
|
||
.season-span { color: var(--muted); font-weight: 400; font-size: 13px; }
|
||
|
||
/* Temperature heat-map tints — the interactive tool's diverging cold→hot palette
|
||
brought onto the static tables. The value itself stays in the ink token; the tint
|
||
is a wash behind it, so a record high glows red and a record low reads cold-blue.
|
||
Mix percentages are tuned per tier (saturated darks need less) to keep the number
|
||
legible in both themes; the date column stays muted and untinted. */
|
||
td.t-cell { font-weight: 600; }
|
||
td.rec-date { color: var(--muted); font-size: 13px; }
|
||
/* The date each record occurred, folded under its value so the month/season tables
|
||
stay 3 columns and fit a phone without horizontal scroll. */
|
||
.rec-on { display: block; font-size: 11px; font-weight: 400; color: var(--muted); margin-top: 1px; }
|
||
/* Month/season records: each metric cell carries two extremes — ▲ warmest and
|
||
▼ coldest it has ever been — as tinted chips, each with the date it occurred. */
|
||
.rec-ext { margin: 4px 0; }
|
||
.rec-arrow { display: inline-block; width: 12px; font-size: 10px; }
|
||
.rec-arrow.up { color: var(--very-hot); }
|
||
.rec-arrow.down { color: var(--very-cold); }
|
||
.records-ext .t-inline { font-weight: 700; }
|
||
.records-ext .rec-on { margin: 1px 0 2px 12px; font-size: 10.5px; }
|
||
.t-rec-cold { background: color-mix(in srgb, var(--rec-cold) 26%, var(--surface)); }
|
||
.t-very-cold { background: color-mix(in srgb, var(--very-cold) 26%, var(--surface)); }
|
||
.t-cold { background: color-mix(in srgb, var(--cold) 28%, var(--surface)); }
|
||
.t-cool { background: color-mix(in srgb, var(--cool) 34%, var(--surface)); }
|
||
.t-normal { background: color-mix(in srgb, var(--normal) 26%, var(--surface)); }
|
||
.t-warm { background: color-mix(in srgb, var(--warm) 40%, var(--surface)); }
|
||
.t-hot { background: color-mix(in srgb, var(--hot) 40%, var(--surface)); }
|
||
.t-very-hot { background: color-mix(in srgb, var(--very-hot) 40%, var(--surface)); }
|
||
.t-rec-hot { background: color-mix(in srgb, var(--rec-hot) 30%, var(--surface)); }
|
||
.t-none { background: transparent; }
|
||
/* Inline tinted value (month-page hero numbers, record list) — same washes, padded
|
||
so the colour reads as a chip around the number. */
|
||
.t-inline { padding: 1px 7px; border-radius: 7px; }
|
||
|
||
/* ---- Climate score page ---------------------------------------------------
|
||
Hero (overall score), per-metric cards reuse .normal-card, a by-season chip
|
||
grid, and a button-revealed summary with tinted score tables. All color comes
|
||
from the shared temperature scale via --cat / the .t-* washes. */
|
||
.score-hero {
|
||
background: var(--surface); border: 1px solid var(--border); border-radius: 14px;
|
||
padding: 18px 20px; margin: 4px 0 18px;
|
||
}
|
||
.score-eyebrow { margin: 0 0 8px; } /* else styled by .section-title */
|
||
.score-hero-row { display: flex; align-items: center; gap: 16px; }
|
||
.score-num {
|
||
font-size: 52px; font-weight: 800; line-height: 1;
|
||
color: color-mix(in oklab, var(--cat, var(--text)), var(--text) 15%);
|
||
}
|
||
.score-hero-label { display: flex; flex-direction: column; gap: 4px; }
|
||
.score-grade {
|
||
font-size: 18px; font-weight: 700;
|
||
color: color-mix(in oklab, var(--cat, var(--text)), var(--text) 22%);
|
||
}
|
||
.score-sub { font-size: 13px; color: var(--muted); }
|
||
.score-note { font-size: 12px; color: var(--muted); margin: 12px 0 0; line-height: 1.4; }
|
||
|
||
.score-section { margin: 20px 0; }
|
||
|
||
/* Score metric cards: name · score · tier · detail, each on its own line so a
|
||
long grade never crams into the corner on a narrow (2-up) phone card. */
|
||
.score-card h3 { margin-bottom: 6px; }
|
||
.score-card .nc-tier {
|
||
font-size: 12px; font-weight: 700; margin-top: 3px; line-height: 1.25;
|
||
color: color-mix(in oklab, var(--cat, var(--muted)), var(--text) 22%);
|
||
}
|
||
.score-card .range { margin-top: 1px; }
|
||
|
||
/* Wet-bulb explainer + heat-stress-day share. */
|
||
.score-callout {
|
||
background: var(--surface-2); border: 1px solid var(--border); border-radius: 12px;
|
||
padding: 12px 14px; margin: 14px 0 4px; font-size: 13px; line-height: 1.5; color: var(--text);
|
||
}
|
||
|
||
/* By-season scores: seasons as rows, one uniform column per metric so every
|
||
metric lines up vertically across the seasons. Fixed layout keeps the columns
|
||
equal width; the min-width lets it scroll (not squash) on a phone. */
|
||
.season-table { table-layout: fixed; width: 100%; min-width: 620px; max-width: 740px; }
|
||
.season-table th:first-child, .season-table td:first-child { width: 62px; text-align: left; }
|
||
|
||
/* Summary reveal — pill button + caret, mirrors the season-expand pattern. */
|
||
.summary-toggle {
|
||
display: inline-flex; align-items: center; gap: 6px; min-height: 44px;
|
||
margin: 8px 0 4px; padding: 8px 16px; border: 1px solid var(--border);
|
||
border-radius: 10px; background: var(--surface); color: var(--text);
|
||
font-size: 15px; font-weight: 600; cursor: pointer;
|
||
}
|
||
.summary-toggle:hover { border-color: var(--accent); }
|
||
.summary-caret { transition: transform .15s ease; }
|
||
.summary-toggle[aria-expanded="true"] { border-color: var(--accent); }
|
||
.summary-toggle[aria-expanded="true"] .summary-caret { transform: rotate(180deg); }
|
||
|
||
#score-summary { margin-top: 14px; }
|
||
.score-summary-lead { font-size: 14px; color: var(--text); margin: 6px 0; }
|
||
.score-sentences { margin: 8px 0 18px; padding-left: 20px; }
|
||
.score-sentences li { font-size: 14px; line-height: 1.5; margin-bottom: 8px; color: var(--text); }
|
||
|
||
#score-body .table-wrap { margin: 8px 0 20px; } /* .table-wrap supplies overflow-x */
|
||
.score-table { border-collapse: collapse; font-size: 13px; }
|
||
.score-table th, .score-table td {
|
||
padding: 6px 10px; border-bottom: 1px solid var(--border); text-align: center; white-space: nowrap;
|
||
}
|
||
.score-table thead th { color: var(--muted); font-weight: 600; }
|
||
.score-table tbody th { text-align: left; font-weight: 600; color: var(--text); }
|
||
.score-table-overall th, .score-table-overall td {
|
||
border-top: 2px solid var(--border); font-weight: 800;
|
||
}
|
||
.pq-table td { text-align: left; font-size: 12px; color: var(--muted); }
|
||
.pq-d { font-weight: 700; }
|
||
.pq-up { color: color-mix(in oklab, var(--hot), var(--text) 30%); }
|
||
.pq-down { color: color-mix(in oklab, var(--cool), var(--text) 30%); }
|
||
|
||
@media (max-width: 640px) {
|
||
.score-num { font-size: 40px; }
|
||
.score-hero { padding: 14px 15px; }
|
||
}
|
||
|
||
/* Monthly temperature-range strip: one gradient bar per month, low→high on a shared
|
||
axis. The signature climate visual — colourful and unmistakably Thermograph. */
|
||
.range-strip { display: grid; gap: 6px; margin: 16px 0 8px; max-width: 620px; }
|
||
.range-row { display: grid; grid-template-columns: 34px 1fr 92px; align-items: center; gap: 10px; }
|
||
.range-month { font-size: 13px; font-weight: 600; color: var(--muted); }
|
||
.range-track { position: relative; height: 14px; background: var(--surface-2); border-radius: 999px; }
|
||
.range-fill {
|
||
position: absolute; top: 0; bottom: 0; border-radius: 999px;
|
||
background: linear-gradient(90deg, var(--c1), var(--c2));
|
||
}
|
||
.range-vals { font-size: 13px; color: var(--text); font-variant-numeric: tabular-nums; text-align: right; white-space: nowrap; }
|
||
.range-note { font-size: 13px; margin-top: 6px; }
|
||
/* Phone: cap the strip so bars + values pack into a tight group on the left
|
||
instead of stretching edge-to-edge, and shrink the values to fit (no reserved
|
||
92px column). Placed after the base rules above so it actually wins. */
|
||
@media (max-width: 560px) {
|
||
.range-strip { max-width: 300px; }
|
||
.range-row { grid-template-columns: 26px 1fr auto; gap: 8px; }
|
||
.range-month { font-size: 12px; }
|
||
.range-vals { font-size: 12px; }
|
||
}
|
||
.climate-foot { font-size: 14px; margin-top: 28px; border-top: 1px solid var(--border); padding-top: 16px; }
|
||
.city-blurb { font-size: 15px; margin: 4px 0 14px; }
|
||
.blurb-src { color: var(--muted); text-decoration: none; font-size: 13px; white-space: nowrap; }
|
||
.blurb-src:hover { color: var(--accent); }
|
||
.city-event { border-left: 4px solid var(--accent); background: var(--surface); border-radius: 0 10px 10px 0; padding: 12px 18px; margin: 22px 0; }
|
||
.city-event h2 { margin: 2px 0 6px; font-size: 18px; }
|
||
.city-event p { margin: 0; }
|
||
.city-travel { background: var(--surface); border: 1px solid var(--border); border-radius: 12px; padding: 14px 18px; margin: 28px 0; }
|
||
.city-travel h2 { margin-top: 4px; }
|
||
/* Month page: the "visiting in <month>?" prompt as a compact callout — text on
|
||
the left, a CTA button on the right (stacks on narrow screens). */
|
||
.travel-note {
|
||
display: flex; flex-wrap: wrap; align-items: center; gap: 10px 18px;
|
||
background: var(--surface); border: 1px solid var(--border); border-radius: 12px;
|
||
padding: 14px 18px; margin: 22px 0;
|
||
}
|
||
.travel-note p { margin: 0; flex: 1 1 240px; }
|
||
.travel-note .cta { flex-shrink: 0; margin-top: 0; }
|
||
.travel-note { font-size: 15px; }
|
||
|
||
/* Hub / directory of cities */
|
||
/* Collapsible country sections (native <details>). */
|
||
/* Hub search box + flat alphabetical results. */
|
||
.hub-search { margin: 16px 0 4px; }
|
||
.hub-search input {
|
||
width: 100%; padding: 12px 14px; border-radius: 10px; border: 1px solid var(--border);
|
||
background: var(--bg); color: var(--text); font-size: 16px;
|
||
}
|
||
.hub-search input:focus { outline: none; border-color: var(--accent); }
|
||
.hub-results { list-style: none; margin: 8px 0; padding: 0; }
|
||
.hub-results[hidden] { display: none; }
|
||
.hub-results li { padding: 11px 4px; border-bottom: 1px solid var(--border); display: flex; align-items: baseline; gap: 12px; }
|
||
.hub-results li a { text-decoration: none; font-weight: 600; }
|
||
.hub-r-country { color: var(--muted); font-size: 13px; margin-left: auto; text-align: right; white-space: nowrap; }
|
||
.hub-results .hub-empty { color: var(--muted); }
|
||
#hub-directory[hidden] { display: none; }
|
||
|
||
details.hub-country { border-bottom: 1px solid var(--border); }
|
||
details.hub-country > summary {
|
||
cursor: pointer; padding: 12px 4px; font-weight: 700; font-size: 16px;
|
||
list-style: none; display: flex; align-items: center; gap: 8px;
|
||
}
|
||
details.hub-country > summary::-webkit-details-marker { display: none; }
|
||
details.hub-country > summary::before {
|
||
content: "\25B8"; color: var(--muted); transition: transform .15s ease; display: inline-block;
|
||
}
|
||
details.hub-country[open] > summary::before { transform: rotate(90deg); }
|
||
details.hub-country > summary:hover { color: var(--accent); }
|
||
.hub-count { margin-left: auto; color: var(--muted); font-weight: 500; font-size: 13px; }
|
||
.city-links { display: flex; flex-wrap: wrap; gap: 8px 16px; margin: 0; padding: 2px 4px 16px 22px; list-style: none; }
|
||
.city-links a { text-decoration: none; }
|
||
|
||
/* Glossary / about editorial */
|
||
.glossary-term { border-bottom: 1px solid var(--border); padding: 16px 0; }
|
||
.glossary-term h2 { margin: 0 0 6px; }
|
||
|
||
.site-footer {
|
||
max-width: 880px; margin: 0 auto; padding: 24px 20px 48px; border-top: 1px solid var(--border);
|
||
color: var(--muted); font-size: 14px;
|
||
}
|
||
.site-footer nav { display: flex; flex-wrap: wrap; gap: 8px 20px; margin-bottom: 10px; }
|
||
.site-footer a { color: var(--accent); text-decoration: none; }
|
||
.site-footer p { margin: 0; }
|
||
|
||
@media (max-width: 640px) {
|
||
main.content { padding: 8px 14px 40px; }
|
||
.today-cards { grid-template-columns: repeat(auto-fill, minmax(130px, 1fr)); }
|
||
}
|
||
|
||
/* ============================================================
|
||
Homepage distribution surfaces: hero, records strip, how it
|
||
works, explore cards, city chips, digest form. Mobile-first;
|
||
the wide-monitor steps at the top of this file widen `main`,
|
||
and these grids flow into whatever room that gives them.
|
||
============================================================ */
|
||
|
||
/* --- hero ---------------------------------------------------------------- */
|
||
.hero { display: grid; gap: 18px; margin: 8px 0 22px; }
|
||
.hero h1 { font-size: clamp(28px, 6vw, 44px); line-height: 1.1; letter-spacing: -0.03em; }
|
||
.hero-sub { margin: 10px 0 0; color: var(--muted); font-size: clamp(15px, 2.4vw, 19px); max-width: 46ch; }
|
||
|
||
/* The grade card is the LCP element. Its three text slots have reserved
|
||
heights so hydrating the live numbers can't shift the page. */
|
||
.hero-card { display: grid; gap: 12px; justify-items: start; }
|
||
.grade-card {
|
||
background: var(--surface-2);
|
||
border: 1px solid var(--border);
|
||
border-left: 4px solid var(--cat, var(--border));
|
||
border-radius: 11px;
|
||
padding: 16px 18px;
|
||
width: 100%;
|
||
max-width: 460px;
|
||
}
|
||
.grade-where { margin: 0; color: var(--muted); font-size: 13px; min-height: 18px; }
|
||
.grade-band {
|
||
margin: 4px 0 0;
|
||
font-size: clamp(26px, 5vw, 34px);
|
||
font-weight: 700;
|
||
letter-spacing: -0.02em;
|
||
min-height: 40px;
|
||
color: color-mix(in oklab, var(--cat, var(--text)) 78%, var(--text));
|
||
}
|
||
.grade-detail { margin: 6px 0 0; font-size: 15px; min-height: 22px; }
|
||
.grade-why { margin: 6px 0 0; font-size: 12px; }
|
||
|
||
.hero-actions { display: flex; flex-wrap: wrap; gap: 10px; }
|
||
.hero-actions button {
|
||
min-height: 44px; padding: 11px 18px; border-radius: 10px;
|
||
font-size: 16px; font-weight: 600; cursor: pointer;
|
||
}
|
||
.btn-primary { background: var(--accent); color: #1a1206; border: 1px solid var(--accent); }
|
||
.btn-ghost { background: var(--surface); color: var(--text); border: 1px solid var(--border); }
|
||
.btn-ghost:hover, .btn-primary:hover { border-color: var(--accent); }
|
||
.hero-locate-msg { margin: 0; font-size: 13px; color: var(--muted); max-width: 42ch; }
|
||
.hero-jump { margin: 0; font-size: 14px; }
|
||
.hero-jump a { color: var(--muted); text-decoration: none; }
|
||
.hero-jump a:hover { color: var(--accent); }
|
||
|
||
/* Desktop: copy beside the card rather than stacked above it. */
|
||
@media (min-width: 900px) {
|
||
.hero { grid-template-columns: 1.1fr 1fr; align-items: center; gap: 40px; }
|
||
}
|
||
|
||
.stance-line { margin: 14px 0 28px; font-size: 13px; }
|
||
.stance-line a { color: var(--muted); }
|
||
.stance-line a:hover { color: var(--accent); }
|
||
|
||
/* --- unusual right now --------------------------------------------------- */
|
||
.unusual-strip { margin: 32px 0; }
|
||
.unusual-strip h2, .how-it-works h2, .explore h2, .city-hub h2 {
|
||
font-size: 12px; text-transform: uppercase; letter-spacing: .05em;
|
||
color: var(--muted); margin: 0 0 12px;
|
||
}
|
||
/* Scroll-snap, not a JS carousel: keyboard- and touch-scrollable for free. */
|
||
.unusual-row {
|
||
display: flex; gap: 10px; overflow-x: auto; padding: 2px 2px 10px;
|
||
margin: 0; list-style: none; scroll-snap-type: x proximity;
|
||
}
|
||
.unusual-card { scroll-snap-align: start; flex: 0 0 auto; }
|
||
.unusual-card a {
|
||
display: grid; gap: 2px; min-width: 190px; padding: 12px 14px;
|
||
/* A wash of the band colour, so the card reads as its grade at a glance and
|
||
the strip isn't a row of identical grey boxes. */
|
||
background: color-mix(in oklab, var(--cat, var(--surface)) 10%, var(--surface));
|
||
border: 1px solid color-mix(in oklab, var(--cat, var(--border)) 25%, var(--border));
|
||
border-left: 4px solid var(--cat, var(--border));
|
||
border-radius: 11px; text-decoration: none; color: var(--text);
|
||
}
|
||
.unusual-card a:hover { border-color: var(--accent); }
|
||
.unusual-city { font-weight: 600; font-size: 14px; }
|
||
/* The reading itself leads: a percentile with no value attached says nothing
|
||
about what the weather actually is. */
|
||
/* 50% is the most band colour these can carry and still clear 4.5:1 against the
|
||
tinted card in BOTH schemes — measured across the ramp, worst case being the
|
||
deliberately-dark --rec-cold/--rec-hot "Near Record" tiers. Raising it fails
|
||
dark mode; the band word carries the meaning anyway, colour only reinforces. */
|
||
.unusual-value { font-size: 24px; font-weight: 700; letter-spacing: -0.02em; line-height: 1.15;
|
||
color: color-mix(in oklab, var(--cat, var(--text)) 50%, var(--text)); }
|
||
.unusual-band { font-size: 13px; font-weight: 600;
|
||
color: color-mix(in oklab, var(--cat, var(--text)) 50%, var(--text)); }
|
||
.unusual-meta { font-size: 12px; color: var(--muted); }
|
||
.unusual-more { margin: 2px 0 0; font-size: 14px; }
|
||
.unusual-more a { color: var(--accent); text-decoration: none; }
|
||
|
||
/* --- how it works -------------------------------------------------------- */
|
||
.how-it-works { margin: 32px 0; }
|
||
.how-steps { margin: 0 0 10px; padding-left: 22px; display: grid; gap: 8px; }
|
||
.how-steps li { line-height: 1.45; }
|
||
.how-it-works a { color: var(--accent); }
|
||
|
||
/* --- explore ------------------------------------------------------------- */
|
||
.explore { margin: 32px 0; }
|
||
.explore-cards { display: grid; gap: 10px; }
|
||
@media (min-width: 700px) { .explore-cards { grid-template-columns: repeat(2, 1fr); } }
|
||
@media (min-width: 1400px) { .explore-cards { grid-template-columns: repeat(4, 1fr); } }
|
||
.explore-card {
|
||
display: grid; gap: 4px; padding: 14px 16px;
|
||
/* Each card is tinted by its own token so the row carries the product's
|
||
palette instead of reading as four identical grey boxes. */
|
||
background: color-mix(in oklab, var(--cat, var(--surface)) 9%, var(--surface));
|
||
border: 1px solid color-mix(in oklab, var(--cat, var(--border)) 28%, var(--border));
|
||
border-left: 4px solid var(--cat, var(--border));
|
||
border-radius: 11px; text-decoration: none; color: var(--text);
|
||
}
|
||
.explore-card:hover { border-color: var(--accent); }
|
||
.explore-name { font-weight: 600;
|
||
color: color-mix(in oklab, var(--cat, var(--text)) 45%, var(--text)); }
|
||
/* The nine grade tiers in order — a miniature of the calendar itself. */
|
||
.explore-ramp { display: flex; gap: 2px; margin: 2px 0 1px; }
|
||
.explore-ramp i { flex: 1 1 0; height: 6px; border-radius: 2px; }
|
||
.explore-desc { color: var(--muted); font-size: 14px; line-height: 1.4; }
|
||
|
||
/* --- city chips ---------------------------------------------------------- */
|
||
.city-hub { margin: 32px 0; }
|
||
.city-chips { display: flex; flex-wrap: wrap; gap: 8px; margin: 0 0 12px; padding: 0; list-style: none; }
|
||
.city-chips a {
|
||
display: inline-block; padding: 8px 13px; border-radius: 999px;
|
||
background: var(--surface); border: 1px solid var(--border);
|
||
color: var(--text); text-decoration: none; font-size: 14px;
|
||
}
|
||
.city-chips a:hover {
|
||
border-color: var(--accent);
|
||
background: color-mix(in oklab, var(--accent) 12%, var(--surface));
|
||
}
|
||
.city-hub > p a { color: var(--accent); text-decoration: none; }
|
||
|
||
/* --- digest form (site-wide footer pattern) ------------------------------ */
|
||
.digest-form { display: grid; gap: 8px; margin: 0 0 20px; max-width: 480px; }
|
||
.digest-form label { font-size: 14px; font-weight: 600; }
|
||
.digest-row { display: flex; flex-wrap: wrap; gap: 8px; }
|
||
.digest-row input {
|
||
flex: 1 1 200px; min-height: 44px; padding: 10px 12px;
|
||
background: var(--surface); border: 1px solid var(--border);
|
||
border-radius: 10px; color: var(--text);
|
||
font-size: 16px; /* hard minimum: smaller makes iOS zoom on focus */
|
||
}
|
||
.digest-row input:focus { outline: 2px solid var(--accent); outline-offset: 1px; }
|
||
.digest-row button {
|
||
min-height: 44px; padding: 10px 18px; border-radius: 10px;
|
||
background: var(--accent); color: #1a1206; border: 1px solid var(--accent);
|
||
font-size: 16px; font-weight: 600; cursor: pointer;
|
||
}
|
||
.digest-note { margin: 0; font-size: 12px; }
|
||
.digest-status { margin: 0; font-size: 14px; color: var(--accent); }
|
||
|
||
/* iOS "Add to Home Screen" hint (frontend/ios-install.js) — a slim dismissible
|
||
card pinned above the home indicator, shown only to iOS Safari visitors who
|
||
haven't installed yet. Themed like the rest of the surface chrome. */
|
||
.ios-a2hs {
|
||
position: fixed; z-index: 950;
|
||
left: 50%; transform: translateX(-50%);
|
||
bottom: calc(12px + env(safe-area-inset-bottom));
|
||
width: min(440px, calc(100vw - 24px));
|
||
display: flex; align-items: center; gap: 12px;
|
||
padding: 12px 14px;
|
||
background: var(--surface); color: var(--text);
|
||
border: 1px solid var(--border); border-radius: 14px;
|
||
box-shadow: 0 8px 30px rgba(0, 0, 0, .35);
|
||
animation: ios-a2hs-in .25s ease-out;
|
||
}
|
||
@keyframes ios-a2hs-in { from { opacity: 0; transform: translate(-50%, 12px); } }
|
||
.ios-a2hs-share { width: 26px; height: 26px; flex: 0 0 auto; color: var(--accent); }
|
||
.ios-a2hs-text { display: flex; flex-direction: column; gap: 2px; font-size: 13px; line-height: 1.3; }
|
||
.ios-a2hs-text b { font-weight: 700; }
|
||
.ios-a2hs-text span { color: var(--muted); }
|
||
.ios-a2hs-x {
|
||
flex: 0 0 auto; margin-left: auto;
|
||
width: 32px; height: 32px; border-radius: 8px;
|
||
background: none; border: none; color: var(--muted);
|
||
font-size: 22px; line-height: 1; cursor: pointer;
|
||
}
|
||
.ios-a2hs-x:hover { color: var(--text); background: var(--surface-2); }
|