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