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