From 8d9adc2817406fd5b6e53d687e25493e239d3932 Mon Sep 17 00:00:00 2001 From: Julien Danjou Date: Thu, 10 Sep 2026 21:21:34 +0200 Subject: [PATCH] ci: fail a PR that leaves an orphaned image under src/content MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Prose moves and an asset does not, and a one-off deletion pass alone just recurs on the next page rewrite or removal. Adds scripts/check-orphaned-images.mjs, matching the internal-leaks check's shape (plain-Node scanner, its own vitest suite, pnpm script, CI job): for every image file under any images/ directory within src/content/, it checks whether any source file (src/, integrations/, plugins/) spells out `/images/` — the substring every relative import carries, anchored on the leading slash, regardless of how many `../` it climbs. Matches on that anchored path, not on basename alone. A basename-only sweep of the whole tree is what missed workflow/writing-your-first-rule/config-editor.png on the first pass of #12770: a same-named config-editor.png under configuration/ is genuinely imported, so the orphan read as referenced. It also does not assume every image lives under src/content/images/ — src/content/docs/images/merge-queue-hero.jpg is real and referenced, sitting next to the page that imports it instead of in the shared tree, so the scan walks for any images/ directory under src/content/ rather than that one path. Also covers .avif (a supported documentation image format the first pass omitted), and fails closed when two images share the same `/images/<...>` needle across different roots: rather than let one import clear both, it reports every image in a colliding group so a human resolves the ambiguity. Deletes src/content/images/billing/plan.png in the same commit. #12764 dropped billing.mdx's import of it after #12770 merged, so it is the check's first real catch, and without the deletion this commit would land red on main. Co-Authored-By: Claude Sonnet 5 Co-Authored-By: Claude Opus 5 (1M context) Change-Id: I7a8544e1209554c67202175985a75db65b0ac00f --- .github/workflows/ci.yaml | 17 +++ .mergify.yml | 1 + AGENTS.md | 1 + package.json | 1 + scripts/check-orphaned-images.mjs | 181 +++++++++++++++++++++++++ scripts/check-orphaned-images.test.mjs | 120 ++++++++++++++++ src/content/images/billing/plan.png | Bin 30102 -> 0 bytes 7 files changed, 321 insertions(+) create mode 100644 scripts/check-orphaned-images.mjs create mode 100644 scripts/check-orphaned-images.test.mjs delete mode 100644 src/content/images/billing/plan.png diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index d108664ae9..6a0f41d52f 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -121,6 +121,23 @@ jobs: - name: Check that diagrams resolve their colours from tokens run: node scripts/check-diagram-tokens.mjs + orphaned-images: + timeout-minutes: 5 + runs-on: ubuntu-24.04 + steps: + - name: Checkout 🛎️ + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + + - name: Setup Node 🔧 + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + with: + node-version-file: .node-version + + # No dependencies needed: the scanner is plain Node. Its own unit tests + # run in the `test` job. + - name: Check for orphaned images + run: node scripts/check-orphaned-images.mjs + build: timeout-minutes: 20 runs-on: ubuntu-24.04 diff --git a/.mergify.yml b/.mergify.yml index e5e0d77175..b426fd36f9 100644 --- a/.mergify.yml +++ b/.mergify.yml @@ -32,6 +32,7 @@ merge_protections: - check-success = test - check-success = diagram-tokens - check-success = test-broken-links + - check-success = orphaned-images - or: - check-success=Cloudflare Pages # fork can't have the preview build diff --git a/AGENTS.md b/AGENTS.md index 3fa440d0d6..e7420e94b2 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -50,6 +50,7 @@ pnpm test # Run Vitest unit tests # Safety pnpm check:internal-leaks # Scan docs for leaked internal information pnpm check:diagram-tokens # Scan diagrams for hardcoded colors +pnpm check:orphaned-images # Scan for images nothing references ``` ## Key Directories diff --git a/package.json b/package.json index bf446e2fcb..f61f9102cb 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,7 @@ "check:config-examples": "node scripts/validate-config-examples.mjs", "check:diagram-tokens": "node scripts/check-diagram-tokens.mjs", "check:internal-leaks": "node scripts/check-internal-leaks.mjs", + "check:orphaned-images": "node scripts/check-orphaned-images.mjs", "check:links": "linkinator / enterprise/ --server-root dist --config linkinator.config.mjs" }, "devDependencies": { diff --git a/scripts/check-orphaned-images.mjs b/scripts/check-orphaned-images.mjs new file mode 100644 index 0000000000..709eb66d61 --- /dev/null +++ b/scripts/check-orphaned-images.mjs @@ -0,0 +1,181 @@ +#!/usr/bin/env node +/** + * Scan `src/content/` for images nothing references. + * + * Prose moves — a page is rewritten, an example is cut, a page is deleted + * outright — and the image it imported is easy to leave behind, because + * removing a page never fails a build the way removing a component does. + * docs#12728 is the case that prompted this: it dropped a billing example's + * two `` uses and both `import` lines, and the PNGs stayed on disk + * with nothing pointing at them. + * + * A page imports an image by a relative path that always contains + * `/images/` — `../../images/billing/plan.png`, `./images/hero.jpg`, three + * levels up, one level up, it does not matter, that suffix is constant. So + * for every image file that sits under some `images/` directory anywhere in + * `src/content/`, this checks whether that exact suffix appears anywhere in + * the docs source. No suffix found anywhere means nothing imports the file, + * under any relative path. The leading `/` matters: it is what stops an + * unrelated string like `cdn-images/billing/plan.png` from reading as a + * reference to `images/billing/plan.png` just because one contains the + * other. + * + * This deliberately does NOT match by basename alone: two images in + * different directories can share a filename (`config-editor.png` exists + * under both `configuration/` and, until it was deleted as part of this same + * change, the removed `workflow/writing-your-first-rule/` page), and a + * basename-only sweep reads the orphan as referenced because a same-named + * file elsewhere genuinely is. That false negative is exactly how + * `workflow/writing-your-first-rule/config-editor.png` survived a first, + * basename-only pass. + * + * Nor does it assume images only live under `src/content/images/` — most do, + * but `src/content/docs/images/merge-queue-hero.jpg` is a real, referenced + * counterexample sitting right next to the page that imports it. Any + * `images/` directory under `src/content/` counts. + * + * What it cannot see: an image loaded through `import.meta.glob` or any + * other pattern that does not spell out the path literally. Nothing in the + * docs does that today; if it starts, add the path to ALLOWLIST below rather + * than trying to make the regex understand glob patterns. + * + * Two images under different `images/` roots can share the same suffix + * (same subpath after the nearest `images/` ancestor) and so need the same + * needle. `findOrphans` cannot tell which one an import actually resolves + * to, so it reports every image in a colliding group rather than guessing. + * + * Usage: + * node scripts/check-orphaned-images.mjs + * node scripts/check-orphaned-images.mjs --json + */ + +import fs from 'node:fs'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const ROOT = path.resolve(fileURLToPath(import.meta.url), '..', '..'); +const CONTENT_DIR = 'src/content'; +const SEARCH_DIRS = ['src', 'integrations', 'plugins']; +const IMAGES_DIRNAME = 'images'; +const SEARCH_EXTENSIONS = ['.mdx', '.md', '.astro', '.ts', '.tsx', '.js', '.mjs']; +const IMAGE_EXTENSIONS = ['.png', '.jpg', '.jpeg', '.gif', '.svg', '.webp', '.avif']; + +/** + * Paths relative to the repo root that are known to be unreferenced on + * purpose, each with why. Empty today — a legitimate entry should be rare, + * since an image that nothing renders belongs in an asset host, not the + * docs source tree. + */ +export const ALLOWLIST = new Set([]); + +function* walk(dir) { + for (const entry of fs.readdirSync(dir, { withFileTypes: true })) { + const full = path.join(dir, entry.name); + if (entry.isDirectory()) yield* walk(full); + else yield full; + } +} + +/** + * Image files under any `images/` directory within `src/content/`, as paths + * relative to the repo root (posix separators). A file with no `images/` + * ancestor is outside this checker's scope: nothing imports an image by a + * path containing `/images/` unless it sits under a directory named that. + */ +export function findImages(contentDir = path.join(ROOT, CONTENT_DIR)) { + const found = []; + for (const full of walk(contentDir)) { + if (!IMAGE_EXTENSIONS.includes(path.extname(full).toLowerCase())) continue; + const parts = path.relative(ROOT, full).split(path.sep); + if (parts.includes(IMAGES_DIRNAME)) found.push(parts.join('/')); + } + return found.sort(); +} + +/** + * The substring a relative import of `imagePath` (repo-relative, posix) must + * contain: `/images/` through the rest of the path, taken from the nearest + * `images` segment. Anchored on the leading slash so an unrelated + * `cdn-images/...` cannot satisfy it by accident. + */ +export function needleFor(imagePath) { + const parts = imagePath.split('/'); + const idx = parts.lastIndexOf(IMAGES_DIRNAME); + return `/${parts.slice(idx).join('/')}`; +} + +/** Concatenated text of every scanned source file under SEARCH_DIRS. */ +export function buildCorpus(root = ROOT, dirs = SEARCH_DIRS) { + let corpus = ''; + for (const dir of dirs) { + const abs = path.join(root, dir); + if (!fs.existsSync(abs)) continue; + for (const full of walk(abs)) { + if (SEARCH_EXTENSIONS.includes(path.extname(full).toLowerCase())) { + corpus += fs.readFileSync(full, 'utf8'); + corpus += '\n'; + } + } + } + return corpus; +} + +/** + * Images (repo-relative paths) that `corpus` never mentions, plus any image + * whose needle collides with another image's — two images living under + * different `images/` roots can end up with the identical suffix (e.g. + * `src/content/images/billing/hero.png` and + * `src/content/docs/images/billing/hero.png` both need `/images/billing/hero.png`), + * and a single import of either would otherwise clear both regardless of + * which one it actually references. Fail closed: a colliding pair is always + * reported, even when the needle is present, so a human resolves the + * ambiguity instead of the checker silently guessing. + */ +export function findOrphans(corpus, images = findImages(), allowlist = ALLOWLIST) { + const needleCounts = new Map(); + for (const img of images) { + const needle = needleFor(img); + needleCounts.set(needle, (needleCounts.get(needle) ?? 0) + 1); + } + return images.filter((img) => { + if (allowlist.has(img)) return false; + const needle = needleFor(img); + if (needleCounts.get(needle) > 1) return true; + return !corpus.includes(needle); + }); +} + +function main(argv) { + const jsonMode = argv.includes('--json'); + + const images = findImages(); + const corpus = buildCorpus(); + const orphans = findOrphans(corpus, images); + + if (jsonMode) { + process.stdout.write(`${JSON.stringify(orphans, null, 2)}\n`); + return orphans.length === 0 ? 0 : 1; + } + + console.log(`Scanned ${images.length} image(s) under ${CONTENT_DIR}/**/${IMAGES_DIRNAME}/.`); + if (orphans.length === 0) { + console.log('No orphaned images found.'); + return 0; + } + console.error(`\n${orphans.length} orphaned image(s) — nothing imports them:\n`); + for (const o of orphans) { + console.error(` ${o}`); + } + console.error( + '\nDelete the file, or if the page that used it still needs it, restore the\n' + + '`import` and `` that reference it. If it is genuinely unreferenced\n' + + 'on purpose, add it to ALLOWLIST in scripts/check-orphaned-images.mjs with why.' + ); + return 1; +} + +// Run as a CLI only when invoked directly, so tests can import the helpers. +if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) { + process.exit(main(process.argv.slice(2))); +} diff --git a/scripts/check-orphaned-images.test.mjs b/scripts/check-orphaned-images.test.mjs new file mode 100644 index 0000000000..a0c31a7b03 --- /dev/null +++ b/scripts/check-orphaned-images.test.mjs @@ -0,0 +1,120 @@ +import { describe, expect, it } from 'vitest'; +import { buildCorpus, findImages, findOrphans, needleFor } from './check-orphaned-images.mjs'; + +describe('needleFor', () => { + it('anchors on the nearest images/ ancestor, leading slash included', () => { + expect(needleFor('src/content/images/billing/plan.png')).toBe('/images/billing/plan.png'); + expect(needleFor('src/content/docs/images/hero.jpg')).toBe('/images/hero.jpg'); + }); +}); + +describe('findOrphans', () => { + it('reports an image no import mentions', () => { + const corpus = 'import hero from "../../images/other/hero.png"\n'; + expect( + findOrphans(corpus, [ + 'src/content/images/billing/plan.png', + 'src/content/images/other/hero.png', + ]) + ).toEqual(['src/content/images/billing/plan.png']); + }); + + it('matches any relative import depth, not just one style', () => { + const corpus = [ + 'import a from "../../images/billing/plan.png"', + 'import b from "../../../images/ci-insights/token.png"', + 'import c from "./images/merge-queue-hero.jpg"', + ].join('\n'); + expect( + findOrphans(corpus, [ + 'src/content/images/billing/plan.png', + 'src/content/images/ci-insights/token.png', + 'src/content/docs/images/merge-queue-hero.jpg', + ]) + ).toEqual([]); + }); + + it('finds images outside src/content/images/, under any images/ directory', () => { + // src/content/docs/images/merge-queue-hero.jpg is the real counterexample: + // an image colocated with the page that imports it rather than living + // under the shared src/content/images/ tree. + const corpus = 'import hero from "./images/merge-queue-hero.jpg"\n'; + expect(findOrphans(corpus, ['src/content/docs/images/merge-queue-hero.jpg'])).toEqual([]); + expect(findOrphans('', ['src/content/docs/images/merge-queue-hero.jpg'])).toEqual([ + 'src/content/docs/images/merge-queue-hero.jpg', + ]); + }); + + it('does not let a same-named file elsewhere hide a real orphan', () => { + // Two images share a basename in different directories; only one is + // imported. A basename-only check would call both referenced — this is + // the false negative that let workflow/writing-your-first-rule/ + // config-editor.png survive a first, basename-only sweep. + const corpus = 'import x from "../../images/configuration/config-editor.png"\n'; + const images = [ + 'src/content/images/configuration/config-editor.png', + 'src/content/images/workflow/first-rule/config-editor.png', + ]; + expect(findOrphans(corpus, images)).toEqual([ + 'src/content/images/workflow/first-rule/config-editor.png', + ]); + }); + + it('does not let a directory-prefix collision produce a false reference', () => { + // "/images/a/logo.png" must not read as referenced by an import of + // "images/prefix-a/logo.png" just because the shorter string is a + // substring of the longer one everywhere except at the images/ anchor. + const corpus = 'import x from "../../images/prefix-a/logo.png"\n'; + expect(findOrphans(corpus, ['src/content/images/a/logo.png'])).toEqual([ + 'src/content/images/a/logo.png', + ]); + }); + + it('does not let an unrelated cdn-images/ path satisfy the reference', () => { + // Before the leading-slash anchor, "cdn-images/billing/plan.png" (no + // slash before "images") would have contained "images/billing/plan.png" + // as a plain substring and wrongly cleared the file. + const corpus = 'See https://cdn-images/billing/plan.png for the source asset.\n'; + expect(findOrphans(corpus, ['src/content/images/billing/plan.png'])).toEqual([ + 'src/content/images/billing/plan.png', + ]); + }); + + it('honors the allowlist', () => { + const corpus = ''; + const images = ['src/content/images/billing/plan.png']; + expect(findOrphans(corpus, images, new Set(images))).toEqual([]); + }); + + it('reports an unreferenced .avif file', () => { + const corpus = 'import hero from "../../images/other/hero.png"\n'; + expect(findOrphans(corpus, ['src/content/images/billing/plan.avif'])).toEqual([ + 'src/content/images/billing/plan.avif', + ]); + }); + + it('fails closed when two images share a needle, even if it is present', () => { + // Same subpath after the nearest images/ ancestor, different roots — a + // single import of either satisfies both needles, so which one is truly + // referenced is ambiguous. Report both rather than clearing either. + const corpus = 'import hero from "../../images/billing/hero.png"\n'; + const images = [ + 'src/content/images/billing/hero.png', + 'src/content/docs/images/billing/hero.png', + ]; + expect(findOrphans(corpus, images)).toEqual(images); + }); +}); + +describe('published docs', () => { + it('contain no orphaned images', () => { + const images = findImages(); + const corpus = buildCorpus(); + const orphans = findOrphans(corpus, images); + if (orphans.length) { + throw new Error(`Orphaned image(s) — nothing imports them:\n${orphans.join('\n')}`); + } + // Sanity: the scan actually walked the images tree. + expect(images.length).toBeGreaterThan(50); + }); +}); diff --git a/src/content/images/billing/plan.png b/src/content/images/billing/plan.png deleted file mode 100644 index ae62be9e3687a45aa9191986cca54630d897c91a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 30102 zcmce;cT`i+_b-SAQ4mpS5}Jxs1r$OH%>oI6^d?I z*U&piC-f3}nH#_F&3iLzz4g|b`OWahy{vmrZq7M(pMCcJd_FtD8fq|FY7jLA1qJPM zB?T=Cii@Tc6z3{0p9ikE#5KwRr*lqPu%{IHooq|MC5`6_PjuWzR>!Fv7_9JXo7H=I z<3{Dgz6z_fLFD?{u*o{)b=1|);afA8#h0&&i#J}pATAY;cIuQ`V57XueO)R99rgHT z>y;JSy$I7WJM|u=s}Dcc67_Y||NW_WcU4X9b#&!>@5usRO))m_+sIG;ha&X``FI^g zKE8)vB%c(x$j95DbL11@GWi$;B_AJy$=&~d{*mZdcytUV(UCuzx2d#4c!XLxgJOiz-iI9!*9 zv-}>0vr9Vf=k!m;*TRE~dES_$4>+$)^xB!$Y>iWuF3apWic!PJJ+6m-+B~a;{^4S~?RvXqb^5%(SfAzT}VasS*}j7EHCi1%C;7 zH_(n2El>$JO)R%_6OgB261JVdn{?1wBTm+;uawnTj9%Lvio;cF9u*6#VWe_QJ^Ckd zzl+5rd+Q+wa!m?4YHa!x+L$9FOJ!a)KNMUaDYkE1sc<2RX>cTp_{8q6%ebsGx;Nyx zO`KtYX;`2awx02%O_batYSJcnr+HP9h?RGE!phlzem!nZ)g;yWr99kzJ}#`DEVbZt zq2Z3LgMGYXSItfcQ>&im1K&3BytAKbCW6Qgu5kH0AtzXP@}jhZ!(3KUZ-NM;01|aR zM*P11(!0DuOin_&MF97Sm)~&6fQS8YdbXMne*G`??nbTLOTD_5q|UX~?us0{SihYX zH+fuMor<9;GfP9|HJ~TA!2~6#rmZhM&8+rkCY$r!f73dvbM6ih^9ON*B{pf`Hx{W= z(tRGVv;~~bX!+SIYtPR_HT*#SsA=A)Th%vo>YBHG?cR5Kl;idmCvG2W$06^b zED_EoZJN``?Eg7z%7knw*Aw9P;tht)d0%*L#tdQ*xB9j_4l5A#knt6H_+paJmH7Z% z0lxx5M=Lljg2(6a2xe16T#7zly8(1CgAGr+c|Oe2HAGrstB z;u1Pw$RXk#J+J0a86C?p0#7$!qlSWIbtZNnwCU%&!nj&$y)B+O=hi2B?kov)H2wO| z9A6|XYFQ^7{@}Od3mi&VK^rd{SaEsqsKsYmSS3jM1T1f8n)9kBzBU;daaqyo9=F@j zunJ5#GD1!q?MF>Rxd>IRcq#XxX7pH9dbCP%KXOWe%cY!E%&xxq(Tq)-Q!sPx9_G@6 z&BlKd@gxuuDiOkRh|^QW=0(Wk4gMBpsI^m z-2HuR)r8WLAL*-Nwdl;^oXLHpkET{(Dxsdc?Ln68=1S;Duxx=M7p*t$;Is)pobGX- zHneBf-Ect1rD~ZxT~Fyz;DgbxH_`-TH^0!%92mtrCLDJhY~5XU&QeMwsmTNH)$EveDr}HMS=NT|L7bCmm zs!GXYu}g!e#$!>Kl^S{!s{_ni^ zQxGarTa?_LDREf;KZm}kL0(t?-?@4QQP8T2q(lFyKTEtp=V4Wwe#zE;i?+mRP?WZ4 zC$EW!VP~y(L&fq=MJUp|++u;aSkqh0pUq+zkBQ2f`;yDHcS&ON%krWc`uPid^qOgW zR9u1oi;m{4p_liCGt*;Mf?YW!@$ZgmevPJEyj|koa=v+BK^_r-?>*qUc7ZKId@q)N zPp@MSa3=Yr=lpRcnY#-lyW<;Oruh7`Z}DMw=;!SwN+;J0>8llyYDv{!<1sk_6>Wpa zxer{tr8al!hNm*oNcMiT#WEEt(H17>NUS%{>KrzqRxvDwSQj@vD{(x|wcz%`2Rp^m zB|YSISP2dq#$`$RJ!&`-3Mf~V-sOL>r9$2^Q#GG}Jy+rvm>-xz%y-8Q&BRUsJ6D%E zdSmI@sTvNI-ep&l^uBA^w)Qt_VR?ROn^JJU>i9l$q_*Iv{V&kYOA@APcBdtjIWG59 z?!Iy3VbfD$6-S)DFDwpAmh;1;Mo?SC^@v=sr@swim-JM2Yp@+{6DE73ASfkaF~izk zQbPVDwdSU#@v&t%-1(7|#bK_LYC?IWqDtIoE2eS}jO-V;>z!9ri50V*um-k6PaSNk zrXjR!{L4g@iiF#`JZ?^wDqPU4{Lz6Hz7{@flai7eui~ zI4$l&N1<5q5&4&6_rjE!vkH7>`QYgQen^%smvAdqixhW+;>hX6MAje&70v?K## zt&`#`kC9$MeTVd(upvEtrAxTyB0}F2p$6{B37w2cX)oKGDjcg9h&n%-%Z+pyjP|&% zpIeKkmsOB{;J#94E>W(17Z|KVRGQ|Inn_GhY~IVcFetG|ciZ1^FD`HY9R}Z3ZLGQs z3uDP50p50C%Zt-bEA#0bjKj~c4cM!10REWi?297uQdnZf!&&eSBge5KS2%5EyP1^p zbQPhO31L9V#u^@-kR*n4=vZ6|r3y>Tvn|QJ<%9~Xeq|B88x<3^Y?`g)L>vovm8$vv zf}HblNo-U5%iL%&o1Usd(O8CSWrw~!V~oVAEXn|OX#`$BL%>TB!q|BwzC!ZNpJ+<| zrY=HJ2LGz|$)>Zmmo~Hq6!2&q4e{%*nu~z^ZPh&90){x{`UTo5zkE0C!C$m&@?sd2 zS3Q~K;oEO(g6)hfd$PuV`-$t41Z*qP-r7RVZi1?mxHw<;8}{@jsu7*AzbB_x;Qi?3wkV3dd-f7TTiW4AmCp;y*Z?2XeZ( zKfl~4)z_g>3RHA#m>BAiVh zJd*gs%s{CjmCkx_X&|Sd%ng?IH?7gsHd`H&pXf9%e0B9Dm)J7qhBPqAWgqyiGTcMe zKM{*s$kTZTrDUp9N%IF@M;{O&{8V6pzH8WJdkfz3_?3E26GuslksKr<1Y9f9BJM^F zaLV~MiVu8*fgM}zS`ANABP@S(_c13tfAi|^Rj&l=u9jCoKY!Uu3^e}@gPM5m^lUZm z3mU4aboDi-va+B$(dtD6k?$c;Jsz#(M=HY9w}9Cp9%r7wGcUTB6pI<{u{jIK58gy+ z^@`@fGt>GX&=?j3AB_>3TNM)MyEu@d->6tnCC^5SGc$h#bAr$PK4xTfj_b(#7m=az zp=sHomWT@d+7E<6R$#Za-1aeBho|pW^IUofx`B*xLmrlDC~|d#K7=3JForRhH+C4} z{z?FyP-Y(a?wau@_4l_zzNhNnF{4A@k-^aAnKC0bt#h`2aUWYaq65YVO2uMuaY~x6 z3EFk0nh^vgN+I^YpN899dDsYbIi}#Z)5b9sV8<3qxP$&LU?wP1346e11H?EfbxL%x zehMb*rXFdHv_Vj46fV1^W-hS6oAx=lNX?T*Pm!O3&t)im6fkw_!*r9HBHtO;HpT?F zn#ok8c5kTIK=Z=6h1t3LcMYcqKklO_i}LN%z&BOfQ&6P75G(Qf&mstm%5`t)Dx+l> zzk=t+57JdUU3~&`JI38^Gw_nr6+STnpF2@G?xZ(9TSeI7g4JQ^+_D73jGtAYRcg>Y zC9LDsT9klHHj6=7?)v+l`y)Ev82$NO5n9r2_};p0BstzlCFHF)2N#PokCn+%>56GB z;0e9VF+_eUh7g@&0)vMDCi}AG*u%FxX z&cT;lH0y?OR+mZ8Xwfp$5xb<@9RuEOL0PWlMj011)0(8Ut(f(F6~H1i5>g4Wdt=+8 z1<76VXUBRFb`?DsiB&4&ZTNVV(u*YjV#2m_H19;|8*N-Fou-?@-}t26pnJsMM$T6whtvc9Naovfs5;kDE zA?vkCw1@02kNN2+=v3wDU&&BhI9IvHc~I6BbZ5pAu4?JJ*5}b?=*BwseQ!fbW#H%N zp1=)fPVnxs{gGOHrSsnq5);aX&NJG!czufCTK!py&Y)1JtnEDkHA(`3Ek}m`+{;^% zdRsN}GZmJRX~k6a>syDXt8K6SpSPopwksu+qRjb@b{r5_oW0uj1pg)o!byPX?YsUMbef${<^>YqtF^ z-3eG#dP|`q|4kuR6k0~#9<4V188wO~u^)wvm9BSV`L}n%c5cb6&BkxbO?oC5A?fnP_hFd1qcJ~c_hcj<5hf5tFJ#VvQ?-Rj{lLjqaR7K%{yUwy>{0@7QORAX;_c%2^rg2Ik!V;=NScolULYATUr^8CO zkneCE?6p1x36a*hIZtvErT5rJiky;1n&_N)%Bio8#6at9MyqR|AIXjQ%La(N-+o-} zJx7ainu9Dv6Mwj6_yjv}24`P`y!&rw>21)JNK_sY&LV zYy8>rojk#CFKnQ>2r3&j93Q_xAvZIW6^)m9v-eDzazQ5be`h?4@wA{EP1AD&RDRz_ z4)j0w#bj@1t0=`G&p2^$b?EDWGaUN_*Qs!v)5qyLVZpNsX=Z2h5-PC|Iy%~zbw8D4 z>gA2ZY4tifZjY4M<|Mi-5A-)$btN6ipDmmnPo5q5&4}9x@O@*G+ZvT#@2k#IPktwF zqNipD)hnaQxE9Wog=16ur4jSEfi0`SV|E{5R1k0}uQKQA8;Sn0!RpSbD~;Fb zqrV;rY+Y==+RAEHx?1YdWSZp`pU_zrU-=BvdLHsHr(|~CWS3(g851>#wN=kZxF4|1 z$0z^CcHBZbUfjN-I3D*X%2N0W`nB(aKZ^ExN)DZke`m5{qR}B=IZ{jJ3G70@>|(}N zzCLIfLT&~@JBM%?$W)!M``&{3&N$tkLK1?-0QbQi#uYOT*<+nk2ohrzFFpF+P;l>u zJL!^<-WH)aJ>AlM&t%6V_B@Ph!E+-FYKy01k@Un76zAB2!+QAxmQEa7wU^?DJ>y%S zo(_1y(kPg5+G0H(Wwg5-lBwI4=J=0NcgWk%6+-h2OsXju2$(w?E!txH6AoyYY{tt> zbdXx{i{Aq|dQ1$nD^2qa``04fCVkYA^PPzczW5q09wTNkiEwC^A-a{TQ&o&@xty4P z2@4A~OYZgRvt>}sJ;+y6R+BcheDIP*5;TmaqDFjZ0&$xu>xb$ozE{k-OS+!Cpe;r( z?aoMDvb{1UBmGbpfwpy?ov>Y`AT+aI-FYdi(DZi4c%oSB;?U~x9J0TaQ`EMG$m*di zGkFjMRX$&4Iw#riFcd>Fbbhz>4I47ej)JPjOPl{yc3XhRwszt=6LqoF=ik~TCC%+G zIdRbKxsfN#@0lk95IiTRtG$Ctbp%5@i-(T#CxSZ<`Z`F3MkjfDVNgX=EOj!zD(0h( zrH)Ro=Q0ILJK2r=QVoKJG6||x0PBOUOc11s2Dg;y#4q}t6r9ar(n>_`OT%WueCL<5 zY%5(keDc9#_5K16IRw?RIQ5^nEdGi26vX2W76}XkR%gV>S>F`TINLG25lYi%w!l|; zt+AvFUa}q7{!0C?EPNqk5PTI!lNT~Jzd+iue}998i><$~AS@1L7gI~& z_NAnk*6xF{vnH!7g#((cVtXQ?IY6~Yg{d~B(6T^ZAT_;6{kw)5co z4XM}Df8=})tpzr20?6W_mWRSBA&s{an;ZlA;UN*=QgB$7eIZdd72IfIi)58CfBm>c z!FH_qXC8>to}dKINxW3Jc=Wi`sx%4Ho^pacij9oX3M4-D45Gb3_~K+8l=CuO7AYix z${=`_T8R0hacmqUp){rn&%^A!X!iD)dZ~dO1*Y;olyZUHAf2M1U(<{*Xh~@YryJ0% z#2avzJ;~0~k2|7kT%dU=n@hs37ujbL;-E69Kt+#IC@_(NVOq;plr>;tf?hDL4%Y9@ z3^&*d%Aplcb@@q?G;*K=X_bYWQMnqK1j&R8&zuqh1wg|``JEqvpYwYbN6&%j6yEe? zP%MYKd?(7>@8cIiWI*c>JP;h2Fez68hfG$@$)9b@_xtCM0J)-FZzG4a>zI2^toQz3 z7~W&h#7)AUFzrt#*LTllZ^FwSYN>Z6H6k-cy8>##B33HKG?{QZq59foSc~Q*C2gq& zRapFd67%bU)P5X4g05A?`j7M^GF|FY-&_4AUftNfS(ntHH=m(Xi%U9kJy$K&{A2>S z#Ue;mAp10pbX*XTVg%Y-nw^%=HB%lTaLA#%r%dan*=x|0hJo3RtmW>x$pCp~wRJUL z^;=!>=+J9N34y;R?Pz9T_LxW27yIAlh8sU5C|!BN^0+I9KHobvn6@e{LRLXkV$nk2 zCj6ruFWaT~i^mvl6oXdY3lCi}nTGJ|P_F9rkt_^7$=j)hX<+hj+7z3>XY7X9rNmrU zt@#H?PB8^TDXTxX+>!KBuZNZ6Lpr%Y#^Gl=@3(~NB~KP@q&D8ZSrE?T zlUp)duRNCRp4_~cvK8UxR`bd=M}I-KCP!%`N;D#cBYQ04`SG!BZtt3GzsrVq5_d-K zG>6lMa|wp`oTYx-j7;1+iJjbihcm&+8H2Q7Hj& zp@MWmhSG>rLsK%bFE1-rI3puj&Piy$X>FuX|{^xf~uT|!Yj_+JeABVpS>DcCmiqBRA(E$4c@DlHXP>GVmc4gLk zw!_Nd7H=&L65U7IhXMG?@P~Ug>0@Z<Y6?E77$pgCOoit7MlgXj^q&W0%4s!zbV*lc1h<-EVxTsT9zmLK}1XfNk=ujPo=S$^2dg(|bqxV*)F z`;?fw{TQwLTm$wS9xfI9+Do1qA2iQF%_NpSfs>hA0F2Y+E55{8=``$5#n%f)V#9h2w z-_I-fy#{?#zrC~(GAKSI+GUg_iImOp>PcL$Is*7j@kV=sa;bu@zx8n0@1s&VL4YDA zjrA(85=T47`=-6;P#{mGC`&=NwTZ-N$LC$7eNo}X3J%}dLOgAa7Sg>be|%&LAYxnk zSc+HXdl++eYH=L8ncm#Cxfk5?JXKws@X-l+0Hs)>9#hpSfB3db?!>1|Vf?q$Se|-# z*HVSRn2u)dt|e{5D~unnC?8@G({pgP5!J;I?|i}r@WO2)&@-I)VwD+9+=Jfw%nlVPH$#c&T>=cOJ(c+!JK8m)1A=6!*bK#JccY&cRfgXl|%+0#>J+roAe&EFtFD{*$Uz*75I~ z$XHTE%^WvU4<{d~XE$lkJ>8czfB63Rdq}u%ZxIT`qb03d_q@$|P!7;3=(_a1(0N_w zYC&iQ0=Fa;)CgBon9=v=%W38khM{^btzo?gYx~hc6&l>7p$$4&4VNoMQocPpd3{;? z_@zye6R}6O)m~N7Cf|G=ebq2}TPy(C96RZLwEkw9G_U7Nl)68@!z%d)@)}=RD779( z6vfNn_ttV^It)v7-8GZ|dzDhhd6h!bVpcr&)7RpZYtISYWUbCzEtT;Yxta`{Hdf*w zC^7%gvo1F*^WR}Xjj*2mc}v|eX?ZMUB9Xh!8$ z`00A@L$}f-UE?0uyjnS5GC`?})E=sfNzySaaQ#sV@4vho1hqc0{u^v%R${L0WFTm` zdKmf{Wm|oC(Xa`3*C&5k7<#L?>V{j&!^KEtQVP6P`*eg3;q4O^#Qvqf)Bh5YvQxk^`wWd>9m zeu4D5xAE}T#7MT9{{7N#$v#VrMtZGUQTm)i$P1`H8_cMkD(feP;anM@yyVJP5tX*9 zcGkj1g~~J_jajyDjGQZl)N0m`Ri;i=)|jjxCeKZl^~I=F51Qh3KNI2yf3O*u&5YBb zRhnC!fjG|kv!UKkN9!kY1wsqYU1Z_YREckl^!1NpZSh}2s+7nXHhE;?t4Yv;rB;Mc47PH?I&RJhFUKR;y1^#8)unb z5Jcuy0X#@_LY}o9E;IeX+m6;+zn3bEcSkW8+sWZ{)lVFcE#*%Jhox?zdNLTYtwv{>tuQ_JUm3KWF>cbm_ikh;-=`s|YBb&r4X(#ye&>7@nsz zug+?1!5w2Crp6?+{F;DO)2x-NlkTGMgv!i>^wp+?{yCY@d@76J`Mgov;j)t%h|B8u zgRQ&ofHV;XB;7lGpo0Ot&y$NiAlsDFRKo!p^dql)Vk#3#A>aWmXVEao+jL_Mo}P0^ z0{H>Do^a1zS~f`451G>2_7y}rpTnM<4w3-Aa)B_ROt^MAF;JQi5bxkoX z%{HRVp(>%^D)YU6(bVJYg;U8%gr2pl4e}Zu@vGV`56YSBWxwPbbmV8e=fvpq%5<_e z+XUfQOhLD~KXGf_RiLB$jiyxd&4R>yS}XkO8xxZWbo{WtpVE~u1y+yth*$Tk%iD#~ zknAtQm+)5rR(YNxrNj}4s(r3>@@}qqMCApvGB>}cx%n>yVj!vZY~0+VTF5AW zM9dWDi+cazKX)7w9sRm^eolDatoa<5dTn6q&#CMpZ+Vq z?|&)MyFTBS3Vk=8K;XO2D=a3f;&u7Xsz&A9Aeo=}iw>F=x3eBgM^sQV`fcWzoh|sC zu+?=}^sw}L^bS|Xeqk^r@SSbBwLV0|4-Z#(0GhmKO45CHrB1SxwJyLDF8?;((T&QS zMI+OlUQRm$&6Nn;qSCx2-tr0-3{)O|=po6#qxT{#?i{_QR=TL~59GFumYl81K8uOOhgSGgSkAF)%{ikpwTrNj|TSMetRq$2J zKm1QaRd6d8$fO(|?RFo^wamYe`CxY9m*am(F9oi#RzmZ0IXihMCgC?aW>9iqadt`c zh>pXtV7UA&MGqn&DpI`5&8Acl4Vn!tS`{rWs$N%C>0%S{*4Z9doi4Yb`7tb}+|}Hd z)Rm-ub+q;}AQp7t*b(+)J2$3e)oCX4Zr;14Mo6{%-(I;Osh^_QSQq>F=dW? z`*!YAi>@=j;IpQvc+Brgr)9;orets2n+)YOvAqemTD=L6!Ep&mM;VM71GVjjOB>pGX4F{n&oX_)>t6s1mdsC-4*h% zKq7CVcaL|yI4@P4q-L}MN+{RrGwPqo7A}7v?0*wVmk(wxPcxWzv|_3FI3;Yd#mD4IzTC0W0!Ujl&(XSyq*!SjNM@}9^@VI+Q8w=nH`$2V6(}_rKmGz(7ZusSyk9g9!5`|KU?0 zqd7})#Vnub|C%unGJY{F%=Mmi#^#My)UHp0>tgTDXwB=JsN9uTXT>(-F)3+=cpjpm z>qk~7iN!<(0D-H|5#WscP-KVNk=LKYb^urlYQ_ zWp<;Pi7Ih|T%Z6+kO5l}b)?|3*`E8o2a&>gt=9mupaeN&*kRsqE0Az~Nd~*vzSRK|3FHO0c|vEY50F==p&@xe8KeT; zg5g|UBNF#^5H#e`Ggz42P{}ZmAC(pk%$n5nq^cfqx9N{u0<>@Z?yp{_c2Cynt=5nX zh{HfG<|=Jo0BK8I4P-V1BhAI#=I zwe-G`XhgQzRUalvxt;dK7n5W4C(tIl<4sxB=lzyuKI2u0Z9O2Mw9RNLW`lU^&;Sz6 z?Y)tPGAY;NSwQUQdc z%fw1U&K^5tKDVp0!-(mIsNcSdXJ{dlDuIT$18mUsR~ae zfGgz?^PXNwN&pY;a;HF<&=}NJ#}Q?NT5v=x@R`+`4fs4AeIyD zz5@VwYwhuPrmYuGhv}QRsPkW(^Z}sADK?kD;p~!;X?C(M=GrDy9S$s4-P=8h`AzdU z%8l_o{3XZ^N4^z{q-BE8opqXipK~w4TJ3s_Z~zJ(nHa9+5PLm;ci|RKr(v#ncl{3K z{hf#>)l4QOQMQ$P%pRXSO-~?;0Y{a%ciO3h_a83sswQ|(p3r?^m#|x_@*vgZu1{>W z9nzL)xgjptiaT`Q&hy?qV7BZ?$_p$5tHBj59_78 z+)1T_yf4t$)toBhelAe;;UVj@Kw7gOuU;>U+vc&&0zp{1>-M1Au{amk3_wsDZUjMz zLPLd43l?p0>Pa$l4RN?3##2Y!Jv~P z&8go>iXMj9%%d8R1NEQbmSSy3G3{v-z3B!jd4tW#fP_{@xaTl|v#;l=Rx7s~7~(uT z1n90!NF*XV#H~L+$$O1E<655I`iweSSq0@L-@U7R*-+0>~Vx!EKc_ASVHeJhGufXIZ^-tk1$BIO}2 zkZ%_PuOV4&v&{#tRqigD01+rs99x+>RE==gb36{qi3e7$v3zU;m^b;`v)ol;6a z0#e za13VUjl6t*UK?jQkk{SUv5H%)0w8Y&hwdo z5`*>E^ocIS$rKkiK_n`Jy*ivJ(Mr&8&gVN`m?9L4IYjkbavfeY065lFO}r)gH?l7? z>t-C2^K-EEs5Y#}YX2U)Q3MRzR`fcN1JDTUxarPhDx{rIU4!F@zhPl$S%9)W+YcPr zTj?JaU+yh0u=71T*~6=_!iz6Z(Lodwfd=rd1$uf$sY`S$I5BCrwXrhWN~VE)BP%cW z94M`(D zNM2++uE52x+S*t^|D|@1VXAc=jwelUK`^vi)hEo3vmGgQ7g95IfrPFqlMNI|ervhF zlabEoO_cW}_Mbn+zeM`uSxY$pxMH<6H3F1Ntyq@f#jC79YqX~^_oOYWlgIibx_m?}ai*MkQ$Zt5vX(}P<1Xf}i|3p`=KeQyJ{6*Xcj+`kR97h_l z@v*7!tlF7+ zMz=g);OhiyQo|UQS=&mNJMT2fpBS`-s2k=V?75X1pRAVK_I*u_XjN`~Sn0N2GgIzH z%(v_?f>%>Z186{4mPcqT9wGdP9mVj8q22kbq3;nU!BF0M2fiTIi562~6wJt8Sy`cB z7A6K|QFWk^6-1Jj+|6_xG; zT0-I49a$!wXcirT&0EMF-#m3vK`*<|V{p%KZ};ux3Cj^T;^B%)0mIHmNq41D1!zO= z%Tj0t;~bcqDzD&Vm|2-&Lip{;XSi*xkVvbXo3k6kRS^)SZ~UA4{n~1Y*&s^^-6TiP zWs8MbA_Wwc*q!-|cTQqe<}sLhW5kwqK}qLluj+libB~+T2yudykU0ijduMITaGwx^ z&&`4XTTjcfQUykF8&S%8<0V?*r~Trc91n~QK5MFs$eEc{$aO9)Fx>_njK(N(mHJ?Q zySk>H&Fk1|fUcOo;nFTGgHd8xC%bn7rTIZ!X)Icx;884kkM;j=Xw)oBD=vt*?TW(-Z# z{z||$bB$pf_M6UfeN6g)1{0LnyH@g9M6HOKo^{oy^G1f>#C`C2wP$rA9E4`bAd4y_ zYnAb9rdp!!Tr1;vj+$M_i&l9+6yhr0TcpUfYDSYdJriTCAiL)5G>g@oASfwjjg1-K zx3aLXsMeXYSTBS^#QRf9l1(aI5!LuEsRB)l`=gfq#C|-&AnclpFISWo)PC<)`c3bG z2+-c7>)Niz2~?FPhs{&zpjDkgA!5O_2b=hD3I1kUWWh*p9-!rHu6emHS^alc2Q`;cN+a%IyTflr?9PA?ZxX!Er{)#lA9`&->y>vi?}9U~Dy z;HU8bF^K%u$}i$Navq~j!Z?MvLx1B-eAhp4anMb!csAKyqU23|x#94+WLPI;R~*n2 z?*$OqQD`MsK#?=f1_(U%()##2t-#c$mrsEAtVU3Bs?5mD#PimgR=Ec|e!O%BL}VKd zP%4ALpW%QJ{e^KNpO}BJsV0h_0leopkYe(9=DLK80*iZ=U)$DzpcKF*Xfs*!+rw1Q z2#h;f$}&_^QtD233RA;Wy2H{wogBfwut~u;zIMqK8Y)K>1HUTX+bn zc$l&zcbI0y)WwoqLzcMk%L=TOZKeCiQ1Iu%(~ZIJYk&rQaS7c)t@VeF#KQi~(yqI( z!$jlZ=Ph#WRWtTu$iS5@gsF=XktS1_$?vh!-0U0qokpUsW9`p0Yq{-Q=jf8q8WVd6_ zA#mpr)dEk;PX8=zcRozN3)G$UTowUIajSMLrX-3#D$i#TygxE*%x-v!028eR>!-rI zCv`g%9Wki{yvuS7H3RR;eDeB1PSGdm=}^fZ<+Q>j4OHFjnCtaKxu)@U!S0ECf2@SLf`c5;yX!FK?}|DS3ZyG=d;js&HK&vIO<@eT-0u2|#paSO*2AFQJ;s?i|STRnFZDuE8?E+ALS!;)j zO59WFe&(!id^~?Yzhw^`{_(Jr@pu#eHJyv_9T%PqXJfkf$yS|&*XqxFK6=Lk zThV-G?{|SOOG{we~zt+@Ps`l~tv`*O1GVp@3mlG_n z_BPFmSH_+1iK#c3DxoTs1hgE96+co&b@S$Ke|lvXifWY*I4KKMYsPWdLhPrxnP z1J^`cI=>>|0Pcw!9fDu)kF$JWy!2L(HlSxKH6{X(s1`;uE)$mnrV!o{jYE>)sisWM z?jepn%Y@6Ck90MkynC~h0B1A>n+J+jI1e*8I~x6XYG0zulV@-G9?nqi5*G_p7SvS! zs5Ait;W=g1(fy2A=NMwP;5CCu`Toj2Sc&#))t+FexcMicZ`uKqtgN#)4uKpBDA`F3 z6$!SMTONd2_mrX0Y;Nmf0i2|ty^?GlHP_kM^D13eh7fLfrp1kx8ZS%W@0688T&G1r zOGhtVoKzJ);ad<&k-PH>T#Oi-_g@8`CMZ1dXpjGVM?gS%e3jvqwa-?trABue__YZj zC*k?2-M_qzH^cU1)Tt<>eW4g%ka54q$qSBNUeTU>A>eX~Tj4=BDVjTk&O6;T2-k|d3JcaL>KYDNK z9%(soz@O+Q^t1XN6F&-8-h?oPMe65h`cD=6?!Q$1t!V=hCZs#@(u}4U`Hpxz_lerU zsB}KOnOsTDhEnLw9R6f^$lyn;ve=#cV46GcU&h~-0O(5+IfCvpz3PW z!h`;|&aF0vfQs9X7qp~dD*{nHy>a~C4FG=kV#B5TBk+Wu`QvzLH+~SpS;PM`D%vRe zk~87kX-2!YF~n$W3r7m`$Nv)7{L z`+Vmo(6lMW6+vzii#%er7k0CM{C&JG|H8eeuv1oC8i zrE6Ij4KO$etYGbvYB9^BsFLcg%08#A6IGa9f{0MF{d?LM(g%L>XOw0KAguN5qetbN zr)&D_&CbRW51IMy(_ujI|M^-AP21(zIIiDxzL_<>=5slKC3&lil{_z1-m0* zl@;}o<&6RvG5(bYLOli_w#d-d#eVw-JCpfOyB4@k)r8wWaQf480JT{;Wzzo?0*E&S z2|?si`}bP(s8(I9QsfmWjP8Fcpvd3{i5=q}J?EKq)>C;0R#qL9NSHTSH%`y$rr-1e z>M7nkq^!RP4GFu`0zl)qqoYM-18{3=i%MI24G`(6N5uTrj|Aj6w~eV`6&?o+VNXp( zdJF{hTKK9`BN)R(ikGx6PIC{j>r&d~k8lu!0mUi4W$&bhW zpx;Vz0Sa|;Fw6Y=v_N+s|3pc)cDC3K1&t8I217w&1Mv^m_|hD{K-Ib+7PnUKd@6Z% zNFYDAs#nu=_A~Ypsz$)5=DesUGcw5z^9Un>sjm0beD0|E4N9g$rgm2hvOEJrI1GlR z;thCM75ODKA?6T!h!^)F4E7I+uNPtuwxs(B5x7TJ$1h_7v3EVM;(Z!S2KFCITHa%5 zD@}I&htL0i$EP^Vq!Rf5Cn5)+bdt5Up8fw*M*BarM4zNev6{x^mjB+iYF7`(Q!*QK zKo(~`mgG(SiAn@XAPn=LHkcg~2hR@7_tFM0{o@=E>tH$be_GCh;{voqZe>%U^0%(aXR+1fPuI1d)-g(8A&#||Y#Be-Key?MW z1fzcBbsNA|iry~qeMyagRAv8gI&q93Xm=Nre-xlE6w4j}uqm>m@8{};_Gts^v{-31^%+&v&dUBW#XcXm~qQ!Xt z)*C3AQUK#KfQ9t&V>0~C#_osk)v3Q+M>nszCr8jcZ!z?HBsKT>1eb2C+%K8{NQ*xI zwqFN+A_5?(rV4JoH+pOc9hmTkb#e2U)Z7#bWGMDnC<4ncaAe%4|8MT)l(C|F#c10l>es&pdj3Wu&;vhfZHfx#AC}2t@||j)+xD zf^dS{BiT#gDp$fME>7;U5U{FUK&mU4Q&;V6`=TeK1UY=gE+dC;gZl!Yc;9ut@Jto}KO`_UBAa!5LU8o^%~e z`kksgWYzZ~mHJisZf$MFJ}~fX!qoPfa1Q3@iD4?8xj_LUAOl`VR^@mpEbX&uVuii{ z>zGmIm})Rov+!M$Iy&S_2;t7sd=X<#NNLoa->@Z3ZaJV`Iq~ni&BWEr`Omz}=lE6! zp!Ryd3xVnzpvI+Wz&81BRQ=yyKvAUdJ+l*5Y4bW|_My5o-U18WpE3EVzv4c%>fsoM z^?&|PB#hPnl7umdgU`0tOtq`Jp$ORHH1&xijROENM#r2`(e^95fwk<*0o9x@$z_|v z=Wum6?$v!dcDa#U-BF(-y4{uDT9>1+GRK3~{tlyZWbgMGKtXmy%O(PpZ&P>ywGn|~ zY{iMVFY?FD<|8q&`oNDM3>JpE4&lbj0j-$R?!bN`Q}q$uSebd8?Aw8{1Awr}#w=o) z9+(o-F>BRF;)nsQ(PbM+b+_4}&F@^WLHpyT2Lhl{!#6wor_{=&(wt)Tp@U7a6 zSNa`BfUJ&JmaA9q8mkmEu*3rB8h{M|(vNFBCjP{J`&WQ^gc$6czmL2;l?flWbvV%* z?`Leay9SarS#AE^q+VGr;qAdt|cgd#Qc z-plvIwb!1#XTCY#%sz9@`R6zz!%W^Jq`i6H-+f=#vt3t8U!ZF+jjQ{WBz*)@ayc{!@b^metsIdHcdOZtFunLA~vFu%d>O)}H6h40oRVsDT^up93QZ z$sEGbHcMkydwZ47ehIwE^BXI!~F#|)A(LNe1Utl(Q$hn}UdIT<*V9XvtpN_)+=9Ou{W z&+ympeP0|d-B7d8-5FW+%|5Oq_7DjX)_L7Yxhli9-!@{Q(%G4)Y>=@-Z3HmK1uE5o zH=r6{ajZ;`eXgRlYgx_m5u^r+SHHlBdyfV&BqrP)q_ERj!2o>iG_Fd~>lYGF$>i6y z(i0;KYzmE#-UdPd(ZEyUD^Owul|ku4Jb$PP$<{MsQuR^$$F^ua{#y5Z3!mk}O}8=; z=LveU2Xb`fOo!on#eN$HKLiY}U12$m9%~ohwK9)CgYJJ~J7oST5o5h+Rc#e4MW8_T z8y&+`Dl%|6tL;Gr-q}OLxg(zdz}asE03oquImv0jJ-V|&bg58>5#QVS5YXIXgCaS- z@`7Ns)K_$Z>%<$;k!D8>E4%6 zwd5WM-S+I%otYPQ?EG}ESDWWL)ch_(>vmPBY?{KN8}1hc63&XV-WI!(dqP+q0o0LHJ~5DV{z^ht81t**Z`N(&CSKTR1G=$r zfxka_u*#`7YkJy@oKL`p8aH3p1_u!!>$tfq5;s3`mjKeS<31{#JHk9?k3gM)5{L}& zYd13AZH6!`-?EaJ=G_OvyZuA43^`vw$wZ-+FL zL(A(NTfB~UJysq&Z(@)8!~5n;b}qN1Su)U!ytVP8ZEFG{^k-gT<+xH|E1WQRG|{d4SB2u!Y=e1sycJ`}vl>9Ld-YbZeQ z=?@ARF=b?fLF6=a{#%~ezkMu$hhfj=$Idz_{s_c`wTa=tZpcW8$Ow>G9VRy|nULRP zIU+8($+4zAj^yOLi*XEt8R$|T6qy{AJPizGh>j{dHflU=UOn9#H%NUs`FLo9&NaT} zHu$bI_F78|cH7iztVWw+R0RynD^P}F2UDph`Q|O$0)t0~wFZSJJ0rr@3OEIGPV6pP*?R?VhZuXVo0+&sH(kSXB>-Gvz1|U86v~bivfhWBG@7PmUBh zBQjLtE8&9lPTZU`WhEjb+|qsUeS(miR{eyr-==_oN|F>|VEptMV+6tswfLS3)w@Hf zpoM`qI?sfz=x~e3am}RmV${TwtcKDfcZvex>+4F;bXJcVQRhA$;G{^`vbQgcEiEZ2 zG0xp6*?j^`AJ*!_-GxTAWyXZLV%h-+{nOx{ZNw#wINeInqP7m^gFCg|~lW^TA zV`SoW-P*e_ch{q>!oQw3yw7H0v3fo2%{e;i(XJk&3&A=v z>hOmnWy|+&-y>?iMwk&QDZ*10m{X9pIOZd$eBbsSnw z=tzo%Y;u#zFM3QjGoFG?Q8n9r^NGo9Qn}SJiE)Mx;JM&Mn`c^nq;=&;U3PfUHG8u% zn9LM8Dv_=+2Y@ja>`>Cro?~SKnV+mYo}*J z4XvSPbA`e$;Hj#}1EvDQO5>b|PG9X$_EQ@9lo8VraOGz@9By)@-U2`7sov$QWOy&z zyt~s0^j7rgURz7%BG9_F^vJkwlsKj&;zevA{|%1}M8+|Lib$nvj-)JMfz|scWSnq( z*kp=iUOD@t(jhg=vPXo)YKYHBmSg;RE9E2BKy*|6>TkA;W)MwX&n@B{Eg1zhi$+iC z7dj+FN?vkQ+gZaL=A?l7^UfOB)8d5r+7V1=`^_k48}g0WvYora!q`{59FW-;!W)l0M=EILy#=EoguvScF0 zM>~=O>?9{e)Z4gV_{W)Iq{sBUv~#EwtzNAOU=&#bY1C~fs!tpIs2tnJ3{8RHDcxNl3}KG z1kylbbJ!r2$?WO_K(<;OixhAi<=P1@D-rfxpRHVabAD1Mqa-Ehjn(kQdh$dg^*{!4 z0~_c4T4B<6wsLwqBBSyNL7XnT?}nC|os+^03!%So@Q*IT`Ocd^Q|>)Bx~oR_%E9)7 zeEW>+TlD|ah~rJ0k80tJOW&W%G1Jc#x60qGVrA-mU&FB<4Evpf3vBU?e-3TG# z&{HbCTFZ+pY53>?{iNw-jOtS_kaLV5BE2vj0l(GkfKl}@!|!n^T83<>4xI&A9Ww|(ae z*ROxeA!47nFORaObD_PeL>HlgrEr$4=AQG^H|(eFmlhgnd@UTb@Iy>Gakk+l^hOk& zCJor}e|)mZALO!TS3mtDP-gJa@P+t4N#_M`Wa9&z1C>+~o8f_pO@px4h-oK9T1F&$ z`cJwy`E6N3Y?ZU-Jv}$O5G=fA+$+=X;Hq)XSXv`M8+~psB3%lu630e%7GOmhR<-y&v$fVp5Bh;%9Su zwJ*z}AL811naSTtV#zX^)_Fx;$Mp(cnXeR4Y-9~IMted|lepe#!Ly!t^U%8`lX``&S&meYma!NJI}D|--pBuWF}L+c%qTKRm|ExP zyk(^`#2Le}o$tc_fj+<1=)>Dcdou{$&u z?#-oijT7_T$|`nkS6w%Ihj@hci@&C>Y7joXH1=P)N60@4{aJ{wwZdvsE@%*nj_QM~U9Jr-3dh&tOw;lvyl;^rspM`Ww|FUkKZ+jo`qI#zn z^di7r{^x?`e~ladJ2$BPQ;1j#jr7r4^FM!RDZR=jO~%Q{SQbb>|97=w|1+pgx|2UJ z^w7}7f%htKeaQMhkG+`8iqIN++!;E_jDd_FkCD@qzT|;@tTlY%KO8 zHVHdcZkDkY3fqrw)CyAU=)_{XIyxGeAOD?m6_C=D>$B-ai4e}YryQu}s~9xcJoGLY z#-~xs*$-Ez_?t+8y9qG7>y_t4?N55I#NA;D)PQIPi_p1)aP7VqrPlx{EAYBfj$T8; za_0i-*ihXmZnWXxdY7fDdAQs;o|E#uu%z&>JWO=V-`%isK|`CQOJSTKD^R$fCddpT zkzb@8AjsMqedJ2>BF!GFTI$Tjfd~S;$`6S=j5%)KB?yC7FbCAg3%c-M?~e@VH9{qJ^WFW6}wSl1n`ocKE?H22#vrHem&INp&Y^y9MFz9o-XZ`FMNt!cc zzbfzqMtS)IXrlt_BmiF4q&P_0>ysDG-;t-NZ?!(=BjHA1YCY9`jx69i@SLe7i<*=a zDbo~8_iYh3^^zr$UiNzUUj-%N!NA*$`l8RB>5P;*de%Xyb8ThR3pc;cotpb3C(664 zewR=b>oMlGOC8Qk6Dxq3JKh)cqw8#KJSG?o|Ih}|pEow7_H<3Y;Hbmo6b9jgL58EF zvO9ZEd$I)G^XL7ZaH8P`moC34yZ=y((#KYylnctdHz2D9{&;eVTCzQS2CO zWfpV)MQ~U(x7xW25Cy6V)yjZ!%IJn-JU+CJjJ|02(+tp*(b}Bi*2O{3d4`&L6R}m~ zcf2@{GhKn2wuT??Hl7|^Y%hSLX*rmE&*sT)>gm2=DvPeW*?Q;sF1E3bi0-=j_rIfkCH(XOQ4p@b-#$n2MfR_> zBt^<)W$gZpy$@tz7`|nW!8k);rK+4f=ewj`9$LxIpelK`}npX)#1*OYK0FY1D)5J>0zx<*<{mNCe~5f z;H~#>gORSg`G*(;t4PX^Q1LW42_Mff@+auy^Rc4N+NDooO*&-=2L4Wi>B%Z0r* zfey|B;L3e2;VBP<><|3G`M9a@Dzm5+_Go4iYoy@ruDSN%V;35OO{#yn-5?|F9t?I= z|Av+Lb=v7!w_A5KG;on$LOp$xkJ3v-258FiAP>;G!*2s0Re{*dlFaEq>8^ z#3&asAj3-)_N%`1vMUJA5GCj&`IN(m4s`r0dqB5n#^l7lw?YyazKS~(&$L$aPxcf8 zSs#L?rbe^3=){D%);xZ!E$mwk-(@0v(9~s<@M#q<|2WX;kd{WV@8G1YM}TZkX@lilInW+m{Cs_HIB_TQKZv z2&4x=7un2hNS1r=S&x8)!3Nf7 zxN%2!dQwy^<3m;w@LtyBvk{tl0k5Oxx3-K!O%OjUE6seCzygDB(^6>Z3rpGS#25XKa~68jkK zJ>E=5E4(xdP`$rGk*adr5t4gh>xt*mp5pSKxiTl|8WYW%;V?1oP&HqQi_W=$5H(n10q%@8|A8bdPwCNN*g5uW$gl;ezUTfI`bvTY0} z@*f5(=G14RJC{a5y5O&NEU#97koxuivmE~k%K;V}$i@eSfO0JhF)M31y!z#LH0J5{ zo@bGLVqTyrV|+?zg4pWTtGT;!=s-)FIMDoFP5AjG_g%RGIx(WAn3qcwmkoGc!G?_k zuuy|aftU$T=zO5z&byEi*X_*=g14&+Sh*rPM*BlCV%mXbg=4?Mr4r<0xG)|8Dx{FmxlL455=T&}KI@W6we7sx9vG zbaC!3@^h@H=Y9>JHm|dP@cmc<)NJ%3p{y`s4A<_P2C@^KNGJDrv1-wN%uTdLqAH_%&+*qc|*Tz+)l_< z9EaE;1{jRQziRN^?~C?kT6zkdUz=rkVOb^K7n^UL5lADwuM*Op-VQ=_0xo;@kWG4j zRGIHwwYi14w;2sQc>;WngbLeG0#r*& z>5={t5=h<>LyfxUFt9yQ)^1@RQHfP(~&7o0bHNO%xFsLplTqP%{QMMNWr&rM~*gAPhI6XxD)1!2YBsQ$Lgao zD&^*K9CYDjpM4K6&3TsF;=ryn)%w=0TaTi#qGE0TUf<$~yw)KX9#Q9*KKBJ#^k>(=@8iHf(4LNqn&Iz{D71fD1bL`X-Ohr9q*W}4 zic0%vyYNEcQ;~Jv`9@00gRC{%3x_gStcrTmu;Z)}2UsfuiLMy+KR)d&iMHgPkqNR| z>H(^xA;<*)hU1Hek}$UvhpSnt8&QU-O^l^;VmiEpJ`pG{i`8==uEJ`K#}qmI_WFF>&OR`{}YlGk)Gn16G4qlrOMz)TH zbj_`Y+&;aC(Djn%fZ8n=pSaV3?+yKC1Bhk|FC}rp?&5JvOdnW-WB4^cKjO|)MgEH0 z+Cs$_Y9$|B*|I*jy~pw&;u&7$8dtVRz3gg*LM@0vrG35To1X!MqfJRaow%NMwxqbG zT-ZXW#je3;9K&V(1clweA)K_3c! zw6`Z}dcUjbm)zOA)>wPE3p}x+qvt+5j7!10KRJluOLS^L$ZU}vR^zzgU?bngiF$R% z+FABzNJNFeN{uuup^cZ>PEE8Dx}Pr~pDZ006HUAb+1T%gJ%Vwxgn+f8R1=;9lZ+mv zWv~kvAx}y?Ik^(6+h?xoq=1h4N63*l_DQ4HLJ3OlGOqLLYsFUOIhxRW!)apeIsScs zp%dW0V>k55?ZGPkD`nJdL$ANraZY6li|1>Uo^O3kR6lUJL26NA6Z#Om7rg12Nn*xc zhT<|{K<`a9-8wNi1HLUTeaeme{^;N!5vq~2#p~S|sRfQ}3T+basjx+lEE!WOfWPle zFKge=_oG#IvtRw~^^WD~%sx&l6ZrjkQXq!Jki;+rLTN5xa>N< zeRJA1*4hJmZe%lVi3*3q)!4L!qud7j>W$O9#7AMz#~Q_Ip(JeO#>cRM{k!2e__@HTJAPwo<8AbHq4ZgSp=0wyzo+)==xqbx89@ z&As|$VN^T>-Hx9YWhEaV$1?KXD*Motk!-u@@M z?(0~tvu}@77t8b0+*-+g=gBe-0_-c(|h;nmq{@~NFxPsr* z#+C({B=@(3bcuKgw?s;RM9k#sp`n^thWij3dQ}jAL`Zco5nh;gr@o7~b<-7hK6g8i zeNmoag6g@lqwT8*QBLTZ9$C)T^q)~>TPpD1F^WxL<>%&#^@qsE?bh_`DU_fq3FW1J zdu$?m&6~TImj|M*k~O`}GIsP!%dO?itsNcL+nkcQ|1UHLK!v_6@%=GZi+FV~zO~Dn zcIbEcOV7yidXShynS5G~LLwKgfykp^)v&|%j7Cg#H{6 zZJp?lu<=flGh{r=61fmijX+1LsX{g`2_#k8;yf6wUQp+ikbBIpJ>M?x6`_TK@2RgW+L?|oiZt|% z{m01HDilWDXstrRMfx?F!MZI}4b^jAbL^6VDeuN*z-wW*X zJS3mFA?laA{vBDQouvFjtQ_Q|H};(KBpafxmqOerc1RMtsp zLZxN5SiiXTRyzPrI`0qhD18a}%S*-oNLuVy)^ypOr~vV9>esmCd0usri1#eL_T=ZL z+F#hnJ&v>|g~w&rNKf>C5=l&m$)o>@v|i)sksC!a5G4>6)cOPh6Wj`)n zLE`V1fK`wF>8A~2S9ynyoHl8QN{B`GweV7ajW3_j8q+@C2KfDOmjE#`iJ!Fbdqj2Ujx<>1}k)piBCz$WX>A%PP{|| zZug%mUqIs&yJ)PZNA{MgUQrQM>CSlr4=f=){}Wui3Y>rWd|Q^~^}Zs^z_a~HQ6v8$ zNxwxlsvS5ad#eSn^o@e|IRDS)q$PdQwQe#$ojRhHHl-vzOaCWU5dEJ5s$Urh*+lc{ ZIVxkl7_Uo1zZR!alv9;0eEiJ+zW`WpO