From 27b90b3aca8b13fc4cbf6d36a0defa86b6571117 Mon Sep 17 00:00:00 2001 From: Cmanjah <116933114+Cmanjah@users.noreply.github.com> Date: Fri, 23 Oct 2026 17:43:33 +0100 Subject: [PATCH 1/2] Improve tutorial site navigation and homepage layout --- index.rst | 60 +++++++++++--------------- pages/about.rst | 2 +- pages/all-tutorials.rst | 5 ++- pages/learning-paths.rst | 4 +- scripts/generate_homepage.py | 54 ++++++++++++++++-------- source/_data/news.yml | 21 +++------ source/_static/custom.css | 61 +++++++++++++++++++++------ source/_static/js/catalog.js | 18 +++++++- source/_templates/pky-onthispage.html | 3 +- tests/test_homepage.py | 13 +++++- 10 files changed, 147 insertions(+), 94 deletions(-) diff --git a/index.rst b/index.rst index 3c2bb64..69eb412 100644 --- a/index.rst +++ b/index.rst @@ -15,7 +15,7 @@ Learn with PyKale
Start learning Browse tutorials - View on GitHub ↗
@@ -37,52 +37,37 @@ Learn with PyKale -What's new ----------- +.. raw:: html -.. include:: _generated/news.rst +
Get started ----------- -.. grid:: 1 2 2 4 - :gutter: 3 - :class-container: pky-getstarted - - .. grid-item-card:: 1 · Install PyKale - :link: https://github.com/pykale/pykale#installation - - ``pip install pykale`` — then you are ready to run any tutorial. - - .. grid-item-card:: 2 · Run your first tutorial - :link: _pages/others - :link-type: doc +.. container:: pky-start-steps - Domain adaptation on digit images: the recommended starting point. + 1. **Install PyKale.** Run ``pip install pykale``. :doc:`Setup guide ` + 2. **Try the first tutorial.** :doc:`Domain adaptation on digit images + ` + is the recommended starting point. + 3. **Find your next topic.** :doc:`Browse all tutorials ` or + :doc:`follow a learning path `. - .. grid-item-card:: 3 · Explore a domain - :link: pages/all-tutorials - :link-type: doc +What's new +---------- - Browse by Protein, Cancer, Cardiac, Materials or Others. +.. include:: _generated/news.rst - .. grid-item-card:: 4 · Adapt it to your data - :link: CONTRIBUTING - :link-type: doc +.. raw:: html - Every tutorial ships its full script — fork it and point it at your own dataset. +
Featured learning paths ----------------------- -Curated routes through the tutorials, using the catalogue's own metadata. - -.. include:: _generated/paths.rst - -Browse by domain ----------------- +Choose a route, then follow its tutorials in order. -.. include:: _generated/domains.rst +.. include:: _generated/featured_paths.rst Browse all tutorials -------------------- @@ -91,20 +76,23 @@ Filter the full catalogue by domain, task, modality, difficulty, runtime and sta .. raw:: html -
+
-
+
+ Filters +
+

@@ -128,7 +116,7 @@ Contributing See which narratives are still drafts and pick one to write. .. grid-item-card:: Report an issue - :link: https://github.com/pykale/pykale/issues + :link: https://github.com/pykale/tutorials/issues Something wrong or unclear? Open an issue. diff --git a/pages/about.rst b/pages/about.rst index c6b47fe..ea17472 100644 --- a/pages/about.rst +++ b/pages/about.rst @@ -10,7 +10,7 @@ What this is Each tutorial is a self-contained folder: a narrated ``plot_.py`` that reads top to bottom and builds into a notebook, plus the full ``main.py`` experiment it is drawn from. Tutorials are -organised by the domain that owns them — General, Protein, Cancer, Cardiac and Materials — and every +organised by the domain that owns them — Others, Protein, Cancer, Cardiac and Materials — and every tutorial carries structured metadata (task, modality, difficulty, runtime, status) that drives the filters, learning paths and status page. diff --git a/pages/all-tutorials.rst b/pages/all-tutorials.rst index 5e496b7..c32a8bc 100644 --- a/pages/all-tutorials.rst +++ b/pages/all-tutorials.rst @@ -14,7 +14,10 @@ count and URL update as you go, so a filtered view can be shared.
-
+
+ Filters +
+

