PageSourceSearch

https://pkv-energoreport.pages.dev/

html pkv-energoreport.pages.dev collected 2026-10-03 05:43:03 UTC 318,458 bytes, 4,970 lines download raw bytes

1<!doctype html>
1<script type="application/json" id="cowork-artifact-meta">
2{
3  "name": "Energoreport",
4  "schemaVersion": 1,
5  "description": "Energoreport – data z české energetiky: burzovní ceny elektřiny a plynu (PXE, TTF), emisní povolenky, ropa, spotové ceny OTE, výroba a přeshraniční toky (ENTSO-E), zásobníky plynu CZ/EU, ceny a dodávky tepla, podíl OZE a konečná spotřeba energie vč. výhledů. Data se obnovují automaticky každý den v 5:30.",
6  "mcpTools": [
7    "mcp__workspace__web_fetch"
8  ],
9  "mcpServerNames": [
10    "workspace"
11  ]
12}
13</script>
13
14<html lang="cs">
15<head>
16<meta charset="utf-8" />
17<meta name="viewport" content="width=device-width,initial-scale=1" />
18<meta name="robots" content="noindex, nofollow, noarchive, nosnippet" />
19<meta name="googlebot" content="noindex, nofollow" />
20<meta http-equiv="cache-control" content="no-cache, no-store, must-revalidate" />
21<meta http-equiv="pragma" content="no-cache" />
22<meta http-equiv="expires" content="0" />
23<title>Energoreport</title>
24<!-- Safari nepodporuje data-URL/SVG favicony → skutečné soubory nasazované vedle index.html -->
25<link rel="icon" type="image/png" sizes="64x64" href="favicon.png">
26<link rel="icon" href="favicon.ico" sizes="32x32">
27<link rel="apple-touch-icon" href="apple-touch-icon.png">
28<script src="https://cdn.jsdelivr.net/npm/@supabase/supabase-js@2"></script>
vendor: 1 bytes, line 28
28
29<script>
30window.__SUPABASE_URL = 'https://kgjqffzudemmnsjttgxu.supabase.co';
31window.__SUPABASE_ANON = 'sb_publishable_NmSCvKfxREJ0HqD89o1-QQ_3zQ9k58l';
32window.__SUPA = null; // Lazy-init po načtení supabase-js
33</script>
vendor: 1 bytes, line 33
33
34<script>
35/* ECB EUR->CZK denni referencni kurz, forward-fill od 2007-01-01. Prepocet EUR/MWh -> CZK/MWh. */
36window.FX_BASE_MS = Date.UTC(2007,0,1);
37window.FX_DAILY = [27.485,27.525,27.455,27.605,27.635,27.635,27.635,27.649,27.6,27.735,27.774,27.755,27.755,27.755,27.765,27.763,27.868,27.855,27.768,27.768,27.768,27.805,27.914,28.063,28.148,28.125,28.125,28.125,28.215,28.3,28.163,28.095,28.202,28.202,28.202,28.138,28.125,28.128,28.212,28.245,28.245,28.245,28.31,28.233,28.268,28.28,28.249,28.249,28.249,28.16,28.125,28.167,28.237,28.325,28.325,28.325,28.404,28.463,28.295,28.225,28.158,28.158,28.158,28.188,28.176,28.153,28.145,28.168,28.168,28.168,28.238,28.202,28.148,28.056,27.868,27.868,27.868,27.778,27.808,27.938,27.986,27.973,27.973,27.973,27.935,27.986,28.075,28.035,28.01,28.01,28.01,27.94,28.011,27.933,27.943,27.943,27.943,27.943,27.943,27.95,28.003,27.963,27.965,27.965,27.965,27.963,27.964,27.992,28.02,28.033,28.033,28.033,28.026,28.065,28.106,28.115,28.166,28.166,28.166,28.123,28.123,28.127,28.137,28.118,28.118,28.118,28.167,28.232,28.236,28.257,28.272,28.272,28.272,28.288,28.273,28.235,28.11,28.2,28.2,28.2,28.164,28.186,28.247,28.252,28.308,28.308,28.308,28.325,28.288,28.34,28.324,28.285,28.285,28.285,28.323,28.399,28.433,28.412,28.42,28.42,28.42,28.442,28.462,28.511,28.603,28.586,28.586,28.586,28.599,28.597,28.728,28.605,28.65,28.65,28.65,28.718,28.696,28.648,28.633,28.718,28.718,28.718,28.737,28.777,28.699,28.65,28.624,28.624,28.624,28.675,28.629,28.475,28.353,28.318,28.318,28.318,28.205,28.271,28.248,28.281,28.253,28.253,28.253,28.192,28.183,28.121,28.124,28.027,28.027,28.027,28.025,28.037,28.0,28.017,28.043,28.043,28.043,28.067,28.149,28.183,28.119,28.044,28.044,28.044,28.046,27.978,27.935,27.51,27.663,27.663,27.663,27.699,27.668,27.749,27.734,27.693,27.693,27.693,27.808,27.716,27.628,27.594,27.726,27.726,27.726,27.67,27.645,27.64,27.608,27.631,27.631,27.631,27.6,27.63,27.515,27.483,27.508,27.508,27.508,27.501,27.499,27.673,27.487,27.523,27.523,27.523,27.568,27.554,27.59,27.606,27.532,27.532,27.532,27.538,27.495,27.592,27.535,27.53,27.53,27.53,27.513,27.483,27.49,27.473,27.482,27.482,27.482,27.523,27.526,27.44,27.447,27.239,27.239,27.239,27.215,27.182,27.178,27.114,26.962,26.962,26.962,26.922,26.86,26.973,27.026,26.941,26.941,26.941,26.975,26.95,26.934,26.894,26.73,26.73,26.73,26.645,26.691,26.695,26.624,26.655,26.655,26.655,26.693,26.69,26.781,26.765,26.759,26.759,26.759,26.765,26.767,26.494,26.38,26.261,26.261,26.261,26.242,26.29,26.237,26.121,26.164,26.164,26.164,25.998,26.04,26.04,26.248,26.415,26.415,26.415,26.395,26.353,26.3,26.372,26.448,26.448,26.448,26.545,26.545,26.545,26.608,26.58,26.58,26.58,26.628,26.628,26.364,26.175,26.129,26.129,26.129,26.145,26.125,25.98,25.856,25.908,25.908,25.908,25.865,25.905,26.051,26.128,26.118,26.118,26.118,26.319,26.2,26.07,25.974,25.912,25.912,25.912,25.89,25.903,26.006,26.07,25.858,25.858,25.858,25.751,25.671,25.635,25.621,25.658,25.658,25.658,25.65,25.605,25.48,25.346,25.224,25.224,25.224,25.234,25.319,25.29,25.088,25.046,25.046,25.046,25.0,25.02,25.048,25.143,25.228,25.228,25.228,25.068,24.916,25.048,25.125,25.17,25.17,25.17,25.041,25.125,25.066,25.12,25.034,25.034,25.034,25.017,25.25,25.463,25.491,25.491,25.491,25.491,25.491,25.455,25.595,25.383,25.25,25.25,25.25,25.335,25.184,25.07,24.938,25.035,25.035,25.035,25.019,24.985,25.098,25.129,25.025,25.025,25.025,24.94,24.819,24.848,24.975,25.12,25.12,25.12,25.091,25.055,25.075,25.128,25.254,25.254,25.254,25.161,25.25,25.205,25.205,25.265,25.265,25.265,25.227,25.175,25.142,25.171,25.147,25.147,25.147,24.95,24.939,25.029,25.04,24.977,24.977,24.977,25.056,25.062,25.085,25.147,25.103,25.103,25.103,25.083,25.143,25.209,25.06,25.088,25.088,25.088,25.03,24.84,24.67,24.57,24.583,24.583,24.583,24.653,24.433,24.4,24.331,24.195,24.195,24.195,24.211,24.194,24.0,24.11,24.17,24.17,24.17,24.125,24.075,24.07,24.086,24.002,24.002,24.002,23.893,23.825,23.865,23.816,23.694,23.694,23.694,23.55,23.608,23.49,23.465,23.515,23.515,23.515,23.31,23.375,23.213,23.142,23.063,23.063,23.063,22.968,23.013,23.753,23.58,23.595,23.595,23.595,23.695,23.724,23.941,23.947,24.003,24.003,24.003,23.994,23.928,23.98,24.105,24.211,24.211,24.211,24.101,23.954,23.942,24.351,24.485,24.485,24.485,24.523,24.398,24.408,24.385,24.373,24.373,24.373,24.402,24.52,24.533,24.703,24.735,24.735,24.735,24.815,24.848,24.798,24.782,24.822,24.822,24.822,24.938,24.768,24.85,24.63,24.436,24.436,24.436,24.3
3713,24.07,24.005,23.965,24.242,24.242,24.242,24.075,24.143,24.329,24.414,24.436,24.436,24.436,24.605,24.66,24.513,24.764,24.795,24.795,24.795,24.675,24.48,24.568,24.675,24.935,24.935,24.935,24.653,24.611,24.758,24.813,25.232,25.232,25.232,24.986,25.308,25.485,25.827,24.995,24.995,24.995,24.668,24.34,23.88,24.48,24.22,24.22,24.22,24.28,24.163,24.3,24.905,25.092,25.092,25.092,25.273,25.35,25.415,25.293,25.369,25.369,25.369,25.395,25.655,25.718,25.635,25.7,25.7,25.7,25.383,25.458,25.08,25.18,25.209,25.209,25.209,25.613,25.688,25.636,25.72,25.765,25.765,25.765,25.712,25.738,25.9,25.965,25.99,25.99,25.99,26.157,26.371,26.255,26.562,26.335,26.335,26.335,26.44,26.314,26.394,26.394,26.394,26.394,26.394,26.455,26.63,26.875,26.875,26.825,26.825,26.825,26.76,26.41,26.117,26.198,26.46,26.46,26.46,26.57,26.765,26.917,27.308,27.17,27.17,27.17,27.708,27.895,27.584,27.7,28.106,28.106,28.106,27.712,27.638,27.351,27.48,27.882,27.882,27.882,28.132,28.398,28.38,28.248,28.02,28.02,28.02,27.799,28.02,28.575,28.62,28.56,28.56,28.56,29.15,29.49,28.847,28.587,28.81,28.81,28.81,28.451,28.363,28.35,28.333,28.09,28.09,28.09,28.291,27.961,27.736,27.744,28.038,28.038,28.038,27.64,27.213,26.963,27.015,26.601,26.601,26.601,26.523,26.513,26.988,26.779,26.628,26.628,26.628,26.82,27.015,27.29,27.233,27.225,27.225,27.225,27.469,27.388,27.151,26.888,26.593,26.593,26.593,26.558,26.569,26.593,26.51,26.51,26.51,26.51,26.51,26.571,26.883,26.92,26.797,26.797,26.797,27.018,27.035,27.073,26.831,26.736,26.736,26.736,26.636,26.723,26.69,26.701,26.701,26.701,26.701,26.637,26.478,26.805,26.463,26.65,26.65,26.65,26.767,26.744,26.785,26.94,27.028,27.028,27.028,26.92,26.669,26.615,26.727,26.708,26.708,26.708,26.683,26.699,26.725,26.761,26.825,26.825,26.825,26.783,26.83,26.816,26.931,27.003,27.003,27.003,27.0,26.796,26.748,26.745,26.655,26.655,26.655,26.839,26.773,26.695,26.645,26.357,26.357,26.357,26.019,26.207,26.16,26.08,25.998,25.998,25.998,26.02,25.882,25.77,25.755,25.865,25.865,25.865,25.954,25.882,26.047,25.933,26.03,26.03,26.03,26.028,26.014,25.9,25.861,25.918,25.918,25.918,25.838,25.815,25.825,25.625,25.472,25.472,25.472,25.517,25.501,25.54,25.581,25.573,25.573,25.573,25.695,25.863,25.945,25.944,25.91,25.91,25.91,25.678,25.77,25.768,25.738,25.733,25.733,25.733,25.785,25.568,25.711,25.585,25.482,25.482,25.482,25.44,25.356,25.4,25.406,25.42,25.42,25.42,25.376,25.567,25.686,25.615,25.561,25.561,25.561,25.488,25.479,25.522,25.499,25.488,25.488,25.488,25.455,25.351,25.245,25.094,25.089,25.089,25.089,25.167,25.131,25.244,25.159,25.18,25.18,25.18,25.252,25.18,25.164,25.42,25.454,25.454,25.454,25.462,25.539,25.68,25.763,25.866,25.866,25.866,25.821,25.923,25.87,25.872,25.771,25.771,25.771,25.785,25.718,25.879,25.917,25.909,25.909,25.909,25.895,26.084,26.365,26.495,26.458,26.458,26.458,26.462,26.279,26.085,25.87,25.676,25.676,25.676,25.578,25.524,25.379,25.505,25.535,25.535,25.535,25.51,25.495,25.443,25.605,25.887,25.887,25.887,25.844,25.894,25.972,26.188,26.186,26.186,26.186,26.135,25.95,25.972,25.81,25.842,25.842,25.842,25.718,25.757,25.749,25.705,25.727,25.727,25.727,25.734,26.105,26.35,26.105,26.285,26.285,26.285,26.361,26.266,26.403,26.424,26.424,26.424,26.424,26.418,26.413,26.4,26.473,26.473,26.473,26.473,26.285,26.231,26.338,26.37,26.311,26.311,26.311,26.192,26.258,26.168,26.045,25.947,25.947,25.947,25.889,25.905,25.89,25.979,26.183,26.183,26.183,26.011,26.097,26.113,26.232,26.223,26.223,26.223,26.065,25.998,26.024,26.12,26.185,26.185,26.185,26.168,26.099,26.108,26.008,26.025,26.025,26.025,26.002,26.028,25.929,25.72,25.765,25.765,25.765,25.75,25.805,25.88,25.935,25.97,25.97,25.97,25.933,25.77,25.765,25.818,25.771,25.771,25.771,25.578,25.666,25.616,25.555,25.517,25.517,25.517,25.49,25.504,25.402,25.284,25.365,25.365,25.365,25.465,25.442,25.378,25.363,25.418,25.418,25.418,25.453,25.443,25.44,25.385,25.385,25.385,25.385,25.385,25.295,25.24,25.19,25.232,25.232,25.232,25.165,25.155,25.048,25.085,25.178,25.178,25.178,25.25,25.309,25.286,25.365,25.405,25.405,25.405,25.43,25.514,25.57,25.54,25.527,25.527,25.527,25.57,25.71,25.878,25.968,25.976,25.976,25.976,25.603,25.59,25.39,25.416,25.465,25.465,25.465,25.56,25.458,25.727,25.913,25.917,25.917,25.917,25.664,25.647,25.55,25.629,25.78,25.78,25.78,25.505,25.628,25.779,25.75,26.055,26.055,26.055,25.898,25.92,25.892,25.965,25.695,25.695,25.695,25.681,25.663,25.741,25.731,25.733,25.733,25.733,25.76,25.788,25.725,25.773,25.795,25.795,25.795,25.745,25.753,25.691,25.77,25.753,25.753,25.753,25.586,25.485,25.548,25.448,25.36,25.36,25.36,25.325,25.344,25.47,25.464,25.428,25.428,25.428,25.429,25.35,25.346,25.179,25.18,25.18,25.18,25.125,25.09,24.985,24.763,24.777,24.777,24.777,24.68,24.714,24.735,24.75,24.755,24.755,24.755,24.768,24.775,24.82,24.89,24.885,24.885,24.885,24.853,24.805,24.816,24.793,24.803,24.803,24.803,24.805,24.888,24.92,24.858,24.755,24.755,24.755,24.808,24.85,24.735,24.71,24.698,24.698,24.698,24.703,24.726,24.718,24.683,24.681,24.681,24.681,24.672,24.563,24.615,24.616,24.68,24.68,24.68,24.675,24.655,24.578,24.624,24.633,24.633,24.633,24.57,24.618,24.57,24.6,24.42,24.42,24.42,24.469,24.498,24.53,24.518,24.49,24.49,24.49,24.542,24.524,24.47,24.44,24.515,24.515,24.515,24.536,24.52,24.503,24.54,24.626,24.626,24.626,24.51,24.618,24.663,24.63,24.598,24.598,24.598,24.533,24.5,24.497,24.408,24.585,24.585,24.585,24.575,24.583,24.618,24.634,24.63,24.63,24.63,24.627,24.607,24.595,24.648,24.719,24.719,24.719,24.685,24.68,24.688,24.72,24.725,24.725,24.725,24.758,24.915,24.961,25.01,25.018,25.018,25.018,25.043,25.069,25.09,25.073,25.173,25.173,25.173,25.168,25.162,25.155,25.155,25.218,25.218,25.218,25.265,25.258,25.28,25.305,25.328,25.328,25.328,25.35,25.355,25.263,25.24,25.061,25.061,25.061,25.088,24.888,24.875,24.71,24.565,24.565,24.565,24.648,24.553,24.39,24.37,24.384,24.384,24.384,24.335,24.285,24.258,24.42,24.285,24.285,24.285,24.219,24.222,24.219,24.24,24.248,24.248,24.248,24.223,24.111,24.124,24.088,24.018,24.018,24.018,24.07,24.018,24.212,24.246,24.25,24.25,24.25,24.235,24.293,24.325,24.325,24.375,24.375,24.375,24.458,24.495,24.527,24.529,24.479,24.479,24.479,24.353,24.355,24.289,24.208,24.312,24.312,24.312,24.226,24.233,24.29,24.362,24.335,24.335,24.335,24.331,24.394,24.368,24.396,24.388,24.388,24.388,24.473,24.45,24.417,24.558,24.529,24.529,24.529,24.543,24.511,24.528,24.543,24.512,24.512,24.512,24.466,24.443,24.426,24.435,24.43,24.43,24.43,24.435,24.448,24.39,24.269,24.213,24.213,24.213,24.188,24.125,24.174,24.18,24.18,24.18,24.18,24.18,24.1,24.136,24.124,24.223,24.223,24.223,24.175,24.172,24.209,24.192,24.093,24.093,24.093,24.176,24.227,24.258,24.277,24.388,24.388,24.388,24.373,24.454,24.505,24.468,24.476,24.476,24.476,24.53,24.58,24.577,24.618,24.585,24.585,24.585,24.509,24.547,24.501,24.533,24.45,24.45,24.45,24.341,24.219,24.206,24.138,24.133,24.133,24.133,24.132,24.118,24.215,24.286,24.13,24.13,24.13,24.125,24.213,24.29,24.35,24.378,24.378,24.378,24.442,24.398,24.342,24.345,24.31,24.3
371,24.31,24.278,24.32,24.265,24.28,24.224,24.224,24.224,24.18,24.248,24.366,24.435,24.49,24.49,24.49,24.405,24.48,24.498,24.411,24.408,24.408,24.408,24.388,24.353,24.285,24.215,24.188,24.188,24.188,24.16,24.231,24.291,24.308,24.248,24.248,24.248,24.196,24.22,24.088,24.185,24.186,24.186,24.186,24.316,24.384,24.43,24.423,24.475,24.475,24.475,24.493,24.417,24.488,24.24,24.166,24.166,24.166,24.123,24.098,24.11,24.154,24.304,24.304,24.304,24.462,24.45,24.456,24.405,24.43,24.43,24.43,24.522,24.551,24.545,24.535,24.457,24.457,24.457,24.63,24.653,24.93,24.878,24.87,24.87,24.87,24.675,24.478,24.523,24.563,24.754,24.754,24.754,24.878,24.908,24.811,24.845,24.784,24.784,24.784,24.78,24.779,24.779,24.746,24.74,24.74,24.74,24.762,24.925,24.873,24.903,24.993,24.993,24.993,24.982,24.906,24.927,24.833,24.7,24.7,24.7,24.801,25.03,25.145,24.911,24.992,24.992,24.992,24.996,25.172,25.452,25.496,25.701,25.701,25.701,25.743,25.768,25.606,25.646,25.475,25.475,25.475,25.598,25.485,25.642,25.693,26.031,26.031,26.031,25.757,25.548,25.321,25.279,25.202,25.202,25.202,25.146,25.23,25.328,25.23,25.475,25.475,25.475,25.578,25.633,25.649,25.534,25.343,25.343,25.343,25.292,25.491,25.618,25.625,25.839,25.839,25.839,25.839,25.785,25.82,25.91,25.787,25.787,25.787,25.505,25.688,25.761,25.914,25.851,25.851,25.851,25.818,25.785,25.821,25.581,25.45,25.45,25.45,25.595,25.65,25.545,25.298,25.466,25.466,25.466,25.349,25.422,25.371,25.193,25.156,25.156,25.156,25.27,25.188,25.157,25.151,25.064,25.064,25.064,24.98,25.0,24.812,24.985,25.245,25.245,25.245,25.045,25.097,25.17,25.268,25.001,25.001,25.001,24.907,24.912,25.172,25.075,25.033,25.033,25.033,25.068,24.9,24.843,24.89,24.715,24.715,24.715,24.78,24.866,24.871,24.767,24.715,24.715,24.715,24.563,24.585,24.615,24.558,24.511,24.511,24.511,24.533,24.464,24.629,24.736,24.725,24.725,24.725,24.63,24.603,24.603,24.778,24.73,24.73,24.73,24.773,24.621,24.595,24.704,24.704,24.704,24.704,24.704,24.795,24.823,24.803,24.75,24.75,24.75,24.789,24.799,24.816,24.842,24.919,24.919,24.919,25.043,24.997,24.804,24.758,24.87,24.87,24.87,24.867,24.867,24.903,24.933,25.023,25.023,25.023,25.025,25.133,25.243,25.162,25.253,25.253,25.253,25.395,25.515,25.46,25.515,25.333,25.333,25.333,25.225,25.294,25.495,25.39,25.423,25.423,25.423,25.309,25.512,25.658,25.693,25.787,25.787,25.787,25.733,25.72,25.565,25.325,25.481,25.481,25.481,25.395,25.682,25.578,25.54,25.592,25.592,25.592,25.493,25.495,25.453,25.683,25.775,25.775,25.775,25.813,25.963,25.916,25.81,25.64,25.64,25.64,25.515,25.555,25.5,25.457,25.7,25.7,25.7,25.536,25.431,25.395,25.444,25.395,25.395,25.395,25.388,25.34,25.278,25.331,25.57,25.57,25.57,25.579,25.539,25.56,25.505,25.31,25.31,25.31,25.258,25.255,25.358,25.26,25.285,25.285,25.285,25.158,25.114,25.173,25.135,25.205,25.205,25.205,25.145,25.04,24.962,24.912,25.02,25.02,25.02,24.905,24.783,24.968,24.903,24.906,24.906,24.906,24.845,24.828,24.81,24.917,24.84,24.84,24.84,24.894,24.88,24.794,24.703,24.587,24.587,24.587,24.53,24.548,24.44,24.482,24.433,24.433,24.433,24.505,24.815,24.87,24.904,24.803,24.803,24.803,24.94,24.921,24.983,24.859,25.141,25.141,25.141,25.08,25.06,25.033,24.957,24.915,24.915,24.915,24.9,24.936,24.95,24.94,24.95,24.95,24.95,24.931,24.875,24.794,24.756,24.823,24.823,24.823,24.899,24.908,24.973,24.901,24.9,24.9,24.9,25.021,25.022,25.064,25.226,25.232,25.232,25.232,25.234,25.305,25.395,25.423,25.396,25.396,25.396,25.366,25.445,25.5,25.566,25.534,25.534,25.534,25.418,25.399,25.484,25.422,25.339,25.339,25.339,25.293,25.304,25.265,25.226,25.262,25.262,25.262,25.256,25.243,25.218,25.195,25.208,25.208,25.208,25.245,25.29,25.265,25.289,25.231,25.231,25.231,25.222,25.2,25.254,25.228,25.189,25.189,25.189,25.127,25.127,25.127,25.115,25.14,25.14,25.14,25.151,25.151,25.218,25.26,25.355,25.355,25.355,25.539,25.587,25.529,25.629,25.613,25.613,25.613,25.613,25.613,25.58,25.546,25.635,25.635,25.635,25.626,25.613,25.597,25.597,25.605,25.605,25.605,25.69,25.659,25.652,25.619,25.638,25.638,25.638,25.669,25.647,25.732,25.275,25.243,25.243,25.243,25.24,25.315,25.423,25.383,25.386,25.386,25.386,25.388,25.434,25.393,25.496,25.492,25.492,25.492,25.52,25.561,25.637,25.637,25.677,25.677,25.677,25.66,25.629,25.565,25.51,25.443,25.443,25.443,25.542,25.662,25.632,25.615,25.575,25.575,25.575,25.62,25.647,25.67,25.813,25.832,25.832,25.832,25.738,25.806,25.8,25.74,25.74,25.74,25.74,25.74,25.881,25.828,25.817,25.763,25.763,25.763,25.727,25.762,25.865,25.942,25.871,25.871,25.871,25.864,25.868,25.854,25.875,25.857,25.857,25.857,25.93,25.909,25.909,25.899,25.744,25.744,25.744,25.697,25.799,25.799,25.65,25.64,25.64,25.64,25.693,25.747,25.862,25.792,25.808,25.808,25.808,25.844,25.887,26.003,25.98,25.989,25.989,25.989,26.121,26.098,26.069,26.094,25.993,25.993,25.993,25.968,25.889,25.899,25.795,25.711,25.711,25.711,25.753,25.775,25.846,25.778,25.569,25.569,25.569,25.666,25.613,25.66,25.728,25.718,25.718,25.718,25.724,25.68,25.686,25.806,25.825,25.825,25.825,25.865,25.788,25.858,25.89,25.949,25.949,25.949,25.98,25.983,26.02,26.05,25.875,25.875,25.875,25.938,25.875,25.929,25.898,25.951,25.951,25.951,26.01,25.96,25.968,25.925,25.931,25.931,25.931,25.976,25.977,25.93,25.959,25.948,25.948,25.948,25.914,25.856,25.857,25.955,25.959,25.959,25.959,25.937,25.92,25.978,25.816,25.925,25.925,25.925,25.88,25.874,25.822,25.815,25.8,25.8,25.8,25.855,25.773,25.775,25.738,25.665,25.665,25.665,25.626,25.758,25.713,25.678,25.735,25.735,25.735,25.683,25.725,25.787,25.737,25.765,25.765,25.765,25.833,25.83,25.783,25.82,25.775,25.775,25.775,25.769,25.718,25.813,25.735,25.828,25.828,25.828,25.925,25.933,25.88,25.809,25.69,25.69,25.69,25.73,25.647,25.606,25.558,25.554,25.554,25.554,25.513,25.518,25.598,25.529,25.534,25.534,25.534,25.56,25.615,25.68,25.704,25.782,25.782,25.782,25.803,25.727,25.817,25.773,25.75,25.75,25.75,25.732,25.762,25.742,25.729,25.854,25.854,25.854,25.835,25.833,25.784,26.85,26.9
3766,26.966,26.966,26.996,27.01,27.055,27.176,27.155,27.155,27.155,27.117,27.368,27.329,27.197,27.258,27.258,27.258,27.269,27.329,27.341,27.35,27.391,27.391,27.391,27.407,27.468,27.458,27.45,27.48,27.48,27.48,27.498,27.453,27.434,27.475,27.534,27.534,27.534,27.6,27.657,27.734,27.657,27.66,27.66,27.66,27.574,27.536,27.536,27.536,27.445,27.445,27.445,27.48,27.427,27.427,27.481,27.525,27.525,27.525,27.485,27.454,27.448,27.433,27.392,27.392,27.392,27.41,27.409,27.441,27.491,27.458,27.458,27.458,27.524,27.555,27.535,27.49,27.525,27.525,27.525,27.475,27.507,27.54,27.594,27.5,27.5,27.5,27.527,27.53,27.535,27.525,27.503,27.503,27.503,27.547,27.53,27.533,27.53,27.436,27.436,27.436,27.396,27.392,27.43,27.382,27.368,27.368,27.368,27.362,27.346,27.325,27.339,27.344,27.344,27.344,27.354,27.369,27.353,27.363,27.333,27.333,27.333,27.348,27.35,27.353,27.355,27.358,27.358,27.358,27.411,27.411,27.46,27.488,27.458,27.458,27.458,27.411,27.395,27.459,27.41,27.423,27.423,27.423,27.442,27.449,27.463,27.432,27.429,27.429,27.429,27.444,27.415,27.406,27.427,27.452,27.452,27.452,27.466,27.459,27.463,27.502,27.502,27.502,27.502,27.502,27.481,27.459,27.443,27.46,27.46,27.46,27.458,27.434,27.454,27.454,27.444,27.444,27.444,27.436,27.438,27.412,27.403,27.392,27.392,27.392,27.396,27.404,27.43,27.44,27.443,27.443,27.443,27.474,27.479,27.467,27.443,27.438,27.438,27.438,27.421,27.433,27.442,27.474,27.471,27.471,27.471,27.482,27.463,27.46,27.45,27.461,27.461,27.461,27.451,27.451,27.445,27.42,27.44,27.44,27.44,27.438,27.445,27.481,27.435,27.433,27.433,27.433,27.458,27.443,27.435,27.447,27.459,27.459,27.459,27.453,27.433,27.432,27.439,27.444,27.444,27.444,27.434,27.437,27.433,27.44,27.44,27.44,27.44,27.439,27.431,27.446,27.44,27.433,27.433,27.433,27.468,27.485,27.454,27.48,27.482,27.482,27.482,27.481,27.49,27.509,27.57,27.638,27.638,27.638,27.645,27.668,27.834,27.853,27.825,27.825,27.825,27.852,27.845,27.839,27.845,27.882,27.882,27.882,27.9,28.004,27.928,27.814,27.838,27.838,27.838,27.833,27.841,27.75,27.775,27.725,27.725,27.725,27.738,27.784,27.658,27.662,27.596,27.596,27.596,27.637,27.678,27.724,27.635,27.6,27.6,27.6,27.579,27.555,27.532,27.518,27.574,27.574,27.574,27.543,27.517,27.497,27.572,27.534,27.534,27.534,27.54,27.5,27.502,27.495,27.479,27.479,27.479,27.481,27.47,27.48,27.47,27.493,27.493,27.493,27.545,27.545,27.545,27.546,27.492,27.492,27.492,27.562,27.613,27.683,27.69,27.695,27.695,27.695,27.705,27.749,27.784,27.729,27.77,27.77,27.77,27.788,27.788,27.823,27.769,27.705,27.705,27.705,27.585,27.592,27.603,27.644,27.653,27.653,27.653,27.701,27.677,27.683,27.657,27.66,27.66,27.66,27.592,27.583,27.579,27.609,27.652,27.652,27.652,27.626,27.605,27.623,27.616,27.635,27.635,27.635,27.611,27.618,27.618,27.633,27.59,27.59,27.59,27.565,27.607,27.626,27.606,27.636,27.636,27.636,27.618,27.66,27.777,27.777,27.777,27.777,27.777,27.717,27.728,27.735,27.735,27.693,27.693,27.693,27.676,27.695,27.783,27.865,28.062,28.062,28.062,28.287,28.405,28.229,27.841,27.795,27.795,27.795,27.881,27.849,27.939,27.9,27.831,27.831,27.831,27.754,27.872,27.846,27.792,27.797,27.797,27.797,27.749,27.748,27.77,27.708,27.698,27.698,27.698,27.72,27.727,27.68,27.702,27.64,27.64,27.64,27.638,27.619,27.583,27.373,27.508,27.508,27.508,27.444,27.487,27.419,27.515,27.438,27.438,27.438,27.488,27.453,27.464,27.422,27.297,27.297,27.297,27.251,27.285,27.293,27.303,27.315,27.315,27.315,27.308,27.183,27.318,27.425,27.479,27.479,27.479,27.374,27.39,27.378,27.376,27.523,27.523,27.523,27.473,27.533,27.543,27.57,27.57,27.57,27.57,27.57,27.455,27.378,27.378,27.383,27.383,27.383,27.392,27.345,27.415,27.503,27.486,27.486,27.486,27.417,27.418,27.469,27.399,27.454,27.454,27.454,27.441,27.471,27.435,27.43,27.43,27.43,27.43,27.364,27.362,27.415,27.405,27.411,27.411,27.411,27.4,27.394,27.45,27.458,27.401,27.401,27.401,27.401,27.357,27.33,27.351,27.395,27.395,27.395,27.372,27.407,27.404,27.463,27.401,27.401,27.401,27.44,27.439,27.46,27.467,27.396,27.396,27.396,27.431,27.356,27.318,27.318,27.3,27.3,27.3,27.279,27.254,27.24,27.275,27.211,27.211,27.211,27.206,27.189,27.206,27.235,27.23,27.23,27.23,27.246,27.253,27.246,27.265,27.145,27.145,27.145,27.135,27.1,27.116,27.11,27.123,27.123,27.123,27.102,27.08,27.078,27.116,27.054,27.054,27.054,27.072,27.072,27.068,27.039,27.042,27.042,27.042,27.029,27.023,27.066,27.053,27.031,27.031,27.031,27.041,27.055,27.037,27.029,27.024,27.024,27.024,27.034,27.026,27.021,27.024,27.028,27.028,27.028,27.021,27.021,27.022,27.023,27.021,27.021,27.021,27.068,27.109,27.085,27.098,27.048,27.048,27.048,27.021,27.022,27.023,27.022,27.03,27.03,27.03,27.026,27.047,27.057,27.038,27.074,27.074,27.074,27.116,27.088,27.065,27.112,27.071,27.071,27.071,27.053,27.057,27.09,27.162,27.193,27.193,27.193,27.22,27.208,27.187,27.173,27.163,27.163,27.163,27.122,27.109,27.099,27.117,27.109,27.109,27.109,27.108,27.131,27.113,27.084,27.081,27.081,27.081,27.083,27.08,27.079,27.069,27.074,27.074,27.074,27.088,27.098,27.134,27.113,27.09,27.09,27.09,27.097,27.1,27.074,27.044,27.026,27.026,27.026,27.042,27.044,27.022,27.031,27.032,27.032,27.032,27.031,27.031,27.029,27.025,27.031,27.031,27.031,27.025,27.033,27.025,27.032,27.024,27.024,27.024,27.03,27.023,27.024,27.036,27.035,27.035,27.035,27.022,27.022,27.021,27.021,27.023,27.023,27.023,27.024,27.022,27.03,27.031,27.03,27.03,27.03,27.028,27.028,27.033,27.028,27.028,27.028,27.028,27.022,27.028,27.029,27.023,27.023,27.023,27.023,27.023,27.022,27.026,27.03,27.022,27.022,27.022,27.021,27.021,27.021,27.021,27.021,27.021,27.021,27.037,27.023,27.042,27.064,27.026,27.026,27.026,27.021,27.021,27.025,27.021,27.026,27.026,27.026,27.021,27.026,27.025,27.021,27.037,27.037,27.037,27.062,27.058,27.031,27.069,27.063,27.063,27.063,27.037,27.029,27.044,27.03,27.023,27.023,27.023,27.022,27.028,27.047,27.052,27.064,27.064,27.064,27.057,27.056,27.046,27.057,27.058,27.058,27.058,27.056,27.058,27.047,27.037,27.059,27.059,27.059,27.05,27.043,27.05,27.044,27.035,27.035,27.035,27.035,27.037,27.038,27.064,27.064,27.064,27.064,27.064,27.073,27.075,27.051,27.03,27.03,27.03,27.046,27.036,27.029,27.025,27.022,27.022,27.022,27.026,27.03,27.036,27.028,27.025,27.025,27.025,27.022,27.021,27.021,27.024,27.046,27.046,27.046,27.047,27.027,27.034,27.045,27.038,27.038,27.038,27.048,27.037,27.031,27.028,27.021,27.021,27.021,27.022,27.023,27.022,27.023,27.021,27.021,27.021,27.023,27.022,27.022,27.023,27.025,27.025,27.025,27.033,27.025,27.028,27.023,27.028,27.028,27.028,27.021,27.022,27.027,27.029,27.025,27.025,27.025,27.023,27.021,27.021,27.021,27.026,27.026,27.026,27.035,27.048,27.073,27.067,27.069,27.069,27.069,27.063,27.064,27.071,27.062,27.103,27.103,27.103,27.151,27.108,27.114,27.131,27.093,27.093,27.093,27.095,27.071,27.078,27.049,27.033,27.033,27.033,27.037,27.034,27.033,27.049,27.04,27.04,27.04,27.024,27.014,27.022,27.025,27.023,27.023,27.023,27.025,27.021,27.035,27.045,27.031,27.031,27.031,27.029,27.032,27.034,27.027,27.022,27.022,27.022,27.027,27.027,27.025,27.02,27.018,27.018,27.018,27.023,27.02,27.023,27.024,27.022,27.022,27.022,27.024,27.023,27.022,27.026,27.024,27.024,27.024,27.023,27.025,27.026,27.025,27.022,27.022,27.022,27.021,27.021,27.021,27.021,27.022,27.022,27.022,27.024,27.021,27.023,27.023,27.024,27.024,27.024,27.021,27.022,27.022,27.021,27.022,27.022,27.022,27.021,27.025,27.023,27.022,27.021,27.021,27.021,27.021,27.021,27.028,27.021,27.021,27.021,27.021,27.021,27.021,27.025,27.021,27.029,27.029,27.029,27.024,27.021,27.021,27.021,27.021,27.021,27.021,27.021,27.021,27.021,27.022,27.022,27.022,27.022,27.024,27.024,27.022,27.021,27.021,27.021,27.021,27.021,27.021,27.027,27.022,27.054,27.054,27.054,27.026,27.03,27.036,27.05,27.035,27.035,27.035,27.032,27.044,27.027,27.034,27.039,27.039,27.039,27.044,27.045,27.06,27.061,27.058,27.058,27.058,27.057,27.045,27.036,27.038,27.027,27.027,27.027,27.027,27.022,27.023,27.021,27.021,27.021,27.021,27.021,27.021,27.021,27.021,27.023,27.023,27.023,27.023,27.021,27.042,27.021,27.021,27.021,27.021,27.021,27.02,27.021,27.021,27.021,27.021,27.021,27.021,27.021,27.021,27.021,27.021,27.021,27.021,27.021,27.021,27.021,27.021,27.021,27.021,27.021,27.027,27.021,27.022,27.021,27.021,27.021,27.021,27.022,27.021,27.021,27.022,27.021,27.021,27.021,27.021,27.021,27.021,27.021,27.021,27.021,27.021,27.021,27.021,27.021,27.021,27.021,27.021,27.021,27.021,27.021,27.021,27.021,27.021,27.021,27.021,27.021,27.021,27.021,27.021,27.021,27.021,27.021,27.021,27.021,27.021,27.021,27.021,27.021,27.021,27.021,27.021,27.021,27.021,27.021,27.021,27.021,27.021,27.021,27.021,27.021,27.021,27.021,27.021,27.021,27.021,27.021,27.022,27.03,27.03,27.03,27.044,27.054,27.058,26.736,26.563,26.563,26.563,26.528,26.658,26.697,26.704,26.704,26.704,26.704,26.704,26.759,26.794,26.907,26.933,26.933,26.933,26.793,26.771,26.947,26.937,26.922,26.922,26.922,26.922,26.893,26.871,26.767,26.781,26.781,26.781,26.712,26.672,26.642,26.598,26.576,26.576,26.576,26.471,26.42,26.435,26.632,26.503,26.503,26.503,26.488,26.483,26.463,26.413,26.434,26.434,26.434,26.435,26.467,26.422,26.397,26.36,26.36,26.36,26.327,26.336,26.316,26.268,26.223,26.223,26.223,26.198,26.147,26.163,26.213,26.233,26.233,26.233,26.172,26.293,26.266,26.27,26.297,26.297,26.297,26.243,26.27,26.326,26.3,26.197,26.197,26.197,26.14,26.132,26.109,26.147,26.079,26.079,26.079,26.096,26.124,26.11,26.12,26.075,26.075,26.075,26.09,26.08,26.035,26.04,26.017,26.017,26.017,26.024,26.013,26.047,26.045,26.048,26.048,26.048,26.079,26.132,26.128,25.965,26.068,26.068,26.068,26.116,26.15,26.16,26.157,26.155,26.155,26.155,26.118,26.142,26.044,26.012,26.108,26.108,26.108,26.086,26.083,26.124,26.098,26.084,26.084,26.084,26.102,26.142,26.045,26.101,26.077,26.077,26.077,26.062,26.077,26.112,26.113,26.099,26.099,26.099,26.077,26.105,26.101,26.103,26.081,26.081,26.081,26.091,26.103,26.091,26.083,26.046,26.046,26.046,26.033,26.056,26.045,26.041,25.981,25.981,25.981,25.995,25.932,25.886,25.837,25.816,25.816,25.816,25.893,25.9,25.879,25.891,25.813,25.813,25.813,25.77,25.733,25.705,25.725,25.69,25.69,25.69,25.641,25.583,25.589,25.589,25.673,25.673,25.673,25.645,25.669,25.557,25.535,25.652,25.
37652,25.652,25.612,25.591,25.56,25.528,25.543,25.543,25.543,25.567,25.566,25.669,25.559,25.588,25.588,25.588,25.568,25.533,25.469,25.462,25.413,25.413,25.413,25.431,25.463,25.474,25.491,25.524,25.524,25.524,25.557,25.653,25.636,25.611,25.555,25.555,25.555,25.601,25.596,25.65,25.695,25.678,25.678,25.678,25.685,25.662,25.674,25.715,25.75,25.75,25.75,25.75,25.75,25.84,25.645,25.535,25.535,25.535,25.535,25.494,25.545,25.515,25.594,25.594,25.594,25.524,25.535,25.57,25.547,25.521,25.521,25.521,25.531,25.516,25.447,25.365,25.433,25.433,25.433,25.399,25.403,25.374,25.381,25.357,25.357,25.357,25.297,25.33,25.272,25.267,25.192,25.192,25.192,25.196,25.233,25.23,25.247,25.335,25.335,25.335,25.358,25.386,25.37,25.37,25.34,25.34,25.34,25.327,25.318,25.361,25.316,25.337,25.337,25.337,25.391,25.398,25.418,25.434,25.409,25.409,25.409,25.404,25.38,25.421,25.427,25.454,25.454,25.454,25.448,25.471,25.447,25.416,25.415,25.415,25.415,25.432,25.423,25.422,25.399,25.405,25.405,25.405,25.446,25.473,25.462,25.425,25.425,25.425,25.425,25.425,25.367,25.34,25.327,25.339,25.339,25.339,25.357,25.335,25.323,25.316,25.307,25.307,25.307,25.265,25.254,25.303,25.327,25.34,25.34,25.34,25.408,25.437,25.466,25.472,25.471,25.471,25.471,25.542,25.542,25.601,25.585,25.503,25.503,25.503,25.518,25.526,25.564,25.498,25.506,25.506,25.506,25.488,25.551,25.545,25.557,25.59,25.59,25.59,25.68,25.702,25.813,25.809,25.765,25.765,25.765,25.731,25.907,25.835,25.797,25.827,25.827,25.827,25.696,25.653,25.648,25.64,25.795,25.795,25.795,25.682,25.652,25.711,25.651,25.714,25.714,25.714,25.739,25.811,25.835,25.863,25.792,25.792,25.792,25.935,25.9,25.777,26.001,26.02,26.02,26.02,25.998,26.073,26.028,25.938,25.942,25.942,25.942,25.858,25.898,25.929,25.938,25.924,25.924,25.924,25.883,25.875,25.853,25.92,25.875,25.875,25.875,25.848,25.761,25.675,25.647,25.622,25.622,25.622,25.624,25.598,25.59,25.589,25.657,25.657,25.657,25.65,25.606,25.578,25.57,25.635,25.635,25.635,25.656,25.692,25.705,25.702,25.729,25.729,25.729,25.706,25.737,25.725,25.713,25.744,25.744,25.744,25.744,25.713,25.745,25.749,25.735,25.735,25.735,25.749,25.728,25.746,25.715,25.697,25.697,25.697,25.648,25.646,25.59,25.52,25.473,25.473,25.473,25.457,25.448,25.434,25.56,25.585,25.585,25.585,25.615,25.613,25.605,25.715,25.731,25.731,25.731,25.765,25.788,25.779,25.78,25.742,25.742,25.742,25.737,25.805,25.807,25.848,25.777,25.777,25.777,25.798,25.821,25.843,25.863,25.867,25.867,25.867,25.848,25.814,25.833,25.831,25.86,25.86,25.86,25.849,25.87,25.922,25.879,25.782,25.782,25.782,25.846,25.833,25.88,25.887,25.936,25.936,25.936,25.942,25.939,25.994,25.985,25.985,25.985,25.985,26.013,26.032,26.002,25.99,25.954,25.954,25.954,25.918,25.914,25.931,25.967,25.957,25.957,25.957,25.921,25.901,25.886,25.89,25.851,25.851,25.851,25.866,25.845,25.866,25.829,25.794,25.794,25.794,25.801,25.753,25.765,25.76,25.855,25.855,25.855,25.916,25.916,25.916,25.858,25.778,25.778,25.778,25.724,25.724,25.752,25.683,25.653,25.653,25.653,25.572,25.642,25.629,25.626,25.606,25.606,25.606,25.561,25.572,25.564,25.537,25.58,25.58,25.58,25.593,25.612,25.693,25.695,25.697,25.697,25.697,25.726,25.747,25.802,25.76,25.695,25.695,25.695,25.728,25.697,25.781,25.809,25.806,25.806,25.806,25.836,25.871,25.795,25.79,25.709,25.709,25.709,25.715,25.717,25.679,25.648,25.667,25.667,25.667,25.653,25.663,25.661,25.601,25.636,25.636,25.636,25.621,25.623,25.592,25.61,25.641,25.641,25.641,25.656,25.667,25.668,25.668,25.668,25.668,25.668,25.616,25.604,25.646,25.648,25.727,25.727,25.727,25.759,25.769,25.797,25.786,25.802,25.802,25.802,25.791,25.753,25.724,25.693,25.
37613,25.613,25.613,25.634,25.618,25.608,25.606,25.623,25.623,25.623,25.625,25.663,25.657,25.682,25.682,25.682,25.682,25.682,25.743,25.729,25.737,25.705,25.705,25.705,25.682,25.659,25.659,25.648,25.712,25.712,25.712,25.715,25.731,25.727,25.718,25.732,25.732,25.732,25.776,25.751,25.76,25.696,25.753,25.753,25.753,25.766,25.775,25.793,25.821,25.83,25.83,25.83,25.834,25.843,25.861,25.841,25.816,25.816,25.816,25.832,25.744,25.657,25.663,25.642,25.642,25.642,25.626,25.64,25.614,25.581,25.54,25.54,25.54,25.56,25.609,25.635,25.619,25.609,25.609,25.609,25.601,25.557,25.486,25.434,25.447,25.447,25.447,25.478,25.462,25.458,25.434,25.471,25.471,25.471,25.509,25.543,25.565,25.605,25.59,25.59,25.59,25.569,25.575,25.603,25.586,25.546,25.546,25.546,25.534,25.549,25.542,25.519,25.542,25.542,25.542,25.613,25.65,25.658,25.742,25.763,25.763,25.763,25.776,25.727,25.719,25.789,25.828,25.828,25.828,25.831,25.829,25.884,25.876,25.734,25.734,25.734,25.78,25.796,25.794,25.784,25.769,25.769,25.769,25.792,25.814,25.844,25.853,25.914,25.914,25.914,25.919,25.885,25.834,25.853,25.836,25.836,25.836,25.855,25.869,25.867,25.824,25.843,25.843,25.843,25.882,25.916,25.892,25.892,25.913,25.913,25.913,25.89,25.872,25.867,25.851,25.842,25.842,25.842,25.816,25.74,25.739,25.739,25.741,25.741,25.741,25.781,25.805,25.823,25.893,25.807,25.807,25.807,25.823,25.82,25.758,25.673,25.659,25.659,25.659,25.63,25.57,25.638,25.598,25.568,25.568,25.568,25.48,25.543,25.512,25.509,25.514,25.514,25.514,25.511,25.536,25.498,25.534,25.486,25.486,25.486,25.51,25.499,25.591,25.56,25.583,25.583,25.583,25.593,25.565,25.541,25.52,25.513,25.513,25.513,25.485,25.505,25.515,25.574,25.515,25.515,25.515,25.534,25.527,25.52,25.526,25.533,25.533,25.533,25.528,25.527,25.522,25.519,25.508,25.508,25.508,25.483,25.445,25.49,25.473,25.445,25.445,25.445,25.499,25.485,25.485,25.485,25.509,25.509,25.509,25.463,25.408,25.408,25.411,25.36,25.36,25.36,25.301,25.276,25.265,25.253,25.265,25.265,25.265,25.23,25.155,25.144,25.17,25.147,25.147,25.147,25.125,25.066,25.136,25.159,25.16,25.16,25.16,25.231,25.221,25.208,25.25,25.21,25.21,25.21,25.177,25.143,25.055,24.893,25.03,25.03,25.03,25.026,24.965,24.878,24.835,24.828,24.828,24.828,24.793,24.898,24.962,25.037,25.061,25.061,25.061,25.186,25.226,25.344,25.283,25.39,25.39,25.39,25.525,25.475,25.34,25.346,25.458,25.458,25.458,25.504,25.725,25.77,26.203,26.042,26.042,26.042,26.96,26.985,27.154,27.606,27.191,27.191,27.191,27.635,27.808,27.446,27.55,27.299,27.299,27.299,27.315,27.312,27.369,27.553,27.539,27.539,27.539,27.603,27.215,27.183,26.909,26.909,26.909,26.909,26.909,26.864,26.991,27.053,27.163,27.163,27.163,27.328,27.447,27.534,27.551,27.31,27.31,27.31,27.184,27.227,27.125,27.097,27.097,27.097,27.097,27.119,26.978,26.987,27.158,27.251,27.251,27.251,27.6,27.423,27.408,27.571,27.589,27.589,27.589,27.61,27.49,27.43,27.212,27.21,27.21,27.21,27.275,27.073,27.051,27.017,26.921,26.921,26.921,26.859,26.645,26.66,26.623,26.589,26.589,26.589,26.585,26.634,26.609,26.679,26.698,26.698,26.698,26.683,26.564,26.561,26.689,26.683,26.683,26.683,26.681,26.612,26.733,26.796,26.808,26.808,26.808,26.848,26.74,26.67,26.632,26.665,26.665,26.665,26.705,26.681,26.748,26.622,26.691,26.691,26.691,26.648,26.638,26.587,26.693,26.682,26.682,26.682,26.598,26.429,26.365,26.342,26.268,26.268,26.268,26.193,26.251,26.291,26.248,26.175,26.175,26.175,26.319,26.221,26.097,26.202,26.279,26.279,26.279,26.183,26.155,26.117,26.125,26.118,26.118,26.118,26.14,26.134,26.118,26.061,26.083,26.083,26.083,26.091,26.121,26.262,26.276,26.2,26.2,26.2,26.208,26.226,26.338,26.368,26.435,26.435,26.435,26.468,26.521,26.519,26.585,26.566,26.566,26.566,26.66,26.827,26.726,26.747,26.727,26.727,26.727,26.98,27.007,26.965,27.015,27.107,27.107,27.107,27.133,27.148,27.233,26.918,27.02,27.02,27.02,27.096,27.035,27.064,27.092,27.11,27.11,27.11,27.177,27.356,27.313,27.34,27.279,27.279,27.279,27.346,27.233,27.185,27.23,27.222,27.222,27.222,27.295,27.337,27.422,27.368,27.251,27.251,27.251,27.131,26.913,26.781,26.766,26.667,26.667,26.667,26.475,26.431,26.447,26.465,26.461,26.461,26.461,26.353,26.508,26.401,26.358,26.34,26.34,26.34,26.311,26.258,26.175,26.138,26.213,26.213,26.213,26.192,26.235,26.411,26.42,26.518,26.518,26.518,26.482,26.398,26.239,26.307,26.328,26.328,26.328,26.317,26.361,26.2,26.204,26.139,26.139,26.139,26.275,26.303,26.372,26.299,26.299,26.299,26.299,26.247,26.302,26.252,26.242,26.242,26.242,26.242,26.141,26.227,26.145,26.147,26.163,26.163,26.163,26.239,26.19,26.173,26.19,26.163,26.163,26.163,26.185,26.161,26.125,26.094,26.152,26.152,26.152,26.08,26.08,26.025,26.114,26.02,26.02,26.02,25.975,25.9,25.925,25.895,25.806,25.806,25.806,25.739,25.738,25.835,25.772,25.753,25.753,25.753,25.682,25.753,25.883,25.864,25.849,25.849,25.849,25.954,25.906,25.978,26.11,26.195,26.195,26.195,26.087,26.142,26.138,26.208,26.303,26.303,26.303,26.417,26.288,26.226,26.154,26.15,26.15,26.15,26.193,26.197,26.159,26.17,26.127,26.127,26.127,26.075,26.198,26.206,26.233,26.081,26.081,26.081,26.079,26.122,26.143,26.085,26.085,26.085,26.085,26.085,26.051,25.9
3719,25.875,25.945,25.945,25.945,26.031,26.016,25.929,25.942,25.927,25.927,25.927,25.903,25.903,25.893,25.86,25.847,25.847,25.847,25.807,25.895,25.96,25.817,25.883,25.883,25.883,25.773,25.841,25.825,25.847,25.682,25.682,25.682,25.591,25.571,25.522,25.575,25.492,25.492,25.492,25.498,25.427,25.445,25.519,25.452,25.452,25.452,25.424,25.447,25.451,25.439,25.451,25.451,25.451,25.454,25.462,25.465,25.448,25.451,25.451,25.451,25.4,25.394,25.377,25.391,25.335,25.335,25.335,25.422,25.45,25.472,25.51,25.519,25.519,25.519,25.569,25.533,25.408,25.423,25.487,25.487,25.487,25.499,25.477,25.488,25.507,25.606,25.606,25.606,25.586,25.61,25.688,25.894,25.791,25.791,25.791,25.723,25.644,25.646,25.587,25.538,25.538,25.538,25.605,25.642,25.695,25.639,25.65,25.65,25.65,25.629,25.685,25.625,25.506,25.501,25.501,25.501,25.476,25.49,25.44,25.415,25.413,25.413,25.413,25.391,25.382,25.405,25.393,25.386,25.386,25.386,25.427,25.441,25.447,25.501,25.559,25.559,25.559,25.514,25.533,25.531,25.546,25.563,25.563,25.563,25.566,25.523,25.406,25.382,25.422,25.422,25.422,25.401,25.423,25.394,25.398,25.288,25.288,25.288,25.348,25.389,25.318,25.281,25.296,25.296,25.296,25.432,25.4,25.38,25.352,25.408,25.408,25.408,25.443,25.517,25.457,25.495,25.309,25.309,25.309,25.32,25.308,25.415,25.415,25.434,25.434,25.434,25.399,25.376,25.369,25.387,25.403,25.403,25.403,25.453,25.505,25.519,25.615,25.655,25.655,25.655,25.734,25.7,25.684,25.709,25.697,25.697,25.697,25.634,25.572,25.546,25.469,25.275,25.275,25.275,25.252,25.197,25.209,25.254,25.238,25.238,25.238,25.217,25.227,25.22,25.23,25.413,25.413,25.413,25.373,25.445,25.501,25.475,25.662,25.662,25.662,25.671,25.526,25.443,25.42,25.415,25.415,25.415,25.421,25.465,25.475,25.434,25.365,25.365,25.365,25.401,25.334,25.271,25.282,25.255,25.255,25.255,25.234,25.24,25.24,25.088,25.014,25.014,25.014,25.139,24.98,24.958,24.917,24.858,24.858,24.858,24.818,24.745,24.581,24.528,24.439,24.439,24.439,24.357,24.412,24.423,24.458,24.493,24.493,24.493,24.467,24.426,24.313,24.263,24.347,24.347,24.347,24.528,24.5,24.531,24.427,24.443,24.443,24.443,24.372,24.335,24.298,24.135,24.36,24.36,24.36,24.222,24.259,24.288,24.35,24.405,24.405,24.405,24.527,24.419,24.365,24.383,24.337,24.337,24.337,24.345,24.496,24.473,25.09,24.66,24.66,24.66,24.997,25.465,25.866,25.634,25.737,25.737,25.737,25.584,25.642,25.364,25.316,25.213,25.213,25.213,24.89,24.867,24.687,24.777,24.837,24.837,24.837,24.683,24.679,24.605,24.72,24.645,24.645,24.645,24.65,24.464,24.45,24.375,24.376,24.376,24.376,24.32,24.338,24.441,24.512,24.479,24.479,24.479,24.429,24.45,24.45,24.42,24.42,24.42,24.42,24.42,24.424,24.409,24.38,24.32,24.32,24.32,24.418,24.423,24.55,24.526,24.605,24.605,24.605,24.671,24.662,24.644,24.606,24.665,24.665,24.665,25.055,25.014,25.365,24.925,24.74,24.74,24.74,24.71,24.712,24.647,24.7,24.67,24.67,24.67,24.594,24.663,24.648,24.676,24.7,24.7,24.7,24.712,24.714,24.748,24.702,24.708,24.708,24.708,24.715,24.739,24.622,24.689,24.705,24.705,24.705,24.724,24.749,24.703,24.742,24.742,24.742,24.742,24.728,24.69,24.712,24.75,24.731,24.731,24.731,24.724,24.726,24.739,24.739,24.753,24.753,24.753,24.745,24.751,24.778,24.779,24.614,24.614,24.614,24.592,24.582,24.397,24.417,24.561,24.561,24.561,24.508,24.555,24.493,24.496,24.514,24.514,24.514,24.535,24.607,24.575,24.609,24.61,24.61,24.61,24.628,24.644,24.65,24.659,24.581,24.581,24.581,24.515,24.532,24.397,24.346,24.38,24.38,24.38,24.46,24.54,24.566,24.611,24.625,24.625,24.625,24.651,24.658,24.629,24.648,24.635,24.635,24.635,24.592,24.577,24.55,24.488,24.481,24.481,24.481,24.622,24.55,24.631,24.543,24.536,24.536,24.536,24.546,24.551,24.527,24.518,24.497,24.497,24.497,24.494,24.556,24.637,24.657,24.658,24.658,24.658,24.64,24.661,24.65,24.687,24.549,24.549,24.549,24.527,24.544,24.524,24.479,24.517,24.517,24.517,24.521,24.535,24.561,24.569,24.587,24.587,24.587,24.562,24.593,24.563,24.525,24.511,24.511,24.511,24.482,24.472,24.535,24.53,24.465,24.465,24.465,24.488,24.484,24.506,24.539,24.422,24.422,24.422,24.301,24.326,24.337,24.361,24.278,24.278,24.278,24.289,24.326,24.355,24.399,24.351,24.351,24.351,24.356,24.351,24.356,24.392,24.367,24.367,24.367,24.348,24.334,24.338,24.361,24.377,24.377,24.377,24.351,24.316,24.322,24.324,24.293,24.293,24.293,24.307,24.287,24.276,24.27,24.262,24.262,24.262,24.233,24.181,24.218,24.215,24.247,24.247,24.247,24.247,24.26,24.252,24.191,24.116,24.116,24.116,24.176,24.124,24.028,24.028,24.054,24.054,24.054,23.99,23.984,24.027,24.036,24.011,24.011,24.011,23.997,23.966,23.954,23.924,23.922,23.922,23.922,23.881,23.874,23.808,23.818,23.826,23.826,23.826,23.861,23.792,23.775,23.809,23.725,23.725,23.725,23.843,23.832,23.782,23.698,23.692,23.692,23.692,23.739,23.749,23.666,23.685,23.715,23.715,23.715,23.693,23.73,23.669,23.686,23.643,23.643,23.643,23.619,23.497,23.426,23.487,23.508,23.508,23.508,23.545,23.495,23.585,23.62
376,23.635,23.635,23.635,23.747,23.791,23.942,24.011,23.948,23.948,23.948,23.988,23.846,23.722,23.658,23.682,23.682,23.682,23.732,23.668,23.631,23.547,23.492,23.492,23.492,23.44,23.418,23.429,23.409,23.409,23.409,23.409,23.409,23.469,23.421,23.271,23.341,23.341,23.341,23.345,23.373,23.475,23.502,23.565,23.565,23.565,23.439,23.476,23.477,23.512,23.502,23.502,23.502,23.502,23.587,23.559,23.46,23.401,23.401,23.401,23.41,23.363,23.419,23.488,23.606,23.606,23.606,23.574,23.675,23.632,23.683,23.764,23.764,23.764,23.676,23.681,23.671,23.645,23.648,23.648,23.648,23.693,23.709,23.741,23.687,23.657,23.657,23.657,23.573,23.52,23.625,23.625,23.666,23.666,23.666,23.759,23.813,23.796,23.751,23.817,23.817,23.817,23.778,23.756,23.763,23.683,23.654,23.654,23.654,23.649,23.609,23.683,23.686,23.742,23.742,23.742,23.711,23.695,23.775,23.839,23.945,23.945,23.945,23.843,23.855,23.828,23.752,23.777,23.777,23.777,23.757,23.818,23.927,23.961,24.062,24.062,24.062,24.139,24.047,24.05,24.032,24.028,24.028,24.028,23.9,23.951,23.929,24.034,24.26,24.26,24.26,24.224,24.24,24.293,24.223,24.103,24.103,24.103,24.038,24.138,24.06,24.078,24.037,24.037,24.037,24.02,24.0,24.132,24.13,24.117,24.117,24.117,24.138,24.163,24.107,24.072,24.118,24.118,24.118,24.108,24.161,24.217,24.37,24.452,24.452,24.452,24.508,24.605,24.487,24.468,24.496,24.496,24.496,24.406,24.432,24.388,24.437,24.36,24.36,24.36,24.377,24.411,24.47,24.378,24.339,24.339,24.339,24.46,24.494,24.428,24.416,24.423,24.423,24.423,24.477,24.566,24.563,24.55,24.67,24.67,24.67,24.664,24.603,24.68,24.674,24.704,24.704,24.704,24.645,24.659,24.693,24.714,24.658,24.658,24.658,24.557,24.558,24.677,24.634,24.414,24.414,24.414,24.443,24.622,24.666,24.555,24.525,24.525,24.525,24.581,24.551,24.506,24.446,24.461,24.461,24.461,24.541,24.509,24.48,24.367,24.406,24.406,24.406,24.364,24.365,24.26,24.292,24.331,24.331,24.331,24.394,24.369,24.335,24.356,24.354,24.354,24.354,24.367,24.42,24.476,24.408,24.477,24.477,24.477,24.543,24.525,24.538,24.481,24.589,24.589,24.589,24.589,24.589,24.673,24.714,24.724,24.724,24.724,24.724,24.687,24.675,24.652,24.616,24.616,24.616,24.488,24.594,24.562,24.659,24.689,24.689,24.689,24.714,24.71,24.755,24.734,24.813,24.813,24.813,24.758,24.824,24.786,24.756,24.748,24.748,24.748,24.806,24.831,24.891,24.89,24.875,24.875,24.875,25.006,24.925,24.984,24.954,25.172,25.172,25.172,25.217,25.285,25.348,25.416,25.46,25.46,25.46,25.455,25.429,25.38,25.309,25.354,25.354,25.354,25.367,25.332,25.352,25.363,25.333,25.333,25.333,25.356,25.353,25.359,25.359,25.308,25.308,25.308,25.322,25.272,25.273,25.198,25.166,25.166,25.166,25.2,25.268,25.289,25.243,25.371,25.371,25.371,25.266,25.277,25.319,25.305,25.305,25.305,25.305,25.305,25.361,25.352,25.322,25.286,25.286,25.286,25.354,25.38,25.368,25.392,25.337,25.337,25.337,25.324,25.21,25.228,25.254,25.267,25.267,25.267,25.275,25.256,25.242,25.152,25.164,25.164,25.164,25.175,25.14,25.14,25.09,25.025,25.025,25.025,24.995,25.014,25.036,24.935,24.936,24.936,24.936,24.779,24.75,24.766,24.709,24.722,24.722,24.722,24.748,24.682,24.748,24.738,24.736,24.736,24.736,24.733,24.655,24.731,24.77,24.709,24.709,24.709,24.724,24.755,24.654,24.632,24.577,24.577,24.577,24.636,24.706,24.686,24.699,24.736,24.736,24.736,24.699,24.767,24.91,24.898,24.949,24.949,24.949,24.893,24.819,24.877,24.933,25.025,25.025,25.025,25.048,25.185,25.194,25.143,25.202,25.202,25.202,25.162,25.231,25.379,25.361,25.341,25.341,25.341,25.422,25.352,25.29,25.264,25.261,25.261,25.261,25.231,25.362,25.425,25.389,25.37,25.37,25.37,25.371,25.429,25.457,25.454,25.271,25.271,25.271,25.314,25.286,25.27,25.273,25.234,25.234,25.234,25.178,25.203,25.197,25.2,25.239,25.239,25.239,25.181,25.158,25.11,25.092,25.078,25.078,25.078,25.034,25.034,25.053,25.043,25.031,25.031,25.031,25.045,25.071,25.086,25.027,25.034,25.034,25.034,25.051,25.058,25.088,25.122,25.147,25.147,25.147,25.128,25.125,25.07,25.081,25.078,25.078,25.078,25.098,25.155,25.123,25.159,25.158,25.158,25.158,25.184,25.272,25.316,25.357,25.347,25.347,25.347,25.346,25.313,25.348,25.317,25.293,25.293,25.293,25.263,25.242,25.296,25.256,25.229,25.229,25.229,25.268,25.252,25.235,25.23,25.25,25.25,25.25,25.348,25.372,25.377,25.327,25.33,25.33,25.33,25.288,25.343,25.339,25.262,25.208,25.208,25.208,25.339,25.39,25.295,25.305,25.286,25.286,25.286,25.282,25.284,25.286,25.346,25.358,25.358,25.358,25.295,25.294,25.267,25.271,25.256,25.256,25.256,25.266,25.224,25.196,25.146,25.11,25.11,25.11,25.093,25.098,25.117,25.065,25.019,25.019,25.019,25.036,25.064,25.127,25.127,25.121,25.121,25.121,25.165,25.135,25.135,25.135,25.201,25.201,25.201,25.226,25.185,25.185,25.177,25.166,25.166,25.166,25.154,25.137,25.15,25.116,25.09,25.09,25.09,25.23,25.294,25.218,25.231,25.265,25.265,25.265,25.209,25.178,25.139,25.14,25.097,25.097,25.097,25.102,25.091,25.122,25.121,25.166,25.166,25.166,25.254,25.172,25.136,25.139,25.108,25.108,25.108,25.073,25.101,25.075,25.057,25.043,25.043,25.043,25.056,25.098,25.101,25.073,25.082,25.082,25.082,25.062,24.939,24.948,24.987,25.029,25.029,25.029,25.097,25.025,25.048,25.047,25.016,25.016,25.016,24.957,24.963,25.013,25.093,25.014,25.014,25.014,25.035,25.038,24.994,25.013,24.981,24.981,24.981,24.97,24.884,24.934,24.982,24.96,24.96,24.96,24.962,24.954,24.963,24.972,25.142,25.142,25.142,25.195,25.139,25.168,25.11,25.147,25.147,25.147,25.108,25.064,24.995,25.009,25.009,25.009,25.009,25.009,25.081,25.015,24.977,24.929,24.929,24.929,24.969,24.922,24.92,24.92,24.922,24.922,24.922,24.905,24.96,24.924,24.924,24.946,24.946,24.946,24.98,24.948,24.928,24.922,24.936,24.936,24.936,24.907,24.92,24.867,24.942,24.915,24.915,24.915,24.864,24.907,24.922,24.923,24.918,24.918,24.918,24.899,24.896,24.808,24.812,24.757,24.757,24.757,24.793,24.771,24.784,24.787,24.831,24.831,24.831,24.782,24.8,24.805,24.82,24.83,24.83,24.83,24.862,24.794,24.78,24.785,24.748,24.748,24.748,24.746,24.68,24.663,24.646,24.655,24.655,24.655,24.625,24.648,24.629,24.639,24.667,24.667,24.667,24.648,24.67,24.647,24.639,24.623,24.623,24.623,24.618,24.619,24.616,24.553,24.539,24.539,24.539,24.6,24.609,24.562,24.571,24.588,24.588,24.588,24.582,24.61,24.566,24.523,24.446,24.446,24.446,24.483,24.469,24.471,24.47,24.482,24.482,24.482,24.471,24.457,24.491,24.53,24.565,24.565,24.565,24.544,24.544,24.542,24.557,24.458,24.458,24.458,24.433,24.485,24.446,24.452,24.421,24.421,24.421,24.366,24.326,24.388,24.385,24.318,24.318,24.318,24.327,24.326,24.324,24.301,24.292,24.292,24.292,24.255,24.242,24.29,24.289,24.326,24.326,24.326,24.302,24.335,24.275,24.248,24.277,24.277,24.277,24.308,24.318,24.369,24.326,24.299,24.299,24.299,24.358,24.324,24.28,24.294,24.308,24.308,24.308,24.29,24.314,24.308,24.308,24.336,24.336,24.336,24.323,24.348,24.338,24.378,24.327,24.327,24.327,24.339,24.371,24.365,24.334,24.333,24.333,24.333,24.282,24.288,24.229,24.191,24.198,24.198,24.198,24.169,24.188,24.159,24.179,24.235,24.235,24.235,24.169,24.133,24.168,24.174,24.177,24.177,24.177,24.181,24.161,24.125,24.157,24.21,24.21,24.21,24.268,24.277,24.245,24.22,24.239,24.239,24.239,24.299,24.317,24.346,24.392,24.323,24.323,24.323,24.32,24.321,24.271,24.271,24.271,24.271,24.271,24.281,24.255,24.237,24.237,24.177,24.177,24.177,24.195,24.195,24.29,24.262,24.337,24.337,24.337,24.288,24.253,24.262,24.278,24.286,24.286,24.286,24.29,24.333,24.356,24.31,24.257,24.257,24.257,24.251,24.267,24.294,24.335,24.325,24.325,24.325,24.305,24.312,24.357,24.3
371,24.24,24.24,24.24,24.217,24.257,24.245,24.245,24.263,24.263,24.263,24.259,24.276,24.256,24.245,24.241,24.241,24.241,24.223,24.225,24.226,24.246,24.244,24.244,24.244,24.274,24.378,24.36,24.396,24.419,24.419,24.419,24.399,24.38,24.377,24.406,24.437,24.437,24.437,24.436,24.437,24.467,24.517,24.505,24.505,24.505,24.438,24.477,24.437,24.478,24.542,24.542,24.542,24.555,24.514,24.516,24.54,24.54,24.54,24.54,24.54,24.531,24.38,24.401,24.365,24.365,24.365,24.378,24.349,24.36,24.349,24.317,24.317,24.317,24.289,24.301,24.347,24.353,24.373,24.373,24.373,24.357,24.372,24.382,24.366,24.366,24.366,24.366,24.393,24.38,24.34,24.306,24.304,24.304,24.304,24.333,24.324,24.347,24.302,24.333,24.333,24.333,24.321,24.297,24.322,24.292,24.289,24.289,24.289,24.259,24.257,24.284,24.293,24.282,24.282,24.282,24.285,24.243,24.189,24.218,24.218];
38window.FX_YEAR_AVG = {"2006":27.485,"2007":27.766,"2008":24.946,"2009":26.435,"2010":25.284,"2011":24.59,"2012":25.149,"2013":25.98,"2014":27.536,"2015":27.279,"2016":27.034,"2017":26.326,"2018":25.647,"2019":25.67,"2020":26.455,"2021":25.64,"2022":24.566,"2023":24.004,"2024":25.12,"2025":24.688,"2026":24.332};
39</script>
39
40<style>
41@import url('https://fonts.googleapis.com/css2?family=Open+Sans:wght@400;600;700;800&family=Onest:wght@600;700;800&family=Inter:wght@400;500;700&display=swap');
42:root {
43  color-scheme: light;
44  /* Light theme */
45  --bg: #fafafa;
46  --bg-card: #ffffff;
47  --bg-subtle: #f4f4f5;
48  --bg-hover: #f4f4f5;
49  --bg-banner-info: #fef2f2;
50  --bg-banner-info-border: #fecaca;
51  --bg-banner-success: #f0fdf4;
52  --bg-banner-success-border: #bbf7d0;
53  --bg-banner-error: #fef2f2;
54  --bg-banner-error-border: #fecaca;
55  --text: #18181b;
56  --text-strong: #09090b;
57  --text-muted: #71717a;
58  --text-subtle: #a1a1aa;
59  --border: #e4e4e7;
60  --border-strong: #d4d4d8;
61  --accent: #dc2626;        /* primary: red for elektřina */
62  --accent-strong: #b91c1c;
63  --accent-bg: #fef2f2;
64  --brand: #00CA85; --brand-strong: #00A26A; --brand-bg: #E1F7EE;  /* PKV brand chrome */
65  --gas: #f59e0b;           /* gold for plyn */
66  --gas-strong: #d97706;
67  --shadow-sm: 0 1px 2px 0 rgba(0,0,0,0.04);
68  --shadow: 0 1px 3px 0 rgba(0,0,0,0.06), 0 1px 2px -1px rgba(0,0,0,0.05);
69  --shadow-lg: 0 10px 15px -3px rgba(0,0,0,0.08), 0 4px 6px -4px rgba(0,0,0,0.05);
70  --delta-up: #b91c1c;
71  --delta-down: #00ca85;  /* PKV zelená */
72  --gauge-low: #dc2626;
73  --gauge-mid: #f59e0b;
74  --gauge-high: #00ca85;  /* PKV zelená */
75}
76[data-theme="dark"] {
77  color-scheme: dark;
78  --bg: #171717;
79  --bg-card: #1E1E1E;
80  --bg-subtle: #2A2A2A;
81  --bg-hover: #2A2A2A;
82  --bg-banner-info: #1f1818;
83  --bg-banner-info-border: #7f1d1d;
84  --bg-banner-success: #14201a;
85  --bg-banner-success-border: #166534;
86  --bg-banner-error: #1f1818;
87  --bg-banner-error-border: #7f1d1d;
88  --text: #e4e4e7;
89  --text-strong: #fafafa;
90  --text-muted: #a1a1aa;
91  --text-subtle: #71717a;
92  --border: #333333;
93  --border-strong: #3f3f46;
94  --accent: #ef4444;
95  --accent-strong: #f87171;
96  --accent-bg: #2a1414;
97  --brand: #00CA85; --brand-strong: #4FE0B0; --brand-bg: #14251E;
98  --gas: #fbbf24;
99  --gas-strong: #fcd34d;
100  --shadow-sm: 0 1px 2px 0 rgba(0,0,0,0.3);
101  --shadow: 0 1px 3px 0 rgba(0,0,0,0.35), 0 1px 2px -1px rgba(0,0,0,0.3);
102  --shadow-lg: 0 10px 15px -3px rgba(0,0,0,0.4), 0 4px 6px -4px rgba(0,0,0,0.3);
103  --delta-up: #f87171;
104  --delta-down: #34d399;
105}
106* { box-sizing: border-box; }
107html, body { background: var(--bg); }
108table { font-family: 'Inter', system-ui, sans-serif; }
109.big .value, .gauge-info .pct { font-family: 'Onest', 'Open Sans', sans-serif; }
110body {
111  font-family: 'Open Sans', 'Inter', ui-sans-serif, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
112  color: var(--text);
113  margin: 0;
114  padding: 28px 24px 80px;
115  line-height: 1.55;
116  font-feature-settings: 'cv02', 'cv03', 'cv04', 'cv11';
117  -webkit-font-smoothing: antialiased;
118  transition: background-color 0.2s, color 0.2s;
119}
120.container { max-width: 1800px; margin: 0 auto; }
121header {
122  display: flex; align-items: center; justify-content: space-between; gap: 20px;
123  margin-bottom: 32px;
124}
125.brand { display: flex; flex-direction: column; gap: 4px; }
126h1 {
127  font-size: 26px; margin: 0; color: var(--text-strong); font-weight: 700;
128  letter-spacing: -0.02em;
129}
130.subtitle { color: var(--text-muted); font-size: 14px; }
131.header-actions { display: flex; gap: 10px; align-items: center; }
132.theme-switch { display: inline-flex; align-items: center; gap: 3px; height: 40px; box-sizing: border-box; background: var(--bg-subtle); border: 1px solid var(--border); border-radius: 10px; padding: 3px; }
133.theme-switch button { border: 0; background: transparent; color: var(--text-muted); cursor: pointer; width: 30px; height: 100%; border-radius: 7px; display: inline-flex; align-items: center; justify-content: center; line-height: 0; transition: background .15s ease, color .15s ease; }
134.theme-switch button svg { width: 16px; height: 16px; display: block; }
135.theme-switch button:not(.active):hover { color: var(--text); }
136.theme-switch button.active { background: var(--bg-card); color: var(--text-strong); box-shadow: 0 1px 2px rgba(0,0,0,.08); }
137[data-theme="dark"] .theme-switch button.active { background: rgba(255,255,255,.12); color: #fff; }
138button.icon-btn {
139  background: var(--bg-card); color: var(--text); border: 1px solid var(--border);
140  width: 40px; height: 40px; padding: 0; border-radius: 10px;
141  cursor: pointer; display: flex; align-items: center; justify-content: center;
142  transition: all 0.15s; font-size: 18px;
143}
144button.icon-btn:hover { background: var(--bg-hover); border-color: var(--border-strong); }
145button.refresh {
146  background: var(--brand); color: #fff; border: 0; padding: 0 18px; height: 40px;
147  border-radius: 10px; font-size: 14px; font-weight: 600; cursor: pointer;
148  white-space: nowrap; display: inline-flex; align-items: center; gap: 6px;
149  transition: all 0.15s;
150  box-shadow: var(--shadow-sm);
151}
152button.refresh:hover { background: var(--brand-strong); transform: translateY(-1px); box-shadow: var(--shadow); }
153button.refresh:active { transform: translateY(0); }
154button.refresh:disabled { background: var(--text-subtle); cursor: progress; transform: none; }
155.status-banner {
156  margin: -16px 0 24px; padding: 12px 16px; border-radius: 10px;
157  font-size: 13px; display: none; border: 1px solid;
158}
159.status-banner.info { background: var(--bg-banner-info); color: var(--accent-strong); border-color: var(--bg-banner-info-border); display: block; }
160.status-banner.success { background: var(--bg-banner-success); color: var(--delta-down); border-color: var(--bg-banner-success-border); display: block; }
161.status-banner.error { background: var(--bg-banner-error); color: var(--accent-strong); border-color: var(--bg-banner-error-border); display: block; }
162.freshness {
163  display: inline-block; font-size: 11px; font-weight: 500; padding: 2px 8px;
164  margin-left: 8px; border-radius: 999px; vertical-align: middle;
165  background: var(--bg-banner-info); color: var(--text-subtle); border: 1px solid var(--border);
166}
167.freshness.fresh { background: var(--bg-banner-success); color: var(--delta-down); border-color: var(--bg-banner-success-border); }
168.freshness.aging { background: #fef3c7; color: #92400e; border-color: #fcd34d; }
169.freshness.stale { background: var(--bg-banner-error); color: var(--accent-strong); border-color: var(--bg-banner-error-border); }
170[data-theme="dark"] .freshness.aging { background: #422006; color: #fcd34d; border-color: #78350f; }
171section {
172  background: var(--bg-card); border: 1px solid var(--border); border-radius: 14px;
173  padding: 22px 24px; margin-bottom: 18px;
174  box-shadow: var(--shadow-sm);
175}
176section h2 {
177  margin: 0 0 16px; font-size: 15px; color: var(--text-strong);
178  font-weight: 600; letter-spacing: 0.04em; text-transform: uppercase;
179}
180section h2::before { content:''; display:inline-block; width:4px; height:0.8em; background:var(--brand); border-radius:2px; margin-right:9px; vertical-align:-1px; }
181/* Vlastní legenda grafů 3A/3B — elektřina a plyn na samostatných řádcích */
182.price-legend { display: flex; flex-direction: column; gap: 5px; margin: 2px 0 12px; }
183.price-legend-row { display: flex; flex-wrap: wrap; gap: 16px; align-items: center; }
184.price-legend .pl-item {
185  display: inline-flex; align-items: center; gap: 6px; cursor: pointer;
186  font-size: 12px; font-weight: 500; color: var(--text-muted); user-select: none;
187}
188.price-legend .pl-item.pl-hidden { opacity: 0.35; }
189.price-legend .pl-swatch { width: 14px; height: 4px; border-radius: 2px; display: inline-block; flex: 0 0 auto; }
190table { width: 100%; border-collapse: collapse; font-size: 14px; }
191th, td { padding: 10px 12px; text-align: left; border-bottom: 1px solid var(--border); vertical-align: middle; }
192th {
193  background: transparent; font-weight: 500; color: var(--text-muted);
194  font-size: 11px; letter-spacing: 0.04em; text-transform: uppercase;
195  padding-bottom: 8px; padding-top: 4px;
196}
197tbody tr:last-child td { border-bottom: 0; }
198tbody tr:hover td { background: var(--bg-subtle); }
199td.num { text-align: right; font-variant-numeric: tabular-nums; font-weight: 500; color: var(--text-strong); }
200.delta-up, .delta-down {
201  font-weight: 700; padding: 3px 10px; border-radius: 6px;
202  display: inline-block; min-width: 140px; text-align: right;
203  font-variant-numeric: tabular-nums; white-space: nowrap;
204  box-sizing: border-box; vertical-align: middle;
205}
206.delta-up { color: var(--delta-up); background: rgba(185, 28, 28, 0.12); }
207.delta-down { color: var(--delta-down); background: rgba(0, 202, 133, 0.14); }
208[data-theme="dark"] .delta-up { background: rgba(248, 113, 113, 0.18); }
209[data-theme="dark"] .delta-down { background: rgba(52, 211, 153, 0.18); }
210/* Velké číslo v sekci 1/2 nemá pill background, ani min-width */
211.value.delta-up, .value.delta-down { background: none; padding: 0; min-width: 0; display: inline; text-align: left; }
212/* Mini-badge u velkého čísla je inline-flex bez ovlivnění layoutu */
213.value .delta-up, .value .delta-down { min-width: 0; padding: 2px 8px; }
214.delta-flat { color: var(--text-muted); }
215.muted { color: var(--text-muted); font-style: italic; font-weight: 400; }
216.big {
217  display: flex; gap: 28px; flex-wrap: wrap; align-items: baseline; margin-bottom: 14px;
218}
219.big > div { display: flex; flex-direction: column; gap: 2px; }
220.big .value {
221  font-size: 32px; font-weight: 700; color: var(--text-strong);
222  font-variant-numeric: tabular-nums; letter-spacing: -0.02em;
223}
224.big .label { color: var(--text-muted); font-size: 12px; }
225.chart-wrap { position: relative; width: 100%; height: 320px; }
226.chart-wrap-tall { position: relative; width: 100%; height: 400px; }
227.gauge-wrap { display: flex; align-items: center; gap: 28px; flex-wrap: wrap; }
228.gauge-wrap canvas { max-width: 220px; }
229.gauge-info { font-size: 14px; color: var(--text-muted); }
230.gauge-info .pct { font-size: 36px; font-weight: 700; color: var(--text-strong); font-variant-numeric: tabular-nums; }
231.range-controls {
232  display: flex; gap: 4px; flex-wrap: wrap; margin: 10px 0 16px; align-items: center;
233}
234.range-controls .label-small { font-size: 12px; color: var(--text-muted); margin-right: 8px; }
235.range-controls button {
236  background: var(--bg-subtle); color: var(--text-muted); border: 1px solid transparent;
237  padding: 6px 12px; border-radius: 7px; font-size: 12px; cursor: pointer;
238  font-weight: 500; transition: all 0.12s;
239}
240.range-controls button:hover { background: var(--bg-hover); color: var(--text-strong); }
241.range-controls button.active {
242  background: var(--brand); color: #fff; border-color: var(--brand);
243}
244.range-controls button[data-dot] { display: inline-flex; align-items: center; gap: 6px; }
245.range-controls button[data-dot]::before {
246  content: ''; display: inline-block; width: 8px; height: 8px; border-radius: 50%;
247  background: var(--dot-color, currentColor);
248}
249.range-controls button[data-dot]:not(.active) { opacity: 0.55; }
250.range-controls button[data-dot]:not(.active):hover { opacity: 0.85; }
251.range-controls select { font: inherit; font-size: 13px; padding: 6px 8px; border-radius: 8px; border: 1px solid var(--border); background: var(--bg-card); color: var(--text); cursor: pointer; }
252.range-controls select:hover { border-color: var(--border-strong); }
253.date-pickers { margin-left: auto; display: flex; align-items: center; gap: 8px; font-size: 12px; color: var(--text-muted); }
254.date-pickers input[type="date"] {
255  border: 1px solid var(--border); border-radius: 7px; padding: 6px 8px; font-size: 12px;
256  background: var(--bg-card); color: var(--text);
257  font-family: inherit; box-sizing: border-box; min-width: 124px; height: 30px;
258  color-scheme: light dark; line-height: 1;
259}
260/* Sjednocení: prázdné i vyplněné okno mají STEJNOU barvu textu (číslice i lomítka).
261   Chrome u prázdných polí používá -webkit-text-fill-color pro placeholder → nutno přepsat. */
262.date-pickers input[type="date"]::-webkit-datetime-edit,
263.date-pickers input[type="date"]::-webkit-datetime-edit-fields-wrapper,
264.date-pickers input[type="date"]::-webkit-datetime-edit-text,
265.date-pickers input[type="date"]::-webkit-datetime-edit-month-field,
266.date-pickers input[type="date"]::-webkit-datetime-edit-day-field,
267.date-pickers input[type="date"]::-webkit-datetime-edit-year-field {
268  color: var(--text) !important;
269  -webkit-text-fill-color: var(--text) !important;
270  opacity: 1 !important;
271}
272.date-pickers input[type="date"]::-webkit-calendar-picker-indicator {
273  filter: var(--theme-filter, none);
274}
275[data-theme="dark"] .date-pickers input[type="date"]::-webkit-calendar-picker-indicator { filter: invert(0.8); }
276footer {
277  margin-top: 36px; padding-top: 24px; border-top: 1px solid var(--border);
278  font-size: 12px; color: var(--text-muted);
279}
280footer a { color: var(--text); text-decoration: none; border-bottom: 1px dotted var(--border-strong); }
281footer a:hover { color: var(--brand); border-bottom-color: var(--brand); }
282.sources { margin-top: 8px; }
283.sources a { margin-right: 14px; display: inline-block; margin-bottom: 4px; }
284.note { font-size: 12px; color: var(--text-muted); margin-top: 10px; line-height: 1.5; }
285/* Supabase loading overlay */
286/* ── Přihlašovací brána ─────────────────────────────────────────────── */
287#auth-gate { position: fixed; inset: 0; z-index: 10000; display: flex; align-items: center; justify-content: center;
288  background: var(--bg, #fff); padding: 20px; }
289#auth-gate[hidden] { display: none; }
290#auth-gate .auth-card { width: 100%; max-width: 380px; background: var(--bg-card); border: 1px solid var(--border);
291  border-radius: 14px; padding: 28px 26px; box-shadow: 0 10px 40px rgba(0,0,0,.18); }
292#auth-gate .auth-brand { font-size: 22px; font-weight: 700; color: var(--text-strong); letter-spacing: -.02em; }
293#auth-gate .auth-sub { font-size: 13px; color: var(--text-muted); margin-top: 4px; margin-bottom: 22px; }
294#auth-gate label { display: block; font-size: 12px; font-weight: 600; color: var(--text-muted);
295  margin: 14px 0 5px; text-transform: uppercase; letter-spacing: .04em; }
296#auth-gate input { width: 100%; padding: 10px 12px; border-radius: 8px; border: 1px solid var(--border);
297  background: var(--bg); color: var(--text-strong); font-size: 14px; box-sizing: border-box; }
298#auth-gate input:focus { outline: 2px solid var(--brand); outline-offset: 1px; }
299#auth-gate button { width: 100%; margin-top: 20px; padding: 11px; border-radius: 8px; border: none;
300  background: var(--brand); color: #fff; font-size: 14px; font-weight: 600; cursor: pointer; }
301#auth-gate button[disabled] { opacity: .6; cursor: default; }
302#auth-gate .auth-oddelovac { display:flex; align-items:center; gap:10px; margin:18px 0 2px; color:var(--text-muted); font-size:11px; text-transform:uppercase; letter-spacing:.05em; }
303#auth-gate .auth-oddelovac::before, #auth-gate .auth-oddelovac::after { content:''; flex:1; height:1px; background:var(--border); }
304#auth-gate .auth-err { margin-top: 14px; padding: 9px 11px; border-radius: 8px; font-size: 13px;
305  background: #fee2e2; color: #991b1b; }
306[data-theme="dark"] #auth-gate .auth-err { background: #450a0a; color: #fca5a5; }
307.auth-who { display: flex; align-items: center; gap: 8px; font-size: 12px; color: var(--text-muted); }
308.auth-who a, .auth-who button { font-size: 12px; color: var(--text-muted); background: none; border: none;
309  cursor: pointer; text-decoration: underline; padding: 0; font-family: inherit; }
310#supa-loading { position: fixed; inset: 0; background: var(--bg, #fff); display: flex; align-items: center; justify-content: center; z-index: 9999; transition: opacity 0.3s; }
311#supa-loading.hidden { opacity: 0; pointer-events: none; }
312#supa-loading .spinner { width: 48px; height: 48px; border: 4px solid var(--border); border-top-color: var(--brand); border-radius: 50%; animation: spin 0.8s linear infinite; }
313#supa-loading .msg { margin-left: 16px; color: var(--text-muted); font-size: 14px; }
314@keyframes spin { to { transform: rotate(360deg); } }
315/* News section */
316.news-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 12px; margin-top: 14px; }
317@media (max-width: 700px) { .news-grid { grid-template-columns: 1fr; } }
318.news-card { background: var(--bg-card); border: 1px solid var(--border); border-radius: 10px; padding: 14px 16px; transition: border-color 0.2s, box-shadow 0.2s; }
319.news-card:hover { border-color: var(--brand); box-shadow: var(--shadow-sm); }
320.news-tag { display: inline-block; font-size: 10px; font-weight: 700; letter-spacing: 0.04em; text-transform: uppercase; padding: 2px 8px; border-radius: 999px; background: var(--bg-banner-info); color: var(--accent-strong); margin-bottom: 8px; }
321.news-tag.cz { background: rgba(220, 38, 38, 0.12); color: var(--accent-strong); }
322.news-tag.eu { background: rgba(59, 130, 246, 0.14); color: #1d4ed8; }
323.news-tag.gas { background: rgba(245, 158, 11, 0.14); color: #b45309; }
324.news-tag.oil { background: rgba(124, 58, 237, 0.14); color: #5b21b6; }
325.news-tag.nuclear { background: rgba(16, 185, 129, 0.14); color: #047857; }
326[data-theme="dark"] .news-tag.eu { color: #93c5fd; }
327[data-theme="dark"] .news-tag.gas { color: #fcd34d; }
328[data-theme="dark"] .news-tag.oil { color: #c4b5fd; }
329[data-theme="dark"] .news-tag.nuclear { color: #6ee7b7; }
330.news-card h3 { font-size: 14.5px; font-weight: 600; margin: 0 0 6px; line-height: 1.35; color: var(--text-strong); }
331.news-card .news-summary { font-size: 13px; color: var(--text-muted); line-height: 1.5; margin-bottom: 10px; }
332.news-card .news-meta { font-size: 11px; color: var(--text-subtle); display: flex; gap: 8px; align-items: center; }
333.news-card .news-meta a { color: var(--brand-strong); text-decoration: none; }
334.news-card .news-meta a:hover { text-decoration: underline; }
335.data-disclaimer {
336  background: var(--bg-subtle); border: 1px solid var(--border); color: var(--text-muted);
337  padding: 10px 14px; border-radius: 8px; font-size: 12px; margin-top: 10px;
338  line-height: 1.55;
339}
340/* KPI tiles row */
341.kpi-row {
342  display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
343  gap: 12px; margin: 16px 0 4px;
344}
345.kpi {
346  padding: 14px 16px; border-radius: 10px; background: var(--bg-subtle);
347  display: flex; flex-direction: column; gap: 2px;
348}
349.kpi-label { font-size: 11px; color: var(--text-muted); text-transform: uppercase; letter-spacing: 0.04em; }
350.kpi-value { font-size: 22px; font-weight: 700; color: var(--text-strong); font-variant-numeric: tabular-nums; letter-spacing: -0.01em; }
351.kpi-delta { font-size: 12px; font-weight: 500; }
352@media (max-width: 640px) {
353  header { flex-direction: column; align-items: flex-start; }
354  body { padding: 20px 16px 60px; }
355  section { padding: 18px 16px; }
356  .big .value { font-size: 26px; }
357  h1 { font-size: 22px; }
358}
359</style>
360</head>
361<body>
362<div class="container">
363
364<header>
365  <div style="display:flex;align-items:center;gap:13px">
366  <svg viewBox="0 0 80 73" width="44" height="40" style="flex:none;display:block" xmlns="http://www.w3.org/2000/svg"><path d="M14.59 57.58S-16.82 45.12 12.53 28.31L55.75 3.56S80.89 -10.84 79.98 18.27L79.02 48.74S77.95 82.71 46.54 70.25Z" fill="#00CA85"></path><path d="M33.8 38.37C33.8 37.05 33.22 35.79 32.5 34.89C31.63 34.16 30.56 33.73 29.38 33.62C28.2 33.73 27.13 34.16 26.26 34.89C25.55 35.79 24.97 37.05 24.97 38.37C24.97 39.72 25.55 40.9 26.26 41.81C27.13 42.54 28.2 43 29.38 43.12C30.55 43 31.63 42.54 32.5 41.81C33.22 40.9 33.8 39.68 33.8 38.37ZM24.94 53.92H20.49V29.79H21.55L23.32 31.89C24.63 30.82 26.63 29.7 29.38 29.15C31.75 29.38 33.89 30.26 35.64 31.68C37.08 33.46 38.24 36.02 38.24 38.37C38.24 41.1 37.08 43.65 35.64 45.16C33.9 46.57 31.75 47.63 29.38 47.63C27.46 47.3 25.98 46.65 24.94 46.08V53.91Z" fill="#fff"></path><path d="M45.13 47.65H40.68V23.23H45.13V38.63L51.73 30.22H56.82L51.07 37.49L56.82 47.65H51.73L48.21 41.31L45.13 45.41V47.65Z" fill="#fff"></path><path d="M63.23 47.65L56.64 30.22H61.65L65.45 41.05L69.21 30.21L74.3 30.22L67.64 47.65H63.23Z" fill="#fff"></path></svg>
367  <div class="brand">
368    <h1>Energoreport</h1>
369    <div class="subtitle" id="report-date">úterý 19. května 2026</div>
370    <div id="data-freshness-header" style="margin-top:6px;font-size:13px;display:flex;align-items:center;gap:8px;flex-wrap:wrap;"></div>
371  </div>
372  </div>
373  <div class="header-actions">
374    <div class="theme-switch" role="group" aria-label="Motiv zobrazení">
375      <button type="button" data-mode="light" onclick="setThemeMode('light')" title="Světlý režim" aria-label="Světlý režim"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="4"/><path d="M12 2v2M12 20v2M4.93 4.93l1.41 1.41M17.66 17.66l1.41 1.41M2 12h2M20 12h2M6.34 17.66l-1.41 1.41M19.07 4.93l-1.41 1.41"/></svg></button>
376      <button type="button" data-mode="system" onclick="setThemeMode('system')" title="Podle systému" aria-label="Podle systému"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="3" width="20" height="14" rx="2"/><path d="M8 21h8M12 17v4"/></svg></button>
377      <button type="button" data-mode="dark" onclick="setThemeMode('dark')" title="Tmavý režim" aria-label="Tmavý režim"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 3a6 6 0 0 0 9 9 9 9 0 1 1-9-9Z"/></svg></button>
378    </div>
379    <button class="refresh" id="refresh-btn" onclick="refreshData()">
380      <span>↻</span>
381      <span>Aktualizovat data</span>
382    </button>
383  </div>
384</header>
385<nav class="page-nav" style="display:flex;gap:10px;flex-wrap:wrap;margin:4px 0 24px"><a href="index.html" style="background:var(--brand,#00ca85);color:#fff;border:1px solid var(--brand,#00ca85);text-decoration:none;display:inline-flex;align-items:center;gap:7px;padding:8px 15px;border-radius:9px;font-weight:600;font-size:14px;white-space:nowrap"><svg width="17" height="17" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/><polyline points="9 22 9 12 15 12 15 22"/></svg>Přehled</a><a href="elektrina.html" style="background:var(--bg-card,#fff);color:var(--text,#3f3f46);border:1px solid var(--border,#e4e4e7);text-decoration:none;display:inline-flex;align-items:center;gap:7px;padding:8px 15px;border-radius:9px;font-weight:600;font-size:14px;white-space:nowrap"><svg width="17" height="17" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2"/></svg>Elektřina</a><a href="teplo.html" style="background:var(--bg-card,#fff);color:var(--text,#3f3f46);border:1px solid var(--border,#e4e4e7);text-decoration:none;display:inline-flex;align-items:center;gap:7px;padding:8px 15px;border-radius:9px;font-weight:600;font-size:14px;white-space:nowrap"><svg width="17" height="17" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M2 12h10"/><path d="M9 4v16"/><path d="m3 9 3 3-3 3"/><path d="M12 6 9 9 6 6"/><path d="m6 18 3-3 1.5 1.5"/><path d="M20 4v10.54a4 4 0 1 1-4 0V4a2 2 0 0 1 4 0Z"/></svg>Teplo</a><a href="plyn.html" style="background:var(--bg-card,#fff);color:var(--text,#3f3f46);border:1px solid var(--border,#e4e4e7);text-decoration:none;display:inline-flex;align-items:center;gap:7px;padding:8px 15px;border-radius:9px;font-weight:600;font-size:14px;white-space:nowrap"><svg width="17" height="17" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M8.5 14.5A2.5 2.5 0 0 0 11 12c0-1.38-.5-2-1-3-1.072-2.143-.224-4.054 2-6 .5 2.5 2 4.9 4 6.5 2 1.6 3 3.5 3 5.5a7 7 0 1 1-14 0c0-1.153.433-2.294 1-3a2.5 2.5 0 0 0 2.5 2.5z"/></svg>Plyn</a><a href="pocasi.html" style="background:var(--bg-card,#fff);color:var(--text,#3f3f46);border:1px solid var(--border,#e4e4e7);text-decoration:none;display:inline-flex;align-items:center;gap:7px;padding:8px 15px;border-radius:9px;font-weight:600;font-size:14px;white-space:nowrap"><svg width="17" height="17" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 2v2"/><path d="m4.93 4.93 1.41 1.41"/><path d="M20 12h2"/><path d="m19.07 4.93-1.41 1.41"/><path d="M15.947 12.65a4 4 0 0 0-5.925-4.128"/><path d="M13 22H7a5 5 0 1 1 4.9-6H13a3 3 0 0 1 0 6Z"/></svg>Počasí a OZE</a></nav>
386
387<div class="status-banner" id="status-banner"></div>
388<div id="auth-gate" hidden>
389  <form class="auth-card" id="auth-form" autocomplete="on">
390    <div class="auth-brand">Energoreport</div>
391    <div class="auth-sub">Modul aplikace Energetika — přihlaste se firemním účtem.</div>
392    <button type="button" id="auth-ms" onclick="authMicrosoft()" style="background:#2f2f2f;display:flex;align-items:center;justify-content:center;gap:9px;margin-top:4px">
393      <svg width="16" height="16" viewBox="0 0 23 23" aria-hidden="true"><path fill="#f35325" d="M1 1h10v10H1z"/><path fill="#81bc06" d="M12 1h10v10H12z"/><path fill="#05a6f0" d="M1 12h10v10H1z"/><path fill="#ffba08" d="M12 12h10v10H12z"/></svg>
394      Přihlásit přes Microsoft
395    </button>
396    <div class="auth-oddelovac"><span>nebo e-mailem</span></div>
397    <label for="auth-email">E-mail</label>
398    <input id="auth-email" type="email" autocomplete="username" required autofocus />
399    <label for="auth-pass">Heslo</label>
400    <input id="auth-pass" type="password" autocomplete="current-password" required />
401    <button type="submit" id="auth-submit">Přihlásit se</button>
402    <div class="auth-err" id="auth-err" hidden></div>
403  </form>
404</div>
405<div id="supa-loading"><div class="spinner"></div><div class="msg" id="supa-loading-msg">Načítám data ze Supabase…</div></div>
406
407<section>
408  <h2>Nejdůležitější zprávy z energetiky <span style="font-size:13px;font-weight:500;color:var(--text-muted);margin-left:8px">posledních 48 hodin</span></h2>
409  <div class="news-grid" id="news-grid">
410    <!-- zprávy se načítají po přihlášení ze Supabase; v souboru záměrně nejsou -->
411  </div>
412</section>
413
414<section data-chart="el" data-comm="sel">
415  <h2>1. Elektřina — burzovní ceny a historický vývoj (PXE)</h2>
416  <div class="big">
417    <div>
418      <div class="value" id="el-current">— EUR/MWh</div>
419      <div class="label">Cal-27 (referenční CZ baseload, <span id="el-current-date">—</span> PXE settlement)</div>
420    </div>
421  </div>
422  <table>
423    <thead>
424      <tr><th>Produkt</th><th>Cena (EUR/MWh)</th><th>Cena (CZK/MWh)</th><th>Denní změna</th><th>Platnost k</th></tr>
425    </thead>
426    <tbody id="el-table">
427      <tr data-series="el27"><td>Cal-27 (baseload, dodání 2027)</td><td class="num">—</td><td class="num">—</td><td class="num">—</td><td class="num">—</td></tr>
428      <tr data-series="el28"><td>Cal-28 (baseload, dodání 2028)</td><td class="num">—</td><td class="num">—</td><td class="num">—</td><td class="num">—</td></tr>
429      <tr data-series="el29"><td>Cal-29 (baseload, dodání 2029)</td><td class="num">—</td><td class="num">—</td><td class="num">—</td><td class="num">—</td></tr>
430    </tbody>
431  </table>
432
433  <div class="range-controls">
434    <span class="label-small">Období:</span>
435    <button data-range="1m" class="active" onclick="setPriceRange(this, '1m')">1 měsíc</button>
436    <button data-range="3m" onclick="setPriceRange(this, '3m')">3 měsíce</button>
437    <button data-range="6m" onclick="setPriceRange(this, '6m')">6 měsíců</button>
438    <button data-range="12m" onclick="setPriceRange(this, '12m')">12 měsíců</button>
439    <button data-range="ytd" onclick="setPriceRange(this, 'ytd')">YTD</button>
440    <button data-range="all" onclick="setPriceRange(this, 'all')">Vše</button>
441    <span class="date-pickers">
442      <label for="date-from-el">Od:</label>
443      <input type="date" id="date-from-el" onchange="applyCustomRange(this)" />
444      <label for="date-to-el">Do:</label>
445      <input type="date" id="date-to-el" onchange="applyCustomRange(this)" />
446    </span>
447    <button data-yzero="el" onclick="togglePriceYZero('el', this)" style="margin-left:8px">Začít od 0</button>
448    <button data-ccy-for="el" onclick="toggleCcy('el', this)" title="Přepnout měnu (EUR ⇄ CZK)" style="margin-left:8px">Kč</button>
449  </div>
450  <div class="price-legend" id="price-legend-el"></div>
451  <div class="chart-wrap-tall"><canvas id="price-chart-el"></canvas></div>
452  <div class="note">PXE baseload kontrakty pro dodávku elektřiny v Česku (CZ delivery area). Řada <strong>„Next year"</strong> = cena vždy na nejbližší kalendářní rok (PXE/kurzy.cz, historie od r. 2007), přepočtená z CZK na EUR/MWh denním referenčním kurzem ECB. Pro celou historii klikněte na <em>Vše</em>; další řady (Cal-26/27/28/29) zapnete v legendě.</div>
453</section>
454
455<section data-chart="gas" data-comm="sgas">
456  <h2>2. Zemní plyn — burzovní ceny a historický vývoj (PXE / TTF)</h2>
457  <div class="big">
458    <div>
459      <div class="value" id="gas-current">— EUR/MWh</div>
460      <div class="label">CZ VTP BL Cal-27 (<span id="gas-current-date">—</span> PXE settlement)</div>
461    </div>
462  </div>
463  <table>
464    <thead>
465      <tr><th>Produkt</th><th>Cena (EUR/MWh)</th><th>Cena (CZK/MWh)</th><th>Denní změna</th><th>Platnost k</th></tr>
466    </thead>
467    <tbody id="gas-table">
468      <tr data-series="gasTTF"><td>TTF front-month</td><td class="num">—</td><td class="num">—</td><td class="num">—</td><td class="num">—</td></tr>
469      <tr data-series="gas27"><td>CZ VTP BL Cal-27 (dodání 2027)</td><td class="num">—</td><td class="num">—</td><td class="num">—</td><td class="num">—</td></tr>
470      <tr data-series="gas28"><td>CZ VTP BL Cal-28 (dodání 2028)</td><td class="num">—</td><td class="num">—</td><td class="num">—</td><td class="num">—</td></tr>
471      <tr data-series="gas29"><td>CZ VTP BL Cal-29 (dodání 2029)</td><td class="num">—</td><td class="num">—</td><td class="num">—</td><td class="num">—</td></tr>
472    </tbody>
473  </table>
474
475  <div class="range-controls">
476    <span class="label-small">Období:</span>
477    <button data-range="1m" class="active" onclick="setPriceRange(this, '1m')">1 měsíc</button>
478    <button data-range="3m" onclick="setPriceRange(this, '3m')">3 měsíce</button>
479    <button data-range="6m" onclick="setPriceRange(this, '6m')">6 měsíců</button>
480    <button data-range="12m" onclick="setPriceRange(this, '12m')">12 měsíců</button>
481    <button data-range="ytd" onclick="setPriceRange(this, 'ytd')">YTD</button>
482    <button data-range="all" onclick="setPriceRange(this, 'all')">Vše</button>
483    <span class="date-pickers">
484      <label for="date-from-gas">Od:</label>
485      <input type="date" id="date-from-gas" onchange="applyCustomRange(this)" />
486      <label for="date-to-gas">Do:</label>
487      <input type="date" id="date-to-gas" onchange="applyCustomRange(this)" />
488    </span>
489    <button data-yzero="gas" onclick="togglePriceYZero('gas', this)" style="margin-left:8px">Začít od 0</button>
490    <button data-ccy-for="gas" onclick="toggleCcy('gas', this)" title="Přepnout měnu (EUR ⇄ CZK)" style="margin-left:8px">Kč</button>
491  </div>
492  <div class="price-legend" id="price-legend-gas"></div>
493  <div class="chart-wrap-tall"><canvas id="price-chart-gas"></canvas></div>
494  <div class="note">PXE CZ VTP baseload kontrakty zemního plynu (delivery v ČR). Řada <strong>„Next year"</strong> = cena vždy na nejbližší kalendářní rok (PXE/kurzy.cz, historie od r. 2015), přepočtená z CZK na EUR/MWh denním referenčním kurzem ECB. Vše v EUR/MWh — vykreslují se pouze ověřené body, žádná interpolace. Pro celou historii klikněte na <em>Vše</em>.</div>
495</section>
496
497<section data-comm="seua">
498  <h2>3. Emisní povolenky — EU ETS I (EUA)</h2>
499  <div class="big">
500    <div>
501      <div class="value" id="eua-current">— EUR/t</div>
502      <div class="label">EUA · kontrakt s dodáním na nejbližší prosinec · <span id="eua-current-date">—</span> · zdroj oEnergetice</div>
503    </div>
504  </div>
505  <table>
506    <thead>
507      <tr><th>Produkt</th><th>Cena (EUR/t)</th><th>Cena (CZK/t)</th><th>Denní změna</th><th>Platnost k</th></tr>
508    </thead>
509    <tbody>
510      <tr data-series="eua"><td>EUA — EU ETS I (Dec-front)</td><td class="num">—</td><td class="num">—</td><td class="num">—</td><td class="num">—</td></tr>
511    </tbody>
512  </table>
513  <div class="range-controls" id="eua-range">
514    <span class="label-small">Období:</span>
515    <button data-euarange="3m" onclick="setEuaRange(this,'3m')">3 měsíce</button>
516    <button data-euarange="12m" class="active" onclick="setEuaRange(this,'12m')">12 měsíců</button>
517    <button data-euarange="all" onclick="setEuaRange(this,'all')">Vše</button>
518    <button data-euayzero onclick="toggleEuaYZero(this)" style="margin-left:8px">Začít od 0</button>
519    <button data-euaccy onclick="toggleEuaCcy(this)" title="Přepnout měnu (EUR ⇄ CZK)" style="margin-left:8px">Kč</button>
520  </div>
521  <div class="chart-wrap"><canvas id="eua-chart"></canvas></div>
522  <div class="note">Cena evropské emisní povolenky (EUA) — hlavní uhlíková cena systému <strong>EU ETS I</strong>, klíčový driver cen elektřiny i plynu. Zobrazen kontrakt s dodáním na nejbližší prosinec. Zdroj: <a href="https://oenergetice.cz/energostat/ceny-aktualne/emisni-povolenka" target="_blank" rel="noopener">oEnergetice</a>. (ETS II — nový trh pro budovy a dopravu od 2027 — zatím nezobrazujeme, chybí spolehlivý denní zdroj.)</div>
523</section>
524
525<section data-comm="soil">
526  <h2>4. Ropa — Brent, WTI (ICE / NYMEX)</h2>
527  <div class="big">
528    <div>
529      <div class="value" id="oil-current">— USD/bbl</div>
530      <div class="label">Ropa Brent (ICE, přední kontrakt) · <span id="oil-current-date">—</span> · zdroj oEnergetice</div>
531    </div>
532  </div>
533  <table>
534    <thead>
535      <tr><th>Produkt</th><th id="oil-price-header">Cena (USD/bbl)</th><th>Denní změna</th><th>Platnost k</th></tr>
536    </thead>
537    <tbody>
538      <tr data-oil="brent"><td>Ropa Brent (ICE, evropský benchmark)</td><td class="num">—</td><td class="num">—</td><td class="num">—</td></tr>
539      <tr data-oil="wti"><td>Ropa WTI (NYMEX, americký benchmark)</td><td class="num">—</td><td class="num">—</td><td class="num">—</td></tr>
540    </tbody>
541  </table>
542  <div class="range-controls" id="oil-range">
543    <span class="label-small">Období:</span>
544    <button data-oilrange="1m" onclick="setOilRange(this,'1m')">1 měsíc</button>
545    <button data-oilrange="3m" onclick="setOilRange(this,'3m')">3 měsíce</button>
546    <button data-oilrange="6m" onclick="setOilRange(this,'6m')">6 měsíců</button>
547    <button data-oilrange="12m" class="active" onclick="setOilRange(this,'12m')">12 měsíců</button>
548    <button data-oilrange="ytd" onclick="setOilRange(this,'ytd')">YTD</button>
549    <button data-oilrange="all" onclick="setOilRange(this,'all')">Vše</button>
550    <button data-oilyzero onclick="toggleOilYZero(this)" style="margin-left:8px">Začít od 0</button>
551    <span class="label-small" style="margin-left:12px">Jednotka:</span>
552    <select id="oil-unit" onchange="setOilUnit(this.value)" title="Jednotka ceny ropy">
553      <option value="usd_bbl">USD/bbl</option>
554      <option value="usd_t">USD/t</option>
555      <option value="usd_gj">USD/GJ</option>
556      <option value="czk_bbl">Kč/bbl</option>
557      <option value="czk_l">Kč/l</option>
558    </select>
559  </div>
560  <div class="chart-wrap"><canvas id="oil-chart"></canvas></div>
561  <div class="note">Ceny surové ropy — <strong>Brent</strong> (ICE, evropský/globální benchmark) a <strong>WTI</strong> (NYMEX, americký benchmark), přední futures kontrakt v USD za barel. Ropa je klíčový vstup pro dopravu i petrochemii a nepřímo ovlivňuje ceny plynu. Zdroj: <a href="https://oenergetice.cz/energostat/ceny-aktualne/ropa-brent" target="_blank" rel="noopener">oEnergetice</a>.</div>
562</section>
563
564<section data-comm="s4">
565  <h2>5. Spotová cena elektřiny — OTE denní trh (15 min)</h2>
566  <div class="big">
567    <div>
568      <div class="value" id="ote-base">— EUR/MWh</div>
569      <div class="label" id="ote-base-label">Base Load</div>
570    </div>
571    <div>
572      <div class="value" id="ote-peak" style="font-size:22px">110,44</div>
573      <div class="label">Peak (8–20 h)</div>
574    </div>
575    <div>
576      <div class="value" id="ote-offpeak" style="font-size:22px">154,85</div>
577      <div class="label">Offpeak</div>
578    </div>
579  </div>
580  <div class="range-controls">
581    <span class="label-small">Den:</span>
582    <input type="date" id="ote-date-picker" onchange="drawOteChart()" />
583    <button onclick="setOteDate(-1)" style="font-size:14px;line-height:1">‹</button>
584    <button onclick="setOteDate(1)" style="font-size:14px;line-height:1">›</button>
585    <button onclick="setOteDate(0)">Dnes</button>
586    <span id="ote-date-note" style="margin-left:12px;color:var(--text-muted);font-size:12px"></span>
587    <button data-ccy-for="ote" onclick="toggleCcy('ote', this)" title="Přepnout měnu (EUR ⇄ CZK)" style="margin-left:8px">Kč</button>
588  </div>
589  <div class="chart-wrap"><canvas id="ote-chart"></canvas></div>
590  <div class="note">15-minutová spotová cena (15min produkt) v EUR/MWh. Plnou čarou aktuálně vybraný den, čárkovaně D+1 (zítřek) pokud je zveřejněn. OTE publikuje D+1 každý den po obědě po sesouhlasení. Databáze obsahuje <span id="ote-history-count">2</span> dnů × 96 hodnot; scheduled task přidává každý den nový profil + zítřek pokud dostupný.</div>
591</section>
592
593<section data-comm="s5">
594  <h2>6. Hodinové spotové ceny — historie 2020-2026</h2>
595  <div class="range-controls">
596    <span class="label-small">Období:</span>
597    <button data-spot-range="7d" onclick="setSpotRange('7d')">7 dní</button>
598    <button data-spot-range="30d" onclick="setSpotRange('30d')">30 dní</button>
599    <button data-spot-range="90d" onclick="setSpotRange('90d')">90 dní</button>
600    <button data-spot-range="1y" class="active" onclick="setSpotRange('1y')">1 rok</button>
601    <button data-spot-range="ytd" onclick="setSpotRange('ytd')">YTD</button>
602    <button data-spot-range="3y" onclick="setSpotRange('3y')">3 roky</button>
603    <button data-spot-range="all" onclick="setSpotRange('all')">Vše (6 let)</button>
604    <button data-spot-range="compare" onclick="setSpotRange('compare')">Srovnání let</button>
605    <span class="date-pickers">
606      <label for="spot-from">Od:</label>
607      <input type="date" id="spot-from" onchange="applySpotCustomRange()" />
608      <label for="spot-to">Do:</label>
609      <input type="date" id="spot-to" onchange="applySpotCustomRange()" />
610    </span>
611    <button data-ccy-for="spot5" onclick="toggleCcy('spot5', this)" title="Přepnout měnu (EUR ⇄ CZK)" style="margin-left:8px">Kč</button>
612  </div>
613  <div class="chart-wrap-tall">
613<canvas id="spot-hourly-chart"></canvas></div>
614  <div class="note">Hodinová marginální cena ČR (EUR/MWh) z OTE denního trhu. V režimu <strong>Srovnání let</strong> jsou roky překryté podle dne v roce; osa je proto společná nepřestupná škála, kdežto tooltip ukazuje <strong>skutečné datum dané křivky</strong> — v přestupných letech (2020, 2024) se tak od března liší o den. <span id="spot-stats"></span>. Zdroj: historický základ z Excelu 260408 pkv (od 2020-01-01) + denní automatické doplňování z OTE. Data jsou v Supabase a aktualizují se denně.</div>
615</section>
616
617<section data-comm="s20">
618  <h2>7. Konečná spotřeba energie ČR podle nosičů</h2>
619  <div data-ai-col>
620    <div class="range-controls" id="fec-controls">
621      <span class="label-small">Zobrazení:</span>
622      <button data-fecview="pj" class="active" onclick="setFecView('pj')">Objem (PJ)</button>
623      <button data-fecview="pct" onclick="setFecView('pct')">Struktura (%)</button>
624      <span class="label-small" style="margin-left:12px" id="fec-scen-label" hidden>Výhled:</span>
625      <span id="fec-scen-buttons"></span>
626    </div>
627    <div class="chart-wrap-tall"><canvas id="fec-chart"></canvas></div>
628  </div>
629  <div style="overflow-x:auto;margin-top:14px">
630    <table id="fec-table" style="font-size:12px;min-width:900px">
631      <thead><tr id="fec-thead"></tr></thead>
632      <tbody id="fec-tbody"></tbody>
633    </table>
634  </div>
635  <div class="note" id="fec-note-def">Sloupce = <strong>konečná spotřeba energie</strong> (energetické užití) v ČR podle nosiče energie, v PJ za rok. Zdroj: <strong>Eurostat</strong>, energetické bilance <a href="https://ec.europa.eu/eurostat/databrowser/view/nrg_bal_c/default/table?lang=en" target="_blank" rel="noopener">nrg_bal_c</a>, ukazatel <code>FC_E</code> (Final consumption – energy use), staženo přes SDMX API v TJ a přepočteno na PJ. Rozklad na nosiče je nepřekrývající se a jeho součet <strong>přesně odpovídá</strong> součtu Eurostatu za všechny roky.</div>
636  <div class="note" id="fec-note-outlook">📐 <strong>Výhled 2025–2050</strong> pochází z modelu <strong>TIMES-CZ</strong> (projekt SEEPIA), tedy ze stejného modelování, které stojí za Grafem č. 11 v NKEP — scénáře <strong>WEM</strong> (se stávajícími opatřeními) a <strong>WAM</strong> (s dodatečnými opatřeními). Samotný NKEP tato čísla publikuje <strong>pouze jako obrázek</strong>; číselně je uvádí Tab. 1 odborné analýzy ČVUT FS pro Ministerstvo financí (<a href="https://mf.gov.cz/assets/attachments/2026-03-27_Priloha-k-Info-106-99-MF-12889-2026-74.pdf" target="_blank" rel="noopener">PDF na mf.gov.cz</a>), odkud jsou převzata. Součty za rok 2050 (WEM 955,0 PJ / WAM 752,4 PJ) souhlasí s grafem v NKEP. ⚠ Tatáž analýza ČVUT <strong>modelování SEEPIA kritizuje</strong> — uvádí, že tuzemská netto spotřeba vychází v modelu o 7,1 % níže než ve statistice ERÚ. Ber výhled jako <strong>scénář jednoho modelu</strong>, ne jako předpověď.</div>
637  <div class="note" id="fec-note-granularity">⏱ <strong>Model počítá v pětiletých krocích</strong> (2025, 2030, 2035, 2040, 2045, 2050 — dokumentace TIMES-CZ v03.0). Roční hodnoty pro budoucnost <strong>neexistují</strong>. Osa grafu je proto roční, ale sloupce jsou jen v modelových letech; tlačítkem „Dopočítat lineárně" lze mezilehlé roky doplnit — pak jsou <strong>světlejší a v tabulce označené jako „dopočet"</strong>, protože to nejsou data z modelu, ale prostá interpolace mezi jeho výsledky.</div>
638  <div class="note" id="fec-note-compare">🔍 <strong>Skutečnost a výhled nejsou totožně vymezené.</strong> Historie je z Eurostatu (<code>FC_E</code>), výhled z TIMES-CZ v metodice energetické bilance EK. Pro rok 2019, kde se obojí překrývá, dává Eurostat <strong>1 014,4 PJ</strong> a model <strong>1 029,5 PJ</strong> (+1,5 %). Model také nevede biopaliva v dopravě jako samostatnou položku (Eurostat ano, 2024: 14,9 PJ) a „Koks" u něj neobsahuje vyrobené plyny. Rozdíl v úrovni si při čtení přechodu 2024 → 2025 odečti.</div>
639  <div class="note" id="fec-note-caveats">⚠ Co v číslech <strong>není</strong>: neenergetické užití (suroviny pro chemii, maziva, asfalt) — ukazatel <code>FC_E</code> počítá jen energii spotřebovanou jako energii. Elektřina a teplo jsou vedeny jako <strong>nosiče u konečného spotřebitele</strong>, ne podle paliva, ze kterého vznikly — palivo na jejich výrobu je v transformaci, mimo tento ukazatel. „Energie prostředí" je teplo odebrané tepelnými čerpadly, „Odpad" jen nerecyklovatelná (neobnovitelná) část.</div>
640</section>
641
642<footer>
643  <strong>Zdroje:</strong>
644  <div class="sources">
645    <a href="https://oenergetice.cz/energostat/ceny-aktualne/elektrina" target="_blank">oEnergetice – cena elektřiny</a>
646    <a href="https://oenergetice.cz/energostat/ceny-aktualne/zemni-plyn" target="_blank">oEnergetice – cena plynu</a>
647    <a href="https://www.ote-cr.cz/cs/kratkodobe-trhy/elektrina/denni-trh" target="_blank">OTE – denní trh elektřiny</a>
648    <a href="https://agsi.gie.eu/" target="_blank">AGSI+ – zásobníky plynu</a>
649    <a href="https://www.zasobyplynu.cz/" target="_blank">zasobyplynu.cz</a>
650    <a href="https://pxe.cz/cs/derivatovy-trh/elektrina" target="_blank">PXE Market Data Hub</a>
651    <a href="https://www.kurzy.cz/komodity/elektrina-graf-vyvoje-ceny/" target="_blank">kurzy.cz – elektřina</a>
652    <a href="https://www.kurzy.cz/komodity/pxe-zemni-plyn-graf-vyvoje-ceny/" target="_blank">kurzy.cz – plyn</a>
653    <a href="https://eru.gov.cz/prehled-cen-tepelne-energie-v-cleneni-podle-cenovych-lokalit" target="_blank">ERÚ – výsledné ceny tepla</a>
654    <a href="https://www.eex.com/en/market-data/market-data-hub" target="_blank">EEX Market Data Hub</a>
655  </div>
656  <div class="note" id="generated-at">Vygenerováno: 19. května 2026.</div>
657</footer>
658
659</div>
660
661<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/chart.umd.js" integrity="sha384-iU8HYtnGQ8Cy4zl7gbNMOhsDTTKX02BTXptVP/vqAWIaTfM7isw76iyZCsjL2eVi" crossorigin="anonymous"></script>
vendor: 1 bytes, line 661
661
662<script src="https://cdn.jsdelivr.net/npm/[email protected]/build/index.umd.min.js"></script>
vendor: 1 bytes, line 662
662
663<script src="https://cdn.jsdelivr.net/npm/[email protected]/hammer.min.js"></script>
vendor: 1 bytes, line 663
663
664<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/chartjs-plugin-zoom.min.js"></script>
vendor: 1 bytes, line 664
664
665<script>
666// ===== Zoom v čárových grafech: táhnutím přiblížíš, Ctrl+kolečko, Shift+táhnutí = posun, dvojklik = reset =====
667(function () {
668  if (!window.Chart) return;
669  // 1) registruj zoom plugin (pokud se build nezaregistroval sám)
670  try {
671    if (Chart.registry && !Chart.registry.plugins.get('zoom')) {
672      var zp = window.ChartZoom || window['chartjs-plugin-zoom'] || window.chartjsPluginZoom;
673      if (zp) Chart.register(zp.default || zp);
674    }
675  } catch (e) { console.warn('zoom register failed', e); }
676
677  var ZOOM_CFG = {
678    zoom: {
679      drag: { enabled: true, backgroundColor: 'rgba(0,202,133,0.15)', borderColor: 'rgba(0,202,133,0.75)', borderWidth: 1 },
680      wheel: { enabled: true, modifierKey: 'ctrl', speed: 0.08 },
681      pinch: { enabled: true },
682      mode: 'x'
683    },
684    pan: { enabled: true, mode: 'x', modifierKey: 'shift' },
685    limits: { x: { min: 'original', max: 'original' } }
686  };
687
688  // 2) zoom čte config při vytvoření grafu → vlož ho do configu čárových grafů obalením konstruktoru
689  var _Chart = window.Chart;
690  function PatchedChart(item, config) {
691    try {
692      if (config && config.type === 'line') {
693        config.options = config.options || {};
694        config.options.plugins = config.options.plugins || {};
695        if (!config.options.plugins.zoom) config.options.plugins.zoom = ZOOM_CFG;
696      }
697    } catch (e) {}
698    return new _Chart(item, config);
699  }
700  PatchedChart.prototype = _Chart.prototype;
701  Object.setPrototypeOf(PatchedChart, _Chart);   // Chart.register/getChart/defaults/registry projdou na originál
702  window.Chart = PatchedChart;
703
704  // 3) dvojklik na graf = reset přiblížení
705  document.addEventListener('dblclick', function (e) {
706    if (!e.target || e.target.tagName !== 'CANVAS') return;
707    var ch = _Chart.getChart(e.target);
708    if (ch && typeof ch.resetZoom === 'function') ch.resetZoom();
709  });
710})();
711</script>
vendor: 1 bytes, line 711
711
712<script>
713// ===== Motiv: Světlý / Podle systému / Tmavý =====
714function effectiveTheme(mode) {
715  const m = mode || window.__themeMode || 'system';
716  if (m === 'light' || m === 'dark') return m;
717  return (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) ? 'dark' : 'light';
718}
719function redrawChartsForTheme() {
720  // Překreslení grafů s barvami motivu (každé volání izolované: graf z jiné stránky nesmí shodit ostatní)
721  const __rt = fn => { try { fn(); } catch (e) {} };
722  __rt(() => { if (typeof drawPriceChart === 'function') { drawPriceChart('el', priceChartState.el.range); drawPriceChart('gas', priceChartState.gas.range); } });
723  __rt(() => { if (typeof drawStorageHistoryChart === 'function') drawStorageHistoryChart(); });
724  __rt(() => { if (typeof drawOteChart === 'function') drawOteChart(); });
725  __rt(() => { if (typeof drawStorageGauge === 'function') drawStorageGauge(window.__lastStoragePct || 33); });
726  __rt(() => { if (typeof drawHeatChart === 'function') drawHeatChart(); });
727  __rt(() => { if (typeof drawTapChart === 'function') drawTapChart(); });
728  __rt(() => { if (typeof drawSpotHourlyChart === 'function') { if (__spotRange === 'compare') drawSpotCompareChart(); else drawSpotHourlyChart(); } });
729  __rt(() => { if (typeof drawSpotHourChart === 'function') drawSpotHourChart(); });
730  __rt(() => { if (typeof drawSpotDowChart === 'function') drawSpotDowChart(); });
731  __rt(() => { if (typeof drawForwardSpotChart === 'function') drawForwardSpotChart(); });
732  __rt(() => { if (typeof drawBalancingChart === 'function') drawBalancingChart(); });
733  __rt(() => { if (typeof drawEuStorage === 'function') drawEuStorage(); });
734  __rt(() => { if (typeof drawEuaChart === 'function') drawEuaChart(); });
735  __rt(() => { if (typeof drawFecChart === 'function') drawFecChart(); });
736  __rt(() => { if (typeof drawOilChart === 'function') drawOilChart(); });
737}
738function syncThemeSwitch() {
739  const mode = window.__themeMode || 'system';
740  document.querySelectorAll('.theme-switch [data-mode]').forEach(b => {
741    const on = b.getAttribute('data-mode') === mode;
742    b.classList.toggle('active', on);
743    b.setAttribute('aria-pressed', on ? 'true' : 'false');
744  });
745}
746function applyTheme(redraw) {
747  const prev = document.documentElement.getAttribute('data-theme');
748  const eff = effectiveTheme();
749  document.documentElement.setAttribute('data-theme', eff);
750  syncThemeSwitch();
751  if (redraw && prev && prev !== eff) redrawChartsForTheme();
752}
753function setThemeMode(mode) {
754  window.__themeMode = mode;
755  try { localStorage.setItem('themeMode', mode); } catch (e) {}
756  applyTheme(true);
757}
758// Zpětná kompatibilita se starým voláním
759function toggleTheme() { setThemeMode(effectiveTheme() === 'dark' ? 'light' : 'dark'); }
760(function initTheme() {
761  let mode = null;
762  try { mode = localStorage.getItem('themeMode'); } catch (e) {}
763  if (!mode) {
764    let old = null; try { old = localStorage.getItem('theme'); } catch (e) {}   // migrace ze starého klíče
765    mode = (old === 'light' || old === 'dark') ? old : 'system';
766  }
767  window.__themeMode = mode;
768  document.documentElement.setAttribute('data-theme', effectiveTheme());
769  syncThemeSwitch();
770  if (window.matchMedia) {
771    try {
772      window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', function () {
773        if ((window.__themeMode || 'system') === 'system') applyTheme(true);   // v režimu „Podle systému" reaguj na OS
774      });
775    } catch (e) {}
776  }
777})();
778function getCssVar(name) {
779  return getComputedStyle(document.documentElement).getPropertyValue(name).trim();
780}
781
782// Adaptivní popisky osy X podle rozsahu (i při zoomu): dny → „d. M.", týdny/měsíce → „měs rr", roky → rok
783function adaptiveXTick(value) {
784  var span = (this && isFinite(this.max) && isFinite(this.min)) ? (this.max - this.min) : Infinity;
785  var days = span / 86400000;
786  var d = new Date(value);
787  if (days <= 90) return d.toLocaleDateString('cs-CZ', { day: 'numeric', month: 'numeric' });   // dny → „12. 2."
788  if (days <= 1400) return d.toLocaleDateString('cs-CZ', { month: 'short', year: '2-digit' });  // měsíce → „úno 26"
789  return String(d.getFullYear());                                                               // dlouhé → rok
790}
791
792// Kalendářně zarovnané popisky osy X (1. dne měsíce/roku) — rovnoměrné rozestupy bez prázdných míst
793// Uloží skutečné meze dat; při zoomu jsou to meze výřezu → zoom zůstává funkční
794function captureXDataLimits(scale) { scale.$dMin = scale.min; scale.$dMax = scale.max; }
795function calendarXTicks(scale) {
796  try {
797  // osa přesně v rozsahu dat — jinak ji Chart.js roztáhne na zaokrouhlené ticky a vzniknou mezery po krajích
798  if (isFinite(scale.$dMin) && isFinite(scale.$dMax) && scale.$dMax > scale.$dMin) { scale.min = scale.$dMin; scale.max = scale.$dMax; }
799  var min = scale.min, max = scale.max;
800  if (!isFinite(min) || !isFinite(max) || max <= min) return;
801  var days = (max - min) / 86400000, out = [], d, t, step;
802  if (days <= 90) {
803    step = Math.max(1, Math.round(days / 8));
804    d = new Date(min); d.setHours(0, 0, 0, 0);
805    if (d.getTime() < min) d.setDate(d.getDate() + 1);
806    for (t = d.getTime(); t <= max; t += step * 86400000) out.push({ value: t });
807  } else if (days <= 1400) {
808    step = days > 900 ? 3 : (days > 450 ? 2 : 1);
809    d = new Date(min); d.setDate(1); d.setHours(0, 0, 0, 0);
810    if (d.getTime() < min) d.setMonth(d.getMonth() + 1);
811    while (d.getTime() <= max) { out.push({ value: d.getTime() }); d.setMonth(d.getMonth() + step); }
812  } else {
813    var years = days / 365.25;
814    step = years > 12 ? 3 : (years > 6 ? 2 : 1);
815    d = new Date(min); d.setMonth(0, 1); d.setHours(0, 0, 0, 0);
816    if (d.getTime() < min) d.setFullYear(d.getFullYear() + 1);
817    while (d.getTime() <= max) { out.push({ value: d.getTime() }); d.setFullYear(d.getFullYear() + step); }
818  }
819  // Prořídnutí podle ŠÍŘKY osy — jinak se v úzkém grafu (např. vedle mapy) popisky překrývají.
820  if (out.length > 2) {
821    var lblW = 0;
822    try {
823      var c = scale.ctx;
824      if (c) {
825        c.save();
826        var fs = (scale.options.ticks.font && scale.options.ticks.font.size) || 11;
827        c.font = fs + 'px system-ui, -apple-system, sans-serif';
828        var stepS = Math.max(1, Math.floor(out.length / 5));
829        for (var i = 0; i < out.length; i += stepS) lblW = Math.max(lblW, c.measureText(adaptiveXTick.call(scale, out[i].value)).width);
830        c.restore();
831      }
832    } catch (e) { /* měření selhalo → použij konzervativní odhad níže */ }
833    var need = (lblW || 62) + 14;
834    var avail = scale.width || (scale.chart && scale.chart.width * 0.8) || 600;
835    var maxT = Math.max(2, Math.floor(avail / need));
836    if (out.length > maxT) {
837      var k = Math.ceil(out.length / maxT);
838      out = out.filter(function (_, i) { return i % k === 0; });
839    }
840  }
841  if (out.length) scale.ticks = out;   // Chart.js v4: ticky se mění mutací, návratová hodnota se ignoruje
842  } catch (e) { /* při chybě ponech výchozí ticky — osa se nesmí rozbít */ }
843}
844function paletteColor(c) {
845  // Světlé téma: zelené odstíny v grafech sjednoceny na PKV zelenou (#00ca85)
846  if ((document.documentElement.getAttribute('data-theme') || 'light') !== 'light') return c;
847  return (c === '#10b981' || c === '#16a34a' || c === '#047857') ? '#00ca85' : c;
848}
849function updateThemeIcon() { syncThemeSwitch(); }
850</script>
vendor: 1 bytes, line 850
850
851<script>
852// ===== OTE 15min spot data — databáze 15min profilů =====
853// Key: YYYY-MM-DD ISO date. Value: array of 96 čtvrthodinových cen (15min produkt) v EUR/MWh.
854// Scheduled task přidává každý den nový profil + zítřek pokud zveřejněn.
855window.ote15Data = {};
856// Generuj časové popisky 00:00, 00:15, ..., 23:45
857const ote15Hours = [];
858for (let h = 0; h < 24; h++) {
859  for (let m of [0, 15, 30, 45]) {
860    ote15Hours.push(String(h).padStart(2,'0') + ':' + String(m).padStart(2,'0'));
861  }
862}
863// Default zobrazený den
864const oteData = {
865  date: Object.keys(window.ote15Data).sort().reverse()[0] || "2026-05-18",
866  hours: ote15Hours,
867  prices15: null
868};
869oteData.prices15 = window.ote15Data[oteData.date];
870// Dny, které mají reálná data (≥1 platná hodnota) — kvůli mezerám v 15min datech
871function oteAvailableDays() {
872  return Object.keys(window.ote15Data || {})
873    .filter(d => Array.isArray(window.ote15Data[d]) && window.ote15Data[d].some(v => v !== null && v !== undefined && isFinite(v)))
874    .sort();
875}
876function setOteDate(offset) {
877  const picker = document.getElementById('ote-date-picker');
878  if (!picker) return;
879  const days = oteAvailableDays();
880  if (!days.length) { drawOteChart(); return; }
881  if (offset === 0) {
882    picker.value = days[days.length-1]; // poslední dostupný den
883  } else {
884    // Posun jen mezi dostupnými dny (přeskoč mezery)
885    let idx = days.indexOf(picker.value);
886    if (idx < 0) idx = days.length - 1;
887    idx = Math.min(days.length - 1, Math.max(0, idx + offset));
888    picker.value = days[idx];
889  }
890  drawOteChart();
891}
892
893// ===== Načti uložené OTE dny z localStorage při startu =====
894(function loadCachedOte() {
895  try {
896    const cached = localStorage.getItem('ote15DataCache');
897    if (cached) {
898      const obj = JSON.parse(cached);
899      Object.assign(window.ote15Data, obj);
900    }
901  } catch(e) { console.warn('OTE cache load failed', e); }
902})();
903
904function saveOteCache() {
905  try {
906    localStorage.setItem('ote15DataCache', JSON.stringify(window.ote15Data));
907  } catch(e) { console.warn('OTE cache save failed', e); }
908}
909
910// ===== Auto-fetch 15min OTE data pro daný den =====
911async function loadOteFromCR(date) {
912  if (!window.cowork || !window.cowork.callMcpTool) {
913    return { ok: false, error: 'Cowork MCP není dostupný (artefakt mimo Cowork prostředí).' };
914  }
915  const url = `https://www.ote-cr.cz/cs/kratkodobe-trhy/elektrina/denni-trh?time_resolution=PT15M&date=${date}`;
916  try {
917    const html = await window.cowork.callMcpTool('mcp__workspace__web_fetch', {url});
918    // Sparsuj 15min tabulku přímo (rychlejší než askClaude)
919    const doc = new DOMParser().parseFromString(String(html), 'text/html');
920    let arr = [];
921    for (const tbl of doc.querySelectorAll('table')) {
922      const rows = tbl.querySelectorAll('tr');
923      if (rows.length >= 90 && rows.length <= 100) {
924        for (let i = 1; i < rows.length; i++) {
925          const cells = Array.from(rows[i].querySelectorAll('td')).map(c => c.textContent.trim());
926          if (cells.length < 2 || !/^\d\d:\d\d-\d\d:\d\d$/.test(cells[0])) continue;
927          const p = parseFloat(cells[1].replace(/\s/g, '').replace(',', '.'));
928          if (isFinite(p)) arr.push(p);
929        }
930        break;
931      }
932    }
933    if (arr.length !== 96) {
934      // Fallback: zkus askClaude
935      const prompt = `Z následujícího HTML stránky OTE-CR pro den ${date} vytáhni 96 15-minutových cen elektřiny (15min produkt) v EUR/MWh. Vrať POUZE JSON pole 96 čísel, bez vysvětlení. Pokud nedostupné, []`;
936      const result = await window.cowork.askClaude(prompt, [String(html)]);
937      const match = String(result).match(/\[[\s\S]*?\]/);
938      if (match) try { arr = JSON.parse(match[0]); } catch(e) {}
939    }
940    if (!Array.isArray(arr) || arr.length !== 96) {
941      return { ok: false, error: `OTE pro ${date} nevrátil 96 hodnot (našlo se ${arr.length}). Den asi není zveřejněný.` };
942    }
943    if (arr.some(v => typeof v !== 'number' || !isFinite(v))) {
944      return { ok: false, error: 'Některé hodnoty nejsou čísla.' };
945    }
946    window.ote15Data[date] = arr;
947    saveOteCache();
948    return { ok: true, count: 96 };
949  } catch (e) {
950    return { ok: false, error: e.message || String(e) };
951  }
952}
953
954// ===== DATABÁZE — pouze ověřené body z veřejných zdrojů =====
955// Tento JSON je perzistentní úložiště. Scheduled task ho při každém spuštění:
956//   1. parsuje (regex nebo DOMParser),
957//   2. přidá dnešní hodnoty,
958//   3. zapíše zpět do HTML.
959// Žádná interpolace ani odhady — pokud data nejsou, prostě se nezobrazí.
960
961const HISTORICAL_DATA = {series: {el26:{label:"Elektřina Cal-26",points:[]}, el27:{label:"Elektřina Cal-27",points:[]}, el28:{label:"Elektřina Cal-28",points:[]}, el29:{label:"Elektřina Cal-29",points:[]}, el_nextyear:{label:"Elektřina Next year",points:[]}, gasTTF:{label:"Plyn TTF",points:[]}, gas27:{label:"Plyn CZ VTP Cal-27",points:[]}, gas28:{label:"Plyn CZ VTP Cal-28",points:[]}, gas29:{label:"Plyn CZ VTP Cal-29",points:[]}, gas_nextyear:{label:"Plyn Next year",points:[]}, eua:{label:"Emisní povolenky EU ETS (EUA)",points:[]}, brent:{label:"Ropa Brent",points:[]}, wti:{label:"Ropa WTI",points:[]}}};
962
963// Spojí Cal-X kontrakty podle aktivního roku do jedné „front year" série.
964// Pravidlo: během kalendářního roku N je „next year" kontrakt s dodávkou v roce N+1.
965// Příklady: 2025-06-15 → el26 (Cal-26 = delivery 2026), 2026-03-01 → el27, …
966function computeNextYearSeries(prefix) {
967  const points = [];
968  Object.entries(HISTORICAL_DATA.series).forEach(([sid, series]) => {
969    const m = sid.match(/^(el|gas)(\d{2})$/);
970    if (!m || m[1] !== prefix) return;
971    const deliveryYear = 2000 + parseInt(m[2]);
972    const activeYear = deliveryYear - 1;  // rok, ve kterém je tento kontrakt „next year"
973    series.points.forEach(p => {
974      const py = parseInt(p.date.substr(0, 4));
975      if (py === activeYear) points.push({ date: p.date, value: p.value, from: sid });
976    });
977  });
978  points.sort((a, b) => a.date.localeCompare(b.date));
979  return points;
980}
981// ===== Historická spotová data (Excel 260408 pkv Analýza spotových cen) =====
982// 53 405 hodinových bodů 2020-01-01 až 2026-04-05, marginální cena ČR (EUR/MWh).
983window.spotData = {prices:[], firstTs: 1577836800, firstDate: "2020-01-01 00:00", lastDate: "", totalHours: 0, avgByHour3yr: [], avgByHourByYear: {}, avgByDow3yr: [], avgByDowByYear: {}, dailyByYear: {}};
984const TODAY = new Date();
985
986// Pomocné funkce nad HISTORICAL_DATA
987function getSeriesPoints(seriesId) {
988  const s = HISTORICAL_DATA.series[seriesId];
989  return s ? s.points : [];
990}
991
992function rangeStart(range) {
993  let start = new Date(TODAY);
994  if (range === '1m') start.setMonth(start.getMonth() - 1);
995  else if (range === '3m') start.setMonth(start.getMonth() - 3);
996  else if (range === '6m') start.setMonth(start.getMonth() - 6);
997  else if (range === '12m') start.setFullYear(start.getFullYear() - 1);
998  else if (range === 'ytd') start = new Date(TODAY.getFullYear(), 0, 1);
999  else if (range === 'all') start = new Date('2020-01-01');
1000  return start;
1001}
1002
1003// ===== Storage history 5 let, týdenní data =====
1004// Pattern: každý rok klesá v lednu-březnu, narůstá duben-listopad, vrchol kolem listopadu
1005// Anchor: 11/2022 ~95% (po Ukraine war intensive filling), 04/2023 ~60%, 11/2023 ~99% (rekord), 04/2024 ~59%,
1006// 11/2024 ~89%, 04/2025 ~25%, 11/2025 ~93%, 05/2026 ~34%
1007
1008function generateStorageHistory() {
1009  // Týdny 1-52 pro každý rok (storage rok = duben-březen, ale tady kalendářní)
1010  const years = {};
1011  // [datumStart, hodnota%] anchor body
1012  const anchors = {
1013    '2021': [
1014      ['2021-01-01', 73], ['2021-02-01', 60], ['2021-03-01', 45], ['2021-04-01', 33],
1015      ['2021-05-01', 38], ['2021-06-01', 48], ['2021-07-01', 58], ['2021-08-01', 68],
1016      ['2021-09-01', 75], ['2021-10-01', 80], ['2021-11-01', 77], ['2021-12-01', 62]
1017    ],
1018    '2022': [
1019      ['2022-01-01', 35], ['2022-03-01', 22], ['2022-04-01', 25], ['2022-05-01', 35],
1020      ['2022-06-01', 52], ['2022-07-01', 68], ['2022-08-01', 82], ['2022-09-01', 92],
1021      ['2022-10-01', 96], ['2022-11-01', 95], ['2022-12-01', 88]
1022    ],
1023    '2023': [
1024      ['2023-01-01', 80], ['2023-02-01', 72], ['2023-03-01', 65], ['2023-04-01', 60],
1025      ['2023-05-01', 65], ['2023-06-01', 72], ['2023-07-01', 88], ['2023-08-01', 96],
1026      ['2023-09-01', 99], ['2023-10-01', 99], ['2023-11-01', 99], ['2023-12-01', 90]
1027    ],
1028    '2024': [
1029      ['2024-01-01', 82], ['2024-02-01', 72], ['2024-03-01', 62], ['2024-04-01', 59],
1030      ['2024-05-01', 62], ['2024-06-01', 70], ['2024-07-01', 82], ['2024-08-01', 90],
1031      ['2024-09-01', 94], ['2024-10-01', 92], ['2024-11-01', 89], ['2024-12-01', 78]
1032    ],
1033    '2025': [
1034      ['2025-01-01', 65], ['2025-02-01', 50], ['2025-03-01', 35], ['2025-04-01', 25],
1035      ['2025-05-01', 30], ['2025-06-01', 42], ['2025-07-01', 58], ['2025-08-01', 75],
1036      ['2025-09-01', 88], ['2025-10-01', 94], ['2025-11-01', 93], ['2025-12-01', 80]
1037    ],
1038    '2026': [
1039      ['2026-01-01', 68], ['2026-02-01', 50], ['2026-03-01', 38], ['2026-04-01', 32],
1040      ['2026-05-13', 33], ['2026-06-01', 37]
1041    ]
1042  };
1043
1044  function interp(arr, dateStr) {
1045    const t = new Date(dateStr).getTime();
1046    if (t <= new Date(arr[0][0]).getTime()) return arr[0][1];
1047    if (t >= new Date(arr[arr.length-1][0]).getTime()) return arr[arr.length-1][1];
1048    for (let i = 0; i < arr.length - 1; i++) {
1049      const t1 = new Date(arr[i][0]).getTime();
1050      const t2 = new Date(arr[i+1][0]).getTime();
1051      if (t >= t1 && t <= t2) {
1052        const ratio = (t - t1) / (t2 - t1);
1053        return arr[i][1] + ratio * (arr[i+1][1] - arr[i][1]);
1054      }
1055    }
1056    return arr[arr.length-1][1];
1057  }
1058
1059  Object.keys(anchors).forEach(year => {
1060    const weekly = [];
1061    for (let w = 1; w <= 52; w++) {
1062      // ISO week to date
1063      const jan4 = new Date(parseInt(year), 0, 4);
1064      const weekStart = new Date(jan4.getTime() + (w-1) * 7 * 24*60*60*1000);
1065      if (year === '2026' && weekStart > TODAY) break;
1066      const ds = weekStart.toISOString().slice(0,10);
1067      const val = interp(anchors[year], ds);
1068      weekly.push({ week: w, value: Math.round(val*10)/10 });
1069    }
1070    years[year] = weekly;
1071  });
1072  return years;
1073}
1074
1075let storageHistory = generateStorageHistory(); // fallback — přepíše se reálnými AGSI+ daty v loadFromSupabase()
1076
1077// ===== Update latest price tables (sekce 1 + 2) =====
1078// Naplní řádky tabulky aktuálními hodnotami z HISTORICAL_DATA a obarví denní změnu
1079// (+ červená = růst, − zelená = pokles) podle směru.
1080function fmtNum(n, dec) { return n.toFixed(dec).replace('.', ','); }
1081function fmtCZ(n) { return Math.round(n).toLocaleString('cs-CZ'); }
1082function updateLatestPriceTables() {
1083  document.querySelectorAll('[data-series]').forEach(row => {
1084    if (row.tagName !== 'TR') return;
1085    const sid = row.dataset.series;
1086    const series = HISTORICAL_DATA?.series?.[sid];
1087    if (!series || !series.points || series.points.length === 0) return;
1088    const pts = series.points;
1089    const last = pts[pts.length - 1];
1090    const prev = pts.length > 1 ? pts[pts.length - 2] : null;
1091    const cells = row.querySelectorAll('td.num');
1092    if (cells.length < 3) return;
1093    cells[0].textContent = fmtNum(last.value, 2);
1094    // CZK kurzem platným v den poslední ceny (ČNB fixing z fx_rates, fallback poslední známý)
1095    const rate = fxOnDate(last.date) || fxLatest();
1096    cells[1].textContent = rate ? fmtCZ(last.value * rate) : '—';
1097    // Pill jde do <span> uvnitř buňky — td samotná zůstane table-cell s vertical-align: middle.
1098    cells[2].classList.remove('delta-up', 'delta-down', 'muted');
1099    if (prev) {
1100      const diff = last.value - prev.value;
1101      const pct = (diff / prev.value) * 100;
1102      const arrow = diff > 0 ? '▲' : (diff < 0 ? '▼' : '·');
1103      const sign = diff > 0 ? '+' : (diff < 0 ? '−' : '');
1104      const absDiff = Math.abs(diff), absPct = Math.abs(pct);
1105      const cls = diff > 0 ? 'delta-up' : (diff < 0 ? 'delta-down' : 'muted');
1106      const text = `${arrow} ${sign}${fmtNum(absDiff, 2)} (${sign}${fmtNum(absPct, 2)} %)`;
1107      cells[2].innerHTML = `<span class="${cls}">${text}</span>`;
1108    } else {
1109      cells[2].innerHTML = '<span class="muted">—</span>';
1110    }
1111    // Datum platnosti hodnoty (obchodní den posledního settlementu dané série)
1112    if (cells[3]) cells[3].textContent = fmtDateCZ(last.date);
1113  });
1114  // Hlavička aktuálních cen — velké číslo + datum settlementu pod ním
1115  function fmtDateCZ(isoDate) {
1116    if (!isoDate) return '—';
1117    const [y, m, d] = isoDate.split('-');
1118    return `${parseInt(d, 10)}. ${parseInt(m, 10)}. ${y}`;
1119  }
1120  function colorizeBigValue(elemId, pts) {
1121    if (!pts.length) return;
1122    const last = pts[pts.length-1];
1123    const prev = pts.length > 1 ? pts[pts.length-2] : null;
1124    const elCur = document.getElementById(elemId);
1125    if (!elCur) return;
1126    elCur.classList.remove('delta-up', 'delta-down');
1127    if (prev) {
1128      const diff = last.value - prev.value;
1129      if (diff > 0) elCur.classList.add('delta-up');
1130      else if (diff < 0) elCur.classList.add('delta-down');
1131    }
1132    // Inline mini-badge se šipkou + denní změnou
1133    let html = `${fmtNum(last.value, 2)} EUR/MWh`;
1134    if (prev) {
1135      const diff = last.value - prev.value;
1136      const pct = (diff / prev.value) * 100;
1137      const arrow = diff > 0 ? '▲' : (diff < 0 ? '▼' : '·');
1138      const sign = diff > 0 ? '+' : (diff < 0 ? '−' : '');
1139      const cls = diff > 0 ? 'delta-up' : (diff < 0 ? 'delta-down' : 'muted');
1140      html += ` <span class="${cls}" style="font-size:.5em;vertical-align:middle;margin-left:8px;font-weight:700">${arrow} ${sign}${fmtNum(Math.abs(diff), 2)} (${sign}${fmtNum(Math.abs(pct), 2)} %)</span>`;
1141    }
1142    elCur.innerHTML = html;
1143  }
1144  const el27Pts = HISTORICAL_DATA?.series?.el27?.points || [];
1145  colorizeBigValue('el-current', el27Pts);
1146  if (el27Pts.length) {
1147    const last = el27Pts[el27Pts.length-1];
1148    const elDate = document.getElementById('el-current-date');
1149    if (elDate) elDate.textContent = fmtDateCZ(last.date);
1150  }
1151  const gas27Pts = HISTORICAL_DATA?.series?.gas27?.points || [];
1152  colorizeBigValue('gas-current', gas27Pts);
1153  if (gas27Pts.length) {
1154    const last = gas27Pts[gas27Pts.length-1];
1155    const gasDate = document.getElementById('gas-current-date');
1156    if (gasDate) gasDate.textContent = fmtDateCZ(last.date);
1157  }
1158  const euaPts = HISTORICAL_DATA?.series?.eua?.points || [];
1159  if (euaPts.length) {
1160    const l = euaPts[euaPts.length-1], p = euaPts.length>1?euaPts[euaPts.length-2]:null;
1161    const elc = document.getElementById('eua-current');
1162    if (elc) {
1163      elc.classList.remove('delta-up','delta-down');
1164      let html = `${fmtNum(l.value,2)} EUR/t`;
1165      if (p) { const diff=l.value-p.value, pct=diff/p.value*100, up=diff>0; const arrow=up?'\u25B2':(diff<0?'\u25BC':'\u00B7'), sign=up?'+':(diff<0?'\u2212':''), cls=up?'delta-up':(diff<0?'delta-down':'muted'); if(up)elc.classList.add('delta-up'); else if(diff<0)elc.classList.add('delta-down'); html += ` <span class="${cls}" style="font-size:.5em;vertical-align:middle;margin-left:8px;font-weight:700">${arrow} ${sign}${fmtNum(Math.abs(diff),2)} (${sign}${fmtNum(Math.abs(pct),2)} %)</span>`; }
1166      elc.innerHTML = html;
1167    }
1168    const dEl = document.getElementById('eua-current-date'); if (dEl) dEl.textContent = fmtDateCZ(l.date);
1169  }
1170  // Ropa — velké číslo (Brent) + tabulka; jednotka dle přepínače (USD/bbl · USD/t · USD/GJ · Kč/l · Kč/bbl)
1171  const brentPts = HISTORICAL_DATA?.series?.brent?.points || [];
1172  if (brentPts.length) {
1173    const l = brentPts[brentPts.length-1], p = brentPts.length>1?brentPts[brentPts.length-2]:null;
1174    const lv = oilConv(l.value, 'brent', l.date), pv = p ? oilConv(p.value, 'brent', p.date) : null;
1175    const oc = document.getElementById('oil-current');
1176    if (oc) {
1177      oc.classList.remove('delta-up','delta-down');
1178      let html = `${oilFmt(lv)} ${oilUnitLabel()}`;
1179      if (pv != null) { const diff=lv-pv, pct=diff/pv*100, up=diff>0; const arrow=up?'\u25B2':(diff<0?'\u25BC':'\u00B7'), sign=up?'+':(diff<0?'\u2212':''), cls=up?'delta-up':(diff<0?'delta-down':'muted'); if(up)oc.classList.add('delta-up'); else if(diff<0)oc.classList.add('delta-down'); html += ` <span class="${cls}" style="font-size:.5em;vertical-align:middle;margin-left:8px;font-weight:700">${arrow} ${sign}${oilFmt(Math.abs(diff))} (${sign}${fmtNum(Math.abs(pct),2)} %)</span>`; }
1180      oc.innerHTML = html;
1181    }
1182    const dO = document.getElementById('oil-current-date'); if (dO) dO.textContent = fmtDateCZ(l.date);
1183    const th = document.getElementById('oil-price-header'); if (th) th.textContent = `Cena (${oilUnitLabel()})`;
1184  }
1185  document.querySelectorAll('tr[data-oil]').forEach(row => {
1186    const sid = row.dataset.oil;
1187    const pts = HISTORICAL_DATA?.series?.[sid]?.points || [];
1188    if (!pts.length) return;
1189    const last = pts[pts.length-1], prev = pts.length>1?pts[pts.length-2]:null;
1190    const lv = oilConv(last.value, sid, last.date), pv = prev ? oilConv(prev.value, sid, prev.date) : null;
1191    const cells = row.querySelectorAll('td.num');
1192    if (cells.length < 2) return;
1193    cells[0].textContent = oilFmt(lv);
1194    cells[1].classList.remove('delta-up','delta-down','muted');
1195    if (pv != null) { const diff=lv-pv, pct=diff/pv*100; const arrow=diff>0?'\u25B2':(diff<0?'\u25BC':'\u00B7'), sign=diff>0?'+':(diff<0?'\u2212':''), cls=diff>0?'delta-up':(diff<0?'delta-down':'muted'); cells[1].innerHTML=`<span class="${cls}">${arrow} ${sign}${oilFmt(Math.abs(diff))} (${sign}${fmtNum(Math.abs(pct),2)} %)</span>`; }
1196    else cells[1].innerHTML='<span class="muted">\u2014</span>';
1197    if (cells[2]) cells[2].textContent = fmtDateCZ(last.date);
1198  });
1199}
1200
1201// ===== Legenda: skryté řady NEŠKRTAT, jen ztlumit průhledností — platí pro VŠECHNY grafy =====
1202(function () {
1203  if (typeof Chart === 'undefined' || !Chart.defaults) return;
1204  function fadeColor(c, a) {
1205    if (typeof c !== 'string') return c;
1206    if (a == null) a = 0.28;
1207    var m = c.match(/^rgba?\(([^)]+)\)$/i);
1208    if (m) {
1209      var p = m[1].split(',');
1210      return 'rgba(' + p[0].trim() + ',' + p[1].trim() + ',' + p[2].trim() + ',' + a + ')';
1211    }
1212    if (c.charAt(0) === '#') {
1213      var h = c.slice(1);
1214      if (h.length === 3) h = h.split('').map(function (x) { return x + x; }).join('');
1215      if (h.length === 8) h = h.slice(0, 6);
1216      var r = parseInt(h.slice(0, 2), 16), g = parseInt(h.slice(2, 4), 16), b = parseInt(h.slice(4, 6), 16);
1217      if (isNaN(r) || isNaN(g) || isNaN(b)) return c;
1218      return 'rgba(' + r + ',' + g + ',' + b + ',' + a + ')';
1219    }
1220    return c;
1221  }
1222  var baseGen = Chart.defaults.plugins.legend.labels.generateLabels;
1223  Chart.defaults.plugins.legend.labels.generateLabels = function (chart) {
1224    var items = baseGen.call(this, chart);
1225    items.forEach(function (it) {
1226      var visible = true;
1227      try {
1228        if (it.datasetIndex != null) visible = chart.isDatasetVisible(it.datasetIndex);
1229        else if (it.index != null && typeof chart.getDataVisibility === 'function') visible = chart.getDataVisibility(it.index);
1230      } catch (e) {}
1231      if (!visible) {
1232        it.hidden = false;                       // zruší přeškrtnutí textu
1233        it.fontColor = 'rgba(140,140,140,0.5)';  // ztlumený text
1234        it.fillStyle = fadeColor(it.fillStyle, 0.22);
1235        it.strokeStyle = fadeColor(it.strokeStyle, 0.35);
1236      }
1237    });
1238    return items;
1239  };
1240})();
1241
1242// ===== Glow efekt pro „Next year" řady (jako glow v Excelu) =====
1243// Stejná tloušťka čáry jako ostatní řady, ale s měkkou září v barvě řady.
1244const nextYearGlowPlugin = {
1245  id: 'nextYearGlow',
1246  beforeDatasetDraw: function (chart, args) {
1247    const ds = chart.data.datasets[args.index];
1248    if (ds && /Next year/i.test(ds.label || '') && chart.isDatasetVisible(args.index)) {
1249      const ctx = chart.ctx;
1250      ctx.save();
1251      ctx.shadowColor = ds.borderColor;
1252      ctx.shadowBlur = 10;
1253    }
1254  },
1255  afterDatasetDraw: function (chart, args) {
1256    const ds = chart.data.datasets[args.index];
1257    if (ds && /Next year/i.test(ds.label || '') && chart.isDatasetVisible(args.index)) {
1258      chart.ctx.restore();
1259    }
1260  }
1261};
1262
1263// ===== Charts =====
1264// priceCharts: { el: Chart, gas: Chart } — dva nezávislé grafy 3a (elektřina) + 3b (plyn)
1265const priceCharts = {};
1266// Defaultně zobrazen v graf 3A jen el27, v 3B jen gas27 — ostatní jsou skryté (ztlumené v legendě),
1267// zapnout je lze kliknutím na ztlumenou položku v legendě Chart.js.
1268// yZero=false (default) = Y-osa adaptivní; true = vždy začíná od 0.
1269const priceChartState = {
1270  // Default: zobrazen jen Cal-27, období poslední měsíc (1m). Ostatní řady skryté (lze zapnout v legendě).
1271  el: { range: '1m', yZero: false, hidden: { el_nextyear:false, el26:true, el28:true, el29:true, gas_nextyear:true, gasTTF:true, gas27:true, gas28:true, gas29:true } },
1272  gas: { range: '1m', yZero: false, hidden: { el_nextyear:true, el26:true, el27:true, el28:true, el29:true, gas_nextyear:false, gasTTF:true, gas28:true, gas29:true } }
1273};
1274let priceChart, storageHistoryChart; // legacy reference (kept for backwards compat)
1275
1276// ===== Supabase async data loader =====
1277/* ── Přihlášení ────────────────────────────────────────────────────────
1278   Brána neslouží jen ke skrytí stránky: bez platné session a bez záznamu
1279   v cz_pristup_energoreport nevrátí databáze žádná data (RLS), takže ani
1280   obejití téhle obrazovky nic neodhalí. */
1281function supaClient() {
1282  if (!window.__SUPA && typeof supabase !== 'undefined' && supabase.createClient) {
1283    window.__SUPA = supabase.createClient(window.__SUPABASE_URL, window.__SUPABASE_ANON,
1284      // PKCE: po návratu z Microsoftu přijde ?code=… místo tokenu v adrese;
1285      // supabase-js si výměnu za session udělá sám (detectSessionInUrl).
1286      { auth: { flowType: 'pkce', detectSessionInUrl: true, persistSession: true, autoRefreshToken: true } });
1287  }
1288  return window.__SUPA;
1289}
1290
1291async function authStav() {          // 'ok' | 'bezpristupu' | 'neprihlasen'
1292  const s = supaClient(); if (!s) return 'neprihlasen';
1293  let session = null;
1294  try { session = (await s.auth.getSession()).data.session; } catch (e) { return 'neprihlasen'; }
1295  if (!session) return 'neprihlasen';
1296  try {
1297    const { data, error } = await s.rpc('je_clen_energoreport');
1298    return (!error && data === true) ? 'ok' : 'bezpristupu';
1299  } catch (e) { return 'bezpristupu'; }
1300}
1301
1302function authUkazBranu(hlaska) {
1303  const ov = document.getElementById('supa-loading'); if (ov) ov.classList.add('hidden');
1304  const g = document.getElementById('auth-gate'); if (!g) return;
1305  g.hidden = false;
1306  if (hlaska) { const e = document.getElementById('auth-err'); if (e) { e.textContent = hlaska; e.hidden = false; } }
1307  const f = document.getElementById('auth-form');
1308  if (f && !f.dataset.bound) {
1309    f.dataset.bound = '1';
1310    f.addEventListener('submit', async (ev) => {
1311      ev.preventDefault();
1312      const btn = document.getElementById('auth-submit');
1313      const err = document.getElementById('auth-err');
1314      const email = document.getElementById('auth-email').value.trim();
1315      const heslo = document.getElementById('auth-pass').value;
1316      btn.disabled = true; btn.textContent = 'Přihlašuji…'; if (err) err.hidden = true;
1317      const s = supaClient();
1318      const { error } = await s.auth.signInWithPassword({ email, password: heslo });
1319      if (error) {
1320        if (err) { err.textContent = 'Přihlášení se nezdařilo — zkontrolujte e-mail a heslo.'; err.hidden = false; }
1321        btn.disabled = false; btn.textContent = 'Přihlásit se';
1322        return;
1323      }
1324      // přihlášení prošlo, ale rozhoduje whitelist
1325      const { data: clen } = await s.rpc('je_clen_energoreport');
1326      if (clen !== true) {
1327        await s.auth.signOut();
1328        if (err) { err.textContent = 'Účet nemá k tomuto reportu přístup. Požádejte správce.'; err.hidden = false; }
1329        btn.disabled = false; btn.textContent = 'Přihlásit se';
1330        return;
1331      }
1332      location.reload();
1333    });
1334  }
1335}
1336
1337async function authMicrosoft() {
1338  const s = supaClient(); if (!s) return;
1339  const err = document.getElementById('auth-err');
1340  if (err) err.hidden = true;
1341  // Stejné nastavení jako přihlášení do aplikace Energetika (provider azure).
1342  // Statická stránka nemá serverovou /auth/callback — supabase-js si výměnu kódu
1343  // za session udělá sám při načtení (detectSessionInUrl), takže se vracíme sem.
1344  const { error } = await s.auth.signInWithOAuth({
1345    provider: 'azure',
1346    options: { redirectTo: window.location.origin + window.location.pathname, scopes: 'openid email profile' }
1347  });
1348  if (error && err) {
1349    err.textContent = 'Přihlášení přes Microsoft selhalo: ' + error.message;
1350    err.hidden = false;
1351  }
1352}
1353
1354async function authOdhlasit() {
1355  try { await supaClient().auth.signOut(); } catch (e) {}
1356  try { indexedDB.deleteDatabase('erpcache'); } catch (e) {}   // ať další uživatel nevidí data z cache
1357  location.reload();
1358}
1359
1360async function authHlavicka() {
1361  const s = supaClient(); if (!s) return;
1362  let email = '';
1363  try { email = (await s.auth.getUser()).data.user?.email || ''; } catch (e) {}
1364  const box = document.querySelector('.header-actions'); if (!box) return;
1365  const el = document.createElement('div');
1366  el.className = 'auth-who';
1367  el.innerHTML = '<span title="Přihlášen">' + email.replace(/</g, '&lt;') + '</span>'
1368    + '<span>·</span><button type="button" onclick="authOdhlasit()">Odhlásit</button>';
1369  box.appendChild(el);
1370}
1371
1372async function loadFromSupabase() {
1373  if (typeof supabase === 'undefined' || !supabase.createClient) {
1374    console.warn('supabase-js not loaded — pokračuji s prázdnými daty');
1375    return false;
1376  }
1377  if (!window.__SUPA) {
1378    window.__SUPA = supabase.createClient(window.__SUPABASE_URL, window.__SUPABASE_ANON,
1379      // PKCE: po návratu z Microsoftu přijde ?code=… místo tokenu v adrese;
1380      // supabase-js si výměnu za session udělá sám (detectSessionInUrl).
1381      { auth: { flowType: 'pkce', detectSessionInUrl: true, persistSession: true, autoRefreshToken: true } });
1382  }
1383  const supa = window.__SUPA;
1384  const setMsg = (m) => { const el = document.getElementById('supa-loading-msg'); if (el) el.textContent = m; };
1385
1386  // PostgREST/Supabase vrací max 1000 řádků na požadavek. Tahle pomocná funkce
1387  // si nejdřív zjistí počet řádků a pak natáhne VŠECHNY stránky (paralelně).
1388  // RPC má stejný strop 1000 řádků jako běžný select — bez stránkování by se data
1389  // TIŠE uřízla (spot_daily: 1000 z 2430). Tady je offset neškodný, jde o pár stránek.
1390  async function fetchAllRpc(fn) {
1391    const PAGE = 1000;
1392    let out = [], off = 0;
1393    for (let i = 0; i < 50; i++) {
1394      const r = await supa.rpc(fn).range(off, off + PAGE - 1);
1395      if (r.error) { console.error('rpc ' + fn, r.error); break; }
1396      const d = r.data || [];
1397      out = out.concat(d);
1398      if (d.length < PAGE) break;
1399      off += PAGE;
1400    }
1401    return out;
1402  }
1403
1404  async function fetchAllRows(table, columns, orderCol) {
1405    const ckey = 'erp:v2:' + table + ':' + (columns || '*');
1406    try { const o = await Promise.race([__idbCache.get(ckey), new Promise(res => setTimeout(() => res(null), 1500))]); if (o && (Date.now() - o.t) < 1800000) return o.d; } catch (e) {}
1407    const PAGE = 1000;
1408    const krajniHodnota = async (vzestupne) => {
1409      if (!orderCol) return null;
1410      const r = await supa.from(table).select(orderCol).order(orderCol, { ascending: vzestupne }).limit(1);
1411      return (!r.error && r.data && r.data.length) ? r.data[0][orderCol] : null;
1412    };
1413    // count i krajní hodnoty najednou — jinak by to byla dvě kola latence za sebou
1414    const [{ count, error: ce }, krajMin, krajMax] = await Promise.all([
1415      supa.from(table).select('*', { count: 'exact', head: true }),
1416      krajniHodnota(true), krajniHodnota(false)
1417    ]);
1418    if (ce) { console.error('count ' + table, ce); }
1419    const total = count || 0;
1420
1421    // Bez řadicího sloupce nebo do jedné stránky → jeden prostý dotaz.
1422    if (!orderCol || total <= PAGE) {
1423      let q = supa.from(table).select(columns);
1424      if (orderCol) q = q.order(orderCol);
1425      const r = await q.limit(PAGE);
1426      if (r.error) { console.error('fetchAll ' + table, r.error); return []; }
1427      const d = r.data || [];
1428      __idbCache.set(ckey, { t: Date.now(), d: d });
1429      return d;
1430    }
1431
1432    // POZOR na OFFSET: PostgREST musí u N-té stránky přeskákat N×1000 řádků, takže
1433    // náklady rostou kvadraticky (u spot_hourly ~1,7 mil. navštívených řádků na jedno
1434    // načtení, poslední stránka 465 ms). Místo toho dělíme rozsah PODLE HODNOTY klíče
1435    // — každý dotaz je pak rozsahové čtení indexu (~13 ms) a jdou dál paralelně.
1436    const minV = krajMin, maxV = krajMax;
1437    if (minV == null || maxV == null) return [];
1438
1439    // Klíč je datum ('YYYY-MM-DD') nebo časové razítko — hranice se musí posílat ve
1440    // STEJNÉM formátu, jinak by je Postgres nepřetypoval.
1441    const jenDatum = /^\d{4}-\d{2}-\d{2}$/.test(String(minV));
1442    const naMs = (v) => new Date(jenDatum ? String(v) + 'T00:00:00Z' : v).getTime();
1443    const naHodnotu = (ms) => jenDatum ? new Date(ms).toISOString().slice(0, 10) : new Date(ms).toISOString();
1444    const krok = jenDatum ? 86400000 : 1000;          // nejmenší dělitelný krok
1445
1446    const odMs = naMs(minV), doMs = naMs(maxV);
1447    if (!isFinite(odMs) || !isFinite(doMs) || doMs < odMs) return [];
1448
1449    // Načti úsek [a, b); poslední úsek včetně horní hranice. Když se vrátí přesně PAGE
1450    // řádků, mohl být oříznutý → rozpůlíme a načteme znovu (rekurzivně).
1451    const usek = async (aMs, bMs, posledni, hloubka) => {
1452      let q = supa.from(table).select(columns).order(orderCol).gte(orderCol, naHodnotu(aMs));
1453      q = posledni ? q.lte(orderCol, naHodnotu(bMs)) : q.lt(orderCol, naHodnotu(bMs));
1454      const r = await q.limit(PAGE);
1455      if (r.error) { console.error('fetchAll ' + table, r.error); return []; }
1456      const d = r.data || [];
1457      if (d.length < PAGE) return d;
1458      if (bMs - aMs > krok && hloubka < 24) {          // možná oříznuto → rozpůlit
1459        const stred = aMs + Math.floor((bMs - aMs) / 2 / krok) * krok;
1460        if (stred > aMs && stred < bMs) {
1461          const [l, p] = await Promise.all([usek(aMs, stred, false, hloubka + 1), usek(stred, bMs, posledni, hloubka + 1)]);
1462          return l.concat(p);
1463        }
1464      }
1465      // Dál dělit nelze (jedna hodnota klíče s ≥1000 řádky) → dobrat zbytek přes offset.
1466      console.warn('fetchAll ' + table + ': úsek nelze dělit, dobírám offsetem');
1467      let zbytek = [], off = PAGE;
1468      for (let i = 0; i < 20; i++) {
1469        let q2 = supa.from(table).select(columns).order(orderCol).gte(orderCol, naHodnotu(aMs));
1470        q2 = posledni ? q2.lte(orderCol, naHodnotu(bMs)) : q2.lt(orderCol, naHodnotu(bMs));
1471        const r2 = await q2.range(off, off + PAGE - 1);
1472        if (r2.error || !r2.data || !r2.data.length) break;
1473        zbytek = zbytek.concat(r2.data);
1474        if (r2.data.length < PAGE) break;
1475        off += PAGE;
1476      }
1477      return d.concat(zbytek);
1478    };
1479
1480    // Úseky míří na ~60 % limitu, ne na 100 %. Kdyby cílily přesně na 1000 řádků,
1481    // každá nerovnoměrnost v datech by strop překročila a vyvolala další kolo dělení
1482    // (a tím další round-trip). Radši víc menších dotazů — jdou stejně paralelně.
1483    const pocet = Math.max(1, Math.ceil(total / (PAGE * 0.6)));
1484    const sirka = Math.max(krok, Math.ceil((doMs - odMs + krok) / pocet / krok) * krok);
1485    const ukoly = [];
1486    for (let a = odMs; a <= doMs; a += sirka) {
1487      const b = Math.min(a + sirka, doMs);
1488      ukoly.push(usek(a, b, a + sirka > doMs, 0));
1489    }
1490    const casti = await Promise.all(ukoly);
1491    let out = [];
1492    for (const c of casti) out = out.concat(c);
1493
1494    if (total && out.length !== total) {
1495      console.warn('fetchAll ' + table + ': načteno ' + out.length + ' z ' + total + ' řádků');
1496    }
1497    __idbCache.set(ckey, { t: Date.now(), d: out });
1498    return out;
1499  }
1500
1501  const PG = location.pathname.includes('elektrina') ? 'el' : location.pathname.includes('teplo') ? 'teplo' : location.pathname.includes('plyn') ? 'plyn' : 'main';
1502  setMsg('Načítám PXE futures ceny…');
1503  const prices = await fetchAllRows('price_series', 'series_id, trading_date, value', 'trading_date');
1504  prices.forEach(r => {
1505    const s = HISTORICAL_DATA.series[r.series_id];
1506    if (s) s.points.push({ date: r.trading_date, value: parseFloat(r.value) });
1507  });
1508  // „Next year" série: pokud jsou v DB nahraná historická data (PXE/kurzy.cz xlsx,
1509  // přepočtená na EUR/MWh denním kurzem ECB), použij je a doplň novější dny dopočtem
1510  // ze stitched Cal-X kontraktů; jinak použij jen dopočet.
1511  ['el', 'gas'].forEach(p => {
1512    const s = HISTORICAL_DATA.series[p + '_nextyear'];
1513    if (!s) return;
1514    const uploaded = s.points.slice().sort((a, b) => a.date < b.date ? -1 : (a.date > b.date ? 1 : 0));
1515    const computed = computeNextYearSeries(p);
1516    if (uploaded.length) {
1517      const lastUp = uploaded[uploaded.length - 1].date;
1518      s.points = uploaded.concat(computed.filter(pt => pt.date > lastUp));
1519    } else {
1520      s.points = computed;
1521    }
1522  });
1523
1524  setMsg('Načítám kurzy EUR/CZK…');
1525  try {
1526    // ČNB denní fixing z fx_rates (od 2020, plní denní task). Překryje statický
1527    // ECB seznam FX_DAILY a prodlouží ho do dneška (forward-fill víkendů/svátků).
1528    const fx = await fetchAllRows('fx_rates', 'rate_date, eur_czk, usd_czk', 'rate_date');
1529    if (fx && fx.length) {
1530      const a = window.FX_DAILY, base = window.FX_BASE_MS;
1531      const idxOf = ds => Math.floor((Date.UTC(+ds.slice(0,4), +ds.slice(5,7)-1, +ds.slice(8,10)) - base) / 86400000);
1532      const byIdx = {};
1533      fx.forEach(r =>
1533 { byIdx[idxOf(r.rate_date)] = parseFloat(r.eur_czk); });
1534      const firstIdx = idxOf(fx[0].rate_date);
1535      const todayIdx = Math.floor((Date.now() - base) / 86400000);
1536      let lastVal = a[Math.min(firstIdx, a.length) - 1] || a[a.length - 1];
1537      for (let i = firstIdx; i <= todayIdx; i++) {
1538        if (byIdx[i] !== undefined) lastVal = byIdx[i];
1539        a[i] = lastVal;
1540      }
1541      const sums = {};
1542      fx.forEach(r => { const y = r.rate_date.slice(0,4); (sums[y] = sums[y] || []).push(parseFloat(r.eur_czk)); });
1543      Object.keys(sums).forEach(y => { window.FX_YEAR_AVG[y] = Math.round(sums[y].reduce((s,v)=>s+v,0) / sums[y].length * 1000) / 1000; });
1544      // USD/CZK (od 2020; pro ropu 2022+ stačí) — vlastní bázový index
1545      const usdRows = fx.filter(r => r.usd_czk != null);
1546      if (usdRows.length) {
1547        const ubase = Date.UTC(+usdRows[0].rate_date.slice(0,4), +usdRows[0].rate_date.slice(5,7)-1, +usdRows[0].rate_date.slice(8,10));
1548        window.FX_USD_BASE_MS = ubase;
1549        const uidx = ds => Math.floor((Date.UTC(+ds.slice(0,4), +ds.slice(5,7)-1, +ds.slice(8,10)) - ubase) / 86400000);
1550        const uByIdx = {}; usdRows.forEach(r => { uByIdx[uidx(r.rate_date)] = parseFloat(r.usd_czk); });
1551        const utoday = Math.floor((Date.now() - ubase) / 86400000);
1552        const uarr = []; let ulast = parseFloat(usdRows[0].usd_czk);
1553        for (let i = 0; i <= utoday; i++) { if (uByIdx[i] !== undefined) ulast = uByIdx[i]; uarr[i] = ulast; }
1554        window.FX_USD_DAILY = uarr;
1555      }
1556      window.__fxLatestDate = fx[fx.length-1].rate_date;
1557    }
1558  } catch (e) { console.error('fx_rates load failed', e); }
1559
1560  setMsg('Načítám OTE 15min profily…');
1561  const ote15 = PG === 'main' ? await fetchAllRows('spot_15min', 'trading_date, slot_index, price_eur_mwh', 'trading_date') : [];
1562  if (ote15) {
1563    ote15.forEach(r => {
1564      if (!window.ote15Data[r.trading_date]) window.ote15Data[r.trading_date] = new Array(96).fill(null);
1565      window.ote15Data[r.trading_date][r.slot_index] = parseFloat(r.price_eur_mwh);
1566    });
1567  }
1568
1569  setMsg('Načítám hodinové spot ceny 2020-2026 (56k bodů)…');
1570  const all = PG === 'main' ? await fetchAllRows('spot_hourly', 'ts, price_eur_mwh', 'ts') : [];
1571  window.spotData.prices = all.map(r => parseFloat(r.price_eur_mwh));
1572  window.spotData.totalHours = all.length;
1573  if (all.length) {
1574    window.spotData.firstTs = Math.floor(new Date(all[0].ts).getTime()/1000);
1575    window.spotData.firstDate = all[0].ts.slice(0,10) + ' ' + all[0].ts.slice(11,16);
1576    window.spotData.lastTs = Math.floor(new Date(all[all.length-1].ts).getTime()/1000);
1577    window.spotData.lastDate = all[all.length-1].ts.slice(0,10) + ' 23:00';
1578  }
1579
1580  // ===== Kombinovaná vysokorozlišovací série pro graf 5 (reálné timestampy) =====
1581  // Hodinová data + 15min tam, kde jsou dostupná (spot_15min). Pro dny s 15min daty
1582  // nahradíme hodinové body čtvrthodinovými, aby graf 5 ukazoval co největší detail.
1583  (function buildSpotSeries() {
1584    const fifteenDays = new Set(Object.keys(window.ote15Data || {}));
1585    const series = [];
1586    all.forEach(r => {
1587      const ms = new Date(r.ts).getTime();
1588      const day = new Date(ms).toLocaleDateString('sv-SE'); // YYYY-MM-DD v lokálním (CET) čase
1589      if (fifteenDays.has(day)) return;                      // den má 15min → vynech hodinová
1590      const v = parseFloat(r.price_eur_mwh);
1591      if (isFinite(v)) series.push({ x: ms, y: v });
1592    });
1593    Object.keys(window.ote15Data || {}).forEach(day => {
1594      const arr = window.ote15Data[day];
1595      if (!Array.isArray(arr)) return;
1596      const base = new Date(day + 'T00:00:00').getTime();    // lokální půlnoc (CET)
1597      arr.forEach((v, slot) => {
1598        if (v === null || v === undefined || !isFinite(v)) return;
1599        series.push({ x: base + slot * 15 * 60000, y: v });
1600      });
1601    });
1602    series.sort((a, b) => a.x - b.x);
1603    window.spotData.series = series;
1604  })();
1605
1606  setMsg('Načítám hodinové/DoW agregáty a denní data…');
1607  const [{ data: hby }, { data: dby }, dd] = await Promise.all([
1608    supa.rpc('spot_hourly_by_year_read'),
1609    supa.rpc('spot_dow_by_year_read'),
1610    fetchAllRpc('spot_daily_read')   // matview je zamčený → fail-closed RPC, stránkovaně (strop 1000 platí i na RPC)
1611  ]);
1612  if (hby) {
1613    hby.forEach(r => {
1614      const y = String(r.year);
1615      if (!window.spotData.avgByHourByYear[y]) window.spotData.avgByHourByYear[y] = new Array(24).fill(0);
1616      window.spotData.avgByHourByYear[y][r.hour_local] = parseFloat(r.avg_price);
1617    });
1618    const years = ['2023','2024','2025'];
1619    window.spotData.avgByHour3yr = new Array(24).fill(0).map((_, h) => {
1620      let s = 0, n = 0;
1621      years.forEach(y => { if (window.spotData.avgByHourByYear[y]) { s += window.spotData.avgByHourByYear[y][h]; n++; } });
1622      return n ? s/n : 0;
1623    });
1624  }
1625  if (dby) {
1626    dby.forEach(r => {
1627      const y = String(r.year);
1628      if (!window.spotData.avgByDowByYear[y]) window.spotData.avgByDowByYear[y] = new Array(7).fill(0);
1629      window.spotData.avgByDowByYear[y][r.dow] = parseFloat(r.avg_price);
1630    });
1631    const years = ['2023','2024','2025'];
1632    window.spotData.avgByDow3yr = new Array(7).fill(0).map((_, d) => {
1633      let s = 0, n = 0;
1634      years.forEach(y => { if (window.spotData.avgByDowByYear[y]) { s += window.spotData.avgByDowByYear[y][d]; n++; } });
1635      return n ? s/n : 0;
1636    });
1637  }
1638  if (dd) {
1639    dd.forEach(r => {
1640      const d = new Date(r.trading_date);
1641      const y = String(d.getUTCFullYear());
1642      if (!window.spotData.dailyByYear[y]) {
1643        const isLeap = (d.getUTCFullYear() % 4 === 0 && d.getUTCFullYear() % 100 !== 0) || d.getUTCFullYear() % 400 === 0;
1644        window.spotData.dailyByYear[y] = new Array(isLeap ? 366 : 365).fill(null);
1645      }
1646      const jan1 = Date.UTC(d.getUTCFullYear(), 0, 1);
1647      const doy = Math.floor((d.getTime() - jan1) / 86400000);
1648      window.spotData.dailyByYear[y][doy] = parseFloat(r.avg_price);
1649    });
1650  }
1651
1652  setMsg('Načítám ENTSOE výrobu a bilanci…');
1653  const [gen, flow, units] = PG === 'el' ? await Promise.all([
1654    fetchAllRows('entsoe_generation', '*', 'trading_date'),  // 1600+ řádků → paginovat
1655    fetchAllRows('entsoe_flows', '*', 'trading_date'),
1656    fetchAllRows('entsoe_unit_generation', '*', 'trading_date')  // výroba po elektrárnách (A73)
1657  ]) : [[], [], []];
1658  if (units) {
1659    units.forEach(r => {
1660      const d = r.trading_date;
1661      if (!window.entsoeUnitHistory[d]) window.entsoeUnitHistory[d] = {};
1662      const uh = window.entsoeUnitHistory[d];
1663      if (!uh[r.plant]) uh[r.plant] = new Array(24).fill(0);
1664      uh[r.plant][r.hour_local] = parseFloat(r.mw_avg);
1665    });
1666  }
1667  if (gen) {
1668    gen.forEach(r => {
1669      const d = r.trading_date;
1670      if (!window.entsoeHistory[d]) window.entsoeHistory[d] = { generation_mw_hourly: {}, flows_mw_hourly: {} };
1671      const gh = window.entsoeHistory[d].generation_mw_hourly;
1672      if (!gh[r.source_type]) gh[r.source_type] = new Array(24).fill(0);
1673      gh[r.source_type][r.hour_local] = parseFloat(r.mw_avg);
1674    });
1675  }
1676  if (flow) {
1677    flow.forEach(r => {
1678      const d = r.trading_date;
1679      if (!window.entsoeHistory[d]) window.entsoeHistory[d] = { generation_mw_hourly: {}, flows_mw_hourly: {} };
1680      const fh = window.entsoeHistory[d].flows_mw_hourly;
1681      if (!fh[r.neighbor]) fh[r.neighbor] = { export_to_cz_neighbor: new Array(24).fill(0), import_from_cz_neighbor: new Array(24).fill(0), net_export_cz: new Array(24).fill(0) };
1682      fh[r.neighbor].export_to_cz_neighbor[r.hour_local] = parseFloat(r.export_mw);
1683      fh[r.neighbor].import_from_cz_neighbor[r.hour_local] = parseFloat(r.import_mw);
1684      fh[r.neighbor].net_export_cz[r.hour_local] = parseFloat(r.net_export_mw);
1685    });
1686  }
1687  const days = Object.keys(window.entsoeHistory).sort();
1688  if (days.length) {
1689    const latest = days[days.length - 1];
1690    window.entsoeData = Object.assign({date: latest, timezone_note: 'CEST aligned (UTC+2)'}, window.entsoeHistory[latest]);
1691  }
1692
1693  // Ceny regulačních záloh (FCR/aFRR/mFRR) — ENTSO-E A15, denní agregát
1694  try {
1695    const bal = false ? await fetchAllRows('balancing_capacity', 'trading_date, reserve_type, direction, price_avg, price_max, reserve_mw', 'trading_date') : [];
1696    window.balancingData = {};
1697    bal.forEach(r => {
1698      const k = r.reserve_type + '_' + r.direction;
1699      (window.balancingData[k] = window.balancingData[k] || []).push({
1700        date: r.trading_date,
1701        avg: r.price_avg == null ? null : parseFloat(r.price_avg),
1702        max: r.price_max == null ? null : parseFloat(r.price_max),
1703        mw:  r.reserve_mw == null ? null : parseFloat(r.reserve_mw)
1704      });
1705    });
1706  } catch (e) { console.error('balancing load failed', e); window.balancingData = window.balancingData || {}; }
1707
1708  // Zásobníky plynu EU (AGSI) — mapa (poslední stav) + denní vývoj
1709  try {
1710    const es = PG === 'plyn' ? await fetchAllRows('gas_storage_eu', 'observation_date, country_code, country_name, full_percent, gas_twh, trend', 'observation_date') : [];
1711    window.euStorageSeries = {}; window.euStorageNames = {};
1712    es.forEach(r => {
1713      (window.euStorageSeries[r.country_code] = window.euStorageSeries[r.country_code] || []).push({
1714        date: r.observation_date,
1715        full: r.full_percent == null ? null : parseFloat(r.full_percent),
1716        twh: r.gas_twh == null ? null : parseFloat(r.gas_twh)
1717      });
1718      if (r.country_name) window.euStorageNames[r.country_code] = r.country_name;
1719    });
1720    window.euStorageLatest = {}; window.euStorageLatestDate = null;
1721    Object.keys(window.euStorageSeries).forEach(code => {
1722      const s = window.euStorageSeries[code];
1723      const last = s[s.length - 1];
1724      if (last && last.date > (window.euStorageLatestDate || '')) window.euStorageLatestDate = last.date;
1725      if (code !== 'EU' && last) window.euStorageLatest[code] = { full: last.full, twh: last.twh, name: window.euStorageNames[code] || code };
1726    });
1727    window.euStorageHistory = {};
1728    (window.euStorageSeries['EU'] || []).forEach(p => {
1729      if (p.full == null) return;
1730      const dt = new Date(p.date + 'T12:00:00'); const y = String(dt.getFullYear());
1731      const doy = Math.round((dt - new Date(y + '-01-01T12:00:00')) / 86400000) + 1;
1732      (window.euStorageHistory[y] = window.euStorageHistory[y] || []).push({ week: Math.round(((doy - 1) / 7 + 1) * 100) / 100, value: p.full });
1733    });
1734  } catch (e) { console.error('eu storage load failed', e); window.euStorageSeries = {}; window.euStorageLatest = {}; }
1735
1736  // Skladba dovozu plynu do EU (ENTSOG + ALSI) — jen stránka Plyn
1737  try {
1738    const imp = (PG === 'plyn') ? await fetchAllRows('gas_imports_eu', 'trading_date, source, flow_gwh', 'trading_date') : [];
1739    window.euImports = {};
1740    imp.forEach(r => { (window.euImports[r.source] = window.euImports[r.source] || []).push({ date: r.trading_date, gwh: r.flow_gwh == null ? null : parseFloat(r.flow_gwh) }); });
1741  } catch (e) { console.error('imports load failed', e); window.euImports = {}; }
1742
1743  setMsg('Načítám ceny tepla, zprávy, ZEVO a spotřebu energie…');
1744  const [{ data: heat }, storageAll, { data: news }, { data: zevo }, { data: fec }] = await Promise.all([
1745    supa.from('heat_prices').select('*'),
1746    fetchAllRows('gas_storage', 'observation_date, full_percent', 'observation_date'),  // celá AGSI historie 2020+ (sekce 9 + 10)
1747    supa.from('news_articles').select('*').order('published_at', { ascending: false }).limit(4),
1748    supa.from('zevo_annual').select('*').order('year', { ascending: false }),
1749    supa.from('energy_consumption_annual').select('*').order('year', { ascending: true })
1750  ]);
1751  if (zevo) {
1752    window.zevoData = zevo;
1753  }
1754  window.fecData = fec || [];
1755  if (heat) {
1756    heat.forEach(r => {
1757      const y = String(r.year);
1758      if (!heatData.byYear[y]) heatData.byYear[y] = [];
1759      heatData.byYear[y].push({ kraj: r.region, price: parseFloat(r.price_czk_gj), gj: parseFloat(r.weighted_volume_gj), locality: r.location_count });
1760    });
1761  }
1762  if (storageAll && storageAll.length) {
1763    // Sekce 9 — vždy nejnovější hodnota z DB (denní AGSI+ data, ~2denní publikační zpoždění)
1764    const lastRow = storageAll[storageAll.length - 1];
1765    window.__lastStoragePct = parseFloat(lastRow.full_percent);
1766    const pctEl = document.getElementById('storage-pct');
1767    if (pctEl) pctEl.textContent = window.__lastStoragePct.toFixed(1).replace('.', ',') + ' %';
1768    const asofEl = document.getElementById('storage-asof');
1769    if (asofEl) {
1770      const d = new Date(lastRow.observation_date + 'T12:00:00');
1771      asofEl.textContent = 'Zaplněnost CZ (AGSI+ k ' + d.getDate() + '. ' + (d.getMonth() + 1) + '. ' + d.getFullYear() + ')';
1772    }
1773    // Meziroční srovnání: stejné datum před rokem (fallback nejbližší den do ±5 dní)
1774    const yoyEl = document.getElementById('storage-yoy');
1775    if (yoyEl) {
1776      const byDate = {};
1777      storageAll.forEach(r => { byDate[r.observation_date] = parseFloat(r.full_percent); });
1778      const lastD = lastRow.observation_date;
1779      const targetISO = (String(+lastD.slice(0, 4) - 1)) + lastD.slice(4);
1780      let prevVal = null, prevDate = null;
1781      for (let off = 0; off <= 5 && prevVal === null; off++) {
1782        for (const s of (off === 0 ? [0] : [-off, off])) {
1783          const t = new Date(targetISO + 'T12:00:00'); t.setDate(t.getDate() + s);
1784          const iso = t.toISOString().slice(0, 10);
1785          if (byDate[iso] !== undefined) { prevVal = byDate[iso]; prevDate = t; break; }
1786        }
1787      }
1788      if (prevVal !== null) {
1789        const diff = window.__lastStoragePct - prevVal;
1790        const sign = diff >= 0 ? '+' : '−';
1791        // víc plynu = pozitivní → zelená (ne cenová sémantika delta-up/down)
1792        const col = diff >= 0 ? (getCssVar('--gauge-high') || '#16a34a') : (getCssVar('--gauge-low') || '#dc2626');
1793        yoyEl.innerHTML = 'Před rokem (' + prevDate.getDate() + '. ' + (prevDate.getMonth() + 1) + '. ' + prevDate.getFullYear() + '): '
1794          + prevVal.toFixed(1).replace('.', ',') + ' % <span style="color:' + col + ';font-weight:600">' + sign + ' '
1795          + Math.abs(diff).toFixed(1).replace('.', ',') + ' p. b.</span>';
1796      }
1797    }
1798    // Sekce 10 — reálné denní křivky po letech (x = kalendářní týden, spojitě)
1799    const byYear = {};
1800    storageAll.forEach(r => {
1801      const y = r.observation_date.slice(0, 4);
1802      const doy = Math.round((new Date(r.observation_date + 'T12:00:00') - new Date(y + '-01-01T12:00:00')) / 86400000) + 1;
1803      (byYear[y] = byYear[y] || []).push({ week: Math.round(((doy - 1) / 7 + 1) * 100) / 100, value: parseFloat(r.full_percent) });
1804    });
1805    if (Object.keys(byYear).length) storageHistory = byYear;
1806  }
1807  if (news) {
1808    const grid = document.getElementById('news-grid');
1809    if (grid) {
1810      grid.innerHTML = news.map(n => {
1811        const d = new Date(n.published_at);
1812        const dateStr = d.getDate() + '. ' + (d.getMonth()+1) + '. ' + d.getFullYear();
1813        const title = String(n.title || '').replace(/</g,'&lt;');
1814        const summary = String(n.summary || '').replace(/</g,'&lt;');
1815        return '<div class="news-card"><span class="news-tag ' + n.category + '">' + n.source + '</span><h3>' + title + '</h3><p class="news-summary">' + summary + '</p><div class="news-meta"><strong>' + dateStr + '</strong><span>·</span><a href="' + n.url + '" target="_blank">' + n.source + '</a></div></div>';
1816      }).join('');
1817    }
1818  }
1819
1820  // ===== AI komentáře (chart_commentary) — postranní rámeček vedle obsahu sekce =====
1821  try {
1822    const { data: comm } = await supa.from('chart_commentary').select('*');
1823    if (comm && comm.length) {
1824      const st = document.createElement('style');
1825      st.textContent =
1826        '.sec-flex{display:flex;gap:20px;align-items:stretch}' +
1827        '.sec-main{flex:1 1 auto;min-width:0}' +
1828        '.ai-side{flex:0 0 270px;box-sizing:border-box;padding:14px 16px;border:1px solid var(--border,#e4e4e7);' +
1829        'border-top:3px solid var(--brand,#00ca85);border-radius:10px;background:var(--bg-subtle,#f4f4f5);' +
1830        'font-size:13.5px;line-height:1.55;color:var(--text,#3f3f46)}' +
1831        '.ai-side .ai-tag{display:block;font-size:10.5px;font-weight:700;text-transform:uppercase;' +
1832        'letter-spacing:.05em;
1832color:var(--brand,#00ca85);margin-bottom:8px}' +
1833        '.ai-side .ai-when{display:block;margin-top:10px;font-size:11px;color:var(--text-muted,#71717a)}' +
1834        '.ai-side .ai-sep{border:none;border-top:1px solid var(--border,#e4e4e7);margin:16px 0 12px}' +
1835        '@media (max-width:980px){.sec-flex{flex-direction:column}.ai-side{flex:none;width:auto}}';
1836      document.head.appendChild(st);
1837      // Seskup komentáře podle cílové sekce (sekce může nést více klíčů, např. data-comm="s1 s3a").
1838      const bySection = new Map();
1839      comm.forEach(c => {
1840        if (!c || !c.commentary) return;
1841        const key = String(c.section_id || '').toLowerCase();
1842        const section = document.querySelector('section[data-comm~="' + key + '"]');
1843        if (!section) return;
1844        if (!bySection.has(section)) bySection.set(section, []);
1845        bySection.get(section).push(c);
1846      });
1847      bySection.forEach((list, section) => {
1848        const h2 = section.querySelector('h2');
1849        if (!h2) return;
1850        list.sort((a, b) => String(a.section_id).localeCompare(String(b.section_id)));
1851        // přeskládání: <h2> + <div.sec-flex>[ <div.sec-main>obsah</div> | <aside.ai-side> ]
1852        // data-ai-col: AI box jen vedle vymezeného sloupce (graf); zbytek sekce zůstane plnou šířkou pod ním
1853        const scopedCol = section.querySelector('[data-ai-col]');
1854        const flex = document.createElement('div');
1855        flex.className = 'sec-flex';
1856        const main = document.createElement('div');
1857        main.className = 'sec-main';
1858        if (scopedCol) { scopedCol.parentNode.insertBefore(flex, scopedCol); main.appendChild(scopedCol); }
1859        else { while (h2.nextSibling) main.appendChild(h2.nextSibling); }
1860        const aside = document.createElement('aside');
1861        aside.className = 'ai-side';
1862        aside.innerHTML = list.map((c, i) => {
1863          let when = '';
1864          if (c.generated_at) {
1865            const dt = new Date(c.generated_at);
1866            when = 'vygenerováno ' + dt.getDate() + '. ' + (dt.getMonth() + 1) + '. ' +
1867              String(dt.getHours()).padStart(2, '0') + ':' + String(dt.getMinutes()).padStart(2, '0') +
1868              ' · automatický komentář';
1869          }
1870          return (i > 0 ? '<hr class="ai-sep">' : '') +
1871            '<span class="ai-tag">⚡ AI hodnocení</span>' +
1872            String(c.commentary).replace(/</g, '&lt;') +
1873            (when ? '<span class="ai-when">' + when + '</span>' : '');
1874        }).join('');
1875        flex.appendChild(main);
1876        flex.appendChild(aside);
1877        if (!scopedCol) section.appendChild(flex);
1878      });
1879    }
1880  } catch (e) { console.error('commentary load failed', e); }
1881
1882  setMsg('Hotovo, vykresluji grafy…');
1883  return true;
1884}
1885
1886window.addEventListener("DOMContentLoaded", async function() {
1887  // Nejdřív přihlášení — bez něj se data ani nezkouší načítat.
1888  const stav = await authStav();
1889  if (stav !== 'ok') {
1890    authUkazBranu(stav === 'bezpristupu' ? 'Účet nemá k tomuto reportu přístup. Požádejte správce.' : '');
1891    return;
1892  }
1893  try { await loadFromSupabase(); } catch (e) { console.error('Supabase load failed:', e); }
1894  const overlay = document.getElementById('supa-loading');
1895  if (overlay) setTimeout(() => overlay.classList.add('hidden'), 300);
1896  __runOriginalInit();
1897  authHlavicka();
1898});
1899
1900let euaChart = null; window.__euaRange = window.__euaRange || '12m'; window.__euaYZero = window.__euaYZero || false; window.__euaCcy = window.__euaCcy || 'EUR';
1901function setEuaRange(btn, r) {
1902  window.__euaRange = r;
1903  if (btn && btn.parentElement) btn.parentElement.querySelectorAll('[data-euarange]').forEach(b => b.classList.toggle('active', b === btn));
1904  drawEuaChart();
1905}
1906function toggleEuaYZero(btn) { window.__euaYZero = !window.__euaYZero; if (btn) btn.classList.toggle('active', window.__euaYZero); drawEuaChart(); }
1907function toggleEuaCcy(btn) { window.__euaCcy = window.__euaCcy === 'EUR' ? 'CZK' : 'EUR'; if (btn) { btn.classList.toggle('active', window.__euaCcy === 'CZK'); btn.textContent = window.__euaCcy === 'CZK' ? '\u20ac' : 'K\u010d'; } drawEuaChart(); }
1908/* ── Sekce 7: konečná spotřeba energie ČR podle nosičů (Eurostat FC_E) ─────────────────
1909   Skutečnost je roční řada z Eurostatu; výhledové scénáře se do stejné tabulky ukládají
1910   pod vlastním `scenario` a v grafu se odliší průhledností + rámečkem. Pokud v DB žádný
1911   výhled není, sekce ukáže jen skutečnost a přepínač scénářů se vůbec nezobrazí. */
1912let fecChart = null;
1913window.__fecView = 'pj';
1914window.__fecScenario = null;
1915const FEC_CARRIERS = [   // pořadí = zdola nahoru ve sloupci
1916  { key: 'odpad',             label: 'Odpad (nerecyklovatelný)',  color: '#c9a66b' },
1917  { key: 'solarni',           label: 'Solární termální',          color: '#f59e0b' },
1918  { key: 'ostatni',           label: 'Ostatní',                   color: '#d4d4d8' },
1919  { key: 'biopaliva',         label: 'Biopaliva v dopravě',       color: '#6b8e23' },
1920  { key: 'vodik',             label: 'Vodík',                     color: '#1d4ed8' },
1921  { key: 'teplo',             label: 'Teplo',                     color: '#fdba74' },
1922  { key: 'elektrina',         label: 'Elektřina',                 color: '#d946ef' },
1923  { key: 'ropna_paliva',      label: 'Ropná paliva',              color: '#ef4444' },
1924  { key: 'koks_plyny',        label: 'Koks a vyrobené plyny',     color: '#9ca3af' },
1925  { key: 'zemni_plyn',        label: 'Zemní plyn',                color: '#facc15' },
1926  { key: 'hnede_uhli',        label: 'Hnědé uhlí',                color: '#8b5a2b' },
1927  { key: 'cerne_uhli',        label: 'Černé uhlí',                color: '#18181b' },
1928  { key: 'biomasa',           label: 'Biomasa',                   color: '#16a34a' },
1929  { key: 'bioplyn',           label: 'Bioplyn',                   color: '#a6d96a' },
1930  { key: 'energie_prostredi', label: 'Energie prostředí',         color: '#e7ded3' }
1931];
1932
1933// černé uhlí by na tmavém pozadí splynulo — v tmavém motivu ho posvětlíme
1934function fecColor(c) {
1935  const dark = (document.documentElement.getAttribute('data-theme') || 'light') !== 'light';
1936  if (c.key === 'cerne_uhli' && dark) return '#52525b';
1937  return paletteColor(c.color);
1938}
1939
1940function fecScenarios() {
1941  const s = [...new Set((window.fecData || []).map(r => r.scenario))].filter(x => x !== 'skutecnost');
1942  return s.sort();
1943}
1944function setFecView(v) {
1945  window.__fecView = v;
1946  document.querySelectorAll('[data-fecview]').forEach(b => b.classList.toggle('active', b.getAttribute('data-fecview') === v));
1947  drawFecChart();
1948}
1949function setFecScenario(s) {
1950  window.__fecScenario = s;
1951  document.querySelectorAll('[data-fecscen]').forEach(b => b.classList.toggle('active', b.getAttribute('data-fecscen') === String(s)));
1952  drawFecChart();
1953}
1954function fecScenarioLabel(s) {
1955  if (s === 'wem') return 'WEM — se stávajícími opatřeními';
1956  if (s === 'wam') return 'WAM — s dodatečnými opatřeními';
1957  return s;
1958}
1959window.__fecFill = false;
1960function setFecFill(on) {
1961  window.__fecFill = !!on;
1962  document.querySelectorAll('[data-fecfill]').forEach(b => b.classList.toggle('active', (b.getAttribute('data-fecfill') === '1') === !!on));
1963  drawFecChart();
1964}
1965
1966function drawFecChart() {
1967  const el = document.getElementById('fec-chart'); if (!el) return;
1968  const rows = window.fecData || [];
1969  if (!rows.length) return;
1970  const muted = getCssVar('--text-muted') || '#71717a';
1971  const border = getCssVar('--border') || '#e4e4e7';
1972  const strong = getCssVar('--text-strong') || '#09090b';
1973  const bgCard = getCssVar('--bg-card') || '#fff';
1974
1975  // přepínač scénářů se vykreslí jen když v DB nějaký výhled je
1976  const scens = fecScenarios();
1977  const btnBox = document.getElementById('fec-scen-buttons');
1978  const scenLabel = document.getElementById('fec-scen-label');
1979  if (btnBox && !btnBox.dataset.built) {
1980    if (scens.length) {
1981      if (scenLabel) scenLabel.hidden = false;
1982      btnBox.innerHTML = scens.map(s =>
1983        '<button data-fecscen="' + s + '" onclick="setFecScenario(\'' + s + '\')">' + fecScenarioLabel(s) + '</button>').join('')
1984        + '<button data-fecscen="null" class="active" onclick="setFecScenario(null)">Bez výhledu</button>'
1985        + '<span class="label-small" style="margin-left:12px">Mezilehlé roky:</span>'
1986        + '<button data-fecfill="0" class="active" onclick="setFecFill(false)">Nechat prázdné</button>'
1987        + '<button data-fecfill="1" onclick="setFecFill(true)">Dopočítat lineárně</button>';
1988    }
1989    btnBox.dataset.built = '1';
1990  }
1991
1992  const hist = rows.filter(r => r.scenario === 'skutecnost');
1993  const sc = window.__fecScenario;
1994  const proj = sc ? rows.filter(r => r.scenario === sc) : [];
1995  const histYears = [...new Set(hist.map(r => r.year))].sort((a, b) => a - b);
1996  const lastHist = histYears[histYears.length - 1];
1997  const modelYears = [...new Set(proj.map(r => r.year))].filter(y => y > lastHist).sort((a, b) => a - b);
1998  // osa je ROČNÍ i do budoucna; model ale počítá jen v pětiletých krocích (TIMES-CZ),
1999  // takže mezilehlé roky jsou buď prázdné, nebo — po zapnutí — lineárně dopočtené.
2000  const lastYear = modelYears.length ? modelYears[modelYears.length - 1] : lastHist;
2001  const years = [];
2002  for (let y = histYears[0]; y <= lastYear; y++) years.push(y);
2003  const isProj = y => y > lastHist;
2004  const isModel = y => modelYears.indexOf(y) > -1;
2005  const isFill = y => isProj(y) && !isModel(y);
2006
2007  const val = {};
2008  rows.forEach(r => { val[r.scenario + '|' + r.year + '|' + r.carrier] = parseFloat(r.pj); });
2009  const raw = (y, c) => val[(isProj(y) ? sc : 'skutecnost') + '|' + y + '|' + c];
2010  const get = (y, c) => {
2011    if (!isFill(y)) return raw(y, c) || 0;
2012    if (!window.__fecFill) return 0;
2013    const lo = [lastHist].concat(modelYears).filter(x => x < y).pop();
2014    const hi = modelYears.find(x => x > y);
2015    if (lo === undefined || hi === undefined) return 0;
2016    const a = raw(lo, c) || 0, b = raw(hi, c) || 0;
2017    return a + (b - a) * (y - lo) / (hi - lo);
2018  };
2019  const totals = {};
2019 years.forEach(y => { totals[y] = FEC_CARRIERS.reduce((a, c) => a + get(y, c.key), 0); });
2020
2021  const pct = window.__fecView === 'pct';
2022  const alpha = (hex, a) => {
2023    const h = hex.replace('#', '');
2024    const n = parseInt(h.length === 3 ? h.split('').map(x => x + x).join('') : h, 16);
2025    return 'rgba(' + ((n >> 16) & 255) + ',' + ((n >> 8) & 255) + ',' + (n & 255) + ',' + a + ')';
2026  };
2027  const datasets = FEC_CARRIERS
2028    .filter(c => years.some(y => get(y, c.key) > 0))          // nosič bez dat se nezobrazuje
2029    .map(c => ({
2030      label: c.label,
2031      data: years.map(y => pct ? (totals[y] ? get(y, c.key) / totals[y] * 100 : 0) : get(y, c.key)),
2032      backgroundColor: years.map(y => isFill(y) ? alpha(fecColor(c), 0.28) : isProj(y) ? alpha(fecColor(c), 0.6) : fecColor(c)),
2033      borderColor: years.map(y => isProj(y) ? muted : 'transparent'),
2034      borderWidth: years.map(y => isModel(y) ? 0.5 : 0),
2035      stack: 'fec', maxBarThickness: 40, $raw: c.key
2036    }));
2037
2038  if (fecChart) fecChart.destroy();
2039  fecChart = new Chart(el.getContext('2d'), {
2040    type: 'bar',
2041    data: { labels: years.map(String), datasets: datasets },
2042    options: {
2043      responsive: true, maintainAspectRatio: false, animation: false,
2044      interaction: { mode: 'index', intersect: false },
2045      plugins: {
2046        legend: { display: true, position: 'top', reverse: true,
2047          labels: { color: muted, usePointStyle: true, pointStyle: 'rectRounded', boxWidth: 12, boxHeight: 8, padding: 8, font: { size: 10, weight: '500' } } },
2048        tooltip: {
2049          backgroundColor: strong, titleColor: bgCard, bodyColor: bgCard, padding: 10, cornerRadius: 8,
2050          itemSort: (a, b) => b.datasetIndex - a.datasetIndex,
2051          filter: item => item.parsed.y > 0.05,
2052          callbacks: {
2053            title: items => {
2054              const y = years[items[0].dataIndex];
2055              if (isFill(y)) return y + ' — LINEÁRNÍ DOPOČET (model tento rok nepočítá)';
2056              return y + (isProj(y) ? ' — výhled ' + fecScenarioLabel(sc) : '');
2057            },
2058            label: c => {
2059              const y = years[c.dataIndex];
2060              const raw = get(y, datasets[c.datasetIndex].$raw);
2061              return c.dataset.label + ': ' + raw.toFixed(1).replace('.', ',') + ' PJ'
2062                + (totals[y] ? ' (' + (raw / totals[y] * 100).toFixed(1).replace('.', ',') + ' %)' : '');
2063            },
2064            footer: items => {
2065              const y = years[items[0].dataIndex];
2066              return 'Celkem: ' + totals[y].toFixed(1).replace('.', ',') + ' PJ';
2067            }
2068          }
2069        }
2070      },
2071      scales: {
2072        x: { stacked: true, grid: { display: false }, ticks: { color: muted, font: { size: 10 }, autoSkip: false, maxRotation: 60 } },
2073        y: { stacked: true, beginAtZero: true, max: pct ? 100 : undefined,
2074          title: { display: true, text: pct ? 'podíl na konečné spotřebě (%)' : 'konečná spotřeba energie (PJ/rok)', color: muted },
2075          grid: { color: border },
2076          ticks: { color: muted, callback: v => pct ? v + ' %' : v.toLocaleString('cs-CZ') } }
2077      }
2078    }
2079  });
2080  renderFecTable(years.filter(y => !isFill(y) || window.__fecFill), get, totals, isProj, isFill);
2081}
2082
2083function renderFecTable(years, get, totals, isProj, isFill) {
2084  const th = document.getElementById('fec-thead'), tb = document.getElementById('fec-tbody');
2085  if (!th || !tb) return;
2086  const used = FEC_CARRIERS.filter(c => years.some(y => get(y, c.key) > 0));
2087  th.innerHTML = '<th>Rok</th>' + used.map(c => '<th class="num">' + c.label + '</th>').join('') + '<th class="num">Celkem</th>';
2088  const f = v => v === 0 ? '—' : v.toFixed(1).replace('.', ',');
2089  tb.innerHTML = years.slice().reverse().map(y =>
2090    '<tr><td><strong>' + y + '</strong>' + (isFill(y) ? ' <span style="color:var(--text-muted);font-size:11px">dopočet</span>'
2091      : isProj(y) ? ' <span style="color:var(--text-muted);font-size:11px">výhled</span>' : '') + '</td>'
2092    + used.map(c => '<td class="num"' + (isFill(y) ? ' style="opacity:.6"' : '') + '>' + f(get(y, c.key)) + '</td>').join('')
2093    + '<td class="num"><strong>' + f(totals[y]) + '</strong></td></tr>').join('');
2094}
2095
2096function drawEuaChart() {
2097  const cv = document.getElementById('eua-chart');
2098  if (!cv || typeof Chart === 'undefined') return;
2099  const pts = (typeof getSeriesPoints === 'function') ? getSeriesPoints('eua') : [];
2100  if (!pts.length) return;
2101  const r = window.__euaRange || '12m';
2102  let start = -Infinity;
2103  if (r !== 'all') { const last = new Date(pts[pts.length-1].date); const d = new Date(last); d.setMonth(d.getMonth() - ({'3m':3,'12m':12}[r] || 12)); start = d.getTime(); }
2104  const czk = window.__euaCcy === 'CZK';
2105  const unit = czk ? 'CZK/t' : 'EUR/t';
2106  const conv = (v, date) => czk ? v * (fxOnDate(date) || fxLatest() || 25) : v;
2107  const data = pts.filter(p => new Date(p.date).getTime() >= start).map(p => ({ x: new Date(p.date).getTime(), y: conv(p.value, p.date) }));
2108  const muted = getCssVar('--text-muted') || '#71717a', border = getCssVar('--border') || '#e4e4e7',
2109        strong = getCssVar('--text-strong') || '#09090b', bgCard = getCssVar('--bg-card') || '#fff';
2110  const col = (typeof getSeriesColor === 'function') ? getSeriesColor
2110('eua') : '#3B5BC6';
2111  if (euaChart) euaChart.destroy();
2112  euaChart = new Chart(cv.getContext('2d'), {
2113    type: 'line', data: { datasets: [{ label: 'EUA', data, borderColor: col, backgroundColor: col + '14', borderWidth: 2, pointRadius: 0, pointHoverRadius: 6, pointHoverBorderWidth: 2, tension: 0.05, fill: false, spanGaps: true, parsing: false }] },
2114    options: { responsive: true, maintainAspectRatio: false, interaction: { mode: 'nearest', intersect: false },
2115      plugins: { legend: { display: false }, tooltip: { backgroundColor: strong, titleColor: bgCard, bodyColor: bgCard, borderColor: border, padding: 12, cornerRadius: 8, displayColors: true, boxPadding: 4,
2116        callbacks: { title: items => items.length ? new Date(items[0].parsed.x).toLocaleDateString('cs-CZ') : '', label: ctx => ctx.parsed.y == null ? '\u2014' : ctx.parsed.y.toFixed(2).replace('.', ',') + ' ' + unit } } },
2117      scales: { y: { type: 'linear', position: 'left', beginAtZero: window.__euaYZero, title: { display: true, text: unit, color: muted, font: {size:11} }, grid: { color: border, lineWidth: 1 }, ticks: { color: muted, font: {size:11} } },
2118        x: { type: 'linear', bounds: 'data', grid: { display: false }, title: { display: false }, afterDataLimits: captureXDataLimits, afterBuildTicks: calendarXTicks, ticks: { color: muted, autoSkip: false, maxRotation: 0, font: {size:11}, callback: adaptiveXTick } } } }
2119  });
2120}
2121
2122let oilChart = null; window.__oilRange = window.__oilRange || '12m'; window.__oilYZero = window.__oilYZero || false;
2123window.__oilUnit = window.__oilUnit || (function(){ try { return localStorage.getItem('oilUnit') || 'usd_bbl'; } catch(e){ return 'usd_bbl'; } })();
2124// Jednotky ropy — ověřené konstanty (EIA/BP): 1 bbl = 158,987 l; bbl/t Brent 7,53 / WTI 7,61; 6,12 GJ/bbl
2125const OIL_BBL_PER_T = { brent: 7.53, wti: 7.61 };
2126const OIL_L_PER_BBL = 158.987, OIL_GJ_PER_BBL = 6.12;
2127const OIL_UNITS = {
2128  usd_bbl: { label: 'USD/bbl', dec: 2, conv: function(v){ return v; } },
2129  usd_t:   { label: 'USD/t',   dec: 0, conv: function(v, sid){ return v * (OIL_BBL_PER_T[sid] || 7.33); } },
2130  usd_gj:  { label: 'USD/GJ',  dec: 2, conv: function(v){ return v / OIL_GJ_PER_BBL; } },
2131  czk_bbl: { label: 'K\u010d/bbl', dec: 0, czk: true, conv: function(v, sid, date){ return v * (usdCzkOn(date) || usdCzkLatest() || 21); } },
2132  czk_l:   { label: 'K\u010d/l',   dec: 2, czk: true, conv: function(v, sid, date){ return v * (usdCzkOn(date) || usdCzkLatest() || 21) / OIL_
2132L_PER_BBL; } }
2133};
2134function oilUnit(){ return OIL_UNITS[window.__oilUnit] ? window.__oilUnit : 'usd_bbl'; }
2135function oilUnitLabel(){ return OIL_UNITS[oilUnit()].label; }
2136function oilConv(v, sid, date){ if (v == null || !isFinite(v)) return v; return OIL_UNITS[oilUnit()].conv(v, sid, date); }
2137function oilFmt(v){ return (v == null || !isFinite(v)) ? '\u2014' : fmtNum(v, OIL_UNITS[oilUnit()].dec); }
2138function setOilUnit(u){ if (!OIL_UNITS[u]) return; window.__oilUnit = u; try { localStorage.setItem('oilUnit', u); } catch(e){} const sel = document.getElementById('oil-unit'); if (sel && sel.value !== u) sel.value = u; if (typeof updateLatestPriceTables === 'function') updateLatestPriceTables(); drawOilChart(); }
2139function setOilRange(btn, r) {
2140  window.__oilRange = r;
2141  if (btn && btn.parentElement) btn.parentElement.querySelectorAll('[data-oilrange]').forEach(b => b.classList.toggle('active', b === btn));
2142  drawOilChart();
2143}
2144function toggleOilYZero(btn) { window.__oilYZero = !window.__oilYZero; if (btn) btn.classList.toggle('active', window.__oilYZero); drawOilChart(); }
2145function drawOilChart() {
2146  const cv = document.getElementById('oil-chart');
2147  if (!cv || typeof Chart === 'undefined') return;
2148  const SERIES = [{ id: 'brent', label: 'Brent' }, { id: 'wti', label: 'WTI' }];
2149  const all = SERIES.map(sr => ({ ...sr, pts: (typeof getSeriesPoints === 'function') ? getSeriesPoints(sr.id) : [] })).filter(sr => sr.pts.length);
2150  if (!all.length) return;
2151  const r = window.__oilRange || '12m';
2152  let start = -Infinity;
2153  const anchor = all[0].pts;
2154  const lastD = new Date(anchor[anchor.length-1].date);
2155  if (r === 'ytd') { start = Date.UTC(lastD.getUTCFullYear(), 0, 1); }
2156  else if (r !== 'all') { const d = new Date(lastD); d.setMonth(d.getMonth() - ({'1m':1,'3m':3,'6m':6,'12m':12}[r] || 12)); start = d.getTime(); }
2157  const muted = getCssVar('--text-muted') || '#71717a', border = getCssVar('--border') || '#e4e4e7',
2158        strong = getCssVar('--text-strong') || '#09090b', bgCard = getCssVar('--bg-card') || '#fff';
2159  const datasets = all.map(sr => {
2160    const col = (typeof getSeriesColor === 'function') ? getSeriesColor(sr.id) : '#0d9488';
2161    return { label: sr.label, data: sr.pts.filter(pp => new Date(pp.date).getTime() >= start).map(pp => ({ x: new Date(pp.date).getTime(), y: oilConv(pp.value, sr.id, pp.date) })), borderColor: col, backgroundColor: col + '14', borderWidth: 2, pointRadius: 0, pointHoverRadius: 6, pointHoverBorderWidth: 2, tension: 0.05, fill: false, spanGaps: true, parsing: false };
2162  });
2163  if (oilChart) oilChart.destroy();
2164  oilChart = new Chart(cv.getContext('2d'), {
2165    type: 'line', data: { datasets },
2166    options: { responsive: true, maintainAspectRatio: false, interaction: { mode: 'nearest', intersect: false },
2167      plugins: { legend: { display: true, position: 'top', labels: { color: muted, usePointStyle: true, pointStyle: 'line', boxWidth: 22, padding: 14, font: {size:12} } },
2168        tooltip: { backgroundColor: strong, titleColor: bgCard, bodyColor: bgCard, borderColor: border, padding: 12, cornerRadius: 8, displayColors: true, boxPadding: 4,
2169          callbacks: { title: items => items.length ? new Date(items[0].parsed.x).toLocaleDateString('cs-CZ') : '', label: ctx => ctx.dataset.label + ': ' + (ctx.parsed.y == null ? '\u2014' : fmtNum(ctx.parsed.y, OIL_UNITS[oilUnit()].dec) + ' ' + oilUnitLabel()) } } },
2170      scales: { y: { type: 'linear', position: 'left', beginAtZero: window.__oilYZero, title: { display: true, text: oilUnitLabel(), color: muted, font: {size:11} }, grid: { color: border, lineWidth: 1 }, ticks: { color: muted, font: {size:11} } },
2171        x: { type: 'linear', bounds: 'data', grid: { display: false }, title: { display: false }, afterDataLimits: captureXDataLimits, afterBuildTicks: calendarXTicks, ticks: { color: muted, autoSkip: false, maxRotation: 0, font: {size:11}, callback: adaptiveXTick } } } }
2172  });
2173}
2174function __runOriginalInit() {
2175  // Dynamicky nastav dnešní datum v hlavičce + patičce (česká lokalizace bez závislosti na Intl)
2176  (function updateDateLabels() {
2177    const days = ['neděle','pondělí','úterý','středa','čtvrtek','pátek','sobota'];
2178    const months = ['ledna','února','března','dubna','května','června','července','srpna','září','října','listopadu','prosince'];
2179    const d = new Date();
2180    const dayName = days[d.getDay()];
2181    const dayNum = d.getDate();
2182    const monthName = months[d.getMonth()];
2183    const year = d.getFullYear();
2184    const headerEl = document.getElementById('report-date');
2185    if (headerEl) headerEl.textContent = `${dayName} ${dayNum}. ${monthName} ${year}`;
2186    const footerEl = document.getElementById('generated-at');
2187    if (footerEl) footerEl.textContent = `Vygenerováno: ${dayNum}. ${monthName} ${year}.`;
2188  })();
2189  updateThemeIcon();
2190  // Jediné tlačítko „Aktualizovat data": spustí nový výpočet dat (je-li dostupný),
2191  // počká na dokončení a zobrazí čerstvá data; jinak aspoň přenačte aktuální stav z DB.
2192  (function setupRefreshButtons() {
2193    const btn = document.getElementById('refresh-btn');
2194    if (btn) btn.title = 'Spustí nový výpočet dat a po dokončení (~1–2 min) zobrazí čerstvá data.';
2195  })();
2196  // Aplikuj barevné tečky na toggle tlačítka
2197  document.querySelectorAll('.range-controls button[data-dot]').forEach(b => {
2198    b.style.setProperty('--dot-color', b.getAttribute('data-dot'));
2199  });
2200  // OTE day picker — default na dnešek (pokud máme data), jinak poslední dostupný
2201  const otePicker = document.getElementById('ote-date-picker');
2202  if (otePicker) {
2203    const days = oteAvailableDays();
2204    const todayISO = new Date().toISOString().slice(0,10);
2205    if (days.includes(todayISO)) otePicker.value = todayISO;
2206    else if (days.length) otePicker.value = days[days.length-1];
2207    if (days.length) { otePicker.min = days[0]; otePicker.max = days[days.length-1]; }
2208  }
2209  const oteCountEl = document.getElementById('ote-history-count');
2210  if (oteCountEl) oteCountEl.textContent = Object.keys(window.ote15Data || {}).length;
2211  // Každé volání izolovaně — chybějící sekce (jiná stránka) nesmí shodit zbytek initu.
2212  const __s = (fn) => { try { fn(); } catch (e) { console.warn('init skip:', e && e.message); } };
2213  __s(drawOteChart);
2214  __s(() => { if (typeof drawFecChart === 'function') drawFecChart(); });
2215  __s(() => drawStorageGauge(window.__lastStoragePct || 33));
2216  __s(updateLatestPriceTables);
2217  __s(updateFreshnessBadges);
2218  __s(updateOverallFreshness);
2219  __s(() => drawPriceChart('el', '1m'));
2220  __s(() => drawPriceChart('gas', '1m'));
2221  __s(drawEuaChart);
2222  { const _ou = document.getElementById('oil-unit'); if (_ou) _ou.value = oilUnit(); }
2223  __s(drawOilChart);
2224  __s(drawStorageHistoryChart);
2225  __s(drawHeatChart);
2226  __s(drawTapChart);
2227  __s(() => { if (typeof drawZevoChart === 'function') drawZevoChart(); });
2228  __s(drawSpotHourlyChart);
2229  __s(drawSpotHourChart);
2230  __s(drawSpotDowChart);
2231  __s(drawForwardSpotChart);
2232  __s(drawBalancingChart);
2233  __s(drawEuStorage);
2234  __s(drawImportsChart);
2235  __s(() => {
2236    if (typeof setEntsoeDate === 'function') { setEntsoeDate('latest'); }
2237    else { drawEntsoeGenChart(); drawEntsoeFlowsChart(); }
2238  });
2239  __s(() => { if (typeof setEntsoeUnitDate === 'function') setEntsoeUnitDate('latest'); });
2240  __s(() => { if (typeof drawHeatFuelChart === 'function') drawHeatFuelChart(); });
2241  __s(() => { if (typeof drawHeatPlantsChart === 'function') drawHeatPlantsChart(); });
2242  // Vypiš počet bodů v databázi
2243  const dataCountEl = document.getElementById('data-counter');
2244  if (dataCountEl) {
2245    const parts = SERIES_DEF.map(s => {
2246      const n = getSeriesPoints(s.id).length;
2247      return s.label + ' = <strong>' + n + '</strong>';
2248    });
2249    dataCountEl.innerHTML = parts.join(' &middot; ');
2250  }
2251  // init custom date pickers pro oba grafy (el-sekce z el27, gas-sekce z gas27)
2252  function initDatePickers(suffix, sid) {
2253    const pts = getSeriesPoints(sid);
2254    if (!pts.length) return;
2255    const first = pts[0].date, last = pts[pts.length-1].date;
2256    const fromEl = document.getElementById('date-from-' + suffix);
2257    const toEl = document.getElementById('date-to-' + suffix);
2258    if (fromEl) { fromEl.min = first; fromEl.max = last; }
2259    if (toEl)   { toEl.min = first;   toEl.max = last;   toEl.value = last; }
2260  }
2261  initDatePickers('el', 'el27');
2262  initDatePickers('gas', 'gas27');
2263}
2264
2265let oteChart = null;
2266let oteLoadingDate = null;
2267function addDays(dateStr, n) {
2268  const d = new Date(dateStr); d.setDate(d.getDate() + n);
2269  return d.toISOString().slice(0,10);
2270}
2271async function drawOteChart() {
2272  const ctx = document.getElementById('ote-chart').getContext('2d');
2273  const accent = getCssVar('--accent') || '#dc2626';
2274  const muted = getCssVar('--text-muted') || '#71717a';
2275  const border = getCssVar('--border') || '#e4e4e7';
2276  const strong = getCssVar('--text-strong') || '#09090b';
2277  const bgCard = getCssVar('--bg-card') || '#fff';
2278  const picker = document.getElementById('ote-date-picker');
2279  const selectedDate = (picker && picker.value) ? picker.value : oteData.date;
2280  const tomorrowDate = addDays(selectedDate, 1);
2281  const todayISO = new Date().toISOString().slice(0,10);
2282  const noteEl = document.getElementById('ote-date-note');
2283  // Pokus se mít data pro vybraný den
2284  const hasData = Array.isArray(window.ote15Data[selectedDate]) && window.ote15Data[selectedDate].some(v => v !== null && v !== undefined && isFinite(v));
2285  if (!hasData) {
2286    if (window.cowork && window.cowork.callMcpTool) {
2287      // V Cowork prostředí lze den dotáhnout z OTE-CR
2288      if (noteEl) noteEl.textContent = 'Načítám ' + selectedDate + ' z OTE-CR…';
2289      if (oteLoadingDate !== selectedDate) {
2290        oteLoadingDate = selectedDate;
2291        const r = await loadOteFromCR(selectedDate);
2292        oteLoadingDate = null;
2293        if (!r.ok && noteEl) noteEl.textContent = '⚠ ' + r.error;
2294        else if (noteEl) noteEl.textContent = '✓ Stáhnuto z OTE-CR a uloženo do databáze.';
2295      }
2296    } else {
2297      // Mimo Cowork: pro tento den data nejsou → skoč na poslední dostupný den
2298      const days = oteAvailableDays();
2299      if (days.length && selectedDate !== days[days.length-1]) {
2300        if (picker) picker.value = days[days.length-1];
2301        return drawOteChart();
2302      }
2303      if (noteEl) noteEl.textContent = 'Pro tento den nejsou data; zobrazuji poslední dostupný den.';
2304    }
2305  } else {
2306    if (noteEl) noteEl.textContent = '';
2307  }
2308  // Auto-fetch zítřek pokud uživatel volí dnešek a zítřek tam není
2309  if (selectedDate === todayISO && !window.ote15Data[tomorrowDate]) {
2310    if (oteLoadingDate !== tomorrowDate) {
2311      oteLoadingDate = tomorrowDate;
2312      loadOteFromCR(tomorrowDate).then(r => {
2313        oteLoadingDate = null;
2314        if (r.ok) drawOteChart();
2315      });
2316    }
2317  }
2318  // Sestav datasety
2319  const datasets = [];
2320  const dayData = window.ote15Data[selectedDate];
2321  // ===== Statistiky (Base/Peak/Offpeak) + datum — počítané z vybraného dne, ignorují chybějící hodnoty =====
2322  const avgValid = arr => { const v = (arr || []).filter(x => x !== null && x !== undefined && isFinite(x)); return v.length ? v.reduce((a, b) => a + b, 0) / v.length : null; };
2323  const fmt2 = v => v === null ? '—' : v.toFixed(2).replace('.', ',');
2324  const baseEl = document.getElementById('ote-base'), peakEl = document.getElementById('ote-peak'), offEl = document.getElementById('ote-offpeak'), labelEl = document.getElementById('ote-base-label');
2325  if (dayData) {
2326    const peakSlots = dayData.filter((_, i) => { const h = Math.floor(i / 4); return h >= 8 && h < 20; });
2327    const offSlots = dayData.filter((_, i) => { const h = Math.floor(i / 4); return !(h >= 8 && h < 20); });
2328    const base = avgValid(dayData), peak = avgValid(peakSlots), off = avgValid(offSlots);
2329    const missing = dayData.filter(x => x === null || x === undefined || !isFinite(x)).length;
2330    const _r = _czk('ote') ? (fxOnDate(selectedDate) || 1) : 1;
2331    if (baseEl) baseEl.textContent = fmt2(base == null ? null : base * _r) + ' ' + ccyUnit('ote');
2332    if (peakEl) peakEl.textContent = fmt2(peak == null ? null : peak * _r);
2333    if (offEl) offEl.textContent = fmt2(off == null ? null : off * _r);
2334    if (labelEl) {
2335      const dstr = new Date(selectedDate + 'T00:00:00').toLocaleDateString('cs-CZ');
2336      labelEl.textContent = 'Base Load — ' + dstr + (selectedDate === todayISO ? ' (dnes)' : '') + (missing ? ' · chybí ' + missing + '/96 hodnot' : '');
2337    }
2338  } else {
2339    if (baseEl) baseEl.textContent = '— ' + ccyUnit('ote');
2340    if (peakEl) peakEl.textContent = '—';
2341    if (offEl) offEl.textContent = '—';
2342    if (labelEl) labelEl.textContent = 'Base Load — ' + new Date(selectedDate + 'T00:00:00').toLocaleDateString('cs-CZ') + (selectedDate === todayISO ? ' (dnes)' : '') + ' · data nedostupná';
2343  }
2344  if (dayData) {
2345    datasets.push({
2346      label: selectedDate + ' (15min)',
2347      data: dayData.map(v => cvDate(v, selectedDate, 'ote')), borderColor: accent, backgroundColor: hexToRgba(accent, 0.12),
2348      fill: true, tension: 0.25, pointRadius: 0, pointHoverRadius: 5, borderWidth: 2, spanGaps: true
2349    });
2350  }
2351  const nextData = window.ote15Data[tomorrowDate];
2352  if (nextData) {
2353    const lighter = '#f97316'; // zítřek = oranžová pro odlišení
2354    datasets.push({
2355      label: tomorrowDate + ' (D+1)',
2356      data: nextData.map(v => cvDate(v, tomorrowDate, 'ote')), borderColor: lighter, backgroundColor: hexToRgba(lighter, 0.08),
2357      fill: false, tension: 0.25, pointRadius: 0, pointHoverRadius: 5, borderWidth: 2, borderDash: [4, 4]
2358    });
2359  }
2360  if (oteChart) oteChart.destroy();
2361  oteChart = new Chart(ctx, {
2362    type: 'line',
2363    data: { labels: oteData.hours, datasets: datasets },
2364    options: {
2365      responsive: true, maintainAspectRatio: false,
2366      interaction: { mode: 'index', intersect: false },
2367      plugins: {
2368        legend: { display: true, position: 'top', labels: { color: muted, font: {size: 12, weight: '500'}, usePointStyle: true, pointStyle: 'rectRounded', boxWidth: 12 } },
2369        tooltip: { backgroundColor: strong, titleColor: bgCard, bodyColor: bgCard, padding: 10, cornerRadius: 8,
2370          callbacks: { label: c => c.dataset.label + ': ' + (c.parsed.y == null ? '—' : c.parsed.y.toFixed(2).replace('.', ',') + ' ' + ccyUnit('ote')) } }
2371      },
2372      scales: {
2373        y: { title: { display: true, text: ccyUnit('ote'), color: muted }, grid: { color: border, lineWidth: 1 }, ticks: { color: muted } },
2374        x: { title: { display: true, text: 'Čas (15min intervaly)', color: muted }, grid: { display: false }, ticks: { color: muted, autoSkip: true, maxTicksLimit: 13, callback: function(v, i) { return i % 4 === 0 ? oteData.hours[i] : ''; } } }
2375      }
2376    }
2377  });
2378}
2379function hexToRgba(hex, alpha) {
2380  hex = (hex || '#000').replace('#','');
2381  if (hex.length === 3) hex = hex.split('').map(c=>c+c).join('');
2382  const num = parseInt(hex, 16);
2383  return `rgba(${(num>>16)&255},${(num>>8)&255},${num&255},${alpha})`;
2384}
2385
2386function drawStorageGauge(pct) {
2387  window.__lastStoragePct = pct;
2388  const canvas = document.getElementById('storage-gauge');
2389  const ctx = canvas.getContext('2d');
2390  // HiDPI/retina: render v plném rozlišení displeje, jinak je kresba rozmazaná
2391  const dpr = window.devicePixelRatio || 1;
2392  const cssW = 220, cssH = 220;
2393  canvas.width = cssW * dpr;
2394  canvas.height = cssH * dpr;
2395  canvas.style.width = cssW + 'px';
2396  canvas.style.height = cssH + 'px';
2397  ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
2398  ctx.clearRect(0, 0, cssW, cssH);
2399  const cx = cssW / 2, cy = cssH / 2;
2400  const r = 88;
2401  ctx.lineWidth = 18;
2402  ctx.lineCap = 'round';
2403  // Background ring
2404  ctx.beginPath();
2405  ctx.strokeStyle = getCssVar('--bg-subtle') || '#e4e4e7';
2406  ctx.arc(cx, cy, r, -Math.PI/2, 1.5*Math.PI);
2407  ctx.stroke();
2408  // Filled portion
2409  const angle = -Math.PI/2 + (pct/100) * 2*Math.PI;
2410  ctx.beginPath();
2411  ctx.strokeStyle = pct < 30 ? (getCssVar('--gauge-low') || '#dc2626') : (pct < 60 ? (getCssVar('--gauge-mid') || '#f59e0b') : (getCssVar('--gauge-high') || '#16a34a'));
2412  ctx.arc(cx, cy, r, -Math.PI/2, angle);
2413  ctx.stroke();
2414  // Hodnota se zobrazuje vedle v #storage-pct — v canvasu ji nekreslíme (duplikovala se)
2415}
2416
2417function filterSeriesByRange(seriesId, range) {
2418  const start = rangeStart(range);
2419  const end = TODAY;
2420  return getSeriesPoints(seriesId).filter(p => {
2421    const dt = new Date(p.date);
2422    return dt >= start && dt <= end;
2423  });
2424}
2425
2426function filterSeriesByDates(seriesId, fromStr, toStr) {
2427  return getSeriesPoints(seriesId).filter(p => p.date >= fromStr && p.date <= toStr);
2428}
2429
2430// Barvy: elektřina = červené odstíny, plyn = žluto-zlaté odstíny.
2431// Dva paletové fallbacky pro light/dark — JS vybere podle aktivního tématu.
2432const COLOR_PALETTE = {
2433  light: {
2434    el26: '#7f1d1d',  // tmavě bordó
2435    el27: '#dc2626',  // primární červená
2436    el28: '#ef4444',  // střední červená
2437    el29: '#fb7185',  // růžovo-červená
2438    el_nextyear: '#ff1a1a',  // výrazná červená — next year elektřina
2439    gasTTF: '#b45309',  // tmavě zlatá
2440    gas27: '#f59e0b',  // amber
2441    gas28: '#fbbf24',  // žlutá
2442    gas29: '#fde047',  // světle žlutá
2443    gas_nextyear: '#ffcc00',  // výrazná žlutá — next year plyn
2444    eua: '#3B5BC6',  // emisní povolenky EUA (modrá)
2445    brent: '#0d9488',  // ropa Brent (teal)
2446    wti: '#8b5cf6'  // ropa WTI (violet)
2447  },
2448  dark: {
2449    el26: '#fca5a5',  // světle růžová
2450    el27: '#ef4444',
2451    el28: '#f87171',
2452    el29: '#fda4af',
2453    el_nextyear: '#ff5252',  // výrazná červená — next year elektřina (dark)
2454    gasTTF: '#fbbf24',
2455    gas27: '#fcd34d',
2456    gas28: '#fde047',
2457    gas29: '#fef08a',
2458    gas_nextyear: '#ffe14d',  // výrazná žlutá — next year plyn (dark)
2459    eua: '#7C9BF0',  // emisní povolenky EUA (dark)
2460    brent: '#2dd4bf',  // ropa Brent (dark)
2461    wti: '#c4b5fd'  // ropa WTI (dark)
2462  }
2463};
2464function getSeriesColor(id) {
2465  const theme = document.documentElement.getAttribute('data-theme') || 'light';
2466  return (COLOR_PALETTE[theme] || COLOR_PALETTE.light)[id];
2467}
2468const SERIES_DEF = [
2469  { id: 'el_nextyear', key: 'el_nextyear', label: 'Elektřina Next year' },
2470  { id: 'el26', key: 'el26', label: 'Elektřina Cal-26' },
2471  { id: 'el27', key: 'el27', label: 'Elektřina Cal-27' },
2472  { id: 'el28', key: 'el28', label: 'Elektřina Cal-28' },
2473  { id: 'el29', key: 'el29', label: 'Elektřina Cal-29' },
2474  { id: 'gas_nextyear', key: 'gas_nextyear', label: 'Plyn Next year' },
2475  { id: 'gasTTF', key: 'gasTTF', label: 'Plyn TTF front-month' },
2476  { id: 'gas27', key: 'gas27', label: 'Plyn CZ Cal-27' },
2477  { id: 'gas28', key: 'gas28', label: 'Plyn CZ Cal-28' },
2478  { id: 'gas29', key: 'gas29', label: 'Plyn CZ Cal-29' }
2479];
2480
2481function buildDatasetsForRange(range, chartKey) {
2482  const hiddenMap = (chartKey && priceChartState[chartKey] && priceChartState[chartKey].hidden) || {};
2483  return SERIES_DEF.map(s => {
2484    const hidden = !!hiddenMap[s.id];
2485    const pts = filterSeriesByRange(s.id, range);
2486    return {
2487      label: s.label,
2488      data: pts.map(p => ({ x: new Date(p.date).getTime(), y: cvDate(p.value, p.date, chartKey) })),
2489      borderColor: getSeriesColor(s.id),
2490      backgroundColor: getSeriesColor(s.id) + '14',
2491      pointRadius: 0,
2492      pointHoverRadius: 6,
2493      pointHoverBorderWidth: 2,
2494      borderWidth: 2,
2495      borderDash: s.id === 'gasTTF' ? [6, 4] : [],
2496      tension: 0.05,
2497      hidden: hidden,
2498      spanGaps: true,
2499      showLine: true,
2500      parsing: false
2501    };
2502  });
2503}
2504
2505function buildDatasetsForDates(fromStr, toStr, chartKey) {
2506  const hiddenMap = (chartKey && priceChartState[chartKey] && priceChartState[chartKey].hidden) || {};
2507  return SERIES_DEF.map(s => {
2508    const hidden = !!hiddenMap[s.id];
2509    const pts = filterSeriesByDates(s.id, fromStr, toStr);
2510    return {
2511      label: s.label,
2512      data: pts.map(p => ({ x: new Date(p.date).getTime(), y: cvDate(p.value, p.date, chartKey) })),
2513      borderColor: getSeriesColor(s.id),
2514      backgroundColor: getSeriesColor(s.id) + '14',
2515      pointRadius: 0,
2516      pointHoverRadius: 6,
2517      pointHoverBorderWidth: 2,
2518      borderWidth: 2,
2519      borderDash: s.id === 'gasTTF' ? [6, 4] : [],
2520      tension: 0.05,
2521      hidden: hidden,
2522      spanGaps: true,
2523      showLine: true,
2524      parsing: false
2525    };
2526  });
2527}
2528
2529function priceChartCommonOptions(chartKey) {
2530  const yZero = !!(chartKey && priceChartState[chartKey] && priceChartState[chartKey].yZero);
2531  const muted = getCssVar('--text-muted') || '#71717a';
2532  const strong = getCssVar('--text-strong') || '#09090b';
2533  const border = getCssVar('--border') || '#e4e4e7';
2534  const bgCard = getCssVar('--bg-card') || '#fff';
2535  return {
2536    responsive: true,
2537    maintainAspectRatio: false,
2538    interaction: { mode: 'nearest', intersect: false },
2539    plugins: {
2540      // Vestavěná legenda vypnuta — používáme vlastní HTML legendu (elektřina / plyn na samostatných řádcích).
2541      legend: { display: false },
2542      tooltip: {
2543        backgroundColor: strong, titleColor: bgCard, bodyColor: bgCard,
2544        borderColor: border, padding: 12, cornerRadius: 8, displayColors: true, boxPadding: 4,
2545        callbacks: {
2546          title: function(items) {
2547            if (!items.length) return '';
2548            return new Date(items[0].parsed.x).toLocaleDateString('cs-CZ');
2549          },
2550          label: function(ctx) {
2551            const v = ctx.parsed.y;
2552            return ctx.dataset.label + ': ' + (v == null ? '—' : v.toFixed(2).replace('.', ',') + ' ' + ccyUnit(chartKey));
2553          }
2554        }
2555      }
2556    },
2557    scales: {
2558      y: {
2559        type: 'linear', position: 'left', beginAtZero: yZero,
2560        title: { display: true, text: ccyUnit(chartKey), color: muted, font: {size: 11} },
2561        grid: { color: border, lineWidth: 1 }, ticks: { color: muted, font: {size: 11} }
2562      },
2563      x: {
2564        type: 'linear',
2565        bounds: 'data',
2566        afterDataLimits: captureXDataLimits, afterBuildTicks: calendarXTicks,
2567        ticks: {
2568          color: muted, autoSkip: false, maxRotation: 0, font: {size: 11},
2569          callback: adaptiveXTick
2570        },
2571        grid: { display: false },
2572        title: { display: false }
2573      }
2574    }
2575  };
2576}
2577
2578// Pomocná funkce — zjistí, ke kterému grafu daný element patří
2579function chartKeyFromEl(el) {
2580  const section = el?.closest('section[data-chart]');
2581  return section?.dataset?.chart || 'el';
2582}
2583function canvasIdFor(key) { return key === 'gas' ? 'price-chart-gas' : 'price-chart-el'; }
2584
2585function togglePriceYZero(chartKey, btn) {
2586  if (!priceChartState[chartKey]) return;
2587  priceChartState[chartKey].yZero = !priceChartState[chartKey].yZero;
2588  if (btn) {
2589    btn.classList.toggle('active', priceChartState[chartKey].yZero);
2590    btn.textContent = priceChartState[chartKey].yZero ? 'Adaptivní Y' : 'Začít od 0';
2591  }
2592  // Pokud je aktivní vlastní rozsah dat, použij applyCustomRange; jinak drawPriceChart
2593  const section = document.querySelector('section[data-chart="' + chartKey + '"]');
2594  const customFrom = section ? section.querySelector('input[id^="date-from-"]') : null;
2595  const customTo = section ? section.querySelector('input[id^="date-to-"]') : null;
2596  const isCustomActive = section && !section.querySelector('.range-controls [data-range].active');
2597  if (isCustomActive && customFrom && customFrom.value && customTo && customTo.value) {
2598    applyCustomRange(customFrom);
2599  } else {
2600    drawPriceChart(chartKey, priceChartState[chartKey].range || '1m');
2601  }
2602}
2603
2604// ===== Měna EUR ⇄ CZK: přepočet denním referenčním kurzem ECB =====
2605window.__CCY = window.__CCY || {};
2606function ccyOf(k){ return window.__CCY[k] || 'EUR'; }
2607function ccyUnit(k){ return ccyOf(k) === 'CZK' ? 'CZK/MWh' : 'EUR/MWh'; }
2608function ccySym(k){ return ccyOf(k) === 'CZK' ? 'Kč' : '€'; }
2609function _czk(k){ return ccyOf(k) === 'CZK'; }
2610function fxOnMs(ms){
2611  const a = window.FX_DAILY;
2612  if (!a || !a.length) return null;
2613  let i = Math.floor((ms - window.FX_BASE_MS) / 86400000);
2614  if (i < 0) i = 0; if (i >= a.length) i = a.length - 1;
2615  return a[i];
2616}
2617function fxOnDate(s){ return fxOnMs(new Date(s + 'T12:00:00Z').getTime()); }
2618function fxLatest(){ const a = window.FX_DAILY; return (a && a.length) ? a[a.length - 1] : null; }
2619function usdCzkOnMs(ms){ const a = window.FX_USD_DAILY; if (!a || !a.length) return null; let i = Math.floor((ms - window.FX_USD_BASE_MS) / 86400000); if (i < 0) i = 0; if (i >= a.length) i = a.length - 1; return a[i]; }
2620function usdCzkOn(ds){ return usdCzkOnMs(new Date(ds + 'T12:00:00Z').getTime()); }
2621function usdCzkLatest(){ const a = window.FX_USD_DAILY; return (a && a.length) ? a[a.length - 1] : null; }
2622function fxYearAvg(y){ const m = window.FX_YEAR_AVG; return (m && m[y]) ? m[y] : fxLatest(); }
2623function fxYearKeyRate(yk){
2624  if (yk === '3yr') { const rs = [2023,2024,2025].map(fxYearAvg).filter(Boolean); return rs.length ? rs.reduce((a,b)=>a+b,0)/rs.length : fxLatest(); }
2625  return fxYearAvg(parseInt(yk, 10));
2626}
2627// Převody EUR → zobrazovaná měna grafu k (EUR ponecháno; CZK = EUR × kurz daného dne / roku)
2628function cvMs(eur, ms, k){ if (eur == null || !isFinite(eur)) return eur; if (!_czk(k)) return eur; const r = fxOnMs(ms); return r ? eur * r : eur; }
2629function cvDate(eur, ds, k){ if (eur == null || !isFinite(eur)) return eur; if (!_czk(k)) return eur; const r = fxOnDate(ds); return r ? eur * r : eur; }
2630function cvRate(eur, rate, k){ if (eur == null || !isFinite(eur)) return eur; if (!_czk(k)) return eur; return rate ? eur * rate : eur; }
2631function toggleCcy(k, btn){
2632  window.__CCY[k] = ccyOf(k) === 'EUR' ? 'CZK' : 'EUR';
2633  document.querySelectorAll('[data-ccy-for="' + k + '"]').forEach(b => {
2634    b.textContent = ccyOf(k) === 'EUR' ? 'Kč' : '€';
2635    b.classList.toggle('active', ccyOf(k) === 'CZK');
2636  });
2637  redrawChartCcy(k);
2638}
2639function redrawChartCcy(k){
2640  if (k === 'el' || k === 'gas') return redrawPriceChart(k);
2641  if (k === 'ote') return drawOteChart();
2642  if (k === 'spot5') return redrawSpot5();
2643  if (k === 'fwd') return drawForwardSpotChart();
2644  if (k === 'hour') return drawSpotHourChart();
2645  if (k === 'dow') return drawSpotDowChart();
2646}
2647function redrawPriceChart(k){
2648  const section = document.querySelector('section[data-chart="' + k + '"]');
2649  const cf = section ? section.querySelector('input[id^="date-from-"]') : null;
2650  const ct = section ? section.querySelector('input[id^="date-to-"]') : null;
2651  const isCustom = section && !section.querySelector('.range-controls [data-range].active');
2652  if (isCustom && cf && cf.value && ct && ct.value) applyCustomRange(cf);
2653  else drawPriceChart(k, (priceChartState[k] && priceChartState[k].range) || '1m');
2654}
2655function redrawSpot5(){
2656  if (__spotRange === 'compare') return drawSpotCompareChart();
2657  if (__spotRange === 'custom') return applySpotCustomRange();
2658  return drawSpotHourlyChart();
2659}
2660
2661function drawPriceChart(key, range) {
2662  key = key || 'el';
2663  priceChartState[key].range = range;
2664  if (priceCharts[key]) priceCharts[key].destroy();
2665  const ctx = document.getElementById(canvasIdFor(key)).getContext('2d');
2666  priceCharts[key] = new Chart(ctx, {
2667    type: 'line',
2668    data: { datasets: buildDatasetsForRange(range, key) },
2669    options: priceChartCommonOptions(key),
2670    plugins: [nextYearGlowPlugin]
2671  });
2672  // legacy alias (other code mohlo používat priceChart)
2673  if (key === 'el') priceChart = priceCharts.el;
2674  renderPriceLegend(key);
2675}
2676
2677// ===== Vlastní HTML legenda grafů 3A/3B =====
2678// 3A (elektřina): řádek 1 = elektřina, řádek 2 = plyn. 3B (plyn): přesně naopak.
2679function renderPriceLegend(key) {
2680  const cont = document.getElementById('price-legend-' + key);
2681  if (!cont) return;
2682  const elSeries = SERIES_DEF.filter(s => s.id.indexOf('el') === 0);
2683  const gasSeries = SERIES_DEF.filter(s => s.id.indexOf('gas') === 0);
2684  const rows = (key === 'gas') ? [gasSeries, elSeries] : [elSeries, gasSeries];
2685  const hiddenMap = (priceChartState[key] && priceChartState[key].hidden) || {};
2686  cont.innerHTML = rows.map(function (group) {
2687    const items = group.map(function (s) {
2688      const hidden = !!hiddenMap[s.id];
2689      const color = getSeriesColor(s.id);
2690      return '<span class="pl-item' + (hidden ? ' pl-hidden' : '') + '" onclick="togglePriceSeries(\'' + key + '\',\'' + s.id + '\')">' +
2691             '<span class="pl-swatch" style="background:' + color + '"></span>' + s.label + '</span>';
2692    }).join('');
2693    return '<div class="price-legend-row">' + items + '</div>';
2694  }).join('');
2695}
2696
2697function togglePriceSeries(key, id) {
2698  const chart = priceCharts[key];
2699  if (!chart) return;
2700  const idx = SERIES_DEF.findIndex(s => s.id === id);
2701  if (idx < 0) return;
2702  const next = !priceChartState[key].hidden[id];
2703  priceChartState[key].hidden[id] = next;
2704  chart.setDatasetVisibility(idx, !next); // visible = !hidden
2705  chart.update();
2706  renderPriceLegend(key);
2707}
2708
2709function toggleSeries(btnOrId, maybeSeriesId) {
2710  // Kompatibilita: starší volání toggleSeries('el27') i nové toggleSeries(this, 'el27')
2711  let btn, seriesId, key;
2712  if (typeof btnOrId === 'string') {
2713    seriesId = btnOrId;
2714    btn = document.querySelector('[data-series="' + seriesId + '"]');
2715    key = chartKeyFromEl(btn);
2716  } else {
2717    btn = btnOrId;
2718    seriesId = maybeSeriesId;
2719    key = chartKeyFromEl(btn);
2720  }
2721  btn.classList.toggle('active');
2722  const idx = SERIES_DEF.findIndex(s => s.id === seriesId);
2723  const chart = priceCharts[key];
2724  if (chart && idx >= 0) {
2725    const meta = chart.getDatasetMeta(idx);
2726    meta.hidden = !btn.classList.contains('active');
2727    chart.update();
2728  }
2729}
2730
2731function setPriceRange(btnOrRange, maybeRange) {
2732  let btn, range, key;
2733  if (typeof btnOrRange === 'string') {
2734    range = btnOrRange;
2735    btn = document.querySelector(`[data-range="${range}"]`);
2736    key = chartKeyFromEl(btn);
2737  } else {
2738    btn = btnOrRange;
2739    range = maybeRange;
2740    key = chartKeyFromEl(btn);
2741  }
2742  // Aktivace tlačítka jen v dané sekci
2743  const section = btn.closest('section[data-chart]');
2744  section.querySelectorAll('.range-controls [data-range]').forEach(b => {
2745    b.classList.toggle('active', b.dataset.range === range);
2746  });
2747  const fromInp = section.querySelector('input[id^="date-from-"]');
2748  const toInp = section.querySelector('input[id^="date-to-"]');
2749  // Naplň OBĚ okýnka skutečným rozsahem (Od i Do), ať jsou vždy vyplněná a reagují na tlačítka.
2750  const probeSid = key === 'gas' ? 'gas27' : 'el27';
2751  const allPts = getSeriesPoints(probeSid);
2752  const firstDate = allPts.length ? allPts[0].date : null;
2753  const lastDate = allPts.length ? allPts[allPts.length-1].date : null;
2754  const rs = rangeStart(range);
2755  const pad = n => String(n).padStart(2, '0');
2756  let fromStr = rs.getFullYear() + '-' + pad(rs.getMonth() + 1) + '-' + pad(rs.getDate());
2757  if (firstDate && fromStr < firstDate) fromStr = firstDate; // neukazuj datum před začátkem dat
2758  if (fromInp) fromInp.value = fromStr;
2759  if (toInp && lastDate) toInp.value = lastDate;
2760  drawPriceChart(key, range);
2761}
2762
2763function applyCustomRange(inp) {
2764  const section = inp.closest('section[data-chart]');
2765  const key = section.dataset.chart;
2766  const from = section.querySelector('input[id^="date-from-"]').value;
2767  const to = section.querySelector('input[id^="date-to-"]').value;
2768  if (!from || !to) return;
2769  section.querySelectorAll('.range-controls [data-range]').forEach(b => b.classList.remove('active'));
2770  if (priceCharts[key]) priceCharts[key].destroy();
2771  const ctx = document.getElementById(canvasIdFor(key)).getContext('2d');
2772  priceCharts[key] = new Chart(ctx, {
2773    type: 'line',
2774    data: { datasets: buildDatasetsForDates(from, to, key) },
2775    options: priceChartCommonOptions(key),
2776    plugins: [nextYearGlowPlugin]
2777  });
2778  if (key === 'el') priceChart = priceCharts.el;
2779  renderPriceLegend(key);
2780}
2781
2782// Barvy roků sjednocené se sekcí 5 (SPOT_YEAR_COLORS)
2783const YEAR_COLORS = {
2784  '2020': '#737373',  // šedá
2785  '2021': '#8b5cf6',  // fialová
2786  '2022': '#3b82f6',  // modrá
2787  '2023': '#06b6d4',  // tyrkysová
2788  '2024': '#10b981',  // zelená
2789  '2025': '#f59e0b',  // oranžová
2790  '2026': '#dc2626'   // červená
2791};
2792
2793function drawStorageHistoryChart() {
2794  const muted = getCssVar('--text-muted') || '#71717a';
2795  const border = getCssVar('--border') || '#e4e4e7';
2796  const strong = getCssVar('--text-strong') || '#09090b';
2797  const bgCard = getCssVar('--bg-card') || '#fff';
2798  const datasets = Object.keys(storageHistory).map(year => ({
2799    label: year,
2800    data: storageHistory[year].map(p => ({ x: p.week, y: p.value })),
2801    borderColor: paletteColor(YEAR_COLORS[year]),
2802    backgroundColor: paletteColor(YEAR_COLORS[year]) + '14',
2803    pointRadius: 0,
2804    pointHoverRadius: 4,
2805    borderWidth: year === '2026' ? 2.5 : 1.8,
2806    tension: 0.35,
2807    hidden: false
2808  }));
2809  // Průměrná křivka přes kompletní roky 2020–2025 (čárkovaně, neutrální barva).
2810  // Přidává se NA KONEC datasets — legend-toggle dle indexu roků zůstává funkční.
2811  (function addAvgDataset() {
2812    const fullYears = Object.keys(storageHistory).filter(y => y >= '2020' && y <= '2025');
2813    if (!fullYears.length) return;
2814    const sum = {}, n = {};
2815    fullYears.forEach(y => storageHistory[y].forEach(p => {
2816      const w = Math.round(p.week);
2817      sum[w] = (sum[w] || 0) + p.value; n[w] = (n[w] || 0) + 1;
2818    }));
2819    const avgData = Object.keys(sum).map(Number).sort((a, b) => a - b)
2820      .map(w => ({ x: w, y: Math.round(sum[w] / n[w] * 10) / 10 }));
2821    datasets.push({
2822      label: 'Průměr 2020–2025', data: avgData,
2823      borderColor: muted, backgroundColor: 'transparent',
2824      borderDash: [6, 4], pointRadius: 0, pointHoverRadius: 3,
2825      borderWidth: 2, tension: 0.35, fill: false
2826    });
2827  })();
2828  if (storageHistoryChart) storageHistoryChart.destroy();
2829  const _shcEl = document.getElementById('storage-history-chart');
2830  if (!_shcEl) return;
2831  const ctx = _shcEl.getContext('2d');
2832  storageHistoryChart = new Chart(ctx, {
2833    type: 'line',
2834    data: { datasets: datasets },
2835    options: {
2836      responsive: true, maintainAspectRatio: false,
2837      interaction: { mode: 'nearest', intersect: false },
2838      plugins: {
2839        legend: { display: true, position: 'top', labels: { color: muted, usePointStyle: true, pointStyle: 'rectRounded', boxWidth: 12, boxHeight: 8, padding: 12, font: {size: 12, weight: '500'} } },
2840        tooltip: { backgroundColor: strong, titleColor: bgCard, bodyColor: bgCard, padding: 10, cornerRadius: 8,
2841          callbacks: { title: items => 'Týden ' + Math.round(items[0].parsed.x),
2842                       label: item => item.dataset.label + ': ' + item.parsed.y.toFixed(1).replace('.', ',') + ' %' } }
2843      },
2844      scales: {
2845        x: {
2846          type: 'linear', min: 1, max: 53.2,
2847          title: { display: true, text: 'Kalendářní týden', color: muted, font: {size: 11} },
2848          ticks: { stepSize: 4, color: muted, font: {size: 11} },
2849          grid: { display: false }
2850        },
2851        y: {
2852          min: 0, max: 100,
2853          title: { display: true, text: '% zaplnění (working gas volume)', color: muted, font: {size: 11} },
2854          grid: { color: border, lineWidth: 1 },
2855          ticks: { color: muted, font: {size: 11} }
2856        }
2857      }
2858    }
2859  });
2860}
2861
2862// ===== Ceny tepla v krajích ČR — vážené průměry (CZK/GJ vč. DPH) =====
2863// Vypočítáno váženým průměrem (cena × dodané GJ) přes všechny cenové lokality v kraji.
2864// Extrahováno z PDF přes pdf.js + parser SPZ-kódů (A/B/C/E/H/J/K/L/M/P/S/T/U/Z).
2865// Zdroj 2024: "Výsledné ceny tepelné energie v ČR 2024" (51 str., 3 557 lokalit).
2866// Zdroj 2025: "Předběžné ceny tepelné energie v ČR k 1. 1. 2025" (56 str., 3 700 lokalit).
2867const heatData = {source: "ERÚ", defaultYear: "2025", byYear: {}};
2868let __heatYear = heatData.defaultYear;
2869
2870let heatChart = null;
2871function setHeatYear(year) {
2872  __heatYear = year;
2873  document.querySelectorAll('.range-controls [data-heat-year]').forEach(b => {
2874    b.classList.toggle('active', b.dataset.heatYear === year);
2875  });
2876  drawHeatChart();
2877}
2878// ===== Sekce 17: ZEVO ČR =====
2879let zevoChart = null;
2880window.__zevoYear = 2024;
2881window.__zevoMetric = 'waste';
2882window.__zevoStatus = 'all';
2883function setZevoYear(y) {
2884  window.__zevoYear = y;
2885  document.querySelectorAll('[data-zevo-year]').forEach(b => b.classList.toggle('active', parseInt(b.dataset.zevoYear) === y));
2886  drawZevoChart();
2887}
2888function setZevoMetric(m) {
2889  window.__zevoMetric = m;
2890  document.querySelectorAll('[data-zevo-metric]').forEach(b => b.classList.toggle('active', b.dataset.zevoMetric === m));
2891  drawZevoChart();
2892}
2893function setZevoStatus(s) {
2894  window.__zevoStatus = s;
2895  document.querySelectorAll('[data-zevo-status]').forEach(b => b.classList.toggle('active', b.dataset.zevoStatus === s));
2896  drawZevoChart();
2897}
2898function drawZevoChart() {
2899  if (!window.zevoData) return;
2900  const muted = getCssVar('--text-muted') || '#71717a';
2901  const border = getCssVar('--border') || '#e4e4e7';
2902  const strong = getCssVar('--text-strong') || '#09090b';
2903  const bgCard = getCssVar('--bg-card') || '#fff';
2904  const accent = getCssVar('--accent') || '#dc2626';
2905  // Pro plánované/rozestavěné používáme „snímek" z year=2024 řádku (status, expected_year, kapacita)
2906  // ale data o produkci se ukazují jen pro řádky odpovídající vybranému roku.
2907  const statusFilter = window.__zevoStatus;
2908  // Pro každé ZEVO: vezmi řádek pro vybraný rok, pokud neexistuje a status≠operational, vezmi 2024 snímek
2909  const byId = {};
2910  window.zevoData.forEach(r => {
2911    if (!byId[r.zevo_id]) byId[r.zevo_id] = {};
2912    byId[r.zevo_id][r.year] = r;
2913  });
2914  const rows = Object.values(byId).map(yMap => {
2915    const yearRow = yMap[window.__zevoYear];
2916    if (yearRow) return yearRow;
2917    // pro neoperační ZEVO může chybět řádek pro 2023 nebo 2025 → použij 2024 jako fallback
2918    return yMap[2024] || Object.values(yMap)[0];
2919  }).filter(r => {
2920    if (statusFilter === 'all') return true;
2921    return r.status === statusFilter;
2922  });
2923  // Sort: operational první, pak under_construction, pak planned; v rámci skupiny podle kapacity desc
2924  const statusOrder = { operational: 0, under_construction: 1, planned: 2 };
2925  rows.sort((a, b) => {
2926    const sa = statusOrder[a.status] ?? 99, sb = statusOrder[b.status] ?? 99;
2927    if (sa !== sb) return sa - sb;
2928    return (parseFloat(b.capacity_t_year) || 0) - (parseFloat(a.capacity_t_year) || 0);
2929  });
2930  // Render tabulka pro daný rok
2931  const tbody = document.getElementById('zevo-tbody');
2932  if (tbody) {
2933    const fmt = n => n === null || n === undefined ? '—' : Math.round(parseFloat(n)).toLocaleStr
2933ing('cs-CZ');
2934    const statusBadge = r => {
2935      if (r.status === 'operational') return '<span style="background:#dcfce7;color:#166534;padding:2px 8px;border-radius:10px;font-size:11px;font-weight:600">v provozu</span>';
2936      if (r.status === 'under_construction') return `<span style="background:#fef3c7;color:#92400e;padding:2px 8px;border-radius:10px;font-size:11px;font-weight:600">ve výstavbě${r.expected_year ? ' → ' + r.expected_year : ''}</span>`;
2937      if (r.status === 'planned') return `<span style="background:#e0e7ff;color:#3730a3;padding:2px 8px;border-radius:10px;font-size:11px;font-weight:600">plán${r.expected_year ? ' ' + r.expected_year : ''}</span>`;
2938      return r.status;
2939    };
2940    // V neoperačních řádcích nemá smysl zobrazovat produkční data pro vybraný rok (ještě nejedou)
2941    const dataCells = r => {
2942      if (r.status !== 'operational' || r.year !== window.__zevoYear) {
2943        return `<td class="num">${fmt(r.capacity_t_year)}</td><td class="num" colspan="3" style="color:var(--text-muted);font-style:italic">${r.status === 'operational' ? 'data za ' + window.__zevoYear + ' nedostupná' : 'zatím neprovozuje'}</td>`;
2944      }
2945      return `<td class="num">${fmt(r.capacity_t_year)}</td><td class="num">${fmt(r.waste_tonnes)}</td><td class="num">${fmt(r.electricity_mwh)}</td><td class="num">${fmt(r.heat_tj)}</td>`;
2946    };
2947    tbody.innerHTML = rows.map(r => `<tr>
2948      <td><strong>${r.zevo_name}</strong><br><span style="color:var(--text-muted);font-size:11px">${r.city || ''}</span></td>
2949      <td>${r.operator || '—'}</td>
2950      <td class="num">${statusBadge(r)}</td>
2951      ${dataCells(r)}
2952    </tr>`).join('');
2953  }
2954  // Render bar chart pro vybranou metriku — jen operational ZEVO s daty pro daný rok
2955  const metric = window.__zevoMetric;
2956  const colMap = { waste: 'waste_tonnes', electricity: 'electricity_mwh', heat: 'heat_tj' };
2957  const labelMap = { waste: 'Tunáž odpadu (t)', electricity: 'Elektřina (MWh)', heat: 'Teplo (TJ)' };
2958  const colorMap = { waste: '#71717a', electricity: '#dc2626', heat: '#f59e0b' };
2959  const col = colMap[metric];
2960  const chartRows = rows.filter(r => r.status === 'operational' && r.year === window.__zevoYear && r[col] !== null && r[col] !== undefined);
2961  const labels = chartRows.map(r => r.zevo_name);
2962  const data = chartRows.map(r => parseFloat(r[col]) || 0);
2963  const ctx = document.getElementById('zevo-chart').getContext('2d');
2964  if (zevoChart) zevoChart.destroy();
2965  zevoChart = new Chart(ctx, {
2966    type: 'bar',
2967    data: { labels, datasets: [{
2968      label: labelMap[metric] + ' (' + window.__zevoYear + ')',
2969      data,
2970      backgroundColor: colorMap[metric],
2971      borderRadius: 6,
2972      borderWidth: 0
2973    }] },
2974    options: {
2975      responsive: true, maintainAspectRatio: false,
2976      plugins: {
2977        legend: { display: false },
2978        tooltip: { backgroundColor: strong, titleColor: bgCard, bodyColor: bgCard, padding: 10, cornerRadius: 8,
2979          callbacks: { label: ctx => Math.round(ctx.parsed.y).toLocaleString('cs-CZ') + ' ' + labelMap[metric].match(/\(([^)]+)\)/)[1] } }
2980      },
2981      scales: {
2982        y: { beginAtZero: true, title: { display: true, text: labelMap[metric], color: muted }, grid: { color: border }, ticks: { color: muted, callback: v => v.toLocaleString('cs-CZ') } },
2983        x: { grid: { display: false }, ticks: { color: muted } }
2984      }
2985    }
2986  });
2987}
2988
2989function drawHeatChart() {
2990  const muted = getCssVar('--text-muted') || '#71717a';
2991  const border = getCssVar('--border') || '#e4e4e7';
2992  const strong = getCssVar('--text-strong') || '#09090b';
2993  const bgCard = getCssVar('--bg-card') || '#fff';
2994  const accent = getCssVar('--accent') || '#dc2626';
2995  const year = __heatYear;
2996  const prevYear = String(parseInt(year) - 1);
2997  const values = heatData.byYear[year] || [];
2998  const prevValues = heatData.byYear[prevYear] || [];
2999  // Sort by price descending
3000  const sorted = [...values].sort((a, b) => b.price - a.price);
3001  const labels = sorted.map(d => d.kraj);
3002  const data = sorted.map(d => d.price);
3003  // Spočítej meziroční změnu
3004  sorted.forEach(s => {
3005    const prev = prevValues.find(p => p.kraj === s.kraj);
3006    s.prevPrice = prev ? prev.price : null;
3007    s.yoyDelta = prev ? Math.round((s.price - prev.price) * 10) / 10 : null;
3008    s.yoyPct = prev ? Math.round(((s.price - prev.price) / prev.price) * 1000) / 10 : null;
3009  });
3010  // Barvy: gradient od max po min, null = šedá
3011  const max = Math.max(...data.filter(v => v !== null));
3012  const min = Math.min(...data.filter(v => v !== null));
3013  const colors = sorted.map(d => {
3014    if (d.price === null) return getCssVar('--bg-subtle') || '#e4e4e7';
3015    const t = (d.price - min) / (max - min);
3016    // Interpoluj mezi delta-down (cool) a accent (warm)
3017    return interpolateColor(getCssVar('--delta-down') || '#047857', accent, t);
3018  });
3019  if (heatChart) heatChart.destroy();
3020  const ctx = document.getElementById('heat-chart').getContext('2d');
3021  heatChart = new Chart(ctx, {
3022    type: 'bar',
3023    data: {
3024      labels: labels,
3025      datasets: [{
3026        label: 'Cena tepla (CZK/GJ vč. DPH)',
3027        data: data,
3028        backgroundColor: colors,
3029        borderColor: colors,
3030        borderWidth: 0,
3031        borderRadius: 6,
3032        borderSkipped: false
3033      }]
3034    },
3035    options: {
3036      indexAxis: 'y',
3037      responsive: true,
3038      maintainAspectRatio: false,
3039      plugins: {
3040        legend: { display: false },
3041        tooltip: {
3042          backgroundColor: strong, titleColor: bgCard, bodyColor: bgCard,
3043          padding: 12, cornerRadius: 8, displayColors: false,
3044          callbacks: {
3045            label: (ctx) => {
3046              const v = ctx.parsed.x;
3047              if (v === null || v === undefined) return 'Data nedostupná';
3048              const row = sorted[ctx.dataIndex];
3049              const lines = [v.toLocaleString('cs-CZ') + ' CZK/GJ (' + year + ')'];
3050              if (row.yoyDelta !== null && row.yoyDelta !== undefined) {
3051                const sign = row.yoyDelta >= 0 ? '+' : '';
3052                lines.push('vs ' + prevYear + ': ' + sign + row.yoyDelta + ' (' + sign + row.yoyPct + ' %)');
3053              }
3054              if (row.locality) lines.push(row.locality + ' lokalit v kraji');
3055              if (row.gj) lines.push((row.gj / 1000).toLocaleString('cs-CZ', {maximumFractionDigits: 0}) + ' tis. GJ celkem');
3056              return lines;
3057            }
3058          }
3059        }
3060      },
3061      scales: {
3062        x: {
3063          title: { display: true, text: 'CZK/GJ vč. DPH', color: muted, font: {size: 11} },
3064          grid: { color: border, lineWidth: 1 },
3065          ticks: { color: muted, font: {size: 11}, callback: v => v.toLocaleString('cs-CZ') }
3066        },
3067        y: {
3068          ticks: { color: muted, font: {size: 12, weight: '500'} },
3069          grid: { display: false }
3070        }
3071      }
3072    }
3073  });
3074}
3075
3076function interpolateColor(c1, c2, t) {
3077  const parse = (c) => {
3078    const h = c.replace('#','');
3079    return [parseInt(h.slice(0,2),16), parseInt(h.slice(2,4),16), parseInt(h.slice(4,6),16)];
3080  };
3081  const a = parse(c1), b = parse(c2);
3082  const r = Math.round(a[0] + (b[0]-a[0])*t);
3083  const g = Math.round(a[1] + (b[1]-a[1])*t);
3084  const bl = Math.round(a[2] + (b[2]-a[2])*t);
3085  return `rgb(${r},${g},${bl})`;
3086}
3087
3088// ===== Spot ceny grafy =====
3089let spotHourlyChart = null;
3090let spotHourChart = null;
3091let spotDowChart = null;
3092let __spotRange = '1y';
3093let __spotHourYear = '3yr';
3094let __spotDowYear = '3yr';
3095
3096const DAY_NAMES = ['Pondělí','Úterý','Středa','Čtvrtek','Pátek','Sobota','Neděle'];
3097
3098// Pozičně, ne podle timestamp — Excel data mají DST gapy a nepokrývají kontinuálně každou hodinu
3099function spotRangeStartIdx(range, totalHours) {
3100  const hoursMap = { '7d': 7*24, '30d': 30*24, '90d': 90*24, '1y': 365*24, '3y': 3*365*24 };
3101  if (range === 'all') return 0;
3102  const n = hoursMap[range];
3103  return n ? Math.max(0, totalHours - n) : 0;
3104}
3105// Proporcionální mapování idx → datum (firstTs..lastTs linearly spread přes celé pole)
3106function spotIdxToMs(idx, firstTsMs, lastTsMs, totalHours) {
3107  if (totalHours <= 1) return firstTsMs;
3108  return firstTsMs + idx * (lastTsMs - firstTsMs) / (totalHours - 1);
3109}
3110function spotMsToIdx(ms, firstTsMs, lastTsMs, totalHours) {
3111  if (lastTsMs <= firstTsMs) return 0;
3112  return Math.round((ms - firstTsMs) / (lastTsMs - firstTsMs) * (totalHours - 1));
3113}
3114
3115// Lokální datum ms → 'YYYY-MM-DD' pro <input type=date>
3116function spotFmtInput(ms) {
3117  const d = new Date(ms);
3118  const p = n => String(n).padStart(2, '0');
3119  return d.getFullYear() + '-' + p(d.getMonth() + 1) + '-' + p(d.getDate());
3120}
3121
3122function setSpotRange(range) {
3123  __spotRange = range;
3124  document.querySelectorAll('[data-spot-range]').forEach(b => b.classList.toggle('active', b.dataset.spotRange === range));
3125  // Okýnka Od/Do se nevymazávají — draw funkce je naplní skutečným rozsahem (ať jsou vždy bílá a vyplněná).
3126  // Zobraz/skryj toggles roků pro compare mode
3127  const toggles = document.getElementById('spot-year-toggles');
3128  if (toggles) toggles.style.display = range === 'compare' ? '' : 'none';
3129  if (range === 'compare') drawSpotCompareChart();
3130  else drawSpotHourlyChart();
3131}
3132
3133const SPOT_YEAR_COLORS = {
3134  '2020':'#737373',  // šedá
3135  '2021':'#8b5cf6',  // fialová
3136  '2022':'#3b82f6',  // modrá
3137  '2023':'#06b6d4',  // tyrkysová
3138  '2024':'#10b981',  // zelená
3139  '2025':'#f59e0b',  // oranžová
3140  '2026':'#dc2626'   // červená
3141};
3142
3143function toggleSpotYear(year) {
3144  const btn = document.querySelector('[data-spot-year-toggle="' + year + '"]');
3145  if (btn) btn.classList.toggle('active');
3146  drawSpotCompareChart();
3147}
3148
3149function drawSpotCompareChart() {
3150  if (!window.spotData) return;
3151  const muted = getCssVar('--text-muted') || '#71717a';
3152  const border = getCssVar('--border') || '#e4e4e7';
3153  const strong = getCssVar('--text-strong') || '#09090b';
3154  const bgCard = getCssVar('--bg-card') || '#fff';
3155  // Srovnání let v HODINOVÉM rozlišení: z vysokorozlišovací série (hodinová + 15min),
3156  // body zarovnané podle pozice v roce (frakční den v roce), roky překryté přes sebe.
3157  const series = window.spotData.series || [];
3158  if (!series.length) return;
3159  // Okýnka Od/Do naplň celým rozsahem dat (vždy vyplněná, bílá)
3160  const _fEl = document.getElementById('spot-from'), _tEl = document.getElementById('spot-to');
3161  if (_fEl) _fEl.value = spotFmtInput(series[0].x);
3162  if (_tEl) _tEl.value = spotFmtInput(series[series.length - 1].x);
3163  const byYear = {};
3164  const sumY = {}, cntY = {};
3165  for (let i = 0; i < series.length; i++) {
3166    const p = series[i];
3167    const d = new Date(p.x);
3168    const y = d.getFullYear();
3169    const jan1 = new Date(y, 0, 1).getTime();
3170    const doy = (p.x - jan1) / 86400000 + 1; // 1-based frakční den v roce
3171    const yv = cvMs(p.y, p.x, 'spot5');
3172    (byYear[y] = byYear[y] || []).push({ x: doy, y: yv });
3173    if (isFinite(yv)) { sumY[y] = (sumY[y] || 0) + yv; cntY[y] = (cntY[y] || 0) + 1; }
3174  }
3175  const datasets = [];
3176  // Pole roků vzestupně (kvůli legendě 2020→2026). Z-order ale řídí vlastnost `order`:
3177  // Chart.js kreslí dataset s NEJNIŽŠÍM `order` navrch → nejnovější rok dostane order 0
3178  // (popředí), nejstarší nejvyšší order (pozadí). Hloubkový gradient: novější sytější+silnější.
3179  const yearsAsc = Object.keys(byYear).sort();
3180  const n = yearsAsc.length;
3181  yearsAsc.forEach((year, idx) => {
3182    const depth = n > 1 ? idx / (n - 1) : 1; // 0 = nejstarší, 1 = nejnovější
3183    const base = paletteColor(SPOT_YEAR_COLORS[year] || '#71717a');
3184    const alpha = 0.40 + 0.60 * depth;       // starší = průhlednější (pozadí)
3185    const bw = 0.7 + 0.9 * depth;            // novější = silnější čára (popředí)
3186    datasets.push({
3187      label: year,
3188      data: byYear[year],
3189      borderColor: hexToRgba(base, alpha),
3190      backgroundColor: 'transparent',
3191      pointRadius: 0,
3192      pointHoverRadius: 4,
3193      borderWidth: bw,
3194      tension: 0,
3195      hidden: false,
3196      spanGaps: false,
3197      parsing: false,
3198      order: (n - 1) - idx   // nejnovější rok → nejnižší order → kreslí se navrch (popředí)
3199    });
3200  });
3201  // Stats: roční průměry (z hodinových dat)
3202  const yearAvgs = Object.keys(byYear).sort().map(y => ({ year: y, avg: cntY[y] ? sumY[y] / cntY[y] : 0 }));
3203  const statsEl = document.getElementById('spot-stats');
3204  if (statsEl) statsEl.innerHTML = 'Roční průměry (hodinové): ' + yearAvgs.map(s => `<strong>${s.year}</strong>: ${s.avg.toFixed(1)} ${ccySym('spot5')}`).join(' • ');
3205  if (spotHourlyChart) spotHourlyChart.destroy();
3206  const ctx = document.getElementById('spot-hourly-chart').getContext('2d');
3207  // Month tick positions (day-of-year for 1st of each month, non-leap)
3208  const monthStarts = [1, 32, 60, 91, 121, 152, 182, 213, 244, 274, 305, 335];
3209  const monthNames = ['Led','Úno','Bře','Dub','Kvě','Čvn','Čvc','Srp','Zář','Říj','Lis','Pro'];
3210  spotHourlyChart = new Chart(ctx, {
3211    type: 'line',
3212    data: { datasets: datasets },
3213    options: {
3214      responsive: true, maintainAspectRatio: false,
3215      animation: false,
3216      interaction: { mode: 'nearest', intersect: false },
3217      plugins: {
3218        legend: { display: true, position: 'top', labels: { color: muted, usePointStyle: true, pointStyle: 'rectRounded', boxWidth: 12, padding: 12, font: {size: 12, weight: '500'} } },
3219        tooltip: {
3220          backgroundColor: strong, titleColor: bgCard, bodyColor: bgCard, padding: 10, cornerRadius: 8,
3221          callbacks: {
3222            title: function(items) {
3223              if (!items.length) return '';
3224              // Datum se počítá z ROKU dané křivky — je to přesná inverze vzorce, kterým
3225              // se x stavělo (doy = (ts - 1.1. roku)/den + 1). Dřívější pevný základ
3226              // new Date(2024,0,dd) byl PŘESTUPNÝ rok, kdežto osa používá nepřestupné
3227              // monthStarts → od března se tooltip s osou rozcházel o den.
3228              const it = items[0];
3229              const x = it.parsed.x;
3230              const rok = parseInt(it.dataset && it.dataset.label, 10);
3231              if (!isFinite(rok)) return '';
3232              const ms = new Date(rok, 0, 1).getTime() + (x - 1) * 86400000;
3233              const d = new Date(Math.round(ms / 3600000) * 3600000);   // zaokrouhli na celou hodinu
3234              return d.toLocaleDateString('cs-CZ', {day:'numeric', month:'long', year:'numeric'})
3235                     + ', ' + d.getHours() + 'h';
3236            },
3237            label: function(ctx) { return ctx.dataset.label + ': ' + ctx.parsed.y.toFixed(2) + ' ' + ccyUnit('spot5'); }
3238          }
3239        }
3240      },
3241      scales: {
3242        y: { title: {display: true, text: ccyUnit('spot5') + ' (hodinová)', color: muted}, grid: {color: border}, ticks: {color: muted} },
3243        x: {
3244          type: 'linear', min: 1, max: 367,
3245          title: {display: true, text: 'Měsíc', color: muted},
3246          grid: {display: false},
3247          ticks: {
3248            color: muted,
3249            callback: function(v) {
3250              const den = Math.round(v);
3251              const rozsah = (isFinite(this.max) && isFinite(this.min)) ? (this.max - this.min) : 366;
3252              if (rozsah > 120) {                       // celý rok → jen názvy měsíců
3253                const idx = monthStarts.indexOf(den);
3254                return idx >= 0 ? monthNames[idx] : '';
3255              }
3256              // po zoomu má „Led/Úno" nulovou vypovídací hodnotu → konkrétní datum
3257              let m = 0;
3258              for (let i = 0; i < monthStarts.length; i++) if (monthStarts[i] <= den) m = i;
3259              return (den - monthStarts[m] + 1) + '. ' + (m + 1) + '.';
3260            },
3261            autoSkip: false,
3262            stepSize: 1
3263          },
3264          afterBuildTicks: function(axis) {
3265            // Ticky se musí přepočítat podle AKTUÁLNÍHO rozsahu, jinak osa po zoomu
3266            // dál kreslí všech 12 měsíců včetně těch mimo výřez.
3267            const min = axis.min, max = axis.max, rozsah = max - min;
3268            if (!isFinite(min) || !isFinite(max)) { axis.ticks = monthStarts.map(s => ({ value: s })); return; }
3269            if (rozsah > 120) {
3270              axis.ticks = monthStarts.filter(s => s >= min && s <= max).map(s => ({ value: s }));
3271              return;
3272            }
3273            const krok = rozsah > 60 ? 7 : rozsah > 30 ? 3 : rozsah > 12 ? 2 : 1;
3274            const out = [];
3275            for (let d = Math.ceil(min); d <= max; d++) if ((d - 1) % krok === 0) out.push({ value: d });
3276            axis.ticks = out.length ? out : [{ value: Math.round(min) }, { value: Math.round(max) }];
3277          }
3278        }
3279      }
3280    }
3281  });
3282}
3283
3284function applySpotCustomRange() {
3285  const from = document.getElementById('spot-from').value;
3286  const to = document.getElementById('spot-to').value;
3287  if (!from || !to) return;
3288  document.querySelectorAll('[data-spot-range]').forEach(b => b.classList.remove('active'));
3289  __spotRange = 'custom';
3290  drawSpotHourlyChart(from, to);
3291}
3292
3293function drawSpotHourlyChart(fromStr, toStr) {
3294  if (!window.spotData) return;
3295  const accent = getCssVar('--accent') || '#dc2626';
3296  const muted = getCssVar('--text-muted') || '#71717a';
3297  const border = getCssVar('--border') || '#e4e4e7';
3298  const strong = getCssVar('--text-strong') || '#09090b';
3299  const bgCard = getCssVar('--bg-card') || '#fff';
3300  // Vysokorozlišovací série s reálnými timestampy (hodinová + 15min kde jsou).
3301  const series = window.spotData.series || [];
3302  if (!series.length) return;
3303  const firstMs = series[0].x, lastMs = series[series.length - 1].x;
3304  // Časové meze rozsahu
3305  let fromMs, toMs;
3306  if (fromStr && toStr) {
3307    fromMs = new Date(fromStr + 'T00:00:00').getTime();
3308    toMs = new Date(toStr + 'T23:59:59').getTime();
3309  } else {
3310    const DAY = 86400000;
3311    const spanDaysMap = { '7d': 7, '30d': 30, '90d': 90, '1y': 365, '3y': 3 * 365 };
3312    toMs = lastMs;
3313    if (__spotRange === 'all') {
3314      fromMs = firstMs;
3315    } else if (__spotRange === 'ytd') {
3316      fromMs = Math.max(firstMs, new Date(new Date(lastMs).getFullYear(), 0, 1).getTime()); // 1. ledna roku posledního bodu
3317    } else {
3318      fromMs = Math.max(firstMs, lastMs - (spanDaysMap[__spotRange] || 365) * DAY);
3319    }
3320  }
3321  // Naplň okýnka Od/Do skutečným rozsahem (vždy vyplněná, bílá; YTD → 1.1. – dnešek apod.)
3322  const fEl = document.getElementById('spot-from'), tEl = document.getElementById('spot-to');
3323  if (fEl) fEl.value = spotFmtInput(fromMs);
3324  if (tEl) tEl.value = spotFmtInput(toMs);
3325  // Body v rozsahu (série je seřazená podle času)
3326  const pts = series.filter(p => p.x >= fromMs && p.x <= toMs);
3327  // Statistiky z PLNÉHO rozsahu (ne z případně podvzorkovaných bodů)
3328  let sum = 0, cnt = 0, mn = Infinity, mx = -Infinity;
3329  for (let i = 0; i < pts.length; i++) {
3330    const v = cvMs(pts[i].y, pts[i].x, 'spot5');
3331    if (!isFinite(v)) continue;
3332    sum += v; cnt++;
3333    if (v < mn) mn = v;
3334    if (v > mx) mx = v;
3335  }
3336  const avg = cnt ? sum / cnt : 0;
3337  if (!cnt) { mn = 0; mx = 0; }
3338  // Plné rozlišení; podvzorkuj jen u opravdu velkých rozsahů a i tak zachovej extrémy.
3339  const maxPoints = 15000;
3340  let data;
3341  if (pts.length <= maxPoints) {
3342    data = pts;
3343  } else {
3344    const stepN = Math.ceil(pts.length / maxPoints);
3345    data = [];
3346    for (let b = 0; b < pts.length; b += stepN) {
3347      const e = Math.min(b + stepN, pts.length);
3348      let minI = -1, maxI = -1, minV = Infinity, maxV = -Infinity;
3349      for (let i = b; i < e; i++) {
3350        const v = pts[i].y;
3351        if (!isFinite(v)) continue;
3352        if (v < minV) { minV = v; minI = i; }
3353        if (v > maxV) { maxV = v; maxI = i; }
3354      }
3355      if (minI < 0) continue;
3356      const lo = Math.min(minI, maxI), hi = Math.max(minI, maxI);
3357      data.push(pts[lo]);
3358      if (hi !== lo) data.push(pts[hi]);
3359    }
3360  }
3361  const spanDays = (toMs - fromMs) / 86400000;
3362  const stats = document.getElementById('spot-stats');
3363  if (stats) stats.textContent = `Průměr ${avg.toFixed(1)} • Min ${mn.toFixed(1)} • Max ${mx.toFixed(1)} ${ccyUnit('spot5')} (${cnt.toLocaleString('cs-CZ')} bodů${data.length < pts.length ? ', zobrazeno ' + data.length.toLocaleString('cs-CZ') : ''})`;
3364  if (spotHourlyChart) spotHourlyChart.destroy();
3365  const ctx = document.getElementById('spot-hourly-chart').getContext('2d');
3366  spotHourlyChart = new Chart(ctx, {
3367    type: 'line',
3368    data: { datasets: [{ label: 'Spot ' + ccyUnit('spot5'), data: data.map(p => ({ x: p.x, y: cvMs(p.y, p.x, 'spot5') })), borderColor: accent, backgroundColor: hexToRgba(accent, 0.15), fill: 'origin', tension: 0, pointRadius: 0, pointHoverRadius: 4, borderWidth: 0.8, spanGaps: false }] },
3369    options: {
3370      responsive: true, maintainAspectRatio: false,
3371      animation: false,
3372      interaction: { mode: 'nearest', intersect: false },
3373      plugins: {
3374        legend: { display: false },
3375        tooltip: { backgroundColor: strong, titleColor: bgCard, bodyColor: bgCard, padding: 10, cornerRadius: 8,
3376          callbacks: {
3377            title: items => items.length ? new Date(items[0].parsed.x).toLocaleString('cs-CZ') : '',
3378            label: c => c.parsed.y.toFixed(2) + ' ' + ccyUnit('spot5')
3379          } }
3380      },
3381      scales: {
3382        y: { title: {display: true, text: ccyUnit('spot5'), color: muted}, grid: {color: border}, ticks: {color: muted} },
3383        x: { type: 'linear', min: fromMs, max: toMs, ticks: {color: muted, maxTicksLimit: 12, callback: function(v) {
3384          const d = new Date(v);
3385          if (spanDays <= 3) return d.toLocaleDateString('cs-CZ', {day:'2-digit', month:'2-digit'}) + ' ' + d.getHours() + 'h';
3386          if (spanDays <= 100) return d.toLocaleDateString('cs-CZ', {day:'2-digit', month:'short'});
3387          return d.toLocaleDateString('cs-CZ', {month:'short', year:'2-digit'});
3388        }}, grid: {display: false} }
3389      }
3390    }
3391  });
3392}
3393
3394// ===== Multi-year hourly + DoW profile výpočet ze surových cen =====
3395// spotData.prices je hodinová série od spotData.firstDate (2020-01-01 00:00).
3396// Pro daný rok spočítáme avg per hour-of-day (0-23) nebo per day-of-week (0-6, Po=0).
3397// Cache výsledků v window.spotData._cachedHourly / _cachedDow.
3398
3399function _computeSpotProfiles(year) {
3400  if (!window.spotData || !Array.isArray(spotData.prices) || !spotData.firstDate) return null;
3401  window.spotData._cachedHourly = window.spotData._cachedHourly || {};
3402  window.spotData._cachedDow = window.spotData._cachedDow || {};
3403  if (window.spotData._cachedHourly[year] && window.spotData._cachedDow[year]) return true;
3404  // first hour
3405  const firstD = new Date(spotData.firstDate.replace(' ', 'T') + ':00Z');
3406  const firstMs = firstD.getTime();
3407  const prices = spotData.prices;
3408  const hourSum = new Array(24).fill(0), hourCount = new Array(24).fill(0);
3409  const dowSum = new Array(7).fill(0), dowCount = new Array(7).fill(0);
3410  for (let i = 0; i < prices.length; i++) {
3411    const v = prices[i];
3412    if (v === null || v === undefined || !Number.isFinite(v)) continue;
3413    const d = new Date(firstMs + i * 3600 * 1000);
3414    if (d.getUTCFullYear() !== year) continue;
3415    const h = d.getUTCHours();
3416    hourSum[h] += v; hourCount[h]++;
3417    // ISO weekday: 0=Sun, 1=Mon,...,6=Sat. We want Po=0,...,Ne=6
3418    let dow = d.getUTCDay(); // 0=Sun
3419    dow = (dow === 0) ? 6 : (dow - 1);  // 0=Mon,...,6=Sun
3420    dowSum[dow] += v; dowCount[dow]++;
3421  }
3422  const hourAvg = hourSum.map((s,i) => hourCount[i] > 0 ? s / hourCount[i] : null);
3423  const dowAvg = dowSum.map((s,i) => dowCount[i] > 0 ? s / dowCount[i] : null);
3424  window.spotData._cachedHourly[year] = hourAvg;
3425  window.spotData._cachedDow[year] = dowAvg;
3426  return true;
3427}
3428
3429function getSpotHourData(yearKey) {
3430  if (yearKey === '3yr') return window.spotData?.avgByHour3yr || null;
3431  // Hodinový profil bere pouze prebuilt agregáty (CET timestamps z původního Excelu).
3432  // Roky bez prebuilt (2020-2022 a 2026) by z plochého spotData.prices vyšly špatně
3433  // — mezery v sérii (1481 hodin) rozhodí mapování i mod 24 ⇒ CET hour.
3434  // Bude doplněno přes ENTSOE day-ahead API s explicitními timestamps.
3435  if (window.spotData?.avgByHourByYear?.[yearKey]) return window.spotData.avgByHourByYear[yearKey];
3436  return null;
3437}
3438// Day-of-week profil z dailyByYear (per-day data se SPRÁVNÝMI timestampy).
3439// dailyByYear[year][i] = průměrná denní cena pro i-tý den roku (i=0 = 1. ledna).
3440function _computeDowFromDaily(yearKey) {
3441  if (yearKey === '3yr') return window.spotData?.avgByDow3yr || null;
3442  const y = parseInt(yearKey, 10);
3443  if (!isFinite(y)) return null;
3444  const dailyArr = window.spotData?.dailyByYear?.[yearKey];
3445  if (!dailyArr) return null;
3446  // Den-of-week 1. ledna daného roku (Mon=0,...,Sun=6)
3447  const jan1 = new Date(Date.UTC(y, 0, 1));
3448  const jsDow = jan1.getUTCDay(); // 0=Sun
3449  const startDow = (jsDow === 0) ? 6 : (jsDow - 1); // 0=Mon
3450  const dowSum = new Array(7).fill(0), dowCnt = new Array(7).fill(0);
3451  dailyArr.forEach((v, i) => {
3452    if (v === null || v === undefined || !Number.isFinite(v)) return;
3453    const dow = (startDow + i) % 7;
3454    dowSum[dow] += v;
3455    dowCnt[dow]++;
3456  });
3457  return dowSum.map((s, i) => dowCnt[i] > 0 ? s / dowCnt[i] : null);
3458}
3459
3460function getSpotDowData(yearKey) {
3461  if (yearKey === '3yr') return window.spotData?.avgByDow3yr || null;
3462  // Nepoužívej prebuilt avgByDowByYear (nemusí mít všechny roky) — vždy počítej z dailyByYear (správné timestampy)
3463  window.spotData = window.spotData || {};
3464  window.spotData._cachedDowFromDaily = window.spotData._cachedDowFromDaily || {};
3465  if (window.spotData._cachedDowFromDaily[yearKey] !== undefined) return window.spotData._cachedDowFromDaily[yearKey];
3466  const result = _computeDowFromDaily(yearKey);
3467  window.spotData._cachedDowFromDaily[yearKey] = result;
3468  return result;
3469}
3470
3471// Multi-select state
3472window.__spotHourYears = new Set(['3yr']);
3473window.__spotDowYears = new Set(['3yr']);
3474
3475const YEAR_PALETTE = {
3476  '3yr':  '#71717a',  // grafit (průměr, čárkovaně)
3477  '2020': '#737373',  // šedá
3478  '2021': '#8b5cf6',  // fialová
3479  '2022': '#3b82f6',  // modrá
3480  '2023': '#06b6d4',  // tyrkysová
3481  '2024': '#10b981',  // zelená
3482  '2025': '#f59e0b',  // oranžová
3483  '2026': '#dc2626'   // červená
3484};
3485
3486function toggleSpotHourYear(year) {
3487  const s = window.__spotHourYears;
3488  if (s.has(year)) s.delete(year); else s.add(year);
3489  if (s.size === 0) s.add('3yr'); // vždy aspoň 1 vybraný
3490  document.querySelectorAll('[data-spot-year]').forEach(b => b.classList.toggle('active', s.has(b.dataset.spotYear)));
3491  drawSpotHourChart();
3492}
3493
3494function drawSpotHourChart() {
3495  if (!window.spotData) return;
3496  const accent = getCssVar('--accent') || '#dc2626';
3497  const muted = getCssVar('--text-muted') || '#71717a';
3498  const border = getCssVar('--border') || '#e4e4e7';
3499  const strong = getCssVar('--text-strong') || '#09090b';
3500  const bgCard = getCssVar('--bg-card') || '#fff';
3501  const labels = Array.from({length: 24}, (_, i) => String(i).padStart(2,'0') + ':00');
3502  const selected = Array.from(window.__spotHourYears);
3503  if (spotHourChart) spotHourChart.destroy();
3504  const ctx = document.getElementById('spot-hour-chart').getContext('2d');
3505
3506  if (selected.length === 1) {
3507    // Single year — bar chart se zelený→červený gradient
3508    const _rate = fxYearKeyRate(selected[0]);
3509    const data = (getSpotHourData(selected[0]) || []).map(v => v == null ? null : cvRate(v, _rate, 'hour'));
3510    const valid = data.filter(v => v !== null);
3511    if (!valid.length) return;
3512    const mn = Math.min(...valid), mx = Math.max(...valid);
3513    const colors = data.map(v => {
3514      if (v === null) return 'transparent';
3515      const t = mx === mn ? 0 : (v - mn) / (mx - mn);
3516      return interpolateColor(getCssVar('--delta-down') || '#047857', accent, t);
3517    });
3518    spotHourChart = new Chart(ctx, {
3519      type: 'bar',
3520      data: { labels: labels, datasets: [{ label: labelForYear(selected[0]), data: data, backgroundColor: colors, borderRadius: 4 }] },
3521      options: {
3522        responsive: true, maintainAspectRatio: false,
3523        plugins: {
3524          legend: { display: false },
3525          tooltip: { backgroundColor: strong, titleColor: bgCard, bodyColor: bgCard, padding: 10, cornerRadius: 8,
3526            callbacks: { label: ctx => ctx.parsed.y.toFixed(2).replace('.', ',') + ' ' + ccyUnit('hour') } }
3527        },
3528        scales: {
3529          y: { title: {display: true, text: ccyUnit('hour'), color: muted}, grid: {color: border}, ticks: {color: muted} },
3530          x: { title: {display: true, text: 'Hodina', color: muted}, grid: {display: false}, ticks: {color: muted, maxRotation: 0} }
3531        }
3532      }
3533    });
3534  } else {
3535    // Multi-year — line chart, jedna čára per rok
3536    const datasets = selected.map(yk => {
3537      const _rate = fxYearKeyRate(yk);
3538      const data = (getSpotHourData(yk) || []).map(v => v == null ? null : cvRate(v, _rate, 'hour'));
3539      const color = paletteColor(YEAR_PALETTE[yk] || '#71717a');
3540      return {
3541        label: labelForYear(yk),
3542        data: data,
3543        borderColor: color,
3544        backgroundColor: color + '20',
3545        pointRadius: 0,
3546        pointHoverRadius: 5,
3547        borderWidth: yk === '3yr' ? 1.5 : 2,
3548        borderDash: yk === '3yr' ? [4, 3] : [],
3549        tension: 0.3,
3550        spanGaps: true
3551      };
3552    });
3553    spotHourChart = new Chart(ctx, {
3554      type: 'line',
3555      data: { labels: labels, datasets: datasets },
3556      options: {
3557        responsive: true, maintainAspectRatio: false,
3558        interaction: { mode: 'index', intersect: false },
3559        plugins: {
3560          legend: { display: true, position: 'top', labels: { color: muted, usePointStyle: true, pointStyle: 'rectRounded', boxWidth: 12, boxHeight: 8 } },
3561          tooltip: { backgroundColor: strong, titleColor: bgCard, bodyColor: bgCard, padding: 10, cornerRadius: 8,
3562            callbacks: { label: ctx => ctx.dataset.label + ': ' + ctx.parsed.y.toFixed(2).replace('.', ',') + ' ' + ccyUnit('hour') } }
3563        },
3564        scales: {
3565          y: { title: {display: true, text: ccyUnit('hour'), color: muted}, grid: {color: border}, ticks: {color: muted} },
3566          x: { title: {display: true, text: 'Hodina', color: muted}, grid: {display: false}, ticks: {color: muted, maxRotation: 0} }
3567        }
3568      }
3569    });
3570  }
3571}
3572
3573function toggleSpotDowYear(year) {
3574  const s = window.__spotDowYears;
3575  if (s.has(year)) s.delete(year); else s.add(year);
3576  if (s.size === 0) s.add('3yr');
3577  document.querySelectorAll('[data-spot-dow-year]').forEach(b => b.classList.toggle('active', s.has(b.dataset.spotDowYear)));
3578  drawSpotDowChart();
3579}
3580
3581function labelForYear(yk) {
3582  if (yk === '3yr') return '2023-2025 (průměr)';
3583  if (yk === '2026') return '2026 (YTD)';
3584  return yk;
3585}
3586
3587function drawSpotDowChart() {
3588  if (!window.spotData) return;
3589  const accent = getCssVar('--accent') || '#dc2626';
3590  const muted = getCssVar('--text-muted') || '#71717a';
3591  const border = getCssVar('--border') || '#e4e4e7';
3592  const strong = getCssVar('--text-strong') || '#09090b';
3593  const bgCard = getCssVar('--bg-card') || '#fff';
3594  const selected = Array.from(window.__spotDowYears);
3595  if (spotDowChart) spotDowChart.destroy();
3596  const ctx = document.getElementById('spot-dow-chart').getContext('2d');
3597
3598  if (selected.length === 1) {
3599    const _rate = fxYearKeyRate(selected[0]);
3600    const data = (getSpotDowData(selected[0]) || []).map(v => v == null ? null : cvRate(v, _rate, 'dow'));
3601    const valid = data.filter(v => v !== null);
3602    if (!valid.length) return;
3603    const mn = Math.min(...valid), mx = Math.max(...valid);
3604    const colors = data.map(v => {
3605      if (v === null) return 'transparent';
3606      const t = mx === mn ? 0 : (v - mn) / (mx - mn);
3607      return interpolateColor(getCssVar('--delta-down') || '#047857', accent, t);
3608    });
3609    spotDowChart = new Chart(ctx, {
3610      type: 'bar',
3611      data: { labels: DAY_NAMES, datasets: [{ label: labelForYear(selected[0]), data: data, backgroundColor: colors, borderRadius: 6 }] },
3612      options: {
3613        responsive: true, maintainAspectRatio: false,
3614        plugins: {
3615          legend: { display: false },
3616          tooltip: { backgroundColor: strong, titleColor: bgCard, bodyColor: bgCard, padding: 10, cornerRadius: 8,
3617            callbacks: { label: ctx => ctx.parsed.y.toFixed(2).replace('.', ',') + ' ' + ccyUnit('dow') } }
3618        },
3619        scales: {
3620          y: { title: {display: true, text: ccyUnit('dow'), color: muted}, grid: {color: border}, ticks: {color: muted} },
3621          x: { grid: {display: false}, ticks: {color: muted} }
3622        }
3623      }
3624    });
3625  } else {
3626    // Multi-year: grouped bars (vedle sebe, 7 dnů × N roků)
3627    const datasets = selected.map(yk => {
3628      const _rate = fxYearKeyRate(yk);
3629      return {
3630        label: labelForYear(yk),
3631        data: (getSpotDowData(yk) || []).map(v => v == null ? null : cvRate(v, _rate, 'dow')),
3632        backgroundColor: paletteColor(YEAR_PALETTE[yk] || '#71717a'),
3633        borderRadius: 4,
3634        borderWidth: 0
3635      };
3636    });
3637    spotDowChart = new Chart(ctx, {
3638      type: 'bar',
3639      data: { labels: DAY_NAMES, datasets: datasets },
3640      options: {
3641        responsive: true, maintainAspectRatio: false,
3642        plugins: {
3643          legend: { display: true, position: 'top', labels: { color: muted, usePointStyle: true, pointStyle: 'rectRounded', boxWidth: 12, boxHeight: 8 } },
3644          tooltip: { backgroundColor: strong, titleColor: bgCard, bodyColor: bgCard, padding: 10, cornerRadius: 8,
3645            callbacks: { label: ctx => ctx.dataset.label + ': ' + ctx.parsed.y.toFixed(2).replace('.', ',') + ' ' + ccyUnit('dow') } }
3646        },
3647        scales: {
3648          y: { title: {display: true, text: ccyUnit('dow'), color: muted}, grid: {color: border}, ticks: {color: muted} },
3649          x: { grid: {display: false}, ticks: {color: muted} }
3650        }
3651      }
3652    });
3653  }
3654}
3655
3656// ===== Sekce 13: Forward (PXE Cal) vs Spot (OTE denní průměr) =====
3657let fwdSpotChart = null;
3658
3659// Roční průměry: forward (el_nextyear) vs spot (OTE denní průměr)
3660function yearlyFwdSpotAverages() {
3661  const mean = a => (a && a.length) ? a.reduce((s, v) => s + v, 0) / a.length : null;
3662  const fwdByYear = {}, spotByYear = {};
3663  const fwdPts = (typeof getSeriesPoints === 'function' ? getSeriesPoints('el_nextyear') : []);
3664  fwdPts.forEach(p => {
3665    const v = p.value; if (v === null || v === undefined || !isFinite(v)) return;
3666    const y = new Date(p.date).getUTCFullYear();
3667    (fwdByYear[y] = fwdByYear[y] || []).push(cvDate(v, p.date, 'fwd'));
3668  });
3669  fwdSpotDaily().forEach(p => {
3670    if (!isFinite(p.y)) return;
3671    const y = new Date(p.x).getUTCFullYear();
3672    (spotByYear[y] = spotByYear[y] || []).push(cvMs(p.y, p.x, 'fwd'));
3673  });
3674  // Spot 2018–2019: spot_hourly v DB začíná 2020 → roční průměry doplněny z ENTSOE
3675  // day-ahead CZ (A44), přesný průměr 8 760 hodin/rok, extrakce 2026-06-10. Reálná data, ne odhad.
3676  const SPOT_AVG_STATIC = { 2018: 46.03, 2019: 40.21 };  // EUR/MWh
3677  Object.keys(SPOT_AVG_STATIC).forEach(y => {
3678    if (!spotByYear[y] || !spotByYear[y].length) {
3679      spotByYear[y] = [cvRate(SPOT_AVG_STATIC[y], fxYearAvg(parseInt(y, 10)), 'fwd')];
3680    }
3681  });
3682  const years = Array.from(new Set([...Object.keys(fwdByYear), ...Object.keys(spotByYear)].map(Number)))
3683    .filter(y => y >= 2008)  // 2007: jen zlomek forwardových dat, zkresluje průměr → vynechat
3684    .sort((a, b) => a - b);
3685  return years.map(y => ({ year: y, fwd: mean(fwdByYear[y]), spot: mean(spotByYear[y]) }));
3686}
3687
3688// Spot jako denní průměr (rekonstrukce z dailyByYear → reálné timestampy)
3689function fwdSpotDaily() {
3690  const out = [];
3691  const dby = window.spotData && window.spotData.dailyByYear;
3692  if (!dby) return out;
3693  Object.keys(dby).forEach(y => {
3694    const year = parseInt(y, 10); const arr = dby[y];
3695    if (!Array.isArray(arr)) return;
3696    arr.forEach((v, i) => {
3697      if (v === null || v === undefined || !isFinite(v)) return;
3698      out.push({ x: Date.UTC(year, 0, 1) + i * 86400000, y: v });
3699    });
3700  });
3701  out.sort((a, b) => a.x - b.x);
3702  return out;
3703}
3704
3705function drawForwardSpotChart() {
3706  const canvas = document.getElementById('fwd-spot-chart');
3707  if (!canvas) return;
3708  const muted = getCssVar('--text-muted') || '#71717a';
3709  const border = getCssVar('--border') || '#e4e4e7';
3710  const strong = getCssVar('--text-strong') || '#09090b';
3711  const bgCard = getCssVar('--bg-card') || '#fff';
3712  const rows = yearlyFwdSpotAverages();
3713  const thisYear = new Date().getFullYear();
3714  const labels = rows.map(r => String(r.year) + (r.year === thisYear ? ' (YTD)' : ''));
3715  const fwdCol = (typeof getSeriesColor === 'function' ? getSeriesColor('el_nextyear') : '#6366f1');
3716  const spotCol = '#06b6d4';
3717  const r1 = v => (v === null || v === undefined) ? null : Math.round(v * 10) / 10;
3718  const ds = [
3719    { label: 'Forward (Ø rok)', data: rows.map(r => r1(r.fwd)), backgroundColor: fwdCol, borderRadius: 4, maxBarThickness: 46, order: 2 },
3720    { label: 'Spot (Ø rok)', data: rows.map(r => r1(r.spot)), backgroundColor: spotCol, borderRadius: 4, maxBarThickness: 46, order: 2 },
3721    { label: 'Spot − Forward', type: 'line', data: rows.map(r => (r.fwd === null || r.spot === null) ? null : r1(r.spot - r.fwd)),
3722      borderColor: muted, borderDash: [5, 4], borderWidth: 1.5, pointRadius: 3, pointBackgroun
3722dColor: muted,
3723      spanGaps: false, tension: 0.2, order: 1 }
3724  ];
3725  // Shrnutí pod graf: v kolika uzavřených letech (kde existují obě řady) skončil spot pod forwardem
3726  const closed = rows.filter(r => r.year < thisYear && r.fwd !== null && r.spot !== null);
3727  const below = closed.filter(r => r.spot < r.fwd).length;
3728  const sumEl = document.getElementById('fwd-spot-summary');
3729  if (sumEl && closed.length) {
3730    sumEl.textContent = ' Ve ' + below + ' z ' + closed.length + ' uzavřených let s oběma řadami skončil spot POD forwardem (zajištění bylo dražší než spot).';
3731  }
3732  if (fwdSpotChart) fwdSpotChart.destroy();
3733  const ctx = canvas.getContext('2d');
3734  fwdSpotChart = new Chart(ctx, {
3735    type: 'bar',
3736    data: { labels: labels, datasets: ds },
3737    options: {
3738      responsive: true, maintainAspectRatio: false, animation: false,
3739      interaction: { mode: 'index', intersect: false },
3740      plugins: {
3741        legend: { display: true, position: 'top', labels: { color: muted, usePointStyle: true, pointStyle: 'rectRounded', boxWidth: 12, boxHeight: 8, padding: 12, font: {size: 12, weight: '500'} } },
3742        tooltip: { backgroundColor: strong, titleColor: bgCard, bodyColor: bgCard, padding: 10, cornerRadius: 8,
3743          callbacks: {
3744            label: c => c.dataset.label + ': ' + (c.parsed.y === null ? '—' : c.parsed.y.toFixed(1).replace('.', ',') + ' ' + ccyUnit('fwd')),
3745            afterBody: items => {
3746              if (!items.length) return '';
3747              const r = rows[items[0].dataIndex];
3748              if (r.fwd === null || r.spot === null) return '';
3749              const d = r.spot - r.fwd, sign = d >= 0 ? '+' : '−';
3750              const pct = Math.abs(d / r.fwd * 100).toFixed(1).replace('.', ',');
3751              return 'Rozdíl spot − forward: ' + sign + Math.abs(d).toFixed(1).replace('.', ',') + ' ' + ccyUnit('fwd')
3752                + ' (' + sign + pct + ' %)';
3753            }
3754          } }
3755      },
3756      scales: {
3757        y: { beginAtZero: true, title: {display: true, text: ccyUnit('fwd'), color: muted}, grid: {color: border}, ticks: {color: muted} },
3758        x: { grid: {display: false}, ticks: {color: muted, font: {size: 12, weight: '500'}} }
3759      }
3760    }
3761  });
3762}
3763
3764function toggleYear(year) {
3765  const btn = document.querySelector('[data-year="' + year + '"]');
3766  btn.classList.toggle('active');
3767  // Najdi dataset v Chart.js
3768  const idx = Object.keys(storageHistory).indexOf(year);
3769  if (storageHistoryChart && idx >= 0) {
3770    const meta = storageHistoryChart.getDatasetMeta(idx);
3771    meta.hidden = !btn.classList.contains('active');
3772    storageHistoryChart.update();
3773  }
3774}
3775
3776// ===== Client-side fetch z kurzy.cz JSONP (funguje i v běžném prohlížeči) =====
3777function fetchKurzyJSONP(chartId, cbName) {
3778  return new Promise((resolve, reject) => {
3779    const script = document.createElement('script');
3780    const timeoutId = setTimeout(() => { cleanup(); reject(new Error('Timeout: ' + chartId)); }, 15000);
3781    function cleanup() {
3782      clearTimeout(timeoutId);
3783      try { delete window[cbName]; } catch (e) { window[cbName] = undefined; }
3784      if (script.parentNode) script.parentNode.removeChild(script);
3785    }
3786    window[cbName] = function(data) { cleanup(); resolve(data); };
3787    script.onerror = function() { cleanup(); reject(new Error('JSONP script load failed: ' + chartId)); };
3788    script.src = 'https://idb.kurzy.cz/' + chartId + '/history?callback=' + cbName;
3789    document.head.appendChild(script);
3790  });
3791}
3792
3793function jsonpTsToTradingDate(tsMs) {
3794  // kurzy.cz timestamp = půlnoc CET (= 22:00 UTC předchozího dne v DST). +12 h posune do dne obchodu.
3795  const d = new Date(tsMs + 12 * 3600 * 1000);
3796  const y = d.getUTCFullYear();
3797  const m = String(d.getUTCMonth() + 1).padStart(2, '0');
3798  const day = String(d.getUTCDate()).padStart(2, '0');
3799  return y + '-' + m + '-' + day;
3800}
3801
3802function fmtDateCZShort(isoDate) {
3803  if (!isoDate) return '—';
3804  const parts = isoDate.split('-');
3805  if (parts.length !== 3) return isoDate;
3806  return parseInt(parts[2], 10) + '. ' + parseInt(parts[1], 10) + '.';
3807}
3808
3809function daysAgo(isoDate) {
3810  if (!isoDate) return null;
3811  const today = new Date();
3812  today.setHours(0,0,0,0);
3813  const d = new Date(isoDate + 'T00:00:00');
3814  return Math.round((today.getTime() - d.getTime()) / 86400000);
3815}
3816
3817function updateOverallFreshness() {
3818  // Najdi nejnovější datum napříč hlavními sériemi (Cal-29 vynechané — je trvale stale)
3819  const liveSeries = ['el27','el28','gas27','gas28','gasTTF'];
3820  let newest = null;
3821  liveSeries.forEach(sid => {
3822    const s = window.HISTORICAL_DATA && HISTORICAL_DATA.series && HISTORICAL_DATA.series[sid];
3823    if (s && s.points && s.points.length) {
3824      const d = s.points[s.points.length-1].date;
3825      if (!newest || d > newest) newest = d;
3826    }
3827  });
3828  const el = document.getElementById('data-freshness-header');
3829  if (!el) return;
3830  if (!newest) { el.innerHTML = ''; return; }
3831  const ago = daysAgo(newest);
3832  const months = ['ledna','února','března','dubna','května','června','července','srpna','září','října','listopadu','prosince'];
3833  const parts = newest.split('-');
3834  const human = parseInt(parts[2], 10) + '. ' + months[parseInt(parts[1], 10) - 1];
3835  let cls = 'fresh', icon = '✓';
3836  if (ago > 1 && ago <= 3) { cls = 'aging'; icon = '⚠'; }
3837  else if (ago > 3) { cls = 'stale'; icon = '⚠'; }
3838  el.innerHTML = '<span class="freshness ' + cls + '">' + icon + ' Data ke dni ' + human + ' (poslední trading close)</span>';
3839  // Cal-29 sub-status
3840  const cal29 = HISTORICAL_DATA?.series?.el29?.points;
3841  if (cal29 && cal29.length) {
3842    const c29 = cal29[cal29.length-1].date;
3843    const c29ago = daysAgo(c29);
3844    if (c29ago > 3) {
3845      const c29Parts = c29.split('-');
3846      const c29Human = parseInt(c29Parts[2], 10) + '. ' + months[parseInt(c29Parts[1], 10) - 1];
3847      el.innerHTML += '<span class="freshness stale" title="Cal-29 nemá veřejný zdroj — bez ENTSOE/EEX subscription se neaktualizuje">Cal-29: ' + c29Human + ' (' + c29ago + ' dní zpět)</span>';
3848    }
3849  }
3850}
3851
3852function updateFreshnessBadges() {
3853  try {
3854    if (!window.HISTORICAL_DATA || !HISTORICAL_DATA.series) {
3855      console.warn('updateFreshnessBadges: HISTORICAL_DATA not ready');
3856      return;
3857    }
3858    const seriesLabelMap = {
3859      el27: 'Cal-27', el28: 'Cal-28', el29: 'Cal-29',
3860      gas27: 'Cal-27', gas28: 'Cal-28', gas29: 'Cal-29', gasTTF: 'TTF'
3861    };
3862    const allKeys = Object.keys(HISTORICAL_DATA.series);
3863    console.log('updateFreshnessBadges: processing', allKeys.length, 'series');
3864    allKeys.forEach(function(sid) {
3865      try {
3866        const series = HISTORICAL_DATA.series[sid];
3867        if (!series || !series.points || !series.points.length) return;
3868        const last = series.points[series.points.length - 1];
3869        if (!last || !last.date) return;
3870        const badges = document.querySelectorAll('[data-fresh="' + sid + '"]');
3871        if (!badges.length) return;
3872        const ago = daysAgo(last.date);
3873        const seriesLabel = seriesLabelMap[sid] || sid;
3874        badges.forEach(function(badge) {
3875          try {
3876            const parent = badge.parentElement;
3877            const inMany = parent && parent.querySelectorAll('[data-fresh]').length > 1;
3878            let label = (inMany ? seriesLabel + ': ' : 'Aktualizováno ') + fmtDateCZShort(last.date);
3879            let cls = 'freshness';
3880            if (ago !== null && ago !== undefined) {
3881              if (ago <= 1) cls += ' fresh';
3882              else if (ago <= 3) { cls += ' aging'; if (!inMany) label += ' (' + ago + ' dny zpět)'; }
3883              else { cls += ' stale'; if (!inMany) label += ' (' + ago + ' dní zpět)'; }
3884            }
3885            badge.className = cls;
3886            badge.textContent = label;
3887            const val = (last.value !== undefined && last.value !== null) ? last.value : '?';
3888            badge.title = 'Série ' + sid + ': poslední bod ' + last.date + ' = ' + val + (ago !== null ? ' (' + ago + ' dní zpět)' : '');
3889          } catch (eBadge) {
3890            console.error('updateFreshnessBadges badge error for', sid, eBadge);
3891          }
3892        });
3893      } catch (eSeries) {
3894        console.error('updateFreshnessBadges series error for', sid, eSeries);
3895      }
3896    });
3897  // ENTSOE freshness badges
3898  ['entsoeGen', 'entsoeFlows'].forEach(key => {
3899    const badge = document.querySelector('[data-fresh="' + key + '"]');
3900    if (!badge || !window.entsoeData?.date) return;
3901    const ago = daysAgo(entsoeData.date);
3902    let cls = 'freshness';
3903    if (ago <= 2) cls += ' fresh';
3904    else if (ago <= 4) cls += ' aging';
3905    else cls += ' stale';
3906    badge.className = cls;
3907    badge.textContent = 'ENTSOE: ' + fmtDateCZShort(entsoeData.date);
3908  });
3909  // Speciální badge pro ote15Data
3910  const oteBadge = document.querySelector('[data-fresh="ote15"]');
3911  if (oteBadge && window.ote15Data) {
3912    const days = Object.keys(window.ote15Data).sort();
3913    const lastDay = days[days.length-1];
3914    const ago = daysAgo(lastDay);
3915    let label = 'Aktualizováno ' + fmtDateCZShort(lastDay);
3916    let cls = 'freshness';
3917    if (ago === 0) cls += ' fresh';
3918    else if (ago === 1) cls += ' fresh';
3919    else if (ago <= 3) cls += ' aging';
3920    else cls += ' stale';
3921    oteBadge.className = cls;
3922    oteBadge.textContent = label;
3923  }
3924  } catch (eFatal) {
3925    console.error('updateFreshnessBadges FATAL:', eFatal);
3926  }
3927}
3928
3929async function refreshFromKurzy() {
3930  const banner = document.getElementById('status-banner');
3931  banner.className = 'status-banner info';
3932  banner.textContent = 'Stahuji nejnovější PXE Cal-27 ceny z kurzy.cz…';
3933  const stamp = Date.now();
3934  const [el27Raw, gas27Raw] = await Promise.all([
3935    fetchKurzyJSONP('ch-22273-eur-1MWh', '__cbEl27_' + stamp),
3936    fetchKurzyJSONP('ch-22271-eur-1MWh', '__cbGas27_' + stamp)
3937  ]);
3938  function addPoints(seriesId, raw) {
3939    const series = HISTORICAL_DATA && HISTORICAL_DATA.series && HISTORICAL_DATA.series[seriesId];
3940    if (!series || !Array.isArray(series.points)) return 0;
3941    const existing = new Set(series.points.map(p => p.date));
3942    let added = 0;
3943    raw.forEach(function(row) {
3944      const ts = row[0], v = Number(row[1]);
3945      if (!Number.isFinite(ts) || !Number.isFinite(v)) return;
3946      const date = jsonpTsToTradingDate(ts);
3947      if (!existing.has(date)) {
3948        series.points.push({ date: date, value: v });
3949        existing.add(date);
3950        added++;
3951      }
3952    });
3953    series.points.sort(function(a, b) { return a.date.localeCompare(b.date); });
3954    return added;
3955  }
3956  const elAdded = addPoints('el27', el27Raw);
3957  const gasAdded = addPoints('gas27', gas27Raw);
3958  updateLatestPriceTables();
3959  updateFreshnessBadges();
3960  updateOverallFreshness();
3961  if (typeof drawPriceChart === 'function') {
3962    drawPriceChart('el', (priceChartState.el && priceChartState.el.range) || '1m');
3963    drawPriceChart('gas', (priceChartState.gas && priceChartState.gas.range) || '1m');
3964  }
3965  // Multi-řádkový status banner: jeden řádek per série
3966  banner.className = 'status-banner success';
3967  const lines = ['<strong>✓ Aktualizace dokončena</strong>'];
3968  const seriesLabels = {
3969    el27: 'el. Cal-27', el28: 'el. Cal-28', el29: 'el. Cal-29',
3970    gas27: 'plyn Cal-27', gas28: 'plyn Cal-28', gas29: 'plyn Cal-29', gasTTF: 'TTF front-month'
3971  };
3972  Object.entries(seriesLabels).forEach(([sid, label]) => {
3973    const series = HISTORICAL_DATA?.series?.[sid];
3974    if (!series || !series.points || !series.points.length) return;
3975    const last = series.points[series.points.length - 1];
3976    const ago = daysAgo(last.date);
3977    const added = sid === 'el27' ? elAdded : (sid === 'gas27' ? gasAdded : 0);
3978    let line = '• ' + label + ': ' + last.value.toFixed(2).replace('.', ',') + ' EUR/MWh (' + fmtDateCZShort(last.date);
3979    if (ago !== null && ago > 1) line += ', ' + ago + ' dní zpět';
3980    line += ')';
3981    if (added > 0) line += ' <strong>+' + added + ' nový</strong>';
3982    lines.push(line);
3983  });
3984  if (elAdded === 0 && gasAdded === 0) {
3985    lines.push('<em>Z kurzy.cz nic nového — data jsou aktuální. Cal-28/29 a TTF vyžadují Cowork (CORS).</em>');
3986  }
3987  banner.innerHTML = lines.join('<br>');
3988}
3989
3990// ===== ENTSOE Transparency Platform data (Hodinová výroba + Přeshraniční bilance) =====
3991window.entsoeHistory = {}; window.entsoeData = {date:null, generation_mw_hourly:{}, flows_mw_hourly:{}};
3992window.entsoeData = Object.assign({date:"2026-06-01",timezone_note:"CEST aligned (UTC+2). Hour 0 = CEST 00:00 of date."}, window.entsoeHistory["2026-06-01"]);
3993
3994// Mapa typů zdrojů → {cs popisek, barva}. Klíče pokrývají OBĚ varianty názvů
3995// z DB: plné ENTSOE názvy ("Fossil Gas") i zkrácené ("Gas") — aby graf fungoval
3996// bez ohledu na to, který běh data zapsal.
3997const ENTSOE_GEN_TYPES = {
3998  'Nuclear':                   { cs: 'Jaderná',          color: '#7c3aed' },
3999  'Fossil Brown coal/Lignite': { cs: 'Hnědé uhlí',       color: '#78350f' },
4000  'Lignite':                   { cs: 'Hnědé uhlí',       color: '#78350f' },
4001  'Fossil Hard coal':          { cs: 'Černé uhlí',       color: '#1c1917' },
4002  'HardCoal':                  { cs: 'Černé uhlí',       color: '#1c1917' },
4003  'Fossil Gas':                { cs: 'Plyn',             color: '#f59e0b' },
4004  'Gas':                       { cs: 'Plyn',             color: '#f59e0b' },
4005  'Fossil Coal-derived gas':   { cs: 'Plyn z uhlí',      color: '#b45309' },
4006  'CoalGas':                   { cs: 'Plyn z uhlí',      color: '#b45309' },
4007  'Fossil Oil':                { cs: 'Olej',             color: '#7f1d1d' },
4008  'Oil':                       { cs: 'Olej',             color: '#7f1d1d' },
4009  'Solar':                     { cs: 'Solární',          color: '#fbbf24' },
4010  'Wind Onshore':              { cs: 'Vítr onshore',     color: '#0ea5e9' },
4011  'WindOnshore':               { cs: 'Vítr onshore',     color: '#0ea5e9' },
4012  'Wind Offshore':             { cs: 'Vítr offshore',    color: '#0284c7' },
4013  'WindOffshore':              { cs: 'Vítr offshore',    color: '#0284c7' },
4014  'Hydro Run-of-river':        { cs: 'Vodní (průtočná)', color: '#1d4ed8' },
4015  'HydroROR':                  { cs: 'Vodní (průtočná)', color: '#1d4ed8' },
4016  'Hydro Water Reservoir':     { cs: 'Vodní (nádrž)',    color: '#1e3a8a' },
4017  'HydroReservoir':            { cs: 'Vodní (nádrž)',    color: '#1e3a8a' },
4018  'Hydro Pumped Storage':      { cs: 'Přečerpávací',     color: '#06b6d4' },
4019  'PumpedStorage':             { cs: 'Přečerpávací',     color: '#06b6d4' },
4020  'Biomass':                   { cs: 'Biomasa',          color: '#16a34a' },
4021  'Waste':                     { cs: 'Odpad',            color: '#65a30d' },
4022  'Other renewable':           { cs: 'Ostatní obnov.',   color: '#10b981' },
4023  'OtherRenewable':            { cs: 'Ostatní obnov.',   color: '#10b981' },
4024  'Geothermal':                { cs: 'Geotermální',      color: '#ea580c' },
4025  'Other':                     { cs: 'Ostatní',          color: '#71717a' }
4026};
4027
4028let entsoeGenChart = null, entsoeFlowsChart = null;
4029window.__entsoeCurrentDate = null;
4030
4031function getEntsoeAvailableDates() {
4032  return Object.keys(window.entsoeHistory || {}).sort();
4033}
4034
4035function setEntsoeDate(date) {
4036  const avail = getEntsoeAvailableDates();
4037  if (!avail.length) return;
4038  let target;
4039  if (date === 'latest') target = avail[avail.length - 1];
4040  else target = date;
4041  // Najdi nejbližší dostupný (pokud zadané datum není v historii)
4042  const note = document.getElementById('entsoe-day-note');
4043  if (window.entsoeHistory[target]) {
4044    window.__entsoeCurrentDate = target;
4045    window.entsoeData = Object.assign({date: target, timezone_note: 'CEST aligned (UTC+2). Hour 0 = CEST 00:00 of date.'}, window.entsoeHistory[target]);
4046    if (note) note.textContent = '';
4047  } else {
4048    if (note) note.textContent = 'Data pro ' + target + ' nejsou k dispozici. Dostupné: ' + avail.join(', ');
4049    return;
4050  }
4051  // Update picker UI
4052  const picker = document.getElementById('entsoe-gen-picker');
4053  if (picker) {
4054    picker.value = target;
4055    if (avail.length) { picker.min = avail[0]; picker.max = avail[avail.length-1]; }
4056  }
4057  drawEntsoeGenChart();
4058  drawEntsoeFlowsChart();
4059}
4060
4061function shiftEntsoeDate(delta) {
4062  const avail = getEntsoeAvailableDates();
4063  if (!avail.length) return;
4064  const cur = window.__entsoeCurrentDate || avail[avail.length - 1];
4065  const idx = avail.indexOf(cur);
4066  const newIdx = Math.max(0, Math.min(avail.length - 1, idx + delta));
4067  setEntsoeDate(avail[newIdx]);
4068}
4069
4070function drawEntsoeGenChart() {
4071  if (!window.entsoeData?.generation_mw_hourly) return;
4072  const muted = getCssVar('--text-muted') || '#71717a';
4073  const border = getCssVar('--border') || '#e4e4e7';
4074  const strong = getCssVar('--text-strong') || '#09090b';
4075  const bgCard = getCssVar('--bg-card') || '#fff';
4076  const gen = entsoeData.generation_mw_hourly;
4077  const labels = Array.from({length: 24}, (_, i) => String(i).padStart(2,'0') + ':00');
4078  // Sort series by total magnitude desc for nicer stacking
4079  const sortedKeys = Object.keys(gen).sort((a, b) => {
4080    const sa = gen[a].reduce((s,v)=>s+Math.abs(v),0);
4081    const sb = gen[b].reduce((s,v)=>s+Math.abs(v),0);
4082    return sb - sa;
4083  });
4084  const datasets = sortedKeys.map(k => {
4085    const t = ENTSOE_GEN_TYPES[k] || { cs: k, color: '#a3a3a3' };
4086    return {
4087      label: t.cs,
4088      data: gen[k],
4089      backgroundColor: paletteColor(t.color) + 'cc',
4090      borderColor: paletteColor(t.color),
4091      borderWidth: 0.5,
4092      fill: true,
4093      pointRadius: 0,
4094      tension: 0.2
4095    };
4096  });
4097  if (entsoeGenChart) entsoeGenChart.destroy();
4098  const ctx = document.getElementById('entsoe-gen-chart').getContext('2d');
4099  entsoeGenChart = new Chart(ctx, {
4100    type: 'line',
4101    data: { labels: labels, datasets: datasets },
4102    options: {
4103      responsive: true, maintainAspectRatio: false,
4104      interaction: { mode: 'index', intersect: false },
4105      plugins: {
4106        legend: { display: true, position: 'top', labels: { color: muted, usePointStyle: true, pointStyle: 'rectRounded', boxWidth: 12, boxHeight: 8, padding: 8, font: {size: 11} } },
4107        tooltip: { backgroundColor: strong, titleColor: bgCard, bodyColor: bgCard, padding: 10, cornerRadius: 8,
4108          callbacks: { label: ctx => ctx.dataset.label + ': ' + Math.round(ctx.parsed.y).toLocaleStr
4108ing('cs-CZ') + ' MW' } }
4109      },
4110      scales: {
4111        y: { stacked: true, beginAtZero: true, title: {display: true, text: 'MW', color: muted}, grid: {color: border}, ticks: {color: muted, callback: v => Math.round(v).toLocaleString('cs-CZ')} },
4112        x: { title: {display: true, text: 'Hodina (CET)', color: muted}, grid: {display: false}, ticks: {color: muted, maxRotation: 0} }
4113      }
4114    }
4115  });
4116  const dateEl = document.getElementById('entsoe-gen-date');
4117  if (dateEl && entsoeData.date) {
4118    const p = entsoeData.date.split('-');
4119    dateEl.textContent = parseInt(p[2], 10) + '. ' + parseInt(p[1], 10) + '. ' + p[0];
4120  }
4121}
4122
4123// ===== Výroba jednotlivých elektráren ČR (ENTSOE A73) =====
4124window.entsoeUnitHistory = window.entsoeUnitHistory || {};
4125window.entsoeUnitData = { date: null, units: {} };
4126let entsoeUnitChart = null;
4127window.__entsoeUnitDate = null;
4128
4129const ENTSOE_PLANT_PALETTE = {
4130  'Temelín':            '#7c3aed',  // jádro – fialová
4131  'Dukovany':           '#a78bfa',  // jádro – světle fialová
4132  'Počerady':           '#78350f',  // hnědé uhlí
4133  'Prunéřov II':        '#92400e',
4134  'Tušimice II':        '#b45309',
4135  'Chvaletice':         '#a16207',
4136  'Ledvice':            '#854d0e',
4137  'Tisová':             '#a8a29e',
4138  'Mělník':             '#713f12',
4139  'Vřesová':            '#f59e0b',  // plyn – amber
4140  'Kladno':             '#dc2626',  // uhlí/plyn – červená pro odlišení
4141  'Dalešice (PVE)':     '#06b6d4',  // přečerpávací – cyan
4142  'Dlouhé Stráně (PVE)':'#0891b2'
4143};
4144const PLANT_FALLBACK = ['#64748b','#0ea5e9','#16a34a','#db2777','#ca8a04','#4f46e5'];
4145
4146window.__unitPeriod = window.__unitPeriod || 1;   // počet dní v okně (1 = jeden den)
4147window.__unitGroup = window.__unitGroup || 'plant'; // 'plant' | 'fuel'
4148
4149// Elektrárna → palivo (pro seskupení) a barvy palivových skupin
4150const UNIT_FUEL = {
4151  'Temelín':'Jádro', 'Dukovany':'Jádro',
4152  'Počerady':'Hnědé uhlí', 'Prunéřov II':'Hnědé uhlí', 'Tušimice II':'Hnědé uhlí',
4153  'Chvaletice':'Hnědé uhlí', 'Ledvice':'Hnědé uhlí', 'Tisová':'Hnědé uhlí', 'Mělník':'Hnědé uhlí',
4154  'Vřesová':'Zemní plyn', 'Kladno':'Ostatní',
4155  'Dalešice (PVE)':'Přečerpávací', 'Dlouhé Stráně (PVE)':'Přečerpávací'
4156};
4157const FUEL_COLOR = { 'Jádro':'#7c3aed', 'Hnědé uhlí':'#78350f', 'Zemní plyn':'#f59e0b',
4158                     'Přečerpávací':'#06b6d4', 'Ostatní':'#71717a' };
4159
4160function getEntsoeUnitDates() { return Object.keys(window.entsoeUnitHistory || {}).sort(); }
4161
4162function setEntsoeUnitDate(date) {
4163  const avail = getEntsoeUnitDates();
4164  if (!avail.length) return;
4165  const target = (date === 'latest') ? avail[avail.length - 1] : date;
4166  const note = document.getElementById('entsoe-unit-note');
4167  if (window.entsoeUnitHistory[target]) {
4168    window.__entsoeUnitDate = target;
4169    if (note) note.textContent = '';
4170  } else {
4171    if (note) note.textContent = 'Data pro ' + target + ' nejsou k dispozici.';
4172    return;
4173  }
4174  const picker = document.getElementById('entsoe-unit-picker');
4175  if (picker) { picker.value = target; picker.min = avail[0]; picker.max = avail[avail.length - 1]; }
4176  drawEntsoeUnitChart();
4177}
4178function shiftEntsoeUnitDate(delta) {
4179  const avail = getEntsoeUnitDates();
4180  if (!avail.length) return;
4181  const cur = window.__entsoeUnitDate || avail[avail.length - 1];
4182  const idx = avail.indexOf(cur);
4183  setEntsoeUnitDate(avail[Math.max(0, Math.min(avail.length - 1, idx + delta))]);
4184}
4185function setUnitPeriod(p) {
4186  window.__unitPeriod = parseInt(p, 10);
4187  document.querySelectorAll('[data-unit-period]').forEach(b => b.classList.toggle('active', b.dataset.unitPeriod === String(p)));
4188  drawEntsoeUnitChart();
4189}
4190function setUnitGroup(g) {
4191  window.__unitGroup = g;
4192  document.querySelectorAll('[data-unit-group]').forEach(b => b.classList.toggle('active', b.dataset.unitGroup === g));
4193  drawEntsoeUnitChart();
4194}
4195
4196// Spočítej profil {klíč: [24]} jako PRŮMĚR za posledních N dní končících zvoleným dnem.
4197function unitProfile() {
4198  const avail = getEntsoeUnitDates();
4199  if (!avail.length) return { data: {}, days: [] };
4200  const end = window.__entsoeUnitDate || avail[avail.length - 1];
4201  const endIdx = Math.max(0, avail.indexOf(end));
4202  const N = window.__unitPeriod || 1;
4203  const days = avail.slice(Math.max(0, endIdx - N + 1), endIdx + 1);
4204  const acc = {};
4205  days.forEach(d => {
4206    const day = window.entsoeUnitHistory[d] || {};
4207    Object.keys(day).forEach(pl => {
4208      const arr = acc[pl] || (acc[pl] = new Array(24).fill(0));
4209      for (let h = 0; h < 24; h++) arr[h] += (day[pl][h] || 0);
4210    });
4211  });
4212  const cnt = days.length || 1;
4213  const out = {};
4214  if (window.__unitGroup === 'fuel') {
4215    Object.keys(acc).forEach(pl => {
4216      const f = UNIT_FUEL[pl] || 'Ostatní';
4217      const arr = out[f] || (out[f] = new Array(24).fill(0));
4218      for (let h = 0; h < 24; h++) arr[h] += acc[pl][h] / cnt;
4219    });
4220  } else {
4221    Object.keys(acc).forEach(pl => { out[pl] = acc[pl].map(v => v / cnt); });
4222  }
4223  return { data: out, days: days };
4224}
4225
4226function drawEntsoeUnitChart() {
4227  const muted = getCssVar('--text-muted') || '#71717a';
4228  const border = getCssVar('--border') || '#e4e4e7';
4229  const strong = getCssVar('--text-strong') || '#09090b';
4230  const bgCard = getCssVar('--bg-card') || '#fff';
4231  const { data: u, days } = unitProfile();
4232  const labels = Array.from({length: 24}, (_, i) => String(i).padStart(2,'0') + ':00');
4233  const sortedKeys = Object.keys(u).sort((a, b) =>
4234    u[b].reduce((s,v)=>s+Math.abs(v),0) - u[a].reduce((s,v)=>s+Math.abs(v),0));
4235  let fb = 0;
4236  const datasets = sortedKeys.map(k => {
4237    const color = (window.__unitGroup === 'fuel')
4238      ? (FUEL_COLOR[k] || '#a3a3a3')
4239      : (ENTSOE_PLANT_PALETTE[k] || PLANT_FALLBACK[(fb++) % PLANT_FALLBACK.length]);
4240    return { label: k, data: u[k], backgroundColor: color + 'cc', borderColor: color,
4241             borderWidth: 0.5, fill: true, pointRadius: 0, tension: 0.2 };
4242  });
4243  if (entsoeUnitChart) entsoeUnitChart.destroy();
4244  const ctx = document.getElementById('entsoe-unit-chart').getContext('2d');
4245  entsoeUnitChart = new Chart(ctx, {
4246    type: 'line',
4247    data: { labels: labels, datasets: datasets },
4248    options: {
4249      responsive: true, maintainAspectRatio: false,
4250      interaction: { mode: 'index', intersect: false },
4251      plugins: {
4252        legend: { display: true, position: 'top', labels: { color: muted, usePointStyle: true, pointStyle: 'rectRounded', boxWidth: 12, boxHeight: 8, padding: 8, font: {size: 11} } },
4253        tooltip: { backgroundColor: strong, titleColor: bgCard, bodyColor: bgCard, padding: 10, cornerRadius: 8,
4254          callbacks: { label: ctx => ctx.dataset.label + ': ' + Math.round(ctx.parsed.y).toLocaleString('cs-CZ') + ' MW' } }
4255      },
4256      scales: {
4257        y: { stacked: true, beginAtZero: true, title: {display: true, text: 'MW', color: muted}, grid: {color: border}, ticks: {color: muted, callback: v => Math.round(v).toLocaleString('cs-CZ')} },
4258        x: { title: {display: true, text: 'Hodina (CET)', color: muted}, grid: {display: false}, ticks: {color: muted, maxRotation: 0} }
4259      }
4260    }
4261  });
4262  const dateEl = document.getElementById('entsoe-unit-date');
4263  if (dateEl && days.length) {
4264    const fmt = s => { const p = s.split('-'); return parseInt(p[2],10) + '. ' + parseInt(p[1],10) + '.'; };
4265    dateEl.textContent = (days.length === 1)
4266      ? fmt(days[0]) + ' ' + days[0].slice(0,4)
4267      : 'Ø ' + days.length + ' dní (' + fmt(days[0]) + '–' + fmt(days[days.length-1]) + ' ' + days[days.length-1].slice(0,4) + ')';
4268  }
4269}
4270
4271// ===== Teplárenství — statická kurátorská data (roční, z veřejných zdrojů) =====
4272// Pro teplo NEEXISTUJE real-time/hodinový open feed (na rozdíl od elektřiny/ENTSOE).
4273// ERÚ Roční zprávy o provozu teplárenských soustav ČR (XLSX, list 4.1 — výroba tepla
4274// brutto podle paliv, celá ČR, TJ). Podíly spočteny z oficiálních ročních součtů,
4275// extrakce 2026-06-10 z eru.gov.cz (eruteplo2021–2024.xlsx). Žádné odhady.
4276const HEAT_FUEL_COLORS = { hnede: '#78350f', plyn: '#f59e0b', biomasa: '#16a34a', cerne: '#1c1917', ostatni: '#71717a' };
4277const HEAT_FUEL_BY_YEAR = {
4278  '2021': { total_tj: 161657, hnede: 37.5, plyn: 21.3, biomasa: 14.7, cerne: 9.0, ostatni: 17.5 },
4279  '2022': { total_tj: 151093, hnede: 39.1, plyn: 19.6, biomasa: 15.0, cerne: 8.3, ostatni: 18.0 },
4280  '2023': { total_tj: 140923, hnede: 38.5, plyn: 19.5, biomasa: 15.5, cerne: 7.7, ostatni: 18.8 },
4281  '2024': { total_tj: 132837, hnede: 35.2, plyn: 20.9, biomasa: 19.3, cerne: 5.9, ostatni: 18.7 }
4282};
4283window.__heatFuelYear = '2024';
4284function setHeatFuelYear(y) {
4285  window.__heatFuelYear = y;
4286  document.querySelectorAll('[data-heat-fuel-year]').forEach(b => b.classList.toggle('active', b.dataset.heatFuelYear === y));
4287  drawHeatFuelChart();
4288}
4289// ERÚ Roční zprávy o provozu teplárenských soustav ČR (XLSX, list 5.2 — dodávky tepla
4290// v krajích, TJ/rok). Oficiální roční součty, extrakce 2026-06-10 z eru.gov.cz.
4291// (Původní per-operátor data nahrazena: konzistentní řada po letech pro operátory
4292// veřejně neexistuje — mix kalendářních/hospodářských roků a výroby/prodeje.)
4293const HEAT_KRAJE_BY_YEAR = {
4294  '2021': { 'Praha': 4513, 'Jihočeský': 5113, 'Jihomoravský': 5788, 'Karlovarský': 3504, 'Vysočina': 1747, 'Královéhradecký': 3086, 'Liberecký': 2233, 'Moravskoslezský': 16076, 'Olomoucký': 3554, 'Pardubický': 4406, 'Plzeňský': 4366, 'Středočeský': 21200, 'Ústecký': 12750, 'Zlínský': 4092 },
4295  '2022': { 'Praha': 3793, 'Jihočeský': 4658, 'Jihomoravský': 5176, 'Karlovarský': 3258, 'Vysočina': 1539, 'Královéhradecký': 2837, 'Liberecký': 1952, 'Moravskoslezský': 14000, 'Olomoucký': 3153, 'Pardubický': 3903, 'Plzeňský': 3908, 'Středočeský': 18710, 'Ústecký': 11516, 'Zlínský': 3668 },
4296  '2023': { 'Praha': 3382, 'Jihočeský': 4235, 'Jihomoravský': 4828, 'Karlovarský': 3265, 'Vysočina': 1466, 'Královéhradecký': 2685, 'Liberecký': 1869, 'Moravskoslezský': 12536, 'Olomoucký': 3010, 'Pardubický': 3680, 'Plzeňský': 3727, 'Středočeský': 16946, 'Ústecký': 10840, 'Zlínský': 3331 },
4297  '2024': { 'Praha': 3521, 'Jihočeský': 3961, 'Jihomoravský': 4746, 'Karlovarský': 3164, 'Vysočina': 1487, 'Královéhradecký': 2639, 'Liberecký': 1800, 'Moravskoslezský': 10002, 'Olomoucký': 2949, 'Pardubický': 3552, 'Plzeňský': 3713, 'Středočeský': 17103, 'Ústecký': 11167, 'Zlínský': 3199 }
4298};
4299window.__heatPlantsYear = '2024';
4300function setHeatPlantsYear(y) {
4301  window.__heatPlantsYear = y;
4302  document.querySelectorAll('[data-heat-plants-year]').forEach(b => b.classList.toggle('active', b.dataset.heatPlantsYear === y));
4303  drawHeatPlantsChart();
4304}
4305let heatFuelChart = null, heatPlantsChart = null;
4306
4307// ===== Sekce 18: Energeticke vyuziti odpadu a TAP (MPO 1989-2024) =====
4308// Zdroj: MPO "Statistika energetickeho vyuzivani odpadu a alternativnich paliv 
43081989-2024"
4309// (rijen 2025), str. 20 (sektory) a str. 15 (cementarny). Extrahovano z PDF 2026-06-11,
4310// soucty krizove overeny. Aktualizace 1x rocne po vydani MPO (~rijen).
4311const TAP_YEARS = [1989, 1990, 1991, 1992, 1993, 1994, 1995, 1996, 1997, 1998, 1999, 2000, 2001, 2002, 2003, 2004, 2005, 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019, 2020, 2021, 2022, 2023, 2024];
4312const TAP_S_TKO = [44685, 65550, 57710, 82200, 101800, 156425, 163115, 171000, 174127, 244535, 284646, 333572, 382025, 403178, 407820, 409288, 388303, 391930, 391620, 376381, 360399, 469003, 613082, 634280, 637627, 642806, 631908, 666018, 702831, 668258, 684918, 715968, 720391, 701379, 815666, 830348];
4313const TAP_S_PRO = [12050, 14100, 16880, 19810, 27050, 38355, 43065, 43290, 45371, 45588, 46572, 38396, 52461, 56144, 65835, 64553, 61347, 59038, 76086, 78025, 68029, 72341, 78547, 80130, 78551, 82333, 89130, 81313, 89150, 94283, 91420, 90373, 86795, 80541, 79527, 65777];
4314const TAP_S_CEM = [0, 0, 0, 0, 0, 0, 0, 0, 20594, 37824, 43112, 66497, 90486, 77980, 90534, 151368, 187504, 174708, 194904, 219764, 245543, 252708, 268552, 296476, 274142, 336658, 372506, 396769, 395187, 439781, 465513, 470904, 486565, 469657, 380160, 390928];
4315const TAP_S_ENE = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 9745, 2002, 1228, 2420, 3189, 3875, 1807, 1978, 6567, 4765, 5462, 4429, 4467, 2952, 2266, 2629, 30537, 71732];
4316const TAP_S_TOTAL = [56735, 79650, 74590, 102010, 128850, 194780, 206180, 214290, 240092, 327947, 374330, 438465, 524972, 537302, 564189, 625209, 637154, 625676, 672355, 676172, 675199, 796472, 963370, 1014761, 992127, 1063775, 1100111, 1148865, 1192480, 1207170, 1248044, 1281386, 1297594, 1255587, 1306598, 1359120];
4317const TAP_S_GJ = [688550, 938700, 914020, 1204075, 1531864, 2266111, 2386593, 2456393, 3085501, 4150493, 4625892, 5561421, 6865857, 6727959, 7318852, 8832778, 9545248, 9039837, 10308320, 10376719, 10475555, 11033230, 12552627, 12981617, 12617584, 14038061, 14950786, 16330153, 16650802, 18191055, 19039972, 19440981, 19248854, 18717926, 17959064, 19105142];
4318const TAP_CEM_YEARS = [1997, 1998, 1999, 2000, 2001, 2002, 2003, 2004, 2005, 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019, 2020, 2021, 2022, 2023, 2024];
4319const TAP_C_TAP = [70, 75, 3489, 7191, 9856, 22211, 31850, 51537, 68704, 66111, 104510, 107131, 146142, 165010, 180307, 203198, 176260, 232674, 261021, 279822, 290983, 317496, 340013, 350661, 367935, 345505, 281354, 266775];
4320const TAP_C_ADIT = [0, 0, 1069, 9829, 15029, 13579, 14521, 18562, 12923, 19780, 18711, 20620, 18844, 19680, 16406, 27447, 28655, 32479, 29044, 14227, 18136, 2745, 4122, 3509, 1773, 364, 1276, 1260];
4321const TAP_C_PNEU = [13709, 15316, 18242, 22517, 27640, 27101, 27918, 36796, 42893, 37319, 38127, 44411, 44902, 45537, 50756, 47252, 49064, 34134, 46892, 66977, 57702, 62818, 71203, 57702, 58560, 77083, 55783, 68039];
4322const TAP_C_KAP = [6815, 22433, 20312, 26939, 36387, 10979, 11101, 15458, 21703, 18842, 12512, 14870, 8128, 6130, 5837, 5247, 2678, 6713, 5791, 10995, 11824, 10474, 10422, 10918, 7713, 7016, 3227, 6943];
4323const TAP_C_OZE = [0, 0, 0, 21, 1574, 4110, 5144, 29015, 41283, 32657, 21045, 32732, 27528, 16351, 15246, 13332, 17485, 20273, 19422, 18196, 11329, 36780, 23706, 24239, 28656, 20907, 21504, 30233];
4324const TAP_C_ODPAD = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 10385, 10336, 6552, 5213, 9468, 16048, 23875, 21928, 18782, 17016, 17677];
4325const TAP_C_TOTAL = [20594, 37824, 43112, 66497, 90486, 77980, 90534, 151368, 187504, 174708, 194904, 219764, 245543, 252708, 268552, 296476, 274142, 336658, 372506, 396769, 395187, 439781, 465513, 470904, 486565, 469657, 380160, 390928];
4326const TAP_C_GJ = [561618, 1036236, 1148205, 1721110, 2345216, 1901382, 2201627, 3535648, 4445136, 4199668, 4556955, 5025604, 5597459, 5531634, 5662907, 5885365, 5575666, 6975079, 7781882, 8938830, 8709818, 10489698, 10988700, 11070601, 10908448, 10599523, 8456319, 8986321];
4327let tapChart = null;
4328window.__tapView = 'sectors';
4329function setTapView(v) {
4330  window.__tapView = v;
4331  document.querySelectorAll('[data-tap-view]').forEach(b => b.classList.toggle('active', b.dataset.tapView === v));
4332  const ns = document.getElementById('tap-note-sectors'), nc = document.getElementById('tap-note-cement');
4333  if (ns) ns.style.display = v === 'sectors' ? '' : 'none';
4334  if (nc) nc.style.display = v === 'cement' ? '' : 'none';
4335  drawTapChart();
4336}
4337function drawTapChart() {
4338  const el = document.getElementById('tap-chart'); if (!el) return;
4339  const muted = getCssVar('--text-muted') || '#71717a';
4340  const border = getCssVar('--border') || '#e4e4e7';
4341  const strong = getCssVar('--text-strong') || '#09090b';
4342  const bgCard = getCssVar('--bg-card') || '#fff';
4343  let labels, datasets, y1title;
4344  if (window.__tapView === 'cement') {
4345    labels = TAP_CEM_YEARS.map(String);
4346    y1title = 'tuny / rok';
4347    const mk = (lbl, data, color) => ({ label: lbl, data: data, backgroundColor: paletteColor(color), stack: 's', borderRadius: 2, maxBarThickness: 26 });
4348    datasets = [
4349      mk('Čistá TAP', TAP_C_TAP, '#3b82f6'),
4350      mk('Pneumatiky', TAP_C_PNEU, '#1c1917'),
4351      mk('Kapalná alt. paliva', TAP_C_KAP, '#f59e0b'),
4352      mk('OZE / biomasa', TAP_C_OZE, '#16a34a'),
4353      mk('Odpady přímo', TAP_C_ODPAD, '#71717a'),
4354      mk('Tuhá aditivní', TAP_C_ADIT, '#a78bfa')
4355    ];
4356  } else {
4357    labels = TAP_YEARS.map(String);
4358    y1title = 'tuny / rok';
4359    const mk = (lbl, data, color) => ({ label: lbl, data: data, backgroundColor: paletteColor(color), stack: 's', borderRadius: 2, maxBarThickness: 26 });
4360    datasets = [
4361      mk('Spalovny TKO (ZEVO)', TAP_S_TKO, '#dc2626'),
4362      mk('Cementárny a vápenky', TAP_S_CEM, '#78350f'),
4363      mk('Prům. a nemocniční spalovny', TAP_S_PRO, '#f59e0b'),
4364      mk('Energetika a teplárenství', TAP_S_ENE, '#10b981'),
4365      { label: 'Energie v palivu (TJ)', type: 'line', yAxisID: 'y2',
4366        data: TAP_S_GJ.map(v => Math.round(v / 1000)),
4367        borderColor: muted, borderDash: [5, 4], borderWidth: 1.5, pointRadius: 0, pointHoverRadius: 3, tension: 0.25 }
4368    ];
4369  }
4370  if (tapChart) tapChart.destroy();
4371  tapChart = new Chart(el.getContext('2d'), {
4372    type: 'bar',
4373    data: { labels: labels, datasets: datasets },
4374    options: {
4375      responsive: true, maintainAspectRatio: false, animation: false,
4376      interaction: { mode: 'index', intersect: false },
4377      plugins: {
4378        legend: { display: true, position: 'top', labels: { color: muted, usePointStyle: true, pointStyle: 'rectRounded', boxWidth: 12, boxHeight: 8, padding: 10, font: {size: 11, weight: '500'} } },
4379        tooltip: { backgroundColor: strong, titleColor: bgCard, bodyColor: bgCard, padding: 10, cornerRadius: 8,
4380          callbacks: {
4381            label: c =>
4381 c.dataset.label + ': ' + c.parsed.y.toLocaleString('cs-CZ') + (c.dataset.yAxisID === 'y2' ? ' TJ' : ' t'),
4382            footer: items => {
4383              const i = items[0].dataIndex;
4384              if (window.__tapView === 'cement') {
4385                return 'Celkem: ' + TAP_C_TOTAL[i].toLocaleString('cs-CZ') + ' t | ' + Math.round(TAP_C_GJ[i] / 1000).toLocaleString('cs-CZ') + ' TJ';
4386              }
4387              return 'Celkem (dle MPO): ' + TAP_S_TOTAL[i].toLocaleString('cs-CZ') + ' t';
4388            }
4389          } }
4390      },
4391      scales: {
4392        x: { stacked: true, grid: {display: false}, ticks: {color: muted, font: {size: 10}, maxRotation: 60, autoSkip: true} },
4393        y: { stacked: true, beginAtZero: true, title: {display: true, text: y1title, color: muted}, grid: {color: border}, ticks: {color: muted, callback: v => v.toLocaleString('cs-CZ')} },
4394        y2: { display: window.__tapView !== 'cement', position: 'right', beginAtZero: true, title: {display: true, text: 'TJ', color: muted}, grid: {display: false}, ticks: {color: muted, callback: v => v.toLocaleString('cs-CZ')} }
4395      }
4396    }
4397  });
4398}
4399
4400function drawHeatFuelChart() {
4401  const el = document.getElementById('heat-fuel-chart'); if (!el) return;
4402  const muted = getCssVar('--text-muted') || '#71717a';
4403  const strong = getCssVar('--text-strong') || '#09090b';
4404  const bgCard = getCssVar('--bg-card') || '#fff';
4405  const yr = window.__heatFuelYear || '2024';
4406  const f = HEAT_FUEL_BY_YEAR[yr];
4407  const d = { total_tj: f.total_tj, items: [
4408    { label: 'Hnědé uhlí',                 pct: f.hnede,   color: HEAT_FUEL_COLORS.hnede },
4409    { label: 'Zemní plyn',                 pct: f.plyn,    color: HEAT_FUEL_COLORS.plyn },
4410    { label: 'Biomasa',                    pct: f.biomasa, color: paletteColor(HEAT_FUEL_COLORS.biomasa) },
4411    { label: 'Černé uhlí',                 pct: f.cerne,   color: HEAT_FUEL_COLORS.cerne },
4412    { label: 'Ostatní (odpad, druhotné…)', pct: f.ostatni, color: HEAT_FUEL_COLORS.ostatni }
4413  ]};
4414  const totalEl = document.getElementById('heat-fuel-total');
4415  if (totalEl) totalEl.textContent = f.total_tj.toLocaleString('cs-CZ') + ' TJ (rok ' + yr + ')';
4416  if (heatFuelChart) heatFuelChart.destroy();
4417  heatFuelChart = new Chart(el.getContext('2d'), {
4418    type: 'doughnut',
4419    data: { labels: d.items.map(i => i.label), datasets: [{ data: d.items.map(i => i.pct), backgroundColor: d.items.map(i => i.color), borderColor: bgCard, borderWidth: 2 }] },
4420    options: { responsive: true, maintainAspectRatio: false,
4421      plugins: {
4422        legend: { position: 'right', labels: { color: muted, font: {size: 12}, padding: 10, usePointStyle: true, pointStyle: 'rectRounded' } },
4423        tooltip: { backgroundColor: strong, titleColor: bgCard, bodyColor: bgCard, padding: 10, cornerRadius: 8,
4424          callbacks: { label: c => c.label + ': ' + c.parsed.toFixed(1).replace('.', ',') + ' % (' + Math.round(d.total_tj * c.parsed / 100).toLocaleString('cs-CZ') + ' TJ)' } }
4425      } }
4426  });
4427}
4428function drawHeatPlantsChart() {
4429  const el = document.getElementById('heat-plants-chart'); if (!el) return;
4430  const muted = getCssVar('--text-muted') || '#71717a';
4431  const border = getCssVar('--border') || '#e4e4e7';
4432  const strong = getCssVar('--text-strong') || '#09090b';
4433  const bgCard = getCssVar('--bg-card') || '#fff';
4434  const accent = getCssVar('--accent') || '#dc2626';
4435  const yr = window.__heatPlantsYear || '2024';
4436  const prevYr = String(parseInt(yr, 10) - 1);
4437  const kraje = HEAT_KRAJE_BY_YEAR[yr];
4438  const prevKraje = HEAT_KRAJE_BY_YEAR[prevYr] || null;
4439  const it = Object.keys(kraje).map(k => {
4440    const prev = prevKraje ? prevKraje[k] : null;
4441    return { label: k, tj: kraje[k],
4442             yoyDelta: prev ? kraje[k] - prev : null,
4443             yoyPct: prev ? Math.round((kraje[k] - prev) / prev * 1000) / 10 : null };
4444  }).sort((a, b) => b.tj - a.tj);
4445  const total = it.reduce((s, i) => s + i.tj, 0);
4446  // Big čísla (stejný vzor jako sekce 14 — ceny tepla)
4447  const setTxt = (id, t) => { const e = document.getElementById(id); if (e) e.textContent = t; };
4448  setTxt('heat-plants-max', it[0].tj.toLocaleString('cs-CZ') + ' TJ');
4449  setTxt('heat-plants-max-label', it[0].label + ' (největší v r. ' + yr + ')');
4450  setTxt('heat-plants-min', it[it.length - 1].tj.toLocaleString('cs-CZ') + ' TJ');
4451  setTxt('heat-plants-min-label', it[it.length - 1].label + ' (nejmenší v r. ' + yr + ')');
4452  setTxt('heat-plants-sum', total.toLocaleString('cs-CZ'));
4453  setTxt('heat-plants-sum-label', 'TJ celkem ČR v r. ' + yr);
4454  const totalEl = document.getElementById('heat-plants-total');
4455  if (totalEl) totalEl.textContent = total.toLocaleString('cs-CZ') + ' TJ (rok ' + yr + ')';
4456  // Gradient dle hodnoty (stejně jako sekce 14): nejvíc = accent, nejméně = delta-down
4457  const max = it[0].tj, min = it[it.length - 1].tj;
4458  const colors = it.map(d => interpolateColor(getCssVar('--delta-down') || '#047857', accent, (d.tj - min) / (max - min)));
4459  if (heatPlantsChart) heatPlantsChart.destroy();
4460  heatPlantsChart = new Chart(el.getContext('2d'), {
4461    type: 'bar',
4462    data: { labels: it.map(i => i.label), datasets: [{ label: 'Roční dodávka tepla (TJ)', data: it.map(i => i.tj),
4463      backgroundColor: colors, borderColor: colors, borderWidth: 0, borderRadius: 6, borderSkipped: false }] },
4464    options: { indexAxis: 'y', responsive: true, maintainAspectRatio: false,
4465      plugins: {
4466        legend: { display: false },
4467        tooltip: { backgroundColor: strong, titleColor: bgCard, bodyColor: bgCard, padding: 12, cornerRadius: 8, displayColors: false,
4468          callbacks: { label: c => {
4469            const row = it[c.dataIndex];
4470            const lines = [c.parsed.x.toLocaleString('cs-CZ') + ' TJ (' + yr + ') — ' + (c.parsed.x / total * 100).toFixed(1).replace('.', ',') + ' % ČR'];
4471            if (row.yoyDelta !== null) {
4472              const sign = row.yoyDelta >= 0 ? '+' : '';
4473              lines.push('vs ' + prevYr + ': ' + sign + row.yoyDelta.toLocaleString('cs-CZ') + ' TJ (' + sign + String(row.yoyPct).replace('.', ',') + ' %)');
4474            }
4475            return lines;
4476          } } }
4477      },
4478      scales: {
4479        x: { title: {display: true, text: 'TJ / rok', color: muted}, grid: {color: border}, ticks: {color: muted, callback: v => v.toLocaleString('cs-CZ')} },
4480        y: { grid: {display: false}, ticks: {color: muted, font: {size: 11}} }
4481      } }
4482  });
4483}
4484
4485const ENTSOE_FLOW_PALETTE = {
4486  'DE': '#000000',   // černá (Německo)
4487  'SK': '#1e40af',   // modrá (Slovensko)
4488  'PL': '#dc2626',   // červená (Polsko)
4489  'AT': '#dc2626'    // červeno-bílá AT, ale modrá ji odliší
4490};
4491
4492function drawEntsoeFlowsChart() {
4493  if (!window.entsoeData?.flows_mw_hourly) return;
4494  const muted = getCssVar('--text-muted') || '#71717a';
4495  const border = getCssVar('--border') || '#e4e4e7';
4496  const strong = getCssVar('--text-strong') || '#09090b';
4497  const bgCard = getCssVar('--bg-card') || '#fff';
4498  const flows = entsoeData.flows_mw_hourly;
4499  const labels = Array.from({length: 24}, (_, i) => String(i).padStart(2,'0') + ':00');
4500  const nbrs = ['DE', 'SK', 'PL', 'AT'];
4501  // Kontrastní paleta, dobře čitelná na světlém i tmavém pozadí
4502  const nbrColors = {
4503    DE: '#f97316',  // oranžová
4504    SK: '#3b82f6',  // modrá
4505    PL: '#dc2626',  // červená
4506    AT: '#16a34a'   // zelená
4507  };
4508  // Grouped bary per soused (jako sekce 10 v multi-year módu) — žádný překryv barev.
4509  const datasets = nbrs.map(n => ({
4510    label: 'CZ ↔ ' + n,
4511    data: flows[n].net_export_cz,
4512    backgroundColor: nbrColors[n],
4513    borderRadius: 4,
4514    borderWidth: 0,
4515    order: 2
4516  }));
4517  // Net celkem CZ — tenká elegantní linka v popředí
4518  const total = labels.map((_, h) => nbrs.reduce((s, n) => s + flows[n].net_export_cz[h], 0));
4519  datasets.push({
4520    label: 'Net celkem CZ',
4521    data: total,
4522    type: 'line',
4523    borderColor: strong,
4524    backgroundColor: strong,
4525    borderWidth: 2,
4526    pointRadius: 3,
4527    pointHoverRadius: 6,
4528    pointBackgroundColor: bgCard,
4529    pointBorderColor: strong,
4530    pointBorderWidth: 1.5,
4531    tension: 0.35,
4532    fill: false,
4533    order: 0
4534  });
4535  if (entsoeFlowsChart) entsoeFlowsChart.destroy();
4536  const ctx = document.getElementById('entsoe-flows-chart').getContext('2d');
4537  entsoeFlowsChart = new Chart(ctx, {
4538    type: 'bar',
4539    data: { labels: labels, datasets: datasets },
4540    options: {
4541      responsive: true, maintainAspectRatio: false,
4542      interaction: { mode: 'index', intersect: false },
4543      plugins: {
4544        legend: { display: true, position: 'top', labels: { color: muted, usePointStyle: true, pointStyle: 'rectRounded', boxWidth: 12, boxHeight: 8 } },
4545        tooltip: { backgroundColor: strong, titleColor: bgCard, bodyColor: bgCard, padding: 10, cornerRadius: 8,
4546          callbacks: { label: ctx => {
4547            const v = ctx.parsed.y;
4548            const dir = v >= 0 ? 'export' : 'import';
4549            return ctx.dataset.label + ': ' + Math.round(Math.abs(v)).toLocaleString('cs-CZ') + ' MW (' + dir + ')';
4550          } } }
4551      },
4552      scales: {
4553        y: {
4554          title: {display: true, text: 'MW (+ export CZ / − import)', color: muted},
4555          grid: {
4556            color: function(ctx) { return ctx.tick.value === 0 ? strong : border; },
4557            lineWidth: function(ctx) { return ctx.tick.value === 0 ? 2 : 1; }
4558          },
4559          ticks: {color: muted, callback: v => Math.round(v).toLocaleString('cs-CZ')}
4560        },
4561        x: { grid: {display: false}, ticks: {color: muted, maxRotation: 0} }
4562      }
4563    }
4564  });
4565  const dateEl = document.getElementById('entsoe-flows-date');
4566  if (dateEl && entsoeData.date) {
4567    const p = entsoeData.date.split('-');
4568    dateEl.textContent = parseInt(p[2], 10) + '. ' + parseInt(p[1], 10) + '. ' + p[0];
4569  }
4570}
4571
4572// ===== Sekce 19: Ceny regulačních záloh (FCR/aFRR/mFRR) — ENTSO-E A15 =====
4573const BAL_SERIES = [
4574  { key: 'FCR_sym',   label: 'FCR (sym.)', color: '#7c3aed' },
4575  { key: 'aFRR_up',   label: 'aFRR ↑',     color: '#dc2626' },
4576  { key: 'aFRR_down', label: 'aFRR ↓',     color: '#fb923c' },
4577  { key: 'mFRR_up',   label: 'mFRR ↑',     color: '#0ea5e9' },
4578  { key: 'mFRR_down', label: 'mFRR ↓',     color: '#16a34a' }
4579];
4580const balChartState = { range: '3m', metric: 'avg', hidden: {} };
4581let balChart = null;
4582
4583function balRangeStartMs(range, lastMs) {
4584  if (range === 'all') return -Infinity;
4585  const d = new Date(lastMs);
4586  d.setMonth(d.getMonth() - ({ '1m': 1, '3m': 3, '6m': 6, '12m': 12 }[range] || 3));
4587  return d.getTime();
4588}
4589
4590function drawBalancingChart() {
4591  const cv = document.getElementById('bal-chart');
4592  if (!cv || typeof Chart === 'undefined') return;
4593  const data = window.balancingData || {};
4594  let lastMs = 0;
4595  BAL_SERIES.forEach(s => (data[s.key] || []).forEach(p => { const t = new Date(p.date).getTime(); if (t > lastMs) lastMs = t; }));
4596  if (!lastMs) lastMs = Date.now();
4597  const startMs = balRangeStartMs(balChartState.range, lastMs);
4598  const metric = balChartState.metric;
4599  const datasets = BAL_SERIES.map(s => ({
4600    label: s.label,
4601    data: (data[s.key] || [])
4602      .filter(p => new Date(p.date).getTime() >= startMs && p[metric] != null)
4603      .map(p => ({ x: new Date(p.date).getTime(), y: p[metric], mw: p.mw })),
4604    borderColor: s.color, backgroundColor: s.color + '22',
4605    borderWidth: 2, pointRadius: 0, tension: 0.15, spanGaps: true, parsing: false,
4606    hidden: !!balChartState.hidden[s.key]
4607  }));
4608  const muted = getCssVar('--text-muted') || '#71717a';
4609  const strong = getCssVar('--text-strong') || '#09090b';
4610  const border = getCssVar('--border') || '#e4e4e7';
4611  const bgCard = getCssVar('--bg-card') || '#fff';
4612  if (balChart) balChart.destroy();
4613  balChart = new Chart(cv.getContext('2d'), {
4614    type: 'line',
4615    data: { datasets },
4616    options: {
4617      responsive: true, maintainAspectRatio: false,
4618      interaction: { mode: 'nearest', intersect: false },
4619      plugins: {
4620        legend: { display: false },
4621        tooltip: {
4622          backgroundColor: strong, titleColor: bgCard, bodyColor: bgCard, borderColor: border,
4623          padding: 12, cornerRadius: 8, boxPadding: 4,
4624          callbacks: {
4625            title: items => items.length ? new Date(items[0].parsed.x).toLocaleDateString('cs-CZ') : '',
4626            label: ctx => {
4627              const mw = ctx.raw && ctx.raw.mw != null ? ', ' + Math.round(ctx.raw.mw) + ' MW' : '';
4628              return ctx.dataset.label + ': ' + (ctx.parsed.y == null ? '—' : ctx.parsed.y.toFixed(2).replace('.', ',') + ' EUR/MW' + mw);
4629            }
4630          }
4631        }
4632      },
4633      scales: {
4634        y: { type: 'linear', beginAtZero: true, title: { display: true, text: 'EUR/MW', color: muted, font: { size: 11 } },
4635             grid: { color: border, lineWidth: 1 }, ticks: { color: muted, font: { size: 11 } } },
4636        x: { type: 'linear', bounds: 'data', grid: { display: false }, afterDataLimits: captureXDataLimits, afterBuildTicks: calendarXTicks,
4637             ticks: { color: muted, autoSkip: false, maxRotation: 0, font: { size: 11 },
4638                      callback: adaptiveXTick } }
4639      }
4640    }
4641  });
4642  renderBalLegend();
4643}
4644
4645function renderBalLegend() {
4646  const el = document.getElementById('bal-legend');
4647  if (!el) return;
4648  el.innerHTML = BAL_SERIES.map(s => {
4649    const off = balChartState.hidden[s.key] ? 'opacity:.35;' : '';
4650    return '<span style="cursor:pointer;display:inline-flex;align-items:center;gap:5px;margin-right:14px;font-size:13px;' + off + '"'
4651      + ' onclick="toggleBalSeries(\'' + s.key + '\')">'
4652      + '<span style="display:inline-block;width:11px;height:11px;border-radius:3px;background:' + s.color + '"></span>'
4653      + s.label + '</span>';
4654  }).join('');
4655}
4656function toggleBalSeries(key) { balChartState.hidden[key] = !balChartState.hidden[key]; drawBalancingChart(); }
4657function setBalRange(btn, range) {
4658  balChartState.range = range;
4659  if (btn && btn.parentElement) btn.parentElement.querySelectorAll('[data-balrange]').forEach(b => b.classList.toggle('active', b === btn));
4660  drawBalancingChart();
4661}
4662function setBalMetric(btn, metric) {
4663  balChartState.metric = metric;
4664  if (btn && btn.parentElement) btn.parentElement.querySelectorAll('[data-balmetric]').forEach(b => b.classList.toggle('active', b === btn));
4665  drawBalancingChart();
4666}
4667
4668// ===== Sekce 20: Zásobníky plynu v Evropě (AGSI) — choropleth mapa + denní vývoj =====
4669const EU_STO_SERIES = [
4670  { code: 'EU', label: 'EU',          color: '#0ea5e9' },
4671  { code: 'CZ', label: 'Česko',       color: '#dc2626' },
4672  { code: 'DE', label: 'Německo',     color: '#f59e0b' },
4673  { code: 'FR', label: 'Francie',     color: '#7c3aed' },
4674  { code: 'IT', label: 'Itálie',      color: '#16a34a' },
4675  { code: 'NL', label: 'Nizozemsko',  color: '#0284c7' },
4676  { code: 'AT', label: 'Rakousko',    color: '#db2777' },
4677  { code: 'PL', label: 'Polsko',      color: '#65a30d' },
4678  { code: 'ES', label: 'Španělsko',   color: '#ea580c' },
4679  { code: 'HU', label: 'Maďarsko',    color: '#0891b2' }
4680];
4681const euStoState = { range: '6m', hidden: {} };
4682EU_STO_SERIES.forEach(s => { if (!['EU', 'CZ', 'DE'].includes(s.code)) euStoState.hidden[s.code] = true; });
4683let euStoTrendChart = null, euMapChart = null, __euGeo = null;
4684
4685function euStoRangeStart(range, lastMs) {
4686  if (range === 'all') return -Infinity;
4687  const d = new Date(lastMs);
4688  d.setMonth(d.getMonth() - ({ '3m': 3, '6m': 6, '12m': 12 }[range] || 6));
4689  return d.getTime();
4690}
4691
4692function drawEuStorageYears() {
4693  const cv = document.getElementById('eu-storage-trend'); if (!cv || typeof Chart === 'undefined') return;
4694  const hist = window.euStorageHistory || {};
4695  const legEl = document.getElementById('eu-storage-legend'); if (legEl) legEl.innerHTML = '<span style="font-size:12.5px;color:var(--text-muted,#71717a)">EU agregát — % naplnění po kalendářních týdnech</span>';
4696  const muted = getCssVar('--text-muted') || '#71717a', border = getCssVar('--border') || '#e4e4e7', strong = getCssVar('--text-strong') || '#09090b', bgCard = getCssVar('--bg-card') || '#fff';
4697  const yc = (typeof YEAR_COLORS !== 'undefined') ? YEAR_COLORS : {};
4698  const pc = (typeof paletteColor === 'function') ? paletteColor : (x => x);
4699  const datasets = Object.keys(hist).sort().map(year => ({
4700    label: year, data: hist[year].map(p => ({ x: p.week, y: p.value })),
4701    borderColor: pc(yc[year]) || undefined, backgroundColor: 'transparent',
4702    pointRadius: 0, pointHoverRadius: 4, borderWidth: year === '2026' ? 2.5 : 1.8, tension: 0.35
4703  }));
4704  const fy = Object.keys(hist).filter(y => y >= '2020' && y <= '2025');
4705  if (fy.length) {
4706    const sum = {}, n = {};
4707    fy.forEach(y => hist[y].forEach(p => { const w = Math.round(p.week); sum[w] = (sum[w] || 0) + p.value; n[w] = (n[w] || 0) + 1; }));
4708    datasets.push({ label: 'Průměr 2020–2025', data: Object.keys(sum).map(Number).sort((a, b) => a - b).map(w => ({ x: w, y: Math.round(sum[w] / n[w] * 10) / 10 })), borderColor: muted, backgroundColor: 'transparent', borderDash: [6, 4], pointRadius: 0, borderWidth: 2, tension: 0.35 });
4709  }
4710  if (euStoTrendChart) euStoTrendChart.destroy();
4711  euStoTrendChart = new Chart(cv.getContext('2d'), {
4712    type: 'line', data: { datasets },
4713    options: { responsive: true, maintainAspectRatio: false, interaction: { mode: 'nearest', intersect: false },
4714      plugins: { legend: { display: true, position: 'top', labels: { color: muted, usePointStyle: true, pointStyle: 'rectRounded', boxWidth: 12, boxHeight: 8, padding: 10, font: { size: 11 } } },
4715        tooltip: { backgroundColor: strong, titleColor: bgCard, bodyColor: bgCard, padding: 10, cornerRadius: 8, callbacks: { title: items => 'Týden ' + Math.round(items[0].parsed.x), label: it => it.dataset.label + ': ' + it.parsed.y.toFixed(1).replace('.', ',') + ' %' } } },
4716      scales: { x: { type: 'linear', min: 1, max: 53.2, title: { display: true, text: 'Kalendářní týden', color: muted, font: { size: 11 } }, ticks: { stepSize: 4, color: muted, font: { size: 11 } }, grid: { display: false } },
4717        y: { min: 0, max: 100, title: { display: true, text: '% naplnění', color: muted, font: { size: 11 } }, grid: { color: border }, ticks: { color: muted, font: { size: 11 } } } } }
4718  });
4719}
4720
4721function drawEuStorageTrend() {
4722  const cv = document.getElementById('eu-storage-trend');
4723  if (!cv || typeof Chart === 'undefined') return;
4724  if (euStoState.range === 'years') return drawEuStorageYears();
4725  const series = window.euStorageSeries || {};
4726  let lastMs = 0;
4727  EU_STO_SERIES.forEach(s =>
4727 (series[s.code] || []).forEach(p => { const t = new Date(p.date).getTime(); if (t > lastMs) lastMs = t; }));
4728  if (!lastMs) lastMs = Date.now();
4729  const startMs = euStoRangeStart(euStoState.range, lastMs);
4730  const datasets = EU_STO_SERIES.map(s => ({
4731    label: s.label,
4732    data: (series[s.code] || []).filter(p => new Date(p.date).getTime() >= startMs && p.full != null)
4733      .map(p => ({ x: new Date(p.date).getTime(), y: p.full, twh: p.twh })),
4734    borderColor: s.color, backgroundColor: s.color + '22',
4735    borderWidth: s.code === 'EU' ? 3 : 1.8, pointRadius: 0, tension: 0.15, spanGaps: true, parsing: false,
4736    hidden: !!euStoState.hidden[s.code]
4737  }));
4738  const muted = getCssVar('--text-muted') || '#71717a', strong = getCssVar('--text-strong') || '#09090b',
4739        border = getCssVar('--border') || '#e4e4e7', bgCard = getCssVar('--bg-card') || '#fff';
4740  if (euStoTrendChart) euStoTrendChart.destroy();
4741  euStoTrendChart = new Chart(cv.getContext('2d'), {
4742    type: 'line', data: { datasets },
4743    options: {
4744      responsive: true, maintainAspectRatio: false, interaction: { mode: 'nearest', intersect: false },
4745      plugins: {
4746        legend: { display: false },
4747        tooltip: {
4748          backgroundColor: strong, titleColor: bgCard, bodyColor: bgCard, borderColor: border, padding: 12, cornerRadius: 8, boxPadding: 4,
4749          callbacks: {
4750            title: items => items.length ? new Date(items[0].parsed.x).toLocaleDateString('cs-CZ') : '',
4751            label: ctx => ctx.dataset.label + ': ' + (ctx.parsed.y == null ? '—' : ctx.parsed.y.toFixed(1).replace('.', ',') + ' %')
4752              + (ctx.raw && ctx.raw.twh != null ? ' (' + ctx.raw.twh.toFixed(1) + ' TWh)' : '')
4753          }
4754        }
4755      },
4756      scales: {
4757        y: { type: 'linear', min: 0, max: 100, title: { display: true, text: '% naplnění', color: muted, font: { size: 11 } },
4758             grid: { color: border, lineWidth: 1 }, ticks: { color: muted, font: { size: 11 } } },
4759        x: { type: 'linear', bounds: 'data', grid: { display: false }, afterDataLimits: captureXDataLimits, afterBuildTicks: calendarXTicks,
4760             ticks: { color: muted, autoSkip: false, maxRotation: 0, font: { size: 11 },
4761                      callback: adaptiveXTick } }
4762      }
4763    }
4764  });
4765  renderEuStoLegend();
4766}
4767
4768function renderEuStoLegend() {
4769  const el = document.getElementById('eu-storage-legend');
4770  if (!el) return;
4771  el.innerHTML = EU_STO_SERIES.map(s => {
4772    const off = euStoState.hidden[s.code] ? 'opacity:.35;' : '';
4773    return '<span style="cursor:pointer;display:inline-flex;align-items:center;gap:5px;margin-right:12px;font-size:12.5px;' + off + '"'
4774      + ' onclick="toggleEuSto(\'' + s.code + '\')">'
4775      + '<span style="display:inline-block;width:11px;height:11px;border-radius:3px;background:' + s.color + '"></span>' + s.label + '</span>';
4776  }).join('');
4777}
4778function toggleEuSto(code) { euStoState.hidden[code] = !euStoState.hidden[code]; drawEuStorageTrend(); }
4779function setEuStoRange(btn, range) {
4780  euStoState.range = range;
4781  if (btn && btn.parentElement) btn.parentElement.querySelectorAll('[data-eustorange]').forEach(b => b.classList.toggle('active', b === btn));
4782  drawEuStorageTrend();
4783}
4784
4785async function drawEuStorageMap() {
4786  const cv = document.getElementById('eu-storage-map');
4787  if (!cv || typeof Chart === 'undefined') return;
4788  if (!Chart.registry.controllers.get('choropleth')) { console.warn('chartjs-chart-geo nenačten'); return; }
4789  try {
4790    if (!__euGeo) {
4791      const r = await fetch('https://cdn.jsdelivr.net/gh/leakyMirror/map-of-europe@27a335110674ae5b01a84d3501b227e661beea2b/GeoJSON/europe.geojson');
4792      __euGeo = await r.json();
4793    }
4794    const latest = window.euStorageLatest || {};
4795    const feats = __euGeo.features.filter(f => !['AZ','AM','GE','CY','IS','TR','RU','IL'].includes(f.properties.ISO2));
4796    const muted = getCssVar('--text-muted') || '#71717a', strong = getCssVar('--text-strong') || '#09090b',
4797          bgCard = getCssVar('--bg-card') || '#fff', border = getCssVar('--border') || '#e4e4e7';
4798    const ramp = t => (t < 0.5 ? interpolateColor('#dc2626', '#f59e0b', t * 2) : interpolateColor('#f59e0b', '#16a34a', (t - 0.5) * 2));
4799    if (euMapChart) euMapChart.destroy();
4800    euMapChart = new Chart(cv.getContext('2d'), {
4801      type: 'choropleth',
4802      data: {
4803        labels: feats.map(f => f.properties.NAME),
4804        datasets: [{
4805          outline: feats, showOutline: true,
4806          data: feats.map(f => ({ feature: f, value: latest[f.properties.ISO2] ? latest[f.properties.ISO2].full : null })),
4807          borderColor: border, borderWidth: 0.4
4808        }]
4809      },
4810      options: {
4811        responsive: true, maintainAspectRatio: false,
4812        plugins: {
4813          legend: { display: false },
4814          tooltip: {
4815            backgroundColor: strong, titleColor: bgCard, bodyColor: bgCard, borderColor: border, padding: 10, cornerRadius: 8,
4816            callbacks: {
4817              title: () => '',
4818              label: ctx => {
4819                const f = ctx.raw.feature, iso = f.properties.ISO2, o = latest[iso], v = ctx.raw.value;
4820                return v == null ? f.properties.NAME + ': bez dat'
4821                  : f.properties.NAME + ': ' + v.toFixed(1).replace('.', ',') + ' %' + (o && o.twh != null ? ' (' + o.twh.toFixed(1) + ' TWh)' : '');
4822              }
4823            }
4824          }
4825        },
4826        scales: {
4827          projection: { axis: 'x', projection: 'mercator' },
4828          color: { axis: 'x', min: 0, max: 100, interpolate: ramp,
4829            legend: { position: 'bottom-right', align: 'bottom', length: 120, width: 8 },
4830            ticks: { color: muted } }
4831        }
4832      }
4833    });
4834  } catch (e) { console.error('eu map failed', e); }
4835}
4836
4837function drawEuStorage() { drawEuStorageMap(); drawEuStorageTrend(); }
4838
4839// ===== Sekce 21: Skladba dovozu plynu do EU (ENTSOG + ALSI) =====
4840const IMPORT_SERIES = [
4841  { key: 'Norway', label: 'Norsko', color: '#0ea5e9' },
4842  { key: 'LNG', label: 'LNG', color: '#7c3aed' },
4843  { key: 'Russia', label: 'Rusko', color: '#dc2626' },
4844  { key: 'Algeria', label: 'Alžírsko', color: '#f59e0b' },
4845  { key: 'Azerbaijan', label: 'Ázerbájdžán', color: '#16a34a' },
4846  { key: 'Libya', label: 'Libye', color: '#ca8a04' },
4847  { key: 'Turkey', label: 'Turecko', color: '#0891b2' },
4848  { key: 'Ukraine', label: 'Ukrajina', color: '#64748b' },
4849  { key: 'Serbia', label: 'Srbsko', color: '#db2777' }
4850];
4851const impState = { range: '6m', hidden: {} };
4852let importsChart = null;
4853function impRangeStart(range, lastMs) { if (range === 'all') return -Infinity; const d = new Date(lastMs); d.setMonth(d.getMonth() - ({ '3m': 3, '6m': 6, '12m': 12 }[range] || 6)); return d.getTime(); }
4854function drawImportsChart() {
4855  const cv = document.getElementById('imports-chart'); if (!cv || typeof Chart === 'undefined') return;
4856  const data = window.euImports || {};
4857  const allD = new Set(); IMPORT_SERIES.forEach(s =>
4857 (data[s.key] || []).forEach(p => allD.add(p.date)));
4858  let dates = Array.from(allD).sort();
4859  if (!dates.length) { if (importsChart) { importsChart.destroy(); importsChart = null; } return; }
4860  const lastMs = new Date(dates[dates.length - 1]).getTime();
4861  const startMs = impRangeStart(impState.range, lastMs);
4862  dates = dates.filter(d => new Date(d).getTime() >= startMs);
4863  const dset = new Set(dates);
4864  const datasets = IMPORT_SERIES.map(s => { const m = {}; (data[s.key] || []).forEach(p => { if (dset.has(p.date)) m[p.date] = p.gwh; });
4865    return { label: s.label, data: dates.map(d => m[d] == null ? 0 : m[d]), borderColor: s.color, backgroundColor: s.color + 'cc', borderWidth: 1, pointRadius: 0, fill: true, tension: 0.1, hidden: !!impState.hidden[s.key] }; });
4866  const muted = getCssVar('--text-muted') || '#71717a', strong = getCssVar('--text-strong') || '#09090b', border = getCssVar('--border') || '#e4e4e7', bgCard = getCssVar('--bg-card') || '#fff';
4867  if (importsChart) importsChart.destroy();
4868  importsChart = new Chart(cv.getContext('2d'), {
4869    type: 'line', data: { labels: dates, datasets },
4870    options: { responsive: true, maintainAspectRatio: false, interaction: { mode: 'index', intersect: false },
4871      plugins: { legend: { display: false },
4872        tooltip: { backgroundColor: strong, titleColor: bgCard, bodyColor: bgCard, borderColor: border, padding: 10, cornerRadius: 8,
4873          callbacks: { title: items => items.length ? new Date(items[0].label).toLocaleDateString('cs-CZ') : '', label: ctx => ctx.dataset.label + ': ' + Math.round(ctx.parsed.y) + ' GWh/d' } } },
4874      scales: { y: { stacked: true, beginAtZero: true, title: { display: true, text: 'GWh/d', color: muted, font: { size: 11 } }, grid: { color: border }, ticks: { color: muted, font: { size: 11 } } },
4875        x: { stacked: true, grid: { display: false }, ticks: { color: muted, maxTicksLimit: 8, autoSkip: true, font: { size: 11 }, callback: function (v) { return new Date(this.getLabelForValue(v)).toLocaleDateString('cs-CZ', { month: 'short', year: '2-digit' }); } } } } }
4876  });
4877  renderImpLegend();
4878}
4879function renderImpLegend() { const el = document.getElementById('imports-legend'); if (!el) return;
4880  el.innerHTML = IMPORT_SERIES.map(s => { const off = impState.hidden[s.key] ? 'opacity:.35;' : ''; return '<span style="cursor:pointer;display:inline-flex;align-items:center;gap:5px;margin-right:12px;font-size:12.5px;' + off + '" onclick="toggleImp(\'' + s.key + '\')"><span style="display:inline-block;width:11px;height:11px;border-radius:3px;background:' + s.color + '"></span>' + s.label + '</span>'; }).join(''); }
4881function toggleImp(k) { impState.hidden[k] = !impState.hidden[k]; drawImportsChart(); }
4882function setImpRange(btn, range) { impState.range = range; if (btn && btn.parentElement) btn.parentElement.querySelectorAll('[data-imprange]').forEach(b => b.classList.toggle('active', b === btn)); drawImportsChart(); }
4883
4884// Jediné tlačítko „Aktualizovat data" sjednocuje obě činnosti:
4885//  1) vynutí nový výpočet dat (Edge Function „trigger-pipeline" → GitHub Actions),
4886//  2) počká na dokončení běhu a pak přenačte report s čerstvými daty.
4887// Když vynucení není dostupné (token/funkce/síť), spadne na prostý reload z DB — tlačítko
4888// tedy VŽDY udělá něco užitečného. GitHub token drží server, ne klientské HTML.
4889// Trvalá cache stažených tabulek v IndexedDB (přežije navigaci mezi listy; TTL 30 min).
4890const __idbCache = (() => {
4891  let dbp = null;
4892  function open() {
4893    if (dbp) return dbp;
4894    dbp = new Promise((res, rej) => {
4895      let r; try { r = indexedDB.open('erpcache', 1); } catch (e) { return rej(e); }
4896      r.onupgradeneeded = () => { try { r.result.createObjectStore('t'); } catch (e) {} };
4897      r.onsuccess = () => res(r.result);
4898      r.onerror = () => rej(r.error);
4899    });
4900    return dbp;
4901  }
4902  return {
4903    async get(k) { try { const db = await open(); return await new Promise((res) => { const rq = db.transaction('t').objectStore('t').get(k); rq.onsuccess = () => res(rq.result || null); rq.onerror = () => res(null); }); } catch (e) { return null; } },
4904    set(k, v) { open().then(db => { try { db.transaction('t', 'readwrite').objectStore('t').put(v, k); } catch (e) {} }).catch(() => {}); },
4905    async clear() { try { const db = await open(); db.transaction('t', 'readwrite').objectStore('t').clear(); } catch (e) {} }
4906  };
4907})();
4908
4909async function refreshData() {
4910  try { await __idbCache.clear(); } catch (e) {}
4911  const btn = document.getElementById('refresh-btn');
4912  const banner = document.getElementById('status-banner');
4913  const fnUrl = window.__SUPABASE_URL + '/functions/v1/trigger-pipeline';
4914  // POZOR: funkce už nepřijímá pouhý anon klíč (kdokoli s URL by jinak mohl spouštět běhy pipeline).
4915  // Posíláme token přihlášeného uživatele; anon klíč zůstává jen jako apikey.
4916  const { data: { session } } = await supaClient().auth.getSession();
4917  if (!session) { alert('Pro aktualizaci dat je potřeba být přihlášen.'); return; }
4918  const authHdr = { 'Authorization': 'Bearer ' + session.access_token,
4919                    'apikey': window.__SUPABASE_ANON, 'Content-Type': 'application/json' };
4920  const setBtn = (t) => { if (btn) btn.innerHTML = '<span>↻</span><span>' + t + '</span>'; };
4921  const setBanner = (cls, html) => { if (banner) { banner.className = 'status-banner ' + cls; banner.innerHTML = html; } };
4922  const reload = () => {
4923    const url = new URL(window.location.href);
4924    url.searchParams.set('_t', Date.now());
4925    window.location.replace(url.toString());
4926  };
4927  if (btn) btn.disabled = true;
4928  setBtn('Aktualizuji…');
4929  setBanner('info', 'Spouštím aktualizaci dat…');
4930
4931  // 1) Zkus vynutit nový běh pipeline. Když to nejde, aspoň přenačti aktuální data z DB.
4932  let trig;
4933  try {
4934    const r = await fetch(fnUrl, { method: 'POST', headers: authHdr });
4935    if (!r.ok) { setBanner('info', 'Načítám aktuální data z databáze…'); return setTimeout(reload, 500); }
4936    trig = await r.json();
4937  } catch (e) {
4938    setBanner('info', 'Načítám aktuální data z databáze…');
4939    return setTimeout(reload, 500);
4940  }
4941  if (trig && trig.mode === 'recent') {
4942    setBanner('success', '✓ Data byla právě aktualizována, načítám…');
4943    return setTimeout(reload, 700);
4944  }
4945
4946  // 2) Běh běží — čekej na dokončení a pak přenačti report.
4947  const sinceMs = (trig && trig.watch_since_ms) || 0;
4948  const startedAt = Date.now();
4949  const MAX_MS = 5 * 60 * 1000;
4950  setBanner('info', 'Aktualizuji data — nový výpočet běží (obvykle ~1–2&nbsp;min). Stránka se pak sama obnoví.');
4951  while (Date.now() - startedAt < MAX_MS) {
4952    await new Promise((res) => setTimeout(res, 12000));
4953    setBtn('Aktualizuji… ' + Math.round((Date.now() - startedAt) / 1000) + ' s');
4954    let st = null;
4955    try {
4956      const sr = await fetch(fnUrl, { method: 'POST', headers: authHdr, body: JSON.stringify({ action: 'status' }) });
4957      if (sr.ok) st = await sr.json();
4958    } catch (_) { /* ignoruj přechodné chyby a zkus znovu */ }
4959    if (st && st.status === 'completed' && st.created_at && Date.parse(st.created_at) >= sinceMs) {
4960      setBanner('success', '✓ Hotovo, načítám čerstvá data…');
4961      return setTimeout(reload, 800);
4962    }
4963  }
4964  setBanner('info', 'Výpočet trvá déle než obvykle; načítám zatím poslední data…');
4965  reload();
4966}
4967</script>
4967
4968
4969</body>
4970</html>

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.