// ════════════════════════════════════════════════════════════════════════
//  app.js — Main UTCIForecast component.
//
//  This is the top-level Preact component that owns all state, fetches
//  the forecast, runs the per-hour computations, and renders the page.
//
//  Reading order inside UTCIForecast():
//    1. STATE       (useState calls)       — bits that change on interaction
//    2. EFFECTS     (useEffect calls)      — runs on search / location change
//    3. COMPUTATION (hourlyRows, days, …)  — API data → display rows
//    4. JSX RETURN  (the big html`...`)    — actual page markup
//
//  QUICK MAP
//  ──────────────────────────────────────────────────────────────────────
//    Forecast length .............. fetch URL contains  &forecast_days=14
//    Free tier day limit .......... const FREE_DAYS = 3
//    Preview the Pro view ......... useState(false) on isPro → flip to true
//    Starting location ............ useState({...}) on `location` near top
//    Default columns shown ........ useState({...}) on visibleCols
//    Page tagline / about copy .... search "utci-tagline" or "utci-about-text"
// ════════════════════════════════════════════════════════════════════════

import { h, render, Fragment } from '../vendor/preact.js';
import { useState, useRef, useEffect } from '../vendor/preact-hooks.js';
import htm from '../vendor/htm.js';
import {
  utciCategory, UTCI_BANDS, bandGradient,
  SKIN_TYPES, sunburnMinutes, burnLabel,
  VEHICLE_TYPES, BUILDING_TYPES,
  confidenceBand, moonGlyph,
} from './utils.js';
import { SkyScope, WindVane, CloudIcon, ScopeReticle, PrecipIcon, CustomSelect, VentPill } from './components.js';
import { getCellTagEvents, getUpcomingEvents } from './events.js';
import { POLLEN_TYPES, COL_DESCRIPTIONS, UTCI_ENVIRONMENTS } from './config.js';
import { useAppState } from './hooks/useAppState.js';
import { DayTabs } from './components/DayTabs.js';
import { computeWhyFeelsLike, computeGlanceSummary } from './compute.js';

const html = htm.bind(h);

