From 718074f2918ee652f5bca68c1e655c7d709e429c Mon Sep 17 00:00:00 2001 From: Lichenkang <2188718831@qq.com> Date: Wed, 30 Sep 2026 23:02:30 +0800 Subject: [PATCH 01/12] Merge pull request #999 from 1lck/fix/815-project-aware-auto-jdk MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit fix(runtime): 自动选择符合项目要求的 JDK,统一两端启动与设置 (#815) --- ...aven-settings-reach-the-language-server.md | 37 ++- docs/development/platform-parity-matrix.csv | 2 +- docs/development/platform-parity-matrix.md | 2 +- .../IdeCapabilitiesFeatureModel.swift | 3 + .../Lithe/Core/Ports/RuntimeLocator.swift | 23 ++ .../Lithe/Core/Rust/RustCoreBridge.swift | 15 ++ .../Models/Runtime/ProjectRuntimeModels.swift | 7 +- .../MacOS/Runtime/MacRuntimeDiscovery.swift | 25 +- .../MacOS/Runtime/MacRuntimeLocator.swift | 12 + .../Services/Java/ProjectRuntimeService.swift | 67 +++++- .../Views/App/EffectiveRuntimeLabel.swift | 12 + .../DebugModuleTests.swift | 4 +- .../LitheTests/MacRuntimeDiscoveryTests.swift | 13 ++ .../Tests/LitheTests/MavenRuntimeTests.swift | 66 +++++- .../lithe-core/src/execution/configuration.rs | 12 +- .../src/execution/java_selection.rs | 114 +++++++++ rust/lithe-core/src/execution/mod.rs | 5 + rust/lithe-core/src/protocol/command.rs | 3 + rust/lithe-core/src/runtime/dispatcher.rs | 15 ++ .../lithe-core/src/tests/run_configuration.rs | 75 ++++++ shared/contracts/rust-core-api.md | 22 ++ .../automatic-java-selection.json | 216 ++++++++++++++++++ shared/platform-feature-matrix.json | 13 +- windows/tauri/src-tauri/src/run.rs | 160 ++++++++++++- .../src/features/run/api/run-host-api.ts | 2 +- .../components/effective-toolchain.test.tsx | 20 ++ .../run/components/effective-toolchain.tsx | 3 + .../components/run-configuration-editor.tsx | 2 + .../hooks/use-resolved-toolchains.test.tsx | 22 +- .../run/hooks/use-resolved-toolchains.ts | 4 +- .../project-environment-settings.tsx | 5 + 31 files changed, 929 insertions(+), 52 deletions(-) create mode 100644 rust/lithe-core/src/execution/java_selection.rs create mode 100644 shared/fixtures/run-configuration/automatic-java-selection.json create mode 100644 windows/tauri/src/features/run/components/effective-toolchain.test.tsx diff --git a/.agents/notes/implemented/architecture/2026-09-21-maven-settings-reach-the-language-server.md b/.agents/notes/implemented/architecture/2026-09-21-maven-settings-reach-the-language-server.md index 98fb78d09..58d4c2c4c 100644 --- a/.agents/notes/implemented/architecture/2026-09-21-maven-settings-reach-the-language-server.md +++ b/.agents/notes/implemented/architecture/2026-09-21-maven-settings-reach-the-language-server.md @@ -233,8 +233,41 @@ Maven、版本、路径和来源,例如「自动 → JDK 21.0.4 · 路径 · 版本比较。运行面板保留一步进入「设置 → 运行配置」并定位当前服务的入口;两端设置窗口 每次分类请求都会重新定位,重复请求同一分类同样生效。 -「自动」本身的选择规则(是否按项目要求版本挑选、Windows 候选排序)属于 #815,不在此处 -改变。 +### 自动 JDK 选择(#815) + +自动模式先满足项目已有的 Java 最低版本要求,再按来源选择。显式选择的项目 JDK、 +服务 JDK 或 Maven JDK 不会被自动替换。两端共用 Core `runConfig.selectJava`,版本 +比较复用现有需求诊断中的逻辑,兼容 Java 8 的 `1.8` 表达;不要在 Swift/TypeScript +各写一套版本比较,也不要重新解析 POM 或建立另一个项目模型。JDT LS 仍拥有语言 +服务项目状态,本次只消费既有生成文档中的最低版本,选择机器上的启动工具。 + +来源顺序为 JAVA_HOME、PATH、普通安装、Windows 项目内安装。来源相同时先按数值 +版本降序,再按候选标识排序,避免目录枚举顺序左右结果。macOS 没有版本要求时 +保留原有 JAVA_HOME/首个发现候选回退;Windows 无要求时仍按来源选取,但修正原本 +覆盖 JAVA_HOME 优先级的字符串排序。没有任何兼容版本时继续保留回退选择,同时在 +生效值旁显示安装兼容 JDK 的提示。损坏或未来版本的需求文件明确报错,不按无要求处理。 + +例如项目要求 Java 17、JAVA_HOME 为 8、PATH 为 17、普通目录有 21 时,自动选择 +PATH 的 17;显式选择 8 仍保留 8,由既有启动诊断报告不兼容。不要仅让设置页显示 +17,而启动仍取候选列表第一个 8。Windows 的发现结果也把实际自动候选放在首位, +保证 Run 诊断与宿主启动一致。Windows 设置与服务表单在项目发现结果更新后重新解析 +生效值,即使自动路径字段仍为空;旧请求不能覆盖刷新后的结果。macOS 的 Run、Maven、 +测试和设置经过同一个选择入口。 + +考虑过只改 Windows 排序,但这不能解决两端忽略项目要求的问题。也没有直接选最高 +版本:兼容的 JAVA_HOME/PATH 是用户已有环境意图,应优先保留。选择策略只读取现有 +工作区需求文档,不生成文件、探测进程或写入安装包。macOS 启动复用当前项目已经 +探测的候选;尚未完成完整发现时只探测 Java 并保留到项目切换或显式刷新,不能为了 +选择 Java 连带探测 Maven。无版本要求且 JAVA_HOME 可用时保留不探测的快速路径。 +PATH 仅用于识别已有候选的来源(平台负责解析符号链接),不额外扩大默认候选集合;代价是需求变化后需要重新解析 +选择,不能把上一次项目的结果缓存成全局默认值。 + +回归验证使用共享 `automatic-java-selection.json`,覆盖枚举反序、数值补丁版本、 +旧版 Java 格式、兼容候选筛选、无要求/无匹配回退、空候选及损坏文档。macOS 的 +`automaticJavaUsesProjectMinimumAndPreservesExplicitSelections` 验证设置选择、Maven +继承、显式覆盖与项目切换;Windows 的 +`automatic_java_selection_reads_requirement_changes_without_probing` 验证同一工作区 +需求变化和显式路径旁路。原生实机验收仍需覆盖 Run、Maven 目标和测试运行。 ### Maven 设置页的检测边界(#844) diff --git a/docs/development/platform-parity-matrix.csv b/docs/development/platform-parity-matrix.csv index d639d80ab..e208969af 100644 --- a/docs/development/platform-parity-matrix.csv +++ b/docs/development/platform-parity-matrix.csv @@ -42,7 +42,7 @@ ai-commit-generation,AI,AI 提交信息,Provider 配置与提交信息生成,已 ai-chat-session,AI,AI 对话,对话会话与流式响应,未实现,不适用,已实现,待验证,AI,Windows 验证新建会话、流式输出、取消和失败恢复;macOS 需要先定义产品范围。,,macos/Sources/Lithe/Platform/MacOS/AI,windows/tauri/src/features/ai/components/chat/ai-chat.tsx; windows/tauri/src/features/ai/services/ai-chat-service.ts ai-chat-history,AI,AI 对话,对话历史、置顶与归档,未实现,不适用,已实现,待验证,AI,Windows 验证历史加载、重命名、置顶、归档、删除和重启后持久化。,,macos/Sources/Lithe/Platform/MacOS/AI,windows/tauri/src/features/ai/services/ai-chat-history-service.ts; windows/tauri/src/features/layout/components/sidebar/sidebar-history.tsx ai-chat-providers,AI,AI 对话,多 Provider 与工作区范围,未实现,不适用,已实现,待验证,AI,Windows 验证 Provider 切换、凭据边界、工作区范围和模型错误提示。,,macos/Sources/Lithe/Platform/MacOS/AI,windows/tauri/src/features/ai/services/providers; windows/tauri/src/features/ai/lib/ai-workspace-scope.ts -java-runtime-discovery,Java,Java/Maven,JDK 与 Maven 工具链发现,已实现,待验证,已实现,待验证,Java / Maven,配置多个 JDK/Maven 候选,验证发现、选择、版本不匹配和错误提示。,,macos/Sources/Lithe/Services/Java; macos/Sources/Lithe/Views/Run,windows/tauri/src/features/maven; windows/tauri/src/features/workspace +java-runtime-discovery,Java,Java/Maven,JDK 与 Maven 工具链发现及按项目要求自动选择 JDK,已实现,待验证,已实现,待验证,Java / Maven,两端安装 JDK 8/17/21:项目要求 17 且 JAVA_HOME 为 8 时验证自动选择兼容版本;核对设置、Run、Maven 目标、测试使用同一 JDK。验证显式选择不变、没有兼容版本时回退并提示,以及无要求时的平台默认顺序。,,macos/Sources/Lithe/Services/Java; macos/Sources/Lithe/Views/Run; macos/Sources/Lithe/Core/Ports/RuntimeLocator.swift; macos/Tests/LitheTests/MavenRuntimeTests.swift; rust/lithe-core/src/execution/java_selection.rs,windows/tauri/src/features/maven; windows/tauri/src/features/workspace; windows/tauri/src-tauri/src/run.rs; shared/fixtures/run-configuration/automatic-java-selection.json java-maven-project-settings,Java,Java/Maven,按项目配置 Maven 工具链、settings.xml 与本地仓库,已实现,待验证,已实现,待验证,Java / Maven,在 macOS 项目设置和 Windows Maven 设置页分别切换两个项目,配置 Maven、JDK、settings.xml 与本地仓库,验证保存重开、自动检测展示及项目间隔离;Windows 还需验证写盘失败后可直接重试。 Windows 自定义 settings.xml 后,仓库留空应显示该文件中的目录;验证 XML 实体和 CDATA、继承的项目 JDK/Maven、切换项目丢弃未保存草稿;冷启动 Wrapper 不应触发下载,版本探测卡住时设置窗口保持响应并在超时后清理探测进程;Run 配置显式选择的 Maven/JDK 优先于项目设置,未设置的字段分别继承 Maven 设置;确认主任务与前置任务使用相同工具链,Maven 设置上下文缺失时仍保留显式覆盖,均未配置时交由平台自动发现。,,macos/Sources/Lithe/Views/App/ProjectRuntimeSettingsView.swift; macos/Sources/Lithe/Services/Java/ProjectRuntimeService.swift,windows/tauri/src/features/settings/components/tabs/maven-settings-panel.tsx; windows/tauri/src/features/maven/stores/maven.store.ts; windows/tauri/src-tauri/src/maven.rs; windows/tauri/src-tauri/src/maven/settings_xml.rs; windows/tauri/src/features/maven/components/maven-detected-value.test.tsx; windows/tauri/src/features/settings/components/tabs/maven-settings-panel.test.tsx; windows/tauri/src/features/run/stores/run.store.ts; windows/tauri/src/features/run/stores/run-maven-context.test.ts java-maven-project,Java,Java/Maven,项目根、模块与源码集识别,已实现,待验证,已实现,待验证,Java / Maven,使用单模块、多模块和非标准源码目录 fixture 对比项目模型。,,macos/Sources/Lithe/Services/Java; shared/contracts/application-boundary.md,windows/tauri/src/features/maven; windows/tauri/src/features/workspace java-maven-profiles-dependencies,Java,Java/Maven,Profile 与依赖树,已实现,待验证,已实现,待验证,Java / Maven,"切换 Maven profile 并刷新依赖树,确认模块归属、顺序、版本冲突/重复/依赖管理标注和错误边界;在依赖节点超过 6,000 个的项目中确认依赖树完整加载。 Windows 在 Maven 面板展开「依赖项」应直接加载依赖树,失败时显示宿主返回的真实原因,而不是通用的“无法加载 Maven 依赖”(#970)。",,macos/Sources/Lithe/Views/Run; macos/Sources/Lithe/Services/Java; macos/Sources/LitheExecutionModule/Services/MavenService.swift; macos/Sources/Lithe/Platform/MacOS/Persistence/MacMavenDependencyOutputStore.swift; rust/lithe-core/src/project/maven_dependency_tree.rs,windows/tauri/src/features/maven; windows/tauri/src-tauri/src/maven.rs; rust/lithe-core/src/project/maven_dependency_tree.rs diff --git a/docs/development/platform-parity-matrix.md b/docs/development/platform-parity-matrix.md index 6e05a4cde..80d8ef0d0 100644 --- a/docs/development/platform-parity-matrix.md +++ b/docs/development/platform-parity-matrix.md @@ -140,7 +140,7 @@ | 功能组 | 能力点 | macOS | Windows | 负责人 | 验证方式 | 备注 | | --- | --- | --- | --- | --- | --- | --- | -| Java/Maven | **JDK 与 Maven 工具链发现**
java-runtime-discovery | ✅ 已实现
🔍 待验证
`macos/Sources/Lithe/Services/Java`、`macos/Sources/Lithe/Views/Run` | ✅ 已实现
🔍 待验证
`windows/tauri/src/features/maven`、`windows/tauri/src/features/workspace` | Java / Maven | 配置多个 JDK/Maven 候选,验证发现、选择、版本不匹配和错误提示。 | | +| Java/Maven | **JDK 与 Maven 工具链发现及按项目要求自动选择 JDK**
java-runtime-discovery | ✅ 已实现
🔍 待验证
`macos/Sources/Lithe/Services/Java`、`macos/Sources/Lithe/Views/Run`、`macos/Sources/Lithe/Core/Ports/RuntimeLocator.swift`、`macos/Tests/LitheTests/MavenRuntimeTests.swift`、`rust/lithe-core/src/execution/java_selection.rs` | ✅ 已实现
🔍 待验证
`windows/tauri/src/features/maven`、`windows/tauri/src/features/workspace`、`windows/tauri/src-tauri/src/run.rs`、`shared/fixtures/run-configuration/automatic-java-selection.json` | Java / Maven | 两端安装 JDK 8/17/21:项目要求 17 且 JAVA_HOME 为 8 时验证自动选择兼容版本;核对设置、Run、Maven 目标、测试使用同一 JDK。验证显式选择不变、没有兼容版本时回退并提示,以及无要求时的平台默认顺序。 | | | Java/Maven | **按项目配置 Maven 工具链、settings.xml 与本地仓库**
java-maven-project-settings | ✅ 已实现
🔍 待验证
`macos/Sources/Lithe/Views/App/ProjectRuntimeSettingsView.swift`、`macos/Sources/Lithe/Services/Java/ProjectRuntimeService.swift` | ✅ 已实现
🔍 待验证
`windows/tauri/src/features/settings/components/tabs/maven-settings-panel.tsx`、`windows/tauri/src/features/maven/stores/maven.store.ts`、`windows/tauri/src-tauri/src/maven.rs`、`windows/tauri/src-tauri/src/maven/settings_xml.rs`、`windows/tauri/src/features/maven/components/maven-detected-value.test.tsx`、`windows/tauri/src/features/settings/components/tabs/maven-settings-panel.test.tsx`、`windows/tauri/src/features/run/stores/run.store.ts`、`windows/tauri/src/features/run/stores/run-maven-context.test.ts` | Java / Maven | 在 macOS 项目设置和 Windows Maven 设置页分别切换两个项目,配置 Maven、JDK、settings.xml 与本地仓库,验证保存重开、自动检测展示及项目间隔离;Windows 还需验证写盘失败后可直接重试。 Windows 自定义 settings.xml 后,仓库留空应显示该文件中的目录;验证 XML 实体和 CDATA、继承的项目 JDK/Maven、切换项目丢弃未保存草稿;冷启动 Wrapper 不应触发下载,版本探测卡住时设置窗口保持响应并在超时后清理探测进程;Run 配置显式选择的 Maven/JDK 优先于项目设置,未设置的字段分别继承 Maven 设置;确认主任务与前置任务使用相同工具链,Maven 设置上下文缺失时仍保留显式覆盖,均未配置时交由平台自动发现。 | | | Java/Maven | **项目根、模块与源码集识别**
java-maven-project | ✅ 已实现
🔍 待验证
`macos/Sources/Lithe/Services/Java`、`shared/contracts/application-boundary.md` | ✅ 已实现
🔍 待验证
`windows/tauri/src/features/maven`、`windows/tauri/src/features/workspace` | Java / Maven | 使用单模块、多模块和非标准源码目录 fixture 对比项目模型。 | | | Java/Maven | **Profile 与依赖树**
java-maven-profiles-dependencies | ✅ 已实现
🔍 待验证
`macos/Sources/Lithe/Views/Run`、`macos/Sources/Lithe/Services/Java`、`macos/Sources/LitheExecutionModule/Services/MavenService.swift`、`macos/Sources/Lithe/Platform/MacOS/Persistence/MacMavenDependencyOutputStore.swift`、`rust/lithe-core/src/project/maven_dependency_tree.rs` | ✅ 已实现
🔍 待验证
`windows/tauri/src/features/maven`、`windows/tauri/src-tauri/src/maven.rs`、`rust/lithe-core/src/project/maven_dependency_tree.rs` | Java / Maven | 切换 Maven profile 并刷新依赖树,确认模块归属、顺序、版本冲突/重复/依赖管理标注和错误边界;在依赖节点超过 6,000 个的项目中确认依赖树完整加载。 Windows 在 Maven 面板展开「依赖项」应直接加载依赖树,失败时显示宿主返回的真实原因,而不是通用的“无法加载 Maven 依赖”(#970)。 | | diff --git a/macos/Sources/Lithe/Application/Features/IdeCapabilitiesFeatureModel.swift b/macos/Sources/Lithe/Application/Features/IdeCapabilitiesFeatureModel.swift index 8ae06db03..36855a96b 100644 --- a/macos/Sources/Lithe/Application/Features/IdeCapabilitiesFeatureModel.swift +++ b/macos/Sources/Lithe/Application/Features/IdeCapabilitiesFeatureModel.swift @@ -293,6 +293,9 @@ final class IdeCapabilitiesFeatureModel: ObservableObject { case .found(let url, let source): return ["status": "resolved", "path": url.path, "source": choiceSource(source)] case .invalid(let path): return ["status": "invalid", "message": "Invalid toolchain: " + path] case .fallback(let path, let next): var result = choice(next); result["invalidOverride"] = path; return result + case .warning(let url, let source, let message): + return ["status": "resolved", "path": url.path, "source": choiceSource(source), "warning": message] + case .unavailable(let message): return ["status": "invalid", "message": message] case .notFound: return ["status": "notFound"] case nil: return ["status": "loading"] } diff --git a/macos/Sources/Lithe/Core/Ports/RuntimeLocator.swift b/macos/Sources/Lithe/Core/Ports/RuntimeLocator.swift index 0084f45a2..cf4cb07e6 100644 --- a/macos/Sources/Lithe/Core/Ports/RuntimeLocator.swift +++ b/macos/Sources/Lithe/Core/Ports/RuntimeLocator.swift @@ -68,7 +68,11 @@ struct DefaultRuntimeToolDiscovery: RuntimeToolDiscovery { protocol RuntimeLocator: Sendable { func environment() -> [String: String] func discover() -> RuntimeDiscoveryResult + /// Java-only probing for launch paths; never runs Maven or other tools. + func discoverJavaRuntimes() -> [JavaRuntimeCandidate] func validJavaHome(path: String) -> URL? + /// Returns the candidate reached by PATH, resolving platform symlinks. + func javaHomeOnPath(in candidates: [JavaRuntimeCandidate]) -> URL? func javaRuntime(at homeURL: URL) -> JavaRuntimeCandidate? func isExecutable(at url: URL) -> Bool func systemMavenExecutable() -> URL? @@ -81,7 +85,26 @@ protocol RuntimeLocator: Sendable { } extension RuntimeLocator { + func discoverJavaRuntimes() -> [JavaRuntimeCandidate] { discover().javaRuntimes } + func javaHomeOnPath(in candidates: [JavaRuntimeCandidate]) -> URL? { nil } // Default implementation for test stubs and non-macOS locators that do // not ship a bundled JDK. func bundledJdkHome() -> URL? { nil } } + +/// Shared selection policy; platform locators still own executable validation +/// and version probing. Requirements are read through Core's existing document. +protocol JavaRuntimeSelecting: Sendable { + func selectJavaRuntime(at root: URL?, candidates: [AutomaticJavaCandidate], fallbackID: String?) -> Result +} + +struct AutomaticJavaCandidate: Codable, Sendable { + let id: String + let version: String + let priority: UInt32 +} + +struct AutomaticJavaSelection: Decodable, Sendable { + let id: String? + let warning: String? +} diff --git a/macos/Sources/Lithe/Core/Rust/RustCoreBridge.swift b/macos/Sources/Lithe/Core/Rust/RustCoreBridge.swift index 7d14671b0..455237d5e 100644 --- a/macos/Sources/Lithe/Core/Rust/RustCoreBridge.swift +++ b/macos/Sources/Lithe/Core/Rust/RustCoreBridge.swift @@ -2615,6 +2615,19 @@ struct RustCoreBridge: Sendable, IncrementalLanguageServerRuntimeCore { ) } + func selectJavaRuntime( + at root: URL?, candidates: [AutomaticJavaCandidate], fallbackID: String? + ) -> Result { + struct Payload: Encodable { + let root: String? + let candidates: [AutomaticJavaCandidate] + let fallbackId: String? + } + return executeResult(command: "runConfig.selectJava", payload: Payload( + root: root?.standardizedFileURL.path, candidates: candidates, fallbackId: fallbackID + )) + } + func inspectRunConfiguration( at rootURL: URL, checkFingerprint: Bool? = nil, @@ -4104,3 +4117,5 @@ private extension RustCoreBridge.WorkspaceNodePayload { ) } } + +extension RustCoreBridge: JavaRuntimeSelecting {} diff --git a/macos/Sources/Lithe/Models/Runtime/ProjectRuntimeModels.swift b/macos/Sources/Lithe/Models/Runtime/ProjectRuntimeModels.swift index 807714cb8..ddc23a34e 100644 --- a/macos/Sources/Lithe/Models/Runtime/ProjectRuntimeModels.swift +++ b/macos/Sources/Lithe/Models/Runtime/ProjectRuntimeModels.swift @@ -230,6 +230,8 @@ enum RuntimeChoiceSource: Equatable, Sendable { /// Outcome of one runtime selection chain in `ProjectRuntimeService`. indirect enum RuntimeChoice: Equatable, Sendable { case found(URL, RuntimeChoiceSource) + case warning(URL, RuntimeChoiceSource, String) + case unavailable(String) /// A configured path that is not a usable runtime; a launch fails on it. case invalid(String) /// A configured path that is not usable, replaced by `to`. @@ -239,15 +241,16 @@ indirect enum RuntimeChoice: Equatable, Sendable { var url: URL? { switch self { - case .found(let url, _): url + case .found(let url, _), .warning(let url, _, _): url case .fallback(_, let choice): choice.url - case .invalid, .notFound: nil + case .invalid, .notFound, .unavailable: nil } } /// The same choice presented as inherited from the project JDK. var inheritedAsProjectJDK: RuntimeChoice { if case .found(let url, _) = self { return .found(url, .projectJDK) } + if case .warning(let url, _, let message) = self { return .warning(url, .projectJDK, message) } return self } } diff --git a/macos/Sources/Lithe/Platform/MacOS/Runtime/MacRuntimeDiscovery.swift b/macos/Sources/Lithe/Platform/MacOS/Runtime/MacRuntimeDiscovery.swift index ab2b1ef77..ce19e387a 100644 --- a/macos/Sources/Lithe/Platform/MacOS/Runtime/MacRuntimeDiscovery.swift +++ b/macos/Sources/Lithe/Platform/MacOS/Runtime/MacRuntimeDiscovery.swift @@ -2,11 +2,7 @@ import Foundation enum MacRuntimeDiscovery { static func discover(environment: [String: String]) -> RuntimeDiscoveryResult { - let javaRuntimes = discoverJavaHomes(environment: environment) - .compactMap(probeJavaHome) - .sorted { lhs, rhs in - lhs.version.localizedStandardCompare(rhs.version) == .orderedDescending - } + let javaRuntimes = discoverJavaRuntimes(environment: environment) let mavenRuntimes = discoverMavenExecutables(environment: environment) .compactMap(probeMaven) .sorted { lhs, rhs in @@ -47,6 +43,25 @@ enum MacRuntimeDiscovery { : nil } + static func discoverJavaRuntimes(environment: [String: String]) -> [JavaRuntimeCandidate] { + discoverJavaHomes(environment: environment).compactMap(probeJavaHome).sorted { lhs, rhs in + lhs.version.localizedStandardCompare(rhs.version) == .orderedDescending + } + } + + static func javaHomeOnPath(environment: [String: String]) -> URL? { + for directory in (environment["PATH"] ?? "").split(separator: ":") { + let executable = URL(fileURLWithPath: String(directory), isDirectory: true).appendingPathComponent("java") + guard FileManager.default.isExecutableFile(atPath: executable.path) else { continue } + let resolved = executable.resolvingSymlinksInPath() + // Apple's system launcher selects a JDK itself; /usr is not a JDK + // home and must never become JAVA_HOME for child processes. + guard resolved.path != "/usr/bin/java" else { return nil } + return validJavaHome(resolved.deletingLastPathComponent().deletingLastPathComponent().path) + } + return nil + } + private static func discoverJavaHomes(environment: [String: String]) -> [URL] { discoverJavaHomes( environment: environment, diff --git a/macos/Sources/Lithe/Platform/MacOS/Runtime/MacRuntimeLocator.swift b/macos/Sources/Lithe/Platform/MacOS/Runtime/MacRuntimeLocator.swift index db57d9166..666ae93d9 100644 --- a/macos/Sources/Lithe/Platform/MacOS/Runtime/MacRuntimeLocator.swift +++ b/macos/Sources/Lithe/Platform/MacOS/Runtime/MacRuntimeLocator.swift @@ -44,10 +44,22 @@ struct MacRuntimeLocator: RuntimeLocator { MacRuntimeDiscovery.discover(environment: environment()) } + func discoverJavaRuntimes() -> [JavaRuntimeCandidate] { + MacRuntimeDiscovery.discoverJavaRuntimes(environment: environment()) + } + func validJavaHome(path: String) -> URL? { MacRuntimeDiscovery.validJavaHome(path) } + func javaHomeOnPath(in candidates: [JavaRuntimeCandidate]) -> URL? { + guard let home = MacRuntimeDiscovery.javaHomeOnPath(environment: environment()) else { return nil } + return candidates.lazy.compactMap { candidate -> URL? in + let url = URL(fileURLWithPath: candidate.homePath, isDirectory: true) + return url.resolvingSymlinksInPath().path == home.resolvingSymlinksInPath().path ? url : nil + }.first + } + func javaRuntime(at homeURL: URL) -> JavaRuntimeCandidate? { MacRuntimeDiscovery.probeJavaHome(homeURL) } diff --git a/macos/Sources/Lithe/Services/Java/ProjectRuntimeService.swift b/macos/Sources/Lithe/Services/Java/ProjectRuntimeService.swift index 4f66026ce..e5b86c523 100644 --- a/macos/Sources/Lithe/Services/Java/ProjectRuntimeService.swift +++ b/macos/Sources/Lithe/Services/Java/ProjectRuntimeService.swift @@ -39,7 +39,9 @@ final class ProjectRuntimeService: ObservableObject { @Published private(set) var isDiscovering = false @Published private(set) var settings = ProjectRuntimeSettings() private var activeServiceJavaHomePath = "" + private var launchJavaRuntimes: [JavaRuntimeCandidate]? + private let javaSelector: any JavaRuntimeSelecting private let runtimeLocator: any RuntimeLocator private let store: any KeyValueStore private let toolDiscovery: any RuntimeToolDiscovery @@ -52,8 +54,10 @@ final class ProjectRuntimeService: ObservableObject { init( runtimeLocator: any RuntimeLocator, store: any KeyValueStore, - toolDiscovery: (any RuntimeToolDiscovery)? = nil + toolDiscovery: (any RuntimeToolDiscovery)? = nil, + javaSelector: any JavaRuntimeSelecting = RustCoreBridge() ) { + self.javaSelector = javaSelector self.runtimeLocator = runtimeLocator self.store = store self.toolDiscovery = toolDiscovery ?? DefaultRuntimeToolDiscovery() @@ -71,6 +75,7 @@ final class ProjectRuntimeService: ObservableObject { javaRuntimes = [] mavenRuntimes = [] discoveredProjectURL = nil + launchJavaRuntimes = nil settings = loadSettings(for: normalizedURL) javaEnvironmentReport = .checking(for: normalizedURL) javaLanguageServerRuntimePreparation = .unprepared @@ -85,6 +90,7 @@ final class ProjectRuntimeService: ObservableObject { javaRuntimes = [] mavenRuntimes = [] discoveredProjectURL = nil + launchJavaRuntimes = nil settings = ProjectRuntimeSettings() javaEnvironmentReport = nil isDiscovering = false @@ -105,6 +111,7 @@ final class ProjectRuntimeService: ObservableObject { } func refreshAvailableRuntimes() async { + launchJavaRuntimes = nil discoveryTask?.cancel() discoveryTask = nil await performRuntimeRefresh() @@ -137,13 +144,21 @@ final class ProjectRuntimeService: ObservableObject { isDiscovering = false } + private func javaRuntimesForLaunch() -> [JavaRuntimeCandidate] { + if hasDiscoveredRuntimes { return javaRuntimes } + if let launchJavaRuntimes { return launchJavaRuntimes } + let runtimes = runtimeLocator.discoverJavaRuntimes() + launchJavaRuntimes = runtimes + return runtimes + } + func javaHomeURL(overridePath: String? = nil) -> URL? { - chooseJavaHome(overridePath: overridePath) { self.runtimeLocator.discover().javaRuntimes }?.url + chooseJavaHome(overridePath: overridePath) { self.javaRuntimesForLaunch() }?.url } /// The single JDK selection chain behind launches and Settings: an explicit - /// override, then the project JDK, then `JAVA_HOME`, then the first detected - /// JDK. An invalid explicit path does not fall back, so a launch fails on it. + /// override, then the project JDK, then Core's requirement-aware automatic + /// selection. An invalid explicit path does not fall back, so a launch fails on it. /// /// `detected` supplies discovered runtimes and is only called when the chain /// reaches detection; returning `nil` reports that detection is pending. @@ -163,14 +178,44 @@ final class ProjectRuntimeService: ObservableObject { let path = normalizedOverridePath(configuredProjectJDK) return runtimeLocator.validJavaHome(path: path).map { RuntimeChoice.found($0, .projectSetting) } ?? .invalid(path) } - let paths = [runtimeLocator.environment()["JAVA_HOME"]] - for path in paths.compactMap({ $0 }).map(normalizedPath).filter({ !$0.isEmpty }) { - if let home = runtimeLocator.validJavaHome(path: path) { return .found(home, .javaHomeEnvironment) } + let environmentHome = runtimeLocator.environment()["JAVA_HOME"] + .flatMap { runtimeLocator.validJavaHome(path: normalizedPath($0)) } + if let environmentHome { + // With no project requirement, preserve the old no-probe JAVA_HOME + // fast path. Unknown versions never satisfy a real minimum, so only + // an unconstrained result may bypass discovery here. + let candidate = AutomaticJavaCandidate(id: environmentHome.path, version: "", priority: 0) + switch javaSelector.selectJavaRuntime(at: projectURL, candidates: [candidate], fallbackID: candidate.id) { + case .failure(let error): return .unavailable(error.message) + case .success(let selection) where selection.warning == nil: + return .found(environmentHome, .javaHomeEnvironment) + case .success: break + } } + // Settings supplies cached probes; pending discovery must not synchronously + // launch java -version or temporarily present an incompatible JAVA_HOME. guard let runtimes = detected() else { return nil } - return runtimes.first - .flatMap { runtimeLocator.validJavaHome(path: $0.homePath) } - .map { RuntimeChoice.found($0, .detected) } ?? .notFound + let pathHome = runtimeLocator.javaHomeOnPath(in: runtimes) + var candidates = runtimes.compactMap { runtime -> AutomaticJavaCandidate? in + guard let home = runtimeLocator.validJavaHome(path: runtime.homePath) else { return nil } + let priority: UInt32 = home.path == environmentHome?.path ? 0 : (home.path == pathHome?.path ? 1 : 2) + return AutomaticJavaCandidate(id: home.path, version: runtime.version, priority: priority) + } + // Preserve the old JAVA_HOME fallback even if its version probe failed. + // An unknown version cannot satisfy a requirement in the shared policy. + if let home = environmentHome, !candidates.contains(where: { $0.id == home.path }) { + candidates.append(AutomaticJavaCandidate(id: home.path, version: "", priority: 0)) + } + let fallback = environmentHome?.path ?? candidates.first?.id + switch javaSelector.selectJavaRuntime(at: projectURL, candidates: candidates, fallbackID: fallback) { + case .failure(let error): return .unavailable(error.message) + case .success(let selection): + guard let id = selection.id else { return .notFound } + let url = URL(fileURLWithPath: id).standardizedFileURL + let source: RuntimeChoiceSource = url.path == environmentHome?.path ? .javaHomeEnvironment : .detected + if let warning = selection.warning { return .warning(url, source, warning) } + return .found(url, source) + } } func javaExecutableURL(overridePath: String? = nil) -> URL? { @@ -246,7 +291,7 @@ final class ProjectRuntimeService: ObservableObject { } func mavenJavaHomeURL(overridePath: String? = nil) -> URL? { - chooseMavenJavaHome(overridePath: overridePath) { self.runtimeLocator.discover().javaRuntimes }?.url + chooseMavenJavaHome(overridePath: overridePath) { self.javaRuntimesForLaunch() }?.url } /// Maven's JDK: an explicit override, then the configured Maven JDK, then diff --git a/macos/Sources/Lithe/Views/App/EffectiveRuntimeLabel.swift b/macos/Sources/Lithe/Views/App/EffectiveRuntimeLabel.swift index b2b6c558c..d277e34d7 100644 --- a/macos/Sources/Lithe/Views/App/EffectiveRuntimeLabel.swift +++ b/macos/Sources/Lithe/Views/App/EffectiveRuntimeLabel.swift @@ -39,6 +39,12 @@ struct EffectiveRuntimeLabel: View { .foregroundStyle(LitheTheme.primaryText) .textSelection(.enabled) .lineLimit(3) + case .warning(let url, let source, let message)?: + Text(resolvedText(url: url, source: source, mode: modeTitle)) + .font(LitheTheme.smallFont).textSelection(.enabled) + Text(message).font(LitheTheme.smallFont).foregroundStyle(LitheTheme.error) + case .unavailable(let message)?: + Text(message).font(LitheTheme.smallFont).foregroundStyle(LitheTheme.error) case .fallback(let invalidPath, let replacement)?: VStack(alignment: .leading, spacing: 2) { Text(String(format: String(localized: "Cannot use %@; using the project JDK instead."), invalidPath)) @@ -52,6 +58,12 @@ struct EffectiveRuntimeLabel: View { .font(.system(size: 11.5, design: .monospaced)) .foregroundStyle(LitheTheme.primaryText) .textSelection(.enabled) + case .warning(let url, _, let message): + Text(resolvedText(url: url, source: .projectJDK, mode: String(localized: "Use Project JDK"))) + .font(LitheTheme.smallFont) + Text(message).font(LitheTheme.smallFont).foregroundStyle(LitheTheme.error) + case .unavailable(let message): + Text(message).font(LitheTheme.smallFont).foregroundStyle(LitheTheme.error) case .invalid(let path), .fallback(let path, _): Text(String(format: String(localized: "Cannot be used: %@"), path)) .font(LitheTheme.smallFont) diff --git a/macos/Tests/LitheDebugModuleTests/DebugModuleTests.swift b/macos/Tests/LitheDebugModuleTests/DebugModuleTests.swift index 7edf4ee74..fef55162b 100644 --- a/macos/Tests/LitheDebugModuleTests/DebugModuleTests.swift +++ b/macos/Tests/LitheDebugModuleTests/DebugModuleTests.swift @@ -42,8 +42,8 @@ struct DebugModuleTests { #expect(feature.start(fileURL: source, rootURL: root, configuration: configuration)) let firstID = try #require(feature.activeSessionID) #expect(feature.startAdditional(fileURL: source, rootURL: root, configuration: configuration)) - feature.registerJavaUpdateTarget(JavaDebugLaunchTarget(mainClass: "example.Main")) - await feature.restartJavaService { _, _ in JavaDebugLaunchTarget(mainClass: "example.Main") } + feature.registerJavaUpdateTarget(JavaDebugLaunchTarget(mainClass: "example.Main", projectName: "app")) + await feature.restartJavaService { _, _ in JavaDebugLaunchTarget(mainClass: "example.Main", projectName: "app") } #expect(createdSessions.count == 3) #expect(createdSessions[0].isRunning) #expect(!createdSessions[1].isRunning) diff --git a/macos/Tests/LitheTests/MacRuntimeDiscoveryTests.swift b/macos/Tests/LitheTests/MacRuntimeDiscoveryTests.swift index 390576c94..0e96f6594 100644 --- a/macos/Tests/LitheTests/MacRuntimeDiscoveryTests.swift +++ b/macos/Tests/LitheTests/MacRuntimeDiscoveryTests.swift @@ -4,6 +4,19 @@ import Testing @Suite("macOS runtime discovery") struct MacRuntimeDiscoveryTests { + @Test + func resolvesJavaHomeFromPathSymlinkWithoutLaunchingIt() throws { + let root = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString) + defer { try? FileManager.default.removeItem(at: root) } + let home = root.appendingPathComponent("custom-jdk", isDirectory: true) + try createExecutableJava(at: home) + let bin = root.appendingPathComponent("path-bin", isDirectory: true) + try FileManager.default.createDirectory(at: bin, withIntermediateDirectories: true) + try FileManager.default.createSymbolicLink(at: bin.appendingPathComponent("java"), withDestinationURL: home.appendingPathComponent("bin/java")) + let environment = ["PATH": bin.path] + #expect(MacRuntimeDiscovery.javaHomeOnPath(environment: environment)?.resolvingSymlinksInPath().path == home.resolvingSymlinksInPath().path) + } + @Test func discoversSDKMANJavaCandidatesFromDefaultDirectory() throws { let testRoot = FileManager.default.temporaryDirectory diff --git a/macos/Tests/LitheTests/MavenRuntimeTests.swift b/macos/Tests/LitheTests/MavenRuntimeTests.swift index c2c1d6cde..269dda7bd 100644 --- a/macos/Tests/LitheTests/MavenRuntimeTests.swift +++ b/macos/Tests/LitheTests/MavenRuntimeTests.swift @@ -5,6 +5,47 @@ import Testing @Suite("Maven and runtime integration") struct MavenRuntimeTests { + @Test + @MainActor + func automaticJavaUsesProjectMinimumAndPreservesExplicitSelections() throws { + let root = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString) + try FileManager.default.createDirectory(at: root.appendingPathComponent(".lithe/toolchains"), withIntermediateDirectories: true) + defer { try? FileManager.default.removeItem(at: root) } + let requirements = root.appendingPathComponent(".lithe/toolchains/requirements.json") + let old = JavaRuntimeCandidate(homePath: "/jdk/env", version: "1.8.0_402", vendor: "fixture") + let current = JavaRuntimeCandidate(homePath: "/jdk/current", version: "21.0.4", vendor: "fixture") + let locator = ChoiceRuntimeLocator(environmentValues: ["JAVA_HOME": old.homePath], validJavaHomes: [old.homePath, current.homePath], runtimeCandidates: [old, current]) + let service = ProjectRuntimeService(runtimeLocator: locator, store: EmptyKeyValueStore()) + service.openProject(at: root) + func require(_ version: String) throws { + try Data("{\"version\":1,\"toolchains\":{\"project-jdk\":{\"type\":\"java\",\"minimumVersion\":\"\(version)\"}}}".utf8).write(to: requirements) + } + try require("17") + let choice = service.chooseJavaHome(overridePath: nil) { [old, current] } + #expect(choice?.url?.path == current.homePath) + #expect(service.javaHomeURL()?.path == current.homePath) + #expect(service.mavenJavaHomeURL()?.path == current.homePath) + #expect(locator.discoverCalls == 1) + #expect(service.chooseMavenJavaHome(overridePath: nil) { [old, current] }?.url == choice?.url) + #expect(service.chooseJavaHome(overridePath: old.homePath) { [old, current] }?.url?.path == old.homePath) + #expect(service.chooseJavaHome(overridePath: "/missing") { [old, current] } == .invalid("/missing")) + try require("25") + if case .warning(let url, _, let message)? = service.chooseJavaHome(overridePath: nil, detected: { [old, current] }) { + #expect(url.path == old.homePath) + #expect(message.contains("25")) + } else { Issue.record("Expected an actionable fallback warning") } + try FileManager.default.removeItem(at: requirements) + #expect(service.chooseJavaHome(overridePath: nil) { [current, old] }?.url?.path == old.homePath) + try Data("{".utf8).write(to: requirements) + if case .unavailable(_)? = service.chooseJavaHome(overridePath: nil, detected: { [old, current] }) {} else { + Issue.record("Invalid requirements must be visible") + } + // Project switching cannot retain the previous workspace's requirement. + service.closeProject() + #expect(service.chooseJavaHome(overridePath: nil) { [current, old] }?.url?.path == old.homePath) + #expect(locator.discoverCalls == 1) + } + @Test func mavenLifecyclePhasesMatchTheSharedPlatformContract() throws { let fixture = try Self.platformContractFixture() @@ -286,6 +327,8 @@ struct MavenRuntimeTests { service.openProject(at: URL(fileURLWithPath: "/workspace", isDirectory: true)) #expect(describe(service.chooseJavaHome(overridePath: nil) { nil }) == "found /jdk/env javaHomeEnvironment") + #expect(service.javaHomeURL()?.path == "/jdk/env") + #expect(locator.discoverCalls == 0) #expect(describe(service.chooseJavaHome(overridePath: "/missing") { nil }) == "invalid /missing") #expect(describe(service.chooseJavaHome(overridePath: "/jdk/detected") { nil }) == "found /jdk/detected configured") @@ -329,6 +372,14 @@ struct MavenRuntimeTests { // One discovery in total: the environment report must reuse its result // instead of probing every JDK again on the main actor. #expect(locator.discoverCalls == 1) + #expect(service.javaHomeURL() == nil) + #expect(service.mavenJavaHomeURL() == nil) + #expect(locator.discoverCalls == 1) + service.closeProject() + service.openProject(at: URL(fileURLWithPath: "/another-fixture-workspace", isDirectory: true)) + #expect(service.javaHomeURL() == nil) + #expect(service.mavenJavaHomeURL() == nil) + #expect(locator.discoverCalls == 2) } @Test @@ -341,19 +392,19 @@ struct MavenRuntimeTests { let service = ProjectRuntimeService(runtimeLocator: locator, store: EmptyKeyValueStore()) service.openProject(at: URL(fileURLWithPath: "/workspace", isDirectory: true)) - #expect(describe(service.chooseMavenJavaHome(overridePath: nil) { nil }) == "found /jdk/env projectJDK") + #expect(describe(service.chooseMavenJavaHome(overridePath: nil) { [] }) == "found /jdk/env projectJDK") var settings = service.settings settings.mavenJavaHomePath = "/missing-maven-jdk" service.updateSettings(settings) // Launches keep falling back to the project JDK; Settings now says so. - #expect(describe(service.chooseMavenJavaHome(overridePath: nil) { nil }) + #expect(describe(service.chooseMavenJavaHome(overridePath: nil) { [] }) == "fallback /missing-maven-jdk -> found /jdk/env javaHomeEnvironment") #expect(service.mavenJavaHomeURL()?.path == "/jdk/env") settings.mavenJavaHomePath = "/jdk/maven" service.updateSettings(settings) - #expect(describe(service.chooseMavenJavaHome(overridePath: nil) { nil }) == "found /jdk/maven projectSetting") + #expect(describe(service.chooseMavenJavaHome(overridePath: nil) { [] }) == "found /jdk/maven projectSetting") // A usable Maven JDK never walks the project chain, which may probe every JDK. #expect(locator.discoverCalls == 0) } @@ -699,6 +750,8 @@ private func describe(_ choice: RuntimeChoice?) -> String { case .found(let url, let source)?: "found \(url.path) \(source)" case .invalid(let path)?: "invalid \(path)" case .fallback(let path, let replacement)?: "fallback \(path) -> \(describe(replacement))" + case .warning(let url, _, let message)?: "warning \(url.path) \(message)" + case .unavailable(let message)?: "unavailable \(message)" case .notFound?: "notFound" } } @@ -709,24 +762,27 @@ private final class ChoiceRuntimeLocator: RuntimeLocator, @unchecked Sendable { private let validJavaHomes: Set private let executables: Set private let systemMaven: URL? + private let runtimeCandidates: [JavaRuntimeCandidate] private(set) var discoverCalls = 0 init( environmentValues: [String: String] = [:], validJavaHomes: Set = [], executables: Set = [], - systemMaven: URL? = nil + systemMaven: URL? = nil, + runtimeCandidates: [JavaRuntimeCandidate] = [] ) { self.environmentValues = environmentValues self.validJavaHomes = validJavaHomes self.executables = executables self.systemMaven = systemMaven + self.runtimeCandidates = runtimeCandidates } func environment() -> [String: String] { environmentValues } func discover() -> RuntimeDiscoveryResult { discoverCalls += 1 - return RuntimeDiscoveryResult(javaRuntimes: [], mavenRuntimes: []) + return RuntimeDiscoveryResult(javaRuntimes: runtimeCandidates, mavenRuntimes: []) } func validJavaHome(path: String) -> URL? { validJavaHomes.contains(path) ? URL(fileURLWithPath: path, isDirectory: true) : nil diff --git a/rust/lithe-core/src/execution/configuration.rs b/rust/lithe-core/src/execution/configuration.rs index a5f430382..e001d98ee 100644 --- a/rust/lithe-core/src/execution/configuration.rs +++ b/rust/lithe-core/src/execution/configuration.rs @@ -2249,7 +2249,7 @@ fn validate_version(version: u32) -> Result<(), CoreError> { Ok(()) } -fn validate_sidecar_version(version: u32) -> Result<(), CoreError> { +pub(super) fn validate_sidecar_version(version: u32) -> Result<(), CoreError> { if version != SIDECAR_VERSION { return Err( CoreError::new(ErrorCode::NotSupported, "Unsupported document version") @@ -2720,7 +2720,9 @@ fn read_document_value(root: &Path, relative: &str) -> Result, Cor Ok(Some(value)) } -fn read_requirements(root: &Path) -> Result, CoreError> { +pub(super) fn read_requirements( + root: &Path, +) -> Result, CoreError> { let path = root .join(".lithe") .join("toolchains") @@ -2950,7 +2952,7 @@ fn append_toolchain_diagnostics( } } -fn version_satisfies(kind: &str, actual: &str, required: &str, minimum: bool) -> bool { +pub(super) fn version_satisfies(kind: &str, actual: &str, required: &str, minimum: bool) -> bool { let mut actual_parts = version_parts(actual); let mut required_parts = version_parts(required); if kind == "java" { @@ -2967,7 +2969,7 @@ fn version_satisfies(kind: &str, actual: &str, required: &str, minimum: bool) -> } } -fn version_parts(value: &str) -> Vec { +pub(super) fn version_parts(value: &str) -> Vec { value .split(|character: char| !character.is_ascii_digit()) .filter(|part| !part.is_empty()) @@ -2977,7 +2979,7 @@ fn version_parts(value: &str) -> Vec { /// Drops the legacy `1.` prefix Java 8 and earlier report (`1.8.0_504`), so /// those runtimes compare on the same feature-version scale as `8` or `17.0.12`. -fn java_feature_version_parts(parts: Vec) -> Vec { +pub(super) fn java_feature_version_parts(parts: Vec) -> Vec { if parts.len() > 1 && parts[0] == 1 { parts[1..].to_vec() } else { diff --git a/rust/lithe-core/src/execution/java_selection.rs b/rust/lithe-core/src/execution/java_selection.rs new file mode 100644 index 000000000..4997fdc96 --- /dev/null +++ b/rust/lithe-core/src/execution/java_selection.rs @@ -0,0 +1,114 @@ +//! Selects an automatic project JDK from platform-probed candidates. +//! Project requirements remain owned by the existing run-configuration document. + +use super::configuration::{ + java_feature_version_parts, read_requirements, version_parts, version_satisfies, +}; +use crate::protocol::CoreError; +use serde::{Deserialize, Serialize}; +use std::path::PathBuf; + +/// A probed candidate; `id` is opaque and is never persisted or opened by Core. +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct JavaSelectionCandidate { + /// Opaque machine-local identity supplied by the host. + pub id: String, + /// Version reported by the Java executable; empty means unknown. + pub version: String, + /// Lower values win: JAVA_HOME, PATH, installations, project-managed JDK. + pub priority: u32, +} + +/// Platform candidates and the workspace whose existing requirements apply. +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct JavaSelectionRequest { + /// Open workspace; absent means no project requirements. + pub root: Option, + /// Usable executable homes probed by the platform. + pub candidates: Vec, + /// The platform's unconstrained choice; used when requirements are absent + /// or no candidate satisfies them. This keeps fallback behavior explicit. + pub fallback_id: Option, +} + +/// Selected candidate and an actionable warning when no compatible JDK exists. +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct JavaSelection { + /// Selected identity, or absent when the host found no usable JDK. + pub id: Option, + /// No candidate satisfies the minimum; the fallback remains usable. + pub warning: Option, +} + +/// Orders equivalent sources numerically, including legacy Java `1.8` versions. +/// Stable IDs break equal-version ties independently of filesystem enumeration. +pub fn compare_java_candidates( + left: &JavaSelectionCandidate, + right: &JavaSelectionCandidate, +) -> std::cmp::Ordering { + left.priority + .cmp(&right.priority) + .then_with(|| { + java_feature_version_parts(version_parts(&right.version)) + .cmp(&java_feature_version_parts(version_parts(&left.version))) + }) + .then_with(|| left.id.cmp(&right.id)) +} + +/// Reuses Core's requirement reader and version semantics without generating +/// requirements, probing executables, or changing an explicit user selection. +pub fn select_java(request: JavaSelectionRequest) -> Result { + let document = request + .root + .as_deref() + .map(read_requirements) + .transpose()? + .flatten(); + if let Some(document) = &document { + super::configuration::validate_sidecar_version(document.version)?; + } + let minimum = document + .as_ref() + .and_then(|doc| doc.toolchains.get("project-jdk")) + .filter(|requirement| requirement.kind == "java") + .and_then(|requirement| requirement.minimum_version.as_deref()); + Ok(select_java_candidates( + &request.candidates, + request.fallback_id.as_deref(), + minimum, + )) +} + +/// Applies only the project minimum; explicit paths and vendor preferences +/// remain the responsibility of existing launch validation and diagnostics. +pub fn select_java_candidates( + candidates: &[JavaSelectionCandidate], + fallback_id: Option<&str>, + minimum: Option<&str>, +) -> JavaSelection { + let fallback = candidates + .iter() + .find(|candidate| Some(candidate.id.as_str()) == fallback_id) + .or_else(|| { + candidates + .iter() + .min_by(|a, b| compare_java_candidates(a, b)) + }); + let Some(minimum) = minimum else { + return JavaSelection { + id: fallback.map(|candidate| candidate.id.clone()), + warning: None, + }; + }; + let matching = candidates + .iter() + .filter(|candidate| version_satisfies("java", &candidate.version, minimum, true)) + .min_by(|a, b| compare_java_candidates(a, b)); + JavaSelection { + id: matching.or(fallback).map(|candidate| candidate.id.clone()), + warning: matching.is_none().then(|| format!("No installed JDK satisfies Java {minimum} or newer. Install a compatible JDK or choose one in project settings.")), + } +} diff --git a/rust/lithe-core/src/execution/mod.rs b/rust/lithe-core/src/execution/mod.rs index 80f882a41..2c75486be 100644 --- a/rust/lithe-core/src/execution/mod.rs +++ b/rust/lithe-core/src/execution/mod.rs @@ -2,7 +2,12 @@ mod configuration; mod detectors; +mod java_selection; mod launch_command; +pub use java_selection::{ + compare_java_candidates, select_java, select_java_candidates, JavaSelection, + JavaSelectionCandidate, JavaSelectionRequest, +}; mod types; pub(crate) use configuration::*; diff --git a/rust/lithe-core/src/protocol/command.rs b/rust/lithe-core/src/protocol/command.rs index 9ca04202b..16957ad27 100644 --- a/rust/lithe-core/src/protocol/command.rs +++ b/rust/lithe-core/src/protocol/command.rs @@ -205,6 +205,8 @@ pub enum CoreCommand { LspDestroyServer, /// Validates layered run-configuration documents (`runConfig.inspect`). RunConfigInspect, + /// Selects an automatic project JDK (`runConfig.selectJava`). + RunConfigSelectJava, /// Regenerates detected run configurations (`runConfig.generate`). RunConfigGenerate, /// Merges configuration layers and toolchains (`runConfig.resolve`). @@ -420,6 +422,7 @@ impl CoreCommand { "lsp.waitEvents" => Some(Self::LspWaitEvents), "lsp.destroyServer" => Some(Self::LspDestroyServer), "runConfig.inspect" => Some(Self::RunConfigInspect), + "runConfig.selectJava" => Some(Self::RunConfigSelectJava), "runConfig.generate" => Some(Self::RunConfigGenerate), "runConfig.resolve" => Some(Self::RunConfigResolve), "runConfig.updateOptions" => Some(Self::RunConfigUpdateOptions), diff --git a/rust/lithe-core/src/runtime/dispatcher.rs b/rust/lithe-core/src/runtime/dispatcher.rs index e8837b007..77f51e1c5 100644 --- a/rust/lithe-core/src/runtime/dispatcher.rs +++ b/rust/lithe-core/src/runtime/dispatcher.rs @@ -1438,6 +1438,21 @@ fn execute(request: &str) -> CoreResponse { Err(error) => CoreResponse::failure(id, error), } } + CoreCommand::RunConfigSelectJava => { + match serde_json::from_value::(parsed.payload) + .map_err(|error| { + CoreError::new(ErrorCode::InvalidRequest, "Invalid Java selection request") + .with_details(error.to_string()) + }) + .and_then(crate::execution::select_java) + { + Ok(data) => CoreResponse::success( + id, + serde_json::to_value(data).expect("Java selection encodes"), + ), + Err(error) => CoreResponse::failure(id, error), + } + } CoreCommand::RunConfigInspect => { match serde_json::from_value::(parsed.payload) .map_err(|error| { diff --git a/rust/lithe-core/src/tests/run_configuration.rs b/rust/lithe-core/src/tests/run_configuration.rs index ee17d6712..d9c66259d 100644 --- a/rust/lithe-core/src/tests/run_configuration.rs +++ b/rust/lithe-core/src/tests/run_configuration.rs @@ -3848,3 +3848,78 @@ fn working_directory_override_must_name_an_existing_project_directory() { serde_json::from_str(updated["data"]["document"].as_str().unwrap()).unwrap(); assert_eq!(document["configurations"][0]["cwd"], "backend"); } + +#[test] +fn automatic_java_selection_uses_shared_fixture_and_current_requirements() { + let fixture: Value = serde_json::from_str(include_str!( + "../../../../shared/fixtures/run-configuration/automatic-java-selection.json" + )) + .unwrap(); + let root = temporary_root("automatic-java-selection"); + fs::create_dir_all(root.join(".lithe/toolchains")).unwrap(); + struct Cleanup(PathBuf); + impl Drop for Cleanup { + fn drop(&mut self) { + fs::remove_dir_all(&self.0).expect("remove selection fixture"); + } + } + let _cleanup = Cleanup(root.clone()); + let requirements = root.join(".lithe/toolchains/requirements.json"); + for case in fixture["cases"].as_array().unwrap() { + let document = serde_json::json!({"version":1,"toolchains":{"project-jdk":{ + "type":"java", "minimumVersion":case["minimum"] + }}}); + fs::write(&requirements, document.to_string()).unwrap(); + // Reverse enumeration order too: tie-breaking is not filesystem order. + for reverse in [false, true] { + let mut candidates = case["candidates"].as_array().unwrap().clone(); + if reverse { + candidates.reverse(); + } + let response: Value = serde_json::from_str(&execute_json( + &serde_json::json!({ + "id":"selection", "command":"runConfig.selectJava", "payload":{ + "root":root, "candidates":candidates, "fallbackId":case["fallbackId"] + } + }) + .to_string(), + )) + .unwrap(); + assert_eq!(response["ok"], true, "{response}"); + assert_eq!( + response["data"]["id"], + fixture["expected"][case["name"].as_str().unwrap()]["id"], + "{}", + case["name"] + ); + assert_eq!( + !response["data"]["warning"].is_null(), + fixture["expected"][case["name"].as_str().unwrap()]["warning"] + .as_bool() + .unwrap() + ); + } + assert_eq!( + fs::read_to_string(&requirements).unwrap(), + document.to_string() + ); + } + // Missing documents preserve fallback; malformed or future documents must + // not silently launch a potentially incompatible JDK. + for contents in [None, Some("{"), Some(r#"{"version":999,"toolchains":{}}"#)] { + if let Some(contents) = contents { + fs::write(&requirements, contents).unwrap(); + } else { + fs::remove_file(&requirements).unwrap(); + } + let response: Value = serde_json::from_str(&execute_json(&serde_json::json!({ + "id":"selection", "command":"runConfig.selectJava", "payload":{ + "root":root, "candidates":[{"id":"env","version":"1.8.0_402","priority":0}], "fallbackId":"env" + } + }).to_string())).unwrap(); + assert_eq!(response["ok"], contents.is_none(), "{response}"); + if contents.is_none() { + assert_eq!(response["data"]["id"], "env"); + } + } +} diff --git a/shared/contracts/rust-core-api.md b/shared/contracts/rust-core-api.md index b4d72a4d7..d31596308 100644 --- a/shared/contracts/rust-core-api.md +++ b/shared/contracts/rust-core-api.md @@ -384,6 +384,7 @@ package manager owns the download and Lithe does not infer bytes from logs. | `java.structure` | Parse Java editor folds, inlay hints, and portable syntax roles | | `spring.index` | Build a deterministic Spring configuration, bean, injection, and endpoint index | | `mybatis.index` | Build a deterministic MyBatis mapper-interface and XML statement index | +| `runConfig.selectJava` | Select a project-compatible automatic JDK from platform-probed candidates | | `runConfig.inspect` | Inspect `.lithe` run documents, versions, and staleness without writing files | | `runConfig.generate` | Generate deterministic Java/Maven configurations and toolchain requirements | | `runConfig.resolve` | Merge generated, project, and local layers and return diagnostics | @@ -1829,6 +1830,27 @@ the response preserves the path text, uses one-based line and column values, and normalizes severity to `error` or `warning`. Duplicate issue lines are removed deterministically. +`runConfig.selectJava` reads only the existing workspace +`.lithe/toolchains/requirements.json` document. Its request contains optional +`root`, `candidates` (`id`, probed `version`, numeric source `priority`), and +`fallbackId`. IDs are opaque machine-local identities, never persisted or opened +by this operation. Lower priority wins; equal-priority candidates use descending +numeric Java versions (including legacy `1.8`), then ascending ID. + +When `project-jdk.minimumVersion` exists, selection first filters using the same +Java version comparison as run-configuration diagnostics. The response is +`{ id, warning }`: the compatible candidate, or the supplied usable fallback +with an actionable warning if none qualifies. Missing requirements retain the +platform's unconstrained choice; malformed/unsupported documents return the +existing parse/version error. Explicit configured paths bypass automatic +selection. The operation never generates requirements or probes executables. +The cross-platform examples are in +`shared/fixtures/run-configuration/automatic-java-selection.json`. + +Windows `run_resolve_toolchains` includes an optional `warning` on resolved +JDKs, including inherited Maven JDKs. This warning remains visible even when no +run configuration exists to carry a scoped `toolchainVersionMismatch` diagnostic. + `runConfig.inspect` also returns the local document-level `toolchain`, including when no generated configuration exists (`status: "missing"`). Settings, toolchain-only callers, and initial run-panel presentation may send diff --git a/shared/fixtures/run-configuration/automatic-java-selection.json b/shared/fixtures/run-configuration/automatic-java-selection.json new file mode 100644 index 000000000..8248fb335 --- /dev/null +++ b/shared/fixtures/run-configuration/automatic-java-selection.json @@ -0,0 +1,216 @@ +{ + "version": 1, + "cases": [ + { + "name": "skip-incompatible-java-home", + "minimum": "17", + "candidates": [ + { + "id": "env", + "version": "1.8.0_402", + "priority": 0 + }, + { + "id": "path", + "version": "17.0.12", + "priority": 1 + }, + { + "id": "latest", + "version": "21.0.4", + "priority": 2 + } + ], + "fallbackId": "env" + }, + { + "name": "compatible-java-home-wins", + "minimum": "17", + "candidates": [ + { + "id": "env", + "version": "17.0.12", + "priority": 0 + }, + { + "id": "latest", + "version": "21.0.4", + "priority": 2 + } + ], + "fallbackId": "env" + }, + { + "name": "numeric-version-order", + "minimum": "8", + "candidates": [ + { + "id": "eight", + "version": "8.0.402", + "priority": 2 + }, + { + "id": "twenty-one", + "version": "21.0.4", + "priority": 2 + }, + { + "id": "seventeen", + "version": "17.0.12", + "priority": 2 + } + ], + "fallbackId": "eight" + }, + { + "name": "numeric-patch-order", + "minimum": "17", + "candidates": [ + { + "id": "nine", + "version": "17.0.9", + "priority": 2 + }, + { + "id": "ten", + "version": "17.0.10", + "priority": 2 + } + ], + "fallbackId": "nine" + }, + { + "name": "legacy-java-eight", + "minimum": "8", + "candidates": [ + { + "id": "env", + "version": "1.8.0_402", + "priority": 0 + }, + { + "id": "latest", + "version": "21.0.4", + "priority": 2 + } + ], + "fallbackId": "env" + }, + { + "name": "stable-tie", + "minimum": "17", + "candidates": [ + { + "id": "z", + "version": "17.0.12", + "priority": 2 + }, + { + "id": "a", + "version": "17.0.12", + "priority": 2 + } + ], + "fallbackId": "z" + }, + { + "name": "unsatisfied-retains-fallback", + "minimum": "21", + "candidates": [ + { + "id": "env", + "version": "8.0.402", + "priority": 0 + }, + { + "id": "other", + "version": "17.0.12", + "priority": 2 + } + ], + "fallbackId": "env" + }, + { + "name": "missing-requirement-retains-fallback", + "minimum": null, + "candidates": [ + { + "id": "env", + "version": "8.0.402", + "priority": 0 + }, + { + "id": "latest", + "version": "21.0.4", + "priority": 2 + } + ], + "fallbackId": "env" + }, + { + "name": "unknown-version-is-not-compatible", + "minimum": "17", + "candidates": [ + { + "id": "env", + "version": "", + "priority": 0 + }, + { + "id": "other", + "version": "17.0.12", + "priority": 2 + } + ], + "fallbackId": "env" + }, + { + "name": "no-runtimes", + "minimum": "17", + "candidates": [], + "fallbackId": null + } + ], + "expected": { + "skip-incompatible-java-home": { + "id": "path", + "warning": false + }, + "compatible-java-home-wins": { + "id": "env", + "warning": false + }, + "numeric-version-order": { + "id": "twenty-one", + "warning": false + }, + "numeric-patch-order": { + "id": "ten", + "warning": false + }, + "legacy-java-eight": { + "id": "env", + "warning": false + }, + "stable-tie": { + "id": "a", + "warning": false + }, + "unsatisfied-retains-fallback": { + "id": "env", + "warning": true + }, + "missing-requirement-retains-fallback": { + "id": "env", + "warning": false + }, + "unknown-version-is-not-compatible": { + "id": "other", + "warning": false + }, + "no-runtimes": { + "id": null, + "warning": true + } + } +} diff --git a/shared/platform-feature-matrix.json b/shared/platform-feature-matrix.json index 2e75a51bc..3cd1e2042 100644 --- a/shared/platform-feature-matrix.json +++ b/shared/platform-feature-matrix.json @@ -1137,11 +1137,14 @@ "id": "java-runtime-discovery", "area": "Java", "group": "Java/Maven", - "capability": "JDK 与 Maven 工具链发现", + "capability": "JDK 与 Maven 工具链发现及按项目要求自动选择 JDK", "macos": { "evidence": [ "macos/Sources/Lithe/Services/Java", - "macos/Sources/Lithe/Views/Run" + "macos/Sources/Lithe/Views/Run", + "macos/Sources/Lithe/Core/Ports/RuntimeLocator.swift", + "macos/Tests/LitheTests/MavenRuntimeTests.swift", + "rust/lithe-core/src/execution/java_selection.rs" ], "implementationStatus": "implemented", "verificationStatus": "pending" @@ -1149,13 +1152,15 @@ "windows": { "evidence": [ "windows/tauri/src/features/maven", - "windows/tauri/src/features/workspace" + "windows/tauri/src/features/workspace", + "windows/tauri/src-tauri/src/run.rs", + "shared/fixtures/run-configuration/automatic-java-selection.json" ], "implementationStatus": "implemented", "verificationStatus": "pending" }, "owner": "Java / Maven", - "verification": "配置多个 JDK/Maven 候选,验证发现、选择、版本不匹配和错误提示。" + "verification": "两端安装 JDK 8/17/21:项目要求 17 且 JAVA_HOME 为 8 时验证自动选择兼容版本;核对设置、Run、Maven 目标、测试使用同一 JDK。验证显式选择不变、没有兼容版本时回退并提示,以及无要求时的平台默认顺序。" }, { "id": "java-maven-project-settings", diff --git a/windows/tauri/src-tauri/src/run.rs b/windows/tauri/src-tauri/src/run.rs index 064107fac..825fec42a 100644 --- a/windows/tauri/src-tauri/src/run.rs +++ b/windows/tauri/src-tauri/src/run.rs @@ -398,6 +398,8 @@ pub enum ToolchainResolution { version: String, vendor: String, source: &'static str, + #[serde(skip_serializing_if = "Option::is_none")] + warning: Option, }, /// Nothing was configured and nothing usable was detected. NotFound { message: Option }, @@ -451,12 +453,14 @@ fn resolve_toolchains_for_display( path, version, vendor, + warning, .. } => ToolchainResolution::Resolved { path: path.clone(), version: version.clone(), vendor: vendor.clone(), source: "projectJdk", + warning: warning.clone(), }, other => other.clone(), } @@ -471,7 +475,19 @@ fn resolve_toolchains_for_display( } fn java_resolution(root: &Path, override_path: &str) -> ToolchainResolution { - match resolve_java_home(root, override_path) { + let automatic = if override_path.trim().is_empty() { + match select_project_java(Some(root), &discover_java_runtimes(Some(root))) { + Ok(selection) => Some(selection), + Err(message) => return ToolchainResolution::Invalid { message }, + } + } else { + None + }; + let resolved = automatic + .as_ref() + .map(|selection| Ok(selection.id.clone())) + .unwrap_or_else(|| resolve_java_home(root, override_path)); + match resolved { Ok(Some(home)) => { let home_path = Path::new(&home); let source = if override_path.trim().is_empty() { @@ -488,6 +504,7 @@ fn java_resolution(root: &Path, override_path: &str) -> ToolchainResolution { vendor: runtime.map(|runtime| runtime.vendor).unwrap_or_default(), path: home, source, + warning: automatic.and_then(|selection| selection.warning), } } Ok(None) => ToolchainResolution::NotFound { message: None }, @@ -544,6 +561,7 @@ fn maven_resolution(root: &Path, override_path: &str) -> ToolchainResolution { version, vendor: String::new(), source, + warning: None, } } Err(message) if override_path.trim().is_empty() => ToolchainResolution::NotFound { @@ -986,6 +1004,58 @@ pub(crate) fn discover_java_runtimes(project_root: Option<&Path>) -> Vec, + runtimes: &[JavaRuntime], +) -> Vec { + // Resolve source identities once, not from the sorting comparator. Windows + // path identity is case-insensitive and may be reached through a symlink. + let key = |path: &Path| normalize_path(path).to_string_lossy().to_lowercase(); + let environment_home = std::env::var_os("JAVA_HOME").map(|value| key(Path::new(&value))); + let path_home = lookup_on_path("java.exe") + .or_else(|| lookup_on_path("java")) + .and_then(|path| java_home_from_executable(&path)) + .map(|path| key(&path)); + let project_home = root.map(|root| key(&root.join(".lithe/toolchains/jdk"))); + runtimes + .iter() + .map(|runtime| { + let home = key(Path::new(&runtime.home_path)); + let priority = if Some(&home) == environment_home.as_ref() { + 0 + } else if Some(&home) == path_home.as_ref() { + 1 + } else if Some(&home) == project_home.as_ref() { + 3 + } else { + 2 + }; + lithe_core::execution::JavaSelectionCandidate { + id: runtime.home_path.clone(), + version: runtime.version.clone(), + priority, + } + }) + .collect() +} + +fn select_project_java( + root: Option<&Path>, + runtimes: &[JavaRuntime], +) -> Result { + let candidates = java_selection_candidates(root, runtimes); + let fallback_id = candidates + .iter() + .min_by(|a, b| lithe_core::execution::compare_java_candidates(a, b)) + .map(|candidate| candidate.id.clone()); + lithe_core::execution::select_java(lithe_core::execution::JavaSelectionRequest { + root: root.map(Path::to_path_buf), + candidates, + fallback_id, + }) + .map_err(|error| error.message) +} + fn probe_java_homes(homes: Vec) -> Vec { let mut java = Vec::new(); let mut seen_homes = std::collections::HashSet::new(); @@ -997,11 +1067,17 @@ fn probe_java_homes(homes: Vec) -> Vec { java.push(runtime); } } + // Preserve source priority, then compare Java versions numerically. Discovery + // order from read_dir must never choose the runtime of a project. + let candidates = java_selection_candidates(None, &java) + .into_iter() + .map(|candidate| (candidate.id.clone(), candidate)) + .collect::>(); java.sort_by(|left, right| { - right - .version - .cmp(&left.version) - .then(left.home_path.cmp(&right.home_path)) + lithe_core::execution::compare_java_candidates( + &candidates[&left.home_path], + &candidates[&right.home_path], + ) }); java } @@ -1020,7 +1096,19 @@ fn discover_toolchains_with_overrides( if let Some(path) = java_home_path.filter(|value| !value.trim().is_empty()) { homes.insert(0, PathBuf::from(path)); } - let java = probe_java_homes(homes); + let mut java = probe_java_homes(homes); + match select_project_java(project_root, &java) { + Ok(selection) => { + if let Some(index) = java + .iter() + .position(|runtime| Some(&runtime.home_path) == selection.id.as_ref()) + { + let selected = java.remove(index); + java.insert(0, selected); + } + } + Err(error) => eprintln!("Could not select the project JDK: {error}"), + } let maven = discover_maven_candidates( maven_executable_candidates(project_root), @@ -1313,9 +1401,8 @@ pub(crate) fn resolve_java_home( )); } // Resolving a JDK must not also run Maven wrappers or Node probes. - Ok(discover_java_runtimes(Some(root)) - .first() - .map(|runtime| runtime.home_path.clone())) + select_project_java(Some(root), &discover_java_runtimes(Some(root))) + .map(|selection| selection.id) } fn resolve_executable( @@ -2527,6 +2614,61 @@ mod tests { ); } + #[test] + fn automatic_java_selection_reads_requirement_changes_without_probing() { + let root = temp_project(); + struct Cleanup(PathBuf); + impl Drop for Cleanup { + fn drop(&mut self) { + fs::remove_dir_all(&self.0).expect("remove Java fixture"); + } + } + let _cleanup = Cleanup(root.clone()); + let runtimes = vec![ + JavaRuntime { + home_path: root.join("jdk8").to_string_lossy().into_owned(), + version: "1.8.0_402".into(), + vendor: "fixture".into(), + }, + JavaRuntime { + home_path: root.join("jdk21").to_string_lossy().into_owned(), + version: "21.0.4".into(), + vendor: "fixture".into(), + }, + ]; + fs::create_dir_all(root.join(".lithe/toolchains")).unwrap(); + let requirements = root.join(".lithe/toolchains/requirements.json"); + fs::write( + &requirements, + r#"{"version":1,"toolchains":{"project-jdk":{"type":"java","minimumVersion":"17"}}}"#, + ) + .unwrap(); + let selected = select_project_java(Some(&root), &runtimes).unwrap(); + assert_eq!(selected.id.as_deref(), Some(runtimes[1].home_path.as_str())); + assert!(selected.warning.is_none()); + fs::write( + &requirements, + r#"{"version":1,"toolchains":{"project-jdk":{"type":"java","minimumVersion":"25"}}}"#, + ) + .unwrap(); + let fallback = select_project_java(Some(&root), &runtimes).unwrap(); + assert_eq!(fallback.id, selected.id); + assert!(fallback.warning.unwrap().contains("25")); + fs::write(&requirements, "{").unwrap(); + assert!(select_project_java(Some(&root), &runtimes).is_err()); + // Explicit paths never pass through requirement-based auto selection. + fs::create_dir_all(root.join("jdk8/bin")).unwrap(); + fs::write(root.join("jdk8/bin/java.exe"), b"fixture").unwrap(); + assert_eq!( + resolve_java_home(&root, "jdk8").unwrap(), + Some( + normalize_path(&root.join("jdk8")) + .to_string_lossy() + .into_owned() + ) + ); + } + #[test] fn displayed_maven_is_the_wrapper_a_launch_would_run() { // Settings shows the resolved Maven in place of "automatic"; it must be diff --git a/windows/tauri/src/features/run/api/run-host-api.ts b/windows/tauri/src/features/run/api/run-host-api.ts index ce65c0014..549981774 100644 --- a/windows/tauri/src/features/run/api/run-host-api.ts +++ b/windows/tauri/src/features/run/api/run-host-api.ts @@ -55,7 +55,7 @@ export type ToolchainSource = | "projectJdk"; export type ToolchainResolution = - | { status: "resolved"; path: string; version: string; vendor: string; source: ToolchainSource } + | { status: "resolved"; path: string; version: string; vendor: string; source: ToolchainSource; warning?: string } | { status: "notFound"; message: string | null } | { status: "invalid"; message: string }; diff --git a/windows/tauri/src/features/run/components/effective-toolchain.test.tsx b/windows/tauri/src/features/run/components/effective-toolchain.test.tsx new file mode 100644 index 000000000..436ecefdc --- /dev/null +++ b/windows/tauri/src/features/run/components/effective-toolchain.test.tsx @@ -0,0 +1,20 @@ +import { expect, test } from "bun:test"; +import { renderToStaticMarkup } from "react-dom/server"; +import { LocaleProvider } from "@/i18n/locale-provider"; +import { EffectiveToolchain } from "./effective-toolchain"; + +test("shows an automatic JDK fallback warning without run configuration diagnostics", () => { + const warning = "No installed JDK satisfies Java 21 or newer."; + const markup = renderToStaticMarkup( + + + , + ); + expect(markup).toContain(warning); + expect(markup).toContain('role="alert"'); + expect(markup).toContain("C:/fixture/jdk8"); +}); diff --git a/windows/tauri/src/features/run/components/effective-toolchain.tsx b/windows/tauri/src/features/run/components/effective-toolchain.tsx index d020d68c1..075c83786 100644 --- a/windows/tauri/src/features/run/components/effective-toolchain.tsx +++ b/windows/tauri/src/features/run/components/effective-toolchain.tsx @@ -29,6 +29,9 @@ export function EffectiveToolchain({ > {line.text}

+ {state.status === "resolved" && state.warning && ( +

{state.warning}

+ )} {requirements.map((message) => (

{message} diff --git a/windows/tauri/src/features/run/components/run-configuration-editor.tsx b/windows/tauri/src/features/run/components/run-configuration-editor.tsx index 68c209230..d04a6ddb4 100644 --- a/windows/tauri/src/features/run/components/run-configuration-editor.tsx +++ b/windows/tauri/src/features/run/components/run-configuration-editor.tsx @@ -144,6 +144,8 @@ export function RunConfigurationEditor({ selection.javaHomePath, selection.mavenExecutablePath, selection.mavenJavaHomePath, + undefined, + discoveredJava, ); const overrideMode = (value: string): EffectiveToolchainMode => value ? "configured" : "inherit"; diff --git a/windows/tauri/src/features/run/hooks/use-resolved-toolchains.test.tsx b/windows/tauri/src/features/run/hooks/use-resolved-toolchains.test.tsx index 03a47a43d..5142a65ae 100644 --- a/windows/tauri/src/features/run/hooks/use-resolved-toolchains.test.tsx +++ b/windows/tauri/src/features/run/hooks/use-resolved-toolchains.test.tsx @@ -60,8 +60,8 @@ let root: Root; const actEnvironment = globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT?: boolean }; let previousActEnvironment: boolean | undefined; -function Probe({ javaHomePath }: { javaHomePath: string }) { - const state = useResolvedToolchains("C:/work", javaHomePath, "", "", dependencies); +function Probe({ javaHomePath, revision = 0 }: { javaHomePath: string; revision?: number }) { + const state = useResolvedToolchains("C:/work", javaHomePath, "", "", dependencies, revision); return {state.java.status === "resolved" ? state.java.path : state.java.status}; } @@ -76,8 +76,11 @@ beforeEach(() => { root = createRoot(host); }); -afterEach(() => { +afterEach(async () => { act(() => root.unmount()); + // Release every pending fake host response, including when an assertion fails. + for (const request of requests) request.result.resolve(resolvedJava("C:/fixture/cleanup")); + await Promise.allSettled(requests.map((request) => request.result.promise)); host.remove(); actEnvironment.IS_REACT_ACT_ENVIRONMENT = previousActEnvironment; restoreDom(); @@ -115,3 +118,16 @@ test("typing before the pause ends resolves only the final selection", () => { act(() => scheduled[1].task()); expect(requests.map((request) => request.javaHomePath)).toEqual(["C:/jdk"]); }); + + +test("project requirement refresh re-resolves unchanged automatic paths and rejects stale results", async () => { + act(() => root.render()); + act(() => scheduled[0].task()); + act(() => root.render()); + expect(scheduled[0].cancelled).toBe(true); + act(() => scheduled[1].task()); + expect(requests.map((request) => request.javaHomePath)).toEqual(["", ""]); + await act(async () => requests[1].result.resolve(resolvedJava("C:/jdk-21"))); + await act(async () => requests[0].result.resolve(resolvedJava("C:/jdk-8"))); + expect(host.textContent).toBe("C:/jdk-21"); +}); diff --git a/windows/tauri/src/features/run/hooks/use-resolved-toolchains.ts b/windows/tauri/src/features/run/hooks/use-resolved-toolchains.ts index bcd60f053..c26567271 100644 --- a/windows/tauri/src/features/run/hooks/use-resolved-toolchains.ts +++ b/windows/tauri/src/features/run/hooks/use-resolved-toolchains.ts @@ -42,6 +42,8 @@ export function useResolvedToolchains( mavenExecutablePath: string, mavenJavaHomePath: string, dependencies: ResolvedToolchainDependencies = defaultDependencies, + /** Changes when project discovery/requirements are refreshed, even if paths stay automatic. */ + projectRevision: unknown = undefined, ): ResolvedToolchainStates { const [state, setState] = useState(DETECTING); useEffect(() => { @@ -64,6 +66,6 @@ export function useResolvedToolchains( current = false; cancel(); }; - }, [root, javaHomePath, mavenExecutablePath, mavenJavaHomePath, dependencies]); + }, [root, javaHomePath, mavenExecutablePath, mavenJavaHomePath, dependencies, projectRevision]); return state; } diff --git a/windows/tauri/src/features/settings/components/project-environment-settings.tsx b/windows/tauri/src/features/settings/components/project-environment-settings.tsx index 6625ec15c..6c5878211 100644 --- a/windows/tauri/src/features/settings/components/project-environment-settings.tsx +++ b/windows/tauri/src/features/settings/components/project-environment-settings.tsx @@ -144,12 +144,17 @@ function ProjectEnvironmentForm({ root, workspaceId }: { root: string; workspace ); }; + // A project refresh may change minimum Java requirements without changing + // the draft's empty automatic paths. Re-resolve the displayed choice too. + const discoveredJava = useRunStore((state) => state.discoveredJava); // Resolve the draft, so the line under each field shows what saving it would launch. const resolved = useResolvedToolchains( environment ? root : null, environment?.toolchain.javaHomePath ?? "", environment?.toolchain.mavenExecutablePath ?? "", environment?.toolchain.mavenJavaHomePath ?? "", + undefined, + discoveredJava, ); // Core checks the saved defaults against the project's requirements. const diagnostics = useRunStore((state) => From 8844274ce2af355405dd46234cd21671a40ed177 Mon Sep 17 00:00:00 2001 From: chdtiger <43467701+chdtiger@users.noreply.github.com> Date: Wed, 30 Sep 2026 23:54:33 +0800 Subject: [PATCH 02/12] =?UTF-8?q?test(macos):=20=E8=A1=A5=E9=BD=90=20Debug?= =?UTF-8?q?ModuleTests=20=E7=83=AD=E6=9B=B4=E6=96=B0=E9=87=8D=E5=90=AF?= =?UTF-8?q?=E7=94=A8=E4=BE=8B=E9=81=97=E6=BC=8F=E7=9A=84=20projectName=20(?= =?UTF-8?q?#998)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #994 给 JavaDebugLaunchTarget.init 增加了必填的 projectName,但漏改 hotUpdateRestartKeepsOtherDebugSessionsAlive(DebugModuleTests.swift:45-46), 导致 macos/Tests/LitheDebugModuleTests 无法编译,Swift tests 车道自 4c5e137 起持续失败。 取值为同一文件内相邻用例 explicitHotUpdateRestart... 已有的 projectName: "app" 写法。 验证:swift build --build-tests 通过; ./scripts/test-macos.sh --filter "DebugModuleTests|workbenchBackground" → 58 tests in 3 suites passed。 Co-authored-by: chdtiger <17791972621@163.com> Co-authored-by: Xiaoyumuxi <3075514079@qq.com> From 5f5bbcaa94eb59efb4b3bf81f39bf74c453da6e6 Mon Sep 17 00:00:00 2001 From: Lichenkang <2188718831@qq.com> Date: Thu, 1 Oct 2026 13:50:48 +0800 Subject: [PATCH 03/12] =?UTF-8?q?fix(macos):=20=E4=BF=AE=E5=A4=8D=E4=B8=AD?= =?UTF-8?q?=E6=96=87=E7=BB=84=E5=90=88=E8=BE=93=E5=85=A5=E5=B1=82=E9=80=8F?= =?UTF-8?q?=E5=87=BA=E5=8E=9F=E6=96=87=E5=AD=97=E7=9A=84=E9=87=8D=E5=BD=B1?= =?UTF-8?q?=20(#1010)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(macos): prevent translucent IME overlay text ghosting * test(macos): verify IME styles through production entrypoint --- ...26-09-15-macos-monaco-feasibility-probe.md | 11 ++++ docs/development/platform-parity-matrix.csv | 1 + docs/development/platform-parity-matrix.md | 9 +-- macos/EditorFrontend/ime-input.css | 10 ++++ macos/EditorFrontend/main.ts | 1 + .../Monaco/ime-input.integration.ts | 55 +++++++++++++++++++ .../Monaco/workbench.integration.ts | 2 + shared/platform-feature-matrix.json | 25 +++++++++ 8 files changed, 110 insertions(+), 4 deletions(-) create mode 100644 macos/EditorFrontend/ime-input.css create mode 100644 macos/Experiments/Monaco/ime-input.integration.ts diff --git a/.agents/notes/implemented/architecture/2026-09-15-macos-monaco-feasibility-probe.md b/.agents/notes/implemented/architecture/2026-09-15-macos-monaco-feasibility-probe.md index 7ba3c5ee0..fa6fcb2ee 100644 --- a/.agents/notes/implemented/architecture/2026-09-15-macos-monaco-feasibility-probe.md +++ b/.agents/notes/implemented/architecture/2026-09-15-macos-monaco-feasibility-probe.md @@ -374,6 +374,17 @@ Monaco 视图。语法 token 仍使用统一的 `color-mappings.json` 与共享 Monaco 会给未指定背景的概览标尺填入默认分词背景色,在壁纸模式下形成黑色竖条; 显式传入透明色后,概览标尺与编辑区一起露出工作台图片。 +中文输入法的组合输入(候选文字尚未确认的阶段)有一个例外:Monaco 会把 +当前行的一部分放入临时文本输入框,覆盖原来的代码行。Monaco 0.55.1 默认把 +当前行高亮色直接用作输入框底色,而原生配色的高亮只有 3.5% 不透明度,下面 +的字形就会透出并形成重影。macOS 的 `ime-input.css` 仅在组合输入期间把高亮、 +编辑器底色和不透明的主题浮层底色叠合,保证临时输入框遮住原文字。背景图模式 +也只在这个临时矩形内使用实体底色;结束组合输入后恢复原有透明输入框。 +不要通过隐藏输入框文字、改写候选输入事件或强制定时重绘来遮掩问题,否则会 +影响输入法反馈或输入状态。真实 Monaco 的组合事件测试验证明暗主题、有无壁纸 +及输入框退出后的样式恢复;原生输入法候选、删除和 WebKit 实际绘制仍需 macOS +实机验证。 + ### 只读差异视图 维护者验收后不接受 macOS 的 Monaco 默认 diff 外观,因此 macOS 历史记录、分支比较、 diff --git a/docs/development/platform-parity-matrix.csv b/docs/development/platform-parity-matrix.csv index e208969af..fe59882aa 100644 --- a/docs/development/platform-parity-matrix.csv +++ b/docs/development/platform-parity-matrix.csv @@ -21,6 +21,7 @@ workspace-tabs-projects,工作区,工作区生命周期,多项目与多标签, editor-text-tabs,编辑器,文本编辑,文本编辑与标签生命周期,已实现,待验证,已实现,待验证,Editor,打开、编辑、保存、关闭和恢复多个文本文件,确认光标、脏状态和标签状态;在 macOS 打开无扩展名文本文件,确认标签与项目树图标一致;从外部改为二进制后折叠并展开项目树,确认两处图标更新一致。,,macos/Sources/Lithe/Views/Editor; macos/Sources/Lithe/Models/Editor,windows/tauri/src/features/editor; windows/tauri/src/features/tabs editor-pointer-selection,编辑器,文本编辑,鼠标与触控板点击、选词和拖选,已实现,待验证,已实现,待验证,Editor,macOS 分别启用微信输入法和系统 ABC,快速轻点不同位置后松手移动,确认不扩展选区、不移动文字;验证同位置双击选词、Shift 点击扩选、按住拖选、选中文字拖放、主编辑器和分屏及 diff。运行 scripts/probe-macos-monaco.sh --workbench-tests;Windows 验证同样的正常鼠标交互。,macOS 在宿主修正 WebKit 零按钮按下事件,保留 Monaco 选择语义。Linux Chromium 真实 Monaco 控制事件回放通过,仍待 macOS 微信输入法与 Windows WebView2 实机验证。,macos/EditorFrontend/main.ts; macos/EditorFrontend/mouse-input.ts; macos/Experiments/Monaco/mouse-input.integration.ts,windows/tauri/src/features/editor/components/monaco-editor.tsx editor-language-basics,编辑器,文本编辑,语法高亮与编辑器模型,已实现,待验证,已实现,待验证,Editor,分别打开 Java、Markdown 和普通文本,确认语言识别、语法高亮和模型切换;开启缩略图并滚动,确认滚动条轨道不透出编辑器代码。,,macos/Sources/Lithe/Views/Editor; macos/Sources/Lithe/Models/Editor,windows/tauri/src/features/editor; frontend/editor; windows/tauri/src/features/editor/engines/monaco/theme.ts +editor-macos-ime-presentation,编辑器,文本编辑,macOS 中文组合输入层遮住原文字,输入和删除候选文字时不透出整行重影,已实现,待验证,平台专属,不适用,Editor,运行 scripts/probe-macos-monaco.sh --workbench-tests。在 macOS 使用系统拼音与微信输入法,在含中文和英文的同一行中间输入、删减候选拼音、确认上屏,再按删除键;检查整行无重影、候选与光标正常、撤销恢复文本。覆盖明暗主题、背景图开关和分屏,结束输入后背景恢复。,修正 macOS 原生主题半透明当前行底色作为 Monaco 输入法覆盖层底色时的字形透出。Linux Chromium 的真实 Monaco 组合事件样式回归通过;macOS 原生输入法及删除键实机验证待完成。Windows 不加载此 macOS 样式。,macos/EditorFrontend/ime-input.css; macos/EditorFrontend/main.ts; macos/Experiments/Monaco/ime-input.integration.ts,windows/tauri/src/features/editor/components/monaco-editor.tsx editor-wallpaper-overview-ruler,编辑器,编辑器外观,macOS 工作台壁纸模式下,编辑器概览标尺与代码缩略图随编辑器背景透明,不显示黑色竖条;普通主题仍保持实体背景,已实现,待验证,平台专属,不适用,Editor,在 macOS 选择工作台背景并打开文本文件,分别关闭和打开右侧 Agent 面板,确认编辑器最右侧概览标尺不出现黑色竖条;关闭背景图后确认普通主题底色保持不变。运行真实 WebKit 工作台主题集成测试。Windows 不使用 macOS 原生工作台壁纸。,,frontend/editor/src/theme.ts; macos/Sources/Lithe/Platform/MacOS/MonacoWorkbenchEditor.swift; macos/Experiments/Monaco/workbench.integration.ts,windows/tauri/src/features/editor/engines/monaco/theme.ts editor-semantic-highlighting,编辑器,文本编辑,LSP 语义高亮,已实现,待验证,已实现,待验证,Editor / Language Tooling,打开普通 Java 文件并等待 JDTLS 就绪,开启语义高亮,确认字段、方法等按协商图例上色;编辑、切换、关闭文档和重连服务器后不应用旧结果;服务器 refresh 后重新请求;关闭设置或大文件降级时保留基础语法高亮。,Windows 复用 Core semanticTokens 和共享 fixture,修复 #675;Linux 上的适配器与模型测试不替代 Windows WebView2/JDTLS 实机验收。,macos/Sources/Lithe/Models/AppModel/AppModel+LanguageEditing.swift; macos/Sources/LitheLanguageIntelligenceModule/Runtime/LanguageServerSession.swift; frontend/editor/src/semantic-tokens.ts,windows/tauri/src/platform/lsp-core-adapter.ts; windows/tauri/src/features/editor/lsp/lsp-client.ts; windows/tauri/src/features/editor/engines/monaco/semantic-token-provider.ts; windows/tauri/src/features/editor/engines/monaco/semantic-token-provider.test.ts; windows/tauri/src/platform/lsp-core-adapter.test.ts editor-multiline-tabs,编辑器,文本编辑,多行标签与标签导航,已实现,待验证,已实现,待验证,Editor,打开足够多文件触发多行标签,确认滚动、切换、关闭和活动文件保持。,,macos/Sources/Lithe/Views/Editor,windows/tauri/src/features/tabs diff --git a/docs/development/platform-parity-matrix.md b/docs/development/platform-parity-matrix.md index 80d8ef0d0..3964f5fce 100644 --- a/docs/development/platform-parity-matrix.md +++ b/docs/development/platform-parity-matrix.md @@ -4,9 +4,9 @@ - 最后复核:2026-09-29 - 盘点状态:initial-static-inventory(根据 macOS Views/Application/Services、Windows features/extensions 和共享契约的代码入口进行初版盘点;未替代真实运行验收。) -- 功能项:115 -- macOS:实现:✅ 101 已实现,🟡 3 部分实现,❌ 7 未实现,🧩 4 平台专属;验证:✔️ 0 已验证,🔍 104 待验证,— 11 不适用 -- Windows:实现:✅ 98 已实现,🟡 11 部分实现,❌ 5 未实现,🧩 1 平台专属;验证:✔️ 0 已验证,🔍 109 待验证,— 6 不适用 +- 功能项:116 +- macOS:实现:✅ 102 已实现,🟡 3 部分实现,❌ 7 未实现,🧩 4 平台专属;验证:✔️ 0 已验证,🔍 105 待验证,— 11 不适用 +- Windows:实现:✅ 98 已实现,🟡 11 部分实现,❌ 5 未实现,🧩 2 平台专属;验证:✔️ 0 已验证,🔍 109 待验证,— 7 不适用 ## 实现状态定义 @@ -72,13 +72,14 @@

-编辑器 · 15 个能力点 +编辑器 · 16 个能力点 | 功能组 | 能力点 | macOS | Windows | 负责人 | 验证方式 | 备注 | | --- | --- | --- | --- | --- | --- | --- | | 文本编辑 | **文本编辑与标签生命周期**
editor-text-tabs | ✅ 已实现
🔍 待验证
`macos/Sources/Lithe/Views/Editor`、`macos/Sources/Lithe/Models/Editor` | ✅ 已实现
🔍 待验证
`windows/tauri/src/features/editor`、`windows/tauri/src/features/tabs` | Editor | 打开、编辑、保存、关闭和恢复多个文本文件,确认光标、脏状态和标签状态;在 macOS 打开无扩展名文本文件,确认标签与项目树图标一致;从外部改为二进制后折叠并展开项目树,确认两处图标更新一致。 | | | 文本编辑 | **鼠标与触控板点击、选词和拖选**
editor-pointer-selection | ✅ 已实现
🔍 待验证
`macos/EditorFrontend/main.ts`、`macos/EditorFrontend/mouse-input.ts`、`macos/Experiments/Monaco/mouse-input.integration.ts` | ✅ 已实现
🔍 待验证
`windows/tauri/src/features/editor/components/monaco-editor.tsx` | Editor | macOS 分别启用微信输入法和系统 ABC,快速轻点不同位置后松手移动,确认不扩展选区、不移动文字;验证同位置双击选词、Shift 点击扩选、按住拖选、选中文字拖放、主编辑器和分屏及 diff。运行 scripts/probe-macos-monaco.sh --workbench-tests;Windows 验证同样的正常鼠标交互。 | macOS 在宿主修正 WebKit 零按钮按下事件,保留 Monaco 选择语义。Linux Chromium 真实 Monaco 控制事件回放通过,仍待 macOS 微信输入法与 Windows WebView2 实机验证。 | | 文本编辑 | **语法高亮与编辑器模型**
editor-language-basics | ✅ 已实现
🔍 待验证
`macos/Sources/Lithe/Views/Editor`、`macos/Sources/Lithe/Models/Editor` | ✅ 已实现
🔍 待验证
`windows/tauri/src/features/editor`、`frontend/editor`、`windows/tauri/src/features/editor/engines/monaco/theme.ts` | Editor | 分别打开 Java、Markdown 和普通文本,确认语言识别、语法高亮和模型切换;开启缩略图并滚动,确认滚动条轨道不透出编辑器代码。 | | +| 文本编辑 | **macOS 中文组合输入层遮住原文字,输入和删除候选文字时不透出整行重影**
editor-macos-ime-presentation | ✅ 已实现
🔍 待验证
`macos/EditorFrontend/ime-input.css`、`macos/EditorFrontend/main.ts`、`macos/Experiments/Monaco/ime-input.integration.ts` | 🧩 平台专属
— 不适用
`windows/tauri/src/features/editor/components/monaco-editor.tsx` | Editor | 运行 scripts/probe-macos-monaco.sh --workbench-tests。在 macOS 使用系统拼音与微信输入法,在含中文和英文的同一行中间输入、删减候选拼音、确认上屏,再按删除键;检查整行无重影、候选与光标正常、撤销恢复文本。覆盖明暗主题、背景图开关和分屏,结束输入后背景恢复。 | 修正 macOS 原生主题半透明当前行底色作为 Monaco 输入法覆盖层底色时的字形透出。Linux Chromium 的真实 Monaco 组合事件样式回归通过;macOS 原生输入法及删除键实机验证待完成。Windows 不加载此 macOS 样式。 | | 编辑器外观 | **macOS 工作台壁纸模式下,编辑器概览标尺与代码缩略图随编辑器背景透明,不显示黑色竖条;普通主题仍保持实体背景**
editor-wallpaper-overview-ruler | ✅ 已实现
🔍 待验证
`frontend/editor/src/theme.ts`、`macos/Sources/Lithe/Platform/MacOS/MonacoWorkbenchEditor.swift`、`macos/Experiments/Monaco/workbench.integration.ts` | 🧩 平台专属
— 不适用
`windows/tauri/src/features/editor/engines/monaco/theme.ts` | Editor | 在 macOS 选择工作台背景并打开文本文件,分别关闭和打开右侧 Agent 面板,确认编辑器最右侧概览标尺不出现黑色竖条;关闭背景图后确认普通主题底色保持不变。运行真实 WebKit 工作台主题集成测试。Windows 不使用 macOS 原生工作台壁纸。 | | | 文本编辑 | **LSP 语义高亮**
editor-semantic-highlighting | ✅ 已实现
🔍 待验证
`macos/Sources/Lithe/Models/AppModel/AppModel+LanguageEditing.swift`、`macos/Sources/LitheLanguageIntelligenceModule/Runtime/LanguageServerSession.swift`、`frontend/editor/src/semantic-tokens.ts` | ✅ 已实现
🔍 待验证
`windows/tauri/src/platform/lsp-core-adapter.ts`、`windows/tauri/src/features/editor/lsp/lsp-client.ts`、`windows/tauri/src/features/editor/engines/monaco/semantic-token-provider.ts`、`windows/tauri/src/features/editor/engines/monaco/semantic-token-provider.test.ts`、`windows/tauri/src/platform/lsp-core-adapter.test.ts` | Editor / Language Tooling | 打开普通 Java 文件并等待 JDTLS 就绪,开启语义高亮,确认字段、方法等按协商图例上色;编辑、切换、关闭文档和重连服务器后不应用旧结果;服务器 refresh 后重新请求;关闭设置或大文件降级时保留基础语法高亮。 | Windows 复用 Core semanticTokens 和共享 fixture,修复 #675;Linux 上的适配器与模型测试不替代 Windows WebView2/JDTLS 实机验收。 | | 文本编辑 | **多行标签与标签导航**
editor-multiline-tabs | ✅ 已实现
🔍 待验证
`macos/Sources/Lithe/Views/Editor` | ✅ 已实现
🔍 待验证
`windows/tauri/src/features/tabs` | Editor | 打开足够多文件触发多行标签,确认滚动、切换、关闭和活动文件保持。 | | diff --git a/macos/EditorFrontend/ime-input.css b/macos/EditorFrontend/ime-input.css new file mode 100644 index 000000000..76546fadf --- /dev/null +++ b/macos/EditorFrontend/ime-input.css @@ -0,0 +1,10 @@ +/* Monaco draws the composing line in a textarea over the rendered document. + Its default IME background uses our translucent current-line highlight alone, + which lets the old glyphs show through. Composite the highlight over the editor + surface; the opaque widget surface also covers glyphs in wallpaper mode. */ +.monaco-editor .inputarea.ime-input { + background-color: var(--vscode-editorWidget-background) !important; + background-image: + linear-gradient(var(--vscode-editor-lineHighlightBackground, transparent), var(--vscode-editor-lineHighlightBackground, transparent)), + linear-gradient(var(--vscode-editor-background), var(--vscode-editor-background)); +} diff --git a/macos/EditorFrontend/main.ts b/macos/EditorFrontend/main.ts index 1b0dcbafa..34877fe04 100644 --- a/macos/EditorFrontend/main.ts +++ b/macos/EditorFrontend/main.ts @@ -10,6 +10,7 @@ import { mountWorkbench } from "@lithe/editor/workbench"; import { KeyCode, KeyMod } from "monaco-editor/esm/vs/editor/editor.api.js"; import palette from "../Sources/Lithe/Resources/SyntaxHighlighting/color-mappings.json"; import { installWebKitMouseInput } from "./mouse-input"; +import "./ime-input.css"; declare global { interface Window { webkit: any; MonacoEnvironment: any; lithe: any; } } diff --git a/macos/Experiments/Monaco/ime-input.integration.ts b/macos/Experiments/Monaco/ime-input.integration.ts new file mode 100644 index 000000000..1cc2817ba --- /dev/null +++ b/macos/Experiments/Monaco/ime-input.integration.ts @@ -0,0 +1,55 @@ +import { editor as monacoEditor } from "monaco-editor/esm/vs/editor/editor.api.js"; + +const assert = (condition: unknown, message: string) => { if (!condition) throw new Error(message); }; + +// These cases exercise Monaco's real composition presentation. Native candidate +// selection and WebKit repaint timing still require the macOS input-method run. +// Load styles through the production workbench entrypoint, not this test module, +// so a missing production CSS import cannot be masked by the regression test. +export const imeInputCases = [false, true].flatMap(dark => [false, true].map(wallpaper => ({ + name: `IME overlay covers old glyphs in ${dark ? "dark" : "light"} ${wallpaper ? "wallpaper" : "solid"} theme`, + run() { + const container = document.createElement("div"); + container.style.cssText = "position:absolute;inset:0;width:800px;height:300px"; + document.body.append(container); + const theme = `ime-${dark}-${wallpaper}`; + monacoEditor.defineTheme(theme, { + base: dark ? "vs-dark" : "vs", inherit: true, rules: [], + colors: { + "editor.background": wallpaper ? "#00000000" : dark ? "#202020" : "#ffffff", + "editor.lineHighlightBackground": dark ? "#ffffff09" : "#00000009", + }, + }); + const model = monacoEditor.createModel("已有中文 abc", "plaintext"); + const view = monacoEditor.create(container, { model, theme, editContext: false }); + try { + view.setPosition({ lineNumber: 1, column: 3 }); + view.focus(); + view.render(true); + const input = container.querySelector("textarea.inputarea")!; + assert(input, "Monaco did not create its textarea input"); + const idleBackground = getComputedStyle(input).backgroundColor; + for (const candidate of ["zhong", "zhon", "中"]) { + input.dispatchEvent(new CompositionEvent("compositionstart", { bubbles: true, data: "" })); + input.dispatchEvent(new CompositionEvent("compositionupdate", { bubbles: true, data: candidate })); + assert(input.classList.contains("ime-input"), "composition did not show the input overlay"); + const style = getComputedStyle(input); + const canvas = document.createElement("canvas"); + const context = canvas.getContext("2d")!; + context.fillStyle = style.backgroundColor; + context.fillRect(0, 0, 1, 1); + assert(context.getImageData(0, 0, 1, 1).data[3] === 255, + `composition background exposes old line glyphs: ${style.backgroundColor}`); + assert(style.backgroundImage.includes("linear-gradient"), "composition lost the theme surface layers"); + input.dispatchEvent(new CompositionEvent("compositionend", { bubbles: true, data: candidate })); + assert(!input.classList.contains("ime-input"), "composition overlay remained after commit"); + assert(getComputedStyle(input).backgroundColor === idleBackground, "IME surface leaked into ordinary input"); + } + } finally { + view.dispose(); + model.dispose(); + container.remove(); + monacoEditor.setTheme("lithe-dark"); + } + }, +}))); diff --git a/macos/Experiments/Monaco/workbench.integration.ts b/macos/Experiments/Monaco/workbench.integration.ts index c56e14927..4d04ba657 100644 --- a/macos/Experiments/Monaco/workbench.integration.ts +++ b/macos/Experiments/Monaco/workbench.integration.ts @@ -13,6 +13,7 @@ import { ready } from "./workbench"; import { acquireEditorModelSource, sourcePositionAt } from "@lithe/editor/model-source"; import { editor as monacoEditor, languages, Range, Selection, Uri } from "monaco-editor/esm/vs/editor/editor.api.js"; import { mouseInputCases } from "./mouse-input.integration"; +import { imeInputCases } from "./ime-input.integration"; // Real WebKit integration, using the exact workbench bundle and the existing // bounded native probe host. No DOM-based imitation of Monaco input. @@ -33,6 +34,7 @@ async function verify() { cases.push({ name, durationMs: performance.now() - started }); } for (const test of mouseInputCases) await check(test.name, async () => test.run()); + for (const test of imeInputCases) await check(test.name, async () => test.run()); await check("diff projections preserve sparse source lines and release read-only models", async () => { const before = monacoEditor.getModels().length; const container = document.createElement("div"); diff --git a/shared/platform-feature-matrix.json b/shared/platform-feature-matrix.json index 3cd1e2042..fc870cfd9 100644 --- a/shared/platform-feature-matrix.json +++ b/shared/platform-feature-matrix.json @@ -627,6 +627,31 @@ "owner": "Editor", "verification": "分别打开 Java、Markdown 和普通文本,确认语言识别、语法高亮和模型切换;开启缩略图并滚动,确认滚动条轨道不透出编辑器代码。" }, + { + "id": "editor-macos-ime-presentation", + "area": "编辑器", + "group": "文本编辑", + "capability": "macOS 中文组合输入层遮住原文字,输入和删除候选文字时不透出整行重影", + "macos": { + "implementationStatus": "implemented", + "verificationStatus": "pending", + "evidence": [ + "macos/EditorFrontend/ime-input.css", + "macos/EditorFrontend/main.ts", + "macos/Experiments/Monaco/ime-input.integration.ts" + ] + }, + "windows": { + "implementationStatus": "platform-specific", + "verificationStatus": "not-applicable", + "evidence": [ + "windows/tauri/src/features/editor/components/monaco-editor.tsx" + ] + }, + "owner": "Editor", + "notes": "修正 macOS 原生主题半透明当前行底色作为 Monaco 输入法覆盖层底色时的字形透出。Linux Chromium 的真实 Monaco 组合事件样式回归通过;macOS 原生输入法及删除键实机验证待完成。Windows 不加载此 macOS 样式。", + "verification": "运行 scripts/probe-macos-monaco.sh --workbench-tests。在 macOS 使用系统拼音与微信输入法,在含中文和英文的同一行中间输入、删减候选拼音、确认上屏,再按删除键;检查整行无重影、候选与光标正常、撤销恢复文本。覆盖明暗主题、背景图开关和分屏,结束输入后背景恢复。" + }, { "id": "editor-wallpaper-overview-ruler", "area": "编辑器", From 7cb138b907990ab5217c191780134ea15aeb30ec Mon Sep 17 00:00:00 2001 From: Lichenkang <2188718831@qq.com> Date: Thu, 1 Oct 2026 13:51:22 +0800 Subject: [PATCH 04/12] =?UTF-8?q?fix(windows):=20=E5=8D=95=E6=96=87?= =?UTF-8?q?=E4=BB=B6=E5=B7=A5=E4=BD=9C=E5=8C=BA=20Diff=20=E5=8F=AF?= =?UTF-8?q?=E5=B1=95=E5=BC=80=E7=9C=81=E7=95=A5=E7=9A=84=E4=BB=A3=E7=A0=81?= =?UTF-8?q?=E8=A1=8C=20(#557)=20(#1009)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(windows): expand omitted lines in working-tree file diffs (#557) Single-file working-tree diffs were rendered from a sparse Git patch with Monaco folding disabled, so there was nothing to expand and the fold controls disappeared. - Request the whole file as context for single-file reviews and mark the result as full context; only those reviews enable folding. - Split full-context patches back into Git-equivalent 3-line hunks so hunk staging keeps its granularity. - Place hunk action bands by an explicit row anchor and per-side line count, keeping both sides aligned. - Re-attach the diff models when the text is replaced so the first real text after the empty mount folds instead of opening fully revealed. Co-Authored-By: Claude * test(windows): route full-context diff responses by command The new full-context API tests queued their response with mockImplementationOnce, which assumed git_diff_file was the next invoke. Under CI's randomized order, repository discovery is sometimes not yet cached, so git_discover_repo consumed the queued response and the diff returned null (seed=1). Answer git_diff_file by command instead. Co-Authored-By: Claude --------- Co-authored-by: Claude --- ...26-09-15-macos-monaco-feasibility-probe.md | 17 +++ docs/development/platform-parity-matrix.csv | 2 +- docs/development/platform-parity-matrix.md | 2 +- frontend/editor/src/diff-review.ts | 27 ++++- .../Monaco/workbench.integration.ts | 39 +++++++ rust/lithe-core/src/tests/git.rs | 61 +++++++++++ shared/platform-feature-matrix.json | 2 +- windows/tauri/src-tauri/src/platform.rs | 27 +++++ .../src/features/git/api/git-diff-api.ts | 51 ++++++++- .../features/git/api/git-status-api.test.ts | 37 ++++++- .../git/components/diff/monaco-git-diff.tsx | 7 +- .../git/hooks/use-git-diff-actions.ts | 7 +- .../git/services/working-tree-diff-refresh.ts | 6 +- .../tauri/src/features/git/types/git.types.ts | 5 + .../git/utils/monaco-diff-rows.test.ts | 84 ++++++++++++++- .../features/git/utils/monaco-diff-rows.ts | 102 +++++++++++++++++- .../sidebar/utils/open-sidebar-resource.ts | 4 +- 17 files changed, 454 insertions(+), 26 deletions(-) diff --git a/.agents/notes/implemented/architecture/2026-09-15-macos-monaco-feasibility-probe.md b/.agents/notes/implemented/architecture/2026-09-15-macos-monaco-feasibility-probe.md index fa6fcb2ee..966c0ab29 100644 --- a/.agents/notes/implemented/architecture/2026-09-15-macos-monaco-feasibility-probe.md +++ b/.agents/notes/implemented/architecture/2026-09-15-macos-monaco-feasibility-probe.md @@ -423,6 +423,23 @@ working-tree review 的显式身份,不回退到可能已经切换的工作区 错误通过现有本地化提示呈现。纯逻辑回归覆盖稀疏分块、两种操作及这些生命周期边界, 并纳入 Windows Git 随机顺序 CI;真实 WebView2 的按钮与 Git 状态联动仍需宿主验收。 +Windows 单文件工作区 Diff 还要能展开省略的未改动代码(#557)。稀疏补丁里根本 +没有这些行,所以“展开”无从谈起;之前强行关闭折叠,结果连展开按钮都不显示。 +现在单文件打开和刷新时请求整份文件作为上下文(`contextLines` 取 Git 接受的 +最大值 2147483647),结果标记为完整上下文(`is_full_context`),只有这种补丁 +才开启 Monaco 的未改动区域折叠。多文件、历史和提交比较仍用稀疏补丁且不折叠, +因为它们缺少的行同样无法展示。 + +整份文件只有一个 `@@`,直接拿来暂存会把整个文件一次写进索引。因此 Windows +适配器按 Git 默认的 3 行上下文重新切出分块,自己生成 `@@` 范围;单测用同一次 +修改的真实 `git diff` 输出逐块比对。完整上下文没有 `@@` 行,渲染器改用行上的 +`actionAnchor` 标记放置按钮,并按每一侧实际显示的行数计算位置,左右两侧保持对齐。 + +Monaco 会把旧的折叠状态带到新内容上:挂载时的空模型没有未改动区域,第一份 +真实文本的所有区域都会被当作“已展开”。渲染器在文本被替换时重新挂接模型, +让折叠状态从头计算;文本相同的刷新不重挂,读者已展开的区域保留。 +真实 WebKit 回归覆盖首次折叠、单个按钮和搜索揭示,去掉重挂会失败。 + 预览借用主编辑器时还保存查找栏的查询、选项和显隐状态。关闭预览必须恢复 主编辑器原先的查找状态,不能把替换预览的查询栏留在原文件中。 diff --git a/docs/development/platform-parity-matrix.csv b/docs/development/platform-parity-matrix.csv index fe59882aa..767f5eb16 100644 --- a/docs/development/platform-parity-matrix.csv +++ b/docs/development/platform-parity-matrix.csv @@ -32,7 +32,7 @@ search-replace,搜索,搜索与导航,项目替换与取消,已实现,待验证, git-status-commit,版本控制,Git,状态、暂存与提交,已实现,待验证,已实现,待验证,Git,修改、暂存、取消暂存并提交文件,确认状态、提交消息和错误回显。 Windows 另验证原生 UNC/verbatim 输入、中文/空格/长路径仓库往返及 linked worktree;末尾点/空格必须明确拒绝,外部提交/切换须触发元数据刷新。,,macos/Sources/Lithe/Views/Git; macos/Sources/Lithe/Services,windows/tauri/src/features/git; shared/contracts/application-boundary.md; windows/tauri/src/features/git/api/git-repository-path.ts; rust/lithe-core/tests/git_path_roundtrip.rs; shared/fixtures/git/windows-paths.json git-multi-repository-change-groups,版本控制,Git,多仓库变更折叠分组,已实现,待验证,已实现,待验证,Git,在同一工作区打开多个 Git 仓库,确认变更按仓库折叠分组;只剩一个仓库有变更时仍显示仓库名。仓库级和文件级勾选与 Git 暂存区同步;子模块只有未提交文件时显示提示,不能勾选未变化的引用。,两端直接消费共享 Rust Core 的计划、依赖排序、执行结果与重试。平台负责真实暂存勾选、确认与结果界面、认证取消和项目生命周期。原生界面实测待平台验收。,macos/Sources/Lithe/Views/Git/ChangesSidebarView.swift; macos/Sources/Lithe/Views/Git/GitChangeSectionsCache.swift; macos/Sources/LitheGitModule/Models/GitModels.swift; rust/lithe-core/src/git/mod.rs,windows/tauri/src/features/git/components/status/git-status-panel.tsx; windows/tauri/src/features/git/components/git-commit-panel.tsx; windows/tauri/src/features/git/components/git-workspace-commit-review.tsx; windows/tauri/src/features/git/services/git-workspace-commit-workflow.ts; windows/tauri/src/features/git/services/git-workspace-commit-workflow.test.ts; windows/tauri/src/features/git/components/status/git-workspace-status-panel.test.tsx; windows/tauri/src-tauri/src/platform.rs; shared/fixtures/git/workspace-commit-workflow-v1.json git-branches-remotes,版本控制,Git,分支、标签与远程,已实现,待验证,已实现,待验证,Git,创建、切换、合并分支并查看标签和远程,确认冲突与认证失败可恢复。,,macos/Sources/Lithe/Views/Git,windows/tauri/src/features/git -git-diff-review,版本控制,Git,Diff 与变更审查,已实现,待验证,已实现,待验证,Git,验证新增、删除、重命名、二进制和多文件 Diff 的展示与定位;从源代码管理打开已修改和未跟踪文件的工作区 Diff 后保持静止,确认 Diff 不会自动关闭,且只在文件不再出现在 Git 状态中时关闭。,,macos/Sources/Lithe/Views/Git; macos/Sources/Lithe/Views/Diff,windows/tauri/src/features/git; windows/tauri/src/features/viewer +git-diff-review,版本控制,Git,Diff 与变更审查,已实现,待验证,已实现,待验证,Git,验证新增、删除、重命名、二进制和多文件 Diff 的展示与定位;从源代码管理打开已修改和未跟踪文件的工作区 Diff 后保持静止,确认 Diff 不会自动关闭,且只在文件不再出现在 Git 状态中时关闭;在 Windows 单文件工作区 Diff 中点击折叠条展开省略的未改动代码,确认 Diff 保持打开且每个修改块仍可单独暂存。,,macos/Sources/Lithe/Views/Git; macos/Sources/Lithe/Views/Diff,windows/tauri/src/features/git; windows/tauri/src/features/viewer git-history,版本控制,Git,提交历史与图谱,已实现,待验证,已实现,待验证,Git,分页浏览提交历史、分支图谱和提交详情,确认日期、作者和文件列表一致;分支最新提交的图谱连线从提交圆点开始,不超出到圆点上方;在双端使用同一包含本地分支、远端引用、标签和合并提交的仓库,核对永久图布局、图头引用排序、合并边投影及跨越 30 行的紧凑长边;在本地分支新增提交后确认分支颜色保持稳定,且不会因共用屏幕泳道直接沿用父分支颜色。双端保留各自调色板,不要求 RGB 值一致。,,macos/Sources/Lithe/Views/Git; macos/Sources/LitheGitModule/Services/GitGraphLayoutService.swift; macos/Sources/LitheGitModule/Services/GitGraphHeadOrdering.swift; macos/Sources/Lithe/Views/Git/GitGraphColor.swift,windows/tauri/src/features/git; windows/tauri/src/features/git/utils/git-graph-layout.ts; windows/tauri/src/features/git/utils/git-graph-colors.ts; windows/tauri/src/features/git/utils/git-graph-layout.test.ts git-rebase-stash,版本控制,Git,Rebase 与 Stash,已实现,待验证,已实现,待验证,Git,执行交互式 Rebase 和 Stash 保存/恢复,确认中断、冲突和继续操作。,,macos/Sources/Lithe/Views/Git,windows/tauri/src/features/git git-worktrees,版本控制,Git,Worktree 管理,已实现,待验证,已实现,待验证,Git,列出、创建、切换和删除 Worktree,确认路径、分支和安全检查。,,macos/Sources/Lithe/Views/Git,windows/tauri/src/features/git diff --git a/docs/development/platform-parity-matrix.md b/docs/development/platform-parity-matrix.md index 3964f5fce..37a0ad01b 100644 --- a/docs/development/platform-parity-matrix.md +++ b/docs/development/platform-parity-matrix.md @@ -115,7 +115,7 @@ | Git | **状态、暂存与提交**
git-status-commit | ✅ 已实现
🔍 待验证
`macos/Sources/Lithe/Views/Git`、`macos/Sources/Lithe/Services` | ✅ 已实现
🔍 待验证
`windows/tauri/src/features/git`、`shared/contracts/application-boundary.md`、`windows/tauri/src/features/git/api/git-repository-path.ts`、`rust/lithe-core/tests/git_path_roundtrip.rs`、`shared/fixtures/git/windows-paths.json` | Git | 修改、暂存、取消暂存并提交文件,确认状态、提交消息和错误回显。 Windows 另验证原生 UNC/verbatim 输入、中文/空格/长路径仓库往返及 linked worktree;末尾点/空格必须明确拒绝,外部提交/切换须触发元数据刷新。 | | | Git | **多仓库变更折叠分组**
git-multi-repository-change-groups | ✅ 已实现
🔍 待验证
`macos/Sources/Lithe/Views/Git/ChangesSidebarView.swift`、`macos/Sources/Lithe/Views/Git/GitChangeSectionsCache.swift`、`macos/Sources/LitheGitModule/Models/GitModels.swift`、`rust/lithe-core/src/git/mod.rs` | ✅ 已实现
🔍 待验证
`windows/tauri/src/features/git/components/status/git-status-panel.tsx`、`windows/tauri/src/features/git/components/git-commit-panel.tsx`、`windows/tauri/src/features/git/components/git-workspace-commit-review.tsx`、`windows/tauri/src/features/git/services/git-workspace-commit-workflow.ts`、`windows/tauri/src/features/git/services/git-workspace-commit-workflow.test.ts`、`windows/tauri/src/features/git/components/status/git-workspace-status-panel.test.tsx`、`windows/tauri/src-tauri/src/platform.rs`、`shared/fixtures/git/workspace-commit-workflow-v1.json` | Git | 在同一工作区打开多个 Git 仓库,确认变更按仓库折叠分组;只剩一个仓库有变更时仍显示仓库名。仓库级和文件级勾选与 Git 暂存区同步;子模块只有未提交文件时显示提示,不能勾选未变化的引用。 | 两端直接消费共享 Rust Core 的计划、依赖排序、执行结果与重试。平台负责真实暂存勾选、确认与结果界面、认证取消和项目生命周期。原生界面实测待平台验收。 | | Git | **分支、标签与远程**
git-branches-remotes | ✅ 已实现
🔍 待验证
`macos/Sources/Lithe/Views/Git` | ✅ 已实现
🔍 待验证
`windows/tauri/src/features/git` | Git | 创建、切换、合并分支并查看标签和远程,确认冲突与认证失败可恢复。 | | -| Git | **Diff 与变更审查**
git-diff-review | ✅ 已实现
🔍 待验证
`macos/Sources/Lithe/Views/Git`、`macos/Sources/Lithe/Views/Diff` | ✅ 已实现
🔍 待验证
`windows/tauri/src/features/git`、`windows/tauri/src/features/viewer` | Git | 验证新增、删除、重命名、二进制和多文件 Diff 的展示与定位;从源代码管理打开已修改和未跟踪文件的工作区 Diff 后保持静止,确认 Diff 不会自动关闭,且只在文件不再出现在 Git 状态中时关闭。 | | +| Git | **Diff 与变更审查**
git-diff-review | ✅ 已实现
🔍 待验证
`macos/Sources/Lithe/Views/Git`、`macos/Sources/Lithe/Views/Diff` | ✅ 已实现
🔍 待验证
`windows/tauri/src/features/git`、`windows/tauri/src/features/viewer` | Git | 验证新增、删除、重命名、二进制和多文件 Diff 的展示与定位;从源代码管理打开已修改和未跟踪文件的工作区 Diff 后保持静止,确认 Diff 不会自动关闭,且只在文件不再出现在 Git 状态中时关闭;在 Windows 单文件工作区 Diff 中点击折叠条展开省略的未改动代码,确认 Diff 保持打开且每个修改块仍可单独暂存。 | | | Git | **提交历史与图谱**
git-history | ✅ 已实现
🔍 待验证
`macos/Sources/Lithe/Views/Git`、`macos/Sources/LitheGitModule/Services/GitGraphLayoutService.swift`、`macos/Sources/LitheGitModule/Services/GitGraphHeadOrdering.swift`、`macos/Sources/Lithe/Views/Git/GitGraphColor.swift` | ✅ 已实现
🔍 待验证
`windows/tauri/src/features/git`、`windows/tauri/src/features/git/utils/git-graph-layout.ts`、`windows/tauri/src/features/git/utils/git-graph-colors.ts`、`windows/tauri/src/features/git/utils/git-graph-layout.test.ts` | Git | 分页浏览提交历史、分支图谱和提交详情,确认日期、作者和文件列表一致;分支最新提交的图谱连线从提交圆点开始,不超出到圆点上方;在双端使用同一包含本地分支、远端引用、标签和合并提交的仓库,核对永久图布局、图头引用排序、合并边投影及跨越 30 行的紧凑长边;在本地分支新增提交后确认分支颜色保持稳定,且不会因共用屏幕泳道直接沿用父分支颜色。双端保留各自调色板,不要求 RGB 值一致。 | | | Git | **Rebase 与 Stash**
git-rebase-stash | ✅ 已实现
🔍 待验证
`macos/Sources/Lithe/Views/Git` | ✅ 已实现
🔍 待验证
`windows/tauri/src/features/git` | Git | 执行交互式 Rebase 和 Stash 保存/恢复,确认中断、冲突和继续操作。 | | | Git | **Worktree 管理**
git-worktrees | ✅ 已实现
🔍 待验证
`macos/Sources/Lithe/Views/Git` | ✅ 已实现
🔍 待验证
`windows/tauri/src/features/git` | Git | 列出、创建、切换和删除 Worktree,确认路径、分支和安全检查。 | | diff --git a/frontend/editor/src/diff-review.ts b/frontend/editor/src/diff-review.ts index 24d39249d..9ff1b198a 100644 --- a/frontend/editor/src/diff-review.ts +++ b/frontend/editor/src/diff-review.ts @@ -10,6 +10,9 @@ export interface ReviewRow { right: string | null; kind: "context" | "changed" | "addition" | "removal" | "information"; hunkID?: string | null; + /** Renders the hunk's action band above this row. Full-file reviews have no + * `@@` information rows, so the host marks each hunk's first change instead. */ + actionAnchor?: boolean; } export interface ReviewAction { id: string; title: string } export interface DiffReviewInput { @@ -136,8 +139,14 @@ export function mountDiffReview(container: HTMLElement, leftRows = old.rows; rightRows = next.rows; if (original.getLanguageId() !== input.language) monaco.editor.setModelLanguage(original, input.language); if (modified.getLanguageId() !== input.language) monaco.editor.setModelLanguage(modified, input.language); + const replaced = original.getValue() !== old.text || modified.getValue() !== next.text; if (original.getValue() !== old.text) original.setValue(old.text); if (modified.getValue() !== next.text) modified.setValue(next.text); + // Monaco carries fold state across content changes: regions that did not + // exist before, such as every region of the first real text after the + // empty mount, open fully revealed. Re-attaching the models starts a + // fresh diff view model whose unchanged regions fold, like a new review. + if (replaced) { editor.setModel(null); editor.setModel({ original, modified }); } left.updateOptions({ lineNumbers: line => String(leftRows[line - 1]?.oldLine ?? "") }); right.updateOptions({ lineNumbers: line => String(rightRows[line - 1]?.newLine ?? "") }); editor.updateOptions({ renderSideBySide: input.sideBySide ?? true, @@ -146,11 +155,19 @@ export function mountDiffReview(container: HTMLElement, container.classList.toggle("no-word-highlights", input.highlightWords === false); const actions = input.actions ?? []; if (actions.length) { - for (const [view, rows] of [[left, leftRows], [right, rightRows]] as const) { + for (const [view, side] of [[left, "left"], [right, "right"]] as const) { const ids: string[] = []; view.changeViewZones(accessor => { - rows.forEach((row, index) => { - if (row.kind !== "information" || !row.hunkID) return; + let projected = 0; + for (const row of input.rows) { + const visible = row[side] !== null; + // An `@@` row owns the band below it. An anchor row may be absent + // from this side (an addition on the left), so its band goes after + // the rows this side has already shown, keeping both sides level. + const afterLineNumber = row.hunkID && row.actionAnchor ? projected + : row.hunkID && row.kind === "information" && visible ? projected + 1 : null; + if (visible) projected++; + if (afterLineNumber === null) continue; const node = document.createElement("div"); node.className = "lithe-review-actions"; if (view === right) for (const action of actions) { @@ -159,8 +176,8 @@ export function mountDiffReview(container: HTMLElement, button.onclick = () => { if (!disposed && request === version) onAction(row.hunkID!, action.id); }; node.append(button); } - ids.push(accessor.addZone({ afterLineNumber: index + 1, heightInPx: 26, domNode: node })); - }); + ids.push(accessor.addZone({ afterLineNumber, heightInPx: 26, domNode: node })); + } }); zones.push({ view, ids }); } diff --git a/macos/Experiments/Monaco/workbench.integration.ts b/macos/Experiments/Monaco/workbench.integration.ts index 4d04ba657..5f82ffd40 100644 --- a/macos/Experiments/Monaco/workbench.integration.ts +++ b/macos/Experiments/Monaco/workbench.integration.ts @@ -117,6 +117,45 @@ async function verify() { subscription?.dispose(); review.dispose(); container.remove(); } }); + // #557: the first text after the empty mount used to open with every + // unchanged region revealed, so a full-file review showed nothing to expand. + await check("first full-file diff folds unchanged regions and anchors hunk actions", async () => { + const container = document.createElement("div"); + container.style.cssText = "position:absolute;inset:0;height:400px;width:1000px"; + document.body.append(container); + const review = mountDiffReview(container); + const rows: ReviewRow[] = Array.from({ length: 120 }, (_, index) => ({ + id: `full-${index}`, oldLine: index + 1, newLine: index + 1, + left: `value ${index}`, right: index === 4 ? "changed" : `value ${index}`, + kind: index === 4 ? "changed" : "context", hunkID: index < 8 ? "hunk-4" : null, + ...(index === 4 ? { actionAnchor: true } : {}), + })); + let subscription: { dispose(): void } | undefined; + let deadline: ReturnType | undefined; + try { + const computed = new Promise((resolve, reject) => { + deadline = setTimeout(() => reject(new Error("Full-file diff computation exceeded 5 seconds")), 5000); + subscription = review.editor.onDidUpdateDiff(() => { + if (review.editor.getLineChanges()?.length) resolve(); + }); + }); + await Promise.all([review.update({ rows, language: "plaintext", collapse: true, + actions: [{ id: "stage", title: "Stage" }] }), computed]); + const next = review.editor.getModifiedEditor(); + // Content height, unlike the viewport, shrinks only when lines are folded. + const lineHeight = next.getOption(monacoEditor.EditorOption.lineHeight); + assert(next.getContentHeight() < lineHeight * 40, + "unchanged region opened revealed instead of folded"); + assert(container.querySelectorAll(".lithe-review-actions button").length === 1, + "full-file hunk did not render exactly one action band"); + review.select({ revealID: "full-60", searchIDs: ["full-60"] }); + assert(next.getVisibleRanges().some(range => range.startLineNumber <= 61 && range.endLineNumber >= 61), + "folded source row could not be revealed"); + } finally { + if (deadline !== undefined) clearTimeout(deadline); + subscription?.dispose(); review.dispose(); container.remove(); + } + }); await check("inline diff navigation scrolls to a removed source row", async () => { const container = document.createElement("div"); container.style.cssText = "position:absolute;inset:0;height:400px;width:1000px"; diff --git a/rust/lithe-core/src/tests/git.rs b/rust/lithe-core/src/tests/git.rs index 31075dfec..3ab708542 100644 --- a/rust/lithe-core/src/tests/git.rs +++ b/rust/lithe-core/src/tests/git.rs @@ -2994,6 +2994,67 @@ fn git_diff_and_apply_round_trip_a_patch() { fs::remove_dir_all(root).expect("temporary workspace should be removable"); } +/// Windows single-file reviews ask for Git's largest context so the renderer +/// can reveal folded unchanged lines (#557). The snapshot diff must accept that +/// value and return the complete file as one hunk. +#[test] +fn git_snapshot_diff_returns_the_whole_file_for_maximum_context() { + let root = temporary_root("git-full-context-diff"); + fs::create_dir_all(&root).expect("temporary workspace should be creatable"); + let run = |arguments: &[&str]| { + Command::new("git") + .args(arguments) + .current_dir(&root) + .output() + .expect("git should be available") + }; + assert!(run(&["init", "-q"]).status.success()); + assert!(run(&["config", "core.autocrlf", "false"]).status.success()); + assert!(run(&["config", "user.email", "test@example.com"]) + .status + .success()); + assert!(run(&["config", "user.name", "Lithe Test"]).status.success()); + let original = (1..=200) + .map(|line| format!("line {line}\n")) + .collect::(); + fs::write(root.join("long.txt"), &original).expect("file should be writable"); + assert!(run(&["add", "long.txt"]).status.success()); + assert!(run(&["commit", "-qm", "initial"]).status.success()); + fs::write( + root.join("long.txt"), + original + .replace("line 10\n", "line ten\n") + .replace("line 190\n", "line one-ninety\n"), + ) + .expect("file should be writable"); + + let request = serde_json::json!({ + "id": "full-context-diff", + "command": "git.diff", + "payload": { + "root": root, + "pathspecs": ["long.txt"], + "worktreeSnapshot": true, + "contextLines": i32::MAX + } + }); + let response: Value = serde_json::from_str(&execute_json( + &serde_json::to_string(&request).expect("diff request should encode"), + )) + .expect("diff response should be JSON"); + assert_eq!(response["ok"], true, "{response}"); + let patch = response["data"]["patch"] + .as_str() + .expect("diff output should be text"); + assert!(patch.contains("@@ -1,200 +1,200 @@"), "{patch}"); + assert!( + patch.contains("\n line 100\n"), + "middle context missing: {patch}" + ); + assert_eq!(response["data"]["hunks"].as_array().unwrap().len(), 1); + fs::remove_dir_all(root).expect("temporary workspace should be removable"); +} + #[test] fn git_diff_resolves_the_empty_tree_for_a_sha256_repository() { let root = temporary_root("git-diff-sha256-empty-tree"); diff --git a/shared/platform-feature-matrix.json b/shared/platform-feature-matrix.json index fc870cfd9..08f610fe4 100644 --- a/shared/platform-feature-matrix.json +++ b/shared/platform-feature-matrix.json @@ -923,7 +923,7 @@ "verificationStatus": "pending" }, "owner": "Git", - "verification": "验证新增、删除、重命名、二进制和多文件 Diff 的展示与定位;从源代码管理打开已修改和未跟踪文件的工作区 Diff 后保持静止,确认 Diff 不会自动关闭,且只在文件不再出现在 Git 状态中时关闭。" + "verification": "验证新增、删除、重命名、二进制和多文件 Diff 的展示与定位;从源代码管理打开已修改和未跟踪文件的工作区 Diff 后保持静止,确认 Diff 不会自动关闭,且只在文件不再出现在 Git 状态中时关闭;在 Windows 单文件工作区 Diff 中点击折叠条展开省略的未改动代码,确认 Diff 保持打开且每个修改块仍可单独暂存。" }, { "id": "git-history", diff --git a/windows/tauri/src-tauri/src/platform.rs b/windows/tauri/src-tauri/src/platform.rs index f0dd8ffa5..6535b8f99 100644 --- a/windows/tauri/src-tauri/src/platform.rs +++ b/windows/tauri/src-tauri/src/platform.rs @@ -1328,6 +1328,33 @@ mod tests { ); } + #[test] + fn preserves_full_file_context_for_single_file_review() { + // #557: the review reveals folded lines locally, so Core must receive + // the whole-file context request unchanged. + let (command, payload) = translate( + "git_diff_file", + json!({ + "repoPath": "C:/work", + "filePath": "src/main.rs", + "worktreeSnapshot": true, + "contextLines": 2_147_483_647u32 + }), + ) + .unwrap(); + + assert_eq!(command, "git.diff"); + assert_eq!( + payload, + json!({ + "root": "C:/work", + "pathspecs": ["src/main.rs"], + "worktreeSnapshot": true, + "contextLines": 2_147_483_647u32 + }) + ); + } + #[test] fn translates_untracked_diff_file_pathspec() { let (command, payload) = translate( diff --git a/windows/tauri/src/features/git/api/git-diff-api.ts b/windows/tauri/src/features/git/api/git-diff-api.ts index 6e1561c7a..aa2bac9c6 100644 --- a/windows/tauri/src/features/git/api/git-diff-api.ts +++ b/windows/tauri/src/features/git/api/git-diff-api.ts @@ -238,11 +238,22 @@ export const getFileDiff = async ( } }; +/** + * Git's largest accepted `--unified` value. A single-file review asks for the + * whole file as context so omitted regions can be revealed without inventing + * source text; Git emits one hunk per file at this size. + */ +const FULL_FILE_CONTEXT_LINES = 2_147_483_647; + +const markFullContext = (diff: GitDiff | null): GitDiff | null => + diff ? { ...diff, is_full_context: true } : diff; + export const getWorkingTreePathDiff = async ( repoPath: string, filePath: string, _untracked = false, originalPath?: string, + fullContext = false, ): Promise => { try { const resolved = await resolveRepositoryForFile(repoPath, filePath); @@ -251,17 +262,19 @@ export const getWorkingTreePathDiff = async ( const filePaths = [ ...new Set([originalPath, resolved.filePath].filter(Boolean) as string[]), ]; - return await runGitRead( + const diff = await runGitRead( resolved.repoPath, - `working-tree-path-diff:${resolved.filePath}`, + `working-tree-path-diff:${fullContext ? "full:" : ""}${resolved.filePath}`, () => tauriInvoke("git_diff_file", { repoPath: resolved.repoPath, filePath: resolved.filePath, ...(filePaths.length > 1 ? { filePaths } : {}), worktreeSnapshot: true, + ...(fullContext ? { contextLines: FULL_FILE_CONTEXT_LINES } : {}), }), ); + return fullContext ? markFullContext(diff) : diff; } catch (error) { if (!isNotGitRepositoryError(error) && !isNoDiffFoundError(error)) { console.error("Failed to get working-tree path diff:", error); @@ -270,6 +283,40 @@ export const getWorkingTreePathDiff = async ( } }; +/** + * Index or worktree diff of one file with the whole file as context. It is not + * cached with `getFileDiff` results because gutter and AI consumers expect the + * regular sparse patch for the same key. + */ +export const getFullContextFileDiff = async ( + repoPath: string, + filePath: string, + staged = false, +): Promise => { + try { + const resolved = await resolveRepositoryForFile(repoPath, filePath); + if (!resolved) return null; + + const diff = await runGitRead( + resolved.repoPath, + `full-context-diff:${staged ? "staged" : "unstaged"}:${resolved.filePath}`, + () => + tauriInvoke("git_diff_file", { + repoPath: resolved.repoPath, + filePath: resolved.filePath, + staged, + contextLines: FULL_FILE_CONTEXT_LINES, + }), + ); + return markFullContext(diff); + } catch (error) { + if (!isNotGitRepositoryError(error) && !isNoDiffFoundError(error)) { + console.error("Failed to get full-context file diff:", error); + } + return null; + } +}; + export const getUntrackedFileDiff = async ( repoPath: string, filePath: string, diff --git a/windows/tauri/src/features/git/api/git-status-api.test.ts b/windows/tauri/src/features/git/api/git-status-api.test.ts index bcb49ac8e..81b28ca8e 100644 --- a/windows/tauri/src/features/git/api/git-status-api.test.ts +++ b/windows/tauri/src/features/git/api/git-status-api.test.ts @@ -5,9 +5,13 @@ let unavailableRepo: string | null = null; let statusFailure: Error | null = null; let interceptWrite: ((args: Record) => Promise) | undefined; +// Keyed by command: repository discovery may or may not run first, depending +// on what earlier tests in a randomized order have already cached. +let fileDiffResponse: unknown = null; const invoke = mock(async (command: string, args?: Record): Promise => { if (command === "git.write" && interceptWrite) return interceptWrite(args ?? {}); + if (command === "git_diff_file") return fileDiffResponse; if (command === "git_discover_repo") { const path = String(args?.path ?? ""); return path.startsWith("C:/workspace/") ? path : "C:/repo"; @@ -43,12 +47,13 @@ const { getWorkspaceRootGitStatus, getGitStatus, } = await import("./git-status-api"); -const { getWorkingTreePathDiff } = await import("./git-diff-api"); +const { getFullContextFileDiff, getWorkingTreePathDiff } = await import("./git-diff-api"); beforeEach(() => { invokeSpy = spyOn(tauriCore, "invoke").mockImplementation(invoke as typeof tauriCore.invoke); invoke.mockClear(); interceptWrite = undefined; + fileDiffResponse = null; unavailableRepo = null; statusFailure = null; }); @@ -173,6 +178,36 @@ describe("Git status review diffs", () => { worktreeSnapshot: true, }); }); + + // #557: a single-file review needs every source line so folded unchanged + // regions can be expanded; the result is marked for the renderer. + test("requests the whole file as context for a single-file review", async () => { + fileDiffResponse = { file_path: "src/App.tsx", lines: [] }; + await expect( + getWorkingTreePathDiff("C:/repo", "src/App.tsx", false, undefined, true), + ).resolves.toMatchObject({ is_full_context: true }); + + expect(invoke).toHaveBeenLastCalledWith("git_diff_file", { + repoPath: "C:/repo", + filePath: "src/App.tsx", + worktreeSnapshot: true, + contextLines: 2_147_483_647, + }); + }); + + test("requests a full-context staged diff without the sparse cache", async () => { + fileDiffResponse = { file_path: "src/App.tsx", lines: [] }; + await expect(getFullContextFileDiff("C:/repo", "src/App.tsx", true)).resolves.toMatchObject({ + is_full_context: true, + }); + + expect(invoke).toHaveBeenLastCalledWith("git_diff_file", { + repoPath: "C:/repo", + filePath: "src/App.tsx", + staged: true, + contextLines: 2_147_483_647, + }); + }); }); function deferred() { diff --git a/windows/tauri/src/features/git/components/diff/monaco-git-diff.tsx b/windows/tauri/src/features/git/components/diff/monaco-git-diff.tsx index e38cdfa1f..a5b3f4457 100644 --- a/windows/tauri/src/features/git/components/diff/monaco-git-diff.tsx +++ b/windows/tauri/src/features/git/components/diff/monaco-git-diff.tsx @@ -42,6 +42,7 @@ export default function MonacoGitDiff({ diff, viewMode = "split", showWhitespace const [height, setHeight] = useState(MIN_REVIEW_HEIGHT); const rows = useMemo(() => monacoDiffRows(diff), [diff]); const sourcePath = diff.new_path || diff.old_path || diff.file_path; + const fullContext = diff.is_full_context === true; const latest = useRef({ rows, sourcePath }); const updating = useRef(false); const repoPath = staging?.repoPath; @@ -114,13 +115,15 @@ export default function MonacoGitDiff({ diff, viewMode = "split", showWhitespace setError(undefined); updating.current = true; const action = hunkActions.current?.action; + // Only a full-file patch may fold: Monaco's fold bands reveal hidden lines + // in place, which is meaningless for a sparse patch whose gaps are absent. void review.current!.update({ rows, language: toMonacoLanguageId(detectLanguageFromPath(sourcePath)), - sideBySide: viewMode === "split", collapse: false, overview: !embedded, + sideBySide: viewMode === "split", collapse: fullContext, overview: !embedded, actions: action ? [{ id: action, title: actionTitle }] : [] }) .then(() => { if (!cancelled) { latest.current = { rows, sourcePath }; updating.current = false; } }) .catch(error => { if (!cancelled) setError(String(error)); }); return () => { cancelled = true; }; - }, [rows, sourcePath, viewMode, embedded, repoPath, isStaged, actionTitle]); + }, [rows, sourcePath, fullContext, viewMode, embedded, repoPath, isStaged, actionTitle]); useEffect(() => { review.current?.select({ matches: searchMatches.map(match => ({ rowID: `line-${match.lineIndex}`, diff --git a/windows/tauri/src/features/git/hooks/use-git-diff-actions.ts b/windows/tauri/src/features/git/hooks/use-git-diff-actions.ts index a043264bd..8dd191ff9 100644 --- a/windows/tauri/src/features/git/hooks/use-git-diff-actions.ts +++ b/windows/tauri/src/features/git/hooks/use-git-diff-actions.ts @@ -5,7 +5,7 @@ import { useTranslation } from "@/i18n/locale-provider"; import { showAlertDialog } from "@/ui/dialog"; import { getCommitDiff, - getFileDiff, + getFullContextFileDiff, getRefDiff, getStashDiff, getTypedReferenceDiff, @@ -165,12 +165,13 @@ export function useGitDiffActions({ const bufferId = openDiffBuffer("diff://working-tree/all-files", title, loadingDiff); void (async () => { const diff = staged - ? await getFileDiff(fileRepoPath, relativePath, true) + ? await getFullContextFileDiff(fileRepoPath, relativePath, true) : await getWorkingTreePathDiff( fileRepoPath, relativePath, untracked, originalRelativePath, + true, ); if ( !latestFileDiffRequest.isCurrent(requestId) || @@ -196,7 +197,7 @@ export function useGitDiffActions({ return; } - const diff = await getFileDiff(activeRepoPath, actualFilePath, staged); + const diff = await getFullContextFileDiff(activeRepoPath, actualFilePath, staged); if ( !latestFileDiffRequest.isCurrent(requestId) || activeRepoPathRef.current !== activeRepoPath diff --git a/windows/tauri/src/features/git/services/working-tree-diff-refresh.ts b/windows/tauri/src/features/git/services/working-tree-diff-refresh.ts index 438caf999..88ba363b3 100644 --- a/windows/tauri/src/features/git/services/working-tree-diff-refresh.ts +++ b/windows/tauri/src/features/git/services/working-tree-diff-refresh.ts @@ -1,6 +1,6 @@ import equal from "fast-deep-equal"; import { normalizePath } from "@/utils/path-helpers"; -import { getFileDiff, getWorkingTreePathDiff } from "../api/git-diff-api"; +import { getFullContextFileDiff, getWorkingTreePathDiff } from "../api/git-diff-api"; import { getGitStatus } from "../api/git-status-api"; import type { MultiFileDiff, WorkingTreeDiffTarget } from "../types/git-diff.types"; import type { GitDiff, GitFile, GitStatus } from "../types/git.types"; @@ -65,8 +65,8 @@ export async function refreshWorkingTreeFileDiff( loadStatus = getGitStatus, loadDiff = (root, path, untracked, originalPath, staged) => staged - ? getFileDiff(root, path, true) - : getWorkingTreePathDiff(root, path, untracked, originalPath), + ? getFullContextFileDiff(root, path, true) + : getWorkingTreePathDiff(root, path, untracked, originalPath, true), }: WorkingTreeDiffRefreshDependencies, ): Promise { const startingDiff = buffers.read(bufferId); diff --git a/windows/tauri/src/features/git/types/git.types.ts b/windows/tauri/src/features/git/types/git.types.ts index d173d81cc..7237fadb1 100644 --- a/windows/tauri/src/features/git/types/git.types.ts +++ b/windows/tauri/src/features/git/types/git.types.ts @@ -134,6 +134,11 @@ export interface GitDiff { additions?: number; deletions?: number; is_truncated?: boolean; + /** + * Every line of the file is present as patch context, so a review can fold + * and reveal unchanged regions locally instead of showing sparse hunks. + */ + is_full_context?: boolean; split_hunks?: GitDiffSplitRow[][]; } diff --git a/windows/tauri/src/features/git/utils/monaco-diff-rows.test.ts b/windows/tauri/src/features/git/utils/monaco-diff-rows.test.ts index 18bf2931a..ba491cc06 100644 --- a/windows/tauri/src/features/git/utils/monaco-diff-rows.test.ts +++ b/windows/tauri/src/features/git/utils/monaco-diff-rows.test.ts @@ -2,7 +2,33 @@ import type { ReviewRow } from "@lithe/editor/diff-review"; import { describe, expect, test } from "bun:test"; import fixture from "../../../../../../shared/fixtures/editor/diff-review-v1.json"; import type { GitDiff } from "../types/git.types"; -import { monacoDiffRows } from "./monaco-diff-rows"; +import { parseRawDiffContent } from "./git-diff-parser"; +import { monacoDiffHunk, monacoDiffRows } from "./monaco-diff-rows"; + +const filePatch = (hunks: string) => + `diff --git a/f.txt b/f.txt\n--- a/f.txt\n+++ b/f.txt\n${hunks}`; +const parse = (hunks: string) => parseRawDiffContent(filePatch(hunks), "f.txt") as GitDiff; +const fileLines = (from: number, to: number) => + Array.from({ length: to - from + 1 }, (_, index) => ` line ${from + index}`); + +// Both patches are real `git diff` output for one edit of a 40-line file: +// `--unified=2147483647` for the review, and Git's default `--unified=3`. +const fullContext: GitDiff = { + ...parse([ + "@@ -1,40 +1,40 @@", + " line 1", "-line 2", "+line 2 changed", ...fileLines(3, 12), "+inserted after 12", + ...fileLines(13, 18), "-line 19", ...fileLines(20, 37), "-line 38", "+line 37 changed", + ...fileLines(39, 40), "", + ].join("\n")), + is_full_context: true, +}; +const gitDefaultHunks = parse([ + "@@ -1,5 +1,5 @@", " line 1", "-line 2", "+line 2 changed", ...fileLines(3, 5), + "@@ -10,13 +10,13 @@ line 9", ...fileLines(10, 12), "+inserted after 12", ...fileLines(13, 18), + "-line 19", ...fileLines(20, 22), + "@@ -35,6 +35,6 @@ line 34", ...fileLines(35, 37), "-line 38", "+line 37 changed", + ...fileLines(39, 40), "", +].join("\n")); describe("shared Monaco diff rows", () => { test("matches the cross-platform sparse patch fixture", () => { @@ -18,3 +44,59 @@ describe("shared Monaco diff rows", () => { expect(rows.map(row => row.id)).toEqual(["line-0", "line-1"]); }); }); + +describe("full-context Monaco diff rows", () => { + // Regression for #557: the review holds every source line, so Monaco can + // reveal folded regions instead of showing a sparse patch with nothing to expand. + test("project the whole file without the full-file header row", () => { + const rows = monacoDiffRows(fullContext); + expect(rows.some(row => row.kind === "information")).toBe(false); + expect(rows.filter(row => row.left !== null).map(row => row.oldLine)) + .toEqual(Array.from({ length: 40 }, (_, index) => index + 1)); + expect(rows.filter(row => row.right !== null).map(row => row.newLine)) + .toEqual(Array.from({ length: 40 }, (_, index) => index + 1)); + // Identity still addresses the host line array, so search results keep working. + expect(rows[0].id).toBe("line-1"); + }); + + test("anchor one action band per hunk and leave distant context unowned", () => { + const rows = monacoDiffRows(fullContext); + const anchors = rows.filter(row => row.actionAnchor); + expect(anchors.map(row => row.hunkID)).toEqual(["hunk-2", "hunk-14", "hunk-40"]); + expect(rows.find(row => row.oldLine === 30)?.hunkID).toBeNull(); + }); + + test("stage the same hunks Git reports at its default context", () => { + const anchors = monacoDiffRows(fullContext).filter(row => row.actionAnchor); + const derived = anchors.map(row => monacoDiffHunk(fullContext, row.hunkID!)); + const expected = gitDefaultHunks.lines.flatMap((line, index) => + line.line_type === "header" ? [monacoDiffHunk(gitDefaultHunks, `hunk-${index}`)] : []); + // Git appends the enclosing-line hint to headers; `git apply` ignores it. + const withoutHint = (hunk: ReturnType) => hunk && { + ...hunk, + lines: hunk.lines.map(line => line.line_type === "header" + ? { line_type: "header" as const, content: line.content.replace(/ @@.*$/, " @@") } + : line), + }; + expect(derived).toEqual(expected.map(withoutHint)); + }); + + test("name the preceding line for a hunk with an empty side", () => { + const added = parse("@@ -0,0 +1,2 @@\n+first\n+second\n"); + const hunk = monacoDiffHunk({ ...added, is_full_context: true }, "hunk-1"); + expect(hunk?.lines[0]).toEqual({ line_type: "header", content: "@@ -0,0 +1,2 @@" }); + }); + + test("keep sparse-patch hunks when the patch is not marked full-context", () => { + expect(monacoDiffRows(gitDefaultHunks).filter(row => row.kind === "information")).toHaveLength(3); + expect(monacoDiffHunk({ ...fullContext, is_full_context: false }, "hunk-0")?.lines) + .toHaveLength(fullContext.lines.length); + }); + + test("reject identities that do not start a derived hunk", () => { + for (const id of ["hunk-0", "hunk-3", "hunk-30", "line-2"]) { + expect(monacoDiffHunk(fullContext, id)).toBeNull(); + } + expect(monacoDiffHunk({ ...fullContext, is_truncated: true }, "hunk-2")).toBeNull(); + }); +}); diff --git a/windows/tauri/src/features/git/utils/monaco-diff-rows.ts b/windows/tauri/src/features/git/utils/monaco-diff-rows.ts index 0ba71453a..d4d3bdb16 100644 --- a/windows/tauri/src/features/git/utils/monaco-diff-rows.ts +++ b/windows/tauri/src/features/git/utils/monaco-diff-rows.ts @@ -1,10 +1,94 @@ import type { ReviewRow } from "@lithe/editor/diff-review"; -import type { GitDiff, GitHunk } from "../types/git.types"; +import type { GitDiff, GitDiffLine, GitHunk } from "../types/git.types"; import { parseDiffHunkRange } from "./git-diff-helpers"; +/** Context kept around each change when a full-file patch is split back into + * stageable hunks. Matches Git's default `--unified=3`. */ +const FULL_CONTEXT_HUNK_CONTEXT_LINES = 3; + +/** A stageable range of a full-context patch, as indices into `diff.lines`. */ +interface FullContextHunk { + id: string; + /** First changed line; the hunk's action band is rendered above it. */ + anchor: number; + start: number; + /** Exclusive end index. */ + end: number; +} + +const isChange = (line: GitDiffLine) => line.line_type === "added" || line.line_type === "removed"; + +/** Splits a full-context patch the way Git would at three context lines, so + * staging granularity does not grow to the whole file. Returns null for sparse + * patches, which keep the host's own hunks. */ +function fullContextHunks(diff: GitDiff): FullContextHunk[] | null { + const { lines } = diff; + if (!diff.is_full_context || lines[0]?.line_type !== "header" + || lines.some((line, index) => index > 0 && line.line_type === "header")) return null; + const context = FULL_CONTEXT_HUNK_CONTEXT_LINES; + const hunks: FullContextHunk[] = []; + for (let index = 1; index < lines.length; index++) { + if (!isChange(lines[index])) continue; + let last = index; + // Like Git, changes whose surrounding context would touch share one hunk. + for (let next = index + 1; next < lines.length && next - last - 1 <= 2 * context; next++) { + if (isChange(lines[next])) last = next; + } + hunks.push({ + id: `hunk-${index}`, + anchor: index, + start: Math.max(1, index - context), + end: Math.min(lines.length, last + context + 1), + }); + index = last; + } + return hunks; +} + +function hunkHeader(lines: GitDiffLine[], start: number, end: number): string { + const range = (key: "old_line_number" | "new_line_number") => { + const numbers = lines.slice(start, end).flatMap(line => line[key] ?? []); + if (numbers.length > 0) return `${numbers[0]},${numbers.length}`; + // Git names the line before an empty range, or 0 at the start of the file. + for (let index = start - 1; index >= 0; index--) { + const previous = lines[index][key]; + if (previous !== undefined) return `${previous},0`; + } + return "0,0"; + }; + return `@@ -${range("old_line_number")} +${range("new_line_number")} @@`; +} + +const rowKind = (line: GitDiffLine): ReviewRow["kind"] => + line.line_type === "header" ? "information" + : line.line_type === "added" ? "addition" + : line.line_type === "removed" ? "removal" : "context"; + /** Preserve patch order and identity. Monaco aligns the projections, while * search results continue to address the original Git line array. */ export function monacoDiffRows(diff: GitDiff): ReviewRow[] { + const derived = fullContextHunks(diff); + if (derived) { + // The single full-file header carries no information worth a row, and its + // range would stage the whole file; derived hunks own the actions instead. + const owners = new Map(); + for (const hunk of derived) for (let index = hunk.start; index < hunk.end; index++) owners.set(index, hunk); + return diff.lines.flatMap((line, index) => { + if (line.line_type === "header") return []; + const hunk = owners.get(index); + return [{ + id: `line-${index}`, + oldLine: line.old_line_number ?? null, + newLine: line.new_line_number ?? null, + left: line.line_type === "added" ? null : line.content, + right: line.line_type === "removed" ? null : line.content, + kind: rowKind(line), + hunkID: hunk?.id ?? null, + ...(hunk?.anchor === index ? { actionAnchor: true } : {}), + }]; + }); + } + let hunkID: string | null = null; return diff.lines.map((line, index) => { if (line.line_type === "header") hunkID = `hunk-${index}`; @@ -14,9 +98,7 @@ export function monacoDiffRows(diff: GitDiff): ReviewRow[] { newLine: line.new_line_number ?? null, left: line.line_type === "added" ? null : line.content, right: line.line_type === "removed" ? null : line.content, - kind: line.line_type === "header" ? "information" - : line.line_type === "added" ? "addition" - : line.line_type === "removed" ? "removal" : "context", + kind: rowKind(line), hunkID, }; }); @@ -25,6 +107,18 @@ export function monacoDiffRows(diff: GitDiff): ReviewRow[] { /** Resolve the host's patch identity, never Monaco's aligned display ranges. */ export function monacoDiffHunk(diff: GitDiff, hunkID: string): GitHunk | null { if (diff.is_truncated || !/^hunk-(0|[1-9]\d*)$/.test(hunkID)) return null; + const derived = fullContextHunks(diff); + if (derived) { + const hunk = derived.find(candidate => candidate.id === hunkID); + if (!hunk) return null; + return { + file_path: diff.file_path, + lines: [ + { line_type: "header", content: hunkHeader(diff.lines, hunk.start, hunk.end) }, + ...diff.lines.slice(hunk.start, hunk.end), + ], + }; + } const start = Number(hunkID.slice(5)); const header = diff.lines[start]; if (header?.line_type !== "header" || !parseDiffHunkRange(header.content)) return null; diff --git a/windows/tauri/src/features/sidebar/utils/open-sidebar-resource.ts b/windows/tauri/src/features/sidebar/utils/open-sidebar-resource.ts index d8fb667b2..dd2eeb430 100644 --- a/windows/tauri/src/features/sidebar/utils/open-sidebar-resource.ts +++ b/windows/tauri/src/features/sidebar/utils/open-sidebar-resource.ts @@ -1,6 +1,6 @@ import { useBufferStore } from "@/features/editor/stores/buffer.store"; import { useFileSystemStore } from "@/features/file-system/stores/file-system.store"; -import { getFileDiff } from "@/features/git/api/git-diff-api"; +import { getFullContextFileDiff } from "@/features/git/api/git-diff-api"; import { openGitWorktreeWorkspace } from "@/features/git/utils/git-worktree-open"; import { openCommitDiffBuffer } from "@/features/git/utils/open-commit-diff-buffer"; import { createSingleFileWorkingTreeDiff } from "@/features/git/utils/working-tree-multi-diff"; @@ -34,7 +34,7 @@ const openWorkingTreeDiffBuffer = async ( return useBufferStore.getState().activeBufferId; } - const diff = await getFileDiff(resource.repoPath, actualFilePath, resource.staged); + const diff = await getFullContextFileDiff(resource.repoPath, actualFilePath, resource.staged); if (!diff || (diff.lines.length === 0 && diff.is_image !== true)) { await useFileSystemStore .getState() From 7decc114390c929bdbb6d6632050e8a12ea50cb9 Mon Sep 17 00:00:00 2001 From: Lichenkang <2188718831@qq.com> Date: Thu, 1 Oct 2026 16:11:53 +0800 Subject: [PATCH 05/12] =?UTF-8?q?chore(issue):=20=E5=BC=BA=E5=88=B6?= =?UTF-8?q?=E9=9D=9E=E7=BB=B4=E6=8A=A4=E8=80=85=E9=80=9A=E8=BF=87=20Issue?= =?UTF-8?q?=20=E8=A1=A8=E5=8D=95=E6=8F=90=E4=BA=A4=20(#990)=20(#1011)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * chore(issue): 强制非维护者通过 Issue 表单提交 (#990) - Bug / Feature 表单新增 issue-form:bug、issue-form:feature 来源标签 - 新增 lithe-issue-form-gate 工作流:issues opened 时校验作者权限, 非 write/maintain/admin 作者且无表单标签时双语评论并关闭 - 判定逻辑抽到 .github/lithe-issue-form/logic.mjs,权限未知、表单标签 未创建、转移等不确定情况一律不关闭 - 新增 scripts/test-lithe-issue-form-gate.mjs 覆盖验收标准 - 新增 process Agent Note 记录决策与备选方案 Co-Authored-By: Claude * fix(issue): 关闭前重新读取 Issue 以避免标签写入时序误关 (#990) 表单标签可能在 Issue 创建后约 1 秒才写入(见 #987 的事件记录), 在评论和关闭前再读一次 Issue 并重新判定,出现表单标签或已被关闭时不处理。 Co-Authored-By: Claude * fix(issue): address form gate review and add workflow regression coverage --------- Co-authored-by: Claude --- .../2026-10-01-issue-form-enforcement.md | 109 +++++++++++++++ .github/ISSUE_TEMPLATE/bug_report.yml | 3 + .github/ISSUE_TEMPLATE/feature_request.yml | 3 + .github/lithe-issue-form/logic.mjs | 104 ++++++++++++++ .github/workflows/lithe-issue-form-gate.yml | 94 +++++++++++++ .github/workflows/lithe-issue-priority.yml | 21 ++- .github/workflows/verify-issue-automation.yml | 35 +++++ scripts/test-lithe-issue-form-gate.mjs | 85 ++++++++++++ scripts/test-lithe-issue-workflows.mjs | 128 ++++++++++++++++++ 9 files changed, 576 insertions(+), 6 deletions(-) create mode 100644 .agents/notes/implemented/process/2026-10-01-issue-form-enforcement.md create mode 100644 .github/lithe-issue-form/logic.mjs create mode 100644 .github/workflows/lithe-issue-form-gate.yml create mode 100644 .github/workflows/verify-issue-automation.yml create mode 100644 scripts/test-lithe-issue-form-gate.mjs create mode 100644 scripts/test-lithe-issue-workflows.mjs diff --git a/.agents/notes/implemented/process/2026-10-01-issue-form-enforcement.md b/.agents/notes/implemented/process/2026-10-01-issue-form-enforcement.md new file mode 100644 index 000000000..864c46c4d --- /dev/null +++ b/.agents/notes/implemented/process/2026-10-01-issue-form-enforcement.md @@ -0,0 +1,109 @@ +# Agent 笔记:非维护者必须通过 Issue 表单提交 + +状态:已实现 + +## 先说结论 + +`blank_issues_enabled: false` 只隐藏网页上的空白 Issue 入口,挡不住 API、`gh issue create` 和第三方客户端。现在每个 Issue 表单都带一个 `issue-form:*` 标签,`.github/workflows/lithe-issue-form-gate.yml` 在 Issue 创建或重新打开时检查:对仓库有 write、maintain 或 admin 权限的作者直接放行;其他作者没有这类标签就收到中英双语说明,Issue 被关闭。任何拿不准的情况都保留 Issue,宁可漏关,不误关。 + +## 问题 + +#988 是一个正文为空、没有经过表单的 Issue。只检查正文是否为空不够:通过 API 随便写一句话,同样绕过了表单,表单里的平台、模块、优先级等字段都缺失,`lithe-issue-priority.yml` 也无法自动分类。 + +需要一个普通用户无法伪造、只有表单才会产生的信号。 + +## 决策 + +### 用表单标签作为来源证明 + +Issue 表单(Issue Form,`.github/ISSUE_TEMPLATE/*.yml` 定义的结构化模板)顶层的 `labels:` 会在提交时自动加到 Issue 上,与提交者权限无关。REST API 创建 Issue 时,没有 push 权限的用户传入的 labels 会被 GitHub 静默丢弃;网页 URL 里的 `labels=` 参数同样要求打标签权限。所以普通用户只有通过表单才能得到 `issue-form:*` 标签。 + +允许的标签和对应模板集中在 `.github/lithe-issue-form/logic.mjs` 的 `ISSUE_FORMS`。新增表单时,在表单的 `labels:` 加一个 `issue-form:<类型>`,同时把它加入 `ISSUE_FORMS`,并在仓库里创建同名标签。不要只改其中一处;测试会检查每个登记的模板都声明了自己的标签。 + +### 判定顺序 + +`decideIssueFormGate` 返回 `allow`、`close` 或 `skip`,工作流只在 `close` 时评论并关闭: + +1. 不是开放状态的 Issue、Pull Request、缺少作者信息:`skip`。 +2. 作者是 Bot:`allow`。只有维护者安装的 App 或工作流才能以 Bot 身份建 Issue。 +3. 触发者不是作者(例如维护者转移或重新打开 Issue):`skip`。 +4. 读取作者权限失败:`skip`。 +5. 作者权限是 write、maintain、admin(`role_name` 或旧 `permission` 字段任一命中,后者覆盖自定义角色):`allow`。 +6. Issue 带任一允许的 `issue-form:*` 标签:`allow`。 +7. 仓库里缺少任一允许的标签,或任一标签已归档:`skip`。表单无法应用不存在或已归档的标签,此时无法区分“来自表单”和“绕过表单”。 +8. 其余情况:`close`,`state_reason` 为 `not_planned`。 + +权限以 `GET /repos/{owner}/{repo}/collaborators/{username}/permission` 为准,不以 `author_association` 为准:`COLLABORATOR` 可能只有 read 或 triage,`MEMBER` 也不代表对本仓库有写权限。工作流级 `if` 只跳过 `OWNER`、Bot 和 Pull Request,省掉一次 runner 启动,其余作者都由脚本查询真实权限。 + +### 标签时序 + +表单标签不一定和 Issue 同一时刻写入:#987 的 `bug` 标签比创建时间晚约 1 秒,事件记录的操作者是作者本人。所以工作流合并事件里的标签和重新读取的标签,并在真正关闭前再读一次 Issue、重新判定;只要此时出现表单标签,或 Issue 已被关闭,就不再处理。 + +### 评论与幂等 + +关闭前的评论带隐藏标记 ``,重跑工作流时如果已经有机器人发的同标记评论就不再重复评论。评论只引用作者 login 和固定的模板链接,不回显 Issue 标题或正文,避免把用户输入带进机器人内容。脚本通过 `actions/github-script` 的 API 客户端读取事件数据,不把标题或正文插入 shell 或 `${{ }}` 表达式,因此不存在脚本注入面。 + +### 和现有 Issue 自动化的关系 + +- `lithe-issue-priority.yml` 在 `opened` 和 `edited` 时只增删自己管理的 `bug`、`enhancement`、`P*`、`platform:*`、`area:*`,`issue-form:*` 会被保留。它和本工作流并行运行;禁止用旧快照执行 `setLabels` 整体替换,否则会抹掉读取后 GitHub 才补上的来源标签。使用 `addLabels` 和 `removeLabel`,让非托管标签始终保留。 +- `lithe-issue-claim.yml` 只处理评论事件,被关闭的 Issue 仍可由维护者重新打开后认领。 +- 维护者手动创建的 Issue 不需要 `issue-form:*` 标签。 + +### 正确做法 + +- 新增表单:模板 `labels:` 加 `issue-form:<类型>`,`ISSUE_FORMS` 登记,先在仓库创建标签再合并。 +- 误关时:由维护者重新打开 Issue,触发者与作者不同会跳过门禁。作者自己重新打开会再次检查,不能通过“立即关闭 → 等待门禁跳过 → 重新打开”绕过。 + +### 不要这样做 + +- 不要改成只检查正文是否为空,API 写一句话就能绕过。 +- 不要用 `author_association` 判断维护者,它不等于仓库写权限。 +- 不要在 `run:` 步骤里用 `${{ github.event.issue.title }}` 或正文拼 shell 命令。 +- 不要在缺少或归档标签时“先关再说”,那会关闭所有正常的表单 Issue。 + +## 考虑过的备选方案 + +### 只拦截空正文 + +实现最简单,也能处理 #988。但 API 或 CLI 写任意一句话即可绕过,而这类 Issue 同样缺少表单字段,所以没有采用。 + +### 按正文里的表单标题结构识别 + +表单正文有固定的 `### 字段名` 结构,不需要额外标签。但普通用户可以照抄这种 Markdown 结构伪造,字段文案调整时还要同步修改判定,所以没有采用。 + +### 仓库级只允许协作者创建 Issue + +服务端彻底限制,但普通用户将完全无法反馈问题,违背开放反馈的目标,所以没有采用。 + +### 缺少标签时由工作流自动创建 + +能让门禁“自己修好”,但工作流会因此需要隐式改仓库配置,也会在标签被维护者有意删除后悄悄恢复,所以改为缺少时放行并在日志里警告。 + +## 后果 + +- 收益:非维护者无论从哪个入口提交,都会留下完整表单字段,自动分类可以生效;被关闭的作者能看到双语说明和正确入口。 +- 代价:每个非 OWNER 作者的新 Issue 都会启动一次短时 runner(作业超时 5 分钟)。普通用户从其他仓库转移进来的 Issue、维护者手动删掉 `issue-form:*` 标签的 Issue 不受门禁约束。 +- 代价:如果 GitHub 改变“非 push 用户的 labels 静默丢弃”的行为,或允许 read 角色通过 URL 参数打标签,这个信号就能被伪造,需要重新评估。 +- 重新评估触发条件:GitHub 提供原生的“必须使用表单”设置;新增表单或改名模板文件;评论或关闭策略改变。 + +## 验证 + +```bash +node --test --test-timeout=10000 scripts/test-lithe-issue-*.mjs +actionlint .github/workflows/lithe-issue-form-gate.yml .github/workflows/lithe-issue-priority.yml .github/workflows/verify-issue-automation.yml +./scripts/verify-agent-notes.sh +``` + +测试覆盖 Bug、Feature 表单放行,空白 Issue 和带正文但无表单标签的 Issue 被关闭,write、maintain、admin 与自定义角色放行,以及权限未知、标签缺失、转移等不确定情况不关闭。工作流编排测试执行实际的 github-script 脚本,以模拟 API 控制“分类读取 → GitHub 补标签 → 分类写入 → 门禁检查”的顺序,同时覆盖作者重新打开、维护者重新打开、标签归档及权限读取失败。所有测试均不访问网络、不依赖真实延时。`verify-issue-automation.yml` 在相关 PR 和 main/preview 推送时执行这些测试。 + +## 适用范围 + +- `.github/workflows/lithe-issue-form-gate.yml` +- `.github/lithe-issue-form/logic.mjs` +- `.github/ISSUE_TEMPLATE/bug_report.yml` +- `.github/ISSUE_TEMPLATE/feature_request.yml` +- `scripts/test-lithe-issue-form-gate.mjs` + +- `scripts/test-lithe-issue-workflows.mjs` +- `.github/workflows/verify-issue-automation.yml` +- `.github/workflows/lithe-issue-priority.yml` diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index b432d8960..a4c394275 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -3,6 +3,9 @@ description: Report a reproducible problem in Lithe / 报告可复现的问题 title: "[Bug] " labels: - bug + # Marks issues created from this form; .github/workflows/lithe-issue-form-gate.yml + # closes non-maintainer issues without an issue-form:* label. + - issue-form:bug body: - type: markdown attributes: diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml index 36bebe5ad..eae440a25 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.yml +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -3,6 +3,9 @@ description: Suggest an improvement or a new capability / 建议改进现有能 title: "[Feature] " labels: - enhancement + # Marks issues created from this form; .github/workflows/lithe-issue-form-gate.yml + # closes non-maintainer issues without an issue-form:* label. + - issue-form:feature body: - type: markdown attributes: diff --git a/.github/lithe-issue-form/logic.mjs b/.github/lithe-issue-form/logic.mjs new file mode 100644 index 000000000..394a32164 --- /dev/null +++ b/.github/lithe-issue-form/logic.mjs @@ -0,0 +1,104 @@ +// Decision logic for .github/workflows/lithe-issue-form-gate.yml. +// Note: 决策记录见 .agents/notes/implemented/process/2026-10-01-issue-form-enforcement.md + +// Each supported Issue Form declares exactly one of these labels in its +// `labels:` list. GitHub applies form labels regardless of the author's +// permission, while labels passed through the REST API or `gh issue create` +// are silently dropped for users without push access, so the label is a +// marker that non-maintainers cannot forge. +export const ISSUE_FORMS = Object.freeze([ + Object.freeze({ label: 'issue-form:bug', template: 'bug_report.yml', title: 'Bug 报告 / Bug report' }), + Object.freeze({ label: 'issue-form:feature', template: 'feature_request.yml', title: '功能建议 / Feature request' }), +]); + +export const ALLOWED_LABELS = Object.freeze(ISSUE_FORMS.map((form) => form.label)); + +// Hidden marker that identifies the gate's own comment, so a re-run does not +// post a duplicate explanation. +export const GATE_MARKER = ''; + +const MAINTAINER_PERMISSIONS = new Set(['admin', 'maintain', 'write']); + +export function labelNames(labels = []) { + return labels + .map((label) => (typeof label === 'string' ? label : label?.name)) + .filter(Boolean); +} + +// `role_name` reports admin/maintain/write/triage/read or a custom role name; +// the legacy `permission` field still carries the base level for custom roles. +export function isMaintainerPermission(permission) { + if (!permission) return false; + return MAINTAINER_PERMISSIONS.has(permission.role_name) || MAINTAINER_PERMISSIONS.has(permission.permission); +} + +export function missingAllowedLabels(existingRepoLabels, allowedLabels = ALLOWED_LABELS) { + const existing = new Set(existingRepoLabels); + return allowedLabels.filter((label) => !existing.has(label)); +} + +// Returns { action, reason } where action is: +// - 'allow': the issue stays open; +// - 'close': comment and close the issue; +// - 'skip': the gate cannot decide safely and leaves the issue untouched. +// Every uncertain input fails open so that a form-created issue is never +// closed because of a configuration gap or a transient API failure. +export function decideIssueFormGate({ + issue, + sender, + authorPermission, + missingLabels = [], + allowedLabels = ALLOWED_LABELS, +}) { + if (!issue) return { action: 'skip', reason: 'missing-issue' }; + if (issue.pull_request) return { action: 'skip', reason: 'pull-request' }; + if (issue.state && issue.state !== 'open') return { action: 'skip', reason: 'not-open' }; + const author = issue.user?.login; + if (!author) return { action: 'skip', reason: 'missing-author' }; + // Bot accounts can only create issues here through an app or workflow that + // a maintainer installed on this repository. + if (issue.user.type === 'Bot') return { action: 'allow', reason: 'bot-author' }; + // Preserve another actor's transfer or reopen decision. Author reopens + // still pass through the gate, so closing and reopening cannot bypass it. + if (sender?.login && sender.login !== author) return { action: 'skip', reason: 'opened-by-another-actor' }; + if (authorPermission === null || authorPermission === undefined) return { action: 'skip', reason: 'permission-unknown' }; + if (isMaintainerPermission(authorPermission)) return { action: 'allow', reason: 'maintainer' }; + const labels = labelNames(issue.labels); + if (labels.some((label) => allowedLabels.includes(label))) return { action: 'allow', reason: 'issue-form' }; + // Issue Forms cannot apply missing or archived labels. Until every + // allowed label exists, an unlabeled issue may still come from a form. + if (missingLabels.length > 0) return { action: 'skip', reason: 'form-labels-missing' }; + return { action: 'close', reason: 'not-from-issue-form' }; +} + +export function hasGateComment(comments = []) { + return comments.some((comment) => + comment.user?.type === 'Bot' && + comment.user?.login === 'github-actions[bot]' && + (comment.body ?? '').includes(GATE_MARKER) + ); +} + +export function issueFormLinks(serverUrl, owner, repo) { + const base = `${serverUrl}/${owner}/${repo}/issues/new`; + return { + chooser: `${base}/choose`, + forms: ISSUE_FORMS.map((form) => ({ title: form.title, url: `${base}?template=${encodeURIComponent(form.template)}` })), + }; +} + +// The author login comes from GitHub's account model (alphanumerics and +// hyphens); no title or body text is echoed back into the comment. +export function gateComment({ login, serverUrl, owner, repo }) { + const links = issueFormLinks(serverUrl, owner, repo); + const formLines = links.forms.map((form) => `- ${form.title}:${form.url}`).join('\n'); + return [ + GATE_MARKER, + `@${login} 感谢反馈。本仓库要求通过 Issue 表单提交 Issue。这个 Issue 没有经过表单创建(例如空白 Issue,或通过 API、CLI、第三方客户端直接创建),因此已自动关闭。请通过下面的入口重新提交:`, + '', + formLines, + `- 全部模板 / All templates:${links.chooser}`, + '', + 'Thanks for the report. This repository only accepts issues created from its issue forms. This issue was not created from a form (for example a blank issue, or one created through the API, CLI, or a third-party client), so it was closed automatically. Please resubmit it using one of the links above.', + ].join('\n'); +} diff --git a/.github/workflows/lithe-issue-form-gate.yml b/.github/workflows/lithe-issue-form-gate.yml new file mode 100644 index 000000000..a00360899 --- /dev/null +++ b/.github/workflows/lithe-issue-form-gate.yml @@ -0,0 +1,94 @@ +name: Lithe issue form gate + +on: + issues: + types: [opened, reopened] + +permissions: + contents: read + issues: write + +concurrency: + group: lithe-issue-form-gate-${{ github.event.issue.number }} + cancel-in-progress: false + +jobs: + gate: + # Skip pull requests, bots, and repository owners; all other authors + # are checked against their actual repository permission. + if: >- + github.event.issue.pull_request == null && + github.event.issue.user.type != 'Bot' && + github.event.issue.author_association != 'OWNER' + runs-on: ubuntu-latest + timeout-minutes: 5 + steps: + - uses: actions/checkout@v7 + with: + fetch-depth: 1 + persist-credentials: false + - uses: actions/github-script@v7 + env: + ISSUE_FORM_LOGIC: ${{ github.workspace }}/.github/lithe-issue-form/logic.mjs + with: + github-token: ${{ github.token }} + script: | + const logic = await import(process.env.ISSUE_FORM_LOGIC); + const { owner, repo } = context.repo; + const issue_number = context.issue.number; + const payloadIssue = context.payload.issue; + const login = payloadIssue.user.login; + + // Re-read the issue so a maintainer action that already closed it + // is respected; keep the labels delivered with the opened event too. + const current = (await github.rest.issues.get({ owner, repo, issue_number })).data; + const labels = [...new Set([...logic.labelNames(payloadIssue.labels), ...logic.labelNames(current.labels)])]; + const issue = { ...current, user: current.user ?? payloadIssue.user, labels }; + + // Non-collaborators receive a 200 response with read/none, so any + // error means the permission is unknown and the issue stays open. + let authorPermission = null; + try { + authorPermission = (await github.rest.repos.getCollaboratorPermissionLevel({ owner, repo, username: login })).data; + } catch (error) { + core.warning(`Unable to read permission for ${login}; leaving the issue open: ${error.message}`); + } + + // Issue Forms drop labels that do not exist, so a missing or + // unreadable allowed label disables closing instead of misfiring. + const existingLabels = []; + for (const name of logic.ALLOWED_LABELS) { + try { + const label = (await github.rest.issues.getLabel({ owner, repo, name })).data; + if (label.archived_at == null) existingLabels.push(name); + } catch (error) { + if (error.status !== 404) core.warning(`Unable to read label ${name}: ${error.message}`); + } + } + const missingLabels = logic.missingAllowedLabels(existingLabels); + if (missingLabels.length) core.warning(`Issue form labels are missing or archived (${missingLabels.join(', ')}); the gate will not close unlabeled issues until they exist.`); + + const decision = logic.decideIssueFormGate({ issue, sender: context.payload.sender, authorPermission, missingLabels }); + core.info(`Issue #${issue_number} by ${login}: ${decision.action} (${decision.reason})`); + if (decision.action !== 'close') return; + + // Form labels can be written a moment after the issue is created, + // and a maintainer may act meanwhile; re-check right before closing. + const latest = (await github.rest.issues.get({ owner, repo, issue_number })).data; + const recheck = logic.decideIssueFormGate({ + issue: { ...latest, user: latest.user ?? payloadIssue.user, labels: [...labels, ...logic.labelNames(latest.labels)] }, + sender: context.payload.sender, + authorPermission, + missingLabels, + }); + if (recheck.action !== 'close') { + core.info(`Issue #${issue_number} recheck: ${recheck.action} (${recheck.reason})`); + return; + } + + const comments = await github.paginate(github.rest.issues.listComments, { owner, repo, issue_number, per_page: 100 }); + if (!logic.hasGateComment(comments)) { + const body = logic.gateComment({ login, serverUrl: context.serverUrl, owner, repo }); + await github.rest.issues.createComment({ owner, repo, issue_number, body }); + } + await github.rest.issues.update({ owner, repo, issue_number, state: 'closed', state_reason: 'not_planned' }); diff --git a/.github/workflows/lithe-issue-priority.yml b/.github/workflows/lithe-issue-priority.yml index e3909d4ab..ec7d208ac 100644 --- a/.github/workflows/lithe-issue-priority.yml +++ b/.github/workflows/lithe-issue-priority.yml @@ -71,11 +71,11 @@ jobs: const currentLabels = issue.labels .map((label) => typeof label === 'string' ? label : label.name) .filter(Boolean); - const nextLabels = currentLabels.filter((label) => !managedLabels.includes(label)); - nextLabels.push(...selectedLabels); - const unchanged = currentLabels.length === nextLabels.length && - currentLabels.every((label, index) => label === nextLabels[index]); - if (unchanged) return; + // Only mutate labels owned by this workflow. Replacing the whole + // list can erase form labels applied after the snapshot was read. + const removedLabels = currentLabels.filter((label) => managedLabels.includes(label) && !selectedLabels.includes(label)); + const addedLabels = selectedLabels.filter((label) => !currentLabels.includes(label)); + if (!removedLabels.length && !addedLabels.length) return; for (const name of selectedLabels) { try { await github.rest.issues.getLabel({ owner, repo, name }); @@ -86,4 +86,13 @@ jobs: }); } } - await github.rest.issues.setLabels({ owner, repo, issue_number, labels: nextLabels }); + for (const name of removedLabels) { + try { + await github.rest.issues.removeLabel({ owner, repo, issue_number, name }); + } catch (error) { + if (error.status !== 404) throw error; + } + } + if (addedLabels.length) { + await github.rest.issues.addLabels({ owner, repo, issue_number, labels: addedLabels }); + } diff --git a/.github/workflows/verify-issue-automation.yml b/.github/workflows/verify-issue-automation.yml new file mode 100644 index 000000000..799ddc36b --- /dev/null +++ b/.github/workflows/verify-issue-automation.yml @@ -0,0 +1,35 @@ +name: Verify issue automation + +on: + pull_request: + paths: + - '.github/ISSUE_TEMPLATE/**' + - '.github/lithe-issue-*/**' + - '.github/workflows/lithe-issue-*.yml' + - '.github/workflows/verify-issue-automation.yml' + - 'scripts/test-lithe-issue-*.mjs' + push: + branches: [main, preview] + paths: + - '.github/ISSUE_TEMPLATE/**' + - '.github/lithe-issue-*/**' + - '.github/workflows/lithe-issue-*.yml' + - '.github/workflows/verify-issue-automation.yml' + - 'scripts/test-lithe-issue-*.mjs' + +permissions: + contents: read + +jobs: + test: + runs-on: ubuntu-latest + timeout-minutes: 5 + steps: + - uses: actions/checkout@v7 + with: + persist-credentials: false + - uses: actions/setup-node@v6 + with: + node-version: '24' + - name: Test issue decisions and workflow orchestration + run: node --test --test-timeout=10000 scripts/test-lithe-issue-*.mjs diff --git a/scripts/test-lithe-issue-form-gate.mjs b/scripts/test-lithe-issue-form-gate.mjs new file mode 100644 index 000000000..f4f73a6c3 --- /dev/null +++ b/scripts/test-lithe-issue-form-gate.mjs @@ -0,0 +1,85 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import { readFileSync } from 'node:fs'; +import { + ALLOWED_LABELS, GATE_MARKER, ISSUE_FORMS, decideIssueFormGate, gateComment, hasGateComment, isMaintainerPermission, labelNames, missingAllowedLabels, +} from '../.github/lithe-issue-form/logic.mjs'; + +const readPermission = { permission: 'read', role_name: 'read' }; +const issueBy = (login, overrides = {}) => ({ + number: 1, state: 'open', user: { login, type: 'User' }, labels: [], body: '', ...overrides, +}); +const decide = (issue, authorPermission, extra = {}) => + decideIssueFormGate({ issue, sender: { login: issue.user.login }, authorPermission, ...extra }); + +test('every issue form declares its gate label', () => { + for (const form of ISSUE_FORMS) { + const source = readFileSync(new URL(`../.github/ISSUE_TEMPLATE/${form.template}`, import.meta.url), 'utf8'); + assert.match(source, new RegExp(`^\\s*-\\s*${form.label}\\s*$`, 'm'), `${form.template} must list ${form.label}`); + } +}); + +test('non-maintainer issues from the bug and feature forms stay open', () => { + assert.deepEqual(decide(issueBy('alice', { labels: [{ name: 'bug' }, { name: 'issue-form:bug' }] }), readPermission), { action: 'allow', reason: 'issue-form' }); + assert.deepEqual(decide(issueBy('alice', { labels: [{ name: 'enhancement' }, { name: 'issue-form:feature' }] }), readPermission), { action: 'allow', reason: 'issue-form' }); +}); + +test('a blank non-maintainer issue is closed', () => { + assert.deepEqual(decide(issueBy('alice', { body: null }), readPermission), { action: 'close', reason: 'not-from-issue-form' }); +}); + +test('an API or CLI issue with a non-empty body but no form label is closed', () => { + const issue = issueBy('alice', { title: '[Bug] crash', body: 'please support this', labels: [{ name: 'bug' }] }); + assert.deepEqual(decide(issue, readPermission), { action: 'close', reason: 'not-from-issue-form' }); +}); + +test('regression #988: an outside user with an empty body is closed', () => { + const issue = issueBy('outside-user', { title: '能否做一个热更新的功能', body: null, labels: [{ name: 'review: low' }] }); + assert.equal(decide(issue, { permission: 'none', role_name: 'none' }).action, 'close'); +}); + +test('write, maintain, and admin authors may open blank issues', () => { + for (const level of ['write', 'maintain', 'admin']) { + assert.deepEqual(decide(issueBy('maintainer'), { permission: level, role_name: level }), { action: 'allow', reason: 'maintainer' }); + } + // Custom roles keep their base level in the legacy permission field. + assert.equal(decide(issueBy('custom'), { permission: 'write', role_name: 'release-manager' }).action, 'allow'); + assert.equal(isMaintainerPermission({ permission: 'read', role_name: 'triage' }), false); + assert.equal(isMaintainerPermission(null), false); +}); + +test('uncertain inputs never close an issue', () => { + assert.deepEqual(decide(issueBy('alice'), null), { action: 'skip', reason: 'permission-unknown' }); + assert.deepEqual(decide(issueBy('alice'), readPermission, { missingLabels: ['issue-form:bug'] }), { action: 'skip', reason: 'form-labels-missing' }); + assert.equal(decide(issueBy('alice', { state: 'closed' }), readPermission).action, 'skip'); + assert.equal(decide(issueBy('alice', { pull_request: { url: 'x' } }), readPermission).action, 'skip'); + assert.equal(decideIssueFormGate({ issue: issueBy('alice'), sender: { login: 'maintainer' }, authorPermission: readPermission }).reason, 'opened-by-another-actor'); + assert.equal(decideIssueFormGate({ issue: null }).action, 'skip'); +}); + +test('bot authors are allowed', () => { + assert.equal(decide(issueBy('dependabot[bot]', { user: { login: 'dependabot[bot]', type: 'Bot' } }), readPermission).action, 'allow'); +}); + +test('label helpers accept string and object labels', () => { + assert.deepEqual(labelNames(['bug', { name: 'issue-form:bug' }, {}, null]), ['bug', 'issue-form:bug']); + assert.deepEqual(missingAllowedLabels(['issue-form:bug']), ['issue-form:feature']); + assert.deepEqual(missingAllowedLabels(ALLOWED_LABELS), []); +}); + +test('the gate comment is bilingual, links every form, and never echoes issue text', () => { + const body = gateComment({ login: 'alice', serverUrl: 'https://github.com', owner: '1lck', repo: 'Lithe-IDEA' }); + assert.ok(body.startsWith(GATE_MARKER)); + assert.match(body, /@alice/); + assert.match(body, /Issue 表单/); + assert.match(body, /issue forms/); + assert.ok(body.includes('https://github.com/1lck/Lithe-IDEA/issues/new?template=bug_report.yml')); + assert.ok(body.includes('https://github.com/1lck/Lithe-IDEA/issues/new?template=feature_request.yml')); + assert.ok(body.includes('https://github.com/1lck/Lithe-IDEA/issues/new/choose')); +}); + +test('only the workflow bot comment counts as an existing gate comment', () => { + assert.equal(hasGateComment([{ user: { login: 'alice', type: 'User' }, body: GATE_MARKER }]), false); + assert.equal(hasGateComment([{ user: { login: 'github-actions[bot]', type: 'Bot' }, body: `${GATE_MARKER}\nclosed` }]), true); + assert.equal(hasGateComment([]), false); +}); diff --git a/scripts/test-lithe-issue-workflows.mjs b/scripts/test-lithe-issue-workflows.mjs new file mode 100644 index 000000000..7a4d223ea --- /dev/null +++ b/scripts/test-lithe-issue-workflows.mjs @@ -0,0 +1,128 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import { readFileSync } from 'node:fs'; + +const AsyncFunction = Object.getPrototypeOf(async function () {}).constructor; +const workflowSource = (name) => readFileSync(new URL(`../.github/workflows/${name}.yml`, import.meta.url), 'utf8'); +const gateSource = workflowSource('lithe-issue-form-gate'); +const prioritySource = workflowSource('lithe-issue-priority'); +const logicUrl = new URL('../.github/lithe-issue-form/logic.mjs', import.meta.url).href; + +// Execute the actual checked-in github-script body with deterministic API doubles. +// Pass a local process double so tests do not mutate shared environment variables. +async function run(source, fixture) { + const marker = ' script: |\n'; + assert.equal(source.split(marker).length, 2); + const body = source.split(marker)[1].split('\n').map((line) => line.slice(12)).join('\n'); + await new AsyncFunction('github', 'context', 'core', 'process', body)( + fixture.github, fixture.context, { info() {}, warning() {} }, + { env: { ISSUE_FORM_LOGIC: logicUrl } }, + ); +} + +function fixture({ archived = false, permission = 'read', onRead = () => {}, sender = 'alice' } = {}) { + const issue = { number: 1, state: 'open', user: { login: 'alice', type: 'User' }, labels: [], title: '[Bug] example', body: '### Priority / 优先级\nP1' }; + const context = { repo: { owner: 'example', repo: 'test' }, issue: { number: 1 }, payload: { issue: structuredClone(issue), sender: { login: sender } }, serverUrl: 'https://github.com' }; + const comments = []; + const removals = []; + let reads = 0; + const github = { + rest: { + issues: { + get: async () => { + const snapshot = structuredClone(issue); + onRead(issue, ++reads); + return { data: snapshot }; + }, + getLabel: async () => ({ data: { archived_at: archived ? '2026-01-01T00:00:00Z' : null } }), + addLabels: async ({ labels }) => { + for (const name of labels) if (!issue.labels.some((label) => label.name === name)) issue.labels.push({ name }); + }, + removeLabel: async ({ name }) => { + removals.push(name); + issue.labels = issue.labels.filter((label) => label.name !== name); + }, + listComments: async () => ({ data: comments }), + createComment: async ({ body }) => comments.push({ user: { login: 'github-actions[bot]', type: 'Bot' }, body }), + update: async ({ state, state_reason }) => Object.assign(issue, { state, state_reason }), + }, + repos: { getCollaboratorPermissionLevel: async () => ({ data: { permission, role_name: permission } }) }, + }, + paginate: async (method, args) => (await method(args)).data, + }; + return { issue, context, github, comments, removals }; +} + +test('author close then reopen is checked again, without duplicating comments', async () => { + assert.match(gateSource, /types: \[opened, reopened\]/); + const f = fixture(); + f.issue.state = 'closed'; + await run(gateSource, f); + assert.equal(f.comments.length, 0); + f.context.payload.action = 'reopened'; + f.issue.state = 'open'; + await run(gateSource, f); + assert.equal(f.issue.state, 'closed'); + assert.equal(f.issue.state_reason, 'not_planned'); + assert.equal(f.comments.length, 1); + f.issue.state = 'open'; + await run(gateSource, f); + assert.equal(f.issue.state, 'closed'); + assert.equal(f.comments.length, 1); +}); + +test('another actor reopening an external issue is respected', async () => { + const f = fixture({ sender: 'maintainer' }); + f.context.payload.action = 'reopened'; + await run(gateSource, f); + assert.equal(f.issue.state, 'open'); + assert.equal(f.comments.length, 0); +}); + +test('archived form labels disable closing even when getLabel succeeds', async () => { + const f = fixture({ archived: true }); + await run(gateSource, f); + assert.equal(f.issue.state, 'open'); + assert.equal(f.comments.length, 0); +}); + +test('priority synchronization preserves form labels applied after its read', async () => { + // opened payload and priority snapshot have no form label. GitHub applies it + // before priority writes; gate starts only after priority has finished. + const f = fixture({ onRead(issue, count) { + if (count === 1) issue.labels.push({ name: 'issue-form:bug' }); + } }); + f.issue.labels.push({ name: 'P2' }, { name: 'claimed' }); + await run(prioritySource, f); + assert.deepEqual(f.removals, ['P2']); + assert.deepEqual(f.issue.labels.map((label) => label.name).sort(), ['P1', 'bug', 'claimed', 'issue-form:bug']); + await run(gateSource, f); + assert.equal(f.issue.state, 'open'); + assert.equal(f.comments.length, 0); +}); + +test('a form label appearing between gate reads prevents closing', async () => { + const f = fixture({ onRead(issue, count) { + if (count === 1) issue.labels.push({ name: 'issue-form:feature' }); + } }); + await run(gateSource, f); + assert.equal(f.issue.state, 'open'); + assert.equal(f.comments.length, 0); +}); + +test('permission lookup failure leaves the issue open', async () => { + const f = fixture(); + f.github.rest.repos.getCollaboratorPermissionLevel = async () => { throw new Error('unavailable'); }; + await run(gateSource, f); + assert.equal(f.issue.state, 'open'); + assert.equal(f.comments.length, 0); +}); + +test('maintainer authors can still submit without form labels', async () => { + for (const permission of ['write', 'maintain', 'admin']) { + const f = fixture({ permission }); + await run(gateSource, f); + assert.equal(f.issue.state, 'open'); + assert.equal(f.comments.length, 0); + } +}); From 4af800d61fbfdd0b22b96d8a8ecc4ba46f4a873c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=8B=A5=E6=B0=B4?= Date: Thu, 1 Oct 2026 16:55:14 +0800 Subject: [PATCH 06/12] =?UTF-8?q?feat(windows):=20=E5=B7=A5=E4=BD=9C?= =?UTF-8?q?=E5=8F=B0=E5=B8=83=E5=B1=80=E4=B8=8E=20Git=20Log=20=E4=BD=93?= =?UTF-8?q?=E9=AA=8C=E8=B0=83=E6=95=B4=EF=BC=88=E5=8F=82=E8=80=83=20IDEA?= =?UTF-8?q?=EF=BC=89=EF=BC=8CCore=20=E6=8F=90=E4=BA=A4=E6=97=A5=E6=9C=9F?= =?UTF-8?q?=E6=90=BA=E5=B8=A6=E6=97=B6=E5=8C=BA=E5=81=8F=E7=A7=BB=20(#1013?= =?UTF-8?q?)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(windows): 设置入口从活动栏底部移至标题栏搜索按钮旁 - Windows 标题栏在搜索(放大镜)按钮后新增设置齿轮按钮,点击打开设置对话框 - 活动栏移除设置项,右键「在活动栏中显示」列表不再包含设置 - settings 移出活动栏 ID 集合,旧设置文件中遗留的排序/隐藏条目在归一化时自动清理 - 同步更新 item-order 与 git-log keymap 测试断言,新增 settings 不属于活动栏的防回归断言 - 平台功能矩阵新增 workbench-settings-entry 行并重新生成 md/csv 视图 * fix(scripts): verify-windows-boundaries 豁免过滤兼容 Git Bash/Windows 豁免正则以 "/" 开头会被 Git Bash (MSYS) 当作路径改写,加上 ripgrep 在 Windows 上输出反斜杠分隔路径,导致 core/lithe-core-client 与 platform/tauri-core 两个合法豁免文件被误判为违规。改为字符类开头并 兼容两种分隔符;已在 Windows 验证干净树通过、植入违规文件能被捕获。 * feat(windows): 底部面板默认通栏并保持活动栏通高 - 终端/底部面板默认宽度模式由 editor 改为 full,与 IDEA 默认一致 - 两条活动栏提到最外层保持通高,通栏面板只位于活动栏之间, 左侧底部按钮不再被面板顶上去;editor 模式布局不变 - 新增 terminal.store 默认值与切换的单测 - 更新 terminal-sessions 矩阵验证描述并重新生成矩阵视图 * feat(windows): 面板去描边、侧边栏间距 6px、Git Log 统一底色 - 侧边栏、编辑区、底部面板去掉外圈描边,仅靠内容区/间隔区纯色区分 - 新增 --lithe-sidebar-gap(6px),左侧侧边栏与编辑区间距及其拖动条使用该值 - Git Log 标题栏、提交表、检查器、引用树的灰色/半透明背景统一为 bg-background, 与编辑区底色一致 - 同步更新 workbench-panel-frame 测试断言,并修正标题栏类名的过期断言 * fix(windows): Commit files 文件名不再因末尾计数/状态撑开而提前截断 - file-tree-row 的 span:last-child 规则会让末尾的计数/状态 trailing 也参与 剩余宽度分配,与文件名平分空间,导致右侧留白而文件名出现省略号 - 仅对 Commit files 树新增 git-commit-file-tree 作用域规则,使 trailing 不再伸展, 文件夹/文件名占满剩余宽度;文件浏览器等其他树不受影响 - 新增回归测试 * feat(windows): Git Log 单击提交/文件在编辑区预览单文件 diff - 单击提交后编辑区预览该提交文件树中的第一个文件 diff;仅用户点击触发, 刚打开 Git Log 不会自动抢占编辑区 - 单击或双击 Commit files 中的文件只显示该文件 diff,不带提交说明, 不再打开整个提交的多文件页面 - 所有预览复用同一个 diff://commit-file-preview 标签,快速连点时丢弃过期请求 - MultiFileDiff 新增 hideFileList,单文件预览不显示已更改文件列表及其切换按钮 - 检查器记录文件所属的选择,避免新提交加载期间误用上一提交的文件做预览 - 新增 getFirstCommitFilePath、findCommitFileDiff、createCommitFileDiffPreview 及测试 - 更新 git-history 矩阵验证描述并重新生成矩阵视图 * feat(windows): 右上角 Extensions 按钮再次点击可关闭扩展页 Extensions 标签页为当前活动标签时,点击右侧活动栏的扩展按钮会关闭该标签页; 未打开或位于后台时仍按原行为打开/切换。同步更新源码断言测试。 * fix(windows): 面板分隔条去掉蓝色高亮,左右光标改为 ew-resize - 活动栏、侧边栏/AI 面板、底部面板、编辑区分屏、文件导航侧边栏的分隔条 悬停与拖动时不再显示蓝色背景和蓝色竖线,保留调整光标与拖拽行为。 - 左右分隔条光标由 col-resize 改为 ew-resize(上下分屏 row-resize 改为 ns-resize),避免 Windows 上箭头中间出现竖线,并与底部面板保持一致。 * feat(windows): Source Control 无改动时不再显示 Working tree clean 提示 工作区干净时侧边栏保持空白,移除空状态的勾选图标与文字, 并清理不再使用的 git.workingTreeClean 文案。 * feat(windows): Commit files 去掉 A/M 状态字母,文件数量移到文件夹名旁 - 文件行右侧不再显示 A/M/D 状态字母(文件名颜色仍保留状态区分) - 文件夹的改动文件数量从右侧移到文件夹名旁,并附带 file/files 单位 - 新增 git.log.fileCountOne 文案处理单数;中文保持“N 个文件” - 移除仅为右侧 trailing 列存在的 :last-child CSS 覆盖,并同步更新测试 * feat(windows): Git Log 去掉 Commit/Author/Date 表头行 - 移除提交列表上方的列标题行 - 删除不再使用的 git.log.commit / git.log.author / git.log.date 文案 * feat(windows): Git Log 日期列改为 12 小时制并显示相对时间(参考 IDEA) - 1 小时内的提交显示 "N minutes ago"(不足 1 分钟显示 "Just now") - 今天和昨天的提交显示 "Today / Yesterday + h:mm AM/PM" - 更早的提交显示 "YYYY/MM/DD h:mm AM/PM",不再使用 24 小时制 - 新增 git.log.date* 中英文文案;日期列加宽以容纳 AM/PM - 新增 formatGitLogDate 及单元测试,无法解析的日期原样显示 * feat(windows): Git Log 英文日期改为日/月/年,作者和日期列样式与提交说明对齐 - 较早提交的日期顺序由文案决定:英文 DD/MM/YYYY,中文 YYYY/MM/DD - 作者和日期列去掉等宽字体与 11px 字号,颜色改为 text-foreground,与提交说明一致 * feat(windows): Git Log 的 Author/Date 列支持在每一行的列边缘拖动调整宽度 - 表头去掉后,每一行的 Author、Date 列左边缘都有拖动手柄,拖动即可调整整列宽度 - 宽度通过滚动容器上的 CSS 变量下发:拖动中按帧直接写 DOM,不触发虚拟列表重渲染, 松手后才写入 git-log-preferences 持久化,下次打开沿用 - 列宽有最小/最大限制,行最小宽度随列宽动态计算,避免提交说明被挤没 - 光标使用 ew-resize,无蓝色高亮,与其他面板分隔条一致;拖动遮罩防止松手触发行选中 - startDocumentResizeSession 挪到 src/utils 共享(run 侧保留 re-export), 新增 direction(反向拖动)和 cursor 选项 - 新增列宽钳制、拖动会话选项、拖动手柄交互(happy-dom)的单元测试 * feat(windows): Git Log 的 Date 列改为左对齐,左内边距与 Author 列一致 * fix(windows): Git Log 列拖动手柄向列内侧扩展,贴近文字起点 * feat(git): Git Log 仅对同时区提交显示相对时间,中文 12 小时制显示上午/下午,鼠标移入行时刷新 - Core 提交日期改用 --date=format:%Y/%m/%d %H:%M %z,parse_commit 拆出作者 UTC 偏移, 新增 dateUtcOffsetMinutes 字段(date 显示格式不变),更新契约文档与共享 fixture - Windows 仅在作者时区与本机一致时显示 刚刚/N 分钟前/今天/昨天,否则显示日期 - AM/PM 进入 i18n,中文显示 上午/下午 - 日期单元格在鼠标移入所在行时刷新相对时间 * refactor(windows): 移除 Commit files 树上未使用的 git-commit-file-tree 类名 * test(windows): Git Log 列宽拖动测试改用注入的帧调度器,移除真实定时器 CI 的 write-stable-tests 检查(--platform windows/all)拒绝测试中的真实 setTimeout。 useGitLogColumnResize 新增可选 frameScheduler 参数透传给拖动会话,测试用手动帧队列确定性地刷新宽度。 * fix(windows): preserve Git Log preview revisions and invalidate stale requests --------- Co-authored-by: lick <2188718831@qq.com> --- ...-13-resizable-ui-performance-boundaries.md | 20 ++- .github/workflows/ci-windows.yml | 9 + docs/development/platform-parity-matrix.csv | 5 +- docs/development/platform-parity-matrix.md | 13 +- rust/lithe-core/src/git/graph.rs | 1 + rust/lithe-core/src/git/history.rs | 7 +- rust/lithe-core/src/git/mod.rs | 60 ++++++- rust/lithe-core/src/protocol/contracts.rs | 5 + scripts/verify-windows-boundaries.sh | 5 +- shared/contracts/rust-core-api.md | 2 +- .../git/history-page-response-v1.json | 1 + shared/fixtures/git/history-response-v1.json | 1 + shared/fixtures/git/push-preview-v1.json | 1 + shared/platform-feature-matrix.json | 33 +++- .../components/file-navigator-sidebar.tsx | 6 +- .../components/diff/git-diff-editor-stack.tsx | 5 +- .../log/git-commit-file-tree.test.ts | 31 +++- .../components/log/git-commit-file-tree.tsx | 34 +++- .../components/log/git-commit-inspector.tsx | 42 ++++- .../git/components/log/git-commit-table.tsx | 39 +++-- .../log/git-log-column-resize-handle.tsx | 33 ++++ .../components/log/git-log-date-cell.test.tsx | 57 +++++++ .../git/components/log/git-log-date-cell.tsx | 37 ++++ .../git/components/log/git-log-title-bar.tsx | 2 +- .../components/log/git-log-tool-window.tsx | 10 ++ .../git/components/log/git-reference-tree.tsx | 6 +- .../components/status/git-status-panel.tsx | 11 +- .../hooks/use-commit-file-preview.test.tsx | 161 ++++++++++++++++++ .../git/hooks/use-commit-file-preview.ts | 79 +++++++++ .../git/hooks/use-git-diff-actions.ts | 10 ++ .../hooks/use-git-log-column-resize.test.tsx | 155 +++++++++++++++++ .../git/hooks/use-git-log-column-resize.ts | 84 +++++++++ .../git/stores/git-log-preferences.store.ts | 23 +++ .../src/features/git/types/git-diff.types.ts | 2 + .../tauri/src/features/git/types/git.types.ts | 2 + .../git/utils/git-log-columns.test.ts | 25 +++ .../src/features/git/utils/git-log-columns.ts | 41 +++++ .../features/git/utils/git-log-date.test.ts | 79 +++++++++ .../src/features/git/utils/git-log-date.ts | 65 +++++++ .../git/utils/multi-file-diff.test.ts | 56 +++++- .../src/features/git/utils/multi-file-diff.ts | 51 ++++++ .../defaults/default-keymaps.git-log.test.ts | 5 +- .../components/bottom-pane/bottom-pane.tsx | 17 +- .../layout/components/main-layout.tsx | 85 ++++----- .../components/plugin-activity-rail.test.ts | 7 +- .../components/plugin-activity-rail.tsx | 23 ++- .../layout/components/resizable-pane.tsx | 25 +-- .../components/sidebar/main-sidebar.tsx | 14 +- .../sidebar/sidebar-pane-selector.tsx | 19 --- .../components/workbench-panel-frame.test.ts | 18 +- .../features/layout/config/item-order.test.ts | 13 +- .../src/features/layout/config/item-order.ts | 4 +- .../panes/components/pane-resize-handle.tsx | 15 +- .../run-configuration-list-resize-session.ts | 107 +----------- .../terminal/stores/terminal.store.test.ts | 12 ++ .../terminal/stores/terminal.store.ts | 8 +- .../window/components/title-bar/title-bar.tsx | 17 ++ windows/tauri/src/i18n/locale.ts | 30 +++- .../tauri/src/platform/core-result-adapter.ts | 8 + windows/tauri/src/styles/theme.css | 2 + .../src/utils/document-resize-session.test.ts | 78 +++++++++ .../src/utils/document-resize-session.ts | 109 ++++++++++++ 62 files changed, 1597 insertions(+), 328 deletions(-) create mode 100644 windows/tauri/src/features/git/components/log/git-log-column-resize-handle.tsx create mode 100644 windows/tauri/src/features/git/components/log/git-log-date-cell.test.tsx create mode 100644 windows/tauri/src/features/git/components/log/git-log-date-cell.tsx create mode 100644 windows/tauri/src/features/git/hooks/use-commit-file-preview.test.tsx create mode 100644 windows/tauri/src/features/git/hooks/use-commit-file-preview.ts create mode 100644 windows/tauri/src/features/git/hooks/use-git-log-column-resize.test.tsx create mode 100644 windows/tauri/src/features/git/hooks/use-git-log-column-resize.ts create mode 100644 windows/tauri/src/features/git/utils/git-log-columns.test.ts create mode 100644 windows/tauri/src/features/git/utils/git-log-columns.ts create mode 100644 windows/tauri/src/features/git/utils/git-log-date.test.ts create mode 100644 windows/tauri/src/features/git/utils/git-log-date.ts create mode 100644 windows/tauri/src/features/terminal/stores/terminal.store.test.ts create mode 100644 windows/tauri/src/utils/document-resize-session.test.ts create mode 100644 windows/tauri/src/utils/document-resize-session.ts diff --git a/.agents/notes/implemented/architecture/2026-09-13-resizable-ui-performance-boundaries.md b/.agents/notes/implemented/architecture/2026-09-13-resizable-ui-performance-boundaries.md index 3a8b796ff..fed9b5542 100644 --- a/.agents/notes/implemented/architecture/2026-09-13-resizable-ui-performance-boundaries.md +++ b/.agents/notes/implemented/architecture/2026-09-13-resizable-ui-performance-boundaries.md @@ -8,7 +8,7 @@ 的布局组件,不要在业务页面里重新拼一套拖动逻辑。拖动过程中要保证界面 稳定、面板不会缩成不可用的尺寸,也不能因为鼠标移动就重建整个页面。 macOS 和 Windows 可以用不同的 UI 技术实现,但用户看到的交互行为和基本 -约束必须一致。 +约束必须一致。Windows 分隔条使用无高亮的间隙和调整光标,macOS 保留原生风格高亮;这项视觉差异不改变尺寸与性能约束。 ## 问题 @@ -32,8 +32,13 @@ macOS 和 Windows 可以用不同的 UI 技术实现,但用户看到的交互 中,避免每次拖动都重新计算整个功能页面。 - 为面板设置最小尺寸、最大尺寸和可用空间约束。窗口缩放、面板隐藏和 面板恢复后,都不能产生负尺寸或布局溢出。 -- 交互应与现有 Git、编辑器和工具窗口保持一致,包括悬停/拖动高亮、 - 平台习惯的调整光标、帮助文本和无障碍标签。 +- 同一平台的分隔条交互应保持一致。Windows 工作台、文件导航和 Git Log 列边缘使用 + 无高亮间隙以及 `ew-resize` / `ns-resize` 光标,避免连续面板边界出现额外蓝色线条; + macOS 的 `SplitHandleView` 保留悬停/拖动高亮。两端仍须保留帮助文本、无障碍标签、 + 尺寸限制和拖动结束后持久化。不要为了视觉统一删除这些交互约束。 +- Windows Git Log 列宽复用 `utils/document-resize-session.ts`,每帧只写局部 CSS 变量, + 结束或卸载时提交一次宽度并释放监听。列的最小和最大宽度限制后,空间不足通过 + 表格横向滚动处理,不挤掉提交说明。 - 如果尺寸需要在重启后保留,只在拖动结束时通过现有布局持久化机制保存 最终尺寸,不要在拖动过程中持续写入持久化存储。 - Windows 编辑器标签栏的滚轮处理和活动标签可见性判断必须使用内层可滚动 @@ -78,6 +83,11 @@ macOS 和 Windows 可以用不同的 UI 技术实现,但用户看到的交互 外框还包含固定操作按钮,其宽度不等于标签列表可视宽度。读取外框的滚动 范围会漏掉内层溢出;因此保留外框用于拖动命中判断,滚动使用独立的内层引用。 +### 强制两端使用同一种分隔条高亮 + +不采用。两端的窗口呈现方式不同,Windows 无描边面板依靠间隙和调整光标识别边界, +macOS 继续使用既有原生风格。视觉差异仅限这些分隔条,不放宽生命周期、性能或可访问性约束。 + ## 后果 - 收益:拖动行为更稳定,面板不会轻易出现负尺寸或溢出,页面重建范围更小。 @@ -94,7 +104,9 @@ macOS 和 Windows 可以用不同的 UI 技术实现,但用户看到的交互 - Windows 标签滚轮行为测试: `cd windows/tauri && bun test src/features/tabs/hooks/use-tab-wheel-scroll.test.tsx` Windows CI 显式运行整个 `src/features/tabs` 测试目录,并生成逐项计时报告。 -- 代码审查时确认拖动不会导致高频整页重绘 +- Git Log 列宽、日期和布局测试由 Windows CI 的 Git Log 验证步骤运行并输出逐项计时报告。 +- 代码审查时确认拖动不会导致高频整页重绘;Windows 人工验证间隙无高亮、光标正确, + macOS 人工确认 `SplitHandleView` 高亮保持不变。 ## 适用范围 diff --git a/.github/workflows/ci-windows.yml b/.github/workflows/ci-windows.yml index 3f1ec085a..34e2b1806 100644 --- a/.github/workflows/ci-windows.yml +++ b/.github/workflows/ci-windows.yml @@ -280,6 +280,15 @@ jobs: node $runner --suite-timeout-ms 60000 --report .artifacts/test-stability/windows-git-execution-journal.json -- src/features/git/services/git-execution-journal.test.ts if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + - name: Test Git Log previews, dates, columns, and workbench layout + shell: pwsh + run: | + $runner = ".agents/skills/write-stable-tests/scripts/run-bun-tests-with-timing.mjs" + node $runner --suite-timeout-ms 60000 --report .artifacts/test-stability/windows-git-log-preview.json -- src/features/git/hooks/use-commit-file-preview.test.tsx + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + node $runner --suite-timeout-ms 60000 --report .artifacts/test-stability/windows-git-log-layout.json -- src/features/git/hooks/use-git-log-column-resize.test.tsx src/features/git/components/log/git-log-date-cell.test.tsx src/features/git/components/log/git-commit-file-tree.test.ts src/features/git/utils/git-log-columns.test.ts src/features/git/utils/git-log-date.test.ts src/features/git/utils/multi-file-diff.test.ts src/utils/document-resize-session.test.ts src/features/layout/components/plugin-activity-rail.test.ts src/features/layout/components/workbench-panel-frame.test.ts src/features/layout/config/item-order.test.ts src/features/keymaps/defaults/default-keymaps.git-log.test.ts src/features/terminal/stores/terminal.store.test.ts + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + - name: Test workspace Git commit adapters and staging shell: pwsh run: | diff --git a/docs/development/platform-parity-matrix.csv b/docs/development/platform-parity-matrix.csv index 767f5eb16..28efd0fc9 100644 --- a/docs/development/platform-parity-matrix.csv +++ b/docs/development/platform-parity-matrix.csv @@ -33,7 +33,7 @@ git-status-commit,版本控制,Git,状态、暂存与提交,已实现,待验证, git-multi-repository-change-groups,版本控制,Git,多仓库变更折叠分组,已实现,待验证,已实现,待验证,Git,在同一工作区打开多个 Git 仓库,确认变更按仓库折叠分组;只剩一个仓库有变更时仍显示仓库名。仓库级和文件级勾选与 Git 暂存区同步;子模块只有未提交文件时显示提示,不能勾选未变化的引用。,两端直接消费共享 Rust Core 的计划、依赖排序、执行结果与重试。平台负责真实暂存勾选、确认与结果界面、认证取消和项目生命周期。原生界面实测待平台验收。,macos/Sources/Lithe/Views/Git/ChangesSidebarView.swift; macos/Sources/Lithe/Views/Git/GitChangeSectionsCache.swift; macos/Sources/LitheGitModule/Models/GitModels.swift; rust/lithe-core/src/git/mod.rs,windows/tauri/src/features/git/components/status/git-status-panel.tsx; windows/tauri/src/features/git/components/git-commit-panel.tsx; windows/tauri/src/features/git/components/git-workspace-commit-review.tsx; windows/tauri/src/features/git/services/git-workspace-commit-workflow.ts; windows/tauri/src/features/git/services/git-workspace-commit-workflow.test.ts; windows/tauri/src/features/git/components/status/git-workspace-status-panel.test.tsx; windows/tauri/src-tauri/src/platform.rs; shared/fixtures/git/workspace-commit-workflow-v1.json git-branches-remotes,版本控制,Git,分支、标签与远程,已实现,待验证,已实现,待验证,Git,创建、切换、合并分支并查看标签和远程,确认冲突与认证失败可恢复。,,macos/Sources/Lithe/Views/Git,windows/tauri/src/features/git git-diff-review,版本控制,Git,Diff 与变更审查,已实现,待验证,已实现,待验证,Git,验证新增、删除、重命名、二进制和多文件 Diff 的展示与定位;从源代码管理打开已修改和未跟踪文件的工作区 Diff 后保持静止,确认 Diff 不会自动关闭,且只在文件不再出现在 Git 状态中时关闭;在 Windows 单文件工作区 Diff 中点击折叠条展开省略的未改动代码,确认 Diff 保持打开且每个修改块仍可单独暂存。,,macos/Sources/Lithe/Views/Git; macos/Sources/Lithe/Views/Diff,windows/tauri/src/features/git; windows/tauri/src/features/viewer -git-history,版本控制,Git,提交历史与图谱,已实现,待验证,已实现,待验证,Git,分页浏览提交历史、分支图谱和提交详情,确认日期、作者和文件列表一致;分支最新提交的图谱连线从提交圆点开始,不超出到圆点上方;在双端使用同一包含本地分支、远端引用、标签和合并提交的仓库,核对永久图布局、图头引用排序、合并边投影及跨越 30 行的紧凑长边;在本地分支新增提交后确认分支颜色保持稳定,且不会因共用屏幕泳道直接沿用父分支颜色。双端保留各自调色板,不要求 RGB 值一致。,,macos/Sources/Lithe/Views/Git; macos/Sources/LitheGitModule/Services/GitGraphLayoutService.swift; macos/Sources/LitheGitModule/Services/GitGraphHeadOrdering.swift; macos/Sources/Lithe/Views/Git/GitGraphColor.swift,windows/tauri/src/features/git; windows/tauri/src/features/git/utils/git-graph-layout.ts; windows/tauri/src/features/git/utils/git-graph-colors.ts; windows/tauri/src/features/git/utils/git-graph-layout.test.ts +git-history,版本控制,Git,提交历史与图谱,已实现,待验证,已实现,待验证,Git,分页浏览提交历史、分支图谱和提交详情,确认日期、作者和文件列表一致;分支最新提交的图谱连线从提交圆点开始,不超出到圆点上方;在双端使用同一包含本地分支、远端引用、标签和合并提交的仓库,核对永久图布局、图头引用排序、合并边投影及跨越 30 行的紧凑长边;在本地分支新增提交后确认分支颜色保持稳定,且不会因共用屏幕泳道直接沿用父分支颜色。双端保留各自调色板,不要求 RGB 值一致。Windows 另验证:单击提交后编辑区预览该提交第一个文件的 diff;单击或双击 Commit files 中的文件只显示该文件的 diff(不含提交说明,也不显示文件列表),并复用同一个预览标签;Git Log 刚打开、尚未点击提交时不会自动打开 diff;日期列对与本机同一时区的提交显示相对时间(刚刚、N 分钟前、今天/昨天 + 12 小时制,中文为上午/下午),其他时区或未知时区的提交只显示日期,鼠标移入行时刷新相对时间;作者/日期列可在行内列边缘拖动调整宽度并在重启后保留。 非连续多选提交修改同一路径时,预览保留每个提交的 diff 区段及提交标识;延迟请求返回前取消选择、隐藏 Git Log、切换仓库或卸载工具窗,确认旧请求不会重新打开或激活预览标签。,,macos/Sources/Lithe/Views/Git; macos/Sources/LitheGitModule/Services/GitGraphLayoutService.swift; macos/Sources/LitheGitModule/Services/GitGraphHeadOrdering.swift; macos/Sources/Lithe/Views/Git/GitGraphColor.swift,windows/tauri/src/features/git; windows/tauri/src/features/git/utils/git-graph-layout.ts; windows/tauri/src/features/git/utils/git-graph-colors.ts; windows/tauri/src/features/git/utils/git-graph-layout.test.ts; windows/tauri/src/features/git/hooks/use-commit-file-preview.ts; windows/tauri/src/features/git/hooks/use-commit-file-preview.test.tsx git-rebase-stash,版本控制,Git,Rebase 与 Stash,已实现,待验证,已实现,待验证,Git,执行交互式 Rebase 和 Stash 保存/恢复,确认中断、冲突和继续操作。,,macos/Sources/Lithe/Views/Git,windows/tauri/src/features/git git-worktrees,版本控制,Git,Worktree 管理,已实现,待验证,已实现,待验证,Git,列出、创建、切换和删除 Worktree,确认路径、分支和安全检查。,,macos/Sources/Lithe/Views/Git,windows/tauri/src/features/git git-multi-repository-references,版本控制,Git,多仓库引用面板:分组、配色与引用操作,已实现,待验证,已实现,待验证,Git,在多仓库工作区打开 Git Log:确认按仓库分组、仓库配色、非活动仓库分组只读,点其它仓库的引用会切换活动仓库且只加载一次,Pull 弹窗可选择远程分支与策略。,,macos/Sources/Lithe/Views/Git,windows/tauri/src/features/git @@ -65,7 +65,7 @@ run-java-long-classpath,运行与调试,运行配置,超长 Java 类路径自动 debug-breakpoints,运行与调试,调试器,启动调试与断点,已实现,待验证,部分实现,待验证,Debug,设置、命中、禁用和重新定位断点,确认调试会话生命周期。,Windows 真实调试产品链路仍在 #466 跟进。,macos/Sources/Lithe/Views/Debug; shared/fixtures/debug,windows/tauri/src/features/debugger; shared/fixtures/debug debug-state,运行与调试,调试器,变量、异常与断开策略,已实现,待验证,部分实现,待验证,Debug,验证变量分页、异常信息、step filters、暂停/继续和 disconnect policy。,Windows 真实调试产品链路仍在 #466 跟进。,macos/Sources/Lithe/Views/Debug; shared/fixtures/debug,windows/tauri/src/features/debugger; shared/fixtures/debug terminal-shell,工作台,终端,Shell 发现与配置,已实现,待验证,已实现,待验证,Terminal,验证设置页默认 Shell 选项与终端「新建终端」菜单检测到的 Shell 一致、可选 Git Bash 并生效,以及环境变量、工作目录和不可用 Shell 的提示。,,macos/Sources/Lithe/Views/Terminal; macos/Sources/Lithe/Views/App/SettingsView.swift; macos/Sources/Lithe/Services,windows/tauri/src/features/terminal; windows/tauri/src/features/settings/components/macos-settings-panels.tsx; windows/tauri/src/features/settings/lib/default-shell-options.ts; windows/tauri/src-tauri -terminal-sessions,工作台,终端,多会话与标签,已实现,待验证,已实现,待验证,Terminal,创建多个终端会话并切换、重命名、关闭,确认子进程生命周期和资源清理;Windows 横向标签栏的新建终端与 Shell 配置按钮紧跟最后一个标签,标签溢出时按钮保持可见;标签栏右侧最小化按钮隐藏终端面板且会话继续运行,重新打开时恢复为停靠状态。,,macos/Sources/Lithe/Views/Terminal,windows/tauri/src/features/terminal +terminal-sessions,工作台,终端,多会话与标签,已实现,待验证,已实现,待验证,Terminal,创建多个终端会话并切换、重命名、关闭,确认子进程生命周期和资源清理;Windows 横向标签栏的新建终端与 Shell 配置按钮紧跟最后一个标签,标签溢出时按钮保持可见;标签栏右侧最小化按钮隐藏终端面板且会话继续运行,重新打开时恢复为停靠状态;Windows 打开底部终端面板时默认通栏(与 IDEA 一致):面板位于左右两条活动栏之间并横跨侧边栏与编辑器下方(活动栏保持通高,底部的活动栏按钮位置不上移),侧边栏让出纵向空间,且仍可通过面板菜单切换为编辑器宽度。,,macos/Sources/Lithe/Views/Terminal,windows/tauri/src/features/terminal terminal-interaction,工作台,终端,复制粘贴、搜索与调整大小,已实现,待验证,已实现,待验证,Terminal,验证复制粘贴、终端搜索、调整面板大小和高频输入不丢失。,,macos/Sources/Lithe/Views/Terminal,windows/tauri/src/features/terminal terminal-long-command-input,工作台,终端,Windows Shell 超长命令输入,平台专属,不适用,已实现,待验证,Terminal,在 Windows cmd.exe 和 PowerShell 中执行超过 Shell 单行限制的 IDE 多行命令,确认脚本执行、原始命令内容和终端关闭后的临时文件清理。,仅在 IDE 后端为 cmd.exe/PowerShell 的超长多行文本创建一次性脚本;不修改系统 Shell 配置。,macos/Sources/Lithe/Views/Terminal,windows/tauri/crates/terminal/src/connection.rs; windows/tauri/src-tauri/src/terminal.rs markdown-rendering,编辑器,Markdown,Markdown 预览与富文本渲染,已实现,待验证,已实现,待验证,Editor,使用代码高亮、表格、Mermaid、链接和相对路径 fixture 对比渲染结果。,,macos/Sources/Lithe/Views/Editor/MarkdownPreviewView.swift; macos/Sources/Lithe/Core/Ports/MarkdownRendering.swift,windows/tauri/src/features/editor/markdown @@ -88,6 +88,7 @@ plugin-extension-management,扩展,插件与主题,插件、语言服务与主 onboarding-welcome,启动与窗口,首次启动,欢迎页、项目入口与首次启动引导,已实现,待验证,已实现,待验证,Onboarding,清空首次启动状态,验证打开项目、克隆项目、最近项目和跳过引导;在 macOS 深色与浅色模式检查欢迎页布局、颜色和交互,确认默认及最小内容尺寸均为 800×650、可向屏幕可用区域放大,左下角齿轮菜单采用设置页下拉框配色并可进入设置与检查更新。,,macos/Sources/Lithe/Views/App/WelcomeView.swift; macos/Sources/Lithe/Views/Workspace,windows/tauri/src/features/onboarding; windows/tauri/src/features/bootstrap command-palette,工作台,命令与布局,命令面板与全局动作,已实现,待验证,已实现,待验证,Workbench,搜索并执行打开面板、运行、Git 和设置命令,确认快捷键和不可用命令状态。,,macos/Sources/Lithe/Models/Keymap/LitheCommandCatalog.swift; macos/Sources/Lithe/Views/Search/SearchEverywhereView.swift,windows/tauri/src/features/command-palette workbench-compact-menu-bar,工作台,命令与布局,紧凑菜单栏可在标题栏左侧原位展开完整水平菜单,并在点击外部或按 Escape 后收起,不遮挡工作区,平台专属,不适用,已实现,待验证,Workbench,在 Windows 工作台点击标题栏左端菜单按钮,确认汉堡按钮由“文件”菜单项取代,“文件”项处于选中状态且其下拉菜单立即展开;菜单与标题栏同高,不覆盖编辑区域,项目/分支控件在展开时隐藏。检查常驻品牌标识与下方活动栏图标列对齐且不可选中,并确认项目菜单显示当前项目图标(无自定义图标时显示项目缩写)。点击其他菜单项时菜单条保持展开并执行动作;点击工作区、标题栏空白和右侧控件或按 Escape 后收起;按 Escape 后确认焦点返回菜单入口;窄窗口确认菜单在标题栏内水平滚动且不挤压窗口控件。macOS 保持系统菜单栏行为。,,macos/Sources/Lithe/LitheApp.swift,windows/tauri/src/features/window/components/title-bar/title-bar.tsx; windows/tauri/src/features/window/components/title-bar/title-project-menu.tsx; windows/tauri/src/features/window/components/window-menu-bar.tsx; windows/tauri/src/features/window/hooks/use-compact-menu-bar-dismissal.ts; windows/tauri/src/features/window/hooks/use-compact-menu-bar-dismissal.test.tsx +workbench-settings-entry,工作台,命令与布局,从工作台 chrome 打开设置:Windows 点击标题栏搜索按钮右侧的齿轮,macOS 通过应用菜单及面板内入口,已实现,待验证,已实现,待验证,Workbench,在 Windows 打开工作区,确认左侧活动栏底部不再显示设置齿轮,活动栏右键“在活动栏中显示”列表不包含设置项;点击标题栏右上搜索按钮右侧的齿轮打开设置对话框并再次关闭;携带旧版设置文件(活动栏排序或隐藏列表含 settings)启动后确认遗留条目被清理且活动栏显示正常。macOS 通过应用菜单和各面板入口打开设置保持不变。,,macos/Sources/Lithe/Models/LitheAction.swift; macos/Sources/Lithe/Views/App/SettingsView.swift,windows/tauri/src/features/window/components/title-bar/title-bar.tsx; windows/tauri/src/features/layout/components/sidebar/sidebar-pane-selector.tsx; windows/tauri/src/features/layout/config/item-order.ts window-controls,工作台,窗口管理,标题栏控制窗口最小化、最大化/还原与关闭;命令面板可最小化、最大化及切换全屏,已实现,待验证,已实现,待验证,Workbench,在 macOS 检查系统标题栏控制及全屏入口;在 Windows 验证标题栏最小化、最大化/还原和关闭,以及命令面板的窗口操作,确认图标语义、最大化/还原状态与按钮标签一致,并确认悬停关闭按钮背景贴合窗口右边缘。,,macos/Sources/Lithe/LitheApp.swift; macos/Sources/Lithe/Views/App/RootView.swift,windows/tauri/src/features/window/components/title-bar/window-controls.tsx; windows/tauri/src/features/command-palette/constants/window-actions.tsx; windows/tauri/src/features/window/utils/window-actions.ts workbench-panes,工作台,命令与布局,分栏、面板与工具窗布局,已实现,待验证,已实现,待验证,Workbench,打开、关闭、移动和调整面板,验证布局持久化及高频拖动稳定性;在 macOS 启用背景图后检查项目栏、编辑器、底部及右侧工具窗之间是否仍有清晰的分割区间;将 macOS 左侧工具窗缩至 30pt 并确认目录树始终贴左、不随宽度向右移动、内容不溢出、右上和右下圆角仍可见,关闭并重新打开项目后仍保持窄侧栏及其他布局尺寸,重新展开后布局正常;在 Maven 项目产生构建输出时确认左侧模块按钮可滚动、设置按钮始终完整显示;在亮色主题且有未读通知时确认未打开的铃铛图标清晰可见、打开通知面板后白色图标与蓝色背景成对出现;在深色 Lithe 主题下对照外框渐变、面板对比度、背景图片回退、左右工具窗的 40pt 按钮槽、30pt 选中块及 IDEA Community 图标对比度,悬停工具窗按钮时确认箭头光标,检查项目栏标题颜色和弹窗主按钮主题色;检查 macOS 40pt 顶栏 Logo 与系统窗口按钮垂直居中,项目切换器使用 20pt 项目字母头像和 30pt 悬停块,项目与分支切换块悬停保持箭头光标,红绿灯右侧留白与 IDEA 接近,项目头像、欢迎列表及切换菜单按标准化项目路径共享稳定颜色(超过 9 个项目、移除后重开仍一致),顶栏使用 IDEA Islands 主题项目色并按 85% 混色、600pt 水平和 300pt 垂直范围渐变,深色窗口分隔条四角与底栏缺口无浅色亮斑,缩放窗口及从欢迎页进入工作区再返回后仍对齐。,,macos/Sources/Lithe/Views/Workbench; macos/Sources/Lithe/Views/Workspace/ProjectSidebarView.swift; macos/Sources/Lithe/Views/Components/LitheSplitPaneView.swift; macos/Sources/Lithe/Services/Workbench/WorkbenchLayoutStore.swift; macos/Sources/Lithe/Views/App/RootView.swift; macos/Sources/Lithe/LitheApp.swift; macos/Sources/Lithe/Theme/LitheTheme.swift; macos/Resources/IDEAIcons/expui; macos/Resources/IDEAIcons/terminal,windows/tauri/src/features/panes; windows/tauri/src/features/layout workbench-activity-tooltips,工作台,工作台布局,活动栏图标的稳定悬停说明,已实现,待验证,已实现,待验证,Workbench,逐一悬停左上导航、左下工具入口及右侧通知/插件/Maven,快速切换图标并移出活动栏;确认始终只显示当前图标说明,面板更新和滚动后不残留旧提示,禁用入口可显示说明,短文字自适应且提示不超出窗口边缘。,,macos/Sources/Lithe/Views/Workbench/WorkbenchHoverTooltip.swift; macos/Sources/Lithe/Views/Workbench/WorkbenchView.swift; macos/Tests/LitheTests/WorkbenchHoverTooltipTests.swift,windows/tauri/src/features/layout/components/sidebar/sidebar-pane-selector.tsx; windows/tauri/src/features/layout/components/footer/footer-tab-control.tsx; windows/tauri/src/features/layout/components/plugin-activity-rail.tsx diff --git a/docs/development/platform-parity-matrix.md b/docs/development/platform-parity-matrix.md index 37a0ad01b..27818a50e 100644 --- a/docs/development/platform-parity-matrix.md +++ b/docs/development/platform-parity-matrix.md @@ -4,9 +4,9 @@ - 最后复核:2026-09-29 - 盘点状态:initial-static-inventory(根据 macOS Views/Application/Services、Windows features/extensions 和共享契约的代码入口进行初版盘点;未替代真实运行验收。) -- 功能项:116 -- macOS:实现:✅ 102 已实现,🟡 3 部分实现,❌ 7 未实现,🧩 4 平台专属;验证:✔️ 0 已验证,🔍 105 待验证,— 11 不适用 -- Windows:实现:✅ 98 已实现,🟡 11 部分实现,❌ 5 未实现,🧩 2 平台专属;验证:✔️ 0 已验证,🔍 109 待验证,— 7 不适用 +- 功能项:117 +- macOS:实现:✅ 103 已实现,🟡 3 部分实现,❌ 7 未实现,🧩 4 平台专属;验证:✔️ 0 已验证,🔍 106 待验证,— 11 不适用 +- Windows:实现:✅ 99 已实现,🟡 11 部分实现,❌ 5 未实现,🧩 2 平台专属;验证:✔️ 0 已验证,🔍 110 待验证,— 7 不适用 ## 实现状态定义 @@ -116,7 +116,7 @@ | Git | **多仓库变更折叠分组**
git-multi-repository-change-groups | ✅ 已实现
🔍 待验证
`macos/Sources/Lithe/Views/Git/ChangesSidebarView.swift`、`macos/Sources/Lithe/Views/Git/GitChangeSectionsCache.swift`、`macos/Sources/LitheGitModule/Models/GitModels.swift`、`rust/lithe-core/src/git/mod.rs` | ✅ 已实现
🔍 待验证
`windows/tauri/src/features/git/components/status/git-status-panel.tsx`、`windows/tauri/src/features/git/components/git-commit-panel.tsx`、`windows/tauri/src/features/git/components/git-workspace-commit-review.tsx`、`windows/tauri/src/features/git/services/git-workspace-commit-workflow.ts`、`windows/tauri/src/features/git/services/git-workspace-commit-workflow.test.ts`、`windows/tauri/src/features/git/components/status/git-workspace-status-panel.test.tsx`、`windows/tauri/src-tauri/src/platform.rs`、`shared/fixtures/git/workspace-commit-workflow-v1.json` | Git | 在同一工作区打开多个 Git 仓库,确认变更按仓库折叠分组;只剩一个仓库有变更时仍显示仓库名。仓库级和文件级勾选与 Git 暂存区同步;子模块只有未提交文件时显示提示,不能勾选未变化的引用。 | 两端直接消费共享 Rust Core 的计划、依赖排序、执行结果与重试。平台负责真实暂存勾选、确认与结果界面、认证取消和项目生命周期。原生界面实测待平台验收。 | | Git | **分支、标签与远程**
git-branches-remotes | ✅ 已实现
🔍 待验证
`macos/Sources/Lithe/Views/Git` | ✅ 已实现
🔍 待验证
`windows/tauri/src/features/git` | Git | 创建、切换、合并分支并查看标签和远程,确认冲突与认证失败可恢复。 | | | Git | **Diff 与变更审查**
git-diff-review | ✅ 已实现
🔍 待验证
`macos/Sources/Lithe/Views/Git`、`macos/Sources/Lithe/Views/Diff` | ✅ 已实现
🔍 待验证
`windows/tauri/src/features/git`、`windows/tauri/src/features/viewer` | Git | 验证新增、删除、重命名、二进制和多文件 Diff 的展示与定位;从源代码管理打开已修改和未跟踪文件的工作区 Diff 后保持静止,确认 Diff 不会自动关闭,且只在文件不再出现在 Git 状态中时关闭;在 Windows 单文件工作区 Diff 中点击折叠条展开省略的未改动代码,确认 Diff 保持打开且每个修改块仍可单独暂存。 | | -| Git | **提交历史与图谱**
git-history | ✅ 已实现
🔍 待验证
`macos/Sources/Lithe/Views/Git`、`macos/Sources/LitheGitModule/Services/GitGraphLayoutService.swift`、`macos/Sources/LitheGitModule/Services/GitGraphHeadOrdering.swift`、`macos/Sources/Lithe/Views/Git/GitGraphColor.swift` | ✅ 已实现
🔍 待验证
`windows/tauri/src/features/git`、`windows/tauri/src/features/git/utils/git-graph-layout.ts`、`windows/tauri/src/features/git/utils/git-graph-colors.ts`、`windows/tauri/src/features/git/utils/git-graph-layout.test.ts` | Git | 分页浏览提交历史、分支图谱和提交详情,确认日期、作者和文件列表一致;分支最新提交的图谱连线从提交圆点开始,不超出到圆点上方;在双端使用同一包含本地分支、远端引用、标签和合并提交的仓库,核对永久图布局、图头引用排序、合并边投影及跨越 30 行的紧凑长边;在本地分支新增提交后确认分支颜色保持稳定,且不会因共用屏幕泳道直接沿用父分支颜色。双端保留各自调色板,不要求 RGB 值一致。 | | +| Git | **提交历史与图谱**
git-history | ✅ 已实现
🔍 待验证
`macos/Sources/Lithe/Views/Git`、`macos/Sources/LitheGitModule/Services/GitGraphLayoutService.swift`、`macos/Sources/LitheGitModule/Services/GitGraphHeadOrdering.swift`、`macos/Sources/Lithe/Views/Git/GitGraphColor.swift` | ✅ 已实现
🔍 待验证
`windows/tauri/src/features/git`、`windows/tauri/src/features/git/utils/git-graph-layout.ts`、`windows/tauri/src/features/git/utils/git-graph-colors.ts`、`windows/tauri/src/features/git/utils/git-graph-layout.test.ts`、`windows/tauri/src/features/git/hooks/use-commit-file-preview.ts`、`windows/tauri/src/features/git/hooks/use-commit-file-preview.test.tsx` | Git | 分页浏览提交历史、分支图谱和提交详情,确认日期、作者和文件列表一致;分支最新提交的图谱连线从提交圆点开始,不超出到圆点上方;在双端使用同一包含本地分支、远端引用、标签和合并提交的仓库,核对永久图布局、图头引用排序、合并边投影及跨越 30 行的紧凑长边;在本地分支新增提交后确认分支颜色保持稳定,且不会因共用屏幕泳道直接沿用父分支颜色。双端保留各自调色板,不要求 RGB 值一致。Windows 另验证:单击提交后编辑区预览该提交第一个文件的 diff;单击或双击 Commit files 中的文件只显示该文件的 diff(不含提交说明,也不显示文件列表),并复用同一个预览标签;Git Log 刚打开、尚未点击提交时不会自动打开 diff;日期列对与本机同一时区的提交显示相对时间(刚刚、N 分钟前、今天/昨天 + 12 小时制,中文为上午/下午),其他时区或未知时区的提交只显示日期,鼠标移入行时刷新相对时间;作者/日期列可在行内列边缘拖动调整宽度并在重启后保留。 非连续多选提交修改同一路径时,预览保留每个提交的 diff 区段及提交标识;延迟请求返回前取消选择、隐藏 Git Log、切换仓库或卸载工具窗,确认旧请求不会重新打开或激活预览标签。 | | | Git | **Rebase 与 Stash**
git-rebase-stash | ✅ 已实现
🔍 待验证
`macos/Sources/Lithe/Views/Git` | ✅ 已实现
🔍 待验证
`windows/tauri/src/features/git` | Git | 执行交互式 Rebase 和 Stash 保存/恢复,确认中断、冲突和继续操作。 | | | Git | **Worktree 管理**
git-worktrees | ✅ 已实现
🔍 待验证
`macos/Sources/Lithe/Views/Git` | ✅ 已实现
🔍 待验证
`windows/tauri/src/features/git` | Git | 列出、创建、切换和删除 Worktree,确认路径、分支和安全检查。 | | | Git | **多仓库引用面板:分组、配色与引用操作**
git-multi-repository-references | ✅ 已实现
🔍 待验证
`macos/Sources/Lithe/Views/Git` | ✅ 已实现
🔍 待验证
`windows/tauri/src/features/git` | Git | 在多仓库工作区打开 Git Log:确认按仓库分组、仓库配色、非活动仓库分组只读,点其它仓库的引用会切换活动仓库且只加载一次,Pull 弹窗可选择远程分支与策略。 | | @@ -175,18 +175,19 @@
-工作台 · 12 个能力点 +工作台 · 13 个能力点 | 功能组 | 能力点 | macOS | Windows | 负责人 | 验证方式 | 备注 | | --- | --- | --- | --- | --- | --- | --- | | 终端 | **Shell 发现与配置**
terminal-shell | ✅ 已实现
🔍 待验证
`macos/Sources/Lithe/Views/Terminal`、`macos/Sources/Lithe/Views/App/SettingsView.swift`、`macos/Sources/Lithe/Services` | ✅ 已实现
🔍 待验证
`windows/tauri/src/features/terminal`、`windows/tauri/src/features/settings/components/macos-settings-panels.tsx`、`windows/tauri/src/features/settings/lib/default-shell-options.ts`、`windows/tauri/src-tauri` | Terminal | 验证设置页默认 Shell 选项与终端「新建终端」菜单检测到的 Shell 一致、可选 Git Bash 并生效,以及环境变量、工作目录和不可用 Shell 的提示。 | | -| 终端 | **多会话与标签**
terminal-sessions | ✅ 已实现
🔍 待验证
`macos/Sources/Lithe/Views/Terminal` | ✅ 已实现
🔍 待验证
`windows/tauri/src/features/terminal` | Terminal | 创建多个终端会话并切换、重命名、关闭,确认子进程生命周期和资源清理;Windows 横向标签栏的新建终端与 Shell 配置按钮紧跟最后一个标签,标签溢出时按钮保持可见;标签栏右侧最小化按钮隐藏终端面板且会话继续运行,重新打开时恢复为停靠状态。 | | +| 终端 | **多会话与标签**
terminal-sessions | ✅ 已实现
🔍 待验证
`macos/Sources/Lithe/Views/Terminal` | ✅ 已实现
🔍 待验证
`windows/tauri/src/features/terminal` | Terminal | 创建多个终端会话并切换、重命名、关闭,确认子进程生命周期和资源清理;Windows 横向标签栏的新建终端与 Shell 配置按钮紧跟最后一个标签,标签溢出时按钮保持可见;标签栏右侧最小化按钮隐藏终端面板且会话继续运行,重新打开时恢复为停靠状态;Windows 打开底部终端面板时默认通栏(与 IDEA 一致):面板位于左右两条活动栏之间并横跨侧边栏与编辑器下方(活动栏保持通高,底部的活动栏按钮位置不上移),侧边栏让出纵向空间,且仍可通过面板菜单切换为编辑器宽度。 | | | 终端 | **复制粘贴、搜索与调整大小**
terminal-interaction | ✅ 已实现
🔍 待验证
`macos/Sources/Lithe/Views/Terminal` | ✅ 已实现
🔍 待验证
`windows/tauri/src/features/terminal` | Terminal | 验证复制粘贴、终端搜索、调整面板大小和高频输入不丢失。 | | | 终端 | **Windows Shell 超长命令输入**
terminal-long-command-input | 🧩 平台专属
— 不适用
`macos/Sources/Lithe/Views/Terminal` | ✅ 已实现
🔍 待验证
`windows/tauri/crates/terminal/src/connection.rs`、`windows/tauri/src-tauri/src/terminal.rs` | Terminal | 在 Windows cmd.exe 和 PowerShell 中执行超过 Shell 单行限制的 IDE 多行命令,确认脚本执行、原始命令内容和终端关闭后的临时文件清理。 | 仅在 IDE 后端为 cmd.exe/PowerShell 的超长多行文本创建一次性脚本;不修改系统 Shell 配置。 | | 本地历史 | **快照列表与 Diff**
history-snapshots | ✅ 已实现
🔍 待验证
`macos/Sources/Lithe/Views/History`、`macos/Sources/Lithe/Services` | ✅ 已实现
🔍 待验证
`windows/tauri/src/features/local-history` | Local History | 编辑同一文件多次,比较快照时间、内容 Diff 和文件范围。 | | | 本地历史 | **恢复、删除与持久化**
history-restore | ✅ 已实现
🔍 待验证
`macos/Sources/Lithe/Views/History` | ✅ 已实现
🔍 待验证
`windows/tauri/src/features/local-history` | Local History | 验证恢复、删除、应用重启后持久化和失败回滚。 | | | 命令与布局 | **命令面板与全局动作**
command-palette | ✅ 已实现
🔍 待验证
`macos/Sources/Lithe/Models/Keymap/LitheCommandCatalog.swift`、`macos/Sources/Lithe/Views/Search/SearchEverywhereView.swift` | ✅ 已实现
🔍 待验证
`windows/tauri/src/features/command-palette` | Workbench | 搜索并执行打开面板、运行、Git 和设置命令,确认快捷键和不可用命令状态。 | | | 命令与布局 | **紧凑菜单栏可在标题栏左侧原位展开完整水平菜单,并在点击外部或按 Escape 后收起,不遮挡工作区**
workbench-compact-menu-bar | 🧩 平台专属
— 不适用
`macos/Sources/Lithe/LitheApp.swift` | ✅ 已实现
🔍 待验证
`windows/tauri/src/features/window/components/title-bar/title-bar.tsx`、`windows/tauri/src/features/window/components/title-bar/title-project-menu.tsx`、`windows/tauri/src/features/window/components/window-menu-bar.tsx`、`windows/tauri/src/features/window/hooks/use-compact-menu-bar-dismissal.ts`、`windows/tauri/src/features/window/hooks/use-compact-menu-bar-dismissal.test.tsx` | Workbench | 在 Windows 工作台点击标题栏左端菜单按钮,确认汉堡按钮由“文件”菜单项取代,“文件”项处于选中状态且其下拉菜单立即展开;菜单与标题栏同高,不覆盖编辑区域,项目/分支控件在展开时隐藏。检查常驻品牌标识与下方活动栏图标列对齐且不可选中,并确认项目菜单显示当前项目图标(无自定义图标时显示项目缩写)。点击其他菜单项时菜单条保持展开并执行动作;点击工作区、标题栏空白和右侧控件或按 Escape 后收起;按 Escape 后确认焦点返回菜单入口;窄窗口确认菜单在标题栏内水平滚动且不挤压窗口控件。macOS 保持系统菜单栏行为。 | | +| 命令与布局 | **从工作台 chrome 打开设置:Windows 点击标题栏搜索按钮右侧的齿轮,macOS 通过应用菜单及面板内入口**
workbench-settings-entry | ✅ 已实现
🔍 待验证
`macos/Sources/Lithe/Models/LitheAction.swift`、`macos/Sources/Lithe/Views/App/SettingsView.swift` | ✅ 已实现
🔍 待验证
`windows/tauri/src/features/window/components/title-bar/title-bar.tsx`、`windows/tauri/src/features/layout/components/sidebar/sidebar-pane-selector.tsx`、`windows/tauri/src/features/layout/config/item-order.ts` | Workbench | 在 Windows 打开工作区,确认左侧活动栏底部不再显示设置齿轮,活动栏右键“在活动栏中显示”列表不包含设置项;点击标题栏右上搜索按钮右侧的齿轮打开设置对话框并再次关闭;携带旧版设置文件(活动栏排序或隐藏列表含 settings)启动后确认遗留条目被清理且活动栏显示正常。macOS 通过应用菜单和各面板入口打开设置保持不变。 | | | 窗口管理 | **标题栏控制窗口最小化、最大化/还原与关闭;命令面板可最小化、最大化及切换全屏**
window-controls | ✅ 已实现
🔍 待验证
`macos/Sources/Lithe/LitheApp.swift`、`macos/Sources/Lithe/Views/App/RootView.swift` | ✅ 已实现
🔍 待验证
`windows/tauri/src/features/window/components/title-bar/window-controls.tsx`、`windows/tauri/src/features/command-palette/constants/window-actions.tsx`、`windows/tauri/src/features/window/utils/window-actions.ts` | Workbench | 在 macOS 检查系统标题栏控制及全屏入口;在 Windows 验证标题栏最小化、最大化/还原和关闭,以及命令面板的窗口操作,确认图标语义、最大化/还原状态与按钮标签一致,并确认悬停关闭按钮背景贴合窗口右边缘。 | | | 命令与布局 | **分栏、面板与工具窗布局**
workbench-panes | ✅ 已实现
🔍 待验证
`macos/Sources/Lithe/Views/Workbench`、`macos/Sources/Lithe/Views/Workspace/ProjectSidebarView.swift`、`macos/Sources/Lithe/Views/Components/LitheSplitPaneView.swift`、`macos/Sources/Lithe/Services/Workbench/WorkbenchLayoutStore.swift`、`macos/Sources/Lithe/Views/App/RootView.swift`、`macos/Sources/Lithe/LitheApp.swift`、`macos/Sources/Lithe/Theme/LitheTheme.swift`、`macos/Resources/IDEAIcons/expui`、`macos/Resources/IDEAIcons/terminal` | ✅ 已实现
🔍 待验证
`windows/tauri/src/features/panes`、`windows/tauri/src/features/layout` | Workbench | 打开、关闭、移动和调整面板,验证布局持久化及高频拖动稳定性;在 macOS 启用背景图后检查项目栏、编辑器、底部及右侧工具窗之间是否仍有清晰的分割区间;将 macOS 左侧工具窗缩至 30pt 并确认目录树始终贴左、不随宽度向右移动、内容不溢出、右上和右下圆角仍可见,关闭并重新打开项目后仍保持窄侧栏及其他布局尺寸,重新展开后布局正常;在 Maven 项目产生构建输出时确认左侧模块按钮可滚动、设置按钮始终完整显示;在亮色主题且有未读通知时确认未打开的铃铛图标清晰可见、打开通知面板后白色图标与蓝色背景成对出现;在深色 Lithe 主题下对照外框渐变、面板对比度、背景图片回退、左右工具窗的 40pt 按钮槽、30pt 选中块及 IDEA Community 图标对比度,悬停工具窗按钮时确认箭头光标,检查项目栏标题颜色和弹窗主按钮主题色;检查 macOS 40pt 顶栏 Logo 与系统窗口按钮垂直居中,项目切换器使用 20pt 项目字母头像和 30pt 悬停块,项目与分支切换块悬停保持箭头光标,红绿灯右侧留白与 IDEA 接近,项目头像、欢迎列表及切换菜单按标准化项目路径共享稳定颜色(超过 9 个项目、移除后重开仍一致),顶栏使用 IDEA Islands 主题项目色并按 85% 混色、600pt 水平和 300pt 垂直范围渐变,深色窗口分隔条四角与底栏缺口无浅色亮斑,缩放窗口及从欢迎页进入工作区再返回后仍对齐。 | | | 工作台布局 | **活动栏图标的稳定悬停说明**
workbench-activity-tooltips | ✅ 已实现
🔍 待验证
`macos/Sources/Lithe/Views/Workbench/WorkbenchHoverTooltip.swift`、`macos/Sources/Lithe/Views/Workbench/WorkbenchView.swift`、`macos/Tests/LitheTests/WorkbenchHoverTooltipTests.swift` | ✅ 已实现
🔍 待验证
`windows/tauri/src/features/layout/components/sidebar/sidebar-pane-selector.tsx`、`windows/tauri/src/features/layout/components/footer/footer-tab-control.tsx`、`windows/tauri/src/features/layout/components/plugin-activity-rail.tsx` | Workbench | 逐一悬停左上导航、左下工具入口及右侧通知/插件/Maven,快速切换图标并移出活动栏;确认始终只显示当前图标说明,面板更新和滚动后不残留旧提示,禁用入口可显示说明,短文字自适应且提示不超出窗口边缘。 | | diff --git a/rust/lithe-core/src/git/graph.rs b/rust/lithe-core/src/git/graph.rs index e7577ee7b..b70a84c96 100644 --- a/rust/lithe-core/src/git/graph.rs +++ b/rust/lithe-core/src/git/graph.rs @@ -369,6 +369,7 @@ mod tests { author_name: "Lithe Test".to_string(), author_email: "test@example.com".to_string(), date: "2026-09-04".to_string(), + date_utc_offset_minutes: None, subject: hash.to_string(), decorations: decorations.to_string(), } diff --git a/rust/lithe-core/src/git/history.rs b/rust/lithe-core/src/git/history.rs index 04d1c9c94..4d79da81e 100644 --- a/rust/lithe-core/src/git/history.rs +++ b/rust/lithe-core/src/git/history.rs @@ -2,7 +2,8 @@ use super::{ command_value, execute_git_readonly_with_environment, git_process, parse_commit, - parse_reference, readonly_command, validate_root, GitCommandRequest, INTERNAL_REF_PREFIX, + parse_reference, readonly_command, validate_root, GitCommandRequest, GIT_COMMIT_DATE_ARGUMENT, + INTERNAL_REF_PREFIX, }; use crate::protocol::{ cancellation, CoreError, ErrorCode, GitCommitResponse, GitHistoryCursorCloseResponse, @@ -340,7 +341,7 @@ fn offset_history_page( offset.to_string(), "-n".to_string(), limit.saturating_add(1).to_string(), - "--date=format:%Y/%m/%d %H:%M".to_string(), + GIT_COMMIT_DATE_ARGUMENT.to_string(), "--pretty=format:%H%x1f%h%x1f%P%x1f%an%x1f%ae%x1f%ad%x1f%s%x1f%D".to_string(), ]); let output = readonly_command(GitCommandRequest { @@ -479,7 +480,7 @@ impl HistorySession { format!("--decorate-refs-exclude={INTERNAL_REF_PREFIX}*"), "-n".to_string(), MAX_HISTORY_COMMITS.to_string(), - "--date=format:%Y/%m/%d %H:%M".to_string(), + GIT_COMMIT_DATE_ARGUMENT.to_string(), "--pretty=format:%H%x1f%h%x1f%P%x1f%an%x1f%ae%x1f%ad%x1f%s%x1f%D".to_string(), ]); let mut process = git_process(); diff --git a/rust/lithe-core/src/git/mod.rs b/rust/lithe-core/src/git/mod.rs index df263141b..d00d9c518 100644 --- a/rust/lithe-core/src/git/mod.rs +++ b/rust/lithe-core/src/git/mod.rs @@ -72,6 +72,10 @@ use std::time::Duration; const DEFAULT_PUSH_PREVIEW_LIMIT: usize = 500; const INTERNAL_REF_PREFIX: &str = "refs/lithe/"; +/// Commit date argument for every `parse_commit` producer. The trailing `%z` +/// carries the author's UTC offset, which `parse_commit` splits off so the +/// displayed `date` keeps its `%Y/%m/%d %H:%M` shape. +const GIT_COMMIT_DATE_ARGUMENT: &str = "--date=format:%Y/%m/%d %H:%M %z"; const DEFAULT_REPOSITORY_SCAN_MAX_DIRECTORIES: usize = usize::MAX; const DEFAULT_REPOSITORY_SCAN_MAX_DEPTH: usize = usize::MAX; static TEMPORARY_INDEX_SEQUENCE: AtomicU64 = AtomicU64::new(0); @@ -2036,7 +2040,7 @@ fn read_commit_log( "--decorate=short".to_string(), "-n".to_string(), (limit.saturating_add(1)).to_string(), - "--date=format:%Y/%m/%d %H:%M".to_string(), + GIT_COMMIT_DATE_ARGUMENT.to_string(), "--pretty=format:%H%x1f%h%x1f%P%x1f%an%x1f%ae%x1f%ad%x1f%s%x1f%D".to_string(), ]); let commit_output = readonly_command(GitCommandRequest { @@ -2067,7 +2071,7 @@ pub fn commit(request: GitCommitRequest) -> Result Option { if columns.len() < 8 { return None; } + let (date, date_utc_offset_minutes) = split_commit_date_offset(columns[5]); Some(GitCommitResponse { hash: columns[0].to_string(), short_hash: columns[1].to_string(), parent_hashes: columns[2].split_whitespace().map(String::from).collect(), author_name: columns[3].to_string(), author_email: columns[4].to_string(), - date: columns[5].to_string(), + date: date.to_string(), + date_utc_offset_minutes, subject: columns[6].to_string(), decorations: columns[7].to_string(), }) } +/// Splits a `GIT_COMMIT_DATE_ARGUMENT` value into the displayed local date and +/// the author's UTC offset in minutes. Values without a valid `±HHMM` suffix are +/// kept whole with no offset so unexpected Git output stays visible. +fn split_commit_date_offset(value: &str) -> (&str, Option) { + let Some((date, zone)) = value.rsplit_once(' ') else { + return (value, None); + }; + let bytes = zone.as_bytes(); + if bytes.len() != 5 + || !matches!(bytes[0], b'+' | b'-') + || !zone[1..].bytes().all(|b| b.is_ascii_digit()) + { + return (value, None); + } + let hours: i32 = zone[1..3].parse().unwrap_or(0); + let minutes: i32 = zone[3..5].parse().unwrap_or(0); + let sign = if bytes[0] == b'-' { -1 } else { 1 }; + (date, Some(sign * (hours * 60 + minutes))) +} + fn validate_revision(value: &str) -> Result<(), CoreError> { if value.trim().is_empty() || value.starts_with('-') || value.contains('\0') { return Err(CoreError::new( @@ -6780,9 +6806,9 @@ fn relative_or_absolute(path: &Path, root: &Path) -> String { #[cfg(test)] mod tests { use super::{ - annotation_message_from_tag_object, line_similarity, pair_diff_entries, parse_diff, - pathspec_batches, simplified_canonical_path, structured_diff_from_output, DiffEntry, - GitCommandInvocation, GitCommandResponse, GitProcessOutput, MAX_ALIGNMENT_CELLS, + annotation_message_from_tag_object, line_similarity, pair_diff_entries, parse_commit, + parse_diff, pathspec_batches, simplified_canonical_path, structured_diff_from_output, + DiffEntry, GitCommandInvocation, GitCommandResponse, GitProcessOutput, MAX_ALIGNMENT_CELLS, }; use crate::protocol::{ CoreError, ErrorCode, GitCommitResponse, GitHistoryPageResponse, GitHistoryResponse, @@ -7241,6 +7267,25 @@ mod tests { ); } + #[test] + fn parse_commit_splits_the_author_utc_offset_from_the_displayed_date() { + let line = + |date: &str| format!("h\u{1f}s\u{1f}\u{1f}A\u{1f}a@x\u{1f}{date}\u{1f}msg\u{1f}"); + + let east = parse_commit(&line("2026/09/30 21:05 +0800")).expect("commit should parse"); + assert_eq!(east.date, "2026/09/30 21:05"); + assert_eq!(east.date_utc_offset_minutes, Some(480)); + + let west = parse_commit(&line("2026/09/30 08:05 -0330")).expect("commit should parse"); + assert_eq!(west.date, "2026/09/30 08:05"); + assert_eq!(west.date_utc_offset_minutes, Some(-210)); + + // Output without a recognizable zone stays visible unchanged instead of being trimmed. + let legacy = parse_commit(&line("2026/09/30 08:05")).expect("commit should parse"); + assert_eq!(legacy.date, "2026/09/30 08:05"); + assert_eq!(legacy.date_utc_offset_minutes, None); + } + #[test] fn history_response_matches_shared_fixture() { let fixture: Value = serde_json::from_str(include_str!(concat!( @@ -7278,6 +7323,7 @@ mod tests { author_name: "Lithe Test".into(), author_email: "test@example.invalid".into(), date: "2026/08/30 12:00".into(), + date_utc_offset_minutes: Some(480), subject: "Initial commit".into(), decorations: "HEAD -> feature/recent".into(), }], @@ -7347,6 +7393,7 @@ mod tests { author_name: "Lithe Test".into(), author_email: "test@example.invalid".into(), date: "2026/08/30 12:00".into(), + date_utc_offset_minutes: Some(480), subject: "Initial commit".into(), decorations: "HEAD -> feature/recent".into(), }], @@ -7386,6 +7433,7 @@ mod tests { author_name: "Lithe Developer".into(), author_email: "developer@lithe.local".into(), date: "2026/08/31 10:30".into(), + date_utc_offset_minutes: Some(-300), subject: "Add push preview".into(), decorations: "HEAD -> feature/core".into(), }], diff --git a/rust/lithe-core/src/protocol/contracts.rs b/rust/lithe-core/src/protocol/contracts.rs index dc03e484a..55f834617 100644 --- a/rust/lithe-core/src/protocol/contracts.rs +++ b/rust/lithe-core/src/protocol/contracts.rs @@ -626,7 +626,12 @@ pub struct GitCommitResponse { pub parent_hashes: Vec, pub author_name: String, pub author_email: String, + /// Author date as `YYYY/MM/DD HH:MM` in the author's own time zone. pub date: String, + /// Author's UTC offset in minutes (east positive) for `date`; `null` when + /// Git did not report one. Hosts compare it with the local offset before + /// presenting relative times. + pub date_utc_offset_minutes: Option, pub subject: String, pub decorations: String, } diff --git a/scripts/verify-windows-boundaries.sh b/scripts/verify-windows-boundaries.sh index 6465443d5..729a729fb 100755 --- a/scripts/verify-windows-boundaries.sh +++ b/scripts/verify-windows-boundaries.sh @@ -10,9 +10,12 @@ if find windows \ exit 1 fi +# The exemption pattern accepts backslash separators (ripgrep emits them on +# Windows) and starts with a character class so Git Bash does not rewrite a +# leading-slash argument into a Windows path. direct_imports=$(rg -l 'from "@tauri-apps/api/core"' windows/tauri/src \ --glob '*.ts' --glob '*.tsx' | \ - rg -v '/(core/lithe-core-client|platform/tauri-core)\.ts$' || true) + rg -v '[/\\](core[/\\]lithe-core-client|platform[/\\]tauri-core)\.ts$' || true) if [[ -n "$direct_imports" ]]; then echo "Frontend modules must use @/platform/tauri-core:" >&2 echo "$direct_imports" >&2 diff --git a/shared/contracts/rust-core-api.md b/shared/contracts/rust-core-api.md index d31596308..ec1df40ee 100644 --- a/shared/contracts/rust-core-api.md +++ b/shared/contracts/rust-core-api.md @@ -418,7 +418,7 @@ package manager owns the download and Lithe does not infer bytes from logs. | `git.apply` | Apply or check a patch in `stage`, `unstage`, `discard`, or Shelf restore mode | | `git.history` | Return the legacy combined reference snapshot and first bounded commit page | | `git.references` | Return deterministic refs, recent local branches, ahead/behind state, and effective Git identity without scanning commit history | -| `git.historyPage` | Return one bounded commit page, parent hashes, decorations, and an opaque continuation cursor | +| `git.historyPage` | Return one bounded commit page, parent hashes, decorations, author dates with their UTC offset (`dateUtcOffsetMinutes`, east positive, `null` when unknown), and an opaque continuation cursor | | `git.historyCursorClose` | Release an unfinished incremental history cursor and its Git process | | `git.pushPreview` | Resolve a local branch push destination and the bounded commits not present on that remote base | | `git.commit` | Return one structured commit by revision | diff --git a/shared/fixtures/git/history-page-response-v1.json b/shared/fixtures/git/history-page-response-v1.json index 8d8f14bdf..3fd5d9581 100644 --- a/shared/fixtures/git/history-page-response-v1.json +++ b/shared/fixtures/git/history-page-response-v1.json @@ -7,6 +7,7 @@ "authorName": "Lithe Test", "authorEmail": "test@example.invalid", "date": "2026/08/30 12:00", + "dateUtcOffsetMinutes": 480, "subject": "Initial commit", "decorations": "HEAD -> feature/recent" } diff --git a/shared/fixtures/git/history-response-v1.json b/shared/fixtures/git/history-response-v1.json index a5e303309..4efa5904a 100644 --- a/shared/fixtures/git/history-response-v1.json +++ b/shared/fixtures/git/history-response-v1.json @@ -51,6 +51,7 @@ "authorName": "Lithe Test", "authorEmail": "test@example.invalid", "date": "2026/08/30 12:00", + "dateUtcOffsetMinutes": 480, "subject": "Initial commit", "decorations": "HEAD -> feature/recent" } diff --git a/shared/fixtures/git/push-preview-v1.json b/shared/fixtures/git/push-preview-v1.json index 49125b431..9a11e7246 100644 --- a/shared/fixtures/git/push-preview-v1.json +++ b/shared/fixtures/git/push-preview-v1.json @@ -19,6 +19,7 @@ "authorName": "Lithe Developer", "authorEmail": "developer@lithe.local", "date": "2026/08/31 10:30", + "dateUtcOffsetMinutes": -300, "subject": "Add push preview", "decorations": "HEAD -> feature/core" } diff --git a/shared/platform-feature-matrix.json b/shared/platform-feature-matrix.json index 08f610fe4..a6b65474f 100644 --- a/shared/platform-feature-matrix.json +++ b/shared/platform-feature-matrix.json @@ -945,13 +945,15 @@ "windows/tauri/src/features/git", "windows/tauri/src/features/git/utils/git-graph-layout.ts", "windows/tauri/src/features/git/utils/git-graph-colors.ts", - "windows/tauri/src/features/git/utils/git-graph-layout.test.ts" + "windows/tauri/src/features/git/utils/git-graph-layout.test.ts", + "windows/tauri/src/features/git/hooks/use-commit-file-preview.ts", + "windows/tauri/src/features/git/hooks/use-commit-file-preview.test.tsx" ], "implementationStatus": "implemented", "verificationStatus": "pending" }, "owner": "Git", - "verification": "分页浏览提交历史、分支图谱和提交详情,确认日期、作者和文件列表一致;分支最新提交的图谱连线从提交圆点开始,不超出到圆点上方;在双端使用同一包含本地分支、远端引用、标签和合并提交的仓库,核对永久图布局、图头引用排序、合并边投影及跨越 30 行的紧凑长边;在本地分支新增提交后确认分支颜色保持稳定,且不会因共用屏幕泳道直接沿用父分支颜色。双端保留各自调色板,不要求 RGB 值一致。" + "verification": "分页浏览提交历史、分支图谱和提交详情,确认日期、作者和文件列表一致;分支最新提交的图谱连线从提交圆点开始,不超出到圆点上方;在双端使用同一包含本地分支、远端引用、标签和合并提交的仓库,核对永久图布局、图头引用排序、合并边投影及跨越 30 行的紧凑长边;在本地分支新增提交后确认分支颜色保持稳定,且不会因共用屏幕泳道直接沿用父分支颜色。双端保留各自调色板,不要求 RGB 值一致。Windows 另验证:单击提交后编辑区预览该提交第一个文件的 diff;单击或双击 Commit files 中的文件只显示该文件的 diff(不含提交说明,也不显示文件列表),并复用同一个预览标签;Git Log 刚打开、尚未点击提交时不会自动打开 diff;日期列对与本机同一时区的提交显示相对时间(刚刚、N 分钟前、今天/昨天 + 12 小时制,中文为上午/下午),其他时区或未知时区的提交只显示日期,鼠标移入行时刷新相对时间;作者/日期列可在行内列边缘拖动调整宽度并在重启后保留。 非连续多选提交修改同一路径时,预览保留每个提交的 diff 区段及提交标识;延迟请求返回前取消选择、隐藏 Git Log、切换仓库或卸载工具窗,确认旧请求不会重新打开或激活预览标签。" }, { "id": "git-rebase-stash", @@ -1742,7 +1744,7 @@ "verificationStatus": "pending" }, "owner": "Terminal", - "verification": "创建多个终端会话并切换、重命名、关闭,确认子进程生命周期和资源清理;Windows 横向标签栏的新建终端与 Shell 配置按钮紧跟最后一个标签,标签溢出时按钮保持可见;标签栏右侧最小化按钮隐藏终端面板且会话继续运行,重新打开时恢复为停靠状态。" + "verification": "创建多个终端会话并切换、重命名、关闭,确认子进程生命周期和资源清理;Windows 横向标签栏的新建终端与 Shell 配置按钮紧跟最后一个标签,标签溢出时按钮保持可见;标签栏右侧最小化按钮隐藏终端面板且会话继续运行,重新打开时恢复为停靠状态;Windows 打开底部终端面板时默认通栏(与 IDEA 一致):面板位于左右两条活动栏之间并横跨侧边栏与编辑器下方(活动栏保持通高,底部的活动栏按钮位置不上移),侧边栏让出纵向空间,且仍可通过面板菜单切换为编辑器宽度。" }, { "id": "terminal-interaction", @@ -2275,6 +2277,31 @@ "owner": "Workbench", "verification": "在 Windows 工作台点击标题栏左端菜单按钮,确认汉堡按钮由“文件”菜单项取代,“文件”项处于选中状态且其下拉菜单立即展开;菜单与标题栏同高,不覆盖编辑区域,项目/分支控件在展开时隐藏。检查常驻品牌标识与下方活动栏图标列对齐且不可选中,并确认项目菜单显示当前项目图标(无自定义图标时显示项目缩写)。点击其他菜单项时菜单条保持展开并执行动作;点击工作区、标题栏空白和右侧控件或按 Escape 后收起;按 Escape 后确认焦点返回菜单入口;窄窗口确认菜单在标题栏内水平滚动且不挤压窗口控件。macOS 保持系统菜单栏行为。" }, + { + "id": "workbench-settings-entry", + "area": "工作台", + "group": "命令与布局", + "capability": "从工作台 chrome 打开设置:Windows 点击标题栏搜索按钮右侧的齿轮,macOS 通过应用菜单及面板内入口", + "macos": { + "evidence": [ + "macos/Sources/Lithe/Models/LitheAction.swift", + "macos/Sources/Lithe/Views/App/SettingsView.swift" + ], + "implementationStatus": "implemented", + "verificationStatus": "pending" + }, + "windows": { + "evidence": [ + "windows/tauri/src/features/window/components/title-bar/title-bar.tsx", + "windows/tauri/src/features/layout/components/sidebar/sidebar-pane-selector.tsx", + "windows/tauri/src/features/layout/config/item-order.ts" + ], + "implementationStatus": "implemented", + "verificationStatus": "pending" + }, + "owner": "Workbench", + "verification": "在 Windows 打开工作区,确认左侧活动栏底部不再显示设置齿轮,活动栏右键“在活动栏中显示”列表不包含设置项;点击标题栏右上搜索按钮右侧的齿轮打开设置对话框并再次关闭;携带旧版设置文件(活动栏排序或隐藏列表含 settings)启动后确认遗留条目被清理且活动栏显示正常。macOS 通过应用菜单和各面板入口打开设置保持不变。" + }, { "id": "window-controls", "area": "工作台", diff --git a/windows/tauri/src/features/file-explorer/components/file-navigator-sidebar.tsx b/windows/tauri/src/features/file-explorer/components/file-navigator-sidebar.tsx index 3d5765100..8418712a1 100644 --- a/windows/tauri/src/features/file-explorer/components/file-navigator-sidebar.tsx +++ b/windows/tauri/src/features/file-explorer/components/file-navigator-sidebar.tsx @@ -366,7 +366,7 @@ export const FileNavigatorSidebar = memo(function FileNavigatorSidebar({ document.addEventListener("pointermove", handlePointerMove); document.addEventListener("pointerup", handlePointerUp); - document.body.style.cursor = "col-resize"; + document.body.style.cursor = "ew-resize"; document.body.style.userSelect = "none"; }, [navigatorLayout.width, resizeTo], @@ -462,7 +462,7 @@ export const FileNavigatorSidebar = memo(function FileNavigatorSidebar({ )}
{isResizing ? ( -
+
) : null} ); diff --git a/windows/tauri/src/features/git/components/diff/git-diff-editor-stack.tsx b/windows/tauri/src/features/git/components/diff/git-diff-editor-stack.tsx index bc82e74a0..2eda5beca 100644 --- a/windows/tauri/src/features/git/components/diff/git-diff-editor-stack.tsx +++ b/windows/tauri/src/features/git/components/diff/git-diff-editor-stack.tsx @@ -340,6 +340,7 @@ const GitDiffEditorStack = memo(function GitDiffEditorStack({ const [isFileTreeVisible, setIsFileTreeVisible] = useState(true); const [fileNavigatorViewMode, setFileNavigatorViewMode] = useState("tree"); const isWorkingTree = multiDiff.commitHash === "working-tree"; + const showsFileList = !isWorkingTree && !multiDiff.hideFileList; const isWorkingTreeBuffer = activeBuffer?.path === "diff://working-tree/all-files"; const isActiveMultiDiff = activeBuffer?.type === "diff" && activeBuffer.diffData === multiDiff; const isRefreshingRef = useRef(false); @@ -798,7 +799,7 @@ const GitDiffEditorStack = memo(function GitDiffEditorStack({ > - {!isWorkingTree ? ( + {showsFileList ? ( - {!isWorkingTree && isFileTreeVisible ? ( + {showsFileList && isFileTreeVisible ? ( { test("counts every descendant file in a nested directory", () => { @@ -18,4 +18,33 @@ describe("GitCommitFileTree", () => { expect(root?.type).toBe("branch"); expect(root ? countCommitFileTreeLeaves(root) : 0).toBe(3); }); + + test("shows the file count beside folder names and no status letter on file rows", async () => { + const css = await Bun.file( + new URL("../../../file-explorer/styles/file-explorer-tree.css", import.meta.url), + ).text(); + const source = await Bun.file(new URL("./git-commit-file-tree.tsx", import.meta.url)).text(); + + // No CSS targets a Git-specific tree class, so the shared file-tree styling applies unchanged. + expect(source).not.toContain("git-commit-file-tree"); + // The count rides in the label's description slot so it sits right after the folder name. + expect(source).toContain("git.log.fileCountOne"); + expect(source).not.toContain("trailing="); + expect(source).not.toContain("{file.status}"); + // No trailing span remains, so the shared `:last-child` flex rule must not be overridden. + expect(css).not.toContain(".git-commit-file-tree .file-tree-row > span:last-child"); + }); + + test("picks the first file in the order the tree renders, not input order", () => { + const files: GitCommitFile[] = [ + { path: "windows/tauri/src/b.ts", status: "M" }, + { path: "docs/development/a.md", status: "M" }, + { path: "shared/c.json", status: "M" }, + ]; + + const first = getFirstCommitFilePath(files); + expect(first).not.toBeNull(); + expect(files.map((file) => file.path)).toContain(first as string); + expect(getFirstCommitFilePath([])).toBeNull(); + }); }); diff --git a/windows/tauri/src/features/git/components/log/git-commit-file-tree.tsx b/windows/tauri/src/features/git/components/log/git-commit-file-tree.tsx index f9f5bcc78..7f7cd5515 100644 --- a/windows/tauri/src/features/git/components/log/git-commit-file-tree.tsx +++ b/windows/tauri/src/features/git/components/log/git-commit-file-tree.tsx @@ -15,7 +15,6 @@ import { } from "@/features/sidebar/lib/path-tree"; import { useTranslation } from "@/i18n/locale-provider"; import { bindScrollContainerWheel } from "@/ui/scroll-container-wheel"; -import { cn } from "@/utils/cn"; import type { GitCommitFile } from "../../types/git.types"; import { getCommitFileStatusColorClassName } from "../../utils/git-file-status-visuals"; @@ -24,6 +23,25 @@ export function countCommitFileTreeLeaves(node: PathTreeNode): nu return node.children.reduce((count, child) => count + countCommitFileTreeLeaves(child), 0); } +/** Path of the first file in the order the tree renders, so previews match what the user sees. */ +export function getFirstCommitFilePath(files: readonly GitCommitFile[]): string | null { + const firstLeaf = (nodes: readonly PathTreeNode[]): string | null => { + for (const node of nodes) { + if (node.type === "leaf") return node.path; + const nested = firstLeaf(node.children); + if (nested) return nested; + } + return null; + }; + + return firstLeaf( + buildPathTree([...files], { + getPath: (file) => file.path, + getKey: (file) => file.path, + }), + ); +} + function FileNode({ node, depth, @@ -51,6 +69,7 @@ function FileNode({ : { branch: node, label: node.name }; const branch = compacted.branch; const expanded = !collapsed.has(branch.path); + const fileCount = countCommitFileTreeLeaves(branch); return (
@@ -73,9 +92,11 @@ function FileNode({ /> ) : null } - trailing={ - - {countCommitFileTreeLeaves(branch)} + description={ + + {t(fileCount === 1 ? "git.log.fileCountOne" : "git.log.filesCount", { + count: fileCount, + })} } title={branch.path} @@ -123,11 +144,6 @@ function FileNode({ /> ) : null } - trailing={ - - {file.status} - - } title={t("git.log.openFileDiff")} className="h-full py-0.5" style={{ height: presentation.rowHeight }} diff --git a/windows/tauri/src/features/git/components/log/git-commit-inspector.tsx b/windows/tauri/src/features/git/components/log/git-commit-inspector.tsx index ef8760b07..fb1e2bccc 100644 --- a/windows/tauri/src/features/git/components/log/git-commit-inspector.tsx +++ b/windows/tauri/src/features/git/components/log/git-commit-inspector.tsx @@ -14,7 +14,7 @@ import { resolveGitCommitSelectionDiff, type GitCommitSelectionDiff, } from "../../utils/git-commit-selection-diff"; -import { GitCommitFileTree } from "./git-commit-file-tree"; +import { GitCommitFileTree, getFirstCommitFilePath } from "./git-commit-file-tree"; type FilesLoadState = "idle" | "loading" | "ready" | "failed"; @@ -25,6 +25,8 @@ export function GitCommitInspector({ onOpenDiff, onOpenRangeDiff, onOpenSelectionDiff, + onPreviewFile, + previewRequest, }: { repoPath: string | null; commit: GitCommit | null; @@ -38,6 +40,10 @@ export function GitCommitInspector({ selection: Extract, filePath?: string, ) => void; + /** Shows only one file's diff in the editor preview tab. */ + onPreviewFile: (selection: GitCommitSelectionDiff, filePath: string) => void; + /** Bumped when the user selects a commit; the first file is previewed once its files load. */ + previewRequest: number; }) { const { t } = useTranslation(); const [files, setFiles] = useState([]); @@ -47,10 +53,28 @@ export function GitCommitInspector({ const inspectorPanelLayout = useGitLogPreferencesStore.use.inspectorPanelLayout(); const { setInspectorPanelLayout } = useGitLogPreferencesStore.use.actions(); const selectionDiff = useMemo(() => resolveGitCommitSelectionDiff(commits), [commits]); + // The selection `files` were loaded for; guards against previewing the previous commit's files + // during the render in which a new selection has arrived but its files have not loaded yet. + const [loadedSelection, setLoadedSelection] = useState(null); + const handledPreviewRequestRef = useRef(previewRequest); + + // Selecting a commit previews its first file, like the IDEA Git log. Only explicit user + // selections bump `previewRequest`, so opening the Log never steals the editor on its own. + useEffect(() => { + if (!selectionDiff || loadedSelection !== selectionDiff) return; + if (handledPreviewRequestRef.current === previewRequest) return; + handledPreviewRequestRef.current = previewRequest; + + const firstPath = getFirstCommitFilePath(files); + if (!firstPath) return; + setSelectedPath(firstPath); + onPreviewFile(selectionDiff, firstPath); + }, [files, loadedSelection, onPreviewFile, previewRequest, selectionDiff]); useEffect(() => { const requestId = ++requestIdRef.current; setFiles([]); + setLoadedSelection(null); setSelectedPath(null); if (!repoPath || !selectionDiff) { setLoadState("idle"); @@ -84,6 +108,7 @@ export function GitCommitInspector({ return; } setFiles(loadedFiles); + setLoadedSelection(selectionDiff); setLoadState("ready"); }); @@ -93,7 +118,7 @@ export function GitCommitInspector({ }, [repoPath, selectionDiff]); return ( -
+
-
+
{t("git.log.commitFiles")} {loadState === "loading" @@ -148,13 +173,12 @@ export function GitCommitInspector({ { + setSelectedPath(path); + if (selectionDiff) onPreviewFile(selectionDiff, path); + }} onOpen={(path) => { - if (selectionDiff?.kind === "commit") onOpenDiff(selectionDiff.commit, path); - else if (selectionDiff?.kind === "range") onOpenRangeDiff(selectionDiff, path); - else if (selectionDiff?.kind === "selection") { - onOpenSelectionDiff(selectionDiff, path); - } + if (selectionDiff) onPreviewFile(selectionDiff, path); }} /> )} diff --git a/windows/tauri/src/features/git/components/log/git-commit-table.tsx b/windows/tauri/src/features/git/components/log/git-commit-table.tsx index 656a53b00..d5d2c2161 100644 --- a/windows/tauri/src/features/git/components/log/git-commit-table.tsx +++ b/windows/tauri/src/features/git/components/log/git-commit-table.tsx @@ -27,6 +27,7 @@ import { import { Button } from "@/ui/button"; import { cn } from "@/utils/cn"; import { useTranslation } from "@/i18n/locale-provider"; +import { useGitLogColumnResize } from "../../hooks/use-git-log-column-resize"; import { type GitLogFilterScope, useGitLogPreferencesStore, @@ -39,6 +40,8 @@ import { selectedCommitsInHistoryOrder, } from "../../utils/git-history-selection"; import { GitGraphRow } from "./git-graph-row"; +import { GitLogColumnResizeHandle } from "./git-log-column-resize-handle"; +import { GitLogDateCell } from "./git-log-date-cell"; import { isGitHeadCommit } from "../../utils/git-history-message"; const ROW_HEIGHT = 30; @@ -121,6 +124,7 @@ export function GitCommitTable({ estimateSize: () => ROW_HEIGHT, overscan: 14, }); + const { columnStyle, activeColumn, startResize } = useGitLogColumnResize(scrollRef); useLayoutEffect(() => { const element = scrollRef.current; @@ -202,7 +206,7 @@ export function GitCommitTable({ return (
-
+
-
- {t("git.log.commit")} - {t("git.log.author")} - {t("git.log.date")} -
-
{visibleRows.length === 0 ? ( commits.length === 0 && emptyState ? ( @@ -278,7 +277,10 @@ export function GitCommitTable({ ) ) : ( <> -
+
{virtualizer.getVirtualItems().map((virtualRow) => { const row = visibleRows[virtualRow.index]; const isSelected = selectedCommitHashes.has(row.commit.hash); @@ -323,12 +325,19 @@ export function GitCommitTable({ title={t("git.log.openDiffHint")} > - - {row.commit.author} - - - {row.commit.date} - +
+ + {row.commit.author} + + +
+
+ + +
{hasMultipleContextCommits ? ( @@ -460,6 +469,8 @@ export function GitCommitTable({ )}
+ {/* Shields the rows from hover and the trailing click while a column is being dragged. */} + {activeColumn ?
: null}
); } diff --git a/windows/tauri/src/features/git/components/log/git-log-column-resize-handle.tsx b/windows/tauri/src/features/git/components/log/git-log-column-resize-handle.tsx new file mode 100644 index 000000000..acf831294 --- /dev/null +++ b/windows/tauri/src/features/git/components/log/git-log-column-resize-handle.tsx @@ -0,0 +1,33 @@ +import { useTranslation } from "@/i18n/locale-provider"; +import type { GitLogColumn } from "../../utils/git-log-columns"; + +/** + * Drag handle on the left edge of an author or date cell. Every row renders one, so the whole + * column edge is draggable. It reaches 4px outside the edge and 8px (the cell's text inset) into + * the cell, so the resize cursor appears right up to the text; it stays unstyled (no highlight) + * like the app's other splitters. + */ +export function GitLogColumnResizeHandle({ + column, + onStartResize, +}: { + column: GitLogColumn; + onStartResize: (column: GitLogColumn, event: React.PointerEvent) => void; +}) { + const { t } = useTranslation(); + + return ( +
onStartResize(column, event)} + // A drag ends with a click on a common ancestor; keep clicks off the row's own handlers. + onClick={(event) => event.stopPropagation()} + onDoubleClick={(event) => event.stopPropagation()} + /> + ); +} diff --git a/windows/tauri/src/features/git/components/log/git-log-date-cell.test.tsx b/windows/tauri/src/features/git/components/log/git-log-date-cell.test.tsx new file mode 100644 index 000000000..1daa382e8 --- /dev/null +++ b/windows/tauri/src/features/git/components/log/git-log-date-cell.test.tsx @@ -0,0 +1,57 @@ +import { afterEach, beforeEach, expect, setSystemTime, test } from "bun:test"; +import { act } from "react"; +import { createRoot, type Root } from "react-dom/client"; +import { LocaleProvider } from "@/i18n/locale-provider"; +import { installHappyDom } from "@/test-utils/happy-dom"; +import { GitLogDateCell } from "./git-log-date-cell"; + +// The commit is authored in the machine's own zone so relative labels apply. +const commitTime = new Date(2026, 8, 10, 15, 0); +const localOffset = -commitTime.getTimezoneOffset(); + +let restoreDom: () => void; +let container: HTMLElement; +let root: Root; +const actEnvironment = globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT?: boolean }; +let previousActEnvironment: boolean | undefined; + +beforeEach(async () => { + restoreDom = installHappyDom(); + previousActEnvironment = actEnvironment.IS_REACT_ACT_ENVIRONMENT; + actEnvironment.IS_REACT_ACT_ENVIRONMENT = true; + setSystemTime(new Date(2026, 8, 10, 15, 5)); + container = document.createElement("div"); + document.body.append(container); + root = createRoot(container); + await act(async () => { + root.render( + +
+ +
+
, + ); + }); +}); + +afterEach(async () => { + await act(async () => { + root.unmount(); + }); + setSystemTime(); + actEnvironment.IS_REACT_ACT_ENVIRONMENT = previousActEnvironment; + restoreDom(); +}); + +test("relative date refreshes when the pointer enters the row, not on its own", async () => { + const row = container.querySelector('[data-testid="row"]') as HTMLElement; + expect(row.textContent).toBe("5 minutes ago"); + + setSystemTime(new Date(2026, 8, 10, 15, 20)); + expect(row.textContent).toBe("5 minutes ago"); + + await act(async () => { + row.dispatchEvent(new Event("mouseenter")); + }); + expect(row.textContent).toBe("20 minutes ago"); +}); diff --git a/windows/tauri/src/features/git/components/log/git-log-date-cell.tsx b/windows/tauri/src/features/git/components/log/git-log-date-cell.tsx new file mode 100644 index 000000000..c0bc63814 --- /dev/null +++ b/windows/tauri/src/features/git/components/log/git-log-date-cell.tsx @@ -0,0 +1,37 @@ +import { useEffect, useRef, useState } from "react"; +import { useTranslation } from "@/i18n/locale-provider"; +import { formatGitLogDate } from "../../utils/git-log-date"; + +/** + * Commit date cell of a Git log row. Relative labels ("5 minutes ago", "Today") are computed + * against the time the row was rendered and refreshed when the pointer enters the row, instead + * of re-rendering the whole table on a timer. + */ +export function GitLogDateCell({ + date, + utcOffsetMinutes, +}: { + date: string; + utcOffsetMinutes: number | undefined; +}) { + const { t } = useTranslation(); + const cellRef = useRef(null); + const [now, setNow] = useState(() => new Date()); + + useEffect(() => { + const row = cellRef.current?.closest("[data-git-commit-index]"); + if (!row) return; + const refresh = () => setNow(new Date()); + row.addEventListener("mouseenter", refresh); + return () => row.removeEventListener("mouseenter", refresh); + }, []); + + return ( + + {formatGitLogDate(date, t, utcOffsetMinutes, now)} + + ); +} diff --git a/windows/tauri/src/features/git/components/log/git-log-title-bar.tsx b/windows/tauri/src/features/git/components/log/git-log-title-bar.tsx index 879a60371..d95edc8c3 100644 --- a/windows/tauri/src/features/git/components/log/git-log-title-bar.tsx +++ b/windows/tauri/src/features/git/components/log/git-log-title-bar.tsx @@ -25,7 +25,7 @@ export function GitLogTitleBar({ const { t } = useTranslation(); return ( -
+
{t("workbench.gitLog")} diff --git a/windows/tauri/src/features/git/components/log/git-log-tool-window.tsx b/windows/tauri/src/features/git/components/log/git-log-tool-window.tsx index 5f50d16c1..736e11ba8 100644 --- a/windows/tauri/src/features/git/components/log/git-log-tool-window.tsx +++ b/windows/tauri/src/features/git/components/log/git-log-tool-window.tsx @@ -93,6 +93,7 @@ export function GitLogToolWindow() { if (!rootFolderPath || availableRepoPaths.length > 0) return; void syncWorkspaceRepositories(rootFolderPath); }, [availableRepoPaths.length, rootFolderPath, syncWorkspaceRepositories]); + const isBottomPaneVisible = useUIState((state) => state.isBottomPaneVisible); const setIsBottomPaneVisible = useUIState((state) => state.setIsBottomPaneVisible); const openSettingsDialog = useUIState((state) => state.openSettingsDialog); const pendingReferenceSelectionRef = useRef(null); @@ -112,6 +113,7 @@ export function GitLogToolWindow() { const pullWorkflow = useGitPullWorkflow({ repoPath: repoPath ?? "", refresh }); const [selectedCommit, setSelectedCommit] = useState(null); const [selectedCommitHashes, setSelectedCommitHashes] = useState>(new Set()); + const [previewRequest, setPreviewRequest] = useState(0); const [isReferenceOperating, setIsReferenceOperating] = useState(false); const [showFetchOptions, setShowFetchOptions] = useState(false); const [showRemoteManager, setShowRemoteManager] = useState(false); @@ -199,12 +201,17 @@ export function GitLogToolWindow() { const { isLoadingCommitDiff, viewCommitDiff, + previewCommitFileDiff, viewCommitRangeDiff, viewCommitSelectionDiff, viewBranchDiff, viewReferenceWorkingTreeDiff, } = useGitDiffActions({ activeRepoPath: repoPath, + commitPreviewScope: + isBottomPaneVisible && panel === "log" && selectedCommits.length > 0 + ? JSON.stringify(selectedCommits.map((commit) => commit.hash)) + : null, gitFileByPath: emptyGitFileByPath, workingTreeDiffEntriesByScope: emptyWorkingTreeEntries, commitByHash, @@ -230,6 +237,7 @@ export function GitLogToolWindow() { ? commit.hash : visibleCommitHashes.find((hash) => result.selected.has(hash)); setSelectedCommit(activeHash ? (commitByHash.get(activeHash) ?? null) : null); + setPreviewRequest((request) => request + 1); }; const selectCommitForContextMenu = (commit: GitCommit) => { @@ -735,6 +743,8 @@ export function GitLogToolWindow() { repoPath={repoPath} commit={activeSelectedCommit} commits={selectedCommits} + previewRequest={previewRequest} + onPreviewFile={previewCommitFileDiff} onOpenDiff={openDiff} onOpenRangeDiff={(range, filePath) => { if (isLoadingCommitDiff) return; diff --git a/windows/tauri/src/features/git/components/log/git-reference-tree.tsx b/windows/tauri/src/features/git/components/log/git-reference-tree.tsx index 86e496d84..60ce88db5 100644 --- a/windows/tauri/src/features/git/components/log/git-reference-tree.tsx +++ b/windows/tauri/src/features/git/components/log/git-reference-tree.tsx @@ -405,7 +405,7 @@ function GitReferenceToolbar({ return (
{primaryActions.map((action) => renderAction(action))}
@@ -419,7 +419,7 @@ function GitReferenceToolbar({ return (
{visibleActions.map((action) => renderAction(action))}
@@ -1022,7 +1022,7 @@ export function GitReferenceTree({ ); return ( -
+
- ) : ( - - - - - {t("git.workingTreeClean")} - - )} + ) : null} ({ + hash, + shortHash: hash, + parentHashes: [], + message: hash, + author: "Test", + date: "", + decorations: "", +}); +const diff = (additions: number, path = "a.ts"): GitDiff => ({ + file_path: path, + is_new: false, + is_deleted: false, + is_renamed: false, + lines: [], + additions, + deletions: 0, +}); +let preview: ReturnType; +let root: Root | undefined; +let container: HTMLElement; +let restoreDom: () => void; +let previousAct: boolean | undefined; +const actGlobal = globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT?: boolean }; +let published: MultiFileDiff[]; +let releases: Array<() => void>; +let inFlight: Promise[]; +let loadCommit: ReturnType>; +let loadRange: ReturnType>; +const openPreview = (_path: string, _name: string, data: MultiFileDiff) => { + published.push(data); +}; + +function Harness({ scope = "A", repo = "C:/repo" }: { scope?: string | null; repo?: string }) { + preview = useCommitFilePreview(repo, scope, openPreview); + return null; +} +async function render(scope: string | null, repo = "C:/repo") { + await act(async () => { + root!.render(); + }); +} +function pendingDiff() { + let resolve!: (diffs: GitDiff[]) => void; + const promise = new Promise((done) => { + resolve = done; + }); + releases.push(() => resolve([])); + return { promise, resolve }; +} +function start(hash = "A") { + const task = preview({ kind: "commit", commit: commit(hash) }, "a.ts"); + inFlight.push(task); + return task; +} + +beforeEach(async () => { + restoreDom = installHappyDom(); + previousAct = actGlobal.IS_REACT_ACT_ENVIRONMENT; + actGlobal.IS_REACT_ACT_ENVIRONMENT = true; + published = []; + releases = []; + inFlight = []; + loadCommit = spyOn(api, "getCommitDiff").mockResolvedValue([diff(1)]); + loadRange = spyOn(api, "getRefDiff").mockResolvedValue([diff(2)]); + container = document.createElement("div"); + document.body.append(container); + root = createRoot(container); + await render("A"); +}); +afterEach(async () => { + try { + await act(async () => { + root?.unmount(); + }); + // Resolve every controlled request even when an assertion fails. + for (const release of releases) release(); + await Promise.all(inFlight); + } finally { + loadCommit.mockRestore(); + loadRange.mockRestore(); + container.remove(); + actGlobal.IS_REACT_ACT_ENVIRONMENT = previousAct; + restoreDom(); + } +}); + +for (const reason of [ + "deselected", + "hidden", + "new-selection", + "new-repository", + "unmounted", +] as const) { + test(`does not publish a pending preview after ${reason}`, async () => { + const pending = pendingDiff(); + loadCommit.mockReturnValue(pending.promise); + const task = start(); + if (reason === "unmounted") { + await act(async () => { + root!.unmount(); + }); + root = undefined; + } else if (reason === "new-repository") { + await render("A", "C:/other"); + } else { + await render(reason === "new-selection" ? "B" : null); + } + pending.resolve([diff(1)]); + await task; + expect(published).toEqual([]); + }); +} + +test("a later file click supersedes an earlier request and only opens once", async () => { + const first = pendingDiff(); + const second = pendingDiff(); + loadCommit.mockReturnValueOnce(first.promise).mockReturnValueOnce(second.promise); + const older = start("A"); + const newer = start("B"); + second.resolve([diff(2)]); + await newer; + first.resolve([diff(1)]); + await older; + expect(published.map((data) => data.totalAdditions)).toEqual([2]); +}); + +test("disconnected commits retain every matching revision and its label", async () => { + loadCommit + .mockResolvedValueOnce([diff(3), diff(20, "other.ts")]) + .mockResolvedValueOnce([diff(7)]); + await preview({ kind: "selection", commits: [commit("C"), commit("A")] }, "a.ts"); + expect(published).toHaveLength(1); + expect(published[0].files.map((entry) => entry.additions)).toEqual([3, 7]); + expect(published[0].fileLabels).toEqual(["C", "A"]); + expect(published[0].fileKeys).toEqual(["C:a.ts:0", "A:a.ts:0"]); + expect(published[0].totalFiles).toBe(1); + expect(published[0].hideFileList).toBe(true); +}); + +test("contiguous selections still use the range diff and missing files do not open a tab", async () => { + await preview( + { kind: "range", baseRef: "A", targetRef: "C", oldest: commit("B"), newest: commit("C") }, + "a.ts", + ); + expect(loadRange).toHaveBeenCalledWith("C:/repo", "A", "C"); + expect(published).toHaveLength(1); + expect(published[0].totalAdditions).toBe(2); + loadCommit.mockResolvedValue([]); + await start(); + expect(published).toHaveLength(1); +}); diff --git a/windows/tauri/src/features/git/hooks/use-commit-file-preview.ts b/windows/tauri/src/features/git/hooks/use-commit-file-preview.ts new file mode 100644 index 000000000..8e144eaca --- /dev/null +++ b/windows/tauri/src/features/git/hooks/use-commit-file-preview.ts @@ -0,0 +1,79 @@ +import { useCallback, useLayoutEffect, useRef } from "react"; +import { getCommitDiff, getRefDiff } from "../api/git-diff-api"; +import type { MultiFileDiff } from "../types/git-diff.types"; +import type { GitDiff } from "../types/git.types"; +import { mapGitReadsInBatches } from "../utils/git-async-batch"; +import { + aggregateSelectedCommitDiffs, + type GitCommitSelectionDiff, +} from "../utils/git-commit-selection-diff"; +import { createCommitFileDiffPreview } from "../utils/multi-file-diff"; +import { createRequestGeneration } from "../utils/request-generation"; + +/** Owns automatic preview publication for one visible Git Log selection. */ +export function useCommitFilePreview( + repoPath: string | null, + scopeKey: string | null, + openPreview: (path: string, name: string, data: MultiFileDiff) => unknown, +) { + const requests = useRef(createRequestGeneration()); + // Invalidate before passive effects can start a preview for the new selection. + // Cleanup also prevents a closed/unmounted Log from activating a global editor buffer. + useLayoutEffect(() => { + requests.current.begin(); + return () => { + requests.current.begin(); + }; + }, [repoPath, scopeKey]); + + return useCallback( + async (selection: GitCommitSelectionDiff, filePath: string) => { + if (!repoPath || scopeKey === null) return; + const requestId = requests.current.begin(); + try { + let diffs: readonly GitDiff[] | null; + let commitHash: string; + let label: string; + let fileKeys: string[] | undefined; + let fileLabels: string[] | undefined; + if (selection.kind === "commit") { + diffs = await getCommitDiff(repoPath, selection.commit.hash); + commitHash = selection.commit.hash; + label = selection.commit.shortHash; + } else if (selection.kind === "range") { + diffs = await getRefDiff(repoPath, selection.baseRef, selection.targetRef); + commitHash = `${selection.baseRef ?? "root"}..${selection.targetRef}`; + label = `${selection.oldest.shortHash}..${selection.newest.shortHash}`; + } else { + const results = await mapGitReadsInBatches(selection.commits, async (commit) => ({ + commit, + diffs: await getCommitDiff(repoPath, commit.hash), + })); + if (results.some((result) => result.diffs === null)) return; + const aggregate = aggregateSelectedCommitDiffs( + results.map((result) => ({ commit: result.commit, diffs: result.diffs ?? [] })), + ); + diffs = aggregate.diffs; + fileKeys = aggregate.fileKeys; + fileLabels = aggregate.fileLabels; + commitHash = selection.commits[0]?.hash ?? "selection"; + label = String(selection.commits.length); + } + if (!requests.current.isCurrent(requestId) || !diffs) return; + const preview = createCommitFileDiffPreview({ + repoPath, + commitHash, + diffs, + filePath, + fileKeys, + fileLabels, + label, + }); + if (preview) openPreview(preview.virtualPath, preview.displayName, preview.diffData); + } catch (error) { + console.error("Error previewing commit file diff:", error); + } + }, + [repoPath, scopeKey, openPreview], + ); +} diff --git a/windows/tauri/src/features/git/hooks/use-git-diff-actions.ts b/windows/tauri/src/features/git/hooks/use-git-diff-actions.ts index 8dd191ff9..835c88e9d 100644 --- a/windows/tauri/src/features/git/hooks/use-git-diff-actions.ts +++ b/windows/tauri/src/features/git/hooks/use-git-diff-actions.ts @@ -30,6 +30,8 @@ import { createRequestGeneration, type RequestGeneration } from "../utils/reques import { createCommitDiffBuffer, createMultiFileDiff } from "../utils/multi-file-diff"; import { createSingleFileWorkingTreeDiff } from "../utils/working-tree-multi-diff"; +import { useCommitFilePreview } from "./use-commit-file-preview"; + const WORKING_TREE_TITLES: Record = { all: "git.diff.uncommitted", unstaged: "git.diff.unstagedChanges", @@ -73,6 +75,7 @@ export function useGitDiffActions({ currentBranch, currentReference, onBranchDiffOpened, + commitPreviewScope = null, }: { activeRepoPath: string | null; onFileSelect?: (path: string, isDir: boolean) => void; @@ -82,6 +85,7 @@ export function useGitDiffActions({ currentBranch?: string; currentReference?: GitReference; onBranchDiffOpened?: () => void; + commitPreviewScope?: string | null; }) { const { t } = useTranslation(); const [isLoadingCommitDiff, setIsLoadingCommitDiff] = useState(false); @@ -94,6 +98,11 @@ export function useGitDiffActions({ latestFileDiffRequestRef.current = latestFileDiffRequest; const activeRepoPathRef = useRef(activeRepoPath); activeRepoPathRef.current = activeRepoPath; + const previewCommitFileDiff = useCommitFilePreview( + activeRepoPath, + commitPreviewScope, + openDiffBuffer, + ); const openOriginalFile = useCallback( async (filePath: string) => { @@ -625,6 +634,7 @@ export function useGitDiffActions({ viewFileDiff, viewWorkingTreeDiff, viewCommitDiff, + previewCommitFileDiff, viewCommitRangeDiff, viewCommitSelectionDiff, viewStashDiff, diff --git a/windows/tauri/src/features/git/hooks/use-git-log-column-resize.test.tsx b/windows/tauri/src/features/git/hooks/use-git-log-column-resize.test.tsx new file mode 100644 index 000000000..16a2167d2 --- /dev/null +++ b/windows/tauri/src/features/git/hooks/use-git-log-column-resize.test.tsx @@ -0,0 +1,155 @@ +import { afterEach, beforeEach, expect, test } from "bun:test"; +import { act, useRef } from "react"; +import { createRoot, type Root } from "react-dom/client"; +import { GitLogColumnResizeHandle } from "@/features/git/components/log/git-log-column-resize-handle"; +import { LocaleProvider } from "@/i18n/locale-provider"; +import { installHappyDom } from "@/test-utils/happy-dom"; +import { useGitLogPreferencesStore } from "../stores/git-log-preferences.store"; +import { GIT_LOG_COLUMN_DEFAULT_WIDTHS } from "../utils/git-log-columns"; +import { useGitLogColumnResize } from "./use-git-log-column-resize"; + +let rowClicks = 0; +// Manual animation-frame queue so live widths are applied deterministically, without real timers. +let pendingFrames = new Map(); +let nextFrameHandle = 1; +const frameScheduler = { + scheduleFrame: (callback: FrameRequestCallback) => { + const handle = nextFrameHandle++; + pendingFrames.set(handle, callback); + return handle; + }, + cancelFrame: (handle: number) => { + pendingFrames.delete(handle); + }, +}; + +// Mirrors the table: one scroll container whose CSS variables drive every row's column widths. +function Harness() { + const scrollRef = useRef(null); + const { columnStyle, activeColumn, startResize } = useGitLogColumnResize( + scrollRef, + frameScheduler, + ); + + return ( + +
+
(rowClicks += 1)}> +
+ +
+
+ +
+
+
+ {activeColumn ?? "none"} +
+ ); +} + +function pointerEvent(type: string, init: { clientX: number; button?: number }) { + const event = new Event(type, { bubbles: true, cancelable: true }); + return Object.assign(event, { button: 0, ...init }); +} + +const flushFrames = () => { + const frames = [...pendingFrames.values()]; + pendingFrames = new Map(); + for (const frame of frames) frame(0); +}; + +let restoreDom: () => void; +let container: HTMLElement; +let root: Root; +const actEnvironment = globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT?: boolean }; +let previousActEnvironment: boolean | undefined; + +beforeEach(async () => { + restoreDom = installHappyDom(); + previousActEnvironment = actEnvironment.IS_REACT_ACT_ENVIRONMENT; + actEnvironment.IS_REACT_ACT_ENVIRONMENT = true; + rowClicks = 0; + pendingFrames = new Map(); + useGitLogPreferencesStore.setState({ + authorColumnWidth: GIT_LOG_COLUMN_DEFAULT_WIDTHS.author, + dateColumnWidth: GIT_LOG_COLUMN_DEFAULT_WIDTHS.date, + }); + container = document.createElement("div"); + document.body.append(container); + root = createRoot(container); + await act(async () => { + root.render(); + }); +}); + +afterEach(async () => { + await act(async () => { + root.unmount(); + }); + actEnvironment.IS_REACT_ACT_ENVIRONMENT = previousActEnvironment; + restoreDom(); +}); + +const query = (testId: string) => + container.querySelector(`[data-testid="${testId}"]`) as HTMLElement; +const handle = (cellTestId: string) => + query(cellTestId).querySelector('[role="separator"]') as HTMLElement; + +test("dragging a column's left edge leftwards widens it live and persists once on release", async () => { + const scroll = query("scroll"); + expect(scroll.style.getPropertyValue("--git-log-author-width")).toBe("112px"); + + await act(async () => { + handle("author-cell").dispatchEvent(pointerEvent("pointerdown", { clientX: 300 })); + }); + expect(query("active").textContent).toBe("author"); + + await act(async () => { + document.dispatchEvent(pointerEvent("pointermove", { clientX: 260 })); + flushFrames(); + }); + // The live width lands on the DOM, but nothing is written to the store mid-drag. + expect(scroll.style.getPropertyValue("--git-log-author-width")).toBe("152px"); + expect(useGitLogPreferencesStore.getState().authorColumnWidth).toBe(112); + + await act(async () => { + document.dispatchEvent(pointerEvent("pointerup", { clientX: 260 })); + }); + expect(useGitLogPreferencesStore.getState().authorColumnWidth).toBe(152); + expect(useGitLogPreferencesStore.getState().dateColumnWidth).toBe(144); + expect(scroll.style.getPropertyValue("--git-log-author-width")).toBe("152px"); + expect(query("active").textContent).toBe("none"); +}); + +test("the date column resizes independently and respects its bounds", async () => { + const scroll = query("scroll"); + + await act(async () => { + handle("date-cell").dispatchEvent(pointerEvent("pointerdown", { clientX: 500 })); + }); + await act(async () => { + document.dispatchEvent(pointerEvent("pointermove", { clientX: -5000 })); + flushFrames(); + }); + expect(scroll.style.getPropertyValue("--git-log-date-width")).toBe("280px"); + + await act(async () => { + document.dispatchEvent(pointerEvent("pointerup", { clientX: -5000 })); + }); + expect(useGitLogPreferencesStore.getState().dateColumnWidth).toBe(280); + expect(useGitLogPreferencesStore.getState().authorColumnWidth).toBe(112); +}); + +test("pressing the handle neither selects the row nor starts a drag with the right button", async () => { + await act(async () => { + handle("author-cell").dispatchEvent(pointerEvent("pointerdown", { clientX: 10, button: 2 })); + }); + expect(query("active").textContent).toBe("none"); + + await act(async () => { + handle("author-cell").dispatchEvent(new Event("click", { bubbles: true })); + handle("author-cell").dispatchEvent(new Event("dblclick", { bubbles: true })); + }); + expect(rowClicks).toBe(0); +}); diff --git a/windows/tauri/src/features/git/hooks/use-git-log-column-resize.ts b/windows/tauri/src/features/git/hooks/use-git-log-column-resize.ts new file mode 100644 index 000000000..9c8f4cf65 --- /dev/null +++ b/windows/tauri/src/features/git/hooks/use-git-log-column-resize.ts @@ -0,0 +1,84 @@ +import { useCallback, useEffect, useMemo, useRef, useState, type CSSProperties } from "react"; +import { + startDocumentResizeSession, + type DocumentResizeSession, + type DocumentResizeSessionOptions, +} from "@/utils/document-resize-session"; +import { useGitLogPreferencesStore } from "../stores/git-log-preferences.store"; +import { + GIT_LOG_COLUMN_CSS_VARIABLES, + GIT_LOG_MESSAGE_MIN_WIDTH, + clampGitLogColumnWidth, + type GitLogColumn, +} from "../utils/git-log-columns"; + +/** + * Owns the commit table's resizable author and date columns. + * + * Widths reach every row through CSS variables on the scroll container. While dragging, the + * variable is written straight to the DOM (once per frame) so the virtualized rows are not + * re-rendered; the final width is persisted once when the drag ends. + * + * `frameScheduler` replaces `requestAnimationFrame` so tests can flush frames deterministically. + */ +export function useGitLogColumnResize( + scrollRef: React.RefObject, + frameScheduler?: Pick, +) { + const authorWidth = useGitLogPreferencesStore.use.authorColumnWidth(); + const dateWidth = useGitLogPreferencesStore.use.dateColumnWidth(); + const { setColumnWidth } = useGitLogPreferencesStore.use.actions(); + const sessionRef = useRef(null); + const [activeColumn, setActiveColumn] = useState(null); + + const startResize = useCallback( + (column: GitLogColumn, event: React.PointerEvent) => { + // Only the primary button resizes; the secondary button must still open the row menu. + if (event.button !== 0) return; + event.preventDefault(); + // Keep the row from treating the press as a selection or the start of a double-click open. + event.stopPropagation(); + sessionRef.current?.dispose({ commit: true }); + + const variable = GIT_LOG_COLUMN_CSS_VARIABLES[column]; + sessionRef.current = startDocumentResizeSession({ + startX: event.clientX, + startWidth: column === "author" ? authorWidth : dateWidth, + // Both columns hug the right edge, so dragging their left edge leftwards widens them. + direction: -1, + cursor: "ew-resize", + clampWidth: (value) => clampGitLogColumnWidth(column, value), + applyWidth: (width) => scrollRef.current?.style.setProperty(variable, `${width}px`), + commitWidth: (width) => { + setColumnWidth(column, width); + sessionRef.current = null; + }, + onActiveChange: (active) => setActiveColumn(active ? column : null), + ...frameScheduler, + }); + }, + [authorWidth, dateWidth, frameScheduler, scrollRef, setColumnWidth], + ); + + // Persist an in-flight drag if the table unmounts mid-gesture, and drop listeners and styles. + useEffect( + () => () => { + sessionRef.current?.dispose({ commit: true }); + sessionRef.current = null; + }, + [], + ); + + const columnStyle = useMemo( + () => + ({ + [GIT_LOG_COLUMN_CSS_VARIABLES.author]: `${authorWidth}px`, + [GIT_LOG_COLUMN_CSS_VARIABLES.date]: `${dateWidth}px`, + // Rows never get narrower than their columns plus the message area; overflow scrolls. + "--git-log-row-min-width": `calc(var(${GIT_LOG_COLUMN_CSS_VARIABLES.author}) + var(${GIT_LOG_COLUMN_CSS_VARIABLES.date}) + ${GIT_LOG_MESSAGE_MIN_WIDTH}px)`, + }) as CSSProperties, + [authorWidth, dateWidth], + ); + + return { columnStyle, activeColumn, startResize }; +} diff --git a/windows/tauri/src/features/git/stores/git-log-preferences.store.ts b/windows/tauri/src/features/git/stores/git-log-preferences.store.ts index cc909a107..68729879b 100644 --- a/windows/tauri/src/features/git/stores/git-log-preferences.store.ts +++ b/windows/tauri/src/features/git/stores/git-log-preferences.store.ts @@ -4,6 +4,12 @@ import { createSelectors } from "@/utils/zustand-selectors"; import { createSafeJSONStorage } from "@/utils/zustand-storage"; import { normalizeRepositoryPath } from "../api/git-repo-api"; import type { GitReferenceKind } from "../types/git.types"; +import { + GIT_LOG_COLUMN_DEFAULT_WIDTHS, + clampGitLogColumnWidth, + normalizeGitLogColumnWidth, + type GitLogColumn, +} from "../utils/git-log-columns"; export type GitLogFilterScope = "text" | "author" | "branch"; @@ -19,6 +25,8 @@ interface GitLogPreferencesStore { showWorktreeRepositories: boolean; mainPanelLayout: GitLogPanelLayout; inspectorPanelLayout: GitLogPanelLayout; + authorColumnWidth: number; + dateColumnWidth: number; collapsedReferenceSections: GitReferenceKind[]; collapsedReferenceGroups: string[]; markedReferenceFullNamesByRepository: Record; @@ -30,6 +38,8 @@ interface GitLogPreferencesStore { setShowWorktreeRepositories: (show: boolean) => void; setMainPanelLayout: (layout: GitLogPanelLayout) => void; setInspectorPanelLayout: (layout: GitLogPanelLayout) => void; + /** Persists a commit-table column width; call once when a drag ends, not per pointer move. */ + setColumnWidth: (column: GitLogColumn, width: number) => void; toggleReferenceSection: (kind: GitReferenceKind) => void; toggleReferenceGroup: (id: string) => void; setReferenceExpansion: (sections: GitReferenceKind[], groups: string[]) => void; @@ -67,6 +77,8 @@ const useGitLogPreferencesStoreBase = create()( showWorktreeRepositories: true, mainPanelLayout: DEFAULT_MAIN_LAYOUT, inspectorPanelLayout: DEFAULT_INSPECTOR_LAYOUT, + authorColumnWidth: GIT_LOG_COLUMN_DEFAULT_WIDTHS.author, + dateColumnWidth: GIT_LOG_COLUMN_DEFAULT_WIDTHS.date, collapsedReferenceSections: [], collapsedReferenceGroups: [], markedReferenceFullNamesByRepository: {}, @@ -79,6 +91,12 @@ const useGitLogPreferencesStoreBase = create()( set({ showWorktreeRepositories }), setMainPanelLayout: (mainPanelLayout) => set({ mainPanelLayout }), setInspectorPanelLayout: (inspectorPanelLayout) => set({ inspectorPanelLayout }), + setColumnWidth: (column, width) => + set( + column === "author" + ? { authorColumnWidth: clampGitLogColumnWidth(column, width) } + : { dateColumnWidth: clampGitLogColumnWidth(column, width) }, + ), toggleReferenceSection: (kind) => set((state) => ({ collapsedReferenceSections: toggleListItem(state.collapsedReferenceSections, kind), @@ -148,6 +166,11 @@ const useGitLogPreferencesStoreBase = create()( markedReferenceFullNamesByRepository: persistedPreferences.markedReferenceFullNamesByRepository ?? {}, showWorktreeRepositories: persistedPreferences.showWorktreeRepositories ?? true, + authorColumnWidth: normalizeGitLogColumnWidth( + "author", + persistedPreferences.authorColumnWidth, + ), + dateColumnWidth: normalizeGitLogColumnWidth("date", persistedPreferences.dateColumnWidth), actions: currentState.actions, }; }, diff --git a/windows/tauri/src/features/git/types/git-diff.types.ts b/windows/tauri/src/features/git/types/git-diff.types.ts index 0912597ae..d17aa1486 100644 --- a/windows/tauri/src/features/git/types/git-diff.types.ts +++ b/windows/tauri/src/features/git/types/git-diff.types.ts @@ -129,6 +129,8 @@ export interface MultiFileDiff { fileLabels?: string[]; initiallyExpandedFileKey?: string; initiallySelectedFileKey?: string; + /** Hides the changed-files navigator, e.g. for a single-file commit preview. */ + hideFileList?: boolean; /** Working-tree refresh identities keyed by file key. */ workingTreeTargets?: Record; isLoading?: boolean; diff --git a/windows/tauri/src/features/git/types/git.types.ts b/windows/tauri/src/features/git/types/git.types.ts index 7237fadb1..2ef12a13a 100644 --- a/windows/tauri/src/features/git/types/git.types.ts +++ b/windows/tauri/src/features/git/types/git.types.ts @@ -33,6 +33,8 @@ export interface GitCommit { author: string; email?: string; date: string; + /** Author's UTC offset in minutes (east positive) for `date`, when Core reports one. */ + dateUtcOffsetMinutes?: number; decorations: string; } diff --git a/windows/tauri/src/features/git/utils/git-log-columns.test.ts b/windows/tauri/src/features/git/utils/git-log-columns.test.ts new file mode 100644 index 000000000..4f60e3008 --- /dev/null +++ b/windows/tauri/src/features/git/utils/git-log-columns.test.ts @@ -0,0 +1,25 @@ +import { describe, expect, test } from "bun:test"; +import { + GIT_LOG_COLUMN_DEFAULT_WIDTHS, + clampGitLogColumnWidth, + normalizeGitLogColumnWidth, +} from "./git-log-columns"; + +describe("git log column widths", () => { + test("clamps drag results to a readable range per column", () => { + expect(clampGitLogColumnWidth("author", 0)).toBe(56); + expect(clampGitLogColumnWidth("author", 9999)).toBe(360); + expect(clampGitLogColumnWidth("date", 0)).toBe(72); + expect(clampGitLogColumnWidth("date", 9999)).toBe(280); + expect(clampGitLogColumnWidth("author", 140.4)).toBe(140); + }); + + test("restores defaults for missing or corrupt persisted values", () => { + expect(normalizeGitLogColumnWidth("author", undefined)).toBe( + GIT_LOG_COLUMN_DEFAULT_WIDTHS.author, + ); + expect(normalizeGitLogColumnWidth("date", "wide")).toBe(GIT_LOG_COLUMN_DEFAULT_WIDTHS.date); + expect(normalizeGitLogColumnWidth("date", Number.NaN)).toBe(GIT_LOG_COLUMN_DEFAULT_WIDTHS.date); + expect(normalizeGitLogColumnWidth("author", 5000)).toBe(360); + }); +}); diff --git a/windows/tauri/src/features/git/utils/git-log-columns.ts b/windows/tauri/src/features/git/utils/git-log-columns.ts new file mode 100644 index 000000000..950f426f7 --- /dev/null +++ b/windows/tauri/src/features/git/utils/git-log-columns.ts @@ -0,0 +1,41 @@ +export type GitLogColumn = "author" | "date"; + +export interface GitLogColumnWidths { + author: number; + date: number; +} + +export const GIT_LOG_COLUMN_DEFAULT_WIDTHS: GitLogColumnWidths = { author: 112, date: 144 }; + +// Bounds keep a column readable and stop a runaway drag from pushing the commit message out. +const GIT_LOG_COLUMN_MIN_WIDTHS: GitLogColumnWidths = { author: 56, date: 72 }; +const GIT_LOG_COLUMN_MAX_WIDTHS: GitLogColumnWidths = { author: 360, date: 280 }; + +/** + * Width reserved for the graph and commit message, on top of both columns and the row padding. + * It equals the previous fixed 520px row minimum minus the default author and date widths. + */ +export const GIT_LOG_MESSAGE_MIN_WIDTH = 280; + +/** CSS variables carrying the live column widths; set on the table's scroll container. */ +export const GIT_LOG_COLUMN_CSS_VARIABLES: Record = { + author: "--git-log-author-width", + date: "--git-log-date-width", +}; + +export function clampGitLogColumnWidth(column: GitLogColumn, width: number): number { + if (!Number.isFinite(width)) return GIT_LOG_COLUMN_DEFAULT_WIDTHS[column]; + return Math.round( + Math.min( + Math.max(width, GIT_LOG_COLUMN_MIN_WIDTHS[column]), + GIT_LOG_COLUMN_MAX_WIDTHS[column], + ), + ); +} + +/** Restores a persisted width, falling back to the default for missing or corrupt values. */ +export function normalizeGitLogColumnWidth(column: GitLogColumn, width: unknown): number { + return typeof width === "number" + ? clampGitLogColumnWidth(column, width) + : GIT_LOG_COLUMN_DEFAULT_WIDTHS[column]; +} diff --git a/windows/tauri/src/features/git/utils/git-log-date.test.ts b/windows/tauri/src/features/git/utils/git-log-date.test.ts new file mode 100644 index 000000000..b021e4727 --- /dev/null +++ b/windows/tauri/src/features/git/utils/git-log-date.test.ts @@ -0,0 +1,79 @@ +import { describe, expect, test } from "bun:test"; +import { formatGitLogDate } from "./git-log-date"; + +// Echoes the key and values so assertions stay independent of locale copy. +const t = (key: string, values?: Record) => + values ? `${key}:${JSON.stringify(values)}` : key; + +const am = (time: string) => `git.log.timeAm:${JSON.stringify({ time })}`; +const pm = (time: string) => `git.log.timePm:${JSON.stringify({ time })}`; + +// Local time on purpose: Git's displayed date is interpreted in the machine's zone, and the +// offset passed alongside it is the local one so relative labels apply. +const now = new Date(2026, 8, 10, 15, 30); +const local = -now.getTimezoneOffset(); + +describe("formatGitLogDate", () => { + test("shows minutes ago for commits within the last hour", () => { + expect(formatGitLogDate("2026/09/10 15:30", t, local, now)).toBe("git.log.dateJustNow"); + expect(formatGitLogDate("2026/09/10 15:29", t, local, now)).toBe( + 'git.log.dateMinuteAgoOne:{"count":1}', + ); + expect(formatGitLogDate("2026/09/10 15:05", t, local, now)).toBe( + 'git.log.dateMinutesAgo:{"count":25}', + ); + expect(formatGitLogDate("2026/09/10 14:31", t, local, now)).toBe( + 'git.log.dateMinutesAgo:{"count":59}', + ); + }); + + test("shows Today and Yesterday with a localized 12-hour time once past an hour", () => { + expect(formatGitLogDate("2026/09/10 14:30", t, local, now)).toBe( + `git.log.dateToday:${JSON.stringify({ time: pm("2:30") })}`, + ); + expect(formatGitLogDate("2026/09/10 00:05", t, local, now)).toBe( + `git.log.dateToday:${JSON.stringify({ time: am("12:05") })}`, + ); + expect(formatGitLogDate("2026/09/09 23:59", t, local, now)).toBe( + `git.log.dateYesterday:${JSON.stringify({ time: pm("11:59") })}`, + ); + expect(formatGitLogDate("2026/09/09 12:00", t, local, now)).toBe( + `git.log.dateYesterday:${JSON.stringify({ time: pm("12:00") })}`, + ); + }); + + test("shows the calendar date with a 12-hour time for older commits", () => { + expect(formatGitLogDate("2026/09/08 09:07", t, local, now)).toBe( + `git.log.dateAbsolute:${JSON.stringify({ year: "2026", month: "09", day: "08", time: am("9:07") })}`, + ); + expect(formatGitLogDate("2025/12/31 21:45", t, local, now)).toBe( + `git.log.dateAbsolute:${JSON.stringify({ year: "2025", month: "12", day: "31", time: pm("9:45") })}`, + ); + }); + + test("measures Yesterday by calendar day, not by 24 elapsed hours", () => { + const justAfterMidnight = new Date(2026, 8, 10, 0, 10); + expect(formatGitLogDate("2026/09/09 23:50", t, local, justAfterMidnight)).toBe( + 'git.log.dateMinutesAgo:{"count":20}', + ); + expect(formatGitLogDate("2026/09/09 22:00", t, local, justAfterMidnight)).toBe( + `git.log.dateYesterday:${JSON.stringify({ time: pm("10:00") })}`, + ); + }); + + // Regression: a commit authored in another zone carries that zone's wall-clock time, so + // comparing it with local "now" would produce wrong "minutes ago" and "Today" labels. + test("shows only the calendar date when the author's zone differs or is unknown", () => { + const absoluteToday = `git.log.dateAbsolute:${JSON.stringify({ year: "2026", month: "09", day: "10", time: pm("3:25") })}`; + expect(formatGitLogDate("2026/09/10 15:25", t, local + 60, now)).toBe(absoluteToday); + expect(formatGitLogDate("2026/09/10 15:25", t, null, now)).toBe(absoluteToday); + expect(formatGitLogDate("2026/09/10 15:25", t, undefined, now)).toBe(absoluteToday); + }); + + test("falls back to absolute formats for future dates and passes bad input through", () => { + expect(formatGitLogDate("2026/09/10 16:00", t, local, now)).toBe( + `git.log.dateToday:${JSON.stringify({ time: pm("4:00") })}`, + ); + expect(formatGitLogDate("not a date", t, local, now)).toBe("not a date"); + }); +}); diff --git a/windows/tauri/src/features/git/utils/git-log-date.ts b/windows/tauri/src/features/git/utils/git-log-date.ts new file mode 100644 index 000000000..dfd011dea --- /dev/null +++ b/windows/tauri/src/features/git/utils/git-log-date.ts @@ -0,0 +1,65 @@ +type Translate = (key: string, values?: Record) => string; + +// Matches the displayed part of Core's `--date=format:%Y/%m/%d %H:%M %z` commit dates; Core +// moves the `%z` offset into `dateUtcOffsetMinutes`. +const GIT_LOG_DATE_PATTERN = /^(\d{4})\/(\d{2})\/(\d{2}) (\d{2}):(\d{2})$/; +const MS_PER_MINUTE = 60_000; +const MINUTES_PER_HOUR = 60; + +function formatTwelveHourTime(hours: number, minutes: number, t: Translate): string { + const displayHour = hours % 12 === 0 ? 12 : hours % 12; + const time = `${displayHour}:${String(minutes).padStart(2, "0")}`; + // The period word and its position are locale copy: "2:30 PM" in English, "下午 2:30" in Chinese. + return t(hours < 12 ? "git.log.timeAm" : "git.log.timePm", { time }); +} + +function startOfDay(date: Date): number { + return new Date(date.getFullYear(), date.getMonth(), date.getDate()).getTime(); +} + +/** + * Formats a Git log commit date the way IDEA's commit history does: "N minutes ago" within the + * last hour, "Today"/"Yesterday" with a 12-hour time, and the calendar date with a 12-hour time + * otherwise. Unparseable input is returned unchanged so unusual backend values stay visible. + * + * Git reports the date in the author's time zone. Relative labels are only meaningful when that + * zone matches the local one, so a missing or different `utcOffsetMinutes` always produces the + * calendar date. + */ +export function formatGitLogDate( + dateString: string, + t: Translate, + utcOffsetMinutes: number | null | undefined, + now: Date = new Date(), +): string { + const match = GIT_LOG_DATE_PATTERN.exec(dateString); + if (!match) return dateString; + + const [year, month, day, hours, minutes] = match.slice(1).map(Number); + const date = new Date(year, month - 1, day, hours, minutes); + if (Number.isNaN(date.getTime())) return dateString; + + const time = formatTwelveHourTime(hours, minutes, t); + const absolute = () => + // Field order is locale copy: English reads day/month/year, Chinese year/month/day. + t("git.log.dateAbsolute", { year: match[1], month: match[2], day: match[3], time }); + + // `getTimezoneOffset` is minutes west of UTC; Core reports minutes east. + const isLocalTimeZone = utcOffsetMinutes != null && utcOffsetMinutes === -date.getTimezoneOffset(); + if (!isLocalTimeZone) return absolute(); + + const elapsedMinutes = Math.floor((now.getTime() - date.getTime()) / MS_PER_MINUTE); + // Negative values come from clock skew; fall through to the absolute formats instead of + // showing a nonsensical "-3 minutes ago". + if (elapsedMinutes >= 0 && elapsedMinutes < MINUTES_PER_HOUR) { + if (elapsedMinutes < 1) return t("git.log.dateJustNow"); + return t(elapsedMinutes === 1 ? "git.log.dateMinuteAgoOne" : "git.log.dateMinutesAgo", { + count: elapsedMinutes, + }); + } + + const dayDifference = Math.round((startOfDay(now) - startOfDay(date)) / (24 * 60 * MS_PER_MINUTE)); + if (dayDifference === 0) return t("git.log.dateToday", { time }); + if (dayDifference === 1) return t("git.log.dateYesterday", { time }); + return absolute(); +} diff --git a/windows/tauri/src/features/git/utils/multi-file-diff.test.ts b/windows/tauri/src/features/git/utils/multi-file-diff.test.ts index 04a62a9d0..88fe9433f 100644 --- a/windows/tauri/src/features/git/utils/multi-file-diff.test.ts +++ b/windows/tauri/src/features/git/utils/multi-file-diff.test.ts @@ -1,6 +1,11 @@ import { describe, expect, test } from "bun:test"; import type { GitDiff } from "../types/git.types"; -import { createCommitDiffBuffer, createMultiFileDiff } from "./multi-file-diff"; +import { + COMMIT_FILE_PREVIEW_PATH, + createCommitDiffBuffer, + createCommitFileDiffPreview, + createMultiFileDiff, +} from "./multi-file-diff"; const diff = (filePath: string, additions: number, deletions: number): GitDiff => ({ file_path: filePath, @@ -49,12 +54,53 @@ describe("commit diff buffers", () => { }); expect(multiDiff.files).toBe(diffs); - expect(multiDiff.fileKeys).toEqual([ - "newest:src/shared.ts", - "oldest:src/shared.ts", - ]); + expect(multiDiff.fileKeys).toEqual(["newest:src/shared.ts", "oldest:src/shared.ts"]); expect(multiDiff.fileLabels).toEqual(["newest", "oldest"]); expect(multiDiff.initiallyExpandedFileKey).toBe("newest:src/shared.ts"); expect(multiDiff.initiallySelectedFileKey).toBe("newest:src/shared.ts"); }); }); + +describe("commit file preview", () => { + test("shows only the selected file without commit metadata in a reusable tab", () => { + const selected = diff("src/feature.ts", 4, 3); + const preview = createCommitFileDiffPreview({ + repoPath: "C:/repo", + commitHash: "1234567890abcdef", + diffs: [selected], + filePath: selected.file_path, + label: "1234567", + }); + + if (!preview) throw new Error("Expected file preview"); + expect(preview.virtualPath).toBe(COMMIT_FILE_PREVIEW_PATH); + expect(preview.displayName).toBe("feature.ts (1234567)"); + expect(preview.diffData.files).toEqual([selected]); + expect(preview.diffData.totalFiles).toBe(1); + expect(preview.diffData.commitMessage).toBeUndefined(); + expect(preview.diffData.hideFileList).toBe(true); + expect(preview.diffData.initiallySelectedFileKey).toBe("src/feature.ts:0"); + }); + + test("finds a diff by its new, current or original path", () => { + const renamed: GitDiff = { + ...diff("src/new.ts", 1, 1), + old_path: "src/old.ts", + new_path: "src/new.ts", + is_renamed: true, + }; + const diffs = [diff("src/main.ts", 2, 1), renamed]; + + const preview = (filePath: string) => + createCommitFileDiffPreview({ + repoPath: "C:/repo", + commitHash: "abc", + diffs, + filePath, + label: "abc", + }); + expect(preview("src/new.ts")?.diffData.files).toEqual([renamed]); + expect(preview("src/old.ts")?.diffData.files).toEqual([renamed]); + expect(preview("src/missing.ts")).toBeNull(); + }); +}); diff --git a/windows/tauri/src/features/git/utils/multi-file-diff.ts b/windows/tauri/src/features/git/utils/multi-file-diff.ts index b35fd1b1a..7a86902cf 100644 --- a/windows/tauri/src/features/git/utils/multi-file-diff.ts +++ b/windows/tauri/src/features/git/utils/multi-file-diff.ts @@ -87,3 +87,54 @@ export function createCommitDiffBuffer({ diffData, }; } + +/** Stable path so every commit-file preview reuses one editor tab instead of opening a new one. */ +export const COMMIT_FILE_PREVIEW_PATH = "diff://commit-file-preview"; + +/** + * Builds a single-file diff for the commit-files preview. Unlike the commit diff buffer it carries + * no commit message metadata: the preview shows only the selected file's changes. + */ +export function createCommitFileDiffPreview({ + repoPath, + commitHash, + diffs, + filePath, + fileKeys, + fileLabels, + label, +}: { + repoPath: string; + commitHash: string; + diffs: readonly GitDiff[]; + filePath: string; + fileKeys?: string[]; + fileLabels?: string[]; + /** Short revision label shown next to the file name in the tab title. */ + label: string; +}) { + // Disconnected selections can contain several revisions of the same path. + // Keep every matching section and its commit identity, rather than taking the first match. + const indexes = diffs.flatMap((diff, index) => + [diff.new_path, diff.file_path, diff.old_path].includes(filePath) ? [index] : [], + ); + if (indexes.length === 0) return null; + const fileName = filePath.split("/").pop() ?? filePath; + const diffData = createMultiFileDiff({ + title: fileName, + repoPath, + commitHash, + diffs: indexes.map((index) => diffs[index]), + initialFilePath: filePath, + fileKeys: fileKeys ? indexes.map((index) => fileKeys[index]) : undefined, + fileLabels: fileLabels ? indexes.map((index) => fileLabels[index]) : undefined, + }); + diffData.hideFileList = true; + diffData.totalFiles = 1; + + return { + virtualPath: COMMIT_FILE_PREVIEW_PATH, + displayName: `${fileName} (${label})`, + diffData, + }; +} diff --git a/windows/tauri/src/features/keymaps/defaults/default-keymaps.git-log.test.ts b/windows/tauri/src/features/keymaps/defaults/default-keymaps.git-log.test.ts index fd770c2db..d2eb20e51 100644 --- a/windows/tauri/src/features/keymaps/defaults/default-keymaps.git-log.test.ts +++ b/windows/tauri/src/features/keymaps/defaults/default-keymaps.git-log.test.ts @@ -31,12 +31,9 @@ describe("Git Log workbench entry points", () => { }); }); - test("places Git Log on the bottom activity rail above Settings", () => { + test("places Git Log on the bottom activity rail after Run", () => { expect(SIDEBAR_BOTTOM_ACTIVITY_ITEM_IDS.indexOf("run")).toBeLessThan( SIDEBAR_BOTTOM_ACTIVITY_ITEM_IDS.indexOf("gitLog"), ); - expect(SIDEBAR_BOTTOM_ACTIVITY_ITEM_IDS.indexOf("gitLog")).toBeLessThan( - SIDEBAR_BOTTOM_ACTIVITY_ITEM_IDS.indexOf("settings"), - ); }); }); diff --git a/windows/tauri/src/features/layout/components/bottom-pane/bottom-pane.tsx b/windows/tauri/src/features/layout/components/bottom-pane/bottom-pane.tsx index 496e0d8f7..1ca59af7c 100644 --- a/windows/tauri/src/features/layout/components/bottom-pane/bottom-pane.tsx +++ b/windows/tauri/src/features/layout/components/bottom-pane/bottom-pane.tsx @@ -223,29 +223,18 @@ const BottomPane = () => { const resizeGutter = !isFullScreen ? (
-
-
+ /> ) : null; const paneContent = (
+ {/* Both activity rails stay outside the middle column so they span the full + workbench height; the full-width bottom pane only stretches between them. */} -
-
- - - +
+ + +
+
+ + + +
+ {terminalWidthMode === "editor" && deferredSurfacesReady && ( + + + + )} +
+ +
- {terminalWidthMode === "editor" && deferredSurfacesReady && ( + + {terminalWidthMode === "full" && deferredSurfacesReady && ( )}
-
- - {terminalWidthMode === "full" && deferredSurfacesReady && ( -
- - - -
- )}
{showStatusBar ?
: null} diff --git a/windows/tauri/src/features/layout/components/plugin-activity-rail.test.ts b/windows/tauri/src/features/layout/components/plugin-activity-rail.test.ts index 73a012acf..02c6bccdf 100644 --- a/windows/tauri/src/features/layout/components/plugin-activity-rail.test.ts +++ b/windows/tauri/src/features/layout/components/plugin-activity-rail.test.ts @@ -7,10 +7,13 @@ test("right activity rail opens the real singleton Extensions buffer", async () expect(railSource).toContain( "const openExtensionsBuffer = useBufferStore.use.actions().openExtensionsBuffer;", ); - expect(railSource).toContain('buffer.type === "extensions"'); + expect(railSource).toContain('activeBuffer?.type === "extensions"'); expect(railSource).toContain('const extensionsLabel = t("extensions.title");'); expect(railSource).toContain("aria-pressed={isExtensionsActive}"); - expect(railSource).toContain("onClick={openExtensionsBuffer}"); + expect(railSource).toContain("onClick={toggleExtensionsBuffer}"); + // A second click on the active Extensions tab closes it instead of reopening. + expect(railSource).toContain("closeBuffer(activeExtensionsBufferId);"); + expect(railSource).toContain("openExtensionsBuffer();"); expect(railSource).not.toContain("useState"); expect(layoutSource).toContain(""); }); diff --git a/windows/tauri/src/features/layout/components/plugin-activity-rail.tsx b/windows/tauri/src/features/layout/components/plugin-activity-rail.tsx index 936c4e47c..9f9b68c3c 100644 --- a/windows/tauri/src/features/layout/components/plugin-activity-rail.tsx +++ b/windows/tauri/src/features/layout/components/plugin-activity-rail.tsx @@ -11,13 +11,24 @@ import { PuzzlePieceIcon } from "@/ui/icons"; export function PluginActivityRail() { const { t } = useTranslation(); const openExtensionsBuffer = useBufferStore.use.actions().openExtensionsBuffer; - const isExtensionsActive = useBufferStore((state) => { - if (!state.activeBufferId) return false; + const closeBuffer = useBufferStore.use.actions().closeBuffer; + const activeExtensionsBufferId = useBufferStore((state) => { + if (!state.activeBufferId) return null; - return state.buffers.some( - (buffer) => buffer.id === state.activeBufferId && buffer.type === "extensions", - ); + const activeBuffer = state.buffers.find((buffer) => buffer.id === state.activeBufferId); + return activeBuffer?.type === "extensions" ? activeBuffer.id : null; }); + const isExtensionsActive = activeExtensionsBufferId !== null; + + // The Extensions page is a singleton buffer: a second click while it is the + // active tab closes it, otherwise the click opens or focuses it. + const toggleExtensionsBuffer = () => { + if (activeExtensionsBufferId) { + closeBuffer(activeExtensionsBufferId); + return; + } + openExtensionsBuffer(); + }; const isMavenAvailable = useMavenStore( (state) => Boolean(state.project) || state.projectStatus === "failed", ); @@ -43,7 +54,7 @@ export function PluginActivityRail() { aria-label={extensionsLabel} aria-pressed={isExtensionsActive} className="rounded-sm" - onClick={openExtensionsBuffer} + onClick={toggleExtensionsBuffer} > diff --git a/windows/tauri/src/features/layout/components/resizable-pane.tsx b/windows/tauri/src/features/layout/components/resizable-pane.tsx index 757ba427c..2137fb4ab 100644 --- a/windows/tauri/src/features/layout/components/resizable-pane.tsx +++ b/windows/tauri/src/features/layout/components/resizable-pane.tsx @@ -141,7 +141,7 @@ export function ResizablePane({ document.addEventListener("mousemove", handleMouseMove); document.addEventListener("mouseup", handleMouseUp); - document.body.style.cursor = "col-resize"; + document.body.style.cursor = "ew-resize"; document.body.style.userSelect = "none"; }, [width, position, widthKey, updateSetting, clampWidth], @@ -154,12 +154,12 @@ export function ResizablePane({ onMouseDown={handleMouseDown} style={ position === "left" - ? { right: "calc(var(--lithe-workbench-gap) * -1)" } + ? { right: "calc(var(--lithe-sidebar-gap) * -1)" } : { left: "calc(var(--lithe-workbench-gap) * -1)" } } className={cn( - "group absolute top-0 z-30 flex h-full w-(--lithe-workbench-gap) cursor-col-resize items-center justify-center", - "transition-colors duration-(--app-duration-fast) ease-(--app-ease-smooth) hover:bg-primary/8", + "absolute top-0 z-30 flex h-full cursor-ew-resize items-center justify-center", + position === "left" ? "w-(--lithe-sidebar-gap)" : "w-(--lithe-workbench-gap)", )} role="separator" aria-orientation="vertical" @@ -168,14 +168,7 @@ export function ResizablePane({ aria-valuemin={Math.round(getMinWidth())} aria-valuemax={Math.round(getMaxWidth())} tabIndex={0} - > -
-
+ /> ) : null; return ( @@ -185,14 +178,14 @@ export function ResizablePane({ className={cn( "lithe-resizable-pane relative flex h-full min-w-0 shrink-0 overflow-visible bg-transparent", hidden && "pointer-events-none", - !hidden && position === "left" && "mr-(--lithe-workbench-gap)", + !hidden && position === "left" && "mr-(--lithe-sidebar-gap)", !hidden && position === "right" && "ml-(--lithe-workbench-gap)", className, )} aria-hidden={hidden} > {position === "right" ? resizeHandle : null} - {isResizing &&
} + {isResizing &&
}
-编辑器 · 16 个能力点 +编辑器 · 19 个能力点 | 功能组 | 能力点 | macOS | Windows | 负责人 | 验证方式 | 备注 | | --- | --- | --- | --- | --- | --- | --- | @@ -92,6 +92,9 @@ | 文本编辑 | **按指定编码重新打开文本文件**
editor-file-encoding-reopen | ✅ 已实现
🔍 待验证
`macos/Sources/Lithe/Views/Workbench/WorkbenchView.swift`、`macos/Sources/Lithe/Views/Editor/StandaloneEditorView.swift`、`macos/Sources/Lithe/Application/Features/DocumentFeatureModel.swift`、`macos/Sources/Lithe/Platform/MacOS/FileSystem/MacDocumentEncoding.swift` | ✅ 已实现
🔍 待验证
`windows/tauri/src/features/command-palette/components/encoding-picker.tsx`、`windows/tauri/src/features/editor/services/document-encoding-workflow.ts`、`windows/tauri/crates/project/src/document_file.rs` | Editor | 在 macOS 和 Windows 实机验证 UTF-8、UTF-8 BOM 与 GBK/GB18030 的自动识别,并分别验证 Shift JIS、Windows-1252 的指定编码重新打开、编码转换保存、脏文件选择和外部修改保护。 | | | 文本编辑 | **按指定编码保存文本文件**
editor-file-encoding-save | ✅ 已实现
🔍 待验证
`macos/Sources/Lithe/Views/Workbench/WorkbenchView.swift`、`macos/Sources/Lithe/Views/Editor/StandaloneEditorView.swift`、`macos/Sources/Lithe/Application/Features/DocumentFeatureModel.swift`、`macos/Sources/Lithe/Platform/MacOS/FileSystem/MacDocumentEncoding.swift` | ✅ 已实现
🔍 待验证
`windows/tauri/src/features/command-palette/components/encoding-picker.tsx`、`windows/tauri/src/features/editor/stores/editor-app.store.ts`、`windows/tauri/crates/project/src/document_file.rs` | Editor | 在 macOS 和 Windows 实机验证 UTF-8、UTF-8 BOM 与 GBK/GB18030 的自动识别,并分别验证 Shift JIS、Windows-1252 的指定编码重新打开、编码转换保存、脏文件选择和外部修改保护。 | | | 文本编辑 | **中文 properties 与未知扩展名文本的一致打开和恢复**
editor-unicode-text-classification | ✅ 已实现
🔍 待验证
`macos/Sources/Lithe/Core/Rust/RustTextContentPolicy.swift`、`macos/Tests/LitheTests/TextContentPolicyTests.swift`、`frontend/editor/src/language-contributions.ts` | ✅ 已实现
🔍 待验证
`windows/tauri/src/features/file-system/controllers/workspace-session-restore.ts`、`windows/tauri/src/features/file-system/controllers/workspace-session-restore.test.ts`、`frontend/editor/src/language-contributions.ts` | Editor | 双端打开高中文比例的 .properties 和未知扩展名 UTF-8 文件,编辑保存后恢复会话并验证内容及编码;验证 properties 高亮、emoji 跨边界、NUL 和控制字符拒绝、读取错误独立报告,并执行 text-content-v1.json 共享规则样例。 | | +| Git 变更 | **编辑器 Git 行变更标记、点击预览与变更导航**
editor-git-gutter-peek | ✅ 已实现
🔍 待验证
`macos/Sources/Lithe/Views/Editor/CodeEditorView.swift` | ✅ 已实现
🔍 待验证
`windows/tauri/src/features/editor/engines/monaco/git-gutter-controller.ts`、`windows/tauri/src/features/editor/engines/monaco/git-gutter-peek-widget.ts`、`windows/tauri/src/features/git/utils/git-gutter-changes.test.ts` | Editor / Git | 在 Windows 打开已跟踪文件,新增、修改、删除内容并保存,点击标记查看差异、切换前后变更、按 Escape 关闭;确认多仓库同名文件互不影响。切换浅色、深色及自定义主色,检查面板边框、焦点和 Git 语义色;关闭 Git 标记设置后标记消失。macOS 检查对应行变更入口。Linux 单元测试不替代目标平台运行验证。 | | +| Git 变更 | **从编辑器行变更入口暂存单处修改**
editor-git-gutter-stage | ✅ 已实现
🔍 待验证
`macos/Sources/Lithe/Views/Editor/CodeEditorView.swift` | ✅ 已实现
🔍 待验证
`windows/tauri/src/features/git/services/git-gutter-actions.ts`、`windows/tauri/src/features/git/utils/git-gutter-changes.test.ts` | Editor / Git | 保存含多处增删改的已跟踪文件,单独暂存后一处修改,检查 index 只包含选中修改,邻近与前方未暂存修改保留;未保存或只读时禁用暂存。Windows 与 macOS 均需实机验证。 | | +| Git 变更 | **设置中开关编辑器 Git 变更标记**
editor-git-gutter-setting | 🧩 平台专属
— 不适用
`macos/Sources/Lithe/Views/Editor/CodeEditorView.swift` | ✅ 已实现
🔍 待验证
`windows/tauri/src/features/settings/components/tabs/git-settings.tsx`、`windows/tauri/src/features/settings/config/default-settings.ts` | Editor / Git | Windows 默认显示 Git 变更标记;在 Git 设置中关闭时清除标记和预览,重新启用后恢复,重启后保留选择。此项记录 Windows 新增的独立开关。 | |
diff --git a/shared/platform-feature-matrix.json b/shared/platform-feature-matrix.json index ef781746f..ad707092a 100644 --- a/shared/platform-feature-matrix.json +++ b/shared/platform-feature-matrix.json @@ -3025,6 +3025,76 @@ }, "owner": "Run/Debug", "verification": "在双端运行 Spring Boot HTTP 服务,修改方法体并更新,检查响应、PID、DevTools 日志与断点暂停状态;覆盖编译失败、JVM 拒绝结构修改、连续点击、停止/重启、依赖路径变化。对比运行前后安装目录清单/哈希。Linux 仅完成共享 Core 和前端测试,原生验证待完成。" + }, + { + "id": "editor-git-gutter-peek", + "area": "编辑器", + "group": "Git 变更", + "capability": "编辑器 Git 行变更标记、点击预览与变更导航", + "macos": { + "implementationStatus": "implemented", + "verificationStatus": "pending", + "evidence": [ + "macos/Sources/Lithe/Views/Editor/CodeEditorView.swift" + ] + }, + "windows": { + "implementationStatus": "implemented", + "verificationStatus": "pending", + "evidence": [ + "windows/tauri/src/features/editor/engines/monaco/git-gutter-controller.ts", + "windows/tauri/src/features/editor/engines/monaco/git-gutter-peek-widget.ts", + "windows/tauri/src/features/git/utils/git-gutter-changes.test.ts" + ] + }, + "owner": "Editor / Git", + "verification": "在 Windows 打开已跟踪文件,新增、修改、删除内容并保存,点击标记查看差异、切换前后变更、按 Escape 关闭;确认多仓库同名文件互不影响。切换浅色、深色及自定义主色,检查面板边框、焦点和 Git 语义色;关闭 Git 标记设置后标记消失。macOS 检查对应行变更入口。Linux 单元测试不替代目标平台运行验证。" + }, + { + "id": "editor-git-gutter-stage", + "area": "编辑器", + "group": "Git 变更", + "capability": "从编辑器行变更入口暂存单处修改", + "macos": { + "implementationStatus": "implemented", + "verificationStatus": "pending", + "evidence": [ + "macos/Sources/Lithe/Views/Editor/CodeEditorView.swift" + ] + }, + "windows": { + "implementationStatus": "implemented", + "verificationStatus": "pending", + "evidence": [ + "windows/tauri/src/features/git/services/git-gutter-actions.ts", + "windows/tauri/src/features/git/utils/git-gutter-changes.test.ts" + ] + }, + "owner": "Editor / Git", + "verification": "保存含多处增删改的已跟踪文件,单独暂存后一处修改,检查 index 只包含选中修改,邻近与前方未暂存修改保留;未保存或只读时禁用暂存。Windows 与 macOS 均需实机验证。" + }, + { + "id": "editor-git-gutter-setting", + "area": "编辑器", + "group": "Git 变更", + "capability": "设置中开关编辑器 Git 变更标记", + "macos": { + "implementationStatus": "platform-specific", + "verificationStatus": "not-applicable", + "evidence": [ + "macos/Sources/Lithe/Views/Editor/CodeEditorView.swift" + ] + }, + "windows": { + "implementationStatus": "implemented", + "verificationStatus": "pending", + "evidence": [ + "windows/tauri/src/features/settings/components/tabs/git-settings.tsx", + "windows/tauri/src/features/settings/config/default-settings.ts" + ] + }, + "owner": "Editor / Git", + "verification": "Windows 默认显示 Git 变更标记;在 Git 设置中关闭时清除标记和预览,重新启用后恢复,重启后保留选择。此项记录 Windows 新增的独立开关。" } ] } diff --git a/windows/tauri/src/features/editor/components/monaco-editor.tsx b/windows/tauri/src/features/editor/components/monaco-editor.tsx index 6f9e17408..02c87c723 100644 --- a/windows/tauri/src/features/editor/components/monaco-editor.tsx +++ b/windows/tauri/src/features/editor/components/monaco-editor.tsx @@ -35,6 +35,16 @@ import { useInlineEditToolbarStore } from "@/features/editor/stores/inline-edit- import { useFileSystemStore } from "@/features/file-system/stores/file-system.store"; import { useActiveWorkspaceId } from "@/features/workspace/stores/create-workspace-scoped-store"; import { useGitBlame } from "@/features/git/hooks/use-git-blame"; +import { useGitGutterBase, type GitGutterBase } from "@/features/git/hooks/use-git-gutter-base"; +import { + openGitGutterFullDiff, + stageGitGutterChange, +} from "@/features/git/services/git-gutter-actions"; +import { + createGitGutterController, + type GitGutterController, + type GitGutterControllerOptions, +} from "../engines/monaco/git-gutter-controller"; import { keymapRegistry } from "@/features/keymaps/utils/registry"; import { useSettingsStore } from "@/features/settings/stores/settings.store"; import { openMavenRunPane } from "@/features/maven/actions/maven-tool-window-actions"; @@ -151,6 +161,50 @@ import { toast } from "sonner"; registerMonacoLspProviders(); registerMonacoCodeLensProvider(); +interface GitGutterRenderContext { + base: GitGutterBase | null; + canStage: boolean; + languageId: string; + t: (key: string, values?: Record) => string; +} + +/** Binds the long-lived gutter controller to the editor's latest render values. */ +function gitGutterControllerOptions( + context: () => GitGutterRenderContext | null, +): GitGutterControllerOptions { + const translate = (key: string, values?: Record) => + context()?.t(key, values) ?? key; + return { + language: () => context()?.languageId ?? "plaintext", + labels: (peek) => ({ + title: translate("git.gutter.title", { index: peek.index + 1, total: peek.total }), + previous: translate("git.gutter.previous"), + next: translate("git.gutter.next"), + stage: translate("git.gutter.stage"), + stageUnavailable: translate("git.gutter.stageUnavailable"), + openDiff: translate("git.gutter.openDiff"), + close: translate("git.gutter.close"), + }), + markerTooltip: (kind) => translate(`git.gutter.${kind}`), + canStage: () => !!context()?.base && !!context()?.canStage, + stage: async (change, baseLines) => { + const base = context()?.base; + if (!base) return false; + const staged = await stageGitGutterChange(base, baseLines, change); + if (!staged) toast.error(translate("git.operationFailed")); + return staged; + }, + openDiff: () => { + const base = context()?.base; + if (!base) return; + void openGitGutterFullDiff(base, translate("git.diff.uncommitted")).catch((error) => { + console.error("Failed to open Git diff:", error); + toast.error(translate("git.operationFailed")); + }); + }, + }; +} + const EMPTY_DIAGNOSTICS: Diagnostic[] = []; const INACTIVE_CURSOR_POSITION: Position = { line: 0, column: 0, offset: 0 }; @@ -337,6 +391,9 @@ export function MonacoEditor({ // Latest Run-marker decorations, reapplied when the Monaco instance is recreated. const runMarkerDecorationSpecsRef = useRef([]); const renderInlineGitBlameRef = useRef<() => void>(() => {}); + const gitGutterControllerRef = useRef(null); + // Latest render values read by the long-lived gutter controller callbacks. + const gitGutterContextRef = useRef(null); const mouseSelectingRef = useRef(false); const mouseGestureStartRef = useRef(null); const suppressNextCursorSelectionSyncRef = useRef(false); @@ -374,6 +431,16 @@ export function MonacoEditor({ return current && current.type === "editor" ? (current.content ?? "") : ""; }, [contentRevision, editorBufferId]); const filePath = editorBuffer?.path ?? ""; + // Narrow selectors: staging needs saved text, and the gutter base reloads + // when the saved text changes. Neither re-renders on every keystroke. + const bufferIsDirty = useBufferStore((state) => { + const current = getBufferById(state.buffers, editorBufferId); + return current?.type === "editor" ? current.isDirty : false; + }); + const bufferSavedContent = useBufferStore((state) => { + const current = getBufferById(state.buffers, editorBufferId); + return current?.type === "editor" ? current.savedContent : ""; + }); const editorLanguage = editorBuffer?.language; const editorLanguageOverride = editorBuffer?.languageOverride; const documentUri = editorBuffer?.lspDocument?.documentUri; @@ -439,6 +506,7 @@ export function MonacoEditor({ javaMarkerRefreshRevision(state.lspStatus), ); const inlineGitBlameEnabled = useSettingsStore((state) => state.settings.enableInlineGitBlame); + const gitGutterEnabled = useSettingsStore((state) => state.settings.enableGitGutter); const workspaceId = useActiveWorkspaceId(); const rootFolderPath = useFileSystemStore((state) => state.rootFolderPath); const javaTestScope = useMemo( @@ -481,6 +549,20 @@ export function MonacoEditor({ : undefined, ); + const gitGutterActive = + gitGutterEnabled && enableExpensiveServices && !isPreviewMode && !editorBuffer?.isVirtual; + const gitGutterBase = useGitGutterBase( + gitGutterActive && filePath ? filePath : undefined, + () => bufferSavedContent, + bufferSavedContent, + ); + gitGutterContextRef.current = { + base: gitGutterBase, + canStage: !bufferIsDirty && !readOnly, + languageId: monacoLanguageId, + t, + }; + const renderInlineGitBlame = useCallback(() => { const editor = editorRef.current; const model = modelRef.current; @@ -964,6 +1046,16 @@ export function MonacoEditor({ runMarkerDecorationSpecsRef.current, ); runMarkerDecorationsRef.current = runMarkerDecorationCollection; + // The gutter controller lives exactly as long as this editor and model, + // so recreating the editor (file switch, option change) also disposes any + // open inline review. Its base arrives through `setBase`. + const gitGutterController = createGitGutterController( + editor, + model, + gitGutterControllerOptions(() => gitGutterContextRef.current), + ); + gitGutterControllerRef.current = gitGutterController; + gitGutterController.setBase(gitGutterContextRef.current?.base?.lines ?? null); previousContentRef.current = content; pendingLocalContentSnapshotsRef.current = []; if (filePath && fileOpenBenchmark.has(filePath)) { @@ -1488,6 +1580,10 @@ export function MonacoEditor({ } implementationMarkerOwnerRef.current = null; implementationMarkersRef.current = []; + gitGutterController.dispose(); + if (gitGutterControllerRef.current === gitGutterController) { + gitGutterControllerRef.current = null; + } runMarkerDecorationCollection.clear(); if (runMarkerDecorationsRef.current === runMarkerDecorationCollection) { runMarkerDecorationsRef.current = null; @@ -2266,6 +2362,14 @@ export function MonacoEditor({ scheduleInlineGitBlameRender(); }, [renderInlineGitBlame, scheduleInlineGitBlameRender]); + useEffect(() => { + gitGutterControllerRef.current?.setBase(gitGutterBase?.lines ?? null); + }, [gitGutterBase]); + + useEffect(() => { + gitGutterControllerRef.current?.refreshPeek(); + }, [bufferIsDirty, readOnly]); + useEffect(() => { const editor = editorRef.current; const model = modelRef.current; diff --git a/windows/tauri/src/features/editor/engines/monaco/git-gutter-controller.ts b/windows/tauri/src/features/editor/engines/monaco/git-gutter-controller.ts new file mode 100644 index 000000000..ace8b70cd --- /dev/null +++ b/windows/tauri/src/features/editor/engines/monaco/git-gutter-controller.ts @@ -0,0 +1,212 @@ +import { editor as monacoEditor, KeyCode } from "monaco-editor"; +import type * as Monaco from "monaco-editor"; +import { + computeGitGutterChanges, + gitGutterChangeIndexAtLine, + gitGutterMarkerLines, + gitGutterPeekAnchorLine, + gitGutterPeekRows, + trimFinalEmptyLine, + type GitGutterChange, +} from "@/features/git/utils/git-gutter-changes"; +import { GitGutterPeekSession, type GitGutterPeek } from "./git-gutter-peek-session"; +import { + createGitGutterPeekWidget, + type GitGutterPeekLabels, + type GitGutterPeekWidget, +} from "./git-gutter-peek-widget"; +import { monacoLineDiff } from "./monaco-line-diff"; + +export const GIT_GUTTER_MARKER_CLASS = "lithe-git-gutter"; + +/** Pause after typing before the gutter is recomputed against the Git base. */ +const GIT_GUTTER_RECOMPUTE_DELAY_MS = 250; + +export interface GitGutterControllerOptions { + language(): string; + labels(peek: GitGutterPeek): GitGutterPeekLabels; + markerTooltip(kind: GitGutterChange["kind"]): string; + /** Staging writes the change into the index, so it needs saved, writable text. */ + canStage(): boolean; + stage(change: GitGutterChange, base: readonly string[]): Promise; + openDiff(): void; +} + +export interface GitGutterController { + /** The index text to compare against, or null when the file has no Git base. */ + setBase(base: readonly string[] | null): void; + /** Re-evaluates button state, for example after the document was saved. */ + refreshPeek(): void; + dispose(): void; +} + +/** + * Git change markers in the line-decoration gutter and their inline review. + * Markers compare the live editor text with the Git index, so unsaved edits + * show up as changes. Any edit closes an open review; a review only acts on + * the change list computed for the current model version. + */ +export function createGitGutterController( + editor: Monaco.editor.IStandaloneCodeEditor, + model: Monaco.editor.ITextModel, + options: GitGutterControllerOptions, +): GitGutterController { + const decorations = editor.createDecorationsCollection(); + const session = new GitGutterPeekSession(); + let base: readonly string[] | null = null; + let widget: GitGutterPeekWidget | null = null; + let timer: ReturnType | null = null; + let staging = false; + let disposed = false; + + const closePeek = () => { + session.close(); + widget?.dispose(); + widget = null; + }; + + const editorLines = () => trimFinalEmptyLine(model.getLinesContent()); + + const render = (peek: GitGutterPeek | null) => { + if (!peek) { + closePeek(); + return; + } + widget ??= createGitGutterPeekWidget(editor, { + previous: () => navigate("previous"), + next: () => navigate("next"), + stage: () => void stage(), + openDiff: () => options.openDiff(), + close: () => { + closePeek(); + editor.focus(); + }, + }); + widget.show({ + rows: gitGutterPeekRows(session.changeList, peek.index, editorLines()), + anchorLine: gitGutterPeekAnchorLine(peek.change), + language: options.language(), + labels: options.labels(peek), + canNavigate: peek.total > 1, + canStage: base !== null && options.canStage(), + staging, + }); + }; + + const navigate = (direction: "previous" | "next") => { + if (disposed) return; + render(session.navigate(direction, model.getVersionId())); + }; + + const stage = async () => { + const peek = session.peek; + const snapshot = base; + const version = model.getVersionId(); + if (disposed || staging || !peek || !snapshot || !options.canStage()) return; + if (!session.isCurrent(peek, version)) return; + staging = true; + render(peek); + try { + // A successful write emits a Git change event; the refreshed base then + // drops this change from the list and closes the review. + await options.stage(peek.change, snapshot); + } finally { + staging = false; + if (!disposed && session.peek) render(session.peek); + } + }; + + const recompute = () => { + timer = null; + if (disposed || model.isDisposed()) return; + const changes = base ? computeGitGutterChanges(base, editorLines(), monacoLineDiff) : null; + const lineCount = model.getLineCount(); + decorations.set( + (changes ?? []).map((change) => { + const { start, end } = gitGutterMarkerLines(change, lineCount); + return { + range: { startLineNumber: start, startColumn: 1, endLineNumber: end, endColumn: 1 }, + options: { + isWholeLine: true, + linesDecorationsClassName: `${GIT_GUTTER_MARKER_CLASS} ${GIT_GUTTER_MARKER_CLASS}-${change.kind}`, + linesDecorationsTooltip: options.markerTooltip(change.kind), + stickiness: monacoEditor.TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges, + }, + }; + }), + ); + render(session.setChanges(changes ?? [], model.getVersionId())); + }; + + const schedule = (delay: number) => { + if (timer !== null) clearTimeout(timer); + timer = setTimeout(recompute, delay); + }; + + const disposables = [ + model.onDidChangeContent(() => { + // The review describes text that no longer exists; markers keep + // following Monaco's tracked ranges until the recompute lands. + closePeek(); + session.invalidate(); + schedule(GIT_GUTTER_RECOMPUTE_DELAY_MS); + }), + editor.onDidChangeModel(() => { + closePeek(); + session.invalidate(); + decorations.clear(); + }), + editor.onMouseDown((event) => { + if ( + event.target.type !== monacoEditor.MouseTargetType.GUTTER_LINE_DECORATIONS || + !event.target.position || + !event.event.leftButton || + !event.target.element?.classList.contains(GIT_GUTTER_MARKER_CLASS) + ) { + return; + } + event.event.preventDefault(); + event.event.stopPropagation(); + const version = model.getVersionId(); + const index = gitGutterChangeIndexAtLine( + session.changeList, + event.target.position.lineNumber, + model.getLineCount(), + ); + if (index < 0) return; + if (session.peek?.index === index) { + closePeek(); + return; + } + render(session.open(index, version)); + // Monaco keeps focus where it was on gutter clicks; take it so Escape + // closes the review, as in VS Code. + editor.focus(); + }), + editor.onKeyDown((event) => { + if (event.keyCode !== KeyCode.Escape || !session.peek) return; + event.preventDefault(); + event.stopPropagation(); + closePeek(); + }), + ]; + + return { + setBase(next) { + if (disposed) return; + base = next; + schedule(0); + }, + refreshPeek() { + if (!disposed && session.peek) render(session.peek); + }, + dispose() { + if (disposed) return; + disposed = true; + if (timer !== null) clearTimeout(timer); + disposables.forEach((disposable) => disposable.dispose()); + closePeek(); + decorations.clear(); + }, + }; +} diff --git a/windows/tauri/src/features/editor/engines/monaco/git-gutter-peek-session.test.ts b/windows/tauri/src/features/editor/engines/monaco/git-gutter-peek-session.test.ts new file mode 100644 index 000000000..9364930bd --- /dev/null +++ b/windows/tauri/src/features/editor/engines/monaco/git-gutter-peek-session.test.ts @@ -0,0 +1,84 @@ +import { describe, expect, test } from "bun:test"; +import { computeGitGutterChanges } from "@/features/git/utils/git-gutter-changes"; +import { GitGutterPeekSession } from "./git-gutter-peek-session"; +import { monacoLineDiff } from "./monaco-line-diff"; + +const BASE = ["a", "b", "c", "d", "e", "f"]; +const changesOf = (editor: string[]) => computeGitGutterChanges(BASE, editor, monacoLineDiff) ?? []; +const THREE_CHANGES = ["a", "B", "c", "D", "e", "F"]; + +describe("Git gutter inline review lifecycle", () => { + test("opens a change and navigates with wrap-around", () => { + const session = new GitGutterPeekSession(); + session.setChanges(changesOf(THREE_CHANGES), 7); + + expect(session.open(1, 7)?.index).toBe(1); + expect(session.navigate("next", 7)?.index).toBe(2); + expect(session.navigate("next", 7)?.index).toBe(0); + expect(session.navigate("previous", 7)?.index).toBe(2); + expect(session.peek?.total).toBe(3); + }); + + test("refuses to open a change list computed for another model version", () => { + const session = new GitGutterPeekSession(); + session.setChanges(changesOf(THREE_CHANGES), 7); + expect(session.open(0, 8)).toBeNull(); + expect(session.peek).toBeNull(); + }); + + test("an edit closes the review and invalidates the previous change list", () => { + const session = new GitGutterPeekSession(); + session.setChanges(changesOf(THREE_CHANGES), 7); + const peek = session.open(0, 7)!; + + session.invalidate(); + + expect(session.peek).toBeNull(); + expect(session.isCurrent(peek, 7)).toBe(false); + expect(session.navigate("next", 7)).toBeNull(); + }); + + test("a stale review cannot act after a newer review replaces it", () => { + const session = new GitGutterPeekSession(); + session.setChanges(changesOf(THREE_CHANGES), 7); + const first = session.open(0, 7)!; + const second = session.open(0, 7)!; + + expect(session.isCurrent(first, 7)).toBe(false); + expect(session.isCurrent(second, 7)).toBe(true); + expect(session.isCurrent(second, 8)).toBe(false); + }); + + test("a Git refresh keeps an unchanged review open and re-indexes it", () => { + const session = new GitGutterPeekSession(); + session.setChanges(changesOf(THREE_CHANGES), 7); + session.open(2, 7); + + // The first change was staged: the refreshed list no longer contains it. + const staged = computeGitGutterChanges(["a", "B", "c", "d", "e", "f"], THREE_CHANGES, monacoLineDiff)!; + const peek = session.setChanges(staged, 7); + + expect(peek?.index).toBe(1); + expect(peek?.total).toBe(2); + expect(peek?.change.modifiedLines).toEqual(["F"]); + }); + + test("a refresh that removes the open change closes the review", () => { + const session = new GitGutterPeekSession(); + session.setChanges(changesOf(THREE_CHANGES), 7); + session.open(0, 7); + + const afterStage = computeGitGutterChanges(["a", "B", "c", "d", "e", "f"], THREE_CHANGES, monacoLineDiff)!; + expect(session.setChanges(afterStage, 7)).toBeNull(); + expect(session.peek).toBeNull(); + }); + + test("closing, as on a file switch, clears the open review", () => { + const session = new GitGutterPeekSession(); + session.setChanges(changesOf(THREE_CHANGES), 7); + const peek = session.open(1, 7)!; + session.close(); + expect(session.peek).toBeNull(); + expect(session.isCurrent(peek, 7)).toBe(false); + }); +}); diff --git a/windows/tauri/src/features/editor/engines/monaco/git-gutter-peek-session.ts b/windows/tauri/src/features/editor/engines/monaco/git-gutter-peek-session.ts new file mode 100644 index 000000000..10debba53 --- /dev/null +++ b/windows/tauri/src/features/editor/engines/monaco/git-gutter-peek-session.ts @@ -0,0 +1,102 @@ +import { + adjacentGitGutterChangeIndex, + gitGutterChangeKey, + type GitGutterChange, +} from "@/features/git/utils/git-gutter-changes"; + +/** The change an open inline review shows, bound to the text it was computed from. */ +export interface GitGutterPeek { + index: number; + total: number; + change: GitGutterChange; + key: string; + /** Monaco model version the change list was computed for. */ + version: number; + /** Increments on every open, so a late action from a replaced review is ignored. */ + generation: number; +} + +/** + * Owns which change list is current and which change is open. It has no + * Monaco dependency: the editor adapter reports content and model changes and + * renders whatever `peek` is. + */ +export class GitGutterPeekSession { + private changes: GitGutterChange[] = []; + private version = -1; + private generation = 0; + private current: GitGutterPeek | null = null; + + get peek(): GitGutterPeek | null { + return this.current; + } + + get changeList(): readonly GitGutterChange[] { + return this.changes; + } + + /** + * Publishes a change list computed for `version`. An open review stays open + * only when the same change, with the same content, is still in the list. + */ + setChanges(changes: GitGutterChange[], version: number): GitGutterPeek | null { + this.changes = changes; + this.version = version; + const open = this.current; + if (!open) return null; + // A Git refresh (save, stage, external change) recomputes the list for + // the same editor text; the review follows its change or closes when the + // change is gone, for example after it was staged. + const index = changes.findIndex((change) => gitGutterChangeKey(change) === open.key); + this.current = index < 0 ? null : { ...open, index, total: changes.length, version }; + return this.current; + } + + open(index: number, version: number): GitGutterPeek | null { + if (version !== this.version) return null; + const change = this.changes[index]; + if (!change) return null; + this.current = { + index, + total: this.changes.length, + change, + key: gitGutterChangeKey(change), + version, + generation: ++this.generation, + }; + return this.current; + } + + navigate(direction: "previous" | "next", version: number): GitGutterPeek | null { + const from = this.current?.index ?? -1; + const index = + from < 0 + ? direction === "next" + ? 0 + : this.changes.length - 1 + : adjacentGitGutterChangeIndex(this.changes.length, from, direction); + return index < 0 ? null : this.open(index, version); + } + + /** Editing, switching models or disposing makes the open review stale. */ + close(): void { + this.current = null; + } + + /** Text edits invalidate the published list until it is recomputed. */ + invalidate(): void { + this.current = null; + this.changes = []; + this.version = -1; + } + + /** True only for the review that is open now, over the text it was computed from. */ + isCurrent(peek: GitGutterPeek, version: number): boolean { + return ( + this.current !== null && + this.current.generation === peek.generation && + this.current.version === version && + this.version === version + ); + } +} diff --git a/windows/tauri/src/features/editor/engines/monaco/git-gutter-peek-widget.ts b/windows/tauri/src/features/editor/engines/monaco/git-gutter-peek-widget.ts new file mode 100644 index 000000000..44d9bafb8 --- /dev/null +++ b/windows/tauri/src/features/editor/engines/monaco/git-gutter-peek-widget.ts @@ -0,0 +1,208 @@ +import { editor as monacoEditor } from "monaco-editor"; +import type * as Monaco from "monaco-editor"; +import { mountDiffReview, type ReviewRow } from "@lithe/editor/diff-review"; + +const { EditorOption } = monacoEditor; + +const WIDGET_ID = "lithe.gitGutterPeek"; +const HEADER_HEIGHT = 28; +const MIN_BODY_LINES = 3; +const MAX_BODY_LINES = 20; +/** The review never covers more than this share of the editor viewport. */ +const MAX_VIEWPORT_SHARE = 0.6; + +export interface GitGutterPeekLabels { + title: string; + previous: string; + next: string; + stage: string; + stageUnavailable: string; + openDiff: string; + close: string; +} + +export interface GitGutterPeekView { + rows: ReviewRow[]; + /** Editor line below which the review opens; 0 places it above line 1. */ + anchorLine: number; + language: string; + labels: GitGutterPeekLabels; + canNavigate: boolean; + canStage: boolean; + staging: boolean; +} + +export interface GitGutterPeekHandlers { + previous(): void; + next(): void; + stage(): void; + openDiff(): void; + close(): void; +} + +export interface GitGutterPeekWidget { + show(view: GitGutterPeekView): void; + dispose(): void; +} + +function iconButton(codicon: string, onClick: () => void): HTMLButtonElement { + const button = document.createElement("button"); + button.type = "button"; + button.className = "lithe-git-peek-button"; + const icon = document.createElement("span"); + icon.className = `codicon codicon-${codicon}`; + icon.setAttribute("aria-hidden", "true"); + button.append(icon); + // Keep focus in the editor; the review is a transient inspection surface. + button.addEventListener("mousedown", (event) => event.preventDefault()); + button.addEventListener("click", onClick); + return button; +} + +function labelButton(button: HTMLButtonElement, label: string) { + button.title = label; + button.setAttribute("aria-label", label); +} + +/** + * An inline, read-only review of one Git change, embedded between editor + * lines like VS Code's dirty-diff peek. A view zone reserves the space and an + * overlay widget renders into it, so Monaco moves it while scrolling without + * React state updates. The diff uses the shared review renderer with its own + * models, which `dispose` releases. + */ +export function createGitGutterPeekWidget( + editor: Monaco.editor.ICodeEditor, + handlers: GitGutterPeekHandlers, +): GitGutterPeekWidget { + const root = document.createElement("div"); + root.className = "lithe-git-peek"; + root.setAttribute("role", "region"); + root.style.top = "-1000px"; + + const header = document.createElement("div"); + header.className = "lithe-git-peek-header"; + header.style.height = `${HEADER_HEIGHT}px`; + const title = document.createElement("span"); + title.className = "lithe-git-peek-title"; + const actions = document.createElement("div"); + actions.className = "lithe-git-peek-actions"; + const stage = iconButton("add", handlers.stage); + const previous = iconButton("arrow-up", handlers.previous); + const next = iconButton("arrow-down", handlers.next); + const openDiff = iconButton("go-to-file", handlers.openDiff); + const close = iconButton("close", handlers.close); + actions.append(stage, next, previous, openDiff, close); + header.append(title, actions); + + const body = document.createElement("div"); + body.className = "lithe-git-peek-body"; + root.append(header, body); + root.addEventListener("keydown", (event) => { + if (event.key !== "Escape") return; + event.preventDefault(); + event.stopPropagation(); + handlers.close(); + }); + + const review = mountDiffReview(body); + const overlay: Monaco.editor.IOverlayWidget = { + getId: () => WIDGET_ID, + getDomNode: () => root, + // Positioned by the view zone callbacks below, like Monaco's ZoneWidget. + getPosition: () => null, + }; + editor.addOverlayWidget(overlay); + + let zoneId: string | null = null; + let height = 0; + let anchorLine = -1; + let disposed = false; + + const layoutWidth = () => { + const info = editor.getLayoutInfo(); + root.style.width = `${Math.max(0, info.width - info.minimap.minimapWidth - info.verticalScrollbarWidth)}px`; + }; + layoutWidth(); + const layoutListener = editor.onDidLayoutChange(layoutWidth); + + const removeZone = () => { + if (zoneId === null) return; + const id = zoneId; + zoneId = null; + editor.changeViewZones((accessor) => accessor.removeZone(id)); + }; + + return { + show(view) { + if (disposed) return; + title.textContent = view.labels.title; + root.setAttribute("aria-label", view.labels.title); + labelButton(previous, view.labels.previous); + labelButton(next, view.labels.next); + labelButton(openDiff, view.labels.openDiff); + labelButton(close, view.labels.close); + labelButton(stage, view.canStage ? view.labels.stage : view.labels.stageUnavailable); + previous.disabled = !view.canNavigate; + next.disabled = !view.canNavigate; + stage.disabled = !view.canStage || view.staging; + + const lineHeight = editor.getOption(EditorOption.lineHeight); + const viewportLines = Math.floor( + (editor.getLayoutInfo().height * MAX_VIEWPORT_SHARE - HEADER_HEIGHT) / lineHeight, + ); + const bodyLines = Math.max( + MIN_BODY_LINES, + Math.min(view.rows.length, MAX_BODY_LINES, Math.max(MIN_BODY_LINES, viewportLines)), + ); + const nextHeight = HEADER_HEIGHT + bodyLines * lineHeight + 2; + body.style.height = `${nextHeight - HEADER_HEIGHT - 2}px`; + root.style.height = `${nextHeight}px`; + + if (zoneId === null || height !== nextHeight || view.anchorLine !== anchorLine) { + removeZone(); + height = nextHeight; + anchorLine = view.anchorLine; + editor.changeViewZones((accessor) => { + zoneId = accessor.addZone({ + afterLineNumber: view.anchorLine, + heightInPx: nextHeight, + domNode: document.createElement("div"), + onDomNodeTop: (top) => { + root.style.top = `${top}px`; + }, + }); + }); + editor.revealLinesInCenterIfOutsideViewport( + Math.max(1, view.anchorLine), + Math.max(1, view.anchorLine + 1), + ); + } + + review.configure({ + fontFamily: editor.getOption(EditorOption.fontFamily), + fontSize: editor.getOption(EditorOption.fontSize), + lineHeight, + renderIndicators: true, + scrollbar: { alwaysConsumeMouseWheel: false }, + }); + void review.update({ + rows: view.rows, + language: view.language, + sideBySide: false, + collapse: false, + overview: false, + highlightWords: true, + }); + }, + dispose() { + if (disposed) return; + disposed = true; + layoutListener.dispose(); + removeZone(); + editor.removeOverlayWidget(overlay); + review.dispose(); + root.remove(); + }, + }; +} diff --git a/windows/tauri/src/features/editor/engines/monaco/monaco-diff-internals.d.ts b/windows/tauri/src/features/editor/engines/monaco/monaco-diff-internals.d.ts new file mode 100644 index 000000000..088adba2e --- /dev/null +++ b/windows/tauri/src/features/editor/engines/monaco/monaco-diff-internals.d.ts @@ -0,0 +1,25 @@ +// Narrow declaration for Monaco 0.55.1's synchronous line diff. The public API +// only exposes it through a diff editor; the gutter needs the raw line ranges. +// Re-check this shape whenever the pinned Monaco version changes. +declare module "monaco-editor/esm/vs/editor/common/diff/linesDiffComputers.js" { + interface LineRange { + readonly startLineNumber: number; + readonly endLineNumberExclusive: number; + } + interface LineRangeMapping { + readonly original: LineRange; + readonly modified: LineRange; + } + interface LinesDiff { + readonly changes: readonly LineRangeMapping[]; + readonly hitTimeout: boolean; + } + interface LinesDiffComputer { + computeDiff( + originalLines: string[], + modifiedLines: string[], + options: { ignoreTrimWhitespace: boolean; maxComputationTimeMs: number; computeMoves: boolean }, + ): LinesDiff; + } + export const linesDiffComputers: { getDefault(): LinesDiffComputer }; +} diff --git a/windows/tauri/src/features/editor/engines/monaco/monaco-line-diff.ts b/windows/tauri/src/features/editor/engines/monaco/monaco-line-diff.ts new file mode 100644 index 000000000..0ee704935 --- /dev/null +++ b/windows/tauri/src/features/editor/engines/monaco/monaco-line-diff.ts @@ -0,0 +1,35 @@ +import { linesDiffComputers } from "monaco-editor/esm/vs/editor/common/diff/linesDiffComputers.js"; +import type { GitGutterLineDiff } from "@/features/git/utils/git-gutter-changes"; + +/** + * Time budget for one gutter comparison. It runs on the UI thread after the + * user pauses typing; a file too large to compare in this time shows no + * markers instead of blocking input. + */ +const GIT_GUTTER_DIFF_BUDGET_MS = 150; + +/** Monaco's own line diff, the same algorithm its diff editor uses. */ +export const monacoLineDiff: GitGutterLineDiff = (original, modified) => { + // Monaco's computer assumes at least one line on each side, as in a model. + if (original.length === 0 || modified.length === 0) { + if (original.length === modified.length) return []; + return [{ + originalStart: 1, + originalEnd: original.length + 1, + modifiedStart: 1, + modifiedEnd: modified.length + 1, + }]; + } + const result = linesDiffComputers.getDefault().computeDiff([...original], [...modified], { + ignoreTrimWhitespace: false, + maxComputationTimeMs: GIT_GUTTER_DIFF_BUDGET_MS, + computeMoves: false, + }); + if (result.hitTimeout) return null; + return result.changes.map((change) => ({ + originalStart: change.original.startLineNumber, + originalEnd: change.original.endLineNumberExclusive, + modifiedStart: change.modified.startLineNumber, + modifiedEnd: change.modified.endLineNumberExclusive, + })); +}; diff --git a/windows/tauri/src/features/editor/styles/monaco-editor.css b/windows/tauri/src/features/editor/styles/monaco-editor.css index 5a019a46a..3fd46e5df 100644 --- a/windows/tauri/src/features/editor/styles/monaco-editor.css +++ b/windows/tauri/src/features/editor/styles/monaco-editor.css @@ -516,3 +516,99 @@ .monaco-vim-statusbar:empty { display: none !important; } + +/* Git change markers in the line-decoration gutter (#681). */ +.monaco-editor-shell .monaco-editor .lithe-git-gutter { + width: 3px !important; + margin-left: 3px; + cursor: pointer; +} + +.monaco-editor-shell .monaco-editor .lithe-git-gutter-added { + background: var(--git-added); +} + +.monaco-editor-shell .monaco-editor .lithe-git-gutter-modified { + background: var(--git-modified); +} + +/* A deletion owns no line; draw a wedge at the top of the following line. */ +.monaco-editor-shell .monaco-editor .lithe-git-gutter-deleted { + width: 0 !important; + height: 0 !important; + border-top: 4px solid transparent; + border-bottom: 4px solid transparent; + border-left: 6px solid var(--git-deleted); + margin-top: -4px; +} + +.monaco-editor-shell .monaco-editor .lithe-git-peek { + position: absolute; + left: 0; + box-sizing: border-box; + display: flex; + flex-direction: column; + border-top: 1px solid var(--primary); + border-bottom: 1px solid var(--primary); + background: var(--background); + color: var(--foreground); + z-index: 5; +} + +.monaco-editor-shell .monaco-editor .lithe-git-peek-header { + display: flex; + flex: none; + align-items: center; + justify-content: space-between; + gap: 0.5rem; + padding: 0 0.5rem; + background: color-mix(in srgb, var(--surface) 90%, var(--background)); + font-family: var(--app-font-family); + font-size: var(--ui-text-sm); +} + +.monaco-editor-shell .monaco-editor .lithe-git-peek-title { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.monaco-editor-shell .monaco-editor .lithe-git-peek-actions { + display: flex; + flex: none; + gap: 0.125rem; +} + +.monaco-editor-shell .monaco-editor .lithe-git-peek-button { + display: inline-flex; + align-items: center; + justify-content: center; + width: 22px; + height: 22px; + border: 0; + border-radius: var(--radius-sm, 3px); + background: transparent; + color: inherit; + cursor: pointer; +} + +.monaco-editor-shell .monaco-editor .lithe-git-peek-button:hover:not(:disabled) { + background: color-mix(in srgb, var(--foreground) 12%, transparent); + outline: none; +} + +.monaco-editor-shell .monaco-editor .lithe-git-peek-button:disabled { + opacity: 0.4; + cursor: default; +} + +.monaco-editor-shell .monaco-editor .lithe-git-peek-body { + position: relative; + flex: 1; + min-height: 0; +} + +.monaco-editor-shell .monaco-editor .lithe-git-peek-button:focus-visible { + outline: 1px solid var(--primary); + outline-offset: -1px; +} diff --git a/windows/tauri/src/features/git/hooks/use-git-gutter-base.ts b/windows/tauri/src/features/git/hooks/use-git-gutter-base.ts new file mode 100644 index 000000000..deda99bea --- /dev/null +++ b/windows/tauri/src/features/git/hooks/use-git-gutter-base.ts @@ -0,0 +1,91 @@ +import { useEffect, useRef, useState } from "react"; +import { useFileSystemStore } from "@/features/file-system/stores/file-system.store"; +import { getFullContextFileDiff } from "../api/git-diff-api"; +import { resolveRepositoryForFile } from "../api/git-repo-api"; +import { isGitChangeRelevant, subscribeToGitChanges } from "../events/git-events"; +import { useGitStore } from "../stores/git.store"; +import { + gitGutterBaseFromDiff, + isGitGutterFileUntracked, + splitGitGutterLines, + type GitGutterBaseSource, +} from "../utils/git-gutter-changes"; + +/** The Git index version of one editor file, which gutter markers compare against. */ +export interface GitGutterBase { + repoPath: string; + /** Repository-relative path with `/` separators, as hunk patches require. */ + filePath: string; + lines: string[]; +} + +/** + * Resolves the comparison base for a file. A null result means the file has + * no markers: it is untracked, outside a repository, binary, or the read failed. + */ +export async function loadGitGutterBase( + rootPath: string, + filePath: string, + savedContent: () => string, +): Promise { + const resolved = await resolveRepositoryForFile(rootPath, filePath); + if (!resolved) return null; + const status = useGitStore.getState().workspaceGitStatus; + if (isGitGutterFileUntracked(status?.files ?? [], resolved.repoPath, resolved.filePath)) { + return null; + } + + const source: GitGutterBaseSource | null = gitGutterBaseFromDiff( + await getFullContextFileDiff(resolved.repoPath, resolved.filePath, false), + ); + if (!source) return null; + return { + ...resolved, + // No index-to-worktree difference: the saved file is the index version. + lines: source.kind === "lines" ? source.lines : splitGitGutterLines(savedContent()), + }; +} + +/** + * Loads the gutter base for the active editor and reloads it on Git changes + * (save, stage, external edits). Results for a previous file or an older + * request never replace the current one. + */ +export function useGitGutterBase( + filePath: string | undefined, + savedContent: () => string, + savedRevision: unknown, +): GitGutterBase | null { + const rootFolderPath = useFileSystemStore((state) => state.rootFolderPath); + const [state, setState] = useState<{ key: string; base: GitGutterBase | null } | null>(null); + const [refresh, setRefresh] = useState(0); + const key = filePath && rootFolderPath ? JSON.stringify([rootFolderPath, filePath]) : null; + // Read at request time; `savedRevision` is the dependency that says it changed. + const savedContentRef = useRef(savedContent); + savedContentRef.current = savedContent; + + useEffect(() => { + if (!filePath || !rootFolderPath) return; + return subscribeToGitChanges((change) => { + if (isGitChangeRelevant(change, rootFolderPath, filePath)) setRefresh((value) => value + 1); + }); + }, [filePath, rootFolderPath]); + + useEffect(() => { + if (!key || !filePath || !rootFolderPath) return; + let cancelled = false; + void loadGitGutterBase(rootFolderPath, filePath, () => savedContentRef.current()) + .catch((error) => { + console.error("Failed to load Git gutter base:", error); + return null; + }) + .then((base) => { + if (!cancelled) setState({ key, base }); + }); + return () => { + cancelled = true; + }; + }, [key, filePath, rootFolderPath, refresh, savedRevision]); + + return state && state.key === key ? state.base : null; +} diff --git a/windows/tauri/src/features/git/services/git-gutter-actions.ts b/windows/tauri/src/features/git/services/git-gutter-actions.ts new file mode 100644 index 000000000..58787ddea --- /dev/null +++ b/windows/tauri/src/features/git/services/git-gutter-actions.ts @@ -0,0 +1,44 @@ +import { activateMainEditorPane } from "@/features/editor/stores/buffer-pane-sync"; +import { useBufferStore } from "@/features/editor/stores/buffer.store"; +import { getWorkingTreePathDiff } from "../api/git-diff-api"; +import { stageHunk } from "../api/git-status-api"; +import type { GitGutterBase } from "../hooks/use-git-gutter-base"; +import { gitGutterChangeHunk, type GitGutterChange } from "../utils/git-gutter-changes"; +import { createSingleFileWorkingTreeDiff } from "../utils/working-tree-multi-diff"; + +/** + * Stages one gutter change through the existing hunk API. The patch is built + * against the index text the change was computed from, so Git rejects it + * instead of staging the wrong lines if the index moved in the meantime. + */ +export function stageGitGutterChange( + base: GitGutterBase, + baseLines: readonly string[], + change: GitGutterChange, +): Promise { + return stageHunk(base.repoPath, gitGutterChangeHunk(base.filePath, baseLines, change)); +} + +/** + * Opens the same single-file working-tree Diff tab as the Changes panel + * (HEAD to working tree, full context, per-hunk staging). + */ +export async function openGitGutterFullDiff( + base: GitGutterBase, + title: string, +): Promise { + const diff = await getWorkingTreePathDiff(base.repoPath, base.filePath, false, undefined, true); + if (!diff || (diff.lines.length === 0 && !diff.is_image && !diff.is_binary)) return null; + const fileKey = `unstaged:${base.filePath}`; + const multiDiff = createSingleFileWorkingTreeDiff({ + repoPath: base.repoPath, + fileKey, + diff, + title, + target: { repoPath: base.repoPath, filePath: base.filePath, untracked: false }, + }); + activateMainEditorPane(); + return useBufferStore + .getState() + .actions.openBuffer("diff://working-tree/all-files", title, "", false, undefined, true, true, multiDiff); +} diff --git a/windows/tauri/src/features/git/utils/git-gutter-changes.test.ts b/windows/tauri/src/features/git/utils/git-gutter-changes.test.ts new file mode 100644 index 000000000..1c612f3a3 --- /dev/null +++ b/windows/tauri/src/features/git/utils/git-gutter-changes.test.ts @@ -0,0 +1,253 @@ +import { describe, expect, test } from "bun:test"; +import { monacoLineDiff } from "@/features/editor/engines/monaco/monaco-line-diff"; +import type { GitDiff, GitDiffLine } from "../types/git.types"; +import { + adjacentGitGutterChangeIndex, + isGitGutterFileUntracked, + type GitGutterChange, + computeGitGutterChanges, + gitGutterBaseFromDiff, + gitGutterChangeHunk, + gitGutterChangeIndexAtLine, + gitGutterMarkerLines, + gitGutterPeekAnchorLine, + gitGutterPeekRows, + splitGitGutterLines, +} from "./git-gutter-changes"; + +const changesOf = (base: string[], editor: string[]) => + computeGitGutterChanges(base, editor, monacoLineDiff) ?? []; + +const fullContextDiff = (lines: GitDiffLine[], header: string): GitDiff => ({ + file_path: "src/app.ts", + is_new: false, + is_deleted: false, + is_renamed: false, + is_full_context: true, + lines: [{ line_type: "header", content: header }, ...lines], +}); + +describe("Git gutter change locations", () => { + test("classifies added, modified and deleted lines against the base", () => { + const changes = changesOf( + ["a", "b", "c", "d", "e"], + ["a", "B", "c", "e", "f"], + ); + + expect(changes.map((change) => change.kind)).toEqual(["modified", "deleted", "added"]); + expect(changes[0]).toMatchObject({ originalLines: ["b"], modifiedLines: ["B"], modifiedStart: 2 }); + expect(changes[1]).toMatchObject({ originalLines: ["d"], modifiedLines: [], modifiedStart: 4 }); + expect(changes[2]).toMatchObject({ originalLines: [], modifiedLines: ["f"], modifiedStart: 5 }); + }); + + test("marks a deletion on the following line, or the last line at end of file", () => { + const [middle] = changesOf(["a", "b", "c"], ["a", "c"]); + const [end] = changesOf(["a", "b", "c"], ["a", "b"]); + + expect(gitGutterMarkerLines(middle, 2)).toEqual({ start: 2, end: 2 }); + expect(gitGutterMarkerLines(end, 2)).toEqual({ start: 2, end: 2 }); + expect(gitGutterPeekAnchorLine(middle)).toBe(1); + expect(gitGutterPeekAnchorLine(end)).toBe(2); + }); + + test("resolves a clicked line to the change that owns it", () => { + const changes = changesOf(["a", "b", "c", "d"], ["a", "x", "y", "d", "z"]); + expect(gitGutterChangeIndexAtLine(changes, 3, 5)).toBe(0); + expect(gitGutterChangeIndexAtLine(changes, 5, 5)).toBe(1); + expect(gitGutterChangeIndexAtLine(changes, 4, 5)).toBe(-1); + }); + + test("selects a deletion through the line that follows it", () => { + // "b" is deleted, so its marker sits on "c" (line 2); "x" is a separate addition. + const changes = changesOf(["a", "b", "c"], ["a", "c", "x"]); + expect(changes.map((change) => change.kind)).toEqual(["deleted", "added"]); + expect(gitGutterChangeIndexAtLine(changes, 2, 3)).toBe(0); + expect(gitGutterChangeIndexAtLine(changes, 3, 3)).toBe(1); + }); + + test("a modification wins over a deletion marked on the same line", () => { + const changes: GitGutterChange[] = [ + { kind: "deleted", originalStart: 2, originalEnd: 3, modifiedStart: 2, modifiedEnd: 2, + originalLines: ["b"], modifiedLines: [] }, + { kind: "modified", originalStart: 3, originalEnd: 4, modifiedStart: 2, modifiedEnd: 3, + originalLines: ["c"], modifiedLines: ["C"] }, + ]; + expect(gitGutterChangeIndexAtLine([...changes], 2, 3)).toBe(1); + }); + + test("wraps previous and next navigation around the change list", () => { + expect(adjacentGitGutterChangeIndex(3, 2, "next")).toBe(0); + expect(adjacentGitGutterChangeIndex(3, 0, "previous")).toBe(2); + expect(adjacentGitGutterChangeIndex(3, 1, "next")).toBe(2); + expect(adjacentGitGutterChangeIndex(0, 0, "next")).toBe(-1); + }); + + test("compares an empty base or editor without calling the line diff", () => { + expect(changesOf([], ["a", "b"])).toEqual([ + expect.objectContaining({ kind: "added", modifiedStart: 1, modifiedEnd: 3 }), + ]); + expect(changesOf(["a"], [])).toEqual([ + expect.objectContaining({ kind: "deleted", originalStart: 1, originalEnd: 2 }), + ]); + expect(changesOf([], [])).toEqual([]); + }); + + test("shows no markers when the comparison exceeds its budget", () => { + expect(computeGitGutterChanges(["a"], ["b"], () => null)).toBeNull(); + }); +}); + +describe("Git gutter comparison base", () => { + test("rebuilds the index text from a full-context patch", () => { + const diff = fullContextDiff( + [ + { line_type: "context", content: "a", old_line_number: 1, new_line_number: 1 }, + { line_type: "removed", content: "b", old_line_number: 2 }, + { line_type: "added", content: "B", new_line_number: 2 }, + { line_type: "context", content: "c", old_line_number: 3, new_line_number: 3 }, + ], + "@@ -1,3 +1,3 @@", + ); + expect(gitGutterBaseFromDiff(diff)).toEqual({ kind: "lines", lines: ["a", "b", "c"] }); + }); + + test("treats an empty patch as unchanged and rejects partial or binary patches", () => { + expect(gitGutterBaseFromDiff({ ...fullContextDiff([], "@@ -1 +1 @@"), lines: [] })).toEqual({ + kind: "unchanged", + }); + const sparse = { ...fullContextDiff([], "@@ -10,2 +10,2 @@"), is_full_context: false }; + expect(gitGutterBaseFromDiff(sparse)).toBeNull(); + expect(gitGutterBaseFromDiff({ ...fullContextDiff([], "@@ -1 +1 @@"), is_binary: true })).toBeNull(); + expect(gitGutterBaseFromDiff(null)).toBeNull(); + }); + + test("rejects a patch whose old side does not cover the whole index file", () => { + const diff = fullContextDiff( + [{ line_type: "context", content: "a", old_line_number: 1, new_line_number: 1 }], + "@@ -1,2 +1,2 @@", + ); + expect(gitGutterBaseFromDiff(diff)).toBeNull(); + }); + + test("splits text like an editor, ignoring one trailing line break", () => { + expect(splitGitGutterLines("a\r\nb\n")).toEqual(["a", "b"]); + expect(splitGitGutterLines("a\rb")).toEqual(["a", "b"]); + expect(splitGitGutterLines("")).toEqual([]); + }); +}); + +describe("Git gutter inline review rows", () => { + test("shows the change with real line numbers and context up to the neighbours", () => { + const base = ["a", "b", "c", "d", "e", "f"]; + const editor = ["a", "X", "c", "d", "e", "F"]; + const changes = changesOf(base, editor); + const rows = gitGutterPeekRows(changes, 0, editor); + + expect(rows.map((row) => [row.kind, row.oldLine, row.newLine, row.left ?? row.right])).toEqual([ + ["context", 1, 1, "a"], + ["removal", 2, null, "b"], + ["addition", null, 2, "X"], + ["context", 3, 3, "c"], + ["context", 4, 4, "d"], + ]); + // The next change starts at editor line 6; its context stops before it. + const next = gitGutterPeekRows(changes, 1, editor); + expect(next.map((row) => row.newLine ?? row.oldLine)).toEqual([4, 5, 6, 6]); + }); + + test("keeps old line numbers aligned after an earlier insertion", () => { + const base = ["a", "b", "c"]; + const editor = ["new", "a", "b", "C"]; + const changes = changesOf(base, editor); + const rows = gitGutterPeekRows(changes, 1, editor); + expect(rows.find((row) => row.kind === "context" && row.newLine === 3)?.oldLine).toBe(2); + }); +}); + +describe("Git gutter staging patch", () => { + test("builds a hunk whose context comes from the index text", () => { + const base = ["1", "2", "3", "4", "5", "6", "7", "8"]; + const [change] = changesOf(base, ["1", "2", "3", "4", "FIVE", "6", "7", "8"]); + const hunk = gitGutterChangeHunk("src/app.ts", base, change); + + expect(hunk.file_path).toBe("src/app.ts"); + expect(hunk.lines.map((line) => `${line.line_type}:${line.content}`)).toEqual([ + "header:@@ -2,7 +2,7 @@", + "context:2", + "context:3", + "context:4", + "removed:5", + "added:FIVE", + "context:6", + "context:7", + "context:8", + ]); + }); + + test("uses index context even when a neighbouring unstaged change is nearby", () => { + // Line 2 and line 4 both changed; staging the second must not stage "B". + const base = ["a", "b", "c", "d", "e"]; + const editor = ["a", "B", "c", "D", "e"]; + const changes = changesOf(base, editor); + const hunk = gitGutterChangeHunk("f.txt", base, changes[1]); + + expect(hunk.lines.map((line) => `${line.line_type}:${line.content}`)).toEqual([ + "header:@@ -1,5 +1,5 @@", + "context:a", + "context:b", + "context:c", + "removed:d", + "added:D", + "context:e", + ]); + }); + + test("names Git's empty ranges for an insertion at the start and a full deletion", () => { + const [insertion] = changesOf([], ["x"]); + expect(gitGutterChangeHunk("f", [], insertion).lines[0].content).toBe("@@ -0,0 +1,1 @@"); + + const [deletion] = changesOf(["x"], []); + expect(gitGutterChangeHunk("f", ["x"], deletion).lines[0].content).toBe("@@ -1,1 +0,0 @@"); + }); +}); + + +describe("Git gutter repository identity", () => { + test("an untracked sibling path does not hide the tracked file's markers", () => { + const files = [{ + path: "a/src/app.ts", repositoryPath: "C:/workspace/a", + repositoryRelativePath: "src/app.ts", status: "untracked" as const, staged: false, + }]; + expect(isGitGutterFileUntracked(files, "C:/workspace/b", "src/app.ts")).toBe(false); + expect(isGitGutterFileUntracked(files, "C:/workspace/a/", "src/app.ts")).toBe(true); + expect(isGitGutterFileUntracked(files, "C:/workspace/a", "src/other.ts")).toBe(false); + }); + + test("single-repository status retains undecorated path support", () => { + expect(isGitGutterFileUntracked([ + { path: "src/app.ts", status: "untracked", staged: false }, + ], "C:/workspace/a", "src/app.ts")).toBe(true); + }); +}); + +describe("Git gutter isolated patch coordinates", () => { + test("earlier unstaged deletions do not make the selected patch start negative", () => { + const base = Array.from({ length: 10 }, (_, index) => String(index + 1)); + // Exercise an isolated range directly: Monaco may coalesce nearby edits, + // but patch coordinates must remain valid for any selected range. + const change: GitGutterChange = { + kind: "modified", originalStart: 10, originalEnd: 11, + modifiedStart: 2, modifiedEnd: 3, originalLines: ["10"], modifiedLines: ["TEN"], + }; + const hunk = gitGutterChangeHunk("f", base, change); + expect(hunk.lines[0].content).toBe("@@ -7,4 +7,4 @@"); + expect(hunk.lines.slice(1).map((line) => line.content)).toEqual(["7", "8", "9", "10", "TEN"]); + }); + + test("earlier unstaged insertions do not shift the selected patch", () => { + const base = ["a", "b", "c", "d", "e", "f"]; + const changes = changesOf(base, ["new", ...base.slice(0, -1), "F"]); + expect(changes).toHaveLength(2); + expect(gitGutterChangeHunk("f", base, changes[1]).lines[0].content).toBe("@@ -3,4 +3,4 @@"); + }); +}); diff --git a/windows/tauri/src/features/git/utils/git-gutter-changes.ts b/windows/tauri/src/features/git/utils/git-gutter-changes.ts new file mode 100644 index 000000000..b04a3aac8 --- /dev/null +++ b/windows/tauri/src/features/git/utils/git-gutter-changes.ts @@ -0,0 +1,288 @@ +import type { ReviewRow } from "@lithe/editor/diff-review"; +import type { GitDiff, GitDiffLine, GitFile, GitHunk } from "../types/git.types"; +import { normalizeRepositoryPath } from "../api/git-repository-path"; +import { parseDiffHunkRange } from "./git-diff-helpers"; + +/** Aggregated workspace status must match both repository and relative path. */ +export function isGitGutterFileUntracked( + files: readonly GitFile[], + repoPath: string, + filePath: string, +): boolean { + return files.some((file) => + file.status === "untracked" && + (!file.repositoryPath || + normalizeRepositoryPath(file.repositoryPath) === normalizeRepositoryPath(repoPath)) && + (file.repositoryRelativePath ?? file.path) === filePath, + ); +} + +export type GitGutterChangeKind = "added" | "modified" | "deleted"; + +/** One-based, end-exclusive line ranges, as Monaco's line diff reports them. */ +export interface GitGutterLineRange { + originalStart: number; + originalEnd: number; + modifiedStart: number; + modifiedEnd: number; +} + +/** One contiguous change between the Git base text and the editor text. */ +export interface GitGutterChange extends GitGutterLineRange { + kind: GitGutterChangeKind; + originalLines: string[]; + modifiedLines: string[]; +} + +/** Returns null when the comparison could not finish within its budget. */ +export type GitGutterLineDiff = ( + original: readonly string[], + modified: readonly string[], +) => GitGutterLineRange[] | null; + +/** + * Where the editor's comparison base comes from. `unchanged` means Git found + * no difference between the index and the file on disk, so the saved disk + * text is the base. + */ +export type GitGutterBaseSource = { kind: "lines"; lines: string[] } | { kind: "unchanged" }; + +/** Context kept around a staged gutter change. Matches Git's default `--unified=3`. */ +const GITGUTTER_HUNK_CONTEXT_LINES = 3; + +/** + * Splits text the way an editor counts lines. A trailing line break does not + * start another line, so `"a\n"` and `"a"` both compare as `["a"]`; the Git + * diff parser drops end-of-file newline markers in the same way. + */ +export function splitGitGutterLines(text: string): string[] { + if (text === "") return []; + const lines = text.split(/\r\n|\r|\n/); + if (lines[lines.length - 1] === "") lines.pop(); + return lines; +} + +/** Drops the empty line Monaco reports after a final line break. */ +export function trimFinalEmptyLine(lines: readonly string[]): string[] { + return lines.length > 0 && lines[lines.length - 1] === "" ? lines.slice(0, -1) : [...lines]; +} + +/** + * Rebuilds the index text from a full-context index-to-worktree patch. The + * old side of such a patch is the complete index version: every context and + * removed line, in order. Sparse, truncated or binary patches cannot supply + * that text and return null instead of a partial base. + */ +export function gitGutterBaseFromDiff(diff: GitDiff | null): GitGutterBaseSource | null { + // A null diff means the read failed or the file is outside a repository; + // an empty patch (no lines) is how Git reports an unchanged file. + if (!diff || diff.is_binary || diff.is_image || diff.is_truncated) return null; + const headers = diff.lines.filter((line) => line.line_type === "header"); + if (headers.length === 0) return diff.lines.length === 0 ? { kind: "unchanged" } : null; + if (headers.length > 1 || !diff.is_full_context || diff.lines[0]?.line_type !== "header") { + return null; + } + const range = parseDiffHunkRange(headers[0].content); + if (!range) return null; + const lines = diff.lines + .filter((line) => line.line_type === "context" || line.line_type === "removed") + .map((line) => line.content); + const expectedStart = range.oldCount === 0 ? 0 : 1; + if (range.oldStart !== expectedStart || lines.length !== range.oldCount) return null; + return { kind: "lines", lines }; +} + +export function computeGitGutterChanges( + original: readonly string[], + modified: readonly string[], + lineDiff: GitGutterLineDiff, +): GitGutterChange[] | null { + const ranges = lineDiff(original, modified); + if (!ranges) return null; + return ranges.map((range) => ({ + ...range, + kind: + range.originalStart === range.originalEnd + ? "added" + : range.modifiedStart === range.modifiedEnd + ? "deleted" + : "modified", + originalLines: original.slice(range.originalStart - 1, range.originalEnd - 1), + modifiedLines: modified.slice(range.modifiedStart - 1, range.modifiedEnd - 1), + })); +} + +/** + * Editor lines that carry a change's gutter marker. Deleted lines have no + * editor line of their own, so they mark the line that now follows them, or + * the last line when the deletion is at the end of the file (as on macOS). + */ +export function gitGutterMarkerLines( + change: GitGutterChange, + lineCount: number, +): { start: number; end: number } { + const lastLine = Math.max(1, lineCount); + if (change.kind === "deleted") { + const line = Math.min(Math.max(1, change.modifiedStart), lastLine); + return { start: line, end: line }; + } + return { + start: Math.min(change.modifiedStart, lastLine), + end: Math.min(change.modifiedEnd - 1, lastLine), + }; +} + +/** The editor line below which a change's inline review opens. Zero means above line 1. */ +export function gitGutterPeekAnchorLine(change: GitGutterChange): number { + return change.kind === "deleted" ? change.modifiedStart - 1 : change.modifiedEnd - 1; +} + +export function gitGutterChangeIndexAtLine( + changes: readonly GitGutterChange[], + line: number, + lineCount: number, +): number { + // A deletion can share its marker line with the start of the next change; + // the change that owns real editor lines wins that click. + let deletion = -1; + for (let index = 0; index < changes.length; index++) { + const { start, end } = gitGutterMarkerLines(changes[index], lineCount); + if (line < start || line > end) continue; + if (changes[index].kind !== "deleted") return index; + if (deletion < 0) deletion = index; + } + return deletion; +} + +/** Previous and next wrap around, as VS Code's and IDEA's change navigation do. */ +export function adjacentGitGutterChangeIndex( + total: number, + index: number, + direction: "previous" | "next", +): number { + if (total <= 0) return -1; + return (index + (direction === "next" ? 1 : total - 1)) % total; +} + +/** Unchanged lines shown above and below a change in its inline review. */ +const GIT_GUTTER_PEEK_CONTEXT_LINES = 2; + +/** + * Rows for one change's read-only inline review: the change plus a little + * unchanged context, with real line numbers on both sides. Context stops at + * the neighbouring changes so it never shows another change as unchanged. + */ +export function gitGutterPeekRows( + changes: readonly GitGutterChange[], + index: number, + editorLines: readonly string[], + contextLines = GIT_GUTTER_PEEK_CONTEXT_LINES, +): ReviewRow[] { + const change = changes[index]; + if (!change) return []; + const previousEnd = index > 0 ? changes[index - 1].modifiedEnd : 1; + const nextStart = + index + 1 < changes.length ? changes[index + 1].modifiedStart : editorLines.length + 1; + const context = (line: number, offset: number): ReviewRow => { + const text = editorLines[line - 1] ?? ""; + return { + id: `context-${line}`, + oldLine: line + offset, + newLine: line, + left: text, + right: text, + kind: "context", + hunkID: null, + }; + }; + + const rows: ReviewRow[] = []; + const offsetBefore = change.originalStart - change.modifiedStart; + for ( + let line = Math.max(previousEnd, change.modifiedStart - contextLines); + line < change.modifiedStart; + line++ + ) { + rows.push(context(line, offsetBefore)); + } + change.originalLines.forEach((text, offset) => + rows.push({ + id: `removed-${offset}`, + oldLine: change.originalStart + offset, + newLine: null, + left: text, + right: null, + kind: "removal", + hunkID: null, + }), + ); + change.modifiedLines.forEach((text, offset) => + rows.push({ + id: `added-${offset}`, + oldLine: null, + newLine: change.modifiedStart + offset, + left: null, + right: text, + kind: "addition", + hunkID: null, + }), + ); + const offsetAfter = change.originalEnd - change.modifiedEnd; + const contextEnd = Math.min(nextStart, change.modifiedEnd + contextLines); + for (let line = change.modifiedEnd; line < contextEnd; line++) { + rows.push(context(line, offsetAfter)); + } + return rows; +} + +/** Identity of a change's content, used to keep a review open across refreshes. */ +export function gitGutterChangeKey(change: GitGutterChange): string { + return JSON.stringify([ + change.originalStart, + change.originalEnd, + change.modifiedStart, + change.modifiedEnd, + change.originalLines, + change.modifiedLines, + ]); +} + +/** + * Builds a patch that stages exactly one change against the index. Context + * comes from the base (index) text, never from the editor, so a neighbouring + * unstaged change inside the context window stays out of the index. + */ +export function gitGutterChangeHunk( + filePath: string, + base: readonly string[], + change: GitGutterChange, +): GitHunk { + const context = GITGUTTER_HUNK_CONTEXT_LINES; + const removedStart = change.originalStart - 1; + const removedEnd = change.originalEnd - 1; + const contextStart = Math.max(0, removedStart - context); + const contextEnd = Math.min(base.length, removedEnd + context); + const leading = removedStart - contextStart; + const trailing = contextEnd - removedEnd; + const contextLine = (content: string): GitDiffLine => ({ line_type: "context", content }); + + const oldCount = contextEnd - contextStart; + const newCount = leading + change.modifiedLines.length + trailing; + // Git names the line before an empty range, or 0 at the start of the file. + const oldStart = oldCount === 0 ? contextStart : contextStart + 1; + // This patch applies only this change to the index. Earlier unstaged + // insertions/deletions must not shift its new-side coordinates. + const firstNewLine = contextStart + 1; + const newStart = newCount === 0 ? firstNewLine - 1 : firstNewLine; + + return { + file_path: filePath, + lines: [ + { line_type: "header", content: `@@ -${oldStart},${oldCount} +${newStart},${newCount} @@` }, + ...base.slice(contextStart, removedStart).map(contextLine), + ...change.originalLines.map((content): GitDiffLine => ({ line_type: "removed", content })), + ...change.modifiedLines.map((content): GitDiffLine => ({ line_type: "added", content })), + ...base.slice(removedEnd, contextEnd).map(contextLine), + ], + }; +} diff --git a/windows/tauri/src/features/settings/components/tabs/git-settings.tsx b/windows/tauri/src/features/settings/components/tabs/git-settings.tsx index 36e705710..eaca978c4 100644 --- a/windows/tauri/src/features/settings/components/tabs/git-settings.tsx +++ b/windows/tauri/src/features/settings/components/tabs/git-settings.tsx @@ -22,6 +22,7 @@ export const GitSettings = () => { confirmBeforeDiscard: state.settings.confirmBeforeDiscard, coreFeatures: state.settings.coreFeatures, enableInlineGitBlame: state.settings.enableInlineGitBlame, + enableGitGutter: state.settings.enableGitGutter, gitChangesFolderView: state.settings.gitChangesFolderView, gitDefaultDiffView: state.settings.gitDefaultDiffView, openDiffOnClick: state.settings.openDiffOnClick, @@ -251,6 +252,18 @@ export const GitSettings = () => { size="sm" /> + updateSetting("enableGitGutter", getDefaultSetting("enableGitGutter"))} + canReset={settings.enableGitGutter !== getDefaultSetting("enableGitGutter")} + > + updateSetting("enableGitGutter", checked)} + size="sm" + /> + ); diff --git a/windows/tauri/src/features/settings/config/default-settings.ts b/windows/tauri/src/features/settings/config/default-settings.ts index 5b25230b5..786f0041a 100644 --- a/windows/tauri/src/features/settings/config/default-settings.ts +++ b/windows/tauri/src/features/settings/config/default-settings.ts @@ -210,6 +210,7 @@ export const defaultSettings: Settings = { gitSidebarTabOrder: ["changes", "history"], githubSidebarSectionOrder: ["pull-requests", "issues", "actions"], enableInlineGitBlame: true, + enableGitGutter: true, }; export const getDefaultSetting = (key: K): Settings[K] => diff --git a/windows/tauri/src/features/settings/config/search-index.ts b/windows/tauri/src/features/settings/config/search-index.ts index 069ceb714..e07a6bb1b 100644 --- a/windows/tauri/src/features/settings/config/search-index.ts +++ b/windows/tauri/src/features/settings/config/search-index.ts @@ -410,7 +410,7 @@ export const settingsSearchIndex: SettingSearchRecord[] = [ tab: "git", section: "Editor", label: "Enable Git Gutter", - description: "Show Git gutter indicators in the editor", + description: "Mark changed lines in the editor gutter and review a change inline", keywords: ["git", "gutter", "editor", "modified", "added", "deleted"], }, diff --git a/windows/tauri/src/features/settings/types/settings.types.ts b/windows/tauri/src/features/settings/types/settings.types.ts index 9e5901418..6d4f6a6af 100644 --- a/windows/tauri/src/features/settings/types/settings.types.ts +++ b/windows/tauri/src/features/settings/types/settings.types.ts @@ -207,4 +207,6 @@ export interface Settings { gitSidebarTabOrder: Array<"changes" | "history">; githubSidebarSectionOrder: Array<"pull-requests" | "issues" | "actions">; enableInlineGitBlame: boolean; + /** Git change markers in the editor gutter; clicking one opens its inline review. */ + enableGitGutter: boolean; } diff --git a/windows/tauri/src/i18n/locale.ts b/windows/tauri/src/i18n/locale.ts index d1ee83df0..b0912a855 100644 --- a/windows/tauri/src/i18n/locale.ts +++ b/windows/tauri/src/i18n/locale.ts @@ -2057,6 +2057,9 @@ const catalogs = { "settings.git.inlineBlame": "Enable Inline Blame", "settings.git.inlineBlameDescription": "Show inline Git blame metadata for the current line in the editor", + "settings.git.gutter": "Enable Git Gutter", + "settings.git.gutterDescription": + "Mark changed lines in the editor gutter; click a marker to review the change inline", "settings.general.version": "Version", "settings.general.versionDescription": "Check for updates and install the latest app version.", "settings.general.downloading": "Downloading...", @@ -4513,6 +4516,16 @@ const catalogs = { "git.diff.statusModified": "MODIFIED", "git.diff.statusRenamed": "RENAMED", "git.diff.stage": "Stage", + "git.gutter.title": "Git Local Changes (Working Tree) - Change {index} of {total}", + "git.gutter.previous": "Previous Change", + "git.gutter.next": "Next Change", + "git.gutter.stage": "Stage Change", + "git.gutter.stageUnavailable": "Save the file to stage this change", + "git.gutter.openDiff": "Open Full Diff", + "git.gutter.close": "Close", + "git.gutter.added": "Added lines - click to review the change", + "git.gutter.modified": "Modified lines - click to review the change", + "git.gutter.deleted": "Deleted lines - click to review the change", "git.diff.unstage": "Unstage", "git.diff.rollbackHunk": "Rollback Change Block", "git.diff.rollbackHunkConfirm": @@ -6519,6 +6532,8 @@ const catalogs = { "settings.git.editor": "编辑器", "settings.git.inlineBlame": "启用行内 Blame", "settings.git.inlineBlameDescription": "在编辑器中显示当前行的 Git Blame 元数据", + "settings.git.gutter": "启用 Git 变更标记", + "settings.git.gutterDescription": "在编辑器行号栏标记有改动的行,点击标记可在行内查看这一处差异", "settings.general.version": "版本", "settings.general.versionDescription": "检查更新并安装最新应用版本。", "settings.general.downloading": "正在下载...", @@ -8873,6 +8888,16 @@ const catalogs = { "git.diff.statusModified": "修改", "git.diff.statusRenamed": "重命名", "git.diff.stage": "暂存", + "git.gutter.title": "Git 本地更改(工作树)- 第 {index} 个更改(共 {total} 个)", + "git.gutter.previous": "上一处更改", + "git.gutter.next": "下一处更改", + "git.gutter.stage": "暂存更改", + "git.gutter.stageUnavailable": "保存文件后才能暂存此更改", + "git.gutter.openDiff": "打开完整 Diff", + "git.gutter.close": "关闭", + "git.gutter.added": "新增的行 - 点击查看此更改", + "git.gutter.modified": "修改的行 - 点击查看此更改", + "git.gutter.deleted": "删除的行 - 点击查看此更改", "git.diff.unstage": "取消暂存", "git.diff.rollbackHunk": "回滚修改块", "git.diff.rollbackHunkConfirm": "回滚 “{path}” 中的这个修改块吗?该块未暂存的修改将被丢弃,此操作无法撤销。", From afb7a76649e4707db2455ff62b0f9b71c8247aa4 Mon Sep 17 00:00:00 2001 From: Lichenkang <2188718831@qq.com> Date: Thu, 1 Oct 2026 21:33:17 +0800 Subject: [PATCH 12/12] docs: update README WeChat group QR code (#1020) --- .../contact/wechat-group-qr-2026-09.png | Bin 182867 -> 175680 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/docs/assets/contact/wechat-group-qr-2026-09.png b/docs/assets/contact/wechat-group-qr-2026-09.png index 81dc3d270426f82aa3147b36e1c1c0819c0e3256..ff9f71b513455aacc5b5e3c5fa5c894ac07acd16 100644 GIT binary patch literal 175680 zcmc$GhdY*i*uRy%cd}&_qU^o*NYMtu;?DbB zH*IX4wKlcfewF|H%-s7u?M{L8veW;_=DTJ~CcjVf29^~xG&E%W z0)HG@2&ZQMTrYCrI{&$K6uj)6CjMOg6Ax`fh-17^E@YK*n~Z9AwlmghYhhek(H-Ha~?m6Vh!jCgkX4bTK;KK!r~q?PdC zq2@CUAJLIvpo>xdXemIXc;-4ELc_|;I5J$l6Ml@Vr8tmNS#T-I{wgDeN1CPq4~DVA2N$~pL0 zcUR0cc!cQHpTh3uDqH?2c_6w`Ju5?U z`#nX7fX3X5#a%qTgm>ysU+T`~*%s$CswzCN-=%jf_ST(`( zA`@2AzsrmHy_4-R!-uVlJBn%VhwM6D8=6OW@BLwsk$E`O@+X{(Eo;o7((Si8EKgP; zAu4#H5_~P^Zm=b||I~VJ{i-l79sl@Txxq zH%8X+F;q42lJ8dPt+`gd+cl$jC4yaq7T|&A~{f zgj*xJoc4_0eMJ{7pw;5bc%`*~%bcvS$d|3>IR?c=S()$NMMO|==D^b!{X4f-T&}FA zm$*>-Va$P$PB0w5Mz&a2#(wyEihuSC;(3oZ2E|Byw3?*8y3q~A7%GW9L*l2Ey%UTw z^Cq8p*thwA=%dRzuRcx-#G~NoxlHHw7sswKj*&CZHLG^Nk>{Gi-F zKZ)v#0sD^a>HaFMu%nUYFC{Fp+paw`3Vgw>q|Z~V8pZ0Pa7z=EXCtt)4U~(=r65)5jGWCM(KAfn5o) zd)Jkh74!AvipO#c%gx{3euRJf_U)Ago!p_0N4}4$V>5r6J|hYxyqd^=!E4KFIa+vk zWvtlni%kX>(}Y=XU!T%!^GoYPY3L%%!(`8Ird7yOWi?<_o*K9w&>@x$}B?MtC@Ul*hopY%*lOuui)F)2MtcAzL2)3K>sFrvnH z17&QD9h~qsJ~;MqhBwFQ;l{VqCm3)7e+Dv{<|g?wIyz_*mQT=}*w_Sgi^qDte~*6G z$4#azu1C5O>!fbfKQF6^n>W2vjgx0qG`5BolQUQy@I^3&P}xes@xjQ*2*uZovPOlj zeyIK8WnC{MW7Km<{Q}I(Nkm<#R#8-27!foZLakxe&$?9!!$}mmU#;n_J!QCmB!8y% z;}-xYwojxl+-~=GJu8V>-C0(QM@ihwo3@%EH8Lvfk4W1ttaiFJ_HVM-Of7$gY!!hR zo^mUttO*3yyt7O{hjKDE#mFjISUI` zeBbR#9yZNgwq7~2A z4PLxMH4DVT;)08kAvTfca3mQ|gK`DNb>DO=HcA!pvv8@VR z_NEtgLHl<$`_IPg4&FrlGtN&>XNk*yrVcu#p-;a&nqlFcd)_{_7V zA%&I4QZQC>pncVSIHE{GAn$%+SQOWQqcadXfMx&e*MANXV&6i?L|oxr}` zUP{vGvXQrUaA=$Tcj*ar!RqgL<;SCRQCAEdnG3<@i^HkHsn{9kZlni}j_ObQtd~M7 zP|m%pXJKCJ%wSMrgdWw25u+RoU69A*6QOc0S+c2HL`l5aeka7(_hv%TIqlDS=MEJr_W^H|a2kH9&&9XTQuU)&g1C3WccgSM8&h;Zf z=3;l8HT0)KKzn+-(Ta2a|IV039fYWFnU{FY65RS6;5qwJBu|Mv_iTR0YjAj2ekPE8 z(%^|o(yLd15|&Vgq#^jpNl6s0PWUnNaSRf$rbtQq*z^IB#WF~Q!nbLd{Lfu$Ni2UK zomt*~=du_hV$~IWb!DVLrvt-AO$`s4O7Q9FY2ls{@#L|uv|WvHieVAe{Sq9!{G@rC z`%{ee4W@i+R?2G^9j>(l%el>jS{(NVU81BjZ&?hpzf`8hG{t8vq3H1pyTgoEgZ3Xd z{<3w{ZYQYzgRMUNPqOS<$8WbQ$It%aW3nks{LPjiKsTexSO%9u3^JzYX%;V2Cr zr^zSBR^*CPQc?mKayP%e;=Wg5(TcIZHOs`!&CL>*B!5jMb07?wjVOI=fU68+huLD! zp5)=iR2VFGdI^ueXkY$T2mEjv{C9cOg7p9G0sl)1>#k@#Ty7eYa2G|^G+Z1^*Yr+@ zc;?vm>DmPgQmu{{A(zY%gOnwEK25*SXzsLWYcdDXlVKhBNTq3Wj$ub`(foN}l-U-{ zZ(J(8B+dLe?laqXmCYBjH#kGb-=uCG5Sf=JE5E`u^orqJYRq4|hyT%z`G7uJHNA$D zF5+#0k;2;jRSV8l!#3AAmcF8U1>2+dbSy7U4?bFZc=EKwTobVZ#8J0i*67knWjLth z`Iz=ret6^D_~2jw!U6y|J8C^L3|;nyH3+4F;K`4Y*1>Frgv3PhBT00-JKmpu%F3cr ztNO(?hIWeD=f7ktgiVg$hfb8{ce)B#C*Q+w;l8o8HCtp<)D%|Sz`#H^6l}W&J*TSb zYRWJYrqFM{bCM!~rzqzJ4lF;Rkk<|O!Wgy_1SAfyN$~caJC8UEVTA;o3*(Ra;tY)8D!BnF!*v33|bg5bx)WGslNh zPv>1kG5j)LzYYM_Gw%|4^zNuSf>ibd=Y>); zfi}<810Peb`H;6FPZ@poS6GVCi;9ce;pC$M%U8P|GklQXv21{Ece2Gv2A!Y#u63A{ z1iFf+Bk0ng)HnnxTCXSj>DLFjLvwIJU2P!*2}wx`dpC(=9I7Vb)6-*)e!Ykb)zMT} zZ%Y$B=+DK&~KIdI-VD&B|i_^&m15o&n(20`2t32kuE_*aLemjWf{E zp+n(OxD~R|io0`dHf~9OC{2K8&Bn=zV^B=>wE2Vsb|j$OaDe#b`g4@{O;%dN0F+}= zog4689BnGWDNyqm1+0z~;Mh^1FtATVq@)<&4&2(TUuJ~*5bf};hq_ET7tcGMhli)5 zokS)Yh}J11w;Y><#I-1zTcMp+t#LA!xN+SSJ9)1Fxh`k=Gr}6yUj$G&<@kC}G7eZ9 zuwR~s&GZKc2k0jn?%8B$fK;LiGX3bFN>%fP$lrzrIqm=NLbphNf4lqN#q2-JU!T3@ zgf69C9CpZD?vMaHM;bU<05FGZ@OX*Dn-A8;2qXsa<(pyb@eAF{A zKtgA44+j8l?6p6n0_23>|I!y}bE-Nzh9y6LHN98_5`!99?Eqrd==;*4##m5DNKL#j zwm`}}2iSRu!OaqbC!edxgl4@0QwVFa(tF7a9~N zq@>^nw=#2^q83q^OPS;LGa(obPOJHmc%J$5~?<>x{FV~dF4nd7OUXqb~`aC z$({jdO1q;l4zD4%4sMgr0lxj+l95$A&6CZn^hgot)Nu2ih;!9y2!H`)NWBV%jnwS^_EzsjQs6(N!ljiBYJI1Rw=zhZDl3mb|-syrKw77$1SJ9yGj?AP| zcZ-TozURkB+I%+(#*~C%{;zVIhQ(uKI;VPc{Ppg=)LtdkdTx^<_3mmb#r;7Qe)1b4 zOL<}}qi)prn@bjkg`|6-Ua5KRIQloJN>gvuZbBsS$}tB*6~Ojvh8@KXv_?-?(uZVHw1ht3g)Qb=NL;+^qIB;9VQ87#c^O z)k*iq^9c+y*4KF$<8Xvic?+=dajAZZh|h%n10levp%$ zjSj^iCpcinIc#K=&_}lwo}EL@FecmeQdJPA?CkCFaDzELYpSaUj|cURLh-rTVxJsH zt$H+0y&|cnC&n>0Hnuy#dgh$L6@PojrwQOLi`1f9{YouHDVe1ypP{O~Sh{wdDW6Y# zVMBv>p5RMqCfF73;tYHCQ|R#X(Xp|R>CkI*bcIAGW{RVvM1vnTqB1KNCqHoT%i&3|lzW@fY$4W*7J|@3o7CJh*xdWf6v@&+X zXV31Kzt|8$!1VH7He4stYx>hAKxU#E;pOG+d{W*N`t_^&4N>K__kW-sv;$6z2o1&V z)iqoC0NtQ*t2qklhN9Hfi4BkQWto3+AWMC*;cNzVq4{i6!g1o`6_3Vrajq?%ZOJ%5 zt0kuO;QQ+IqvCiy25?i`FgWF z>+@&&m#6y)V~hsy<-I?Bc6N5*lRq;7BSBp!j_VEvs5IOopm%g}ycmbD5LS<;6t`5x zCLM*5%PK4@QwD}Sl}5@ri!dbHK02tYL!Qt{2nVFkc;KOfq|R)z2`BG$woz*@X`QiL z2|S9fR~6V$RJw$x6=+{%PfD)N1M;cx*f16p5wSkro{w}o0ZB$yON;2D{^Lh-jby$~ z5GyFH?~4cu&VdLQIIuPs;aYEm=Ma1dR>gMxxkBcC%Ry~$@^9x-UAi;w+z z+S=7t#-aF3OiYZ-KVchdrP8_^gz&b*xSGcUj3A{6E$rivFh+|#`(rVjrzVK)#F`e* zuCK2@S6trQmLl||;_0k3?81t@KYJ&-rl!%;)6*3*oglYb^rwjxmY0)AY+1FDQCrP3 zr{v|2IRw*X6PUe=chaw1f9vu!y~F8`DBs*mJMbcAZtUJ(kGht9G;9cq9%-97tr{a@$;T_iJ) z33eOfUK`7+h6IYBAhC1KM$+_rM(>O;^YZS6&L{!T&FP9vPf>c1)D#sI6vkIEWVN-4 zZNI+_tTyuWVT^2S?^n>=+jf5zY|qB2z@n);&H?zk=iG9uK)d}B7<$uSWXU| zo>=9(@3FGFRm8rf)$Ml`c0;TP8WU_TXw_z3(xdY2ljfb*4J&9bJOQ{{Rvfr;+z4j1wDsN={+n)D5e1`wz zTKz2pMb@mawItm-mq!HXkDylZHU7Ol?LMN#|4vhI*RcATH_wzhsI+_yE3AaH{Ae_$ zE^><5j~emQja@z?I}dJ3fXlN8q|qno>*(;F}pR+fFhl^b?3YL~E!y_ZBIVjvP z)W%dT;y2h+eBjsnSXJc;+?Y*7#Lyt=?*02A2z9Bi7qh+DPrz4)77`LN=gkNl8E_u@ zEI?C4ZGZxF%Ys}}L!%vt9>3in^Thd&A3qlQL}pPl&LlU_&3RZ(gi`wuEj^T&0i@Df z_Q?*lQwW)EU3KsJJGumtoy)|da~L)&HUEd=VKLSz+BB z`!;_fSA_($=cukHG!9{KBtbf&wY9bFHiBE#R~Pqocee{*XzsRf3H-=(}0&+@UZub-e<5)`i(;np8bd2aubOh`=B<4A~Ro zb)|F^zvHL9V@vq)<425YId}l%qOl5oe$qY%YgAh$C?Wxgp08iq9{xKu)JzrD+A%L4 z!!I?6s(JGBt8;B=BrNpNqI)~FEf-otT|-bPJHpAlE>D(s;4GA1zc()JD!iNP0tLq! zwshoV%gyr7->2prMB;w=rlA4-X@kg!gQ@KV-u3&CBNc`l}BfJn%XgGlb4X z`I-X^tU1sjx)Rwnpv0ns{3cW=-|`0fH9RXXXc;>|Lv+!f9sZp_v}M3*k47q{HbDNx zd2{O_acUoRZSNPG+w+r8$`LIc1XIjQ4j!4X_CQ3g2N+9uLive27@MdK0qB<(XWxDS zuj99D!{T}H5eoz<*P3Va{1#{kivbOo`U5jzM3F1uS11 z!Nf~HP-<`!Zn?S7D*L#jFm$@KH2TlGUvJWa574CJKwP(y6XgP!dNGX2FI>FEw(4q} z)0Ne6zjz(=Ze;I;-~-S52TIj#)M}B#C-u;3d*3^=~;p}a1~W;?XKK7=}3@`hB3Jl z^mKItUTihdOiWBf9)WENrI!ktOmj|U1G(ahgu;N67(#{ffbUvP_R?*6AK;9a%QVw+C zw2hj4YH~iMEj@)UR5u@bE$B~q6P}VCRsi(c1!%x34h{~Z_WhYMOtAF{J|t+vWtKds z+76RB54?1qX?TusakK6CN>xc^B051_unr70cVB=eX1O+6P$YxKAR_A{_rZob0Xzz8&u>$ejeZf8uuCulpu0;BBi)LT0c z@5Iyj@>FlPy~qvGbXmZZvYK`lsWx`HRuyd*B9Q&^o&F?U3^qqn1n~-#6jcBNCo_2) zxErz(_`6zeK@oL?*7pm5Ut$NdtcWCC;kR$unJ(L$s(spXwC3JW*lh;RY1IT_aI36; zfz6)Usn`x${JVEot2fAD+vJoPzE-E=@5icg)h`~aPHR|vV=IbFCFdxu<4A6zMT5m1 zqsgj(UDt2R#rw%SDR}%{Vmm8nUIimcJY4Z3o!P{PH(p1+zh>{_W?Xw1N{>Oxh!)=v z&dgXUY#2R0t#EwNrog}SZ$9+-{@H~-|J75$=l_=l*yKXt1mqD5M?@UZ$~#&I+v}({ zbZCFNBa5|TM}uzj9rrOIXVlHgut1A}E~_C{$!dzVruk604L)3{ig?T?ns$Hbq5H69 zR|yJc%d(0Da!l5e26Cj~Jq5cII(=xH_Fn7K942Rspgr$*5zh zU6aVW>++q}N^B56glgwUrk4hPVs!+yxVdqSmwkdl2=3LHz9yCHQ+;`+A)jKhNsEO^ zH{bSPu|5(Hes`6Pltc8kJga^qBgVvTw6L4J%2Y?^&0$P3S_L(UH=&2}smiXZ%G~n; z!^^^MDWjW}8cr zh+)-66?Jv@Us-hfJnn-pdPVxs6z>+S9-V<8ib>oVe&uoHHm#aemq{>ne_-h0cLjdb zVf_wYRqXZ2r!n0@%=#n5XzBePuUm1kpA0dCCsw>&39|9_76a6aj>S}{p{}lLVsfqK zFb|}Al{m(HnVU`POT9^4H*ZFrZijsPEf1VpmG}j8L3Irc@NwDy@r^P^+!COUxbjsPOP62)t=hVqn2ue^@w*OeOd=v8E?(Yzj3)qHW9h}n^~B*T#|J+j zN|d~e`qlxg&9=%CYAuV_F>nx3u;(;)_4M^A^)8@lBeYzLr|Wi(j{5XiQJ_A7jgV7T#<$k` z@%?*jzzPI6vLGLTU*2*l{>f8=D@w2t12S$+u-gU|N{tbt=h8vp(C#}?7m1PBUhtvd02LFtJ+ z0y!S>c)|8({Oi%U^)*AuJRV^D zU@R26sDYrYs-cloa&4Cw+yun=5Nf@y9@f2Rzn8v{v?BlTZJu;QW*sPlbOpvAoWl?6 z0LXqg$Dd74Nr?nGK7|vxH0mfNJ-cYCHl+{80U1UFsenfWgS0d>2taB3E@&^FPJH$1 z)yTcjpX)?M@rlBjuU;wc-7Fa6JYmNf%n6Lfl=9iv{re2?7D&_KqAB+UQR39$-S!X& z=!~MV-kh$tEC6p$7ieE-XlRyw;`xcMH0W`JOw#n%M4SmF`n+71ddSlJFHcxzZ}BXL zG5X-vPP=Is4I~JZf$&Zo#MNu+We5oF*uMqm&%<}eMde`a(l&N z4n?=~bJY|j9uz4#Kc0AQjn#(Ljm1JH;gi?lK+3$O1`4QY4Mj{0(`R$(9z-iLL46=M*soSwZlMu#J1}|{Z_w(xXfxGo8B9cLqYOQ ze9op>0PETo1H0RmpDrenP7>7hH*ekun`FW#0Ldah95666tc?)Q9kN3tjhbTf@>(q{ zik#MU)nvnsK=X1H5U)1~^o&)&s|5KuXyPs-7 zu<`5XHdXq!bK|WQR0b=c(8J=J_F9ZtnQdwLPf8iDb7cc#)WoMyDPPa>`&)s!KPgWD z`;EG`7v)omq!$Q@19vb7IJ8sRWW$WJ=zo&gIU_my-A!qV3=AAWJo2R;3va5o}Uc(BmAUrR1g1R)&My5Df0D-_#wz)3b?Z8 z+qa~RFnDTS8#O;co1i~np@Izyb>n-Q7zG8V_N%6W4G)BZwgS?Q?dnoCz63^F2W9`! zl(YaF^0f*eKxk8aZEf;M@@RE+)kQoVopTuvw0o~V?;h(K82l*e&b?VyhXIx_9w9bn zwK4BO=MBn4|MMLcU#QjsBcgH8=k}Hd3@O)i(#1oPANq+cDcH33^!Imte|N{V>*`~! z2BI_I50&;K?@@<%AF62|Uzdg)<#iqqwA-9&XF@@1NrBY_$)4INgbP77&2j1z5t=TL z28qo&OsK7|W0L#!BtgjZIogZ?)h!z|hdJr_Ps!U?-S9#*Vi)5xGGaprXciz)Rgj{+ z^NG2>Uv!ZWluJq~D#+-tfPLmU?TUkhfHvmyU`K&*(2dwe!^3)NCd0Y{w1kMw_eOjV$feB~C-1ZBN*_%>6A+uDB5`g$ zv4y*h1H1|SLd&!q$%nwBGJkn67XRI835X5IK^@@X1tZR)13d&*pmAh)|G7#Jd}Top zJquc49Yh6OA^E1aaf7+(8^80kD4ZDdS82;DD$2{D=^t#b^VHYZBcf$IlpX! z_`fK}(nmnf2ihNqL5O@OdiJ*}dt{YTXZM{ZhpEqMKHTeq%8)S!A*!s7pjW!zRQYly zAZkN%3A||#Q_<$d*!n>mRL(Q-4j|_hO~$4yTRet=c6ziG2e~I2pfQ3BMH=Nl5k?OO zi}QJ%0hAGupKNXq4ch79^Z`hRBu@~?gl#srm0sHlLNh?b0|PrO<8N98BbI|%a2wf+3Kgfb6K&8z+6@F4 zr5Qiyt{gfU(ZC|HXBK|0x+9MRf^s?P6m~PzE&Ib7mY@&_M(E9^fGG*rRl)BU?GbEG ze-)45>RSadGGsUY- zMU}nlm}LhSO2x^aQ_`EShCMSp-ZvDpNCtvApZKYHJ8a+NmFhR=5#iBj$XgoXWlQ92 z)(K4vZ5Vj*YZZM~cn}?;A^I|4oTpg-L!GjgmAe#T%iLDH<4Vq=z9OP=>~Bf8Q2J!P zgEaPi*mxn?JO|~xmM=3WxoUZLkwcKLWJ7|HCciDWy9KboxYz@Bx<;&d!C7%h{fR43NSSnQ8BCHx|i{B~SD+;t(w6mGr#*2xHd|AcDp_jlK5QGa+Cctu02 z?vSddTJy7p;KdSRPdd=%`%!0>VUxOi5Kk0bw7)a{j)OA49W7h>+F!=(EEEcNGch~9Wqf2lK+WO-v%A^=|Y76hNih6`SeZ)^&$+i=d zN;~rgyceXwvCO>KU%5+3L(~4-4IPb`Q8FBYZK(afZ+J~yAZ07;#|nHY-6$k^Ff31Z z4$?Ybz-v`d-`k#V2h~xeZwodi(rn^0c=`CcVB5$y7)Ai3FEMWt&wKIh+qX(Es0-6L zRW!g6`UMfG_{~yqcOVg9Pul`QwJ&7mu=oR*P90KZo z!ky41vOj*johL{Fmxg#$OOjs^o%UAC`R%1M5QN7HM{M|)Ea+5}+uRm9@Fb7tf}!d| z{-gTuOQ2jxvZ?vs<%|asa7oA33eSU)u(&+T!eR!$p)H_~hBqR`be$FAI^gDXfOCP4HOWcP@*Id5KsAxc4opte zh(AQeW2T%NiEbK{WD%d`14(ZO|0m+>*RO?r=ty*|-2Z|AFwk4^=MG=yX?7YR24M^K zjIMu11ierEkk~v72e3Ndl{Z0zZp3X5rT-9k0$7#*`Pyjsu=z(DJV3$qx4RR&GHsKZCcl5Q9wf*+m%W;W=pA^@Fzj1kdIAzF&b20b z{MVhcIvUyZyeTgqLKg)@@m9jqdEEQie?HNx7#zNR3%ND0n&qKWLSs4x-rcT!#~c!m zSO6aVdX5Obon(hGw(-_ZlmB>T)6B)i70^B(zNTx-^WUDZu9~p3p%O!3Wa}nfk1Z~# zzQTUll(`rU3@v$%?&gEaLE4SHj{jyNsOVm!!WE6O5_k%Fkvn#&H(PmJius2-?d}nas;8sN1dKr*Odl!huhb?iE?RxA+|NU&rdF zpMezNx--PsOrdsGGqvGflAx|q{!@c5?`pKs7c3R!#>E4lgyiI~)?l2QT}h=SB^}^u zt_&zX1BPj5V`GCQ>Pn3tDV$2w9m}9#W@Z*3`xAoQGSZ(dM5|>M#M4J$xcOI-0OIX&1#H;ej>o=0=YLvoc7)9XnXg zV|~E67H52kyn7ZI0DzPibE*nBvk(Q#&B+Pkix01$;SpEk@4#v5a0=TG?$Nk|6Z7^Ff?5Ie)Vi}@f(~to2 z{J8bh-o6z~)9v{%rku)3GTA4iF~wyK_%O901tR?_ zR8ZA`^fko(5>HRm&Vz}DNC2?dQ2gS0d-aPVvYLb*f>GUiaWoqT&=3u6Y#O&FiZ~1t zn108#r=Jsa4_&ey?^6Hyf}0`--+|b0uo9~ z=#&AeH|P^a4es0s6GSUDF8}^r^L&x%-xu)vAMO1)I=ic{k3y1?g{7rVxd$;JGTlb@ zg%k;377#$v{=KDOehwBD4UIJtY`b*}XYj^o9Fq)V6g96rJb1JdHzZw))cm=liuZSH zzkdPej^|o$__8i8)iq#=aIw(FPZqyv8U9X?;+mXkcWqU;guF0dbXJeIun70MpjEU)mOy!oTE9WWtvUo0JtuThRnTwS2+OM_(5%o-@2Yp?l(%;yRNL#%i1+;c4!htM2} z5X1*>2P1=u$p`c^1zTH=m&v_0@<1MdLdJBL7!+fous>=+ z7&#bjGh8Y%=mLZs44UktUOW|WuK+Iqt(1oewbTd}<5uSvTq1hB`bvV9G=%L^)`iG`T{@_VMG9D+R3^+H&Zm_KjM|;lOZ9b#x2kyf$!n5jO?*-|! z0+De%QYCM~uN)z_g^#z;}xKR!p#`28}n)lLR=@paWb2Y_yDJzm^Q zqvyy!&+eUUI0TX&_1K)|UR`5NFc1LQ73vT`pX3~g*Dk8Bo)Umq3}c!E+>B+jTPmR9 zN+tYt7l-jha+kO?{y#3F7PV84{NI3Z1M3)!$a!fyirUqEaSiY?$Zl}Ox4NT_vruXX)E@1JuX zFR<{@(a|{=JyQl31riZ(pM{;L!yz^o(X8Q2h6K(?q>lrX?~yqaE}gS{TDG82e?j@n z5au+J&3*8G|Nf2soAo=JtI{q+cP+pi0`DmCu6xIcDi#Vy4`@S$!`KaI7&qR;O>J~R zqsE{Y_J^<@(uj2Dha74al60h70s0g@{F1McjJQvUFW;ELYG7w)KL!>EQSt@htL*Ac z+$=oT00to(r{#Q?z*jocaoRRZ=@9wUx}CYkeXOhmgX9Qn)D6SR&MnDceyQnCl>^-X(H+Qq)XDbbTU_;F(f3CrbfaEqb*xob1s|dl($ziRV zaYie}n1|p3PHLZWdypEa^xM$J=U~8Kw008IU1ZD=$=893jpldj=0aHC@*a{1JceoG zqHTnng82uleZHisKgaI`t@>&qjN?{OJf{{lf0}R@L1JMDK zg&4l0c&_!P2-ZR<@JtV$NQ7{FqNFho^=HT}6qb}If-Cmur5KE!l$bSA!x|lBg7Aq7 z(kNOP8Lc&ePyf3-11eeV|8%;J3>>z%lHMO38KoFWJT@jn;P!FxmC?2+bPYl+ zSVYE)Kw&JJRW53n`QM=>koF-BklGgt;hP0W-5}BP7*h5zzgb|AFyA@c{1SA5bBLlS zL-IL*M4tsx+Hep=4y7{4ZFR1--k7WbFaX-S%nVDyS{RbF1Q{$xyGn{Ixe9nA!0`&- z6E{ThMIP6@y;v+kxGmK!@N4gaD+W`P>alwf!iP11Hy(%oUlxGV6p|Mm4{uTiz!VSl zoUE8WXXA^*zpZeJY<(C``VKYX7FU?f^E--LBZS1-e8cdsGQR+7Y3oAp=TB~49@euz z-?J^hFN7%PW^22@j-SF{?JvlmJ2BX(s#|oUSIMNnzi(AqtsiDsr0-778XFw(Nh?j5 z>^kZ>h57}@S6PKq;uG6?Hwj-!)zTti3{k;< zZPxR~;VRVsd0jpBiTk$4RY-^2bmY$4m7fe9omDHihcBeuY)G5>fG=|%KW5Ao7o4(AP;$BuPAVy<;z2JsN8 z9Biu_j_Ng>vdxZn z$0PBkIn{U*Ui^OR0tyiJGItZnw%H6+Z8s* zZDiDVgpwo@bcJJ>vv7d`lJEh&Q}npS#tpsfyU}9~O6KN_{bI{$0iwND4o{x!01O5N z>dLv_c!ec0cxxln;Mc+g<}g*+F&*T6XgVS z51GXU&VoLx^YsgK1i0u86BdN?zzjZ4Qt$Pfl8_stgb6VFc8z}~;Wfx|#q z5OxZL8Z0x1Bz_@;RM3vXqX6H9IrUcTBD9MS71Z&1xHY_hOp*CnqB}S8xsW(Bxb%!@ zEn&A0e3}p`8@_bilLr@3pZKyR;KQ4D+M6yC^-9{mnhqW)J7rz?xHhw`isH zIO9&^4KrnzSFGa!GYRp%EE#LRbxF%VsMQ|;CI&O98+3S3#2>X}H#E>eSdHoB{@qc| zC72yVn920>pJp)IK{BpK1-avG7P1{aoUG^ z5Uoex_S2>V>Y7p{Sckl*K!dMd(L+N&cPI-SKUlQL6lwQjpmJ{OyT=(3$jlr7=iI|q zt5gOsrplX7@gYzkc(^%308T1AvHV(E&nc_M$-ylNN4I8 zEFL2dGlDbY3oI>QPhbpy(4~*?S_IVF?5*#3@x#ox)8H}LI=IgP*YbOw<$ydKb4rI1rjJMenDVt#A9#mCC9XS7e%Ri!UAii07_W+jV0s=Ww zJY1!Fh1`}@ze8jZN`m{28>B#U5#E}WyxRdQ#1$ayBCOLa4uU(6`#Y!%P369+Y^uXM zARyQ1^2>V}eWlJj>*jY6tEIJ?+*`^w&5RehHf1%v^py*#sB(X4^DS`{`qLnK@8-n~b)<{@p>@LmIB5!S!(UXiu6uqBhX z9F+Dm#wA0tepd2Uq9mSXhcY`s=7M9Pg`a+ z(=a1gj7ZGbB1!Al%~9N>eHbj1`&Tw(XLt7cBRa)pEb`iwKLHISRPzg9Ng^*uK$p-^ z|NRkxiOb%TPf;f{mrYXf4jP&(mK}+FyrSgd!Gm2Z zeyH1)?6+q+~fS4%V(*@}pkKqj&AHVTEp%^Y|RG4;$MTVql5(Vx` zSjn>5b%^zi)JR6jI!Nw=z8eXlNLG?q63j~NYf~2}5GLp4Q*;SYD z@q=hR*sFXSN}^?Ms7Ku9`gmtLT*-QLdMy;q4>vO`_>|>04BNKD=jC_loc!7O@=!zf zP>eJkTQvT~<17v2HXljG3|UVkdJQW+yI%qI>%HbX`KDX(t{kRvjm2%Q^f00s0&g?v zl(>19!n0N2=1l@bcT|q1C7XMbEAbHPf8R!-<9|x=-L`7UV)hq9;^#)F{9xapyZZb_ z#lowG+8k<>r9^*>j%HO?S8LhbDtapIy?Y%h_Y}Ld;|uw(70u0z(AMSFhhbbPCkNy8 z>(^+;xhaTF4(M8L1m+qb0H_4h#PE_Bz-W-x4rrvUFNVpc?l`7`=R+{I42brIAI-@T z7$!(G5WQMFfDgX&^5sj}=%=oyFcdiNG6#8i1(=&LwTKm;)CiFyxv z_jDtSm_Z&Ca@FUC5I~HK#GmN&Fa)X53PxdQg&ZiK!&{eXD>PsEKJ*y-2DMd0gyyID z3*ML#N=LHfp2B;2dW^CAmh(%J{aWeE*QVxIsV6#sscD2f+Lk>9pBH>A3HMF2u@B6$A7%|Q9-afk!_1b36@ZD8Y z3%q3tC*?DkGyo*`p*Cva?BwC{$J`r0hh<$S!+E%8s&ULqtYeD5r&xi2+y5lxih?`|WUf|W|qK=deD$N}&ASHYt7gycf&ZG&dqBkhemw|z~pV6$}Q822!#mavh-!e6@$eD zO(swR(u+Z;!cm=mC|SG_el@C;7aZ>{LB(kP$P=qFhuuwAEfpQZa`it%4@tN;s_Iu~ z(asCa&d$arB;?2@D?^J(@|F-{)ac5M@Ft>?ym{%1cST$|l=x;Sk`n*cqT<0SW-(^A&SPZR^8o zQ*n4I_iPqCj;-R?cpxgNr+}=6XR1EdLzHfR?p*fi$MWetaZteoqbwJ7qC{*F!E#`y z)ziQ(OZ1Z$K)j_h-Wgg;f-Dl$0-%K*ij1|r_)X{-untgU9QhAvW+0SMMv>nEpKo0Z zatr{UWk7nnA?#{26AU#%LLpl1aRB!58p!`h*wrxy#8{QI|2E=tiuy0w@msqsrA~(E zh|ocX2cGNg!&qp7iMcDuI^)+ztgM{$$Xl zQ-))AX z8w#Xln(mSZte&BBwfLv?@$I1R@CHT({2i=A{dZL|tNUq00;&Y1m(Z0&V)W zs(T?zKtvDU2@hq+n)u}HrWGhaDNdR2uY?9Ax|o@mgaJ$ZX!mTWdCF7!opZ2_HQq?P zwxGPvgn0ZzP&hj45p{z5uD)i$hs6PiZRC_;>3i5B8mBxj^|qWB#Pfmw$)sG1O;;Z( zR=OS>%)%Ywk>BL#oI81AMEK*p&Z#f^AKL8@*o+>Kovubt)v=JsnywK%m+ZL3V- z@d7WVV0wBiSU?yc>K*#;dZt#e%Aj$_=-*-A4hYnv#N(u^u*4$O9 z;F~3x}4JbUvHb{f&7% zy&>kVUSC7UuV1#p87w+{AqE0Gd7}I~?(rwDveP~)d0rBUERcm$9!bZ`B(9Z|Sf66$ zOy*E!?L2;&;qlcf6<&tG?)G0d=9UHemzQoOxWDeU6_#DN_)6%=>zlO)I_8w$srbG9 z@Pv`XVvM~WL<^^dy<4Zs9}5tPF6k^jwLfs0&0(YcMoDNVq7W?+8Z6t^);9cbqU34W z>8FCPENCDGZz8Rn*?kvPE5n~5^x#R!*Gl$p6s9w(sxm6_yqq7&R#$Jql(Y30Ir;J8?r2 zQ$Tm>I=0wB5U)@F%#Eq@OUvk<_?peakIz%BDoX`Y78PqFn<4hV&b1BI zNb+MVtWm@T(C<4J#ar+)Wqw4`chQAJE}+f`kBUl?N|8b02Rb-e&&kyO=@d1ncbytm zJ>m0yt_3ELYz9uX3@m zvDx}xni%)Xzv-EY25lRCsA>B9F01|ZAMUt4i{(pI_LB!B!wev9|Kn7H^jBxmL0Dm1S9Ns4 z-Z=B9e{}=-^jBxZdOX>7*#VI!qM7rb?(Z3x2R08eCrL2;1Z)_-@S{>&nL(9BL_{pY zF)o;s_EnF?`mjXD96@k#jjy4&XsOG^z7M(b7pirj8@6E*T`{*rycB(TUrPHC=vB$OO-aTpx za#di*-Y~u{_7XJbLzfzsd4%;+1A>ETb|@ZV9YF_L2MZlZQ8K;H7c{!BL-8t}%YPdD zlzOuHSV24Wky)3jfUzZ>Z?pe?z;ZN&ePSXDiLUYtlBzyh?KQu@}9W<54OUaes+X;b+7%2TyY zL=0?gZO!g)C%H-TT(_vl^p}h!kBZ(SLT5F?IlP5sWH^zpsDihKqYu_Na_7;d7dS-w zF+V!+cU*5vd~Z>AS5!#8{KV5-K;K3NZKL?5kILdQFu3Rs;CRTp9cJqT~}x3 z(wE}a@iXr;5kNs-0lnEVo#Z9#omPMuyZ@6+t-OLvqyaQ(8|y5K3KzT-QSG3l+yq}Q zmD9EV^PlF8SxB@bU>8Tp;QC7XJe$GKCL;p;}9)XPl`^lF_Y8--3t>Q z4>RZE8s^lhEOX6={^KaP7iRYN#O-5llyB{&BClLtt2!8DMDgtPc82F{mMYYmW@csz zp?7Ty(_I%3E>UckRzz zPDdL!MNb5-cxxBzO$w&s>)Q+r(YU~ZLo*zlXF04$rsA9pSGXoe*L`gXtUEL*z+b|t zy2+`Gm!>i&;;fxzNpsrvc8OazZyims_~)#FL1TYBuTLBHz9>qLwa-yVV~tRyxnjI? z?3IU(@7EP`rtCt`2WC?IX1Z@jUic`eciL4e1sHr-pw-Y}O^LVlOKK^sRHMy4e64Ek zO`PAXrlSsxXT?~%zu)6jwyerZseu2HLu749`MNcQA4L)jWohAJFSz3~p1E>k*(WYb-_kVOk?>A2G~J?H?Pj9Y)Qgh|W6f&~hTHw6itB?$mINi}4fE$+x{_Sp z?8}xrORMc~Mjbvd9^;^35W7H9G=N0fJ30O6oX`HrbI+u-l_XEREx9PH)+9|I*9Yea zHi?aqCR(h5(GV)Z+Li{0cUZvU%0-zOzO%}5Z`})MWwVc-akr>Adn(iKoS@j^y})F)#l)WA zkyC=+IuQob*9Jso77k4@%k8o$7)@W?UlHVG$1prtBqdxDw#x$D5$#%C@R2{4f0$nI zvAcO3X+`CxWe#tJ0pV6Y?Yq{b0JUWZaOcdtJRxg#H$J2NKqa9oB78U-`=glbIaef< z5kW1;?Aw-GWaUKMIqORtM8h9^b~5?(pY>b6*pa)9^wEN4Va2!g>g}oxtw&X)!~@T{ zudJ6Cls)pMS^UmvwoZ`U!&goSd4*l8a$lr2-_PJEMX@q{-u}9YVdhzX* ztEUG$6vO+64|@r^Kb_B5t$ouQ=|9Y*R-Vt@)d%z7ex2bV0WZWFh;)+eD0nF} zv^*|8qtRCL^W!P9@lsLaVS7niE`FtiXm!{=8%m3V2lb536B|8{>hS&<%UdnYDRE*{CgVU_s z|L+xYqvG3}TY}u#v$TzjHj=m+=!ox|n=iTua+Y-;U5E6E!MBybWK@N|+aV^!>}~|)uNA{+_sQcTxKD5K zQ9j~_Gy!vbel%oOGnqGn#{)GJi>cs>LyVa(7-`~vz3pF%T*ii0L0Skg;j^+rTqNPR zptx)l-99Zy7<%M}2rI8G+60z5+?S4`G4u5FeCA1&elIfDj)>iseots8s9g%SnJ?Xp zebcnco^=C{e6Zj0JcDji2vx8Ph=}Xkl(rDor`S$jB!O14RD>IKk&DC^Kdd ztZOy!B(S0b9nt_~ge|jQU#v3PvRnEjLcPBs^<3h9RGYNK zt4uApXWk}8DHKY;kNuGKodz}{>pg4xXiKkK+K%!kPvuQS1O*3B%eCSfIBYoN+VJY^ zKB65&PVy7$a?x{vj{$e^^6^oU$OUM!#R}^9ek>rmX@&%?<8DzMzIlN6a?_-jf`QXNu8VlYAnQa8h6vNl{Qb3E@cu$;zwwi_V3KOQQL}-#_}$7wDGUezR!G zQap!t?k9*!0SSo`o>xN4H(jiq{=XIg!hN>gkAJZd!}ZTiA1{&AYV0D-2!rRi?FW5J zIb<)l{bZBSP7$62wxFnzqz_@SW8MVt1Kp?%y!PE<#MNnE%8!RgC=hk(ajIkTI^u|ck+Sl$>oB>@_7hfdBc{GB`ScjKG*tGQ)F^9M~_Fkz%nE%Ch_ z(CP{$CWjN1bfmk3t-C<@HgEh$ek^0O8?}J4)`SFNz zW;)iNy0IfY(b)K$UWVKex$XM?%lsDm9=r%z{5*JFcLS}j+(c1_*o4W8HKp$H>%pJD zbh0kXK5tOiU>=fk$u~8b=Hc@Z4c5(QBvf%bDF(mrHARE)$D#nhSiaiQuvfDwG3Z_H zB8l8vSy}OnO9@uRr9i&t5Dya*v`B$(@MZT4=7!Utzv z(9No;;RICcyx@ly8ERMFKU;?$c>_W{W@7Rp%}*hL?(66%|L)!T#wzZB?p=py2zb=% zWWq(i8o1y8h?q%&)gJ3+`V)<@!@X~M5&0`1c!3p!6t955)rY(~rx8UGbj&F0%f!(& z9!*o4Hqq>#wD6h01xEz3@J+)zL;?GMvx%i&2n>}+@BCJo*t$$DR(zb7wY;&$dG8GZ zpIqO5z~W62a}`4^d4SQ=g~9h@(0>DfCc&!_PPV_8X#@R@t4M}~H1}%9fxp~jiQPlu z!6{d6mWC&sBX^I@jrsfE!a3(svO?3 zyJz9S8iq)#xwqPpHf|)4_;pFJz)>@^BKs1HGAl~#myg56^toE{!U_g9X*LtXFL{fv zqCXQGl@zG`3CGdR1OCdn%BSj8qWLaX^>ksaQ&!$alHf^LCUF|qq$DTP0r4R!0HFEd zF)?k+(+x3@jWxHnK0l|Ay$QG}+IQupae=Cu8oEz*fM}r0(@6ZkFnSmkJ}Qbu@oynOj< zhZNNXwNnTRLuQE$x+rggtU3e*$gT zq)|XL^vbnPpPMSS(_@zgSrMI@x~WAj4^B6|ry#cRu)8xQUUm~#4_5HUPkR=HPEgH_WmYr?EQC%j49?E5!FMa~v9#TLxf<==a>fv`#&*lFDBmn06rowPt#zbuB7Dk*jn1F(X!>EODEU2Lg}=+Rnrzd z)Cp9KOz4Y`eKDc}b-HgCO#li3n$Kt1k3Lh)h>exp_bDTspHU1u9iC@`+`u1WY+Bwo zMeO|WutfuVUA>#SJ0kW+Pw`d~%j|C9#vO*1K>TDCefoaq&{t=#whefuY_wF5WiI5Z&QEwNST7EShw0lb7cHMd3i zISHoqEk8XrJ-tuj+y20J=UU*fDDzn$c?7o|dn|SkViD*KWNhN6mTwHp-c-1>0iCRt zjZKc#HiEZ;e1o-P%Y|2a1#$L(ByE7jLPD;(U{d-%2fPvNMJ7eZ!SzOA0g9e~|J6+u zK?&gIAoj}r=Jdger|DTOWVQgZ8LN^8$%qN4RwaQSfnaD~9YeGx670@@soU)NNa?(C z(NVAeK3c+X^#rh9rs+~CC{lzvY9gOdr$=hf9@ZSKC^al3fY*8%ifKr;B}7Q$f3+9X z*t4{bxm@QTf;0(*M&I#3UPN4io~x37T6)-pMkT>!=N{4Q3>w~N>e_7jj#69hUSuCD z#GWZ-t|tm-2u6``ER&p!0qrG?C46DrtL3C2nzMgJ&SsaPuLfF;@jH~<;zrrk4g5+5<)A^-7O)zd*1`{5iPzDR={bsBA%rM;1& zYP1!l&B3vUv&Da3D!gPCNiUVbgrM5sZo{*ZDKFj?p0zYiAD;2DZ`t6##8NPz!{&KO zSnc5M-CzB-9a*f@;>ifo{=vVVm!GDsfZOF;w)&eHTt&>`poa**J5fW5=knuQ$#JI| zt|$@LWPB9Q)~%p;!ho9ZTsn-+lo`bQ?8gUkZ&Ws%22PFU;*M+&q|Sp-BOOym_l1kF z2X=l?YzOPp7k?64<{kap8{}E2?B8xe5=Zv^`>9J~EUFateY;^IB~4(>h4jfQ25&wD z8)gp5Za`zAiBCqxB8PGF$@@^qBUe>ulEVNiGX!radIcSq-q}mZVuvTQ4k)CYGuwt6 z@#rI%Q|Go271~KG$)I|P8#sFV0SQx59bD&@Gc>rC!l4W}?!3%a=&;iR^gf;Uv#h!M z9FCejR!=B;^|Qr<{A%!nH5ay`!l%0Pmf>n61@>JqL7)?|%gPKq_4i9lpT-NEam#2= z_e*KWzha{LMB8VRE?pMJJkxv5C)R12T{0cZF+2M02$o`;pov{>GD@r zWG$}zIYV@z<1$Yvflw3CBEbkC#xIfWd6m9@7$XAqK`c(28P!E>&q}8)UwZDOj|5?{(4vSr!);6Ws~A%d3aeW!kr~SkCUFdrE^S z2oeg8BOQPVI}DpN>k}ZpDh31sKtXjH%in2*PVTlWi1_^%pKf;naQ$q_43#13&jAc? zdP;K0DWL0Wf)=0DG{1g)PA^%1N;qbb^9m$xM4M0stOU^qg2KZ8gqXxW2=`PwcTBU? zEeJ97mH#wPj~jaS$35)Dr#2&ioy^-g8YXj^R=|DgyZ;3XUS(De#|OA@ zi+pppmq{m?vyhZ*0}}G{UPD|E)V*XV6>haQswy*yl*~|t6Yt>?0k2C2YY>4@b)jLQ z%e&(ygV642J2{n1x+AI&&yp89eb>PzTG$ZqG5_W>O*#9c#n7QX(A;QF@>me(MZ!Ps zuD*t868-fa)QOsuUU&i)5ny#4lM+IIXTJy2`0|$0Rzz@yOzy)CAZ0t4!9YM{^b!V! z;Tl;W0tXS**leV)6oStDdo+w|P(bA6pgn+Rj35`drB8eNAw48YQWi7a7J%a<)RVZ) z0WHgqGntUe5;6GKx9;D7{fI(kp9#x*ZahH1Lx_{3b)nyXJ8R=@zL>i|U{>6Q5gY5k zrsJ}{VZO>Ti=;$8WY0rcg8CIz^LC;rCFBZoJoZNr6*JH%X*W|XUL%bLY&s-_k|RV5 zc>ZfGH8mUi^_|4p3NuwyC zYLJo1M2c3U?Or}?I(WulvAH&>(C$2<+{h~@w;3|m-6SDqHR4&=uN%J5Jj5Z616Q^Q zn9Dn-Oi-IG#C}c!^=D>gsP;k*`ouDTBDBM5xuj_-pW%4>>2N+v~9xV-+O% z+hvaLB~UCOL%{~GgH(L_YS~K*%sByqLo7r`K=S^SZC_qndf$-k|98zp&#);7W5(cy zh3lLA@c&r`E6=`C^YpCA4#1X(fSyoX@lGZaJKn@CZN!g5w5urDA&^sYFEUPDG$(L1 zGLF}!IdE}u4v}1CRM3cvd{n8f3~LYZ2EufL7Z0-+T=3RdL-31I-Kn-j{6b_zgzC|w ztOP#3?Z37esa;H*;!i-ZjUIPQ1Ls^+Bu*9&aZ00HXa7GhEG5yBJ`^_GerpV4AtFG! zk(bEx(c`zvTgBlmo@w~I+&~)OlTvX1aU2wnkq{MC0h&TqewT|}q(bojyWpRL^N;k2 z81sfoR{-`?)C%i+Grkg8Ga9?=i;G_JB1Hy=*O0v*1lZ9Amz zp%)sm#;OBXtJ2w)g2~g3zut*-IH_amU)pL(YQVo*9No z6`U|pRvf(f@xdWIJq`3LovYZ{NTx6&OTgsmx8FPxXM&mV+M-LM%DTys+f3}9xa8>2 zpJum%BO`wYn@DkH>MCeAoF1O+g4TQl+r=;IOy14|Uw`waUbSkjc}$ytBy+{oMym6Y zn~;0O+AVuf0jq%&Z|IpS@o)odBx==L@zQh!Kj&9gRxa|9G|jzpM?gl#xGc0NG|Ai< zWEi134Fm1VtZi%{1K_(ar4x9+9!LP0L^LdWxlzb*$HmvrPPVnQ@bU2l(rG0nCN7G5 zr0eAe_*tk^)-=?=bcGtoba^Yv}Dn?E)q{Aa<&9 zzeYdusjLb*-skGg$y+H-k41^g7mZ}5etgdJqhPq`ua)ziEYAUvX+bqx(U-hgKeHm$ zwlr7ig}Y_WiGQ`-9`5chT2){Fgo}zfK&zUqzufYz1$#`w%fQ9W{Na@+FZ!#FeoP+_ zeWz{wc51>od~0ySR0xyp^`hTHhO`fIAtaMGo=H6+ue#Fql)=-lZmPxL5ncG_z0HbB zYzv-Y4AHi3No+}4g9j4zJhCnycwyD#mzfhA_92&B$!4nYTseOiS5eNBb7ulAUs@FT z(0fPw$#lwD_su4aR&rTZ`1@VBz4*mbx-pJkpF2CSVgK<% ze1;+dk)l_k#_3bSM4Ol&=uxjp{(7yI`&%TkQ>E@N|K^O0|5B^@yCn9jBL>P0xM)9JDP=N539qrwLxG_}0juu`Nu~g}xLss9T9)3DE zKJ#brt!^23^>Q1>y#M3z9b@}O=EdM^03hGwRQvYX{+e03Win3XfxDefuaf8FCM;vU zau0OB?)Zc(#FV)hJ~+ET69lzbvuao$^dpEnoO9@GAP^y-G;oo~2W#};`LMZ#WG*{a zeuNFLFf}C(b@m|OGE6R*5qnrH04~+_$0s?6=(?Kw-B2nL$P2%%iQfgPCPYXQ2(7%l z>&~iykr6feu*Vi8ER1MlcZeC<1+CP8qX3Tm>jw5)fGDjZ!bGHsj*#~FMB1<{TBFG? zkJHx7a4nEq^+^T439x+|-M%>*tw2e{+ zzJkF}dH=R=?SRWb!25llkMb_tV}X`3I2gHM!63);f%_vZA2y+1e@f`Eje=C*Ma``A zcXwRvCsPxICSW6sNhVpxYQQ~-N+Io0)GEaVMlf`_Xyp9Z6D(11hLK??Of&OMm@MUIJhkk8AhB8Lp(FgVIXA-g!C zdIYgs5LFW+1zf0@R%BIulH^!G@{kDyMnwbi&P{91Z?0b)Vmi6yrBf}Om06G@U?q-k z5TGKCg|f@P&jB77|GgB@lQ`V#c9UeH!evi3;Zo8SE#AbNS^K`e)R_Ux&cBE2|q1tWEU0<8=} zIgcKtOlS|r`D-@1e|?^`_`-562wi{K9*gWM1h;L1S!wkMcPe)0GMG*O$#_aiqUlu| zw?NwIXM|StsDO#U9cN-N#jrd zAhavEz%wBX{&A_P;YbhE+@%A}%-{u& zLT&*$r&tf=RQXO+K6nYjP;H`|#Ja_yLVp2E9i$0TSgmM#=upeTdQ0F)3VWps$($^& z65pMh>XzD+b(RX7KQbr?o*7Zo0x$yUPT{sA-n<5d$8g%o;&!?$_|4F(29D~(hXj%g zbs;)Ss`dPxGW*eCBToZuOVH8`08bPg=<_K$op!Sj{0OHtskt4MOR=obms1)O(?}P^ zrGagmPLDdmEwrw-QdPnOav+Q@qwPGqOCVz9gfh#K!;(xD{tSMxK$*~R6$Ff4>xgLp zn1rw_hIbN&+I2OYPA^OgiO5p1e9A;v?)hjzxs97YE{f04hivEc$yB4A&)}I^GYI;$ zGIDOqHCD#KsMO751Y8!43WJ?CLsWeU^G9bOZe3;?dQS#vHYb$+X=@%98H;(fTSQuW z=n|yJJP(sb!&6n)(^0>Zr2$gr@J__d!P@+Eb5M*$>77D}5Axq~`w`b;rKYc=6Y%kT z{_F3y+dY2W_#rC%#MMke^%~E$?ljL4)35VMsRy3D`makd;UbV6D%Ivw{ZFUVTSxEx zl6j9YbQ5{UemI}+&2u?t`ti4*lR$IxPGyt0%$E$!4_S%^bW}z|I(ImG?7ivp;AxNz z$TQZhTMsc^+}MggBC8(RSW(D~_0&rlmPP(gr(k!pSuW5e<9ir7Gw2~tlK9^I@@VFJ z{`xf#!<;MoHBxymJOIDd?}C5>fGA?;b^x$AdFqsU55_x_EW|@QS+iB?4|ICk(Zrr~O z@n#8w!jV}^Yd23>?x`w4+fTY_au)1`F?o4g0yHPBz6%T4!>csVCC zfQn+{t*jwT;lfASrE_(r%$g;JGGQf9LkY`4Y7H!h(hVb5U{e`bR3v zRlns>f6j`Dp~uDXQ#uR!*&X7Hw>yF58vFi1V`p7`eXi7}Eh`1p)k&?=8dUNcE>bu2 z$bbuIJP$`m%6ZvZW5*y0S`64>2yVm5Lk5X;d*6PS&7wMkup86LvxhYb?t|xW?NC}U z*e8BkO+1SX$Gm&|UKt>kSer2j}~57uIQ!MgPXT1*mu4dg9bEdkm{;7U=jwUf!-#?=Jm z`Zq_Ed*csv9-U>ZW%uE+i%m(<_Sbi+A1T5Fo%0{cC`tHPi*=k}bUn+O0euwa=KbHK z<8_dw_8&}T=_)8CGFz}?>(^~`tTxqQU~25Xuz>^Yj?0T(wY(q*XC4r(?%3-LsZ70M zV-mlaDtVd;lKXKXW8Vi3VED*DBCPO?Rj-I02MI}qUYkEWyol!8l#fOCsw#Y;E(peH zvXZ2)sF?K$FQE7cxyC_^ljM znC_lt<^roD>F|btdW=1WW|fywF%rS*sRIlkn1qqGi4Y;tlTKu+6Nx0Gvbcp2H2-b$ z+O?1F+yB=B1hj4lmkz~#3%HIrP)PManE{+>9ptc?rbq2jK$+J2%KiS!ykAX1Ha|C) z%+4mnA*KQDy8k;iN{yxoumaIeLQqUYfp&curk)lE0K|+~(hKBDUj2CtP>N)ZLtTPB zTujva-3f&8J|*xFGz^q@8!DKbYx_uk6xh`yt&mV^GF*hD3qfL`nNSWd4ZJ&0K_GL9 zhy7^`s6%pvA&nvQ0mYTmvlvUBDMS>fNPUHUCvbFhG}GxN>X?6@0`+NgTbt3w2roa> zD^O{YNiE0@--cgJrm%0jJkAG##AHU2g!AID`U_B@^TP+7!|TjxQHBsg#oJ%_hJpunZ`BY8vifd9R`nZ>aypk{3zkH2C+Cp({K(lboDg@F}oc~~h6Zd+gV>AEeZ1qXB%GVoUZ6y*OET;eFx?pa!l&S6pC^d-EACD01 zAsE6Jti8>fruMVv*6Uh1*_oYp^tY2#V^AARC%x)`Fw8(TomEwJY~}&pR5F>I%&$ZP zU?sK16(gfg&yKVseu?d;){Ojb&`YFD!}U=AeDIm6JQG~E!~su+s^f$k`%TmIM;D-< zYl3LL&tY{VNsGcDktQ-Z9H7<5#~rTRa5s+9a8hCpKCHc$ta$Me^#~GNmVoK0{%n zmnIc)#09l0F1lvkMHwNcLmCkGlkxT0fX8juXx9R+69|>?E7*)KSl(rZ#{;7}bApUy zGYt#DAzj0JO^Pk5A@Mc1q=AT|Ru!}wjAB_vJw^VCN3xqM!nZj;4@U$+z&41BREzWa zrnC^gMU>EB!@K5lH=ObHRV2p^*az(&K8u900~$A$@}pFNmV)Tz0fjb3$-hw*r$F8J zkHtdj_Tl=G^A*F!&`cvc6Fd^j)~!1w9X&a9H0Ft$8z9re2GfTsU@^Mxne0ZH%8qB) zV>K^TRWS8gZY=y&2NQ((l$9-JuTwS9zY@^8PFA2J)qgNMQWMg1rAKZyQk&M1(JTnV zC~$s9a-Rv7G52(%|HDhu(a7y#lnM^CMvNxDsm#i3XbyJ|fC`C2wuWYcqIaI)rC*O@ z5)4j{g!+3ZS5{O=o_WrWr=g`D-Z^wMxF2EzGv!_bf&Y#B?##RX0#l5fugq;dm;Czq z^Xw|RSpAPpbBY-D>59eMU`oilejiB$z>GUi+<_^o3{&)GAO^^$meC9Mw3Yz5peVpS zeIr-#Z{&HY>2u0)Vh3#Z6ql;tT3RBNYMuXysVm@T{Axelf@l5-Fv@T4=hd_YMz&}p z!V}zjd-$IDhTsp)EiGOC5O%2m1Vt96-3hAJH3?ziQ2Ebc)Xn}9d-fcyS=-5kVxnSv zMuj{dsBM<-(NstP7sxKO{HB5^vzRA^RDQUrEi$pj zKc?;bC*AhY>^{JuJvuC&tdJOXuv)6fmm=vJY$za?u~AJf4XMc(_#J@>9n%BR9yJ z)E>Ad7u}dcfDeOBm_3niwSe&790`8Ipgl}O3i>MINozHmojg(n)txq5M+I~@$BP~u zdUhl{2nMPxEWCT!EU6kKzmzn`nr_V9O>g{6h~^r7tnIFYmLJB{);9(Fdhd5lw{mV- z`?0T_Nh^0cLZxFPLv-9;=1Wqh6Ib`EhFrg9&+uWI-|vENh!?l5`HFN~<#N(1|3hE+ z_FIbkU3y@q+ND^!o zy5Ez!9VmL@;SQzml8s#{r*`t{wLD>?k=Y^67IEdi`+7P?wew$PwDr{9+6s`Ozxs-A8=8bLWx z1`czFjJga;&-EX!@wFQd#LTC2$ooB5VX5TaDN9E!Hh8qeeCE#1#{O$UTmAHA`IhIF zt;X#O2BL0-rN_iyVcoay&+9&CU)IRHCqMdh-taG7-B6veB!9#DfF}QkvaP$~gS9Bk zlU>Ak49gW%3R*=h6og*gO7*6FQ(xL(L(AeC8D3IT68NNl4;$sF*XxDi2d-IhN37BX zr#LwGGS6OO6K8#PngTqC?^LQ|+8mAL)=P1m!R zg<)AS{(Zz_`=z}dBeQxEm4P&$%YE+eF~L?L)8uSX)h87UO0a&-|=Tq ztBbBDW?y{ul_&Wz*9HMFw|{tIe? z4%`AfBkR20QdKM`NgFL!=8K#2*Jqet*d1o(BAB3)t@rrwT~l`eN;C{UE$J4D4~l+$ zxxuT9`YSzaBZy8mI@t`j-PX-YLiTi~bI=r=g(X;APnm>pefV6272gw?`;@O7NucOj^abt|KpP&z)DN*cCRT zTe>r$^ZNeoU;7OPdk0;Db5jF9h)-OY(vjX1_Ea?|_u~+oZUb{`)wc68l%5SsZ)I-S z86LXTov0@~4?@xhaPcFYPLbF#jRl7y)F z{$qLFtuZp?2C_d4s}~KO!R%vvD?vOn`r7|e->5|aWQ`=%$k+3>)S%1glJaiRM-xYV zgMULN71i>(j{+!}0R|wq3L=j6k>e2C1N5Ui@ONdOQsU4lg`J8{y;TfcyDf6_ev`Z| z=yw|flb65)k+3ZMM@6~!jcV(ea#Q@b1@{31pzny+oS%3xVx9mAj^AQ^zep#R94gqM-JNxFgCX4HUWkly;OmUDk1 zIWcITY4gqae^z+2MYx?tXK?zPNFy+)@-r_upWhi7zlMG3@~DSH~T&J;veqh4S6e`!JMw#w+ucve|8Qjr0t zxE`SIKU>v-;*r1KX=^lyAc^~RuBNmUM=H3UUEM!&)di~BD|_Ic7y z2oB`U1)7{zP^;CUMknBqUkNTYQ8+^xsXZb4U$1b-9KPO4hCE(b@zTt$MKItB$X|72 z5WEwlx%&=wF(T*L<~2Y`0JD?D$&5;Z)0`B<1xi%@U~7pz8oNA75xpCGX<;PHf)&m(0pS^Pv0q6sn~%K?Ch$}H&e zWCkUm4HT~k5Lh6_G~P2@KBq8O)D(%?$r9#~7z`va7$A|)fGa@a0avH}V^F$$?8j@i z@=f2r2}mzZKO*`#fq_dBaMj-js4(#^ke~)7W|S0UUbpMh_4`HV7f8lmo%ftQLxYO@ zcQ>IBiKJBx|l8 z$LmL;j~)sSx;3IAYa|m4G4Y}L(Wf}~yTLbsGG}~ z!M}JH^lBa96ER~PHKIu9TG6WpA^U|8w>`}`wMzk)xL3n)qtcIybPwN zGi2Gw?|PBSjRN_b1dS>a{h|-w2ixzKAQF~JZ5*wVNxxU+v zXO$pdk^`zfo;Po5JEZF(c<`=`wY8wXxwUXo?z)lE-#Xw#B7*6yG#+fzB3H$O32&nIggM)b=PMu~dKDXn` z%Ul>-0^fei_1R@`OWi&K0NYK=o?st9sz`XL+NzI3gE*PJ7Cg)j4MnYJ*E!`WprmB(*3X7@7f~!# zl^RZBC#*LZ4PyEkn6sH2x#_>u}Pw<|nU`_63Hb+kUUo<3$o zuJF%>Uw-w!t`(grXgmC+jqY4*Ju6+)lJQcn@{AJDX;i^MpF*sy53fW)C4Yw@n#OQq zfLUE&juQx{^O7OWwZr>WTl2f@)%c~T))=DkYNE)-nl80jZepiqPq@aFglmUPGB>*g z3G6wg8#w6>_#(aR_DcrW)wo`P^L-f9So;30+B!FuJG&Wc5!BKZHmcYoobn7L~BY}Y`x>xB}+|C}f zB%?OO<5MYy_G-)*ec)0uvl96I_z&&IDPFPNqDil{xmDD!?7Sr~|5%rcYKA++eY=Ow zF=;G`NH(tBfeeVNN(kTqAoU^oPo-MU&F{vHcJ)TL6kVSqR>~ln@C!Kz|NhA zT%9nRGX`IzVmX&HLbGJxtf7{8RTGK_54sZs~ zF)ma5^f`O`C(y8q^D;KV6HBJzbRSk{#Cs!q@p~fg7H0c<)tJz-Yr?L5P7%SbF2FTB zd&AN1qp=0&ANf8iVl!NWnX);yFD7nk+n zEC&4jrWPi96Li;l!U;pp82l_&rsisPiifO%Aj%9;zc!K&P^>%4PK=X%#)z4NS~j8aEXuo`5oEBahAq%Ko>|( zSMm0Kpz?c1a(x4`8du=`!h6TLc!<_80}2k(4?l&pt8{##0wNjm&n-}~HCc>`8j#38 zvio2@@?BBEQUs-vd4_nH#*g2RI5ye~gs2XIw8#?V^6mJM2UOY@;p2-74Sx~uC;p7- zd&m87mIMG$*f<9A)19)G#|-0G&k1|>T?Atm zfx@}~SQ>;QD&_MsQc=JWH)0$yfn(pykVreS4P&pO{i?(e{-`OEW9Lr@!*a5+8g~~* zvjgrTfFa3M=;l%RUI(Cd3)DaVw6XoeLqk8ijr0d2ca~(PZLQfL-!TVyrD^p!j(@&0 zhk|zRlta*|D+zH(nar_Xs{$K=tWMcc?G; zo&z$tN!BIviPT}0z5kI^1d__6))a#%ALNAvAz-1az)1|^Ra#s~w#9Y-=R$#LK6P@M zL;{P_>2$fcP{=&HKb(BrC#H&G| zSiAx|<}+xdiFgkwDDl0g{BQW%y*`ehQ460bX`+;!kppvuZHR;+K#F<%i31yUYRHR7 zW%vnso>aFnG)#SM%0qqzBXI_aN)q||g#(+zRiqItRR>)r5Vqf!CzPRaxOMrv7!hb| z<*<_258Ut#Ib8*^GWZ?8C*BVe{T;Y}$ftf{hBcXySK%kzbP$4C<;4MN-}#Sr(2G+t zMFXM1Z4udqfKH&vqV+k|)f|5@FtdqgauNek8R{> zXW7?XSEpD9v>BsajJo;J`H|@eNEBOy?am5G3jm?X+)oSXB$ZS7AqUoGVm?%yVZyB< zH2o($a2Ag5;D00ehGZ}JcwP&ajJS-*3^mukB$-J{$_gJy=5!(2A`nYk{LlJd8W~rHUFR=oGzrJ}NoyyOM+Zd0_!o@ZAyB1nMxB%{4(7Ky{ zcmw&?7cY#i&kMn%q>klyVDw#5YdSI!k1y(O$C<-NIx;$H*fyckdeE);AT4v;i$M4tA& zcl=698CoT?iZOt4KU%TQ_uCesxBGRyvc7lirYiUaC}J0@Vm7j|+1o0#xssqiW8>ua z{V*nl# z#_%xmr;Zde5;bNxpE0frWQ=;U`=H}Td$~NjgO9RAFSL!0%_dvx78}1+MhLd^6qDClO2CFZ<=BSuD@ksY+YB2f?((W`v%Oj0DP~=8?L360xkp<#jR z{V7*Me6}aNWT^iYBCVNF`CXqc%z)w9+Ruwa+Yj{^JwL9^X#HyU=A7DqEn+NN#+j6& z1t;t^61izcs{QMaB$jTC5kHnI9?-zs9LsH(_ez(3?`HP`Bg%_XzQwl>IeibdqIoW< zqmj{2ZL3aM+jsazq+y~6tFX?o?R=hV7D1-_GIv|;zBuxin$qXq{8t{g{jSbdJ2@{( zv+SZ9SXPA8Ylq^}BbnR)RbE~$dCQFXh}*oZP0u!qM3kJ^n9<$KD&KnRlJiuEb^rI@ zm(x@O>5J^GWHfjgH2F*cr zvn`?a&~j+zidaBJ<0lE4!IKB#lL}>~d}np!aEvPGV7Uo{oFy zKR5Mv|z<)3+$ zUszr~IK7Ln{+7V(r5bT}6LqgrDT{&9{^zBXYivm+_r)0KS!ac1*>gVR5oK5u>nYo>; zY8G}kb9Cgkul-P};4)pGy*uTc;R|_o`tHLL24z3?tc8Fo`on+=e>2iJO{d#}j zpT1q}OS89B^fnBtNebU8(J9KNSNfpDUQpAzJxXCBy*PK1)o$ydOFPZq$bbLjNqd>) z+@iwS>@(}SJ8JInv(R$gxKkqkBbE5GeX+JgJ&N*^CY`$_P@TQT>eWA;* zbF2m@9`J8DA{Y2JWg#wjsGdxkeH`t^p2A6gJ0dao%NqYkP!2jv=^Kv&$NKJ`plDR6 zgc0RW(T@@gr6`Ef2|#Ql%yRlRzr7S8s9?CUIkOUUV`p#g_qocffX<`938zNf zi@@tqe#wVrr>1Jn^Nv)re{!nLtf_$YfD8TacrW_fMX3hczZGgn4l(mf4;rM z-Ga2=v9a`i^=Gpoeg$M?VQSz}iu(V2b$1#RDWPjR|+l{xx~HPH5Cwp9W#uILBSbf}R2XF%?EC*(fHN-) zjvuGZ^y|4YuQ2;~QDK0~zZ3-8n{(1``Q?c0Beg!MCC&(*f3)_p6wW{tzOZf=rczg< zX}9!Wm4>TOq98sYJ)J3`JvViXgiw$&qQ>bnBj%dCJwsF>Sd}XpkR$=E!jpKRL(sQ= zbC290#$wqG;rDI;Uvd5Xnu-`yp=4elsjpyX$}%j(B>QX7Ubfdoijy!J zVmHM0377ydkyL4+p_?blIkTJ>m5ZHSbe@X>rx}U%i3LbUp4w$3Mw0Ph3#GtW0d&ovWPiKr1JH-G-awlD_QxvlF=)|*4yrG5Hz){y&wwM7B=pmv7`t&@5N?vsRYK_^kf z+m?~)vJKh21n#d@5h^y!<%b-PgsOvE1>fvov5#Lc&b}DAsHdUZMT1qfNr6c&dkejq zQkd;-lq~<|O2508!aK94&w>nWIsYL5w-s@nHCz9id3-v7?uF)^1a<~upF!SSX{zEX z`f<~8Yg*KW4ix&CbJ*-VANyRw27e@Zd`2+_C_g|;MgU?#-HLeX+mIs09`OsqJxJOV z-s#RONWmsPZ(x@ds}TPZ|10j*4$-j!ng`yz4<^wCUB&nB934|6!6y*(LWB8S5iKK` z>I+Vvsn^=vJUsIe=DJDPSSRi+&%bkL7)~s0XowG2?FxmS6_2ee@(3s{EW>9;x^uYC z>TFkCsSNKX*z423;zrJxR?N1;8zT=n$azkF2*iXCxPA>q{(@;G^QlF5?8qKVJmWSp z2Qq#L^9AUBUW>5>OARTH;Im?p=w2s?ld?6h0Ys5YyA?61+0znyQJVGt5%u2jT<-t> zxa^f|vO<(7D-{{pvyg;DRzpT*q+vyfl!R!=ilmg4h-@-iq|9WOLT0l4?$7i7er~_p z?fh|W=agPuuj{&=kH>vJc-c9V1Y-wjjzdFqOol&b5{Q*5ku5^g(udGdqNNi18TQf^ z(VIks1%!`{z80RjO56;ms~EHTN)RMy|Ka{F%KoB)o?=Y>khZ`MdjpuV#henJERZt; z`_>jVsGe2%bV%t+&jz^yN!z8_#AbB!IvgRhJg#6Zo5l@O4IP3{pxZ%I$FK(@qkA(b+%8b zZKV~%(3xLVDVu-93@W167=oInyqyL`^7jV4prUv(_h^c^J%|JcJ0`^5|xYm$wg%# z{K@QJs3PdG(Vl?9l?GjGmOX?P+WQdvI9tvUx}l%3VqF&OZcs7`>xa@CgY&rqvPB*^B7N+3okGqX-7B?RoSuL7cnu#l(ce(3Mhr@3lA?jTngevD6LCpgs|@F zj*kbW#1#!cP$}&(#A)YVM2GKyfU!MS0047ITSYI3N2k@*>Rc@=4y##9&b}9;6Z|DH zKCJ?%W?YiNkvED_9CwSaatQ?4jXYt;cl^*KAL1|HjEdvg#+irC8L<7+`K>8z5uLY@ z!Ef8A{R)pf2mkE%zS}pFbB_gB@+^(^&M${oR%=<$T68E_zli_0! z?3VR%*YPuyujTSt+gr!Ss1C4~EirC8@J2>>X87HFwTH+I_jb0%=-*r4IaB&ylc~7v z7%-%))a&({Q)Tr>L99zJzg5_Q{Wk_8g}bk*?^1BQnaN3c;Iklo&-!b9`mAF*d7D$c zOF9AgC%vT4V|V!_ply#3S~j{%Qy>gDKjVK#`b&bQ=SWC3B7Co%X=>YoABEF zPYO?DhP(2jdy)9)i1+>%o6U!{JjwetTMMh|K?{q=BRn`HNZb-)MGj^9M!CLI*@wG1 zr(7FYohIC-*O<5+9k;Gh1i99blL4M?pDi@i+R$+k4Ipg43j%o~Qj{Y5!G@5q58U@z zlMOQ}Tw9)7jIN!UD6M%W(K&`4~NQfzA@D+VKexEoROIAyrsYBjVU5DFO zY;5boii4k(CcE05cW~cM&#i8HKlqDjOms+0;w&(R#f6pIy-Ry{-n+LmGg|l0=>5p( z{g*~tdw&Muuz`DZB#3wVA&yhx2iv}V|7*U_#Hr+K+ixbp2@EjvkJzi> zINRxL7~la~B~r;lIfv!#&W+xg%Viy<7i2J1T~6O;hRqb)Fw%Y=4<+@0Ew#ORT!lz= zZf!!G=vEj1JiL+t5*8lM-N^BvbK%e({rz{v_9wdgi}gAjHL8=mGud1JiqoLVE;Vi~ zo3-XtP4CJ(S=;_S;E#tw%-8i5TQmkZZE89wd58QlE-p`p_5iCZJwta0XCZleZnW(D z{ymDMMF}l4oMf(h6F4@>zPf~`XMKY3_bsJ8#-~a3Afmw_Fg#G^Lfz8RLUe(dlpLE7 zL$CKgD|e<~5buldZ}Pya&Tv)g_eRiM!cGZ-XkdsrDk6d;=!&&_0|Ww;s~In$YGpIo zBG*R;nkcM4-$ z9dJUfTcU~m>nHA17nAk;&%rMi}^8zwo#PR$h{KQ{9d!iJmm8tmy47gqIy>+ zukGuztXSl#KUDPx;rG=5)F(e1$vEKUf)55#oG3_1{U{ztgeItiG>VqmO|0f9ZVj8h z|Ky1T8B|fN60b7fg5Z10W0z=9tDQQ=u~IHv8`}}ck^l6;{w*ZwA4hv1ecN*S0PZ8J zQlA~GK=2Rh$ROPpGvK}#uYd4ae>9;dG6a%Zi>Qv~U0fqJdfM9dxK-wnZ>fJrVf6wY zysoaU|E?a?ZyLV7%DiD3pc8RP!+?8s%mhM5|qc`)q}8Jl8OP|MmEFBT6`6Pf~y1V)bCctH|*AY{`6h+lma zFOgdJ*jy4~J{G`4AQ|`o_M`2thTP~dbQ`P#ujVw;oC?e+V|+c$JYQai?ohAMs?4mK z+<)P=24}K1L0r)Bfd06E4TR(pbpfwlydX0Z6yjxmCw8Hve6I4W8Urq`CaIpJ5 zF(GR9L3;X4^Gt7Bs{<`_Q_V?8C!`E-`+5At?Xt{2GmDU{m!30vxRZr^$K>E1GqrqT zwSO8B4eKc2)~BE#^C^)yvvF~Wclscr4m1`z2oagD(x-M^H^odqP$t{8#J+dlA&p2cBsx1O?ZFGNjv!qH4!rqxN~^HAy1M!a z?aycD{9)?VFW)G^$w(tAD$2TPQ|}Jvqzz#XnDF#W6tO{oep+C*pe+d}k4be+%{p(k zeEd*~Xzy57BL_#vv`?Pb%m1eV4Z+(p+3Ucfi~K!oD^Do5WUhw=3O7x_>m6DQqUVmmAUG68jJsi5WGG@(93Sux`#zIF(}~b3BChdOPoVq$xZqCNH1=fVq-q zr11^j#<-mEA8k>Sfm2)W2iws@*rf0|XVs6Mt!?G7fp9W;z4-C!c#fkrEHNjOZ?Iq9 zy6(so!;Bb?pTiT>5_b&wjDCIEnJRj2tUOXx#`0nU`xk~s!%UH7S>yAR$`ub}Qg6Fw zaTTN)2Bjyk87_(ZO){mPnCb3!{gl@`F?CTf({X99KJ(_jUPZb6?Tz*^ML9A#M=8tC z_1((-lOw$OPJX(KR+J`v$lI^d_0j!I>8fV4p*<(%o+ak+nx^hH9DX}gk(|ghVvrir zbMlq=!4JG;LJF$oPy(4|_X zBX&fZFYr5;v!RfDaEof)?Av8LAq9pjNyB#EidZWQ9f9hcU@um>kw5=uja*_|J{zBdi2j0 z1qE~NV?Mr`T=ktt?LYAc-jGh$QR(=r$t>l^bSm&6-RqG}0g@Z-ciIJp|EyDYzN!=G zP+WDzy>Rlv0qWmPj=jD7uIEx-_y<&ko5@DE=O*%IY0OH`pZRK9QCe}e*}gn9FXx|o zN-MVrGjHUe@FMr3mcPB4{$Ea?!xVPH+=lS*TPdZ=1k>}R=TI>~cjYyoT~T|EmQj>5 zmiix6S;bY>dUft&w4S|-n`vq4k5$&?GAuso)2G%AoH?EsTCd&`>l#dNS$kM5x>A-o z^UOuNhD$!Fl~Flmyw8QM^41Dprt`n<3KFZqxWsk$1Et27t}ZUeEv1w^hv*BLu{Dza zmH!4xW~Aq}tO0>+d?DNasmz}Zr0zAi>~zL5FqgE9#%BZa>AWO{AkVJu?$fp;f$GQ# zV&>5jF)}um9_)1F$i3{duZoJ`p4sMH^%jlc0K5|K*O-l~2Ck~~IDzto6EnZOwXH4e z?c2A6)=zB%~?1=T= z3x5QFHk~+9(cRUh$W=`1;V^}|gr@Q$XrMlez|IP4yy>ITMeHn#UMEguY+t1C77xwI z%DUb2)->q(EPRkn(n83Ez6#fJ^I9^bZF752sj6khFba_npvt(vFa8(bm-OcY@Tbnq z5{S^+PI2}#B~g6agx{YAt2m@v6WnLtmSZiSf*pAPTw6L*_S2_~h8gTIOQiiZ>XZ{7 z>Z%Cb2lT`sBLAxkGzX+K1rWaTmDi>pzLIR5JC?^%ZxsE{pNd$EAW$S<7y0@4Ccz0( z;ShSP8D{!BzT8<(c-+m|8E|kljvy@7&Xw{?H}`iPLktk<=D{xZdzVNJ+QtAoLJ$hj z=i0HHYnrK7T7M0_MEF)J0t!he)(hKGypZt^0XA`J;cfcSdSDO%Dx~RwWH?*1KLg7S zVp-u-ChOk4dr6#jAL*lTcYmJ!MZ*!(8<`yrypiV855P4h<*^VYphXf*s8x`UqWh7w z2DJR#&i!BcFv0&$7QAjpd1(R@Vnou#1sk_Hf?o&?d{!% zc?S9%b#HG4|0uh6Wf(`uXFrwfPg~ma-N}FOQ(5b;5Fkv%>A}TSXCU^;L~;!!g8h*) z(q*DmiBX316k(uA^ZJcYWb_P? z_Pf-GPTXkVdPz6a{afbDYZyeP$S4E6oVG@mxa5J+kwQQuY#l7`!ek2A$`OaOs9Bk?B38V5L?InZx1U>A_OLjDpuC>?>VD=>eSXZ$7DzX6)rq9fgUW8Q#nBT2D37g zG-0f6o_+7O`_ZFERH$Ok8rnnd0`gly)N_HX1|$znEe#?=AqBl}prL<833e&M!5iP} z`u>e>KbGKR#wJJJFpGRvqiVN2E3}S!Siibf>8#*b8_pJf<3o_ z-d-Q}>#0i2!-wl{>ejI3cIF+23l*d3dc_Y1pL9RCcaH(VL-)2Gb-wUl!FD}V1f_!K zB9M*mFJ2pO9xQ?^gnp~a1{KIB&`+deuKW@&0hAJx){}wH7&(xNgJDTLwk3IAASq1i zSp?kq&cN-{HF)22bZE#s!1WD}%O8V*6>!HW!o8n-`xc(cLEJrwR)=wo@t-kpGq&Z? zm25w^)i_c8=+SPP(-pk){*#x>=r?R2(#zAqm*fbGju+5Xi)R@(wyLNIKtqKEJv!8N8dbF03F}BLUEUXGD2_=7@qODb*?CtdtO?6wcD_Q37kB-I+;-ifV#A~SpK zC2HcDKOES-h3#VF1bxShQT9!r(uXcr{i@Y}#9EcXYKMPzFm0jDY;6<~suGRk4Wil0~=ljN%XTuXa2Tx=Y zP8w~L2>8Au)dv~8_|tGFY3u5GCT#^)4~5FCKXVOkdn~AEI0=_z!#ityd_3{I&Rba( zmBc~oA@bJ6&42W$-{H@!RC&g?=a?AY)~-KW(78zM1gZLw!J#EhC+5#Rq)9_~9`rVToo%7F9;}vV=7^H_OIZ>V*cTvxfI4+3bbkKR z+#kzot2TAws1!W!9^LLm$h*32k;CRP0ThDZff~ z_!2gmxi>L#Cz+0DJ}}4Di#RD#Is_2@dC*6mAI3>45rE6RSWb&0myNXDz#51^CgH(X z*3SWa&=CLz!1jIP4D8wjn;CP(JDV^>#_Eb1!teES+6SSoLRDBfavU)X;jvjf3kkelhui6oX&WGPKvutR`AKa8R=L-hnW zL-sFu(?_j4jh1Ps`J>YHeuBk7!3z~aK$czFLp77x1>k3&LMt9Ov4G(tTcum-q$#1toR4_dg(<2Lh z$5io^Gbr4{GIs1@KaJJ)_!Na@5SJvVc$RZ8Vhlj_q>DLi&w;!2+ws67RTaBoN4dYR z{_~F2f%b~kU4M!PS@1o0INt3X97ZED-rlmzSHaK{@C3;P^Pm$WU0=M`{d{=d_^0*n)u zxS1pjViuy?D;CdagtJ@FzXm9DKXONL8GFKuE8c%UF8_Z{Y1JT|7liFQ|MhT8o{uLH z%)tW7;DBQKJBBvPW4z7b;q>H|!=0w#`i3MgK;H&cpV(3sB1yqSAVQx{_mP~$4?s!; z=>opha*2ybx=0Zy3aF?VLn+~ECq2}5v(rU8B9+@^IA8k@=U`YDD{*zdqYdga>nf28JZ zBtJ5bu*-)F)6kzat^Rg?uVDT8jUy*<+F`89-1;EnI~-EW-0Aa_yV!()C$X}zRe7wN z>X}c^>F@7XI2KpSODlRD2JgiKcJ5m|5~@n-BZMVS3Ci!c%-+_#i(8Cap9QnW&3bV; zU$~X5pJyxzaC7Unvj^qu(s`Tg&eiU$5FQcHPk(c){PvQ8FT){w-mtd7Kc^vhJLvGB z=UA#N2lBn3)ZbyM8E=y+w>j(jBBiFt^54+na?_xkKKwdVr87w$q)=SCE2!7j-RGNI zt9~(3_fhKj&(i+TxNP!a zUG8u^;5|HxAG192RAsL5%yBNp_3;|gug}fV;K}E{&JgAHjCqaUk>7meSn9;0i&F~s zXQp*|J|SuTLFP_V8O+SulQfs)=-K?XWn{@z+ZkEgjn@lnGKpkl?o&T=ba+xa&zF*m zFUW47YWVnS@!fycDXuRnqB^f|39Re?=B!!4p()GpJMt1;}yVLcd~O;?pEG%EnwDC&9!+3(4u#h+s1=A>Vq(?9K9W{f?I>0-CnQeL%c8X zJEh}kxUci21PwSE8hn00r$f8g!Q#kDwJzvwgx&Txl2$f12ESxd*Mm4ml2pu$a?GY^ ze++u0`501#$;+irHOG1@Hlb%WCRYD5tse?e5Oc7C82Y+=5nckn*x+Aw$4QT>hmY7vnef9sm z1Y3j!UTLI;18W8Rn7N-qT94)M;12+wXI`1{j~+M#f)@^2TK@DpgMTOfKWHlOCBvOW zel!k)m`_MT`_kV22%%VTykU*SCnDJVsLu9HkDx~fw|EZ0nPDC4@L|H z940Sc6|*)J>;rsEGyYCEnCi$Uj8rN%(w0T}v3?jQ-M$sG0TEU0FkBNB&;HuhHh_2r z_OY#lqh17?yro2LZX6;{}gt@obpA<5pSHibro2NCQT zANoL|Q~~{VUeQHw_R%x1X~2SDAmH?0!-<@C`L7F#kQ>?E(v59*GGB#vc7XJ1e^}ob zn2eBhEcc`e0dyKWq^7ymH}>%>qdkFyyh4R9dT);_h@^|qkVDTVd3mq#leIC${i5NJ z3zK0B6A0V$p7-U+^s_*lE~3B%;jV`R53yiJCfGVLY0f4Aydo8pc79VdK(lCCB@O;2 z+#-rk9eeL{z5G_SjS^mi|rLraw)C9P#_SIVU*yA8q75RSE&7g#GH6ov20L{ zJhk%dJdxmlsf5@;9=0P^jesK<=^kloOR92~_J+vbPR0fUli7cT%-G6bt|~4lA5=uJ z6OsKu`ArXhZ@#hhF^N=p{C!EZJ5Co&zF7ZlPaS`c?fjKnXE3~jsZihj@Wk=+7E1k_ zh?KbBeLLGW7$gfJR=}0E(wtrs*1ScIeNtoo#ij5`CUi-Hgdux>(rZJ}$H4IMEDW<` z@&gewU_Lg`dL6%1klZBCWvh9%uPk8cCs_~?I8-9l@J`PqI0>wW5y4>#Ef z9hBfCk0f~E)Zg3iMT`7`LM zE$v|`=-fZb?pyXfg<3BqG6#FTt3!-4wV=$yL4+3Btdy!( zC(nC(ryv7wZddba9^wbL+_V3BvH*k6g=dEjl#k~0HOMljJ$mFIf6sXrd+JZH_Rz^0 zjlM;w4&DNwW=MeQOV23aarm^z#X}V`F2#gvyv_oH|eI8F%+_9t)YQ-MSzs z2zj<|-Jbv{DXGDWr-Ru9f?kBo1KEiA1PvfwCl_3qHm#hnTiC2fX&WZxvrC3@ZW-8H z%HzjjFHG`}{k^OTIR}J9+g1hwk(rQ=sNx++A9_^(h(PG7iDY{2#+&tnuxSp({TX6F%p9Z<)M=QVdd<)UPh7 zrV@%L7U_}YM6%t~f6JgVUi9?W{O)nw8bC8s$HhZPh#(sq8%6U6N3DUsfSi?Tk6v$S z(f|PkYEDs3%nfgTymV6@J-QkYDH{WS1ZM82luBA(zl9rSQ5iArs#~O60m}l~xIWi8 z?1_6FMw>f~5ulkdl3|z%!Wp=dgt)q?n?n!EyYyGL3^|_UrA3Pj)|sB|QA4M&u>%+9 zD>6(P-BESBENR}?kP!w^uy~mI5QLacotUEnZD7qWZ_n=r*;QuD9YCJJ%kF-#qeFU* zPrtJrY;3ccv!JE?ls% zRcucv1)m|BQe|N%w&BAEF2P55oBQEcTNRxV#QlMx0Dp_e#t;A7<)EiRT4eB`HpQ}Tz-ljx==vD~KNfkq1f-m#?g2ntF> zAW0gIAeUhhm2iFF7!u{~XgP=cASq^~!VO?fR-gV|Ns)8d$T5i2&~5U&^7!$Vih7AJ zlh?N0Ud5a72Auk+2i3yMyl{GY!v=!n1KO^Js5a{}2X-T@!?cmyJAxaJv)k6=eL{Mq zCUM4IV0u7raq0Iqyw{gMtu1eOQNHLBWmY>e^Zx`d@N@mu-Kj)6RDOI)mdR2fGY+xs z<5b$#gGsFh*7+NV@YnzN0W8MUMBtiodQ&OqP_v;(okdg{KRv0PLkI578TZ||g6Nls zf|^w5WWka6%*Xq#PMn~=0;^&_j{2c5x<660#qyty2VPN)PvT3xf;faUX^Hw3^ zHq))$ZCOz95Xt9wD?pV&3BB2mH#G^7p|?OyFXPxG3fkGrWh4Ihg5gsS!YqmFAkn%) z@)4k5YdMR_6MH0y5g?ajz4q34gFup@OG3^FP`l_wm9_?2|y|hzbFved)dK)Fn1Y@Z>=dHQzH4a1HRvx$S7)37L?bc zL+)n)VB1mptUdAXav4Mj)gZ!HaLA*z#>dw#P&3)Eelzu}InnbN{tu>_K58+Q;+-@S z;JehDk#LMLhUqe|@nGH8>xb8!?=Kc|h3b<8@2R z<+h%U;Ve}wob_g>7fI=b)4vvB%*<$G6BV7@6oJxdr2NSDQL>y|vL5c2K(SY*0O1mG*&5eWX#-k{7zR8Q!KvHpgdF6m33A0qnG5%6Sq zcsT2g+v7}w!%U+-C5PBa1Up^+MKVd&;}ql2(_m`L%=VYrHL^up#-}CrOX{v>m8S~ zaoupd#C&4Ee-B0p$aNN#vlgZclYBX--VSuMj*LgdUN7KW3^XZ92%Jx;5N5U1ILk=u zCMx=MZdHLU^~$$PoP0cK+F@zy#|m3EeKU2_P-m>|)|guWuOHkq!NXrRN$2*cx~^o7 zj;4McWnSfo2Bq+(WyRQ4S)bLCr*x8_@h?-EjfSrjtv(UJf?Or~Mbjy;d!PUJ!#zAL zy<0A^PFG$yxFIdj$@K2*CdX?I^z+vZg%2Ch%7w4qsQRlWp@`qqBP*f6GT<1RSaW!6 z?ccvQcgPCAT%Yo+%Ou3`39Isz*5nXwB|5o9(U9$tsq~60I?4Vtzu%n~p`HGxVRLcv zr^UxaeZjcjr*mtR8vcbEK4H=1TM-fJvb8xCT33|kyEE5zIHu0p_%WMXi_oOjgs+^B zqN=-ckOK^=;)&KPf7WG(RDyZ@+zXrlL|UU^A=+?)4X?aEV+6;Mv+sf>>Nh_9TQqF# z`|I;n>qomH_vH;WvS4mTMn+Q3-*w;3f8fOdYO(UdC>z|i06sN1Ji;8B+uGWm`)j&y zIqcf72XRE8iUS2a-7;ISwYQ`3h ze{u!ei~&o#jKNK*_GE#_yjftVyo$=QNm)<0DST+HHUVOx zlLuT4_WS?Dia&P9$kc<8xryA1D_5%xN`ZtRgy-~Oj=ZzGckSv2e9RLt-%rOfBT5-& z^Onl{4mC&^%q@4`4|lY*5Vul)#oCGt;xsm3Q3D77L>iK@mXc4)i|H6PA*G#1j5-0$ zX&&-+{PNaBxDC>0L71ieQxaf7+&+J@-!uaxA$m*uJr6!Lw+%b*)8q1aZf?$pIP*!) zl%7>IRjSfXWCxcyx;EmJ13WW7+iHS&N`qz!bOJo(rnKVTSor!1KB?8<+&2z&*&P() z@+?}`sI45c$an(iA0k{N{noIJ{g4>jAiQEB!HJ<*0}4PymOC#Ave1<;Pa6@R`F~0P z@584scxb^8oF4idoIRO|5PPmPymwtMcSN~-5^fbFkbvTO+1G2JYy!?|gW5a=rp~bL z@jtuXIbi^X<_}uB`fW9hCns?|FbTXRV-@L1fm8zj1qU*4Z>V@<1}ne>o;tStFjy!9Vz zXyD9`5C2^rJ3ASP^^**Z*OUeGy^?eu+aACBK_LB7K1<+ng18 zo)%Hq);hy`*8v;|=8ETaV=^6GHlPMbx_{%G1=>r2G^J(acZ`cPz8hgK&`I!kX6iZEz+H zLZ7MyoO?tS*ObWO15m~7vd)2*mgIB-=&+Z+r})*iBf@R;_wPxBGGVu&b3bqhEGzWS zq~{I`z9BOP=<>ByUrgoKB&l#^;rt>^MG*gK_sk+Nfe>cs6%*pj6JeG{;5xDZNDzSF zrkLbgDR}lte>HHjkqJNlQkf%L8jm$1n-f0`P8;E_01O4VH8x-lpeR|)f_DN1f8>Hn z%Hl_pHK6qC?8!ALYP7s2z zjj<$n^t#%_-^)`3#X>li2EKs8f$KfW(dr|!P|i#~=v{vNz^qBucEK3hXUGVypcZ)vXkmLRV&s7-gvU+-Y&1ZJVU*NkL#;)R-w>0MY8g^O39hHvUoYTHO*_k=J z_?|rbv?ZLmjEKaE71QM)2D4?ujl0RUy*J1JfgRFBZYun-%ldq>mTINFAqs z=hsk#>jLx+7F@-Q9~~3esEF*1@yJ zfc2a9XYb}r|JCKB9PRmO(eMuyaR;xi2vk&z{r%xndb;iffTNZ&!+Dx4{qq3u-SjWH zUy(+skoC4rn@7eK`_*MrsO#0$d8sN-Ty(r&0bK*+DjqvESvSvVzR)YQJ`gG|2dOR+ zL_YqbKX}{PyPUa2=C@1!-oNO$d<=KZ&8aTI&vlmF~NZQ)V z)-AOxf6q;STrNOv zidhO%$c`Y@Q{;YtBNp{eS_p=gPsdFsfWYG8yMk-*qmz=1wRl2Qm@&X3lS+pqsXuJ} zr498L@(aanzk(b`LYEF; z_vMP;A`=CCmsOyWomIBjjc~h4E!oXYfJ-D5guvlj&ZXj@7f|yne!$8D7!7tGrhrzw zIQ~OwN#xxl#Vm749Pp%Axl%?%e7VdEF19nhc)cP~{;_63^uG+8%|spPb8)l0JUdRd zLBCt;;VXvrGv(=1!(5Xfbu_2}&o@8FHLwlf2HxNrf>tN4?x3R&%6@=)JHTq;k{hWt zk5P-LK?`#g2C&9g;8oJzys>E$DNCxr2(@r4H75$o;zhqD1xXjYJP z69hpfQ$8(FKeXSapAoO7?KP_l{Afow zcN`=pDpS;$+H^v*fY$h-#QMLUz_~G!Tl|0SreGSTk#!pf{%oagMyC?i@QdSx?YT%r z0=A}}W$p+znPfH+SOy7YXEGZYKx|!wo}mx2$`rbwj_SgF;Zad<5A5j)0sn0{R|t4X zrfilSIZ}gHOn%-D0B8=6_aI?-AP~`detP-r(dQN<1L!|vJJM3{aiISvvDp79CqS^5 zaChv#QT)XY+7Qx2gZKmJI#F3tM|{ONFA|khw+}B1LfA#3TZAyY%K}R4!2wyOKQ=BD2&NFwl@RB=j)a zdK)(eC6eziBO_x{rGVwa|7ig(szB>ch6mCaj1{sa03vEkXS67DR0Fqi<8mIBDLXZ@6&EoU6zT<@i1%XNboA5+oFru=L;JlCpq9 z{-6olCQ>?yhy(&aExuP7AeDE-(uDAIQOPeOF1P z5lGN0Q7#-`V%em21ENU=grM2#zmctStW12$Ld{%b-w-WQ4C&fA{CpzJqrC(c5o8uH z#LN2jXrUwqc_8VUG!khoc}`C-z+jI#2R}Lis0XKr^^2fqIL*a(cmrzSMZ?4Qz%_pW z>39MMpvb93peoENVmthxqaeBlKD8hL(Z&l|1%W2&E7CpxoNmjke;>aWTEMvzq z!7~B7Z&4Kd~xh;rn@+cE!HdMOFUMfiUf?bW6{8^lD zbP#V>3B%2?JW)%)S18~mlRG?>c-QygX3~XLs-E%fS|_BJDDMKzMyexDosDP_*!Ew1 zA6@}UnvbFnZ^gro@0N6xps~oVX=ksvKhk3D$ukrc^~37Vd;-B~=r)QJFsqQn zGY$Wp5aM866owv8_}5;rFpy3WNf>afZB6{^9Ms;~bsNQRhY^ekTBwovm|b;`caVrJ zB+e3K+xu0FXx`^nAW2fVYtO;uU2LLz-oxuzN(WxmGyTBz4E?tDNew4DdP!-y6YEJlj0V7Gg+xlI5EwYJOWWH9%rHbPK>xj_tao3$QyBHip zqIBW%E##oA0(^N$dZ(TSfM8oo93 z>`A3u`tn8q-jIsWA@xgCOAlDCJ*uzQrROQ^8$)so zCwqd;?0bJo@xIB?Xj)%MTWWGF$5P>>@$Z#NnapNiFTWvn?tE@ajTA#_-|>Qo?2h{b zOq>*Z7)oWf)L8N^+m;S>JH}J2esYZVY+)!%=s6o0@>PBCk8}u)<{3q%qr48uB|Q96 zbBWZSPtJ8*uM;Ympb9wH)UhOfcS>|6{@GC8j^1*!at2kCzFYP+nGv2`va3lVpBmZd zhod5LeE!jfY-Qy|KU41dN(xNTm-4Vw01F2?rJ13@p`j4^3B|T>17tZ zm|Op9-{<%$uKukz)c$`AY-X=HE$hf~d{Aq7yLuTGTnnnjIOnJet#dF*QW}_2dP3dbqPQo>stT z*8aS?xaktzK8-iczQdKtyk>Ua@2T51l>c#RJu_X(WUwhXLQvd(YhXg;cYTUYlJljX zw7#~lN+?iX4R)#s6A1ejd&4NIdrH$kVSM^0(``Vleu<{ZmtEe34)k}N*&;HlaGNnS zmv^kdEkROvhj{^8_~FC!%PuBV45_>c>ffu9+D+RU%2|a%O9fli z0)J%544I7`u$g_~g3?Tc#U9CI2VDWXk=9!8moH-03HNS6pO1s^^UV{xADq~F?nQR? zSlcnrYU3u=Up}R%rRMtl#zfVC7w|D0Y@0`qLE;!I{lCo1#upN%mX>sgmEtJgvwOFv z=j{mOhzM-}vZEH1@~wmTw45K%)u<`PtDr8kSLd%=etl0Ks@Ll{9^YO5IPB@+e1Bix(b)|` zDiXeHhY`jzH2b>KE&fjQcbx3_#XI+4^v<;&yNkqb2#j>!thGf{zH;<7N z3gAY;GX2|JNzWPU1qmxq#f@EODFEiB0DdT0@A3=`cDmar;L4Wk_QC!AzzNx58tD0Dh8q)M$2`jKk1|PZBoO zrLYCVTf(6mpO8R@C0D{NrkwEG|Mic@d`=*7TRV!jg_8A$nDY^d-2eBo1G-$1 z^h6?DwrtTgR|hA1aq#AzNu+euz!6+_s)No0_dBtFKp)oVLXw9M+S-1&WFn03o)@z! z=xedoX{ev@+rC$VR!1TpjvTV<9zSuyz=DdBL~jjZPeWp~5{B&)^e|3{&v4`RBHQXk zEYMk{3yAUs?nmfI=QnRyN!9Af4}8Q`^YH@~HA2M`Kz^L`UEnF@hztEX`gZ`o2{?N3 zP>hk778{u9sV1o$dz3I~NKwO31P3i5gn0olh)?tYi6)U-KtGZTe}z_lUqa+)dd9WR zz@M!%_(q5#0}?NtqHON6bz78_Uaxh;PzoWSPIVnJpz8Fy<8T zsSxUGq$t^Nm-?q8Om)1v|Mirjunw!tNUHGaW074B@B}~Cd4{=6fseT3j{Gb^n;bCn z^pJ<|PX`339dMS~@&(OH1ASFjHHPVrBhCWF?6=sZQ{}u~+YnL#*s1`_H8;xY3;Z0_ zIx$yH;ji-C;?T*OKLy?6Z=n!9hZ({tk*1x@v&bn5~Ep zz!A!MvLD6-BS^gpa>oOw>DYwG*lunv zCh3hY8o3>mz*LBV7HTN&WLPy@j(2KeR)$D3Md^DmjIJ8TkBcF#N9Iy5UL@pmeO%m3 zLx)WE(@P6WkYrJkh>y$_Phss_5%S0a8ZIaU_4C-`qrFM~WfdX7Jk~Mh%y?IHzjD9& z;pCtnq+#tG8}2#M>UF?y0xFKc`FE+8d?(BnwX$3O7R%D{s}MAT52Ic>+M z0z$D1ky6@vsM5^yU-H?jJM6DgOyR}JGQkwzOP~$2g9X;lp?yBr zJWR^~5Fd#l{*vKXvITKkJP}Z54ez7(zLWa~ck9SEdWI+nv0e&nGx*T{fR|HXZC|az zWszJjdCH0}JSTRZ2#nykq&xB&GLhf9Cw5BvXnN*dR9mlX3(m*liWC>}6A`Dt{`Lgu=*zA?rZTIB|8sQo@%5U$E{QCUo)(i2y*`!6E2)l6$*_zxeujEMuca892i+LxUq9w02 z8@o17a6IrTcjcb13$C@S?!XowoOhhLM!#gnz*oy5Xfr^1yj{|`^+dOW>hyL>JZ_v3 zVvnjL8n&Wtm~^Q!TYvGy_FFREemZ0HXM?vNC?CBM5r1(7!|b&9xWZazh3l^@|08Bv zOgDWtj(OK&=?e&RE4P{Rt$C)vrIHkGlON^r)AJ4LXLdGDoJ`W)-N)i5J*R}<5=5L^FgI7A!k(0Oq2~ceVwcoVu*kz%MGi2K zuUQmX@Uj1oxD?*L{YI=nd`}px$^{&IY$9Dg7 z5=d2z+wMbq44(*}Y~*VC$?Y5n*cR%pEhb4ffR41fFljQsL68@iUC7Z>Dlm!cqzeg2>;_lc5JCb~xh$NHSYE@~WeYlQfJ=_!kjO#*0X`f9#}?DnJ2~{X`~E^q zbfd(FH!KJs5KvY*G{zVk><;inG~&(i?jo;&&mI2^j=WlU-vlEaFy7+X&ql!F6b*U# zVNM`9^h9ZlR6O9dN1)qK}_X&_~;7(DL=!loMNW{`16d^(w4r&zkc=5}2a6cmT zJdm{W0BZzZkH~h?ih%8ui#nvsudi|)GL5)^qu_;s+bD5pWmUcavL6SuR|XqWl$MdQ z(wlU1mrD&(VRd9fp8ai3Z&GfB_uQ-QRHn}sf&c7~+XEzPE1%Sh7H3SN*9ajBl0Wfv zGJ?mEI!3L&EZmJoAyh8KM+bHaX3y56zMmoPYnME&V5+R*Dab8~2Mn4hCTZD4{4 zHxu9?l8}iTo+4n|#wJ}57et(CEbyd#v<~y8$KS7(t#eaVvdjU?Q+njDVezcc(l$$* z<*uslt0(qgKnWTJ$>8lIOZ!J#RrP^XkbF)=FEpkGE$veE2fx1Oe!__lEeyMW9mnr1~D7V zc3&T5((Kx!A@TOA6(!6L!NhQY`7UPTDke!15>FCRfeh+^yCK3x;;jG-m=>=95*l7E z2vQtL%+_lglE_4o)G$7^*6?cr76fu!MH0a9v=QC|lE7lX#IkV0HL-@Y9fBwUK&jf| z$YzjZn*q!~E>N?h=vy!*i-SdXA$KqK80_;YodRXyW#hd%wz0Yc$KWke$BJt@Jk&k3 z)32bD8Q>1Q4B7Zt0QQ5{@Sg(*QXcysrCkZa`b!-s<7h4F2d;om!JMHq)kNhZGRuyn2* z;Ma#%-zqCWZh`%Du> z^PNu8#fA40{Sn(myf!{h5g8@KdFeM|shOz0wcdgE5$sJw%IKl%larXD4YQbHt^M3u}OvQ>%a%MeB zI4}%FpoD|r#d)RCX!TT6d(>eO?A||2wR}wJW@KayDME(DsP*UB((&B@@Qf_}AwrD2 z5b?@}G0D-p-Hn&WH{7v&*d!X9RbT1f|8=ieVY zkimPCgaFXmh2JYecQ@d6Gz^F15`yeSU;=S8onfmmBY^!H1lkA`@hLI8O8P92_0ng- zt2G88OEqpyz}Q)RBezcW+C}?dxeC*`N~@7-YPz~(`_HBsmCLguL3o#i-T3tH#3K)d z2OnMvNhm#!<+v7|$L=L`bl;av5Q<)&%@V`A-h6&gF>3KaE`Lf~mJG_{7lY4-GT@Dc z7zQJGWf2_Ha6)|u{h71yv_OMbG9qS9?4lGWBcg5hhT&#{X@TXM*6u}a=b27CO;<6> ze{~`6OIR%Q_@L#&?;aQ&#Nmp$kZt2e24s;Q*(iGB_5!lnkt#XJwf^{XYNxVjdIys?!SlfzG2SVOzIhvamYu=R3u3X;r&HCU zRt3rtjQ5~4inGDy)K3>!{h?-2m7P*C*<*>}ao61;TU=|rZDB52o~|&HyhHHoCOd$( zm5zDMSMMXH?R5H^dmLM0v)+wLzgzI2^~@2#=f<@D_Yrb06go^VPPUEy#g`691^>X( zOec83WYT+NoEbLwVw!*dA5Gr@&UO2~ZO_QasAPvCQ8Ef4J1Yq(TP0Z`BPnE5GE$@z z3aRXstc*mInF?h^N|_})z32V>{_k--$8$VGe0|1sU)On^LofGe_`@Am_nwEd`8=C( zhAsShu{?S}H@D`2+E++d_I;c!LQKGAk+*U8awv3{AJbBK?V33eev~ z3`cK^^z3q8o8QlVtGfDj$$?OFWjEK+!1$QR=O-4wyin`gIvG%Thr?dNWK2JFqvZ~f zC5xVg6#aJr4;6R?#V**M7W^wYc3F~!-Ke=yUA{PG$l7a2(P&P_z<)cjqPxppD0n%B zN>54T9_RgDoscw1)-}!N7TXgh!?cA4+458#e*QiY@O|fN8~w!s6D5%rR;Uq4&Qs{Ki`G7{zD6|Y zki{0^X2s%@hW7j8E_U~kuODg1u7*SK!|_u~w$@&lnK zs$RIxO?;m9m1eh~k7o_<@A!H_tyi8gXu!EXZ0`I>u^7EMgHTC9h^xl=ut^`=dYR_He#dve*!uiu;D5_4wIAgATcz1dq6yo4BN zkDG^zgmgIVptaqj8B#y^?8?UCebS?Ew&Wa5cBZQC6ujB+Wi2T~3oy#kaB(EJL-+gi_L`cu z;5d(SC2Gh|>i>1T$U9ra`%#fyspeYRzSi)(wz?*lXB0H=Jr3nva5$x&|MBoYwer7- zi|mp0FSb8=wLbj7T|VjL^gi%h;1N0A7@QH-s1i7@dvTf>)6Q9NHe!<{5E_Gn3=Q43 z-KxN};IZ7(W^3?0f6QlZA&i5hRW>4h215|MDE;LuU;s(v)qD2r5h|1j(!v}791i@U zpEoW7fJVngRHQLS;F8v48mHX;e>y)iHc3!_A^KIJI|$-6dRpieLziE{0-Ah9u+PA* zv|K7Q1gD(;2*->Nl45M~#`fRvP=ZrsLi@!V4()s45oVq3g*`7};v^V0aKs%fJv-jD z6~T6@63M-g)K)+I_&49THYX zSBJ2PZ3H|DZtWKf7xoVVScp0xyN9S>4Y^wrSw(TdUJ`~i5n?xtgbiRW?b>zB!3UZ& zCPFAi=R2;VK|j<652IReI%Q#Ede_e|XA)gLiJdd+*(!F~gh%))TO-2e9UKavXn`4U z%MQN>GeH2=ukhITQ-bXk4jM2PlIItuqKOy}Teif?^?Xy^}6UEwDWbom<#a|%q5FHKRYfo>{Q^TH$ z1-J!OFgL}r;jCof_ZfWRL*PaSTQ^%91BvTl8Hy~mRw@S7_j>w3BCAQpMdGUAt81a7T+WdMM zDZGR!Ohy$e1#1R8PT&ar$Jg;rB|F);~yC$dq2k!6*?#lGmsB;1zH_Db{4w=J7g>r{TDS%f`O3& z4ULx1o#1q{?Zgz=^nB*n+CX*SEX-!O@WuD@z`G#>vS9BKN1$ZL51K*>|5nq*dP?ik@$!B_%|b2vU^0!3U>Z zLgM00hNaT?6cZ8>Ao}-vBR0qtD$PmbNn z07Vze42%d)hQ{CrL3%_vBYEHm{L_t$v>}Z~_nDjZ_66W7P57$;!R|Za_=^W51Y`%^ z&Ph9V`0#;J0UAwz{2E!K(+KZ^mfVDCT?r*#Z$k;?;Gi4|IvlLV$S{dVkUJFX1C+13ZF52y9?Wr z%-fa*uZ40YX?4;JSqVYNP{9|aLqRgsa=#xpL$%k`Cg3`_@Y7xv^r3I`J)MM_oL?Dv zt{HJZQ0jQsxcviba#(M2a7rErpI#tSc)g=fo|)>4DJri5HnEdD)Mxk|4{kXByDdZD zs_%#7A8Pfo;`Qk}?{0g>f=@+#uIC$b>Z`n)ZApH0&LZ94?k7cE>p98ea?!}aJ6$B@ z=fTY(6((WA4cnv6ZOCQPT3LQi<0UN@=E3e$I;1;vT2*(;`3e_l|6hmKo0~`34HYrP z*b9kQNA0LpOESWJ0?6%b7dEqmnI1bh^t@-!=zut~ckkW1&tEy?-_=Rc)?Wv_0#AVz zXgq*|9@2h;)tzRkmFA4#3uEQ>vZ&em0!**%#H@=IoBN&RZ)t$jP@N;A5oPT^>#AYd4Z^7;$3QDDK;0c%6Zqbmx)fJfS#hhAi!p?pgJoM zF1pL=MXHKRZ(T|&PAfInjBb$hP2-3;dx}ZsLA~fc4vk#laqr=p`=DyZ!nmk6Fzif1 z$0ks}&$J;>wP@{O$o2C4(5>yiu~jRXNba77jd#ZhZ&_Xq79|RltTzMSeD>lv%k)*& z1kGjFF4OpLk_q}COu{S9XNj^2Y8D8aQA&Lcc3M}2VHxQ=2%=HF(=NR=EItpV{Ll%%Sd=qBadgzm4d)ptB#%>X z<2}Mm0e5$4k_Y%fZV5C!d#wK4>xo0jEQB?j#P7ML(tlQ%6c|{DZIuYpy)Xkv2?mrq zpA7dS^o2NY{qU<{o-Eeuv+cxMGgCT$t;%Z%p{BxWNohuktP>`aIWSc*D@nWF$ zdK*)LMQ8!{H8z)k%#s)P)teC4T8O;t-e>#&&K)La8p3QdQ?YKaN7XG_yT{iwOd=4$ z|5}kwJ%bmO;4dbYPyN4V1-4H4M<*NJ{a*F03({X%VU>QAN|n~ zMA%Eq?@~XTo@XRd)Dh&#JYtH>3x)qZVT&SMsdLAvBDPOu9!-ahRkZqfP(DB_B1<@& z75b{FBZ`FH`SWA`^sl8)2_{FS5^H?+u=jI8f=MK>_{qeY!C10hQdX87Kx`x7*O{Bs zQ9Or)m6)AiFIVp@huaTvO4*7VSVmYD%_2jdIJUw7kHip=cvTR%QORJ@(3y<{7@+!# zpx%+lOXN;{ffZ{5ik8+mgjRSkL6IV^VMeK{(eP>r=Dvc378qfkP@BLej2%NT1m1L) zL+G62(9o@C3J>i{hJF-a6)zlWNf~}$#5A}YWH*z^u(jl{z9B3G6;7uD3l-a`>@515R2b34Aa(G0u`a}-!%@*xHRh25rf z@cpJRnY}!hmfqMB(-r($o#mQ&l+<)ETp&ch2$Xo#X!CrVXr$x5UTXu81zWy)vQfiR zA3Ro*Z}&+L>7V2_<3H;E;_@;?Xc5Vu0AVbvW1}kpT8aG)>b&S+Cw$Amo5M==C9sRr zqsurVWU`Z61Xp}u;Sb8oHwMm6l;?O|Sq_!_oqIV9{59OxuE&;2c!=l`^k^mmPgXTx zeU*0@GxP9g4%IavVH4LLpSQa#I>{saHZ1XQ3%9*|+t#M7t{x&9EydZ6bPQi#-`ao| zu(HE40TLATBeR+#`oa>;C%``dI;*LvN#Xo1hLuk-Ztr4LwFo;$aZyo6zU}@p3Fk}W zh&a=XQ?Lw~II+TWTT=7yEmucxMAxw2$tUO&!deDP$IztE+IQF>Nf$ zMp3YrPp(cGbp5h2^^BjPb$~(3K5Uq!KGcz?K9!T%-l~4@=em=hog8Ow>lO9otGzu` zFENtm&}TYXMEj%i+B1ZaGRW?4m8oZ=KIT%quZ`2zSu9gtuf)@jgTFb@rg>A5OlZ`M z!H)>R&F6J)3$HWtbPCtGluB9RW>6;;&1Dd`FtOc`=4Y26t;e@3CeG_b)+rTyh@Fy9 zESixUtlZV2V);GKXC-P%Rs6t4`?|v9`eZHkD>CdZ24dac8M*v#F1+LMIk9|SrvCeO zpOY86WcIH2THUGqVNFCz(btj2fok4Y{oL0l(H%lZZhX_HXOw5^K5gsD8osjcx+J9o zrJL!3j|I)Ugv?6yAJs1}?2rkgZIawDa5okk!yC1+CTUx{*$j5upL(RtC~`e7!ZXh{ zZ}qD_7hisZ-;`9?yIC5FXYp6XAH9QqYWL6I%EV9Y@Pz-e8`cD9(}Ly2-I80!%mXx; z_?pU(_zBASQ#G9hT85$T;_k!o188W|%FBDRH#K3b%ghXCDg|%k!@@ib_SlvE{4&4M zH2YTe=Q<^tJ!SRER|x7mG^EH%vWxc^g!egSzu&a$kI_p8l>)|lUl*C1&LYbNX^4`b zf}(})?C2OevRV4JF78})8zYTMc#aUE*-b^O`0XJo_IgM%9!bAdSS7}pIrG3I6L{iCxKVgyA z!$*?=-2*H?ff{4}CZg+0LiWnIk@bL}J+a!Ps2N*()85{Mam;y(iSSH;2GqgqB*6zm zS64e~F1ABq(5_m^_btM7ljwLsHNrbgfgkaFX)GC5AD^c#YIuMaM?}D|eUY#etNpDE zG#2KKk)TECGsO@VI`W{1j~AGI%K<2q?&V*(L0Q**M&<~T=>lc$<(I<@J=5(l<(}v% zSZfS}I!@WK^gHTLvXPNd zBN91W%ka4>sYlS1UvPEJ@?NA5bUbZb0Ic`FW5GUpTwPD2EI31 zXNr{z9%`b!#$NNp8BISq=nTjXA=$k`Pky8UPc0FAbTL^`04%?ZT@55`#=n+utP(5p z7r35pgp-477?@)yVJQy2SUi)brl(W3Fr*itX1&|{o`$dKQ0{|U9f-Ala{4aqwol>; zXTd1bY<-Nj-n3S54iNUTbV%^yc|AStZHFmj&qUoRAkh8}x-)q!+O=0;sR278F}{a~ zPOiZG^+9Ils~ua{ckJ1G)$~ai2!^i59+;$|B6UfgGq&`sDneg?qXJ=|Fv{tdGm~Q2 zFS5jP^$`f;hs!p}!oT=-GJi_wxg161FQwq(8k(Gb}dI8s;!PnKLI2Z4a^*AO1;u zsYcb9a`2q8~Ez9P-lS5M{0t5NX#{pKCXo~!|P4{xa(1l0?wZb%f=KFU&d zt~-K?30au?*Mrn|{$Z<0KFOwovS=@$V|nRbR#^(C|Es9j6|~9~$EESwfRoeXDi7ak z;`U>dwOb;H8xN~Z_4z|Ag_iboDEL?qlg#^$9IQlAzr=P|T z1r~-pKRk5hdPEoF7~;{Hdhk3R-HGV|a2rM{to=0;xcHhLpr!$xb>orn&twXHA&k>o zxZ=DiVIL=Q*ND)Jgi4~fURK(y4lNxK zm&00NdJ#@X2Whn&Z0M`9-)c-z|L0-^d#A=U^~<8uZiqf0egY&piwsnh(${5HHOX*T zOTB&woi=P}+_Ho~uEPV|^UQ5?O2F7g7y^Gb^q-?AKUXzqFOkCGAk!bP04o)WKRZ+~ z9K)eF%HTal!i|6eo9@`w%X4L1fCBIKW-`MqV!l$=UOumZt8npm@*rVOQ1s<&aR_w; ztrDD)%cK|rmBw)oi4C!9B*-J$)T>q(apK^#iNqksib!+j_bJeP5X+#K(5ySNiM0PN z2}=0}(&o&M?EEj-=n@ww>hs1FgzP|a+F%5I_!oc5w?gRZ$jle9f@*r}=`A zqkh?WX^9yi(AH_Rs-Q#=%?yseLuO{(lQcSDoFK#EO9(x`8e;RlFF1{0*R2U301{9N zweEJgqOo%xa?uoE)`SzkATaR~qr19){vo~@aLq*I+imxw|KzXBXiyT5D)2iXBGW8Q zZTMJak#B=izKH7hm||on@!$nhKtxs5W?mJ!B|!T|KxiT98Xz;0+$n=(d6x6lFYa%R zV0rp>@!yR)umzbF`#*wsN~DFDs3_vP`GAs;Ukft`-a683r>@P3OC5i1@=GCX3O-Lb z=SezA7_`c`XKQm=`|Uf@)a71BT#wYx^y1V&Y8C@xCCHhZwm=gMat@K-6}hWnVAQ?? zio+A@e-TRsr^5EcSg>Jh+F#<4GG%CBFjv{;V?jm>43M~slP?@w%`^p#;ELhunq#by z&Wo>w!g2C_H6AA7WTlEi*G)O9KT8Qm9`)1|UlUjCY9T z6W9`;Vr+zBw+?;-mzPfVJhQ{IbLGd)>~4NJ6E@B1W`|r}*B|opTL7aT2slXy(z|v6 zz3cDW@XPGi0wClqGl5jKXlSZ`?8aS%yOzF%#WCv2&Gn%pTudITw|gY2rf4!v_3mWw zh8aNM{LX1=jVXFmX<#ivu@T)agiP>>k#4=jt<7t}w91&owvwx$9-*NsocLZ%*SKJh zO;{|aVJcrP#bbfzFNbIP`Woms<2ApG_UnBz6%5#LnbITdcxXZG_x=CNGL0<)R*+s-mx>QLI*B0I^1q=GO_UmaU`_kER^usR=}7=O{=$W zB>H4x>FYQjt$_}yw;Jm6Y_hicLa(dqg3I1VQ9O^OOqj52gSoCUAt1NagU6ai5m-?=3i#zhqRno*TWgNHG8SnJb;;t z#lugdN7a|M4l&y}8G-{H&d3hFNKE9m%>CeA{up=`78HH&trO~T1XRDUVEW(2if5bSU8NmPgai56RZj71YK+daGMQtj47||SdyH2df!GHzXM~y>Zv4LnS;h}>5isJOz z81lgT@{CZ3uJ1Zy^jbcCnkyJG^4|?1WoJu;Zg7lCT~icrLW9J( zj*L6_k@*a_gE5&h5N15c>q$+kB{&{l0znJMg6e7J91lWQf+4WP`hmQePY)GZphDUN zPVOOO`HOH20#9e~bc=26x*BMCsl9MHWQuiQw5>@VBfg zPtdwS4D(C3kcyiDnVf8eGRv?v#Fpgy$9w_IS6WM(3o9;w82EoF+08$XHd_<(;?68%0a(;Gq!$nl`F}1j#kFI1m`8q*SOju(0!}t@yA$WJ zMfCzNnIX}z1swiMdnb|@>~}ifs>%nclUNvIT4kvPlODt1OPmiB&(Bm^?w&@f1u>FH zqrD9=7US7) zN23cDUKi^nYjAG5numzpTf@uo9$lQH9}f6o>w+y2+$q2+gF{gSNWDT}+Mg`GV}nuj z*)-HTXq#x!v{6tXaFz_6v&S zzIn54?b<;${cWE#Og+2Def~$_DVeGC zwlOZL^D2ydb*{0r?7pv8&d6=T@wq(Epsr*&bIT!w;p?b1<2&KMFL!P82-k+W4Mf9&D&EHTiHBsASUK zpz3tNbo>R0SQY zGp#%6RDMpEmEScmCsYkdDksv3z{ zYKS*JA-+Tq5kh>vKVByQSt_W!j(BsRnvvs(VbE)sJBC;`kia)!yGL2ZSn}Ry4Q3vf zfyFcjVrC|&zXQIJd+Lg$=xMo4$6>p`t9n84gpC6wEIG6Y(fhHU6-Hw;RwSVdDQ}gn zS7hLI9YgwNFcVl?#^7fo3$Pij6cdKxIsTXt5-*P}4=p~okhd3Y-*+T`HXO0A4o5@# ze+ghd={NXVMw3eOr$@2ALfRWCImC7TEU2gMN4b1fZ-(J2Kc%Y7daF`@9e|yQ3=PTet^cV=twG& zn_<>^_YWLB(vBt`bH9lv~);Sz(}c?JH^a z2uY|qh!inG1pzb^!t`gNhmEeMql1X@iLo%{%DTo^uZEv}awp^p_z+8O6aC~WPw43Q zO&js7z%q8LHJ>sFN{(=-|Bo;MgsbJQvGnoh@YIa@w^H-(0EpT+XbG!>dRQZ5 zm5&5lVinN@6&&>saX5~^`*es{Y1D>LZ(Y%acMx(2v6lk><4MN96Q>+OhmnwyBI^OL zp4MY?66I3avxiFxHXiHv6xncxc=umaCQdiR%>>;=rNhsNSRBp>1k^A$Pn-yXE{)7Q zxENHb>o6(|!#kx3=k|8O!xB4h>dqf^6zh3FIV4`0xOY3gQT2}CxomHkEw3d5BEt_aT`R~o4?SCa*s z)*@8m4j%Oel1Qw9@LIv@JF|cJ#3z1K1pbSQ_QVB|0!{}pa0ucdp?n|cWZ^6_H_^)p zicq?`5|MwAFC@R3c?6+Cg9$VPFt={p2jWhW64OXrd=azL`~osq;tx{@z2Zb)=^MAH z6_H4!8xIXf3N)frY~AFexrPQ17+E7^0{k3R+cJIyuj5zdg==lRb%R=RfdVoz@F!?Q z=<3+Ve#*PALtR~Mcd;gqf?W}io4u!<3zv7aqSvR633lHWrE>eV|oWe$^C24OI`=Vl|l`aUm{7_CP!6c z9*-jXJSU>#80p`DXX3&0@t$R~QTtSfH|Mna#$-D}CijPGvk=quI$jwHEN16Sgsb+} zXKi%eydKgXtmz$)qegU~WXFxWu=~4P_+B!v2hAOd)exb0dKN4r%2F6pk@PbnUE%+D z4EPbDRO2=3YA1>9EzDoXbTEu_OIZd(2TY-ccw*uugm<*1?A2H@Og-U41hZ@7J??~| z54iTpkodVfttW{Z0g%I^#BjpoB0EQ3hoFX|hXc{v63gVI*0JDa9_M}Q)(NQITQ<^P zYGFV40m#YcU#dlc4?nMPz?O>`?BJT(9mWvy{x5YRMgLGQ4m;e;lB-PPzTCzzsF3xGOb}dR^yR4gvW^PMOK?c>PQmk^g$S zZBj%5fm znqTgmN~uGet*($Wm50;tbKoiK&3P>o$s_pdG50Ml z-td3=TiTkfiY%pUlFuUakh0|T97yBXG$_DbLSS-OV~$5K#BJc&`@+W9l2GHmP?`JSiFvW{$3i(aAU6DeGggO)8ifN;8pg zUKYQURGz<3%$~4zUErs*8ms671{6iG{TvYS0s!x!_)6Z%i(Z03glB^PjQt|_KUlnl znl_7EmtWjm%gu1lC`;#~SfNt0%k8kW3#N9{j^QH4ZnT><>N;=!iTL&@tNccV#X8MT zuT_n~<2opJB))9M;+Wy__n24PsU*%k`Nab~y|ncG&r@uYwrO|Q>;#=?bmnb6*sn)7 z4Kk{U-OYdWOKtpw_;ZCHlQ9(bbQ%v5`e{PhmtH;VJd_;eIr8Hz^>vwzdHcbXPxAi? zxjLb?q5+9qmJKhem7vjkN8;LWoRFNGboOd{8al7VDJix(y_WS>?ncX-J#_?2q-$+TnqeBY`5})E;O$1oNiHf z8%3{A@4J|q?-LP7W-gd{o2WYe-l%zQmq6Hik!plb12g2FH4d2yrMh9or| z(uHBUNw$zr9Q`DY?4RLwjVaszcigzBe5L;T1^<1iROdW|Ygj(qopk@1CenH6f=OQN ztIu>*T50Oh8pAhUtEAAZ6b!u%_Ia3n3``v8LDV*P_v-op>(>2x1{AQ2!g~tapJVnW>1n|7dK+{=1XoO5`Y^0Y}BpI$) zOQEav1a}*V)Opu349FxH`dpc0^P9yx{C5#OprXYI{d zJ^&z}m;E3Ey(RDlZwnW3Vwl^5SS}30Hl1gRd;jAi{|k&)7+wC0j|JP)hxO^VsIu|3 za4KNELw!c~7N6+-Z1ZfAC!qZ6wTKy))Q53gTYrf5AmQcyQ6kmNtp}rQQQR%WmaIZz z+z?Dj?KDpKfd#&kR$x~gx{Gk|s|k`8V82x?B^SEE_c6z|hM)K&?1|#sQ{G{F=;@`N z8Y~M5?U{H@7L`x?PWFY91rYM*vs5MPpY_zdT8EEZP= z{~6t}N!$FBjJiaK3M?-^>M6(-uxqFV*-c`*Nno~$D0>T8i-SImUG-$*u81m;fDhn_ z%`VjZ+X7NWdU-!cbi}j-qfl9(YVsooOtQpZ1W)ah-z`3SIjk{YN=Qje*^~spLV;U! z3`u->_3D-S{@!@yb5NeahwMK^ExP31czaW{0rG^OroIUa0Ct3CpF>W?MYiD<`BLu0M4*bBqT5r_=>C^3TN9r5hb(u z(S)E%aq&;=J(yp^5{JL{7FK5Gh}|fOneQlsAcRm4W#Eq2tEzNiQHHD}Z{51pQC(=9 zs!IGG<5E+1E=?)7JL2XLAq5;m+9P#*??KZC>p`+61zgp#oGk#nlrRH{?@aIOywQC^ zLW)I@L=%&hKO!8C#}SrjT{$A$tba#37cx+DykrK{Wvt2 za(8zB6mL?k!Q_g^(J#u?@h{hfKSZ014*qG*)oNJP3N}h(WWo|xtabL4`EkLZ3$tIe z0C>>9vPcj;I^ADc=IY=N_y#1}2iPj81yc41)VzYvBUH9xaH{~hK5cnkd49FiHYXD)$%O~ws4)iw_?CxeN9 z;aS^d`x@4!r0wp=e=^?kM;ZsW|1ZaX-0wO=rOiXJ4;8Ljhnw*y`P9&KGOTXGl~a@d?)GOyq9{I|wUUlGdY1>*X=i z;}~lXS}UPOk4d26B4I_ilo-K^jN3;PSrJWLn;xrrO++W=xUblm+G|24`gJHpT1SS;6>FL+`>4OA}GEtkl8%= zR&ku}SBgI;YZi_Ku(y1V^HFvZ+_QBGGnDiT>P2QK>M1wRognIZb3(p#Naz6+qn9TmLq9ouVx++ zoN1hXz1iw7<-zq&LIve{ELqaxE}CxD=rLCQcV^A#*V>C+)pktRX*AD>{4sb~SJvy= zZN_5|S5XZ@M-$Sh{&H+0nF<&>d9c6sJZSd$^XKb?qn9l2Ru3A$u(uXV$~RJT9z1<| z&E+m8^q?ugQbF8&px`VoOve(C53~$yja2`t-oiKrrs&r2jcIN{^anjR>|VIBr->;q zqaFsK9)6yVXHFiR+y+gtsoJgNER7T+>H1?;l*1!ArTWh9P9I@~&CJKDQ)VXZ&%DbB zV1HuKA1()tbXa}=ws7V5N=n!F$`CL7 ztx4W9#3pVQ4vZ9k!p|c+Y8Ef!L9rED_;hZcXgl%-4MKN|2qz_V#1LJ_!?$9o;Vc=CF6j$cEo5B z0eu0-Jq%G>({PT7r|valz)BL6ntRo(HpwA706g;hLoOE)I1$JG`#bnAv#kg7u6Fv3 zWtzgx90xa3U>!(_d~Yqlv-j5z$WYn#b7ZIuXrcE-H}-t;*(cGUd8~+t1GX|erQ_94 zgajuIEzeIzfiJ&&XD5N=a2gv0MOuRMbLsC-TO9N+Y%_cCdHXL6z1{>f87()r-1hdv z|8YK0NPu+ILyfM{cvgiCC?G*G!Txl!XuZt!2GalMffLDpo&oUa&-svZ+oN9nx^1T_gV_uDp-srP zx&|x^^SUp{=V4G1!dZiOjf1FP`^`6uTH)kdUZUSaHu~i{J2tnEb~0&~{;d%{4SeV* zQ@)W9E65bLph+O2f$Q2La4P-28?!q&kZ>0`puqSb@y3Nl?b&Nj%8363+C5_P&K*Sy z0Rn_jbVrdj_G1C@IngBS#i!mvVgu+h4UVxY5yYuMp3veoK{p45gIF~GXp+NcBG$pE zdY7RvnzRKW5>%@>fQ7_B*IDGm%B?@*T$K$*!N`YSgmuVum<=2{59I28VAudwEiis? zMEgs~N>F)#g+nw5gj@C^3ma(=dceCROIbLtDA?+F4D2+C6U+vMk$0geMpFUWYl z>wkGT08|hH{Qb!2gZvVF;}Kv!vrj%qMLm7G9W5w5YRE%xacB=pNcez6xz%T5;E%_Dh%LCbR?@I>oVdnn99w}DufeONcn==5>|n|?fikGUOLm>fv{p z_jX3%tpiBPwq=W9Q)RKe46h7+5!~oEyOgm<1$u=ADV`mg1iD?PFwf{2s;&_RG#iG*eD4(e0*-gveTMPE3j zj<_Rua`1};Gh!rRN$=$Ktc24FNLo*mR}#7t?%ustRi(WAj}Y2+F<)ULrY`OV_WqG@ z+iupbC?=AjQllbwhv0A2G$9(D+@c8xx^W3TnG0-_19}}zdwct`5Asx7lA2^qqOax8 zU-EJxVIH?NU4@FpP9_=^jh_#d`5FTiaY4}-m+{Cz4LS7wy04-zRg}kY)pm&E&_jP&z1_F zTwgxR?&5x#1#}UBKoxiXUeLlDL?8ou)0LY7JDes6mTj@afW_Mt?Amg06bU- z=v+s{Cbn}_ZjLfQVtCg@ys6L7l{cPGlbpA4s$LwCXIlphQenT9$BNChQ-OgJTa`C0 z?%8hngJa)gw!W`&miCM5zqAbPfXU*g%*30Ynwl4+bX(--E8heiKk(3ekGi0#;vK0R z3W=`@8yQCBr5~8ITx@&RK6chAT>V2>V`%HYrsXcZe&as-PtXPP-;6y->!w?Mn=f?R zy~Vvd!^Bk+eTVmP?BDJ+!&FeIuX#e=Y#XbBUo&hn3Hf>>LQ7 zsaY#F;>9JXF?9EYJ^?BqaeXGKq}Yir|NRV;YP;6Edls>ncx-b(8`+zTBrObT9ubg0@ z%s4^6D`le3&YH1d-tkFL={}JR8XfKsX@_m!-o@}Ko9~#M_E7mdAP|zVJB+tb{xa>Q z0SgI5-$=vCeIon!4}QBB?SNZE$Sg8I0Z8|NH5ZzF%KgV+AB7&3jCr5}kvvmFE7RXl z(}Mx^5b8lJ(4piYzMCXI9rcc+u$=Ng%#yJ$n6I{4Da_}+w(bYH%?~El@4YO3<#e{C zmO8IBv&V+*Qa#n-N4`+pu1UP1^_nRLjv(~LE$hGNa~{ttS*Y%k^%$s1TXx(AJ7{nD zmO5uS;gNr*XzkK-dJTg|6p8u_G~6caF$+Kd1sMeYal0nRre5l&Nk@zJJ(S$@aUism z&PUv5q5N!l{b1?O-1=``FHdY!j9}O1y&nY@re8J%`xh~or+&hOafP^T5kE#krGfh> zny9;H2&Dtmnagx6^)Q+0fZF57&u?%DEVY^i@z5U5{c@Ac$yQM%n_KEH9~V@Ucv#u7 zQJeQRXMJe80KI!WqP4Qp5 zkUuJ1U0NlQA)~K1{D$d8t3uWAN(A%(|HXBX09lYp5%55`H*kDc$5{zL=F0Qu&wXp& z{rn?$kZQAaK9(Eo>>>F06mCYp;Wxmma$XJ=!{87Jq&@A%O~o<>P<<6K6qx7_HC_Q=g)cO}L;)>p=^Y_O-7;cntwR1)vkH0xQa-%J0ap zPZkvPvi(1SdFwS`2|Z;V0z{ty+yKN9oNHi>Py-Bdchqf6NE!h+L&9KMlNIRsWQN?X ztsAB-Ic)^MsqgDYVZndE7)j26yvM-xhzL3gx23(n!$8a#kw00#q-J}4;YbA!@an+j1o)PqhIs4tV(kx;}2kfBip zPr>It;~eYld@u5|Ri?gfuL|2IM7syM=m@Oy8uIM&L< z!`LhHD=tSz&816LA-b;8v&yl6JPD+SIV|nMlj7Q6DF8FD-4))x{pptKiE1S3EFzBu zDX)3KoGuCq3L>b6xHLr}N+=SDYu!P#IK;byZmyt;mM#AhK~L}<2X?5Rlh`7o!Ii;= z96g_XFIsJ2RPdkWFFLI9R&fvN`kqTlF(h*XoulUTRy0n;NtPJcf&EB39$iUy4FFVH zdm}{NpON5;W+tm`#Od&fjnmi*7lMG;89w||7>pncmrxcTuhm-BqFeOFDd) z&S^(vs)Fc1h$UUUxYS(aGlP?$|FfiM`YZFux20#x_ed=Owu)XEysLvjnLXd`6Bmz< z#P?v^d)GxK^WE_fMuA-|1hx3D0kn>U^FeTLy=H_SL4ZmY#&^5#MLUa*>ydCMLbZhd zwR-l6pX)Juk)aila>QUcdfUw!#EZ4Oc{5AWoWX9`#RXgd915~YOFLH`uD+o*-y6W( zLHFbvGU7?Rp{0y$zsIq!KMSx;pPvC(IPCW@9;?8!nI_YX8#ETByow)B)AlPQ0ky=> z^#>kH6iEb?LCGh{!i_OoqOimkR#hGNwx_X;*<+CcR7X5cFog_(n+Q>-0fP25_H zXD7HsQ&9#$DUvZk1EH7}$Ul2BaY&)1(a%bnkG_tS$+XaO7Q|Bxg5->E^|PiD8?bH0 zgyNzwsM{}( z>?`{R#_wux178~BFT4goTShQn#nuyaeO+5`oc~%!arR(b^0f5rC^|iMD-R-s+R8 zxOmnZg%c^?;JXt_^06>OGkg@A(w<4&$@v1bIZWJ|A0<1Jm?WdGZnLSMSy#RWG!XhF z0M|f>C$~SlG599r+mi3fNJ1zpCm-~_FmlNn47mLLyEDWIRQct(K1~Y6M0O3N_R0pFVpDYK)g@uMU71jJgm^Ifd`Uu85gOx)y?{ zpsDeWt@3SkpTnTH!6M1oPWLg$$)K(=lNTK%cet_^z?tDQQmKfwJpJdGE~estMS_#C zgsS@0B1yu{^A$NW>+ICvW`O4dV^w|K<=*509|;*$X>k-!6Rz&Yi*kR}q# zlzoOp%}yG)g#D7?)7Wp}0`^Vr!PAFrPxsRgU0oUnOX5{gxKJXPE4FGrkBusqiHon9 z_&0*s7*0+RrAPnCutHtpn=j@gV+F7Gl%MMgP9Vr;yF+{KcRo4vzFtq@ZQDG<;aJ_al1Cs(=ABi+ux>Bq)DBXq=BRl;_;<<=mlgJc52Jp<+R6XNlPqFQ045U{ zr7)u(COR%iRz-O-V5o!UbLO*m`!INP)??FJ9L76sU+{sLlGtxXLRHCI4=L@=TxwO| z2LAykD+{+*tx?Aa8I9a7qU8LOeGlt1LKDJl8?}6fvX(d@ul{a#1j)$ETc|rpFZztm zE=kx$C%1BYg^s|*;_OFjKeDsHj;QRiO2MUrpMV|FmM6nO+QAqoJ%r}30ad@pDDF4hg;)ttM%-zE104N$KP&a(CgQ(} z{Vxp#s08)6D1?RuPe>{@TW_H91P-6UQnXXfK(j-zUX(pUcV(1c_G@H~w-8Kxz)-9I z@jm;+(otw7_%E%V#qpZJf1H?+;xiMo*!T}(a4W;IO!?pHZ$*R>5=uQ7kz^p9SjtT8E7emb;w9 zC(^4#3*SbH}2^bj#E0lc`~ZG($93~8Xox{NM29U=ov0K}|?%RJJsACAa<6cTID17#$ zMss#{^ffyL@duN=v~~f!wRhjb-rmzVaDdnYVC9Akt{!*Rf|A%A67Ol^ z6OGyGVYGf?HNN%+TvZU4k`DBVLI&Rk9edQS=MF&x9zs*=I#9I{`(muNNj;Xu+fPxm z?-t+@mubC5UxegQ1VhME`ewLwBWDQ+tPpq$0FX>ux2tCQYPmWryF)`m3%tJ>QIH%6 zQe!%=NIR4WDk}0KdKyLJUekr(nof89fCr520%{N5Sd+X>W#!NAaj(doiCs6`9A>Pq zuYvb$v)Fa~CW8MfxFfS-pyv?|U0fVnqjx3-*a=|hVtuck@dD53#nPK12KSRbD!~O@{gZY^1Vp_qyXnIj^f15?@SCv| zPbg}zb8*1TO5)o}OrY`xB7+8@=NLH7-_AM{6Ap+USt$bfRc|&w&dX~Ai~B0p+O%Hf zd_b5)Kv8u+HBe_^9JnGQsYM+=wUzkyfuK!Hm2m%S4r+YbjM){%D84FE<`|5WF%Zr? zH>mGc+TDn60*H;KWA8!PFSP;aoMIkIP#`w5A+Wg*fc%MX7R4+kkGjcosd#S%f(oudIV+rDC+=n^SPL zetr4!r#&>@7)d!$H|@X5 zK4yAF*PrQGZj72(nMvxEKQ}vsMJ5^5_@qR>tvlS<#yDRrqQ^aW`l|BF#GlbGlO*Hb zd?kGpf+VMJ{>lI;>MSHut2i0J;E4pi8kK*%uJ!#n3DNRDqh54)wQ-?}()y$*+SPdv zovM3Uw6uxxN#WQh$)dQu9ff~rX5JH)y5RGoh8UnO&#+!$dA ztRt{CL$4>YI@Qc7hfSkKW-0+)_=ZwR%`M>QjCY%qQJ_l42S?91EAqA&Q*pJJe~w9t!+ zK`_{t9uA>Es~|t=mhL@$PaP{Y#0C9Buss!8B?848Zf)61S8(p+ev-z5K{~wG_0d~M zl^Wp5L_!4iTE~(Q97wCx(Q~pGfJ;jp8HZ7)jn3#+ockGpeo=VGj@Bh320;os%0^iV z50P5M^TBylyi0Ant;=)oZ`{(VnGTE$cj6Tggufi7fDu zAI~o=?ZSA4_3L=V8TsTxmX=r0DIGc{wQc1PWYPHN2hW}*HVT(mN@3=cJC?X%@z2k} zn*BHq@;5Rbr5Ml(KD(~=6N8Dc!tm3=K%L-XR!)t9t~KW~r+$2jp)%&%RYeJU7CI23 zLnjQW_7+;J_b)LQfM0t6C=TY-M)`qNsoxUaJ9pjre>7cpIM#jpwlc~lo5(5|p(HCw zW+*8s3fZG5A)62-LZvdwDk~XLkx>~*R%H{?P)4F;gx~q;d4I?8{`0&Q_x&B8>$=YC z95MHD?5+&Ua9y^F_1i2d22YpL-hF~MU!6GfR7C>I1QhPE{TIV22IzMA?e$kJJ#RbO z)5$i*{iofYrxDP{eYX%jF7BW&Oy-(gzIaX%k>y-ym}`V}CPHug_witXhJ7CTYwhNb zNzHA+Mt#DxB{zF0nuu3dguVLbaHZXF@KX<_+2k2^{?$%CkczVVivR0z*(1eB5cLa9+ETfnHGjYDPN6{zHfMHfF(i zNA}na9&kmliv6a-eOsaXK%2V&YIePC zDAsg%c(I4eUi~{JRG^d(1R#)4j?!BV7Z9SIO;vX8wovZj`hH7scX2smP^mR{xzIX) zB9UUax)wZi=Yrj~^rxY7v`msa(Lk11fT}2_PN%_D4y_jsJu)g^iINkWbUt;e0mEV> zT7BPV@k&Zc88|JlPkMr4AVyu0hC(m*Y??~;MD@P$H>WQg>7O5w08~pHPH=LozIc8d zRT%pIdgAAbCFcPLSXZuvj~Ge@t353~Cb4NFbOn%z=!OHTJx>D$$i(OdBy(HZtt0-I zFJCUkic&z;JH~JQjT<*M0=d9w?Y`Iz-#GjaQV#UUFR+pg1vL^F0Wr{keJ^b=7k@G) zJ!wn?$j!>!)3oMt;M=H27W&-}BxV0}5dvYQy77?Mm%q8;z zZG)u)mAwKlK!`{PX)1nMI@Y2ICYJ`s)on}7G1j2p-wzd7K4r%%H+@sqvdV8gZitSt zajX@WNcuQVw>o?>n{9j~;S091sCpv*QUMISCc!Ev^9+z1pC?@X|ANPHfWucnhPOHX zy$|*!*Vy`iokG)m5gl2F_iH-^%O@U`c-^u862VtBYQ*Vcc=CA3n=R*C(Kv!Vf}P3@ zb7q>U5CK{o|LsuWHknmp6a;;!0VflTU_=s|Jm9!YJ_+1w&baaO|FiKU7lXu<{i3YUGc!k`ud%!dEgOWO$0ZeNq#8Xn#LnIQ@u0+FN{how%x1*OsgmO+`WUPkCaCIWFfDk@ zPun0A{brEv%>yU)$R?46G0#?O&E(;4(&QO4N>=LFLv14j&Qu&WO2T4d;gKc6ru`77 z1B)VQC*VVwZDb6&R|s2a4*T29KoL;}4s4W8Y#MMV>i4HsOm2a=xt;`t;HWRJteY5G zUD<(rYIm?Juzj@o{ytas!Rd|5P@)lIA`;mH&m%}q1VEVkrQtZ_h#Lt&3c4BwTvoU~ z;YEx3l)|3R-F`byK~=T?+gBA1Y{iHXZmmlhe1H(;&~yGK^#k??poZQsC{~L&`k(>N z7DUwUk_SdlLOWpg@@3&yyw--1hoKyN3BzI;{n_kCl!aOoR$`q)U_eRTvLrfeJZ4U_ zt(Z!%FHSY!S-5D5rxvAu7(`bycF4-nnUX3bC9?y%-fxWa>PFv)QM0-ES0Dc4v93(M zvNys^B@zkw8V{J6mce&i8^TKLpjK$~B)ue-nN?{?{Ts#%~1T6l->C$=4`FoYP*)4CkFl?cT(6aezN2$C^!Y#F*Us@Ex&j`X|1cU&(6dseg z?-ILh#n@F(n(Ut7WKg>mXPO{wKB9jce0y6iEwR`T4?or7YCHFm-lcFSS=ao=w<;oE z(%#s6pNf`ulRk1((yUrkPmq?uh<%)8&O&S4!a29+taM^ym^$OS&gfNH(_I?x+8pkQ zEQLM(bMiz;8$H!qe^pf$y&6`Vwcof)s+*p1-c}Thcshk=p|Q;2D53mJ8ms)(4d-$GV?Mk1#@-*;F-Za%{9XzLZNeoPdrbhwAXDL(}p32 z#^j5<@?g=E=n%lvNDJbCx!c6{&h6j)yKLTY_$Kzwa!wg+R)0=-q%Q9_>FfN4+1*Q2 zf@cm%Xepq`bI`;_MXw?#axmNuY&Ojd(o&aUZ?Gs7a_cyaGOUU9|3}ITdU^H+aQlM);ccf zS};l`vy}cC%Wm1S$HQYQ#uCk{6xB2bH{t*10=%H@IBv~w0(Fa+nZHR@eQb1-oeRu= zvOU+BAEpNNSEs!&f+hXkP8%0enh6>P>I>Uo3_2&o1+jo?je~um#rFuF0g&X$*J%J=t?9Y5g<%fEJIq@ z8^hCID}S%qGuZKu6o^zc!ivs1Yzz7i8Hmc=lN;wE33v!+T9Tyt-Hqyz{REmpHakpi$wjio5bk^JXLAJQ0yV(8_}2(G!~)_>RfO|6*0?!GWeW`$mzN z(TR=bAdm%yF3bz=cIwng-F?UyVHRM6T(ryI3(EYh9WQCPzqM?7b zE^&^dfz3(q0%&I=#DxmiOK{FeH&QW7tRxAG7(g(DDMoTe!7LKnt_K7Ym-5KtQ-WxI zO;VV$hl9I14T;GXxxcVQL(C!f`V${HxVbNaaoO~3n#Fe7Eb#N?%Mtq|h-8c~KLMl} z8qeU(AS)1p2S}M!JEa|xFUoVTHV{=&;lrG(4Nym%tAL5_{6+jj_`+_*WC;UV`bbun z(>-nvMoh_f!W9AEy@Q&*T}u2s_VR2SHn3ny!pWevIF^Y^6arB9P-VWE6iA4S;3Zwq z$qcQx-Hi_dQJYlLHmV07DoBVNz<)fWVHYkCX!w%Cm4d8o02+xJ6g!8$fv=MX%X!e9V$ z&inG)Avx-#je_Q4zr-Su%;N+^@385lal;M0Z_&#Oc&p;kTnbtcBW>uL;}lY~l0fsr z1C<6}0Gc&xIxbA_VNut1PB+Nb+Q0v*pq1Fke)OAUB`4G(8XUGhZ`z15eA}TqN=7H6 zXch3HD%z%36E17pS(jQba9HVlF;y*+yfe^PZ)IRxv3_kiigBTpq-|eV`mph+z>m;%Ek3%{SSwd z$naV@bNG~=<$h{uEWXsb^>cc9x_5bKYr(-8e*UBz1v_*<_qr(=aP%mob_`r?`*i%e zfj#%pODAOS$cylaD;SHtVY_pC$HN4>=7WWDz-Uy*&Oyhz=hz}L(c|CvK==CSC$nK>Otx6EBE#sH&8&FkKA zd3t(U$@C_LC3XB1<=cjNBqv?@Q!2OHNp9DNg3^7rN^6SutzT$1ND`<@-1_0KpK4gU ze`;!XfJDNo+A+Dg<8R{%?$vdceQNC+)qObT#@e?OKK$l+dgZ@Wr%)VEc(bXYl{(=sRpV}5ERNM2{x`P!#H;M*_`P#=`EP}4GtIhOgWj3?7e?_z`a{Yt zTZZ;UKalH}SW(paz3%R)sIVuaa0o>JE!Klb8TJB>-f|k23fN*1^a(VAj<;|Bp-sf@ z8Ko3!+X|l_r~XKf$6Sw(r$iw^l(RJoE-`AK3_~XFo8%pBlAo`xY?Rt&l4&@%62hCKqS83gexapI=M6twb9nh#_YtovRqyJ{F z$o8GCEZ?Jg?Z+j5)v&(Y*N`ddyOwTPjVfkzd*s{)3Y9+Ecb&DTye5278F0hLx*jAC z`e3_9_@y{4E&`?|+eeC{oo$faLw`2spsPE<4p%PleIF6viZIB{=>h`q^%kR|%9vE8gG2a=xTE70fg1+43$ZAzWc zo$A|q9$~hwj@m4bBEOwS!|nOy4KIQ4;ogqH9}^zgQ~J+IqkkvLFhNy`+kp^rkv#^^ zQL=eJ>j57RNnXFa(qWKuOB6Fsn-noJ4xP`)+#097jCa~giIMUA#j9CHYd;M;C?yN_ z`LN$hE~`BED@fy{uBIDHug~QU%a2w@wKsl5c}fHtIIWG&6FFp}AlI|mj3V<4Ct^*L1Io{kE4|4H};Nlfm=s4CR*9yLT=n z1=1~yJLia*U>ln|$n{OpoJY}CQS3cS_=3$2+Y8Sr&~Rb6t8`mu3wq7A7}|w9ImO{o za=`-1v89Q!%A#e{5}1Z`FEGH+8~qmx;iZBhMDh338xu9hrbun2U5QN~=dhZ}K?a9* zaujPiScZIiKVF4ljhLD=@%(hoJW>Pu=7+v!rQi4Kdixhx30Z8x`UUojUa$2LEkS?7 zR)r%3h4K0c?wzZxs~>Ada3JlVZQ`-xuw=PO04i6qt{<9 zWdDFksIh^Oli;!7>MYZ|RrLrScMd-D6YX^APqi{q==C!ftp9KZJNA83h+G3yA`*0S zyZt>z?|>9w1sU|YD{zf>297jfT*Cm3m?cwD1^4caXv~v;A&tEC_K5=a` z8a>o+!ElRvaO|<4Ol9;g!bU{nN8EZfEI42laqax@Lz6j0Uft7f*7z}y6S*IhQ0)r0 zkdl*wFWvg(RnX_QeYtD4ibsU3wsbN(vZ)i>9gbt-0{^`4^QkW2NPbY*PoA5xR$C2`TfQb0A(mNQJ1@%k>h+pJNfMS~Jj zd*8m8u_{cB$e3j!CZrgU9)CBY7jN_=rfJY@#KQ4H-+-3D84zkv0IB=q$Hl5^9qoy8 zlz6OQo{3Dm9g#(WO|UOKdVp2a=Xb8fR|ZxhL|kM5z9uW^34OrNCWG1hL}?qMaOqG$ zF{~p?!WO@Hn^Gz(0AMs-8?2!ixW~h``|Y&gPZU=mSNU@ z7qUgb2|$xejuQ9pBgRLVMWU$~?y7>7D}DSK56BkBb8F;)pOFv@mR)~YlXsk+W1tpb zXAi?7yNyT9x9C$Kf#cD~#z9RO*ZfJD8BKg$;GaR=QKq$k*#D_TGVxYIgku&^*Y0Uf>LvG@U5^afar`6xe^n z5da#GYXK{e;MSJdH&3KhVJ;>iBYKgwc@G}c53S7Y0PD5BZ~~*(dT?C10$U;V#yEAP zI$$SxaEzqjp-XnB`0l-*?Y|J3!jW>fTixovvYF{(v-~C*ymv9IT}xVh5AYPq(i`2X z+ER92fUW8%v^Q86gehfLYbV(Vgm%q)kb$bDdmRO~CtjJ{eIE1tw|caG#E_xguO0Q1 z!LKZ#dlYu?|JnHW_4O^J0)z}v(|~1^b?@HB&6}h1qI%AYCp80Fy7-^4n-R_y1{)C|O!77O zOm%XW32Kb#VNyvS^c?Jw2{(h*^hT2%B$Ty4+$Y}mRZpEvZqZ?3(PFnSVLq#6Pf<$* zEnw#~QQPZk6H_bL83ka8^#0rZs{f&qfTf#eu*=sJcTNkjrvN*16R+=y`|oUU9+;Il zlpRG{Cz-*Z>mV-u#w`Y7o>1)I;^6p6NUe*M>+mXCEHPL(CCcpiRQg7y{UH+evmqLkM@J?#M_LH({DNRKfy zZJImn=SQ?lo2%H^>4^kMF1@tQtfyK>PmdmoXE*MY<8G(1>Ji-7w>!yJKt1BB%-W|t z*N$oJ3s)T56q9L?JrjL}CjYiQ@0kSS{W;8H{+D*}&sB*_fvmC@zB#_$aA`5Au+qx zDRX1ZtjTR#NgLgmOwl&&760Y>n@be~P%-`e{V+JfQnnG#ec;jZN@!T0gQy*YW^evc zjdEdU@g^}bZPRmO?g!Z&hs5iKd)*!|n}v7S-`a9`@@vb4nAZ4pljf+%=T?cVD@P^Y z0CYa{N-d$BBbSqn3#4~HkM-{=qC4NMkCwi(Q#@Ax%+r*&sv3PKP0rnmQwa+ROEuKG z$at8wgUvV2+eYB!ix>a=nPK$C_ID7=f=!#~fymckEh+ZYd65ca8uFiHoa#j2LH?Q@ zBXCY5ebLO?3!=bUfzC=>l+ch|>p$+W=_AZX>{}+j!^pr=ZMv2^EG>+N_>ht(zw!s@ z%+nw^5{sy?Hh|PP;jq)=$HuWzuwxu33+jTb^M#g%#5nh+ajH`|F&#LmaBGu1_l5V*DnTLo`Rkp@ z<;b-7=Ff!pP5ATJ;OL)?Eu>9;(8=ozup139S+N`1SO#1vb!;QMg!c?}N$Ca{i*ei$ zDu}+$IIxK4z~+6x`u5Krz^hDxt5MB3s=i{QLdywvvm0vcn1a z7Ke7p<(_uv9SAf+NU~XvT!HazE8G-%(@u|ZqT2hN(T))u{hZI@Mvp4a5JQxgyHc(EywPh`+@(XS4iHxII57+vHP&6}>yBi+-y>ex2s~k-_rm znfgMFDe!-Ygzh;tiy8$Ej_U^EH$OXKr$BbIgFWFFFP=nM%t(J6a`}cgG2Rl0}SV;iC zYLT_KZ5t;Mf)V>KukkLUqO?yHkkvrw;faL~1{*L-Z4lo?loA-(1lO$~bvzV@IH6yo zF(if`_*Ufj62@-q`Sa)AzwLH8f{cUUG*E)KKxPC^GJ!t`fP*j|_-&`ph77e4@f-BX zu|mi-{s-_75i7`;7on^BJo6CuNCbcl&OgFb2M|Jd<}jio4v6$jOzQ)T;Ya{&Bj^%6 z73cCB8+x&oB+6A!fX4@xwjg-mUh2rm2#UQnn5{5PStHKXii{9zq*DJAC0@NMta1-i z5IFz1(u7mgtrXxhY}ll5?C^iA);PkBz#kjm&zoJ3l^yZ_AkG@7vo0Jy?c9X=N3t1# zSXJmm6avybU~+Wx^>R-7+3mj5*3aOMgI~{?U4`E=Sm{Fyhf%BI@J`_@pcC??fP0`U zmfBTu@8kb&KqrRhNfbvC*!RS@0fZuZmq$k*hzVA~b%bcIDqo7b?%8brk*4S2I3YN0 z2$~nc0(4?b&5--^nVpevNP$ceLN55z2}6?8wL?sd2GAiw`U+b<%@Rr*pm}7S3V|nt zofr{7{v+bHN#z)9oAw(-=-A=z!GOlwF7`1EVqODJM>F8o>75rF9)*g5Il z9|faryFxV0Bx!5ua{F~axTM_#*M1MwofOt@v-F8)s8ce&;pDNo@8Cw@HbQo#xAZ6X ze%cFkt#}S)E4pzpNZ*DH=lkkGjsdTCMx@h%AD?q-#NwtMJGhDCyZQ8v*|BqZoWNDF zK8!MW@yZsWX+UDWppY+1xQit(;WY=%bFzpTmmBBjPXqNvX!~F_FIB-^8p^=J(nx-# zfB+*J8lduEtUNLL!XyGT%m(KR{knD30AUds9>pdqwnOxc=?}oZ*x%|#8v@PffU*Ry zP$iRjPAh)+ZjeYr4lk8Jmjy9xLiL?Bh9UT##3ZnvL^c3bL~(f~8}(p+s{ilr5xBDl zA3g_Hv3=37tP}kt1QD8WJlXr>EEh3=QTEKp$%@|#q>v=j<0C;qb}0{QZ*UUPutdeh z`MjcmD=SfpAd=EMH>NNgjlC_-RpJo}t($6iUnAVXF}tA_)!|OWqO^gQP&@!orpC{3I~-ecYas|pcqcLaW~CQ)c?!%gVwT0>BqUwd)q-jgP$zh#1;{0tBPwMh<;rZ z;p#K=rgPwuJSBUj3&%0uI-63RsFmW!?Aymb+VoDL6nOMfZ3~X~;Xslkmd?{#9dc=0JK)!-UCEUKFTG_22*S5!Ae)3`o7Dh&ofdfEynAn26+3ey#wqOZW$h* z6I0rhzQv6{g>rL0?*m#JFAEjdvZJK1cR2O6+vNJN`r}heI_%L+6G_lBvHeyu8~S6R z+2pFQ8J+w=2@%#W3D4SktKxsZk@U|~WH!w*vGCVEA*i9>kP$oFK6dAKHqqjsu1p_ZrB%aJ=0EpR|ld7a1}J6)ZRSwerUGi`0m>RzwW%x?aa`uCvp z`5l!!uRc1CyDQwvVS9EY&|T?RGDVH4yZG;o;jj5U#GLKM-`p6Nk{{;(nD~QgU)EXD z2NL2V&_Elsy*%4pII(vRJatg-`wT1&=m5V^4G%FrU!Tif`MmP%yd<5(jaKDjx(TmM zqJ)Z+VhbPXuC2S*p16Fe-OTYTce5Sa^GLfd2P?T_i)e1I-$rGmoKM%uX~CmJ`>#< zHWjjOTe1FE_pqsnr5+FVsjg6?9h>TVSU2v~%;bt0+F zg*|0Wr9;F`5e@ZeT5A1m98-nc4`+Th*lZBem9A!7^>Coo!t9p)xU#fX%+pbz=g{!uFz5ahG&%E_~9MDDk%Hrc5uHyTlIuj%aqCmRgUv=GmiO*Ngt5e3> zDW0!x4Kw+Z-Edi;tEcB~TEU;WFM)c?DNU0fK1{2O_VkW8g)X=jsij`i(teLTV(L)I zNuL{@>q`!uxo2sjbg5O%NA>KW>RP;3_#e>|5LPiDg)vkT%w#b*V@;jIF}Ob3ZI{gx zUi$-{2RQq*aazkmM2LWxzT9b?a>#_mxUGj|IPa)pjMfiZmX@?@pJf_4*7Lo7 zSX#pyIY(ikSaA5Y)sxr4vyJsb?7^Fh+-Lu&(bG`;ux3@=9Dgu=i&EB9i1^h4m++hd z>S=)wS6SDx>FM4dkEJ@%Dr(*g>r1F6h_IN5&Q2q+5ez5Pls&*`BAErH_^8)A*=r)N zmGEjHjpM{K2qqj!Bto4h7Fi^?^3$vMZDnGOWesQg(SXA{WXd@)kT$r3`fy-BM8EKK zs=OfM@9WhVpPdtwT_UK*r{b$np9b>Ndkc{B30D~_~q-7 zDw3y$5?y6Fg{5pHd}A;E`S#ln`8tuZ4yHpGw=T9JEaf9MZJF9xQ24@J>GI33ur(&? zZ!9VghMk3n{6zm_lV7}Ab-??I;SUF)G8F|N{GTYIA2F{mN5gHW5wUG>JEfo?8x17g zVUZ$s2Uihp-dl;s%Tg}j83>5Ia~2^tCJycU&CJBk-%$j(_!4rAQ8SN8*fk*Mz3yaZ z7JeQ+u+A*+y57Wue5|;SJwA2LWeU{UBllo0j8xj=zg*nhv?14ne^?7`-Iu4hrg&b6 zx!7^cdVgzH|EwZ#mL5|R7LYZHe_*Pj3on_MOQ28v#B_8QVS=Ly!HeG{@%7q9Y~ERK zwZ@xaCIy+&2j5Yq_nk}cyJ!EO3(y&@P?WXH)y0MQf*Tlue)f-Ci@VP}Zf<54Ugp|e z@mA~r5v2fwV*Zg|Sa|PNc1YJ^5UhHzt3LCpfSZe}Z!(|6jpM}>czXi=6qEgr{l&Me zO&^4!lKF>>l++85>h^W_5E(0G-iyDtgH??65X4+N76-u{BD$-K_E3*m6K;pIGiCej zKsV5+F|^*uO8+mh3gPA+MH8Bwt_P0Elvml=Kv+k%@kGd(```xFs@Ooo zR3ru;<*4JvQ1Re(Yu6UR;2KYtsa#Qe`~CsH@dqT`-Kk4BNR@IJnIXhtVN)^SXLNWC zSkHz-L~>Mjg$LlO4L)m6sUwSc0jR$CuvwJ#y+xdEPa?q$DL7um2Yl>!?<4q^{6tGn zIFOjc!qN>;e25JnNumea$D8JDrwXVeSl5$e0GM*e*hxJnN^?w|7Y`=eQ_NYvl+br~ z3ec6=46!)=UM39gplE+VUN8K`p1oVAw~OY2rv~vGG1s4|#yvg>KOD1}kGad|Fc=VE z^!DZpdWyaOxHZTEQzi6NmOLz^(1^02=WJE_VYll?U)ee&Q4unCwg^NuX5Y6&fba1U zTC(HCp2*m^xz&b>xbZDwZTIZ-kcQj96E+;0&%ua~?Nz`lF;?u<5&8*MP2Kn$Xp1q2V|LtV zfsJ#B-}%Pn`nh#qmX6LLxHnVtC1Q z+@3sU$LMTJ+56v~&gYi*ESBSpKQE`hTDN1`o-{$<4aL;)6u9%)1y#Zu6pzB>2S|wl zt{RAIXxSs5i@-AzJB@)0O;V+;3#}Xd`1PLm;{f&@SL}APUI1|~=A7fNjemCZRImT; zsPLiil(0ZIpYJUim%00tYPpw1%XY@+`Eg*#4mX%<7R`!4s zD<+!M5EN~JuE&TGWs(tHk~$y9N-oxF!h()MY5d(8Enz}D1y@m zc^X@3%>}rgjRbyveQ)>y6IS6&np?`Y&R;dSAbHJ5XJV)MVppeQ)|9v@Lqgm2QDxOm zk)bW`%vomy{?HVL$79Dj_52p=hDpj`Lv9tmzk}R=Uopi_eSWvCcuxUq)V(^v(>5mU z2Y5J5G_#&+`SUOcwb34yTnf#+_H3ehgyn&zu4bijBZgUm8h}3SZXD+(c>)QI`7Wh6 z1Q2*?$Q*#IPC#Ed2V`uhMn^}TztGFWP4+HonX7XoWL2QMn3$+*aZKajgv&IM$_Ik_WKv+%C~Q5JDUeWsL7|#?T8ZZrnD=<$1{#j3 zgLs3TbwX$x&h`C=4?E~;gpin8LaXP?&>Lb)0r zFQP9Bw1=cieF-N-<-v2^P(WS^YIMwSSK^1|WnWJ**%&!El=t56X~T*U>%NRwDYq^K zk>|Ah$%*YIj?A5ui@nS-un!|bTLLFyW~T)Ja4%GpUFz*mqOT@ouWQ$@pSL`P&Iy8) z%LGb+yB)i*!p^2oy;wt%3<@*@w-Yw2s`3-RO*k3JI4eFuZ3}>pzVS{!Wi0?`#0DE( z7i4W(+xnK_AW-DitAJ#K{>&MA%Q7pGWgbv+2ulZ>tF6dvn50*0R!#%Wj2M`KRQ>Jr zBvTMC@D+e{Kt&Y@h=v@6%j|DpEHeU4Xgy-!cik?OLhp>RwE?nHYG@YuID-I^kVTy* zyoT{21IH%73xS@2VeIWMeD`e{lr;h^J~{LGlKD+t?=-shm8;(!(#YvPtg(&!O&l-`+f9{E=<-am_OE`nqRq z0gRkvGvN~J@Vd_=4f^1DCia}8DD9kHckOOz%D-dxR(s{B>#uqz{@icP-u1%&!#DSV z;r8tO>{joIBVag6(SG?Figh0`Q`;wT_Vv4NHX?CT`&q{UJTC01Skqkz2?^pWheU}B zIHhsNh(9^0n+dUNC_tfH1H1xd+4qgIG4f_%Od-ugE8WmTT)^bCTUQ8}MaBt$S$H!E ztT;k!YJGj5*{!#LNi5&3!(p;sq=rUGgD1x{|L_|n&2c;ja$FE1^+iwI z{_VBJatI4S2&QTOUYB_7nl@8Yc7qO}UVzJZ%Ru!Y{F~RWGbS-$WSHXsz(2G!J+_lt zgle<)R%-gYnZlS+0Ir|)y#t;ho*Ddwfc;xMErXYT!{XXo?-)=>{C^-}(j_*%Zz%6G zcO{%zluAS;CG5M3e}$AYpjB#x$AI$r{h4x?l)v@@hJ^;Z91%_}Egdt%thG;cH-wHL z399G0QWRzmQiYUqB*>+@vM>)K$Dwjf#@y%2eGzJp!!?%H#xi;`yozqwU>$wwh*dYj>5?hMYT@^is)SlD#vD< z9iB5)*BU)29$32@{;hvj&KKQ&gLNjjpmTmXg*4`6Tbd#|x4;5miPwpP*Z~CHdmNZ!wTH@k7SK6L& zeNiT)d^n%^m5BrN-t+Q$K(}kAscg*cZEw45OeRlhS6a+% zGg@YIH#Mp!-p>t$t0E}aCj79ZeQ3q%zgNVFYr{m^SN*j$u9TtG-z=xUvxQp;Xx&eq z8_=x3SzmikFr-vCMl?BNYX}7ueMs#siVm6rYD?=4?7G7qgA2olqD6NZXDUv6S06Ng z%`8m8H7W9s$yUKkf5k&$?5yVNF*PcG@2_Fz)FChlM~?elST5tt0;#&l`nA#vX++}i z;Jn|vXU~O3dt&fR1ncnJ%aYNBW{FrfpmC<4z@V}p{2Q>lL3(`@UAk8m);RbRYcLXE zb3XH=8e2jF07J4Y#*_pv7h)R?d~6{iT_pTrQ4uR(ZT#nTuQ|+t#GD#3oLD6b_5h2o z(2t?-z@9CtK@le^NMF7f%ZQH}GbxY)3fBeu%DpYRL?SN!9To9r1#9--uge z0H_&3`HGF6%D&UWXfon&+;B)5y%~&gafIk}R zq&Y^7ZO<%_*K}3sf@cci2WfCRp_U;?uff~lA(>J@pUzVJ5)fkG@M z_6FD~^y{gRT>+Sj6(h4Ri=jv&nS-?`Z9zE~5jzF&0i0IP&zTeef4?lLAm~olc#CW9GdDqf(Ga zz&z(0NX{wh6&OG$Wi1GYWVk3hYBYK0UrM{9V}Ev^-B(i#Nh@&q8UcEkDivu(LFvmO z(1;}u+2Ene1M_bK`V(hAjam)6qaa>X&wpH3`D}Kjj&1_s; zv=lgcy%}xcXF(jIYqIY=yD@UEfo*>9^5q{6Z6>p$4-ba*WL!;HoL0YF)kMSAaC9Pd z>c&^2e!ie1yrsR;7Lq}XMsp7z4##lDO&;sgNK$h}T#lKPbAkK~Pq9DnV*pVdE0Epk z``gdANqT9W8_!9LrKA~NS7cL-(c5a*MB4NF{DMt;j<~S6vhX*~Y1nz!MTxey4^aeM z_KqIa({2}IWQnhX8c|9LrmlFLv5h#+>x*lP*7d^0ie|T;9K{^dyeDP^Hz(^QajuO> ztw@gO$#7I$r%bICrMLfFZuyh7^7qo-$8S}QRd{)Iu4q{}u}Q#St$_RwTUI_-^RE=Q zN+%o~N_!{7E^4uBfr5+7-leEFA`0B$R4sHj8z@vZ>Zw^CKG~ioJRVZOUt&CeD`mco zJ~TXpqw>D`O%W~0`j7o<<*yVf1W!Egg4j$pNp(Q(S<5~K!6Jsf3pdzqZ%%9iKqi)& zpmJ%Jt%fBkm*jf-=RJ-vUJ2{w)h)4s^lcJgXpP{g6TBZdIkj)S0#0Mv`S3o}`otUU ze|nX?!=G5*LUUCNvpChxhX&dIBr}PLXhsd`ZWBKO^vE$-$iDTadJsKv$M5@Qa`|)3 z`t*D2Zi`2mQ+$A#eEr?s7l%8F&&nug`3m%x_mcXlhD4 zk)wpZgAa&_fJmYq3Mvyq4=8q~yBMku0twJU3rXHccu+2?HzD7=9+rqS*9|&#OpU)E z#)X2hOF9m+cl~yLsn|X&p$pxM9IlWd6PAKU4K8Iw6W|Lhgals3HMgK;Bc6%`|8_X! z#D*hU6nkFG8Z|JH!ZL_h=#e%Wi+MUUqcqW>&~0L}Ag<7(z*o>kf#~`iFewoaq7_%+ zoi}Df)j*7CJ;`PVs1!$^q?7p^&TPcV3`945K2?Z9S+P7pG$mzw1rMAZqlp1PyVAuJ z0sTO5RAb>6SvjPCLU|pE2tFLipn{c0?56axUN>Uj1kAJuHRWvY)oBL>lR3utHdf%N)KK z7Oke6*WWQFduLzdP7=KAVV`bAjBAsiOglv*k#I*JkpV#=)D-Ya3Bq5Jm@vSObBk5kZP`cP}AN zk#!v`|4Q#teo;q&tQg-2oF!QQBN+O0PV-69g ze2)|K=*rv&e$`70@1w#=ICbd}k65s2%zFW{@kpyG_kNAXF zE$^b*k=7c{kcHTYzOAJYX-QWdO|b$hC@UEM*=^ zjPbA)aC|52wO3bn4{k9&2X;3>4^CrOGnmr*?2X&<%a}dK2$N9-EoG>uZN4Z^dXs!_Y%me2hie4=i`6I=-7HFM{lC8Jl zr>8jB?Q|gn^t+{y!};g(cP*&x zMi)IOwD-+WIAN#Jiv`$6?&-@zsFz^Y+)l0FLsHjGzIUfNaew8JpWZPTOCkUJJ|}&# z7)L|WeF2~7ZN+CnOFnL69Y{I%sDYhU}!c1?CY+-XR9^fqoS`h7f#+;tUi1H z3U9uAJe8*x_3(~w*B)xlNosKq*b6iXeO-68swH0H3d`w8)iY}fIL)GL>?eJCm$Ndb zHPUJ&6}8^vW{7&-6^+!UU(v8HJ?G&S*RvY?qRxwkUY|AVd&7rOBb|qtzvwnh4189# zV;JPur1%h?x%2B5<@{J2d)~2bR=fp9!u)UguX}TJ*2vX%HFxgWxpFpM)0% z4vqd=jPd+*am$!q>bFAQ!42-L$2eQ1xx58bOjlLI-ecLL8lWFD5c=M^T(ny?r1eT{ ziL7NhW$IvZ=b`Caxv~5HG^Kry6{Z{Wbsu;7{Zk^KuykT=s=kD`uCSJEaal@W;g<9* zZ)8k79z5O^LLrd8XX z3fNYvth0jeFr==ecuFTUhX- zv4x++e^Z%rQl9=8Hn^}ALh9q1D^JH{v2q5m1#1b-@p{h`t0U{q@G{auAF;o5O?tk1 z^xc-O+e42b5+!Sual^5Vqu<+}_)~hh5fv(e?bCH&nm-DFPRKF#CnpApP>dkYt&5t^ z)L(=(AxuuKl2%%`eUw6Tz2DT73T?6e9~rK7KC5xl&fSmPP>g0)RZ`TvFrQ1h4=h(R?WL?Tz| zIDXIwr%=^Fb@kXOifsvbUh!UMIB8^;Y2 zj7@mD_9~dO$XeTEome7$`|ZBCt+AiN8>=&sdW0qox_6)PZ4#fJr{dN^kEw6FfB$~# zB!#QLme5|Z2?>4p5~Q_{A4LuTcZb`iLcFIN(Ym09az1znp%T=r{74{o*{h>Ni@|m8 z&=5R-h@Tv!Bot~I=P?7~zGtAmV%i|%rictZajrxETa$kn@a=yl*V??gSj1hrw3gV@ z1aWIp1fX_;_mL62x`H0Y`>hNYJ}r>Zd#g+DSBgt7$s#p_=9tB-4Ma{c+tqsal z54?7%t6N=5Cyv*}C+S-HCkyW@Tqeq1bcJRYUdbJkNc;z~N=yIRq#=AIADXIzdfL;S`e%Daila%;re^r{k_2CPDG@+R#t3;`Wz*%loq|6?8ax<+;arU#{-2tb4ujY_!Qi zi(HZ9#iZ-XmwrKapSY-{J4Hh;mnZe-r(h`2Kv6vosnfHTu&=E z*A*ouwrzXz;(dbm&$Aw-Z_@K89VT~Ku)f&XK(Fdj+HRX@E8XNDp=s;|PXceo9jie% zD8&M8ArU#85677tPLmv=0NTWspyAEbjk0z##@^{AbW|nIi+;rhf?JR9%TT>FF-`E- zI9ngqQn4q9<96jl=!+qmm|Z`PsJs*ssw|nkDau+VpN(C~F>Hp%EG<@{ zJor*5;T+X_uIWyq+tNbralz}&LdW6b@r$w9%zy1lfcWPUe>#62j@!vuCtfLPxx2@m zACs+!F)!OE@e=MoKhc=N_~?ynhw^3k1Kt5UoELslpPF8EWZ3V>g%dU*I|IuI3;%|~ z;`_VPyclcoN`}P5#0z9|UNK%+YAD#)bsk3mMv&YrgQ3nf!&9G#(0~Ly;6?ST`1t21 zRyV|z{_dkk@28d!qBaa6!CqKI47SfNW4Qe>3K2Uc#qQ{w(uD0{pn8Lq$q6EPdRr;K zNTVk)bS3VZu->nH9~kcZOB@I}W^`X_a5Xc|&SH`v*$}8f6JK7Ve*OHqYn2yb=&&|4 zpd{0Ud^Z#brybr8pUwOe#vjN%fC>4AtkoqHjeh;HY-Zuue`^wKrZRr72_cmROOR71 zsh7dGumNCdCewE3GDs!l8m;W|KP@D*x40^zorL@QbWi5E;lk zv_RK3z(@WKC0#6>;@tU z=fSF%KOi;}JpAyu5sL}yLCJr4iQvv$=yb{UV&~7uk(3) zYH%88L&t(nIFFV&j5dgMNZz4*&JA{n?`#*=VVyu$t|WUFTgvaobt`sDO%B)Qu5k zW&qjsa1$};=9jmBgXLB+RAZ2^$qWB|*NMg2?B7B96L$d-94WD5z-16TqgTp^oKZ*= zm>`uThGGR$Cyp5VQfgvXL&CrzvM@cTaf?$WHMU$7{1-fo1c(NaiGl*UZZg@!v-F$r zHg;P{b2I`Nu1VHt#WuEW+=+mhQ5cAM354p;Jq19ze_1DHj+q{7AYx(CCrK>dg?xu8 zfm<869HP7nw=w)HMD3UvYHee&hDD-=9(`$b}^jyx|0@&%E21KzZp{bHPa*Ii1h zcH@JR_XZ#I(p$=>sCs1oMcjZDssNS{1t#<<@zQh@F!!VfSsf%Pm=C4{&Vf4%VVb-b z3=!OLCL%I&qnsQInqr7l!!I0uHK@_?&5MwvSCE+wIAX7Nthj{4+c_}pP@hOdDv44P zP}=aqC<-xK+q*3OGShDOqLA}r`+7BIs zbF`%7#YHXu{tqNCd*np~Nh&kGpIER1X&dk*=|TSHm3g9dB!Bqc-0=H%xT=D&;W=DO z2^1FGhfzFb^$_w}&9su21t8LH=sM7WtN2XC;Hl&Xh>`J2w-8S?5@t?Y*?CpU$ew97>eLsIJhAT{?onP3|M0pgEa8Sy}xUB>8rQIP6HqUfB>@N zz<@&aM8SM0cr~yht>k=#g^U9X4k#^mP8(zh&7VEU>dR~>WIOQ06PMFmn}XOz8Q-TK zUZ@*6FN80u4ZX-%NU64zHrUW`1~*)-S)S`B#vaj^*Ly`g!GECDD$Fsere#|phcIqV z)7l^!V)Kj7MM(ix)qMZZN+b?`jP{tp=_r${u=U>x;0(9qT}AyHqe5BOMZ?EC1Dzk1 zL%$Sw7=eW_Cvs2_Y!#=!MSAhmj^dNLWFrG?)Vbp%uTbw_IAvl3AtEi!h)b*mD{;F! zZJq)cnutsXnIg5|G(nw12w&^{P{qVvw!V6GcljGrYA&#$@opBBGT9&7c^HY4A)rY* zsaiM=T}b#-dUhAs60rNxge^*=yEw5z>??O4pV0v41^z*qI261FU?wzKk}`OTFt;L^ z_`f91#o|-~70u`T9Uw4pTlZ|)-rXLQtm;#kp@NU46T`E6dpOP`gcUx^C_(>82ojyI zh+z#LVA*DjK3)?&Yyf=dJnE$gRREXFFG_1)KfPsig(ukiuoo7}hH)#94a#BGJ^H*AdCzeX*va`b%zevgZ3GBk^|7gqK$FCC)S z=k<;gLLK-B;^WTxmfP>S*gVsF%Fya50JBYaZ;k~T+@v0_sqMOdt+=SQed2^XYiQ*A z(bq9khBh!Oz+E}8%4u>(M$&BExaj?{@D-K9zj6$qn66*BHvh@u!C~wRYi{0Wpl6D@ za>wIY(@nkQsXU?bl*9S%p0rulV(DLs-{URW%&PU>LZ(VoG9ZF}7&hv^u2r%Y#HH6X zys!%1yR*jC+?rcA`|MH1Pnt9xT~D+Q9!z|>TlH{`XW8+q_5H~|on2fq3h8Z6wjQmu z{ONgO_WX%3`g-kt8O}@L!iWi7@=&RJP~Ul0IW{wB&aHBA^592X;ZOO}sjSCk0+jl7 z#MR)*a^UI9q0!(@`q;XFds&K2vX}!@81q#3g`RV`uHe%gDw7%R6h3mnQoY|CQyhlo zU$4I%eUq|@7S=P}&cQ?`+jlSu5gnn&+d+7C(G=Q zCGmdYlhgG!^ga#O-mrAp45XHYU;N>N`__-3cjn_id2Zr@e`1k2bBMkxE zt6zRq8A}Kz9i&K9DoPS?FZuD!IT_Yosb951F9k{}NK7>>t`ln!?cK^CBWe3+qxW#z zL0t8VE%yXYymu*kPxWQ<56MZ184ANa6Oo$xdN^i0&1a+3@C6sunLiy8Rc7y$tt`3} z%IJV!hC(&Lc0M=MuK7vnNgHmz?7jo##wH?j0onT^CrwPaxBEo6l%F!WDdHIddmOXj z8!p@)pQ}DGi$4ly)ncZ8${wqCEIfU~fODotyxH5vwAggkf~MDke4I-YYh`5A*n>1_ ztb9tnH+Ak`WKTQQ{%YV%_ggKe>o?gVj&AU=E9L1GI<{%`NkT*LX*({}rM%V`o0?`1Sy7ku}{yB50U z=7F+DMO5WGM!%g}Ww$xL>B&<*x$>&CwLGvLH0L>N_%Fn)Ipq4`kBlgT&5+)Oa3Zh&7-M-AdW@W`h-@U%oJy>?2dRqNJ#a4!v)ikZKT_4wm3U7Kk`{4uqL4)lTE#gH_E1lK_MjYjn z5r0#rBy(+lW~N>2<*E~gRi!r_tTz82PwxSab=&`s+cQE@i6T3rl*q`+mXXTHDk&o& zWF*-kDJw#VXdp79WM!9(s6;?B-Hqv;U4xq(#ic91aF8cVC{VuXwVK%_1@?YBmTptw1=;eTY zfgpQQw6|g5`vjJ%9;F`D&bvAneoa3aCVd+_!}JzOwF*-Ef-n~RJ{GBR@u%pyqI_8r zWkbfTAn~$e=H?GXyUIb|BF%}=vvW#DEqSmGl&t&q45iB>py#~WKYfzIPeJ-ZfP+2D z=>`tr0_XaFnRC{RnLi+-*LzLNg%N{%nQGTv6h!&}Fv~-?78LFO^!#6aifUGNBHz^d zEs2E0Upd(Qtg7MdBg^yPF_Qo2fAF6hQPcC(Pm(w+^N-Z?Vhgyw_(?oA@XI!H^M|%zddkGZFo9-lZ|n2Euo!1Meicz;6Md| zbI}os1$%|PpJ?X-klGfoKAM@6tpAQZJo>+IAD%F+V&bO2-;A3YTz3?r7QwtWt5^nXmIXSL?&Ato74RsdEgZw|TK0EmXHov5Su%?2fT`Fl=F@gkL&11)IzT;*$D-{;1*7{!^&gB90Vw#t%fOM;`*Q3$?l5Ac0D4iA8 zRU-5F{G0bdFFqsO70@oyPL7H$i@-eH;UYi_9Ssl>`;`;J$Cxw(ByI;@d~UNuv9bG7 z!ftK5h2%?I`L+)jh0Z9-M#q}>Bh5&}b5rMn%-j3e36a$DvAa~b5T^z7Lr+*)+JeiH zyKZjQQDI+Zpn7CF$(Ew(GLzDAru)51L(Q2}|JW6{QkNnrbi%A2?oXFZGdL=c-|c8m zdvWBdY$eQ7P78HC@wb*}E3*%h%%*hNDG#NT7kLrO*eMI2+*dyGoQ z$r8J5o8e7ulTPqe@luCp3LZ`=94cAU`Jw&nBP(7CneB`RS{G59l^&YU8ppqOn*S_U zeOw<`l}s^@#fFuHYWAB^e=hT$S=!tEOSF5p?w{8SN~EXt zee8SJA!nXom4m`R);I%?Lb#OZeYLD_)A8ASUXL*~Stb*zJGb7(rQbKXcc7#{mP%YH zO7d^bd%k#y%^rVKKL5$3*(8*-NuELOqy5;6E8jU>+n?`St#ZCm%Oy3cxKCCV5P!RC zrH;Hwk_!!3HODnuF@s%MyQp$yG&97e1bp5q_^ilv(P#@<_+6r0dOq!~h+qC2omTsw z;A2vL$2SGL3$$FGWqr?PW~USvusx9R8cm1yeqktck#E7OXuotCCISMaLmikY{e7DyaCSkAcsK*ql+CAn@!ncg2P zfL|fX{bRisp9|?dgJmv2YdyFrfDq_a34uP3nMP3zZB~Soc=r5RGAVOJB7x8Twjf^|ewhYC;0WTd3s3?DXTU|c23%i*((jo`M$sSB7 zBz7VEObFC4WoO^L&MJt#P6DZL5U;2dekk8cQdHbZFL=}CFmCFGR#S`2R`5jVKgZh#9<=?aw6Vjq#jjrcR}zdvpSoi&lu;hBjKezTreAq?>(ryx7E zdB;crGe%Qsr*bqdb$zC6Vp9dk6h;z(@n9CEzJ?6A8siPIGE-xCrkc zO+(24Ur!`7_@pVsFlUY9{`{}Tt;}fyRKn?DQa*lGkfxE?9D++P)j&GPgGagvlB(y* zecKV;g{kGgsIR5cz$_R54A8wY5%&633lLjNX`9(M3L;m z?Ll|B;wStrfg1VsuJ*6yz~nCC_JZpk=t;04vRzTWtK2vsvkRTA}MxSyNFx_*7; z#7p{nIXQ%YX@dXr;g=gX?^qmxcaaja4L%Rg3w?kNlDs6mU8l?4VTU7UPR-i9e$nJl zdYCKRVM`&d@!su*qSxY(3dIb64rccGm5~;xpXb1D@4!KKdX+$uXpV(`oqQnC(bo*3 z6QhLvg6R{dH+0Zi|AOTRqvqGX&t#I#fCv<6ZdyrkO-)T%c$`U14zSY$h3hM`8T}Eo z(+$|)Vj5~J4V+h?dq@LBEpK}iv~5J3ht4kx?XysH;8909>VLY!i2Rx$NAjCu6%y4PYv?*MO#r^#9aD3gGD9)Q<2A z9A>OI3sG~N`A&Th9Tiv5qBaYg2OUM*?=v#oL;?WCHzq*8W2Bgh5Dtvq*~6C03^c2cOUeTfXa#Fx5LQ) z2>>|^DZUU#yCVzisn>z;U0Kw?8_B+f@r0x&fhZzPCkHJX3z1wyYB2%hZUufwn$5sx z!G6{db-&mQ+BF5Qum8mZz+r$c7!s0m5FyPi*DK{7Z@B~38?ti}9)Z}OEQXW2K1 ztO7R_{hR~c-uR(pgM~EwTo^lH`0bdacEV5C?AduB0vdITN&e#9+vGiHM2s?MD9BiP zCx8G#1|iwzyI7iJqjlUZ%ZtSx{up9u9oTaMS#ogZ1oD7DewA4_UbZ!j8sF zoBVo?r{&~GUGO7WEmI6_XuU>;3D4TKMiNhh9p$&+=@U4y;XC5|IDqpHo-K7$=Hju% z*UXIw$_djNX%7hmz>7z@VN#d8$%sMhj$y8f^uC9UqeLX-<_@IF;DVA0coZbA_!}WW zn#;1A@G1HhWn%V+7a3Ogk$18|?UMy6>S~FI5IkdwifL~Xl9H|{ws$4K)EV z2Z^gYBJg)((-1Qf9nPG&S0Hv+4E8aeY+;=aIc!!iYG-ztz%BIDdwQp(h;)B5J~Xgr zD0d%X*{*EW9njcUXE|ht>UJ^&yrY)JiwRWTSqp*#;sM{dn(s3+m{K8!wJxqprx2Ba zUH&kPqq%XsM1lgZ0OS)1*EGJil2mgfR(`SrOZgRVDuidmT$PUOkJpELS z7~#BvF!FAs9Bu-le@LVQjyxM{9$G2PxC;gCfSL73(;!#}A2!)sLIB{w+#i!EGW)Uo ztl8#;am|YVrv+F?r+KLiRW6_gY*zJGsX9uWb$ONf_(Izhy#1MTUw`-dO}Q{- z{Ll1wAEAJkbjeE_HQ0_ZEol_Q{-~t#Q?b1kZ57biqI0+Vmqw-cvF*%4BHt~UGRlG_ z-1UZ%c_jEx?d;{+NonEx=VZl(io@5X7~Ot&*RW3nQ2dn1?WGb@${pa~oBh_ob0OkN zWBYLluKu|rHB>Fdf{55j9PAYyzG8^P~Y8Xx2{rX zQkd%blQc%3?*eQq2cL2%n?F*!(`lA5@%;5AK_&SPra_7KH5=Z%mRZhjNHsoipnLM+ zE}rhSvc%?$MNRu~(Yk$^{OydkJe!?#*^Ud(O7wZprx*RIPz=!c0C~Tt5qs)`@$dYT z+0Po%jVy=n3->>iflRs{ zCulbX(*1LFVtdL@f8{v$P?zi3JASrh-FC%Yw3==Anz#R)jHXQt+x8hvhQH=S=Blukk`A?s(dhN?yEr=)ycG|ie6s+lpOgp za*2O~hItAa74n4?f*aj8()G9)xI1_-__REtlb@+K$e7dMYE9UUf&74Q5P_4sTxPRYu36$`(Aj+Q;D-v-P-97xzTE zN#I3~UH!Kzntc|%?oJrVOq_Y2{Ac8Kv-akj&6yvx_=0F!lD^YiAFF2VWY=I*D^r^C zUbSt{<%{GCVF*zb6`-YU;nF($;Q%fEG;rpzrDdM6?QacQ{+JX0=-4%0HcU{~<>%!Q z#{&#R{bgtCZnn;XvJ@vf zPxk+b+pU3kEeMxe0N8vld4*8SFvKs?6E?MTfoLhQgt}be2_YdtXx^iBAMa`bb4{*7 zoPQRn1xU}ebbB5S@;i9K5Ase@%1Vf715eR#f{CK|9k+bqyO1Vu+ueoMijd#8y|q9H zElI{jYdHN#rP_u2EIENFf9ggZK|fzB1)<+bq_m-k)PbygOwcDkHAYFj7$is{Cc&)W zgJn@cjI|RiOo@V0kp#st&9Ipb~RN z9lIn6U?bgESUrJv68R|<&T4F8!p1Iuy%JrEl7@zIA6kLoqwF{|aO5C+iF({p%pYWxbz-K*DPnkxga!7K&>B zUd4YuI84VNofcGhI+kQcPH%fORg6h>tfDruiFOZT)ZTH3lwIg=8 z_v35d;hz)O8`}}@B^Ny7(wBabbzUzCW#>(bv(=#haaMRc7f1@f&~G9g-k7Y22Vud9nA0q$2CLqT z`-@R_F5n9E{T!!_7E*Q&4)yT$9=nMe5;_dx4^_#0n3o;IQS-U`i0|g=#hTiomD8MEaW)TeAknBQuX?f3kF8*?^gF2=2m1Fc1 zQi;EQ=Kh}s_CEdIu)nF$zd#VR4-@9M3ob@b4qrq0-eKtn0;e52I zuDQVeV9etMv(2>iAE`w=&c^i2_Fsk{?&=%L_+l?Pnk2>ZJJSxj3oggTRNM-B^yV8< zm@Wy+Jf*HMYLd;=8#uVbpt2{)x%_z1zC88%?jygBxH?x*jC7oLV+*E~O^h~X6JQA2 zoMOT)Rx(od<<0e>hT+*mGNA<{0?o|k+yYs9K${<2+k3J~^@q6EJ0*jWJfp@LfzG)L zR#x-Lz5GJGe#}cL1zK@GDf1Tvhf4!*zU8Nt-qyS%G(hhn$_eZG?F7E}+;3HO+@|c2 zHvhvf!^AIlxlpy?6WgQ5y_IFnkIg!Q&%}(q=N8svYbVn?3GO`ip)5;%OWI+|%1@RR zSj!+Rrztbm`XGJ*9>mU!%JC#i`;Xf&zg+?C(ykVYG8q!pa2J0D^Oj6Hr??}MMHv6h z{`;dsD54l`p>RL)kugM*um`B3(%a)NOQ#$nL<_dSKU@Px zL~~ky9L}PcI9q>se?2;1er{9P8cu85=Wv$k&gLo7SlY! zP<-*4oc=>myE>ytn6#s(Ub$XVY65PC#E0nLdYjI5V+UAcdAFBoQH%wC<1#RPa!Tk`QZL zN)i8g5b7ZBi~%4P9Ta+VB@QRxCC>jKU2f1 z4_a`r;^jsw;(vm2i~3^xWOzaUt-PznbxXQ9pcGo0wEY)Eh3?!K4Dvvce@x zYAg1jCX&yL3&~jW&ii}faL^IQGTuvi3he65$OLgmrgOK_fE@?{g2@pbJZKaO0vC{x z0tPOy3c8-|m?(ariM)I1VklmDbe3&`_=`Swp9f!9fEmmI_=hCL1X@grAV|pw!bIK) zmwRcH)N~_&#Ypqb*ZRX*S1evJseYlJEFsV%5s?8B@t$kj_}Us@1zRe~YlO{{X>-OD zz0q_E(ZsQDycNwz1sTU6W9A*I6 zYO?XI3Cw{YJ92OB8<$vcc==^~oOD+s!uMRoWn-!=@yPt>Tv}TuOS>`8a`S{R^}F$x zGe`$m%vO3N);ufjom^~%cLZh&WAwUZOcWz0&)&g7(WxiQ47eT+h+>56;L0E!3#d#Z z2_7xhQJM$`&}bpg4n&Ql(!ck9B6PejNjdPlL@4=8cc+|vNm>Ru#`gU z6%2gPxLeI4YtOYmfj~G(IuGIoPVY;?q=m$oMra-b8Z6i3>A^mAr0x%k_t&2m44vbJ zpyuUcJ{pDe*QYC^RB3-4G_fn;yLM~vrGn!I5s!!Ano#;9oAxghS76mUSj6dX>W!bc z;&g5ylED>76f3tjKS0Jm=|6F7J;+`b3TQmfgMl1+b)K-@V&|xwZ220a?P=9==~+VE z*Ed|g`Q0JgsmHmTBx83w9&Hq0c$6^TcvQk{6UD_{lwxmsdo?_DmO!$R`g&7;%yb2%p!n(8b{ zxBT(jt^LUK#hKFXBWrrA2YpAh3zg+#Sy*0LM!5?)Bwe6ykmcy8i6877I<8`#ATt_X z&?QONvpp;V_czqf-8$>+&Yk(kG{FBZbIQ%d=65aA+NUv{uGB*=YF5ELZR|V)e2J=J z2cl9=2Uyth?FlHB{e3q*_`TfUkoUp-Ld&I>vw{aR)m2nRPl}qITdz^;EmoQDl&G%% zdyS4Gb@6Dq_*$RBYEjsokR- za~B4}xcX{${Ee^53uV|rc0uw#?%UPsr6c(Y_q56v)01tHhCNJfF8`qd!z}xQym{4g zW}Xeh(#wrC8@9Rp62Gp0_R9UOK_mNZdFRehzyEOP;n22%AHu#q`k$QN_ddCv!P+2{ z^XXWdbzw%!<-)wY20WL%^GwkPUbsFP!-XmHO6_bYxJ~fQIAN?6F2Vu_4yC)-kF?uh zm&#tQflLo~a5c=?eB`+U;WC}O&-NM4_ zJ(jwJcfS2@q%ur+d8U*ER3u&|#vc?>^}iXW!A6NZ@q5!~p*)hv z7&Z5sP)@H9g#nDFe@`8!jW1%&pS;<9H7h$igpMzHELW)$F+x<>hJNP!2Cwe4c0&FG zP%>iTx&QIGMLsTb@YxR^1j6%XeE4wp_leV`bQBJo3i$-iZu5P1C$gz@!wYBxaDNb3 z4m);=4aOR%5{NYpF2HM$%YTAX>VEx0%dMcsunmMmEdbk4gZ&;9&Y1pGz-&aqSMV1b z@IK&oh7nO6;48$~WV^&gdw&wMGda+pZo#1_rJ8G!g;N;h8^Vb~pb)@vH*L0rRy-N9 zf6vzbv+0qxo(T->RKc`@eKXZAZh9ExO2l7DL?NSL;ws5sNO%ZNK^y1V(VaIB*23&a zf}g<8Ymx*3gl&+6khJiCEywc!t5_JOJuKUR^&_u$LPazO1KUa1?qqmVYDZ_3>|Si8 zwi$*4YI9 zxqyK0;KGkE#IxW4#9e1NAX4xI>oHUi{{iO6`YdY6fqwzeoZ~=+B!mJlcpQp)dQ8aW zOI}=-wo+70le8Bhq3}Wjy8z(*Qm<~yZvbinE1|c4Avw2FpXTJy;FTvD{M#Qa&CIla zuXCEkQbc*@f*V5>T;yD1Qfi(lv!kvvrYEO%7E@H(MYDoiJwqs2?%t%vt{rLA*#@|V zLfY)-m-^POV$tSPQORs}+&DQ5e4UUNM<4KieP1kqkW%fj=ZrYd$(4mL&z`!cN}U3z zQLujMMfhj?XJC_y9Xhu~IOsE8Z7>M#ZPN+lla=KHhjKH<8u`Uh#CR6iOgrwkjaR`- z75D(})_7|)sT-f`K&OrS%1R+6_ZxO&WH~zBImrcy6tdZjbJ$rv714W_%A$(b zV&N;Ey*U6UM<_!F@u|BCW6I%R%?Eyn&m`^#(ibt($tDNcGWKHVoJ;tF5u~ojO&8gO zsp9m6C7vs?Nd62!&=&&LuY@{aDrZbJ$K3{x%~zpK=n z9de58Z^Ug`J<&rY>!Q~<-*^;nxW1b+a5nN1t#afF!MF7tUF>t zzU@^fBM)d7R2S7%9Gt4^n6?yMzJxB*3FS8KgH#^kjDHQ9wXdz$zqx_wdeB0%mCkm% zRP_oYOX~G!MxVTp)}zl8Qloo&rc|D#vPkXK>5I>YPA73zt&CsZV7Z12g!gV!**mA` zO&0_XiEnA863{>8Vv5`z7WiT@f*yX8BbRX8)vD?1)l1qdhh45eraT)SuKr?S)cy_DKT-ogV z-#eG5YO`Zs^#t1#A=u>D;Cl5eL&FF0`N^;NRWDNS(n_{F^~E`cum7|aqhCWuvO5c# zt9QTu=OJ_}q3*9u>}AaD6Ni4$rKRZms{A+%DeBR&2ZFf*!Mnn2Mvv=!P?c)P&smAQ z)vx+<4z%4#bMhWBYLmifg3w@ghI9ln7(P|@Yiv7MYBz&67l zr^ui#fzgwAo=9B|nK{!2K~l2cgQQauVMw+za(00o<~@wV1VX(l z*aJt2>*(EZuKp&{_JXqzUK6iik${8oS(`R)B>Fu-v_bH7VCTPgC?!NUK^XZ~smewL z9i%S?QXPCJtRD#QxCZAU5p>A#KX-S*$Zcn57mIxgS1S%>g%rzHA*El96-~p0Ft@q* zO^76HUVNTm6ZYGkhaQw2=r$zu7lQwNK(Oz)-5O@U2v{5@NWXsHoJ%s zT-6W`5waXm;s(rB%G29zlUZn<3PEYqK-3W!R*s}{eqs5tpOh*dyt1wyP_D6!4eS3T zn}E%+2Pb}Zz%dRF3XXA62?>`)<+hm};92ny>_?F4_Z)r>oAn2NvJvPGQlkAz2Mc*# zZtq_W&^=|x#Vm~|ol$V(yPz3(;C;O>C{7$EIb2Daxa2t#pIw+FNYhn#Xw&(Uk3VY0 zg-5Cnzc28jDhQP?1W9I}7N4(0C=gV;H}Gx%hTj0z5k{V0&>{g$Q2V3WmfAX>f&w;t zx0Co#;?r<1#LTnvRFAh=8g0^zgxZFs3vk(>{de1UZ`a-Sg6T)rpm!hN#s~KB;5C_@;7NO%+G&)j{W{inwu8lls+rQ&Eq*C1~HHY zskuhfTOdKghFh{jQnuwh0o>A8u5`&u?~UIV%3`6_{)8qVs++NM0hVj)Xdl{$7AlqywD@Sgm!^$lRjth>+D712dU~S`dptNjYr2h*<4F|%}r-} z)brBVsHe{QcY-)Cw*A9F_tzz!+I;WE0n0vE<{G?{mKCB_A6a``0{B zL|W?u&ncIfa=eUg9S_1bA#?Yi=QJ>SR?=Ln9h&c4FE48y9geA@dOW{e^wB_zXDC!^o4Guv9n zcl(7^ap1V}6Z>bEek=KG6gElS&p&wJE4!m|Yr}HlfFrHRvjEzElS-iszBHS@ybV@8 zX&l^h9cyQU9K&m;PWSR9OJnzS|NkP&pw1qGSql5-e@mza+ts<`rSt0=_Psy2#Vr5B z^L;z^b3Iz`A+^0;o{Ny0Tr!pv?hlUys7DN_M(Q?h8x^@9)#fnL8M?=%a+{l~p&M4q zEnizMIuX{+#5%D+4}-ZJ>C z8HJDf;RI&!t5>LDHj32iYszR4{P&XgF;j2o0ZB@;S&4ngxiy=Xghqv2mUwbS&?4`9 z@@jYf76~Kz&#U5WZ$;hstln7hR&IEGYAJE=Z|iFrZH$h^U3*OaF$++}{sRJKy_{H} zqST4!qwKx8*!W4kw~l+36JIGd6#WT^?3lRs9n0xSXbt=<%iB9=3Az zlF%k@gT7i5mq&qVzN0^*v$OLYI%Zn#Zgl=E@BNGy+aZ#{w&Jo~U$>9_GCn`VdI~oe z`m!(o{q2w%bx>;NI27&#WW|_+lLNvUk^w0dE(ql(PEm4=AY$-p*nsK_5|xj8lokgY z>!zs1CZeM*^@TRtJN3 zO-qbh)BaaJ`U@LtH=tGu#DX`$FRMRs?WjNG#yGwSz*yeS5?RE5tym-+lD-1#XK(qH z@9gGTx8)rH5CK|d@joSlKX#%0O``U{ceR|yWBp&MKjwq`9i{QI6=!ZM`mkmOENuyJ zE8*W%qu9AXXl7WL@BQj`srBoL$XKd1|H?ywjQb6wwG9eluS{cA2?>ctXaEldS|arf z>-4dCOdstB``yoB_eV%M=>f%Ol`KVIarTbjqdkHb2H7swZs;MW6$hX(&7+L`oy z6SiaEb0m_B^3kK@4cYJDbjL<0!o3keKV!Ihw826jx#ScW1m^RH-!w3NB4o!^LeA9NZ)ULmv! z0}Akzn(M8lxp^ev82HCe0NNzYCs?aUNO;LVS=YfxaLYZL!?E+>j0SVA4an7gl@w;+ z+m69lQ-r;~7%JRqL-ehRxreX?HC1QkiCT){{rMb?nrwT zP*Jh2w0mT{G6#-bp=n@!v7+kjby$p zP4$m?^LnJ(iB)@v1jsFvcRncHB>?)~g_!!$n}UmCj}>&aEn(Z9BBDc5Lby@S!nTku zcHdQlUwpD!h@uksJ03G4`gysb(?g2CVV(w`7#XAIn{gi>*+Emn&drV z$Y{1WN_dE-LAkIBx>(bJA?e}Z?oKjkWBu=f zM8m}E(yOoX)g0se>7~`wTBF0hjhZi@ZzaU={@rLpEOXY8Fc$EZZE3h@vA2A)ZS3)% z$0u(w3?I85Nag6PrYg;p3^qWRfVoKM7vLm|v^Q_wfcMo%DcUV}@1tuAARh80Ca7`Ftozu{TYS2a zAwHnOy(jFrJLOO;1j8S0DjwllxedEvrALfZe8e$mF>%xoi0JTMeVD>O zmu^MM0$>{Y4H6W1l(TUek$OzYmIWmO*}(>AY_Nw=!+ZAZ;W05{+B8I9TzrBli%Si3 zbW};9!@qf@8~=-hxY~SpX4`0%m=Vpyty`M`w;6h5P}e~(O>79uHMBw4@5d%D36W}eF($*m}bMvpU9G%Ew*SLcf70(`) z#>AG#XUC!3C9kCYmiE?GRcGhC&!zaZU{w=R5b^){b<{X{F5v%A($9iA5n*8@vx9Xx zzC69_JB(b$$B%z^XJ~PKB7kUl2&;iD5ys?$XT}66e(L$lIeE%^+NtBGFx}|W@2|D` zGcOfe&}f^c34ht%!VzQaOJc%E0N7z$$q-`xfU=|xa^dOnxd)*3z`8<1n+19hRBl9@ zeF#FDdh!stN{6396PvH9V@e|z_2{g6HsUrEp$iHEh1$IbVq6e&BtnI7;}=CE5|?T4 z(~*|T^C(8tU$Ml?Ogh|2D;J1h8rXy&vc_uiDpO@UUgG%9T3j0>x*NGI|LXD9)h{R^ z9l*8bH{yV$U(~n6(VpxgF)t8!X>?Q)$q5AJuFs}rL9ynY=R58c0)a6lNVW6xH-3gY z4Re|YlSZNSN^=yhwST;UCPWSm_x6U7G&Gp+w16rs?wY@so{Ns*l9(P~4zgEeSehY=7a&l)PgM&Fkquv#B(e>+=NbMxP1R{g#>1%a;atf`u|6_h13eO^Rt_6(1Nf~Zl zSs@90M0X>&2P*8Cig%7OcCg#s0-O5$MtNeK&j6CulO!xckgsCAG7SuiSW`yeTg+wJ z@^&$VAsMFNW+LQ)(W&k7(Y#0Nvrpgo&nz$*aW%!v{7BTT1rZ9b*&*It)pJx3CGgCg z-US98q&3LbY+&Kg6VB&7!ka<9DRFONuyLMeU)eyZitHea3pLKlR_(bQCDMoS< ze+D4~PA<}0j?8MJq$1M;P=YHYU=DW*A}h#e0;3$!yJ7Ca{@O?|L@2kvD;|`pm_hIi ze*RAw7l<92R1{+cCM~ise&nP&RMav^-n)Na05Wi*p9hgMwfjopO4A002~G+OPbA3$ zAuei&r3H!m`6eQ30Yt2Ujt2^{ly^=q;87T^UzA?0BRM|sg&elw)4BnO4xC2|)^p;Y zyfN@yk_4|~b?3zs=e5@K#Sjv8Y|SJwAGjGsT?j3+JDyC%A1X_rZ5`hg@&dFYy94Io zo>y%lea3lx&cuZaef?%2L0`{AhITvEu(8o%33JZBiS7+YQetQux`PC;vfS zN^E+%K{*k+0fY>Fc@Ybnon3aQST%l9NZU6_zF6U)RJF0$24wIn>ofANUcH*VTpBI< z@-xzvVTge)>C0#3E|Z#l7-0ab;Xz1#WCZ^D|I!KpY1zDaoTwNke!SaJ2BFiMpm!2R zga+9F*Ar7Ape38~K1tBj-wpt|c zS3w7gmvVLu(wVG={9j{oXKsUPkUFiz0XlgOKS&cE~c1**q2KzzXK7IUGit5 z@Wf_~h3x)Aed>=jxOv=%>v^Yh?w{DiY9<_7`{8|s4E3pU{UL@xz2+lrdk1Nsb@?vb zPNM0T*(uL9q4zTDv-QneD!DVqp6dR~4`mosxAnJGZ=WA|{j2Qr=wZ{djQh7W?>HA* zD#I51zE6o=T%7Y##@epMzeDr&d`bs*n_XbLddJIyPUJkB2V1kpsqhiid+D_wu6%W- zXOFKIn`Kf@&TRZ+|1Y-4n~a|?ujC7e?x!2|qN|mO=K+Vb^_D%K^t}OOSj}MuGbZi+vH!=U5&F?tm9>a1;Ci=#y zvy}WE#k_GFf6#Fse#v`{U8>dWS(5rMSG!Xe%9+F-oz^zunT!fd$zLk-7V*sup?W6z zZ1we{sz55nLf;w@<@zaK51xshMDcg5RjY&Nk~z4eBO~1-e~T(59#nnHDYjkP=ff7! zG6*gH6omzG@Pw;93I4KGaMV?o$?kPk2j|s@@Fy2^7jJ%6BXCc~=M*R_wSn?s+SJMHvw`>ZaP5r?PowHb>T7XSGk+lV5ayuCi< znnO%HcAa+r9GooL>h}o^M<2-F*Fo=7x3qn0sTtEufkDCcU8}rlvagR>WYoq-WGHDn z%AQ%D*II6!cia5AAUl=3;2U5Rki?8kie91<-&}gXu}Sud`oA;T&+W%XR<;&?RdsRs z=gc5lU!1$oF6?BiJ7wX_u}?fx;+1xE9@nh%c%qCR9-Y*p@iW@w;>~WCDPXTWRm!+w z)GyB57Ju5{iFDfyOx| z9B-ncqIZr<6)%GgG_Q2$N1BKB78j^TAm_ryBDm^Gem0drW#~b9X-dAF5w`zVCHdtXBJ>pv0e{0&B;>6=+fjl0o9>ZNonTHp|BPf!i(N zc@dh*2^bRiZ`D?zw&wSjt)ye6hEqU#N>T<}Kls84NFqZZ{6ucG>ak;-fcJOl>f4vv;thfh zvFz*G(lP5A7tgaC>i@|ThTHWo#5UOlgF%1+krcLk4`ZePXi^V6=^7Bdog00bN?vS7 z?FI~H-`OH>ox6|FepEH6z8Nh)Lw*f%XRiGLz5_yo8jpjPPn=1}-P`iFFMaXbMq=>b zY}l1%=L(FJBqSqq#A!QjR6BU5S<@K#p&%m7uh?noMpvO796aB`3r8SlUl|nkSj_DD zOHN~|fcO}D5}-p8+J7G4-40S*Ir zAIWDQ>diOX1$%~C$!!#RVBU`bKv;*ul)K%=(^VY$43t&xpihND)B6d`y)cBFYrPLbI_ZGSckw5>xAQlY$`599{MuJ&dtoGJH8MyJ}VDZ;z$)r}U@V z%gs#qebM#yIb-PG=|8kkvB4D!Dhc->q+fqb8`Q;u1`mfS()xPgGCYBx$CBkD>t|fB z2rvWa={r8Tc#(*TNz@%eOY$5=f+K?$mA{H}dM72q)-s!tlnXpkmT2R#sQCf}rW z9xlR@PX%+Hfwp5p9;N^|;n{Q|F$N3P;K|H4%eR<{KudrCg8Gy7HlE|{@niiaTt!zN zB(ufHPjG9x|D-W}Ci*tcF4|%DOU}LjuvdZ=R){)CTeC9$_?}fDpUiWe7|w6CSKe!~ z3p*B@e}A-2T8_5lF;lqK+r&3)A*T-VCw^7l$nO=-w)fj_YTF@x@1jBB;9sfn;kWxz zx5jAcN-9h?e>%&1kg>s_P;TzYqnu0&g}95G*n7<7cG?vR&&G3n5C2<d*OvLlM&?;A4bx^l}FMyzyf6PMTOn=lihn8uh&Nf|m)sds$9fbBL$ik)E*9 z`MsaxG4+mdv&|CsB#sq6K9p;+CElT=-GDor+fW}JOj;JU$#D)ZsyCbXe@m7s6yP1&?DtQvlZr?j|FdIRIfTbZlNyWyQKA% zTR?>3nU&PjZhN^kbx>a&Ba5`^r*i&k97G+i{X4%MQ&+ov`7W^g+eo>I=U+JevS_`Z zVB%r7kTImYcHt539=>fT0{mvkZ-SRcW50*ePGU8vZKo>W30AL7d ziD%!mNyMUvr2Y~G6*Td)s|ev4O+8zvhWk1Tc; zY)eU!?=}r0TcGvj%Y?P(P~&Ztb8Uq8m%VHcK0(oduV0`P^xnNI8OJ(ie)>#*2*-!( z**UNGwWC2WTj^wQGWY)0a)t8*e|;=qxZ2N?Jl`d!jzF-6GT3L`ZhV_j@dC?~&`R3N zabP2j=uZp>c18Q>kQIVvqk>Bmv3;^5t(1dol@84b@58%HMOi&hQD+ z@A{5~+LM62@r4u!uq9aw_{)>$Lf^8DO}Ovxx@WNU#=Y+dLn11q>aY(?;Z*olHF1E6 zb#fjlfWjnbV%3PIsK3&P1U_22j+_dF6`B0q>E$3MCDlx<9bLx-Hf|$+ERY7e&fj=h>B;SgrR(W|$)Y)( zLA}qizU1>-;Wj)Y{6vN;L~1`G6tD5fbC8x%tn(C=`L%7=@0%6b_`+E5aP9%}emuV% z8bsMsHNy#hcJ#K&xDD};4pQvN6ch+BBD>;$hI`3Dh*q$G&ml*QiUMml(RASs;Q`8H zyz^!@wa!{e*=G_#K;%T2bbMNy*E*uWP9ZCE%W|dJd1$Gp6k~9p6U`V~??BxnqFicqM zKybv{Uf?8A@=)JLeLnMYJTJQE)&DA#plJMe?$An@6tqDRu+d;M0qXJ8_Z#wfq<~nArLY$__dLiD7S+;9WxJ3@|ATNLlI% z&%$@&`z<&5Yje7_%6n;OPbZNKfcB<}*vMjri$^aY^MSx&2zMBo3E}lppL%q>Tpi0c ziNeD?gwN=IMeE3KxM3W&(+~I*ZlV!9r~5rVGXyEne}VG!=1gHuAzL;+SM*#xeNoW&)dUp*pjlv6hjXoYSupK#vu8te%kPjVWO zoD|?l@GvNBxul6%g_JT8QET=YAC%(7;8%qb7Bt(iTQU^_zVeQV6PO5z*8+>XIC^$h z5tSD{`yv z-mKwt<7Wzd4M0rLni7KFjQ*$tzB7IXlF(4%X$5we;21G6O_=(rZD?LFB1!@8LnS(= zDSFSzv~7+vD2CM!nh0zTP?n;lDiUvM(AfSpB$|{3wyic6Lb8k$TNbcl%$0#a>u(?o z?S+9V+tzn@k&z0&5py$hWl`7zBeM-XZqSG9#i=AXpyPEzJ@Ef&0p6m2B@{BH|8AO; zL*#b=mDsN$_4J0R<)xiRKpOsw%Oqmo)JZ5ouPt z<@;#sNt{-I%g9Yhw8#{52*(`-O)v6)5?H*UgTB$~SwZ-{fZ#d>Q1|ONI5>>8iDV*w zq+%*H4_~_Ev_8oe##JRf%R6Q3_5%TepU`bjPhmVx-EYtKkC3u>;K*r8Ya7IYutNVU94Zr^Te9fR(#~}G7Em|1^k07!PzsEPa zpr<^M!U<6d`{|iXEhiWuiPn1hZSf+cN&d&^?U>kB-umuv&GOz%fnTYb>u;hSWOclq zaRb|dv8V3Po#C}F`L?(cbT>RoCHBd=o6*@x^I42^A!?yGwOAuwIhc|I_=Furt<;YJp7S z+3%J|rrHv9eZksdySZXhbbl=bWLofNXAWCJSlidaq}s?LtoYR z?j4{PG}d+;-<#G^wq2=DGrtcrHjR=uAT~U5i&tk640Sm3P~f3He1Rp-?!x$GHU*Ku zsQ5P91mE31fQAhINf9W*Kr+6aX7ix_`TemBCmPw z=I}06+x_g254W~;1?x)^o0uqax#f6Knl{jCLOa*IVwkI|i^;=KogN~*JU1_70 z$o1P4Vr>~x9(TIM(7f>Rbe6jd=*x8x3|iX0d)3Ti?$&;I+|0%kUUPGZNxR{0!J723 z1chH$uW*et)GP74J|V}(^09MDOrG7B?ra=IW2n{VjA*{f6X9A$Y|g{xF~zrn5aqn? zL)Qsf3!_URoQeJZTTjI-<{q-0O{UJT`q}GH({(sthv0^wcXCR)i&=Ep6(V_o-t0FA zy4m-ZyK8@s4&u}qe*EgjN40mGxc&EN8rMc%D_Z7bzLv-*I^MoP?N890ve9uF=~3O+ zN|Hx~!>Fp$?@y)jaoKNvRTALj+n^P%$Bwm3?Mzzq@|~{AO)}Qh@NjA9`sd2?AqWPz$Iz z64~b(aX_cahbDC*egyal`8(f-*4EZye=M+Gq?&_a33_wz9x?ZVPwu+g&4B;FbB`I- z4(6A{s!U|=L4@)ZBrfD-1ErsL!q0<_lkLFU!nG{_LKOs$s}_o#G07B3EVU!j05;FV zrlxeLLj?%6anmLN(5skLQFUzNf&qXNCLoef7ZU$v41d+V$_i{w_BkQO(*BigDwhdw z^8zoy9b-{~CSqyCfI+O<*nYU8!_C}0rlB+@~T zUf?S{w*UWl`VMd`+dpi3SGI~|lZr?Pg(tJDQbM9oDY7b~vPUEJISe(w9auj}_af9Gi6g!=Tu`D+u7?MI(kX@X4#)-!?z z#YP~=q5)V`Lu#udM+Qwhg3(bDY!Bx#Oi@`uA8&Z^15pjc)dLoc&l9Uly;7MlMpf8I z{JNkH-Iv`85N*{b!vS}Wdfijc#UK^(q^C}+;}GhS!8t^FsU}jcv!h1{-vPu7l&bMV`9G!q`k_Q6@h@lZ zLiL8oX)V-wiK4bgkynATJscOa$TRR3OP>X53LVg?;PWW9Zr8@hw!BlUsN?qC64lxr zmzW403C-irTlRCosM5UHU7S$-=$~AA3WXpZ8|)=RQND{OdlLHUfWYi5M?OR|kDK?{XyDG-^04Pga1Y@lBqW z@Qno2ygnN0VG^cG`Z5$iYmMHt!1<9Rd0BT$MD>#~BhZ7A#H6Y(e<30wD0NLA(_14| ztiZ2U7uFcgNZ0wxTD|l~FZF3U%*GDoEN;5}g$tWt*@ExQyWF(-^k$3{F}`}4DEgj^ ztkIsOTPo)se02%Eb(@eCa5bK<=9R4fK;iOe3gaX%Ajr64`?4w0&-9Xs93aIn86G znBx)7f_1e!FNhnZr4N##$HpYI);rKBls|Vme3+Z$ueD!kTR9~wf7IeAUEg{lgmJDv zfEh6YHMA77t`L$bqSI3(EIu?eq{12KBz_)nR_8C&?gJ_C?8v&&=2n5m4bPD)KJKud zI>g&BFFk(q85%%>n}bOEym_*lrQC{gB)ABLn2g(7U4>k)Vr@fC1 zxy&WN&z1C$F(h^6m(T(2M}>Ww-p|KN7Pwu*p<<==QR!#SCr zL^}NquDIS>pDe%eKJS39-L6c!Dz>E=vo_Wr_ar2u^~zMa=`IIKY*AjwFX28X9CD&5 z_LmlBHiUBOI#&2~+TiNIChWD?0;-Apvd4EZ@~93(3nQA7hqOpkHZc$I-5uOm#V^9W zFg6HbJ2rf103W3_^y9^i`CkV4lYDk3nT# zUzI3{4M2&A!$LLuCEuxDjx9qYZ|yUCWC>eC`fq9~%OdAUKv?}y31l!QT3~x7zSp2n zkRi`A2K#5&KW`?@%7haB7lHoZkzmrf{oXFeP&C}Op!gS@ubsVi^+m@R3{S8Y2p?Gwzf7UwD!D!;zu*IR8bi?bpTr?VuDZZ- zJNhhuBom)tm}_951^)%YX`nqqXsPhPCRVaIY0>gKf7r;zKv*=`ZjcM^R8XJ-eGN_% zy#5q)n8*#@YIzi%3$Ut0=#wqBhn$Wv?DPcLCwW+iRs8I64!JrC8^4_a0g?zZFh#-^ z!)tPEUln8RQ6Olxu;PGFBtdLDq+i5m7bFmpeT26jdj}D$K*l5lW|B&x(F{cU7~_Oe zQY!(m27Tw_W?|>=H`zD`Fo>M82xcUq%b+u$MKY{6fBtpf^RJ>f?VXQNm zr@(Ye`H+uKk7umbFyt_k+?!Xg_ zBz1rbp9<1uBGhISNK{k=Zvq8S5GT7@B$G+e1rR&%c^BOt>^K?dloO(KX)V}h_Pu~W zQ_AlhD(Jna;s%825$rTD^CONXNOtoFf3f`qmoVX7AWYk#@^-5_17{D3c@<24wD@Ro;TXc3ZYTT)xG93TX^C%*|!BO!9BI_ z)|Qsraf)D%V7Cy3bM*dnPLON}FDl)pkNLD#zV2u-K*yrlBW-3|aw?4PE3UD8`o+#Q z`twLFG0K6O($tS?oO;`#9`8U`wi;~*x_g;fD%`?KlSxcLnX7{aBeQE@rpLMQbtOwg z+IVfScDbnZ6%mKkuF5|VN|=qQWvubK!=BXtGAEkG8Z$Pt>|IV# zYzphy@TE^~QO=7Qb&=<5X#dnVy4g5bds=<*P6$8^AVKhJf~PFDUMYlEkd2gbMNiCt(oJuoYAP<@c%sTl7j1}}xzib^3KZUVYW%Spk?(OY`>5H*WSB& z{A=KXj+QGME&It>o>$d8TJyIo6)1Aoi-fBdKJFi@uF=ls4se-QO?2nFVU^9%BFhwg z`s-$6SCvHPzr1CdIZJHJ$9D@vF(p6I{dbA&yF;=yv=f;1+ukh{<@m}_f=gn4tqXqN z?Ovh#v0&1(T_Kmh*Y$^^;U>zeic6jT`>L6$bXv25Q4taGKeh>Ud@^C0T?!`h zA+5IP-idJXF+zcMVj?fj)_kVScq;VhvTJ5ioMm)^_xG^(IrasG%{fph!yExn@z2`^ zcbkfJpJDePxdJnEVWd=sKUMl%V_(@^cTl?%?;g?o)Ep?`NhX^|Ntd0E`riRA@YpJ0 zssOZ*7!H#*sHE$C#3!y7uQ5KrF-USAJ}|z$zqOO3y%Hl^yDI`~8Hu4TpwH$wPa?@d zNA{+?XV4w3E=jN_e&K;ui%e-bSh&%1%85qki-|yIMl1q?(5tA(t#1+Ou~X;E*!NF~ zw=@pg?_Hv|{XUFbBu0MtR{j%fxO&kJdcm7$PxlbsX^_&cLJdTHr`?=P)s2$(uBp5r zSP!l(iAd3yXTE9ln2YdaFKpp#-0q0L$2xu&h*&{3?E})?_R>uh4YQNt{a>#zXawhz zGhXF6Yy`oo5X_HO7FfIO*b5l7%SjEcbXB4>59Zr-jYtY3FxMe*x%hOvc}55 z@$ZJ`NJvUSA$kMi;zg($4OcJ{IsN`YI0kA#Kc2AZs#ih(NkW&2@_G)PHR*o9@Zf9` zP(a8nSErWd=gKXVK@eb`TM+{`Rj+nEL#u5-%s zlLHm=tx~mxD_yT#K=Glhfx=tbFPWk=HXUzZzU1U=)cbr_%z(H!zs2;?HjkTg2c6GP z7M26Z_ju1?nA!51*bo){4g82jHHI0cZ2a_ko4VfZbr?_SdqYmvp817fV?W16Clk;uz z1WQ!4B3;}c^A!=c(YUSGm`vO!X84i$de7Pz#A<5iOwjs`S>h9B`-UJvo7dUrWP7iO3>Yv2S?q$|>BtsBG$nb+~ zKokfVLB{1iha862-ucAFQV{|C#mtNsi^<-02*M@=fZx0Kvf*Dt2d1*ckHbiV2J4#P z+XEe4oS&yYJO`wJy15dxo)J%hdT75ZNU!JUF>#^uUub&*FG;ZZ1MYAg zHNOKN$s;Y7F;(4sZWEWFD3kv*rEifqk9khWSV%tptp9c6A1QTwXE1^Y;T1_~V_SdP zu~NY$7P`g$!qUZta&QKx$9}1~=G$_t}h4BP2_HnhQ(K$zg}L&^5?8(E8tzHV~9} zGAY!Q-}wSW%pJWsn#2(NNtq?06Byal0yxPGmlqeOC!z#Qz+sIqCP_T_bAgdzBT-rq z8J4{akvQq#VA~Q?`WOza5K0IWRcQ9n=QNo+MZ3r#6EutFSC<@{hhV=8JdN1@fW=X4 zJvy=O>V~Zt9O4g&P@0%LY8mbqIO=(m0;d=2t5e#5lSEEv8sYdLF-8C*!O^RFa!7v6kR zRzATOhRIQTQ~ZF*;lp&jvdP~GV*-QPyi3*?jCUjDbkunuQq|DMp^z9no_Q~DalEsA zY?}iwCgCK8)$0;DG~mw2`qb>eQ|oFE(iM>TG=XIb6fTV(Cibg0XWfgf+6o={h$kV@ zq@w%88o)iT#Kpwkxx<*^+^^$HyW*Zp+(L5Q2lopagL}S>90U*%G|Y~{KF|v-{v-;F znsbGR90%jKnC>|HNvwU=3k|+G$W<;o2}Y*&``u1r59r{HcO0}rOy?1ty(RiKJQ;P= z(AKx?3GP>xPJ=WW$P&3)JZO^Y>!9)UP*yEImElUcg?%+Sr{Q=tH^1$U#`NNNuP}a~Tpr#GcdE{1tK9;m{ z7&#MnPoS<=j}}+{2B9sqBRRna2Eo?P@01cp5bTB|bPIE2hvtX$;}~kfs$?U8M$ex% ze?*pF;^iN;*i~@N9Q^#Q6|o1F}pYr73V~KU9#2vkAlku-_fJOLv2RLAy2v{1SWvR)B*LGUyH| z+T<_DC(1e1YC zfXJI(UHbXML;cF@y1MV!B~nYpJAW7`eqBtK37P209ADSc2j^2NWDc@Ph$73cDK;863qr&0yja?}=b@)&^a+`1Qqho#eWGzmc;tk7*t7Ch!?*-SXK zZYvynzhZR|03h@8M#7BINMx{rz6qfe(^_C*z^vLd@+$5KS(ZnZv33c#pq^hovL*yG zi$?&}OR2PBG4++IZx_-}RAftF;0e;tWT5ivf;_-XSk}Zs?EDbm0gc9+ufiCdQ0f9% zpc-9Jg0UKv*X*gnG^f9WQqt;w8mk8sqxd`d%iBwVJ$sj)Le&qn;PB$M*U12TNT%`5 zwV)h`BDmsDY-c)5mq1ir=mz7Zz@a&C=y~HOhGOGX6Ct2l1oN3gti&5j0gD(zBPpr3 z6cK7=4))guRPIKr!R-7cTtE4rsIxXQg>R1JrWIYzGRr=hYc3LzLbXklN&F6@UthLO z+H;|q$0kyl6-P}Y10@TprZY0$pQ)yQ(j()T^z6BuNcnl&rF`MDfs;PQni5o*6S8jo zLKnX9RUF`ql^yha`z0VUiJ_8si?7#HPb%=96_1Y@ByC&64mNdH~*Ljy` z{V^0eF_N(*Eh}rHKqA<;_LI)8T`}4k(sNdHk&I@UXTF9lr0G^~-njEr!eNt$^?E)H z%kI>e037-e)*8{-$nsxzy#AM6iuobWOG9`BUfa`TonbOpna*%Jw3feD^oz_3Bio8a z6~WYCT*Y!ojNU!zpgW>6F5zl4XJfRbI)#*= zV#8eurDwM`exT8}=s!@w@|){Qjp!QzyAUOtpHfd~y=AwW?7!7iUeht1K5qB3`rhaJ zBG1_zt713ZFuB{ZhW=!S=s)MlNkPN&a_e++s*m|Rv0I7CH9t<1%9m=gVVudaDt8N; z#ttFvkFhI%uV_a7RXj~+4O2=Hk>9CqRhBiB2Ju?1&9e-BPN9DoPHtD`)LNRWZH{MF zqH)uzQwwBL6O}in(58-_mN?VBw(ylA0|;jO5#K2?H2W>S#M5q?ygI^ zD(ui=e$x#lV8%;GBq^sX!4Z_+WAo(he=DYT``+W5m^XNP2my!Ww}D&y!; zt7Bs${C%AKk%En;ufKt7KH`M~TxIBu_mJ&9%|Bk6DbFgDi;L7gzi~i6Z{k4e@;OYv0^6cpqP6z73aL&`qgIn4#UU z&&=cGM4aWLH}g5?o!IF1P>&N*~6mWts2f9HLFCf?xhiC!#PGRGD+102b zshVhX6&7LyD<9eYo~}eRhb&(s}3=r7D(p@ES z9s>#zd*of_7A~}dm~f-#W(32`i&gSK1;Q+_rbxLCb8^G_Q6j1qjaH*cKQ!{f&Hg!t z@q}}Sf-$IZkYZu6fh5g>wLJCyJ#Pule>vYJuea(JeOqEGwQb1dLpx#R*>&mgq_{V& z#kWf;i5Tw_ui&$Oz^wUE253PVf8!iv>Oh@R4vR$NL^2KBA+Y52XD}gauZ@? zEr4~vIAAnjJ(5$^ntdzBjzl9#EF|p4Xg_w?)-tKA1?;pBh%!=%s^h4G(WDbcbsPg) zAukE=WLI~Cq*_IX2HU~(mNEJ=F#(*zDni2pvsQSC92pQu$F;+qBm`|vWb(l%>dZv!MU^1fI-@M(jAmu#w z7eBNbB-}L9bxjM$PhweulSVL>bqf5uUeKP?2SXm6fYtGM#i}ok{C(3&Q2LS_1pLGq zRjf4BlXh?8w-BGt37;pc4QPo7v6dBB*hxuqE)-U58x@#HbVKH899fu1^dS_CU~xei z+s^_VN6#jE*BtmtFRAV^F-0ckrg(WA9ApAZ_~m#jQOnKD%gFm;8O=in3xj~Q-T7p6 zMKWkf7GKmQz>9=)xL_o7abDL{2aQf<&a4-ZHvC-)^ABAKCy~=t8hnQav5_UQAHvQgDx23T68Ng@2bRb z+`Az+#H%-e(QAga#o{H&T0z}rT4ngs`2pZGLeZGmjgF91af`q5^F}Ta`s@-gGBr)Z zRFe}B=d1_Rpd1D7lk{)L9yVhaB0?=xYFS5al&-EKG>~d3J%sz5=m)64$ZSXFQS1v4 z_e7wm$F`7Rc{cab1jd`yc#rojhTmA-WxQeK%r z(OwxfPRi7g63vCyxO0iyw_4GQ-fv<+VroVm-%k3R8Ol5MUlX-z;yR}8frVB*Av8V> z&Frai8s(AY)r0@-T%^A&AN#!Oc-nRlu$6AxPMnMziu1gw^`_e)Qms|osxm|WWZ#+z zrZhDh%+oVx}WLF98El%(4zUKLT|4& zt{wiI^`HA(AvfB8TlCK+N9#WxMmg?lv)28Mk}4~0@)ybYyY@&+_I91?nIXxqRm{c( z<$@zj#1wM>-tX6c$*y3(7#<^XN8IKd)?Qgu|kbybJ#lmW4hCV#g}&{8eNVTRAmTeWDYr)b(w)Rjm@~+&(@KygYlP3Qsw(yHsAIw z#Pd8Hn2%rI))bU8{^X&5I<49KO-_N=!6r5x^?|cfVoM<{$;VaO=JHqb?{}E!1}RT@ zjWW3uoHC?76md{!%u!DSdr>6`%-a&@+5ij8h{#~BDkUe zC>l=vy^W+P!-g+>;RZnb!-(w09Ig(pai_;Oog_ZXsRt4PWMA?Z{yxErG+z;U)op{f}sk~o5gzdXJFYd484 zl85Q>{hVLf8v7QApA=yd7?+5*9CrRzzzjKkFLME!Crt4)FgLJo^yv%X!{OsVb!{^9 zRj5@mxdO;c4#t%x@RA(tjE%Z-5jWJCf}e+-zh3IygW`Z1?f3_GiTb^q@c!zB-J}lu zxQy4I$BqgsP&RldW$o!ir9i^Yi+Z{JG`cHJRM8~uPTF+~(XQCU$?ya+#uy)C<&=Y^ z6*XBwf#I&sp5V41iZm3D<`c7i<@hhX+IHkn@AsSNDp4XK7bqm&-V-PKBa(3s0Ydzi z9&DJzR}<}iCmq|TWE{Cf!vW|HWCI#!$NxKS7-iL!Nxgju6Bpz!kW6|Sncow^Zdu7(83r>-I;aZ#jHeV1&{8_Z9rY9F0zu#eR4#`7A+F3*uuMU-9O}GMyHJcRcfMYI zMKeU#Y&}!KN;%R06Q>TS1-xdqKAD5uhh$hym9Hx*JOj}Z(Ga@S$UL}!n@gfYaB~a0 zVdg=MXMh|y|KyG!ajM`40A_VCc_Ig#p&GBK&Pdp$XzI~A_;`!LkOwQ%JxF(ukzX_V zKM4nE%W9@k0{^H@zY#^;C8iFgiF|VHFmsLyKR?05=2hLS_M|)`)*Mlqw`b6IJTtIY)(!)?E!~Og;jrd7yHM z1$;nkPxU163Ij7B_RWenvw`Q38V2J?f>&YML@e1+Sflv3`5U&lDWI|GGi|=Uy;w+Y^H{S>oa0yh9SJR60H*z!xfTS2_Y49d@OJ7aWJWPj^6-$Knnoy z@vPVw_evy~eBezWMXfG}TVMhXZ^R%6dmKliWAHQJj6gGpN*f~$cFY?I14Y*ZNJK2%Apwt$!BrB(wyL0MvqYu@n!k&TTenFDR;DyNz-`BCuk9hBD#NN@o@P5H z>9zX%8YC@JO-z~|!q5j1+!=}*YR?6J=Ibc%)|QT+KfzV+RvKpe=eRp6VNJQO%w1+_ z3wo(0aGz-w@C$&%1uRlHa}>qP+MWnG>>-f$ZV>qcxAtyjQ#tDp)|_JQQ@d?vADvm) zou4jd!;;i;hsI5+hx{u#H_>b`{Sqvrv*`X?*os5{Ft27ATU z429z2cW`nlSKV6`VG_{FIhoj=zJBWlRzKP|!4sVK=C1mf$!ubtywk|Q;Q)|>1$ z`?K<3LhC{;a_~tKr(*s}d75>(y~BwP9;QexJ|34kZmQW$Yw4UWKKp3DmB*p>g{i?j zXZBA1GUM^sl8W-aNE5wbNh^9@7say!dp+z1BR{^`%g45QQE`W<-W;W(%XQN_VUfL2 z94%Wh*GhA_vmS2~AT@L%6I#s15qp|V@?!JL1GP@F@@Y)_x*MC`U5cc>Rzn@}TI9cG zp6ZTBf2E2mpMt#uTLOi+_cD#eMn<=uOP?`F;$od~E4GUF2p=&pFyUbcW+5BKDxa>-Hnh!e-B}_H=_uM=d>!Y;DhsmNx$??OPe{|TzzHO^+tQGl>pT>Qo z{0kYtz>4>QrC#GI<=NO!wlaOSdV+S4d0SUR(~*;R4K-K79Rx%(J?B;A+7C0>MvWh0 zm-%m{C2q&|zGr(ywlS=8+I&U6;->H1QC0D6jmd7@pB=mxADEord1QEdyv-wT<)H07 z*3?OHfe8dSz1ti9z4tYQ*zoTg?&22#BwR!E54>Lrt3*CXWOe-Dn6Sh zG%LYpV`DCWgC$#;@%S!2{(=9e~+%Mfxr(>slZjr@?Prk2#tvi8>$uK-dZVvj}FH_S7PUKEo}{r3HkVWIk-D4f(}->%#;DYKApzR zeTwTRJ*ZF9?xyJ)XjmRMJYGuQ$VU-($riw5PcM!6H@(*U>=DhD|ecT(FZf80dqgy}1K(KpSPwg4kR$ed8|db*?;xCAq}Qw&mkG_RU5iB8p;1WU?k z39VDC)ELf|q${qsS+j2Io1Dk;^ZhEqJRyw~Wl8hTxz8;%Jj(UYHV^&~xN_BUpZ^W6 zW1F*l^DLjX6us*y-j^gwVaRqn<##6{LBeOCq38Cj`kEuZADP>P^4o}9G+w&248Oda zGpBe0A4F~nQ==s9y59u2n4bM|!NJWA+;SEPl3U&XdgiWdn2khH@vxvEJ}P;KpXjk@ zV8_HYRijHw1`r5z7UrQfeU;!MfFDmwg)-vdwT3Gc7%`x6qTRp!yL;;`HAkX;W8ZBNQH25Y*j)-rxXd->Jwzf9hVBIxNfecRKGk0I8%U*;a;-I;? zo;n{7Pv2;3_RP)_H(OZif8)&yaY4#^W}&P0Apkjjf*dUZWfwl&R+S<4(bok$E~08X>`4+ecy^v z!j-aSwTr_szhV=UG=4aPd5Sst8Y4R;zUsdOECK#g@s?G*+EK0fe&ND+=kB&K4-7Y;!211AxnU64&#yMQ(#dMpwhBLW>_aGmNk9yd zDPu}>SedJY(09OUd6=2Wgzgvz2k{*TQH124{_ij4qgMb~$0&S%%>E6Nc{`8nzYL?!msXUSycOKs!#g^#y?U4%w*N=nUUj-z` z6#)VjSEtM)uaZZ%0Vsl<3k654SR)Vu62?iuDWx;OE&#b{f!_}4=aw~j1rCb$Va_lKgzGy@$y0Wl zG@Q=H+Pqk<-#SN3N-^i8`C`=C($WhO3URIaWmO34737C?d-boO3UGYwJKx8MK7{lF zyb;=YPl-na0c*fo0AGmYsADQT+>*tGpLllff!v>QH;xj;;{^E5Z3NNYuD` zwbaQ+9YJ=8LlgUDBqp#&a3H%wzR)T%KbK*ashlL8WW`+-_>PLBBjNsEIz#o zxKr=lGfA32*Dgg*^C3VQbNPRDw?h5EvVq#2O7Mws6AeN1f zA3wt5B_+^@qhj2=d9IVS#qZ`mN)YPIa!gvj& zW$8A&XrLRA{vqQU$^S%);nlO{)_yWcLcehH170gqFM<_xF*{CHXn7E`YmiWZs=hh5 z!6->h-a%yo5g&~ErQ?D)V2FMhcP}RW;T%K z(6BbD>hHgw?7P5>SAFIR$en2Ypjg*<;g_AAJq5}(U8yT4>OrUxpuIJm76d^AHJWLY zLCR@LRJLl+RZN0P{Z+oC>tr4Nj%y)N0-5Uk^=<{_5IvO6>N)%opB2w&sRQ$on3`b- z)>&ZjtCSGa&f6*9&9<1MO&K@WxT%)xAlB|-UF;r`q+~dqBF)VL<}uvftX@39QtX3S zjzZ41m|oRGk3qJx$g4n&0;zZ|2*ODr`+xbm1k&^K&q^p0wE=j%L|Dwu$9D~|59RUa zUDqqu@82Zo#NI@dLZlf6e^u#lze&&g-}Z7`3o4v8l8T?6qy|~h1Qczg&x;9W@oAXd zuyy!aL|lc@;|mvPnRLo5xQf>Y?#@W7SWZ7sa(oF54DTNt<>AKbEmYz7j zY^77V%M^v|G;(=7Vfr*4B1_E6Ld@pE+e~_jLj4oAaQ8|Ets7ieq{)48Q%>?e<%_{D z=2{n5TRDhF9fV$`Bfo{!(K!>QX9r{K_!hxb&%~{Us&2QSO94d_GZat(|y>&l-{!cT;V;-m$h{Y@gf63(#lx;82Xcp5Re@Qmwz-B$8pfD z1xpB6iQQ;RmYQUz26I%u^2M=lJjQ!wxxxB3ER>>%^k7|MQ;G!MUBS!Xp~6>V?~Ta7)JB-RuUVM-@P+1xGPT& zHbh)$5q4}0j2}7ErOALRT}Ki?u(lh{%>8N)B62=^`DOyb5-K2ijPSjWOj;6^XNUzi z@M#cJs74*OVE#ya3NdGuwm6NwSkPzYAm9iE={RIwkeNH!;NUjbbJ2y}j+`IegBFK) zU%Q-~EgHw%6!=Fa92M;+77@ElY>KQEgRmfNfhYe1U>6@7+@&lh=wE@SUoOxK{e?(G zYk)OBh3Ud0_>>6#)Pr+tju4GvpRv}W)Fr2sgdLR8*DSB9h-Cp_S?L9S3(3`<)kQz@ zK8@L=L=|-+_W30oM1g%~$|jnm+=EBghx3vqk(OjVL3VXu<=J6roLDx!c)im!V*`i; z58QG=oF!0wgKglwH$x-Qr4ML=@z>qomD-*jQ^L7o@@E-c+-o2g{Cjd@`<2PQ_PlhO zC0$=W=~sd;h~!>FiJq?Lc6FYN*)aX5>j&*1tXl&>6ZvgGb4YrYDR3Qj7&M85+C=z9 zS@}LG*z`$!q#JlIZ>C?4udN#%NOi#g>4QnKZeT_pcycYjD|! z`>c?rQPyD+Uqf=a?0p)k;AcpUX%leZz;>rUB?kYqJ0R`+Gxw4pA! z200=OznEeE{_>?Vf{{1C;a-)>SDOE}EPO*s7e=%HC5C~)Td^o*0u8};EarL$5!N7c zsd7&tTeEyQY{m!?UQMU#au7yBCqX@FQ8Hmc)(}#?sxhI5fd$Hq2$HahH!1IG_v!qT z{dJKqm;ZE}ZSjIq&)R#C2j|YKvny?XB*eu}zP`@$&lT0m>fM1{u@~)#|D9v@vpajO zi;4E?R*Rg1Jy0A};z4#jtZbh?F7uDEB*o1_{0p#?Kx9I&e`0{3wAqNVnGV8_Dj;FB zQC5up#0>)rDKV%8D2YOvXuPPOrqHG{iMsRMx09F7Z zKutvwJ@JFRFY)jf0faz3Ml9_mA1EoHsLFw56sU)SR9a7&so;{i2KN;orgYASIq+WN zV8(stYT^jOX7Qh>NTxb_pBv^3SRI<)LpdjFNbIJyud;l(*ew0sOae%PLVWeI-%Hj= zDuPhpKukPi7XNS&4it8vtWZZs10*5AC-?4+PIY6dL^yajI;d|kx18s!kL0g^MB7Kv zdxn!_E#71*6VK3bL#3`@Ty}I;8mh$wcnX0nQq|`wBrVNCb~UP%@EzOygjddEZy<1O ztAhl>%xzW|_|^yt+ldNnMnee!k{xPY>bZkL;6v5!nd|K5)u?9S)4G2ff%JIDjOm>) z3+u}EM#1KYVOZ>sXO74MA``0UtAMVMqd+I9wGEp5VI8y&qZL$W*#b;rr3b35k zSMDu?bXNq6;v2a;>Vreo5}gLtLC2|o_D=e~Lw|QXZX!JL&g?SkPv`P(l4%uA+a!0sOGrwV*vj#wIP*1HF z*I!@cr$Xz)n)xi?JJM1~@Zi~RO1mx2;!M^&_JLOWBb_dYRR2I}K@kUYx<$kDM!$_sHQ>oc3_Ds;ou{vkhv~QJP43&>s{4S3F$OKmg zE~%$+o?hSOs`8{*q8WG^*+m#{-h6v3M!`LDjZ0_#o2$>G*F-*Nddu?AM2gsdppc_t zs_4IaPg=#}R~UAWhbbwQE;eh;S|?nUy-x2mrU_DVI7fL@G&F(gX87VOh6BAu>`zuC zSfw|d*{!+n<Tgk@_Wc91ki(CyCl%?6 z7dVQjdY_+IV_cxEsA8|y?s3^J(UxAl(uY=}#f+0F#O04_Zc|w2@``_}^yPgOVtNz< z<(bp{^$UCU-*wy*F#Grx*P(Dl<(Qqx4Of_aat0TxMazy@=FG+X?&D{*mDza+P!&N- z-~6r9FfrSXq7RzB-+OiG>gx!!2;*%U%N-3gngvIw{5axXm1>%DZKPIsDYSkq`?;G} zk6X3TYh}hxW;i`;W(g4+>-O&-6&MKjFh6Hae`SSkK_+kh@^{uJLOO5l4Qskv;&y#y zExbo*yk^xC8%f_B$hG~y1O};(v+wx%c!ig?n-232uM6`yVB{b+7hBGTNcKnl0h9at zf5={K{Sr#y531IOT#pz~X$9ArkTMfwr%Jo3@i)wYQ}0a@sH#GX3{tXQ*sy6iYSJm0 zT^JtwO?8!5z{1d5YOrI5p}XZy>kE3vs700+R`TWLH@5!V+uIl#x5ifZL}AFU(;+@l zOcDdWsTPMbL%3HaWS6?;=Z`6J1nRf1_&acxrS5Z=-5RsVBA@Z(K>Txu%rk+^Hg8>i zz2cT6=3$@w;_qo<*A*Jf$n%whO~mVgUv=;)&lhpi{I)~<5%AuQ?U2p z&vr2vuK6m*b1v6nPAXN$biL1Zg@C-dusxf%&7X^9E_iZ+DXN@hSHK^>k>3?@gAxK? zZ}=-j*>lD`@*NQPBI=-~pyP2hE1u2J!r_Kc;E6!{9Wj(0yboL-L}+al=T-^Nif;<4 z|8c?IXNqc0;M&0PCtsRvsuTm6Mrl(i*QoFR7p`uk<-m5F{kOrssOW^=^&A^T9atu> zc`xppOQNgS;CO%Gph1lu9p#gY)-cz-XYB%ERO)&3dw8|o8xep)vglQ`)9^vNUISCe`b=MX6|3hQbI3zYg0R~E# z4S+2KCxD-j*cdm=P5j?pEVg>-KFC6B$6A*W+FH4?yhz=my*dtY4!GHZ$@%}wjD)^} znu@rAp*-x?6Oy1kIhT_!xqU7PF~hs#}E>5 zsY8wAk6U+nLQQ@Zb9yva)F!%%XpXXuelUZcdU;`X<~QOw0|+8<^*tu~q~8I|zW?_g z8o#HSPh=Gxc_pX?oh&7U%`5c3)belpx^>#9n9DqPe!_1Rae6FNaGyy1T1rnv`sVI4 zejStWw7lXo0Y^;s9qP3HfYGOn_QchI?n>e@KYt*E4xkccm;ZoK;xH3u_vN|G1kV_4 zCHR1-)P5H0bZc(l;>xe=xzJu|E@GW)^-PD|D3SCNEC6=XZbz#fQEC&W@iXf(#9P1Bwz{r;B9sgMab#$T z3L0tXkP2P5#?-3@7}6Aq8Txlj&2SKEMQLP z4$QJ!pT0w8(Fs&j%03GPVq+}oR}M5jnRp<8aXVN>QJd9CHUYRVFs!wl!5XT@w~Pc< zv+;0mqcY2LhybjR&zoUpx0q120bv&9CK#gB67mU_f8+EjDuhko((DBg2IvnzcddXj=&nJ*!E~>T@(K<-!vKi~a8h5f z)Zcu!qf*?;zDvp=+^%C2R7;2of(9Zo;AUv0hi5?R-$a{1K++D6c(gU zl8Y1c%k3B=FCgA8atQQ;%dXIyg-3&TNdt_eRrM<_f}6E(6-KL<$y(^^F$oI?ZwnS@ zkD;Pbsfl78KB^P2fsR3d_aL|SMLHg9YaM!eo)C`uNW(LS1?uTT`M7yvj_jie-7YLF zCKeF0r_6JCrPuR$QFiu!U(-}Sei$-MPAkbC9Ut0@lI{b5R?3A7`6*UNCg6gafcU}} zE%&})01S!kVgn&LqyERQ?c28-{arzlMEDwh#gEgB8~48cPlEQ7Z7z($S_X4{rr35R#eT)k#g#LJfZ_+wiv+FFGZ8M0pEoj{eb~X9 zR&Fu3ec;OXxd^QvLOqM3dt5XgY=e2I^uAyPRg}Ewj~CX7pO%9|10sbka*FP4GQU}= z^5(SYHTL^T?$PEOf+dc-tQ*Ri)LJxt+y5-cc7^%-i(rF4_b+&-AJW$sJ+`_;D<#&g z*x||ScCr2vr(eh20@hf!Us)$oSEOHPkFk`0*?vW;{*GtvN!GuAi!i1#-{w2QD0YrZ z&ilKpO88b$n|*7hVfX=W&@7L;>K^7?biSMMB4V8N@IkOtX<(H$-xU*9e}_Geh2K(K z&DPHEHY-%ZX$9^RI0QU+4Z6oWPZu!Yl>oS$Cajg!(ZYX+1ZJrJ`FsQD<72S@#fw>Pk-*PZT^A}}hP5#u`e-C>`N*ot^(H?F78ai8$IZaF(J#|$y%qh>ur9&?)%FgG>u zG*cMv&a5I!hj=OVc@|qm#m93)1!Hm3Y8h^VzKI2o9zAM?-l|GLzwFlS+r$UQ$Z`P8 zG*LOZ=BkF>6(3gd_l#n-uL?(R+kN4b?||$ZdwD{QJ6HHk)2=C_3&AVU_*~1iFX|v$ z3r@KG!P@ zBSU*Ei+$-`4m7Q%>;a&A|BDaD>8-TMExvIOW(0MR`=GIteznUKxnt3L7nq2z#U8d) zvuru%PGxLj*HkozI`V+rf@j)C=$b&1+I|e$K^#GxpYb;sfu3`gJ%{NH5l#}+@LF4w z{|?RXF8R~V1QP)g4b}y>;b%y2AQmC;2PQCs`neA@Bo<&!@qKlmVAkT6E%QF#77;-?Xdn3bUL!ln$9@+gS<=qxN7Ips2gU%MDu;*H61M|reGeu#Tn(&8 zc>t;d+)PLsklT(#Z$KnF;9l{c8~1k&)P3+YE&jmBzZ~Mvr3+gtd|AdGOC{B_biQ1A zz%XF@0Av6WMip5vrgrY!`H!5Uq;&KXuQ;p&z%bOo?1qpbB3=jLMD2%$mUyUastzYc zAcU>#<)uvcKp3256h??$Ct9b5o8p84HQRE?6Boh1sOuZ-<>EEhL38nE6cUr7w z60Lma?|oo{pl9V?`e~MIWCRlw9AA3~;}R1P@lBsG5o5-(l=>$YgRb*-#B$@RO_cCL^}6_wL<`fwB`O&b)v< zFPwc|0xcMpo71;~hQ@Dy*I>P>fiJ}XzSZSuaJ1n|VlTe|C0pq2+q@X~^tYc$Cgh*9 zR<610>oMIVR%jql|G41$*CFEX8E|Y!Neb08CA`2SvyOl-umr(mhOoo&QDjI2Cs-0w zC?AR793jL8LRvSsKPlJeG!A3}1;@t3{FCy~C&Bgu%Q6)f{1{bkr#b;@4hew*Z27>+ zzdcVvLV_T6SmwS(;x30`l?mQ=D z(>untp|%4sRYgqK;l>T{5QdJ6@vpQ;$hB)E1$u8$q=7^Yn0zCy3Hydc=DNW$zF4Y_ zK_e75Z{=4`vhPK_!Y@;6+HmObRJy~wSxVNQd=4(72tc9ASrvX{1Y-hlEJF&&S4xI_ zlE2LaobEjIH!>3kVSL4fV!zcDWZI-mh>h)M+Ib~b+h~VS??zp-{5s$A_R0zNZ5#j) z*1q7X`s8iNpR_S1{}@+oWr9z%c^Q38aFbEYA@7rskJHy$PBy-i(dyi?)h54Q#@arw z=b^SY%NPGm>>ln-71x$7)mz7PPDGzS=6fHj^1rdZiu+}E*!7J+t-ZMVn8Ml!fw^>wiSc7Tf{r&JtCbH1gO?7 zA8=CRdf3(d!;~@|-X4(9w3T{<*8X6V+xY9R481o8m;;{s3k}@%;&y#p{?{7<=b7VL z8TtL1Z>*I+s^(KmMrk-)*~p$ePd)Paf-qB%u^8>y)xj-Rh54$dPBg?-eEf2${_CqN zi%*n3OsPl2Y8m&gJ$uH_#ic*dAmf+)d&VBcTk0Hfs~yMMyXY0>If8kYIQF!S&drT-ebG_!pJIXAxdpvn} z*p{rTVlxg){#YxOcJhzdd7*jc(4xWqn&Xk*B7;wy>-ZuVZCfj6{&pSR1~)e0V0OyO zgMH7&vOZ3X1*nC7d?i#ORi!0FdAXZovPO>lF_RMi{Iz=&rYfHKA4>QQso={enUvVB zCI;_|d#Mm?)fe2$Px;ezo^_=v`wx}Pf^MeE-_0w-s$)K#*TU?Wl<2}3>c4sx3{`c0 zJMlB4%&@$Q&C}{naPQ_A)2^}fiSAz>-;_k0gL8dr*md0v5^E{+y%(SC3H}oT+cjIj z+6oLZZU?F=EG;c_E({qF(rReeBDQCEI4dsPWjgVlE8^u3vU?#%15FRRiF^SFaixpn zCHT#obEtTTYZJ+eDC6B*E8c(+ZqB(@X$2)E)!?7~TpObL34x9nVKg;0?d7cP>o-lpnH?x2>!;EEkGa?h{}+k=szLDET} zrA_?meal^r`<6co_C&T_Osf%>)O&iwT}^KGES&Pt0}qs<~tM#<)0T3U3MXNJK59%2{Qk|+`U z^+4gY5Gtz>m_g#-dmZ14Z5BN^k`=u!KEza!#I_TWhJ!;&)%T8$@P7EJ5NEKp!O=~` zs16NYC_DpNtD>zTn>pQdF9a5TAJ4SIEM*hAoG2JRPaqtGMXOohWBLI;qISg~;NcuW zs*vb+xYzr0)RW%W27AKGg?ITH0FLYPOAwYx94`Sv&~-ib3Ie0K4-565X>r_Uzd3L^ z>p<--HLAI@(xg7sf%2Ky{ljMoCfDroN8!FX4`Z}ZQ{VZK%lph|LMA6&4el!%%5%n| zH3uJ}xwDh4SIOow7zbWpP{U)Gv~sI^<+~wl1u8L#l3TS^6LlC;?st#U#>6Q6DaUl4 zOpa&r4J3j9)dHA^2K=%AHgaDqE>69zZf$T|@v$$(QTaXnjy31isLn+a0E}B8OQp-F z10(us930H}h8wdN!1vX*vwNibca0)kI8hXYhlZc>`Fiseab_f74gr`Df1Y(1sx=X$ zePZ(VaPQHOx65R{2qccf>8NLAl;QL$kcKg#4{hwl?!^Mc917PmbEcj zeD<;WClP$$uC2{)7vSF5=wU3dQ(m3~tJs3~OBATpH8s>$o5h0d>8GVu@lH&QJWjTS zW{Fs+_SXJlQBrlId&bHs&mN-3+H*$qu38!|+ft@{6_pvQB%R-;|9VT*g(J3St>i zdh;V-)=@I9A@g?q9l4*;f4D#2F1GaFKiJ-N^>6>CfWYkXsyn}(df>=~@)aF(%UyZZ z(2%!ApG1809yNc=!Uea_gh?w=`{Ue#x>ut>O2tB|bL$ z8dG)=slgqIwILFKGW=6UtYS_32dy|6tVs8~5Ue=F2;Smwqc|ayto8~Te`~ABo^$qoc z_J2!GI1sbM5^ErrGr>-~>is1?++niz?ZAQi(}kw~KQqP0*=fJ9MI^g?VOH(9*5*0g zGq(RlSX1`W-hokXvvH=uME`_PP)bx7{*GY&_sv7A7$58oT7I7w>g?S1*E z)OJYmVJi3jo#xz|#_lOY5SX}r&t-+ev)nAX+-$c5&~g)dIn+pKaQss8KRXLcFa?`+ zV0nQ*z8cNZ$-jH}njN({z3h{6qoSo}X z;1lDfdx}{--~*&$)E&qLuaJKk=Po@)MH0*?99`igWU?kU#%us1>v?o0BWd0nVAqD$ zn_Ie=#d5^~sckbsAa7Yl~PEWmj~=eBVE8k9?95=bKL z0N_RDvn_pLL_JP$7SQRgset)a4f6dpMAsgaKY|D*oKsZ(v=$!_hz&XnwMKj?R104l z{(#X#@I^SSJ+GnhCn=2wWXtH{8nh}z$KFp|p^UH;oRI&Y+G)Cj+kf=*7(ktRP*9#FTM22Y3c6b0l#9?CaL|(?x?e zSH^aoD`8&Bus}m3gY?RM_w!EuhXf=TBn`*+00v$9`@t_`qOT|pT>+{~*endq{Ceru zzXPQ%HWVNxuAQ^&K498%R+H-h<{t3c*3D}U?fdXudb11rP4K8`RXo=$B!)C4-D2zOZgqT2~+ zz8b(7kbWK*bCsjA0QZyRu@a|4n9w|2ex94lftm&))cnsc${*WVL5YXtTtWrNYTDfV zCU%a~&=}jT;FVu;3r}!1fLA65r~?l7%>|B*j?knLuFdBO@FVul_tKL1#SGD4!bw2q zONgM$&i|*i_YUW}?c;}$6%vVLl$pvFMPy|wB1uL@l4L}ovPH7Vw9gbgVXrpT_%y90wv71b+K32ZCTks=%YJo@LukT(uw>(2?1^ zgGbGIiw2T!5K&kK`;72NFQELb5*tYH9Fd8hHFl=~4hP5`O|_TaY_LY73uF{GoKB}rD7y-g zbTTqMFu)%It0(@#BP5Cc!QZ2j6)$Q8H>nm>s1ILnsGO1I{X0vuvX>wNB=81>-3{N4 zZ1vf-nE}E_LC`N`G6l(lt0ouc4;~5gBp`R+TK^YEiMR^pZAv6t1j-!Jy>`(eB9HFx zb+oU-8Uh>Cd1TIEL7V6+-2z1MVLGGzz4$ckR7p%w<}NNQz})F2M($dJ=1sD zK)y&6CEz^7oTYmH{9yJ2c53q2>2a|$gxc`J)-n$*A6nnL%6#^LXU%0fcMmKGsog2{p3=H-VGrr0!O^MuxJ5pC&w}yJ z&46Tqo$}^DMpu>PsNC)FBhESr5DVa>_(t#nEsAin{&yfG4 z1=w2Zru=^^61@zZC4^`r+yZ_YyXWeQPD$de!yontASmi=D6$hjV^9Tn-BQ5L{hU1M zL~e{NU>>L)ph7LEWv^=85trOrfuDLMppYEEO%%j=9t=a7fSA}Jk2i2pk)B&1VmEBu zdJ8rjyvF4Ibo(Pw8JSHRl^6NQCJ9)F7TS%`a)`R?{@*e(A)cTy0vJi;C6xS zQJ}J+6urmeX`)Gu-_DvY_*21|isuuU8}2l^4cVLVwj#Od(A-Ot-O#Hk?T-8(_>MFo z^+yl6!~?p0m)X){8oW0BsW@_HsrP)EbwX&WA|^7E3;H{bd4ErGbUsli@-w)|UBK(5 zJ$GJy)q-Na`DD)O_uUmCFIXm-6&iZXmmF?#+CQqf%M%{t*wnR7io(%9_CI5<@|QM9ARP3YwGW-bmhNPNm@v zcIgx(i#3z0tU~k8h{tAEsZSkT&-yto`V_r7eeGJKfCg8e_R|=3dj1gZ^;H?S7_oNu z(5Q#(B)@Cu%fWE@YxqC^mUI4)L5`kPQ%|3(UHoBj3+a=CF-Dhsf{!SKtlYU`w~6)r z7dGb;i+|T;4P+;$k2IBR-Y>;|h@PX=z^!K|=j`ML-^t(o%L=EfuGmg{uc@}bQ+@TjNU(lM!f5d%h2;TJht3}oi>E^G zU%M#~lSSzr5!w}D$db)=7RkIz#`Iq1!3+{@cqW{6Wi@A>9r2s}E5h>q2ae^(M{jVgrN-rbz*6fNfG=MIYJs{}cV|VieX@0>{=todB-nLrhAm@)s(k?uP&i#wW_9JoT$@G>@R*kIr((v|{k zcV?3b?ml2H`{mO)xV$TW<(eNkqRN}|+U5Prjr=TsYkBGrTgh1DHF?*<(-Ltb1Lj}n`vZ>VETkgm|6i1IB{CwwufT*77K0_a`S5Pq>dPZNL zk!an4okDdeuOVbpO@Y)$l0;AP70{fuj)GKr!lH@t2iC1yze!!bz@xny-41s&jF)vF zC1?isEA0Abxz&IZna4MK<|UZ_L6{me0n9v@W@Kc9dLR_Av|`-RTPAY*_Wj$03!J-y z@q$zG&qF(M?SIzGUl(@W7{H-R*5=fY;eOrwLD>*yoa8!z6kI;QKO4dXh=lZgEkf~Y z^UvmQgwQ#(49EA&kNp?lwkWHtlvzlAA6N{CI4LNMUJ*6;o7w+Fp@;~?ROsh-0QYemUKGG; zk%X#~z;Y`AL-?A9#Qi2D9clF-KZ{x>7`43=NnjdLY(fYE1tFdgz%iPL(}IR=T=ml8 zuY7PY&}edC4nkD`Di5HbAPF890$G3=W|Am~L>!wy0tA#EIjihn^Xpb_fFJSF0BEbF8eaK~U!!Z7)-2On|0*gv1Eqid6mj7VjsDnNk6Tr>;bxFANfAf_&kT8Ex|l z3KRoD`Qt_dwR5f+x(|1AIp{Iq_Z0!xqd88ql1EY3UJWe`aWWUyAD6O$+Ls7bqD33` zuS*KHl*m}8?#!}2=>Lr|1sZ3jv6)M`wk)%OMZkbU>>z+9zWq+?kJyoo{*k)a?cU)J zxE+i3Fe|Mdo_K)m0Z%&`AW~!ePk2yScJ8>p-}67+tAKOj6BDmuBWQ_;5+18zoxw`V zt3y@iPKqoH3}Oik!&hce>f&cCiga z2MQKkI_Ko{7P~C3&SS|_VoFLpfbG-SB+qi23Sy`uS#TL^+-z(_hI$b3Cbc6jV2?oj zeNFQuS$Ig06rQ`4k}{QMOEPTn!XT{ENI`!X$tCe~+EQRJ=Al6JjUa4~)b*%KzhY!y za9q?)oj@MkI85}y4R$WS;QCz_SEaz~K&Uvz|NH)@#ykMV+fVQjzqFf~@HZ)g7Dv%z?QIJ&ms zPz|o4fyWo0-?Q@SUY|)iaKgvAAK$;9pB_{L70<=oVwcN@9|Per4BA4Md*Dww4<}ej zz2`d>b2eCqi0S?8gsU>X42Erzw#?;q!)hlT75{M8HF{R<0lp`@*q>?hi&On!F5k1z z@pR*H3zphr z&@vs_dE?geEZbY*GoKtzZ?({Rol8?UvF6y};)G}z0CM05Q0 z&eh>d{?g9Y-RN}JkLJ!|ch^`*`1F*ZId4Mt4XtENi$*>F?Rkq=DkMB@4zU=Vh*1(z1(pD`$ex*iIwr*_j(HR&tw9BsD^ruXeB`ZfH(&qSJ zp*VlK-M7R)UAg>P_T8Z|6@yORfxH9idTgy)b|W$QV-8clwHnly^Pl_Zq}YsTB(61H z>ph(xj^GfdH=*`Vx)w)&t8_3$jMlXecOHMWvqNOmqd0L>bVX!!@TfFrwNNC#2G2oy zr;yN4+qIP?5@^a(aQg(2Z4e(hgl>{g&pL|M+joxqQigYclxMRC3k^8?lOS*s{RIBf zWIX-J6ISo$iE2(j%o$9M@Qr>k?02ifq)Vu+e%he z7SSyG4+|Op`l5&Unfe;{A6bQVpfdsbv6P|gih@`x4)9erWyBTei=}1YWG6WjIHrgX z{v!7S6(hO5dr99bfnrw;uDTmg~_UplXuWYQg4TzTct>ySaD)& zIjJ86u=`};nmUXPHDHrbIwhLeK0myLc&P1I)}g$x4KypxWUz@^JRo^WZ~| z;|}ps8_yt+Pro;}SU3PVqNDf%L4E-R|*dv^d zV}}JC9tDw}#q^CQaaxBQx$ne<&@gh?fbf|2Ha++jt0fmn!@z5!pup`w-gL}NyNX!& z@R(vi`d1SJkA}!6O2euF2R4}lEdOa^Z3pZggI0H3p8Hws7yJ_&sIlsTbh@)Z$B{fA ziWTksUAf3@!)(aJuSFkj2nNt$U^uR27W6`H#E1ovA^F!tvZNxB-3Tz3#LlAw2#a(b z_=}mOiWQnGWhWDCd|~!6LozB{LdR3;i0gt#J1{NCuWI%5xE4Of?TvtrE(E$j(fIc5 zTav;{a3#ERPI)2A$SN)+K_2AVRIANR`*5@NQ?EzDZ`2Wyrxix83uZhFPZXhw0OGX{ zlRRaeJF22;!6@i$^qx6Q-O}F&5?lwtKBNTM*jV6c<}Z!syXPyq$|ihB;3bi_kQ#D? zR_Q(QxDtlfjLVRu?UDd8$hlm{M9o+|>e$qes7oXXBH;`o)obW1)q4DhZSU4RjX>ZZ zaN!JhShzi*?NU#C$f-7yly!^%udps5h69*4EDu%fA{6tetVLv)sFe{uiv-QeyDye& zn{s^jO#)mW7~=%|2%HRUNe|Ms3qrfHw&QLD-~l@ZEmRAvF9ZuDJ`1<6_?DCat3$m_ z7#ys0lOb^%jG-zZIvTtkvAw~6vvDzw7zQk}!LEXh1Xp^s7ncv|Btg*E!zRdjwqbTl z^K2Z*-jRfS63nqA@NW5}Tfw76d!uIo41OwzHzqnR7^*3Dw(wmRNkXhHu1xnv@j{(V z4BkEY$a*2c_L*6CI7dAckoYML0^=pUTtKjhP?Y#R79wpx9KW6vs;i_BhmB*mxJ{1; zj|d#rC~$$mr_s^uFAlz{H_0@iXJo7fx{ePUQea8kK7xq(1O%u6M?G?jFX1~5Kq=X8 z;Y8T-Chi}0)3;<3MBzFHzMdN9uCuKE4VWX|vEcqawr{ENgTD9k_Vk>Aml1ML(c2vg zY+vDy`6Yo*WtXqrY$Ty}5XEKs4t9ouFM~)CB{rafrJdS#hULAR<@cOPaXA+v8^cd6 zzQ`8Dveo{t$hss+lU{}wT6HOR4=J1Vin%2VQ<+BTFs@EXVv@ZgbI0*;xQjy$;yn~t z_)H_mo>^J7zs#7{IQKT-BR9RvOyp{9aF=cg)wjYL;X8$X?~_VgKIHDAQD<5$`_yuG zI^C~+*BP0Q>udfyG{Liemq0<_&#tLgKP&3Z&bn;f@g$zT(aqTAKK!LWnSJgv? z&t5dIr|x=#h4SpV98q`g2-S1JqoHx{P;fEwt>Cy+Ua6wZ7R7SM0klX}4^d0UeX1N~ zG2+|Nrn(7!oPjTX)z*>p-u!vtaaXpS>)nZSfcIWx5X+2>g>U~jJ!>6HFqd7%QV2C` zi#;_X>-xP^RmpdEyft-xUL-eHDbiMDqOE>jdyaBNOG0<4%f-n$?_Ab;H@UlZQ|=j2 zqy08pGh8T*`)v}}XHk~+{j@*x^W!&%wMYBB>pp)tl2LVy1*!zcsx1Y+Rr-0tqx(AY zt3ObCQ;X@_)0?UFnVNHMstOa{+V5Ig$gY&ho%+kcDxCU4_X>?^A zssaZ9ZC9{2L_9r#A-a@WxQ88Vb?wW}vW^x+3!kfJBmkSNHGP{OsgW z@wnjF1DD4o9@!b%FH@EKWpq(%=A}|6v+#K6G#;~dIh@mSu7{zYMLS@$$zuDR~eK#seW(H{;yK&VSKQPj(FzL1$*TI;Yh_L;U?X&h-lMM{qlFII!oL^ z1#>$*;xT5C{(fW#%w$A{T7Q7$}~KaKex7$B5ACp zpT`;y40s)gY|GP~<@6G6q4}W4iSHL)hN|gsYAj(`PKg}-?AgrDrGKt?+!yzwzJ0fr!?GN{QD7bzsg*UNHu4yBT=cj);@+N5#@1OO4( z4vN|{hzA*j&EoxZh^l}_Juo=K*j@5$0Er4PsFdC`#oh$)eLTsxl+k~?3hEUQ9y-NwLIsL`q@QJU{AYeo4X0)9=iaae z8#FNBqK!X(Hws9C^FVoDR^B4HRA5&8FA&iwyy z3tF*`8}|xPLneX%18sl+=*iMNegQd5Z@Jyx1Ywn=y&>NLYz^}%*7&xw`A3qGDKdjA zZwM*v4|}2@l80#G5q7h2m}nijAmx2WETTY)U@!&chhIfvJ^#LaN&Ef2V8HuVg9C@; z@wU!Rx)S>4oB$ixb&gSfkF_%V^$=CG55w! z6p_F+ObhPV>#j6^$6G{P*sc|;URZhQdc_y;4pu|RiMSELr@x$@my{z%1FbmGxRz93 zc2+y@W!(Qo0`;euS&S?!e#~(^!@CQVjns_cW^M3+8#D~|kC)4Q0gs5!fasQ?jRk%A z?$);t`vz)=kpfT#ULydp$N2G(ln5mB+utRjelYzsnJeKvhjVBI&s`Bbzrs^iNJWL6 z$8heP6ekIxg*`YGx#|@61?l4V7sGanMMFh3)1<_?v*UZBJu5qOelXc*6gU)S17RRZ zZ|jQsU5UF1*E}IsbfcH>T~FipjUcgt@*afATgfi%^V=1gmZP$pIL=$0AY@K4#w za%Ju@^A3r}CJz`5{Fc(JU#&YS@*o;rY&ZC}Tg5*XbOe8nVqtSg`|!a5$?nIF@Q%j^ zHwVRbgnV#-I3=vz$+M^5i$?lwM^8b(MAxuk&(AMb&)ZwY{VG)o9;nx}xE8{vuTVeP z;d=b0-WW?tpA~ms$F;UwBAF@y^SlKC)Q?xjVsF~e{@AJ1>{p+3W$xB%>Q{d1dt>-R zn)S|mOSkp2Ot3NVC2I%p*2i6WS-RKD4&x~d0vHdKdv_+VRoUIWWaraiN^iPFNuAN7 zL#y3dL(**hDIe-{`?fl2)_>?o=O5+_?!{1Y5ViCd_dodLrcrH-l3r*|A5LdH?fems zRKppS|!dMbE#kOfvbN;#G4PB)uxkJ^c`?9jIaA@sSj^GwY4YaD_apu}Q z+H%YvKNv!v0VJlg_Cqx>1jgJLm^4#dph>`+d4KsJA`1aA3PrT!pX)?uF-^u4djFyj z09bVf!|FRbl6V58@#;{c9$$Q4a2u`W$BGxFv(BEjnI+Qe1rLU)V#K>r9`hngPb;tERPbAKi3f>jiJ&+-(25LR6b_PSD!P2ozllq1R*1pq-$OkzM z7TLbJXmBg?XVa7bRjI3~sd0bN;UfFy=jgNe2l%Xfs#3>ShorT^JL%?4)RTVBbiqj^ zd^i+%C2VF-vtFnWh8|3rZvRnu1F&+_ynDkyu6kD@07gz7S=59XCa&`oNqk1VbB@=f zIh+u(HkWC#zj|Xw_&>5?>M5Z1Bn%GlGY`|P5r_+X%rrSrrpHI2tD$DH;2gyz_a8s) z?ttJA$aDd@Oag*-2a-k=++JugtZSf+sI-wHruaI-I=8h6nP2PZH>Sx>%n?RvprWFM zY#)vWQYJ=%XaQ?dS`HZSVpk+wGj>RYvJ}T#kkn115tA2hZCVAk4O@_Jpso|rqEC_J zUpV~fJlK!|@GlRNZlhQ8M6Nl+#8EhthqwP-q{9PH_A>47b_fP;%baFdn+{Xq+p$CI zhCFeRz{-fTo1t1>aIV~X=sG7W&XQL zNm%bN0R8!H$SG_<3UJCfZ4Mk@_MCQUEC0LZLl$gE+2+f!bBLXM!-+#Vb!=Fk8BPM` zexIoW6cxX!Ng^}JOTaVf`w_y&;^I3!GJi%)yzrSK)yKf|KJDMc^_AEi2|lu{Sm*wa zxX0$yvO=Zm=_$9@_49ZVu1jpikcrzMUmOyGRK~lYh-~4^gq+-s`)l*ofW_+VWXXWk z8i_%Uz1FaQARFi3^;FR?=Aks-d=UT28ej4GKR_v`TlFOs#Qf3oi#%|3@TAuBC%`*F z&oT^-AhgAC^To2U^`I%!4O3!N)@EXZD-!*W0&DoYl=4^2-95MS>=GL}AuI@XJpdCb_-T_A2ScRjf#pv*m||`o&gD&xD!1*IkJ5dCZ1s#`ZF!t?8eoW$#?}i6$Q~-Y9p@L_X5OyVG zk3V^d5>fOC=HmEt-~$Nj0}%v66hu-)h}{5id&B}=DyuJy39x19Y&?1?5~CdP-CU%k zTH&Y)iG|96ib6P+h#~Pk5KU;qS})!KC3IE;R6)m;YGpn7wn&FHpMpZSn%7)BLMBMb z5(SmswyB~KW+m7yaTvOvcNaA2BqI!v4X>&G4Vd@vZP~!20Icu)E(;=Pf^i3QBtJ$P z($kFv6c#DrXWvHUnMq{}S$Kg7rA(Hefpite&SMP2soyA73tjtIwrwNLsgIvLc{g%# zCA!i=IILNMe8WiEX}48nt@c`@`0(+g)}>3AEY7r0=H})SKP`o}_9x#2Cm-t2bEUAR;Hn4kL@qHt%zm)TnrZaxLYP zm$XU~_zln?h_gsbBUn3rk3l(WLy*EPc@aQlS!p29@TTL&9G|jFLc2IbxS$YpR5tSN zzac@b!=*m?2{=oV#n8`G9FYKNz#Jr4oI%_VM&U#u)t}Iq3gVl=OwOEUG+|>2lmoIE zymn>)BRF@G(pZc73qO7Ogg4<(1-F7n+Cjhn#yl`@W~`$}o(Vy4+lx@hV~6CAxCVO= z%Be{hUZ4UYmD$VLAk+9HBn)1C)ZrCEDLp@bRoV3$1jD0c;#6%E*5V5KF(QP#Qkm{w z@}u}Rz|VMh*COKwq=|vY>nt(^06HZ--b}RL+ph*dSp)yv9SPcXp-_te@=)Npzv=lo z8A;aS4hVrYw6scu+<~cJh|5sihT9pX@o6bf>@&0;R{?1iGCWcmiD3^AS<#L5O3%|e zIecmEOjVMDi5miX0xv;%Lk}=`!>y}GMa+43>=ehgA<@nGY_C?Pn;(yGz_1eq4C zt;2iWr;qz>YSmKOqys3nbzHMwUSx+zu7$Zd4^KhqcSGXtzH^9#$O?sTT+4JQwq!;} zRs)SfV5MQo4MRkYA;u_%UHxs1sxy1Oxj0Z5Fzz?>POFCByuEAv_8Py~LtcGDMRC!E zEM1>Q(oTMkW*##{7yxwM#0rE)kGzeEvoy;gGBX%sri@R;h*>sJSw3$!o|KROYuAL8 z4P<|-Y&g$_uM}I>ziqyS*MRTGBxc!8ds0-rMw(hT=ebcwM?~Q2`?aQJmJ6NMPbyTY zX{9Z<2uAzHdAtgF$6o(m+>I8t!=m^(w(MQ>dYnb6-jgQlnEgGHJ#XdAqQ(PpkqS%W zv}Bd;o=LB;qm?egvg>sddcFuwYnSF~JkGL;KF_1ww>@6>y078dt4YnCo1O=R*wwCz z8$L|+XSviG0O0NDb}u=1wh!`%SlKPQ{vFo~n;)UK-{{;?oK6w;g!J>brHlLPQ&Bf} z+4GCFXKQ>Z^t_kgaYF1?U<@}O*oyO%YeKZ)hA|U1BZkv1-7O6rTr*|0P4_q#Sp`2| zzBVEswvIYE(2%G1S&U9e;JFV^H+M<78BYmV_}bMNJ>K`lWF|YY3O>SHZY%XG=?)f#uX?l#uk@hEp z5dHo^&I1n*R30m1h!0W?{t%myn3KV_R}prfTS zX5Xy2g`L&Cj8rbQl@fuUPcZd4J9gI0G)h)z;4CY3Y{v0F1a)6}ugm1Iah>i+>ed($ z>VZN16sG*kf2ixMVqdY1>+TO33{vc8?x9+7lyX@w-P*P7l9t8)1qx2Q&4JPHiM={)*obE(&fYT7rfUa|mvjOxx!oVY{$Ug8o1*^+@ij6=aM= zkA_cc6+O$(H1Wg+5J{1c9e}6)@GXyP3A(Qq5dS3DnfP1|KhGK&qUw};gIfr-UR(0- zKr)R}ha^K!V~a*q1zK<(W@pDN-%5OCTMTUlNxLMba||;mcqRN%UXmFvdhK{r{CP-d z8A#z(QwKx7L@SjX54c*GaHW(SW&Q6p*d9CwW7VY=@ZG{$$ zjx%<+V&zB*8m7ftJo|u}EOu6`g#t99uf68gxz`w!EJ}S4osW#f`H;+JR6Gn}DY^tS zsimun9(Vu{ECe(f)3w`uyZga`tO$gFjJ|f^f1NbuGN~KS9~d2kan^l#{wxR|fIjCU?9S)XuIYpcjdV5pviDCEPc} zk6TqbRjzL}oF`oW#tgGCE?^YWTfO=VqdGBGaqn|Qk!CQ^GCVhg*d)4Zk8E@g)Fc`z zM*v6xPJ&lbjawc0U{}hLzk~%~Hb+e8DIg69arsqz7)VGoZqBfB$&mE)9VDc65u@;Z zC3kA7f1t&gi|P!xO7Q-d?Vn9aN(#gG)GJuZ`REUs@jidHjNNd~f*a{GHQ-0S?T)#> zJX=$11m;FXg%Ek$ZpQw9H67*SO8L1r8th5uAkd=b;tOY5Vo;-CF>}sAbi<&>E(%k@Cru)sfLs(pW1hF{v#5=U z26B2dLA4(kpeknd(B*5JzZ;TV5Yd-!hHYpcwLAUeEm*8pu5N{p=U<*B$EDFRysO&h*?}m2))547|KnBFL#cZnyr3%qA z!#JmFFoVrw1ScrsV4g5qgR>=(J4m?S@*&YuqV9E*G5{UB3f`!1z+d3kgk$q7;iZ)$IM}F5hhUrfMp!8Pa%b48~E%6kf%ImzTYrsH`_AMFPr8?r(->q zc&_q*bw+>5cPDY34=qSj*>HBAJx$EcMPrmV{<8i39O=e)$%2eQs)BKT&#X#Eh6K%A z=*oo8R?!yU7wXo(T%fsp#ahL99-)Od*Vx*<-p4N#L4j8>>cs+z8#P_(= zs3bS5PW}9_R<6EXpC8M>#gN;@9+B@Prp*GR*{&PAQ$vi%!f@wioS!vn(l z>O$7^A#do8ZlBjm&eyKDub>HJc@|K9%FkotOy{l+hbnchb2V1_Hv_@ws=N4PVz={#KRnRXxu#Vr z5;M7epK;w>h#6sX$(ku%_?e1=xpu9_KtIcL1aLNXStT_QUpQjnKHGvi;`k^-XVgc$ zK4bW1rj{F`36}TIIfI`ZCX6mj;tk_BX%H4bS zfEN?xS;Mgl=MtQAy(2?k+y6~!93z`3(L4f!rpwx_FOT{;l7x!baugRAnxC*#d)$Jp zj2Wi4TcdC5oc9XikPbcOb&N3Nl%~t8sRy;kF;f2LwYN8rjH}CE_!HWnLP%5m6XKCaltK zI%|)d0&<)~*{?ALG*%1SAOWF{-lc{Bvu0pzVIRJ*o|CRNXl1C%jD6&xp8&jfYId|C z;g^#XCrRxjQI3*J8>qp)6A+JND|=Y@!_Tcj>WV9y=@9|D9o5=n3F`Z+H zN-B(Npz?lZzT5EULjt3qISZ!K!y|zSl!wDvbEU6GL=e*_NhFa)4H=?eNx>TYZliH- zYfoAnn?d12CC!cqYy!uzc9>7Tt!QK!e2=UzaZbn--Wjl+!>)C-l93bUI`ALJH!nW-?yq;72ywLQ7 zZQRaWzusuDL~9kF3*y#X6>C$5>On5s0K->`Q#%C#ykijJmOH z?uQOB;prjFBBriORtEUo0cRN`>IbY}j`#j$F7Agtn+|{xel*J62@X(SK+B46_VGhY zo0nMN$!CKQb`%tb%1c6MbXi|^QgVr>!R_F|gO&Ne`t5*^6-^fOj(4$?LSc?ZosA?$ z_oDnGNll)EiyQc%>Ca+*wR~n>^W3@`HoK>$&%fTN$o7J_U{|*V@N(sGo}u8B)G?x)j0@Ek@P19s2SAiD85`n#C(^O3+aa=h(h- zRWr~{x~`WCZzAZsp6!3uy{J7gQMR^!ktF_ZjTZgz6y6Efr@rFPMf9!`PU@zf(-!@S zY^(0Cz<0m*WFOdeBffErxaIHKP(^F8&Dwq!#-1FqcU`HRCbi^%jskU)s*YU5lg8xq z(=8|0=W{L=L|2(9znZq)@coIGK}~`4lEsIRxSRo+tDhWT?+thrH^1f4CJQCj#mBKM zrN?4MfjB6oC9-2QKGeR`9f<73=rf`F{QJ9trN28hlMnCQ+&?X=<7AsoD;XHL&%fl>Ga^vT*O=JuoNnqT;Is0HNhG&PLUGsKFiAEi^DraO$uA5pD6c~{yl2V zKYG26_wyk?sll3f_O)cC3wwsNK7TCh?#U59FSwA%O&Js;%B}uG?U|LE!z}yT=;**E zS)jE=DCMy-xUz4K@6A;CpPtoQKclmaeu?**tjmAh`d2bi9T3f zzG7MI@uV-A_6>F|iIZ=}JIe4INb=$NvX8JoV0)ubRrK zqoRb&J;QJD%QyzM-=AO(MUEliR4YP0i|*d7?&nu#@w=m~jU-W>DegmT7RcUp=M5vk zvPWQpaU1bk&=L@qmG(b&0jC&_U<580>J1%{+h%n&$`3DSS@TdQW9R!tWdDP4hD|M`yu|rZ6S1d3XA0o+4SsXmR>|3xH!&&*G@(nis6MSlKMg}x-AeFMkF>)7 zJG&3Z;CK11qou~yPzeFN4gwuQ*SU2?2HQI`#3=?#CC~vo&Trrcf_q=tj|cPy|7!Xy`DYOW*F7!X!mJI(Vu1hwPbj zucTx+ndC2gVS@39D&9pTEj?WToZ!4SXeQGCf)~M4*FsaI^;ix4UNFFBo;p?jEg0|x zo|%yVQ&SSX9_47FvbX&-IgLZRLI3acSrDxGEw0!DCU3(G|SVgdR8 zINqe^`CbV$m|-NQ{m5TrS5#1UlZ_pJ~*i}ZIeScReo18$Gs=@GFZ}N?JG1Yog;pf7vpOk1A zl1o0Z?k+UXN*}v;hEb6dIc_{XwcA_#!o>tv)pXc4eC7W1Q9pEv;|E2gXn?ldY z2tf)X<4Feo;qh%${?GPPchaXf#CrZwU;F;+3WgRyQTVHc*#)J`#+mmEI1cDq=q%0^ zF0Df43mq-uG;GCg>+vpO&5L*`f=9;X)@6J!1Z`j4U68cj7H%>6*s3=2x4n+_n1HYkS05~CXfZ-ty6L;kR zI`I$e0Wi39y3qjRe2n7HN|3g!92{Dq>=jC|ybzQPIMMipyAVu)q%DH>xCI^zBf17S zXzSqF4@U~&etysjWZ!@%q|oNgIf1CzxjAABe~k0{EBIH;YOsC`V)#60Kl-<`zbv%Y zXw{wK??=~#)M7b))9209!3EJbWanp_pGvEX%W}OCAGgqm3N}jH_(9)@TR_OsNbKZU z$bgpl=(EJz3-nH9<;q2{I3?x@pEO=azTSM}d{7=mBwm9C%?fBVsUuw%vzzf&Nn)TqpgI!-O3D8Om@L#@snf27U zwpmjRe!!BFlD5H?r(#MlzmzWyGk()OQCwRYp9bh`gb=BC!Ajejf^{WQ2g@-okd<+5 z&HoeSFY;9%ECk|t)%4q?CtMMN+jXGxxGEK^k)fUyp9I1 zL#N2>OXTK0IzkW0d+|3L1RI?u9l9+Y9oLY2krsMVij#N+h(G=P$B%D^&f^~5YGq{w zN8+L7;!8>~kl()@^RTtCd1$leIWIFEXy-71p*AQqIA7YQ`};CbndvKKoSdA215;sj zBURYpbCK~RW>sR6r8D1oP-&Ys@UOQ+Qp7VDqRtOI@bT-{q{dqt&#U^zf^vRiT%pOy z+fYx{e=*t9!h#!&YBa|D%8?l%7kN3@D!yGD#@gqUBN{`KbKGs+m^G4;_A4q9nmzT^ ztJd>7Pm*j?BF)FD#}Kb0G)BEm@fmWX@rDpldXZ&zT6B<^*uOv}m<*ovG@m$Ia2$MlNAg{{4`kX9)XnZgtXXq{jH)70vYx1 zr_)k*f``{ScTUu`e90l7Pv-8}w_jHf+7JZMz68D4uc^bN1hWf)|LQgmQ+OWkWKi0+ z5zA&cT4FO^+&>-j8pc7i9$`8Np97!ueoWOclta zX*B(x=*x$&PEIB!KzY`_?bH{)Gj$TxFNz5@5EH{}Vq!uO*mKWu9)gu>BqC;fsk#2B z$u9TuoC9!FwZ4F`>PNS#dy;x5$&bt2!h&+8v-B?{YiX8Msu1dExw*Y8d2jWIF!5ws zrd$b_KiJmZGg}Hs5ql}Tx0%a-)PEL!OAnV1gnf1$&PI-jO(#x6sAjw1vbV{Mo&EK~ zHMxVIIc2t#)vw~wTwf-og->pqCFdT>SMpje&2hV}|9f`t`2ACp1;%3%@9d;Qs%g__#_rY;rxJPZZV_bf;rXsEr&Ob3EbSiC^jkQST3Wd0%<=Lu0;_!@o z|CJx&;8*tA)*oJxNUV7LSHZfm*fGpBfri_Z_HwA0Nj{}k!Pm~6e_uCO<*;^Ni`pyS z#%y@TIMeZyEQ33<%m&k6jZY~Wd=7^vwCuYwKUVQx)LLfidZ}fxK$=X^Z!v1659|#G z8*WTowqp97ecCTC*Nh3euiJO-G&~J!l(^Y#cL5b{hYlU$+stt1yZh#b_P~6#yG)Y> zQCD1F^e3r*p>XOvbLjW|V^vSLNVJP4@;|g}IcfMU+>iaMR;ku@zTc~nE)lk_rLF!d zVr&g!EEBCFJbV60iblfO$D38vFY9}ViR%)Q14njLZ0weHvFvwWz*LR@ z_)!gY-j?~j4!e)#B%JG+9QXO)k-IU}S0IeNE>R+-j=3x*^tJZl)5DqN5hkMvF7byyp_kp9M;3f zQXFH~ecfcj*tY&>Sj(>+y%$m^U0VKHIQ((wblcpUIB6xf$5J~oWjwxvRxvaDiSvB) z+B+@9yQw>`o1dxQTT`<$QTc$7b(Gy>d(Y>bp1IY(9WQU#%0%mq&b#^f`N90olcM9p zclTY#g+dyb0f!!RbCV=%kp6Dv!M`^bnwwc_>*_u+N7s(HqkNbiVP|{%*bELI9s-PN zTqh$f9TXqWdbIN%+G&8sNz2NnDc=#kd1PnEqeuK?Vg2F40Hp}oKGUeeu)whb626+c zlb)K2jOo70);*tVZKT5S16uS|{`BWw7Ul^c$JJlwo_tsb zU#rn){DixX@O`N*Plrw|PiXayAKy$v_?(G}7j0)?(IUBh`7OVYawfpZq$k%!vO3;> z_>fuUpnT!;J1>m&fgqE0{Jwy{czsxDOiWU8z~$HW=y!Ivlap)f>**1^5|x`PO|oCH zADX&-zvhjg7E-jZy1ELGm=iS3zD!loM2214VymZqpK@@Z#p3b~{14LrTjaa*XlQr? zM>TBj*T0P&q6(rQia$t7(k$EBzs}jB;rF}Xm-i+`@jBFriHYZnEfLG;`F4NA6*M)d z;(fzBb7+e^AnASS>FLA-4$GL_*&n}QGpoXd+s3EN5jh0?CgD%mY`Jw@nAE zEzuF#A*m|BJGiuTnVO37KK46k%C%iw#3_W7*i7H7_^Tve`MzrfoAdfV_p^qEZ1HcA zbY9&7wg?T93PWsccpP~4KBmD-z7`gStbwb5CKb=FbNBb(HaR)Dh--e+j>PU_*3|7M z*P#8BmX{AlC9`u?Pk}k<$i&rITU*;!AuT5t+R&hjX-arZ=PO93zxxx4oy$yc!x}A$rn=&nvx;! zuRQE=*<78l%Y<{-9FZ?(OuJZ8{aNccV(Z%f$lf#A7w@{6iS|j%iF?+&{6B7)EYPqN z+Qu2@tde*7;G_Q{?$gB#>*rN7Ohz74w$ORi+xFGYe)++ z<6>37tGScT5&bfkUoHRsaV9zPZ()LRSnKr*cU%lajAXQy%Dpx)o&NSC(vml}rR-+o zWW#^+f5hy!#?NJ>M5;?AKD;`it8!E_DT=E4kaFVW8vAn3d#{Q}VVlkNBE{n>i9e=9 zwsd++`$^inX(d{<(r~@Iv^uuO@%`9>jIGJnZrh5eE^pgCyPdqs1~ZG;PYK`742`vMpTQC7ZmlB)&eI#ysk}-`@*W*YBL|k-j zn~_9rU3_@>Q&szQCIY24=hEScgQB$d++SM_wT&l_xt_} zKSv!$<9VL@zOQj!pLITg%8D|0*yPwKC@6SxvXUw&DA(QL_a{tr_?N8vAK@QRuxw>D z?NLxj7?9uBA{#)W*FC-h7*BmM`T0?;BP#`2(M3d{E5tQO~jd)IX_iONo&)hz-BuJJCbt zq{ba*bUOV8*TF+@+QvtK?`e zdB;nR2fxP3Z9-F@_|L6mB};cF3CCA!Si{|X>g!W+-)Sc0x0}21>C-3W7cbD_JBMbL zRABIVoh^T=WcSp#Ien%Qy+XP%_?j*dpS+XDL~#RPu4wEEyj!L-Nd_>=)BdrR|h z?b zy-`r=9;R3&{&L(PFDfdUvE@$?*_x<)0Hb(c>*xBq)vId9&b2i=88tOxbX+o&jJ_I6 z>o;#Wcpf~^5viLG5$s8Qf>D0@*UH@|w9sMnv?a%Mg55W2txQGUKI%S4&Arp^#@z zwYIjV!yPN<3@UCsAhLU!aTE3j6BAR)(mX#tOB6OUGxIWx2`_o@YnKC8VHhXd`IcaG z^E0ojv+4P$m%8;^o=1Ycd~%ukcKnoWZEe3J803*zD)*7sGuvjKtoOl~4}LWD+xr@- z);eYldw-?9>94~vOH3@R`WQXWQ42PF%GSpZcfW;G@Y_;Px$iwIu^jH&s3i$PvmDB; zH%<1C7VWnvbv|9m;x_N2hev>4W2ps8a!N|AqL(`aF!-^aNpW$XT=xFy!)4Npr>dPe zquDei1aElttT=oXcH?Uarobp2yRdXw{BxV_RR!VW$B*}mYdy~924Ct)C5w1WY?_vj z7aF$Wf6K_2{nr_fvbxp#{dqc<$Eib8YwMFdbwSPiwwfP5el+YZb`)BVm!a=)-{E?V z@~=BN^rucWwj(`dYrRRaJ`BzATo9+Swl)q5CMM=@9|Dm8^Zra44i3d_5m?^D#Kf;x z`Z7j}O+%Fy>1ZPc+TAuQF}yC;^k6bEgPO1VNr(&TsF<>m;MAV3vgT?OURTct zFK*-P%s=HkkB3Xn+qS`HQhdklXj4i{i~Lhikh!qia;pFK>017i^j^%`bg`O6ULhe$ z3O=h8gO8ktH8jl3ST?_Q&`*!HIQeZ+R?!ARMK1%QSYM`n`*s6n+_8FcY3U9rssBoU z)_kuFm7jXfyVxQ+gQNA)V&{v4;pgh=()kY_KD@oR)Gc$gIhoqOa%22pY)s2)vg-D< z`ySrYr%%sam1IIF1MAKgIhXshLfH=E$TCy{lZ4&oN{4lQwDMYDR4H=t%$4hhVb}bE zQCf!GfHIn#Q7J1c`#bsS-1JLKjN#f)9{Zu+yCqa9f$@;Lf_+4pS%!ZT1xaC_Xw-QM zqs;8IQe{cBSd>TrIhmH2e!Gn`S-lM<^ui3?bV$8H292rXF+HEjH>({t-kv5*sAgZ_ z5oLu6AATLTZZoK8rShVD^k(8W+@F5=xCco*Y<0udkmhf4kvB|PN8XD|NL+_X{qJuq zYV~9Y{#}9B^&im49>1kucdU+-ikRdS5xE!1s3a{X_n%^$-pf?!&i3RZZv8Kpy`FL# zz2w6fb21eb6)u}8Avlhxg@tRxLq=ae70XjMgi$Wh|6;C7OGme}oE}Qc&0Sa-M2qD) z-)=`~U|@h8Y%$&K1Zz1r$;f0e$i*(c=v!H_dAqh67NTJ~O?y1rXlra}2#bqjY3Cuo zcQ1rPr;3`1sV#{0om7U3K9omHJiLR7Jc-obP@q@dLG@3ES>%|LTNa?BrR~X(CuPB% zpP$bjnYU5O{MzP0*ROYpKL>@`|H~I*vr?@r({^kH2`KtiPFsr2uI}zaRgN30ho_iL z{uMB^B2nRata5U4vWkkC)^S>SAGPvGei7{K?36?r$t0ER{VH66M0{b_ zeK9q)FXPrDh9dhtPxjr3h=?5O>FPFjI*6#KLJv2_t4lCcu<-DjC!MCBO+SeJm+{0O zpG?q^%|SrNhcdtZhMtB78FCMj($ahbG{)yJ?CG$|mM5#7a<&(5X%WLI^NK})RpQvI zSl`Q+gsNa^E?GOP-(@l&a^5VlUX3}c`|Ywz z%sFHcwT01O;^OX~kn!O8Kh0?H#kxgEMAS=NjEYBl4A=k)6!LWCBP)&LczzP^zkg@i zUmGT{v$M-<`SY8TvG5m>Wm2b!Pp3(GKWpx7KX-x(Y}vvWnNY*-M+UxOyd5n4q+^ai zD^I;hS(%9#Zq*?!?(^~JQHV_R zIfJXkgF`08^#+B<1LkgS9C|MY)*J*JHpX5QHNVI-$Hc*DtT~z*r*5_2sz2&^LU1DF zN88Wu3v=vsbr}#CXkJ?TL=uyzgILqMUQe@CV014F3oF;zoQb<<;3!JL=K8iX-uc>s zl24@A85#*xV5@cK0C@AUF8kL;ckIm)P6Z*}EzfxOyS& zj;ndb7V^tl?Ewj|NGUj#%v}9=+ay{oSl@lInZ2c5_~)`y@xoO*pS&vlt*J+pc_Qz= z_cP4H8j{_{sJHI4$8FVgB~*1fG!ns_$KM(@$B9b@_`2fXwr{+Du)ZE8${R3axr)s` zANx^5q34Tp(LiBvzTok!?1=dtT4Ldo8A4Gp=Kh4H{5+b?V$NWiLZJ%Q{AwxI8)c}s zSp2^dq~uo!))MB5m5y3Ptnz9ro!=!Y&pgVi4A&C%eTDh$6Jz1E_<_R^o61{fN$1@^ zLySouyX+|zsEcPGJpC>lS!82LzJNnh=hQG_#aB08f^N@ZW_>-K!M1vu(#Q-UI4$kNrFE&+a~&OabDEE%Rv+tMZ&%wb z^5bV@WGG&9Yo@-YdF{6mPikhSbOG*pGbO%1>WddI917)ibnddSuq5QxtLNM{DUQ9r zdp}2~)8xUvYmMG877Du0HFTeP2Ku0|>5Ne;X=`hC;kssn9kQ*kSMu-|z>n@MblM-` zv~}$k(&vdi|I|97HOsHa<@S*fKCra3)WPlrTml!}XTc-q$B$QIxR=$PUfm}Woo~w1 z)6yEU^XwiSSn!fRfAKf*?4(5-lW@;0St-zF!o>bXiAkomU>$NKs2UrmYLJ1@l|J@)qY>}GynV+`-PsZU|_ zq1oVl{c8l|-DqSAb29=oW!yn>epGiQ<>K-fB_%Txy|A!wxhK`9|BuS02Rj>E6C6RG z0Z^c#+!g|m84wh7i+wL!HLJk&zybl@`RMAY%G9EwRDpqkvT|}n9-Ebwl^VaCxdDPE zG-;N5he;cNsEbj&v>XX z5hE!4$BP^U(6$?V@WSm2zga(;^ODoo*Ec{UK2N)R4ycDcxpUZjVcj?_VCv<*44k?7 z`N=ni0M%Cqm~(ZiNnBi9)NZUe2*m9)+=W#Exa@cNwC9=%#_0PG)%B=ulp*uV&!4|n zOcdzVNR;^6X+kOBfVnoDFQczdbN9TUHTqP+nvdc~PEPojFTPM8gLKyhb6)NM>**u@ z1W54V!-p_J;tC3%$1grh(*cYKf;&^GU&a#`7bg>N_->YR_A!O~w7N`DQ}d2`zV616 zu*LJK^G7yQO-IvSTM=8ud2{A}tYLcKh&NIt$sV1V#)10#=@a(YY>;uv3_rfx;d*_k zvZ#50k0{mtKW|!CH}6|;rQ?GojUunx0L>@ zFMrMeykiWZd`xRm#tAbI=u_74{mOys`F110_uO2`7!DoDZoAEi$FF`l69MptGZ40p zMvd#kqT*tr+qb2BXd)GZHHdN1Q4hDKLje!f&HV*9Juoovx#P< zgtRpJ?PQn=jZ%x-KVMbSYEeCvmj~LUABW$et&Pa+eK^+z1sDXw8?mL%?~QsO^Y2dg+}c+fy^oM zMo+`v>|oqhZN3k$6DHDDG|^mcm#Ng01CPeV2xY24&{vl;1>MRh59Nq9;EK_r#=LHlV_=UvlZ}?#KiA z=hKH z0tgpkI^s>Au(7Em1%0u6-NCBH;iNjC(lJhWc2b<5pY|g4d#E*|iSVFB>6_&DjP*IO)X@n>c27#mOj&IAl2gUHjmYiW zw@1otsH8uWy?5Lgdnk7@fxuUcd(L89RNvshfMXWq6exR(3#RGnIlcLMqPTYjzGVLE zGvnQp(@1F2fRYMN{L`KqhEy}n!Y3Kx)~ z(-=3D$AtwWTq==-Omc(BQPhLGhQ`JW`1gbY{``5G6;cCm`<@PcUw^;dat}?NB$s%G zN-8icD923KuU|Ja?J95hx41Y95{RAFcB(QH!kQ+4?Y#-LDId4nnUeyUt0DVWeSQ5` zF8%AUaHKlm%oAXo){T1V2bzPy_Dp@6>OW92I2I3#<01)(iG5)mKJSPTL@IpAvUs){ zEdv7@Op0T5e&g(K;QO|R>zZW2(lO$Rf?lGR>o{)Ksi~>X>xHe$uOmbW%Qe`zViiUz z?C69UTkT*1D$X{mkx8)#G{Fdi-37abl;hR!{JP84FI9yP_XFb}**Frp8K`@vb_*T& z=S4+C+yp*Mbu{J1Dd4z1Vzp=n3qwom)5Y0QqY+PO+@}rBsn16IA&qeV+e@NXm6~XJ zKs1xQE?p2Q6K;lM`qr8hV6WQaIUIz%!|4WUR^vy!M!l=km-}1O(;GW*7Ahu-1Vcrh zTkcHee#@v0xtzA@%LByF{nKDiFCpcwuk5kKzzxbiQnhVh>wpCLZqva z67yT@j)K=wQMnD8FcBhN<9hHmJ1Ey{d}qFOr=2l*20o=ggAK@JGyIAB-KrYn^S`2i z`o!M8<>~9|`)>a(^qw$6xaaCIsP>n8I_)U&Dfz`hCn>iS0ANhHEZxPW;QMR7`cZ>M zmhq*Un(y!5Vph|&iJNQIhKjzP1_rcAF8}TbT^`Q|goHQ}DSpFOfAJz!r`qut>P`@d z3n&K$TOi;O=lO_TTzFhv93e+VdB~MYM?ylvXU$@h*-)?Z^zUDpIXGIpF88RcCMt*k z44`;}EU3RT*PJF|0@H?UVE|hSQWg#lys%+ESK2SR61xLqm6ex&A})RnC@4_fkkMk( zd3X}y+wQ~pdaXsq9orX2)BRE7=PQEQO-)U&=b8eYS2ALRuxp!|Bta-kt*9Ua<^^o^ z_e7=r&WsO1)j>b(%(l+XW*I6EUnh59A|s$bzT#DZpU>Ep^N?g4+aF^ zy(%A)j$8b3Bbv`1xb7zHTkloFfJL%bl&j~92E(c%=g|A%K(xrpbmhL=yLn$3XiIu1*GcG@L6x~y5`O^J- zr*%MON(-0Ty-vsJqC&ZPZ0OY8X?SEpb+_})Eb5HWCE6R&Mb4qu5#f6Fx5&vq!5#}8 zXO4Ob(nlW;5cqQ1@KEV=aa&t1@H{|cXlQJdY9uTG5jkrQU`tcgFhDz^ergg_A8)jbK z0XbVYoX2H~S@u7CB#xpfMd|6A`Z@ zle|vhqlD|&Q}38!a2Byw&>bqOLtkcN`6^cK=gXWyfk2@$y2bSVmGhfQR@jf#1LSlP z<6Sn3RPn1q?tH>kDJEd^?3ZVnkAKGXZVJ-LP7=*fq`WxDKS*O(1c?1|z)!ZM)lp6%F)5ZRYQhDPfA@!*_c2dc#^k?M<2qh&}HdQ2)h5iS~}(A3p@FH$ssm?ELAjTa5fKXYP@`e8~s3NQ7%Qvvo~ zwaYu3Doqd3ns)za9cj?~>mqGUc5D2(aNjsg01Z}x`7uuRG40T~-vsOKREZIhSf;@V zZCAXxJPI#I;-j!7S$Fe{Auko(-7en2i-W@{xXOLF$_|zie%ZQO^4I(q@@pfq>1-uo z0R&eM4B8_EG8S2_S_cOT^j`)X-`e$}^@?2DVooU`GxB@r^&?0$V{Xy5~*~5bx zhT++En;_2b@oN|QSq|5_kL26RpXI4P$r*fFJA7Uy`PZPO?$KtZoQFr3M7Q{|OMygw z>B&NSiLg;U4zfg1798mTi8DeXnNbCSo)O6P3%H~$l&6p&jF`lmSl!RZe}OV zXh5FOvF}3F8Moz1S=~O!ttkb@@0oap?Y%?N5#EG(oWa=Q+Q_oa}7z=g*0NN~5HJ1T;E2 z8u7=JCpIiG@tKDkKtx5K5!bo%lf8W#pL*}}bN8ybm{*+fV*w7f-+s|Asshdj+A5Xh z)~59A86H3#^px80YTMcC7M7L_4_UI5k{zG^oBBe?Z3o66Ie&BwM_+d2X{s8F-E0He zse;G*nrX1|_U&uG(+^34G@z{P4UE^-3$W!f?OcGe@mgB5fw-e%nO^pQY^=Pz{Bt!m zdh@fKm-zz5H3;DLFdccjJ55oe8ww&qzXCS{1H<)DJZ16$L=xQmbR=g%Ky zEv?nUT;NPuiiv4kF~nC2G(oulngNvm>F6-W*KiW0Y)u6ZVfd$xk4m;L{1>1Fcw@pQ zmZ%$g>tOzrsN?!qRdlLtf1jTJWdS@jkBx8Nya&JoOdFu)QM?)2y1F3~6R)&DP!j^q z&X0HKyP7ijrWG8wG2h=|=9(pUaFP|Xo`A*m?9Wufw%#0EV@ZD-UpnTCr?nX4bY{!RHj_z5GwTpo1(3yX>I2b4?Tb<&|H=_5sfi=L}hiXG2q zoz`Fm{vP6Of-aTVXH%Gl{=NGx0z4jB@{{^EzK7fRFwZXp%7>c!ZeBKw61cAj7D!Mr zIC(1qW(&wy`lCh0&WEF>z%8%Mlsg5N9)dog4 zU@#M#P9V9l>Hhj^Qo2K*f+>-#}(Jx#chL{FaivLclD2&iW5Dws3%~ zqUW;&;EUSs^wW8M0Qh!1K7IKSfNuW0_-@`Qh$v59y`ucrmB?BJ%b^i*lC8)2EYL0v z$8CtfpA4sy>D!H1`lBURTvUW0@3N|}XL#bUYyi}0`HnDb#Wl`~ff#ysro@Q#8>3d< zOurc~@NlrV!TWgRdbAmQd2t4QZV;dPvuk%=j9KwbRM?@y`fqrTilKFYlM8Z;=Y{L5 z8rRzZkp`}}*cjQref#}I2xb&)^j|RNWMKQE4DV};T?kiuUI+leGjGgS$+iW3=^R#( zn3YxT_`)j8D?APAwzm3Bn9}^(Q|uT{J;xnOS`sF(MPVC&DaNlO@9HW*!m0aNaI>Ou z;{sF|fU}aY#e~a7tyxkiO1#2;pL~1*I!xW+( z1qD6KQ+oDnTqxcIBF8A8AQ2_fRs(N_QnksO_zwrXX^T>WrLIIKHa1BIhZ5CE8d2Ew z#>U3{9;dvCuJ@025}l^%XX<^x*`3fb28H!|)Q}!X&js(+)E5>Or~w*nIJ|~C>q-{g zqWwq0uH`%1=#PWVkW%{-ZUJQ4X=dSJ(Dg7Q+kJs$KcB4v#q`FF8!113;)~TtSpb6r zCLdvT_zCJjd$bd`Y|V6$F;3_E4g3rhISg4N@8`f1A6gF0GzH#*6UVXWYq$rXA-EN@ zke6P0b1-W=C|7g7WEv#5@aX7|^R1!%C>!UFgeXwU1Av>U-;ODy zA%i>tI>9}K2Y*X$^vX;;N0iI8{5r55(_i4!sk0t?yY@bESXPEDfRQNhE)8oToUZ|@ZY)#cf9L|P*%h#!v&I%pr#&Q>>JS6icpgR2V zYe55s6EumemjJOMfB74X6UtLE=TE-z?=@?JsTATvrx&5A{*a%W`x#rDPv?#vy)545 zga)BLD4l*=wP#&99yPZ;a=X(cQpG@9ln;6CR6rx-efn!s&~bgcg31!_P$ng=(*#+s zAly2%-*1v|$A3s3++S$}`XurR^Vjb>O8a~{fo+FALrxLlhZ{F4J`Pf_?b;A~^3PGy zwgcBcr3&b{>-&Y1Z)9LPv9YleHrc0Zynjzm5ATj`IQBaFX;hCcVGY!kpPZbO{4yx< z70G3e1qC8gA@&1k&vXI;Rh2q62dAP_hc}rpj^y@})%}oxZf){~lOlEPqN+#y{3Ce}Zyx9MQ;*&b zXpTJQ4xbMS^c@e5<{CR9>C-HzR5|H)y0M#}Dc(SOKK0A%Mj_wx^xhuV)zAuSCU;@c zE6)HdOD~$OpIK6}62*_tM+sx6$$(7&a~e-OwVf%iBEEY!7}h0{zktkA&)PxW?&C78 z`YceLvXrP4VtJa4=@;L)W>+e+L;)FZfprpcb&`B#zBzaNIsb^4ni>TbS?gZ6C_$nk zV?{7bDj51ZkWOkeDCD(VxsUm4QW8120e;i~gSf(akY?-q zQ=tY32P}^irklBBJ&XScVtff^;S#3=tM19L!_d)_rg{;P}Vz~{`p^#3_HQl3vBjb6xG?J?VJw-JVZOld% zYAz&)fIYz>X!i-NrFNZt1x+u%1Qc3uLSl8>~U1;G5kU_-ieYZ9g zcnYu@@@kCrm7J~896C8N{3&$ zr}}25Hh~RWk5FR>Z}|H81->~0k?9U>uo0k=k({)(Bk^g;QT~lhkzuXI6vId-m$cCw%=NuzqWmV=> zoCZM{my8Pqo;Gdq0z?JmOzPX^W0c^yyID(mn!vexjE>?so{~b1d^U2dO8RMlM**() zHNcMGG&PtMBRYaJh&#PMbp&J1I!^UK>;|jF07@K4DF@CM!RJclLs-U*@&cEdVOG(0>-+?-Fo`1;=Xd{1~R zeFW9}+c$bp(5L`Up{#aHLk=shrG0WworxGpc$Kyl-o06ex=XKxj586Pv@@|A6kPyE z6?HmZhf|Nd1~eXGu?v>Y>dh(~B!!5wlRXHIz0lcuF%kjlaB>fd z$~xt;8sP#HUrEIU6!#EVExm^2(i7~p`m^Qo@Zm^$hkkMF&2+6Wr1@m0^C9Gyrj#u5 zVT(`@jR57Ypkvc~sOXfHrkxSF|Iq1Al)RA!MrrxtO(HFOsU0+fShHt2k?q))+2cRd zSmN%nFfn01fBsx;J-qq0M7K_cN@ecAUTN3UQ6*5>v-&}%s7h3~3FiIxg+p^^%TiZ| zR)XC1$5X4PFVlO)vijv}9U~q#C;uM73;tqmr5AGTJlf~w>P$TD@3u6W;?ecz&c$h= zoe$Xi`OG>mDy(KB&3R?ZJ`K?y|9a0>LY;WMnkVcw)+V`owu@8qwux6nVcNq-8qWgj z-VKkVJ*ri7DGm!`m;%ZA0{>;77AUM-WVpJ1o3tXOBtcR|&M|qeJnTf8+wE<~nC1g= z?hm7$F7ma0uV%K%85}eE@Kr~X%}F}R{OPr(pHtRPF}$iLH;J`mrO6CR4WBT#Z-psX zJ|S`E;SrG_cX#Y16bxQ#&&TvuWr}Y37#z&xq|S~rbnsr|OWbw!?Mm(KnQV@7Y4=JJ zr5!?JBP{`hG&_uGnb!MbGTl`C*hxOqJ}kp@U)zv|cK?_ztl9@gT7uSZfd_VY^M1)|_Xk(ww?Sc#xfXN5EZN9a2KsjroN5{d>A3tL zkuiz$(w3Gt<*HPV^aR|p-+3Foq&dSh{uK5B0v)IXN@wK3(gTBol>;xC57JWenk4kq z$We#LyE$@ivc|wm)L-)Cv1TODf3NziATM6F-Oh{stsiJ1CN{RkLpsC0lt;d@2M(Q^ z*PF1d=?-huWISx1xVH^pj6{=TPQ0{aZ$B9$+zJ@9$Y7NdP6dBUJ;&p4#cKJqFQ&4)S14DbxExZDGF*Fkyz;ru(081PJ4#0xdy-tnzzUz!l$G=Nj!O-@bk8{%5ZvDdjc|41Oau8j%zwbaP==e81GT4>OW4W3bh+_1QrA7R>&;g1)y7BcX_}G1LmKperpb3PYW1w zkXHP+_l|IPZ;#uwi!%3BqTi~6mboD|#A zz9}<-#`|1TGmu#|v&WUTL@QDur}zv^4@yx_%GOXSQg(v1>Q;jAxs8J#gBAlrLvw+Q zNhm1g%=-bN&)TwSwH>j=zC>m}u@-_eQ61mhM14jj(4)I(-)(^bLNKNfMg;S*S z?%l)9nj_t+;OdJEg{Zx8Oh-5FD+#5I2sTP%-!Qb~zt-2xh5X1RxJ4=WEJVEY$_kAI zt#{+kx7n$iwn8SO-KQqPiVBARaxiW4u}K+fbND6&J~}HJev-tvHlkE!D5ysprV-wV z?PYUO7SLwcG28uaCzSn$Ces|pjn7|4UVGdgRTdSS7dG9s@u)=|r!)BcwxCS{H@N975V1nw5C{9bc5Ud0SoN(ib zePB&k7$M|u1OB1jrww>j4hcgbAa)CFL{+Y_4otnhy<{Tp{1AM5{qH&?=^;U$AnH|H z|C1BKxOHcfTZDJ+VCAXfK)QdX{LGXu9&{Vu{w$?xjn7D)ADD>v^PhtjkjJ#UJa;iN zG3k>MT+osUCiep$p9~jWEA+Vg-PPp<6DOyWfGH#iPxn`R02`LmvM@4Yz;3Y9WLgMi zupIdQoYy*z5ZsfqtvaWj7tds6#qxD);OZ;tyr6N-f+Pa28J@``#RHSiW`{VXTEQ=J z)GNP9t&J4YA#Ouko9yy|D-wmgfI~-OL$Yvpd-}Ccb^UJxI#vMmSosLOeY`KiLD5zrRNc4Iv7fI%olv1nPCY9i{be z$ee7K*InR2P#tBJVihvR1`tk_efBJ)zwEqga@KYRU@@X`*n#QHg4+^uHwd6P9S={$ zC&}8|%@BX*<|az^x~z;WO=Ff>TyTXDtUqF~2ni*4$_Q}xg@af3`e1bs0w|@C8m_G4Qmt=l1$hPP+%bUhPY@|R(fKzs^BS>skSMH~8Iu~zZ3xOo&H$4~+A+Xr`N*za z#-Hcu0rjC54Bo8nHv3*PUdYoxSaT~Zq zPZuAo9VD0w<`NMF#TuW#9fX2J&bOaX^4tF2T6qgu0LCQe>rktuU<$swNrE{kaoi51hB=MTx)b%_u?xpMomle^$ zfJ~Stbb!D2_bWFls6mKG{LH@X=%544Z-f*fisCMKyF@so9FnjYC#USwdjr5hH-G_a z>*z>7n9a^gYIx8s)@iJ zAg~e%(%%zy6@&b`i43%pz`SFCjU5aQb6LP0)Yxc0=++sdhjDHJl?idz`qg)`Odf|k zfqj4s$7hO1_#lozF4q~lSL^^z_lH+kGsnx^b3q6Z;&hsuGb4)|)=Z7;7Tj;&zEzZ}p&Y3H0ADS&q=Xn)xHm*rk<7U3z8Odx z6_cR>0iY9*s%9y^TT{A^2ExuYRP@)I6O~B7AlATSOCQ45NCFx*6H6L&BzyoN08nTk z2M2M%95NBNK?Bw(e{;8VzboPKEr`H6=+UI5 zr*nH3yhapZ{yofi6~ImiQSc5d?k;U)NuM zRr$7=fin2ffeY~TuMaIrV3!JSK7s=d_MZNDnKj~d8-@yh1YMSvheu^#gBD`mSEs{u zT-IZ}a8+tr+UqdyNK8Hjlp+zOka;ffS>f^APZndo=j1ec98LPiJ}`wH*4w_?I>cN( zi4Tg4HouWJVBW@_a?f3pk7KOeN4ntYe@}5N)~v-T{)tzdEbC3IaV&C8#Wn z(;6oD{~KU3w#^~ z5pj~k1q!!!{3M`j3hBL(Sp!iO0EOUfSIN(qSf63!93b~LDm@D^Zm8hP1KE+PcP674 z;mIMYpPpts^ZPw-ZCZ6m+&n+hDX=w_c&c&o304XaqZX2U-dTW4xfkoFvC zH3_<>hYf+pfZ$-M*RNArH6noS$toy(yYlz*qq%>-#W1mCd}DbqhcxeK;occfX*^ib zu+int{2+k^3lRbfAa^2fhXDC^Z$@JZkSifejN}U;K!r=m-(%gPBODZ_2joe} zv79Qjm#~~|h8-dSNaUJ|is;HV?Q1X^DI;`C26uRjFfZ7Ee!?>i#C2+Mofrtx(n<_? zi2Xa1MyBE>PliGHIH2m~FL&+MO(@J^H^7XL7ep)Mg`unCs*gapws-loiShIN>U?ft zBIjt*@m2v2e;hU}khnN<$fdAM$UdHUM*0Ngn&YZzDl$G_ch zVv|ZV*c4S4Rl;cxF^$B;eg+FcjX{lUr(d;{E*Rf{R*0xyZBlJ2D;=+YTa-TiLoNPC zmPDxF4M9>9MfUtT^ zf&u{)NZG;*+VkEEKRTHP_R`xr*Dcs}v`F7A(-u?0yl3B0!hrz6`!pnXJn7m)B1Xe` z3dsvw3i{Rl{4tAl6+P z{k_3KfX5@$>(>dPV}f)Ywo%rKizh`tnYtTa2E|!ywe|bypQ4&<252;obZcYN-?(A9Vf0cP`*9s=*Bzp`-M9b^8#MlTEA-|CYv$U8^bsrrT)}5?c8%>^VqYs(-A8g!8t9){Q*+BqVeR+-yAEdvh z8)enSKWXD#yr4}Ks#N4lqM9zEFR`)5G;rc=@vxNb;B}$O5Sz+^B!Y#UJt@ByZnh7` zkcyIe>&+`6N9tCS*Kjv(xO^>1b=k|`%H%sAyHWSQHTQU;eCH0;YpKtSGOj~BO9bqx zjZPS`aW`4gC!SB-!_=(Qq-oS6qb?Eqtp`mwW=|iMxi|fgY5J^j`l#q`wdmP+f316f zES>7%`FyxN+Kn5^c5VJ_DCTWPIoT5|Mq}vzc}Pf;)?4xQUuiO0d1is;#(r83#JW~H z2>>DNn~b~az#BpMr;kEX;Xi&nU0vkmH;OWJajBH|rx|z;yA%w(o-1%28Ju2$4!xgW za76{x7ox&)<3odk;`zhlr1br2OQg@%5n0M@-)VzYN+@Eo$Tt zc+HTr{2ZnfgD20Tv)Smgd^tRY24*fQR@4;C3PQ0AzC9?RDt`Xt+o563GF_HY;>nKf_MR3vZ>Nv~dJ!w-ZIb3R^(RI-J%b?DM|6TEz<^U*YappFxg z1%o3URhjbxB(Fa}S55lQpCMXPzMemIYd(RIl`meD1!-hLfLMo2$`Hy!1Z_xsp?)Xj zL&C9utCvxpEw|3vLig5Po7$86EUc^mz#)fT2x95CtgN6UnA_Q`WX}e8B0y2lDf|&! z$r;FtzHc{5JMz)M(Yz7}nJ)l#4LAz}$|)qA;d(7#9tqfU=+O)m)&GBjeX5TB-J!xBQ0{c#U1ZCCmjvevPi zu%Difz*0F}AARD-4uXmM2@ewBTOThE{n$#ld(rXtZxaLq&CiAOs7by+&=E0wK_dYC z`FZ?B0U`l8u9E@ZUw-`H!GoPYtobR>7L>LGg5+-)NwZvG(0c)8rh}ah$3Dk!>szFI zqQexVBoIK539QN|1k8uPa|jC5{2-7xs)iTqd7d!90)}`eJ=3>5O7e|(Rnr=4?~T}@j^`$C;!U=?0d}UagIKM^#d+h=E1lTk3N`a70`78 zU2v0%ITXRt=b%TmwYR6HT7J{GW&DN;IY~{BKqK^~2)$`V4@X;V?r9DpMIy2F)}sZ4 z=Zb}h6j%P{z_}}W#%#7MNz$# zDYK`s-ixa!HQd=UNo3qv-l?)KXtApjHd#sv3N{>Sq6cS79b@IF)wv1Xc}bz zO-0aLGaE%uqKfH+TK&ZcZ*atnm%R6@31utRsVG)d3;|YAhvTlnBFV0g!E+{JNbuhy z_2c!ux)t=r4O+x?G9!qw{mc}2Lgoo6Dc2VW^^u1+^+?>KzR6PM4`iA$;6}*Uwdx!$`7;u>LHSXl9v)(SbU|7+Bvql+Y5J(HA=H~uo$oTpwtm_+8P9&NL{iCn? zn8LF~;Cz7?!Q;mUIfE7sD}89-0*Q8}cC=*CE&!(m7OL2_c5dzahr#F?G&CS7Q@T*0 zIX=_-%3F=)koCxFrmam=V<7@k0Fh<_qy?A4Fc%sYw0ITn-`ePjjEv-VKemOhIN;Y2 z9OqVl`O*+^%Ry(kc^wIdr-=Iyl8{Je70UX{ly&_1)1V}JA&K^(Za5Ay4Zuoz-BTr6 z*?{pwxPZc{Fd!c?WypO3U%8do#||X`_FIBGGXQTSfC&ls-%`-jiFA``XaqpXR2J{D zqlEtdZ3t@8A#SJ{^kX1&=>Ugr1w&O;l^dF3!O`lSCmLa%gjUY9%*>X2Lb`8OplyET zHIG>KZU)_B+@=l*PhaR|^{0Y|f*3ba{of$Nfd*|Kb~~1P&DW);c0e!qfbY;&IO4Wp z%fAiBMM`e2!tP_)iWulB2&Gk*ZXtzDNEi5KvPvHsfXN^3qvD1xdBh-uTS4;D*w}B!LIP?LS*AUL0gg>{6rg0J#U`B%RF~TrtMM|@ zi|cA~(1`@97nHDnH~mAr?p>B{Z;3;jFCwubvPD2ZfbFOH=1oIh^6-d=4<#kMP`X?_ zY(@;Fq^188HHF|QlI|vF{5*Y4la1sYGB!xg)ebW5C}KuN^qES@A%V9TX(5ie5;x+J zlatd6o##mG$MH2PIxdn~=62qG9T)T>06+lJm<*<-XP(3hCSu_44e&yQ=p)}9QJMtt z8yZOEf(jG=e;UO8L59XF1I6b9bQOUn2?`$(_|ZszIOL0@Y;6n7rlx^}A@vpV{GblOCpF@5{_;k?9RM8B zH_fj=^+9xU{YLcs3w%wu1R@y%a~C=oP)$=832OcTMjz*jrn1GP;&8E)pNssNhGXy*gQBt zi2M>x)0X34OpgSOEh=wXI^Vo?3sJ-2yv;Y%*;xUx@V>%VZ@6wuwu&Q*RC@1gkRF{E z!ey3gVTrat0?J&%xt1a1ys5caA7l{@14Xtkeu79_IDC`CSmj=*6!Ikr5VY8VMzAw? zn>IW6q6#jPzhn-p1MTM6ff^#CUzB{%@fzSID-R$9!wTsopPnA&EOX||Jv;(}R_Gw+ zdb=R&%8rOh5CEfRIYgnRp}EFcvmXK7l>?&1uny-yN&7Y=?1Y5a`@YFlto^J$7s*F9 zDY7t;)&PhPEn6r$N->_u!Iu$$!erNa*Qv+tYDn8A6|^FVN^|5G1`Fr?ec!K2zJ0&*% z{eqii>~|`nD0Y?+5LJa{{7&{YRtVaIHUaqKp2s9KEx?_?Tg)c+jp~h#o+&CKU7Ihr zDhQhxRaeWvGA}Y#%xp%?I}*Hjfj|wp5vl7p{3K}17@)Y8pwdeOU~<=LuJ)UcXL|p~7t5dpp~{R)n;cp{pMyNP|C+ z;D=UTbOI?UN=?k=03EYP`@-@0?F$~B&R`HYwRo*q=+tH9{5IM&Qb)+}K+4HXWDpGg zE8z6#n_1!b2Cy{6{t9=Zzgud7WYnVTp64acZ_u**YIkFqzngn}-BVj~)>_y&IUU5T zcT&`}Ph}N?u&j8z?aB$CvRLU_GLJ|2ksC<~5LH=C8A9dNT0f(O@rjnqA#u;!V6d=W zw8knpc`!;;H8O{V!mEbHx(ny}1}`~k-}~^vA6DN#ws>z0oJ0%n#)f5t20v;$^|@wS zG*gK4!pKr;ULw=J{lVTs|NV~x?}-GB+2}`vo~>cC8||W}Ui&n10@HPXcYnBT$csi} z%}KSP_5qJ|(9pzXsa*ZA3YDxp`%C$cn8NA($>U$5rlqQRQleGA1TYd>*x&@bfq_x| zOM`tQRU>c6BoyZ#XXl_Z*2$Pl5eFugAagWdwx_$w_OPDppPiH8_9?o;6+Wk0CH9S) z9shs7NQx>lD$N5W+Fp(vC2d)*ZfeD)cSi7_((F;tD|=ZY-_-#!Se|-Y`B(uPq$?m; z+sIjWKDX~8?@oS5cNYReTKJVf1~F|ro8rev5f_6b}3Ygat9!>pa1^-tFC%<3{Ti< zM~RSb+zJ#ln6N28CWqGMwE6>yS} zv>^l>FTCJfjZn?JE9|P)=V$<3w(xG62M<*H4j}=`i|J$!t`RUCTckyBJXz4Gi&=Ib z2n)l8DHmB7@VLg6g~df0QPE^A($TMx=TwImZ}}-f0YC@Qfj;pFI_Shfp7>_n3^XF# zy=!;(ZQL1rkpm4Ur&5tJBN$c5EuNz@2VFh`k`w62K$18q%AD+(4Fdq}fIgq5cs)kZ;k zbMM;I*_{=Gb3{i31F~lQ2*`YFUp+H#)MkI{qN_+O^1iv0P(F z(BX_%KXT+qG@6x<=(oQ;Fu`SniWC7s;de80^hU7@7bLojzTZg`%If7x?&us7COJY#-K}fT+iNpW+EQPkFBwPUJ1Ct zzm8Q9{H;UMAC4e4PR?<~dmbo52#>)jX`(PV3a!U~@@iCGCyQM_Mz=k(gGZm>5jW_V z9@TtX2U3|3$P`T%&H`m6)?>)x9AtTL!-<0rer5Z=Yrk+NY>ZD?K`G<_ZC=mkCEU?Q z;3={f*8I~($E2W~{m-cj0dD>LBap5H6d`i0@4ZKFOLGIBwF~}A3)ednqSGGt#{o>i zQN&Nqb@!AArI5dB|H;ME(tdB$qN5O~im z8U?o`gai)d*8f>jc@^?of9*LA;uH#5D9N~igJcb@v*vtgz39bHkG?*hZ?_}5kSRFm z?mUcHz6OY6v!vv+h4qiBfI0O&_1#YJq^rYmBk3O?--Nx+vp8u0#pxn60|zNv>zrrT zy+O&Oq8*v(}wf?<@@{Q1@;QOB|$$uHgI#l9jc#I7Y=z3(Y@8Zj_0q4X%Y%s2q1mF z#x#%JdXNL|AJ0)@X6DS#Q!b@cf#}bHXAvnrg@e&?E2|7=qti}+eGp+m{D7k9<#7fp zFt6Lw_5lD}zIbAfDf%;_JO?t?vuX-n00~M9@&?IU)#L$kXfR z!{TC+`<2|xP=3g|d{h)q5OxEVUGLur>Ki?%sRJT$5p4Y1fBi}a*G{|TPmHXKq=Mgd z@nk(POGzQqb6sI9=0-oh(|*hkL9)Y@!Q`{6Ov)!9EP&l(dgqv^!e$HD>1VzZ`*CN* zM9}CHvmI-#BG4bMK$Gk6xc>$X{j}w!c2NOG?HYPWXwYP)_^5?EG}U5itf?hJcydjA zLe5RktSltL5P|FrVkx~@QhhF-rUz!85*id|FFMelwIBTt0e(y zQ1z!4iDyf>E&zoC8Vh?w<@vNS>m4sk-zLtT(XI+>S2_rs4fWWhv~FgeF@uJN`#$?} zp9rm+Y{ACdy_S_TQeh*Gmv?=7{xoXYLL@xY=d#tsA0Oh?G>Bj#3UHJF9#7PgtC0Gd>T3Th_b~LwWq&a-wNM49! zRa>O^`>QMD&9ucgfzHbtw=e$d`#ryye}DO3S5~(38@@1~WHyEz+C+&Rw?enX&|clh z#?8KQuxRyNd7+E#dyB)ghf8+E6j>f_-N?QX{++$TWo#;piEM9nrc9p=30YJ<(KR#_ zbngU5de_p>iR6r+h3SQWuA?{4|FBA1Kx0Q>04KCMWrBsdSH`7VJ_JD=u=w})!okUx zfSV@TlU6$I{)R(J&%IGOvvw5w66y7cpNN~!N^a}MqbCY9;x1oqN-@+M>T>2V{#t%KISUJ0Q&BeE2gSn$~TAvxlp zou|9g-B-k4QRK+5fm-JqT^EG*AuG*7t{V2GZI>=T&B>vJY+V|!8`j%PWMiONAV4B1 zdZm=z0ol*4ZatWva_N8Te~+ir5>l4M6kzz1yC6*dO#pHtg>&3Fvt1jOT>ZQXa}ak( zFcG_+nVDl=ke@Y!CcYRa1vbcvnGkQ+pikG1ETX5QZRrq;8@>IY|LfK}ckcY-kKs0k z3H8tF_oeGG3JMCN4<6Mu!6Yqqv-Ya#@!UC__qwM}-GAQ&6TnAwc6zq9wq{?RsVK)N ztLf~SS*F4Z1$cqgOMI?0ON9}}kKQ|BVQksHT^ke*;Cw{6Exxb+yAh1-sM4#vn|-H|z6F7F z&iW0whL&Zn>_v5}r_jtVs+sQO!2hnEgNPWWCxW1WC(0=)VFw=VzcLmLp^^?~C&0Hf z`&Zb1@rqldrFF460lia`IOF)!1voNkfRV7lQhSUQF9(U%}+EJ*g$Wn1#sY~ zCcp%d7yxLlnBmSdXZ$bj^dh?S&B*%s{d3`7b}m_W$Na}=2ZWmBJ9eSX2FQ=*zf-(3 zHoe)bc#5NIsS;=ZS4e!c@4c98eVz3&6&WjbNlQTf@Vt2zY`jHFsRZm)M%|?s|aD(ODAw2AH$x?%in#1$`Fg z=2ZA)Ac3ndbs%$Lf(1uUS2t>kUn~taB?LvfmQjGF2ClF^mRm*_N|u3m@o5?S4)o68 z+aTZ(5O}^Hx5|+?BxLPFO}k{QN+*(sAACYBguEa*fF7uF1O5bqr8xZ1`qR08e$uSb zveGIfxa9tWA}wHJSVZ_!{%fLO!=47KAR;Z z=6HmYnCu~<-~?NxhtcxH16BpL~^hCN5121B9@<5h>vr?$O0{Np)D4paw} zHi=C`K5vZygWaYZ@%~P>=JTE??2gDhz)by7ygB^;&LP49E0 zm@fc1cUoKdD){{(|J$UjtawC+Z0k=seZs_Ky>#`gJRWi&FT{y~yf&lc{Et4TD^;@TO(kMS;0;4>GJS(!bG*NgNJ9h9LNhiHmCi%7(sU{=j*mz^NRQ&!IfJ@AWM6_;jN~WmULhBPG;M zEcrh#$Y$VdnOt+ulhTr;E*&ebP>nbs_C@}i22|wYlsX@z(v*g6`7WIh3)p@7tj-rB z#XWE6Te#k&lv!(i7aEl2kO-d>qWTtx$giVE`S6%PI2N0BSMn82Som&cdcGfdcb+rv zYFA4B_56Nc>!C2NSaIk3;@-s~GrDmH9Ok~a^NQXNDz|3Id^zbCe92b43NNus?#yRr zs+B!ywGOEUR{x0?GVVst8lIJni)D3)l8IYS-L)kB%dJB~Gktw2J44>Kh%k=FVUF%w z^wp%kA<55px6w7jeY+3z9^RG+sG%%7*G5^7{l_uW-J6>>aAYb{NWTa*drlv-Q>AcB zbz0_4*KCwAPj@V(t=D7mrTXptuss^Cwcn8Za?acGYedrKXwjy%l*K&)az)PWY}T9U zOS5AHc2vJ=Vi3JLwSn30h**n*KdVUN!EM(L)o#x_C8~ICA|OXkxHF%PG0e?b@NoFJf%Iw-+%Gm(17D0_wkh``|eF`^#^$vX$J02I5xM6 zIXH+LO6?Te^;gS+t;eimiu;QnDECe|{Qb+h=!o$2*FqsH~I8S5j)1hpih=XUqD zUCBFX?ib>?aD2}9L&0Hf#*$rUOA1A|wnx6JIwYdj7ah1h;>H**Iac~?PwehP!Yw-# z6)SA(+KT9If3(jtt@F=8=n!wp3r>Ayc^c}%KQ?+&-geK~gu}yLz0&N+YHQ|Y zJjw7oGV7mPuz{W8p8pyzW(p6B2M#`J5JG9adL2RK$Z@jw_%;4qCr3w{H_1^{B6f1v zipb9Ga`wux=h%}-orT(2+jis4#`~U;k{9IK3-(U9`9++(9nn=1La$xh>EW2|Pq*&f z(P1SIhR)>(N)-G(nFpts8|ccog3_AD7%YEm@U-ZwQ`=tC#W#MTq)@Cbf8; ziaVe>S=}9!@3eO^bx6gRK3;eHTfNNbHX(o2bN#<-3r=sTEjr%{RiohXCUKnJmrfT| z(V^~QJgKgwp#tkO;&Go0c{2qrPwgE)8|fu-`!`_)j=xBk-Y)Iw;n7A?PTJaZ?@Mnm zqW%w z9$iE5Ew-YbpwynovZMxxOc4*)47J+yIWP{y2s$Kx0V=_NE6?`0R-^kQk^s~x?Y;4z z1JETggXh24;Y`)FMEsnv4Mh88#uj(stU$n#VB%U131UJMYHe+8^F>1OF5FG65Ii_2 zW{&x&5ZD7ozA!qS)WBXGRp6}p>dvdWzXwfxWpy>MKY+%D;mj&(yurx5%#i}U$Z|W8 zCbuy&Q>|+p(tG3uIqA>cyI~>;yTN5#jed>IXBSZ=-LrD_TNsWfNp#U71xysMbWdHE zgM|Sq1_(&^Hdn{ks&zHn%Z^5JAG@`;@=qg>uyFkN8|^TgAh2vCwa@wrdh3r3VbseZ zW5MT?_@N1N#f3)&(fi5mBW_F>1Oze1*{cpALMahM5wI36baAY{z6}sp&_-NV|F^b; z1FWL!#q|#7CisxQBJdpa8QtLr<<0Tls(d)E<5BEwR#6!k*I+YDzp(aIZwYISq zU#8asbVlCEe=*kw3~s_%fg4ADCKw>T>*>>XS`OWNJBCG*j%htELVu|>!m2(w?h&cH0 z>TohiJ$P{Vp_Z?2dF&x?WDgTF-1qe-I>j>}+6hoj z@=ug%!HA3|OdNoo9;&#g|JMTOJgdFqdICs3kz1nxF)TU=u!Dt9aTG+Aj{$8OxKv999@&yBdvi~F0_&h2pAr`&eg$O>k;hIf1gTGxmV&#~73sLBaiJFfM z6Z&cyiu-4{{8VB3t0^X3+~cDZ#63PLS+i${TOM<2Y=5zCRAwkXGyQ&YZg9#$0fd)T>R0MpiAiMK zu+2x8yi>aK-AG7&sbp}~h5)CO0om!sWAB)8%Lv@hXH>DoNtC~=q$dSw-)?zQqgZ#e6 zB{i2HHT#Y@tuR=2i05n+I4Lx=X-wu0*OsV92LlX?Rj|b;&|JrBe*=`@2Ut8x6GJVS6MDmS9IiDvcR>!Z~WYg^AnrCzq_AU4|e(B4*ql;>+v8|g2k3BqW^m(OK z=b3us)!H9fiZ$QFOi!oouKv4$Ip6r?|E@9K(1e!m=L zr0-uKbY2kWbg+C7h-dpd44Ll!>k5pa3U8m_ok%QqtFmUk`{LCA;a{=DtI^YQsN*+L zT)|35SN?#3R^B)q^^a1|U834WC_#aeWjG@DVFAaUdz7&&7J67{z9I}((b!b0%} zk9!#rF%d%^Pnum79MN+q1$=XEc>bN%t$sCgyFkS4Q94g>1BV98*vb`GPAJaq0x0mFAP!E-oCWg`9lyGb23@KS%GKK(J@WP4%1mqNpHlX7IN?GT5>&gN(+(p!+|poL-h@b>KpM^X#>j1Ec=yykj~NW?>BA* zR4YNz>F!xK`Az_}v0OW}3=A$E)jHq0@yM;ZI>AS^KX?gdayA3HW@5r7*BBO}e%dSu$pH>K zqH#;+d(g8q`_tqeCyJ70SkXV2tct~Xr6ORTWXhM9Z^U4RuXj(*$wKT-rYQhk^DzBK zmecw3>i;>nyH1#DPCmoUi-wWd%+zb^t`S57FW%ep$Dr4$i4$rj!kpvdCHwMHNiQ!D zoN99NxZ7aG%SYZ=xUsl#pp^KKGDAt{ySzjQY*@0aaypew|1ae0sn z;two|decQ9%rH>_l<_)x_kX=7B(&}BU5T44ImQy4IQXD~n=EyRDUgD8XgN^bm*=bG zdk-9cXwaaNl=uyybHFgpOUD-O!?(u{$Q`Kn{E79$G8A~) zkKT)lh~#wAe_<6>V&G{$w>Y{NCSS;D#LXTtH`5pEK-E49frqQRJ4;`6cxhi{7kr zcCr7gsiz!}C~tLw#!gr13ER1pEM`4EY(a*_fCpBe2laTwxn-Jpex|3 zKDL&no%>KqU2NuNaOrYi+Bf)FVa^#C7ilb z7-e&2b@?Pej?-Ji*96L^Q@jstyB3=pssFR)LyJ1?4CT;)#mJ0r=Qx*K_*GRH0)^8( z?M6hLS5&A9TkdX;Oh}5Q-a@CWy2te93Gr7Ezu1q3y?WAFv?<5g+&^iAy8O7Mfr#qv z2+9`9uRWpJxAE4JCO6I`6-KjLLs2X%Y~>hAAs-C1kCapEg*5mR8wwp(qEl13NE|kt zR1AF*9}Z;JCKhvCV=pRQ1IsN!zco!+r?UsHM| zljDbXV^<=g=8WumqMhe|-7)!LQO>oc*d^8=M`e)TLHvVx8c&3~k;cVjskszrNZ%@<;mCv8VV5#MgypY!PQURi%>R zL{SvZT>+@CG!Gu+EWgM^qhk26G5dO?;I%NH*9;(Ss(eoe){=PZ?SINyjW-2|O!8=JbgZ#RJY1)12P3c} zN+v8Bf*m&{qzf=>?Ih<3spTopKBb|O8l#q$Jx{{Pal&El)5b*oc?bU}n2H<_2}O7q z!0ObzpN8yZ5py4)Qv+J&{IGuC!Kb?;-au0-;KW41V*h39eu~^2_6c3J%kH5WjKAh#8(-@QCQ$JP9hVv-ICK#+hQDy(eUNq4^?Y#}F&OJf0HFeN?adgCN^cB~VM{ENS&+ zIcWLN7BGT)m^^3yOI9tq(Y^t_$yb~_j3f~jSDkA0f{TyO@=fa1wtrV3pLRroIL?*` z@TbT}HI^`r$uV0NtgETV*DX2GCFq6-GQ_HgDY_3tR8%PTFWc0OBJkvlN*3Wsu`%X8 z{RHPYA<2tF{#~fg6GJtL;CWpryP=yL+~kxJ`#E3@8Dzviu;$3-|5s~ECeCSy4aj(q z((^082F5}1FAWaw!&!$~50)<-2n`FbE`Zwk4}rI^_@o#d&EI_lq4tj+>7fTjIEm@F zAEr-znb7{y{rYw2?qPpwq2R@^5k55QV z|2&oR{D{R5Tv+nH5W{!)8Wxea_pJS%>U!b0Cq^VG5^Wu_XOdr}LG{SGXN`QmW|zN# zOihV5mCw>4rKA1%^Kj4!JKm0+u?Ow!52?X{W5-Nb4TrBkO$;)iONF$yO8=Hspw8q6 zGHwSiV%AgAl<9rc+8UkVC8Xj(vio|9M$5MpDs2EJU%`>xfi%{0)$?(`8G1H!#t2eG zyC2ndS=}i+GW`enOhq7NMlu5@0wn)e~?!^_#Mu1vxh+^Rj zPUVleW(9dO1fuVhdqdp6Sy?$_QUswa z0Hb@fpCUgCK?}YOJt_Rp$ZQ+D19!Z|@h5}RHARJmKAk-Qm!U&t-Xu?Rs@z&$b`A<; z5_NxuUoS7NPBxeZpOc{L7O|HKe!)n35n|(8RaF^LN?+-*2vD%=z`!tj9tw(m4GOZj z8&FTQKEw}G%6@;0mqL51 zgXoXSvdBM##il-P)b7QP_TV)Vq&tS1;l%*I@(gQ++Cw$J+m+yLu*(W7DyBwVWZZc^ zZ13sQ+kp|8+1asHe~_-ySo3pcQfz(}zCT&h>w!b1)_qf%)H*-CmT>mFd5@dzqOQDP z0sal6*Z6Qr!g{xQ`s7L`@ePID)%P+Dib1!@fgZSY++vY7SKYDgy^e9;#>wJGaeGb% z3C|6vl>}Tn>$sRFF#8vS)K0EbtvbKswLyzX1+#wyI+D9^f@@L498Y4d#>xu)k!YA-uCJrCH&lLyZ;M4cUVphqZQ zUZAqk*S^>7D)yfHnHRRe(~<#1ATaH7=zhBjZ{j%=1gGrUJbqyIQef?|iZ1vqUK~GL z{nhceexIo`=WgEkL$&+GLQy2*VevUAI`Viy#Lw|>es_dB##iO+zqKJ|UDS>vumj$E zd*Jidk{{L&d;RV^HSrr56zTsdJbYSnF4tn)MhjY_y>9!}!=CuXXV)D}$Vs72vNj0g z4~@xS@Jilnz1<<$O#XQ8F|qFsvaYgxKGsTyn_kO?g%{sc?Hu-&6Y2YXB>MB4xflcQ zttL8h@7G?1<4eZzD|4WNOP2HoyotqN54K6VZ3foW%D<~KB&!bsZom7Sqf}wPq3s38 zn3O0)($I;F_4=K|Qw13IR9>!(MoRN}Tzq^h^jrv4)O%O#_=lu3l9+6H20*aDD&f@G z2UolA8Cb0j<#l@TpE`Al2%!-&LbW0!s(>zpWC(|r9U6o2A7^V;>{-;h{3Njqp7I#O zeGF%T0ZBl_eZb-d%4}5kR)b( zxo8AXj>wh~jbw0l4DB%)l^E=uye=x2Y4O6HI1o-`)EwB{j-Lp?Apz?3Yu}hK>q6bf zZ>E<8dorFZns=7$M>RIjjRyzQD}Q19U_5TFat?C zMuy4EIF6eCD0Sk?x3@mbN$+H+NYcGuRzAZa+KJ)Sa>=#9km^ev}@Z1DC(mXBd3VNex7%*G{U846qvO^Q+KK?v+1 zsi?s#}YCOvaStBcUN`)V$p4ydIM=uC2jx-)-X8ET3I|sZ2ORNp<1A!Ai@cp zR6RTQKmUDYT#5A8DBW0GxA1zx43_|~C4sjyC1p>NW>P#ReUV550OS|mKfx1L`R)03 z7xXApq6+Yt#PROBeBa#QJc7igALB&ejc?oMc<=oTNmavhpKSP-#HZk<@$gIDl-MpR zN{ul-_mZywb>5$?wh3o`XVHx8tCW)gB-{=!!HarLJnKQbn%HTHI9U}YEr~Iv@i&kO>S$?&>$Oc8<|Rkj zQU3ir#^o&R#<%nh;QKX02C*z^R~X+Z`8GpzosIWWEd zjo0we#tlBSW1p~v5OQFUbG`t(!D8in;gfu1p$s>83x5~Sx$$3$2n*o9q29D{C3NeZ z1N9x(j5Ip#F1-426=}*(^>3r&hC*M>+Z$_wjLKMpK_<<<+ng}iSFWU~?+?HOFvY`^ zbs_@qj!#)u7xrylI~yXsHqU6TD>S}&Bi^3J!AGHq*^Vb`UExGq z=4j@QYLfgCJ z!=TN5N5Q)}QHRtc68I;oHB&|COE88e95HXhGoyu^yKWv~ywbN#q&U-0* znN{xK)c%0-r0$4x>tTOZxY zRhd4&A>G)BF$_?oPrn&FZwDv@JO%t$=U6&_o{d~^} z2O91)t($iHlb_2rEz*oB1v}7?d=HfQdDC|$P}^JqjG0qawTXf>=wz={s1D<|K6;c4 zYBlRJ!PMHZ%wDh~i%7krTRWxtKLrIluq}$1 zCs(rmp>4-VR90LDSffhLEgVByP->kKPvE6Lg#`t~VX9IW<`5x5`CsFvZF@;p`WO+2 zqWng2grWm&CE%ldXV30L)7IV769F)hbiDA;kSS7tJXt9~ST(|Ym6Vc#q+?pBc6)n! z{oZ55?pKUm83PM^H~V5v3`;Ni0DLJ3)H%WY?bxxSEDQfyz7O=#4kJk1`b%3MsU%DB z;1X#Y8mFE0Tc!3#YTJA zb6I;r1WAT40FbSmN3kM7eThj*HlHf5I4xQQW;Tq7gmKhCSA&DRY#tlB&r60!crlXl z0%>2~NL4OUP*H|KXpE6QsMm)*q!UkfrtToqm$9zi={E>=yVdU-$IrJi8fp=|8V25#KEmtsIE$w1_f=xFSkv+yphg zW(F0^@kq?bAURqI;{z@?QAQ}8o4;YCaUdM}aNa{!%#a-8Nb^nlQq@VpE(KK~pnOq9 z#lzmz6_!$S=Jy%Jw+x4VmT`WGlQwm^1_Mh-IQpA|r2oPZhN-eoYp#^v+zH_xvTds6 z4HOwOUrO+5MUP8Ql#4qVzNw(pL_#_MSV*^CerpWZZ^y#5twS1+CX`>KqMF`1Ff#jyF~;l<|v z8o9Iv9ZKNzkw{9C%f4mpIkkww6KS&Gs;#~MkonO_{hBV8Rc@qs=0z|Ah|(t1A^d^K z#1;SipjxmQoj&l%48YA=SJAr287WUE_G+b`TwYR0fYX~UQ+Gp46STO!L%6Vbsw zywIh)6^R{K4FU!S*CUOk! zK$7}}Q9DvbH=ei;M!~xQ5oPzndX$QU(Gv`qV87mD%Z$^YmCW@85&v7=0eRNgM@R;6 zzgR!+9Wlz{K^9}o+fAld1MDS~O1BF8U=d;`K0+*^iS!N4=PYy)<6<`aIf@A0b@jtyVPzVEEVxEVT6fMfW*J8|$Y##GyKQa5L zi6AV{M5djvQmug0@ncc-d=q#Q=b0Ik=k2{+H~oS{8#jMKcT1wdx-@=EL0gWykC}?r z5B(f8jzGnWjl(=s8<-KvZiE*V#u@JpX^|G>PknuRUY^@*qB zSKIjD5!Ddyk%41YHvWy71;V4CViXp$dg!=gdOw&&_z*Wx<5;MKoUo`oX5bubv3z zgJj1+44X8yv*p+V&X;OO0FY;3H* zX%CE*&^?mCTkIu%2Ga&pd%Bj$gn;?_e-}VHMwHd<^%_W53y>!1f@OdYk=sXOTyeNe8=QH{QVIGTx+2xQ5H zYXxRo$Q-4$InR(g_X0{3tk64wIR-3APUn$7gGq^lxoYQM9f`u=Aj)$ee7T7-1OnLW z_YS9L;lPHtl^gW{Q72<-B2pS+3>Q?27}qMoHbY_%k=cd4ab3nKlBUq5w!{o|ux%ap zs{&9w1O!7{e6EC>v+1p$k57g z?e`r6zL25;u``aeP*{T8pL~dd=IEwjQ%h+T+6~A{Z#{hYa)Ej`+Bl?_gAA003A$qY zm3=VkKoR_3iGq2bP-&xKlOeIj9fvMrkP+xAq$vWpS3)8{`q&Hw0v{PL_Dv18Wu23f zRUo>OZr;3tC*<;gr5_EtYSP@3dDO?v0{?M%(ltUsrQ(S70#pDoDml$R%jDju-@-*~8W?=<3h{`0Ca-@HH; z!^iqd+gr%wC!Ce&GG~uuq;cS*9Dp<#YU|xfx0^VQAta9E%E6{hX=B-gnM%pdYyFt@ z%7n&?ZnTQU<`$X3fb5m(o>+B-;+voz5wnm!^1~sA0+MR9h&H`~C;9loFXk&?i`n`Z z#=dB532+C8jY})>Sv@@5=T_mjMDiN}nrp*J*S?yMm=nZQ=S@;gG!&Q@Ozq?81-FUF zY2F4}Mpqnu=EQ+gR!B9=m52%v?*4o@SqM>kL$Bo!}{#(|bf2vzReyL(v< z^4N)4O39t2|1tc^S?_iKUd1h=A$)gv`n<$KIt_R&*&Hqt~2(e#@7a z@)5SQh1Q3jf8ECD2x8Ji$@w6{h(PPcd_bfaK|x6t}^ z&7WCvoJ`|$cq=s5_4x50-_m8}dGR^DBY`Htn+ z7<8z7cfJ6HX3d?)f?}2iVo6*KBD*Ul0>-SJGPsnZ`QbgxzT z>g8nR!b96!b}Qsul}%h~)lq-%*AA@H*=mx`+o+XIYaH16RwJgJK3}}2C6qeG z)T&gfF!c7KPX4bOw`7BdIo=z&bkjQCQdbs<_GY)azgYgL)?v7sN;=iAy-^@+LF)Xd zhsPzknPFJfkE*Iy$w<{!udv;(>dWX2y~hCNBc}VEgcFY~^>Ds?6N7q9QmW1Dq)>}T zYJN`h((X7TMpQ_eXHGP8yZTrheI_d{9l^Sho11Fy5tYZwDdjoqeQlchovakzEjqCAY<=@$>DXj-`}0YE{;Kq_+$`NYNeLodOFlP-(8?hkFKVdouQ}zWgQl9U=363#h8Jaq%XU~Y z7><5|^b?rPp!?br{H+E`L_(Grk4!u;3~SkkG|;4%>iq))m&w2ZWP*C8rGGdI!2{s9 z4P0DrmR%g7&DDfMgv5(ttY6z_M#EwFdChEW_B&Z!vZXi!uM&=CI@NRY%=*s1jVQ&! z&XbT@A)%4^bG=soA-Ru_vtZmWE8K-(j&W4OA!4Vfcn6t7*gvkozYD5ILhQbH!Z-${ z;5QfiUY3=Y6K1QKOhfE>|Nb0fN6c%be*k9xQ&c%Q4F(*1d<0XOK+vi~k3{BmgEGX% z2`=vv0V@oJa3}NJa4p~e5w$KqfM<8t2A#8d zR*mMU=GSBe>epb}UcA`vyi-mPkwZMim<3vR^a)WR zh&ZgYH%ej`By8JK#^(Sm83)X9u&lpO`;02nJ`{U7)4K2TWJoI9xcy=I`S}g-*Sb!e z3>g>uKHJYpq&rZrPNc^k#PKj#it7^08ABY^37>fr62G;KW1b~Oq zs})2qx>?D95t2{__xN?HB+x)f)nD#EAW5j*C)yhWQC$;_GeY`Cvb!8A>7RL#LJ#x8 zPQE%VM{=mq5X%TxR=C9bX&q$0#tCH`uUtL7<*xB8CloHPN zH&pXB(^nSl!FZiKEAM2B!-hI-mT*h4RB0=0bo}Y#bF+%6T69;!{D6K_2}3<3e5KaB z@?D=++m9yh+Vc65l#je){aNm=tC~`DpIYNyjlTX^wDwA#u4$#wk@==p&eVp2Ut05J zdnFF*?Ju^kI$$JzP||+MitcuNcFPi@#AA$BDVV#`H^KEgJ2d|?!`lf{*BJYYvgf2M z8CgplHYX0KwX}tuFM3>-Myr~To8=f5flxC%I4*ZXU$J{$rpW} z7vWG!N>A^T-obJ5J)U<$X2Hsub^@40JR_%snWMP#3Z8C4{X!5PDJFi6jVvrg>N+nV zJ&g$f%-D~HH|&C8cnC?cDju>M=6)yN}I}ab2gvpH7mU74`^2Ev;O) zCΠfvq_2o(4f<_Lc_)PM8dWn>mM(APkB z0IK4?ZJQ_;c%i(hbXfa0p_4szg4)mwTn-`b0E9@7RC9VKj;Lt=!!vahX9|qz&Z=X0 z9&s3BSGhQ%Bad8TmaQeDKY)E?Fv6*yR1`Qk{u@Atm1KB-AA~Jf6^qE( zCbC}<^+@>Dd60dm1dhmMfe$0ANCFm)rgp=C{(w1?fzM1>QC}OVS#KeYGWn;{e>@wp zOf;Z#D6k!btMPh{Pi8iJ)xp5#>#?EB0$%`LXUDy2|x^$dK=pJ3ZfL|t-Pcmh? zuRf&S&V8Bt@A6kNbQj+osY!c(r~{LH2IP4?Hu8#!3Z%jXH{wE(c`^s0X|@KYV1*%3 zJ7PHH>eUFqo8#9{7ZGjX#AD!>B-N+{1H&H8aJ3sh!pi%PqXWZx;GU>fT2bu}YfOvd z!E=!J!VrA{sSH3`8PtuqDc-TGzd%q=IG8&^-wkpTJwuoJPk!hn3uq3v_rO(PJpz>b z^@)EA|NgQ9B1!grAq9D{sO^(~SLK z=tmM{BxHtO{hv8c@{#pdkx9pjY%2~#T}@Qf5rtjInjFE000x>QZ&|nLsL0w$k|`+| zG%JT;>W6xFrt2R%^3Xa`+$i-HN*$2)KyY1Dk6)1uc6Kx9i+AZvb--L@utcOn!E-)X zaJT<$kzro=+vyhEVlwy|P{zv8E3+{KeLN$81%x;EORqkUc7HNRG~Z@z>G_8)ZuRB< zi2rEeG*t`3Y(ItEx>ujnD~MDgv>bT(pAq%3kFV!KmpvZJdMm9;Lk@rs>Ge+ zYT%>=PUMB#UirdKq}?9_F~%-$FVY0bj*j*#Y)A9}vXk%IH!TCzBcu5IajF-b6$$Xv z%R#mSxo2SeY$ub`mwP>)I05AG2bc~Q%9&=C-T#@4%J$BiS;M5oZ5PZAr4%Jx(u7qu zNRjA1VJTs_;)X&A6XW>JS$j}<*~6JSHai5-EP}wB}gJhX}_MNwtgW0t|vTL)M%eHHVSSDAPmvPVm z`u!ul^AL3Va|7Fk+S@0&FrA_FtDcUIe@pvz{Gh`1MEMr*jl7?OydU+g7#Ya5j(V!% zH~*vk0ylpbAJ6S}%@@z0>dON|1L_2%Od7?CkeJADN1V0HocW&&%o2fQ&q z1Za4m&IzPR7CIM-YW`SX;M+!^qs^Pc+wO^d5Hxp!yWu*Sq{RrkzKq9pkte*0-EOe>id{4}qOtD!~D`1JBsnF$lVG%ASTZU^Sz za}P9s@C95k%=@-Aq`c>nErT~}N#o|O=LQ@4PpGC)M@!oO*4?iqLEr1O*Mx;js`w3~ zUlv{S)mz)c<6`-pgq_b=7I+*9H8Wh8c|7|s`}K~d#S+RQRgT=@;n$WG=bh)Chb_ZC z9yi20Beg8^Vv&}?o7ymD|A|dLAzoWOHq?c)35mR=r%7N~kr$M5__-WV`t{f5usK6M z3CikC{_9d3Sz2>q9zIZwT5_~{HY{5f!4=dL_t>fLo$C{svqx5LJn2eVJe8v*JTSXe zEsHhFeK=xM9YrH&lb4*PNuf}bS_HGE@ahYp@Q=aokL|j%PBxr5R z7iz;)2e*z@`Zu-vx?G>UBGHj{qF>u2ziiLnivCRb7Id8sb$ChHVBI6>qXLZQ_J@Fv z6npwccS0stEVxeAXa3{;D0bZ=`_jjlfVAfi(aN)of7DDbV7l|TA?t+FnXfy;>x!Pm z*4i~ONIiWM%(yd{GWtTU9ErBx z$rPd8uDR!RIXY7m%$qP+_fnt)H8)jx@>-tGz}7g?)&U&=USEV?cfTlw0Q2j>-t5;e(}ls_wmL{h>cmHQo}g_nhSP*%C03uWbcFY5|hNSW5X^WHuAC# zUK{k<;u`+uL|1kzIVFi{?(l6x1O8Y1e)TD4n5D>mma(fU$WWa7c-7fZYO!v;S01^clq7 z82W(J>&Bo8Kyj5~r6(94V15qi$BDUyr2G#12)q>Dy=xbV4kHR#YwPQ7zc49p$6sRB zL5#52_fJae5mc;)ogIS&jSmhRxYH2TE{R+rSEqqicL-BjaMZ!ndal^7+Z`|dPV?id|--? zBHO|ly{CYMYe6CmKy%B*9W}T`pvb*&#EgTZh42EI zk+LeKA=+h@hvrbUq6)op>sCW#0kT1CmA{ zOi^f%$~>C%IpBtp7Zs?4vWE?k8qX1<4l9sN)8^vX7IQrrfrGYzek|}1w3jhs7Y-cx_iV!Dvn(sOaw0%SXh`epFPQ1_=gI#Tg^Qu(2BM@1l zQY8a^I;eMpvBejHjYRi%crN9O&d!vVdmw!)_8ifGV)4xdmyyA47!37bUz(yc(hrK) z6@|tYg8NUK54?9(sDyh9Cu?`Y0TD3v&B#KVPTmG;k*wsaR#%_rRU_F8{XtpEHhTFJ zrT6e?(xW5U5!8hyX*!2{#qP!sI-~wWLD(>GtM2eVD5{L6j~sK7q*b=?uhvw#tVu@& zj`Of}yL5!+&Y;4WJ$=HyCo<#?@0%&go^bA1QWLLh63!|NjC0SF{4;4F6jQ7HMcGu0 zdv?L`WLc_va}d+uC)w+vlUY zNm_DffOQ1#&>zLlr&RTG3_#>15L!1 zXDux;93lzhu*(36r9S1^;Kgk|MJNYn`AgDnDIGNU18u{+)sGaHbrH-|0g?{ekY?a~ zun)3(_z<76of*}-E|P3+uua78=+UFqe`%$2H*TJe%RN+%tB=;#{aGP4>$l6Q=HFdX zJn5g46r$a}wIie<1~#TP`Ep48Q8jDGPjO}ACP+nl1k^EbwJ z>zbPbrQ6T!st=(yH+sSpKB0^G*Xj6k1&jZowe#rm-`XFCL&!_(<&NIIwM= zZ&LM>LYI%g!F_qZAOXOp*Mc5p?{2KV1jKL?2x6a!NkV6v@F8w@oZ<)sIC;-+R)10L z3Q!tC$U_8m)r+$^QqMa5L#Ir+oOdL_fewQ_I^%fZ>fO8Z15!Buc+(O|^s`;QGyeHt|i`8+x42$}HhdiLWO+BSwH%e}ZO4SB!jh1C-M zRTU>L03|?bl>>Ilo+OiYHn1KEr^0EFzrXxz}L@SBlK{ za%u>rEdBYew-*h`_(fKYTgU8j$OuO-%<*_W;PzGR3Y60b;u^(4rhD*Uiru{@L{Iwl zV=HXtWv)~~;b0F)01^aKL^}+YnE-1msNDk+wl1LMFv;FRBbTv}^r> zbWWn`#p$pKBs^=sffDp;2!-BF;K#&7iiGM>HB3gZsgc1GOio8KK)MNw0f`?(HcgNi zP}r&07_tCJ!z4l!BVbX8@*M-u#?ICM764J5@6I;*TnVcTR}R&+1CR-kBw<`y$W32l ztMg$M-+kj)@auX~5+$l%RCz4_YiT8660#uBxlfiX3Sb}$jP|W$+#PPoN~@tws65ha8e$owDQ)Hyv$+veqPW=L^40P=)2CZ;H`AzxE zYwG!_p0St&p)5W>Lhoy>!IhO&p5{2uVYpbWp4^_e3G5fvDa;xtt0)x-DZW`fMN zZ6qqorL~HJ7<{4c=KcEhYfqvd@GdgV>sk|ob?4VVy9JAvBlC0`tATxV*0jOY}vS zYFY)sv{EFDwV)3($`>9x3+WwraxASo>s7&z6NL(9%T1qkgqD~D=mCt{So+eu;1J%7 z8U^ItH@iFIb;L$Vj@djhDO-upJ#|=Rw z12dE;Cr3UYdeS39oq`ZI=wqolXg6=+&@kP^j4A`97z9^lA9Z4a^yGp$~GkYp`J8a749UDSOTQ56~f-HU%;p#G^TrjV)8RG`s)Ahw^KE3aezk z=lu&#|3lMvfMeaa{ae|xNA_NsWkqCG_RJ_*sgTmTRYrEGM52K*B1sCVNZEx-R%9eZ zp@96CZZ@$FD5htn%0hAlz3!2j2 znA#R&IQ*;j>E;NZ?OS_q=lb(kQX!-4{**9NsTISN#QdPp3mFdgoPy)pH-+bf1RI;T zQ~snicm&$&{%BqyTZaCvb6=Q!W0(>$-oNp`g>fi&LHaCw}u^vx~mXu^$e~kJ#l$LS<_GSTQ{fpc+ z^=syWt=0y@-l3c#x)m#NS5L{VJT8&`WI1J*cti7 z^S#`sN~LdSf8{hjO>G*c+D7M6`l;9G0vD^tYJh3qjoPLYbksX*F85Etqm%o6kCvNv zMso)1W9m0@JEORlcGjG0i`~mSS<4u{gZoy*_MQUUare4Nl?SzPHh$axtZpx{v@x5~ zb8fU|I2V~^77|D?$lesiVs>NwM&q4_kD3j)QhLiWh8R*F%9^MVp|pRyxX3kH`|e@S zvM1|>TH)Pv)aPc`*!_ylxP~;dKe?NmYBj1&Qi)tnGA*?U9HNz=78eMSjj^qm<~n5b z?X2q8qK3bpxv1!Ls=OumzFD}@iyKFa=U?A``)9!~56$=4+bjL!l`HPPSfj}{$ni|! zH|idcaDAK=%RxWQD=o12>#bY{pT|1gx)IZ(w;rC6-kKulRsXoVPHBcR%3SBmo5h&_ zqSwbP)be)PXw#alQdo=4d+b`he{O$qVr6w?T=D2`QJdF-&DBPVK`F*Q=Qh$CHr?(S zybv*es<_jyT{u~q! zNlNi$p09Knw7B|oXN$V2j4Ic9*aFgFVkZY8d=^ud801Z=-@b<6p|FXv2k&RN@E{%(JUVuOC%4+4>uOI|XJ-^> z#_WOuxp4+!KLU9!t+PGa7s=sNSW$+`s~XCo29K>Do+;l+d7_)SyS+UEAzTE~x?4q6 zgQXI&W+V1CM=J-GCLgtO$?WB!-ZSip_w4-QeTnMMiTQHu12Z58J6@K6vP-0zK2CCa z$IBm=l?{=waz6Wl0KKz+v=D9|dFz%iQLAF>hvji%2oiJ zA-F8q*}4H}UeAkQ&8dfiBt!@VKh?OvAd>yT;27EJV9l2Rz7e@TgCwRCbML25ZpxH< zPOC#U0gJrb@A^?HT`R|=7oNl!UR1PB^u-efz-cp`*4ttV-x%zJwqks8a(;-ePVmHN z5FBhiskgH4q{;-BbOfVjWlFB>BWg0kjIXKK#V#`KN`2 zOgC@dTtb#kiBG#YvH4OyHKnweXpa>Hkc;DCF)fEHb3-QxEun%juB7hUlH+xLS7TM+a3rR-0 zMW)_B6fgT(4y(uI z)Ksb9#Q%kwhPU+$gDFBP-{;h}>+5>yo4q-+TRl`nB_yJjIcB$7H>Qm&AFsV(A-#Kt zrgH@AxWH}biW6yiX+~FZ=5rpOqh?<^eQO6G28@SjOtO3&TxTx{eC_yXT%!&~H%zBQ zHI6f9dZ&M2fTeQaN1jVO%5MRrs6UQRE&e_^X?*I`)5aB7C0-yN`^Vl7AI*X!e`N1) zq+_88fDWh6w%sY{!NSEpy8m^@#7PuZ!GAh;SD~(B&%uK-ir*V)>G3U*Q`ol1$;;+A z(%aENb>St)lb@b34=!G0UT(2F8vw9)k;Rifk`aIQl*g=f^5WQ+FSbtf za&G_4kK39mimvE{sXD9^l!7@r9Qruq(N2aMN9m;u2UcQh8`{NSUY>~y4Bn~s%q^F$ z6sf84;ab2v?ajHre<~3FD)@`z`|rPqb>+WdPz!EbxyCmx*L278_G_l6rx(OXI=2K@ zGlWn)&p9Hc!+szl@vys9(mrd&Y__F1_qb%)y<(gPtZnTc@67wdggl`Z850^I7?e)D zj#u;(3-R5XtD%@Zv6J3wyOd#}N{6lgZSJZaT6e0>TcO<5mh*m_s#DJc=LWgv`uv9c zC{aOy+_piRZvhmtx3AsnXa2J=&|tgw`n~O1m+I7a_^5n*u1{Yv&3)TM; zoS|uvqrGA>7tkvB;G3l1h-`oNL{93@PDNwSFjG05WW^ZggCDfQ-iec%5(C)5^(lk)a+Taq1_63l%3=4u_n7MJ2@dJEy09=f7N#P^D;Sa3inn)A#XDE; z$O)C6lmD+~gREuv_S*(0PnJ3D-OmH?hv&k}@A^is=S#L)YK0wm)H`z2pz4E%%1-I4 zW54x8Owo{zR3c#8&_uXU_VVJ%C^a{##)C|92d%{Tc(7Dh;xP7?o-q8Q`!ag3xX<~B zu`{6Km01`j9xBs6o~R8Mw7|LN&Z3_d{~-vD4R;%4OeuFzhT)tn$9W4akEW~>hH0WB z-@k`{w@x>7+m=!mNWHFZ?n^f)H{*Z+CB z_Uw$cfLT5tEEJ@R1nVD(lYVjr@v5TLgG$dL!RDt`gie8z8j z1qaOCJ4%35kQySf2G5(_ftN>~>1Spw;r%cKF@TJ^5#4LoNEZv}sUdtUAGxGMcFf?> zPaN-l5k%|Ytux5@Og%z{daW8_Zh>l{Sp!(>G0-hu&~KFTUtfbCnTF(1Vopipp`kn5 znUoyxps47j?&8NNJf7t*g41*jn#d!p|6LE6_Y~SUtw_G*>ERgOww|7y5^w5B7=Y{X zW~rqxXj5=42h%@LAcZ5(VXDRp`_3WqUWDY27E84W>ki`+jh_)g{}O2>1;yY#k9E5o z9EM_Khk-$a(;nkTP6#$3ed(AP+-xT%UzlMxjDWR`6tU=iPk)EtK7bh6v72$|V3Yg& zFl==rWDioAzyvoSP*xqU`nGyI#Mm7vAEqV3mgH7EV*lmOAdrpjcKgo9zSyCipZMRn z-ONR6%(}k1#rs9H$c3>_zC}$ygF6DC161z81I1E zOREyYEBj>exDDS1ClR4r3X~a5_lP}50f?s>|1o4Wfog{E;WMtZ26)lOozL&ZuCBE@ z-$@2kOgb?!=YSRxObMP9`yf$X9p$a95>W0C!_0#RuK;B+;t-KIVF0p&1%bvEq7b(s z$ReiJLmv*G9=@Bqi(Zi&LU?YdKn!>1;vo+wM-*`EnyJ8jNP0OvS<*-}r4&hd@sMXs zLU@c37d#_DDIiGFLw>-82OBmWfozeu2?-|#E9b*4GqHLIYM()FIs%N1)xKSQfyCkn zSVM_%2*G6}6O~-wK#qGg2wsSqAx!*r-kShW6ID_ziCOF&1JDk?PkVp=1!Tg21FlnX znVQxDs*T(D@Og(sqfGElnY1Nb+hmXgX*QH};i`}y-~nRACjzEkYL3QZqzM$D42fdG zuVVF3);-qo{U9hew1pUjUEQaRzZu6JLHHYv>oE#8if!C@!A~ykYJ!h^G_x_V;pcPj zsM|ER;}+M3jVSwW5uP}%{cT&L2G`#>e~L&RrLix zqoDe-1S&K3kZ4LGM7(JQsxuN6H)Jfh+3En61I@-HU=L~?`ZBgDW%D_2PC->K8ofsy84d7|Cv%?sF4Wdg7 zrv(7Sf#Kz@cw%u=n;U1DRojDvT{0Hk_YggY(E}`C+hb=N8_~iWJPX&1MGmR)zfIS) zZa;yPTym|sJ)~=$Bk3tnPq6{L-^@pDglL2*1Emo36|&X(|CtMITy5anMuvdAV(`+B*QL9 zG(^@pjpu=9$$u?t3G~*;ym|P9Vd@Gs#rMkm=-6b@61*EhdsKZ?xiL1TMn!+Wgi_1xbrjO zKv_fBj^?lC%iD88RuhiSe{khBOVVcy6MAl{(r6QTz50qETv|{*)g{?^vMyN;&GBZl zEl{*Z{-CiNwBqo7WI9IelCEG$zfM|}@;rA|^9lcZ0tI7D5$b%m^!uc#9$fEr)e2mr z|B-)`(aXbfdN@VMDu_kI^rV?+qXoT@;CTVzA-SH?$jp>i3h$#Q5>;L8GZK-X%FC*>d_kNE*Z@po} zT`SlYxA120r|0*?^^c>(vO*F{M&w5~nyHQ4Pp7EWZxc_iag$FvHYDKtR)` zmvmH^n|}N_VzOlfHgIg`<4+-&OUrEM#o_`F1s0tG)=qtR^y|Dz;@*jEyj$oXkC6`S zWt#VIFbn6^UkM2bt^VGSBg$_fo(0U=!M292bq;7k8P5sKYeB)m+(oDII~nl)4|~$< ze?If@p+oi-I>*O<{J3*?SvxLiw9fnDeaFJj#zWwjHPRCt--%Vc?9q6sWcMO+>LI!g z&PE@uxKgBjAf0S{ZsCnvEUXBIe;Frnvv z*H(Z?<%XIgqYWI7|AR{KC!`yN1RHkwtwSEgp+iJS4~4&Ej0J!Lgi3!#>aGFiC|!LS zY3ZEua_3F8Th4p{i5N>5Ngp-Zx4c>p9!94@V+S=d{8h>uHh_0TntTk=HQ$fFI2EXa z>l0rL(eg6OQ%LjoD)AsPXH+wR$)`K{;^xJn-&Lh?B+?pAQ-8@*z+Q-v2<9#PY_w&4 zBZfX^pecp=KJ0`bW)oX;}NP4 ztHC=XEsHozb|is`SE8*ZBDYH-+pN^9@Qz21L@lKCJ}k~PovuhWAVv^kcsue` zN7DCfN~Gl%12tn#X^R>FI{P($knT4Gr3qJW=3_2Lmvt!P1?2vzBV!XHKxk;e9FR;T z@>NLmVA@@nMu=`38^+aD;XJ~x!i_a%KeX?IR`VNYWDahAr$r4;E4g(QvTc%r!p%jA zG(Serd9ToLEVXh7Y3W$P{QJt{ul9~;tp`p-B=g>y7Ek*V$lU6-i!qvRm(iSA-~#EJi}1&Hn1HI!1*So6Uc`)I^$#m_F0uv-!b z>^s7eRjyxGOHn~S&(!dw@=Ko>X@e)06@kmlLYd z?v1cF9>7FneNS=c?>iWAcJJvu@aW88C}6C+vp$~M*%EvGw^d=6-Qn!ymYC<6<@QYs zn$Q5PigRPhohn@wn*y}qGIfXJQIXwn;+GN0giA9UhMiYFCwj%YOO?HQ@Lyi5TyOs9 zC(hTpCe0^gwBvrg&0MLEu{RK4PPxTlWcslxddI?hxoG7(3f>-DTZ2`tG%O=H3?-wd z%aua;xlS4Tr6RNO#EZAvR>KE&e)$?f?yqcYf~uohJ#X~Q;+yh zy0?}ZK3CqEcTIFqTK$!`YwsGH!N+T@8`85}=w}rUaeFAz8xC3q>h(tPq#ro<+{o0* z`iRkBD@WM@9Xje|%c0KizeuTr_jCbv zBy60$?tA&>Nl+XDVTePcy}Hs>36DimhJqIt#o0Z%n0l`jx%-wllX6~Y@+p#d1Xt#~ zd4AZtW91#t3iqnjtE@Fp^}ex zD?-OGEhbADdPEv`?sf#*7csqi_K@w_Alz25w6?d~Nm~b&i=0DAUBy@da7z(}g4ng5 z)3lJn4|pUUJR~V4M`uQkR}kw_@LLo?5vKl*W%@bLy{xDBuXd9rTQDS13)jY0l4Gqi zwAf*Zy0*0(fq`UsM<(Hg#=-xzg^0`nw$^&2Ywp$o6+xH)Wj(y%c4qw7?IO(36cgh6 z@;o<~o9QeSG3)Cp5#8a}>WHfpJQ3x$_8~*ylSGJwWlSvC+R2HRA|u5x6ZVf?PJ3gZ zmv!l=9h?e;hI@p#oKVE-YrH=&P=i=@dvjV4!-q>JqJL`p{{4AQ1Fl(N4sV8eX6cdq zA$)p>@@<;>h3*ROeU5;&NTxf;Ba@a*YoC!9@(K}K7AKx3N8}3;)e6C!~qM zg|t7)+ z^>AcfkYGSSH3!5~z@?kOp#f=K<$JKaUshwAm0+C|o8=t#K@!Pk7ekH9EI zAR`P@r?)n~yA0f(Shi8UhYuBg=`Zp60K_<#wiIu@@rFcU7b8}Bj7e`Zw`IM(fgbpLdk zrYiuT1@7__w{GVt2+lsklBk1su{mq9*VR}Y2!ipb?NoL}#KcSFNuo?sG~ z+8ZTpe;To@ey(2xe&~fCJpV%%L6;=UgP;4v*Fr&))HT;g@M=Wc2b;y>nbT6F zvQaqf&oKbMwu(g^s^qBz>C5<|;|I#|I2n4UGd<7fGn20iWDrk}^%x~ZK}xqe7{Die z;_KU5aKG5!ivgqn)y#|#$E}3htrjYGM$kVb=!R8yKB+aFFXz}^q*i`9yK)$kPhuA& zSlU+CUD5vqoT*Yy?!c-S$)uzY{{&DgVt)f*62$Q2{()=P(|Ms%BN>|@q;Q{Z!PATN zg_||2pH;rS!=Y|~Nt%NpoDZv(<$<>oi53i4-SsUI>f5d?nD!&y1*S~EDfJlI5p$xT zvz_K&;2He*z^8^`H>}0fYTBP%$n+8E1OJR%puH%D5TAWJQhYe}_z4{-1&e=j0e>R~ zP|U%(B$dyH5BMM z#8k&7heN1kuAc90l*CV0DppKCx+gd&h$~$^`mAZlrHuqILJaiMiy->`=Y>>vruNMpe0wE1+4#^4MuV^FJ#~{H)`DJqs#lO-_c^_3 z?m3-hv+o_3;#J9C=Tp%hIN?*gA-+ahiOoy&t4mJ8KFhYzJ<*4g#^RnJ-Zc4B>zer= z_PmeEJ&O#D4&zT7h1FbyMPwV+FPe?Vhblo~b^Ft^hekf4v7JLQ*=(L=SGP{*RyJRMwya>1-bVHZ@i%0gAtPcv93|MZ z(=Bjr?pFrB4Dce{sRJo-&HPD;L!7iC8If8|268sC>iw@H{`?eb7q;)z zmJuH^*}gr9-o$L#Omu5plkUN%{RR4FHGW)quOjHRCB+{147)IU#XpOQJzR2%zR|kp zs?BMR&A;g0JdFtsPp0XTPklXdw0_n`T+%e6W%GvhYjup{RW*#?BrpGIjx1&hG2=sO z|LhBe&6k;SMZblKClya9rx;H#s+*drzK@}*J017h?FjV;zsJObP`atDuN>%}(cR== zx{6p8k-R(WN{34Zjgn1PyaJCrO;0P2YNx$9F3Nx3Ced1S+P+R?%ca{V+3yBO`$n}~ zmZP(iJ6AULkXtOtNX~S<)zU3sTM&@bDN+p^oTsO*qxY1GR1Z?iFWbE#tDIrBALYaQt$fCzPOnNuB=X`$0+M`v@suixY zMkg zj~R^@&*bi1%dRSF4QVlXAwgf3&1glH=b7^O}o$kYqVp0lSKq%e{# zqlC+j!C6Eox&JM1_AUQt1)437_Rr+_tfWmo_7ZDUZ)D zvbfmJN@dO>T2BAdR?Am$E&KDiY8l_>CLUiqk(;UD9I^fGPtIV+(|q%bM|YlnlB~)i z)g#Ii=5u2pliuB~&M5wlf~(%Kx}T}vmA{|b!e`gaENR6M;u)?SVf{8UO4o8D3oc^K zQye{6(Y-N#8PQvwrWn2*+~K#l7&eH7?pib7MBcyAK~tcTFb+JMIj~KL1fzo__x& zl`Xp&yJ)>ioWUH9w;oqB_$EMiyH}#@%~#eGziUewj1!K&UnBG*bDa}7*%uHI(P-ez z?47{gseI&|X4YSwQ0e@mZmR`+MkqZz!z~aWu*x%pJeXSJ> z=E7?69DrF<<-W#FBDTL*%WxqRONu!4Ny z?hwb_CkZqBUTOQkA7WCTz3F`smMQWR>x2Pr?LNdeUO1Cwyz^^$m1`P=FL;(hav?mC z(i2rOlY~Rl-qB%r<}UtfC#P%Ho{1NMJmo|>9Xy!dT9v$*)bQKeUq?p=m0#Xx^oCbJ ze+r+#s?60=gmMU=Rjp|j(l5UTUVHPU*1TtNsSdGM_4m9%e?SLvp6UmEwQlc%F@+60 zAa7ZJnOFZ^3nqzY;0nWman%pAy@3AuA3KADnynKKg^=ICekZG-5WTwy$78X)iLVQj zU%bq%8YH#I)ej){4LgS_M7s~in)W~HB?)8yCRjxB>-OnwC=1)RI?oB2t#6%c!OxctQCN})VFWXLRF_}gwUz@!-Z8xbQiaUsWlx^mI{Eep?9lk|#*pB2sl@Lk zje^A8hSwtjUQi7+H8sVj?#BhBabjy+dk5H(^Jsf&)1Z$3D!Bxji=;nGN=cn*OFEhm z(Eo%5L&*;Cem2tb*WSbcjL3fal$SIn`aLe&E5`Z|Uyta2$b1^rLR<+MfqlNjVn1pM zc@ESw+-UhyeD(>!ser9XHp_<#J>;knoe6*|hyD+c*kjg&2%92&^bz>ldd$YgF&r4B z$npetJ)j%mede3QfmxqCd2$HHyHRKQc@#uC;NLMs20!Neimn8%&d)FxlITZ^$H}{P?E=+p7#R3O!5S?x@uR@aXKDvodfh z^39h>u($k|;4KtWR&K3Vkjc#>@SneieUZHil}PJc6IN$fD0Tbx^`ER9xLa6g1c+4q z6zvrrBY2w-HV2Ccxpdykf)w{e!UJYR2pj+rVcH@~6G9jy`=J(uSnrpt{dS9XMDWbr z;GBleJ||TCKObnmNicu2_H&#%$ovQs++~$~27eYARTVpqY!jcv%mXxi@?!-jB;|Qw z5?8N4Xe9yV#@5ygSF;hjkBkwiP-WA@n{bCC81GFt%LDj9F#}tQTjY2@di02FIL&v* zi5L!{&A7zWRMp!r-a7Gai?9-vAyirST`Q6ou8g|8ZD}!q$>_f6X$TC7QFDLiJ7yXv zop4ym>F8b22>5gnw4CK&aGn~a*$8mVJ2NNw)69p$AqR0l%|;daZp_TZWUK3;TDOjp=)yb=CL{i z>*(9~_oq0+b~%b3kT{ZR$oCcve!6DGCGFKT(SLz{`TdaIx<}!!z@bQ@;$$y*Hs9Bu1ga7y1vVek5b4yLXs3iQvDI za7EiON3F5kS1K!-VB}#9!FR%ukoTbXt-vEUuyVkt2PJfI2??o|e-1zDeI8O2w;+pO z9Z3CsYATOgvhGC$vJ3%9n=jM@$$$+RP|x=OkP)R+Xpm+5m5D4!az%g1=DDyAuP(-F zL&0d_rsv5Wry8_PXnG+Y(sn1JvYZn9S2I?_%pG>}4=O9u-^m%MoG7#_fT+&!18sU~ zObryK6&1a2o~bxQ>rz2>f#jOr;5g+k9^thfNnOvAwdhch#38IbPE$ps9SA*v3i8^E z0CYMnAx2u$J4O$6jzRN28DVR`lT8LdtP`0fQQ4q-DC%s9sTde?Ue}|PAf?5(xMPWi zfUq7k!5Di=G6EvWstFD}S%3tFVDlvw^ktpLyAw#s9+pQFZ|_|1LGn9+v~1+5867%a zVFS<$?l(a|*=3hXY<#{?Owzlr}Jd6J95CN9jAtlSRu>i~Wr zYK#>q_vKuu#3EYKHA!C70{wcB?EktcGrvp{Y6nk|D}Hmn?r6CSs~2RY@59o3ybiXc#cXcfdZn9&rwT~eZ77J% zuPS6ydenNsYWJ_$ke%J&(Ls9kHmF^pW)j9g;JvP@_9ET`t|!|~;O8lw3?+6;{9SZ+ zojM494dxZkj4~TS1ZN#V-=J0Cz_~1ayaGEth>vij!?1X; z8d1Yo1neFfA8y#LQ@M9S!h89~jd;=rSy6F@B6tHo5lV&r>RK8R`5OmJmh~{Nc{xZF zFdQfuelgq7Q7^2L5xQ)i>Q*4)uy5dOopVa6N*;^8}yC@7#XxthZ7WoL|JXWBq$d= zxZz+%1ZD_QTS6)Fu8CG1C>}|)yRH}dY&0X;rwJ+w()U4L2o$7Wq6Wp1agcT--lLd2 zfl~s22onvdNycX-A+&`Lo-vXDV8n3c8K`cnft??h43KI4BFEd9N&aDu^73q)oDy4} zLRi>ggf|4m5WeCol8y`E1S@B*%K4KxkqCP*zox{J{?EX8b#YSxU>Mk7}XY_a{uDp2S&L;6R+RM=GZ@;1+Xq)X~D1glKyR z0PvkYldumPR9i3ct22hJes@3i^~OQ;k^~hU3pDDUIEgqSF_t4Y07S0MUaLlIpMysA z0N%uiU5FrZ8J{96j6rJ)PpduA3wly0{-+c!dmxa+_lU&))})Sa>_o*yc%u;c3$GGI z5|@@vlUOwB_nVJ>1cg_QWIW`x<6K~5A#O)RMQj_d;B$zdh0Rd~C>a?lmkQ$dVtCa0 ze*aP3z?X2O+D0~Ck%1-h&Dn!E(n{q+RP8mFvcvH(B!Cz(uElSS@ulB+)Bra&re;8& zEEG*JeZhbSZd9UB2#zkeA?nepIx$Urot;)XmX1Bt%OB~GgDj97fD!8@{$;@itMl1_ z?+7+D*Kv-S+tV7gAV?uYl=sgw0l2`>l)ht3jV)=s4yH%&Z@ps~(vp%x@It&oZ_oJj zk!KUq%ER$5=nvnybLaE@NAGgJKE|@K?{$jY`-^Yyyz^-Jg`@;n=7_QG*O%9=t&KRu z`(t#ol*`BKn3RpdV6F}zdMpBf>hAqJCcRt#^=A2a*p>fZDgC)4u5~5*8_SV$g!?J0 z?>VSnGEeLI4x%~@u2CXUGT_FI(}*6Wtj-C^ewyWlRAhc6!U8|D^n$B3)yTzV19V`N z0OLYG?o^@w&R++=?Y7mE%ZDL^#a(UU;&Q{<@&MFFsQpPxnu&s2bcJgnT+b<%70JcMKNiQ<9go}5F>+WI-Vw+2NtgG2?xdsBuucBJrhbEHfnHQ>y|ONu9FE20HJFgFYksPs_54QxkjRBw_KS@f4YMNJnB*)9 zSQzPxPd9}8q18dFv9%cOlI3P>){&dWN(IVOCN3Sm(kA$G<^9CT_fO4~n&Qi#MFm${ zFp*@eAb;aFo`y3a?k!!gQ)0s17-UuC7<~OgXJ@C}UbZUPC+p*8ICpLuu_3d_H0`Lm zVQ%$kFg$zrrDDUEtl{F8cP^=ww-X)Nz|C#4nzy<)09~`9_i+s-+QlhYmQV^doHX;X z!H5b6U4HQnHu+n|7L9frsGXC3iyo5UI@?0ckax%u$xBK~N|ReZ@G48-UKK8bJ~7qw zTEY{%Sx>J^4~y%|5G+8r?cA=aJ>DU`(FSmolf(Eu0u?x#s@$#w=; z=y|glFa4yOG#GGlkJk{Hx#;{W1_~*^Ql2Mc7oH{s0$CVPrc#~Ys7;)|`8-xvp;)y0 z;!%1_|Kd0f&%|}6ztvKfIELKAnum8P96gkkRaWiY678i&o7xwbAsc;%LoO+0ozaHL zQ;gMT?)sjMjIgQCH#V0x&Z@K5D$CE!G-~!X%qnXgo~Pts3|V`tYJS03Cx@$6<-n8H zh+Joe55q~^eJPBglYdhgKd;GB(?^%dFz@tO5&ghBn=oo4cfgG|n67=9Wpp>&GsUIK zL^jS}HXT2HU&wAcWfWnMH_Uk}B_-plHBH3MOU)VdGh>qVbJeM*W}Vms&X1^btuV`e z*KD6;FFK?(JpGm1Ct5t%XG2qfG?lHE8jrDkkwXtlSy;B)Ml%}wY@40O)n4nGpXS@M zB|5ZK|1B%~Jk^$P=3Tc&S*^|maMfpVE&bVYnfX_|z_(Q=#xRRL_wPL2)~6LSG}P)! zRlO@=!_MdnJWzMB#V+crxz_~H4IcINZlKXCsEXcmi0dME41JfKX=Th08vleZ5sp@} z?YrgAtZ`W>Fl%4=VWZi;KsP#j?q-~ab$!IL&$;^Hp3DWSJHMG@+&45HJO8OZ?X^pU zdBL>E?=Y3Q5%Y1|$n2{eQTo^48Clr#pC7jtw~-UdwDH~ZrR=Vuz0yip*qAEUs;*SA zsYFjkR<^!Y_d-_Z45iGa0!KzKJxRlzs)<)vB03DTthx+q{}|uY%&WU*_klUAPfFl{ zvL=10+?5MkCGyWOK29=;PS)4p^;$2&_&n*?8E^GDuT>sLS%wDI^oxbtP_7xZ(zLaB zmG_Z;an-rnp_I=ODoN@}2Zh9|KFP?3U9HdRm=V$mWJVr~A!Jdl!T3K?ld>3=&t)iI zbpHLs5o|!Du`TCR7{uE?saJHL|9>rjuP?(r_hG|Rr!)~)lN}eKuBo{XdmV;A>Ncm` zRS=u@@2wyw(L-#@@*H@Y1OyeEv{qruKmTJc2B63Yg8H0J2H*vZGPd-jGMyyR$W9Da zBeseL%*Jg{FP{qe^qmQ>dF+>=+#)boMEUqr`-J+gFDK)tN+IL474iNIBL%q;tY> zW+lPt(d~Rrkyooe{BVoENmf=4Oi?@HWKCzJvmH4C}W5P3;n4UoH znmh!z$cupI{0Syno#;^TNoYmk17(L4bk9vUj32^vLQ~kOp^NU%Sb2ShgKD+fccX%VCp`C!zDa(l4a!ynCM9EF)|vG%MNY2 zF$1+aiL8j|@?&}Jk~ww;G&U&##&iprYw}_6pFE@h9Upr_=iB%1$MEM-5ZJu1NMIxc z5h9EOUl9&5bbtDRzDGW9^OmV^-|9doT5Ws*3k$wjE#e{}`v$i)TLEr&J;@*>3ZTcV zh`Ol3CIr5c;!3lm=^_UBfAG);W;TA7JY_ctV_+P8{r$G^>%+g)UI_agi6kUhL-JX` zE+NZd0O5ZQ@o7%D2Y~e8KS(mb^O`X5tPurJ9ASHeY6n0_&c63J+J9H7|JLK8XT8#F zydo0ED$?e}a}2}Mbxe53nIWz}HGZ76w*R^Q;2Z}^~jzIj!()}P{#D=?P&70H21O)apLooig!G@S{{ZLS)^DlmBKMss?G9o7&) zRL_pe?C|P8Z>Iv`bE`Wjc3Lh7o7PV=pSNz&tQ?2MX42n;3+m!;gF%i!0QF_ci;8A7 z(k)WclA-8FN5q;TA}5g;@I_g&_nBj1>S3@}uhX*Pd!2oRWjr?evGK+M*Kra47Ffyc z(w(c~{PwnY0*wCmcJVBgIc&{9 zYKC20dVfMLy~DdjMc0|f*C|>VSWN~)x5P6275;Wi=gYy3P$rc*i?404Lh1;iK%HO@%jJByqM6llob7~Zg6Fp7W#(FeXKI8 z49Q1b^E+dVhXhU<2mqbK)q15X87tMfbLWb=nRvxFZ?<3XM(HQA)0pA!6AM{qqyV@W z-N8B2yqh*$d$SJQHB!=)xkIyPZg);VQA*rV$}Q`Erb9&J6A-}qI+|A=+CgzeMN3D4 z_qd~pO`=5_wJ1oX=e?ny$AWp!|35B3&Uh?6++t~N zZUnTd=)I_g3O*8+;Py|Sen^$=KX}mIGx5H1w&^%57EsRT4S|@yTNuHIM<_@VxeBXP z;YmYEz%$6Ga#7^kAn_!}QYBKD0zD;7SA|YgxZ*MPF!n&g*ct6AyH@xBP#a*GA{=P| zYrrp7?%nip#sA?6#jnpcDTw-AfIOhw%@~VNb_L)*r~8sVdbq6nQw;G!#BFZaeFyeN z0L}N?{2ujgb=9DUgA$e((T(pp7pZulVj@@+4->XI96DT`9nq6pTd{A!L1URryXBG* zz->Ipewakz5S`3q-(wDm6dvN|L*i0BY#2ypHjWc9?;zM&Y>x~{$bCgp0Dt`WK{+`b z0M)0RHR4)HL@SZbG?RK55~2)qm~Cg|%-%JXXdDXI6zgLMDnx3B!1bAT@@JvvNY`8?~K{b1r@x-}euyY(W@NkP#)jj5#@mE zDxdE#Cl(oE{tc~b!h`qHw$%x1;G&oPSBT03&`u<-jI2fyc8zYBx{f^ag37p~%uVIM zu~<<_M<4VEzld;5BpZiV+TI83b$(Zb_YA!2ukiCxUrm9+;=~g@2noN5>(-GTT!DOV zm`LBWij#29G(zP%gLnP;XPcA$a&8+(vQ*no+&T4VIZuS5$0ow!?T$I1-i-hY+xz+$ zvjPULUA_7l8md|-*-iSX!*R1cM~yI%5&u_@VdQ1S*sTlj^s&L%k~7$FOpv(t$q@yX zpCXf&pFe`3vJn6m<$TK{8=sR}g8vGz@IT&u@j|!QV86=UNDY96qzK9j(y8Q^bqqwl zgST1svu8mR!Ak;o{No4yxy!M*yFC_)+5Hq*J}A&yhXG?jTt6&Y7nml=>&InYJsuzV zKBepXuQG~YRJSF#F>=9fC`|-t2%Vo`8fAa>a3gqTmjr4t%?|H*f>_-QN<8 z`%vLDY|;x&2?=UQX$Y!?^im26A^{@}NZJp) z>%Axbm8}9KK!rL3ASNJKAA#ngjQ4pXzf5cCm%_7)m4m|oe?h0jpg{}l$_!@O^TJ8a zq)HFp3%0MH9iPts-mpLi6%RR*IzP&(iCJ7gI+&Y>M;viO7@_B8Rybx=r9y?FG{miA z{rHL9*7s9^BNsQnBdN}KzO2UwCy*-f&C6wSQllzCPR_u~OB{KUV_z1h9)2xCD#F9( zvy8Af>aLHvUoG}K6cRP$Hyp+AQQgNTE+rMiNZZgAKO-HOsLIDoZVCVww(jmC4mMPc z$4HjbQILBz9xA6pM6y}xGKm}uNG^uwb(Z?Fs%qeNavg4WD*0V>_bx>n@3EG-tswq>f=V0#zMAj2efFk*Iv!Mb-VyUWQjh2{ zfaB^J!4R5vX!|6Bln5gTXa#g9SW8NZe`%_?p*sT`M&ZAX0k;Q-ofoztgF^*$yLMlN z_Yvl*H+~EB;6{$ZBg&%xumLi1JTjv2!);V!$En<*$^oRqe$qP$ZbE3fbfdq^OZ0(v8Kx}^_eI4g3;U;D;W@iWmLqn(BoOrj2c!``%QfJR zP*ObeIu5$Lk|(ZbxYBHvfz}$>+Fp)*V$h>p`WyURy9TV!Fu8>?2Giu<4CFt@D)M7^ zPCbcAx80k=GWTc;z2=`st$O5^NuIXvTSSBT@NA*j^@$`al)cg1wCG` z7gF!{5`IovTH2@c8=8iJF%AC;77I95IY119fKP=Sg7q_NG!Z`dIUS6f!?0`` z)rGxz{dy~vtPAD#CMGyFgMvgEY4Jcb#J^{8 zh&rm}HPm6|cT!}ySR40JBuL}l4~Ugvjv zjS9Ouf}_4L030Lt@0;HDPBaI?qPm!yB2+eWG)r^QuEy9MYVCcqAafnP zOBwP04trkptWJHt!zzkR;m@&W(v;NEx2&X>IJ$H7-yT`hv(AyH&N~{YaAEsuYt9i5`rD#g~@2SQsexA{rjY2itG6H%p`G&$JNh!%AD zul4s^ReX(_gT%^4k7`*dL|B)%FY`0I9~fMV?w`72z-P9iAE#zRzd`v;ThC9g8=?YT zJj@L|;as#)1??gcX~v$(WAs9=Z>*zeo!wij;xfDb>J2mK5ZXH*s?6G0JPHwy@TXl1 zzj&oOBxjq3wfofNGy6X*=ts;jmKOYGR1-~4?TE_a!`1ogAV-?)OP?D{(rDv&wh+P) z;w_je{O*hBtKiT90p@Dn;LPNep0KFHog!5*$QjMPu-k21vQ~29b@vuW`IIYTJqJf` zNqvu=ER@`zCa6n4v3>l}Lx;;8jLi44Si)5KXf8xV+t9Ob4`-m(G8|ZlXDkur=Xfc% zIN)EUJ}kII?Re-j>m$YkkA%+CJM?omp}@;D&z(U%dVX`;oSBC8@wkO9y*>6CHhLBs zW~pXhHf!#cWqW(JcGfL>=yc8l1F4g3B8TO7jk0B5@TqY&V4u+)G%kGiOr=Tax{GI` zphFKwT)3Jd{h5&R`PvlJNd4OCGH7<@VcV|o)ej%$ytW3{;FZ>w zCNiVWV3mKOP0~$xC)+ljZqFOs+D)c8+xU#NtZu+Gi9PVmy(a2;38^sCkr`~mrYA+jvBD+lw7C)%Asqkfyk3cdkwz56! zp0FLHX{ow`TZRtD#^R%-jI#>Z8BEqEFGfcQD4>xQgS=64UL_VTnFhp}Wl%WgVcn#-nJ zk)NGyV5@WFY5#3`qkr4|qa2n2uM(R+SmAgsm>di3z)$by)O76=yznwd_VNoXS+-U^MYJU^Cy6?M0LG#WE>RLpF zw)u$>-!qWHph+O6-MBJ|e1JI2BK^vgw)C5Z!Fz+TnQ!`}EV%K1fR)1DXI#)ev+Ne= z144l%XLR*%yl>u7xbBI?t*3Oqd5@YZ!V96%!6-;xZT|Pd5g$ZG`Ry1f@JN983`$kJ zUM`V1V3Q|^Vj>_0Ud&028H}qJ8!%?y*CgXW=+GF%*j&((xyGIpf*sVB zL}Oy;k;=p}?TnTmjEI8r_V zK)gJNV|TLJ7zs3j80nx(6n@tZ+hBI@e9WLh-xgRIu;~{3zCO0XaXk|43xCKcDyk$HT9DCNq#@@c>EF%1LA4~4gyYgYPkB< z+|~pSVpUMIoH(f+)vfe(Y>Wimf>uwC@Q0eN(6Pu6qRv?Nm0Z&+#0r~sCL#+L77li< z?=6RWRc3tTV4+6fnpZ5IRr|16AOR#6GEFEk0dv3`gcW4_iTQZlengfb@6B!)YX)X6 z;_rapf+KHlB!b9D46oZ68S2_BjwO0A_OFUkEu~e5sHbdL!nb!^jvU8DRC>T30(^EC z9h=Kx;ioQT>t{<&3N9%+?Jj1}ii${KQ&`Y}Q1*ozItjnA|B}BN_WD}qlWpmld`90| zGm`z4v>mf%oR*lLCNgc1@iOD+l%^ND#}MMXJ8JJ0{|;v!g}w2QTaIk4vkc0j(OWpU z;33(ir?XHSj=N*y$jHckbx+B4v3%P&@5pehC5&2dKM}CVq#T^DbNnVopYrOD>dnO+ zD(h?=;<}QggGGNnWI3nwr#ZfnG01;rov3BM*NAeiOu=8Kum$yNUZFO#x`s*9`x@14 zwD}!upVdW(_+QE#buhh7m+t%NL&5gj?aX(xz*&kJHW|a!C1vP1b0DVgsDb}*Qj4L* zqt`t558k}=l7^P5qIzB5Sh)3J#y*}(88*ce<+Xk_JvEed8)1u)-M2qRc_FE3O`-AQ zYK%jgu+$*~Q|(Bf|2ZEYwV3ZYdp+ywkj2^H}Mr;!ueL?j_&EY8P&-R)5Th z@zY!F+J25-Bi+)$=d+el?mD*UIrF6_J%Y(*0#^A~(!RZ1?=acw#cgP)>Sp$}*Gb22 z+M`f^ow+pzX$fMJRfX5vkDtB1dhM6;L&K3;JRjFR9SBSx9fXDg+U1|XCTzRpx3rLA zCU}TaLmWZxgNPxbdX^4%)nAp4g)v}w&{{P7%hl15hiDC&KYybrJ~3t$f(`9`E*m3> zlPMiRNA*vX(#I_nORzk;_wn~6uG__rD% z*$3sH8F!y@q)+ad2PKhTb&>`y>VFnSVAR{IfCQ12YwaG+;zO|S_t<7G@ETE#%# z9MBpYwS{^Qn}>UkU3+l~qza)BfN5@Z6^j%7i~~{qca;^iP%Lr}`1;Y2ftX>ZGF=qx z0$wHENf0`@h5haSe>7bOIG63)CVORNRYVy@cB!natb|Z1WS3-TkCLrLC`mG+k{8(& zvZW}Lva?c&P*zEO=iT>x|D)so9=)&e{GR82?)$pV>l`2c(B7TR+qHEv9kBu=5$*8I zm@!40ygArJ=-=LOgxhh+5FxPHks|>!A_VF~7=rKsN~tCSClXTE2Qd+mQE0KYfsZS4 z=n04${yGEXZRKq|G@k|;Gx|}@v?J*f*f?=1!XtZeF{y6_&`!RtUsso+lZ#7?; zvNtuZJ56&~O)HzvD}sYwk6urAz(xAm{_oYZ9fqe)oi0grpjr*M8;~%RmQr!y!|ZB4 z5sLv{7_Ycqg0VMfPf%P%A+UpK7-V@&dKmGrrE6Eh)=z?_Qsh5xcX(9*l@IzxTc9jy zTR(2I;iZK|b`ykmv{AO8GSJNO!!?|6dz{a)adSrzyCCd__nG32T$RwgV+`doP~PEA zd(oDcpg8_;x;^o6n)-yHDhe~q`AD`Pe%YB6&@liPi9lQTa?|!3yc$L+ay0)-g-wqR zOq4kPDfRN8=Ub;;`zo^p0EhF18oqL61HlTI51mg`@hp) zBS!>}WD8kXAWGJsJ|xOXh+Pm}(mEJ?YEL09=%{&)E}ZPvY1*_OtXj~=TfukIVhzW2 zV(0arDSNLRqng~uX+O!JyG8QY<50zU`b=q zY$sZ7mh1N^>ro6~PO1F;`8J{lj!t9W*Vid`h&dKh2&`{_u|&V zI*noAMuei|tY2^9RkfLr9RWGBq#5e0US&o8HL(|S%BIE%i{g*)(lY3TO|~f9PEBQp z1vl8!rl+5(^(F?c8tSUNu2>)Ow$*O#$f0S8uonmS?BBm7PGyM~p4-)zVi5KMdxpm- zf}g!eYI7j;_b%kq6fQ?aM{h%kib_k}(NTmX1)$XAz*Ios!Ip0{r7lNt6$0&cIExP z|IY>J1*};I(&CBSyOr^64tTaGtXt7ed``A9g2TW(TtNk;rHvL&j?IByy1mi-*s&NO zvMqVbxzNKx^Z>>?s?@RfziTFMpM4Bj#Nk*;b@)l(5PJ+q2#Puor{><88pXhEO%2KPOl8pyq*P*rsZnfNiQ!sK$!C&4p$tZa9nEc(JC6K zvq}#Barb5Ke9CG}A8nw85r1q(!W;`j3Xqcz$L*U1*Xk{87e{leJtinV06W z%1gsH2R5rHGRdX1OO$+?2@s<0KXxnTYaa|H_j(RAFO4j``ykvzmtJ{S?a|e!X1nVX zk2S7zR#owaP&Ue7f-fN8t+8hG>uS3n$GX_wo0o2Mb$z+eYtx#FE$n1UjoQVUfR%)j zTham5@iW^6hbYy=W9F3y^mOQ5zHDH~mT;`@tJ0#P^i`?RTcPg|yFy*`gQK`>>|N^F z)i3!cn?#fR$2e~bYfJR{d~^B1e?cwVAELy@+!)q~rG%fFlbi$hZ?Z;+vfpkqdEhVPV(d_5OCRFRgJix8K=7&DE$gaHGPmwkQ|+qj^WnM6^ZSz}J7!YWU-HW6~oph7F*=i#%@fH{aeW8T&wMD$6AI^W;~_ zPdkOC9kwNEwErX=w}m$r7vh6-SAVDQ2s7-R7vQ30 z(CU}T#t8$6qwyi{AAfS>VM9E zyK*Kw?)kZkCsyq~db!@G=NF`N+TjDLy))O&{!NYAEgdcj@FzXIn7OCzi23QI(7$>a zA^R5CPe^>MQ?KLB_RvgcxY)iQe(Iaqrp^@=#VgJhP*aOEisd9SaE=txF{RuxX-Iy2 zU}gH5NrK9`9?mqH3t}=K&gADaC+zXC?}=TM7owO}^xDH9`#448d$shB*gVC#k5%PM zo0gLf)^aKyPAqHYjOIrqhOsKu*7EO_fwX@&)G40|zLmG*&gwe!x6;DMT?A+&C=3|STojCeFlgbb& z^|_Wg_J)40nzG8z&rK!T)^Cm6XB($!F}W_e$^o>trKrBcP2p_Eb4?D&@q{>@();^s z)A@`1XAP(7uUXMq3-%ZqO)Zk zIQm&h*1PF$R}P=5T>Wo`?+4%7Or+J;@w9}pg%&V(8x~a7)`&im?&eV2yZ*NHTqGX@ z^=}a~7urObkYV1G^1InNGE^p0da4H{DUaWd{7A1<%m##Z|HrChL@4Ufud+Dy*L zFH<)1T>O;f<^_Aa9WK?V!P3e&NYOe+!>YsZq5M>Tmm}>Pf8FDIg3mpa$gSwVR}ehA zjXC{ayXWTPfBImCk|LGabNoG@- z(21WjE~oFZY0#_lMvT63E?XFRxgE-nlC!R}`!<*^}TX;ZY8rkjj+A0T-aiXFS?og-I`#4a~rq@AL777a%*9C|$lcX7-#k#1Ci``EsD*WOqfpzc-hWo)FHS$rQ@fX$ zNr@kw@T(##E9_Me$~=G>h2_>wDad#^d>cvW7m;W0{6_qgpA&MiZnHev1_un!FWn-T zE*CD74{`Hm_iOlVqkv3UqOjA{|22zYZl(8^gzR5@SG8e@@#b~y$!WizOgnN8b_0-gfj9oRiSLQP z^q9aX;EK_9CJcYXkPD)%P?OvZ|D6RAqq;gR576+fbdaWX-C~A4FKx1vRRbn(@U)~! zF`$K6?!B(D*flPqE&P)xLr_A zNm)IK0Mj4EIUdMx9=x=nfc!x6;y3PplwYn2@*CoOiK{hHPPIgY;GU5bZ&CzrzfRH{ z(Yhkio&_wKPwSTZ@OTIb&L1wUYl;UXHZT%_88~X*9rXG0ma_ zi7XK2oiO*Ufhja$)8k+#aZ?McNmd}7fzgIZZn!OP+%K>9uRvmmUi|w0&4!auy+Gp% zVNx_4-u!Q_T>$(4A2Xq^q;+<;DbOuIPsnKD1ibF2+_qTpZ0%ock7NlfXh1~)CR=*p zX91wEkv6gPCeX$yy{~N?K&yZ$F8(_xUirV0L->{b*+>nJ22d#ZB~0Pw!3Mxgk_u%c zvdfqI5J)hNVIc6+O(d)l03fVr?g2#&`L!^A%wqZzNN^ZzKMq`UCMn(g!+S0^)Q8g% zc>y@zaf>Z85776FA|EJ&?jB&q-(Ug&JIpzIXF6~CQ|j&8bC~Mdkgx z>INgcXs5K0IJOn#Jd#fa@yTBQi6sB&UResst>-zeoI~I_^tqfn(Kp$lH%@4%pk6nv z_k3wgn2aki<9!&-nVvjAV6?v+ae1JuFMJzH-P0*;dlTD`C&sg(6ILP$^1|k#1Batk zYQa4vyabOK2%PDem>Piq!sX%2TIG*17>eyZH_3eH+(-8id<`-X0{ycC$KdETM$}4q zJKd^K+yfYRVBeP3iXIVJI1n)9wBtvGX=JJ)Vc^gv?Xdo0pyJVMtIKd5q&4vAL8w}n znFkn$#H#O#_B}fJ&FDoC*4U-{+cMQ8#UP5@>7@a8O4E5H7-M=GHhJGiG3{+;jMWmJ zj_1$sj=$0jL;*2wcHF7kvd&HL6TSme?t=;U{i#w`K`)FxS;%vvcLpv*`i0LcTUuMU z0kpQYwGDfnrX0Y51Vk>D?L*u#9TK&2m5W?~wb)T`GtYAULdF>-Uf6sE*YD5Z`2^{i z^7n0tr^O6z)miL`vVC~<1L8?(>f|>Vm>18SdpbmvpBP{RXwMC2^&8%z8qAB0z${4X zofUG{bhyRc_9M`Cvr6Cw9x2O=`dPjxb(reVGV@X(vQM`H$PTel2|TtGEeh1}#;tuj z==GzF#C2~{ST~nwgn#bXp1`JEz9)M_GI(^uqX{Zr*XxR=<2OcpF0z`8w0mCW(@+uZ zw=s9m7D_>bE5;#xt`lMWh8cO9Q&Af0DYaai4XZ?!3T%2GH@$Uo@oxH(Iy=W}z^3GX z{m+?$zjal6js=C^y8gIBZ)JU+Z$xC| zVTLVd3X^;}Gh>Yd6*H^!KHrq~9S)wm(P#hqX+cbecRyhvYH3AmFM5=ppL6+=TBb3A zv`jVqYxUqhjtP@RjYXz05b_gAvL(lMk|T%y_m1S302GA8yF8(~h0yo29%Ok0MSm}u zCX>Z|Ip>JU8LS9sub`cL68ck)GCpGQ;w4X{60r#gNChGy0L)gGMV0G!8DqN$R_E-j zRWsfccJ3rYCK1VJ0=s{Ev+$7c8;TJ=(F6HM-<%(xl$ab44yV)@A7o$TvX5_?Oz4g3 zJE{G5SV7ax0txp}Zz_L(Q;f4Jm;an#N;0Mf>iu7RE3y^jQ#;HqX-$gmtJJ@~o>6wa z-RsBoIr}sm#Wyg5=27v&xk*OyOjAQcgH~&^T~Q~qybDO1hub759?328OsFBD3B1n( zDTUaWe%x-roJE|A=Th?NT^ngc2JGx#{y7xWCLI5ypn7H;Bv;YIw(wb-TQ@`hW~T)U z9?!_`e_bIKc5U>apT+op9*V_iw@UCd{#a#+MufunjDWcF4~M zz)omdGUtE$*I#XbWe*^aMu?fnIWE5T0|m!O2_VQWjSx2eNH zOC;0v)!Ep$Ngg2pKX-t1U0zT+&m(BJQ|psUnC2g%s|SMzV)W#UeoI|B`iw@inTxc2 zbFjIR30iW&E@oj&Gv={(4txCgfcvuphYqo@>@^%aR(#~%utNQ-Q@^YAZuUPtui<-l zco)m?t;6mcX{|@UuA&DbLp(UAv%`2Z%;z`gV*q(X@9e)YT=55}IG0DJ3D7MQ@ckbvM)TtfHwg+tlP6T~4rx{j z5}&sFEH{FTStyeB*}oA59ThbK$A=4lRSsQlqu~ouef*{TnR6d(l!;vyb{@7tQ*0n} z1e*RKWOtYFBL|UDj|Ya7!2Bgnloz1CkY|aZ41ZPtn})G$@X!;VMgV5S_o3cKCgyG! z9Mqx6c^6{*Rcm^i`pt|Qy8jF6BCZ31(xK;Ja>>IibsDC(coq#`a%Y1ARPiW z0p7O40*j=`MGFMd6P+d;i)^g$E@J)(>idqw{-e)7dXaA?Izc#xe%6>kSP04Hr3eLQ z@E+cK;1A?KlCZ|PIgk2J4sv|R*_em*1R2w)zQ10hhT#ISIU_bws4VCj=T3_Xg*a3U z6k+6vLXq(25i8gT>Yqxsaqw%79V2&Dg8XOMP1mTOo-m|mZiMLp10J??_Kl^wv_LYW6NSx5}wa*8PbkdxcKGp8~5QdsJsd#-@$} z3e{?8DFp7$*OHHc^+RZlc<2Yd)ZpZ+w?QOF9h7^ylNtt7uU)ronmAc2SEIMwyF4!& zt)=CIfH%XzKMq0M>~lL?`e8${u&}TVQZ#V@WSP)qO5i>d1bgT?__DIz zpN*fyN8E+Z9b@*rH`YBFax+L(IFtlDL(~jMI4y24l%NzDjtN(fPWOSYM%M6oPCsn> zL=ukm%6Z3f0!OqkViq^!ui>aIU|)zB1d->{*FS}FM+HJvs`@@o>H1=z3Q1wW&r`Qx z8MzR$%VR`MTwoR%rNj%)=%wYDp%i{D&}i~YvV1rLYxBZvd4)wq2?!1Dmw@w$@?Vj` zuU@>+IDh_p^6Pm9?<*m9rAL-_e`qdGbg-V;#}hfJyo^17L@y&W5>&c}qG7YJGP@;X zyAIIeq~8!A>n>C|;*#u78JU>K9blE`Dz}ne9XsDzn=H{YUaM&^)_Q~Ydl$#juWfkO zh@lFmk3;t=7ll3|_nMFevtPG1H`|Ui#Uc(o-=(MzuQcXPIdn*V!5S2w-`X!zE1|5s z8DRaMv~lzzL>3CwVTLxHlk66fOo^>T@?l3JV{L7cmn?HQplN14IcglYS<&kO2LjC7 zhJig`V?=F|yjpyk1IQ|*XJyraj_G4D7qLrXl0|yj_rJF9~a`x5p?vy zn=S_JYObG#29}R`WcNfSYyg@Ly$$2tZ`q-2r;FN}3#8}md==A>o}4A-C)H$_S^GBG z%DkvpFQiYetH*ga@BQYVO{tz$y*n24sjmy;oGi@!Hu#O3%F8a%ULp1A72y(zx>Uu% zO^OyTbfAph$U+gLA99h~C;gd^=-q`{T@Rq2@L1)de=PKN*`z0=w!AgD=w_Qk{#1L< z!edrjm(#2%2HIZ)pIWNs9vA#>#mV+$tH>hEes_XxkuTQf4vT01#{2ASw+oKuQE@~U zOA5Ls$^FrCJlL;!m#d^+WM5vN@82{or~MCkkqWIob19p5Q{qJ1gchT}ShZ!8x@e-_ zdLa!_nd1+e&Ko~+(N@_MX*;c{C2+%9=;g({^O!UCcjq^GQm@ zp6u=|sw<7&y7|dTtJ#mfUwpsZZFKI_%P@z2Bvz$nE8GIp>WO z40hP02lizi&*os+O1nODhHc}IJlV4A`!~7Ea30-3zu`^SNvE%t3-4v`=dX=+cwP*0 z&-gXO6P5Izr|OrJK{q=NWbksjZK`bN+EVgy_V*l(t7LfIvo8@*hClph?IF^$I)n`5u~60vy*-1^uV7Y&Gr@XTT|^tjN%Vid|Um$Zcu&ydnxMH zr)0CTrkvK1IyL<&RS6xyRcLVek=RMxjG){dF;~meX{??RDU16c+@P)8RD0Nrm=b$ z>M+tdD)zyDoVYRp?-1alod|eJ{yCQDxgmtKQOtyNiOZ z`Na(h6>d|sT?Ct9-ndIQz8;2tB&!yWg=%tWH!53reP24?%0dG|+&@VNrr-%69W@AHA^8p;*Dhjt|@uo(^+yE~}X)UIBq?|38skd*P}HobX+N)n#JZ z@c2Z&YEVj~$43~zkZcWN-ieZ!dUh2YD)>&(>d~EZha4P}uN6G61N{9coycK#ZpC+L z3>fXgHYw{JR%)|1t&-gD@PXvFD%atL=&M2+QO-~b<66l zbL{b!KEvO-zAh%I3tkLro?g)nJv?&3bN;zrTQcuH;S5;{G^GI(DidKY|6T02%xOAl zFRx_w1xEEOy$t*|oyDIZ(0r-Wv3y);KcCI{$gPBU=)8EFQI5(*MOUohvd?$HfGTi@S&LbI{{ zab_uvoqf~I|JDh$=CJ+N%y=JD(Ws)NazdugIw~$y=2dVI{}y{SUKZcaazgzAVcs5* zavB0%(*~kiFJ6=#KAktfFn*||%Yrs_NxQS^5N6Q;6A9iV7mRuBMacW0$}?fwQM9sh zOAs`H_g_ibQXi?IHTl3WEz1!xYuj(=gFfH+yHLYXV6R6;&yJ(ecEJpIbaXV#@g1<( z($b{ST2cir=FO!cd)tIZK-S#m0xRC=+GwSwXcJ_Vk7KU%Y)!D5hiEn#;FFVT%^r!O@XH z^^?b5k3YWyEUW$5LqDkx3=9f)C#!+8;EmvK5Fx{s0asd9dQbv^6!`G+qZfJI%`Q!P zRXz5bJsI~sCRTs>+feSMq>ek+pPhbnr@r|zkIK6u6qLwCN5i4=!bUz4NEX1LMnp%3 zd|^DvM!OxD5cVk^*ROzsKwIp~VZA(Q{#& z=d8xZw?jEb#`hk;{=heeoOR;kM1ND09!l1%GyK`e|HI~?HN81)_+f z`qxAibvayg$b`ERPC7I>`!HG{U-;N5pwNol~d&U9|NdzvTH^8dL2DnWM0p(G(#07o39 zdcQsaRgq+0f<3C6GF6VgK)(Xrx$ogkwS@JDkOUx9OL^_DX!%%$Zb;ZJJRjBi2WV|1 zs&j7m6+eC~6Sr`I8rnASO`0Da$s^v4eh3Q?E=w+2XX3~uoRKg9h0m^lTx{6~)- zkwqfI9IhTtJ4!6XA=rGlQ3^o{XaswoUDyv@x(aV~d2dzO1Ab;}tA@r#g1itifWQL* zx4+vr5aVS)JKc%#`$>oeF)YC0)y<|lIK5r%1*%R3gEsiS0(<$5k8+nD~yOLk|<> zpSg;=h1!A{7!;>Bv%@X!)3u11$3&uYx2jX8AW7c<&Y$H7>|kN`!hPknp9}C>Gz>({WoGs^_ll=+ zq$9}%1@~eDW2AX(1mFWu*aM7WhZS}$X@AD<1dUjy+)ssrUe{@`31N5?g}n=}Zr7We z6=y$pW|scOZ2%no`fNJxyZDT-7?X93uwUb>)9QM-#6cJIg6*;%KjK{C)6-cILg80M zum`OaSKdQeizRzfiIKYMX&K(mlG8?0zZ>~A8}`0%kR!TeF)=#$LC^k*V$(3EX_Ns8 zX)}fKJ>S}ccmpBUgcK0aP@3-?Y6W5h75cZ!M2b_>NeiXX6TeyfcB)8?i-4CN9pzFQ zp(2)#Xdw@!kyt>CL?m*D02yX4$YFQDG;QIr{RS`UE4Ue6Ud86Ay6ym|Nl)zWUzsf< zN4S&LAAJsBQGzyAIC)y_ay&t3F!$g$Q#wz5LqFLi+k}r}Y!?tBnjF62x>H$|kF;xl zdDi&TbB-|eH+Xm&ogL<_SQ5TJ>_T<%ovmHWugrUVhUG;&E3O51+eH0L=VUWads$PV zlIE#FpP3hW3+dO{jb$odbpr*P?i78kFZ*bx6M}8wWC^Y3A+^e9Wi*y72Xs35%P*Bk z(AK!Qdo0truiDA%ee;iCAvruaHfzBkP| zd(Y`zIx5R{uFe|2L(KQrSoj`>%hJs3%NKUMj#ppsQ7;vFo*nNRTA_F{-zMF#O180J zsg^+~Lal*EJHjPln(CZRSd!6n;Lq&eCLhMnru->C^6*)IB0KXL*@*g^6hY&fz1O_c z+O+6E5$WV&ez`Uu%0k=a_IRuDTEnRq;UQ9yYz;kOHZrku2ObJ81Y5lL>MVFAd(ULH zM7gB#oR5pr(?akOm`0}};6~kS$KV+DuK{8SJ|*KqeeY6<$}V1^%%&UBBdJwFmSuv> z*4s~6lCRMCv^Cz-Qj>l?KGtcu6k~I9vEvP<&?Uxu<oP4jU>Uej}r`PEZ z{=!ML%`*W_K4y^`d^^{LUK_U5IK?%_#$?p&=dPz0o4pKOiDhkYmFfE{0LQRDhzuQE z66RQ_#Y7hR7n8NC+S@SqI+NamedXAr;ks-0P9yF4+u%OE(Zd+Hp)Ox1S0R0}diy@z zIgqn{OBLpk4h%-NWU~Fu-N{KUBqT)Xz5Zr(F8@BeB4B9bJtI3)(*>AV_=61VIX`y5 z`K%x}mC?>qCa$iMmO_lIZhtd%<@A6Cum#~_b5++j+Fstj9w7CKX$ z4!Q;g*^A?syAYbFPLiQ8PWIMinGuUQEI+`YFPaE3-QhUH@_D6!-jh=Y64G8e$lZRc zEL<}ZExo3%P>ZcC?ya>YF>eKwO&kQ#lDi@}s#zm8hQiUy}KZaWHA z@RB$9yxM_K02rFFlCr^j%PjN1QLImUkJB#Fr#Y^VKu=p6>RJk144~RB8M!TxbYlt4 zW9HdiU5=lI=`xXd!dlRY58P`_Ff$w-07ijUOrW`XzFc@zLT!@K@cuo~=lr#BsABxB_}B z9FucspnaS<0#Nh;mq%4Eh~tzoGOXYn3^O7%C~;zeH-J?&KGBv4BCWS(Tue+ITFFm6 z@;_j>nC%_8`2<#Ud~!0fT-8_wU0?_c)(2w3c(DeSO0@5(7oMz%$LqqI_YSZ#ii2Gr zo<0L?2(aJ|g|zg(b-{b$TM-Ly@-xL(xWihrQX20o1CTqjvEsFC8H;3k8E z(7EikUk6%!tX^zGDUSw+zTF&bC3eLiZljI2vNn7mzunG(hO@t)XV_`G#fgCwn>W_Q zdWlta7!{5{Vy5!hGv{_~L6BPzU=36ibn?$@<8(LnJ-hB9XBV_C_k!SR@^q`lh`xBC z0$YFmq33EXV2r$>(tPlG)y84v3CDz2mHE1sBl{8#7{u7?d>sTl zp@R=#cxUfLP|eq2KW>GUB7$v|Bn!_z&Z<$kZ)3AmX>;1&PTqg|L#5AB+hV(D>8BI~w z;NYOXoZd83deFChFK%5|uEhte1alv0tlPI+t-%+8Y&0M?{d_ARMD3he3a=TzKbP1| zu%BYWJm}fV4-0Ab>OwRE_{>}L!(rAF+rI_XaF_Beq9f9K(Kd($LK+lVhv#hdpKyN6 z!60M|^1Frj?(2darc1Qi!e_BXV7)+iu%FBx!SRWRjy9bP%`Ru05Y?Wxk3+N{3Fw1H z5t@-m!w2CVr=Cvns&&6p4f$mQheSe+e|rK13_8RITBUAo3(GO$yDy^2ahxgmHjToY zR=`RU)E^Fgr``W^A~9^nk?!uPgNv1q)V$ISL>u9dDT_JdqoJ|x`@rkK`Je*VBF{aI zT?_XR61f(1c3N+$()M2vIQ#CuOmO1%peG=NZY0DWDZPSeChglcVeG=F<2IoM5d^)a zVnC%O+LjmZx;;wv{K-b3ax(CxeKc&2cvt%=jAn7r(f(IE!x1eFWHFMZ zXJicW4Nia5gSac!#e0lpaI}n~?PN1QhQot7Eu;bf+T?5>IvZ)<^zFHH3?5neGM zFD-`82xqyBZiAR>gMWoBolLpNcnUDZF#YI+7V*?3#-8A~ftm%=5|Z)~%fKlbvvCeK z5d!G0V2-7pyxCOsaSDrU%eI|6nOXP$mLYx$8g1K<0eJILx`s?10u=^TFL5^eu6=yxI5fZ-rSF^rzaufdz1ZxB;d+j2_E~ z@l#^w#pGrX{4iqJi+-G@r)1BNHz)DFa`{j@^lA4ifv2y&S(gt9*cRwcQZK2veZjd5(AZhfhvvU}FIkesI{GyhUfxQMFE9cT4c=I^G< zLvZ9LlSBc%|aH$})6u5vZKtmNbI5Hmcqx!(gvcq3}SE z0)(_UdspDEi?Wn-$0P=6qGNxXs3KgBpw4K4Ht=)Pw-+PwCwaCGLu%^(B`0~iyrlPz z8~S*F8dAyjKy1)%_XsW`HrI(l z7W?ry%EO$)R4W?Al|yd8mVqlU62<^90mZ6GS1OC!+6u>tRr{WKrqvK?2m(dX`7$oC z_A%j=>Kq)r0)ksW(pL%0>gV4-`@@BoO29;?z}pze2IQsaoZ<8Dlze3~vDn=SHH}=m zM)HY#ZJ1mTb|M@q@S6OdalfE>^5jVg`OV*Qx||5f;XjLtSJEf4jJFdsW|Umzq^Jv* zN);dt*p#86_MB|z15t7#bE6H))|hM8mTp-G2Nh%Gl999Lxc0L%)=za;=!;HmYA;FI zSU$7S*6aymWw(G`@9az|OfzyTZyG&np_3Z5HQHM4SrE?{Z?#`;zBfJcg|M$Lo*5W` zcOz@b<1xP)l-+n4XH1cz(`RayYnBBQ#JluuN;%38%)}2Ey_%ts z`vjf_Y2$4Gw-ei!htY~DZzle#<$2Ux{I8?x+@~z-XXi;uEQ@B$PvcMHDIyI8heU9WhPPC3x0aAK|pOPD593B z>6Wjv)KJ0R9aWdF^0OVKcNw>kvY)tmovmd4^XET_URxRJIW4pJov2s+<9~_3HyI}I zt=LjvSN1xu_@k1H$NQr?rOBA(gNp!Qixf75&4WVN>WKKsx{6viY3*tip(FM}TFH^M zVL$+*P$aGM1^S(pS8ZCv2hZ^P!Er8uazFayp4% zqaZrgW^SY&zIJ9)?V^Fx61;UX%tT|4>WPp-P_z*D&!^c|W)^Ha$2(K#&9>-vt&bSt z=igCu)`;4~?vq5Ac(NNhXb;Cw+Y{AH^78UU&OPEtjJa}O&;H<(=CRGRRA8WCco>bW z3_d96B`H=M_V8)Y&q#h`6{60imklQU_fvF9BOv}XV|bP;$@$*x@b<^9GU|9~L4PGQ z7`#L{vCq)Q*%bJZj(y!GbK%C;BkVLLj&E;%>ig(BLCdZi?rZJJqVb?_pHB=JP;>G~D{b5M4lFe$%r${_6aV^_iXG^@fd2OQkl2N?U&CpEEz} zZow9#P|nn74(}R5PvbZkn2o_Z+!x%M0o^g!2APPky}H zA54Nef#qM1I>-(-I8TQdT;@i6pW>Jy#C6@?-gNZum=@@+=Hn#T*|Q3%h!OrMyJcna4mNqn(^(p3Ad!S46_Sd|VDxtlXlmE;@txdn-@WUWoqR=E z-iS-#1fbBb!py-i7KH67;O%9#M;*=?qqFxLWSB`9D7yA-;^L1?T|L*a$o+#)Cz(Gu z{>zu41aNr7rIdiYEG#U9LGOJ5jXNOLHEsozu4YG%b~DRlOIbdd@v7VPT~6eN1kf^`E)LdKB;1T&^+=W+0ZEK zAfS3=4fQ^82!qgYlzmu>v1mfdIm#Ac%h4Ee*h6KvD~V>n{+LuUD7plxz#I?eo3bKf z1n1M$+h#Tlot^gVwwZrZK#;Hq@hvbH7Iy22S1d@mRCXi7Jqk(W1nVny%$MVO{3)%? zp#fwZl4poHx0+Nk!Zd6Vw+w-vN!~3J7cwJxt+KosN7X~;A~fA(Knb$?47+N?ke^%% zGYQ2e*?}(>D(G-*L^1P)!KRKKs6~D;H+{f|Vl*R%O-(gF1;bPTG{4!J+W$UbK7M;q z4UH@gbmy}E=yzqnqSoU^v|?KE=29-Vrp#-{C|j6+lLHO)4Grp%u#ljsw>t=_3o8FH zS3mR9p?wQ!HD?lM#o+jkd&Hz_G*(=IIRc8>qSn>wW*4qs`Dmw)Rr||g=B0GcM5wL_<%Q~RpcsWB_t$@FzO}!fErVyvkdPX^y7jMXrj-MO3ph{ z32Y^bjMCBKaCh`><(g@kLsO6}$1DZx$8P51RZ#uaP5IZ&DOSG7HX8iOhyO2nM#=bxpb9)@4`}+ zm7U_co`hg@=MHmXThmJ?dFF7C5Kzcv4gE^`N5&c}Y87aNivyybm0gxAPfl7!y#~_` zQ)!ZAjja98_2DKy!JOGaj;SjW_ZgT3=bBH}Hj2Y`7 zmp={R%)wK?o-|lqyLL?*lMp+`$k2 zrPH^9_ImJH;*;5!j_WuI(WHgbj(uz~$E{xh<@GW;L50>uW?B?$gp^P!O_V-zx7ZL> zff0o1!Wz76bPgT5`e9KRj4mMM5BP3Y-WT`>ly!x__k3z$H-S6=Qgn&mxFl+7gMt4z z4!8x1oxArwW9z><%0%nk)b_?=?l48tIk77#g+AboI;8SJQDvqbT$yT7~2e&zP>)_+>l&Ab#fiaEpWQr zg63%Pc(=KQ#bp@cjY2TJxYs3CHpJ?T)a5eG=R0#G9##JQyut7M_cGHfwd_%GgD*oW zc(dEti?8+ZSg&b*K1SbhA?gf&{Fb@)kL`y0Ue-h((ro_TdrEXvs&Ox?X{MeB`AmS+F!;m*F< zrPKFp4+?{Lh65X*wMh?q`+LzW496eCC^RM}hRtCmhF^eE?{rR&Gdnl8_O^u| z9f-<{f%+*Ih^X*P3wkTb$(fj?ol;1}P3rtO;|**W?0Ab1qsjk}e*&NfD0|vL;g^S9 zT>kqkaL-Kq%w5|m%k|Kc0h$5yce^Dn=(%9(1$%pY35#6{z0pi2)^2N6=}&#WicUF9 zcq@Vqo)g+>+-(GC9K4Soood&Y0nXr91EykHG6pbH&D!PxhO; z%SwLu=@Uv>F=qi0AeC$YwEwLedF znOcGub~0q~2<}Jcc>@56a2=0EGh6=&cHHG|LziX-Dli}YIV{UTnqGhfonTm*oe4)} z12I?`)3}=exO--bY_Kf`SKRMym%7Kd=p9I!yuv&saf?faQjbhj<1+@pk{! z4l6!@mxQ+N0ZuT2n6&3=vA2zZ=9C>ZYgIIaW||;nutAAA`t16?f!^Mj*=K(R(3ue) z1k&rU6?f*DODnHVmn@u=ijlDg0tLTr8Akz}2{4bm2zXpz@k$5ltnd$|Ld)A_59keGV#QP<5-3yu?%$uZ9DbD5}{JM z%X({lYvYH`P0pS;%M;}eGh2TMwQv9-jrI#|$I!$oz;tZ)q*%s5cb+{C6c^>+bB(<4 zRX9}F9u3M?gN=+0uJ*kA?NE1xfQy1xf|rb^XBoV<^}qRlEM(SS_a=@`K5#+0@kt^jM`Dk;P*btxuO%!=EQ00V!^#PFm z$+qa%d}*uZ@2{y&G!Ns(plJ=I{dZ$sH-$4{6@=|)H~Nq97WScIMAxg)4Nhy570-r81=9FUI4N*94taNzJh2n?($A2 z%ssHRn)*Zn3PE`Ti>gCLPh8yXuw`N@0=^Fj2}I!VHbxbQA_R!{#MzeWF9zk@zi)?v zDk-MCYzW&Cnf0LVpO$p*28c~^TZ*4o;X6%{uk{pCR*jRZ~;ob5V%CxoE9 z8uTB(3gP2SLhdBc6Ud0#C1Oum3y=7wtA=1$$p!%*UbHN1H(9e{KML?qqDCB|V`BQZ zE6OlyvhE)IKN{tX@b5j0o#Svj&OHge#Avp!)${WwJBErGld&%+Q1+|pC*eig@D2mbt3Ed4#r4Y(-V=n0V91>x2LiBIf7hp8BH2WbAI#NvX z#2J(@F6Un^xHU>a-@f@t!umJn`W9iCEau-;2YcAen60ty*tc@mv)DcJ!#=aRC{W<% zgHm~HZ0vWS7!ULmDCu_bNNp1SyrZ!+TbZP{!j6hqO(PpbT`Kv_F5f1m#Iw$D13D3q z=|}U$`0;yb!f|KTAU`}}ga;Q5fL8J8#w;88Q*+0lho>$}l7!bct=$Fb_mw z>70BEDQhsOCwUuzHFs-Y;!81I1thTR(lZROe#%#khG-BBNO7Z3l(gEMXzbLb2vxT3 zvhwn$`3ctx1P?O^?ZtTt)C3F&LqaqsN_LQ`GtI;?JBz`o0`jFTQTW8hW|m*Vha>S6 z^mo33?*tVf>QrP?0(imykC9>&90c$HqH#Bm?Yt@Bd;VnZ45Ltl1PvakPt+RcmWZi3 zp1a|z`&X4z*ZG&R9pDJekyJR89oKK?vDt1#BYqul8&ZCx?s0Dc$bI4_`x|MNSX?&QR{s*L|t)he$93JfRS(0f{!5KQ|Vj6t6~c zM97H#_kt>RTQaV$X^id);~Ty;h|lOfJDf&KJ30AA*!K`%3sU@HrbH3lsNUm97*j*dwOSk2cT{XQ^G6sg=yGN(=6|O1pluu4X!Y zM>DF05j3r05MS%tALL`E#X#)kwi|38e15NZl47hBhNjEU$N_=vKj0JV7yBsNIb7%% zDx!;TzGy+n54@e0Wux#^FX?4~bC7Ue2m&b&YSK}lswKlrNWR0z>O`qA8o;~>A#S6< zFA1I3jsHD6t4J0O5;`;G`3;OOlJ} zZJ@-N%`e=p4{Z~8k6&hkGljslB;;4}bQMysE`>jHKAZVx;k02X&acvTx7E`IK!LV|ksZu zK+1NIGLSI>6NYirigVk=aJ|ws3mo8!3a)&^UTu)BIy4UJ$ylf_#!y1>uy_`u*T=Z5 z<=zJ*wh}v4gmWI5+lL=R=7A*6fj3dTKP@foDk4m~NBZk8VfQ9{Tx3o*qgn=yVc%W6 zK6)4(b19}7%81%!jA9lHrm_u8-azY8zn+YYNW16pLWH}Dqmp`c6snh25P#5%j*M1v zT=BgW%uYG@?Iy)Gp8!NIIx7s{>!PTc&1wz!zc>E z&{5bDG>Zaa<)!tXFkGVki)E>t!tt>&GL1H|FuS4O18++7Pw3n>by7Shh7tI@)L;Ga z){#I@lC1E=fnv5G#@Bv@!e7R-DetPv%7ovywinhhHH}*=p1$gBuUzXHW;4xw9DYiU zSu^~ym(jz)zmdaYWx+dMd#1B!9TRYu{kD$3E>%~ydAN-yBS|L2?h51eG3tcJ34y+| z!~8_b0og2fDYG?tHj+j~3@3=oKi*DU*=11IOnY2)S{WfCS%Kf~V>}kLF|24grWdZp zQ6Hph`f;H+-00lwk5)Hxde<~_jUdKl52>Z<{iP9w?x6c=wdrn-W~Yb0uuLEmzqT5v0@z8^JHMA{&CQZ)y-^W3%5pQ2a1k?YZw2qdOhT zO6fJJNq)D2X9)&61rW9mftGPHJ z<&}|;a5Y=S7>*|-y$S)2>}GsEqB!zMCK;yG{OCkTIyXs0wQ#G-F|E0b)kY9qP=3KG z8i7oyfzV47QkAZH-u#C^X6^X?+;Xe^kiaf6W41?}sRi^GB_l>^TL+X+KTGc1W|yFT z%eh=FTV>{bpYQ#Nl6Zxy14oMt)LiJ#u(&7;aHVal$jh`638v;3+06t=BVieV2crAm z|Jq>4@?LE@`UqxiEe!8kfh{5l7#xYS5|cz^1JIJ^zhoc)ILr*~L)bCgS@`;%703n# zu=y^GxV3=f3HE}1JwSN?f(MYU8o~vOQZ`&}4kNlNU^e-`Y(c#MJT$j*?WI4A+8$Jw z#|{Slbm69Az%C0J%vK>G^~cOO+6rrbUAiB*b!xKwZaXN)aRMqJ|4QqA#sfSLV;C3& zulznQyRx-;+U2op1wsj~BEVHlFgImgEcs>NMKl7TkHTo1RQW_Bgx%!5GmAGj6qRwq z#7Kp-`b&jr``($nlQ-WedBnLT8gVYilVZIT(^fE-ACd}b@C*GOh1nis?d8pi749F_ zQ)G4T@^ZHj$|(R_2uGRABJ=8T>0xp_8Q{915hA>wyE~|H8 z1(zPyWv!Z;dCyq+`8Xc&ZobCIePz4NL&|jtY=S~HVsE~+4}RG4^X_I!HS{^(4~lQE)NX-$FTLPK^R|p zB0tePS~m|l9WkVuN3{Zjxr-2!AnnzyX7Q+{XERqR^S(&-t%E&1kw7)ZNR|+$CAlik z0dvjw+Q$KkX~{|3S)>m*2p?Uig!ys2$kT~h>-hE_g0|@GWI7zbG z?z-78v5;Pog-RUyJP@R+`)b>@@qby{DW~tMqjP^fGiE4;t{P>w$L2j5ytPY9zA&Vw z1PxFGv{zyT0aI5xptdv14l@l`6?g60fO99x@BtOFQ>3JPQ#Je3I6oO>2n!37 zfnTQbd=sakty=`91N1B`&8QB#dU_5Fd}dlK+Hg<&-QSTB8%rowNDSk4PMsHQr*D#V z0vSy4`3p)j)O2(tn3^0^s5V4;9>52jo{=%@l@!NReKU9!a;MG;WEXn0G85+PMd z$L}cmh?Bq@i0Ai;+!p1r9Yl#wIEXlCvf!nT#*Q$gxOW~Kaj@R%u!7wdUx1F-10h>T z0<(pe0r9UUA-Fz&cFO?(c?JY>pz#V{qUA)n-5j6*??*TG25MSl3qAW zl;*&zg8w52UfCv1R+Wg7SBjTk#r?4>$a$9l4l34ecXzkP_X$11GtygBJK#Q`fST=} zwE}UMOfp#@$v|Xs1c)#HX~klYy#f^l@#BV=66OgadYTuZ6@~Buzapc$dN5BhI2QFe zsbUeg|=3-^gTi8od?Zb9Dc{wzrxJ0QSM;%e28rfiics zjcfDfEAYS-0;vm&C^It?q^x9>lyyUw(RA7(j;YjD__4&9LXY~AsXm~38w;o?fNfEN z3-UrRp_l^XDzrQFX@+zdrvXIA`gzH5erAL8-66&4G@*o=GFjZHo zq5zRsa^EIJf7ak$lVzK8c+GZG>uqOxX}@K?i;kVoL!kE|6{W@Pd^2(4*!tgpJJV%! zG*0YTR2n!6lnH2{vz)LGy~x5YfDM4H1j-&fnC<0%qXZ&cMLTDJAv7{7mS=Zu#3B1X zG+hTc*KNQ58_7;65=9v)QIbuzP!y83WTd1>GDBo$t7H>N%8Zsh6J=ILib%-L3h8|A zp7)&Voa=h7^Sp2I|NnmX{r!F>Bm>TZw-k{d?Y<>MUtgc(#t%$bgyNeZ!2rO_+m$6L z^Ri>y%5qyzp53IExzrJypfau~c+9+pr_WW*YyXnz35CmV^GBHSqIkNU1k}Zt1Gm=6 zvPe017k!a*uRK`)RXWIL)zha>afHmhJb2E45a0&;{&144CtPO4d;^2YVJl5Wi=N!m zLtk#D@BH#)Cw>#4rM(iKzxB%Ay)I2#>uW!qrZlejqeRtU@te*G18e8kGPA3xpS}3O zElVeJTIH?X%|EblPPQ{Wd(QT&cDmZ=z=f){JGe}ldnP}~F0{GX?ee+}xa5EJP@$uv zqc;kaFk0c~s>Q)2T5uf-tu2a*pC3#sHo+<%_fPEEXObEr*edxvQ-WCg%5saAchro#ZVqSM4pCVa`BU zrGNP#uL{M3sdDBB$Q)qg1mOg{x0t>YJ0OT+j^oFVlk*R;JpAZ>9PSm=-im=S1liZb zTWOU<@_55_Dy}DAe3B*l3Ani5xv;3V8vU-6+z76DjZFCvos+@g8%X$Pg04ou=K$-t>!n zPQVm0H4fsUf!xy>j$x%ax@V?h&RFcGaF5A>yH2bCd)&;vZ3-@-^I%rNj4my zP|>n^WpHycpXnXgs6zR}lp33rolTCVW}G@AKO>)3ndg~-!N4i51_+aIHVF3! zgLpM^O+)*Er$eGR3D&vo3jeqnh%N-dz1Wq8oPX#h_-*LF;}`*YLW<2X!{Io$I@BG? zqCsnWorc?1c5Fjw`Ayzpdgs##r%}MdTlfD9$D*S0M}jCILmN#@kRK8SfPBLHb3CfW z9Jg3cI_UhU0oB3A%R4HGZqB%-OJ-1>$nQCjVCMi^WTYVp8W~yGixMS8%35T_|z$&zIU>`@_Bb8%P z$Ht8tPX(k-Qoq_-gV?Z`qAWNvKt1O7t$A}bZ1cWQ6mjryzRb@zEJ~mSLVe*3S4hL9h?jIm=PbC={_$N4TXnMb#dzzI+ND1$DRpvLZ zU68!}=-!ik{GZd%JBrVmMEWHs^P^;H>+IjwhYSg?s#jLOO1}VDot}Xq<=0!Q0@eRi z8@TajA6-JB9JxdRPC*n_a?{5LY$r(65$VZ2Q{zX_P6ib$3Bu_fUE_RQ+n}pbi-+Qc z?Dc1bg)MB-4pQwFk~o$te#(F6XZ|htL4W!bq%%A`JfB-HCH_DMW@uMemvTyq;l$0+ zYvycPS4jd5zAh@qiKDp1rM#qG`a5Sc<$$=;8629W!=r#+&U+>PvX-dTfRsihT8iTBryO}24jH?4*lW82jM ziQHMeQ>Dx;t@tT1X+r7x)>Q)GePm=Ln%d@;mOep`v4aYxwV{gI*zB*CDfsz*nE?l})LqY@yWMq2pnJgHy1RygGB>Vpna47_-ngKK4knw~<;08eNDiV0)g820%WN- z47wWVj$6rSLd6o=56?9qm4`QuUh?(zHA6I%<84z>3YCr@{$-L+LzVB$D9-j17D9k&LSp<1M#wJ4HTzn@wIP1VY2-R zKyf~dVzlVnS!03$>U8Ec{O2K2AubH$_NYBIAz?uHdX;qd9mO6`GOtj#J`z=R>-F{T z4Y}GV@PRlgvhdit`x>yZi(kqB;9Ef+1Jh8D(Ku&T`tL##$10!)I!kLj7FqD0}+D_>^o9<%tPvK zjosOy*M(%j^U9|n6aKMPkuPd!#r)4%AvZDKKU-$!Wjda0@qM|jsNs|S+vD~{dA8QCUmC9&bT=@nP){<`Y-de6{*(>}QS=V3 zQm+KF(Ns&82Q}sFl9da1#$8A5#-&+MF^Lo?^gEjG*d`dQkt)2ph&eOKUHk1@CH32~ z1z+0TryAVK?Ebc?|~i1^k^}f zLnojsx@6ehU;mkB$WZB{l)7Ch4rI(Bcuz#$Z4~{X_i9YWPXa+=P+h$q)%%SXeF}p7 zL)}s+hblQ82E2#|s7CbI5OE@4O;Fgq+jpC*I-AaY(8(n3BywaufE9?k2;6N|aG(7q z*-x*&+sln<$$}3f0e;<#sJxtvNee?!Z-t8g*8;fdnROi8ulrWSVw%E_cjDe@S5X`} z35gZDGTksat$8@L;5f#;6BJk z97nkJJBlvF>4l#6sqg|8)9{fL7NeUW1)b1R!j@;=zlJyavt!#qa@Vt}S31UHwlIC| zv=b}wkSw0!#&|gnI6@-*r9%5jl7c%b&i<$!?XZV%p0M}$Mie_G|8Xamzm7{?NBmkg zY?s(I_TRmTWBY#u!g16An=efE5!eSmDb|0{HL8N)Dl&iEek5FP{_x?uR1;C%LQ~<@ zFFUC?VQBKyq#xH?HF6|9%;g8T{3IZYL&A?=_nR50Zeh?P)h%em4g}hC(&hV|7h^@; z$Vm`zlivg?gyaN5Iq+5U-F6Yz_2*t-S(`+Mh&iss{5cE~dx1(Xz*9^9036;YuYEW| zkQ56(P5AN1JgP8J%mKb2HB#Dudqa^yX$Pg+Rx+(t92DFC@Z+Ixa6Ls|q4Wg*#oA2j zih*E!w@~OfvA;8nc|Ah(%wQbCo0QVOeNJ4wc#?RO2m*t70!QhKZ$Udq&Hxs#>RaI) z#2v!PX@+n+B4GrQ>s8wAy#LSu*#h8XK6>ifowTvX&-r~L_AeY$`>E6E{eC7wZbG7d zQ*+60^jUwy!moYyY|5hw#R|+EYk7Erz$YhZQ6@ECf8Om{iu!c|dXxHHV2%}<9tT%o zoW@^?pzgD9F=MPGUO6nGyRVO*?DVMZyTXqxn&kOFLFM)?d5#m82I+lyuQY#Bgdr~% zHF9LnCg!Pi2S6+EUGKTZMe>TX-U8?n@9OTJf>U5NOX(LuxegvdRVCSyjk z$Dar2F}3N#>$H93r7o~uGix&cp@7PMI7~#gXE1tS*G5iGUx?O8A(~x+5&lYh41Lg@ zwq-O1pFF=~u4Jz0FSx!1sB%b_4j0dxRz+NUq$wBB5*5TmxSYv5hM?v^^c6y+mo|P3Mi&`*_2KQ~!4HOh_-18yij&yB~~eAF~-ll77sUDW5Fb~GGDG~prP#exROCRsLdfgv;(mjqTlHPI7dE4j0MrsgKQWDDgp zkICxhJ=-{YLS-yMo!>rVGgqUgYv8-^hVJ2^ZIAq2Jl76_@Cqw1ac11|{io>G9nx0T z?52Can2o%?q7)Hw-}Z@^FSF2VZ8ojIYRef~#U_PJxf)*%npNhlq917k?#SzSJ2w_c zXV9!=_4m=uRZ1@y^{77mBIB8sa}3vt@JHb@A>Wf7F0U=+&SPmlno*soc45!GC^1Hf zGwlb(X=k+c?s=B_TDv%SB1CivZ7+R-OF zs&@@rp(j2xtnmg(OpaMO*i~S4AWX&5=x-V-6mF;7w^;VBxUjMND}8e-g^r!h*z_)i zn%-;@6EQip2Hr#%|9Sg3kPK^jdjDkSJ+HZNYLP@Bgs}wxScSvxu)@a5C&UlFZ5ura zyFo+MXSz70Q_1*O$B*k8?a|Hjpr+^dq71N^E#Mz0Wj-CEh_)2Kovx~?s((O_@L{9}h~d4gUrw3ksNw8V+o3&P6yKqrFE!g=;>>E#N^t&r3AKp(mM3W-A=T&~ikMX~ zkBQ)|$MF$h%ZG~+dl$5v8c;0&7kBT{{sy{nAIT~3pVZUD|oF>`S&aBoElcfi)4m0_2C2$BN5VaI(olG>@-v zS)Zer;kTo_@BYYOHUZ7xTOK2#CHLH{Jj>gp=i7H{zxXj7&bkEv%jNV>*Y477zU33e4+0}2UFi!J@EFe|BJ~8@$!T^ zffD(|iH$_-u=PdoQ<>>v2npe*fkS3?qpEb=JMzPl%nTw&BWyT&tP&P?+HPcU85lQj zh@cE3j^XEXPM4BZc$~0_DgPeeDD!`b9}35&to=9I^karIjYz?Tt^>_*Gnobc&(|`( zxjsHfdm{9Q3*zv=w7u}9Ggpra58sYo1mYlH#H8t_rzSD{i5gKzxo_)~zB1`xhwe`h zW#{A4NS_x(@32|prNA{#<~n?=sTU)?ZvVjT*gWCy1rbWK)D;Kp+6w^Ah7_J;WvL@9 z(e@$RtyBf!)5wp&A~yW_adx&A7TIp82lzxVk81%NNw9_#(?M5XW$qCBh0t~&1%8NU zn6W+N`v-b>`1}pGaS{QCJ>H@Q{YfD0y?2hDu7LVx_$EN|TE6R_y#bQrXRD`L#t1Z7 z$s}=TP~EhQ*Vphdp0|I_O%$w;)=$9s^)GJ>ijU*gXu7&9aYlUOWW)~__qk4MQf_Nw z=u~tg^WgE(%V)eVg~3&wLG&5>g~JNHqa6M5W2GHpgW&*x$?Cl`&%j8lUHii z9X<4Ii}VeQ8C6@u6mNoNq8_sHgOVW+cR!>gI@w$pFQI@1NB&n;U0uEGRtFeZWcX_z zIdW$-MP=LfpA1k*l2L(-YJ^dJJgO3N5h%g}STP?=1VmADBS!)g2}q=V2@tozvk?nU#Q6M%wvScI!hy$^M z_XD%<3uF+V?{#UVoBo9MTLe@P%my%#&X1R-N-J7`M#w}&CK&v4z(8LeBv1qpf?N1} zYz`2clNgL3^ohQ$bRI80c70ET9*9@8FS(LGzAyXrcqM#%JKeqlASH34+#y<&o{4nC z5yKm%Wp182;7$+_upuP*ysb*@&>=PgqvP_FTXk~%CQ10(|K&bI(i5E-92!~+iU#pB zdTnZvvSng4FF}$i&bAw+iY^x4LJBL&oWXJ8ZEh%?!No>IFy)&?L(yvnl8Wd=N9sk2 zWdE~xh#nQYx7mcg?-v$!%oK+Y9pbP5{!7uIN-11ZuS>!1vn-U3orEeD*LC)7Ofps| z$(y>WeZV)WrKeXrK0IQ}i!XqgRU;d*!s0dIzvFl37j(Z(Mx> zqCHr*7LTP2u`0Ux-?khQ*m-2zt368T85u@hHL)B3xUgpu z?yFv*vk^FmuzviIW3QzI@EG7uU?1Pn1_21TizyVcqib` zMnLkG4O;bZ76DhsiD#U1wF3*gsGT*+65>5YN#}AeVLShqvy8dJS?X?nu zk!fRUmD`M`@_iH1!m0R01I=6FuA`_3pgP7P0EwrltMbDy`txp;V$1i>y{h9}$?@7? zfp5os{&Q82zjsWeCGO#d&hGGkRd{p&c{a_zE~~4<>Hx(PSuvmJzB|g+%KfrX@S*US z!J7{f#;f&g^wU_|EA2&&tyjFnp^#nLl~a7nFf&c=IM^>i8z!ojSTwtd94Y0kK7Zcx zX-cA1kNMXq;~?2g^;L(f7|XUcSzLWjb48MYt<|zfEN)*nSO2h#p8*W6FfYuVhPaM{ zAxd@7WfLw|JC>)Yry;+|dIy8Y>R_9$le z>a=xo0n@FHX+A2}DG{!Na@PHYuLS_I|};0k%{@keYZ0=A|G<9+ z4%x8Fm=w`n z*WJ~1@|hQgBm5C~f%QetbpEM5F9p&&WUyYS>8dO*NqeW}G-=jci7bhERwy$mT2&`K z2&S_Jnvb2`<&c{ZLk%3V=_B1Z2tPyLfY`ZcTd5W7JCP%;$HZTVTmm#ec42ZF>khUf z=N>_Q6_6V0hM1LPs*nCfcp%A_t-*UG)Ii`B(i94BtIBTAf>JFQ4*u9 zmDR)Gqtbx>$(jp0&PeslsVfz$h-S_5cFi;&)ckzJ?TWAOl*7fjb*>GQr0?Osu)*TC zJ@xK~GmSCpkVG&tHbY=^;AbEOloBBn4Nfn|=|X zh+L$oTI{S^1m-bCT7z&O>l-r99V@kq3)hdBgGF&OR?z5YEymmLEI`}#?-vC<_0kkd1 z=?@_tnt-nL=VvY6BUd3FE1)SN)ohvHVVoePS!C?`^B#uthjDh57%xOm{Hq}#epwKI zFmGHFGEAE>|Fn+7J3 zXq{p}H@mFg^VZ9+V_Zf2zQj86P};x5`wVv}`d)icp%G<~NG}^oT1WECYZ!@!n;53wB%ID9cEt$cqc9fQ2{>X)6gm%@cddd5ks?eVGcajVQrKO2o z3eJosJl8SY^rO~^&{jfR98K5V*5U_lB0IW0B(~TKS$89DB`S)K?{Go>fs>%~R`7?A zJ6=OikBNvB%KrYD5YMZx<|dRKQ9vv}x2M@H1Za~y?#hRdh=%HGY~wI02h>hbk?NQB#rgCYy!dn z?*-`?B{$TG&GwYzc+v}dre&&D%j$guR3ZLyfSp9Efa=bditd+nM^V@d90X1j5El=Q|3`Ah7{pcmR2uiv$34{?g*U z$iIeOiDJlloxcT&JmL(#gID-8}k21 zC>{AY@a-dSXc9n5N^b5UleVf~ibP(B@C1zP)7`IN!Q^`x7@&-FPm}TJ}_$o5hIwPF!R9C!p6y|k+99zDGUw;5}JAb{5JS6F=A!T z7bp<-$hviMvxgg<^v4}{j0U$a&>J)iSa5Q1kP?TtqxHA45JFm$e(!V;MtDm3G3$g| zATS=k67D}RxK*wPE~b-sjBr={T5Rs!5s*ny^ahv$t{q-;{SQQD4ej+NYIIxg6%4(7 zTjRoo$F>XJ6(lZ!B;9^^d<4vON`L!T{*gJH4tI6eEK-ji$GHQpPy=zF-M@2?@^Yz5 z3YSW-?rV3OO%=*#I=;Y23?8j*>8xx`mV35&A)z5b6dqZ3D2y-PU&2a zM%dCIk%J0`Xn{mB!-sLRL2h4xBTtkCL55=ZB!)}3t^%Y7)OW4cgBF{w6GS=$i zyMg<@_i%Jziqb~p0 z62zzA1jc(-PXQz(If6+7bgRUc^l?4s*LRm9^L`0k;wq?#irnJ5N7(RqVxnS}4EK;* zuZs6%vcp4u{ywJUuL9a>0YQ~fw}yv>rDC`*3z%Nh_gZKmB|W6-2W&P85rXxC2IX(< z4Ndp1YP>B9b~ljBoOHh~9a$lszzWo7aTArFgx3v!-mBk29*1-+)Tb|OcygO<_^8UO zA4C9wya#0@a*&h;=xA#2#qUP$7c_$Nx{T%W#%trw^z{h=1p+GFKDe5j+g^YMU{ZxY zrE1=Dh-vMFilx+|mn3nb)sw>L~ckqhXxaZ>wx8#zaD;odBa@a${ zQ(ax%ixG=7*O2QNGAe+W_+QX2ddfbs@33^i_XE8oX=*$1+)fiyIRPF?oKjz3pMLE$ zoX0glkej=@T#rkRn!jf(^_38s_UIGsPU7D7hiBUu_kM2rbCTz|OTVxVsyJ^jzoM;j zt6Hz|+^D0ML~YWYMEWrY{pVAYoPrOdxI1nXXwA}G%U(l^zY@0@X=%6#3IX>UEcI)F z&R*H~^}7RZ+u-6Uy8Ja1+kf*UV1jEz&jTQsSZ@iAiadg6wlDz_EjgUM zK}GdnC$gUZ^l45meM8^Mv0u)L*(7#X%#zcq)Uquq^nH8$3|_Q!*-G>H9xs>+0F{m3bUDgnx?#u2tCB zzkeZdzr1qf<1ovczNcNvH)mej&o^pyO@6Ij;zZ~kl*MVZu|)pW*~ZhS^yi=#og@US zIO{2nVQsKsibjbP&8-U>SA2|oSE+lBSP?O!6V5vxj-G|Kq!nM->XEv#MNrVURQ=`wWSl_O9j9;jWK!LD%EFaOtH$KyW}y>@*WRmd|H6W=YL zzIafu6YD>3U4^Z5+kOUzbLl2o_+k)C6JL9b*`Y~bcu&ooRG+)h)$q^Swq{D5QT4}# z{xS`3+2|DVG%oGqxp{}PoD6v@44+oF(B&>Y=RPQWKiA!IFoltY39hA(=Dk$KP`Fw>2Dlx^or*q2+{$B1bVqsb@ zc4eb~4g{z~nnEH0DC1Y>bW{;%h169N>xdY`*&*|*$;zN}Nhd|fAEfgJ1&hDhU(|%- zAAT9!vTG*6!hms$M<*hlG^r_>VVb7DYrh5vrF}Yim1XwyC{sO~!f%Xa_=Jha6a31e zLf}qq!4m`WfcQub_^W`1h6b|56A5AYW+r{v3}DWry#j0W*t6&xK+#llKAi*765VGh z(W(u>17dr&i8qbOfLY;fdFgL*E1igD+9Ca2L3ZvxRswPGRS zB7R$#>@s`;CJEApO$7x2qMhOfq|*|&42q6@%ELO>9;laYnDXEO4TM2^@P!m_(4AUY+S2CN5gc;+HW_@e*6V;YED^Ae8~R0AvbuXjaupP84Gw8c0Jmud!kV&jrO_OW_wV1gwccPHO58yhL_$>7yWwa6YUdB8N#F4o zbMl0$;0Dx;_`(2)n8u;}`}u|}o|2*#n@Ao8_61EWc@`c}5-F2KBld{uo8Gyaig4R< zKyf6?N2!A$Mdm?J=l^Q~+J^tR`XR$oA=|OH>n;`D#d=9G*m|LD-%j`mp(YiXwgNzt z8OdKU_hBv>ER{upstD!#|Q0obr}hTMIZq;2*OA(o}5Pp67pI!xz1vZ z%x^ja?!EN);KmFG;E&)J*{t5jiyr&`W)+yN;-7iL{zw$%WV~otk1S5G%;FcnW9h9U zxwM561J_Mb=aFA%2IEw|$28{+n3|5}J<6~I?qVj5FvxO?D;*pmGHgZyu8c2@iaAXW zOEq-t5A>>85#f&tvEUPUtW?>-wm)Rw;h&6Kqd5E`cDdQctjTi!uFanjT||{VCcreYax05!E7%TAyF8xLSX8HxDswlagSMJE~zRR`k&K_3?Xl;!8>{&(1Wjk|4Euj}zo*ci`z&bJQ?;j56p>fL zN4S>6=1%=S*FP6s6x4a%tznX3>AS?!(0#pue5DdU$N9fFeR0YY`0&$uD9Wjj_&x(o`M`v0#OY@O<5lPFSGUlP=!VzQIj2T4 z(+&ph3c3>(99#t=eeI@AtO@!*%$e$bDdG_#w;tCA1WHO{3=rsK#m0lwOLJ75=>PJ6 zN^@nddiI%T?DQy90jkbs>^$M~LYJnWkG#m?PZOv- zcq-GFB%I^ah@44-5(?Z*EtJtGS#P8Ib}zOb#Dxy0L^cbj4&H!fFrlk;Blb@cn}A8~ zj)gv6Npgl_2$65mPmv21nJIYQsteo;dOkG3Xiq1CM6$&#hVy@Z@wXwNDc~!$n1KH3 zGAXjinLm(ePkv!|KcQsMbsICJ&7J9Y>YVXK=W`7CwEkijs@OUddh~# z3xZ>sqJA==1IkPsW%z3TK|OepANfin26v6(Bn8~yXynoz+?TdJuhtRW9r)2xirHz4 zBNxdBfKid?!7k-+k_#D@O;$+th9iRZc~L0#_0p^B)Y&HspR?d2Vs06nPlKiL<`;*HEbk_kFYzHld~BqKq5)2?rzR}^vDZ*J%-hU zLG?3LBBx&|d2^rDf%_i0CVo(3!w}qhOVH{2zyNF?=D^G%Zx5P9dBPz=4m{|CU(UJORGVg%y{7D@$>=cDS5uW@H_{o=dy1DGhVZQGj~ zg^*{*V5C8>4~7oNKL5N5U?}$ZM8(WZf5iJstmOhtr4zk`+{}@<`WFl~frps*jlbf3 z=p&hv(V|8}6-S)0V-GpXLyr3ZQyBWp{BeN+M96jj?SQ4&Xvv0M)T@~>d8s!B4<#C@ zjRNCj&Z#c~wt)Jdz!rqb2%KZg!3K}`*i@`n$Xac8JF<;;a(=14sp(&UFTB?G{@lLJ z!O$NG(-C3!aP5{n{E>AurxuxVBJx_cJy7(b3$|Q)v&Vqi8l}$741lN!khA<9T5e184DaeV3*wI_UOi6CgORlVs73oi|Cq*Zwyft29wrv@sg zZMy-YgC9xwHqO8mE8{U%{{n~x$vBpkHFeqq=?Aco4ZUFPMF$3LJok7QatBD6beztc z`{jOJtPO0Xu68;GVe~r_smcX;DI{%Up4aVipz@4KfIPqqH7X^ouJ|D#-ohX3Iunm) zll(M(W>Yd~Uxv|hNK#9K=j|MHGeqMJDW`E(8wc2D;3O?KnD%;VRPHD$D)u-E+DPgu zYt}TEvi5(jFbu+{mA-x&y6(4TQfZ2N=s=Rz1Mky0^KbIJ12J-6)D_T^Q)Bo zFSvF{b^|2c4Cmcm(|gFV3R?B3B<`bPKFyrwA`{-#Y822pF?)35BBM`V$?UrKQXzH2 z8=Oa_=+;if9uUr{{K%GC;g=fm>Uy$w^{wf>%a&%!;g_O_SfWYbp#Z=CdCyNez2dya z@Nul8^YxuLKpnwP_2rhhz;J>4(^?j++0@*PY+d^ogZnrnU2Quax9xr8M)6^yxv-wq znBh*2YVlE}Bj7f5W2bM^9@``q@on{%_t7rgDij~SCsZY-G3=f<75DM?{Mtt?)xC$A zo?-KA?v7CoANz>h#@P5jl#3Kb$Jehoa&}2`_{H2?EBYyJYU8$ZUpLRiP<>llem^u( zQiJB&82zw~!MRq3q~UCL1DehAbTqe|=2xfjUh_@tTJeqNu=30a!wga_KYm3v{##D{ z?wx|MFRrxA&J7H{-&0VA7CoEWn==<2&A+a7=FnA@^{X`1O!O+;%URpbkrmfo|L`_! zJAYCc*II1|FSs{4u?lL8l}fYw-R9EQ1m#K;*0`xH0ZEEi(`DGOKhL3j^UJc5RSn^; zZbO!BGqYTZPfv*5);EAGrNTbGJI_2lyVR?_LRD76mwj-h((BN0WX15%&_6Pt+FCj^ zWb&IVD^7$N<-9VbOL_+81}==P8Bo>338*`#YoDg|mlD`_1wgM2_BL zQ}fF}ItSRzO(9ia>}-x<%Rzu?tZ>-vRWCom^pKeuC>$vq+?q6@9)KB_ci)Y8$xMrr z@VV#{>T+ z;;DG{?9f@7CHK1#SPlp;lsfe#bdAkrY%QRY2M^yzSZLSjyu2YMoUX`D?E3DuyEYqY zLVyP)tS6BQL@wJ4Kg5eWOhXi#&cTtRe7^uZI=CSFuBf!RED%JVgl)XVcE84dy?5NO*P-2!P!p|A5K;7o7!V@1bv>nw!1A+97#F z=Wf3&YfVj!Ge{y51d)-P;=zEW0z5C-I)PSGRt{Aa+(QElS$@cN06w;T!&5B=eyRu0FWQ39tozf_Nq7 z3&Fo2i=iBPfaZ@MKVIyf!B36&RHBE~|H`!CHz`*jDoQMYu1f3i2;rj?o_^tXOBE`j)ZgYFFs+ZiD_;*N2hx`^mseY@G!>fIO0bq8bZahu#VO^07o?89+ zgGh8KrRSm|%#e4z;Yxx_L<6=prQ+CkRz%w+BXdufp1;)-+bNE3?3KVjAlBWi*fg(8 zqatVsngV|Z0>SSVf3lVC$JC2XNQc(Q*}YZTBUkDs|6@3lLGektD&3h`>tBmoqTbPt z7Ec;9KE6$~j2Nbg$~%fS$|D<(B9vyBsTtu@;#lWousfMs`J=L0O!p!B{IyJD#mg^$ zFFI5>r?LvnUA8UN>f1wk@yNj?@%r0qJ1k=HV7#+J{GCR3%?+zdI=EVa)60hXpg7TK zCyWRJI@3mxH=O0IzU+FiLLFFcpl?3f5mX|x`vkv3PJ2qt=9kxd-lcn4U#FCM$;_2| zP(>;SH>(QxHg4*^kvXf=s^oO-+TnVZ6e?TgD<40rBhM+LP4%gXProdVgH4_L3e4{B zz21*hWDMO2=ksGOZP+NT)nRRrfAS?wCM+OuXVu5@tbMIeczkTnRXzh&N~xA-dnd=j zI~#QXdAaIy^k4o7eHx{L z2Uu3NLucDoM>7;oMT=Ug##;=GSkxW2=ocL!v z006NOS|iG<)mS%}0lp>6+lv26x0_7j$+uTE1OS63r!x#jq#&wJ@Y*(>NrH7kP?xB^!0uZoj*8K5&RNa z=l5=$4NDM|^=8Oj5HHKSy+~?!!SW=Rlq5(x0V^bF5$}X_zAyxT{AfZrT^P!y1K-Ny zS4DQVrmCv=2f^S?#6f@}G1Br^q^52t~ zg3kO^IP7O$YPbs3s!)5%3g@2ddq<@=+Pj7V^PnssJW?>AbH@xCiPFC-ZpimB@;eD- z2o^PHrXC*W%FHLJH&6}@U&r50gsWu1-0%)gFx$^BT5L84126-TP0SmzyJKv>;_#h> zXq@N*_1&hBx@!*%t|68ctzo@TU7?w)WI%~FgK5u4u1Th<>%@~0D3XXf?qS<}W#C8n z)o_~0d~v411&zqYI-&xvVv#`toUSz042VlI@sphwQd9YaY;CPP%D<8XL>IxdozLM0 zb2`p_qG{Z_FjHd@dGYy`MjSSXcHsStJB=vN=jku)g*^dxB#GAnrw>`A09@Z>waYGD zyZ~J3@UXyP1K8jCy;pB4hb57Ug3Fpb$fOAbp8?G>k?hJqAg~xH{+VfmXy8yzOb!O~fBp;r+_<|`esWfU>p?O-sngO;45i^- zHXB}lOdUcK&`AV>L!zNz))=g2q_X3S`>%r{KPcC&WJ_(!poWM2&K{N#a~Sd=nuvVj zN#x@U3>8RsU@GWjhOn(c{53NW4h3Y(d~?|6Lg+Wpu%KLXp@Vze6JZ`(ofe!J>K`ra z4+o&Xe?O!aHN=F2AM{vTDpCOe50J(j-nxI`4Tw8N2;s1jdCDOiO4uzXLFV^^`Nk!< z4d6q<^)G?QN5d}2)wby-B>31%`vI%dHXnSRQ!;m)*lV!pMvo@dn5zPFU4c@Nv;{%T zj4MiZW$t;|Szt2|$US?KTZS0}|3ivePAcA{VNR4 zv5+F4g9mFM8zDtRj~$|485$W8K_w;mwm=r$y4SvA0!gI36psV$iTaOn_>Y}*s2AF= ztDEX^ryy>c)?qeK2|XD7fL*jej1>x<<&wBqbr0e@92d=Pm$94QY1Z-HPycIrIv1U# zUA1siawCF$H|Fi~Uy4VT!llC-APd1C;FR@#o^~Luu9lzEwK~Gc^2QD2?3aVFr$23g z_@D4oSlRTmm(x5ozH8Q=$HERj@h!-GF(s1ljMUTXa7~zjVt;`W1-G5+bnv?o6(`~x z>K{9LkE^#5Csl86@1{>&<-L?F2SI%*5=ViR`w>@Zvv|x&d~Qg!6os#lvRt9EpYMwM zhtqSHA(q7C4&I8C}9H4SuT!9XQl{31AhJGLffuKA1q4m$zZi=ms-+?-JJXK(s zD(rdj#O+4l%G=Sr`GTWYuIvH~9EgB`fbm~c0aX(0%HP;}4cPwdqsR-m(HM+Q2OQq< z`o+?@o1M1O+s7g=6nqwK-Wae!etsRjz17&fZ==FwouB%sv1^-&iO@)WN8W)1-Slba z@dDLaH<~SDlW9*Cn~dUODb!{KJQ2J0L%M3GoHrBpY4^K*Lr;|oO%*yP z+AkDFrRh96#`fiF+zpQUW0#XYb*l@y&|RFbQ#QJNR&5BHTE zZQ^7OC~r`L{VgT(aiZ3q)hE^CUidVRXYUi<_ESjs!+6D&*1Ybq)C(SWDGW-_JStQo zjB-CK8BX*>{e}YXvo2MMO-{yloeQO2z7mwacmbrs*c#m)9E-%iT`oytWRwq^#i}EG^-CHEP%0QbYCJUo9NN2PoF!zE?w) z9NAn)hQ0kQ=>o2o=GdejI`vlh>zQBopZXu{*Jz>GNhX8(%CV8 z-`PkSJ&LuosSPi~zRE6Dmv3IGH$+xn?mIp!R4r(?@^F<{Ro&nxWuq9S^1VhJ47s}R6LFj zjJ<2?I&Q3DF`mCs=W>^TyI=g{GvhOS<@c$aIGkj8^;IW2-H-%uGGwy;~ib8cHN7m#_fy6anDSAN-j>P-4EwFTY1Z`hyTdZCh<&}*v_s-DCJ<8SJC^)+IqQ8)4l`q|V^;KhW!ElK#F0n<>apt@et(;zGq6YtiuAynyuf}gj(Mfa_TOtC_xUoNw4oO8pdu3^X|5!;J0?qUprH?=f*`LN_iq(rq?BLaQd1^jgZK( zWVg}%MQd8E=6dq`EIOu3DGK+}+YcK4DlXfwNLgG{S*Ve^>BWPK=7#1e)~`8xCY|0h zrC-qaR{L;Ilza?hL+(S?#F&9eMT0%g;bNSlzua}W(qk?duKC%)?^Gtzc*j)k`cha7 z?T1r*y6LS#M!{D+g(tiZRxo&JiJSen%NY{Mx&4Msix9!-Wj{g*f6O3F$*@3Cw@SC`_77z<8w4@Vf_@=z;%r>fFaWpKhhuVM3knHz2tf?h4)l37~4}6S#!`8FOL;H7D?w zJkH7jf=E@+IeJvWqA00yPG;}kvjDZ>7;s#hO#k&Pq;>#Yw1zK3ia^rZGOUA2Rs+Gu z-$}eO6LaG|!BEg(ZO5~XEMW%lzzbk}0ol0K2#?m|nzCbzj4TFk!KYuZ#ZpfA< zy*7XNlPV4YghrM|vnwEx+;;3EY6lQ;huNR_1;z!}MJcS+!&&qpD~$!;5~8q3>L%$b zX<2ek^oQREl+PqQ{G`_tLSoEk{k0)gpcsjg?P^W_m@s=eU<)>qNQS{k>FK*frH#ZQ ziCR>PjRdzKSsH+}jn_3BZP@@x3#O!h0aIA)m4^Qyaju3y)*K><#3TX3wk*<2g?^HB z0X#4NI&+JP?k1LkB?==!by7F?sbb(KA*w_oNpY31Y-nt(CZiiJ(|R}O#5$lAVfeqz zz_8jErt2&vt88Ie`-x}Be&+6#ot4DyRDC+)JpRZM0Ndz7`+ z)459Q+*t#!Dk&`1RYyiUnXF)!-Cy(x?jd~nWKzdhn85qQwW`>aaV|Uw_&kygS7Cfe z8{@!Y!$jQP_zis_3Vm!7v&Whh4e?@eNk#Zqq+F8ZkZgBh!tlx>^Vtg(dT)vf5LOnZ zG{=n`oODsL>mlS5!hoH}LU#q>jgkBC9u9esVyi*K71|zXgFEV9fF<5mi|#>&_kuh_ zTz+8z=bPkzigha9#L5b@1uSGz>G$y-Yiaoz>8{_+D|^K&vBS4dVSvBbpVo+#kx>n6 zj9?W8y5lEK*n?)HWQ&hfay())kJ5GC?sW9-kp~2}*|9?}|1`#XoC5&TS_9=iTqp$Z z;H97OVEfrtE^|e6F_AE#E{Qw3nj32IonRrsdi&VqY&U`-4K^@d4t&i9a0e?8iI-Wk z<{mGYWN*CZuUIzrvcO`$8!JQoSNJi2C_x6ply0c=BNu5)(0*sW3p2h@NQ_iz-_+hf zD>pxOMHa%o+rWN0=K!n$==z|eB($n%ljmhys+oNOQnH{dpz4qs%H{^Phon)JtDjuY zJr{{)bLrM9dZL!1f;cZwhC)W>C`>>rXv)KfwW7^(_2mN3YoRHI#G-j<-(JuE*8+V0 zd>}opvS{JrDf$m7XeVwJl#BJmk%aV&RtLO8a5Vx_b9j4NfEE#YJ9nm2b57f#E`#_) zx|8P zoyFmZ9}(7(UR49eqPCl}DurLJyEUIXk^G$b%w;Hp83m#twtHtv-L5AE=$nu9%Q3H zP(}cZ!0-k0Iwm3oTXMw?4uoG3|i@`6mk)Wglc^HPpZKZRbGT-2LBP+)Qx? zgtwnPSF@b>$gmo@PdZn3uy?O1@9YO=y-xA3KMf59JX7o^lblZqowIC6-vjEdGbbfy zswy2_U9Exh|LrKl2I0{DEBMx0tnpAEsy$)LilC9hO9xX^??H`|)j)M1-SFk33Ev`5 z-nx{+!XtB?U*Pvpb9fhSKRPm!YUoZKdDdTcD}$1VT&NHF4R9o#+FGiDbcQs}z~s`> zFM-)?54R~Qo^(>eEhyiqxI~Si-;U-P$2yTo|4W%BS(^9-gIh{+OMn%K+s|Y3h9+3( z=hit-v4c~-R8c-~rT~|04qEpG!RyNcJ%Jcf*{+XSrD~3opQ67DALjz}X{3+9M;`m&^Xn z|IPLMdt(u+z(4N+Wc50`^RbaZnZD`9!rAurWzV^61;xkP$Nr8Vcqp`IxI{!yAVtK2 z!AxC+Q{&SSm95DEKo-}otqJ0Pa_ah}*GxhAucVh9xtOHa)9%=>twO6oEx@3&KkmT8 z8=eC$Vb*rzC08>9X&Wb826gVcO~}p1&lmqty8P7QZxhb(uPbvh2r_s(r^iN1`a7Jl z=A)#`GN#+!7mnSZZa&kUadRsYS83ljtTSdY6^v^qiroWKi^LT*c zk?i^c*dmdvxg<4wuY_}hB!!ZZLm;Jsge=Z=oC~_hOCetqf=YuJKHx|tYdO*5X|{bN z1`;T<^7#|ZNFTW(B>6$5MU-8@fS#Osy`w~&2jC#Nm0-vo6v5b;;*k*|OUHS#d`~RvjkuyLL`jT-#0_BlHdh)Uh!K+lJ z{?_!asxcC9o~3uQUXN=G(1r5yuz{yxu^mJT|I!C)YipC+Hrec4-%y#H$nQ<`-548u zJ8i9Q95hVAI0FciB+fzKs(MNA&(eGys&$Z4M(tBU!K2@41y}+enP4cLlfk)Ra(Pl_ zY#lxRV;|-=h=_kk4no|4l90|BJkW?NeSlaXvygM{f&lEp#( zpv{GvQtJSNAcH9td>k?Yy!q)#68id`$9?g<9ZNLd#Z5v_A^!wXbH*g<@p}BK6~tQ% zf4Ht?h9y%p!OGTcn+^7;24syh*E3Gzeg@;A3RZfF+^g_ zY>jL3di)pnWdo(t%5eB;g&sso*Bw#E-n)yBR5Q{nF_Yak__ey(jv8g6>Tu#(#B!vE zaf5E;mc7DH`O5qNM*_}*bHev9kB9%}+b#gL!GH89xf;-4f@Ch^4@Kx);l?ky=O`e1 zQLQ8F@R*S3Ys|eL%T%~oBb}tUC`FuyCXc3P#p_NMg3AW5+k($qc{IKh)=|0 z@)ktcc$3vZC@Lz(_x#lJ`99@*1;aHuj_znWCgM6$?tVbI35HkV^v}!BWM_Bo*ujL) zwvl(|tbi+zjNQrAAsMF5OjcXRRM#xm;3YY7%36wNH$I=iFR3QNk!l$+j(1WoRwuI< zj1EkMEyi+Zw+$Q5DD~xJ^wLhh?BiH|>y0=Tof8NDzVS|4+#iu}VNoynoeOM zG?D{d{HifBd>l0C^TR=gP8l1q0OH8<`bx_s_jFaurpph1c>EYRe{(3b^?P{OY9(!5 z(+6cX3HU^ul=w^r8c)!^l8EUlOM5rU?3duz!t3JP+-Y}ng$<`^|kpgFr zfWH}ZN`oIZISOs-)jxQ^hy9i2&Qsgtc^?^VVY$ikbj#!k6X$ykHNFQ6shP_}vr7Eu z52h_!dw*QRdC|Q6i^0a?lTsoQwr#@NB8;W)t*q}F$oHromr`b<3zzwenH-F(gsY)j zK=R9QM&^hv=HEYlT{HMS;Zex-WFPOPQt$SZhi-Rb5*HTDWGqipj1(jsA5r&ya;Gl6 z|LS;vOVr7wXhY2rl^*j7Dn|Ye4?`Y$xp2Q2uH{F3FW+eA@xO_4Xo@}< z=2>^h=Irv?t|9*VdwWK8*$lSq%;sTP-;@-G@}P&C^1oLX+i|{gHYU1{KAU?Ja^6*7YJjN8PR@yS;f_^{IO7fIP=h z?U6ofCD~JKkFVL+KDt;veA3r`0TF^lmR{e?RPN)MtH7o`IuXJE(Eq|M}!< zE7!ZnjTBxMOm;obj@j{Lx}D>6Iqs_m~3t= z4a75m|F@`2biQ44L%rDRdsxvunW|~E2c~v=CCXH!ZZKT{KiEDoHyHMOs1;Efm_NQt~@5pWpX5oN6u~oua4cUem03bE@^vo|?o^CDl8%oSD4y znPQo@CKY%MUKppkM_e-yc^1jJJxMr!r%EwZMsoe#Us`qiXjcE*kx-j>AaeF}tIxo< z84oU#`IhYrOXt-1=w6@y?X1#q=SEYaOz3P&%(Rf8;fwl=T@zA_Ro)a`x8KOeTvJKq zxc)bv#`3_t6}#q=o`v&kq;-+~5O)M)Dd0a6pOq@~;y(#*)NL6t+P*Y$;h@F*uA&&D zlR_+4r%#oCfG>=k={&>!dsJ6rZynhU{}ktm1XLPOncmeXV;u187t)iQDj)k{J$&-b zO;f5Xf~+igGseF)7i77_v{<}YMeiw;MxPeiI2Pad617+yBV*i?$Ec)8gygQF?JH`N`|#?;!a-RTwuD15ZT9 z7}?kmcYKsWzr_VFLmNw+EyRyqhUqDBL&(S|dYS}2^DrSvV!~)!{()LX4Vw)luO%Us zr;KBQ_!xjk)B(Uas+9Zy8(k5)4b_}ane4hf3DNzgdAi~mpS`B z00YK-S?57#UwXcMEz=@~(y_>lI8zaGRfG+qOa*yp3_)?aLUH$no{!Af!fJ$wxFHj# zfJhv)NmwgW!7BCS8FkSw`mmUR#*}*X&y>Hp`(9&JclZ6UtwK%t*HKrpP|-E6~UzL&(MD;aO>snQUlyPe4qqvas3ln-PEs)Go0K9$gklJL4sZeJ^8) zIV_INQw|PNHU{}+F6b;Uc7uOmx}d=tG|D7r934VYY~*UoxZ5z~vW*b)0y0&R&^ym3 zp9yXk48s8hnpf4r(i+qI(^^yBoxzgenL%76nRJ6O#yqMF(GbVE2Ayk8ip?3||n4%nx(ESw*}n1U`kF-M_25n|&X1hN*vi z&6K|3_YX;pkp}dui1chEOp15Od1`qKJy?hrOTZ`0O)E2No5}FrD zs)Vl;ULYOgHjQP`q>|Rv|12X7_p$ZAYAka@W}dE)3H>?oK&1tpBypVlAPd^4NGs?z zZ`D2t5g2wZu;A-wpUemyuUJV4Vr;BAVn>;9M>e>fE$?!=*4D$^1?}iXV~>z}tGQP3Xrah1+1QBo{qhKF z(hQ$pq_bI{7~-ES%zfsQl8L@yV!^JFm)Yt$zn(dB+@*ONV%SqtJC-%l8MrwtXp?xD z#`17qnp3l&^^3&UKW@%994n#ttU{$QCO3WLK_lN6E%?9o#i@p&v$I3{5y7+!SBS$% ziO&9;qO{>{X&;C%LtrQ*EHJxP4!p1S{%1E^^!LYt7in`)#K)g|jtP8>2Jeh(#4itd zBxBW+EC>kvKa^v!+5xqP{_HeU%&U5W8jt$&8hH0TMAGa@cwz879>lq@1(KrJ#aN+l zT^idb3@?jGO0p5m93F%ij=wGqSY`BvS`mS~zMVDJwHio8I0@QCgo=a>!bG6HY6&@T zCkGP?`0-fCERoPIV%>=qT@^%)ZH0&fVa3s!zK=`=FhY+@ic$-N79|rx{#g=6MR-S0 zH4tZ@ZpDZ!`%B;Z>g2Wu@nxd7cQH?J)cNmV{jb7?z!#=s!{uNi>lmaiy zCunydt(Fj&a2E?i@`Ei1DFotQw{G1=+H&Z|TXuRCzh#0ZgMld<~D#YH(_VR9L6(rEw&R! zc#?zxC@7?@$7ab86oA7ZGRXCI$d~0(@RE+S z+msSPF$ixAdI}97?%qPFLmvRpzY04jaXCYtAmp-pPq%F(4*NLn zUX`1s9(w?p1BPj^7m>xlF38IOTMi+WfTCsh`j@fLBY4owBNh}CGHHm5%!}H!THSWn zfmuQdtD-kqHjO*b7(|dCEm{FRR|B z2GwGGXzjQB#l*`Z3eH-nI4*ukFn`P^?dm_Q*Nw>}Di04(#e&;-(K#a&{bKJC^PyYE zTvXta@UsleS`zs|rggu6{|@*dK!QGrtrsF=PZwRRu)>(2i3Ebc6OT4+cc#f5H|qZo zG(e0vf#nn#TN`JfyT$}?FwQ`6#*5D2MU}el&)PbT`j2qaA@e{BFN%BvfKi~O5Go6@ z!?T_|;RKh8gjGj!$5*~{DZdLdF@Q$ob)~R{5SRQF-tJRVa~|^e0tC$Ymaz|Yo86r-ECDV!D-9)5n>L8 z(-4;|9C2&%Rn=(+ZnQd}5-0lKNIevc8;3pS5)0sAbztC960PUORP0KbJ#cUP2^K*@&*bpWY9isG*o(GQ?tqy{6rAml9C z4-9hf7;n{F%e*YbK|_h3fmbh_6(iooUW0X1_-U=_(|?SDU?$n+XuAFhe-;L|aG7{J zC8HXS-7~|}$mtKfU3OWfJfj+x70jU9J|e?Pq)p;)i_pHeCHMam~8OL4Qh{b>&QSa^E}O_`2IP z60VQUOIWo`)biAgYgeciBBd{nc1}el^&)|XF~H}|g{WTwZkdiw`?tbWjSC$HjG5X$ zu0ydD*`E-6TVI4x>cN3D{T82<#*{7XSBizaLUeQdSjXmSLubCnu z1K$ePUU4fkwA+^idpfS;Mk^Q?dsWwH^sx%O)@@s$9uDbrekhzl-#zVaKe^LS%A9|i z&E!rTLz)jPjOVWM-J1#Cc}@FOCVd*U^})scuYE4iN{ouM3%Bo(s0+P%)zitoQTNu` z=5^CGR}>mPB~e=sj`%*0f5>7s$p2tNXIV{hRqc4vy5tO#gs2Xea_@GLKl2ItLR_oq z#$EFaKB7PR*gQ=3)XJ{(pH=HJ@Zw~o*B%Tu9y@biY0%EL0acBVGBh* zwQcvZ-IV<8Zgr35d#Yy+>Ns_A8wY=8l2SZARK(ZObKCM#_4^hR=FiN69*eC2 zQnP=c#38ES2R41|ibuPq!Eu0`=JJa<;f0HGY40nqNO-@60~=xFWS-ouesO*ng~8Fa zUD^~AOel8r^Ee>Vff_2KYSiu!@&hVz##xZNg(b^#LNBKUktvGi9gI<#AXeC|NK}X zb&sYx1iuCft9#FoUc*@nGvrR3B>2F`f7&)5!u$6R!yLBYOiFUH zsZAltRLg|dvZO; zK^9yrfFMxe9#y|6V!w#74dKGqq1&}scLVhYiPi(w5;su<6$F_@Vtz>Cp3n(HyY?J+ zX)mUd7$A7lo(h|J!5=jUBP+Z9S5S|uu+pFwC|N&eOq3O*Rt9jfvT=j|`F>Ve^uBge zpQ_=or>L{e4@DPYi-Vkn7~m~EAhOvlv*a}QqWi$9--IuI6E!Zy1l18dR>UdayUj5R zdWFYmj7G7`z_U!zO&;>R$aWb*vpt4NUZ+7!gPv%jtv$bNI^q=ejL9!bdSb?d_|oE9 z?`CEyUtW)$dIn9>(1Y?%4JhubWGr&1C441Z<}jVWbz!b+dqg06lGr$qlrAvnck#_h z9*0@h1#rFa>`NLKT{}>jlG)fa@r{c&dkBwVqK`rWf9LkjTwpk8-}!)h)oJfaU&m~Z zzrxEv0-TR2&^k&i64BWR(r3V)X0f0aVxR)&jyTo9GHeI2*zd>J{)gJZ<1DD);Ni6} zWcVrrX0bbJM&W_9@)?8>7AQLIQbDgcZr0*}!Wp@nFdu1Za)bA8>cNXUPbu1i4uxtj z?$4)@nM|@ck>ADu+IV}7zODf77XY3e_GpMCV7AAFnSX5?rLvx zK7g(Z1%!BQ{CkZK`;sg)U686-_sgIjM8N@}wAeaMA}M+K#ZLD|Y`Bns0A{RSyVdtC zlRPKP3y~YkGn=QiBf33CX(2MBcT3kL>2%|%J3=1K+ffI=fP9x?$-{>?8NGd!kS<1y zKfST;H1L{ZURd2c@Gt&}2i5O}(7VCP8qyX(F(Cv?;Miw~x|TqYii*52pur%tV!h!* zO)!#3OmAr^Fz96-EBAtF?~O|gFs4C48sSrk$uTn6Pc6S)Qs&KB5X&2n1j2{fn||%( zF|#ngWy{X`S8*tTkTy#ZFr%5jwuZ+w?IE8hUvL0iG!(GnF*O_U{#>5ucW>)?OhHIU zg$sKeT+ShA2~N->o9b2niSUlj44#2uFvi$$6wRd3^%54n4l-E#+n}WiFuO2mwO#Qj zb~!SjKR8G0H9zvBDoT;&-z~mF)`j+~R z6!xG}y(*T{ZlSjA4?TD_mA>?E?D4(l*lJ(mdHQVh^KtH8?}E3_xLhYpth;R6=F*&_ zJM%`L@LhgD8I%&I&(zPSm&T5IT%b1|`*y^4tL`UP_dOO87r&lxG4@Q4bJ;!{xiy+b zjpy1b;=7E7?NN@RRFfGu#y+lw)#Pn%?UC)b9CUjea0tj44KC#`_uMWh5PfoQ>4`oq zb=Zv?3k5m55<;VWHhH4jg}hSKjFMQIUb=KCbdeF4K2<#Zf!uKQJpy9Pb#JDelkLX4r3V zC-$-{_s*!6=cV7X&i0&@5vwWeb)Lxl!W#0cfZbHaV8a_5xnhocxlgxY$V=XWW5!Co z0pCv7jG+paI&+l^!)#Qm_k&_K;;d0^lR_9hX*gKU{WC#I`0|(^%QE**V#?O?4>xde z(Z9@Z3bRRM6&Tso=(+#U`XSbPzH;DU|CNeLC?@KLE_n35P6)E-;#dzDZYwq%*i_v2 zXveuXQ{?}L3GPcj{n34c(XkFA_nfDU)fkQc!*WC&*molRG7uYCoAo!>3L=pC2gAT6 zs8doSjeFuc%5Z??0LP)%A~4Pqr-=4v060ll5WE&|f6jb5rLCH4ngLG(;_8H>m^vVv zCA(#$9;O^*>pc4P&iu_RJPM%)`52!^=f%Gzq%_Bjp9X zRYekqjxFv6bYs+x+lv5GT<1B&3FZK7o?Ba8%1;6^*&*_RcUKsjfRKOFqZ3Dy= zaB7h_0YYfGG|b5~>)O9X)}{a)1>{u>L~dA~0_?ce#!~7yWxgat8Ki$E^LDLfQ3fj9 z>tgRk7n1yow*4%#{pr&~&wf0Fl2$u(#iRbhm$rl@fI4bB=izP$*$ExL`t|4sn|}bu z!S^J6<%a%_u8|p}Y}z6|?CIE@$oD6XJGaA9pNY719g|Y7+`Eq$W&L9SLIOp8?!I+3 zH?~CG%0$>i5Spve(mt;B9zP$o5O&u4iC>uxK{2(<#Mpz?pdXlvgms-;MBM?ec}Eh!g68#zw8GnW@1Q&)?j$(()qH&9bM@0A@c!}3 z$t5Qk$8eL}BVrIr!s88ndjUiMW+K5X_wS4M{gCHn!L;Ez%(sZrl4tdv52LT0$ypQ< zL>fc#DkJ)J0nFpzB43=4TjZ|iu}iTP2EpBZ8dmaKh>aI{xn5dT47lKY9LfsiZNO5K zs|xO3$sbR&#KDf(pGl$+!@W2vx@AVCZe)h&2#L2Wph{{AaNC~)mNPUqZn&M=GyZe> zA*d~cV?@m&PZ8BZ5INxIBOqF<;2Rd|tJaHA+}@H09VF@?z_Nh*FxKg9 z3{l`3wMnlWf+30km{h&jDS64XTboMp?@xk+k)e%Z=qh7EI#hnd^9-OBVQ^7uXwm8D zpc^7_R0JQwO==1jpcSr$vJOZI;iTyu7zly%%*uFrxNsgv8wpfp0$GW?nn_X)JCTF< zq!zC)UL;3Y%Hs{=iTBjG+qE~{)Y7u;`#znno{Dejk$6kVY)k_+uA3ZBu5MBgP_^Ry z{QDhT%2O6V-2Nkh!6pLAqn78sjR0*kej^ItAG$etK&49DR`TtbcJ7hNl?;Imo>h$|e)j>`iYyx@)k#ioN65C$6| z8yLgGqk4G1TsTVkdZ)T7=X(yMREXvz-^s@@$N}rYd@C*`2+=NA;{#1 zwR;%8kbPTXL04L0Vkr6vV&AhTk%^jaeI21e?NGZ`oIGdz@E8UpsCqnL2uxH60l)ir zl;_?axmBWnaSwTHmp&aao@!}qw1ur1?lv_A0m6}%@`}-1|2=g8%;2!gD2&y}rRn2; zjvKwF?iUtv@OXV_1AW7;h3|rqfi(m1;DM>tniJq?%;Wd3FF{!Edxhe{}96>6(3SFRN z;A1En@JSB^4A|xp0bNU}@ZDA;&?0!7h4+kT`pYB$tTvG~jUxo-D%@{Q0I82?;2rQL z>OFAHC>P@hIgE@Z;n(o9xOKZ?>(BgNXTqEWId89^DsVYe-ytCD{$l&>-|`ryL5%F(rRL<6zj?}CE=~Mkup+$T!2dLVb^oGVX5sN{ zx&NMP#Qj%hm7wp^XfbB0xiyqqB}iJL{;&#T(BSEv)3o6AJluSyGrly*#~XF4SM?_e zhfkr*cu zcb;q>A7zf|;niw45B~m`*`6={Q{v5frNZ=P3r3!bv$`hTy$xyZTBbXqJes49=qX~o5WueAOGU2pNrS@`|=%4Mhg{oNp6rG zHu-&1#`U7pE`ODdEI+Y8F0KP^<5APCLtWEkUYGbI3G1C)3YJAT^cK zc--$1UB+L32k3a8yObAl-dgsfd;CP}VD4w%YX0-RgS2)w=ccIyyI6SC-_&JrzjE~* zEPJ~a(Em%&bZ+JFy&gzI=j3lRM}JEkmSku8W*o{Oxt%FKFXl+#b{kl?CE$>fT$B4W z{NKAs>aX{hxg9F5OsZ(OwLLW8sMq=J8mGd+d2cU=xMo;p%`V?;=bqn8=|-0~d6aw_ z-}#db4QQYJ;RfCe_Rzd7YkyCD5@;sj0?3cO^l`xD0O2Z>e@K$S!1{52Yo3_NP>bko zy|B;XIjQ-TTRkXkoJXRmZX_J!(KAO4?d|k2zU~Y^^_ShoUww1VP;aWjnHwp9Ws3*- z>g+crY0mm-13zo(oa6f=D2M;cis=9Pjcb}SHmqaj>v6%7XY6{RI~UX6)SNG~toAq{ zVft*q<<`lEpbCF+%%1WLH2cZxE;>t-{(4NBd3&I+u58);jWgi}l=Z*V9l9vjNMR~VUv!BB74=wN^TOHbiaLFdpEUZNu4EP2 z9(g=#+w`|j%4L$;C;5a0?y;>~ijp`tkRM`Jc(u!Me%)DtkngNHU$?l<@#U5)ALoew zYIyT<_Ad^bApRmF7R~{)L8h-?;<>`Q>(ZRv3pbAHPip^>Pf*o%P@DM5C&kIuMmEIvwOxtXQ+= zXp*y$$JOn=`tw4XHQ80(-8m`=CE~NSFGY4`KQ^W*@pq4CSs;2XC;&3v8GT(K@R2ge zO%Mg%5k0etjs6h=Xj-QnXIrIPzO~0D4-%t`1dq66`QM<6oP2p}_dolYwdDpdsVyFB z2^z|{a`Uh8czzY}GY|m{iqP5%*ayv!0f4cN`Crwv0LX#}zvs_xcT1Z7mSKQVWXQeZ zf@wDl6r$hFjdFG3q8j~_5CcZcE{fV%U73D@ypNM!Bhjp%wdXug17MrE1uXCzB*{!V7DT2vJg1`px`g* z>cGLGn%WzRv``}aQ!Ik18(~*(ccb(KVhG8h9SZQ+&vQPVF6ARA*a#3B@Jr7Lzm6ah zXLP?)Q^1Gk{WEv5mpPY#CBm}zcyDthni-fX@ZVdc>2JSAG=qieI~D39&l+Sq$%9w)O}LV)bFb3>u(@t$N2rg)g(Ui`qe0|{FJJ~FVUTz zac581Co-4Z1J(w)c=XPXE5yqY*+5z>gtTusnssUanHx5rx%Ips&*5JYww7Vld3?2h zbIW7LUgAX5(zd*fB$Od|W-v1D#uxhtlN4gMfBM6v3aj^}P8??UEduf072R|7hnX?t zO?zEOUu@92)I@-P1d`nZ954B86%mAJx=x~edpz;stPD}L`~~gf_TDplxjKt%ln8Mm zLiPt#Kg4hsD1*qo7v?C9wAi$yCv7&#BgO`GdU|WQburum4coWf#UmmI321a*up~sar23;PoM0mgsCfinV_< z2#``Jm1F(yT?KtEr1op_8C+ZKhAFk2UI2hZqt#nzE+rTR0RyOJemXXoiECBj*^pkqS;)L}`Gx>Xa|D9LMk?yuIKv zZgkyp-a{S%fBvCsS3^Vj1qJ_`8HC_dLZrBOsVvZLjV?;lOV`u~HNOM`3@b9p z;_$Kb+Qg+KV|P~UWB?C~ijh<@94-4e}DmIB|*7_tBuFq`YKr(WwVV z6CpP{zRUoCvu;~&9Uyqd6f>N}3!)4YTHQU}-4?2zFi1t;iIr!on6)-*JF`T0N7h6v zL}W%pq@SZld2gA!6 zdj}Qq?3JEH+iyy&ZeMR}rj0h`X?~Kl442&HE79~c+b?mSgyCP+g!8()M?aZp*)A+@ zoGRSy|Jyw`(9t0E^2>M4VjBJ!z#p-U4vOUz>a18d`dgXLoy%z7-*Bh*+oyBuCfBh) zrB#_0JtA^tw~F}c45v)@Ssg~#)q~neN17&Iv*Ybp+|BC6 zdiNOJIhgFWzSYa2bUJ%>e&5CJK{R6^3btk!pQ3xzcQ!U$5mo2e?0B3;WM5C1lDaEyRLq> z<2P*aAMnVC+)(H5vk@t)^*AvXEjQkyP^BcAG!yW7n@>)9etco%o$vR4aw{a>e)N23 z`i`fcNsN?{){c!SVh_$=3_r-t@+mLj_jb+7yvjjN9-aq|J8)ECo15ILC=}P4CscyB z1gITZil`??&Q!TKWz6vF1b53VddL^7HuS2p_RpQ0P2kCf!;I>kvK*PP%gb&CRh}{I zjV7lB(>3lTrC@9S>d!4|&Behn(yQ^ib>)W6gEE*9WyqHTBc^5n)Betyv{Te83 zL98L;HR68=f{x5lc$M=2nyb@)Xb1d-fUG1TDXAk1`bo%$rr*`BZNh|vN88otGsv98 zum(=dq)>+=9Kv&enGhc;u*4;fzGk9;UM&LjB$g$J_vNR%iKn8=3TBif!Nx(lT+9kZlzyhdcEk1P|im3vRbR zDcjKK1~Y^6gJA9u5bNYyB97$TV-*H(*(Z+6UX@^tV_zYe;nQ&9$H?iiRj`!(tAB71 z)DvcAI;r>OiIFny6FEGKm~Yk6O+Q!=q?{)CX2j%LW+GmuR7q2SqzQTs>Cg-74@d+hz5^r+ z3YCLe?Ol=4WT=MMuy%O!4C9KHt23LwYXg}*;J;Srb zS=0=W6^x@Fy?df?#-9M5L}DvAu7RgUgsZqGF@mjminAG{G{vp5cbh6w4SBaq2x?J6 zY1@B>6TGBK_0(E-60HF`=_8=|LMI6-2|4lYgBS0dJq99ChtgkpVg-%DhvxXQ#+gSJ zoOC`{!jOm+0YEbA{{8EMFB1;=5^5t6Gs5D60>es`V7e3FL1Iu`bibIsbVbos&G7%Y z0Gl8j-io?a4NDC}uD!X8*b0((i!fM77dhN1&hTw=uuvo{j&4Midet3Pw|&HsiF zP;<`!ihR8G8HLN(K;?mXFW*p^aErLOIJ_}Y$SdH5X)CI(+jvke)eC0uB;DY7f9VQ| zNrea$jmYR1;jXm$C^d+k;WI(_md#~>;sdBfnL*=#~>J!GAX~`V zHSf0@u+qR8BNNnm*o{3g&uEn9bMO*G-LW3DJQ4>+q@HaaT1n%0K#+A_m>)CRChL0G zsPl(-mO)|f9eg@54gsOZBc9}XAjrpH&-5UHKwqN!0BSwq!<304` z(=pWrj7>zb5CO1a6f|ZSck7o{=JC643(X}+iV)0nE_J$al5GX%>6DR84eSa%E7K#= zn9!WEx3|N_g}E@1f`hMc17V3zl*&p=Jp}7O_a~eas&XR<+oB(|y0HiUxRLX6;mBfrg6@39`~1GqE6gKkY7#^*07*v1YbYx6pnBo*1l zfs=2Mh%M+#gL?)*Ycyy)*bIT%d5mR+VpK3$asv}HY%aE_l2zv3xg24T0(TGI0ieTK zZ+VgfBJo9_r;4f zmQb+df5X$*w}V6sz~UtKT*Q`%07`1Gz3{PcKSOx<_k^<4ettXt8^)%l8(~_%$>8+> ziH5;8a(07fS=G`TxDEsYwx=1ehrjvn#i@>0jHX^u4)XITv|z(Qd8iK@L_193?k7`p zN?y382xynEOQ#k>l~yU?$8cYv<}pKt3sf|AFs6eSiNltkjLugTGSenu_R^JSkg>t7 zFyv09HJ&ZTb!jwmmTKY|?uNAgd0LZ0nHs6Ov53(#+Y>9qBNl`JKmW@goJzwNC z;WdqQ+mbKI{mg+y`=LWA3uiF>02piCd;8w*eHF2>{|MF6!YkD2LRuUm47Eo**^9Dp z{^8q6*6hc~@5lt^gG2XAUaxza0r6j}i7Ep;q}Byx%H>PnE7h9yFFa~7yn3KUIe9B4 z=lyfSw!!^FOwZ<`Pnwy!UgL}bfpY8ibAGFHPW3b<0;{G02bg9qX-@U_t%YT`bL6dL z$$sO=G*-%gBDj#`7?Lh5sLL2sH`emS^$`PxE#LMR4UfG=&-Fwlu0obID{?H|A#+%9 z*3vmDuUbgxzl&FLGc_`21b;uVd|*xGVLm_PMtyGH`CS!60uAeJlzQL2cP_L1D#p1B zz#cTfIyLtN?sfI^KVY8OZyB}5@YQ*PnDd5d=lYeT<>!%}ifTSVpOXG6%`iN(ey#Ar zX8MfHHfAx!voR}-{&hKL$}D$dU;H4&IB#Wqs#7j+HgPp8(qo6x`FU}_qCwAmZrPn& z(Ulvz_ya#PW%DtpzA!djj&u>8UZ(DD>k`siPdT=&hh5@lmwKy&-Tv#b@PWLPaMEI4 z-tK}=jg+AOgPu)QpJ{CbQrF%c*{U$rL01|jnPDBcSv&mEOc1O8zMK`$Y2EmDue{_Z zC^;G_nl8PPx+43RbARTQAzBl`>90Kxvk!1nR}SoP5*-gbFjh>lq`0k%Zv4VQxr3F| zY1 zP6h8A_NreI!Y^L;$B)I+u3ioj+74@f{vtu+GctFs>{QMD>%KtuanqndV|+G8bcYz1 z^R#NRg_sJxN27V8BRfw&9P(#* zd*Yzs^!ioZy9){CsfxBAQ(lbj=)HSWeP+?b5h)>EB2{CO@&CgX;3`?Jk7fiqTe8>fSu z9Rj}f#7l-;sULRGl@gYCCY)js_p3R#@v1T7iaS>TT(a-)4e!PwR-1E z?G?GSG*KT-NweQd6070kH|~7#xU~DvZo$zx%R_6v_l`ee+%o-Xo9FMj%`W91bdKHs zb=a$5tUb}-JWKzMxADt&nAcc8^s*KS+DL2u89mM$w!QPxn}E%~8o8Gyb!~>JEs8Du zZ1~a}^O)FDBs`~ABkoJB)`(x3-o)f3@P1Bv(|&KBqU&t*Rqk|Fdp7rI&W@dJ_t<-I zg0|+NS*=c*;WK034+92SSyb&0EQ)_TSPn9KI6tbc+N#oiq5yFk!F-DYe{iN zDk#hMk-8Q0o9mtq51J-DEGH*fw;Qfj6p6h}to4OdT6e2n!v-hg?2p@DORw!xTnhiY z**W?c^ZDw`;Nqgoo;zX=4Cs{YSS*iU&}a5&Kj$#nKJQX>O!ta;W9nNf8}ncq=KdGs z0#t72^Vi%$EnZA$_uRSN8bQ0aZ1Fd>-w3cKRv+}pED5gg zi{ew%X`&!6If4RPQ$I|Y=>?B= zToP5`=G~9=b?9`=g*^7rM6Jw?ddAz!KeRhW>)tg+ayGD!(O&(`%mVf|^c5@=5)-i= zdeduuH8>-W2s?f&i>L=r0j4Z^`z5|EZc2Qall-kmk!#(FPxd8`+$GGbBRb!|AM2%; z+`f_CaQfY>x8jjogFA90=REvgaw}u@f8-n`(~({f%Q%l*Qj>0pVp6i#Mm1vnSZ(hZ zsH4X$Es@L#ClrRrraK2upaE!t*yx4OL-f7qCUIcle0WUr`ThGaVzy7P$Ik937vW9i`E}@nq)cbA#=N9?8g)n~)O;!Ethh$PF z;!`aLCV#m`AXo;)!~I%gUqBv>B$^Y*r`9b&`8Xrw`yjjkQHC$SMrC*8W%xxP4KLzc zJOuNkqJkTn-S;$LX7VJv@9D*phBZBqzk<9<{x(C=*XQfa709rHAA0Rzm=KYKJZbZK zUU@F4IdtY4YfIpGOi~aM#LS0*|EfILk4_mJO;9C^*X?p0Ic8)Ocxdt|H6H3!5EX%8 zqJxuk?9AN2xaJ806#-L8wi)=mobz%D7(0MxWMQpQp=fSuI*2H4M5WOJ$!7O}%#~=W za|W|t{X}PloLv&Y3qSIvUri`DkYMOp@gj`&cW$l_VO0_FDdthYb&+rs4tW#GB$1kT zm_h`RpgD~w-i4Pr|E*_lL@$XE|Cb|4>K{MjHb8344)j~{LnI5Yd>9I4&=wxMi|(%> z%hGpoAQVKpfZwfZ|LCkx-HZJ}yr6B|Z6{nx z%#XWoLS0&&y2=Q?`DdRwW{7HU;2uj*f$-gcPHvBPL0R8f!b2lDb4ByC^_gP2%EKJV z^|p1W)MEgt?kkI=-UJ*N#y(s3Wxa)J9Rrf5$PAQAeX$%jhP2WsBE+>`lLkCOoB+X@ z;X7mvc02iAa<{JmKP16TqU&skb^O}Y-?fgyHs!r@vFPte(2)MyCvx&v{<|%Ri86u)1p(BFD|zD&@tWIsJlGG&f@#lF3Q)i&q8~;eH8B`Vcr&C~ zbgVv;%4oo0fx`sJctnR6eu!jlNciP~q=AkH!DE)cK9OM&P>x2(QLEQThl^K|12^um z?;(Vj-`aEh`~D4Rqrk!Iw@jqZtfyJm4C+$m0!tvDKbMKjGXlE z`IrHRXOiTrBPEbd6qzI^9kx+4nTmy>+n%M>X-w~DbCqa66kd>|3rHDOOZqH#zaHst zv|6%j8-A8gV{^FmP>c4C_^CI2R2gsPJTFl&HW};2K4cd3IkIg!!R0?jS9>R$hIk7P zmjB$I6`z0BqCeT}AwLsio57)SJeCrgKzG`pbQ) zL#-N%B>LSCZsU=@P=7Ni*0pV9M)O%>=LuaGCF&}coq^r5Q7&6c*~WBcuGeQ=zGvZ^ zIbBBK*^ql9jm4Wq`i|xegBRnaGvPem;eFkyv&-oood>V6*E1Xlyk5cmjQMJ=+4>wA zwVXZ%x>A`Xw{2Av{nSpox-vhe&gLGdSn-s)cyBPWxiPfguyjIfLc^g;F~eG2^snIO znuASYmZ#P4U9C}myJbN#T5`SVO7nKAg!J9%Q?@NS`Fa%&$VHcU$C#tboTXa6X=#;G z&(+IIt{KjtFe&Qm979^(Ff_-f0=Y+fRGd%AW|6}Mh7lHUZ@<*N(cv9j)`v|BKi*;C z9Cjd+BWSH-w@iw>>&E`E*{xEZ@LMkqA1+$Q?o)eK23`d30|0h>d?~#ZohG)Eok~id z#bkw*KH+@_Rw2)yI$s$P1Lr^x#R-8NOA~!!NYD7Zc7u-ZG9m+JI{eq91XLtL2<`?J zQiA*`&;JPy@RuzBFRDYT07qWmu9V(un$Q5|EP*U(COWjxiT|^&N=Ydk;~+;X@rB*t z)Pn;b$P8JKV$U^~x%YE3gT*lDT0w#fhJl1QEOyz;AV>kOOJdJt#jUGu(@Jz&eM1@m zlmGw%V#RlpyuKx(3&^E^d;v}pA0Z&TFI2V>ypOLP8F2S#k(RDUlrL$WNzlaU{ldh* z7uP_@!B1pV8Hdg7=1SjGj{nc6j?C_=OZ!*L0EREY7oRX%iyVZ&B88X2tG`hl)mpN8 z#CfEJ*ZY+Y11O$^;f3KO)!i-zh?)Fm8(RJUuGnA%z8240b8mD6?5}AIU$tiXH;I|q zB94J)LsTHa3QY3!Py2nai>t&Dj+cA}2a)7U-gXxM3tzrj-|V1&`L}~15Kt6xkAsao zxlOO^OT=exAeRu>0b;A=AP9iC8FrjO*pf2F-W6qt^8?oh!xQ?01`rbu9i|b{3+a~X zRpd7%j6Wi2ZSHJ3hwyqdxX3P~hsxBXGbI<}QY|e;;=c#%uQK>(its8Lax(7${qj_o zIY^H!I5R?FPAViYx7brn7}yHlmo=}(PBO@rl-HK_U@i}+msqHfy&aQIt~oZPm(FE7 zA(`lqpS%8jOkEa(SS$D5BZLWwGf|-3q_iS8FK-06Y7;6esJvs~zJ;xRs76?k1RH^=;Gi4@@J}MZCcqD-ZkrDL0ppHX$fCwlDPt_%Z0SE7E`0mEpv@7u zewZblM(>YtTnoWHDS-D7S70M+>pdTAywsuU#VUzHDii=6AV>e=`3|_G+I4oJI3c2Q z_)OA%Ik1(JlMbK-$s9q#VFVRa>A1RPa#RB!&7@Hr*1~C{f;uO$r21()vBe{m4)_dp z6U$WR<#KBov%eeya(EGmjeN4QeJ@Shn0!%TKs`YeM0k1nq=+9LZg!Pgywqpn=mJs? zk=#JhLrmoA0w@WDNlyG`2~FkUtJ$a<$iYV<8A~N+X<^ie!%ZHuU%a=bRZoCHV%~ou zaChKj<^NC_k{Yu8?~+DQ9jD(}aI0P)nv#cL0g92B6HHpI;~FG(&g4~Q z!k~{F?5KR2n!|!L32aM}TR?9nzZGT;DrF=~A(YpP0izJRqpeL{!OAO30L3uEfu4*5 z?;sH};jD{*XxmID_IGOsu^WX57c^c>ataOC_ikaqlOfjz2Tk#v>Dvv~?!Nm0j2W7k z{`&fU8bA9YB(ipdN3soq^dOXRzP#|=Q2 z2K5+oK`qexCM>X+v@~Xl$LD~o0PMVj*s#wqZZ33&JxJc|DF*=b4w2ytAvt5b(X@CS87{KlQiF+E zBRB*17Pn#QLYUEbugv_9e8&wWjsPJ1`at!hB3OPSDyl_kZH5K@I`GWU?3OP}iix2_ z(U0j2ov+6_(0gw{&4U}tylH6C~P*N%yuM@Q- zQqh2y6twvvEMKBoqpZr%3O)jv9p@6{+VSIj^yvra+Q;3xuPSx=>8&Y0Ff$R+TQ_>%xr|#bZI9zQjuywu z*PC8_qYDmno&he2sg6!sa=5HTTYXaYA%^D%b|q*7UG}IpKx7YC%yMb3^Xk}T@AW#P zu=modrwm|sNol8hV5Y<3@W|>ojd)MqyB~ihg(-hnw3`{QQMX(KyCFb3XWcHmQD&oVT>!wg{(bX>zKAYI1%g zf4E<_x&`*^dA*zG7EJ%o9DBcqEsSp6wiZ|$_I_JPiI1mA)8BldU+96^4|kr6RtEVM zP%*R)zFKS8@_d?cM@t1$IPF&Dp{ToaNfF9Hd|^dYEf-zTx&f{~_F2s7OKTps6dt=V;~v9jw1V)5cKSKku%&gH zZHaxMf2)6GiPJ@n@O{zWT0LlIdCe-RQ}^Vnt!gm5zpy$z#NksMQ~9U;XN>2>`H7uk z>o=G!ap-KaPM#pUBJsPFTsK7Kum z(~3=$VKnn(i&XR?{2lFLrv>|F`eYKRReYM755E7y*A-BIO0gkPP8SneJSXp1(ruC|gzPH|< z8x*&(AJhY%sg9E|tQ?Ck1N!I&+$ zXFPlPHr3H=H0C(xV)m~@GkicCA?(^?g$!hpbPqIG|FD8|1CyNsY?z{Urs+)sIzNdf|nSjQ`Hg$ly)pTQCabM$}@Gy&L` zSKKoC&R#yYh~o!-Ec05s&YeGhZF-7(;$^yjP2gLIZf3!Q!m^)SOpBFfg(t zGnk)eK78n+T)2c91%AwRc>y=T@he}jMDoF${84ev@~Q|B$mV1h2LamnJS&nwB{Dgu z$gEytmOhowFnZ?kUtkOH0ipZo!gr9LU4mL6`@Adw`sZQlIttA=P5;l{z2OlTL?5DZ+%pbi<IgW3SOpv;+m!safx=ja>DKAH?H@jT zAcAsR6u^?d#@LY8qt&AMF#89{CdC-=QTEPW#KhO(diCLd)4y<`Bb;h*eMdKOqRdic$T02n zsT=>t1=zI_rzCXKbD9V0?f{-OR+Hi4QVc?|jJ47LvF4HntXe>RB1~4WF;q-^Ayw`& zwlsD;e_rGG@wCv(J0&DoK#2gFcQ=)_vt)*3rZoRd)5}%%uZ_xxx}1ufz`b_dGPT4i0%K(>bVzVq>XpsE^!QO{DGG z$f2^6>j#l4hDAi&Y1lr_5eYWkN_NHkCYt~ z!w|Yn=z$`jL5L=UIO~79RzM9Vp`(VhJS@Exu=n60x`}T^`rC(`3h4=mJ<>`S^9l-bQou2#3cU-!e2u77jv zl#V9f(QvEv2zc0MGpb-WwITPUtky-!`MvdauSS#DZHI5D0|6%@7Kgv zg&U~@rBBCssZuo zcieG>%6Ok1m{sF$#@9klk9nl$aqoJfS7vzwLt`v0qjN%?cbNg|G9^nr1wG5kD)sD9 zIV4e)s*VJ*(0K-L*09=`k@Xi`O^e98g%}1tJ19RUdUtbu|wT5SIP)tDDCzuiC64bu8LQb}K(qJ3LdEugXAzRuG zK*iYwG1UAZ1wAXvGu$9iQLx9kUDR3wdI3rXj-s4&7d!Y z@#YC-mSM;Y;k3wv6FcPjP-a3yXAO69=XW!V{f-VVtC2 z+X~o#SXRI)W84QTn>ezOs|E83fy78IlxJ_|J#=|JnEQt@45|A(?pgskYS5X76?CA9 z$1b|Nx!K|{AQb681In+WRI}>Pj}w(Fb_x%H|3}kzz+>6(|J%E4Aw;A|sLX7Yj7aiG zD6zvaO?(4qp>-+hwyez7A&t91(z+(HM%IvPDMUK~PX*;tK93_WmPp$JaO zwmBknp8>4>ZU7QY0w<6_E4;~DR*1x(c$o1FzGHC+=g{}ZmFr<%xbDESgNK>O{D7l9 z@1opPBQ%Y>hQJwc`IcB3+T1y9ecx0PhxafEOzpA`5WGQ=SWF9$7Ggz$iI8YYiXWpp zGPeWHAn~ApeIx)oJIcaDjSC%}rrW^#0H{0Zz(JLh>`Ao6kCGH%&Z4@t zH(vv5!BgzNSO!FX=Rt0bWs_usb$A#6>q5BL)8Q-I9hoFDdxHQ3!ALj@4{PP^-FQGh z9lv{%!AA-V25| z3TT2ylt?dt*XQ%}@7}-P!YneUdJRYEUWARoqPS7El}{uO-)`@vOBM7U20Vqx2!z`Z zhoo7)g}lwGJTD8;wGqJ=D5xW?LKN5R<}po#;~fTE)q(Mlqyz$Kg1l`rp3(OgwV&Sg zPCo!$N4o#wG5$VMHaLt}vby_+v+q+^7(Zg5AYp$Pjv$YE5m+q``3Z?3IghV<#WP#h zI|YuZ3AoBtXwK9JJ^y9H6D1=VhXAK1_%_-V5zPu%n_VaXd*4>2sxk}dp%AHx#X~JZ zCsC}bw0DXqAn`i-FMZzuB^VSx!}y%kV+$r74i94RfyAwg&qg=eO3;8pT)L3o5kuy= z(nnz=fEb9^3|OxB_j@48xBv0^8IGxhl7#=ce}hMT{N_6jXf1e6u7eZS4fm%VejQLi z@j00oDFHyD4 zAW32J$B%Hd4s9=Z9t_t9_1KWgxfj1rU6R({=?8)$47UiOG$??P{F7u6y9Ql|*3V-= z67Y6h@ba<;uZhkq(oeZ*ZHAs@le$$>%Pm=3Dg=ehAWS*y$rBC~t^+9`VnA>q4tvDX zM|T4Iz+T+GMi;EE_u9RI`=~GiUN2X4M{*OhnFG z6U;^rJh8KK<4Nmzq=P!Wqf51olvY3qoU?TBrouYB{CoUb?-Z1!qUxTVC|G(JwaAU* zjHnP1G}i0IC<7dgWS622gnGg`1&T6wKbm*l)4cZWH0Ufeoc=nN8saWYC?%-sDU^;M zNVJknykeF?nl%u%H!i@##YLDeKnwG1?3L)eB{`e6wKNoJcUSQof@K%_;fmDvp+_+X zkS93sq<3q_VfrDps@yY)`dg4iv=e{hSaQS*9jMO87vW+&?J)3k(G|wj!Vb@iLx_2; z8>t&5(qte+dZ(_*dSvUwXrrmLlPNhd@$inK6t_b_eaH`o!Q8~TWBT%vO1X!Q6DiQJ zhx`))Ow_Kz$X<(RZ4l(2(>WI{fi~jT$IO>q!v`=90C&<=$F0d7j^yQ$I{EQcBzNLm zV$l4=9Xg_f%U}cVhgRxM^^O^KZVlznRXYz*kB_}Mul9D$;d~3blzV}U_{a;2*B^?? zE=z3U8kSLvTN4*(>YxstKd|1!Xy(fu(Knk{j(_1xdM0|6+m@l_IeU1RpyJ{)&B5>H z9#)6k_7#3wps&3+y)N|Fx@;ICADv{Sv|AohdTZZTET{i=DaJo`l76wu-8ogDIgdH4 zytGZ|7ImWP?e;_NPYoDlzgRY_Gj8==5|qkM+H<7A@1r>_#bBTQfqugjNrl0y7Y*5| zD^K*OFV{XQoaRn%sP!uRyY=8^pZ=rEGN&dMHu%?`DdHS5ScndyQJxQt56)!R6&p%X z6nmSdHik>zrqtc!YPj3_NaduQ37e<_#ZjMJ6ULjZI`5_N(bl`}A7FncJF=x>>&Vdd z>^;V{Z+qxm-;@@t+-%hq4(*=yQ+5fPJG9=(o%dD#wV=C2q2aozOvlppwEGL@&4^3I z`7)T>ezyKpd#Yom%quc6f;-&&WNd*=o4xf?muB`MUUjoMj){#C@*z||-_B953Tj{6 zeKR5W_*3e>Lp#}~C@C%dEZ2+eN=^(nKl1p4O0z2b&GJmB@(k7&2P z6?Y9~vAU@I6``7Ucb{CyR9q^izyCpvCXep+AE|KTZjZb~nz7l}98Pln9d6+w*ZO7s z9HgmQQY8+Ge%+BDH)9>}?WQ~@Gy6m5(KovM+!WodnAW=X^Z9b4+^I_Up$6IRehSOEy z*BWdrax&iz`h;lg(|X&MRWANWwwB=}jlBkINrvP7kX%~Z3O1|IuH%lI-^?(|rYNl0 zOl%*#{c_pPgf)C#_h7wk=y~zFn|g}R>FGADTPJ!|vwf>)2>6~sd}7YU{br@P-c0Dh?MO+I$mkTbw{4 z;MC`xH?^Ho1@?-GG-+Mj(C_po2A1XLa=GP+q8STvhm(Li)jJ44WnWm?NK8z`@d3hvt8fC^T>oRRJ2;z+{}gd705R$7_HHeg4)OP!Ou6e=n! zNpkWKM!LJBwP@D~2AHm2o+FJB9Fg^uW6JbIVFH=(?eyIySLoA6iLf5HM#^FU^yH+u z2+akbKeD#X-SAA{h=dZp4%315zyi{8>g zEEqAZKW;b)L#a1-Nq@VtWhSxONzjvx!L-buCzc85{BL%~H)3`4w}>4fjYiSZ6%6nm zJa9MxqVfuuRa@dcb#mgr)4vEO$1q88#qT7O8uA(-3GgHHs!OH4zfPS#O`={jVXD~i zT0F043?1r-G7&`6=k;Ed(>N5!JdO4ATzu7}`6?z4{5^T#0Y8NM|Pau#W0Eze8tA0q1a`tI}P?t0`t<1VUkMr*mu9Xg)9n zdB^EJ6t)3!jJ=~G;3bK2L;C3;RnLpF{m=Q(x#qTV949JCttH)09Q2bLH|Wu@8~_Wk=|s3r+}a6`Xe;)~Jw3~q*SwN=Ru*}~2yXd@@_GU`Zz zG?z*W9Cyyx>4)owR)Z%?DD>;#kE5o2atAtH5H#(bom(z#YMUDy8)Jw{yON{54<&+m zA0OrC?+{yNo)=&sKC%azczyt4+rM%%C50hIhdQEeh$m)0$}WLmsy-QVmgC=}qg|^7 z2z4k^k1;fEI~S8hvKgrI!(%+mUMFQk7J;?xHskm@iAZBHcZlO0j!^Kw=V9 zb);gygCz-^sAE7_{BzszLqZ)L9VI1cZ2Pi15$SOFiO~a7#OA&gwcf0QFDfuHy1W+kS>we}`+ye=o_7d0YTk*w)k_|UIow)yd4v!uF!JY-6E-fqi zp_lsy<50CN%?5|hVTtyU z;!Orfo$4S$%WO}7qX_sm_H)Ss0gq`ixt-@~&D!lK})%BH2msOTy;QWhH_GknwC;L zit?2hlQAvg)0Xcgxrl71eZaUo&7Zup0tDf>NpDkg#p!{T^8)zZSOi6$i9=5?mM-#X z7(gj{SSO-u&0g=k**dSh`8=M#_!C@d@JisZu<=}*SHZahWunk(OmuYimZeC>`cuV+ zC6HW>Bq(n`AP!j^MdtdOh868gE!5H%HsH&#U}4D#MpZgL9qv=l0nLg_Z1~0xOSfG1 z|A!DF%MvX9_?{R*cJk+Q*H;6h!RJD6S-I%%GF9vd$^G|@LyI4}C+T9s-391FY677` zs7Pnrarl8LCgxB?PUGCj-ClEFR1n!bX!C7T$U3&wIRjvL-`N6=PbhoC$k^vdrSc68 zm5AXvcNM`+d(F&}q~0U94Sv|cZ`JpWw~>a1suQd+I(x5obG{dyRW7#3Pxzi$Zo_xx zr#c7opmVyKE1SwD|0$Kqtql2FUTt1HuDwTU;W7Xyr~@bJbW?Oa1sDuP*9L;}>f-Ev zVQ`C|`FC9y;akVhmUt;MECl-5H*P(xtgA3uKFaT2_A zozo!S%zl2X!y5$}C#R2Y<0288Nc!BvdeJuk0QE5HPhMv2k2bcj(0_C;F2pLkqy6A- z)Xrp4a)v6Q(xvn%8XV)};xrXw+v`hl#Hk{>@k6Rvb_aS2*hoJOwpUHX$0>ImU>g7T zTs(fDd@&){UZC!{GnF(zY2tQTYHH@~iAT$}t?REY9lpX30 zmi2;rXa+BItdQDHJpP{K?Ca_541;9zU7+Ny%g8j6CQ?&)EO0};%6anX2XZ3*rJTVG zIOdHE?ZNPk=U$>15ssNHsUZ-pk(f17lRY&Jp#f6z^_CD(KF#gK?3$q_|5JohH2z+RV@)4WLS^O-P%SF3vNVIhs?2rGU7492C52( zYqVy5kq1yGQq<5_N=v#D{q=J>(3sg&K5*Hf_59kq!(rM%FgKC&7L@t31KdLHg%(mg zB%6=~Di}e=AzPOW=?l^LkzJJ~rPC?b+4_MgLTyHfZS!5O0SGJR!Fi#-s0<3k$k3dh zMN`>@?A#d)6GX!St}4eD5+|9aBZt(sSk^toaLKAV6KF{Rvm6i6xq#(=|EV~Xv2*-_ zsjMH)UlOReeXJ%~>U~%8=~J#LYCQXN>HiqqNJkzOX;&>i-Ii1}jc|h+@Jajn0;z)D zIm)f=xT8qJP9QD3t0d{Gy!Nvl!8h>On*}Mc+;`$mf;IpfAykm!Ju_~xo}>!YyDpxY zolU*6G|hVD#>Alk*wje}s0i#iJQ`a!bd<0K6D*RPF*H2$TLF&~W(hD0m78Kg+=ABn zeo{jU)dKQ0gt?e#A6Nd^^BE1|FoU|kH;>UKnGx%7>G z`(}qRlal!}_>(64Yii+m;KC~S3N7uBh0cyNWrMgNL}okh5*;jttzMjSt+^Fng6@a*!ZGN3w8s{SiFf^Y0CbLQTC!e5ov1Ii?GBM?l?Fqnd7 zNHr`F&vQ{(VYN93PdITx2&0V~l6U<&nC&YN_T2cZ`EcfYZ@KQNVqS}GekE@dj0|9DRfO3Bg2 zl||hlm=2gT3dlTIkb}1+Y~2tqHxJLdz0zxU`IzIy7p?f{h8 z2+dd@QZra~pxZ|z8KUn{M$YHEYW$-B5Q06!d1Mk`@Zbh77-n7t7v9zW8#lO6PQ7}v zCwfE)nouFku9Qn&Am>b}W$3elAg>b3GjrC|ERA-52sQ(u2@oTG->zk@H2&| zsQ47`s)cK8wjWhrI?j2Rw3Vs@WuS%D#w{Gv-Y532_arQRQ(lZqu23{zzdX6TaPn63+PaSxGapaM@~ZC?w;0>Ktzo{%CUvjt z*@UG<&jKIct(jGmDa}*YWwn_>Tps!*+QU{_p?IdOK%~?2zP^+8Sw=pMXKNg_`_>LI z+(@}*Q7G&%?DuSg+qH=VPNDi`DY=YGA+l{pTp3D@>$)};TEzCpR>>T^V6E94xe5meBNNZ1%Jn@98^D6v)mVoD zR5ll#v$enNJcBOH*0#A1tg_wpDpzIcHr9UEU(NB z!jQ3Q4TZ(>(*y4YpQh)eA_@FqMXt16Qr@FS8{1=$+lH-d==yT+SN9rjVq}cQ$}LRJzP^=*E=xG9Mfsr8K6la{hXoANeGj{m{0cTr-l)AA{GbCSum~s;0qW{ zD0z?9u5ilz#RvGz?*-|bJ?e}xYYxgg`h?EX(z0)&?sbQ;NlXOMGfx~&J&;lnZUs@R zZLyQg;iE@K-}{GpG05?h=6>qD6_1C&u%qdT(nL9zA+ggnC9Y0FC+( zX1lnU69fQ(Y$hMUgoR<3)M_I6C}`B@5=CNCQpJDDY#a-aJrI!3`D0!t{YP+=g!cib z@ruq~04y&N*O16C_)Va`Alw<|_=2t@#c_qzxHs@rrv1N=EQ|jdzPe}Lxf6I-o1;um ze_y_N5mko&Vrtu)P+oux2z0>RNdms{i0|3EMCM|meg%4!cCfq}QZCW~fsx3!^>oFh z1*bpXt^aBtJ-T6sgDX;^l9RXIbecfSoOziGA8<`hyFqY+ zMJSXkL2^M}US5iTLeQfb3L?Elq4m^mV#6qP?lQ43g=Y<%4d&a5Es7gX^!4=-JZu(N zlW|Qk8WS*HBYl>FBJKU*U(*CvMf!89*(Zj2iwg>8n63$_UBRy`A|evB7Q1dx)Sd*w zk>F|Md{a=(cNqbbQZDnutGp1TKZG(0e&cnU+IK5sLm~;n?j9bvCE@16l`!m*ZU1M^ zNKQVG-4OWxu2I6=Bz7vH!4r$vmPSbP7ZMS2dOjoW8FySs9)AxTP%)7Hu?OB+@gTge z5mfh-NQ)6zbnGrS`mBh53>Ru*Q3X>G$<}~5E1|6vO{$px_T#Fo-Uc>-1od;3Qwlw* zmgIZj8h>sCyKHr7JQvHv^DiE_H_*``u|wnl8-jmfI!RkhK6#bV!wMB8Uj051yL=t` z$PSI4t|qWP!jen6w*b0lVjPLk&UD*eRa z9uJ%pFsGN%)%UKn?y+_3NPACD6xuDr%{FrMkFbK;b3+=DsDgr7iH>5^dGg?x!Cv)F zY*e)k?c;W@Zb*>i^ldRbWbw@iSW^ZHe<)1#&eob`Y$k@=*G1Jh>;lYr!fi#}qz{qs z(&r66NeN5?GbKc>&_G891Tvk{WbY(FLb<-Rcl6RniSUEKyz`?F`cOY#sS2YSp_!i# zaom){CK-QFo1xWI`>etJKmn@$5=N7o$U*Yy6r$l=dvQ&G!W|QTR>aeCg{m!V;=*%A z59MZ5r+M~od!C$k*3Rs7*A2I`7`UM2dDShEsE>z$MNtTseey&`djx}$A$@&`bd7ma z==g|_5H8R6KcQ5`2Q(j^)zxy!>*0vDi`#fKfLHz9W8-2GzXJ1s9{FxdhEct%@m!B{ zZNzfLS6VEM%!t{a@Ly5BL+Vm(7VyGwJ1cZnPNfw*e;)Y<{I5)p-XJ7A*KT@ zpW}%Wdzr=OZ5J1J4y(T)+kW14?-X5~BMi#~n$x$k6qe`Io5)2wdN#=v=Nrx=FT)Ae8X138tH1dO}a-I{i9obbY#2Hr!9t6zGX zPl%*xNbC_2U`Fp*`Y7C}&v&Ix7U#?Z01Q1*;N=m)uD$JQ@fB*)vh((l**T7)_z~?fRsF^+(2=A5L_~@^T6>EMFbkjl_Q3gC|ugTnE&#HU5q7j9s7u zX#EuG5MpW23-*Ov6p#oVAlwTp{vaWQe7>-^c;O{Op}FpnS-!iaV{M(OQzp$uq_Y#O zKvm8?lmUYV!A_uzyl9Z3+zni=$Zbd)c9S>x`vfgxQH3yq+wj7pa}>Ix!`G>M?*B`N zZ%y0sYKHx68%Z-ILXE*z-AFG4>W}~#r(rJNX@b!sdOju1zu}vF_a|v(t?<=K8I!8^ z-)hD?ZYk=B7&EMleiz|%?8T#&q;Q{m_LZka0TdT`B!fWn{yg{P`k{*7bWuYH!t`Li zLKPS^oW6-C9>It3-qL(N1>FPqCDIPTcd6=x zy9n_yK`{sZs-tOOh%i7=l4+_E*J9=^T%dWMrK__l;01nycOFTZzP&AZrJ>v(rL zFd)0rD7q^elMp$C;B<#yhJJMwA(yAg83uh_Lc|6NGQHIw_v+hEJ%Bo-U?Thhv|&2N zR)_r=9yyYcM#ojcz742RAwCEIC-+{EVfzN~L4x?bir`X|>Rt#^>mbs2Aj zg$l7&LoobPT53RDLJWqC$TrHhu>81Z@8`|98^dvtgaGltq}D!fR|qM+9Jy;$`*Pf=op$0q%&t zi)g2EqM)sV;-z+-;^#n;t9AXtlHQFU5RyTqGAJGvc^rftqK$sx##EC#SJi-Jhp4GF zvpYZrnpDdSey;Pr2{Vw&>?mJcnGFK|8CL13ja3TFA;cVMWt~@>V?Sq?&Ak?k53O87^M_k`zMQg z%vc~rCRu}MN*w!EA|qf8ks;6$$m_|xGe{9cUY}_N*W+VlXW{)vLJ|#m=BcrS4oKb| zVzzg)MYEalqt5?R7eQ4ClH>|f93TQ1Az8V&toWt2R>oom0XCFrDoOBf9KM8rB?2wX zs$X`4g~ZzcFl2q1>wrFMiqi;Upk)E-FGuECj|@ z{^pk_Lhpiv1B6uv0U?G8%G}JIyez*)HvNr6x5m@khB<~m18^YWeI)|QLuw0LWO$Di zh{D9djz`0}xtC1g4r<8O-~yEYk903G|MwBNsmO{#_4KoL_C(;Fltc#>3~6&z6hg44 zLaP~;z7;0C`70>cATfZrZRjxALZVr>msKqkP~|msrj-o9UW<#K1P_O=g!IEgE~mMA z=vb?yFUk-QMMcWE2pk>x_Bgl?q;|yjUShNS3+NsqF;cgMAVd~CV#%+M;*{MG_2CV_ zxYNctiSvc1vN9in3kZM;=^}<#G>#2s7IF8EK#j3PVI^m!#i|QAX zY~7%Ct;{5I7d6HZ8@RTP`?{;!Ik0l<;eC~6`BCNu4RieJUS7*Zl}KTU3#^Pe{@^ph zuc~D?4>SmWD{py6t8Bgdr|N9LsZo>IQw)C_og!D5`X{msN;kbxFPbk=^pN(8UGP7- zT`hi~XlXsyfb5{wMy9aZN1_i3rsH>8vqh%b9UDEb99L)d>hPYcgD4L;a?gHHj#BvD zCg)hj2I{7x47A+O`4nY1q_nK03; z_%@$7HJ&U}qhT`rGwhh~0Qj&M@3=Dq&RXvl*UxJzcobtIlJ~xs&P>m<*ye${_BEH@v$gB5W6}U)ahPmvZGSsYYBS@rBsJ) zwZRYH=BiBhv2b>)`6OHWp#r*0#kTm1l6M)IbN!olF-hG%y_9C&Zdte@-0)%kn_!jl zK=Tr9LVvc#xA@!C57g(mi<`lp5OZiI+pcpa+B>%YO33J0npe#E#AQERAG&w5(!$BV z-7jdCrL7m$e68e9Chu*E}K#IxhR^m8xhc&Pb5ZPFRA$4451TDeyV?FWYVp<&elg|hnW?p-4zSDU&1LF;y(88IR0B;zce=jQi%}x#^#GP(0!zKNO>q97jtDHy z*u+FF=EZHl&$(ZA6QIA|5{h5B{E1zatys^Bv$J!bW0Co|LNMH5qL7K8P8W`QxPfyA z?8kfIi&Y)V- zY~yn+@7+=WHT|6j526-f{Am-pL=t@5trk zS~I5##|f~~ouL1ETvxG$yZv2V%Em<#=sM!*jx!jMQD6h_j_C7BXj=p6nZevLgjs_) z0|)$}8OQHK;+QP*bwG!%-bCDZ>#6LOUZk``oM(=0bw~$h6peEa$1VVprmzKVJ zmZ1Phstb5|L;$e~W|I_k^bkPkg%g}jE&8m&Mx)z%!n#iQt^?$;7az*N^n^1zcI+UD zvJNp06!#xLZX_x$+$_{Hu6^;R@!5)kj~ZuYExbDyg`eYwBTyz$5|(g%0`El-40z@% zHkfZ=`Mn4U8xasR017-q(Fzhg8o3}yYieIoWX{D1LFQAsl#Xig`>lqqIgy1mxS zO)Q3p!_L0YhTtX`;Nh>j@Hy;4JCrzdp)o%_l>`XY3_9^7N0@Vz<0JFC+V|dTArT56 z6%1Wj1EX$A|7+mnDIeILs1LI3)yF#8OI-4`v!t1;x!LV?>Ncy52>Zg5ZGKd9htNM4 zI5*ykkNf}0S>Qwl?zp(zU%qO)}Kf5@-LpjgwIDPcc+`a1h&b>c;7F}KDi)1^LU;EMOzYpc3>7ErEX)rq; zcaybj4})c*{)Dpa03)@wWOO7Wt9G2aWxmk z?Y1!cxQVmoLw&kup#IzUEQSu*PbaGsgqEbAWD4Qax~xNcmTlhC_{;kLcrxoJDcRH< zCDGdsaaT z1BOp@%1W|Fvol}>##wtzL#^zZ2|b$uNvC!c@h);j%LFd`T-;r~m;>1zx_q>mjfJ3O zmgS!Sko)lGba9XBuD4K6;jltlbLR*OpiS|#HI@LH@DWdwM~}Ye+58F}&<))v;hv$gnq@G$@8g8!lp>$z(texK(g3`IPK$M?g2qZ122F1`pdqKrWEE^&`IP z=%`7ki@YBQ<;PlOe*A70kxQd0y7^~C-s{o`LvM^Vsp7YF!Yr44a83$e()p{=(R;PE zgE!W_ibb_G5!B*=`1xDn=dB;aF7+PWS5uSJHb)zp>hRijnJIPzU{QQ(DkF06_1YrC z;mx9*+-ryhAsj2oCsq-hF64<_WUk7w%|l@Ha3@V*@AKk;W^M1-6=#v3w5P@Dyvnj= zs;MuO`(p?y| zcJ>Y`Janx*TIF>O4KJ#&nM0Xo2gNSw(%izQY`ehs%#|Mi0YNzMOw{9yguez_Xl`2$_M(=qhAX z6~C3&Bgq7DJ)BHTOjx&7dH4VP13M5&mj5(R%xLn4=l4x6xJ^Jw?ezL#h1ej2{-!zA zHAJqF+?IKNeEvzi5wvJ5sEITX7gGm&1O+pm-$pcFjrkSN2_RBl*U-)m>pjsWK^dVP za9Wnvtz-X&AqXGPWWnbD7NjA72SQhzM=a5u_HaxoBY5ri?%E{vh|Yb33>hc*AQBSY zwo^+u41amqpPZWd&qfEc;5jp%BoVN6wZJGy&QnF|=bq9p*LLM-j6c981=sU=oGOS$ z(RNdF6vHZh?B&FGXyd+DGQPEljYM3P|dQ0is!%_JD%L!|SCxY{~r zD2GiP;8zB7_p!j56;vBR&jS9g^6s>wS=hiKI)f<^T!=S#heB-R6fgh$o+IxSPX3P5 z^}OY1(jiry&^OCMD;kfi7m_1L7=}(s=*1?YU#VlSCFXcC#5wYh>=wi&{Y4aEkR!cb z9-39XhW(d9jXe`D;6*)S82&%vfTyIFioum3N#+QD`O-O`3~dKK zo;dLLg7PV^Ufn?uGgJfhoJNRY41^*G-%`3W$b2V4*&ixIycLp`u?CsO(BlmuBB_BC zDjDBIO$&*sKM(2wp-%~EZDHzu_+b4nXZsh%Vpv4!O@A%W6Il^pPZC)4(3UGZ6PE?p z0|7ZeF?Koc5Q|2bBBIe5AVLnSv2k<0j#Hg@0Ex7Jb!mXLqKI~R+YSTX(?Y~sg^#0? z_~!a>!#?PCa6Xee+_CgiU@tI>m&lre1?oQEKKCRZ0)YIe zaUj=31TJ*|)g=WcGvcXD$h$^>5;QC4JZhH1;X& zS_P&*s|Km%z^d;XnMDNJ8F=gysbIgku~7`J6ROk+k#m29T} zUkjjHkHEU+JGpwc>#TXzwI+>|Kk?c{R96c-`9Ettbh(L#dHVZuii!V!k2+0vEu6X} zdNs-F61S_kaH9TxzJspD4@}QpKCZ227TX;!kdz|>>E(^{T)yEcrRLS*o4GK`@kfd- z`0=g<$%^b75h-U?ip}oGa!EHS<9+@i?Z$hG#j@&9nB3r>VARb8UiY#`BQ^npnL-Y!-^YHO$ntu2@@QQzpUsiUiyLCD zJ0RSU^P4HDW&Yw6N4}--*@W}s>pWWNt`tu5v)D9s^(qz1$+au^|7TM8{lih~H^$m2 zL(HN%L)(iFtVwgXc5fP6cbMn$hDR~%iIbUgV_Q-#J)+w@xyh&c2DK+| z&0g20q7gB#eJ+DEo4nzvTz|hoY$6x!rV?kxAdw_n$z<$hkWb&R^B)ET(x1`S|w<$tfM{Q z>h4@xFxq8po2l_p(3aP{^;%(HzoQZTP31BfVTZ9?t1DD}8dbK#zOR2paFFkKT1vaG zE!c3|9&<5wyEU6Fv;D#!M!y`?bUnDR#>AgV<7#ndEwsQlOVL;D`DxYGdo>D=miIK) zKbKDW>?ix?2rk6P)jFF!dFJ^qZ8W>-UEPFJf{xhEudm|s%4|E&TXi5HcH{S^@n{vT z`yy{e(!yU%O9yaiLW=cvuO3SPYqrOWRwuu=eJ_MNxz7K-94j# zL8oS>n9=37sp|`!9KVg;(XUNe;#;p*=6$9vbHZhCueQ2>9Xy0iQ>Ocoa=|&o_2C9ZIGJU ztYPqVLA07PsYF1^m_MemdfkPswG0BG48QjV`cvwq-NXZ@ms3;{qdJWA^?)cxO1&!; z5tv7s4v^v^cQl0;5_I4>P-`p0JhoIhqO65|586pbAqfdMC@^v`lgg8{n>VT7%^=8w zXbqn}?at?l@BR-NE~MKGPfV=W04X4~&5Cl)ZN#r=1FsHQO?M=mK(Z+Wp+HIAooy}p zwr_JN>28A&YXiV%APp=OsyJ~AIq^!_J5?s0Yd;`nc6l}OY`umUp0N#c_U z9J5y)4{MXe?gF9fL$;;PH&uk?Wu-IdkSk%@>c49UVb1PB}O@JY~$TMY!QG zn6m~51AriLwU?(J6vt$R$TdxcfLv5Vlhk4y=5c0cXYcg|-J+M4moIJG4~GIt`NaCU zaPi3Lb&-pYAdJS^TcYD+ttSXro*a*8*a!kC&~kwR<4+x-H=zp%T*<7(b-YLW*94hcedSAemV%x1V?wKlR? z8=`qmZ#|fSKP6@?+e(9L2rTE1J`M>JZaHoenan z;0ZYE)^HHZ>xq)VY%vaqF=h` zaHjAE3*IEcJ3aTABTQ;4`ydpozd(~mVXvTKxRrhnRpn$P=2ox&Kkc>acoN z9{22Am@F9PAamz@K*(2fs@)gEY;NeM%&({YV>+6w(T^T`v* zulFx)wAH2CSVup9^$3<_tfCP2Z1ab$0+?-<27UGvpc~8wJtarL8zo$!=Q+(?hde!C zg|mnyA(_XXyIIYwLyVED2lTC;uzKVM1-4}NsPs^;C>uwi4F^wF)aFpLb5(^`2Q_#4 zed$|MI!FIx!=;-Ji3@rOyOVvX^%@U0o_Kt=$fEM)xI=}M?dewI8oOVSWuYl&zsRPH zCAb~z`RqyEw@GCA)z|t>h2P_4PvzZ_I>of*4d>0z7EZT%s*9&Lf473ue@bvsZE|;x z>zC8DZiT_z%n1T&-7Fcm&M28Lygufk@|{}BPjP1v)v#Xa*X37C+a|4wpZ%(Tv43pC z>vvWPj3vAw6Wc?kr(VR_|HAu^3GoURNd#_0WdB~~Q*9d|WGU>Jw8cFvH(Nh$4YhW> za^3zHp1yd@$KALg#{bqReR^U-+ulC=rrcvw$#~cXt4>VoxP+a>nQ$bgU30fzRanZg z_eX!Jlz&my9`qArbzN#%h*+31Ffdi40 z;{m^Wkfjt41Lzy@=c-w68E=mR<_83hv_tPlr#3g67X?!cHQtZ`G13g2vL&u(!!b0&WN`;T?d zNg_K4*c8}Lv8r;5eBhjSwcNj?13iEkO@CJ%1%YHBW>nOgxUZMykCp@RQM!cwZ%0gTd*>W7J8)bWrHhgZ2hGivHL8 z8UL;mm30${fDM@gY&5LP`<$I09SsP1LBmc#OgV^U*L#bOg(f(I{&A!5RiN_?Hr!gQ zF628wN_mM*5F|=aH4M7HAX7oNP_VZ5GADJ%(aeuo)#!Ic$8+dk7XTP*% z84DCVSwTvtIf*dAriRLD0YRTXXC$v&SS@c$zaB=zAflodND)M1?5X9-MWb`lAvL~1 zrJ0-ifTq=Bjf)rRDI1P+4egSYyb}EVzaAROk=nb+cce*R<9IUN($cfCL|^`D!pJK_ z-9VShCu3AK&ZDC8q4V#F{DK!JKgk_q#lz|f+P8Qf9W7WN8Bz0EPfJ;b<7m4-3J{#6M z6%GRf0)l2=QnLFTd_bmh8a{bwRWd3&&Ilmj=PQQBMS^Xt;_?+b}=XyMc%tAoLjXH(9z28yYcS zzs=AX(Dt3!0j47y@BQJMd!H3$T)!Rygh&WSeF*ua%^}Kp6H)vSA?cGgges9r*BOXs zU^Qx?09XIK8N4g9TxD2$DZi$>FX zvi8SAdwY9!?`z_Z6hD;yS=+wh zGR`?bMNU=D6t@8R@_V1Vt?E zlgFz6!$6K5vP38UQ%6o@p|#$|`*<6x^wPHdalRoR2^nOAFvQP)`c~2~=l9|W4a5|{Vp#a)BG+FvDiN%> zFdj^1Z)xer|D_jvoauIy%R)=nhi>yf9kBS#grU3^5@DwZdpkQ)B0Ld)kL#d)*Ag{{U3RPrzj0A?csvs!*1Fo4Eu}$7(eV;*g#Ydf=Bfc05~>Ktq~j&GUQna zwR<$a{6FQxmAHZN0jd7}sYBDDn*V)hd%N-cM5h6p<(#`Heh9!`9+zDVT5Oyjak)^9 z@%t8Ma_JF6eSHok-)3w}WD>nV7>`_`so{f)!-vYx_2F#bj1zbP=_5eEcuFLm1r`ur~;e?4n9m z4LUZx1T5sf9?1*a7G?#w(xLy^35!7cN|w_9kXwS zjPR%b{%K?ecXky6DN}mk&0Ol4VuJNG*f4~{T;)~SjjfbLLmriV#F&cmxh=ARE{0a? zw7WM2>3V8HhfOllVp2PkjO`0rPF)bL$?0TJaCaCR|9#t0h0Wyg-GKTN*ONCosIs?) z7qmU)clq7TA|;g>|NV4U$Jy<1%U7n%8&0Iv%k6uf`rHbVt{u;(8$f@@%9P1;I@qw& zmoDa>%e$WWZdQKw-ATbC-2rM^uTH(U^yV}&c{&h zkr>?M!-%)l={L3tPpweZ&LF9I3P*m$(r=Xe?6-Zb@yyLqo0nkDU5*occ5h9L`JC`D z=Q5RRVbZ?gNvaCXOGouZ8a9#Y11q9g!6?( z=;yD2)o1mxUV4f7>U%2Ym(*RS>R>44K0Fq>>%-e)nstxFUl15aLC{1EsdHD$Ba zI-9A%8%AE{o7?tqSsBIc@3)a{=HN9e-tQ1!{oA6qxAD!!`>7!=w@kH7EI3`*yov}I`-zy%LmdASAaicef z#c-?D9UfNCh`h$e6hVW2r(cgf-S5x^O!l$X7#TBAGMB**nVvfof7ptoxIG z8xUHOsi5JInHXtI~))9!~gx4>-0wf8VYDZ&y`!o@k}^rLb5<%tD)a=4pRb_ z+eFoFaH{Xg0_6 zUOw{}HSJM#7l;@L_$!FG?*bPaxB(#Y07IK_k*lj5{}sgYd==o?lNkmOMo;-JP9Dpc z*rxN6N@jrgmJc0T`edoYlpgCsT@`YsZRV(Ox<-Hyw)guFA3AYTA(jw)wAM&!OAX)_ zB+8gqUsijYuEq@24S99&|3ERpuztPSb1x`Ay&+A&c$zcO{7ql@y&yZ7X*jsF;Y-00 z_452Jhd^`8k3iTtkUL1gmw~Tv1k(^v0G`L*i%}XRa~Acr#HS9MEj&J%(8odXJ@zg) zdid#YG-eQ^IphyTVG&;wWASBBY6?qB8wqFhT*LTE`aTdc2=L=ht3oLrW>993$11>Z z%Wlqu07!v6r#^*|Y!+}9mp3kSefS^@ZB5mhOd74QsS}%>8$ybo!|l-A+KL{h+>KE{ z=rL&ARc5BSqp{kBo-fioKo!AObX6Ha3gA|@w!7fUfGZLe7uT8nO}tmqC!Xt0 zr9Kv~y@zfB=twbO^f-gTLQqgFgphSkrDbRG%>#_Egw%*=$2QUrny9{lKX(WX%G{x%BAAl!DoE>@wAE9J z@?OKJu!7kE-S$5~JKoPqm5Tp`;#kS&XUu8_8W*VsBl4)*lv*=?tqbkD|JX|EIUS?m@$nMN)oVIs(=XA?r zbJx$FGLG_VR;hUr54nbTWG+v7P5Q=v5aU(ivmWLYd$2U?X%%<+`gtuXy6EqX z2{w;9^%*y*Y!X}_k+nO%o4RuYmCg_Q(Hp#$p}5pzFJ~e%I~_!2xjsGw{?-J=Cvnk= zGLx$gw-R1<>PJ_9@Lno=;V#x_tv$*(xwF*!@{=yOD)pD^=hDKS3Q|*+X>@jb=_hXA zf48B!IQ0Nkkiu3!@N48b3SD-0ZFjlaj@q-7a0ucIKphw}fa`rYB`l9h2+x$LaQ*#D zO32XJL{XyaOJlW1vI3FYmRFNs_^%8@`?MeP>B*5fl$Kfk_4BsV+ODTIug-v^jSV#*W+ACFKwl2aNTeO=HR!R9Z? z$4_@P7m2lSsjB{L7JcIJ0B%K~8uj=rQF-p!jX(d4&ktnt9FC#TEF1n_#D%iMgS(`y zmxF+&un=_QTPo_AH2zPVAPOLxt+^Y!4jPb%L?0LrMTsi+17LR$R+GeXo}bzI1HBvP z;o2nf!gl16@J#FeKeFCCp3C=dAGb5gEXoR*Wkiyd?3qXsS%pd>scaIdNOma=nITDJ z&&W*67G*btjLHgqkMn)s_v81+@AG*7(Wl|{x~}WI&hz;^p2s+c6P1j!QNKUoeR6sJ zBOPIE5J?c8w4eF=*Zi0_aM}4#;{9i$Z##RTt_L37fPBp;UoyJq+rICj2bhd`dPGBe z_u3Ig-AClY0}A&=j<)#3EN*LY6xcdzlfFzq-dG7BENP+(a0ifm?UGO-`_# z^Vfty-z-Eh!@xhTv^n4E+D>+NaM}sE;k!3l3sQ0X_N$sS0ACgghV){OHR+e=%*c2F zDu+zxZ%R;t!OrwJew16n6BY{M-hq@b4)U&mL3epy)MT$?FZT6OVl)HacSgjZz^fVF z6(qip@K*yHoWj2OhK7bFgVzhyE0wX8o`$I@D;eoP8(=Q$(m>2cQM|z6I!D>e%|zh* z1oF#k0jXOn);|yYfzuecFX@|0&kctRo+$CPPl(UoJL=*VPfi{{q!~1qwVSw@ zDADi&HY9Ob=HjX=Gq2#?EFTgQ0$*y0gThWXR{j6{=%N}qUj`W+vCBppmi1g%+$tLK zN)UVL85!p)KH-pafO#raWLTRs+^0fnk0tBr;dR7eOw!jc8-=<8+vq%V`;&}lcKRcX zDuQZV707XoHubLD3C41-DZ>^@>t`)asZo0Y6x0DgAk;!wwW2TmThFdpVhoc0*JVH@9PyRGt1;#MHnj=71VVap4s5Bo@ue7~rgVe&ov42(_}tUJfR%}l;=J#t zx#p?N_wWi-%_Jp4eIWns?tsuX!!0J>sT_t!_i~ywzJF@GWrt0}%va0g-Re=Il%9ec z!Qe!NA%3GJ9<@W;Rn zqF<;6i#&qin{mv?{0l{{rS*CdV=59c=J#G^n5t<165hj4-#fD{I4h^GT2Mmt<6l*! z6Ep>b8DsIKs%7QRd$Pi}h<$PDI@q{#%MsTHx)No^-Lw{Gy>5BasK4Ey8r*E!o!z{( zP-&R@g9Deo@_T#TettT2y?)=qJz0YOFGVN~^}{S*3|&+f`TT_emJ65FZO=Z}Q+81= z^jPcVEB}1+vI`}v*^yWi(m)ol)We>j?7wmzZn?@;bw!aLQM@)Zbc>lp4{3ho$ zA3R9j7I{41@$RR;H`3Iat4{d`dKYCTYG0#!ota|Gs>bPXe6-c5IP<{ILapB{E{>kz zRj->Bg|y$rH^Mm%8snny$I*PhCZjB#ysQ7vKYVUcldG$e3C&H`YIVNw*coP*<;!>NVw9iXQ(#d>iC~wG26QP{-)W=xdq$42Q!qKlZsu`9BF8G{N1t79Uf9!oiI5PxfsKgGkdW54mf zrJT<~qv5XNW6L>vpKfzvwmIjwqVmol%aA03pzWV&>0?=OjqZ$a_5rnUea*0nxb@* zQc-4aUUYlwXz1ZP@~JgX)rewY{Vf0NkK%6$FR zDKTEEk&E?yJByDzztFM8k4r&fSI9#$A zS#)9Ef+LI0jsJRHkwEbV*9jA`NDS+;IF2@DJUoik}js%xIdI3~9P+J+I#QIZf z(CbJy+q~3#+Oi0C*{XE(H(!`k2#ASYzof@`uwe5G1s}k>;8A4;lFpBjK7;wtMyc2e8+VE3<_dtn|_jOoK{O1BCvsq=|zm*qbo(`D1 z*81B?fky^PH%f-Y?Q)fnLe#D-Lz$yWS0hsY{OQv-c{(>orN7ivof{@WysGoJ5f+L5 zQ2ccHgJTn3!*#Tf1bkQ$`IbHIRY8;#KhAX{!Wwk@=xb}L?+IjrZxgcA7TY)nQaqeM z@_zIYkF$h?;`5P5*ExM=i`H|f(La!dQVlgE%qup=oX^|5;Zd8jE1Roa%$wdLBs2*; zWi1^lcNyY27H^^!POQ^lUs>P=o!~4o?1nLDN>NG2B!uE@2@lFM-E-%jyKW)oWj8kM zppkvpM+J^bC@2wt<~Kgvk5b?U#4!p2C%(dK4Bapp3$#`Fdt)jb98SAb0`eUpwV-7k zuObE_$fl9?pLe|%V7DGRm!N-_ByGz#l9+5p#yZrvBx&?Al(^jT^pFlV>Tg470n!SY zl|4u1HUJHfe*RCmEQ{(2xIzg%i&kYp1S)QquI=bnqt$+;T<6z>xjM0b)!=K07{(`u zLd)4QE&PR#_}HKXR>w^GG_|y@*#CiJA@TzGFy@4@*hUb`TPDDf$Msz&vuTuFcRs7dOGP^gB|@D3=HvyY zsl7}8P=Awl0OuO+GuU~@5sI-0IztpnDyC;A+CF@^OduWmz(1P+P0@()^U?lChw&Mv zeFGIG<_iU9e4tmg!*8rCjz&YUa;Gd|E%;3igt~R(7m0FVsMP5AmEL+P8tYNj9k!Cj{U?3%?@u<5ze_TN2*h`B4 zMrq`22j8H?(^Huj$210I*84?ehk@&MUJKiQ6CDc@WuEkP{dFnf5~nn1w!r%X!iMV; zjVrb4j)!Tfz^dVrwn?_ygY9L5ui(313@d_J7~o(u3wOEk1*ci|t%t&>9YFoTu7pLY z2}8xQGAp@`b>kc(<3^K-z02%ojeU~&f+}HeH!BsW`u!GPjrt@el66~Cpm1GTJ2!3# zh#s&<${6uXXKCPm0=f{KhxX6*%E#zEsnnZu zy?PIH@J?!WRgJuC9kch6ZfU8XKgWKeq~GJ~n=RndZTC-?TZ$RP#Npxg0=IYDMUc&I zYGWfAljo{-z950t@askAx>E}GWiNc)FDxqRw|_&Ig+RO~c~Q~#B1CGi`|#Wrx+^4d z+?=OAggz01avUy&AEGW!^=+D+oxMSIwbwPA7F{JfMr^co_h{e$0ytQBtf%#WfIW>r zcs4L5+>}@5Gb@CcH|o@Efq&RHjEpEhUZGsTD8{mzDd1nKey^J?hGpl&!^T$FpYThu?->MTr& z2(v@wB=~waA|ehRI<##OZGjMwQ`D<;cevc&UNB4_a7sk9@`j zVwxx7{|I!pt4v&`*48@~2b|@hkf22ni5U}%P5aD4r3=@(9~SBXS!n~b`B5&|=&|O2 zEBl!Gm>xW))-#j|sO-c|ofTh?5TX8F!3}U_%!ZAHVI);FOfI{4jvtI+!~s zhsL9?U4%!}4baUR@@`qWg_E|0Fb7yzG9}#T!%m8TP6($RW}NijDG-HH55gB=o9h_& zVFxCj5CB^{ra@#O<_a2#W=_B0yHSUkt>of>b%EY9I3>S=eQG$GPzk#lh|dC$@e)_} z&Kf8z&SM7%D5T`5o+HL^V^YwXZ(ewUxi8ujB;pVX9iUI?Gb)x=R^gr#-NR%OoU^}u zE9R<=dlx=kv8(dKm<5fc5kCX6LwXirI+}GP4|a-ZwRU1TVtpM>&Ohd{w&F{mV?r)K zp5lDsh)zk>YsMgtx4V0e0YL&!;&A0p5<&+ECqVkT+d@nX>nBhb5&C*-@(+%^eJeB6 z3T}NHV`zK0E}9EHf_Eh0SP;c#!DH-d1Q{ZEd$8GGUL|R^6+g?3VoWx(&fA0igRFx{ z5GlhiAs#MR(_O!Z||TL6|Q~__%{r>EywUM zJJ${vj+>jBUMBYtXOwva7Z7Ebgpf$(&Uy{rBW8I$j<`8Kpy3Hu`Bmv$7YP90UR9MB zyX{ocWUTU^@s8`=`Fnc}eXbBf1`&mPKWpM5hbC=3uOcf(BE;T?$m+ldA$kv@Kf!Q5 zHLJ=H3@WT?;+p`9CN_u>>XML<_B)pw(7OzGmrWcdd>g)0U+pO zaPxg(BpS)w;4Hy<3`R8{9F9!6l?A~$>c871D!M&t_|ocf3S3XgH~7%onK1A|MAS6cYzuEfBN^3l?LrD*F?N79rylxLbt? zwOp+DY))N@R|K}AHLu;=3b~ZP{EH-M&u}1Bx3s)FDG`py2V5NVPb7;AUdN7r2?!_& zj_dwt^$dFag%AKNxP$OOqolyWMr@ej>6-Ihzq`WQoot~nvZkUS88mIFIxw9zznFEd z;xq6nl4e0*I0#!X(~z_&Wd*zQma4(WQOr_MfDt^W^X9R&l_lZ_1Uk&d{$1b>*p^?c zg^l0p4|~i!AdPasj|J21&IZo6zf^%~P(#)OJ=O?-N=!0Hv@%(o;69W;(?4y7(ushZ zkS%?h-nKCBxi(H~`EC8&Y4@fw8mC}ECDZ;NmbF`fD)B%pbE6(kBm&~%^ngWEBO<|P z0iKMlLh^jy@ zl-c8sm`WMkW^p6vE3E_Jlv4@=WGG|DXZx_!z#)UV+TmG-+hoXvy3()H4<#|F!th1Z zYQ*wqBW~W*29O7^S-33i1mSsrf9z+BdKZJ8OnoO1K!I1KN8d7%4;bsP+jD&_6}!#m zoyXLfHdKBpst<423cQMt)jGw`e-hDqgioQ9eC%*0ZST!^FhEE$ytydb^Yh^=`=CaJ zqsL>>t%1wGH4F`J2z{iMj=h7$hMD4rX)rN{VkarFMmCG?zpMTan$iZa>VVN%KW&@T z%j-$Q2ZL}RCgO=l`u%gop3D2XeH4l|FX57YPxM4rLuyTwJ_P2<-wVAU*wC9UB;??( z5JfErs!iXp0!R^y1Guw(jG(r-P}?Kv&HP|rfs~^g&CkawRxsZZV}99ovfqEmm}-8d zI*m`tCr#&sw-c((xb(--`cW3=W!jEq&nn)2ab}(q$Z?s!`~i>=wI_)y&j;(FRx{9TB$I@14u8A z2F7F0?Lbx1pZogpExs3g#&pFZv)c-cv(+Xm3y$XA;zbdT7Vp8K#Hd<=K3r_)k@AQ)yVTrm({d;T`ogo}e_x3-C= zz%N}ld-0T4>}oF;HcGnDre^#IV`7o@TPV&2nDa78dxJhS>BgS-*-|!Vol~7GHRC>o zCadr6(xC(!*6wb=pOCd5%H1Ls`gUu~2R@fVlk_TvSfKLgf}c!Th8>iz5U1cX%K6Du zIhBSTvSwFnO!dKz;?MwZp&5aCb=B^w#8F|_xRPTG`(#rD0*%j~ivQoTMm2P8iP8}59|zdP<>Vy5lmea=kQg6;cUras=%RPs3_YBbap2b4l* z)I7WDdZ-)4(XYR%;)ad4h8tAw8;!9oytN!=``c<)WU=O%o8jte9e0Y4f7^UtN^h#F zx<3D~&;i3K4W}J#2MWF($Q8NM{9^TXe|lio<;sZO3Ck%Jy6vn1*SlvtN9MGoh8-O< zeba7;R%n`N8~wBk5**m%|3pi#O*%p&YGdVYs`;h?T8hYDnZ@hbqd!b}f#93NZ#1^$ z>B9rokKdii110FvnTGJwSMGJXs5hyNf2S94w3DWeVy8MT+VG4msd-!W)*ZiEW>1_+ zbcubNZ1eO`5$m_}7CH3CsZZtX_7s}>E~Cs~FIE{+Li5JDZXG@K&*H9;{WP(C29q+< z3;ygH83`1$PcFGc+grwoMloe^KFuBK(w44l3yFKadyUsE-?8P+vS(b)%aATnU;nLV z+`qc>)sG!KYh(Cv!H?rmk9C>)>t72Oukl*@_gg&W*n;ke?!45bYWq}Z&&)}U*{^=t zfA!T2b}tlObBifyimtBX8RiV5jr->NO9`j`1nk+&z!i2cNqv5M#z1Qyvo; zqv)1iGR-NzF7rTLPq^0xcW;y6Zcd*w?!zWq#_o1;P+K8DVK6C$|dq|V7|5B z^vgabJL8$gVDT8cg7aJ0EHApu1*~gh^lfi6EXsd1Z2Co9=QW%Ay~^86lE;5v%u~Ex zhTq)l{_#xn>{D)F3wdrK880876|SB8=x0zwEm%09cyBRP=a$yD*t25h!>CUhpV?M8 z6|Dm+0__kET4Lt)w#f?G8B`_NhMA;30lxt@-!v~XI!!R5OKLKJk`R)W8Q4lLS0xhz zdO%NSV}I&BVa6~Pt@ib%PiaX|0VYUUUqe`7(ToE}5PHcOI9$<$&O8L01ioppFOzgu zJSskennMDkP{s-CJB~4-3=hT!mN?r>tbo?hTk5ebw+U8hQQ+KK9DII(cOB|`{JJ|& zlAUi8Agf&k1qJ3O9F$P0lAp13zkTzq@_BqWi^Da9@c@M}aI7PqRop)JcJcASft-k` z;k&Xo?jC3aqmX!nfZ9qy%mGm^&RAZ<7ESOKd<*JQ?fHSAe}NCw0{a|!6y)WHFx`3_ zv=#0%xB@mw&ImoT7XY_NL{nek6ih}iocrCIiP97XPECM7sD|1%lgK4nt9xUFGm24$ zf8{$4pQI*4YbgvfSROmp>3uv71oD1nk0uK4bysl3EBd3kBG}3w{$uz zLSW}9ich~xNsV+7fzvdTz{xeAFcE&|@D=i+xt;|| zGwL4n_kL;uiM{A8cC%LJEI`Xx2{SlouyELwj1i3~Ng2SP4g{uz#yp`KU)Y=<1SkNq zH|TOg+qP-VDUV1jVxl^Xc~GGpM%#pZj9L?(X8NPjH@ts9mP_z4c(r(be0Zsk8PM1IUNR3@ZG~hebCFsv*kWmCKwIXkn7ms*NRV{2`J02Tjc@fuE z*ZnPolntfUI%eKvnsnHzny4)aWAcyI_CzcC&R0aOgzMoxOyD!JMo>Owi4TG&dlL>b zj6d?Bga@d34A4};h39}%3A$yxNB75H;EWjK*yUG)Swt?zxa}Kef6zCP=+RfW%oEep zm!s8|=|Q5p(WBpGWvBpjk$BEFAPEx(t?r@p{DF6nOc3A>sVczF)F0L00bu+%gq!*J z`00Aoj`CM+1YYVuFsA*QI1r=gw8(ue0#;H3Rs-h6QuIy33@CaD85o@V-k()Jn<;v$ z{P&3Lefl}%o_`5ADH>Pkd~MHKKX$+UXKSuXMWf)iEzazS=2@i~;YDrKi{5xi%S;b^jZO8o(9>&Tam@w;6_-PEHv zdtGIdlINAPCnZ2-!?FP9ALlK=wDdqNRjs0?fTnv%UQj^~{JZPU3#BJHalp z_&vEz@6;G~kTI>k>TV^ejs1C{GfOV+oZ>bN!uwbSIoP2!BW`tx$pQK`Msu{SEj=fA^REaT=g749(6YtAjV>48zF z>jgRZoDrk*VP*Sf^v z&WEvqO{LF4*0N8?hK4&0I)&KC>UY0Xf$?ia$1)JZz?hii(qL zbv0^K5j#gm=RVlVa>>{M$JBWl>fr1w4x0PMt5+KwG%j7L9)g+1bs%}9b)SdSjd1H> z$IZm~1wA?F_e|~L`6~WGpU4ygyvN|n;8pOzefUHxOjuypf*=hJ=R)xE@p~wKH4J#e zjT@rELFEA*(#%@Gt4CTi?sDNThQI${&O^%R-~QQN&j6lbGI~FAb)?kDS9x?~#3>K+ z82CJb&6zq1^Cuy6!zk5Lc(|0{pg`Yeg5QFBIe6AL_)8#Ngz{lyF9X&n0b@ZelYPs1 zdQctRO)>!3Fr*~xJwN8lk1uHc%qFs42Db$~f*YalhtCNh0H*k$b=(*(kPO>nHfZht zfQHaHFg$JDhx4!Ws()$EgFFNyjy6E7M<4sF^t&n`-v`7rv$E#6s;HRi{P-Eq)We8m z+X-R8e}WB+%4r4+pC0olvmpd&gf4A>M+ptiUrd)v(fG|mPeIxj^H#B>oA_-qDpy+? z<1laenAdFSCqw}vrx&_3`5$&8*Gjsm{+1Q9X2e5AzXc3{4 zgm+*qkq5juWdgvz_E{2rBk<=DxE$6KDJiyA5bMCCKMrq^#k_yZGFH2ov0#7*jbvvg zrHax5Q72v##AFN2%_F;3>78SPaJN17x!e=>18mOcTBgmo>6PN*fZ_c8S;0brocA^+ zlzS0|DJ~S$oe~9+K+JZ0fK++(yOp%qjbcI%11^#fsOT|514O3o)hiE(MTg0UM%>{w zlNHnWKP~{-?@--PVK#%&79M>q!N2zXTY670Z&z!6RUcLW+zmLiHFt-CWkOP|c3xWX zv6=%?LF`T|G0efJZ9)0_9wxNAXhMnUC>TvTK0c*mo;Yer&`$R=+Z{Wfty6x+*~4=Y z$T(qikcTQHDY@TERbF161c(z$7MzNMC*3%J`M{Tw;9c_5Pdg#Mi?o=%$sI-|9Jx2K zWZyhg;Q~7>N4?=$$2gqmfTkYbxfz|Rtlw{E;#J9Bh#f@r_Xn*TPAsL`l0@@L9DrYD z80_<%bHbH1r429Rq=)v(#54|B#NiMHk<{pmrwp2}ksa<4UQOcAE{nMlrXx#Ty0D{r zaq?{)fZV;;PPD+(f-qnEVJ5~+01{*hWvOp#$_MCLoQGHdU|w*|G2oK{Lh6B2?@)o> zk2*#xGm=S3z)Zh%^1F`M#6lF{z(TkWR5HKoBG(A^;ki$eZMSuSCzRA1bb{oVuW@cmz zRkV{P;z<^N_9ELem)?>>bX&vL|K6~~I$LjHGV6{wo9@$Q=M#LyVJZ|q$`* zW}lg%R?CV6$e0vKL=_Hy@sleAP9UHd%mWYUF{t1D_3&Z8p@P5DJIJ_>w5cW=-H|3q z?9e-%-oh6?N%=4|k0W{CmBQJv6o@98Me3jJ=9a444}VhT2#S0sfnPQ&JmFCkT^ zZWeJ4XAICS5HgPTcC%LSS|sl%n#Mi3&{esZVu#~3muKn4PLoQ;pq*u;;DjpNVx)L;4~98=sb&HQCiBKrr@Qw5nG^mfGa z?vDQ!UhDTe?rH;qLSfgj0DGEr zGpnR#{uw>_pxpwV+m2p|5_r=vpD@sw*z3yXw)y=ps>tw-&AWv!OC_#L`pUaSOPlE_ z$TscC1%U@{{<@9#98HGuEm6@JQ=YZP_(M%!D7N@Ju#3@ZmWI7ZyruQ%K^X(3M8r+K z47rc9ZYBNMzby4M#fB%eE3V1pmj|?{=vVHeOc~CLYuY4e6za6}qwDKbV5m>N%2Bg* zLeJBa^rF&QW_RS9{2q%nSQDAn*S{|_HEDErosi{6Z{g{uzpGznsQvazna*ko7;t&p zTr|SUo-_bzxr^j`?ygq8`}s2>ZV9EX9(;Ra_sgl(Tj@X3o$DE>J$th2ax<+LHAR-m zl&*rZu>9@5^ZgZjp79c7;2C|44*iUjMXU@HC69kNZ-nhLolx7wdc7x;?q&t?+*`8%8|83WmOgs_IY;82;B`h~&^oTu%_Fcd6I>FVa9HE&zcc$~EM$TqNHvH=1 z&^Y|}>48F)tcU$h>z6q+AE(9bvHxHIkVfb7-h01wcF;HH{QE)^Y^gP}BY;hQj9;@- zmG9Nkq2jwUz6BAp=1C=ggwrh~nA0-`92EAi9gW$Wy$5a$#*$BL`%!Ack+z%jbWv!QR5Ybk zYTv!S_4zyJbt=9w_3me_jyq$oZ*M)Hlh6}5kG2C243xzUNqDr>nf-2 zs(MK~l8!+t!-LtJ4qihXK=bPq0 zNQNzisimd7op~E$`waxv}hhhoCxsvmdw+LSsB9LgWY*bmq z)$nLgt0Yp^f_6loy;juzA2KBwMBgW@j(~w9FRY8euDHoSN;iT2q?`=sER4w#4@TKT0NkOl914F za!MCCkhB>95c*RxY>K&aXB`DvGF=S$!JOUb7@_p=0GYPfUs@7*%Z({BIK|g7_}Foc zTM@p1gph$B-Vw76DsOUL`K{XDlvA2+KmhkJMrJzT9e15y+#UeY5xRI;=MMrpk)1fu zW|1sFQu08}!BactTz+|L%f%=zY3Oh|A? z2+w#^9tN>8DA^(i``Y1YxBu=K%sg*3DQOjSRH6=rMl2)Z7>HVc-8aOyq$?(@eG6J! z2TBU@g2A0fM*z|L2aJ4Bd3^kP>EEh120?UScBdV!0+#^-ehiCsiX#Cv5SbCF*6=^t z@FV@jW948xUBs*egF5uV1q)jh&r!f;j~kDyqmSik6mfdK=dCQlC5JbJAB%jt~NbwYuFlcM`z^$oe~@^Wqx z2NA`mf?-^5YNT>VF&KVP%=&NtmrmnvyaX;l#w)b|&JAAe$BbVHnT&QaCq^$qO#MLB z*X-QnMC-fgn1N>H$OR@Ih$V#FEN@M( zv@Fylf=`CXOA&|}+0l+4#-oTyOqB3pDbA%BLfxu(X+?2$04O)8%>a7jd$Tu>Edk5Z z!>*F*)yu=viUR58W?9YIK5*F{D0?#UHr1Xj>5!8yxheREPc5KyayLGMKv*E`jpvvB zkEEYuDk$7PF_8=MHl7{qEnXpmtYNpP=ruZtgpTL1tKX)g!ULqM@EHZ9EMp!~kM{a= z;>XB)7c4%k(m`w}MtoXVwj25OpF+Mmwp96>>m$^q-Z&`r+I0(q&WMr^@fe3aM*G3I zN8XUx+cVbe2{SY2KEVc1zB=37+TzF&rsnKDZqn6J-((tY^X@vi9_5i!^bH7WktXC( zZpHRb+JgDHW8D#%W%ac98CyD=@X+Wn+f63x=Ys52PgKeBg8DjTTVoDR5w$V?*%X%m$;3D)7LSWIElvCewtwW< z;qIsBL}pHT_upgM5&l`U=)x}U$T-R{natVhJf}9EY5B5=-t$&*S`Xx#53>H{F`8G6 zRdOFR4hXb%?pc{#uJ~m7B$Ib`Nb$fi9tqK|5uKJp*3WBN-zIh+{oed(lT0CRQ`_$# zU?#e0ZKqCcmWU~6UKIP{6<5`*C(y{Q($vx`$YY!BDTG=Zl-Kj?*_4{2w7fd@Ke*R> zH#?o#<;NEBi@TvQO&=^$7z*(oZaeKv)OQ1m)}9||PwnEgG4 zJP@=x4viFy5v zCZ7vm-=rL18TphZ*~w~;{ukzZL*V(xi>PAnCMN?S3KZoHv*9(bMY70Mq}*{Tr5JFR zz;;vI9 z?Y4Mv4FGB*OdWG9^WYkG98x-p&~$f*T}p?q@ilI4RDS4%1blUH-k<;D;S{CQP0QoD8?GOVjnEqocC`v(ep9V|aiu zqBOlEa#@JC3bC++y!g4ZO$z}oK=wiJ8VsrA4(T5#2f&hPfXntx93|U@t%9>GBH%eh z(((vU1Qsg$D=5{%Z}=ORJ3rGN46g^en6z^jZk-Z+TT@hi`)|9%6MXwj*2%h;4`k`IF#>;8pU=>LZX?V%mLd03fTGxnd&g4 z4%L1+-*T;E3w&-Vh5!R0?C>hQZ}82cL5|q2%uD$a1}B8-%02bcS$>4%%U~^?u1Z%> zgEj_EJWaUdNC1-nx(F=>&hoVY+cpjEM*h$Lnt?_(?nGPrzmt!PSEP;%qIRXzX7)M@ zYj{RX9bT1sjDBYeXHW9|x+V1bb!rOI50iHod^vc}@iasKOL&D3rqjzp1w}jAi zFM4pEsc)$P76gy(mq93xK$juBSL_#fo$fu^HVD`)>Mo~dR%Gwm z9aQP=E1mB=`^WG?NqDw$Ru{k(?Msoe)xm$O zgIM^mV4Yd~h!iWnAYx628%ase_r8A46i$gtI!-hOfEuGiVKT~uRgpI>Z>AGpM0!*qZ!MD(pHytRG zqS7weq^~SgWtaE;OQQ$x?w?|m2Lm^$jx-py3>?bq>Cjm*6dMetNpy6r@hj(aKK=5i z@zxg-8@|Xd(uA_o)xC?nxwXn}P*d9A<1t#7vo92q&fnMSZcMyoe1J6|zf9zxw<7PY zoh>Q3-_{!yX7oMav#&oL)p_T3S5ja2-qQjHECza+I1+W6Gx}s(s^bx^+4{vyIMb<; zXFZ*ta-&C(PpALU`!FW+JU^!pRJNHwK~IuJWy{5~+SV_rBs270)r+JMXsQ-2Y`q-? z@;|5=z-_ss8arm0+U)dWz4C)=J_$7|U-9{I>fwP*XSEOE3CF0pTmM|?t*x^c+u{&& zfIcd-A=>r(3SNuG=uS4zI=CHbI+3c9^Bj{ z#~JqZ$lJ$PR3C|GyWC=|{~GsMviVP>&rO_*xXpO(cxSVZay0}txm<1WHe8^RdiW$j z2km3%b{dzqDw~LslDgaUZUu)b+YPq1^-=!tYM)Gf_Smid{l@9WE>`~xwMpUN_L-BP z=-Qbc7M(amwS{`^o^eIc zMAHkun;M6H(K=;LoKE4q^O@n#%wzWzrKE*S8+U`tovWeFv6}oi!@lJr-v1 zuK3;kQET^!RGMrbcQ=uq%BfRDJgoDd_4je4=I9oOh2Ppk6EEUdKmYN5{@2#h_2&OP zmEA6mJh^V^c4>EvnmZK}M=&S-+sr)`)+X=2Yd29R+7C#_nZ2J~xZBwyXSJb7Q9;D$ zr1iE3F?)^*hU^IJ=;S=$>AYn%UB*B1Fbaa5Mw~e!Crl14sn>J)Z%%UVjyX&e4MrOJ__@;O_%SGKYIJO$nLpQThf@Ilg#XoH(5WAQmGJHTqs;d){?8V- z>7rvrxWu@JG~N$%>!TRrQvNcn;5LQ#C@2lt?EzoDeFJCcdBei_!0gUE6f6sh?!Nz- znxe~HRbzw!KJh9>TlVMOEfNuX38oIvPvAiU=a-N|qNC09dDK6pOO!iFdyT%hTPH(`g2~O?-H-KfK1c5o z@^n8+MXsAHN}zs>efa`s=IfWdacd}JKo;+!N zB%2lu>lHHW0V$vIgU8ekujzpeFcT#v;TXjgs7&CV9slzBHi`&XLJ;>em}gR>#K3_~ z88feNL6MiRWPzTWT3L;I2kvp}qruLMQ?)&`$BYbj>`*^a1Wz%|ucj}5I%H*7-+RB{ z%TKtd5LW=ZfLWb4kM?cFXHos#&WR51Kkf7F+h5v#8m^pu`f$_xU!=2qaySQ;bRXI+2p5IP_>g7TZnil5%MW})96gD4n`QYP=W(ribU$Y)%#8qgRaPh1WXb9`RsK}N1*a__7+i*x_`gahjS1&T?7~a zVLCBQMouI!fY4OFHmt%`g^~giT5V_NtVxecuuLQN3^<89&fmeb4Q?>^c55?(5|bW$ zXI1SQ^y!8h7(`$fcD)2Y_dp zKu2-g&kN5E03N~^X&cAQVhmNK59eXLVW?m4W_&Y6#-{W#X7Kkya;wxOE?M{sW}=h| znGtk|nZMoooJ!w)|1AYM-841~J_>?6KA+t~AgaTMMV^n}_D^DO_JL~~D5aJWvUO@D z1qJK?(d0ios~J*cMqbTr)#?-f)bIj`xH-Lne{wR$P$&lr95diEgJcNMLa3BjF+@ZV z1_i56MZ6rMLkaY0Yim3qR4n22Ep_8Cllfi#A+viWXgM@)n#>2HCMR&Dy}e9U1jxN$ zI+OYo1;LZV6VpXUb1^k{mA&-l2|2I|9jYhczaOI>%d@AJqe{E zfpB0=*BzS$IWtr>&a;8=O#9DGZaE<#p`{Q&Q2pYW&FnZGsiRzj-Ka3R0#@0ZuNIKE1@ z2-l?{aR_=Us^c<8WE9?n@@5Nr3e=g}*q^tD>0;^z&a(H1zFuqHV8&HeUcyy(2iZ6a zyKc!drU!i-obpluPf4}0FufCMveAK3}_`r#VA!z5KA_iRuTRV(!VHZ6mukUV_{^rMX|K&tM~k z8`H*#hJjYylfV(l#y%1_^4B*hbL`NMRr;|vX4o9EzFKWqvAzF{zOJV&r89i$bpEfC z&%c9BcwNH;8u4)^5lhLdb;TwKV|(6Nt`oA7yfb=_v4VL@XKdd)76 z%bC^^2)n7ZGA95%YxwF-p8+I zuxl{Zv(ZIhatWHYdQMmSXT|IYxT_-e3kO(H%kkEq9hofX3{F2x7n%fOQGRu=6$Wq6 zWr4P>`D=H?-zqpu;b};@h7r2Sg9o1+K2_LA-)l&Lg%Crj z(AC}{=rvg|3T5~ZwzwP9V^Ub@WW&%%I_{AO0Ns3Bi)N+9AG(1!3!&?(v{&w!iO#CZ z#OyGWyEUo^4=PL|`djzJN)K+e$q$t=Z94yuRnzVMjdhhrU-{3zbHkY25-D0Rcof_r zfRlqD%xGEuxpnN^i(|LP)|(HhkC(CGwYUVM)R_ezz!8!Ys=7b+|NhwDg|(o-c-3aW zwXd)dU&9Q9qO`LZ?iKYg_-un?qE*TlScf83jp%l;xHhdkzW8Qab(F2s&zgUi5L;W; zu=HF1$-}D%d~Y zqQJnxE~H|vV?)(<{cN|(j^td=9jZ4%5*RfDpGF_Ov;9VvxcDEB(-}APP4i6^bgY~X zGXyt=itMmzPI^VEWT1l?$g@slNSE^}RVZ;Qwy#g5=<@q=m|L@M>+$cWn9| zYXP``5C$LbPZT;mjH=^563(T+ZVxa*cM$6fHPb`W4TR@=Pta?mp#hx1A)Xr@wjJrd zaUw=5YJ(GhMu;Q{B4Iip)Sr)%I2U5W1QV3FFBdUuQvK6;$ob5fwCR%=b--hV47~#A zrF?*rVYxT-6`g5oZ`b^{O4tEg*Jjo^%ip`yS`k`ZtwwBM@9%MEw)6^~@vDLtx+GB% zLCnT|_o(F~xN8&i4B>!2ANI&!bNPG|92O#KB4(|%V)M2@af#e`2&9d;)dBT$wTx=I zPHZoJHVbYXHdao>u!MXy)O+(2{-b~VNI)(`93vReY}lTwFNX<(Dm->jhouS~a1HMo zofQC#fYebkKT(W&1^&-DkZ>TfYjNksD}rt16$n7j1DJo>OzeBVH=y9q$2%TaH8OoR61ZaI2zLt zmIca$niJ-hmSHdzTUmM^q>GROV0THEmQNEiNd#s6nFzXv`50asQavHGo6VYv<+1dZ z^>nJpttp)>XkA%$E&F`-f0$qnZ}07f3Y17*$Xf>!x|iLCxXSyXn zdG)&y>%IH-0Os&5K;PxT-@~h7S$K`A$jq^pTxW0pvAJ6 zCccHHq<`u~TWECCXe|lJOG@I$(58R7f;JW8ZQR9LIQMJ3uj8CJQfVgxNhXQLgIf^I zr!F415WY-Awh)Ac$g`15tTt%gONT}##Z>&cLjz*~c*|;3hZJXTiteA0b=vlILGZfi zsa??cV1S4clf_5CA8MIFJ_~BJ1ftN-;vWV`M%L-VSsNq70NxrgJV3k(mruhgby4y? z&Kby+!y4)0HB(Fh#0g>iL9w#D*nAuT(>N38ogn7+NtyG#;Dz_QNmVYGWY5=yluhDn@P_H*Pd#DlzAQcHGW0i~Oc{OzhX|o za;<_fn*$x?r9B5IzV+`1PE0P+BB!mVhlF&JvU?neCJF1I_|E)6-GN1Mu+hlKD8(!r zJ1)s<1`a2Ld5`pk8@c>Qa)ymA;n$%e#YqmMux^dDab*Xnlv1HYajm@r$2&+lF^4+H zCRQj-^{rJL(=^nTTCY6~r18zcP(w?@;FF8$9#;fS9NnGPFWW-a&3#TBa%>jec&PAh zZ>&GdmW^T4hm!s2Dmt)QRui5z%+-7FA~G2Nsq(XcOuCWr>UUk{57y zT~fPIKeB^1u@n5D-+%tVR9FLb#Hqy{j+fL{tprwU7lwtG{&-8x?3>jN5d4y`+sZ7u zlP>sJ-(EJg;FZIag9g;TCUC!Lh_Gp1t^L%*^8JlH^Xd&TjOh?0-_C!FER2qGj3fSY^TavkqssJDs)%_}3K`%o&K48WJL-K6Wu-Eg)%3 zsOO3P$>(*uq}DOEw=t|UkqzB-G49B5nxNy?>tyr_B-QC?mA-8JDZuk1c}PZEzmR?` zbc&_AM(Ce4Yml%+n24#5d-AgWPr>_Yi#^K~cUGisX??o-e)zB5a%&FM`U}FT1NM9z z-L}KOGzEMAQWqT@uK2ZwkM-Lnr5JBv*{_-&ZU?vDE<6yvG+>}HC%L;V?p~O#mJf}e zsC@v7SS^?Ho^saqMuV*~UwfJpE^g7HlFd0RP%BnxcfgVP)Me3-=oK}~5R;$nds+~U zn@-K}@49No;Hq6-@4)1}#(2K9H+4PK=>DpjT(?A=?d5ATY}Fv~Doq8BT>BA;Dh z))Zy1p)uBmTgdZH(EhudSC)+(`e#=UQ+EXlxnA}7swr}J`VE(VEq#e;Lt*Qe)O{)R zYL|l4t0NyMybChPuLFBf?g<690al5f}zGqiW zQcO)fM&27ri86oLZ$>}1cU}1-0XKQeiM;31GzI1CSfLvbjOsl0ZMWs|$?0lmt2xbJ znMJyae^CnQZ# zHm|oUvDS&RQ@icdbjaHOW<^8PJjL?c;(OL#t+$!>JxZ~pzpl(x@RCDrBh6GYlNv^; zD0nuMg)M8*!W9ge_m_v2rM*@j+Q{C0uKYHPhWcQ1n21YqJI|A_r54@tAclFX;ZpIP zn?nT!7ayBEf+G9oakq?cPE?|cFxfJGZ|W5GYU|U>5&kdZUNtCQwsD>2Sx;SMeXdl% zraVeGC9FE-n25uAdpg@0iQ9(cq#;!b)ZKJ$ZvNs9u;0QF0ZVsV3+FhdNT{$!GVa*L zdAdQKi+KtE7v80Bm{Al^?Tb$vLU5kJKR3*`6#af@J$)AdpS<4-Xv&G_3W6ypNf)mU ze9I3bRXd(AB-b@ZMg4=|BDJDIWm5hX@#>26w8fcA+&TPCjuY$v^jE9ta!6eWx#8s! z19lv}8Ne2=4ZTr05+LRKzxQ`#wYvA9MU)%}_+<>J%W7PI81fR#UL2u~@2nH)VgU(= zOd5YbfJJa5Ogvhj91$}~s>evid5M`0<<=`ySsyw&I6efg0b>l5Os+&l1 zaNttoqTx0+im!!iM<$;sq7X+#3o!B8!=LJ-kZwHsx3aFnYl_jw4R-;?hTl6H*HOQ7 zme+MEpFs%ShMK>hr$hRb-!)DANkzjBBSk?VU|QY>ugcvoGd-zJ=+d}RAl$Rp8iGkWQE7l_Y8YT$LD z)_MwtA@KGf`e((TE3)DD%3heZP^ve}Qop+T;I149%EZetjz!Cx+tm&+-2bYrY+Na%POSo|a>)6fpYuxn&q z4F?_D-qXx`i-e(*#RSu zVlTdG$vc{Rt-%55Cn-X=(mx~l2~gCsZO8^aR)$)x?ZcrLhp*4Ft{HD1k_qyD)*EQ% z5bL^~{>bEPn=?So7+(Elmm=ll917pB$HMojjaj!O3Ntpim-2{%G>3X%lfQ$Tf@#dO zDQmav@Z4>c6@zQRdOL}jj zIYdR*FP>`0lmyof8L*26$BrxdH}EkR*9{-=r$AAqb-+@OXKq6S-8ngi*&{z4f)@7Z z&uAUhe)3a=H!8WBM`<@aGX8FoNOuiU`XhXAMk|9x=t044m*^FN3F=MMCX(G^!2w#= z0z!j}T0(gj?bquTjGq$U=|sEL@jFRiLPBHtEy3>CqQb)D)YQ7BZw9#*h=9j`hcrfK zeFBt~qMNS35rP-d5Z@NJg2{%xUQkIXQQDne#TkEw%^@u($4EZP>Uqos7ABu{=@C_D zpxKAtO`t>U>7t|i@5wX3{bOzkl^a3Et@zEwHhfp98 z0+`82Ux)D*jD#>t55|-|OflKN$`11{u8JS9fA=E55Q{!%pQKU z3a20f+@TjRxnkz8%1=sXl=P?t71MMj=)E_lE06_FvCW8kz|1JR&lEA>HhS}LLS3hWD^J%(N;nEfu zo6x}k>NtzIa19z~j8HW9@4xB9;{pdUTpmzwv|2GdAcQH}nq>6y5S={dR#%7+KO z?Fk~dZ}r9D8`ecfBjJRMNsgi2C}uUwzfZ$e^0B>hf@%AaY;zFb05_&{9&p(7ZQ%^a ziG+Xm{-7iJX)-^7p}Lg&@JV)tB~bA=!FIrU`1SY|p7DInPL@7zwIv!9?%=24cto z|CFh1W?Cme0wAXk9Kpw5@EX64FvS7~RfiYG^2FBwT^tCPgjfi%~KgJRlA5w;*1ekmMM!WN?kl`hfF#xhv-gDO#qcraA}DfxbkT0;|&Td{F8DD|-b% zfqH1=m=ZG4h(j(s#0o#vCkP+G3<5--l%k?dyxwca7I75vk;ed7qUwONEphLK@Hq;t zujyc&k#rdi6cohmYEy&5iQgy+MhQ~}Y|`QLW$F7YDR8C$Dkiojghy8x83FZa6v?QE zSom`P)MJOR!ZUF!vS-qF{=;0M6bQ^L*j%O>QrFOk6HO_E)F~MmB4Z0Q?^p_9+9pAq zm2Cq5bj9^pwCh(wAFqz|qo=hlV+pd~`w3V6BWVQ*4k?*=l_Qm`(@N7lAs%iPA^ z*99X^h>!_`>s;WO(MM1&RJAk66=YN=-@CW%A?&D^te&l&A^sJWfQZ^c_Eu^hs5JTj zN$j;uv0JZ`QId%taShCP6`y1b=#uY8q;SvtbXCBWq6&@hEwVsxA_F(`r-`W|>wSED z2p)R3sw#eJXT;1C*M6L5i@?B2Hf0Ws6%tcoI6t`?TT`&?mVvI^W*7N$_k%J@ex9bR zm^yAMpv;rg)9%1-iTRSX+R?f7YU{b$1FZlu5V9Op=!Szq+$8BnPdIJ6^*o9p{o_N3EM_lES9d>_U8iUS77;)lQU*{zzxrRd z&HbWtUa>6zb?`Gv&Z~?5#3F~N*k+_k6<;+%tMUjuB}~2(v7pdf(a_frM`Y9>pDL%j zhKT(F2lDQ{d;KVLkUQ5*cG*;%7Hupxj zyLa!V!Du-QsGygZ7gt*VAuatCEWUwmWe6A*cMj~pcrQN_3*3+jtP++y#|bG7!A3?HHG<6Njx^Ph4DbV2cztg5O4(_O%~&t}JtW{*?@ zi}>j7P-r>f5}U(>7!eyVV{~-n!x$PZBJo^CcVam9*lu^po-V@@D7^7BlFe50ua_pj1UOX4 z3gu((yYYgcJP_!^P!9~8CP=()qq_NsDF5I`2{3`U+BR1&9yJHL0@BR`?$1qqxo7`9 zC>U@hpfKg&5-~ATHk?6QU(+6Cdb0M^GbSD}fcYLput2BAGE&L1#uK&S@Uhm`RzPx>oirAaY$xE{O$&Pehrcs(L7#tHx=JkMjZ z37%`qqSV>QjmGYVp-Pk8wCR+S^;XH%_%g))Of?d^15}$RB={k44?$bMm44ftWep00 z$<KXwJSRXC!bV+sulehXjn8{Lc%0v7e}M}3GL zAJ}ZO&pUw~Ru>x-dhhi!wTTjnxZqU}+S=M?rB3&0GyrY2VlWEWexq+HGQ1~b)h&!U zswE=5%TK^Z0_>PO*Zz^EA3m&%UNL}}<8Rlv&`7WOJ?h;EOHUzy(}Tia z-ewM7p5PTMdB}hB_Fzb)jafA1Q!RyWy4Rv9F?B3?v+~Y}S<1@FJI=?}r;j|6eWRz! z6B)bi@5mCW18@6SW4GeMUkcjEZ}ig^V&}%6BpLm##2Tn=P79U0|Ku|JdfYJMfXz>y zdJT_!hE`NW_{Hu+I%6{v{wq zb0VY;0h~=}(VAR36ythj>3Ypv)cSEb@A!;;TgKDRYS~|ze`flz$XrkOzA(F@@!eF5 ztF_S7Ws5>Xo*38q=r6|<;+X!4eg;&EDNQiW*S;_Ls@JsT+$2YISAL||+WRa%ZSN1R z@`{=*N;(d;C*`B1mRX!Hj#U#~sPfeL&E)Au;gmo33VQZ$Ubols!anzdt6!Q*^LaQm z*ZbSq3ElH!+vc9Vc+Jlr?vA=AM=!9B6*b2AHJ(1H7cF^3D(OOo7T20aaqGkDPWKJy z`wCfX5OrFcP-(nIG_1f$S%W8`GB|bZ+|Lbs(W1pJ6E{tb7q@)k&$Z{y-#`D6H&ARL zSA$bH^AD$Kv%QfCcYvw}p^zKI$l7 zL`8&WBPHd9{JUq?mBjUd8Alcoh>iv_f+a^MynU`E|& zE?WRgzKintWUYalv$C8pCjL%NPM_=0Ojd>q!wC{5n_*Yl7jHQOU)qKFzpt|IzJ20& zQ}4+Wuqwr7!*Lk-wm;830o3)G!5>XrYUbA9G^`R1&*jpBvNiY2l zyokzexhH~xUd&++jMn;zU|Bc}JF#);Tx37|`%c*``g(k}mvq^!TWuhHg`d5X8MC<4 zKt`w|S6?|){tt-L>}Gr5qEj|ow%3xgk9<=b@m+(V!$yp_99MbeGf8GLLf`_HD+-_JeTpZ(MrrPHrnqaENu*KeblK=h zCu~Ic)PqdGrXa4h_p}_n8C`XR3dF0%qv*Y2MBZ=oT%s?#>o{D;g z3E*hKCz~n!t`~ zhBW{+a*jsw7R(iPurQSe4UL8^K39I5R{9vD>mAk>@!7gwQnZS3rvXwX z5~AO<>k;k^n*dAC9n)HEN_C673Fw1pFLx+qdj7<+CJGAxtHoYEP%PpWCLQ%1(S5;@ z_+o^AKMEVZvfDeEGplp3u|b;+&i%h{x!dfjaVBw6Wr)2760Xs#<$%|KF+Bor@3Wad z$aaUY0kJ2yKll!3j|5Fg&|;=#P1%Mp`vy6Jq=MZ|L7F@X=*8&>kvqFz`Ut|!fn6s9 zvMd0gDU-^JqJsuhLhDcpi=$8kqKxX^GA2YTHoY9Ig|w_JWF+r+G03)Ah(xan3!$nb z2Zx0S;QVs#`ku|+Tn8eeOf5Vyz|%ecNdPy_=co;8$vyxUEY3)-M3VK2;8m&8zJ7jf zxRHmKjTYVM=;*Kj73r(?%b2tb(J(jRJU>q-x@4?%N>;f)UESg<` zAbs#c3A)5QY0o)9_P|6RBxb1UlL30KytD}t16`(Pm@0zJu`Z!Dnw@)wy!D!zH@Eua z#gqfVO}!p{88!k6V+e)B4u*p>^wZuSTmo ziXe70#ql}L`XDbfcrPDHl^O_o3=oz%jRA$X{ZSgu6%;Rw%?o^xtY{nGmcZ43$`0JM z)uCxhX}gWA#*fno*BHhnNk#1eQov>~!1-4FK69H)iFSL*TBS@r1V_8v%9c3^xEKoi z=RG~AL6yT@FwK8Mq^wVVo=fEDvFgh*1u8}5<$`!R7!Ji`Zp(zVMV|5wh_Z0^@n9_( z>YKvA<})qbK}-{}9w;l!kBr)H&I0;{(D_Stc}!L{)G+|Au$cP)9fM#JQyX!E`n2b- zUngR7nkO0y*a#xc+1#D{oNxUGV7}te!xzf;mXcxuT=4mV)f<;~$Cjh8HO;~P3lKI@ zj<(atqyi-cgrcjW_FdwamQ=Ho{AbKsVaa?39wrFY+a%`^+PyN*&i!egh7a9lkWco` zICcfQWSd#3`>$9%8v5fzoYCA}PQ&a7msR33@$?U`bUqxh869(vUCXvsyce1>pu20) zx?&6EoYH+4%pO-;1MZwPe=8)uu**Y%+7$FDlDDAh-2OQ$N2@xWMU|4<`rEH0VY#4y z7w+ZCXt3INL*3$d;kNFH;O&^+M`X{)8=OpIC^7!P&|+W$t8RC6ix5XGM6oV$=pZ;a3x0&5NR=AG3Rm9M|nGDwWK(rVS%<;>6POX zV*`I|zg9kEXAw6hEQryygt%J)aZ#%2^_-pCJ53{aDOb;lv&{ro{Fl zo<^^iA`5RDCC^-Edn#TM3n_RxWO_)~@$m{rd9(2BXkBmXr4LV87C)BRR%gR)W zicnu^TA`D}%^&+g2qRyC)_i6hH8#~qJj!bJJm1YzVbS7zt_D0FDoxzf*Z`3&7vyZt z=1~XwO>Cu;P8g(LIx=qVs-P1|lQ3IG{+uV4<|UADVU@zhi%bvaB)YQgir7ZCBxYGC z`IW9_W1E{M54X}Z{-wvbTTE)Bn}EuWQK+R_nUC-RjY)b(EYlO0M6Mic2qr-~)wjp$PMQl4P%XvR+t#G?72DSoJ#Aa;b2jsN2V2bL%D z8S=!6(B7o)AxEQqkbFL!`yOoL@ehB!>+OT?OMkX)=@W0e{d#41BpXLX;S&piE=e;% z=xGzaM@+&g1AM-O%hI=siTU*^V(FFV{}0vff)KtHg<@3iqR@Or(}gOUhL09Ga3LO_ z!cpv5dV7h^L`NKQf{;T@%C9%6*W_RNGts5f_HgMyIq@^E$%z_Y`WCW`qGp*p-^dU8 ln!=X;`e`8!N