/* ════════════════════════════════════════════════════════════════════════
table.css — Day tabs, filter profiles, column toggles, hourly table,
now-row and night-row decorations.
════════════════════════════════════════════════════════════════════════ */
/* ── 5. DAY TABS (today / tomorrow / etc.) ─────────────────────────── */
/* Note: each tab's background colour comes from inline styles in
app.js (the confidence bands). This file only handles spacing,
typography, and the active-tab underline. */
/* Wrapper that hosts the scrollable strip, edge fades, and chevrons. */
.utci-day-tabs-wrap {
position: relative;
display: flex;
margin-top: 12px;
margin-bottom: 15px;
border: 1px solid #c9b08a;
/* Width of the fixed metric-label column; the left fade and left chevron
both offset by this so they never sit on top of it. */
--day-legend-w: 72px;
}
/* Edge fade gradients — only show when there's content to scroll to. */
.utci-day-tabs-wrap::before,
.utci-day-tabs-wrap::after {
content: '';
position: absolute;
top: 0;
bottom: 2px; /* leave the underline border visible */
width: 40px;
pointer-events: none;
z-index: 1;
opacity: 0;
transition: opacity 0.2s;
}
.utci-day-tabs-wrap::before {
left: var(--day-legend-w);
background: linear-gradient(to right, #f5edd6 0%, rgba(245,237,214,0) 100%);
}
.utci-day-tabs-wrap::after {
right: 0;
background: linear-gradient(to left, #f5edd6 0%, rgba(245,237,214,0) 100%);
}
.utci-day-tabs-wrap.fade-left::before { opacity: 1; }
.utci-day-tabs-wrap.fade-right::after { opacity: 1; }
/* Chevron buttons — sit on top of the fades. */
.utci-day-scroll {
position: absolute;
top: 50%;
transform: translateY(-50%);
width: 30px;
height: 30px;
border-radius: 50%;
border: 1px solid #d4c0a0;
background: #fffcf2;
color: #6b4f2a;
font-size: 20px;
font-family: Fraunces, serif;
line-height: 1;
cursor: pointer;
z-index: 2;
display: flex;
align-items: center;
justify-content: center;
padding: 0 0 2px 0;
box-shadow: 0 1px 4px rgba(80,55,20,0.12);
transition: opacity 0.2s, background 0.15s, transform 0.15s;
}
.utci-day-scroll:hover {
background: #fef3dc;
}
.utci-day-scroll.left { left: calc(var(--day-legend-w) - 2px); }
.utci-day-scroll.right { right: -2px; }
.utci-day-scroll.hidden {
opacity: 0;
pointer-events: none;
}
/* Fixed first column naming every stacked row in each tab — day name, date,
the weather/status icon, and the two numbers (which metrics those are
changes with the active profile: SunSoak/Shade, Indoors/Managed, Fur/Shade,
Vehicle/Windows Open) — so without this column the tabs read as unlabelled
numbers. Sits outside .utci-day-tabs so it never scrolls away.
Alignment: every label is absolutely positioned at a fixed offset from the
top of the column, matching the corresponding row inside .utci-day-tab.
Taking the labels out of normal flow is what makes this robust across
profiles: they no longer stack against each other, so a label that wraps
to two lines ("Managed Indoors") can't push the rows below it, and a
shorter one ("Shade Air") can't pull them up. Previously every label's
position depended on the height of the ones above it, so alignment tuned
against one profile drifted on the others.
The offsets below are measured from the tab's own stack:
1px border + 10px padding → day 16px → +3px date 16px → +6px icon 30px
→ +2px values
The legend column has no top border of its own, hence the leading 11px. */
.utci-day-legend-col {
position: relative;
flex: 0 0 var(--day-legend-w);
padding: 0 6px 0 0;
background: #fdf8ee;
text-align: right;
font-family: Manrope, sans-serif;
text-transform: uppercase;
letter-spacing: 0.06em;
overflow-wrap: break-word;
}
/* Shared anchoring for every absolutely-positioned label row. `left: 0` +
`right` (rather than a width) keeps them right-aligned against the column's
padding while still allowing long labels to wrap leftwards. */
.utci-day-legend-day,
.utci-day-legend-date,
.utci-day-legend-icon,
.utci-day-legend-values {
position: absolute;
left: 0;
right: 6px;
}
.utci-day-legend-day {
top: 11px;
height: 16px;
/* Explicit line-height (not the browser default "normal") so the row's
height is identical in Chrome and Firefox — those engines compute
"normal" differently for the same font and size. */
line-height: 16px;
font-size: 8px;
font-weight: 700;
color: #9a7d5a;
}
.utci-day-legend-date {
top: 30px;
height: 16px;
line-height: 16px;
font-size: 8px;
font-weight: 700;
color: #9a7d5a;
}
.utci-day-legend-icon {
top: 52px;
height: 30px;
display: flex;
align-items: center;
justify-content: flex-end;
font-size: 8px;
font-weight: 700;
color: #9a7d5a;
}
/* Positioning context for the two value labels. The separator and the
secondary label are pinned to fixed offsets inside it (matching the tab's
own hi/lo block) rather than stacking after the main label, so a main
label that wraps to two lines can't shove them down. */
.utci-day-legend-values {
top: 84px;
height: 0;
}
/* Divider between the top (warmer) and bottom (cooler) values — mirrors the
thin line added between the hi/lo numbers inside each tab. */
.utci-day-legend-sep {
position: absolute;
top: 17px;
left: 0;
right: 0;
height: 1px;
background: rgba(0, 0, 0, 0.15);
}
/* Each label matches the weight and colour family of the value beside it, so
the big/small pairing reads without a legend. Heights match the value line
boxes: 14px x 1.1 and 11px x 1.1 (see the numeric block in DayTabs.js). */
.utci-day-legend-main {
position: absolute;
top: 0;
left: 0;
right: 0;
display: flex;
align-items: center;
justify-content: flex-end;
min-height: 15px;
font-size: 9px;
font-weight: 800;
line-height: 1.1;
color: #6b4f2a;
}
.utci-day-legend-sec {
/* 20px matches the tab's secondary number, measured from the top of its
hi/lo block (15px main line + 5px separator gap). Absolute, so it holds
that offset even if the main label above it wraps. */
position: absolute;
top: 20px;
left: 0;
right: 0;
display: flex;
/* Top-aligned (not centred) so the label stays level with the TOP of the
cooler/secondary value beside it even when the label text wraps. */
align-items: flex-start;
justify-content: flex-end;
min-height: 12px;
font-size: 8px;
font-weight: 600;
/* Fixed 12.1px line box = the secondary number's own line box (11px x 1.1),
so the smaller label text centres on the number rather than riding above
it. Stated in px, not a ratio, so it holds when the mobile media query
drops the label's font-size. */
line-height: 12.1px;
color: #9a7d5a;
}
.utci-day-tabs {
display: flex;
flex: 1 1 auto;
min-width: 0;
overflow-x: auto;
scroll-behavior: smooth;
scrollbar-width: none; /* Firefox */
-ms-overflow-style: none; /* old Edge */
/* Grab cursor hints drag-to-scroll, matching .utci-tbody-scroll */
cursor: grab;
}
.utci-day-tabs:active {
cursor: grabbing;
}
.utci-day-tabs::-webkit-scrollbar {
display: none; /* Chrome/Safari */
}
.utci-day-tab {
flex: 1 0 0;
min-width: 90px;
padding: 10px 20px;
cursor: pointer;
font-family: Manrope, sans-serif;
font-size: 10px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.08em;
background: transparent;
border: 1px solid #c9b08a;
color: #b09870;
overflow-wrap: break-word;
transition: color 0.2s, border-color 0.2s, background-color 0.2s, filter 0.15s;
}
.utci-day-tab + .utci-day-tab {
border-left: none;
}
/* Explicit line-height (rather than the browser-default "normal") so this
row's rendered height is identical across browsers — Firefox and Chrome
compute "normal" differently for the same font/size, which threw off the
legend column's row-matching in .utci-day-legend-day (table.css). */
.utci-day-name {
display: block;
line-height: 16px;
}
.utci-day-tab:hover:not(.active):not(.locked) {
filter: brightness(1.05) !important;
}
.utci-day-tab.active {
color: #c8922a;
outline: 1px solid #000;
outline-offset: -1px;
z-index: 1;
}
/* Danger-band days breathe a slow dark-red inner glow */
.utci-day-tab.danger {
animation: dayTabDangerPulse 2.4s ease-in-out infinite;
}
@keyframes dayTabDangerPulse {
0%, 100% { box-shadow: inset 0 0 0 0 rgba(136, 0, 0, 0); }
50% { box-shadow: inset 0 0 34px 8px rgba(136, 0, 0, 0.6); }
}
@media (prefers-reduced-motion: reduce) {
.utci-day-tab.danger { animation: none; }
}
/* The "Mon 12 May" date underneath each tab name */
.utci-day-date {
font-family: Fraunces, serif;
font-style: italic;
font-size: 13px;
/* Explicit, not "normal" — pins the row height so it renders identically
across browsers (see .utci-day-name for why this matters). */
line-height: 16px;
text-transform: none;
letter-spacing: 0;
display: block;
margin-top: 3px;
color: inherit;
}
/* ── 5b. FILTER PROFILES (the "Profile: Basic Urban …" bar) ───────── */
.filter-profiles {
display: flex;
flex-direction: column;
gap: 0;
margin-bottom: 0;
padding: 0px;
background: transparent;
border: none;
}
.filter-profiles-label {
font-family: Manrope, sans-serif;
font-size: 12px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.08em;
color: #7a5c2a;
margin-bottom: 6px;
white-space: nowrap;
text-align: center;
}
/* Config strip - value-only pickers for the felt-model settings (vehicle
type, building type, fur colour, skin type, pollen type, SunSoak env).
Sits directly under the profile picker, always visible (not tucked away
in the collapsible Columns accordion) so these settings stay as
prominent as the profile choice that they belong to. */
.col-toggles-config {
display: flex;
flex-wrap: wrap;
justify-content: center;
gap: 12px;
align-items: center;
margin-top: 0;
padding: 10px 14px;
background: #fffcf2;
border: 1.5px solid #c9b08a;
border-top: none;
}
.col-toggles-config-item {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 6px 10px;
background: rgba(255,255,255,0.6);
border: 1px solid #e6d7b8;
border-radius: 5px;
}
.config-item-label {
font-family: Manrope, sans-serif;
font-size: 11px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.05em;
color: #9a7a4a;
white-space: nowrap;
}
/* Main thermal selection - the config item whose value also drives the
day-tab hi/lo readout. Always sorted first (see DayTabs.js) and marked
with a badge so it reads as distinct from the other config pickers. */
.col-toggles-config-item--main {
padding: 8px 8px;
border: 1.5px solid #c8922a;
border-radius: 4px;
background: rgba(200,146,42,0.08);
}
.config-item-main-badge {
display: block;
flex: 0 0 100%;
text-align: center;
font-family: Manrope, sans-serif;
font-size: 13px;
font-weight: 800;
text-transform: uppercase;
letter-spacing: 0.04em;
color: #9a6a1a;
padding: 0;
white-space: nowrap;
cursor: help;
}
/* Hidden reference copy of the Activities tab (see ConfigPanel.js) — stays
in normal flow so its natural width sets the dropdown's fixed size, but
is never seen or reachable. */
.profile-scroll-sizer {
visibility: hidden;
height: 0;
overflow: hidden;
pointer-events: none;
}
.profile-scroll-sizer .profile-scroll { pointer-events: none; }
/* Single scrollable row of profile cards */
.profile-scroll {
display: flex;
flex-wrap: nowrap;
gap: 6px;
align-items: center;
pointer-events: none;
justify-content: center; /* centre when content fits */
justify-content: safe center; /* fall back to flex-start if it overflows */
overflow-x: auto;
overflow-y: auto; /* forced by browser when overflow-x is non-visible */
padding-bottom: 6px;
margin-bottom: 0;
scrollbar-width: none;
-ms-overflow-style: none;
padding-left: 4px; /* breathing room at left edge */
padding-right: 4px; /* breathing room at right edge */
}
.profile-scroll::-webkit-scrollbar { display: none; }
.profile-scroll > * { pointer-events: auto; }
/* Fade-edge wrapper — hosts the ::before/::after gradient overlays */
.profile-scroll-wrap {
position: relative;
width: 100%;
padding-top:8px;
padding-bottom:1px;
background-color: #fffcf2;
border-top: 1.5px solid #c9b08a;
border-left: 1.5px solid #c9b08a;
border-right: 1.5px solid #c9b08a;
border-bottom: 1.5px solid #c9b08a;
}
.profile-scroll-wrap::before,
.profile-scroll-wrap::after {
content: '';
position: absolute;
top: 0;
bottom: 0;
width: 52px;
pointer-events: none;
opacity: 0;
transition: opacity 0.2s ease;
z-index: 2;
}
.profile-scroll-wrap::before {
left: 0;
background: linear-gradient(to right, #f5edd6 10%, transparent 100%);
}
.profile-scroll-wrap::after {
right: 0;
background: linear-gradient(to left, #f5edd6 10%, transparent 100%);
}
.profile-scroll-wrap.fade-left::before { opacity: 1; }
.profile-scroll-wrap.fade-right::after { opacity: 1; }
.profile-scroll-chevron {
position: absolute;
top: 50%;
transform: translateY(-50%);
z-index: 3;
pointer-events: none;
color: #8a6a3a;
font-size: 18px;
line-height: 1;
opacity: 0;
transition: opacity 0.2s ease;
user-select: none;
}
.profile-scroll-chevron.left { left: 4px; }
.profile-scroll-chevron.right { right: 4px; }
.profile-scroll-wrap.fade-left .profile-scroll-chevron.left { opacity: 1; }
.profile-scroll-wrap.fade-right .profile-scroll-chevron.right { opacity: 1; }
/* ── Profile & config nav dropdown ──────────────────────────────────────
The profile picker + config strip used to sit in a permanent full-width
band above the day tabs. They now live in a dropdown that expands
downward directly under the (now permanently floating) top nav, opened by
the profile trigger embedded in the middle of that nav. Keeps the
original full-width horizontal layout (profile tabs, horizontally
scrolling card row, config strip) - just collapsed by default. */
/* Floating trigger, fixed to the bottom of the viewport (the dropdown itself
now slides up from the bottom too — see .profile-dropdown below — so the
button sits right where the panel will appear from). Always visible,
independent of the nav/header, which has gone back to scrolling away
normally instead of floating. */
.floating-profile-btn {
position: fixed;
left: 50%;
bottom: 8px;
transform: translateX(-50%);
z-index: 98;
display: flex;
align-items: center;
gap: 8px;
max-width: 92vw;
padding: 10px 18px;
background: #fdf8ee;
border: 1.5px solid #c9b08a;
border-radius: 999px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.10);
cursor: pointer;
color: #6a4c1a;
transition: background 0.15s, border-color 0.15s, box-shadow 0.15s;
}
@media (min-width: 1024px) {
.floating-profile-btn {
display: none;
}
}
.floating-profile-btn:hover { background: #fff; border-color: #c8922a; box-shadow: 0 3px 10px rgba(0, 0, 0, 0.14); }
.floating-profile-btn.is-open { background: #fff; border-color: #c8922a; }
.floating-profile-icon { font-size: 16px; line-height: 1; flex: none; }
/* No fixed max-width — the pill grows to fit the label. Ellipsis only
kicks in once the whole button hits its 92vw cap (min-width:0 lets these
flex children actually shrink instead of forcing the button to overflow). */
.floating-profile-val {
font-family: Manrope, sans-serif;
font-size: 13px;
font-weight: 800;
text-transform: uppercase;
letter-spacing: 0.02em;
color: #1e1208;
min-width: 0;
flex: 0 1 auto;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.floating-profile-sub {
font-family: Manrope, sans-serif;
font-size: 12px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.02em;
color: #6b4f2a;
min-width: 0;
flex: 0 1 auto;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.floating-profile-caret {
font-size: 10px;
transition: transform 0.2s ease;
}
.floating-profile-btn.is-open .floating-profile-caret { transform: rotate(180deg); }
/* Backdrop + dropdown panel — always mounted once opened for the first time
(see ConfigPanel.js), visibility/position driven by the .is-open class so
both the open AND close transitions can animate (a conditionally-mounted
element only ever gets an entrance animation, never an exit one). */
.profile-dropdown-overlay {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.35);
z-index: 99;
opacity: 0;
visibility: hidden;
pointer-events: none;
transition: opacity 0.2s ease, visibility 0s linear 0.2s;
}
.profile-dropdown-overlay.is-open {
opacity: 1;
visibility: visible;
pointer-events: auto;
transition: opacity 0.2s ease, visibility 0s linear 0s;
}
.profile-dropdown {
position: fixed;
left: 0;
right: 0;
bottom: 0;
max-height: calc(100vh - 24px);
z-index: 101;
background: #f5edd6;
border-top: 1.5px solid #c9b08a;
box-shadow: 0 -10px 28px rgba(0, 0, 0, 0.18);
opacity: 0;
visibility: hidden;
pointer-events: none;
/* Scales up from the floating-profile-btn's position (bottom-centre) so
the panel reads as growing out of that button, and shrinks back into
it on close, instead of a generic slide. */
transform-origin: 50% 100%;
transform: scale(0.85);
transition: opacity 0.22s cubic-bezier(0.22, 1, 0.36, 1),
transform 0.22s cubic-bezier(0.22, 1, 0.36, 1),
visibility 0s linear 0.22s;
}
.profile-dropdown.is-open {
opacity: 1;
visibility: visible;
pointer-events: auto;
transform: scale(1);
transition: opacity 0.22s cubic-bezier(0.22, 1, 0.36, 1),
transform 0.22s cubic-bezier(0.22, 1, 0.36, 1),
visibility 0s linear 0s;
}
.profile-dropdown-body {
max-height: calc(100vh - 24px);
overflow-y: auto;
padding: 16px;
}
.profile-dropdown-close {
position: absolute;
top: 10px;
right: 14px;
background: none;
border: none;
font-size: 24px;
line-height: 1;
color: #b09870;
cursor: pointer;
padding: 2px 8px;
border-radius: 6px;
transition: color 0.15s, background 0.15s;
}
.profile-dropdown-close:hover { color: #1e1208; background: rgba(176, 152, 112, 0.16); }
/* Tablet + desktop: the floating bottom button is mobile-only, so the panel
hangs off the bottom edge of the active-profile-row instead — dropping
down out of that row and shrinking back into it on close.
--profile-anchor-top is the row's live viewport bottom, set by
ConfigPanel.js. Phones (<=640px) keep the bottom sheet untouched. */
@media (min-width: 641px) {
.profile-dropdown {
top: var(--profile-anchor-top, 0px);
bottom: auto;
max-height: calc(100vh - var(--profile-anchor-top, 0px) - 16px);
width: fit-content;
min-width: 380px;
max-width: min(880px, calc(100vw - 32px));
margin: 0 auto;
border: 1.5px solid #c9b08a;
border-radius: 12px;
overflow: hidden; /* keeps the scrolling body inside the rounded corners */
box-shadow: 0 12px 28px rgba(0, 0, 0, 0.18);
transform-origin: 50% 0;
}
.profile-dropdown-body {
max-height: calc(100vh - var(--profile-anchor-top, 0px) - 16px);
}
/* Too little room under the row for the content — revert to the mobile
bottom sheet (set by ConfigPanel.js) so the panel gets the full height. */
.profile-dropdown.is-bottom-sheet {
top: auto;
bottom: 0;
max-height: calc(100vh - 24px);
border: none;
border-top: 1.5px solid #c9b08a;
border-radius: 12px 12px 0 0;
box-shadow: 0 -10px 28px rgba(0, 0, 0, 0.18);
transform-origin: 50% 100%;
}
.profile-dropdown.is-bottom-sheet .profile-dropdown-body {
max-height: calc(100vh - 24px);
}
}
/* Read-only "what am I looking at" row above the day tabs — states the
active profile and the thermal basis (SunSoak env / vehicle / building /
fur colour) driving the numbers below. Clicking/Enter opens the same
profile & config dropdown as the nav trigger. */
.active-profile-row {
display: flex;
align-items: center;
justify-content: center;
flex-wrap: wrap;
gap: 12px;
padding: 8px 16px;
margin-bottom: 4px;
/* --row-bg is set inline per the exact selected option (see ROW_GRADIENTS
in DayTabs.js) — falls back to a flat parchment tone if unset. */
background: var(--row-bg, #fdf8ee);
border: 2px solid #d4c0a0;
border-radius: 9px;
cursor: pointer;
box-shadow: inset 0 -1px 0 rgba(0,0,0,0.05), 0 2px 5px rgba(0,0,0,0.08);
transition: filter 0.2s, border-color 0.2s, box-shadow 0.2s;
}
/* Remove alpha from the default fallback color and any potential inherited
semi-transparent backgrounds while keeping the hue. */
.active-profile-row:not([style*="background"]) {
background-color: #fdf8ee;
}
@media (min-width: 1024px) {
.active-profile-row {
position: sticky;
top: 0 !important; /* Stick to the very top of the viewport */
z-index: 11;
}
.utci-thead-sticky {
position: sticky;
top: 28px !important; /* Corrected to match the height of the active-profile-row + margins */
z-index: 10;
}
}
.active-profile-row:hover,
.active-profile-row:focus-visible {
filter: brightness(1.04);
border-color: #c8922a;
box-shadow: inset 0 -1px 0 rgba(0,0,0,0.05), 0 3px 8px rgba(200,146,42,0.18);
outline: none;
}
/* Breathing room between each "PROFILE" / "VIEWING" label and its value. */
.active-profile-row-item {
display: inline-flex;
align-items: baseline;
gap: 7px;
}
.active-profile-row-label {
font-family: Manrope, sans-serif;
font-size: 12px;
font-weight: 700;
letter-spacing: 0.06em;
text-transform: uppercase;
color: #9a7d5a;
}
.active-profile-row-value {
font-family: Manrope, sans-serif;
font-size: 15px;
font-weight: 800;
letter-spacing: 0.02em;
text-transform: uppercase;
color: #1e1208;
}
.active-profile-row-sep { color: #c0a880; font-size: 16px; }
.active-profile-row-edit {
display: inline-flex;
align-items: center;
gap: 5px;
font-family: Manrope, sans-serif;
font-size: 12.5px;
font-weight: 700;
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;
}
.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);
}
/* Border + text accent per thermal-model group, reusing the same colour
groups the CustomSelect pickers use (see .cs-btn.grp-* in ui.css) — e.g.
SunSoak's "Solar model" dropdown is grp-felt. The background itself comes
from --row-bg (inline, per exact selection — see ROW_GRADIENTS); these
classes only set the border/text so the row still reads as belonging to
its category even though the gradient varies option-to-option. */
.active-profile-row.grp-felt { border-color: rgba(7,92,58,0.35); }
.active-profile-row.grp-wind { border-color: rgba(106,34,16,0.30); }
.active-profile-row.grp-ambient { border-color: rgba(60,58,53,0.30); }
.active-profile-row.grp-surface { border-color: rgba(11,62,114,0.30); }
.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; }
.active-profile-row.grp-surface .active-profile-row-value { color: #0b3e72; }
/* Vertical rule separating general-use profiles from technical ones */
.profile-divider {
display: inline-block;
width: 1px;
align-self: stretch;
background: #c9b08a;
margin: 0 2px;
opacity: 0.6;
flex-shrink: 0;
}
.profile-btn {
width: 100px;
height: 100px;
flex: none; /* never shrink below 100px — the row scrolls instead */
padding: 0;
position: relative;
display: inline-flex;
flex-direction: column;
align-items: stretch;
overflow: hidden;
cursor: pointer;
border: 1.5px solid #c9b08a;
background: transparent;
border-radius: 3px;
transition: border-color 0.15s, box-shadow 0.15s;
-webkit-user-select: none;
user-select: none;
}
.profile-btn-scene {
flex: 1;
background-size: cover;
background-position: center;
display: flex;
align-items: center;
justify-content: center;
font-size: 22px;
line-height: 1;
opacity: 0.7;
transition: opacity 0.2s;
}
.profile-btn:hover .profile-btn-scene {
opacity: 0.8;
}
.profile-btn.active .profile-btn-scene {
opacity: 1;
}
.profile-lock-badge {
position: absolute;
top: 4px;
right: 4px;
font-size: 11px;
line-height: 1;
pointer-events: none;
z-index: 1;
}
.profile-btn-label {
position: absolute;
bottom: 0;
left: 0;
right: 0;
height: 24px;
display: block;
line-height: 24px;
text-align: center;
font-family: Manrope, sans-serif;
font-size: 10px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.05em;
background: rgba(30,18,8,0.52);
color: #fff;
padding: 0 4px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.profile-btn:hover {
border-color: #c8922a;
box-shadow: 0 1px 4px rgba(200,146,42,0.25);
}
.profile-btn.active {
border-color: #c8922a;
}
.profile-btn.active .profile-btn-label {
background: rgba(200,146,42,0.82);
color: #fff;
}
/* Tab strip above the profile scroll row */
.profile-tabs {
display: flex;
width: 360px;
margin-left: auto;
margin-right: auto;
margin-bottom: 0px;
}
.profile-tab {
flex: 1;
padding: 7px 4px;
background: transparent;
border: none;
border-bottom: 1.5px solid transparent;
border-left: 1.5px solid #f5edd6;
border-right: 1.5px solid #f5edd6;
border-top: 1.5px solid #f5edd6;
font-family: Manrope, sans-serif;
font-size: 11px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.06em;
color: #9a7a4a;
cursor: pointer;
transition: color 0.15s, border-color 0.15s;
white-space: nowrap;
border-radius:7px 7px 0px 0px;
z-index:1;
}
.profile-tab:hover {
color: #c8922a;
background: #f0e4c4;
}
.profile-tab.active {
color: #c8922a;
background-color: #fffcf2;
border-bottom-color: #fffcf2;
border-left: 1.5px solid #c9b08a;
border-right: 1.5px solid #c9b08a;
border-top: 1.5px solid #c9b08a;
margin-bottom: -1.5px;
}
/* ── 6. COLUMN TOGGLES (the "Columns: Hour Air RH ..." bar) ────────── */
.col-toggles {
display: flex;
flex-wrap: wrap;
gap: 6px;
align-items: center;
margin-bottom: 0;
padding: 10px 14px;
background: #fdf8ee;
border: 1.5px solid #d4c0a0;
border-bottom: none;
position: relative;
}
/* Whole Columns bar (Hours + column pills) stays collapsed behind the
Edit columns / Hide Columns toggle in the toolbar row until opened.
Scoped to detailed (table) view — quick view keeps its own reduced
col-toggles bar (Hours only) visible regardless of this state.
It slides rather than snapping: the wrapper is a 1-row grid animated
0fr → 1fr, which is the only way to transition to an unknown content
height in CSS. The bar's own padding/top border have to collapse with
it, otherwise a 25px sliver of empty panel stays behind at 0fr.
Note the wrapper carries key=${forecastView} in app.js. Quick view keeps
this bar expanded and detailed view collapses it, so on a view switch the
browser would otherwise animate that difference — the panel would flash
open and slide shut. Remounting gives it no previous value to transition
from, so the collapsed state just applies. */
.col-toggles-wrap {
display: grid;
grid-template-rows: 1fr;
}
.table-main:not(.table-main--simple) .col-toggles-wrap {
grid-template-rows: 0fr;
transition: grid-template-rows 0.28s cubic-bezier(0.4, 0, 0.2, 1);
}
.table-main:not(.table-main--simple) .col-toggles-wrap > .col-toggles {
overflow: hidden;
min-height: 0;
padding-top: 0;
padding-bottom: 0;
border-top-width: 0;
opacity: 0;
transition:
padding 0.28s cubic-bezier(0.4, 0, 0.2, 1),
border-top-width 0.28s cubic-bezier(0.4, 0, 0.2, 1),
opacity 0.18s ease;
}
.table-main:not(.table-main--simple) .col-toggles-wrap--open {
grid-template-rows: 1fr;
}
.table-main:not(.table-main--simple) .col-toggles-wrap--open > .col-toggles {
padding-top: 10px;
padding-bottom: 10px;
border-top-width: 1.5px;
opacity: 1;
transition:
padding 0.28s cubic-bezier(0.4, 0, 0.2, 1),
border-top-width 0.28s cubic-bezier(0.4, 0, 0.2, 1),
opacity 0.22s ease 0.06s;
}
@media (prefers-reduced-motion: reduce) {
.table-main:not(.table-main--simple) .col-toggles-wrap,
.table-main:not(.table-main--simple) .col-toggles-wrap > .col-toggles {
transition: none;
}
}
.col-toggles-display-opts {
display: flex;
align-items: center;
gap: 12px;
margin-left: auto;
padding-left: 10px;
flex-shrink: 0;
}
.col-toggles-label {
display: none;
}
.col-toggles-edit-btn {
display: flex;
align-items: center;
justify-content: space-between;
gap: 6px;
width: 150px;
padding: 6px 12px;
background: #f5edd6;
border: 1.5px solid #c9b08a;
color: #7a5c2a;
font-family: Manrope, sans-serif;
font-size: 10px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.08em;
cursor: pointer;
text-align: left;
}
/* Free tier: the button stays visible but opens the Extra upsell instead of
the Columns bar. Muted to read as locked, still obviously clickable. */
.col-toggles-edit-btn--locked {
background: #efe6d2;
border-color: #c0b090;
color: #9a8060;
}
.col-toggles-edit-btn--locked:hover {
color: #7a5c2a;
}
.col-toggles-edit-btn-icon {
width: 12px;
height: 12px;
flex-shrink: 0;
transition: transform 0.2s ease;
}
.col-toggles-edit-btn--open .col-toggles-edit-btn-icon {
transform: rotate(180deg);
}
.col-toggles-body {
display: contents;
}
/* The little pill buttons */
.col-toggle {
padding: 5px 13px;
cursor: pointer;
border: 1.5px solid #c9b08a;
background: #f5edd6;
color: #6b4228;
font-family: Manrope, sans-serif;
font-size: 12px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.06em;
transition: all 0.15s;
-webkit-user-select: none;
user-select: none;
}
.col-toggle:hover {
border-color: #c8922a;
color: #6b4f2a;
background: #fef3dc;
}
/* The "pressed-in" active look */
.col-toggle.on {
background: #c8922a;
border-color: #c8922a;
color: #fff;
}
/* ── COLUMN TOGGLE - group colour tints ─────────────────────────────────
Idle: light group tint bg + group text + group border.
Hover: darker tint matching th hover palette.
On: solid saturated group colour, white text.
On-hover: slightly darker solid. */
.col-toggle.grp-felt { background: rgba(223,243,236,0.50); color: #075c3a; border-color: rgba(7,92,58,0.35); }
.col-toggle.grp-surface { background: rgba(221,234,248,0.50); color: #0b3e72; border-color: rgba(11,62,114,0.30); }
.col-toggle.grp-ambient { background: rgba(236,234,227,0.50); color: #3c3a35; border-color: rgba(60,58,53,0.30); }
.col-toggle.grp-precip { background: rgba(247,228,238,0.50); color: #6b1e3a; border-color: rgba(107,30,58,0.30); }
.col-toggle.grp-sky { background: rgba(234,232,252,0.50); color: #352e7a; border-color: rgba(53,46,122,0.30); }
.col-toggle.grp-wind { background: rgba(250,230,224,0.50); color: #6a2210; border-color: rgba(106,34,16,0.30); }
.col-toggle.grp-airqual { background: rgba(229,240,216,0.50); color: #244408; border-color: rgba(36,68,8,0.30); }
.col-toggle.grp-solar { background: rgba(250,234,204,0.50); color: #5a3004; border-color: rgba(90,48,4,0.30); }
.col-toggle.grp-felt:hover { background: rgba(213,233,226,0.80); color: #075c3a; border-color: rgba(7,92,58,0.55); }
.col-toggle.grp-surface:hover { background: rgba(211,224,238,0.80); color: #0b3e72; border-color: rgba(11,62,114,0.50); }
.col-toggle.grp-ambient:hover { background: rgba(226,224,217,0.80); color: #3c3a35; border-color: rgba(60,58,53,0.50); }
.col-toggle.grp-precip:hover { background: rgba(237,218,228,0.80); color: #6b1e3a; border-color: rgba(107,30,58,0.50); }
.col-toggle.grp-sky:hover { background: rgba(224,222,242,0.80); color: #352e7a; border-color: rgba(53,46,122,0.50); }
.col-toggle.grp-wind:hover { background: rgba(240,220,214,0.80); color: #6a2210; border-color: rgba(106,34,16,0.50); }
.col-toggle.grp-airqual:hover { background: rgba(219,230,206,0.80); color: #244408; border-color: rgba(36,68,8,0.50); }
.col-toggle.grp-solar:hover { background: rgba(240,224,194,0.80); color: #5a3004; border-color: rgba(90,48,4,0.50); }
.col-toggle.grp-felt.on { background: #0f9e63; border-color: #0f9e63; color: #fff; }
.col-toggle.grp-surface.on { background: #2672c8; border-color: #2672c8; color: #fff; }
.col-toggle.grp-ambient.on { background: #7a7870; border-color: #7a7870; color: #fff; }
.col-toggle.grp-precip.on { background: #b03060; border-color: #b03060; color: #fff; }
.col-toggle.grp-sky.on { background: #6058c8; border-color: #6058c8; color: #fff; }
.col-toggle.grp-wind.on { background: #b04030; border-color: #b04030; color: #fff; }
.col-toggle.grp-airqual.on { background: #4a8018; border-color: #4a8018; color: #fff; }
.col-toggle.grp-solar.on { background: #b07820; border-color: #b07820; color: #fff; }
.col-toggle.grp-felt.on:hover { background: #0c7d4e; border-color: #0c7d4e; color: #fff; }
.col-toggle.grp-surface.on:hover { background: #1e5aa0; border-color: #1e5aa0; color: #fff; }
.col-toggle.grp-ambient.on:hover { background: #5e5c56; border-color: #5e5c56; color: #fff; }
.col-toggle.grp-precip.on:hover { background: #8c264c; border-color: #8c264c; color: #fff; }
.col-toggle.grp-sky.on:hover { background: #4c48a8; border-color: #4c48a8; color: #fff; }
.col-toggle.grp-wind.on:hover { background: #8c3226; border-color: #8c3226; color: #fff; }
.col-toggle.grp-airqual.on:hover { background: #3a6412; border-color: #3a6412; color: #fff; }
.col-toggle.grp-solar.on:hover { background: #8c5e18; border-color: #8c5e18; color: #fff; }
/* ── Vehicle group: select + windows-open checkbox fused together ── */
.col-toggle-group {
display: inline-flex;
align-items: stretch;
gap: 0;
/* Collapse any whitespace-node gap between the cs-wrap span and cs-vent */
font-size: 0;
}
.col-toggle-group > * {
font-size: 12px; /* restore font size for children */
}
/* Remove the right border + radius on the cs-wrap/cs-btn so it butts
flush with the VentPill. The cs-wrap is the direct child, so we
target its inner cs-btn via the grouped-left class on the button. */
.col-toggle-group .cs-wrap {
display: inline-flex;
align-items: stretch;
}
.col-toggle-group .grouped-left {
border-right: none !important;
border-radius: 0 !important;
}
.col-toggle-group .cs-wrap:only-child .grouped-left,
.col-toggle-group .cs-wrap:last-child .grouped-left {
border-right: 1.5px solid #c9b08a !important;
border-radius: 0 !important;
}
/* ── 7. HOURLY TABLE ────────────────────────────────────────────────── */
.utci-table-wrap {
/* No overflow on this wrapper — having any overflow here would make
the wrapper a scroll container, which would break sticky-to-viewport
on the .utci-thead-sticky strip inside.
The body scroller (.utci-tbody-scroll) owns the horizontal scrollbar. */
border: 1.5px solid #c9b08a;
background: #fffcf2;
position: relative; /* needed for ::before / ::after pseudo-element indicators */
}
/* ── SCROLL INDICATORS ──────────────────────────────────────────────────
Fade + chevron overlays on the left/right edges of the table wrap.
They appear only when there is content to scroll in that direction,
driven by the scroll-fade-left / scroll-fade-right classes toggled by JS. */
.utci-table-wrap::before,
.utci-table-wrap::after {
content: '';
position: absolute;
top: 0;
bottom: 0;
width: 48px;
pointer-events: none;
opacity: 0;
transition: opacity 0.2s ease;
z-index: 6; /* above sticky header (z-index 5) */
}
/* Left fade — warm cream fading to transparent rightward.
Offset by the pinned Hour column's width (--hour-col-w, set by JS in
useTableScroll.js) so the fade sits over the first column that
actually scrolls, not over the Hour column itself. */
.utci-table-wrap::before {
left: var(--hour-col-w, 0px);
background: linear-gradient(to right, rgba(240,228,196,0.92) 0%, transparent 100%);
}
/* Right fade — warm cream fading to transparent leftward */
.utci-table-wrap::after {
right: 0;
background: linear-gradient(to left, rgba(240,228,196,0.92) 0%, transparent 100%);
}
.utci-table-wrap.scroll-fade-left::before { opacity: 1; }
.utci-table-wrap.scroll-fade-right::after { opacity: 1; }
/* Chevron arrows — sit on top of the fade, centred vertically */
.utci-table-wrap .utci-scroll-chevron {
position: absolute;
top: 50%;
transform: translateY(-50%);
z-index: 7;
pointer-events: none;
color: #8a6a3a;
font-size: 18px;
line-height: 1;
opacity: 0;
transition: opacity 0.2s ease;
user-select: none;
}
.utci-table-wrap .utci-scroll-chevron.left { left: calc(var(--hour-col-w, 0px) + 6px); }
.utci-table-wrap .utci-scroll-chevron.right { right: 6px; }
.utci-table-wrap.scroll-fade-left .utci-scroll-chevron.left { opacity: 1; }
.utci-table-wrap.scroll-fade-right .utci-scroll-chevron.right { opacity: 1; }
/* Grab cursor on the body scroller to hint drag-to-scroll */
.utci-tbody-scroll {
cursor: grab;
user-select: none; /* prevent text highlighting during drag */
-webkit-user-select: none;
}
.utci-tbody-scroll:active {
cursor: grabbing;
}
/* ── STICKY HEADER STRIP ────────────────────────────────────────────────
Sits above the body and locks to the top of the viewport while the
page scrolls. Has overflow:hidden so the inner .utci-thead-track can
be translateX'd by JS to follow the body's scrollLeft. */
.utci-thead-sticky {
position: sticky;
top: 0;
z-index: 5;
overflow: hidden;
background: #f0e4c4;
border-bottom: 2px solid #c9b08a;
box-shadow: 0 2px 5px rgba(74, 52, 32, 0.10);
}
/* On mobile the nav bar is fixed at 72px - sticky header must clear it */
@media (max-width: 640px) {
.utci-thead-sticky {
top: 72px;
}
/* Profile/config dropdown - full width and scrollable on phones since it
can run taller than the viewport once the config strip wraps. */
.profile-dropdown-body {
max-height: calc(100vh - 16px);
overflow-y: auto;
}
.floating-profile-val { max-width: 90px; }
.floating-profile-sub { max-width: 90px; }
/* Shrink the day-tab labels on phones so more days stay readable
within the viewport before the tab strip needs to scroll. */
.utci-day-tab {
font-size: 9px;
padding: 10px 12px;
}
.utci-day-date {
font-size: 12px;
}
/* Narrow the metric-label column too, so it costs as little tab width as
possible on phones. */
.utci-day-tabs-wrap { --day-legend-w: 56px; }
.utci-day-legend-col { padding: 10px 4px 11px; letter-spacing: 0.04em; }
.utci-day-legend-day, .utci-day-legend-date, .utci-day-legend-icon { font-size: 7px; }
.utci-day-legend-main { font-size: 8px; }
.utci-day-legend-sec { font-size: 7px; }
}
/* The shifted track inside the sticky header — JS sets transform:translate3d
to keep its columns aligned with the body scroller's scrollLeft. */
.utci-thead-track {
will-change: transform;
}
/* Body scroller — owns the horizontal scrollbar for the whole table. */
.utci-tbody-scroll {
overflow-x: auto;
overflow-y: visible;
}
.utci-table {
width: 100%;
border-collapse: collapse;
font-size: 14px;
/* Fixed layout so the explicit per-cell widths set by the JS width-sync
(see useLayoutEffect in app.js) are actually enforced. Without
this the browser's auto layout treats those widths as suggestions and
redistributes space, which makes the header and body columns drift
apart the further right you scroll. */
table-layout: fixed;
}
/* Column headings (Hour, Air, RH, Wind...).
NOTE: stickiness is handled by the .utci-thead-sticky wrapper, not
the
itself — see the new wrapper rules higher up. */
.utci-table th {
background: #f0e4c4;
color: #6b4f2a;
font-family: Manrope, sans-serif;
font-size: 12px;
text-align: center;
text-transform: uppercase;
letter-spacing: 0.07em;
font-weight: 700;
padding: 14px 12px;
white-space: nowrap;
}
@media (max-width: 800px) {
.utci-table th {
font-size: 10px;
letter-spacing: 0.08em;
}
}
/* The Hour column aligns left, the rest align right */
.utci-table th:first-child,
.utci-table td:first-child {
text-align: left;
}
/* ── PINNED HOUR COLUMN ──────────────────────────────────────────────────
The Hour/time column stays put while the rest of the table scrolls
horizontally, mirroring the fixed label column on the day tabs strip.
The body scroller genuinely scrolls (overflow-x: auto) so a plain
sticky left:0 works there. The header track instead gets shifted by
JS via translate3d to mirror the body's scrollLeft - so its first
cells get an equal-and-opposite translate3d applied by JS (see
handleBodyScroll in useTableScroll.js) to cancel that shift and stay
visually pinned at the left edge. */
.utci-table tbody td:first-child {
position: sticky;
left: 0;
z-index: 2;
background: #fffcf2;
}
.utci-table tbody tr:hover td:first-child {
background: #fef8ea;
}
.utci-table tbody tr.is-now td:first-child {
background: #fff8e4;
}
.utci-table thead th:first-child {
position: relative;
z-index: 3;
will-change: transform;
}
/* The "°C" / "m/s" etc. unit underneath each heading */
.utci-table th .col-unit {
display: block;
color: #c8922a;
font-size: 10px;
margin-top: 2px;
}
/* Stands in for the unit when the column has no data for the selected day —
currently only AQI and Pollen, which CAMS stops forecasting before the
14-day table runs out. Muted and italic so it reads as an absence, not a
value. The cells carry the full explanation as a title. */
.utci-table th .col-unit--none {
color: #a89880;
font-style: italic;
text-transform: none;
letter-spacing: 0.02em;
}
/* Environment modifier badge — shown in SunSoak th when non-default env active */
.col-env-badge {
display: block;
font-size: 9px;
font-weight: 700;
font-family: Manrope, sans-serif;
letter-spacing: 0.04em;
color: #a06828;
opacity: 0.85;
margin-top: 2px;
white-space: nowrap;
}
/* col-unit header spans always visible - units toggle only affects body cells */
@media (max-width: 800px) {
.utci-table th .col-unit {
font-size: 9px;
}
}
.utci-table tbody tr {
border-bottom: 1px solid #ffffff;
transition: background 0.1s;
}
.utci-table tbody tr:hover {
background: #fef8ea;
}
.utci-table td {
padding: 10px 12px;
text-align: center;
font-family: JetBrains Mono, monospace;
font-variant-numeric: tabular-nums; /* stops digits jiggling between rows */
white-space: nowrap;
color: #4a3420;
/* Baseline minimum column width — the JS width-sync uses max(60, head,
body) so very-narrow data columns ("Air 5.0") still feel comfortable
to read instead of being squashed. */
min-width: 60px;
}
/* Subtle vertical column dividers — same pale cream as the row
dividers so the table reads as a soft grid, not a hard one. The
last column has no divider so it doesn't double-up with the
container border. */
.utci-table th:not(:last-child),
.utci-table td:not(:last-child) {
border-right: 1px solid #ffffff;
}
/* The Dir column is centered on both desktop and mobile so the
compass vane sits with even padding either side — especially
important on mobile where the compass label is hidden and there's
no text to balance the right-aligned content. */
.utci-table th.utci-dir-cell,
.utci-table td.utci-dir-cell {
text-align: center;
}
/* "Tight" header — for columns whose body content is narrower than
the default tracked-out header would suggest (Air, Pcpt). Reducing
the letter-spacing shrinks the heading's natural width so it stops
driving the column wider than the body needs. Right-aligned values */
.utci-table th.utci-tight-head {
letter-spacing: 0.04em;
}
/* ── CLICKABLE COLUMN HEADERS ───────────────────────────────────────────
.col-info-th makes every | show a pointer cursor and a subtle hover
tint to signal it can be clicked for a description popup. */
.utci-table th.col-info-th {
cursor: pointer;
transition: background 0.15s;
}
.utci-table th.col-info-th:hover {
background: #e8d8a8;
}
/* ── ROW-INTERVAL STEPPER ────────────────────────────────────────────────
"Rows [−] 3h [+]" — sets how many hours each table row covers
(clock-aligned buckets from midnight). Used twice: bare inside the "Hour"
header, where it stays put while the body scrolls, and with its label in
the simple-view toggle bar (see .fvt-interval below). */
.hour-interval {
display: flex;
justify-content: flex-start;
margin-top: 4px;
}
.row-stepper {
display: inline-flex;
align-items: center;
gap: 3px;
}
.row-stepper-label {
font-family: Manrope, sans-serif;
font-size: 11px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.07em;
color: #7a5c2a;
margin-right: 2px;
}
.row-stepper-btn {
display: flex;
align-items: center;
justify-content: center;
min-width: 26px;
min-height: 26px;
padding: 0;
font-family: Manrope, sans-serif;
font-size: 14px;
font-weight: 700;
line-height: 1;
cursor: pointer;
border: 1px solid #c9b08a;
background: #f5edd6;
color: #6b4228;
border-radius: 3px;
touch-action: manipulation;
user-select: none;
transition: background 0.12s, border-color 0.12s, color 0.12s;
}
.row-stepper-btn:hover:not(:disabled) {
border-color: #c8922a;
background: #fef3dc;
}
.row-stepper-btn:active:not(:disabled) {
background: #c8922a;
border-color: #c8922a;
color: #fff;
}
.row-stepper-btn:disabled {
opacity: 0.35;
cursor: default;
}
/* Fixed width so the row doesn't jitter as the value changes. */
.row-stepper-value {
min-width: 26px;
text-align: center;
font-family: Manrope, sans-serif;
font-size: 11px;
font-weight: 700;
line-height: 1;
color: #6b4228;
user-select: none;
}
/* Roomier tap targets on touch-sized screens. The simple-view stepper gets
the full bump; the one in the Hour header stays tighter because that
column is pinned and its width is taken straight off the viewport. */
@media (max-width: 640px) {
.row-stepper-btn {
min-width: 28px;
min-height: 28px;
font-size: 15px;
}
.fvt-interval .row-stepper-btn {
min-width: 34px;
min-height: 34px;
font-size: 16px;
}
.fvt-interval .row-stepper-value {
min-width: 30px;
font-size: 12px;
}
}
/* ── COLUMN DESCRIPTION POPUP ───────────────────────────────────────────
Fixed-position card that appears below a clicked column header.
JS sets left/top via inline style (centred on the clicked th). */
.col-info-popup {
position: fixed; /* set by JS — stays in viewport regardless of scroll */
z-index: 200;
width: 260px;
max-height: 260px;
display: flex;
flex-direction: column;
background: #fffcf2;
border: 1.5px solid #c9b08a;
border-radius: 6px;
box-shadow: 0 4px 16px rgba(74,52,32,0.18);
padding: 14px 16px 16px;
pointer-events: all;
}
/* Arrow points DOWN toward the column header (default — popup is above the th) */
.col-info-popup::after {
content: '';
position: absolute;
bottom: -7px;
left: var(--arrow-left, 50%);
width: 12px;
height: 12px;
background: #fffcf2;
border-right: 1.5px solid #c9b08a;
border-bottom: 1.5px solid #c9b08a;
transform: translateX(-50%) rotate(45deg);
}
/* Arrow points UP toward the column header (popup is below the th) */
.col-info-popup.col-info-popup--below::after {
bottom: auto;
top: -7px;
border-right: none;
border-bottom: none;
border-left: 1.5px solid #c9b08a;
border-top: 1.5px solid #c9b08a;
transform: translateX(-50%) rotate(45deg);
}
.col-info-title {
display: block;
font-family: Manrope, sans-serif;
font-size: 12px;
font-weight: 700;
color: #4a3420;
margin-bottom: 6px;
text-transform: none;
letter-spacing: 0;
}
.col-info-desc {
font-family: Manrope, sans-serif;
font-size: 12px;
color: #6b4f2a;
line-height: 1.5;
margin: 0;
overflow-y: auto; /* scrolls when text exceeds max-height cap */
flex: 1 1 auto;
}
/* "More about this column" deep-link to the reference page. Sits below the
summary and never shrinks, so it stays visible if the summary scrolls. */
.col-info-more {
display: block;
flex: 0 0 auto;
margin-top: 8px;
font-family: Manrope, sans-serif;
font-size: 11px;
font-weight: 700;
color: #b07d1e;
text-decoration: none;
}
.col-info-more:hover,
.col-info-more:focus-visible {
color: #8a5f12;
text-decoration: underline;
}
.col-info-close {
position: absolute;
top: 6px;
right: 8px;
background: none;
border: none;
font-size: 16px;
color: #a08060;
cursor: pointer;
line-height: 1;
padding: 0 2px;
}
.col-info-close:hover {
color: #4a3420;
}
/* ── POPUP OPEN/CLOSE ANIMATION ─────────────────────────────────────────
The popup mounts/unmounts instantly in JS, so we animate it via a
scale + fade keyframe anchored to the arrow tip (transform-origin).
Above-the-target: origin is bottom-center. Below: top-center. */
@keyframes popup-open {
from { opacity: 0; transform: translateX(-50%) translateY(-100%) scaleY(0.6); }
to { opacity: 1; transform: translateX(-50%) translateY(-100%) scaleY(1); }
}
@keyframes popup-open-below {
from { opacity: 0; transform: translateX(-50%) scaleY(0.6); }
to { opacity: 1; transform: translateX(-50%) scaleY(1); }
}
.col-info-popup:not(.col-info-popup--below) {
transform-origin: bottom center;
animation: popup-open 0.22s cubic-bezier(0.34, 1.4, 0.64, 1) both;
}
.col-info-popup.col-info-popup--below {
transform-origin: top center;
animation: popup-open-below 0.22s cubic-bezier(0.34, 1.4, 0.64, 1) both;
}
/* ── EVENT TAG POPUP — SLIDESHOW ────────────────────────────────────────
The stage holds the current slide. On transition, evTransition is set
to 'exiting' (fade out) then 'entering' (fade in) via the hook. */
@keyframes ev-fade-in { from { opacity: 0; } to { opacity: 1; } }
@keyframes ev-fade-out { from { opacity: 1; } to { opacity: 0; } }
.ev-popup-stage {
transition: opacity 0.38s ease;
}
.ev-popup-entering {
animation: ev-fade-in 0.38s ease both;
}
.ev-popup-exiting {
animation: ev-fade-out 0.38s ease both;
}
/* Slideshow navigation dots */
.ev-popup-dots {
display: flex;
gap: 5px;
margin-top: 8px;
justify-content: center;
align-items: center;
}
.ev-popup-dot {
width: 6px;
height: 6px;
border-radius: 50%;
background: #c9b08a;
opacity: 0.4;
cursor: pointer;
transition: opacity 0.2s, transform 0.2s;
flex-shrink: 0;
}
.ev-popup-dot.active {
opacity: 1;
transform: scale(1.35);
background: #7a5530;
}
.ev-popup-dot:hover {
opacity: 0.75;
}
/* ── 8. NOW-ROW & NIGHT-ROW DECORATIONS ─────────────────────────────── */
/* "Currently this hour" — the highlighted row.
The row gets a cream-yellow background and darker text. No left
stripe or pip — the colour shift alone signals "this is now". */
.utci-table tbody tr.is-now {
background: #fff8e4 !important;
}
.utci-table tbody tr.is-now td {
color: #1e1208;
/* Brass bracket framing the current-hour row top and bottom — clean,
definitive, no left stripe needed. */
border-top: 2px solid #c8922a;
border-bottom: 2px solid #c8922a;
}
/* Night-time hours get a slightly muted text colour */
.utci-table tbody tr.is-night td {
color: #7a5c30;
}
/* The time cell — uses the display serif instead of monospace */
.utci-time {
font-family: Fraunces, serif !important;
font-weight: 500;
font-size: 16px;
color: #1e1208 !important;
}
.utci-time svg {
margin: -6px 0;
flex-shrink: 0;
}
/* Lays out the sky scope + "3pm" + any event tags on one line. Shared by
both orientations — it leads the row normally and heads the column when
the table is rotated. */
.utci-time-inner {
display: inline-flex;
align-items: center;
gap: 7px;
vertical-align: middle;
}
.utci-time-inner svg {
margin: -6px 0;
flex-shrink: 0;
}
/* Event tags are grouped rather than left as loose siblings so that when
the rotated view stacks the time cell into a column, they stay on one
horizontal line instead of forming a vertical stack of their own. The
gap matches .utci-time-inner so the inline layout is unchanged. */
.utci-time-tags {
display: inline-flex;
align-items: center;
gap: 7px;
}
@media (max-width: 800px) {
.utci-time {
font-size: 15px;
}
.utci-cell-hero {
font-size: 15px !important;
padding: 5px 12px;
min-width: 60px;
}
}
/* The coloured pill inside UTCI / UTCI+P cells.
The pill's BACKGROUND comes from inline styles in app.js
(the utciCategory() function decides which thermal-stress colour
to apply). This rule just handles the pill shape. */
.utci-cell {
display: inline-block;
padding: 4px 12px;
border-radius: 3px;
font-weight: 600;
min-width: 60px;
text-align: center;
font-size: 14px;
}
/* Hero variant used for the UTCI+P column — the headline number.
Bigger, bolder, more breathing room so the eye lands here first. */
.utci-cell-hero {
padding: 6px 4px;
font-size: 17px;
font-weight: 700;
min-width: 63px;
letter-spacing: 0.01em;
box-shadow: 0 1px 0 rgba(0,0,0,0.15) inset, 0 0 0 1px rgba(255,255,255,0.08) inset;
}
.utci-band-label {
font-family: Manrope, sans-serif;
font-size: 10px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.06em;
}
/* Faint vertical divider before each new column group in the forecast table. */
.utci-table .col-group-start {
border-left: 2px solid rgba(74, 50, 24, 0.18);
}
/* ── COLUMN GROUP COLOURS — header cell tints per group ─────────────────
Each grp-* class gives the th a distinct background + text colour.
The hover state darkens slightly. The col-unit underneath inherits
a matching accent colour. */
.utci-table th.grp-felt { background: rgba(223,243,236,0.50); color: #075c3a; }
.utci-table th.grp-surface { background: rgba(221,234,248,0.50); color: #0b3e72; }
.utci-table th.grp-ambient { background: rgba(236,234,227,0.50); color: #3c3a35; }
.utci-table th.grp-precip { background: rgba(247,228,238,0.50); color: #6b1e3a; }
.utci-table th.grp-sky { background: rgba(234,232,252,0.50); color: #352e7a; }
.utci-table th.grp-wind { background: rgba(250,230,224,0.50); color: #6a2210; }
.utci-table th.grp-airqual { background: rgba(229,240,216,0.50); color: #244408; }
.utci-table th.grp-solar { background: rgba(250,234,204,0.50); color: #5a3004; }
.utci-table th.grp-felt.col-info-th:hover { background: rgba(213,233,226,0.80); }
.utci-table th.grp-surface.col-info-th:hover { background: rgba(211,224,238,0.80); }
.utci-table th.grp-ambient.col-info-th:hover { background: rgba(226,224,217,0.80); }
.utci-table th.grp-precip.col-info-th:hover { background: rgba(237,218,228,0.80); }
.utci-table th.grp-sky.col-info-th:hover { background: rgba(224,222,242,0.80); }
.utci-table th.grp-wind.col-info-th:hover { background: rgba(240,220,214,0.80); }
.utci-table th.grp-airqual.col-info-th:hover { background: rgba(219,230,206,0.80); }
.utci-table th.grp-solar.col-info-th:hover { background: rgba(240,224,194,0.80); }
.utci-table th.grp-felt .col-unit { color: #0f9e63; }
.utci-table th.grp-surface .col-unit { color: #2672c8; }
.utci-table th.grp-ambient .col-unit { color: #7a7870; }
.utci-table th.grp-precip .col-unit { color: #b03060; }
.utci-table th.grp-sky .col-unit { color: #6058c8; }
.utci-table th.grp-wind .col-unit { color: #b04030; }
.utci-table th.grp-airqual .col-unit { color: #4a8018; }
.utci-table th.grp-solar .col-unit { color: #b07820; }
/* ── GROUP LABEL ROW — spans above the column header row ────────────────
A second tr in the thead. Each cell spans all visible columns in its
group. The Hour column gets a blank matching-background cell. */
.grp-label-row th {
font-family: Manrope, sans-serif;
font-size: 9px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.10em;
padding: 4px 8px 2px;
text-align: center;
white-space: nowrap;
border-bottom: 1px solid rgba(0,0,0,0.06);
}
.utci-table .grp-label-row th.grp-felt { background: rgba(223,243,236,0.50); border-left: 2px solid rgba(74,50,24,0.18); border-bottom: 1px solid rgba(74,50,24,0.18);}
.utci-table .grp-label-row th.grp-surface { background: rgba(221,234,248,0.50); border-left: 2px solid rgba(74,50,24,0.18); border-bottom: 1px solid rgba(74,50,24,0.18);}
.utci-table .grp-label-row th.grp-ambient { background: rgba(236,234,227,0.50); border-left: 2px solid rgba(74,50,24,0.18); border-bottom: 1px solid rgba(74,50,24,0.18);}
.utci-table .grp-label-row th.grp-precip { background: rgba(247,228,238,0.50); border-left: 2px solid rgba(74,50,24,0.18); border-bottom: 1px solid rgba(74,50,24,0.18);}
.utci-table .grp-label-row th.grp-sky { background: rgba(234,232,252,0.50); border-left: 2px solid rgba(74,50,24,0.18); border-bottom: 1px solid rgba(74,50,24,0.18);}
.utci-table .grp-label-row th.grp-wind { background: rgba(250,230,224,0.50); border-left: 2px solid rgba(74,50,24,0.18); border-bottom: 1px solid rgba(74,50,24,0.18);}
.utci-table .grp-label-row th.grp-airqual { background: rgba(229,240,216,0.50); border-left: 2px solid rgba(74,50,24,0.18); border-bottom: 1px solid rgba(74,50,24,0.18);}
.utci-table .grp-label-row th.grp-solar { background: rgba(250,234,204,0.50); border-left: 2px solid rgba(74,50,24,0.18); border-bottom: 1px solid rgba(74,50,24,0.18);}
/* ── POPUP — mobile centre-alignment ───────────────────────────────── */
@media (max-width: 640px) {
.col-info-title,
.col-info-desc,
.col-info-more {
text-align: center;
}
}
/* ── FILTER PROFILES + COLUMN TOGGLES — mobile fixes ───────────────── */
@media (max-width: 640px) {
/* ── Profile bar — single scroll row on all screen sizes ───────────
No wrapping needed; scroll strip handles layout. Dividers stay. */
.filter-profiles {
padding: 4px 0 0;
}
.profile-tabs {
width: 100%;
margin-left: 0;
margin-right: 0;
}
/* Slightly smaller cards on mobile */
.profile-btn {
width: 80px;
height: 80px;
flex: none;
}
.profile-btn-scene {
font-size: 18px;
}
.profile-btn-label {
height: 20px;
font-size: 9px;
}
/* ── Column toggles bar ─────────────────────────────────────────────
Same approach — label full row, toggles at ~31% each. */
.col-toggles,
.col-toggles-config {
flex-wrap: wrap;
gap: 6px;
padding: 10px 10px;
align-items: stretch;
}
.col-toggle {
flex: 0 0 calc((100% - 12px) / 3);
min-width: 0;
box-sizing: border-box;
text-align: center;
padding: 7px 2px;
font-size: 11px;
letter-spacing: 0.03em;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
/* Pollen CustomSelect in col-toggles — match toggle width */
.col-toggles .cs-wrap {
flex: 0 0 calc((100% - 12px) / 3);
min-width: 0;
}
.col-toggles .cs-btn {
width: 100%;
min-width: 0;
padding: 7px 6px;
font-size: 11px;
letter-spacing: 0.03em;
justify-content: center;
overflow: hidden;
}
/* Config strip - each labelled item becomes its own stacked card on
mobile (label above control) rather than a label crammed next to a
squeezed pill, so multiple pickers read as separate rows, not a
jumbled wall of controls. */
.col-toggles-config {
gap: 8px;
}
.col-toggles-config-item {
flex: 0 0 100%;
min-width: 0;
flex-direction: column;
align-items: stretch;
gap: 5px;
padding: 8px 10px;
background: rgba(255,255,255,0.55);
border: 1px solid #e6d7b8;
border-radius: 5px;
}
/* Re-affirm the gold emphasis for the "main" item so it still stands
out after the generic card border above. */
.col-toggles-config-item--main {
border-color: #c8922a;
background: rgba(200,146,42,0.10);
}
.config-item-label {
text-align: center;
}
/* The "SunSoak Config:" badge sits above the main item as its own
centred header row, instead of squeezed inline before the pill. */
.config-item-main-badge {
display: block;
flex: 0 0 100%;
width: 100%;
text-align: center;
padding: 0 0 2px;
}
.col-toggles-config-item .cs-wrap,
.col-toggles-config-item .col-toggle-group {
flex: 1 1 auto;
min-width: 0;
width: 100%;
}
.col-toggles-config-item .cs-btn {
width: 100%;
min-width: 0;
padding: 8px 6px;
font-size: 12px;
letter-spacing: 0.03em;
justify-content: center;
overflow: hidden;
}
/* Fused group: 1 slot by default, 2 slots when VentPill is active */
.col-toggle-group {
flex: 0 0 calc((100% - 12px) / 3);
min-width: 0;
display: inline-flex;
align-items: stretch;
}
.col-toggle-group--expanded {
flex: 0 0 calc((100% - 12px) / 3 * 2 + 6px);
}
/* Vehicle group fuses three children (select + speed + vent) — full row. */
.col-toggle-group--expanded.col-toggle-group--triple {
flex: 0 0 100%;
}
.col-toggle-group .cs-wrap {
flex: 1 1 0;
min-width: 0;
width: 100%;
}
.col-toggle-group .cs-btn {
width: 100%;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-size: 11px;
padding: 7px 6px;
justify-content: center;
}
/* Vehicle's triple group (type + speed + vent) doesn't fit fused on one
line at phone widths — the vent pill won't shrink below its text, so
it either overflows or forces the row taller. Give the type select its
own full-width row and let speed+vent share the row below, still
fused to each other, so nothing has to fight for space. */
.col-toggle-group--triple {
flex-wrap: wrap;
align-content: flex-start;
row-gap: 6px;
}
.col-toggle-group--triple .cs-wrap:first-child {
flex: 0 0 100%;
}
.col-toggle-group--triple .cs-wrap:first-child .grouped-left {
border-right: 1.5px solid #c9b08a !important;
border-radius: 3px !important;
}
/* The base .col-toggle-group .cs-wrap rule sets min-width:0, which lets
this select shrink to nothing and get squeezed onto row 1 next to the
now-full-width type select instead of wrapping. Restore a real min
width so it's forced onto row 2 with the vent pill instead. */
.col-toggle-group--triple .cs-wrap:nth-child(2) {
flex: 1 1 0;
min-width: 80px;
width: auto;
}
.col-toggle-group--triple .cs-vent {
flex: 1 1 0;
min-width: 0;
}
}
/* ── MOBILE TABLE DENSITY — squeeze all basic profile columns in ────────
Target: 7 cols (hour air wind cloud utciP precip vis) on a 390px screen.
Worst-case minimum width at these values: 366px - fits iPhone SE (375px).
Levers: tighter cell padding, smaller min-width, reduced font sizes.
The existing 800px and 640px blocks handle tablets and phablets above.
DENSITY-BREAKPOINT - change 480px here to adjust where dense layout kicks in.
-------------------------------------------------------------------- */
/* -- MOBILE TABLE DENSITY - squeeze all basic profile columns in --------
Target: 7 cols (hour air wind cloud utciP precip vis) on a 390px screen.
Worst-case minimum width: 366px - fits iPhone SE (375px) with room to spare.
DENSITY-BREAKPOINT - change 480px to adjust where dense layout kicks in.
-------------------------------------------------------------------- */
@media (max-width: 480px) {
/* -- Table header cells -----------------------------------------------
Reduce padding so headers do not drive columns wider than data needs. */
.utci-table th {
font-size: 9px;
padding: 10px 4px;
letter-spacing: 0.04em;
}
.utci-table th .col-unit {
font-size: 8px;
}
/* -- Table body cells -------------------------------------------------
Drop side padding 12px to 4px, shrink min-width 60px to 44px.
JetBrains Mono at 11px reads cleanly on retina screens. */
.utci-table td {
padding: 10px 4px;
font-size: 11px;
min-width: 44px;
}
/* -- Time (Hour) column -----------------------------------------------
Drop Fraunces from 16px to 13px and tighten the hour cell min-width. */
.utci-time {
font-size: 13px;
}
.utci-table td:first-child {
min-width: 42px;
}
/* -- SkyScope moon-sun icon in the hour cell ---------------------------
JS renders at size=38. Clamp to 25px via CSS to stop it ballooning
the hour column width and driving up row height. */
.utci-time svg {
width: 34px !important;
height: 34px !important;
margin: -5px 0;
flex-shrink: 0;
}
/* -- Cloud, wind-vane and precip icons in body cells -------------------
Clamp the 28px SVGs down to 20px on mobile to recover column width
without touching the JS size prop. */
.utci-table td svg {
width: 20px !important;
height: 20px !important;
flex-shrink: 0;
}
/* Override for SkyScope - keep it larger than other cell icons */
.utci-table td.utci-time svg {
width: 26px !important;
height: 26px !important;
}
/* -- Hero UTCI+P pill -------------------------------------------------
Shrink min-width and font - still the visual anchor but no longer
forcing the column to be the widest. */
.utci-cell-hero {
font-size: 14px;
min-width: 50px;
padding: 5px 2px;
}
/* -- Vis column -----------------------------------------------------------
Short values (8-99km) need less room than the default 44px min-width. */
.utci-vis-cell {
min-width: 30px;
}
/* -- Standard UTCI pill (non-hero) ------------------------------------*/
.utci-cell {
font-size: 12px;
min-width: 44px;
padding: 3px 5px;
}
/* -- Band label inside pills ------------------------------------------
Tighten letter-spacing to save a little width. */
.utci-band-label {
font-size: 9px;
letter-spacing: 0.03em;
}
}
/* ── SIMPLE / TABLE VIEW TOGGLE ─────────────────────────────────────────── */
.forecast-view-toggle-label {
font-family: Manrope, sans-serif;
font-size: 11px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.07em;
color: #7a5c2a;
flex-shrink: 0;
margin-left: 0;
}
.forecast-view-toggle {
display: inline-flex;
align-items: center;
flex-shrink: 0;
}
.fvt-btn {
display: inline-flex;
align-items: center;
gap: 3px;
font-family: Manrope, sans-serif;
font-size: 11px;
font-weight: 700;
line-height: 1;
padding: 6px 8px;
cursor: pointer;
border: 1.5px solid #c9b08a;
background: #f5edd6;
color: #6b4228;
transition: background 0.12s, border-color 0.12s, color 0.12s;
}
.fvt-btn:first-child {
border-radius: 3px 0 0 3px;
}
.fvt-btn:last-child {
border-left: none;
border-radius: 0 3px 3px 0;
}
.fvt-btn:hover {
border-color: #c8922a;
background: #fef3dc;
z-index: 1;
}
.fvt-btn.on {
background: #c8922a;
border-color: #c8922a;
color: #fff;
}
.fvt-btn-icon {
display: inline-block;
vertical-align: middle;
flex-shrink: 0;
}
/* Toolbar row sitting directly above col-toggles (zero gap, normal flow):
View toggle on the left, and — in Quick view — the thermal-model tabs
centered, touching the col-toggles top border so they read as folder
tabs attached to it. Kept as a 3-column grid (with an empty right
track) so the center tabs stay genuinely centered on the row rather
than just sitting flush after the View toggle. */
.table-toolbar-row {
display: grid;
grid-template-columns: 1fr auto 1fr;
align-items: end;
padding: 0;
}
.fvt-toolbar-left {
grid-column: 1;
display: flex;
align-items: center;
gap: 8px;
justify-self: start;
padding-bottom: 8px;
}
.fvt-thermal-tabs {
grid-column: 2;
justify-self: center;
display: flex;
width: 360px;
align-items: center;
flex-shrink: 0;
}
/* Mobile: stack instead of two columns — View on top, thermal tabs
below it so they still sit flush against the col-toggles border
underneath. Placed after the base rules above so it wins the cascade
(equal specificity, later source order) regardless of where in the
file the @media block lives. */
@media (max-width: 640px) {
.table-toolbar-row {
grid-template-columns: 1fr;
justify-items: center;
row-gap: 6px;
padding: 6px 0 10px;
}
.fvt-toolbar-left {
grid-column: 1;
grid-row: 1;
justify-self: center;
padding-bottom: 0;
}
.fvt-thermal-tabs {
grid-column: 1;
grid-row: 2;
justify-self: stretch;
width: 100%;
}
}
.fvt-thermal-tab {
flex: 1;
display: inline-flex;
align-items: center;
justify-content: center;
font-family: Manrope, sans-serif;
font-size: 11px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.06em;
padding: 9px 2px;
cursor: pointer;
background: transparent;
border: 1.5px solid #f5edd6;
border-left: none;
border-bottom: 1.5px solid transparent;
color: #9a7a4a;
transition: color 0.15s, border-color 0.15s, background 0.15s;
border-radius: 7px 7px 0 0;
z-index: 1;
}
.fvt-thermal-tab:first-child {
border-left: 1.5px solid #f5edd6;
}
.fvt-thermal-tab:hover {
color: #c8922a;
background: #f0e4c4;
}
.fvt-thermal-tab.on {
color: #c8922a;
background-color: #fdf8ee;
border-bottom-color: #fdf8ee;
border-left: 1.5px solid #d4c0a0;
border-right: 1.5px solid #d4c0a0;
border-top: 1.5px solid #d4c0a0;
margin-bottom: -1.5px;
}
/* Row stepper shown in the col-toggles bar in simple mode, pushed to the
far right since Columns/Edit controls are hidden there. */
.fvt-interval {
display: none;
align-items: center;
margin-left: auto;
flex-shrink: 0;
}
.table-main--simple .fvt-interval {
display: inline-flex;
}
/* Hide table-mode column controls when in simple mode */
.table-main--simple .col-toggles-label,
.table-main--simple .col-toggles-edit-btn,
.table-main--simple .col-toggles-body {
display: none !important;
}
/* Show simple view, hide table; swap in table mode */
.forecast-simple-wrap {
display: none;
}
.table-main--simple {
display: block;
}
.table-main--simple .forecast-simple-wrap {
display: flex;
flex-direction: column;
height: 420px;
border: 1.5px solid #d4c0a0;
background: #fdf8ee;
}
.table-main--simple .utci-table-wrap {
display: none;
}
/* ── SIMPLE VIEW CARDS ───────────────────────────────────────────────────── */
.forecast-simple-scroll {
flex: 1;
display: flex;
flex-direction: column;
overflow-x: auto;
overflow-y: hidden;
scrollbar-width: thin;
scrollbar-color: #c9b08a transparent;
cursor: grab;
}
.forecast-simple-scroll:active {
cursor: grabbing;
}
.forecast-simple-scroll::-webkit-scrollbar {
height: 4px;
}
.forecast-simple-scroll::-webkit-scrollbar-track {
background: transparent;
}
.forecast-simple-scroll::-webkit-scrollbar-thumb {
background: #c9b08a;
border-radius: 2px;
}
.forecast-simple-inner {
flex: 1;
display: flex;
flex-direction: column;
width: max-content;
min-width: 100%;
}
/* Half-hour tracks across the whole day, plus a blank inset spacer at each
end (see fscPlot in app.js). Each card spans 2*interval tracks positioned
so it is centred on the hour it is labelled with; the outer cards overhang
into the spacers instead of off the edge. Columns are set inline. */
.forecast-simple-cards {
display: grid;
padding: 12px 0 0;
}
.fsc-card {
min-width: 0;
text-align: center;
border: none;
background: transparent;
padding: 10px 8px 12px;
/* 2px each side = 4px between neighbouring cards. Done with margin rather
than a grid gap so the cards stay centred on their landing hour and keep
lining up with the curve below. */
margin: 0 2px;
display: flex;
flex-direction: column;
align-items: center;
gap: 6px;
border-radius: 1px;
}
.fsc-card--now {
border: 1.5px solid #c8922a;
background: #fff9ee;
box-shadow: 0 2px 10px rgba(200,146,42,0.18);
position: relative;
z-index: 1;
}
.fsc-time {
font-family: JetBrains Mono, monospace;
font-size: 12px;
font-weight: 700;
color: #4a3218;
line-height: 1;
}
.fsc-card--now .fsc-time {
color: #c8922a;
}
.fsc-feel {
font-family: Manrope, sans-serif;
font-size: 10px;
font-weight: 700;
padding: 2px 6px;
border-radius: 2px;
line-height: 1.3;
max-width: 100%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.fsc-scope-wrap {
position: relative;
width: 42px;
height: 42px;
flex-shrink: 0;
}
.fsc-wx-overlay {
position: absolute;
inset: 0;
top: 3px;
display: flex;
align-items: center;
justify-content: center;
pointer-events: none;
}
.fsc-meta {
display: flex;
flex-direction: column;
align-items: center;
gap: 2px;
width: 100%;
}
.fsc-meta-row {
font-family: Manrope, sans-serif;
font-size: 10px;
font-weight: 600;
line-height: 1.2;
color: #6b5230;
opacity: 0.8;
}
.fsc-card--now .fsc-meta-row { opacity: 1; color: #4a3218; }
.fsc-meta-rain { color: #2a6a90; }
.fsc-card--now .fsc-meta-rain { color: #1a4a70; }
.fsc-temp {
font-family: Fraunces, serif;
font-size: 22px;
font-weight: 700;
color: #1e1208;
line-height: 1;
}
.fsc-temp-curve {
display: block;
width: 100%;
flex: 1;
min-height: 60px;
}
.fsc-table-btn-wrap {
padding: 10px 16px 12px;
border-top: 1px solid #d4c0a0;
background: #fdf8ee;
flex-shrink: 0;
}
.fsc-table-btn {
display: block;
width: 100%;
padding: 10px 16px;
border: 1.5px solid #a87020;
border-radius: 6px;
background: linear-gradient(to bottom, #d4a030, #b8781a);
font-family: Manrope, sans-serif;
font-size: 14px;
font-weight: 700;
color: #fff;
letter-spacing: 0.03em;
cursor: pointer;
text-align: center;
box-shadow: 0 1px 4px rgba(160,100,20,0.22);
text-shadow: 0 1px 2px rgba(0,0,0,0.18);
}
.fsc-table-btn:hover {
background: linear-gradient(to bottom, #e8b848, #cc9030);
border-color: #c8922a;
box-shadow: 0 2px 8px rgba(200,146,42,0.28);
}
@media (max-width: 600px) {
.fsc-card {
min-width: 55px;
padding: 8px 4px 10px;
gap: 5px;
}
.fsc-temp {
font-size: 19px;
}
.fsc-feel {
font-size: 9px;
padding: 2px 4px;
}
.fsc-temp-curve {
min-height: 48px;
}
}
/* ══════════════════════════════════════════════════════════════════════
ROTATED TABLE — hours along the top, metrics down the side
─────────────────────────────────────────────────────────────────────
Toggled by the rotate button in the table's top-left cell (state lives
in useAppState as tableRotated). Unlike the normal orientation — which
is two tables kept in step by useTableScroll — this is ONE table in a
single scroll pane, so both sticky edges are pure CSS.
A single table in the shared .utci-tbody-scroll horizontal scroller.
The normal orientation splits head from body so its header can stick
to the viewport; here the hour row scrolls away with the table by
design, so that machinery is not needed. The metric column pins with
plain CSS sticky, which works fine inside the scroller. The table
keeps its natural height and the page scrolls normally.
══════════════════════════════════════════════════════════════════════ */
/* Fallback only, for the first paint before the measurement lands.
useTableScroll measures the pinned column and writes --hour-col-w
inline — it can't be fixed here, because auto layout widens that
column to fit the longest metric name on show. */
.utci-table-wrap.is-rotated {
--hour-col-w: 136px;
}
.utci-table.utci-table-rotated {
/* Auto layout: with a single table there are no header widths to match,
so each hour column can simply size to its own content. */
table-layout: auto;
width: max-content;
min-width: 100%;
/* The separate border model is required, not cosmetic. Under
border-collapse:collapse the borders belong to the TABLE rather than
to the cells, so they do not travel with a sticky cell — the brass
now-column brackets and the row separators paint straight over the
pinned metric column and corner as you scroll. Separate borders
belong to their cell and move with it. */
border-collapse: separate;
border-spacing: 0;
}
/* Row separators have to move to the cells: a | border never paints
in the separate model. */
.utci-table-rotated tbody td,
.utci-table-rotated tbody th {
border-bottom: 1px solid #ffffff;
}
/* ── SPACING ──────────────────────────────────────────────────────────
Cells carry icons alongside their numbers (sky scope, cloud, precip,
wind vane), so they need noticeably more room than a bare figure. */
.utci-table-rotated td,
.utci-table-rotated th {
padding: 11px 16px;
min-width: 92px;
white-space: nowrap;
}
/* The hour row is deliberately NOT sticky: pinning it would mean the
metric rows sliding underneath, and with only a dozen of them the
table reads as shrinking away to nothing. It scrolls with the page. */
.utci-table-rotated thead th {
vertical-align: middle;
}
/* Hour headings read as times, not as column labels — same display serif
as the time cells in the normal orientation. */
.utci-table-rotated .rot-hour-th {
font-family: Fraunces, serif;
font-weight: 500;
font-size: 16px;
text-transform: none;
letter-spacing: 0;
color: #1e1208;
min-width: 76px;
}
/* Top-aligned so every scope sits on the same line. The header row is as
tall as its tallest cell — an hour carrying event tags — and centring
would push the emptier hours down out of step with it. */
.utci-table-rotated thead th.rot-hour-th {
vertical-align: top;
}
/* Stacked, not inline: rotated, every hour is its own COLUMN, so sitting
the scope beside the time would set the width of all of them. Stacking
puts the scope over the label and lets the columns stay narrow. The
negative svg margin that tightens the inline layout has to go — here it
would eat into the row above. */
.utci-table-rotated .rot-hour-th .utci-time-inner {
flex-direction: column;
gap: 3px;
}
.utci-table-rotated .rot-hour-th .utci-time-inner svg {
margin: 0;
}
.utci-table-rotated .rot-hour-th.is-night {
color: #7a5c30;
}
/* ── STICKY METRIC COLUMN (left) ──────────────────────────────────────
The column-group tints (.utci-table th.grp-*) are 50% transparent,
which is fine for a heading that has nothing behind it — but data
cells scroll UNDER this column, so any transparency lets them show
through. The tint is therefore composited over an opaque cream base:
--grp-tint carries the group colour, painted as a background-image
layer on top of an opaque background-color. */
/* Set on the group divider rows as well as the metric labels, so a group
heading and the metrics beneath it resolve to the SAME colour. Both
composite over the identical opaque base below — a shared tint over
two different backgrounds is what makes them drift apart. */
.utci-table-rotated .grp-felt { --grp-tint: rgba(223,243,236,0.50); }
.utci-table-rotated .grp-surface { --grp-tint: rgba(221,234,248,0.50); }
.utci-table-rotated .grp-ambient { --grp-tint: rgba(236,234,227,0.50); }
.utci-table-rotated .grp-precip { --grp-tint: rgba(247,228,238,0.50); }
.utci-table-rotated .grp-sky { --grp-tint: rgba(234,232,252,0.50); }
.utci-table-rotated .grp-wind { --grp-tint: rgba(250,230,224,0.50); }
.utci-table-rotated .grp-airqual { --grp-tint: rgba(229,240,216,0.50); }
.utci-table-rotated .grp-solar { --grp-tint: rgba(250,234,204,0.50); }
/* Over-qualified on purpose: the normal orientation's group rules
(.utci-table th.grp-felt, .utci-table .grp-label-row th.grp-felt) are
specific enough to win otherwise and would restore the transparency. */
.utci-table.utci-table-rotated tbody th.rot-metric-label,
.utci-table.utci-table-rotated tbody th.rot-metric-label.col-info-th:hover,
.utci-table.utci-table-rotated tbody .grp-label-row--rotated th {
background-color: #f0e4c4;
background-image: linear-gradient(var(--grp-tint, transparent), var(--grp-tint, transparent));
}
/* The group rules carry a 2px left border that marks where a group STARTS
along the columns. Rotated, groups are stacked rows and have no left
edge to mark, so it shows up as a stray line — and its width offsets
the label 2px against the metric names below it. */
.utci-table.utci-table-rotated tbody .grp-label-row--rotated th {
border-left: 0;
}
/* The group heading pins with the metric names it labels. */
.utci-table.utci-table-rotated tbody th.rot-group-label {
position: sticky;
left: 0;
z-index: 3;
text-align: left;
padding: 5px 16px;
box-shadow: 1px 0 0 rgba(74, 50, 24, 0.18);
}
/* The per-hour cells filling out a divider row. They carry the group tint
and, in the current hour's column, its share of the brass bracket. */
.utci-table.utci-table-rotated tbody td.grp-label-fill {
padding: 5px 0;
min-width: 0;
background: var(--grp-tint, transparent);
border-bottom: 1px solid rgba(74, 50, 24, 0.18);
}
.utci-table.utci-table-rotated tbody th.rot-metric-label {
position: sticky;
left: 0;
z-index: 3;
text-align: left;
min-width: 136px;
/* box-shadow rather than border-right so the divider sits outside the
cell box and cannot add to the column's measured width. */
box-shadow: 1px 0 0 rgba(74, 50, 24, 0.18);
}
/* 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
it beats the normal orientation's `.utci-table thead th:first-child`
rule, which sets position:relative for the JS transform this view
doesn't use. */
.utci-table.utci-table-rotated thead th.rot-corner {
position: sticky;
left: 0;
z-index: 4;
background: #f0e4c4;
text-align: left;
transform: none;
box-shadow: 1px 0 0 rgba(74, 50, 24, 0.18);
}
.utci-table-rotated .rot-corner-date {
display: block;
font-size: 10px;
letter-spacing: 0.08em;
color: #a08040;
margin-bottom: 6px;
}
.utci-table-rotated .rot-corner-controls {
display: flex;
align-items: center;
gap: 8px;
}
@media (max-width: 640px) {
/* Icon-only on narrow screens — the toolbar is already tight. */
.table-rotate-btn {
font-size: 0;
gap: 0;
padding: 6px 7px;
}
}
/* ── GROUP DIVIDER ROWS ───────────────────────────────────────────────
Stand in for the spanning group labels that sit above the columns in
the normal orientation. The cell spans the full width, so the LABEL is
what gets pinned rather than the cell. */
.utci-table-rotated .grp-label-row--rotated th {
text-align: left;
padding: 5px 16px;
border-right: 0;
}
/* ── NOW / NIGHT ──────────────────────────────────────────────────────
Rotated, "now" is a column rather than a row, so the brass bracket
that frames it runs down each side instead of across top and bottom. */
.utci-table-rotated td.is-now,
.utci-table-rotated th.rot-hour-th.is-now {
border-left: 2px solid #c8922a;
border-right: 2px solid #c8922a;
}
.utci-table-rotated th.rot-hour-th.is-now {
background: #fff8e4;
border-top: 2px solid #c8922a;
}
.utci-table-rotated td.is-night {
color: #7a5c30;
}
/* ── TOOLBAR BUTTON HEIGHTS ───────────────────────────────────────────
Quick/Detailed, Rotate and Edit columns sit on one row and have to
agree on height. They didn't: the first two set line-height:1 while
Edit columns leaves it at normal, so the same 6px padding produced a
box ~3px taller. Pinning one line-height and one min-height across all
three settles it regardless of font-size differences. */
.fvt-btn,
.table-rotate-btn,
.col-toggles-edit-btn {
min-height: 29px;
line-height: 1;
}
/* ── ROTATE BUTTON ────────────────────────────────────────────────────
Sits beside the Quick/Detailed view toggle and only while the detailed
table is showing. Deliberately mirrors .fvt-btn — same type, padding,
border and states — so it reads as part of that control group rather
than a stray button. Standalone, so it keeps a radius on all corners
where .fvt-btn splits its radius across the segmented pair. */
.table-rotate-btn {
display: inline-flex;
align-items: center;
gap: 4px;
font-family: Manrope, sans-serif;
font-size: 11px;
font-weight: 700;
line-height: 1;
padding: 6px 8px;
cursor: pointer;
border: 1.5px solid #c9b08a;
border-radius: 3px;
background: #f5edd6;
color: #6b4228;
transition: background 0.12s, border-color 0.12s, color 0.12s;
}
.table-rotate-btn:hover {
border-color: #c8922a;
background: #fef3dc;
}
.table-rotate-btn.on {
background: #c8922a;
border-color: #c8922a;
color: #fff;
}
/* The icon turns with the state it describes — flat when hours run down
the side, quarter-turned when they run along the top. */
.table-rotate-btn .fvt-btn-icon {
transition: transform 0.18s ease;
}
.table-rotate-btn.on .fvt-btn-icon {
transform: rotate(90deg);
}
/* ── MOBILE ───────────────────────────────────────────────────────── */
@media (max-width: 640px) {
.utci-rotated-scroll {
max-height: min(70vh, 620px);
}
.utci-table-rotated td,
.utci-table-rotated th {
padding: 9px 11px;
min-width: 80px;
}
.utci-table-rotated .rot-hour-th {
font-size: 14px;
min-width: 68px;
}
.utci-table-rotated .rot-metric-label {
min-width: 112px;
}
.utci-table-wrap.is-rotated {
--hour-col-w: 112px;
}
}