diff --git a/pages/learning-paths.rst b/pages/learning-paths.rst index 829fa7f..9a913b7 100644 --- a/pages/learning-paths.rst +++ b/pages/learning-paths.rst @@ -1,8 +1,6 @@ Learning paths ============== -Curated routes through the tutorials. Each path is an ordered sequence — follow it top to bottom, or -jump to whichever step you need. Paths are defined in ``tutorial_catalog.yml`` and stay in sync with -the catalogue. +Choose a route that matches your goal. Follow its tutorials in order, or jump to the topic you need. .. include:: /_generated/paths.rst diff --git a/scripts/generate_homepage.py b/scripts/generate_homepage.py index e808729..cf74cc1 100644 --- a/scripts/generate_homepage.py +++ b/scripts/generate_homepage.py @@ -43,22 +43,15 @@ def _gallery_doc(path: str) -> str: def _news_rst() -> str: items = yaml.safe_load(NEWS_FILE.read_text(encoding="utf-8")) or [] - out = [".. grid:: 1 2 2 2", " :gutter: 3", " :class-container: pky-news", ""] + out = [".. container:: pky-news", ""] for item in items: badge = item.get("badge", "") role = BADGE_ROLE.get(badge, "bdg-secondary") badge_rst = f":{role}:`{badge}`" if badge else "" - out += [ - " .. grid-item-card::", - " :class-card: pky-news-card", - "", - f" {badge_rst} **{item['title']}**", - "", - f" {' '.join(item['summary'].split())}", - ] + content = f"{badge_rst} **{item['title']}** — {' '.join(item['summary'].split())}" if item.get("link"): - out += ["", f" :doc:`{item.get('link_text', 'Learn more')} <{item['link']}>`"] - out.append("") + content += f" :doc:`{item.get('link_text', 'Learn more')} <{item['link']}>`" + out.append(f" - {content}") return "\n".join(out) + "\n" @@ -91,19 +84,42 @@ def _domains_rst() -> str: def _paths_rst() -> str: catalog = Catalog.load() slug_to_path = catalog.slug_to_path() - out = [".. grid:: 1 2 2 2", " :gutter: 3", " :class-container: pky-paths", ""] + out: List[str] = [] for path_def in catalog.learning_paths: - steps: List[str] = [] + out += [ + f".. _learning-path-{path_def.id}:", + "", + path_def.title, + "-" * len(path_def.title), + "", + " ".join(path_def.summary.split()), + "", + ] for i, slug in enumerate(path_def.tutorials, 1): meta = _meta(slug_to_path[slug]) - steps.append(f" {i}. :doc:`{meta.title} <{_gallery_doc(slug_to_path[slug])}>`") + out.append(f"{i}. :doc:`{meta.title} <{_gallery_doc(slug_to_path[slug])}>`") + out.append("") + return "\n".join(out) + "\n" + + +def _featured_paths_rst() -> str: + catalog = Catalog.load() + slug_to_path = catalog.slug_to_path() + out = [".. grid:: 1 2 2 4", " :gutter: 2", " :class-container: pky-featured-paths", ""] + for path_def in catalog.learning_paths: + first = _meta(slug_to_path[path_def.tutorials[0]]) out += [ f" .. grid-item-card:: {path_def.title}", - " :class-card: pky-path-card", + f" :link: learning-path-{path_def.id}", + " :link-type: ref", + " :class-card: pky-featured-path", "", f" {' '.join(path_def.summary.split())}", "", - *steps, + f" {len(path_def.tutorials)} tutorials · Starts with {first.title}", + "", + " +++", + " View path →", "", ] return "\n".join(out) + "\n" @@ -128,6 +144,7 @@ def _ecosystem_rst() -> str: def _status_table_rst() -> str: """A list-table of every tutorial's status, for the Tutorial Status page.""" catalog = Catalog.load() + domain_labels = {bucket.id.value: bucket.label for bucket in catalog.buckets} rows = [ ".. list-table::", " :header-rows: 1", @@ -145,9 +162,9 @@ def _status_table_rst() -> str: status = "Draft" if meta.draft else "Written" rows += [ f" * - :doc:`{meta.title} <{_gallery_doc(path)}>`", - f" - {meta.domain.value if meta.domain.value != 'others' else 'general'}", + f" - {domain_labels[meta.domain.value]}", f" - {status}", - f" - {meta.difficulty.value}", + f" - {meta.difficulty.value.capitalize()}", f" - {meta.estimated_runtime}", ] return "\n".join(rows) + "\n" @@ -159,6 +176,7 @@ def build() -> int: "news.rst": _news_rst(), "domains.rst": _domains_rst(), "paths.rst": _paths_rst(), + "featured_paths.rst": _featured_paths_rst(), "ecosystem.rst": _ecosystem_rst(), "status_table.rst": _status_table_rst(), } diff --git a/source/_data/news.yml b/source/_data/news.yml index 1f93104..fc33120 100644 --- a/source/_data/news.yml +++ b/source/_data/news.yml @@ -6,29 +6,18 @@ date: 2026-07-24 badge: New summary: >- - Tutorials are now organised by the domain that owns them -- Protein, Cancer, Cardiac, Materials - and Others -- each with its own page and colour. + Browse tutorials by Protein, Cancer, Cardiac or general methods. - title: Executable Sphinx-Gallery tutorials date: 2026-07-24 badge: New summary: >- - Every tutorial is a runnable script. The narrated version builds into a notebook and a - downloadable Python file. The executing CI build can be run manually; its nightly schedule is - currently disabled. + Each tutorial includes a runnable script. Narrated walkthroughs are being completed. - title: Reference tutorial available date: 2026-07-24 badge: Updated summary: >- - Domain adaptation on digit images is fully written -- the recommended starting point for - transfer learning with PyKale. - link: _pages/others - link_text: Read it - -- title: Materials domain - date: 2026-07-24 - badge: Planned - summary: >- - A Materials domain is reserved for structure/property prediction and crystal descriptors. - Tutorials are in development. + The digit-image domain adaptation walkthrough is ready for first-time readers. + link: /_gallery/others/image_classification/digit_domain_adaptation/plot_digit_domain_adaptation + link_text: Read tutorial diff --git a/source/_static/custom.css b/source/_static/custom.css index e1eb137..186488c 100644 --- a/source/_static/custom.css +++ b/source/_static/custom.css @@ -169,13 +169,13 @@ html[data-theme="dark"] .pky-goto:hover { background: #123024; color: #a7f3d0; } the shared 1880px from the geometry block above -- no narrower landing-only cap. */ body.pky-landing .bd-breadcrumbs { display: none; } /* Keep the real document-title h1 for a11y/SEO but let the hero be the visible headline. */ -body.pky-landing .bd-article > section > h1:first-child { +body.pky-home .bd-article > section > h1:first-child { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; } /* ---- Hero (in-flow, spans the main content width) ------------------------ */ .pky-hero { - margin: 0.5rem 0 2.2rem; border-radius: var(--pykale-radius); overflow: hidden; + margin: 0.5rem 0 1.5rem; border-radius: var(--pykale-radius); overflow: hidden; background: radial-gradient(900px 260px at 88% -30%, rgba(46, 158, 107, 0.18), transparent 60%), linear-gradient(180deg, var(--pykale-surface-2), var(--pykale-surface)); @@ -183,14 +183,14 @@ body.pky-landing .bd-article > section > h1:first-child { } .pky-hero-inner { display: grid; grid-template-columns: 1.25fr 0.75fr; gap: 1.5rem; align-items: center; - padding: 2.4rem 2.6rem; min-height: 320px; + padding: 1.5rem 2rem; min-height: 220px; } .pky-eyebrow { text-transform: uppercase; letter-spacing: 0.14em; font-size: 0.8rem; font-weight: 700; color: var(--pykale-green-700); margin: 0 0 0.5rem; } -.pky-hero-title { font-size: 2.8rem; font-weight: 800; line-height: 1.08; margin: 0 0 0.8rem; color: var(--pykale-ink); } -.pky-hero-lead { font-size: 1.15rem; line-height: 1.5; color: var(--pykale-muted); max-width: 36rem; margin: 0 0 1.4rem; } +.pky-hero-title { font-size: 2.6rem; font-weight: 800; line-height: 1.08; margin: 0 0 0.5rem; color: var(--pykale-ink); } +.pky-hero-lead { font-size: 1.05rem; line-height: 1.45; color: var(--pykale-muted); max-width: 36rem; margin: 0 0 1rem; } .pky-hero-cta { display: flex; flex-wrap: wrap; gap: 0.6rem; } .pky-hero-art { color: var(--pykale-green-500); justify-self: center; width: 100%; max-width: 280px; } .pky-hero-art svg { width: 100%; height: auto; } @@ -233,16 +233,41 @@ html[data-theme="dark"] .pky-btn-ghost:hover { background: #123024; } body.pky-landing .bd-content h2 { font-size: 1.6rem; margin: 3rem 0 1rem; padding-top: 0; color: var(--pykale-ink); border: 0; } -body.pky-landing .bd-content > section > section:first-of-type > h2 { margin-top: 1.5rem; } body.pky-landing .bd-content > section p { color: var(--pykale-muted); max-width: 54rem; } +/* Put the first actions and recent updates in one compact, scannable row. News is plain text; + only the explicit tutorial link is interactive. */ +.pky-home-overview { display: grid; grid-template-columns: 1fr 1fr; gap: 2rem; align-items: start; } +.pky-home-overview > section { min-width: 0; } +body.pky-landing .pky-home-overview > section > h2 { font-size: 1.3rem; margin: 0 0 0.65rem; } +.pky-start-steps ol { list-style: none; counter-reset: pky-steps; padding: 0; margin: 0; } +.pky-start-steps li { + counter-increment: pky-steps; position: relative; padding: 0.35rem 0 0.55rem 2.35rem; + border-bottom: 1px solid var(--pykale-border); line-height: 1.4; +} +.pky-start-steps li:last-child { border-bottom: 0; } +.pky-start-steps li::before { + content: counter(pky-steps); position: absolute; left: 0; top: 0.35rem; + display: grid; place-items: center; width: 1.6rem; height: 1.6rem; + border-radius: 50%; background: var(--pykale-green-100); color: var(--pykale-green-900); + font-size: 0.8rem; font-weight: 700; +} +.pky-start-steps li p, .pky-news li p { margin: 0; font-size: 0.9rem; } +.pky-news ul { list-style: none; padding: 0; margin: 0; } +.pky-news li { padding: 0.35rem 0 0.55rem; border-bottom: 1px solid var(--pykale-border); line-height: 1.4; } +.pky-news li:last-child { border-bottom: 0; } +.pky-news .sd-badge { margin-right: 0.25rem; vertical-align: 0.08em; } +.pky-news a { white-space: nowrap; } + /* ---- sphinx-design cards (shared) --------------------------------------- */ .bd-content .sd-card { border: 1px solid var(--pykale-border); border-radius: var(--pykale-radius); background: var(--pykale-surface); overflow: hidden; transition: transform 0.12s ease, box-shadow 0.12s ease, border-color 0.12s ease; } -.bd-content .sd-card:hover { transform: translateY(-3px); box-shadow: var(--pykale-shadow); } +.bd-content .pky-domain-card:hover, +.bd-content .pky-tut-card:hover, +.bd-content .pky-featured-path:hover { transform: translateY(-3px); box-shadow: var(--pykale-shadow); } .bd-content .sd-card-title { font-weight: 700; } .bd-content a.sd-card:hover .sd-card-title { color: var(--pst-color-primary); } @@ -251,7 +276,12 @@ body.pky-landing .bd-content > section p { color: var(--pykale-muted); max-width .pky-domain-card .sd-card-title { color: var(--pky-accent, var(--pst-color-primary)); } .pky-domain-card:hover { border-color: var(--pky-accent); } -.pky-news-card, .pky-path-card, .pky-eco-card { border-left: 3px solid var(--pst-color-secondary); } +.pky-eco-card { border-left: 3px solid var(--pst-color-secondary); } +.pky-featured-path { border-top: 3px solid var(--pst-color-secondary) !important; } +.pky-featured-path .sd-card-body { padding: 0.8rem 0.9rem; } +.pky-featured-path .sd-card-title { font-size: 1rem; line-height: 1.25; } +.pky-featured-path .sd-card-text { font-size: 0.85rem; line-height: 1.4; } +.pky-featured-path .sd-card-footer { padding: 0.45rem 0.9rem; font-size: 0.85rem; } /* Bucket-page card grid: override sphinx-design's fixed bootstrap row with a CSS grid whose column count is fixed per page (pky-cols-N, N = min(tutorial count, 4)). Fixed 1fr columns mean the cards @@ -283,8 +313,7 @@ body.pky-landing .bd-content > section p { color: var(--pykale-muted); max-width .sd-badge { border-radius: 0.5rem; font-weight: 600; } -/* News badges — a distinct branded colour per type (New green, Updated blue, Planned amber), - overriding sphinx-design's default palette. Filled roles render as .sd-bg-. */ +/* News badges — a distinct branded colour per type. */ .pky-news .sd-badge { text-transform: uppercase; letter-spacing: 0.03em; font-size: 0.7rem; padding: 0.15rem 0.5rem; } .pky-news .sd-bg-success { background-color: #1b7a4f !important; color: #ffffff !important; } .pky-news .sd-bg-info { background-color: #2563eb !important; color: #ffffff !important; } @@ -302,12 +331,14 @@ html[data-theme="dark"] .pky-news .sd-bg-warning { background-color: #f59e0b !im border-radius: var(--pykale-radius); position: sticky; top: 3.8rem; z-index: 5; } .pky-search { grid-row: 1; grid-column: 1; } +.pky-filter-details { grid-row: 2; grid-column: 1 / -1; } +.pky-filter-details summary { display: none; } .pky-search input { width: 20rem; max-width: 100%; padding: 0.4rem 0.7rem; border-radius: 0.5rem; border: 1px solid var(--pykale-border); background: var(--pykale-surface); color: var(--pykale-ink); } .pky-clear { grid-row: 1; grid-column: 2; justify-self: end; } -.pky-facets { grid-row: 2; grid-column: 1 / -1; display: flex; flex-direction: column; gap: 0.3rem; } +.pky-facets { display: flex; flex-direction: column; gap: 0.3rem; } .pky-facet-group { display: flex; flex-wrap: wrap; gap: 0.3rem; align-items: baseline; } .pky-facet-label { font-size: 0.7rem; font-weight: 700; text-transform: uppercase; letter-spacing: 0.06em; @@ -401,7 +432,7 @@ html[data-theme="dark"] .pky-chip-written { color: #a7f3d0; background: #123024; } /* Below desktop, hide the wide field and show the theme's search-button (icon -> dialog). */ @media (max-width: 960px) { - .navbar-header-items .bd-search, header .bd-search { display: none; } + .navbar-header-items .bd-search, header .bd-search { display: none !important; } } /* ---- Left-sidebar context label ------------------------------------------ */ @@ -474,9 +505,13 @@ html[data-theme="dark"] #pst-back-to-top svg { fill: #06231a; color: #06231a; } .pky-footer-cols { grid-template-columns: repeat(2, 1fr); } } @media (max-width: 768px) { + .pky-home-overview { grid-template-columns: 1fr; gap: 1.2rem; } .pky-hero-title { font-size: 2.1rem; } .pky-catalog-controls { position: static; grid-template-columns: 1fr; } - .pky-clear { grid-row: auto; grid-column: 1; justify-self: start; } + .pky-filter-details { grid-row: 2; grid-column: 1; } + .pky-filter-details summary { display: list-item; cursor: pointer; color: var(--pst-color-primary); font-weight: 600; padding: 0.3rem; } + .pky-filter-details .pky-facets { padding-top: 0.5rem; } + .pky-clear { grid-row: 2; grid-column: 1; justify-self: end; } .pky-facet-label { width: auto; flex-basis: 100%; } } @media (max-width: 600px) { diff --git a/source/_static/js/catalog.js b/source/_static/js/catalog.js index d005219..da2be7b 100644 --- a/source/_static/js/catalog.js +++ b/source/_static/js/catalog.js @@ -12,10 +12,10 @@ "use strict"; function pkyInit() { - // Full-width landing treatment for pages that carry the catalogue (homepage, all-tutorials). var root = document.getElementById("pky-catalog"); - if (root) document.body.classList.add("pky-landing"); if (!root) return; + document.body.classList.add("pky-landing"); + if (root.hasAttribute("data-home")) document.body.classList.add("pky-home"); var siteRoot = root.getAttribute("data-root") || ""; var FACETS = [ @@ -33,8 +33,18 @@ var emptyEl = document.getElementById("pky-empty"); var searchEl = document.getElementById("pky-q"); var clearEl = document.getElementById("pky-clear"); + var filterDetails = root.querySelector(".pky-filter-details"); + var filterSummary = document.getElementById("pky-filter-summary"); var syncUrl = root.hasAttribute("data-sync-url"); + if (filterDetails) { + var compactFilters = window.matchMedia("(max-width: 768px)"); + filterDetails.open = !compactFilters.matches; + compactFilters.addEventListener("change", function (event) { + filterDetails.open = !event.matches; + }); + } + var domainLabels = {}; var tutorials = []; var active = {}; // key -> Set of selected values @@ -124,6 +134,10 @@ grid.innerHTML = shown.map(card).join(""); emptyEl.hidden = shown.length !== 0; countEl.textContent = shown.length + " of " + tutorials.length + " tutorials"; + if (filterSummary) { + var selectedCount = FACETS.reduce(function (total, f) { return total + active[f.key].size; }, 0); + filterSummary.textContent = selectedCount ? "Filters (" + selectedCount + " active)" : "Filters"; + } writeUrl(); } diff --git a/source/_templates/pky-onthispage.html b/source/_templates/pky-onthispage.html index 321dbc8..8e701a3 100644 --- a/source/_templates/pky-onthispage.html +++ b/source/_templates/pky-onthispage.html @@ -3,10 +3,9 @@

On this page

diff --git a/tests/test_homepage.py b/tests/test_homepage.py index 1217caa..5578da9 100644 --- a/tests/test_homepage.py +++ b/tests/test_homepage.py @@ -38,8 +38,8 @@ def test_ecosystem_planned_entries_have_no_url(): def test_build_writes_all_partials(tmp_path, monkeypatch): monkeypatch.setattr(generate_homepage, "GENERATED_DIR", tmp_path) count = build() - assert count == 5 - for name in ["news.rst", "domains.rst", "paths.rst", "ecosystem.rst", "status_table.rst"]: + assert count == 6 + for name in ["news.rst", "domains.rst", "paths.rst", "featured_paths.rst", "ecosystem.rst", "status_table.rst"]: assert (tmp_path / name).exists(), name @@ -60,3 +60,12 @@ def test_news_partial_reads_the_data_file(tmp_path, monkeypatch): build() news = (tmp_path / "news.rst").read_text(encoding="utf-8") assert "Domain-based tutorial structure" in news + assert "grid-item-card" not in news + + +def test_status_table_uses_catalog_domain_label(tmp_path, monkeypatch): + monkeypatch.setattr(generate_homepage, "GENERATED_DIR", tmp_path) + build() + status = (tmp_path / "status_table.rst").read_text(encoding="utf-8") + assert " - Others\n" in status + assert " - general\n" not in status From e4963d1335767fa90596cf0292d8c5a7266fb77f Mon Sep 17 00:00:00 2001 From: Cmanjah <116933114+Cmanjah@users.noreply.github.com> Date: Fri, 23 Oct 2026 17:54:37 +0100 Subject: [PATCH 2/2] replaced browser favicon --- conf.py | 4 ++-- source/_static/pykale_favicon.png | Bin 0 -> 1907 bytes 2 files changed, 2 insertions(+), 2 deletions(-) create mode 100644 source/_static/pykale_favicon.png diff --git a/conf.py b/conf.py index 8978c96..26be3e0 100644 --- a/conf.py +++ b/conf.py @@ -287,9 +287,9 @@ def setup(app): ogp_site_name = "PyKale Tutorials" ogp_description_length = 200 -# sphinx-favicon: reuse the PyKale mark (served from _static). +# Use the compact PyKale mark for browser tabs; the wide logo remains in the navbar. favicons = [ - {"href": "pykale_logo.png"}, + {"href": "pykale_favicon.png"}, ] # sphinx-notfound-page: absolute URLs on the 404 so links work from any depth. diff --git a/source/_static/pykale_favicon.png b/source/_static/pykale_favicon.png new file mode 100644 index 0000000000000000000000000000000000000000..e98521c75b5b61ab5c74ecbc58e2a682b81065a1 GIT binary patch literal 1907 zcmV-(2aNcMP)>lXf00001b5ch_0Itp) z=>Px#1ZP1_K>z@;j|==^1poj532;bRa{vGxhX4Q_hXIe}@nrx22M9?-K~z{r?Uz|^ z8`l|zpEEODL~+qV?W=8BvgAXwU|Q z+Xh7p6h)iVPGTpnvs8{FS(0rUl_=4QBvO=QlAGH1;dP-}wT)zf z)n%u=)`MoZ6i@U7#M7EpwOcrN@f40~7XZm{oRLd6+5g&ian!gGLI99Sr5XO?zwvfA z(f;CIgd)(>8G<+bgl+{&X5ETfUgQ3CX>b$OqVnAN=W#bwl9-MXx;@U!=i@XT?ZCUI z2_#6}KpGOwW}(*CO7Qw!R%6SgqDfqhm3UgJS@2JjUCWeCU&Pjm2=Fvl<8JZNbhHDT zyNtknj0BhT??p1Oe#!;Xweh=?pWs0xV5OSxc zS$sH8RcAwC8qT^3j=p}H`Fm5uCL+WpB6NIzA5DjM6^T$Zm71PrM&B7IitYMdZ|-@M zA|L5Ey&p%li_w1!kPOF3hU3)rwc%;;0;Xpm%)Qs_7Mu;0NJCOy=b`OHcTR+%8wkbx z?Wl1xesu_&+d<&U5cPd+xEd>2@XwG*r8k$(S41cRPfIlj=sa_P^_4WC+v8NU)xbt0 z`v3MhGyMTm5TGauLWrU&EYB@6@W!=V`R(p9va1;;Z}{o_!Ty}U@;Xm$4243Zlw?w7 z6DgWX=a~a|yBf)+&6p5^o^wZtj?FUq&R~9q0^$$n8Tr%4be}y88`=y*qFOcjUVWix z@oc)-)bc$dP4C!52tob6cE;Zu$(fI0QSqIB9-%4Ree2VlNPzG_kgGxSFbR z@#tt6tJ{vvU6zZbldCLF&gY&fZPv)}Bu0V#dfIA8CsM>FBDt7q)#yCEA4iRgz?EU5 zqamWBA;Ncp9JqK2XMII37b$so!%y#plesOCQZjt$CPv2enoWv?=;!R9H)<|*gU#Zy=uwxT>~NThC1R#}eJ4VGr2NZla& zG#}CI7RsueBqMPKe)j>hgF#|}S!^EDl9S;$>&tmj)g28u>nqGEY&5Ktl6cT8yHFK` zs*p~uGWds&n7sJ_p(@+nQ?`07(0pV!y%$ap4Ma#MR&yq_JeT19Uq5@i#T$~qzwR*I zKUxqo7<%&~T8{0;_tWFWW>-Lwj~H3qJjShFtS&n>dz+CGmZqXq?QX!`REc6ynRtK1 zyb1_do;5{Pw0gOJxgSMU*!#*cgb+l>!Yob4IQ+|(aW+&UgkX6tL8w1~G)xQFWXm-G zpsKThJ!cPLEw=;UzueDSBE|k!UqG>HEQMl>T)Ij7i#;rb;s7LPqSX3Y+4+(WX-H=8 zJS2GiE(d?{5@l6R4BcRMFi7CaP|0d0+X?|tZ5ow3>*zjr1feR--=89Q?H+wUKS_D5 zhqc8OhA-XR*gEJsdx*N;Rsj5O_oHXB^!)fJTA7vkdtVa#&`&y%Dp@7ZW3P%*H%L87 z5*ePv(^QRjPZJKWi}9<&c)Odixyx`hRwAXuS?{6!g&u?uOnfkczMjQ*{y0eRU%tiE zr=#fUCtnq}ZPr58lWW|4>l2nEi&S?uQn90s;I+E|D5{E36vW0IW@TZC_{2OtuY3n7 zB_n_SnCMuzWXA1zst^D@qvLRk>a;Q4KgPuSBc;Qi$X5~qQ0r?oUtrSw^w@IS5)l4k0)2h! tC&QEY%0j*_p1DW9ZoU;#$~Q*H{{Tav-up}%=>q@&002ovPDHLkV1giQkahq7 literal 0 HcmV?d00001