{"id":11,"date":"2024-09-24T11:29:30","date_gmt":"2024-09-24T11:29:30","guid":{"rendered":"https:\/\/paragliding-in-madeira.com\/weather\/?page_id=11"},"modified":"2026-04-13T18:32:46","modified_gmt":"2026-04-13T17:32:46","slug":"weather-stations","status":"publish","type":"page","link":"https:\/\/paragliding-in-madeira.com\/weather\/weather-stations\/","title":{"rendered":"Weather station"},"content":{"rendered":"\n<figure class=\"wp-block-image aligncenter size-full is-resized\"><a href=\"https:\/\/paragliding-in-madeira.com\/weather\/weather-stations\/\"><img loading=\"lazy\" decoding=\"async\" width=\"777\" height=\"500\" src=\"https:\/\/paragliding-in-madeira.com\/weather\/wp-content\/uploads\/2024\/10\/weather-station.png\" alt=\"weather station\" class=\"wp-image-378\" style=\"width:287px;height:auto\" srcset=\"https:\/\/paragliding-in-madeira.com\/weather\/wp-content\/uploads\/2024\/10\/weather-station.png 777w, https:\/\/paragliding-in-madeira.com\/weather\/wp-content\/uploads\/2024\/10\/weather-station-300x193.png 300w, https:\/\/paragliding-in-madeira.com\/weather\/wp-content\/uploads\/2024\/10\/weather-station-768x494.png 768w\" sizes=\"auto, (max-width: 777px) 100vw, 777px\" \/><\/a><\/figure>\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:\/\/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\n\n\n<div class=\"wp-block-button\"><a class=\"wp-block-button__link wp-element-button\" href=\"https:\/\/paragliding-in-madeira.com\/weather\/contribute\/\">Contribute<\/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\/live-tracking\/\">Live<\/a><\/div>\n<\/div>\n\n\n\n<p class=\"has-text-align-center\"><a href=\"https:\/\/paragliding-in-madeira.com\/weather\/canhas\">Canhas<\/a> | <a href=\"https:\/\/paragliding-in-madeira.com\/weather\/porto-da-Cruz\/\">Porto da Cruz<\/a> | <a href=\"https:\/\/paragliding-in-madeira.com\/weather\/portela\/\">Portela<\/a><\/p>\n\n\n\n<!-- Previous windguru widgets map\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  \/* 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    width: 100%;\n    text-align: center;\n}\n\n\/* ADDED: Gust speed number style (Orange for Gust, with spacing from the separator) *\/\n.gust-value {\n    color: orange; \n    font-weight: 500; \n    padding-left: 1px; \/* The fix you discovered *\/\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 { transform: translate(-50%, -50%) scale(2); z-index: 100; }\n  }\n  .weather-pin:active { transform: translate(-50%, -50%) scale(1.5); }\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 (Hides the Title) --- *\/\n  .wg-content { \nwidth: 100%; \nmargin-top: -11px; \npadding-right: 10px;\n }\n  .wg-sizer { \nmax-width: 48px; \n}\n\n  \n  \/* --- Custom Style for LPPS (Porto Santo) Pin --- *\/\n  .pos-lpps .pin-crop {\n    width: 48px;   \/* custom mobile width *\/\n    height: 45px; \/* Taller for 3 lines *\/\n    background: rgba(255, 255, 255, 0.8); \n  }\n\n  \/* --- The Common Airport Widget Styles (LPMA and LPPS)  --- *\/\n  .airport-container {\n    display: flex;\n    flex-direction: column;\n    align-items: center;\n    justify-content: center;\n    height: 100%;\n    width: 100%;\n    position: relative;\n    font-family: Arial, sans-serif;\n    color: #333;\n  }\n\n  .airport-arrow-icon {\n    width: 21px;\n    height: 17px;\n    fill: #185e96;\n    position: absolute;\n    top: 1px;\n    transition: transform 1s ease-out;\n    display: block;\n  }\n\n  \/* The Speed Number (Applies to the whole Speed \/ Gust text area) *\/\n  .airport-speed-text {\n    color: darkblue; \/* Wind Speed color *\/\n    font-weight: bold;\n    font-size: 12px;\n    margin-top: 14px; \n    z-index: 2;\n  }\n\n\/* The 'km\/h' unit text *\/\n  .airport-unit {\n    font-size: 7px;\n    line-height: 8px;\n    color: #666;\n    margin-top: -2px; \/* Close to the number text *\/\n  }\n\n  \/* Desktop \"GROW\" MODE *\/\n  @media (min-width: 768px) {\n    .weather-pin { \ntransform: translate(-50%, -50%) scale(1.7); }\n\n\/* Adjust Airport Custom Widget for Desktop Size *\/\n    .airport-arrow-icon { width: 22px; \nheight: 18px; \ntop: 1px; }\n    .airport-speed-text { \nfont-size: 12px; \nmargin-top: 11px; \n}\n    .airport-unit { \nfont-size: 8px; }\n\n    .airport-vrb { \ntop: 1px;  \n}\n  }\n\n  \/* --- COORDINATES --- *\/\n\n \n  .pos-canhas { top: 69%; left: 25%; }\n  .pos-portela { top: 55%; left: 69%; }\n  .pos-maiata { top: 36%; left: 76%; }\n  .pos-lpps { top: 15%; left: 92%; } \n\n<\/style>\n\n<div class=\"madeira-weather-map\">\n\n  <img decoding=\"async\" src=\"https:\/\/paragliding-in-madeira.com\/weather\/wp-content\/uploads\/2025\/11\/Madeira-and-Porto-Santo2.webp\" alt=\"Madeira Weather Map\">\n\n  <div class=\"weather-pin pos-canhas\">\n    <div class=\"pin-crop\">\n      <div class=\"wg-content\"><div class=\"wg-sizer\">\n        <script id=\"wglive_6014_1728522217581\">\n          (function (window, document) {\n            var loader = function () {\n              var arg = [\"spot=6014\",\"uid=wglive_6014_1728522217581\",\"color=light\",\"wj=kmh\",\"tj=c\",\"avg_min=0\",\"gsize=200\",\"msize=250\",\"m=3\",\"arrow=y\",\"show=n,g,c,f,m\"];\n              var script = document.createElement(\"script\");\n              var tag = document.getElementsByTagName(\"script\")[0];\n              script.src = \"https:\/\/www.windguru.cz\/js\/wglive.php?\"+(arg.join(\"&\"));\n              tag.parentNode.insertBefore(script, tag);\n            };\n            window.addEventListener ? window.addEventListener(\"load\", loader, false) : window.attachEvent(\"onload\", loader);\n          })(window, document);\n        <\/script>\n      <\/div><\/div>\n    <\/div>\n  <\/div>\n\n  <div class=\"weather-pin pos-portela\">\n    <div class=\"pin-crop\">\n      <div class=\"wg-content\"><div class=\"wg-sizer\">\n        <script id=\"wglive_6008_1730946002158\">\n          (function (window, document) {\n            var loader = function () {\n              var arg = [\"spot=6008\",\"uid=wglive_6008_1730946002158\",\"color=light\",\"wj=kmh\",\"tj=c\",\"avg_min=0\",\"gsize=200\",\"msize=250\",\"m=3\",\"arrow=y\",\"show=n,g,c,f,m\"];\n              var script = document.createElement(\"script\");\n              var tag = document.getElementsByTagName(\"script\")[0];\n              script.src = \"https:\/\/www.windguru.cz\/js\/wglive.php?\"+(arg.join(\"&\"));\n              tag.parentNode.insertBefore(script, tag);\n            };\n            window.addEventListener ? window.addEventListener(\"load\", loader, false) : window.attachEvent(\"onload\", loader);\n          })(window, document);\n        <\/script>\n      <\/div><\/div>\n    <\/div>\n  <\/div>\n\n  <div class=\"weather-pin pos-maiata\">\n    <div class=\"pin-crop\">\n      <div class=\"wg-content\"><div class=\"wg-sizer\">\n        <script id=\"wglive_5899_1728522217581\">\n          (function (window, document) {\n            var loader = function () {\n              var arg = [\"spot=5899\",\"uid=wglive_5899_1728522217581\",\"color=light\",\"wj=kmh\",\"tj=c\",\"avg_min=0\",\"gsize=200\",\"msize=250\",\"m=3\",\"arrow=y\",\"show=n,g,c,f,m\"];\n              var script = document.createElement(\"script\");\n              var tag = document.getElementsByTagName(\"script\")[0];\n              script.src = \"https:\/\/www.windguru.cz\/js\/wglive.php?\"+(arg.join(\"&\"));\n              tag.parentNode.insertBefore(script, tag);\n            };\n            window.addEventListener ? window.addEventListener(\"load\", loader, false) : window.attachEvent(\"onload\", loader);\n          })(window, document);\n        <\/script>\n      <\/div><\/div>\n    <\/div>\n  <\/div>\n\n  \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-text\" id=\"lpps-speed-gust\">--<\/div>\n        <div class=\"airport-unit\" id=\"lpps-unit\">km\/h<\/div>\n      <\/div>\n    <\/div>\n  <\/div>\n\n<\/div>\n\n<script>\ndocument.addEventListener(\"DOMContentLoaded\", function() {\n\n  const STALE_MS = 60 * 60 * 1000; \/\/ 60 minutes\n  const REFRESH_MS = 5 * 60 * 1000; \/\/ refresh every 5 minutes\n\n  const airports = [\n    {\n      \n   \n      id: 'lpps',\n      jsonUrl: 'https:\/\/paragliding-in-madeira.com\/weather\/wp-content\/uploads\/metar-wind-lpps.json',\n      speedId: 'lpps-speed-gust', \/\/ Now holds the combined HTML\n      arrowId: 'lpps-arrow',\n      vrbId: 'lpps-vrb',\n      unitId: 'lpps-unit',\n      lightbox: 'https:\/\/paragliding-in-madeira.com\/weather\/lpps\/'\n    }\n  ];\n\n  \/**\n   * Shows primary display elements (used when fresh data is found).\n   *\/\n  function showWidget(a) {\n    const speedEl = document.getElementById(a.speedId);\n    const unitEl  = document.getElementById(a.unitId);\n    if (speedEl) { speedEl.style.display = 'block'; }\n    if (unitEl)  { unitEl.style.display = 'block'; }\n  }\n\n\n  \/**\n   * Hides all display elements (used when data is stale or on error).\n   *\/\n  function hideWidget(a) {\n    const speedEl = document.getElementById(a.speedId);\n    const arrowEl = document.getElementById(a.arrowId);\n    const vrbEl   = document.getElementById(a.vrbId);\n    const unitEl  = document.getElementById(a.unitId);\n\n    \/\/ Hide all elements\n    if (speedEl) { speedEl.innerHTML = '--'; speedEl.style.display = 'none'; }\n    if (unitEl)  unitEl.style.display = 'none';\n    if (arrowEl) arrowEl.style.display = 'none';\n    if (vrbEl)   vrbEl.style.display = 'none';\n  }\n\n\n  async function updateAirportWidget(a) {\n    try {\n      const res = await fetch(a.jsonUrl + '?t=' + Date.now());\n      if (!res.ok) throw new Error('HTTP ' + res.status);\n      const data = await res.json();\n      if (!data || !Array.isArray(data) || data.length === 0) {\n        hideWidget(a); return;\n      }\n\n      const latest = data[data.length - 1];\n      if (!latest || !latest.time) { hideWidget(a); return; }\n\n      const age = Date.now() - new Date(latest.time).getTime();\n      if (age > STALE_MS) { hideWidget(a); return; }\n      \n      showWidget(a); \n\n      const speedEl = document.getElementById(a.speedId);\n      const arrowEl = document.getElementById(a.arrowId);\n      const vrbEl   = document.getElementById(a.vrbId);\n\n      \/\/ Sanitize numbers\n      const speed = (latest.speed === null || typeof latest.speed === 'undefined') ? null : Math.round(latest.speed);\n      const gust = (latest.gust === null || typeof latest.gust === 'undefined') ? null : Math.round(latest.gust);\n      const dir = (typeof latest.direction === 'number') ? latest.direction : null;\n\n      \/\/ --- SPEED\/GUST LOGIC ---\n      if (speedEl) {\n          let displayContent = '--';\n          \n          if (speed !== null) {\n              if (gust !== null && gust > speed) {\n                  \/\/ Format as Speed \/ <span class=\"gust-value\">Gust<\/span>\n                  \/\/ Space added before \/ for visual separation\n                  displayContent = `${speed} \/<span class=\"gust-value\">${gust}<\/span>`;\n              } else {\n                  \/\/ Format as Speed only\n                  displayContent = String(speed);\n              }\n          }\n          \n          \/\/ Use innerHTML to render the span element\n          speedEl.innerHTML = displayContent;\n      }\n\n\n      \/\/ --- DIRECTION LOGIC ---\n      const isVRB = dir === 1;\n      const isCalm = dir === 0 && speed === 0;\n\n      if (isVRB) {\n        if (arrowEl) arrowEl.style.display = 'none';\n        if (vrbEl) vrbEl.style.display = 'block';\n      } else if (isCalm) {\n        if (arrowEl) arrowEl.style.display = 'none';\n        if (vrbEl) vrbEl.style.display = 'none';\n        if (speedEl) speedEl.innerHTML = '0'; \/\/ Use innerHTML for consistency\n      } else if (dir === null) {\n        if (arrowEl) arrowEl.style.display = 'none';\n        if (vrbEl) vrbEl.style.display = 'none';\n      } else {\n        \/\/ normal numeric direction\n        if (arrowEl) {\n          const rotation = (dir + 180) % 360;\n          arrowEl.style.transform = `rotate(${rotation}deg)`;\n          arrowEl.style.display = 'block';\n        }\n        if (vrbEl) vrbEl.style.display = 'none';\n      }\n\n    } catch (err) {\n      console.error('Airport error:', a.id, err);\n      hideWidget(a);\n    }\n  }\n\n  \/\/ initial + periodic refresh\n  function refreshAll() {\n    airports.forEach(a => updateAirportWidget(a));\n  }\n  refreshAll();\n  setInterval(refreshAll, REFRESH_MS);\n\n  \/* ---------------- LIGHTBOX (UNCHANGED) ---------------- *\/\n  if (!document.getElementById('airport-lightbox')) {\n    document.body.insertAdjacentHTML('beforeend', `\n      <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;\">\n        <div style=\"position:relative;width:90%;max-width:1000px;height:70%;border-radius:8px;overflow:hidden;\">\n          <iframe id=\"airport-lightbox-iframe\" src=\"\" style=\"width:100%;height:100%;border:none;\"><\/iframe>\n          <button id=\"airport-lightbox-close\" style=\"position:absolute;top:7px;right:11px;background:transparent;border:none;color:white;font-size:24px;cursor:pointer;\">\u2715<\/button>\n        <\/div>\n      <\/div>\n    `);\n  }\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 id = pin.getAttribute('data-airport');\n      const a = airports.find(x => x.id === id);\n      if (a) {\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', () => {\n    iframe.src = '';\n    lightbox.style.display = 'none';\n  });\n\n  lightbox.addEventListener('click', (e) => { if (e.target === lightbox) { iframe.src=''; lightbox.style.display='none'; } });\n\n});\n<\/script>\n-->\n\n\n\n<!-- Brilliant with day and night from our solunar and direction logic. windguru lazy load, independent station wind and direction limits and gust check -->\n<!-- FINAL LAUNCH VERSION 14-03-2026 with temp and rain -->\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\/* 1. The Toggle: Hide arrow if NOT in wind mode *\/\n.madeira-weather-map:not([data-view=\"wind\"]) .airport-arrow-icon,\n.madeira-weather-map:not([data-view=\"wind\"]) .airport-unit { \n  display: none !important; \n}\n\n\/* Create a simple grey overlay for Temp and Rain views *\/\n.madeira-weather-map:not([data-view=\"wind\"]) .pin-overlay {\n  background-color: #e5e5e5 !important; \/* Light grey *\/\n opacity: 0.8 !important; \/* increased slightly for better readability *\/\n}\n\n.madeira-weather-map:not([data-view=\"wind\"]) .airport-container { \n  justify-content: center !important; \n}\n\n.madeira-weather-map:not([data-view=\"wind\"]) .airport-speed-text { \n  margin-top: 0 !important; \n  line-height: 1.1; \n}\n\n\/* Sub-text Styling (Humidity % and Total Rain mm) *\/\n.madeira-weather-map:not([data-view=\"wind\"]) .airport-speed-text {\n  font-size: 14px ; \n  color: #185e96; \/* Deep blue for clear visibility *\/\n  letter-spacing: 3px; \/* Reset any mobile wind tracking *\/\n}\n\n\/* Top Value: Temperature or Live Rain mm\/h *\/\n.live-value, .live-rain-val {\nfont-size: 11px;\n  margin-top: -12px; \/* Replaces the inline JS style *\/\n  display: block;\n  line-height: 2;\n}\n\n\/* Sub-label styling for Total Rain - Absolute Centering *\/\n.rain-total {\n  font-size: 10px;\n  font-weight: bold;\n  opacity: 0.85;\n  color: #444; \n  text-shadow: none;\n  position: absolute;\n  bottom: 6px; \/* Locks to bottom of circle *\/\n  left: 0;\n  right: 0;\n  text-align: center;\n  display: block;\n  line-height: 0.7; \/* Tighter vertical stacking for the mm *\/\n}\n\n.temp-hum {\n  font-size: 10px;\n  font-weight: bold;\n  opacity: 0.85;\n  color: #444; \n  text-shadow: none;\n  position: absolute;\n  bottom: 10px; \/* pulls hum closer to temp *\/\n  left: 0;\n  right: 0;\n  text-align: center;\n  display: block;\n  line-height: 0.5;\n}\n\n\/* Individually target the mm unit *\/\n.unit-mm {\n  font-size: 8px;\n  font-weight: normal; \/* making it thinner makes the number pop more *\/\n  text-transform: lowercase;\n  display: inline-block;\n  margin-top: -3px; \/* Pulls it up slightly closer to its number *\/\n}\n\n\/* --- VIEW SWITCHER BUTTONS --- *\/\n.map-view-switcher {\n  display: flex;\n  gap: 10px;\n  justify-content: center;\n  margin-bottom: 15px;\n}\n\n.switcher-btn {\n  padding: 8px 16px;\n  border: 1px solid #185e96;\n  background: white;\n  color: #185e96;\n  font-weight: bold;\n  border-radius: 20px;\n  cursor: pointer;\n  transition: all 0.3s ease;\n  font-size: 12px;\n}\n\n.switcher-btn.active {\n  background: #185e96;\n  color: white;\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    width: 100%;\n    text-align: center;\n  }\n\n  .gust-value {\n    color: #853e08; \/* Slightly darker orange for better contrast *\/\n    font-weight: 500; \n    padding-left: 1px;\nletter-spacing: -0.1px; \/* Keeps the gust digits tight to the slash *\/\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  @media (min-width: 768px) {\n    .weather-pin:hover { transform: translate(-50%, -50%) scale(2); z-index: 100; }\n  }\n  .weather-pin:active { transform: translate(-50%, -50%) scale(1.5); }\n\n  \/* --- THE CROPPER --- *\/\n  .pin-crop {\n    width: 46px;   \n    height: 46px; \n    border-radius: 50%;\n    background: rgba(255, 255, 255, 0.85);  \/* fff if needed Solid White Base *\/\n    box-shadow: 0 1px 3px rgba(0,0,0,0.2);\n    overflow: hidden;\n    position: relative;\n    transition: all 0.3s ease;\n  }\n\n  \/* --- COLOR OVERLAY --- \n     This sits on top of the white bg but behind the text \n     We use JS to change the background-color of this layer \n  *\/\n  .pin-overlay {\n    position: absolute;\n    top: 0; left: 0;\n    width: 100%; height: 100%;\n    opacity: 0.2; \/* The magic opacity for the color overlay *\/\n    z-index: 1;\n    transition: background-color 0.5s ease;\n    background-color: transparent; \/* Default *\/\n  }\n  \/* DESKTOP OVERRIDE: Richer opacity for larger screens *\/\n  @media (min-width: 768px) {\n    .pin-overlay { opacity: 0.4; }\n}\n\n  \/* Specific Adjustments for Porto Santo Pin *\/\n  .pos-lpps .pin-crop {\n    width: 48px;\n    height: 45px;\n  }\n\n  .airport-container {\n    display: flex; \n    flex-direction: column; \n    align-items: center; \n    justify-content: center;\n    height: 100%; \n    width: 100%; \n    position: relative; \n    font-family: Arial, sans-serif; \n    color: #333;\n    z-index: 2; \/* Text sits ABOVE the overlay *\/\n  }\n\n  .airport-arrow-icon {\n    width: 21px; \n    height: 17px; \n    fill: #185e96; \n    position: absolute; \n    top: 1px;\n    transition: transform 1s ease-out; \n    display: block;\n  }\n\n  \/* This is the Wind speed and Gust text size and style *\/\n  .airport-speed-text {\n    color: darkblue; \n    font-weight: bold; \n    font-size: 11px; \n    margin-top: 14px; \n    z-index: 2;\n    \/* Gentle white shadow to ensure text pops against any color *\/\n    text-shadow: 0px 0px 1px rgba(0, 0, 0, 0.3);\nwhite-space: nowrap; \n\/* makes everything into one line *\/\n  display: inline-block; \n\/* for transform scale to work *\/\n  }\n\n  .airport-unit { \n    font-size: 7px; \n    line-height: 8px; \n    color: #444; \n    margin-top: -2px; \n    text-shadow: 0px 0px 1px rgba(0, 0, 0, 0.4);\n  }\n\n  \/* --- TRIGGER LAYER (Invisible but clickable) --- *\/\n  .wg-proxy-layer { \n    position: absolute; \n    top: 0; left: 0; \n    width: 100%; \n    height: 100%; \n    opacity: 0; \n    z-index: 5; \n  }\n\n  \/* Desktop \"GROW\" MODE *\/\n  @media (min-width: 768px) {\n    .weather-pin { transform: translate(-50%, -50%) scale(1.7); }\n    .airport-arrow-icon { width: 22px; height: 18px; top: 1px; }\n    .airport-speed-text { font-size: 11px; margin-top: 11px; }\n    .airport-unit { font-size: 8px; }\n    .airport-vrb { top: 1px; }\n  }\n\n  \/* --- COORDINATES --- *\/\n \n  .pos-canhas { top: 69%; left: 25%; }\n  .pos-portela { top: 55%; left: 69%; }\n  .pos-maiata { top: 36%; left: 76%; }\n  .pos-lpps { top: 15%; left: 92%; } \n\n\n  \/* 4. MOBILE OVERRIDES *\/\n  @media (max-width: 480px) {\n    .pin-crop { width: 40px; height: 40px; }\n    .airport-speed-text { font-size: 11px; margin-top: 8px; }\n    .airport-unit { font-size: 8px; }\n    .airport-arrow-icon { width: 16px; height: 14px; }\n    .pos-maiata { left: 78%; }\n  }\n\n\/* Hide LPPS station on TEMP and RAIN views *\/\n.madeira-weather-map:not([data-view=\"wind\"]) .pos-lpps {\n  display: none;\n}\n<\/style>\n\n<div class=\"map-view-switcher\">\n  <button class=\"switcher-btn active\" id=\"btn-wind\" onclick=\"setMapView('wind')\">WIND<\/button>\n  <button class=\"switcher-btn\" id=\"btn-temp\" onclick=\"setMapView('temp')\">TEMP<\/button>\n  <button class=\"switcher-btn\" id=\"btn-rain\" onclick=\"setMapView('rain')\">RAIN<\/button>\n<\/div>\n\n<div class=\"madeira-weather-map\" id=\"lazy-weather-map\" data-view=\"wind\">\n\n  <img decoding=\"async\" src=\"https:\/\/paragliding-in-madeira.com\/weather\/wp-content\/uploads\/2025\/11\/Madeira-and-Porto-Santo2.webp\" alt=\"Madeira Weather Map\">\n\n  <div class=\"weather-pin pos-canhas\" data-station=\"canhas\">\n    <div class=\"pin-crop\">\n      <div class=\"pin-overlay\" id=\"canhas-overlay\"><\/div>\n      <div class=\"wg-proxy-layer\"><\/div>\n      <div class=\"airport-container\">\n        <svg class=\"airport-arrow-icon\" id=\"canhas-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-text\" id=\"canhas-speed-gust\">&#8212;<\/div>\n        <div class=\"airport-unit\" id=\"canhas-unit\">km\/h<\/div>\n      <\/div>\n    <\/div>\n  <\/div>\n\n  <div class=\"weather-pin pos-portela\" data-station=\"portela\">\n    <div class=\"pin-crop\">\n      <div class=\"pin-overlay\" id=\"portela-overlay\"><\/div>\n      <div class=\"wg-proxy-layer\">\n              <\/div>\n      <div class=\"airport-container\">\n        <svg class=\"airport-arrow-icon\" id=\"portela-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-text\" id=\"portela-speed-gust\">&#8212;<\/div>\n        <div class=\"airport-unit\" id=\"portela-unit\">km\/h<\/div>\n      <\/div>\n    <\/div>\n  <\/div>\n\n  <div class=\"weather-pin pos-maiata\" data-station=\"maiata\">\n    <div class=\"pin-crop\">\n      <div class=\"pin-overlay\" id=\"maiata-overlay\"><\/div>\n      <div class=\"wg-proxy-layer\">\n              <\/div>\n      <div class=\"airport-container\">\n        <svg class=\"airport-arrow-icon\" id=\"maiata-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-text\" id=\"maiata-speed-gust\">&#8212;<\/div>\n        <div class=\"airport-unit\" id=\"maiata-unit\">km\/h<\/div>\n      <\/div>\n    <\/div>\n  <\/div>\n\n    <div class=\"weather-pin pos-lpps\" data-station=\"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-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\" locked to Madeira time\nfunction getMadeiraDateString() {\n  return new Date().toLocaleDateString('en-CA', { timeZone: 'Atlantic\/Madeira' });\n}\n\n\/\/ Global state - default wind\nlet currentMapMode = 'wind';\n\n\/\/ 1. GLOBAL SWITCHER FUNCTION\n\nwindow.setMapView = function(mode) {\n  currentMapMode = mode;\ndocument.getElementById('lazy-weather-map').setAttribute('data-view', mode);\n  \n    \/\/ Update Map Attribute for CSS centering\n  const map = document.getElementById('lazy-weather-map');\n  if (map) map.setAttribute('data-view', mode);\n  \n  \/\/ 2. Update Button UI\n  document.querySelectorAll('.switcher-btn').forEach(btn => btn.classList.remove('active'));\n  const activeBtn = document.getElementById('btn-' + mode);\n  if (activeBtn) activeBtn.classList.add('active');\n  \n  \/\/ 3. Trigger Re-render using the global bridge\n  if (typeof window.refreshAll === \"function\") {\n    window.refreshAll();\n  }\n};\n\n\ndocument.addEventListener(\"DOMContentLoaded\", function() {\n  const STALE_MS = 60 * 60 * 1000;\n  const REFRESH_MS = 1 * 60 * 1000;\n\n  \/\/ --- 1. ROBUST DAY\/NIGHT CYCLE (Moon Script Logic) ---\n  \/\/ Default fallbacks in case fetch is slow (09:00 to 21:00)\n  \/\/ We use simple strings \"HH:MM\" to compare, just like your Moon script.\n  let globalSunrise = \"09:00\"; \n  let globalSunset = \"21:00\";\n\n  \/\/ Fetch only once per session (or every hour) to save resources\n  async function fetchSolunarData() {\n    try {\n      const res = await fetch('https:\/\/paragliding-in-madeira.com\/weather\/wp-content\/moon-time\/solunar-data.json?t=' + Date.now());\n      const data = await res.json();\n      if (data && data.today) {\n        \/\/ We just store the strings. No complex Date math needed.\n        if(data.today.sunrise) globalSunrise = data.today.sunrise; \/\/ e.g., \"07:56\"\n        if(data.today.sunset) globalSunset = data.today.sunset;    \/\/ e.g., \"18:48\"\n        \/\/ Force a refresh of the widgets now that we have accurate times\n        refreshAll();\n      }\n    } catch (e) {\n      console.log(\"Solunar sync failed, using defaults:\", e);\n    }\n  }\n\n  \/\/ The Check: Atlantic\/Madeira time for DST Safety\n  function isNightTime() {\n    const now = new Date();\n    \n\/\/ toLocaleString extracts exact HH:mm in Madeira, handles DST shifts (WET - WEST)\n    const madeiraTimeStr = now.toLocaleString('en-GB', {\n      hour: '2-digit',\n      minute: '2-digit',\n      hour12: false,\n      timeZone: 'Atlantic\/Madeira'\n    });\n    \n\/\/ It is DAY if current Madeira time is between Rise and Set, Otherwise Night.\n    const isDay = (madeiraTimeStr >= globalSunrise && madeiraTimeStr < globalSunset);\n\n    return !isDay;\n  }\n\n  \/\/ --- STATION CONFIGURATION ---\n  \/\/ ADJUST EACH STATION LIMITS HERE INDEPENDENTLY\n  \/\/ low\/med\/high correspond to Light Blue \/ Green \/ Yellow\n  \/\/ Anything above 'high' becomes Orange\n  \/\/ gustMax: If gust exceeds this, it turns Orange\n  const stations = [\n    {\n       id: 'canhas', isWindguru: true, wgSpot: '6014',\n      jsonUrl: 'https:\/\/paragliding-in-madeira.com\/weather\/wp-content\/uploads\/weather-stations\/canhas\/' + getMadeiraDateString() + '.json',\n      speedId: 'canhas-speed-gust', arrowId: 'canhas-arrow', unitId: 'canhas-unit', overlayId: 'canhas-overlay',\n      \/\/ Canhas: Takeoff orientation SW (222) - Flyable from 180 to 280\n      sector: { start: 180, end: 280 },\n      \/\/ Speed limits: <= 5 is blue, <= 17 green, <= 22 yellow, >22 orange\n      limits: { low: 5, med: 17, high: 22 }, \n      gustMax: 27 \/\/ Above this gust limit shows orange\n    },\n    {\n      id: 'portela', isWindguru: true, wgSpot: '6008',\n      jsonUrl: 'https:\/\/paragliding-in-madeira.com\/weather\/wp-content\/uploads\/weather-stations\/portela\/' + getMadeiraDateString() + '.json',\n      speedId: 'portela-speed-gust', arrowId: 'portela-arrow', unitId: 'portela-unit', overlayId: 'portela-overlay',\n      \/\/ Portela: Takeoff orientation North (360) - Flyable from 330 to 050 \n      sector: { start: 330, end: 50 },\n      \/\/ Speed limits: <= 5 is blue, <= 18 green, <= 24 yellow, >24 orange\n      limits: { low: 5, med: 18, high: 24 }, \n      gustMax: 30 \/\/ Above this gust limit shows orange\n    },\n    {\n       id: 'maiata', isWindguru: true, wgSpot: '5899',\n      jsonUrl: 'https:\/\/paragliding-in-madeira.com\/weather\/wp-content\/uploads\/weather-stations\/maiata\/' + getMadeiraDateString() + '.json',\n      speedId: 'maiata-speed-gust', arrowId: 'maiata-arrow', unitId: 'maiata-unit', overlayId: 'maiata-overlay',\n      \/\/ Maiata: Takeoff orientation North (360) - Flyable from 333 to 055 \n      sector: { start: 333, end: 55 },\n\/\/ Speed limits: <= 17 is blue, <= 26 green, <= 32 yellow, >32 orange\n      limits: { low: 17, med: 26, high: 32 },\n      gustMax: 33 \/\/ Above this gust limit shows orange\n    },\n        {\n      id: 'lpps',\n      jsonUrl: 'https:\/\/paragliding-in-madeira.com\/weather\/wp-content\/uploads\/metar-wind-lpps.json',\n      speedId: '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\/\/ Delayed windguru load logic\nlet windguruInitialized = false;\n\n  function injectWindguru() {\n    if (windguruInitialized) return;\n    console.log(\"Master Switch: Loading Windguru Widgets...\");\n    \n    stations.forEach(s => {\n      if (s.isWindguru && s.wgSpot) {\n        const container = document.querySelector(`.pos-${s.id} .wg-proxy-layer`);\n        if (container) {\n          container.innerHTML = ''; \/\/ Clear any old scripts\n          const script = document.createElement('script');\n          const uid = `wglive_${s.wgSpot}_${Math.floor(Math.random()*1000)}`;\n          script.id = uid;\n          \n          \/\/ Using the specific Windguru URL pattern\n          const params = [\n            `spot=${s.wgSpot}`,\n            `uid=${uid}`,\n            \"color=light\", \"wj=kmh\", \"tj=c\", \"avg_min=0\",\n            \"gsize=200\", \"msize=250\", \"m=3\", \"arrow=y\", \"show=n,g,c,f,m\"\n          ];\n          \n          script.src = \"https:\/\/www.windguru.cz\/js\/wglive.php?\" + params.join(\"&\");\n          container.appendChild(script);\n        }\n      }\n    });\n    windguruInitialized = true;\n  }\n\n function showWidget(s) {\n    [s.speedId, s.unitId, s.arrowId].forEach(id => { \n      const el = document.getElementById(id);\n      if(el) el.style.display = 'block'; \n    });\n  }\n\n  function hideWidget(s) {\n    const el = document.getElementById(s.speedId); if(el){ el.innerHTML = '--'; el.style.display = 'none'; }\n    [s.unitId, s.arrowId, s.vrbId].forEach(id => { if(document.getElementById(id)) document.getElementById(id).style.display = 'none'; });\n  }\n\n  \/\/ --- HELPER: CHECK DIRECTION SECTOR ---\n  function isFlyableDirection(deg, sector) {\n    if (deg === null || !sector) return false;\n    if (sector.start < sector.end) {\n      \/\/ Standard case (e.g., 210 to 280)\n      return deg >= sector.start && deg <= sector.end;\n    } else {\n      \/\/ Crossing North sector (e.g., 330 to 40)\n      return deg >= sector.start || deg <= sector.end;\n    }\n  }\n\n  async function updateStationWidget(s) {\nconsole.log(\"Checking station:\", s.id); \/\/ This is our single live log line\n    try {\n      const res = await fetch(s.jsonUrl + '?t=' + Date.now());\n      const data = await res.json();\n      if (!data || data.length === 0) { hideWidget(s); return; }\n      \n      const latest = data[data.length - 1];\n      if (Date.now() - new Date(latest.time).getTime() > STALE_MS) { hideWidget(s); return; }\n      \n      showWidget(s); \n      let speed, gust, dir;\n      if (s.isWindguru) {\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      const speedEl = document.getElementById(s.speedId);\n      const overlayEl = s.overlayId ? document.getElementById(s.overlayId) : null;\n\n     \n \/\/ --- GUST DISPLAY LOGIC (Nested to avoid wordpress cleaning the &&) ---\n        if (speedEl) {\n\n\/\/ Clear the display first to prevent old data showing\n  speedEl.innerHTML = '';\n  const arrowEl = document.getElementById(s.arrowId);\n  const unitEl = document.getElementById(s.unitId);\n\nif (currentMapMode === 'wind') {\n  let content = String(speed);\n  if (gust !== null && gust > speed) {\n    content = `${speed} \/<span class=\"gust-value\">${gust}<\/span>`;\n  }\n  speedEl.innerHTML = content;\n} \nelse if (currentMapMode === 'temp') {\n  const hum = latest.humidity ? Math.round(latest.humidity) : '--';\n  \/\/ Wrapped the temp in a div to nudge it up and keep it separate from the absolute label\n  speedEl.innerHTML = `<div  class=\"live-value\">${latest.temp.c.toFixed(1)}\u00b0<\/div><span class=\"temp-hum\">${hum}%<\/span>`;\n\n} \nelse if (currentMapMode === 'rain') {\n  \/\/ 1. LIVE RAIN: Pulled from the raw daily JSON already in memory ('latest')\n  const liveR = (latest.precipRate && latest.precipRate.mm) ? latest.precipRate.mm : 0;\n  \n  \/\/ 2. TOTAL RAIN: Fetch the summary file only for this station (excluding LPPS)\n  let totalR = 0;\n  if (s.id !== 'lpps') {\n    const sRes = await fetch(s.jsonUrl.replace('.json', '.summary.json'));\n    if (sRes.ok) { \n      const sData = await sRes.json(); \n      totalR = (sData.stats && sData.stats.rain_total) ? sData.stats.rain_total : 0; \n    }\n  }\n\n  \/\/ 3. DISPLAY: Live rate on top (blue), Total accumulation on bottom (grey)\n  speedEl.innerHTML = `<div class=\"live-rain-val\">${liveR.toFixed(1)}<\/div><span class=\"rain-total\">${totalR.toFixed(1)}<br><span class=\"unit-mm\">mm<\/span><\/span>`;\n}\n\n \/\/ --- COLOR & LOGIC ---\n        if (overlayEl) {\n          let color = \"transparent\"; \/\/ Default\n\n          \/\/ 1. Determine Speed Color (Based on Station Limits)\n          if (speed > 0) {\n            if (s.limits) {\n              if (speed <= s.limits.low) color = \"#add8e6\";       \/\/ Light Blue\n              else if (speed <= s.limits.med) color = \"#90ee90\";  \/\/ Green\n              else if (speed <= s.limits.high) color = \"#ffff96\"; \/\/ Yellow\n              else color = \"#ffa500\";                             \/\/ Orange\n            }\n          }\n\n    \/\/ 2. Gust Check (Overrides speed color if gust is high) (Nested to avoid &#038;&#038;)\n          if (gust !== null) {\n            if (s.gustMax) {\n              if (gust > s.gustMax) {\n                color = \"#ffa500\"; \/\/ Makes it Orange on high Gust\n              }\n            }\n          }\n\n          \/\/ 3. Night Mode Check (Overrides everything)\n          \/\/ We call the synchronous helper here. Fast and safe\n          if (isNightTime()) {\n            color = \"#808080\"; \/\/ Grey\n          }\n\n          \/\/ 4. Direction Check  (Overrides everything) (Nested to avoid &&)\n          if (dir !== null) {\n            if (s.sector) {\n              if (!isFlyableDirection(dir, s.sector)) {\n                color = \"#808080\"; \n              }\n            }\n          }\n\n          overlayEl.style.backgroundColor = color;\n        }\n\n\/\/ --- AUTO-SCALE FONT 3 digit wind and gust ---\nconst charCount = speedEl.textContent.length;\n\nif (window.innerWidth <= 600) {\n  if (charCount >= 8) {\n\/\/ Triple digit (ex. 104 \/ 117) - fit 40px circle\n    speedEl.style.setProperty('font-size', '9.9px', 'important');\n    speedEl.style.setProperty('letter-spacing', '-0.3px', 'important');\n  } else {\n\/\/ RESET for 1 or 2 digits on mobile to 11px\n    speedEl.style.setProperty('font-size', '11px', 'important');\n    speedEl.style.setProperty('letter-spacing', '-0.1px', 'important');\n  }\n} else {\n  \/\/ Desktop handling: Let the CSS take over naturally\n  speedEl.style.fontSize = \"\";\n  speedEl.style.letterSpacing = \"\";\n}\n}\n      const arrowEl = document.getElementById(s.arrowId);\n      const vrbEl = document.getElementById(s.vrbId);\n      \n      if (vrbEl && dir === 1) { \n        if(arrowEl) arrowEl.style.display = 'none'; \n        vrbEl.style.display = 'block'; \n      } else {\n        if(vrbEl) vrbEl.style.display = 'none';\n        if(arrowEl) {\n          arrowEl.style.transform = `rotate(${(dir + 180) % 360}deg)`;\n          arrowEl.style.display = (dir === null || (dir === 0 && speed === 0)) ? 'none' : 'block';\n        }\n      }\n    } catch (e) { hideWidget(s); }\n  }\n\n  let refreshCount = 0;\n\n \/\/ REFRESH ALL FUNCTION\n  function refreshAll() {\n\n\/\/ just upd stations, Solunar data updates in background\n    stations.forEach(updateStationWidget); \n    \n    \/\/ MASTER SWITCH: If this is the second update (the 1-min mark), load Windguru\n    refreshCount++;\n    if (refreshCount === 2) {\n      injectWindguru();\n    }\n  }\n\n  \/\/ GLOBAL BRIDGE\n  window.refreshAll = refreshAll;\n\n  \/\/ --- STARTUP SEQUENCE ---\n  \/\/ Fetch Solunar Data (Async, load & keep cached)\n  fetchSolunarData();\n  \n  \/\/ Phase 1: Load Map & Airport Stations immediately\n  refreshAll(); \n\n\/\/ Phase 2: Set 1-minute interval that will eventually trigger Phase 3 (Windguru)\n  setInterval(refreshAll, REFRESH_MS);\n  \n\/\/ Refresh Solunar Data occasionally (every hour)\n  setInterval(fetchSolunarData, 60 * 60 * 1000);\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 lb = document.getElementById('airport-lightbox'), ifr = document.getElementById('airport-lightbox-iframe');\n\n  document.querySelectorAll('.weather-pin[data-station]').forEach(pin => {\n    pin.addEventListener('click', () => {\n      const s = stations.find(x => x.id === pin.getAttribute('data-station'));\n      if (s.isWindguru) {\n        const trig = pin.querySelector('.wg-proxy-layer div div');\n        if (trig) trig.click();\n      } else {\n        ifr.src = s.lightbox + '?nocache=' + Date.now(); lb.style.display = 'flex';\n      }\n    });\n  });\n  document.getElementById('airport-lightbox-close').addEventListener('click', () => { ifr.src = ''; lb.style.display = 'none'; });\n  lb.addEventListener('click', (e) => { if (e.target === lb) { ifr.src=''; lb.style.display='none'; } });\n});\n<\/script>\n\n\n\n<p class=\"has-text-align-center\"><sup>Bg grey at night and non flyable directions<\/sup><\/p>\n\n\n\n<p class=\"has-text-align-center\"><sup>3Ws maps <a href=\"https:\/\/www.windguru.cz\/map\/station\/?lat=32.73153146089865&amp;lon=-16.994618926545627&amp;zoom=9\" target=\"_blank\" rel=\"noopener nofollow\" title=\"\">Windguru<\/a> \/ <a href=\"https:\/\/www.wunderground.com\/wundermap?lat=32.70&amp;lon=-16.99&amp;zoom=10&amp;Units=english\" target=\"_blank\" rel=\"noopener nofollow\" title=\"\">Wunderground<\/a> \/ <a href=\"https:\/\/www.windy.com\/?32.725,-16.942,10,p:wind\" target=\"_blank\" rel=\"noopener nofollow\" title=\"\">Windy<\/a><\/sup><\/p>\n\n\n\n<p class=\"has-text-align-center\">Weather <a style=\"text-decoration:none\" href=\"https:\/\/paragliding-in-madeira.com\/weather\/golden-stream\/\" title=\"\">stations<\/a> and <a style=\"text-decoration:none\" href=\"https:\/\/paragliding-in-madeira.com\/weather\/webcams\/\">webcams<\/a> at the most frequent flying <a style=\"text-decoration:none\" href=\"https:\/\/paragliding-in-madeira.com\/weather\/all25\" rel=\"nofollow\">spots<\/a><\/p>\n\n\n\n<figure data-wp-context=\"{&quot;imageId&quot;:&quot;69e22ddc5316f&quot;}\" data-wp-interactive=\"core\/image\" data-wp-key=\"69e22ddc5316f\" class=\"wp-block-image aligncenter size-large is-resized wp-lightbox-container\"><img loading=\"lazy\" decoding=\"async\" width=\"1024\" height=\"1024\" 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\/2024\/11\/wind-rose-1024x1024.png\" alt=\"\" class=\"wp-image-1702\" style=\"width:93px;height:auto\" srcset=\"https:\/\/paragliding-in-madeira.com\/weather\/wp-content\/uploads\/2024\/11\/wind-rose-1024x1024.png 1024w, https:\/\/paragliding-in-madeira.com\/weather\/wp-content\/uploads\/2024\/11\/wind-rose-300x300.png 300w, https:\/\/paragliding-in-madeira.com\/weather\/wp-content\/uploads\/2024\/11\/wind-rose-150x150.png 150w, https:\/\/paragliding-in-madeira.com\/weather\/wp-content\/uploads\/2024\/11\/wind-rose-768x768.png 768w, https:\/\/paragliding-in-madeira.com\/weather\/wp-content\/uploads\/2024\/11\/wind-rose.png 1111w\" sizes=\"auto, (max-width: 1024px) 100vw, 1024px\" \/><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-center\"><\/p>\n\n\n\n<div style=\"display:block; overflow: hidden; width:100%; height:1088px;\">\n<iframe loading=\"lazy\" style=\"margin-top:-888px;margin-left:-36px\" height=\"1921px\" width=\"395px\" scrolling=\"no\" border=\"0\" src=\"https:\/\/paragliding-in-madeira.com\/weather\/canhas\/?iframe=true\"><\/iframe><\/div>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<div style=\"display:block; overflow: hidden; width:100%; height:1088px;\">\n<iframe loading=\"lazy\" style=\"margin-top:-888px;margin-left:-36px\" height=\"1921px\" width=\"395px\" scrolling=\"no\" border=\"0\" src=\"https:\/\/paragliding-in-madeira.com\/weather\/porto-da-Cruz\/?iframe=true\"><\/iframe><\/div>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<div style=\"display:block; overflow: hidden; width:100%; height:1111px;\">\n<iframe loading=\"lazy\" style=\"margin-top:-888px;margin-left:-36px\" height=\"2111px\" width=\"395px\" scrolling=\"no\" border=\"0\" src=\"https:\/\/paragliding-in-madeira.com\/weather\/portela\/?iframe=true\"><\/iframe><\/div>\n\n<!--- div width:100%; height:315px;\">\niframe style=\"margin-top:-980px;margin-left:-36px\" height=\"1511px\" width=\"400px\" --->\n\n\n\n<div style=\"height:2px\" 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<details class=\"wp-block-details center-details is-layout-flow wp-block-details-is-layout-flow\"><summary><strong> &nbsp;Relevant Notes <\/strong><\/summary>\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<p class=\"has-text-align-center\"><sup><em>Portela weather station is installed 100m away from the takeoff, 15m higher, in a location that often gets thermal passing through and the wind accelerates passing from North to South. <\/em><\/sup><\/p>\n\n\n\n<p class=\"has-text-align-center\"><sup><em>The actual wind intensity at the takeoff will almost always be slightly lower than the measured, to take into consideration.<\/em><\/sup><\/p>\n<\/div>\n<\/div>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<p class=\"has-text-align-center\"><sup><strong>Important note for pilots:<\/strong> <\/sup><br><sup><em>these tools are an amazing help to increase awareness, however, they are not meant to decide for you whether the conditions are good to fly or otherwise.<br> You should use your skills and knowledge to assess that<\/em><\/sup><\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<!-- Old Windguru colors speed limits\n<details>\n<summary>Windguru speed color \/ takeoff limit<\/summary>\n<table style=\"border-collapse: collapse; font-family: sans-serif; font-size: 16px; color: #333; width: 100%;\">\n<tr>\n<td style=\"padding:4px; width: 30px;\"><span style=\"display:inline-block;width:18px;height:18px;background-color:#FFFFFF;border:1px solid #ccc;\"><\/span><\/td>\n<td style=\"padding:4px;\">0 \u2013 9 km\/h<\/td>\n<\/tr>\n<tr>\n<td style=\"padding:4px;\"><span style=\"display:inline-block;width:18px;height:18px;background-color:#D5F7F8;border:1px solid #ccc;\"><\/span><\/td>\n<td style=\"padding:4px;\">10 \u2013 14 km\/h<\/td>\n<\/tr>\n<tr>\n<td style=\"padding:4px;\"><span style=\"display:inline-block;width:18px;height:18px;background-color:#80F0FA;border:1px solid #ccc;\"><\/span><\/td>\n<td style=\"padding:4px;\">15 \u2013 19 km\/h (mountain limit)<\/td>\n<\/tr>\n\n<tr>\n<td style=\"padding:4px;\"><span style=\"display:inline-block;width:18px;height:18px;background-color:#96ff00;border:1px solid #ccc;\"><\/span><\/td>\n<td style=\"padding:4px;\">20 \u2013 25 km\/h (above mountain limit)<\/td>\n<\/tr>\n<tr>\n<td style=\"padding:4px;\"><span style=\"display:inline-block;width:18px;height:18px;background-color:#00D800;border:1px solid #ccc;\"><\/span><\/td>\n<td style=\"padding:4px;\">26 \u2013 29 km\/h (sea level limit)<\/td>\n<\/tr>\n<tr>\n<td style=\"padding:4px;\"><span style=\"display:inline-block;width:18px;height:18px;background-color:#f0ff00;border:1px solid #ccc;\"><\/span><\/td>\n<td style=\"padding:4px; font-weight: bold; color: #000;\">30 \u2013 34 km\/h<\/td>\n<\/tr>\n<tr>\n<td style=\"padding:4px;\"><span style=\"display:inline-block;width:18px;height:18px;background-color:#FFFF00;border:1px solid #ccc;\"><\/span><\/td>\n<td style=\"padding:4px; font-weight: bold; color: #000;\">35 \u2013 37 km\/h (above sea level limit)<\/td>\n<\/tr>\n<tr>\n<td style=\"padding:4px;\"><span style=\"display:inline-block;width:18px;height:18px;background-color:#FFC800;border:1px solid #ccc;\"><\/span><\/td>\n<td style=\"padding:4px;\">38 \u2013 42 km\/h<\/td>\n<\/tr>\n<tr>\n<td style=\"padding:4px;\"><span style=\"display:inline-block;width:18px;height:18px;background-color:#FF9600;border:1px solid #ccc;\"><\/span><\/td>\n<td style=\"padding:4px;\">43 \u2013 48 km\/h<\/td>\n<\/tr>\n<tr>\n<td style=\"padding:4px;\"><span style=\"display:inline-block;width:18px;height:18px;background-color:#FF0000;border:1px solid #ccc;\"><\/span><\/td>\n<td style=\"padding:4px;\">49 \u2013 67 km\/h<\/td>\n<\/tr>\n<tr>\n<td style=\"padding:4px;\"><span style=\"display:inline-block;width:18px;height:18px;background-color:#C000FF;border:1px solid #ccc;\"><\/span><\/td>\n<td style=\"padding:4px;\">68+ km\/h<\/td>\n<\/tr>\n<\/table>\n<\/details>\n-->\n\n\n\n<div style=\"height:21px\" aria-hidden=\"true\" class=\"wp-block-spacer\"><\/div>\n\n\n\n<div style=\"margin: 0 auto; width: 300px;\">\n  <a href=\"https:\/\/metar-taf.com\/LPPS\" target=\"_blank\" id=\"metartaf-nmmNXCeW\" style=\"font-size:18px; font-weight:500; color:#000; width:300px; height:435px; display:block\" rel=\"noopener\">METAR Porto Santo Airport<\/a>\n  <script async defer crossorigin=\"anonymous\" src=\"https:\/\/metar-taf.com\/embed-js\/LPPS?u=60001&#038;speed=kph&#038;qnh=hPa&#038;rh=rh&#038;target=nmmNXCeW\"><\/script>\n<\/div>\n\n\n\n<div style=\"height:14px\" aria-hidden=\"true\" class=\"wp-block-spacer\"><\/div>\n\n\n\n    <div style=\"max-width: 600px; margin: auto;\">\n \u00a0 \u00a0 \u00a0<canvas id=\"windGraphSpeed-lpps\" height=\"250\"><\/canvas>\n      <canvas id=\"windGraphDirection-lpps\" 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 = 'lpps';\n        const JSON_URL = 'https:\/\/paragliding-in-madeira.com\/weather\/wp-content\/uploads\/metar-wind-lpps.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:11px\" aria-hidden=\"true\" class=\"wp-block-spacer\"><\/div>\n\n\n\n<!-- LPPS RAW METAR friendly Decoder Widget -->\n<div id=\"metarw-lpps\" style=\"font-family: Inter, system-ui, sans-serif; max-width: 555px; margin: auto;\">\n  <style>\n    #metarw-lpps * { \nbox-sizing: border-box; \nline-height: 1.4; \nfont-weight: 400; \n}\n    #metarw-lpps a { \ncolor: #1e40af; \ntext-decoration: \nunderline; \n}\n    #metarw-lpps p { \nmargin: 0.75rem 0; \ntext-align: justify; \n}\n    #metarw-lpps .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-lpps .header { \nmargin-bottom: 1rem; \n}\n    #metarw-lpps .title { \nfont-weight: 700; \ncolor: #1f2937; \nfont-size: 1.05rem; \ndisplay: flex; \nalign-items: center; \n}\n    #metarw-lpps .title span { \nmargin-left: 0.5rem; \nfont-size: 1rem; \ncolor: #4b5563; \nfont-weight: 500; \n}\n    #metarw-lpps #metarw-upd-lpps { \nfont-size: 0.85rem; \ncolor: #6b7280; \nmargin-top: 0.25rem; \n}\n    #metarw-lpps .section-label { \nfont-weight: 600; \nmargin-bottom: 0.2rem; \ndisplay: block; \ncolor: #1a1a1a; \nfont-size: 1.0rem; \nletter-spacing: 0.1em; \n}\n    #metarw-lpps #metarw-raw-lpps { \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-lpps #metarw-decoded-lpps { \nfont-size: 1rem; \nfont-weight: 400; \ncolor: #1f2937; \ndisplay:block; \nmargin-top:0.5rem; \n}\n\n#metarw-lpps strong {\n  font-weight: 600;\n}\n\n    #metarw-lpps #metarw-thermal-lpps { \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 LPPS <span>(Porto Santo &#8220;live&#8221;)<\/span><\/div>\n    <\/div>\n\n    <p>\n      <span class=\"section-label\">Official:<\/span>\n      <span id=\"metarw-raw-lpps\">Loading\u2026<\/span>\n    <\/p>\n\n    <p>\n      <span class=\"section-label\">Decoded:<\/span>\n      <span id=\"metarw-decoded-lpps\">Loading\u2026<\/span>\n      <span id=\"metarw-thermal-lpps\"><\/span>\n    <\/p>\n\n    <small id=\"metarw-upd-lpps\"><\/small>\n  <\/div>\n<\/div>\n\n<script>\n\/* --- INTEGRATED JAVASCRIPT: ROBUST PARSING FUNCTIONS & CLEAN OUTPUT --- *\/\n\n(async function() {\n  \/\/ --- CONFIG ---\n  const RAW_URL = \"https:\/\/paragliding-in-madeira.com\/weather\/wp-content\/uploads\/metar-full-data-lpps.json\";\n  const STATION_ELEVATION = 103; \/\/ LPPS station elevation in meters\n\n  \/\/ --- DOM ---\n  const rawEl = document.getElementById('metarw-raw-lpps');\n  const decodedEl = document.getElementById('metarw-decoded-lpps');\n  const thermalEl = document.getElementById('metarw-thermal-lpps');\n  const updEl = document.getElementById('metarw-upd-lpps');\n\n  \/\/ --- Helpers ---\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  \/\/ --- ROBUST WEATHER PARSING FUNCTION ---\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'; \/\/ Default to \"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      \/\/ Combine: [label] + [cloudType] + \" at \" + [height]\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('[lpps 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:8px\" aria-hidden=\"true\" class=\"wp-block-spacer\"><\/div>\n\n\n\n<!-- LPPS TAF Gold Decoder Widget -->\n<div id=\"tafw-lpps\" style=\"font-family: Inter, system-ui, sans-serif; max-width: 555px; margin: auto;\">\n  <style>\n\n    \/* Reset and general styling *\/\n    #tafw-lpps * { \nbox-sizing: border-box; \nline-height: 1.4; \nfont-weight: 400; \n}\n    #tafw-lpps a { \ncolor: #1e40af; \ntext-decoration: underline; \n}\n    #tafw-lpps p { \nmargin: 0.75rem 0; \ntext-align: justify; \n}\n\n    \/* Card Container *\/\n    #tafw-lpps .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-lpps .header { \nmargin-bottom: 1.25rem; \npadding-bottom: 0.5rem; \nborder-bottom: 1px solid #f3f4f6; \n}\n    #tafw-lpps .title { \nfont-weight: 700; \ncolor: #1f2937; \nfont-size: 1.05rem; \ndisplay: flex; \nalign-items: center; \n}\n    #tafw-lpps .title span { \nmargin-left: 0.5rem; \nfont-size: 1rem; \ncolor: #4b5563; \nfont-weight: 500; }\n    #tafw-lpps #tafw-issued-lpps { \nfont-size: 0.85rem; \ncolor: #6b7280; \nmargin-top: 0.25rem; \n}\n\n    \/* Section Labels *\/\n    #tafw-lpps .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-lpps #tafw-raw-lpps { \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-lpps #tafw-summary-lpps { \nfont-size: 1rem; \nfont-weight: 500; \ncolor: #1f2937; \n}\n\n    \/* Toggle Button *\/\n    #tafw-lpps 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-lpps button:hover { \nbackground: transparent; \n}\n\n    \/* Details Section *\/\n    #tafw-lpps #tafw-details-lpps { \nmargin-top: 1rem; \nborder-top: 1px solid #e5e7eb; \npadding-top: 1rem; \nfont-size: 0.95rem; \ncolor: #374151;\n}\n    #tafw-lpps #tafw-details-lpps p { \nmargin: 0.5rem 0; \nline-height: 1.6; \n}\n    #tafw-lpps #tafw-details-lpps 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 LPPS <span>(Porto Santo Forecast)<\/span><\/div>\n        <div id=\"tafw-issued-lpps\"><\/div>\n      <\/div>\n    <\/div>\n\n    <p>\n      <span class=\"section-label\">Official:<\/span>\n      <span id=\"tafw-raw-lpps\">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-lpps\">Loading\u2026<\/span>\n    <\/p>\n\n    <button id=\"tafw-toggle-lpps\" aria-expanded=\"false\" title=\"&#10133;\" style=\"display:none;\">&#10133;<\/button>\n    <div id=\"tafw-details-lpps\" 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-lpps.json\";\n\n  const rawEl = document.getElementById('tafw-raw-lpps');\n  const issuedEl = document.getElementById('tafw-issued-lpps');\n  const summaryEl = document.getElementById('tafw-summary-lpps');\n  const detailsEl = document.getElementById('tafw-details-lpps');\n  const toggleBtn = document.getElementById('tafw-toggle-lpps');\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 LPPs 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<details class=\"wp-block-details is-layout-flow wp-block-details-is-layout-flow\"><summary><\/summary>\n<p class=\"has-text-align-center\"><sup><a href=\"https:\/\/paragliding-in-madeira.com\/weather\/metar-history\/\" title=\"\">Madeira &amp; Porto Santo METAR and TAF<\/a><\/sup><\/p>\n<\/details>\n\n\n\n<div style=\"height:35px\" aria-hidden=\"true\" class=\"wp-block-spacer\"><\/div>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity is-style-default\"\/>\n\n\n\n<div style=\"height:6px\" aria-hidden=\"true\" class=\"wp-block-spacer\"><\/div>\n\n\n\n<figure class=\"wp-block-image size-large\"><a href=\"https:\/\/webhostmost.com\/#a_aid=rubengt\" target=\"_blank\" rel=\" noreferrer noopener\"><img loading=\"lazy\" decoding=\"async\" width=\"1024\" height=\"683\" src=\"https:\/\/paragliding-in-madeira.com\/weather\/wp-content\/uploads\/2025\/08\/WebHostMost-partner-banner-1024x683.png\" alt=\"\" class=\"wp-image-4467\" srcset=\"https:\/\/paragliding-in-madeira.com\/weather\/wp-content\/uploads\/2025\/08\/WebHostMost-partner-banner-1024x683.png 1024w, https:\/\/paragliding-in-madeira.com\/weather\/wp-content\/uploads\/2025\/08\/WebHostMost-partner-banner-300x200.png 300w, https:\/\/paragliding-in-madeira.com\/weather\/wp-content\/uploads\/2025\/08\/WebHostMost-partner-banner-768x512.png 768w, https:\/\/paragliding-in-madeira.com\/weather\/wp-content\/uploads\/2025\/08\/WebHostMost-partner-banner.png 1536w\" sizes=\"auto, (max-width: 1024px) 100vw, 1024px\" \/><\/a><\/figure>\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>Affiliate partnership at no extra cost to you because i love what they are doing<\/sup><\/p>\n<\/details>\n\n\n\n<div style=\"height:23px\" aria-hidden=\"true\" class=\"wp-block-spacer\"><\/div>\n<\/details>\n","protected":false},"excerpt":{"rendered":"<p>Canhas | Porto da Cruz | Portela WIND TEMP RAIN &#8212; km\/h &#8212; km\/h &#8212; km\/h VRB &#8212; km\/h Bg grey at night and non flyable directions 3Ws maps Windguru \/ Wunderground \/ Windy Weather stations and webcams at the most frequent flying spots<\/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":"Weather stations at Madeira Island","description":"Weather stations with live data at the most frequent Paragliding places on Madeira Island combined with live stream webcams"},"footnotes":""},"class_list":["post-11","page","type-page","status-publish","hentry"],"_links":{"self":[{"href":"https:\/\/paragliding-in-madeira.com\/weather\/wp-json\/wp\/v2\/pages\/11","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=11"}],"version-history":[{"count":437,"href":"https:\/\/paragliding-in-madeira.com\/weather\/wp-json\/wp\/v2\/pages\/11\/revisions"}],"predecessor-version":[{"id":10539,"href":"https:\/\/paragliding-in-madeira.com\/weather\/wp-json\/wp\/v2\/pages\/11\/revisions\/10539"}],"wp:attachment":[{"href":"https:\/\/paragliding-in-madeira.com\/weather\/wp-json\/wp\/v2\/media?parent=11"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}