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