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/* ===== NEWS ITEMS ===== */830.news-list { list-style: none; padding: 0; margin: 0; }831.news-item {832 margin-bottom: 2rem;833 padding-bottom: 2rem;834 border-bottom: 1px solid var(--color-border);835}836.news-item:last-child { border-bottom: none; margin-bottom: 0; padding-bottom: 0; }837.news-date-header {838 display: flex;839 align-items: baseline;840 gap: .6rem;841 margin-bottom: .6rem;842 flex-wrap: wrap;843}844.news-date-icon { font-size: 1rem; line-height: 1; flex-shrink: 0; }845.news-dow {846 font-family: var(--font-ui);847 font-size: .82rem;848 font-weight: 700;849 text-transform: uppercase;850 letter-spacing: .07em;851 color: var(--color-accent);852}853.news-date {854 font-family: var(--font-ui);855 font-size: .9rem;856 font-weight: 600;857 color: var(--color-text-muted);858}859.news-title {860 font-family: var(--font-heading);861 font-size: 1.2rem;862 font-weight: 700;863 color: var(--color-primary);864 margin-bottom: .4rem;865 line-height: 1.3;866}867.news-body { font-size: .97rem; }868.news-body p:last-child { margin-bottom: 0; }869.news-body p { margin-bottom: .6rem; }870
871/* News calendar widget */872.news-cal {873 font-family: var(--font-ui);874 font-size: .82rem;875 user-select: none;876}877.news-cal-nav {878 display: flex;879 align-items: center;880 justify-content: space-between;881 margin-bottom: .5rem;882}883.news-cal-nav button {884 background: none;885 border: 1px solid var(--color-border);886 border-radius: 3px;887 padding: .15rem .45rem;888 cursor: pointer;889 color: var(--color-text-muted);890 font-size: .8rem;891 line-height: 1.4;892 transition: background var(--transition), color var(--transition);893}894.news-cal-nav button:hover { background: var(--color-bg-alt); color: var(--color-text); }895.news-cal-nav button:focus-visible { outline: 2px solid var(--color-accent); outline-offset: 2px; }896.news-cal-month {897 font-weight: 700;898 font-size: .85rem;899 color: var(--color-primary);900 text-align: center;901 flex: 1;902}903.news-cal-grid {904 display: grid;905 grid-template-columns: repeat(7, 1fr);906 gap: 2px;907 text-align: center;908}909.news-cal-dow {910 color: var(--color-text-muted);911 font-size: .7rem;912 font-weight: 700;913 padding: .1rem 0 .3rem;914 text-transform: uppercase;915}916.news-cal-day {917 padding: .2rem .1rem;918 border-radius: 3px;919 line-height: 1.4;920 color: var(--color-text-muted);921}922.news-cal-day.empty { visibility: hidden; }923.news-cal-day.today {924 background: var(--color-bg-alt);925 color: var(--color-primary);926 font-weight: 700;927}928.news-cal-day.has-news {929 background: var(--color-primary);930 color: var(--color-text-inverse);931 font-weight: 700;932 cursor: pointer;933 border-radius: 3px;934 transition: background var(--transition);935}936.news-cal-day.has-news:hover { background: var(--color-accent); }937.news-cal-day.has-news:focus-visible { outline: 2px solid var(--color-accent); outline-offset: 2px; }938.news-cal-day.has-news.today { background: var(--color-accent); }939
940/* ===== FOOTER ===== */941#site-footer {942 background: var(--color-primary-dark);943 color: rgba(255,255,255,.7);944 padding: 1.5rem 2rem;945 display: flex;946 align-items: center;947 flex-wrap: wrap;948 gap: 1rem;949 font-family: var(--font-ui);950 font-size: .85rem;951}952#site-footer a { color: rgba(255,255,255,.85); text-decoration: none; transition: color var(--transition); }953#site-footer a:hover { color: #fff; text-decoration: underline; }954.footer-copyright { flex: 1; }955.footer-links { display: flex; gap: 1rem; flex-wrap: wrap; }956.footer-links a {957 padding: .2rem .5rem;958 border-radius: 3px;959 transition: background var(--transition);960}961.footer-links a:hover { background: rgba(255,255,255,.1); }962
963/* ===== LOADING / ERROR ===== */964#loading-screen {965 position: fixed; inset: 0;966 background: var(--color-bg);967 display: flex;968 align-items: center;969 justify-content: center;970 z-index: 9999;971 flex-direction: column;972 gap: 1rem;973}974.loading-spinner {975 width: 40px; height: 40px;976 border: 3px solid var(--color-border);977 border-top-color: var(--color-primary);978 border-radius: 50%;979 animation: spin .8s linear infinite;980}981@keyframes spin { to { transform: rotate(360deg); } }982.loading-text {983 font-family: var(--font-ui);984 color: var(--color-text-muted);985 font-size: .9rem;986}987#error-screen {988 display: none;989 position: fixed; inset: 0;990 background: var(--color-bg);991 align-items: center;992 justify-content: center;993 z-index: 9999;994 flex-direction: column;995 gap: 1rem;996 padding: 2rem;997 text-align: center;998}999#error-screen.active { display: flex; }1000.error-icon { font-size: 3rem; }1001.error-title { font-family: var(--font-heading); font-size: 1.5rem; color: var(--color-primary); }1002.error-msg { font-family: var(--font-ui); color: var(--color-text-muted); max-width: 500px; line-height: 1.6; }1003.error-msg code { font-family: var(--font-mono); font-size: .9em; background: var(--color-bg-alt); padding: .1em .4em; border-radius: 3px; }1004
1005/* ===== SIDEBAR MOBILE TOGGLE ===== */1006/* Overlay a toggle tlačítko se na mobilu nepoužívají — sidebar je pod obsahem */1007#sidebar-overlay { display: none; }1008
1009/* ===== RESPONSIVE ===== */1010@media (max-width: 900px) {1011 #main-wrapper { flex-direction: column; padding: 1rem; gap: 1rem; }1012 #menu-toggle { display: none; }1013
1014 /* Sidebar: statický, plná šířka, pod obsahem */1015 #sidebar {1016 position: static;1017 width: 100%;1018 max-height: none;1019 overflow-y: visible;1020 }1021 /* Na mobilu zobrazit sekce sidebaru vedle sebe (grid), nebo pod sebou */1022 .sidebar-inner {1023 display: grid;1024 grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));1025 gap: .75rem;1026 }1027 /* Každá sidebar sekce jako samostatná karta */1028 .sidebar-section {1029 margin-bottom: 0;1030 }1031
1032 #page-content { padding: 1.5rem 1.25rem; }1033 #site-header { padding: 1.25rem 1.25rem 0; }1034 #site-nav { margin: 0 -1.25rem; padding-left: 1.25rem; padding-right: 1.25rem; }1035}1036@media (max-width: 560px) {1037 #site-title { font-size: 1.5rem; }1038 #page-content { padding: 1rem .875rem; }1039 #main-wrapper { padding: .75rem; }1040 #search-box { max-width: none; }1041 /* Na malých displejích grid padne na jeden sloupec */1042 .sidebar-inner { grid-template-columns: 1fr; }1043}1044
1045/* ===== PRINT ===== */1046@media print {1047 #site-nav, #toolbar, #sidebar, #site-footer { display: none; }1048 #main-wrapper { padding: 0; }1049 #page-content { box-shadow: none; border: none; }1050}1051
1052/* ===== FOCUS VISIBLE ===== */1053:focus-visible {1054 outline: 3px solid var(--color-accent);1055 outline-offset: 2px;1056 border-radius: 2px;1057}1058</style>1059<link rel="preconnect" href="https://fonts.googleapis.com">1060<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>1061<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">1062</head>1063<body>1064
1065<a href="#main-content" class="skip-link" id="skip-link">Přeskočit na obsah</a>1066
1067<div id="loading-screen" aria-live="polite" aria-busy="true">1068 <div class="loading-spinner" role="presentation"></div>1069 <p class="loading-text" id="loading-text">Načítám…</p>1070</div>1071
1072<div id="error-screen" role="alert" aria-live="assertive">1073 <div class="error-icon" aria-hidden="true">⚠️</div>1074 <h1 class="error-title" id="error-title">Chyba načítání</h1>1075 <p class="error-msg" id="error-msg"></p>1076</div>1077
1078<header id="site-header" role="banner">1079 <div class="site-branding">1080 <h2 id="site-title"></h2>1081 <p id="site-tagline"></p>1082 </div>1083 <nav id="site-nav" role="navigation" aria-label="Hlavní navigace">1084 <!-- filled by JS -->1085 </nav>1086</header>1087
1088<div id="toolbar" role="search">1089 <button id="menu-toggle" aria-expanded="false" aria-controls="sidebar" aria-label="Otevřít postranní panel">☰ Menu</button>1090 <div id="search-box">1091 <label for="search-input" class="sr-only" id="search-label">Hledat</label>1092 <input type="search" id="search-input" autocomplete="off" aria-label="Hledat na webu">1093 <button id="search-btn" type="button">🔍 <span id="search-btn-label">Hledat</span></button>1094 <button id="search-clear" type="button" hidden id="search-clear-btn"><span id="search-clear-label">Zrušit</span></button>1095 </div>1096</div>1097
1098<div id="sidebar-overlay" aria-hidden="true"></div>1099
1100<div id="main-wrapper">1101 <aside id="sidebar" role="complementary" aria-label="Postranní panel">1102 <div class="sidebar-inner">1103 <div id="sidebar-custom" class="sidebar-section" hidden>1104 <div id="sidebar-content"></div>1105 </div>1106 <nav class="sidebar-section" aria-label="Obsah stránky" id="toc-nav">1107 <h2 class="sidebar-heading" id="toc-heading">Obsah</h2>1108 <ul id="toc-list"></ul>1109 </nav>1110 <nav class="sidebar-section" id="ext-links-nav" hidden aria-label="Externí odkazy na stránce">1111 <h2 class="sidebar-heading" id="ext-links-heading">Odkazy</h2>1112 <ul id="ext-links-list"></ul>1113 </nav>1114 <nav class="sidebar-section" id="comments-nav" hidden aria-label="Komentáře v textu">1115 <h2 class="sidebar-heading" id="comments-heading">Komentáře</h2>1116 <ol id="comments-list"></ol>1117 </nav>1118 <div class="sidebar-section" id="news-calendar-nav" hidden>1119 <h2 class="sidebar-heading" id="news-calendar-heading">Kalendář</h2>1120 <div id="news-calendar"></div>1121 </div>1122 </div>1123 </aside>1124
1125 <main id="main-content" tabindex="-1">1126 <div id="search-results" role="region" aria-live="polite" aria-label="Výsledky hledání">1127 <h2 id="search-results-title"></h2>1128 <div id="search-results-list"></div>1129 </div>1130 <article id="page-content" aria-live="polite">1131 <!-- filled by JS -->1132 </article>1133 </main>1134</div>1135
1136<footer id="site-footer" role="contentinfo">1137 <p class="footer-copyright" id="footer-copyright"></p>1138 <nav class="footer-links" id="footer-links" aria-label="Patičkové odkazy"></nav>1139</footer>1140
1141<script>1142// =============================================1143// WEBSITE ENGINE v1.01144// Single-file static site renderer1145// =============================================1146
1147const Engine = (() => {1148
1149 // --- State ---1150 let config = {};1151 let lang = {};1152 let sections = []; // [{id, title, slug, content, headings}]1153 let sidebarMD = '';1154 let searchIndex = [];1155 let currentSection = null;1156
1157 // --- Fetch helpers ---1158 async function fetchText(url) {1159 const r = await fetch(url);1160 if (!r.ok) throw new Error(`HTTP ${r.status}: ${url}`);1161 const text = await r.text();1162 return text.replace(/\r\n/g, '\n').replace(/\r/g, '\n');1163 }1164 async function fetchJSON(url) {1165 const r = await fetch(url);1166 if (!r.ok) throw new Error(`HTTP ${r.status}: ${url}`);1167 return r.json();1168 }1169
1170 // --- Slug ---1171 function slugify(text) {1172 return text.toLowerCase()1173 .normalize('NFD').replace(/[\u0300-\u036f]/g, '')1174 .replace(/[^a-z0-9]+/g, '-')1175 .replace(/^-|-$/g, '');1176 }1177
1178 // =============================================1179 // MARKDOWN PARSER1180 // =============================================1181 function parseMD(md, basePath) {1182 let html = md;1183
1184 // Escape HTML entities in code blocks first1185 const codeBlocks = [];1186 html = html.replace(/```([\s\S]*?)```/g, (_, code) => {1187 const idx = codeBlocks.push(`<pre><code>${escHtml(code.replace(/^\n/, ''))}</code></pre>`) - 1;1188 return `\x00CODE${idx}\x00`;1189 });1190 const inlineCodes = [];1191 html = html.replace(/`([^`]+)`/g, (_, code) => {1192 const idx = inlineCodes.push(`<code>${escHtml(code)}</code>`) - 1;1193 return `\x00ICODE${idx}\x00`;1194 });1195
1196 // CriticMarkup — order matters1197 // {++ insertion ++}1198 html = html.replace(/\{\+\+([\s\S]*?)\+\+\}/g, (_, t) =>1199 `<ins class="critic" aria-label="${lang.critic_ins||'Vloženo'}: ${escAttr(t)}">${t}</ins>`);1200 // {-- deletion --}1201 html = html.replace(/\{--([\s\S]*?)--\}/g, (_, t) =>1202 `<del class="critic" aria-label="${lang.critic_del||'Smazáno'}: ${escAttr(t)}">${t}</del>`);1203 // {~~ old ~> new ~~}1204 html = html.replace(/\{~~([\s\S]*?)~>([\s\S]*?)~~\}/g, (_, o, n) =>1205 `<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>`);1206 // {== highlight ==}1207 html = html.replace(/\{==([\s\S]*?)==\}/g, (_, t) =>1208 `<mark class="critic" aria-label="${lang.critic_mark||'Zvýrazněno'}: ${escAttr(t)}">${t}</mark>`);1209 // {>> comment <<}1210 html = html.replace(/\{>>([\s\S]*?)<<\}/g, (_, t) =>1211 `<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>`);1212
1213 // Tables1214 html = html.replace(/^(\|.+\|\n)([ \t]*\|[ \t]*[-:]+[ \t]*\|.+\n)((?:\|.+\|\n?)*)/gm,1215 (_, head, sep, body) => {1216 const alignments = sep.split('|').filter(c => c.trim())1217 .map(c => { c = c.trim(); return c.endsWith(':') && c.startsWith(':') ? 'center' : c.endsWith(':') ? 'right' : 'left'; });1218 const ths = head.split('|').filter(c => c.trim() !== '').map((c, i) =>1219 `<th style="text-align:${alignments[i]||'left'}">${c.trim()}</th>`).join('');1220 const rows = body.trim().split('\n').filter(Boolean).map(row => {1221 const tds = row.split('|').filter(c => c.trim() !== '').map((c, i) =>1222 `<td style="text-align:${alignments[i]||'left'}">${parseMDInline(c.trim())}</td>`).join('');1223 return `<tr>${tds}</tr>`;1224 }).join('');1225 return `<table><thead><tr>${ths}</tr></thead><tbody>${rows}</tbody></table>`;1226 });1227
1228 // Headings (h1–h6)1229 html = html.replace(/^(#{1,6})\s+(.+)$/gm, (_, hashes, text) => {1230 const level = hashes.length;1231 const id = slugify(text);1232 return `<h${level} id="${id}">${parseMDInline(text)}</h${level}>`;1233 });1234
1235 // Horizontal rules1236 html = html.replace(/^[-*_]{3,}\s*$/gm, '<hr>');1237
1238 // Blockquotes1239 html = html.replace(/^(>+)\s?(.*)$/gm, (_, marks, text) => `<blockquote><p>${parseMDInline(text)}</p></blockquote>`);1240
1241 // Unordered lists1242 html = html.replace(/(^[-*+] .+\n?)+/gm, match => {1243 const items = match.trim().split('\n').map(l => `<li>${parseMDInline(l.replace(/^[-*+] /, '').trim())}</li>`).join('');1244 return `<ul>${items}</ul>`;1245 });1246 // Ordered lists1247 html = html.replace(/(^\d+\. .+\n?)+/gm, match => {1248 const items = match.trim().split('\n').map(l => `<li>${parseMDInline(l.replace(/^\d+\. /, '').trim())}</li>`).join('');1249 return `<ol>${items}</ol>`;1250 });1251
1252 // Paragraphs (lines not starting with block elements)1253 const blockTags = ['<h','<ul','<ol','<li','<blockquote','<table','<thead','<tbody','<tr','<td','<th','<pre','<hr','<ins','<del','<mark'];1254 const lines = html.split('\n');1255 const result = [];1256 let pBuf = [];1257 const flushP = () => {1258 if (pBuf.length) { result.push(`<p>${pBuf.join(' ')}</p>`); pBuf = []; }1259 };1260 for (const line of lines) {1261 const trimmed = line.trim();1262 if (!trimmed || trimmed.startsWith('\x00CODE') || blockTags.some(t => trimmed.startsWith(t)) || trimmed.endsWith('>')) {1263 flushP();1264 if (trimmed) result.push(trimmed);1265 } else {1266 pBuf.push(parseMDInline(trimmed));1267 }1268 }1269 flushP();1270 html = result.join('\n');1271
1272 // Restore code1273 html = html.replace(/\x00CODE(\d+)\x00/g, (_, i) => codeBlocks[i]);1274 html = html.replace(/\x00ICODE(\d+)\x00/g, (_, i) => inlineCodes[i]);1275
1276 return html;1277 }1278
1279 function parseMDInline(text) {1280 // Images before links1281 text = text.replace(/!\[([^\]]*)\]\(([^)]+)\)/g, (_, alt, src) =>1282 `<img src="${escAttr(src)}" alt="${escAttr(alt)}" loading="lazy">`);1283 // Internal links [text] (no URL)1284 text = text.replace(/\[([^\]]+)\](?!\()/g, (_, title) => {1285 const slug = slugify(title);1286 return `<a href="#${slug}" class="internal-link" data-target="${escAttr(title)}">${title}</a>`;1287 });1288 // External links [text](url)1289 text = text.replace(/\[([^\]]+)\]\(([^)]+)\)/g, (_, t, url) =>1290 `<a href="${escAttr(url)}"${url.startsWith('http') ? ' target="_blank" rel="noopener noreferrer"' : ''}>${t}</a>`);1291 // Bold+italic1292 text = text.replace(/\*\*\*([^*]+)\*\*\*/g, '<strong><em>$1</em></strong>');1293 // Bold1294 text = text.replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>');1295 text = text.replace(/__([^_]+)__/g, '<strong>$1</strong>');1296 // Italic1297 text = text.replace(/\*([^*]+)\*/g, '<em>$1</em>');1298 text = text.replace(/_([^_]+)_/g, '<em>$1</em>');1299 // Auto-linkify bare URLs not already inside a tag or attribute1300 text = text.replace(/(?<![="'>])(https?:\/\/[^\s<>"')\]]+)/g, url =>1301 `<a href="${escAttr(url)}" target="_blank" rel="noopener noreferrer">${escHtml(url)}</a>`);1302 // Strikethrough1303 text = text.replace(/~~([^~]+)~~/g, '<del>$1</del>');1304 return text;1305 }1306
1307 function escHtml(s) {1308 return s.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"');1309 }1310 function escAttr(s) {1311 return s.replace(/"/g,'"').replace(/'/g,''');1312 }1313
1314 // =============================================1315 // SECTION SPLITTING1316 // =============================================1317 // A section whose content begins with a line containing only "multisection"1318 // (case-insensitive) is a multisection group: its H2s become sub-pages and1319 // it acts as a nav group rather than a navigable page itself.1320 function splitSections(md) {1321 const lines = md.split('\n');1322 const rawH1s = [];1323 let current = null;1324
1325 for (const line of lines) {1326 const h1 = line.match(/^#(?!#)\s+(.+)$/);1327 if (h1) {1328 if (current) rawH1s.push(current);1329 const title = h1[1].trim();1330 current = { title, slug: slugify(title), rawContent: '' };1331 } else if (current) {1332 current.rawContent += line + '\n';1333 }1334 }1335 if (current) rawH1s.push(current);1336
1337 // Detect multisection: first non-empty line of rawContent is "multisection"1338 const result = [];1339 for (const sec of rawH1s) {1340 const firstLine = sec.rawContent.split('\n').find(l => l.trim() !== '');1341 if (firstLine && firstLine.trim().toLowerCase() === 'multisection') {1342 // Strip the "multisection" keyword line from content1343 const bodyAfterKeyword = sec.rawContent.replace(/^[\s\S]*?multisection[ \t]*\n/i, '');1344 // Split into sub-sections by H21345 const subLines = bodyAfterKeyword.split('\n');1346 const subSections = [];1347 let curSub = null;1348 for (const line of subLines) {1349 const h2 = line.match(/^##\s+(.+)$/);1350 if (h2) {1351 if (curSub) subSections.push(curSub);1352 const subTitle = h2[1].trim();1353 curSub = { title: subTitle, slug: slugify(subTitle), rawContent: '', parentSlug: sec.slug, parentTitle: sec.title };1354 } else if (curSub) {1355 curSub.rawContent += line + '\n';1356 }1357 }1358 if (curSub) subSections.push(curSub);1359
1360 // Push a group placeholder (not a real page)1361 result.push({1362 title: sec.title,1363 slug: sec.slug,1364 isGroup: true,1365 children: subSections1366 });1367 // Push each sub-section as a top-level navigable section1368 subSections.forEach(sub => result.push(sub));1369 } else {1370 result.push(sec);1371 }1372 }1373 return result;1374 }1375
1376 // =============================================1377 // TOC EXTRACTION1378 // =============================================1379 function extractHeadings(html) {1380 const headings = [];1381 const re = /<h([234])[^>]*id="([^"]*)"[^>]*>([\s\S]*?)<\/h\1>/gi;1382 let m;1383 while ((m = re.exec(html)) !== null) {1384 headings.push({ level: parseInt(m[1]), id: m[2], text: m[3].replace(/<[^>]+>/g,'') });1385 }1386 return headings;1387 }1388
1389 // =============================================1390 // SEARCH INDEX1391 // =============================================1392 function buildSearchIndex() {1393 searchIndex = sections1394 .filter(sec => !sec.isGroup)1395 .map(sec => ({1396 slug: sec.slug,1397 title: sec.title,1398 text: (sec.rawContent || '').replace(/[#*_`\[\](){}|>]/g,'').toLowerCase(),1399 rawContent: sec.rawContent || ''1400 }));1401 }1402
1403 function doSearch(query) {1404 const q = query.toLowerCase().normalize('NFD').replace(/[\u0300-\u036f]/g,'');1405 const words = q.split(/\s+/).filter(Boolean);1406 const results = [];1407
1408 for (const entry of searchIndex) {1409 const haystack = (entry.title + ' ' + entry.text).normalize('NFD').replace(/[\u0300-\u036f]/g,'');1410 const score = words.reduce((s, w) => {1411 const inTitle = entry.title.toLowerCase().normalize('NFD').replace(/[\u0300-\u036f]/g,'').includes(w) ? 3 : 0;1412 const inBody = (haystack.match(new RegExp(w.replace(/[.*+?^${}()|[\]\\]/g,'\\$&'), 'g')) || []).length;1413 return s + inTitle + inBody;1414 }, 0);1415 if (score > 0) {1416 // Find excerpt1417 let excerpt = entry.text;1418 const firstWord = words[0];1419 const idx = excerpt.normalize('NFD').replace(/[\u0300-\u036f]/g,'').indexOf(firstWord);1420 if (idx > -1) {1421 const start = Math.max(0, idx - 80);1422 excerpt = (start > 0 ? '…' : '') + excerpt.slice(start, idx + 200) + '…';1423 } else {1424 excerpt = excerpt.slice(0, 200) + '…';1425 }1426 // Highlight1427 let highlighted = escHtml(excerpt);1428 words.forEach(w => {1429 highlighted = highlighted.replace(new RegExp(w.replace(/[.*+?^${}()|[\]\\]/g,'\\$&'), 'gi'), m => `<mark>${m}</mark>`);1430 });1431 results.push({ entry, score, excerpt: highlighted });1432 }1433 }1434 return results.sort((a,b) => b.score - a.score);1435 }1436
1437 // =============================================1438 // ROUTING1439 // =============================================1440 function getHashParts() {1441 const hash = location.hash.replace(/^#/, '');1442 if (!hash) return { section: null, anchor: null };1443 // Exact section slug match — skip group entries (they have no page)1444 for (const sec of sections) {1445 if (!sec.isGroup && hash === sec.slug) return { section: sec, anchor: null };1446 }1447 // Exact heading id match within any non-group section1448 for (const sec of sections) {1449 if (sec.headings && sec.headings.find(h => h.id === hash)) {1450 return { section: sec, anchor: hash };1451 }1452 }1453 return { section: null, anchor: null };1454 }1455
1456 function navigate(hash, replace = false) {1457 if (replace) {1458 history.replaceState(null, '', hash ? `#${hash}` : location.pathname);1459 } else {1460 history.pushState(null, '', hash ? `#${hash}` : location.pathname);1461 }1462 render();1463 }1464
1465 // =============================================1466 // RENDER1467 // =============================================1468 function render() {1469 // Hide search if active1470 hideSearch();1471
1472 const { section, anchor } = getHashParts();1473 // If resolved section is a group (no parsedHTML), navigate to first child1474 let sec = section || sections.find(s => !s.isGroup);1475 if (sec && sec.isGroup) {1476 sec = sec.children && sec.children.length ? sec.children[0] : sections.find(s => !s.isGroup);1477 }1478 if (!sec) return;1479 currentSection = sec;1480
1481 // Update page title1482 document.title = `${sec.title} | ${config.title || 'Web'}`;1483
1484 // Update nav active state1485 document.querySelectorAll('#site-nav a[data-slug]').forEach(a => {1486 const active = a.dataset.slug === sec.slug;1487 a.setAttribute('aria-current', active ? 'page' : 'false');1488 });1489 // Auto-expand parent group if a child page is active1490 document.querySelectorAll('.nav-group').forEach(group => {1491 const hasActive = group.querySelector(`a[aria-current="page"]`);1492 const btn = group.querySelector('.nav-group-btn');1493 const submenuId = btn && btn.getAttribute('aria-controls');1494 const submenu = submenuId && document.getElementById(submenuId);1495 if (btn && submenu) {1496 if (hasActive) {1497 submenu.hidden = false;1498 btn.setAttribute('aria-expanded', 'true');1499 }1500 }1501 });1502
1503 // Render content1504 const html = sec.parsedHTML;1505 const pageContent = document.getElementById('page-content');1506 pageContent.innerHTML = `<h1 class="page-title">${escHtml(sec.title)}</h1>${html}`;1507
1508 // Resolve and validate internal links — broken ones become <span>1509 processInternalLinks(pageContent);1510
1511 // Build TOC1512 buildTOC(sec.headings);1513
1514 // Render news items into page (before TOC/links — must happen after innerHTML set)1515 if (sec.hasNews && sec.newsItems && sec.newsItems.length) {1516 renderNewsItems(sec.newsItems, pageContent);1517 }1518
1519 // Build external links sidebar section1520 buildExtLinks(sec.parsedHTML);1521
1522 // Build critic comments sidebar section (after DOM is rendered)1523 buildComments(pageContent);1524
1525 // Build news calendar sidebar section1526 buildNewsCalendar(sec.hasNews ? sec.newsItems : null);1527
1528 // Sidebar custom content1529 if (sidebarMD) {1530 const sidebarEl = document.getElementById('sidebar-content');1531 sidebarEl.innerHTML = parseMD(sidebarMD);1532 processInternalLinks(sidebarEl);1533 document.getElementById('sidebar-custom').hidden = false;1534 }1535
1536 // Scroll / anchor1537 requestAnimationFrame(() => {1538 if (anchor) {1539 const el = document.getElementById(anchor);1540 if (el) { el.scrollIntoView({ behavior: 'smooth', block: 'start' }); return; }1541 }1542 window.scrollTo({ top: 0, behavior: 'smooth' });1543 });1544
1545 // Update TOC active state on scroll1546 setupScrollSpy();1547 }1548
1549 // Returns the hash to navigate to, or null if no target exists1550 function resolveInternalTarget(target) {1551 const targetSlug = slugify(target);1552
1553 // 1) Exact section match (not a group)1554 if (sections.find(s => !s.isGroup && s.slug === targetSlug)) {1555 return targetSlug;1556 }1557
1558 // 2) Exact heading match in any section1559 for (const s of sections) {1560 if (!s.headings) continue;1561 const hit = s.headings.find(h =>1562 h.id === targetSlug || slugify(h.text) === targetSlug1563 );1564 if (hit) return hit.id;1565 }1566
1567 return null;1568 }1569
1570 // Walk all .internal-link elements in a container:1571 // - if target exists → attach click handler1572 // - if target doesn't exist → replace <a> with <span class="broken-link">1573 function processInternalLinks(container) {1574 container.querySelectorAll('.internal-link').forEach(a => {1575 const target = a.dataset.target;1576 const resolved = resolveInternalTarget(target);1577
1578 if (resolved === null) {1579 // Replace with plain text including the original brackets1580 const text = document.createTextNode('[' + a.textContent + ']');1581 a.replaceWith(text);1582 } else {1583 a.href = `#${resolved}`;1584 a.addEventListener('click', e => {1585 e.preventDefault();1586 navigate(resolved);1587 closeMobileMenu();1588 });1589 }1590 });1591 }1592
1593 // =============================================1594 // NEWS ITEMS PARSER & RENDERER1595 // =============================================1596
1597 // Supported date formats:1598 // YYYY-MM-DD 2026-05-181599 // DD.MM.YYYY 18.05.2026 or 18.5.20261600 // MM/DD/YYYY 5/18/2026 or 05/18/20261601 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*/;1602
1603 function parseNewsDate(line) {1604 const m = line.match(NEWS_DATE_RE);1605 if (!m) return null;1606 let y, mo, d;1607 if (m[1]) { y = +m[1]; mo = +m[2]; d = +m[3]; } // YYYY-MM-DD1608 else if (m[4]) { d = +m[4]; mo = +m[5]; y = +m[6]; } // DD.MM.YYYY1609 else { mo = +m[7]; d = +m[8]; y = +m[9]; } // MM/DD/YYYY1610 const date = new Date(y, mo - 1, d);1611 if (isNaN(date.getTime())) return null;1612 return { date, isoKey: `${y}-${String(mo).padStart(2,'0')}-${String(d).padStart(2,'0')}`,1613 rest: line.slice(m[0].length).trim() };1614 }1615
1616 function formatNewsDate(date) {1617 const l = config.language || 'cs';1618 const months = lang.months1619 ? lang.months1620 : (l === 'cs'1621 ? ['ledna','února','března','dubna','května','června','července','srpna','září','října','listopadu','prosince']1622 : ['January','February','March','April','May','June','July','August','September','October','November','December']);1623 const dows = lang.days_long1624 ? lang.days_long1625 : (l === 'cs'1626 ? ['neděle','pondělí','úterý','středa','čtvrtek','pátek','sobota']1627 : ['Sunday','Monday','Tuesday','Wednesday','Thursday','Friday','Saturday']);1628 const dow = dows[date.getDay()];1629 const day = date.getDate();1630 const month = months[date.getMonth()];1631 const year = date.getFullYear();1632 const dateStr = l === 'cs' ? `${day}. ${month} ${year}` : `${month} ${day}, ${year}`;1633 return { dow, dateStr };1634 }1635
1636 // Parse rawContent and split into news items + regular MD1637 // Returns { hasNews, newsItems, regularMD }1638 // newsItems: [{isoKey, date, title, bodyMD}]1639 function extractNewsItems(rawContent) {1640 const lines = rawContent.split('1641');1642 const newsItems = [];1643 const regularLines = [];1644 let i = 0;1645
1646 while (i < lines.length) {1647 const line = lines[i];1648 const parsed = parseNewsDate(line.trim());1649
1650 if (parsed) {1651 // First line after date prefix is the title (inline rest of line)1652 // Subsequent indented/non-empty lines before next date entry are the body1653 const titleFirstLine = parsed.rest;1654 const bodyLines = [];1655
1656 i++;1657 while (i < lines.length) {1658 const nextLine = lines[i];1659 // Stop if next line starts a new news item1660 if (parseNewsDate(nextLine.trim())) break;1661 bodyLines.push(nextLine);1662 i++;1663 }1664
1665 // Trim trailing empty lines from body1666 while (bodyLines.length && !bodyLines[bodyLines.length - 1].trim()) bodyLines.pop();1667
1668 newsItems.push({1669 isoKey: parsed.isoKey,1670 date: parsed.date,1671 title: titleFirstLine,1672 bodyMD: bodyLines.join('1673')1674 });1675 } else {1676 regularLines.push(line);1677 i++;1678 }1679 }1680
1681 return {1682 hasNews: newsItems.length > 0,1683 newsItems: newsItems.sort((a, b) => b.date - a.date), // newest first1684 regularMD: regularLines.join('1685')1686 };1687 }1688
1689 // Render news items into page content el, inserting before first h2 or at end1690 function renderNewsItems(newsItems, pageContentEl) {1691 if (!newsItems.length) return;1692
1693 const ul = document.createElement('ul');1694 ul.className = 'news-list';1695 ul.setAttribute('aria-label', lang.news_list || 'Novinky');1696
1697 newsItems.forEach(item => {1698 const { dow, dateStr } = formatNewsDate(item.date);1699 const li = document.createElement('li');1700 li.className = 'news-item';1701 li.id = `news-${item.isoKey}`;1702
1703 // Date header1704 const header = document.createElement('div');1705 header.className = 'news-date-header';1706 header.innerHTML =1707 `<span class="news-date-icon" aria-hidden="true">📅</span>` +1708 `<span class="news-dow">${escHtml(dow)}</span>` +1709 `<span class="news-date">${escHtml(dateStr)}</span>`;1710
1711 li.appendChild(header);1712
1713 // Title (if present)1714 if (item.title) {1715 const titleEl = document.createElement('div');1716 titleEl.className = 'news-title';1717 titleEl.innerHTML = parseMDInline(item.title);1718 li.appendChild(titleEl);1719 }1720
1721 // Body (if present) — full markdown1722 if (item.bodyMD.trim()) {1723 const bodyEl = document.createElement('div');1724 bodyEl.className = 'news-body';1725 bodyEl.innerHTML = parseMD(item.bodyMD);1726 li.appendChild(bodyEl);1727 }1728
1729 ul.appendChild(li);1730 });1731
1732 // Insert before first h2 or append1733 const firstH2 = pageContentEl.querySelector('h2');1734 if (firstH2) {1735 pageContentEl.insertBefore(ul, firstH2);1736 } else {1737 pageContentEl.appendChild(ul);1738 }1739
1740 // Activate internal links within news items1741 processInternalLinks(ul);1742 }1743
1744 // Build calendar widget in sidebar1745 function buildNewsCalendar(newsItems) {1746 const nav = document.getElementById('news-calendar-nav');1747 const calEl = document.getElementById('news-calendar');1748
1749 if (!newsItems || !newsItems.length) {1750 nav.hidden = true;1751 return;1752 }1753 nav.hidden = false;1754 document.getElementById('news-calendar-heading').textContent =1755 (lang.calendar_icon || '📅') + ' ' + (lang.calendar || 'Kalendář');1756
1757 // Build a set of ISO keys for fast lookup1758 const newsDays = new Set(newsItems.map(n => n.isoKey));1759
1760 const today = new Date();1761 // Start with month of most recent news item1762 let viewYear = newsItems[0].date.getFullYear();1763 let viewMonth = newsItems[0].date.getMonth(); // 0-based1764
1765 function renderCalendar() {1766 calEl.innerHTML = '';1767 const l = config.language || 'cs';1768 const monthNames = lang.months_short1769 ? lang.months_short1770 : (l === 'cs'1771 ? ['Led','Ún','Bře','Dub','Kvě','Čvn','Čvc','Srp','Zář','Říj','Lis','Pro']1772 : ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec']);1773 const dowShort = lang.days_short1774 ? lang.days_short1775 : (l === 'cs'1776 ? ['Po','Út','St','Čt','Pá','So','Ne']1777 : ['Mo','Tu','We','Th','Fr','Sa','Su']);1778
1779 const cal = document.createElement('div');1780 cal.className = 'news-cal';1781
1782 // Nav row1783 const nav = document.createElement('div');1784 nav.className = 'news-cal-nav';1785 const prevBtn = document.createElement('button');1786 prevBtn.textContent = '‹';1787 prevBtn.type = 'button';1788 prevBtn.setAttribute('aria-label', lang.cal_prev || 'Předchozí měsíc');1789 prevBtn.addEventListener('click', () => { viewMonth--; if (viewMonth < 0) { viewMonth = 11; viewYear--; } renderCalendar(); });1790 const nextBtn = document.createElement('button');1791 nextBtn.textContent = '›';1792 nextBtn.type = 'button';1793 nextBtn.setAttribute('aria-label', lang.cal_next || 'Následující měsíc');1794 nextBtn.addEventListener('click', () => { viewMonth++; if (viewMonth > 11) { viewMonth = 0; viewYear++; } renderCalendar(); });1795 const monthLabel = document.createElement('div');1796 monthLabel.className = 'news-cal-month';1797 monthLabel.textContent = `${monthNames[viewMonth]} ${viewYear}`;1798 nav.appendChild(prevBtn); nav.appendChild(monthLabel); nav.appendChild(nextBtn);1799 cal.appendChild(nav);1800
1801 // Grid1802 const grid = document.createElement('div');1803 grid.className = 'news-cal-grid';1804
1805 // Day-of-week headers (Mon-first for CS, Mon-first for EN too — ISO week)1806 dowShort.forEach(d => {1807 const cell = document.createElement('div');1808 cell.className = 'news-cal-dow';1809 cell.textContent = d;1810 cell.setAttribute('aria-hidden', 'true');1811 grid.appendChild(cell);1812 });1813
1814 // First day of month, adjust to Mon=0 week start1815 const firstDay = new Date(viewYear, viewMonth, 1);1816 // getDay(): 0=Sun,1=Mon..6=Sat → convert to Mon=01817 const startOffset = (firstDay.getDay() + 6) % 7;1818 const daysInMonth = new Date(viewYear, viewMonth + 1, 0).getDate();1819
1820 // Empty cells before first day1821 for (let i = 0; i < startOffset; i++) {1822 const cell = document.createElement('div');1823 cell.className = 'news-cal-day empty';1824 cell.setAttribute('aria-hidden', 'true');1825 grid.appendChild(cell);1826 }1827
1828 // Day cells1829 for (let d = 1; d <= daysInMonth; d++) {1830 const isoKey = `${viewYear}-${String(viewMonth+1).padStart(2,'0')}-${String(d).padStart(2,'0')}`;1831 const isToday = today.getFullYear() === viewYear && today.getMonth() === viewMonth && today.getDate() === d;1832 const hasNews = newsDays.has(isoKey);1833
1834 const cell = document.createElement(hasNews ? 'button' : 'div');1835 cell.className = 'news-cal-day' + (isToday ? ' today' : '') + (hasNews ? ' has-news' : '');1836 cell.textContent = d;1837
1838 if (hasNews) {1839 cell.type = 'button';1840 cell.setAttribute('aria-label', `${lang.go_to_news || 'Přejít na novinku'} ${isoKey}`);1841 cell.addEventListener('click', () => {1842 const target = document.getElementById(`news-${isoKey}`);1843 if (target) { target.scrollIntoView({ behavior: 'smooth', block: 'start' }); target.focus(); }1844 });1845 } else {1846 cell.setAttribute('aria-hidden', 'true');1847 }1848 grid.appendChild(cell);1849 }1850
1851 cal.appendChild(grid);1852 calEl.appendChild(cal);1853 }1854
1855 renderCalendar();1856 }1857
1858 function buildTOC(headings) {1859 const root = document.getElementById('toc-list');1860 root.innerHTML = '';1861 if (!headings || !headings.length) {1862 document.getElementById('toc-nav').hidden = true;1863 return;1864 }1865 document.getElementById('toc-nav').hidden = false;1866
1867 // Build a proper nested tree: h2 = top level, h3 under last h2, h4 under last h31868 // We keep a stack of {level, ul} pairs1869 const makeLink = h => {1870 const a = document.createElement('a');1871 a.href = `#${h.id}`;1872 a.textContent = h.text;1873 a.addEventListener('click', e => {1874 e.preventDefault();1875 history.pushState(null, '', `#${h.id}`);1876 const el = document.getElementById(h.id);1877 if (el) el.scrollIntoView({ behavior: 'smooth', block: 'start' });1878 closeMobileMenu();1879 });1880 return a;1881 };1882
1883 // stack entries: { level, ul }1884 const stack = [{ level: 1, ul: root }];1885
1886 headings.forEach(h => {1887 const li = document.createElement('li');1888 li.appendChild(makeLink(h));1889
1890 // Pop stack until we find a parent whose level is less than current1891 while (stack.length > 1 && stack[stack.length - 1].level >= h.level) {1892 stack.pop();1893 }1894 stack[stack.length - 1].ul.appendChild(li);1895
1896 // This li can be a parent for deeper headings — push a new ul onto stack1897 const childUl = document.createElement('ul');1898 li.appendChild(childUl);1899 stack.push({ level: h.level, ul: childUl });1900 });1901
1902 // Remove empty trailing uls1903 root.querySelectorAll('ul:empty').forEach(u => u.remove());1904 }1905
1906 function buildExtLinks(html) {1907 const nav = document.getElementById('ext-links-nav');1908 const list = document.getElementById('ext-links-list');1909 list.innerHTML = '';1910
1911 // Collect all external href values from rendered HTML1912 const seen = new Set();1913 const tmp = document.createElement('div');1914 tmp.innerHTML = html;1915 tmp.querySelectorAll('a[href]').forEach(a => {1916 const href = a.getAttribute('href');1917 if (href && href.startsWith('http') && !seen.has(href)) {1918 seen.add(href);1919 const li = document.createElement('li');1920 const link = document.createElement('a');1921 link.href = href;1922 link.target = '_blank';1923 link.rel = 'noopener noreferrer';1924 link.textContent = href;1925 link.title = href;1926 li.appendChild(link);1927 list.appendChild(li);1928 }1929 });1930
1931 nav.hidden = seen.size === 0;1932 if (document.getElementById('ext-links-heading')) {1933 document.getElementById('ext-links-heading').textContent = (lang.ext_links_icon || '🔗') + ' ' + (lang.ext_links || 'Page links');1934 }1935 }1936 function buildComments(pageContentEl) {1937 const nav = document.getElementById('comments-nav');1938 const list = document.getElementById('comments-list');1939 list.innerHTML = '';1940
1941 // Collect all critic comments rendered in the page1942 const markers = pageContentEl.querySelectorAll('.critic-comment');1943 if (!markers.length) {1944 nav.hidden = true;1945 document.getElementById('comments-heading').textContent =1946 (lang.comments_icon || '💬') + ' ' + (lang.comments || 'Comments');1947 return;1948 }1949
1950 markers.forEach((comment, idx) => {1951 const n = idx + 1;1952 // Give each comment an anchor id so we can link to it1953 const anchorId = `critic-comment-${n}`;1954 comment.id = anchorId;1955 comment.classList.add('critic-comment-anchor');1956
1957 // Extract comment text from the tooltip span1958 const tooltip = comment.querySelector('.critic-comment-text');1959 const text = tooltip ? tooltip.textContent.trim() : '';1960
1961 const li = document.createElement('li');1962 const a = document.createElement('a');1963 a.href = `#${anchorId}`;1964 a.textContent = text || `(${lang.comment_empty || 'empty comment'})`;1965 a.setAttribute('aria-label', `${lang.comments || 'Comment'} ${n}: ${text}`);1966 a.addEventListener('click', e => {1967 e.preventDefault();1968 const target = document.getElementById(anchorId);1969 if (target) {1970 target.scrollIntoView({ behavior: 'smooth', block: 'center' });1971 // Move focus to the comment button for keyboard/screen-reader users1972 const btn = target.querySelector('.critic-comment-marker');1973 if (btn) btn.focus();1974 }1975 });1976 li.appendChild(a);1977 list.appendChild(li);1978 });1979
1980 nav.hidden = false;1981 document.getElementById('comments-heading').textContent =1982 (lang.comments_icon || '💬') + ' ' + (lang.comments || 'Comments');1983 }1984
1985 let scrollSpyObserver = null;1986
1987 function setupScrollSpy() {1988 if (scrollSpyObserver) scrollSpyObserver.disconnect();1989 scrollSpyObserver = new IntersectionObserver(entries => {1990 entries.forEach(entry => {1991 if (entry.isIntersecting) {1992 const id = entry.target.id;1993 document.querySelectorAll('#toc-list a').forEach(a => {1994 a.classList.toggle('active', a.getAttribute('href') === `#${id}`);1995 });1996 }1997 });1998 }, { rootMargin: '-20% 0px -70% 0px' });1999 document.querySelectorAll('#page-content h2, #page-content h3, #page-content h4').forEach(el => scrollSpyObserver.observe(el));2000 }2001
2002 // =============================================2003 // SEARCH UI2004 // =============================================2005 function showSearch(results, query) {2006 const container = document.getElementById('search-results');2007 const titleEl = document.getElementById('search-results-title');2008 const listEl = document.getElementById('search-results-list');2009 const pageContent = document.getElementById('page-content');2010
2011 container.classList.add('active');2012 pageContent.style.display = 'none';2013
2014 titleEl.textContent = results.length2015 ? (lang.search_results_for || 'Results for') + ` "${query}" (${results.length})`2016 : (lang.search_no_results || 'No results for') + ` "${query}"`;2017
2018 if (!results.length) {2019 listEl.innerHTML = `<p class="search-no-results">${lang.search_try_other || 'Try a different keyword.'}</p>`;2020 return;2021 }2022 listEl.innerHTML = results.map(r => `2023 <div class="search-result-item">2024 <h3><a href="#${r.entry.slug}">${escHtml(r.entry.title)}</a></h3>2025 <p class="result-section">${lang.section || 'Section'}</p>2026 <p class="result-excerpt">${r.excerpt}</p>2027 </div>2028 `).join('');2029 listEl.querySelectorAll('a').forEach(a => {2030 a.addEventListener('click', e => {2031 e.preventDefault();2032 navigate(a.getAttribute('href').replace('#',''));2033 clearSearch();2034 });2035 });2036 }2037
2038 function hideSearch() {2039 document.getElementById('search-results').classList.remove('active');2040 const pageContent = document.getElementById('page-content');2041 if (pageContent) pageContent.style.display = '';2042 }2043
2044 function clearSearch() {2045 const inp = document.getElementById('search-input');2046 inp.value = '';2047 document.getElementById('search-clear').hidden = true;2048 hideSearch();2049 }2050
2051 // =============================================2052 // MOBILE MENU2053 // =============================================2054 function openMobileMenu() { /* sidebar je na mobilu staticky pod obsahem */ }2055 function closeMobileMenu() { /* nic k zavírání */ }2056
2057 // =============================================2058 // APPLY CONFIG2059 // =============================================2060 function applyConfig() {2061 const palette = (config.color || 'blue').toLowerCase();2062 document.documentElement.setAttribute('data-palette', palette);2063 document.documentElement.lang = config.language || 'cs';2064
2065 document.getElementById('site-title').textContent = config.title || 'Web';2066 document.getElementById('site-tagline').textContent = config.tagline || '';2067
2068 // Footer2069 const copy = document.getElementById('footer-copyright');2070 if (config.copyright) {2071 const { name, url, year } = config.copyright;2072 const y = year || new Date().getFullYear();2073 copy.innerHTML = `© ${y} ` + (url ? `<a href="${escAttr(url)}" target="_blank" rel="noopener">${escHtml(name||url)}</a>` : escHtml(name||''));2074 }2075 const footerLinks = document.getElementById('footer-links');2076 if (config.footer_links) {2077 footerLinks.innerHTML = config.footer_links.map(l =>2078 `<a href="${escAttr(l.url)}"${l.url.startsWith('http') ? ' target="_blank" rel="noopener"' : ''}>${escHtml(l.label)}</a>`2079 ).join('');2080 }2081 }2082
2083 function applyLang() {2084 document.getElementById('toc-heading').textContent = (lang.toc_icon || '📋') + ' ' + (lang.toc || 'On this page');2085 document.getElementById('search-btn-label').textContent = lang.search || 'Search';2086 document.getElementById('search-clear-label').textContent = lang.cancel || 'Clear';2087 document.getElementById('search-input').placeholder = lang.search_placeholder || 'Search…';2088 document.getElementById('search-input').setAttribute('aria-label', lang.search || 'Search');2089 document.getElementById('skip-link').textContent = lang.skip_to_content || 'Skip to content';2090 const menuToggle = document.getElementById('menu-toggle');2091 menuToggle.textContent = '☰ ' + (lang.menu || 'Menu');2092 menuToggle.setAttribute('aria-label', lang.open_sidebar || 'Open sidebar');2093 }2094
2095 function buildNav() {2096 const nav = document.getElementById('site-nav');2097 nav.innerHTML = '';2098
2099 const sectionLinks = document.createElement('div');2100 sectionLinks.className = 'nav-section-links';2101
2102 // Only iterate top-level items: groups + non-child sections2103 sections.forEach(sec => {2104 if (sec.parentSlug) return; // sub-pages rendered inside their group2105
2106 if (sec.isGroup) {2107 // ── Dropdown group ────────────────────────────────────────────────2108 const wrapper = document.createElement('div');2109 wrapper.className = 'nav-group';2110 wrapper.dataset.groupSlug = sec.slug;2111
2112 const btn = document.createElement('button');2113 btn.className = 'nav-group-btn';2114 btn.type = 'button';2115 btn.textContent = sec.title;2116 btn.setAttribute('aria-expanded', 'false');2117 const submenuId = `submenu-${sec.slug}`;2118 btn.setAttribute('aria-controls', submenuId);2119
2120 const submenu = document.createElement('ul');2121 submenu.className = 'nav-submenu';2122 submenu.id = submenuId;2123 submenu.setAttribute('role', 'list');2124 submenu.hidden = true;2125
2126 sec.children.forEach(child => {2127 const li = document.createElement('li');2128 const a = document.createElement('a');2129 a.href = `#${child.slug}`;2130 a.textContent = child.title;2131 a.dataset.slug = child.slug;2132 a.addEventListener('click', e => {2133 e.preventDefault();2134 navigate(child.slug);2135 closeAllSubmenus();2136 closeMobileMenu();2137 });2138 li.appendChild(a);2139 submenu.appendChild(li);2140 });2141
2142 btn.addEventListener('click', () => {2143 const isOpen = btn.getAttribute('aria-expanded') === 'true';2144 closeAllSubmenus();2145 if (!isOpen) openSubmenu(btn, submenu);2146 });2147
2148 // Close on Escape2149 wrapper.addEventListener('keydown', e => {2150 if (e.key === 'Escape') closeAllSubmenus();2151 });2152
2153 wrapper.appendChild(btn);2154 wrapper.appendChild(submenu);2155 sectionLinks.appendChild(wrapper);2156
2157 } else {2158 // ── Regular link ──────────────────────────────────────────────────2159 const a = document.createElement('a');2160 a.href = `#${sec.slug}`;2161 a.textContent = sec.title;2162 a.dataset.slug = sec.slug;2163 a.addEventListener('click', e => {2164 e.preventDefault();2165 closeAllSubmenus();2166 navigate(sec.slug);2167 closeMobileMenu();2168 });2169 sectionLinks.appendChild(a);2170 }2171 });2172
2173 nav.appendChild(sectionLinks);2174
2175 // Extra menu links (right side)2176 if (config.menu_links && config.menu_links.length) {2177 const extra = document.createElement('div');2178 extra.className = 'nav-menu-links';2179 config.menu_links.forEach(l => {2180 const a = document.createElement('a');2181 a.href = l.url;2182 a.textContent = l.label;2183 if (l.url.startsWith('http')) { a.target = '_blank'; a.rel = 'noopener noreferrer'; }2184 extra.appendChild(a);2185 });2186 nav.appendChild(extra);2187 }2188
2189 // Close submenus when clicking outside nav2190 document.addEventListener('click', e => {2191 if (!nav.contains(e.target)) closeAllSubmenus();2192 });2193 }2194
2195 function openSubmenu(btn, submenu) {2196 submenu.hidden = false;2197 btn.setAttribute('aria-expanded', 'true');2198 // Focus first item for keyboard users2199 const first = submenu.querySelector('a');2200 if (first) first.focus();2201 }2202
2203 function closeAllSubmenus() {2204 document.querySelectorAll('.nav-group-btn').forEach(b => {2205 b.setAttribute('aria-expanded', 'false');2206 const id = b.getAttribute('aria-controls');2207 const sub = document.getElementById(id);2208 if (sub) sub.hidden = true;2209 });2210 }2211
2212 // =============================================2213 // INIT2214 // =============================================2215 async function init() {2216 const loadingText = document.getElementById('loading-text');2217
2218 try {2219 loadingText.textContent = 'Načítám konfiguraci…';2220 config = await fetchJSON('./website.json');2221
2222 // Load language2223 const langFile = `./${config.language || 'cs'}.json`;2224 loadingText.textContent = 'Načítám jazyk…';2225 try { lang = await fetchJSON(langFile); } catch { lang = {}; }2226
2227 applyConfig();2228 applyLang();2229
2230 // Load content2231 loadingText.textContent = 'Načítám obsah…';2232 const mdText = await fetchText('./website.md');2233
2234 // Load sidebar (optional)2235 try { sidebarMD = await fetchText('./sidebar.md'); } catch { sidebarMD = ''; }2236
2237 // Parse sections — groups have no rawContent, skip parsing for them2238 const rawSections = splitSections(mdText);2239 sections = rawSections.map(sec => {2240 if (sec.isGroup) return sec;2241 // Extract news items before parsing MD (they have their own renderer)2242 const { hasNews, newsItems, regularMD } = extractNewsItems(sec.rawContent);2243 const parsedHTML = parseMD(regularMD);2244 const headings = extractHeadings(parsedHTML);2245 return { ...sec, parsedHTML, headings, hasNews, newsItems };2246 });2247
2248 buildNav();2249 buildSearchIndex();2250
2251 // Search events2252 const searchInput = document.getElementById('search-input');2253 const searchBtn = document.getElementById('search-btn');2254 const searchClear = document.getElementById('search-clear');2255
2256 const performSearch = () => {2257 const q = searchInput.value.trim();2258 if (!q) { clearSearch(); return; }2259 searchClear.hidden = false;2260 const results = doSearch(q);2261 showSearch(results, q);2262 };2263
2264 searchInput.addEventListener('keydown', e => { if (e.key === 'Enter') performSearch(); });2265 searchBtn.addEventListener('click', performSearch);2266 searchClear.addEventListener('click', () => { clearSearch(); render(); });2267
2268 // Mobile menu2269 document.getElementById('menu-toggle').addEventListener('click', openMobileMenu);2270 document.getElementById('sidebar-overlay').addEventListener('click', closeMobileMenu);2271
2272 // Routing2273 window.addEventListener('popstate', render);2274
2275 // Hide loading2276 document.getElementById('loading-screen').style.display = 'none';2277
2278 // Initial render — outside main try/catch so file-load errors stay separate2279 render();2280
2281 } catch(err) {2282 // Only reached if file loading failed, not render errors2283 console.error(err);2284 document.getElementById('loading-screen').style.display = 'none';2285 const errScreen = document.getElementById('error-screen');2286 errScreen.classList.add('active');2287 document.getElementById('error-title').textContent = lang.error_title || 'Loading error';2288 document.getElementById('error-msg').innerHTML =2289 (lang.error_msg || 'Could not load site files. Make sure the files') +2290 ' <code>website.json</code>, <code>website.md</code>' +2291 (lang.error_server || ' are present and the site is served over HTTP (not opened as a local file).') +2292 `<br><br><small>${escHtml(err.message)}</small>`;2293 }2294 }2295
2296 return { init };2297})();2298
2299document.addEventListener('DOMContentLoaded', Engine.init);2300</script>2301</body>2302</html> v1
message create index.html via macOS
date Jun 7, 2026
size 78.0 KB