// ------------------------------------------------------------------------
//  components.js - Preact UI components (SVG widgets and controls).
//
//  All components use the html'' tagged template from htm + Preact.
//  No external state; they depend only on utils.js and events.js.
//
//  Exports (in order of appearance):
//    CustomSelect({ value, options, onChange, ... })   styled dropdown pill
//    VentPill({ checked, onChange, label, title })     checkbox fused to pill
//    SkyScope({ elev, dt, glob, size })                small porthole per hour
//    WindVane({ bearing, size })                       compass-arrow widget
//    CloudIcon({ category, size, elev, dt })           cloud/sun glyph
//    ScopeReticle({ value, cat, loading, elev, dt, glob, activeEvent, utciEnvShort })
//                                                      large UTCI dial
//    PrecipIcon({ precip, snow, size })                rain/snow icon
// ------------------------------------------------------------------------

import { h, Fragment } from '../vendor/preact.js';
import { useState, useEffect, useRef } from '../vendor/preact-hooks.js';
import htm from '../vendor/htm.js';
import {
  skyGradientForElev, skyFillForElev, grassFillForElev,
  moonPhaseFraction,
} from './utils.js';
import { getLensOverlaySVG } from './events.js';

const html = htm.bind(h);

const MOON_TEX = 'data:image/png;base64, ';

// -------------------------------------------------------------------
// CUSTOMSELECT - cross-platform styled dropdown pill.
// -------------------------------------------------------------------
//  props:
//    value        - current value string
//    options      - array of { value, label, disabled }
//    onChange     - fn(newValue)
//    hideLabel    - label shown when value === 'off' (e.g. 'Burn')
//    hidingLabel  - label shown for the 'off' option when active (e.g. 'Hide Burn')
//    isOn         - bool - whether the pill shows as active (brass)
//    groupedLeft  - bool - fuse right side with a VentPill
//    isLastChild  - bool - restore right border-radius when no sibling follows
// -------------------------------------------------------------------
export function CustomSelect({ value, options, onChange, hideLabel, hidingLabel, buttonLabel, isOn, noHide, groupedLeft, isLastChild, grpClass, sceneStyle, sceneContent }) {
  const [open, setOpen] = useState(false);
  const [flipRight, setFlipRight] = useState(false);
  const wrapRef  = useRef(null);
  const panelRef = useRef(null);

  // Close on outside click or Escape
  useEffect(() => {
    if (!open) return;
    const onDown = (e) => {
      if (wrapRef.current && !wrapRef.current.contains(e.target)) setOpen(false);
    };
    const onKey = (e) => { if (e.key === 'Escape') setOpen(false); };
    document.addEventListener('mousedown', onDown);
    document.addEventListener('touchstart', onDown);
    document.addEventListener('keydown', onKey);
    return () => {
      document.removeEventListener('mousedown', onDown);
      document.removeEventListener('touchstart', onDown);
      document.removeEventListener('keydown', onKey);
    };
  }, [open]);

  // After the panel renders, check if it overflows the right viewport edge.
  // If so, flip it to right-align; otherwise keep it left-aligned.
  useEffect(() => {
    if (!open) { setFlipRight(false); return; }
    if (!panelRef.current) return;
    const rect = panelRef.current.getBoundingClientRect();
    setFlipRight(rect.right > window.innerWidth - 8);
  }, [open]);

  // Build the label shown on the button
  const activeOpt = options.find(o => o.value === value);
  const btnLabel = buttonLabel || (isOn
    ? (activeOpt ? activeOpt.label : hideLabel)
    : hideLabel);

  const btnClass = [
    'cs-btn',
    grpClass || '',
    isOn ? 'on' : '',
    groupedLeft ? 'grouped-left' : '',
    (groupedLeft && isLastChild) ? 'last-child' : '',
  ].filter(Boolean).join(' ');

  return html`
    <span class=${`cs-wrap${open ? ' open' : ''}`} ref=${wrapRef}>
      <button
        class=${btnClass}
        onClick=${(e) => { e.stopPropagation(); setOpen(o => !o); }}
        type="button"
      >
        ${sceneStyle !== undefined && html`<span class="cs-btn-scene" style=${sceneStyle}>${sceneContent || ''}</span>`}
        <span class="cs-btn-overlay"></span>
        <span class="cs-btn-label">${btnLabel}</span>
        <span class="cs-arrow"></span>
      </button>
      ${open && html`
        <div class=${`cs-panel${flipRight ? ' cs-panel--right' : ''}`} ref=${panelRef} role="listbox">
          ${isOn && !noHide && html`
            <span
              class="cs-option"
              role="option"
              onClick=${() => { onChange('off'); setOpen(false); }}
            >${hidingLabel || ('Hide ' + hideLabel)}</span>
            <div class="cs-divider"></div>
          `}
          ${options.map(opt => opt.divider
            ? html`<div key=${opt.value} class="cs-divider"></div>`
            : html`<span
              key=${opt.value}
              class=${`cs-option${opt.value === value && isOn ? ' selected' : ''}${opt.disabled ? ' disabled' : ''}`}
              role="option"
              onClick=${() => {
                if (opt.disabled) return;
                onChange(opt.value);
                setOpen(false);
              }}
            >${opt.label}</span>`
          )}
        </div>
      `}
    </span>`;
}

