{"id":4192,"date":"2025-07-06T13:51:34","date_gmt":"2025-07-06T13:51:34","guid":{"rendered":"https:\/\/paragliding-in-madeira.com\/weather\/?page_id=4192"},"modified":"2026-04-12T21:57:07","modified_gmt":"2026-04-12T20:57:07","slug":"airport-live","status":"publish","type":"page","link":"https:\/\/paragliding-in-madeira.com\/weather\/airport-live\/","title":{"rendered":"Airport live"},"content":{"rendered":"\n<style>\n  \/* --- CONTAINER & MAP --- *\/\n  .madeira-weather-map {\n    position: relative;\n    width: 100%;\n    max-width: 1000px;\n    margin: 0 auto;\n    overflow: hidden;\n  }\n\n  \/* Custom Style for Santa Cruz Pin *\/\n  .pos-santacruz .pin-crop {\n    background: rgba(255, 255, 255, 0.9); \n    width: 46px;   \n    height: 46px;\n    border-radius: 50%;\n  }\n\n  \/* VRB text style *\/\n  .airport-vrb {\n    position: absolute;\n    top: 3px;\n    font-size: 10px;\n    font-weight: normal;\n    color: #185e96;\n    display: none;\n    z-index: 3;\n  }\n\n  \/* Gust speed number style *\/\n  .gust-value {\n    color: orange; \n    font-weight: 500;\n    padding-left:1px;\n  }\n\n  .madeira-weather-map img {\n    width: 100%;\n    height: auto;\n    display: block;\n  }\n\n  \/* --- WIDGET ANCHOR --- *\/\n  .weather-pin {\n    position: absolute;\n    z-index: 10;\n    transform: translate(-50%, -50%);\n    transition: transform 0.3s ease;\n    cursor: pointer; \n  }\n  \n  \/* Hover effects *\/\n  @media (min-width: 768px) {\n    .weather-pin:hover {\n      transform: translate(-50%, -50%) scale(2.0); \n      z-index: 100;\n    }\n  }\n  .weather-pin:active {\n    transform: translate(-50%, -50%) scale(1.5);\n  }\n\n  \/* --- THE CROPPER (The Circular Mask) --- *\/\n  .pin-crop {\n    width: 44px;   \n    height: 33px; \n    border-radius: 50%;\n    background: rgba(255, 255, 255, 0.7); \n    box-shadow: 0 2px 4px rgba(0,0,0,0.2);\n    overflow: hidden; \n    position: relative;\n    transition: all 0.3s ease; \n  }\n  \n  \/* --- WINDGURU IFRAME ADJUSTMENTS (For the hidden trigger) --- *\/\n  .wg-content { width: 100%; margin-top: -11px; padding-right: 10px; }\n  .wg-sizer { max-width: 46px; }\n  .wg-proxy-layer { position: absolute; top: 0; left: 0; width: 100%; height: 100%; opacity: 0; z-index: 5; }\n\n  \/* --- Custom Style for LPMA (Madeira) Pin --- *\/\n  .pos-lpma .pin-crop {\n    background: rgba(255, 255, 255, 0.85); \n    width: 46px;   \n    height: 46px;\n    border-radius: 50%;\n  }\n  \n  \/* --- Custom Style for LPPS (Porto Santo) Pin --- *\/\n  .pos-lpps .pin-crop {\n    width: 48px;\n    height: 45px;\n    background: rgba(255, 255, 255, 0.8); \n  }\n\n  \/* --- The Common Airport Widget Styles --- *\/\n  .airport-container {\n    display: flex; flex-direction: column; align-items: center; justify-content: center;\n    height: 100%; width: 100%; position: relative; font-family: Arial, sans-serif; color: #333;\n  }\n\n  .airport-arrow-icon { width: 21px; height: 17px; fill: #185e96; position: absolute; top: 1px; transition: transform 1s ease-out; }\n\n    .airport-speed-gust-text { color: darkblue; font-weight: bold; font-size: 12px; margin-top: 14px; z-index: 2; white-space: nowrap; display: inline-block; }\n\n\n  .airport-unit { font-size: 7px; line-height: 8px; color: #666; margin-top: -2px; }\n\n  \/* --- DESKTOP \"GROW\" MODE --- *\/\n  @media (min-width: 768px) {\n    .weather-pin { transform: translate(-50%, -50%) scale(1.9); }\n    .airport-arrow-icon { width: 22px; height: 18px; top: 1px; }\n    .airport-speed-gust-text { font-size: 12px; margin-top: 11px; }\n    .airport-unit { font-size: 8px; }\n    .airport-vrb { top: 1px; }\n  }\n\n  \/* --- COORDINATES --- *\/\n  .pos-lpma { top: 73%; left: 62%; } \n  .pos-santacruz { top: 85%; left: 55%; }\n  .pos-lpps { top: 15%; left: 90%; } \n\n\/* Mobile-only adjustment for Santa Cruz Pin position *\/\n@media (max-width: 767px) {\n  .pos-santacruz {\n    left: 49% !important; \/* Adjust this number to move it further left or right *\/\n  }\n}\n\n<\/style>\n\n<div class=\"madeira-weather-map\">\n  <img decoding=\"async\" src=\"https:\/\/paragliding-in-madeira.com\/weather\/wp-content\/uploads\/2024\/09\/Madeira-Porto-Santo.png\" alt=\"Madeira Weather Map\">\n\n  <div class=\"weather-pin pos-santacruz\" data-airport=\"santacruz\">\n    <div class=\"pin-crop\">\n        <div class=\"wg-proxy-layer\" id=\"wg-trigger-sc\">\n          <script id=\"wglive_15575_1767102502139\">\n          (function(w,d){var l=function(){var a=[\"spot=15575\",\"uid=wglive_15575_1767102502139\",\"color=light\",\"wj=kmh\",\"tj=c\",\"avg_min=0\",\"gsize=200\",\"msize=230\",\"m=45\",\"arrow=y\",\"show=n,g,c,f,m\"];var s=d.createElement(\"script\");var t=d.getElementsByTagName(\"script\")[0];s.src=\"https:\/\/www.windguru.cz\/js\/wglive.php?\"+(a.join(\"&\"));t.parentNode.insertBefore(s,t);};w.addEventListener?w.addEventListener(\"load\",l,false):w.attachEvent(\"onload\",l);})(window,document);\n          <\/script>\n        <\/div>\n        <div class=\"airport-container\">\n            <svg class=\"airport-arrow-icon\" id=\"santacruz-arrow\" viewBox=\"0 0 24 24\"><path d=\"M12 2L4.5 20.29l.71.71L12 18l6.79 3 .71-.71z\"\/><\/svg>\n            <div class=\"airport-speed-gust-text\" id=\"santacruz-speed-gust\">&#8212;<\/div>\n            <div class=\"airport-unit\" id=\"santacruz-unit\">km\/h<\/div>\n        <\/div>\n    <\/div>\n  <\/div>\n\n  <div class=\"weather-pin pos-lpma\" data-airport=\"lpma\">\n    <div class=\"pin-crop\">\n        <div class=\"airport-container\">\n            <svg class=\"airport-arrow-icon\" id=\"lpma-arrow\" viewBox=\"0 0 24 24\"><path d=\"M12 2L4.5 20.29l.71.71L12 18l6.79 3 .71-.71z\"\/><\/svg>\n            <div class=\"airport-vrb\" id=\"lpma-vrb\">VRB<\/div>\n            <div class=\"airport-speed-gust-text\" id=\"lpma-speed-gust\">&#8212;<\/div>\n            <div class=\"airport-unit\" id=\"lpma-unit\">km\/h<\/div>\n        <\/div>\n    <\/div>\n  <\/div>\n\n  <div class=\"weather-pin pos-lpps\" data-airport=\"lpps\">\n    <div class=\"pin-crop\">\n        <div class=\"airport-container\">\n            <svg class=\"airport-arrow-icon\" id=\"lpps-arrow\" viewBox=\"0 0 24 24\"><path d=\"M12 2L4.5 20.29l.71.71L12 18l6.79 3 .71-.71z\"\/><\/svg>\n            <div class=\"airport-vrb\" id=\"lpps-vrb\">VRB<\/div>\n            <div class=\"airport-speed-gust-text\" id=\"lpps-speed-gust\">&#8212;<\/div>\n            <div class=\"airport-unit\" id=\"lpps-unit\">km\/h<\/div>\n        <\/div>\n    <\/div>\n  <\/div>\n<\/div>\n\n<script>\n\/\/ Returns \"YYYY-MM-DD\" Madeira time\nfunction getMadeiraDateString() {\n  return new Date().toLocaleDateString('en-CA', { timeZone: 'Atlantic\/Madeira' });\n}\n\ndocument.addEventListener(\"DOMContentLoaded\", function() {\n const STALE_MS = 60 * 60 * 1000;\n  const REFRESH_MS = 1 * 60 * 1000;\n\n  const airports = [\n    {\n      id: 'santacruz',\n      basePath: 'https:\/\/paragliding-in-madeira.com\/weather\/wp-content\/uploads\/weather-stations\/santacruz\/',\n      speedGustId: 'santacruz-speed-gust', arrowId: 'santacruz-arrow', unitId: 'santacruz-unit',\n      isWindguru: true\n    },\n    {\n      id: 'lpma',\n      jsonUrl: 'https:\/\/paragliding-in-madeira.com\/weather\/wp-content\/uploads\/metar-wind-lpma.json',\n      speedGustId: 'lpma-speed-gust', arrowId: 'lpma-arrow', vrbId: 'lpma-vrb', unitId: 'lpma-unit',\n      lightbox: 'https:\/\/paragliding-in-madeira.com\/weather\/lpma\/'\n    },\n    {\n      id: 'lpps',\n      jsonUrl: 'https:\/\/paragliding-in-madeira.com\/weather\/wp-content\/uploads\/metar-wind-lpps.json',\n      speedGustId: 'lpps-speed-gust', arrowId: 'lpps-arrow', vrbId: 'lpps-vrb', unitId: 'lpps-unit',\n      lightbox: 'https:\/\/paragliding-in-madeira.com\/weather\/lpps\/'\n    }\n  ];\n\n  function showWidget(a) { [a.speedGustId, a.unitId, a.arrowId].forEach(id => { const el = document.getElementById(id); if (el) el.style.display = 'block'; }); }\n  function hideWidget(a) { \n    const sg = document.getElementById(a.speedGustId); if (sg) { sg.innerText = '--'; sg.style.display = 'none'; }\n    [a.unitId, a.arrowId, a.vrbId].forEach(id => { const el = document.getElementById(id); if (el) el.style.display = 'none'; });\n  }\n\n  async function updateAirportWidget(a) {\n    try {\n      const finalUrl = a.id === 'santacruz' \n        ? a.basePath + getMadeiraDateString() + '.json' \n        : a.jsonUrl;\n\n      const res = await fetch(finalUrl + '?t=' + Date.now());\n      if (!res.ok) throw new Error('HTTP ' + res.status);\n      \n      const data = await res.json();\n      if (!data || data.length === 0) { hideWidget(a); return; }\n      \n      const latest = data[data.length - 1];\n      if (Date.now() - new Date(latest.time).getTime() > STALE_MS) { hideWidget(a); return; }\n\n      let speed, gust, dir;\n      if (a.id === 'santacruz') {\n          speed = Math.round(latest.windSpeed.kph); gust = Math.round(latest.windGust.kph); dir = latest.winddir;\n      } else {\n          speed = latest.speed === null ? null : Math.round(latest.speed);\n          gust = latest.gust === null ? null : Math.round(latest.gust);\n          dir = typeof latest.direction === 'number' ? latest.direction : null;\n      }\n\n            showWidget(a);\n      const speedGustEl = document.getElementById(a.speedGustId);\n      if (speedGustEl) {\n          speedGustEl.innerHTML = (gust !== null && gust > speed) ? `${speed} \/<span class=\"gust-value\">${gust}<\/span>` : String(speed);\n\n          const charCount = speedGustEl.textContent.length;\n          if (window.innerWidth <= 600) {\n            if (charCount >= 8) {\n              speedGustEl.style.setProperty('font-size', '9.9px', 'important');\n              speedGustEl.style.setProperty('letter-spacing', '-0.4px', 'important');\n            } else {\n              speedGustEl.style.setProperty('font-size', '12px', 'important');\n              speedGustEl.style.setProperty('letter-spacing', '-0.1px', 'important');\n            }\n          } else {\n            speedGustEl.style.fontSize = \"\";\n            speedGustEl.style.letterSpacing = \"\";\n          }\n      }\n\n      const arrowEl = document.getElementById(a.arrowId);\n      const vrbEl = document.getElementById(a.vrbId);\n\n\/\/ Check if station uses METAR VRB logic (LPMA and LPPS)\n      const isMetarStation = (a.id === 'lpma' || a.id === 'lpps');\n      \n      if (isMetarStation && dir === 1) {\n          if (arrowEl) arrowEl.style.display = 'none';\n          if (vrbEl) vrbEl.style.display = 'block';\n      } else {\n          if (vrbEl) vrbEl.style.display = 'none';\n          if (arrowEl) {\n              const isCalm = (dir === null || (dir === 0 && speed === 0));\n              if (isCalm) {\n                  arrowEl.style.display = 'none';\n              } else {\n                  arrowEl.style.display = 'block';\n                  arrowEl.style.transform = `rotate(${(dir + 180) % 360}deg)`;\n              }\n          }\n      }\n    } catch (e) { hideWidget(a); }\n  }\n\n  \/\/ LIGHTBOX\n  if (!document.getElementById('airport-lightbox')) {\n    document.body.insertAdjacentHTML('beforeend', `<div id=\"airport-lightbox\" style=\"display:none;position:fixed;top:0;left:0;width:100vw;height:100vh;background:rgba(0,0,0,0.7);z-index:9999;justify-content:center;align-items:center;\"><div style=\"position:relative;width:90%;max-width:1000px;height:75%;border-radius:8px;overflow:hidden;background:white;\"><iframe id=\"airport-lightbox-iframe\" src=\"\" style=\"width:100%;height:100%;border:none;\"><\/iframe><button id=\"airport-lightbox-close\" style=\"position:absolute;top:7px;right:11px;background:rgba(0,0,0,0.2);border:none;color:white;font-size:24px;cursor:pointer;border-radius:50%;width:35px;height:35px;\">\u2715<\/button><\/div><\/div>`);\n  }\n  const lightbox = document.getElementById('airport-lightbox');\n  const iframe = document.getElementById('airport-lightbox-iframe');\n\n  document.querySelectorAll('.weather-pin[data-airport]').forEach(pin => {\n    pin.addEventListener('click', () => {\n      const a = airports.find(x => x.id === pin.getAttribute('data-airport'));\n      if (a.isWindguru) {\n          \/\/ Triggers Windguru native popup\n          const trigger = pin.querySelector('.wg-proxy-layer div div');\n          if (trigger) trigger.click();\n      } else {\n          iframe.src = a.lightbox + '?nocache=' + Date.now();\n          lightbox.style.display = 'flex';\n      }\n    });\n  });\n\n  document.getElementById('airport-lightbox-close').addEventListener('click', () => { iframe.src = ''; lightbox.style.display = 'none'; });\n  lightbox.addEventListener('click', (e) => { if (e.target === lightbox) { iframe.src=''; lightbox.style.display='none'; } });\n\n  function refreshAll() { airports.forEach(updateAirportWidget); }\n  refreshAll(); setInterval(refreshAll, REFRESH_MS);\n});\n<\/script>\n\n\n\n<div class=\"wp-block-buttons is-content-justification-center is-layout-flex wp-container-core-buttons-is-layout-a89b3969 wp-block-buttons-is-layout-flex\">\n<div class=\"wp-block-button\"><a class=\"wp-block-button__link wp-element-button\" href=\"https:\/\/www.paragliding-in-madeira.com\/weather\">Home<\/a><\/div>\n\n\n\n<div class=\"wp-block-button\"><a class=\"wp-block-button__link wp-element-button\" href=\"https:\/\/paragliding-in-madeira.com\/weather\/weather-info\/\">Weather info<\/a><\/div>\n<\/div>\n\n\n\n<p class=\"has-text-align-center\"><a href=\"#travel\">Find your best route<\/a> | <a href=\"https:\/\/paragliding-in-madeira.com\/weather\/metar-history\/\" title=\"\">METAR and TAF<\/a><\/p>\n\n\n\n<div style=\"height:13px\" aria-hidden=\"true\" class=\"wp-block-spacer\"><\/div>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<p class=\"has-text-align-center\">Madeira Airport <a style=\"text-decoration:none\" href=\"https:\/\/youtube.com\/@madeiraairport\" target=\"_blank\" rel=\"noopener\" title=\"\">live<\/a> info<\/p>\n\n\n\n<p class=\"has-text-align-center\">Webcam, real time airplane tracking and runway weather conditions<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<div style=\"height:24px\" aria-hidden=\"true\" class=\"wp-block-spacer\"><\/div>\n\n\n\n<!-- Time zones -->\n<style>\n  \/* Widget Design: \"Cockpit Light\" & \"Delicate\"\n    Concept: Clean lines, high legibility, airy whitespace. \n  *\/\n  #airport-time-widget {\n    font-family: -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, \"Helvetica Neue\", Arial, sans-serif;\n    background: rgba(237,237,237,0.8);\n    border: 1px solid #eef2f5;\n    border-radius: 16px;\n    box-shadow: 0 10px 30px rgba(0, 0, 0, 0.04);\n    max-width: 505px;\n    margin: 20px auto;\n    overflow: hidden;\n    color: #444;\n  }\n\n  \/* --- Top Section: Madeira Time --- *\/\n  .atw-header {\n    text-align: center;\n    padding: 30px 20px 10px;\n  }\n\n  .atw-title {\n    font-size: 0.8rem;\n    text-transform: uppercase;\n    letter-spacing: 2px;\n    color: #8898aa;\n    margin-bottom: 5px;\n    font-weight: 600;\n  }\n\n  .atw-main-time {\n    font-size: 3.0rem; \/* Large but delicate *\/\n    font-weight: 200;  \/* Thin font weight *\/\n    line-height: 1;\n    color: #2d3436;\n    letter-spacing: -2px;\n  }\n\n  .atw-date {\n    font-size: 1.0rem;\n    color: #8898aa;\n    margin-top: 5px;\n    font-weight: 300;\n  }\n\n  \/* --- Middle Section: Sun Data --- *\/\n  .atw-sun-bar {\n    display: flex;\n    justify-content: center;\n    gap: 30px;\n    padding: 10px 0 25px;\n    font-size: 0.95rem;\n    color: transparent;\n  }\n\n  .atw-sun-item {\n    display: flex;\n    align-items: center;\n    gap: 6px;\n  }\n\n  .atw-sun-item span.label {\n    font-weight: 400;\n    color: rgba(77,77,77,0.5); \/\/ sunrise and set letters color\n  }\n\n  .atw-sun-item span.val {\n    font-weight: 600;\n    color: rgba(28,28,28,0.8); \/\/ sunrise time color\n  }\n\n  \/* --- Bottom Section: World Grid --- *\/\n  .atw-world-grid {\n    background: #f8f9fa; \/* Very light grey for contrast *\/\n    border-top: 1px solid #eef2f5;\n    padding: 15px;\n    display: grid;\n    \/* Responsive grid: fits as many as possible, minimum 90px width *\/\n    grid-template-columns: repeat(auto-fit, minmax(90px, 1fr)); \n    gap: 7px;\n  }\n\n  .atw-city {\n    display: flex;\n    flex-direction: column;\n    align-items: center;\n    text-align: center;\n  }\n\n  .atw-city-name {\n    font-size: 0.70rem;\n    text-transform: uppercase;\n    letter-spacing: 1px;\n    color: #aaa;\n    margin-bottom: 3px;\n    font-weight: 500;\n  }\n\n  .atw-city-time {\n    font-size: 1.0rem;\n    font-weight: 410;\n    color: #444;\n  }\n\n  \/* Mobile adjustment *\/\n  @media (max-width: 600px) {\n    .atw-main-time { font-size: 3rem; }\n    .atw-sun-bar { gap: 15px; font-size: 0.8rem; }\n    .atw-world-grid { grid-template-columns: repeat(4, 1fr); } \/* 2 rows of 4 on mobile *\/\n  }\n<\/style>\n\n<div id=\"airport-time-widget\">\n  \n  <div class=\"atw-header\">\n    <div class=\"atw-title\">Madeira Airport Time<\/div>\n    <div class=\"atw-main-time\" id=\"madeira-clock\">&#8211;:&#8211;<\/div>\n    <div class=\"atw-date\" id=\"madeira-date\">Loading&#8230;<\/div>\n  <\/div>\n\n  <div class=\"atw-sun-bar\">\n    <div class=\"atw-sun-item\">\n      <span class=\"label\">Sunrise<\/span>\n      <span class=\"val\" id=\"sun-rise\">&#8211;:&#8211;<\/span>\n    <\/div>\n    <div class=\"atw-sun-item\">\n      <span class=\"label\">Sunset<\/span>\n      <span class=\"val\" id=\"sun-set\">&#8211;:&#8211;<\/span>\n    <\/div>\n    <div class=\"atw-sun-item\">\n      <span class=\"label\">Day Length<\/span>\n      <span class=\"val\" id=\"day-len\">&#8211;h &#8211;m<\/span>\n    <\/div>\n  <\/div>\n\n  <div class=\"atw-world-grid\">\n    <\/div>\n\n<\/div>\n\n<script>\n  \/\/ 1. Configuration: List of cities and their Timezones\n  const worldCities = [\n    { name: \"Azores\", zone: \"Atlantic\/Azores\" },\n    { name: \"London\", zone: \"Europe\/London\" },\n    { name: \"Germany\", zone: \"Europe\/Berlin\" },\n    { name: \"France\", zone: \"Europe\/Paris\" },\n    { name: \"New York\", zone: \"America\/New_York\" },\n    { name: \"Brazil\", zone: \"America\/Sao_Paulo\" },\n{ name: \"China\", zone: \"Asia\/Shanghai\" },\n    { name: \"Australia\", zone: \"Australia\/Sydney\" }   \n  ];\n\n  \/\/ 2. Helper: Get Time String (HH:MM) for a timezone\n  function getTimeInZone(timezone) {\n    const now = new Date();\n    return new Intl.DateTimeFormat('en-GB', {\n      timeZone: timezone,\n      hour: '2-digit',\n      minute: '2-digit',\n      hour12: false \/\/ 24h format as per your preference\n    }).format(now);\n  }\n\n  \/\/ 3. Helper: Get Date String for Madeira\n  function getMadeiraDate() {\n    const now = new Date();\n    return new Intl.DateTimeFormat('en-GB', {\n      timeZone: 'Atlantic\/Madeira',\n      weekday: 'long',\n      day: 'numeric',\n      month: 'long',\n      year: 'numeric'\n    }).format(now);\n  }\n\n  \/\/ 4. Helper: Calculate Day Length (Time difference)\n  function calculateDayLength(sunriseStr, sunsetStr) {\n    if(!sunriseStr || !sunsetStr) return \"--\";\n    \n    \/\/ Parse \"08:03\" format\n    const [riseH, riseM] = sunriseStr.split(':').map(Number);\n    const [setH, setM] = sunsetStr.split(':').map(Number);\n\n    const startMins = riseH * 60 + riseM;\n    const endMins = setH * 60 + setM;\n    const diff = endMins - startMins;\n\n    const hours = Math.floor(diff \/ 60);\n    const mins = diff % 60;\n    \n    return `${hours}h ${mins}m`;\n  }\n\n  \/\/ 5. Main Update Function (Clocks)\n  function updateClocks() {\n    \/\/ Update Madeira\n    document.getElementById('madeira-clock').innerText = getTimeInZone('Atlantic\/Madeira');\n    document.getElementById('madeira-date').innerText = getMadeiraDate();\n\n    \/\/ Update World Grid\n    const gridContainer = document.querySelector('.atw-world-grid');\n    gridContainer.innerHTML = ''; \/\/ Clear current\n\n    worldCities.forEach(city => {\n      const time = getTimeInZone(city.zone);\n      const html = `\n        <div class=\"atw-city\">\n          <div class=\"atw-city-name\">${city.name}<\/div>\n          <div class=\"atw-city-time\">${time}<\/div>\n        <\/div>\n      `;\n      gridContainer.insertAdjacentHTML('beforeend', html);\n    });\n  }\n\n  \/\/ 6. Fetch Sun Data\n  async function fetchSunData() {\n    try {\n      \/\/ Fetching from your specific URL\n      const response = await fetch('https:\/\/paragliding-in-madeira.com\/weather\/wp-content\/moon-time\/solunar-data.json');\n      const data = await response.json();\n\n      if (data && data.today) {\n        document.getElementById('sun-rise').innerText = data.today.sunrise;\n        document.getElementById('sun-set').innerText = data.today.sunset;\n        \n        \/\/ Auto-calculate length\n        document.getElementById('day-len').innerText = calculateDayLength(data.today.sunrise, data.today.sunset);\n      }\n    } catch (error) {\n      console.log('Sun data fetch error:', error);\n      \/\/ Fallback or leave as --:--\n    }\n  }\n\n  \/\/ Initialize\n  document.addEventListener('DOMContentLoaded', () => {\n    fetchSunData();     \/\/ Get static data once\n    updateClocks();     \/\/ Run clock once immediately\n    setInterval(updateClocks, 10000); \/\/ Update clocks every 10 seconds (no seconds display, so 10s is fine)\n  });\n\n<\/script>\n\n\n\n<!-- Brilliant fit\n<div style=\"\n  margin-left: calc(-50vw + 50%);\n  margin-right: calc(-50vw + 50%);\n  width: 101vw;\n  position: relative;\n  left: 49%;\n  transform: translateX(-51%);\n  overflow: hidden; \/* Hides any horizontal or vertical spill *\/\n\">\n  <iframe\n    src=\"https:\/\/www.airnavradar.com\/live-widget\/LPMA?type=table&arrivalsOnly=true&showIcao=true&limit=10\"\n    scrolling=\"no\" \n    style=\"\n      display: block;\n      width: 100%;\n      height: 333px; \/* Adjusted height to fit the 10 rows without scrolling *\/\n      border: 0;\n      overflow: hidden;\n      transform: scale(0.93);\n      transform-origin: top center; \/* Centers the scaling *\/\n    \"\n  ><\/iframe>\n<\/div>\n-->\n\n\n\n<div style=\"height:9px\" aria-hidden=\"true\" class=\"wp-block-spacer\"><\/div>\n\n\n\n<!-- Added loader below\n<style>\n  \/* The Container Grid *\/\n  .airport-dashboard {\n    display: grid;\n    grid-template-columns: 1fr; \/* Stacked on Mobile *\/\n    gap: 30px;\n    width: 100%;\n    margin: 20px 0;\n  }\n\n .fnc-label {\n    display: inline-block;\n    padding: 4px 8px;\n    background: rgba(237,237,237,0.9);\n    color: #8898aa;\n    font-size: 0.95rem;\n    font-weight: 500;\n    text-transform: uppercase;\n    letter-spacing: 1.1px;\n    border-radius: 20px;\n    margin-bottom: 2px;\n  }\n\n  \/* Desktop View: Side by Side *\/\n  @media (min-width: 1024px) {\n    .airport-dashboard {\n      grid-template-columns: 1fr 1fr; \/* Two equal columns *\/\n      max-width: 933px;\n      margin: 0 auto;\n    }\n  }\n\n   \/* Mobile View: Clean Edge-to-Edge *\/\n  @media (max-width: 767px) {\n    .mobile-edge {\n\/* This makes the box be the exact width of the screen *\/\n      width: 102vw;\n      margin-left: 43%;\n      border-radius: 14px; \/* round edges makes it look more integrated on mobile *\/\n      overflow: hidden; \/* Hides any horizontal or vertical spill *\/\n      height: 400px; \/* Adjusted height to fit the 10 rows without scrolling *\/\n      border: 0;\n      transform: translateX(-52%) scale(0.92);\n      transform-origin: top center; \/* Centers the scaling *\/\n    }\n    \n    .flight-box {\n      border-left: none ;\n      border-right: none ;\n      border-radius: 0 ;\n    }\n  }\n\n  .flight-box {\n    background: rgba(237,237,237,0.8); \/* Light, clean background *\/\n    border: 1px solid #eee;\n    border-radius: 12px;\n    overflow: hidden;\n    box-shadow: 0 4px 12px rgba(0,0,0,0.05);\n  }\n\n  .flight-header {\n    background: transparent; \/* Professional Slate Blue *\/\n    color: Grey;\n    padding: 12px;\n    text-align: center;\n    font-weight: bold;\n    letter-spacing: 1px;\n    text-transform: uppercase;\n    font-size: 14px;\n  }\n\n  .iframe-wrapper {\n    overflow: hidden;\n    line-height: 0;\n  }\n<\/style>\n\n<div class=\"airport-dashboard\">\n  \n  <div class=\"mobile-edge\">\n    <div class=\"flight-box\">\n      <div class=\"flight-header\"><span class=\"fnc-label\">Arrivals<\/span><\/div>\n      <div class=\"iframe-wrapper\">\n        <iframe\n          src=\"https:\/\/www.airnavradar.com\/live-widget\/LPMA?type=table&arrivalsOnly=true&showIcao=true&limit=10\"\n          scrolling=\"no\" \n          style=\" margin-left: -6px; width: 100%; height: 395px; border: 0;\"\n        ><\/iframe>\n      <\/div>\n    <\/div>\n  <\/div>\n\n  <div class=\"mobile-edge\">\n    <div class=\"flight-box\">\n      <div class=\"flight-header\"><span class=\"fnc-label\">Departures<\/span><\/div>\n       <div class=\"iframe-wrapper\">\n        <iframe\n          src=\"https:\/\/www.airnavradar.com\/live-widget\/LPMA?type=table&departuresOnly=false&showIcao=true&limit=10\"\n          scrolling=\"no\" \n          style=\" margin-left: -6px; width: 100%; height: 395px; border: 0;\"\n        ><\/iframe>\n      <\/div>\n    <\/div>\n  <\/div>\n<\/div>\n-->\n\n\n\n<!-- Arrivals and departures airport board -->\n<style>\n  \/* The Container Grid *\/\n  .airport-dashboard {\n    display: grid;\n    grid-template-columns: 1fr; \/* Stacked on Mobile *\/\n    gap: 30px;\n    width: 100%;\n    margin: 20px 0;\n  }\n\n .fnc-label {\n    display: inline-block;\n    padding: 4px 8px;\n    background: rgba(237,237,237,0.9);\n    color: #8898aa;\n    font-size: 0.95rem;\n    font-weight: 500;\n    text-transform: uppercase;\n    letter-spacing: 1.1px;\n    border-radius: 20px;\n    margin-bottom: 2px;\n  }\n\n  \/* Desktop View: Side by Side *\/\n  @media (min-width: 1024px) {\n    .airport-dashboard {\n      grid-template-columns: 1fr 1fr; \/* Two equal columns *\/\n      max-width: 933px;\n      margin: 0 auto;\n    }\n  }\n\n   \/* Mobile View: Clean Edge-to-Edge *\/\n  @media (max-width: 767px) {\n    .mobile-edge {\n\/* This makes the box be the exact width of the screen *\/\n      width: 102vw;\n      margin-left: 43%;\n      border-radius: 14px; \/* round edges makes it look more integrated on mobile *\/\n      overflow: hidden; \/* Hides any horizontal or vertical spill *\/\n      height: 400px; \/* Adjusted height to fit the 10 rows without scrolling *\/\n      border: 0;\n      transform: translateX(-52%) scale(0.92);\n      transform-origin: top center; \/* Centers the scaling *\/\n    }\n    \n    .flight-box {\n      border-left: none ;\n      border-right: none ;\n      border-radius: 0 ;\n    }\n  }\n\n  .flight-box {\n    background: rgba(237,237,237,0.8); \/* Light, clean background *\/\n    border: 1px solid #eee;\n    border-radius: 12px;\n    overflow: hidden;\n    box-shadow: 0 4px 12px rgba(0,0,0,0.05);\n  }\n\n  .flight-header {\n    background: transparent; \/* Professional Slate Blue *\/\n    color: Grey;\n    padding: 12px;\n    text-align: center;\n    font-weight: bold;\n    letter-spacing: 1px;\n    text-transform: uppercase;\n    font-size: 14px;\n  }\n\n  .iframe-wrapper {\n    overflow: hidden;\n    line-height: 0;\n    position: relative; \/* added *\/\n  }\n\n\/* CLEAN, PROFESSIONAL LOADER *\/\n  .flight-loader {\n    position: absolute;\n    inset: 0;\n    display: flex;\n    align-items: center;\n    justify-content: center;\n    font-size: 16px;\n    letter-spacing: 1.5px;\n    text-transform: uppercase;\n    color: #8898aa;\n    background: rgba(237,237,237,0.9);\n    z-index: 2;\n    transition: opacity 0.9s ease;\n  }\n\n  .flight-loader.hide {\n    opacity: 0;\n    pointer-events: none;\n}\n<\/style>\n\n<div class=\"airport-dashboard\">\n\n  <div class=\"mobile-edge\">\n    <div class=\"flight-box\">\n      <div class=\"flight-header\"><span class=\"fnc-label\">Arrivals<\/span><\/div>\n      <div class=\"iframe-wrapper\">\n\n        <div class=\"flight-loader\">Loading live data\u2026<\/div>\n\n        <iframe\n          src=\"https:\/\/www.airnavradar.com\/live-widget\/LPMA?type=table&#038;arrivalsOnly=true&#038;showIcao=true&#038;limit=10\"\n          scrolling=\"no\"\n          style=\"margin-left: -6px; width: 100%; height: 395px; border: 0;\"\n        ><\/iframe>\n\n      <\/div>\n    <\/div>\n  <\/div>\n\n  <div class=\"mobile-edge\">\n    <div class=\"flight-box\">\n      <div class=\"flight-header\"><span class=\"fnc-label\">Departures<\/span><\/div>\n      <div class=\"iframe-wrapper\">\n\n        <div class=\"flight-loader\">Loading live data\u2026<\/div>\n\n        <iframe\n          src=\"https:\/\/www.airnavradar.com\/live-widget\/LPMA?type=table&#038;departuresOnly=false&#038;showIcao=true&#038;limit=10\"\n          scrolling=\"no\"\n          style=\"margin-left: -6px; width: 100%; height: 395px; border: 0;\"\n        ><\/iframe>\n\n      <\/div>\n    <\/div>\n  <\/div>\n\n<\/div>\n\n<script>\n  document.querySelectorAll('.iframe-wrapper iframe').forEach(iframe => {\n    iframe.addEventListener('load', () => {\n      const loader = iframe.parentElement.querySelector('.flight-loader');\n      if (loader) loader.classList.add('hide');\n    });\n  });\n<\/script>\n\n\n\n<!-- tooltip adjusted to mobile below\n\n<p><sub>&#x2708;&#xfe0f; In December, 191,134 people landed*<\/sub><br><sub>* that's like 70% <span title=\"Madeira population \u2248 260,000 people\">of Madeira\u2019s population arriving in 30 days<\/span> <br>&#x1f3dd;&#xfe0f; In 2025, Madeira welcomed 5.6 Million visitors, that's half the <span title=\"Portugal population \u2248 10.8 Million people\"> population of Portugal<\/span><\/sub><\/p>\n\n-->\n\n\n\n<!-- visitors tests below in styling\n\n<style>\n.tip {\n  position: relative;\n text-align: center;\n  cursor: help;\n  text-decoration: underline dotted;\n\/* PREVENTS THE SEARCH POP-UP & TEXT SELECTION *\/\n  -webkit-user-select: none; \/* Safari *\/\n  user-select: none;        \/* Standard *\/\n-webkit-touch-callout: none; \/* iOS long-press menu *\/\n touch-action: manipulation;\n\n}\n\n.tip::after {\n  content: attr(data-tip);\n  position: absolute;\n  bottom: 120%;\n  left: 50%;\n  transform: translateX(-50%);\n  background: rgba(0,0,0,0.85);\n  color: #fff;\n  padding: 6px 8px;\n  border-radius: 6px;\n  white-space: nowrap;\n text-align: center;\n  width: max-content;\n  font-size: 12px;\n  opacity: 0;\n  pointer-events: none;\n  transition: opacity 0.15s ease;\n}\n\n\/* Desktop *\/\n.tip:hover::after,\n\/* Mobile tap *\/\n.tip:active::after {\n  opacity: 1;\n}\n<\/style>\n<p style=\"text-align: center;\">\n  <sub>&#x2708;&#xfe0f; In December,\n<span class=\"tip\" data-tip=\"That's \u2248 70% of Madeira population \u2248 260,000 people\"> \n191,134 people landed\n    <\/span>\n<\/sub>\n  <sub>\n    <br>\n&#x1f3dd;&#xfe0f; In 2025, Madeira welcomed\n <span class=\"tip\" data-tip=\"That's half of Portugal population \u2248 10.8 Million people\">\n      5.6 Million visitors\n    <\/span>\n  <\/sub>\n<\/p>\n\n-->\n\n\n\n<!-- styled further below\n\n<style>\n\/* 1. We anchor the tooltip to the paragraph, not the word *\/\n.centered-container {\n  position: relative;\n  text-align: center;\n  \n  \/* Applying the shield to the whole block *\/\n  -webkit-user-select: none;  \/* Safari\/Chrome *\/\n  -moz-user-select: none;     \/* Firefox *\/\n  -ms-user-select: none;      \/* IE\/Edge *\/\n  user-select: none;          \/* Standard *\/\n  -webkit-touch-callout: none; \/* Prevents iOS context menu *\/\n}\n\n.tip {\n  cursor: help;\n  text-decoration: underline dotted;\n  -webkit-user-select: none;\n  user-select: none;\n  -webkit-touch-callout: none;\n  touch-action: manipulation;\n  \/* We removed 'position: relative' from here *\/\n}\n\n.tip::after {\n  content: attr(data-tip);\n  position: absolute;\n  \/* 2. Positioning relative to the .centered-container *\/\n  bottom: 100%; \n  left: 50%;\n  transform: translateX(-50%);\n  \n  background: rgba(0,0,0,0.85);\n  color: #fff;\n  padding: 6px 8px;\n  border-radius: 6px;\n  white-space: nowrap;\n  width: max-content;\n  font-size: 12px;\n  opacity: 0;\n  pointer-events: none;\n  transition: opacity 0.15s ease;\n  z-index: 999;\n}\n\n.tip:hover::after,\n.tip:active::after {\n  opacity: 1;\n}\n<\/style>\n\n<p class=\"centered-container\">\n  <sub>&#x2708;&#xfe0f; In December, \n    <span class=\"tip\" data-tip=\"That's \u2248 70% of Madeira population \u2248 260,000 people\"> \n      191,134 people landed\n    <\/span>\n  <\/sub>\n  <br>\n  <sub>&#x1f3dd;&#xfe0f; 2025 welcomed \n    <span class=\"tip\" data-tip=\"That's half of Portugal population \u2248 10.8 Million people\">\n      5.6 Million visitors\n    <\/span>\n  <\/sub>\n<\/p>\n\n-->\n\n\n\n<!-- added link below\n<style>\n  \/*  warm background style for the stats box *\/\n  .insight-box {\n    background: rgba(237, 237, 237, 0.9);\n    border: 1px solid #eee;\n    border-radius: 12px;\n    padding: 15px;\n    margin-top: 20px;\n    box-shadow: 0 4px 12px rgba(0,0,0,0.09);\n    \/* Keeps the tooltips centered within this specific box *\/\n    position: relative; \n    text-align: center;\n    \n    \/* Global selection shield *\/\n    -webkit-user-select: none;\n    user-select: none;\n    -webkit-touch-callout: none;\n  }\n\n  \/* The Tooltip logic stays the same but anchored to .insight-box *\/\n  .tip {\n    cursor: help;\n    text-decoration: underline dotted #8898aa;\n    font-weight: 500;\n    color: #444;\n  }\n\n  .tip::after {\n    content: attr(data-tip);\n    position: absolute;\n    bottom: 85%; \/* Adjusted to sit nicely inside the padded box *\/\n    left: 50%;\n    transform: translateX(-50%);\n    background: rgba(0,0,0,0.85);\n    color: #fff;\n    padding: 8px 12px;\n    border-radius: 8px;\n    white-space: nowrap;\n    width: max-content;\n    font-size: 12px;\n    opacity: 0;\n    pointer-events: none;\n    transition: opacity 0.2s ease;\n    z-index: 999;\n  }\n\n  .tip:hover::after,\n  .tip:active::after {\n    opacity: 1;\n  }\n\n  \/* Desktop width sync *\/\n  @media (min-width: 1024px) {\n    .insight-box {\n      max-width: 444px;\n      margin: 20px auto;\n    }\n  }\n<\/style>\n\n<div class=\"insight-box\" role=\"button\">\n  <sub class=\"centered-container\" onselectstart=\"return false;\">&#x2708;&#xfe0f; December brought in\n    <span class=\"tip\" data-tip=\"That's 1.5x Madeira population \u2248 260,000 people\"> \n383,681 people\n    <\/span>\n  <\/sub>\n  <br>\n  <sub class=\"centered-container\" onselectstart=\"return false;\">&#x1f3dd;&#xfe0f; 2025 welcomed \n    <span class=\"tip\" data-tip=\"That's half of Portugal population \u2248 10.8 Million people\">\n      5.7 Million visitors\n    <\/span>\n  <\/sub>\n<\/div>\n-->\n<!-- https:\/\/app.powerbi.com\/view?r=eyJrIjoiNjFhZjg0M2YtMGY5My00Y2VjLTg4ZTUtNDlmNzQ4NjMyOGIxIiwidCI6IjcxOTQwYTg2LTUyYmQtNGVkMy04OWI3LWUwYTdjZDcwNDA0MyIsImMiOjl9 -->\n\n\n\n<!-- added table lightbox below \n<style>\n  \/*  warm background style for the stats box *\/\n  .insight-box {\n    background: rgba(237, 237, 237, 0.9);\n    border: 1px solid #eee;\n    border-radius: 12px;\n    padding: 15px;\n    margin-top: 20px;\n    box-shadow: 0 4px 12px rgba(0,0,0,0.09);\n    \/* Keeps the tooltips centered within this specific box *\/\n    position: relative; \n    text-align: center;\n    \n    \/* Global selection shield *\/\n    -webkit-user-select: none;\n    user-select: none;\n    -webkit-touch-callout: none;\n  }\n\n  \/* The Tooltip logic stays the same but anchored to .insight-box *\/\n  .tip {\n    cursor: help;\n    text-decoration: underline dotted #8898aa;\n    font-weight: 500;\n    color: #444;\n  }\n\n  .tip::after {\n    content: attr(data-tip);\n    position: absolute;\n    bottom: 85%; \/* Adjusted to sit nicely inside the padded box *\/\n    left: 50%;\n    transform: translateX(-50%);\n    background: rgba(0,0,0,0.85);\n    color: #fff;\n    padding: 8px 12px;\n    border-radius: 8px;\n    white-space: nowrap;\n    width: max-content;\n    font-size: 12px;\n    opacity: 0;\n    pointer-events: none;\n    transition: opacity 0.2s ease;\n    z-index: 999;\n  }\n\n  .tip:hover::after,\n  .tip:active::after {\n    opacity: 1;\n  }\n\n  \/* Desktop width sync *\/\n  @media (min-width: 1024px) {\n    .insight-box {\n      max-width: 444px;\n      margin: 20px auto;\n    }\n  }\n<\/style>\n\n<div class=\"insight-box\" role=\"button\">\n  <sub class=\"centered-container\" onselectstart=\"return false;\">\n    <a href=\"https:\/\/app.powerbi.com\/view?r=eyJrIjoiNjFhZjg0M2YtMGY5My00Y2VjLTg4ZTUtNDlmNzQ4NjMyOGIxIiwidCI6IjcxOTQwYTg2LTUyYmQtNGVkMy04OWI3LWUwYTdjZDcwNDA0MyIsImMiOjl9\" style=\"text-decoration: none\" target=\"_blank\" rel=\"noopener noreferrer\">\n      &#x2708;&#xfe0f; \n    <\/a>\nDecember brought in\n    <span class=\"tip\" data-tip=\"That's 82% of Madeira population \u2248 260,000 people\"> \n212,200 people\n    <\/span>\n  <\/sub>\n  <br>\n  <sub class=\"centered-container\" onselectstart=\"return false;\">&#x1f3dd;&#xfe0f; 2025 welcomed \n    <span class=\"tip\" data-tip=\"That's 1\/4 of Portugal population \u2248 10.4 Million people\">\n      2.8 Million visitors\n    <\/span>\n  <\/sub>\n<\/div>\n-->\n\n\n\n<style> \n\/* warm background style for the stats box *\/ \n.insight-box { \n    background: rgba(237, 237, 237, 0.9); \n    border: 1px solid #eee; \n    border-radius: 12px; \n    padding: 15px; \n    margin-top: 20px; \n    box-shadow: 0 4px 12px rgba(0,0,0,0.09); \n    \/* Keeps the tooltips centered within this specific box *\/ \n    position: relative; \n    text-align: center; \n    \/* Global selection shield *\/ \n    -webkit-user-select: none; \n    user-select: none; \n    -webkit-touch-callout: none; \n} \n\n\/* The Tooltip logic stays the same but anchored to .insight-box *\/ \n.tip { \n    cursor: help; \n    text-decoration: underline dotted #8898aa; \n    font-weight: 500; \n    color: #444; \n} \n\n.tip::after { \n    content: attr(data-tip); \n    position: absolute; \n    bottom: 85%; \n    \/* Adjusted to sit nicely inside the padded box *\/ \n    left: 50%; \n    transform: translateX(-50%); \n    background: rgba(0,0,0,0.85); \n    color: #fff; \n    padding: 8px 12px; \n    border-radius: 8px; \n    white-space: nowrap; \n    width: max-content; \n    font-size: 12px; \n    opacity: 0; \n    pointer-events: none; \n    transition: opacity 0.2s ease; \n    z-index: 999; \n} \n\n.tip:hover::after, .tip:active::after { \n    opacity: 1; \n} \n\n\/* Lightbox Overlay Styles *\/\n.stats-lightbox {\n    display: none;\n    position: fixed;\n    top: 0;\n    left: 0;\n    width: 100%;\n    height: 100%;\n    background: rgba(0, 0, 0, 0.85);\n    z-index: 10000;\n    align-items: center;\n    justify-content: center;\n    cursor: zoom-out;\n}\n\n.stats-lightbox img {\n    max-width: 90%;\n    max-height: 80%;\n    border-radius: 8px;\n    box-shadow: 0 5px 25px rgba(0,0,0,0.5);\n    cursor: default;\n}\n\n\/* Desktop width sync *\/ \n@media (min-width: 1024px) { \n    .insight-box { \n        max-width: 444px; \n        margin: 20px auto; \n    } \n}\n<\/style>\n\n<div class=\"insight-box\" role=\"button\"> \n    <sub class=\"centered-container\" onselectstart=\"return false;\"> \n        <a href=\"https:\/\/app.powerbi.com\/view?r=eyJrIjoiNjFhZjg0M2YtMGY5My00Y2VjLTg4ZTUtNDlmNzQ4NjMyOGIxIiwidCI6IjcxOTQwYTg2LTUyYmQtNGVkMy04OWI3LWUwYTdjZDcwNDA0MyIsImMiOjl9\" style=\"text-decoration: none\" target=\"_blank\" rel=\"noopener noreferrer\"> &#x2708;&#xfe0f; <\/a>December brought in <span class=\"tip\" data-tip=\"That's 82% of Madeira population \u2248 260,000 people\"> 212,200 people <\/span> \n    <\/sub> \n    <br> \n    <sub class=\"centered-container\" onselectstart=\"return false;\">\n        <span id=\"open-stats\" style=\"cursor: pointer;\">&#x1f3dd;&#xfe0f;<\/span> 2025 welcomed <span class=\"tip\" data-tip=\"That's 1\/4 of Portugal population \u2248 10.4 Million people\"> 2.8 Million visitors <\/span> \n    <\/sub>\n<\/div>\n\n<div id=\"stats-overlay\" class=\"stats-lightbox\" onclick=\"this.style.display='none'\">\n    <img decoding=\"async\" src=\"https:\/\/paragliding-in-madeira.com\/weather\/wp-content\/uploads\/2026\/02\/2025-airport-arrivals.jpg\" alt=\"2025 Airport Arrivals\" onclick=\"event.stopPropagation()\">\n<\/div>\n\n<script>\n    \/\/ Logic to open the lightbox\n    document.getElementById('open-stats').addEventListener('click', function() {\n        document.getElementById('stats-overlay').style.display = 'flex';\n    });\n<\/script>\n\n\n\n<div style=\"height:22px\" aria-hidden=\"true\" class=\"wp-block-spacer\"><\/div>\n\n\n\n<!-- webcam -->\n<div style=\"position: relative; overflow: hidden; width: 344px; height: 180px; transform: translateX(-4px);\" id=\"iframeContainerAirport\">\n  <div id=\"loaderAirport\" style=\"width:100%;height:100%;background:#eee;display:flex;align-items:center;justify-content:center;transition: opacity 0.2s ease;\">\n    <span>Loading webcam&#8230;<\/span>\n  <\/div>\n<\/div>\n\n<style>\n  .fade-in-iframe {\n    opacity: 0;\n    transition: opacity 0.2s ease;\n    position: absolute; \n    top: -70px; \n    left: -25px; \n    width: 380px; \n    height: 444px; \n    border: 0;\n  }\n  .fade-in-iframe.loaded {\n    opacity: 1;\n  }\n<\/style>\n\n<script>\n  const containerAirport = document.getElementById('iframeContainerAirport');\n  const loaderAirport = document.getElementById('loaderAirport');\n\n  const observerAirport = new IntersectionObserver((entries, observer) => {\n    entries.forEach(entry => {\n      if (entry.isIntersecting) {\n        \/\/ Insert iframe but keep it transparent initially\n        containerAirport.insertAdjacentHTML('beforeend', `\n          <iframe id=\"iframeAirport\" class=\"fade-in-iframe\" src=\"https:\/\/paragliding-in-madeira.com\/weather\/webcams\/\" scrolling=\"no\" tabindex=\"-1\"><\/iframe>\n        `);\n\n        const iframe = document.getElementById('iframeAirport');\n\n        iframe.addEventListener('load', () => {\n          \/\/ Fade out loader\n          loaderAirport.style.opacity = '0';\n          \/\/ After fade out, remove loader and fade in iframe\n          setTimeout(() => {\n            loaderAirport.remove();\n            iframe.classList.add('loaded');\n          }, 11);\n\n          \/\/ Safely update iframe hash\n          try {\n            iframe.contentWindow.location.hash = 'Airport';\n          } catch(e) {\n            console.warn('Could not set iframe hash:', e);\n          }\n        });\n\n        observer.unobserve(containerAirport);\n      }\n    });\n  }, {\n    root: null,\n    threshold: 0.6\n  });\n\n  observerAirport.observe(containerAirport);\n<\/script>\n\n\n\n<!-- airnav live radar -->\n<div style=\"text-align: center; margin-top: 1px; transform: translateX(-8px);\">\n  <div id=\"loading\" style=\"margin-bottom: 10px;\">\n    <div class=\"radar-container\">\n      <div class=\"radar\">\n        <!-- Radar Sweep -->\n        <div class=\"sweep\"><\/div>\n\n        <!-- Crosshair lines -->\n        <div class=\"crosshair horizontal\"><\/div>\n        <div class=\"crosshair vertical\"><\/div>\n\n        <!-- Plane Dots -->\n        <div class=\"plane-dot\" style=\"top: 20%; left: 30%; animation-delay: 0s;\"><\/div>\n        <div class=\"plane-dot\" style=\"top: 50%; left: 70%; animation-delay: 1.2s;\"><\/div>\n        <div class=\"plane-dot\" style=\"top: 75%; left: 40%; animation-delay: 0.6s;\"><\/div>\n        <div class=\"plane-dot\" style=\"top: 35%; left: 80%; animation-delay: 1.8s;\"><\/div>\n        <div class=\"plane-dot\" style=\"top: 60%; left: 20%; animation-delay: 1.0s;\"><\/div>\n      <\/div>\n      <div style=\"margin-top: 10px; color: black; font-weight: bold;\">Loading radar&#8230;<\/div>\n    <\/div>\n  <\/div>\n\n  <iframe loading=\"lazy\"\n  style=\"display: none; margin: 0 auto;\"\n    id=\"radarIframe\"\n    align=center\n    frameborder=\"0\"\n    scrolling=\"no\"\n    marginheight=\"0\"\n    marginwidth=\"0\"\n    width=\"330\"\n    height=\"555\"\n    src=\"\"\n    style=\"display: none;\"\n  ><\/iframe>\n\n  <button id=\"reloadBtn\" onclick=\"reloadIframe()\" style=\"margin-top: 6px; padding: 5px 15px; cursor: pointer;\">\n  Reload Radar\n<\/button>\n\n  <div id=\"fallback\" style=\"display: none; margin-top: 10px; color: grey;\">\n    <p>Radar may have failed to load. <a href=\"https:\/\/www.airnavradar.com\/?z=9&#038;airport=LPMA\" target=\"_blank\">Open it in a new tab<\/a>.<\/p>\n  <\/div>\n<\/div>\n\n<style>\n  .radar-container {\n    text-align: center;\n    margin-top: 40px;\n  }\n\n  .radar {\n    position: relative;\n    width: 120px;\n    height: 120px;\n    border-radius: 50%;\n    background: repeating-radial-gradient(\n      circle,\n      transparent 0px,\n      transparent 14px,\n      rgba(0, 0, 0, 0.15) 15px,\n      transparent 16px\n    );\n    border: 2px solid #0f0; \/* Outer circle green *\/\n    overflow: hidden;\n    box-shadow: 0 0 10px #0f0;\n    margin: 0 auto;\n  }\n\n  .sweep {\n    position: absolute;\n    width: 100%;\n    height: 100%;\n    background: conic-gradient(\n      rgba(0, 255, 0, 0.4) 0deg,\n      transparent 40deg 360deg\n    );\n    animation: sweep 2s linear infinite;\n    border-radius: 50%;\n  }\n\n  .plane-dot {\n    position: absolute;\n    width: 6px;\n    height: 6px;\n    background: #333;\n    border-radius: 50%;\n    opacity: 0.4;\n    animation: blink 3s ease-in-out infinite;\n  }\n\n  .crosshair {\n    position: absolute;\n    background-color: rgba(0, 0, 0, 0.1);\n    z-index: 1;\n  }\n\n  .crosshair.horizontal {\n    top: 50%;\n    left: 0;\n    width: 100%;\n    height: 1px;\n  }\n\n  .crosshair.vertical {\n    left: 50%;\n    top: 0;\n    width: 1px;\n    height: 100%;\n  }\n\n  #radarIframe {\n    opacity: 0;\n    transition: opacity 0.8s ease;\n  }\n\n  #radarIframe.show {\n    opacity: 1;\n  }\n\n  @keyframes sweep {\n    from {\n      transform: rotate(0deg);\n    }\n    to {\n      transform: rotate(360deg);\n    }\n  }\n\n  @keyframes blink {\n    0%, 100% {\n      opacity: 0.2;\n    }\n    50% {\n      opacity: 1;\n    }\n  }\n<\/style>\n\n<script>\n  function reloadIframe(attempt = 1) {\n    const iframe = document.getElementById('radarIframe');\n    const loading = document.getElementById('loading');\n    const fallback = document.getElementById('fallback');\n    const reloadBtn = document.getElementById('reloadBtn');\n    const maxAttempts = 3;\n\n    const baseSrc = 'https:\/\/www.airnavradar.com\/';\n    const params = 'widget=1&z=9&showLabels=true&showAircraftModel=true&showFn=true&showAirlineLogo=true&airport=LPMA';\n    const newSrc = `${baseSrc}?timestamp=${Date.now()}&${params}`;\n\n    \/\/ Start loading state\n    loading.style.display = 'block';\n    fallback.style.display = 'none';\n    iframe.style.display = 'none';\n    iframe.classList.remove('show');\n    iframe.src = '';\n    reloadBtn.disabled = true;\n\n    \/\/ Set the onload FIRST\n    let loadComplete = false;\n    iframe.onload = () => {\n      loadComplete = true;\n\n      \/\/ Ensure spinner is shown at least 4.4 seconds\n      setTimeout(() => {\n        loading.style.display = 'none';\n        iframe.style.display = 'block';\n\n        \/\/ Trigger fade-in\n        setTimeout(() => {\n          iframe.classList.add('show');\n        }, 50);\n\n        reloadBtn.disabled = false;\n        console.log('Radar iframe loaded successfully.');\n      }, 4400);\n    };\n\n    \/\/ Delay setting src a bit to avoid too-fast reload\n    setTimeout(() => {\n      iframe.src = newSrc;\n\n      \/\/ Retry logic if iframe doesn't load in time\n      setTimeout(() => {\n        if (!loadComplete && attempt < maxAttempts) {\n          console.warn(`Radar failed to load. Retrying attempt ${attempt + 1}...`);\n          reloadIframe(attempt + 1);\n        } else if (!loadComplete) {\n          console.error('Radar failed after max attempts.');\n          fallback.style.display = 'block';\n          loading.style.display = 'none';\n          reloadBtn.disabled = false;\n        }\n      }, 4000);\n    }, 300); \/\/ Delay before setting src\n  }\n\n  \/\/ Auto-load on first visit\n  window.onload = () => reloadIframe();\n<\/script>\n\n\n\n<div class=\"wp-block-group has-global-padding is-layout-constrained wp-block-group-is-layout-constrained\">\n<div style=\"height:20px\" aria-hidden=\"true\" class=\"wp-block-spacer\"><\/div>\n\n\n\n<p class=\"has-text-align-center\">Airport \u2013 <a style=\"text-decoration:none\" href=\"https:\/\/paragliding-in-madeira.com\/weather\/golden-stream\/\" title=\"\">Ros\u00e1rio weather station<\/a><\/p>\n\n\n\n<!-- Real Time Widget -->\n<style>\n  \/* --- WIDGET CONTAINER --- *\/\n  .sc-weather-box {\n    font-family: 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;\n    background: rgba(0,0,0,0.03);\n    border: 1px solid #eaeaea;\n    border-radius: 8px;\n    padding: 15px 20px;\n    max-width: 400px;\n    margin: 20px auto;\n    color: #333;\n    box-shadow: 0 2px 10px rgba(0,0,0,0.03);\n  }\n\n  \/* --- HEADER \/ TIMESTAMP --- *\/\n  .sc-header {\n    display: grid; \n    grid-template-columns: 2fr auto 1fr; \n    align-items: baseline;\n    padding: 0 10px 8px 10px;\n    margin-bottom: 12px;\n    border-bottom: 1px solid #f0f0f0;\n  }\n  .sc-timestamp { font-size: 13px; color: #333; font-weight: 600; grid-column: 2; text-align: center; white-space: nowrap; }\n  .sc-station-name { font-size: 14px; font-weight: 600; grid-column: 1; text-align: center; color: #555; }\n\n  \/* --- MAIN CONTENT LAYOUT --- *\/\n  .sc-content { display: flex; align-items: center; gap: 15px; }\n\n  \/* --- DIRECTION --- *\/\n  .sc-direction-group { display: flex; flex-direction: column; align-items: center; margin: -3px; min-width: 65px; }\n  .sc-arrow-svg { width: 32px; height: 32px; fill: #185e96; transition: transform 1s ease-out; }\n  .sc-deg-text { font-size: 13px; font-weight: bold; margin-top: 4px; color: #185e96; }\n  .sc-cardinal { font-size: 12px; color: #333; letter-spacing: 1px; }\n\n  \/* --- SPEED --- *\/\n  .sc-speed-group { display: flex; flex-direction: column; margin-left: -10px; min-width: 110px; }\n  .sc-speed-value { font-size: 32px; font-weight: 700; line-height: 1; margin-top: 2px; margin-bottom: -3px; color: #333; }\n  .sc-gust-label { font-size: 12px; color: #333; margin-top: 5px; }\n  .sc-gust-value { color: orange; font-weight: 600; font-size: 20px; }\n  .sc-unit { font-size: 12px; font-weight: normal; color: #333; margin-left: 4px; }\n\n  \/* --- TEMP & HUMIDITY --- *\/\n  .sc-env-group { display: flex; flex-direction: column; gap: 2px; border-left: 2px solid #ddd; padding-left: 8px; margin-left: 5px; }\n  .sc-env-item { display: flex; align-items: baseline; gap: 5px; }\n  .sc-env-label { font-size: 11px; color: #888; text-transform: uppercase; font-weight: 600; width: 35px; }\n  .sc-env-value { font-size: 18px; font-weight: 600; color: #333; }\n  .sc-env-unit { font-size: 11px; color: #666; }\n\n  \/* --- BOTTOM ROW (The 3-Column Breathe Layout) --- *\/\n  .sc-bottom-row {\n    display: grid;\n    grid-template-columns: repeat(3, 1fr);\n    margin-top: 15px;\n    padding-top: 10px;\n    border-top: 1px solid #f0f0f0;\n    text-align: center;\n  }\n  .sc-bottom-item { display: flex; flex-direction: column; align-items: center; transition: opacity 0.2s; }\n  .sc-bottom-item:active { opacity: 0.6; } \/* Visual feedback for touch *\/\n  .sc-bottom-label { font-size: 8px; color: #999; text-transform: uppercase; font-weight: 800; margin-bottom: 2px; }\n  .sc-bottom-value { font-size: 14px; font-weight: 700; color: #444; }\n  .sc-bottom-unit { font-size: 9px; color: #888; margin-left: 1px; }\n\n  \/* --- DESKTOP --- *\/\n  @media (min-width: 768px) {\n    .sc-content { justify-content: center; gap: 20px; }\n    .sc-weather-box { max-width: 500px; padding: 20px 30px; }\n    .sc-env-group { padding-left: 70px; margin-left: 15px; }\n    .sc-bottom-row { gap: 30px; }\n  }\n<\/style>\n\n<div class=\"sc-weather-box\">\n  <div class=\"sc-header\">\n    <span class=\"sc-station-name\">Real Time<\/span>\n    <span class=\"sc-timestamp\" id=\"sc-time\">&#8212;<\/span>\n  <\/div>\n  \n  <div class=\"sc-content\">\n    <div class=\"sc-direction-group\">\n      <svg class=\"sc-arrow-svg\" id=\"sc-arrow\" viewBox=\"0 0 24 24\"><path d=\"M12 2L4.5 20.29l.71.71L12 18l6.79 3 .71-.71z\"\/><\/svg>\n      <div class=\"sc-deg-text\" id=\"sc-deg\">&#8211;\u00b0<\/div>\n      <div class=\"sc-cardinal\" id=\"sc-card\">&#8212;<\/div>\n    <\/div>\n\n    <div class=\"sc-speed-group\">\n      <div class=\"sc-speed-value\"><span id=\"sc-speed\">&#8212;<\/span><span class=\"sc-unit\">km\/h<\/span><\/div>\n      <div class=\"sc-gust-label\">MAX &nbsp;<span class=\"sc-gust-value\" id=\"sc-gust\">&#8212;<\/span> <span class=\"sc-unit\">km\/h<\/span><\/div>\n    <\/div>\n\n    <div class=\"sc-env-group\">\n      <div class=\"sc-env-item\">\n        <span class=\"sc-env-label\">Temp<\/span>\n        <span class=\"sc-env-value\" id=\"sc-temp\">&#8212;<\/span><span class=\"sc-env-unit\">\u00b0C<\/span>\n      <\/div>\n      <div class=\"sc-env-item\">\n        <span class=\"sc-env-label\">Hum<\/span>\n        <span class=\"sc-env-value\" id=\"sc-hum\">&#8212;<\/span><span class=\"sc-env-unit\">%<\/span>\n      <\/div>\n    <\/div>\n  <\/div>\n\n  <div class=\"sc-bottom-row\">\n    <div class=\"sc-bottom-item\" id=\"sc-rain-toggle\" style=\"cursor:pointer;\" title=\"Click to toggle Intensity\/Daily\">\n      <span class=\"sc-bottom-label\" id=\"sc-rain-label\">Rain Live<\/span>\n      <div><span class=\"sc-bottom-value\" id=\"sc-rain-val\">&#8212;<\/span><span class=\"sc-bottom-unit\" id=\"sc-rain-unit\">mm\/h<\/span><\/div>\n    <\/div>\n\n    <div class=\"sc-bottom-item\" id=\"sc-solar-toggle\" style=\"cursor:pointer;\" title=\"Click to toggle UV\/Solar\">\n      <span class=\"sc-bottom-label\" id=\"sc-solar-label\">Solar<\/span>\n      <div><span class=\"sc-bottom-value\" id=\"sc-solar-val\">&#8212;<\/span><span class=\"sc-bottom-unit\" id=\"sc-solar-unit\">W\/m\u00b2<\/span><\/div>\n    <\/div>\n\n    <div class=\"sc-bottom-item\">\n      <span class=\"sc-bottom-label\">Pressure<\/span>\n      <div><span class=\"sc-bottom-value\" id=\"sc-pres\">&#8212;<\/span><span class=\"sc-bottom-unit\">hPa<\/span><\/div>\n    <\/div>\n  <\/div>\n<\/div>\n\n<script>\n(function() {\n  const baseDir = 'https:\/\/paragliding-in-madeira.com\/weather\/wp-content\/uploads\/weather-stations\/santacruz\/';\n\n\/\/ NEW TIME FORMAT (Madeira based)\nconst madeiraDate = new Date().toLocaleString(\"en-CA\", {timeZone: \"Atlantic\/Madeira\", hour12: false}).split(',')[0];\nconst todayPrefix = madeiraDate;\n  const rawUrl = baseDir + todayPrefix + '.json';\n  const summaryUrl = baseDir + todayPrefix + '.summary.json';\n  \n  let showUV = false;\n  let showDailyRain = false;\n  let globalLatestData = null;\n  let globalSummaryStats = null;\n\n  function getCardinal(angle) {\n    const directions = ['N', 'NNE', 'NE', 'ENE', 'E', 'ESE', 'SE', 'SSE', 'S', 'SSW', 'SW', 'WSW', 'W', 'WNW', 'NW', 'NNW'];\n    return directions[Math.round(angle \/ 22.5) % 16];\n  }\n\n  function renderInteractive() {\n    if (!globalLatestData) return;\n    \n    \/\/ Solar\/UV Render\n    const sLabel = document.getElementById('sc-solar-label');\n    const sVal = document.getElementById('sc-solar-val');\n    const sUnit = document.getElementById('sc-solar-unit');\n    if (showUV) {\n      sLabel.innerText = \"UV Index\";\n      sVal.innerText = globalLatestData.uv !== undefined ? globalLatestData.uv.toFixed(1) : \"0.0\";\n      sUnit.innerText = \"\";\n    } else {\n      sLabel.innerText = \"Solar\";\n       sVal.innerText = globalLatestData.solarRadiation !== undefined ? Math.round(globalLatestData.solarRadiation) : \"--\";\n      sUnit.innerText = \"W\/m\u00b2\";\n    }\n\n  \/\/ Rain Render\n    const rLabel = document.getElementById('sc-rain-label');\n    const rVal = document.getElementById('sc-rain-val');\n    const rUnit = document.getElementById('sc-rain-unit');\n\n    if (showDailyRain) {\n      rLabel.innerText = \"Rain Daily\";\n\n      \/\/ THE TRUTH CHAIN: \n      \/\/ 1. Try our refined summary total first.\n      \/\/ 2. If that's missing (new day), fall back to station midnight total.\n      \/\/ 3. If both are missing, use 0.\n      const rainTotal = globalSummaryStats?.rain_total \n                        ?? globalLatestData?.precipSinceMidnight?.mm \n                        ?? 0;\n\n      rVal.innerText = rainTotal.toFixed(1);\n      rUnit.innerText = \"mm\";\n    } else {\n      rLabel.innerText = \"Rain Live\";\n      \n      \/\/ Safety for Live Rain Rate\n      const rainRate = globalLatestData?.precipRate?.mm ?? 0;\n      rVal.innerText = rainRate.toFixed(1);\n      rUnit.innerText = \"mm\/h\";\n    }\n}\n\n  \/\/ Event Listeners\n  document.getElementById('sc-solar-toggle').addEventListener('click', () => { showUV = !showUV; renderInteractive(); });\n  document.getElementById('sc-rain-toggle').addEventListener('click', () => { showDailyRain = !showDailyRain; renderInteractive(); });\n\n  async function updateWidget() {\n    try {\n      const cacheBust = '?t=' + Date.now();\n      \n      \/\/ Fetch Raw and Summary in parallel for efficiency\n      const [rawRes, summaryRes] = await Promise.all([\n        fetch(rawUrl + cacheBust),\n        fetch(summaryUrl + cacheBust).catch(() => null) \/\/ Fallback if summary doesn't exist yet\n      ]);\n\n      const data = await rawRes.json();\n      if (!data || data.length === 0) return;\n\n      if (summaryRes && summaryRes.ok) {\n        const summaryData = await summaryRes.json();\n        globalSummaryStats = summaryData.stats;\n      }\n      \n      const latest = data[data.length - 1];\n      globalLatestData = latest;\n      \n      \/\/ 1. Timestamp\n     if (latest.time) {\n          const dateObj = new Date(latest.time);\n          document.getElementById('sc-time').innerText = `${String(dateObj.getDate()).padStart(2, '0')}-${String(dateObj.getMonth() + 1).padStart(2, '0')}-${dateObj.getFullYear()} ${String(dateObj.getHours()).padStart(2, '0')}:${String(dateObj.getMinutes()).padStart(2, '0')}`;\n      }\n\n      \/\/ 2. Temperature & Humidity (Safety fallback)\n      const tempVal = latest.temp?.c;\n      document.getElementById('sc-temp').innerText = (tempVal !== undefined && tempVal !== null) ? tempVal.toFixed(1) : \"--\";\n      \n      const humVal = latest.humidity;\n      document.getElementById('sc-hum').innerText = (humVal !== undefined && humVal !== null) ? Math.round(humVal) : \"--\";\n      \n      \/\/ 3. Wind Data\n      document.getElementById('sc-speed').innerText = latest.windSpeed?.kph?.toFixed(1) ?? \"--\";\n      document.getElementById('sc-gust').innerText = latest.windGust?.kph?.toFixed(1) ?? \"--\";\n\n      \/\/ 4. Direction & Arrow\n      if (latest.winddir !== undefined) {\n        document.getElementById('sc-deg').innerText = latest.winddir + '\u00b0';\n        document.getElementById('sc-card').innerText = getCardinal(latest.winddir);\n        const arrow = document.getElementById('sc-arrow');\n        arrow.style.transform = `rotate(${(latest.winddir + 180) % 360}deg)`;\n      }\n\n      \/\/ 5. Pressure\n      document.getElementById('sc-pres').innerText = latest.pressure?.hPa ? Math.round(latest.pressure.hPa) : \"--\";\n\n      \/\/ 6. Rain\/Solar Toggles\n      renderInteractive();\n\n    } catch (e) { console.error(\"Widget error:\", e); }\n  }\n\n  updateWidget();\n  setInterval(updateWidget, 60000);\n})();\n<\/script>\n<\/div>\n\n\n\n<!-- 2h wind widget -->\n<style>\n  .sc-pro-cockpit {\n    font-family: 'Segoe UI', Roboto, Arial, sans-serif;\n    max-width: 500px;\n    margin: 20px auto;\n    background: rgba(0,0,0,0.02);\n    border: 1px solid #eee;\n    border-radius: 12px;\n    padding: 15px;\n    color: #333;\n  }\n  .sc-graph-label {\n    font-size: 11px;\n    font-weight: 700;\n    color: #888;\n\/* text-transform: uppercase; *\/\n    margin-bottom: 8px;\n    letter-spacing: 1px;\n    display: flex;\n    justify-content: space-between;\n  }\n  .sc-chart-wrapper { margin-bottom: 25px; position: relative; height: 180px; }\n<\/style>\n\n<div class=\"sc-pro-cockpit\">\n  <div class=\"sc-graph-label\"><span>2h Wind Speed &#038; Gust evolution (km\/h)<\/span><\/div>\n  <div class=\"sc-chart-wrapper\">\n    <canvas id=\"sc-speed-chart\"><\/canvas>\n  <\/div>\n  <div class=\"sc-graph-label\"><span>2h Wind Direction<\/span><\/div>\n  <div class=\"sc-chart-wrapper\" style=\"height: 160px;\">\n    <canvas id=\"sc-dir-chart\"><\/canvas>\n  <\/div>\n<\/div>\n\n<script src=\"https:\/\/cdn.jsdelivr.net\/npm\/chart.js\"><\/script>\n\n<script>\n(function() {\n  let speedChart, dirChart;\n\n  const baseDir = 'https:\/\/paragliding-in-madeira.com\/weather\/wp-content\/uploads\/weather-stations\/santacruz\/';\n\n  const getCardinal = (angle) => {\n    const directions = ['N', 'NNE', 'NE', 'ENE', 'E', 'ESE', 'SE', 'SSE', 'S', 'SSW', 'SW', 'WSW', 'W', 'WNW', 'NW', 'NNW'];\n    return directions[Math.round(angle \/ 22.5) % 16];\n  };\n\n\/\/ Auto hide Tooltip function\n  function autoHideTooltip(chart, delay = 5000) {\n    let hideTimer;\n    const canvas = chart.canvas;\n\n    const clearActiveElements = () => {\n      if (chart) {\n\n\/\/ Removes highlight dots from the lines\/points\n        chart.setActiveElements([]);\n  \/\/ Hides the tooltip box\n        chart.tooltip.setActiveElements([], { x: 0, y: 0 });\n        chart.update();\n      }\n    };\n\n    \/\/ Listen for touch end or mouse out to start the timer\n    const startTimer = () => {\n      clearTimeout(hideTimer);\n      hideTimer = setTimeout(clearActiveElements, delay);\n    };\n\n    \/\/ Reset timer if the user interacts again before it hides\n    const resetTimer = () => {\n      clearTimeout(hideTimer);\n    };\n\n    canvas.addEventListener('touchend', startTimer);\n    canvas.addEventListener('touchstart', resetTimer); \n  }\n\n  async function updateLighthouse() {\n    try {\n      const now = Date.now();\n      const cacheBust = '?t=' + now; \/\/ Defines the cache cleaner variable\n      const twoHoursAgo = now - (120 * 60 * 1000);\n      \n  \/\/ 1. Fetch Solar Data (UPDATED: Uses local json instead of external API)\n      const solunarRes = await fetch('https:\/\/paragliding-in-madeira.com\/weather\/wp-content\/moon-time\/solunar-data.json');\n      const solunarData = await solunarRes.json();\n\n\/\/ turn HH:mm into a real timestamp for today\n      const parseTime = (timeStr) => {\n        const [h, m] = timeStr.split(':');\n        const d = new Date();\n        d.setHours(h, m, 0, 0);\n        return d.getTime();\n      };\n\n      const sunrise = parseTime(solunarData.today.sunrise);\n      const sunset = parseTime(solunarData.today.sunset);\n\n      const timeFormatter = (val) => {\n        const d = new Date(val);\n        return `${d.getHours()}:${String(d.getMinutes()).padStart(2, '0')}`;\n      };\n\n           const getFileUrl = (date) => {\n    const madeiraDate = date.toLocaleString(\"en-CA\", {timeZone: \"Atlantic\/Madeira\", hour12: false}).split(',')[0];\n    return baseDir + `${madeiraDate}.json`;\n  };\n      \n\/\/ Get Today with Cache Busting\n      const resToday = await fetch(getFileUrl(new Date()) + cacheBust);\n      let combinedData = await resToday.json();\n\n\/\/ Handle Midnight Cross-over\n      if (combinedData.length === 0 || (combinedData.length > 0 && combinedData[0].time > twoHoursAgo)) {\n        try {\n          const yesterday = new Date();\n          yesterday.setDate(yesterday.getDate() - 1);\n          const resYesterday = await fetch(getFileUrl(yesterday) + cacheBust);\n          const yesterdayData = await resYesterday.json();\n          combinedData = [...yesterdayData, ...combinedData];\n        } catch(e) {}\n      }\n      \n      const windowData = combinedData.filter(d => d.time > (twoHoursAgo - 10000));\n\n  \/\/ 2. THE SOLAR PLUGIN (Defined once, used for both charts)\n      const solarBackgroundPlugin = {\n        id: 'solarBackground',\n        beforeDraw: (chart) => {\n          const { ctx, chartArea: { top, bottom, left, right }, scales: { x } } = chart;\n          function drawSection(startTime, endTime, color) {\n            const startPx = Math.max(left, x.getPixelForValue(startTime));\n            const endPx = Math.min(right, x.getPixelForValue(endTime));\n            if (startPx < endPx) {\n              ctx.save();\n              ctx.fillStyle = color;\n              ctx.fillRect(startPx, top, endPx - startPx, bottom - top);\n              ctx.restore();\n            }\n          }\n          ctx.save();\n          ctx.fillStyle = 'rgba(0, 0, 0, 0.06)'; \/\/ The \"Night\" tint\n          ctx.fillRect(left, top, right - left, bottom - top);\n          const yesterdaySunrise = sunrise - 86400000;\n          const yesterdaySunset = sunset - 86400000;\n          drawSection(yesterdaySunrise, yesterdaySunset, 'rgba(255, 255, 255, 0.5)'); \/\/ clears the Night tint for yesterday\ndrawSection(sunrise, sunset, 'rgba(255, 255, 255, 0.5)'); \/\/ clears the Night tint for today\n          ctx.restore();\n        }\n      };\n\n   \/\/ --- SPEED CHART ---\n      if (speedChart) speedChart.destroy();\n      speedChart = new Chart(document.getElementById('sc-speed-chart'), {\n        type: 'line',\n        data: {\n          datasets: [\n            { label: 'Gust',\ndata: windowData.reduce((acc, d, i, arr) => {\n  if (i > 0 && (d.time - arr[i-1].time) > 600000) acc.push({ x: d.time - 1, y: null });\n  acc.push({ x: d.time, y: d.windGust.kph });\n  return acc;\n}, []), \/\/ gap detect no line connection when no data exists\n borderColor: 'rgba(255, 165, 0, 0.45)', backgroundColor: 'rgba(255, 165, 0, 0.05)', fill: true, pointRadius: 0, borderWidth: 1.5, tension: 0.3,\nspanGaps: false \/\/ Don't connect across null non existent values\n },\n            { label: 'Speed', data: windowData.reduce((acc, d, i, arr) => {\n  if (i > 0 && (d.time - arr[i-1].time) > 600000) acc.push({ x: d.time - 1, y: null });\n  acc.push({ x: d.time, y: d.windSpeed.kph });\n  return acc;\n}, []), \/\/ gap detect no line connection when no data exists\nborderColor: 'rgba(24, 94, 150, 0.8)', backgroundColor: 'rgba(24, 94, 150, 0.1)', fill: true, pointRadius: 0, borderWidth: 2, tension: 0.3,\nspanGaps: false \/\/ Don't connect across null non existent values\n }\n          ]\n        },\n        options: {\n          responsive: true, maintainAspectRatio: false,\n          interaction: { mode: 'index', intersect: false },\n          plugins: { \n            legend: { display: false }, \n            tooltip: { \n              enabled: true,\n              callbacks: { \n                title: (items) => timeFormatter(items[0].parsed.x),\n                label: (ctx) => `${ctx.dataset.label}: ${ctx.parsed.y.toFixed(1)} km\/h` \n              } \n            } \n          },\n          scales: {\n            x: { \n              type: 'linear', min: twoHoursAgo, max: now,\n              ticks: { stepSize: 15 * 60 * 1000, callback: (val) => timeFormatter(val), font: { size: 10 }, maxRotation: 0, minRotation: 0, autoSkip: false },\n              grid: { display: true, color: 'rgba(0,0,0,0.08)' }\n            },\n            y: { \n              beginAtZero: true, \n              ticks: { font: { size: 10 } } \n            },\n  \n\/\/ --- MIRRORED RIGHT AXIS ---\n yRight: { \n  position: 'right', \n  beginAtZero: true,\ndisplay: true, \/\/ false to hide\ngrid: { display: false }, \nticks: { font: { size: 10 } },\n\n\/\/ function tells the right axis to copy left axis exactly\n afterDataLimits: (axis) => {\n  axis.max = axis.chart.scales.y.max;\n  axis.min = axis.chart.scales.y.min;\n              }\n            }\n          }\n        },\n\n plugins: [solarBackgroundPlugin] \/\/ Change speed bg color based on day\/night\n      });\n\n\/\/ --- RAW DIRECTION CHART ---\n\/\/ map d.winddir directly without any averaging\n      const rawDirData = windowData.map(d => ({ x: d.time, y: d.winddir }));\n\n      if (dirChart) dirChart.destroy();\n      dirChart = new Chart(document.getElementById('sc-dir-chart'), {\n type: 'scatter', \/\/ points for better visual interpretation\n        data: { \n          datasets: [{ \n            data: rawDirData, \n            \n            backgroundColor: 'rgba(24, 94, 150, 0.6)', \n            pointRadius: 2.5,\n            pointHoverRadius: 5\n          }] \n        },\n        options: {\n          responsive: true, maintainAspectRatio: false,\n          interaction: { mode: 'nearest', axis: 'x', intersect: false },\n          plugins: { \n            legend: { display: false },\n            tooltip: { \n              callbacks: { \n                title: (items) => timeFormatter(items[0].raw.x),\n                label: (ctx) => `Dir: ${Math.round(ctx.raw.y)}\u00b0 (${getCardinal(ctx.raw.y)})` \n              } \n            }\n          },\n          scales: {\n            x: { \n              type: 'linear', min: twoHoursAgo, max: now,\n              ticks: { stepSize: 15 * 60 * 1000, callback: (val) => timeFormatter(val), font: { size: 10 }, maxRotation: 0, minRotation: 0, autoSkip: false },\n              grid: { display: true, color: 'rgba(0,0,0,0.08)' } \n            },\n            y: { min: 0, max: 360, ticks: { stepSize: 90, font: { size: 10 }, callback: v => v + '\u00b0' }, grid: { color: '#bbb', lineWidth: 1 } },\n            yRight: { position: 'right', min: 0, max: 360, grid: { display: false }, ticks: { stepSize: 90, font: { weight: 'bold', size: 11 }, callback: v => ({ 0: 'N', 90: 'E', 180: 'S', 270: 'W', 360: 'N' }[v] || '') } }\n          }\n        },\n    plugins: [solarBackgroundPlugin] \/\/ Change dir bg color based on day\/night\n\n      });    autoHideTooltip(speedChart);\nautoHideTooltip(dirChart);\n\n    } catch (e) { console.error(\"Update Error:\", e); }\n  }\n\n  updateLighthouse();\n  setInterval(updateLighthouse, 60000);\n})();\n<\/script>\n\n\n\n<!-- 12h temp and hum graph -->\n<!-- added independent temp hum below\n<div class=\"sc-pro-cockpit\" style=\"margin-top: 10px;\">\n <div class=\"sc-legend\" style=\"display: flex; justify-content: left; gap: 11px; margin-bottom: 8px; font-size: 11px; font-weight: 600; color: #888;\">\n  <div style=\"display: flex; align-items: center; gap: 4px;\">\n   12h &nbsp;<span style=\"width: 12px; height: 3px; background: #3498db; border-radius: 2px;\"><\/span> Hum %\n  <\/div>\n  <div style=\"display: flex; align-items: center; gap: 4px;\">\n    <span style=\"width: 12px; height: 3px; background: #e67e22; border-radius: 2px;\"><\/span> Temp \u00baC\n  <\/div>\n  <div style=\"display: flex; align-items: center; gap: 4px;\">\n    <span style=\"width: 12px; height: 9px; background: rgba(200, 200, 200, 0.6); border-radius: 2px;\"><\/span> Daytime Cloudbase\n  <\/div>\n<\/div>\n\n  <div class=\"sc-chart-wrapper\" style=\"height: 200px; position: relative;\">\n    <canvas id=\"sc-8h-story-chart\"><\/canvas>\n  <\/div>\n<\/div>\n\n<script>\n(function() {\n  let storyChart;\n  \/\/ The base directory\n  const baseDir = 'https:\/\/paragliding-in-madeira.com\/weather\/wp-content\/uploads\/weather-stations\/santacruz\/';\n\n\/\/ Auto hide Tooltip function\n function autoHideTooltip(chart, delay = 5000) {\n    let hideTimer;\n    const clearTooltip = () => {\n      if (chart) {\n \/\/ This clears the highlight points on the lines\n        chart.setActiveElements([]); \n  \/\/ This hides the tooltip box\n        if (chart.tooltip) {\n          chart.tooltip.setActiveElements([], { x: 0, y: 0 });\n        }\n        chart.update();\n      }\n    };\n   chart.canvas.addEventListener('touchstart', () => {\n      clearTimeout(hideTimer);\n    });\n  chart.canvas.addEventListener('touchend', () => {\n      clearTimeout(hideTimer);\n      hideTimer = setTimeout(clearTooltip, delay);\n    });\n  }\n\n  async function updateStory() {\n    try {\n      const now = Date.now();\n      const cacheBust = '?t=' + now; \/\/ Ensuring cache busting is defined\n      const eightHoursAgo = now - (12 * 60 * 60 * 1000);\n\/\/ 12h graph window\n      \n\/\/ CONFIG TOGGLE - CHANGE THIS TO False to ALWAYS SHOW CLOUDS\n      const showCloudsOnlyDuringDay = true; \n\n\/\/ --- Get Sunrise and set from our json file ---\n      const solunarRes = await fetch('https:\/\/paragliding-in-madeira.com\/weather\/wp-content\/moon-time\/solunar-data.json');\n      const solunarData = await solunarRes.json();\n\n \/\/ Turn HH:mm into timestamp for today\n      const parseTime = (timeStr) => {\n        const [h, m] = timeStr.split(':');\n        const d = new Date();\n        d.setHours(h, m, 0, 0);\n        return d.getTime();\n      };\n\n      const sunriseToday = parseTime(solunarData.today.sunrise);\n      const sunsetToday = parseTime(solunarData.today.sunset);\n      \n\/\/ Determine if a timestamp is \"Night\"\n      const getIsNight = (timestamp) => {\n        const d = new Date(timestamp);\n\/\/ Simplified check: is it between sunset and sunrise? Adjusts window cross midnight\n        if (timestamp > sunriseToday && timestamp < sunsetToday) return false;\n        if (timestamp < (sunriseToday - 86400000) || timestamp > (sunsetToday + 86400000)) return true; \n\/\/ Check yesterday's sun if timestamp is very old\n        const yesterdaySunset = sunsetToday - 86400000;\n        const yesterdaySunrise = sunriseToday - 86400000;\n        if (timestamp > yesterdaySunrise && timestamp < yesterdaySunset) return false;\n        return true; \n      };\n\n      const timeFormatter = (val) => {\n        const d = new Date(val);\n        return `${d.getHours()}:${String(d.getMinutes()).padStart(2, '0')}`;\n      };\n\n      const getFileUrl = (date) => baseDir + `${date.toISOString().slice(0, 10)}.json`;\n      \n\/\/ Get Today with Cache Busting\n      const resToday = await fetch(getFileUrl(new Date()) + cacheBust);\n      let combinedData = await resToday.json();\n\n      if (combinedData.length === 0 || (combinedData.length > 0 && combinedData[0].time > eightHoursAgo)) {\n        try {\n          const yesterday = new Date();\n          yesterday.setDate(yesterday.getDate() - 1);\n          const resYesterday = await fetch(getFileUrl(yesterday) + cacheBust);\n          const yesterdayData = await resYesterday.json();\n          combinedData = [...yesterdayData, ...combinedData];\n        } catch(e) {}\n      }\n\n\/\/ Filter time window, keep only unique timestamps\nconst windowData = combinedData\n  .filter(d => d.time > (eightHoursAgo - 10000))\n  .filter((value, index, self) => \n    index === self.findIndex((t) => t.time === value.time)\n  );\n\n\/\/ --- GAP DETECTOR START ---\nconst processedStoryData = [];\nconst GAP_THRESHOLD = 10 * 60 * 1000; \/\/ 10 minutes\n\nwindowData.forEach((d, i) => {\n  if (i > 0 && (d.time - windowData[i - 1].time) > GAP_THRESHOLD) {\n\/\/ Place null for all 3 metrics to hide all lines at once\n          processedStoryData.push({ \n      time: d.time - 1, \n      humidity: null, \n      temp: null, \n      dewpoint: null \n    });\n  }\n  processedStoryData.push(d); \n});\n\/\/ --- GAP DETECTOR END ---\n\n      if (storyChart) storyChart.destroy();\n      \n      const ctx = document.getElementById('sc-8h-story-chart').getContext('2d');\n\n      const cloudLayerPlugin = {\n        id: 'cloudLayer',\n        beforeDatasetsDraw: (chart) => {\n          const { ctx, chartArea: { top, bottom, left, right }, scales: { x } } = chart;\n          ctx.save();\n          ctx.beginPath();\n          let first = true;\n          windowData.forEach(d => {\n            if (d.temp && d.dewpoint) {\n              const xPx = x.getPixelForValue(d.time);\n              const cbMeters = ((d.temp.c - d.dewpoint.c) * 125) + 125; \/\/ + station height for accuracy\n              const daytimeCloudY = bottom - (Math.min(cbMeters, 2000) \/ 2000) * (bottom - top);\n              \n  \/\/ Apply dynamic night check\n              const isNight = getIsNight(d.time);\n              const cloudY = (showCloudsOnlyDuringDay && isNight) ? bottom : daytimeCloudY;\n\n              if (first) { ctx.moveTo(xPx, cloudY); first = false; }\n              else { ctx.lineTo(xPx, cloudY); }\n            }\n          });\n          ctx.lineTo(right, top);\n          ctx.lineTo(left, top);\n          ctx.closePath();\n          const gradient = ctx.createLinearGradient(0, top, 0, bottom);\n          gradient.addColorStop(0, 'rgba(100, 100, 100, 0.16)');\n          gradient.addColorStop(1, 'rgba(100, 100, 100, 0.08)');\n          ctx.fillStyle = gradient;\n          ctx.fill();\n          ctx.restore();\n        }\n      };\n\n      const solarBackgroundPlugin = {\n        id: 'solarBackground',\n        beforeDraw: (chart) => {\n          const { ctx, chartArea: { top, bottom, left, right }, scales: { x } } = chart;\n          function drawSection(startTime, endTime, color) {\n            const startPx = Math.max(left, x.getPixelForValue(startTime));\n            const endPx = Math.min(right, x.getPixelForValue(endTime));\n            if (startPx < endPx) {\n              ctx.save(); ctx.fillStyle = color;\n              ctx.fillRect(startPx, top, endPx - startPx, bottom - top);\n              ctx.restore();\n            }\n          }\n          ctx.save();\n          ctx.fillStyle = 'rgba(0, 0, 0, 0.06)'; \n          ctx.fillRect(left, top, right - left, bottom - top);\n          drawSection(sunriseToday - 86400000, sunsetToday - 86400000, 'rgba(255, 255, 255, 0.5)');\n          drawSection(sunriseToday, sunsetToday, 'rgba(255, 255, 255, 0.5)');\n          ctx.restore();\n        }\n      };\n\n      storyChart = new Chart(ctx, {\n        type: 'line',\n        data: {\n         datasets: [\n  { \n    label: 'Humidity', \ndata: processedStoryData.map(d => ({x: d.time, y: d.humidity})),\n    borderColor: '#3498db', \n    backgroundColor: 'rgba(52, 152, 219, 0.05)', \n    yAxisID: 'yHum', \n    pointRadius: 0, \n    borderWidth: 2, \n    tension: 0.4, \n    fill: true, \n    spanGaps: false,\n    zIndex: 1 \/\/ Lowest visual layer, but top of tooltip\n  },\n  { \n    label: 'Temp', \ndata: processedStoryData.map(d => ({x: d.time, y: d.temp ? d.temp.c : null})),\n    borderColor: '#e67e22', \n    backgroundColor: 'rgba(230, 126, 34, 0.1)', \n    yAxisID: 'yTemp', \n    pointRadius: 0, \n    borderWidth: 2, \n    tension: 0.4, \n    spanGaps: false,\n    zIndex: 3 \/\/ appears on top\n  },\n  { \n    label: 'DewPoint', \ndata: processedStoryData.map(d => ({x: d.time, y: d.dewpoint ? d.dewpoint.c : null})),\n    borderColor: 'transparent', \/\/ Visually remove the line\n    pointRadius: 0, \n    borderWidth: 0,\n    yAxisID: 'yTemp', \n    tension: 0.1, \n    spanGaps: false,\n    zIndex: 2 \n  }\n]\n        },\n        options: {\n          responsive: true,\n          maintainAspectRatio: false,\n\n          interaction: { mode: 'index', intersect: false },\n          plugins: { \n            legend: { display: false },\n           tooltip: {\n  enabled: true,\n  callbacks: {\n    title: (items) => timeFormatter(items[0].parsed.x),\n    label: (ctx) => {\n      const val = ctx.parsed.y;\n      if (val === null) return '';\n      \n      \/\/ 1. Humidity is first in the dataset array, so it shows at the top\n      if (ctx.dataset.label === 'Humidity') return `Humidity: ${Math.round(val)}%`;\n      \n      \/\/ 2. Temp is in the middle\n      if (ctx.dataset.label === 'Temp') return `Temp: ${val.toFixed(1)}\u00b0C`;\n      \n      \/\/ 3. DewPoint is last, so we attach the Cloud Base here to put it at the bottom\n      if (ctx.dataset.label === 'DewPoint') {\n        const d = windowData[ctx.dataIndex];\n        const dpLine = `Dew Point: ${val.toFixed(1)}\u00b0C`;\n        const cb = (d.temp.c - d.dewpoint.c) * 125 + 125; \/\/ + station height for accuracy\n \/\/ dynamic night check for the tooltip to hide the cloudbase calc\n        const isNight = getIsNight(d.time);\n\n        if (showCloudsOnlyDuringDay && isNight) return dpLine;\n        return [dpLine, `Est. Cloud Base: ${Math.max(0, Math.round(cb))}m` ];\n      }\n      return '';\n    }\n  }\n}\n          },\n          scales: {\n            x: { type: 'linear', min: eightHoursAgo, max: now, ticks: { stepSize: 3600000, callback: (v) => timeFormatter(v), font: { size: 10 }, maxRotation: 0 }, grid: { color: 'rgba(0,0,0,0.09)' } },\n            yTemp: { \n              type: 'linear', \n              position: 'left', \n              grace: '14%', \n              ticks: { font: { size: 10, weight: '600' }, callback: (v) => v.toFixed(0) + '\u00b0', stepSize: 1 }, \n              grid: { display: true, color: 'rgba(34, 34, 34, 0.12)', drawTicks: false, borderDash: [3, 3] }, \n         \/\/  suggestedMin: 14, \n         \/\/  suggestedMax: 28,\n \/\/ FILTER:  scale to ignore invisible DewPoint for min\/max calculation\n              afterDataLimits: (axis) => {\n                const tempDataset = axis.chart.data.datasets.find(ds => ds.label === 'Temp');\n                if (tempDataset) {\n                  const values = tempDataset.data.map(d => d.y).filter(v => v !== null);\n                                    const min = Math.min(...values);\nconst max = Math.max(...values);\n\n\/\/ Adaptive Scale - afterDataLimits logic for yTemp \"secret sauce\" calculates real temperature spread and makes the axis roughly double that height ((max - min) * 1.1 + 1.1). Mathematically places Temp line on bottom half of the graph, whether 15\u00b0C or 30\u00b0C\n\naxis.min = min - 1;\naxis.max = max + ((max - min) * 1.1 + 1.1); \n                }\n              }\n            },\n            yHum: { type: 'linear', position: 'right', min: 0, max: 100, ticks: { font: { size: 10 }, callback: (v) => v + '%' }, grid: { display: false } }\n          }\n        },\n        plugins: [solarBackgroundPlugin, cloudLayerPlugin]\n      });\n      autoHideTooltip(storyChart);\n    } catch (e) { console.error(\"Story Chart Error:\", e); }\n  }\n\n  updateStory();\n  setInterval(updateStory, 300000); \/\/ 5 minute refresh\n})();\n<\/script>\n-->\n\n\n\n<!-- 12h temp and hum graph -->\n<div class=\"sc-pro-cockpit\" style=\"margin-top: 10px;\">\n <div class=\"sc-legend\" style=\"display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px; font-size: 11px; font-weight: 600; color: #888;\">\n  \n  <div style=\"display: flex; gap: 11px;\">\n    <div id=\"leg-temp\" style=\"display: flex; align-items: center; gap: 4px; cursor: pointer; transition: opacity 0.3s;\">\n     12h &nbsp;<span style=\"width: 12px; height: 3px; background:  #e67e22; border-radius: 2px;\"><\/span> Temp \u00baC\n    <\/div>\n    <div id=\"leg-hum\" style=\"display: flex; align-items: center; gap: 4px; cursor: pointer; transition: opacity 0.3s;\">\n      <span style=\"width: 12px; height: 3px; background: #3498db; border-radius: 2px;\"><\/span> Hum %\n    <\/div>\n    <div id=\"leg-clouds\" style=\"display: flex; align-items: center; gap: 4px; cursor: pointer; transition: opacity 0.3s;\">\n      <span style=\"width: 12px; height: 9px; background: rgba(200, 200, 200, 0.6); border-radius: 2px;\"><\/span> Cloudbase\n    <\/div>\n  <\/div>\n\n  <div id=\"leg-all\" style=\"cursor: pointer; color: #333; border-bottom: 2px solid #333; padding-bottom: 2px; font-size: 11px; margin-right: 5px;\">ALL<\/div>\n<\/div>\n\n  <div class=\"sc-chart-wrapper\" style=\"height: 200px; position: relative;\">\n    <canvas id=\"sc-8h-story-chart\"><\/canvas>\n  <\/div>\n<\/div>\n\n<script>\n(function() {\n  let storyChart;\n\n\/\/ The base directory\n  const baseDir = 'https:\/\/paragliding-in-madeira.com\/weather\/wp-content\/uploads\/weather-stations\/santacruz\/';\n\n\/\/ Auto hide Tooltip function\n function autoHideTooltip(chart, delay = 5000) {\n    let hideTimer;\n    const clearTooltip = () => {\n      if (chart) {\n\/\/ Clear line highlight point  \n        chart.setActiveElements([]); \n\/\/ hide the tooltip box\n        if (chart.tooltip) {\n          chart.tooltip.setActiveElements([], { x: 0, y: 0 });\n        }\n        chart.update();\n      }\n    };\n   chart.canvas.addEventListener('touchstart', () => {\n      clearTimeout(hideTimer);\n    });\n  chart.canvas.addEventListener('touchend', () => {\n      clearTimeout(hideTimer);\n      hideTimer = setTimeout(clearTooltip, delay);\n    });\n  }\n\n\/\/ For human-readable time\n  const timeFormatter = (val) => {\n    const d = new Date(val);\n    return `${d.getHours()}:${String(d.getMinutes()).padStart(2, '0')}`;\n  };\n\n\/\/ Function to make identical twin scales and \"Secret Sauce\"\n  function updateScales(chart) {\n    const humVisible = chart.isDatasetVisible(0);\n    const tempVisible = chart.isDatasetVisible(1);\n    \n    const yTemp = chart.options.scales.yTemp;\n    const yHum = chart.options.scales.yHum;\n\n    if (tempVisible && !humVisible) {\n\/\/ TEMP ONLY: identical steps for degree alignment\n      const tempDataset = chart.data.datasets[1].data.map(d => d.y).filter(v => v !== null);\n      const min = Math.floor(Math.min(...tempDataset)) - 1;\n      const max = Math.ceil(Math.max(...tempDataset)) + 1;\n\n      yTemp.min = yHum.min = min;\n      yTemp.max = yHum.max = max;\n      yTemp.ticks.stepSize = yHum.ticks.stepSize = 1; \n      yTemp.ticks.callback = yHum.ticks.callback = (v) => v.toFixed(0) + '\u00b0';\n      yTemp.grace = yHum.grace = 0;\n    } \n    else if (humVisible && !tempVisible) {\n\/\/ HUM ONLY:identical 10% steps\n      yTemp.min = yHum.min = 0;\n      yTemp.max = yHum.max = 100;\n      yTemp.ticks.stepSize = yHum.ticks.stepSize = 10; \n      yTemp.ticks.callback = yHum.ticks.callback = (v) => v.toFixed(0) + '%';\n      yTemp.grace = yHum.grace = 0;\n    } \n    else {\n\/\/ ALL VIEW: \"Secret Sauce\" logic\n      yTemp.min = undefined; yTemp.max = undefined;\n      yTemp.ticks.stepSize = undefined; \n      yHum.min = 0; yHum.max = 100;\n      yHum.ticks.stepSize = 10;\n      yTemp.ticks.callback = (v) => v.toFixed(0) + '\u00b0';\n      yHum.ticks.callback = (v) => v + '%';\n      yTemp.grace = '14%';\n    }\n  }\n\n  function syncLegendUI(chart) {\n    const humVisible = chart.isDatasetVisible(0);\n    const tempVisible = chart.isDatasetVisible(1);\n    document.getElementById('leg-hum').style.opacity = humVisible ? '1' : '0.3';\n    document.getElementById('leg-temp').style.opacity = tempVisible ? '1' : '0.3';\n    document.getElementById('leg-clouds').style.opacity = chart.isDatasetVisible(2) ? '1' : '0.3';\n    document.getElementById('leg-all').style.opacity = (humVisible && tempVisible) ? '1' : '0.5';\n    document.getElementById('leg-all').style.borderBottomColor = (humVisible && tempVisible) ? '#333' : 'transparent';\n  }\n\n  async function updateStory() {\n    try {\n\/\/ Ensure cache busting defined\n const now = Date.now();\n const cacheBust = '?t=' + now; \n \n\/\/ 12h graph window\n const twelveHoursAgo = now - (12 * 60 * 60 * 1000); \n\n\/\/ CONFIG TOGGLE - CHANGE THIS TO False to ALWAYS SHOW CLOUDS\n      const showCloudsOnlyDuringDay = true; \n\n\/\/ Sunrise & set from our json\n      const solunarRes = await fetch('https:\/\/paragliding-in-madeira.com\/weather\/wp-content\/moon-time\/solunar-data.json');\n      const solunarData = await solunarRes.json();\n\n\/\/ Turn HH:mm into timestamp for today\n      const parseTime = (timeStr) => {\n        const [h, m] = timeStr.split(':');\n        const d = new Date();\n        d.setHours(h, m, 0, 0);\n        return d.getTime();\n      };\n\n      const sunriseToday = parseTime(solunarData.today.sunrise);\n      const sunsetToday = parseTime(solunarData.today.sunset);\n      \n      \/\/ Determine if a timestamp is \"Night\"\n      const getIsNight = (timestamp) => {\n        const d = new Date(timestamp);\n\/\/ Simplified check: is it between sunset and sunrise? Adjusts window cross midnight\n        if (timestamp > sunriseToday && timestamp < sunsetToday) return false;\n        if (timestamp < (sunriseToday - 86400000) || timestamp > (sunsetToday + 86400000)) return true; \n\/\/ Check yesterday's sun if timestamp is very old\n        const yesterdaySunset = sunsetToday - 86400000;\n        const yesterdaySunrise = sunriseToday - 86400000;\n        if (timestamp > yesterdaySunrise && timestamp < yesterdaySunset) return false;\n        return true; \n      };\n\n      const timeFormatter = (val) => {\n        const d = new Date(val);\n        return `${d.getHours()}:${String(d.getMinutes()).padStart(2, '0')}`;\n      };\n\n       const getFileUrl = (date) => {\n    const madeiraDate = date.toLocaleString(\"en-CA\", {timeZone: \"Atlantic\/Madeira\", hour12: false}).split(',')[0];\n    return baseDir + `${madeiraDate}.json`;\n  };\n\n\/\/ Get Today with Cache Busting\n      const resToday = await fetch(getFileUrl(new Date()) + cacheBust);\n      let combinedData = await resToday.json();\n\n      if (combinedData.length === 0 || (combinedData.length > 0 && combinedData[0].time > twelveHoursAgo)) {\n        try {\n          const yesterday = new Date();\n          yesterday.setDate(yesterday.getDate() - 1);\n          const resYesterday = await fetch(getFileUrl(yesterday) + cacheBust);\n          const yesterdayData = await resYesterday.json();\n          combinedData = [...yesterdayData, ...combinedData];\n        } catch(e) {}\n      }\n\n\/\/ Filter time window, keep only unique timestamps\nconst windowData = combinedData\n        .filter(d => d.time > (twelveHoursAgo - 10000))\n        .filter((value, index, self) => index === self.findIndex((t) => t.time === value.time));\n\n   \/\/ GAP DETECTOR Logic\n      const processedStoryData = [];\n      windowData.forEach((d, i) => {\n        if (i > 0 && (d.time - windowData[i - 1].time) > (10 * 60 * 1000)) {\n\/\/ 10 minutes\n\n\/\/ Place null for all 3 metrics to hide all lines\nprocessedStoryData.push({ time: d.time - 1, humidity: null, temp: null, dewpoint: null });\n        }\n        processedStoryData.push(d); \n      });\n\/\/ --- GAP DETECTOR END ---\n\n      if (storyChart) storyChart.destroy();\n      const ctx = document.getElementById('sc-8h-story-chart').getContext('2d');\n\n      const cloudLayerPlugin = {\n        id: 'cloudLayer',\n        beforeDatasetsDraw: (chart) => {\n          if (!chart.isDatasetVisible(2)) return;\n          const { ctx, chartArea: { top, bottom, left, right }, scales: { x } } = chart;\n          ctx.save(); ctx.beginPath();\n          let first = true;\n          windowData.forEach(d => {\n            if (d.temp && d.dewpoint) {\n              const xPx = x.getPixelForValue(d.time);\n              const cbMeters = ((d.temp.c - d.dewpoint.c) * 125) + 125; \n\/\/ +station height for accuracy\n\n\/\/ Apply dynamic night check\n     const isNight = getIsNight(d.time);\n              const daytimeCloudY = bottom - (Math.min(cbMeters, 2000) \/ 2000) * (bottom - top);\n              const cloudY = (showCloudsOnlyDuringDay && getIsNight(d.time)) ? bottom : daytimeCloudY;\n              if (first) { ctx.moveTo(xPx, cloudY); first = false; }\n              else { ctx.lineTo(xPx, cloudY); }\n            }\n          });\n                    ctx.lineTo(right, top);\n          ctx.lineTo(left, top);\n          ctx.closePath();\n          const gradient = ctx.createLinearGradient(0, top, 0, bottom);\n          gradient.addColorStop(0, 'rgba(100, 100, 100, 0.16)');\n          gradient.addColorStop(1, 'rgba(100, 100, 100, 0.08)');\n          ctx.fillStyle = gradient;\n          ctx.fill();\n          ctx.restore();\n        }\n      };\n\n      const solarBackgroundPlugin = {\n        id: 'solarBackground',\n        beforeDraw: (chart) => {\n          const { ctx, chartArea: { top, bottom, left, right }, scales: { x } } = chart;\n                   function drawSection(startTime, endTime, color) {\n            const startPx = Math.max(left, x.getPixelForValue(startTime));\n            const endPx = Math.min(right, x.getPixelForValue(endTime));\n            if (startPx < endPx) {\n              ctx.save(); ctx.fillStyle = color;\n              ctx.fillRect(startPx, top, endPx - startPx, bottom - top);\n              ctx.restore();\n            }\n          }\n          ctx.save();\n          ctx.fillStyle = 'rgba(0, 0, 0, 0.06)'; \n          ctx.fillRect(left, top, right - left, bottom - top);\n          drawSection(sunriseToday - 86400000, sunsetToday - 86400000, 'rgba(255, 255, 255, 0.5)');\n          drawSection(sunriseToday, sunsetToday, 'rgba(255, 255, 255, 0.5)');\n          ctx.restore();\n        }\n      };\n\n      storyChart = new Chart(ctx, {\n        type: 'line',\n        data: {\n          datasets: [\n            { label: 'Humidity', data: processedStoryData.map(d => ({x: d.time, y: d.humidity})), borderColor: '#3498db', backgroundColor: 'rgba(52, 152, 219, 0.05)', yAxisID: 'yHum', pointRadius: 0, borderWidth: 2, tension: 0.4, fill: true, spanGaps: false },\n            { label: 'Temp', data: processedStoryData.map(d => ({x: d.time, y: d.temp ? d.temp.c : null})), borderColor: '#e67e22', backgroundColor: 'rgba(230, 126, 34, 0.1)', yAxisID: 'yTemp', pointRadius: 0, borderWidth: 2, tension: 0.4, spanGaps: false },\n            { label: 'DewPoint', data: processedStoryData.map(d => ({x: d.time, y: d.dewpoint ? d.dewpoint.c : null})), borderColor: 'transparent', pointRadius: 0, borderWidth: 0, yAxisID: 'yTemp', tension: 0.1, spanGaps: false }\n          ]\n        },\n        options: {\n          responsive: true,\n          maintainAspectRatio: false,\n          interaction: { mode: 'index', intersect: false },\n          plugins: { \n            legend: { display: false },\n            tooltip: {\n              enabled: true,\n              callbacks: {\n                title: (items) => timeFormatter(items[0].parsed.x),\n                label: (ctx) => {\n                  const val = ctx.parsed.y;\n                  if (val === null) return '';\n                  if (ctx.dataset.label === 'Humidity') return `Humidity: ${Math.round(val)}%`;\n                  if (ctx.dataset.label === 'Temp') return `Temp: ${val.toFixed(1)}\u00b0C`;\n                  if (ctx.dataset.label === 'DewPoint') {\n                    const d = windowData[ctx.dataIndex];\n                    if (!d || !d.temp) return `Dew Point: ${val.toFixed(1)}\u00b0C`;\n                    const cb = (d.temp.c - d.dewpoint.c) * 125 + 125; \n                    return [`Dew Point: ${val.toFixed(1)}\u00b0C`, `Cloudbase: ${Math.max(0, Math.round(cb))}m` ];\n                  }\n                  return '';\n                }\n              }\n            }\n          },\n          scales: {\n            x: { type: 'linear', min: twelveHoursAgo, max: now, ticks: { stepSize: 3600000, callback: (v) => timeFormatter(v), font: { size: 10 }, maxRotation: 0 }, grid: { color: 'rgba(0,0,0,0.09)' } },\n            yTemp: { \n              type: 'linear', position: 'left', grace: '14%', \n              ticks: { font: { size: 10, weight: '600' }, callback: (v) => v.toFixed(0) + '\u00b0' }, \n              grid: { display: true, color: 'rgba(34, 34, 34, 0.12)', borderDash: [3, 3] }, \n              afterDataLimits: (axis) => {\n                const humVis = axis.chart.isDatasetVisible(0);\n                const tempVis = axis.chart.isDatasetVisible(1);\n                if (tempVis && humVis) {\n                  const vals = axis.chart.data.datasets[1].data.map(d => d.y).filter(v => v !== null);\n                  const min = Math.min(...vals); const max = Math.max(...vals);\n                  axis.min = min - 1;\n                  axis.max = max + ((max - min) * 1.1 + 1.1); \n                }\n              }\n            },\n            yHum: { \n              type: 'linear', position: 'right', min: 0, max: 100, \n              ticks: { font: { size: 10 }, callback: (v) => v + '%' }, \n              grid: { display: false } \n            }\n          }\n        },\n        plugins: [solarBackgroundPlugin, cloudLayerPlugin]\n      });\n\n      const refreshUI = () => {\n        updateScales(storyChart);\n        syncLegendUI(storyChart);\n        storyChart.update();\n      };\n\n\/\/ - SET DEFAULT VIEW ON LOAD -\n\/\/ 0 = Humidity, 1 = Temp, 2 = DewPoint\/Clouds\n      storyChart.setDatasetVisibility(0, false); \/\/ Hide Humidity\n      storyChart.setDatasetVisibility(1, true);  \/\/ Show Temp\n      storyChart.setDatasetVisibility(2, false);  \/\/ Hide Cloudbase\n      \n\/\/ Make scales and legend sync to this view immediately\n      refreshUI();\n     document.getElementById('leg-all').onclick = () => {\n        storyChart.setDatasetVisibility(0, true);\n        storyChart.setDatasetVisibility(1, true);\n        storyChart.setDatasetVisibility(2, true);\n        refreshUI();\n      };\n\ndocument.getElementById('leg-hum').onclick = () => {\n        storyChart.setDatasetVisibility(0, true);\n        storyChart.setDatasetVisibility(1, false);\n        storyChart.setDatasetVisibility(2, false);\n        refreshUI();\n      };\n    document.getElementById('leg-temp').onclick = () => {\n        storyChart.setDatasetVisibility(0, false);\n        storyChart.setDatasetVisibility(1, true);\n        storyChart.setDatasetVisibility(2, false);\n        refreshUI();\n      };\n\ndocument.getElementById('leg-clouds').onclick = () => {\n        storyChart.setDatasetVisibility(2, !storyChart.isDatasetVisible(2));\n        syncLegendUI(storyChart);\n        storyChart.update();\n      };\n\nautoHideTooltip(storyChart);\n    } catch (e) { console.error(\"Story Chart Error:\", e); }\n  }\n\n  updateStory();\n\/\/ 5 minute refresh\n  setInterval(updateStory, 300000); \n})();\n<\/script>\n\n\n\n<div style=\"height:15px\" aria-hidden=\"true\" class=\"wp-block-spacer\"><\/div>\n\n\n\n<a href=\"https:\/\/metar-taf.com\/LPMA\" id=\"metartaf-C0L9iMMw\" style=\"font-size:18px; font-weight:500; color:#000; width:300px; height:435px; display:block\">METAR Madeira International Airport Cristiano Ronaldo<\/a>\n<script async defer crossorigin=\"anonymous\" src=\"https:\/\/metar-taf.com\/embed-js\/LPMA?speed=kph&#038;qnh=hPa&#038;rh=rh&#038;target=C0L9iMMw\"><\/script>\n\n\n\n<div style=\"height:15px\" aria-hidden=\"true\" class=\"wp-block-spacer\"><\/div>\n\n\n\n<figure data-wp-context=\"{&quot;imageId&quot;:&quot;69f520a4aefd7&quot;}\" data-wp-interactive=\"core\/image\" data-wp-key=\"69f520a4aefd7\" class=\"wp-block-image alignfull size-full airport-wind wp-lightbox-container\" id=\"wind-limits\"><img loading=\"lazy\" decoding=\"async\" width=\"925\" height=\"1111\" data-wp-class--hide=\"state.isContentHidden\" data-wp-class--show=\"state.isContentVisible\" data-wp-init=\"callbacks.setButtonStyles\" data-wp-on--click=\"actions.showLightbox\" data-wp-on--load=\"callbacks.setButtonStyles\" data-wp-on-window--resize=\"callbacks.setButtonStyles\" src=\"https:\/\/paragliding-in-madeira.com\/weather\/wp-content\/uploads\/2025\/11\/Airport-wind-speed-limits-landing.webp\" alt=\"Airport wind speed limits landing\" class=\"wp-image-6280\" title=\"\" srcset=\"https:\/\/paragliding-in-madeira.com\/weather\/wp-content\/uploads\/2025\/11\/Airport-wind-speed-limits-landing.webp 925w, https:\/\/paragliding-in-madeira.com\/weather\/wp-content\/uploads\/2025\/11\/Airport-wind-speed-limits-landing-250x300.webp 250w, https:\/\/paragliding-in-madeira.com\/weather\/wp-content\/uploads\/2025\/11\/Airport-wind-speed-limits-landing-853x1024.webp 853w, https:\/\/paragliding-in-madeira.com\/weather\/wp-content\/uploads\/2025\/11\/Airport-wind-speed-limits-landing-768x922.webp 768w\" sizes=\"auto, (max-width: 925px) 100vw, 925px\" \/><button\n\t\t\tclass=\"lightbox-trigger\"\n\t\t\ttype=\"button\"\n\t\t\taria-haspopup=\"dialog\"\n\t\t\taria-label=\"Enlarge\"\n\t\t\tdata-wp-init=\"callbacks.initTriggerButton\"\n\t\t\tdata-wp-on--click=\"actions.showLightbox\"\n\t\t\tdata-wp-style--right=\"state.imageButtonRight\"\n\t\t\tdata-wp-style--top=\"state.imageButtonTop\"\n\t\t>\n\t\t\t<svg xmlns=\"http:\/\/www.w3.org\/2000\/svg\" width=\"12\" height=\"12\" fill=\"none\" viewBox=\"0 0 12 12\">\n\t\t\t\t<path fill=\"#fff\" d=\"M2 0a2 2 0 0 0-2 2v2h1.5V2a.5.5 0 0 1 .5-.5h2V0H2Zm2 10.5H2a.5.5 0 0 1-.5-.5V8H0v2a2 2 0 0 0 2 2h2v-1.5ZM8 12v-1.5h2a.5.5 0 0 0 .5-.5V8H12v2a2 2 0 0 1-2 2H8Zm2-12a2 2 0 0 1 2 2v2h-1.5V2a.5.5 0 0 0-.5-.5H8V0h2Z\" \/>\n\t\t\t<\/svg>\n\t\t<\/button><\/figure>\n\n\n\n<p class=\"has-text-align-right\" id=\"metar\" style=\"margin-top:0px;margin-bottom:0px\"><a href=\"https:\/\/youtube.com\/@madeiraairport?si=Z4VOuVUC6mMyg_cX\" target=\"_blank\" rel=\"noreferrer noopener\"><em>spotting<\/em><\/a><\/p>\n\n\n\n<div style=\"height:23px\" aria-hidden=\"true\" class=\"wp-block-spacer\"><\/div>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n    <div style=\"max-width: 600px; margin: auto;\">\n \u00a0 \u00a0 \u00a0<canvas id=\"windGraphSpeed-lpma\" height=\"250\"><\/canvas>\n      <canvas id=\"windGraphDirection-lpma\" height=\"200\"><\/canvas>\n    <\/div>\n\n    <script>\n    document.addEventListener('DOMContentLoaded', function() {\n        \n        \/\/ Use the dynamic station code and JSON file URL\n        const STATION_CODE = 'lpma';\n        const JSON_URL = 'https:\/\/paragliding-in-madeira.com\/weather\/wp-content\/uploads\/metar-wind-lpma.json';\n        const TIME_OPTIONS = { hour: '2-digit', minute: '2-digit', hour12: false };\n        \n        let windData = [];\n\n \/\/ --- Helper Functions (No change) ---\n        function getDirectionLabels(data) {\n            \/\/ (functions remain the same)\n            return data.map(d => new Date(d.time).toLocaleTimeString([], TIME_OPTIONS));\n        }\n        \n        function formatDirection(value) {\n            if (value === 0) return 'N';\n            if (value === 90) return 'E';\n            if (value === 180) return 'S';\n            if (value === 270) return 'W';\n            if (value === 360) return 'N'; \n            return null;\n        }\n        \n        function getCardinalDirection(deg) {\n            if (deg === 1) return \"VRB\"; \n            const directions = [\"N\", \"NNE\", \"NE\", \"ENE\", \"E\", \"ESE\", \"SE\", \"SSE\", \"S\", \"SSW\", \"SW\", \"WSW\", \"W\", \"WNW\", \"NW\", \"NNW\"];\n            const index = Math.round(deg \/ 22.5) % 16; \n            return directions[index];\n        }\n\n        function prepareDirectionData(data) {\n            const windDirectionData = data.map(d => d.direction === 1 ? null : d.direction); \n            const vrbData = data.map(d => d.direction === 1 ? 0 : null); \n            return { windDirectionData, vrbData };\n        }\n\n    \/\/ --- Mobile Tooltip Auto-hide ---\n        function autoHideTooltip(chart, delay = 5555) {\n            chart.canvas.addEventListener('touchstart', function() {\n                setTimeout(() => {\n                    chart.tooltip.setActiveElements([]);\n                    chart.update();\n                }, delay);\n            });\n        }\n        \n \/\/ --- Chart Setup (Use Dynamic IDs) ---\n        const ctxSpeed = document.getElementById('windGraphSpeed-' + STATION_CODE)?.getContext('2d');\n        const ctxDirection = document.getElementById('windGraphDirection-' + STATION_CODE)?.getContext('2d');\n        \n        if (!ctxSpeed || !ctxDirection) {\n\/\/ This is expected if the shortcode is used multiple times on the same page \n\/\/ but the browser hasn't loaded the elements yet. Should be fine since \n\/\/ DOMContentLoaded is running. Add more specific error if needed.\n            return;\n        }\n        \n  \/\/ Get the full name for the title\n        let stationName = STATION_CODE.toUpperCase();\n        if (STATION_CODE === 'lpma') {\n            stationName = 'Madeira (LPMA)';\n        } else if (STATION_CODE === 'lpps') {\n            stationName = 'Porto Santo (LPPS)';\n        }\n\n        const chartSpeed = new Chart(ctxSpeed, {\n            type: 'line',\n            data: { labels: [], datasets: [\n                { label: 'Wind Speed', data: [], borderColor: 'rgba(54,162,235,1)', backgroundColor: 'rgba(54,162,235,0.1)', tension: 0.25, pointRadius: 3, fill: true },\n                { label: 'Gust', data: [], borderColor: 'rgba(255,165,80,0.8)', backgroundColor: 'rgba(255,165,80,0.1)', tension: 0.25, pointRadius: 3, fill: false }\n            ] },\n            options: {\n                responsive: true,\n                interaction: { mode: 'index', intersect: false },\n                plugins: { \n                    legend: { position: 'top', labels: { boxWidth: 10, color: '#333' } }, \n                    \/\/ Dynamic Title\n                    title: { display: true, text: 'Windspeed for ' + stationName + ' in the last 12 hours (km\/h)', font: { size: 14 } } \n                },\n                scales: {\n \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0y: { position: 'left', beginAtZero: true, grace: '10%', title: { display: false }, grid: { color: 'rgba(0,0,0,0.1)', lineWidth: 1 }, ticks: { color: '#555', autoSkip: false, } },\n                    x: { display: true, grid: { drawBorder: false, drawOnChartArea: true, color: 'rgba(0,0,0,0.05)' }, title: { display: false }, ticks: { display: true, maxRotation: 90, minRotation: 90 } }\n                },\n                layout: { padding: { top: 10, bottom: 0, left: 0, right: 0 } }\n            }\n        });\n        \n        const chartDirection = new Chart(ctxDirection, {\n            type: 'line',\n            data: { labels: [], datasets: [\n                { label: 'VRB Wind', data: [], borderColor: 'rgba(150,150,200,0)', backgroundColor: 'rgba(150,150,200,0.8)', pointRadius: 5, pointStyle: 'circle', showLine: false },\n                { label: 'Wind Direction (deg)', data: [], borderColor: 'rgba(75, 192, 192, 0.9)', backgroundColor: 'rgba(75, 192, 192, 0.1)', tension: 0.25, pointRadius: 4, showLine: false }\n            ] },\n            options: {\n                responsive: true,\n                interaction: { mode: 'index', intersect: false },\n                plugins: { \n                    legend: { display: false }, \n                    \/\/ Dynamic Title\n                    title: { display: true, text: 'Wind Direction for ' + stationName + ' in the last 12 hours', font: { size: 14 } },\n                    tooltip: { \n                        callbacks: {\n                            label: function(context) {\n                                const datasetLabel = context.dataset.label;\n                                const directionDeg = context.parsed.y;\n                                if (datasetLabel === 'VRB Wind') { return 'VRB Wind: Variable'; }\n                                \n                                let cardinal = getCardinalDirection(directionDeg);\n                                \/\/ The speed data needs to come from the chartSpeed object\n                                const speed = chartSpeed.data.datasets[0].data[context.dataIndex]; \n                                if (speed === 0) { cardinal = \"CALM\/VRB\"; }\n                                \n                                return `Wind Direction: ${cardinal} (${directionDeg}deg)`;\n                            },\n                            afterLabel: function(context) { return ''; },\n                            usePointStyle: true,\n                            pointStyle: 'dash' \n                        } \n                    }\n                },\n                scales: {\n                    y: { position: 'left', min: 0, max: 360, title: { display: false }, ticks: { stepSize: 90, callback: formatDirection, color: '#555' }, grid: { color: 'rgba(0,0,0,0.1)', lineWidth: 1 } },\n                    x: { title: { display: false }, ticks: { color: '#555', maxRotation: 90, minRotation: 90 }, grid: { drawBorder: false, drawOnChartArea: true, color: 'rgba(0,0,0,0.05)' } }\n                },\n                layout: { padding: { top: 0, bottom: 10, left: 0, right: 0 } }\n            }\n        });\n\n        \/\/ Apply auto-hide to both charts using the dynamic chart objects\n        autoHideTooltip(chartSpeed, 5555);\n        autoHideTooltip(chartDirection, 5555);\n        \n        \/\/ --- Data Update Logic (No change) ---\n        function updateCharts() {\n            if (!windData || windData.length === 0) return;\n            \n            const labels = getDirectionLabels(windData);\n            const { windDirectionData, vrbData } = prepareDirectionData(windData); \n            \n            chartSpeed.data.labels = labels;\n            chartSpeed.data.datasets[0].data = windData.map(d => d.speed);\n            chartSpeed.data.datasets[1].data = windData.map(d => d.gust);\n            chartSpeed.update();\n\n            chartDirection.data.labels = labels;\n            chartDirection.data.datasets[0].data = vrbData;        \n            chartDirection.data.datasets[1].data = windDirectionData; \n            chartDirection.update();\n        }\n\n        async function fetchWindData() {\n            try {\n                const response = await fetch(JSON_URL);\n                if (!response.ok) throw new Error('Failed to fetch wind data file. Status: ' + response.status + ' from ' + JSON_URL);\n                \n                windData = await response.json(); \n                updateCharts();\n            } catch (e) {\n                console.error(\"Error loading stored wind data for \" + STATION_CODE + \":\", e);\n            }\n        }\n\n\/\/ Initial fetch and set interval for front-end updates\n        fetchWindData();\n        setInterval(fetchWindData, 5 * 60 * 1000);\n        \n    });\n    <\/script>\n    \n\n\n\n<div style=\"height:26px\" aria-hidden=\"true\" class=\"wp-block-spacer\"><\/div>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<div style=\"height:11px\" aria-hidden=\"true\" class=\"wp-block-spacer\"><\/div>\n\n\n\n<!-- LPMA RAW METAR friendly Decoder Widget -->\n<div id=\"metarw-lpma\" style=\"font-family: Inter, system-ui, sans-serif; max-width: 555px; margin: auto;\">\n  <style>\n  \n    #metarw-lpma * { \nbox-sizing: border-box; \nline-height: 1.4; \nfont-weight: 400; \n}\n    #metarw-lpma a { \ncolor: #1e40af; \ntext-decoration: \nunderline; \n}\n    #metarw-lpma p { \nmargin: 0.75rem 0; \ntext-align: justify; \n}\n    #metarw-lpma .card { \nbackground: #ffffff; \nborder-radius: 16px; \npadding: 1.25rem; \nbox-shadow: 0 10px 15px -3px rgba(0,0,0,0.1), 0 4px 6px -2px rgba(0,0,0,0.05); border: 1px solid #e5e7eb; \n}\n    #metarw-lpma .header { \nmargin-bottom: 1rem; \n}\n    #metarw-lpma .title { \nfont-weight: 700; \ncolor: #1f2937; \nfont-size: 1.05rem; \ndisplay: flex; \nalign-items: center; \n}\n    #metarw-lpma .title span { \nmargin-left: 0.5rem; \nfont-size: 1rem; \ncolor: #4b5563; \nfont-weight: 500; \n}\n    #metarw-lpma #metarw-upd-lpma { \nfont-size: 0.85rem; \ncolor: #6b7280; \nmargin-top: 0.25rem; \n}\n    #metarw-lpma .section-label { \nfont-weight: 600; \nmargin-bottom: 0.2rem; \ndisplay: block; \ncolor: #1a1a1a; \nfont-size: 1.0rem; \nletter-spacing: 0.1em; \n}\n    #metarw-lpma #metarw-raw-lpma { \ndisplay: block; \nbackground-color: #f3f4f6; \ncolor: #1f2937; \nfont-family: monospace; \nfont-size: 0.9rem; \npadding: 0.45rem; \nborder-radius: 8px; \nwhite-space: pre-wrap; \nword-break: keep-all; \nmargin-top: 0.5rem; \n}\n    #metarw-lpma #metarw-decoded-lpma { \nfont-size: 1rem; \nfont-weight: 400; \ncolor: #1f2937; \ndisplay:block; \nmargin-top:0.5rem; \n}\n\n#metarw-lpma strong {\n  font-weight: 600;\n}\n\n    #metarw-lpma #metarw-thermal-lpma { \nfont-size: 0.84rem; \nfont-weight: 500; \ncolor: #1a4e8c; \ndisplay:block; \nmargin-top:0.2rem; \n}\n  <\/style>\n\n  <div class=\"card\">\n    <div class=\"header\">\n      <div class=\"title\">&#127780;&#65039; METAR LPMA <span>(Madeira &#8220;live&#8221;)<\/span><\/div>\n    <\/div>\n\n    <p>\n      <span class=\"section-label\">Official:<\/span>\n      <span id=\"metarw-raw-lpma\">Loading\u2026<\/span>\n    <\/p>\n\n    <p>\n      <span class=\"section-label\">Decoded:<\/span>\n      <span id=\"metarw-decoded-lpma\">Loading\u2026<\/span>\n      <span id=\"metarw-thermal-lpma\"><\/span>\n    <\/p>\n\n    <small id=\"metarw-upd-lpma\"><\/small>\n  <\/div>\n<\/div>\n\n<script>\n\/* --- INTEGRATED JAVASCRIPT: ROBUST PARSING FUNCTIONS & CLEAN OUTPUT (LPMA) --- *\/\n\n(async function() {\n  \/\/ --- CONFIG ---\n  \/\/ 1. RAW_URL changed to point to LPMA data\n  const RAW_URL = \"https:\/\/paragliding-in-madeira.com\/weather\/wp-content\/uploads\/metar-full-data-lpma.json\";\n  \/\/ 2. STATION_ELEVATION (LPMA's is ~58m\/192ft, same as LPPS, but kept explicit)\n  const STATION_ELEVATION = 58; \n\n  \/\/ --- DOM ---\n  \/\/ 3. DOM ID variables changed (lpps -> lpma)\n  const rawEl = document.getElementById('metarw-raw-lpma');\n  const decodedEl = document.getElementById('metarw-decoded-lpma');\n  const thermalEl = document.getElementById('metarw-thermal-lpma');\n  const updEl = document.getElementById('metarw-upd-lpma');\n\n  \/\/ --- Helpers (Functions remain identical as they are robust parsers) ---\n  const ktsToKmh = k => Math.round(Number(k) * 1.852);\n  const hundredsFeetToMeters = h => Math.round(Number(h) * 30.48);\n  const degToCardinal = deg => deg==='VRB'||isNaN(deg) ? 'variable' : ['N','NNE','NE','ENE','E','ESE','SE','SSE','S','SSW','SW','WSW','W','WNW','NW','NNW'][Math.round(((Number(deg)%360)\/22.5))%16];\n  const feetToMeters = f => Math.round(Number(f)*0.3048);\n\n  function computeHumidity(tempC, dewC){\n    if(tempC==null || dewC==null) return null;\n    const a=17.625, b=243.04;\n    const alphaT=(a*tempC)\/(b+tempC);\n    const alphaD=(a*dewC)\/(b+dewC);\n    return Math.round(100*Math.exp(alphaD-alphaT));\n  }\n  \n  const intensityMap = {'+': 'heavy', '-': 'light', 'VC': 'in the vicinity'};\n  const descriptorMap = {'MI':'shallow','PR':'partial','BC':'patches of','DR':'low drifting','BL':'blowing','SH':'showers','TS':'thunderstorm','FZ':'freezing'};\n  const precipMap = {'DZ':'drizzle','RA':'rain','SN':'snow','SG':'snow grains','IC':'ice crystals','PL':'ice pellets','GR':'hail','GS':'small hail','UP':'unknown precipitation'};\n  const obscurationMap = {'BR':'mist','FG':'fog','FU':'smoke','VA':'volcanic ash','DU':'dust','SA':'sand','HZ':'haze','PY':'spray'};\n  const otherMap = {'SQ':'squalls','SS':'sandstorm','DS':'duststorm','PO':'dust\/sand whirls','FC':'tornado or waterspout','NSW':'no significant weather','NOSIG':'no significant change', 'RERA':'recent rain'};\n\n  function normalize(raw){\n    return raw.replace(\/\\s+\/g,' ').trim();\n  }\n\n  function parseMetarTime(raw, referenceDate = new Date()){\n    const m = raw.match(\/\\b(\\d{2})(\\d{2})(\\d{2})Z\\b\/);\n    if(!m) return null;\n    const day = Number(m[1]), hour = Number(m[2]), min = Number(m[3]);\n    let year = referenceDate.getUTCFullYear(), month = referenceDate.getUTCMonth();\n    const candidate = new Date(Date.UTC(year, month, day, hour, min));\n    if(candidate - referenceDate > 2*24*3600*1000){\n      const prev = new Date(Date.UTC(year, month-1, day, hour, min));\n      return prev;\n    }\n    if(referenceDate - candidate > 28*24*3600*1000){\n      const next = new Date(Date.UTC(year, month+1, day, hour, min));\n      return next;\n    }\n    return candidate;\n  }\n\n  function parseWeatherGroups(raw){\n    const re = \/([+-])?(VC)?([A-Z]{2,6})\/g;\n    const matches = [];\n    let m;\n    while((m = re.exec(raw)) !== null){\n      const rawToken = m[0];\n      const intensity = m[1] || '';\n      const vicinity = Boolean(m[2]);\n      const code = m[3];\n      const keys = ['TS','SH','DZ','RA','SN','SG','PL','GR','GS','FG','BR','HZ','DU','SA','SQ','FC','IC','UP','RERA','VC','MI','BC','PR','DR','BL','PO','FZ','SS','DS','NSW','NOSIG'];\n      const found = keys.some(k => code.includes(k));\n      if(found){\n        matches.push({ raw: rawToken, intensity, vicinity, code });\n      }\n    }\n    const uniq = [];\n    const seen = new Set();\n    for(const it of matches){\n      const key = it.raw + '|' + it.code;\n      if(seen.has(key)) continue;\n      seen.add(key);\n      uniq.push(it);\n    }\n    const humanized = uniq.map(it => {\n      const code = it.code;\n      if(code === 'RERA') return { ...it, human: otherMap['RERA'] };\n\n      const partRe = \/(TS|SH|DZ|RA|SN|SG|PL|GR|GS|FG|BR|HZ|DU|SA|SQ|FC|IC|UP|MI|BC|PR|DR|BL|PO|FZ|SS|DS|NSW)\/g;\n      const parts = code.match(partRe) || [code];\n      const mapped = parts.map(p => {\n        if(descriptorMap[p]) return descriptorMap[p];\n        if(precipMap[p]) return precipMap[p];\n        if(obscurationMap[p]) return obscurationMap[p];\n        if(otherMap[p]) return otherMap[p];\n        return p.toLowerCase();\n      });\n      const human = mapped.join(' ');\n      \n      let finalHuman = human;\n      if (it.intensity) {\n        finalHuman = `${intensityMap[it.intensity]} ${finalHuman}`;\n      }\n      if (it.vicinity) {\n        finalHuman = `${finalHuman} ${intensityMap['VC']}`;\n      }\n      \n      return { ...it, human: finalHuman.trim() };\n    });\n\n    return humanized;\n  }\n  \n  function parseClouds(raw){\n    const re = \/(FEW|SCT|BKN|OVC|NSC|SKC|CLR)(\\d{3}|000)?(CB|TCU)?\/g;\n    const out = [];\n    let m;\n    while((m = re.exec(raw)) !== null){\n      const cover = m[1];\n      const hundreds = m[2] ? m[2] : null;\n      const extra = m[3] || null;\n      const meters = hundreds ? hundredsFeetToMeters(Number(hundreds)) : null;\n      out.push({ cover, hundreds, meters, extra });\n    }\n    return out;\n  }\n  \n  function parseWind(raw){\n    const m = raw.match(\/\\b((?:VRB|\\d{3}))(\\d{2})(?:G(\\d{2,3}))?KT\\b\/);\n    const variable = raw.match(\/\\b(\\d{3})V(\\d{3})\\b\/);\n    if(!m) return { dir: null };\n    const dir = m[1] === 'VRB' ? 'VRB' : m[1];\n    const speedKts = Number(m[2]);\n    const gustKts = m[3] ? Number(m[3]) : null;\n    const speedKmh = ktsToKmh(speedKts);\n    const gustKmh = gustKts ? ktsToKmh(gustKts) : null;\n    const variableRange = variable ? { from: variable[1], to: variable[2], fromCard: degToCardinal(variable[1]), toCard: degToCardinal(variable[2]) } : null;\n    return { dir, dirCardinal: degToCardinal(dir), speedKts, speedKmh, gustKts, gustKmh, variableRange };\n  }\n  \n  function parseVisibility(raw){\n    if(\/\\bCAVOK\\b\/.test(raw)) return { meters: 10000, human: '10+ km (CAVOK)' };\n    const m = raw.match(\/\\b(\\d{4})\\b\/);\n    if(!m) return { meters: null, human: 'unknown' };\n    const meters = Number(m[1]);\n    const human = meters >= 9999 ? '10+ km' : `${Math.round(meters\/1000*10)\/10} km`;\n    return { meters, human };\n  }\n\n  function parseTempDew(raw){\n    const m = raw.match(\/\\b(M?\\d{2})\\\/(M?\\d{2})\\b\/);\n    if(!m) return null;\n    const parseVal = s => (s.startsWith('M') ? -Number(s.slice(1)) : Number(s));\n    const temp = parseVal(m[1]);\n    const dew = parseVal(m[2]);\n    return { temp, dew };\n  }\n\n  function parsePressure(raw){\n    const q = raw.match(\/\\bQ(\\d{4})\\b\/);\n    if(q) return { hPa: Number(q[1]) };\n    const a = raw.match(\/\\bA(\\d{4})\\b\/);\n    if(a) return { inHg: Number(a[1]) \/ 100 };\n    return null;\n  }\n\n  \/\/ --- REVISED humanizeClouds FUNCTION ---\n  function humanizeClouds(clouds){\n    if(!clouds || clouds.length===0) return 'no significant clouds';\n    const map = { FEW:'a few', SCT:'scattered', BKN:'broken', OVC:'overcast', NSC:'no significant clouds', SKC:'sky clear', CLR:'sky clear' };\n    return clouds.map(c=>{\n      if(c.cover === 'SKC' || c.cover === 'CLR' || c.cover === 'NSC') return map[c.cover];\n      const h = c.meters !== null ? `${Math.round(c.meters)}m` : 'unknown height';\n      \n      let cloudType = ' clouds';\n      if (c.extra === 'CB') cloudType = ' cumulonimbus';\n      else if (c.extra === 'TCU') cloudType = ' towering cumulus';\n      \n      const label = map[c.cover] || c.cover.toLowerCase();\n      return `${label}${cloudType} at ${h}`;\n    }).join(', ');\n  }\n  \n  function estimateCloudBase(temp, dew, stationElevation=STATION_ELEVATION){\n    if(temp==null || dew==null) return null;\n    const spread = temp - dew;\n    const obsTime = new Date();\n    const hourUTC = obsTime.getUTCHours();\n    if(hourUTC < 8 || hourUTC >= 20) return null; \n    return Math.round(spread * 125 + stationElevation);\n  }\n\n\n  \/\/ --- MAIN LOAD AND RENDER LOOP ---\n  async function loadAndRender() {\n    try {\n      rawEl.textContent = 'Loading\u2026';\n      decodedEl.textContent = 'Loading\u2026';\n      thermalEl.textContent = '';\n      updEl.textContent = '';\n\n      const resp = await fetch(`${RAW_URL}?t=${Date.now()}`, { cache: 'no-store' });\n      if(!resp.ok) throw new Error(`Error ${resp.status}`);\n\n      const data = await resp.json();\n      const rec = (Array.isArray(data) ? data[0] : data) || {};\n      const raw = (rec.rawOb || rec.rawTAF || rec.raw || '').toString().trim();\n      if(!raw) throw new Error('Raw METAR not found in response');\n\n      const normalized = normalize(raw);\n      rawEl.textContent = normalized;\n\n      \/\/ --- PARSE ALL GROUPS FROM RAW ---\n      const parsed = {};\n      parsed.reportTime = parseMetarTime(normalized, new Date());\n      parsed.wind = parseWind(normalized);\n      parsed.vis = parseVisibility(normalized);\n      parsed.clouds = parseClouds(normalized);\n      parsed.weatherArr = parseWeatherGroups(normalized);\n      parsed.tempDew = parseTempDew(normalized);\n      parsed.pressure = parsePressure(normalized);\n      parsed.humidity = parsed.tempDew ? computeHumidity(parsed.tempDew.temp, parsed.tempDew.dew) : null;\n      parsed.cloudBase = parsed.tempDew ? estimateCloudBase(parsed.tempDew.temp, parsed.tempDew.dew, STATION_ELEVATION) : null;\n      \n      \n      \/\/ --- FORMATTING DECODED TEXT ---\n      \n      \/\/ Wind\n      let windDirText = parsed.wind.dir === 'VRB' ? 'variable direction' : `${parsed.wind.dirCardinal} (${parsed.wind.dir}\u00b0)`;\n      if (parsed.wind.variableRange) {\n        windDirText += `, varying between ${parsed.wind.variableRange.fromCard} (${parsed.wind.variableRange.from}\u00b0) and ${parsed.wind.variableRange.toCard} (${parsed.wind.variableRange.to}\u00b0)`;\n      }\n      const windSpd = parsed.wind.speedKmh ? `${parsed.wind.speedKmh} km\/h` : 'calm';\n      const gustText = parsed.wind.gustKmh ? `, gusting to ${parsed.wind.gustKmh} km\/h` : '';\n\n      \/\/ Visibility\n      let vis = parsed.vis.human;\n      \n      \/\/ Clouds\n      let cloudsText = humanizeClouds(parsed.clouds);\n\n      \/\/ Weather\n      let weatherText = '';\n      if(parsed.weatherArr.length){\n        weatherText = parsed.weatherArr.map(w => w.human).join(', ');\n      }\n\n      \/\/ Temp & Pressure\n      const temp = parsed.tempDew ? `${parsed.tempDew.temp}\u00b0C` : 'unknown';\n      const dew = parsed.tempDew ? `${parsed.tempDew.dew}\u00b0C` : 'unknown';\n      const pressText = parsed.pressure?.hPa ? `${parsed.pressure.hPa} hPa` : (parsed.pressure?.inHg ? `${parsed.pressure.inHg.toFixed(2)} inHg` : 'unknown');\n      const humidityText = parsed.humidity ? `${parsed.humidity}% humidity` : '';\n      \n      \/\/ --- Decoded text ---\n      decodedEl.innerHTML = `\n  <strong>Wind:<\/strong> ${windDirText} at ${windSpd}${gustText}<br>\n  <strong>Visibility:<\/strong> ${vis} with ${cloudsText}${weatherText ? ` and ${weatherText}` : ''}<br>\n  <strong>Temp:<\/strong> ${temp} \/ Dew point: ${dew}<br>\n  ${humidityText}<br>\n  <strong>Pressure:<\/strong> ${pressText}\n`;\n\n      \/\/ --- Last updated ---\n      const obsTime = parsed.reportTime ? parsed.reportTime : new Date();\n      updEl.textContent = `Last updated: ${obsTime.toLocaleTimeString('en-GB',{hour12:false,timeZone:'UTC',hour:'2-digit',minute:'2-digit'})} UTC`;\n\n      \/\/ --- Thermal lift (daytime only) ---\n      if(parsed.cloudBase){\n        thermalEl.textContent = `Thermal lift potential, ${parsed.cloudBase}m est. cloud base`;\n      } else {\n        thermalEl.textContent = '';\n      }\n\n    } catch(err){\n      console.error('[lpma METAR Widget] Fetch failed:',err);\n      rawEl.textContent='Error fetching METAR.';\n      decodedEl.textContent=err.message;\n      thermalEl.textContent='';\n      updEl.textContent='';\n    }\n  }\n\n  await loadAndRender();\n  setInterval(loadAndRender,30*60*1000);\n})();\n<\/script>\n\n\n\n<div style=\"height:11px\" aria-hidden=\"true\" class=\"wp-block-spacer\"><\/div>\n\n\n\n<!-- LPMA TAF Decoder Widget (Gold + Full Weather) -->\n<div id=\"tafw-lpma\" style=\"font-family: Inter, system-ui, sans-serif; max-width: 555px; margin: auto;\">\n  <style>\n\n    \/* Reset and general styling *\/\n    #tafw-lpma * { \nbox-sizing: border-box; \nline-height: 1.4; \nfont-weight: 400; \n}\n    #tafw-lpma a { \ncolor: #1e40af; \ntext-decoration: underline; \n}\n    #tafw-lpma p { \nmargin: 0.75rem 0; \ntext-align: justify; \n}\n\n    \/* Card Container *\/\n    #tafw-lpma .card { \nbackground: #ffffff; \nborder-radius: 16px; \npadding: 1.25rem; \nbox-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05); \nborder: 1px solid #e5e7eb; \n}\n\n    \/* Header and Title *\/\n    #tafw-lpma .header { \nmargin-bottom: 1.25rem; \npadding-bottom: 0.5rem; \nborder-bottom: 1px solid #f3f4f6; \n}\n    #tafw-lpma .title { \nfont-weight: 700; \ncolor: #1f2937; \nfont-size: 1.05rem; \ndisplay: flex; \nalign-items: center; \n}\n    #tafw-lpma .title span { \nmargin-left: 0.5rem; \nfont-size: 1rem; \ncolor: #4b5563; \nfont-weight: 500; }\n    #tafw-lpma #tafw-issued-lpma { \nfont-size: 0.85rem; \ncolor: #6b7280; \nmargin-top: 0.25rem; \n}\n\n    \/* Section Labels *\/\n    #tafw-lpma .section-label { \nfont-weight: 600; \nmargin-bottom: 0.2rem; \ndisplay: block; \ncolor: #1a1a1a; \nfont-size: 0.9rem; \nletter-spacing: 0.1em; \n}\n\n    \/* Raw TAF Code Block *\/\n    #tafw-lpma #tafw-raw-lpma { \ndisplay: block; \nbackground-color: #f3f4f6; \ncolor: #1f2937; \nfont-family: monospace; \nfont-size: 0.9rem; \npadding: 0.75rem; \nborder-radius: 8px; \nwhite-space: pre-wrap; \nword-break: keep-all; \nmargin-top: 0.5rem; \n}\n    \/* Decoded Summary *\/\n    #tafw-lpma #tafw-summary-lpma { \nfont-size: 1rem; \nfont-weight: 500; \ncolor: #1f2937; \n}\n\n    \/* Toggle Button *\/\n    #tafw-lpma button { \nbackground: transparent; \nfont-weight: 500; \nfont-family: system-ui, \nsans-serif; \nborder: none; \nfont-size: 1.0em; \ncursor: pointer; \nmargin-top: 0.2em; \n}\n    #tafw-lpma button:hover { \nbackground: transparent; \n}\n\n    \/* Details Section *\/\n    #tafw-lpma #tafw-details-lpma { \nmargin-top: 1rem; \nborder-top: 1px solid #e5e7eb; \npadding-top: 1rem; \nfont-size: 0.95rem; \ncolor: #374151;\n}\n    #tafw-lpma #tafw-details-lpma p { \nmargin: 0.5rem 0; \nline-height: 1.6; \n}\n    #tafw-lpma #tafw-details-lpma strong { \nfont-weight: 600; \ncolor: #1f2937; \n}\n\n  <\/style>\n\n  <div class=\"card\">\n    <div class=\"header\">\n      <div>\n        <div class=\"title\">&#9992;&#65039;  TAF LPMA <span>(Madeira Forecast)<\/span><\/div>\n        <div id=\"tafw-issued-lpma\"><\/div>\n      <\/div>\n    <\/div>\n\n    <p>\n      <span class=\"section-label\">Official:<\/span>\n      <span id=\"tafw-raw-lpma\">Loading\u2026<\/span>\n    <\/p>\n\n    <p style=\"margin-top: 1.5rem;\">\n      <span class=\"section-label\">Decoded:<\/span>\n      <span id=\"tafw-summary-lpma\">Loading\u2026<\/span>\n    <\/p>\n\n    <button id=\"tafw-toggle-lpma\" aria-expanded=\"false\" title=\"&#10133;\" style=\"display:none;\">&#10133;<\/button>\n    <div id=\"tafw-details-lpma\" style=\"display:none;\"><\/div>\n  <\/div>\n<\/div>\n\n<script>\n(async function() {\n  const RAW_URL = \"https:\/\/paragliding-in-madeira.com\/weather\/wp-content\/uploads\/taf-full-data-lpma.json\";\n\n  const rawEl = document.getElementById('tafw-raw-lpma');\n  const issuedEl = document.getElementById('tafw-issued-lpma');\n  const summaryEl = document.getElementById('tafw-summary-lpma');\n  const detailsEl = document.getElementById('tafw-details-lpma');\n  const toggleBtn = document.getElementById('tafw-toggle-lpma');\n\n  const weatherMap = {\n    'MI':'shallow','BC':'patches of','PR':'partial','VC':'vicinity','SH':'showers','TS':'thunderstorms','FZ':'freezing',\n    'DR':'low drifting','BL':'blowing','PO':'dust\/sand whirls','DZ':'drizzle','RA':'rain','SN':'snow','SG':'snow grains',\n    'IC':'ice crystals','PL':'ice pellets','GR':'hail','GS':'small hail\/snow pellets','UP':'unknown precipitation',\n    'BR':'mist','FG':'fog','FU':'smoke','VA':'volcanic ash','DU':'dust','SA':'sand','HZ':'haze','PY':'spray',\n    'SQ':'squalls','SS':'sandstorm','DS':'duststorm','FC':'funnel clouds (tornado\/waterspout)','NSW':'no significant weather'\n  };\n\n  const degToCardinal = deg => deg==='VRB'||isNaN(deg)?'variable':['N','NNE','NE','ENE','E','ESE','SE','SSE','S','SSW','SW','WSW','W','WNW','NW','NNW'][Math.round(((Number(deg)%360)\/22.5))%16];\n  const ktsToKmh = k => Math.round(Number(k)*1.852);\n  const hundredsFeetToMeters = h => Math.round(Number(h)*30.48); \n\n  const formatIssueDate = d => !(d instanceof Date)||isNaN(d)?'':\n    `${d.getUTCDate().toString().padStart(2,'0')} ${[\"Jan\",\"Feb\",\"Mar\",\"Apr\",\"May\",\"Jun\",\"Jul\",\"Aug\",\"Sep\",\"Oct\",\"Nov\",\"Dec\"][d.getUTCMonth()]} ${d.getUTCHours().toString().padStart(2,'0')}:${d.getUTCMinutes().toString().padStart(2,'0')} UTC`;\n  const formatBlockDate = d => !(d instanceof Date)||isNaN(d)?'':\n    `${d.getUTCDate().toString().padStart(2,'0')} ${[\"Jan\",\"Feb\",\"Mar\",\"Apr\",\"May\",\"Jun\",\"Jul\",\"Aug\",\"Sep\",\"Oct\",\"Nov\",\"Dec\"][d.getUTCMonth()]} ${d.getUTCHours().toString().padStart(2,'0')}:${d.getUTCMinutes().toString().padStart(2,'0')}`;\n\n  function splitBlocks(raw){\n    const text = raw.replace(\/\\r?\\n\/g,' ').replace(\/\\s+\/g,' ').replace(\/^TAF\\s+\\w+\\s+\/, '').trim();\n    const re = \/\\s\\b(PROB\\d{2}|TEMPO|BECMG|FM)\\s\/g; \n    const matches=[...text.matchAll(re)];\n    if(matches.length===0) return [{type:'MAIN',text:text}];\n    const blocks=[];\n    const mainText=text.slice(0,matches[0].index).trim(); \n    if(mainText.length>0) blocks.push({type:'MAIN',text:mainText});\n    matches.forEach((m,i)=>{\n      const end=matches[i+1]?matches[i+1].index:text.length;\n      blocks.push({type:m[1],text:text.slice(m.index+1,end).trim()});\n    });\n    return blocks.filter(b=>b.text.length>0);\n  }\n\n  const extractPeriod=(blockText,issueDate)=>{\n    const ddhhddhhMatch=blockText.match(\/\\b(\\d{4}\\\/\\d{4})\\b\/); \n    const fmddhhmmMatch=blockText.match(\/\\bFM\\s+(\\d{6})\\b\/); \n    if(ddhhddhhMatch){\n      const token=ddhhddhhMatch[1], sDay=parseInt(token.slice(0,2),10), sHour=parseInt(token.slice(2,4),10), eDay=parseInt(token.slice(5,7),10), eHour=parseInt(token.slice(7,9),10);\n      let year=issueDate.getUTCFullYear(), month=issueDate.getUTCMonth(), endMonth=month, endYear=year;\n      if(eDay<sDay){endMonth=(month+1)%12; if(endMonth===0) endYear++;}\n      return [new Date(Date.UTC(year, month, sDay, sHour)), new Date(Date.UTC(endYear, endMonth, eDay, eHour))];\n    } else if(fmddhhmmMatch){\n      const token=fmddhhmmMatch[1];\n      const day=parseInt(token.slice(0,2),10), hour=parseInt(token.slice(2,4),10), min=parseInt(token.slice(4,6),10);\n      return [new Date(Date.UTC(issueDate.getUTCFullYear(), issueDate.getUTCMonth(), day, hour, min)), null];\n    }\n    return [issueDate,new Date(issueDate.getTime()+24*3600*1000)];\n  };\n\n  const extractWind=t=>{const m=t.match(\/\\b((?:VRB|[\\d]{3}))(\\d{2})(?:G(\\d{2}))?KT\\b\/); if(m) return {dir:m[1],speed:+m[2],gust:m[3]?+m[3]:null}; if(\/\\bNSW\\b\/.test(t)) return {dir:'NSW',speed:0,gust:null}; return null;};\n  const extractVis=t=>{if(\/\\bCAVOK\\b\/.test(t)) return 9999; const m=t.match(\/(?:\\s|^)(\\d{4})(?:\\s|$)\/); return m?+m[1]:null;};\n  const extractClouds=t=>[...t.matchAll(\/(FEW|SCT|BKN|OVC|NSC|SKC|CLR)(\\d{3}|000)?(CB|TCU)?\/g)].map(m=>({type:m[1],hundreds:m[2],extra:m[3]||null}));\n  const extractPrecip=t=>[...t.matchAll(\/\\b([+\\-]?)(VC|MI|BC|PR|SH|TS|FZ|DR|BL|PO|DZ|RA|SN|SG|IC|PL|GR|GS|UP|BR|FG|FU|VA|DU|SA|HZ|PY|SQ|SS|DS|FC){1,3}\\b\/g)].map(m=>m[0]);\n\n  function humanizeBlock(block,issueDate,nextBlockStart){\n    const labelMap={MAIN:'Main Forecast',BECMG:'Gradually becoming',TEMPO:'Temporary conditions',FM:'From'};\n    const label=labelMap[block.type]||(block.type.startsWith('PROB')?`${block.type.slice(4)}% chance`:block.type);\n    let [start,end]=extractPeriod(block.text,issueDate);\n    if(block.type==='FM'&&nextBlockStart) end=nextBlockStart; else if(block.type==='FM'&&end===null){const mainBlock=splitBlocks(rawEl.textContent)[0]; if(mainBlock){const mainPeriod=extractPeriod(mainBlock.text,issueDate); end=mainPeriod[1];}}\n    const wind=extractWind(block.text);\n    const vis=extractVis(block.text);\n    const clouds=extractClouds(block.text);\n    const precipCodes=extractPrecip(block.text);\n    const pieces=[];\n    if(wind){if(wind.dir==='NSW'){pieces.push('no significant wind change');}else{const dirTxt=wind.dir==='VRB'?'variable':`${wind.dir}\u00b0 (${degToCardinal(wind.dir)})`; let w=`${dirTxt} at ${ktsToKmh(wind.speed)} km\/h`; if(wind.gust) w+=` (gusts to ${ktsToKmh(wind.gust)} km\/h)`; pieces.push(`wind ${w}`);}}\n    if(vis!==null) pieces.push(`visibility ${vis>=9999?'10+ km':Math.round(vis\/1000)+' km'}`);\n    if(precipCodes.length){const decoded=precipCodes.map(code=>{const intensity=code.startsWith('+')?'heavy ':code.startsWith('-')?'light ':'', baseCode=code.replace(\/^[+\\-]\/,''), weatherParts=baseCode.match(\/[A-Z]{2}\/g)||[baseCode]; const weather=weatherParts.map(w=>weatherMap[w]||w.toLowerCase()).join(' '); return intensity+weather;}).join(' and '); pieces.push(decoded);}\n    if(clouds.length){const cTxt=clouds.map(c=>{const map={FEW:'a few',SCT:'scattered',BKN:'broken',OVC:'overcast',NSC:'no significant clouds',SKC:'sky clear',CLR:'sky clear'}; if(c.type==='SKC'||c.type==='CLR'||c.type==='NSC') return map[c.type]; const h=hundredsFeetToMeters(c.hundreds); const extra=c.extra?(c.extra==='CB'?' (cumulonimbus)':' (towering cumulus)'):''; return `${map[c.type]||c.type.toLowerCase()} clouds around ${h} m${extra}`;}).join(', '); pieces.push(cTxt);}\n    if(!pieces.length) return null;\n    let periodPhrase;\n    if(end===null||block.type==='FM'){periodPhrase=`from ${formatBlockDate(start)}`; if(end!==null) periodPhrase+=` to ${formatBlockDate(end)}`; else if(block.type==='FM') periodPhrase+=` until next change`;}\n    else{periodPhrase=`for ${formatBlockDate(start)} to ${formatBlockDate(end)}`; if(block.type==='TEMPO'||block.type.startsWith('PROB')) periodPhrase=`between ${formatBlockDate(start)} and ${formatBlockDate(end)}`;}\n    return `${label} ${periodPhrase} \u2014 ${pieces.join(', ')}.`;\n  }\n\n  async function loadAndRender(){\n    try{\n      rawEl.textContent=summaryEl.textContent='Loading\u2026';\n      detailsEl.innerHTML=issuedEl.textContent='';\n      toggleBtn.style.display='none';  toggleBtn.innerHTML ='&#10133;';\n      const timestampedUrl=`${RAW_URL}?t=${Date.now()}`;\n      const resp=await fetch(timestampedUrl,{cache:'no-cache'}); if(!resp.ok) throw new Error(`API ${resp.status}`);\n      const data=await resp.json();\n      const taf=data[0]; if(!taf||!taf.rawTAF) throw new Error('TAF data missing or JSON is empty.');\n      const raw=taf.rawTAF.trim();\n      rawEl.textContent=raw.replace(\/^TAF\\s+\\w+\\s+\/,'').trim();\n\n      const issueDate = new Date(taf.issueTime);\n      issuedEl.textContent=`Issued at ${formatIssueDate(issueDate)}`;\n\n      const blocks=splitBlocks(raw);\n      const main=blocks.find(b=>b.type==='MAIN')||blocks[0];\n      summaryEl.textContent=humanizeBlock(main,issueDate)||'No significant information available.';\n\n      const detailBlocks=blocks.filter(b=>b.type!=='MAIN'); \n      const details=[];\n      for(let i=0;i<detailBlocks.length;i++){\n        const currentBlock=detailBlocks[i]; \n        const nextBlockStart=i+1<detailBlocks.length?extractPeriod(detailBlocks[i+1].text,issueDate)[0]:null; \n        const humanized=humanizeBlock(currentBlock,issueDate,nextBlockStart); \n        if(humanized) details.push(humanized);\n      }\n      if(details.length){\n        detailsEl.innerHTML=details.map(d=>`<p>${d.replace(\/^(.*?) \u2014\/,'<strong>$1<\/strong> \u2014')}<\/p>`).join('');\n        toggleBtn.style.display='block';\n      } else detailsEl.innerHTML='<em style=\"color:#666\">No additional temporary or becoming changes reported.<\/em>';\n    }catch(err){\n      console.error('TAF lpma Widget Error:',err); \n      rawEl.textContent='Error fetching TAF.'; \n      summaryEl.textContent=`Loading failed: ${err.message}.`; \n      detailsEl.innerHTML=''; \n      issuedEl.textContent='';\n    }\n  }\n\n  toggleBtn.addEventListener('click',()=>{\n    const showing=detailsEl.style.display!=='none'; \n    detailsEl.style.display=showing?'none':'block'; \n    toggleBtn.innerHTML = showing?'&#10133;':'&#10134;'; \n    toggleBtn.setAttribute('aria-expanded',!showing);\n  });\n\n  await loadAndRender();\n  setInterval(loadAndRender,30*60*1000);\n})();\n<\/script>\n\n\n\n<p class=\"has-text-align-center\"><a href=\"https:\/\/paragliding-in-madeira.com\/weather\/metar-history\/\" title=\"\"><small><em>Porto Santo and Madeira METAR and TAF<\/em><\/small><\/a><\/p>\n\n\n\n<div class=\"wp-block-group has-global-padding is-layout-constrained wp-block-group-is-layout-constrained\">\n<div style=\"height:50px\" aria-hidden=\"true\" class=\"wp-block-spacer\"><\/div>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity is-style-wide\"\/>\n\n\n\n<p class=\"has-text-align-center\"><strong>Travel Partners<\/strong><\/p>\n\n\n\n<figure class=\"wp-block-image aligncenter size-large\" id=\"travel\"><a href=\"https:\/\/omio.sjv.io\/QjXKbo\" target=\"_blank\" rel=\" noreferrer noopener\"><img loading=\"lazy\" decoding=\"async\" width=\"708\" height=\"1024\" src=\"https:\/\/paragliding-in-madeira.com\/weather\/wp-content\/uploads\/2025\/07\/Omio-banner-708x1024.jpg\" alt=\"\" class=\"wp-image-4351\" srcset=\"https:\/\/paragliding-in-madeira.com\/weather\/wp-content\/uploads\/2025\/07\/Omio-banner-708x1024.jpg 708w, https:\/\/paragliding-in-madeira.com\/weather\/wp-content\/uploads\/2025\/07\/Omio-banner-208x300.jpg 208w, https:\/\/paragliding-in-madeira.com\/weather\/wp-content\/uploads\/2025\/07\/Omio-banner-768x1110.jpg 768w, https:\/\/paragliding-in-madeira.com\/weather\/wp-content\/uploads\/2025\/07\/Omio-banner-1063x1536.jpg 1063w, https:\/\/paragliding-in-madeira.com\/weather\/wp-content\/uploads\/2025\/07\/Omio-banner.jpg 1080w\" sizes=\"auto, (max-width: 708px) 100vw, 708px\" \/><\/a><\/figure>\n<\/div>\n\n\n\n<div class=\"wp-block-group has-global-padding is-layout-constrained wp-block-group-is-layout-constrained\">\n<div class=\"wp-block-group has-global-padding is-layout-constrained wp-block-group-is-layout-constrained\">\n<div class=\"wp-block-group has-global-padding is-layout-constrained wp-block-group-is-layout-constrained\">\n<div align=\"center\">\n<script>\n  (function addWidget(d) {\n    var widgetStyle = d.createElement('link');\n    widgetStyle.setAttribute('rel', 'stylesheet');\n    widgetStyle.setAttribute('href', 'https:\/\/www.omio.com\/gcs-proxy\/b2b-nemo-prod\/bundle\/en\/bundle.css?v=' + new Date().getTime());\n    d.head.appendChild(widgetStyle);\n    var widgetScript = d.createElement('script');\n    widgetScript.setAttribute('src', 'https:\/\/www.omio.com\/gcs-proxy\/b2b-nemo-prod\/bundle\/en\/bundle.js?v=' + new Date().getTime());\n    d.body.appendChild(widgetScript);\n  })(document);\n<\/script>\n<div\n  data-omio-widget=\"true\"\n  data-partner-id=\"origensbyrubengt\"\n  data-default-travel-mode=\"flight\"\n  data-new-tab=\"true\"\n  data-redirect=\"https:\/\/omio.sjv.io\/c\/4493810\/861892\/7385?url=\"\n><\/div>\n<\/div>\n\n\n\n<details class=\"wp-block-details is-style-default is-layout-flow wp-block-details-is-layout-flow\" style=\"padding-right:0;padding-left:0\"><summary><\/summary>\n<p class=\"has-text-align-center\"><sup>Affiliate partner that helps simplify your travels at no extra cost to you<\/sup><\/p>\n<\/details>\n\n\n\n<div style=\"height:24px\" aria-hidden=\"true\" class=\"wp-block-spacer\"><\/div>\n<\/div>\n<\/div>\n<\/div>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<div class=\"wp-block-group has-global-padding is-layout-constrained wp-block-group-is-layout-constrained\">\n<div class=\"wp-block-group has-global-padding is-layout-constrained wp-block-group-is-layout-constrained\">\n<div class=\"wp-block-group has-global-padding is-layout-constrained wp-block-group-is-layout-constrained\">\n<div class=\"wp-block-group has-global-padding is-layout-constrained wp-block-group-is-layout-constrained\">\n<div class=\"wp-block-group has-global-padding is-layout-constrained wp-block-group-is-layout-constrained\">\n<div style=\"height:24px\" aria-hidden=\"true\" class=\"wp-block-spacer\"><\/div>\n\n\n\n<h5 class=\"wp-block-heading has-text-align-center\"><strong>Car and hotel<\/strong><\/h5>\n\n\n\n<div data-skyscanner-widget=\"MultiVerticalWidget\"><\/div>\n<script src=\"https:\/\/widgets.skyscanner.net\/widget-server\/js\/loader.js\" async><\/script>\n<\/div>\n<\/div>\n<\/div>\n<\/div>\n\n\n\n<details class=\"wp-block-details is-layout-flow wp-block-details-is-layout-flow\"><summary><\/summary>\n<p class=\"has-text-align-center\"><sup>Skyscanner is not an active partner yet, included it here because it works great<\/sup><\/p>\n<\/details>\n\n\n\n<div style=\"height:33px\" aria-hidden=\"true\" class=\"wp-block-spacer\"><\/div>\n<\/div>\n","protected":false},"excerpt":{"rendered":"<p>&#8212; km\/h VRB &#8212; km\/h VRB &#8212; km\/h Find your best route | METAR and TAF Madeira Airport live info Webcam, real time airplane tracking and runway weather conditions Madeira Airport Time &#8211;:&#8211; Loading&#8230; Sunrise &#8211;:&#8211; Sunset &#8211;:&#8211; Day Length &#8211;h &#8211;m Arrivals Loading live data\u2026 Departures Loading live data\u2026 &#x2708;&#xfe0f; December brought in 212,200 [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":0,"parent":0,"menu_order":0,"comment_status":"closed","ping_status":"closed","template":"","meta":{"slim_seo":{"title":"Madeira Airport live - Madeira Island weather","description":"Webcam, real time airplane tracking and runway weather conditions. Find your best route."},"footnotes":""},"class_list":["post-4192","page","type-page","status-publish","hentry"],"_links":{"self":[{"href":"https:\/\/paragliding-in-madeira.com\/weather\/wp-json\/wp\/v2\/pages\/4192","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/paragliding-in-madeira.com\/weather\/wp-json\/wp\/v2\/pages"}],"about":[{"href":"https:\/\/paragliding-in-madeira.com\/weather\/wp-json\/wp\/v2\/types\/page"}],"author":[{"embeddable":true,"href":"https:\/\/paragliding-in-madeira.com\/weather\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/paragliding-in-madeira.com\/weather\/wp-json\/wp\/v2\/comments?post=4192"}],"version-history":[{"count":557,"href":"https:\/\/paragliding-in-madeira.com\/weather\/wp-json\/wp\/v2\/pages\/4192\/revisions"}],"predecessor-version":[{"id":10499,"href":"https:\/\/paragliding-in-madeira.com\/weather\/wp-json\/wp\/v2\/pages\/4192\/revisions\/10499"}],"wp:attachment":[{"href":"https:\/\/paragliding-in-madeira.com\/weather\/wp-json\/wp\/v2\/media?parent=4192"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}