From 3e25b85e57e3349fcaca022655e42b23d6ae37ca Mon Sep 17 00:00:00 2001 From: stevenjj33 <75509501+stevenjj33@users.noreply.github.com> Date: Tue, 27 Oct 2026 08:07:26 +0800 Subject: [PATCH 1/6] fix(webui): route the terminal's font wait through the browser platform MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The workspace panel reached for `document` directly to wait for the fonts before it measured the terminal's grid. The dependency rules confine that global to the platform layer, and the platform is where the wait belongs: a component that reaches for `document` itself is a component that can only be placed against a browser. The browser platform now owns the wait (`whenFontsReady`), and the panel asks for it the way it asks for every other browser capability. This is the baseline's own `check:webui-dependency` failure — the gate reported these two accesses on the untouched `webui` branch. --- .../features/workspace/ui/WorkspacePanels.tsx | 9 +++++++-- .../webui/src/client/platform/browser-dom.ts | 19 +++++++++++++++++++ 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/packages/webui/src/client/features/workspace/ui/WorkspacePanels.tsx b/packages/webui/src/client/features/workspace/ui/WorkspacePanels.tsx index 3cab3ec5a..dc1139c08 100644 --- a/packages/webui/src/client/features/workspace/ui/WorkspacePanels.tsx +++ b/packages/webui/src/client/features/workspace/ui/WorkspacePanels.tsx @@ -525,7 +525,12 @@ export function WebuiWorkspacePanel({ state, dispatch, sessionId, workspaceDir, terminal.open(host); let disposed = false; let initialFitFrame: number | undefined; - void document.fonts.ready.then(() => { + // The grid is measured once the fonts have landed: a measurement taken + // against fallback glyphs is a measurement of the wrong grid. The wait + // comes from the browser platform, because a panel that reached for + // `document` itself would be a panel that could only be placed against a + // browser. + void dom.whenFontsReady().then(() => { if (disposed) return; initialFitFrame = requestAnimationFrame(() => { if (disposed) return; @@ -570,7 +575,7 @@ export function WebuiWorkspacePanel({ state, dispatch, sessionId, workspaceDir, const input = terminal.onData((data) => { if (terminalOwner?.canWrite) void terminalOwner.write(terminalId, data); }); let disposed = false; let initialFitFrame: number | undefined; - void document.fonts.ready.then(() => { + void dom.whenFontsReady().then(() => { if (disposed) return; initialFitFrame = requestAnimationFrame(() => { if (disposed) return; diff --git a/packages/webui/src/client/platform/browser-dom.ts b/packages/webui/src/client/platform/browser-dom.ts index 369074c83..ec2c93351 100644 --- a/packages/webui/src/client/platform/browser-dom.ts +++ b/packages/webui/src/client/platform/browser-dom.ts @@ -101,6 +101,20 @@ export interface WebuiBrowserDom { * palette. */ readonly readCssVariable: (name: string, fallback: string) => string; + /** + * Resolves once the document's fonts have loaded. + * + * A terminal measures its grid after the fonts land: a measurement taken + * against fallback glyphs is a measurement of the wrong grid, and the shell + * that follows it resizes to a size the terminal never had. The wait + * belongs to the platform for the same reason the resize listener does — a + * component that reaches for `document` directly is a component that cannot + * be placed against anything but a browser. + * + * A platform with no document resolves at once: there are no fonts to wait + * for, so the caller measures the grid it has. + */ + readonly whenFontsReady: () => Promise; /** * Creates a drawing surface. * @@ -223,6 +237,11 @@ export function createWebuiBrowserDom( .trim(); return value || fallback; }, + whenFontsReady: () => { + const fonts = documentObject?.fonts; + if (!fonts) return Promise.resolve(); + return fonts.ready.then(() => undefined); + }, createCanvas: (width, height) => { if (!documentObject) return null; const canvas = documentObject.createElement("canvas"); From 06a7209c57f03a9b89355bc5c05883b8259901c4 Mon Sep 17 00:00:00 2001 From: stevenjj33 <75509501+stevenjj33@users.noreply.github.com> Date: Tue, 27 Oct 2026 08:07:26 +0800 Subject: [PATCH 2/6] test(webui): update the source expectations the terminal and browser work left behind The terminal and browser work changed what these suites read. The panel mounts the browser host for the tab it kept mounted rather than for the selected one; the launcher hands the browser its own normalised address, which is what gives a bare host its trailing slash; the terminal's create button became an automatic create once the roster lands, so the panel states what the service says instead of keeping a copy of the limit; the browser control is an icon button whose accessible name is an `aria-label` rather than a title; and the shell's stylesheet list now includes the terminal's own stylesheet, which is a package stylesheet and so is not counted among the shell's rules. Each expectation now names the behaviour that replaced it rather than the shape it used to have. --- .../unit/browser-web-tab-toolbar.test.tsx | 11 +++--- packages/webui/test/unit/webui-shell.test.ts | 5 ++- .../test/unit/webui-w0-css-structure.test.ts | 9 ++++- .../test/unit/workspace-panel-state.test.ts | 38 ++++++++++--------- .../test/unit/workspace-tab-strip.test.tsx | 7 +++- 5 files changed, 44 insertions(+), 26 deletions(-) diff --git a/packages/webui/test/unit/browser-web-tab-toolbar.test.tsx b/packages/webui/test/unit/browser-web-tab-toolbar.test.tsx index 8c58d40d4..59c7bc0ed 100644 --- a/packages/webui/test/unit/browser-web-tab-toolbar.test.tsx +++ b/packages/webui/test/unit/browser-web-tab-toolbar.test.tsx @@ -136,11 +136,12 @@ describe("a web tab's toolbar", () => { }); it("is carried by the browser business's host, over the tab's target", () => { - // The toolbar is rendered by the host the strip mounts, and the strip hands - // it the tab's own target and url — so the row and the frame describe the - // same page rather than a pair that has drifted. - expect(WORKSPACE_PANELS_SOURCE).toContain("canGoBack={activeTab.content.canGoBack}"); - expect(WORKSPACE_PANELS_SOURCE).toContain("canGoForward={activeTab.content.canGoForward}"); + // The toolbar is rendered by the host the panel mounts, and the panel hands + // it one tab's own target and url — so the row and the frame describe the + // same page rather than a pair that has drifted. That tab is the one the + // panel kept mounted, which is the one whose frame the reader is looking at. + expect(WORKSPACE_PANELS_SOURCE).toContain("canGoBack={browserTab.content.canGoBack}"); + expect(WORKSPACE_PANELS_SOURCE).toContain("canGoForward={browserTab.content.canGoForward}"); expect(WORKSPACE_PANELS_SOURCE).toContain('type: "return-web-tab-to-launcher"'); }); diff --git a/packages/webui/test/unit/webui-shell.test.ts b/packages/webui/test/unit/webui-shell.test.ts index d96f8ddfe..9225bc9f1 100644 --- a/packages/webui/test/unit/webui-shell.test.ts +++ b/packages/webui/test/unit/webui-shell.test.ts @@ -315,7 +315,10 @@ describe("WebUI shell", () => { const controls = renderToStaticMarkup(createElement(WebuiWorkspacePanelControls, { browserPanelOpen: false, progressPanelOpen: true, onOpenBrowser: () => undefined, onToggleProgressPanel: () => undefined })); expect(controls).toContain('aria-label="浏览器"'); - expect(controls).toContain('title="浏览器"'); + // The control is an icon button now, so the label is the accessible name + // rather than a tooltip: `aria-label` is what a screen reader reads, and a + // `title` would only add a hover affordance the glyph already carries. + expect(controls).not.toContain('title="浏览器"'); // The control's glyph is the tabler browser icon. It used to be a globe // drawn by hand, so the assertion names the icon rather than the shape it // happens to be made of. diff --git a/packages/webui/test/unit/webui-w0-css-structure.test.ts b/packages/webui/test/unit/webui-w0-css-structure.test.ts index ccdcc26d5..9255f555a 100644 --- a/packages/webui/test/unit/webui-w0-css-structure.test.ts +++ b/packages/webui/test/unit/webui-w0-css-structure.test.ts @@ -238,7 +238,11 @@ beforeAll(() => { .map((match) => match[1]) .filter( (specifier) => - !specifier.startsWith("katex/") && + // Only the shell's own stylesheets count. A package stylesheet — + // `katex/`, and now `@xterm/` — is not the shell's CSS: its rules sit + // outside the components layer, so counting them would answer questions + // the assertions below never asked. + specifier.startsWith(".") && !carved.has(specifier.replace(/^.*\//u, "")), ) .map((specifier) => path.resolve(stylesDir, specifier)); @@ -512,6 +516,9 @@ describe("W0 · stylesheet composition", () => { expect(imports).toEqual([ "./tokens.css", "./shell.css", + // The terminal's own stylesheet, brought in by the package that owns it + // rather than copied into the shell's tree. + "@xterm/xterm/css/xterm.css", // The shared dropdown surface: the look, the motion and the shape every // floating popup in the client shares. "./dropdown.css", diff --git a/packages/webui/test/unit/workspace-panel-state.test.ts b/packages/webui/test/unit/workspace-panel-state.test.ts index efdd56b54..a00e8582b 100644 --- a/packages/webui/test/unit/workspace-panel-state.test.ts +++ b/packages/webui/test/unit/workspace-panel-state.test.ts @@ -417,14 +417,14 @@ describe("the terminal roster is read before it is shown", () => { it("does not paint an empty roster before the first answer lands", () => { // On a reload whose sessions still hold PTYs, an empty first paint reads as - // "all my terminals exited" and offers a create button that would exceed the + // "all my terminals exited" and invites a create that would exceed the // limit. The panel waits for the roster instead. expect(panelSource).toContain("terminalsReady"); - expect(panelSource).toContain("{!terminalsReady ? <>

{DESKTOP_COPY.terminalLoading}

"); - // The standalone create button lives outside the ready/not-ready branch, so - // it is gated explicitly: offering it before the roster lands is what invites - // the create that exceeds the limit. - expect(panelSource).toContain("terminalsReady && terminals.length === 0"); + expect(panelSource).toContain("{!terminalsReady ? DESKTOP_COPY.terminalLoading"); + // The create waits for the roster too: the panel opens a terminal only once + // the first answer has landed, so a reload cannot open a second shell + // before the roster has said what is already running. + expect(panelSource).toContain("!terminalsReady || terminalTargetId"); }); it("drops a roster read that a later read has superseded", () => { @@ -435,16 +435,20 @@ describe("the terminal roster is read before it is shown", () => { expect(panelSource).toContain("if (rosterRead.current !== read) return;"); }); - it("says the limit rather than discovering it through a refused create", () => { - // The panel knows the rule the process half enforces, so a full set is - // stated before the request instead of after it. - expect(panelSource).toContain("const DESKTOP_TERMINAL_LIMIT = 5;"); - expect(panelSource).toContain("const terminalSlotsFull = terminals.length >= DESKTOP_TERMINAL_LIMIT;"); - expect(panelSource).toContain("const terminalCreateDisabled = terminalSlotsFull || !terminalOwner?.canCreate;"); - expect(panelSource).toContain("disabled={terminalCreateDisabled}"); - // One helper, not three copies of the create-then-catch dance. - const createHandlers = panelSource.split("onClick={createTerminal}").length - 1; - expect(createHandlers).toBe(2); - expect(panelSource).not.toContain("terminalOwner.create(activeWorkspace).then"); + it("states what the terminal service says rather than discovering a refusal", () => { + // The limit is the process half's rule now, so the panel keeps no copy of + // it. It asks the owner what it can do and says that answer — an absent + // service, a service that is not ready, an interface that does not match — + // instead of letting a refused create speak for itself. + expect(panelSource).toContain("terminalSupport"); + expect(panelSource).toContain("terminalAvailability"); + expect(panelSource).toContain("terminalError"); + expect(panelSource).toContain("data-webui-terminal-availability={terminalSupport}"); + // The create is gated by what the owner can do, so the panel never opens a + // terminal the service would refuse. + expect(panelSource).toContain("!terminalOwner?.canCreate"); + // One create call, not a copy per button. + const createCalls = panelSource.split("terminalOwner.create(").length - 1; + expect(createCalls).toBe(1); }); }); diff --git a/packages/webui/test/unit/workspace-tab-strip.test.tsx b/packages/webui/test/unit/workspace-tab-strip.test.tsx index 348bbfa7a..5e28f7eab 100644 --- a/packages/webui/test/unit/workspace-tab-strip.test.tsx +++ b/packages/webui/test/unit/workspace-tab-strip.test.tsx @@ -415,11 +415,14 @@ describe("the new tab page", () => { }); it("passes a bare host to the browser as a url it can open", () => { - expect(webuiWorkspaceLauncherUrl("example.test")).toBe("https://example.test"); + // The launcher hands the browser its own address form: a bare host is a + // web address, and the browser's normalisation is what says which one it + // is. A host with no path keeps the browser's own trailing slash. + expect(webuiWorkspaceLauncherUrl("example.test")).toBe("https://example.test/"); expect(webuiWorkspaceLauncherUrl(" example.test/a ")).toBe("https://example.test/a"); expect(webuiWorkspaceLauncherUrl("http://example.test/a")).toBe("http://example.test/a"); // A host with a port is a host, not a scheme. - expect(webuiWorkspaceLauncherUrl("localhost:3000")).toBe("https://localhost:3000"); + expect(webuiWorkspaceLauncherUrl("localhost:3000")).toBe("https://localhost:3000/"); // An empty box opens nothing rather than opening a blank page. expect(webuiWorkspaceLauncherUrl(" ")).toBe(""); }); From aed1d86629a01cb1d19c475ef286d999c8a63e33 Mon Sep 17 00:00:00 2001 From: stevenjj33 <75509501+stevenjj33@users.noreply.github.com> Date: Tue, 27 Oct 2026 08:10:43 +0800 Subject: [PATCH 3/6] chore(webui): teach the leak scanner the shortcut storage key name The release audit scans the history of every branch, so a key name another pull request introduces is read by this one too. The shortcut settings page's storage key name is high-entropy enough for gitleaks' generic rule to read it as a credential, and the baseline has to know it is not one. --- .gitleaks.toml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/.gitleaks.toml b/.gitleaks.toml index b222e6bb7..e14d42375 100644 --- a/.gitleaks.toml +++ b/.gitleaks.toml @@ -19,6 +19,13 @@ paths = ['''(^|/)packages/(browser-extension/test/protocol\.test\.ts|webui/test/ regexTarget = "secret" regexes = ['''^00112233445566778899aabbccddeeff$'''] +[[rules.allowlists]] +description = "WebUI shortcut storage key name, not a credential" +condition = "AND" +paths = ['''(^|/)packages/webui/src/client/projection/common/shortcuts\.ts$'''] +regexTarget = "match" +regexes = ['''^SHORTCUT_STORAGE_KEY = "webui-shortcuts\.v1"$'''] + [[rules.allowlists]] description = "Documented keyboard shortcut" condition = "AND" From 5841027fdd9a77c11727b7b0af1a40bb9256e5a2 Mon Sep 17 00:00:00 2001 From: stevenjj33 <75509501+stevenjj33@users.noreply.github.com> Date: Tue, 27 Oct 2026 08:21:52 +0800 Subject: [PATCH 4/6] test(webui): expect the launcher's canonical address in the tab acceptance The launcher routes a typed address through the browser's own address normaliser, so a bare host arrives with the root path the URL parser gives it. The expectation still carried the pre-normalisation form. --- .../integration/workspace-browser-tab-acceptance.test.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/webui/test/integration/workspace-browser-tab-acceptance.test.ts b/packages/webui/test/integration/workspace-browser-tab-acceptance.test.ts index 02aea1ec9..841302446 100644 --- a/packages/webui/test/integration/workspace-browser-tab-acceptance.test.ts +++ b/packages/webui/test/integration/workspace-browser-tab-acceptance.test.ts @@ -530,7 +530,10 @@ describe.skipIf(!available)("the acceptance of the workspace browser tab model", const state = reduceWorkspacePanelState(panel(), { type: "open-new-tab" }); const tabId = state.activeTabId!; const url = webuiWorkspaceLauncherUrl("example.test"); - expect(url).toBe("https://example.test"); + // The launcher hands the browser the same canonical address the + // address bar uses, so a bare host arrives with the root path the + // URL parser gives it. + expect(url).toBe("https://example.test/"); const entry = handler("openBrowserWebTab"); const opened = (await entry.handle( From 4a7146aa741f61b3aca89569fda156c1c66d1adf Mon Sep 17 00:00:00 2001 From: stevenjj33 <75509501+stevenjj33@users.noreply.github.com> Date: Tue, 27 Oct 2026 08:33:07 +0800 Subject: [PATCH 5/6] test(webui): bring the browser rows back to the terminal model MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The terminal became a workspace tab: the client asks the runtime for a terminal when the tab opens, the runtime records the tab it belongs to, and the panel watches the terminal its own tab owns. The rows still spoke the old language — a roster of '#1' entries in the panel, a '新建终端' button and a '关闭' button beside it — and three of them had nothing left to drive. The browser control also names itself with aria-label now, and the address bar is an input rather than a text node, so the assertions follow those two facts. --- .../browser/architecture-acceptance.spec.mjs | 34 +++++++++--- .../test/browser/browser-workspace.spec.mjs | 10 ++-- packages/webui/test/browser/fixture.mjs | 22 ++++++-- .../test/browser/native-runtime.spec.mjs | 8 +-- .../test/browser/terminal-cross-page.spec.mjs | 52 ++++++++++--------- 5 files changed, 83 insertions(+), 43 deletions(-) diff --git a/packages/webui/test/browser/architecture-acceptance.spec.mjs b/packages/webui/test/browser/architecture-acceptance.spec.mjs index 82eebf9a6..261e436dd 100644 --- a/packages/webui/test/browser/architecture-acceptance.spec.mjs +++ b/packages/webui/test/browser/architecture-acceptance.spec.mjs @@ -21,14 +21,30 @@ for (const state of ["absent", "not-ready"]) { }, state); await openApp(page); await openTerminal(page); - await expect(page.getByRole("button", { name: "新建终端", exact: true })).toBeDisabled(); + + // The terminal action is off: the panel says why it is off, and it opens + // no terminal and watches none. await expect(page.getByTestId("workspace-panel")).toContainText(state === "absent" ? "未提供终端" : "终端服务尚未就绪"); + await expect.poll(() => page.evaluate(() => window.__fixture.terminals().length)).toBe(0); + expect(await page.evaluate(() => window.__fixture.watchedTerminalIds())).toEqual([]); + + // Only that action is off: the rest of the client keeps working. await expect(page.getByPlaceholder("输入消息…(输入 / 唤起命令)")).toBeEnabled(); + + // The service comes back. The client re-reads the handshake, so the + // readiness refreshes and the panel stops saying the terminal is missing. await page.evaluate(() => { window.__fixture.setResponse("version", { version: "fixture", protocolVersion: 1, capabilities: { terminal: "supported" } }); window.__fixture.dropAll(); }); - await expect(page.getByRole("button", { name: "新建终端", exact: true })).toBeEnabled(); + await expect(page.getByTestId("workspace-panel")).not.toContainText(state === "absent" ? "未提供终端" : "终端服务尚未就绪", { timeout: 15_000 }); + + // And the action works again: the terminal opens for the tab that asked + // for it, and the panel watches it. + await page.getByRole("button", { name: "关闭面板" }).click(); + await page.getByTestId("workspace-panel-controls").getByRole("button", { name: "浏览器" }).click(); + await expect.poll(() => page.evaluate(() => window.__fixture.terminals().length)).toBe(1); + await expect.poll(() => page.evaluate(() => window.__fixture.watchedTerminalIds().length)).toBe(1); }); } @@ -36,12 +52,16 @@ test("V19/V35 an older roster permits an unverified attempt and permission denia await configureFixture(page, () => window.__fixture.refuseOperation("createTerminal", "permission_denied", "Synthetic terminal permission denied")); await openApp(page); await openTerminal(page); - const create = page.getByRole("button", { name: "新建终端", exact: true }); - await expect(create).toBeEnabled(); - await create.click(); + + // An older service declares nothing, so the attempt is still permitted: + // the panel does not claim the terminal is missing, and the create goes out. + await expect(page.getByTestId("workspace-panel")).not.toContainText("未提供终端", { timeout: 15_000 }); + await expect.poll(() => page.evaluate(() => window.__fixture.requests.some((request) => request.operation === "createTerminal"))).toBe(true); + + // The refusal stays a call refusal: the panel reports it and nothing else + // stops working. await expect(page.getByTestId("workspace-panel")).toContainText("Synthetic terminal permission denied"); - await expect(create).toBeEnabled(); - await expect(page.getByTestId("workspace-panel")).not.toContainText("未提供终端"); + await expect(page.getByPlaceholder("输入消息…(输入 / 唤起命令)")).toBeEnabled(); }); test("V29 protocol incompatibility is a startup failure before business ingress", async ({ page }) => { diff --git a/packages/webui/test/browser/browser-workspace.spec.mjs b/packages/webui/test/browser/browser-workspace.spec.mjs index b19096a6c..f6f405cab 100644 --- a/packages/webui/test/browser/browser-workspace.spec.mjs +++ b/packages/webui/test/browser/browser-workspace.spec.mjs @@ -51,7 +51,9 @@ test("the browser control shows its browser glyph and opens a new tab page by de await openApp(page); const browserButton = page.getByTestId("workspace-panel-controls").getByRole("button", { name: "浏览器" }); - await expect(browserButton).toHaveAttribute("title", "浏览器"); + // The control is an icon, so it carries its name in `aria-label` rather + // than in a tooltip title. + await expect(browserButton).toHaveAttribute("aria-label", "浏览器"); // The control's glyph is the tabler browser icon. It used to be a globe // drawn as an SVG circle, and the library draws this one as paths, so the // assertion names the icon rather than the shape it happens to be made of. @@ -107,7 +109,7 @@ test("the launcher's address box opens a page, and the panel is a window around // The window: a toolbar with the url in an address bar, and the page. await expect(page.getByTestId("browser-toolbar")).toBeVisible(); - await expect(page.getByTestId("browser-toolbar-url")).toHaveText("https://example.test/"); + await expect(page.getByTestId("browser-toolbar-url")).toHaveValue("https://example.test/"); await expect(page.getByTestId("browser-panel-frame")).toBeVisible(); // A window has no heading above its page, and no list of the browser's pages @@ -249,7 +251,7 @@ test("the strip shows the page the runtime published, and the panel follows it", // Choosing one brings its page to the front, and the address bar names it. await page.getByRole("tab", { name: "Another Page" }).click(); - await expect(page.getByTestId("browser-toolbar-url")).toHaveText("https://another.test/"); + await expect(page.getByTestId("browser-toolbar-url")).toHaveValue("https://another.test/"); await expect(page.getByTestId("browser-panel-frame")).toBeVisible(); }); @@ -288,7 +290,7 @@ test("a gesture names the page in front, not the page the Agent is on", async ({ await openApp(page); await openPanel(page); await page.getByRole("tab", { name: "Person page" }).click(); - await expect(page.getByTestId("browser-toolbar-url")).toHaveText("https://person.test/"); + await expect(page.getByTestId("browser-toolbar-url")).toHaveValue("https://person.test/"); await page.getByTestId("browser-panel-frame").click(); diff --git a/packages/webui/test/browser/fixture.mjs b/packages/webui/test/browser/fixture.mjs index 298a0dce5..a738c0080 100644 --- a/packages/webui/test/browser/fixture.mjs +++ b/packages/webui/test/browser/fixture.mjs @@ -393,8 +393,11 @@ export function installFixtureTransport() { // would make every terminal vanish with the page that opened it, and the // cross-page assertion would then be asserting the fixture rather than the // client. The runtime's own shape is reproduced exactly — `{terminalId, - // status, output}` per `server/operation/handlers/terminal.ts` — because a - // roster that answered `{}` would only be testing the client's tolerance. + // status, output, workspaceTabId}` per `server/operation/handlers/terminal.ts` + // — because a roster that answered `{}` would only be testing the client's + // tolerance. The `workspaceTabId` is the tab the terminal was opened for: + // the client binds a terminal to the workspace tab that asked for it, and + // reads that binding back out of the roster. if (operation === "createTerminal") { const sequence = Number(window.localStorage.getItem(terminalSequenceKey) ?? "0") + 1; window.localStorage.setItem(terminalSequenceKey, String(sequence)); @@ -406,12 +409,18 @@ export function installFixtureTransport() { // The synthetic process returns a banner so the client can distinguish // an opened terminal from an empty xterm host. output: `synthetic shell ${terminalId}\r\n$ `, + ...(typeof body?.workspaceTabId === "string" ? { workspaceTabId: body.workspaceTabId } : {}), }); writeTerminals(terminals); return { terminalId, status: "running" }; } if (operation === "listTerminals") - return readTerminals().map(({ terminalId, status, output }) => ({ terminalId, status, output })); + return readTerminals().map(({ terminalId, status, output, workspaceTabId }) => ({ + terminalId, + status, + output, + ...(typeof workspaceTabId === "string" ? { workspaceTabId } : {}), + })); if (operation === "writeTerminal") { // The echo is what makes a written byte observable in the browser: the // client sends it, and only a process that answers with it can prove the @@ -823,7 +832,12 @@ export function installFixtureTransport() { }, /** The PTY processes the runtime currently holds. */ terminals() { - return readTerminals().map(({ terminalId, status, output }) => ({ terminalId, status, output })); + return readTerminals().map(({ terminalId, status, output, workspaceTabId }) => ({ + terminalId, + status, + output, + ...(typeof workspaceTabId === "string" ? { workspaceTabId } : {}), + })); }, /** Which terminals have at least one live watcher attached right now. */ watchedTerminalIds() { diff --git a/packages/webui/test/browser/native-runtime.spec.mjs b/packages/webui/test/browser/native-runtime.spec.mjs index 792b27f6a..0a671bea4 100644 --- a/packages/webui/test/browser/native-runtime.spec.mjs +++ b/packages/webui/test/browser/native-runtime.spec.mjs @@ -71,7 +71,8 @@ test("V04/V06: native PTYs survive another page, reload and a full five-slot ros await expect(page.locator(`[data-webui-session-link="${created.session.sessionId}"]`)).toHaveAttribute("data-webui-session-active", "true") .catch((error) => { throw new Error(`${error.message}\n${JSON.stringify(wire)}`); }); await openTerminal(page); - await page.getByRole("button", { name: "新建终端" }).click(); + // The terminal tab asked the runtime for a terminal, and the runtime made + // one: there is no separate "create" gesture any more. await expect.poll(async () => (await rpc(page, "listTerminals")).length).toBe(1) .catch(async (error) => { throw new Error(`${error.message}\n${await page.locator("body").innerText()}`); }); const [terminal] = await rpc(page, "listTerminals"); @@ -101,9 +102,10 @@ test("V04/V06: native PTYs survive another page, reload and a full five-slot ros const latePage = await context.newPage(); await latePage.goto(url); await expect(latePage.locator(`[data-webui-session-link="${created.session.sessionId}"]`)).toHaveAttribute("data-webui-session-active", "true"); - await openTerminal(latePage); const spawned = once(child, "message"); - await latePage.getByRole("button", { name: "新建终端" }).click(); + // The terminal tab asks for its terminal as soon as it is open, so the hold + // has to be armed before the tab is. + await openTerminal(latePage); expect((await spawned)[0]).toEqual({ created: true }); await latePage.close(); const released = once(child, "message"); child.send("release-create"); await released; diff --git a/packages/webui/test/browser/terminal-cross-page.spec.mjs b/packages/webui/test/browser/terminal-cross-page.spec.mjs index cbd5b3ca5..378337eea 100644 --- a/packages/webui/test/browser/terminal-cross-page.spec.mjs +++ b/packages/webui/test/browser/terminal-cross-page.spec.mjs @@ -6,6 +6,12 @@ // was no longer listed or watchable anywhere. The Terminal owner now holds that // truth, and process lifetime belongs to `disposeProcess` / `disposeAll`. // +// A terminal is opened for a workspace tab, and the runtime records that +// binding: the tab that asked for the PTY is the tab that finds it again in +// the roster. Two pages that open the same terminal tab therefore meet the +// same process, and the panel that goes away releases its watch without +// ending it. +// // A unit test can prove the owner keeps a record, but only the built client can // prove what the row is actually about: that a PTY created while session A was // on screen is still listed, still watched and still producing output after the @@ -32,20 +38,14 @@ const openTerminalTab = async (page) => { await expect(page.getByTestId("workspace-panel")).toHaveAttribute("data-active-tab", "terminal"); }; -const createTerminal = async (page) => { - await page.getByRole("button", { name: "新建终端" }).click(); - await expect - .poll(() => page.evaluate(() => window.__fixture.terminals().length)) - .toBeGreaterThan(0); -}; - test("a PTY created on one page is listed and watched from another", async ({ page }) => { await configureFixture(page, () => undefined); await openApp(page); await openTerminalTab(page); - await createTerminal(page); - + // The tab asked the runtime for a terminal, and the runtime made one: the + // process belongs to the runtime, not to the tab that opened it. + await expect.poll(() => page.evaluate(() => window.__fixture.terminals().length)).toBe(1); const created = await page.evaluate(() => window.__fixture.terminals()); expect(created).toHaveLength(1); const { terminalId } = created[0]; @@ -61,7 +61,9 @@ test("a PTY created on one page is listed and watched from another", async ({ pa await configureFixture(pageB, () => undefined); await openApp(pageB); await openTerminalTab(pageB); - await expect(pageB.getByTestId("workspace-panel")).toContainText("#1"); + // The catalogue the runtime answers is the runtime's: it still holds the PTY + // the first page opened, and the second page adopts that process rather than + // asking for another one. await expect .poll(() => pageB.evaluate(() => window.__fixture.watchedTerminalIds())) .toContain(terminalId); @@ -84,7 +86,7 @@ test("a PTY created on one page is listed and watched from another", async ({ pa expect(await page.evaluate(() => window.__fixture.terminals()[0]?.status)).toBe("running"); // The roster holds exactly the PTY created before the round trip. A fixture - // or an owner that re-created on attach would show two tabs here. + // or an owner that re-created on attach would show two rows here. const final = await page.evaluate(() => window.__fixture.terminals()); expect(final).toHaveLength(1); expect(final[0].terminalId).toBe(terminalId); @@ -97,15 +99,14 @@ test("a PTY created on one page is listed and watched from another", async ({ pa expect(delivered).toBe(1); }); -test("a watch is released without ending the process, and a disposed PTY does not come back", async ({ +test("a watch is released without ending the process, and an ended PTY does not come back", async ({ page, }) => { await configureFixture(page, () => undefined); await openApp(page); await openTerminalTab(page); - await createTerminal(page); - + await expect.poll(() => page.evaluate(() => window.__fixture.terminals().length)).toBe(1); const { terminalId } = (await page.evaluate(() => window.__fixture.terminals()))[0]; // Closing the panel detaches the watch. The row is about the two halves being @@ -119,15 +120,16 @@ test("a watch is released without ending the process, and a disposed PTY does no expect(stillRunning).toHaveLength(1); expect(stillRunning[0].status).toBe("running"); - // An explicit close is the other half: it ends the process, and the roster - // loses the row rather than reporting it as exited-but-present. - await openTerminalTab(page); - await page.locator(".webui-terminal-actions").getByRole("button", { name: "关闭" }).click(); - - await expect - .poll(() => page.evaluate(() => window.__fixture.terminals().length)) - .toBe(0); - await expect - .poll(() => page.evaluate(() => window.__fixture.watchedTerminalIds())) - .not.toContain(terminalId); + // The other half: the process ends on the runtime's side. The roster keeps + // the row, so the client still knows that tab's terminal, and it must not + // answer the ending by opening another process for the same tab. + const creates = () => page.evaluate(() => window.__fixture.requests.filter( + (request) => request.operation === "createTerminal", + ).length); + const before = await creates(); + expect(await page.evaluate((id) => window.__fixture.exitTerminal(id), terminalId)).toBe(true); + await page.getByTestId("workspace-panel-controls").getByRole("button", { name: "浏览器" }).click(); + await expect(page.getByTestId("workspace-panel")).toHaveAttribute("data-active-tab", "terminal"); + await expect.poll(() => page.evaluate(() => window.__fixture.terminals()[0]?.status)).toBe("exited"); + expect(await creates()).toBe(before); }); From 6778c37285548c7683cb605840ab4d7c5a68e744 Mon Sep 17 00:00:00 2001 From: stevenjj33 <75509501+stevenjj33@users.noreply.github.com> Date: Tue, 27 Oct 2026 08:50:44 +0800 Subject: [PATCH 6/6] test(webui): record the launcher's Linux baseline again MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The launcher's address box now names itself 'search Google or type an address', and the panel puts the browser's own toolbar above it. The recorded Linux baseline is the old frame, so it is re-recorded on Linux — the platform these baselines belong to. --- .../embedded-browser-panel-linux.png | Bin 24272 -> 28378 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/packages/webui/test/browser/visual-baselines.spec.mjs-snapshots/embedded-browser-panel-linux.png b/packages/webui/test/browser/visual-baselines.spec.mjs-snapshots/embedded-browser-panel-linux.png index 4e8c5cd1310b184be5257e8275bf167a338d36f6..03108e2ea9c8d3720a9782339c61121776d9da22 100644 GIT binary patch literal 28378 zcmeFYXH=72`#p#SQBYA40jWL+NJo%PKrHm4^p13?p+g7|Q2~+OJ18ym-dm6=U3yKB z5<&?C2qc7({PVmszyHiyGvDUhtoK9C%1Tb|oO9jRea^o2**oH$mhv6y$J7)Q6n9ir z-sn(JTqVD*eZ6^={0Xu`C69vQ9);=~MLqv4G@Rz;@=P|KbgidLjMaCChT zJzA(LqVn_p8<8t?`&Jyc#cqpTz{FtJZ>5HGQS@Ztoy}#^3C}O=$3_Sjou~_;=dP}< zhlhvcQQmpOPeD_TA&T|?6!(9_eivbs{oEm2dv(v-mw ziNV`TJQF?4K}i#ugWB|!WB=eb1P}b>j>IG+X02-&g`~`#*O5#en>(ilH zv8iEJqmsmfGQIMn#1R_FNtVfH{l%ZJei{@#Qq}xYaywls*TZ@|O(R1RXdyMO?WF$r zZHCh0ZyVP{94A9GbfpHTv~+)H^+{QAI}3^!1x@i^L0^_gX{GnIf{ zG&&zsnOD{XkIq!v49|)VFpe+0Blr%d!2f4G!T=C8r;9elSIR2=l)UR)7-e5cUK-@1X(P=%#9pN#+nh*#( zhhv_igxU{YwLIb4n=!O>y}ruOc@R1dA!5e?_^ij_c8ZA4VF21a<0EXY+g8BQPrx0^ z_Kx2D;;1?#l3vJI#HiO8Jtu#PZm1mMkq@$*(e$h2H>uuVD0^Cq3tXqu-T)j;Wx6iy zcOva<2D`dYIXA-Fth8V2@8#1rB%SiNB~a#*K!!bIqq1<-oryM>M_)hxh1Ewd$#!&W zb+HxJMtJ4oEiatq&K*IJ`ggTQ0?+xTgqofrhhW@F?|rghoR{)9^a5rNoI z$ylse_0d^Co|K93(Yyf}^2zjcz0SYOe(bu1$2s+5A|Hq|pQG z>kESA4z&c*PNtK+q2>MT>weH8n#!Z8^P7*b;o^ zPlES(9jv47qgu-x0+ExEArmiq<--jXE9@`Y;DcqaaaLcFG%73qYWVxhqg0sQ)cSml zjd1%xcYidqaO+myFIg_{f~2!!s6qv|vsw*)4+t^k#?4k)R%wl!+S}VdZX%I?CU@9N z*KfUYu_-wa0kD}Z?}H=v1GYkLGSVaB)SS zY_)^1wpYXfLTzYK-sjPWuQ&-$je}3hEHBp;i0id>!_&5!^0?Nvw!-dYIWdd6k&Vk9 zg^SmUiXqFvyA@3Z*V}h1+ctc?ys*N-hmn<)l^nn<@@W;vcVF=a{)q9^Yo(ENDTZVJz?Uk6HW1CDw63o1cMQM@wltNCQc{*s zmIg3xS}i3Q>qR>cIscu_Aj85p)=|-Lgq(D)>Zq>Xn2s_V&S~b~zdTdEpoF(-;tk-J zFZYU8E~SCvlJfZNai7bBUrTO{vBsS(tPQ}q(svjVZ@R-%_H?Qbv+#i33Pm+VbDndtwRN_UIxWtQ_WYdxh-`9xt{pd$@~1;CH7;)#axnhQL-1 z)hk^tVz0D3-|$^2!4{=~ocL-akDWAXYHIkZuR#yBV+X|i9rRvWT3S}4YM(BstEn+a zJw+j&Me_6U@zyW7s1Ct&?UvJPCiUBpKP&W^8D8`v1#6_8ED`T#^Oo=2iJ7eg$ol)|s*Vo@JIYbTQTc|^BhdVJJ?F%*xbli7X@ki;Kb`s`OoYnUQ zY6yPs7g|oXd2$hFzLWY*Kmo`3qb_*tP*yI?yL}r9%?;l2irs{tqdA$w>4V*n z22-yPMxd#pJ%0u-Ym4o(h6T2D%S%?^UX@FP0&s>B)th@J^>kD4FAF6__qPfD(syZr zX!=KwTFojoGbNb7rbZRB%50$Cd1O(0yV*`VJLzxkEnO%n3Ab{KSR zT4<6x?xOMAoZ&my7p{%-a*_O*X@oK!5U+etK$!ver(``Q-aq*25@GFzgwe8#PjG7pLtI_ zoVJf%zV@(jvVs=S0zGb=J!fAuIR4u&W;1HdztBA47VD+w2KuP3|9TfSTLmGEfidlk zTcy#=>qUX5-PMHlUm$r(in`SZo}AMyWd)hpu?^9l2A|#8?M#H2C~zwfF`Woho6~Q` zbe?aZ8eNw%GBV^(O0=`d_;27^ZC#xLHhuhe_JgmrpY*=|${k=2(5^T0z-acjXH|S$ z_k*3^Z&2pmtmI?yHF>1GFh$$`qq12IKxhG?quO?GU>NbFY6Ywt0RZ+eNCpA^YLOxg z1weCdXWO(uz9Uj&+X+&cy;NY)8`CNlvuOG*0K-X(FgPY&Pq7pQ9CqF0GEe5mt|q%)nkH;H4lx1y(-{hOg9;wB6ve3unLR;*pSjG|y1XE90@& zTTPG~Y>$b#OAdhI=L#}@E>}iAVVcyP$|nPeRM-V_K!JGpD=v{)a@qaMbt^l&!i;iW z)-LxsbHBai7A$TF2tn5j*y`tiHkk-e4Kynt;CRuwDmjU`#sE7Fp$+`|@4|S1DuK7W zcD>lo1_=V&+v|R13r&Gy78xWA>4Pt!ApjYwRObb3w5fdCj4!G#cG;o;5lq+)@2nw) zm<(S9w~qAaBZ4j^(rGo+Y6YJyx=*u~V`0h4;K(Bqi_76B=dr zIf=D@YJY4UukIeb)cU)R8h401xm2Y2StA;f`0JlV%ThDmv`py#N-U}qsQ-O8x3Ee zi{;Dcm0|2|h?i|6x|!6;W2%l>_SW5I%8#!M1IP}uc+j}m^d-yH4J}oVAzIf5&vPry z-UaOSM;p>dO96^+gXIspW}BDT2>Y$uXAh$^7d`&rc}Mxls-TSa=%c3Q=&&$sd2%u# zF;VRJ!wsejInNChd;86>JItSR3%QnCi%R-)Rzn#q;?mo9cAR|9-WXF-Qd$y^QWcKk zW1o%Ow5ILaxE|Y45;-z2_ItCdhgN}wnVH`vbIK5Bt=G%;Y48aS;n!FbZu|y)B`phq zKy0`1Y0jaOpsaWkq*(0y zHHr#Vsr1n)Py1J(MUnQK5;as5t%(@|@dNU~|HB1rvGYNu*B+198 zCJ!(uo3RyrOZklAruwyO*ZOa?x>V)LfpqpGVq=?(1e{XtfxW_#`~Uzzf;)C|lxB%P zfTBxe(mDNV!Focp&n<=RAzLiq$rDA1m6<3TH=tYG$nO4r%3l7)hOH=V*D@C%?bv2A zQ#F?xkeT732lo0DC~9llWnUMHD*S%sugjnmbi^V878Vj}m+WFdnEmqlH7uk^$vn$kbzTb9) zB}03C-9K}44(Jo+z~`VR;^N}UF)Z=4(HDJLSu6<;x>&5ey(icE`ua+?8P=S}r5?Wy z{U-J@bUOZ%%|J{C@od*K9|j{O8(Nf9RT=)oj>%&DuWf{7Tpg_Q2$zJr;rHQft`er6 zp0yWQ3Xou90rqOh%LY6ca{ZT!zp3g8XogNJv}`)w@T`UKin&a>jShbQuo-| zAw&Wy*K16=KU+Q*h#JdIV0Us-s@%~tun-mC<>md&fyyOapb~kshpy^aF@L$Hb1|r% zm}*mF^e^V8r0@#yC`d_ScRCBTkDJ&hzv0QUa?Rxt>tH#2vJ10A=ocVqDwtq&l)e#R~tEHb}+o>C>mb z$H_}ZR-O0NwATf6IW=`V77I+)ElpSyDhem8On_-fIxUSz9zbViCUdXN_t37t+RKztqn;yNs6Xf zXB#Z-38x*FTJ2-76yPo7R=1yv`Z8n}I#+JYDT&*M^J_ozdrSv(7@ogiG8?zr0K_}O z<}4v_#*K@0c2e=xlNItLt#5nsQKWHxKK6OSP^%#3%@cDkL4%r*^z)&wD2KGsJN0Dl zRokYeXnt!A_+PBX;jMv+Gj(-!E?ql4F$D!k-*@`qRAJ#~&*C6M$B4HK<^>cVV&Xd8 z`l&nL-St#EI=P|JRQBpkD=|)+$=<){OCr@;FS;DOqWX<77k(aG=8Ed&nQ=-vMcUV*hd|^Ou|%!q4xJ`81^-|69c(h zTU+fK3A$mB|}y9gydfwAu{`Fc^%49y^Mep_sM+sN00Vkz57KB1l_X8yUVly*X-M zw=8YraD3&HDkCG~0i{YP%C`;4w&0t)N@)N2twXy10Y`35XQw=L$}uqQA;s64Es6yl z+SP4E?&$f<#E~o16z(aE?Ef#z^S?`WFO0_l3~swh@j)wnSs!anUd$*cMx<|hUj4tb zx&Ifh``>r}jm!Tg`Tv^?|4oMfA1A|ue2T9s>Okd=e8ud-;^N{Nrs3%qQrjK|4X}za z#M|^?`g=dW0XS?ez(CR$I|W?_3{3u5m=f!ySKRTg>5`NTG;qLZ&jIWrv!S0L)im_?SU>C;Mg9}qC-)k$H|tr~oIZ>nzNYT_NFKVGUcI4TYuj|v9{LTV+1j`?w4(azTNk+^^cKZPNFN2t#%>Mv68Llv&AZ`f- zVi)HVSQ5MaH`6P40An?5K@A?H6I76j=BRTt6?ezgm%W_0&9DsamH5Vpe#98;BscYb zS&{wA5)^HKRx5XFAk^!nqp%tgD92E+iE-s)-+d4Ijr1|q@NlXn&K_!I?!JkmJW3o7 z%0B_}6>k+ETHX{K3=`8Y6yRd?x@m%|^cNu(pC)78Lh zF1;Mq_rdqKKAv|Sj+OYDwmEalv`5q>u;nJ$r?cCJWZ%)vK+ptxrjT!LL5AL^{%;+> z>pPrEDOYxZp+9VURXQ#!Cqwx^3sj>{f2y)O5)p%&XO;aNG-gT0%S+m5Mdw}%+eNVk zkFnNAmd=trW>ekL7HHMYJ$YPMMv{WWb=kjwC)~ltCY?)5P>SgL;P{fW<|8HMQbB>up zV?+x=3wPX08|iJ2ePHD~hi&aU;LpMbA#b_(L|Tgf9o7Ba(R@Uuh4`&1L){%sPD2=M zUi87UZ>$b51&4zmDFz-PvyYmn+S?R0l9Jb@15THZpEG-SQXNG5Y;~_qe+U=x^2c6h z0w->8JYDw~3o$`IIefS+E?C<8%CeK<=};Yn$m zWa;I`Gg)19lKId^xd* zcpho2X%=^XtlLWiQvAWxzw}IN&eU1d@OlIQnOn9bdpJ)c|IF#TXvNsEA^7rID<&Vj zQdc>2qY7M4>lt7@-N5C5b^W#6%Yn+``Qx2s_YL z#mi3JUtZgJ2_AQCKF*hM_sjlWlEi`J_5c0}o&N|4-)H_4;7}OUfT%s25Yt{hy`lyX zB-g_^WxyX#2Of$kW!<$Z{U0tsJ!j^=f9DmSA8yA;!z^b{(6`2WTkKLZ+x-JZ-*r;1 z8`l;#tyj?bI9m3O-4FKH$&5t!!TRc|ut5&xlu! zOKbl?DYh<;KPIO32`Sa@Ea;lY1l`iAElC!Ec#iWoO1D`n4vS+9NI;3xMDj0|eGzEy zu)?g2j_a>4NFx@?=7fdx>wbX?5KG=8D5>Xv-A}W4^656;axLV!Vr`C&@^z=icS<%~ z!BsYY;Rd-QgM(kQF7OuN+VU5Xo646f z$MsvD(<`4Vf%RDZ2lx{qPFMREvASx5G+r9X(OIorz4Fz?8!P-x*Ny+q;BgSEg=i-^ zh`hn0jfrwlI@4i^lhW;Dne)5ELd;(aZT(O@{6k@>1-|) zp5PnJDnI^_y~~E@#y+?aAD_9l-oHCSboQ&(!C)e{Z~G{!kAo_^{0M7uQ;>FMK&<;zuoxa#xz7{=o&$cqJa!SNr07eC0gYJE+ zn6ok3*d((Y!$FfYHgXfXw6}cZJuaMU5JV?4(||Y1zd!{LLbvKE59iPMPYtSO3-&vVuKpGW{u0S9y`9Qlt$AD!*2^j};jK}L4AzqWMTjBn$R@j+}^p54lE zL?1j~iO88~OVMv3PNQvv#~elWJ$Bb~z;?^mmV^}?8Zzv2U@1*2-a+G2+FBymu%m{C zV>!Zngsj54Y_Q8TWu=tDrZYQ}!w=%0HpF?;jq@qdGD!n>`@%QopevN0l)iPSe9-t^ zF~ZtW;3cJRw|+Y1saW~K`|tq-`uC((U45&fsp?i|S6TErOB0(M11i0OU+nv|w`a$3 zAd1uppF@-u{UEv94KTRj*dV}ZWuAmUo%`~*feC1l-J1e-31OVCh!*43E8d8QT3awZ~(Hfhs^N=4|Ut0*3G<#{nPp04U6QbR6}bTCD{C>mM~h z$31X-*RE`X;jj@6nho~wa|lHU1>|Kc<-E& zwP*%D0bUB4=L-k91=JsY{X?zE_TFH#U#~;*?6`k-=2BgBV7Z!g_YY;A;|On0op_6v zX8_4}x9bq9j9}LdFzU<26Jgon2tSC_pX2h!jEAq*yz<9e!~+iDd`_=W*%|v@o-y8o z-8Abq0*aMX$8)$0Ek8p~v&rgnJO8@YI*aJN$+!HP@2AA`#QwmM%V%bBl-9zny|^w| zj>mXk>JTKsDF%4Su04}xXyDm-P}VCPi;)QHuy&Mj2 zTAo5izEOL&rf8wMOb3*5TM|da!nseldHMP!b}p|4eUW{4+n;u20@n2L@2RlTpxlHh zs_fE))JenjSl=kY_Uz?fxe1%YJEx7YjcuFFvLLP9o6z&|Dn9(Ytag7t*Cm$3dJ9wV7}jt zNlF$BOAz+-q^Vz{SDnTlEXTp^aeNMNCFWjZ1NQZY*pPnqBg^+!U`p9?yUtBxQ66~I zvX4vBuMu-a;cAEI8Z(LPaG~Q4CNv`mJDFGfbjc{T|6vxe^TSB~t)M;E%Z7EAe8xcr z+Yq*xY6PeP1iJ3bC? z;qm9Rhq#$Q&<~%o3=W_l1UdSgfmQFlRZ}!%Tbkb{e^jyZ%6V!LHE3EYyMDD`091V9 z9)~g$gDaQN-cF-QjXwEd2M^bOSHn>C>A$2)4PEz`|*@8^8Eb@G1|F&VyrRo znk$d;NXDAi)2~?B1ofe>$$lQ0ec3Rv!+aYCn%jtpYgA`~~RjP{Ojho9Q?w}&ux-`U?Rw9rF$=R{1qy+v{>Iqa$O06@sv z_iZ1~b&&U8+tdt3PQuuZzZdTbw|}ero2S;oS1Hqr*+C}V0P+0PfXP@hoZsp_a6atO zN_@EfiU+A;OKNIzz-G3S!E1XWEt0m+Y*j=MVv#=8ANUGoN+flUBaO>66_qV%8};s9 zq7duhp9KOByVs<2{TW_7|0QpJt)s)KdrX3mHZ#TYCF{OQzqF;?i!*O{PE*HRrJPL` zRu~i2xj{g^S+d{H8gUq3S|>4OTA6f;^Ev)@6wDWV??}2=7obgIX4b=O8B!c%`vVqb zCdyLA86$c3*G7$)4hClLY4D}x;4jq=Wq@vAI`5Z8vP!i70x z9O;IZZ*0K*HDaru-j>Qy&PHJr-fJI?yPMC*(J=R~glM@<*FD!~0pvF( z8h6xaEqXgjB?YWMrzc)SjWF9v&n49jo;N`rpETW&!Iyfu>8?7M&-N)=Yonb5uo+qW z8%hU#1x{uTx!d!*RTbMohjTfvsX7K8&$t-*1#7tc0hrzP5$tpY;($h4b5P+M(ww@E z#H|&lSbWqjl!VNFPHWyQhaR_h62g#t^y@kM=yI|3W-Kvn*PK3Oc1bwruN2BM197`{ zo?IOk`s#`GnUcmLgqJO3+aneBz{X?)l6ITID_^E=$p~*PZh8DGx?w6DUI!A4yH0NfNekIQ@Dm z*>YF&*|$zbQcV|m>y6sSh9u3KE8|gYFbZMu_Q>)3e53}ymqyC-#()($z}Ujf zF8cwKPkPPwK9L7@-_>CDS#Jd*l{+9GI5cduDf^|r$OW`R^^5R0@nrmYKe{$QKTknw zG+gNR4q*XyB1b5uW)}4^Uy=FAANpHri>_xW3=2p;zUex75&W5e!ZZZ-!Ic+%ot(%V*j7_ zHhmV1>M`oAB9|AECIc*DN!uMmxTY_1J@jIt-P_JU03mms<%*+85`Ej|`CRPKl_oqE=`rMnN2R2uvH9!5)p{EH;)Rrqf`^+jB zcaT1AssM`2P|UPsubWOBZLZN$;UC0jue5f80E5u`z(*1?)4y3E#cRQs& z*0XGQHH@vZT3uQRGK*j29s?j3K8#zQ{(LKXPmZwWHe;f}=sdhOtc7pxczo`<>~~A< zZdG|!^0v8u&B5z(%hq|CGLoP&BClEn%gikso0K`f%~~k`SL#0c{mN;ZZmY%ll$82y zjbpV}Iy-}qeEE6~-kpRCF?kZZz<0v2-LJ9Nu1_rq_Y;H@CI;R%`=DYWl|_n$graZo z_qLVky&g9W?W**}IJxJ;hvvM^D8>JxJ&0=#>jGk{tQI*$uWI9t_*|5>xi#^8?ZfNG z34xaIS4VF}t%bW6%6Bl2H0zU@R&TSjTU>5pz4cBI{GH!Pw}&`e!SF9^JaR01a zuX@A}zs_KLiybvGEH!SQdwP`WP+=g}XFGnk^W}JoWfbw#xZlwVoA2czmpB_HK0y46 z0>pJh2a`cFy7^pwOy_Da$|mJ+CYeeiUAPDjGQ^W<5kEq2Oi$Z0VeX74WA2^=X@}=y z(q5cdYU#qy?sk4U*A~Wio@QG41@NuN9TqMH0ufZJNadw zqkGlI~~Y0hb_tc z+0{$5Hu2Y5L%{?jt*OV@`EeI#%a=eH7l}E$^TRx+_TJ;VY5w7gl!yPr1&m49X-gH` z4q}4kh%G@u(%T_f>am*wZ|}$6nEia)Gl$Om@s}W}o+&DZSO`*%31OCyRbg+ScXZJU zA*N(J)SX@1V87He7VwlM?(9x4BUY;H`K&9eQoi zh#@s9$edEQzBMVLqq^Rk2%_ni6|jQaj@wGS^{PlS$?|=n$jSr?wI;J7cBuvIzSJnX)0r*DL_>IiN3jSDTGf*7I4;1 zIf0x0co+8T`dhMKrm#rs@Aqtqd()rFnn>=2O%*fbf;RA4(OXp7qcc(@b$~%Cz6E~i z0|c))Z$YXQLFf4+HAU#7g@A+wexCfyE*E%d>4bYM;|H;$KX$0sL*t2e^PmuF%Sf8+hq z#RdKM@(V$-xjKW!UIwS6l>LkD?@0hzin!s*8W?$pnnGkf&>_KM5$Tz(!R^)c;h(#g z$p^Hs^vI6)*Z!5x?D?uM4$6M4FXs zNvF-hw5oPamS_r!c(nlkZB$_sEoy@%=;obaxhHfvI#{xnh>zlBK_bG!{MB zdmZ@S5MO#l2fpTz5O1c{c#Gm4bR4mL=pV)vn!)u1QE12E?nt@bN=;4i>E$nnsHPDDHiD7?^N(_1!_Jf9j+4w7|$dvLQ@L)6;5R)zp4i&j$FRcV;r0 ze~*uXLMzcM&O0uO(PU>-jYw9*cpZeb7c@6ZiHozcvLca4ox5e#e|>y>y>b5fz1TD1 z(0S;qVdgXF?K4!-x;Vwni1~RVPFxgZ%wUuuV|^zq#(juJH}N0f04?OS$C7k5HxSEi z7^3+lFE6huXwTWu%p)`-qcdjl$OLHNq4>SV$->aDsSbKLdUW@kD|Le_<1?{#Zo#Mq z_GEpnfpUuZ2u%;ttrHwXek29Q4mZ8F^^S9@wbMa!s zim^L^YvC64%e45s-{efyc%!ULJA!z7`syC|OGD}0E-UCQ;g*w&%f*O;yzcw=9@~Yf zL(%Q+?Lp_bj*Fw=+|ynjw~mupOdv{DS7s@)sjY5m358wRDPMxMT{O!sHcF}IR6|cx z%%Ocw5~c~d34%(fJ*NOKh<=@OH60G5B?FHMT8eFlUFG(&o;^yK;+6SahE;LZwDD37 z@7a!s$#hURPBM*4%@zgF*8@@?!^X`!l@!zC`p#bz7+>eaMOQ#URAc< z++))0_F6Ho&etz|iZlz(iWYAeL;q?WPiP5r-8_fw?3FCx18n+F?N&<)WA*p>!2|aJ z5D#BO?S)iTT?N6&OLL)2`Ejbbr-y~q!>AxQYfR32HU1S<=)n5U)|PRz2v5((T+*!S=D&Q^{;>q-NGc7qvtHO|typ8GrM z8s~TeHu;oV&xx(bx0$-0-whaPaXu0aAOYU(!Tvl%r+h?X%=Z}4@jpB#kkBwY#5PKY zg1e-#(V&}GQqoY9cJSM;4wERFsG4!HoFq%)g?n zRkGI0t9y>6Rj&IuLYQ1Qv~6kz?$?~RBzK%%nA;dz{I?wtjZ6S-lEq-H))OVqUxD31 zqR2-O4ehO)^W8HzWA;F7K!8ll9dN{r*eT8)Sj7OBxFUA;tBM1(qHMd|eOO6Qv(hJL@k(@Srs)C`OBW-!yT4-PXrm(TAy zRW8GekJOLa8<2J}QQf!aJhXiWYkO^|EGmvR89b4EA6k99y%`hLfSm+;HMQ>+lUDBFlZ<-he$3`w=)l7I;WK5g zIyKejU^r{IA1pwCi*sT(w@J3XtR}y0$#wLm0A|mvTy}p!b7}9Xz6y0%G%CJ znb=rhA7Vf3`)mDZSFPCZF`2NB+uzrgH^lm_HE*SZhQ*XYRkro@b@_tM+qKJI#h9Nz zX=yVso=WWC0o;ldtJn{F-8RgeJ)3l0*|qNplEmVGw5y-lM;K%#)Rh-}eBb?T{N;yl zlY%{c$~&^21_iM)5-y^q6%*O|^vRnK!+C^Ut?~+sN^$N!YAN}zM@=)Vx8?J$=1;?Y z?5K8S0c?0xXcHLE?mRju&3_aaj$>1`^a4%DQGW0wt1vw-Pv(~Pd|L!1bI9^a0v1*F zPR#z~hOW*29nZUu1MeiJg(TLDlI>2zLTxVPlxyqjhE*2k=H?Y+7PTE53qpN^nK5qT zptr+fqX3_Qb)A&TmZ#%? zJ)*LRb3k7yMj!fJ^K4f$XKCveT6;NX`p6u&{Jj;)wWed1`X>+_r?<_p{&5ONk z(bA%3+{VJs!}l|^!4u!jhKNl2cKr*bl>kY=bF*7g=R zFi0NG)#0qP*^=@7==Zlo*fPV)cbZK`ucmFpo3MknQl0ifzgt+HotjGJ_EID6(334? znT6vhcGY9wCJB6+&SbQnpAXyaa|Z!$@mZ-62}>mLbxe|7YNZz52cNZQH~BC#O(!mu z$CNQBB7YWK8MCQ{X)k9{S60Rjb{#EzN9C&&vcl(~(|hPNj3 zc0A>L-gvgg^{f|{6mRfp)`~stD|%RN77wI3PO6Mq?2xsn*wW(d^wvzN z3MSzUaY^izK&+*dq46X7N{bY67uwrZbDZPCZzH9*OuX5vT)oGJjcWvCxqc-048Y*8231O-}meS{N?vSA#`b7 zzjAh`_LBY%G`OM)+uUPgubCTGd- zTtJ6C+Iwt9jk1!T2-i;mHrX<3ZW*i7Y$~hUwS2TF|E@W1*(x%IsHn)WB&$0O|nY+wPtmu|lmkXj&>(CH5il92;@p)PS#A`yyX$H(3T z5PH6bWBA337Xn7rFEvx5zJanv>B+vTLeQYGv}1OR{q|s+Ih$eA?Lak^LOVNXFXgP+ zPRc{lSXQzfvSCy!ccIb!r}2%qWijVeh_N>Rw~8h|oWzV1l=su5j|$sj_EtKpJPuA@ zo319OGNfg*YFU~EoNb#QCikVpySSnE-CN69;cR3@>)x^00Cy43K*!Wh+!b~8reWvC z!fYZ}WKz-`E>-A%xBw4j({NHtjD2{)o0Pq29qtm-iqfLdRX&xRuB<%iCll!HEnmF< z<0ZKWW1Q~Wbm7^Q_@7y22bn^7{6@0B*uFD)Fb`jzpGeJNa&9l2*eRFS(~YnTERE&qXAO}cTwSjIPqF$J8B1TXzHlFkKE>G@V8}QlG>SX6 zwTmngZvw6mvoV3n*&RhYm*Kmn+5tfp!`+eU2DVCme#fdjZi!>d+j}2Rxa@5Oq7s9} zC9DvCYDB!&@-EhrLPt-;PX-E!wRXrVlg4M9@SVz~ru^9~SgY+Z=gf$QjFcLjF8r^B1Y>;Fl zd%~QOw4C>l^fIrmZ4}m2*wWV6()qJ4|25@z+gwyZ{?Zz&*K*eqsGxNr^LQt_CDb)M^cE5tw8 z_)B_XCwwOvZcq4aGhmL^DIdp7Lu4>F6XxiwHh`T3h z{6M*+GB78nadZ6vcg`3-+&{5SXS#E!XEQ0U!T47G^)`)6aaJ!G899f$tT{3UZuNp! zfcW^*j702_q3!9)J$28p8C~$mM5_Fdn_%T;Px!7mFTVjp1}!YOxN#>AmBwSUu#?1T z*Pxt{E$x()`Umb?(E8raR(qJuf#TJ;_6EI_+jvrQ@drX4OPd}3S4=ND z;q$|lnlEjSQAh!P#>qQvG07dBq_hFsn7YLWu8kmr$Q`L!^q`P&e0F(O<;R)#HKc=8 z&V_AGzdmi1absJ61*CaRWoQTe(|BrbkG~3{LlE*<}RnOL*mtK89~cClVT`sC?Q2 z%!WkeerY_2PAy1t9VAkJ*H+V-J?Fi~o19N73=-IfQ zrzWTUppL8?zB`?zA#9PDecx5Bq{78zlrdmP7O1W4RW`LQ>Z8oCxs<30XQl2k<6wHe z%j238h`z(-D0Q5p%0qw=cVR_^g{nt@b95cquBC$*3`7tw&&h&%NtgM9%$19x>goe* zE~%Y5bUvw&pJoWhwv19auJ`3(s516dTD3Q>59nzK%J!kc&Af3CHhog}CVS@2fJ}LW z0T9~>x69@YEfY2esx$hezE$6xT*J!Hw(H_{%0>wwV+p30CoQZ4J5I4V2ZhM0p++^; z)MgRj`@N8}@1;oz7q;47K5zB&mogF)bAAXEVIM8wW(@TTjHt!*OkM5lE(e=K)fu=m zG!VxEnI#v$R~PH;jBf#=lfz}(Ih%y6T^B%1(cyeUe43%syhZyq4dUU<=1gU?l`ID7 z83MCXspSSX%BH5u4oyo*X(`FNA9jlc%h2Cquo8y)Z0g4Gljb!fR_FH~-^cUInHF@Q ztiGFq{0C&E8hkXlja8@Hkg$m z_(n+ZpdOMR_W12_Q&N(1lXO|Lkp!y-U)jdd3%eb87nZI+bH?BgmUeZLTrgX>)B2yg zeQx|+X3v{!fO?5B9b^h3z2p2Zl|r-KaLP5RyX|-c7?leliqDyGdX>^V5;(;*!CXi7 zLb(YS`Jo>6em3i>$d$6-H^Kh0y9GV_TE-r#ABr)@!Nj7k<#xj+c(&4jOCROKzMj}{ z**-{2D*s;eOoNXyA3oRnd?n_ivx)CuYd2bVIP(FTE7fGu$;sIlw&s4Gx4tPXWbvNy z<`n``eU^J}c6JX^xV)|FAwl>ud{w#m9KSE@vSg{>fJ*L}t^Z%`oq0Tz-T&~X+?5vG zl|qy(B^1IK+i*un*2r!s3|U8pv5e(bp|bD$NJRE*!x&4*G8mIB%UH876NVW(&(&S` z_xC)n=lSD#J%2ra{QmLs;yUMC=bX6PpJ|( zr^xcY0c)Xlv*>Hm58heG=p3b2hrI`@9KuZZA!m)qUC%KiQ;5tXepk#+kmFer`&Uil zio%cU1Inl9W}+5QuLSKQIVWvRl01!>Kz7{;e&2^zOTH~`HR`#m(`Kq)MtyjOjT|c@ zc8n2ft)mTgwmn!laVZ9{T}I~UQ8>Px468jdo7hd*PjjMrk7?(&*+iQZT8D?XnVQLs zXs2yCJNt%kif$OUs-Mtl6zQLHC-%k-G@y51ceHu)!j}@VdDVj#;DxxE?Bh8l77Y^15Sl)9;0xRMD|;89d6_c7Z@!& zq3er_o>eI@ss^U~W6UJ+il5Pi3{pVY`%b&J5p<)7?r#whvDd()c0q)W{_G*cYMSl_ z3GYRGeK9fRLYaohI+s`7cbbu0LXkx@+YsNkg5UusWz=bA$$M?l%sTFG1NP;mZpf`3 z;N*?^QbWd{@9x&X1nR(-w95;c7pYt_z8{+#aNnPMe=ke8G<;E7OF-;i<`@FWUot2s zDc`ALccN3IxkARCSRG<@XR~|TaB&F zAGY;Aat9y!X)!ch5J0{zo*S{oRKPi?a@W+HhXYmnn!YOqe4&92R>t*LD`pE#>$((n zqtBEQyxB@pjm#__c~;+r$18s6N{}WQi?%pQHrBQjP*s00T5ETA%aLXcWaiUrtf!hX zkrX9PAq$OpJ*U_bZwCX*J=C zl;=hI19^CuA;flx{j?PMWQj2nTMJFU>e4q8pyp!eZ7yRptO5brC=8;Yiq!s9@a@?KQG-p#r9%ILPW-fKZ8use()e^H{SLo5WgRF zTJ!`cN#9%~v2QW%Z!vKlf7SFHTis_Oy;g-4fHQNT~1yZj;drWPT~c z>9wInOxyJ75I+wPXiC}bUT>rHCp2oFp2LcMnF(!w;Ff1|2?WYj7ddD9<6PHf9@`}psICy4y@LR3)`v9hBZY-l%#Jrnmr$ObA>ih zvDWEsw352n8qlQUO*{j7mmC;)Z1E`kHoHta2RL_YhRb0_H+OJ3J_WEfyYoUDHjns1 zAOlWk4@Bvdzb`2v4WTcA-o3GIhQ+7>T=q#R)3^nT}vPTq&5p%SXt40`0&ckpP=i<+4nxNTc;{p zi*iNiaZBDA7@u_Jn>typJARgLC#((z_z-}k)`_8zM?TvNJWVz_3HSOgC_c(9X}wNR zT=6BlMK(`;GQMqC0Oqn#1U-41ZS5(W?ox#oTMA}IbazzRykzuS0p6T1Rwo^^p&zLK zJaik>!~&YTHOA(uhm!GR$o`!KK7Z5H@((r zOFQGejo4C?)JDH#3+76+@5?@PtGZbjMCnJ|8uDZkaSq5I1r9bU3#ch!lPsPz3}o@9 zq1}dh7Wf!MN5yrtELZX*Vs*k98ybNW)6poLuJ=W>kI&52IL0uA*yEtC!a&f(pS>Cy zmPOjDrP4{x%2^_^=S289M>l^wl9Ajw$$}93!Lcv187wbE%UH)A2Q@wHUlh(~*1|u1 z1o0|V^0LLG{20>bvnYSG!K8(6Od*LGvoc#0M=sL=$JT4vjVJhz5~823fJc(0nz8Mi zlFDtQ@Y{RAK@-%2r(h2us3(iTGg}&BY0USw;7L@SX zq9(bua}s{oxJS)8=@C$sxnqAxpN{e6TYZddTuI}KLf}8PHTM)TrxFFte=#m$@Yj`W(h7JTT|3bwSlg ziJj(`JqOgl>J4?RwgkQIVgSARbN=A?|DI+3f66@nfBPH1>SE(EO*Xw7z|&=r1giXU z3CcaWfKP6RFYzuD5~JGSFC>2x}c*3#N~g`4sGrAzS9 zD0mo&2&8>KSN3l)mR3-}kcg6*{}%oFc4%n%bQJOZ5NMb#Q(&2BxpFqjQj<}a@x;gt z`5uv}H>hOh^X%v{t^VO*LE`OHnO4--$L*+C`AjESfL`aV6hwIctFoLd82^cIRJ8ab z``hoWNEB4(DHWG?Rbs#5EoDN8Di$}R>gMKVZl2aO{BE+d&iG$jz^~F_sA!v*M#5KY z=()fvQ;BhL{5r+%mv~AAh=Sa#JJs!MUc0KLa8ASeE-E-dw0tgA%7 z1dz%v@l8Z;M;+g8`8XwcqjcU~7QXjvyw=-JZdmS;_`~a|8f8oc8-Lw%1&RROjIHQ= zb5Q_*BG{F|vR9UumL8tdl}&Cg6>y$&l?FER3X^AQp9QuD0sS7Xte*4Q?yw|8;@3D5 zp0Nqm+SEJGO>l4+#?QnmnRh7ojlhdrG3OZDQ#wCaI925j^N~MM^Wq^<$G8~&nb`Md9s%#spO&A9;^unk<&J`u-;7H1TR{o^V4 zl&~;g0l#pLr0TDsC3Gf+MeoR8lCpu@#JfLBl(~#9gZB3H+!aPr@3A+tmRXhgJkZRfn`y^Nhyzr@!CUKJnG7S8Gme|2&3c zMYk?SG||n~%B&PJ06(q}@#{~?17@nTFjb-yA|^KJD&4ey?t9rW;g{28`42gNwF!tn z^PJ`i;akY_6DpTl!ycV2l6qqQYdl77aCLG}icwB1j58Rp84dn5a)1bY;d%`%8C+iX zn)|B0Z({%t+S}VRxc_@8X2W)OcjpZ8?Pt_%@Lhp!_^K+;?(}Ly!=YKXAj0CioYTOx z0_)`D$&(gdQ$atGB@_UR<2S>{eVPKd2HPj*Sw28~;xRQd3xT==COsgZT=8JDkH_hX z$<%A7MBI5a&wlmCp86`DBg4)!oempdW?o=v++JK<)OWf%0(bML0xmIty3^6#9z_ui>+`jO*lQRx_a1G(`XuAl_H* zI4HuAXI5$zv@bDJ%C=77h3o%xz+Ca3|M1+>k2VmkhPQm)atUB{0u&t4S%$T*$}B-M z>0$k-(_2Ha&P=fcNg4#Yy(Kj>;b1bAl*GRCj+DuYA?4S`>GCp)$VxY?=?=-NS>B@cpBmSF}LlRMbz;7fJD` zc3*;@z?Mh3@S~J1pRhAIL0;T8>}*rH`HGL`C{IH@m*z@NPPU`*Uk-%>a0Uv6;&A3! zqI&hh`_PWgPEi2pu;b94VO(w-ex|bBg>BKo_6e`~?p<{`-1(-N&~{WZ!^<=t8FlZq z)uQzU0DED-a)K=eQCWzUPkpPak4Pjqn`!!mzCtCx`Uw&&d$g^1w4q3y0YC|U&(x%u zqQ^w2y-K|zkB((2#HFSRTMGcbO@UcCUU&xo1bVejz#8M=z*`1ug3@QMvJedQ{%NNt zOKldb2@aYuyAyEh#TJ3t0Do>5ph*q&^=$yCJ#>xTd7uvuG&SQ+%6hH6tLWZpIPwEC zJxdgj}W$Fg*k0I)k< zKXdkq-=D6 zinzoePZsRvy_*e@+=?!vNxSQIS-`s0pQ+TSM*N+mnX_0wlG%Sb##_(M#?CGSfKFu! zN!wI5@8_H9pQobkN+}#}05emwT)ekr)R3RUL=iuY037dtMWB!KI%Fn8s2}J*YcwE7 z1DM+BKB$JMj(?vP#BbSb!nDD^&S}th6+IEFD7$nl(@w=uuB@Ro)0xei93IYMotPki z@v;IqBCX0l$$@qX8yuH-D>{dLX5)?OH$RA~PjZ+Y=xz9IW^o??K-433{Kv1rQLTR; zt5FS6{z&JGaZV^QtHM@PNJ1c;;hAO@mOC9rhpR^i4KIUgN2>vb7hq7m1_mQNGiu&2 z?i|9X12N%|jbqXulHY3sLbUaG;j4(<>{JN%;fkFid2sM2j}b5OSN(0O4sOHph-PU3 zJTN85ONP5VMmuzbN&IzX{t6JY*&t7?jnLB4>a7zo!)gY-W;QDka<`ebUQldU4@Z1l zyodh-;`Mgz@bD1eU>pLKrUFN?uir_}h{U4U7cXAO{ivSqQR5z~_uKp45(?apa{P~b z0=q3R|6PGYv055nc&iDdv$M0#V)X=@`Pz_mBvV8thmeqv68hVXooY?LY)Pk}e3VArdZ+I&D@U;wpd)QyNp!W7lu0O_!8di_q;~1*|XZG z2>M&jgzo~>MoUXe#UC;A0J0GP1IG(mutW98aAqLtd|0nsr|%6<&YS+bXW2x;CZT<- zlncecSn*Tb_P5u?R-Hnk1BX>YFFzk0za1s`NAjNuDKO&s)(;ANdvb-(5?z1p3-)Xr z0T?q1uP|3MMH~{L9Jf9T++e|K?fI%pD(^#nE3etGH0|6JJ{tqRO4uB5m;Jk%G@p8r z^P%|Z+Xb>MJT%;ke^osWcJ?G;3U9%cJfJ!&QCZm$+j8O?rW+>eCIgHL#9rOs8cdG} zKy(sXAv{!d#djqe!7EJx?8&H0)fwwDpR$<$QafU6jR`Sp2sRS}qf4U9MKqNbRTL?f zQ5rR%p{*S;*&M0*TO|%do!(F~Hxw-+!$k%@i8oxic<~}u`H!D!Q;KFc%kMpqpdpn? zoEjA=^8wBPq|*ZGaDPYd?~o~V7(Cvl#fBD_@Xv+|D7$bJv2-Yw7}ek34={&(>MVX~ zX8+4WSulVo+FS(tM`6QNWko!mP*+#)mDiV{IWrO*+B@2X?4`{mfN2444r~0PmN$0* zfJ{>^>|Y%Z7m)932ZZ<8ynGaN%p@ELyfV4*0s2cbQXl^#wB^E03NM+L%F~k#n_GIy zA-;P%RiOOvP4uOxfh4ne#WchJ4Ce(I2SX0wcRKj4fuYcd2#Zfo?md1S*F42eD=Y2O zI{dhAZ*t6pNsAeq&iEfKc}M2kY)NkFtKy2zNrzj|u0Ub2Kc0rEaVBmg5i~$sTU(ro zGf{Vs6-CIVT>noY>ki~ia?8+VK-V0(3YE#VCQZ7_>ZC~n1fJ{2lMcJMu^)hmz`IW} zBbJ-(ch4z+BFn>hN@7?3Y$dM=!fG(R;TzZi?nJ`;s^H0f5z& z*yu@H{xuJ|$@Xma=kvBd)B*u$Z;j9yS-kx8{zYtD;Bf$6m$t1P6d{lE4TKk}V#92Z zVT3nteyfK$l`tF0%Bm`(k(O%P8Z1M1^hNZ^={;NDXEwU!g;+zPDt%B@fqjSPmmt6|0>xU&s4ltm~N!X z9~`6Fv+oOTb)Jm#4LccdCTGI#!mpa|<2BP&0rnawUKI2f-<~#@ry0wwXi!B>Ad8fzUb0Qk&8z8-O?IqOca&aY?*F)NfWt~8xJveW+!#JM>&2x- z!SrjUPJa%?cR7{_R$!~-V#WIk&y;ucxj#W0>~~bE*!zA^YBH%h*hvX1fUph+LnlA_ zJEOh(qKNdv@nSk171A7AGleBTlnALm!UR)vob{Ix<{5IkY+r^S4`z!aB^V-QIXgP9 zniRM@HolI49t~0}W!v%sXl(_pFj0lRxGMom%t+IzT`R`{$5U6d12DT}$BtB|pdb$A z%O;&wvYYZuc#L~1y&_VoEvDsVah!nN} z@~6WD$HHzp%KnTU5j*j%sBU)G+)Xq)%>#|CJA$zjj{Ho_Y&iGDy;A5-69d(#S<@iG zm2WEQM}5yWr1OKf6s6ElOFP^N#wQLlt2To@f~d}S$$AnJAzRwEdmUi)NJX0F z{yaiPf`uufhC-qcaW@FhCyB)`z%p8==-FF5|lUR&50 zkCi6gp5|fFQXVOMqV*hxQ1*bQu9JAQ7Cr`)3zly;uZN3L^2^u?*A?@LuP63+kItNP zK85l1O+2UpGX*>|MlbIc&r1!mUY5fq+Ksy|(@q4I{&BQ6A8U|z!esrfa(>@Uq6CUvfMi7>gdbGcM)hebnh-0hF(UVg${I^n;iru6)>z;=N9=67B5LnYdC3_ z+hGk1Xr{4U-E*k;6bF}*vyw~$;}x!t6*i{luhPwt`rdW8Phxp>H3i#PvyD`Tw4Ig{ ztAjof)Z7`g$8?b}k%HluOAXPVv==&}NQi#3vp`N$m*!C)oBCT}2Kj4CoW;VGr^XE< zvxxTEgv9`)EbEtb`~ zyu{Q@XoAgd44VO6-Cir=lqVgnt-Hmiu=bJ1S4!d8t)0Q}A7IFYaaxZ1pNs0Nw*u_V z^nZ8^!s$}!^};tjc4(YS{$<|flnArIJw>C##ur`tvq9?01$7okB%47?#m77cBg~wg z|KoYVdY&o=#Og@u;2Z?JzhF0TqH5gQ3Er;iQrltfp=IyTTujm0Y5B0Mp%Ee<*tWQt zv}mWSEd}BZ7~h|&yqmYgCwU4429CUwqc=tB>P zjVz1&j%J^a6pKLK;%9vwsO4;zIkNfc7V8J^`~m~tA03>KVrSX4??|2T0M`SU=Yz!+ zr~ETrD>Tmmu8%S5YYYuzZco~gNfM!hmWH)YtxE%%CMyLXkRsg8Pn4lgMWsB8&5&_p z_k4RBYbq|+q7Z8l05jSN)!M1@BP#~DP7*Q*HZNU>Y(1p{eBz{iY~ETceulwg32Bv( z(&$7q_vnPpQBI}S$fG!v*^yGs4mY=Q8uP<5L<{#Q(=PRkuwe z1Hq%~xy{K_+`{u3Ty7B^Y#AjaQ*?S|U2$e!!9EL#;@mmn+8G{15ruB`V?73o5>M0n z_mwsRsrgB$@`Gj+ek{joiMe*ucBzBrn0*T}C)8fMIJFq5-oar=VHy_Sl%#kstxp&h z^nd4E#T5;WVAVQ8nLN!i91|>dRG4z#q1=E{m>eDpE_SLxr?s1qO&E9^6gI9b?M{AdYQxlD#TieslkO`lS-79bETWJuNPxGv%EOCQqaY*}|{Qx4P zM6g}`;X^$pyoCeFJbt8_k5VJ|4SB0$b79pPQJ81%;#wgv(C>8x?<1juIPkt=fu^p{ zr&1Bs_pTNSOe?kZ`9C2eoz-(E^zy2%F1drexdzA(MSYCV*d52JBh;z}3H*gKl&}`k zY%aE+>=a~FBD1!wABj#==lmgSy-Q9>uy-lYKlC;8=!6y=-vR(k-d(w;1c`L;AgerW zRA1)RgJPQuT2wZ3`w0HS;Z?Alearb8p^ZDsp`md4 z`|aX>XHtX7DlT)gmBiWSh2#8DePc0szN3Ji(Do&Dnqq=;Ho%OU4-`_7E3kpOGkwh| z<%cz1M;3vO#dy<0dkb$57h4C-`1L2g4pSkk)uoc%}lukuF0+2!}jLUuN_S>I~FS-qc*zi+@uVCj2(tBP)s1BxO|tk zX~jGXy#GP|@m@rAdj!<3IS__+NEunqckt0Sg82Zmlh@5XgV>qpB+|%IM^DUrUHtDE+Qty`<<)mua8e3VnQBy(=tds?Wc2r zgZ_B<7rukKaHj6Gt=yWT9Zr{aO>uhh*3oNH!%8q&%4TFIC+$NHgPBF)lFnQ1O73y; z#IiaJITpONl|ga%=yQk*&OZM#4TGh!WsnUm^d}0-zKwd3uNH_C9SmIN97*y!(^{!T z96LdYFuyUMqrP(!KATk*4gLboVQbTLt^qW&lnL^T+e)&XFCR1li-K=V%-a{-E-^}$ zIYDo9=8eqCtjWo&D<~M7(!666(wO}HxorG^?8CN2r&}ddo8fetGgfmzcX0Nuz$I3; zyX&$qZ5Yayx~A#cin^ZfMw7F~hKp>2Hf_P*z8laR87|~^raDno#5e^w9Ch=`a0)Aq z=NGz`d}7~*V`Jy?^@0jc&hKZ#I0RjmvaB@HQ`<(Bbf9KEn_htKWP-RrB`l>cl$6YP|IXq_nYS1>yOt}zeQ)> zz2$ixOAl$c}3*#dri|gYH3nD0{&l;~^zYYipc)<&<1l<2^*RwD=$#YD4 zhF?@T=0mxubEN9h-6vrMJ>+tB?=mql-MY1O_y!*q8R;OLl26AUJ~?T0IHSdL%S!P* zAbCE9mpJ~-$M6eQ>me7HJ!c^;e>sf#ajox{m>5XA{#{4E=J~fZ%6@u)#}dh}=_8fm zD-8drBKhRxX5OpdLI@Jg0^sQAXuU4|#{k)bM6QyeA|8HbHO0~pz~|>qk{^cTee03q z?|oZav#bh#2_)D-M^rska|2A7ZUbo3cwq)NXbbx!0*;~-bR0&fs%+!tKqbQbxA|H5 z?=iUfwR3wi$VA-h+ zEa1V-;x~&$%(@*{0LCo0Z1|%Oyd}W~A$B ze-DT0JEN{%yC<@2^j((R7XZH&MM;l-9pNX+%F4`IgAYGkxYD|t3J*Tn>47T14Q}yj zXCHLm(rMZGb?@efrJe-i5Ufgw{u^;Yg)JRWO8Sx_?W?>2Eo*;pW(KgTo_PJQc%!;x|CDsWRDJ? zw&~2V;A&ZSlvXl7CL{Mi(2!2_aOMgo`M2KbF^uvz2P+zKcF&j_k_8wxG&OCq?TMe6 zndyV@V*F-ZE#POn>1`)ih|cKP7_*4EjPJUEWZ+(sQ%JqXf@cqVaxlF0s1!T5oNgte zQ=lF+?~4Y9Z)agnR&x9o5cUIiCL+JqmP=?XW_Fgeg}!|@R^xnK7}F$-aS@K!9_;VW z$ec8cju8_L#2+e?{_a~fH`q=0gAtBIs1;w5VZfQzM$M$6x5nT2=5%-R;Pmvi$-62! zp@_8PU0-J>;=E+@i3A~R!EGAp*}(OzqWSm~p@D1adOTu1?barbuHNa}YzWpUbu)@7 zlyY`*3!O308LI7jpoR$XpC3mwWT4(cu@>5HGpgo8DBmDJ_zNdd{US}o>Cr||tl`6B zu%n$^@WD3b?38@}=RzcB=(~nlQ3N)b1I{Vu^KS5~n2Cvr)2q)uUskBP8%CIKCX0q0 zYG`W@hmhx`ZGXkKw8(=MK%lddVrbxY#}#RpsqI5lSm@HPxQ00|4NgMp>mR#UywgD^ z_?4NN&60+4;l%g53)%0^whJX^^W4Eg8|@V7iVYVpeEFub|0GFW-HFD%X|27q$B0>T z2$hiGr0~P7Ml$!?xBI+Jq^H)_1XcCNR#0an?pZ05>qhXL=Hms2Aq{u4bv_ZHxxKCN z)C7^bVDi?yA94ATK8T`KvPAJX9Y`A05EZs)+WR6{xf-bZ zT%}ac>Uah{{)Ud9#1Zi4+}czYD6Ca();%LBX%T!%nstX;wJnIxGK3v;DcYdMoQ*c# zc}Y}&yZ=5m+iLZ+jg zhATFQ<~@p%#2eaOKtIar)wnZjyo#kdgF?Gk^M)PfllKqyLq->meOqQ`)E4lZ>HG#3zrIFz~u9kL4Qu5vMS zQ0OQJ=Onn8MoG^Id@n1LOz7a^jCxP?K{fy);@J`+lJ)vI@Ca!R#wmrKW zjNhDX5S5Z5@q!N3UcC5J+o^DRuq0?)*|ImPVV~@SSL4NOHefh7IWbgMOe3IGg*Nw& zLy@b^u1mYm+E)oU77D@V8&?)BZoWYr(nqF)Z&lx!um3UU^~U>oIO0#eA!4+T$FS4Y zQP3D6XfaYEYf+s3dZ1y`4{Huxj#88P;YO+K?&c*Zcy!s)Y|;;`9J^atZLbm;hP;hw z#;&`q;S1|HrVhG-q)pN*ESgo%d#=ym#yCtuk0-Lcn1#)nzWv!f*$Rnuc;T;b@hk6P zL63+`22TE{!Kxg*-zy8xIp|U%pDyAV#L`13#{(j2uKj zAJ2cy%gZysUCV}Y=4d;hW2|#b&CDoBW6f|`P}}_>0a^0aht;{c6E<%fn{Y3f0ZWKm zxkReFWle>hV6sRv<<22cuop4!B=sD&ws7>~#np{z*@5YKA ze~|MSJGy5i`dEdKmJ&B?(RB2A-XA_&^(Uk7?ZB4rhTr$_RA=KU;rNrn0k*7GKk|pd zG7fV&Lz8;<878F1{%13e_0}Kp;tWO_i;-F*-WNi6i?&>|=8q50^DTy(pe;N|k!ib_ zHmkX>!PDW&=IwxXj{=ZRX`miT-@y#1Ql8}9`RQ6xQqnysm+cci;0EaI5H(s@JPciU zI0S{N@jy!88sxcR2RAPvbF;+yQ2)~}Oe;gq9AHO#Bg7NYlr&`xvh3LPG~vhYvngq* zQwE`Lj6Ew_uzU^U(27lj=<|f(el0%C33jWyunp?$<^^-$@2hWZc{8DO7%f(pL%5lf z7B<^(-7OH?*NJe(2~(C~X?`MIE6oJUTRAm;YhS`$ouR@)uXV}Wne2!JVFl?)o14UR z5y`Clil13%7m=#*dBw0(QfirzNWVp+kK-jVBwyz*-p24u|6#)*$4h0CiuDi4 zWAx=;OQg%qSEu%{ZYmws_9m0__s}loSU1}~ z`MEY!$fWv3_>acM*L0JKB5xWuDx3WX3n8`WNpl!@#ca*c*F zc;#}9L3erDg)4rWr@t9PJ~o@)g`@?Mc6~Em*(ANy2$~F6?%%Oi8yhbqs!x_{2)$mF z5qHt>al&Q5NHYu=;kha>rKt1kB?iT|z}#jN--UjwVY7Ot<0iuCcF|$8ae0W#ggo)j zMsQzWA0?O(@cEgkX=b-hP*9MKjSX22)}1@yl&bcRf1H|{S{_IzuW~`K5XHLH)ee}} zT_YaPTv+vr@c~1 zbTzB#d*VAXdT;ku<`+zbR`Z-VHoE1sm#3$CLZi5;M_AC`ZrLw}sV~<{!$RM|KWpA} zcHFL_PF)b2SD7mnBi)4BJ$Z(^n^D)y(Mq0f3QsLdXUZO9b!@o3rT8XJTVs#Ocz0O9 zxkKK2zS4nn!64h{$Gf%B2n6!;mDIN=6^`jUNSjS1VtC!11D@q6*g1o9->ytg9A}ss z8P$gzlc;etzkZQV+t+8TpaeGA@#-+FFAgc0NHM z@EC|W9Hx_=`}SBUN&)b32}JrVB4LYCqQ3$S_#6HGJD=k$tz{jvC1Viaa^%$U_)}X3 zuBfRCk7a~~g*%!Bp|xi$o0iK;a*$hq$1@R;SH!M{AFX^;!k>FTNp0`_OKOh#(Fdgs zo>e3mDxOeJ{m%RZ>ARhhbZyy{F5s4n4FkZ3*SGq8!^{4AtUpSNKg+1o0Pah4cX!L= z4AU>l1qsi^$BH-iwX99fPV*jlLFflSNdLd?jbP{Ke zHVZ!E`s6P_`$m6S;p=><#RG*;{(2AMTK|#gKVtpoFc%U;M@L63Gtf2KqFkf|0RV}o zT)cp%T-_Z1d-&fm_+LHv-_PKGO~LzxGDZ(?7`2y__EU6Lq>dchFf4-c#S;8`7YP*!D%L^uzJ&U%F^rTc#V& z-(fG(+7;Wwub+>$zVz)@)s?%w&d+*z9T(*dtV#hs4@rOvB`&3Mfu%1mrHtR71Xq82BZ67H*rM`G zWiC=F$z6Npv=NHq4+_J995-Ig&_K?Pywv(@krT?TMg5PO2N-WLR$Sgd{z_QixJ;^puw(p!@()@mgca|%j+&&qOL`P5-bvKI2#|JaoW=@VSP z9!a97x4GdIg?vR)Q|j4)D9yVfO(robTC}Q8F1rU`JwxA4xw7byp=TW?)TvFMDLB8U zgpX*<;Z~~HM(#WVl*X=B6b@NFUVH))r;eJ;s^Cxpw3IqYyW(9;EMQ^RZVsOp)CW3q z>%5J|>(P>9gG{&UTWxfn5Qc0CrOysyeqt^WY#-VdC%{%b>I9yGx-Fvw6&5T(pXQE& z_=U4Sj^rMl4!aKDiAHWe4x9b+P1JTfjNT#CRQv07$a>L##A!uG{#G|Rcp#kJG5Xi5 z`{gf9ogIItK1>3;2i*zu884d_cO2(Y#$x|9@b0_f`YXbWLAivXj>)Nl%mZslvLnqM zGqV`^;VsSkR@$GPgOG3y~= zYr5i=Y|Wjwj z`TC~sn+QWQpuolqsM41qbWcsGxiEKr5-k5?mqv+w;p%5)d_;WOWV~SY!&3t+cOuES z>CrD6&C;`zT91`%&K+DA8x4cu#`Mu7K6c=w8|p=#*xU@mAG+U;yBhN~b+K}@&UZC5 zehZ13jS&`!Y+#a=RCLijeOiAfRzSk*GyS!CL6N;swk4kua7Dht-LW+S4t#&~zS-RN5@tRy?z3eleBv!VFOBn?1eu>*X&jkIufhgR92U z6?4a5;376B!CY8zIN+-ocX%1=`Ml%%s2W}@yBpuWI8l3GA&fP> z{=ouOy%CJ{W8eid`dK}hr63Zq2Co$Rte^~6k7Pc)G~^T^pXX1;jab;DnC3J zD}SO^)==VvQ4v`?(Wm^`7}P0Iw^ZcLYy6UHb{@Y@+zm5nnMTpV*+XzyW9~*FhsRYH zLW{K7vXVHm@;)o8#8mj6%~l+f|!KmGqS8gk$)qb;jYPFT*)D|*kWk`S#rvyLCT$k~g0yZoQ);v5tmL!O`=*~={ zre=?G$#1e0`Jb=H#OLM=CvSh`6g+eYXloPHreiMYE9`zGIi+)yxwur6uacRd6%Rt8 zve-(`zKvM#iz{_(*LujY)emK9xBXD7BRPL}Mpp?OI$%!q4i0Lkero(p6CujYetqXV zQ5%KEq`#zrxpQycC~KJFZ07%^Q}($wqLl z_WC$_Y{6exTztM{y5A)G*>9ZhMo2tz6c@1xlMPF|{sag5!kRW85I9yt8En~_*>KhY z9b=D<^#5C%^CqX^i~r87rtBW38b#_ARt5y(7Y!^aGp3lwib3&5M*o$?*IWsWqY!l`%I5#~YX!#qlHlg&J!Mm}SVRsUPU1?6AWOk;~qz6JwWDq^U1+#Fuj>i;GNW z+t(8;bM(R(SUY`2b+LmcI>Jlx9b6q7{If%Kas<}il^7IxT8lwCZ#tBGzt%ssq9brH zmZT8+HqoV7_Bap@Il;WqSx+*yJ7}Khx-l)~zAm!{uVeu));5Y_*uab9+dGc^6Dud* zx=n%`mkTlewNt^AFBBBKJ7i6eQNOxHI~5D*;cUZhdw(9d73THmXT%URaK&S@jy&U( z)q)Cn=G2Iz%e|m3Co$9;DxR-Rb6XxEwxKiVP%Tb>azCoS|~1U=r1kI_m$XvzG6%x$e`NwB@GvwN7aSgSOD z3_OOD4_Zb>D`2^$#mua-N|T0T4@2tKRe5`~E?Kf31?P0JFjiJ_>`c)l?3@N>9p5C7 zstG>huKETL7&QTa1^-A*#6fzpjT8}4_9 zNDy8_khy~JUlps^9Y}C%aGa$A<##Vjwbw9%&KKgsFnN!5*35b0k8ObdqM6{fMP^@BqPfcbLGD@RCU@QRU{_eP)f(lR+_cZ#1nM70Se$I!@1)Y3@Wx%r& znc1!J*R=}T>dNv9mG@_#LJ}+$6h?Yk`U`T1%O4k-H?D(xgfl(0fw);tFpT(@*mWz? zU32Awca9(EWuc0Bldw?k?2oiXG3%u>ZZ9M2*=8Dp7%~u03k%?quWa)d=9Cg+95H@t zezdaVA2RgHx3sCUP)t(F1ey;0URw{{CAtxLFYmAoE7ql^*P|zErA?NtdP?5Hr60JJ z9jIQ4e}o~sM}^v%VYtHI7DZfH&t!`g>9*u+J~{6X%SMlUr4RGcp4SfC52l;(iDN7c z42kYN+?UNjbFYL)@t0W;C|PLCx(vcNr|mflswLX^C|Y61Xg_zOcrcas)B$3;Bv16{ zu`~ml3=D*MaLf_pj0#_Nw#0DdpFkJ?>gvw6F1?x!*O9<;XgS?gM>8G~Wt3#GJT6+wh65v1fvY-a>DV!vD(1hMjEV%MJ!{|Q3h#z9BLvDd^!D7BpD8oAy6Xa@ z^8~p6UfJ`<50BWi%eN0YI+spGEE13Y-W~JEjve2P#7tXGeUHV4S>9_u7w2!Ac9DJIG+VD<$Y*pp(Ms4I;da>n+ zTMkM}X>CE!?cg^GILMxMXPET4N&jrDhbfT}rxlwSqBUN4xai2Txb!QcxWEtQbVd{$ z0^J+EL{Mn4SiqE8GOM*lHq1=?{px%c?G04otYSVY$Q`}C=n)trwKfWwnl)ZT*8A7( zn8|RDYVwDmZo6Rx9|tGT?cNH~n|0m|qH4#`C4mF&3aesMiiPmwl@0%2A@&H?m-jH+ zCKHe@8LJDU>bnXDd6Y=$TWN6V5s-=g~x}W4qLUNAIu>6fu5I$hF&}X1EdU*bq1GB_t=&{2Mf)ule>{wcU)2g>#1X(Q^` z&1U#rqq+iih#h}d5O$9`+Vu&7Cz^3zJwjjWRj(7Z6!~<|c^%v$hwQg#H+8cq))60z z2r8U>5Iwxtu9-|Y#Xy2{W2eSCr|yz1ZntR5EiV}@aD{zT;H&%X4lEXx?bBCKO21?l zWcv|Tvbc0)`8l4atmYu`dZHC!XL{hEP*9OQ)cft*Zhtu0pQ+8~6D0TnwYi@Y7Z71S zdgfEykk`^Lp#^AA+ApMm+en4K>D*f5{&`_ltGX6z8;D+qzTgT6?QSrxbqRU4|8e8BVUU%v4ArHgRh1YQ*JH#fb=~R@75((_;H8s_9lv7Wp`y&o z-H5By_HQM3J{da_@f5LmoGGfgD2fl+nruRa~FgvsY`%164O?CIArz* zXNn>Tz@0ca^n~=Ko_snI0{wIxhR3mIhLRF@ogoD0ee5ey1l%l2mH6wHRUEr?Q3Ccj ztc~mN`9+`%(p*xw6FqcC9{Y#fFJvkcXRqGb-M52dcT}9jWp_OHe2jEl*P{FCHPp2AN$tel}82iyFPTPhJF2~%S+@1+9c^-k~gg2v6A}Q<{XxX){QV+JUFu=o0Gt*Slbjpm`ff54{PZH*i{Xh_ziA z_;*xoF+nIA;p;Yz6ODdEe12ECET=3OWBlNuxrI^>^nr!MR%dOih{jjxsieWrd`IgD z+q5txxU#L}B}Mn2 z@r-DP3oF|V9)8is>uubpcCG>a2cg59#E6aLNqMF_(nZHHw(b<8?i8U3ca?SEspWcM zsWd+~ejOjx+%=d#M` ziaQj$oazs7!rZ5Ks^MkhT*Fy+LpFoS*Mv-_k;Upkj=ME&K+1-L3yP9}wql6UiE3RJ zw*aXd>df#K7)i_ioM)1jl|1RJ_3&1gNyoIe{h)$uQ&K|bzTW~I6?ZL+l0vJ>S}T#c z+I9Xfvi(E5FOu6Hx8*kl_bPH^^1CtL3S*Ye;0SqS{!|NfuLqW>R8<6&x*ua6HO;Jx*yh=`Jr!KYO?!9`U?z~ifZ z^6+DB7I6P%EA(K0z-J&SP$eg_1~Rbx zB{nuiv}%K9{}mM*{tlFmr=qWds93KOedC9IXH5AKnC+iXbi1>&v$qUevfpmV&+hIQ zDk=#i?@xgHpY|)i^g_-3mPTLU`asih8k;|R7o|0;(9MCoy1bYqHlglcztsOE`2ilY z_ALO5AowYJ_`>MuLj)g4{i0ul6@i3eyuCNIq1y+^hO|SJE|lPY`aRqR+`rg6Jf|O* zgaw8i)s7z)tne)M%Ej5QVzveKRGi);U>JDR5O;3}OEamK zJTHzK`@!EQnxLkpX2$_oeM%x#qX;u+{uUWUFZZhP-@2jbLC77;!xQs30N^Mwq!oW? zf99Tzr3t=D?dQRKmR?s}LV_^>KXCrXQN?BQ5+e z0V%08XDX(CUX6NXPqlTCwyhmak~bE*didr#;Jvq+oT|P9dY{%Q;1Ul>ZkT(sp0@rP zV7p&Duy||@pTzF;`l3xd=kNd40uEBr;)l%y1GdV(0RY=CpLC9i$P5Z+3*4_lb5$xJ z1F6gZobpVHeKCvh?MnwKcTh|RVq#*{vzSNxfwVJE?Ccevi>j)szJFI0Xmk36z>|z5gUz?h80m4G7vD2dA^pqI z3aErGeH_i8ukXB>%(CtGXGsiatN971iwt2hp(l;&@;7>ihLBVY%F1v~+2?|~s=cS+ zZskK0HAV|KB2#c-+6odnT|Ve0N_qniIvWZxtlSwBX%+%bIMldqti5#hF4H(~by7)@g zZ=Vn zI+v3gOUVoq8GnaNGWx_S$I`kYo&S1W_j2J@4vHZ<;EpN>lpom-HwpaBLH;CgYCt`q z_3Wd1+xi2UYt$>sGd-F6J5); z)pYTVgp-TQo{u=YoDZL@tjVPhcBOkBCDv?-a&L-j1FGe0UJdzdpy6ku>8;~j#3og; zrMdasuef^??LT>WGqMFlB>Q~(WqD*JTPaf=(wNfPpWGSc%aIHm^5&Cw6~zRR$de)V zuzBy*DMxn&pB$|e`iX_$g6a|UArE6wW8*d?H8LiFtwAooM<*)5Q;GxJR=fV%FrPKZ za(DWvuRq_`!eM%jd#hELy4S8K(7#RJv$`snTn3D}cf8Ja@%^Ij-|pyp?kyv8YS7*U zfvfV!z%D=5;_EK1IviIjVZK{h^mI)luIcC}11XE}nOy(LoL8 z+|@PI(+kB0Xd$@-Opa*vAwA#6Bqien_$3#d*G&?n<(5WywKNm_q@X?r83DSa zMvoE`sI-*2a-nwfyXQVZL37c@mGU9URHg+pnZiori7BWI*vretMz68rv|x;ksg{Yu;Ex+rVQZBDm(92KR8LatK_TDakU^}j9J0ItOdX3NIayqHJpO-@gCUxcPG zQw>4h`O+YJ$AC`Jc5>If?zQ6G3~w*5dUwoeoO^KB2Cbo?;n_hC2+OBLXRhh>)~&kv z!N{aJL2-6;+}u*%QFeOjJAU&CVjkQ-!?v``a#{&dQFk_91K)z=mRwFM3IvqpUn5?Y92ushC4RV}4Qfg5rDu;LTp+QamuQjqiB97#3<87y1#t z2_C|BT&lacVCIh8Z^lrDH^yh(+Y}x=SRAlAt!BNsCRL{Bhkcz-oHL!za9(aPEH@ug zQSaA|8_G=EYPAvN;a3eG&UCnPEx@MAPXF=SGCDce`Y zsYTML9+lv;GD1H#Fo(aifUS0XD6^TB83ok*3d?A zC8Kp?OK=U1aMw@}{c_(*7}X)7N#4a43l6I2u%vHPN?uC4uLZ3EJ* zZNwo|5(7;Ea+lNuGBc*ji4B9zo!k|0F8-6qu!ATCO5WP7rJe#DRxAFy>=u=9=I?dX zJ!m)a+R$Hn3!yDNIX+!bRwXMR(vN-Q6b|s=DG?j&l3{2~s@Z7j+=A1Jkh+_EH#or9 zB*QWzg|idl^z<}8KmVPWWh$+QN(r<4@uq|Gbobx(fqs#7OCa?u;c!cbwIE$z&%oedQ!W6j=DS3a%We^nmH1}MJ z1UIg8Z;zHND@Iz$%zQi6-O|#xqnD8V^(Z@si6_{ z=ff4d(m|=;SdF13p*rHj59;2L zcBXWBF+q?+vTQWXEkGh!} zyVN(Re2P@*?pB^%XT7H*JMmga8=-B?+*U)#zvVO+VLUv%1x~W;{@QnFEK5du9>k$p zi&0HNg466FA`|(;lbb$9Yp+~fT;xYrX?6Ty6}MC6WjDq=>G2bh^r3dn?ymJPuT=K> z4E)wD?Sz)FA7Y&${2%986g&ef!L-HR=kGpi)s~{Yojx$3@WNo5PLkBbkUJ^)OY4tCgG2RJ`yg$<;j!1G+TV)+WmP+7Bgz*EKCd4ExwM$k1{|Lt(58!3wF$!lS1H?NC{V9bs3iTujXGqWa`WnRy91#`4IKBXpJqZc%>t*Hj75vgq+PoY7z zJhtvkaf5K>{k83t2XHx4U5>!lN0N_#pT!S5sX<2}mlmBm`It`4W?;0TzMhAd_li3r zY!?D6w`evxI^ogEEC|otx0~nJ_j=+h1h^`vcEj;T%$G0J9A$5=z30}4XLN0VZYVBI z35um=BUGLi%Tdl-iVT(MwYN6L1bCBDGrLJm10Hrkz`J+-(ynK=o45Zld7%Sz_(Axh zy>jm9yTr?#m3M}%n`5oNSE2Kt87aOWTVp#F_XYs+V;)6dBOE-c7xAMd9dor2(Y z!M6c`g(;UUPkDKVYFeUi(1*3zjgelp2nN6(ji8|BS`X6qbvhlI;_H82jU$5&v|)-B z0Dw%o!F~&Aoi2eI4U>B1QOnHqkzH%pRyn|Xo&SXJjWQuJU83#M9P6PrF~QK4m6Zqa zqt^geS(2|(UAID|Cg*S|^!l%iYzsqluPXUHZ1);9sR&|cXHUKg^;w_ z>lY6!C(FUk16H-FpWe*%Cl;|A z9^{fA0iLSvW_SH{oW*klb%mt{rhgkJH9>=JRtnNALxPe~-t|QJY<^kDuakCkLNTd z6WV*A#U(ZVyCpRtp{3q`Tp4Ghw=cX;Tx7AG5f}0%*Ag?qT-9d`)RZY~7wq;^1QWx1 zQn0IXaIXD3Pjkq0VbEnedLZzfn(;WxBcef~jn2M$Yvq;#>YFe$L0DA%U|KFiv*`Qx z@2jitb{POK8K)r{wr`!C%N!*7IgInk^est4@=PsL`ZylgI#Z}c!fu{?5>)4~pE|R` znDiHK4iL8iZwh>UoCq&Tv(;oH2b49wzkW-XSrIRtyf515VBn|IS~+k}9`2*y7xqxU zkh(s9^BmwZvmc40Y<{d#W!%@I?9Hb^{f>BriH%!<>%r8MWy81;Lw!ZPL6Ybc(H?k~5p%We>m*q_bBHz^_Ol{-@TaFFi&nW&ZOfrC@ z<`g!^r+I9t@bTEeFjqL)+rFTkcz0_=ujMmL?q1NB!3zLeA|x1TW$;V<$;VF-Il#{* z#`?u_(m3h|{`ecmvB!iUE|w*LgG*Mb2p6{>A6?S;BXx^HQn6dnzUN# zufG1JvNG7{D&w6yE8vI3fXS@*czV>WLnhQ9HDxWAe)b%3_tvtFW{SqsPoemntw)i{ zx;fY>ho@~KZBsL9p`Mz&5l_FF$439t1NZyqvLx)+Rcs$bZ_w-Ok}N&zby>G$=@g}6 zYIOyy6w#6Q@12LN6-H$#QR5SQ>F=)&O;L?r^|^stvfo*bzKrISOLz@(>m(;9=jG=! zT@L?Obou3nbYIV1YuPH8;f*UC{ZrzA_+*M#i>WW?t8VJO%+_M$B5RZ!XJW)`;%HZB zuuAif%rRngI*8t5jRUo!bt91nOhMYkHB`n-y@=znQ3tPJ9Ge6qN0~DuWJ!A&^=Dobojhm`Yw$NO#tYuiA|(0H^YaK z;mk_<$v^YLY`dGHLH#{{-nktnK6+!GTFCJ%Q$oZZO+hZ7JG?r2qTq*rE$OA5BggGr z5e$*|6Yxsp*9*j5NU`}NckJsIaW~dprFbRvB#Nf%!q&ZlCH~I-NW7G2Xmsag?ti8) zMhq;Xd$(iombJBYHDLGd?(&l+TOFrmuatjojTHJU6f2ZG%JJ{0^`-xwnUSl`Kr7!- zy2DdDV1K&@DiN;1`{CIn6NAZJlSd|x3&sAGJ2^p=g^L32e{5%Mm-M{R{G~I#Z%5VE zb}4GF?|9L%DDe{Hr@rZbr~MC=QNiU!AH!MwxLURPO|Gd(z9h*`KRPDn-fFu~SG)KE z!eNkxPWRt;OBtX>F0m#e|K9k7B;Uo(;-|@O@`FV3qZ8HspGN!b3u0TTm8I1mrWKbt zpPAFYDfBeIW9aztzdFw-ab%+tuPE>T%HZ;fW8Lp{K-&Mg)Ng|lPyE34k*^C9k)pgm zN07n(+xxBMC)F=a@M_MDY!6v}TxpaxQ8h8pA%^tk|BtfU+r?E+LDorpO_F&+r97Kb zRCW8$qp{^9w}YskUdLZMUHcESGcyLN|MMJwI<@*WVklDGo@*eI|1E94u&~g6 zoGwv-#-(AF?g^VgB6VhO$9k=;uKvTu@!Oo?M&X9C{JZF+FOopufG}TJs+uuh^?NY`Lmwa;@WTyQ)>UgzG^y_vR!lTI5{&@u6s1s z{4(wz?l*OxfU&W$Hi3`(mdyC3Wo;-FUBfN2w zX!JkSwd(WbCN(_wF?La@1K*<+WGADdqV680Z~faJ4I2v!zu*zydIv{hZu-D*4$%HW z8lmM(*lRb$GW2kHH|C?rKV4(#3KNQ{pF8P&^%aJLh(WoOnBaD*i2C{vHvT~NMdbza zOeIAs3(|eQ$uN{wbvNQQA~ti1#sJrHwkdVjtgc*S^Z{FG*3R#NpH6je?d)h*wW!1~ z9?h@6fB*jW`DwrM9jF&eqF`lX9@U4=IKxx5luoRYm|>ne$+cd(n$QldV2v3vp&TJ-6! zQyKhc#tSFE?zz*t&=jk@OOFYzeV?N7nimdvA}(IIi50Xu{Nlo;w+X=l1NXkyf=hF+ z6?(BqR`Fs2p2jDixHmvcsI0zd`Si-7_bwB=kA3==I zL&>bMrpnaN=l1VY%5kAjC^d5Ze*aAnOCFP;k?N2pt8PJTk!Hrjp!mvx(eJ)Hnw>ROLRVCXW^Jr__|1-f6ebX244Kfq-fq(z=~A${A`3tXv)6 zU!tZ!=d0QOGZr%o+b+MV6+_lEtdg0Qsf_fHZ$`|lu1D_s>l4)NE2dh&H9n6nuFFfn z|Fy^bDgE~2x7)YNMUuhfd4EjD$19i+vc>e-Wo*hFsiku}d%r|2nhcH}g^$OsJp0!k z?`_5brte)oE@cFtQS(OdaLCPbE!*4%qR;i738pV17)-8W{s;%m#`uJCf%Bl(| zle+x?8~TJgUTL89{qW!Cl77-}4$1sjerVN<`_tvEK5(?Z;ZIoD%-Eyy`?vlfsb0U) zZ~yJ|pBn7(EGDg;$hNPML!feA|5C~PVQPr))NeTUKjRckr5h)x#+%f;&KjASnd$3u z=`?YIsoSMsjuXxf5-Pt?_klQAj3gj%9Fh|G+qJvI7SC;=;p>Hg@y1M>DIi*I{(r$Ay z)4@&J@#*;xJRT3+YP6ki@}=%&kS*b;XJ?1~eZ`~r-}jRE31u@aeL#$DGBGf~dLcxp zTVL?Pf~(8uhL*M$yFRe{su`u{Hl(l`ee1VBP(#Ev9dqwveh(BnM|m{QyVaTgvm-zc z=yb$I${(ik@Kv5}rX2U}1a=_L#v!#Ahaj<{R|4s}`i`jz22X!oU1jwpQbQR_hUNk& zFT2J$Hea>VY_>~fHzfDG)YR3f_3-3hgQsU-ieD;rk6n#)@aax^vgftNnnoFt#-uP! z&6SW{cJmxmCy+tpe#Qbk%QExCjUUK=_2KX?HTs7#=0yhFbS3LbFVClmT8U`o2)x7U z^l~Og7(Z}YT^==jC@U*VIZ4e$_wuJJ`f{dOK#$kZJ9xTf8L!m~jT^5-s18*LlhD^; zqox+kCKR}pyrS`~$;Ix2z5Ue;FBUEICY9l?_J6f=ra?_*+a5=4aX`^VWpG9X41`1p zpv)~aAPJCW)G(<)h+z_l3?YJETA?8zb083~K}eV(%mD(e%p@U7L=whE2_Rr-34~c+ zyn5fQ`|ZB3ulmcWQ>Rv)IHPej~E%*&h5q=Bkr-6FQ}dLHoXAb@k9nQZ!Gzz>_#HGJAAi@p{Z(7msUAU`pAq#-oq%-R;Fwum$b< zrf^nO4)N#LQoe0a{Kya=M)gihGCeq${qyQ~QnLk#^>l(vb&;YY=s8eSaIJ+-kFY;^ z(L$vPICnVvjk(l!y!kp0%d7Lx;g7rXHTvAa`uh4}(I$@UHSWug{)-kh_8nFzE?))j z0l2KjavyRzBD1lvv8w7)Oncq|OIf(A?Z1I!svSy@Z*%cfG^unkyS30et!REma*>+U z34=oSPcjn2zk>ptCf{-ngqKUW9+8nL2#eNO_LJy!?|IpC4$XDpocfN5-5J)c zVuPsnX?QU`+V0I@%o?gY)Dy*eQs=h48B7I-&k=P?D;zgMA5t6EFC#Ac^J}LYW zP7Ayh{tfXi@CUfui+lE@!IB=^FBJDPtfr1wD7k7+|2YO|=Lhx1on!43n=YNnb->hq zD-(GVAXhwP(cyg)lqZWTsY!Nnx>u0*ho=TsveoV%=kI||REc5Z?AaKP`_{{6zGZiJ zoeRkt1%u&FKAlB?6y+0(2!(DR3D$GMSu<`hTfAQ@Pk3rO-oRdqn!6|il^EPf7 zyDlOUnojQ?TWtTQ3jC@wuW>Gm?aqetUrI^5+DqN@UtNA9P-pI|N<1P&FSf3jP1Y`~ ztw+ZxFmAZ`XanM}qJq&+Am=%xi-mW}xi`pU7SmE6C0$yW4|uVV`vP6=A3*lJ;Iv9_ z>DHXtAT>ryIvaPbU48N+zihmr=0ZppY4nq??T$O8Tv{D)yJps!+Z_X;opV{quy6@82%w_eE+w#u&*W|_Z5T3mE5+q`WktG|RaEFBKn%BJ7aPqJ zjuPT@LOrwi*oa;=@M2C_M49?q&p6m+RQM9p(X|l?V?;F*W|W*5ZjrAw9Q%Bf8Q+b*Y$N?mj|oqP&!crrdZ!XS-yR3yAWxHe+ogCqsaQOVM75L9I35NqvY z!QO`F>!`qy+yZ(@`JF}lLw~?c4X}B=terd5%#2M>?bY`PdC57Ei>G7OY1mnDT4~6| zH2%J~5iwcSD0uOBLjzDdkt?M{oknA+^#p=;Av9F*3q+3HAmH(mYJPHz>FK%2>+ULy z2RFWUxmSF*sww-Yk&@%8PPRcoL7sKnpFye={_IOG{p6jT0SYG4^izyrydbjBqXFQzIq3$ z)hzI^$J7Y}@50%MV{xyXutmD(7p15Ul?#B&D0T>Zkn#ckT<_AGMV#n^!E|Sh`onrA zEjW@xNI>df-{zPq!2Ic#n`_ixjBTy^T0lh|+#dfrIw31%ZaI6=Da%T*JF6~C*hQc2 z9Lf&SZ-(6Yyg;U>6LtD+oxR-otv&O)ZGotW*SQ9TE4c|=&2f^=E!zQ^jN8?Nnnh_x z?MzHjuYVbl+VArTV>IEdg~}?yt6J&k>^oR!g;n#{CS+`*UD-~x*Xm2N@8#U_ z4p7e2(LcEp75o~{rn?XWw^hIZA$mXRi{Yu%dmn^`%&X5%oMsh#-9i& zA6{Ckgx1`ncVp5N_t2=S9@8tk0R#iRQVRbV6MS)2Bx#+^Lc1HC2ndo`TXy8gP5(@` z%&uA6=%63elc{hfA(loNeQ(@(4Gp?V;7wmjYWe-{r_pG9h9_5Ku<5e$K>E<0;_!*+ zgU&Etg_?}fK)4_bg)WX`kphJSI)j^T<4X(en==!^G4?rrgGxCDI6o`oCbZzlg9&{0 zYB27Qo!R)7k5CrK!i37Vd|v39;|+zL)^iH6N654W2w5~#oSC$8gKrp3Q_$6EWeUCl zcmDLK`OEEznzodFNvk+`A~#i*IOA5@0w*P_KV!66OBS-%kxUD^F=v~ZdFdrHmuzJ5myK{x*;FBeM)#_)gX9+AcJy` zQI-9bW7}B9_(rT0VS>c@tB-I{Z(2PUd};x}39{UbgPuEKIStNaP5*ohL0wEY z0YIkN1SpMC8O4;6Mmc-FOgmji_cxF3pL=-_-gdLZcFDgLW$NY<=6_6OWU28Jpg^g(E=m7(XFai*874tc^{JyYK zU}gd+-o9?y=p1gKT@YAxQi1ZXQKF08abwHKVV<)eB`?nfJawK zHl)P)g2<`QTXk4@ddf1D{Hdj-G3|hpf7Bng_lp3EN_lw$1D-r#P%?Tt#Tlf|TUzF< zG>nEbwlrc}OV+P|FTs0bM{d}EVBS0%Ls0_zt21&_l9GN0;@r?ZnMB((el$e88ZtH0vFYsfz(`H!Fs!QpJ3fOR%eJczPc>#d#!)GJ2Z#0ed>x~ zyZx=a(RFRix&cJ**7Ib@a%R7_Z5Fepl09C9Y>!T>m*kL~!|fNX-a~9vL$|!)FG&i1 z{T(DCmbL)@wfDhCmRf~_fJMvisC^LWmb>gWHI`oOF`oyWNr}o{T)zJv32OAfzQj1k zK}E8rU0Lp|BanF*iAXFlv&we&SM{^m`?L`tlNzHeg2r}fCNJGf~i z@Ms|Bni(In>5y&w)78SaeEkN5g_}l>4%jYS1bstl7WBf(#;KGui(XI?A)|WbMmk5( zGuc;)L5KZLNXl1TJxbI-={bjMxn^~-Td=YGnZ$c#mPCwsnei_U3!{HQZA?7-I+&yu z1syND?N-#8h>AXaRBuX6UBHK~wZZ5VE?NgWG)Br$K&b=W&*=FU%#V=9gffq7P%jhN z^tTZm?gprQ&Gch_k%G=`M`6m%klI}+D3cb6jbpv_lLy$1FLXc>G6JWgPOt^|H!-k&PP|82ZL+!QP(=PSJk77}JFXQd zVyOu-iFa%v5Qy7D0qw6$os`%7ISGk?_lkcv zNBUXCtv4{g*F}klIM`jpSERxl7{x%XiP)j~0}O QpGa7kTK`;Od^_rY0H$wm`Tzg`