// -------------------------------------------------------------------
// VENTPILL - the checkbox pill fused to the right of a CustomSelect.
// -------------------------------------------------------------------
//  props:
//    checked  - bool
//    onChange - fn()
//    label    - string
//    title    - tooltip string
// -------------------------------------------------------------------
export function VentPill({ checked, onChange, label, title, grpClass }) {
  return html`
    <label class=${[`cs-vent`, grpClass || '', checked ? 'on' : ''].filter(Boolean).join(' ')} title=${title}>
      <input
        type="checkbox"
        checked=${checked}
        onChange=${onChange}
        style=${{ position: 'absolute', opacity: 0, width: 0, height: 0, pointerEvents: 'none' }}
      />
      <span class=${`cs-checkbox${checked ? ' checked' : ''}`}></span>
      ${label}
    </label>`;
}

// -------------------------------------------------------------------
// SKYSCOPE - the little porthole circle next to each hour.
// -------------------------------------------------------------------
// What gets drawn (back to front):
//   1. Sky disk        - colour from skyFillForElev() above
//   2. Horizon line    - faint dashed line at the middle
//   3. Sun OR moon     - sun positioned vertically by elevation;
//                        moon shows real phase (synodic period)
//   4. Brass ring      - the telescope/scope edge (#c8922a)
//   5. Lens highlight  - subtle inner glint
//
// Tweakable bits inside this function:
//   - size       - pass a different size= when calling for bigger/smaller
//   - innerR     - how thick the brass ring looks
//   - sunR       - the sun's drawn radius
//   - the stroke colour #c8922a is the brass - change for a different metal
// -------------------------------------------------------------------
export function SkyScope({ elev, dt, glob = 0, size = 26 }) {
  const r = size / 2;
  const innerR = r - 1.2;

  // Rising if local hour < 12 (dt.getUTCHours() == local hour after timezone fix)
  const localHour = dt ? dt.getUTCHours() : 12;
  const isRising = localHour < 12;

  const skyGrad = skyGradientForElev(elev, isRising);

  const isDay   = elev > 0;    // sun only drawn when actually above horizon
  const isTwil  = elev > -8 && !isDay;  // moon shown in twilight too
  const isNight = elev <= -8;

  // Sun position - clamped so it never goes below the horizon line (y = r)
  const elevClamped = Math.max(0, Math.min(90, elev));
  const sunY = r - Math.sin((elevClamped * Math.PI) / 180) * (innerR - 2.5);
  const sunR = Math.max(2.2, innerR * 0.32);

  // Radial glow: strength driven by glob (W/m-). Uses a radialGradient
  // so it's intense at the sun centre and fades smoothly to transparent.
  const radFrac   = Math.min(1, (glob || 0) / 900); // 0 (dark) - 1 (blazing)
  const horizFrac = Math.max(0, 1 - elev / 20);     // extra warmth near horizon
  const glowR     = sunR + 4 + radFrac * 6;          // total glow radius
  const glowPeak  = 0.35 + radFrac * 0.45 + horizFrac * 0.15; // centre opacity

  const phase = moonPhaseFraction(dt);
  const moonR = innerR * 0.55;
  const shadowOffset = (1 - Math.abs(2 * phase - 1)) * 2 * moonR;
  const shadowCx = r + (phase < 0.5 ? -shadowOffset : shadowOffset);
  const grass = grassFillForElev(elev);

  const uid    = `ss-${size}-${Math.round(elev * 10)}-${Math.round(phase * 1000)}-${isRising ? 'r' : 's'}`;
  const clipId = `clip-${uid}`;
  const skyId  = `sky-${uid}`;
  const grassId = `grass-${uid}`;

  return html`
    <svg width=${size} height=${size} viewBox=${`0 0 ${size} ${size}`}
         style=${{ flexShrink: 0, display: 'inline-block', verticalAlign: 'middle' }}>
      <defs>
        <clipPath id=${clipId}>
          <circle cx=${r} cy=${r} r=${innerR} />
        </clipPath>
        <!-- Sky gradient — top of disk to bottom (zenith → horizon) -->
        <linearGradient id=${skyId} x1="0" y1="0" x2="0" y2="1">
          <stop offset="0%"   stop-color=${skyGrad.top} />
          <stop offset="100%" stop-color=${skyGrad.bot} />
        </linearGradient>
        <!-- Grass gradient — horizon to ground -->
        <linearGradient id=${grassId} x1="0" y1="0" x2="0" y2="1">
          <stop offset="0%"   stop-color=${grass.top} />
          <stop offset="100%" stop-color=${grass.bot} />
        </linearGradient>
        <!-- Sun glow — same colour as sun disk, fades from opaque at centre to transparent -->
        <radialGradient id=${`glow-${uid}`} gradientUnits="userSpaceOnUse"
                        cx=${r} cy=${sunY} r=${glowR}>
          <stop offset="0%"   stop-color="#fff8c0" stop-opacity="1" />
          <stop offset=${`${Math.round(sunR / glowR * 100)}%`} stop-color="#fff8c0" stop-opacity="1" />
          <stop offset="70%"  stop-color="#ffe060" stop-opacity=${glowPeak * 0.5} />
          <stop offset="100%" stop-color="#ffcc30" stop-opacity="0" />
        </radialGradient>
        <!-- Moon phase mask: lit area = white, shadow = black punched out -->
        <mask id=${`moon-mask-${uid}`}>
          <circle cx=${r} cy=${r} r=${moonR} fill="white" />
          <circle cx=${shadowCx} cy=${r} r=${moonR} fill="black" />
        </mask>
        <!-- Moon texture pattern - sized to fit the moon disk -->
        <pattern id=${`moon-tex-${uid}`} x=${r - moonR - 2} y=${r - moonR - 2}
                 width=${moonR * 2 + 4} height=${moonR * 2 + 4} patternUnits="userSpaceOnUse">
          <image href=${MOON_TEX} x="0" y="0" width=${moonR * 2 + 4} height=${moonR * 2 + 4} />
        </pattern>
      </defs>

      <!-- Sky disk -->
      <circle cx=${r} cy=${r} r=${innerR} fill=${`url(#${skyId})`} />

      <!-- Ground half (lower semicircle) -->
      <path d=${`M ${r - innerR} ${r} A ${innerR} ${innerR} 0 0 0 ${r + innerR} ${r} Z`}
            fill=${`url(#${grassId})`}
            clip-path=${`url(#${clipId})`} />

      ${isDay && html`
        <g clip-path=${`url(#${clipId})`}>
          <!-- Glow + sun disk as one seamless radial gradient circle -->
          <circle cx=${r} cy=${sunY} r=${glowR} fill=${`url(#glow-${uid})`} />
        </g>`}

      ${(isTwil || isNight) && html`
        <g clip-path=${`url(#${clipId})`}>
          <!-- Lit crescent via mask -->
          <circle cx=${r} cy=${r} r=${moonR}
                  fill="#e7e3d4" mask=${`url(#moon-mask-${uid})`} />
          <!-- Texture overlay on lit face -->
          <circle cx=${r} cy=${r} r=${moonR}
                  fill=${`url(#moon-tex-${uid})`}
                  mask=${`url(#moon-mask-${uid})`}
                  opacity="0.55" />
          <!-- Dim unlit face so the full disk outline remains visible -->
          <circle cx=${r} cy=${r} r=${moonR}
                  fill="rgba(200,192,230,0.08)" stroke="none" />
        </g>`}

      <!-- Brass scope ring -->
      <circle cx=${r} cy=${r} r=${innerR} fill="none" stroke="#c8922a" stroke-width="0.9" />
      <!-- Inner highlight glint -->
      <circle cx=${r} cy=${r} r=${innerR - 0.5} fill="none"
              stroke="rgba(255,255,255,0.22)" stroke-width="0.4" />
    </svg>`;
}

// -------------------------------------------------------------------
// WINDVANE - clean compass arrow on transparent background.
// -------------------------------------------------------------------
// Traditional weather-vane behaviour: the ARROWHEAD points INTO the
// wind (toward the source). Wind from the north - head points north.
//
//   props:
//     bearing - degrees, 0 = wind FROM north
//     size    - pixel diameter (default 30 to match SkyScope)
//
// Tweakable bits:
//   - arrowColor / nMarkerColor - line/fill colours
//   - Tiny N letter sits just above the arrow tail for orientation
// -------------------------------------------------------------------
export function WindVane({ bearing, size = 30 }) {
  if (bearing == null || isNaN(bearing)) {
    return html`<span style=${{ opacity: 0.5, fontSize: '11px' }}>—</span>`;
  }
  const r = size / 2;
  // The shaft is drawn pointing DOWN by default (head at the bottom).
  // To place the head at the bearing direction, rotate by bearing + 180.
  const rot = (bearing + 180) % 360;
  const arrowColor = '#2a1a08';
  const headColor  = '#c44a3a';
  const nColor     = '#9a7d5a';
  // Geometry of the single-ended arrow (drawn pointing DOWN by default).
  // After rotation by (bearing + 180), the head lands on the bearing
  // direction - i.e. the side the wind is coming FROM.
  const tipY     = r * 0.15;            // arrowhead tip
  const headBase = r * 0.58;            // bottom of the triangle head
  const headW    = r * 0.42;
  const tailEndY = r * 1.78;            // shaft's tail end
  const tailDotR = Math.max(1.0, size * 0.06);
  return html`
    <svg width=${size} height=${size} viewBox=${`0 0 ${size} ${size}`}
         style=${{ flexShrink: 0, display: 'inline-block', verticalAlign: 'middle', overflow: 'visible' }}>
      <!-- Static N marker — small letter above the centre, doesn't rotate -->
      <text x=${r} y=${size * 0.18} text-anchor="middle"
            font-family="Manrope, sans-serif"
            font-size=${size * 0.26} font-weight="700"
            fill=${nColor} opacity="0.55">N</text>
      <!-- Rotating arrow. Shaft + ONE arrowhead. A small round nub on
           the tail end keeps the orientation unambiguous without
           looking like a second arrowhead. -->
      <g transform=${`rotate(${rot} ${r} ${r})`}>
        <!-- shaft -->
        <line x1=${r} y1=${headBase} x2=${r} y2=${tailEndY}
              stroke=${arrowColor} stroke-width=${Math.max(1.4, size*0.07)} stroke-linecap="round" />
        <!-- single arrowhead at the upwind end (head into the wind) -->
        <polygon points=${`${r - headW},${headBase} ${r + headW},${headBase} ${r},${tipY}`}
                 fill=${headColor} stroke=${arrowColor} stroke-width="0.6" stroke-linejoin="round" />
        <!-- small dot at the downwind end of the shaft, so orientation
             is unambiguous (no chance of reading the tail as a head) -->
        <circle cx=${r} cy=${tailEndY} r=${tailDotR} fill=${arrowColor} />
      </g>
    </svg>`;
}

// -------------------------------------------------------------------
// CLOUDICON - Brass Line cloud / haze / overcast glyphs.
// No sun or moon here; the time-cell SkyScope owns day/night imagery.
// -------------------------------------------------------------------
//   category: 'clear' | 'wispy' | 'scattered' | 'overcast'
//   elev:     solar elevation in degrees (negative = night)
//   dt:       Date used for moon phase
// -------------------------------------------------------------------
export function CloudIcon({ category, size = 30, elev = 90, dt = new Date() }) {
  const r = size / 2;
  const brass = '#c8922a';
  const ink = '#2a1a08';
  const muted = '#b09870';
  const sw = Math.max(1.25, size * 0.058);
  const cap = 'round';
  const join = 'round';
  const cloudPath = (dx = 0, dy = 0, scale = 1) => {
    const x = (n) => r + n * r * scale + dx;
    const y = (n) => r + n * r * scale + dy;
    return `M ${x(-0.76)} ${y(0.25)}
            H ${x(0.56)}
            C ${x(0.82)} ${y(0.25)}, ${x(0.86)} ${y(-0.08)}, ${x(0.58)} ${y(-0.10)}
            C ${x(0.48)} ${y(-0.44)}, ${x(0.08)} ${y(-0.52)}, ${x(-0.10)} ${y(-0.22)}
            C ${x(-0.42)} ${y(-0.33)}, ${x(-0.76)} ${y(-0.08)}, ${x(-0.76)} ${y(0.25)} Z`;
  };
  const line = (x1, y1, x2, y2, color = muted, width = sw, opacity = 1) => html`
    <line x1=${x1} y1=${y1} x2=${x2} y2=${y2}
          stroke=${color} stroke-width=${width} stroke-linecap=${cap} opacity=${opacity} />`;
  const mist = (y0, color = muted) => html`
    <g>
      ${line(size * 0.18, y0, size * 0.80, y0, color, sw * 0.9, 0.88)}
      ${line(size * 0.30, y0 + size * 0.18, size * 0.92, y0 + size * 0.18, ink, sw * 0.82, 0.72)}
    </g>`;

  // Sun shape - circle + 8 rays, centred at (cx, cy)
  const sun = (cx, cy, cr) => {
    const rays = Array.from({ length: 8 }, (_, k) => {
      const angle = (k * Math.PI) / 4;
      const r1 = cr * 1.55;
      const r2 = cr * 2.1;
      return html`<line key=${k}
        x1=${cx + Math.cos(angle) * r1} y1=${cy + Math.sin(angle) * r1}
        x2=${cx + Math.cos(angle) * r2} y2=${cy + Math.sin(angle) * r2}
        stroke=${brass} stroke-width=${sw * 0.85} stroke-linecap="round" opacity="0.9" />`;
    });
    return html`<g>
      ${rays}
      <circle cx=${cx} cy=${cy} r=${cr} fill="none" stroke=${brass} stroke-width=${sw} opacity="0.95" />
    </g>`;
  };

  return html`
    <svg width=${size} height=${size} viewBox=${`0 0 ${size} ${size}`}
         style=${{ flexShrink: 0, display: 'inline-block', verticalAlign: 'middle', overflow: 'visible' }}>
      ${category === 'clear' && elev >= 0 && html`
        ${sun(size * 0.50, size * 0.38, size * 0.16)}`}
      ${category === 'clear' && elev < 0 && html`
        <path d=${`M ${size*0.45} ${size*0.15}
                   A ${size*0.20} ${size*0.20} 0 1 1 ${size*0.45} ${size*0.55}
                   A ${size*0.235} ${size*0.235} 0 1 0 ${size*0.45} ${size*0.15} Z`}
              fill=${brass} opacity="0.85" />
        <circle cx=${size*0.82} cy=${size*0.22} r=${size*0.030} fill=${brass} opacity="0.75" />
        <circle cx=${size*0.20} cy=${size*0.28} r=${size*0.022} fill=${brass} opacity="0.65" />
        <circle cx=${size*0.75} cy=${size*0.60} r=${size*0.025} fill=${brass} opacity="0.70" />
        <circle cx=${size*0.18} cy=${size*0.66} r=${size*0.018} fill=${brass} opacity="0.55" />
        <circle cx=${size*0.88} cy=${size*0.48} r=${size*0.018} fill=${brass} opacity="0.55" />`}
      ${category === 'wispy' && html`
        ${mist(size * 0.18, brass)}
        ${line(size * 0.18, size * 0.56, size * 0.60, size * 0.56, muted, sw * 0.7, 0.6)}`}
      ${category === 'scattered' && html`
        <path d=${cloudPath(0, -size * 0.2425, 0.82)} fill="none" stroke=${ink}
              stroke-width=${sw} stroke-linecap=${cap} stroke-linejoin=${join} />
        ${line(size * 0.20, size * 0.58, size * 0.48, size * 0.58, brass, sw * 0.75, 0.78)}`}
      ${category === 'overcast' && html`
        <path d=${cloudPath(-size * 0.02, -size * 0.2275, 0.70)} fill="none" stroke=${muted}
              stroke-width=${sw * 0.9} stroke-linecap=${cap} stroke-linejoin=${join} opacity="0.82" />
        <path d=${cloudPath(0, -size * 0.25, 0.88)} fill="none" stroke=${ink}
              stroke-width=${sw} stroke-linecap=${cap} stroke-linejoin=${join} />`}
    </svg>`;
}

// -------------------------------------------------------------------
// SCOPERETICLE - the big circular UTCI dial in the page header.
// -------------------------------------------------------------------
// This is the one with tick marks, the swept colour band, and the
// needle pointing at the current felt-temperature.
//
// Tweakable bits:
//   - R = 86            outer ring radius (changes overall size)
//   - cx, cy = 100      centre point (leave alone unless you also
//                       change the viewBox="0 0 200 200" below)
//   - { length: 36 }    number of tick marks (1 every 10-)
//   - stressBands[]     the colour ramp around the rim (matches UTCI)
//   - (value + 10) / 60 maps UTCI -10..50 onto the 270- sweep -
//                       widen the dial range by changing those numbers
// -------------------------------------------------------------------
export function ScopeReticle({ value, cat, loading, elev = 0, dt = new Date(), glob = 0, activeEvent = null, utciEnvShort = null }) {
  // -- Day/night scene behind the readout -----------------------------
  // The whole back of the lens is a SkyScope-style scene: sky on top,
  // grass on the bottom, sun positioned by elevation (or moon at night
  // with its phase shadow). Mirrors the little hourly SkyScopes.
  const cx = 100, cy = 100, R = 86;
  const localHour = dt ? dt.getUTCHours() : 12;
  const isRising  = localHour < 12;
  const isDay     = elev > 0;    // sun only above the actual horizon
  const isTwil    = elev > -8 && !isDay;
  const skyGrad   = skyGradientForElev(elev, isRising);
  const skyHorizon = skyGrad.bot;
  const grass = grassFillForElev(elev);
  const phase = moonPhaseFraction(dt);
  const lensR = 95;
  const elevClamped = Math.max(0, Math.min(90, elev));   // clamp to above horizon
  const sunY = cy - Math.sin((elevClamped * Math.PI) / 180) * (lensR - 18);
  const sunDrawR = 14;
  // Radial glow for the big dial - sun is always at least faintly glowing
  // when above the horizon; scales up with radiation strength.
  const radFrac    = Math.min(1, (glob || 0) / 900);
  const horizFrac  = Math.max(0, 1 - elev / 20);
  const sunGlowCol = isRising ? '#ffb040' : '#ffc060';
  const sunGlowR   = sunDrawR + 20 + radFrac * 30;    // total glow radius
  const sunGlowPeak = 0.30 + radFrac * 0.45 + horizFrac * 0.15; // centre opacity
  const moonDrawR = 15;
  const moonY = cy - 45;
  const shadowOffset = (1 - Math.abs(2 * phase - 1)) * 2 * moonDrawR;
  const moonShadowCx = cx + (phase < 0.5 ? -shadowOffset : shadowOffset);
  // Readout text colours flip with day/night for legibility against the sky.
  const readoutColor = isDay ? '#1e1208' : '#f5edd6';
  const readoutMutedColor = isDay ? '#9a7d5a' : '#d4c5a8';
  const ticks = Array.from({ length: 36 }, (_, i) => {
    const deg = i * 10 - 90;
    const rad = (deg * Math.PI) / 180;
    const major = i % 9 === 0;
    const medium = i % 3 === 0;
    const r2 = major ? R - 14 : medium ? R - 8 : R - 4;
    return {
      x1: cx + R * Math.cos(rad), y1: cy + R * Math.sin(rad),
      x2: cx + r2 * Math.cos(rad), y2: cy + r2 * Math.sin(rad),
      major, medium,
    };
  });
  function fracToXY(frac, r) {
    const deg = 135 + frac * 270;
    const rad = (deg * Math.PI) / 180;
    return [cx + r * Math.cos(rad), cy + r * Math.sin(rad)];
  }
  // Gradient colour stops: frac 0 = −10 °C, frac 1 = 50 °C
  const ARC_GRAD = [
    [0,     [106, 169, 225]],
    [0.167, [147, 204, 217]],
    [0.317, [146, 212, 163]],
    [0.467, [218, 225,  95]],
    [0.600, [248, 161,  63]],
    [0.700, [225,  83,  51]],
    [0.800, [174,  24,  28]],
    [1.000, [141,   6,  22]],
  ];
  function arcColorAt(f) {
    for (let i = 0; i < ARC_GRAD.length - 1; i++) {
      const [f1, c1] = ARC_GRAD[i], [f2, c2] = ARC_GRAD[i + 1];
      if (f <= f2) {
        const t = (f - f1) / (f2 - f1);
        return `rgb(${Math.round(c1[0]+(c2[0]-c1[0])*t)},${Math.round(c1[1]+(c2[1]-c1[1])*t)},${Math.round(c1[2]+(c2[2]-c1[2])*t)})`;
      }
    }
    return 'rgb(141,6,22)';
  }
  const arcR = R - 18;
  const gradArc = Array.from({ length: 90 }, (_, i) => {
    const f1 = i / 90, f2 = (i + 1) / 90;
    const [x1, y1] = fracToXY(f1, arcR);
    const [x2, y2] = fracToXY(f2, arcR);
    return { d: `M ${x1} ${y1} A ${arcR} ${arcR} 0 0 1 ${x2} ${y2}`, color: arcColorAt((f1+f2)/2) };
  });
  let needleX = cx, needleY = cy + 54;
  if (value != null) {
    const frac = Math.min(1, Math.max(0, (value + 10) / 60));
    const deg = 135 + frac * 270;
    const rad = (deg * Math.PI) / 180;
    needleX = cx + 54 * Math.cos(rad);
    needleY = cy + 54 * Math.sin(rad);
  }
  const glowColor = cat ? cat.bg : '#c8922a';
  return html`
    <svg viewBox="0 0 200 200" class="scope-ring" aria-label="Current UTCI scope readout">
      <defs>
        <radialGradient id="scope-bg-glow" cx="50%" cy="50%" r="50%">
          <stop offset="0%" stop-color=${glowColor} stop-opacity="0.12" />
          <stop offset="100%" stop-color=${glowColor} stop-opacity="0" />
        </radialGradient>
        <!-- 3D lens: inner rim shadow — dark around the edge, transparent centre -->
        <radialGradient id="scope-rim-shadow" cx="50%" cy="50%" r="50%">
          <stop offset="72%" stop-color="transparent" />
          <stop offset="100%" stop-color="rgba(20,10,0,0.38)" />
        </radialGradient>
        <!-- 3D lens: specular highlight — bright crescent top-left -->
        <radialGradient id="scope-specular" cx="36%" cy="28%" r="42%">
          <stop offset="0%"   stop-color="rgba(255,255,255,0.28)" />
          <stop offset="55%"  stop-color="rgba(255,255,255,0.06)" />
          <stop offset="100%" stop-color="rgba(255,255,255,0)" />
        </radialGradient>
        <!-- 3D lens: subtle bottom-right counter-glow for depth -->
        <radialGradient id="scope-depth" cx="72%" cy="76%" r="38%">
          <stop offset="0%"   stop-color="rgba(180,120,20,0.10)" />
          <stop offset="100%" stop-color="rgba(180,120,20,0)" />
        </radialGradient>
        <!-- Specular glint: feathered radial gradient, top-left -->
        <radialGradient id="scope-glint" gradientUnits="userSpaceOnUse"
                        cx="62" cy="52" r="24" fx="62" fy="50">
          <stop offset="0%"   stop-color="rgba(255,255,255,0.80)" />
          <stop offset="30%"  stop-color="rgba(255,255,255,0.30)" />
          <stop offset="100%" stop-color="rgba(255,255,255,0)" />
        </radialGradient>
        <clipPath id="scope-lens-clip">
          <circle cx=${cx} cy=${cy} r=${lensR} />
        </clipPath>
        <linearGradient id="scope-lens-sky" x1="0" y1="0" x2="0" y2="1">
          <stop offset="0%"   stop-color=${skyGrad.top} />
          <stop offset="100%" stop-color=${skyGrad.bot} />
        </linearGradient>
        <linearGradient id="scope-lens-grass" x1="0" y1="0" x2="0" y2="1">
          <stop offset="0%" stop-color=${grass.top} />
          <stop offset="100%" stop-color=${grass.bot} />
        </linearGradient>
        <!-- Sun glow — same colour as sun disk, fades from opaque at centre to transparent -->
        <radialGradient id="scope-sun-glow" gradientUnits="userSpaceOnUse"
                        cx=${cx} cy=${sunY} r=${sunGlowR}>
          <stop offset="0%"   stop-color="#fff8c0" stop-opacity="1" />
          <stop offset=${`${Math.round(sunDrawR / sunGlowR * 100)}%`} stop-color="#fff8c0" stop-opacity="1" />
          <stop offset="70%"  stop-color="#ffe060" stop-opacity=${sunGlowPeak * 0.5} />
          <stop offset="100%" stop-color="#ffcc30" stop-opacity="0" />
        </radialGradient>
        <!-- Moon phase mask: white = lit area, black = shadow punched out -->
        <mask id="scope-moon-mask">
          <circle cx=${cx} cy=${moonY} r=${moonDrawR} fill="white" />
          <circle cx=${moonShadowCx} cy=${moonY} r=${moonDrawR} fill="black" />
        </mask>
        <!-- Moon texture pattern - sized to fit the landscape moon disk -->
        <pattern id="scope-moon-tex" x=${cx - moonDrawR - 2} y=${moonY - moonDrawR - 2}
                 width=${moonDrawR * 2 + 4} height=${moonDrawR * 2 + 4} patternUnits="userSpaceOnUse">
          <image href=${MOON_TEX} x="0" y="0" width=${moonDrawR * 2 + 4} height=${moonDrawR * 2 + 4} />
        </pattern>
      </defs>
      <circle cx=${cx} cy=${cy} r="96" fill="url(#scope-bg-glow)" />

      <!-- Full sky + grass + sun/moon scene behind the readout -->
      <g clip-path="url(#scope-lens-clip)">
        <circle cx=${cx} cy=${cy} r=${lensR} fill="url(#scope-lens-sky)" />
        <path d=${`M ${cx - lensR} ${cy} A ${lensR} ${lensR} 0 0 0 ${cx + lensR} ${cy} Z`}
              fill="url(#scope-lens-grass)" />
        ${isDay && html`<${Fragment}>
            <!-- Glow + sun disk as one seamless radial gradient circle -->
            <circle cx=${cx} cy=${sunY} r=${sunGlowR} fill="url(#scope-sun-glow)" />
          </>`}
        ${(isTwil || (!isDay && !isTwil)) && html`<${Fragment}>
            <!-- Lit crescent — full moon disk clipped by the phase mask -->
            <circle cx=${cx} cy=${moonY} r=${moonDrawR}
                    fill="#e7e3d4" mask="url(#scope-moon-mask)" />
            <!-- Texture overlay on lit face -->
            <circle cx=${cx} cy=${moonY} r=${moonDrawR}
                    fill="url(#scope-moon-tex)"
                    mask="url(#scope-moon-mask)"
                    opacity="0.55" />
            <!-- Dim unlit face so the full disk outline is still visible -->
            <circle cx=${cx} cy=${moonY} r=${moonDrawR}
                    fill="rgba(200,192,230,0.08)" stroke="none" />
          </>`}
      </g>
      ${(() => {
        const overlaySVG = getLensOverlaySVG(activeEvent, cx, cy, lensR);
        if (!overlaySVG) return null;
        return html`<g dangerouslySetInnerHTML=${{ __html: overlaySVG }} />`;
      })()}
      ${gradArc.map((seg, i) => html`
        <path key=${i} d=${seg.d} fill="none"
              stroke=${seg.color} stroke-width="4" stroke-linecap="butt" />`)}
      <circle cx=${cx} cy=${cy} r=${R} fill="none" stroke="#c9b08a" stroke-width="1.5" />
      ${ticks.map((t, i) => html`
        <line key=${i} x1=${t.x1} y1=${t.y1} x2=${t.x2} y2=${t.y2}
              stroke=${t.major ? '#c8922a' : t.medium ? '#c9b08a' : '#e0d0b0'}
              stroke-width=${t.major ? 1.5 : 0.75} />`)}
      <line x1=${cx - R + 3} y1=${cy} x2=${cx - 32} y2=${cy} stroke="#d4b896" stroke-width="0.75" />
      <line x1=${cx + 32} y1=${cy} x2=${cx + R - 3} y2=${cy} stroke="#d4b896" stroke-width="0.75" />
      <line x1=${cx} y1=${cy - R + 3} x2=${cx} y2=${cy - 32} stroke="#d4b896" stroke-width="0.75" />
      <line x1=${cx} y1=${cy + 32} x2=${cx} y2=${cy + R - 3} stroke="#d4b896" stroke-width="0.75" />
      <circle cx=${cx} cy=${cy} r="58" fill="none" stroke="#e8d8c0" stroke-width="0.75" />
      <circle cx=${cx} cy=${cy} r="32" fill="none" stroke="#e8d8c0" stroke-width="0.5" />

      ${[[-1,-1],[1,-1],[-1,1],[1,1]].map(([sx, sy], i) => html`
        <g key=${i}>
          <line x1=${cx + sx*72} y1=${cy + sy*72} x2=${cx + sx*60} y2=${cy + sy*72}
                stroke="#c9b08a" stroke-width="1" />
          <line x1=${cx + sx*72} y1=${cy + sy*72} x2=${cx + sx*72} y2=${cy + sy*60}
                stroke="#c9b08a" stroke-width="1" />
        </g>`)}
      ${value != null && html`
        <${Fragment}>
          <line x1=${cx} y1=${cy} x2=${needleX} y2=${needleY}
                stroke="#c8922a" stroke-width="5" stroke-linecap="round" opacity="0.18" />
          <line x1=${cx} y1=${cy} x2=${needleX} y2=${needleY}
                stroke="#c8922a" stroke-width="2.5" stroke-linecap="round" opacity="0.95" />
        </>`}
      <circle cx=${cx} cy=${cy} r="5.5" fill="#f5edd6" stroke="#c8922a" stroke-width="1.5" />
      <circle cx=${cx} cy=${cy} r="2.5" fill="#c8922a" />
      <!-- 3D lens overlays: rim shadow + specular highlight + depth, all on top -->
      <circle cx=${cx} cy=${cy} r=${lensR} fill="url(#scope-rim-shadow)" pointer-events="none" />
      <circle cx=${cx} cy=${cy} r=${lensR} fill="url(#scope-specular)" pointer-events="none" />
      <circle cx=${cx} cy=${cy} r=${lensR} fill="url(#scope-depth)" pointer-events="none" />
      <!-- Specular glint: smooth feathered glow top-left, like light on curved glass -->
      <circle cx="62" cy="52" r="24" fill="url(#scope-glint)" pointer-events="none" />
      <!-- Outer brass bezel ring with a subtle 3D bevel -->
      <circle cx=${cx} cy=${cy} r="97" fill="none"
              stroke="rgba(255,220,140,0.45)" stroke-width="1.5" pointer-events="none" />
      <circle cx=${cx} cy=${cy} r="99" fill="none"
              stroke="rgba(80,40,0,0.25)" stroke-width="1.5" pointer-events="none" />
      ${loading
        ? html`<text x=${cx} y=${cy + 5} text-anchor="middle"
                     fill=${readoutMutedColor} font-size="11" font-family="monospace">· · ·</text>`
        : value != null
          ? html`<${Fragment}>
              <text x=${cx} y="152" text-anchor="middle"
                    fill=${readoutColor} font-size="26"
                    font-family="Fraunces, serif" font-weight="700">
                ${value.toFixed(1)}°
              </text>
              <text x=${cx} y=${cy + 14} text-anchor="middle"
                    fill=${readoutColor} font-size="8"
                    font-family="Manrope, sans-serif" font-weight="700" letter-spacing="1.2">
                SUNSOAK INDEX
              </text>
              <text x=${cx} y=${cy + 23} text-anchor="middle"
                    fill=${glowColor} font-size="9"
                    font-family="Manrope, sans-serif" font-weight="700" letter-spacing="0.5">
                ${cat.label.toUpperCase()}
              </text>
              ${utciEnvShort && html`
                <${Fragment}>
                  <rect x=${cx - 34} y="158" width="68" height="11" rx="3"
                        fill=${glowColor} opacity="0.22" />
                  <text x=${cx} y="166" text-anchor="middle"
                        fill=${readoutColor} font-size="7"
                        font-family="Manrope, sans-serif" font-weight="700" letter-spacing="0.6">
                    ${utciEnvShort.toUpperCase()}
                  </text>
                </>`}
            </>`
          : html`<text x=${cx} y=${cy + 5} text-anchor="middle"
                       fill=${readoutMutedColor} font-size="8"
                       font-family="Manrope, sans-serif" font-weight="700" letter-spacing="0.8">
                   AWAITING
                 </text>`}
    </svg>`;
}

// -------------------------------------------------------------------
// PRECIPICON - rain drop or snowflake SVG for the precip cell.
// -------------------------------------------------------------------
//   precip - mm/h rainfall
//   snow   - cm/h snowfall
//   size   - pixel size (default 28 to match CloudIcon)
//
//   Intensity bands:
//     dry          - faint dash
//     light rain   (< 1 mm/h)  - 1 drop
//     moderate rain(< 4 mm/h)  - 2 drops
//     heavy rain   (- 4 mm/h)  - 3 drops
//     light snow   (< 0.5cm/h) - 1 flake
//     heavy snow   (- 0.5cm/h) - 2 flakes
//     mixed        - 1 drop + 1 flake
// -------------------------------------------------------------------
export function PrecipIcon({ precip = 0, snow = 0, size = 28 }) {
  const r = size / 2;
  const hasRain = precip > 0;
  const hasSnow = snow > 0;
  const mixed   = hasRain && hasSnow;
  const brass = '#c8922a';
  const ink = '#2a1a08';
  const rain = '#2a6a90';
  const snowBlue = '#3f73c4';
  const sw = Math.max(1.25, size * 0.055);
  const cloud = html`
    <path d=${`M ${size*0.18} ${size*0.36}
              H ${size*0.72}
              C ${size*0.86} ${size*0.36}, ${size*0.88} ${size*0.20}, ${size*0.72} ${size*0.19}
              C ${size*0.66} ${size*0.04}, ${size*0.42} ${size*0.02}, ${size*0.34} ${size*0.17}
              C ${size*0.22} ${size*0.14}, ${size*0.14} ${size*0.24}, ${size*0.18} ${size*0.36} Z`}
          fill="none" stroke=${ink} stroke-width=${sw} stroke-linecap="round" stroke-linejoin="round" />`;
  const rainStroke = (cx, cy, heavy = false) => html`
    <line x1=${cx + size * 0.04} y1=${cy} x2=${cx - size * 0.04} y2=${cy + size * 0.18}
          stroke=${rain} stroke-width=${heavy ? sw * 1.1 : sw}
          stroke-linecap="round" />`;
  const flake = (cx, cy, fr) => html`
    <g stroke=${snowBlue} stroke-width=${Math.max(1, size * 0.045)} stroke-linecap="round">
      <line x1=${cx - fr} y1=${cy} x2=${cx + fr} y2=${cy} />
      <line x1=${cx} y1=${cy - fr} x2=${cx} y2=${cy + fr} />
      <line x1=${cx - fr * 0.68} y1=${cy - fr * 0.68} x2=${cx + fr * 0.68} y2=${cy + fr * 0.68} />
      <line x1=${cx + fr * 0.68} y1=${cy - fr * 0.68} x2=${cx - fr * 0.68} y2=${cy + fr * 0.68} />
    </g>`;

  // Dry - just a faint dash
  if (!hasRain && !hasSnow) {
    return html`
      <svg width=${size} height=${size} viewBox=${`0 0 ${size} ${size}`}
           style=${{ flexShrink: 0, display: 'inline-block', verticalAlign: 'middle' }}>
        <line x1=${r - size * 0.2} y1=${r} x2=${r + size * 0.2} y2=${r}
              stroke=${brass} stroke-width="1.5" stroke-linecap="round" opacity="0.75" />
      </svg>`;
  }

  // Rain intensity - number of drops
  const rainDrops = !hasRain ? 0 : precip < 1 ? 1 : precip < 4 ? 2 : 3;
  // Snow intensity - number of flakes
  const snowFlakes = !hasSnow ? 0 : snow < 0.5 ? 1 : 2;
  const fr = size * 0.08;   // flake arm length

  // Layout: spread items evenly across the icon width
  const items = (mixed ? rainDrops + snowFlakes : hasRain ? rainDrops : snowFlakes);
  const spacing = size / (items + 1);

  return html`
    <svg width=${size} height=${size} viewBox=${`0 0 ${size} ${size}`}
         style=${{ flexShrink: 0, display: 'inline-block', verticalAlign: 'middle', overflow: 'visible' }}>
      ${cloud}
      ${mixed
        ? html`
            ${Array.from({ length: rainDrops }, (_, i) => html`
              <${Fragment} key=${'d' + i}>
                ${rainStroke(spacing * (i + 1), size * 0.52, false)}
              </>`)}
            ${Array.from({ length: snowFlakes }, (_, i) =>
              flake(spacing * (rainDrops + i + 1), size * 0.68, fr)
            )}`
        : hasRain
          ? Array.from({ length: rainDrops }, (_, i) => html`
              <${Fragment} key=${i}>
                ${rainStroke(spacing * (i + 1), size * 0.53, precip >= 4)}
              </>`)
          : Array.from({ length: snowFlakes }, (_, i) =>
              flake(spacing * (i + 1), size * 0.66, fr)
            )}
    </svg>`

}
