From eb56efbd9e406b50b9268fd4a82972cb3e485f03 Mon Sep 17 00:00:00 2001 From: Shuery <2463253700@qq.com> Date: Sat, 19 Sep 2026 20:27:39 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=AF=94=E4=BE=8B=E9=80=82=E9=85=8D?= =?UTF-8?q?=E3=80=81bgm=20=E9=9F=B3=E6=BA=90=E9=80=89=E6=8B=A9=E4=B8=8E=20?= =?UTF-8?q?fps=20=E9=99=90=E6=B5=81=EF=BC=8C=E6=89=93=E5=8C=85=20v3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 取景(issue 04) - 新增 viewport-fitter.js:让立绘层缩放跟随背景图的 cover 缩放——背景放大多少倍、 特效就放大多少倍。原先背景走 cover、立绘层走 contain,比例一变两者就分叉。 16:9 下与作者设定逐像素一致(kv37 为 0 差异)。实测证伪了"contain 会裁掉作者视口" 这个最初的判断:contain 在任何比例下都完整含住作者视口。 - 应用方式为原地改写 config.viewport 字段后调 setViewport(),不重建播放器; 维护挂在 config.frame 上按画布比例增量进行(config.success 早于 setViewport,拿不到视口)。 WE 属性(issue 05) - project.json 新增 bgm combo(所有壁纸音源的并集 + 「随预设」);所选项不属于当前壁纸时 音频回落该壁纸默认,选择本身保留。 - fps 限流包裹实例 drawFrame 并接管排程。修掉两个实测缺陷:朴素丢帧会把 rAF 抖动放大成 整拍丢失(目标 30fps 实测只有 22.5fps);fps:0 被真值判断吞掉导致无法取消限流。 打包(issue 06) - project.json version 升至 3 并更新描述;preset 的 combo 取值保持 xilian/kv37 不变。 - preview.gif 821 KB → 347 KB(每 2 帧取 1 帧 + 128 色不抖动,逐帧与源帧像素完全相同)。 - index.html 加内联 favicon,消除 CEF 自动请求 /favicon.ico 造成的 404 报错。 其它 - dist/scripts/wallpaper-engine.js 移到 tools/:它没有任何入口引用,且环境判据依赖真实 WE 并不存在的 wallpaperRegisterListener,一旦按它自己的说明挂进 index.html 会在生产环境自我激活。 - 修正死文件检查:原检查会拿文件名匹配到文件自身的内容,使这类文件漏网。 - 新增离线算式验证与 resize / 属性 / 全量验收三套浏览器测试脚本。 --- .gitignore | 4 + .scratch/fit-measure.ps1 | 26 + .scratch/test-acceptance.mjs | 135 ++ .scratch/test-props.mjs | 137 ++ .scratch/test-resize.mjs | 126 ++ .scratch/verify-fitter.mjs | 72 + .../issues/04-aspect-adaptation.md | 68 +- .../issues/05-we-properties.md | 41 +- .../issues/06-package-v3.md | 36 +- .scratch/wallpaper-optimization/spec.md | 8 +- dist/index.html | 2 + dist/preview.gif | Bin 840520 -> 355310 bytes dist/project.json | 38 +- dist/scripts/index.js | 44 +- dist/scripts/spine-controller.js | 130 +- dist/scripts/viewport-fitter.js | 64 + dist/scripts/wallpaper-engine.js | 974 ---------- .../0003-spine-follows-background-cover.md | 36 + tools/ascii.mjs | 109 ++ tools/frame.mjs | 54 + tools/subject.mjs | 85 + tools/wallpaper-engine-simulator.js | 1589 +++++++++++++++++ 22 files changed, 2766 insertions(+), 1012 deletions(-) create mode 100644 .scratch/fit-measure.ps1 create mode 100644 .scratch/test-acceptance.mjs create mode 100644 .scratch/test-props.mjs create mode 100644 .scratch/test-resize.mjs create mode 100644 .scratch/verify-fitter.mjs create mode 100644 dist/scripts/viewport-fitter.js delete mode 100644 dist/scripts/wallpaper-engine.js create mode 100644 docs/adr/0003-spine-follows-background-cover.md create mode 100644 tools/ascii.mjs create mode 100644 tools/frame.mjs create mode 100644 tools/subject.mjs create mode 100644 tools/wallpaper-engine-simulator.js diff --git a/.gitignore b/.gitignore index a0f9299..178544c 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,10 @@ tools/shots/ tools/.cache/ +# 对拍中间产物:截图、抽取帧、试压的 GIF 都是可重生成的二进制,不入库 +.scratch/*.png +.scratch/*.gif + # 无损音频母带:只在本地留存,绝不进 dist、绝不入库(体积大且与发布无关) masters/ diff --git a/.scratch/fit-measure.ps1 b/.scratch/fit-measure.ps1 new file mode 100644 index 0000000..f329039 --- /dev/null +++ b/.scratch/fit-measure.ps1 @@ -0,0 +1,26 @@ +$ErrorActionPreference = "Stop" +Set-Location D:\Workspace\Temp\SpineWallpaper + +$sizes = @( + @(3440, 1440, "219"), + @(1920, 1080, "169"), + @(1080, 1920, "916") +) +$props = @{ + "xilian" = "" + "kv37" = "&__props=%7B%22preset%22%3A%22kv37%22%7D" +} + +New-Item -ItemType Directory -Force -Path tools\shots\fit | Out-Null + +foreach ($p in @("xilian", "kv37")) { + foreach ($s in $sizes) { + $w = $s[0]; $h = $s[1]; $tag = $s[2] + $url = "http://127.0.0.1:8190/?__freeze=1" + $props[$p] + Write-Host "=== $p $tag ($w x $h) ===" + node tools/shot.mjs "tools/shots/fit/${p}_${tag}.png" $url $w $h 2>&1 | Select-Object -First 1 + node tools/shot.mjs "tools/shots/fit/${p}_${tag}_ref.png" "$url&__hide=canvas" $w $h 2>&1 | Select-Object -First 1 + node tools/subject.mjs "tools/shots/fit/${p}_${tag}.png" "tools/shots/fit/${p}_${tag}_ref.png" + Write-Host "" + } +} diff --git a/.scratch/test-acceptance.mjs b/.scratch/test-acceptance.mjs new file mode 100644 index 0000000..ef4104a --- /dev/null +++ b/.scratch/test-acceptance.mjs @@ -0,0 +1,135 @@ +// Issue 06 全量验收。 +// ① dist 体积与死文件;② project.json 与代码一致;③ 三种比例 0 控制台报错; +// ④ 预设往返不丢用户级音源/音量设置。 +import { readdirSync, readFileSync, statSync } from "node:fs"; +import { basename, extname, join } from "node:path"; +import { withPage, sleep } from "../tools/cdp.mjs"; + +let failures = 0; +const check = (ok, msg, detail) => { + if (!ok) failures++; + console.log(` ${ok ? "✓" : "✗"} ${msg}${detail !== undefined ? ` → ${detail}` : ""}`); +}; + +// ── ① 体积与死文件 ── +console.log("=== ① dist 体积与死文件 ==="); +const walk = (dir) => + readdirSync(dir, { withFileTypes: true }).flatMap((e) => { + const p = join(dir, e.name); + return e.isDirectory() ? walk(p) : [p]; + }); +const files = walk("dist"); +const total = files.reduce((s, f) => s + statSync(f).size, 0); +check(total <= 35 * 1024 * 1024, "总体积 ≤ 35 MB", `${(total / 1048576).toFixed(2)} MB / ${files.length} 个文件`); + +const TEXT = new Set([".html", ".js", ".json", ".txt", ".css", ".atlas"]); +const texts = files.filter((f) => TEXT.has(extname(f))); +const contents = new Map(texts.map((f) => [f, readFileSync(f, "utf8")])); +// 这四个是 WE 的元数据/入口/说明,本来就不被页面引用 +const ALLOWED = new Set(["project.json", "README.txt", "index.html", "preview.gif"]); +// 关键:比对时要把文件**自身**的内容排除掉。否则一个文件只要在自己开头写上自己的名字 +// (比如注释里的 "// wallpaper-engine.js")就会被判为"被引用",死文件因此漏网。 +const dead = files.filter((f) => { + const n = basename(f); + if (ALLOWED.has(n)) return false; + const others = texts + .filter((t) => t !== f) + .map((t) => contents.get(t)) + .join("\n"); + return !others.includes(n); +}); +check(dead.length === 0, "0 个死文件", dead.length ? dead.join(", ") : "无孤立资源"); + +// ── ② project.json ── +console.log("\n=== ② project.json ==="); +const project = JSON.parse(readFileSync("dist/project.json", "utf8")); +check(project.version === 3, "version = 3", String(project.version)); +check(project.preview === "preview.gif", "preview 指向 preview.gif"); +check(!!project.general.properties.bgm, "新增 bgm combo"); +check(project.general.properties.preset.value === "xilian", "preset 默认值未变"); +const presetValues = project.general.properties.preset.options.map((o) => o.value).sort(); +check( + JSON.stringify(presetValues) === JSON.stringify(["kv37", "xilian"]), + "preset 的 combo 取值未变(用户已保存的设置依赖它)", + presetValues.join(", "), +); +const bgmValues = project.general.properties.bgm.options.map((o) => o.value); +check(bgmValues.includes("auto"), "bgm 含「随预设」档", bgmValues.join(", ")); +const gifSize = statSync("dist/preview.gif").size; +check(gifSize < 500 * 1024, "preview.gif < 500 KB", `${(gifSize / 1024).toFixed(0)} KB`); + +// ── ③ 三种比例 0 报错 ── +console.log("\n=== ③ 21:9 / 16:9 / 9:16 控制台与异常 ==="); +for (const [w, h, tag] of [ + [3440, 1440, "21:9"], + [1920, 1080, "16:9"], + [1080, 1920, "9:16"], +]) { + const r = await withPage({ width: w, height: h }, async ({ send, evaluate, consoleLines, exceptions }) => { + await send("Page.navigate", { url: "http://127.0.0.1:8190/?__freeze=1" }); + const deadline = Date.now() + 30000; + let ready = false; + while (Date.now() < deadline) { + if (await evaluate('!!(window.__test && (window.__test.successAt || window.__test.error))')) { + ready = true; + break; + } + await sleep(150); + } + await sleep(500); + const state = await evaluate(`(function(){ + var p = window.__player, v = p.currentViewport; + return { w: p.canvas.clientWidth, h: p.canvas.clientHeight, + vw: +v.width.toFixed(1), vh: +v.height.toFixed(1), + err: window.__test.error || null }; + })()`); + return { ready, state, consoleLines, exceptions }; + }); + const noisy = r.consoleLines.filter((l) => /error|uncaught|fail/i.test(l)); + const aspectOk = r.state && Math.abs(r.state.vw / r.state.vh - r.state.w / r.state.h) < 0.002; + check(r.ready && !r.state?.err, `${tag} 播放器就绪`, r.state?.err ?? "无错"); + check(aspectOk, `${tag} 可见矩形比例与画布一致`, r.state ? `${r.state.vw}×${r.state.vh}` : "—"); + check(noisy.length === 0 && r.exceptions.length === 0, `${tag} 0 控制台报错 / 0 异常`, noisy.concat(r.exceptions).slice(0, 3).join(" | ") || "干净"); +} + +// ── ④ 预设往返不丢音频设置 ── +console.log("\n=== ④ 预设往返 10 次,音频设置不得丢失 ==="); +const rt = await withPage({ width: 1280, height: 720 }, async ({ send, evaluate, exceptions }) => { + await send("Page.navigate", { url: "http://127.0.0.1:8190/?__freeze=1" }); + const deadline = Date.now() + 30000; + while (Date.now() < deadline) { + if (await evaluate('!!(window.__test && (window.__test.successAt || window.__test.error))')) break; + await sleep(150); + } + await sleep(500); + + await evaluate(`window.wallpaperPropertyListener.applyUserProperties({ + audio_file: { value: "https://example.com/custom-track.mp3" }, + audio_volume: { value: 0.2 } + })`); + await sleep(300); + + const rows = []; + for (let i = 0; i < 10; i++) { + const id = i % 2 === 0 ? "kv37" : "xilian"; + await evaluate(`window.wallpaperPropertyListener.applyUserProperties({ preset: { value: "${id}" } })`); + await sleep(320); + rows.push( + await evaluate(`(function(){ + var a = document.getElementById("background-music"); + return { id: "${id}", src: a.src.split("/").pop(), vol: a.volume, + draws: window.__test.drawCalls }; + })()`), + ); + } + return { rows, exceptions }; +}); +const srcOk = rt.rows.every((r) => r.src === "custom-track.mp3"); +const volOk = rt.rows.every((r) => Math.abs(r.vol - 0.2) < 0.001); +check(srcOk, "10 次切换后自定义音源仍生效", [...new Set(rt.rows.map((r) => r.src))].join(", ")); +check(volOk, "10 次切换后音量仍为 0.2", [...new Set(rt.rows.map((r) => r.vol))].join(", ")); +check(rt.rows[rt.rows.length - 1].draws > rt.rows[0].draws, "切换后仍在持续绘制", `${rt.rows[0].draws} → ${rt.rows[rt.rows.length - 1].draws}`); +check(rt.exceptions.length === 0, "往返过程 0 未捕获异常", rt.exceptions.join(" | ") || "无"); + +console.log(`\n结论:${failures === 0 ? "全部通过 ✓" : `${failures} 项失败 ✗`}`); +process.exit(failures === 0 ? 0 : 1); diff --git a/.scratch/test-props.mjs b/.scratch/test-props.mjs new file mode 100644 index 0000000..cfea666 --- /dev/null +++ b/.scratch/test-props.mjs @@ -0,0 +1,137 @@ +// Issue 05 验收:bgm 音源 combo 与 fps 限流。 +// +// A 段验音源优先级:用户自填 URL > bgm 选择(仅当本壁纸声明过)> 本壁纸默认音源; +// 且 bgm 选择跨壁纸时"音频回落、选择保留"。 +// B 段验 fps:WE 不替壁纸限流,得自己门控。关键是限流只该丢帧, +// 不该改变动画相位推进速度——用同一段真实时间里 trackTime 前进了多少来判定。 +import { withPage, sleep } from "../tools/cdp.mjs"; + +const BASE = "http://127.0.0.1:8190/"; +const name = (src) => (src ? decodeURIComponent(String(src).split("/").pop()) : "(空)"); + +let failures = 0; +const check = (ok, msg, detail) => { + if (!ok) failures++; + console.log(` ${ok ? "✓" : "✗"} ${msg}${detail !== undefined ? ` → ${detail}` : ""}`); +}; + +// ───────────────────────── A 段:音源 ───────────────────────── +console.log("=== A 段:bgm 音源选择与回落 ==="); +await withPage({ width: 1280, height: 720 }, async ({ send, evaluate, exceptions }) => { + await send("Page.navigate", { url: BASE + "?__freeze=1" }); + const deadline = Date.now() + 30000; + while (Date.now() < deadline) { + if (await evaluate('!!(window.__test && (window.__test.successAt || window.__test.error))')) break; + await sleep(150); + } + await sleep(600); + + const src = () => evaluate('document.getElementById("background-music").src'); + const preset = (v) => evaluate(`window.wallpaperPropertyListener.applyUserProperties({ preset: { value: ${JSON.stringify(v)} } })`); + const bgm = (v) => evaluate(`window.wallpaperPropertyListener.applyUserProperties({ bgm: { value: ${JSON.stringify(v)} } })`); + const file = (v) => evaluate(`window.wallpaperPropertyListener.applyUserProperties({ audio_file: { value: ${JSON.stringify(v)} } })`); + + const seen = []; + const step = async (label, action, expected) => { + await action(); + await sleep(350); + const got = name(await src()); + seen.push(`${label}=${got}`); + check(got === expected, label, `期望 ${expected},实得 ${got}`); + }; + + // 载入即 xilian,默认音源应是「再度和你」 + check(name(await src()) === "再度和你.ogg", "初始默认音源 = 昔涟立绘默认", name(await src())); + + await step("bgm 选「昔涟」", () => bgm("xilian"), "昔涟.ogg"); + await step("切到 kv37(选择不属于它)→ 回落", () => preset("kv37"), "pv37.mp3"); + await step("bgm 选「版本 PV」(属于 kv37)", () => bgm("pv37"), "pv37.mp3"); + await step("切回 xilian(pv37 不属于它)→ 回落", () => preset("xilian"), "再度和你.ogg"); + await step("bgm 选「「再度和你」」", () => bgm("zaiduheni"), "再度和你.ogg"); + await step("bgm 回到「随预设」", () => bgm("auto"), "再度和你.ogg"); + + // 用户自填 URL 优先级最高 + await step("bgm 选「昔涟」后填自定义 URL", async () => { + await bgm("xilian"); + await file("https://example.com/custom-track.mp3"); + }, "custom-track.mp3"); + await step("切壁纸不得清掉自定义 URL", () => preset("kv37"), "custom-track.mp3"); + await step("清空自定义 URL → 回到预设默认", () => file(""), "pv37.mp3"); + + console.log(` 音源轨迹: ${seen.join(" | ")}`); + console.log(` 未捕获异常: ${exceptions.length ? exceptions.join(" | ") : "无 ✓"}`); + if (exceptions.length) failures++; +}); + +// ───────────────────────── B 段:fps ───────────────────────── +console.log("\n=== B 段:fps 限流 ==="); +await withPage({ width: 1280, height: 720 }, async ({ send, evaluate, exceptions }) => { + // 空 __props 只用来触发测试驱动注入(不带它就不会注入),既不冻结也不改属性 + await send("Page.navigate", { url: BASE + "?__props=%7B%7D" }); + const deadline = Date.now() + 30000; + while (Date.now() < deadline) { + if (await evaluate('!!(window.__test && (window.__test.successAt || window.__test.error))')) break; + await sleep(150); + } + await sleep(1200); // 让动画先跑起来,避开启动瞬态 + + const probe = `(function(){ + var p = window.__player; + var e = p.animationState.getCurrent(0); + return { trackTime: e.trackTime, duration: e.animation.duration, + draws: window.__test.drawCalls, t: performance.now() }; + })()`; + + const sample = async (seconds) => { + const a = await evaluate(probe); + await sleep(seconds * 1000); + const b = await evaluate(probe); + const wall = (b.t - a.t) / 1000; + let phase = b.trackTime - a.trackTime; + if (phase < 0) phase += b.duration; // 动画正好绕回 + return { wall, draws: b.draws - a.draws, fps: (b.draws - a.draws) / wall, phaseRate: phase / wall }; + }; + + const setFps = async (v) => { + await evaluate(`window.wallpaperPropertyListener.applyGeneralProperties({ fps: ${v} })`); + await sleep(400); + }; + + const results = []; + const run = async (label, target) => { + const r = await sample(2.0); + const row = { label, target, ...r }; + results.push(row); + console.log( + ` ${label.padEnd(16)} 实测 ${r.fps.toFixed(1)} fps` + + (target ? `(目标 ${target})` : "(不限流)") + + ` 动画相位推进 ${r.phaseRate.toFixed(3)} 倍速`, + ); + return row; + }; + + const base = await run("不限流", 0); + await setFps(30); + const f30 = await run("限 30 fps", 30); + await setFps(20); + const f20 = await run("限 20 fps", 20); + await setFps(0); + const back = await run("恢复不限流", 0); + + check(Math.abs(f30.fps - 30) / 30 < 0.25, "限 30 fps 生效", `实测 ${f30.fps.toFixed(1)}`); + check(Math.abs(f20.fps - 20) / 20 < 0.25, "限 20 fps 生效", `实测 ${f20.fps.toFixed(1)}`); + check(f30.fps < base.fps * 0.75, "限流确实减少了绘制次数", `${base.fps.toFixed(1)} → ${f30.fps.toFixed(1)}`); + check(back.fps > f30.fps * 1.5, "取消限流后帧率恢复", `${f30.fps.toFixed(1)} → ${back.fps.toFixed(1)}`); + for (const r of [f30, f20, back]) { + check( + Math.abs(r.phaseRate - base.phaseRate) / base.phaseRate < 0.1, + `动画相位推进速度未受 ${r.label} 影响`, + `${r.phaseRate.toFixed(3)} vs 基准 ${base.phaseRate.toFixed(3)}`, + ); + } + console.log(` 未捕获异常: ${exceptions.length ? exceptions.join(" | ") : "无 ✓"}`); + if (exceptions.length) failures++; +}); + +console.log(`\n结论:${failures === 0 ? "全部通过 ✓" : `${failures} 项失败 ✗`}`); +process.exit(failures === 0 ? 0 : 1); diff --git a/.scratch/test-resize.mjs b/.scratch/test-resize.mjs new file mode 100644 index 0000000..c883679 --- /dev/null +++ b/.scratch/test-resize.mjs @@ -0,0 +1,126 @@ +// 验收测试:改窗口尺寸不得重建播放器,且取景要跟着画布比例走。 +// +// 判据是"播放器实例身份":给加载后的实例打上标记,之后每次改视口都核对 +// window.__player 是否还是同一个对象、标记是否还在。重建会换掉实例, +// 标记随之消失——这比数日志可靠得多。 +import { withPage, sleep } from "../tools/cdp.mjs"; + +const URL = "http://127.0.0.1:8190/?__freeze=1"; +const sizes = [ + [1920, 1080], + [2560, 1440], + [3440, 1440], + [1080, 1920], + [1600, 900], + [2560, 1600], + [3840, 2160], + [1280, 720], + [1920, 1080], +]; +const stress = []; +for (let i = 0; i < 20; i++) stress.push(i % 2 ? [1080, 1920] : [1920, 1080]); + +const out = await withPage( + { width: 1920, height: 1080, extraArgs: ["--enable-precise-memory-info", "--js-flags=--expose-gc"] }, + async ({ send, evaluate, consoleLines, exceptions }) => { + await send("Page.navigate", { url: URL }); + const deadline = Date.now() + 30000; + while (Date.now() < deadline) { + if (await evaluate('!!(window.__test && (window.__test.successAt || window.__test.error))')) break; + await sleep(150); + } + await sleep(600); + + const stamp = await evaluate(`(function(){ + window.__first = window.__player; + if (window.__player) window.__player.__probe = 'A'; + return { hasPlayer: !!window.__player, error: window.__test ? window.__test.error : null }; + })()`); + + const rows = []; + const measure = async (w, h, tag) => { + await send("Emulation.setDeviceMetricsOverride", { + width: w, + height: h, + deviceScaleFactor: 1, + mobile: false, + }); + await sleep(420); + // 先强制回收再读堆,否则量到的是 V8 的延迟回收节奏而不是真实占用 + await evaluate('(typeof gc === "function") ? (gc(), true) : false'); + const r = await evaluate(`(function(){ + var p = window.__player; + if (!p) return null; + var v = p.currentViewport; + return { + same: p === window.__first, + probe: p.__probe, + css: [p.canvas.clientWidth, p.canvas.clientHeight], + resolved: [ +v.x.toFixed(2), +v.y.toFixed(2), +v.width.toFixed(2), +v.height.toFixed(2) ], + pad: [v.padLeft, v.padRight, v.padTop, v.padBottom], + childViews: window.__player.config.viewport.animations ? Object.keys(window.__player.config.viewport.animations).length : -1, + heap: performance.memory ? performance.memory.usedJSHeapSize : 0, + drawCalls: window.__test ? window.__test.drawCalls : 0 + }; + })()`); + rows.push({ tag, w, h, ...(r ?? { same: false, probe: null, css: null, resolved: null, pad: null, heap: 0, drawCalls: 0 }) }); + }; + + for (const [w, h] of sizes) await measure(w, h, `${w}x${h}`); + const heapBeforeStress = rows[rows.length - 1].heap; + for (const [w, h] of stress) await measure(w, h, `压力 ${w}x${h}`); + const heapAfterStress = rows[rows.length - 1].heap; + + return { stamp, rows, heapBeforeStress, heapAfterStress, consoleLines, exceptions }; + }, +); + +const { stamp, rows, heapBeforeStress, heapAfterStress, consoleLines, exceptions } = out; + +let failures = 0; +const fail = (msg) => { + failures++; + console.log(` ✗ ${msg}`); +}; + +console.log(`播放器就绪: ${stamp.hasPlayer ? "是" : "否"} 驱动报错: ${stamp.error ?? "无"}`); +console.log(""); +console.log("画布尺寸 实例未重建 内边距 可见矩形 比例核对"); +for (const r of rows) { + if (r.tag.startsWith("压力")) continue; + const a = r.w / r.h; + const ra = r.resolved ? r.resolved[2] / r.resolved[3] : NaN; + const ratioOk = Math.abs(ra - a) < 0.002; + const padOk = r.pad && r.pad.every((p) => p === 0); + const rect = r.resolved ? r.resolved.map((n) => String(n).padStart(9)).join(" ") : "—"; + console.log( + `${r.tag.padEnd(14)} ${String(r.same).padEnd(10)} ${(padOk ? "全 0 ✓" : JSON.stringify(r.pad)).padEnd(10)} ${rect} ${ratioOk ? "✓" : `✗ 期望 ${a.toFixed(4)} 实得 ${ra.toFixed(4)}`}`, + ); + if (!r.same) fail(`${r.tag} 重建了播放器(标记丢失: ${r.probe})`); + if (!padOk) fail(`${r.tag} 内边距没有归零`); + if (!ratioOk) fail(`${r.tag} 可见矩形比例与画布不符`); +} + +// 首尾同为 1920x1080:取景必须回到完全相同的值,证明重算可逆、无漂移 +const first = rows.find((r) => r.tag === "1920x1080"); +const last = [...rows].reverse().find((r) => r.tag === "1920x1080"); +const reversible = first.resolved.every((n, i) => Math.abs(n - last.resolved[i]) < 0.01); +if (!reversible) fail(`回到 16:9 后取景漂移:${first.resolved} → ${last.resolved}`); +console.log(`\n回到 16:9 取景可逆: ${reversible ? "✓" : "✗"} ${first.resolved} → ${last.resolved}`); + +const growth = heapAfterStress - heapBeforeStress; +console.log(`连续 20 次 resize 堆内存: ${(heapBeforeStress / 1048576).toFixed(1)} MB → ${(heapAfterStress / 1048576).toFixed(1)} MB(增长 ${(growth / 1048576).toFixed(2)} MB)`); +if (growth > 4 * 1048576) fail(`连续 resize 内存增长过大:${(growth / 1048576).toFixed(2)} MB`); + +const drawCalls = rows[rows.length - 1].drawCalls; +console.log(`累计绘制帧数: ${drawCalls}`); +if (!(drawCalls > 0)) fail("没有绘制任何帧"); + +const noisy = consoleLines.filter((l) => /error|warn|uncaught|fail/i.test(l)); +console.log(`控制台异常: ${noisy.length ? noisy.slice(0, 5).join(" | ") : "无 ✓"}`); +if (noisy.length) fail("控制台有报错/警告"); +console.log(`未捕获异常: ${exceptions.length ? exceptions.slice(0, 4).join(" | ") : "无 ✓"}`); +if (exceptions.length) fail("有未捕获异常"); + +console.log(`\n结论:${failures === 0 ? "全部通过 ✓" : `${failures} 项失败 ✗`}`); +process.exit(failures === 0 ? 0 : 1); diff --git a/.scratch/verify-fitter.mjs b/.scratch/verify-fitter.mjs new file mode 100644 index 0000000..165ee9c --- /dev/null +++ b/.scratch/verify-fitter.mjs @@ -0,0 +1,72 @@ +// 取景公式的离线验证:不依赖浏览器,直接对 viewport-fitter.js 求值。 +// 两条硬约束:① 16:9 下必须与作者设定完全一致;② 立绘层缩放比必须等于背景 cover 缩放比。 +import { readFileSync } from "node:fs"; + +const src = readFileSync(new URL("../dist/scripts/viewport-fitter.js", import.meta.url), "utf8").replace(/^export\s+/gm, ""); +const mod = new Function( + src + "\nreturn { contain, frameForAspect, coverScale, resolveViewport, toViewportConfig, REFERENCE_ASPECT };", +)(); + +const rect = (x, y, width, height) => ({ x, y, width, height }); +const fmt = (r) => + `x=${r.x.toFixed(2)} y=${r.y.toFixed(2)} w=${r.width.toFixed(2)} h=${r.height.toFixed(2)} 比例=${(r.width / r.height).toFixed(4)}`; + +const cases = [ + { + name: "xilian(昔涟立绘)", + R: rect(-2181.72, 1442.39, 4226.57, 3403.35), + imgW: 2048, + imgH: 1080, + }, + { + name: "kv37(「成为昨日的明天」)", + R: rect(-1419.8, -512.4, 2457.6, 1228.8), + imgW: 2560, + imgH: 1440, + }, +]; +const canvases = [ + [3440, 1440, "21:9"], + [1920, 1080, "16:9"], + [2560, 1600, "16:10"], + [1080, 1920, "9:16"], +]; + +// 背景走 CSS background-size:cover,它在画布上真实的像素缩放就是这两个候选里的较大者。 +const bgScale = (w, h, imgW, imgH) => Math.max(w / imgW, h / imgH); + +let failures = 0; +for (const c of cases) { + const imgAspect = c.imgW / c.imgH; + const V0 = mod.contain(c.R, mod.REFERENCE_ASPECT); + console.log(`\n=== ${c.name} 背景图 ${c.imgW}x${c.imgH}(比例 ${imgAspect.toFixed(4)}) ===`); + console.log(`作者视口 R : ${fmt(c.R)}`); + console.log(`参考取景 V0 : ${fmt(V0)}`); + + // 基准取 16:9 1080p:立绘与背景都以此为准比较"被放大了多少倍" + const refCanvas = [1920, 1080]; + const spinePx0 = refCanvas[1] / V0.height; + const bgPx0 = bgScale(refCanvas[0], refCanvas[1], c.imgW, c.imgH); + + for (const [w, h, tag] of canvases) { + const aspect = w / h; + const V = mod.frameForAspect(V0, aspect, imgAspect); + const spineRatio = h / V.height / spinePx0; + const bgRatio = bgScale(w, h, c.imgW, c.imgH) / bgPx0; + const synced = Math.abs(spineRatio - bgRatio) < 1e-6; + if (!synced) failures++; + console.log( + ` ${tag.padEnd(5)} ${String(w).padStart(4)}x${String(h).padEnd(4)} ${fmt(V)}` + + ` 立绘 x${spineRatio.toFixed(4)} 背景 x${bgRatio.toFixed(4)} ${synced ? "同步 ✓" : "不同步 ✗"}`, + ); + } + + // 参考比例下必须与作者设定逐像素一致,且必须与拟合前的真实渲染值吻合 + const Vref = mod.frameForAspect(V0, mod.REFERENCE_ASPECT, imgAspect); + const untouched = ["x", "y", "width", "height"].every((k) => Math.abs(Vref[k] - V0[k]) < 1e-9); + if (!untouched) failures++; + console.log(` 参考比例回归: ${untouched ? "与作者设定完全一致 ✓" : "被改动 ✗"}`); +} + +console.log(`\n结论:${failures === 0 ? "全部通过 ✓" : `${failures} 项失败 ✗`}`); +process.exit(failures === 0 ? 0 : 1); diff --git a/.scratch/wallpaper-optimization/issues/04-aspect-adaptation.md b/.scratch/wallpaper-optimization/issues/04-aspect-adaptation.md index 4d7e0ca..804a78b 100644 --- a/.scratch/wallpaper-optimization/issues/04-aspect-adaptation.md +++ b/.scratch/wallpaper-optimization/issues/04-aspect-adaptation.md @@ -1,20 +1,68 @@ # 04 比例适配:按容器比例重算视口 -Status: ready-for-agent +Status: resolved ## 问题 -viewport 以等比 "contain" 映射到画布:单轴贴边、另一轴多露出世界,因此现在的负 padding 只在目标比例下成立; -在超宽屏(3440×1440)或竖屏(1080×1920)下会露出多余的世界或裁掉主体。 +viewport 以等比 "contain" 映射到画布:单轴贴边、另一轴多露出世界。 +原本的判断是"负 padding 只在目标比例下成立,其它比例会裁掉主体"——**实测证明这个判断是错的**, +真正的病根是另一件事。 -## 步骤 +## 诊断(实测,非推测) -1. 测出每档壁纸在 21:9 / 16:9 / 9:16 下的构图偏差(已有对拍矩阵)。 -2. 按容器宽高比在运行时重算 pad(视口是活对象:原地改 pad 后调用 `player.setViewport(当前动画名)` 即生效, - 整体替换 viewport 会丢 `viewport.animations` 并在 spine-player 第 14973 行崩溃)。 -3. 目标:主体完整、底部对齐、无空边。 +先量清楚事实,再改代码: + +1. **contain 永远不会裁掉作者视口**。可见矩形是宽高比等于画布、且恰好含住作者视口 R 的最小矩形, + 所以 R 在**任何**比例下都完整可见。比例不同只是"另一轴多露出世界": + - 21:9 → 6453×2701(竖轴贴边、横轴多露) + - 16:9 → 6050×3403 + - 9:16 → 1914×3403(横轴贴边) + 三种比例下 `coversView` 全为 true,"裁掉作者视口"这条假设不成立。 +2. **这不是我重构引入的回归**。基线 `d8c7e49` 的 `preset-store.js` 里 xilian 的四个 pad + (-25% / -28% / -30% / -23%)与 kv37 的 `x: -1215, y: -410, w: 2048, h: 1024` 与现在一字不差; + 基线的容器也是 `#spine-container` 绝对定位铺满、body flex 居中。取景语义未变。 +3. **真正的病根:背景走 cover,立绘层走 contain,两者缩放分叉。** + `body { background-size: cover }` 会按需放大裁切,立绘层却按 contain 固定世界矩形。 + 以 1080×1920 竖屏为例,背景被放大约 1.78 倍,立绘层的世界高度却纹丝不动—— + 特效于是和背景插画脱节,"多余世界"其实看不太出来,脱节才看得出来。 +4. **spine 资源路径是 `./effects/*.json`:立绘层是特效层,背景插画才是主体。** + 所以判定标准不该是"特效不被裁切"(粒子飘出画面本来就正常), + 而应是"特效与背景插画始终同倍率、同中心"。 + +## 做法 + +新增 `dist/scripts/viewport-fitter.js`(纯函数)与 `SpineController` 的取景维护: + +- 参考比例 `16/9`:先让播放器自行解出作者视口 R,换算成参考比例下的可见矩形 V0。 + **这个比例下与作者设定逐像素一致**。 +- 其它比例:让立绘层的像素缩放**跟着背景的 cover 缩放走**—— + `V.h = V0.h × coverScale(参考比例) / coverScale(当前比例)`,中心不变。 + 于是背景放大 1.78 倍时,特效也放大 1.78 倍。 +- 应用方式:原地改写 `config.viewport` 的 x/y/width/height 与四个 pad,再调 + `player.setViewport(动画名)`,并把 `viewportTransitionStart` 推到足够久以前(避免 0.25s 插值拖影)。 + 绝不整体替换 `config.viewport`:`setViewport()` 会无条件读 `config.viewport.animations[name]`, + 换掉整个对象会在 spine-player 第 14973 行崩溃。 +- 维护时机:挂在 `config.frame` 回调上(它在每帧相机计算**之前**被调用),以画布比例作键—— + 比例没变就是一次浮点比较后返回,比例变了就地重算。 + `config.success` 用不了:它在 `setViewport` 之前触发,那时 `currentViewport` 还不存在。 +- 预设可用 `framing: "author"` 退回作者原始的 contain 行为。 ## 验收 -- 三种比例下都对拍留档,主体不被裁切、底部不悬空。 -- 切换窗口大小时不重建播放器(用 `setViewport`),连续 resize 无内存增长。 +| 项目 | 结果 | +| --- | --- | +| 参考比例回归 | kv37 **逐像素 0 差异**;xilian 仅 1/255(浮点末位舍入) | +| 立绘/背景缩放同步 | 21:9 / 16:9 / 16:10 / 9:16 × 两个预设,**全部精确相等**(离线算式验证 + 渲染实测) | +| 渲染实测吻合预测 | 三种比例可见矩形与公式预测逐值一致(pads 全 0、base == resolved) | +| 对拍留档 | `tools/shots/fit/` 下 6 组(2 预设 × 3 比例)改动前后各一套(该目录按仓库约定不入库,可随时重跑重生成) | +| 不重建播放器 | 9 次改视口 + 连续 20 次压力往返,实例身份标记**从未丢失** | +| 连续 resize 内存 | 强制 GC 后 7.9 MB → 8.0 MB(增长 **0.04 MB**) | +| 回到 16:9 可逆 | 取景完全回到原值,无漂移 | +| 控制台 | 0 报错 / 0 未捕获异常 | + +复现:`node .scratch/verify-fitter.mjs`(离线算式)、`node .scratch/test-resize.mjs`(resize 行为)。 + +## 附带修复 + +Chromium 会自动请求 `/favicon.ico` 并在控制台留下 404。已在 `dist/index.html` 加 +`` 消除,使"0 console 报错"这条验收真正可达成。 diff --git a/.scratch/wallpaper-optimization/issues/05-we-properties.md b/.scratch/wallpaper-optimization/issues/05-we-properties.md index 2b06bc6..61dc8e1 100644 --- a/.scratch/wallpaper-optimization/issues/05-we-properties.md +++ b/.scratch/wallpaper-optimization/issues/05-we-properties.md @@ -1,17 +1,40 @@ # 05 WE 属性扩展:音源 combo、fps 限制 -Status: ready-for-agent +Status: resolved ## 内容 -1. **音源(音源作用域已定)**:新增 `project.json` combo `bgm`,列表是所有壁纸音源声明的**并集**(WE 的 combo 是静态列表,只能手工同步); - 每档壁纸在自己 `preset.js` 的 `audioChoices` 里声明可选音源。切换壁纸时,若当前音源不属于新壁纸,回落"随预设"。 -2. **fps 限制**:`properties.fps` 经 `applyGeneralProperties` 下发,WE 不会替壁纸限流,需自行实现。 - 实现方式:包裹实例上的 `player.drawFrame`(循环用箭头闭包懒查,因此可以门控);注意 `config.frame/update/draw` - 都在下一帧排程之后运行,用它们限不了流。 -3. `setPaused` 暂不实现(WE 会整体冻结进程);除非用户在 WE 里实测发现音频仍在响。 +1. **音源**:`project.json` 新增 combo `bgm`,列表是所有壁纸音源声明的**并集** + (WE 的 combo 是静态列表,只能手工同步),另加一档「随预设」`auto`: + `auto` / `zaiduheni`「「再度和你」」/ `xilian`「昔涟」/ `pv37`「版本 PV」。 + 每档壁纸仍在自己 `preset.js` 的 `audioChoices` 里声明可选音源。 +2. **fps 限制**:`properties.fps` 经 `applyGeneralProperties` 下发,WE 不会替壁纸限流,自行实现。 +3. `setPaused` 不实现(WE 会整体冻结进程)。 + +## 实现要点 + +- **音源优先级**:用户自填 URL(`audio_file`)> `bgm` 选中的音源 > 本壁纸默认音源。 + 所选音源**只在本壁纸声明过时才生效**;不属于当前壁纸则音频回落本壁纸默认, + **选择本身保留**,切回那份壁纸时用户的选择还在。 +- **门控绘制**:包裹实例上的 `drawFrame`。播放器循环写作 + `requestAnimationFrame(() => this.drawFrame())`——箭头闭包在调用时才读 `this.drawFrame`, + 所以在实例上换掉它就能接管排程。 + - 原实现只在**真正绘制**时才排下一帧,而被限流丢掉的帧同样要排,否则循环停摆; + 因此排程由包装层负责,再让原实现以 `requestNextFrame = false` 调用。 + - **踩到的坑**:朴素地"距上一帧不足 `interval` 就丢"会把 rAF 抖动放大成整拍丢失—— + 目标 30fps 时落在 33.2ms 的那一帧被判为不足 33.33ms 而丢掉,实测只有 **22.5fps**。 + 改为"留 4ms 容差 + 到期时间按固定步长推进"后精确到 30.0。 + - **另一个坑**:`if (properties.fps)` 的真值判断会把 `fps: 0`(取消限流)整个吞掉, + 改判 `!== undefined`。 ## 验收 -- combo 切音源即时生效,且切壁纸时的回落行为正确。 -- 设置 30/60 fps 时实际帧率符合,且不影响动画相位推进速度。 +`node .scratch/test-props.mjs`: + +| 项目 | 结果 | +| --- | --- | +| bgm 切音源即时生效 | ✓(10 项音源断言全过) | +| 跨壁纸回落 + 选择保留 | ✓ xilian 选「昔涟」→ 切 kv37 回落 pv37 → 切回 xilian 回落默认 | +| 自定义 URL 优先级最高且不被切壁纸清掉 | ✓ | +| 限 30 / 20 / 取消限流 | 实测 **30.0 / 20.0 / 60.0 fps** | +| 不影响动画相位推进速度 | 0.999 / 0.999 / 1.000 倍速(基准 1.000) | diff --git a/.scratch/wallpaper-optimization/issues/06-package-v3.md b/.scratch/wallpaper-optimization/issues/06-package-v3.md index 9b0b24d..4fd5370 100644 --- a/.scratch/wallpaper-optimization/issues/06-package-v3.md +++ b/.scratch/wallpaper-optimization/issues/06-package-v3.md @@ -1,10 +1,34 @@ # 06 打包 v3 并重新发布 -Status: ready-for-agent +Status: resolved(第 4 步为人工步骤,待用户执行) -## 步骤 +## 步骤与结果 -1. `project.json`:`version` 升到 3,更新 `description`(提及新增音源切换等),核对 `properties` 与代码一致。 -2. `preview.gif` 现在 160×160 / 50 帧 / 840 KB——合规(≤256×256、≤1 MB)但超过推荐的 500 KB,重新压缩。 -3. 全量验收:0 死文件、≤35 MB、3 种比例 0 console 报错、预设往返不丢音源设置。 -4. 上传到 Workshop(人工步骤,需要用户操作)。 +1. **`project.json`**:`version` 升到 3;描述新增"可切换各预设自带的背景音乐""自适应窗口比例"; + 新增 `bgm` combo 与 `bgm_note`。核对:`preset` 的 combo 取值仍是 `xilian` / `kv37` + (用户已保存的设置依赖它,绝不能改)。**完成**。 +2. **`preview.gif`**:原 160×160 / 50 帧 / 25fps / 2.0s / **821 KB**,超推荐值 500 KB。 + 重新压缩命令(结果可复现): + + ```powershell + ffmpeg -y -i dist/preview.gif -filter_complex ` + "[0:v]fps=12.5,split[a][b];[a]palettegen=max_colors=128:stats_mode=full[p];[b][p]paletteuse=dither=none" ` + -loop 0 .scratch/pv-I.gif + Copy-Item .scratch/pv-I.gif dist/preview.gif -Force + ``` + + (**不能把输入输出写成同一个文件**,ffmpeg 会拒绝;必须先输出到临时文件再覆盖。) + 即"每 2 帧取 1 帧 + 128 色 + 不抖动",**逐帧与源帧像素完全相同**(第 k 帧对原第 2k 帧, + 抽查 0/6/12/18/24 帧全部 SSIM 1.0、max|diff| 0),只是时间采样率减半: + 160×160 / 25 帧 / 12.5fps / 2.0s / **347 KB**。**完成**。 + 注:减色到 96/64/48 色收效甚微(604/519/474 KB)——体积由帧间变化量主导,不由调色板主导, + 所以走帧采样率这条路更划算。 +3. **全量验收**(`node .scratch/test-acceptance.mjs`):**全部通过** + - 总体积 **28.52 MB** / 34 个文件(≤ 35 MB) + - **0 死文件** + - `project.json` 解析正常、version 3、bgm 存在、preset 取值未变 + - 21:9 / 16:9 / 9:16 三种比例:播放器就绪、可见矩形比例与画布一致、**0 控制台报错 / 0 异常** + - 预设往返 10 次:自定义音源与音量 0.2 均未丢失,仍在持续绘制,0 未捕获异常 +4. **上传到 Workshop**:人工步骤,需要用户操作。 + 本次为破坏性较小的重构(16:9 逐像素不变),但 21:9 / 9:16 下构图按设计改变,建议在 + 超宽屏与竖屏各看一眼再发布。 diff --git a/.scratch/wallpaper-optimization/spec.md b/.scratch/wallpaper-optimization/spec.md index b2060de..8b50dd0 100644 --- a/.scratch/wallpaper-optimization/spec.md +++ b/.scratch/wallpaper-optimization/spec.md @@ -35,7 +35,7 @@ | --- | --- | --- | | 01 | 资源按 游戏/壁纸 分层,预设拆进各自壁纸目录 | 完成(`09ff322`) | | 02 | 体积:音频转 Opus + 图片无损 WebP(105.1 → 28.9 MB) | 完成 | -| 03 | 健壮性:初始化状态机、死代码、用户级覆盖语义 | 待做 | -| 04 | 比例适配:按容器比例重算视口 | 待做 | -| 05 | WE 属性扩展:音源 combo、fps 限制 | 待做 | -| 06 | 打包 v3 并重新发布 | 待做 | +| 03 | 健壮性:初始化状态机、死代码、用户级覆盖语义 | 完成(`4662856`) | +| 04 | 比例适配:立绘层取景跟随背景 cover 缩放 | 完成 | +| 05 | WE 属性扩展:音源 combo、fps 限制 | 完成 | +| 06 | 打包 v3 并重新发布 | 完成(上传为待人工执行) | diff --git a/dist/index.html b/dist/index.html index 2d72e4b..b6461e6 100644 --- a/dist/index.html +++ b/dist/index.html @@ -4,6 +4,8 @@
nyq>4=ppF4DfP-E(yp z8YIpbg}$?b(^AxW*QhOg#M$E(BqfAQBpnQtTCnL{t=jR%B_ge{d)CW@*E)0k(a~O= zUW+|X2Yg;S{B$d;oTjM48snnAYc&t9f(Na)gRc~QSH>IU*KseRiY=C|&)w)~puUG> zSy!nFamGAYp4m%Gj^4H_-jE;{MkltV8#Gp3veL7UJL8p8{CUK5NMrXYUpRXADf4aK zu2*A-s~1rTLEJuFtkM-uce5FnSOu3k6$gr(*H{h81{$CETK72MW1zH!;|Bl;TF;O? zW=T3wgmsKcgGlYND**H~*i=$0OW(zdo60Bbs87EWrs7++qbu;GgX1C%_0LXHx7aO{ zOP?v;*|Fwz^svV9*;9(E%Ql|)a_ YeWY-Q+bAN4JDyeUT6YV-m eU&i+3fYT8R}&q{{lr9Yw87Q;%CYi>dK@98kO6B_ zg79pl|M9SEF72(qmCm7eTAdZVj*P)zn6DLSDUzoQTmG{@+Y6Of2(o#EcnV&n*Og{7 z(($Os(L@aX7|LJMES8EOHcfkp@^ cfiK1YJu*TpHdaS6rVJX@DjEZL~dVi5fF zl{Wds61w?pmjndD22mSw8mDtAE7asV$}%4^S>AjOf>P!*?so;ys+LbtHiAZj^+uq2 z$H1`9=vG%1>YjLqhp8I%XMNcHB+Ep)>`D-y{>)iEf`i(3lv9c)KG=7(=k&MVM%MTW zL|)wsKDT~;Tk!K%thFSVqRasAoY$s&&a9PNwI`!?@0$f`0&ZS=klFrITHt8`bSktA z6An+GnR2I*>lBW F(|o;22CnE01Bzh0y1Pq^op&=&RN?Y<&l7H*Nl0`l zq7Ikc{9a8 Ja06?lxRQ@9mC6j81TBy{k$_8-j08ntqu<5}{)J;!PPZ|IC zh9WVvSG5c-zp?F`vD*6E3A 1I0|Y!%EHC9Jg}#PkVWO51!Dh*u-_G?W z>B|+9_f)}{?`i0dht4$@Wbkl>;uDhAbWmV0eJl0awV~Wp`_rt&`JQoqgTxuXZZ3Mw zF$VSp`iM_8@s-tcMya>Iab-!r?zeOVJloV)cj3w03-J&tvd92kb`3he%MGJArWFYY z4GorrMm{tPh}IUA@gIV>E-M9Fmu^W*^G=<^Xja?e-vzAw_^3puUm>p1Z2Q7t`1^0f zsI%j^eBsk^v&8#2(Kk&Zm+C%xQkpE!=EIU|lf~=XPW?;KW)f7<8a9laLzd|OYN^lN zx6Hn5H{++APJVG@^$Ddkw1*E-7+^7;w_!WHBlTJK5&tDU1uoU^IPh4CO=aSpxNzXE zt5SMm20ry9lb389?L)0UYSfg0nIg?FPD00r;za8R&r#Zz0K8>Bf6L}z45?$1)szd> z*)U-Ya36znT99w~z485o&8|4X5w#+Rxp%!kuG@#h8UwR)L4{OB8zD#)Q!cTtg3`t> z`0}OAz3G0;?%&t)1+nS9T+kq@_q+Qw dz$JfZ60Ib_B zTonf2 C(gILNb|>p~P^J&; z(Gh|n73AX}T(>pTk?rAxMyD~2gW}TkNeVh*8cCEB+W8N8O#p~!LslS02zo%!u4Cq3 zXScoX(yW9)0ym#3g2~ QLP+aN~kbnxGP zL#Z>MYD(^!u)VB`tO$ws7dQK>X=%z>P+Lr+ApZnea Pn!n)#VL z?A2|WcDWG@1GdC3%yv^?oEq;Ps3c<2B O4UI zus!RlU<94|Uj`}!2X5e^*%jzeQMPLZdiYp2oz1uB7Plz#n-ck!5-^;Y{Kznl<4k_B zyS!_j7DWVma0!3h%OankRoQ!tsOljurI`aLj}BB&T*@8Ln}R^mFc|tc$B`2%BLRG@ z>^4sLdp!kp*n4iy?J50+a^$CaQZrmu&y=T5mj9>B*AhbxoM=M<>h9KPBQdlovBIGG zh^hcI7MAPdP^t`=^ffC&fHI(#L<1;$G0KUvkxfmG0-&B1l+3e@krJ@#X&{z-^v70@ z<|A6^6Pr=Oy;c&mEA?<_6FQLXOZ!22oB@tn7x*AY*5ya*iGw1W!NX50{YT2#Mpb1C z1)RcSzmclLS1X hY8K sZn>7ojBW+r3k=d1 zfd5fW*omPiV(EI~(ZRw)s(TcBJV89$EU*Hg0O*yi-iFTrDi c&;)f z<|d6yuRy6ws-s4#oT;!mxANk(lXaot>-LnhlgjP6r`K-d>$jBKiD4&pMq4%X%iz@4 z<0vy^3r;1xEj&oAx;m1L#+*f6PO|+c%%k^8?pAOYr2~Ad19j-te!#*+E~vt-S0q4d z*=wyp^k(HcYQ#qU3V79q?7-PrQFPj_19rPg`@IW&@foB&T>kF(N&540-|%8%apkkK z(1z6t*jaM?*SiDsCX~rymD-A5|7lH&kRXwV(Mm`-ol4HsOUntd59wD>5`vd5qKE`= z=_&6rJ(S96zTAVdo zyY%;x6nr~k{F$H)^3UzFFwx D#`o@+xTnuQp$|s^E9SY6~uX8rH?}&)O9>{1@;!4bn9%;=c4aE zP784{e!UDSQ^De703!qfCYUZI!1C_k{%hce`YYjKy39dfT|%*HR ;rlu6Iunx0(y^Q3xY%XrQCwLdR9g|0ZM=TcfwB#2mhQ+c4zhX({xp)$mhJVyH$BB~t--W QiA2E@MRJhVB`<&A7{kbnubz1GZLKcsB~&}H5cGvfx%=-;_nt4jQ- zsc|I+T>)%NzYvyS97w&LBMn85d0K~s$0iOlB)6m3H8ICgeq8uO9?H`Pl I1y=O8(6E1|51_BW!;^52NbA8tJ;Ik!^A91YsFyto$%UOK_ zpF!gjaHvS=EQS|WQ{YhXnKdU-UmZ`)j|&4Br*f2;;bO>hYLtPXpQ(&RaN) sys6g$k zI%D*!vZfbyWurMJ= KU#P@PLCo$Ii><^3;9DBFsxPp)X%f435c?%NB|rH3>A zG=myG57f_t$LL&c$m3g4dd4A;^j+|eHTlT{INGe1 eg@ zCl}om*$ZE7@nnduGN_ U-F2aKD))>W$%ez03=*tNSQa$hH%;Nr;9To@-!l z`#<2TTDlvDFy1T|7-DXTl2J|oY@0Up#q+`1CQl>aYA^@Ren0gzddyugHUE2Xs10%u z-Q$~~W6w`HS ^#fqJpnsZc{eRk@@*y{JZ z6KJy2#n)%s$k;#! ~cuYcZs{aD^6nqzyBq_0MeL7B%KJag3n6lk=)wT;{sy6)88(K8#O zA9Xrjzq|e2-4C!s1^Po49LhL#bo^at2b}imA=l)pgYapm`cwM~D70dr{CSp^U4hOA z;RaxE>P$(g`JI_T?)Kt6Ru`?GbiK}l(3Sn@k+0C`03eWE)TavGg+{B2C-8r+n?#&9 ze^}7^arjE!^mO!nKf%=9=w3=G`a|OTP&UesI>!)~IZ)?P%+a=%f_c{^)%KI2z^kuk zK`+rGueDt_PQXUn`0AFBF6U0~(v3;Yx;0TT_7|;vxyO#itlipZ%x`u%xtjA)U=CTf zXl|OHxc1*I2V!)i+Ng%lKn(I}+U(`X!FL07m)zN~0UO1(Tu!M&2V9t1yZ@m#7cCzI zXUn`Qtwp&WANqP0_4a*@+C0SK3B7;p&;cMF(RV|^A#?WZYDkR9 #OgV8??+)56D5Tf5`V)xij4A^l s1&HffDKn;w13gC_eoCl38@0+wj%()%w625zis*=uux)j(|n3m3jQynsn6`E(3 zlb`ed{q4K QE#&Y)c|J>N$5?{U7 zh(ZTiE(e#RPv3bOu;Ckzj3!tJpP5!^pSTo* {6MHq-s_2b9d&6;xh--pElic6^pZ(tjH-YZiSouR6X&p+GNvg?M=9 zh@V>2l`L=(8oGDV{U#$^C%%ZR9k4BaB;9RWm2qgJX=i!@;iySaiw`e7o=|lvtlND; z$JR?hYy19L_A=Asu%6%1S<4CM3$NrdcFvB2x*rdiMP ?W_f@(D#GBfl=6oulT4=Xaj5HnVqGyl4q-)=ku%`B{x0eZz)LU z<|*mB5}dV{nNHHf6`u1S5d4^F5AF29-udy%xwILmsgiw|YJ1EJY01@Mb =5U5mI9Q=ybxfsj=((|azv zBBemli(23QZ?^aoig-rAD^Q9OZ5&=!K77AGc9(L6-niw1!V;ssY>BR-D36c9`a3XD z_{auJ9T~4lXOwbE^<5XGZs7+hjr7T+!4r+A+g1q+ kG~~AE+&*U3AkuY|`)H zla|Fdx9H`|w)&hDK!v6q!)3)L?)r;nUs@b4 AvgW6$Ws_VT5C9yJ(I`fy=LKfc0}N9aJXuO@i- zAgZ!PqYxLE3(5YKdVOCeui~SmVkE@{dZ6)JY=&olaOGM}B^-Khx?1@_!&guy5rNG` zeUB={fF>7Q6UF9BPwAm-l41$iB*h=2b71A3Pv}SKuYYZkp#D4}VoV(C$TMp+<${}{ z!dDmy&u!7>>Rf^j!x?QX22~v`=qY`{m8HV2lZkFpp;pY`vfR#>FgD-Fbz|16kPTPe zEXscGk^a{BNLe*U2uHObDB6QOqgq#e8^)29=8CKF%Ytm$W*x8dHroHT-GOa`KtJO% z6iKO}<;p`{QW?iazp~xmF^i$<|5>P7F^1cb<{f#73oA{Mq{i&;1udlyJ{P$Wkc_G< z1LYg@3uofWSHt5sAq*IrHo(YAkndUS8opqGI_?l>cnI)fUn1c?BoFV+5kcCIHlkVw zMglm1zV=x`N#HeS`K4|J-ChK6ln7P#A!u{rI8MqXG%}A3(;5+65CR1sHd44Qo;^Od zJT zjf0DdQsWw=C+C!JObRb&f9`{g);^e~$6nyspVVy9$=At76T+$eC> M(cc*tzs{GX}2}cSXsLrt_5M@BK4@C>p zO$fJi(nLNPKC4%dyFv{b+x02in#~GWmyD9pOW^x;CI>3MJ6gXYZ6BUhOu8&U>$n0a z!$}~mEuj5Ybqnj%IaPk90B!7=Z|n)a(tQ71U2cCx&5tFC)7zs>t`Ses5YOmUFZ&^( zbmzg>GLTe@rITJy@TIp27f$r7Mca*vVG3(;>jnl*yRd8PjWunqJ6GActIR?AU#X7d z$K~T)0m7>^uYwB<8!gHLqF~mX-}mPA`W*8{jUmq|#? ?A@x(28Y-ER$4oDr{OA(FB?^% zW&aO=V ep=$xuhic%McdTduU=Ad}VK>2&VgzVEW3(b5Yq z1|B9Ex(uRyDtHlYg5HPBZKwU0RmxIZa*gwzb*p`gI&k1k?CS4l&s|2z)XGv{cr@fi zJZ^$lyM5&%b1N04ErR?kI`~HfBjH-SdmEb>CMk21sP)7q;yD|%P9f*!aqom3oWX^A zds1)h@JyvRnnf8DiiJ#JY_MtXggEn{+0)?8g1bJUG-lOp=$~>a1VC@x`%dby)5GTo z1NxZ+;7-)?2RDlhRHFdcDonR?%<$%clJm1KzaYb^FV}wy8F_10o$-#4!woCAlD_yP zaVbd5*AM_G>%u;K4>8{y{`9)Ftut{0pedfPJ+5?fgi9ZfZ3?=FHO~UY2Ja;*7ku&T z`M!0@D~1+uELsJ}m#!c}m$el2-$oi<_BndeZ`bOBOY22x|F(X;@i7|zj=bdC{2}7~ zJ5y=%`z0@~A3;Eh4JWr+$6?mjwX!+gw^K*DJ5pl!<<&K6w|NJT)S8#4d;XxQfys>p zk~6ISh>eO&{Exsu_{1eKq*MsX0cS7)4fiZ{;zzn}_35p(!c9loh|e8XqTd`(>%r!> zRs=ZZ^y7~bKxrxzIAPYprnHEmeI_*z9NNUhvJ?ACYDJxmmXH$hugU|~QX9UVR+#DO zSqX$LQXw6Nw0ujwo^qiYechj58#Jo{EC;lWII?Wqu+8M!t4JG6isJMeo&|&dQ5zh! zcRI3o$w#Uz`?k<$4Wu0okr=$$K(ke?qGD;3yt1WPUzVB>QTsD&v&Qtn#5k4)8ja z|JkU}`JA2cJoqs1uO7hH(&IZk1Qaa!M#_9O4j+$$h$|Z`!ur2>UN;z6uh-q9TjN~+ z+}UnXy54!vB*R9bLSRv;tx`i(7_SK&I~SVHdzaPG{!G`f1#BhCg^@}}J)6YrI)z6? zf6<@20ICpq57QuZnpVBTK*1Uv*X(LH*ty^|Q@I85ROc%fc1eXHQX+m0bklYlRk4tF zPeNCHe* ko7Xorz9d?d zA&Yt)gPg9fR5EY#R&PmEG-z75Mkv8bMUUZX%o(6k`36}$4Gv!&Xwc?BB~nmYDRMg< z&= os>1|hwQKPnBaW8D(ZqRWFF?p;rJr?QeGu(n%(zo#o2kJ_oO9Hl_PtC z3@NJ+XnjTWZ6%Hb?TFi_H!ky)rOR|Cz>V^*dh@I=&`75}zk@U24)c|=Km$5tLF}Xs zBg#ac)G$&V)xS()*lrVK{? f&;_!&K*ynpxf3#;nnC z=55huo@xtEhf~~c1u3+E?$=888(#`49uy|F#^y YO_hBIw`KmM-g{ zRVp1yf%|oZJT-xLsdAx#=$3Lff?@URk0VBj&i$@M6-CZh-{1JUsGSr)_G%qYeG;K| z6onc=mfNK6K5Th2@2ntHr=@MwYN>}gz@sIVKb%9eExfRlyZhv&gDzdMu;Go2)wEI} zMu~f;>4gKQt8_VMwW0 N)zn{?Y{ufXOs&8dk9ewdeGCinC><2-bzRAF)qUwf z=;xWRZkp{=-nlq(+ESe$RibGF>Lw28Ci1T}dQ-A^#N&Je%YFmmoz-8rRy7`YZ(U}~ zMV~HL(faapI6mA$li|-}DC#-;Wd7_;`>gfK0n#d#X_DyNc&MW+;wAk&8M9dPz)DK% z5Z{aiYQMbmWL!PSI-swDs!L<)N{C`laE9Qe3TSC&Z7`l^YT^Ra=lgVVe0-C)3P89K z4pO>#Hok#*gzK4l<2Bnf3 Q;(B2&%B5sFTOc>Fa!cZ Ug+DUke2Dpk;%#Ly zA6EAOikp!+`%9^}@L+oV?VRToSBKXru)t&akDjO9rg8OX+}71>SE|5Tnu_4){QcUe zUr35_aWBeS Qgq3czadsT_Pmi493--8+b?T;n?_Y7?#TJoU9GEYno_>gmAp z589W|l@aL3lXy3EqTqMKK@tDjvcyMQJU;#dSe2*qSqiVJ*Wa~N)l)r;$Z-IO69WlA z3n8chA7h%R2oze4BoK`at-LA2JxAP&G}I+L3SGL@c3pK3Pl55U70oMGffm(U^Z0=$ z)bXdj=`pYOBzU@(O@;|*#~0eV&RvJd%64y03h~(k#^S=ByBe4dCBro(v(*CAi`qB@ z(MbSEO`tg)k`@Bb(Bksng+c1}mn4MwgAf__)RZb#1)#a?dotSrqOee{n`Z#<)G<6Y zk>;VW3oA1I^!2Bx#6lOBhTpb2dg8(tMAAHPZ1V0V!6LM^RZ5Ek;Qyu{h&X|=w8C)x z*^Eo>%cSRR+SiEEPX)j#TyYpz!E5C~(l{QD&0p9bXOt4cRgogxexNJj(P;cH@v&q} zkR%+CIXEpPjy328Y0CT$BwG_@e&(g~#LjzPV)^EDaxT#(S bTAq=iLU>?y){-wX37kgrsS0;hsi0IDfTDw_LI73rMoI8yQvZTxR@@AK zLvXaKF8#S8$rmGdE`tW=r9q2HzM8O98wd5q^R vY1#I`6!UvdQ^OLQJu07&z~ zRg~0pg1t>V=2(V7iIEWDXx`nb=u2RPxqOTwwiJGSyrCGFt1_tdtlLCFH&MPde(3CS z7%Dr~b5QsB>^YYi-Ot*&aWS^(igv|3rE1merLANZNSXwGmLj=Fk9U=qsI-L2~7YM)k?U^Bf zB$7KGShqq2F_r+T8$i8ST1Zg5m^0(MxFEx@qzWModW5#^^`wwVqEKzsT$!&B1{&ZX zmSWNMu0&TCyzSm3123)X_Bw}uMbu%Gf)z*xc8p`rGq;msGz$UJAn!{` Tn zbWlZjQl>i&pH)}={$zeyyXquB$AF}kvZcup7byzi^k&nQH#9o`iz(k!@vcnP`@G;u zsY%|c>VdBcMP@72W{L5Kyw9}h`&5@D`NW(+qhG{@#5ttS)e*$oHiJ-Dm(E}9wHVp2 z9r|3K|HTG6z5Y3cc<=6(MVS^*>8~5&dbWfK77YQRwvpw(G JzK7I$cpx6r1#*chKKo1)kq?=$pp^>gcH;u%xoZj*}mqeFJb z@wjaoLO!7y@F2ygvp{*l7d@pnI&}V~W<;riM+G1fou|ZERDX@AE%wXI3-8*27}4Ew zF`v=(VK#B?1`M=8cAkvzr*huItGCc=11m8+1%zud{N6l_NAw&z 4$7x#p3|fF#pNoX2rE7gt)W?_GelJp1?+)KX^4= z$=m+V=k!yG>8CPw-cRU#dF %NlMH ze_$EFsIFwV9#_tKsdl}s29xFoerI|U3)dcxF_&F^V(gywdDlpmuBD%b5DdPEpy $VE4&hT>YzZ$W&Aj-Mj+PGBCmOhuP2yx%)N;Y@g+2=9Br}{#s3^|#fAex%d zsRYo=0O};LzI>5P1izbeSFi8*v1{LtVvG5r$w>=3)PHMFpH_Bd)WYqJucd*H(vy>K zXRYM25Z;B%ckU`GK;=_6oa2q`ak;kxinW@O!uy5@nG5df16NZIxhL(8z0IFe)i9?) zu7-R(9f`5);2Z^pVVty%vulM=m|vtyP*@!14^H1U^$%dt`v;|tmB#dPfQyhTj;Q1w zlVy2{nJY!@$xxX|A3j)ZvSrJ#`0ZFF)tB#6KV8qyB{&-r6Jjg#516Z1?44-H!^-JJ zT**GWEm*3`BIp(;syJ^51;RsKT>sgWLo|OqRr_BexXM)T5&(X7!OE&w;B16MkO@mG zw@??Bb@Fhl1ZipWlwP}%0Ug_7; e 4FAjXO?x9W`%z@b)Q5M#|^ zgi~yld2ZK?e5PBZJS!z2fjC7Kf)*SBOyvK_>_7Ezu?VIFPU%gZMeWRtf+PDEo4mB= zM_r_~rz{5)BdII{vhQzIvY)y!b4iAoB}_n#1h8&zH;o+*I<-2HLyv+DU3uu6Vcjix z5o=P3Aj(!u4bPm21!0nFBj8HXWwTvTSZ>5uJRdzY7p%MG?Wevyn5+J9ZJ%m#q6@t2 zEay;l%}pur>!>SH>COO5g6 U<~k9^ zVWMX}<{2cEg9LGNc8h1C6%Wd%4PfYOHUov85+d@}etkw5wO;kO;Godpx2+zCBy%g& zn&Cp$9FRDNnr_diNs3 +Evh4COSzWO98|6 ag$&+k{fb~>e${+T>_o}I z)6iORzmkC6tRbAmC_XIm?rF?BGeE@&10V_?;S&U$y|Mz$Z`7}dEQX0URTCd*Z9>y< zvuH!Ql zwcnNT*EAV+eF(RoZ}U+}hLoZLnos(G=$)q{{hEcjBtHR2>wN<%Qh{5s03qzM4yC3e zUrHSTlsOFNrOkXw;a@jbY6k!L@~waE2~j@r!tuI76x$Oq4(=^h$i0nH{3@ta7DjJ% zus;0aKO=9AS$@*4hZk1=hRN{jXb)v7#XN;221ZTVSvoyQ= ;*m-P2Fi(Ar~pph&v-JnNxGr?>%TYuo93WUbVJ!<4E*GfhpUBDLa=-I=$<%6 zY}DscuOL2ugUmE~Er+kvC4%jH&N>H*os}5FXwCCT>MeU3p+JZx$$u%^ 891GFfXeA(&Nc+htb8n})`@xI$a~Rd6Kwd3CV66~=HWpfL zd}p0aw+7_K=LHu4rO{j&)!l=+@9~4afdHh`TE$ZvKZwzVrtiEf;A1Bzs<&*9a9==! z1m#trQ476*iU)3=tp%(P#saA4>3PcUDC^4KoZ?6$07_awEm+U& K7&-EIJI{-vw;< zm*StMg0Jit?fuUfzqZ&- UH%HWI A7z7?M)a1(w{Z4NOv
iLXcvb7uy zWg(zv+5hvN4ryL+&g%IGvVva68tsEWEHH>LB0s3&kln62)3A%lhn^olGXEuHU41K? zutMHyZn67{Jd-Hs$eqqUZ_8{kKTp4WoJiXvEE`G$sUn^{QSk`g+?l`;K!&wrX|wOA zoSW&4^C?2A-QA?W5o=8XO$@avj^lmQ+Zy_xb>ak(1FZB>*+px=v9Bp*MP9j@KGT*y zfF~q05duEyAyk}0j-o9PaQpKrL>E2 z^`B3An8zo-UpcpbyT?uhg(WgGdT$(6H|@g(TV8#WVd<)0`g3a}213CN7ldp8Lhx0v z>-wkrzg&MEe}mD!Auzh=4uXxA@KYR5aILiM^?WD$ZYO>Fs?l9N!<=M3wX9oiz7b+c zmgiv}$ZHhRWnVgRA!)zVkEU#_)YJ<^ky`<|GY{zGOgv2u)KI?l$ivH0Pk(#1W~9>C zXtGWRmGX1f`i75UjPj}AM{eZt3pWIxY2DpVR+T;+YmfZ5pkmMXX7xL}$ivY%>>tO> z43RaBCHPj;W8=0 u=eDMyf2RqBrhT_ZX7ndAGmT8#Re$e2?zwI6r6U@98bbd| z)@c98ux|e$HR)d*S$FEphVqYtxGvL!X&@?Ih?4$o$2TNihLH|Xr|{-STS00|VSM!a zp(%?$CDU6%qmoOWOW9~#Y-bGsWG?eH?8c_NXqXt?vJ$?>Oab@%o?2S)7yaksZkt$% zF|Z@{7tWQyfFq?zsdqyFY0qyQ;IKqU>J`eW_fg fV;32qBD0@Xj>K3@LhJJSgZlwdS7RF=(U^nR}n^?k7*09{? zS5l= ?}myW*RaU=(%gdAD>e0$vpO*dWyruReQ_0o~oZ`%x6c+ ztROg1LHhVn0H=vrVyGE|q!syQUs(+UCYB{6L?PomLT4;eEkn}!;|9AH=5d67t|_WK zpUw(bGHd|I8GWyRN1*4G^vr8~ivX5hjlP%m;}Hf?9ektCBog}``)i!%0N83oN*X>k zd*@8c6j4D5_nQiiuPU;ts**b!>d{Q%$V;6CAS>(8&5C+PSSYjdIVU*0s{X)8MeMC_ z=x=TePw`34qza*QT*ptBY GmJ {B!`! z0^A4~uSa#9DB-=|R7_Y O14nLXE$HKq@2bkKlCu0rP NJK z@+tQKwQEzt5*6RwA@(Wifn+`AQJ5r8?0YF13*V9;BRgW*W8jJ@FmHJKGI(} Bc M12 8q37fb@ z1bL7gQKxoMop2rrB>Lv%$i5-=VS*BOHh32J&3W+?3u_iIyf?+_Jd=-r>?GT-3Xra3 z$eyS93q)b!&A8sizfXU;D;Q!JQy#e^7X@ta )7}KuDp06Y7068Q_VI5mGGz(HLM$Ym6-7Gd_?eza_5b?D1mSF~06Gd0uo( zp9~`W8<&*MMs_}~*)uQ?nF3I6WBmy5W}YQKiOBpGVg?u+^%uWV0e`WukuZc&7B;3% zN(~%5AkQoR@17J53<+}M@O3HNzBqFOr4BrivC3%WpFKC^>Z8Z_nQ25E79M!e8tcs5 zESqv{NJkbCN{0z?VGOAj>>+{ZWWnIY2Ee{Xyo1( z6ZQs_Zx22`aBz6njYX_ctoSg3 B& z-d-3NvUO2 Mjq{2ZJ*0&M<;G)GYauubIn)kU^W95F)Eydp!89|75HV; zXX|j@wZD1s33bo!g3N*n!iA9)ZC1LiKZ?wm=!z+b@FY{~n6u06*r zASCrc!RZIBfzfQ*96xm~;K<55QZ4)fPczs#DE=+ {m z8#2x&NG`-0Z|Rj?HI>blfF=N%{z--{BwI=VZjw(3cQNs7d-?_G$+wD|JKCLrYU2yo zeINS^BoMXsgySIHi5&A%GsX(oS#)3S!=rmsFPC0wJRQZ&Ey)%Bk;FYlr}Zw8ayKh% z418pM5jCBRb?hTs?%3)63AjGnmdZZsHRq0#Hn=={{hdDQ%%bwKW!yMIyDxlYc?6p{ zO5e5JZZL-4#E52&p0#^A>Lv60?J|jy`sabM8HpGB)5<#}O<|Qm3p@huX#Zkvt-Haj z`g2oDh@>#!GwQNuVOF2bR%=@egZy*}XqZAP|NQsKud@0a f;5 zYp&0Hi6ilxP|M>wTfFT+oCZ^VpQV+_j#dYer;3g4!@FM`%ufu&>utD<$NC7nO>8%% z2U~1eOzCj{$3kWI7o6+cuS>qcDsJyb7~_o$f+T%okx*_NXyO8>C5F$ZN-*zz;d2`W z2CtcTCulz7Rr5mOO}&~ AtE {b2{{qSoAp zHMin@?E2*N)~2sn8&Gm-%`V~2T%o)PBhF(a)4MZhYnT4UH7Bxo8}7`?reyBK^eNf! zJW4EVI}sTiPj;Ar6C+ZzJh}01IqrE`q@I)wpt(X!>5*6ODse2}-InE}#j8T+f0yGD z3~hF@$dY-!T37m-t27gaixtx+1Mt_ftjR=hQ`RS3Gp2jqXP)u99 ov zF8N$+(I=mHo=ESLzmm8&a8&M!TjV1K#1+mcH{Y$@wko|Rujj=auG{Z&yv;v9Fxo3e zl kx8E;aWp ST>VKq2FN1U&dQq-k*hJqfal=4;*#{cMaa&9wGl54V6vn}FTUKDeDBS` zr@6e1(bXv{dS|{_8;p{f74JJnc7VFx*^Sr?cbBIVof9n) OJCak{CTF$et(mI9MsM?Bt1g$bS?e zWhd)wG2_bGEQSdX>$%^(;xA8OVAUl-IgZ1-FVl9fGUWfq*15+s`Tzg_+O>^shMk-b z4I^i(5E2{1oacP3Mif#;DoSs=W|&jvd`j;TA|0fYq?%Jv6jD^GDV2_Iohh~NK7amh zx8L8_KiBQLuG{N+y`Im<{r>FY2c8nlz7!*~tmfYvZVz!&n-i;NXI&rMuGn+!&{~F3 zlXmZXkJ7nq$g>q@x7fCCxlbOt;Q)F=bLQSQaXoeiTKz)Wro)}*4}(X8s$wpyMQ^zo zXCJor_pAK0gT%Xmz_l}bXhgw3o#4cevAc=djjFTxhlpYOCN{53{LNZZ^CNQ4A1W>1 zHg~6535>eLu70QNxS2MFvl|>hqTo19mL1Yd&2lS`rpI%kyA`QYW6js`wigjRwbL>e zhwqG7(gcDJu}!fMh-%9!v#;3UTj!9pFnwji3A9c`7edAKf&gz|)5opV18?E={R^~H zP4)T#ba2;>aWA?Ql0_Ru{EwjFNa@<0lTI4O!s8$uC2t(u1G`Ns3#vC}CkC9|ipsVx zHlBRp!7QiTARCzkw$$`(@y|GCf2B>0=(ui%3Z@&YGzA=aH<8vdSgb_g0A(122k#`> zFX4e^)2kWKYakC9Yy0sLWDp)`ykvW3?lSg>;FUhlY8whe<{0 SCEGxV;m+x$7k zN= J)5uLzDRTb zMeZtv{b`rl@xbc8W)l9Kh&fPr;h*zdp62}eyIeUucNc)t@)BRV0i4$#e6P9v+*RC_ z3!a P_9)u_mEzy{AIz dX=r`2G^kuU`>0@DD_VZfF0tpr-P@-?`7gFiS`%q1FCtGC13jm2qQ@Br@TBLl zf~(R8j^ReXRU?0(1P1#(;l@4eTrGqS;q&vip>ujSwN4a3V=_O30_&V{Gs@~J`S`$f z#i=}uAVg!AK3bc@90;GUdZ>}YMSi*pJu*O{{wK{qqGUde|3{jEL@av<$y@*ctZSFf z4j?qv|82D6bV+U?rE&y{2p_|R tt}pzQNYXI*QfbcFCT`4LX{Z>}Bm zg6>iTB81b-hc7+kDH}*nf8X;SRkR4%f3D}H(dZf@t<}{7G|O!yc6`G8e2hu?abMAt z1cKbL%EqRSf+FA@PY$%0%Am~{aPyM_M712@TK8L8*Q)=F+>gz}nuRV|IMjOA7*Vbw zKK(8gRLvtcncI@HtS}VogymPJFW2{zADOI`mZa|EMZ>MB*O?^?zm+T%G5TZbF&5+5 zDF=2O^;XRWVAoU-M?KAkxKo^*E8p&=naq>#Bgv%cerL7t;4J}WWGMI$%Z((%WB+j* zbPgG{?PsD|b_%0E{vqmPP~pKdkybrb&p~~B2rfczcqcwA`p*Uiokd@};Os*iW_WEA zrVp|2)qT6qM+VTQF!J@J9KyKDmFXaBuDotRbO_lsT8=O5M{Z-2!<7B>GT|7#F^|Bg z<&dG-W5{`1q3qX5qR?Hj_?()xhBgaZUDuzB^Zdu{mwyEk_W7P_ZR9ssc@iUBuL}8* zqfIX@x8@V9)72)GtPuDG0U}c>7m3sba5IP-e>UVsJ90c(C~2RF_|KP*C;+3D$Zyz= zPHYXPdub%aR2O|3W?Y=eoNUPn9lDU~hF*n(DAC_R$P20W)54R5fDMuGI_i_9$5L}a zMfu`%njG}$m%nm=Au<`a78jL#43^PF+nzc~Vnb@?oESP$D-oae+;GJ|mEv1Av^S`d z@d~@S>;iP@kWJH&lG*X05MN89@As7GBm4hz2FW;aN~8}&Q{K$Ss$ez=0CPG+ujLI0 zw{AQBuaW%(ZP+BndqQ=@a6~%?TSyJB_@|MbxGg9h-x`CzgwvrHeYRov^kaHS0+u2^ z7Q0cNExzlaCFSx7OwtN(-0}9C0Qe0>Dr>t@Vo0D|U`0B++9+{6%scu5fAF%!;dvxT zN&bLT36p+3`dG>;IQZ^;`R$?JR@-wqA}dMBXWWsK#le9Fx?C=xA(Dg@f9%%;eb 6Rr0X)i-zu6^vHCDWiQa_)Vd)EfWl% z^LzIVDvsDkQckWnw|N1st4Gtpk(~Mo2HML+`|fQ*yUK?WavB|f{)l@y+>T~tlKSs( z98X(bWdRxdn78dOJ-0d;LTl5;oV{qmy>v3{cK bmuA7gWi%UK{4#!}oWEa2P(E1H=#trrSzPBI+E7dA 8cwN>f;$Q51#&^Y_gTnqB*oR`5wx4IkdzpiK43XjUk9LqzN8y&h32?{1+$Y7{|;h2%MH?xajN?d@gk|fUmH1o%A z?xf(qB?C7Kc}5pbCmEv2{_eZ*b+`G_Lw0$cD?Q&%BVtP7yR46+$B{XQE=fl8O5z(m zn;`-Pu(yb%PoxjCmcAu1h)Zas&?c7?Baa4LAH&=WFKqg|9ay(%Z`H%3>7*D$MBRph zEIpg&D^oN25vYI9v;i~9i~Al%vFEi1ZFm>|0C^sN^5q7?9W^iHWAuJws@BX2nL%XV zUN0b63h{>v`1dvXX8{*6|4Uh%3(;!;SNxdRtX}p&%Y0PLQ65QNl)#bbh|Q1)CVdyI z@XuRNVJ}UnTDu<++$>fmv(;U+dKLbOfmOX~OqS^Z dXhW$TtK$?a6x$Z{^wDvU6etE1vqT6Ba=*MtoO|%oHwEl6fKRZ` zwGjVaW6{;mcjv%Z`?zpRVDXBz1ZnY&d)o^YajzMyCQtvj=I+f?43elbJAG{h7%tuo zauAl*FeGk*hIEnTTf)&bI;doMr#wn|k8GL9TFHXgyzIHY=bd|@qi9~Patr%JWWp$$ z&B7^t5V>4aGEpE_lF3~{#A8eyA*(f4^>F--tAlALgf`rNg`Y;V2I^n}(RumR1yIk} z-Z{*pwYfeH6(_xgVsKOH%>OPkxR 7m%_d!Du+)71-3HUc(PTs`YqKkvkQsGuK1P+%)NoT0x*Qhja0Hy%xEtlw+ zu%r!KLkXP%nK+2OWfBY%5j>(D9xag7))6dfl#*T4RsgYh{_uDh&f~;k%4ejrhs)x9 zwdHD1COxnf%H#uZ13tKcWvR&ywiWI*nIJ?`55@=%Bj!n)#Osl%Fr1VFb4>$7(~>nd zn#<%8Duk`_WNm#KZqpJR%1t*5#(4_K{lTV7JN6pUGd*emO?J4=&%(T)u%AO&2?-fV z3HUWEe`E}}v*mw22A7GHjl+Zy(EE5e$48b}Yk`+f1qUx}^qb$eX+k%8USM+$z7~)% z{>hQR##i3~MK++wfvExdZ(KkJTw!mUErrG5{#15g!>O#4T}&KRSZdGS`#m(}JSt_^ zNS5l}BHS#62KaA!g 77F)kxLF03$|CCC56-owZ95m9eQA1}jmT=b+|_&PB{ z7YTdDL^>`jD~qjA5(AIpDQ^b19d9==ipl>ulJ`Tp0H*@s&fkO9h_af(2}{+bk1rpt zl!rU7!1jkD5HLsvO^4&4Ae@7dan$SW$l!9QM%Fdv_=|69bI K+3s&h?es~GET;ON!gwaXR)Gg({audpr<5k4OiWnS}c`-(77PA4RYOQ zl5PYpu?)!H4}#x`9JqgV8f;f!D0#xc1jyN!h)*I42J$?j#Wi-GIIbt5?9HBUf>PBc zP?KEuWM@j5XXald+fAzNFztKJ;9b<?4~Chg6OZo9U_{^sMs{qh5=bTRU8rLrkvJX9F~fPG zPLj8i5Wco>yRRqcO)Yi-4w|GA2T>{7WCUn?JmnS1i-uTBhOP1&WUso%zFqW#NTSMO zhN8k(#A8wyTv}C%-l&VltOywceQqZ#{ixIbrR*m`lB}%`qZ`L=)v|jJkIgihlIu*x zDb (;of+fDKQw`B67#sw0CVVWW^8f7=z%fgB3r5 zahC0ef5t&Fxk=@E $(7cfg=aQgJnJN`Je`NUmgK)`E3%YM z5>k5*7iQ}$m*Dzltb3QM5m8-HJEGAnVE8s1UP#jF@_zsW2|X~+nU-qVuB*!uipgKS znc_`8i-1Vx3!pv+p(#H5uprN{z3E{= D-Xiq(TeKS`S<8(}2wMfCT2a-x>Pahs};>2>gkCD@@mp)z~O z*sZp!@3J;Sv8s9(v}uM#P=h-h-!^0BMFWGnWoL z9%GeeH1D zn{A;4SYu|JRs!BZj0hnk-KoXyA~=nM2ocvUnIcc^GLeZKJjrL8qRt+;iil2tuc3;9 z07U*w9k<9oy>3tLLt6?Nv|B*ZxjGR)FEaID_b$hqv8aaYkSZ%js~n1 ;&Z85zPsN+p9h?aKohQD1*6y9nX%lgLl1R~_Ed2hB>kZs4Q` zZg;P(4!{|lcX7KEzeTdocWQV0RJ3$T$BNtVW_@C#D}m5(PNiXBeK; RtM61t%{ zCEL47n+i9etv~+I_TB+Y^I(r)mw^qglwKZ5?|iU6aQN&T5p}|&W&n0LK7T23$U|HG z7%_hk6&6J6w8UNReYb5jbj&&Vj5}*=wffT0 N# c-q6hJS?N`w5n>%a8bSy*# F$;_(Zl zg-@p+RTRwPxv<7Acs-!#A#@6niSse3i@@s7DWGe^bZq; zTuVC_Wqzkenj00#k*t@%Z02FsWZ3#inDd0+-&WXyLvgh!!V-WGP1E4p;xEQeyt(2O zrCW&9sK!SKr=M?wxyXnKLWB_MWF8jALH>PmtMXR8ED>Nd*}L3x__kgc$_>%9w~~Hk zrpF<&ea `2hF-q{l`0^6%j|a@OpnK8y;*XsUssK6PdxdG()7H7`aD~DobZX(xA_KS|uy6 z0_oA$$Lv4W3?e_$5smFH&C`osgyC+@f7ty{cbg&=*n7O3WFDXVb^O4v&ktDW61;!p z{ncNvl{vK+qaCJ)q6SCK2H;+PIf#tFAuqSW{ls4JZ$3x*JIt!Bm&q=|#J65z;2Q&w zdGv)rIwDsflTseLWLNQDpW5$nWZe!zu0Kv+1oOFZH@F$LF(&2f);eE{Qq671+z)U& z?&VuPP^r1>?my25`E%~QKB%kG+ @&Wp!&vg>S|b=DA<&t`S<=8<-*E(`z*vY z)1{pOP_)*77?#UW%2jk;;CIsD1(c}2yXQON2b?X5nHji(p*xkuiV?CDcqy_Z+e%&3 z_c)e(N7erDLeD$O35*Uc%`TM??1k5 E(5zUZPqC z)uVjK`aY{~dh;j^^b<#MriI3etwGRflg^&zLVXd{rO>Qz Axm_Gihd)l{ETwleUq< z<8mGZl#{cxu3t_sZ3@V1PMa+ykInu{G&Qas0h=NH&9P^p>fRTa^I)G>zJbxT7ndE7 zU!Z&0)mpYL4fQ!JCo}qk?}DzOm!k?0PaHdWGlSMV)s@lnZOu(fik{QVKg%?fV%o_g zMzEM3b3K*vuQ++d4$+H?+4`I5l+tdQURU$z?+%R9zC|;%Rd=mRBy{xE?VhfaRI^hy zk5 p7RXZ#!P6wfmvYp3G?)zLY*C zMz*Ujyii!Pw)RCXM!%I_^c5FGGJ~dmhpzjv)iSaBeR3C_tmicTNWMYN|IBIaXMgN3 zivn|Gnh{3%<0k#$uBxXGtP$@8q`y~Ur>^Jy-ieMmzcnklSt;ccSq=G%3m;i1JfraB zcXJF(zT0K~YLB#O(|hRpK7Wx+_SyZq{&%q6n|oeX@9$=tzb;IlxKWvf(YD1U m$2)on(PF)9we%z=TxU;>>UM4RX-o!J{` l$z_Jkc$RF$vliSs6g9@2v-^tj4FxC7y@0cxGyJ$#G4s58!! zOfMK8^h($)vZly;xBok+uiEtMPmV0+Tcz4A%C*X(D@?59d-_--vAM+oQfeu-enPVp zH5B<}wBfRF$wG62CTD3(S4f@!M;EZP)=s~Z)_jdPPd#G;lAw== hxz2-kz)kWXm@N8Mq3ZVo z)N(}YB`wS=A#YWm{dQ7sQ;)1lwFJ(?a{c}61eiFG)HQAD9XKJH*F9*a%@ZJK%wnui zate-N2;K&Oq$5BTg1RVYCXg|LVvGGslywE?VnYebrNQglHa~0LsVtL@+_M=<*BN;C z7ModTiXK8*Elo-lC_;pCgw@6ub*~t=!1^7$o9K@EQU4YVvOx>i$7OcSwu>V&wPh34 zYqstDDSe^Q>hVj`a60PxTP~CVoXr>?X(3eT2JPUlTTDi^3P^~L% OcM+MA@BE(n@^s?;W2MQR6;Hp_}DJXfKnu?h1|Jw|kk|-Qlw$l!$p; z9+poIRIde7E{EJ_67u9Te9zGQ9_FNc=2D-RKfb|6TfdC^zBVh8ho4o~dz9YJkMExo z%vK8T*~;4=-RH6*09AKyF gz`ZRkQ`1?jcQqBedfJ8kKyH|q}jH? zUCfwnDcW=4HhHZ)y*#V^IH(eRGsws|&h_v2hIiPV{#ynNy9h3(YuefQqx7%A)@g`J zFZ$N%cMze=5N9f>lE1oM56=S-`Xg-nEyh^Xz}M}D_eI>=z+m_F>!W)c%{N2)tl^nm z&{-Gbi0ywL&$Mma+E9y<3$WFWc wEw4LG&R;QrK# zy@;QfC;urTP%L0TNtIBITZK0V9kfM*gl=bUMd-rGxx4uD>8|7eaqK$1~BPLMk8^fqDr3mA;t5X1ac}$=>ao~+K zn_r3A6n=ts@I$Wdb21#fLW3VM$HC0xGR*FH+2WJkQbz0K_R5uSIhP5>tc^oEw9BIG zG}8$z#h9*FJ#+H74RKoR0uP`o7UOcwxvF+W6`sKrHu}UKU>A$-GIi}ca^dgb#V#Z- zIN4EGpO)L93x~S!rnsfmFPjE;fw7E7ixpJ_O!RE?`S+LDRzN?((T={a7~q?aq3vB% zQ2$gfL3NvIU};U`2jpY*<=Bs!?lz83eD#Z0ujO`w3ce}=T9Dk%T@B$)Ji>>fczxsk zn;tmZxN*x5* eTL*x&<)4}ev>mr6uZG+hR6^Som>BLE=(QYz`13P37 zMbC52Pe$%d7b2pX S?U^Djg835|>~dldu?1C8Jre^-;y! zX7nS0DM6zI%4q3au8D(PuCtRBL?NOvN#Lnu!i34B>s6U&nS@i2v7t_v(4gz=%?wC} zY`T?ItFTR>1nzx>j As>pyg+YL^<3dgRUsH8YVcRSUZMErW77r$jdyt3d|Wtkh05 zIIoDbC<4e0ru;0HCYQGMl(72Sl6pR?_v#fb?XH6ML{Nuj-`Y=@psN-&b(>|K(O+~? zII3x30P8TeLalI39AGk{Q%(rXiG^lfb;O0v{zaJSEXsoGFt{CMWCx8jLuA_jg4HS$ z{hcHI^Xn=az5qsmb#hsY)e=~>-o JkKcCZW*!XNDxIi4i2dz^1+CV24;nNn43gJZ6XU=N`znqtZOK&;sX@jBjO{ z+R1y9a#Iy|f98=UAYh#iqp(}rg{xxB{hJ+tGhbw`LsYN7Q-EM|^V(%xD;6yT(3m2x zE0; RLf!Cc4zd7c4v-akTE-lOX##9)qY5kfP073u|`? 4E^GGTuF 3BU?v{Q!S73Wm3{eKywSRYy|z2HG2d zM9GyGtNG>H0>MkqCy_okVmE(Zs(zf*dy4IK)l +xt2fERF4?b_9q*F2`oREHo{ok3ZX ^rg91Gpt{( R=fl%#XC7>_jo`MC U!{ngXgu=(33V*Z_Clu@cQmCIQbEV z{iv_6&@y^B;;ZFL6Hg5Lh8x6FW3`hc$4K#9HFOX{#r4qy%wWRa&$_TxVyVxB-KgRc zYk7S>n`hcB28S%1V@A7v=NC52d2YGp`>Oij+q-K%hnHly*G~mgP~2=9*K!EVq(5kV zN|L?BmIsct{m@ItTL^WTvlmikwQEj{=i$cYB#P2!%R?JCKpqaf)9{ )YGPgyG zBGQweHuyex`hB34CG_W)Bn`|!;d8@V#yUs1`R^4NkL=DUC}_|s+m4ftyt0#58h2DF zXmJaHICMzN(_eNEGoz0#aVr=A#Jm>d0~@T^;tU;s?t6!17u|_(J~6IYpun8+4)lnh zfZ74rlcLWbPAi90UTL0se+_l^>6F5{=Iwk?e7Iy|gQmi03B|b31OPwT^N2Jc=K+c$ zgr;4G>lKQ!HT3!)u;scmuP=k2&@ic??*woilcbH3OAXcDgF-&MJGP6AQB>FihGnh_ zFyu#hwH6PwA3kqqYxnS82{knmDp4Z;_int~1zbhea2M;z3@dDaUkC|R<>)XRC;Irx zR2OUO%&csMC$XwkSO+1>f`Zke!o{gJy|fa&=1MOUNw)eHEMohM;zGAT;XREbrgpqP zD=@MhWR3|aIFAvI+*9NNE* Kb?tNjVdqEd3iR@k ze1&-5P<1&6UefsX%0x-29w<|rKoiiMfcMDD`;8FO$*FF5570M^!Eu5<^Wq_!h)xLn zG1z^e1U%OHZ2wT96!&b$^dWuof2z8eL!AjFoMUqpBmp)<0}{nN&S?o c`a9~9B%8ES5e$-Bd28SW_53h_x zJmU#yQ60+efx!V75(p@xP9d%ezI1A7?byz@xg_7iRhfl;ei?=mc#rFokliTb1TaPc z+_S=F?bv*@Icf0H)+75Yu(RCum*@4xJW+KGHnEa!a_SkGr_WI}+K)CmRzKT1Gq-1Z zrdOi$$UbInKvKJ6-4$?LcDvg5cTENH05^z;;TcI_j>&-hY>~SOS#t@cyA< FOx$aAp*Al6)3{=j_XeoK*+TDvmxsUWpQiQVX+EIl9pF3hA@ zfiu^+$TL_3*JRK6K$Rw@U)H9Tpl3(rvw?F5QMPBSj0Iffo3Qku?y0mHsFwX>zv8rQ zx TZ-l zNy>IzL7(Ff+%OkMd$gz7WMyLFU?EqF>O +O(v-L`rIzk7>o4*~_d zenEDb+w&9etpng}uB?NJATOdv?vIX1{3&Ivi!1e2Tl#zes4HNq^>f=V5b+*Tb{fTy zi6piuX~a9{Ff68`x@M(%du4TOpSYL{8ZKEq+_g{oOJOz74k-Ki?9hV!aWTxy3D6mT zcuMpok8AK`zap}q8>TpU^9DwA;D>_tpIA3zN5 o)h{uXNQWqO7pn*zYp0fhe?8?r#xoX&W3)GF}~|at8Ya1nFV= zKrFcgO$_LxK|BeP$BYeD5AW|E$}yAU2}j~2M<8{lW|Yju>&25N)0XA+nhFa1(xTT9 z++xchn ^}=VpO=?CN=qG)Y-$6#6C>2IdM1g8p_W z65>1%4M{WZ3!-L(3))jzNjIaCT`kRyD1SIn_EiC=?O7xdnU={WqC|9H5oYIPW8Rb< z9FnO^A83X?Jxp^AO00l c|AyHeYm&m3Am!3Zk| zx=_z_#L3h1dlg5_m+uYVShPR1`c-)UTMjPHAn8QWLGtZvM{Y)C!u?k_zDu{xK85E0 z@xw2>vrxojiA!LBwmVKWU|Pacv=1&rs;cP!^j9RN?icR0k746NiHU|UoXs;HlKco; zB%vt%XpXBIJie waCBT8)L|bmZ+=xaQ$SAO=3&za-A&OxH>}81CI4K?^c^$>^3DICG zi)2SriLZO)7AID_NN7PSzgD!S7ZQD!T z2Q$j%UEx7a`ScEA{2_O1bHD zgx e&{`nrJw&2IThS7Jb@8v8G{b@)+ef#m SfSepyqoe?-|^3s+)&d53` zTxF6$KtQ5K*+ij>V@$+uP8zciI|by3 6hDNb2uD8QR)=n&EedRm-cs zv>tiW1TygzMq~Y0NCV-n1kHqgXBrFi$Xr|#{TMf|#DlikoJmD>1O@e68hrEzZgb$E z=&@0Ib*=Rt!@YXom51HDD(pgz-rO_#8Fl=p2dDWu3WZyfzvSQ&Xz+D>2=!unZysA5 zXRD1O$#>`G^l?q}sZ0PTo8su68|LQ7L=-I>FX+5?8rKKaLYK;{T4*^4J(s`74&X@P zDP-Cr>*uBB;g1hKt}$tc?XTa*LkZZR CJ0>JpGjdWgw6U?E?$)A~*;GTlpyVz+IW>3yzzWSUqiu zV5}#5t+Xh)NE2Y#Tk (NShHi$%PButcE*@Wq2;*b{#shHqVf0JcGV5%! z7nSX0>NyND#VsXYr|=9-yeZ#mpOpI6pbkW6aHV2Ul|170ALF!+pBPf3K1+zBc~L0V zNQ#8ZSU3Rq+IMeSUl(=AkA|RdPzphI14=Ah4%vsU5Zq<1!UgyS>4B-IuK2k(xVZ5s z?4t;K`b(O6nco23C&6&! c&evRj_f3CSw z78$DCP33icLU}AT^#-y7la9@tl5XQIj9ULbuF-SR ;;dr=ze|4Sa!WdomB z!@C)>X0au35>f~B>txe;;!J9kU4M4_>46*3U&hgQzIr#QwC81aRhj0OpdLHIYE#9% z*%F#uZpc$53 PMxR`EmkR>Vz +?;#^WtEq zwTl+nU$Ip-htoq+Qw0 }0RI?6}H}-Td7kc}h zw>{z=#4b^5mrPhFwV>}9Ntp-Ggq^&;%h!zxo;wR^mT0DLQp0A`8dE;p>yLp*KJ%lN zTSjO~q^)D|C(6Bvhadh^YJvOL#$jo=J^yAbPbL(gYC3y1TM5!FeNaEUFK>Dxz^?AH zOBExGM4b>@{fM2I=XonS1x*lM!`H~p_NE{HiK;$y<#U%|1ISymDv=P%l6zEeYR5da zEOgH_1fR7C%y;;P^6j$DlK-;R>+3hN`duCRbWzTJU4*=a=Ik}!xyoC2YcWUbR+te{ zmwRYzIe*C&|AXa#g*PK{BP*_lNWO?~lbe39K%qPRRxUiWp~>ps-9?wv2)tVPwny7< z%Q^hhsPjN;hLD1Zkm~t7y@R>h_k4j6w2+DD^;p>Fd+wQHGO_*Re$k@*y#B812cCZ1 zoq$pL=#nc{$J;j4OV$P7U*)f8%L%!5_957v 3y RX>)`sa~PeYgSEf8mjfUGXrE*Gb4FQ4$KAa#p&3VYBswZMTY7{d|E b^`5@uhCghrUT)!o#s8{txmku$Efz2Z_eh0G@u4c1D`|xx}2>$Tlk3M&tHQftRJD{SD4Yrq3-bG* zx28K!YafglEgrb`oIW&z;s6>q)z}tl_A5BNhz4C5R}*m!b%(IVVmV4bVd>~?&jN;a zF1nT$$ap^3G&3ryoy9R;k~C`)>vyD94_-s8aUa^3Nk@GUU$p5b6h=*Z4q<_tsx)ax z67vaO03iDw0IROB%2-qz116wd-F%DQ4xGZ>q)*dwnM1uJ0C2E&mwls0;{>`zjHbvQ z0qx@F;!j79D-NU7WTq2Zb?ZJgOFzWheUMyPfi(=GSUj1)^3^_wF-IRe_dW2cnsY|v zu5IP7Rn1%52xz(UJA1q^Ohx@z;o6u5bRV#P(SVK>pG@#?Q5=E~Jcyi|V7|V&-2aHd zdhGajP)>=VCQ!FO7j{5#fmS?1&!DSe`xUWtwaQna8L@r~O0_-D@NuyH#*a}ZF#H_y zFar^y%OJ+Rq8q mjClYdg)$Zx318e?)<>?L z@|91g@#`LJ{SxYx(EszO__89y*(+KJqU;V-BT1go%(V|haDUTuA__+?x$*69Dy(_& zim@EWUdmuzS4t)9qeC zz(b84xrf|`1k^1#FLwHg$vIsZ-!p4T>2of^P`~s*URuG*{5D4dX KYXz#F1Y7=QJ2DeNE00&5U<~lq*}R0@dX& z#d`%WMz!%JtvhRbb;nLORU5CRQvp|0vq2d;hz?~LCWNFDQ^hZ^EN;q>F;i2FEq-YK z#l&qnL4qHm{5ARX=#``nsQH_tgvi~?HrVC^EBb{ )L z_c%uHl@s%pkcj_3YzSY~Lb2JhSsl7q?eO{G8#p}{fa~G(4`hP~*}iQ_asz-VVaGV< z7R2fsLszc`Ocjl0uSXosD0s20INQ_9D_lnIApb>aesiRC$4%yHu$Gp8@@^?8uZvp} zeJ+*HBh%Y7r|tUtL4>9}rq)=f-=8aQ>ci1|F@z?O2Q6zeWF_3I+S17_?O$Grwan$^ zR%RCT)hbxHG40t`OLvaiOX=zYG7NDCzG;>C_GfG!E+}5S`iAa~>+s4zG@YxIQ|yB2 znrAHOa{-}3qECjn&cF?(^0yJyZ+$boP=CDesDNcPa!f5u`*7yo5w{uzE#-x@S#GhV zw1+FbAvk_}c*Iaue|ZKPi2GMja6q@m#bnIHQFNQ7fAL+?X=@QD3Q&l{@pt$9xt~LT zE1q*jX-~STr5cZPiBqNK>;_Ny08-7)sVdTJmd|QXv!}$jUz|lVj~jOJ44>>rN>6#b zbgm5m2DVAR#RvLYkBD-y>pqaH60ZE&psuK^EewF@cJ^e#=5N$lB_9gJgB`S4i^EvO zarCxmq$aTFAg8Uo)C==^ra+4A=kHU9IG|5r`K)_R3NuH%akk*npdD%nc?L%|fo)U1 zwx5Ry%D?N329<(8QfHH-tXK6FdKxk$xu-QRgt%!AFmglsri&HK%&9e7G=E87H2|B> zX}DrtC+rc&jV2Rc(6TQ@#1eD=9M9dRmHfHc(|Ri~fZbS4)j&p(p0GV#=Va%|h#14c zmY@$J&5$d@IH*G2hZ^pF`Ol!l#Rjs6<*yTD3b!m?|Mmo){3&S*+aPvV>>0y%njc)~ z%LgabEa{VPRJRoWELFj+PP=&i*DA@~&c}!DZVW=u86%s9mIe~*dQ} o#5@r ztNxa|J*qOed1R|k67j6`gD4JbHq-Mgq|*NF8Nz?=>5-fcOHz|ZOo8;`BbN)+3{rz* zeejZ-4}W~!tw)N%vr73fKkt$3!s#|D(frMy<#Z4SUk<~|#2GvF?}&3B59__%(GXE% zKkKYMQo;z?B;tba`s?UHvR|Wn6zHc zia@3O3hcAnvf!3mHcM0gq5S3wAKI3I#yN@YESMtrgO{L+`UadChrFw9<(B+maBaan z4aC|F+$5{=)he;bb+mr`^iAuw$r7d>O?;3CI0FI%8hg@{A@vjIs8in kD!a+IqZjmdo;1 zKz%p<6dLQLsJMTLCwEFdkE+21R2lG!Myy%M)E$riW9!`ing0L(|9n389NQS%%=t77 zBQ!#kvdw6Y%`vBHL{X{ftwN=3&ZmqJMa`ict0@&yZF1^7qUbo%K}Vfl>A3GczkmON zT`qe(pWFR@f84LP>s^=tlb9B&xI&*t-;Zdw!m!4hG5_kSdV3!+U+w1{v~d=xTLY`c z{$8WII-|4Al3y}EBS~P1XyF?Mkv1N;=NE`q7hFE9SI`6z2^^khdk*S-3?{VwoG_+7 zw{W#`MB#S5_S)p=($l|2O4X-GccNZ Olt)_{(}_1117I)DYB&+Wcy;}M8vq6Y3SG{a=n`9wr39uqczKycAkpZc^P zzdFg%jQVD7kPJLfXAD{WYXUKs&GlA>2JP}uQQf#WKvbV}H?sRkjUh=Hex%c0yGiJq z2B;raJ_1+-P}!_Io`k;BHg9ZdR*`hEDk86l@S#Q%(z+c obNkdH|9VYU4g?Vz6Zn)rrK<avko5Cg$W*0x3XA|auximaZ`CY~Zztc7zrK cRlz&!N0D>eJp!M(9ex))Ps#2dW@DzW;fW_RinJYvt0a|b(par5(k;ympRBXT z@#D577Q9(}xig1eWo2z^*|phev(6!9fNXQKkG4g?+(gPA$w?CT5R|@QbZt^4z}t|3 zLN21x{CeuE-tMW?ockbJ-Xm<8ztLu;SD+GYbI&HNA=6ZRd^7bgLU&@EX+)tzl|%5K zX7$@afu%Kc^_7d3|5xQ=lS#E!*N_0>fI6t9Co?&$xAjRx>uEnhz3--_0Wy2X2=@~z zj_VzmYBr02Y8q9zDYYf@Mq=6UfpNaZn@i{!lOOS4opHfyXC$GD^domzF#w7ud5Wol zum*kxUCnLam1F_RoF^cWMbC9_q@SJftL3a)HgnS_-CciJOk~=5#MZo~iAe)Vx+^?9 zdGz6f@vZdTG}R058Kctsxf$npNDmG>HFPzu3a w2ow4C zb?kG4eJdXF;m9p0rY;M#6D$lL JgU*p&7D73w;36dindw39IbG_kMKh=&NNjITLzh5i<5u`f9an zc0ti*kq-#px-K1t2$h_(O{&J|Q`DeqeR9NFq@6;Sb^-zUXQ;$B(bU{{i0jT~tI)tM zc%Se;8}^JOt>dQ2&UpQf9eY+Sroz# u=1$!;Yx zmw?{8TUFIhEe*C2fmAB4NjFWg0FuS@)7>~X5g7K+@5Nrct1@w$m$gm>Dmz6BmxQ88 zK_ejtqeU5C{|C35DJ=z{Mi->qDIm(jlm%S0+|wWqzvChzD*-T)fMy(EbPYI<=QA!v zIt+v?El(R!K}ub4X6bl?1<;m?8`BAGXhs?=gnb!AMr~OYyP#Z)$tEr!EJYyUaA;yP z7`)sMyL*?4_i`^i1dE0UQADg RdTrA50%%Cqm z8$^i!V-XxJ+Z3Q1%8guy;3Fb_Xu1nyryuCs%CbhjAdmlWR?l{K01z*?AS}`lBo3u= z81$5^GGO~Si2NO^!u){9AQzk$#lVs67>GlV0MKtyGfcQE)0{9tBnpY(d>8m>jeu8S z!b7cBqeG^oRMW7Gsu943txPUN{(Ix$I9bH6N=a_rtq`Uvgn8&FLdg9hH9Gtl>C;Qe zoOi~VQ4xrld?QijN|#JuiLqMcu8dtAT65TJ2 ;Ys)0C5&-9= zNQ5G9t^Npjf#}-1cz7v@=+#?6mP0 Dvf<7_q^X#r;2?@; z2%DPq7kcbg$Po$}6qaqgfnD+ANZ3EK)e0U&lmNNXSPU-U?kPC-9H8XqvL>{!I5>us zr~u#x%W?C;K5Ej#EtQBM{YrOsR*+5V_jJ5f5ANk&p<1x;YibqolFtYO8H`&SQj1{7 z5seScXB=JX?*|#MS1A?{P3xdB>rfBsN}DaqJlIaGb9+$~=& &v z(!pFXbJKi2d@(fYr{*iEp2KHQ2?bi>1yxaH9w&Eu>=r(a5->zU<=!Nrw5=v6DuXJ5 zJcaOTHf+<13<*so%Mndcf~gt30Is70*U_l9_?fGTmrrRSYajY$z#$Zv9jEVE4y=9Y z@bDWz<^#^-fHkyhA+qKtOfl)j9r_H;X5bm3sz?Ay;~`ei%1pTj6tcXFkwUY`GLuJn zRV`35t7*^;$54V{|7BB$YNuU~x^ke37kbFN6a%5 xedC(obm?u^=v5mM}sQ~eg_7`I+wUg8k61x zc_r6dPUWTO37j7_M%uvtOR~GlDnuwph+_DnA_>tEwq3%27~FgZ5vcY6GELSC^jl6R zfl4${xo#ygXeW}|KYi9bVUV?o3;&65b|$V!{FZNCv(}aCc&CQ*b+JJ$?kH*js_=pt zVvh6Mnv6_*dQqP1qr9RhLhu6jb8lGWJYwvPZpGW9hP|AecH@>C$WVj!XDe_G?b{Fw zCMS#9W4>vcyxE25vgp(@36SSj|EzmxAxOBfijB*ha>q5ILmN|pEA7_D3iIMK@O_!J zE^iSBZyxoL1+ M)#}-=0O0-Dyjtq57dbiqIH-O25PpN@RBxlB! z zsg?c$ZD?II1+~>863V~OcLaYX n-qJ18 zW2Hbo)=-*U+}MV&al;880Q5~4&^H?6Z}cc>LHzzJ7h4gR2!aQ$`ZA2Y vpnKxaS@AFG($6gELa1w9Te~}|{70lN`?8sQlUf5}`Uk=9 z5&Q%+VR&Sg&ciR85g{ZPF9!DIR#p0*pmCADoZcWg>_baEaE^25UAHV54w56lmAEJl zWb@}hqfJ6Im2M=1dRb885;*KZYRg`4o=u{KHf;ID*J uGP zCtGqsYj%O<4AhigZ?QPaxABXs6$WvRsyQO#+pHPlA+mT~i6XlH%^%GmQOybtWW~;U z-#^f3R3E|_HcUI{#)02d-lx)_hmQvmZ=SZ 7fTmGW$HjQEXBxu&GBZxf8Pv*SR#kYx+SC%eZf+ams;^;`k`! z-jw_LOR7INBaXFAm~y+OZh_V` 8Q!1JF;l|u_MwJ^EA9&3L%gbaNfcoEH&3) z!NgbefNI{B8eOuaL~`wB&uTdwB8jmPb0$~fT-aF*T)j;!l7X$lPuHkkk%ra5geTTt zVK}J&-gUR?@ea_2hpfAD$t`+P!Oe^+av}0g5)@o$gMCp!_KNQ?l1AK#_zayYwy^ky zASL^cN1Tt>@01=z#L9{{h``mf6AqG1vJddq{sEgO4;wB&kFP}vx43w-`*X1MhIT#k zaHJdUF+QN%EVAEFgv?GRxPPdNEwHc@BLZn~IuoYh#_GqA`r$$8VNB`_XuS~mu^ab( z)GcmXTk6plOZqu0NPbc5thDvzSn|E!{rB)C_m>;jBN`B_zhIOMUMGihQcTu~YhMK; zkKI6e ~CRqnW0TRGB(Co~n;PF*>wb)U=T!YV@uYu+8JRJ|d=imW kP4C?ODIz z?97ARa69VDudNH)dtEW>v~+>!1k`LU&TOyrc6xVI|JF?gX--y)_*&P5&zbPK3agnQ z!+vPqF+c{JNZU$>SqJnYg~+x4vx^ix8A~>aGpem9{J8T#z4BJGwBd{O#|DhPVBhZH zLz@WMsglGOxRVz?Wi{yENjl&whY(A9%v-JVQ(>z7M(Lp&-jaJuI=`_#+ %6r9NZDDzWTU7g-PFUFD9 zVU}*?sJ{<4iSiFF{g<*xt;$v=(nqs-QgUG)-$QN1PdRi`cPaH*gyZb>d<|obCiRK( zR8o1Vr~2x@{z~N3+|u|&toh&!;g}BgT*`0R%w7JPvaH|`^7~-rDwC=DzR-c|_Ax!O z-0{TXAuGHhUH+%yf@<+WR^;fyQQq|lzOf%SX>9hn4wh>B#o0}3M%PHYKF!(uywwPw zsz}UO!7Ugv5ALUAMJA96?V?)~dS?gC{7h}_Jm1R)Nc3#qS22GC1!G&!#dSCR3x110 zddf(J{2t|C=uC4aN|~gn$5JDo>e)(qmbrkvuB)e~Atfm0McK0~_8E7>BwH3nFN{rB zC9PH>i&Cyks#@58OVD#LDapLpqOr9> iXE z%)`HFxwRT~_s$>P&z 8vyC*IZ@S;(ECMRnkOhHS;$`91VsiiJ9lp&e^;v;lLOQDOS 7CR?UZ1v^~MBNZc`Yyb<%>zYbg4!Rbp;o$q*NyK9%%*Y1pU$&GRFc zebKNsjg_Nsk(;?((uw`lneO}m!zr2Oc2-=`(p`oH0|)yS2Glf-3P81`J9+>wgYFNd z TQW5iH!8R!pJN1hu+I^CApJLnL=8IWW4}sKd|NVN+KWMARQlK5EnFZD- zP=`XyY@&zS8uCddqa;4;6cJfG@o!DGSdYv{kr=sK-CM6p#L&`8@t*coKc56Hd6xbH zgVY!vNPc~A`vTuyTbbZ@y2#lu>47;<)_|dObLUO7k{60P4zg}` %qr!7LGq+w6_~AK)GlBwfS|ZE6{Hz5AN915HprY z0#li$TN|9sj9PA3|FpenrkdXCf3KjlA4P~!=aJ;5wucrNSB7?#qVoB~!~Y@_sTjQv zNzTG&pYK|$`pIZ}hDq2rWsKq9`G`s}LO+a_K{{VBlJiY-0Gp#wB|B;Tu!GLN>K;OS zlneH#G(y?2^B4yzq|G8+){2}%g~>Kf-@RmGqc?-v|8u0Wm9*;oDl)I~@uQjm%i0^p zEIOIj*znJ>?hShufHN=!XS}Z4CAT(;skZzpZXJ~jVe67?lYZwPu(B|< zVf%12YM-7wM@25Bfogtkpo$8Fr2O&TZPvyxJ|e@DJdzQJj4GDIb!A;EH(>QIO|Z}# zG6MLhHR(^4ya7V=b4S8@;Etybl}}24iFB4h>*_{EDpIzkWt^+MRF{86%VxwpX&F{Y zu9;Rgb5m9f+|Vu(=Gr-j91P2bHt^hpcAOCOd>I+9D zB;?dAs9(i9UUncrEoB8uxBW|*l{#6)Pu!v|q`7E*r%|Fs#r7AzTknixiQGVe8d0~i zCakFL(7A9Qg9o^%_0COJizDa_T!AqyVRa>0LtTV&EsmWD*>tObZXV(qn;1xPNVmkc zitH^%?Yx}o9(ZxNaN8gU*>tQzp_)WRs;UnzueNOJ;P|X^-i(Y k+ zP=3{%qv=2I@&2tuFI$p*URU@ZX0vvMrh|(qp(l*I=5~^H`pAje>XZwRJD-*;g2{7Q zXuY|*h?>VWd}n@w5=>N5M@Y?d0Nwb%w>vmKgbKOSk?NrvYmWLO ef@{4zlMdR<{tA@14Es_Ko6C zkwh0A%?e(8uDg^#i_)ZPUb$zC@-!WvKGsmK8r^dzWdoa7WU~FLS#o&7p%8?2&0AC8 z86D=*eEnu4;)6uxQdsfTCDuKmXLiZ4{jnkhC(&|f4Vw`(?LL)HyyzHbP_MiCoIsDm zQ+48ejBuMW+ -m826#sqW?QpXfEwyBYVxN??ZqsfN>$ zvEuj{o5{NRgoxH9MR#-dF7#P#c$|aS?t<*Tf4?ZCOp FN2h+fdzxXGX7ZBd=g;3cQ_(`Aw(eb0!a5WWW!JtLy%*qG-xe!`VgfB! zo8hM(p(;|kUXGgij8 OMC!D`{_Bspm~b-)`TaFXQxitA0lGT(M%c3? z|Cq?+kKgPn?kYZhv6=FiVvF~RK;5bQd-3Kc#`4|?PC36`x5u&IeE1LHVZ8>G!n!)I zq`7SUm!c5s=X);1oJ6i&J9L*Z18N7{d5O{0@%x+wn#OUF6`UU+TYg0S%OK)imk65Q z|BTpOv9{RlQzG>pKPNwsO{f7*hw5vAW+8afpQy%E=Z3ZE10mu1Qh3kYiu1X?Z2_AT zWP;C7KwfS9>O^m~HE)D;)yA3p_i|*)Q~Iq{{Q?qCZfdUA=RGm?zm%8fRsHWh@DhIf zq?f<^G+8d?t1KD`+?cCB-*GE7`3P_JObFLHnWS?jDELC{PP7jXefZ`1BW~ew9LRhm;+{&2(;xIC^Vo*g zqNQwg`7uNDMU?sC Cvm|z z0okHP-J-p239SRsR(~+E^dN0XHbuL`YstU+y!_v_`G#f$;8IL4^mqP3$1m$4nb2-q z$+49JW0Ihq#@D6T?zyG>XE>rFzI3#9*(rkpl7!YkgGOJtY}|)g`L2a`3un&dm(>_4 ztf_pPUg!!(ToSEOBUa =THWXU5K(UvG@ *O@7y3bTpViqg&^%-H%$8l3k#mdPLumuQX$J zIGP)g1k1XeEJ@2>>(@892@I%Ukdaa=In+2%WGL6IqxO#lThwtN2LNv`G;!W9RBs>Q zv-{HCf`3!_%YK*H;-DKW$U^Vj^gO;PNpDq}`cJ8D9ciTPjbmtUx8j(&Uvfm(~e88T=9nizAP*Ugu1 zh*`QXOmK%kYEh#bGzMBGS)MHbtLON7dbJ1jG&L{t@~3X8cbTZA@%F_F93 G#ub%mQwS(%? z0O)T$rYYOTDlW9pMpZB;00O1CG<`IGG>FjZs&7=VSZhEQL3rA7GtleXte~ENF=~`f zicfTo^S4bOQByj;dw3VWh#QJ%#Wn3a+B# 9A083N=CG~??J?JczhA>qB|cW Ek$%p351rF@$ z1)6p@#I)Ggd^X*uo5AA?0fhkx(qHUsm=fK2WO_lp$baGlMRbj?^s7t2m(y!&|EOMX zI>x+wt51vitN 3faeNP7ORXlsPY|-8l(jR*^V1>n5Dr=&;_?V|KzR*T z%G9TENl}4X%j}k6Mvfl_G5yvpFN%~GM86#cpAYkhk)`?p{CGZiUmqo$1qf^*oMfSm z2VZItV!ZGDO|93h;ThO!mbde@jZw6rD;w*NOZD&aQMX+zDjbqz&v-hahr{2{?`~Ni z@#bcAi_k#vMV+{DANY4V0u6)^D51ritEVWau_dMx95qb(!-E{27Uh1+0EU}ZmNzNT z!tqgVfWDG~ra<4RApXs*6GIME?TTp$6;xAJA=VqRZAXln#|qn9D~4XjSuZ2hqa|Q_ zn~DBZ#O4r9>M?1Dg#cT?Tl0wbY5|>3J)v16MfCvmN4)#TCO_>{J{|6zJg8bb-26?r z!JnzxKlxYTskT5JdlI{@RezvUTsIIR>}6t$!tOMNC8eG}SYY~<*xYp9baPnMHCo4y z7`wE?K*!~~f^Qdp6{yvb(781+&D)lTc6gdt-Z)qQktoplcaQ(VPx4XhM#v8$_9)07 zbPOoRUo}$8q8{Z9bxtNke$paX-f1|iR2f|<$QhefQUtHQ8sYUOiAk7UcGbxNAdz+l z6&bZO-6LhJD8WYGUINjK3Vv-<_b?7u;Q-n*{&tnVZs@!Di-A^RL3 Kqo@NQieX slmh3A&z|ne<^m=IncCxu8L)aL@jNRjWXA zy_4p0wGDrZ?IfWde^o;#`B))vfk3|gJFr;Kwl2NXa8cDNspBb Is}4j* zt7H|dZVwwh5U-_01|`5>sgA{qG5tD>(c<&2)0iV{fs^-g#3Gs?;VE_wp*voLFYMpA zx&4|Ji*LXJNiv9%6pW+Jw4nrBw8b_VYfn?A_#(;C>F2FYR9lYR@5+nF-4o>!j4fw1 zLI$?FL!ayl!^fTsQv^in>3mUCbz;;6c~C#Llp3X-Wk2>Q620|Nfw_2wNbR^d{=zs5 zb-GMn}%PJv2|c pKhP*U>|+PS#9M zqWC+52;8B<`^SpxSAz}TA)_h(n2RV0R+FTJ)qWMM%ivjlcR5y$`F>#e$*Rf7+`Db* zfn|Tr6l|FM#535fGFiN+mZcp^5jmJs m^B#3%t9#g}c*l_+d zr#c^{8DJ!#92(lZ`vK|bR*7(ZbA;xPj%$jF$WA`(=D|e}!!{`<@va^y7%l+P$nR;+ z^<+oB-)cc^+r(a154CBMWC%Fma2)%#$%-~Iu{xgdZ?dJP8e`E!u_|;l_h;8}U T_+b(A`&Q5xSFmtN`82;t-+2uc@S6@a1~YMCZK}n zwUl6EU|cb-d3b+3#+dem7P2(*9-lf>Z9+JJ;6EAipVbo_&%R|<&zQJa&~uzn-Zti2 zU(`~;Un?N2d;qCSfDO4b3Z?#K=Meb!v&L#mXydBUgY3y;e=)vcS4E<6()jpBq`m`p z*Q_#6^!)3W=|whwwoLqeN}2*=8GM9TV(}mEtvjU^SKCq$?>AIfDgmzD4R1Yl&HpOO zoXV3O7`@i&*>Z!K(hOi)8?J_xy|z3gwM~6{=A*+A-fV=)OW*BZ{d8K~Lh1^>aJPrd z>{o)904A&{+W4Ks``H;~>g-s|wY<@5fWSfsD8cU5Wy$OVUvK?avzPZsbfQo`s1Vm zz8(#_9I!2J4^NewW{D@vnQix29pmXSHF0#i-fKV}fft_mC}@HzHons>G{xC4Ng &c)*(xetd4H@cW j*`0PR}>e!n;Uev#hLc zF<&Z@9W+$eW8wHo3_R1SI_1&s_%fMYr1Mq&z$SWmnL_)I>Pu5Xw)1B5;NNA%NA4(y zYu!(a83oF_jlSK#@3{>D4n@qd)Y;zuq4yXsDM$Bg#u%$~B`r@H_ 9-U7 zgRdv|s(6l_-9BScel?xWZDAIziVT=*Sj=K8oV&udvtd4nQF 1mbn2C=< zxlVbG>&pcwRs!EkM`b-Lcpvd+m4I%@B!Mc1EYSuKWyXD0{PD^Ww&oWXlwfDaU&MeK zG)|>6Rv65IYRa_afwI$<@Q{Y$y(AbF6p 2Z~o%hQT_5 zjH5G|_(&W6wDy(Q)`plv--5%n5_9ROb^5jJO{a}-B}9}yW1l$Nnad@Hgs(t5s_6D; z51_RP|J@6WYQBtm?RiY-VW%dYlcN_ zV>pRMPu WfghqnWcxF@3dVUMQAcgKP?vR#rgw@-09 zwqc($gn}|;A|hCX%Pm 1Sq?b9TEffp>bLiT1Z?UR>}O=zTb6o758}78<`T2WQn-negY|GkhB(u8x+4%B z+H;O3{l`A;A4HfB23gw(pL^EuaJWAjMfzL)U=#Lr^c2^8_~%n3W_+nq3v5?zUU1f& zW|EEMFYzK7K! *K3N% o)AUAx)0s)ZmkV>rO&kp$ea5rgsm`6YG~|zN&d- z#9sb%8IqFo)%3+T-%2J=fF(ApwAM>_PLZ+kG|3^W+R@`dVua=XuN!ILfOaFf_aKXD zu1Yyo@AOS?Mf?r>*S&yaQ~NDJw@(r|{FK{~0>n4V9F*4+-{qH&2=}`tV#4(1BL{6P z(aQSQO~s}F#Zh|7yXA o{gEx=v&wv?)< z;<@Yx*|npQHcl+xd)IL>LfwULz7PJaQ6~^D=LFELpI<=fZtxmE$UbkbuyieOQUmy@ zefgcrtnR@5@xl{Zf1EUBgX*|rxG)Ei=>CUx^(=Xeq&J-0%6zycdQ|+Xy&$-c$XDNB z9ZiYrSsl1|O1GN^8>OuqyyVC#NF{Api3KQUA4^?t_?a&(GM9VmNj-f4fkr?{>ixcD z )Hl$`FcWr%4moV#crz~XCFZ$_TioUC!6oj zBxAO8IhBKNRJcymZJ8 h4U?HggDkRum~v zp}=RGSU*s#xa_n%Oo x>etzq9kL3HHEql6<6Bms98`tEBBkNfw*sZj@6;e4qbG)W%nv*s6 zu7EpxkVtzfJu1~on%sI}*AG*gn-t%tpIl@Vv)p)TE8R!FqiAY{*&2I-%3c2ntms}3 zA~NdoR-|&qUUZ5aq&XNwLI_swKejF6Hh$u=;Y_^FT}-6AH}zv}d4B%oF0GrsWuzCR zzk|;dLi&@56n->g)m`zS^Gwa$Gd6>HQD^ kXq>K9~ kJPnug~>f z7ktnb3T`Mp&gQj?$A9** tIO^!vE4s9dG;SJ#bGNe)A zCm<6#F2JZW }yT01Z z1~dp}e)#}n)TQcg)V7?Cf#jxL?|pOwq+Ud`q&R*1k0*Gz`0#=aw@M`!@6=wqouiQ+ z^4{@rBX`oiBJ6jmw$5VJmTA-T!F(OD{2lFC;s?2Bq|{X~g6XGeb@KGj#0KsLON^iO z@b&X;IBQG2t^OehduUoyk2;4Gq_r~PUOi?znTb+@Ubm|g0 7ru~8Xo60 z7`Pa~p%fpt>4wDACOUi0#&!cP;0AVoc##|^!5cB}HpWkxyJbuVkjx5KG4+0JWE7}r zq~&YQ(KPg?cIwmP^kDeOL-ZcGO8WXf{Xt@%ic4PUr$D&)WG-DU@wU=l^8~Or(fDdP zh?)q4`Xz2xe75(fT)2{_sX@Be9i0%!1k-p>7N6D~>x}~2+~_y|A`JEbZFNOQS+sBN zG3lsWaBj^yFz7Um?|f+TQ)*)LRGxE`G^8(ISDjT!1+|zyovYSgyyL4X >1&D}t2Ohc$@NN2$!u-dx`>>e#-~cZ6%&+P zEzMWmR?_GAI-r}U?aX&I2w-rZqm%dnN#KAu)*ig2vR%g2;eWcLf!_`sh+yVgHv3*V zq`BgGbs=wWugWC$_2D#nyaruMsk(_)KDoyJ7f;&hzob`F3QrsQ#nblo #B&bjM60w93&x GzwOewL|CmYjhNqO^ffClh5vfw4C0tuk+xNsM=q~UD{jPVR;quMF z`e*YS?c};bd(*9y^nyv%J=R20O3ZL>qW9yF+f1njK>X(4_bO>PbN+}xxpMXH-hXz} zMYic_?*ra%`rT`tPN4@DZaK?%yIGb}N$#K%x6g%tz$F-=oX2T}H{Nqksk+#|OgJzd zAp9_0l+qQEGb5# zcB R1O?J%h>t&(bBaeYY3Nd`j~0n`G=Sm8xZ@6zY+6@|lgiv)C<}t8C6~ zx(;X=IWZSM aPD z%cLJZk2llzFnOu}dqq-Qq~uPs7s>&A5BnNQtKi$gaOE^3XneL2yn!S9c_#zFyxR(m zWvlEFz7xOjil`3(2=F~lTO^u{{GW@qm%6T=T${DUcft@uv18Y5_uKs~Vh!jaoW?A9 z!o419js2{d#&h}h)RUTl2cgh6FTL05S;l!2eEx=RQpGiIiSqKrUS!}rWTb2z4-c2< zErGQBNyIWUHec0Q2v-BE-aL#-b6vG3V#g=?;yp+;jfoG$CJ&O cG3E@S^Bahb_u4h){Ed*zdAKN)l7<_EE zs}v?$|9Ro kbv4?HK*8%FOzcmX;eU;og1@#HLPUA9B(BT&d<`iN# zR>H9HZGhqBlk)DpcL3GwX`#ZoVX&78n_bs%WP7KEHfZDz^nSVdIJ-TN492PB$ArgH zH4}uOmr-tboHtT&pH{YNz 47iR{GQX -N#UJH1-K5XZl0kt0(qV1VIwcb{>y~)wd4m^*Oc1t%|k26mE`)sYUx(rnN zv!*y!oa>N8=Z__i2K@sCvh~f;)>06y$3raTh~s!@L5sR8ymGoNoL