diff --git a/assets/css/table.css b/assets/css/table.css index 29a9941..c61604b 100644 --- a/assets/css/table.css +++ b/assets/css/table.css @@ -753,11 +753,14 @@ outline: none; } -/* Breathing room between each "PROFILE" / "VIEWING" label and its value. */ +/* Each "PROFILE" / "VIEWING" label sits on its own line, with its value + stacked underneath. */ .active-profile-row-item { display: inline-flex; - align-items: baseline; - gap: 7px; + flex-direction: column; + align-items: flex-start; + gap: 1px; + line-height: 1.2; } .active-profile-row-label { font-family: Manrope, sans-serif; @@ -776,28 +779,42 @@ color: #1e1208; } .active-profile-row-sep { color: #c0a880; font-size: 16px; } +/* Small scene picture of the active profile — same image as its card in + the profile picker, so the row shows the choice, not just names it. */ +.active-profile-row-thumb { + flex-shrink: 0; + width: 40px; + height: 40px; + border-radius: 8px; + box-shadow: 0 1px 4px rgba(0,0,0,0.18); +} .active-profile-row-edit { display: inline-flex; align-items: center; gap: 5px; font-family: Manrope, sans-serif; - font-size: 12.5px; - font-weight: 700; + font-size: 13.5px; + font-weight: 800; letter-spacing: 0.04em; - color: #9a6a1a; - margin-left: 8px; - padding: 4px 10px; - background: rgba(200,146,42,0.12); - border: 1px solid rgba(200,146,42,0.35); - border-radius: 999px; - transition: background 0.2s, border-color 0.2s, color 0.2s, transform 0.2s; + /* Solid fill in the row's own accent (--row-accent, set per thermal-model + group below) — reads as a real button, and always matches the value + text colour however the row's background changes. */ + color: #fffcf2; + /* Same 40px height, 8px rounding and row gap as the thumbnail on the + left, so the two bookend the row symmetrically. */ + box-sizing: border-box; + height: 40px; + padding: 0 16px; + background: var(--row-accent, #9a6a1a); + border: none; + border-radius: 8px; + box-shadow: 0 2px 6px rgba(0,0,0,0.18); + transition: filter 0.2s, box-shadow 0.2s; } .active-profile-row:hover .active-profile-row-edit, .active-profile-row:focus-visible .active-profile-row-edit { - color: #c8922a; - background: rgba(200,146,42,0.2); - border-color: #c8922a; - transform: translateY(-1px); + filter: brightness(1.15); + box-shadow: 0 4px 10px rgba(0,0,0,0.22); } /* Border + text accent per thermal-model group, reusing the same colour @@ -812,6 +829,13 @@ .active-profile-row.grp-surface { border-color: rgba(11,62,114,0.30); } .active-profile-row.grp-pets { border-color: rgba(10,81,88,0.30); } +/* Same accents as a variable, for the Change button's fill. */ +.active-profile-row.grp-felt { --row-accent: #075c3a; } +.active-profile-row.grp-wind { --row-accent: #6a2210; } +.active-profile-row.grp-ambient { --row-accent: #3c3a35; } +.active-profile-row.grp-surface { --row-accent: #0b3e72; } +.active-profile-row.grp-pets { --row-accent: #0a5158; } + .active-profile-row.grp-felt .active-profile-row-value { color: #075c3a; } .active-profile-row.grp-wind .active-profile-row-value { color: #6a2210; } .active-profile-row.grp-ambient .active-profile-row-value { color: #3c3a35; } @@ -1926,6 +1950,21 @@ gap: 7px; } +/* Cycling hazard marker beside the hour (time cell + forecast cards). + Text-style ⚠ so it can take an amber "care" or red "risky" colour. */ +.bike-risk-tag { + font-size: 14px; + font-weight: 700; + line-height: 1; + cursor: help; +} +.fsc-time .bike-risk-tag { + margin-left: 3px; + font-size: 13px; +} +.bike-risk-tag--caution { color: #c98a12; } +.bike-risk-tag--danger { color: #c0392b; } + @media (max-width: 800px) { .utci-time { font-size: 15px; @@ -2354,9 +2393,11 @@ /* ── SIMPLE / TABLE VIEW TOGGLE ─────────────────────────────────────────── */ +/* View label and segments are sized up from the 11px toolbar default - + switching view is one of the main features, so it should read as one. */ .forecast-view-toggle-label { font-family: Manrope, sans-serif; - font-size: 11px; + font-size: 13px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.07em; @@ -2373,12 +2414,12 @@ .fvt-btn { display: inline-flex; align-items: center; - gap: 3px; + gap: 5px; font-family: Manrope, sans-serif; - font-size: 11px; + font-size: 13px; font-weight: 700; line-height: 1; - padding: 6px 8px; + padding: 8px 12px; cursor: pointer; border: 1.5px solid #c9b08a; background: #f5edd6; @@ -2391,7 +2432,7 @@ /* Only the outer corners are rounded; every segment after the first drops its left border so neighbours share one 1.5px rule rather than stacking two. Keyed off the sibling rather than :last-child so it still holds with - three segments (Quick / Detailed / Table). */ + three segments (Simple / Detailed / Table). */ .fvt-btn + .fvt-btn { border-left: none; } @@ -2410,6 +2451,8 @@ } .fvt-btn-icon { + width: 14px; /* up from the 12px set on the svg, to match the larger text */ + height: 14px; display: inline-block; vertical-align: middle; flex-shrink: 0; @@ -2463,6 +2506,11 @@ grid-row: 1; justify-self: center; padding-bottom: 0; + /* View toggle + Edit columns no longer fit one 375px line at the + larger toggle size - let Edit columns drop beneath. */ + flex-wrap: wrap; + justify-content: center; + row-gap: 6px; } /* Quick view needs the row flush so its thermal tabs attach to the box below. Detailed and Table have no tabs, so the View buttons would @@ -2892,10 +2940,12 @@ /* ── SPACING ────────────────────────────────────────────────────────── Cells carry icons alongside their numbers (sky scope, cloud, precip, wind vane), so they need noticeably more room than a bare figure. */ +/* Kept tight so more hours fit across — the cells were reading as wide, + airy tiles at 11px/16px with a 92px floor. */ .utci-table-rotated td, .utci-table-rotated th { - padding: 11px 16px; - min-width: 92px; + padding: 7px 8px; + min-width: 64px; white-space: nowrap; } @@ -2915,7 +2965,39 @@ text-transform: none; letter-spacing: 0; color: #1e1208; - min-width: 76px; + min-width: 60px; +} + +/* Hour row on white rather than the table-head cream — reads as a clean + time axis over the shaded data. Over-qualified to beat `.utci-table th`. + The hairline keeps it separated from the first metric row. */ +.utci-table.utci-table-rotated thead th.rot-hour-th, +.utci-table.utci-table-rotated thead th.rot-corner { + background: #ffffff; + border-bottom: 1px solid rgba(74, 50, 24, 0.06); +} +.utci-table.utci-table-rotated thead th.rot-hour-th.is-now { + background: #fff8e4; +} + +/* Gust value in the wind cell — inline after the speed in Table view, + stacked underneath it in Detailed so the hour columns stay narrow. */ +.wind-gust { + margin-left: 4px; +} +.utci-table-rotated .wind-gust { + display: block; + margin-left: 0; + font-size: 0.85em; + line-height: 1.2; +} + +/* The shared sticky strip draws a heavy 2px tan rule + drop shadow under + the head — right for the cream Table header, too dark under the white + hour row. Lighten it here only; the Table view keeps its own. */ +.utci-table-wrap.is-rotated .utci-thead-sticky { + border-bottom: 1px solid rgba(74, 50, 24, 0.10); + box-shadow: none; } /* Top-aligned so every scope sits on the same line. The header row is as @@ -2923,6 +3005,9 @@ would push the emptier hours down out of step with it. */ .utci-table-rotated thead th.rot-hour-th { vertical-align: top; + /* Extra top padding to balance the space below the time label, which + its serif line-height already pads out. */ + padding-top: 12px; } /* Stacked, not inline: rotated, every hour is its own COLUMN, so sitting @@ -3037,6 +3122,14 @@ box-shadow: 1px 0 0 rgba(74, 50, 24, 0.18); } +/* Name and unit on ONE line — stacked, the unit doubled every row's + height. Same colour/size as before, just inline after the name. */ +.utci-table-rotated th.rot-metric-label .col-unit, +.utci-table-rotated th.rot-metric-label .col-env-badge { + display: inline; + margin: 0 0 0 5px; +} + /* The corner cell sits in both sticky tracks, so it needs to win over each of them. */ /* The corner pins alongside the metric names below it. Over-qualified so @@ -3050,7 +3143,7 @@ translated track, not in the scroller. */ position: relative; z-index: 4; - background: #f0e4c4; + background: #f0e4c4; /* keeps the cream of the metric-name column below */ text-align: left; box-shadow: 1px 0 0 rgba(74, 50, 24, 0.18); } @@ -3093,8 +3186,9 @@ /* Tighter segments on narrow screens — the toolbar is already tight, and there are three of them now. */ .fvt-btn { - padding: 6px 6px; - gap: 2px; + font-size: 12px; + padding: 7px 8px; + gap: 4px; } } @@ -3132,7 +3226,7 @@ it regardless of font-size differences. */ .fvt-btn, .col-toggles-edit-btn { - min-height: 29px; + min-height: 34px; line-height: 1; } @@ -3143,12 +3237,12 @@ } .utci-table-rotated td, .utci-table-rotated th { - padding: 9px 11px; - min-width: 80px; + padding: 6px 6px; + min-width: 56px; } .utci-table-rotated .rot-hour-th { font-size: 14px; - min-width: 68px; + min-width: 52px; } .utci-table-rotated .rot-metric-label { min-width: 96px; diff --git a/assets/js/app.js b/assets/js/app.js index d679a6a..8f672ba 100644 --- a/assets/js/app.js +++ b/assets/js/app.js @@ -739,6 +739,53 @@ export function UTCIForecast() { // I drive / when is the house bearable", not always "when is it nice out". const weekAhead = computeBestDay(days, nowLocalISO, activeProfile, outdoorsVariant); + // Cycling: a ⚠ beside the hour when it is risky (red) or needs care (amber) + // on a bike - see cyclingRisk() in compute.js. Shared by the forecast + // cards and both table orientations. + const isCycling = activeProfile === 'outdoors' && outdoorsVariant === 'cycling'; + + // One line of advice per hazard, keyed by the start of its cyclingRisk() text. + const BIKE_ADVICE = [ + ['Gusts', 'sudden gusts can shove you across the lane'], + ['Wind', 'strong wind makes steering and climbs hard work'], + ['Heavy rain', 'spray and longer braking distances'], + ['Snow', 'snow on the road cuts grip'], + ['Ice likely', 'icy patches are likely, especially on bridges and shaded lanes'], + ['Possible ice', 'watch for icy patches on bridges and shaded lanes'], + ['Fog', 'drivers may not see you, so run lights'], + ['Lightning', 'thunderstorms about, so stay near shelter'], + ]; + + // The hour's hazard as an event-tag event, so the ⚠ opens the same animated + // popup (with Quick Record) as the other cell emojis. null when no hazard. + const bikeRiskEvent = (r) => { + if (!isCycling || !(r.bikeRisk >= 1)) return null; + const why = r.bikeRiskWhy ?? []; + const advice = why + .map(w => BIKE_ADVICE.find(([k]) => w.startsWith(k))?.[1]) + .filter(Boolean); + const lead = advice.join('; '); + return { + id: `bike-risk-${r.iso}`, + emoji: '⚠️', + title: r.bikeRisk >= 2 ? 'Risky to ride' : 'Ride with care', + message: `${why.join(', ')}${lead ? ` — ${lead.charAt(0).toUpperCase()}${lead.slice(1)}.` : ''}`, + }; + }; + + // The ⚠ tag itself. tagEvents is the hour's full tag list with the hazard + // first, so the popup can crossfade on to the hour's other events. + const bikeRiskTag = (r, tagEvents) => { + const ev = tagEvents[0]; + if (!ev || !ev.id.startsWith('bike-risk-')) return null; + const date = r.iso.slice(0, 10); + return html`= 2 ? 'danger' : 'caution'}`} + onClick=${(e) => handleEventTagClick(tagEvents, 0, date, e)} + onMouseEnter=${(e) => handleEventTagEnter(tagEvents, 0, date, e)} + onMouseLeave=${handleEventTagLeave} + role="button" tabIndex="0" aria-label=${ev.title}>⚠︎`; + }; + // Human-readable date for the "Day at a glance" heading, e.g. "Mon 2nd June 2026". const glanceDate = (() => { const key = days[selectedDay]?.key; @@ -865,6 +912,12 @@ export function UTCIForecast() { hourAt: j2 => (bucket + 2 * j2 + 1) / total, // grid-column for the card whose landing hour is j (lines are 1-based). cardCol: j2 => `${2 * j2 + 2} / span ${2 * bucket}`, + // Range mode (multi-hour cards over the hourly curve): the curve shows a + // band across each card's hours, so the card is centred over that whole + // block instead of its landing hour. Hour j occupies tracks + // bucket+2j+1 and bucket+2j+2, so a block of `span` hours from j is: + rangeMode: bucket > 1 && hourly !== tableRows, + blockCol: (j2, span) => `${bucket + 2 * j2 + 1} / span ${2 * span}`, }; })(); @@ -1423,18 +1476,18 @@ export function UTCIForecast() {
View: - ${/* One three-way control. Quick is the card layout; the other + ${/* One three-way control. Simple is the card layout; the other two are the same hourly table on opposite axes — Detailed runs hours along the top, Table runs them down the side. Rotation used to be a separate toggle that only appeared once you were already in the table, which hid the layout people wanted behind a mode they had to find first. */''} -