Jun 7, 2026 93.1 KB html of v5
1<!DOCTYPE html>
2<html lang="cs">
3<head>
4<meta charset="UTF-8">
5<base href="./">
6<meta name="viewport" content="width=device-width, initial-scale=1.0">
7<title>Web</title>
8<style>
9/* ===== RESET & BASE ===== */
10*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
11
12:root {
13 --font-heading: 'Playfair Display', 'Georgia', serif;
14 --font-body: 'Source Serif 4', 'Georgia', serif;
15 --font-mono: 'JetBrains Mono', 'Courier New', monospace;
16 --font-ui: 'DM Sans', 'Helvetica Neue', sans-serif;
17
18 /* Palette defaults — overridden by JS */
19 --color-primary: #1a3a5c;
20 --color-primary-light: #2a5a8c;
21 --color-primary-dark: #0f2035;
22 --color-accent: #c9873a;
23 --color-accent-light: #e4a85a;
24 --color-bg: #fafaf7;
25 --color-bg-alt: #f0ede6;
26 --color-surface: #ffffff;
27 --color-border: #d8d3c8;
28 --color-text: #1c1a16;
29 --color-text-muted: #6b6457;
30 --color-text-inverse: #fafaf7;
31 --color-critic-ins-bg: #d4edda;
32 --color-critic-ins-fg: #155724;
33 --color-critic-del-bg: #f8d7da;
34 --color-critic-del-fg: #721c24;
35 --color-critic-mark-bg: #fff3cd;
36 --color-critic-mark-fg: #856404;
37 --color-critic-comment-bg: #d1ecf1;
38 --color-critic-comment-fg: #0c5460;
39
40 --sidebar-width: 280px;
41 --header-height: auto;
42 --radius: 6px;
43 --shadow-sm: 0 1px 3px rgba(0,0,0,.08), 0 1px 2px rgba(0,0,0,.06);
44 --shadow-md: 0 4px 12px rgba(0,0,0,.10), 0 2px 6px rgba(0,0,0,.07);
45 --shadow-lg: 0 10px 30px rgba(0,0,0,.12), 0 4px 12px rgba(0,0,0,.08);
46 --transition: 200ms ease;
47}
48
49/* ===== PALETTES ===== */
50[data-palette="blue"] {
51 --color-primary: #1a3a5c; --color-primary-light: #2a5a8c; --color-primary-dark: #0f2035;
52 --color-accent: #c9873a; --color-accent-light: #e4a85a;
53 --color-bg: #fafaf7; --color-bg-alt: #f0ede6; --color-surface: #fff;
54 --color-border: #d8d3c8; --color-text: #1c1a16; --color-text-muted: #6b6457;
55}
56[data-palette="green"] {
57 --color-primary: #1a4a2e; --color-primary-light: #2d7a4f; --color-primary-dark: #0e2a1a;
58 --color-accent: #b5802a; --color-accent-light: #d4a045;
59 --color-bg: #f7faf5; --color-bg-alt: #e8f0e4; --color-surface: #fff;
60 --color-border: #c8d8c0; --color-text: #161c14; --color-text-muted: #5a6e54;
61}
62[data-palette="red"] {
63 --color-primary: #5c1a1a; --color-primary-light: #8c2a2a; --color-primary-dark: #350f0f;
64 --color-accent: #c9873a; --color-accent-light: #e4a85a;
65 --color-bg: #faf7f7; --color-bg-alt: #f0e6e6; --color-surface: #fff;
66 --color-border: #d8c8c8; --color-text: #1c1616; --color-text-muted: #6b5454;
67}
68[data-palette="purple"] {
69 --color-primary: #3a1a5c; --color-primary-light: #5a2a8c; --color-primary-dark: #200f35;
70 --color-accent: #b58a2a; --color-accent-light: #d4aa45;
71 --color-bg: #f9f7fa; --color-bg-alt: #ece6f0; --color-surface: #fff;
72 --color-border: #d3c8d8; --color-text: #1a161c; --color-text-muted: #635468;
73}
74[data-palette="teal"] {
75 --color-primary: #1a4a4a; --color-primary-light: #2a7a7a; --color-primary-dark: #0e2a2a;
76 --color-accent: #c9873a; --color-accent-light: #e4a85a;
77 --color-bg: #f5fafa; --color-bg-alt: #e0ecec; --color-surface: #fff;
78 --color-border: #c0d8d8; --color-text: #141c1c; --color-text-muted: #4e6b6b;
79}
80[data-palette="orange"] {
81 --color-primary: #5c2e1a; --color-primary-light: #8c4a2a; --color-primary-dark: #35190f;
82 --color-accent: #d4822a; --color-accent-light: #e8a048;
83 --color-bg: #faf8f5; --color-bg-alt: #f0e8de; --color-surface: #fff;
84 --color-border: #d8ccc0; --color-text: #1c1814; --color-text-muted: #6b5e50;
85}
86[data-palette="dark"] {
87 --color-primary: #2a2a2a; --color-primary-light: #3d3d3d; --color-primary-dark: #111;
88 --color-accent: #e8a840; --color-accent-light: #f0c060;
89 --color-bg: #1a1a1a; --color-bg-alt: #242424; --color-surface: #2d2d2d;
90 --color-border: #3d3d3d; --color-text: #e8e4dc; --color-text-muted: #9a9490;
91 --color-critic-ins-bg: #1e3a25; --color-critic-ins-fg: #6dd67f;
92 --color-critic-del-bg: #3a1e1e; --color-critic-del-fg: #d6706d;
93 --color-critic-mark-bg: #3a2e10; --color-critic-mark-fg: #e8cc6a;
94 --color-critic-comment-bg: #1a2e35; --color-critic-comment-fg: #6abcd6;
95}
96[data-palette="slate"] {
97 --color-primary: #2a3550; --color-primary-light: #3d4e70; --color-primary-dark: #161d2e;
98 --color-accent: #6699cc; --color-accent-light: #88b4e0;
99 --color-bg: #f5f7fa; --color-bg-alt: #e5e8f0; --color-surface: #fff;
100 --color-border: #ccd0da; --color-text: #1a1d26; --color-text-muted: #545e78;
101}
102[data-palette="rose"] {
103 --color-primary: #5c1a30; --color-primary-light: #8c2a4a; --color-primary-dark: #350f1a;
104 --color-accent: #c97a5a; --color-accent-light: #e49a78;
105 --color-bg: #faf7f8; --color-bg-alt: #f0e4e8; --color-surface: #fff;
106 --color-border: #d8c0c8; --color-text: #1c1416; --color-text-muted: #6b4e58;
107}
108[data-palette="brown"] {
109 --color-primary: #4a2e1a; --color-primary-light: #7a4e2a; --color-primary-dark: #2a1a0e;
110 --color-accent: #c9a03a; --color-accent-light: #e4c05a;
111 --color-bg: #faf8f5; --color-bg-alt: #f0e8dc; --color-surface: #fff;
112 --color-border: #d8ccc0; --color-text: #1c1814; --color-text-muted: #6b5e50;
113}
114[data-palette="indigo"] {
115 --color-primary: #1e2a6a; --color-primary-light: #2e3e9a; --color-primary-dark: #10163e;
116 --color-accent: #d4882a; --color-accent-light: #e8a845;
117 --color-bg: #f5f6fa; --color-bg-alt: #e4e6f4; --color-surface: #fff;
118 --color-border: #c8cce0; --color-text: #141620; --color-text-muted: #5058a0;
119}
120[data-palette="gold"] {
121 --color-primary: #3a2a0a; --color-primary-light: #6a4e18; --color-primary-dark: #1e1505;
122 --color-accent: #c4880a; --color-accent-light: #e0a820;
123 --color-bg: #fdfaf0; --color-bg-alt: #f4f0d8; --color-surface: #fff;
124 --color-border: #dcd4a0; --color-text: #1a1808; --color-text-muted: #7a6820;
125}
126[data-palette="cyan"] {
127 --color-primary: #0e3a4a; --color-primary-light: #1a5a70; --color-primary-dark: #07202a;
128 --color-accent: #c88a30; --color-accent-light: #e0aa50;
129 --color-bg: #f2fafc; --color-bg-alt: #daeef5; --color-surface: #fff;
130 --color-border: #b8dce8; --color-text: #0c1a20; --color-text-muted: #3a7090;
131}
132[data-palette="forest"] {
133 --color-primary: #1e3820; --color-primary-light: #2e5c32; --color-primary-dark: #101e12;
134 --color-accent: #8a7a20; --color-accent-light: #b0a030;
135 --color-bg: #f5f8f2; --color-bg-alt: #e2ece0; --color-surface: #fff;
136 --color-border: #c0d4bc; --color-text: #141a14; --color-text-muted: #4a6848;
137}
138[data-palette="dusk"] {
139 --color-primary: #4a3050; --color-primary-light: #6e4878; --color-primary-dark: #28182e;
140 --color-accent: #c08040; --color-accent-light: #daa060;
141 --color-bg: #f8f5fa; --color-bg-alt: #ede5f2; --color-surface: #fff;
142 --color-border: #d4c8dc; --color-text: #1c1620; --color-text-muted: #705880;
143}
144[data-palette="mint"] {
145 --color-primary: #1a4040; --color-primary-light: #2a6868; --color-primary-dark: #0e2222;
146 --color-accent: #7aaa4a; --color-accent-light: #9acc68;
147 --color-bg: #f2faf8; --color-bg-alt: #daf0ea; --color-surface: #fff;
148 --color-border: #b8e0d4; --color-text: #141a18; --color-text-muted: #3a6858;
149}
150[data-palette="night"] {
151 --color-primary: #0a0e2a; --color-primary-light: #1a2050; --color-primary-dark: #040614;
152 --color-accent: #5090e0; --color-accent-light: #70b0f8;
153 --color-bg: #0e1228; --color-bg-alt: #161c38; --color-surface: #1a2040;
154 --color-border: #2a3060; --color-text: #c8d0f0; --color-text-muted: #7080b0;
155 --color-critic-ins-bg: #0e2e20; --color-critic-ins-fg: #50d080;
156 --color-critic-del-bg: #2e0e10; --color-critic-del-fg: #e05060;
157 --color-critic-mark-bg: #2e2a08; --color-critic-mark-fg: #e0c840;
158 --color-critic-comment-bg: #082028; --color-critic-comment-fg: #50b8d8;
159}
160[data-palette="warm"] {
161 --color-primary: #4a2a10; --color-primary-light: #7a4a20; --color-primary-dark: #281508;
162 --color-accent: #d4500a; --color-accent-light: #f07030;
163 --color-bg: #fdf8f0; --color-bg-alt: #f5ece0; --color-surface: #fff;
164 --color-border: #e0d0bc; --color-text: #1e1610; --color-text-muted: #7a5840;
165}
166[data-palette="cool"] {
167 --color-primary: #1a2a4a; --color-primary-light: #2a3e68; --color-primary-dark: #0e1628;
168 --color-accent: #3888cc; --color-accent-light: #58a8ec;
169 --color-bg: #f4f7fc; --color-bg-alt: #e0e8f4; --color-surface: #fff;
170 --color-border: #c0cce0; --color-text: #14181e; --color-text-muted: #405068;
171}
172[data-palette="earth"] {
173 --color-primary: #3a2e20; --color-primary-light: #5e4a30; --color-primary-dark: #1e1810;
174 --color-accent: #8a6020; --color-accent-light: #b07e30;
175 --color-bg: #faf5ee; --color-bg-alt: #f0e8d8; --color-surface: #fff;
176 --color-border: #ddd0b8; --color-text: #1a1810; --color-text-muted: #7a6848;
177}
178
179/* ===== LAYOUT ===== */
180body {
181 font-family: var(--font-body);
182 font-size: 1rem;
183 line-height: 1.7;
184 color: var(--color-text);
185 background: var(--color-bg);
186 min-height: 100vh;
187 display: flex;
188 flex-direction: column;
189}
190
191.skip-link {
192 position: absolute;
193 top: -100%;
194 left: 0;
195 background: var(--color-accent);
196 color: var(--color-text-inverse);
197 padding: .5rem 1rem;
198 z-index: 1000;
199 font-family: var(--font-ui);
200 text-decoration: none;
201 font-weight: 600;
202 border-radius: 0 0 var(--radius) 0;
203}
204.skip-link:focus { top: 0; }
205
206/* ===== HEADER ===== */
207#site-header {
208 background: var(--color-primary);
209 color: var(--color-text-inverse);
210 padding: 2rem 2rem 0;
211 position: relative;
212 overflow: hidden;
213}
214#site-header::before {
215 content: '';
216 position: absolute;
217 top: -60px; right: -80px;
218 width: 320px; height: 320px;
219 border-radius: 50%;
220 background: rgba(255,255,255,.04);
221 pointer-events: none;
222}
223#site-header::after {
224 content: '';
225 position: absolute;
226 bottom: -40px; left: 10%;
227 width: 200px; height: 200px;
228 border-radius: 50%;
229 background: rgba(255,255,255,.03);
230 pointer-events: none;
231}
232
233.site-branding {
234 position: relative;
235 z-index: 1;
236 margin-bottom: 1.5rem;
237}
238#site-title {
239 font-family: var(--font-heading);
240 font-size: clamp(1.8rem, 4vw, 2.8rem);
241 font-weight: 700;
242 line-height: 1.15;
243 letter-spacing: -.02em;
244 color: var(--color-text-inverse);
245 margin: 0 0 .4rem;
246}
247#site-tagline {
248 font-family: var(--font-ui);
249 font-size: .95rem;
250 opacity: .75;
251 font-style: italic;
252 letter-spacing: .01em;
253}
254
255/* ===== NAVIGATION ===== */
256#site-nav {
257 position: relative;
258 z-index: 1;
259 display: flex;
260 align-items: center;
261 gap: .25rem;
262 flex-wrap: wrap;
263 border-top: 1px solid rgba(255,255,255,.1);
264 padding: .5rem 0 0;
265 margin: 0 -2rem;
266 padding-left: 2rem;
267 padding-right: 2rem;
268 overflow-x: auto;
269}
270#site-nav a {
271 display: inline-block;
272 font-family: var(--font-ui);
273 font-size: .875rem;
274 font-weight: 500;
275 color: rgba(255,255,255,.8);
276 text-decoration: none;
277 padding: .6rem .9rem;
278 border-radius: var(--radius) var(--radius) 0 0;
279 transition: background var(--transition), color var(--transition);
280 white-space: nowrap;
281 letter-spacing: .01em;
282}
283#site-nav a:hover { background: rgba(255,255,255,.1); color: #fff; }
284#site-nav a[aria-current="page"] {
285 background: var(--color-bg);
286 color: var(--color-primary);
287 font-weight: 700;
288}
289.nav-section-links {
290 display: flex;
291 flex-wrap: wrap;
292 gap: .25rem;
293 align-items: center;
294 flex: 1;
295}
296
297/* Dropdown group */
298.nav-group {
299 position: relative;
300}
301.nav-group-btn {
302 display: inline-flex;
303 align-items: center;
304 gap: .35rem;
305 font-family: var(--font-ui);
306 font-size: .875rem;
307 font-weight: 500;
308 color: rgba(255,255,255,.8);
309 background: transparent;
310 border: none;
311 padding: .6rem .9rem;
312 border-radius: var(--radius) var(--radius) 0 0;
313 cursor: pointer;
314 transition: background var(--transition), color var(--transition);
315 white-space: nowrap;
316 letter-spacing: .01em;
317}
318.nav-group-btn::after {
319 content: '▾';
320 font-size: .75em;
321 transition: transform var(--transition);
322 display: inline-block;
323}
324.nav-group-btn[aria-expanded="true"] { background: rgba(255,255,255,.12); color: #fff; }
325.nav-group-btn[aria-expanded="true"]::after { transform: rotate(180deg); }
326.nav-group-btn:hover { background: rgba(255,255,255,.1); color: #fff; }
327.nav-group-btn:focus-visible { outline: 3px solid var(--color-accent); outline-offset: 2px; }
328
329/* Submenu panel */
330.nav-submenu {
331 position: absolute;
332 top: 100%;
333 left: 0;
334 min-width: 200px;
335 background: var(--color-surface);
336 border: 1px solid var(--color-border);
337 border-top: 3px solid var(--color-accent);
338 border-radius: 0 var(--radius) var(--radius) var(--radius);
339 box-shadow: var(--shadow-lg);
340 list-style: none;
341 padding: .35rem 0;
342 z-index: 500;
343 margin: 0;
344}
345.nav-submenu li { margin: 0; }
346.nav-submenu a {
347 display: block;
348 font-family: var(--font-ui);
349 font-size: .875rem;
350 color: var(--color-text);
351 text-decoration: none;
352 padding: .55rem 1.1rem;
353 transition: background var(--transition), color var(--transition);
354 white-space: nowrap;
355}
356.nav-submenu a:hover { background: var(--color-bg-alt); color: var(--color-primary); }
357.nav-submenu a[aria-current="page"] {
358 background: var(--color-bg-alt);
359 color: var(--color-accent);
360 font-weight: 700;
361}
362.nav-submenu a:focus-visible { outline: 3px solid var(--color-accent); outline-offset: -2px; }
363
364/* Mobile: submenu not absolute */
365@media (max-width: 900px) {
366 .nav-group { width: 100%; }
367 .nav-group-btn { width: 100%; border-radius: var(--radius); justify-content: space-between; }
368 .nav-submenu {
369 position: static;
370 border-top: none;
371 border-left: 3px solid var(--color-accent);
372 border-radius: 0 0 var(--radius) var(--radius);
373 box-shadow: none;
374 background: rgba(255,255,255,.05);
375 margin: 0 0 .25rem .5rem;
376 }
377 .nav-submenu a { color: rgba(255,255,255,.85); }
378 .nav-submenu a:hover { background: rgba(255,255,255,.08); color: #fff; }
379 .nav-submenu a[aria-current="page"] { background: rgba(255,255,255,.12); color: var(--color-accent-light); }
380}
381
382.nav-menu-links {
383 margin-left: auto;
384 display: flex;
385 align-items: center;
386 gap: .25rem;
387 flex-shrink: 0;
388}
389.nav-menu-links a {
390 color: rgba(255,255,255,.65) !important;
391 font-size: .8rem !important;
392}
393
394/* ===== TOOLBAR ===== */
395#toolbar {
396 background: var(--color-bg-alt);
397 border-bottom: 1px solid var(--color-border);
398 padding: .5rem 1.5rem;
399 display: flex;
400 align-items: center;
401 gap: .75rem;
402 flex-wrap: wrap;
403}
404#search-box {
405 display: flex;
406 align-items: center;
407 gap: .5rem;
408 flex: 1;
409 max-width: 400px;
410}
411#search-input {
412 flex: 1;
413 font-family: var(--font-ui);
414 font-size: .875rem;
415 padding: .4rem .75rem;
416 border: 1px solid var(--color-border);
417 border-radius: var(--radius);
418 background: var(--color-surface);
419 color: var(--color-text);
420 outline: none;
421 transition: border-color var(--transition);
422}
423#search-input:focus { border-color: var(--color-primary-light); box-shadow: 0 0 0 3px rgba(42,90,140,.15); }
424#search-btn {
425 font-family: var(--font-ui);
426 font-size: .8rem;
427 padding: .4rem .8rem;
428 background: var(--color-primary);
429 color: var(--color-text-inverse);
430 border: none;
431 border-radius: var(--radius);
432 cursor: pointer;
433 transition: background var(--transition);
434 white-space: nowrap;
435}
436#search-btn:hover { background: var(--color-primary-light); }
437#search-clear {
438 font-family: var(--font-ui);
439 font-size: .8rem;
440 padding: .4rem .8rem;
441 background: transparent;
442 color: var(--color-text-muted);
443 border: 1px solid var(--color-border);
444 border-radius: var(--radius);
445 cursor: pointer;
446 transition: all var(--transition);
447}
448#search-clear:hover { background: var(--color-bg); color: var(--color-text); }
449.toolbar-spacer { flex: 1; }
450#menu-toggle {
451 display: none;
452 font-family: var(--font-ui);
453 font-size: .875rem;
454 padding: .4rem .75rem;
455 background: var(--color-primary);
456 color: var(--color-text-inverse);
457 border: none;
458 border-radius: var(--radius);
459 cursor: pointer;
460}
461
462/* ===== SEARCH RESULTS ===== */
463#search-results {
464 display: none;
465 padding: 1.5rem 2rem;
466 max-width: 900px;
467 margin: 0 auto;
468 width: 100%;
469}
470#search-results.active { display: block; }
471#search-results h2 {
472 font-family: var(--font-heading);
473 font-size: 1.4rem;
474 color: var(--color-primary);
475 margin-bottom: 1rem;
476 padding-bottom: .5rem;
477 border-bottom: 2px solid var(--color-border);
478}
479.search-result-item {
480 background: var(--color-surface);
481 border: 1px solid var(--color-border);
482 border-radius: var(--radius);
483 padding: 1rem 1.25rem;
484 margin-bottom: .75rem;
485 box-shadow: var(--shadow-sm);
486}
487.search-result-item h3 { font-family: var(--font-heading); font-size: 1.1rem; margin-bottom: .25rem; }
488.search-result-item h3 a { color: var(--color-primary); text-decoration: none; }
489.search-result-item h3 a:hover { text-decoration: underline; }
490.search-result-item .result-section { font-family: var(--font-ui); font-size: .8rem; color: var(--color-text-muted); margin-bottom: .5rem; }
491.search-result-item .result-excerpt { font-size: .9rem; line-height: 1.5; }
492.search-result-item .result-excerpt mark { background: var(--color-critic-mark-bg); color: var(--color-critic-mark-fg); padding: 0 .15em; border-radius: 2px; }
493.search-no-results { color: var(--color-text-muted); font-style: italic; font-family: var(--font-ui); }
494
495/* ===== MAIN AREA ===== */
496#main-wrapper {
497 display: flex;
498 flex: 1;
499 align-items: flex-start;
500 max-width: 1400px;
501 margin: 0 auto;
502 width: 100%;
503 padding: 2rem 1.5rem;
504 gap: 2rem;
505}
506
507/* ===== SIDEBAR ===== */
508#sidebar {
509 width: var(--sidebar-width);
510 flex-shrink: 0;
511 position: sticky;
512 top: 1.5rem;
513 max-height: calc(100vh - 3rem);
514 overflow-y: auto;
515}
516.sidebar-inner {
517 background: var(--color-surface);
518 border: 1px solid var(--color-border);
519 border-radius: var(--radius);
520 box-shadow: var(--shadow-sm);
521 padding: 1.25rem;
522}
523.sidebar-section { margin-bottom: 1.5rem; }
524.sidebar-section:last-child { margin-bottom: 0; }
525.sidebar-heading {
526 font-family: var(--font-ui);
527 font-size: .7rem;
528 font-weight: 700;
529 text-transform: uppercase;
530 letter-spacing: .1em;
531 color: var(--color-text-muted);
532 margin-bottom: .75rem;
533 padding-bottom: .4rem;
534 border-bottom: 1px solid var(--color-border);
535}
536
537/* Sidebar TOC — nested lists */
538#toc-list { list-style: none; }
539#toc-list li { position: relative; }
540#toc-list ul { list-style: none; padding-left: .9rem; border-left: 2px solid var(--color-border); margin: .1rem 0 .1rem .3rem; }
541#toc-list ul ul { border-left-color: var(--color-bg-alt); }
542#toc-list a {
543 color: var(--color-text-muted);
544 text-decoration: none;
545 font-family: var(--font-ui);
546 display: block;
547 padding: .2rem 0;
548 transition: color var(--transition);
549 line-height: 1.3;
550 font-size: .875rem;
551}
552#toc-list > li > a { font-size: .9rem; font-weight: 600; color: var(--color-text); }
553#toc-list a:hover { color: var(--color-primary); }
554#toc-list a.active { color: var(--color-accent); font-weight: 600; }
555
556/* Sidebar comments list */
557#comments-list { list-style: none; counter-reset: comment-counter; }
558#comments-list li {
559 counter-increment: comment-counter;
560 position: relative;
561 padding: .3rem .3rem .3rem 1.6rem;
562 margin-bottom: .4rem;
563 font-family: var(--font-ui);
564 font-size: .82rem;
565 line-height: 1.4;
566 border-left: 2px solid var(--color-critic-comment-fg);
567 background: var(--color-critic-comment-bg);
568 border-radius: 0 3px 3px 0;
569}
570#comments-list li::before {
571 content: counter(comment-counter);
572 position: absolute;
573 left: .35rem;
574 top: .3rem;
575 font-size: .7rem;
576 font-weight: 700;
577 color: var(--color-critic-comment-fg);
578 line-height: 1;
579}
580#comments-list a {
581 color: var(--color-critic-comment-fg);
582 text-decoration: none;
583 display: block;
584}
585#comments-list a:hover { text-decoration: underline; }
586
587/* Critic comment anchors in text */
588.critic-comment-anchor { scroll-margin-top: 1.5rem; }
589
590/* Sidebar external links */
591#ext-links-list { list-style: none; }
592#ext-links-list li { margin-bottom: .25rem; }
593#ext-links-list a {
594 font-family: var(--font-ui);
595 font-size: .82rem;
596 color: var(--color-primary-light);
597 text-decoration: none;
598 display: block;
599 white-space: nowrap;
600 overflow: hidden;
601 text-overflow: ellipsis;
602 padding: .15rem 0;
603 transition: color var(--transition);
604}
605#ext-links-list a:hover { color: var(--color-accent); text-decoration: underline; }
606#ext-links-list a::before { content: '↗ '; font-size: .75rem; opacity: .6; }
607
608/* Sidebar custom content */
609#sidebar-content { font-size: .9rem; }
610#sidebar-content h2, #sidebar-content h3 {
611 font-family: var(--font-heading);
612 font-size: 1rem;
613 color: var(--color-primary);
614 margin: .75rem 0 .35rem;
615}
616#sidebar-content p { margin-bottom: .5rem; color: var(--color-text-muted); }
617#sidebar-content a { color: var(--color-primary-light); }
618#sidebar-content ul, #sidebar-content ol { padding-left: 1.25rem; margin-bottom: .5rem; }
619#sidebar-content li { margin-bottom: .2rem; }
620
621/* ===== CONTENT AREA ===== */
622#main-content {
623 flex: 1;
624 min-width: 0;
625}
626#page-content {
627 background: var(--color-surface);
628 border: 1px solid var(--color-border);
629 border-radius: var(--radius);
630 box-shadow: var(--shadow-sm);
631 padding: 2.5rem 3rem;
632}
633
634/* ===== TYPOGRAPHY ===== */
635.page-title {
636 font-family: var(--font-heading);
637 font-size: clamp(1.8rem, 3.5vw, 2.6rem);
638 font-weight: 700;
639 letter-spacing: -.03em;
640 color: var(--color-primary);
641 margin-bottom: .5rem;
642 line-height: 1.15;
643 padding-bottom: .75rem;
644 border-bottom: 3px solid var(--color-accent);
645}
646#page-content h2 {
647 font-family: var(--font-heading);
648 font-size: 1.65rem;
649 font-weight: 700;
650 color: var(--color-primary);
651 margin: 2.5rem 0 .75rem;
652 padding-bottom: .35rem;
653 border-bottom: 1px solid var(--color-border);
654 letter-spacing: -.02em;
655}
656#page-content h3 {
657 font-family: var(--font-heading);
658 font-size: 1.25rem;
659 font-weight: 600;
660 color: var(--color-primary-light);
661 margin: 1.75rem 0 .5rem;
662 letter-spacing: -.01em;
663}
664#page-content h4 {
665 font-family: var(--font-ui);
666 font-size: 1rem;
667 font-weight: 700;
668 color: var(--color-text);
669 margin: 1.25rem 0 .4rem;
670 text-transform: uppercase;
671 letter-spacing: .04em;
672}
673#page-content p { margin-bottom: 1rem; }
674#page-content a { color: var(--color-primary-light); }
675#page-content a:hover { color: var(--color-accent); }
676#page-content ul, #page-content ol { padding-left: 1.75rem; margin-bottom: 1rem; }
677#page-content li { margin-bottom: .3rem; }
678#page-content ul li::marker { color: var(--color-accent); }
679#page-content ol li::marker { color: var(--color-accent); font-weight: 700; }
680#page-content blockquote {
681 margin: 1.5rem 0;
682 padding: 1rem 1.25rem 1rem 1.5rem;
683 border-left: 4px solid var(--color-accent);
684 background: var(--color-bg-alt);
685 border-radius: 0 var(--radius) var(--radius) 0;
686 font-style: italic;
687 color: var(--color-text-muted);
688}
689#page-content blockquote p { margin: 0; }
690#page-content code {
691 font-family: var(--font-mono);
692 font-size: .875em;
693 background: var(--color-bg-alt);
694 color: var(--color-primary);
695 padding: .1em .4em;
696 border-radius: 3px;
697 border: 1px solid var(--color-border);
698}
699#page-content pre {
700 background: var(--color-primary-dark);
701 color: #e8e4dc;
702 padding: 1.25rem 1.5rem;
703 border-radius: var(--radius);
704 overflow-x: auto;
705 margin-bottom: 1.25rem;
706 font-family: var(--font-mono);
707 font-size: .875rem;
708 line-height: 1.6;
709 box-shadow: var(--shadow-md);
710}
711#page-content pre code {
712 background: none;
713 border: none;
714 padding: 0;
715 color: inherit;
716 font-size: inherit;
717}
718#page-content hr {
719 border: none;
720 border-top: 2px solid var(--color-border);
721 margin: 2rem 0;
722}
723#page-content img {
724 max-width: 100%;
725 height: auto;
726 border-radius: var(--radius);
727 box-shadow: var(--shadow-md);
728 margin: .5rem 0;
729}
730#page-content strong { font-weight: 700; color: var(--color-primary); }
731#page-content em { font-style: italic; }
732
733/* ===== TABLES ===== */
734#page-content table {
735 width: 100%;
736 border-collapse: collapse;
737 margin: 1.5rem 0;
738 font-size: .93rem;
739 box-shadow: var(--shadow-sm);
740 border-radius: var(--radius);
741 overflow: hidden;
742}
743#page-content thead { background: var(--color-primary); color: var(--color-text-inverse); }
744#page-content thead th {
745 padding: .75rem 1rem;
746 text-align: left;
747 font-family: var(--font-ui);
748 font-size: .82rem;
749 font-weight: 700;
750 text-transform: uppercase;
751 letter-spacing: .06em;
752}
753#page-content tbody tr { border-bottom: 1px solid var(--color-border); transition: background var(--transition); }
754#page-content tbody tr:hover { background: var(--color-bg-alt); }
755#page-content tbody tr:last-child { border-bottom: none; }
756#page-content td { padding: .65rem 1rem; vertical-align: top; }
757
758/* ===== CRITIC MARKUP ===== */
759ins.critic {
760 background: var(--color-critic-ins-bg);
761 color: var(--color-critic-ins-fg);
762 text-decoration: none;
763 padding: .05em .25em;
764 border-radius: 3px;
765 border-bottom: 2px solid var(--color-critic-ins-fg);
766}
767del.critic {
768 background: var(--color-critic-del-bg);
769 color: var(--color-critic-del-fg);
770 text-decoration: line-through;
771 text-decoration-color: var(--color-critic-del-fg);
772 padding: .05em .25em;
773 border-radius: 3px;
774 opacity: .85;
775}
776mark.critic {
777 background: var(--color-critic-mark-bg);
778 color: var(--color-critic-mark-fg);
779 padding: .05em .25em;
780 border-radius: 3px;
781}
782.critic-comment {
783 display: inline-block;
784 position: relative;
785 cursor: help;
786}
787.critic-comment-marker {
788 background: var(--color-critic-comment-bg);
789 color: var(--color-critic-comment-fg);
790 font-size: .78em;
791 padding: .1em .4em;
792 border-radius: 3px;
793 font-family: var(--font-ui);
794 font-weight: 700;
795 vertical-align: middle;
796 border: 1px solid var(--color-critic-comment-fg);
797}
798.critic-comment-text {
799 display: none;
800 position: absolute;
801 bottom: 130%;
802 left: 50%;
803 transform: translateX(-50%);
804 background: var(--color-primary-dark);
805 color: var(--color-text-inverse);
806 padding: .5rem .75rem;
807 border-radius: var(--radius);
808 font-size: .82rem;
809 font-family: var(--font-ui);
810 white-space: nowrap;
811 max-width: 250px;
812 white-space: normal;
813 box-shadow: var(--shadow-lg);
814 z-index: 100;
815 line-height: 1.4;
816}
817.critic-comment-text::after {
818 content: '';
819 position: absolute;
820 top: 100%; left: 50%;
821 transform: translateX(-50%);
822 border: 6px solid transparent;
823 border-top-color: var(--color-primary-dark);
824}
825.critic-comment:hover .critic-comment-text,
826.critic-comment:focus-within .critic-comment-text { display: block; }
827.critic-comment-marker:focus { outline: 2px solid var(--color-accent); outline-offset: 2px; }
828
829/* ===== ARTICLES ===== */
830.articles-header {
831 display: flex;
832 align-items: center;
833 justify-content: space-between;
834 flex-wrap: wrap;
835 gap: .75rem;
836 margin-bottom: 1.5rem;
837}
838.articles-count {
839 font-family: var(--font-ui);
840 font-size: .85rem;
841 color: var(--color-text-muted);
842}
843.articles-active-filter {
844 display: inline-flex;
845 align-items: center;
846 gap: .4rem;
847 font-family: var(--font-ui);
848 font-size: .82rem;
849 background: var(--color-primary);
850 color: var(--color-text-inverse);
851 padding: .2rem .6rem;
852 border-radius: 999px;
853}
854.articles-active-filter button {
855 background: none;
856 border: none;
857 color: inherit;
858 cursor: pointer;
859 padding: 0;
860 font-size: 1em;
861 line-height: 1;
862 opacity: .7;
863}
864.articles-active-filter button:hover { opacity: 1; }
865
866.articles-list { list-style: none; padding: 0; margin: 0; }
867.article-card {
868 background: var(--color-surface);
869 border: 1px solid var(--color-border);
870 border-radius: var(--radius);
871 padding: 1.25rem 1.5rem;
872 margin-bottom: 1rem;
873 box-shadow: var(--shadow-sm);
874 transition: box-shadow var(--transition), border-color var(--transition);
875}
876.article-card:hover { box-shadow: var(--shadow-md); border-color: var(--color-primary-light); }
877.article-card-title {
878 font-family: var(--font-heading);
879 font-size: 1.2rem;
880 font-weight: 700;
881 margin-bottom: .35rem;
882 line-height: 1.3;
883}
884.article-card-title a {
885 color: var(--color-primary);
886 text-decoration: none;
887}
888.article-card-title a:hover { color: var(--color-accent); text-decoration: underline; }
889.article-card-meta {
890 display: flex;
891 align-items: center;
892 flex-wrap: wrap;
893 gap: .5rem;
894 margin-bottom: .6rem;
895 font-family: var(--font-ui);
896 font-size: .82rem;
897 color: var(--color-text-muted);
898}
899.article-card-date { display: flex; align-items: center; gap: .25rem; }
900.article-card-perex { font-size: .93rem; color: var(--color-text-muted); line-height: 1.55; }
901.article-card.hidden { display: none; }
902
903/* Tag badges */
904.tag-badge {
905 display: inline-block;
906 font-family: var(--font-ui);
907 font-size: .75rem;
908 font-weight: 600;
909 padding: .15rem .55rem;
910 border-radius: 999px;
911 background: var(--color-bg-alt);
912 color: var(--color-primary);
913 border: 1px solid var(--color-border);
914 cursor: pointer;
915 transition: background var(--transition), color var(--transition), border-color var(--transition);
916 text-decoration: none;
917 white-space: nowrap;
918}
919.tag-badge:hover, .tag-badge.active {
920 background: var(--color-primary);
921 color: var(--color-text-inverse);
922 border-color: var(--color-primary);
923}
924.tag-badge:focus-visible { outline: 2px solid var(--color-accent); outline-offset: 2px; }
925
926/* Article detail view */
927.article-back {
928 display: inline-flex;
929 align-items: center;
930 gap: .35rem;
931 font-family: var(--font-ui);
932 font-size: .875rem;
933 color: var(--color-primary-light);
934 text-decoration: none;
935 margin-bottom: 1.5rem;
936 padding: .3rem .6rem;
937 border: 1px solid var(--color-border);
938 border-radius: var(--radius);
939 transition: background var(--transition), color var(--transition);
940}
941.article-back:hover { background: var(--color-bg-alt); color: var(--color-primary); }
942.article-detail-meta {
943 display: flex;
944 align-items: center;
945 flex-wrap: wrap;
946 gap: .6rem;
947 margin-bottom: 1.5rem;
948 padding-bottom: 1rem;
949 border-bottom: 1px solid var(--color-border);
950 font-family: var(--font-ui);
951 font-size: .875rem;
952 color: var(--color-text-muted);
953}
954.article-perex {
955 font-size: 1.05rem;
956 font-weight: 600;
957 color: var(--color-text);
958 line-height: 1.6;
959 margin-bottom: 1.5rem;
960 padding-bottom: 1.5rem;
961 border-bottom: 1px solid var(--color-border);
962}
963
964/* Sidebar tags panel */
965#tags-nav .tags-cloud { display: flex; flex-wrap: wrap; gap: .35rem; }
966
967/* ===== NEWS ITEMS ===== */
968.news-list { list-style: none; padding: 0; margin: 0; }
969.news-item {
970 margin-bottom: 2rem;
971 padding-bottom: 2rem;
972 border-bottom: 1px solid var(--color-border);
973}
974.news-item:last-child { border-bottom: none; margin-bottom: 0; padding-bottom: 0; }
975.news-date-header {
976 display: flex;
977 align-items: baseline;
978 gap: .6rem;
979 margin-bottom: .6rem;
980 flex-wrap: wrap;
981}
982.news-date-icon { font-size: 1rem; line-height: 1; flex-shrink: 0; }
983.news-dow {
984 font-family: var(--font-ui);
985 font-size: .82rem;
986 font-weight: 700;
987 text-transform: uppercase;
988 letter-spacing: .07em;
989 color: var(--color-accent);
990}
991.news-date {
992 font-family: var(--font-ui);
993 font-size: .9rem;
994 font-weight: 600;
995 color: var(--color-text-muted);
996}
997.news-title {
998 font-family: var(--font-heading);
999 font-size: 1.2rem;
1000 font-weight: 700;
1001 color: var(--color-primary);
1002 margin-bottom: .4rem;
1003 line-height: 1.3;
1004}
1005.news-body { font-size: .97rem; }
1006.news-body p:last-child { margin-bottom: 0; }
1007.news-body p { margin-bottom: .6rem; }
1008
1009/* News calendar widget */
1010.news-cal {
1011 font-family: var(--font-ui);
1012 font-size: .82rem;
1013 user-select: none;
1014}
1015.news-cal-nav {
1016 display: flex;
1017 align-items: center;
1018 justify-content: space-between;
1019 margin-bottom: .5rem;
1020}
1021.news-cal-nav button {
1022 background: none;
1023 border: 1px solid var(--color-border);
1024 border-radius: 3px;
1025 padding: .15rem .45rem;
1026 cursor: pointer;
1027 color: var(--color-text-muted);
1028 font-size: .8rem;
1029 line-height: 1.4;
1030 transition: background var(--transition), color var(--transition);
1031}
1032.news-cal-nav button:hover { background: var(--color-bg-alt); color: var(--color-text); }
1033.news-cal-nav button:focus-visible { outline: 2px solid var(--color-accent); outline-offset: 2px; }
1034.news-cal-month {
1035 font-weight: 700;
1036 font-size: .85rem;
1037 color: var(--color-primary);
1038 text-align: center;
1039 flex: 1;
1040}
1041.news-cal-grid {
1042 display: grid;
1043 grid-template-columns: repeat(7, 1fr);
1044 gap: 2px;
1045 text-align: center;
1046}
1047.news-cal-dow {
1048 color: var(--color-text-muted);
1049 font-size: .7rem;
1050 font-weight: 700;
1051 padding: .1rem 0 .3rem;
1052 text-transform: uppercase;
1053}
1054.news-cal-day {
1055 padding: .2rem .1rem;
1056 border-radius: 3px;
1057 line-height: 1.4;
1058 color: var(--color-text-muted);
1059}
1060.news-cal-day.empty { visibility: hidden; }
1061.news-cal-day.today {
1062 background: var(--color-bg-alt);
1063 color: var(--color-primary);
1064 font-weight: 700;
1065}
1066.news-cal-day.has-news {
1067 background: var(--color-primary);
1068 color: var(--color-text-inverse);
1069 font-weight: 700;
1070 cursor: pointer;
1071 border-radius: 3px;
1072 transition: background var(--transition);
1073}
1074.news-cal-day.has-news:hover { background: var(--color-accent); }
1075.news-cal-day.has-news:focus-visible { outline: 2px solid var(--color-accent); outline-offset: 2px; }
1076.news-cal-day.has-news.today { background: var(--color-accent); }
1077
1078/* ===== FOOTER ===== */
1079#site-footer {
1080 background: var(--color-primary-dark);
1081 color: rgba(255,255,255,.7);
1082 padding: 1.5rem 2rem;
1083 display: flex;
1084 align-items: center;
1085 flex-wrap: wrap;
1086 gap: 1rem;
1087 font-family: var(--font-ui);
1088 font-size: .85rem;
1089}
1090#site-footer a { color: rgba(255,255,255,.85); text-decoration: none; transition: color var(--transition); }
1091#site-footer a:hover { color: #fff; text-decoration: underline; }
1092.footer-copyright { flex: 1; }
1093.footer-links { display: flex; gap: 1rem; flex-wrap: wrap; }
1094.footer-links a {
1095 padding: .2rem .5rem;
1096 border-radius: 3px;
1097 transition: background var(--transition);
1098}
1099.footer-links a:hover { background: rgba(255,255,255,.1); }
1100
1101/* ===== LOADING / ERROR ===== */
1102#loading-screen {
1103 position: fixed; inset: 0;
1104 background: var(--color-bg);
1105 display: flex;
1106 align-items: center;
1107 justify-content: center;
1108 z-index: 9999;
1109 flex-direction: column;
1110 gap: 1rem;
1111}
1112.loading-spinner {
1113 width: 40px; height: 40px;
1114 border: 3px solid var(--color-border);
1115 border-top-color: var(--color-primary);
1116 border-radius: 50%;
1117 animation: spin .8s linear infinite;
1118}
1119@keyframes spin { to { transform: rotate(360deg); } }
1120.loading-text {
1121 font-family: var(--font-ui);
1122 color: var(--color-text-muted);
1123 font-size: .9rem;
1124}
1125#error-screen {
1126 display: none;
1127 position: fixed; inset: 0;
1128 background: var(--color-bg);
1129 align-items: center;
1130 justify-content: center;
1131 z-index: 9999;
1132 flex-direction: column;
1133 gap: 1rem;
1134 padding: 2rem;
1135 text-align: center;
1136}
1137#error-screen.active { display: flex; }
1138.error-icon { font-size: 3rem; }
1139.error-title { font-family: var(--font-heading); font-size: 1.5rem; color: var(--color-primary); }
1140.error-msg { font-family: var(--font-ui); color: var(--color-text-muted); max-width: 500px; line-height: 1.6; }
1141.error-msg code { font-family: var(--font-mono); font-size: .9em; background: var(--color-bg-alt); padding: .1em .4em; border-radius: 3px; }
1142
1143/* ===== SIDEBAR MOBILE TOGGLE ===== */
1144/* Overlay a toggle tlačítko se na mobilu nepoužívají — sidebar je pod obsahem */
1145#sidebar-overlay { display: none; }
1146
1147/* ===== RESPONSIVE ===== */
1148@media (max-width: 900px) {
1149 #main-wrapper { flex-direction: column; padding: 1rem; gap: 1rem; }
1150 #menu-toggle { display: none; }
1151
1152 /* Sidebar: statický, plná šířka, pod obsahem */
1153 #sidebar {
1154 position: static;
1155 width: 100%;
1156 max-height: none;
1157 overflow-y: visible;
1158 }
1159 /* Na mobilu zobrazit sekce sidebaru vedle sebe (grid), nebo pod sebou */
1160 .sidebar-inner {
1161 display: grid;
1162 grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
1163 gap: .75rem;
1164 }
1165 /* Každá sidebar sekce jako samostatná karta */
1166 .sidebar-section {
1167 margin-bottom: 0;
1168 }
1169
1170 #page-content { padding: 1.5rem 1.25rem; }
1171 #site-header { padding: 1.25rem 1.25rem 0; }
1172 #site-nav { margin: 0 -1.25rem; padding-left: 1.25rem; padding-right: 1.25rem; }
1173}
1174@media (max-width: 560px) {
1175 #site-title { font-size: 1.5rem; }
1176 #page-content { padding: 1rem .875rem; }
1177 #main-wrapper { padding: .75rem; }
1178 #search-box { max-width: none; }
1179 /* Na malých displejích grid padne na jeden sloupec */
1180 .sidebar-inner { grid-template-columns: 1fr; }
1181}
1182
1183/* ===== PRINT ===== */
1184@media print {
1185 #site-nav, #toolbar, #sidebar, #site-footer { display: none; }
1186 #main-wrapper { padding: 0; }
1187 #page-content { box-shadow: none; border: none; }
1188}
1189
1190/* ===== FOCUS VISIBLE ===== */
1191:focus-visible {
1192 outline: 3px solid var(--color-accent);
1193 outline-offset: 2px;
1194 border-radius: 2px;
1195}
1196</style>
1197<link rel="preconnect" href="https://fonts.googleapis.com">
1198<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
1199<link href="https://fonts.googleapis.com/css2?family=DM+Sans:wght@400;500;700&family=Playfair+Display:ital,wght@0,700;1,400&family=Source+Serif+4:ital,wght@0,400;0,600;1,400&family=JetBrains+Mono&display=swap" rel="stylesheet">
1200</head>
1201<body>
1202
1203<a href="#main-content" class="skip-link" id="skip-link">Přeskočit na obsah</a>
1204
1205<div id="loading-screen" aria-live="polite" aria-busy="true">
1206 <div class="loading-spinner" role="presentation"></div>
1207 <p class="loading-text" id="loading-text">Načítám…</p>
1208</div>
1209
1210<div id="error-screen" role="alert" aria-live="assertive">
1211 <div class="error-icon" aria-hidden="true">⚠️</div>
1212 <h1 class="error-title" id="error-title">Chyba načítání</h1>
1213 <p class="error-msg" id="error-msg"></p>
1214</div>
1215
1216<header id="site-header" role="banner">
1217 <div class="site-branding">
1218 <h2 id="site-title"></h2>
1219 <p id="site-tagline"></p>
1220 </div>
1221 <nav id="site-nav" role="navigation" aria-label="Hlavní navigace">
1222 <!-- filled by JS -->
1223 </nav>
1224</header>
1225
1226<div id="toolbar" role="search">
1227 <button id="menu-toggle" aria-expanded="false" aria-controls="sidebar" aria-label="Otevřít postranní panel">☰ Menu</button>
1228 <div id="search-box">
1229 <label for="search-input" class="sr-only" id="search-label">Hledat</label>
1230 <input type="search" id="search-input" autocomplete="off" aria-label="Hledat na webu">
1231 <button id="search-btn" type="button">🔍 <span id="search-btn-label">Hledat</span></button>
1232 <button id="search-clear" type="button" hidden id="search-clear-btn"><span id="search-clear-label">Zrušit</span></button>
1233 </div>
1234</div>
1235
1236<div id="sidebar-overlay" aria-hidden="true"></div>
1237
1238<div id="main-wrapper">
1239 <aside id="sidebar" role="complementary" aria-label="Postranní panel">
1240 <div class="sidebar-inner">
1241 <div id="sidebar-custom" class="sidebar-section" hidden>
1242 <div id="sidebar-content"></div>
1243 </div>
1244 <nav class="sidebar-section" aria-label="Obsah stránky" id="toc-nav">
1245 <h2 class="sidebar-heading" id="toc-heading">Obsah</h2>
1246 <ul id="toc-list"></ul>
1247 </nav>
1248 <nav class="sidebar-section" id="ext-links-nav" hidden aria-label="Externí odkazy na stránce">
1249 <h2 class="sidebar-heading" id="ext-links-heading">Odkazy</h2>
1250 <ul id="ext-links-list"></ul>
1251 </nav>
1252 <nav class="sidebar-section" id="comments-nav" hidden aria-label="Komentáře v textu">
1253 <h2 class="sidebar-heading" id="comments-heading">Komentáře</h2>
1254 <ol id="comments-list"></ol>
1255 </nav>
1256 <div class="sidebar-section" id="news-calendar-nav" hidden>
1257 <h2 class="sidebar-heading" id="news-calendar-heading">Kalendář</h2>
1258 <div id="news-calendar"></div>
1259 </div>
1260 <nav class="sidebar-section" id="tags-nav" hidden aria-label="Tagy článků">
1261 <h2 class="sidebar-heading" id="tags-heading">Tagy</h2>
1262 <div class="tags-cloud" id="tags-cloud"></div>
1263 </nav>
1264 </div>
1265 </aside>
1266
1267 <main id="main-content" tabindex="-1">
1268 <div id="search-results" role="region" aria-live="polite" aria-label="Výsledky hledání">
1269 <h2 id="search-results-title"></h2>
1270 <div id="search-results-list"></div>
1271 </div>
1272 <article id="page-content" aria-live="polite">
1273 <!-- filled by JS -->
1274 </article>
1275 </main>
1276</div>
1277
1278<footer id="site-footer" role="contentinfo">
1279 <p class="footer-copyright" id="footer-copyright"></p>
1280 <nav class="footer-links" id="footer-links" aria-label="Patičkové odkazy"></nav>
1281</footer>
1282
1283<script>
1284// =============================================
1285// WEBSITE ENGINE v1.0
1286// Single-file static site renderer
1287// =============================================
1288
1289const Engine = (() => {
1290
1291 // --- State ---
1292 let config = {};
1293 let lang = {};
1294 let sections = []; // [{id, title, slug, content, headings}]
1295 let sidebarMD = '';
1296 let searchIndex = [];
1297 let currentSection = null;
1298
1299 // --- Fetch helpers ---
1300 async function fetchText(url) {
1301 const r = await fetch(url);
1302 if (!r.ok) throw new Error(`HTTP ${r.status}: ${url}`);
1303 const text = await r.text();
1304 return text.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
1305 }
1306 async function fetchJSON(url) {
1307 const r = await fetch(url);
1308 if (!r.ok) throw new Error(`HTTP ${r.status}: ${url}`);
1309 return r.json();
1310 }
1311
1312 // --- Slug ---
1313 function slugify(text) {
1314 return text.toLowerCase()
1315 .normalize('NFD').replace(/[\u0300-\u036f]/g, '')
1316 .replace(/[^a-z0-9]+/g, '-')
1317 .replace(/^-|-$/g, '');
1318 }
1319
1320 // =============================================
1321 // MARKDOWN PARSER
1322 // =============================================
1323 function parseMD(md, basePath) {
1324 let html = md;
1325
1326 // Escape HTML entities in code blocks first
1327 const codeBlocks = [];
1328 html = html.replace(/```([\s\S]*?)```/g, (_, code) => {
1329 const idx = codeBlocks.push(`<pre><code>${escHtml(code.replace(/^\n/, ''))}</code></pre>`) - 1;
1330 return `\x00CODE${idx}\x00`;
1331 });
1332 const inlineCodes = [];
1333 html = html.replace(/`([^`]+)`/g, (_, code) => {
1334 const idx = inlineCodes.push(`<code>${escHtml(code)}</code>`) - 1;
1335 return `\x00ICODE${idx}\x00`;
1336 });
1337
1338 // CriticMarkup — order matters
1339 // {++ insertion ++}
1340 html = html.replace(/\{\+\+([\s\S]*?)\+\+\}/g, (_, t) =>
1341 `<ins class="critic" aria-label="${lang.critic_ins||'Vloženo'}: ${escAttr(t)}">${t}</ins>`);
1342 // {-- deletion --}
1343 html = html.replace(/\{--([\s\S]*?)--\}/g, (_, t) =>
1344 `<del class="critic" aria-label="${lang.critic_del||'Smazáno'}: ${escAttr(t)}">${t}</del>`);
1345 // {~~ old ~> new ~~}
1346 html = html.replace(/\{~~([\s\S]*?)~>([\s\S]*?)~~\}/g, (_, o, n) =>
1347 `<del class="critic" aria-label="${lang.critic_del||'Smazáno'}: ${escAttr(o)}">${o}</del><ins class="critic" aria-label="${lang.critic_ins||'Vloženo'}: ${escAttr(n)}">${n}</ins>`);
1348 // {== highlight ==}
1349 html = html.replace(/\{==([\s\S]*?)==\}/g, (_, t) =>
1350 `<mark class="critic" aria-label="${lang.critic_mark||'Zvýrazněno'}: ${escAttr(t)}">${t}</mark>`);
1351 // {>> comment <<}
1352 html = html.replace(/\{>>([\s\S]*?)<<\}/g, (_, t) =>
1353 `<span class="critic-comment"><button class="critic-comment-marker" tabindex="0" aria-label="${lang.critic_comment||'Komentář'}: ${escAttr(t)}">💬</button><span class="critic-comment-text" role="tooltip">${escHtml(t)}</span></span>`);
1354
1355 // Tables
1356 html = html.replace(/^(\|.+\|\n)([ \t]*\|[ \t]*[-:]+[ \t]*\|.+\n)((?:\|.+\|\n?)*)/gm,
1357 (_, head, sep, body) => {
1358 const alignments = sep.split('|').filter(c => c.trim())
1359 .map(c => { c = c.trim(); return c.endsWith(':') && c.startsWith(':') ? 'center' : c.endsWith(':') ? 'right' : 'left'; });
1360 const ths = head.split('|').filter(c => c.trim() !== '').map((c, i) =>
1361 `<th style="text-align:${alignments[i]||'left'}">${c.trim()}</th>`).join('');
1362 const rows = body.trim().split('\n').filter(Boolean).map(row => {
1363 const tds = row.split('|').filter(c => c.trim() !== '').map((c, i) =>
1364 `<td style="text-align:${alignments[i]||'left'}">${parseMDInline(c.trim())}</td>`).join('');
1365 return `<tr>${tds}</tr>`;
1366 }).join('');
1367 return `<table><thead><tr>${ths}</tr></thead><tbody>${rows}</tbody></table>`;
1368 });
1369
1370 // Headings (h1–h6)
1371 html = html.replace(/^(#{1,6})\s+(.+)$/gm, (_, hashes, text) => {
1372 const level = hashes.length;
1373 const id = slugify(text);
1374 return `<h${level} id="${id}">${parseMDInline(text)}</h${level}>`;
1375 });
1376
1377 // Horizontal rules
1378 html = html.replace(/^[-*_]{3,}\s*$/gm, '<hr>');
1379
1380 // Blockquotes
1381 html = html.replace(/^(>+)\s?(.*)$/gm, (_, marks, text) => `<blockquote><p>${parseMDInline(text)}</p></blockquote>`);
1382
1383 // Unordered lists
1384 html = html.replace(/(^[-*+] .+\n?)+/gm, match => {
1385 const items = match.trim().split('\n').map(l => `<li>${parseMDInline(l.replace(/^[-*+] /, '').trim())}</li>`).join('');
1386 return `<ul>${items}</ul>`;
1387 });
1388 // Ordered lists
1389 html = html.replace(/(^\d+\. .+\n?)+/gm, match => {
1390 const items = match.trim().split('\n').map(l => `<li>${parseMDInline(l.replace(/^\d+\. /, '').trim())}</li>`).join('');
1391 return `<ol>${items}</ol>`;
1392 });
1393
1394 // Paragraphs (lines not starting with block elements)
1395 const blockTags = ['<h','<ul','<ol','<li','<blockquote','<table','<thead','<tbody','<tr','<td','<th','<pre','<hr','<ins','<del','<mark'];
1396 const lines = html.split('\n');
1397 const result = [];
1398 let pBuf = [];
1399 const flushP = () => {
1400 if (pBuf.length) { result.push(`<p>${pBuf.join(' ')}</p>`); pBuf = []; }
1401 };
1402 for (const line of lines) {
1403 const trimmed = line.trim();
1404 if (!trimmed || trimmed.startsWith('\x00CODE') || blockTags.some(t => trimmed.startsWith(t)) || trimmed.endsWith('>')) {
1405 flushP();
1406 if (trimmed) result.push(trimmed);
1407 } else {
1408 pBuf.push(parseMDInline(trimmed));
1409 }
1410 }
1411 flushP();
1412 html = result.join('\n');
1413
1414 // Restore code
1415 html = html.replace(/\x00CODE(\d+)\x00/g, (_, i) => codeBlocks[i]);
1416 html = html.replace(/\x00ICODE(\d+)\x00/g, (_, i) => inlineCodes[i]);
1417
1418 return html;
1419 }
1420
1421 function parseMDInline(text) {
1422 // Images before links
1423 text = text.replace(/!\[([^\]]*)\]\(([^)]+)\)/g, (_, alt, src) =>
1424 `<img src="${escAttr(src)}" alt="${escAttr(alt)}" loading="lazy">`);
1425 // Internal links [text] (no URL)
1426 text = text.replace(/\[([^\]]+)\](?!\()/g, (_, title) => {
1427 const slug = slugify(title);
1428 return `<a href="#${slug}" class="internal-link" data-target="${escAttr(title)}">${title}</a>`;
1429 });
1430 // External links [text](url)
1431 text = text.replace(/\[([^\]]+)\]\(([^)]+)\)/g, (_, t, url) =>
1432 `<a href="${escAttr(url)}"${url.startsWith('http') ? ' target="_blank" rel="noopener noreferrer"' : ''}>${t}</a>`);
1433 // Bold+italic
1434 text = text.replace(/\*\*\*([^*]+)\*\*\*/g, '<strong><em>$1</em></strong>');
1435 // Bold
1436 text = text.replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>');
1437 text = text.replace(/__([^_]+)__/g, '<strong>$1</strong>');
1438 // Italic
1439 text = text.replace(/\*([^*]+)\*/g, '<em>$1</em>');
1440 text = text.replace(/_([^_]+)_/g, '<em>$1</em>');
1441 // Auto-linkify bare URLs not already inside a tag or attribute
1442 text = text.replace(/(?<![="'>])(https?:\/\/[^\s<>"')\]]+)/g, url =>
1443 `<a href="${escAttr(url)}" target="_blank" rel="noopener noreferrer">${escHtml(url)}</a>`);
1444 // Strikethrough
1445 text = text.replace(/~~([^~]+)~~/g, '<del>$1</del>');
1446 return text;
1447 }
1448
1449 function escHtml(s) {
1450 return s.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');
1451 }
1452 function escAttr(s) {
1453 return s.replace(/"/g,'&quot;').replace(/'/g,'&#39;');
1454 }
1455
1456 // =============================================
1457 // SECTION SPLITTING
1458 // =============================================
1459 // A section whose content begins with a line containing only "multisection"
1460 // (case-insensitive) is a multisection group: its H2s become sub-pages and
1461 // it acts as a nav group rather than a navigable page itself.
1462 function splitSections(md) {
1463 const lines = md.split('\n');
1464 const rawH1s = [];
1465 let current = null;
1466
1467 for (const line of lines) {
1468 const h1 = line.match(/^#(?!#)\s+(.+)$/);
1469 if (h1) {
1470 if (current) rawH1s.push(current);
1471 const title = h1[1].trim();
1472 current = { title, slug: slugify(title), rawContent: '' };
1473 } else if (current) {
1474 current.rawContent += line + '\n';
1475 }
1476 }
1477 if (current) rawH1s.push(current);
1478
1479 // Detect multisection: first non-empty line of rawContent is "multisection"
1480 const result = [];
1481 for (const sec of rawH1s) {
1482 const firstLine = sec.rawContent.split('\n').find(l => l.trim() !== '');
1483 if (firstLine && firstLine.trim().toLowerCase() === 'multisection') {
1484 // Strip the "multisection" keyword line from content
1485 const bodyAfterKeyword = sec.rawContent.replace(/^[\s\S]*?multisection[ \t]*\n/i, '');
1486 // Split into sub-sections by H2
1487 const subLines = bodyAfterKeyword.split('\n');
1488 const subSections = [];
1489 let curSub = null;
1490 for (const line of subLines) {
1491 const h2 = line.match(/^##\s+(.+)$/);
1492 if (h2) {
1493 if (curSub) subSections.push(curSub);
1494 const subTitle = h2[1].trim();
1495 curSub = { title: subTitle, slug: slugify(subTitle), rawContent: '', parentSlug: sec.slug, parentTitle: sec.title };
1496 } else if (curSub) {
1497 curSub.rawContent += line + '\n';
1498 }
1499 }
1500 if (curSub) subSections.push(curSub);
1501
1502 // Push a group placeholder (not a real page)
1503 result.push({
1504 title: sec.title,
1505 slug: sec.slug,
1506 isGroup: true,
1507 children: subSections
1508 });
1509 // Push each sub-section as a top-level navigable section
1510 subSections.forEach(sub => result.push(sub));
1511 } else if (firstLine && firstLine.trim().toLowerCase() === 'articles') {
1512 // ── Articles section ──────────────────────────────────────────────
1513 const bodyAfterKeyword = sec.rawContent.replace(/^[\s\S]*?articles[ \t]*\n/i, '');
1514 // Each H2 is one article
1515 const subLines = bodyAfterKeyword.split('\n');
1516 const articles = [];
1517 let curArt = null;
1518 for (const line of subLines) {
1519 const h2 = line.match(/^##(?!#)\s+(.+)$/);
1520 if (h2) {
1521 if (curArt) articles.push(curArt);
1522 const artTitle = h2[1].trim();
1523 curArt = { title: artTitle, slug: slugify(artTitle), rawContent: '' };
1524 } else if (curArt) {
1525 curArt.rawContent += line + '\n';
1526 }
1527 }
1528 if (curArt) articles.push(curArt);
1529
1530 result.push({
1531 title: sec.title,
1532 slug: sec.slug,
1533 isArticles: true,
1534 rawContent: '',
1535 articles: articles
1536 });
1537 } else {
1538 result.push(sec);
1539 }
1540 }
1541 return result;
1542 }
1543
1544 // =============================================
1545 // TOC EXTRACTION
1546 // =============================================
1547 function extractHeadings(html) {
1548 const headings = [];
1549 const re = /<h([234])[^>]*id="([^"]*)"[^>]*>([\s\S]*?)<\/h\1>/gi;
1550 let m;
1551 while ((m = re.exec(html)) !== null) {
1552 headings.push({ level: parseInt(m[1]), id: m[2], text: m[3].replace(/<[^>]+>/g,'') });
1553 }
1554 return headings;
1555 }
1556
1557 // =============================================
1558 // SEARCH INDEX
1559 // =============================================
1560 function buildSearchIndex() {
1561 searchIndex = sections
1562 .filter(sec => !sec.isGroup && !sec.isArticles)
1563 .map(sec => ({
1564 slug: sec.slug,
1565 title: sec.title,
1566 text: (sec.rawContent || '').replace(/[#*_`\[\](){}|>]/g,'').toLowerCase(),
1567 rawContent: sec.rawContent || ''
1568 }));
1569 }
1570
1571 function doSearch(query) {
1572 const q = query.toLowerCase().normalize('NFD').replace(/[\u0300-\u036f]/g,'');
1573 const words = q.split(/\s+/).filter(Boolean);
1574 const results = [];
1575
1576 for (const entry of searchIndex) {
1577 const haystack = (entry.title + ' ' + entry.text).normalize('NFD').replace(/[\u0300-\u036f]/g,'');
1578 const score = words.reduce((s, w) => {
1579 const inTitle = entry.title.toLowerCase().normalize('NFD').replace(/[\u0300-\u036f]/g,'').includes(w) ? 3 : 0;
1580 const inBody = (haystack.match(new RegExp(w.replace(/[.*+?^${}()|[\]\\]/g,'\\$&'), 'g')) || []).length;
1581 return s + inTitle + inBody;
1582 }, 0);
1583 if (score > 0) {
1584 // Find excerpt
1585 let excerpt = entry.text;
1586 const firstWord = words[0];
1587 const idx = excerpt.normalize('NFD').replace(/[\u0300-\u036f]/g,'').indexOf(firstWord);
1588 if (idx > -1) {
1589 const start = Math.max(0, idx - 80);
1590 excerpt = (start > 0 ? '…' : '') + excerpt.slice(start, idx + 200) + '…';
1591 } else {
1592 excerpt = excerpt.slice(0, 200) + '…';
1593 }
1594 // Highlight
1595 let highlighted = escHtml(excerpt);
1596 words.forEach(w => {
1597 highlighted = highlighted.replace(new RegExp(w.replace(/[.*+?^${}()|[\]\\]/g,'\\$&'), 'gi'), m => `<mark>${m}</mark>`);
1598 });
1599 results.push({ entry, score, excerpt: highlighted });
1600 }
1601 }
1602 return results.sort((a,b) => b.score - a.score);
1603 }
1604
1605 // =============================================
1606 // ROUTING
1607 // =============================================
1608 function getHashParts() {
1609 const raw = location.hash.replace(/^#/, '');
1610 if (!raw) return { section: null, anchor: null, articleSlug: null, tagFilter: null };
1611
1612 // Articles: #sec-slug/article-slug or #sec-slug?tag=xxx
1613 const slashIdx = raw.indexOf('/');
1614 const qIdx = raw.indexOf('?');
1615 let secHash = raw, articleSlug = null, tagFilter = null;
1616 if (slashIdx > -1) {
1617 secHash = raw.slice(0, slashIdx);
1618 articleSlug = raw.slice(slashIdx + 1);
1619 } else if (qIdx > -1) {
1620 secHash = raw.slice(0, qIdx);
1621 const qs = raw.slice(qIdx + 1);
1622 const tagMatch = qs.match(/(?:^|&)tag=([^&]*)/);
1623 if (tagMatch) tagFilter = decodeURIComponent(tagMatch[1]);
1624 }
1625
1626 // Exact section slug match
1627 for (const sec of sections) {
1628 if (!sec.isGroup && secHash === sec.slug) {
1629 return { section: sec, anchor: null, articleSlug, tagFilter };
1630 }
1631 }
1632 // Exact heading id match within any non-group, non-articles section
1633 for (const sec of sections) {
1634 if (!sec.isArticles && sec.headings && sec.headings.find(h => h.id === raw)) {
1635 return { section: sec, anchor: raw, articleSlug: null, tagFilter: null };
1636 }
1637 }
1638 return { section: null, anchor: null, articleSlug: null, tagFilter: null };
1639 }
1640
1641 function navigate(hash, replace = false) {
1642 if (replace) {
1643 history.replaceState(null, '', hash ? `#${hash}` : location.pathname);
1644 } else {
1645 history.pushState(null, '', hash ? `#${hash}` : location.pathname);
1646 }
1647 render();
1648 }
1649
1650 // =============================================
1651 // RENDER
1652 // =============================================
1653 function render() {
1654 // Hide search if active
1655 hideSearch();
1656
1657 const { section, anchor, articleSlug, tagFilter } = getHashParts();
1658 // If resolved section is a group (no parsedHTML), navigate to first child
1659 let sec = section || sections.find(s => !s.isGroup);
1660 if (sec && sec.isGroup) {
1661 sec = sec.children && sec.children.length ? sec.children[0] : sections.find(s => !s.isGroup);
1662 }
1663 if (!sec) return;
1664 currentSection = sec;
1665
1666 // Update page title
1667 document.title = `${sec.title} | ${config.title || 'Web'}`;
1668
1669 // Update nav active state
1670 document.querySelectorAll('#site-nav a[data-slug]').forEach(a => {
1671 const active = a.dataset.slug === sec.slug;
1672 a.setAttribute('aria-current', active ? 'page' : 'false');
1673 });
1674 // Auto-expand parent group if a child page is active
1675 document.querySelectorAll('.nav-group').forEach(group => {
1676 const hasActive = group.querySelector(`a[aria-current="page"]`);
1677 const btn = group.querySelector('.nav-group-btn');
1678 const submenuId = btn && btn.getAttribute('aria-controls');
1679 const submenu = submenuId && document.getElementById(submenuId);
1680 if (btn && submenu) {
1681 if (hasActive) { submenu.hidden = false; btn.setAttribute('aria-expanded', 'true'); }
1682 }
1683 });
1684
1685 // ── Articles section ──────────────────────────────────────────────────
1686 if (sec.isArticles) {
1687 clearSidebarPanels();
1688 if (articleSlug) {
1689 const art = sec.articles.find(a => a.slug === articleSlug);
1690 if (art) { renderArticleDetail(sec, art); return; }
1691 }
1692 renderArticlesList(sec, tagFilter);
1693 return;
1694 }
1695
1696 // ── Normal section ────────────────────────────────────────────────────
1697 const html = sec.parsedHTML;
1698 const pageContent = document.getElementById('page-content');
1699 pageContent.innerHTML = `<h1 class="page-title">${escHtml(sec.title)}</h1>${html}`;
1700
1701 // Resolve and validate internal links — broken ones become plain text
1702 processInternalLinks(pageContent);
1703
1704 // Build TOC
1705 buildTOC(sec.headings);
1706
1707 // Render news items into page (before TOC/links — must happen after innerHTML set)
1708 if (sec.hasNews && sec.newsItems && sec.newsItems.length) {
1709 renderNewsItems(sec.newsItems, pageContent);
1710 }
1711
1712 // Build external links sidebar section
1713 buildExtLinks(sec.parsedHTML);
1714
1715 // Build critic comments sidebar section (after DOM is rendered)
1716 buildComments(pageContent);
1717
1718 // Build news calendar sidebar section
1719 buildNewsCalendar(sec.hasNews ? sec.newsItems : null);
1720
1721 // Hide tags panel (not an articles section)
1722 document.getElementById('tags-nav').hidden = true;
1723
1724 // Sidebar custom content
1725 if (sidebarMD) {
1726 const sidebarEl = document.getElementById('sidebar-content');
1727 sidebarEl.innerHTML = parseMD(sidebarMD);
1728 processInternalLinks(sidebarEl);
1729 document.getElementById('sidebar-custom').hidden = false;
1730 }
1731
1732 // Scroll / anchor
1733 requestAnimationFrame(() => {
1734 if (anchor) {
1735 const el = document.getElementById(anchor);
1736 if (el) { el.scrollIntoView({ behavior: 'smooth', block: 'start' }); return; }
1737 }
1738 window.scrollTo({ top: 0, behavior: 'smooth' });
1739 });
1740
1741 // Update TOC active state on scroll
1742 setupScrollSpy();
1743 }
1744
1745 // Returns the hash to navigate to, or null if no target exists
1746 function resolveInternalTarget(target) {
1747 const targetSlug = slugify(target);
1748
1749 // 1) Exact section match (not a group)
1750 if (sections.find(s => !s.isGroup && s.slug === targetSlug)) {
1751 return targetSlug;
1752 }
1753
1754 // 2) Exact heading match in any section
1755 for (const s of sections) {
1756 if (!s.headings) continue;
1757 const hit = s.headings.find(h =>
1758 h.id === targetSlug || slugify(h.text) === targetSlug
1759 );
1760 if (hit) return hit.id;
1761 }
1762
1763 return null;
1764 }
1765
1766 // Walk all .internal-link elements in a container.
1767 // Supports [Text](#secslug|tagname) for tag-filtered article links.
1768 function processInternalLinks(container) {
1769 container.querySelectorAll('.internal-link').forEach(a => {
1770 const target = a.dataset.target;
1771
1772 // Tag-filter link: [Text](#secslug|tagname)
1773 if (target && target.startsWith('#') && target.includes('|')) {
1774 const parts = target.slice(1).split('|');
1775 const secSlug = slugify(parts[0]);
1776 const tagName = parts.slice(1).join('|').trim();
1777 const hash = `${secSlug}?tag=${encodeURIComponent(tagName)}`;
1778 a.href = `#${hash}`;
1779 a.addEventListener('click', e => { e.preventDefault(); navigate(hash); closeMobileMenu(); });
1780 return;
1781 }
1782
1783 const resolved = resolveInternalTarget(target);
1784 if (resolved === null) {
1785 const text = document.createTextNode('[' + a.textContent + ']');
1786 a.replaceWith(text);
1787 } else {
1788 a.href = `#${resolved}`;
1789 a.addEventListener('click', e => { e.preventDefault(); navigate(resolved); closeMobileMenu(); });
1790 }
1791 });
1792 }
1793
1794 // =============================================
1795 // NEWS ITEMS PARSER & RENDERER
1796 // =============================================
1797
1798 // Supported date formats:
1799 // YYYY-MM-DD 2026-05-18
1800 // DD.MM.YYYY 18.05.2026 or 18.5.2026
1801 // MM/DD/YYYY 5/18/2026 or 05/18/2026
1802 const NEWS_DATE_RE = /^(?:(\d{4})-(\d{1,2})-(\d{1,2})|(\d{1,2})\.(\d{1,2})\.(\d{4})|(\d{1,2})\/(\d{1,2})\/(\d{4})):\s*/;
1803
1804 function parseNewsDate(line) {
1805 const m = line.match(NEWS_DATE_RE);
1806 if (!m) return null;
1807 let y, mo, d;
1808 if (m[1]) { y = +m[1]; mo = +m[2]; d = +m[3]; } // YYYY-MM-DD
1809 else if (m[4]) { d = +m[4]; mo = +m[5]; y = +m[6]; } // DD.MM.YYYY
1810 else { mo = +m[7]; d = +m[8]; y = +m[9]; } // MM/DD/YYYY
1811 const date = new Date(y, mo - 1, d);
1812 if (isNaN(date.getTime())) return null;
1813 return { date, isoKey: `${y}-${String(mo).padStart(2,'0')}-${String(d).padStart(2,'0')}`,
1814 rest: line.slice(m[0].length).trim() };
1815 }
1816
1817 function formatNewsDate(date) {
1818 const l = config.language || 'cs';
1819 const months = lang.months
1820 ? lang.months
1821 : (l === 'cs'
1822 ? ['ledna','února','března','dubna','května','června','července','srpna','září','října','listopadu','prosince']
1823 : ['January','February','March','April','May','June','July','August','September','October','November','December']);
1824 const dows = lang.days_long
1825 ? lang.days_long
1826 : (l === 'cs'
1827 ? ['neděle','pondělí','úterý','středa','čtvrtek','pátek','sobota']
1828 : ['Sunday','Monday','Tuesday','Wednesday','Thursday','Friday','Saturday']);
1829 const dow = dows[date.getDay()];
1830 const day = date.getDate();
1831 const month = months[date.getMonth()];
1832 const year = date.getFullYear();
1833 const dateStr = l === 'cs' ? `${day}. ${month} ${year}` : `${month} ${day}, ${year}`;
1834 return { dow, dateStr };
1835 }
1836
1837 // Parse rawContent and split into news items + regular MD
1838 // Returns { hasNews, newsItems, regularMD }
1839 // newsItems: [{isoKey, date, title, bodyMD}]
1840 function extractNewsItems(rawContent) {
1841 const lines = rawContent.split('
1842');
1843 const newsItems = [];
1844 const regularLines = [];
1845 let i = 0;
1846
1847 while (i < lines.length) {
1848 const line = lines[i];
1849 const parsed = parseNewsDate(line.trim());
1850
1851 if (parsed) {
1852 // First line after date prefix is the title (inline rest of line)
1853 // Subsequent indented/non-empty lines before next date entry are the body
1854 const titleFirstLine = parsed.rest;
1855 const bodyLines = [];
1856
1857 i++;
1858 while (i < lines.length) {
1859 const nextLine = lines[i];
1860 // Stop if next line starts a new news item
1861 if (parseNewsDate(nextLine.trim())) break;
1862 bodyLines.push(nextLine);
1863 i++;
1864 }
1865
1866 // Trim trailing empty lines from body
1867 while (bodyLines.length && !bodyLines[bodyLines.length - 1].trim()) bodyLines.pop();
1868
1869 newsItems.push({
1870 isoKey: parsed.isoKey,
1871 date: parsed.date,
1872 title: titleFirstLine,
1873 bodyMD: bodyLines.join('
1874')
1875 });
1876 } else {
1877 regularLines.push(line);
1878 i++;
1879 }
1880 }
1881
1882 return {
1883 hasNews: newsItems.length > 0,
1884 newsItems: newsItems.sort((a, b) => b.date - a.date), // newest first
1885 regularMD: regularLines.join('
1886')
1887 };
1888 }
1889
1890 // Render news items into page content el, inserting before first h2 or at end
1891 function renderNewsItems(newsItems, pageContentEl) {
1892 if (!newsItems.length) return;
1893
1894 const ul = document.createElement('ul');
1895 ul.className = 'news-list';
1896 ul.setAttribute('aria-label', lang.news_list || 'Novinky');
1897
1898 newsItems.forEach(item => {
1899 const { dow, dateStr } = formatNewsDate(item.date);
1900 const li = document.createElement('li');
1901 li.className = 'news-item';
1902 li.id = `news-${item.isoKey}`;
1903
1904 // Date header
1905 const header = document.createElement('div');
1906 header.className = 'news-date-header';
1907 header.innerHTML =
1908 `<span class="news-date-icon" aria-hidden="true">📅</span>` +
1909 `<span class="news-dow">${escHtml(dow)}</span>` +
1910 `<span class="news-date">${escHtml(dateStr)}</span>`;
1911
1912 li.appendChild(header);
1913
1914 // Title (if present)
1915 if (item.title) {
1916 const titleEl = document.createElement('div');
1917 titleEl.className = 'news-title';
1918 titleEl.innerHTML = parseMDInline(item.title);
1919 li.appendChild(titleEl);
1920 }
1921
1922 // Body (if present) — full markdown
1923 if (item.bodyMD.trim()) {
1924 const bodyEl = document.createElement('div');
1925 bodyEl.className = 'news-body';
1926 bodyEl.innerHTML = parseMD(item.bodyMD);
1927 li.appendChild(bodyEl);
1928 }
1929
1930 ul.appendChild(li);
1931 });
1932
1933 // Insert before first h2 or append
1934 const firstH2 = pageContentEl.querySelector('h2');
1935 if (firstH2) {
1936 pageContentEl.insertBefore(ul, firstH2);
1937 } else {
1938 pageContentEl.appendChild(ul);
1939 }
1940
1941 // Activate internal links within news items
1942 processInternalLinks(ul);
1943 }
1944
1945 // Build calendar widget in sidebar
1946 function buildNewsCalendar(newsItems) {
1947 const nav = document.getElementById('news-calendar-nav');
1948 const calEl = document.getElementById('news-calendar');
1949
1950 if (!newsItems || !newsItems.length) {
1951 nav.hidden = true;
1952 return;
1953 }
1954 nav.hidden = false;
1955 document.getElementById('news-calendar-heading').textContent =
1956 (lang.calendar_icon || '📅') + ' ' + (lang.calendar || 'Kalendář');
1957
1958 // Build a set of ISO keys for fast lookup
1959 const newsDays = new Set(newsItems.map(n => n.isoKey));
1960
1961 const today = new Date();
1962 // Start with month of most recent news item
1963 let viewYear = newsItems[0].date.getFullYear();
1964 let viewMonth = newsItems[0].date.getMonth(); // 0-based
1965
1966 function renderCalendar() {
1967 calEl.innerHTML = '';
1968 const l = config.language || 'cs';
1969 const monthNames = lang.months_short
1970 ? lang.months_short
1971 : (l === 'cs'
1972 ? ['Led','Ún','Bře','Dub','Kvě','Čvn','Čvc','Srp','Zář','Říj','Lis','Pro']
1973 : ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec']);
1974 const dowShort = lang.days_short
1975 ? lang.days_short
1976 : (l === 'cs'
1977 ? ['Po','Út','St','Čt','Pá','So','Ne']
1978 : ['Mo','Tu','We','Th','Fr','Sa','Su']);
1979
1980 const cal = document.createElement('div');
1981 cal.className = 'news-cal';
1982
1983 // Nav row
1984 const nav = document.createElement('div');
1985 nav.className = 'news-cal-nav';
1986 const prevBtn = document.createElement('button');
1987 prevBtn.textContent = '‹';
1988 prevBtn.type = 'button';
1989 prevBtn.setAttribute('aria-label', lang.cal_prev || 'Předchozí měsíc');
1990 prevBtn.addEventListener('click', () => { viewMonth--; if (viewMonth < 0) { viewMonth = 11; viewYear--; } renderCalendar(); });
1991 const nextBtn = document.createElement('button');
1992 nextBtn.textContent = '›';
1993 nextBtn.type = 'button';
1994 nextBtn.setAttribute('aria-label', lang.cal_next || 'Následující měsíc');
1995 nextBtn.addEventListener('click', () => { viewMonth++; if (viewMonth > 11) { viewMonth = 0; viewYear++; } renderCalendar(); });
1996 const monthLabel = document.createElement('div');
1997 monthLabel.className = 'news-cal-month';
1998 monthLabel.textContent = `${monthNames[viewMonth]} ${viewYear}`;
1999 nav.appendChild(prevBtn); nav.appendChild(monthLabel); nav.appendChild(nextBtn);
2000 cal.appendChild(nav);
2001
2002 // Grid
2003 const grid = document.createElement('div');
2004 grid.className = 'news-cal-grid';
2005
2006 // Day-of-week headers (Mon-first for CS, Mon-first for EN too — ISO week)
2007 dowShort.forEach(d => {
2008 const cell = document.createElement('div');
2009 cell.className = 'news-cal-dow';
2010 cell.textContent = d;
2011 cell.setAttribute('aria-hidden', 'true');
2012 grid.appendChild(cell);
2013 });
2014
2015 // First day of month, adjust to Mon=0 week start
2016 const firstDay = new Date(viewYear, viewMonth, 1);
2017 // getDay(): 0=Sun,1=Mon..6=Sat → convert to Mon=0
2018 const startOffset = (firstDay.getDay() + 6) % 7;
2019 const daysInMonth = new Date(viewYear, viewMonth + 1, 0).getDate();
2020
2021 // Empty cells before first day
2022 for (let i = 0; i < startOffset; i++) {
2023 const cell = document.createElement('div');
2024 cell.className = 'news-cal-day empty';
2025 cell.setAttribute('aria-hidden', 'true');
2026 grid.appendChild(cell);
2027 }
2028
2029 // Day cells
2030 for (let d = 1; d <= daysInMonth; d++) {
2031 const isoKey = `${viewYear}-${String(viewMonth+1).padStart(2,'0')}-${String(d).padStart(2,'0')}`;
2032 const isToday = today.getFullYear() === viewYear && today.getMonth() === viewMonth && today.getDate() === d;
2033 const hasNews = newsDays.has(isoKey);
2034
2035 const cell = document.createElement(hasNews ? 'button' : 'div');
2036 cell.className = 'news-cal-day' + (isToday ? ' today' : '') + (hasNews ? ' has-news' : '');
2037 cell.textContent = d;
2038
2039 if (hasNews) {
2040 cell.type = 'button';
2041 cell.setAttribute('aria-label', `${lang.go_to_news || 'Přejít na novinku'} ${isoKey}`);
2042 cell.addEventListener('click', () => {
2043 const target = document.getElementById(`news-${isoKey}`);
2044 if (target) { target.scrollIntoView({ behavior: 'smooth', block: 'start' }); target.focus(); }
2045 });
2046 } else {
2047 cell.setAttribute('aria-hidden', 'true');
2048 }
2049 grid.appendChild(cell);
2050 }
2051
2052 cal.appendChild(grid);
2053 calEl.appendChild(cal);
2054 }
2055
2056 renderCalendar();
2057 }
2058
2059 // =============================================
2060 // ARTICLES ENGINE
2061 // =============================================
2062
2063 // Parse date/tags metadata from article rawContent
2064 // Returns { date, dateRaw, tags, perexMD, bodyMD, headings }
2065 function parseArticle(art) {
2066 const lines = art.rawContent.split('\n');
2067 let date = null, dateRaw = '', tags = [], metaEnd = 0;
2068
2069 // Scan first non-empty lines for Datum:/Date: and Tagy:/Tags:
2070 let i = 0;
2071 while (i < lines.length) {
2072 const l = lines[i].trim();
2073 if (!l) { i++; continue; }
2074 const dateMatch = l.match(/^(?:datum|date)\s*:\s*(.+)$/i);
2075 const tagsMatch = l.match(/^(?:tagy|tags)\s*:\s*(.+)$/i);
2076 if (dateMatch) {
2077 dateRaw = dateMatch[1].trim();
2078 // Try to parse as news date format
2079 const parsed = parseNewsDate(dateRaw + ':');
2080 if (parsed) date = parsed.date;
2081 metaEnd = i + 1; i++; continue;
2082 }
2083 if (tagsMatch) {
2084 tags = tagsMatch[1].split(',').map(t => t.trim()).filter(Boolean);
2085 metaEnd = i + 1; i++; continue;
2086 }
2087 // Stop scanning for meta after first non-meta non-empty line
2088 if (metaEnd === 0 && l) break;
2089 break;
2090 }
2091
2092 // Rest of content: find perex (first non-empty paragraph) and body
2093 const rest = lines.slice(metaEnd).join('\n').trimStart();
2094 const paraBreak = rest.search(/
2095\s*
2096/);
2097 let perexMD = '', bodyMD = '';
2098 if (paraBreak === -1) {
2099 perexMD = rest.trim();
2100 } else {
2101 perexMD = rest.slice(0, paraBreak).trim();
2102 bodyMD = rest.slice(paraBreak).trim();
2103 }
2104
2105 const parsedBody = bodyMD ? parseMD(bodyMD) : '';
2106 const headings = extractHeadings(parsedBody);
2107
2108 return { ...art, date, dateRaw, tags, perexMD, parsedPerex: parseMDInline(perexMD), parsedBody, headings };
2109 }
2110
2111 // Collect all tags from a section, sorted case-insensitively
2112 function collectTags(sec) {
2113 const tagSet = new Set();
2114 sec.articles.forEach(a => a.tags.forEach(t => tagSet.add(t)));
2115 return [...tagSet].sort((a, b) => a.toLowerCase().localeCompare(b.toLowerCase()));
2116 }
2117
2118 // Format article date
2119 function formatArticleDate(art) {
2120 if (art.date) return formatNewsDate(art.date).dateStr;
2121 return art.dateRaw || '';
2122 }
2123
2124 function clearSidebarPanels() {
2125 document.getElementById('toc-nav').hidden = true;
2126 document.getElementById('ext-links-nav').hidden = true;
2127 document.getElementById('comments-nav').hidden = true;
2128 document.getElementById('news-calendar-nav').hidden = true;
2129 if (sidebarMD) {
2130 const sidebarEl = document.getElementById('sidebar-content');
2131 sidebarEl.innerHTML = parseMD(sidebarMD);
2132 processInternalLinks(sidebarEl);
2133 document.getElementById('sidebar-custom').hidden = false;
2134 } else {
2135 document.getElementById('sidebar-custom').hidden = true;
2136 }
2137 }
2138
2139 // Build sidebar tags cloud for an articles section
2140 function buildTagsSidebar(sec, activeTag, onTagClick) {
2141 const nav = document.getElementById('tags-nav');
2142 const cloud = document.getElementById('tags-cloud');
2143 const tags = collectTags(sec);
2144 cloud.innerHTML = '';
2145 if (!tags.length) { nav.hidden = true; return; }
2146
2147 document.getElementById('tags-heading').textContent =
2148 (lang.tags_icon || '🏷') + ' ' + (lang.tags || 'Tagy');
2149
2150 tags.forEach(tag => {
2151 const btn = document.createElement('button');
2152 btn.className = 'tag-badge' + (activeTag && activeTag.toLowerCase() === tag.toLowerCase() ? ' active' : '');
2153 btn.textContent = tag;
2154 btn.type = 'button';
2155 btn.setAttribute('aria-pressed', activeTag && activeTag.toLowerCase() === tag.toLowerCase() ? 'true' : 'false');
2156 btn.addEventListener('click', () => onTagClick(tag));
2157 cloud.appendChild(btn);
2158 });
2159 nav.hidden = false;
2160 }
2161
2162 // Render articles list view
2163 function renderArticlesList(sec, activeTag) {
2164 const pageContent = document.getElementById('page-content');
2165 clearSidebarPanels();
2166
2167 // Filtered articles
2168 const filtered = activeTag
2169 ? sec.articles.filter(a => a.tags.some(t => t.toLowerCase() === activeTag.toLowerCase()))
2170 : sec.articles;
2171
2172 // Page title + header
2173 let headerHTML = `<h1 class="page-title">${escHtml(sec.title)}</h1>`;
2174 headerHTML += `<div class="articles-header">`;
2175 headerHTML += `<span class="articles-count">${filtered.length} ${filtered.length === 1 ? (lang.article_singular || 'článek') : (lang.article_plural || 'článků')}</span>`;
2176 if (activeTag) {
2177 headerHTML += `<span class="articles-active-filter">` +
2178 `${escHtml(activeTag)} <button type="button" aria-label="${lang.clear_filter || 'Zrušit filtr'}">×</button></span>`;
2179 }
2180 headerHTML += `</div>`;
2181
2182 if (!filtered.length) {
2183 pageContent.innerHTML = headerHTML + `<p>${lang.no_articles || 'Žádné články pro tento tag.'}</p>`;
2184 } else {
2185 const listHTML = filtered.map(art => {
2186 const dateStr = formatArticleDate(art);
2187 const tagsHTML = art.tags.map(t =>
2188 `<button class="tag-badge${activeTag && t.toLowerCase() === activeTag.toLowerCase() ? ' active' : ''}" type="button" data-tag="${escAttr(t)}">${escHtml(t)}</button>`
2189 ).join(' ');
2190 return `<li class="article-card" data-slug="${escAttr(art.slug)}">
2191 <div class="article-card-title"><a href="#${escAttr(sec.slug)}/${escAttr(art.slug)}">${escHtml(art.title)}</a></div>
2192 <div class="article-card-meta">
2193 ${dateStr ? `<span class="article-card-date">📅 ${escHtml(dateStr)}</span>` : ''}
2194 ${tagsHTML}
2195 </div>
2196 <div class="article-card-perex">${art.parsedPerex}</div>
2197 </li>`;
2198 }).join('');
2199 pageContent.innerHTML = headerHTML + `<ul class="articles-list" aria-label="${lang.articles_list || 'Seznam článků'}">${listHTML}</ul>`;
2200 }
2201
2202 // Attach events
2203 if (activeTag) {
2204 const clearBtn = pageContent.querySelector('.articles-active-filter button');
2205 if (clearBtn) clearBtn.addEventListener('click', () => navigate(sec.slug));
2206 }
2207 pageContent.querySelectorAll('.tag-badge[data-tag]').forEach(btn => {
2208 btn.addEventListener('click', () => {
2209 const tag = btn.dataset.tag;
2210 navigate(`${sec.slug}?tag=${encodeURIComponent(tag)}`);
2211 });
2212 });
2213 pageContent.querySelectorAll('.article-card-title a').forEach(a => {
2214 a.addEventListener('click', e => {
2215 e.preventDefault();
2216 navigate(`${sec.slug}/${a.closest('.article-card').dataset.slug}`);
2217 });
2218 });
2219
2220 buildTagsSidebar(sec, activeTag, tag => navigate(`${sec.slug}?tag=${encodeURIComponent(tag)}`));
2221 document.title = `${sec.title} | ${config.title || 'Web'}`;
2222 window.scrollTo({ top: 0, behavior: 'smooth' });
2223 }
2224
2225 // Render single article detail view
2226 function renderArticleDetail(sec, art) {
2227 const pageContent = document.getElementById('page-content');
2228 clearSidebarPanels();
2229
2230 const dateStr = formatArticleDate(art);
2231 const tagsHTML = art.tags.map(t =>
2232 `<button class="tag-badge" type="button" data-tag="${escAttr(t)}">${escHtml(t)}</button>`
2233 ).join(' ');
2234
2235 pageContent.innerHTML =
2236 `<a class="article-back" href="#${escAttr(sec.slug)}">← ${escHtml(lang.back_to_list || 'Zpět na seznam')}</a>` +
2237 `<h1 class="page-title">${escHtml(art.title)}</h1>` +
2238 `<div class="article-detail-meta">` +
2239 (dateStr ? `<span>📅 ${escHtml(dateStr)}</span>` : '') +
2240 tagsHTML +
2241 `</div>` +
2242 (art.parsedPerex ? `<p class="article-perex">${art.parsedPerex}</p>` : '') +
2243 art.parsedBody;
2244
2245 // Events
2246 pageContent.querySelector('.article-back').addEventListener('click', e => {
2247 e.preventDefault(); navigate(sec.slug);
2248 });
2249 pageContent.querySelectorAll('.tag-badge[data-tag]').forEach(btn => {
2250 btn.addEventListener('click', () => navigate(`${sec.slug}?tag=${encodeURIComponent(btn.dataset.tag)}`));
2251 });
2252 processInternalLinks(pageContent);
2253
2254 // TOC from article headings (H3+)
2255 buildTOC(art.headings);
2256 buildExtLinks(art.parsedBody);
2257 buildComments(pageContent);
2258 document.getElementById('news-calendar-nav').hidden = true;
2259
2260 buildTagsSidebar(sec, null, tag => navigate(`${sec.slug}?tag=${encodeURIComponent(tag)}`));
2261
2262 document.title = `${art.title} | ${sec.title} | ${config.title || 'Web'}`;
2263 window.scrollTo({ top: 0, behavior: 'smooth' });
2264 }
2265
2266 function buildTOC(headings) {
2267 const root = document.getElementById('toc-list');
2268 root.innerHTML = '';
2269 if (!headings || !headings.length) {
2270 document.getElementById('toc-nav').hidden = true;
2271 return;
2272 }
2273 document.getElementById('toc-nav').hidden = false;
2274
2275 // Build a proper nested tree: h2 = top level, h3 under last h2, h4 under last h3
2276 // We keep a stack of {level, ul} pairs
2277 const makeLink = h => {
2278 const a = document.createElement('a');
2279 a.href = `#${h.id}`;
2280 a.textContent = h.text;
2281 a.addEventListener('click', e => {
2282 e.preventDefault();
2283 history.pushState(null, '', `#${h.id}`);
2284 const el = document.getElementById(h.id);
2285 if (el) el.scrollIntoView({ behavior: 'smooth', block: 'start' });
2286 closeMobileMenu();
2287 });
2288 return a;
2289 };
2290
2291 // stack entries: { level, ul }
2292 const stack = [{ level: 1, ul: root }];
2293
2294 headings.forEach(h => {
2295 const li = document.createElement('li');
2296 li.appendChild(makeLink(h));
2297
2298 // Pop stack until we find a parent whose level is less than current
2299 while (stack.length > 1 && stack[stack.length - 1].level >= h.level) {
2300 stack.pop();
2301 }
2302 stack[stack.length - 1].ul.appendChild(li);
2303
2304 // This li can be a parent for deeper headings — push a new ul onto stack
2305 const childUl = document.createElement('ul');
2306 li.appendChild(childUl);
2307 stack.push({ level: h.level, ul: childUl });
2308 });
2309
2310 // Remove empty trailing uls
2311 root.querySelectorAll('ul:empty').forEach(u => u.remove());
2312 }
2313
2314 function buildExtLinks(html) {
2315 const nav = document.getElementById('ext-links-nav');
2316 const list = document.getElementById('ext-links-list');
2317 list.innerHTML = '';
2318
2319 // Collect all external href values from rendered HTML
2320 const seen = new Set();
2321 const tmp = document.createElement('div');
2322 tmp.innerHTML = html;
2323 tmp.querySelectorAll('a[href]').forEach(a => {
2324 const href = a.getAttribute('href');
2325 if (href && href.startsWith('http') && !seen.has(href)) {
2326 seen.add(href);
2327 const li = document.createElement('li');
2328 const link = document.createElement('a');
2329 link.href = href;
2330 link.target = '_blank';
2331 link.rel = 'noopener noreferrer';
2332 link.textContent = href;
2333 link.title = href;
2334 li.appendChild(link);
2335 list.appendChild(li);
2336 }
2337 });
2338
2339 nav.hidden = seen.size === 0;
2340 if (document.getElementById('ext-links-heading')) {
2341 document.getElementById('ext-links-heading').textContent = (lang.ext_links_icon || '🔗') + ' ' + (lang.ext_links || 'Page links');
2342 }
2343 }
2344 function buildComments(pageContentEl) {
2345 const nav = document.getElementById('comments-nav');
2346 const list = document.getElementById('comments-list');
2347 list.innerHTML = '';
2348
2349 // Collect all critic comments rendered in the page
2350 const markers = pageContentEl.querySelectorAll('.critic-comment');
2351 if (!markers.length) {
2352 nav.hidden = true;
2353 document.getElementById('comments-heading').textContent =
2354 (lang.comments_icon || '💬') + ' ' + (lang.comments || 'Comments');
2355 return;
2356 }
2357
2358 markers.forEach((comment, idx) => {
2359 const n = idx + 1;
2360 // Give each comment an anchor id so we can link to it
2361 const anchorId = `critic-comment-${n}`;
2362 comment.id = anchorId;
2363 comment.classList.add('critic-comment-anchor');
2364
2365 // Extract comment text from the tooltip span
2366 const tooltip = comment.querySelector('.critic-comment-text');
2367 const text = tooltip ? tooltip.textContent.trim() : '';
2368
2369 const li = document.createElement('li');
2370 const a = document.createElement('a');
2371 a.href = `#${anchorId}`;
2372 a.textContent = text || `(${lang.comment_empty || 'empty comment'})`;
2373 a.setAttribute('aria-label', `${lang.comments || 'Comment'} ${n}: ${text}`);
2374 a.addEventListener('click', e => {
2375 e.preventDefault();
2376 const target = document.getElementById(anchorId);
2377 if (target) {
2378 target.scrollIntoView({ behavior: 'smooth', block: 'center' });
2379 // Move focus to the comment button for keyboard/screen-reader users
2380 const btn = target.querySelector('.critic-comment-marker');
2381 if (btn) btn.focus();
2382 }
2383 });
2384 li.appendChild(a);
2385 list.appendChild(li);
2386 });
2387
2388 nav.hidden = false;
2389 document.getElementById('comments-heading').textContent =
2390 (lang.comments_icon || '💬') + ' ' + (lang.comments || 'Comments');
2391 }
2392
2393 let scrollSpyObserver = null;
2394
2395 function setupScrollSpy() {
2396 if (scrollSpyObserver) scrollSpyObserver.disconnect();
2397 scrollSpyObserver = new IntersectionObserver(entries => {
2398 entries.forEach(entry => {
2399 if (entry.isIntersecting) {
2400 const id = entry.target.id;
2401 document.querySelectorAll('#toc-list a').forEach(a => {
2402 a.classList.toggle('active', a.getAttribute('href') === `#${id}`);
2403 });
2404 }
2405 });
2406 }, { rootMargin: '-20% 0px -70% 0px' });
2407 document.querySelectorAll('#page-content h2, #page-content h3, #page-content h4').forEach(el => scrollSpyObserver.observe(el));
2408 }
2409
2410 // =============================================
2411 // SEARCH UI
2412 // =============================================
2413 function showSearch(results, query) {
2414 const container = document.getElementById('search-results');
2415 const titleEl = document.getElementById('search-results-title');
2416 const listEl = document.getElementById('search-results-list');
2417 const pageContent = document.getElementById('page-content');
2418
2419 container.classList.add('active');
2420 pageContent.style.display = 'none';
2421
2422 titleEl.textContent = results.length
2423 ? (lang.search_results_for || 'Results for') + ` "${query}" (${results.length})`
2424 : (lang.search_no_results || 'No results for') + ` "${query}"`;
2425
2426 if (!results.length) {
2427 listEl.innerHTML = `<p class="search-no-results">${lang.search_try_other || 'Try a different keyword.'}</p>`;
2428 return;
2429 }
2430 listEl.innerHTML = results.map(r => `
2431 <div class="search-result-item">
2432 <h3><a href="#${r.entry.slug}">${escHtml(r.entry.title)}</a></h3>
2433 <p class="result-section">${lang.section || 'Section'}</p>
2434 <p class="result-excerpt">${r.excerpt}</p>
2435 </div>
2436 `).join('');
2437 listEl.querySelectorAll('a').forEach(a => {
2438 a.addEventListener('click', e => {
2439 e.preventDefault();
2440 navigate(a.getAttribute('href').replace('#',''));
2441 clearSearch();
2442 });
2443 });
2444 }
2445
2446 function hideSearch() {
2447 document.getElementById('search-results').classList.remove('active');
2448 const pageContent = document.getElementById('page-content');
2449 if (pageContent) pageContent.style.display = '';
2450 }
2451
2452 function clearSearch() {
2453 const inp = document.getElementById('search-input');
2454 inp.value = '';
2455 document.getElementById('search-clear').hidden = true;
2456 hideSearch();
2457 }
2458
2459 // =============================================
2460 // MOBILE MENU
2461 // =============================================
2462 function openMobileMenu() { /* sidebar je na mobilu staticky pod obsahem */ }
2463 function closeMobileMenu() { /* nic k zavírání */ }
2464
2465 // =============================================
2466 // APPLY CONFIG
2467 // =============================================
2468 function applyConfig() {
2469 const palette = (config.color || 'blue').toLowerCase();
2470 document.documentElement.setAttribute('data-palette', palette);
2471 document.documentElement.lang = config.language || 'cs';
2472
2473 document.getElementById('site-title').textContent = config.title || 'Web';
2474 document.getElementById('site-tagline').textContent = config.tagline || '';
2475
2476 // Footer
2477 const copy = document.getElementById('footer-copyright');
2478 if (config.copyright) {
2479 const { name, url, year } = config.copyright;
2480 const y = year || new Date().getFullYear();
2481 copy.innerHTML = `© ${y} ` + (url ? `<a href="${escAttr(url)}" target="_blank" rel="noopener">${escHtml(name||url)}</a>` : escHtml(name||''));
2482 }
2483 const footerLinks = document.getElementById('footer-links');
2484 if (config.footer_links) {
2485 footerLinks.innerHTML = config.footer_links.map(l =>
2486 `<a href="${escAttr(l.url)}"${l.url.startsWith('http') ? ' target="_blank" rel="noopener"' : ''}>${escHtml(l.label)}</a>`
2487 ).join('');
2488 }
2489 }
2490
2491 function applyLang() {
2492 document.getElementById('toc-heading').textContent = (lang.toc_icon || '📋') + ' ' + (lang.toc || 'On this page');
2493 document.getElementById('search-btn-label').textContent = lang.search || 'Search';
2494 document.getElementById('search-clear-label').textContent = lang.cancel || 'Clear';
2495 document.getElementById('search-input').placeholder = lang.search_placeholder || 'Search…';
2496 document.getElementById('search-input').setAttribute('aria-label', lang.search || 'Search');
2497 document.getElementById('skip-link').textContent = lang.skip_to_content || 'Skip to content';
2498 const menuToggle = document.getElementById('menu-toggle');
2499 menuToggle.textContent = '☰ ' + (lang.menu || 'Menu');
2500 menuToggle.setAttribute('aria-label', lang.open_sidebar || 'Open sidebar');
2501 }
2502
2503 function buildNav() {
2504 const nav = document.getElementById('site-nav');
2505 nav.innerHTML = '';
2506
2507 const sectionLinks = document.createElement('div');
2508 sectionLinks.className = 'nav-section-links';
2509
2510 // Only iterate top-level items: groups + non-child sections
2511 sections.forEach(sec => {
2512 if (sec.parentSlug) return; // sub-pages rendered inside their group
2513
2514 if (sec.isGroup) {
2515 // ── Dropdown group ────────────────────────────────────────────────
2516 const wrapper = document.createElement('div');
2517 wrapper.className = 'nav-group';
2518 wrapper.dataset.groupSlug = sec.slug;
2519
2520 const btn = document.createElement('button');
2521 btn.className = 'nav-group-btn';
2522 btn.type = 'button';
2523 btn.textContent = sec.title;
2524 btn.setAttribute('aria-expanded', 'false');
2525 const submenuId = `submenu-${sec.slug}`;
2526 btn.setAttribute('aria-controls', submenuId);
2527
2528 const submenu = document.createElement('ul');
2529 submenu.className = 'nav-submenu';
2530 submenu.id = submenuId;
2531 submenu.setAttribute('role', 'list');
2532 submenu.hidden = true;
2533
2534 sec.children.forEach(child => {
2535 const li = document.createElement('li');
2536 const a = document.createElement('a');
2537 a.href = `#${child.slug}`;
2538 a.textContent = child.title;
2539 a.dataset.slug = child.slug;
2540 a.addEventListener('click', e => {
2541 e.preventDefault();
2542 navigate(child.slug);
2543 closeAllSubmenus();
2544 closeMobileMenu();
2545 });
2546 li.appendChild(a);
2547 submenu.appendChild(li);
2548 });
2549
2550 btn.addEventListener('click', () => {
2551 const isOpen = btn.getAttribute('aria-expanded') === 'true';
2552 closeAllSubmenus();
2553 if (!isOpen) openSubmenu(btn, submenu);
2554 });
2555
2556 // Close on Escape
2557 wrapper.addEventListener('keydown', e => {
2558 if (e.key === 'Escape') closeAllSubmenus();
2559 });
2560
2561 wrapper.appendChild(btn);
2562 wrapper.appendChild(submenu);
2563 sectionLinks.appendChild(wrapper);
2564
2565 } else {
2566 // ── Regular link ──────────────────────────────────────────────────
2567 const a = document.createElement('a');
2568 a.href = `#${sec.slug}`;
2569 a.textContent = sec.title;
2570 a.dataset.slug = sec.slug;
2571 a.addEventListener('click', e => {
2572 e.preventDefault();
2573 closeAllSubmenus();
2574 navigate(sec.slug);
2575 closeMobileMenu();
2576 });
2577 sectionLinks.appendChild(a);
2578 }
2579 });
2580
2581 nav.appendChild(sectionLinks);
2582
2583 // Extra menu links (right side)
2584 if (config.menu_links && config.menu_links.length) {
2585 const extra = document.createElement('div');
2586 extra.className = 'nav-menu-links';
2587 config.menu_links.forEach(l => {
2588 const a = document.createElement('a');
2589 a.href = l.url;
2590 a.textContent = l.label;
2591 if (l.url.startsWith('http')) { a.target = '_blank'; a.rel = 'noopener noreferrer'; }
2592 extra.appendChild(a);
2593 });
2594 nav.appendChild(extra);
2595 }
2596
2597 // Close submenus when clicking outside nav
2598 document.addEventListener('click', e => {
2599 if (!nav.contains(e.target)) closeAllSubmenus();
2600 });
2601 }
2602
2603 function openSubmenu(btn, submenu) {
2604 submenu.hidden = false;
2605 btn.setAttribute('aria-expanded', 'true');
2606 // Focus first item for keyboard users
2607 const first = submenu.querySelector('a');
2608 if (first) first.focus();
2609 }
2610
2611 function closeAllSubmenus() {
2612 document.querySelectorAll('.nav-group-btn').forEach(b => {
2613 b.setAttribute('aria-expanded', 'false');
2614 const id = b.getAttribute('aria-controls');
2615 const sub = document.getElementById(id);
2616 if (sub) sub.hidden = true;
2617 });
2618 }
2619
2620 // =============================================
2621 // INIT
2622 // =============================================
2623 async function init() {
2624 const loadingText = document.getElementById('loading-text');
2625
2626 try {
2627 loadingText.textContent = 'Načítám konfiguraci…';
2628 config = await fetchJSON('./website.json');
2629
2630 // Load language
2631 const langFile = `./${config.language || 'cs'}.json`;
2632 loadingText.textContent = 'Načítám jazyk…';
2633 try { lang = await fetchJSON(langFile); } catch { lang = {}; }
2634
2635 applyConfig();
2636 applyLang();
2637
2638 // Load content
2639 loadingText.textContent = 'Načítám obsah…';
2640 const mdText = await fetchText('./website.md');
2641
2642 // Load sidebar (optional)
2643 try { sidebarMD = await fetchText('./sidebar.md'); } catch { sidebarMD = ''; }
2644
2645 // Parse sections — groups have no rawContent, skip parsing for them
2646 const rawSections = splitSections(mdText);
2647 sections = rawSections.map(sec => {
2648 if (sec.isGroup) return sec;
2649 if (sec.isArticles) {
2650 // Parse each article's metadata and content
2651 sec.articles = sec.articles.map(art => parseArticle(art));
2652 return sec;
2653 }
2654 // Extract news items before parsing MD (they have their own renderer)
2655 const { hasNews, newsItems, regularMD } = extractNewsItems(sec.rawContent);
2656 const parsedHTML = parseMD(regularMD);
2657 const headings = extractHeadings(parsedHTML);
2658 return { ...sec, parsedHTML, headings, hasNews, newsItems };
2659 });
2660
2661 buildNav();
2662 buildSearchIndex();
2663
2664 // Search events
2665 const searchInput = document.getElementById('search-input');
2666 const searchBtn = document.getElementById('search-btn');
2667 const searchClear = document.getElementById('search-clear');
2668
2669 const performSearch = () => {
2670 const q = searchInput.value.trim();
2671 if (!q) { clearSearch(); return; }
2672 searchClear.hidden = false;
2673 const results = doSearch(q);
2674 showSearch(results, q);
2675 };
2676
2677 searchInput.addEventListener('keydown', e => { if (e.key === 'Enter') performSearch(); });
2678 searchBtn.addEventListener('click', performSearch);
2679 searchClear.addEventListener('click', () => { clearSearch(); render(); });
2680
2681 // Mobile menu
2682 document.getElementById('menu-toggle').addEventListener('click', openMobileMenu);
2683 document.getElementById('sidebar-overlay').addEventListener('click', closeMobileMenu);
2684
2685 // Routing
2686 window.addEventListener('popstate', render);
2687
2688 // Hide loading
2689 document.getElementById('loading-screen').style.display = 'none';
2690
2691 // Initial render — outside main try/catch so file-load errors stay separate
2692 render();
2693
2694 } catch(err) {
2695 // Only reached if file loading failed, not render errors
2696 console.error(err);
2697 document.getElementById('loading-screen').style.display = 'none';
2698 const errScreen = document.getElementById('error-screen');
2699 errScreen.classList.add('active');
2700 document.getElementById('error-title').textContent = lang.error_title || 'Loading error';
2701 document.getElementById('error-msg').innerHTML =
2702 (lang.error_msg || 'Could not load site files. Make sure the files') +
2703 ' <code>website.json</code>, <code>website.md</code>' +
2704 (lang.error_server || ' are present and the site is served over HTTP (not opened as a local file).') +
2705 `<br><br><small>${escHtml(err.message)}</small>`;
2706 }
2707 }
2708
2709 return { init };
2710})();
2711
2712document.addEventListener('DOMContentLoaded', Engine.init);
2713</script>
2714</body>
2715</html>