export function UTCIForecast() {

  // ── STATE + EFFECTS ───────────────────────────────────────────────────
  // All useState, useEffect, useCallback and useRef logic lives in
  // useAppState. See hooks/useAppState.js for the full reading order.
  const {
    location, setLocationAndSave,
    forecast, airQuality, loading, error, now, fetchedAt,
    searchQuery, setSearchQuery, searchResults, setSearchResults, searching,
    selectedDay, setSelectedDay,
    proPromptDay, setProPromptDay,
    proPromptSource, setProPromptSource,
    dayTabsRef, canScrollLeft, canScrollRight, scrollDayTabs,
    isPro, setIsPro,
    activeProfile, setActiveProfile, activateProfile, activeCols,
    visibleCols, setVisibleCols, toggleCol,
	showDecimals, toggleShowDecimals,
	showUnits, toggleShowUnits,
	tableInterval, setTableInterval,
    activityOptions, placeOptions, workOptions,
    activityValue, activityLabel, placeValue, placeLabel, workValue, workLabel,
    skinType, setSkinType,
    vehicleType, setVehicleType,
    vehicleVent, setVehicleVent,
    outdoorsVariant, setOutdoorsVariantAndSave,
    buildingType, setBuildingType,
    indoorManaged, setIndoorManaged,
    indoorMode, setIndoorMode,
    pollenType, setPollenTypeAndSave,
    utciEnv, setUtciEnv,
    headStickyRef, headTrackRef, headTableRef,
    bodyScrollRef, bodyTableRef, tableWrapRef,
    colPopup, colPopupRef,
    handleThClick, handleThEnter, handleThLeave,
    handlePopupEnter, handlePopupLeave,
    eventTagPopup, eventTagPopupRef,
    evSlideIndex, evTransition, evSlideTo,
    handleEventTagClick, handleEventTagEnter, handleEventTagLeave,
    handleEventTagPopupEnter, handleEventTagPopupLeave,
    closePopup, closeEventTagPopup,
    tableCanScrollLeft, tableCanScrollRight, handleBodyScroll,
    hourlyRows, days, utcOffsetMs,
    visible, tableRows, nowLocalISO, currentRow, currentCat,
    liveElev,
    activeEvents, lensEvent, selectedDayEvents,
    bannerIndex, bannerTransition, bannerPrevIndex,
    bannerVisible, bannerStageRef,
    bannerSlideTo, dismissBanner,
  } = useAppState();

  // Search is hidden by default and revealed via the magnifier next to the
  // location. The input then overlays the location line to save space.
  const [searchOpen, setSearchOpen] = useState(false);
  const searchWrapRef = useRef(null);
  const searchInputRef = useRef(null);

  // Focus the input the moment the search opens.
  useEffect(() => {
    if (searchOpen) searchInputRef.current?.focus();
  }, [searchOpen]);

  // Close the search on outside-click or Escape, clearing any stray query.
  useEffect(() => {
    if (!searchOpen) return;
    const onDown = (e) => {
      if (searchWrapRef.current && !searchWrapRef.current.contains(e.target)) {
        setSearchOpen(false);
        setSearchQuery('');
      }
    };
    const onKey = (e) => {
      if (e.key === 'Escape') { setSearchOpen(false); setSearchQuery(''); }
    };
    document.addEventListener('mousedown', onDown);
    document.addEventListener('keydown', onKey);
    return () => {
      document.removeEventListener('mousedown', onDown);
      document.removeEventListener('keydown', onKey);
    };
  }, [searchOpen, setSearchQuery]);

	// Columns that mark the start of a logical group - used to draw a faint
	// vertical border separating groups in the forecast table.
	const GROUP_ORDER = {
	  felt:    ['utciP', 'vehicleT', 'indoorT', 'managedT', 'burn', 'utci', 'delta', 'tmrt'],
	  surface: ['concreteT', 'soilT', 'soilT6', 'soilM'],
	  ambient: ['air', 'rh', 'dew'],
	  precip:  ['precip', 'precipProb'],
	  sky:     ['cloud', 'vis'],
	  wind:    ['wind', 'dir'],
	  airqual: ['aqi', 'pollen'],
	  solar:   ['uvA', 'uvB', 'sun', 'direct', 'diffuse'],
	};
	const GROUP_OF = Object.fromEntries(
	  Object.entries(GROUP_ORDER).flatMap(([g, keys]) => keys.map(k => [k, g]))
	);
	// Returns col-group-start when this column is the leftmost VISIBLE member
	// of its group. If the canonical first member is hidden the border migrates
	// to the next visible column in the same group.
	const isColVisible = (k) => {
	  if (k === 'indoorT')  return indoorMode === 'on' && !indoorManaged;
	  if (k === 'managedT') return indoorMode === 'on' &&  indoorManaged;
	  return !!visibleCols[k];
	};
	const groupStart = (key) => {
	  const group = GROUP_OF[key];
	  if (!group) return '';
	  const first = GROUP_ORDER[group].find(isColVisible);
	  return first === key ? 'col-group-start' : '';
	};
	// Returns a CSS class encoding the group name - used to tint header cells
	// and group label spans.
	const groupColor = (key) => {
	  const g = GROUP_OF[key];
	  return g ? `grp-${g}` : '';
	};
	// Builds the group label row above the column headers.
	// Each visible group gets one spanning cell; groups with no visible
	// columns are skipped entirely. Hour always gets a blank lead cell.
	const groupLabelRow = () => {
	  const groups = Object.keys(GROUP_ORDER);
	  const cells = [html`<th class="grp-label-hour" scope="col"><span class="grp-label-date">${glanceDate}</span></th>`];
	  for (const g of groups) {
	    const span = GROUP_ORDER[g].filter(isColVisible).length;
	    if (span === 0) continue;
	    const labels = { felt: 'Felt', surface: 'Surface', ambient: 'Ambient', precip: 'Precip', sky: 'Sky', wind: 'Wind', airqual: 'Air quality', solar: 'Solar' };
	    cells.push(html`<th class=${`grp-label grp-label-${g} grp-${g}`} colspan=${span} scope="colgroup">${labels[g]}</th>`);
	  }
	  return html`<tr class="grp-label-row">${cells}</tr>`;
	};

  // Continuous temperature colour scale - hoisted so both the table columns
  // and the thermal stress legend can share the same function.
  const TEMP_STOPS = [
    [-10, [ 90, 155, 220]],
    [  0, [140, 195, 235]],
    [ 10, [155, 215, 195]],
    [ 16, [140, 210, 140]],
    [ 20, [195, 225, 110]],
    [ 24, [240, 225,  80]],
    [ 28, [250, 175,  65]],
    [ 32, [240, 120,  55]],
    [ 36, [220,  70,  50]],
    [ 40, [185,  30,  30]],
    [ 50, [130,   0,  20]],
  ];
  const airTempRgb = (t) => {
    if (t == null) return null;
    const clamp = (v, lo, hi) => Math.max(lo, Math.min(hi, v));
    let i = TEMP_STOPS.findIndex(s => t < s[0]);
    if (i === -1) i = TEMP_STOPS.length;
    const raw = i === 0 ? TEMP_STOPS[0][1]
      : i >= TEMP_STOPS.length ? TEMP_STOPS[TEMP_STOPS.length-1][1]
      : (() => {
          const [t0, c0] = TEMP_STOPS[i-1];
          const [t1, c1] = TEMP_STOPS[i];
          const x = clamp((t - t0) / (t1 - t0), 0, 1);
          const lerp = (a, b) => Math.round(a + (b - a) * x);
          return [lerp(c0[0],c1[0]), lerp(c0[1],c1[1]), lerp(c0[2],c1[2])];
        })();
    return raw.map(c => Math.min(255, Math.round(c + (255 - c) * 0.66)));
  };

  // ─── 3b. PANEL COMPUTATIONS ──────────────────────────────────────────
  // Derived from currentRow and the active day - no side effects.
  const whyFeelsLike  = computeWhyFeelsLike(currentRow ?? null, UTCI_ENVIRONMENTS[utciEnv]);
  const staleThreshMs = isPro ? 15 * 60 * 1000 : 30 * 60 * 1000;
  const isStale       = fetchedAt ? (now - fetchedAt) > staleThreshMs : false;
  const glanceSummary = computeGlanceSummary(
    days[selectedDay]?.rows ?? [],
    activeProfile,
    outdoorsVariant,
    skinType,
  );

  // Human-readable date for the "Day at a glance" heading, e.g. "Mon 2nd June 2026".
  const glanceDate = (() => {
    const key = days[selectedDay]?.key;
    if (!key) return '';
    const d = new Date(key + 'T00:00Z');
    const wd  = d.toLocaleDateString('en-GB', { weekday: 'short', timeZone: 'UTC' });
    const day = d.getUTCDate();
    const mon = d.toLocaleDateString('en-GB', { month: 'long', timeZone: 'UTC' });
    const yr  = d.getUTCFullYear();
    const ord = (n => { const s = ['th', 'st', 'nd', 'rd'], v = n % 100; return s[(v - 20) % 10] || s[v] || s[0]; })(day);
    return `${wd} ${day}${ord} ${mon} ${yr}`;
  })();

  // Day's events surfaced in the "Day at a glance" box. Reuses the same
  // row shape as glanceSummary items: { icon, label, value, alert }.
  const hhmm = (iso) => (iso ? iso.slice(11, 16) : '');
  const eventGlanceItems = (selectedDayEvents ?? [])
    .filter(ev => ev.type !== 'promo')
    .map(ev => ({
      icon: ev.emoji,
      label: ev.title,
      value: ev.isoRange
        ? (ev.isoRange[0] === ev.isoRange[1]
            ? hhmm(ev.isoRange[0])
            : `${hhmm(ev.isoRange[0])} – ${hhmm(ev.isoRange[1])}`)
        : (ev.nightOnly ? 'Overnight' : 'All day'),
      alert: false,
    }));

  // ─── 4. JSX RETURN ───────────────────────────────────────────────────
  // Everything below is the actual page markup, written as one big HTM
  // template. Search tips:
  //   • "utci-header"       — the top section (title + dial + search)
  //   • "utci-day-tabs"     — the 14 day buttons with band colours
  //   • "col-toggles"       — the column-customisation row (Pro only)
  //   • "utci-table"        — the hourly table itself
  //   • "utci-legend"       — the thermal-stress band legend
  //   • "utci-about"        — the explainer paragraphs at the bottom
  //   • "utci-footer"       — the "reading the table" note
  return html`
    <div class="utci-app">
      <nav class="utci-topnav" id="site-nav">
        <div class="nav-overlay" onClick=${() => { const n = document.getElementById('site-nav'); n.classList.remove('nav-open'); document.getElementById('nav-drawer').classList.remove('is-open'); }}></div>
        <div class="nav-logo" aria-hidden="true">
          <span class="nav-logo-wordmark"><span class="nav-logo-sun">Sun</span><svg class="nav-logo-icon" viewBox="0 0 174 173" aria-hidden="true" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:1.5;"><g transform="matrix(0.986226,0,0,0.986226,-585.724252,-110.041209)"><clipPath id="ss-clip-n"><circle cx="681.9838" cy="199.4502" r="83.2987"/></clipPath><g clip-path="url(#ss-clip-n)"><g transform="matrix(0.948836,0,0,0.948836,19.787839,-2.49381)"><path d="M666.992,233.0768C666.3949,230.6581 666.1785,228.6317 666.1785,226.0296C666.1785,208.6786 680.2653,194.5919 697.6163,194.5919C714.9672,194.5919 729.054,208.6786 729.054,226.0296C729.054,228.4125 728.9892,230.9346 728.4859,233.1663" fill="none" stroke="#c8922a" stroke-width="7.48"/></g><g transform="matrix(0.520349,0,0,0.520349,304.766642,64.226716)"><path d="M724.9315,291.8131C777.359,291.8131 826.5862,305.6245 869.1698,329.8044" fill="none" stroke="currentColor" stroke-width="13.64"/></g><g transform="matrix(0.520349,0,0,0.520349,304.766642,64.226716)"><path d="M580.8436,329.719C623.3927,305.592 672.5657,291.8131 724.9315,291.8131" fill="none" stroke="currentColor" stroke-width="13.64"/></g></g><circle cx="681.9838" cy="199.4502" r="83.2987" fill="none" stroke="currentColor" stroke-width="9.13"/></g></svg><span class="nav-logo-scope">Scope</span></span>
          <span class="nav-logo-tag">See the world the way your skin does.</span>
        </div>
        <div class="nav-links" id="nav-drawer">
          <a href="./index.html">Forecast</a>
          <a href="./about.html">About</a>
          <a href="./faq.html">FAQ</a>
          <a href=${isPro
              ? 'https://billing.stripe.com/p/login/9B63cw7vl8k15Ei9DQd7q00'
              : 'https://buy.stripe.com/9B63cw7vl8k15Ei9DQd7q00'}
             target="_blank" rel="noopener noreferrer">Account</a>
        </div>
        <button class="utci-burger" aria-label="Open menu" aria-expanded="false" id="burger-btn"
          onClick=${() => {
            const nav = document.getElementById('site-nav');
            const drawer = document.getElementById('nav-drawer');
            const btn = document.getElementById('burger-btn');
            const open = nav.classList.toggle('nav-open');
            drawer.classList.toggle('is-open', open);
            btn.setAttribute('aria-expanded', open ? 'true' : 'false');
          }}>
          <span></span><span></span><span></span>
        </button>
      </nav>
      <main class="utci-shell">

        
        <div class=${`event-banner-wrap${bannerVisible ? ' visible' : ''}`}>
          ${activeEvents.length > 0 && (() => {
            const renderSlide = (idx, isOutgoing) => {
              const ev = activeEvents[idx] || activeEvents[0];
              const fmtDate = (iso) => iso
                ? new Date(iso + 'T00:00Z').toLocaleDateString('en-GB', { day: 'numeric', month: 'short', timeZone: 'UTC' })
                : null;
              const startFmt = fmtDate(ev.start);
              const peakFmt  = fmtDate(ev.peak);
              const endFmt   = fmtDate(ev.end);
              const showDates = startFmt && endFmt;
              const dateLine = showDates
                ? (startFmt === endFmt
                    ? peakFmt ? `Peak: ${peakFmt}` : `Date: ${startFmt}`
                    : peakFmt && peakFmt !== startFmt && peakFmt !== endFmt
                      ? `Active ${startFmt} – ${endFmt} · Peak: ${peakFmt}`
                      : `Active ${startFmt} – ${endFmt}`)
                : null;
              return html`
                <div class=${isOutgoing ? 'event-banner event-banner--outgoing' : 'event-banner'}
                  style=${{ background: ev.color, color: ev.textColor }}
                >
                  <span class="event-banner-emoji">${ev.emoji}</span>
                  <div class="event-banner-body">
                    <div class="event-banner-title">${ev.title}</div>
                    <div class="event-banner-msg">${ev.message}</div>
                    ${dateLine && html`
                      <div class="event-banner-dates" style=${{ opacity: 0.75, fontSize: '11px', fontFamily: 'Manrope, sans-serif', fontWeight: 700, letterSpacing: '0.05em', textTransform: 'uppercase', marginTop: '5px' }}>
                        ${dateLine}
                      </div>
                    `}
                    ${activeEvents.length > 1 && html`
                      <div class="event-banner-dots">
                        ${activeEvents.map((_, i) => html`
                          <span
                            key=${i}
                            class=${`event-banner-dot${i === bannerIndex ? ' active' : ''}`}
                            onClick=${() => bannerSlideTo(i)}
                            style=${{ background: ev.textColor }}
                          />
                        `)}
                      </div>
                    `}
                  </div>
                  ${!isOutgoing && html`<button
                    class="event-banner-dismiss"
                    style=${{ color: ev.textColor }}
                    onClick=${() => dismissBanner(ev.id)}
                    aria-label="Dismiss"
                    title="Dismiss this event"
                  >✕</button>`}
                </div>`;
            };
            return html`
              <div class="event-banner-stage" ref=${bannerStageRef}>
                ${bannerTransition === 'crossfading' && bannerPrevIndex !== null
                  ? renderSlide(bannerPrevIndex, true)
                  : null}
                ${renderSlide(bannerIndex, false)}
              </div>`;
          })()}
        </div>

        <div class="utci-header">
          <div class="header-left">
            <h1 class="utci-title">
              <span class="title-sun">Sun</span><svg class="title-icon" viewBox="0 0 174 173" aria-hidden="true" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:1.5;"><g transform="matrix(0.986226,0,0,0.986226,-585.724252,-110.041209)"><clipPath id="ss-clip-t"><circle cx="681.9838" cy="199.4502" r="83.2987"/></clipPath><g clip-path="url(#ss-clip-t)"><g transform="matrix(0.948836,0,0,0.948836,19.787839,-2.49381)"><path d="M666.992,233.0768C666.3949,230.6581 666.1785,228.6317 666.1785,226.0296C666.1785,208.6786 680.2653,194.5919 697.6163,194.5919C714.9672,194.5919 729.054,208.6786 729.054,226.0296C729.054,228.4125 728.9892,230.9346 728.4859,233.1663" fill="none" stroke="#c8922a" stroke-width="7.48"/></g><g transform="matrix(0.520349,0,0,0.520349,304.766642,64.226716)"><path d="M724.9315,291.8131C777.359,291.8131 826.5862,305.6245 869.1698,329.8044" fill="none" stroke="currentColor" stroke-width="13.64"/></g><g transform="matrix(0.520349,0,0,0.520349,304.766642,64.226716)"><path d="M580.8436,329.719C623.3927,305.592 672.5657,291.8131 724.9315,291.8131" fill="none" stroke="currentColor" stroke-width="13.64"/></g></g><circle cx="681.9838" cy="199.4502" r="83.2987" fill="none" stroke="currentColor" stroke-width="9.13"/></g></svg><span class="title-scope">Scope</span>
            </h1>
            <div class="utci-tagline">See the world the way your skin does.</div>
            <div class="utci-loc-search" ref=${searchWrapRef}>
              <div class="utci-current-loc">
                <button
                  type="button"
                  class="utci-loc-name"
                  aria-label="Search for a town or city"
                  aria-expanded=${searchOpen}
                  onClick=${() => setSearchOpen((v) => !v)}
                >
                  <svg class="utci-loc-pin" viewBox="0 0 24 24" width="15" height="15" aria-hidden="true">
                    <path fill="currentColor" d="M12 2c-3.87 0-7 3.13-7 7 0 5.25 7 13 7 13s7-7.75 7-13c0-3.87-3.13-7-7-7z" />
                    <circle cx="12" cy="9" r="2.5" fill="#fffcf2" />
                  </svg>
                  ${location.name}
                </button>
                <button
                  type="button"
                  class="utci-search-toggle"
                  aria-label="Search for a town or city"
                  aria-expanded=${searchOpen}
                  onClick=${() => setSearchOpen((v) => !v)}
                >
                  <svg viewBox="0 0 24 24" width="16" height="16" aria-hidden="true">
                    <circle cx="10.5" cy="10.5" r="6.5" fill="none" stroke="currentColor" stroke-width="2" />
                    <line x1="15.5" y1="15.5" x2="21" y2="21" stroke="currentColor" stroke-width="2" stroke-linecap="round" />
                  </svg>
                </button>
                <span class="utci-loc-coords">
                  ${location.lat.toFixed(3)}°, ${location.lon.toFixed(3)}°
                </span>
              </div>
              ${fetchedAt && !loading && html`
                <div class=${'utci-fetch-time' + (isStale ? ' is-stale' : '')}>
                  ${isStale ? html`<span class="stale-dot" title="Data is older than expected - retrying">● </span>` : ''}Last update: ${fetchedAt.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}
                </div>`}
              ${searchOpen && html`
                <div class="utci-search-wrap utci-search-overlay">
                  <input
                    ref=${searchInputRef}
                    class="utci-search"
                    type="text"
                    placeholder="Search any town or city…"
                    value=${searchQuery}
                    onInput=${(e) => setSearchQuery(e.currentTarget.value)}
                  />
                  ${searchResults.length > 0 && html`
                    <div class="utci-results">
                      ${searchResults.map((r) => html`
                        <div
                          key=${`${r.id}-${r.latitude}`}
                          class="utci-result"
                          onClick=${() => {
                            setLocationAndSave({
                              name: `${r.name}${r.admin1 ? ', ' + r.admin1 : ''}`,
                              lat: r.latitude,
                              lon: r.longitude,
                              country: r.country_code,
                            });
                            setSearchQuery('');
                            setSearchResults([]);
                            setSelectedDay(0);
                            setSearchOpen(false);
                          }}
                        >
                          <div>${r.name}${r.admin1 ? `, ${r.admin1}` : ''}</div>
                          <div class="utci-result-meta">
                            ${r.country} · ${r.latitude.toFixed(2)}°, ${r.longitude.toFixed(2)}°
                          </div>
                        </div>`)}
                    </div>`}
                  ${searching && html`<div class="utci-searching">Searching…</div>`}
                </div>`}
            </div>
          </div>

          <div class="scope-col">
            <${ScopeReticle}
              value=${currentRow?.utciAdj ?? null}
              cat=${currentCat}
              loading=${loading}
              elev=${liveElev ?? currentRow?.elev ?? 0}
              dt=${now}
              glob=${currentRow?.glob ?? 0}
              activeEvent=${lensEvent}
              utciEnvShort=${UTCI_ENVIRONMENTS[utciEnv]?.shortLabel ?? null}
            />
          </div>

          <div class="scope-env-bar">
            <div class="scope-env-row" data-env=${utciEnv}>
              <span class="scope-env-label">Environment</span>
              <${CustomSelect}
                value=${utciEnv}
                isOn=${true}
                noHide=${true}
                grpClass="grp-felt"
                options=${Object.entries(UTCI_ENVIRONMENTS).map(([k, v]) => ({
                  value: k,
                  label: v.label,
                }))}
                onChange=${(v) => setUtciEnv(v)}
              />
            </div>
          </div>

          <div class="header-right">
            ${forecast && currentRow && whyFeelsLike && html`
              <div class="insight-panel insight-panel--why">
                <div class="insight-panel-title">Why it feels like this</div>
                ${[
                  { label: 'Sun and sky',    icon: '☀',  value: whyFeelsLike.sunAndSky },
                  { label: 'Wind',           icon: '🌬',  value: whyFeelsLike.wind },
                  { label: 'Humidity',       icon: '💧',  value: whyFeelsLike.humidity },
                  ...(whyFeelsLike.environment !== 0 ? [
                    { label: UTCI_ENVIRONMENTS[utciEnv]?.label ?? 'Environment', icon: '🌍', value: whyFeelsLike.environment }
                  ] : []),
                  ...(whyFeelsLike.precipitation !== 0 ? [
                    { label: 'Precipitation', icon: '🌧', value: whyFeelsLike.precipitation }
                  ] : []),
                ].map(({ label, icon, value }) => html`
                  <div class="insight-row" key=${label}>
                    <span class="insight-icon">${icon}</span>
                    <span class="insight-label">${label}</span>
                    <span class=${`insight-delta ${value >= 0 ? 'delta-pos' : 'delta-neg'}`}>
                      ${value >= 0 ? '+' : ''}${value.toFixed(1)}°
                    </span>
                  </div>
                `)}
              </div>`}
          </div>
		</div>

        ${error && html`
          <div class="utci-status" style=${{ borderColor: '#3a1010', color: '#c44a3a' }}>
            ⚠ ${error}
          </div>`}
        ${loading && !error && html`
          <div class="acquiring-overlay">
            <div class="acquiring-popup">
              <div class="acquiring-spinner"></div>
              <div class="acquiring-label">Acquiring forecast data…</div>
            </div>
          </div>`}

        ${forecast && days.length > 0 && html`<${DayTabs}
          days=${days}
          selectedDay=${selectedDay} setSelectedDay=${setSelectedDay}
          isPro=${isPro}
          proPromptDay=${proPromptDay} setProPromptDay=${setProPromptDay}
          proPromptSource=${proPromptSource} setProPromptSource=${setProPromptSource}
          activeProfile=${activeProfile} activateProfile=${activateProfile} activeCols=${activeCols}
          visibleCols=${visibleCols}
          activityOptions=${activityOptions} placeOptions=${placeOptions} workOptions=${workOptions}
          activityValue=${activityValue} activityLabel=${activityLabel}
          placeValue=${placeValue} placeLabel=${placeLabel}
          workValue=${workValue} workLabel=${workLabel}
          outdoorsVariant=${outdoorsVariant} setOutdoorsVariantAndSave=${setOutdoorsVariantAndSave}
          setActiveProfile=${setActiveProfile} setVisibleCols=${setVisibleCols}
          setIndoorMode=${setIndoorMode} setIndoorManaged=${setIndoorManaged}
          setBuildingType=${setBuildingType}
          dayTabsRef=${dayTabsRef} canScrollLeft=${canScrollLeft} canScrollRight=${canScrollRight}
          scrollDayTabs=${scrollDayTabs}
        />`}

            
            <div class="table-with-rail">
            <div class="table-main">
            <div class="col-toggles">
                <span class="col-toggles-label">Columns:</span>
                ${(isPro ? (activeProfile === 'custom' || activeProfile === 'showall' || activeCols['utciP']) : activeCols['utciP']) && html`
				  <span class=${'col-toggle-group' + (visibleCols.utciP ? ' col-toggle-group--expanded' : '')}>
					<${CustomSelect}
					  value=${utciEnv}
					  isOn=${visibleCols.utciP}
					  grpClass="grp-felt"
					  hideLabel="SunSoak"
					  hidingLabel="Hide SunSoak"
					  groupedLeft=${true}
					  isLastChild=${true}
					  options=${Object.entries(UTCI_ENVIRONMENTS).map(([k, v]) => ({
						value: k,
						label: v.label,
					  }))}
					  onChange=${(v) => {
						if (v === 'off') {
						  setVisibleCols(prev => ({ ...prev, utciP: false }));
						} else {
						  setUtciEnv(v);
						  setVisibleCols(prev => ({ ...prev, utciP: true }));
						}
					  }}
					/>
				  </span>`}
                ${(isPro ? (activeProfile === 'custom' || activeProfile === 'showall' || activeCols['vehicleT']) : activeCols['vehicleT']) && html`
                  <span class=${'col-toggle-group' + (visibleCols.vehicleT ? ' col-toggle-group--expanded' : '')}>
                    <${CustomSelect}
                      value=${vehicleType}
                      isOn=${visibleCols.vehicleT}
                      grpClass="grp-felt"
                      hideLabel="Vehicle"
                      hidingLabel="Hide Vehicle"
                      groupedLeft=${true}
                      isLastChild=${!visibleCols.vehicleT}
                      options=${Object.entries(VEHICLE_TYPES).map(([k, v]) => ({
                        value: k,
                        label: v.name,
                      }))}
                      onChange=${(v) => {
                        if (v === 'off') {
                          setVisibleCols(prev => ({ ...prev, vehicleT: false }));
                          setVehicleVent(false);
                        } else {
                          setVehicleType(v);
                          setVisibleCols(prev => ({ ...prev, vehicleT: true }));
                        }
                      }}
                    />
                    ${visibleCols.vehicleT && html`
                      <${VentPill}
                        checked=${vehicleVent}
                        onChange=${() => setVehicleVent(v => !v)}
                        grpClass="grp-felt"
                        label="Ventilation"
                        title="Ventilation — open windows significantly reduce cabin heat build-up"
                      />`}
                  </span>`}
                ${(isPro ? (activeProfile === 'custom' || activeProfile === 'showall' || activeCols['indoorT'] || activeCols['managedT']) : (activeCols['indoorT'] || activeCols['managedT'])) && html`
                  <span class=${'col-toggle-group' + (indoorMode === 'on' ? ' col-toggle-group--expanded' : '')}>
                    <${CustomSelect}
                      value=${buildingType}
                      isOn=${indoorMode === 'on'}
                      grpClass="grp-felt"
                      hideLabel="Indoors"
                      hidingLabel="Hide Indoors"
                      groupedLeft=${true}
                      isLastChild=${indoorMode !== 'on'}
                      options=${Object.entries(BUILDING_TYPES).map(([k, v]) => ({
                        value: k,
                        label: v.name,
                      }))}
                      onChange=${(v) => {
                        if (v === 'off') {
                          setIndoorMode('off');
                          setIndoorManaged(false);
                        } else {
                          setBuildingType(v);
                          setIndoorMode('on');
                        }
                      }}
                    />
                    ${indoorMode === 'on' && html`
                      <${VentPill}
                        checked=${indoorManaged}
                        onChange=${() => setIndoorManaged(v => !v)}
                        grpClass="grp-felt"
                        label="Managed"
                        title="Managed: curtains closed by day, windows open when cooler outside"
                      />`}
                  </span>`}
                ${(isPro ? (activeProfile === 'custom' || activeProfile === 'showall' || activeCols['burn']) : activeCols['burn']) && html`
                  <${CustomSelect}
                    value=${skinType}
                    isOn=${visibleCols.burn}
                    grpClass="grp-felt"
                    hideLabel="Burn"
                    hidingLabel="Hide Burn"
                    options=${Object.entries(SKIN_TYPES).map(([k, v]) => ({
                      value: k,
                      label: v.name.split(' · ')[1] + ' skin',
                    }))}
                    onChange=${(v) => {
                      if (v === 'off') {
                        setVisibleCols(prev => ({ ...prev, burn: false }));
                      } else {
                        setSkinType(v);
                        setVisibleCols(prev => ({ ...prev, burn: true }));
                      }
                    }}
                  />`}
                ${isPro && (activeProfile === 'custom' || activeProfile === 'showall' || activeCols['utci']) && html`<button class=${`col-toggle grp-felt${visibleCols.utci ? ' on' : ''}`} onClick=${() => toggleCol('utci')}>UTCI</button>`}
                ${isPro && (activeProfile === 'custom' || activeProfile === 'showall' || activeCols['delta']) && html`<button class=${`col-toggle grp-felt${visibleCols.delta ? ' on' : ''}`} onClick=${() => toggleCol('delta')}>Δ</button>`}
                ${isPro && (activeProfile === 'custom' || activeProfile === 'showall' || activeCols['tmrt']) && html`<button class=${`col-toggle grp-felt${visibleCols.tmrt ? ' on' : ''}`} onClick=${() => toggleCol('tmrt')}>Tmrt</button>`}
                ${isPro && (activeProfile === 'custom' || activeProfile === 'showall' || activeCols['concreteT']) && html`<button class=${`col-toggle grp-surface${visibleCols.concreteT ? ' on' : ''}`} onClick=${() => toggleCol('concreteT')}>Concrete</button>`}
                ${isPro && (activeProfile === 'custom' || activeProfile === 'showall' || activeCols['soilT']) && html`<button class=${`col-toggle grp-surface${visibleCols.soilT ? ' on' : ''}`} onClick=${() => toggleCol('soilT')}>Soil °C</button>`}
                ${isPro && (activeProfile === 'custom' || activeProfile === 'showall' || activeCols['soilT6']) && html`<button class=${`col-toggle grp-surface${visibleCols.soilT6 ? ' on' : ''}`} onClick=${() => toggleCol('soilT6')}>Soil 6cm</button>`}
                ${isPro && (activeProfile === 'custom' || activeProfile === 'showall' || activeCols['soilM']) && html`<button class=${`col-toggle grp-surface${visibleCols.soilM ? ' on' : ''}`} onClick=${() => toggleCol('soilM')}>Soil moist</button>`}
                ${isPro && (activeProfile === 'custom' || activeProfile === 'showall' || activeCols['air']) && html`<button class=${`col-toggle grp-ambient${visibleCols.air ? ' on' : ''}`} onClick=${() => toggleCol('air')}>Air</button>`}
                ${isPro && (activeProfile === 'custom' || activeProfile === 'showall' || activeCols['rh']) && html`<button class=${`col-toggle grp-ambient${visibleCols.rh ? ' on' : ''}`} onClick=${() => toggleCol('rh')}>RH</button>`}
                ${isPro && (activeProfile === 'custom' || activeProfile === 'showall' || activeCols['dew']) && html`<button class=${`col-toggle grp-ambient${visibleCols.dew ? ' on' : ''}`} onClick=${() => toggleCol('dew')}>Dew</button>`}
                ${isPro && (activeProfile === 'custom' || activeProfile === 'showall' || activeCols['precip']) && html`<button class=${`col-toggle grp-precip${visibleCols.precip ? ' on' : ''}`} onClick=${() => toggleCol('precip')}>Precip</button>`}
                ${isPro && (activeProfile === 'custom' || activeProfile === 'showall' || activeCols['precipProb']) && html`<button class=${`col-toggle grp-precip${visibleCols.precipProb ? ' on' : ''}`} onClick=${() => toggleCol('precipProb')}>Rain%</button>`}
                ${isPro && (activeProfile === 'custom' || activeProfile === 'showall' || activeCols['cloud']) && html`<button class=${`col-toggle grp-sky${visibleCols.cloud ? ' on' : ''}`} onClick=${() => toggleCol('cloud')}>Cloud</button>`}
                ${isPro && (activeProfile === 'custom' || activeProfile === 'showall' || activeCols['vis']) && html`<button class=${`col-toggle grp-sky${visibleCols.vis ? ' on' : ''}`} onClick=${() => toggleCol('vis')}>Visibility</button>`}
                ${isPro && (activeProfile === 'custom' || activeProfile === 'showall' || activeCols['wind']) && html`<button class=${`col-toggle grp-wind${visibleCols.wind ? ' on' : ''}`} onClick=${() => toggleCol('wind')}>Wind</button>`}
                ${isPro && (activeProfile === 'custom' || activeProfile === 'showall' || activeCols['dir']) && html`<button class=${`col-toggle grp-wind${visibleCols.dir ? ' on' : ''}`} onClick=${() => toggleCol('dir')}>Dir</button>`}
                ${isPro && (activeProfile === 'custom' || activeProfile === 'showall' || activeCols['aqi']) && html`<button class=${`col-toggle grp-airqual${visibleCols.aqi ? ' on' : ''}`} onClick=${() => toggleCol('aqi')}>Air Quality</button>`}
                ${(isPro ? (activeProfile === 'custom' || activeProfile === 'showall' || activeCols['pollen']) : activeCols['pollen']) && html`<${CustomSelect}
                  value=${pollenType}
                  isOn=${visibleCols.pollen}
                  grpClass="grp-airqual"
                  hideLabel="Pollen"
                  hidingLabel="Hide Pollen"
                  options=${Object.entries(POLLEN_TYPES).flatMap(([k, v], i) => [
                    { value: k, label: v.name },
                    ...(i === 0 ? [{ value: '_div', divider: true }] : []),
                  ])}
                  onChange=${(v) => {
                    if (v === 'off') {
                      setVisibleCols(prev => ({ ...prev, pollen: false }));
                    } else {
                      setPollenTypeAndSave(v);
                      setVisibleCols(prev => ({ ...prev, pollen: true }));
                    }
                  }}
                />`}
                ${isPro && (activeProfile === 'custom' || activeProfile === 'showall' || activeCols['uvA']) && html`<button class=${`col-toggle grp-solar${visibleCols.uvA ? ' on' : ''}`} onClick=${() => toggleCol('uvA')}>UV-A</button>`}
                ${isPro && (activeProfile === 'custom' || activeProfile === 'showall' || activeCols['uvB']) && html`<button class=${`col-toggle grp-solar${visibleCols.uvB ? ' on' : ''}`} onClick=${() => toggleCol('uvB')}>UV-B</button>`}
                ${isPro && (activeProfile === 'custom' || activeProfile === 'showall' || activeCols['sun']) && html`<button class=${`col-toggle grp-solar${visibleCols.sun ? ' on' : ''}`} onClick=${() => toggleCol('sun')}>Sun</button>`}
                ${isPro && (activeProfile === 'custom' || activeProfile === 'showall' || activeCols['direct']) && html`<button class=${`col-toggle grp-solar${visibleCols.direct ? ' on' : ''}`} onClick=${() => toggleCol('direct')}>Direct</button>`}
                ${isPro && (activeProfile === 'custom' || activeProfile === 'showall' || activeCols['diffuse']) && html`<button class=${`col-toggle grp-solar${visibleCols.diffuse ? ' on' : ''}`} onClick=${() => toggleCol('diffuse')}>Diffuse</button>`}

                <span class="col-toggles-display-opts">
                  <label class="display-opt">
                    <input type="checkbox" checked=${showDecimals} onChange=${toggleShowDecimals} />
                    Decimals
                  </label>
                  <label class="display-opt">
                    <input type="checkbox" checked=${showUnits} onChange=${toggleShowUnits} />
                    Units
                  </label>
                </span>
            </div>

            <div class=${`utci-table-wrap${tableCanScrollLeft ? ' scroll-fade-left' : ''}${tableCanScrollRight ? ' scroll-fade-right' : ''}${showUnits ? '' : ' no-units'}`} ref=${tableWrapRef}>
              <span class="utci-scroll-chevron left" aria-hidden="true">‹</span>
              <span class="utci-scroll-chevron right" aria-hidden="true">›</span>
              
              <div class="utci-thead-sticky" ref=${headStickyRef}>
                <div class="utci-thead-track" ref=${headTrackRef}>
                  <table class="utci-table utci-table-head" ref=${headTableRef}>
                    <thead>
                      ${groupLabelRow()}
                      <tr>
						<th class="col-info-th" scope="col" onClick=${(e) => handleThClick('hour', e)} onMouseEnter=${(e) => handleThEnter('hour', e)} onMouseLeave=${handleThLeave}>
							<span>Hour</span>
							<span class="hour-interval" onClick=${(e) => e.stopPropagation()}>
								${[1, 2, 3, 4].map(n => html`
									<button key=${n} type="button"
										class=${`hour-interval-btn${tableInterval === n ? ' on' : ''}`}
										title=${n === 1 ? 'Every hour' : `Every ${n} hours`}
										onClick=${(e) => { e.stopPropagation(); setTableInterval(n); }}
									>${n}h</button>`)}
							</span>
						</th>
						${visibleCols.utciP   && html`<th class=${`col-info-th ${groupStart('utciP')} ${groupColor('utciP')}`} scope="col" onClick=${(e) => handleThClick('utciP', e)} onMouseEnter=${(e) => handleThEnter('utciP', e)} onMouseLeave=${handleThLeave}>SunSoak <span class="col-unit">°C felt</span>${UTCI_ENVIRONMENTS[utciEnv]?.shortLabel ? html`<span class="col-env-badge">${UTCI_ENVIRONMENTS[utciEnv].shortLabel}</span>` : null}</th>`}
						${visibleCols.vehicleT  && html`<th class=${`col-info-th ${groupStart('vehicleT')} ${groupColor('vehicleT')}`} scope="col" onClick=${(e) => handleThClick('vehicleT', e)} onMouseEnter=${(e) => handleThEnter('vehicleT', e)} onMouseLeave=${handleThLeave}>Vehicle <span class="col-unit">°C peak</span></th>`}
						${indoorMode === 'on' && !indoorManaged && html`<th class=${`col-info-th ${groupStart('indoorT')} ${groupColor('indoorT')}`} scope="col" onClick=${(e) => handleThClick('indoorT', e)} onMouseEnter=${(e) => handleThEnter('indoorT', e)} onMouseLeave=${handleThLeave}>Indoors <span class="col-unit">°C est.</span></th>`}
						${indoorMode === 'on' &&  indoorManaged && html`<th class=${`col-info-th ${groupStart('managedT')} ${groupColor('managedT')}`} scope="col" onClick=${(e) => handleThClick('managedT', e)} onMouseEnter=${(e) => handleThEnter('managedT', e)} onMouseLeave=${handleThLeave}>Managed <span class="col-unit">°C est.</span></th>`}
						${visibleCols.burn    && html`<th class=${`col-info-th ${groupStart('burn')} ${groupColor('burn')}`} scope="col" onClick=${(e) => handleThClick('burn', e)} onMouseEnter=${(e) => handleThEnter('burn', e)} onMouseLeave=${handleThLeave}>Burn <span class="col-unit">to MED</span></th>`}
						${visibleCols.utci    && html`<th class=${`col-info-th ${groupStart('utci')} ${groupColor('utci')}`} scope="col" onClick=${(e) => handleThClick('utci', e)} onMouseEnter=${(e) => handleThEnter('utci', e)} onMouseLeave=${handleThLeave}>UTCI <span class="col-unit">°C felt</span></th>`}
						${visibleCols.delta   && html`<th class=${`col-info-th ${groupStart('delta')} ${groupColor('delta')}`} scope="col" onClick=${(e) => handleThClick('delta', e)} onMouseEnter=${(e) => handleThEnter('delta', e)} onMouseLeave=${handleThLeave}>Δ <span class="col-unit">UTCI−Air</span></th>`}
						${visibleCols.tmrt    && html`<th class=${`col-info-th ${groupStart('tmrt')} ${groupColor('tmrt')}`} scope="col" onClick=${(e) => handleThClick('tmrt', e)} onMouseEnter=${(e) => handleThEnter('tmrt', e)} onMouseLeave=${handleThLeave}>Tmrt <span class="col-unit">°C</span></th>`}
						${visibleCols.concreteT && html`<th class=${`col-info-th ${groupStart('concreteT')} ${groupColor('concreteT')}`} scope="col" onClick=${(e) => handleThClick('concreteT', e)} onMouseEnter=${(e) => handleThEnter('concreteT', e)} onMouseLeave=${handleThLeave}>Concrete <span class="col-unit">°C surface</span></th>`}
						${visibleCols.soilT   && html`<th class=${`col-info-th ${groupStart('soilT')} ${groupColor('soilT')}`} scope="col" onClick=${(e) => handleThClick('soilT', e)} onMouseEnter=${(e) => handleThEnter('soilT', e)} onMouseLeave=${handleThLeave}>Soil °C <span class="col-unit">surface</span></th>`}
						${visibleCols.soilT6  && html`<th class=${`col-info-th ${groupStart('soilT6')} ${groupColor('soilT6')}`} scope="col" onClick=${(e) => handleThClick('soilT6', e)} onMouseEnter=${(e) => handleThEnter('soilT6', e)} onMouseLeave=${handleThLeave}>Soil 6cm <span class="col-unit">°C root</span></th>`}
						${visibleCols.soilM   && html`<th class=${`col-info-th ${groupStart('soilM')} ${groupColor('soilM')}`} scope="col" onClick=${(e) => handleThClick('soilM', e)} onMouseEnter=${(e) => handleThEnter('soilM', e)} onMouseLeave=${handleThLeave}>Soil moist <span class="col-unit">%</span></th>`}
						${visibleCols.air     && html`<th class=${`utci-tight-head col-info-th ${groupStart('air')} ${groupColor('air')}`} scope="col" onClick=${(e) => handleThClick('air', e)} onMouseEnter=${(e) => handleThEnter('air', e)} onMouseLeave=${handleThLeave}>Air <span class="col-unit">°C</span></th>`}
						${visibleCols.rh      && html`<th class=${`col-info-th ${groupStart('rh')} ${groupColor('rh')}`} scope="col" onClick=${(e) => handleThClick('rh', e)} onMouseEnter=${(e) => handleThEnter('rh', e)} onMouseLeave=${handleThLeave}>RH <span class="col-unit">%</span></th>`}
						${visibleCols.dew     && html`<th class=${`col-info-th ${groupStart('dew')} ${groupColor('dew')}`} scope="col" onClick=${(e) => handleThClick('dew', e)} onMouseEnter=${(e) => handleThEnter('dew', e)} onMouseLeave=${handleThLeave}>Dew <span class="col-unit">°C</span></th>`}
						${visibleCols.precip  && html`<th class=${`utci-tight-head col-info-th ${groupStart('precip')} ${groupColor('precip')}`} scope="col" onClick=${(e) => handleThClick('precip', e)} onMouseEnter=${(e) => handleThEnter('precip', e)} onMouseLeave=${handleThLeave}>Pcpt <span class="col-unit">mm/h</span></th>`}
						${visibleCols.precipProb && html`<th class=${`utci-tight-head col-info-th ${groupStart('precipProb')} ${groupColor('precipProb')}`} scope="col" onClick=${(e) => handleThClick('precipProb', e)} onMouseEnter=${(e) => handleThEnter('precipProb', e)} onMouseLeave=${handleThLeave}>Rain <span class="col-unit">%</span></th>`}
						${visibleCols.cloud   && html`<th class=${`col-info-th ${groupStart('cloud')} ${groupColor('cloud')}`} scope="col" onClick=${(e) => handleThClick('cloud', e)} onMouseEnter=${(e) => handleThEnter('cloud', e)} onMouseLeave=${handleThLeave}>Cloud <span class="col-unit">%</span></th>`}
						${visibleCols.vis     && html`<th class=${`utci-tight-head col-info-th ${groupStart('vis')} ${groupColor('vis')}`} scope="col" onClick=${(e) => handleThClick('vis', e)} onMouseEnter=${(e) => handleThEnter('vis', e)} onMouseLeave=${handleThLeave}>Vis <span class="col-unit">km</span></th>`}
						${visibleCols.wind    && html`<th class=${`col-info-th ${groupStart('wind')} ${groupColor('wind')}`} scope="col" onClick=${(e) => handleThClick('wind', e)} onMouseEnter=${(e) => handleThEnter('wind', e)} onMouseLeave=${handleThLeave}>Wind <span class="col-unit">mph (gust)</span></th>`}
						${visibleCols.dir     && html`<th class=${`utci-dir-cell col-info-th ${groupStart('dir')} ${groupColor('dir')}`} scope="col" onClick=${(e) => handleThClick('dir', e)} onMouseEnter=${(e) => handleThEnter('dir', e)} onMouseLeave=${handleThLeave}>Dir <span class="col-unit">-</span></th>`}
						${visibleCols.aqi     && html`<th class=${`col-info-th ${groupStart('aqi')} ${groupColor('aqi')}`} scope="col" onClick=${(e) => handleThClick('aqi', e)} onMouseEnter=${(e) => handleThEnter('aqi', e)} onMouseLeave=${handleThLeave}>AQI <span class="col-unit">EU idx</span></th>`}
						${visibleCols.pollen  && html`<th class=${`col-info-th ${groupStart('pollen')} ${groupColor('pollen')}`} scope="col" onClick=${(e) => handleThClick('pollen', e)} onMouseEnter=${(e) => handleThEnter('pollen', e)} onMouseLeave=${handleThLeave}>${pollenType === 'all_pollen' ? 'Pollen' : POLLEN_TYPES[pollenType]?.name.replace(' pollen','') ?? 'Pollen'} <span class="col-unit">grains/m³</span></th>`}
						${visibleCols.uvA     && html`<th class=${`col-info-th ${groupStart('uvA')} ${groupColor('uvA')}`} scope="col" onClick=${(e) => handleThClick('uvA', e)} onMouseEnter=${(e) => handleThEnter('uvA', e)} onMouseLeave=${handleThLeave}>UV-A <span class="col-unit">est. idx</span></th>`}
						${visibleCols.uvB     && html`<th class=${`col-info-th ${groupStart('uvB')} ${groupColor('uvB')}`} scope="col" onClick=${(e) => handleThClick('uvB', e)} onMouseEnter=${(e) => handleThEnter('uvB', e)} onMouseLeave=${handleThLeave}>UV-B <span class="col-unit">est. idx</span></th>`}
						${visibleCols.sun     && html`<th class=${`col-info-th ${groupStart('sun')} ${groupColor('sun')}`} scope="col" onClick=${(e) => handleThClick('sun', e)} onMouseEnter=${(e) => handleThEnter('sun', e)} onMouseLeave=${handleThLeave}>Sun <span class="col-unit">elev°</span></th>`}
						${visibleCols.direct  && html`<th class=${`col-info-th ${groupStart('direct')} ${groupColor('direct')}`} scope="col" onClick=${(e) => handleThClick('direct', e)} onMouseEnter=${(e) => handleThEnter('direct', e)} onMouseLeave=${handleThLeave}>Direct <span class="col-unit">W/m²</span></th>`}
						${visibleCols.diffuse && html`<th class=${`col-info-th ${groupStart('diffuse')} ${groupColor('diffuse')}`} scope="col" onClick=${(e) => handleThClick('diffuse', e)} onMouseEnter=${(e) => handleThEnter('diffuse', e)} onMouseLeave=${handleThLeave}>Diffuse <span class="col-unit">W/m²</span></th>`}
                      </tr>
                    </thead>
                  </table>
                </div>
              </div>
              
              <div class="utci-tbody-scroll" ref=${bodyScrollRef} onScroll=${handleBodyScroll}>
                <table class="utci-table utci-table-body" ref=${bodyTableRef}>
                  <tbody>
                  ${tableRows.map((r, rowIdx) => {
                    const rPrev = tableRows[rowIdx - 1];
                    const rNext = tableRows[rowIdx + 1];
                    const cat = utciCategory(r.utci);
                    const isNight = r.elev < 0;
                    const isNow = r.isoHours ? r.isoHours.includes(nowLocalISO) : r.iso.slice(0, 13) === nowLocalISO;
                    const delta = r.utci - r.Ta;
                    const adjCat = utciCategory(r.utciAdj);
                    // Converts a band hex colour to rgba at the given alpha — used to
                    // tint the UTCI and UTCI+P cells directly from the band colour.
                    const hexToRgba = (hex, a) => {
                      const h = hex.replace('#', '');
                      const r = parseInt(h.slice(0,2),16);
                      const g = parseInt(h.slice(2,4),16);
                      const b = parseInt(h.slice(4,6),16);
                      return `rgba(${r},${g},${b},${a})`;
                    };
				const fmt = (v, dp = 1) => v == null ? '—' : (showDecimals ? v.toFixed(dp) : String(Math.round(v)));
                const u = (unit) => showUnits ? unit : '';
                // Returns a background tint based on temperature value - all share the same
                // Continuous temperature colour scale — used for all temp columns.
                // airTempRgb(t) returns the whiteblended [r,g,b] for a temperature.
                // airTempBg(t, tPrev, tNext) returns a top→bottom gradient that
                // flows from the previous row's colour through this row's colour to
                // the next row's colour, making the whole column one seamless heatmap.
                // airTempRgb and TEMP_STOPS hoisted to component scope above.
                const toRgb = (rgb) => rgb ? `rgb(${rgb[0]},${rgb[1]},${rgb[2]})` : 'transparent';
                const airTempBg = (t, tPrev, tNext) => {
                  if (t == null) return 'transparent';
                  // Top colour = midpoint temp between prev row and this row.
                  // Bottom colour = midpoint temp between this row and next row.
                  // This guarantees the boundary colour is identical on both sides
                  // of every cell edge, giving a perfectly seamless column gradient.
                  const tTop = tPrev != null ? (tPrev + t) / 2 : t;
                  const tBot = tNext != null ? (t + tNext) / 2 : t;
                  return `linear-gradient(to bottom, ${toRgb(airTempRgb(tTop))} 0%, ${toRgb(airTempRgb(tBot))} 100%)`;
                };
                const airTempFontColor = () => '#1a1a1a';
                // Returns any extra band styles (fontWeight, textShadow) for a given temp.
                const bandExtras = (t) => {
                  const band = utciCategory(t);
                  if (!band) return {};
                  return {
                    ...(band.fontWeight  ? { fontWeight:  band.fontWeight  } : {}),
                    ...(band.textShadow  ? { textShadow:  band.textShadow  } : {}),
                  };
                };
                const scaleBg = (v, min, max, rgb, maxAlpha = 0.14) => {
                      if (v == null || isNaN(v)) return 'transparent';
                      if (v <= min) return 'transparent';
                      const x = Math.max(0, Math.min(1, (v - min) / (max - min)));
                      const a = 0.035 + x * maxAlpha;
                      return `rgba(${rgb},${a.toFixed(3)})`;
                    };
                    const soilMoistureBg = (v) => {
                      if (v == null || isNaN(v)) return { bg: 'transparent', fg: '#555' };
                      const pct = v * 100;
                      // stops: [pct, r, g, b]
                      const stops = [
                        [0,  212, 180, 131],  // sandy tan  – bone dry
                        [10, 160, 120,  64],  // mid brown  – dry
                        [22, 138, 122,  80],  // olive-brown – damp/firm
                        [32, 200, 160,  64],  // amber       – soft, caution
                        [38,  58, 138,  92],  // green       – wet
                        [50,  42, 106, 144],  // blue        – saturated
                      ];
                      let s0 = stops[0], s1 = stops[stops.length - 1];
                      for (let i = 0; i < stops.length - 1; i++) {
                        if (pct >= stops[i][0] && pct <= stops[i+1][0]) { s0 = stops[i]; s1 = stops[i+1]; break; }
                      }
                      const t  = s1[0] === s0[0] ? 1 : Math.max(0, Math.min(1, (pct - s0[0]) / (s1[0] - s0[0])));
                      const r  = Math.round(s0[1] + t * (s1[1] - s0[1]));
                      const g  = Math.round(s0[2] + t * (s1[2] - s0[2]));
                      const b  = Math.round(s0[3] + t * (s1[3] - s0[3]));
                      const lum = 0.299*r + 0.587*g + 0.114*b;
                      return { bg: `rgba(${r},${g},${b},0.18)`, fg: '#3a2a10' };
                    };
                    const deltaBg = (v) => {
                      if (v == null || isNaN(v)) return 'transparent';
                      if (Math.abs(v) < 1) return 'transparent';
                      const x = Math.min(1, Math.abs(v) / 12);
                      const a = 0.035 + x * 0.13;
                      return v > 0 ? `rgba(230,140,50,${a.toFixed(3)})` : `rgba(80,135,210,${a.toFixed(3)})`;
                    };
                    const burnBg = (mins, uv) => {
                      if (!isFinite(mins) || uv <= 0) return 'transparent';
                      if (mins >= 240) return 'transparent';
                      const x = Math.max(0, Math.min(1, (240 - mins) / 220));
                      return `rgba(210,70,50,${(0.04 + x * 0.15).toFixed(3)})`;
                    };
                    // r.iso is the local wall-clock string from the API — slice it directly.
                    const h24 = parseInt(r.iso.slice(11, 13), 10);
                    const localHHMM = h24 === 0 ? '12am' : h24 < 12 ? `${h24}am` : h24 === 12 ? '12pm' : `${h24 - 12}pm`;
                    const burnMins = sunburnMinutes(r.uv, skinType);
                    return html`
                      <tr key=${r.iso}
                          class=${`${isNight ? 'is-night' : ''} ${isNow ? 'is-now' : ''}`.trim()}>
                        <td class="utci-time">
                          <span style=${{ display: 'inline-flex', alignItems: 'center', gap: '7px', verticalAlign: 'middle' }}>
                            <${SkyScope} elev=${r.elev} dt=${r.dt} glob=${r.glob} size=${38} />
                            <span>${localHHMM}</span>
                            ${(() => {
                              const rowEvents = getCellTagEvents(selectedDayEvents, r);
                              return rowEvents.map((ev, idx) => html`
                                <span key=${ev.id} class="event-cell-tag"
                                  onClick=${(e) => handleEventTagClick(rowEvents, idx, e)}
                                  onMouseEnter=${(e) => handleEventTagEnter(rowEvents, idx, e)}
                                  onMouseLeave=${handleEventTagLeave}
                                  role="button" tabIndex="0" aria-label=${ev.title}
                                >${ev.emoji}</span>
                              `);
                            })()}
                          </span>
                        </td>
							${visibleCols.utciP && html`
						  <td class=${groupStart('utciP')} style=${{ background: airTempBg(r.utciAdj, rPrev?.utciAdj, rNext?.utciAdj), color: airTempFontColor(), fontSize: '13px' }}>
							${fmt(r.utciAdj)}${u('°C')}
						  </td>`}
						${visibleCols.vehicleT && html`
						  <td class=${groupStart('vehicleT')} style=${{ color: airTempFontColor(), background: airTempBg(r.vehicleT, rPrev?.vehicleT, rNext?.vehicleT) }}>
							${fmt(r.vehicleT)}${u('°C')}
						  </td>`}
						${indoorMode === 'on' && !indoorManaged && html`
						  <td class=${groupStart('indoorT')} style=${{ color: airTempFontColor(), background: airTempBg(r.indoorT, rPrev?.indoorT, rNext?.indoorT) }}>
							${fmt(r.indoorT)}${u('°C')}
						  </td>`}
						${indoorMode === 'on' && indoorManaged && html`
						  <td class=${groupStart('managedT')} style=${{ color: airTempFontColor(), background: airTempBg(r.managedT, rPrev?.managedT, rNext?.managedT) }}>
							${fmt(r.managedT)}${u('°C')}
						  </td>`}
						${visibleCols.burn && html`
						  <td class=${groupStart('burn')} style=${{ color: r.uv > 0 ? (burnMins < 30 ? '#c44a3a' : '#c8601a') : '#4a3218', background: burnBg(burnMins, r.uv) }}>
							${burnLabel(burnMins)}
						  </td>`}
						${visibleCols.utci && html`
						  <td class=${groupStart('utci')} style=${{ background: airTempBg(r.utci, rPrev?.utci, rNext?.utci), color: airTempFontColor() }}>
							${fmt(r.utci)}${u('°C')}
						  </td>`}
						${visibleCols.delta   && html`
						  <td class=${groupStart('delta')} style=${{
							color: delta > 3 ? '#c8601a' : delta < -3 ? '#3f73c4' : '#4a3218',
							background: deltaBg(delta),
						  }}>
							${delta > 0 ? '+' : ''}${fmt(delta)}${u('°C')}
						  </td>`}
						${visibleCols.tmrt    && html`<td class=${groupStart('tmrt')} style=${{ background: airTempBg(r.Tmrt, rPrev?.Tmrt, rNext?.Tmrt), color: airTempFontColor() }}>${fmt(r.Tmrt)}${u('°C')}</td>`}
						${visibleCols.concreteT && html`
						  <td class=${groupStart('concreteT')} style=${{ color: airTempFontColor(), background: airTempBg(r.concreteT, rPrev?.concreteT, rNext?.concreteT) }}>
							${fmt(r.concreteT)}${u('°C')}
						  </td>`}
						${visibleCols.soilT && html`
						  <td class=${groupStart('soilT')} style=${{ color: airTempFontColor(), background: airTempBg(r.soilT0, rPrev?.soilT0, rNext?.soilT0) }}>${r.soilT0 != null ? fmt(r.soilT0) + u('°C') : '—'}</td>`}
						${visibleCols.soilT6 && html`
						  <td class=${groupStart('soilT6')} style=${{ color: airTempFontColor(), background: airTempBg(r.soilT6, rPrev?.soilT6, rNext?.soilT6) }}>${r.soilT6 != null ? fmt(r.soilT6) + u('°C') : '—'}</td>`}
						${visibleCols.soilM && html`
						  <td class=${groupStart('soilM')} style=${(() => { const sm = soilMoistureBg(r.soilM); return { color: sm.fg, background: sm.bg }; })()}>${r.soilM != null ? fmt(r.soilM * 100, 1) + u('%') : '—'}</td>`}
							${visibleCols.air     && html`<td class=${groupStart('air')} style=${{ background: airTempBg(r.Ta, rPrev?.Ta, rNext?.Ta), color: airTempFontColor() }}>${fmt(r.Ta)}${u('°C')}</td>`}
							${visibleCols.rh      && html`<td class=${groupStart('rh')} style=${{ background: scaleBg(r.RH, 30, 100, '70,145,200') }}>${Math.round(r.RH)}${u('%')}</td>`}
							${visibleCols.dew     && html`<td class=${groupStart('dew')} style=${{ background: airTempBg(r.dew, rPrev?.dew, rNext?.dew), color: airTempFontColor() }}>${fmt(r.dew)}${u('°C')}</td>`}
							${visibleCols.precip && html`
							  <td class=${groupStart('precip')} style=${{ background: r.snow > 0 ? scaleBg(r.snow, 0, 4, '90,140,210') : scaleBg(r.precip, 0, 8, '70,145,200') }}>
								<span style=${{ display: 'inline-flex', alignItems: 'center', gap: '5px', verticalAlign: 'middle' }}>
								  <${PrecipIcon} precip=${r.precip} snow=${r.snow} size=${28} />
								  <span style=${{ color: r.snow > 0 ? '#2a5fa8' : r.precip > 0 ? '#2a6a90' : '#7a5c30' }}>
									${r.snow > 0 ? fmt(r.snow) + u('cm') : r.precip > 0 ? fmt(r.precip) + u('mm') : '—'}
								  </span>
								</span>
							  </td>`}
							${visibleCols.precipProb && html`
							  <td class=${groupStart('precipProb')} style=${{ background: scaleBg(r.precipProb, 0, 100, '70,145,200'), color: r.precipProb >= 50 ? '#1a4a70' : r.precipProb > 0 ? '#2a6a90' : '#7a8a90' }}>
								${r.precipProb}${u('%')}
							  </td>`}
							${visibleCols.cloud   && html`<td class=${groupStart('cloud')} style=${{ background: scaleBg(r.cc, 0, 100, '110,130,150', 0.07), verticalAlign: 'middle' }}>
							  <span style=${{ display: 'inline-flex', alignItems: 'center', gap: '6px', verticalAlign: 'middle' }}>
								<span style=${{ position: 'relative', top: '6px' }}>
								  <${CloudIcon} category=${r.cloudCat} elev=${r.elev} dt=${r.dt} size=${28} />
								</span>
								<span>${Math.round(r.cc)}${u('%')}</span>
							  </span>
							</td>`}
							${visibleCols.vis && (() => {
							  const v = r.visKm;
							  const bg = v == null ? 'transparent'
								: v < 1   ? 'rgba(180,80,80,0.18)'
								: v < 4   ? 'rgba(210,140,50,0.15)'
								: v < 10  ? 'rgba(200,190,80,0.12)'
								: 'transparent';
							  const color = v != null && v < 4 ? '#8a3a1a' : '#4a3218';
							  return html`<td class=${groupStart('vis')} style=${{ background: bg, color }}>
								${v != null ? fmt(v) + u('km') : '—'}
							  </td>`;
							})()}
							${visibleCols.wind    && html`<td class=${groupStart('wind')} style=${{ background: scaleBg((r.gust ?? r.va) * 2.237, 0, 40, '85,130,180') }}>
							  ${Math.round(r.va * 2.237)}${u('mph')}${r.gust != null && r.gust > r.va + 0.5
								? (gm => html`<span style=${{ marginLeft: '4px', fontWeight: gm >= 25 ? 700 : 'normal', opacity: gm >= 25 ? 1 : 0.65, color: gm >= 55 ? '#b81010' : gm >= 40 ? '#d44010' : gm >= 25 ? '#c47a00' : 'inherit' }}>(${Math.round(gm)}${u('mph')})</span>`)(r.gust * 2.237)
								: ''}
							</td>`}
							${visibleCols.dir     && html`<td class=${`utci-dir-cell ${groupStart('dir')}`}>
							  <span style=${{ display: 'inline-flex', alignItems: 'center', gap: '6px', verticalAlign: 'middle' }}>
								<${WindVane} bearing=${r.wd} size=${28} />
								<span class="wind-dir-label" style=${{ fontFamily: 'Manrope, sans-serif', fontSize: '11px', fontWeight: 700 }}>${r.compass.label}</span>
							  </span>
							</td>`}
							${visibleCols.aqi && (() => {
							  const v = r.aqi == null ? null : Math.round(r.aqi);
							  const bg = v == null ? 'transparent'
								: v < 20  ? 'rgba(80,180,100,0.15)'
								: v < 40  ? 'rgba(140,200,100,0.13)'
								: v < 60  ? 'rgba(220,200,60,0.15)'
								: v < 80  ? 'rgba(220,130,50,0.18)'
								: v < 100 ? 'rgba(200,70,50,0.18)'
								: 'rgba(160,30,100,0.20)';
							  const color = v != null && v >= 60 ? '#7a2010' : v != null && v >= 40 ? '#7a4a10' : '#2a4a20';
							  const label = v == null ? '—'
								: v < 20  ? `${v} Good`
								: v < 40  ? `${v} Fair`
								: v < 60  ? `${v} Mod`
								: v < 80  ? `${v} Poor`
								: v < 100 ? `${v} V.Poor`
								: `${v} Hazard`;
							  return html`<td class=${groupStart('aqi')} style=${{ background: bg, color, fontWeight: v != null && v >= 60 ? 600 : 400 }}>${label}</td>`;
							})()}
							${visibleCols.pollen && (() => {
							  const pollenMap = {
								all_pollen:     [r.grassPollen, r.birchPollen, r.alderPollen, r.mugwortPollen, r.olivePollen, r.ragweedPollen].reduce((s, x) => x != null ? s + x : s, null),
								grass_pollen:   r.grassPollen,
								birch_pollen:   r.birchPollen,
								alder_pollen:   r.alderPollen,
								mugwort_pollen: r.mugwortPollen,
								olive_pollen:   r.olivePollen,
								ragweed_pollen: r.ragweedPollen,
							  };
							  const v = pollenMap[pollenType] ?? null;
							  const bg = v == null ? 'transparent'
								: v < 10  ? 'transparent'
								: v < 50  ? 'rgba(180,200,80,0.13)'
								: v < 200 ? 'rgba(210,150,50,0.16)'
								: 'rgba(200,70,50,0.18)';
							  const color = v != null && v >= 200 ? '#8a2010' : v != null && v >= 50 ? '#7a4a10' : '#4a3218';
							  const label = v == null ? '—'
								: v < 10  ? `${Math.round(v)} Low`
								: v < 50  ? `${Math.round(v)} Mod`
								: v < 200 ? `${Math.round(v)} High`
								: `${Math.round(v)} V.High`;
							  return html`<td class=${groupStart('pollen')} style=${{ background: bg, color, fontWeight: v != null && v >= 50 ? 600 : 400 }}>${label}</td>`;
							})()}
							${visibleCols.uvA && html`
							  <td class=${groupStart('uvA')} style=${{ color: r.uvA > 0 ? '#c8922a' : '#4a3218', background: scaleBg(r.uvA, 0, 8, '220,155,40') }}>
								${r.uvA > 0 ? fmt(r.uvA) + u(' idx') : '—'}
							  </td>`}
							${visibleCols.uvB && html`
							  <td class=${groupStart('uvB')} style=${{ color: r.uvB > 0 ? '#c44a3a' : '#4a3218', background: scaleBg(r.uvB, 0, 1.2, '210,70,50') }}>
								${r.uvB > 0 ? fmt(r.uvB, 2) + u(' idx') : '—'}
							  </td>`}
							${visibleCols.sun     && html`<td class=${groupStart('sun')} style=${{ background: scaleBg(r.elev > 0 ? r.elev : null, 0, 70, '225,160,45') }}>${r.elev > 0 ? fmt(r.elev) + u('°') : '—'}</td>`}
							${visibleCols.direct  && html`<td class=${groupStart('direct')} style=${{ background: scaleBg(r.dir, 0, 850, '230,155,35') }}>${Math.round(r.dir)}${u('W/m²')}</td>`}
							${visibleCols.diffuse && html`<td class=${groupStart('diffuse')} style=${{ background: scaleBg(r.dif, 0, 450, '230,190,70') }}>${Math.round(r.dif)}${u('W/m²')}</td>`}
                      </tr>`;
                  })}
                  </tbody>
                </table>
              </div>
            </div>
            </div>
            ${forecast && ((glanceSummary && glanceSummary.length > 0) || eventGlanceItems.length > 0) && html`
              <aside class="glance-rail">
                <div class="insight-panel insight-panel--glance">
                  <div class="insight-panel-title">At a glance${glanceDate ? ` · ${glanceDate}` : ''}</div>
                  ${glanceSummary.map(({ icon, label, value, alert }) => html`
                    <div class=${`insight-row${alert ? ' insight-row--alert' : ''}`} key=${label}>
                      <span class="insight-icon">${icon}</span>
                      <span class="insight-label">${label}</span>
                      <span class="insight-value">${value}</span>
                    </div>
                  `)}
                  ${eventGlanceItems.map(({ icon, label, value, alert }) => html`
                    <div class=${`insight-row${alert ? ' insight-row--alert' : ''}`} key=${`ev-${label}`}>
                      <span class="insight-icon">${icon}</span>
                      <span class="insight-label">${label}</span>
                      <span class="insight-value">${value}</span>
                    </div>
                  `)}
                </div>
              </aside>`}
            </div>

            ${colPopup && COL_DESCRIPTIONS[colPopup.key] && html`
              <div
                ref=${colPopupRef}
                class=${`col-info-popup${colPopup.below ? ' col-info-popup--below' : ''}`}
                style=${{
                  position: 'fixed',
                  left: `${colPopup.x}px`,
                  top: `${colPopup.y}px`,
                  transform: colPopup.below ? 'translateX(-50%)' : 'translateX(-50%) translateY(-100%)',
                  '--arrow-left': `${colPopup.arrowLeft}px`,
                }}
                onMouseEnter=${handlePopupEnter}
                onMouseLeave=${handlePopupLeave}
              >
                <button class="col-info-close" onClick=${closePopup} aria-label="Close">×</button>
                <strong class="col-info-title">${COL_DESCRIPTIONS[colPopup.key].title}</strong>
                <p class="col-info-desc">${COL_DESCRIPTIONS[colPopup.key].desc}</p>
              </div>`}

            ${eventTagPopup && (() => {
              const evs = eventTagPopup.events;
              const ev  = evs[evSlideIndex] || evs[0];
              return html`
                <div
                  ref=${eventTagPopupRef}
                  class=${`col-info-popup col-info-popup--ev${eventTagPopup.below ? ' col-info-popup--below' : ''}`}
                  style=${{
                    position: 'fixed',
                    left: `${eventTagPopup.x}px`,
                    top: `${eventTagPopup.y}px`,
                    transform: eventTagPopup.below ? 'translateX(-50%)' : 'translateX(-50%) translateY(-100%)',
                    '--arrow-left': `${eventTagPopup.arrowLeft}px`,
                  }}
                  onMouseEnter=${handleEventTagPopupEnter}
                  onMouseLeave=${handleEventTagPopupLeave}
                >
                  <button class="col-info-close" onClick=${closeEventTagPopup} aria-label="Close">×</button>
                  <div class=${`ev-popup-stage${evTransition ? ` ev-popup-${evTransition}` : ''}`}>
                    <strong class="col-info-title">${ev.emoji} ${ev.title}</strong>
                    <p class="col-info-desc">${ev.message}</p>
                  </div>
                  ${evs.length > 1 && html`
                    <div class="ev-popup-dots">
                      ${evs.map((_, i) => html`
                        <span
                          key=${i}
                          class=${`ev-popup-dot${i === evSlideIndex ? ' active' : ''}`}
                          onClick=${() => evSlideTo(i)}
                        />
                      `)}
                    </div>
                  `}
                </div>`;
            })()}

            <div class="utci-legend">
              <span class="utci-legend-label">Thermal stress bands</span>
              <div class="utci-legend-row">
                ${[
                  { t: -9,  label: 'Freezing' },
                  { t:  1,  label: 'Cold' },
                  { t: 11,  label: 'Cool' },
                  { t: 21,  label: 'Comfortable', bold: true },
                  { t: 31,  label: 'Hot' },
                  { t: 41,  label: 'Very hot' },
                  { t: 51,  label: 'Danger' },
                ].map((b, i) => {
                  const rgb = airTempRgb(b.t) || [200, 200, 200];
                  const mid = `rgb(${rgb[0]},${rgb[1]},${rgb[2]})`;
                  const light = `rgb(${rgb.map(c => Math.min(255, Math.round(c + (255-c)*0.30))).join(',')})`;
                  const dark  = `rgb(${rgb.map(c => Math.round(c * 0.96)).join(',')})`;
                  const bg = `linear-gradient(135deg, ${light} 0%, ${mid} 60%, ${dark} 100%)`;
                  const lum = (rgb[0]*299 + rgb[1]*587 + rgb[2]*114) / 1000;
                  const fg = lum > 165 ? '#1a1a1a' : '#ffffff';
                  return html`<span key=${i} class="utci-legend-item" style=${{ background: bg, color: fg, ...(b.bold ? { fontWeight: 700 } : {}) }}>${b.label}</span>`;
                })}
              </div>
            </div>

            
            ${(() => {
              const upcoming = getUpcomingEvents(location, 90);
              const formatPeak = (iso) => {
                const d = new Date(iso + 'T00:00Z');
                return d.toLocaleDateString('en-GB', { day: 'numeric', month: 'short', year: 'numeric', timeZone: 'UTC' });
              };
              const countdownLabel = (days) => {
                if (days === 0) return 'Tonight!';
                if (days === 1) return 'Tomorrow';
                if (days < 7)  return `In ${days} days`;
                if (days < 14) return 'Next week';
                if (days < 60) return `In ${Math.round(days / 7)} weeks`;
                return `In ${Math.round(days / 30)} months`;
              };
              return html`
                <div class="almanac-panel">
                  <div class="almanac-header">
                    <span class="almanac-title">🔭 What's Coming</span>
                    <span class="almanac-subtitle">Cosmic events · next 90 days · ${location.name}</span>
                  </div>
                  <div class="almanac-list">
                    ${upcoming.length === 0
                      ? html`<div class="almanac-empty">No major cosmic events in the next 90 days — clear skies ahead.</div>`
                      : upcoming.map(ev => html`
                        <div key=${ev.id} class="almanac-entry">
                          <div class="almanac-entry-accent" style=${{ background: ev.color === '#fdf8ee' ? '#c8922a' : ev.color }} />
                          <div class="almanac-entry-icon">${ev.emoji}</div>
                          <div class="almanac-entry-body">
                            <div class="almanac-entry-head">
                              <span class="almanac-entry-title">${ev.title}</span>
                              <span class="almanac-entry-date">${formatPeak(ev.peak)}</span>
                              <span class="almanac-entry-countdown">${countdownLabel(ev.daysUntil)}</span>
                            </div>
                            <div class="almanac-entry-desc">${ev.desc}</div>
                            ${ev.visibilityNote && html`
                              <span class="almanac-entry-visibility">📍 ${ev.visibilityNote}</span>
                            `}
                          </div>
                        </div>
                      `)
                    }
                  </div>
                </div>`;
            })()}

        <div class="utci-about">
          <h2 class="utci-about-heading">What is SunScope?</h2>
          <p class="utci-about-text">
            SunScope is a free hourly weather forecast built around <strong>felt temperature</strong>,
            not just air temperature. It uses the <strong>Universal Thermal Climate Index (UTCI)</strong>
            — the biometeorological standard used in heat-health warning systems worldwide — as its
            foundation, combining air temperature, humidity, wind, and solar radiation into a single
            honest number. Then it goes further. Our <strong>SunSoak</strong> index layers three extra
            dimensions on top: a rain and snow penalty so wet, windy days read as cold as they feel;
            an <strong>environment modifier</strong> that adjusts the solar load for where you actually
            are — forest canopy, alpine altitude, lakeside glare, shaded riverbank, desert ground heat;
            and the full radiant heat absorbed from surrounding surfaces. One number that honestly
            answers: <em>what will my body actually feel out there?</em>
          </p>
          <p class="utci-about-text">
            Beyond SunSoak, SunScope calculates <strong>vehicle cabin heat</strong> (choose your
            vehicle type; toggle windows open), <strong>indoor temperature</strong> (seven building
            types including office blocks; managed heatwave mode), <strong>urban concrete surface
            temperature</strong>, <strong>UV index and sunburn time</strong> by skin type, and
            <strong>soil temperature and moisture</strong> for farming and motorhome use. Switch
            profiles — Places, Activities, Work — to see the data that matters for your situation,
            or go Custom and build your own view.
            <a href="./about.html" class="utci-about-link">Learn more →</a>
          </p>
        </div>

        <div class="utci-footer">
          <em>Reading the table.</em> SunSoak is the number to watch — it reflects everything your body
          actually experiences, not just what the thermometer says. A large gap between SunSoak and Air
          temperature means solar radiation is doing significant work on your body. On clear sunny days
          that gap can exceed 10 °C even at modest air temperatures. The environment modifier in the
          SunSoak dropdown adjusts the solar load for your surroundings — switch it to match where you are
          for the most accurate reading.
          ${isPro && html`
            <div style=${{ marginTop: '10px', paddingTop: '10px', borderTop: '1px solid #d4c0a0' }}>
              SunScope Extra is active.
              <a href="https://billing.stripe.com/p/login/9B63cw7vl8k15Ei9DQd7q00"
                 target="_blank" rel="noopener noreferrer"
                 style=${{ color: '#9a7d5a', borderBottom: '1px solid rgba(154,125,90,0.4)', paddingBottom: '1px', textDecoration: 'none' }}>
                Manage or cancel subscription →
              </a>
            </div>
          `}
        </div>

      <footer class="utci-site-footer">
        © 2026 <a href="https://fraxle.net" target="_blank" rel="noopener noreferrer">Fraxle.NET</a>
        · <a href="./index.html">Forecast</a>
        · <a href="./about.html">About</a>
        · <a href="./faq.html">FAQ</a>
        · <a href=${isPro
            ? 'https://billing.stripe.com/p/login/9B63cw7vl8k15Ei9DQd7q00'
            : 'https://buy.stripe.com/9B63cw7vl8k15Ei9DQd7q00'}
           target="_blank" rel="noopener noreferrer">Account</a>
        · Data: <a href="https://open-meteo.com/" target="_blank" rel="noopener noreferrer">Open-Meteo</a>
        · UTCI: <a href="https://utci.org/" target="_blank" rel="noopener noreferrer">Bröde 2012</a>
      </footer>
      </main>
    </div>`;
}