{"id":46288,"date":"2025-09-04T10:36:09","date_gmt":"2025-09-04T08:36:09","guid":{"rendered":"https:\/\/cmm.imgw.pl\/?page_id=46288"},"modified":"2026-08-17T11:02:37","modified_gmt":"2026-08-17T09:02:37","slug":"prognozy-wiazkowe-imgw","status":"publish","type":"page","link":"https:\/\/cmm.imgw.pl\/?page_id=46288","title":{"rendered":"IMGW-PIB LMM: Prognozy wi\u0105zkowe"},"content":{"rendered":"\t\t<div data-elementor-type=\"wp-page\" data-elementor-id=\"46288\" class=\"elementor elementor-46288\">\n\t\t\t\t\t\t\t\t\t<section class=\"elementor-section elementor-top-section elementor-element elementor-element-ec94721 elementor-section-boxed elementor-section-height-default elementor-section-height-default\" data-id=\"ec94721\" data-element_type=\"section\">\n\t\t\t\t\t\t<div class=\"elementor-container elementor-column-gap-default\">\n\t\t\t\t\t<div class=\"elementor-column elementor-col-100 elementor-top-column elementor-element elementor-element-3669e89\" data-id=\"3669e89\" data-element_type=\"column\">\n\t\t\t<div class=\"elementor-widget-wrap elementor-element-populated\">\n\t\t\t\t\t\t\t\t<div class=\"elementor-element elementor-element-1af22bf elementor-widget elementor-widget-html\" data-id=\"1af22bf\" data-element_type=\"widget\" data-widget_type=\"html.default\">\n\t\t\t\t<div class=\"elementor-widget-container\">\n\t\t\t<div id=\"forecast-app\" class=\"forecast-app\">\n  <div class=\"layout-container\">\n    <!-- Lewa kolumna z przyciskami -->\n    <div class=\"left-column\">\n      <!-- Krok 1: Wyb\u00f3r modelu -->\n      <div class=\"section\">\n        <div class=\"model-group-title\">Model<\/div>\n        <div id=\"models\" class=\"buttons models-buttons\"><\/div>\n      <\/div>\n\n      <!-- Krok 2: Wyb\u00f3r terminu startu -->\n      <div class=\"section\">\n        <div class=\"model-group-title\" id=\"termTitle\">Termin startu<\/div>\n        <div id=\"terms\" class=\"buttons terms-buttons\"><\/div>\n      <\/div>\n\n      <!-- Krok 3: Wyb\u00f3r parametru -->\n      <div class=\"section\">\n        <div class=\"model-group-title\">Parametr<\/div>\n        <div id=\"params\" class=\"select-container\">\n          <select id=\"paramsSelect\"><\/select>\n        <\/div>\n      <\/div>\n\n      <!-- Krok 4: Wyb\u00f3r statystyki (dla standardowych modeli) -->\n      <div class=\"section\" id=\"statsSection\" style=\"display: none;\">\n        <div class=\"model-group-title\">Statystyka<\/div>\n        <div id=\"stats\" class=\"select-container\">\n          <select id=\"statsSelect\"><\/select>\n        <\/div>\n      <\/div>\n      \n      <!-- Krok 4b: Wyb\u00f3r regionu (dla modeli z podsumowania z regionami) -->\n      <div class=\"section\" id=\"regionSection\" style=\"display: none;\">\n        <div class=\"model-group-title\">Obszar<\/div>\n        <div id=\"regions\" class=\"buttons region-buttons\"><\/div>\n      <\/div>\n      \n      <!-- Krok 5: Wyb\u00f3r terminu prognozy -->\n      <div class=\"section\" id=\"hoursSection\">\n        <div class=\"model-group-title\" id=\"hoursTitle\">Godziny prognozy<\/div>\n        <div id=\"hours\" class=\"buttons hours-buttons\"><\/div>\n      <\/div>\n\n      <div class=\"section compare-section\" id=\"compareSection\">\n        <div class=\"compare-actions-row\">\n          <button id=\"animateBtn\" class=\"compare-action-btn\" type=\"button\" title=\"Animuj godziny prognozy\">\u25b6 Animuj<\/button>\n          <button id=\"compareBtn\" class=\"compare-action-btn\" type=\"button\">\u229e Por\u00f3wnaj modele<\/button>\n          <button id=\"compareListBtn\" class=\"compare-action-btn\" type=\"button\" style=\"display: none;\" title=\"Poka\u017c modele jeden pod drugim\">\u2630 Lista<\/button>\n          <button id=\"compareFullscreenBtn\" class=\"compare-action-btn\" type=\"button\" style=\"display: none;\">\u26f6 Pe\u0142ny ekran<\/button>\n        <\/div>\n      <\/div>\n      \n      <!-- Informacja o czasie -->\n      <div class=\"time-note\">\n        *Je\u015bli przy formacie czasu nie widnieje adnotacja UTC to znaczy, \u017ce obraz generowany jest na wskazany termin wg czasu urz\u0119dowego.\n      <\/div>\n      \n      <!-- Informacja o obs\u0142udze klawiatury -->\n      <div class=\"keyboard-info\">\n        <strong>\ud83d\udca1 Skr\u00f3ty klawiszowe:<\/strong><br>\n        1-6 Wyb\u00f3r modelu<br>\n        &lt; &gt; Zmiana terminu startu<br>\n        \u2190 \u2192 Zmiana godzin prognozy<br>\n        <!---\u2191 \u2193 Zmiana poziom\u00f3w ci\u015bnienia<br-->\n        Tab Rozwini\u0119cie parametr\u00f3w<br>\n        <!---Q Rozwini\u0119cie statystyk<br-->\n        A Animacja godzin prognozy<br>\n        C Por\u00f3wnanie modeli<br>\n        <span id=\"compareFullscreenShortcutRow\" style=\"display: none;\">F | Pe\u0142ny ekran por\u00f3wnania<br><\/span>\n        <span id=\"compareEscShortcutRow\" style=\"display: none;\">Esc | Wyj\u015bcie z pe\u0142nego ekranu<br><\/span>\n      <\/div>\n    <\/div>\n    \n    <!-- \u015arodkowa kolumna z suwakiem poziom\u00f3w ci\u015bnienia (widoczna tylko dla odpowiednich parametr\u00f3w) -->\n    <div class=\"slider-column\" id=\"pressureLevelSliderSection\" style=\"display: none;\">\n      <div class=\"section pressure-slider\">\n        <div class=\"model-group-title\">Poziom ci\u015bnienia<\/div>\n        <div class=\"pressure-display\">\n          <span id=\"pressureValue\">1000 hPa<\/span>\n        <\/div>\n        <!-- Slider - ukryty na urz\u0105dzeniach mobilnych -->\n        <div class=\"vertical-slider-container\" id=\"verticalSliderContainer\">\n          <input type=\"range\" id=\"pressureLevelSlider\" class=\"vertical-slider\" min=\"0\" max=\"5\" step=\"1\" value=\"0\" orient=\"vertical\">\n          <div class=\"pressure-labels\">\n            <span class=\"pressure-label\" style=\"bottom: 100%\">300 hPa<\/span>\n            <span class=\"pressure-label\" style=\"bottom: 80%\">500 hPa<\/span>\n            <span class=\"pressure-label\" style=\"bottom: 60%\">700 hPa<\/span>\n            <span class=\"pressure-label\" style=\"bottom: 40%\">850 hPa<\/span>\n            <span class=\"pressure-label\" style=\"bottom: 20%\">950 hPa<\/span>\n            <span class=\"pressure-label\" style=\"bottom: 0%\">1000 hPa<\/span>\n          <\/div>\n        <\/div>\n        <!-- Przyciski - widoczne tylko na urz\u0105dzeniach mobilnych -->\n        <div class=\"buttons\" id=\"pressureLevelButtons\" style=\"display: none;\"><\/div>\n      <\/div>\n    <\/div>\n    \n    <!-- Prawa kolumna z obrazkiem -->\n    <div class=\"right-column\">\n      <!--- Wy\u015bwietlanie obrazka -->\n      <div class=\"section images\">\n        <h3 id=\"imageTitle\" style=\"display: none;\"><\/h3>\n        \n        <!-- Dodatkowy poziomy slider do zmiany terminu prognozy (nad obrazkiem) -->\n        <div class=\"horizontal-slider-container\" id=\"horizontalSliderContainer\">\n          <input type=\"range\" id=\"forecastHourSlider\" class=\"horizontal-slider\" min=\"0\" max=\"0\" step=\"1\" value=\"0\" aria-label=\"Wybierz termin\">\n          <div class=\"hour-labels\" id=\"hourLabels\"><\/div>\n        <\/div>\n        \n        <div id=\"images\" class=\"images\"><\/div>\n        <div id=\"parameterDescription\" class=\"parameter-description\"><\/div>\n      <\/div>\n    <\/div>\n  <\/div>\n<\/div>\n\n<div id=\"wiazkoweCompareModal\" class=\"compare-modal\" style=\"display: none;\">\n  <div class=\"compare-modal-content compare-modal-wide\">\n    <div class=\"compare-modal-header\">\n      <div class=\"model-group-title\">Wybierz parametr i modele do por\u00f3wnania<\/div>\n      <button id=\"wiazkoweCompareClose\" class=\"compare-modal-close\" type=\"button\">\u2715<\/button>\n    <\/div>\n    <div class=\"compare-modal-body\">\n      <div class=\"compare-term-section\">\n        <div class=\"model-group-title\">Zestawienie<\/div>\n        <div id=\"wiazkoweCompareTerms\" class=\"buttons compare-term-buttons\"><\/div>\n      <\/div>\n      <div id=\"wiazkoweCompareSelectGrid\" class=\"compare-modal-select-grid\">\n        <div id=\"wiazkoweCompareParamSection\" class=\"compare-param-section\">\n          <div class=\"model-group-title\">Parametr do por\u00f3wnania<\/div>\n          <div id=\"wiazkoweCompareParams\" class=\"compare-params-list\"><\/div>\n        <\/div>\n        <div id=\"wiazkoweCompareModelSection\" class=\"compare-model-section\">\n          <div class=\"model-group-title\">Modele do por\u00f3wnania<\/div>\n          <div id=\"wiazkoweCompareList\" class=\"compare-models-list\"><\/div>\n        <\/div>\n      <\/div>\n    <\/div>\n    <div class=\"compare-modal-footer\">\n      <button id=\"wiazkoweCompareConfirm\" class=\"confirm-btn\" type=\"button\">Por\u00f3wnaj<\/button>\n    <\/div>\n  <\/div>\n<\/div>\n\n<div id=\"wiazkoweCompareFullscreen\" class=\"compare-fullscreen\">\n  <div id=\"wiazkoweCompareFullscreenGrid\" class=\"compare-fullscreen-grid\"><\/div>\n<\/div>\n\n<div class=\"lightbox\" id=\"wiazkoweLightbox\">\n  <span class=\"lightbox-close\" id=\"wiazkoweLightboxClose\">&times;<\/span>\n  <img id=\"wiazkoweLightboxImg\" src=\"\" alt=\"Powi\u0119kszony obraz prognozy wi\u0105zkowej\">\n<\/div>\n\n<style>\n  .forecast-app { \n    font-family: Arial, sans-serif;\n    max-width: 1200px;\n    margin: 0 auto;\n  }\n  \n  \/* Uk\u0142ad dwukolumnowy *\/\n  .layout-container {\n    display: flex;\n    flex-wrap: wrap;\n    gap: 10px;\n  }\n  \n  .left-column {\n    flex: 1;\n    min-width: 230px;\n    max-width: 230px;\n  }\n  \n  .time-note {\n    font-size: 10px;\n    color: #666;\n    margin-top: 15px;\n    padding: 0 10px;\n    line-height: 1.4;\n    font-style: italic;\n    text-align: justify;\n  }\n  \n  .keyboard-info {\n    font-size: 11px;\n    color: #555;\n    margin-top: 20px;\n    padding: 10px;\n    line-height: 1.6;\n    background-color: #f0f8ff;\n    border-left: 3px solid rgba(86, 221, 208, 1);\n    border-radius: 4px;\n  }\n  \n  .keyboard-info strong {\n    color: rgba(86, 221, 208, 1);\n    font-size: 12px;\n  }\n  \n  .slider-column {\n    flex: 0 0 auto;\n    width: 110px;\n    margin-top: 180px;\n    \n  }\n  \n  .right-column {\n    flex: 2;\n    min-width: 400px;\n  }\n  \n  \/* Style dla suwaka pionowego *\/\n  .pressure-slider {\n    height: 400px;\n    display: flex;\n    flex-direction: column;\n    align-items: flex-start;\n  }\n  \n  .vertical-slider-container {\n    height: 265px;\n    position: relative;\n    margin: 70px 0 10px 0;\n    padding-left: 0px;\n    display: flex; \n    justify-content: center;\n    align-items: center;\n  }\n  \n  .vertical-slider {\n    -webkit-appearance: none;\n    appearance: none;\n    width: 260px;\n    height: 10px;\n    border-radius: 5px;\n    background: #e0e0e0;\n    outline: none;\n    position: absolute;\n    left: -115px;\n    top: 50px;\n    transform: rotate(270deg);\n    margin: 0;\n  }\n  \n  .vertical-slider::-webkit-slider-thumb {\n    -webkit-appearance: none;\n    appearance: none;\n    width: 20px;\n    height: 20px;\n    border-radius: 50%;\n    background: rgba(86, 221, 208, 1);\n    cursor: pointer;\n  }\n  \n  .vertical-slider::-moz-range-thumb {\n    width: 20px;\n    height: 20px;\n    border-radius: 50%;\n    background: rgba(86, 221, 208, 1);\n    cursor: pointer;\n  }\n  \n  .pressure-display {\n    margin-top: 0px;\n    margin-bottom: 20px;\n    padding-left: 0px;\n    padding-right: 0px;\n    padding-top: 10px;\n    font-size: 1.4em;\n    font-weight: bold;\n    align-self: center;\n    color: rgba(86, 221, 208, 1);\n  }\n  \n  \/* Style dla etykiet poziomu ci\u015bnienia *\/\n  .pressure-labels {\n    position: absolute;\n    height: 240px;\n    right: -72px;\n    top: -60px;\n    width: 40px;\n    z-index: 10;\n  }\n  \n  .pressure-label {\n    position: absolute;\n    right: 0;\n    font-size: 0.65em;\n    color: #666;\n    white-space: nowrap;\n    margin-bottom: -5px;\n    width: 45px;\n    text-align: right;\n  }\n  \n  \/* Dodanie ma\u0142ych kropek przy etykietach *\/\n  .pressure-label:before {\n    content: \"\";\n    position: absolute;\n    left: -15px;\n    top: 50%;\n    width: 4px;\n    height: 4px;\n    background-color: #666;\n    border-radius: 50%;\n    transform: translateY(-50%);\n  }\n  \n  \/* Style dla poziomego slidera termin\u00f3w prognozy *\/\n  .horizontal-slider-container {\n    width: 100%;\n    margin: 0 0 20px 0;\n    padding: 10px;\n    background-color: #f5f5f5;\n    border-radius: 5px;\n    box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);\n  }\n  \n  .horizontal-slider {\n    -webkit-appearance: none;\n    appearance: none;\n    width: 100%;\n    height: 10px;\n    border-radius: 5px;\n    background: #dcdcdc;\n    outline: none;\n  }\n  \n  .horizontal-slider::-webkit-slider-thumb {\n    -webkit-appearance: none;\n    appearance: none;\n    width: 22px;\n    height: 22px;\n    border-radius: 50%;\n    background: rgba(86, 221, 208, 1);\n    cursor: pointer;\n    border: 2px solid #fff;\n  }\n  \n  .horizontal-slider::-moz-range-thumb {\n    width: 22px;\n    height: 22px;\n    border-radius: 50%;\n    background: rgba(86, 221, 208, 1);\n    cursor: pointer;\n    border: 2px solid #fff;\n  }\n\n  .compare-section {\n    text-align: center;\n    padding: 8px 10px;\n  }\n\n  .compare-actions-row {\n    display: flex;\n    gap: 6px;\n  }\n\n  .compare-actions-row.compare-actions-grid {\n    display: grid;\n    grid-template-columns: 1fr 1fr;\n    gap: 6px;\n  }\n\n  .compare-actions-row .compare-action-btn {\n    flex: 1;\n    min-width: 0;\n    font-size: 0.78em;\n    padding: 8px 6px;\n    margin-top: 0;\n    width: auto;\n  }\n\n  .compare-actions-row.compare-actions-grid .compare-action-btn {\n    width: 100%;\n  }\n\n  .hour-labels {\n    position: relative;\n    height: 20px;\n    margin-top: 6px;\n    color: #666;\n    font-size: 0.78em;\n    font-variant-numeric: tabular-nums;\n  }\n\n  .hour-label-marker {\n    position: absolute;\n    top: 0;\n    transform: translateX(-50%);\n    white-space: nowrap;\n  }\n\n  .hour-label-marker.first {\n    left: 0 !important;\n    transform: none;\n  }\n\n  .hour-label-marker.last {\n    transform: translateX(-100%);\n  }\n  \n  \n  .section { \n    margin-bottom: 12px; \/* Zmniejszony margines dolny *\/\n    background: #f9f9f9;\n    padding: 10px; \/* Zmniejszony padding ca\u0142ej sekcji *\/\n    border-radius: 8px;\n    box-shadow: 0 1px 3px rgba(0,0,0,0.1);\n  }\n  \n  .model-group-title {\n    width: 100%;\n    font-size: 0.78em;\n    font-weight: 700;\n    text-transform: uppercase;\n    letter-spacing: 0.04em;\n    color: #5d5d5d;\n    padding: 3px 4px;\n    margin-top: 0;\n    margin-bottom: 6px;\n    border-bottom: 1px dashed #c9c9c9;\n  }\n\n  .buttons.hours-buttons {\n    display: grid;\n    grid-template-columns: repeat(3, minmax(0, 1fr));\n    gap: 5px;\n    flex-wrap: unset;\n  }\n\n  .buttons.hours-buttons.hours-buttons-2col {\n    grid-template-columns: repeat(2, minmax(0, 1fr));\n  }\n\n  .buttons.hours-buttons button {\n    width: 100%;\n    min-width: 0;\n    text-align: center;\n    font-size: 0.78em;\n    padding: 4px 6px;\n    white-space: nowrap;\n  }\n\n  .buttons.hours-buttons.hours-buttons-2col button {\n    font-size: 0.72em;\n    padding: 4px 4px;\n    white-space: normal;\n    line-height: 1.25;\n  }\n\n  .buttons.hours-buttons .loading-hours {\n    grid-column: 1 \/ -1;\n    width: 100%;\n    box-sizing: border-box;\n  }\n  \n  .buttons { \n    display: flex;\n    flex-wrap: wrap;\n    gap: 5px; \/* Zmniejszony odst\u0119p *\/\n    margin-top: 3px; \/* Dodany ma\u0142y margines na g\u00f3rze *\/\n  }\n  \n  .buttons button {\n    padding: 4px 10px; \/* Zmniejszony padding *\/\n    border: 1px solid #666;\n    border-radius: 6px;\n    background: #f5f5f5;\n    cursor: pointer;\n    transition: all 0.2s;\n    font-size: 0.9em;\n  }\n  \n  .buttons button:hover { \n    background: #ddd; \n  }\n  \n  .section.images {\n    min-height: 600px;\n  }\n  \n  #images.images {\n    min-height: 600px;\n  }\n  \n  .parameter-description {\n    margin-top: 15px;\n    padding: 10px;\n    background-color: #f8f9fa;\n    border: 1px solid #dee2e6;\n    border-radius: 4px;\n    font-size: 14px;\n    color: #333;\n    line-height: 1.5;\n    text-align: justify;\n  }\n  \n  .images img {\n    max-width: 100%;\n    margin: 5px auto;\n    border: 1px solid #ccc;\n    display: block;\n    box-shadow: 0 2px 8px rgba(0,0,0,0.1);\n    cursor: zoom-in;\n  }\n\n  .compare-card img {\n    cursor: zoom-in;\n  }\n\n  .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.9);\n    z-index: 1300;\n    justify-content: center;\n    align-items: center;\n    cursor: pointer;\n  }\n\n  .lightbox.active {\n    display: flex;\n  }\n\n  .lightbox img {\n    max-width: 95%;\n    max-height: 95%;\n    object-fit: contain;\n    cursor: default;\n  }\n\n  .lightbox-close {\n    position: absolute;\n    top: 20px;\n    right: 30px;\n    color: #fff;\n    font-size: 40px;\n    cursor: pointer;\n    line-height: 1;\n  }\n  \n  \/* Kontener obraz\u00f3w *\/\n  .images {\n    position: relative;\n    width: 100%;\n  }\n  \n  .buttons button.active {\n    background-color: rgba(86, 221, 208, 1);\n    color: white;\n    border-color: #2E7D32;\n  }\n\n  .buttons.hours-buttons button.preload-pending:not(.active) {\n    background: #ececec;\n    border-color: #b8b8b8;\n    color: #7a7a7a;\n  }\n\n  .buttons.hours-buttons button.preload-loaded:not(.active) {\n    background: linear-gradient(135deg, #d8d8d8 0%, #c9c9c9 100%);\n    border-color: #b1b1b1;\n    color: #4f4f4f;\n  }\n\n  .buttons.hours-buttons button.preload-loading:not(.active) {\n    background-image: repeating-linear-gradient(\n      -45deg,\n      #e8e8e8 0,\n      #e8e8e8 8px,\n      #dfdfdf 8px,\n      #dfdfdf 16px\n    );\n  }\n\n  .hour-label-marker.preload-pending {\n    color: #8f8f8f;\n  }\n\n  .hour-label-marker.preload-loaded {\n    color: #7a7a7a;\n  }\n  \n  \/* Style dla przycisk\u00f3w modeli - ustawienie jeden pod drugim *\/\n  .models-buttons {\n    flex-direction: column;\n    align-items: center;\n    width: 100%;\n    gap: 3px; \/* Zmniejszony odst\u0119p mi\u0119dzy przyciskami *\/\n  }\n  \n  .models-buttons button {\n    width: 100%;\n    text-align: center;\n    margin: 1px 0; \/* Zmniejszony margines zewn\u0119trzny *\/\n    padding: 3px 12px; \/* Zmniejszony padding od g\u00f3ry i do\u0142u *\/\n  }\n  \n  .models-buttons button {\n    width: 100%;\n    text-align: center;\n    margin: 1px 0; \/* Zmniejszony margines zewn\u0119trzny *\/\n    padding: 3px 12px; \/* Zmniejszony padding od g\u00f3ry i do\u0142u *\/\n  }\n\n  .models-buttons button.compare-selected {\n    background-color: rgba(86, 221, 208, 1);\n    color: #fff;\n    border-color: #2e7d32;\n  }\n\n  .models-buttons button.compare-ineligible {\n    opacity: 0.45;\n    cursor: not-allowed;\n  }\n\n  .compare-action-btn {\n    display: block;\n    width: 100%;\n    border: 2px dashed rgba(86, 221, 208, 1);\n    border-radius: 8px;\n    background: linear-gradient(135deg, rgba(86, 221, 208, 0.12) 0%, rgba(86, 221, 208, 0.04) 100%);\n    color: #2a6f68;\n    font-size: 0.82em;\n    font-weight: 600;\n    cursor: pointer;\n    transition: all 0.2s;\n    line-height: 1.3;\n  }\n\n  .compare-action-btn:hover:not(:disabled) {\n    background: rgba(86, 221, 208, 0.2);\n    border-style: solid;\n  }\n\n  .compare-action-btn.active {\n    background: rgba(86, 221, 208, 1);\n    border-style: solid;\n    border-color: #2e7d32;\n    color: #fff;\n  }\n\n  .compare-action-btn:disabled {\n    opacity: 0.45;\n    cursor: not-allowed;\n  }\n\n  .compare-out-of-range-banner {\n    flex: 0 0 auto;\n    width: 100%;\n    margin: 0 0 10px;\n    padding: 8px 12px;\n    border-radius: 6px;\n    background: #fff6e8;\n    border: 1px solid #e6c48a;\n    color: #6a4b12;\n    font-size: 0.82em;\n    line-height: 1.35;\n    text-align: center;\n    box-sizing: border-box;\n  }\n\n  .compare-fullscreen .compare-out-of-range-banner {\n    margin: 0 0 8px;\n    background: rgba(255, 246, 232, 0.12);\n    border-color: rgba(230, 196, 138, 0.45);\n    color: #f0e0c0;\n  }\n\n  .compare-layout {\n    display: flex;\n    flex-direction: column;\n    width: 100%;\n    height: 100%;\n    min-height: 0;\n  }\n\n  .compare-grid-side {\n    display: grid;\n    grid-template-columns: repeat(2, minmax(0, 1fr));\n    gap: 12px;\n    align-items: stretch;\n  }\n\n  .compare-grid-side.compare-grid-list {\n    grid-template-columns: minmax(0, 1fr);\n  }\n\n  .compare-layout .compare-grid-side {\n    flex: 1 1 auto;\n    min-height: 0;\n  }\n\n\n  .compare-card {\n    background: #fff;\n    border: 1px solid #dcdcdc;\n    border-radius: 8px;\n    padding: 6px;\n    display: flex;\n    flex-direction: column;\n    min-height: 100%;\n  }\n\n  .compare-card-label {\n    text-align: center;\n    font-size: 0.85em;\n    font-weight: 600;\n    margin-bottom: 4px;\n    color: #333;\n    flex: 0 0 auto;\n  }\n\n  .compare-card img {\n    width: 100%;\n    height: auto;\n    display: block;\n    border: 1px solid #eee;\n    flex: 1 1 auto;\n  }\n\n  .compare-card-placeholder {\n    display: flex;\n    align-items: center;\n    justify-content: center;\n    flex: 1 1 auto;\n    min-height: 180px;\n    padding: 16px;\n    text-align: center;\n    color: #666;\n    font-size: 0.88em;\n    line-height: 1.4;\n    background: #f5f5f5;\n    border: 1px dashed #c9c9c9;\n    border-radius: 4px;\n  }\n\n  .compare-fullscreen {\n    display: none;\n    position: fixed;\n    inset: 0;\n    z-index: 1100;\n    background: rgba(0, 0, 0, 0.94);\n    padding: 12px;\n    box-sizing: border-box;\n  }\n\n  .compare-fullscreen.active {\n    display: flex;\n    align-items: stretch;\n    justify-content: center;\n  }\n\n  .compare-fullscreen-grid {\n    width: 100%;\n    height: 100%;\n    min-height: 0;\n    overflow: hidden;\n    display: flex;\n    flex-direction: column;\n  }\n\n  .compare-fullscreen-grid > .compare-layout {\n    flex: 1 1 auto;\n    min-height: 0;\n  }\n\n  .compare-fullscreen .compare-grid-side {\n    display: grid;\n    width: 100%;\n    height: 100%;\n    max-height: 100%;\n    gap: 10px;\n    grid-template-columns: repeat(2, minmax(0, 1fr));\n    grid-auto-rows: minmax(0, 1fr);\n    align-content: stretch;\n    overflow: hidden;\n  }\n\n  .compare-fullscreen .compare-card {\n    background: transparent;\n    border: none;\n    padding: 0;\n    display: flex;\n    flex-direction: column;\n    min-height: 0;\n    height: 100%;\n  }\n\n  .compare-fullscreen .compare-card-label {\n    color: #eee;\n    flex: 0 0 auto;\n  }\n\n  .compare-fullscreen .compare-card img {\n    flex: 1 1 auto;\n    min-height: 0;\n    width: 100%;\n    height: 100%;\n    object-fit: contain;\n    border: none;\n  }\n\n  .compare-fullscreen .compare-card-placeholder {\n    flex: 1 1 auto;\n    width: 100%;\n    min-height: 0;\n    height: 100%;\n    margin: 0;\n    color: #e8e8e8;\n    font-size: 1rem;\n    background: rgba(255, 255, 255, 0.06);\n    border-color: rgba(255, 255, 255, 0.22);\n  }\n\n  .compare-modal {\n    position: fixed;\n    inset: 0;\n    background: rgba(0, 0, 0, 0.45);\n    z-index: 1200;\n    display: flex;\n    justify-content: center;\n    align-items: center;\n    padding: 16px;\n  }\n\n  .compare-modal-content {\n    width: min(520px, 95vw);\n    background: #fff;\n    border-radius: 10px;\n    box-shadow: 0 8px 28px rgba(0, 0, 0, 0.25);\n    overflow: hidden;\n  }\n\n  .compare-modal-content.compare-modal-wide {\n    width: min(720px, 96vw);\n  }\n\n  .compare-modal-header {\n    display: flex;\n    justify-content: space-between;\n    align-items: center;\n    padding: 12px 14px;\n    border-bottom: 1px solid #e1e1e1;\n    gap: 10px;\n  }\n\n  .compare-modal-header .model-group-title {\n    border-bottom: none;\n    margin-bottom: 0;\n    padding-bottom: 0;\n  }\n\n  .compare-modal-close {\n    border: 1px solid #aaa;\n    background: #fff;\n    border-radius: 6px;\n    width: 32px;\n    height: 32px;\n    cursor: pointer;\n  }\n\n  .compare-modal-body {\n    padding: 12px 14px;\n    max-height: 62vh;\n    overflow: auto;\n  }\n\n  .compare-term-section {\n    margin-bottom: 14px;\n    text-align: center;\n  }\n\n  .compare-term-section .model-group-title {\n    margin-bottom: 6px;\n    border-bottom: none;\n  }\n\n  .compare-term-buttons {\n    justify-content: center;\n    width: 100%;\n  }\n\n  .compare-term-buttons button {\n    min-width: 64px;\n  }\n\n  .compare-modal-select-grid {\n    display: grid;\n    grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);\n    gap: 12px;\n    align-items: start;\n  }\n\n  .compare-param-section,\n  .compare-model-section {\n    min-width: 0;\n    text-align: center;\n  }\n\n  .compare-param-section .model-group-title,\n  .compare-model-section .model-group-title {\n    text-align: center;\n  }\n\n  .compare-models-list,\n  .compare-params-list {\n    display: flex;\n    flex-direction: column;\n    gap: 10px;\n    align-items: stretch;\n  }\n\n  .compare-model-item,\n  .compare-param-item {\n    padding: 12px;\n    border: 2px solid #ddd;\n    border-radius: 8px;\n    cursor: pointer;\n    transition: all 0.2s;\n    background: #f9f9f9;\n    text-align: center;\n  }\n\n  .compare-model-item:hover,\n  .compare-param-item:hover {\n    border-color: rgba(86, 221, 208, 1);\n    background: rgba(86, 221, 208, 0.08);\n  }\n\n  .compare-model-item.selected,\n  .compare-param-item.selected {\n    border-color: #2e7d32;\n    background: rgba(86, 221, 208, 0.18);\n    font-weight: bold;\n  }\n\n  .compare-model-item.disabled {\n    opacity: 0.45;\n    cursor: not-allowed;\n  }\n\n  .compare-modal-footer {\n    padding: 12px 14px;\n    border-top: 1px solid #e1e1e1;\n    text-align: center;\n  }\n\n  @media (max-width: 640px) {\n    .compare-modal-select-grid {\n      grid-template-columns: 1fr;\n    }\n  }\n\n  .confirm-btn {\n    padding: 10px 18px;\n    border: none;\n    border-radius: 8px;\n    background: rgba(86, 221, 208, 1);\n    color: #fff;\n    font-weight: 600;\n    cursor: pointer;\n  }\n\n  .confirm-btn:disabled {\n    opacity: 0.45;\n    cursor: not-allowed;\n  }\n  \n  \/* Style dla przycisk\u00f3w termin\u00f3w - wype\u0142nianie ca\u0142ej przestrzeni w wierszu *\/\n  .terms-buttons {\n    width: 100%;\n    justify-content: space-between;\n  }\n  \n  .terms-buttons button {\n    flex-grow: 1;\n    text-align: center;\n    margin: 1px 0; \/* Zmniejszony margines zewn\u0119trzny *\/\n  }\n  \n  \/* Style dla przycisk\u00f3w region\u00f3w - kolumna pionowa *\/\n  .region-buttons {\n    display: flex;\n    flex-direction: column;\n    gap: 4px;\n    width: 100%;\n  }\n  \n  .region-buttons button {\n    width: 100%;\n    text-align: center;\n    padding: 6px 12px;\n    font-size: 0.85em;\n  }\n  \n  \/* Style dla list rozwijanych *\/\n  .select-container {\n    width: 100%;\n    margin-bottom: 8px;\n    position: relative;\n  }\n  \n  .select-container select {\n    width: 100%;\n    padding: 8px 12px;\n    border: 1px solid #ccc;\n    border-radius: 6px;\n    background-color: #f5f5f5;\n    font-size: 0.9em;\n    min-height: 36px; \/* Minimalna wysoko\u015b\u0107 pola select *\/\n    height: auto; \/* Wysoko\u015b\u0107 dostosowuje si\u0119 do zawarto\u015bci *\/\n    white-space: normal; \/* Tekst mo\u017ce zawija\u0107 si\u0119 do nast\u0119pnej linii *\/\n    overflow: visible; \/* Tekst nie zostanie uci\u0119ty *\/\n    appearance: none;\n    -webkit-appearance: none;\n    -moz-appearance: none;\n    background-image: url(\"data:image\/svg+xml;utf8,<svg fill='black' height='24' viewBox='0 0 24 24' width='24' xmlns='http:\/\/www.w3.org\/2000\/svg'><path d='M7 10l5 5 5-5z'\/><path d='M0 0h24v24H0z' fill='none'\/><\/svg>\");\n    background-repeat: no-repeat;\n    background-position: right 8px top 50%;\n  }\n  \n  .select-container select option {\n    padding: 8px 12px; \/* Dodatkowe wype\u0142nienie dla opcji *\/\n    min-height: 28px; \/* Minimalna wysoko\u015b\u0107 opcji *\/\n    line-height: 1.4; \/* Zwi\u0119kszony odst\u0119p mi\u0119dzy wierszami *\/\n    white-space: normal; \/* Tekst mo\u017ce zawija\u0107 si\u0119 do nast\u0119pnej linii *\/\n  }\n  \n  \/* Style dla niestandardowego selecta *\/\n  .custom-select-wrapper {\n    position: relative;\n    width: 100%;\n  }\n  \n  .custom-select {\n    width: 100%;\n    padding: 8px 12px;\n    border: 1px solid #ccc;\n    border-radius: 6px;\n    background-color: #f5f5f5;\n    font-size: 0.9em;\n    min-height: 36px;\n    cursor: pointer;\n    display: flex;\n    align-items: center;\n    justify-content: space-between;\n  }\n  \n  .custom-select:after {\n    content: '';\n    width: 0;\n    height: 0;\n    border-left: 5px solid transparent;\n    border-right: 5px solid transparent;\n    border-top: 5px solid #333;\n    margin-left: 10px;\n  }\n  \n  .custom-options {\n    position: absolute;\n    top: 100%;\n    left: 0;\n    right: 0;\n    background: white;\n    border: 1px solid #ccc;\n    border-radius: 0 0 6px 6px;\n    box-shadow: 0 2px 8px rgba(0,0,0,0.2);\n    max-height: 300px;\n    overflow-y: auto;\n    z-index: 100;\n    display: none;\n  }\n  \n  .custom-option {\n    padding: 8px 12px;\n    cursor: pointer;\n  }\n  \n  .custom-option:hover {\n    background-color: #f0f0f0;\n  }\n  \n  .custom-option.pressure-level {\n    color: rgba(86, 221, 208, 1) !important;\n    font-weight: bold;\n  }\n  \n  .select-container select:focus {\n    outline: none;\n    border-color: rgba(86, 221, 208, 1);\n    box-shadow: 0 0 5px rgba(76, 175, 80, 0.3);\n    height: auto; \/* Automatyczna wysoko\u015b\u0107 przy rozwini\u0119ciu *\/\n  }\n  \n  \/* Animacja \u0142adowania *\/\n  .loader-container {\n    display: flex;\n    justify-content: center;\n    align-items: center;\n    flex-direction: column;\n    width: 100%;\n    height: 300px;\n    z-index: 100;\n    position: relative;\n    background-color: rgba(245, 245, 245, 0.7); \/* Lekkie t\u0142o dla lepszej widoczno\u015bci *\/\n  }\n  \n  .loader {\n    border: 12px solid #f3f3f3;\n    border-radius: 50%;\n    border-top: 12px solid rgba(86, 221, 208, 1);\n    width: 100px;\n    height: 100px;\n    animation: spin 0.8s linear infinite;\n    z-index: 101;\n    box-shadow: 0px 0px 20px rgba(86, 221, 208, 0.7);\n  }\n  \n  @keyframes spin {\n    0% { transform: rotate(0deg); }\n    100% { transform: rotate(360deg); }\n  }\n  \n  \/* Responsywno\u015b\u0107 na ma\u0142ych ekranach *\/\n  @media (max-width: 768px) {\n    .layout-container {\n      flex-direction: column;\n    }\n    \n    .left-column, .right-column, .slider-column {\n      max-width: 100%;\n      width: 100%;\n    }\n    \n    .slider-column {\n      width: auto;\n      flex: 0 0 auto;\n      margin-top: 20px;\n    }\n    \n    .pressure-slider {\n      height: auto;\n      width: fit-content;\n      margin: 0 auto;\n    }\n    \n    #pressureLevelButtons {\n      display: flex !important;\n      flex-wrap: wrap;\n      gap: 10px;\n      justify-content: center;\n      width: fit-content;\n      margin: 4px auto;\n    }\n    \n    #pressureLevelButtons button {\n      min-width: 70px;\n    }\n    \n    .vertical-slider-container {\n      display: none !important;\n    }\n    \n    \/* Ukryj skr\u00f3ty klawiszowe na urz\u0105dzeniach mobilnych *\/\n    .keyboard-info {\n      display: none;\n    }\n  }\n<\/style>\n\n<script>\nconst models = [\"a-laef\", \"cosmo_ens\", \"icon_ens\", \"ecmwf_ifs_ens\", \"ecmwf_aifs_ens\", \"gefs\"];\n\/\/ const models = [\"EPS28km\", \"EPS70km\", \"gfs_lagged\", \"icon_lagged\", \"a-laef\", \"cosmo_ens\", \"icon_ens\", \"ecmwf_ifs_ens\", \"ecmwf_aifs_ens\"];\nconst termsDefault = [\"00\", \"06\", \"12\", \"18\"];\nconst alaefVariants = [\"1h\", \"12h\"];\nconst summaryTermVariants = {\n  \"a-laef\": [\"1h\", \"12h\"],\n  \"cosmo_ens\": [\"1h\", \"12h\"],\n  \"icon_ens\": [\"1h\", \"12h\"],\n  \"ecmwf_ifs_ens\": [\"3h\", \"12h\"],\n  \"ecmwf_aifs_ens\": [\"12h\"],\n  \"gefs\": [\"12h\"]\n};\n\n\/\/ parametry dla poszczeg\u00f3lnych modeli\nconst paramsForModels = {\n  \/\/ Dla a-laef dost\u0119pne s\u0105 r\u00f3\u017cne parametry zale\u017cnie od wariantu (1h lub 12h)\n  \"a-laef\": {\n    \"1h\": [\"tp\", \"t2m\", \"gst\"],\n    \"12h\": [\"t0m_min\", \"t2m_max\", \"t2m_min\", \"ws_max\", \"gst_max\", \"tp\", \"24h_tp\", \"sf\"]\n  },\n  \/\/ Dla cosmo_ens\n  \"cosmo_ens\": {\n    \"1h\": [\"cin_mu\", \"cin_ml\", \"cape_ml\", \"cape_mu\"],\n    \"12h\": [\"t2m_max\", \"t2m_min\", \"ws_max\", \"gst_max\", \"tp\", \"24h_tp\", \"sf\", \"cin_mu_max\", \"cin_ml_max\", \"cape_ml_max\", \"cape_mu_max\"] \/\/\"t0m_min\", \n  },\n  \/\/ Dla icon_ens\n  \"icon_ens\": {\n    \"1h\": [\"cape_ml\"],\n    \"12h\": [\"t2m_max\", \"t2m_min\", \"ws_max\", \"gst_max\", \"tp\", \"24h_tp\", \"sf\", \"cape_ml_max\"]\n  },\n  \/\/ Dla ecmwf_ifs_ens\n  \"ecmwf_ifs_ens\": {\n     \"3h\": [\"vis_3h\", \"ptype3h\", \"cape_mu_3h\"],\n    \"12h\": [\"t0m_min\", \"t2m_max\", \"t2m_min\", \"ws_max\", \"gst_max\", \"ptype\", \"tp\", \"24_tp\", \"sf\", \"sd\", \"vis_min\", \"cape_mu_max\"]\n  },\n  \/\/ Dla ecmwf_aifs_ens\n  \"ecmwf_aifs_ens\": {\n    \"12h\": [\"t2m_max\", \"t2m_min\", \"ws_max\", \"tp\", \"24_tp\", \"sf\"]\n  },\n  \/\/ Dla gefs\n  \"gefs\": {\n    \"12h\": [\"t2m_max\", \"t2m_min\", \"ws_max\", \"gst_max\", \"tp\", \"24h_tp\", \"cape_mu_max\"]\n  },\n  \"EPS28km\": [\"3CAPE\", \"CLCT\", \"CXCL\", \"GUST\", \"HIN\", \"IFS\", \"LCL_ML\", \"LFC_ML\", \"LPI\", \n             \"MCAPE\", \"NCIN\", \"PMSL\", \"PS\", \"Q1001\", \"Q3001\", \"Q6001\", \"Q8001\", \"RAIN\", \n             \"RELHUM\", \"SDI_1\", \"SDI_2\", \"SLI\", \"SNOW\", \"SWI\", \"SWISS00\", \"SWISS12\", \n             \"T2M\", \"TD2M\", \"TOT_PREC\", \"T_SURF\", \"U10M\", \"UCAPE\", \"VCIN\", \"VRANGE\",\n             \/\/ Parametry z poziomami: tylko mean\n             \"A\", \"A0300\", \"A0500\", \"A0700\", \"A0850\", \"A0950\", \"A1000\",\n             \"B\", \"B0300\", \"B0500\", \"B0700\", \"B0850\", \"B0950\", \"B1000\",\n             \"D\", \"D0300\", \"D0500\", \"D0700\", \"D0850\", \"D0950\", \"D1000\",\n             \"E\", \"E0300\", \"E0500\", \"E0700\", \"E0850\", \"E0950\", \"E1000\",\n             \"F\", \"F0300\", \"F0500\", \"F0700\", \"F0850\", \"F0950\", \"F1000\",\n             \"G\", \"G0300\", \"G0500\", \"G0700\", \"G0850\", \"G0950\", \"G1000\",\n             \"K\", \"K0300\", \"K0500\", \"K0700\", \"K0850\", \"K0950\", \"K1000\",\n             \"N\", \"N0300\", \"N0500\", \"N0700\", \"N0850\", \"N0950\", \"N1000\",\n             \"R\", \"R0300\", \"R0500\", \"R0700\", \"R0850\", \"R0950\", \"R1000\",\n             \"T\", \"T0300\", \"T0500\", \"T0700\", \"T0850\", \"T0950\", \"T1000\",\n             \"U\", \"U0300\", \"U0500\", \"U0700\", \"U0850\", \"U0950\", \"U1000\",\n             \"W\", \"W0300\", \"W0500\", \"W0700\", \"W0850\", \"W0950\", \"W1000\",\n             \"Y\", \"Y0300\", \"Y0500\", \"Y0700\", \"Y0850\", \"Y0950\", \"Y1000\",\n             \"Z\", \"Z0300\", \"Z0500\", \"Z0700\", \"Z0850\", \"Z0950\", \"Z1000\"],\n  \"EPS70km\": [\"A\", \"A0300\", \"A0500\", \"A0700\", \"A0850\", \"A0950\", \"A1000\",\n             \"B\", \"B0300\", \"B0500\", \"B0700\", \"B0850\", \"B0950\", \"B1000\",\n             \"CLCT\", \"CXCL\", \n             \"D\", \"D0300\", \"D0500\", \"D0700\", \"D0850\", \"D0950\", \"D1000\",\n             \"E\", \"E0300\", \"E0500\", \"E0700\", \"E0850\", \"E0950\", \"E1000\",\n             \"F\", \"F0300\", \"F0500\", \"F0700\", \"F0850\", \"F0950\", \"F1000\",\n             \"G\", \"G0300\", \"G0500\", \"G0700\", \"G0850\", \"G0950\", \"G1000\",\n             \"GUST\", \"HIN\", \"IFS\",\n             \"K\", \"K0300\", \"K0500\", \"K0700\", \"K0850\", \"K0950\", \"K1000\",\n             \"LPI\", \n             \"N\", \"N0300\", \"N0500\", \"N0700\", \"N0850\", \"N0950\", \"N1000\",\n             \"PMSL\", \"PS\", \n             \"Q\",\n             \"R\", \"R0300\", \"R0500\", \"R0700\", \"R0850\", \"R0950\", \"R1000\", \"RELHUM\",\n             \"T\", \"T0300\", \"T0500\", \"T0700\", \"T0850\", \"T0950\", \"T1000\", \"T2M\", \"TD2M\", \"TOT_PREC\", \"T_SURF\",\n             \"U\", \"U0300\", \"U0500\", \"U0700\", \"U0850\", \"U0950\", \"U1000\", \"U10M\",\n             \"VRANGE\",\n             \"W\", \"W0300\", \"W0500\", \"W0700\", \"W0850\", \"W0950\", \"W1000\",\n             \"Y\", \"Y0300\", \"Y0500\", \"Y0700\", \"Y0850\", \"Y0950\", \"Y1000\",\n             \"Z\", \"Z0300\", \"Z0500\", \"Z0700\", \"Z0850\", \"Z0950\", \"Z1000\"],\n  \"gfs_lagged\": [\"A\", \"A0300\", \"A0500\", \"A0700\", \"A0850\", \"A0950\", \"A1000\",\n             \"B\", \"B0300\", \"B0500\", \"B0700\", \"B0850\", \"B0950\", \"B1000\",\n             \"CLCT\", \"CXCL\", \n             \"D\", \"D0300\", \"D0500\", \"D0700\", \"D0850\", \"D0950\", \"D1000\",\n             \"E\", \"E0300\", \"E0500\", \"E0700\", \"E0850\", \"E0950\", \"E1000\",\n             \"F\", \"F0300\", \"F0500\", \"F0700\", \"F0850\", \"F0950\", \"F1000\",\n             \"G\", \"G0300\", \"G0500\", \"G0700\", \"G0850\", \"G0950\", \"G1000\",\n             \"GUST\", \"HIN\", \"IFS\",\n             \"K\", \"K0300\", \"K0500\", \"K0700\", \"K0850\", \"K0950\", \"K1000\",\n             \"LPI\", \n             \"N\", \"N0300\", \"N0500\", \"N0700\", \"N0850\", \"N0950\", \"N1000\",\n             \"PMSL\", \"PS\", \n             \"Q1001\", \"Q3001\", \"Q6001\", \"Q8001\", \n             \"R\", \"R0300\", \"R0500\", \"R0700\", \"R0850\", \"R0950\", \"R1000\", \"RELHUM\",\n             \"T\", \"T0300\", \"T0500\", \"T0700\", \"T0850\", \"T0950\", \"T1000\", \"T2M\", \"TD2M\", \"TOT_PREC\", \"T_SURF\",\n             \"U\", \"U0300\", \"U0500\", \"U0700\", \"U0850\", \"U0950\", \"U1000\", \"U10M\",\n             \"VRANGE\",\n             \"W\", \"W0300\", \"W0500\", \"W0700\", \"W0850\", \"W0950\", \"W1000\",\n             \"Y\", \"Y0300\", \"Y0500\", \"Y0700\", \"Y0850\", \"Y0950\", \"Y1000\",\n             \"Z\", \"Z0300\", \"Z0500\", \"Z0700\", \"Z0850\", \"Z0950\", \"Z1000\"],\n  \"icon_lagged\": [\"CAPE_CON\", \"CIN_ML\", \"CLCL\", \"CLCT\", \"PMSL\", \"PS\", \n              \"RELHUM\", \"T2M\", \"TD2M\", \"T_G\", \"TMAX_2M\", \"TMIN_2M\", \n              \"TOT_PREC\", \"U10M\", \"VMAX_10M\"]\n};\n\n\/\/ Konfiguracja region\u00f3w dla modeli z podsumowania\n\/\/ Parametry z tej listy maj\u0105 dodatkowe warianty regionalne (Sudety, Tatry, Bieszczady)\nconst regionsConfig = {\n  \"a-laef\": {\n    \/\/ Parametry kt\u00f3re maj\u0105 warianty regionalne w modelu a-laef\n    \"12h\": [\"sf\", \"gst_max\"]\n  },\n  \"cosmo_ens\": {\n    \"12h\": [\"sf\", \"gst_max\"]\n  }\n};\n\n\/\/ Definicja region\u00f3w\nconst regions = [\n  { id: null, name: \"Polska\" },\n  { id: \"sudety\", name: \"Sudety\" },\n  { id: \"tatry\", name: \"Tatry i Beskidy Zachodnie\" },\n  { id: \"bieszczady\", name: \"Bieszczady i Beskidy \u015arodkowe\" }\n];\n\n\n\/\/ Konfiguracja termin\u00f3w prognozy dla r\u00f3\u017cnych modeli\nconst forecastHours = {\n  \"a-laef\": {\n    \"1h\": Array.from({length: 61}, (_, i) => i + 6), \/\/ od 6 do 66\n    \"12h\": Array.from({length: 5}, (_, i) => i + 1)  \/\/ od 1 do 5\n  },\n  \"cosmo_ens\": {\n    \"1h\": Array.from({length: 36}, (_, i) => i + 6), \/\/ od 1 do 36\n    \"12h\": Array.from({length: 3}, (_, i) => i + 1)  \/\/ od 1 do 3\n  },\n  \"icon_ens\": {\n    \"1h\": Array.from({length: 44}, (_, i) => i + 5), \/\/ od 5 do 48\n    \"12h\": Array.from({length: 16}, (_, i) => i + 1)  \/\/ od 1 do 16\n  },\n  \"ecmwf_ifs_ens\": {\n    \"3h\": Array.from({length: 22}, (_, i) => i * 3 + 9).filter(h => h <= 72), \/\/ co 3h od 9 do 72\n    \"12h\": Array.from({length: 21}, (_, i) => i + 1)  \/\/ od 1 do 21\n  },\n  \"ecmwf_aifs_ens\": {\n    \"12h\": Array.from({length: 21}, (_, i) => i + 1)  \/\/ od 1 do 21\n  },\n  \"gefs\": {\n    \"12h\": Array.from({length: 21}, (_, i) => i + 1)  \/\/ od 1 do 21\n  },\n  \"default\": Array.from({length: 13}, (_, i) => i * 3).filter(h => h <= 36) \/\/ co 3h od 00 do 36\n};\n\n\/\/ Obiekt do przechowywania dynamicznie okre\u015blonych godzin dla parametr\u00f3w\nconst paramHours = {\n  \"cosmo_ens\": {},\n  \"icon_ens\": {},\n  \"ecmwf_ifs_ens\": {},\n  \"ecmwf_aifs_ens\": {},\n  \"gefs\": {},\n  \"a-laef\": {}\n};\n\n\/\/ Cache znalezionych sufiks\u00f3w dat dla model\/termin\/obszar (przyspiesza zmian\u0119 parametru)\nconst termDateSuffixCache = {};\n\n\/\/ Mapowanie przyjaznych nazw modeli dla interfejsu u\u017cytkownika\nconst modelDisplayNames = {\n  \"a-laef\": \"A-LAEF (podsumowanie)\",\n  \"EPS28km\": \"COSMO2k8\",\n  \"EPS70km\": \"COSMO7k0\",\n  \"gfs_lagged\": \"GFS\",\n  \"icon_lagged\": \"ICON-EU\",\n  \"cosmo_ens\": \"COSMO (podsumowanie)\",\n  \"icon_ens\": \"ICON-EU (podsumowanie)\",\n  \"ecmwf_ifs_ens\": \"ECMWF IFS (podsumowanie)\",\n  \"ecmwf_aifs_ens\": \"ECMWF AIFS (podsumowanie)\",\n  \"gefs\": \"GEFS (podsumowanie)\"\n};\n\n\/\/ Mapowanie przyjaznych nazw parametr\u00f3w dla interfejsu u\u017cytkownika\nconst parameterDisplayNames = {\n  \/\/ a-laef, cosmo_ens parametry\n  \"tp\": \"Opad ca\u0142kowity\",\n  \"t2m\": \"Temperatura 2m npg\",\n  \"gst\": \"Poryw wiatru\",\n  \"gst_max\": \"Maks. poryw wiatru\",\n  \"t0m_min\": \"Min. temperatura przy powierzchni\",\n  \"t2m_max\": \"Maks. temperatura 2m npg\",\n  \"t2m_min\": \"Min. temperatura 2m npg\",\n  \"ws_max\": \"Maks. pr\u0119dko\u015b\u0107 wiatru\",\n  \"vis_min\": \"Min. widzialno\u015b\u0107\",\n  \"vis_3h\": \"Widzialno\u015b\u0107\",\n  \"ptype\": \"Rodzaj opadu\",\n  \"ptype3h\": \"Rodzaj opadu\",\n  \"sf\": \"Opad \u015bniegu\",\n  \"sd\": \"Zmiana pokrywy \u015bnie\u017cnej\",\n  \"24_tp\": \"Suma opadu 24h\",\n  \"24h_tp\": \"Suma opadu 24h\",\n  \"cin_mu\": \"CIN MU\",\n  \"cin_ml\": \"CIN ML\",\n  \"cape_ml\": \"CAPE ML\",\n  \"cape_mu\": \"CAPE MU\",\n  \"cape_mu_3h\": \"CAPE MU\",\n  \"cin_mu_max\": \"Maks. CIN MU\",\n  \"cin_ml_max\": \"Maks. CIN ML\",\n  \"cape_ml_max\": \"Maks. CAPE ML\",\n  \"cape_mu_max\": \"Maks. CAPE MU\",\n\n  \/\/ EPS28km, EPS70km, gfs_lagged parametry\n  \"RAIN\": \"Opad deszczu\",\n  \"SNOW\": \"Opad \u015bniegu\",\n  \"T2M\": \"Temperatura 2m npg\",\n  \"3CAPE\": \"CAPE 3km\",\n  \"UCAPE\": \"CAPE MU\",\n  \"VCIN\": \"CIN MU\",\n  \"PMSL\": \"Ci\u015bnienie na poz. morza\",\n  \"PS\": \"Ci\u015bnienie na poz. gruntu\",\n  \"Y\": \"Dywergencja\",\n  \"F\": \"Geopotencja\u0142\",\n  \"K\": \"Gradient temperatury\",\n  \"LPI\": \"Indeks potencja\u0142u wy\u0142adowa\u0144\",\n  \"IFS\": \"Indeks stabilno\u015bci mg\u0142y\",\n  \"SWI\": \"Indeks Showaltera\",\n  \"SWISS00\": \"Indeks Showaltera, uskok wiatru 00\",\n  \"SWISS12\": \"Surface Lifted Index, uskok wiatru 12\",\n  \"MCAPE\": \"Maksymalna warto\u015b\u0107 CAPE\",\n  \"NCIN\": \"Convective Inhibition\",\n  \"LCL_ML\": \"Poziom kondensacji\",\n  \"LFC_ML\": \"Poziom swobodnej kondensacji\",\n  \"U\": \"Pr\u0119dko\u015b\u0107 wiatru\",\n  \"TOT_PREC\": \"Opad ca\u0142kowity\",\n  \"SDI_1\": \"Supercell Detection Index 1\",\n  \"SDI_2\": \"Supercell Detection Index 2\",\n  \"SLI\": \"Surface Lifted Index\",\n  \"E\": \"Temp. ekwiw. -potencjal.\",\n  \"HIN\": \"Temp. odczuwalna (cie\u0144)\",\n  \"D\": \"Temp. punktu rosy\",\n  \"TD2M\": \"Temp. punktu rosy 2m npg\",\n  \"B\": \"Temp. term. zwil\u017c.\",\n  \"T\": \"Temperatura\",\n  \"A\": \"Topog. wzgl.\/Ci\u015b.\/Geopot.\",\n  \"W\": \"Pr\u0119dko\u015b\u0107 pionowa\",\n  \"R\": \"Wilgotno\u015b\u0107 wzgl\u0119dna\",\n  \"RELHUM\": \"Wilgotno\u015b\u0107 wzgl\u0119dna 2m npg\",\n  \"Z\": \"Wirowo\u015b\u0107\",\n  \"G\": \"Wysoko\u015b\u0107 izotermy\",\n  \"CLCT\": \"Zachmurzenie ca\u0142kowite\",\n  \"CXCL\": \"Zachmurzenie niskiego poziomu\",\n  \"VRANGE\": \"Zasi\u0119g widzialno\u015bci\",\n  \"GUST\": \"Poryw wiatru 10m npg\",\n  \"VRANGE\": \"Zasi\u0119g widzialno\u015bci\",\n  \"Q1001\": \"Uskok wiatru 0-1000m\",\n  \"Q3001\": \"Uskok wiatru 0-3000m\",\n  \"Q6001\": \"Uskok wiatru 0-6000m\",\n  \"Q8001\": \"Uskok wiatru 0-8000m\",\n  \"T_SURF\": \"Temp. powierzchni\",\n  \"U10M\": \"Pr\u0119dko\u015b\u0107 wiatru 10m npg\",\n  \"N\": \"Niedosyt punktu rosy\",\n  \"Q\": \"Uskok wiatru\",\n\n  \/\/ icon_lagged parametry\n  \"TOT_PREC\": \"Opad ca\u0142kowity\",\n  \"T2M\": \"Temperatura 2m npg\",\n  \"VMAX_10M\": \"Maks. pr\u0119dko\u015b\u0107 wiatru 10m npg\",\n  \"U10M\": \"Pr\u0119dko\u015b\u0107 wiatru 10m npg\",\n  \"CLCT\": \"Zachmurzenie ca\u0142kowite\",\n  \"CLCL\": \"Zachmurzenie chmurami niskimi\",\n  \"CAPE_CON\": \"CAPE\",\n  \"CIN_ML\": \"CIN\",\n  \"TMAX_2M\": \"Maks. temperatura 2m npg\",\n  \"TMIN_2M\": \"Min. temperatura 2m npg\",\n  \"T_G\": \"Temp. przy gruncie\",\n  \"TD2M\": \"Temp. punktu rosy 2m npg\",\n  \"RELHUM\": \"Wilgotno\u015b\u0107 wzgl\u0119dna 2m npg\",\n  \"PS\": \"Ci\u015bnienie na poz. gruntu\",\n  \"PMSL\": \"Ci\u015bnienie na poz. morza\",\n};\n\n\/\/ Przyjazne nazwy dla statystyk\nconst statisticsDisplayNames = {\n  \"mean\": \"\u015arednia z wi\u0105zki\",\n  \"maxof\": \"Absolutne maksimum w wi\u0105zce\",\n  \"minof\": \"Absolutne minimum w wi\u0105zce\",\n  \"spread\": \"Rozrzut w wi\u0105zce\",\n  \"max-min_dif\": \"R\u00f3\u017cnica max-min w wi\u0105zce\",\n  \/\/prawdopodobie\u0144stwa\n  \/\/CLOUD\n  \"prb1_12pct\": \"Prawdopodobie\u0144stwo przekroczenia 12%\",\n  \"prb2_25pct\": \"Prawdopodobie\u0144stwo przekroczenia 25%\",\n  \"prb3_36pct\": \"Prawdopodobie\u0144stwo przekroczenia 36%\",\n  \"prb4_50pct\": \"Prawdopodobie\u0144stwo przekroczenia 50%\",\n  \"prb5_62pct\": \"Prawdopodobie\u0144stwo przekroczenia 62%\",\n  \"prb6_75pct\": \"Prawdopodobie\u0144stwo przekroczenia 75%\",\n  \"prb7_85pct\": \"Prawdopodobie\u0144stwo przekroczenia 85%\",\n  \/\/GUST, WIND\n  \"prb1_30kmh\": \"Prawdopodobie\u0144stwo przekroczenia 30km\/h\",\n  \"prb2_50kmh\": \"Prawdopodobie\u0144stwo przekroczenia 50km\/h\",\n  \"prb3_70kmh\": \"Prawdopodobie\u0144stwo przekroczenia 70km\/h\",\n  \"prb4_90kmh\": \"Prawdopodobie\u0144stwo przekroczenia 90km\/h\",\n  \"prb5_110kmh\": \"Prawdopodobie\u0144stwo przekroczenia 110km\/h\",\n  \"prb6_115kmh\": \"Prawdopodobie\u0144stwo przekroczenia 115km\/h\",\n  \/\/SNOW, TOT PREC\n  \"prb1_0.1mm\": \"Prawdopodobie\u0144stwo przekroczenia 0.1mm\",\n  \"prb2_0.5mm\": \"Prawdopodobie\u0144stwo przekroczenia 0.5mm\",\n  \"prb3_1mm\": \"Prawdopodobie\u0144stwo przekroczenia 1mm\",\n  \"prb4_2mm\": \"Prawdopodobie\u0144stwo przekroczenia 2mm\",\n  \"prb5_5mm\": \"Prawdopodobie\u0144stwo przekroczenia 5mm\",\n  \"prb6_10mm\": \"Prawdopodobie\u0144stwo przekroczenia 10mm\",\n\n  \"prb2_1mm\": \"Prawdopodobie\u0144stwo przekroczenia 1mm\",\n  \"prb3_5mm\": \"Prawdopodobie\u0144stwo przekroczenia 5mm\",\n  \"prb4_10mm\": \"Prawdopodobie\u0144stwo przekroczenia 10mm\",\n  \"prb5_20mm\": \"Prawdopodobie\u0144stwo przekroczenia 20mm\",\n  \"prb6_30mm\": \"Prawdopodobie\u0144stwo przekroczenia 30mm\",\n  \"prb7_40mm\": \"Prawdopodobie\u0144stwo przekroczenia 40mm\",\n  \"prb8_50mm\": \"Prawdopodobie\u0144stwo przekroczenia 50mm\",\n  \/\/TEMP, TSURF, HIN\n  \"prb1_2C\": \"Prawdopodobie\u0144stwo przekroczenia 2\u00b0C\",\n  \"prb2_5C\": \"Prawdopodobie\u0144stwo przekroczenia 5\u00b0C\",\n  \"prb3_10C\": \"Prawdopodobie\u0144stwo przekroczenia 10\u00b0C\",\n  \"prb4_15C\": \"Prawdopodobie\u0144stwo przekroczenia 15\u00b0C\",\n  \"prb5_20C\": \"Prawdopodobie\u0144stwo przekroczenia 20\u00b0C\",\n  \"drb1_-5C\": \"Prawdopodobie\u0144stwo warto\u015bci < -5\u00b0C\",\n  \"drb2_-1C\": \"Prawdopodobie\u0144stwo warto\u015bci < -1\u00b0C\",\n  \"drb3_0C\": \"Prawdopodobie\u0144stwo warto\u015bci < 0\u00b0C\",\n  \"drb4_2C\": \"Prawdopodobie\u0144stwo warto\u015bci < 2\u00b0C\",\n  \"drb5_5C\": \"Prawdopodobie\u0144stwo warto\u015bci < 5\u00b0C\",\n  \"drb6_10C\": \"Prawdopodobie\u0144stwo warto\u015bci < 10\u00b0C\",\n\n  \"prb1_0C\": \"Prawdopodobie\u0144stwo przekroczenia 0\u00b0C\",\n  \"prb2_2C\": \"Prawdopodobie\u0144stwo przekroczenia 2\u00b0C\",\n  \"prb3_5C\": \"Prawdopodobie\u0144stwo przekroczenia 5\u00b0C\",\n  \"drb2_-2C\": \"Prawdopodobie\u0144stwo warto\u015bci < -2\u00b0C\",\n  \"drb3_-1C\": \"Prawdopodobie\u0144stwo warto\u015bci < -1\u00b0C\",\n  \"drb4_0C\": \"Prawdopodobie\u0144stwo warto\u015bci < 0\u00b0C\",\n\n  \"prb1_10C\": \"Prawdopodobie\u0144stwo przekroczenia 10\u00b0C\",\n  \"prb2_20C\": \"Prawdopodobie\u0144stwo przekroczenia 20\u00b0C\",\n  \"prb3_30C\": \"Prawdopodobie\u0144stwo przekroczenia 30\u00b0C\",\n  \"drb1_5C\": \"Prawdopodobie\u0144stwo warto\u015bci < 5\u00b0C\",\n  \"drb2_0C\": \"Prawdopodobie\u0144stwo warto\u015bci < 0\u00b0C\",\n  \"drb3_-5C\": \"Prawdopodobie\u0144stwo warto\u015bci < -5\u00b0C\",\n  \"drb4_-10C\": \"Prawdopodobie\u0144stwo warto\u015bci < -10\u00b0C\",\n  \"drb5_-20C\": \"Prawdopodobie\u0144stwo warto\u015bci < -20\u00b0C\",\n  \/\/LPI\n  \"prb1_0.001_\": \"Prawdopodobie\u0144stwo przekroczenia 0.001\",\n  \"prb2_0.01_\": \"Prawdopodobie\u0144stwo przekroczenia 0.01\",\n  \/\/SDI\n  \"prb1_0.00031s\": \"Prawdopodobie\u0144stwo przekroczenia 0.00031s\",\n  \"prb2_0.0031s\": \"Prawdopodobie\u0144stwo przekroczenia 0.0031s\",\n  \"drb1_-0.0031s\": \"Prawdopodobie\u0144stwo warto\u015bci < -0.0031s\",\n  \"drb2_-0.00031s\": \"Prawdopodobie\u0144stwo warto\u015bci < -0.00031s\",\n  \/\/SLI, SWI\n  \"drb1_-5.0K\": \"Prawdopodobie\u0144stwo warto\u015bci < -5.0K\",\n  \"drb2_-4.0K\": \"Prawdopodobie\u0144stwo warto\u015bci < -4.0K\",\n  \"drb3_-3.0K\": \"Prawdopodobie\u0144stwo warto\u015bci < -3.0K\",\n  \"drb4_0.0K\": \"Prawdopodobie\u0144stwo warto\u015bci < 0.0K\",\n  \/\/SWISS12\n  \"drb1_0.6K\": \"Prawdopodobie\u0144stwo warto\u015bci < 0.6K\",\n};\n\n\/\/ Formatowanie termin\u00f3w prognozy w formacie dwucyfrowym\nconst forecastHoursDisplayNames = {\n  \"0\": \"00\",\n  \"1\": \"01\",\n  \"2\": \"02\",\n  \"3\": \"03\",\n  \"4\": \"04\", \n  \"5\": \"05\",\n  \"6\": \"06\",\n  \"7\": \"07\",\n  \"8\": \"08\",\n  \"9\": \"09\"\n};\n\n\/\/ Etykiety dat prognozy (DD.MM) wyci\u0105gane z sufiksu YYYYMMDDHH w nazwie pliku\nconst hourDateLabels = {};\n\nfunction pad2(value) {\n  return String(value).padStart(2, \"0\");\n}\n\nfunction getHourDateKey(model, term, param, hour) {\n  return model + \"_\" + term + \"_\" + param + \"_\" + String(hour).padStart(2, \"0\");\n}\n\nfunction clearHourDateLabels(model, term, param) {\n  const prefix = model + \"_\" + term + \"_\" + param + \"_\";\n  Object.keys(hourDateLabels).forEach(function(key) {\n    if (key.indexOf(prefix) === 0) {\n      delete hourDateLabels[key];\n    }\n  });\n}\n\nfunction clearHourDateLabelsForModel(model) {\n  Object.keys(hourDateLabels).forEach(function(key) {\n    if (key.indexOf(model + \"_\") === 0) {\n      delete hourDateLabels[key];\n    }\n  });\n  clearDiscoveredCycleBaseCacheForModel(model);\n}\n\nfunction rememberHourDateLabel(model, term, param, hour, dateSuffix) {\n  if (!dateSuffix) {\n    return;\n  }\n  hourDateLabels[getHourDateKey(model, term, param, hour)] = {\n    suffix: dateSuffix,\n    short: formatForecastDateLabel(dateSuffix, param, term),\n    verbose: formatDateSuffixVerbose(dateSuffix, param, term)\n  };\n}\n\nfunction getStoredHourDateLabel(model, term, param, hour) {\n  return hourDateLabels[getHourDateKey(model, term, param, hour)] || null;\n}\n\nfunction parseDateSuffixFromFileName(fileName) {\n  const match = String(fileName).match(\/(\\d{10})(?=\\.webp$)\/i);\n  return match ? match[1] : null;\n}\n\nconst WARSAW_TZ = \"Europe\/Warsaw\";\n\nfunction parseUtcSuffixToDate(dateSuffix) {\n  if (!dateSuffix || dateSuffix.length !== 10) {\n    return null;\n  }\n  return new Date(Date.UTC(\n    parseInt(dateSuffix.slice(0, 4), 10),\n    parseInt(dateSuffix.slice(4, 6), 10) - 1,\n    parseInt(dateSuffix.slice(6, 8), 10),\n    parseInt(dateSuffix.slice(8, 10), 10),\n    0,\n    0,\n    0\n  ));\n}\n\nfunction getWarsawPartsFromUtcDate(utcDate) {\n  if (!utcDate) {\n    return null;\n  }\n  const parts = new Intl.DateTimeFormat(\"en-GB\", {\n    timeZone: WARSAW_TZ,\n    year: \"numeric\",\n    month: \"2-digit\",\n    day: \"2-digit\",\n    hour: \"2-digit\",\n    hour12: false\n  }).formatToParts(utcDate);\n  const get = function(type) {\n    const part = parts.find(function(p) { return p.type === type; });\n    return part ? parseInt(part.value, 10) : 0;\n  };\n  return {\n    year: get(\"year\"),\n    month: get(\"month\"),\n    day: get(\"day\"),\n    hour: get(\"hour\")\n  };\n}\n\nfunction getWarsawPartsFromUtcSuffix(dateSuffix) {\n  return getWarsawPartsFromUtcDate(parseUtcSuffixToDate(dateSuffix));\n}\n\nfunction formatDateSuffixLabel(dateSuffix) {\n  if (!dateSuffix || dateSuffix.length !== 10) {\n    return \"\";\n  }\n  return pad2(parseInt(dateSuffix.slice(6, 8), 10)) + \".\" + pad2(parseInt(dateSuffix.slice(4, 6), 10));\n}\n\nfunction formatNightSpanLabel(dateSuffix) {\n  if (!dateSuffix || dateSuffix.length !== 10) {\n    return \"\";\n  }\n\n  const year = parseInt(dateSuffix.slice(0, 4), 10);\n  const month = parseInt(dateSuffix.slice(4, 6), 10);\n  const day = parseInt(dateSuffix.slice(6, 8), 10);\n  const prevDate = new Date(Date.UTC(year, month - 1, day - 1));\n  const prevDay = prevDate.getUTCDate();\n  const prevMonth = prevDate.getUTCMonth() + 1;\n  const dayStr = pad2(day);\n  const monthStr = pad2(month);\n  const prevDayStr = pad2(prevDay);\n  const prevMonthStr = pad2(prevMonth);\n\n  if (prevMonth === month) {\n    return prevDayStr + \"\/\" + dayStr + \".\" + monthStr;\n  }\n\n  return prevDayStr + \".\" + prevMonthStr + \"\/\" + dayStr + \".\" + monthStr;\n}\n\nfunction formatDaySpanDateLabel(dateSuffix) {\n  if (!dateSuffix || dateSuffix.length !== 10) {\n    return \"\";\n  }\n\n  const year = parseInt(dateSuffix.slice(0, 4), 10);\n  const month = parseInt(dateSuffix.slice(4, 6), 10);\n  const day = parseInt(dateSuffix.slice(6, 8), 10);\n  const dayStr = pad2(day);\n  const monthStr = pad2(month);\n  const nextDate = new Date(Date.UTC(year, month - 1, day + 1));\n  const nextDayStr = pad2(nextDate.getUTCDate());\n  const nextMonth = nextDate.getUTCMonth() + 1;\n  const nextMonthStr = pad2(nextMonth);\n\n  if (nextMonth === month) {\n    return dayStr + \"\/\" + nextDayStr + \".\" + monthStr;\n  }\n\n  return dayStr + \".\" + monthStr + \"\/\" + nextDayStr + \".\" + nextMonthStr;\n}\n\nfunction formatNightDateLabel(dateSuffix) {\n  return formatNightSpanLabel(dateSuffix);\n}\n\nfunction formatPrecip24DateLabel(dateSuffix) {\n  if (!dateSuffix || dateSuffix.length !== 10) {\n    return \"\";\n  }\n\n  const parts = getWarsawPartsFromUtcSuffix(dateSuffix);\n  if (!parts) {\n    return \"\";\n  }\n  return formatNightSpanLabel(dateSuffix) + \" (\" + pad2(parts.hour) + \")\";\n}\n\nfunction formatForecastHourOnlyLabel(dateSuffix) {\n  const parts = getWarsawPartsFromUtcSuffix(dateSuffix);\n  if (!parts) {\n    return \"\";\n  }\n  return pad2(parts.hour);\n}\n\nfunction formatForecastDateLabel(dateSuffix, param, term) {\n  if (!dateSuffix || dateSuffix.length !== 10) {\n    return \"\";\n  }\n\n  if (term === \"1h\" || term === \"3h\") {\n    return formatForecastHourOnlyLabel(dateSuffix);\n  }\n\n  const periodType = getParam12hPeriodType(param);\n  const forecastHour = parseInt(dateSuffix.slice(8, 10), 10);\n\n  if (periodType === \"precip24\") {\n    return formatPrecip24DateLabel(dateSuffix);\n  }\n\n  if (periodType === \"night\" || forecastHour === 6) {\n    return formatNightDateLabel(dateSuffix);\n  }\n\n  return formatDateSuffixLabel(dateSuffix);\n}\n\nfunction formatPrecip24Verbose(dateSuffix) {\n  if (!dateSuffix || dateSuffix.length !== 10) {\n    return \"\";\n  }\n\n  const endDate = parseUtcSuffixToDate(dateSuffix);\n  if (!endDate) {\n    return \"\";\n  }\n\n  const startDate = new Date(endDate.getTime() - 24 * 3600000);\n  const startParts = getWarsawPartsFromUtcDate(startDate);\n  const endParts = getWarsawPartsFromUtcDate(endDate);\n  if (!startParts || !endParts) {\n    return \"\";\n  }\n\n  const startLabel = pad2(startParts.day) + \".\" + pad2(startParts.month);\n  const endLabel = pad2(endParts.day) + \".\" + pad2(endParts.month);\n\n  return startLabel + \" | \" + pad2(startParts.hour) + \":00 - \" + endLabel + \" | \" + pad2(endParts.hour) + \":00\";\n}\n\nfunction formatDateSuffixVerbose(dateSuffix, param, term) {\n  if (!dateSuffix || dateSuffix.length !== 10) {\n    return \"\";\n  }\n\n  if (term === \"1h\" || term === \"3h\") {\n    const label = formatForecastHourOnlyLabel(dateSuffix);\n    return label ? (\"Godzina: \" + label) : \"\";\n  }\n\n  if (getParam12hPeriodType(param) === \"precip24\") {\n    return formatPrecip24Verbose(dateSuffix);\n  }\n\n  const label = formatForecastDateLabel(dateSuffix, param, term);\n  if (!label) {\n    return \"\";\n  }\n\n  const utcHour = parseInt(dateSuffix.slice(8, 10), 10);\n  const periodLabel = utcHour === 6 ? \"noc\" : \"dzie\u0144\";\n  return label + \" | \" + periodLabel;\n}\n\nfunction shouldUseForecastDateLabels(model, term) {\n  if (term === \"12h\") {\n    return model === \"a-laef\" || model === \"cosmo_ens\" || model === \"gefs\" ||\n      model === \"icon_ens\" || model === \"ecmwf_ifs_ens\" || model === \"ecmwf_aifs_ens\";\n  }\n  if (term === \"1h\") {\n    return model === \"a-laef\" || model === \"cosmo_ens\" || model === \"icon_ens\";\n  }\n  if (term === \"3h\") {\n    return model === \"ecmwf_ifs_ens\";\n  }\n  return false;\n}\n\nfunction shouldShowDateLabelForHour(model, term, param, hour) {\n  return shouldUseForecastDateLabels(model, term);\n}\n\nfunction getParam12hPeriodType(param) {\n  const baseParam = String(param || \"\").replace(\/_(sudety|tatry|bieszczady)$\/, \"\");\n\n  if (baseParam === \"24h_tp\" || baseParam === \"24_tp\") {\n    return \"precip24\";\n  }\n  if (baseParam === \"t2m_min\" || baseParam === \"t0m_min\") {\n    return \"night\";\n  }\n  if (baseParam === \"t2m_max\") {\n    return \"day\";\n  }\n  return \"alternate\";\n}\n\nfunction computeForecastDateSuffix(hour, param) {\n  const hourIndex = parseInt(String(hour).replace(\/^0+\/, \"\") || \"0\", 10);\n  if (!hourIndex || hourIndex < 1) {\n    return null;\n  }\n\n  const idx = hourIndex - 1;\n  const periodType = getParam12hPeriodType(param);\n  const targetDate = new Date();\n  targetDate.setHours(0, 0, 0, 0);\n\n  let forecastHour = 18;\n\n  if (periodType === \"day\") {\n    targetDate.setDate(targetDate.getDate() + idx);\n    forecastHour = 18;\n  } else if (periodType === \"night\") {\n    targetDate.setDate(targetDate.getDate() + idx);\n    forecastHour = 6;\n  } else if (periodType === \"precip24\") {\n    const startHoursFromMidnight = 14 + idx * 6;\n    targetDate.setDate(targetDate.getDate() + Math.floor(startHoursFromMidnight \/ 24));\n    forecastHour = startHoursFromMidnight % 24;\n  } else {\n    if (idx % 2 === 0) {\n      targetDate.setDate(targetDate.getDate() + idx \/ 2);\n      forecastHour = 18;\n    } else {\n      targetDate.setDate(targetDate.getDate() + Math.floor((idx + 1) \/ 2));\n      forecastHour = 6;\n    }\n  }\n\n  return String(targetDate.getFullYear()) +\n    pad2(targetDate.getMonth() + 1) +\n    pad2(targetDate.getDate()) +\n    pad2(forecastHour);\n}\n\nfunction getLatestModelCycleBase(model, term) {\n  const processingDelayHours = 4;\n  const adjusted = new Date(Date.now() - processingDelayHours * 3600000);\n  const hour = adjusted.getUTCHours();\n  const cycleStarts = getModelCycleHours(model, term).slice().sort(function(a, b) {\n    return b - a;\n  });\n  let cycleHour = cycleStarts[cycleStarts.length - 1] || 0;\n\n  for (let i = 0; i < cycleStarts.length; i += 1) {\n    if (hour >= cycleStarts[i]) {\n      cycleHour = cycleStarts[i];\n      break;\n    }\n  }\n\n  return new Date(Date.UTC(\n    adjusted.getUTCFullYear(),\n    adjusted.getUTCMonth(),\n    adjusted.getUTCDate(),\n    cycleHour,\n    0,\n    0,\n    0\n  ));\n}\n\nconst discoveredCycleBaseCache = {};\n\nfunction getModelCycleHours(model, term) {\n  if (model === \"ecmwf_ifs_ens\" && term === \"3h\") {\n    return [0, 12];\n  }\n  return [0, 6, 12, 18];\n}\n\nfunction getModelCycleCandidatesForModel(model, term, maxCycles) {\n  maxCycles = maxCycles || 10;\n  const allowedHours = getModelCycleHours(model, term);\n  const candidates = [];\n  const now = Date.now();\n  const startMs = now - 36 * 3600000;\n  const gridMs = 6 * 3600000;\n  const alignedStart = Math.floor(startMs \/ gridMs) * gridMs;\n\n  for (let t = alignedStart; t <= now + gridMs; t += gridMs) {\n    const d = new Date(t);\n    if (allowedHours.indexOf(d.getUTCHours()) !== -1) {\n      candidates.push(new Date(t));\n    }\n  }\n\n  return candidates.reverse().slice(0, maxCycles);\n}\n\nfunction getModelCycleCandidates(maxCycles) {\n  return getModelCycleCandidatesForModel(null, null, maxCycles);\n}\n\nfunction getDiscoveredCycleCacheKey(model, term, region) {\n  return model + \"_\" + term + \"_\" + (region || \"none\");\n}\n\nfunction getDiscoveredCycleBase(model, term, region) {\n  const cached = discoveredCycleBaseCache[getDiscoveredCycleCacheKey(model, term, region)];\n  return cached ? new Date(cached.getTime()) : null;\n}\n\nfunction setDiscoveredCycleBase(model, term, region, cycleBase) {\n  if (!cycleBase) {\n    return;\n  }\n  discoveredCycleBaseCache[getDiscoveredCycleCacheKey(model, term, region)] = new Date(cycleBase.getTime());\n}\n\nfunction clearDiscoveredCycleBaseCacheForModel(model) {\n  Object.keys(discoveredCycleBaseCache).forEach(function(key) {\n    if (key.indexOf(model + \"_\") === 0) {\n      delete discoveredCycleBaseCache[key];\n    }\n  });\n}\n\nfunction computeValidSuffixFromLead(cycleBase, leadHours) {\n  const validTime = new Date(cycleBase.getTime());\n  validTime.setUTCHours(validTime.getUTCHours() + leadHours);\n  return buildDateSuffixFromParts(\n    validTime.getUTCFullYear(),\n    validTime.getUTCMonth() + 1,\n    validTime.getUTCDate(),\n    validTime.getUTCHours()\n  );\n}\n\nfunction getLeadDateSuffixCandidates(hour, cycleBase, model, term) {\n  const leadHours = parseInt(String(hour).replace(\/^0+\/, \"\") || \"0\", 10);\n  if (!leadHours) {\n    return [];\n  }\n\n  const suffixes = [];\n  const bases = [];\n\n  if (cycleBase) {\n    bases.push(cycleBase);\n  }\n\n  getModelCycleCandidatesForModel(model, term, 12).forEach(function(base) {\n    if (!bases.some(function(existing) {\n      return existing.getTime() === base.getTime();\n    })) {\n      bases.push(base);\n    }\n  });\n\n  bases.forEach(function(base) {\n    const suffix = computeValidSuffixFromLead(base, leadHours);\n    if (suffixes.indexOf(suffix) === -1) {\n      suffixes.push(suffix);\n    }\n  });\n\n  return suffixes;\n}\n\nfunction deriveCycleBaseFromValidSuffix(validSuffix, leadHours) {\n  const validDate = parseUtcSuffixToDate(validSuffix);\n  if (!validDate || !leadHours) {\n    return null;\n  }\n  return new Date(validDate.getTime() - leadHours * 3600000);\n}\n\nfunction getResolvedCycleBase(model, term, region) {\n  if (model) {\n    const discovered = getDiscoveredCycleBase(model, term, region);\n    if (discovered) {\n      return discovered;\n    }\n  }\n  return getLatestModelCycleBase(model, term);\n}\n\nfunction buildLeadHourFileNameCandidates(paramToUse, formattedHour, dateSuffixes) {\n  const candidates = [];\n\n  dateSuffixes.forEach(function(dateSuffix) {\n    const datedOnly = `${paramToUse}_${dateSuffix}.webp`;\n    if (!candidates.includes(datedOnly)) {\n      candidates.push(datedOnly);\n    }\n  });\n\n  return candidates;\n}\n\nfunction getHourDateSuffix(model, term, param, hour) {\n  const stored = getStoredHourDateLabel(model, term, param, hour);\n  if (stored && stored.suffix) {\n    return stored.suffix;\n  }\n  if (shouldUseForecastDateLabels(model, term) && term === \"12h\") {\n    return computeForecastDateSuffix(hour, param);\n  }\n  return null;\n}\n\nfunction getHourDisplayLabel(hour, model, term, param) {\n  const hourNorm = String(hour).padStart(2, \"0\");\n  if (shouldShowDateLabelForHour(model, term, param, hourNorm)) {\n    const suffix = getHourDateSuffix(model, term, param, hourNorm);\n    if (suffix) {\n      const label = formatForecastDateLabel(suffix, param, term);\n      if (label) {\n        return label;\n      }\n    }\n  }\n  if (term === \"12h\" && summaryModels.includes(model)) {\n    return hourNorm;\n  }\n  if (shouldUseForecastDateLabels(model, term) && (term === \"1h\" || term === \"3h\")) {\n    const suffix = getHourDateSuffix(model, term, param, hourNorm);\n    const label = suffix ? formatForecastDateLabel(suffix, param, term) : \"\";\n    return label || hourNorm;\n  }\n  const displayHour = forecastHoursDisplayNames[hour] || hourNorm;\n  return displayHour + \"h\";\n}\n\nfunction getHourDisplayVerbose(hour, model, term, param) {\n  const hourNorm = String(hour).padStart(2, \"0\");\n  if (shouldShowDateLabelForHour(model, term, param, hourNorm)) {\n    const suffix = getHourDateSuffix(model, term, param, hourNorm);\n    const label = suffix ? formatDateSuffixVerbose(suffix, param, term) : \"\";\n    if (!label) {\n      return \"\";\n    }\n    if (term === \"1h\" || term === \"3h\") {\n      return label;\n    }\n    return \"Termin: \" + label;\n  }\n  if (term === \"12h\" && summaryModels.includes(model)) {\n    return \"Termin: \" + hourNorm;\n  }\n  const displayHour = forecastHoursDisplayNames[hour] || hourNorm;\n  return \"Godzina: \" + displayHour + \"h\";\n}\n\nfunction buildHourDisplayNames(hours, model, term, param) {\n  const names = {};\n  hours.forEach(function(hour) {\n    names[hour] = getHourDisplayLabel(hour, model, term, param);\n  });\n  return names;\n}\n\nfunction buildHourButtonTitles(hours, model, term, param) {\n  const titles = {};\n  hours.forEach(function(hour) {\n    titles[hour] = getHourDisplayVerbose(hour, model, term, param);\n  });\n  return titles;\n}\n\nfunction updateForecastHoursTitles() {\n  const hoursTitle = document.getElementById(\"hoursTitle\");\n\n  if (hoursTitle) {\n    if (selectedTerm === \"1h\" || selectedTerm === \"3h\") {\n      hoursTitle.textContent = \"Godziny prognozy\";\n    } else if (shouldUseForecastDateLabels(selectedModel, selectedTerm)) {\n      hoursTitle.textContent = \"Termin prognozy\";\n    } else {\n      hoursTitle.textContent = \"Termin prognozy\";\n    }\n  }\n\n  updateHoursButtonsLayout();\n}\n\nfunction getModelAvailableTerms(model) {\n  return summaryTermVariants[model] || (paramsForModels[model] ? Object.keys(paramsForModels[model]) : []);\n}\n\nfunction resolveTermForModelSwitch(prevTerm, model) {\n  if (!summaryModels.includes(model)) {\n    if (prevTerm && termsDefault.includes(prevTerm)) {\n      return prevTerm;\n    }\n    return defaultSettings[model].term;\n  }\n\n  const availableTerms = getModelAvailableTerms(model);\n  if (prevTerm && availableTerms.includes(prevTerm)) {\n    return prevTerm;\n  }\n  return defaultSettings[model].term;\n}\n\nfunction captureHourSelectionContext(model, term, param, hour) {\n  const hourNorm = String(hour).padStart(2, \"0\");\n  const suffix = getHourDateSuffix(model, term, param, hourNorm);\n  return {\n    suffix: suffix,\n    shortLabel: getHourDisplayLabel(hourNorm, model, term, param),\n    hour: hourNorm\n  };\n}\n\nfunction resolveHourFromSelectionContext(context, model, term, param, availableHours) {\n  const normalizedHours = (availableHours || []).map(function(h) {\n    return String(h).padStart(2, \"0\");\n  });\n\n  if (!normalizedHours.length) {\n    return String(defaultSettings[model]?.hour || \"01\").padStart(2, \"0\");\n  }\n\n  if (!context) {\n    return normalizedHours[0];\n  }\n\n  if (context.suffix) {\n    for (let i = 0; i < normalizedHours.length; i += 1) {\n      const hourValue = normalizedHours[i];\n      const hourSuffix = getHourDateSuffix(model, term, param, hourValue);\n      if (hourSuffix === context.suffix) {\n        return hourValue;\n      }\n      const stored = getStoredHourDateLabel(model, term, param, hourValue);\n      if (stored && stored.suffix === context.suffix) {\n        return hourValue;\n      }\n    }\n  }\n\n  if (context.shortLabel) {\n    for (let i = 0; i < normalizedHours.length; i += 1) {\n      const hourValue = normalizedHours[i];\n      if (getHourDisplayLabel(hourValue, model, term, param) === context.shortLabel) {\n        return hourValue;\n      }\n    }\n  }\n\n  if (normalizedHours.indexOf(context.hour) !== -1) {\n    return context.hour;\n  }\n\n  return normalizedHours[0];\n}\n\nlet hourSelectionPreserveContext = null;\n\nfunction updateHoursButtonsLayout() {\n  const hoursContainer = document.getElementById(\"hours\");\n  if (!hoursContainer) {\n    return;\n  }\n  hoursContainer.classList.toggle(\n    \"hours-buttons-2col\",\n    getParam12hPeriodType(selectedParam) === \"precip24\"\n  );\n}\n\nfunction getHourLabelContext() {\n  if (compareMode && getActiveCompareModelIds().length >= 2) {\n    const labelModel = getCompareReferenceModelId();\n    const labelParam = getCompareParamForModel(labelModel) || selectedParam;\n    return { model: labelModel, param: labelParam };\n  }\n  return { model: selectedModel, param: selectedParam };\n}\n\nfunction refreshForecastDateLabels() {\n  const hourButtons = document.querySelectorAll(\"#hours button\");\n  if (!hourButtons.length) {\n    return;\n  }\n\n  const labelCtx = getHourLabelContext();\n  const hours = [];\n  hourButtons.forEach(function(btn) {\n    const hour = btn.dataset.value;\n    hours.push(hour);\n    btn.textContent = getHourDisplayLabel(hour, labelCtx.model, selectedTerm, labelCtx.param);\n    btn.title = getHourDisplayVerbose(hour, labelCtx.model, selectedTerm, labelCtx.param);\n  });\n\n  renderHourSliderLabels(hours);\n  refreshPreloadUi();\n}\n\nfunction renderHourSliderLabels(hours) {\n  const labelsContainer = document.getElementById(\"hourLabels\");\n  if (!labelsContainer) {\n    return;\n  }\n\n  labelsContainer.innerHTML = \"\";\n\n  if (!hours || hours.length <= 1) {\n    return;\n  }\n\n  const labelCtx = getHourLabelContext();\n  const formattedHours = hours.map(function(h) {\n    return String(h).padStart(2, \"0\");\n  });\n  const denominator = Math.max(1, hours.length - 1);\n  const labelsToShow = Math.min(7, hours.length);\n  const step = Math.max(1, Math.floor((hours.length - 1) \/ Math.max(1, labelsToShow - 1)));\n  const addedIndexes = new Set();\n\n  function appendLabel(index) {\n    if (index < 0 || index >= formattedHours.length || addedIndexes.has(index)) {\n      return;\n    }\n    addedIndexes.add(index);\n\n    const label = document.createElement(\"div\");\n    label.className = \"hour-label-marker\";\n    label.textContent = getHourDisplayLabel(\n      formattedHours[index],\n      labelCtx.model,\n      selectedTerm,\n      labelCtx.param\n    );\n    label.dataset.hour = formattedHours[index];\n\n    const loadState = getHourLoadState(formattedHours[index]);\n    if (loadState === \"pending\" || loadState === \"loading\") {\n      label.classList.add(\"preload-pending\");\n    }\n    if (loadState === \"loaded\") {\n      label.classList.add(\"preload-loaded\");\n    }\n\n    if (index === 0) {\n      label.classList.add(\"first\");\n    } else if (index === hours.length - 1) {\n      label.classList.add(\"last\");\n      label.style.left = \"100%\";\n    } else {\n      label.style.left = ((index \/ denominator) * 100) + \"%\";\n    }\n\n    labelsContainer.appendChild(label);\n  }\n\n  appendLabel(0);\n  for (let i = step; i < hours.length - 1; i += step) {\n    appendLabel(i);\n  }\n  appendLabel(hours.length - 1);\n}\n\n\/\/ Opisy parametr\u00f3w dla dynamicznego wy\u015bwietlania\n\/\/ Wsp\u00f3lny opis dla wszystkich parametr\u00f3w - informacje o prognozach wi\u0105zkowych EPS\nconst epsDescription = `\n  <div style=\"font-size: 1.4rem;\">\n    <p>Czym s\u0105 meteorologiczne prognozy wi\u0105zkowe (EPS &ndash; Ensemble Prediction System)?<\/p>\n    <p>Opracowanie: dr Andrzej Mazur, IMGW-PIB CMM.<\/p>\n  <\/div>\n  <div>\n    <p>Niekt\u00f3rych proces\u00f3w atmosferycznych nie jeste\u015bmy w stanie opisa\u0107 z niesko\u0144czon\u0105 dok\u0142adno\u015bci\u0105. Dlaczego? Ze wzgl\u0119du na:<\/p>\n    <p>niepewno\u015b\u0107 (b\u0142\u0105d) pomiaru i jego propagacj\u0119 do warunk\u00f3w pocz\u0105tkowych modeli;<br \/>\n    brak wiedzy o chwilowych warunkach pogodowych na tyle dok\u0142adnej, aby b\u0142\u0105d w d\u0142ugookresowych obliczeniach by\u0142 niezauwa\u017calny;<br \/>\n    sko\u0144czon\u0105 dok\u0142adno\u015b\u0107 oblicze\u0144, parametryzacje zamiast dok\u0142adnego opisu zjawisk;<br \/>\n    b\u0142\u0119dy metod numerycznych, kt\u00f3re s\u0105 coraz bardziej widoczne wraz z wyd\u0142u\u017caniem horyzontu czasowego oblicze\u0144;<br \/>\n    &bdquo;efekt motyla&rdquo; &ndash; wra\u017cliwo\u015b\u0107 na warunki pocz\u0105tkowe (wg s\u0142\u00f3w E. Lorenza, autora koncepcji &bdquo;efektu motyla&rdquo;: &bdquo;dowolny uk\u0142ad fizyczny, kt\u00f3ry zachowuje si\u0119 nieokresowo, jest nieprzewidywalny&rdquo;).<\/p>\n    <div><p><a style=\"color: #c40000\" href=\"\/?page_id=43948\">Czytaj dalej w #AkademiaCMM.<\/a><\/p><\/div>\n  <\/div>\n`;\n\n\/\/ Opisy parametr\u00f3w - wszystkie parametry u\u017cywaj\u0105 wsp\u00f3lnego opisu EPS\nconst parameterDescriptions = {};\n\n\/\/ Konfiguracja sufix\u00f3w dla obrazk\u00f3w standardowych modeli\nconst suffixConfig = {\n  \/\/ Domy\u015blny zestaw sufix\u00f3w dla wszystkich parametr\u00f3w\n  \"default\": [\"mean\", \"maxof\", \"minof\", \"spread\", \"max-min_dif\"],\n  \n  \/\/ Parametry z pe\u0142nym zestawem statystyk (mean, maxof, minof, spread, max-min_dif)\n  \"3CAPE\": [\"mean\", \"maxof\", \"minof\", \"spread\", \"max-min_dif\"],\n  \"CAPE\": [\"mean\", \"maxof\", \"minof\", \"spread\", \"max-min_dif\"],\n  \"PMSL\": [\"mean\", \"maxof\", \"minof\", \"spread\", \"max-min_dif\"],\n  \"PS\": [\"mean\", \"maxof\", \"minof\", \"spread\", \"max-min_dif\"],\n  \n  \/\/ Parametry icon_lagged z podstawowym zestawem statystyk\n  \"CAPE_CON\": [\"mean\", \"maxof\", \"minof\", \"spread\", \"max-min_dif\"],\n  \"CIN_ML\": [\"mean\", \"maxof\", \"minof\", \"spread\", \"max-min_dif\"],\n  \"CLCL\": [\"mean\", \"maxof\", \"minof\", \"spread\", \"max-min_dif\"],\n  \"CLCT\": [\"mean\", \"maxof\", \"minof\", \"spread\", \"max-min_dif\"],\n  \"PMSL\": [\"mean\", \"maxof\", \"minof\", \"spread\", \"max-min_dif\"],\n  \"PS\": [\"mean\", \"maxof\", \"minof\", \"spread\", \"max-min_dif\"],\n  \"RELHUM\": [\"mean\", \"maxof\", \"minof\", \"spread\", \"max-min_dif\"],\n  \"TD2M\": [\"mean\", \"maxof\", \"minof\", \"spread\", \"max-min_dif\"],\n  \"T_G\": [\"mean\", \"maxof\", \"minof\", \"spread\", \"max-min_dif\"],\n  \"TMAX_2M\": [\"mean\", \"maxof\", \"minof\", \"spread\", \"max-min_dif\"],\n  \"TMIN_2M\": [\"mean\", \"maxof\", \"minof\", \"spread\", \"max-min_dif\"],\n  \"U10M\": [\"mean\", \"maxof\", \"minof\", \"spread\", \"max-min_dif\"],\n  \"VMAX_10M\": [\"mean\", \"maxof\", \"minof\", \"spread\", \"max-min_dif\"],\n  \"IFS\": [\"mean\", \"maxof\", \"minof\", \"spread\", \"max-min_dif\"],\n  \"LCL_ML\": [\"mean\", \"maxof\", \"minof\", \"spread\", \"max-min_dif\"],\n  \"LFC_ML\": [\"mean\", \"maxof\", \"minof\", \"spread\", \"max-min_dif\"],\n  \"MCAPE\": [\"mean\", \"maxof\", \"minof\", \"spread\", \"max-min_dif\"],\n  \"NCIN\": [\"mean\", \"maxof\", \"minof\", \"spread\", \"max-min_dif\"],\n  \"PMSL\": [\"mean\", \"maxof\", \"minof\", \"spread\", \"max-min_dif\"],\n  \"PS\": [\"mean\", \"maxof\", \"minof\", \"spread\", \"max-min_dif\"],\n  \"Q1001\": [\"mean\", \"maxof\", \"minof\", \"spread\", \"max-min_dif\"],\n  \"Q3001\": [\"mean\", \"maxof\", \"minof\", \"spread\", \"max-min_dif\"],\n  \"Q6001\": [\"mean\", \"maxof\", \"minof\", \"spread\", \"max-min_dif\"],\n  \"Q8001\": [\"mean\", \"maxof\", \"minof\", \"spread\", \"max-min_dif\"],\n  \"RAIN\": [\"mean\", \"maxof\", \"minof\", \"spread\", \"max-min_dif\"],\n  \"RELHUM\": [\"mean\", \"maxof\", \"minof\", \"spread\", \"max-min_dif\"],\n  \"SDI_2\": [\"mean\", \"maxof\", \"minof\", \"spread\", \"max-min_dif\"],\n  \"TD2M\": [\"mean\", \"maxof\", \"minof\", \"spread\", \"max-min_dif\"],\n  \"UCAPE\": [\"mean\", \"maxof\", \"minof\", \"spread\", \"max-min_dif\"],\n  \"VCIN\": [\"mean\", \"maxof\", \"minof\", \"spread\", \"max-min_dif\"],\n  \n  \/\/ Parametry z prawdopodobie\u0144stwami (prb)\n  \"CLCT\": [\"mean\", \"maxof\", \"minof\", \"spread\", \"max-min_dif\", \"prb1_12pct\", \"prb2_25pct\", \"prb3_36pct\", \"prb4_50pct\", \"prb5_62pct\", \"prb6_75pct\", \"prb7_85pct\"],\n  \"CXCL\": [\"mean\", \"maxof\", \"minof\", \"spread\", \"max-min_dif\", \"prb1_12pct\", \"prb2_25pct\", \"prb3_36pct\", \"prb4_50pct\", \"prb5_62pct\", \"prb6_75pct\", \"prb7_85pct\"],\n  \"GUST\": [\"mean\", \"maxof\", \"minof\", \"spread\", \"max-min_dif\", \"prb1_30kmh\", \"prb2_50kmh\", \"prb3_70kmh\", \"prb4_90kmh\", \"prb5_110kmh\", \"prb6_115kmh\"],\n  \"SNOW\": [\"mean\", \"maxof\", \"minof\", \"spread\", \"max-min_dif\", \"prb1_0.1mm\", \"prb2_0.5mm\", \"prb3_1mm\", \"prb4_2mm\", \"prb5_5mm\", \"prb6_10mm\"],\n  \"TOT_PREC\": [\"mean\", \"maxof\", \"minof\", \"spread\", \"max-min_dif\", \"prb1_0.1mm\", \"prb2_1mm\", \"prb3_5mm\", \"prb4_10mm\", \"prb5_20mm\", \"prb6_30mm\", \"prb7_40mm\", \"prb8_50mm\"],\n  \"U10M\": [\"mean\", \"maxof\", \"minof\", \"spread\", \"max-min_dif\", \"prb1_30kmh\", \"prb2_50kmh\", \"prb3_70kmh\", \"prb4_90kmh\", \"prb5_110kmh\", \"prb6_115kmh\", \"drb1_1.8kmh\", \"drb2_3.6kmh\"],\n  \/\/ Parametry icon_lagged z prawdopodobie\u0144stwami\n  \"tot_prec\": [\"mean\", \"maxof\", \"minof\", \"spread\", \"max-min_dif\", \"prb1_0.1mm\", \"prb2_1mm\", \"prb3_5mm\", \"prb4_10mm\", \"prb5_20mm\", \"prb6_30mm\", \"prb7_40mm\", \"prb8_50mm\"],\n  \n  \/\/ Parametry z dodatkowymi statystykami (drb)\n  \"HIN\": [\"mean\", \"maxof\", \"minof\", \"spread\", \"max-min_dif\", \"prb1_10C\", \"prb2_20C\", \"prb3_30C\", \"drb1_5C\", \"drb2_0C\", \"drb3_-5C\", \"drb4_-10C\", \"drb5_-20C\"],\n  \"LPI\": [\"mean\", \"maxof\", \"minof\", \"spread\", \"max-min_dif\", \"prb1_0.001_\", \"prb2_0.01_\"],\n  \"SDI_1\": [\"mean\", \"maxof\", \"minof\", \"spread\", \"max-min_dif\", \"prb1_0.00031s\", \"prb2_0.0031s\", \"drb1_-0.0031s\", \"drb2_-0.00031s\"],\n  \"SLI\": [\"mean\", \"maxof\", \"minof\", \"spread\", \"max-min_dif\", \"drb1_-5.0K\", \"drb2_-4.0K\", \"drb3_-3.0K\", \"drb4_0.0K\"],\n  \"SWI\": [\"mean\", \"maxof\", \"minof\", \"spread\", \"max-min_dif\", \"drb1_-5.0K\", \"drb2_-4.0K\", \"drb3_-3.0K\", \"drb4_0.0K\"],\n  \"SWISS12\": [\"mean\", \"maxof\", \"minof\", \"spread\", \"max-min_dif\", \"drb1_0.6K\"],\n  \"T2M\": [\"mean\", \"maxof\", \"minof\", \"spread\", \"max-min_dif\", \"prb1_2C\", \"prb2_5C\", \"prb3_10C\", \"prb4_15C\", \"prb5_20C\", \"drb1_-5C\", \"drb2_-1C\", \"drb3_0C\", \"drb4_2C\", \"drb5_5C\", \"drb6_10C\"],\n  \/\/ Parametry icon_lagged z dodatkowymi statystykami\n  \"T2M\": [\"mean\", \"maxof\", \"minof\", \"spread\", \"max-min_dif\", \"prb1_2C\", \"prb2_5C\", \"prb3_10C\", \"prb4_15C\", \"prb5_20C\", \"drb1_-5C\", \"drb2_-1C\", \"drb3_0C\", \"drb4_2C\", \"drb5_5C\", \"drb6_10C\"],\n  \"T_SURF\": [\"mean\", \"maxof\", \"minof\", \"spread\", \"max-min_dif\", \"prb1_0C\", \"prb2_2C\", \"prb3_5C\", \"drb1_-5C\", \"drb2_-2C\", \"drb3_-1C\", \"drb4_0C\"],\n  \"VRANGE\": [\"mean\", \"maxof\", \"minof\", \"spread\", \"max-min_dif\", \"drb1_0.1km\", \"drb2_0.5km\", \"drb3_1.0km\", \"drb4_5.0km\", \"drb5_10.0km\"],\n  \n  \/\/ Parametry tylko z mean\n  \"A\": [\"mean\"], \"A0300\": [\"mean\"], \"A0500\": [\"mean\"], \"A0700\": [\"mean\"], \"A0850\": [\"mean\"], \"A0950\": [\"mean\"], \"A1000\": [\"mean\"],\n  \"B\": [\"mean\"], \"B0300\": [\"mean\"], \"B0500\": [\"mean\"], \"B0700\": [\"mean\"], \"B0850\": [\"mean\"], \"B0950\": [\"mean\"], \"B1000\": [\"mean\"],\n  \"D\": [\"mean\"], \"D0300\": [\"mean\"], \"D0500\": [\"mean\"], \"D0700\": [\"mean\"], \"D0850\": [\"mean\"], \"D0950\": [\"mean\"], \"D1000\": [\"mean\"],\n  \"E\": [\"mean\"], \"E0300\": [\"mean\"], \"E0500\": [\"mean\"], \"E0700\": [\"mean\"], \"E0850\": [\"mean\"], \"E0950\": [\"mean\"], \"E1000\": [\"mean\"],\n  \"F\": [\"mean\"], \"F0300\": [\"mean\"], \"F0500\": [\"mean\"], \"F0700\": [\"mean\"], \"F0850\": [\"mean\"], \"F0950\": [\"mean\"], \"F1000\": [\"mean\"],\n  \"G\": [\"mean\"], \"G0300\": [\"mean\"], \"G0500\": [\"mean\"], \"G0700\": [\"mean\"], \"G0850\": [\"mean\"], \"G0950\": [\"mean\"], \"G1000\": [\"mean\"],\n  \"K\": [\"mean\"], \"K0300\": [\"mean\"], \"K0500\": [\"mean\"], \"K0700\": [\"mean\"], \"K0850\": [\"mean\"], \"K0950\": [\"mean\"], \"K1000\": [\"mean\"],\n  \"N\": [\"mean\"], \"N0300\": [\"mean\"], \"N0500\": [\"mean\"], \"N0700\": [\"mean\"], \"N0850\": [\"mean\"], \"N0950\": [\"mean\"], \"N1000\": [\"mean\"],\n  \"Q\": [\"mean\"],\n  \"R\": [\"mean\"], \"R0300\": [\"mean\"], \"R0500\": [\"mean\"], \"R0700\": [\"mean\"], \"R0850\": [\"mean\"], \"R0950\": [\"mean\"], \"R1000\": [\"mean\"],\n  \"SWISS00\": [\"mean\"],\n  \"T\": [\"mean\"], \"T0300\": [\"mean\"], \"T0500\": [\"mean\"], \"T0700\": [\"mean\"], \"T0850\": [\"mean\"], \"T0950\": [\"mean\"], \"T1000\": [\"mean\"],\n  \"U\": [\"mean\"], \"U0300\": [\"mean\"], \"U0500\": [\"mean\"], \"U0700\": [\"mean\"], \"U0850\": [\"mean\"], \"U0950\": [\"mean\"], \"U1000\": [\"mean\"],\n  \"W\": [\"mean\"], \"W0300\": [\"mean\"], \"W0500\": [\"mean\"], \"W0700\": [\"mean\"], \"W0850\": [\"mean\"], \"W0950\": [\"mean\"], \"W1000\": [\"mean\"],\n  \"Y\": [\"mean\"], \"Y0300\": [\"mean\"], \"Y0500\": [\"mean\"], \"Y0700\": [\"mean\"], \"Y0850\": [\"mean\"], \"Y0950\": [\"mean\"], \"Y1000\": [\"mean\"],\n  \"Z\": [\"mean\"], \"Z0300\": [\"mean\"], \"Z0500\": [\"mean\"], \"Z0700\": [\"mean\"], \"Z0850\": [\"mean\"], \"Z0950\": [\"mean\"], \"Z1000\": [\"mean\"]\n};\n\n\/\/ Funkcja do renderowania przycisk\u00f3w\nfunction renderButtons(containerId, items, onClick, activeItem = null, displayNames = null, buttonTitles = null) {\n  const container = document.getElementById(containerId);\n  container.innerHTML = \"\";\n  \n  \/\/ Sprawd\u017a czy renderujemy przyciski modeli\n  const isModelButtons = containerId === \"models\";\n  if (isModelButtons) {\n    window.__onModelSelected = onClick;\n  }\n  \n  items.forEach(item => {\n    const btn = document.createElement(\"button\");\n    \n    \/\/ Ustaw data-value dla identyfikacji przycisku\n    btn.dataset.value = item;\n    \n    \/\/ Je\u015bli istnieje przyjazna nazwa dla tego elementu, u\u017cyj jej\n    if (displayNames && displayNames[item]) {\n      btn.textContent = displayNames[item];\n    } else {\n      btn.textContent = item;\n    }\n\n    if (buttonTitles && buttonTitles[item]) {\n      btn.title = buttonTitles[item];\n    }\n    \n    \/\/ Ustaw klas\u0119 'active' dla aktywnego przycisku\n    if (isModelButtons && compareMode && compareSelectedModels.includes(item)) {\n      btn.classList.add(\"compare-selected\");\n    } else if (activeItem && item === activeItem) {\n      btn.classList.add('active');\n    }\n\n    if (isModelButtons && compareMode) {\n      const isEligible = getEligibleCompareModelIds().indexOf(item) !== -1;\n      if (!isEligible) {\n        btn.disabled = true;\n        btn.classList.add(\"compare-ineligible\");\n        btn.title = \"Model niedost\u0119pny dla bie\u017c\u0105cej konfiguracji\";\n      } else if (compareSelectedModels.includes(item)) {\n        btn.title = \"Wybrany do por\u00f3wnania \u2014 kliknij inny model, aby go podmieni\u0107\";\n      } else {\n        btn.title = \"Kliknij, aby podmieni\u0107 model w por\u00f3wnaniu\";\n      }\n    }\n\n    if (containerId === \"hours\") {\n      applyPreloadClassesToButton(btn, getHourLoadState(item));\n    }\n    \n    btn.onclick = () => {\n      if (isModelButtons && compareMode) {\n        toggleModelInComparison(item);\n        return;\n      }\n\n      \/\/ Resetuj stan przy zmianie modelu\n      if (isModelButtons) {\n        \/\/ Wyczy\u015b\u0107 bufor obraz\u00f3w przy zmianie modelu\n        if (selectedModel !== item) {\n          resetStateForModelChange();\n        }\n      }\n      \n      \/\/ Usu\u0144 klas\u0119 'active' ze wszystkich przycisk\u00f3w w kontenerze\n      container.querySelectorAll('button').forEach(b => b.classList.remove('active'));\n      \/\/ Dodaj klas\u0119 'active' do klikni\u0119tego przycisku\n      btn.classList.add('active');\n      \n      \/\/ Wywo\u0142aj funkcj\u0119 onClick\n      onClick(item);\n    };\n    \n    container.appendChild(btn);\n  });\n}\n\n\/\/ Funkcja do renderowania list rozwijanych\nfunction renderSelect(selectId, items, onChange, displayNames = null) {\n  const select = document.getElementById(selectId);\n  select.innerHTML = \"\";\n  \n  \/\/ Dodanie domy\u015blnej opcji\n  const defaultOption = document.createElement(\"option\");\n  defaultOption.value = \"\";\n  defaultOption.textContent = \"-- Wybierz --\";\n  select.appendChild(defaultOption);\n  \n  \/\/ Dodanie opcji z listy\n  items.forEach(item => {\n    const option = document.createElement(\"option\");\n    option.value = item;\n    \n    \/\/ Je\u015bli istnieje przyjazna nazwa dla tego parametru, u\u017cyj jej\n    if (displayNames && displayNames[item]) {\n      option.textContent = displayNames[item];\n    } else {\n      option.textContent = item;\n    }\n    \n    \/\/ Ustawienie atrybutu dla parametr\u00f3w z poziomami ci\u015bnienia\n    if (selectId === \"paramsSelect\" && hasPressureLevels(item)) {\n      option.setAttribute(\"style\", \"color: rgba(86, 221, 208, 1) !important; font-weight: bold;\");\n    }\n    \n    select.appendChild(option);\n  });\n  \n  \/\/ Dodanie obs\u0142ugi zmiany\n  if (selectId === \"paramsSelect\" || selectId === \"statsSelect\") {\n    \/\/ Specjalna obs\u0142uga dla paramsSelect i statsSelect - zmiana tylko na Enter lub klikni\u0119cie myszk\u0105\n    let isSelectingWithMouse = false;\n    \n    \/\/ Wykryj klikni\u0119cie myszk\u0105 na opcji\n    select.addEventListener(\"mousedown\", function(e) {\n      isSelectingWithMouse = true;\n    });\n    \n    \/\/ Obs\u0142uga zmiany\n    select.onchange = function(e) {\n      \/\/ Je\u015bli to klikni\u0119cie myszk\u0105, wywo\u0142aj onChange\n      if (isSelectingWithMouse && this.value) {\n        onChange(this.value);\n        this.size = 0;\n        this.blur();\n        isSelectingWithMouse = false;\n        return;\n      }\n      \n      \/\/ W przeciwnym razie (strza\u0142ki) tylko zapisz warto\u015b\u0107, nie wywo\u0142uj onChange\n      e.preventDefault();\n      isSelectingWithMouse = false;\n    };\n    \n    \/\/ Obs\u0142uga Enter dla zatwierdzenia\n    select.addEventListener(\"keydown\", function(e) {\n      if (e.key === \"Enter\" && this.value) {\n        e.preventDefault();\n        onChange(this.value);\n        \/\/ Zamknij list\u0119 natychmiast\n        this.size = 0;\n        this.blur();\n      }\n    });\n  } else {\n    \/\/ Dla pozosta\u0142ych select - standardowa obs\u0142uga\n    select.onchange = function() {\n      if (this.value) {\n        onChange(this.value);\n      }\n    };\n  }\n}\n\n\/\/ Definiowanie domy\u015blnych warto\u015bci dla ka\u017cdego modelu\nconst defaultSettings = {\n  \"a-laef\": {\n    term: \"12h\",\n    param: \"t2m_max\",\n    hour: \"01\",\n    stat: null \/\/ a-laef nie ma statystyk\n  },\n  \"cosmo_ens\": {\n    term: \"12h\",\n    param: \"t2m_max\",\n    hour: \"01\",\n    stat: null \/\/ cosmo_ens nie ma statystyk\n  },\n  \"icon_ens\": {\n    term: \"12h\",\n    param: \"t2m_max\",\n    hour: \"01\",\n    stat: null \/\/ icon_ens nie ma statystyk\n  },\n  \"ecmwf_ifs_ens\": {\n    term: \"12h\",\n    param: \"t2m_max\",\n    hour: \"01\",\n    stat: null \/\/ ecmwf_ifs_ens nie ma statystyk\n  },\n  \"ecmwf_aifs_ens\": {\n    term: \"12h\",\n    param: \"t2m_max\",\n    hour: \"01\",\n    stat: null \/\/ ecmwf_aifs_ens nie ma statystyk\n  },\n  \"gefs\": {\n    term: \"12h\",\n    param: \"t2m_max\",\n    hour: \"01\",\n    stat: null\n  },\n  \"EPS28km\": {\n    term: \"00\",\n    param: \"T2M\",\n    hour: \"0\",\n    stat: \"mean\"\n  },\n  \"EPS70km\": {\n    term: \"00\",\n    param: \"T2M\",\n    hour: \"0\",\n    stat: \"mean\"\n  },\n  \"gfs_lagged\": {\n    term: \"00\",\n    param: \"T2M\",\n    hour: \"0\",\n    stat: \"mean\"\n  },\n  \"icon_lagged\": {\n    term: \"00\",\n    param: \"T2M\",\n    hour: \"0\",\n    stat: \"mean\"\n  }\n};\n\n\/\/ Inicjalizacja interfejsu z domy\u015blnymi warto\u015bciami\nfunction initializeInterface() {\n  \/\/ Ustawienie domy\u015blnego modelu\n  selectedModel = models[0];\n  \n  \/\/ Ustawienie domy\u015blnych warto\u015bci dla wybranego modelu\n  selectedTerm = defaultSettings[selectedModel].term;\n  selectedParam = defaultSettings[selectedModel].param;\n  selectedHour = defaultSettings[selectedModel].hour;\n  selectedStat = defaultSettings[selectedModel].stat;\n  \n  \/\/ Aktualizacja etykiety terminu\n  updateTermLabel();\n  selectedPressureLevel = null;\n  \n  \/\/ Inicjalizacja przycisk\u00f3w modeli z aktywnym modelem\n  renderButtons(\"models\", models, (model) => {\n    const prevModel = selectedModel;\n    selectedModel = model;\n    \n    \/\/ Zapisz poprzednie warto\u015bci przed zmian\u0105\n    const prevTerm = selectedTerm;\n    const prevStat = selectedStat;\n    const prevHour = selectedHour;\n    \n    \/\/ Sprawd\u017a czy zmieniamy typ modelu\n    const prevModelWasSummary = summaryModels.includes(prevModel);\n    const newModelIsSummary = summaryModels.includes(model);\n    const changingModelType = prevModelWasSummary !== newModelIsSummary;\n    \n    \/\/ Zachowaj poprzedni parametr\n    const prevParam = selectedParam;\n    \n    \/\/ Ustawienie domy\u015blnych warto\u015bci dla wybranego modelu\n    \/\/ Zachowaj parametr je\u015bli prze\u0142\u0105czamy mi\u0119dzy modelami podsumowania i parametr istnieje w nowym modelu\n    if (!changingModelType && newModelIsSummary && prevParam) {\n      const newTerm = resolveTermForModelSwitch(prevTerm, model);\n      selectedParam = resolveParamForModelSwitch(prevParam, model, newTerm);\n    } else {\n      selectedParam = resolveParamForModelSwitch(prevParam, model, resolveTermForModelSwitch(prevTerm, model));\n    }\n    \n    \/\/ Zachowaj godzin\u0119 TYLKO je\u015bli pozostajemy w tym samym typie modelu\n    if (!changingModelType && prevHour) {\n      selectedHour = prevHour;\n    } else {\n      selectedHour = defaultSettings[model].hour;\n    }\n    \n    \/\/ Zachowaj termin dla zwyk\u0142ych modeli\n    if (!summaryModels.includes(model)) {\n      if (prevTerm && termsDefault.includes(prevTerm)) {\n        selectedTerm = prevTerm;\n      } else {\n        selectedTerm = defaultSettings[model].term;\n      }\n      \n      \/\/ Zachowaj statystyk\u0119 dla zwyk\u0142ych modeli\n      const availableStats = suffixConfig[selectedParam] || suffixConfig[\"default\"];\n      if (prevStat && availableStats.includes(prevStat)) {\n        selectedStat = prevStat;\n      } else {\n        selectedStat = defaultSettings[model].stat;\n      }\n    } else {\n      selectedTerm = defaultSettings[model].term;\n      selectedStat = null; \/\/ Modele podsumowania nie maj\u0105 statystyk\n      selectedPressureLevel = null; \/\/ Modele podsumowania nie maj\u0105 poziom\u00f3w ci\u015bnienia\n      \/\/ Ukryj slider ci\u015bnienia dla modeli podsumowania\n      document.getElementById(\"pressureLevelSliderSection\").style.display = \"none\";\n    }\n    \n    \/\/ Resetuj region przy zmianie modelu (tylko warto\u015b\u0107, nie ukrywaj sekcji)\n    selectedRegion = null;\n    \n    \n    \/\/ NATYCHMIAST ukryj\/poka\u017c regiony dla nowego modelu\n    renderRegions();\n    \n    \n    \/\/ Aktualizacja etykiety terminu\n    updateTermLabel();\n\n    \/\/ Je\u015bli to model podsumowania, potrzebujemy sprawdzi\u0107 dost\u0119pne godziny\n    if (summaryModels.includes(model)) {\n      determineAvailableHours(model, selectedTerm, selectedParam)\n        .then(() => {\n          updateTermButtons();\n          renderParams();\n          renderRegions();\n          renderForecastHours();\n        })\n        .catch(err => {\n          console.error(\"determineAvailableHours failed:\", err);\n        });\n    } else {\n      \/\/ Dla standardowych modeli ukryj sekcj\u0119 region\u00f3w\n      const regionSection = document.getElementById(\"regionSection\");\n      const regionsContainer = document.getElementById(\"regions\");\n      if (regionsContainer) regionsContainer.innerHTML = \"\";\n      if (regionSection) regionSection.style.display = \"none\";\n      selectedRegion = null;\n      updateTermButtons();\n    }\n  }, selectedModel, modelDisplayNames);\n  \n  \/\/ Inicjalizacja pozosta\u0142ych element\u00f3w\n  \n  \/\/ Je\u015bli pocz\u0105tkowy model jest modelem podsumowania, sprawdzamy dost\u0119pne godziny\n  if (summaryModels.includes(selectedModel)) {\n    determineAvailableHours(selectedModel, selectedTerm, selectedParam)\n      .then(() => {\n        updateTermButtons();\n        renderForecastHours();\n      });\n  } else {\n    updateTermButtons();\n  }\n}\n\n\/\/ Lista wszystkich modeli typu \"podsumowanie\"\nconst summaryModels = [\"a-laef\", \"cosmo_ens\", \"icon_ens\", \"ecmwf_ifs_ens\", \"ecmwf_aifs_ens\", \"gefs\"];\n\n\/\/ Dodajemy style CSS dla komunikatu \u0142adowania\nconst styleElement = document.createElement('style');\nstyleElement.textContent = `\n  .loading-hours {\n    padding: 10px;\n    text-align: center;\n    color: #666;\n    font-style: italic;\n    grid-column: 1 \/ -1;\n    width: 100%;\n    box-sizing: border-box;\n  }\n`;\ndocument.head.appendChild(styleElement);\n\n\/\/ Funkcja do sprawdzenia czy plik istnieje\n\/\/ Cache dla sprawdzania obrazk\u00f3w\nconst imageExistsCache = {};\n\nfunction checkImageExists(url) {\n  \/\/ Sprawd\u017a cache\n  if (imageExistsCache.hasOwnProperty(url)) {\n    return Promise.resolve(imageExistsCache[url]);\n  }\n  \n  \/\/ U\u017cyj fetch z HEAD request - szybsze ni\u017c \u0142adowanie ca\u0142ego obrazka\n  return fetch(url, { \n    method: 'HEAD',\n    cache: 'no-cache'\n  })\n    .then(response => {\n      const exists = response.ok;\n      imageExistsCache[url] = exists;\n      return exists;\n    })\n    .catch(error => {\n      \/\/ Je\u015bli fetch nie dzia\u0142a (np. CORS), u\u017cyj fallback z Image\n      return new Promise((resolve) => {\n        const img = new Image();\n        let timer = null;\n        \n        timer = setTimeout(() => {\n          img.onload = img.onerror = null;\n          imageExistsCache[url] = false;\n          resolve(false);\n        }, 400);\n        \n        img.onload = () => {\n          clearTimeout(timer);\n          imageExistsCache[url] = true;\n          resolve(true);\n        };\n        \n        img.onerror = () => {\n          clearTimeout(timer);\n          imageExistsCache[url] = false;\n          resolve(false);\n        };\n        \n        img.src = url;\n      });\n    });\n}\n\n\/\/ Dla wybranych parametr\u00f3w dopuszczamy wi\u0119cej ni\u017c jeden format nazwy pliku.\nfunction buildSummaryParamName(model, term, param, pressureLevel = null, region = null) {\n  let paramToUse = param;\n\n  if (pressureLevel) {\n    if (pressureLevel === \"1000\") {\n      paramToUse = param + pressureLevel;\n    } else {\n      paramToUse = param + \"0\" + pressureLevel;\n    }\n  }\n\n  if ((model === \"cosmo_ens\" || model === \"icon_ens\") && term === \"1h\") {\n    const summary1hParamAliases = {\n      \"cin_mu_max\": \"cin_mu\",\n      \"cin_ml_max\": \"cin_ml\",\n      \"cape_ml_max\": \"cape_ml\",\n      \"cape_mu_max\": \"cape_mu\"\n    };\n    paramToUse = summary1hParamAliases[paramToUse] || paramToUse;\n  }\n\n  if (region) {\n    paramToUse = paramToUse + \"_\" + region;\n  }\n\n  return paramToUse;\n}\n\nfunction buildDateSuffixFromParts(year, month, day, hour) {\n  return String(year) + pad2(month) + pad2(day) + pad2(hour);\n}\n\nfunction get12hMaxTermCount(model) {\n  const configured = forecastHours[model] && forecastHours[model][\"12h\"];\n  if (configured && configured.length) {\n    return configured.length;\n  }\n  return 21;\n}\n\nfunction get12hHorizonDays(model, periodType) {\n  const maxTerms = get12hMaxTermCount(model);\n  if (periodType === \"precip24\") {\n    return Math.ceil(maxTerms \/ 4) + 1;\n  }\n  if (periodType === \"alternate\") {\n    return Math.ceil(maxTerms \/ 2) + 1;\n  }\n  return maxTerms + 1;\n}\n\nfunction getTermSuffixCacheKey(model, term, region, param) {\n  return model + \"_\" + term + \"_\" + (region || \"none\") + \"_\" + param;\n}\n\nfunction clearTermDateSuffixCacheForModel(model) {\n  Object.keys(termDateSuffixCache).forEach(function(key) {\n    if (key.indexOf(model + \"_\") === 0) {\n      delete termDateSuffixCache[key];\n    }\n  });\n}\n\nasync function findFirstExistingFileName(basePath, fileNames) {\n  if (!fileNames.length) {\n    return null;\n  }\n\n  if (await checkImageExists(basePath + fileNames[0])) {\n    return fileNames[0];\n  }\n\n  if (fileNames.length === 1) {\n    return null;\n  }\n\n  const remainingChecks = await Promise.all(\n    fileNames.slice(1).map(async function(fileName) {\n      return (await checkImageExists(basePath + fileName)) ? fileName : null;\n    })\n  );\n\n  for (let i = 0; i < remainingChecks.length; i += 1) {\n    if (remainingChecks[i]) {\n      return remainingChecks[i];\n    }\n  }\n\n  return null;\n}\n\nfunction build12hDateSuffixCandidates(param, model) {\n  const periodType = getParam12hPeriodType(param);\n  const candidates = [];\n  const seen = {};\n  const today = new Date();\n  today.setHours(0, 0, 0, 0);\n  const horizonDays = get12hHorizonDays(model || \"gefs\", periodType);\n\n  function add(date, hour) {\n    const suffix = buildDateSuffixFromParts(\n      date.getFullYear(),\n      date.getMonth() + 1,\n      date.getDate(),\n      hour\n    );\n    if (!seen[suffix]) {\n      seen[suffix] = true;\n      candidates.push(suffix);\n    }\n  }\n\n  if (periodType === \"day\") {\n    for (let d = 0; d < horizonDays; d += 1) {\n      const date = new Date(today);\n      date.setDate(date.getDate() + d);\n      add(date, 18);\n    }\n  } else if (periodType === \"night\") {\n    for (let d = 0; d < horizonDays; d += 1) {\n      const date = new Date(today);\n      date.setDate(date.getDate() + d);\n      add(date, 6);\n    }\n  } else if (periodType === \"precip24\") {\n    for (let d = 0; d < horizonDays; d += 1) {\n      const date = new Date(today);\n      date.setDate(date.getDate() + d);\n      add(date, 0);\n      add(date, 6);\n      add(date, 12);\n      add(date, 18);\n    }\n  } else {\n    for (let d = 0; d < horizonDays; d += 1) {\n      const dayDate = new Date(today);\n      dayDate.setDate(dayDate.getDate() + d);\n      add(dayDate, 18);\n      const nightDate = new Date(today);\n      nightDate.setDate(nightDate.getDate() + d + 1);\n      add(nightDate, 6);\n    }\n  }\n\n  return candidates.sort();\n}\n\nfunction build12hFileNamesForSuffix(paramToUse, suffix) {\n  return [`${paramToUse}_${suffix}.webp`];\n}\n\nasync function scanAvailable12hSuffixes(model, term, param, region) {\n  const paramToUse = buildSummaryParamName(model, term, param, null, region);\n  const suffixCandidates = build12hDateSuffixCandidates(param, model);\n  const foundSuffixes = [];\n  const BATCH_SIZE = 24;\n  const maxTerms = get12hMaxTermCount(model);\n  const basePath = `\/wp-content\/uploads\/production\/prognozy_wiazkowe\/${model}\/${term}\/`;\n  let emptyBatchStreak = 0;\n\n  for (let i = 0; i < suffixCandidates.length; i += BATCH_SIZE) {\n    if (foundSuffixes.length >= maxTerms) {\n      break;\n    }\n    if (emptyBatchStreak >= 2 && foundSuffixes.length > 0) {\n      break;\n    }\n\n    const batch = suffixCandidates.slice(i, i + BATCH_SIZE);\n    const results = await Promise.all(batch.map(async function(suffix) {\n      const fileName = await findFirstExistingFileName(\n        basePath,\n        build12hFileNamesForSuffix(paramToUse, suffix)\n      );\n      return fileName ? suffix : null;\n    }));\n\n    const batchHits = results.filter(Boolean);\n    if (!batchHits.length) {\n      emptyBatchStreak += 1;\n    } else {\n      emptyBatchStreak = 0;\n      batchHits.forEach(function(suffix) {\n        if (foundSuffixes.indexOf(suffix) === -1) {\n          foundSuffixes.push(suffix);\n        }\n      });\n    }\n  }\n\n  return foundSuffixes.sort();\n}\n\nasync function verify12hSuffixesForParam(model, term, param, region, suffixes) {\n  const paramToUse = buildSummaryParamName(model, term, param, null, region);\n  const basePath = `\/wp-content\/uploads\/production\/prognozy_wiazkowe\/${model}\/${term}\/`;\n  const BATCH_SIZE = 24;\n  const verified = [];\n\n  for (let i = 0; i < suffixes.length; i += BATCH_SIZE) {\n    const batch = suffixes.slice(i, i + BATCH_SIZE);\n    const results = await Promise.all(batch.map(async function(suffix) {\n      const fileName = await findFirstExistingFileName(\n        basePath,\n        build12hFileNamesForSuffix(paramToUse, suffix)\n      );\n      return fileName ? suffix : null;\n    }));\n\n    results.forEach(function(suffix) {\n      if (suffix) {\n        verified.push(suffix);\n      }\n    });\n  }\n\n  return verified.sort();\n}\n\nasync function performAvailable12hHoursBySuffix(model, term, param, region) {\n  const cacheKey = getTermSuffixCacheKey(model, term, region, param);\n  const cachedEntry = termDateSuffixCache[cacheKey];\n  let uniqueFound;\n\n  if (cachedEntry && cachedEntry.suffixes && cachedEntry.suffixes.length) {\n    uniqueFound = await verify12hSuffixesForParam(model, term, param, region, cachedEntry.suffixes);\n    if (!uniqueFound.length) {\n      uniqueFound = await scanAvailable12hSuffixes(model, term, param, region);\n      termDateSuffixCache[cacheKey] = {\n        suffixes: uniqueFound.slice(),\n        probeParam: param\n      };\n    }\n  } else {\n    uniqueFound = await scanAvailable12hSuffixes(model, term, param, region);\n    termDateSuffixCache[cacheKey] = {\n      suffixes: uniqueFound.slice(),\n      probeParam: param\n    };\n  }\n\n  const availableHours = [];\n  uniqueFound.forEach(function(suffix, index) {\n    const hour = String(index + 1).padStart(2, \"0\");\n    rememberHourDateLabel(model, term, param, hour, suffix);\n    availableHours.push(hour);\n  });\n\n  if (!paramHours[model]) {\n    paramHours[model] = {};\n  }\n  paramHours[model][param] = availableHours;\n  return availableHours;\n}\n\nfunction getLeadTermMaxCount(model, term) {\n  const configured = forecastHours[model] && forecastHours[model][term];\n  if (configured && configured.length) {\n    return configured.length;\n  }\n  return term === \"3h\" ? 24 : 72;\n}\n\nfunction buildLeadTermDateSuffixCandidates(model, term) {\n  const stepHours = term === \"3h\" ? 3 : 1;\n  const maxTerms = getLeadTermMaxCount(model, term);\n  const candidates = [];\n  const seen = {};\n  const nowMs = Date.now();\n  const startMs = nowMs - 48 * 3600000;\n  const endMs = nowMs + (maxTerms + 12) * stepHours * 3600000;\n  const gridMs = stepHours * 3600000;\n  let t = Math.floor(startMs \/ gridMs) * gridMs;\n\n  for (; t <= endMs; t += gridMs) {\n    const d = new Date(t);\n    const suffix = buildDateSuffixFromParts(\n      d.getUTCFullYear(),\n      d.getUTCMonth() + 1,\n      d.getUTCDate(),\n      d.getUTCHours()\n    );\n    if (!seen[suffix]) {\n      seen[suffix] = true;\n      candidates.push(suffix);\n    }\n  }\n\n  return candidates;\n}\n\nasync function scanAvailableLeadTermSuffixes(model, term, param, region) {\n  const paramToUse = buildSummaryParamName(model, term, param, null, region);\n  const suffixCandidates = buildLeadTermDateSuffixCandidates(model, term);\n  const foundSuffixes = [];\n  const BATCH_SIZE = 24;\n  const maxTerms = getLeadTermMaxCount(model, term);\n  const basePath = `\/wp-content\/uploads\/production\/prognozy_wiazkowe\/${model}\/${term}\/`;\n  let emptyBatchStreak = 0;\n\n  for (let i = 0; i < suffixCandidates.length; i += BATCH_SIZE) {\n    if (foundSuffixes.length >= maxTerms) {\n      break;\n    }\n    if (emptyBatchStreak >= 2 && foundSuffixes.length > 0) {\n      break;\n    }\n\n    const batch = suffixCandidates.slice(i, i + BATCH_SIZE);\n    const results = await Promise.all(batch.map(async function(suffix) {\n      const fileName = await findFirstExistingFileName(\n        basePath,\n        [`${paramToUse}_${suffix}.webp`]\n      );\n      return fileName ? suffix : null;\n    }));\n\n    const batchHits = results.filter(Boolean);\n    if (!batchHits.length) {\n      emptyBatchStreak += 1;\n    } else {\n      emptyBatchStreak = 0;\n      batchHits.forEach(function(suffix) {\n        if (foundSuffixes.indexOf(suffix) === -1) {\n          foundSuffixes.push(suffix);\n        }\n      });\n    }\n  }\n\n  return foundSuffixes.sort();\n}\n\nasync function verifyLeadTermSuffixesForParam(model, term, param, region, suffixes) {\n  const paramToUse = buildSummaryParamName(model, term, param, null, region);\n  const basePath = `\/wp-content\/uploads\/production\/prognozy_wiazkowe\/${model}\/${term}\/`;\n  const BATCH_SIZE = 24;\n  const verified = [];\n\n  for (let i = 0; i < suffixes.length; i += BATCH_SIZE) {\n    const batch = suffixes.slice(i, i + BATCH_SIZE);\n    const results = await Promise.all(batch.map(async function(suffix) {\n      const fileName = await findFirstExistingFileName(\n        basePath,\n        [`${paramToUse}_${suffix}.webp`]\n      );\n      return fileName ? suffix : null;\n    }));\n\n    results.forEach(function(suffix) {\n      if (suffix) {\n        verified.push(suffix);\n      }\n    });\n  }\n\n  return verified.sort();\n}\n\nasync function performAvailableLeadTermHoursBySuffix(model, term, param, region) {\n  const cacheKey = getTermSuffixCacheKey(model, term, region, param);\n  const cachedEntry = termDateSuffixCache[cacheKey];\n  let uniqueFound;\n\n  if (cachedEntry && cachedEntry.suffixes && cachedEntry.suffixes.length) {\n    uniqueFound = await verifyLeadTermSuffixesForParam(\n      model,\n      term,\n      param,\n      region,\n      cachedEntry.suffixes\n    );\n    if (!uniqueFound.length) {\n      uniqueFound = await scanAvailableLeadTermSuffixes(model, term, param, region);\n      termDateSuffixCache[cacheKey] = {\n        suffixes: uniqueFound.slice(),\n        probeParam: param\n      };\n    }\n  } else {\n    uniqueFound = await scanAvailableLeadTermSuffixes(model, term, param, region);\n    termDateSuffixCache[cacheKey] = {\n      suffixes: uniqueFound.slice(),\n      probeParam: param\n    };\n  }\n\n  const availableHours = [];\n  uniqueFound.forEach(function(suffix, index) {\n    const hour = String(index + 1).padStart(2, \"0\");\n    rememberHourDateLabel(model, term, param, hour, suffix);\n    availableHours.push(hour);\n  });\n\n  if (!paramHours[model]) {\n    paramHours[model] = {};\n  }\n  paramHours[model][param] = availableHours;\n  return availableHours;\n}\n\nfunction getFileLookupDateSuffixes(model, term, param, hour) {\n  const suffixes = [];\n  const stored = getStoredHourDateLabel(model, term, param, hour);\n  if (stored && stored.suffix) {\n    suffixes.push(stored.suffix);\n  }\n\n  const computed = getHourDateSuffix(model, term, param, hour);\n  if (computed && suffixes.indexOf(computed) === -1) {\n    suffixes.push(computed);\n  }\n\n  \/\/ W por\u00f3wnaniu do\u0142\u00f3\u017c sufiks modelu referencyjnego (ta sama data wa\u017cno\u015bci).\n  if (compareMode && getActiveCompareModelIds().length >= 2) {\n    const refModel = getCompareReferenceModelId();\n    if (refModel && refModel !== model) {\n      const refParam = getCompareParamForModel(refModel) || param;\n      const refSuffix = getHourDateSuffix(refModel, term, refParam, hour);\n      if (refSuffix && suffixes.indexOf(refSuffix) === -1) {\n        suffixes.push(refSuffix);\n      }\n    }\n  }\n\n  return suffixes;\n}\n\nfunction appendDatedFileNameCandidates(candidates, paramToUse, formattedHour, dateSuffixes) {\n  dateSuffixes.forEach(function(dateSuffix) {\n    const datedOnly = `${paramToUse}_${dateSuffix}.webp`;\n    const datedWithHour = `${paramToUse}_${formattedHour}_${dateSuffix}.webp`;\n    if (!candidates.includes(datedOnly)) {\n      candidates.push(datedOnly);\n    }\n    if (!candidates.includes(datedWithHour)) {\n      candidates.push(datedWithHour);\n    }\n  });\n}\n\nfunction generateFileNameCandidates(model, term, param, hour, stat, pressureLevel = null, region = null) {\n  const candidates = [];\n\n  if (summaryModels.includes(model) && (term === \"12h\" || term === \"1h\" || term === \"3h\") &&\n      shouldUseForecastDateLabels(model, term)) {\n    const paramToUse = buildSummaryParamName(model, term, param, pressureLevel, region);\n    const formattedHour = String(hour).padStart(2, \"0\");\n    return buildLeadHourFileNameCandidates(\n      paramToUse,\n      formattedHour,\n      getFileLookupDateSuffixes(model, term, param, hour)\n    );\n  }\n\n  const primaryName = generateFileName(model, term, param, hour, stat, pressureLevel, region);\n  if (primaryName) {\n    candidates.push(primaryName);\n  }\n\n  if (summaryModels.includes(model) && (param === \"24h_tp\" || param === \"24_tp\")) {\n    const paramToUse = buildSummaryParamName(model, term, param, pressureLevel, region);\n    const formattedHour = String(hour).padStart(2, \"0\");\n    appendDatedFileNameCandidates(\n      candidates,\n      paramToUse,\n      formattedHour,\n      getFileLookupDateSuffixes(model, term, param, hour)\n    );\n  }\n\n  return candidates;\n}\n\n\/\/ Funkcja do dynamicznego ustalania dost\u0119pnych godzin dla parametr\u00f3w\n\/\/ Zmienna do \u015bledzenia aktualnie sprawdzanych dost\u0119pno\u015bci godzin\nconst pendingHourChecks = {};\n\nasync function determineAvailableHours(model, term, param, region, options) {\n  \/\/ modele typu podsumowanie\n  if (!summaryModels.includes(model)) return;\n\n  options = options || {};\n\n  if (region === undefined) {\n    region = hasRegions(model, term, param) ? selectedRegion : null;\n  }\n  \n  \/\/ Stw\u00f3rz klucz do identyfikacji tego konkretnego sprawdzenia\n  const checkKey = `${model}_${term}_${param}_${region || \"none\"}`;\n  \n  \/\/ Sprawd\u017a czy takie samo sprawdzenie jest ju\u017c w toku\n  if (pendingHourChecks[checkKey]) {\n    return pendingHourChecks[checkKey]; \/\/ Zwr\u00f3\u0107 obietnic\u0119 z ju\u017c trwaj\u0105cego sprawdzania\n  }\n  \n  \/\/ Utw\u00f3rz now\u0105 obietnic\u0119 i zapisz j\u0105, aby unikn\u0105\u0107 duplikacji \u017c\u0105da\u0144\n  const promise = new Promise(async (resolve) => {\n    try {\n      const result = await performAvailableHoursCheck(model, term, param, region, options);\n      delete pendingHourChecks[checkKey];\n      resolve(result);\n    } catch (error) {\n      console.error(\"B\u0142\u0105d podczas sprawdzania dost\u0119pnych godzin:\", error);\n      delete pendingHourChecks[checkKey];\n      resolve([]);\n    }\n  });\n  \n  pendingHourChecks[checkKey] = promise;\n  \n  return promise;\n}\n\n\/\/ Funkcja wykonuj\u0105ca rzeczywiste sprawdzenie dost\u0119pnych godzin\nasync function performAvailableHoursCheck(model, term, param, region, options) {\n  options = options || {};\n  const silent = options.silent === true;\n\n  if (region === undefined) {\n    region = hasRegions(model, term, param) ? selectedRegion : null;\n  }\n\n  clearHourDateLabels(model, term, param);\n\n  const hoursContainer = silent ? null : document.getElementById(\"hours\");\n  if (hoursContainer) {\n    hoursContainer.innerHTML = '<div class=\"loading-hours\">Sprawdzanie dost\u0119pnych godzin...<\/div>';\n  }\n  \n  const imagesContainer = silent ? null : document.getElementById(\"images\");\n  let loaderId = null;\n  if (imagesContainer) {\n    imagesContainer.innerHTML = \"\";\n    loaderId = \"loader-\" + new Date().getTime();\n    const loaderContainer = document.createElement(\"div\");\n    loaderContainer.className = \"loader-container\";\n    loaderContainer.id = loaderId;\n    const loader = document.createElement(\"div\");\n    loader.className = \"loader\";\n    loaderContainer.appendChild(loader);\n    imagesContainer.appendChild(loaderContainer);\n    \n    setTimeout(function() {\n      const loaderElement = document.getElementById(loaderId);\n      if (loaderElement) {\n        imagesContainer.innerHTML = \"\";\n\n        const errorImg = document.createElement(\"img\");\n        errorImg.src = \"\/wp-content\/uploads\/production\/prognozy_wiazkowe\/configuration_not.png\";\n        errorImg.alt = \"Konfiguracja niedost\u0119pna\";\n        errorImg.style.maxWidth = \"100%\";\n        errorImg.style.display = \"block\";\n        imagesContainer.appendChild(errorImg);\n        \n        const errorMsg = document.createElement(\"p\");\n        errorMsg.textContent = \"Brak dost\u0119pnych prognoz dla wybranej konfiguracji.\";\n        errorMsg.style.color = \"red\";\n        errorMsg.style.textAlign = \"center\";\n        imagesContainer.appendChild(errorMsg);\n      }\n    }, 5000);\n  }\n  \n  \/\/ Inicjalizacja struktury paramHours dla modelu, je\u015bli nie istnieje\n  if (!paramHours[model]) {\n    paramHours[model] = {};\n  }\n\n  if (term === \"12h\" && summaryModels.includes(model)) {\n    const datedHours = await performAvailable12hHoursBySuffix(model, term, param, region);\n\n    if (loaderId) {\n      const loaderElement = document.getElementById(loaderId);\n      if (loaderElement && loaderElement.parentNode) {\n        loaderElement.parentNode.removeChild(loaderElement);\n      }\n    }\n\n    return datedHours;\n  }\n\n  if ((term === \"1h\" || term === \"3h\") && shouldUseForecastDateLabels(model, term)) {\n    const datedHours = await performAvailableLeadTermHoursBySuffix(model, term, param, region);\n\n    if (loaderId) {\n      const loaderElement = document.getElementById(loaderId);\n      if (loaderElement && loaderElement.parentNode) {\n        loaderElement.parentNode.removeChild(loaderElement);\n      }\n    }\n\n    return datedHours;\n  }\n\n  \/\/ tablica wszystkich mo\u017cliwych godzin dla modelu i wariantu\n  let allPossibleHours = [];\n  if (forecastHours[model] && forecastHours[model][term]) {\n    allPossibleHours = forecastHours[model][term].map(h => String(h).padStart(2, \"0\"));\n  } else if (model === \"ecmwf_ifs_ens\" && term === \"3h\") {\n    for (let h = 9; h <= 72; h += 3) {\n      allPossibleHours.push(String(h).padStart(2, \"0\"));\n    }\n  } else {\n    allPossibleHours = Array.from({length: 10}, (_, i) => String(i + 1).padStart(2, \"0\"));\n  }\n  \n  \/\/ Optymalizacja: sprawdzanie godzin w BATCHACH (po 24 r\u00f3wnolegle z HEAD request)\n  const BATCH_SIZE = 24;\n  const availableHours = [];\n  \n  \/\/ Funkcja sprawdzaj\u0105ca pojedyncz\u0105 godzin\u0119\n  const checkSingleHour = async (hour) => {\n    const checkCandidatesForHour = async (hourToCheck) => {\n      const fileNames = generateFileNameCandidates(model, term, param, hourToCheck, null, null, region);\n      const basePath = `\/wp-content\/uploads\/production\/prognozy_wiazkowe\/${model}\/${term}\/`;\n      const fileName = await findFirstExistingFileName(basePath, fileNames);\n      if (fileName) {\n        const dateSuffix = parseDateSuffixFromFileName(fileName);\n        if (dateSuffix) {\n          rememberHourDateLabel(model, term, param, hourToCheck, dateSuffix);\n        }\n        return true;\n      }\n      return false;\n    };\n\n    \/\/ Sprawdzanie z wiod\u0105cym zerem\n    let hourAvailable = await checkCandidatesForHour(hour);\n    \n    \/\/ Je\u015bli nie znaleziono pliku z wiod\u0105cym zerem, spr\u00f3buj bez wiod\u0105cego zera\n    if (!hourAvailable && hour.startsWith(\"0\")) {\n      const hourWithoutLeadingZero = hour.replace(\/^0+\/, '');\n      hourAvailable = await checkCandidatesForHour(hourWithoutLeadingZero);\n    }\n    \n    return { hour, available: hourAvailable };\n  };\n  \n  \/\/ Przetwarzaj godziny w batchach z early stopping\n  let consecutiveMissing = 0;\n  const MAX_CONSECUTIVE_MISSING = 3; \/\/ Je\u015bli 3 z rz\u0119du brakuje, przestajemy sprawdza\u0107\n  \n  for (let i = 0; i < allPossibleHours.length; i += BATCH_SIZE) {\n    \/\/ Early stop je\u015bli za du\u017co kolejnych brakuj\u0105cych\n    if (consecutiveMissing >= MAX_CONSECUTIVE_MISSING) {\n      break;\n    }\n    \n    const batch = allPossibleHours.slice(i, i + BATCH_SIZE);\n    const batchResults = await Promise.all(batch.map(checkSingleHour));\n    \n    \/\/ Dodaj dost\u0119pne godziny z tego batcha i \u015bled\u017a kolejne braki\n    batchResults.forEach(result => {\n      if (result.available) {\n        availableHours.push(result.hour);\n        consecutiveMissing = 0; \/\/ Reset licznika\n      } else {\n        consecutiveMissing++;\n      }\n    });\n  }\n  \n  \/\/ Usu\u0144 animacj\u0119 \u0142adowania, je\u015bli wci\u0105\u017c istnieje\n  if (loaderId) {\n    const loaderElement = document.getElementById(loaderId);\n    if (loaderElement && loaderElement.parentNode) {\n      loaderElement.parentNode.removeChild(loaderElement);\n    }\n  }\n  \n  \/\/ u\u017cycie tylko znalezionych godzin\n  paramHours[model][param] = availableHours;\n  \n  return availableHours;\n}\n\n\/\/ Aktualnie wybrane opcje\nlet selectedModel = models[0];  \/\/ Domy\u015blny model\nlet selectedTerm = defaultSettings[selectedModel]?.term || \"12h\";       \/\/ Domy\u015blny termin startu\nlet selectedParam = defaultSettings[selectedModel]?.param || \"t2m_max\";     \/\/ Domy\u015blny parametr\nlet selectedHour = defaultSettings[selectedModel]?.hour || \"01\";        \/\/ Domy\u015blny termin prognozy\nlet selectedStat = defaultSettings[selectedModel]?.stat || null;     \/\/ Domy\u015blna statystyka\nlet selectedPressureLevel = null;\nlet selectedRegion = null; \/\/ null = ca\u0142a Polska, \"sudety\", \"tatry\", \"bieszczady\"\n\nlet compareMode = false;\nlet compareSelectedModels = [];\nlet compareFullscreen = false;\nlet compareListLayout = false;\nlet compareImageToken = 0;\nlet selectedModelBeforeCompare = null;\nlet compareModalTerm = \"12h\";\nlet compareModalParam = null;\n\nconst compareModalTerms = [\"1h\", \"12h\"];\n\nfunction getModelParams(model, term) {\n  if (summaryModels.includes(model)) {\n    return paramsForModels[model]?.[term] || [];\n  }\n  return paramsForModels[model] || [];\n}\n\nfunction modelSupportsConfig(model, term, param) {\n  const terms = summaryTermVariants[model] || [];\n  if (!terms.includes(term)) {\n    return false;\n  }\n  const params = getModelParams(model, term);\n  return resolveCompareEquivalentParam(param, params) !== null;\n}\n\nfunction modelSupportsCurrentConfig(model) {\n  return modelSupportsConfig(model, selectedTerm, selectedParam);\n}\n\nfunction getEligibleCompareModelIds(term, param) {\n  const termToUse = term != null ? term : selectedTerm;\n  const paramToUse = param != null ? param : selectedParam;\n  return models.filter(function(modelId) {\n    return modelSupportsConfig(modelId, termToUse, paramToUse);\n  });\n}\n\nfunction getParamEquivalenceKey(param) {\n  for (let i = 0; i < compareEquivalentParamGroups.length; i += 1) {\n    if (compareEquivalentParamGroups[i].indexOf(param) !== -1) {\n      return compareEquivalentParamGroups[i][0];\n    }\n  }\n  return param;\n}\n\nfunction getCompareParamOptions(term) {\n  const termToUse = term || compareModalTerm;\n  const optionsByKey = {};\n\n  models.forEach(function(modelId) {\n    const terms = summaryTermVariants[modelId] || [];\n    if (terms.indexOf(termToUse) === -1) {\n      return;\n    }\n    getModelParams(modelId, termToUse).forEach(function(paramId) {\n      const key = getParamEquivalenceKey(paramId);\n      if (!optionsByKey[key]) {\n        optionsByKey[key] = {\n          id: paramId,\n          key: key,\n          label: parameterDisplayNames[paramId] || paramId,\n          models: []\n        };\n      }\n      if (optionsByKey[key].models.indexOf(modelId) === -1) {\n        optionsByKey[key].models.push(modelId);\n      }\n      if (!parameterDisplayNames[optionsByKey[key].id] && parameterDisplayNames[paramId]) {\n        optionsByKey[key].id = paramId;\n        optionsByKey[key].label = parameterDisplayNames[paramId];\n      }\n    });\n  });\n\n  return Object.keys(optionsByKey)\n    .map(function(key) {\n      return optionsByKey[key];\n    })\n    .filter(function(option) {\n      return option.models.length >= 2;\n    })\n    .sort(function(a, b) {\n      return String(a.label).localeCompare(String(b.label), \"pl\");\n    });\n}\n\nfunction getActiveCompareModelIds() {\n  const eligible = getEligibleCompareModelIds();\n  return compareSelectedModels.filter(function(modelId) {\n    return eligible.indexOf(modelId) !== -1;\n  });\n}\n\nfunction canUseCompareFullscreen() {\n  return compareMode && getActiveCompareModelIds().length >= 2 && window.innerWidth > 768;\n}\n\nfunction exitCompareMode() {\n  compareMode = false;\n  compareSelectedModels = [];\n  compareListLayout = false;\n  compareImageToken += 1;\n  resetComparePreloadQueue();\n  closeCompareFullscreen();\n\n  if (selectedModelBeforeCompare) {\n    selectedModel = selectedModelBeforeCompare;\n    selectedModelBeforeCompare = null;\n  }\n\n  lastDisplayedImage = null;\n  isShowImagesRunning = false;\n  isShowImagesRunningTimestamp = 0;\n  pendingImageRequest = false;\n  const container = document.getElementById(\"images\");\n  if (container) {\n    container.innerHTML = \"\";\n  }\n\n  updateCompareButtons();\n  refreshModelsButtons();\n}\n\nfunction refreshModelsButtons() {\n  renderButtons(\"models\", models, window.__onModelSelected || function() {}, compareMode ? null : selectedModel, modelDisplayNames);\n}\n\nfunction canUseCompareListLayout() {\n  return compareMode && getActiveCompareModelIds().length >= 2;\n}\n\nfunction updateCompareButtons() {\n  const compareBtn = document.getElementById(\"compareBtn\");\n  const listBtn = document.getElementById(\"compareListBtn\");\n  const fullscreenBtn = document.getElementById(\"compareFullscreenBtn\");\n  const fullscreenShortcut = document.getElementById(\"compareFullscreenShortcutRow\");\n  const escShortcut = document.getElementById(\"compareEscShortcutRow\");\n  const actionsRow = document.querySelector(\".compare-actions-row\");\n  const showList = canUseCompareListLayout();\n  const showFullscreen = canUseCompareFullscreen();\n\n  if (actionsRow) {\n    actionsRow.classList.toggle(\"compare-actions-grid\", compareMode);\n  }\n  if (compareBtn) {\n    compareBtn.classList.toggle(\"active\", compareMode);\n    compareBtn.textContent = compareMode ? \"\u2715 Zako\u0144cz por\u00f3wnanie\" : \"\u229e Por\u00f3wnaj modele\";\n    \/\/ Modal sam filtruje parametry\/modele \u2014 przycisk zawsze dost\u0119pny poza trybem compare.\n    compareBtn.disabled = false;\n  }\n  if (listBtn) {\n    listBtn.style.display = showList ? \"block\" : \"none\";\n    listBtn.classList.toggle(\"active\", compareListLayout);\n    listBtn.textContent = compareListLayout ? \"\u229e Siatka\" : \"\u2630 Lista\";\n    listBtn.title = compareListLayout\n      ? \"Poka\u017c modele obok siebie\"\n      : \"Poka\u017c modele jeden pod drugim\";\n  }\n  if (fullscreenBtn) {\n    fullscreenBtn.style.display = showFullscreen ? \"block\" : \"none\";\n    fullscreenBtn.classList.toggle(\"active\", compareFullscreen);\n    fullscreenBtn.textContent = compareFullscreen ? \"\u2715 Pe\u0142ny ekran\" : \"\u26f6 Pe\u0142ny ekran\";\n  }\n  if (fullscreenShortcut) {\n    fullscreenShortcut.style.display = showFullscreen ? \"inline\" : \"none\";\n  }\n  if (escShortcut) {\n    escShortcut.style.display = compareFullscreen ? \"inline\" : \"none\";\n  }\n}\n\nfunction closeCompareModal() {\n  const modal = document.getElementById(\"wiazkoweCompareModal\");\n  if (modal) {\n    modal.style.display = \"none\";\n  }\n}\n\nfunction updateCompareModalConfirm() {\n  const confirmBtn = document.getElementById(\"wiazkoweCompareConfirm\");\n  if (!confirmBtn) {\n    return;\n  }\n  const count = compareSelectedModels.length;\n  if (count >= 2) {\n    confirmBtn.disabled = false;\n    if (compareMode) {\n      confirmBtn.textContent = \"Zastosuj\";\n    } else if (count === 2) {\n      confirmBtn.textContent = \"Por\u00f3wnaj 2 modele\";\n    } else {\n      confirmBtn.textContent = \"Por\u00f3wnaj \" + count + \" modele\";\n    }\n    return;\n  }\n  confirmBtn.disabled = true;\n  confirmBtn.textContent = count < 1 ? \"Wybierz modele do por\u00f3wnania\" : \"Wybierz co najmniej 2 modele\";\n}\n\nfunction renderCompareTermPicker() {\n  const wrap = document.getElementById(\"wiazkoweCompareTerms\");\n  if (!wrap) {\n    return;\n  }\n  wrap.innerHTML = \"\";\n\n  compareModalTerms.forEach(function(term) {\n    const btn = document.createElement(\"button\");\n    btn.type = \"button\";\n    btn.textContent = term;\n    btn.dataset.value = term;\n    if (term === compareModalTerm) {\n      btn.classList.add(\"active\");\n    }\n    btn.addEventListener(\"click\", function() {\n      if (compareModalTerm === term) {\n        return;\n      }\n      compareModalTerm = term;\n      compareSelectedModels = [];\n      compareModalParam = null;\n      renderCompareTermPicker();\n      renderCompareParamPicker();\n      renderCompareModelPicker();\n    });\n    wrap.appendChild(btn);\n  });\n}\n\nfunction renderCompareParamPicker() {\n  const wrap = document.getElementById(\"wiazkoweCompareParams\");\n  if (!wrap) {\n    return;\n  }\n  wrap.innerHTML = \"\";\n\n  const options = getCompareParamOptions(compareModalTerm);\n  if (!options.length) {\n    const empty = document.createElement(\"div\");\n    empty.style.padding = \"16px\";\n    empty.style.color = \"#666\";\n    empty.textContent = \"Brak parametr\u00f3w wsp\u00f3lnych dla co najmniej dw\u00f3ch modeli w tym zestawieniu.\";\n    wrap.appendChild(empty);\n    compareModalParam = null;\n    updateCompareModalConfirm();\n    return;\n  }\n\n  const optionIds = options.map(function(option) {\n    return option.id;\n  });\n  if (!compareModalParam || optionIds.indexOf(compareModalParam) === -1) {\n    const preferred = options.find(function(option) {\n      return getParamEquivalenceKey(option.id) === getParamEquivalenceKey(selectedParam) ||\n        option.id === selectedParam;\n    });\n    compareModalParam = preferred ? preferred.id : options[0].id;\n  }\n\n  options.forEach(function(option) {\n    const item = document.createElement(\"div\");\n    item.className = \"compare-param-item\";\n    item.textContent = option.label;\n    item.dataset.param = option.id;\n    if (option.id === compareModalParam) {\n      item.classList.add(\"selected\");\n    }\n    item.addEventListener(\"click\", function() {\n      if (compareModalParam === option.id) {\n        return;\n      }\n      compareModalParam = option.id;\n      compareSelectedModels = [];\n      renderCompareParamPicker();\n      renderCompareModelPicker();\n    });\n    wrap.appendChild(item);\n  });\n}\n\nfunction renderCompareModelPicker() {\n  const wrap = document.getElementById(\"wiazkoweCompareList\");\n  if (!wrap) {\n    return;\n  }\n  wrap.innerHTML = \"\";\n\n  if (!compareModalParam) {\n    updateCompareModalConfirm();\n    return;\n  }\n\n  const eligible = getEligibleCompareModelIds(compareModalTerm, compareModalParam);\n  compareSelectedModels = compareSelectedModels.filter(function(modelId) {\n    return eligible.indexOf(modelId) !== -1;\n  });\n\n  if (eligible.length < 2) {\n    const noModelsMsg = document.createElement(\"div\");\n    noModelsMsg.style.padding = \"20px\";\n    noModelsMsg.style.textAlign = \"center\";\n    noModelsMsg.style.color = \"#666\";\n    noModelsMsg.textContent = \"Por\u00f3wnanie modeli nie jest mo\u017cliwe dla tego parametru \u2014 mniej ni\u017c dwa modele maj\u0105 ten parametr.\";\n    wrap.appendChild(noModelsMsg);\n    updateCompareModalConfirm();\n    return;\n  }\n\n  if (!compareSelectedModels.length) {\n    if (eligible.indexOf(selectedModel) !== -1 &&\n        modelSupportsConfig(selectedModel, compareModalTerm, compareModalParam)) {\n      compareSelectedModels.push(selectedModel);\n    }\n    if (compareSelectedModels.length < 2) {\n      for (let i = 0; i < eligible.length && compareSelectedModels.length < 2; i += 1) {\n        if (compareSelectedModels.indexOf(eligible[i]) === -1) {\n          compareSelectedModels.push(eligible[i]);\n        }\n      }\n    }\n  }\n\n  eligible.forEach(function(modelId) {\n    const item = document.createElement(\"div\");\n    item.className = \"compare-model-item\";\n    item.textContent = modelDisplayNames[modelId] || modelId;\n    item.dataset.model = modelId;\n    if (compareSelectedModels.indexOf(modelId) !== -1) {\n      item.classList.add(\"selected\");\n    }\n    item.addEventListener(\"click\", function() {\n      const index = compareSelectedModels.indexOf(modelId);\n      if (index !== -1) {\n        compareSelectedModels.splice(index, 1);\n      } else {\n        compareSelectedModels.push(modelId);\n      }\n      renderCompareModelPicker();\n    });\n    wrap.appendChild(item);\n  });\n\n  updateCompareModalConfirm();\n}\n\nfunction openCompareModal() {\n  compareModalTerm = compareModalTerms.indexOf(selectedTerm) !== -1 ? selectedTerm : \"12h\";\n  compareModalParam = selectedParam;\n\n  const options = getCompareParamOptions(compareModalTerm);\n  if (!options.length) {\n    const fallbackTerm = compareModalTerms.find(function(term) {\n      return getCompareParamOptions(term).length > 0;\n    });\n    if (!fallbackTerm) {\n      return;\n    }\n    compareModalTerm = fallbackTerm;\n    compareModalParam = null;\n  }\n\n  renderCompareTermPicker();\n  renderCompareParamPicker();\n  renderCompareModelPicker();\n  updateCompareModalConfirm();\n  document.getElementById(\"wiazkoweCompareModal\").style.display = \"flex\";\n}\n\nfunction applyCompareModalSelection() {\n  if (compareSelectedModels.length < 2 || !compareModalParam) {\n    return false;\n  }\n\n  selectedTerm = compareModalTerm;\n  selectedModel = compareSelectedModels[0];\n  const modelParams = getModelParams(selectedModel, selectedTerm);\n  selectedParam = resolveCompareEquivalentParam(compareModalParam, modelParams) || compareModalParam;\n\n  return true;\n}\n\nfunction toggleCompareMode() {\n  if (document.getElementById(\"wiazkoweCompareModal\").style.display === \"flex\") {\n    closeCompareModal();\n    return;\n  }\n  if (compareMode) {\n    exitCompareMode();\n    renderParams();\n    renderForecastHours();\n    showImages();\n    return;\n  }\n  openCompareModal();\n}\n\nfunction toggleModelInComparison(modelId) {\n  const eligible = getEligibleCompareModelIds();\n  if (eligible.indexOf(modelId) === -1) {\n    return;\n  }\n\n  const index = compareSelectedModels.indexOf(modelId);\n  if (index !== -1) {\n    if (compareSelectedModels.length <= 2) {\n      return;\n    }\n    compareSelectedModels.splice(index, 1);\n  } else {\n    compareSelectedModels.push(modelId);\n  }\n\n  if (compareSelectedModels.indexOf(selectedModel) === -1) {\n    selectedModel = compareSelectedModels[0];\n  }\n\n  refreshModelsButtons();\n  if (compareMode) {\n    refreshViewAfterConfigChange();\n    return;\n  }\n  showImages();\n}\n\nfunction getForecastHoursForModel(model, term, param) {\n  let hours = [];\n\n  if (summaryModels.includes(model) && paramHours[model] && Array.isArray(paramHours[model][param])) {\n    hours = paramHours[model][param];\n  } else if (forecastHours[model] && forecastHours[model][term]) {\n    hours = forecastHours[model][term];\n  } else {\n    hours = forecastHours[\"default\"];\n  }\n\n  return hours.map(function(h) {\n    return String(h).padStart(2, \"0\");\n  });\n}\n\nfunction getCompareReferenceModelId() {\n  const compareModels = (selectedRegion ? getVisibleCompareModelIds() : getActiveCompareModelIds());\n  const modelsForRef = compareModels.length ? compareModels : getActiveCompareModelIds();\n  if (!modelsForRef.length) {\n    return selectedModel;\n  }\n\n  let refModel = modelsForRef[0];\n  let maxLen = 0;\n  modelsForRef.forEach(function(modelId) {\n    const param = getCompareParamForModel(modelId);\n    const hours = getForecastHoursForModel(modelId, selectedTerm, param);\n    if (hours.length > maxLen) {\n      maxLen = hours.length;\n      refModel = modelId;\n    }\n  });\n  return refModel;\n}\n\nfunction getCompareForecastHoursList() {\n  const refModel = getCompareReferenceModelId();\n  const refParam = getCompareParamForModel(refModel) || selectedParam;\n  return getForecastHoursForModel(refModel, selectedTerm, refParam);\n}\n\nfunction getCompareReferenceDateSuffix(referenceHour) {\n  const refModel = getCompareReferenceModelId();\n  const refParam = getCompareParamForModel(refModel);\n  const hourNorm = String(referenceHour).padStart(2, \"0\");\n  return getHourDateSuffix(refModel, selectedTerm, refParam, hourNorm);\n}\n\nfunction getCompareHourForModel(model, referenceHour) {\n  const refModel = getCompareReferenceModelId();\n  const hourNorm = String(referenceHour).padStart(2, \"0\");\n  const param = getCompareParamForModel(model);\n  if (!param) {\n    return null;\n  }\n\n  const availableHours = getForecastHoursForModel(model, selectedTerm, param);\n  if (!availableHours.length) {\n    return null;\n  }\n\n  if (model === refModel) {\n    return availableHours.indexOf(hourNorm) !== -1 ? hourNorm : null;\n  }\n\n  const refParam = getCompareParamForModel(refModel);\n  const context = captureHourSelectionContext(refModel, selectedTerm, refParam, hourNorm);\n\n  if (context.suffix) {\n    for (let i = 0; i < availableHours.length; i += 1) {\n      const hourValue = availableHours[i];\n      const hourSuffix = getHourDateSuffix(model, selectedTerm, param, hourValue);\n      if (hourSuffix === context.suffix) {\n        return hourValue;\n      }\n      const stored = getStoredHourDateLabel(model, selectedTerm, param, hourValue);\n      if (stored && stored.suffix === context.suffix) {\n        return hourValue;\n      }\n    }\n    return null;\n  }\n\n  if (context.shortLabel) {\n    for (let i = 0; i < availableHours.length; i += 1) {\n      const hourValue = availableHours[i];\n      if (getHourDisplayLabel(hourValue, model, selectedTerm, param) === context.shortLabel) {\n        return hourValue;\n      }\n    }\n  }\n\n  return availableHours.indexOf(hourNorm) !== -1 ? hourNorm : null;\n}\n\nfunction isHourAvailableForCompareModel(model, referenceHour) {\n  return getCompareHourForModel(model, referenceHour) !== null;\n}\n\nfunction ensureCompareModelsHoursLoaded(options) {\n  options = options || {};\n  const forceReload = options.forceReload === true;\n  const compareModels = getActiveCompareModelIds();\n\n  return Promise.all(compareModels.map(function(modelId) {\n    const param = getCompareParamForModel(modelId);\n    if (!param) {\n      return Promise.resolve([]);\n    }\n\n    if (forceReload) {\n      if (paramHours[modelId]) {\n        delete paramHours[modelId][param];\n      }\n      clearHourDateLabels(modelId, selectedTerm, param);\n      clearTermDateSuffixCacheForModel(modelId);\n    } else {\n      const existing = paramHours[modelId] && paramHours[modelId][param];\n      if (existing && existing.length > 0) {\n        return Promise.resolve(existing);\n      }\n    }\n\n    const region = getCompareRegionForModel(modelId, param);\n    \/\/ Ciche skanowanie \u2014 r\u00f3wnoleg\u0142e \u0142adowanie nie mo\u017ce nadpisywa\u0107 #hours\/#images.\n    return determineAvailableHours(modelId, selectedTerm, param, region, { silent: true });\n  }));\n}\n\nfunction showCompareConfigLoading() {\n  const hoursContainer = document.getElementById(\"hours\");\n  if (hoursContainer) {\n    hoursContainer.innerHTML = '<div class=\"loading-hours\">Aktualizacja por\u00f3wnania...<\/div>';\n  }\n  const imagesContainer = document.getElementById(\"images\");\n  if (imagesContainer) {\n    imagesContainer.innerHTML = \"\";\n    const loaderContainer = document.createElement(\"div\");\n    loaderContainer.className = \"loader-container\";\n    const loader = document.createElement(\"div\");\n    loader.className = \"loader\";\n    loaderContainer.appendChild(loader);\n    loaderContainer.appendChild(document.createElement(\"p\")).textContent = \"\u0141adowanie por\u00f3wnania...\";\n    imagesContainer.appendChild(loaderContainer);\n  }\n}\n\nfunction clearCompareModelsHoursCache() {\n  compareSelectedModels.forEach(function(modelId) {\n    const modelParam = getCompareParamForModel(modelId) || selectedParam;\n    if (paramHours[modelId]) {\n      if (modelParam) {\n        delete paramHours[modelId][modelParam];\n      }\n      delete paramHours[modelId][selectedParam];\n    }\n    if (modelParam) {\n      clearHourDateLabels(modelId, selectedTerm, modelParam);\n    }\n    clearHourDateLabels(modelId, selectedTerm, selectedParam);\n    clearTermDateSuffixCacheForModel(modelId);\n  });\n}\n\nfunction reloadCompareAfterConfigChange(options) {\n  options = options || {};\n  if (!compareMode || getActiveCompareModelIds().length < 2) {\n    refreshViewAfterConfigChange(options);\n    return;\n  }\n\n  resetComparePreloadQueue();\n  compareImageToken += 1;\n  showCompareConfigLoading();\n\n  ensureCompareModelsHoursLoaded({ forceReload: options.forceHoursReload !== false })\n    .then(function() {\n      const refModel = getCompareReferenceModelId();\n      const refParam = getCompareParamForModel(refModel) || selectedParam;\n      const hours = getForecastHoursForModel(refModel, selectedTerm, refParam);\n      if (hours.length && hours.indexOf(String(selectedHour).padStart(2, \"0\")) === -1) {\n        selectedHour = hours[0];\n      }\n      renderRegions();\n      renderForecastHours();\n      refreshViewAfterConfigChange({ skipHoursReload: true });\n    });\n}\n\nfunction handleCompareTermChange(variant) {\n  selectedTerm = variant;\n  selectedRegion = null;\n  selectedParam = resolveParamForModelSwitch(selectedParam, selectedModel, variant);\n  clearCompareModelsHoursCache();\n  resetComparePreloadQueue();\n  document.getElementById(\"hours\").innerHTML = \"\";\n  document.getElementById(\"images\").innerHTML = \"\";\n  renderParams();\n  reloadCompareAfterConfigChange({ forceHoursReload: true });\n}\n\nfunction getCompareParamForModel(model) {\n  return resolveCompareEquivalentParam(selectedParam, getModelParams(model, selectedTerm));\n}\n\nfunction modelSupportsCompareRegion(model, term, param, region) {\n  if (!region) {\n    return true;\n  }\n  const modelParam = resolveCompareEquivalentParam(param, getModelParams(model, term)) || param;\n  return hasRegions(model, term, modelParam);\n}\n\nfunction getCompareRegionForModel(model, param) {\n  if (!hasRegions(model, selectedTerm, param)) {\n    return null;\n  }\n  return selectedRegion;\n}\n\nfunction getVisibleCompareModelIds() {\n  return getActiveCompareModelIds().filter(function(modelId) {\n    const param = getCompareParamForModel(modelId);\n    if (!param) {\n      return false;\n    }\n    return modelSupportsCompareRegion(modelId, selectedTerm, param, selectedRegion);\n  });\n}\n\nfunction getCompareModelsHiddenByRegion() {\n  if (!selectedRegion) {\n    return [];\n  }\n  return getActiveCompareModelIds().filter(function(modelId) {\n    const param = getCompareParamForModel(modelId);\n    return !param || !modelSupportsCompareRegion(modelId, selectedTerm, param, selectedRegion);\n  });\n}\n\nfunction getCompareImageCacheKey(model, referenceHour) {\n  const refSuffix = getCompareReferenceDateSuffix(referenceHour != null ? referenceHour : selectedHour);\n  const timeKey = refSuffix || String(referenceHour != null ? referenceHour : selectedHour).padStart(2, \"0\");\n  const param = getCompareParamForModel(model);\n  const region = getCompareRegionForModel(model, param);\n  return `cmp_${model}_${selectedTerm}_${param}_${timeKey}_${region || \"none\"}`;\n}\n\nfunction isCompareHourImageLoaded(model, hour) {\n  const cacheKey = getCompareImageCacheKey(model, hour);\n  const cached = imageCache[cacheKey];\n  return !!(cached && cached.complete && cached.naturalWidth > 0);\n}\n\nfunction ensureCompareModelHourImage(model, referenceHour) {\n  const referenceHourNorm = String(referenceHour).padStart(2, \"0\");\n  const modelHour = getCompareHourForModel(model, referenceHourNorm);\n  const param = getCompareParamForModel(model);\n\n  if (!param || !modelHour) {\n    return Promise.resolve(null);\n  }\n\n  const cacheKey = getCompareImageCacheKey(model, referenceHourNorm);\n  if (isCompareHourImageLoaded(model, referenceHourNorm)) {\n    return Promise.resolve(imageCache[cacheKey]);\n  }\n\n  if (imageLoadPromises[cacheKey]) {\n    return imageLoadPromises[cacheKey];\n  }\n\n  const region = getCompareRegionForModel(model, param);\n  const candidates = generateFileNameCandidates(model, selectedTerm, param, modelHour, null, null, region);\n  const basePath = `\/wp-content\/uploads\/production\/prognozy_wiazkowe\/${model}\/${selectedTerm}\/`;\n\n  imageLoadPromises[cacheKey] = new Promise(function(resolve, reject) {\n    let candidateIndex = 0;\n    const img = new Image();\n\n    function tryNextCandidate() {\n      if (candidateIndex >= candidates.length) {\n        delete imageLoadPromises[cacheKey];\n        refreshPreloadUi();\n        reject(new Error(\"Nie uda\u0142o si\u0119 za\u0142adowa\u0107 obrazu por\u00f3wnania dla terminu \" + modelHour));\n        return;\n      }\n\n      img.onload = function() {\n        imageCache[cacheKey] = img.cloneNode(true);\n        trimImageCacheIfNeeded();\n        const dateSuffix = parseDateSuffixFromFileName(candidates[candidateIndex]);\n        if (dateSuffix) {\n          rememberHourDateLabel(model, selectedTerm, param, modelHour, dateSuffix);\n        }\n        delete imageLoadPromises[cacheKey];\n        refreshPreloadUi();\n        resolve(imageCache[cacheKey]);\n      };\n\n      img.onerror = function() {\n        candidateIndex += 1;\n        tryNextCandidate();\n      };\n\n      img.src = basePath + candidates[candidateIndex];\n    }\n\n    tryNextCandidate();\n  });\n\n  return imageLoadPromises[cacheKey];\n}\n\nfunction loadCompareModelImage(model) {\n  return new Promise(function(resolve) {\n    const param = getCompareParamForModel(model);\n    const label = modelDisplayNames[model] || model;\n    const referenceHour = String(selectedHour).padStart(2, \"0\");\n\n    if (!param) {\n      resolve({ modelId: model, label: label, img: null, outOfRange: false });\n      return;\n    }\n\n    if (!getCompareHourForModel(model, referenceHour)) {\n      resolve({ modelId: model, label: label, img: null, outOfRange: true });\n      return;\n    }\n\n    ensureCompareModelHourImage(model, referenceHour).then(function(img) {\n      resolve({ modelId: model, label: label, img: img, outOfRange: false });\n    }).catch(function() {\n      resolve({ modelId: model, label: label, img: null, outOfRange: false });\n    });\n  });\n}\n\nfunction getAverageCompareImageAspectRatio(results) {\n  let sum = 0;\n  let count = 0;\n\n  (results || []).forEach(function(result) {\n    const img = result && result.img;\n    if (img && img.naturalWidth > 0 && img.naturalHeight > 0) {\n      sum += img.naturalWidth \/ img.naturalHeight;\n      count += 1;\n    }\n  });\n\n  return count ? sum \/ count : 0;\n}\n\nfunction computeCompareFullscreenColumns(itemCount, results) {\n  const padding = 24;\n  const gap = 10;\n  const minCellWidth = 220;\n  const minCellHeight = 140;\n  const vw = Math.max(320, window.innerWidth - padding);\n  const vh = Math.max(240, window.innerHeight - padding);\n  const aspectRatio = getAverageCompareImageAspectRatio(results) || 1.35;\n  const maxColsByWidth = Math.floor((vw + gap) \/ (minCellWidth + gap));\n  const maxCols = Math.max(1, Math.min(itemCount, maxColsByWidth, 3));\n  let bestCols = Math.min(2, itemCount);\n  let bestScore = -1;\n\n  if (itemCount <= 1) {\n    return 1;\n  }\n\n  for (let cols = 1; cols <= maxCols; cols += 1) {\n    const rows = Math.ceil(itemCount \/ cols);\n    const cellW = (vw - gap * (cols - 1)) \/ cols;\n    const cellH = (vh - gap * (rows - 1)) \/ rows;\n\n    if (cellW < minCellWidth * 0.8 || cellH < minCellHeight * 0.8) {\n      continue;\n    }\n\n    let fitW = Math.min(cellW, cellH * aspectRatio);\n    let fitH = fitW \/ aspectRatio;\n    if (fitH > cellH) {\n      fitH = cellH;\n      fitW = fitH * aspectRatio;\n    }\n\n    const area = fitW * fitH;\n    const wastePenalty = itemCount \/ (cols * rows);\n    \/\/ Lekka preferencja 3 kolumn przy 3 lub 6 modelach na szerokim ekranie.\n    const preferredLayoutBonus = ((itemCount === 3 || itemCount === 6) && cols === 3) ? 1.08 : 1;\n    const score = area * wastePenalty * preferredLayoutBonus;\n\n    if (score > bestScore) {\n      bestScore = score;\n      bestCols = cols;\n    }\n  }\n\n  return bestCols;\n}\n\nfunction appendCompareGrid(container, results, options) {\n  options = options || {};\n  const showLabels = options.showLabels === true;\n  const isFullscreen = options.fullscreen === true;\n\n  container.innerHTML = \"\";\n\n  const visibleResults = (results || []).filter(function(result) {\n    return !!result.img && !result.outOfRange;\n  });\n  const outOfRangeResults = (results || []).filter(function(result) {\n    return !!result.outOfRange;\n  });\n  const missingResults = (results || []).filter(function(result) {\n    return !result.outOfRange && !result.img;\n  });\n\n  const layout = document.createElement(\"div\");\n  layout.className = \"compare-layout\";\n\n  const bannerParts = [];\n  const regionHiddenModels = getCompareModelsHiddenByRegion();\n  if (regionHiddenModels.length) {\n    const regionName = (regions.find(function(region) {\n      return region.id === selectedRegion;\n    }) || {}).name || selectedRegion;\n    bannerParts.push(\n      \"Bez obszaru \\\"\" + regionName + \"\\\": \" +\n      regionHiddenModels.map(function(modelId) {\n        return modelDisplayNames[modelId] || modelId;\n      }).join(\", \")\n    );\n  }\n  if (outOfRangeResults.length) {\n    bannerParts.push(\n      \"Poza zasi\u0119giem prognozy: \" +\n      outOfRangeResults.map(function(result) {\n        return result.label || modelDisplayNames[result.modelId] || result.modelId;\n      }).join(\", \")\n    );\n  }\n  if (missingResults.length) {\n    bannerParts.push(\n      \"Brak mapy: \" +\n      missingResults.map(function(result) {\n        return result.label || modelDisplayNames[result.modelId] || result.modelId;\n      }).join(\", \")\n    );\n  }\n  if (bannerParts.length) {\n    const banner = document.createElement(\"div\");\n    banner.className = \"compare-out-of-range-banner\";\n    banner.textContent = bannerParts.join(\" \u00b7 \");\n    layout.appendChild(banner);\n  }\n\n  if (!visibleResults.length) {\n    const msg = document.createElement(\"p\");\n    msg.textContent = results && results.length\n      ? \"Brak modeli z map\u0105 dla wybranego terminu.\"\n      : (regionHiddenModels.length\n        ? \"\u017baden z por\u00f3wnywanych modeli nie ma wybranego obszaru.\"\n        : \"Brak dost\u0119pnych map do por\u00f3wnania dla wybranej konfiguracji.\");\n    msg.style.textAlign = \"center\";\n    msg.style.color = isFullscreen ? \"#ddd\" : \"#c00\";\n    msg.style.margin = \"24px 0\";\n    layout.appendChild(msg);\n    container.appendChild(layout);\n    return;\n  }\n\n  const grid = document.createElement(\"div\");\n  const useListLayout = !isFullscreen && compareListLayout;\n  grid.className = \"compare-grid-side\" + (useListLayout ? \" compare-grid-list\" : \"\");\n\n  const columnCount = isFullscreen\n    ? computeCompareFullscreenColumns(visibleResults.length, visibleResults)\n    : (useListLayout\n      ? 1\n      : Math.min(visibleResults.length, visibleResults.length <= 2 ? visibleResults.length : 2));\n  const rowCount = Math.ceil(visibleResults.length \/ columnCount);\n  grid.style.gridTemplateColumns = \"repeat(\" + columnCount + \", minmax(0, 1fr))\";\n  if (isFullscreen) {\n    grid.style.gridTemplateRows = \"repeat(\" + rowCount + \", minmax(0, 1fr))\";\n  }\n\n  visibleResults.forEach(function(result) {\n    const card = document.createElement(\"div\");\n    card.className = \"compare-card\";\n\n    if (result.label && showLabels) {\n      const caption = document.createElement(\"div\");\n      caption.className = \"compare-card-label\";\n      caption.textContent = result.label;\n      card.appendChild(caption);\n    }\n\n    const displayImg = new Image();\n    displayImg.src = result.img.currentSrc || result.img.src;\n    displayImg.alt = showLabels && result.label ? result.label : \"Por\u00f3wnanie prognozy\";\n    card.appendChild(enableImageZoomClick(displayImg, displayImg.alt));\n\n    grid.appendChild(card);\n  });\n\n  layout.appendChild(grid);\n  container.appendChild(layout);\n}\n\nfunction closeCompareFullscreen() {\n  if (!compareFullscreen) {\n    return;\n  }\n  compareFullscreen = false;\n  const overlay = document.getElementById(\"wiazkoweCompareFullscreen\");\n  const grid = document.getElementById(\"wiazkoweCompareFullscreenGrid\");\n  if (overlay) {\n    overlay.classList.remove(\"active\");\n  }\n  if (grid) {\n    grid.innerHTML = \"\";\n  }\n  updateCompareButtons();\n}\n\nfunction renderCompareFullscreen() {\n  if (!compareFullscreen || !canUseCompareFullscreen()) {\n    closeCompareFullscreen();\n    return;\n  }\n\n  const grid = document.getElementById(\"wiazkoweCompareFullscreenGrid\");\n  const selectedForCompare = getVisibleCompareModelIds();\n  if (!grid) {\n    return;\n  }\n  if (!selectedForCompare.length) {\n    grid.innerHTML = \"\";\n    appendCompareGrid(grid, [], { fullscreen: true });\n    return;\n  }\n\n  Promise.all(selectedForCompare.map(loadCompareModelImage)).then(function(results) {\n    if (!compareFullscreen) {\n      return;\n    }\n    appendCompareGrid(grid, results, { fullscreen: true });\n  });\n}\n\nfunction openCompareFullscreen() {\n  if (!canUseCompareFullscreen()) {\n    return;\n  }\n  compareFullscreen = true;\n  const overlay = document.getElementById(\"wiazkoweCompareFullscreen\");\n  if (overlay) {\n    overlay.classList.add(\"active\");\n  }\n  updateCompareButtons();\n  renderCompareFullscreen();\n}\n\nfunction toggleCompareFullscreen() {\n  if (compareFullscreen) {\n    closeCompareFullscreen();\n    return;\n  }\n  openCompareFullscreen();\n}\n\nfunction toggleCompareListLayout() {\n  if (!canUseCompareListLayout()) {\n    return;\n  }\n  compareListLayout = !compareListLayout;\n  updateCompareButtons();\n  if (compareMode && getActiveCompareModelIds().length >= 2) {\n    showCompareImages();\n  }\n}\n\nfunction showCompareImages() {\n  const container = document.getElementById(\"images\");\n  if (!container) {\n    return;\n  }\n\n  if (getActiveCompareModelIds().length < 2) {\n    exitCompareMode();\n    showImages();\n    return;\n  }\n\n  const selectedForCompare = getVisibleCompareModelIds();\n  const token = ++compareImageToken;\n  updateCompareButtons();\n  container.innerHTML = \"\";\n\n  if (!selectedForCompare.length) {\n    appendCompareGrid(container, []);\n    renderCompareFullscreen();\n    return;\n  }\n\n  const loader = document.createElement(\"div\");\n  loader.className = \"loader-container\";\n  loader.innerHTML = '<div class=\"loader\"><\/div><p>\u0141adowanie por\u00f3wnania...<\/p>';\n  container.appendChild(loader);\n\n  Promise.all(selectedForCompare.map(loadCompareModelImage)).then(function(results) {\n    if (token !== compareImageToken) {\n      return;\n    }\n    appendCompareGrid(container, results);\n    renderCompareFullscreen();\n  });\n}\n\nfunction validateCompareMode() {\n  if (!compareMode) {\n    return true;\n  }\n  if (getActiveCompareModelIds().length < 2) {\n    exitCompareMode();\n    return false;\n  }\n  return true;\n}\n\nfunction refreshViewAfterConfigChange(options) {\n  options = options || {};\n\n  if (compareMode) {\n    if (!validateCompareMode()) {\n      renderForecastHours();\n      showImages();\n      return;\n    }\n\n    compareImageToken += 1;\n    resetComparePreloadQueue();\n    refreshModelsButtons();\n    updateCompareButtons();\n\n    const finalize = function() {\n      renderForecastHours();\n      showCompareImages();\n      preloadCompareForecastImages();\n    };\n\n    if (options.skipHoursReload) {\n      finalize();\n      return;\n    }\n\n    ensureCompareModelsHoursLoaded().then(finalize);\n    return;\n  }\n\n  if (options.showImages !== false) {\n    showImages();\n  }\n  if (options.preload !== false) {\n    preloadForecastImages();\n  }\n}\n\n\/\/ Zmienna do obs\u0142ugi animacji \u0142adowania\nlet loaderTimeout = null;\n\n\/\/ Funkcja pod\u015bwietlaj\u0105ca aktywny przycisk\nfunction setActiveButton(container, activeValue) {\n  if (activeValue === undefined || activeValue === null) return;\n  \n  \/\/ Konwertuj activeValue na string\n  let activeValueStr = String(activeValue).trim();\n  \n  \/\/ Usu\u0144 suffix \"h\" je\u015bli istnieje (dla przycisku godziny)\n  if (activeValueStr.endsWith('h')) {\n    activeValueStr = activeValueStr.substring(0, activeValueStr.length - 1);\n  }\n  \n  const buttons = document.querySelectorAll(`#${container} button`);\n  buttons.forEach(btn => {\n    const btnValue = btn.dataset.value !== undefined\n      ? String(btn.dataset.value).trim()\n      : String(btn.textContent).trim();\n    \n    const isMatch = btnValue === activeValueStr ||\n      (!\/^\\d{10}$\/.test(activeValueStr) &&\n        String(btnValue).padStart(2, \"0\") === String(activeValueStr).padStart(2, \"0\"));\n    \n    if (isMatch) {\n      btn.classList.add('active');\n    } else {\n      btn.classList.remove('active');\n    }\n  });\n}\n\n\/\/ 1. Renderowanie modeli\nrenderButtons(\"models\", models, (model) => {\n  const prevModel = selectedModel;\n  let prevTerm = selectedTerm;\n  let prevParam = isParameterWithPressureLevel(selectedParam) ? getBaseParam(selectedParam) : selectedParam;\n  let prevHour = selectedHour;\n  let prevStat = selectedStat;\n  let prevPressureLevel = selectedPressureLevel;\n\n  if (prevModel !== model) {\n    hourSelectionPreserveContext = captureHourSelectionContext(\n      prevModel,\n      prevTerm,\n      prevParam,\n      prevHour\n    );\n  }\n  \n  selectedModel = model;\n  \n  setActiveButton(\"models\", model);\n  \n  \/\/ Sprawd\u017a czy zmieniamy typ modelu (podsumowanie \u2194 zwyk\u0142y)\n  const prevModelWasSummary = summaryModels.includes(prevModel);\n  const newModelIsSummary = summaryModels.includes(model);\n  const changingModelType = prevModelWasSummary !== newModelIsSummary;\n  \n  selectedTerm = resolveTermForModelSwitch(prevTerm, model);\n  \n  \/\/ Sprawd\u017a czy poprzedni parametr jest dost\u0119pny w nowym modelu\n  \/\/ Pobierz list\u0119 parametr\u00f3w dla nowego modelu\n  let availableParams = [];\n  if (summaryModels.includes(model)) {\n    \/\/ Dla modeli typu podsumowanie pobieramy parametry zale\u017cnie od wariantu (1h lub 12h)\n    availableParams = paramsForModels[model][selectedTerm] || [];\n  } else {\n    \/\/ Dla pozosta\u0142ych modeli parametry s\u0105 takie same niezale\u017cnie od terminu\n    availableParams = paramsForModels[model] || [];\n  }\n  \n  \/\/ Filtruj parametry (usu\u0144 te z poziomem ci\u015bnienia)\n  let filteredParams = [];\n  let baseParamsAdded = new Set();\n  availableParams.forEach(param => {\n    if (\/^[A-Z]$\/.test(param)) {\n      if (!baseParamsAdded.has(param)) {\n        filteredParams.push(param);\n        baseParamsAdded.add(param);\n      }\n    } else if (isParameterWithPressureLevel(param)) {\n      const baseParam = getBaseParam(param);\n      if (!baseParamsAdded.has(baseParam)) {\n        filteredParams.push(baseParam);\n        baseParamsAdded.add(baseParam);\n      }\n    } else {\n      filteredParams.push(param);\n    }\n  });\n  \n  \/\/ Je\u015bli poprzedni parametr jest dost\u0119pny w nowym modelu, zachowaj go\n  selectedParam = resolveParamForModelSwitch(prevParam, model, selectedTerm);\n  \n  \/\/ Godzin\u0119 dopasowujemy po dacie w renderForecastHours()\n  if (!changingModelType && prevHour) {\n    selectedHour = String(prevHour).padStart(2, \"0\");\n  } else {\n    selectedHour = String(defaultSettings[model].hour).padStart(2, \"0\");\n  }\n  \n  \/\/ Zachowaj statystyk\u0119 dla zwyk\u0142ych modeli\n  if (!summaryModels.includes(model)) {\n    const availableStats = suffixConfig[selectedParam] || suffixConfig[\"default\"];\n    \n    if (prevStat && availableStats.includes(prevStat)) {\n      selectedStat = prevStat;\n    } else {\n      selectedStat = defaultSettings[model].stat;\n    }\n  } else {\n    selectedStat = null; \/\/ Modele podsumowania nie maj\u0105 statystyk\n  }\n  \n  document.getElementById(\"images\").innerHTML = \"\";\n  document.getElementById(\"hours\").innerHTML = \"\";\n  document.getElementById(\"statsSelect\").innerHTML = \"\";\n  document.getElementById(\"statsSection\").style.display = \"none\";\n  document.getElementById(\"pressureLevelSliderSection\").style.display = \"none\";\n  selectedPressureLevel = null; \/\/ Reset poziomu ci\u015bnienia przy zmianie modelu\n  \n  \/\/ Je\u015bli a-laef -> warianty, je\u015bli nie -> terminy\n  if (model === \"a-laef\") {\n    selectedTerm = resolveTermForModelSwitch(prevTerm, \"a-laef\");\n    \n    \/\/ Pobierz dost\u0119pne parametry dla tego wariantu\n    const availableParamsForAlaef = paramsForModels[\"a-laef\"][selectedTerm] || [];\n    \n    \/\/ Filtruj parametry\n    let filteredParamsForAlaef = [];\n    let baseParamsAdded = new Set();\n    availableParamsForAlaef.forEach(param => {\n      if (\/^[A-Z]$\/.test(param)) {\n        if (!baseParamsAdded.has(param)) {\n          filteredParamsForAlaef.push(param);\n          baseParamsAdded.add(param);\n        }\n      } else if (isParameterWithPressureLevel(param)) {\n        const baseParam = getBaseParam(param);\n        if (!baseParamsAdded.has(baseParam)) {\n          filteredParamsForAlaef.push(baseParam);\n          baseParamsAdded.add(baseParam);\n        }\n      } else {\n        filteredParamsForAlaef.push(param);\n      }\n    });\n    \n    \/\/ Je\u015bli poprzedni parametr jest dost\u0119pny (lub ma alias), zachowaj go\n    selectedParam = resolveParamForModelSwitch(prevParam, \"a-laef\", selectedTerm);\n    \n    \/\/ Okre\u015bl dost\u0119pne godziny dla a-laef z danym wariantem \u2014 dopasowanie daty w renderForecastHours()\n    if (prevHour) {\n      selectedHour = String(prevHour).padStart(2, \"0\");\n    } else {\n      selectedHour = String(defaultSettings[\"a-laef\"].hour).padStart(2, \"0\");\n    }\n    \n    renderButtons(\"terms\", alaefVariants, (variant) => {\n      selectedTerm = variant;\n      \n      \/\/ Resetuj region przy zmianie wariantu\n      selectedRegion = null;\n      document.getElementById(\"regionSection\").style.display = \"none\";\n      \n      \/\/ Pobierz dost\u0119pne parametry dla tego wariantu\n      const availableParamsForVariant = paramsForModels[\"a-laef\"][variant] || [];\n      \n      \/\/ Filtruj parametry (usu\u0144 te z poziomem ci\u015bnienia)\n      let filteredParamsForVariant = [];\n      let baseParamsAdded = new Set();\n      availableParamsForVariant.forEach(param => {\n        if (\/^[A-Z]$\/.test(param)) {\n          if (!baseParamsAdded.has(param)) {\n            filteredParamsForVariant.push(param);\n            baseParamsAdded.add(param);\n          }\n        } else if (isParameterWithPressureLevel(param)) {\n          const baseParam = getBaseParam(param);\n          if (!baseParamsAdded.has(baseParam)) {\n            filteredParamsForVariant.push(baseParam);\n            baseParamsAdded.add(baseParam);\n          }\n        } else {\n          filteredParamsForVariant.push(param);\n        }\n      });\n      \n      \/\/ Je\u015bli poprzedni parametr jest dost\u0119pny (lub ma alias), zachowaj go\n      const preservedParamForVariant = resolveEquivalentParam(prevParam, filteredParamsForVariant);\n      if (preservedParamForVariant) {\n        selectedParam = preservedParamForVariant;\n      } else {\n        \/\/ W przeciwnym razie u\u017cyj domy\u015blnego parametru dla tego wariantu\n        if (variant === \"12h\") {\n          selectedParam = \"t2m_max\";\n        } else if (variant === \"1h\") {\n          selectedParam = \"t2m\";\n        }\n      }\n      \n      renderParams();\n      \n      \/\/ resetowanie interfejsu gdy trzeba\n      if (prevParam !== selectedParam || prevHour !== selectedHour) {\n        document.getElementById(\"hours\").innerHTML = \"\";\n        document.getElementById(\"images\").innerHTML = \"\";\n      }\n    }, selectedTerm);\n  } else {\n    renderButtons(\"terms\", termsDefault, (term) => {\n      selectedTerm = term;\n      \n      \/\/ Wyczy\u015b\u0107 paramHours dla modeli podsumowania przy zmianie zestawienia\n      if (summaryModels.includes(selectedModel) && paramHours[selectedModel]) {\n        paramHours[selectedModel] = {};\n        clearHourDateLabelsForModel(selectedModel);\n        clearTermDateSuffixCacheForModel(selectedModel);\n      }\n      \n      \/\/ Resetuj region przy zmianie terminu\n      selectedRegion = null;\n      document.getElementById(\"regionSection\").style.display = \"none\";\n      \n      \/\/ Zachowanie poprzednich warto\u015bci je\u015bli to mo\u017cliwe\n      \/\/ NIE przywracaj selectedPressureLevel - to zosta\u0142o ju\u017c zresetowane przy zmianie modelu\n      if (prevTerm && prevTerm === term && prevParam) {\n        selectedParam = prevParam;\n        selectedHour = prevHour;\n        selectedStat = prevStat;\n        \/\/ selectedPressureLevel zosta\u0142 ju\u017c odpowiednio ustawiony lub zresetowany\n      }\n      \n      renderParams();\n\n      \/\/ resetowanie interfejsu gdy trzeba\n      if (prevParam !== selectedParam || prevHour !== selectedHour) {\n        document.getElementById(\"hours\").innerHTML = \"\";\n        document.getElementById(\"images\").innerHTML = \"\";\n      }\n    }, selectedTerm);\n  }\n\n  updateTermButtons();\n  refreshViewAfterConfigChange();\n}, selectedModel, modelDisplayNames);\n\n\/\/ Zdefiniowanie poziom\u00f3w ci\u015bnienia (od najni\u017cszego do najwy\u017cszego)\nconst pressureLevels = [\"1000\", \"950\", \"850\", \"700\", \"500\", \"300\"];\n\n\/\/ Funkcja pobieraj\u0105ca dost\u0119pne poziomy ci\u015bnienia dla parametru\nfunction getAvailablePressureLevels(param) {\n  if (!param || !selectedModel) return [];\n  \n  const params = paramsForModels[selectedModel] || [];\n  const levels = [];\n  \n  \/\/ Znajd\u017a wszystkie warianty tego parametru z _LVL_\n  params.forEach(p => {\n    if (p.startsWith(param + \"_LVL_\")) {\n      \/\/ Wyci\u0105gnij numer poziomu (np. z \"WWIND_LVL_950\" -> \"950\")\n      const level = p.split(\"_LVL_\")[1];\n      if (level && !levels.includes(level)) {\n        levels.push(level);\n      }\n    }\n  });\n  \n  \/\/ Sortuj od najwy\u017cszego (1000) do najni\u017cszego (100)\n  levels.sort((a, b) => parseInt(b) - parseInt(a));\n  \n  return levels;\n}\n\n\/\/ Sprawdzenie czy parametr ma warianty poziom\u00f3w ci\u015bnienia\nfunction hasPressureLevels(param) {\n  \/\/ Weryfikacja czy parametr ko\u0144czy si\u0119 liczb\u0105 (np. T0300, T0500, itp.) lub czy jest to pojedyncza litera\n  \/\/ (parametry z pojedyncz\u0105 liter\u0105 jak T, A, B, K, N itp. zawsze maj\u0105 poziomy ci\u015bnienia)\n  return \/[A-Z]0(300|500|700|850|950|1000)$\/.test(param) || \/^[A-Z]$\/.test(param);\n}\n\n\/\/ Sprawdzenie czy parametr jest z poziomem ci\u015bnienia\nfunction isParameterWithPressureLevel(param) {\n  \/\/ Sprawdza czy parametr ma poziom ci\u015bnienia (np. A0300, A0500, A1000, T1000)\n  return \/[A-Z]0(300|500|700|850|950)$\/.test(param) || \/[A-Z]1000$\/.test(param);\n}\n\n\/\/ Funkcja do ekstrakcji podstawowego parametru bez poziomu ci\u015bnienia\nfunction getBaseParam(param) {\n  \/\/ Zwraca cz\u0119\u015b\u0107 parametru przed cyframi poziomu\n  \/\/ Obs\u0142uguje zar\u00f3wno format z 0 (np. T0300) jak i bez 0 (np. T1000)\n  return param.replace(\/0(300|500|700|850|950)$\/, '').replace(\/1000$\/, '');\n}\n\n\/\/ Preferencje zachowania parametru przy prze\u0142\u0105czaniu modelu\/wariantu.\n\/\/ Kolejno\u015b\u0107 ma znaczenie \u2014 pierwszy dost\u0119pny wariant wygrywa.\n\/\/ NIE \u0142\u0105czymy tp z 24h_tp\/24_tp (to r\u00f3\u017cne wielko\u015bci fizyczne).\nconst paramSwitchPreferenceLists = {\n  \"24h_tp\": [\"24h_tp\", \"24_tp\"],\n  \"24_tp\": [\"24_tp\", \"24h_tp\"],\n  \"t2m_max\": [\"t2m_max\", \"t2m\"],\n  \"t2m_min\": [\"t2m_min\", \"t0m_min\", \"t2m\"],\n  \"t0m_min\": [\"t0m_min\", \"t2m_min\"],\n  \"t2m\": [\"t2m\", \"t2m_max\", \"t2m_min\"],\n  \"gst_max\": [\"gst_max\", \"gst\"],\n  \"gst\": [\"gst\", \"gst_max\"],\n  \"ws_max\": [\"ws_max\", \"ws\"],\n  \"ws\": [\"ws\", \"ws_max\"],\n  \"ptype\": [\"ptype\", \"ptype3h\"],\n  \"ptype3h\": [\"ptype3h\", \"ptype\"],\n  \"vis_min\": [\"vis_min\", \"vis_3h\"],\n  \"vis_3h\": [\"vis_3h\", \"vis_min\"],\n  \"cape_ml_max\": [\"cape_ml_max\", \"cape_ml\"],\n  \"cape_ml\": [\"cape_ml\", \"cape_ml_max\"],\n  \"cape_mu_max\": [\"cape_mu_max\", \"cape_mu_3h\", \"cape_mu\"],\n  \"cape_mu_3h\": [\"cape_mu_3h\", \"cape_mu_max\", \"cape_mu\"],\n  \"cape_mu\": [\"cape_mu\", \"cape_mu_3h\", \"cape_mu_max\"],\n  \"cin_ml_max\": [\"cin_ml_max\", \"cin_ml\"],\n  \"cin_ml\": [\"cin_ml\", \"cin_ml_max\"],\n  \"cin_mu_max\": [\"cin_mu_max\", \"cin_mu\"],\n  \"cin_mu\": [\"cin_mu\", \"cin_mu_max\"]\n};\n\n\/\/ W\u0119\u017csze aliasy tylko do por\u00f3wnania modeli \u2014 NIE \u0142\u0105czymy t2m_max z t2m_min itd.\nconst compareEquivalentParamGroups = [\n  [\"24h_tp\", \"24_tp\"],\n  [\"gst\", \"gst_max\"],\n  [\"ws\", \"ws_max\"],\n  [\"ptype3h\", \"ptype\"],\n  [\"vis_3h\", \"vis_min\"],\n  [\"cape_ml\", \"cape_ml_max\"],\n  [\"cape_mu\", \"cape_mu_3h\", \"cape_mu_max\"],\n  [\"cin_ml\", \"cin_ml_max\"],\n  [\"cin_mu\", \"cin_mu_max\"]\n];\n\nfunction resolveParamAgainstGroups(param, availableParams, groups) {\n  if (!param || !Array.isArray(availableParams) || availableParams.length === 0) {\n    return null;\n  }\n\n  if (availableParams.includes(param)) {\n    return param;\n  }\n\n  for (let i = 0; i < groups.length; i += 1) {\n    const group = groups[i];\n    if (group.indexOf(param) === -1) {\n      continue;\n    }\n    for (let j = 0; j < group.length; j += 1) {\n      if (availableParams.indexOf(group[j]) !== -1) {\n        return group[j];\n      }\n    }\n  }\n\n  return null;\n}\n\nfunction resolveEquivalentParam(param, availableParams) {\n  if (!param || !Array.isArray(availableParams) || availableParams.length === 0) {\n    return null;\n  }\n\n  if (availableParams.includes(param)) {\n    return param;\n  }\n\n  const preferences = paramSwitchPreferenceLists[param];\n  if (preferences) {\n    for (let i = 0; i < preferences.length; i += 1) {\n      if (availableParams.indexOf(preferences[i]) !== -1) {\n        return preferences[i];\n      }\n    }\n  }\n\n  const grouped = resolveParamAgainstGroups(param, availableParams, compareEquivalentParamGroups);\n  if (grouped) {\n    return grouped;\n  }\n\n  \/\/ Fallback dla nazw r\u00f3\u017cni\u0105cych si\u0119 tylko sufiksem czasu, np. ptype3h\/ptype6h\/ptype.\n  const normalizeDurationSuffix = function(value) {\n    return value.replace(\/_?\\d+h$\/i, \"\");\n  };\n  const normalizedParam = normalizeDurationSuffix(param);\n  if (normalizedParam !== param) {\n    const directMatch = availableParams.find(function(candidate) {\n      return candidate === normalizedParam;\n    });\n    if (directMatch) {\n      return directMatch;\n    }\n  }\n\n  const durationAliasMatch = availableParams.find(function(candidate) {\n    return normalizeDurationSuffix(candidate) === normalizedParam;\n  });\n  if (durationAliasMatch) {\n    return durationAliasMatch;\n  }\n\n  return null;\n}\n\nfunction resolveCompareEquivalentParam(param, availableParams) {\n  if (!param || !Array.isArray(availableParams) || availableParams.length === 0) {\n    return null;\n  }\n\n  if (availableParams.includes(param)) {\n    return param;\n  }\n\n  const preferences = paramSwitchPreferenceLists[param];\n  if (preferences) {\n    for (let i = 0; i < preferences.length; i += 1) {\n      if (availableParams.indexOf(preferences[i]) !== -1) {\n        return preferences[i];\n      }\n    }\n  }\n\n  return resolveParamAgainstGroups(param, availableParams, compareEquivalentParamGroups);\n}\n\nfunction filterBaseParamsList(availableParams) {\n  const filteredParams = [];\n  const baseParamsAdded = new Set();\n\n  (availableParams || []).forEach(function(param) {\n    if (\/^[A-Z]$\/.test(param)) {\n      if (!baseParamsAdded.has(param)) {\n        filteredParams.push(param);\n        baseParamsAdded.add(param);\n      }\n    } else if (isParameterWithPressureLevel(param)) {\n      const baseParam = getBaseParam(param);\n      if (!baseParamsAdded.has(baseParam)) {\n        filteredParams.push(baseParam);\n        baseParamsAdded.add(baseParam);\n      }\n    } else {\n      filteredParams.push(param);\n    }\n  });\n\n  return filteredParams;\n}\n\nfunction getFilteredParamsForModelTerm(model, term) {\n  let params = [];\n  if (summaryModels.includes(model)) {\n    params = (paramsForModels[model] && paramsForModels[model][term]) ? paramsForModels[model][term] : [];\n  } else {\n    params = paramsForModels[model] || [];\n  }\n  return filterBaseParamsList(params);\n}\n\nfunction getDefaultParamForModelTerm(model, term) {\n  const filteredParams = getFilteredParamsForModelTerm(model, term);\n  if (!filteredParams.length) {\n    return defaultSettings[model]?.param || null;\n  }\n\n  const defaultParam = defaultSettings[model]?.param;\n  if (defaultParam && filteredParams.includes(defaultParam)) {\n    return defaultParam;\n  }\n\n  const equivalentDefault = resolveEquivalentParam(defaultParam, filteredParams);\n  if (equivalentDefault) {\n    return equivalentDefault;\n  }\n\n  return filteredParams[0];\n}\n\nfunction resolveParamForModelSwitch(prevParam, model, term) {\n  const filteredParams = getFilteredParamsForModelTerm(model, term);\n  if (!filteredParams.length) {\n    return getDefaultParamForModelTerm(model, term);\n  }\n\n  const normalizedPrev = prevParam && isParameterWithPressureLevel(prevParam)\n    ? getBaseParam(prevParam)\n    : prevParam;\n  const preserved = resolveEquivalentParam(normalizedPrev, filteredParams);\n  if (preserved) {\n    return preserved;\n  }\n\n  return getDefaultParamForModelTerm(model, term);\n}\n\nfunction ensureSelectedParamValidForModelTerm(model, term) {\n  const filteredParams = getFilteredParamsForModelTerm(model, term);\n  if (!filteredParams.length) {\n    return;\n  }\n  if (!selectedParam || !filteredParams.includes(selectedParam)) {\n    selectedParam = resolveParamForModelSwitch(selectedParam, model, term);\n  }\n}\n\n\/\/ 2. Renderowanie parametr\u00f3w\nfunction renderParams() {\n  if (!selectedModel || !selectedTerm) return;\n\n  const compareParamsActive = compareMode && compareSelectedModels.length >= 2;\n  if (!compareParamsActive) {\n    ensureSelectedParamValidForModelTerm(selectedModel, selectedTerm);\n  }\n  \n  \/\/ Ukryj sekcj\u0119 statystyk i slider ci\u015bnienia dla modeli podsumowania\n  if (summaryModels.includes(selectedModel)) {\n    document.getElementById(\"statsSection\").style.display = \"none\";\n    document.getElementById(\"pressureLevelSliderSection\").style.display = \"none\";\n    selectedPressureLevel = null;\n  }\n  \n  let params = [];\n  \n  \/\/ W por\u00f3wnaniu: lista parametr\u00f3w wsp\u00f3lnych dla wybranych modeli (\u22652).\n  if (compareParamsActive) {\n    const compareOptions = getCompareParamOptions(selectedTerm).filter(function(option) {\n      const modelsWithParam = compareSelectedModels.filter(function(modelId) {\n        return option.models.indexOf(modelId) !== -1;\n      });\n      return modelsWithParam.length >= 2;\n    });\n    params = compareOptions.map(function(option) {\n      return option.id;\n    });\n    if (!params.length) {\n      params = paramsForModels[selectedModel] && paramsForModels[selectedModel][selectedTerm]\n        ? paramsForModels[selectedModel][selectedTerm]\n        : (paramsForModels[selectedModel] || []);\n    }\n  } else if (summaryModels.includes(selectedModel)) {\n    \/\/ Dla modeli typu podsumowanie pobieramy parametry zale\u017cnie od wariantu (1h lub 12h)\n    params = paramsForModels[selectedModel][selectedTerm] || [];\n  } else {\n    \/\/ Dla pozosta\u0142ych modeli parametry s\u0105 takie same niezale\u017cnie od terminu\n    params = paramsForModels[selectedModel] || [];\n  }\n  \n  \/\/ Sprawd\u017a czy model w og\u00f3le ma parametry z poziomami ci\u015bnienia\n  const modelHasAnyPressureLevels = params.some(p => \n    \/^[A-Z]$\/.test(p) || \/[A-Z]0(300|500|700|850|950)$\/.test(p) || \/[A-Z]1000$\/.test(p)\n  );\n  \n  \/\/ Je\u015bli model nie ma \u017cadnych parametr\u00f3w z poziomami, ukryj slider i zresetuj\n  if (!modelHasAnyPressureLevels) {\n    document.getElementById(\"pressureLevelSliderSection\").style.display = \"none\";\n    selectedPressureLevel = null;\n  }\n  \n  \/\/ Zapami\u0119tanie aktualnie wybranego parametru\n  const currentParam = selectedParam;\n  \n  \/\/ Filtrowanie parametr\u00f3w, aby usun\u0105\u0107 te z poziomem ci\u015bnienia, ale zachowa\u0107 parametr podstawowy\n  let filteredParams = [];\n  let baseParamsAdded = new Set();\n  \n  params.forEach(param => {\n    \/\/ Dla jednoliterowych parametr\u00f3w (A, B, T, itp.) - zawsze traktujemy jako parametry z poziomami\n    if (\/^[A-Z]$\/.test(param)) {\n      if (!baseParamsAdded.has(param)) {\n        filteredParams.push(param);\n        baseParamsAdded.add(param);\n      }\n    }\n    \/\/ Je\u015bli to jest parametr z poziomem ci\u015bnienia (np. A0300, A1000)\n    else if (isParameterWithPressureLevel(param)) {\n      \/\/ Pobieranie podstawowego parametru\n      const baseParam = getBaseParam(param);\n      \/\/ dodawanie\n      if (!baseParamsAdded.has(baseParam)) {\n        filteredParams.push(baseParam);\n        baseParamsAdded.add(baseParam);\n      }\n    } else {\n      \/\/ Dla parametr\u00f3w bez poziomu ci\u015bnienia, dodajemy je normalnie\n      filteredParams.push(param);\n    }\n  });\n  \n  renderSelect(\"paramsSelect\", filteredParams, (param) => {\n    \/\/ Resetowanie wszystkich flag wykonywania przy zmianie parametru\n    isShowImagesRunning = false;\n    isShowImagesRunningTimestamp = 0;\n    isRenderingForecastHours = false;\n    isRenderingForecastHoursTimestamp = 0;\n    resetPreloadQueue();\n    if (compareMode) {\n      resetComparePreloadQueue();\n    }\n    \n    \/\/ Zapami\u0119tanie poprzednich warto\u015bci terminu, statystyki i poziomu ci\u015bnienia\n    let prevHour = selectedHour;\n    let prevStat = selectedStat;\n    let prevPressureLevel = selectedPressureLevel;\n    \n    selectedParam = param;\n    \n    \/\/ Natychmiast zaktualizuj widoczno\u015b\u0107 region\u00f3w dla nowego parametru\n    if (summaryModels.includes(selectedModel)) {\n      renderRegions();\n    }\n    \n    \/\/ Nie resetujemy wcze\u015bniej wybranego terminu prognozy i statystyki\n    \/\/ je\u015bli zmieniamy tylko parametr\n    \n    document.getElementById(\"images\").innerHTML = \"\";\n    document.getElementById(\"hours\").innerHTML = \"\";\n    \n    \/\/ Weryfikacja czy wybrany parametr ma poziomy ci\u015bnienia\n    \/\/ Dla jednoliterowych parametr\u00f3w (A, B, T, itp.) zawsze true\n    const paramHasLevels = \/^[A-Z]$\/.test(param) ? true : params.some(p => \n      p.startsWith(param + \"0\") || p === param + \"1000\");\n    \n    \/\/ Pokazanie lub ukrycie suwaka poziom\u00f3w ci\u015bnienia - tylko dla zwyk\u0142ych modeli (nie podsumowania)\n    const shouldShowPressureSlider = !summaryModels.includes(selectedModel) && paramHasLevels;\n    document.getElementById(\"pressureLevelSliderSection\").style.display = shouldShowPressureSlider ? \"block\" : \"none\";\n    \n    \/\/ Je\u015bli parametr ma poziomy ci\u015bnienia i to NIE jest model podsumowania, ustaw domy\u015blny poziom na 1000 hPa lub zachowaj poprzedni\n    if (shouldShowPressureSlider) {\n      \/\/ Je\u015bli by\u0142 wcze\u015bniej wybrany poziom ci\u015bnienia i przechodzimy mi\u0119dzy parametrami z poziomami, zachowaj\n      if (prevPressureLevel && pressureLevels.includes(prevPressureLevel)) {\n        const levelIndex = pressureLevels.indexOf(prevPressureLevel);\n        if (levelIndex !== -1) {\n          document.getElementById(\"pressureLevelSlider\").value = levelIndex;\n          document.getElementById(\"pressureValue\").textContent = prevPressureLevel + \" hPa\";\n          selectedPressureLevel = prevPressureLevel;\n        } else {\n          document.getElementById(\"pressureLevelSlider\").value = 0; \n          document.getElementById(\"pressureValue\").textContent = \"1000 hPa\";\n          selectedPressureLevel = \"1000\";\n        }\n      } else {\n        document.getElementById(\"pressureLevelSlider\").value = 0; \n        document.getElementById(\"pressureValue\").textContent = \"1000 hPa\";\n        selectedPressureLevel = \"1000\";\n      }\n      \/\/ Renderuj przyciski dla urz\u0105dze\u0144 mobilnych\n      renderPressureLevelButtons();\n    } else {\n      selectedPressureLevel = null; \/\/ Reset gdy suwak jest ukryty\n    }\n    \n    \/\/ Ukrywamy sekcj\u0119 statystyk dla wszystkich modeli typu \"podsumowanie\"\n    if (summaryModels.includes(selectedModel)) {\n      document.getElementById(\"statsSection\").style.display = \"none\";\n\n      \/\/ W por\u00f3wnaniu: prze\u0142aduj godziny\/sufiksy dla WSZYSTKICH modeli, nie tylko primary.\n      if (compareMode && getActiveCompareModelIds().length >= 2) {\n        reloadCompareAfterConfigChange({ forceHoursReload: true });\n        return;\n      }\n\n      determineAvailableHours(selectedModel, selectedTerm, selectedParam)\n        .then(function() {\n          const availableHours = paramHours[selectedModel] && paramHours[selectedModel][selectedParam];\n\n          if (availableHours && availableHours.length > 0) {\n            const formattedSelectedHour = String(selectedHour).padStart(2, \"0\");\n            if (!availableHours.includes(formattedSelectedHour)) {\n              selectedHour = availableHours[0];\n            }\n\n            document.getElementById(\"images\").innerHTML = \"\";\n            renderForecastHours();\n            refreshViewAfterConfigChange({ skipHoursReload: true });\n          } else {\n            console.error(`Brak dost\u0119pnych godzin dla parametru ${selectedParam} w modelu ${selectedModel}`);\n            document.getElementById(\"hours\").innerHTML = '<div class=\"loading-hours\">Brak dost\u0119pnych godzin<\/div>';\n          }\n        });\n    } else {\n      \/\/ Ukryj sekcj\u0119 region\u00f3w dla standardowych modeli\n      document.getElementById(\"regionSection\").style.display = \"none\";\n      selectedRegion = null;\n      \n      \/\/ Dla standardowych modeli najpierw pokazujemy wyb\u00f3r statystyki\n      getElementCached(\"statsSection\").style.display = \"block\";\n      \n      \/\/ Sprawdzanie czy poprzednia statystyka jest dost\u0119pna dla nowego parametru\n      \/\/ Je\u015bli nie, ustawia domy\u015blnie na 'mean'\n      const availableStats = suffixConfig[param] || suffixConfig[\"default\"];\n      \n      \/\/ Zawsze prze\u0142\u0105czamy na statystyk\u0119 \"mean\" przy zmianie parametru\n      selectedStat = \"mean\";\n      \n      \/\/ Zachowaj selectedHour przed wywo\u0142aniem renderStatistics()\n      const preservedHour = selectedHour;\n      renderStatistics();\n      \n      \/\/ Przywr\u00f3\u0107 zachowan\u0105 godzin\u0119 je\u015bli zosta\u0142a zmieniona\n      if (preservedHour && selectedHour !== preservedHour) {\n        selectedHour = preservedHour;\n      }\n\n      refreshViewAfterConfigChange({ skipHoursReload: true });\n    }\n  }, parameterDisplayNames);\n  \n  \/\/ Po zako\u0144czeniu renderowania parametr\u00f3w, ustaw warto\u015b\u0107 select'a\n  if (currentParam && filteredParams.includes(currentParam)) {\n    const paramsSelect = document.getElementById(\"paramsSelect\");\n    paramsSelect.value = currentParam;\n    selectedParam = currentParam;\n  } else if (selectedParam && filteredParams.includes(selectedParam)) {\n    const paramsSelect = document.getElementById(\"paramsSelect\");\n    paramsSelect.value = selectedParam;\n  } else {\n    const resolvedCompare = compareParamsActive\n      ? resolveCompareEquivalentParam(selectedParam || currentParam, filteredParams)\n      : null;\n    const fallbackParam = resolvedCompare\n      || (filteredParams.includes(getDefaultParamForModelTerm(selectedModel, selectedTerm))\n        ? getDefaultParamForModelTerm(selectedModel, selectedTerm)\n        : filteredParams[0]);\n    if (fallbackParam) {\n      const paramsSelect = document.getElementById(\"paramsSelect\");\n      paramsSelect.value = fallbackParam;\n      selectedParam = fallbackParam;\n    }\n  }\n  \n  \/\/ Dla zwyk\u0142ych modeli (nie-podsumowania) wywo\u0142aj renderStatistics()\n  if (!summaryModels.includes(selectedModel)) {\n    renderStatistics();\n  }\n}\n\n\/\/ 3. Renderowanie statystyk (tylko dla standardowych modeli)\nfunction renderStatistics() {\n  \/\/ Modele podsumowania nie maj\u0105 statystyk\n  if (summaryModels.includes(selectedModel) || !selectedParam) return;\n  \n  \/\/ Pobieranie dost\u0119pnych statystyk dla wybranego parametru\n  const stats = suffixConfig[selectedParam] || suffixConfig[\"default\"];\n  \n  \/\/ Je\u015bli jest tylko jedna statystyka (mean) - wybierz j\u0105 automatycznie\n  if (stats.length === 1 && stats[0] === \"mean\") {\n    selectedStat = \"mean\";\n    \/\/ Ukrycie sekcji wyboru statystyk, gdy\u017c nie ma co wybiera\u0107\n    document.getElementById(\"statsSection\").style.display = \"none\";\n    \/\/ Bezpo\u015brednie przej\u015bcie do wyboru terminu prognozy\n    renderForecastHours();\n    \/\/ Wst\u0119pne wczytanie obrazk\u00f3w po automatycznym wyborze statystyki\n    preloadForecastImages();\n    return;\n  }\n  \n  \/\/ Je\u015bli jest wi\u0119cej statystyk, poka\u017c list\u0119 wyboru\n  document.getElementById(\"statsSection\").style.display = \"block\";\n  \n  \/\/ Zapami\u0119taj aktualnie wybran\u0105 statystyk\u0119\n  const currentStat = selectedStat;\n  \n  renderSelect(\"statsSelect\", stats, (stat) => {\n    \/\/ Resetowanie wszystkich flag wykonywania przy zmianie statystyki\n    isShowImagesRunning = false;\n    isShowImagesRunningTimestamp = 0;\n    isRenderingForecastHours = false;\n    isRenderingForecastHoursTimestamp = 0;\n    resetPreloadQueue();\n    if (compareMode) {\n      resetComparePreloadQueue();\n    }\n    \n    selectedStat = stat;\n    renderForecastHours();\n    refreshViewAfterConfigChange({ skipHoursReload: true });\n  }, statisticsDisplayNames);\n  \n  \/\/ Je\u015bli mamy wybran\u0105 statystyk\u0119 i jest ona dost\u0119pna, ustaw j\u0105 w interfejsie\n  if (currentStat && stats.includes(currentStat)) {\n    const statsSelect = document.getElementById(\"statsSelect\");\n    statsSelect.value = currentStat;\n    \/\/ Renderuj interfejs godzin bez symulowania zdarzenia change\n    renderForecastHours();\n    \/\/ Wst\u0119pne wczytanie obrazk\u00f3w\n    preloadForecastImages();\n  } else if (selectedStat && stats.includes(selectedStat)) {\n    \/\/ Je\u015bli mamy domy\u015bln\u0105 statystyk\u0119 i jest dost\u0119pna\n    const statsSelect = document.getElementById(\"statsSelect\");\n    statsSelect.value = selectedStat;\n    \/\/ Renderuj interfejs godzin bez symulowania zdarzenia change\n    renderForecastHours();\n    \/\/ Wst\u0119pne wczytanie obrazk\u00f3w\n    preloadForecastImages();\n  }\n}\n\n\/\/ 3b. Sprawdzanie czy parametr ma warianty regionalne\nfunction hasRegions(model, term, param) {\n  return regionsConfig[model] && \n         regionsConfig[model][term] && \n         regionsConfig[model][term].includes(param);\n}\n\nfunction shouldShowRegionsSection() {\n  if (compareMode && getActiveCompareModelIds().length >= 2) {\n    return getActiveCompareModelIds().some(function(modelId) {\n      const param = getCompareParamForModel(modelId);\n      return !!param && hasRegions(modelId, selectedTerm, param);\n    });\n  }\n  return hasRegions(selectedModel, selectedTerm, selectedParam);\n}\n\n\/\/ 3c. Renderowanie przycisk\u00f3w region\u00f3w (dla modeli z podsumowania)\nfunction renderRegions() {\n  const regionSection = document.getElementById(\"regionSection\");\n  const regionsContainer = document.getElementById(\"regions\");\n  const previousRegion = selectedRegion;\n  \n  \/\/ Wyczy\u015b\u0107 zawarto\u015b\u0107\n  regionsContainer.innerHTML = \"\";\n  \n  \/\/ Sprawd\u017a czy pokaza\u0107 regiony\n  const shouldShow = shouldShowRegionsSection();\n  \n  if (!shouldShow) {\n    selectedRegion = null;\n    if (regionSection) {\n      regionSection.style.display = \"none\";\n    }\n    return;\n  }\n  \n  \/\/ Poka\u017c sekcj\u0119\n  if (regionSection) {\n    regionSection.style.display = \"block\";\n  }\n\n  \/\/ Zachowaj wybrany obszar, je\u015bli nadal ma sens (np. przy por\u00f3wnaniu).\n  selectedRegion = previousRegion === undefined ? null : previousRegion;\n  \n  \/\/ Dodaj przyciski dla ka\u017cdego regionu\n  regions.forEach(region => {\n    const btn = document.createElement(\"button\");\n    btn.textContent = region.name;\n    btn.dataset.regionId = region.id || \"\";\n    \n    if (selectedRegion === region.id) {\n      btn.classList.add(\"active\");\n    } else if (selectedRegion === null && region.id === null) {\n      btn.classList.add(\"active\");\n    }\n    \n    btn.addEventListener(\"click\", () => {\n      selectedRegion = region.id;\n      \n      \/\/ Aktualizuj aktywny przycisk\n      regionsContainer.querySelectorAll(\"button\").forEach(b => b.classList.remove(\"active\"));\n      btn.classList.add(\"active\");\n      \n      if (compareMode && getActiveCompareModelIds().length >= 2) {\n        reloadCompareAfterConfigChange({ forceHoursReload: true });\n        return;\n      }\n\n      if (summaryModels.includes(selectedModel) && (selectedTerm === \"12h\" || selectedTerm === \"1h\" || selectedTerm === \"3h\")) {\n        determineAvailableHours(selectedModel, selectedTerm, selectedParam, selectedRegion)\n          .then(function() {\n            renderForecastHours();\n            refreshViewAfterConfigChange({ skipHoursReload: true });\n          });\n        return;\n      }\n\n      refreshViewAfterConfigChange({ skipHoursReload: true });\n    });\n    \n    regionsContainer.appendChild(btn);\n  });\n\n  \/\/ Upewnij si\u0119, \u017ce Polska jest aktywna, gdy nic nie wybrano\n  if (selectedRegion === undefined) {\n    selectedRegion = null;\n  }\n}\n\n\/\/ 4. Renderowanie termin\u00f3w prognozy\n\/\/ Zmienna do \u015bledzenia, czy funkcja renderForecastHours jest aktualnie wykonywana\nlet isRenderingForecastHours = false;\nlet isRenderingForecastHoursTimestamp = 0;\nlet latestRenderRequest = null;\nlet pendingRenderRequest = false;\nlet compareHoursReloadAttempted = false;\n\nfunction renderForecastHours() {\n  if (!selectedModel || !selectedTerm || !selectedParam) {\n    return;\n  }\n  \n  \/\/ Dla modeli innych ni\u017c \"podsumowanie\", potrzebujemy statystyki\n  if (!summaryModels.includes(selectedModel) && !selectedStat) {\n    return;\n  }\n  \n  \/\/ Zapami\u0119taj aktualne \u017c\u0105danie\n  latestRenderRequest = {\n    model: selectedModel,\n    term: selectedTerm,\n    param: selectedParam,\n    stat: selectedStat\n  };\n  \n  \/\/ Je\u015bli funkcja ju\u017c jest wykonywana, zaznacz \u017ce jest nowe \u017c\u0105danie\n  if (isRenderingForecastHours) {\n    pendingRenderRequest = true;\n    return;\n  }\n  \n  \/\/ Ustaw flag\u0119 wykonywania\n  isRenderingForecastHours = true;\n  isRenderingForecastHoursTimestamp = Date.now();\n  \n  \/\/ Dodaj wska\u017anik \u0142adowania je\u015bli nie mamy wcze\u015bniej preloadowanych danych\n  if (!preloadedModels[selectedModel]) {\n    const hoursContainer = document.getElementById(\"hours\");\n    if (hoursContainer) {\n      hoursContainer.innerHTML = '<div class=\"loading-hours\">Wczytywanie danych modelu...<\/div>';\n    }\n    \n    const imagesContainer = document.getElementById(\"images\");\n    if (imagesContainer) {\n      imagesContainer.innerHTML = \"\";\n      const loaderContainer = document.createElement(\"div\");\n      loaderContainer.className = \"loader-container\";\n      const loader = document.createElement(\"div\");\n      loader.className = \"loader\";\n      loaderContainer.appendChild(loader);\n      loaderContainer.appendChild(document.createElement(\"p\")).textContent = \"\u0141adowanie prognozy...\";\n      imagesContainer.appendChild(loaderContainer);\n    }\n  }\n  \n  \/\/ Aktualizacja tytu\u0142u sekcji godzin w zale\u017cno\u015bci od typu modelu\n  updateForecastHoursTitles();\n  \n  let hours = [];\n  const compareActive = compareMode && getActiveCompareModelIds().length >= 2;\n\n  if (compareActive) {\n    hours = getCompareForecastHoursList();\n    if (!hours.length) {\n      const hoursContainer = document.getElementById(\"hours\");\n      if (compareHoursReloadAttempted) {\n        compareHoursReloadAttempted = false;\n        if (hoursContainer) {\n          hoursContainer.innerHTML = '<div class=\"loading-hours\">Brak dost\u0119pnych godzin do por\u00f3wnania<\/div>';\n        }\n        isRenderingForecastHours = false;\n        isRenderingForecastHoursTimestamp = 0;\n        return;\n      }\n      compareHoursReloadAttempted = true;\n      if (hoursContainer) {\n        hoursContainer.innerHTML = '<div class=\"loading-hours\">Sprawdzanie dost\u0119pnych godzin...<\/div>';\n      }\n      ensureCompareModelsHoursLoaded().then(function() {\n        isRenderingForecastHours = false;\n        isRenderingForecastHoursTimestamp = 0;\n        renderForecastHours();\n      });\n      return;\n    }\n    compareHoursReloadAttempted = false;\n  } else if ((selectedModel === \"cosmo_ens\" || selectedModel === \"icon_ens\" || selectedModel === \"ecmwf_ifs_ens\" || selectedModel === \"ecmwf_aifs_ens\" || selectedModel === \"gefs\" || selectedModel === \"a-laef\") &&\n      paramHours[selectedModel] && \n      paramHours[selectedModel][selectedParam] &&\n      paramHours[selectedModel][selectedParam].length > 0) {\n    \/\/ U\u017cywamy tylko godzin dost\u0119pnych dla wybranego parametru w tym modelu\n    hours = paramHours[selectedModel][selectedParam];\n  } \n  \/\/ Wy\u015bwietl komunikat, je\u015bli nie ma dost\u0119pnych godzin dla wybranego parametru\n  else if ((selectedModel === \"cosmo_ens\" || selectedModel === \"icon_ens\" || selectedModel === \"ecmwf_ifs_ens\" || selectedModel === \"ecmwf_aifs_ens\" || selectedModel === \"gefs\" || selectedModel === \"a-laef\") && \n      (!paramHours[selectedModel] || \n       !paramHours[selectedModel][selectedParam] || \n       paramHours[selectedModel][selectedParam].length === 0)) {\n    \n    \/\/ Sprawd\u017a czy sprawdzanie ju\u017c trwa\n    const checkKey = `${selectedModel}_${selectedTerm}_${selectedParam}`;\n    if (!pendingHourChecks[checkKey]) {\n      \/\/ Tylko je\u015bli sprawdzanie NIE trwa, wy\u015bwietl komunikat i dodaj animacj\u0119\n      document.getElementById(\"hours\").innerHTML = '<div class=\"loading-hours\">Sprawdzanie dost\u0119pnych godzin...<\/div>';\n      \n      \/\/ NIE dodajemy tutaj animacji \u0142adowania - performAvailableHoursCheck() to zrobi\n    }\n    \n    \/\/ Pr\u00f3ba sprawdzenia dost\u0119pnych godzin\n    determineAvailableHours(selectedModel, selectedTerm, selectedParam)\n      .then((availableHours) => {\n        \/\/ Zresetuj flag\u0119 renderowania przed dalszym przetwarzaniem\n        isRenderingForecastHours = false;\n        isRenderingForecastHoursTimestamp = 0;\n        \n        if (!availableHours || availableHours.length === 0) {\n          document.getElementById(\"hours\").innerHTML = '<div class=\"loading-hours\">Brak dost\u0119pnych godzin dla tego parametru<\/div>';\n          \n          \/\/ Pokazujemy obrazek zast\u0119pczy zamiast animacji \u0142adowania\n          const imagesContainer = document.getElementById(\"images\");\n          if (imagesContainer) {\n            \/\/ Wyczy\u015b\u0107 kontener (usu\u0144 animacj\u0119)\n            imagesContainer.innerHTML = \"\";\n            \n            \/\/ Dodaj obrazek zast\u0119pczy\n            const errorImg = document.createElement(\"img\");\n            errorImg.src = \"\/wp-content\/uploads\/production\/prognozy_wiazkowe\/configuration_not.png\";\n            errorImg.alt = \"Konfiguracja niedost\u0119pna\";\n            errorImg.style.maxWidth = \"100%\";\n            errorImg.style.display = \"block\";\n            imagesContainer.appendChild(errorImg);\n            \n            \/\/ Dodaj komunikat\n            const errorMsg = document.createElement(\"p\");\n            errorMsg.textContent = \"Brak dost\u0119pnych prognoz dla wybranej konfiguracji.\";\n            errorMsg.style.color = \"red\";\n            errorMsg.style.textAlign = \"center\";\n            imagesContainer.appendChild(errorMsg);\n          }\n        } else {\n          \/\/ Od\u015bwie\u017c interfejs z nowymi godzinami\n          renderForecastHours();\n        }\n      });\n    \n    return; \/\/ Przerywamy wykonywanie funkcji\n  }\n  \/\/ Je\u015bli nie ma specjalnych godzin lub to inny model, u\u017cywamy standardowych godzin\n  else if (forecastHours[selectedModel] && forecastHours[selectedModel][selectedTerm]) {\n    hours = forecastHours[selectedModel][selectedTerm];\n  } else {\n    hours = forecastHours[\"default\"];\n  }\n  \n  \/\/ Zachowujemy aktualnie wybrany termin\n  const formattedHoursList = hours.map(function(h) {\n    return String(h).padStart(2, \"0\");\n  });\n\n  if (hourSelectionPreserveContext) {\n    selectedHour = resolveHourFromSelectionContext(\n      hourSelectionPreserveContext,\n      selectedModel,\n      selectedTerm,\n      selectedParam,\n      formattedHoursList\n    );\n    hourSelectionPreserveContext = null;\n  }\n\n  const currentHour = selectedHour;\n  const labelModel = compareActive ? getCompareReferenceModelId() : selectedModel;\n  const labelParam = compareActive ? (getCompareParamForModel(labelModel) || selectedParam) : selectedParam;\n\n  const hourDisplayNames = buildHourDisplayNames(hours.map(h => String(h).padStart(2, \"0\")), labelModel, selectedTerm, labelParam);\n  const hourButtonTitles = buildHourButtonTitles(hours.map(h => String(h).padStart(2, \"0\")), labelModel, selectedTerm, labelParam);\n  \n  renderButtons(\"hours\", hours.map(h => {\n    \/\/ Zawsze u\u017cywamy sformatowanych warto\u015bci z wiod\u0105cym zerem\n    return String(h).padStart(2, \"0\");\n  }), (hour) => {\n    \/\/ Ustaw wybran\u0105 godzin\u0119\n    selectedHour = hour;\n    \n    \/\/ Aktualizacja poziomego slidera\n    \/\/ Najpierw konwertujemy wszystkie godziny do tego samego formatu (string z wiod\u0105cym zerem)\n    const formattedSelectedHour = String(selectedHour).padStart(2, \"0\");\n    const formattedHours = hours.map(h => String(h).padStart(2, \"0\"));\n    \n    \/\/ Teraz szukamy indeksu po sformatowanych godzinach\n    const hourIndex = formattedHours.indexOf(formattedSelectedHour);\n    \n    if (hourIndex !== -1) {\n      document.getElementById(\"forecastHourSlider\").value = hourIndex;\n    }\n    \n    showImages();\n    \n    \/\/ WA\u017bNE: Po zmianie godziny przez klikni\u0119cie przycisku, zaktualizuj preloading\n    setTimeout(() => {\n      preloadForecastImages();\n    }, 10);\n  }, currentHour, hourDisplayNames, hourButtonTitles);\n\n  updateHoursButtonsLayout();\n  \n  \/\/ Aktualizuj wizualizacj\u0119 za\u0142adowanych godzin\n  updateLoadedHoursVisualization();\n  \n  \/\/ Preload wszystkich obrazk\u00f3w dla wybranego parametru i terminu\n  \/\/ U\u017cywamy setTimeout aby preloadForecastImages wykona\u0142o si\u0119 po zako\u0144czeniu renderowania\n  setTimeout(() => {\n    preloadForecastImages();\n  }, 10); \/\/ Bardzo szybkie wywo\u0142anie aby obrazy by\u0142y pre\u0142adowane natychmiast\n  \n  \/\/ Inicjalizacja poziomego slidera termin\u00f3w prognozy\n  setupHorizontalSlider(hours);\n  \n  \/\/ Resetuj flag\u0119 wykonywania\n  setTimeout(() => {\n    isRenderingForecastHours = false;\n    isRenderingForecastHoursTimestamp = 0;\n    \n    \/\/ Sprawd\u017a, czy pojawi\u0142o si\u0119 nowe \u017c\u0105danie podczas wykonywania tej funkcji\n    if (pendingRenderRequest) {\n      pendingRenderRequest = false;\n      \n      \/\/ Natychmiast renderuj najnowsze \u017c\u0105danie\n      setTimeout(() => {\n        renderForecastHours();\n      }, 10); \/\/ Bardzo kr\u00f3tki timeout\n    }\n  }, 10); \/\/ Bardzo kr\u00f3tki timeout, aby szybko reagowa\u0107 na zmiany\n  \n  \/\/ Je\u015bli mamy wybrany termin prognozy, pod\u015bwietl odpowiedni przycisk\n  \/\/ hours mo\u017ce zawiera\u0107 liczby [0, 3, 6, ...] lub stringi [\"01\", \"02\", \"03\", ...]\n  \/\/ Sprawdzamy oba typy dla kompatybilno\u015bci\n  const currentHourNum = parseInt(currentHour, 10);\n  const currentHourStr = String(currentHour).padStart(2, \"0\");\n  \n  \/\/ Sprawd\u017a czy hours zawiera liczby czy stringi\n  const hoursContainsNumbers = hours.length > 0 && typeof hours[0] === 'number';\n  const hoursContainsStrings = hours.length > 0 && typeof hours[0] === 'string';\n  \n  const isCurrentHourAvailable = hoursContainsNumbers \n    ? hours.includes(currentHourNum) \n    : hours.includes(currentHourStr);\n  \n  if (currentHour && isCurrentHourAvailable) {\n    setActiveButton(\"hours\", currentHour);\n    selectedHour = currentHour;\n    \n    \/\/ Aktualizuj slider\n    const hourIndex = hoursContainsNumbers ? hours.indexOf(currentHourNum) : hours.indexOf(currentHourStr);\n    if (hourIndex !== -1) {\n      document.getElementById(\"forecastHourSlider\").value = hourIndex;\n    }\n    \n    \/\/ Wy\u015bwietl obrazek\n    showImages();\n  } else {\n    \/\/ Fallback: sprawd\u017a selectedHour\n    const selectedHourNum = parseInt(selectedHour, 10);\n    const selectedHourStr = String(selectedHour).padStart(2, \"0\");\n    const isSelectedHourAvailable = hoursContainsNumbers \n      ? hours.includes(selectedHourNum) \n      : hours.includes(selectedHourStr);\n    \n    if (selectedHour && isSelectedHourAvailable) {\n      setActiveButton(\"hours\", selectedHour);\n      \n      \/\/ Aktualizuj slider\n      const hourIndex = hoursContainsNumbers ? hours.indexOf(selectedHourNum) : hours.indexOf(selectedHourStr);\n      if (hourIndex !== -1) {\n        document.getElementById(\"forecastHourSlider\").value = hourIndex;\n      }\n      \n      \/\/ Wy\u015bwietl obrazek\n      showImages();\n    } else {\n      \/\/ Je\u015bli nie mamy wybranego terminu prognozy, wybierz domy\u015blny dla danego modelu\n      let defaultHour = defaultSettings[selectedModel].hour;\n      \/\/ Zapewniamy, \u017ce domy\u015blna godzina jest w odpowiednim formacie\n      defaultHour = String(defaultHour).padStart(2, \"0\");\n      \n      \/\/ Sprawd\u017a czy domy\u015blna godzina jest dost\u0119pna w aktualnych godzinach\n      const hourExists = hours.some(h => String(h).padStart(2, \"0\") === defaultHour);\n      if (hourExists) {\n        selectedHour = defaultHour;\n      } else {\n        \/\/ Je\u015bli nie, wybierz pierwsz\u0105 dost\u0119pn\u0105\n        selectedHour = String(hours[0]).padStart(2, \"0\");\n      }\n      \n      \/\/ Ustawiamy aktywny przycisk dla sformatowanej godziny\n      setActiveButton(\"hours\", selectedHour);\n      \n      const hourIndex = hours.findIndex(h => h === selectedHour.padStart(2, \"0\"));\n      document.getElementById(\"forecastHourSlider\").value = hourIndex !== -1 ? hourIndex : 0;\n      \n      \/\/ Wy\u015bwietl obrazek\n      showImages();\n    }\n  }\n}\n\n\/\/ Funkcja renderuj\u0105ca przyciski poziom\u00f3w ci\u015bnienia dla urz\u0105dze\u0144 mobilnych\nfunction renderPressureLevelButtons() {\n  const container = document.getElementById(\"pressureLevelButtons\");\n  if (!container) return;\n  \n  container.innerHTML = \"\";\n  \n  \/\/ Zachowaj poprzedni poziom je\u015bli jest dost\u0119pny, w przeciwnym razie ustaw pierwszy\n  if (!selectedPressureLevel || !pressureLevels.includes(selectedPressureLevel)) {\n    selectedPressureLevel = pressureLevels[0];\n  }\n  \n  pressureLevels.forEach(level => {\n    const btn = document.createElement(\"button\");\n    btn.dataset.value = level;\n    btn.textContent = `${level} hPa`;\n    \n    \/\/ Zaznacz aktywny przycisk\n    if (level === selectedPressureLevel) {\n      btn.classList.add(\"active\");\n    }\n    \n    \/\/ Obs\u0142uga klikni\u0119cia\n    btn.onclick = function() {\n      selectedPressureLevel = level;\n      \n      \/\/ Zaktualizuj wy\u015bwietlan\u0105 warto\u015b\u0107\n      document.getElementById(\"pressureValue\").textContent = `${level} hPa`;\n      \n      \/\/ Zaktualizuj slider (dla sp\u00f3jno\u015bci)\n      const levelIndex = pressureLevels.indexOf(level);\n      if (levelIndex !== -1) {\n        document.getElementById(\"pressureLevelSlider\").value = levelIndex;\n      }\n      \n      \/\/ Zaznacz aktywny przycisk\n      container.querySelectorAll(\"button\").forEach(b => b.classList.remove(\"active\"));\n      this.classList.add(\"active\");\n      \n      \/\/ Za\u0142aduj obrazek\n      showImages();\n      preloadForecastImages();\n    };\n    \n    container.appendChild(btn);\n  });\n}\n\n\/\/ Obs\u0142uga zdarzenia zmiany poziomu ci\u015bnienia\ndocument.getElementById(\"pressureLevelSlider\").addEventListener(\"input\", function() {\n  \/\/ Pobranie warto\u015bci z suwaka (0-5) i mapowanie jej na odpowiedni poziom ci\u015bnienia\n  const sliderValue = parseInt(this.value);\n  \/\/ Teraz kolejno\u015b\u0107 jest naturalna: 0=1000, 1=950, 2=850, 3=700, 4=500, 5=300\n  selectedPressureLevel = pressureLevels[sliderValue];\n  \n  \/\/ Aktualizacja wy\u015bwietlanej warto\u015bci\n  document.getElementById(\"pressureValue\").textContent = `${selectedPressureLevel} hPa`;\n  \n  \/\/ Aktualizuj przyciski mobilne\n  const buttonsContainer = document.getElementById(\"pressureLevelButtons\");\n  if (buttonsContainer) {\n    buttonsContainer.querySelectorAll(\"button\").forEach(btn => {\n      btn.classList.toggle(\"active\", btn.dataset.value === selectedPressureLevel);\n    });\n  }\n\n  \/\/ Od\u015bwie\u017cenie obrazka, je\u015bli wszystkie potrzebne parametry s\u0105 ju\u017c wybrane\n  if (selectedModel && selectedTerm && selectedParam && selectedHour && \n      (summaryModels.includes(selectedModel) || selectedStat)) {\n    showImages();\n    \/\/ Preload obrazk\u00f3w dla nowego poziomu ci\u015bnienia\n    preloadForecastImages();\n  }\n});\n\n\/\/ Funkcja do inicjalizacji i konfiguracji poziomego slidera termin\u00f3w prognozy\nfunction setupHorizontalSlider(hours) {\n  if (!hours || hours.length === 0) {\n    document.getElementById(\"horizontalSliderContainer\").style.display = \"none\";\n    return;\n  }\n  \n  document.getElementById(\"horizontalSliderContainer\").style.display = \"block\";\n  updateForecastHoursTitles();\n  \n  const slider = document.getElementById(\"forecastHourSlider\");\n  const formattedHours = hours.map(h => String(h).padStart(2, \"0\"));\n  \n  slider.min = 0;\n  slider.max = hours.length - 1;\n  \n  if (selectedHour) {\n    const hourIndex = formattedHours.indexOf(String(selectedHour).padStart(2, \"0\"));\n    slider.value = hourIndex !== -1 ? hourIndex : 0;\n  } else {\n    slider.value = 0;\n  }\n\n  renderHourSliderLabels(hours);\n  \n  const oldSlider = slider.cloneNode(true);\n  slider.parentNode.replaceChild(oldSlider, slider);\n  \n  let lastSliderPosition = -1;\n  \n  oldSlider.addEventListener(\"input\", function() {\n    const index = parseInt(this.value, 10);\n    \n    if (index === lastSliderPosition) return;\n    lastSliderPosition = index;\n    \n    selectedHour = formattedHours[index];\n    setActiveButton(\"hours\", selectedHour);\n    showImages();\n    \n    setTimeout(() => {\n      preloadForecastImages();\n    }, 10);\n  });\n}\n\n\/\/ Funkcja do generowania nazwy pliku na podstawie parametr\u00f3w\nfunction generateFileName(model, term, param, hour, stat, pressureLevel = null, region = null, dateSuffix = null) {\n  const fileExtension = \"webp\";\n  let paramToUse = param;\n\n  if (summaryModels.includes(model)) {\n    paramToUse = buildSummaryParamName(model, term, param, pressureLevel, region);\n    const hourKey = String(hour).padStart(2, \"0\");\n    const suffix = dateSuffix ||\n      (getStoredHourDateLabel(model, term, param, hourKey) || {}).suffix ||\n      getHourDateSuffix(model, term, param, hourKey);\n\n    if (suffix && (term === \"12h\" || term === \"1h\" || term === \"3h\")) {\n      return `${paramToUse}_${suffix}.${fileExtension}`;\n    }\n\n    return \"\";\n  }\n\n  if (pressureLevel) {\n    if (pressureLevel === \"1000\") {\n      paramToUse = param + pressureLevel;\n    } else {\n      paramToUse = param + \"0\" + pressureLevel;\n    }\n  }\n\n  const formattedHour = String(hour).padStart(2, \"0\");\n\n  if (model === \"icon_lagged\") {\n    return `${formattedHour}_${paramToUse}_${stat}.${fileExtension}`;\n  }\n\n  return `${formattedHour}_${paramToUse}_${stat}.${fileExtension}`;\n}\n\n\/\/ System preloadingu (jak w srednioterm.html)\nconst PRELOAD_CONCURRENCY = 6;\nlet preloadQueue = [];\nlet preloadGeneration = 0;\nlet isPreloading = false;\nlet comparePreloadQueue = [];\nlet comparePreloadGeneration = 0;\nlet isComparePreloading = false;\nlet imageLoadPromises = {};\n\nfunction getLoadedHoursConfigKey(model, term, param, pressureLevel) {\n  return model + \"_\" + term + \"_\" + param + \"_\" + pressureLevel;\n}\n\nfunction isHourMarkedAsLoaded(hour, model, term, param, pressureLevel) {\n  const configKey = getLoadedHoursConfigKey(model, term, param, pressureLevel);\n  const hourStr = String(hour).padStart(2, \"0\");\n  return !!(loadedHours[configKey] && loadedHours[configKey].has(hourStr));\n}\n\nfunction getImageCacheLimit() {\n  const hours = getCurrentForecastHoursList();\n  const compareModels = (compareMode && getActiveCompareModelIds().length >= 2)\n    ? getVisibleCompareModelIds()\n    : null;\n  const modelCount = compareModels ? Math.max(1, compareModels.length) : 1;\n  \/\/ W por\u00f3wnaniu ka\u017cda godzina trzyma osobny obrazek per model.\n  return Math.max(120, hours.length * modelCount + 24);\n}\n\nfunction getProtectedImageCacheKeys() {\n  const protectedKeys = {};\n  const hours = getCurrentForecastHoursList();\n\n  if (compareMode && getActiveCompareModelIds().length >= 2) {\n    const compareModels = getVisibleCompareModelIds();\n    hours.forEach(function(hour) {\n      compareModels.forEach(function(model) {\n        if (!isHourAvailableForCompareModel(model, hour)) {\n          return;\n        }\n        protectedKeys[getCompareImageCacheKey(model, hour)] = true;\n      });\n    });\n    return protectedKeys;\n  }\n\n  hours.forEach(function(hour) {\n    protectedKeys[buildHourImageMeta(hour).imageId] = true;\n  });\n  return protectedKeys;\n}\n\nfunction trimImageCacheIfNeeded() {\n  const keys = Object.keys(imageCache);\n  const limit = getImageCacheLimit();\n  if (keys.length <= limit) {\n    return;\n  }\n\n  const protectedKeys = getProtectedImageCacheKeys();\n  let remaining = keys.length;\n\n  for (let i = 0; i < keys.length && remaining > limit; i += 1) {\n    const key = keys[i];\n    if (protectedKeys[key]) {\n      continue;\n    }\n    delete imageCache[key];\n    remaining -= 1;\n  }\n}\n\nfunction clearAllLoadedHours() {\n  Object.keys(loadedHours).forEach(function(key) {\n    delete loadedHours[key];\n  });\n}\n\nfunction buildHourImageMeta(hour) {\n  const formattedHour = String(hour).padStart(2, \"0\");\n  const pressureLevelToUse = summaryModels.includes(selectedModel) ? null : selectedPressureLevel;\n  const effectiveRegion = (summaryModels.includes(selectedModel) && hasRegions(selectedModel, selectedTerm, selectedParam)) ? selectedRegion : null;\n  const fileName = generateFileName(\n    selectedModel,\n    selectedTerm,\n    selectedParam,\n    formattedHour,\n    selectedStat,\n    pressureLevelToUse,\n    effectiveRegion\n  );\n  const url = `\/wp-content\/uploads\/production\/prognozy_wiazkowe\/${selectedModel}\/${selectedTerm}\/${fileName}`;\n  const imageId = `img_${selectedModel}_${selectedTerm}_${selectedParam}_${formattedHour}_${selectedStat}_${pressureLevelToUse}_${effectiveRegion}`.replace(\/\\s+\/g, \"_\");\n\n  return {\n    formattedHour: formattedHour,\n    fileName: fileName,\n    url: url,\n    imageId: imageId,\n    pressureLevelToUse: pressureLevelToUse,\n    effectiveRegion: effectiveRegion,\n    hourNum: parseInt(formattedHour, 10)\n  };\n}\n\nfunction isHourImageLoaded(hour) {\n  const meta = buildHourImageMeta(hour);\n  const cached = imageCache[meta.imageId];\n  return !!(cached && cached.complete && cached.naturalWidth > 0);\n}\n\nfunction getHourLoadState(hour) {\n  if (compareMode && getActiveCompareModelIds().length >= 2) {\n    const compareModels = getVisibleCompareModelIds();\n    let hasAvailableModel = false;\n    let anyLoading = false;\n    let allLoaded = true;\n\n    if (!compareModels.length) {\n      return \"pending\";\n    }\n\n    compareModels.forEach(function(model) {\n      if (!isHourAvailableForCompareModel(model, hour)) {\n        return;\n      }\n      hasAvailableModel = true;\n      const cacheKey = getCompareImageCacheKey(model, hour);\n      const cached = imageCache[cacheKey];\n      if (cached && cached.complete && cached.naturalWidth > 0) {\n        return;\n      }\n      allLoaded = false;\n      if (imageLoadPromises[cacheKey]) {\n        anyLoading = true;\n      }\n    });\n\n    if (hasAvailableModel && allLoaded) {\n      return \"loaded\";\n    }\n    if (anyLoading) {\n      return \"loading\";\n    }\n    return \"pending\";\n  }\n\n  const meta = buildHourImageMeta(hour);\n  if (isHourMarkedAsLoaded(hour, selectedModel, selectedTerm, selectedParam, meta.pressureLevelToUse)) {\n    return \"loaded\";\n  }\n  if (isHourImageLoaded(hour)) {\n    markHourAsLoaded(selectedModel, selectedTerm, selectedParam, meta.hourNum, meta.pressureLevelToUse, false);\n    return \"loaded\";\n  }\n  if (imageLoadPromises[meta.imageId]) {\n    return \"loading\";\n  }\n  return \"pending\";\n}\n\nfunction applyPreloadClassesToButton(button, loadState) {\n  button.classList.remove(\"preload-pending\", \"preload-loading\", \"preload-loaded\", \"loaded\");\n  if (loadState === \"pending\") {\n    button.classList.add(\"preload-pending\");\n  }\n  if (loadState === \"loading\") {\n    button.classList.add(\"preload-pending\");\n    button.classList.add(\"preload-loading\");\n  }\n  if (loadState === \"loaded\") {\n    button.classList.add(\"preload-loaded\");\n  }\n}\n\nfunction getCurrentForecastHoursList() {\n  if (compareMode && getActiveCompareModelIds().length >= 2) {\n    return getCompareForecastHoursList();\n  }\n  return getForecastHoursForModel(selectedModel, selectedTerm, selectedParam);\n}\n\nfunction buildHourPreloadOrder() {\n  const hours = getCurrentForecastHoursList();\n  const selectedHourNorm = String(selectedHour).padStart(2, \"0\");\n  const selectedHourIndex = hours.indexOf(selectedHourNorm);\n  if (selectedHourIndex === -1) {\n    return [];\n  }\n\n  const order = [];\n  const maxOffset = Math.max(selectedHourIndex, hours.length - selectedHourIndex - 1);\n\n  for (let offset = 0; offset <= maxOffset; offset += 1) {\n    if (offset === 0) {\n      order.push(hours[selectedHourIndex]);\n      continue;\n    }\n    if (selectedHourIndex + offset < hours.length) {\n      order.push(hours[selectedHourIndex + offset]);\n    }\n    if (selectedHourIndex - offset >= 0) {\n      order.push(hours[selectedHourIndex - offset]);\n    }\n  }\n\n  return order;\n}\n\nfunction resetPreloadQueue() {\n  preloadGeneration += 1;\n  preloadQueue = [];\n  isPreloading = false;\n}\n\nfunction ensureHourImage(hour) {\n  const meta = buildHourImageMeta(hour);\n  const cacheKey = meta.imageId;\n\n  if (isHourImageLoaded(hour)) {\n    return Promise.resolve(imageCache[cacheKey]);\n  }\n\n  if (imageLoadPromises[cacheKey]) {\n    return imageLoadPromises[cacheKey];\n  }\n\n  const candidates = generateFileNameCandidates(\n    selectedModel,\n    selectedTerm,\n    selectedParam,\n    meta.formattedHour,\n    selectedStat,\n    meta.pressureLevelToUse,\n    meta.effectiveRegion\n  );\n  const basePath = `\/wp-content\/uploads\/production\/prognozy_wiazkowe\/${selectedModel}\/${selectedTerm}\/`;\n\n  imageLoadPromises[cacheKey] = new Promise(function(resolve, reject) {\n    let candidateIndex = 0;\n    const img = new Image();\n\n    function tryNextCandidate() {\n      if (candidateIndex >= candidates.length) {\n        delete imageLoadPromises[cacheKey];\n        refreshPreloadUi();\n        reject(new Error(\"Nie uda\u0142o si\u0119 za\u0142adowa\u0107 obrazu dla terminu \" + meta.formattedHour));\n        return;\n      }\n\n      refreshPreloadUi();\n\n      img.onload = function() {\n        imageCache[cacheKey] = img;\n        trimImageCacheIfNeeded();\n        const loadedDateSuffix = parseDateSuffixFromFileName(candidates[candidateIndex]);\n        if (loadedDateSuffix) {\n          rememberHourDateLabel(selectedModel, selectedTerm, selectedParam, meta.formattedHour, loadedDateSuffix);\n        }\n        markHourAsLoaded(selectedModel, selectedTerm, selectedParam, meta.hourNum, meta.pressureLevelToUse);\n        delete imageLoadPromises[cacheKey];\n        refreshPreloadUi();\n        resolve(img);\n      };\n\n      img.onerror = function() {\n        candidateIndex += 1;\n        tryNextCandidate();\n      };\n\n      img.src = basePath + candidates[candidateIndex];\n    }\n\n    tryNextCandidate();\n  });\n\n  return imageLoadPromises[cacheKey];\n}\n\nfunction enqueueHourPreloads(hours) {\n  const unique = {};\n  const filtered = [];\n\n  hours.forEach(function(hour) {\n    const hourNorm = String(hour).padStart(2, \"0\");\n    if (!unique[hourNorm] && !isHourImageLoaded(hourNorm)) {\n      unique[hourNorm] = true;\n      filtered.push(hourNorm);\n    }\n  });\n\n  if (!filtered.length) {\n    refreshPreloadUi();\n    return;\n  }\n\n  const existing = {};\n  const merged = [];\n\n  filtered.forEach(function(hour) {\n    merged.push(hour);\n    existing[hour] = true;\n  });\n  preloadQueue.forEach(function(hour) {\n    if (!existing[hour]) {\n      merged.push(hour);\n      existing[hour] = true;\n    }\n  });\n  preloadQueue = merged;\n\n  if (!isPreloading) {\n    processPreloadQueue(preloadGeneration);\n  }\n}\n\nasync function processPreloadQueue(generation) {\n  if (isPreloading) {\n    return;\n  }\n\n  isPreloading = true;\n  try {\n    while (generation === preloadGeneration && preloadQueue.length > 0) {\n      const batch = preloadQueue.splice(0, PRELOAD_CONCURRENCY);\n      await Promise.all(batch.map(function(hour) {\n        return ensureHourImage(hour).catch(function() {\n          return null;\n        });\n      }));\n      if (generation === preloadGeneration) {\n        refreshPreloadUi();\n      }\n    }\n  } finally {\n    isPreloading = false;\n    if (preloadQueue.length > 0) {\n      processPreloadQueue(preloadGeneration);\n    }\n  }\n}\n\nfunction resetComparePreloadQueue() {\n  comparePreloadGeneration += 1;\n  comparePreloadQueue = [];\n  isComparePreloading = false;\n}\n\nfunction buildComparePreloadOrder() {\n  const hours = getCompareForecastHoursList();\n  const compareModels = getVisibleCompareModelIds();\n  const tasks = [];\n  const seen = {};\n\n  function addTask(model, referenceHour) {\n    const key = model + \":\" + referenceHour;\n    if (seen[key]) {\n      return;\n    }\n    seen[key] = true;\n    tasks.push({ model: model, hour: referenceHour });\n  }\n\n  hours.forEach(function(referenceHour) {\n    compareModels.forEach(function(model) {\n      if (isHourAvailableForCompareModel(model, referenceHour)) {\n        addTask(model, referenceHour);\n      }\n    });\n  });\n\n  return tasks;\n}\n\nfunction enqueueComparePreloads(tasks) {\n  const filtered = tasks.filter(function(task) {\n    return !isCompareHourImageLoaded(task.model, task.hour);\n  });\n\n  if (!filtered.length) {\n    refreshPreloadUi();\n    return;\n  }\n\n  const existing = {};\n  const merged = [];\n\n  filtered.forEach(function(task) {\n    const key = task.model + \":\" + task.hour;\n    merged.push(task);\n    existing[key] = true;\n  });\n  comparePreloadQueue.forEach(function(task) {\n    const key = task.model + \":\" + task.hour;\n    if (!existing[key]) {\n      merged.push(task);\n      existing[key] = true;\n    }\n  });\n  comparePreloadQueue = merged;\n\n  if (!isComparePreloading) {\n    processComparePreloadQueue(comparePreloadGeneration);\n  }\n}\n\nasync function processComparePreloadQueue(generation) {\n  if (isComparePreloading) {\n    return;\n  }\n\n  isComparePreloading = true;\n  try {\n    while (generation === comparePreloadGeneration && comparePreloadQueue.length > 0) {\n      const batch = comparePreloadQueue.splice(0, PRELOAD_CONCURRENCY);\n      await Promise.all(batch.map(function(task) {\n        return ensureCompareModelHourImage(task.model, task.hour).catch(function() {\n          return null;\n        });\n      }));\n      if (generation === comparePreloadGeneration) {\n        refreshPreloadUi();\n      }\n    }\n  } finally {\n    isComparePreloading = false;\n    if (comparePreloadQueue.length > 0) {\n      processComparePreloadQueue(comparePreloadGeneration);\n    }\n  }\n}\n\nfunction preloadCompareForecastImages() {\n  if (!compareMode || getActiveCompareModelIds().length < 2) {\n    return;\n  }\n  enqueueComparePreloads(buildComparePreloadOrder());\n}\n\nfunction preloadForecastImages() {\n  if (!selectedModel || !selectedTerm || !selectedParam) {\n    return;\n  }\n  if (!summaryModels.includes(selectedModel) && !selectedStat) {\n    return;\n  }\n\n  if (compareMode && getActiveCompareModelIds().length >= 2) {\n    preloadCompareForecastImages();\n    return;\n  }\n\n  enqueueHourPreloads(buildHourPreloadOrder());\n}\n\nfunction renderHourSliderGradient(hours) {\n  const slider = document.getElementById(\"forecastHourSlider\");\n  if (!slider) {\n    return;\n  }\n\n  if (!hours || hours.length <= 1) {\n    slider.style.background = \"#e0e0e0\";\n    return;\n  }\n\n  const segmentWidth = 100 \/ hours.length;\n  const gradientParts = [];\n\n  for (let index = 0; index < hours.length; index += 1) {\n    const start = index * segmentWidth;\n    const end = (index + 1) * segmentWidth;\n    const color = getHourLoadState(hours[index]) === \"loaded\" ? \"#c7c7c7\" : \"#e0e0e0\";\n    gradientParts.push(color + \" \" + start + \"% \" + end + \"%\");\n  }\n\n  slider.style.background = \"linear-gradient(to right, \" + gradientParts.join(\", \") + \")\";\n}\n\nfunction refreshPreloadUi() {\n  const hours = getCurrentForecastHoursList();\n  const hourButtons = document.querySelectorAll(\"#hours button\");\n\n  hourButtons.forEach(function(button) {\n    const hour = button.dataset.value;\n    if (!hour) {\n      return;\n    }\n    applyPreloadClassesToButton(button, getHourLoadState(hour));\n  });\n\n  if (hours.length > 1) {\n    renderHourSliderLabels(hours);\n  }\n  renderHourSliderGradient(hours);\n}\n\nfunction markHourAsLoaded(model, term, param, hour, pressureLevel, refreshUi) {\n  const configKey = getLoadedHoursConfigKey(model, term, param, pressureLevel);\n  if (!loadedHours[configKey]) {\n    loadedHours[configKey] = new Set();\n  }\n  const hourStr = String(hour).padStart(2, \"0\");\n  loadedHours[configKey].add(hourStr);\n\n  if (refreshUi !== false &&\n      model === selectedModel && term === selectedTerm &&\n      param === selectedParam && pressureLevel === selectedPressureLevel) {\n    refreshPreloadUi();\n  }\n}\n\nfunction updateLoadedHoursVisualization() {\n  refreshPreloadUi();\n}\n\n\/\/ Funkcja do aktualizacji interfejsu na podstawie wybranych warto\u015bci\nfunction updateTermLabel() {\n  const termLabel = document.getElementById(\"termTitle\");\n  if (termLabel) {\n    termLabel.textContent = summaryModels.includes(selectedModel) ? \"Zestawienie\" : \"Termin startu\";\n  }\n}\n\nfunction updateTermButtons() {\n  if (!selectedModel) return;\n  \n  \/\/ Aktualizacja etykiety na podstawie wybranego modelu\n  updateTermLabel();\n  \n  \/\/ Ustawienie domy\u015blnych warto\u015bci z ustawie\u0144 dla danego modelu\n  if (!selectedTerm) {\n    selectedTerm = defaultSettings[selectedModel].term;\n  }\n  ensureSelectedParamValidForModelTerm(selectedModel, selectedTerm);\n  if (!selectedHour) {\n    selectedHour = defaultSettings[selectedModel].hour;\n  }\n  if (!selectedStat && defaultSettings[selectedModel].stat) {\n    selectedStat = defaultSettings[selectedModel].stat;\n  }\n  \n  \/\/ Renderowanie przycisk\u00f3w terminu\n  \n  if (summaryModels.includes(selectedModel)) {\n    \/\/ Dla a-laef pokazujemy 1h i 12h\n    if (selectedModel === \"a-laef\") {\n      renderButtons(\"terms\", alaefVariants, (variant) => {\n        if (compareMode && compareSelectedModels.length >= 2) {\n          handleCompareTermChange(variant);\n          return;\n        }\n\n        selectedTerm = variant;\n        \n        \/\/ Wyczy\u015b\u0107 cache paramHours dla a-laef przy zmianie wariantu\n        if (paramHours[\"a-laef\"]) {\n          paramHours[\"a-laef\"] = {};\n          clearHourDateLabelsForModel(\"a-laef\");\n          clearTermDateSuffixCacheForModel(\"a-laef\");\n        }\n        \n        if (variant === \"1h\") {\n          selectedParam = resolveParamForModelSwitch(selectedParam, \"a-laef\", variant);\n          document.getElementById(\"hours\").innerHTML = \"\";\n          document.getElementById(\"images\").innerHTML = \"\";\n          determineAvailableHours(selectedModel, variant, selectedParam)\n            .then(() => {\n              const hoursForParam = paramHours[selectedModel][selectedParam] || [];\n              if (hoursForParam.length > 0) {\n                selectedHour = String(hoursForParam[0]).padStart(2, \"0\");\n              }\n              renderParams();\n              renderRegions();\n              renderForecastHours();\n            });\n        } else { \/\/ 12h\n          selectedParam = resolveParamForModelSwitch(selectedParam, \"a-laef\", variant);\n          \/\/ Zachowaj godzin\u0119 je\u015bli jest dost\u0119pna, inaczej pierwsza dost\u0119pna\n          const allowedHours = forecastHours[\"a-laef\"][\"12h\"].map(h => String(h).padStart(2, \"0\"));\n          if (!allowedHours.includes(selectedHour)) {\n            selectedHour = allowedHours[0];\n          }\n          \n          \/\/ Wyczy\u015b\u0107 interfejs\n          document.getElementById(\"hours\").innerHTML = \"\";\n          document.getElementById(\"images\").innerHTML = \"\";\n          \n          \/\/ Sprawd\u017a dost\u0119pne godziny dla nowego wariantu i parametru (tylko dla 12h)\n          determineAvailableHours(selectedModel, variant, selectedParam)\n            .then(() => {\n              renderParams();\n              renderRegions();\n              renderForecastHours();\n            });\n        }\n      }, selectedTerm);\n    } else if (selectedModel === \"ecmwf_ifs_ens\") {\n      \/\/ Dla ecmwf_ifs_ens pokazujemy 3h i 12h\n      const variants = [\"3h\", \"12h\"];\n      renderButtons(\"terms\", variants, (variant) => {\n        if (compareMode && compareSelectedModels.length >= 2) {\n          handleCompareTermChange(variant);\n          return;\n        }\n\n        selectedTerm = variant;\n        \n        selectedParam = resolveParamForModelSwitch(selectedParam, \"ecmwf_ifs_ens\", variant);\n        \n        if (paramHours[\"ecmwf_ifs_ens\"]) {\n          paramHours[\"ecmwf_ifs_ens\"] = {};\n          clearHourDateLabelsForModel(\"ecmwf_ifs_ens\");\n          clearTermDateSuffixCacheForModel(\"ecmwf_ifs_ens\");\n        }\n\n        \/\/ Wyczy\u015b\u0107 interfejs\n        document.getElementById(\"hours\").innerHTML = \"\";\n        document.getElementById(\"images\").innerHTML = \"\";\n        \n        \/\/ Sprawd\u017a dost\u0119pne godziny dla nowego wariantu i parametru\n        determineAvailableHours(selectedModel, variant, selectedParam)\n          .then(() => {\n            const hoursForParam = paramHours[selectedModel][selectedParam] || [];\n            if (hoursForParam.length > 0) {\n              selectedHour = String(hoursForParam[0]).padStart(2, \"0\");\n            } else {\n              selectedHour = variant === \"3h\" ? \"01\" : \"01\";\n            }\n            renderParams();\n            renderRegions();\n            renderForecastHours();\n          });\n      }, selectedTerm);\n    } else {\n      \/\/ Dla pozosta\u0142ych modeli typu \"podsumowanie\"\n      const variants = summaryTermVariants[selectedModel] || [\"12h\"];\n      renderButtons(\"terms\", variants, (variant) => {\n        if (compareMode && compareSelectedModels.length >= 2) {\n          handleCompareTermChange(variant);\n          return;\n        }\n\n        selectedTerm = variant;\n        \n        \/\/ Przypisanie odpowiedniego domy\u015blnego parametru dla wybranego modelu typu \"podsumowanie\"\n        if (selectedModel === \"cosmo_ens\") {\n          selectedParam = resolveParamForModelSwitch(selectedParam, \"cosmo_ens\", variant);\n\n          const hoursContainer = document.getElementById(\"hours\");\n          if (hoursContainer) {\n            hoursContainer.innerHTML = '<div class=\"loading-hours\">Aktualizacja parametr\u00f3w...<\/div>';\n          }\n          if (paramHours[\"cosmo_ens\"]) {\n            paramHours[\"cosmo_ens\"] = {};\n            clearHourDateLabelsForModel(\"cosmo_ens\");\n            clearTermDateSuffixCacheForModel(\"cosmo_ens\");\n          }\n          determineAvailableHours(selectedModel, variant, selectedParam)\n            .then(() => {\n              const hoursForParam = paramHours[selectedModel][selectedParam] || [];\n              const fallbackHour = forecastHours[selectedModel]?.[variant]?.[0];\n              selectedHour = hoursForParam.length > 0\n                ? String(hoursForParam[0]).padStart(2, \"0\")\n                : String(fallbackHour || 1).padStart(2, \"0\");\n              renderParams();\n              renderRegions();\n              renderForecastHours();\n            });\n          return; \/\/ Przerywanie wykonania\n          \n        } else if (selectedModel === \"icon_ens\") {\n          selectedParam = resolveParamForModelSwitch(selectedParam, \"icon_ens\", variant);\n\n          const hoursContainer = document.getElementById(\"hours\");\n          if (hoursContainer) {\n            hoursContainer.innerHTML = '<div class=\"loading-hours\">Aktualizacja parametr\u00f3w...<\/div>';\n          }\n          if (paramHours[\"icon_ens\"]) {\n            paramHours[\"icon_ens\"] = {};\n            clearHourDateLabelsForModel(\"icon_ens\");\n            clearTermDateSuffixCacheForModel(\"icon_ens\");\n          }\n          determineAvailableHours(selectedModel, variant, selectedParam)\n            .then(() => {\n              const hoursForParam = paramHours[selectedModel][selectedParam] || [];\n              const fallbackHour = forecastHours[selectedModel]?.[variant]?.[0];\n              selectedHour = hoursForParam.length > 0\n                ? String(hoursForParam[0]).padStart(2, \"0\")\n                : String(fallbackHour || 1).padStart(2, \"0\");\n              renderParams();\n              renderRegions();\n              renderForecastHours();\n            });\n          return; \/\/ Przerywanie wykonania\n          \n        } else {\n          selectedParam = resolveParamForModelSwitch(selectedParam, selectedModel, variant);\n          selectedHour = \"1\"; \/\/ Domy\u015blna godzina dla modeli typu \"podsumowanie\"\n        }\n        \n        renderParams();\n        renderRegions();\n      }, selectedTerm);\n    }\n  } else {\n    \/\/ Dla standardowych modeli pokazujemy standardowe terminy startu\n    renderButtons(\"terms\", termsDefault, (term) => {\n      selectedTerm = term;\n      renderParams();\n    }, selectedTerm);\n  }\n  \n  renderParams();\n}\n\n\/\/ Funkcja do optymalizacji pobierania element\u00f3w DOM - buforuje znalezione elementy\nconst domCache = {};\nfunction getElementCached(id) {\n  if (!domCache[id]) {\n    domCache[id] = document.getElementById(id);\n  }\n  return domCache[id];\n}\n\n\/\/ Funkcja do resetowania wszystkich flag wykonywania\nfunction resetAllExecutionFlags() {\n  isShowImagesRunning = false;\n  isShowImagesRunningTimestamp = 0;\n  isRenderingForecastHours = false;\n  isRenderingForecastHoursTimestamp = 0;\n\n  if (loaderTimeout) {\n    clearTimeout(loaderTimeout);\n    loaderTimeout = null;\n  }\n\n  if (imageLoadTimeout) {\n    clearTimeout(imageLoadTimeout);\n    imageLoadTimeout = null;\n  }\n}\n\n\/\/ Funkcja resetuj\u0105ca stan przy zmianie modelu\nfunction resetStateForModelChange() {\n  \/\/ Resetuj wszystkie flagi wykonywania\n  resetAllExecutionFlags();\n  \n  \/\/ Resetuj tylko selectedRegion, ale NIE ukrywaj sekcji - renderRegions() to zrobi\n  selectedRegion = null;\n  \n  \/\/ Resetuj bufor obraz\u00f3w - czy\u015bcimy zawarto\u015b\u0107 obiektu zamiast przypisywa\u0107 nowy\n  Object.keys(imageCache).forEach(key => {\n    delete imageCache[key];\n  });\n  imageLoadPromises = {};\n  clearAllLoadedHours();\n  resetPreloadQueue();\n  resetComparePreloadQueue();\n  \n  \/\/ Resetuj ostatnio wy\u015bwietlany obraz\n  lastDisplayedImage = {\n    model: \"\",\n    term: \"\",\n    param: \"\",\n    hour: \"\",\n    stat: \"\",\n    pressureLevel: \"\",\n    region: \"\"\n  };\n  \n  \/\/ Wyczy\u015b\u0107 aktywny timeout loadera je\u015bli istnieje\n  if (loaderTimeout) {\n    clearTimeout(loaderTimeout);\n    loaderTimeout = null;\n  }\n  \n  \/\/ Wyczy\u015b\u0107 kontenery obraz\u00f3w\n  const imagesContainer = getElementCached(\"images\");\n  if (imagesContainer) {\n    imagesContainer.innerHTML = \"\";\n  }\n  \n  \/\/ Wyczy\u015b\u0107 timeout \u0142adowania obrazu je\u015bli istnieje\n  if (imageLoadTimeout) {\n    clearTimeout(imageLoadTimeout);\n    imageLoadTimeout = null;\n  }\n}\n\n\/\/ Timeout \u0142adowania obrazu\nlet imageLoadTimeout = null;\n\n\/\/ Zmienna do \u015bledzenia ostatniego wy\u015bwietlonego obrazu\nlet lastDisplayedImage = {\n  model: \"\",\n  term: \"\",\n  param: \"\",\n  hour: \"\",\n  stat: \"\",\n  pressureLevel: \"\",\n  region: \"\"\n};\n\n\/\/ Bufor za\u0142adowanych obraz\u00f3w dla optymalizacji\nconst imageCache = {};\n\n\/\/ \u015aledzenie za\u0142adowanych godzin dla wizualizacji\nconst loadedHours = {};\n\n\/\/ Zmienna do \u015bledzenia, czy funkcja showImages jest aktualnie wykonywana\nlet isShowImagesRunning = false;\nlet isShowImagesRunningTimestamp = 0;\n\/\/ Zmienne do \u015bledzenia ostatniego \u017c\u0105dania obrazu\nlet latestImageRequest = null;\nlet pendingImageRequest = false;\n\nfunction enableImageZoomClick(img, altText) {\n  if (altText) {\n    img.alt = altText;\n  }\n  img.style.cursor = \"zoom-in\";\n  img.addEventListener(\"click\", function() {\n    openLightbox(img.currentSrc || img.src);\n  });\n  return img;\n}\n\nfunction appendZoomableImage(container, img, imageId, altText) {\n  if (imageId) {\n    img.id = imageId;\n  }\n  img.style.display = \"block\";\n  container.appendChild(enableImageZoomClick(img, altText));\n}\n\nfunction openLightbox(src) {\n  const box = document.getElementById(\"wiazkoweLightbox\");\n  const img = document.getElementById(\"wiazkoweLightboxImg\");\n  if (!box || !img || !src) {\n    return;\n  }\n  img.src = src;\n  box.classList.add(\"active\");\n}\n\nfunction closeLightbox() {\n  const box = document.getElementById(\"wiazkoweLightbox\");\n  const img = document.getElementById(\"wiazkoweLightboxImg\");\n  if (!box || !img) {\n    return;\n  }\n  box.classList.remove(\"active\");\n  img.src = \"\";\n}\n\nfunction isLightboxOpen() {\n  const box = document.getElementById(\"wiazkoweLightbox\");\n  return !!(box && box.classList.contains(\"active\"));\n}\n\nfunction showImages() {\n  if (!selectedModel || !selectedTerm || !selectedParam || !selectedHour) return;\n  if (!summaryModels.includes(selectedModel) && !selectedStat) return;\n\n  if (compareMode) {\n    if (!validateCompareMode()) {\n      \/\/ fall through to normal view\n    } else {\n      showCompareImages();\n      return;\n    }\n  }\n\n  updateCompareButtons();\n  \n  \/\/ Dla modeli podsumowania wymuszamy selectedPressureLevel na null\n  const effectivePressureLevel = summaryModels.includes(selectedModel) ? null : selectedPressureLevel;\n  \n  \/\/ Efektywny region - tylko dla modeli z podsumowania i parametr\u00f3w z regionami\n  const effectiveRegion = (summaryModels.includes(selectedModel) && hasRegions(selectedModel, selectedTerm, selectedParam)) ? selectedRegion : null;\n  \n  \/\/ Zapami\u0119taj aktualne \u017c\u0105danie\n  latestImageRequest = {\n    model: selectedModel,\n    term: selectedTerm,\n    param: selectedParam,\n    hour: selectedHour,\n    stat: selectedStat,\n    pressureLevel: effectivePressureLevel,\n    region: effectiveRegion\n  };\n  \n  \/\/ Je\u015bli funkcja jest ju\u017c w trakcie wykonywania, zaznacz, \u017ce jest nowe \u017c\u0105danie oczekuj\u0105ce\n  if (isShowImagesRunning) {\n    pendingImageRequest = true;\n    return;\n  }\n  \n  \/\/ Ustaw flag\u0119 na true - funkcja jest teraz wykonywana\n  isShowImagesRunning = true;\n  isShowImagesRunningTimestamp = Date.now();\n  \n  try {\n    \/\/ Generujemy unikalny identyfikator obrazu na podstawie parametr\u00f3w zanim cokolwiek zrobimy\n    const imageId = `img_${selectedModel}_${selectedTerm}_${selectedParam}_${selectedHour}_${selectedStat}_${effectivePressureLevel}_${effectiveRegion}`.replace(\/\\s+\/g, '_');\n  \n  \/\/ Sprawd\u017a, czy te same parametry obrazu s\u0105 ju\u017c wy\u015bwietlane (zapobieganie duplikacji)\n  if (lastDisplayedImage && \n      lastDisplayedImage.model === selectedModel &&\n      lastDisplayedImage.term === selectedTerm &&\n      lastDisplayedImage.param === selectedParam &&\n      lastDisplayedImage.hour === selectedHour &&\n      lastDisplayedImage.stat === selectedStat &&\n      lastDisplayedImage.pressureLevel === effectivePressureLevel &&\n      lastDisplayedImage.region === effectiveRegion) {\n    \/\/ Sprawd\u017a, czy obraz faktycznie istnieje w DOM\n    if (document.getElementById(imageId)) {\n      \/\/ Resetuj flag\u0119 wykonywania\n      isShowImagesRunning = false;\n      return; \/\/ Obraz ju\u017c istnieje, przerywamy\n    }\n    \/\/ Je\u015bli obraz nie istnieje mimo \u017ce parametry si\u0119 zgadzaj\u0105, kontynuujemy aby go doda\u0107\n  }\n  \n  \/\/ U\u017cywamy zoptymalizowanego dost\u0119pu do element\u00f3w DOM\n  const container = getElementCached(\"images\");\n  \n  \/\/ Dodatkowe sprawdzenie czy obraz z tym ID ju\u017c istnieje w DOM\n  if (document.getElementById(imageId)) {\n    \/\/ Resetuj flag\u0119 wykonywania\n    isShowImagesRunning = false;\n    return; \/\/ Przerywamy funkcj\u0119 je\u015bli obraz ju\u017c istnieje\n  }\n  \n  \/\/ Sprawd\u017a, czy mamy ju\u017c za\u0142adowany ten obraz w buforze\n  const isCachedImage = imageCache[imageId];\n  \n  \/\/ Tworzymy znacznik identyfikuj\u0105cy to konkretne wywo\u0142anie funkcji\n  const requestId = Date.now();\n  container.dataset.lastRequestId = requestId;\n  \n  \/\/ Je\u015bli obraz jest w buforze, dodajmy go natychmiast bez pokazywania animacji \u0142adowania\n  if (isCachedImage) {\n    if (container.querySelector(\".compare-grid-side, .compare-grid, .compare-card\")) {\n      container.innerHTML = \"\";\n    }\n\n    \/\/ Sprawd\u017a czy przypadkiem nie jest to dok\u0142adnie ten sam obraz, kt\u00f3ry ju\u017c jest wy\u015bwietlany\n    const existingImages = container.querySelectorAll(\"img\");\n    if (existingImages.length === 1 && existingImages[0].id === imageId) {\n      isShowImagesRunning = false;\n      return;\n    }\n    \n    \/\/ Sprawd\u017a czy ju\u017c jest jaki\u015b obraz w kontenerze\n    if (existingImages.length > 0) {\n      container.innerHTML = \"\";\n      const cachedImg = imageCache[imageId].cloneNode(true);\n      appendZoomableImage(container, cachedImg, imageId);\n    } else {\n      \/\/ Je\u015bli nie ma obrazu, usu\u0144 loadery i dodaj nowy\n      container.innerHTML = '';\n      const cachedImg = imageCache[imageId].cloneNode(true);\n      appendZoomableImage(container, cachedImg, imageId);\n    }\n    \n    \/\/ Zapisz stan ostatniego wy\u015bwietlonego obrazu\n    lastDisplayedImage = {\n      model: selectedModel,\n      term: selectedTerm,\n      param: selectedParam,\n      hour: selectedHour,\n      stat: selectedStat,\n      pressureLevel: effectivePressureLevel,\n      region: effectiveRegion,\n      imageId: imageId\n    };\n\n    delete imageLoadPromises[imageId];\n    markHourAsLoaded(selectedModel, selectedTerm, selectedParam, parseInt(selectedHour, 10), effectivePressureLevel);\n    preloadForecastImages();\n    \n    \/\/ WA\u017bNE: Resetuj flag\u0119 wykonywania, poniewa\u017c sko\u0144czyli\u015bmy \u0142adowa\u0107 obraz z cache\n    isShowImagesRunning = false;\n    \n    return; \/\/ Ko\u0144czymy funkcj\u0119 wcze\u015bniej - bez pokazywania loadera\n  }\n  \n  \/\/ Je\u015bli obrazu NIE MA w buforze, teraz czy\u015bcimy kontener i pokazujemy loader\n  container.innerHTML = \"\";\n  \n  \/\/ Dodanie identyfikatora i timeout aby usun\u0105\u0107 animacj\u0119 je\u015bli utkn\u0119\u0142a\n  const loaderId = \"loader-\" + new Date().getTime();\n  \n  \/\/ Dodanie loadera do kontenera z unikalnym ID (u\u017cywamy DocumentFragment dla lepszej wydajno\u015bci)\n  const fragment = document.createDocumentFragment();\n  const loaderContainer = document.createElement(\"div\");\n  loaderContainer.className = \"loader-container\";\n  loaderContainer.id = loaderId;\n  \n  const loader = document.createElement(\"div\");\n  loader.className = \"loader\";\n  \n  loaderContainer.appendChild(loader);\n  fragment.appendChild(loaderContainer);\n  container.appendChild(fragment);\n\n  \/\/ Resetowanie timeouta je\u015bli istnieje\n  if (loaderTimeout) {\n    clearTimeout(loaderTimeout);\n  }\n\n  \/\/ je\u015bli animacja \u0142adowania nie zostanie usuni\u0119ta w ci\u0105gu 15 sekund,\n  \/\/ to pokazany b\u0119dzie komunikat o b\u0142\u0119dzie\n  loaderTimeout = setTimeout(function() {\n    const loaderElement = document.getElementById(loaderId);\n    if (loaderElement) {\n      container.innerHTML = \"\";\n      \n      const errorMessage = document.createElement(\"div\");\n      errorMessage.innerHTML = '<h3 style=\"color: red; text-align: center;\">Nie uda\u0142o si\u0119 za\u0142adowa\u0107 obrazu<\/h3>' +\n                               '<p style=\"text-align: center;\">Sprawd\u017a po\u0142\u0105czenie internetowe lub spr\u00f3buj ponownie p\u00f3\u017aniej.<\/p>';\n      container.appendChild(errorMessage);\n      \n      \/\/ Resetuj lastDisplayedImage aby nie blokowa\u0107 ponownych pr\u00f3b wczytania obrazu\n      lastDisplayedImage = {\n        model: \"\",\n        term: \"\",\n        param: \"\",\n        hour: \"\",\n        stat: \"\",\n        pressureLevel: \"\",\n        region: \"\"\n      };\n    }\n  }, 15000); \/\/ 15 sekund\n\n  updateForecastHoursTitles();\n  \n  \/\/ Ukrywanie g\u0142\u00f3wnego tytu\u0142u sekcji obrazka\n  document.getElementById(\"imageTitle\").textContent = \"\";\n  \n  \/\/ Aktualizacja opisu parametru - u\u017cyj wsp\u00f3lnego opisu EPS dla wszystkich parametr\u00f3w\n  const descriptionContainer = document.getElementById(\"parameterDescription\");\n  descriptionContainer.innerHTML = epsDescription;\n\n  refreshPreloadUi();\n\n  ensureHourImage(selectedHour).then(function(loadedImg) {\n    if (String(container.dataset.lastRequestId) !== String(requestId)) {\n      return;\n    }\n\n    if (loaderTimeout) {\n      clearTimeout(loaderTimeout);\n      loaderTimeout = null;\n    }\n\n    const loaderElements = container.querySelectorAll(\".loader-container\");\n    for (let i = 0; i < loaderElements.length; i += 1) {\n      if (loaderElements[i] && loaderElements[i].parentNode) {\n        container.removeChild(loaderElements[i]);\n      }\n    }\n\n    container.innerHTML = \"\";\n    const displayImg = loadedImg.cloneNode(true);\n    appendZoomableImage(container, displayImg, imageId);\n\n    refreshForecastDateLabels();\n\n    lastDisplayedImage = {\n      model: selectedModel,\n      term: selectedTerm,\n      param: selectedParam,\n      hour: selectedHour,\n      stat: selectedStat,\n      pressureLevel: effectivePressureLevel,\n      region: effectiveRegion,\n      imageId: imageId\n    };\n\n    preloadForecastImages();\n  }).catch(function() {\n    if (String(container.dataset.lastRequestId) !== String(requestId)) {\n      return;\n    }\n\n    if (loaderTimeout) {\n      clearTimeout(loaderTimeout);\n      loaderTimeout = null;\n    }\n\n    isShowImagesRunning = false;\n    isShowImagesRunningTimestamp = 0;\n    container.innerHTML = \"\";\n    lastDisplayedImage = {\n      model: \"\",\n      term: \"\",\n      param: \"\",\n      hour: \"\",\n      stat: \"\",\n      pressureLevel: \"\",\n      region: \"\"\n    };\n\n    try {\n      const errorImg = document.createElement(\"img\");\n      errorImg.src = \"\/wp-content\/uploads\/production\/prognozy_wiazkowe\/configuration_not.png\";\n      errorImg.alt = \"Konfiguracja niedost\u0119pna\";\n      errorImg.style.maxWidth = \"100%\";\n      errorImg.style.display = \"block\";\n      container.appendChild(errorImg);\n      const errorMsg = document.createElement(\"p\");\n      errorMsg.textContent = \"Brak dost\u0119pnych prognoz dla wybranej konfiguracji.\";\n      errorMsg.style.color = \"red\";\n      errorMsg.style.textAlign = \"center\";\n      container.appendChild(errorMsg);\n      errorImg.onerror = function() {\n        this.style.display = \"none\";\n      };\n    } catch (e) {\n      console.error(\"B\u0142\u0105d podczas obs\u0142ugi b\u0142\u0119du \u0142adowania obrazka:\", e);\n      container.innerHTML = '<div style=\"color: red; text-align: center; padding: 20px;\"><h3>Brak dost\u0119pnej prognozy<\/h3><p>Nie znaleziono prognozy dla wybranych parametr\u00f3w.<\/p><\/div>';\n    }\n  });\n  } finally {\n    \/\/ Zawsze resetuj flag\u0119 wykonywania na ko\u0144cu, nawet w przypadku b\u0142\u0119du\n    setTimeout(() => {\n      isShowImagesRunning = false;\n      isShowImagesRunningTimestamp = 0;\n      \n      \/\/ Sprawd\u017a, czy pojawi\u0142o si\u0119 nowe \u017c\u0105danie podczas wykonywania tej funkcji\n      if (pendingImageRequest) {\n        pendingImageRequest = false;\n        \n        \/\/ Natychmiast wczytaj najnowszy \u017c\u0105dany obraz\n        setTimeout(() => {\n          showImages();\n        }, 10); \/\/ Bardzo kr\u00f3tki timeout\n      }\n    }, 10); \/\/ Bardzo kr\u00f3tki timeout, aby szybko reagowa\u0107 na zmiany\n  }\n}\n\n\/\/ Funkcja do wst\u0119pnego \u0142adowania danych dla modelu\nfunction preloadModelData(model) {\n  return new Promise((resolve) => {\n    \/\/ Zapisz, \u017ce model jest preloadowany\n    preloadedModels[model] = true;\n    \n    \/\/ Ustal domy\u015blne warto\u015bci dla tego modelu\n    const term = defaultSettings[model].term;\n    const param = defaultSettings[model].param;\n    const stat = defaultSettings[model].stat;\n    \n    \/\/ Sprawd\u017a dost\u0119pne godziny, je\u015bli to model podsumowania\n    if (summaryModels.includes(model)) {\n      determineAvailableHours(model, term, param)\n        .then((hours) => {\n          if (hours && hours.length > 0) {\n            \/\/ Dla ka\u017cdej godziny preloaduj pierwszy obrazek\n            const firstHour = hours[0];\n            \/\/ Utw\u00f3rz \u015bcie\u017ck\u0119 obrazka\n            const imgPrefix = (summaryModels.includes(model)) ? \"summary\" : \"standard\";\n            const imgSuffix = (summaryModels.includes(model)) ? \"\" : `_${stat}`;\n            const fileName = `${imgPrefix}_${model}_${term}_${param}${imgSuffix}_${firstHour}.png`;\n            \n            \/\/ Wst\u0119pnie za\u0142aduj tylko pierwszy obrazek\n            const img = new Image();\n            img.src = `\/wp-content\/uploads\/production\/prognozy_wiazkowe\/${model}\/${term}\/${fileName}`;\n            \n            \/\/ Zawsze zwracaj sukces, nawet je\u015bli obrazek si\u0119 nie za\u0142aduje\n            setTimeout(resolve, 500);\n          } else {\n            resolve();\n          }\n        })\n        .catch(() => resolve()); \/\/ Zawsze resolve, nawet w przypadku b\u0142\u0119du\n    } else {\n      \/\/ Dla modeli standardowych, preloaduj pierwszy obrazek\n      const hours = forecastHours[model]?.[term] || forecastHours[\"default\"];\n      if (hours && hours.length > 0) {\n        const firstHour = String(hours[0]).padStart(2, \"0\");\n        const fileName = `standard_${model}_${term}_${param}_${stat}_${firstHour}.png`;\n        \n        \/\/ Wst\u0119pnie za\u0142aduj tylko pierwszy obrazek\n        const img = new Image();\n        img.src = `\/wp-content\/uploads\/production\/prognozy_wiazkowe\/${model}\/${term}\/${fileName}`;\n        \n        \/\/ Zawsze zwracaj sukces, nawet je\u015bli obrazek si\u0119 nie za\u0142aduje\n        setTimeout(resolve, 500);\n      } else {\n        resolve();\n      }\n    }\n  });\n}\n\n\/\/ Funkcja do aktualizacji wszystkich aktywnych przycisk\u00f3w\nfunction updateAllActiveButtons() {\n  \/\/ Usuni\u0119cie starych przycisk\u00f3w i renderowanie nowych z aktywnym zaznaczeniem\n  renderButtons(\"models\", models, (model) => {\n    const previousModel = selectedModel;\n    \n    \/\/ Zapisz poprzednie warto\u015bci\n    let prevParam = isParameterWithPressureLevel(selectedParam) ? getBaseParam(selectedParam) : selectedParam;\n    const prevStat = selectedStat;\n    const prevHour = selectedHour;\n    const prevTerm = selectedTerm;\n\n    if (previousModel !== model) {\n      hourSelectionPreserveContext = captureHourSelectionContext(\n        previousModel,\n        prevTerm,\n        prevParam,\n        prevHour\n      );\n    }\n    \n    selectedModel = model;\n    \n    \/\/ Sprawd\u017a czy zmieniamy typ modelu\n    const prevModelWasSummary = summaryModels.includes(previousModel);\n    const newModelIsSummary = summaryModels.includes(model);\n    const changingModelType = prevModelWasSummary !== newModelIsSummary;\n    \n    selectedTerm = resolveTermForModelSwitch(prevTerm, model);\n    \n    \/\/ Sprawd\u017a czy poprzedni parametr jest dost\u0119pny w nowym modelu\n    \/\/ Pobierz list\u0119 parametr\u00f3w dla nowego modelu\n    let availableParams = [];\n    if (summaryModels.includes(model)) {\n      \/\/ Dla modeli typu podsumowanie pobieramy parametry zale\u017cnie od wariantu (1h lub 12h)\n      availableParams = paramsForModels[model][selectedTerm] || [];\n    } else {\n      \/\/ Dla pozosta\u0142ych modeli parametry s\u0105 takie same niezale\u017cnie od terminu\n      availableParams = paramsForModels[model] || [];\n    }\n    \n    \/\/ Filtruj parametry (usu\u0144 te z poziomem ci\u015bnienia)\n    let filteredParams = [];\n    let baseParamsAdded = new Set();\n    availableParams.forEach(param => {\n      if (\/^[A-Z]$\/.test(param)) {\n        if (!baseParamsAdded.has(param)) {\n          filteredParams.push(param);\n          baseParamsAdded.add(param);\n        }\n      } else if (isParameterWithPressureLevel(param)) {\n        const baseParam = getBaseParam(param);\n        if (!baseParamsAdded.has(baseParam)) {\n          filteredParams.push(baseParam);\n          baseParamsAdded.add(baseParam);\n        }\n      } else {\n        filteredParams.push(param);\n      }\n    });\n    \n    \/\/ Je\u015bli poprzedni parametr jest dost\u0119pny w nowym modelu, zachowaj go\n    selectedParam = resolveParamForModelSwitch(prevParam, model, selectedTerm);\n    \n    \/\/ Godzin\u0119 dopasowujemy po dacie w renderForecastHours(), gdy znane b\u0119d\u0105 terminy nowego modelu\n    if (!changingModelType && prevHour) {\n      selectedHour = String(prevHour).padStart(2, \"0\");\n    } else {\n      selectedHour = String(defaultSettings[model].hour).padStart(2, \"0\");\n    }\n    \n    \/\/ Zachowaj statystyk\u0119 dla zwyk\u0142ych modeli\n    if (!summaryModels.includes(model)) {\n      const availableStats = suffixConfig[selectedParam] || suffixConfig[\"default\"];\n      \n      if (prevStat && availableStats.includes(prevStat)) {\n        selectedStat = prevStat;\n      } else {\n        selectedStat = defaultSettings[model].stat;\n      }\n    } else {\n      selectedStat = null; \/\/ Modele podsumowania nie maj\u0105 statystyk\n    }\n\n    \/\/ Zresetuj region przy zmianie modelu\n    const regionSection = document.getElementById(\"regionSection\");\n    const regionsContainer = document.getElementById(\"regions\");\n    selectedRegion = null;\n    if (!summaryModels.includes(model)) {\n      if (regionsContainer) regionsContainer.innerHTML = \"\";\n      if (regionSection) regionSection.style.display = \"none\";\n    } else {\n      renderRegions();\n    }\n    \n    updateTermLabel();\n    \n    \/\/ Wyczy\u015b\u0107 obrazy i poka\u017c animacj\u0119 \u0142adowania, je\u015bli jest to zmiana modelu\n    if (previousModel !== model) {\n      resetStateForModelChange();\n      \n      const imagesContainer = document.getElementById(\"images\");\n      if (imagesContainer) {\n        imagesContainer.innerHTML = \"\";\n        if (!preloadedModels[model]) {\n          const loaderContainer = document.createElement(\"div\");\n          loaderContainer.className = \"loader-container\";\n          const loader = document.createElement(\"div\");\n          loader.className = \"loader\";\n          loaderContainer.appendChild(loader);\n          const loadingText = document.createElement(\"p\");\n          loadingText.textContent = \"\u0141adowanie prognozy...\";\n          loadingText.style.marginTop = \"10px\";\n          loaderContainer.appendChild(loadingText);\n          imagesContainer.appendChild(loaderContainer);\n        }\n      }\n    }\n\n    \/\/ Je\u015bli to model podsumowania, potrzebujemy sprawdzi\u0107 dost\u0119pne godziny\n    if (summaryModels.includes(model)) {\n      \/\/ Je\u015bli model by\u0142 preloadowany, mo\u017cemy przyspieszy\u0107 proces\n      if (preloadedModels[model]) {\n        \/\/ Szybkie prze\u0142\u0105czenie bez dodatkowego sprawdzania\n        updateTermButtons();\n        renderParams();\n        renderRegions();\n        renderForecastHours();\n      } else {\n        \/\/ Standardowe sprawdzanie godzin\n        determineAvailableHours(model, selectedTerm, selectedParam)\n          .then(() => {\n            updateTermButtons();\n            renderParams();\n            renderRegions();\n            \/\/ Po sprawdzeniu dost\u0119pnych godzin, renderuj interfejs\n            renderForecastHours();\n            \n            \/\/ Dodajemy model do listy preloadowanych\n            preloadedModels[model] = true;\n          });\n      }\n    } else {\n      \/\/ Dla zwyk\u0142ych modeli (nie-podsumowania) r\u00f3wnie\u017c musimy zaktualizowa\u0107 interfejs\n      updateTermButtons();\n      renderParams();\n      renderForecastHours(); \/\/ WA\u017bNE: Renderuj godziny aby pokaza\u0107 zachowan\u0105 godzin\u0119!\n      \n      \/\/ Dodajemy model do listy preloadowanych\n      preloadedModels[model] = true;\n    }\n  }, selectedModel, modelDisplayNames);\n  \n  \/\/ Je\u015bli mamy slider z poziomami ci\u015bnienia i ustawiony poziom, aktualizuje jego warto\u015b\u0107\n  if (document.getElementById(\"pressureLevelSliderSection\").style.display !== \"none\" && selectedPressureLevel) {\n    const levelIndex = pressureLevels.indexOf(selectedPressureLevel);\n    if (levelIndex !== -1) {\n      document.getElementById(\"pressureLevelSlider\").value = levelIndex;\n      document.getElementById(\"pressureValue\").textContent = selectedPressureLevel + \" hPa\";\n    }\n  }\n}\n\n\/\/ Funkcja do parsowania parametr\u00f3w URL\nfunction parseURLParams() {\n  const urlParams = new URLSearchParams(window.location.search);\n  const params = {};\n  \n  \/\/ Pobierz wszystkie parametry z URL\n  for (const [key, value] of urlParams) {\n    params[key] = value;\n  }\n  \n  return params;\n}\n\n\/\/ Funkcja do walidacji i zastosowania parametr\u00f3w z URL\nfunction validateAndApplyURLParams() {\n  const params = parseURLParams();\n  \n  \/\/ W przypadku braku parametr\u00f3w w URL, stosowane s\u0105 domy\u015blne warto\u015bci\n  if (Object.keys(params).length === 0) {\n    return false;\n  }\n  \n  \/\/ Walidacja modelu\n  if (params.model && isValidModel(params.model)) {\n    selectedModel = params.model;\n  } else if (params.model) {\n    console.warn(`Nieprawid\u0142owy model: ${params.model}, u\u017cywam domy\u015blnego.`);\n  }\n  \n  \/\/ Walidacja terminu startu\n  if (params.term && isValidTerm(params.term, selectedModel)) {\n    selectedTerm = params.term;\n  } else if (params.term) {\n    console.warn(`Nieprawid\u0142owy termin startu: ${params.term}, u\u017cywam domy\u015blnego dla modelu ${selectedModel}.`);\n    selectedTerm = defaultSettings[selectedModel].term;\n  } else {\n    selectedTerm = defaultSettings[selectedModel].term;\n  }\n  \n  \/\/ Walidacja parametru\n  if (params.param && isValidParam(params.param, selectedModel, selectedTerm)) {\n    selectedParam = params.param;\n  } else if (params.param) {\n    console.warn(`Nieprawid\u0142owy parametr: ${params.param}, u\u017cywam domy\u015blnego dla modelu ${selectedModel}.`);\n    selectedParam = defaultSettings[selectedModel].param;\n  } else {\n    selectedParam = defaultSettings[selectedModel].param;\n  }\n  \n  \/\/ Walidacja statystyki (je\u015bli nie jest to model a-laef)\n  if (selectedModel !== \"a-laef\") {\n    if (params.stat && isValidStat(params.stat, selectedParam)) {\n      selectedStat = params.stat;\n    } else if (params.stat) {\n      console.warn(`Nieprawid\u0142owa statystyka: ${params.stat}, u\u017cywam domy\u015blnej dla parametru ${selectedParam}.`);\n      selectedStat = defaultSettings[selectedModel].stat;\n    } else {\n      selectedStat = defaultSettings[selectedModel].stat;\n    }\n  }\n  \n  \/\/ Walidacja terminu prognozy\n  if (params.hour && isValidHour(params.hour, selectedModel, selectedTerm)) {\n    selectedHour = params.hour;\n  } else if (params.hour) {\n    console.warn(`Nieprawid\u0142owy termin prognozy: ${params.hour}, u\u017cywam domy\u015blnego.`);\n    selectedHour = defaultSettings[selectedModel].hour;\n  } else {\n    selectedHour = defaultSettings[selectedModel].hour;\n  }\n  \n  \/\/ Poziom ci\u015bnienia (opcjonalny)\n  if (params.level && pressureLevels.includes(params.level)) {\n    selectedPressureLevel = params.level;\n  }\n  \n  return true;\n}\n\n\/\/ Funkcja sprawdzaj\u0105ca czy model jest prawid\u0142owy\nfunction isValidModel(model) {\n  return models.includes(model);\n}\n\n\/\/ Funkcja sprawdzaj\u0105ca czy termin jest prawid\u0142owy dla wybranego modelu\nfunction isValidTerm(term, model) {\n  if (summaryModels.includes(model)) {\n    const terms = summaryTermVariants[model] || [];\n    return terms.includes(term);\n  }\n  return termsDefault.includes(term);\n}\n\n\/\/ Funkcja sprawdzaj\u0105ca czy parametr jest prawid\u0142owy dla wybranego modelu\nfunction isValidParam(param, model, term) {\n  if (summaryModels.includes(model)) {\n    return (paramsForModels[model] && paramsForModels[model][term] && paramsForModels[model][term].includes(param));\n  }\n  return (Array.isArray(paramsForModels[model]) && paramsForModels[model].includes(param));\n}\n\n\/\/ Funkcja sprawdzaj\u0105ca czy statystyka jest prawid\u0142owa dla wybranego parametru\nfunction isValidStat(stat, param) {\n  const stats = suffixConfig[param] || suffixConfig[\"default\"];\n  return stats.includes(stat);\n}\n\n\/\/ Funkcja sprawdzaj\u0105ca czy termin prognozy jest prawid\u0142owy dla wybranego modelu\nfunction isValidHour(hour, model, term) {\n  const hours = (forecastHours[model] && forecastHours[model][term])\n    ? forecastHours[model][term]\n    : forecastHours[\"default\"];\n\n  const normalizedHour = String(hour).padStart(2, \"0\");\n  return hours.map(h => String(h).padStart(2, \"0\")).includes(normalizedHour);\n}\n\n\/\/ Zmienne do \u015bledzenia stanu gotowo\u015bci aplikacji\nlet isAppReady = false;\nlet preloadedModels = {};\nlet initialLoadDone = false;\n\n\/\/ Inicjalizacja po za\u0142adowaniu strony\ndocument.addEventListener(\"DOMContentLoaded\", function() {\n  \/\/ Dodanie wska\u017anika \u0142adowania na pocz\u0105tku\n  const mainContent = document.querySelector(\".container\");\n  const initialLoader = document.createElement(\"div\");\n  initialLoader.id = \"initialLoader\";\n  initialLoader.className = \"loader-container\";\n  initialLoader.style.position = \"fixed\";\n  initialLoader.style.top = \"0\";\n  initialLoader.style.left = \"0\";\n  initialLoader.style.width = \"100%\";\n  initialLoader.style.height = \"100%\";\n  initialLoader.style.backgroundColor = \"rgba(255, 255, 255, 0.8)\";\n  initialLoader.style.zIndex = \"9999\";\n  initialLoader.innerHTML = `\n    <div class=\"loader\"><\/div>\n    <p style=\"margin-top: 10px; font-weight: bold;\">\u0141adowanie aplikacji...<\/p>\n  `;\n  document.body.appendChild(initialLoader);\n  \n  \/\/ Dodanie mo\u017cliwo\u015bci resetowania wszystkich flag przez podw\u00f3jne klikni\u0119cie na nag\u0142\u00f3wek\n  document.querySelector(\"h1\")?.addEventListener(\"dblclick\", function() {\n    resetAllExecutionFlags();\n    alert(\"Flagi wykonywania zosta\u0142y zresetowane\");\n  });\n  \n  \/\/ Ustawienie bezpiecznika czasowego dla resetowania flag\n  \/\/ Sprawdza co 30 sekund, czy flagi s\u0105 aktywne d\u0142u\u017cej ni\u017c 60 sekund (prawdziwy deadlock)\n  setInterval(() => {\n    const now = Date.now();\n    const DEADLOCK_TIMEOUT = 60000; \/\/ 60 sekund\n    \n    let shouldReset = false;\n    \n    \/\/ Sprawd\u017a ka\u017cd\u0105 flag\u0119 osobno z jej timestampem\n    if (isShowImagesRunning && isShowImagesRunningTimestamp > 0 && (now - isShowImagesRunningTimestamp) > DEADLOCK_TIMEOUT) {\n      shouldReset = true;\n    }\n    \n    if (isRenderingForecastHours && isRenderingForecastHoursTimestamp > 0 && (now - isRenderingForecastHoursTimestamp) > DEADLOCK_TIMEOUT) {\n      shouldReset = true;\n    }\n    \n    if (shouldReset) {\n      resetAllExecutionFlags();\n    }\n  }, 30000);\n  \n  \/\/ Dodanie mechanizmu automatycznego naprawiania interfejsu\n  setInterval(function() {\n    const imagesContainer = document.getElementById(\"images\");\n    \n    \/\/ Sprawd\u017a czy kontener obraz\u00f3w jest pusty, a powinien zawiera\u0107 obrazy\n    if (imagesContainer && \n        isAppReady && \n        selectedModel && \n        selectedTerm && \n        selectedParam && \n        selectedHour && \n        (summaryModels.includes(selectedModel) || selectedStat) && \n        imagesContainer.innerHTML.trim() === \"\") {\n      \n      \/\/ Dodaj animacj\u0119 \u0142adowania\n      const loaderContainer = document.createElement(\"div\");\n      loaderContainer.className = \"loader-container\";\n      const loader = document.createElement(\"div\");\n      loader.className = \"loader\";\n      loaderContainer.appendChild(loader);\n      const loadingText = document.createElement(\"p\");\n      loadingText.textContent = \"Od\u015bwie\u017canie prognozy...\";\n      loadingText.style.marginTop = \"10px\";\n      loaderContainer.appendChild(loadingText);\n      imagesContainer.appendChild(loaderContainer);\n      \n      \/\/ Po kr\u00f3tkim op\u00f3\u017anieniu spr\u00f3buj za\u0142adowa\u0107 obrazy ponownie\n      setTimeout(() => {\n        showImages();\n      }, 100);\n    }\n  }, 2000); \/\/ Sprawdzaj co 2 sekundy\n\n  \/\/ Pr\u00f3ba za\u0142adowania parametr\u00f3w z URL\n  if (validateAndApplyURLParams()) {\n    updateTermButtons();\n    \n    \/\/ Renderuj list\u0119 godzin dla modeli podsumowania\n    if (summaryModels.includes(selectedModel)) {\n      determineAvailableHours(selectedModel, selectedTerm, selectedParam)\n        .then(() => {\n          renderForecastHours();\n        });\n    }\n  } else {\n    initializeInterface();\n  }\n  \n  \/\/ Rozpocznij preload domy\u015blnego modelu i kilku alternatywnych\n  const preloadTasks = [preloadModelData(selectedModel)];\n  const secondaryModel = models.find(model => model !== selectedModel);\n  if (secondaryModel) {\n    preloadTasks.push(preloadModelData(secondaryModel));\n  }\n\n  Promise.all(preloadTasks).then(() => {\n    initialLoadDone = true;\n    \n    \/\/ Usu\u0144 wska\u017anik \u0142adowania\n    const loader = document.getElementById(\"initialLoader\");\n    if (loader) {\n      loader.style.opacity = \"0\";\n      loader.style.transition = \"opacity 0.5s\";\n      setTimeout(() => {\n        loader.remove();\n      }, 500);\n    }\n    \n    \/\/ Oznacz aplikacj\u0119 jako gotow\u0105\n    isAppReady = true;\n  });\n  \n  \/\/ Dodatkowa weryfikacja poprawnego pod\u015bwietlenia przycisk\u00f3w i za\u0142adowania obrazka\n  setTimeout(function() {\n    updateAllActiveButtons();\n    updateCompareButtons();\n    \/\/ Zapewnienie poprawnego za\u0142adowania obrazka\n    showImages();\n    \/\/ Zaktualizuj wizualizacj\u0119 za\u0142adowanych godzin (slider i przyciski)\n    updateLoadedHoursVisualization();\n  }, 1000);\n  \n  \/\/ Obs\u0142uga przycisku animacji\n  let animationInterval = null;\n  let isAnimating = false;\n\n  document.getElementById(\"compareBtn\")?.addEventListener(\"click\", toggleCompareMode);\n  document.getElementById(\"compareListBtn\")?.addEventListener(\"click\", toggleCompareListLayout);\n  document.getElementById(\"compareFullscreenBtn\")?.addEventListener(\"click\", toggleCompareFullscreen);\n  document.getElementById(\"wiazkoweLightbox\")?.addEventListener(\"click\", function(e) {\n    if (e.target.id === \"wiazkoweLightbox\" || e.target.id === \"wiazkoweLightboxClose\") {\n      closeLightbox();\n    }\n  });\n  document.getElementById(\"wiazkoweLightboxClose\")?.addEventListener(\"click\", closeLightbox);\n  document.getElementById(\"wiazkoweCompareClose\")?.addEventListener(\"click\", closeCompareModal);\n  document.getElementById(\"wiazkoweCompareConfirm\")?.addEventListener(\"click\", function() {\n    if (compareSelectedModels.length < 2) {\n      return;\n    }\n    if (!compareMode) {\n      selectedModelBeforeCompare = selectedModel;\n      compareMode = true;\n    }\n    if (!applyCompareModalSelection()) {\n      return;\n    }\n\n    \/\/ Po wyborze zestawienia\/parametru wyczy\u015b\u0107 cache godzin por\u00f3wnania.\n    clearCompareModelsHoursCache();\n\n    resetComparePreloadQueue();\n    closeCompareModal();\n    updateTermButtons();\n    renderParams();\n    renderRegions();\n    refreshModelsButtons();\n    updateCompareButtons();\n    refreshViewAfterConfigChange();\n  });\n  document.getElementById(\"wiazkoweCompareModal\")?.addEventListener(\"click\", function(e) {\n    if (e.target.id === \"wiazkoweCompareModal\") {\n      closeCompareModal();\n    }\n  });\n\n  let compareResizeTimer = null;\n  window.addEventListener(\"resize\", function() {\n    updateCompareButtons();\n    if (!compareFullscreen) {\n      return;\n    }\n    clearTimeout(compareResizeTimer);\n    compareResizeTimer = setTimeout(function() {\n      if (compareFullscreen) {\n        renderCompareFullscreen();\n      }\n    }, 120);\n  });\n  \n  document.getElementById(\"animateBtn\")?.addEventListener(\"click\", function() {\n    const animateBtn = this;\n    \n    if (isAnimating) {\n      \/\/ Zatrzymaj animacj\u0119\n      clearInterval(animationInterval);\n      animationInterval = null;\n      isAnimating = false;\n      animateBtn.classList.remove(\"active\");\n      animateBtn.textContent = \"\u25b6 Animuj\";\n      animateBtn.title = \"Animuj godziny prognozy\";\n    } else {\n      \/\/ Rozpocznij animacj\u0119\n      isAnimating = true;\n      animateBtn.classList.add(\"active\");\n      animateBtn.textContent = \"\u23f8 Zatrzymaj\";\n      animateBtn.title = \"Zatrzymaj animacj\u0119\";\n      \n      \/\/ Pobierz dost\u0119pne godziny\n      const hours = getCurrentForecastHoursList();\n      \n      if (hours.length === 0) {\n        isAnimating = false;\n        animateBtn.classList.remove(\"active\");\n        animateBtn.textContent = \"\u25b6 Animuj\";\n        return;\n      }\n      \n      animationInterval = setInterval(() => {\n        \/\/ Znajd\u017a aktualny indeks\n        const currentIndex = hours.findIndex(h => String(h).padStart(2, \"0\") === String(selectedHour).padStart(2, \"0\"));\n        \n        \/\/ Przejd\u017a do nast\u0119pnej godziny (zap\u0119tlenie)\n        const nextIndex = (currentIndex + 1) % hours.length;\n        const newHour = String(hours[nextIndex]).padStart(2, \"0\");\n        selectedHour = newHour;\n        \n        \/\/ Zaktualizuj interfejs\n        setActiveButton(\"hours\", newHour);\n        \n        \/\/ Zaktualizuj slider\n        const slider = document.getElementById(\"forecastHourSlider\");\n        if (slider) {\n          slider.value = nextIndex;\n        }\n        \n        \/\/ Za\u0142aduj obrazek\n        showImages();\n      }, 1000); \/\/ Zmiana co 1 sekund\u0119\n    }\n  });\n  \n  \/\/ Obs\u0142uga klawiszy strza\u0142ek do przesuwania slidera prognoz\n  \/\/ Zmienne do kontroli op\u00f3\u017anienia skr\u00f3t\u00f3w klawiszowych\n  let lastKeyPressTime = 0;\n  const keyPressDelay = 130; \/\/ Op\u00f3\u017anienie w milisekundach (130ms)\n\n  document.addEventListener(\"keydown\", function(event) {\n    \/\/ Ignoruj tylko je\u015bli u\u017cytkownik pisze w polu tekstowym lub textarea\n    if (event.target.tagName === 'INPUT' || event.target.tagName === 'TEXTAREA') {\n      return;\n    }\n\n    const compareModal = document.getElementById(\"wiazkoweCompareModal\");\n    const isCompareModalOpen = compareModal && compareModal.style.display === \"flex\";\n\n    if (event.key === \"Escape\") {\n      if (isLightboxOpen()) {\n        closeLightbox();\n        event.preventDefault();\n        return;\n      }\n      if (compareFullscreen) {\n        closeCompareFullscreen();\n        event.preventDefault();\n        return;\n      }\n      if (isCompareModalOpen) {\n        closeCompareModal();\n        return;\n      }\n    }\n\n    if (event.key === \"f\" || event.key === \"F\") {\n      if (!canUseCompareFullscreen()) {\n        return;\n      }\n      event.preventDefault();\n      toggleCompareFullscreen();\n      return;\n    }\n\n    if (event.key === \"c\" || event.key === \"C\") {\n      event.preventDefault();\n      toggleCompareMode();\n      return;\n    }\n    \n    \/\/ Najpierw zablokuj domy\u015blne zachowanie dla klawiszy nawigacyjnych\n    const paramsSelect = document.getElementById('paramsSelect');\n    const statsSelect = document.getElementById('statsSelect');\n    const isParamsSelectActive = document.activeElement === paramsSelect;\n    const isStatsSelectActive = document.activeElement === statsSelect;\n    const isAnySelectActive = isParamsSelectActive || isStatsSelectActive;\n    \n    \/\/ Blokuj przewijanie strony dla strza\u0142ek (chyba \u017ce edytujemy list\u0119 parametr\u00f3w lub statystyk)\n    if ((event.key === \"ArrowLeft\" || event.key === \"ArrowRight\" || \n         event.key === \"ArrowUp\" || event.key === \"ArrowDown\") && !isAnySelectActive) {\n      event.preventDefault();\n    }\n    \n    \/\/ Sprawd\u017a op\u00f3\u017anienie dla wszystkich skr\u00f3t\u00f3w klawiszowych\n    const currentTime = Date.now();\n    if (currentTime - lastKeyPressTime < keyPressDelay) {\n      return; \/\/ Ignoruj je\u015bli min\u0119\u0142o za ma\u0142o czasu\n    }\n    lastKeyPressTime = currentTime;\n    \n    \/\/ Strza\u0142ki lewo\/prawo - zmiana godzin prognozy\n    if (event.key === \"ArrowLeft\" || event.key === \"ArrowRight\") {\n      const hours = getCurrentForecastHoursList();\n      if (hours.length === 0) return;\n      \n      \/\/ Znajd\u017a aktualny indeks\n      const currentIndex = hours.findIndex(h => String(h).padStart(2, \"0\") === String(selectedHour).padStart(2, \"0\"));\n      \n      let newIndex;\n      if (event.key === \"ArrowLeft\") {\n        \/\/ Strza\u0142ka w lewo - poprzednia godzina (zatrzymaj si\u0119 na pocz\u0105tku)\n        newIndex = currentIndex > 0 ? currentIndex - 1 : currentIndex;\n      } else {\n        \/\/ Strza\u0142ka w prawo - nast\u0119pna godzina (zatrzymaj si\u0119 na ko\u0144cu)\n        newIndex = currentIndex < hours.length - 1 ? currentIndex + 1 : currentIndex;\n      }\n      \n      \/\/ Je\u015bli indeks si\u0119 nie zmieni\u0142, nie r\u00f3b nic\n      if (newIndex === currentIndex) {\n        return;\n      }\n      \n      \/\/ Ustaw now\u0105 godzin\u0119\n      const newHour = String(hours[newIndex]).padStart(2, \"0\");\n      selectedHour = newHour;\n      \n      \/\/ Zaktualizuj interfejs\n      setActiveButton(\"hours\", newHour);\n      \n      \/\/ Zaktualizuj slider\n      const slider = document.getElementById(\"forecastHourSlider\");\n      if (slider) {\n        slider.value = newIndex;\n      }\n      \n      \/\/ Za\u0142aduj obrazek\n      showImages();\n      preloadForecastImages();\n    }\n    \n    \/\/ Strza\u0142ki g\u00f3ra\/d\u00f3\u0142 - zmiana poziom\u00f3w ci\u015bnienia\n    if (event.key === \"ArrowUp\" || event.key === \"ArrowDown\") {\n      \/\/ Je\u015bli lista parametr\u00f3w lub statystyk ma focus, pozw\u00f3l domy\u015blnej akcji (nawigacja po li\u015bcie)\n      if (isAnySelectActive) {\n        return; \/\/ Nie blokuj domy\u015blnego zachowania\n      }\n      \n      \/\/ W przeciwnym razie - steruj suwakiem ci\u015bnienia\n      \/\/ Sprawd\u017a czy suwak poziom\u00f3w jest widoczny\n      const sliderSection = document.getElementById(\"pressureLevelSliderSection\");\n      if (!sliderSection || sliderSection.style.display === \"none\") {\n        return;\n      }\n      \n      \/\/ W testy.html u\u017cywamy sta\u0142ej tablicy pressureLevels\n      if (!selectedPressureLevel) return;\n      \n      \/\/ Znajd\u017a aktualny indeks\n      const currentIndex = pressureLevels.indexOf(selectedPressureLevel);\n      if (currentIndex === -1) return;\n      \n      let newIndex;\n      if (event.key === \"ArrowUp\") {\n        \/\/ Strza\u0142ka w g\u00f3r\u0119 - ni\u017csze ci\u015bnienie (wy\u017cszy indeks w tablicy posortowanej malej\u0105co)\n        \/\/ Zatrzymaj si\u0119 na ko\u0144cu\n        newIndex = currentIndex < pressureLevels.length - 1 ? currentIndex + 1 : currentIndex;\n      } else {\n        \/\/ Strza\u0142ka w d\u00f3\u0142 - wy\u017csze ci\u015bnienie (ni\u017cszy indeks)\n        \/\/ Zatrzymaj si\u0119 na pocz\u0105tku\n        newIndex = currentIndex > 0 ? currentIndex - 1 : currentIndex;\n      }\n      \n      \/\/ Je\u015bli indeks si\u0119 nie zmieni\u0142, nie r\u00f3b nic\n      if (newIndex === currentIndex) {\n        return;\n      }\n      \n      \/\/ Ustaw nowy poziom\n      selectedPressureLevel = pressureLevels[newIndex];\n      \n      \/\/ Zaktualizuj slider\n      const slider = document.getElementById(\"pressureLevelSlider\");\n      if (slider) {\n        slider.value = newIndex;\n        document.getElementById(\"pressureValue\").textContent = selectedPressureLevel + \" hPa\";\n      }\n      \n      \/\/ Za\u0142aduj obrazek\n      showImages();\n      preloadForecastImages();\n    }\n    \n    \/\/ Cyfry 1-9 - szybki wyb\u00f3r modelu\n    if (event.key >= '1' && event.key <= '9') {\n      event.preventDefault();\n      \n      const modelIndex = parseInt(event.key) - 1;\n      if (modelIndex < models.length) {\n        const newModel = models[modelIndex];\n        \n        \/\/ Symuluj klikni\u0119cie w przycisk modelu\n        const modelButtons = document.querySelectorAll('#models button');\n        modelButtons.forEach(btn => {\n          if (btn.textContent.trim() === modelDisplayNames[newModel] || btn.textContent.trim() === newModel) {\n            btn.click();\n          }\n        });\n      }\n    }\n    \n    \/\/ Tab - rozwini\u0119cie\/zwini\u0119cie listy parametr\u00f3w\n    if (event.key === 'Tab') {\n      event.preventDefault();\n      \n      const paramsSelect = document.getElementById('paramsSelect');\n      \n      \/\/ Je\u015bli lista parametr\u00f3w jest ju\u017c otwarta, zamknij j\u0105\n      if (paramsSelect && paramsSelect.size > 1) {\n        paramsSelect.size = 0;\n        \/\/ Przywr\u00f3\u0107 aktualnie wybrany parametr\n        paramsSelect.value = selectedParam || '';\n        paramsSelect.blur();\n        return;\n      }\n      \n      \/\/ W przeciwnym razie otw\u00f3rz list\u0119 parametr\u00f3w i zamknij statystyki\n      \/\/ Zamknij list\u0119 statystyk je\u015bli jest otwarta\n      const statsSelect = document.getElementById('statsSelect');\n      if (statsSelect && statsSelect.size > 1) {\n        statsSelect.size = 0;\n        \/\/ Przywr\u00f3\u0107 aktualnie wybran\u0105 statystyk\u0119\n        statsSelect.value = selectedStat || '';\n        statsSelect.blur();\n      }\n      \n      if (paramsSelect) {\n        paramsSelect.focus();\n        \/\/ Symuluj klikni\u0119cie aby rozwin\u0105\u0107 list\u0119\n        paramsSelect.click();\n        \/\/ Alternatywnie: programowo rozwi\u0144 select\n        paramsSelect.size = Math.min(paramsSelect.options.length, 10);\n      }\n    }\n    \n    \/\/ A - w\u0142\u0105cz\/wy\u0142\u0105cz animacj\u0119 godzin prognozy\n    if (event.key === 'a' || event.key === 'A') {\n      event.preventDefault();\n      const animateBtn = document.getElementById(\"animateBtn\");\n      if (animateBtn) {\n        animateBtn.click();\n      }\n    }\n    \n    \/\/ Q - rozwini\u0119cie\/zwini\u0119cie listy statystyk (tylko dla zwyk\u0142ych modeli, nie dla modeli podsumowania)\n    if (event.key === 'q' || event.key === 'Q') {\n      event.preventDefault();\n      \/\/ Sprawd\u017a czy to model podsumowania\n      if (summaryModels.includes(selectedModel)) {\n        \/\/ Dla modeli podsumowania nie ma statystyk, wi\u0119c nic nie r\u00f3b\n        return;\n      }\n      \n      const statsSelect = document.getElementById('statsSelect');\n      \n      \/\/ Je\u015bli lista statystyk jest ju\u017c otwarta, zamknij j\u0105\n      if (statsSelect && statsSelect.size > 1) {\n        statsSelect.size = 0;\n        \/\/ Przywr\u00f3\u0107 aktualnie wybran\u0105 statystyk\u0119\n        statsSelect.value = selectedStat || '';\n        statsSelect.blur();\n        return;\n      }\n      \n      \/\/ W przeciwnym razie otw\u00f3rz list\u0119 statystyk i zamknij parametry\n      \/\/ Zamknij list\u0119 parametr\u00f3w je\u015bli jest otwarta\n      const paramsSelect = document.getElementById('paramsSelect');\n      if (paramsSelect && paramsSelect.size > 1) {\n        paramsSelect.size = 0;\n        \/\/ Przywr\u00f3\u0107 aktualnie wybrany parametr\n        paramsSelect.value = selectedParam || '';\n        paramsSelect.blur();\n      }\n      \n      if (statsSelect && statsSelect.options.length > 1) { \/\/ Sprawd\u017a czy ma opcje opr\u00f3cz domy\u015blnej\n        statsSelect.focus();\n        \/\/ Symuluj klikni\u0119cie aby rozwin\u0105\u0107 list\u0119\n        statsSelect.click();\n        \/\/ Alternatywnie: programowo rozwi\u0144 select\n        statsSelect.size = Math.min(statsSelect.options.length, 10);\n      }\n    }\n    \n    \/\/ , i . - zmiana terminu inicjacji (, = lewo\/wcze\u015bniej, . = prawo\/p\u00f3\u017aniej)\n    if (event.key === ',' || event.key === '<' || event.key === '.' || event.key === '>') {\n      event.preventDefault();\n      \n      \/\/ Pobierz dost\u0119pne terminy dla aktualnego modelu\n      let availableTerms;\n      if (selectedModel === \"a-laef\") {\n        availableTerms = alaefVariants; \/\/ [\"1h\", \"12h\"]\n      } else if (summaryModels.includes(selectedModel)) {\n        availableTerms = summaryTermVariants[selectedModel] || [\"12h\"];\n      } else {\n        \/\/ Zwyk\u0142e modele\n        availableTerms = termsDefault; \/\/ [\"00\", \"06\", \"12\", \"18\"]\n      }\n      \n      if (availableTerms.length === 0) return;\n      \n      \/\/ Znajd\u017a aktualny indeks\n      const currentIndex = availableTerms.indexOf(selectedTerm);\n      if (currentIndex === -1) return;\n      \n      let newIndex;\n      if (event.key === ',' || event.key === '<') {\n        \/\/ , lub < - poprzedni termin (zatrzymaj si\u0119 na pocz\u0105tku)\n        newIndex = currentIndex > 0 ? currentIndex - 1 : currentIndex;\n      } else {\n        \/\/ . lub > - nast\u0119pny termin (zatrzymaj si\u0119 na ko\u0144cu)\n        newIndex = currentIndex < availableTerms.length - 1 ? currentIndex + 1 : currentIndex;\n      }\n      \n      \/\/ Je\u015bli indeks si\u0119 nie zmieni\u0142, nie r\u00f3b nic\n      if (newIndex === currentIndex) {\n        return;\n      }\n      \n      \/\/ Ustaw nowy termin\n      const newTerm = availableTerms[newIndex];\n      \n      \/\/ Symuluj klikni\u0119cie w przycisk terminu\n      const termButtons = document.querySelectorAll('#terms button');\n      termButtons.forEach(btn => {\n        if (btn.dataset.value === newTerm || btn.textContent.includes(newTerm)) {\n          btn.click();\n        }\n      });\n    }\n  });\n});\nconsole.log(\"Autor Bart\u0142omiej Sobczyk 2025\");\n<\/script>\n\t\t<\/div>\n\t\t\t\t<\/div>\n\t\t\t\t\t<\/div>\n\t\t<\/div>\n\t\t\t\t\t\t\t<\/div>\n\t\t<\/section>\n\t\t\t\t\t\t\t<\/div>\n\t\t","protected":false},"excerpt":{"rendered":"<p>Model Termin startu Parametr Statystyka Obszar Godziny prognozy \u25b6 Animuj \u229e Por\u00f3wnaj modele \u2630 Lista \u26f6 Pe\u0142ny ekran *Je\u015bli przy formacie czasu nie widnieje adnotacja UTC to znaczy, \u017ce obraz generowany jest na wskazany termin wg czasu urz\u0119dowego. \ud83d\udca1 Skr\u00f3ty klawiszowe: 1-6 Wyb\u00f3r modelu &lt; &gt; Zmiana terminu startu \u2190 \u2192 Zmiana godzin prognozy Tab [&hellip;]<\/p>\n","protected":false},"author":11,"featured_media":0,"parent":0,"menu_order":0,"comment_status":"closed","ping_status":"closed","template":"","meta":{"ocean_post_layout":"","ocean_both_sidebars_style":"","ocean_both_sidebars_content_width":0,"ocean_both_sidebars_sidebars_width":0,"ocean_sidebar":"","ocean_second_sidebar":"","ocean_disable_margins":"enable","ocean_add_body_class":"","ocean_shortcode_before_top_bar":"","ocean_shortcode_after_top_bar":"","ocean_shortcode_before_header":"","ocean_shortcode_after_header":"","ocean_has_shortcode":"","ocean_shortcode_after_title":"","ocean_shortcode_before_footer_widgets":"","ocean_shortcode_after_footer_widgets":"","ocean_shortcode_before_footer_bottom":"","ocean_shortcode_after_footer_bottom":"","ocean_display_top_bar":"default","ocean_display_header":"default","ocean_header_style":"","ocean_center_header_left_menu":"","ocean_custom_header_template":"","ocean_custom_logo":0,"ocean_custom_retina_logo":0,"ocean_custom_logo_max_width":0,"ocean_custom_logo_tablet_max_width":0,"ocean_custom_logo_mobile_max_width":0,"ocean_custom_logo_max_height":0,"ocean_custom_logo_tablet_max_height":0,"ocean_custom_logo_mobile_max_height":0,"ocean_header_custom_menu":"","ocean_menu_typo_font_family":"","ocean_menu_typo_font_subset":"","ocean_menu_typo_font_size":0,"ocean_menu_typo_font_size_tablet":0,"ocean_menu_typo_font_size_mobile":0,"ocean_menu_typo_font_size_unit":"px","ocean_menu_typo_font_weight":"","ocean_menu_typo_font_weight_tablet":"","ocean_menu_typo_font_weight_mobile":"","ocean_menu_typo_transform":"","ocean_menu_typo_transform_tablet":"","ocean_menu_typo_transform_mobile":"","ocean_menu_typo_line_height":0,"ocean_menu_typo_line_height_tablet":0,"ocean_menu_typo_line_height_mobile":0,"ocean_menu_typo_line_height_unit":"","ocean_menu_typo_spacing":0,"ocean_menu_typo_spacing_tablet":0,"ocean_menu_typo_spacing_mobile":0,"ocean_menu_typo_spacing_unit":"","ocean_menu_link_color":"","ocean_menu_link_color_hover":"","ocean_menu_link_color_active":"","ocean_menu_link_background":"","ocean_menu_link_hover_background":"","ocean_menu_link_active_background":"","ocean_menu_social_links_bg":"","ocean_menu_social_hover_links_bg":"","ocean_menu_social_links_color":"","ocean_menu_social_hover_links_color":"","ocean_disable_title":"default","ocean_disable_heading":"default","ocean_post_title":"","ocean_post_subheading":"","ocean_post_title_style":"","ocean_post_title_background_color":"","ocean_post_title_background":0,"ocean_post_title_bg_image_position":"","ocean_post_title_bg_image_attachment":"","ocean_post_title_bg_image_repeat":"","ocean_post_title_bg_image_size":"","ocean_post_title_height":0,"ocean_post_title_bg_overlay":0.5,"ocean_post_title_bg_overlay_color":"","ocean_disable_breadcrumbs":"default","ocean_breadcrumbs_color":"","ocean_breadcrumbs_separator_color":"","ocean_breadcrumbs_links_color":"","ocean_breadcrumbs_links_hover_color":"","ocean_display_footer_widgets":"default","ocean_display_footer_bottom":"default","ocean_custom_footer_template":""},"yoast_head":"<!-- This site is optimized with the Yoast SEO plugin v19.5.1 - https:\/\/yoast.com\/wordpress\/plugins\/seo\/ -->\n<title>Prognozy wi\u0105zkowe IMGW<\/title>\n<meta name=\"description\" content=\"Wyniki modeli numerycznych prognoz wi\u0105zkowych IMGW-PIB\" \/>\n<meta name=\"robots\" content=\"index, follow, max-snippet:-1, max-image-preview:large, max-video-preview:-1\" \/>\n<link rel=\"canonical\" href=\"https:\/\/cmm.imgw.pl\/?page_id=46288\" \/>\n<meta property=\"og:locale\" content=\"pl_PL\" \/>\n<meta property=\"og:type\" content=\"article\" \/>\n<meta property=\"og:title\" content=\"Prognozy wi\u0105zkowe\" \/>\n<meta property=\"og:description\" content=\"Prognozy wi\u0105zkowe\" \/>\n<meta property=\"og:url\" content=\"https:\/\/cmm.imgw.pl\/?page_id=46288\" \/>\n<meta property=\"og:site_name\" content=\"Laboratorium Modelowania Meteorologicznego CMOK IMGW-PIB\" \/>\n<meta property=\"article:publisher\" content=\"https:\/\/www.facebook.com\/Meteoimgw\/\" \/>\n<meta property=\"article:modified_time\" content=\"2026-08-17T09:02:37+00:00\" \/>\n<meta property=\"og:image\" content=\"https:\/\/cmm.imgw.pl\/wp-content\/uploads\/2025\/10\/MODELE_LOGO_UNIFIKACJA_v2.png\" \/>\n\t<meta property=\"og:image:width\" content=\"1356\" \/>\n\t<meta property=\"og:image:height\" content=\"365\" \/>\n\t<meta property=\"og:image:type\" content=\"image\/png\" \/>\n<meta name=\"twitter:card\" content=\"summary_large_image\" \/>\n<meta name=\"twitter:title\" content=\"Prognozy wi\u0105zkowe\" \/>\n<meta name=\"twitter:description\" content=\"Prognozy wi\u0105zkowe\" \/>\n<meta name=\"twitter:site\" content=\"@IMGW_CMM\" \/>\n<meta name=\"twitter:label1\" content=\"Szacowany czas czytania\" \/>\n\t<meta name=\"twitter:data1\" content=\"1 minuta\" \/>\n<script type=\"application\/ld+json\" class=\"yoast-schema-graph\">{\"@context\":\"https:\/\/schema.org\",\"@graph\":[{\"@type\":\"WebPage\",\"@id\":\"https:\/\/cmm.imgw.pl\/?page_id=46288\",\"url\":\"https:\/\/cmm.imgw.pl\/?page_id=46288\",\"name\":\"Prognozy wi\u0105zkowe IMGW\",\"isPartOf\":{\"@id\":\"https:\/\/cmm.imgw.pl\/#website\"},\"datePublished\":\"2025-09-04T08:36:09+00:00\",\"dateModified\":\"2026-08-17T09:02:37+00:00\",\"description\":\"Wyniki modeli numerycznych prognoz wi\u0105zkowych IMGW-PIB\",\"breadcrumb\":{\"@id\":\"https:\/\/cmm.imgw.pl\/?page_id=46288#breadcrumb\"},\"inLanguage\":\"pl-PL\",\"potentialAction\":[{\"@type\":\"ReadAction\",\"target\":[\"https:\/\/cmm.imgw.pl\/?page_id=46288\"]}]},{\"@type\":\"BreadcrumbList\",\"@id\":\"https:\/\/cmm.imgw.pl\/?page_id=46288#breadcrumb\",\"itemListElement\":[{\"@type\":\"ListItem\",\"position\":1,\"name\":\"Home\",\"item\":\"https:\/\/cmm.imgw.pl\/\"},{\"@type\":\"ListItem\",\"position\":2,\"name\":\"IMGW-PIB LMM: Prognozy wi\u0105zkowe\"}]},{\"@type\":\"WebSite\",\"@id\":\"https:\/\/cmm.imgw.pl\/#website\",\"url\":\"https:\/\/cmm.imgw.pl\/\",\"name\":\"Laboratorium Modelowania Meteorologicznego CMOK IMGW-PIB\",\"description\":\"CMOK-LMM Laboratorium pe\u0142ni pa\u0144stwow\u0105 s\u0142u\u017cb\u0119 hydrologiczno-meteorologiczn\u0105 w zakresie numerycznych prognoz pogody, kt\u00f3rego zadaniem jest konsolidacja kompetencji w obszarze modelowania zjawisk pogodowych oraz dalszego rozwoju numerycznych modeli pogody (NMP).\",\"publisher\":{\"@id\":\"https:\/\/cmm.imgw.pl\/#organization\"},\"potentialAction\":[{\"@type\":\"SearchAction\",\"target\":{\"@type\":\"EntryPoint\",\"urlTemplate\":\"https:\/\/cmm.imgw.pl\/?s={search_term_string}\"},\"query-input\":\"required name=search_term_string\"}],\"inLanguage\":\"pl-PL\"},{\"@type\":\"Organization\",\"@id\":\"https:\/\/cmm.imgw.pl\/#organization\",\"name\":\"Laboratorium Modelowania Meteorologicznego CMOK IMGW-PIB\",\"url\":\"https:\/\/cmm.imgw.pl\/\",\"sameAs\":[\"https:\/\/www.facebook.com\/Meteoimgw\/\",\"https:\/\/twitter.com\/IMGW_CMM\"],\"logo\":{\"@type\":\"ImageObject\",\"inLanguage\":\"pl-PL\",\"@id\":\"https:\/\/cmm.imgw.pl\/#\/schema\/logo\/image\/\",\"url\":\"https:\/\/cmm.imgw.pl\/wp-content\/uploads\/2025\/10\/MODELE_LOGO_UNIFIKACJA_v2.png\",\"contentUrl\":\"https:\/\/cmm.imgw.pl\/wp-content\/uploads\/2025\/10\/MODELE_LOGO_UNIFIKACJA_v2.png\",\"width\":1356,\"height\":365,\"caption\":\"Laboratorium Modelowania Meteorologicznego CMOK IMGW-PIB\"},\"image\":{\"@id\":\"https:\/\/cmm.imgw.pl\/#\/schema\/logo\/image\/\"}}]}<\/script>\n<!-- \/ Yoast SEO plugin. -->","yoast_head_json":{"title":"Prognozy wi\u0105zkowe IMGW","description":"Wyniki modeli numerycznych prognoz wi\u0105zkowych IMGW-PIB","robots":{"index":"index","follow":"follow","max-snippet":"max-snippet:-1","max-image-preview":"max-image-preview:large","max-video-preview":"max-video-preview:-1"},"canonical":"https:\/\/cmm.imgw.pl\/?page_id=46288","og_locale":"pl_PL","og_type":"article","og_title":"Prognozy wi\u0105zkowe","og_description":"Prognozy wi\u0105zkowe","og_url":"https:\/\/cmm.imgw.pl\/?page_id=46288","og_site_name":"Laboratorium Modelowania Meteorologicznego CMOK IMGW-PIB","article_publisher":"https:\/\/www.facebook.com\/Meteoimgw\/","article_modified_time":"2026-08-17T09:02:37+00:00","og_image":[{"width":1356,"height":365,"url":"https:\/\/cmm.imgw.pl\/wp-content\/uploads\/2025\/10\/MODELE_LOGO_UNIFIKACJA_v2.png","type":"image\/png"}],"twitter_card":"summary_large_image","twitter_title":"Prognozy wi\u0105zkowe","twitter_description":"Prognozy wi\u0105zkowe","twitter_site":"@IMGW_CMM","twitter_misc":{"Szacowany czas czytania":"1 minuta"},"schema":{"@context":"https:\/\/schema.org","@graph":[{"@type":"WebPage","@id":"https:\/\/cmm.imgw.pl\/?page_id=46288","url":"https:\/\/cmm.imgw.pl\/?page_id=46288","name":"Prognozy wi\u0105zkowe IMGW","isPartOf":{"@id":"https:\/\/cmm.imgw.pl\/#website"},"datePublished":"2025-09-04T08:36:09+00:00","dateModified":"2026-08-17T09:02:37+00:00","description":"Wyniki modeli numerycznych prognoz wi\u0105zkowych IMGW-PIB","breadcrumb":{"@id":"https:\/\/cmm.imgw.pl\/?page_id=46288#breadcrumb"},"inLanguage":"pl-PL","potentialAction":[{"@type":"ReadAction","target":["https:\/\/cmm.imgw.pl\/?page_id=46288"]}]},{"@type":"BreadcrumbList","@id":"https:\/\/cmm.imgw.pl\/?page_id=46288#breadcrumb","itemListElement":[{"@type":"ListItem","position":1,"name":"Home","item":"https:\/\/cmm.imgw.pl\/"},{"@type":"ListItem","position":2,"name":"IMGW-PIB LMM: Prognozy wi\u0105zkowe"}]},{"@type":"WebSite","@id":"https:\/\/cmm.imgw.pl\/#website","url":"https:\/\/cmm.imgw.pl\/","name":"Laboratorium Modelowania Meteorologicznego CMOK IMGW-PIB","description":"CMOK-LMM Laboratorium pe\u0142ni pa\u0144stwow\u0105 s\u0142u\u017cb\u0119 hydrologiczno-meteorologiczn\u0105 w zakresie numerycznych prognoz pogody, kt\u00f3rego zadaniem jest konsolidacja kompetencji w obszarze modelowania zjawisk pogodowych oraz dalszego rozwoju numerycznych modeli pogody (NMP).","publisher":{"@id":"https:\/\/cmm.imgw.pl\/#organization"},"potentialAction":[{"@type":"SearchAction","target":{"@type":"EntryPoint","urlTemplate":"https:\/\/cmm.imgw.pl\/?s={search_term_string}"},"query-input":"required name=search_term_string"}],"inLanguage":"pl-PL"},{"@type":"Organization","@id":"https:\/\/cmm.imgw.pl\/#organization","name":"Laboratorium Modelowania Meteorologicznego CMOK IMGW-PIB","url":"https:\/\/cmm.imgw.pl\/","sameAs":["https:\/\/www.facebook.com\/Meteoimgw\/","https:\/\/twitter.com\/IMGW_CMM"],"logo":{"@type":"ImageObject","inLanguage":"pl-PL","@id":"https:\/\/cmm.imgw.pl\/#\/schema\/logo\/image\/","url":"https:\/\/cmm.imgw.pl\/wp-content\/uploads\/2025\/10\/MODELE_LOGO_UNIFIKACJA_v2.png","contentUrl":"https:\/\/cmm.imgw.pl\/wp-content\/uploads\/2025\/10\/MODELE_LOGO_UNIFIKACJA_v2.png","width":1356,"height":365,"caption":"Laboratorium Modelowania Meteorologicznego CMOK IMGW-PIB"},"image":{"@id":"https:\/\/cmm.imgw.pl\/#\/schema\/logo\/image\/"}}]}},"_links":{"self":[{"href":"https:\/\/cmm.imgw.pl\/index.php?rest_route=\/wp\/v2\/pages\/46288"}],"collection":[{"href":"https:\/\/cmm.imgw.pl\/index.php?rest_route=\/wp\/v2\/pages"}],"about":[{"href":"https:\/\/cmm.imgw.pl\/index.php?rest_route=\/wp\/v2\/types\/page"}],"author":[{"embeddable":true,"href":"https:\/\/cmm.imgw.pl\/index.php?rest_route=\/wp\/v2\/users\/11"}],"replies":[{"embeddable":true,"href":"https:\/\/cmm.imgw.pl\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=46288"}],"version-history":[{"count":513,"href":"https:\/\/cmm.imgw.pl\/index.php?rest_route=\/wp\/v2\/pages\/46288\/revisions"}],"predecessor-version":[{"id":51035,"href":"https:\/\/cmm.imgw.pl\/index.php?rest_route=\/wp\/v2\/pages\/46288\/revisions\/51035"}],"wp:attachment":[{"href":"https:\/\/cmm.imgw.pl\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=46288"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}