From e1a5e6d76184d2167dac4da64f17fa20cfa9dbc3 Mon Sep 17 00:00:00 2001 From: bigbrother666sh Date: Tue, 19 May 2026 22:20:46 +0800 Subject: [PATCH 1/4] feat: Enhance crew and investor skills with new functionalities and improvements - Added Python dependency installation logic in apply-addons.sh to manage skill dependencies. - Introduced init-workspace skill for creating standard directory structures for design tasks. - Created investor-materials skill for generating and updating fundraising materials. - Developed investor-outreach skill for drafting investor communication documents. - Implemented market-research skill for conducting market analysis and competitor research. - Added xianyu-ops skill for managing product searches and messaging on the Xianyu platform. - Introduced a layout check script for PPTX files to ensure visual consistency and detect defects. - Updated browser-guide documentation with best practices for using the managed browser. - Improved exec-tiers.sh to include additional commands for tier 2 access. --- .../crew/business-developer/HEARTBEAT.md | 30 +- .../openclaw_setting_sample.json | 1 - addons/officials/crew/designer/AGENTS.md | 184 ++++++----- addons/officials/crew/designer/BUILTIN_SKILLS | 2 + .../designer/openclaw_setting_sample.json | 8 +- .../designer/skills/init-workspace/SKILL.md | 39 +++ .../skills/init-workspace/scripts/init.sh | 51 +++ addons/officials/crew/ir/BUILTIN_SKILLS | 1 - addons/officials/crew/ir/HEARTBEAT.md | 30 +- addons/officials/crew/ir/TOOLS.md | 1 - .../crew/ir/openclaw_setting_sample.json | 4 +- .../ir/skills/investor-materials/SKILL.md | 121 +++++++ .../crew/ir/skills/investor-outreach/SKILL.md | 114 +++++++ .../crew/ir/skills/market-research/SKILL.md | 120 +++++++ .../crew/selfmedia-operator/HEARTBEAT.md | 30 ++ .../openclaw_setting_sample.json | 3 +- addons/officials/skills/ppt-maker/SKILL.md | 124 ++++---- .../ppt-maker/references/slide-layouts.md | 14 +- .../skills/ppt-maker/scripts/check_layout.py | 301 ++++++++++++++++++ .../skills/ppt-maker/scripts/generate_pptx.py | 50 ++- addons/officials/skills/xianyu-ops/SKILL.md | 135 ++++++++ config-templates/openclaw-aihubmix.json | 3 +- config-templates/openclaw.json | 3 +- crews/main/AGENTS.md | 8 +- .../scripts/dismiss-internal-crew.sh | 0 .../crew-list/scripts/list-internal-crews.sh | 0 crews/main/skills/crew-recruit/SKILL.md | 23 +- .../scripts/recruit-internal-crew.sh | 0 scripts/apply-addons.sh | 64 ++++ scripts/lib/exec-tiers.sh | 2 +- skills/browser-guide/SKILL.md | 50 ++- 31 files changed, 1319 insertions(+), 197 deletions(-) create mode 100644 addons/officials/crew/designer/skills/init-workspace/SKILL.md create mode 100755 addons/officials/crew/designer/skills/init-workspace/scripts/init.sh create mode 100644 addons/officials/crew/ir/skills/investor-materials/SKILL.md create mode 100644 addons/officials/crew/ir/skills/investor-outreach/SKILL.md create mode 100644 addons/officials/crew/ir/skills/market-research/SKILL.md create mode 100644 addons/officials/skills/ppt-maker/scripts/check_layout.py create mode 100644 addons/officials/skills/xianyu-ops/SKILL.md mode change 100755 => 100644 crews/main/skills/crew-dismiss/scripts/dismiss-internal-crew.sh mode change 100755 => 100644 crews/main/skills/crew-list/scripts/list-internal-crews.sh mode change 100755 => 100644 crews/main/skills/crew-recruit/scripts/recruit-internal-crew.sh diff --git a/addons/officials/crew/business-developer/HEARTBEAT.md b/addons/officials/crew/business-developer/HEARTBEAT.md index 312876bd..e2278fd7 100644 --- a/addons/officials/crew/business-developer/HEARTBEAT.md +++ b/addons/officials/crew/business-developer/HEARTBEAT.md @@ -2,13 +2,29 @@ ## 执行约束 -1. **无时间限制**:HEARTBEAT/cron 触发后必须执行完清单全部内容 -2. **遇到技术故障时**: - - 先尝试关闭并重启浏览器 - - 仍不解决 → spawn IT Engineer 协助 - - 仍无法解决 → 跳过当前任务,继续后续步骤,不卡住整个流程 -3. **不可呼唤用户协助**(定时任务可能深夜执行) -4. **浏览器操作必须串行**,不可并行,避免竞态抢夺 +1. **无时间限制**:任务执行不受深夜时间限制,必须执行完 HEARTBEAT 清单全部内容 + +2. **遇到技术故障时处理方案**: + +遇到技术故障时,按如下方案处置: + - 先尝试彻底关闭浏览器,再打开; + - 重启浏览器不解决问题时,**spawn IT Engineer**协助解决:调用 `sessions_spawn`,将问题现象、错误信息、当前任务上下文完整传递给 IT Engineer,请它协助解决; + - 仍无法解决 → **跳过当前任务,继续执行后续步骤**,不要卡住整个 HEARTBEAT + +期间不可: + - ❌ 呼唤用户协助解决,HEARTBEAT 在深夜执行,喊用户也没用 + - ❌ 不可中断任务,通过以上三步依然无法进行的任务则跳过,继续执行后续步骤,绝对不允许中断HEARTBEAT! + +3. **Subagent 产出校验与重试**:每次 spawn subagent 后,收到其返回结果时,必须按以下规则判定: + + - ❌ **无效产出**:返回内容为空、仅包含错误信息、不包含任何实质性产出(如文章正文、发布确认等)、或明确表示任务未完成 + - ✅ **有效产出**:包含预期的实质内容(如完整的文章 markdown、发布成功的确认信息等) + + 若判定为无效产出,**立即重新 spawn 同一个 subagent**(使用相同的 agentId 和 task),最多重试 3 次。每次重试前检查是否因浏览器问题导致(如超时、连接断开),若是则先彻底关闭浏览器再重试。 + + 3次均失败后,**跳过当前步骤,继续后续流程**,返回最终报告「XX 步骤:重试 3 次均失败,原因:xxx」。 + +4. HEARTBEAT 任务涉及大量浏览器操作,因此涉及浏览器的 subagent 任务不能并行发,必须排队串行执行,避免浏览器竞态抢夺。 --- diff --git a/addons/officials/crew/business-developer/openclaw_setting_sample.json b/addons/officials/crew/business-developer/openclaw_setting_sample.json index 39d865d3..792a495d 100644 --- a/addons/officials/crew/business-developer/openclaw_setting_sample.json +++ b/addons/officials/crew/business-developer/openclaw_setting_sample.json @@ -5,7 +5,6 @@ "pitch-deck", "social-graph-ranker", "smart-search", - "council", "browser-guide", "rss-reader", "xhs-interact", diff --git a/addons/officials/crew/designer/AGENTS.md b/addons/officials/crew/designer/AGENTS.md index 8af13d23..e3affc1c 100644 --- a/addons/officials/crew/designer/AGENTS.md +++ b/addons/officials/crew/designer/AGENTS.md @@ -1,124 +1,140 @@ # 设计师 — Workflow -## 素材积累与归档 +## 通用规则 -所有设计产出统一存储在 `design_assets/` 中,并维护 `design_assets/index.md`。 +### 任务文件夹 -**目录结构**: -``` -design_assets/ -├── index.md # 全局素材索引 -├── references/ # 参考图 / 灵感截图(来源标注) -├── brand/ # 品牌资产(Logo、色板、字体说明) -└── YYYY-MM-DD-<任务>/ # 各任务输出目录 - ├── *.png / *.jpg # 成品图 - └── prompts.json # 生图参数记录 -``` - -`index.md` 格式: +**每项设计任务必须先创建独立文件夹**,所有产出归档其中: -| Instance ID | 内容概要 | Type | 文件路径 | 来源/Prompt | 创建日期 | -|-------------|---------|------|---------|------------|---------| - -Type 枚举:`海报` | `封面` | `Banner` | `配图` | `网页原型` | `改图` | `参考图` - ---- +```bash +bash ./skills/init-workspace/scripts/init.sh <任务名> +``` -## 工作流 A:AI 出图(文生图 / 改图) +产出目录结构: ``` -1. 接收需求,确认三要素(若缺失则追问): - - 使用场景(公众号封面 / 小红书配图 / 活动海报 / 产品 Banner ...) - - 风格偏好(写实 / 极简 / 插画 / 赛博朋克 / 国潮 / 商务 ...) - - 尺寸要求(若无则按场景自动推断) +design_assets/YYYY-MM-DD-<任务名>/ +├── brief.md # 设计需求文档(必须填写,确认后不可跳过) +├── prompts.json # 生图参数记录 +├── source/ # 原始素材(参考图、品牌资产) +└── output/ # 成品输出 +``` -2. 从 MEMORY.md 调取品牌色、字体偏好等已知规范 +### Brief 确认机制 -3. 起草 2–3 套提示词方案,向用户展示提示词(L2 预确��,可跳过) +1. 接到需求后,将需求整理写入 `brief.md` +2. **将 brief 发给父 Agent / 用户确认**,等待明确同意 +3. 确认前不得进入后续步骤 +4. 后续视觉 review 以 brief 为基准对照 -4. 调用 siliconflow-img-gen 生成: - - 单需求通常生成 2 张(--batch-size 2,Kolors 模型)供选择 - - 改图需求:附带原图 URL,使用 Qwen 改图模型 +### 视觉 Review 机制 -5. 将结果路径告知用户,展示图片(Read 工具内联图片) +生成图片后**必须**调用视觉模型 review,不得跳过: -6. 根据反馈:调整提示词迭代(最多 3 轮),或接受交付 +1. 用 `image` 工具查看生成结果 +2. 对照 `brief.md` 逐项检查:主题文案是否体现、风格是否匹配、尺寸是否正确、品牌色是否准确 +3. 发现偏差 → 调整提示词重新生成(最多 3 轮) +4. Review 通过 → 发送给用户/父 Agent -7. 归档到 design_assets/ 并更新 index.md -``` +> 注:AI 生图偶尔返回纯色背景(无主体内容),也在此步骤排除——纯色图直接重试。 --- -## 工作流 B:海报设计 +## 工作流 A:复杂图文设计(海报、邀请函、品牌 logo 等) ``` -1. 接收海报需求,确认: - - 主题文案(活动名、核心 slogan、时间地点等关键信息) - - 目标平台与尺寸(线上 / 印刷 / 朋友圈 / 展架 ...) - - 风格方向(提供 1–3 个参考词,如"科技感+深色系") - -2. 搜索竞品 / 灵感参考(smart-search → Dribbble / Pinterest) - 将参考图保存到 design_assets/references/ - -3. 设计提示词时结合:主题文案 + 品牌色 + 参考风格 + 文字排版说明 - 注意:提示词写明"留有文字排版区域"(AI 生成的文字不可靠) - -4. 生成 2–3 个版本(风格/色调各有差异) - -5. 向用户展示各版本(附设计思路说明),等待 L2 确认选版 - -6. 根据确认版本进行: - - 直接交付(若文案由用户自行在图片编辑器添加) - - 或输出含文字的最终版(用户提供文案后重新生成) - -7. 归档并更新 index.md +1. 接收需求 → 调用 init-workspace 创建任务文件夹 +2. 将需求整理为 brief.md,包含: + - 主题文案(活动名、slogan、时间地点等关键信息) + - 目标平台与尺寸(线上/印刷/朋友圈/展架...) + - 风格方向(1–3 个参考词,如"科技感+深色系") + - 品牌约束(品牌色、字体、LOGO — 从 MEMORY.md 获取) +3. 搜索竞品 / 灵感参考以丰富 brief.md: + - 使用 smart-search 搜索竞品或相关设计灵感 + - 可以调用 image 工具获得素材的视觉描述作为参考 + - 综合搜索结果完善/丰富 brief.md +4. 将 brief 发给父 Agent / 用户确认,等待明确同意 +5. 素材获取(按优先级): + a. pexels-footage / pixabay-footage 搜索下载免版权素材 → 保存到 source/ + b. 无合适素材时,使用 siliconflow-img-gen 生成底图 → 保存到 output/ + - 提示词必须结合:主题文案 + 品牌色 + 参考风格 + 文字排版说明 + - 提示词中写明"留有文字排版区域"(AI 生成的文字不可靠) +6. 合成产出 + 按 brief.md 要求排版拼接原始素材,增加相关文字等,最终产出成稿 +7. 视觉 Review(对照 brief.md)— 此步骤必须 `sessions_spawn` 独立的 subagent 完成: + - 创建subagent(`designer`)按如下步骤进行 review + - 用 image 工具查看产出成稿 + - 逐项检查:主题、风格、尺寸、品牌色是否与 brief.md 一致 + - 反馈待改动点 + - 按 subagent 返回重复 5~6 步骤,再次提交审核,直至没有待改动点或往复超过 3 轮。 +8. 发给用户/父 Agent,根据反馈迭代修改 +9. 最终确认后归档,更新 design_assets/index.md ``` --- -## 工作流 C:网页 / 落地页设计 +## 工作流 B:网页 / 落地页设计 ``` -1. 接收需求,确认: - - 页面类型(产品介绍页 / 活动落地页 / 团队介绍 / 404 页 ...) - - 交互功能范围(纯静态展示 / 含表单 / 含轮播 ...) +1. 接收需求 → 调用 init-workspace 创建任务文件夹 +2. 将需求整理为 brief.md,包含: + - 页面类型(产品介绍页/活动落地页/团队介绍/404 页...) + - 交互功能范围(纯静态展示/含表单/含轮播...) - 风格参考(可提供网址或描述) - 是否需要深色模式 - -2. 从 MEMORY.md 获取品牌色、字体、LOGO URL 等品牌资产 - -3. 规划页面结构(Sections 列表),向用户确认信息架构(L2) - -4. 编写 HTML + CSS: + - 品牌约束(品牌色、字体、LOGO — 从 MEMORY.md 获取) +3. 搜索竞品 / 灵感参考以丰富 brief.md: + - 规划页面结构(Sections 列表),向用户确认信息架构 + - 使用 smart-search 搜索竞品或相关设计灵感 + - 可以调用 image 工具获得素材的视觉描述作为参考 + - 综合搜索结果完善/丰富 brief.md +4. 将 brief 发给父 Agent / 用户确认,等待明确同意 +5. 素材获取: + - 页面所需配图/背景图 → pexels-footage / pixabay-footage 优先,siliconflow-img-gen 备选 + - 下载/生成的图片保存到 source/ 目录 +6. 编写 HTML + CSS: - CSS custom properties 定义设计 token(颜色、间距、字号) - 语义化标签(header / main / section / footer) - 响应式(min-width: 768px / 1024px 断点) - hover / focus 状态 - - 图片用 siliconflow-img-gen 生成后内联路径 - -5. 将文件保存到 design_assets/web/YYYY-MM-DD-<页面名称>/index.html - 告知用户:`open design_assets/web/.../index.html` 本地预览 - -6. 展示完整代码,等待 L2 确认 - -7. 根据反馈修改(CSS 细节 / 文案替换 / 颜色调整) + - 图片引用 source/ 中的素材 +7. 视觉 Review(对照 brief.md)— 此步骤必须 `sessions_spawn` 独立的 subagent 完成: + - 创建subagent(`designer`)按如下步骤进行 review + - 用 image 工具对页面截图进行审查 + - 逐项检查:页面类型、交互元素、风格、深色模式、品牌色是否与 brief 一致 + - 按 subagent 返回重复 5~6 步骤,再次提交审核,直至没有待改动点或往复超过 3 轮。 +8. 发给用户/父 Agent,根据反馈迭代修改 +9. 最终确认后将文件保存到任务文件夹 output/ 目录,归档并更新 index.md ``` --- -## 工作流 D:改图 / 素材适配 +## 工作流 C:改图 / 素材适配 ``` -1. 接收参考图(URL 或本地路径)与改图指令 - 常见指令:换色调 / 换背景 / 去除水印 / 风格迁移 / 尺寸裁切 - -2. 根据改图类型选择处理方案: - - 风格/颜色修改 → siliconflow-img-gen 改图模式(--image 参数) - - 尺寸裁切/格式转换 → 告知用户需要本地图片工具(如 ImageMagick)或 IT Engineer 协助 - -3. 生成改图结果,展示对比(原图 vs 改图) - -4. 归档到 design_assets/,更新 index.md +1. 接收需求 → 调用 init-workspace 创建任务文件夹 +2. 将需求整理为 brief.md,包含: + - 原始素材(URL 或本地路径) + - 改图指令(换色调/换背景/去除水印/风格迁移/尺寸裁切...) + - 期望效果描述 + - 品牌约束(如涉及色调修改,需注明品牌色) +3. 搜索竞品 / 灵感参考以丰富 brief.md: + - 使用 smart-search 搜索竞品或相关设计灵感 + - 可以调用 image 工具获得素材的视觉描述作为参考 + - 综合搜索结果完善/丰富 brief.md +4. 将 brief 发给父 Agent / 用户确认,等待明确同意 +5. 根据改图类型选择处理方案: + a. 风格/颜色修改 → siliconflow-img-gen 改图模式(--image 参数) + - --image 指向 source/ 中的原始素材 + - --out-dir 指向 output/ 目录 + b. 尺寸裁切/格式转换 → 告知用户需要本地图片工具(如 ImageMagick)或 IT Engineer 协助 +6. 视觉 Review(对照 brief.md)— 此步骤必须 `sessions_spawn` 独立的 subagent 完成: + - 创建subagent(`designer`)按如下步骤进行 review: + - 用 image 工具对比原图与改图结果 + - 检查:改图指令是否准确执行、色调/风格是否符合 brief、是否引入不期望的变形 + - 按 subagent 返回重复 4~5 步骤,再次提交审核,直至没有待改动点或往复超过 3 轮。 +7. Review 通过 → 展示对比(原图 vs 改图),发给用户/父 Agent 确认 +8. 最终确认后归档,更新 design_assets/index.md ``` --- diff --git a/addons/officials/crew/designer/BUILTIN_SKILLS b/addons/officials/crew/designer/BUILTIN_SKILLS index 7ea22813..2c5916ee 100644 --- a/addons/officials/crew/designer/BUILTIN_SKILLS +++ b/addons/officials/crew/designer/BUILTIN_SKILLS @@ -1,2 +1,4 @@ siliconflow-img-gen summarize +pexels-footage +pixabay-footage diff --git a/addons/officials/crew/designer/openclaw_setting_sample.json b/addons/officials/crew/designer/openclaw_setting_sample.json index 9bf13eff..75844357 100644 --- a/addons/officials/crew/designer/openclaw_setting_sample.json +++ b/addons/officials/crew/designer/openclaw_setting_sample.json @@ -1,5 +1,11 @@ { - "skills": ["siliconflow-img-gen", "siliconflow-video-gen", "smart-search", "council"], + "skills": [ + "siliconflow-img-gen", + "pexels-footage", + "pixabay-footage", + "smart-search", + "browser-guide" + ], "subagents": { "allowAgents": ["it-engineer"] }, diff --git a/addons/officials/crew/designer/skills/init-workspace/SKILL.md b/addons/officials/crew/designer/skills/init-workspace/SKILL.md new file mode 100644 index 00000000..853aad80 --- /dev/null +++ b/addons/officials/crew/designer/skills/init-workspace/SKILL.md @@ -0,0 +1,39 @@ +--- +name: init-workspace +description: 为单项设计任务创建标准目录结构和 brief 模板。每次接到设计需求时首先调用。 +metadata: + openclaw: + emoji: 📁 +--- + +# Init Workspace + +为每一项设计任务创建独立的文件夹和 brief 模板。 + +## 用法 + +```bash +bash ./skills/init-workspace/scripts/init.sh <任务名> +``` + +示例: + +```bash +bash ./skills/init-workspace/scripts/init.sh wiseflow-5-launch-poster +``` + +## 产出 + +在 `design_assets/` 下创建 `YYYY-MM-DD-<任务名>/` 目录,包含: + +``` +design_assets/YYYY-MM-DD-<任务名>/ +├── brief.md # 设计需求模板(待填写) +├── prompts.json # 生图参数记录 +├── source/ # 原始素材(参考图、品牌资产等) +└── output/ # 成品输出 +``` + +## 使用时机 + +每项设计任务开始前**必须**调用此脚本,确保所有产出有独立归档。 diff --git a/addons/officials/crew/designer/skills/init-workspace/scripts/init.sh b/addons/officials/crew/designer/skills/init-workspace/scripts/init.sh new file mode 100755 index 00000000..8e23054d --- /dev/null +++ b/addons/officials/crew/designer/skills/init-workspace/scripts/init.sh @@ -0,0 +1,51 @@ +#!/usr/bin/env bash +# init-workspace — 为 designer 单项任务创建标准目录结构 +# 用法: bash ./skills/init-workspace/scripts/init.sh <任务名> +# 示例: bash ./skills/init-workspace/scripts/init.sh wiseflow-5-launch-poster + +set -euo pipefail + +TASK_NAME="${1:?用法: init.sh <任务名>}" + +# 任务目录命名: design_assets/YYYY-MM-DD-<任务名> +TODAY="$(date +%Y-%m-%d)" +TASK_DIR="design_assets/${TODAY}-${TASK_NAME}" + +# 确保设计资产根目录存在 +mkdir -p design_assets/references design_assets/brand + +# 创建任务目录(含子目录) +mkdir -p "${TASK_DIR}/source" "${TASK_DIR}/output" + +# 初始化 brief.md 模板 +if [ ! -f "${TASK_DIR}/brief.md" ]; then + cat > "${TASK_DIR}/brief.md" <<'BRIEF' +# 设计 Brief + +## 需求摘要 + + +## 主题文案 + + +## 目标平台与尺寸 + + +## 风格方向 + + +## 品牌约束 + + +## 参考素材 + +BRIEF +fi + +# 初始化 prompts.json +if [ ! -f "${TASK_DIR}/prompts.json" ]; then + echo '[]' > "${TASK_DIR}/prompts.json" +fi + +echo "✅ 任务目录已创建: ${TASK_DIR}/" +echo " brief.md 模板已就绪,请填写后发送确认" diff --git a/addons/officials/crew/ir/BUILTIN_SKILLS b/addons/officials/crew/ir/BUILTIN_SKILLS index 8e24529b..db09d22d 100644 --- a/addons/officials/crew/ir/BUILTIN_SKILLS +++ b/addons/officials/crew/ir/BUILTIN_SKILLS @@ -8,5 +8,4 @@ email-ops xhs-interact social-graph-ranker connections-optimizer -council ir-record diff --git a/addons/officials/crew/ir/HEARTBEAT.md b/addons/officials/crew/ir/HEARTBEAT.md index e433a5fe..3ea9e249 100644 --- a/addons/officials/crew/ir/HEARTBEAT.md +++ b/addons/officials/crew/ir/HEARTBEAT.md @@ -2,13 +2,29 @@ ## 执行约束 -1. **无时间限制**:HEARTBEAT 触发后必须执行完清单全部内容 -2. **遇到技术故障时**: - - 先尝试关闭并重启浏览器 - - 仍不解决 → spawn IT Engineer 协助 - - 仍无法解决 → 跳过当前任务,继续后续步骤,不卡住整个流程 -3. **不可呼唤用户协助**(定时任务可能深夜执行) -4. **浏览器操作必须串行**,不可并行,避免竞态抢夺 +1. **无时间限制**:任务执行不受深夜时间限制,必须执行完 HEARTBEAT 清单全部内容 + +2. **遇到技术故障时处理方案**: + +遇到技术故障时,按如下方案处置: + - 先尝试彻底关闭浏览器,再打开; + - 重启浏览器不解决问题时,**spawn IT Engineer**协助解决:调用 `sessions_spawn`,将问题现象、错误信息、当前任务上下文完整传递给 IT Engineer,请它协助解决; + - 仍无法解决 → **跳过当前任务,继续执行后续步骤**,不要卡住整个 HEARTBEAT + +期间不可: + - ❌ 呼唤用户协助解决,HEARTBEAT 在深夜执行,喊用户也没用 + - ❌ 不可中断任务,通过以上三步依然无法进行的任务则跳过,继续执行后续步骤,绝对不允许中断HEARTBEAT! + +3. **Subagent 产出校验与重试**:每次 spawn subagent 后,收到其返回结果时,必须按以下规则判定: + + - ❌ **无效产出**:返回内容为空、仅包含错误信息、不包含任何实质性产出(如文章正文、发布确认等)、或明确表示任务未完成 + - ✅ **有效产出**:包含预期的实质内容(如完整的文章 markdown、发布成功的确认信息等) + + 若判定为无效产出,**立即重新 spawn 同一个 subagent**(使用相同的 agentId 和 task),最多重试 3 次。每次重试前检查是否因浏览器问题导致(如超时、连接断开),若是则先彻底关闭浏览器再重试。 + + 3次均失败后,**跳过当前步骤,继续后续流程**,返回最终报告「XX 步骤:重试 3 次均失败,原因:xxx」。 + +4. HEARTBEAT 任务涉及大量浏览器操作,因此涉及浏览器的 subagent 任务不能并行发,必须排队串行执行,避免浏览器竞态抢夺。 --- diff --git a/addons/officials/crew/ir/TOOLS.md b/addons/officials/crew/ir/TOOLS.md index 2fb7f17a..d56fb2cd 100644 --- a/addons/officials/crew/ir/TOOLS.md +++ b/addons/officials/crew/ir/TOOLS.md @@ -31,6 +31,5 @@ | `xhs-interact` | 社交媒体投资人触达 | Investor Hunting | | `social-graph-ranker` | 人脉网络中找热心引荐人 | Relationship Tracking | | `connections-optimizer` | 发现 warm intro 路径 | Relationship Tracking | -| `council` | 复杂决策时做多方讨论 | 融资策略决策 | | `ir-record` | 投资人数据库与进展跟踪 | 全部模式 | | `summarize` | 会议纪要、信息摘要 | Relationship Tracking | diff --git a/addons/officials/crew/ir/openclaw_setting_sample.json b/addons/officials/crew/ir/openclaw_setting_sample.json index 277c9b3b..d9a8ae4b 100644 --- a/addons/officials/crew/ir/openclaw_setting_sample.json +++ b/addons/officials/crew/ir/openclaw_setting_sample.json @@ -9,8 +9,10 @@ "xhs-interact", "social-graph-ranker", "connections-optimizer", - "council", "ir-record", + "investor-materials", + "investor-outreach", + "market-research", "summarize" ], "subagents": { diff --git a/addons/officials/crew/ir/skills/investor-materials/SKILL.md b/addons/officials/crew/ir/skills/investor-materials/SKILL.md new file mode 100644 index 00000000..a171a203 --- /dev/null +++ b/addons/officials/crew/ir/skills/investor-materials/SKILL.md @@ -0,0 +1,121 @@ +--- +name: investor-materials +description: > + 创建和更新融资路演材料:Pitch Deck、One-Pager、投资人备忘录、加速器申请、财务模型、资金用途表、里程碑计划。 + 当用户需要投资人面向的文档、财务预测、BP 更新、路演 PPT 时触发。 +metadata: + openclaw: + emoji: 📑 +--- + +# 融资材料制作 + +制作投资人面向的材料——数据一致、逻辑清晰、经得起质疑。 + +## 触发条件 + +- 创建或修改 Pitch Deck / 路演 PPT +- 撰写投资人备忘录或 One-Pager +- 构建财务模型、里程碑计划、资金用途表 +- 填写加速器/孵化器申请表 +- 需要多份融资材料之间保持数据一致 + +## 核心原则:单一信源 + +所有融资材料必须数据一致。起草前先确认或创建单一信源: + +| 信源项 | 说明 | +|--------|------| +| 牵引指标 | 用户数、营收、增长率等核心数据 | +| 定价与收入假设 | 单价、转化率、收入构成 | +| 融资规模与工具 | 融多少钱、什么结构(股权/可转债/SAFE) | +| 资金用途 | 分配比例和逻辑 | +| 团队信息 | 姓名、职位、背景(以最新为准) | +| 里程碑与时间线 | 关键节点和达成日期 | + +发现数据冲突时,**先停下来与用户确认,再继续起草**。 + +## 核心工作流 + +``` +1. 盘点已知事实,列出信源清单 +2. 识别缺失假设,向用户提问补全 +3. 确定材料类型(Pitch Deck / One-Pager / 财务模型 / 申请表) +4. 按对应模板起草,每处数据标注来源 +5. 交叉校验:每个数字是否与信源一致 +``` + +## 材料类型指引 + +### Pitch Deck(路演演示) + +推荐结构: + +1. 公司定位 + 切入点 +2. 问题 +3. 解决方案 +4. 产品 / Demo +5. 市场规模 +6. 商业模式 +7. 牵引力 +8. 团队 +9. 竞争与差异化 +10. 融资需求(Ask) +11. 资金用途 / 里程碑 +12. 附录 + +**制作方式**: +- 在线联系场景(邮件、微信冷接触)→ 调用 `pitch-deck` 生成 HTML,手机/微信直接打开 +- 现场路演/拜访场景 → 调用 `ppt-maker` 生成 PPTX +- 用户未指定时,简要介绍两种方式让用户选择 + +**配图**:优先使用 `siliconflow-img-gen`(16:9),不可用时尝试 `pexels-footage` 或 `pixabay-footage` + +### One-Pager / 投资人备忘录 + +- 一句话讲清公司做什么 +- 尽早展示牵引力和核心论据 +- 融资需求精确具体 +- 所有主张可验证 + +### 财务模型 + +必须包含: +- 明确的假设前提 +- 悲观/基准/乐观三种情景(当决策依赖假设时) +- 逐层收入逻辑(不要只给一个总数) +- 与里程碑挂钩的支出计划 +- 关键假设的敏感性分析 + +### 加速器申请 + +- 精确回答问题本身,不跑题 +- 优先展示牵引力、洞察和团队优势 +- 不用空泛夸大 +- 内部指标与 Deck 和模型保持一致 + +## 红线:绝不出现 + +- 无法验证的声明 +- 没有假设前提的模糊市场规模 +- 前后不一致的团队角色或头衔 +- 收入数学加不上的数字 +- 脆弱假设上的过度自信 + +## 质量门控 + +交付前检查: + +- [ ] 每个数字与当前信源一致 +- [ ] 资金用途和收入逻辑加总正确 +- [ ] 假设可见、不埋藏 +- [ ] 故事清晰,不靠空话撑场 +- [ ] 材料经得起合伙人级别的质疑 + +## 与其他技能协作 + +| 场景 | 配合技能 | +|------|---------| +| 投资人触达需要发送材料 | `email-ops` 发邮件,`investor-outreach` 写话术 | +| 搜索竞品/市场数据支撑材料 | `market-research` | +| 记录材料发送状态 | `ir-record` 更新投资人状态为 `bp_sent` | diff --git a/addons/officials/crew/ir/skills/investor-outreach/SKILL.md b/addons/officials/crew/ir/skills/investor-outreach/SKILL.md new file mode 100644 index 00000000..2709db17 --- /dev/null +++ b/addons/officials/crew/ir/skills/investor-outreach/SKILL.md @@ -0,0 +1,114 @@ +--- +name: investor-outreach +description: > + 撰写投资人触达邮件、暖介绍请求、跟进邮件、投资人更新等融资沟通文案。 + 当用户需要向天使投资人、VC、战略投资人、加速器发起接触,或需要精简、个性化的投资人沟通文案时触发。 +metadata: + openclaw: + emoji: ✉️ +--- + +# 投资人触达 + +撰写投资人沟通文案——简短、具体、容易行动。 + +## 触发条件 + +- 给投资人写冷邮件 +- 起草暖介绍(Warm Intro)请求 +- 会议或无回复后的跟进 +- 融资过程中的投资人更新 +- 针对特定基金策略或合伙人偏好定制话术 + +## 核心规则 + +1. **每封外发邮件都必须个性化**——绝不发模板 +2. **降低对方行动门槛**——Ask 要具体、简单 +3. **用事实代替形容词**——数据 > 描述 +4. **简洁**——投资人每天看上百封邮件 +5. **绝不出海投文案**——每封邮件都应只能发给这一个投资人 + +## 语气与品牌 + +如果用户有品牌语气要求,先确认语气风格再起草。本技能专注投资人沟通的结构和 Ask 纪律,不另建独立的语气体系。 + +## 硬禁令 + +出现以下表达时,删除重写: + +- "希望与您交流" / "I'd love to connect" +- "很激动地分享" / "excited to share" +- 没有实质关联的泛泛基金赞美 +- 模糊的创始人形容词("有激情"、"经验丰富"而无佐证) +- 哀求语气 +- 可以直接提问却用了软弱收尾 + +## 冷邮件结构 + +``` +1. 主题行:短且具体 +2. 开头:为什么找这个投资人(而非别人) +3. 主体:公司做什么、为什么是现在、有什么证据 +4. Ask:一个具体的下一步动作 +5. 签名:姓名、职位,必要时一个信用锚点 +``` + +## 个性化来源 + +至少引用以下一项: + +| 来源 | 示例 | +|------|------| +| 相关被投企业 | "注意到贵基金投了 X,我们解决的是同一产业链的 Y 环节" | +| 公开观点 | 某次演讲、文章、博客中的论点 | +| 共同人脉 | "经 Z 介绍" | +| 策略/市场匹配 | 清晰的产品-基金策略对应关系 | + +**如果缺少个性化信息,明确告知用户此草稿仍需补充个性化内容,不要假装已完成。** + +## 跟进节奏 + +| 时间 | 动作 | +|------|------| +| 第 0 天 | 首次发出 | +| 第 4-5 天 | 短跟进,附带一个新数据点 | +| 第 10-12 天 | 最终跟进,干净收尾 | + +12 天后不再继续跟进,除非用户要求更长序列。 + +## 暖介绍请求 + +让引荐人省心: + +- 说清为什么这个介绍是合适的 +- 附一段可转发的 blurb(100 字以内) +- blurb 格式:谁 + 做什么 + 关键数据点 + Ask + +## 会议后更新 + +包含: + +1. 会议讨论的具体内容 +2. 承诺提供的信息/材料 +3. 一个新的证明点(如有) +4. 明确的下一步 + +## 质量门控 + +交付前检查: + +- [ ] 内容真正个性化(不是模板换名字) +- [ ] Ask 明确具体 +- [ ] 证明点是具体事实,不是空话 +- [ ] 删掉了所有填充性赞美和软化语 +- [ ] 字数精简 + +## 与其他技能协作 + +| 场景 | 配合技能 | +|------|---------| +| 搜索投资人信息和被投企业 | `market-research`,`smart-search` | +| 发送邮件 | `email-ops` | +| 社交媒体私信 | `xhs-interact` | +| 记录接触历史 | `ir-record`(用 `record-contact.sh` 记录,用 `update-status.sh` 更新状态为 `contacted`) | +| 投前调研基金策略 | `market-research`(基金尽调模式) | diff --git a/addons/officials/crew/ir/skills/market-research/SKILL.md b/addons/officials/crew/ir/skills/market-research/SKILL.md new file mode 100644 index 00000000..be077283 --- /dev/null +++ b/addons/officials/crew/ir/skills/market-research/SKILL.md @@ -0,0 +1,120 @@ +--- +name: market-research +description: > + 开展市场研究、竞品分析、投资人尽调、行业情报,附带来源标注和决策导向的摘要。 + 当用户需要市场规模估算、竞品对比、基金研究、技术扫描或支撑商业决策的研究时触发。 +metadata: + openclaw: + emoji: 🔍 +--- + +# 市场研究 + +产出支撑决策的研究,而非研究表演。 + +## 触发条件 + +- 研究市场、品类、公司、投资人或技术趋势 +- 构建 TAM/SAM/SOM 估算 +- 比较竞品或相邻产品 +- 触达前的投资人/基金尽调 +- 在进入市场、融资、投资前压力测试论点 + +## 研究标准 + +1. **每个重要论断必须有来源** +2. 优先使用最新数据,标注过时数据 +3. 包含反面证据和下行情景 +4. 把发现翻译成决策建议,而非仅做摘要 +5. 事实、推断、建议三者清晰分离 + +## 研究模式 + +### 投资人/基金尽调 + +收集以下信息: + +| 维度 | 内容 | +|------|------| +| 基金规模与阶段 | 管理规模、偏好轮次、典型支票大小 | +| 相关被投企业 | 与本业务领域相关的已投项目 | +| 公开策略 | 基金 thesis、近期公开言论/文章 | +| 活跃度 | 近期投资动态、新基金募集 | +| 匹配判断 | 适合/不适合的理由 | +| 红旗 | 明显的策略错配或潜在问题 | + +**搜索方法**:使用 `smart-search` 搜索基金官网、IT桔子/企查查公开数据、行业媒体报道;使用 `browser-guide` 访问关键页面提取详细信息。 + +### 竞品分析 + +收集以下信息: + +| 维度 | 内容 | +|------|------| +| 产品实际 | 真实功能,不是营销文案 | +| 融资与投资方 | 公开的融资历史和投资人 | +| 牵引指标 | 公开的用户/营收/增长数据 | +| 分销与定价 | 渠道、定价策略线索 | +| 优劣势 | 核心强项、明显短板 | +| 定位缺口 | 市场中未被覆盖的空间 | + +**搜索方法**:使用 `smart-search` 搜索竞品官网、融资新闻、用户评价;使用 `browser-guide` 直接访问竞品产品页面。 + +### 市场规模估算 + +方法: + +- **自上而下**:从行业报告或公开数据集推算 +- **自下而上**:从现实的获客假设做合理性校验 +- **每个逻辑跳跃标注假设前提** + +TAM/SAM/SOM 框架: + +| 层级 | 定义 | 方法 | +|------|------|------| +| TAM | 全球/全国总需求 | 行业报告 × 总人口/企业数 | +| SAM | 可服务市场 | TAM × 目标细分比例 | +| SOM | 可获得市场 | SAM × 现实占有率假设 | + +### 技术/供应商研究 + +收集: + +- 工作原理 +- 折中取舍与采纳信号 +- 集成复杂度 +- 锁定风险、安全、合规、运维风险 + +## 输出格式 + +默认结构: + +``` +1. 执行摘要 +2. 核心发现 +3. 启示(对业务的影响) +4. 风险与注意事项 +5. 建议 +6. 来源列表 +``` + +## 质量门控 + +交付前检查: + +- [ ] 所有数字有来源或标注为估算 +- [ ] 过时数据已标注 +- [ ] 建议基于证据推导得出 +- [ ] 包含风险和反面论据 +- [ ] 输出让决策更容易,而非更困惑 + +## 与其他技能协作 + +| 场景 | 配合技能 | +|------|---------| +| 搜索投资人/基金信息 | `smart-search`(Bing 主推,百度 backup) | +| 访问需要登录的数据库页面 | `browser-guide` | +| 竞品分析后记录到投资人库 | `ir-record` | +| 研究结果用于制作路演材料 | `investor-materials` | +| 研究结果用于撰写触达邮件 | `investor-outreach` | +| 分析人脉网络找暖介绍路径 | `social-graph-ranker`,`connections-optimizer` | diff --git a/addons/officials/crew/selfmedia-operator/HEARTBEAT.md b/addons/officials/crew/selfmedia-operator/HEARTBEAT.md index 955b7996..55262a41 100644 --- a/addons/officials/crew/selfmedia-operator/HEARTBEAT.md +++ b/addons/officials/crew/selfmedia-operator/HEARTBEAT.md @@ -1 +1,31 @@ # 心跳检查清单 + +## 执行约束 + +1. **无时间限制**:任务执行不受深夜时间限制,必须执行完 HEARTBEAT 清单全部内容 + +2. **遇到技术故障时处理方案**: + +遇到技术故障时,按如下方案处置: + - 先尝试彻底关闭浏览器,再打开; + - 重启浏览器不解决问题时,**spawn IT Engineer**协助解决:调用 `sessions_spawn`,将问题现象、错误信息、当前任务上下文完整传递给 IT Engineer,请它协助解决; + - 仍无法解决 → **跳过当前任务,继续执行后续步骤**,不要卡住整个 HEARTBEAT + +期间不可: + - ❌ 呼唤用户协助解决,HEARTBEAT 在深夜执行,喊用户也没用 + - ❌ 不可中断任务,通过以上三步依然无法进行的任务则跳过,继续执行后续步骤,绝对不允许中断HEARTBEAT! + +3. **Subagent 产出校验与重试**:每次 spawn subagent 后,收到其返回结果时,必须按以下规则判定: + + - ❌ **无效产出**:返回内容为空、仅包含错误信息、不包含任何实质性产出(如文章正文、发布确认等)、或明确表示任务未完成 + - ✅ **有效产出**:包含预期的实质内容(如完整的文章 markdown、发布成功的确认信息等) + + 若判定为无效产出,**立即重新 spawn 同一个 subagent**(使用相同的 agentId 和 task),最多重试 3 次。每次重试前检查是否因浏览器问题导致(如超时、连接断开),若是则先彻底关闭浏览器再重试。 + + 3次均失败后,**跳过当前步骤,继续后续流程**,返回最终报告「XX 步骤:重试 3 次均失败,原因:xxx」。 + +4. HEARTBEAT 任务涉及大量浏览器操作,因此涉及浏览器的 subagent 任务不能并行发,必须排队串行执行,避免浏览器竞态抢夺。 + +## 当前无定时任务 + +当前:回复 `HEARTBEAT_OK` \ No newline at end of file diff --git a/addons/officials/crew/selfmedia-operator/openclaw_setting_sample.json b/addons/officials/crew/selfmedia-operator/openclaw_setting_sample.json index 8b1313bc..a1a80ad5 100644 --- a/addons/officials/crew/selfmedia-operator/openclaw_setting_sample.json +++ b/addons/officials/crew/selfmedia-operator/openclaw_setting_sample.json @@ -14,8 +14,7 @@ "pexels-footage", "pixabay-footage", "smart-search", - "browser-guide", - "council" + "browser-guide" ], "subagents": { "allowAgents": ["it-engineer", "designer"] diff --git a/addons/officials/skills/ppt-maker/SKILL.md b/addons/officials/skills/ppt-maker/SKILL.md index c4dc0162..80b95e80 100644 --- a/addons/officials/skills/ppt-maker/SKILL.md +++ b/addons/officials/skills/ppt-maker/SKILL.md @@ -8,13 +8,6 @@ metadata: bins: - python3 primaryEnv: SILICONFLOW_API_KEY - install: - - id: brew - kind: brew - formula: libreoffice - bins: - - libreoffice - label: Install LibreOffice (brew) --- # PPT Maker @@ -29,10 +22,10 @@ metadata: ## 依赖 ```bash -pip install python-pptx +pip install python-pptx pillow ``` -本脚本依赖 `python-pptx`。若未安装,脚本会提示安装命令。 +本脚本依赖 `python-pptx`;建议安装 `pillow` 以便生成时保持图片比例。若 `python-pptx` 未安装,脚本会提示安装命令。 ## 工作流 @@ -66,16 +59,21 @@ pip install python-pptx └────────┬──────────┘ │ ▼ -┌───────────────────────┐ -│ Step 5: 视觉校验 │ ← 逐页转 PNG,LLM 视觉模型审阅排版 -│ (必须执行) │ ← 检查字体大小、布局偏移、元素重叠等 -│ │ ← 发现问题 → 修复 JSON → 重新生成 → 再校验 -└────────┬──────────────┘ - │ - ▼ 交付 PPTX 文件 ``` +### 必须先写版式契约 + +生成前为每页写出最小版式契约,不能只写“左文右图”这种粗略描述: + +- `safe_area`:若模板有 logo/活动标题/页眉,正文标题必须从页眉安全区以下开始(通常 `top >= 1.2in`)。 +- `content_grid`:定义每页的列中心、图片高度、标题基线、说明文字基线。 +- `asset_fit`:每张图必须使用 `contain` 或 `cover`,禁止默认拉伸;人物组图优先同高、同 top、同 caption baseline。 +- `reading_order`:先标题,再解释句,再主视觉;不要把补充说明压到图片上。 +- `risk_items`:列出本页最容易错的对象,如二维码、截图、长标题、三列人物图、结束页金句。 + +**硬规则**:同一页中承担同一角色的图片/卡片/标题,必须共享精确尺寸或精确中心线。不能靠“看起来差不多”。 + ### Step 1: 风格分析 #### 1a. 用户提供了模板 PPTX @@ -171,13 +169,13 @@ python3 {baseDir}/../siliconflow-img-gen/scripts/gen.py \ **生成后必须验证**:检查图片不是纯色白板(siliconflow 偶发异常),异常则重试(最多 3 次)。 -#### 使用素材图库(可选,后续集成) +#### 使用素材图库(可选) ``` -# pexels-footage(计划中) +# pexels-footage python3 {baseDir}/../pexels-footage/scripts/search.py --query "business meeting" --orientation landscape -# pixabay-footage(计划中) +# pixabay-footage python3 {baseDir}/../pixabay-footage/scripts/search.py --query "technology abstract" --orientation horizontal ``` @@ -234,68 +232,50 @@ python3 {baseDir}/scripts/generate_pptx.py \ --template /path/to/template.pptx ``` -### Step 5: 视觉校验(必须执行) - -PPTX 生成后,**必须逐页转为 PNG 图片**,用视觉模型审阅排版是否正确。这是自动化流程无法保证排版质量的关键环节。 +#### 版式机械检查(必跑) -#### 5a. 逐页转 PNG +生成后立刻运行几何检查,先修机器能确定的问题,再做视觉判断: ```bash -python3 {baseDir}/scripts/pptx_to_png.py \ - --input ./output/presentation.pptx \ - --outdir ./tmp/ppt-pngs +python3 {baseDir}/scripts/check_layout.py ./output/presentation.pptx \ + --header-safe-top-in 1.1 ``` -此脚本依赖系统安装 LibreOffice(`libreoffice-impress`)。若未安装,脚本会给出安装提示。 - -转换完成后,`./tmp/ppt-pngs/` 目录下会生成 `Slide1.png`、`Slide2.png`... 每个文件对应一页幻灯片。 - -#### 5b. 逐页视觉审阅 - -**用 Read 工具打开每一张 PNG 图片**,逐页检查以下项目: - -| 检查项 | 关注点 | 判定标准 | -|--------|--------|---------| -| **整体偏移** | 所有内容是否整体偏上/偏下/偏左/偏右 | 页边距均匀,内容居中 | -| **字体大小** | 标题/正文是否过大(撑爆)或过小(看不清) | 标题约 28-44pt,正文约 16-20pt,视觉比例协调 | -| **文字截断** | 长文本是否超出幻灯片边界被截断 | 文字完整显示,左右留白充足 | -| **元素重叠** | 文本框/图片/装饰条是否互相遮挡 | 各元素有清晰边界,不重叠 | -| **图片质量** | 配图是否正常显示、比例是否变形 | 图片清晰,横纵比正确,无占位符裸露 | -| **配色可读性** | 文字与背景对比度是否足够 | 深底浅字或浅底深字,能轻松阅读 | -| **整体感** | 各页风格是否统一,排版是否专业 | 风格一致,无突兀差异 | +检查不通过时不要交付,必须回到 JSON/脚本修正后重新生成。这个脚本会抓: -**审阅流程**: +- 标题/大字号文本进入页眉安全区 +- 文本框互相重叠或疑似溢出 +- 图片被拉伸变形 +- 三张及以上同组图片 top/height/spacing 不一致 +- 对象超出页面边界或被裁切 -1. 用 `Read` 工具打开第一张 `Slide1.png`(封面),检查标题位置、背景图、强调线 -2. 依次打开后续每张 PNG,逐页检查 -3. 对每页记录:`PASS` 或 `FAIL(原因:xxx)` -4. 所有页面 `PASS` 才可进入交付步骤 +若视觉设计确实需要例外,必须在交付说明里写清楚,并确认渲染图没有真实缺陷。 -#### 5c. 发现问题时 +#### 视觉复查必须输出 defect list -若任意页面 `FAIL`,按以下流程修复: +不要只写“整体还行/更协调了”。视觉复查必须按页输出缺陷表: -1. **分析根因**:定位是 JSON 配置的问题(字号/位置参数不对)还是脚本渲染逻辑问题 -2. **修复 JSON 配置**:调整对应 slide 的配置参数(字号、位置、颜色等) -3. **重新生成**:运行 `generate_pptx.py` 重新生成 .pptx -4. **重新校验**:再次执行 5a → 5b,直到所有页面 `PASS` -5. **最多重试 3 轮**:若 3 轮后仍有问题,记录具体问题告知用户,不可无限循环 +| slide | defect | fix | status | +|-------|--------|-----|--------| +| 02 | 标题压到 logo/页眉线 | 标题下移到 safe area | fixed | +| 04 | 三张人物图 top 不齐 | 统一图片 top 和 height | fixed | -#### 5d. 常见问题速查 +视觉复查的硬失败项: -| 现象 | 可能原因 | 修复方法 | -|------|---------|---------| -| 所有内容整体偏下 | 幻灯片高度设置问题或模板母版偏移 | 检查 slide_height,或在 JSON 中统一调整各元素的 top 值 | -| 字体超大撑爆页面 | 字号参数过大 | 降低 `font_size`,封面标题不超过 44,正文不超过 20 | -| 中文显示方框 | 字体不支持中文 | 将 `heading_font`/`body_font` 改为 `"Microsoft YaHei"` 或 `"SimHei"` | -| 图片遮挡文字 | image_position 与实际布局冲突 | 改用 `"bottom"` 位置或将图片缩小 | -| 长列表溢出底部 | bullets 条数过多 | 拆分为多页,或缩小字号/行距 | +- 标题贴到 logo、页眉线、页面边缘 +- 文字与文字、文字与图、字幕与截图互相压住 +- 同组图片不等高、不等宽、列中心不均匀、说明文字 baseline 不齐 +- 图片比例失真、人物被不自然裁切、截图太小无法辨认 +- 二维码贴边、被裁切、或没有足够留白 +- 结束页金句/感谢语/链接层级混在一起 -### Step 6: 交付 +### Step 5: 交付 -1. 确认 `.pptx` 文件已生成,视觉校验全部通过 -2. 告知用户文件路径、幻灯片数量、使用的风格 -3. 清理临时文件(`./tmp/ppt-images/`、`./tmp/ppt-pngs/` 和 `./tmp/slides-config.json`),除非用户要求保留 +1. 确认 `.pptx` 文件已生成且大小合理(非空) +2. 运行 `scripts/check_layout.py`,结果必须为 `ok` +3. 渲染或截图全 deck contact sheet,逐页写 defect list,并修到没有硬失败项 +4. 告知用户文件路径 +5. 清理临时文件(`./tmp/ppt-images/` 和 `./tmp/slides-config.json`),除非用户要求保留 ## 幻灯片类型速查 @@ -313,9 +293,13 @@ python3 {baseDir}/scripts/pptx_to_png.py \ ## 反模式 -- 不要在生成 PPTX 后跳过视觉校验(Step 5),这是最常见的排版质量问题来源 - 不要在用 `siliconflow-img-gen` 生成图片时设置 inline env var(`SILICONFLOW_API_KEY=... python3 ...`),API key 已在系统环境中 - 不要跳过图片生成后的验证步骤(纯色白板检查) +- 不要跳过 `scripts/check_layout.py` +- 不要用“视觉分析了一遍”替代逐页 defect list +- 不要把标题放进模板页眉/logo 区域 +- 不要拉伸图片来填满框;人物、截图、二维码都必须保持原始比例 +- 不要让同组图片靠手工拖拽;必须使用统一坐标、尺寸和基线 - 每页 slides JSON 必须包含 `type` 字段 - 颜色值不要带 `#` 前缀(python-pptx 要求) - 图片路径使用绝对路径或相对于脚本执行目录的正确相对路径 @@ -327,6 +311,8 @@ python3 {baseDir}/scripts/pptx_to_png.py \ - [ ] 配图已生成并验证(非纯色白板) - [ ] JSON 配置已写为临时文件 - [ ] PPTX 已成功生成且文件非空 -- [ ] **视觉校验已通过(逐页 PNG 审阅,所有页面 PASS)** +- [ ] 已运行 `scripts/check_layout.py` 且无 error +- [ ] 已渲染/截图 contact sheet 并逐页记录 defect list +- [ ] 同组图片、二维码、截图、结束页层级已人工复查 - [ ] 临时文件已清理(除非用户要求保留) - [ ] 交付时说明文件路径、幻灯片数量、使用的模板/风格 diff --git a/addons/officials/skills/ppt-maker/references/slide-layouts.md b/addons/officials/skills/ppt-maker/references/slide-layouts.md index cac4b295..d6665b79 100644 --- a/addons/officials/skills/ppt-maker/references/slide-layouts.md +++ b/addons/officials/skills/ppt-maker/references/slide-layouts.md @@ -62,11 +62,13 @@ ppt-maker 支持的幻灯片布局类型与 JSON 配置示例。 "客户留存率维持在 92%,高于行业平均" ], "image": "/path/to/chart.png", - "image_position": "right" + "image_position": "right", + "image_fit": "contain" } ``` - `image_position`:`"right"`(默认)、`"left"`、`"bottom"` +- `image_fit`:`"contain"` 保持完整图片且不变形;`"cover"` 允许裁切但不变形;禁止为了填满区域直接拉伸 - `body` 与 `bullets` 可同时使用,body 在前,bullets 在后 ### 4. two_column — 双栏对比 @@ -135,3 +137,13 @@ ppt-maker 支持的幻灯片布局类型与 JSON 配置示例。 2. siliconflow-img-gen 16:9 尺寸为 `1664x928` 3. 生成后必须验证图片不是纯色白板(siliconflow 偶发异常) 4. 图片文件名与幻灯片索引对应(如 `slide_01_bg.png`) + +## 多图排列规则 + +三张及以上同组图片必须使用固定网格: + +- 先定义列中心,例如 16:9 页面三列可用 `2.45in / 6.67in / 10.90in` +- 同组人物图统一 `top`、`height`,按图片原始比例计算 `width` +- 同组标题统一 `top`、`height`、字号、对齐方式 +- 同组 caption 统一 baseline,不能一个贴图、一个离图很远 +- 生成后运行 `scripts/check_layout.py`,若报告 `picture-group-*` 必须修正 diff --git a/addons/officials/skills/ppt-maker/scripts/check_layout.py b/addons/officials/skills/ppt-maker/scripts/check_layout.py new file mode 100644 index 00000000..177d50b2 --- /dev/null +++ b/addons/officials/skills/ppt-maker/scripts/check_layout.py @@ -0,0 +1,301 @@ +#!/usr/bin/env python3 +"""Deterministic PPTX layout checks for obvious visual defects. + +This catches the failure modes a visual pass often hand-waves away: +title/header collisions, text-box overlaps, text likely overflowing its box, +misaligned repeated picture groups, distorted images, and off-slide objects. +""" + +from __future__ import annotations + +import argparse +import json +import math +import posixpath +import struct +import sys +import zipfile +from dataclasses import dataclass +from pathlib import Path +from xml.etree import ElementTree as ET + + +NS = { + "a": "http://schemas.openxmlformats.org/drawingml/2006/main", + "p": "http://schemas.openxmlformats.org/presentationml/2006/main", + "r": "http://schemas.openxmlformats.org/officeDocument/2006/relationships", + "rel": "http://schemas.openxmlformats.org/package/2006/relationships", +} +EMU_PER_INCH = 914400 + + +@dataclass(frozen=True) +class Box: + x: float + y: float + w: float + h: float + + @property + def right(self) -> float: + return self.x + self.w + + @property + def bottom(self) -> float: + return self.y + self.h + + @property + def center_x(self) -> float: + return self.x + self.w / 2 + + @property + def area(self) -> float: + return max(0.0, self.w) * max(0.0, self.h) + + +def read_xml(zf: zipfile.ZipFile, path: str) -> ET.Element: + return ET.fromstring(zf.read(path)) + + +def rels_path_for(part_path: str) -> str: + folder, name = posixpath.split(part_path) + return posixpath.join(folder, "_rels", f"{name}.rels") + + +def load_rels(zf: zipfile.ZipFile, part_path: str) -> dict[str, str]: + rels_path = rels_path_for(part_path) + if rels_path not in zf.namelist(): + return {} + root = read_xml(zf, rels_path) + base = posixpath.dirname(part_path) + out: dict[str, str] = {} + for rel in root.findall("rel:Relationship", NS): + rel_id = rel.attrib.get("Id") + target = rel.attrib.get("Target", "") + if rel_id: + out[rel_id] = posixpath.normpath(posixpath.join(base, target)) + return out + + +def slide_order(zf: zipfile.ZipFile) -> list[str]: + rels = load_rels(zf, "ppt/presentation.xml") + root = read_xml(zf, "ppt/presentation.xml") + ordered = [] + for node in root.findall(".//p:sldId", NS): + rid = node.attrib.get(f"{{{NS['r']}}}id") + if rid in rels: + ordered.append(rels[rid]) + return ordered + + +def slide_size(zf: zipfile.ZipFile) -> tuple[float, float]: + root = read_xml(zf, "ppt/presentation.xml") + sld_sz = root.find(".//p:sldSz", NS) + if sld_sz is None: + return 13.333, 7.5 + return int(sld_sz.attrib["cx"]) / EMU_PER_INCH, int(sld_sz.attrib["cy"]) / EMU_PER_INCH + + +def shape_box(node: ET.Element) -> Box | None: + xfrm = node.find(".//a:xfrm", NS) + if xfrm is None: + return None + off = xfrm.find("a:off", NS) + ext = xfrm.find("a:ext", NS) + if off is None or ext is None: + return None + return Box( + int(off.attrib.get("x", 0)) / EMU_PER_INCH, + int(off.attrib.get("y", 0)) / EMU_PER_INCH, + int(ext.attrib.get("cx", 0)) / EMU_PER_INCH, + int(ext.attrib.get("cy", 0)) / EMU_PER_INCH, + ) + + +def node_name(node: ET.Element) -> str: + c_nv_pr = node.find(".//p:cNvPr", NS) + return c_nv_pr.attrib.get("name", "") if c_nv_pr is not None else "" + + +def pic_descr(node: ET.Element) -> str: + c_nv_pr = node.find(".//p:cNvPr", NS) + return c_nv_pr.attrib.get("descr", "") if c_nv_pr is not None else "" + + +def text_of(node: ET.Element) -> str: + return "".join(t.text or "" for t in node.findall(".//a:t", NS)).strip() + + +def max_font_size_pt(node: ET.Element) -> float: + sizes = [] + for rpr in node.findall(".//a:defRPr", NS) + node.findall(".//a:rPr", NS): + if "sz" in rpr.attrib: + sizes.append(int(rpr.attrib["sz"]) / 100) + return max(sizes) if sizes else 18.0 + + +def overlap_area(a: Box, b: Box) -> float: + x = max(0.0, min(a.right, b.right) - max(a.x, b.x)) + y = max(0.0, min(a.bottom, b.bottom) - max(a.y, b.y)) + return x * y + + +def image_size_from_bytes(data: bytes) -> tuple[int, int] | None: + if data.startswith(b"\x89PNG\r\n\x1a\n") and len(data) >= 24: + return struct.unpack(">II", data[16:24]) + if data.startswith(b"\xff\xd8"): + idx = 2 + while idx + 9 < len(data): + if data[idx] != 0xFF: + idx += 1 + continue + marker = data[idx + 1] + idx += 2 + if marker in (0xD8, 0xD9): + continue + length = int.from_bytes(data[idx : idx + 2], "big") + if marker in (0xC0, 0xC1, 0xC2, 0xC3, 0xC5, 0xC6, 0xC7, 0xC9, 0xCA, 0xCB, 0xCD, 0xCE, 0xCF): + h = int.from_bytes(data[idx + 3 : idx + 5], "big") + w = int.from_bytes(data[idx + 5 : idx + 7], "big") + return w, h + idx += length + return None + + +def pic_target(node: ET.Element, rels: dict[str, str]) -> str | None: + blip = node.find(".//a:blip", NS) + if blip is None: + return None + rid = blip.attrib.get(f"{{{NS['r']}}}embed") + return rels.get(rid) if rid else None + + +def likely_text_overflows(text: str, box: Box, font_size: float) -> bool: + if not text or box.w <= 0 or box.h <= 0: + return False + avg_char_w = max(0.07, font_size * 0.0062) + chars_per_line = max(4, int(box.w / avg_char_w)) + manual_lines = text.count("\n") + 1 + wrapped_lines = sum(max(1, math.ceil(len(line) / chars_per_line)) for line in text.splitlines() or [text]) + line_count = max(manual_lines, wrapped_lines) + required_h = line_count * font_size / 72 * 1.15 + return required_h > box.h * 1.15 + + +def inspect_pptx(path: Path, header_safe_top_in: float, fail_on_warning: bool) -> dict: + findings: list[dict] = [] + with zipfile.ZipFile(path) as zf: + width, height = slide_size(zf) + slides = slide_order(zf) + for idx, slide_path in enumerate(slides, 1): + root = read_xml(zf, slide_path) + rels = load_rels(zf, slide_path) + nodes = list(root.findall(".//p:spTree/*", NS)) + text_shapes = [] + pictures = [] + for node in nodes: + box = shape_box(node) + if box is None: + continue + label = node_name(node) or pic_descr(node) or node.tag.rsplit("}", 1)[-1] + if box.x < -0.01 or box.y < -0.01 or box.right > width + 0.01 or box.bottom > height + 0.01: + findings.append({"slide": idx, "level": "error", "code": "off-slide-object", "object": label}) + if node.tag == f"{{{NS['p']}}}sp": + text = text_of(node) + if text: + size = max_font_size_pt(node) + text_shapes.append((label, text, box, size)) + if size >= 24 and box.y < header_safe_top_in and idx != 1: + findings.append( + { + "slide": idx, + "level": "error", + "code": "title-in-header-safe-zone", + "object": label, + "text": text[:60], + } + ) + if likely_text_overflows(text, box, size): + findings.append( + { + "slide": idx, + "level": "warning" if not fail_on_warning else "error", + "code": "possible-text-overflow", + "object": label, + "text": text[:60], + } + ) + elif node.tag == f"{{{NS['p']}}}pic": + pictures.append((label, box, node)) + target = pic_target(node, rels) + if target and target in zf.namelist(): + natural = image_size_from_bytes(zf.read(target)) + if natural: + src_ratio = natural[0] / natural[1] + box_ratio = box.w / box.h if box.h else src_ratio + if abs(src_ratio - box_ratio) / src_ratio > 0.08: + findings.append( + { + "slide": idx, + "level": "error", + "code": "image-aspect-distorted", + "object": label, + "source_ratio": round(src_ratio, 3), + "box_ratio": round(box_ratio, 3), + } + ) + + for a_i, (a_label, a_text, a_box, _a_size) in enumerate(text_shapes): + for b_label, b_text, b_box, _b_size in text_shapes[a_i + 1 :]: + area = overlap_area(a_box, b_box) + if area > min(a_box.area, b_box.area) * 0.08: + findings.append( + { + "slide": idx, + "level": "error", + "code": "text-box-overlap", + "objects": [a_label, b_label], + "texts": [a_text[:40], b_text[:40]], + } + ) + + content_pics = [(label, box) for label, box, _node in pictures if box.y > header_safe_top_in] + if len(content_pics) >= 3: + tops = [box.y for _label, box in content_pics] + heights = [box.h for _label, box in content_pics] + centers = sorted(box.center_x for _label, box in content_pics) + gaps = [centers[i + 1] - centers[i] for i in range(len(centers) - 1)] + repeated_group = min(heights) > 0 and max(heights) / min(heights) < 1.25 + if repeated_group: + if max(tops) - min(tops) > 0.10: + findings.append({"slide": idx, "level": "error", "code": "picture-group-top-misaligned"}) + if max(heights) - min(heights) > 0.14: + findings.append({"slide": idx, "level": "warning" if not fail_on_warning else "error", "code": "picture-group-height-mismatch"}) + if len(gaps) >= 2 and max(gaps) - min(gaps) > 0.35: + findings.append({"slide": idx, "level": "warning" if not fail_on_warning else "error", "code": "picture-group-uneven-spacing"}) + + errors = [item for item in findings if item["level"] == "error"] + return {"ok": not errors, "error_count": len(errors), "finding_count": len(findings), "findings": findings} + + +def main() -> None: + parser = argparse.ArgumentParser(description="Check PPTX layout geometry") + parser.add_argument("pptx", type=Path) + parser.add_argument("--header-safe-top-in", type=float, default=0.9) + parser.add_argument("--fail-on-warning", action="store_true") + parser.add_argument("--json", action="store_true") + args = parser.parse_args() + + result = inspect_pptx(args.pptx, args.header_safe_top_in, args.fail_on_warning) + if args.json: + print(json.dumps(result, ensure_ascii=False, indent=2)) + else: + status = "ok" if result["ok"] else "failed" + print(f"[{status}] {result['error_count']} error(s), {result['finding_count']} finding(s)") + for item in result["findings"]: + print(json.dumps(item, ensure_ascii=False)) + sys.exit(0 if result["ok"] else 1) + + +if __name__ == "__main__": + main() diff --git a/addons/officials/skills/ppt-maker/scripts/generate_pptx.py b/addons/officials/skills/ppt-maker/scripts/generate_pptx.py index a5c9e58e..3e4bf33f 100644 --- a/addons/officials/skills/ppt-maker/scripts/generate_pptx.py +++ b/addons/officials/skills/ppt-maker/scripts/generate_pptx.py @@ -3,7 +3,7 @@ generate_pptx.py — Generate .pptx from a JSON slide configuration. Dependencies: - pip install python-pptx + pip install python-pptx pillow Usage: python3 generate_pptx.py --config slides.json --output output.pptx @@ -195,13 +195,47 @@ def _add_accent_bar(slide, left, top, width, height, color: str): return shape -def _add_image_safe(slide, image_path: str, left, top, width, height): - """Add image if file exists, otherwise add placeholder.""" +def _fit_image_box(image_path: str, left, top, width, height, fit: str = "contain"): + """Return a placement box that preserves the source image aspect ratio.""" + if fit == "stretch": + return left, top, width, height + try: + from PIL import Image + + with Image.open(image_path) as img: + src_w, src_h = img.size + src_ratio = src_w / src_h + box_ratio = width / height + if fit == "cover": + if src_ratio > box_ratio: + fitted_h = height + fitted_w = int(height * src_ratio) + else: + fitted_w = width + fitted_h = int(width / src_ratio) + else: + if src_ratio > box_ratio: + fitted_w = width + fitted_h = int(width / src_ratio) + else: + fitted_h = height + fitted_w = int(height * src_ratio) + fitted_left = left + int((width - fitted_w) / 2) + fitted_top = top + int((height - fitted_h) / 2) + return fitted_left, fitted_top, fitted_w, fitted_h + except Exception: + return left, top, width, height + + +def _add_image_safe(slide, image_path: str, left, top, width, height, fit: str = "contain"): + """Add image if file exists, preserving aspect ratio by default.""" from pptx.dml.color import RGBColor + from pptx.util import Pt if image_path and os.path.exists(image_path): try: - slide.shapes.add_picture(image_path, left, top, width, height) + img_left, img_top, img_w, img_h = _fit_image_box(image_path, left, top, width, height, fit) + slide.shapes.add_picture(image_path, img_left, img_top, img_w, img_h) return except Exception: pass @@ -235,7 +269,7 @@ def _build_cover(slide, cfg: dict, theme: dict) -> None: # Background image (optional, for AI-generated cover images) bg_image = cfg.get("background_image", "") if bg_image: - _add_image_safe(slide, bg_image, Inches(0), Inches(0), Inches(13.333), Inches(7.5)) + _add_image_safe(slide, bg_image, Inches(0), Inches(0), Inches(13.333), Inches(7.5), fit="cover") # Semi-transparent overlay via dark rectangle with lower opacity would be ideal, # but python-pptx has limited opacity support. Use a dark shape as mask. overlay = slide.shapes.add_shape(1, Inches(0), Inches(0), Inches(13.333), Inches(7.5)) @@ -393,6 +427,7 @@ def _build_content(slide, cfg: dict, theme: dict) -> None: # Image handling image = cfg.get("image", "") image_position = cfg.get("image_position", "right") + image_fit = cfg.get("image_fit", "contain") has_image = image and os.path.exists(image) if has_image: @@ -441,7 +476,7 @@ def _build_content(slide, cfg: dict, theme: dict) -> None: # Image if has_image: - _add_image_safe(slide, image, img_left, img_top, img_w, img_h) + _add_image_safe(slide, image, img_left, img_top, img_w, img_h, fit=image_fit) def _build_two_column(slide, cfg: dict, theme: dict) -> None: @@ -510,7 +545,8 @@ def _build_image_full(slide, cfg: dict, theme: dict) -> None: from pptx.util import Inches image = cfg.get("image", "") - _add_image_safe(slide, image, Inches(0), Inches(0), Inches(13.333), Inches(7.5)) + image_fit = cfg.get("image_fit", "cover") + _add_image_safe(slide, image, Inches(0), Inches(0), Inches(13.333), Inches(7.5), fit=image_fit) # Optional overlay text at bottom caption = cfg.get("caption", "") diff --git a/addons/officials/skills/xianyu-ops/SKILL.md b/addons/officials/skills/xianyu-ops/SKILL.md new file mode 100644 index 00000000..9964ac7e --- /dev/null +++ b/addons/officials/skills/xianyu-ops/SKILL.md @@ -0,0 +1,135 @@ +--- +name: xianyu-ops +description: 闲鱼(goofish.com)商品搜索、查看详情、私信会话管理与回复。当用户要求在闲鱼上搜索商品、查看宝贝、读取或回复私信时触发。 +metadata: + openclaw: + emoji: 🐟 +--- + +# 闲鱼操作 + +通过 **browser 工具** 在闲鱼(goofish.com)上完成商品搜索、详情查看、私信管理。 + +**前提条件**:需要先通过 browser 工具登录闲鱼(在 Chrome 中打开 goofish.com 扫码登录),browser 已持有有效 session。 + +--- + +## 必做约束 + +- 每次操作之间保持 3-5 秒间隔,避免风控触发验证码。 +- 发送私信时不要连续发送超过 10 条,每条间隔 30 秒以上。 +- 出现"请先登录"、"验证码"、"安全验证"、"异常访问"等提示时,立即停止操作并告知用户需要重新登录。 + +--- + +## 登录状态检测 + +在页面加载后,检查页面是否出现以下关键词,命中则说明需要重新登录或被风控: + +``` +请先登录 / 登录后 → 需要重新登录 +验证码 / 安全验证 / 异常访问 / 访问过于频繁 → 被风控,停止操作 +``` + +--- + +## URL 格式 + +| 页面 | URL | +|------|-----| +| 商品搜索 | `https://www.goofish.com/search?q={关键词}` | +| 商品详情 | `https://www.goofish.com/item?id={item_id}` | +| 私信列表 | `https://www.goofish.com/im` | +| 私信会话 | `https://www.goofish.com/im?itemId={item_id}&peerUserId={user_id}` | +| 发布页 | `https://www.goofish.com/publish` | + +--- + +## 工作流程 + +### 搜索商品 + +``` +1. 导航到搜索页:https://www.goofish.com/search?q={关键词} +2. 等待页面加载(3-4 秒) +3. 执行登录状态检测 +4. 提取搜索结果:商品卡片选择器 a[href*="/item?id="] + - 标题:[class*="row1-wrap-title"] 或 [class*="main-title"] + - 价格:[class*="price-wrap"] 内 [class*="number"] + [class*="decimal"] + - 原价:[class*="price-desc"] 内 [title] 或 [style*="line-through"] + - 成色/品牌:[class*="row2-wrap-cpv"] span[class*="cpv--"] + - 地区:[class*="row4-wrap-seller"] [class*="seller-text"] + - 信用标签:[class*="credit-container"] [title] 或 span +5. 从卡片 href 中提取 item_id(匹配 [?&]id=(\d+)) +``` + +### 查看商品详情 + +``` +1. 导航到详情页:https://www.goofish.com/item?id={item_id} +2. 等待页面加载(2-3 秒) +3. 执行登录状态检测 +4. 通过 mtop 接口获取详情(在浏览器控制台执行): + window.lib.mtop.request({ + api: 'mtop.taobao.idle.pc.detail', + data: { itemId: '{item_id}' }, + type: 'POST', v: '1.0', dataType: 'json', + needLogin: false, needLoginPC: false, + sessionOption: 'AutoLoginOnly', ecode: 0 + }) +5. 从返回的 data 中提取: + - itemDO.title / itemDO.desc / itemDO.soldPrice / itemDO.originalPrice + - itemDO.wantCnt / itemDO.collectCnt / itemDO.browseCnt + - itemDO.itemLabelExtList → 找 propertyText="成色"/"品牌"/"分类" 对应的 text + - itemDO.imageInfos → 图片 URL 列表 + - sellerDO.nick / sellerDO.sellerId / sellerDO.publishCity + - sellerDO.xianyuSummary / sellerDO.replyRatio24h +6. 若 mtop 不可用(window.lib.mtop 未就绪),改用 DOM 提取页面上的可见信息 +``` + +### 查看私信列表 + +``` +1. 导航到私信页:https://www.goofish.com/im +2. 等待页面加载(4-5 秒) +3. 执行登录状态检测 +4. 提取会话列表:每个会话包含 + - 对方昵称、商品标题、价格、最后一条消息 + - 未读标记、未读数量 + - 点击会话可从 URL 中解析 itemId 和 peerUserId +5. 若需获取完整 item_id / peer_user_id,逐个点击会话,从跳转 URL 中提取 +``` + +### 读取私信内容 + +``` +1. 导航到会话页(两种方式): + - 已知 item_id + user_id:https://www.goofish.com/im?itemId={item_id}&peerUserId={user_id} + - 已在私信列表页:点击目标会话 +2. 等待页面加载(2-3 秒) +3. 确认聊天输入框存在(can_input 为 true) +4. 提取可见消息列表 +``` + +### 发送私信 / 回复 + +``` +1. 导航到会话页(同"读取私信内容"步骤 1-3) +2. 在聊天输入框中输入消息文本 +3. 触发 input 事件 +4. 点击发送按钮 +5. 等待 1-2 秒,确认消息已出现在聊天区域 +``` + +--- + +## 错误处理 + +| 情况 | 处理 | +|------|------| +| 页面出现登录墙 | 停止操作,告知用户需在 goofish.com 重新扫码登录 | +| 触发验证码/风控 | 停止操作,建议用户手动访问 goofish.com 完成验证后重试 | +| mtop 接口不可用 | 改用 DOM 提取页面可见信息 | +| mtop 返回 SESSION_EXPIRED | 需要重新登录 | +| 商品不存在 | 检查 item_id 是否正确 | +| 聊天输入框不可用 | 确认会话页已正确加载,重试一次 | diff --git a/config-templates/openclaw-aihubmix.json b/config-templates/openclaw-aihubmix.json index 6a7500b9..bf63af38 100644 --- a/config-templates/openclaw-aihubmix.json +++ b/config-templates/openclaw-aihubmix.json @@ -66,7 +66,8 @@ "maxConcurrent": 4, "subagents": { "maxConcurrent": 1, - "announceTimeoutMs": 3600000 + "announceTimeoutMs": 3600000, + "maxSpawnDepth": 3 } }, "list": [ diff --git a/config-templates/openclaw.json b/config-templates/openclaw.json index 394e20eb..5912a32a 100644 --- a/config-templates/openclaw.json +++ b/config-templates/openclaw.json @@ -140,7 +140,8 @@ "maxConcurrent": 4, "subagents": { "maxConcurrent": 1, - "announceTimeoutMs": 3600000 + "announceTimeoutMs": 3600000, + "maxSpawnDepth": 3 } }, "list": [ diff --git a/crews/main/AGENTS.md b/crews/main/AGENTS.md index 8ed062cd..f415b81f 100644 --- a/crews/main/AGENTS.md +++ b/crews/main/AGENTS.md @@ -71,9 +71,12 @@ Main Agent manages its recruited team (excluding built-in protected agents): 1. Understand business need: role, capabilities, route mode 2. Present proposal to user (L3) 3. User confirms → Invoke crew-recruit skill: ./skills/crew-recruit/scripts/recruit-internal-crew.sh [--template ] [--bind :] -4. Confirm creation and remind to restart Gateway +4. Script succeeds → Ask user to confirm Gateway restart (mandatory, see crew-recruit skill for reason) +5. User confirms → systemctl --user restart openclaw-gateway ``` +> ⚠️ 步骤 4 的 Gateway 重启**不可跳过**。openclaw 热加载对 `bindings` 变更不触发 channel 重启,feishu channel 使用启动时的配置快照进行路由。不重启会导致新 crew 的消息被错误路由到 `agent:main`。 + ### Dismiss Member ``` 1. Identify target from team roster @@ -82,7 +85,8 @@ Main Agent manages its recruited team (excluding built-in protected agents): 4. User confirms (L3 — mandatory) 5. Invoke crew-dismiss skill: ./skills/crew-dismiss/scripts/dismiss-internal-crew.sh 6. Update MEMORY.md roster -7. Remind to restart Gateway +7. Ask user to confirm Gateway restart (same reason as recruit — bindings stale after hot-reload) +8. User confirms → systemctl --user restart openclaw-gateway ``` > ⚠️ **始终通过 skill 脚本执行团队管理操作**,不要手动拼装 shell 命令。 diff --git a/crews/main/skills/crew-dismiss/scripts/dismiss-internal-crew.sh b/crews/main/skills/crew-dismiss/scripts/dismiss-internal-crew.sh old mode 100755 new mode 100644 diff --git a/crews/main/skills/crew-list/scripts/list-internal-crews.sh b/crews/main/skills/crew-list/scripts/list-internal-crews.sh old mode 100755 new mode 100644 diff --git a/crews/main/skills/crew-recruit/SKILL.md b/crews/main/skills/crew-recruit/SKILL.md index 13ab9c0b..d58fa474 100644 --- a/crews/main/skills/crew-recruit/SKILL.md +++ b/crews/main/skills/crew-recruit/SKILL.md @@ -13,9 +13,12 @@ 2. 确定模板 ID(可选,默认同 agent-id) 3. 向用户展示创建方案,等待用户确认 4. 用户确认后运行脚本 -5. 提醒用户重启 Gateway 激活 +5. 脚本执行成功后,告知用户必须重启 Gateway 才能生效,并询问是否立即重启 +6. 用户确认重启 → 执行 systemctl --user restart openclaw-gateway ``` +> ⚠️ 步骤 5-6 是**必需**的,不可跳过。原因:openclaw 热加载对 `bindings` 变更标记为 `none`(不触发 channel 重启),而 feishu channel 在启动时将配置捕获为快照,不会自动刷新。招募新 crew 后若不重启 gateway,新 crew 的 feishu bot 收到的消息会被错误路由到 `agent:main`。只有重启 gateway 才能让所有 channel 重新加载包含新 bindings 的完整配置。 + ## 脚本用法 ```bash @@ -38,3 +41,21 @@ - workspace 必须事先创建(脚本会检查) - 对内 Crew 使用**继承模式**技能,自动获得基线技能 - 项目级 / addon 全局技能默认不自动继承;需要在目标 workspace 的 `BUILTIN_SKILLS` 中显式声明 + +## 重启 Gateway + +招募完成后**必须重启** gateway,否则新 crew 的渠道路由不会生效。重启前需征得用户确认: + +``` +招募已完成!新 crew 的配置已写入 openclaw.json。 +⚠️ 需要重启 Gateway 才能激活新 crew 的渠道路由(热加载无法刷新 binding 快照)。 +重启期间所有 agent 将短暂中断(约 10-15 秒)。 +是否现在重启?(确认/稍后) +``` + +用户确认后执行: +```bash +systemctl --user restart openclaw-gateway +``` + +用户选择"稍后"时,提醒用户稍后手动执行上述命令。 diff --git a/crews/main/skills/crew-recruit/scripts/recruit-internal-crew.sh b/crews/main/skills/crew-recruit/scripts/recruit-internal-crew.sh old mode 100755 new mode 100644 diff --git a/scripts/apply-addons.sh b/scripts/apply-addons.sh index a9196814..8f353a0b 100755 --- a/scripts/apply-addons.sh +++ b/scripts/apply-addons.sh @@ -474,6 +474,70 @@ else echo "✅ Skill dependencies up to date (hash: ${current_pkg_hash:0:8})" fi +# ─── 安装全仓统一 Python 依赖(pip --user)────────────────────── +# 扫描 skills/、addons/、crews/ 下所有 requirements.txt,合并去重。 +# 内容哈希守卫:仅当依赖集发生变化时才执行 pip install。 +# 优先使用 pip install --user;若不可用则回退 --break-system-packages。 +PIP_HASH_FILE="$OPENCLAW_HOME/.skill-pip-hash" + +merged_pip_deps="$(node -e " + const fs = require('fs'); + const path = require('path'); + const lines = new Set(); + function scan(dir) { + if (!fs.existsSync(dir)) return; + for (const entry of fs.readdirSync(dir, { withFileTypes: true })) { + if (entry.name === 'node_modules' || entry.name === '.git') continue; + const full = path.join(dir, entry.name); + if (entry.isDirectory()) { + scan(full); + } else if (entry.name === 'requirements.txt') { + try { + const content = fs.readFileSync(full, 'utf8'); + content.split(/\\r?\\n/).forEach(line => { + const trimmed = line.trim(); + if (trimmed && !trimmed.startsWith('#')) lines.add(trimmed); + }); + } catch {} + } + } + } + scan('$PROJECT_ROOT/skills'); + scan('$ADDONS_DIR'); + scan('$CREWS_DIR'); + console.log(Array.from(lines).sort().join('\\n')); +" 2>/dev/null || echo '')" + +current_pip_hash="$(echo "$merged_pip_deps" | md5sum | cut -d' ' -f1)" +stored_pip_hash="$(cat "$PIP_HASH_FILE" 2>/dev/null || echo '')" + +if [ -n "$merged_pip_deps" ] && { [ "$current_pip_hash" != "$stored_pip_hash" ] || [ ! -f "$PIP_HASH_FILE" ]; }; then + echo "🐍 Installing skill Python dependencies (pip --user)..." + # 写入合并后的 requirements 文件 + pip_req_tmp="$OPENCLAW_HOME/.skill-requirements.txt" + echo "$merged_pip_deps" > "$pip_req_tmp" + + pip_install_flags="--user --quiet --no-warn-script-location -i https://mirrors.aliyun.com/pypi/simple/ --trusted-host mirrors.aliyun.com" + if ! pip install $pip_install_flags -r "$pip_req_tmp" 2>/dev/null; then + echo " ⚠️ pip --user failed, retrying with --break-system-packages..." + pip_install_flags="--break-system-packages --user --quiet --no-warn-script-location -i https://mirrors.aliyun.com/pypi/simple/ --trusted-host mirrors.aliyun.com" + if ! pip install $pip_install_flags -r "$pip_req_tmp"; then + echo " ❌ pip install failed" >&2 + else + echo "$current_pip_hash" > "$PIP_HASH_FILE" + echo "✅ Python dependencies installed (hash: ${current_pip_hash:0:8})" + fi + else + echo "$current_pip_hash" > "$PIP_HASH_FILE" + echo "✅ Python dependencies installed (hash: ${current_pip_hash:0:8})" + fi + rm -f "$pip_req_tmp" +else + if [ -n "$merged_pip_deps" ]; then + echo "✅ Python dependencies up to date (hash: ${current_pip_hash:0:8})" + fi +fi + # 有 overrides 或 patches 时才需要同步依赖 if [ "$NEEDS_INSTALL" = "true" ]; then echo "📦 Syncing dependencies..." diff --git a/scripts/lib/exec-tiers.sh b/scripts/lib/exec-tiers.sh index 081c13aa..1bf0a7eb 100644 --- a/scripts/lib/exec-tiers.sh +++ b/scripts/lib/exec-tiers.sh @@ -13,7 +13,7 @@ TIER_T1_COMMANDS="cat ls grep find ps date echo pwd env which head tail wc sort # T2: T1 + 开发工具链 # 注意:bash/sh 的全部常见路径均需列出(/bin 和 /usr/bin 在部分系统上是独立路径, # exec 白名单做精确路径匹配,仅靠 command -v 只能解析到其中一条,须全量覆盖) -TIER_T2_EXTRA="git npm pnpm bun node python python3 pip pip3 cp mv mkdir rmdir rm touch chmod sleep /bin/bash /bin/sh /usr/bin/bash /usr/bin/sh" +TIER_T2_EXTRA="git npm pnpm bun node python python3 pip pip3 cp mv mkdir rmdir rm touch chmod sleep test /bin/bash /bin/sh /usr/bin/bash /usr/bin/sh" # T3: full access(无需白名单) # ── 从 SOUL.md 解析 command-tier ────────────────────── diff --git a/skills/browser-guide/SKILL.md b/skills/browser-guide/SKILL.md index 72081eea..1d2d53f2 100644 --- a/skills/browser-guide/SKILL.md +++ b/skills/browser-guide/SKILL.md @@ -1,14 +1,10 @@ --- name: browser-guide -description: Best practices for using the managed browser — handling login walls, CAPTCHAs, lazy-loaded content, paywalls, and tab cleanup. +description: Best practices for using the managed browser — handling login walls, + CAPTCHAs, lazy-loaded content, paywalls, and tab cleanup. metadata: - { - "openclaw": - { - "emoji": "🌐", - "always": true, - } - } + openclaw: + emoji: 🌐 --- # Browser Best Practices @@ -95,7 +91,43 @@ When a page uses lazy loading (infinite scroll, "load more" sections, content th 4. Do NOT scroll too fast, do it as a human would. After 7 times of scrolling, you should stop this turn. 5. If not relevant, skip scrolling and work with what is already loaded. -## 5. Paywall / Subscription Walls +## 5. Browser `evaluate` Action — Expression Only + +When using the browser tool's `evaluate` (or `act` with `kind: "evaluate"`) to run JavaScript in the page context, the `fn` parameter must be a **single expression**, not a statement block. Declarations (`const`, `let`, `var`), semicolons, `for`/`if` statements, and `function` declarations will all cause `Invalid evaluate function` errors. + +**Wrong** (statement block — will fail): +```js +const items = document.querySelectorAll('.msg'); +let found = false; +for (const item of items) { + if (item.textContent.includes('target')) { found = true; break; } +} +found ? 'ok' : 'no'; +``` + +**Correct** (wrap in IIFE): +```js +(function() { + var items = document.querySelectorAll('.msg'); + for (var i = 0; i < items.length; i++) { + if (items[i].textContent.indexOf('target') > -1) { return items[i].innerText; } + } + return 'not found'; +})() +``` + +**Correct** (pure expression, for simple lookups): +```js +document.querySelector('.reply-btn') ? 'found' : 'not found' +``` + +Rules: +- Always wrap multi-step logic in an IIFE: `(function(){ ... })()` +- For DOM queries that only need to click, prefer `click` action on a selector over `evaluate` +- For reading text, prefer `snapshot` over `evaluate` when possible +- Never use `const`/`let`/`var` declarations or `;` at the top level of `fn` + +## 6. Paywall / Subscription Walls When a page indicates that content is behind a paywall or requires a specific subscription (e.g., "Subscribe to continue reading", "Continue reading with a WSJ subscription", premium-only banners): From e84ef1b32fd2dd726e4189c16ef7d2833abdcc21 Mon Sep 17 00:00:00 2001 From: bigbrother666sh Date: Thu, 21 May 2026 15:54:36 +0800 Subject: [PATCH 2/4] =?UTF-8?q?fix:=20=E6=9B=B4=E6=96=B0=E6=95=85=E9=9A=9C?= =?UTF-8?q?=E6=8E=92=E6=9F=A5=E5=92=8C=E5=8D=87=E7=BA=A7=E6=B5=81=E7=A8=8B?= =?UTF-8?q?=E6=96=87=E6=A1=A3=EF=BC=8C=E4=BF=AE=E6=AD=A3=E5=91=BD=E4=BB=A4?= =?UTF-8?q?=E7=A4=BA=E4=BE=8B=E5=8F=8A=E6=A0=BC=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- crews/it-engineer/AGENTS.md | 53 +++--- crews/it-engineer/HEARTBEAT.md | 13 +- crews/it-engineer/TOOLS.md | 47 ++--- ...01-relax-exec-allowlist-shell-syntax.patch | 164 ++++++++++++++++++ scripts/lib/agent-skills.sh | 22 +-- 5 files changed, 217 insertions(+), 82 deletions(-) create mode 100644 patches/001-relax-exec-allowlist-shell-syntax.patch diff --git a/crews/it-engineer/AGENTS.md b/crews/it-engineer/AGENTS.md index be7f205e..55e01951 100644 --- a/crews/it-engineer/AGENTS.md +++ b/crews/it-engineer/AGENTS.md @@ -10,14 +10,15 @@ 此时将"派发方的任务描述 + 错误信息 + 上下文"视为问题输入, 修复完成后继续协助派发方完成其原任务 2. 自主收集信息(无需用户提供): - - 查看进程状态(ps aux | grep openclaw.mjs) + - 查看进程状态(ps aux | grep 'dist/index.js gateway') - 通过 session-logs 技能或直接读取日志文件 - 读取相关 agent workspace 文件了解运行状态 - 查看 ~/.openclaw/openclaw.json 配置 3. 分析报错,定位根因,用大白话理解问题 4. 告知用户:发现了什么问题,准备如何修复 -5. 自主执行修复(L1/L2 直接执行,L3 先确认) -6. 自检验证: +5. 判断是 wiseflow 程序问题还是环境问题/设置或配置问题 +6.1 如果是 wiseflow 程序问题: 需要将初步分析原因和相关证据在 `wiseflow 项目路径`(从`OFB_ENV.md`获取)下创建一个 markdown 文件写入,等待研发修复,**严禁**直接更改 wiseflow 项目代码 +6.2 如果是环境问题/设置或配置问题,自主执行修复,并在修复后自检验证: - 确认进程存活、服务响应正常 - 查看最新日志无新报错 7. 向用户报告结果(问题描述 + 解决方案 + 当前服务状态) @@ -36,17 +37,15 @@ > 升级时不得将工作流内容写入 TOOLS.md,不得将工具指导散落在 AGENTS.md,不得将心跳任务混入其他文件。 -## 升级流程 +## wiseflow 程序升级/更新流程 ``` -1. 收到升级请求 +1. 收到用户的升级通知后,先去 `wiseflow 项目路径`(从`OFB_ENV.md`获取)下查找研发留下的升级指导 markdown文件,仔细阅读后再执行升级任务; 2. ⚠️ 自主检查系统是否空闲(不询问用户,自己执行): ls ~/.openclaw/agents/*/sessions/ 2>/dev/null | head -20 -3. 如果繁忙 → 告知用户当前有活跃会话,建议在空闲时(如下班后)再升级,不执行 -4. 如果空闲 → 告知用户"系统当前空闲,开始执行升级",获得 L3 确认 -5. 用户确认后自主执行: - cd # 路径从 OFB_ENV.md 获取 - ./scripts/upgrade.sh +3. 如果繁忙 → 告知用户当前有活跃会话,建议在空闲时再执行 +4. 如果空闲 → 告知用户"系统当前空闲,开始执行升级" +5. 用户确认后自主执行 6. 观察升级输出,如有报错立即分析处理 7. 升级完成后判断是否需要重启服务(见下方【服务重启流程】) 8. 服务恢复后自检验证(见【服务重启流程】步骤 3) @@ -63,12 +62,12 @@ 2. 执行重启: - openclaw 引擎有更新 → 执行 reinstall-daemon.sh(重新生成 systemd service unit) - - 仅配置/wiseflow 更新 → 直接重启服务 + - 仅配置/WiseFlow 更新 → 直接重启服务 systemctl --user restart openclaw-gateway.service - 开发模式下两种情况都用:dev.sh gateway 3. 自检确认服务恢复: - - 检查进程存活:ps aux | grep openclaw.mjs | grep -v grep + - 检查进程存活:ps aux | grep 'dist/index.js gateway' | grep -v grep - 查看启动日志无严重报错 - 确认关键 channel 连接已恢复(如飞书 WebSocket) @@ -85,20 +84,6 @@ 4. 主动问:这样解释清楚了吗?还有其他疑问吗? ``` -## 检查系统状态 - -定期或在升级/重启前运行: -```bash -# 检查 openclaw 进程是否存活(注意:grep 的是 openclaw.mjs,不是 openclaw 命令) -ps aux | grep openclaw.mjs | grep -v grep - -# 查看最近日志(如果使用 pm2 管理) -pm2 logs openclaw --lines 50 - -# 检查配置文件完整性 -node -e "require('fs').readFileSync(process.env.HOME + '/.openclaw/openclaw.json', 'utf8'); console.log('✅ Config OK')" -``` - ## SEO 技术优化流程 ``` @@ -110,7 +95,7 @@ node -e "require('fs').readFileSync(process.env.HOME + '/.openclaw/openclaw.json - 用 Lighthouse / PageSpeed Insights 评估页面性能 3. 分析问题,给出优先级建议(技术问题 → 索引问题 → 性能问题) 4. 告知用户:发现什么问题,建议修复顺序 -5. 执行修复(L2 直接执行,L3 涉及生产环境部署需确认): +5. 执行修复: - sitemap 问题 → 生成或更新 sitemap.xml,提交到 GSC - robots.txt 问题 → 修改并验证不误封重要页面 - 结构化数据(Schema.org JSON-LD)→ 添加或修复 @@ -129,3 +114,17 @@ node -e "require('fs').readFileSync(process.env.HOME + '/.openclaw/openclaw.json - 404 页面数量是否异常增加 - 内链断裂检测 ``` + +## 检查系统状态 + +定期或在升级/重启前运行: +```bash +# 检查 openclaw 进程是否存活(grep dist/index.js gateway) +ps aux | grep 'dist/index.js gateway' | grep -v grep + +# 查看最近日志(如果使用 pm2 管理) +pm2 logs openclaw --lines 50 + +# 检查配置文件完整性 +node -e "require('fs').readFileSync(process.env.HOME + '/.openclaw/openclaw.json', 'utf8'); console.log('✅ Config OK')" +``` \ No newline at end of file diff --git a/crews/it-engineer/HEARTBEAT.md b/crews/it-engineer/HEARTBEAT.md index e47b2ce8..c5ea602c 100644 --- a/crews/it-engineer/HEARTBEAT.md +++ b/crews/it-engineer/HEARTBEAT.md @@ -9,16 +9,7 @@ 每次心跳执行以下检查,发现异常立即告知用户: -1. openclaw 进程是否存活(`ps aux | grep openclaw.mjs | grep -v grep`) +1. openclaw 进程是否存活(`ps aux | grep 'dist/index.js gateway' | grep -v grep`) 2. `~/.openclaw/openclaw.json` 配置完整性(`node -e "require('fs').readFileSync(...)"` 验证 JSON 合法) 3. 近期运行日志是否有新的 ERROR/FATAL(通过 session-logs 技能或直接读日志文件) -4. 调用 healthcheck 技能执行系统安全加固状态检查(防火墙/SSH/更新状态) -5. 检查 https://github.com/TeamWiseFlow/wiseflow/blob/master/version 并与本地 `/version` 比对,如有差异提醒用户可以升级(**严禁**未经确认擅自升级) - -## SEO 巡检项(可按需加入定期执行) - -- GSC 索引覆盖率变化(骤降 → 立即告警) -- Core Web Vitals 退化(LCP>2.5s / INP>200ms / CLS>0.1) -- sitemap 最后修改时间是否与内容更新同步 -- 404 页面数量异常增加 -- 内链断裂检测 +4. 调用 healthcheck 技能执行系统安全加固状态检查(防火墙/SSH/更新状态) \ No newline at end of file diff --git a/crews/it-engineer/TOOLS.md b/crews/it-engineer/TOOLS.md index 4e088d16..db583b7c 100644 --- a/crews/it-engineer/TOOLS.md +++ b/crews/it-engineer/TOOLS.md @@ -6,38 +6,35 @@ - 文件读写:读取日志、配置文件,修改 workspace 文件 - Shell 执行:运行系统命令、检查状态、查看日志 -### wiseflow 内置脚本(需先 cd 到项目目录再执行) +### WiseFlow 内置脚本(需先 cd 到 WiseFlow 项目目录再执行) -> wiseflow 项目路径见同目录的 `OFB_ENV.md`(每次 `setup-crew.sh` 自动更新,里面有完整命令)。 +> WiseFlow 项目路径见同目录的 `OFB_ENV.md`(历史命名保留,每次 `setup-crew.sh` 自动更新,里面有完整命令)。 ```bash # 开发模式前台启动(含日志输出) -cd && ./scripts/dev.sh gateway +cd && ./scripts/dev.sh gateway # 生产模式重新安装后台服务 -cd && ./scripts/reinstall-daemon.sh +cd && ./scripts/reinstall-daemon.sh # 重新同步 crew 配置(幂等,安全执行) -cd && ./scripts/setup-crew.sh +cd && ./scripts/setup-crew.sh # 重新应用 addons -cd && ./scripts/apply-addons.sh - -# 升级 wiseflow 系统(执行前必须确认系统空闲) -cd && ./scripts/upgrade.sh +cd && ./scripts/apply-addons.sh ``` > ⚠️ **禁止直接运行 `openclaw` 命令**(`openclaw` 不在系统 PATH 中)。 > 如需直接调用上游 CLI,必须在 `openclaw/` 子目录内通过 `pnpm openclaw` 执行: > ```bash -> cd /openclaw && pnpm openclaw +> cd /openclaw && pnpm openclaw > ``` ### 检查系统运行状态 ```bash # 检查 openclaw 进程是否存活 -ps aux | grep openclaw.mjs | grep -v grep +ps aux | grep 'dist/index.js gateway' | grep -v grep # 查看 pm2 管理的进程(生产模式) pm2 list @@ -48,38 +45,21 @@ node -e "require('fs').readFileSync(process.env.HOME + '/.openclaw/openclaw.json ``` ### GitHub / 代码相关(需已启用 github、gh-issues、coding-agent 技能) -- `github`:读取 wiseflow 和 OpenClaw 仓库的最新信息(commits、releases、README) -- `gh-issues`:查看 wiseflow 和 OpenClaw 的 issue,了解已知问题和修复状态 +- `github`:读取 WiseFlow 和 OpenClaw 仓库的最新信息(commits、releases、README) +- `gh-issues`:查看 WiseFlow 和 OpenClaw 的 issue,了解已知问题和修复状态 - `coding-agent`:用于分析代码问题、生成配置文件、解读报错信息 -### 查阅其他 Agent 的 Session 历史 - -> ⚠️ **禁止使用 `sessions_send`/`sessions_list`/`sessions_history`/`sessions_status` 等技能命令查询其他 agent 的 session**——这些命令仅限当前自身 agent 使用。 - -如需查阅其他 agent 的对话历史(例如用于持续改进分析),直接读取本地文件: - -```bash -# 查看某 agent 的 session 索引(含所有 session 的元数据) -cat ~/.openclaw/agents//sessions/sessions.json - -# 查看某条 session 的完整对话记录(JSONL 格式,每行一条消息) -cat ~/.openclaw/agents//sessions/.jsonl -``` - -- `sessions.json`:JSON 对象,key = session key(如 `agent:cs-001:awada:direct:user123`),value = session 元数据 -- `.jsonl`:完整对话内容,逐条 JSON 行,包含 role/content/timestamp 等字段 -- 归档的 session transcript:`~/.openclaw/agents//sessions/` 下的 `.archived/` 目录 ## 工具使用规则 1. **备份重要文件**:修改 `~/.openclaw/openclaw.json` 前,先备份 -2. **脚本优先**:优先使用 wiseflow 内置脚本,不要直接操作 `openclaw/` 目录下的代码 +2. **脚本优先**:优先使用 WiseFlow 内置脚本,不要直接操作 `openclaw/` 目录下的代码 3. **日志是第一线索**:遇到问题先查日志,再猜原因 4. **验证结果**:每次操作后确认效果(如重启后检查服务是否正常运行) -### SEO 技术工具 +## SEO 技术工具 -```bash +``` # Lighthouse 性能/SEO 评分(需要 Chrome) npx lighthouse https://yoursite.com --only-categories=performance,seo --output json @@ -99,5 +79,4 @@ curl -o /dev/null -s -w "%{http_code}" https://yoursite.com/some-page | 工具 | 用途 | |------|------| | `smart-search` | 搜索 SEO 最佳实践、查找竞品技术方案 | -| `xurl` | 直接访问 GSC API、PageSpeed API、结构化数据测试 API | | `coding-agent` | 生成 sitemap.xml、JSON-LD Schema、robots.txt 内容 | diff --git a/patches/001-relax-exec-allowlist-shell-syntax.patch b/patches/001-relax-exec-allowlist-shell-syntax.patch new file mode 100644 index 00000000..b7213bb3 --- /dev/null +++ b/patches/001-relax-exec-allowlist-shell-syntax.patch @@ -0,0 +1,164 @@ +diff --git a/src/infra/exec-approvals-analysis.test.ts b/src/infra/exec-approvals-analysis.test.ts +index e823b9400c..327511352d 100644 +--- a/src/infra/exec-approvals-analysis.test.ts ++++ b/src/infra/exec-approvals-analysis.test.ts +@@ -167,11 +167,6 @@ describe("exec approvals shell analysis", () => { + }); + + it.each([ +- { command: 'echo "output: $(whoami)"', reason: "unsupported shell token: $()" }, +- { command: 'echo "output: `id`"', reason: "unsupported shell token: `" }, +- { command: "echo $(whoami)", reason: "unsupported shell token: $()" }, +- { command: "cat < input.txt", reason: "unsupported shell token: <" }, +- { command: "echo ok > output.txt", reason: "unsupported shell token: >" }, + { + command: "/usr/bin/echo first line\n/usr/bin/echo second line", + reason: "unsupported shell token: \n", +@@ -580,8 +575,8 @@ describe("exec approvals shell analysis", () => { + { + allowlist: [{ pattern: "/usr/bin/echo" }], + command: "/usr/bin/echo ok &&", +- expectedAnalysisOk: false, +- expectedAllowlistSatisfied: false, ++ expectedAnalysisOk: true, ++ expectedAllowlistSatisfied: true, + }, + { + allowlist: [{ pattern: "/usr/bin/ping" }], +diff --git a/src/infra/exec-approvals-analysis.ts b/src/infra/exec-approvals-analysis.ts +index 44900f0bb7..fca09df325 100644 +--- a/src/infra/exec-approvals-analysis.ts ++++ b/src/infra/exec-approvals-analysis.ts +@@ -42,7 +42,7 @@ export type ShellChainPart = { + opToNext: ShellChainOperator | null; + }; + +-const DISALLOWED_PIPELINE_TOKENS = new Set([">", "<", "`", "\n", "\r", "(", ")"]); ++const DISALLOWED_PIPELINE_TOKENS = new Set(["\n", "\r"]); + const DOUBLE_QUOTE_ESCAPES = new Set(["\\", '"', "$", "`"]); + const MAX_UNQUOTED_HEREDOC_CONTINUATION_LINES = 1024; + const MAX_UNQUOTED_HEREDOC_LOGICAL_LINE_LENGTH = 64 * 1024; +@@ -296,10 +296,32 @@ function splitShellPipeline(command: string): { ok: boolean; reason?: string; se + continue; + } + if (ch === "$" && next === "(") { +- return { ok: false, reason: "unsupported shell token: $()", segments: [] }; ++ buf += ch; ++ buf += next; ++ i += 1; ++ let depth = 1; ++ while (i + 1 < command.length && depth > 0) { ++ i += 1; ++ const c = command[i]; ++ buf += c; ++ if (c === "(") depth += 1; ++ else if (c === ")") depth -= 1; ++ } ++ emptySegment = false; ++ continue; + } + if (ch === "`") { +- return { ok: false, reason: "unsupported shell token: `", segments: [] }; ++ buf += ch; ++ while (i + 1 < command.length && command[i + 1] !== "`") { ++ i += 1; ++ buf += command[i]; ++ } ++ if (i + 1 < command.length) { ++ i += 1; ++ buf += command[i]; ++ } ++ emptySegment = false; ++ continue; + } + if (ch === "\n" || ch === "\r") { + return { ok: false, reason: "unsupported shell token: newline", segments: [] }; +@@ -337,10 +359,18 @@ function splitShellPipeline(command: string): { ok: boolean; reason?: string; se + } + + if (ch === "|" && next === "|") { +- return { ok: false, reason: "unsupported shell token: ||", segments: [] }; ++ buf += ch; ++ buf += next; ++ i += 1; ++ emptySegment = false; ++ continue; + } + if (ch === "|" && next === "&") { +- return { ok: false, reason: "unsupported shell token: |&", segments: [] }; ++ buf += ch; ++ buf += next; ++ i += 1; ++ emptySegment = false; ++ continue; + } + if (ch === "|") { + emptySegment = true; +@@ -348,7 +378,9 @@ function splitShellPipeline(command: string): { ok: boolean; reason?: string; se + continue; + } + if (ch === "&" || ch === ";") { +- return { ok: false, reason: `unsupported shell token: ${ch}`, segments: [] }; ++ buf += ch; ++ emptySegment = false; ++ continue; + } + if (ch === "<" && next === "<") { + buf += "<<"; +@@ -375,7 +407,32 @@ function splitShellPipeline(command: string): { ok: boolean; reason?: string; se + return { ok: false, reason: `unsupported shell token: ${ch}`, segments: [] }; + } + if (ch === "$" && next === "(") { +- return { ok: false, reason: "unsupported shell token: $()", segments: [] }; ++ buf += ch; ++ buf += next; ++ i += 1; ++ let depth = 1; ++ while (i + 1 < command.length && depth > 0) { ++ i += 1; ++ const c = command[i]; ++ buf += c; ++ if (c === "(") depth += 1; ++ else if (c === ")") depth -= 1; ++ } ++ emptySegment = false; ++ continue; ++ } ++ if (ch === "`") { ++ buf += ch; ++ while (i + 1 < command.length && command[i + 1] !== "`") { ++ i += 1; ++ buf += command[i]; ++ } ++ if (i + 1 < command.length) { ++ i += 1; ++ buf += command[i]; ++ } ++ emptySegment = false; ++ continue; + } + buf += ch; + emptySegment = false; +@@ -774,13 +831,6 @@ export function splitCommandChainWithOperators(command: string): ShellChainPart[ + foundChain = true; + continue; + } +- if (ch === ";") { +- if (!pushPart(";")) { +- invalidChain = true; +- } +- foundChain = true; +- continue; +- } + + buf += ch; + } +@@ -1060,6 +1110,8 @@ export function buildEnforcedShellCommand(params: { + + /** + * Splits a command string by chain operators (&&, ||, ;) while respecting quotes. ++ * Note: semicolons inside compound constructs (while/do/done, if/then/fi, etc.) are ++ * preserved as part of the segment rather than treated as chain separators. + * Returns null when no chain is present or when the chain is malformed. + */ + export function splitCommandChain(command: string): string[] | null { diff --git a/scripts/lib/agent-skills.sh b/scripts/lib/agent-skills.sh index 919916ce..0f327bc5 100644 --- a/scripts/lib/agent-skills.sh +++ b/scripts/lib/agent-skills.sh @@ -459,22 +459,24 @@ inject_exec_guide() { ## exec 命令规范 -exec allowlist 不支持 shell 重定向符(`>`、`<`、`2>`、`&>`),使用时会导致整条命令被 deny,即使其中每个命令都已在白名单中。 +exec allowlist 会解析管道、`&&`、`||`、`;` 和常见重定向,并逐段检查实际执行的命令是否都在白名单中。 -**避免**: -``` +**允许的常见写法**: +```bash ls -la /tmp/file.txt 2>/dev/null && echo "EXISTS" || echo "NOT" some-cmd > /tmp/out.txt +echo a; echo b +cat file.txt | grep keyword ``` -**改用**: -```bash -# 判断文件是否存在 -[ -f /tmp/file.txt ] && echo "EXISTS" || echo "NOT" -test -f /tmp/file.txt && echo "EXISTS" || echo "NOT" -``` +注意:重定向只支持 POSIX 风格写法(如 `> file`、`2> err.log`、`2>&1`)。不要使用 bash/zsh 专属的 `&>` / `&>>`,这类写法在 `/bin/sh` 下可能被解释为后台执行。重定向只改变当前已批准命令的 stdin/stdout/stderr;命令本身仍必须在 allowlist 中。`echo ok; rm file` 只有在 `echo` 和 `rm` 都被允许时才会通过。 + +**仍然禁止使用隐式执行子命令的 shell 扩展:** -如果确实需要重定向,请改用 `bash -c "..."` 方式,并确保 `bash` 已在 exec allowlist 中(T2 及以上 tier 默认包含)。 +- ❌ `echo $(whoami)` — 命令替换会额外执行子命令 +- ❌ ``echo `id` `` — 反引号命令替换会额外执行子命令 +- ❌ `cat <(id)` / `tee >(cmd)` — process substitution 会额外执行子命令 +- ❌ `cmd & other-cmd` — 后台执行不受控 **以下写法同样会导致 allowlist miss,禁止使用:** From 791892f020296b3d85b2602b53c4399139ed7aa1 Mon Sep 17 00:00:00 2001 From: bigbrother666sh Date: Mon, 25 May 2026 18:52:20 +0800 Subject: [PATCH 3/4] merge from pro --- addons/officials/README.md | 11 +- addons/officials/addon.json | 2 +- .../crew/business-developer/AGENTS.md | 59 +- .../business-developer/HEARTBEAT_TEMPLATE.md | 27 +- .../skills/lead-hunting/SKILL.md | 54 +- addons/officials/crew/designer/DENIED_SKILLS | 4 +- addons/officials/crew/ir/DENIED_SKILLS | 4 +- addons/officials/crew/sales-cs/SOUL.md | 2 +- .../crew/selfmedia-operator/AGENTS.md | 3 +- .../crew/selfmedia-operator/ALLOWED_COMMANDS | 1 + .../crew/selfmedia-operator/BOOTSTRAP.md | 82 +- .../crew/selfmedia-operator/BUILTIN_SKILLS | 2 +- .../openclaw_setting_sample.json | 2 +- .../skills/t2video/SKILL.md | 303 ++++ .../skills/t2video/scripts/t2video.py | 1223 +++++++++++++++++ addons/officials/requirements.txt | 2 + .../skills/siliconflow-video-gen/SKILL.md | 78 -- .../siliconflow-video-gen/scripts/gen.py | 165 --- awada/awada-extension/src/message-handler.ts | 2 +- config-templates/openclaw-aihubmix.json | 4 +- config-templates/openclaw.json | 821 +++++------ crews/hrbp/DENIED_SKILLS | 5 +- crews/hrbp/skills/hrbp-common/scripts/lib.sh | 2 +- .../scripts/scan-feedback.sh | 0 .../skills/hrbp-list/scripts/list-agents.sh | 0 .../hrbp-modify/scripts/modify-agent.sh | 7 - .../skills/hrbp-recruit/scripts/add-agent.sh | 40 +- .../hrbp-remove/scripts/remove-agent.sh | 7 - .../skills/hrbp-usage/scripts/agent-usage.sh | 0 crews/it-engineer/AGENTS.md | 2 +- crews/it-engineer/DENIED_SKILLS | 13 +- crews/it-engineer/TOOLS.md | 2 +- crews/it-engineer/skills/seo/SKILL.md | 14 +- crews/main/AGENTS.md | 219 ++- crews/main/ALLOWED_COMMANDS | 10 +- crews/main/BOOTSTRAP.md | 38 +- crews/main/HEARTBEAT.md | 30 +- crews/main/MEMORY.md | 79 +- crews/main/SOUL.md | 90 +- crews/main/TOOLS.md | 91 +- crews/main/skills/crew-dismiss/SKILL.md | 2 +- .../scripts/dismiss-internal-crew.sh | 2 +- .../crew-list}/scripts/sync-team-directory.sh | 0 crews/main/skills/crew-recruit/SKILL.md | 77 +- crews/main/skills/reminder/SKILL.md | 19 + .../reminder/scripts/update-reminders.py | 155 +++ .../main/skills/work-channel-binding/SKILL.md | 66 + .../work-channel-binding/docs/feishu.md | 82 ++ .../skills/work-channel-binding/docs/wecom.md | 31 + .../scripts/apply-work-channel-binding.py | 219 +++ .../scripts/check-work-channel-bindings.py | 108 ++ .../scripts/complete-pending-followup.py | 30 + .../scripts/install-wecom-channel.sh | 89 ++ .../scripts/prepare-work-channel-binding.py | 103 ++ .../scripts/record-pending-followup.py | 41 + .../scripts/restart-gateway-confirmed.sh | 13 + crews/shared/COMMAND_TIERS.md | 4 +- scripts/install.sh | 109 +- scripts/lib/agent-skills.sh | 2 +- scripts/lib/exec-tiers.sh | 4 +- scripts/setup-crew.sh | 77 +- 61 files changed, 3714 insertions(+), 1019 deletions(-) create mode 100644 addons/officials/crew/selfmedia-operator/skills/t2video/SKILL.md create mode 100644 addons/officials/crew/selfmedia-operator/skills/t2video/scripts/t2video.py delete mode 100644 addons/officials/skills/siliconflow-video-gen/SKILL.md delete mode 100644 addons/officials/skills/siliconflow-video-gen/scripts/gen.py mode change 100755 => 100644 crews/hrbp/skills/hrbp-feedback-review/scripts/scan-feedback.sh mode change 100755 => 100644 crews/hrbp/skills/hrbp-list/scripts/list-agents.sh mode change 100755 => 100644 crews/hrbp/skills/hrbp-modify/scripts/modify-agent.sh mode change 100755 => 100644 crews/hrbp/skills/hrbp-recruit/scripts/add-agent.sh mode change 100755 => 100644 crews/hrbp/skills/hrbp-remove/scripts/remove-agent.sh mode change 100755 => 100644 crews/hrbp/skills/hrbp-usage/scripts/agent-usage.sh rename crews/{hrbp/skills/hrbp-common => main/skills/crew-list}/scripts/sync-team-directory.sh (100%) mode change 100755 => 100644 create mode 100644 crews/main/skills/reminder/SKILL.md create mode 100755 crews/main/skills/reminder/scripts/update-reminders.py create mode 100644 crews/main/skills/work-channel-binding/SKILL.md create mode 100644 crews/main/skills/work-channel-binding/docs/feishu.md create mode 100644 crews/main/skills/work-channel-binding/docs/wecom.md create mode 100755 crews/main/skills/work-channel-binding/scripts/apply-work-channel-binding.py create mode 100755 crews/main/skills/work-channel-binding/scripts/check-work-channel-bindings.py create mode 100755 crews/main/skills/work-channel-binding/scripts/complete-pending-followup.py create mode 100755 crews/main/skills/work-channel-binding/scripts/install-wecom-channel.sh create mode 100755 crews/main/skills/work-channel-binding/scripts/prepare-work-channel-binding.py create mode 100755 crews/main/skills/work-channel-binding/scripts/record-pending-followup.py create mode 100755 crews/main/skills/work-channel-binding/scripts/restart-gateway-confirmed.sh diff --git a/addons/officials/README.md b/addons/officials/README.md index e5edb436..a1eaea13 100644 --- a/addons/officials/README.md +++ b/addons/officials/README.md @@ -10,7 +10,6 @@ wiseflow 开源社区版本自带的官方 addon,在 wiseflow 默认全局技 |------|------|------------|----------| | `rss-reader` | RSS/Atom Feed 读取,支持订阅任意标准 feed 格式的内容源 | — | 全部 crew 可用 | | `siliconflow-img-gen` | 文生图、图片修改(SiliconFlow API) | `SILICONFLOW_API_KEY` | designer、selfmedia-operator | -| `siliconflow-video-gen` | 文生视频 / 图生视频(SiliconFlow API) | `SILICONFLOW_API_KEY` | selfmedia-operator | | `connections-optimizer` | 人脉关系网络优化与拓展建议 | — | **仅 business-developer**;其余 crew DENIED | | `email-ops` | 批量邮件撰写与 SMTP 发送 | `SMTP_SERVER` `SMTP_USER` `SMTP_PASSWORD` | **仅 business-developer**;其余 crew DENIED | | `pitch-deck` | 融资/商务演示文稿生成,支持读取 .pptx 文件内容 | — | **仅 business-developer**;其余 crew DENIED | @@ -53,11 +52,11 @@ wiseflow 开源社区版本自带的官方 addon,在 wiseflow 默认全局技 - 两种工作模式:选题研究 → 图文输出 / 草稿扩写 → 完整文章 - 配图优先级:用户上传 > 网络免版权 > AI 生成(siliconflow-img-gen)> 历史素材复用 -- 视频生成:通过 `siliconflow-video-gen` 生成短视频(文生视频 / 图生视频) +- 视频制作:通过 `t2video` 技能完成短视频制作(TTS 语音合成 + AI 视频片段生成 + 素材组装) - 素材统一归档到 `campaign_assets/`,维护 index.md 方便复用 - 支持自动发布到知乎/头条/掘金/Medium(wenyan-publisher)、Twitter、Instagram、TikTok、YouTube -专属技能:`wenyan-publisher` / `twitter-post` / `instagram-post` / `tiktok-post` / `youtube-upload` +专属技能:`wenyan-publisher` / `twitter-post` / `instagram-post` / `tiktok-post` / `youtube-upload` / `t2video` --- @@ -156,9 +155,11 @@ wiseflow 开源社区版本自带的官方 addon,在 wiseflow 默认全局技 --- -## AI 生图 / 生视频服务推荐 +## AI 生图服务推荐 -`siliconflow-img-gen` 和 `siliconflow-video-gen` 两个技能依赖 [SiliconFlow](https://cloud.siliconflow.cn/i/WNLYbBpi) API,用于文生图、图片修改以及文生视频 / 图生视频。 +`siliconflow-img-gen` 技能依赖 [SiliconFlow](https://cloud.siliconflow.cn/i/WNLYbBpi) API,用于文生图、图片修改。 + +`siliconflow-video-gen` 和 `siliconflow-tts` 已迁移为 video-producer(official-plus addon)内置技能。开源版 `selfmedia-operator` 内置的 `t2video` 技能同样集成了 SiliconFlow 视频生成和 TTS 能力。 [硅基流动(SiliconFlow)](https://cloud.siliconflow.cn/i/WNLYbBpi) 提供国内领先的生图和生视频模型,注册并实名认证即可领取免费代金券。 diff --git a/addons/officials/addon.json b/addons/officials/addon.json index 4f8cc055..212ce467 100644 --- a/addons/officials/addon.json +++ b/addons/officials/addon.json @@ -1,7 +1,7 @@ { "name": "wiseflow officials", "version": "0.4.1", - "description": "官方 Crew 模板(selfmedia-operator / business-developer / designer / ir / sales-cs)+ 专属全局技能(rss-reader / siliconflow-img-gen / siliconflow-video-gen / pexels-footage / pixabay-footage / connections-optimizer / email-ops / pitch-deck / ppt-maker / social-graph-ranker / xhs-interact)", + "description": "官方 Crew 模板(selfmedia-operator / business-developer / designer / ir / sales-cs)+ 专属全局技能(rss-reader / siliconflow-img-gen / pexels-footage / pixabay-footage / connections-optimizer / email-ops / pitch-deck / ppt-maker / social-graph-ranker / xhs-interact / xianyu-ops)", "openclaw_version": "2026.5.7", "openclaw_commit": "eeef4864494f859838fec1586bedbab1f8fa5702", "auto-activate": false, diff --git a/addons/officials/crew/business-developer/AGENTS.md b/addons/officials/crew/business-developer/AGENTS.md index 83dcf340..5afc3dcb 100644 --- a/addons/officials/crew/business-developer/AGENTS.md +++ b/addons/officials/crew/business-developer/AGENTS.md @@ -28,13 +28,15 @@ --- -## 模式一:Lead Hunting(通过创作者探索潜在客户) +## 模式一:Lead Hunting(潜在客户探索) ### 初始化对话流程 #### Phase 1: 收集基础信息 -用户告知要搜索的平台和客户画像。支持平台: +询问用户以下信息: + +1. **目标平台**(多选): | 标识 | 平台 | |------|------| @@ -45,15 +47,31 @@ | fb | Facebook | | x | Twitter/X | | wb | 微博 | -| web | 网页 | +| web | 网页(需要有用户给出具体的站点) | + +2. **搜集策略**(二选一,不可组合): + +| 策略 | 说明 | 可搜集信息 | +|------|------|-----------| +| A. 发布者画像匹配 | 上溯帖子发布者主页,判断发布者是否符合目标用户画像 | 昵称、user_id、主页信息(简介、关注量、粉丝量、获赞量等)、主页内容 | +| B. 评论区潜客挖掘 | 嵌入帖子评论区,根据评论内容寻找潜在用户 | 昵称、user_id、IP属地、评论内容、评论日期、原贴url | + +⚠️ 两种策略不兼容——搜索关键词完全不同,不可混用。如需同时使用两种策略,应作为两个独立任务分别配置。 + +3. **潜在客户画像/特征**(越具体越好): + - 策略 A:描述目标客户是什么样的人、做什么、关注什么 + - 策略 B:描述目标客户可能在什么内容的帖子下留言、会说什么样的话 **必须问到**: - 目标平台(多选) -- 潜在客户画像/特征(越具体越好) +- 搜集策略(A 或 B) +- 潜在客户画像/特征 #### Phase 2: 分析并确认 -根据用户提供的画像,分析并**输出给用户确认**: +根据用户选择的搜集策略,分别进行分析并**输出给用户确认**: + +**策略 A(发布者画像匹配)**: 1. **各平台搜索关键词**:为每个目标平台单独构思 - 符合用户画像的创作者可能在平台上发布什么内容?这些内容通过哪些关键词可以搜索到? @@ -62,21 +80,38 @@ - 每个平台列出 3-5 组关键词 - 对于 `web`,如果用户指定站点,则优先使用站点内的搜索框 -2. **潜在客户判定标准**:明确如何通过创作者主页和作品判定是否为潜在客户 +2. **潜在客户判定标准**: + - 明确如何通过创作者主页和作品判定是否为潜在客户 - 特别关注区分真实客户和同行/竞对(发布类似内容但实为同行) - 列出:哪些特征说明是客户、哪些特征说明是同行(应排除) +**策略 B(评论区潜客挖掘)**: + +1. **各平台搜索关键词**:为每个目标平台单独构思 + - 目标用户可能在什么内容的帖子下留言?搜索这些帖子需要什么关键词? + - 注意:策略 B 的关键词与策略 A 完全不同——策略 A 搜"目标用户发布的内容",策略 B 搜"目标用户会去评论的内容" + - 例如:找装修客户时,策略 A 搜"装修日记""新房装修",策略 B 搜"装修避坑""装修求助""新房交付" + - 每个平台列出 3-5 组关键词 + +2. **评论筛选标准**: + - 哪类评论内容表明评论者可能是潜在客户 + - 例如:咨询类评论("怎么买""多少钱")、需求表达类评论("正好需要""求推荐") + - 列出:哪些评论特征应纳入、哪些应排除(如同行互推、无关灌水) + 用户确认(或按反馈修改)后进入 Phase 3。 #### Phase 3: 收集执行参数 逐项询问: 1. **探索频率**:多久执行一次?(不超过一天 6 次,避免平台封号) -2. **每次最大探索量**:每次探索的创作者数量(含不符合的),建议不超过 12 个 +2. **每次最大探索量**: + - 策略 A:每次探索的创作者数量(含不符合的),建议不超过 12 个 + - 策略 B:每次扫描的帖子数量(含无符合评论的),建议不超过 12 个 3. **反馈形式**: - - **A. 列表报告**:潜在客户信息(昵称、ID、主页链接等)列表反馈,用户自行联系 - - **B. Cold Touch 私信**:直接以私信方式联系潜在客户 - - **C. 解析email 地址并发送 email**: 如果能够解析出创作者的 email 地址,则发送 email 进行联系 + - **A. 列表报告**:潜在客户信息列表反馈,用户自行联系 + - **B. Cold Touch 私信**:直接以私信方式联系潜在客户(xhs 不支持) + - **C. 解析email 地址并发送 email**:如果能够解析出创作者的 email 地址,则发送 email 进行联系 + - ⚠️ 小红书平台仅支持 **A. 列表报告** 如用户选择 B: - 询问是否有现成话术 @@ -96,11 +131,13 @@ ## 模式二:Comment Engagement(评论区拓展) +> ⚠️ **小红书不支持此模式**:小红书平台会自动删除评论区的营销回复,无法通过评论或私信与用户互动。如用户仅指定 xhs,应引导至模式一的策略 B(评论区潜客挖掘)。 + ### 初始化对话流程 #### Phase 1: 收集基础信息 -询问目标平台(多选,同模式一平台列表)和要搜索的内容类型。 +询问目标平台(多选,同模式一平台列表,**不含 xhs**)和要搜索的内容类型。 #### Phase 2: 分析并确认 diff --git a/addons/officials/crew/business-developer/HEARTBEAT_TEMPLATE.md b/addons/officials/crew/business-developer/HEARTBEAT_TEMPLATE.md index 26d2c052..39af454e 100644 --- a/addons/officials/crew/business-developer/HEARTBEAT_TEMPLATE.md +++ b/addons/officials/crew/business-developer/HEARTBEAT_TEMPLATE.md @@ -13,22 +13,29 @@ **状态**:已启用 +**搜集策略**: + **目标平台**: -- xhs:<关键词1>、<关键词2>、<关键词3> +- xhs:<关键词1>、<关键词2> - dy:<关键词1>、<关键词2> - web:<站点URL>:<搜索关键词> **潜在客户判定标准**: -- 符合特征: - - <特征描述1> - - <特征描述2> -- 排除特征(同行/竞对): - - <特征描述1> +- 策略 A(发布者画像匹配): + - 符合特征: + - <特征描述1> + - 排除特征(同行/竞对): + - <特征描述1> +- 策略 B(评论区潜客挖掘): + - 纳入评论特征: + - <特征描述1> + - 排除评论特征: + - <特征描述1> **执行参数**: - 频率:<每天N次 / 每N小时> -- 每次最大探索量: -- 反馈形式:<列表报告 / Cold Touch 私信 / Email 联系> +- 每次最大探索量: +- 反馈形式:<列表报告 / Cold Touch 私信 / Email 联系>(策略 B 及 xhs 仅支持列表报告) - Cold Touch 话术:<话术内容> - Email 话术:<话术内容> @@ -39,14 +46,16 @@ ## 模式二:Comment Engagement(评论区拓展) +> ⚠️ 小红书不支持此模式。 + ```markdown ### 模式二:Comment Engagement(评论区拓展) **状态**:已启用 **目标平台**: -- xhs:<关键词1>、<关键词2> - dy:<关键词1> +- fb:<关键词1> **互动策略**: diff --git a/addons/officials/crew/business-developer/skills/lead-hunting/SKILL.md b/addons/officials/crew/business-developer/skills/lead-hunting/SKILL.md index a93bae60..b7be7750 100644 --- a/addons/officials/crew/business-developer/skills/lead-hunting/SKILL.md +++ b/addons/officials/crew/business-developer/skills/lead-hunting/SKILL.md @@ -1,11 +1,11 @@ --- name: lead-hunting -description: 通过自媒体平台搜索内容创作者,按预设关键词和判定标准筛选潜在客户。用于 HEARTBEAT 定时任务。 +description: 通过自媒体平台按搜集策略探索潜在客户——策略 A 分析帖子发布者画像,策略 B 从评论区挖掘潜客。用于 HEARTBEAT 定时任务。 --- # Lead Hunting 技能 -通过自媒体平台搜索特定关键词内容,逐一分析创作者主页,按预设标准判定是否为潜在客户。 +通过自媒体平台搜索特定关键词内容,按搜集策略筛选潜在客户。策略 A 逐一分析创作者主页判定是否为潜在客户;策略 B 扫描帖子评论区,根据评论内容挖掘潜在客户。 **依赖技能**:`smart-search`(构造搜索 URL)、`browser-guide`(浏览器操作)、`email-ops`(email 操作)、`bd-record`(去重记录) @@ -14,8 +14,9 @@ description: 通过自媒体平台搜索内容创作者,按预设关键词和 ## 前置条件 执行前需确认 HEARTBEAT.md 中已配置以下信息: +- 搜集策略(A 发布者画像匹配 / B 评论区潜客挖掘) - 目标平台列表及对应的搜索关键词 -- 潜在客户判定标准 +- 潜在客户判定标准 / 评论筛选标准 - 每次最大探索量 - 反馈形式(列表报告 / Cold Touch 私信 / Email 联系) @@ -26,7 +27,7 @@ description: 通过自媒体平台搜索内容创作者,按预设关键词和 ### Step 1: 准备工作 ``` -1. 读取 HEARTBEAT.md 获取当前配置(平台、关键词、判定标准、最大探索量) +1. 读取 HEARTBEAT.md 获取当前配置(搜集策略、平台、关键词、判定标准、最大探索量) 2. 确保浏览器可用(遵循 browser-guide) 3. 初始化 bd-record 数据库:bash ./skills/bd-record/scripts/init-db.sh ``` @@ -44,7 +45,7 @@ description: 通过自媒体平台搜索内容创作者,按预设关键词和 - 提取每个内容的创作者主页链接 ``` -### Step 3: 逐创作者判定 +### Step 3 (策略 A): 逐创作者判定 对每个搜索到的创作者,按顺序执行: @@ -80,15 +81,52 @@ description: 通过自媒体平台搜索内容创作者,按预设关键词和 8. 操作间隔:每个创作者之间保持 30-60 秒间隔,避免平台风控 ``` +### Step 3 (策略 B): 逐帖子评论区挖掘 + +对每个搜索到的帖子,按顺序执行: + +``` +1. 提取帖子标识(platform, post_url, post_title) + +2. 导航到帖子详情页,等待评论区加载 + +3. 如果支持按时间排序,切换到按时间排序,确保评论从新到旧排列 + +4. 滚动浏览评论区,查找符合 HEARTBEAT.md 中评论筛选标准的评论: + - 提取评论者信息:昵称、user_id、IP属地、评论内容、评论日期 + +5. 对每条符合标准的评论: + a. 以评论者 user_id 作为 --creator-id 做去重检查: + bash ./skills/bd-record/scripts/check-creator.sh --platform <平台> --creator-id <评论者user_id> + 如果 {"exists": true},则跳过该评论者 + + b. 记录到数据库: + bash ./skills/bd-record/scripts/record-creator.sh \ + --platform <平台> \ + --creator-id <评论者user_id> \ + --nickname <昵称> \ + --homepage-url <原贴URL> \ + --qualified <1或0> \ + --notes <评论内容及判定理由> + +6. 操作间隔:每个帖子之间保持 30-60 秒间隔,避免平台风控 +``` + ### Step 4: 汇总报告 ``` -1. 统计本批次结果:探索总数、符合数、跳过数(已记录) +1. 统计本批次结果: + - 策略 A:探索总数、符合数、跳过数(已记录) + - 策略 B:扫描帖子数、发现潜客数、跳过数(已记录) + 2. 列出所有符合标准的潜在客户: - - 平台、昵称、ID、主页 URL、判定理由 -3. 按 HEARTBEAT.md 中配置的反馈形式执行: + - 策略 A:平台、昵称、ID、主页 URL、判定理由 + - 策略 B:平台、昵称、user_id、IP属地、评论内容、评论日期、原贴url + +3. 按 HEARTBEAT.md 中配置的反馈形式执行(仅策略 A 支持 Cold Touch 私信 / Email 联系): - **Cold Touch 私信**:逐一给符合标准的创作者发送预设话术私信,使用各平台的私信/消息功能,每个私信之间保持 30-60 秒间隔 - **Email 联系**:先校验 `email-ops` 所需环境变量是否齐全,若不全则跳过 Email 步骤并记录;齐全则使用 `email-ops` 发送邮件,每个邮件之间保持 30-60 秒间隔 + 4. 使用 message 工具将汇总报告发送给用户 ``` diff --git a/addons/officials/crew/designer/DENIED_SKILLS b/addons/officials/crew/designer/DENIED_SKILLS index 95062baf..026a2045 100644 --- a/addons/officials/crew/designer/DENIED_SKILLS +++ b/addons/officials/crew/designer/DENIED_SKILLS @@ -7,4 +7,6 @@ connections-optimizer email-ops social-graph-ranker rss-reader -xhs-interact \ No newline at end of file +xhs-interact +# 闲鱼/TTS 技能(designer 不需要) +xianyu-ops \ No newline at end of file diff --git a/addons/officials/crew/ir/DENIED_SKILLS b/addons/officials/crew/ir/DENIED_SKILLS index 21efff5e..52d370bd 100644 --- a/addons/officials/crew/ir/DENIED_SKILLS +++ b/addons/officials/crew/ir/DENIED_SKILLS @@ -3,5 +3,5 @@ gh-issues coding-agent rss-reader -login-manager -wx-mp-hunter +# 闲鱼/TTS 技能(IR 不需要) +xianyu-ops diff --git a/addons/officials/crew/sales-cs/SOUL.md b/addons/officials/crew/sales-cs/SOUL.md index 212e9eff..4da67e87 100644 --- a/addons/officials/crew/sales-cs/SOUL.md +++ b/addons/officials/crew/sales-cs/SOUL.md @@ -12,7 +12,7 @@ 2. **售前咨询**:解答客户对 <产品/服务名称> 的疑问,以长期记忆中的客服手册为准 3. **销售推进**:识别购买意图,引导客户进入 <付费转化路径,如:试用→轻付费→订阅> 4. **客户画像维护**:基于系统注入的客户状态,维护 `business_status`、`purpose`、`prompt_source` -5. **人工升级**:遇到敏感/投诉/退款/复杂问题时,引导客户联系人工 +5. **人工升级**:遇到敏感/投诉/退款/复杂问题时,引导客户联系人工,绝对不要给出任何结论或承诺! ## 明确边界 diff --git a/addons/officials/crew/selfmedia-operator/AGENTS.md b/addons/officials/crew/selfmedia-operator/AGENTS.md index da534644..51dbbb26 100644 --- a/addons/officials/crew/selfmedia-operator/AGENTS.md +++ b/addons/officials/crew/selfmedia-operator/AGENTS.md @@ -67,8 +67,7 @@ output_video/ **通用视频生产流程**: - 充分构思故事线; - 生成视频脚本,脚本必须以 5s 为单位拆分为片段列表; -- 对于每个视频片段优先使用`pexels-footage` 或 `pixabay-footage` 下载免版权视频片段,如果`pexels-footage` 或 `pixabay-footage` 不可用,或者搜索不到合适的视频片段时,则使用 `siliconflow-video-gen`生成; -- 使用 `ffmepg` 合成视频片段; +- 使用 `t2video` 技能完成视频制作(TTS 语音合成 + AI 视频片段生成 + 组装),也可用 `pexels-footage` 或 `pixabay-footage` 下载免版权视频片段后用 `t2video compose` 组装; - 制作视频封面(必须)。 注意:每个视频都必须配封面图,封面图必须采用“图+文“的模式,不能仅有背景图片,文字可以是一句吸引人的文案。视频封面图应该 spawn `designer` 所谓 subagent 制作,`designer`不可用时也用 spawn 自己作为 subagent 制作。 diff --git a/addons/officials/crew/selfmedia-operator/ALLOWED_COMMANDS b/addons/officials/crew/selfmedia-operator/ALLOWED_COMMANDS index 5c06728f..95bb2794 100644 --- a/addons/officials/crew/selfmedia-operator/ALLOWED_COMMANDS +++ b/addons/officials/crew/selfmedia-operator/ALLOWED_COMMANDS @@ -1,4 +1,5 @@ # 技能脚本执行 +python3 ./skills/t2video/scripts/t2video.py python3 node npx diff --git a/addons/officials/crew/selfmedia-operator/BOOTSTRAP.md b/addons/officials/crew/selfmedia-operator/BOOTSTRAP.md index 097840b9..30c1bcbb 100644 --- a/addons/officials/crew/selfmedia-operator/BOOTSTRAP.md +++ b/addons/officials/crew/selfmedia-operator/BOOTSTRAP.md @@ -1,10 +1,76 @@ -# Bootstrap +# Media Operator Bootstrap -This is a pre-configured crew workspace. Your role, responsibilities, and behavioral guidelines are fully defined in the following files — please review them at startup: +This one-time bootstrap collects the operating context before content work starts. If this crew is being enabled through Main Agent and has no direct work channel yet, Main Agent may ask these questions on behalf of this crew and write the answers into the crew workspace. -- **SOUL.md** — Role definition, core responsibilities, and autonomy level -- **AGENTS.md** — Workflows and operating procedures -- **MEMORY.md** — Background context and ongoing task state -- **IDENTITY.md** — Name and persona -- **USER.md** — Assumptions about who you are serving -- **TOOLS.md** — Available tools and usage guidelines +## Step 1: Platform Scope + +Ask which platforms the user wants to operate: + +- WeChat Official Account +- WeCom Moments +- Xiaohongshu +- Douyin +- Kuaishou +- Bilibili +- YouTube +- TikTok +- Instagram +- Facebook +- Threads +- Pinterest +- Other platforms + +Clarify: + +- first-launch platforms; +- later/backlog platforms; +- draft-only vs automatic publishing; +- whether human approval is required before publishing. + +## Step 2: WeChat / WeCom Publishing Readiness + +If the user chooses WeChat Official Account or WeCom Moments, remind them: + +> These publishing APIs commonly require an IP allowlist. If this machine has no fixed public IP, use a relay/transit mode before enabling automatic publishing. + +Ask: + +- Does this machine have a fixed public IP? +- Is the platform IP allowlist already configured? +- Do they need relay/transit mode? +- Should this crew only generate drafts until publishing credentials are ready? + +## Step 3: Brand and Business Context + +Collect: + +- brand/company name; +- product/service introduction; +- target audience; +- key selling points; +- brand tone; +- forbidden claims or sensitive topics; +- competitors or differentiation; +- common CTA; +- source material locations; +- approval owner and workflow. + +## Step 4: Content Operating Rhythm + +Ask: + +- publishing frequency by platform; +- daily/weekly topic planning cadence; +- whether heartbeat should generate topics, drafts, or status reports; +- failure handling preference: notify immediately or summarize later. + +## Completion + +After bootstrap is complete: + +1. Update `MEMORY.md` with platform strategy, brand context, and constraints. +2. Update `USER.md` with approval preferences and service recipient information. +3. Update `TOOLS.md` with publishing environment notes, but never write secrets into Markdown. +4. Update `HEARTBEAT.md` only if the user wants periodic tasks. +5. Delete `BOOTSTRAP.md` from the runtime workspace. +6. Suggest the next step, such as creating the first WeChat Official Account draft. diff --git a/addons/officials/crew/selfmedia-operator/BUILTIN_SKILLS b/addons/officials/crew/selfmedia-operator/BUILTIN_SKILLS index 09287ca1..a054ff09 100644 --- a/addons/officials/crew/selfmedia-operator/BUILTIN_SKILLS +++ b/addons/officials/crew/selfmedia-operator/BUILTIN_SKILLS @@ -8,7 +8,7 @@ xhs-publisher juejin-publish toutiao-publish siliconflow-img-gen -siliconflow-video-gen +t2video pexels-footage pixabay-footage smart-search diff --git a/addons/officials/crew/selfmedia-operator/openclaw_setting_sample.json b/addons/officials/crew/selfmedia-operator/openclaw_setting_sample.json index a1a80ad5..5b528871 100644 --- a/addons/officials/crew/selfmedia-operator/openclaw_setting_sample.json +++ b/addons/officials/crew/selfmedia-operator/openclaw_setting_sample.json @@ -10,7 +10,7 @@ "juejin-publish", "toutiao-publish", "siliconflow-img-gen", - "siliconflow-video-gen", + "t2video", "pexels-footage", "pixabay-footage", "smart-search", diff --git a/addons/officials/crew/selfmedia-operator/skills/t2video/SKILL.md b/addons/officials/crew/selfmedia-operator/skills/t2video/SKILL.md new file mode 100644 index 00000000..1bc3b283 --- /dev/null +++ b/addons/officials/crew/selfmedia-operator/skills/t2video/SKILL.md @@ -0,0 +1,303 @@ +--- +name: t2video +description: 一站式短视频制作工具。整合 Edge TTS(免费)/ SiliconFlow TTS 语音合成、SiliconFlow 文生视频/图生视频和 FFmpeg 组装,从脚本到成品视频一步完成。支持用户指定素材的位置编排。 +metadata: + openclaw: + emoji: "🎥" + requires: + bins: + - python3 + env: + - SILICONFLOW_API_KEY + primaryEnv: SILICONFLOW_API_KEY +--- + +# t2video — 一站式短视频制作 + +Use this skill when: +- 需要从脚本生成完整短视频(TTS + 素材 + 组装) +- 需要生成 TTS 语音旁白(Edge TTS 免费 / SiliconFlow 备选) +- 需要生成 AI 视频片段 +- 需要将用户提供的素材(视频/图片)按指定位置编入视频 +- 需要将已有素材 + 音频组装成最终视频 + +**三种模式**: + +| 模式 | 用途 | 输入 | 输出 | +|------|------|------|------| +| `tts` | 语音合成 | 文本 | 音频文件 | +| `gen` | AI 视频生成 | 文本描述/图片 | MP4 视频片段 | +| `compose` | 视频组装 | 素材 + 音频 + 编排计划 | 最终 MP4 | + +**不烧录字幕**:大部分平台支持自动生成字幕,无需手动烧录。 + +--- + +## tts — 语音合成 + +**默认使用 Edge TTS**(完全免费,无需 API Key)。SiliconFlow 为备选(需要 API Key,音质更好)。 + +```bash +# 默认 edge-tts(免费) +python3 ./skills/t2video/scripts/t2video.py tts \ + --text "大家好,欢迎来到今天的视频。" + +# 指定语音 +python3 ./skills/t2video/scripts/t2video.py tts \ + --text "99%的人都不知道这个技巧" \ + --voice zh-CN-YunxiNeural + +# 从文件读取长文本 +python3 ./skills/t2video/scripts/t2video.py tts \ + --text-file ./output_video/my-video/script.txt \ + --out-dir ./output_video/my-video/fragments + +# SiliconFlow TTS(备选,需要 SILICONFLOW_API_KEY) +python3 ./skills/t2video/scripts/t2video.py tts \ + --provider siliconflow \ + --text "这是演示旁白。" \ + --voice "FunAudioLLM/CosyVoice2-0.5B:anna" \ + --format wav +``` + +### tts 参数 + +| Flag | Default | Description | +|------|---------|-------------| +| `--text` | — | 要合成的文本(与 --text-file 二选一) | +| `--text-file` | — | UTF-8 文本文件路径 | +| `--provider` | `edge` | TTS 提供者:edge(免费)/ siliconflow(备选) | +| `--voice` | provider 默认 | 语音 ID(见下方语音列表) | +| `--speed` | — | 语速。Edge: -50~50(百分比);SF: 0.25~4.0 | +| `--format` | `mp3` | 音频格式(siliconflow:mp3/opus/wav/pcm) | +| `--sample-rate` | — | 采样率(siliconflow only) | +| `--output` | — | 精确输出路径 | +| `--out-dir` | `./tmp/tts-` | 输出目录 | + +### Edge TTS 可用语音(免费,无需 Key) + +| Voice ID | 说明 | +|----------|------| +| `zh-CN-XiaoxiaoNeural` | 女声(默认) | +| `zh-CN-XiaoyiNeural` | 女声 | +| `zh-CN-YunjianNeural` | 男声 | +| `zh-CN-YunxiNeural` | 男声 | +| `zh-CN-YunxiaNeural` | 男声 | +| `zh-CN-YunyangNeural` | 男声(新闻播报风格) | +| `zh-CN-liaoning-XiaobeiNeural` | 东北话女声 | +| `zh-CN-shaanxi-XiaoniNeural` | 陕西话女声 | +| `en-US-JennyNeural` | English Female | +| `en-US-GuyNeural` | English Male | +| `en-US-AriaNeural` | English Female | +| `en-US-DavisNeural` | English Male | +| `ja-JP-NanamiNeural` | 日本語 Female | +| `ja-JP-KeitaNeural` | 日本語 Male | +| `ko-KR-SunHiNeural` | 한국어 Female | +| `ko-KR-InJoonNeural` | 한국어 Male | + +### SiliconFlow TTS 可用语音(需要 API Key) + +| Voice ID | 说明 | +|----------|------| +| `FunAudioLLM/CosyVoice2-0.5B:alex` | 男声 | +| `FunAudioLLM/CosyVoice2-0.5B:anna` | 女声 | +| `FunAudioLLM/CosyVoice2-0.5B:bella` | 女声 | +| `FunAudioLLM/CosyVoice2-0.5B:benjamin` | 男声 | +| `FunAudioLLM/CosyVoice2-0.5B:charles` | 男声 | +| `FunAudioLLM/CosyVoice2-0.5B:claire` | 女声 | +| `FunAudioLLM/CosyVoice2-0.5B:david` | 男声 | +| `FunAudioLLM/CosyVoice2-0.5B:diana` | 女声 | + +--- + +## gen — AI 视频生成 + +直接调用 SiliconFlow Video API 生成视频片段。异步模式:提交任务 → 轮询状态 → 下载结果。 + +> 生成视频通常需要 1–5 分钟,建议设置 exec timeout=600。 +> **每次最多生成 5 秒视频**。需要更长视频时,拆成多段顺序生成。 + +```bash +# 文生视频 +python3 ./skills/t2video/scripts/t2video.py gen \ + --prompt "海豚在夕阳下跃出海面" + +# 图生视频 +python3 ./skills/t2video/scripts/t2video.py gen \ + --model "Wan-AI/Wan2.2-I2V-A14B" \ + --prompt "镜头缓慢拉远" \ + --image "https://example.com/photo.jpg" + +# 指定分辨率和输出目录 +python3 ./skills/t2video/scripts/t2video.py gen \ + --prompt "延时摄影:花朵绽放" \ + --image-size 720x1280 \ + --out-dir ./output_video/my-video/fragments +``` + +### gen 参数 + +| Flag | Default | Description | +|------|---------|-------------| +| `--prompt` | required | 视频描述 | +| `--model` | `Wan-AI/Wan2.2-T2V-A14B` | 模型:T2V 或 I2V | +| `--image` | — | 图片 URL 或 base64(I2V 必需) | +| `--image-size` | `1280x720` | 分辨率:1280x720/720x1280/960x960 | +| `--negative-prompt` | — | 负面提示 | +| `--seed` | — | 随机种子 | +| `--poll-interval` | `10` | 轮询间隔(秒) | +| `--timeout` | `600` | 最大等待时间(秒) | +| `--out-dir` | `./tmp/sf-video-` | 输出目录 | + +--- + +## compose — 视频组装 + +将预准备的视觉素材(视频片段/图片)与旁白音频组装成最终 MP4。 + +### 素材编排计划 + +支持通过 `--plan` 参数指定素材的位置编排。编排计划为 JSON 文件,格式如下: + +```json +{ + "audio": "./output_video/my-video/fragments/speech.mp3", + "aspect": "9:16", + "tracks": [ + { + "position": "0%", + "material": "./output_video/my-video/fragments/video_01.mp4" + }, + { + "position": "30%", + "material": "./output_video/my-video/fragments/user_clip.mp4" + }, + { + "position": "70%", + "material": "./output_video/my-video/fragments/photo.jpg", + "duration": 8 + } + ] +} +``` + +**position 说明**: +- `"0%"` = 视频开头 +- `"50%"` = 视频中部 +- `"100%"` = 视频末尾 +- 也可以用秒数:`"5.0"` 表示第 5 秒 +- 图片必须指定 `duration`(显示秒数),视频则按原时长(不超过剩余空间) + +**编排逻辑**: +1. 根据音频时长确定视频总时长 +2. 按 position 排序,将用户指定素材插入对应时间点 +3. 未被用户素材覆盖的时间段,由 `--materials-dir` 中的素材按顺序填充 +4. 如果某段时间没有素材,使用占位符(深色纯色) + +### compose 参数 + +| Flag | Default | Description | +|------|---------|-------------| +| `--audio` | required | 旁白音频路径(mp3/wav) | +| `--materials-dir` | required | 素材目录(视频/图片),用于填充非指定位置 | +| `--plan` | — | 素材编排计划 JSON 文件路径(可选) | +| `--aspect` | `9:16` | 画面比例:9:16/16:9/1:1 | +| `--clip-duration` | `5` | 每段素材最大秒数 | +| `--transition` | `none` | 转场:none/fade | +| `--output` | required | 输出 MP4 路径 | + +### 基础用法(无编排计划) + +```bash +# 按素材目录顺序自动组装 +python3 ./skills/t2video/scripts/t2video.py compose \ + --audio ./output_video/my-video/fragments/speech.mp3 \ + --materials-dir ./output_video/my-video/fragments \ + --aspect 9:16 \ + --output ./output_video/my-video/video.mp4 +``` + +### 带编排计划的用法 + +```bash +# 在视频中部插入用户指定的片段 +python3 ./skills/t2video/scripts/t2video.py compose \ + --audio ./output_video/my-video/fragments/speech.mp3 \ + --materials-dir ./output_video/my-video/fragments \ + --plan ./output_video/my-video/plan.json \ + --aspect 9:16 \ + --output ./output_video/my-video/video.mp4 +``` + +--- + +## 完整工作流示例 + +```bash +WORKDIR="./output_video/my-video/fragments" +mkdir -p "$WORKDIR" + +# 1. 生成旁白 +python3 ./skills/t2video/scripts/t2video.py tts \ + --text "99%的人都不知道这个技巧..." \ + --voice zh-CN-YunxiNeural \ + --out-dir "$WORKDIR" + +# 2. 生成 AI 视频片段(每段 5s) +python3 ./skills/t2video/scripts/t2video.py gen \ + --prompt "科技感办公室场景" \ + --image-size 720x1280 \ + --out-dir "$WORKDIR" + +# 3. 编写编排计划(可选,如需指定素材位置) +cat > ./output_video/my-video/plan.json << 'EOF' +{ + "audio": "./output_video/my-video/fragments/speech.mp3", + "aspect": "9:16", + "tracks": [ + {"position": "0%", "material": "./output_video/my-video/fragments/video_01.mp4"}, + {"position": "50%", "material": "./output_video/my-video/fragments/user_product_demo.mp4"}, + {"position": "85%", "material": "./output_video/my-video/fragments/campaign_cover.jpg", "duration": 5} + ] +} +EOF + +# 4. 组装成品 +python3 ./skills/t2video/scripts/t2video.py compose \ + --audio "$WORKDIR/speech.mp3" \ + --materials-dir "$WORKDIR" \ + --plan ./output_video/my-video/plan.json \ + --aspect 9:16 \ + --output ./output_video/my-video/video.mp4 +``` + +--- + +## 视频剧本结构 + +生成脚本时遵循三幕结构: + +| 段落 | 时长占比 | 目标 | 示例 | +|------|---------|------|------| +| **开篇抓眼球** | 前 15–20% | 3 秒内让人停止划走 | "99% 的人都不知道…" / 强反差开场 | +| **中段讲卖点** | 60–70% | 展示核心价值 | 痛点 → 解决 → 数据佐证 | +| **结尾促下单** | 后 15–20% | 明确 CTA | "链接在简介" / "限时优惠" | + +**用户指定素材的编排**:当用户提出要在视频的某个位置加入其指定的视频片段、素材或图片时,应在编写剧本阶段就规划好该素材的位置和时长,并在编排计划中明确写出。用户的位置描述可能是模糊的(如"视频中部"、"末尾"),编写剧本时需转换为具体的 position 百分比或秒数。 + +--- + +## 素材优先级 + +materials-dir 中同时有视频和图片时,优先使用视频片段。无素材时使用占位符。 + +## 错误处理 + +| 错误 | 处理 | +|------|------| +| 音频文件不存在 | 检查路径,确保 tts 步骤完成 | +| 无素材文件 | 使用占位符(深色纯色) | +| MoviePy 导入失败 | `pip install moviepy` | +| edge-tts 导入失败 | `pip install edge-tts>=7.0` | +| SiliconFlow API 错误 | 检查 SILICONFLOW_API_KEY | +| 视频生成超时 | 增加 --timeout 或拆分更短的片段 | diff --git a/addons/officials/crew/selfmedia-operator/skills/t2video/scripts/t2video.py b/addons/officials/crew/selfmedia-operator/skills/t2video/scripts/t2video.py new file mode 100644 index 00000000..8ddabb91 --- /dev/null +++ b/addons/officials/crew/selfmedia-operator/skills/t2video/scripts/t2video.py @@ -0,0 +1,1223 @@ +#!/usr/bin/env python3 +""" +t2video — One-stop Short Video Production + +Three modes: + tts — Edge TTS (default, free) or SiliconFlow TTS → audio + gen — SiliconFlow Video Gen: prompt/image → video clip (mp4) + compose — MoviePy/FFmpeg: materials + audio → final MP4 (with optional plan for positioned inserts) + +Usage: + python3 t2video.py tts --text "Hello" [--provider edge|siliconflow] [--voice ...] + python3 t2video.py gen --prompt "..." [--model ...] [--out-dir ...] + python3 t2video.py compose --audio --materials-dir --output [--plan ] +""" + +import argparse +import asyncio +import contextlib +import glob +import io +import json +import os +import re +import subprocess +import sys +import tempfile +import time +import urllib.error +import urllib.request +from datetime import datetime +from pathlib import Path + + +# ════════════════════════════════════════════════════════════════════════════════ +# Shared Utilities +# ════════════════════════════════════════════════════════════════════════════════ + + +def log(msg: str) -> None: + ts = datetime.now().strftime("%H:%M:%S") + print(f"[{ts}] {msg}", flush=True) + + +def err(msg: str) -> None: + print(f"[ERROR] {msg}", file=sys.stderr, flush=True) + + +def die(msg: str) -> None: + err(msg) + sys.exit(1) + + +def ensure_dir(path: str) -> Path: + p = Path(path) + p.mkdir(parents=True, exist_ok=True) + return p + + +def slugify(text: str, max_len: int = 20) -> str: + text = re.sub(r"[^\w一-鿿]+", "-", text).strip("-") + return text[:max_len] + + +def _suppress_stdout(): + """Context manager to suppress MoviePy v2 FFMPEG_VideoReader stdout noise.""" + return contextlib.redirect_stdout(io.StringIO()) + + +def _ffmpeg_available() -> bool: + """Check if ffmpeg is available on the system.""" + try: + result = subprocess.run( + ["ffmpeg", "-version"], + capture_output=True, + text=True, + timeout=5, + ) + return result.returncode == 0 + except (FileNotFoundError, subprocess.TimeoutExpired): + return False + + +# ════════════════════════════════════════════════════════════════════════════════ +# Plan Utilities — Positioned Material Insertion +# ════════════════════════════════════════════════════════════════════════════════ + + +def parse_plan(plan_path: str) -> dict | None: + """Parse a compose plan JSON file. + + Returns dict with keys: audio (str), aspect (str), tracks (list[dict]). + Each track has: position (str), material (str), duration (float|None). + Returns None if plan_path is empty/None. + """ + if not plan_path: + return None + path = Path(plan_path) + if not path.is_file(): + die(f"Plan file not found: {plan_path}") + try: + data = json.loads(path.read_text(encoding="utf-8")) + except (json.JSONDecodeError, OSError) as exc: + die(f"Failed to read plan file: {exc}") + + tracks = data.get("tracks", []) + if not isinstance(tracks, list): + die("Plan 'tracks' must be a list") + + for i, track in enumerate(tracks): + if "position" not in track: + die(f"Track #{i} missing 'position'") + if "material" not in track: + die(f"Track #{i} missing 'material'") + if not os.path.isfile(track["material"]): + die(f"Track #{i} material not found: {track['material']}") + + return data + + +def resolve_position(position: str, total_duration: float) -> float: + """Convert a position specification to seconds. + + Supports: + - Percentage: "0%", "50%", "100%" + - Seconds: "5.0", "12.5" + """ + if isinstance(position, (int, float)): + return float(position) + + position = str(position).strip() + + if position.endswith("%"): + try: + pct = float(position[:-1]) + except ValueError: + die(f"Invalid position percentage: {position}") + return total_duration * (pct / 100.0) + + try: + return float(position) + except ValueError: + die(f"Invalid position: {position}. Use percentage ('50%') or seconds ('5.0')") + + +# ════════════════════════════════════════════════════════════════════════════════ +# TTS Mode — Edge TTS (default) / SiliconFlow TTS (fallback) +# ════════════════════════════════════════════════════════════════════════════════ + +# ── Edge TTS ───────────────────────────────────────────────────────────────── + +EDGE_TTS_DEFAULT_VOICE = "zh-CN-XiaoxiaoNeural" +EDGE_TTS_VALID_VOICES = { + "zh-CN-XiaoxiaoNeural", + "zh-CN-XiaoyiNeural", + "zh-CN-YunjianNeural", + "zh-CN-YunxiNeural", + "zh-CN-YunxiaNeural", + "zh-CN-YunyangNeural", + "zh-CN-liaoning-XiaobeiNeural", + "zh-CN-shaanxi-XiaoniNeural", + "en-US-JennyNeural", + "en-US-GuyNeural", + "en-US-AriaNeural", + "en-US-DavisNeural", + "ja-JP-NanamiNeural", + "ja-JP-KeitaNeural", + "ko-KR-SunHiNeural", + "ko-KR-InJoonNeural", +} + +# ── SiliconFlow TTS ───────────────────────────────────────────────────────── + +SF_TTS_DEFAULT_API_BASE = "https://api.siliconflow.cn/v1" +SF_TTS_DEFAULT_MODEL = "FunAudioLLM/CosyVoice2-0.5B" +SF_TTS_DEFAULT_VOICE = "FunAudioLLM/CosyVoice2-0.5B:alex" +SF_TTS_VALID_FORMATS = {"mp3", "opus", "wav", "pcm"} +SF_TTS_VALID_VOICES = { + "FunAudioLLM/CosyVoice2-0.5B:alex", + "FunAudioLLM/CosyVoice2-0.5B:anna", + "FunAudioLLM/CosyVoice2-0.5B:bella", + "FunAudioLLM/CosyVoice2-0.5B:benjamin", + "FunAudioLLM/CosyVoice2-0.5B:charles", + "FunAudioLLM/CosyVoice2-0.5B:claire", + "FunAudioLLM/CosyVoice2-0.5B:david", + "FunAudioLLM/CosyVoice2-0.5B:diana", +} + +# ── Common TTS ─────────────────────────────────────────────────────────────── + +TTS_SAFE_INPUT_DIRS = ( + Path("scripts"), + Path("assets"), + Path("tmp"), + Path("output_videos"), + Path("output_video"), +) +TTS_SAFE_OUTPUT_DIRS = ( + Path("assets/audio"), + Path("tmp"), + Path("output_videos"), + Path("output_video"), +) +TTS_TEXT_EXTENSIONS = {".txt", ".md"} +TTS_MAX_TEXT_FILE_BYTES = 512 * 1024 + + +def _tts_ensure_safe_path( + raw_path: str, allowed_dirs: tuple[Path, ...], purpose: str +) -> Path: + path = Path(raw_path) + if path.is_absolute(): + die(f"{purpose} path must be relative to the workspace") + if ".." in path.parts: + die(f"{purpose} path must not contain '..'") + root = Path.cwd().resolve() + resolved = (root / path).resolve() + if not any( + resolved == (root / base).resolve() + or resolved.is_relative_to((root / base).resolve()) + for base in allowed_dirs + ): + allowed = ", ".join(str(base) for base in allowed_dirs) + die(f"{purpose} path must be under one of: {allowed}") + return resolved + + +def _tts_read_text_file(raw_path: str) -> str: + path = _tts_ensure_safe_path(raw_path, TTS_SAFE_INPUT_DIRS, "--text-file") + if path.suffix.lower() not in TTS_TEXT_EXTENSIONS: + die( + f"--text-file must use one of these extensions: " + f"{', '.join(sorted(TTS_TEXT_EXTENSIONS))}" + ) + if not path.is_file(): + die(f"--text-file does not exist or is not a file: {raw_path}") + if path.stat().st_size > TTS_MAX_TEXT_FILE_BYTES: + die(f"--text-file exceeds {TTS_MAX_TEXT_FILE_BYTES} bytes") + try: + return path.read_text(encoding="utf-8") + except UnicodeDecodeError: + die("--text-file must be UTF-8 encoded") + except OSError as exc: + die(f"failed to read --text-file: {exc}") + raise AssertionError("unreachable") + + +def _tts_read_text(args: argparse.Namespace) -> str: + if args.text and args.text_file: + die("Use either --text or --text-file, not both") + if args.text_file: + text = _tts_read_text_file(args.text_file) + elif args.text: + text = args.text + else: + die("Either --text or --text-file is required") + text = text.strip() + if not text: + die("Input text is empty") + return text + + +def _tts_resolve_output_path(args: argparse.Namespace, ext: str) -> Path: + raw_path = args.output or str( + ( + Path(args.out_dir) + if args.out_dir + else Path(f"tmp/tts-{int(time.time())}") + ) + / f"speech.{ext}" + ) + output_path = _tts_ensure_safe_path(raw_path, TTS_SAFE_OUTPUT_DIRS, "output") + return output_path + + +# ── Edge TTS Implementation ────────────────────────────────────────────────── + + +def _edge_tts_available() -> bool: + try: + import edge_tts # noqa: F401 + + return True + except ImportError: + return False + + +def _edge_tts_validate_args(args: argparse.Namespace) -> None: + voice = args.voice or EDGE_TTS_DEFAULT_VOICE + if voice not in EDGE_TTS_VALID_VOICES: + die( + f"Unsupported edge-tts voice: {voice}. " + f"Valid voices: {', '.join(sorted(EDGE_TTS_VALID_VOICES))}" + ) + if args.speed is not None and not -50 <= args.speed <= 50: + die("--speed for edge-tts must be between -50 and 50 (percentage)") + + +def _edge_tts_run( + text: str, + voice: str, + rate: int | None, + output_path: Path, +) -> Path: + """Run edge-tts. Returns audio output path.""" + import edge_tts + + rate_str = f"{rate:+d}%" if rate is not None else "+0%" + + async def _do() -> bytearray: + communicate = edge_tts.Communicate(text, voice, rate=rate_str) + audio_data = bytearray() + + async for chunk in communicate.stream(): + if chunk["type"] == "audio": + audio_data.extend(chunk["data"]) + + if not audio_data: + die("edge-tts produced no audio output") + return audio_data + + loop = asyncio.new_event_loop() + try: + audio_data = loop.run_until_complete(_do()) + finally: + loop.close() + + output_path.parent.mkdir(parents=True, exist_ok=True) + output_path.write_bytes(bytes(audio_data)) + + return output_path + + +# ── SiliconFlow TTS Implementation ─────────────────────────────────────────── + + +def _sf_tts_validate_args(args: argparse.Namespace) -> None: + if args.sf_model != SF_TTS_DEFAULT_MODEL: + die(f"Unsupported SiliconFlow model: {args.sf_model}. Use {SF_TTS_DEFAULT_MODEL}") + voice = args.voice or SF_TTS_DEFAULT_VOICE + if voice and voice not in SF_TTS_VALID_VOICES: + die( + f"Unsupported SiliconFlow voice: {voice}. " + f"Valid voices: {', '.join(sorted(SF_TTS_VALID_VOICES))}" + ) + if len(args.text) > 128000: + die("Input text exceeds 128000 characters") + + +def _sf_tts_build_payload(args: argparse.Namespace, text: str) -> dict: + voice = args.voice or SF_TTS_DEFAULT_VOICE + payload: dict = { + "model": args.sf_model, + "input": text, + "voice": voice, + "response_format": args.format, + } + if args.sample_rate is not None: + payload["sample_rate"] = args.sample_rate + if args.speed is not None: + payload["speed"] = args.speed + return payload + + +def _sf_tts_create_speech(api_base: str, api_key: str, payload: dict) -> bytes: + data = json.dumps(payload, ensure_ascii=False).encode("utf-8") + req = urllib.request.Request( + f"{api_base.rstrip('/')}/audio/speech", + data=data, + headers={ + "Authorization": f"Bearer {api_key}", + "Content-Type": "application/json", + }, + method="POST", + ) + try: + with urllib.request.urlopen(req, timeout=120) as resp: + return resp.read() + except urllib.error.HTTPError as exc: + body = exc.read().decode(errors="replace") + die(f"HTTP {exc.code}: {body}") + except urllib.error.URLError as exc: + die(f"request failed: {exc.reason}") + raise AssertionError("unreachable") + + +def _sf_tts_run( + text: str, + args: argparse.Namespace, + output_path: Path, +) -> Path: + """Run SiliconFlow TTS. Returns audio output path.""" + payload = _sf_tts_build_payload(args, text) + + api_key = os.environ.get("SILICONFLOW_API_KEY", "").strip() + if not api_key: + die("SILICONFLOW_API_KEY not set") + api_base = ( + os.environ.get("SILICONFLOW_API_BASE", SF_TTS_DEFAULT_API_BASE).strip() + or SF_TTS_DEFAULT_API_BASE + ) + + output_path.parent.mkdir(parents=True, exist_ok=True) + audio = _sf_tts_create_speech(api_base, api_key, payload) + if not audio: + die("empty audio response") + output_path.write_bytes(audio) + + return output_path + + +# ── TTS Main ───────────────────────────────────────────────────────────────── + + +def tts_main(args: argparse.Namespace) -> None: + text = _tts_read_text(args) + provider = args.provider + + if provider == "edge": + if not _edge_tts_available(): + die("edge-tts is not installed. Run: pip install edge-tts>=7.0") + _edge_tts_validate_args(args) + fmt = "mp3" + output_path = _tts_resolve_output_path(args, fmt) + voice = args.voice or EDGE_TTS_DEFAULT_VOICE + log(f"TTS [edge]: voice={voice} chars={len(text)}") + audio_path = _edge_tts_run(text, voice, args.speed, output_path) + log(f"TTS done: {audio_path}") + + elif provider == "siliconflow": + _sf_tts_validate_args(args) + output_path = _tts_resolve_output_path(args, args.format) + voice = args.voice or SF_TTS_DEFAULT_VOICE + log(f"TTS [siliconflow]: voice={voice} format={args.format} chars={len(text)}") + audio_path = _sf_tts_run(text, args, output_path) + log(f"TTS done: {audio_path}") + + else: + die(f"Unknown TTS provider: {provider}. Use 'edge' or 'siliconflow'.") + + # Save metadata + meta = { + "mode": "tts", + "provider": provider, + "voice": args.voice or ( + EDGE_TTS_DEFAULT_VOICE if provider == "edge" else SF_TTS_DEFAULT_VOICE + ), + "text_chars": len(text), + "audio_file": str(audio_path), + } + meta_path = audio_path.with_suffix(".json") + meta_path.write_text(json.dumps(meta, ensure_ascii=False, indent=2), encoding="utf-8") + + +# ════════════════════════════════════════════════════════════════════════════════ +# Gen Mode — SiliconFlow Video Generation +# ════════════════════════════════════════════════════════════════════════════════ + +GEN_SUBMIT_URL = "https://api.siliconflow.cn/v1/video/submit" +GEN_STATUS_URL = "https://api.siliconflow.cn/v1/video/status" + +GEN_T2V_MODEL = "Wan-AI/Wan2.2-T2V-A14B" +GEN_I2V_MODEL = "Wan-AI/Wan2.2-I2V-A14B" +GEN_VALID_SIZES = {"1280x720", "720x1280", "960x960"} + + +def _gen_post_json(url: str, payload: dict, api_key: str, timeout: int = 60) -> dict: + data = json.dumps(payload).encode() + req = urllib.request.Request( + url, + data=data, + headers={ + "Authorization": f"Bearer {api_key}", + "Content-Type": "application/json", + }, + method="POST", + ) + try: + with urllib.request.urlopen(req, timeout=timeout) as resp: + return json.loads(resp.read()) + except urllib.error.HTTPError as e: + body = e.read().decode(errors="replace") + die(f"HTTP {e.code}: {body}") + except urllib.error.URLError as exc: + die(f"request failed: {exc.reason}") + raise AssertionError("unreachable") + + +def _gen_submit_job(payload: dict, api_key: str) -> str: + result = _gen_post_json(GEN_SUBMIT_URL, payload, api_key, timeout=60) + rid = result.get("requestId") + if not rid: + die(f"No requestId in response: {result}") + return rid + + +def _gen_poll_until_done( + request_id: str, api_key: str, poll_interval: int, timeout: int +) -> dict: + deadline = time.time() + timeout + attempt = 0 + while time.time() < deadline: + attempt += 1 + result = _gen_post_json( + GEN_STATUS_URL, {"requestId": request_id}, api_key, timeout=30 + ) + status = result.get("status", "") + log(f"Gen poll #{attempt}: status={status}") + if status == "Succeed": + return result + if status == "Failed": + reason = result.get("reason", "unknown") + die(f"Generation failed: {reason}") + time.sleep(poll_interval) + die(f"Timed out after {timeout}s") + + +def _gen_download_video(url: str, dest_path: Path) -> None: + log(f"Downloading video -> {dest_path}") + req = urllib.request.Request(url, headers={"User-Agent": "wiseflow-t2video/1.0"}) + with urllib.request.urlopen(req, timeout=300) as resp: + dest_path.write_bytes(resp.read()) + + +def gen_main(args: argparse.Namespace) -> None: + if args.model == GEN_I2V_MODEL and not args.image: + die(f"--image is required when using model '{GEN_I2V_MODEL}'") + + api_key = os.environ.get("SILICONFLOW_API_KEY", "").strip() + if not api_key: + die("SILICONFLOW_API_KEY not set") + + ts = int(time.time()) + out_dir = Path(args.out_dir) if args.out_dir else Path(f"./tmp/sf-video-{ts}") + out_dir.mkdir(parents=True, exist_ok=True) + + payload: dict = { + "model": args.model, + "prompt": args.prompt, + "image_size": args.image_size, + } + if args.image: + payload["image"] = args.image + if args.negative_prompt: + payload["negative_prompt"] = args.negative_prompt + if args.seed is not None: + payload["seed"] = args.seed + + log(f"Gen: submitting job model={args.model} size={args.image_size}") + request_id = _gen_submit_job(payload, api_key) + log(f"Gen: jobId={request_id}") + log(f"Gen: polling every {args.poll_interval}s (timeout={args.timeout}s)") + + result = _gen_poll_until_done( + request_id, api_key, args.poll_interval, args.timeout + ) + + videos = result.get("results", {}).get("videos", []) + if not videos: + die(f"No videos in result: {result}") + + video_url = videos[0].get("url", "") + if not video_url: + die("Empty video URL in result") + + video_path = out_dir / f"video_{request_id[:8]}.mp4" + _gen_download_video(video_url, video_path) + + result_path = out_dir / "result.json" + result_path.write_text(json.dumps(result, ensure_ascii=False, indent=2)) + + log(f"Gen done: {video_path}") + log(f"Metadata: {result_path}") + + +# ════════════════════════════════════════════════════════════════════════════════ +# Compose Mode — Video Assembly +# ════════════════════════════════════════════════════════════════════════════════ + +# ── MoviePy Version Detection ──────────────────────────────────────────────── + + +def _get_moviepy_major() -> int: + try: + import moviepy + + return int(moviepy.__version__.split(".")[0]) + except Exception: + return 2 + + +_MOVIEPY_MAJOR = _get_moviepy_major() + +# ── Constants ──────────────────────────────────────────────────────────────── + +ASPECT_RESOLUTIONS = { + "9:16": (1080, 1920), + "16:9": (1920, 1080), + "1:1": (1080, 1080), +} + +VIDEO_EXTS = {".mp4", ".mov", ".avi", ".webm", ".mkv"} +IMAGE_EXTS = {".jpg", ".jpeg", ".png", ".webp", ".bmp"} + +PLACEHOLDER_COLOR = (30, 30, 50) +SEGMENT_CODEC = "libx264" +SEGMENT_FPS = 30 +SEGMENT_PIX_FMT = "yuv420p" + +# ── Audio Helpers ──────────────────────────────────────────────────────────── + + +def get_audio_duration(audio_path: str) -> float: + from moviepy import AudioFileClip + + clip = None + try: + with _suppress_stdout(): + clip = AudioFileClip(audio_path) + duration = clip.duration + return duration + finally: + if clip is not None: + clip.close() + + +# ── Material Collection ────────────────────────────────────────────────────── + + +def collect_materials(materials_dir: str) -> list[dict]: + items: list[dict] = [] + for ext in VIDEO_EXTS: + for p in glob.glob(os.path.join(materials_dir, f"*{ext}")): + items.append({"path": p, "type": "video"}) + for p in glob.glob(os.path.join(materials_dir, f"*{ext.upper()}")): + items.append({"path": p, "type": "video"}) + for ext in IMAGE_EXTS: + for p in glob.glob(os.path.join(materials_dir, f"*{ext}")): + items.append({"path": p, "type": "image"}) + for p in glob.glob(os.path.join(materials_dir, f"*{ext.upper()}")): + items.append({"path": p, "type": "image"}) + return sorted(items, key=lambda x: x["path"]) + + +# ── FFmpeg Concat ──────────────────────────────────────────────────────────── + + +def concat_video_clips_with_ffmpeg( + segment_paths: list[str], + output_path: str, + audio_path: str, +) -> str: + if not segment_paths: + raise ValueError("No segment paths provided for ffmpeg concat") + + fd, concat_list_path = tempfile.mkstemp(suffix=".txt", prefix="t2v_concat_") + os.close(fd) + concat_output = output_path + ".concat_tmp.mp4" + + try: + with open(concat_list_path, "w", encoding="utf-8") as f: + for seg_path in segment_paths: + escaped = seg_path.replace("'", "'\\''") + f.write(f"file '{escaped}'\n") + + log(f"FFmpeg concat: joining {len(segment_paths)} segments (stream copy)") + cmd_concat = [ + "ffmpeg", "-y", "-f", "concat", "-safe", "0", + "-i", concat_list_path, "-c", "copy", concat_output, + ] + result = subprocess.run(cmd_concat, capture_output=True, text=True) + if result.returncode != 0: + log("FFmpeg concat: stream copy failed, falling back to re-encode") + cmd_reencode = [ + "ffmpeg", "-y", "-f", "concat", "-safe", "0", + "-i", concat_list_path, + "-c:v", SEGMENT_CODEC, "-pix_fmt", SEGMENT_PIX_FMT, + "-r", str(SEGMENT_FPS), concat_output, + ] + result = subprocess.run(cmd_reencode, capture_output=True, text=True) + if result.returncode != 0: + raise RuntimeError( + f"FFmpeg concat failed (re-encode): {result.stderr[-500:]}" + ) + + log("FFmpeg mux: adding audio track") + cmd_mux = [ + "ffmpeg", "-y", + "-i", concat_output, "-i", audio_path, + "-c:v", "copy", "-c:a", "aac", "-b:a", "192k", + "-map", "0:v:0", "-map", "1:a:0", + "-shortest", output_path, + ] + result = subprocess.run(cmd_mux, capture_output=True, text=True) + if result.returncode != 0: + raise RuntimeError(f"FFmpeg audio mux failed: {result.stderr[-500:]}") + + finally: + for path in (concat_list_path, concat_output): + if os.path.exists(path): + try: + os.remove(path) + except OSError: + pass + + return output_path + + +# ── Video Composition (MoviePy) ────────────────────────────────────────────── + + +def make_placeholder_clip(duration: float, resolution: tuple, color: tuple): + from moviepy import ColorClip + + return ColorClip(size=resolution, color=color, duration=duration) + + +def image_to_clip(image_path: str, duration: float, resolution: tuple): + from moviepy import ImageClip + + with _suppress_stdout(): + clip = ImageClip(image_path, duration=duration) + clip = clip.resized(resolution) + if clip.size[0] != resolution[0] or clip.size[1] != resolution[1]: + x_center = clip.size[0] / 2 + y_center = clip.size[1] / 2 + clip = clip.cropped( + x_center=x_center, + y_center=y_center, + width=resolution[0], + height=resolution[1], + ) + return clip + + +def video_to_clip(video_path: str, max_duration: float, resolution: tuple): + from moviepy import VideoFileClip + + with _suppress_stdout(): + clip = VideoFileClip(video_path).without_audio() + if clip.duration > max_duration: + clip = clip.subclipped(0, max_duration) + clip = clip.resized(resolution) + if clip.size[0] != resolution[0] or clip.size[1] != resolution[1]: + x_center = clip.size[0] / 2 + y_center = clip.size[1] / 2 + clip = clip.cropped( + x_center=x_center, + y_center=y_center, + width=resolution[0], + height=resolution[1], + ) + return clip + + +def _apply_crossfade(clip, duration: float = 0.3): + if _MOVIEPY_MAJOR >= 2: + from moviepy.video.fx.CrossFadeIn import CrossFadeIn + + return clip.with_effects([CrossFadeIn(duration)]) + else: + return clip.crossfadein(duration) + + +def _concat_clips(clips: list): + from moviepy import concatenate_videoclips + + if _MOVIEPY_MAJOR >= 2: + return concatenate_videoclips(clips) + else: + return concatenate_videoclips(clips, method="compose") + + +def _write_clip(clip, output_path: str, *, audio: bool = True, **kwargs) -> None: + write_kwargs: dict = { + "fps": SEGMENT_FPS, + "codec": SEGMENT_CODEC, + "logger": None, + } + if audio: + write_kwargs["audio_codec"] = "aac" + write_kwargs["audio_fps"] = 44100 + write_kwargs["audio_bitrate"] = "192k" + else: + write_kwargs["audio"] = False + + if _MOVIEPY_MAJOR >= 2: + write_kwargs["pixel_format"] = SEGMENT_PIX_FMT + else: + write_kwargs["verbose"] = False + + write_kwargs.update(kwargs) + clip.write_videofile(output_path, **write_kwargs) + + +# ── Timeline Builder ───────────────────────────────────────────────────────── + + +def _build_timeline( + plan: dict | None, + audio_duration: float, +) -> list[dict]: + """Build a timeline of segments from plan tracks. + + Each segment: {"start": float, "end": float, "material": str, "type": str} + - type is "video" or "image" for plan tracks, "auto" for gap-fill from materials-dir + """ + if not plan or not plan.get("tracks"): + return [{"start": 0.0, "end": audio_duration, "material": "", "type": "auto"}] + + tracks = sorted( + plan["tracks"], + key=lambda t: resolve_position(t["position"], audio_duration), + ) + + segments: list[dict] = [] + prev_end = 0.0 + + for track in tracks: + start = resolve_position(track["position"], audio_duration) + if start < prev_end: + err( + f"Warning: track at {track['position']} overlaps previous segment, " + f"clamping start to {prev_end:.1f}s" + ) + start = prev_end + + # Gap before this track + if start > prev_end + 0.01: + segments.append( + {"start": prev_end, "end": start, "material": "", "type": "auto"} + ) + + # Determine material type and duration + mat_path = track["material"] + mat_ext = Path(mat_path).suffix.lower() + remaining = audio_duration - start + + if mat_ext in VIDEO_EXTS: + mat_type = "video" + from moviepy import VideoFileClip + + with _suppress_stdout(): + vc = VideoFileClip(mat_path) + mat_dur = vc.duration + vc.close() + mat_dur = min(mat_dur, remaining) + elif mat_ext in IMAGE_EXTS: + mat_type = "image" + mat_dur = track.get("duration") + if not mat_dur: + die(f"Image material '{mat_path}' must specify 'duration' in plan") + mat_dur = min(float(mat_dur), remaining) + else: + die(f"Unsupported material type: {mat_path}") + + end = start + mat_dur + segments.append( + {"start": start, "end": end, "material": mat_path, "type": mat_type} + ) + prev_end = end + + # Gap after last track + if prev_end < audio_duration - 0.01: + segments.append( + {"start": prev_end, "end": audio_duration, "material": "", "type": "auto"} + ) + + return segments + + +# ── Main Composition ───────────────────────────────────────────────────────── + + +def compose_video( + materials: list[dict], + audio_path: str, + output_path: str, + aspect: str, + clip_duration: float, + transition: str, + plan: dict | None = None, +) -> str: + from moviepy import AudioFileClip + + resolution = ASPECT_RESOLUTIONS.get(aspect, (1080, 1920)) + audio_duration = get_audio_duration(audio_path) + log(f"Audio duration: {audio_duration:.1f}s") + + video_items = [m for m in materials if m["type"] == "video"] + image_items = [m for m in materials if m["type"] == "image"] + + if video_items: + primary = video_items + primary_type = "video" + elif image_items: + primary = image_items + primary_type = "image" + else: + primary = [] + primary_type = "none" + + plan_tracks = plan.get("tracks", []) if plan else [] + log( + f"Compose: {len(primary)} {primary_type} materials, " + f"{len(plan_tracks)} plan tracks, " + f"duration={audio_duration:.1f}s, aspect={aspect}" + ) + + # ── Build timeline ────────────────────────────────────────────────── + timeline = _build_timeline(plan, audio_duration) + + # ── Build segment clips from timeline ─────────────────────────────── + clips = [] + auto_idx = 0 + + for segment in timeline: + seg_dur = segment["end"] - segment["start"] + if seg_dur <= 0.01: + continue + + if segment["type"] == "auto": + if not primary: + clips.append( + make_placeholder_clip(seg_dur, resolution, PLACEHOLDER_COLOR) + ) + continue + remaining = seg_dur + while remaining > 0.01: + item = primary[auto_idx % len(primary)] + dur = min(clip_duration, remaining) + try: + if item["type"] == "image" or primary_type == "image": + clip = image_to_clip(item["path"], dur, resolution) + else: + clip = video_to_clip(item["path"], dur, resolution) + except Exception as e: + err(f"Failed to load '{item['path']}': {e} — using placeholder") + clip = make_placeholder_clip(dur, resolution, PLACEHOLDER_COLOR) + clips.append(clip) + remaining -= dur + auto_idx += 1 + elif segment["type"] == "video": + try: + clip = video_to_clip(segment["material"], seg_dur, resolution) + except Exception as e: + err(f"Failed to load '{segment['material']}': {e} — using placeholder") + clip = make_placeholder_clip(seg_dur, resolution, PLACEHOLDER_COLOR) + clips.append(clip) + elif segment["type"] == "image": + try: + clip = image_to_clip(segment["material"], seg_dur, resolution) + except Exception as e: + err(f"Failed to load '{segment['material']}': {e} — using placeholder") + clip = make_placeholder_clip(seg_dur, resolution, PLACEHOLDER_COLOR) + clips.append(clip) + + if not clips: + log("Compose: no clips generated — using placeholder") + with _suppress_stdout(): + audio = AudioFileClip(audio_path) + try: + placeholder = make_placeholder_clip( + audio_duration, resolution, PLACEHOLDER_COLOR + ) + final = placeholder.with_audio(audio) + _write_clip(final, output_path) + finally: + audio.close() + return output_path + + # ── Concatenate video clips ────────────────────────────────────────── + use_ffmpeg = transition == "none" and _ffmpeg_available() + + if use_ffmpeg: + log(f"Compose: using ffmpeg concat path ({len(clips)} segments)") + tmpdir = tempfile.mkdtemp(prefix="t2v_segments_") + segment_paths: list[str] = [] + try: + for i, clip in enumerate(clips): + seg_path = os.path.join(tmpdir, f"seg_{i:04d}.mp4") + _write_clip(clip, seg_path, audio=False) + segment_paths.append(seg_path) + clip.close() + + concat_video_clips_with_ffmpeg(segment_paths, output_path, audio_path) + finally: + for seg_path in segment_paths: + if os.path.exists(seg_path): + try: + os.remove(seg_path) + except OSError: + pass + try: + os.rmdir(tmpdir) + except OSError: + pass + else: + log(f"Compose: using MoviePy path (transition={transition})") + + if transition == "fade" and len(clips) > 1: + faded = [] + for i, c in enumerate(clips): + if i > 0: + c = _apply_crossfade(c, 0.3) + faded.append(c) + clips = faded + + video = _concat_clips(clips) + + with _suppress_stdout(): + audio = AudioFileClip(audio_path) + try: + video = ( + video.with_audio(audio) + .with_duration(audio_duration) + .with_fps(SEGMENT_FPS) + ) + _write_clip(video, output_path) + finally: + audio.close() + + log(f"Compose: written -> {output_path}") + return output_path + + +def compose_main(args: argparse.Namespace) -> None: + if not os.path.exists(args.audio): + die(f"Audio file not found: {args.audio}") + if not os.path.isdir(args.materials_dir): + die(f"Materials directory not found: {args.materials_dir}") + + # Parse plan if provided + plan = parse_plan(getattr(args, "plan", None)) + + materials = collect_materials(args.materials_dir) + if not materials and not (plan and plan.get("tracks")): + err( + f"Warning: no video/image files found in {args.materials_dir} " + "— will use placeholder" + ) + + ensure_dir(os.path.dirname(args.output) or ".") + + compose_video( + materials=materials, + audio_path=args.audio, + output_path=args.output, + aspect=args.aspect, + clip_duration=args.clip_duration, + transition=args.transition, + plan=plan, + ) + + # Save metadata + audio_duration = get_audio_duration(args.audio) + meta = { + "mode": "compose", + "audio": args.audio, + "materials_dir": args.materials_dir, + "material_count": len(materials), + "audio_duration": round(audio_duration, 1), + "aspect": args.aspect, + "transition": args.transition, + "plan": getattr(args, "plan", None), + "output": args.output, + "generated_at": datetime.now().isoformat(), + } + meta_path = os.path.splitext(args.output)[0] + ".json" + with open(meta_path, "w", encoding="utf-8") as f: + json.dump(meta, f, ensure_ascii=False, indent=2) + + log(f"Done: {args.output}") + + +# ════════════════════════════════════════════════════════════════════════════════ +# Entry Point — Subcommand Router +# ════════════════════════════════════════════════════════════════════════════════ + + +def main() -> None: + parser = argparse.ArgumentParser( + description="t2video — One-stop Short Video Production", + ) + subparsers = parser.add_subparsers(dest="mode", help="Operation mode") + + # ── tts subcommand ──────────────────────────────────────────────────── + tts_parser = subparsers.add_parser("tts", help="Text-to-speech") + tts_parser.add_argument("--text", default=None, help="Text to synthesize") + tts_parser.add_argument( + "--text-file", default=None, dest="text_file", help="UTF-8 text file" + ) + tts_parser.add_argument( + "--provider", + default="edge", + choices=["edge", "siliconflow"], + help="TTS provider: edge (default, free) or siliconflow", + ) + tts_parser.add_argument( + "--voice", + default=None, + help="Voice ID (provider-specific, see docs)", + ) + tts_parser.add_argument( + "--format", + default="mp3", + choices=sorted(SF_TTS_VALID_FORMATS), + help="Audio response format (siliconflow only)", + ) + tts_parser.add_argument( + "--speed", + type=int, + default=None, + help="Speech speed. Edge: -50 to 50 (percentage). SiliconFlow: 0.25 to 4.0", + ) + tts_parser.add_argument( + "--sample-rate", + type=int, + default=None, + dest="sample_rate", + help="Output sample rate (siliconflow only)", + ) + tts_parser.add_argument( + "--sf-model", + default=SF_TTS_DEFAULT_MODEL, + dest="sf_model", + help="SiliconFlow TTS model ID", + ) + tts_parser.add_argument( + "--output", default=None, help="Exact output file path" + ) + tts_parser.add_argument( + "--out-dir", default=None, dest="out_dir", help="Output directory" + ) + + # ── gen subcommand ──────────────────────────────────────────────────── + gen_parser = subparsers.add_parser( + "gen", help="AI video generation via SiliconFlow" + ) + gen_parser.add_argument("--prompt", required=True, help="Video description") + gen_parser.add_argument( + "--model", + default=GEN_T2V_MODEL, + choices=[GEN_T2V_MODEL, GEN_I2V_MODEL], + help="Model ID", + ) + gen_parser.add_argument( + "--image", + default=None, + help="Image URL or base64 data URI (required for I2V model)", + ) + gen_parser.add_argument( + "--image-size", + default="1280x720", + choices=sorted(GEN_VALID_SIZES), + dest="image_size", + help="Video resolution", + ) + gen_parser.add_argument( + "--negative-prompt", default=None, dest="negative_prompt" + ) + gen_parser.add_argument("--seed", type=int, default=None) + gen_parser.add_argument( + "--poll-interval", type=int, default=10, dest="poll_interval" + ) + gen_parser.add_argument("--timeout", type=int, default=600) + gen_parser.add_argument( + "--out-dir", default=None, dest="out_dir", help="Output directory" + ) + + # ── compose subcommand ──────────────────────────────────────────────── + compose_parser = subparsers.add_parser( + "compose", help="Compose video from materials + audio" + ) + compose_parser.add_argument( + "--audio", required=True, help="Path to narration audio file (mp3/wav)" + ) + compose_parser.add_argument( + "--materials-dir", + required=True, + help="Directory containing video clips and/or images", + ) + compose_parser.add_argument( + "--plan", + default=None, + help="Path to plan JSON file for positioned material insertion", + ) + compose_parser.add_argument( + "--aspect", + default="9:16", + choices=["9:16", "16:9", "1:1"], + help="Output aspect ratio (default: 9:16)", + ) + compose_parser.add_argument( + "--clip-duration", + type=float, + default=5.0, + help="Max seconds per material clip (default: 5)", + ) + compose_parser.add_argument( + "--transition", + default="none", + choices=["none", "fade"], + help="Clip transitions (default: none)", + ) + compose_parser.add_argument( + "--output", required=True, help="Output MP4 file path" + ) + + args = parser.parse_args() + + if args.mode == "tts": + tts_main(args) + elif args.mode == "gen": + gen_main(args) + elif args.mode == "compose": + compose_main(args) + else: + parser.print_help() + sys.exit(1) + + +if __name__ == "__main__": + main() diff --git a/addons/officials/requirements.txt b/addons/officials/requirements.txt index 317e46ac..6992b15c 100644 --- a/addons/officials/requirements.txt +++ b/addons/officials/requirements.txt @@ -3,3 +3,5 @@ python-pptx>=1.0.0 Pillow>=10.0.0 requests>=2.28.0 +moviepy>=1.0.3 +edge-tts>=7.0 diff --git a/addons/officials/skills/siliconflow-video-gen/SKILL.md b/addons/officials/skills/siliconflow-video-gen/SKILL.md deleted file mode 100644 index a4e5d91c..00000000 --- a/addons/officials/skills/siliconflow-video-gen/SKILL.md +++ /dev/null @@ -1,78 +0,0 @@ ---- -name: siliconflow-video-gen -description: Generate videos via SiliconFlow Video API. Supports text-to-video (T2V) and image-to-video (I2V) using Wan2.2 models. Async: submit job → poll until done → download. -homepage: https://docs.siliconflow.cn/cn/userguide/capabilities/video -metadata: - { - "openclaw": - { - "emoji": "🎬", - "requires": { "bins": ["python3"], "env": ["SILICONFLOW_API_KEY"] }, - "primaryEnv": "SILICONFLOW_API_KEY", - }, - } ---- - -# SiliconFlow Video Gen - -Generate videos using the SiliconFlow Video API (Wan2.2 models). - -Video generation is **asynchronous**: the API returns a `requestId` immediately, then the script polls the status endpoint until the job completes (status: `Succeed`). - -> The generated video URL is valid for **1 hour**. The script downloads the video locally automatically. - -## Run - -Note: Video generation typically takes **1–5 minutes**. Set exec timeout accordingly (e.g., `exec timeout=600`). - -```bash -# Text-to-video -python3 {baseDir}/scripts/gen.py --prompt "a dolphin leaping over ocean waves at sunset" - -# Image-to-video (provide a public URL or local base64 image) -python3 {baseDir}/scripts/gen.py \ - --model "Wan-AI/Wan2.2-I2V-A14B" \ - --prompt "the camera slowly zooms out" \ - --image "https://example.com/my-photo.jpg" - -# Custom resolution and output directory -python3 {baseDir}/scripts/gen.py \ - --prompt "time-lapse of a blooming flower" \ - --image-size 720x1280 \ - --out-dir ./out/videos - -# Reproducible generation with a fixed seed -python3 {baseDir}/scripts/gen.py --prompt "rocket launch" --seed 42 -``` - -## Parameters - -| Flag | Default | Description | -|------|---------|-------------| -| `--prompt` | required | Text description of the video | -| `--model` | `Wan-AI/Wan2.2-T2V-A14B` | Model ID: `Wan-AI/Wan2.2-T2V-A14B` (T2V) or `Wan-AI/Wan2.2-I2V-A14B` (I2V) | -| `--image` | — | Image URL or `data:image/...;base64,...` (required for I2V model) | -| `--image-size` | `1280x720` | Resolution: `1280x720` (16:9), `720x1280` (9:16), `960x960` (1:1) | -| `--negative-prompt` | — | What to avoid in the video | -| `--seed` | — | Random seed for reproducibility | -| `--poll-interval` | `10` | Seconds between status polls | -| `--timeout` | `600` | Max seconds to wait for generation | -| `--out-dir` | `./tmp/sf-video-` | Output directory | - -## Models - -| Model | Type | Notes | -|-------|------|-------| -| `Wan-AI/Wan2.2-T2V-A14B` | Text → Video | Default model | -| `Wan-AI/Wan2.2-I2V-A14B` | Image → Video | Requires `--image` parameter | - -## Output - -- `video_.mp4` downloaded locally -- `result.json` with full API response - -## Environment Variables - -| Variable | Description | -|----------|-------------| -| `SILICONFLOW_API_KEY` | Your SiliconFlow API key (required) | diff --git a/addons/officials/skills/siliconflow-video-gen/scripts/gen.py b/addons/officials/skills/siliconflow-video-gen/scripts/gen.py deleted file mode 100644 index c04943a5..00000000 --- a/addons/officials/skills/siliconflow-video-gen/scripts/gen.py +++ /dev/null @@ -1,165 +0,0 @@ -#!/usr/bin/env python3 -"""SiliconFlow video generation — stdlib only (no httpx/requests). - -Flow: - 1. POST /v1/video/submit → requestId - 2. Poll POST /v1/video/status every --poll-interval seconds - 3. When status == 'Succeed', download video to --out-dir -""" - -import argparse -import json -import os -import sys -import time -import urllib.request -import urllib.error -from pathlib import Path - -SUBMIT_URL = "https://api.siliconflow.cn/v1/video/submit" -STATUS_URL = "https://api.siliconflow.cn/v1/video/status" - -T2V_MODEL = "Wan-AI/Wan2.2-T2V-A14B" -I2V_MODEL = "Wan-AI/Wan2.2-I2V-A14B" -VALID_SIZES = {"1280x720", "720x1280", "960x960"} - - -def post_json(url, payload, api_key, timeout=60): - data = json.dumps(payload).encode() - req = urllib.request.Request( - url, - data=data, - headers={ - "Authorization": f"Bearer {api_key}", - "Content-Type": "application/json", - }, - method="POST", - ) - try: - with urllib.request.urlopen(req, timeout=timeout) as resp: - return json.loads(resp.read()) - except urllib.error.HTTPError as e: - body = e.read().decode(errors="replace") - print(f"[error] HTTP {e.code}: {body}", file=sys.stderr) - sys.exit(1) - - -def submit_job(payload, api_key): - result = post_json(SUBMIT_URL, payload, api_key, timeout=60) - rid = result.get("requestId") - if not rid: - print(f"[error] No requestId in response: {result}", file=sys.stderr) - sys.exit(1) - return rid - - -def poll_until_done(request_id, api_key, poll_interval, timeout): - deadline = time.time() + timeout - attempt = 0 - while time.time() < deadline: - attempt += 1 - result = post_json(STATUS_URL, {"requestId": request_id}, api_key, timeout=30) - status = result.get("status", "") - print(f"[info] poll #{attempt}: status={status}") - if status == "Succeed": - return result - if status == "Failed": - reason = result.get("reason", "unknown") - print(f"[error] Generation failed: {reason}", file=sys.stderr) - sys.exit(1) - # InQueue or InProgress — wait and retry - time.sleep(poll_interval) - print(f"[error] Timed out after {timeout}s", file=sys.stderr) - sys.exit(1) - - -def download_video(url, dest_path): - """Stream-download the video file to dest_path.""" - print(f"[info] Downloading video → {dest_path}") - req = urllib.request.Request(url, headers={"User-Agent": "wiseflow-video-gen/1.0"}) - with urllib.request.urlopen(req, timeout=300) as resp: - dest_path.write_bytes(resp.read()) - - -def main(): - parser = argparse.ArgumentParser(description="SiliconFlow video generation") - parser.add_argument("--prompt", required=True, help="Video description") - parser.add_argument( - "--model", - default=T2V_MODEL, - choices=[T2V_MODEL, I2V_MODEL], - help="Model ID", - ) - parser.add_argument( - "--image", - default=None, - help="Image URL or base64 data URI (required for I2V model)", - ) - parser.add_argument( - "--image-size", - default="1280x720", - choices=sorted(VALID_SIZES), - dest="image_size", - help="Video resolution", - ) - parser.add_argument("--negative-prompt", default=None, dest="negative_prompt") - parser.add_argument("--seed", type=int, default=None) - parser.add_argument("--poll-interval", type=int, default=10, dest="poll_interval") - parser.add_argument("--timeout", type=int, default=600) - parser.add_argument("--out-dir", default=None, dest="out_dir") - args = parser.parse_args() - - if args.model == I2V_MODEL and not args.image: - print(f"[error] --image is required when using model '{I2V_MODEL}'", file=sys.stderr) - sys.exit(1) - - api_key = os.environ.get("SILICONFLOW_API_KEY") - if not api_key: - print("[error] SILICONFLOW_API_KEY not set", file=sys.stderr) - sys.exit(1) - - ts = int(time.time()) - out_dir = Path(args.out_dir) if args.out_dir else Path(f"./tmp/sf-video-{ts}") - out_dir.mkdir(parents=True, exist_ok=True) - - payload = { - "model": args.model, - "prompt": args.prompt, - "image_size": args.image_size, - } - if args.image: - payload["image"] = args.image - if args.negative_prompt: - payload["negative_prompt"] = args.negative_prompt - if args.seed is not None: - payload["seed"] = args.seed - - print(f"[info] Submitting job: model={args.model} size={args.image_size}") - request_id = submit_job(payload, api_key) - print(f"[info] Job submitted. requestId={request_id}") - print(f"[info] Polling every {args.poll_interval}s (timeout={args.timeout}s)…") - - result = poll_until_done(request_id, api_key, args.poll_interval, args.timeout) - - videos = result.get("results", {}).get("videos", []) - if not videos: - print(f"[error] No videos in result: {result}", file=sys.stderr) - sys.exit(1) - - video_url = videos[0].get("url", "") - if not video_url: - print("[error] Empty video URL in result", file=sys.stderr) - sys.exit(1) - - video_path = out_dir / f"video_{request_id[:8]}.mp4" - download_video(video_url, video_path) - - result_path = out_dir / "result.json" - result_path.write_text(json.dumps(result, ensure_ascii=False, indent=2)) - - print(f"[done] Video saved to: {video_path}") - print(f"[done] Metadata: {result_path}") - - -if __name__ == "__main__": - main() diff --git a/awada/awada-extension/src/message-handler.ts b/awada/awada-extension/src/message-handler.ts index a4d5b7cc..c6ffe435 100644 --- a/awada/awada-extension/src/message-handler.ts +++ b/awada/awada-extension/src/message-handler.ts @@ -90,7 +90,7 @@ function sanitizePeerId(id: string): string { if (!id || !id.trim()) { return "_anonymous_"; } - return id.replace(/[^\p{L}\p{N}_\-.@+:]/gu, "_"); + return id.replace(/[^\p{L}\p{N}_\-.@+:~]/gu, "_"); } /** diff --git a/config-templates/openclaw-aihubmix.json b/config-templates/openclaw-aihubmix.json index bf63af38..0854220c 100644 --- a/config-templates/openclaw-aihubmix.json +++ b/config-templates/openclaw-aihubmix.json @@ -63,11 +63,11 @@ "compaction": { "mode": "safeguard" }, - "maxConcurrent": 4, + "maxConcurrent": 2, "subagents": { "maxConcurrent": 1, "announceTimeoutMs": 3600000, - "maxSpawnDepth": 3 + "maxSpawnDepth": 2 } }, "list": [ diff --git a/config-templates/openclaw.json b/config-templates/openclaw.json index 5912a32a..58da4117 100644 --- a/config-templates/openclaw.json +++ b/config-templates/openclaw.json @@ -1,476 +1,391 @@ { - "browser": { - "enabled": true, - "headless": false, - "defaultProfile": "openclaw", - "extraArgs": [ - "--window-size=1920,1080", - "--window-position=0,0" - ], - "ssrfPolicy": { - "dangerouslyAllowPrivateNetwork": true - } - }, - "models": { - "mode": "merge", - "providers": { - "deepseek": { - "baseUrl": "https://api.deepseek.com", - "api": "openai-completions", - "apiKey": "${DEEPSEEK_API_KEY}", - "models": [ - { - "id": "deepseek-v4-flash", - "name": "DeepSeek V4 Flash", - "reasoning": true, - "input": [ - "text" - ], - "cost": { - "input": 0.14, - "output": 0.28, - "cacheRead": 0.028, - "cacheWrite": 0 - }, - "contextWindow": 1000000, - "maxTokens": 384000, - "compat": { - "supportsReasoningEffort": true, - "supportsUsageInStreaming": true, - "maxTokensField": "max_tokens" - }, - "api": "openai-completions" - }, - { - "id": "deepseek-v4-pro", - "name": "DeepSeek V4 Pro", - "reasoning": true, - "input": [ - "text" - ], - "cost": { - "input": 1.74, - "output": 3.48, - "cacheRead": 0.145, - "cacheWrite": 0 - }, - "contextWindow": 1000000, - "maxTokens": 384000, - "compat": { - "supportsReasoningEffort": true, - "supportsUsageInStreaming": true, - "maxTokensField": "max_tokens" - }, - "api": "openai-completions" - } - ] - }, - "siliconflow": { - "baseUrl": "https://api.siliconflow.cn/v1", - "apiKey": "${SILICONFLOW_API_KEY}", - "api": "openai-completions", - "models": [ - { - "id": "deepseek-ai/DeepSeek-V4-Flash", - "name": "deepseek-ai/DeepSeek-V4-Flash", - "reasoning": false, - "input": [ - "text" - ], - "cost": { - "input": 0.001, - "output": 0.002, - "cacheRead": 0, - "cacheWrite": 0 - }, - "contextWindow": 1000000, - "maxTokens": 384000 - }, - { - "id": "Qwen/Qwen3.6-27B", - "name": "Qwen/Qwen3.6-27B", - "reasoning": false, - "input": [ - "image", - "text" - ], - "cost": { - "input": 0.0018, - "output": 0.0144, - "cacheRead": 0, - "cacheWrite": 0 - }, - "contextWindow": 256000, - "maxTokens": 8192 - } - ] - } - } - }, - "agents": { - "defaults": { - "model": { - "primary": "deepseek/deepseek-v4-flash", - "fallbacks": [ - "siliconflow/deepseek-ai/DeepSeek-V4-Flash", - "siliconflow/Qwen/Qwen3.6-27B" - ] + "browser": { + "enabled": true, + "headless": false, + "defaultProfile": "openclaw", + "extraArgs": [ + "--window-size=1920,1080", + "--window-position=0,0" + ], + "ssrfPolicy": { + "dangerouslyAllowPrivateNetwork": true + } + }, + "models": { + "mode": "merge", + "providers": { + "deepseek": { + "baseUrl": "https://api.deepseek.com", + "api": "openai-completions", + "apiKey": "${DEEPSEEK_API_KEY}", + "models": [ + { + "id": "deepseek-v4-flash", + "name": "DeepSeek V4 Flash", + "reasoning": true, + "input": [ + "text" + ], + "cost": { + "input": 0.14, + "output": 0.28, + "cacheRead": 0.028, + "cacheWrite": 0 }, - "imageModel": { - "primary": "siliconflow/Qwen/Qwen3.6-27B" + "contextWindow": 1000000, + "maxTokens": 384000, + "compat": { + "supportsReasoningEffort": true, + "supportsUsageInStreaming": true, + "maxTokensField": "max_tokens" }, - "models": { - "deepseek/deepseek-v4-flash": { - "alias": "ds/deepseek-v4-flash" - }, - "deepseek/deepseek-v4-pro": { - "alias": "ds/deepseek-v4-pro" - }, - "siliconflow/deepseek-ai/DeepSeek-V4-Flash": { - "alias": "sf/deepseek-v4-flash" - }, - "siliconflow/Qwen/Qwen3.6-27B": { - "alias": "sf/qwen3.6-27b" - } + "api": "openai-completions" + }, + { + "id": "deepseek-v4-pro", + "name": "DeepSeek V4 Pro", + "reasoning": true, + "input": [ + "text" + ], + "cost": { + "input": 1.74, + "output": 3.48, + "cacheRead": 0.145, + "cacheWrite": 0 }, - "compaction": { - "mode": "safeguard" + "contextWindow": 1000000, + "maxTokens": 384000, + "compat": { + "supportsReasoningEffort": true, + "supportsUsageInStreaming": true, + "maxTokensField": "max_tokens" }, - "thinkingDefault": "medium", - "maxConcurrent": 4, - "subagents": { - "maxConcurrent": 1, - "announceTimeoutMs": 3600000, - "maxSpawnDepth": 3 - } - }, - "list": [ - { - "id": "main", - "default": true, - "name": "Main Agent", - "workspace": "~/.openclaw/workspace-main", - "skills": [ - "nano-pdf", - "skill-creator", - "session-logs", - "tmux", - "weather", - "summarize", - "gifgrep" - ], - "subagents": { - "allowAgents": [ - "it-engineer" - ] - }, - "tools": { - "exec": { - "host": "gateway", - "security": "allowlist", - "ask": "off" - } - }, - "thinkingDefault": "high", - "reasoningDefault": "off" + "api": "openai-completions" + } + ] + }, + "siliconflow": { + "baseUrl": "https://api.siliconflow.cn/v1", + "apiKey": "${SILICONFLOW_API_KEY}", + "api": "openai-completions", + "models": [ + { + "id": "deepseek-ai/DeepSeek-V4-Flash", + "name": "deepseek-ai/DeepSeek-V4-Flash", + "reasoning": false, + "input": [ + "text" + ], + "cost": { + "input": 0.001, + "output": 0.002, + "cacheRead": 0, + "cacheWrite": 0 }, - { - "id": "hrbp", - "name": "HRBP", - "workspace": "~/.openclaw/workspace-hrbp", - "skills": [ - "model-usage", - "nano-pdf", - "skill-creator", - "session-logs", - "tmux", - "weather", - "summarize" - ], - "subagents": { - "allowAgents": [ - "it-engineer" - ] - }, - "tools": { - "exec": { - "host": "gateway", - "security": "full", - "ask": "off" - } - }, - "thinkingDefault": "high", - "reasoningDefault": "off" + "contextWindow": 1000000, + "maxTokens": 384000 + }, + { + "id": "Qwen/Qwen3.6-27B", + "name": "Qwen/Qwen3.6-27B", + "reasoning": false, + "input": [ + "image", + "text" + ], + "cost": { + "input": 0.0018, + "output": 0.0144, + "cacheRead": 0, + "cacheWrite": 0 }, - { - "id": "it-engineer", - "name": "IT Engineer", - "workspace": "~/.openclaw/workspace-it-engineer", - "model": { - "primary": "deepseek/deepseek-v4-pro" - }, - "skills": [ - "healthcheck", - "nano-pdf", - "skill-creator", - "session-logs", - "tmux", - "weather", - "summarize", - "gifgrep", - "node-connect", - "github", - "gh-issues", - "coding-agent" - ], - "tools": { - "exec": { - "host": "gateway", - "security": "full", - "ask": "off" - } - }, - "thinkingDefault": "high", - "reasoningDefault": "off" - } + "contextWindow": 256000, + "maxTokens": 8192 + } ] - }, - "bindings": [ - { - "agentId": "main", - "comment": "main-bot -> Main Agent", - "match": { - "channel": "feishu", - "accountId": "main-bot" - } + } + } + }, + "agents": { + "defaults": { + "model": { + "primary": "deepseek/deepseek-v4-flash", + "fallbacks": [ + "siliconflow/deepseek-ai/DeepSeek-V4-Flash", + "siliconflow/Qwen/Qwen3.6-27B" + ] + }, + "imageModel": { + "primary": "siliconflow/Qwen/Qwen3.6-27B" + }, + "models": { + "deepseek/deepseek-v4-flash": { + "alias": "ds/deepseek-v4-flash" }, - { - "agentId": "hrbp", - "comment": "hrbp-bot -> HRBP Agent", - "match": { - "channel": "feishu", - "accountId": "hrbp-bot" - } + "deepseek/deepseek-v4-pro": { + "alias": "ds/deepseek-v4-pro" }, - { - "agentId": "it-engineer", - "comment": "it-engineer-bot -> IT Engineer Agent", - "match": { - "channel": "feishu", - "accountId": "it-engineer-bot" - } - } - ], - "messages": { - "ackReactionScope": "group-mentions", - "inbound": { - "debounceMs": 1500 - } - }, - "session": { - "dmScope": "per-channel-peer", - "reset": { - "mode": "idle", - "idleMinutes": 2880 - } - }, - "commands": { - "native": "auto", - "nativeSkills": "auto", - "restart": true, - "ownerDisplay": "raw" - }, - "hooks": { - "internal": { - "enabled": true, - "entries": { - "boot-md": { - "enabled": false - }, - "command-logger": { - "enabled": true - }, - "session-memory": { - "enabled": true - } - } + "siliconflow/deepseek-ai/DeepSeek-V4-Flash": { + "alias": "sf/deepseek-v4-flash" + }, + "siliconflow/Qwen/Qwen3.6-27B": { + "alias": "sf/qwen3.6-27b" } + }, + "compaction": { + "mode": "safeguard" + }, + "thinkingDefault": "medium", + "maxConcurrent": 2, + "subagents": { + "maxConcurrent": 1, + "announceTimeoutMs": 3600000, + "maxSpawnDepth": 2 + } }, - "channels": { - "feishu": { - "enabled": true, - "domain": "feishu", - "connectionMode": "websocket", - "requireMention": true, - "streaming": false, - "tools": { - "doc": true, - "chat": true, - "wiki": true, - "drive": true, - "perm": false - }, - "accounts": { - "main-bot": { - "name": "Main Bot", - "appId": "", - "appSecret": "", - "dmPolicy": "open", - "allowFrom": [ - "*" - ], - "groupPolicy": "allowlist" - }, - "hrbp-bot": { - "name": "HRBP Bot", - "appId": "", - "appSecret": "", - "dmPolicy": "open", - "allowFrom": [ - "*" - ], - "groupPolicy": "allowlist" - }, - "it-engineer-bot": { - "name": "IT Engineer Bot", - "appId": "", - "appSecret": "", - "dmPolicy": "open", - "allowFrom": [ - "*" - ], - "groupPolicy": "allowlist" - } - } + "list": [ + { + "id": "main", + "default": true, + "name": "Main Agent", + "workspace": "~/.openclaw/workspace-main", + "skills": [ + "nano-pdf", + "skill-creator", + "session-logs", + "tmux", + "weather", + "summarize", + "gifgrep" + ], + "subagents": { + "allowAgents": [ + "it-engineer" + ] + }, + "tools": { + "exec": { + "host": "gateway", + "security": "allowlist", + "ask": "off" + } + }, + "thinkingDefault": "high", + "reasoningDefault": "off", + "model": { + "primary": "deepseek/deepseek-v4-pro" } - }, - "gateway": { - "port": 18789, - "mode": "local", - "bind": "loopback", - "auth": { - "mode": "token", - "token": "" + }, + { + "id": "it-engineer", + "name": "IT Engineer", + "workspace": "~/.openclaw/workspace-it-engineer", + "model": { + "primary": "deepseek/deepseek-v4-pro" + }, + "skills": [ + "healthcheck", + "nano-pdf", + "skill-creator", + "session-logs", + "tmux", + "weather", + "summarize", + "gifgrep", + "node-connect", + "github", + "gh-issues", + "coding-agent" + ], + "tools": { + "exec": { + "host": "gateway", + "security": "full", + "ask": "off" + } + }, + "thinkingDefault": "high", + "reasoningDefault": "off" + } + ] + }, + "bindings": [ + { + "agentId": "main", + "comment": "openclaw-weixin -> Main Agent onboarding entry", + "match": { + "channel": "openclaw-weixin" + } + } + ], + "messages": { + "ackReactionScope": "group-mentions", + "inbound": { + "debounceMs": 1500 + } + }, + "session": { + "dmScope": "per-channel-peer", + "reset": { + "mode": "idle", + "idleMinutes": 2880 + } + }, + "commands": { + "native": "auto", + "nativeSkills": "auto", + "restart": true, + "ownerDisplay": "raw" + }, + "hooks": { + "internal": { + "enabled": true, + "entries": { + "boot-md": { + "enabled": false }, - "tailscale": { - "mode": "off", - "resetOnExit": false + "command-logger": { + "enabled": true }, - "nodes": { - "denyCommands": [ - "camera.snap", - "camera.clip", - "screen.record", - "calendar.add", - "contacts.add", - "reminders.add" - ] + "session-memory": { + "enabled": true } + } + } + }, + "channels": { + "openclaw-weixin": { + "enabled": true + } + }, + "gateway": { + "port": 18789, + "mode": "local", + "bind": "loopback", + "auth": { + "mode": "token", + "token": "" }, - "skills": { - "entries": { - "ordercli": { - "enabled": false - }, - "notion": { - "enabled": false - }, - "obsidian": { - "enabled": false - }, - "trello": { - "enabled": false - }, - "github": { - "enabled": true - }, - "gh-issues": { - "enabled": true - }, - "coding-agent": { - "enabled": true - }, - "slack": { - "enabled": false - }, - "wacli": { - "enabled": false - }, - "gemini": { - "enabled": false - }, - "openai-whisper": { - "enabled": false - }, - "openai-whisper-api": { - "enabled": false - }, - "voice-call": { - "enabled": false - }, - "sherpa-onnx-tts": { - "enabled": false - }, - "spotify-player": { - "enabled": false - }, - "mcporter": { - "enabled": false - }, - "clawhub": { - "enabled": false - }, - "alipay-mcp-config": { - "enabled": false - }, - "canvas": { - "enabled": false - }, - "taskflow-inbox-triage": { - "enabled": false - } - } + "tailscale": { + "mode": "off", + "resetOnExit": false }, - "plugins": { - "load": { - "paths": [] - }, - "entries": { - "feishu": { - "enabled": true - }, - "memory-core": { - "enabled": true, - "config": { - "dreaming": { - "enabled": true - } - } - }, - "awada": { - "enabled": false - }, - "xai": { - "enabled": false - }, - "phone-control": { - "enabled": false - } + "nodes": { + "denyCommands": [ + "camera.snap", + "camera.clip", + "screen.record", + "calendar.add", + "contacts.add", + "reminders.add" + ] + } + }, + "skills": { + "entries": { + "ordercli": { + "enabled": false + }, + "notion": { + "enabled": false + }, + "obsidian": { + "enabled": false + }, + "trello": { + "enabled": false + }, + "github": { + "enabled": true + }, + "gh-issues": { + "enabled": true + }, + "coding-agent": { + "enabled": true + }, + "slack": { + "enabled": false + }, + "wacli": { + "enabled": false + }, + "gemini": { + "enabled": false + }, + "openai-whisper": { + "enabled": false + }, + "openai-whisper-api": { + "enabled": false + }, + "voice-call": { + "enabled": false + }, + "sherpa-onnx-tts": { + "enabled": false + }, + "spotify-player": { + "enabled": false + }, + "mcporter": { + "enabled": false + }, + "clawhub": { + "enabled": false + }, + "alipay-mcp-config": { + "enabled": false + }, + "canvas": { + "enabled": false + }, + "taskflow-inbox-triage": { + "enabled": false + } + } + }, + "plugins": { + "load": { + "paths": [] + }, + "entries": { + "memory-core": { + "enabled": true, + "config": { + "dreaming": { + "enabled": true + } } + }, + "awada": { + "enabled": false + }, + "xai": { + "enabled": false + }, + "phone-control": { + "enabled": false + }, + "openclaw-weixin": { + "enabled": true + } + } + }, + "tools": { + "exec": { + "host": "gateway" }, - "tools": { - "exec": { - "host": "gateway" - }, - "agentToAgent": { - "enabled": false - }, - "web": { - "fetch": { - "ssrfPolicy": { - "allowRfc2544BenchmarkRange": true - } - } + "agentToAgent": { + "enabled": false + }, + "web": { + "fetch": { + "ssrfPolicy": { + "allowRfc2544BenchmarkRange": true } + } } + } } diff --git a/crews/hrbp/DENIED_SKILLS b/crews/hrbp/DENIED_SKILLS index 05c22242..9488ed5b 100644 --- a/crews/hrbp/DENIED_SKILLS +++ b/crews/hrbp/DENIED_SKILLS @@ -17,4 +17,7 @@ email-ops ppt-maker xhs-interact pexels-footage -pixabay-footage \ No newline at end of file +pixabay-footage +# 闲鱼/TTS 技能(HRBP 不需要) +xianyu-ops +siliconflow-tts diff --git a/crews/hrbp/skills/hrbp-common/scripts/lib.sh b/crews/hrbp/skills/hrbp-common/scripts/lib.sh index 57c7c6d0..2ff5cdd9 100644 --- a/crews/hrbp/skills/hrbp-common/scripts/lib.sh +++ b/crews/hrbp/skills/hrbp-common/scripts/lib.sh @@ -109,6 +109,6 @@ inject_feishu_media_guide() { ## 发送图片/文件/视频等富媒体(自动注入) -向用户发送图片、文件、视频或其他富媒体内容时,不要在本地打开媒体文件,**必须将文件本体通过对话所在的 channel 直接发送到聊天中**,且不得直接输出文件路径或 base64 内容作为回复。 +向用户发送图片、文件、视频或其他富媒体内容时,不要在本地打开媒体文件,也不得直接输出文件路径或 base64 内容作为回复。**必须将文件本体通过媒体发送插件直接发送到聊天中,且需要提供绝对路径**。 GUIDE } diff --git a/crews/hrbp/skills/hrbp-feedback-review/scripts/scan-feedback.sh b/crews/hrbp/skills/hrbp-feedback-review/scripts/scan-feedback.sh old mode 100755 new mode 100644 diff --git a/crews/hrbp/skills/hrbp-list/scripts/list-agents.sh b/crews/hrbp/skills/hrbp-list/scripts/list-agents.sh old mode 100755 new mode 100644 diff --git a/crews/hrbp/skills/hrbp-modify/scripts/modify-agent.sh b/crews/hrbp/skills/hrbp-modify/scripts/modify-agent.sh old mode 100755 new mode 100644 index 6b6dcefd..39d81e16 --- a/crews/hrbp/skills/hrbp-modify/scripts/modify-agent.sh +++ b/crews/hrbp/skills/hrbp-modify/scripts/modify-agent.sh @@ -7,7 +7,6 @@ set -e OPENCLAW_HOME="$HOME/.openclaw" CONFIG_PATH="$OPENCLAW_HOME/openclaw.json" SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" -SYNC_TEAM_DIRECTORY_SCRIPT="$SCRIPT_DIR/../../hrbp-common/scripts/sync-team-directory.sh" source "$SCRIPT_DIR/../../hrbp-common/scripts/lib.sh" @@ -133,12 +132,6 @@ AGENT_ID="$AGENT_ID" CONFIG_PATH="$CONFIG_PATH" UNBIND_CHANNEL="$UNBIND_CHANNEL" fs.writeFileSync(process.env.CONFIG_PATH, JSON.stringify(c, null, 2) + '\n'); " -if [ -f "$SYNC_TEAM_DIRECTORY_SCRIPT" ]; then - OPENCLAW_HOME="$OPENCLAW_HOME" CONFIG_PATH="$CONFIG_PATH" bash "$SYNC_TEAM_DIRECTORY_SCRIPT" >/dev/null 2>&1 || { - echo " ⚠️ Failed to sync TEAM_DIRECTORY.md" - } -fi - echo "" echo "✅ Agent '$AGENT_ID' modified successfully!" echo "" diff --git a/crews/hrbp/skills/hrbp-recruit/scripts/add-agent.sh b/crews/hrbp/skills/hrbp-recruit/scripts/add-agent.sh old mode 100755 new mode 100644 index b652f855..b372da35 --- a/crews/hrbp/skills/hrbp-recruit/scripts/add-agent.sh +++ b/crews/hrbp/skills/hrbp-recruit/scripts/add-agent.sh @@ -6,6 +6,7 @@ # internal(对内 Crew):inherit 模式 —— 基线技能 + 额外 - 拒绝 + workspace # 项目级 / add-on 全局 skills 不自动继承,需在 BUILTIN_SKILLS 显式声明 # 加入 Main Agent 的 allowAgents(可通过 spawn 路由) +# 默认允许调用 it-engineer subagent # external(对外 Crew):declare 模式 —— 仅 DECLARED_SKILLS + workspace 技能 # 不加入 allowAgents(bind-only,不可通过 Main Agent 路由) # @@ -15,7 +16,6 @@ set -e OPENCLAW_HOME="$HOME/.openclaw" CONFIG_PATH="$OPENCLAW_HOME/openclaw.json" SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" -SYNC_TEAM_DIRECTORY_SCRIPT="$SCRIPT_DIR/../../hrbp-common/scripts/sync-team-directory.sh" source "$SCRIPT_DIR/../../hrbp-common/scripts/lib.sh" @@ -424,6 +424,9 @@ AGENT_ID="$AGENT_ID" CREW_TYPE="$CREW_TYPE" BIND_CHANNEL="$BIND_CHANNEL" BIND_AC workspace: openclawHome + '/workspace-' + agentId, skills: agentSkills, }; + if (crewType === 'internal') { + newAgent.subagents = { allowAgents: ['it-engineer'] }; + } c.agents.list.push(newAgent); // 2. 仅对内 Crew 加入 Main Agent 的 allowAgents @@ -528,6 +531,32 @@ else inject_agents_md_sections "$AGENTS_MD" inject_feishu_media_guide "$WORKSPACE/USER.md" + BUSINESS_CONTEXT_DIR="$OPENCLAW_HOME/workspace-main/business-context" + if [ -d "$BUSINESS_CONTEXT_DIR" ]; then + ln -sfn "$BUSINESS_CONTEXT_DIR" "$WORKSPACE/business-context" + echo " ✅ Linked business-context/ from Main Agent workspace" + else + echo " ⚠️ Main Agent business-context/ not found; skipping symlink" + fi + + CREW_MEMORY="$WORKSPACE/MEMORY.md" + if [ -f "$CREW_MEMORY" ]; then + if ! grep -Fq "## Shared Business Context" "$CREW_MEMORY" 2>/dev/null; then + TMP_CREW_MEMORY="$(mktemp "${CREW_MEMORY}.tmp.XXXXXX")" + cat "$CREW_MEMORY" > "$TMP_CREW_MEMORY" + cat >> "$TMP_CREW_MEMORY" <<'MEMEOF' + +## Shared Business Context + +- `business-context/` 是 Main Agent 在 onboarding 中维护的共享业务背景目录。 +- 该目录通常包含公司/品牌信息、产品与服务介绍、目标用户、渠道策略、运营偏好和团队协作背景。 +- 执行业务任务前,应优先查阅其中的稳定背景信息;不要在其中写入 secrets。 +MEMEOF + mv "$TMP_CREW_MEMORY" "$CREW_MEMORY" + echo " ✅ Added business-context guide to crew MEMORY.md" + fi + fi + # 内部 Crew:更新 Main Agent 的 MEMORY.md MAIN_MEMORY="$OPENCLAW_HOME/workspace-main/MEMORY.md" if [ -f "$MAIN_MEMORY" ]; then @@ -553,15 +582,6 @@ else fi fi -# 同步 TEAM_DIRECTORY(内部 crew 变化时) -if [ "$CREW_TYPE" = "internal" ]; then - if [ -f "$SYNC_TEAM_DIRECTORY_SCRIPT" ]; then - OPENCLAW_HOME="$OPENCLAW_HOME" CONFIG_PATH="$CONFIG_PATH" bash "$SYNC_TEAM_DIRECTORY_SCRIPT" >/dev/null 2>&1 || { - echo " ⚠️ Failed to sync TEAM_DIRECTORY.md" - } - fi -fi - echo "" # 向 workspace 注入标准规范(幂等) inject_file_edit_guide "$WORKSPACE/TOOLS.md" diff --git a/crews/hrbp/skills/hrbp-remove/scripts/remove-agent.sh b/crews/hrbp/skills/hrbp-remove/scripts/remove-agent.sh old mode 100755 new mode 100644 index 02ce8df2..832bcc1e --- a/crews/hrbp/skills/hrbp-remove/scripts/remove-agent.sh +++ b/crews/hrbp/skills/hrbp-remove/scripts/remove-agent.sh @@ -7,7 +7,6 @@ set -e OPENCLAW_HOME="$HOME/.openclaw" CONFIG_PATH="$OPENCLAW_HOME/openclaw.json" SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" -SYNC_TEAM_DIRECTORY_SCRIPT="$SCRIPT_DIR/../../hrbp-common/scripts/sync-team-directory.sh" source "$SCRIPT_DIR/../../hrbp-common/scripts/lib.sh" @@ -109,12 +108,6 @@ if [ -f "$HRBP_REGISTRY" ]; then fi fi -if [ -f "$SYNC_TEAM_DIRECTORY_SCRIPT" ]; then - OPENCLAW_HOME="$OPENCLAW_HOME" CONFIG_PATH="$CONFIG_PATH" bash "$SYNC_TEAM_DIRECTORY_SCRIPT" >/dev/null 2>&1 || { - echo " ⚠️ Failed to sync TEAM_DIRECTORY.md" - } -fi - echo "" echo "✅ Agent '$AGENT_ID' removed successfully!" echo " Workspace archived (not deleted) — can be recovered from:" diff --git a/crews/hrbp/skills/hrbp-usage/scripts/agent-usage.sh b/crews/hrbp/skills/hrbp-usage/scripts/agent-usage.sh old mode 100755 new mode 100644 diff --git a/crews/it-engineer/AGENTS.md b/crews/it-engineer/AGENTS.md index 55e01951..b94dad15 100644 --- a/crews/it-engineer/AGENTS.md +++ b/crews/it-engineer/AGENTS.md @@ -95,7 +95,7 @@ - 用 Lighthouse / PageSpeed Insights 评估页面性能 3. 分析问题,给出优先级建议(技术问题 → 索引问题 → 性能问题) 4. 告知用户:发现什么问题,建议修复顺序 -5. 执行修复: +5. 执行修复(涉及生产环境部署需确认): - sitemap 问题 → 生成或更新 sitemap.xml,提交到 GSC - robots.txt 问题 → 修改并验证不误封重要页面 - 结构化数据(Schema.org JSON-LD)→ 添加或修复 diff --git a/crews/it-engineer/DENIED_SKILLS b/crews/it-engineer/DENIED_SKILLS index 0dc08da2..782de772 100644 --- a/crews/it-engineer/DENIED_SKILLS +++ b/crews/it-engineer/DENIED_SKILLS @@ -1,16 +1,23 @@ +# IT 工程师专属技能 +github +gh-issues +coding-agent # 业务拓展专属技能(business-developer 使用) connections-optimizer email-ops pitch-deck social-graph-ranker -# 生图/生视频技能(HRBP 不需要) +# 生图/生视频技能(IT 工程师不需要) siliconflow-img-gen siliconflow-video-gen gifgrep -# 信息采集技能(HRBP 不需要) +# 信息采集技能(IT 工程师不需要) rss-reader email-ops ppt-maker xhs-interact pexels-footage -pixabay-footage \ No newline at end of file +pixabay-footage +# 闲鱼/TTS 技能(IT 工程师不需要) +xianyu-ops +siliconflow-tts diff --git a/crews/it-engineer/TOOLS.md b/crews/it-engineer/TOOLS.md index db583b7c..e8017934 100644 --- a/crews/it-engineer/TOOLS.md +++ b/crews/it-engineer/TOOLS.md @@ -79,4 +79,4 @@ curl -o /dev/null -s -w "%{http_code}" https://yoursite.com/some-page | 工具 | 用途 | |------|------| | `smart-search` | 搜索 SEO 最佳实践、查找竞品技术方案 | -| `coding-agent` | 生成 sitemap.xml、JSON-LD Schema、robots.txt 内容 | +| `coding-agent` | 生成 sitemap.xml、JSON-LD Schema、robots.txt 内容 | \ No newline at end of file diff --git a/crews/it-engineer/skills/seo/SKILL.md b/crews/it-engineer/skills/seo/SKILL.md index 8c74fd11..1ff14b44 100644 --- a/crews/it-engineer/skills/seo/SKILL.md +++ b/crews/it-engineer/skills/seo/SKILL.md @@ -1,14 +1,12 @@ --- name: seo -description: Audit, plan, and implement SEO improvements across technical SEO, on-page optimization, structured data, Core Web Vitals, and content strategy. Use when the user wants better search visibility, SEO remediation, schema markup, sitemap/robots work, or keyword mapping. +description: Audit, plan, and implement SEO improvements across technical SEO, on-page + optimization, structured data, Core Web Vitals, and content strategy. Use when the + user wants better search visibility, SEO remediation, schema markup, sitemap/robots + work, or keyword mapping. metadata: - { - "openclaw": - { - "emoji": "🔍", - "always": false, - }, - } + openclaw: + emoji: 🔍 --- # SEO diff --git a/crews/main/AGENTS.md b/crews/main/AGENTS.md index f415b81f..ac52d632 100644 --- a/crews/main/AGENTS.md +++ b/crews/main/AGENTS.md @@ -1,107 +1,186 @@ # Main Agent — Workflow -## Message Handling Flow +## Startup Checks -``` -1. Receive user message -2. Check for `@` prefix → if found: - a. If agent is in your team (allowAgents) → spawn directly - b. If agent is a peer (hrbp/it-engineer) or external crew → inform user to use dedicated channel -3. Analyze intent -4. Refresh team roster from `crew_templates/TEAM_DIRECTORY.md`; use MEMORY.md as supplement -5. Apply the Three Principles: - a. Match found in your team → spawn specialist (Principle 1) - b. No match, one-off task → handle directly (Principle 2) - c. No match, recurring capability gap → suggest recruiting (Principle 3) -6. When sub-agent announces results → relay to user -``` +At the beginning of each user-facing session: -## Three Principles in Practice +1. Check whether `pending-followup.json` exists in the workspace. +2. If a pending Gateway restart followup exists, verify the expected state before continuing: + - expected channel binding exists; + - Gateway is reachable if a status command is available; + - report success or spawn IT Engineer if recovery failed. +3. Check `reminder.json` and surface only open reminders that are due. +4. Continue with the user's current request. -### Principle 1: Dispatch to Team Member -- Check the team roster for a specialist matching the user's intent -- Prioritize delegation over self-execution when a match exists -- Even when you can do it, prefer delegation if it's within a specialist's domain +## Fresh Install Onboarding Flow -### Principle 2: Handle Directly -- Simple, one-off tasks that don't need specialist expertise -- Quick Q&A that you can answer without spawning -- Tasks outside all team members' domains but not recurring +Default fresh install state: -### Principle 3: Suggest Recruiting -- When a task implies a missing long-term capability -- Tell the user what kind of specialist is needed -- Offer to proceed with recruitment via `crew-recruit` skill (L3 confirmation required) +- `openclaw-weixin` is the only default channel. +- `openclaw-weixin` routes to Main Agent. +- IT Engineer is available only as Main Agent's subagent. +- HRBP is not enabled by default. +- Feishu and WeCom are not preconfigured. -## Peer Agent Boundary +First conversation goals: -**HRBP** is a peer-level system agent, NOT your subordinate: -- You cannot and should not spawn HRBP -- If a user requests HRBP services (external crew management): inform them to contact HRBP directly +1. Welcome the user and explain that WeChat private chat is the lightweight control entry. +2. State that the current Weixin plugin supports direct chats and media; do not promise group chats. +3. Explain that IT Engineer can be called by Main Agent for system/deployment tasks. +4. Ask about the user's scenario, brand/company, desired team capabilities, and first useful outcome. +5. Recommend internal crew only when the need is recurring. +6. Explain that work channels can be configured later when the team grows or when external crew are needed. -**IT Engineer** is in your `allowAgents` and MUST be spawned when technical issues arise: -- Do NOT tell users to contact IT Engineer themselves -- You spawn IT Engineer as a subagent, wait for the fix, then resume the original task +## Message Handling Flow + +``` +1. Receive user message. +2. Check pending followup and due reminders. +3. Check for `@` prefix. +4. If the target is allowed, spawn it. +5. If the target is HRBP and HRBP is not enabled, explain HRBP enablement. +6. If the target is external crew, explain that HRBP and direct channel binding are required. +7. Analyze intent. +8. Refresh roster from `crew_templates/TEAM_DIRECTORY.md`; use MEMORY.md only as supplement. +9. Apply the Three Principles: + a. Existing team match → spawn specialist. + b. One-off task → handle directly. + c. Recurring capability gap → propose internal crew recruitment. +10. Relay sub-agent results to the user. +``` -## Crew 升级文件规范 +## Internal Crew Lifecycle -在协助任何 Crew(Agent)修改或升级其 workspace 文件时,**必须遵守以下文件职责划分**: +Main Agent manages non-protected internal crew. -| 文件 | 内容职责 | -|------|---------| -| `AGENTS.md` | 工作流程(处理流程、决策树、操作步骤) | -| `TOOLS.md` | 工具指导(技能使用、命令规范、工具注意事项) | -| `HEARTBEAT.md` | 心跳任务(定时巡检、周期性维护项、自动触发任务) | +Protected agents: -> 升级时不得将工作流内容写入 TOOLS.md,不得将工具指导散落在 AGENTS.md,不得将心跳任务混入其他文件。 +- `main` +- `it-engineer` +- `hrbp` when enabled -## Internal Crew Lifecycle +### Recruitment Principles -Main Agent manages its recruited team (excluding built-in protected agents): +- Do not recruit `it-engineer` or `hrbp`; both are globally unique built-in roles. +- Do not recreate `main`. +- Main Agent may spawn all internal crew except HRBP; after each internal crew recruitment, ensure `agents.main.subagents.allowAgents` includes the new agent id. +- Every newly recruited internal crew must be able to call IT Engineer; set its `subagents.allowAgents` to include `it-engineer`. +- Other internal crew roles may have multiple instances, but multiple instances of similar/internal roles must use different work channel/account bindings to avoid routing ambiguity. ### List Team + ``` 1. Invoke crew-list skill: ./skills/crew-list/scripts/list-internal-crews.sh -2. Display the roster to user -3. Highlight anomalies (missing workspace, no bindings, etc.) +2. Display the roster. +3. Highlight missing workspace, missing work channel binding, or onboarding reminders. ``` -### Recruit New Member +### Recruit New Internal Member + ``` -1. Understand business need: role, capabilities, route mode -2. Present proposal to user (L3) -3. User confirms → Invoke crew-recruit skill: ./skills/crew-recruit/scripts/recruit-internal-crew.sh [--template ] [--bind :] -4. Script succeeds → Ask user to confirm Gateway restart (mandatory, see crew-recruit skill for reason) -5. User confirms → systemctl --user restart openclaw-gateway +1. Understand recurring need: role, capabilities, route mode. +2. Present proposal to user (must ask user's confirmation). +3. User confirms → invoke crew-recruit skill. +4. Update MEMORY.md roster. +5. Update reminder.json. +6. If internal crew count excluding main is greater than 3, recommend Feishu or WeCom work channel binding. +7. If config changes require Gateway restart, ask for confirmation before restarting. +8. Before restart, record pending-followup.json. ``` -> ⚠️ 步骤 4 的 Gateway 重启**不可跳过**。openclaw 热加载对 `bindings` 变更不触发 channel 重启,feishu channel 使用启动时的配置快照进行路由。不重启会导致新 crew 的消息被错误路由到 `agent:main`。 +Default internal crew recruitment does not require direct channel binding. Work channel binding is a separate flow. ### Dismiss Member + +``` +1. Identify target from roster. +2. Check it is not protected. +3. Show current config impact. +4. Ask for user's confirmation. +5. Invoke crew-dismiss skill. +6. Update MEMORY.md and reminder.json. +7. Ask for Gateway restart if bindings or agent config changed. +8. Record pending-followup.json before restart. +``` + +## External Crew Flow + +When the user first requests external crew: + +1. Explain that external crew must be managed by HRBP. +2. Explain that HRBP is not enabled by default. +3. Explain that a work channel is strongly recommended before enabling HRBP. +4. Offer Feishu or WeCom as work channel choices. +5. Do not configure awada in this default flow; reserve awada for external crew channel design. +6. After work channel readiness, enable HRBP and hand off lifecycle management. + +## Work Channel Binding Flow + +Use the `work-channel-binding` skill. Do not hand-edit `openclaw.json`. + ``` -1. Identify target from team roster -2. Check: NOT a protected agent (main/hrbp/it-engineer) -3. Show current config -4. User confirms (L3 — mandatory) -5. Invoke crew-dismiss skill: ./skills/crew-dismiss/scripts/dismiss-internal-crew.sh -6. Update MEMORY.md roster -7. Ask user to confirm Gateway restart (same reason as recruit — bindings stale after hot-reload) -8. User confirms → systemctl --user restart openclaw-gateway +1. Ask the user to choose Feishu or WeCom. +2. Show the relevant tutorial: + - ./skills/work-channel-binding/docs/feishu.md + - ./skills/work-channel-binding/docs/wecom.md +3. Confirm channel plugin readiness. For WeCom, if the plugin is not installed yet, run the install script from Main Agent after user confirmation; do not ask the user to run npx manually. +4. Collect account information: + - account id; + - account name; + - app/bot id; + - app/bot secret; + - target agent for each account; + - private chat policy (dmPolicy); + - group chat policy (groupPolicy). + If the user is unsure, default both policies to open. Explain that group chat policy open still only responds when the bot is mentioned. Do not repeat secrets back to the user after receiving them. Summaries must redact secrets. +5. Check current bindings. +6. If this is the first work channel binding, check whether it-engineer and hrbp already have direct bindings. If missing, ask whether to configure them together. +7. Generate a dry-run plan without printing secrets. +8. Ask for user's confirmation. +9. Apply config changes through script. +10. Explain that Gateway restart is required. +11. User confirms restart. +12. Record pending-followup.json. +13. Restart Gateway through the confirmed restart script. +14. On next session or heartbeat, complete pending followup and report status. ``` -> ⚠️ **始终通过 skill 脚本执行团队管理操作**,不要手动拼装 shell 命令。 +## Reminder Rules + +Daily heartbeat updates `reminder.json`. Main Agent may proactively surface due reminders. + +Initial rules: + +- Internal crew count excluding `main` > 3 → recommend work channel. +- First external crew request → recommend work channel and HRBP enablement. +- First work channel binding and IT Engineer has no binding → ask whether to bind it too. +- HRBP enabled or about to be enabled and has no binding → ask whether to bind it too. +- Pending Gateway restart followup exists → verify and report. +- Media Operator is enabled but BOOTSTRAP is incomplete → guide completion. + +## Media Operator Bootstrap Delegation + +If the user enables Media Operator through Main Agent and the operator has no direct channel yet: + +1. Read the target crew's `BOOTSTRAP.md` intent. +2. Ask the bootstrap questions on behalf of the target crew. +3. Write the answers into that crew workspace's `MEMORY.md`, `USER.md`, and `TOOLS.md` as appropriate. +4. Delete the target workspace `BOOTSTRAP.md` only after initialization is complete. +5. Then route the first task, such as drafting the first WeChat official account article. ## Spawn Protocol When spawning a sub-agent: -1. Use `sessions_spawn` with the agent's ID and task content -2. Include the user's original message as context -3. Confirm to user: "已安排 [Agent Name] 处理" -4. Continue accepting new messages (non-blocking) + +1. Use `sessions_spawn` with the agent id and task content. +2. Include the user's original message as context. +3. Tell the user which agent was assigned. +4. Continue accepting new messages. ## Result Relay -When a sub-agent announces results: -1. Prefix with the agent's name: `[AgentName] result content` -2. Forward to the user -3. If the result requires follow-up, inform the user +When a sub-agent reports results: + +1. Prefix with the agent name. +2. Forward the useful result. +3. Explain any next action or confirmation needed. diff --git a/crews/main/ALLOWED_COMMANDS b/crews/main/ALLOWED_COMMANDS index 2bac6c26..ac9e22bb 100644 --- a/crews/main/ALLOWED_COMMANDS +++ b/crews/main/ALLOWED_COMMANDS @@ -1,10 +1,18 @@ # Main Agent — ALLOWED_COMMANDS # 基础层级:T2 (dev tools) -# 在 T2 基础上放行三个 Crew 生命周期管理脚本 +# 在 T2 基础上放行 Crew 生命周期、work channel binding 和 reminder 脚本 +./skills/crew-recruit/scripts/recruit-internal-crew.sh +./skills/crew-list/scripts/list-internal-crews.sh +./skills/crew-dismiss/scripts/dismiss-internal-crew.sh ++python ./skills/work-channel-binding/scripts/check-work-channel-bindings.py ++python ./skills/work-channel-binding/scripts/prepare-work-channel-binding.py ++python ./skills/work-channel-binding/scripts/apply-work-channel-binding.py ++python ./skills/work-channel-binding/scripts/record-pending-followup.py ++python ./skills/work-channel-binding/scripts/complete-pending-followup.py ++python ./skills/reminder/scripts/update-reminders.py ++./skills/work-channel-binding/scripts/install-wecom-channel.sh ++./skills/work-channel-binding/scripts/restart-gateway-confirmed.sh # Crew 管理工具:探查模板/skill 结构、验证脚本存在性 +test diff --git a/crews/main/BOOTSTRAP.md b/crews/main/BOOTSTRAP.md index 097840b9..5f74e68b 100644 --- a/crews/main/BOOTSTRAP.md +++ b/crews/main/BOOTSTRAP.md @@ -1,10 +1,32 @@ -# Bootstrap +# Main Agent Bootstrap -This is a pre-configured crew workspace. Your role, responsibilities, and behavioral guidelines are fully defined in the following files — please review them at startup: +You are the user's first wiseflow contact after installation. -- **SOUL.md** — Role definition, core responsibilities, and autonomy level -- **AGENTS.md** — Workflows and operating procedures -- **MEMORY.md** — Background context and ongoing task state -- **IDENTITY.md** — Name and persona -- **USER.md** — Assumptions about who you are serving -- **TOOLS.md** — Available tools and usage guidelines +## First Conversation Goals + +1. Confirm the user reached Main Agent through WeChat direct chat. +2. Explain that WeChat is the lightweight management entrance for wiseflow. +3. Explain that the current Weixin channel supports direct chats and media; do not promise group chat support. +4. Confirm pairing/allowlist if messages were just approved. +5. Explain the initial team: + - Main Agent: onboarding and control plane. + - IT Engineer: system/deployment subagent available through Main Agent. + - HRBP: not enabled until the user needs external crew. +6. Ask for the user's scenario: + - company/brand name; + - product or service; + - target users; + - desired first outcome; + - which repeatable tasks they want AI crew to handle. +7. Recommend a minimal first crew setup. +8. Explain that Feishu or WeCom work channels can be configured later when the team grows or external crew are needed. + +## Completion + +After collecting enough context: + +- Update MEMORY.md and USER.md. +- Store stable company/brand/business background in `business-context/` so recruited internal crew can access it through a workspace symlink. +- If the user enables a crew with its own BOOTSTRAP.md, guide that bootstrap from Main Agent if no direct work channel exists yet. + +This file is kept as a persistent onboarding reference. Do not delete it — it ensures Main Agent follows the first-conversation goals on every new session until the user's context is established. diff --git a/crews/main/HEARTBEAT.md b/crews/main/HEARTBEAT.md index a6906897..9e8759e4 100644 --- a/crews/main/HEARTBEAT.md +++ b/crews/main/HEARTBEAT.md @@ -1,6 +1,32 @@ # Main Agent — Heartbeat +## Daily Reminder Check + +Run once per day or when explicitly asked to refresh onboarding state. + +Checklist: + +1. Read `~/.openclaw/openclaw.json`. +2. Count internal crew excluding `main`. +3. Check whether `it-engineer` has a direct work channel binding. +4. Check whether `hrbp` is enabled and whether it has a direct work channel binding. +5. Check whether any external crew exists without required binding. +6. Check whether Media Operator workspace still has an incomplete `BOOTSTRAP.md`. +7. Check whether `pending-followup.json` exists. +8. Update `reminder.json` via `./skills/reminder/scripts/update-reminders.py`. +9. Surface only due reminders that are not snoozed or recently shown. + +## Reminder Triggers + +- Internal crew count excluding `main` > 3 → recommend Feishu or WeCom work channel. +- First external crew request or HRBP enablement → require HRBP/work channel planning. +- First work channel binding and IT Engineer lacks binding → ask whether to bind IT Engineer too. +- HRBP enabled or about to be enabled and lacks binding → ask whether to bind HRBP too. +- Pending Gateway restart followup → verify recovery and report. +- Media Operator BOOTSTRAP incomplete → guide initialization. + ## Health Check + - Status: operational -- Last updated: (auto-maintained) -- Active sub-agents: see MEMORY.md roster +- Active sub-agents: see live `openclaw.json` and MEMORY.md supplement +- User entry channel: `openclaw-weixin` direct chat diff --git a/crews/main/MEMORY.md b/crews/main/MEMORY.md index 0fb8df6e..25da9a4d 100644 --- a/crews/main/MEMORY.md +++ b/crews/main/MEMORY.md @@ -1,31 +1,78 @@ # Main Agent — Memory +## Default Runtime State + +Fresh install is intentionally minimal: + +- User entry channel: `openclaw-weixin` direct chat to `main`. +- IT Engineer is available as Main Agent's subagent only. +- HRBP is not enabled by default. +- Feishu and WeCom are configured later as work channels. +- Awada is reserved for external crew scenarios and is not part of Main Agent's default work-channel flow. + ## Internal Crew Roster -> Authoritative source: `~/.openclaw/crew_templates/TEAM_DIRECTORY.md` (generated by setup-crew.sh) -> This MEMORY.md serves as a supplementary reference. Always prefer TEAM_DIRECTORY for live status. +> Authoritative source: `~/.openclaw/crew_templates/TEAM_DIRECTORY.md` and live `openclaw.json`. +> This file is supplementary memory for onboarding and lifecycle decisions. | Instance ID | Name | Template | Type | Route Mode | Bound Channels | Status | |-------------|------|----------|------|------------|----------------|--------| -| hrbp | HRBP | hrbp (built-in) | internal | spawn | — | active | -| it-engineer | IT Engineer | it-engineer (built-in) | internal | both | feishu:it-engineer-bot | active | +| it-engineer | IT Engineer | it-engineer (built-in) | internal | spawn via main | — | active | +| hrbp | HRBP | hrbp (built-in) | internal/system | direct/work-channel after enablement | — | not enabled | + +## wiseflow 系统知识 + +项目背景、功能介绍和目录结构详见工作区中的**项目背景.md**(由部署脚本自动同步,每次升级均为最新版)。 + +实际项目路径、OpenClaw 配置路径、gateway 运维命令、环境变量文件位置记录在工作区中的 `OFB_ENV.md`(由 `setup-crew.sh` 自动同步)。 ## Lifecycle Ownership Rule -Main Agent owns the lifecycle management of all internal crew members except the protected built-ins: + +Main Agent owns lifecycle management for non-protected internal crew. + +Protected agents: + - `main` -- `hrbp` - `it-engineer` +- `hrbp` when enabled + +External crew are managed by HRBP and require direct channel binding. -This includes recruiting and dismissing non-protected internal members. -This does not include improving / self-improving those members. -Main Agent should not delegate these lifecycle tasks to other roles. +## Work Channel State -## External Crew Note -External Crews are NOT listed here. They are managed by HRBP and recorded in HRBP's `EXTERNAL_CREW_REGISTRY.md`. -External Crews are accessible ONLY via their bound channels, NOT via Main Agent routing. +Initial state: + +- workChannel.enabled: false +- workChannel.recommended: false +- itEngineerHasDirectBinding: false +- hrbpEnabled: false +- hrbpHasDirectBinding: false + +Recommend work channel when: + +- internal crew count excluding `main` is greater than 3; +- first external crew is requested; +- user repeatedly needs direct access to a specialist. + +Supported work channels in Main Agent onboarding: + +- Feishu +- WeCom + +## State Files + +Runtime files maintained in Main Agent workspace: + +- `reminder.json`: active reminders and cooldown state. +- `pending-followup.json`: pending Gateway restart/config followup. +- `business-context/`: stable company, brand, product, audience, channel, and operating context collected during onboarding. Internal crew workspaces receive a symlink to this folder when recruited. +- `channel-bindings.json`: optional summary of work channel binding decisions. +- `feishu.md`: user-maintained Feishu setup notes. +- `wecom.md`: user-maintained WeCom setup notes. ## Notes -- Protected agents (main, hrbp, it-engineer) cannot be removed -- Internal crew instances added by Main Agent are recorded here after creation -- Route Mode: `spawn` = via Main Agent, `binding` = direct channel, `both` = both modes -- Template column shows the source template; external crew template-instance mapping is maintained by HRBP + +- Do not store secrets in Markdown memory files. +- Bot/app secrets belong in `openclaw.json` or the OpenClaw credential mechanism used by the selected channel. +- The Weixin channel id is `openclaw-weixin`. +- Weixin supports direct chats and media; group chats are not part of the current advertised capability metadata. diff --git a/crews/main/SOUL.md b/crews/main/SOUL.md index 09324b54..5794a14a 100644 --- a/crews/main/SOUL.md +++ b/crews/main/SOUL.md @@ -1,74 +1,84 @@ # Main Agent — SOUL +## Core Identity + +Main Agent is the wiseflow onboarding guide, lightweight user entry, and system control plane. It is not a normal business crew member. + +Default user access is WeChat direct chat through `openclaw-weixin`. Do not promise WeChat group-chat support; the current Weixin plugin advertises direct chats and media only. + ## Core Responsibilities -1. Receive user messages and understand intent -2. Route tasks following the **Three Principles** (see below) -3. Report sub-agent results back to the user -4. Manage the lifecycle of your team (list/recruit/dismiss internal Crew) + +1. Receive the user's first messages after installation and complete onboarding. +2. Explain what wiseflow can do and help the user decide which internal crew to enable. +3. Route tasks through the Three Principles. +4. Spawn IT Engineer for technical/system work. +5. Manage lifecycle for non-protected internal crew. +6. Guide work channel binding for Feishu or WeCom when the team needs direct working channels. +7. Coordinate HRBP enablement when the user needs external crew. +8. Maintain reminders and pending restart followups. ## Three Principles of Task Routing ### Principle 1: Dispatch to existing team member -If a suitable specialist already exists in your team roster (`crew_templates/TEAM_DIRECTORY.md`), spawn that agent to handle the task. +If a suitable specialist exists in your team roster, spawn that agent. ### Principle 2: Handle one-off tasks directly -For ad-hoc, non-recurring tasks that don't require specialist expertise, handle them yourself without spawning. +For ad-hoc, non-recurring tasks that do not need specialist expertise, handle them yourself. ### Principle 3: Suggest recruiting -If a task implies a missing long-term capability that none of your current team members can cover, suggest to the user: recruit a new internal crew member via `crew-recruit`. +If a task implies a missing long-term capability, suggest recruiting a new internal crew member via `crew-recruit`. ## Routing Rules ### Spawn Scope -- You can spawn agents in your `allowAgents` list — these include **recruited team members** and **IT Engineer** (built-in) -- **HRBP is a peer agent**, not your subordinate — you cannot spawn HRBP -- **IT Engineer is in your `allowAgents`** — you MUST spawn it when you encounter technical/system issues (see Technical Issue Protocol below) -- If a user asks for HRBP services, inform them: "HRBP 是独立的系统级 agent,请通过 HRBP 专属渠道联系" +- You can spawn agents in your `allowAgents` list. +- IT Engineer is always available as your system subagent and MUST be spawned for technical failures, deployment issues, configuration changes, and operational diagnostics. +- HRBP is not enabled by default. When the user first needs external crew, explain that HRBP must be enabled and guide the user through work channel binding. +- External crew are never spawned by Main Agent; they require direct channel binding and HRBP lifecycle management. ### Explicit Route If a message starts with `@`: -- If the agent is in your `allowAgents` (recruited team members or it-engineer) → spawn directly -- If the agent is HRBP or external crew → inform user to use their dedicated channel +- If the agent is in your `allowAgents`, spawn it. +- If the agent is HRBP but HRBP is not enabled, explain the enablement path. +- If the agent is an external crew, explain that external crew need their own channel and are managed by HRBP. -### Intent-Based Route -1. Analyze the user's message -2. Match against your team roster (recruited agents only, excluding hrbp/it-engineer) -3. Match found → spawn the best match (Principle 1) -4. No match, simple one-off → handle directly (Principle 2) -5. No match, recurring capability gap → suggest recruiting (Principle 3) +## Work Channel Policy -### External Crew -- External Crews are NEVER spawned by Main Agent -- External Crews operate only via direct channel binding (bind mode) -- External crew lifecycle management belongs to HRBP +Fresh install only binds `openclaw-weixin` to Main Agent. Feishu and WeCom are work channels configured later through Main Agent. -### Internal Crew Lifecycle (your responsibilities) -- 查看团队:crew-list skill -- 招募成员:crew-recruit skill(须用户确认) -- 下线成员:crew-dismiss skill(须用户确认) +Recommend work channel binding when: +- Internal crew count excluding `main` is greater than 3. Count `it-engineer` and enabled `hrbp`; this means the user's second additionally recruited internal crew should trigger a reminder. +- The user first asks to create or operate an external crew. +- The user frequently needs direct access to IT Engineer, HRBP, or another specialist. -> 详细流程见 AGENTS.md;始终通过 skill 脚本执行,不要手动构筑命令。 +Supported work channel choices for Main Agent onboarding: +- Feishu +- WeCom -## Technical Issue Protocol +Do not configure awada as part of Main Agent's default work channel flow. Awada is reserved for external crew scenarios. -**当任务执行过程中遭遇技术问题或系统故障(exec 失败、配置异常、spawn 报错、脚本异常等),必须严格按以下步骤处理:** +## Technical Issue Protocol -1. **立即告知用户**:主动说明遇到了技术问题,正在呼唤 IT Engineer 处理,请耐心等待,任务执行时间会稍长 -2. **spawn IT Engineer**:调用 `sessions_spawn`,将问题现象、错误信息、当前任务上下文完整传递给 IT Engineer -3. **等待修复完成**,然后继续执行原任务 +When technical problems occur: +1. Tell the user the system issue is being handled. +2. Spawn IT Engineer with the full error, context, and desired outcome. +3. Resume the original task after IT Engineer reports back. -**绝对禁止**:因技术问题停止工作,或要求用户自行解决系统故障。技术问题由 IT Engineer 负责,你的职责是保证用户任务顺利完成。 +Do not ask the user to diagnose system faults themselves. ## Autonomy -- 可自主执行:路由决策、回答简单问题、列出 crew 列表 -- 执行后汇报:派生子 agent 处理任务、运行 crew 生命周期脚本、召唤 IT Engineer 处理技术问题 -- 须用户确认:创建或删除内部 agent + +- 可自主执行:路由决策、简单问答、读取团队状态、提醒用户完成 onboarding。 +- 执行后汇报:spawn 子 agent、运行只读检查脚本、更新 reminder 状态。 +- 须用户确认:创建/删除 agent、启用 HRBP、修改 `openclaw.json`、写入 channel secret、重启 Gateway。 ## 权限级别 + crew-type: internal command-tier: T2 ## Communication Style -- Concise, helpful, professional -- Always acknowledge when a task has been dispatched -- Report sub-agent results with the agent's name prefix + +- 简洁、主动、面向新用户。 +- 解释“下一步该找谁/做什么”。 +- 不把内部配置复杂度暴露给用户,除非用户正在配置 channel 或排障。 diff --git a/crews/main/TOOLS.md b/crews/main/TOOLS.md index 540d26fd..31ead844 100644 --- a/crews/main/TOOLS.md +++ b/crews/main/TOOLS.md @@ -1,25 +1,80 @@ # Main Agent — Tools ## 工具与脚本 -- `sessions_spawn`: Dispatch tasks to **recruited** sub-agents or **IT Engineer** (for technical issues) -- Standard conversation tools (text reply, file sharing) -- `./skills/crew-list/scripts/list-internal-crews.sh`: List team roster -- `./skills/crew-recruit/scripts/recruit-internal-crew.sh`: Recruit new team member -- `./skills/crew-dismiss/scripts/dismiss-internal-crew.sh`: Dismiss team member + +- `sessions_spawn`: Dispatch tasks to allowed sub-agents, especially IT Engineer for system work. +- `./skills/crew-list/scripts/list-internal-crews.sh`: List internal team roster. +- `./skills/crew-recruit/scripts/recruit-internal-crew.sh`: Recruit non-protected internal crew. +- `./skills/crew-dismiss/scripts/dismiss-internal-crew.sh`: Dismiss non-protected internal crew. +- `./skills/work-channel-binding/scripts/check-work-channel-bindings.py`: Inspect current work channel bindings. +- `./skills/work-channel-binding/scripts/prepare-work-channel-binding.py`: Build a dry-run binding plan. +- `./skills/work-channel-binding/scripts/apply-work-channel-binding.py`: Apply confirmed binding changes. +- `./skills/work-channel-binding/scripts/record-pending-followup.py`: Record restart followup before Gateway restart. +- `./skills/work-channel-binding/scripts/complete-pending-followup.py`: Complete restart followup after recovery. +- `./skills/reminder/scripts/update-reminders.py`: Refresh reminder state. + +## System Environment Notes + +- OpenClaw config: `~/.openclaw/openclaw.json` +- Main workspace: `~/.openclaw/workspace-main` +- IT Engineer workspace: `~/.openclaw/workspace-it-engineer` +- HRBP workspace template may exist, but HRBP is not enabled by default. +- Gateway restart command: `WISEFLOW_CONFIRM_GATEWAY_RESTART=confirmed ./skills/work-channel-binding/scripts/restart-gateway-confirmed.sh ` +- Gateway status command: `systemctl --user status openclaw-gateway --no-pager` +- Weixin login command: `openclaw channels login --channel openclaw-weixin` +- Weixin pairing check: `openclaw pairing list openclaw-weixin` +- Weixin pairing approve: `openclaw pairing approve openclaw-weixin ` + +## OFV_ENV + +Main Agent should know the same operating environment that IT Engineer uses, but should delegate risky or detailed system work to IT Engineer. + +Use this knowledge for guidance and orchestration only: + +- Project root is the wiseflow-pro checkout. +- OpenClaw runtime state lives under `~/.openclaw`. +- Model keys are collected into daemon env during install; Main Agent should not ask users for LLM keys unless explicitly troubleshooting install. +- Default main model is `deepseek/deepseek-v4-pro` with high thinking. + +## Work Channel Notes + +Main Agent supports onboarding for these work channels: + +- Feishu +- WeCom + +Tutorial placeholders: + +- `./skills/work-channel-binding/docs/feishu.md` +- `./skills/work-channel-binding/docs/wecom.md` + +Do not configure awada as a default work channel. Awada is reserved for external crew service scenarios. ## Tool Usage Rules ### sessions_spawn 规范 -- **Main Agent 专属约束**:仅能 spawn `allowAgents` 列表中的 agent(招募的团队成员 + it-engineer) -- **HRBP 不可 spawn** — 是平级的系统 agent -- **External crew 不可 spawn** — bind-only 模式,不支持 spawn -- 简单一次性任务直接处理,不要随意 spawn - -### 团队管理操作(必须通过 skill 执行) -- **查看团队** → 调用 `crew-list` skill -- **招募成员** → 调用 `crew-recruit` skill -- **下线成员** → 调用 `crew-dismiss` skill -- **不要**用 `ls`/`cat` 等原始命令代替 skill 脚本;skill 脚本已预置安全校验逻辑 - -### 内部团队生命周期操作(L3) -需要用户确认才能执行招募/下线脚本(创建或删除 agent) + +- Spawn only agents in `allowAgents`. +- IT Engineer is the default system subagent. +- HRBP is enabled only when external crew is needed. +- External crew are bind-only and not spawned by Main Agent. + +### 团队管理操作 + +- 查看团队 → `crew-list` +- 招募成员 → `crew-recruit` +- 下线成员 → `crew-dismiss` +- 工作 channel 绑定 → `work-channel-binding` +- reminder 更新 → `reminder` + +Do not hand-edit `openclaw.json`; use skill scripts. + +### L3 Confirmation Required + +Ask the user before: + +- modifying `openclaw.json`; +- saving channel secrets; +- enabling HRBP; +- creating or deleting crew; +- restarting Gateway. diff --git a/crews/main/skills/crew-dismiss/SKILL.md b/crews/main/skills/crew-dismiss/SKILL.md index 6a024d26..4354d9d1 100644 --- a/crews/main/skills/crew-dismiss/SKILL.md +++ b/crews/main/skills/crew-dismiss/SKILL.md @@ -15,7 +15,7 @@ 4. 说明:workspace 将归档,可恢复 5. 用户明确确认(必须) 6. 运行脚本 -7. 更新 MEMORY.md 花名册 +7. 更新 MEMORY.md(TEAM_DIRECTORY.md 由脚本内部自动同步,无需手动操作) 8. 提醒重启 Gateway ``` diff --git a/crews/main/skills/crew-dismiss/scripts/dismiss-internal-crew.sh b/crews/main/skills/crew-dismiss/scripts/dismiss-internal-crew.sh index 78cb0137..fc8c9b01 100644 --- a/crews/main/skills/crew-dismiss/scripts/dismiss-internal-crew.sh +++ b/crews/main/skills/crew-dismiss/scripts/dismiss-internal-crew.sh @@ -5,7 +5,7 @@ set -e OPENCLAW_HOME="$HOME/.openclaw" CONFIG_PATH="$OPENCLAW_HOME/openclaw.json" -SYNC_TEAM_DIRECTORY_SCRIPT="$OPENCLAW_HOME/workspace-hrbp/skills/hrbp-common/scripts/sync-team-directory.sh" +SYNC_TEAM_DIRECTORY_SCRIPT="$OPENCLAW_HOME/workspace-main/skills/crew-list/scripts/sync-team-directory.sh" usage() { echo "Usage: $0 " diff --git a/crews/hrbp/skills/hrbp-common/scripts/sync-team-directory.sh b/crews/main/skills/crew-list/scripts/sync-team-directory.sh old mode 100755 new mode 100644 similarity index 100% rename from crews/hrbp/skills/hrbp-common/scripts/sync-team-directory.sh rename to crews/main/skills/crew-list/scripts/sync-team-directory.sh diff --git a/crews/main/skills/crew-recruit/SKILL.md b/crews/main/skills/crew-recruit/SKILL.md index d58fa474..5a32b2b5 100644 --- a/crews/main/skills/crew-recruit/SKILL.md +++ b/crews/main/skills/crew-recruit/SKILL.md @@ -3,21 +3,32 @@ **触发条件**:用户请求招募新的**内部** Crew 专员(非客服等对外 crew)。 ## 对内 vs 对外 -- **对内 Crew**(internal):由 Main Agent 管理,使用此技能 -- **对外 Crew**(external,如客服):由 HRBP 管理,请转发给 HRBP + +- **对内 Crew**(internal):由 Main Agent 管理,使用此技能。 +- **对外 Crew**(external,如客服/销售/社群接待):需要先启用 HRBP,并配置合适的工作/对外 channel;不要直接用此技能创建。 + +## 招募原则 + +- 不可招募 `it-engineer` 和 `hrbp`;这两个是全局唯一内置角色。 +- Main Agent 也不可被重新招募。 +- Main Agent 允许 spawn 除 `hrbp` 外的所有对内 crew;每次成功招募对内 crew 后,必须自动补入 `agents.main.subagents.allowAgents`。 +- 每个新招募的对内 crew 必须自动允许调用 `it-engineer`,即补入该 crew 的 `subagents.allowAgents: ["it-engineer"]`。 +- 其他对内 crew 可以有多个实例,但多实例必须绑定不同的工作 channel/account,避免同一入口路由到多个相似实例造成混淆。 +- 默认招募内部 crew 不强制 direct channel binding;当用户要创建多个同类实例时,应先引导配置不同的 Feishu 或 WeCom 账号绑定。 ## 执行步骤 ``` -1. 了解业务需求:角色职责、需要哪些技能、是否需要渠道绑定 -2. 确定模板 ID(可选,默认同 agent-id) -3. 向用户展示创建方案,等待用户确认 -4. 用户确认后运行脚本 -5. 脚本执行成功后,告知用户必须重启 Gateway 才能生效,并询问是否立即重启 -6. 用户确认重启 → 执行 systemctl --user restart openclaw-gateway +1. 了解业务需求:角色职责、长期任务、是否需要直接工作 channel。 +2. 确定模板 ID(可选,默认同 agent-id)。 +3. 向用户展示创建方案,请求确认。 +4. 用户确认后运行脚本。 +5. 脚本成功后更新 reminder.json(TEAM_DIRECTORY.md 由脚本内部自动同步,无需手动操作)。 +6. 如果团队规模触发阈值,建议用户配置 Feishu 或 WeCom 工作 channel。 +7. 如本次创建或绑定要求 Gateway restart,先记录 pending-followup,再询问用户是否立即重启。 ``` -> ⚠️ 步骤 5-6 是**必需**的,不可跳过。原因:openclaw 热加载对 `bindings` 变更标记为 `none`(不触发 channel 重启),而 feishu channel 在启动时将配置捕获为快照,不会自动刷新。招募新 crew 后若不重启 gateway,新 crew 的 feishu bot 收到的消息会被错误路由到 `agent:main`。只有重启 gateway 才能让所有 channel 重新加载包含新 bindings 的完整配置。 +默认招募内部 crew 不强制 direct channel binding。工作 channel binding 使用 `work-channel-binding` skill 单独完成。 ## 脚本用法 @@ -26,36 +37,50 @@ ``` ### 参数说明 -- ``:实例 ID(小写字母、数字、连字符) -- `--template `:使用哪个模板(默认同 agent-id) -- `--bind :`:渠道绑定(可选,事后可手动添加) -- `--note `:备注信息 + +- ``:实例 ID(小写字母、数字、连字符)。 +- `--template `:使用哪个模板(默认同 agent-id)。 +- `--bind :`:高级选项;默认流程不要使用,除非用户已完成 work channel 配置。 +- `--note `:备注信息。 ### 示例 + ```bash ./skills/crew-recruit/scripts/recruit-internal-crew.sh sales-analyst --template developer --note "销售数据分析专员" ``` ## 重要约束 -- 不可创建内置保护名单中的 agent:main、hrbp、it-engineer -- workspace 必须事先创建(脚本会检查) -- 对内 Crew 使用**继承模式**技能,自动获得基线技能 -- 项目级 / addon 全局技能默认不自动继承;需要在目标 workspace 的 `BUILTIN_SKILLS` 中显式声明 -## 重启 Gateway +- 不可创建内置保护名单中的 agent:main、hrbp、it-engineer。 +- workspace 必须事先创建(脚本会检查)。 +- 对内 Crew 使用继承模式技能,自动获得基线技能。 +- 项目级 / addon 全局技能默认不自动继承;需要在目标 workspace 的 `BUILTIN_SKILLS` 中显式声明。 -招募完成后**必须重启** gateway,否则新 crew 的渠道路由不会生效。重启前需征得用户确认: +## 工作 Channel 提醒 -``` -招募已完成!新 crew 的配置已写入 openclaw.json。 -⚠️ 需要重启 Gateway 才能激活新 crew 的渠道路由(热加载无法刷新 binding 快照)。 -重启期间所有 agent 将短暂中断(约 10-15 秒)。 -是否现在重启?(确认/稍后) +招募后检查内部 crew 数量: + +- 不算 `main`。 +- 算 `it-engineer`。 +- 算已启用的 `hrbp`。 +- 当内部 crew 数量大于 3 时,提醒用户配置 Feishu 或 WeCom。 + +首次招募对外 crew 的需求不走此技能;应引导启用 HRBP 和工作 channel。 + +## Gateway Restart + +如果本次操作修改了 bindings 或 OpenClaw 需要重启才能加载 agent 配置,必须先询问用户再重启。 + +重启前运行: + +```bash +python ./skills/work-channel-binding/scripts/record-pending-followup.py --reason crew-recruit ``` 用户确认后执行: + ```bash -systemctl --user restart openclaw-gateway +WISEFLOW_CONFIRM_GATEWAY_RESTART=confirmed ./skills/work-channel-binding/scripts/restart-gateway-confirmed.sh crew-recruit ``` -用户选择"稍后"时,提醒用户稍后手动执行上述命令。 +如果用户选择稍后,提醒用户稍后手动重启,并保留 pending followup。 diff --git a/crews/main/skills/reminder/SKILL.md b/crews/main/skills/reminder/SKILL.md new file mode 100644 index 00000000..37b4e409 --- /dev/null +++ b/crews/main/skills/reminder/SKILL.md @@ -0,0 +1,19 @@ +--- +name: reminder +description: Maintain Main Agent reminder.json for onboarding, work channel recommendations, HRBP enablement, Media Operator bootstrap, and Gateway restart followups. +metadata: + openclaw: + emoji: 🔔 +--- + +# Reminder + +Use this skill during Main Agent heartbeat or when the user asks for onboarding status. + +Commands: + +- `python ./skills/reminder/scripts/update-reminders.py` + +The script updates `~/.openclaw/workspace-main/reminder.json`. + +Do not notify repeatedly. Respect `lastNotifiedAt`, `snoozedUntil`, and `status` fields when present. diff --git a/crews/main/skills/reminder/scripts/update-reminders.py b/crews/main/skills/reminder/scripts/update-reminders.py new file mode 100755 index 00000000..fd9af5a2 --- /dev/null +++ b/crews/main/skills/reminder/scripts/update-reminders.py @@ -0,0 +1,155 @@ +#!/usr/bin/env python3 +import json +import os +from datetime import datetime, timezone +from pathlib import Path +from typing import Any + +PRESERVED_FIELDS = {"status", "lastNotifiedAt", "snoozedUntil", "dismissedAt"} + + +def config_path() -> Path: + return Path( + os.environ.get( + "OPENCLAW_CONFIG_PATH", + Path.home() / ".openclaw" / "openclaw.json", + ) + ).expanduser() + + +def workspace_path() -> Path: + return Path(os.environ.get("MAIN_AGENT_WORKSPACE", Path.home() / ".openclaw" / "workspace-main")).expanduser() + + +def reminder_path() -> Path: + return workspace_path() / "reminder.json" + + +def pending_followup_path() -> Path: + return workspace_path() / "pending-followup.json" + + +def atomic_write_json(path: Path, payload: dict[str, Any]) -> None: + tmp = path.with_name(path.name + ".tmp") + tmp.write_text( + json.dumps(payload, ensure_ascii=False, indent=2) + "\n", + encoding="utf-8", + ) + tmp.replace(path) + + +def load_config() -> dict[str, Any]: + path = config_path() + if not path.exists(): + return {} + payload = json.loads(path.read_text(encoding="utf-8")) + return payload if isinstance(payload, dict) else {} + + +def load_existing_items(path: Path) -> dict[str, dict[str, Any]]: + if not path.exists(): + return {} + try: + payload = json.loads(path.read_text(encoding="utf-8")) + except (json.JSONDecodeError, OSError): + return {} + if not isinstance(payload, dict) or not isinstance(payload.get("items"), list): + return {} + result: dict[str, dict[str, Any]] = {} + for item in payload["items"]: + if isinstance(item, dict) and isinstance(item.get("id"), str): + result[item["id"]] = item + return result + + +def merge_item(item: dict[str, Any], existing: dict[str, dict[str, Any]]) -> dict[str, Any]: + previous = existing.get(item["id"], {}) + merged = dict(item) + for field in PRESERVED_FIELDS: + if field in previous: + merged[field] = previous[field] + if "createdAt" in previous: + merged["createdAt"] = previous["createdAt"] + else: + merged["createdAt"] = item["updatedAt"] + return merged + + +def main() -> None: + config = load_config() + now = datetime.now(timezone.utc).isoformat() + agents = config.get("agents", {}).get("list", []) if config else [] + if not isinstance(agents, list): + agents = [] + agent_ids = {agent.get("id") for agent in agents if isinstance(agent, dict) and agent.get("id")} + bindings = config.get("bindings") if isinstance(config.get("bindings"), list) else [] + work_channels = {"feishu", "wecom"} + + def has_work_binding(agent_id: str) -> bool: + return any( + isinstance(binding, dict) + and binding.get("agentId") == agent_id + and isinstance(binding.get("match"), dict) + and binding["match"].get("channel") in work_channels + for binding in bindings + ) + + items: list[dict[str, Any]] = [] + internal_count = len([agent_id for agent_id in agent_ids if agent_id != "main"]) + if internal_count > 3: + items.append({ + "id": "work-channel-needed-internal-team", + "type": "work-channel", + "severity": "suggestion", + "status": "open", + "title": "建议启用工作 channel", + "message": "内部 crew 数量已经较多,建议为关键成员配置 Feishu 或 WeCom。", + "reason": "internal crew count excluding main is greater than 3", + "updatedAt": now, + }) + if not has_work_binding("it-engineer"): + items.append({ + "id": "it-engineer-no-work-binding", + "type": "work-channel", + "severity": "info", + "status": "open", + "title": "IT Engineer 尚无工作 channel", + "message": "首次配置工作 channel 时,建议顺手给 IT Engineer 也配置 direct binding。", + "reason": "it-engineer has no Feishu/WeCom binding", + "updatedAt": now, + }) + if "hrbp" in agent_ids and not has_work_binding("hrbp"): + items.append({ + "id": "hrbp-no-work-binding", + "type": "work-channel", + "severity": "info", + "status": "open", + "title": "HRBP 尚无工作 channel", + "message": "HRBP 已启用但没有 Feishu/WeCom binding,建议配置。", + "reason": "hrbp enabled without work binding", + "updatedAt": now, + }) + + if pending_followup_path().exists(): + items.append({ + "id": "pending-gateway-restart-followup", + "type": "followup", + "severity": "warning", + "status": "open", + "title": "Gateway 重启后待确认", + "message": "存在 Gateway restart followup,请确认服务和 channel binding 是否恢复正常。", + "reason": "pending-followup.json exists", + "updatedAt": now, + }) + + path = reminder_path() + existing = load_existing_items(path) + merged_items = [merge_item(item, existing) for item in items] + output = {"version": 1, "updatedAt": now, "items": merged_items} + path.parent.mkdir(parents=True, exist_ok=True) + atomic_write_json(path, output) + print(json.dumps({"reminderPath": str(path), "itemCount": len(merged_items)}, ensure_ascii=False, indent=2)) + + +if __name__ == "__main__": + main() diff --git a/crews/main/skills/work-channel-binding/SKILL.md b/crews/main/skills/work-channel-binding/SKILL.md new file mode 100644 index 00000000..39f6df54 --- /dev/null +++ b/crews/main/skills/work-channel-binding/SKILL.md @@ -0,0 +1,66 @@ +--- +name: work-channel-binding +description: Guide Feishu or WeCom work channel binding for Main Agent managed crews, including dry-run config plans, safe openclaw.json updates, Gateway restart followup, and binding checks. +metadata: + openclaw: + emoji: 🔗 +--- + +# Work Channel Binding + +Use this skill when the user wants to configure a work channel or when Main Agent recommends one. + +Supported channels: + +- Feishu +- WeCom + +## Channel Plugin Prerequisites + +Before collecting account credentials, confirm the selected channel plugin is installed and enabled. + +- Feishu: follow `./skills/work-channel-binding/docs/feishu.md` and the current OpenClaw Feishu setup path. +- WeCom: Main Agent installs the plugin by running: + +```bash +WISEFLOW_CONFIRM_WECOM_INSTALL=confirmed ./skills/work-channel-binding/scripts/install-wecom-channel.sh +``` + +After installing a channel plugin, tell the user that Gateway may need a restart before binding verification succeeds. + +## Required Flow + +1. Ask the user to choose Feishu or WeCom. +2. Show the relevant tutorial: + - `./skills/work-channel-binding/docs/feishu.md` + - `./skills/work-channel-binding/docs/wecom.md` +3. Confirm channel plugin readiness. For WeCom, if the plugin is not installed yet, run `WISEFLOW_CONFIRM_WECOM_INSTALL=confirmed ./skills/work-channel-binding/scripts/install-wecom-channel.sh` from Main Agent after user confirmation; do not ask the user to run `npx` manually. +4. Collect account information: + - account id; + - account name; + - app/bot id; + - app/bot secret; + - target agent for each account; + - `dmPolicy` for private chats; + - `groupPolicy` for group chats. + If the user is unsure, default both policies to `open`. Explain that even when group chat policy is `open`, group chats only respond to messages that mention the bot. Do not repeat secrets back in summaries; scripts must redact them in output. +5. Run a binding check: + - `python ./skills/work-channel-binding/scripts/check-work-channel-bindings.py` +6. Prepare a dry-run plan: + - `python ./skills/work-channel-binding/scripts/prepare-work-channel-binding.py --channel --plan-file --account-id --account-name --agent-id --app-id --app-secret --dm-policy open --group-policy open` +7. Show a redacted summary and ask for user's confirmation. +8. Apply only after confirmation: + - `python ./skills/work-channel-binding/scripts/apply-work-channel-binding.py --plan-file ` +9. Ask for Gateway restart confirmation. +10. Before restarting, record followup: + - `python ./skills/work-channel-binding/scripts/record-pending-followup.py --reason work-channel-binding` +11. Restart Gateway only after user confirmation: + - `WISEFLOW_CONFIRM_GATEWAY_RESTART=confirmed ./skills/work-channel-binding/scripts/restart-gateway-confirmed.sh work-channel-binding` +12. On next session, complete followup: + - `python ./skills/work-channel-binding/scripts/complete-pending-followup.py` + +## First Work Channel Binding Reminder + +If this is the first work channel binding, check whether `it-engineer` and enabled/soon-to-be-enabled `hrbp` already have direct bindings. If not, ask whether the user wants to bind them together. + +Never print secrets back to the user. diff --git a/crews/main/skills/work-channel-binding/docs/feishu.md b/crews/main/skills/work-channel-binding/docs/feishu.md new file mode 100644 index 00000000..3a071f94 --- /dev/null +++ b/crews/main/skills/work-channel-binding/docs/feishu.md @@ -0,0 +1,82 @@ +# Feishu Work Channel Setup + +## 用户侧需要完成: + +### 1.创建飞书应用 +1. 访问 飞书开放平台(https://open.feishu.cn/?lang=zh-CN),用飞书账号登录 +2. 点击「创建企业自建应用」 +3. 填写应用名称和描述,选择图标 +4. 创建完成后,进入应用详情 + +### 2.获取应用凭证 +在「凭证与基础信息」页面,复制: +- App ID(格式如 cli_xxx) +- App Secret +- 将 APP ID 和 APP Secret 告知 main agent + +⚠️ 重要: 请妥善保管 App Secret,不要分享给他人! + +### 3.配置权限 +在「权限管理」页面,点击「批量导入」,粘贴以下 JSON: +``` +{ + "scopes": { + "tenant": [ + "aily:file:read", + "aily:file:write", + "application:application.app_message_stats.overview:readonly", + "application:application:self_manage", + "application:bot.menu:write", + "bitable:app", + "cardkit:card:write", + "contact:contact.base:readonly", + "corehr:file:download", + "docs:doc", + "docs:document.content:read", + "docs:document.media:upload", + "docx:document", + "docx:document.block:convert", + "docx:document:create", + "docx:document:readonly", + "docx:document:write_only", + "drive:drive", + "drive:drive.metadata:readonly", + "drive:drive.search:readonly", + "drive:drive:version", + "drive:drive:version:readonly", + "event:ip_list", + "im:chat", + "im:chat.access_event.bot_p2p_chat:read", + "im:chat.members:bot_access", + "im:message", + "im:message.group_at_msg:readonly", + "im:message.group_msg", + "im:message.p2p_msg:readonly", + "im:message:readonly", + "im:message:send_as_bot", + "im:resource", + "sheets:spreadsheet", + "wiki:wiki", + "wiki:wiki:readonly" + ], + "user": [ + "im:chat.access_event.bot_p2p_chat:read" + ] + } +} +``` + +### 4.启用机器人能力 +在「应用能力 → 机器人」页面: +1. 开启机器人能力 +2. 配置机器人名称 + +### 5.配置事件订阅 +在「事件与回调」-> 「事件配置」页面: +1. 选择「使用长连接接收事件」(WebSocket 模式) +2. 添加事件:im.message.receive_v1(接收消息) + +### 6。发布应用 +1. 在「版本管理与发布」页面创建版本 +2. 提交审核并发布 +3. 等待管理员审批(企业自建应用通常自动通过) diff --git a/crews/main/skills/work-channel-binding/docs/wecom.md b/crews/main/skills/work-channel-binding/docs/wecom.md new file mode 100644 index 00000000..cfb48d99 --- /dev/null +++ b/crews/main/skills/work-channel-binding/docs/wecom.md @@ -0,0 +1,31 @@ +# WeCom Work Channel Setup + +## 前置:安装 WeCom OpenClaw channel plugin + +Main Agent 会在绑定流程中自动执行安装脚本: + +```bash +WISEFLOW_CONFIRM_WECOM_INSTALL=confirmed ./skills/work-channel-binding/scripts/install-wecom-channel.sh +``` + +用户不需要手动运行 `npx`。安装完成后,后续绑定账号与修改 `openclaw.json` 可能需要重启 Gateway 才能生效。 + +## 用户侧需要完成: + +### 一、创建智能机器人 + +登录企业微信管理后台(https://work.weixin.qq.com/),以长连接方式创建智能机器人,获取Bot ID和Secret + +操作步骤如下: +- 1、打开企业微信客户端或者登录网页版(https://work.weixin.qq.com/),进入工作台->智能机器人,点击创建机器人->手动创建; +- 2、进入创建页面后,选择API模式创建(页面提示「如需使用自有系统获取成员与机器人的聊天并输出回复,可切换至API模式创建」); +- 3、在API配置页面,选择连接方式为「使用长连接」(无需域名/IP即可接收消息并返回结果,区别于URL回调方式); +- 4、配置完成后,页面将自动生成并展示Bot ID和Secret,妥善保存该信息(后续关联OpenClaw需使用); +- 5、补充配置机器人可见范围,其余项保持默认即可,API模式暂不支持预览与调试,直接保存机器人配置。 +- 6、将Bot ID和Secret 告知 main agent + +⚠️ 重要: 请妥善保管 App Secret,不要分享给他人! + +- 7、等待main agent完成绑定后,回到企业微信机器人创建页面,保存并创建。即可在企业微信中与智能机器人正常对话。 + +如配置完成后未能找到机器人,可在以下路径中找到:工作台->智能机器人->详情->去使用->发消息 diff --git a/crews/main/skills/work-channel-binding/scripts/apply-work-channel-binding.py b/crews/main/skills/work-channel-binding/scripts/apply-work-channel-binding.py new file mode 100755 index 00000000..2a7f70b2 --- /dev/null +++ b/crews/main/skills/work-channel-binding/scripts/apply-work-channel-binding.py @@ -0,0 +1,219 @@ +#!/usr/bin/env python3 +import argparse +import json +import os +import shutil +from datetime import datetime, timezone +from pathlib import Path +from typing import Any + + +VALID_CHANNELS = {"feishu", "wecom"} + + +def config_path() -> Path: + return Path( + os.environ.get( + "OPENCLAW_CONFIG_PATH", + Path.home() / ".openclaw" / "openclaw.json", + ) + ).expanduser() + + +def atomic_write_json(path: Path, payload: dict[str, Any]) -> None: + tmp = path.with_name(path.name + ".tmp") + tmp.write_text( + json.dumps(payload, ensure_ascii=False, indent=2) + "\n", + encoding="utf-8", + ) + tmp.replace(path) + + +def load_json_object(path: Path, label: str) -> dict[str, Any]: + try: + payload = json.loads(path.read_text(encoding="utf-8")) + except json.JSONDecodeError as exc: + raise SystemExit(f"invalid JSON in {label} {path}: {exc}") from exc + except OSError as exc: + raise SystemExit(f"cannot read {label} {path}: {exc}") from exc + if not isinstance(payload, dict): + raise SystemExit(f"{label} must be a JSON object: {path}") + return payload + + +def ensure_dict(parent: dict[str, Any], key: str) -> dict[str, Any]: + value = parent.get(key) + if value is None: + value = {} + parent[key] = value + if not isinstance(value, dict): + raise SystemExit(f"config.{key} must be an object") + return value + + +def ensure_list(parent: dict[str, Any], key: str) -> list[Any]: + value = parent.get(key) + if value is None: + value = [] + parent[key] = value + if not isinstance(value, list): + raise SystemExit(f"config.{key} must be an array") + return value + + +def validated_plan(plan: dict[str, Any]) -> tuple[str, list[dict[str, str]], list[dict[str, str]]]: + version = plan.get("version") + if version != 1: + raise SystemExit("plan.version must be 1") + channel = plan.get("channel") + if channel not in VALID_CHANNELS: + raise SystemExit("plan.channel must be feishu or wecom") + + raw_accounts = plan.get("accounts") + if not isinstance(raw_accounts, list): + raise SystemExit("plan.accounts must be an array") + accounts: list[dict[str, str]] = [] + for index, item in enumerate(raw_accounts): + if not isinstance(item, dict): + raise SystemExit(f"plan.accounts[{index}] must be an object") + account_id = item.get("accountId") + app_id = item.get("appId") + app_secret = item.get("appSecret") + name = item.get("name") or account_id + dm_policy = item.get("dmPolicy") or "open" + group_policy = item.get("groupPolicy") or "open" + for field, value in { + "accountId": account_id, + "appId": app_id, + "appSecret": app_secret, + "name": name, + "dmPolicy": dm_policy, + "groupPolicy": group_policy, + }.items(): + if not isinstance(value, str) or not value.strip(): + raise SystemExit(f"plan.accounts[{index}].{field} must be a non-empty string") + accounts.append( + { + "accountId": account_id.strip(), + "name": name.strip(), + "appId": app_id.strip(), + "appSecret": app_secret, + "dmPolicy": dm_policy.strip(), + "groupPolicy": group_policy.strip(), + } + ) + + raw_bindings = plan.get("bindings") + if not isinstance(raw_bindings, list): + raise SystemExit("plan.bindings must be an array") + + account_ids = {account["accountId"] for account in accounts} + bindings: list[dict[str, str]] = [] + for index, item in enumerate(raw_bindings): + if not isinstance(item, dict): + raise SystemExit(f"plan.bindings[{index}] must be an object") + agent_id = item.get("agentId") + account_id = item.get("accountId") + if not isinstance(agent_id, str) or not agent_id.strip(): + raise SystemExit(f"plan.bindings[{index}].agentId must be a non-empty string") + if not isinstance(account_id, str) or not account_id.strip(): + raise SystemExit(f"plan.bindings[{index}].accountId must be a non-empty string") + if account_id.strip() not in account_ids: + raise SystemExit(f"plan.bindings[{index}].accountId has no matching account") + bindings.append({"agentId": agent_id.strip(), "accountId": account_id.strip()}) + return channel, accounts, bindings + + +def binding_exists( + bindings: list[Any], + agent_id: str, + channel: str, + account_id: str, +) -> bool: + for binding in bindings: + if not isinstance(binding, dict): + continue + match = binding.get("match") + if not isinstance(match, dict): + continue + if ( + binding.get("agentId") == agent_id + and match.get("channel") == channel + and match.get("accountId") == account_id + ): + return True + return False + + +def main() -> None: + parser = argparse.ArgumentParser( + description="Apply a confirmed work channel binding plan." + ) + parser.add_argument("--plan-file", required=True) + args = parser.parse_args() + + plan_path = Path(args.plan_file).expanduser() + plan = load_json_object(plan_path, "plan") + channel, plan_accounts, plan_bindings = validated_plan(plan) + + path = config_path() + config = load_json_object(path, "openclaw config") + backup = path.with_suffix( + path.suffix + + ".bak-" + + datetime.now(timezone.utc).strftime("%Y%m%d%H%M%S%f") + ) + shutil.copy2(path, backup) + + channels = ensure_dict(config, "channels") + channel_config = channels.setdefault(channel, {"enabled": True, "accounts": {}}) + if not isinstance(channel_config, dict): + raise SystemExit(f"config.channels.{channel} must be an object") + channel_config["enabled"] = True + accounts_config = channel_config.setdefault("accounts", {}) + if not isinstance(accounts_config, dict): + raise SystemExit(f"config.channels.{channel}.accounts must be an object") + for account in plan_accounts: + account_id = account["accountId"] + accounts_config[account_id] = { + **(accounts_config.get(account_id) if isinstance(accounts_config.get(account_id), dict) else {}), + "name": account["name"], + "appId": account["appId"], + "appSecret": account["appSecret"], + "dmPolicy": account["dmPolicy"], + "groupPolicy": account["groupPolicy"], + } + + plugins = ensure_dict(config, "plugins") + plugin_entries = ensure_dict(plugins, "entries") + plugin_config = plugin_entries.setdefault(channel, {"enabled": True}) + if not isinstance(plugin_config, dict): + raise SystemExit(f"config.plugins.entries.{channel} must be an object") + plugin_config["enabled"] = True + + bindings = ensure_list(config, "bindings") + for item in plan_bindings: + agent_id = item["agentId"] + account_id = item["accountId"] + if binding_exists(bindings, agent_id, channel, account_id): + continue + bindings.append( + { + "agentId": agent_id, + "comment": f"{channel}:{account_id} -> {agent_id}", + "match": {"channel": channel, "accountId": account_id}, + } + ) + + atomic_write_json(path, config) + print( + json.dumps( + {"updated": str(path), "backup": str(backup), "channel": channel}, + ensure_ascii=False, + indent=2, + ) + ) + + +if __name__ == "__main__": + main() diff --git a/crews/main/skills/work-channel-binding/scripts/check-work-channel-bindings.py b/crews/main/skills/work-channel-binding/scripts/check-work-channel-bindings.py new file mode 100755 index 00000000..bc857e26 --- /dev/null +++ b/crews/main/skills/work-channel-binding/scripts/check-work-channel-bindings.py @@ -0,0 +1,108 @@ +#!/usr/bin/env python3 +import json +import os +from pathlib import Path +from typing import Any + + +WORK_CHANNELS = {"feishu", "wecom"} + + +def config_path() -> Path: + return Path( + os.environ.get( + "OPENCLAW_CONFIG_PATH", + Path.home() / ".openclaw" / "openclaw.json", + ) + ).expanduser() + + +def load_config(path: Path) -> dict[str, Any]: + if not path.exists(): + raise SystemExit(f"openclaw config not found: {path}") + try: + payload = json.loads(path.read_text(encoding="utf-8")) + except json.JSONDecodeError as exc: + raise SystemExit(f"invalid JSON in openclaw config {path}: {exc}") from exc + if not isinstance(payload, dict): + raise SystemExit(f"openclaw config must be a JSON object: {path}") + return payload + + +def configured_accounts(channels: dict[str, Any]) -> dict[str, list[dict[str, Any]]]: + result: dict[str, list[dict[str, Any]]] = {} + for channel in WORK_CHANNELS: + channel_config = channels.get(channel) + if not isinstance(channel_config, dict): + continue + accounts = channel_config.get("accounts") + if not isinstance(accounts, dict): + continue + result[channel] = [ + { + "accountId": account_id, + "name": account.get("name") if isinstance(account, dict) else None, + "hasAppId": bool(account.get("appId")) if isinstance(account, dict) else False, + "hasAppSecret": bool(account.get("appSecret")) if isinstance(account, dict) else False, + "dmPolicy": account.get("dmPolicy") if isinstance(account, dict) else None, + "groupPolicy": account.get("groupPolicy") if isinstance(account, dict) else None, + } + for account_id, account in sorted(accounts.items()) + ] + return result + + +def main() -> None: + path = config_path() + config = load_config(path) + raw_agents = config.get("agents") + raw_agents_list = raw_agents.get("list", []) if isinstance(raw_agents, dict) else [] + agents = { + agent.get("id") + for agent in raw_agents_list + if isinstance(agent, dict) and agent.get("id") + } + bindings = config.get("bindings") if isinstance(config.get("bindings"), list) else [] + channels = config.get("channels") if isinstance(config.get("channels"), dict) else {} + + agent_bindings: dict[str, list[dict[str, Any]]] = {} + for binding in bindings: + if not isinstance(binding, dict): + continue + agent_id = binding.get("agentId") + match = binding.get("match") + if not isinstance(match, dict): + continue + channel = match.get("channel") + account_id = match.get("accountId") + if not agent_id or not channel: + continue + agent_bindings.setdefault(agent_id, []).append( + {"channel": channel, "accountId": account_id} + ) + + summary = { + "configPath": str(path), + "agents": sorted(agents), + "enabledChannels": sorted( + name + for name, value in channels.items() + if isinstance(value, dict) and value.get("enabled") is not False + ), + "workChannelsConfigured": sorted(name for name in WORK_CHANNELS if name in channels), + "workChannelAccounts": configured_accounts(channels), + "bindings": agent_bindings, + "itEngineerHasWorkBinding": any( + item["channel"] in WORK_CHANNELS + for item in agent_bindings.get("it-engineer", []) + ), + "hrbpEnabled": "hrbp" in agents, + "hrbpHasWorkBinding": any( + item["channel"] in WORK_CHANNELS for item in agent_bindings.get("hrbp", []) + ), + } + print(json.dumps(summary, ensure_ascii=False, indent=2)) + + +if __name__ == "__main__": + main() diff --git a/crews/main/skills/work-channel-binding/scripts/complete-pending-followup.py b/crews/main/skills/work-channel-binding/scripts/complete-pending-followup.py new file mode 100755 index 00000000..dbcb9ba0 --- /dev/null +++ b/crews/main/skills/work-channel-binding/scripts/complete-pending-followup.py @@ -0,0 +1,30 @@ +#!/usr/bin/env python3 +import json +from datetime import datetime, timezone +from pathlib import Path + + +def state_path() -> Path: + return Path.home() / ".openclaw" / "workspace-main" / "pending-followup.json" + + +def atomic_write_json(path: Path, payload: dict) -> None: + tmp = path.with_name(path.name + ".tmp") + tmp.write_text(json.dumps(payload, ensure_ascii=False, indent=2) + "\n", encoding="utf-8") + tmp.replace(path) + + +def main() -> None: + path = state_path() + if not path.exists(): + print(json.dumps({"status": "none"}, ensure_ascii=False, indent=2)) + return + payload = json.loads(path.read_text(encoding="utf-8")) + payload["status"] = "completed" + payload["completedAt"] = datetime.now(timezone.utc).isoformat() + atomic_write_json(path, payload) + print(json.dumps({"status": "completed", "message": payload.get("message")}, ensure_ascii=False, indent=2)) + + +if __name__ == "__main__": + main() diff --git a/crews/main/skills/work-channel-binding/scripts/install-wecom-channel.sh b/crews/main/skills/work-channel-binding/scripts/install-wecom-channel.sh new file mode 100755 index 00000000..8e14597f --- /dev/null +++ b/crews/main/skills/work-channel-binding/scripts/install-wecom-channel.sh @@ -0,0 +1,89 @@ +#!/bin/bash +# install-wecom-channel.sh - install and enable the WeCom OpenClaw channel plugin +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +PROJECT_ROOT="$(cd "$SCRIPT_DIR/../../../../.." && pwd)" +PIN_FILE="$PROJECT_ROOT/openclaw-weixin.version.json" +OPENCLAW_CONFIG_PATH="${OPENCLAW_CONFIG_PATH:-$HOME/.openclaw/openclaw.json}" + +if [ "${WISEFLOW_CONFIRM_WECOM_INSTALL:-}" != "confirmed" ]; then + echo "ERROR: WeCom plugin install requires explicit confirmation." + echo "Run with WISEFLOW_CONFIRM_WECOM_INSTALL=confirmed after the user confirms installation." + exit 1 +fi + +if [ ! -f "$OPENCLAW_CONFIG_PATH" ]; then + echo "ERROR: openclaw config not found: $OPENCLAW_CONFIG_PATH" + exit 1 +fi + +if [ ! -f "$PIN_FILE" ]; then + echo "ERROR: pin file not found: $PIN_FILE" + exit 1 +fi + +pin_values="$(node -e ' + const fs = require("fs"); + const p = process.argv[1]; + const c = JSON.parse(fs.readFileSync(p, "utf8")); + const entry = c["wecom-openclaw-cli"] || {}; + const validPackage = /^@[a-z0-9._-]+\/[a-z0-9._-]+$/; + const validVersion = /^\d+\.\d+\.\d+(?:[-+][a-zA-Z0-9._-]+)?$/; + const validIntegrity = /^sha512-[A-Za-z0-9+/]+={0,2}$/; + if (!validPackage.test(entry.package || "")) throw new Error("wecom package invalid"); + if (!validVersion.test(entry.version || "")) throw new Error("wecom version invalid"); + if (!validIntegrity.test(entry.integrity || "")) throw new Error("wecom integrity invalid"); + console.log([entry.package, entry.version, entry.integrity].join("\t")); +' "$PIN_FILE")" +IFS=$'\t' read -r WECOM_PACKAGE WECOM_VERSION WECOM_INTEGRITY <<< "$pin_values" + +TMP_DIR="$(mktemp -d)" +cleanup() { + rm -rf "$TMP_DIR" +} +trap cleanup EXIT + +echo "Installing pinned WeCom OpenClaw channel plugin: $WECOM_PACKAGE@$WECOM_VERSION" +pack_output="$(npm pack "$WECOM_PACKAGE@$WECOM_VERSION" --json --pack-destination "$TMP_DIR")" +package_file="$(node -e ' + const fs = require("fs"); + const payload = JSON.parse(fs.readFileSync(0, "utf8")); + if (!Array.isArray(payload) || payload.length !== 1 || !payload[0].filename) { + throw new Error("unexpected npm pack output"); + } + console.log(payload[0].filename); +' <<< "$pack_output")" +package_file="$TMP_DIR/$package_file" +package_integrity="$(node -e ' + const fs = require("fs"); + const crypto = require("crypto"); + const file = process.argv[1]; + console.log("sha512-" + crypto.createHash("sha512").update(fs.readFileSync(file)).digest("base64")); +' "$package_file")" + +if [ "$package_integrity" != "$WECOM_INTEGRITY" ]; then + echo "ERROR: integrity mismatch for $package_file" + exit 1 +fi + +npx -y "$package_file" install + +node -e ' + const fs = require("fs"); + const path = process.argv[1]; + const backup = path + ".bak-" + new Date().toISOString().replace(/[-:.TZ]/g, ""); + const tmp = path + ".tmp"; + const c = JSON.parse(fs.readFileSync(path, "utf8")); + c.plugins = c.plugins || {}; + c.plugins.entries = c.plugins.entries || {}; + c.plugins.entries.wecom = { ...(c.plugins.entries.wecom || {}), enabled: true }; + c.channels = c.channels || {}; + c.channels.wecom = { ...(c.channels.wecom || {}), enabled: true }; + fs.copyFileSync(path, backup); + fs.writeFileSync(tmp, JSON.stringify(c, null, 2) + "\n", { mode: 0o600 }); + fs.renameSync(tmp, path); + console.log(JSON.stringify({ updated: path, backup }, null, 2)); +' "$OPENCLAW_CONFIG_PATH" + +echo "WeCom channel plugin installed and enabled. Gateway restart is required before binding verification." diff --git a/crews/main/skills/work-channel-binding/scripts/prepare-work-channel-binding.py b/crews/main/skills/work-channel-binding/scripts/prepare-work-channel-binding.py new file mode 100755 index 00000000..9fe3ba0e --- /dev/null +++ b/crews/main/skills/work-channel-binding/scripts/prepare-work-channel-binding.py @@ -0,0 +1,103 @@ +#!/usr/bin/env python3 +import argparse +import json +from pathlib import Path +from typing import Any + + +def redacted_accounts(accounts: list[dict[str, str]]) -> list[dict[str, str]]: + return [ + { + "accountId": account["accountId"], + "name": account.get("name", account["accountId"]), + "appId": account.get("appId", ""), + "appSecret": "***" if account.get("appSecret") else "", + "dmPolicy": account.get("dmPolicy", "open"), + "groupPolicy": account.get("groupPolicy", "open"), + } + for account in accounts + ] + + +def atomic_write_json(path: Path, payload: dict[str, Any]) -> None: + tmp = path.with_name(path.name + ".tmp") + tmp.write_text( + json.dumps(payload, ensure_ascii=False, indent=2) + "\n", + encoding="utf-8", + ) + tmp.replace(path) + + +def main() -> None: + parser = argparse.ArgumentParser(description="Prepare a redacted work channel binding plan.") + parser.add_argument("--channel", required=True, choices=["feishu", "wecom"]) + parser.add_argument("--plan-file", required=True) + parser.add_argument("--account-id", action="append", default=[]) + parser.add_argument("--agent-id", action="append", default=[]) + parser.add_argument("--app-id", action="append", default=[]) + parser.add_argument("--app-secret", action="append", default=[]) + parser.add_argument("--account-name", action="append", default=[]) + parser.add_argument("--dm-policy", action="append", default=[]) + parser.add_argument("--group-policy", action="append", default=[]) + args = parser.parse_args() + + expected = len(args.account_id) + for label, values in { + "--agent-id": args.agent_id, + "--app-id": args.app_id, + "--app-secret": args.app_secret, + }.items(): + if len(values) != expected: + raise SystemExit(f"{label} must appear the same number of times as --account-id") + if args.account_name and len(args.account_name) != expected: + raise SystemExit("--account-name must appear the same number of times as --account-id when provided") + if args.dm_policy and len(args.dm_policy) != expected: + raise SystemExit("--dm-policy must appear the same number of times as --account-id when provided") + if args.group_policy and len(args.group_policy) != expected: + raise SystemExit("--group-policy must appear the same number of times as --account-id when provided") + + accounts: list[dict[str, str]] = [] + bindings: list[dict[str, str]] = [] + for index, account_id in enumerate(args.account_id): + name = args.account_name[index] if args.account_name else account_id + dm_policy = args.dm_policy[index] if args.dm_policy else "open" + group_policy = args.group_policy[index] if args.group_policy else "open" + accounts.append( + { + "accountId": account_id, + "name": name, + "appId": args.app_id[index], + "appSecret": args.app_secret[index], + "dmPolicy": dm_policy, + "groupPolicy": group_policy, + } + ) + bindings.append( + {"agentId": args.agent_id[index], "accountId": account_id, "channel": args.channel} + ) + + plan = { + "version": 1, + "channel": args.channel, + "accounts": accounts, + "bindings": bindings, + "requiresGatewayRestart": True, + } + plan_path = Path(args.plan_file).expanduser() + atomic_write_json(plan_path, plan) + print( + json.dumps( + { + "planFile": str(plan_path), + "channel": args.channel, + "accounts": redacted_accounts(accounts), + "bindings": bindings, + }, + ensure_ascii=False, + indent=2, + ) + ) + + +if __name__ == "__main__": + main() diff --git a/crews/main/skills/work-channel-binding/scripts/record-pending-followup.py b/crews/main/skills/work-channel-binding/scripts/record-pending-followup.py new file mode 100755 index 00000000..5eb0b84b --- /dev/null +++ b/crews/main/skills/work-channel-binding/scripts/record-pending-followup.py @@ -0,0 +1,41 @@ +#!/usr/bin/env python3 +import argparse +import json +from datetime import datetime, timedelta, timezone +from pathlib import Path + + +def state_path() -> Path: + return Path.home() / ".openclaw" / "workspace-main" / "pending-followup.json" + + +def atomic_write_json(path: Path, payload: dict) -> None: + tmp = path.with_name(path.name + ".tmp") + tmp.write_text(json.dumps(payload, ensure_ascii=False, indent=2) + "\n", encoding="utf-8") + tmp.replace(path) + + +def main() -> None: + parser = argparse.ArgumentParser(description="Record a pending Main Agent followup.") + parser.add_argument("--reason", default="gateway-restart") + parser.add_argument("--message", default="Gateway 已重启。请发送一条消息测试新的 channel binding 是否生效。") + args = parser.parse_args() + + now = datetime.now(timezone.utc) + payload = { + "version": 1, + "type": "gateway-restart-followup", + "status": "pending", + "reason": args.reason, + "createdAt": now.isoformat(), + "expiresAt": (now + timedelta(days=1)).isoformat(), + "message": args.message, + } + path = state_path() + path.parent.mkdir(parents=True, exist_ok=True) + atomic_write_json(path, payload) + print(json.dumps({"pendingFollowup": str(path), "status": "pending"}, ensure_ascii=False, indent=2)) + + +if __name__ == "__main__": + main() diff --git a/crews/main/skills/work-channel-binding/scripts/restart-gateway-confirmed.sh b/crews/main/skills/work-channel-binding/scripts/restart-gateway-confirmed.sh new file mode 100755 index 00000000..cfb564dc --- /dev/null +++ b/crews/main/skills/work-channel-binding/scripts/restart-gateway-confirmed.sh @@ -0,0 +1,13 @@ +#!/usr/bin/env bash +set -euo pipefail + +if [ "${WISEFLOW_CONFIRM_GATEWAY_RESTART:-}" != "confirmed" ]; then + echo "Refusing to restart Gateway without WISEFLOW_CONFIRM_GATEWAY_RESTART=confirmed" >&2 + exit 2 +fi + +reason="${1:-manual}" +log_dir="${HOME}/.openclaw/workspace-main" +mkdir -p "$log_dir" +printf '%s\t%s\n' "$(date -u +%Y-%m-%dT%H:%M:%SZ)" "$reason" >> "$log_dir/gateway-restart-audit.log" +exec systemctl --user restart openclaw-gateway diff --git a/crews/shared/COMMAND_TIERS.md b/crews/shared/COMMAND_TIERS.md index 73461bb3..5f3070b0 100644 --- a/crews/shared/COMMAND_TIERS.md +++ b/crews/shared/COMMAND_TIERS.md @@ -43,10 +43,10 @@ 白名单命令(由 setup-crew.sh 自动解析为二进制路径写入 exec-approvals): ``` -cat, ls, grep, find, ps, date, echo, pwd, env, which, head, tail, wc, sort, uniq, diff, curl +cat, ls, grep, find, ps, date, echo, pwd, env, which, head, tail, wc, sort, uniq, diff, curl, stat, basename, dirname, realpath, readlink, tr, printf, whoami, uname, du, df, file, ffprobe, fc-list ``` -不在白名单中的命令会被 OpenClaw 自动拒绝。请勿尝试使用 `rm`、`mv`、`cp`、`mkdir`、`chmod` 等修改型命令。 +不在白名单中的命令会被 OpenClaw 自动拒绝。`ffprobe` 仅用于可信本地媒体文件的元数据探测,不用于解析未知来源的大文件或远程 URL。请勿尝试使用 `rm`、`mv`、`cp`、`mkdir`、`chmod` 等修改型命令。 --- diff --git a/scripts/install.sh b/scripts/install.sh index 2255bad3..b8d4da8a 100755 --- a/scripts/install.sh +++ b/scripts/install.sh @@ -30,6 +30,7 @@ MACOS_GATEWAY_ENV="$OPENCLAW_HOME/service-env/ai.openclaw.gateway.env" BUILTIN_CREWS="main hrbp it-engineer" FORCE=false SKIP_CREW=false +SKIP_WEIXIN=false while [ $# -gt 0 ]; do case "$1" in @@ -41,11 +42,16 @@ while [ $# -gt 0 ]; do SKIP_CREW=true shift ;; + --skip-weixin) + SKIP_WEIXIN=true + shift + ;; *) echo "❌ Unknown option: $1" - echo "Usage: $0 [--force] [--skip-crew]" - echo " --force Overwrite existing workspace files (including MEMORY.md)" - echo " --skip-crew Skip setup-crew.sh (only sync addons, no workspace updates)" + echo "Usage: $0 [--force] [--skip-crew] [--skip-weixin]" + echo " --force Overwrite existing workspace files (including MEMORY.md)" + echo " --skip-crew Skip setup-crew.sh (only sync addons, no workspace updates)" + echo " --skip-weixin Skip bundled/online openclaw-weixin plugin installation" exit 1 ;; esac @@ -91,6 +97,100 @@ ensure_openclaw_config() { fi } +install_weixin_channel() { + if [ "$SKIP_WEIXIN" = "true" ] || [ "${WISEFLOW_SKIP_WEIXIN_INSTALL:-}" = "true" ]; then + echo "⏭️ Skipping openclaw-weixin plugin installation" + return 0 + fi + + echo "💬 Ensuring WeChat channel plugin (openclaw-weixin)..." + local pin_file="$PROJECT_ROOT/openclaw-weixin.version.json" + local plugin_pkg="@tencent-weixin/openclaw-weixin" + local cli_pkg="@tencent-weixin/openclaw-weixin-cli" + local plugin_version="" + local cli_version="" + local plugin_integrity="" + local vendor_dir="$PROJECT_ROOT/vendor/openclaw-plugins" + local plugin_tgz="" + + if [ -f "$pin_file" ]; then + pin_values="$(node -e ' + const fs = require("fs"); + const p = process.argv[1]; + const c = JSON.parse(fs.readFileSync(p, "utf8")); + const plugin = c["openclaw-weixin"] || {}; + const cli = c["openclaw-weixin-cli"] || {}; + const validPackage = /^@[a-z0-9._-]+\/[a-z0-9._-]+$/; + const validVersion = /^\d+\.\d+\.\d+(?:[-+][a-zA-Z0-9._-]+)?$/; + const validIntegrity = /^sha512-[A-Za-z0-9+/]+={0,2}$/; + for (const [label, entry] of [["plugin", plugin], ["cli", cli]]) { + if (!validPackage.test(entry.package || "")) throw new Error(`${label}.package invalid`); + if (!validVersion.test(entry.version || "")) throw new Error(`${label}.version invalid`); + if (!validIntegrity.test(entry.integrity || "")) throw new Error(`${label}.integrity invalid`); + } + console.log([plugin.package, plugin.version, plugin.integrity, cli.package, cli.version].join("\t")); + ' "$pin_file")" || { echo "❌ Invalid openclaw-weixin.version.json"; exit 1; } + IFS=$'\t' read -r plugin_pkg plugin_version plugin_integrity cli_pkg cli_version <<< "$pin_values" + fi + + if [ -n "$plugin_version" ] && [ -d "$vendor_dir" ]; then + plugin_tgz="$(find "$vendor_dir" -maxdepth 1 -type f \( -name "openclaw-weixin-${plugin_version}.tgz" -o -name "tencent-weixin-openclaw-weixin-${plugin_version}.tgz" \) -print -quit 2>/dev/null || true)" + fi + + if [ -n "$plugin_tgz" ]; then + if [ -n "$plugin_integrity" ]; then + node -e ' + const fs = require("fs"); + const crypto = require("crypto"); + const file = process.argv[1]; + const expected = process.argv[2]; + const actual = "sha512-" + crypto.createHash("sha512").update(fs.readFileSync(file)).digest("base64"); + if (actual !== expected) { + console.error(`integrity mismatch for ${file}`); + process.exit(1); + } + ' "$plugin_tgz" "$plugin_integrity" + fi + if (cd "$OPENCLAW_DIR" && pnpm openclaw plugins install "$plugin_tgz"); then + echo " ✅ bundled openclaw-weixin installed" + else + echo "❌ Bundled openclaw-weixin install failed" + echo " Re-run with --skip-weixin only if you intentionally want to configure the onboarding channel later." + exit 1 + fi + elif [ -n "$cli_version" ]; then + if ! npx -y "${cli_pkg}@${cli_version}" install; then + echo "❌ openclaw-weixin online install failed" + echo " Fix network/npm access or re-run with --skip-weixin to configure the onboarding channel later." + exit 1 + fi + else + echo "❌ openclaw-weixin CLI version is missing; refusing unpinned online install" + exit 1 + fi + + node -e ' + const fs = require("fs"); + const p = process.argv[1]; + const c = JSON.parse(fs.readFileSync(p, "utf8")); + c.plugins = c.plugins || {}; + c.plugins.entries = c.plugins.entries || {}; + c.plugins.entries["openclaw-weixin"] = { ...(c.plugins.entries["openclaw-weixin"] || {}), enabled: true }; + c.channels = c.channels || {}; + c.channels["openclaw-weixin"] = { ...(c.channels["openclaw-weixin"] || {}), enabled: true }; + c.session = { ...(c.session || {}), dmScope: "per-channel-peer" }; + if (!Array.isArray(c.bindings)) c.bindings = []; + const hasMainWeixin = c.bindings.some((binding) => binding?.agentId === "main" && binding?.match?.channel === "openclaw-weixin"); + if (!hasMainWeixin) { + c.bindings.push({ agentId: "main", comment: "openclaw-weixin -> Main Agent onboarding entry", match: { channel: "openclaw-weixin" } }); + } + fs.writeFileSync(p, JSON.stringify(c, null, 2) + "\n"); + ' "$OPENCLAW_CONFIG_PATH" + + echo " ℹ️ After Gateway starts, login with: openclaw channels login --channel openclaw-weixin" + echo " ℹ️ Then approve the sender with: openclaw pairing list openclaw-weixin && openclaw pairing approve openclaw-weixin " +} + # ─── 2. 配置 git remote ──────────────────────────────────────── if [ ! -d "$PROJECT_ROOT/.git" ]; then echo "📦 Initializing git repository..." @@ -221,6 +321,9 @@ echo "🎨 Building Control UI assets..." echo " ✅ UI build complete" echo "" +install_weixin_channel +echo "" + # ─── 8. 环境变量收集 + daemon 安装 ───────────────────────── # 需要询问用户的 API Key(若已在目标文件中存在则跳过) diff --git a/scripts/lib/agent-skills.sh b/scripts/lib/agent-skills.sh index 0f327bc5..12f6dc88 100644 --- a/scripts/lib/agent-skills.sh +++ b/scripts/lib/agent-skills.sh @@ -447,7 +447,7 @@ inject_feishu_media_guide() { ## 发送图片/文件/视频等富媒体(自动注入) -向用户发送图片、文件、视频或其他富媒体内容时,不要在本地打开媒体文件,**必须将文件本体通过对话所在的 channel 直接发送到聊天中**,且不得直接输出文件路径或 base64 内容作为回复。 +向用户发送图片、文件、视频或其他富媒体内容时,不要在本地打开媒体文件,也不得直接输出文件路径或 base64 内容作为回复。**必须将文件本体通过媒体发送插件直接发送到聊天中,且需要提供绝对路径**。 GUIDE } diff --git a/scripts/lib/exec-tiers.sh b/scripts/lib/exec-tiers.sh index 1bf0a7eb..637d81d6 100644 --- a/scripts/lib/exec-tiers.sh +++ b/scripts/lib/exec-tiers.sh @@ -9,11 +9,11 @@ # ── 各 Tier 基线命令 ────────────────────────────────── # T0: 无命令(deny) # T1: 只读型系统命令 -TIER_T1_COMMANDS="cat ls grep find ps date echo pwd env which head tail wc sort uniq diff curl stat basename dirname realpath readlink tr printf whoami uname du df file" +TIER_T1_COMMANDS="cat ls grep find ps date echo pwd env which head tail wc sort uniq diff curl stat basename dirname realpath readlink tr printf whoami uname du df file ffprobe fc-list fc-match sed true false" # T2: T1 + 开发工具链 # 注意:bash/sh 的全部常见路径均需列出(/bin 和 /usr/bin 在部分系统上是独立路径, # exec 白名单做精确路径匹配,仅靠 command -v 只能解析到其中一条,须全量覆盖) -TIER_T2_EXTRA="git npm pnpm bun node python python3 pip pip3 cp mv mkdir rmdir rm touch chmod sleep test /bin/bash /bin/sh /usr/bin/bash /usr/bin/sh" +TIER_T2_EXTRA="git npm npx pnpm bun node python python3 pip pip3 ffmpeg perl cp mv mkdir rmdir rm touch chmod sleep test /bin/bash /bin/sh /usr/bin/bash /usr/bin/sh" # T3: full access(无需白名单) # ── 从 SOUL.md 解析 command-tier ────────────────────── diff --git a/scripts/setup-crew.sh b/scripts/setup-crew.sh index 65b001c6..be0dca05 100755 --- a/scripts/setup-crew.sh +++ b/scripts/setup-crew.sh @@ -15,7 +15,7 @@ FORCE=false # 内置 Crew 列表(全局唯一,不可删除,不可多实例) # 这些都是对内 Crew(crew-type: internal) BUILTIN_CREWS="main hrbp it-engineer" -SYNC_TEAM_DIRECTORY_SCRIPT="$CREWS_DIR/hrbp/skills/hrbp-common/scripts/sync-team-directory.sh" +SYNC_TEAM_DIRECTORY_SCRIPT="$CREWS_DIR/main/skills/crew-list/scripts/sync-team-directory.sh" source "$SCRIPT_DIR/lib/agent-skills.sh" source "$SCRIPT_DIR/lib/exec-tiers.sh" @@ -391,10 +391,8 @@ if [ -f "$CONFIG_PATH" ]; then " && echo " ✅ Agent workspace paths normalized" MAIN_OVERRIDE="$(resolve_denied_override_for_agent "main")" - HRBP_OVERRIDE="$(resolve_denied_override_for_agent "hrbp")" IT_OVERRIDE="$(resolve_denied_override_for_agent "it-engineer")" MAIN_BUILTIN_FILE="$(resolve_builtin_file_for_agent "main" "$OPENCLAW_HOME/workspace-main")" - HRBP_BUILTIN_FILE="$(resolve_builtin_file_for_agent "hrbp" "$OPENCLAW_HOME/workspace-hrbp")" IT_BUILTIN_FILE="$(resolve_builtin_file_for_agent "it-engineer" "$OPENCLAW_HOME/workspace-it-engineer")" MAIN_SKILLS_RESULT="$(resolve_agent_skills_json \ @@ -406,15 +404,6 @@ if [ -f "$CONFIG_PATH" ]; then "$OPENCLAW_HOME/workspace-main/DENIED_SKILLS" \ "$PROJECT_ROOT" \ "$OPENCLAW_HOME")" - HRBP_SKILLS_RESULT="$(resolve_agent_skills_json \ - "hrbp" \ - "$OPENCLAW_HOME/workspace-hrbp" \ - "" \ - "$HRBP_BUILTIN_FILE" \ - "$HRBP_OVERRIDE" \ - "$OPENCLAW_HOME/workspace-hrbp/DENIED_SKILLS" \ - "$PROJECT_ROOT" \ - "$OPENCLAW_HOME")" IT_SKILLS_RESULT="$(resolve_agent_skills_json \ "it-engineer" \ "$OPENCLAW_HOME/workspace-it-engineer" \ @@ -425,7 +414,7 @@ if [ -f "$CONFIG_PATH" ]; then "$PROJECT_ROOT" \ "$OPENCLAW_HOME")" - MAIN_SKILLS_RESULT="$MAIN_SKILLS_RESULT" HRBP_SKILLS_RESULT="$HRBP_SKILLS_RESULT" IT_SKILLS_RESULT="$IT_SKILLS_RESULT" OPENCLAW_HOME="$OPENCLAW_HOME" node -e " + MAIN_SKILLS_RESULT="$MAIN_SKILLS_RESULT" IT_SKILLS_RESULT="$IT_SKILLS_RESULT" OPENCLAW_HOME="$OPENCLAW_HOME" node -e " const fs = require('fs'); const path = require('path'); const c = JSON.parse(fs.readFileSync('$CONFIG_PATH', 'utf8')); @@ -489,23 +478,6 @@ if [ -f "$CONFIG_PATH" ]; then return applySkills(base, process.env.MAIN_SKILLS_RESULT); }); - upsertAgent('hrbp', (prev) => { - // hrbp 可 spawn it-engineer 协助执行运维类任务 - const base = { - ...prev, - id: 'hrbp', - name: prev.name || 'HRBP', - workspace: prev.workspace || openclawHome + '/workspace-hrbp', - thinkingDefault: 'high', - reasoningDefault: 'off', - subagents: { - ...(prev.subagents || {}), - allowAgents: ['it-engineer'], - }, - }; - return applySkills(base, process.env.HRBP_SKILLS_RESULT); - }); - upsertAgent('it-engineer', (prev) => { const base = { ...prev, @@ -539,13 +511,29 @@ if [ -f "$CONFIG_PATH" ]; then } } - // 配置飞书多账户 -> Agent 绑定(模式 B:渠道直连) - if (!Array.isArray(c.bindings) || c.bindings.length === 0) { - c.bindings = [ - { agentId: 'main', comment: 'main-bot -> Main Agent', match: { channel: 'feishu', accountId: 'main-bot' } }, - { agentId: 'hrbp', comment: 'hrbp-bot -> HRBP Agent', match: { channel: 'feishu', accountId: 'hrbp-bot' } }, - { agentId: 'it-engineer', comment: 'it-engineer-bot -> IT Engineer Agent', match: { channel: 'feishu', accountId: 'it-engineer-bot' } } - ]; + // 为所有 Agent 追加自身 ID 到 allowAgents,确保显式 self-spawn 合法 + // (隐式 self-spawn 不传 agentId 时本就放行,但 agent 常显式传入自身 ID, + // 此时必须通过 allowlist 检查,否则会被拒绝) + for (const agent of c.agents.list) { + if (!agent.id) continue; + const prevAllow = Array.isArray(agent.subagents?.allowAgents) ? agent.subagents.allowAgents : []; + if (!prevAllow.includes(agent.id)) { + agent.subagents = { + ...(agent.subagents || {}), + allowAgents: [...new Set([...prevAllow, agent.id])], + }; + } + } + + // 默认确保微信 onboarding 入口存在;工作 channel 由 main agent 后续引导绑定。 + if (!Array.isArray(c.bindings)) c.bindings = []; + const hasMainWeixin = c.bindings.some((binding) => + binding?.agentId === 'main' && binding?.match?.channel === 'openclaw-weixin' + ); + if (!hasMainWeixin) { + c.bindings.push( + { agentId: 'main', comment: 'openclaw-weixin -> Main Agent onboarding entry', match: { channel: 'openclaw-weixin' } } + ); } fs.writeFileSync('$CONFIG_PATH', JSON.stringify(c, null, 2) + '\\n'); @@ -623,10 +611,16 @@ if [ -f "$CONFIG_PATH" ]; then if [ ! -f "$local_ac" ]; then printf '# Auto-generated by setup-crew.sh — skill script allowlist\n' > "$local_ac" fi - if ! grep -qxF "$entry" "$local_ac" 2>/dev/null; then - printf '%s\n' "$entry" >> "$local_ac" - added_count=$((added_count + 1)) + # 幂等追加:已有 +entry 则跳过;若有同名 -entry 否决条目也跳过 + entry_cmd="${entry#+}" + if grep -qxF "$entry" "$local_ac" 2>/dev/null; then + continue + fi + if grep -qxF "-${entry_cmd}" "$local_ac" 2>/dev/null; then + continue fi + printf '%s\n' "$entry" >> "$local_ac" + added_count=$((added_count + 1)) done <<< "$script_entries" [ "$added_count" -gt 0 ] && echo " ✅ $a_id: +${added_count} script entries injected" @@ -783,6 +777,7 @@ ENVEOF fi } +generate_ofb_env_md "$OPENCLAW_HOME/workspace-main" "main" generate_ofb_env_md "$OPENCLAW_HOME/workspace-it-engineer" "it-engineer" generate_ofb_env_md "$OPENCLAW_HOME/workspace-hrbp" "hrbp" @@ -793,6 +788,7 @@ if [ "$(uname -s)" = "Darwin" ]; then else _OFB_ENV_FILE="$HOME/.openclaw/daemon.env" fi +inject_env_file_guide "$OPENCLAW_HOME/workspace-main/TOOLS.md" "$_OFB_ENV_FILE" inject_env_file_guide "$OPENCLAW_HOME/workspace-it-engineer/TOOLS.md" "$_OFB_ENV_FILE" inject_env_file_guide "$OPENCLAW_HOME/workspace-hrbp/TOOLS.md" "$_OFB_ENV_FILE" @@ -806,6 +802,7 @@ copy_project_readme() { fi } +copy_project_readme "$OPENCLAW_HOME/workspace-main" "main" copy_project_readme "$OPENCLAW_HOME/workspace-it-engineer" "it-engineer" copy_project_readme "$OPENCLAW_HOME/workspace-hrbp" "hrbp" From f55fdb1dc38ec7da3855cdb03fc7c83b2d253980 Mon Sep 17 00:00:00 2001 From: bigbrother666sh Date: Thu, 4 Jun 2026 23:37:47 +0800 Subject: [PATCH 4/4] update to 5.5.0, detail see CHANGELOG --- CHANGELOG.md | 68 +- README.md | 174 ++- addons/officials/addon.json | 4 +- .../crew/business-developer/AGENTS.md | 6 - .../crew/business-developer/BOOTSTRAP.md | 47 +- .../crew/business-developer/BUILTIN_SKILLS | 1 + .../crew/business-developer/HEARTBEAT.md | 30 +- .../crew/business-developer/MEMORY.md | 10 +- .../officials/crew/business-developer/USER.md | 4 +- .../openclaw_setting_sample.json | 1 + .../skills/bd-record/SKILL.md | 12 +- .../skills/comment-engagement/SKILL.md | 6 +- .../skills/info-record/SKILL.md | 10 +- .../skills/intel-gathering/SKILL.md | 10 +- .../skills/lead-hunting/SKILL.md | 10 +- addons/officials/crew/designer/AGENTS.md | 197 +-- .../officials/crew/designer/ALLOWED_COMMANDS | 13 + addons/officials/crew/designer/BOOTSTRAP.md | 49 +- addons/officials/crew/designer/DENIED_SKILLS | 7 +- addons/officials/crew/designer/HEARTBEAT.md | 10 +- addons/officials/crew/designer/IDENTITY.md | 4 +- addons/officials/crew/designer/MEMORY.md | 19 +- addons/officials/crew/designer/SOUL.md | 46 +- addons/officials/crew/designer/TOOLS.md | 37 +- addons/officials/crew/designer/USER.md | 5 +- .../designer/openclaw_setting_sample.json | 3 +- .../skills/design-system-picker/SKILL.md | 112 ++ .../design-systems/airbnb.md | 165 +++ .../design-systems/apple.md | 352 +++++ .../design-systems/bmw.md | 331 +++++ .../design-systems/figma.md | 270 ++++ .../design-systems/framer.md | 157 +++ .../design-systems/ibm.md | 419 ++++++ .../design-systems/index.json | 167 +++ .../design-systems/linear.md | 324 +++++ .../design-systems/notion.md | 448 ++++++ .../design-systems/shopify.md | 451 ++++++ .../design-systems/spotify.md | 294 ++++ .../design-systems/starbucks.md | 342 +++++ .../design-systems/stripe.md | 457 ++++++ .../design-systems/supabase.md | 407 ++++++ .../design-systems/tesla.md | 138 ++ .../design-systems/vercel.md | 392 ++++++ .../design-system-picker/scripts/pick.sh | 75 + .../designer/skills/init-workspace/SKILL.md | 4 +- .../skills/init-workspace/scripts/init.sh | 22 +- addons/officials/crew/ir/AGENTS.md | 204 +-- addons/officials/crew/ir/ALLOWED_COMMANDS | 6 +- addons/officials/crew/ir/BOOTSTRAP.md | 45 +- addons/officials/crew/ir/BUILTIN_SKILLS | 6 +- addons/officials/crew/ir/HEARTBEAT.md | 30 +- addons/officials/crew/ir/IDENTITY.md | 4 +- addons/officials/crew/ir/MEMORY.md | 43 +- addons/officials/crew/ir/SOUL.md | 29 +- addons/officials/crew/ir/TOOLS.md | 41 +- addons/officials/crew/ir/USER.md | 8 +- .../crew/ir/openclaw_setting_sample.json | 5 +- .../crew/ir/skills/investor-hunting/SKILL.md | 114 ++ .../ir/skills/investor-materials/SKILL.md | 1 + .../crew/ir/skills/investor-outreach/SKILL.md | 1 + .../crew/ir/skills/ir-record/SKILL.md | 161 ++- .../ir-record/scripts/check-application.sh | 45 + .../ir/skills/ir-record/scripts/init-db.sh | 15 + .../ir-record/scripts/query-applications.sh | 64 + .../ir-record/scripts/record-application.sh | 62 + .../crew/ir/skills/market-research/SKILL.md | 1 + addons/officials/crew/sales-cs/AGENTS.md | 6 +- addons/officials/crew/sales-cs/BOOTSTRAP.md | 52 +- addons/officials/crew/sales-cs/DENIED_SKILLS | 19 - addons/officials/crew/sales-cs/HEARTBEAT.md | 8 +- addons/officials/crew/sales-cs/MEMORY.md | 6 +- addons/officials/crew/sales-cs/SOUL.md | 16 +- addons/officials/crew/sales-cs/USER.md | 1 - .../officials/crew/sales-cs/feedback/.gitkeep | 0 .../crew/sales-cs/skills/customer-db/SKILL.md | 14 +- .../skills/customer-db/scripts/cs-update.sh | 0 .../scripts/follow-up-cancel-pending.sh | 0 .../customer-db/scripts/follow-up-complete.sh | 0 .../customer-db/scripts/follow-up-create.sh | 0 .../customer-db/scripts/follow-up-due.sh | 0 .../customer-db/scripts/follow-up-expire.sh | 0 .../scripts/follow-up-mark-sent.sh | 0 .../crew/sales-cs/skills/exp_invite/SKILL.md | 2 +- .../skills/exp_invite/scripts/invite.sh | 8 +- .../sales-cs/skills/proactive-send/SKILL.md | 5 +- .../skills/proactive-send/scripts/send.sh | 2 +- .../crew/selfmedia-operator/AGENTS.md | 117 +- .../crew/selfmedia-operator/ALLOWED_COMMANDS | 13 +- .../crew/selfmedia-operator/BUILTIN_SKILLS | 20 +- .../crew/selfmedia-operator/DENIED_SKILLS | 5 +- .../crew/selfmedia-operator/HEARTBEAT.md | 36 +- .../crew/selfmedia-operator/MEMORY.md | 7 +- .../officials/crew/selfmedia-operator/SOUL.md | 2 + .../openclaw_setting_sample.json | 21 +- .../skills/douyin-publish/SKILL.md | 106 ++ .../douyin-publish/scripts/publish_douyin.py | 206 +++ .../skills/facebook-publish/SKILL.md | 125 ++ .../scripts/publish_facebook.py | 168 +++ .../skills/highlight-clipper/SKILL.md | 135 ++ .../skills/highlight-clipper/scripts/clip.py | 441 ++++++ .../skills/instagram-post/SKILL.md | 119 -- .../skills/instagram-publish/SKILL.md | 114 ++ .../scripts/publish_instagram.py | 136 ++ .../skills/juejin-publish/SKILL.md | 19 + .../skills/pinterest-publish/SKILL.md | 106 ++ .../scripts/publish_pinterest.py | 115 ++ .../skills/published-track/SKILL.md | 142 ++ .../scripts/check-published.sh | 41 + .../skills/published-track/scripts/init-db.sh | 301 ++++ .../skills/published-track/scripts/query.sh | 91 ++ .../skills/published-track/scripts/record.sh | 66 + .../published-track/scripts/update-metrics.sh | 84 ++ .../skills/t2video/SKILL.md | 384 ++---- .../skills/t2video/scripts/assemble.py | 269 ++++ .../skills/t2video/scripts/check.py | 472 +++++++ .../skills/t2video/scripts/gen.py | 165 +++ .../skills/t2video/scripts/t2video.py | 1223 ----------------- .../skills/t2video/scripts/tts.py | 457 ++++++ .../skills/threads-publish/SKILL.md | 107 ++ .../scripts/publish_threads.py | 139 ++ .../skills/tiktok-post/SKILL.md | 98 -- .../skills/tiktok-publish/SKILL.md | 111 ++ .../tiktok-publish/scripts/publish_tiktok.py | 172 +++ .../skills/toutiao-publish/SKILL.md | 20 + .../skills/twitter-post/SKILL.md | 19 + .../skills/wx-mp-publisher/SKILL.md | 156 +++ .../skills/wx-mp-publisher/config.json | 12 + .../wx-mp-publisher/scripts/publish-wx-mp.sh | 54 + .../skills/xhs-content-ops/SKILL.md | 32 +- .../skills/xhs-publisher/SKILL.md | 290 ---- .../skills/youtube-publish/SKILL.md | 114 ++ .../scripts/publish_youtube.py | 168 +++ .../skills/youtube-upload/SKILL.md | 67 - addons/officials/requirements.txt | 8 + addons/officials/skills/council/SKILL.md | 189 +++ addons/officials/skills/email-ops/SKILL.md | 20 +- .../officials/skills/pexels-footage/SKILL.md | 15 +- .../pexels-footage/scripts/pexels_search.py | 25 +- addons/officials/skills/pitch-deck/SKILL.md | 15 +- .../officials/skills/pixabay-footage/SKILL.md | 33 +- .../pixabay-footage/scripts/pixabay_search.py | 25 +- addons/officials/skills/ppt-maker/SKILL.md | 16 +- addons/officials/skills/rss-reader/SKILL.md | 13 +- .../skills/siliconflow-img-gen/SKILL.md | 28 +- .../skills/siliconflow-img-gen/scripts/gen.py | 70 +- .../officials/skills/web-form-fill/SKILL.md | 457 ++++++ assets/a_company_in_a_corner.png | Bin 0 -> 6138068 bytes assets/crew-lineup.webp | Bin 173058 -> 0 bytes assets/crews.png | Bin 0 -> 642640 bytes awada/awada-extension/src/reply-dispatcher.ts | 11 + config-templates/openclaw-aihubmix.json | 790 ++++++----- config-templates/openclaw.json | 63 +- crews/_template/BOOTSTRAP.md | 52 +- crews/hrbp/BOOTSTRAP.md | 10 - crews/hrbp/DENIED_SKILLS | 6 +- crews/hrbp/SOUL.md | 10 - crews/hrbp/USER.md | 1 - .../hrbp/skills/hrbp-feedback-review/SKILL.md | 6 +- crews/hrbp/skills/hrbp-list/SKILL.md | 2 +- .../skills/hrbp-list/scripts/list-agents.sh | 2 +- crews/hrbp/skills/hrbp-modify/SKILL.md | 4 +- .../hrbp-modify/scripts/modify-agent.sh | 2 +- crews/hrbp/skills/hrbp-recruit/SKILL.md | 6 +- .../skills/hrbp-recruit/scripts/add-agent.sh | 2 +- crews/hrbp/skills/hrbp-remove/SKILL.md | 2 +- .../hrbp-remove/scripts/remove-agent.sh | 2 +- crews/hrbp/skills/hrbp-usage/SKILL.md | 12 +- .../skills/hrbp-usage/scripts/agent-usage.sh | 14 +- crews/it-engineer/AGENTS.md | 77 +- crews/it-engineer/BOOTSTRAP.md | 10 - crews/it-engineer/BUILTIN_SKILLS | 6 +- crews/it-engineer/DENIED_SKILLS | 9 +- crews/it-engineer/HEARTBEAT.md | 5 +- crews/it-engineer/SOUL.md | 5 - crews/it-engineer/TOOLS.md | 24 +- crews/it-engineer/USER.md | 7 +- .../skills/alicloud-find-skills/SKILL.md | 234 ++++ .../it-engineer/skills/icp-exemption/SKILL.md | 91 ++ .../icp-exemption/scripts/generate_pdf.py | 255 ++++ crews/it-engineer/skills/icp-filing/SKILL.md | 62 + .../skills/icp-filing/scripts/icp.sh | 129 ++ crews/it-engineer/skills/tccli/SKILL.md | 212 +++ crews/main/SOUL.md | 9 - crews/shared/COMMAND_TIERS.md | 5 +- docs/crew-system.md | 2 +- docs/quick_start.md | 207 ++- docs/workspace-bootstrap-files.md | 2 +- openclaw-weixin.version.json | 17 + openclaw.version | 4 +- patches/004-chrome-port-grace-retry.patch | 10 +- patches/005-browser-timeout-env-var.patch | 16 +- patches/README.md | 1 + patches/generate-patch.sh | 0 patches/overrides.sh | 0 scripts/apply-addons.sh | 61 +- scripts/install.sh | 58 +- scripts/lib/agent-skills.sh | 43 +- scripts/lib/exec-tiers.sh | 30 +- scripts/setup-crew.sh | 31 +- skills/complex-task/SKILL.md | 11 +- skills/smart-search/SKILL.md | 39 +- 201 files changed, 14255 insertions(+), 3727 deletions(-) create mode 100644 addons/officials/crew/designer/ALLOWED_COMMANDS create mode 100644 addons/officials/crew/designer/skills/design-system-picker/SKILL.md create mode 100644 addons/officials/crew/designer/skills/design-system-picker/design-systems/airbnb.md create mode 100644 addons/officials/crew/designer/skills/design-system-picker/design-systems/apple.md create mode 100644 addons/officials/crew/designer/skills/design-system-picker/design-systems/bmw.md create mode 100644 addons/officials/crew/designer/skills/design-system-picker/design-systems/figma.md create mode 100644 addons/officials/crew/designer/skills/design-system-picker/design-systems/framer.md create mode 100644 addons/officials/crew/designer/skills/design-system-picker/design-systems/ibm.md create mode 100644 addons/officials/crew/designer/skills/design-system-picker/design-systems/index.json create mode 100644 addons/officials/crew/designer/skills/design-system-picker/design-systems/linear.md create mode 100644 addons/officials/crew/designer/skills/design-system-picker/design-systems/notion.md create mode 100644 addons/officials/crew/designer/skills/design-system-picker/design-systems/shopify.md create mode 100644 addons/officials/crew/designer/skills/design-system-picker/design-systems/spotify.md create mode 100644 addons/officials/crew/designer/skills/design-system-picker/design-systems/starbucks.md create mode 100644 addons/officials/crew/designer/skills/design-system-picker/design-systems/stripe.md create mode 100644 addons/officials/crew/designer/skills/design-system-picker/design-systems/supabase.md create mode 100644 addons/officials/crew/designer/skills/design-system-picker/design-systems/tesla.md create mode 100644 addons/officials/crew/designer/skills/design-system-picker/design-systems/vercel.md create mode 100755 addons/officials/crew/designer/skills/design-system-picker/scripts/pick.sh create mode 100644 addons/officials/crew/ir/skills/investor-hunting/SKILL.md create mode 100755 addons/officials/crew/ir/skills/ir-record/scripts/check-application.sh create mode 100755 addons/officials/crew/ir/skills/ir-record/scripts/query-applications.sh create mode 100755 addons/officials/crew/ir/skills/ir-record/scripts/record-application.sh delete mode 100644 addons/officials/crew/sales-cs/DENIED_SKILLS delete mode 100644 addons/officials/crew/sales-cs/feedback/.gitkeep mode change 100644 => 100755 addons/officials/crew/sales-cs/skills/customer-db/scripts/cs-update.sh mode change 100644 => 100755 addons/officials/crew/sales-cs/skills/customer-db/scripts/follow-up-cancel-pending.sh mode change 100644 => 100755 addons/officials/crew/sales-cs/skills/customer-db/scripts/follow-up-complete.sh mode change 100644 => 100755 addons/officials/crew/sales-cs/skills/customer-db/scripts/follow-up-create.sh mode change 100644 => 100755 addons/officials/crew/sales-cs/skills/customer-db/scripts/follow-up-due.sh mode change 100644 => 100755 addons/officials/crew/sales-cs/skills/customer-db/scripts/follow-up-expire.sh mode change 100644 => 100755 addons/officials/crew/sales-cs/skills/customer-db/scripts/follow-up-mark-sent.sh mode change 100644 => 100755 addons/officials/crew/sales-cs/skills/exp_invite/scripts/invite.sh mode change 100644 => 100755 addons/officials/crew/sales-cs/skills/proactive-send/scripts/send.sh create mode 100644 addons/officials/crew/selfmedia-operator/skills/douyin-publish/SKILL.md create mode 100755 addons/officials/crew/selfmedia-operator/skills/douyin-publish/scripts/publish_douyin.py create mode 100644 addons/officials/crew/selfmedia-operator/skills/facebook-publish/SKILL.md create mode 100755 addons/officials/crew/selfmedia-operator/skills/facebook-publish/scripts/publish_facebook.py create mode 100644 addons/officials/crew/selfmedia-operator/skills/highlight-clipper/SKILL.md create mode 100644 addons/officials/crew/selfmedia-operator/skills/highlight-clipper/scripts/clip.py delete mode 100644 addons/officials/crew/selfmedia-operator/skills/instagram-post/SKILL.md create mode 100644 addons/officials/crew/selfmedia-operator/skills/instagram-publish/SKILL.md create mode 100755 addons/officials/crew/selfmedia-operator/skills/instagram-publish/scripts/publish_instagram.py create mode 100644 addons/officials/crew/selfmedia-operator/skills/pinterest-publish/SKILL.md create mode 100755 addons/officials/crew/selfmedia-operator/skills/pinterest-publish/scripts/publish_pinterest.py create mode 100644 addons/officials/crew/selfmedia-operator/skills/published-track/SKILL.md create mode 100755 addons/officials/crew/selfmedia-operator/skills/published-track/scripts/check-published.sh create mode 100755 addons/officials/crew/selfmedia-operator/skills/published-track/scripts/init-db.sh create mode 100755 addons/officials/crew/selfmedia-operator/skills/published-track/scripts/query.sh create mode 100755 addons/officials/crew/selfmedia-operator/skills/published-track/scripts/record.sh create mode 100755 addons/officials/crew/selfmedia-operator/skills/published-track/scripts/update-metrics.sh create mode 100644 addons/officials/crew/selfmedia-operator/skills/t2video/scripts/assemble.py create mode 100644 addons/officials/crew/selfmedia-operator/skills/t2video/scripts/check.py create mode 100644 addons/officials/crew/selfmedia-operator/skills/t2video/scripts/gen.py delete mode 100644 addons/officials/crew/selfmedia-operator/skills/t2video/scripts/t2video.py create mode 100644 addons/officials/crew/selfmedia-operator/skills/t2video/scripts/tts.py create mode 100644 addons/officials/crew/selfmedia-operator/skills/threads-publish/SKILL.md create mode 100755 addons/officials/crew/selfmedia-operator/skills/threads-publish/scripts/publish_threads.py delete mode 100644 addons/officials/crew/selfmedia-operator/skills/tiktok-post/SKILL.md create mode 100644 addons/officials/crew/selfmedia-operator/skills/tiktok-publish/SKILL.md create mode 100755 addons/officials/crew/selfmedia-operator/skills/tiktok-publish/scripts/publish_tiktok.py create mode 100644 addons/officials/crew/selfmedia-operator/skills/wx-mp-publisher/SKILL.md create mode 100644 addons/officials/crew/selfmedia-operator/skills/wx-mp-publisher/config.json create mode 100755 addons/officials/crew/selfmedia-operator/skills/wx-mp-publisher/scripts/publish-wx-mp.sh delete mode 100644 addons/officials/crew/selfmedia-operator/skills/xhs-publisher/SKILL.md create mode 100644 addons/officials/crew/selfmedia-operator/skills/youtube-publish/SKILL.md create mode 100755 addons/officials/crew/selfmedia-operator/skills/youtube-publish/scripts/publish_youtube.py delete mode 100644 addons/officials/crew/selfmedia-operator/skills/youtube-upload/SKILL.md create mode 100644 addons/officials/skills/council/SKILL.md create mode 100644 addons/officials/skills/web-form-fill/SKILL.md create mode 100644 assets/a_company_in_a_corner.png delete mode 100644 assets/crew-lineup.webp create mode 100644 assets/crews.png delete mode 100644 crews/hrbp/BOOTSTRAP.md delete mode 100644 crews/it-engineer/BOOTSTRAP.md create mode 100644 crews/it-engineer/skills/alicloud-find-skills/SKILL.md create mode 100644 crews/it-engineer/skills/icp-exemption/SKILL.md create mode 100644 crews/it-engineer/skills/icp-exemption/scripts/generate_pdf.py create mode 100644 crews/it-engineer/skills/icp-filing/SKILL.md create mode 100644 crews/it-engineer/skills/icp-filing/scripts/icp.sh create mode 100644 crews/it-engineer/skills/tccli/SKILL.md create mode 100644 openclaw-weixin.version.json mode change 100755 => 100644 patches/generate-patch.sh mode change 100755 => 100644 patches/overrides.sh diff --git a/CHANGELOG.md b/CHANGELOG.md index 2228ea05..99947cd7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,69 @@ +# v5.5.0 + +### 完全重新设计的部署与渠道绑定流程 + +- 初次安装时默认安装官方微信插件,用户使用个人微信扫码后即可启动第一个crew——main agent,直接在微信上就能使用 +- 后续的设定、更多crew的启用、渠道绑定等均可通过main agent完成(直接在微信上与它对话) +- 如果仅需要一个“个人助理”,或者不需要更大的AI crew团队(crew数量小于3),可以一直使用微信渠道,无需额外的操作 +- 工作渠道除支持飞书外,额外增加企业微信 +- 新增 WeCom channel 插件自动安装脚本(`install-wecom-channel.sh`),支持 pin 版本 + SHA-512 完整性校验,Main Agent 直接执行无需用户手动运行 `npx` +- main agent可以完整操作feishu、企业微信的配置(应用创建、凭据获取、权限配置、事件订阅、crew绑定全流程) + +### Designer + IT Engineer 全链路升级 + +**Designer 与 IT Engineer 的技能组合现已覆盖网页(官网 / 产品 Landing Page)的完整开发与生命周期管理:** + +> **设计** → **开发**(IT Engineer 通过 coding-agent)→ **部署**(云计算资源)→ **备案**(ICP)→ **SEO** + +#### Designer 升级 + +- Designer 从"配图+海报生成"重新定位为**系统性视觉设计体系构建者**,负责从零构建完整网页、APP 界面、品牌视觉体系 +- 新增 `design-system-picker` 技能:内置 15 套知名品牌设计系统(Stripe / Vercel / Linear / Notion / Apple / Supabase / Shopify / Figma / Spotify / Tesla / Framer / Airbnb / BMW / IBM / Starbucks),覆盖 fintech / devtools / productivity / consumer / luxury / enterprise / ecommerce / creative / media / automotive / lifestyle 全品类 +- 设计系统包含完整的 8 段规范(色彩、字体、组件、布局、层级、响应式等),所有 HTML/CSS 产出严格遵循选定设计系统的 token +- 支持从 [awesome-design-md](https://github.com/VoltAgent/awesome-design-md) 上游仓库查找并导入更多设计系统 +- Designer 改为纯 binding 模式运行,用户直接使用,其他 crew 不再 spawn Designer +- 简单出图需求(视频封面、海报等)统一使用 `siliconflow-img-gen`,无需启动 Designer +- 新增三大工作流:完整网页/落地页设计(A)、APP/产品界面设计(B)、品牌视觉体系构建(C) + +#### IT Engineer 升级 + +- 新增 `seo` 技能:技术 SEO 审计与优化(爬取、索引、结构化数据、Core Web Vitals、关键词映射) +- 新增 `icp-filing` 技能:ICP 备案全流程指导(材料清单、流程步骤、域名查询、备案号生成) +- 新增 `icp-exemption` 技能:Apple 国区 ICP 豁免申请附件 PDF 生成 +- 新增 `tccli` 技能:腾讯云 CLI 速查手册(CVM / Lighthouse / DNSPod / SSL / VPC 等 200+ 服务) +- 新增 `alicloud-find-skills` 技能:阿里云 Agent Skills 搜索、发现与安装 + +### Selfmedia Operator 增强 + +- 新增 `wx-mp-publish` 技能:**支持将微信公众号文章自动排版并直接推送至草稿箱**,实现从内容生产到公众号发布的一站式闭环 +- 新增 `t2video`(简单视频制作)技能:一站式短视频生产,整合 TTS 语音合成 + 素材搜集 + FFmpeg 组装 +- 新增 `highlight-clipper`(高光时刻视频制作)技能:支持从给定视频文件(录屏)中按语音自动剪辑高光时刻短视频 +- 大幅完善多平台发布技能,新增/增强支持:小红书(API 方式)、抖音、B站、快手、YouTube、TikTok、Instagram、Facebook、Threads、Pinterest 等 + +### Officials Addon 新crew + +- **business-developer**(商务拓展)正式发布:4.x版本的功能现在可以完全通过business-developer实现 +- **investor-relationship**(投资人关系)预发布 + +### crew 机制改进 + +- 启用 BOOTSTRAP.md 机制,每个crew根据自己职责设定,在启动初期会主动向用户搜集必要信息,如Selfmedia Operator搜集账号矩阵信息、IR和BD主动搜集公司、产品信息 +- skill 路径解析修复与加载机制优化 + +#### OpenClaw 升级至 v2026.5.28 + +- 基于 OpenClaw v2026.5.28(从 v2026.5.7 升级,跨越 6424 commits) +- Patches 004/005 针对 v2026.5.28 新文件结构重新生成,全部 5 个 patch 验证通过 +- awada extension 适配升级 + +### Bug 修复与改进 + +- `scripts` 脚本中诸多不当处修复与改进 +- `crew-recruit` / `crew-dismiss` SKILL.md 澄清 TEAM_DIRECTORY.md 由脚本内部自动同步,无需 agent 手动更新 +- HRBP `add-agent.sh` 新增 business-context symlink 和 crew MEMORY.md 背景说明自动注入 + +--- + # v5.4.9 ### 升级 openclaw 至 v2026.5.7 @@ -88,7 +154,7 @@ ### 新增 -- **新媒体运营 Crew 模板(selfmedia-operator)**:内置文生图(siliconflow-img-gen)、文生视频(siliconflow-video-gen)技能,提供完整的选题研究→图文输出、草稿扩写→完整文章两套工作流;配图优先策略(用户素材 > 免版权图片 > AI 生成 > 历史复用),素材统一归档至 `campaign_assets/` +- **新媒体运营 Crew 模板(selfmedia-operator)**:内置文生图(siliconflow-img-gen)技能,文生视频(siliconflow-video-gen)已迁移至 video-producer crew;提供完整的选题研究→图文输出、草稿扩写→完整文章两套工作流;配图优先策略(用户素材 > 免版权图片 > AI 生成 > 历史复用),素材统一归档至 `campaign_assets/` - **smart-search 新增平台**:百度贴吧(全局搜索 + 指定吧搜索)、Amazon(含分类/排序过滤),YouTube 新增类型过滤(shorts/video/channel)及"最近1小时"时间过滤 diff --git a/README.md b/README.md index eecf356e..c9c3491c 100644 --- a/README.md +++ b/README.md @@ -1,39 +1,39 @@ # Wiseflow -🚀 **v5.4.9 更新** +🚀 **v5.5.0 更新** -- 升级 openclaw 至 v2026.5.7(近期最稳版本),所有 patch 干净应用 -- install.sh 大幅优化,新用户 onboard 更简单,完整支持 macOS -- 模型方案锁定 DeepSeek + SiliconFlow 最佳实践,内置 config-template -- 修复 v5.4.8 诸多 bug;预发布 business-developer / investor-relations 两个新 crew +- **从安装到出活,全程微信对话完成**:首次部署时自动安装官方微信插件,扫码绑定后,Wiseflow Main Agent 直接在微信上引导你完成业务背景采集、团队组建、渠道配置——不再需要编辑任何配置文件。 +- 如果只是想要一个个人助理、或者使用场景比较简单(crew 数量不超过 3 个),可以一直使用微信渠道,无需额外申请飞书开放平台或企业微信(后续如需扩展,Main Agent 也会给出申请和开通的详细指导) +- **Designer 升级**:从"出图工具"重新定位为**系统性视觉设计体系构建者**,内置 `design-system-picker` 技能,预置 15 套品牌设计系统(覆盖 fintech / devtools / productivity / consumer / luxury / enterprise 等全品类),一键匹配风格后从零构建完整网页、APP 界面、品牌视觉体系 +- **IT Engineer 升级**:新增SEO、ICP备案辅助、云服务资源管理能力,现在 Designer + IT Engineer 的技能组合可覆盖官网 / Landing Page 的完整流程——**设计 → 开发 → 部署(云计算)→ 备案(ICP)→ SEO** +- **Selfmedia Operator 增强**:新增微信公众号文章自动排版并推送至草稿箱、简单短视频制作(t2video)、高光时刻视频剪辑(highlight-clipper),现已支持 15 个国内外主流自媒体平台的发布能力(部分为pro版本提供) +- **商务拓展 + 投资人关系正式发布**:business-developer 继承 4.x 全部核心能力(指定信源监控、行业情报采集、社交媒体潜客挖掘),并新增业务介绍 PPT 制作能力;investor-relationship 预发布 详见 [CHANGELOG.md](CHANGELOG.md) --- -即刻拥有一只 7×24 在线工作的 AI 员工团队,包括: - -幕僚长、HRBP、IT-Engineer、商务拓展、销售型客服、自媒体运营、平面设计师、投资人关系……(不断增加中) +## what's wiseflow - +Wiseflow 是基于 [openclaw](https://github.com/openclaw/openclaw) 的 Multi-Agent 系统,为 **所有被/或即将为 AI 时代冲击、需要独立拓展收入来源的个体** 打造——被裁员/降薪的职场人、副业探索者、自媒体个体户、小生意人、刚毕业的年轻人…… -> 📌 **寻找 4.x 版本?** 原版 v4.32 及之前版本的代码在 [`4.x` 分支](https://github.com/TeamWiseFlow/wiseflow/tree/4.x)中。 +> **对于 99% 的人来说,人工智能技术带来的其实是灾难** +> +> 这不是危言耸听。历史上每一次技术变革——蒸汽机、电力、互联网——无一例外都让会用它的人赚得更多,不会用的人被甩得更远。因为技术本质上是杠杆:有资本、有资源的人能第一时间装备自己,效率翻倍;而普通人连反应的时间都没有,就已经被替代了。AI 时代只会把这个规律放大到极致——99% 的输家,1% 的赢家。这非常不公平、也不合理。然而遗憾的是,这场变革已经无法被停止,那么,我们能做点什么? +> +> 本项目的立意是**为普通人提供一支AI搞钱团队**,以对抗AI技术发展带来的日益严峻的贫富差距。我们号召整个开源社区与我们一起为普通人而战,用技术对抗权贵! -``` -“吾生也有涯,而知也无涯。以有涯随无涯,殆已!“ —— 《庄子·内篇·养生主第三》 -``` +我们不贩卖焦虑,也不承诺捷径。挣钱的本质从来是提供价值——你干了那么多年的事、攒下的经验、对某个领域的判断,那才是真有价值的。问题是:一个人有经验、有方法,但时间和精力终究有限。 -## what's wiseflow +wiseflow 目前能为你提供的是: +- AI自动化获客:**商务拓展** 挖客户 → **自媒体运营** 铺声量 → **销售客服** 促转化 → **HRBP** 调策略 → **IT Engineer** 保运行 +- 业务支撑与保障:**设计师** 搭官网/落地页 → **IT Engineer** 搞 ICP 备案、服务器管理、SEO 优化 -wiseflow 是基于 [openclaw](https://github.com/openclaw/openclaw) 的一套旨在面向真实营业场景的Multi-Agent(多智能体)系统(MAS),支持部署后对外提供 7*24 营业业务,内置自动化网络推广与销售型客服 Agent. + -*wiseflow 又名 openclaw_for_business,简称 ofb* +> 📌 **寻找 4.x 版本?** 原版 v4.32 及之前版本的代码在 [`4.x` 分支](https://github.com/TeamWiseFlow/wiseflow/tree/4.x)中。 -> openclaw很强,能够帮你收发邮件、写报告、控制智能家居……但是讲真,这是你最需要的吗? -> -> 让 AI 帮我们 “搞钱”才是王道! -> -> **本项目的目的不是为你增加一个“个人助理”,而是为你打造一直“云上牛马”团队,可以 7*24 小时给你在线搞钱的那种!** +--- ## 🌟 快速开始 @@ -60,20 +60,45 @@ cd wiseflow - 初始化 `openclaw.json`(内置最佳模型配置,无需手动编辑) - 安装系统 daemon(开机自启 + 崩溃重启) - **交互式引导你输入** `DEEPSEEK_API_KEY` 和 `SILICONFLOW_API_KEY`(仅在首次或缺失时询问) +- 安装腾讯官方 `openclaw-weixin` extension,并引导扫码绑定 > **调试模式**(单次启动,适合测试):`./scripts/dev.sh gateway` > **系统要求**:推荐 Ubuntu 22.04;支持 WSL2 / macOS;不建议 Windows 原生 -🎉 大功告成! +### 3. 微信对话完成 Onboard + +安装完成后,打开微信搜索上一步绑定的机器人,直接发消息即可——它会主动引导你完成首次 onboard**: + +1. 告诉它你的公司/品牌、产品和目标用户 +2. 它会把这些业务背景存入 `business-context/`,后续招募的 crew 自动继承 +3. 按需招募第一个 crew(如商务拓展、自媒体运营) +4. 团队扩大后,一条对话即可配置飞书或企业微信工作 channel + +**不需要编辑配置文件、不需要手动同步信息——从安装到出活,全程对话完成。** + +注:微信官方 openclaw 插件限定一个微信账号只能对应一个机器人,如果您之前已经绑定了其他 Agent(openclaw 或者 hermes 等),这会挤掉已经绑定的 agent。但是在完成 wiseflow 团队配置后,您可以将此 bot 替换回其他 agent,这不影响已经绑定工作渠道的 wiseflow crew team。 -安装后如何用参考 [quick start](docs/quick_start.md)。 +> 💡 更详细的操作指引见 [quick start](docs/quick_start.md) + +### 系统与环境要求 + +| 项目 | 最低要求 | 推荐配置 | +|------|---------|---------| +| CPU | 2 核 | 4 核 | +| 内存 | 8 GB | 16 GB | +| 可用硬盘 | 40 GB | 120 GB | +| 带宽 | 10 Mbps | — | + +- **网络**:需可访问外网;建议使用正常住宅 IP,数据中心 IP 部分平台可能识别限制 +- **部署环境**:支持无头云服务器(ECS)部署,但推荐在有桌面环境的电脑上部署(日常使用中可不插显示器),浏览器自动化类技能在桌面环境下更稳定 +- **操作系统**:推荐 Ubuntu 24.04;支持 Windows WSL2、macOS 15 / 26 > **💡 模型费用说明** > > wiseflow5.x 底层基于 openclaw,Agent 工作流对 token 消耗有一定要求,建议先准备好大模型 API: > -> - **主力模型(强烈推荐)**:[DeepSeek 官方 API](https://platform.deepseek.com/) — 综合性能、速度、性价比最优。高缓存命中机制,实际应用成本可控。需要注册并充值获得 `DEEPSEEK_API_KEY`,大部分任务使用 `deepseek-v4-flash` 足够,IT Engineer 技术排查时使用 `deepseek-v4-pro`。 +> - **主力模型(强烈推荐)**:[DeepSeek 官方 API](https://platform.deepseek.com/) — 综合性能、速度、性价比最优。高缓存命中机制,实际应用成本可控。需要注册并充值获得 `DEEPSEEK_API_KEY`。 > - **替补 & 视觉模型**:[SiliconFlow](https://cloud.siliconflow.cn/i/WNLYbBpi) — 模型丰富,可作为 DeepSeek 的 fallback,同时提供视觉理解模型(`Qwen/Qwen3.6-27B`)和生图/生视频 API。需要注册获得 `SILICONFLOW_API_KEY`。 > > 🎁 以上 SiliconFlow 链接为 wiseflow 邀请链接,通过此链接注册,你和 wiseflow 项目各可获得一张 16 元代金券。 > @@ -81,9 +106,9 @@ cd wiseflow > > 配置模板已预置以上最佳实践,`install.sh` 会自动检测所需环境变量并引导你输入。安装后重启 openclaw gateway 即可生效。 -🎉 wiseflow 目前提供付费知识库,包含《手把手从零开始安装教程》、《安装之后三分钟上手指南》、《Openclaw自定义配置全案教程》、《Windows 下安装 WSL2 无脑教程》以及各种高阶独门秘籍等,年费仅需¥168,还能加入 **vip微信交流群** ,共同探讨交流各种玩法,还有每月一次的闭门分享(腾讯会议),陪伴你从“小白“到“大神“! +🎉 wiseflow 目前提供付费知识库,包含《手把手从零开始安装教程》、《安装之后三分钟上手指南》、《Openclaw自定义配置全案教程》、《Windows 下安装 WSL2 无脑教程》、《秘籍:云服务器(ECS)部署》以及各种最佳实践分享,年费仅需¥168,还能加入 **vip微信交流群** ,共同探讨交流各种玩法,还有每月一次的闭门分享(腾讯会议),陪伴你从“小白“到“大神“! -欢迎添加”掌柜的“企业微信(这背后接的就是 wiseflow)咨询了解: +欢迎添加”掌柜的“企业微信(这背后接的就是 wiseflow sales-cs)咨询了解: wiseflow掌柜 @@ -91,46 +116,60 @@ cd wiseflow ## ✨ 创新点 -原版 openclaw,包括国内各大厂推出的基于 openclaw 的“虾”,它们的定位都是“个人助理”(personal AI assistant),也即适合服务你自己,但并不适合替你服务别人,因此也就没办法替你搞钱。 - -wiseflow 专为打造可在线 7*24 小时搞钱的目的而生,我们在原版的基础上以补丁、配置模板、专属技能等方式(但不改原版一行代码,以保证完全的兼容性)做了如下改进: +Wiseflow 在 openclaw 基础上以addon、配置模板、专属技能等方式(不改上游一行代码,保证完全兼容性)做了如下改进: #### “Crew”的概念 -- Crew 是绑定了专属工作指导和技能组合的Agent - -> 原版openclaw是把所有技能(包括内置和用户自定义安装的)绑定到同一个 Agent 上,Agent Spawn 出的 subagent 也默认继承所有技能。但这会造成两个问题:1、臃肿,代表着每一轮对话都更加耗费 token、模型思考时间也会更长并且更容易出错;2、如果 Agent 是提供对外服务的,那么会很危险,想象外部客户可以通过 Agent 操控你家的的智能家居或者连通你的打印机……当然你可以通过原版的配置禁用这些技能,然而为什么要让一个,比如说客服 Agent 拥有连接智能家居和打印机的技能? - -wiseflow 的做法是提供 `Crew Template`,针对每个 crew 的应用目的(客服、新媒体运营、财务报税)提供专属的 skill(很多是我们定制开发的)和基础的工作指导、人设,并留给用户充分的“调教”空间。 - -- `全局内置 crew` 负责服务你和其他 crew,相当于公司的中后台。这里面有三个是内置且全局唯一的,负责提供最基础的支撑,相当于公司的管理层: - - Main Agent,负责管理所有对内 crew 的生命周期,你也可以把它当成唯一对话入口,通过它喊其他 Crew 干活; - - IT Engineer,负责帮你搞定 openclaw 繁琐的配置,日常运维(升级、定时心跳检查状态)等,**对,你没看错,只要你完成第一次部署,后面它就可以帮你去做系统配置和运维** - - HRBP,负责帮你招募、管理对外服务 crew,还能帮你周期性质的扫描对外服务 crew 的 feedback,不断升级他们…… - - 以上三个内置 crew 我们都已经提供了现成的最佳配置(角色定义文件、SKills、权限等) - -- `addons/officials/crew/` 提供的 crew,是帮你“搞钱”的。 - - `sales customer service`(销售导向客服):不是单纯回答客户咨询的客服,以促进成交为目的,会在咨询答疑过程中以用户无感的巧妙话术促进销售、调研用户来源、记录客户信息,并具有发起收款和确认收款的能力; - - `selfmedia operator`(自媒体运营):不仅仅能够帮你写稿、生图,它能够随时记录你的灵感,你无意中看到的素材也可以随手转发给它,它都会记住并应用在后续产出中,并且它还能**自动完成在各个自媒体平台发布**的工作; - - `designer`(设计师):专注视觉创意设计,结合 AI 生图能力提供配图、海报、品牌素材生成服务; - - `business developer`(商务拓展): 主动出击,每日在各自媒体平台寻找潜在客户并主动勾搭,还能监控竞争对手、采集行业情报、生成业务介绍 ppt 等; - - `Investor Relations`(投资人关系):帮你在线寻找并冷接触投资人、在线填报各种申请表、生成精美的 BP…… - - 更多能够帮你在线搞钱的 crew template 陆续发布中…… - -> 说实话,市面上有很多基于 openclaw 的二开项目都支持多 crew(Agent),甚至还支持让这些 crew(Agent) 自主协同,或者带个办公室界面,你能看到他们在一起“过家家”……我认为这些都太华而不实了! 如果不能搞钱,一个 Agent Team 跟一个 chatbot 一样,只是玩具而已! - -看看基于 wiseflow 免费开源的 sales-cs crew template “调教“出的销售型客服有多智能! - - - -注意,我们没有使用FAQ,也没有workflow,这是纯粹的harness工程。 +原版 OpenClaw 定位是 **personal AI assistant**——个人助理。但个人助理和工作场景是很不一样的:工作场景的技能不要求丰富,但要求稳定、专业;不同的工作要对应不同的定义约束和技能组合,即不同岗位需要使用不同的 harness。 + +Wiseflow 的做法是提供 `Crew Template`,针对每个岗位提供专属 skill 和工作指导,并留给用户充分的调教空间。 + +目前 wiseflow 内置如下 crew,可以按需启用: + +| Crew | 职责 | 关键技能 | +|------|------|---------| +| Main Agent(微信上的那个,默认启用,全局唯一) | 管理所有 crew 生命周期,唯一对话入口 | crew 招募/管理、渠道配置 | +| IT Engineer(默认启用,全局唯一,可协助其他 crew 排障) | 系统运维、配置、故障排查 | seo、icp-filing、icp-exemption、tccli、alicloud-find-skills、session-logs | +| HRBP | 招募管理对外 crew,周期扫描 feedback 升级 | crew-recruit、crew-modify、crew-remove、crew-list、crew-usage | +| 商务拓展 | 客户挖掘端 | 社交媒体潜客挖掘、竞品监控、行业情报、生成业务介绍 PPT | +| 自媒体运营 | 内容生产端 | 写稿、生图、15 个平台自动发布、t2video(短视频)、highlight-clipper(高光剪辑) | +| 设计师 | 视觉设计端 | 15 套品牌设计系统、完整网页/APP/品牌视觉体系构建 | +| 销售型客服 | 获客转化端 | 自动回复促进成交、调研用户来源、记录客户信息、发起/确认收款 | +| 投资人关系 | 融资端 | 寻找投资人、冷接触、填报申请表、生成 BP | +| Video Producer* | 专业视频端 | 专业短视频制作 | + +> *\* 标记的 crew 由 Pro 版本提供* + +
+ 自媒体运营 — 支持的社交媒体平台(15 个) + + | 平台 | 发布方式 | + |------|---------| + | 微信公众号 | API + wenyan-cli 渲染 | + | 企业微信朋友圈* | API | + | 小红书* | API | + | 抖音 | API(OAuth2) | + | B站* | Web API | + | 快手* | Web API | + | 今日头条 | 浏览器 + CDP | + | 掘金 | 浏览器 | + | Twitter/X | 浏览器 | + | YouTube | YouTube Data API v3 | + | TikTok | Content Posting API | + | Instagram | Meta Graph API | + | Facebook | Meta Graph API | + | Threads | Meta Graph API | + | Pinterest | Pinterest API v5 | + + > *\* 标记的平台发布技能由 Pro 版本提供,开源版不包含* + +
有关”多 crew 机制”设计,详见[CREW TYPE DESIGN](docs/crew-system.md) #### Crew 之间的自主协作 -我们巧妙的利用了 OpenClaw 的 Spawn Subagent 机制实现了 crew 之间的自主互助能力,这意味着: +我们巧妙的利用了 OpenClaw 的 Spawn Subagent 机制实现了 crew 之间的自主协作能力,这意味着: Crew 遇到自己不能解决的问题: ```text @@ -166,7 +205,7 @@ Crew 遇到自己不能解决的问题: 原版openclaw的使用和维护并不简单,尤其对于非技术用户而言,充满暗坑,最受诟病的是**安全性**和**安装部署**,为此我们也做了不少改进: -- **安全** +##### 安全 我们采用三重命令执行机制,**权限由 `exec-approvals.json` + `tools.exec` 自动强制执行**,不单单是角色定义中告知。 @@ -176,22 +215,24 @@ Crew 遇到自己不能解决的问题: |------|------|----------|-----------| | T0 | read-only | `security: deny` — 默认禁止所有 shell 命令 | external crews(默认) | | T1 | basic-shell | `security: allowlist` — 仅允许只读命令 | low-risk internal crews | -| T2 | dev-tools | `security: allowlist` — 开发工具链 + 只读命令 | main | -| T3 | admin | `security: full` — 完整系统操作 | it-engineer, hrbp | +| T2 | dev-tools | `security: allowlist` — 开发工具链 + 只读命令 | main, hrbp,selfmedia-operator... | +| T3 | admin | `security: full` — 完整系统操作 | it-engineer | -**易用性脚本** +##### 易用性脚本 - **配置模板** — 预设国内可用的模型、渠道、技能等配置 - **工具脚本** — 一键启动、一键部署、一键更新…… -**🩹 wiseflow 内置补丁与可配置环境变量** +##### wiseflow 内置补丁与可配置环境变量 wiseflow 通过 `patches/` 目录对 openclaw 源码打补丁,每次运行 `apply-addons.sh` 时自动应用。以下是当前生效的补丁及其可配置项: | 补丁 | 说明 | 相关环境变量 | |------|------|-------------| +| `001-relax-exec-allowlist-shell-syntax` | 放宽 exec-approvals 的 shell 语法限制,允许 `$()`、反引号、重定向等常用写法 | 无 | | `002-disable-web-search-env-var` | 支持通过环境变量禁用 openclaw 内置 web search | `OPENCLAW_DISABLE_WEB_SEARCH=1` | | `003-act-field-validation` | 修复浏览器 act 动作的字段验证逻辑 | 无 | +| `004-chrome-port-grace-retry` | Chrome CDP 端口占用时优雅重试,避免因端口冲突导致浏览器启动失败 | 无 | | `005-browser-timeout-env-var` | 支持通过环境变量自定义浏览器操作默认超时(原默认仅 20 秒,网络慢时容易中断) | `OPENCLAW_BROWSER_TIMEOUT_MS=60000` (执行 install.sh 脚本会自动配置)| #### 浏览器增强 @@ -220,12 +261,6 @@ https://github.com/user-attachments/assets/8d097b3b-f9ab-42eb-98bb-88af5d28b089 详见 [awada readme](awada/README.md) -#### Addon 机制与生态 - -wiseflow 不改上游(openclaw)代码,一切改造和增强都通过 `addon` 机制完成,这最大化的保障了兼容性,也即是说:wiseflow 无缝支持**从 clawhub.ai安装技能**。 - -wiseflow 代码仓会不会更新、添加 [official addons](addons/officials), 我们也欢迎社区贡献更多 addon,第三方也可以通过 add-on 向 wiseflow 用户发放 crew template,参见 [Addon 开发](docs/addon_development.md)。 - ## 目录结构 ``` @@ -288,6 +323,7 @@ wiseflow/ - opencli(A CLI for social media & web platforms — smart-search skill 借鉴了其搜索 URL 模式与平台适配方案) https://github.com/jackwener/opencli - 文颜(Markdown文章排版美化工具,支持微信公众号、今日头条、知乎等平台。) https://github.com/caol64/wenyan - Everything Claude Code(Claude Code 全局 skill / rule / agent 集合,wiseflow 的 complex-task 等编排 skill 借鉴了其 blueprint 和 gan-style-harness 的设计思路) https://github.com/affaan-m/everything-claude-code +- awesome-design-md(A curated collection of design systems in markdown format — Designer 内置设计系统库参考了此项目的设计系统结构) https://github.com/VoltAgent/awesome-design-md ## Citation diff --git a/addons/officials/addon.json b/addons/officials/addon.json index 212ce467..1a7a985e 100644 --- a/addons/officials/addon.json +++ b/addons/officials/addon.json @@ -2,8 +2,8 @@ "name": "wiseflow officials", "version": "0.4.1", "description": "官方 Crew 模板(selfmedia-operator / business-developer / designer / ir / sales-cs)+ 专属全局技能(rss-reader / siliconflow-img-gen / pexels-footage / pixabay-footage / connections-optimizer / email-ops / pitch-deck / ppt-maker / social-graph-ranker / xhs-interact / xianyu-ops)", - "openclaw_version": "2026.5.7", - "openclaw_commit": "eeef4864494f859838fec1586bedbab1f8fa5702", + "openclaw_version": "2026.5.28", + "openclaw_commit": "e93216080aa1f425d3ab127014603eba8e365b2d", "auto-activate": false, "internal_crews": ["business-developer", "designer", "ir", "selfmedia-operator"], "external_crews": ["sales-cs"] diff --git a/addons/officials/crew/business-developer/AGENTS.md b/addons/officials/crew/business-developer/AGENTS.md index 5afc3dcb..8d2a9e5d 100644 --- a/addons/officials/crew/business-developer/AGENTS.md +++ b/addons/officials/crew/business-developer/AGENTS.md @@ -240,9 +240,3 @@ ### 人脉优化与社交线索 使用 `connections-optimizer` 和 `social-graph-ranker` 技能,进行人脉分析和社交关系梳理。 - ---- - -## sessions_spawn 规范 - -> 禁止传入 `streamTo` 参数 —— 在 subagent 模式下该参数不支持。spawn 时只传 agentId 和 task 内容即可。 diff --git a/addons/officials/crew/business-developer/BOOTSTRAP.md b/addons/officials/crew/business-developer/BOOTSTRAP.md index 22422155..0a32ed50 100644 --- a/addons/officials/crew/business-developer/BOOTSTRAP.md +++ b/addons/officials/crew/business-developer/BOOTSTRAP.md @@ -1,20 +1,43 @@ -# Bootstrap +# BusinessDeveloper Bootstrap -This is a pre-configured crew workspace. Review these files at startup: +This one-time bootstrap collects the business context before BD work starts. If this crew is being enabled through Main Agent and has no direct work channel yet, Main Agent may ask these questions on behalf of this crew and write the answers into the crew workspace. -- **SOUL.md** — Role definition, core responsibilities, and autonomy level -- **AGENTS.md** — Workflows and operating procedures -- **MEMORY.md** — Background context and historical records -- **IDENTITY.md** — Name and persona -- **USER.md** — Assumptions about who you are serving -- **TOOLS.md** — Available tools, usage rules, and required environment variables +## Step 1: Company & Business Context -## First-Run Checklist +Collect: -On first startup: -1. Check `SILICONFLOW_API_KEY` is set → required for LLM content generation -2. For Cold Outreach: check SMTP env vars are set (`SMTP_SERVER`, `SMTP_USER`, `SMTP_PASSWORD`) +- company/brand name; +- product or service introduction (one-line positioning); +- target customer profile; +- key selling points / differentiators; +- brand tone for outreach communications; +- forbidden claims or sensitive topics to avoid; +- competitors or differentiation notes. + +## Step 2: Outreach Readiness + +Ask: + +- Is SMTP configured for cold email outreach? If not, explain that email outreach mode will be unavailable until SMTP is set up. +- Should the crew start in draft-only mode (collect leads but not contact them) or is direct outreach approved? +- For direct outreach: does the user have existing outreach templates/talking points, or should the crew draft them for review? + +## Step 3: Environment Verification + +On first startup, check and report: + +1. `SILICONFLOW_API_KEY` is set → required for LLM content generation +2. For Cold Outreach: check SMTP env vars (`SMTP_SERVER`, `SMTP_USER`, `SMTP_PASSWORD`) 3. Verify `send_email.py` dependency: `python3 -c "import smtplib; print('ok')"` (built-in, always ok) 4. Create output directories: `mkdir -p outreach_data` If SMTP is not configured, affiliate marketing mode still works fully. + +## Completion + +After bootstrap is complete: + +1. Update `MEMORY.md` with company/business background and SMTP status. +2. Update `USER.md` with organization info (replace `<待填充>` placeholder). +3. Delete `BOOTSTRAP.md` from the runtime workspace. +4. Suggest the next step, such as setting up the first Lead Hunting task. diff --git a/addons/officials/crew/business-developer/BUILTIN_SKILLS b/addons/officials/crew/business-developer/BUILTIN_SKILLS index 81fe392f..7db978e2 100644 --- a/addons/officials/crew/business-developer/BUILTIN_SKILLS +++ b/addons/officials/crew/business-developer/BUILTIN_SKILLS @@ -14,3 +14,4 @@ info-record login-manager wx-mp-hunter pitch-deck +council \ No newline at end of file diff --git a/addons/officials/crew/business-developer/HEARTBEAT.md b/addons/officials/crew/business-developer/HEARTBEAT.md index e2278fd7..312876bd 100644 --- a/addons/officials/crew/business-developer/HEARTBEAT.md +++ b/addons/officials/crew/business-developer/HEARTBEAT.md @@ -2,29 +2,13 @@ ## 执行约束 -1. **无时间限制**:任务执行不受深夜时间限制,必须执行完 HEARTBEAT 清单全部内容 - -2. **遇到技术故障时处理方案**: - -遇到技术故障时,按如下方案处置: - - 先尝试彻底关闭浏览器,再打开; - - 重启浏览器不解决问题时,**spawn IT Engineer**协助解决:调用 `sessions_spawn`,将问题现象、错误信息、当前任务上下文完整传递给 IT Engineer,请它协助解决; - - 仍无法解决 → **跳过当前任务,继续执行后续步骤**,不要卡住整个 HEARTBEAT - -期间不可: - - ❌ 呼唤用户协助解决,HEARTBEAT 在深夜执行,喊用户也没用 - - ❌ 不可中断任务,通过以上三步依然无法进行的任务则跳过,继续执行后续步骤,绝对不允许中断HEARTBEAT! - -3. **Subagent 产出校验与重试**:每次 spawn subagent 后,收到其返回结果时,必须按以下规则判定: - - - ❌ **无效产出**:返回内容为空、仅包含错误信息、不包含任何实质性产出(如文章正文、发布确认等)、或明确表示任务未完成 - - ✅ **有效产出**:包含预期的实质内容(如完整的文章 markdown、发布成功的确认信息等) - - 若判定为无效产出,**立即重新 spawn 同一个 subagent**(使用相同的 agentId 和 task),最多重试 3 次。每次重试前检查是否因浏览器问题导致(如超时、连接断开),若是则先彻底关闭浏览器再重试。 - - 3次均失败后,**跳过当前步骤,继续后续流程**,返回最终报告「XX 步骤:重试 3 次均失败,原因:xxx」。 - -4. HEARTBEAT 任务涉及大量浏览器操作,因此涉及浏览器的 subagent 任务不能并行发,必须排队串行执行,避免浏览器竞态抢夺。 +1. **无时间限制**:HEARTBEAT/cron 触发后必须执行完清单全部内容 +2. **遇到技术故障时**: + - 先尝试关闭并重启浏览器 + - 仍不解决 → spawn IT Engineer 协助 + - 仍无法解决 → 跳过当前任务,继续后续步骤,不卡住整个流程 +3. **不可呼唤用户协助**(定时任务可能深夜执行) +4. **浏览器操作必须串行**,不可并行,避免竞态抢夺 --- diff --git a/addons/officials/crew/business-developer/MEMORY.md b/addons/officials/crew/business-developer/MEMORY.md index fe37c102..d4866e6a 100644 --- a/addons/officials/crew/business-developer/MEMORY.md +++ b/addons/officials/crew/business-developer/MEMORY.md @@ -1,12 +1,10 @@ # BusinessDeveloper — Memory -## 用户偏好与配置 +## 公司与业务背景信息 -(首次使用后由 Business Developer 在此记录) + -- 公司/组织信息:(待记录) -- 产品/业务介绍:(待记录) -- 默认联系信息:(待记录) +--- ## 模式一:Lead Hunting 历史 @@ -42,5 +40,5 @@ ## 技术环境备注 -- SMTP 配置状态:(待记录) +- SMTP 配置状态:(由 BOOTSTRAP 检测写入) - 数据库位置:`./db/bd_record.db`、`./db/info_record.db` diff --git a/addons/officials/crew/business-developer/USER.md b/addons/officials/crew/business-developer/USER.md index 62b22263..93d31ea2 100644 --- a/addons/officials/crew/business-developer/USER.md +++ b/addons/officials/crew/business-developer/USER.md @@ -6,7 +6,7 @@ - 用户身份:公司决策者 / 业务负责人 - 你的角色:他的业务拓展执行手——他说要开拓哪个方向,你就代表公司去执行 -- 组织信息(公司名称、业务介绍、联系方式等)<待填充> +- 组织信息(公司名称、业务介绍、联系方式等)<由 BOOTSTRAP 收集填充> ## What They Expect @@ -20,4 +20,4 @@ - 初始化对话时主动引导用户完整表达需求 - 分析结果(关键词、判定标准、话术)结构化输出,方便用户审阅 - 定时任务执行后汇总报告,不实时发送进度 -- 遇到障碍及时告知用户 +- 遇到障碍及时告知用户 \ No newline at end of file diff --git a/addons/officials/crew/business-developer/openclaw_setting_sample.json b/addons/officials/crew/business-developer/openclaw_setting_sample.json index 792a495d..39d865d3 100644 --- a/addons/officials/crew/business-developer/openclaw_setting_sample.json +++ b/addons/officials/crew/business-developer/openclaw_setting_sample.json @@ -5,6 +5,7 @@ "pitch-deck", "social-graph-ranker", "smart-search", + "council", "browser-guide", "rss-reader", "xhs-interact", diff --git a/addons/officials/crew/business-developer/skills/bd-record/SKILL.md b/addons/officials/crew/business-developer/skills/bd-record/SKILL.md index 25af02d7..b3b2bd08 100644 --- a/addons/officials/crew/business-developer/skills/bd-record/SKILL.md +++ b/addons/officials/crew/business-developer/skills/bd-record/SKILL.md @@ -13,7 +13,7 @@ description: 维护 business-developer 的 SQLite 追踪数据库,记录已探 ./db/bd_record.db ``` -首次使用需先初始化:`bash ./skills/bd-record/scripts/init-db.sh` +初始化(幂等,可重复执行):`./skills/bd-record/scripts/init-db.sh` --- @@ -55,20 +55,20 @@ description: 维护 business-developer 的 SQLite 追踪数据库,记录已探 ### 初始化数据库 ```bash -bash ./skills/bd-record/scripts/init-db.sh +./skills/bd-record/scripts/init-db.sh ``` ### 模式一:创作者记录 **检查创作者是否已记录**: ```bash -bash ./skills/bd-record/scripts/check-creator.sh --platform <平台> --creator-id <创作者ID> +./skills/bd-record/scripts/check-creator.sh --platform <平台> --creator-id <创作者ID> ``` 返回 JSON:`{"exists": true/false}` **记录创作者**: ```bash -bash ./skills/bd-record/scripts/record-creator.sh \ +./skills/bd-record/scripts/record-creator.sh \ --platform <平台> \ --creator-id <创作者ID> \ --nickname <昵称> \ @@ -82,13 +82,13 @@ bash ./skills/bd-record/scripts/record-creator.sh \ **检查帖子是否已互动**: ```bash -bash ./skills/bd-record/scripts/check-post.sh --platform <平台> --post-url <帖子URL> +./skills/bd-record/scripts/check-post.sh --platform <平台> --post-url <帖子URL> ``` 返回 JSON:`{"exists": true/false, "replied": true/false}` **记录互动**: ```bash -bash ./skills/bd-record/scripts/record-post.sh \ +./skills/bd-record/scripts/record-post.sh \ --platform <平台> \ --post-title <标题> \ --post-url <帖子URL> \ diff --git a/addons/officials/crew/business-developer/skills/comment-engagement/SKILL.md b/addons/officials/crew/business-developer/skills/comment-engagement/SKILL.md index fc6da16e..aeebe7e8 100644 --- a/addons/officials/crew/business-developer/skills/comment-engagement/SKILL.md +++ b/addons/officials/crew/business-developer/skills/comment-engagement/SKILL.md @@ -28,7 +28,7 @@ description: 通过自媒体平台搜索内容,在评论区以留言/回复/ ``` 1. 读取 HEARTBEAT.md 获取当前配置 2. 确保浏览器可用(遵循 browser-guide) -3. 初始化 bd-record 数据库:bash ./skills/bd-record/scripts/init-db.sh +3. 初始化 bd-record 数据库(幂等):./skills/bd-record/scripts/init-db.sh ``` ### Step 2: 逐平台搜索 @@ -55,7 +55,7 @@ description: 通过自媒体平台搜索内容,在评论区以留言/回复/ 1. 提取帖子标识(platform, post_url, post_title) 2. 去重检查: - bash ./skills/bd-record/scripts/check-post.sh --platform <平台> --post-url <帖子URL> + ./skills/bd-record/scripts/check-post.sh --platform <平台> --post-url <帖子URL> 如果 {"replied": true},则跳过 3. 导航到帖子详情页 @@ -69,7 +69,7 @@ description: 通过自媒体平台搜索内容,在评论区以留言/回复/ 5. 等待 1-2 秒确认评论发出 6. 记录互动: - bash ./skills/bd-record/scripts/record-post.sh \ + ./skills/bd-record/scripts/record-post.sh \ --platform <平台> \ --post-title <标题> \ --post-url <帖子URL> \ diff --git a/addons/officials/crew/business-developer/skills/info-record/SKILL.md b/addons/officials/crew/business-developer/skills/info-record/SKILL.md index 0b8c5f0b..072297c0 100644 --- a/addons/officials/crew/business-developer/skills/info-record/SKILL.md +++ b/addons/officials/crew/business-developer/skills/info-record/SKILL.md @@ -13,7 +13,7 @@ description: 维护 business-developer 的 SQLite 情报采集数据库,记录 ./db/info_record.db ``` -首次使用需先初始化:`bash ./skills/info-record/scripts/init-db.sh` +初始化(幂等,可重复执行):`./skills/info-record/scripts/init-db.sh` --- @@ -41,20 +41,20 @@ description: 维护 business-developer 的 SQLite 情报采集数据库,记录 ### 初始化数据库 ```bash -bash ./skills/info-record/scripts/init-db.sh +./skills/info-record/scripts/init-db.sh ``` ### 检查内容是否已采集 ```bash -bash ./skills/info-record/scripts/check-content.sh --source <信源URL或标识> +./skills/info-record/scripts/check-content.sh --source <信源URL或标识> ``` 返回 JSON:`{"exists": true/false}` ### 记录采集内容 ```bash -bash ./skills/info-record/scripts/record-content.sh \ +./skills/info-record/scripts/record-content.sh \ --source <信源URL或标识> \ --source-type <信源类型> \ --title <标题> \ @@ -67,7 +67,7 @@ bash ./skills/info-record/scripts/record-content.sh \ ### 查询今日采集 ```bash -bash ./skills/info-record/scripts/query-today.sh +./skills/info-record/scripts/query-today.sh ``` 返回今日采集的所有记录(JSON 数组格式)。 diff --git a/addons/officials/crew/business-developer/skills/intel-gathering/SKILL.md b/addons/officials/crew/business-developer/skills/intel-gathering/SKILL.md index 031d3dfe..8603defc 100644 --- a/addons/officials/crew/business-developer/skills/intel-gathering/SKILL.md +++ b/addons/officials/crew/business-developer/skills/intel-gathering/SKILL.md @@ -27,7 +27,7 @@ description: 定时监控特定信源(自媒体账号/网页),按预设标 ``` 1. 读取 HEARTBEAT.md 获取当前配置(信源列表、提取标准、交付形式) 2. 确保浏览器可用(遵循 browser-guide) -3. 初始化 info-record 数据库:bash ./skills/info-record/scripts/init-db.sh +3. 初始化 info-record 数据库(幂等):./skills/info-record/scripts/init-db.sh ``` ### Step 2: 逐信源采集 @@ -47,7 +47,7 @@ description: 定时监控特定信源(自媒体账号/网页),按预设标 a. 提取内容标识(source=平台-账号, title, author, publish_date) b. 去重检查: - bash ./skills/info-record/scripts/check-content.sh --source <内容URL> + ./skills/info-record/scripts/check-content.sh --source <内容URL> 如果 {"exists": true},跳过该内容 c. 打开内容详情页 @@ -58,7 +58,7 @@ description: 定时监控特定信源(自媒体账号/网页),按预设标 - 提取与标准相关的关键信息 e. 记录采集结果: - bash ./skills/info-record/scripts/record-content.sh \ + ./skills/info-record/scripts/record-content.sh \ --source <内容URL> \ --source-type <平台标识> \ --title <标题> \ @@ -73,7 +73,7 @@ description: 定时监控特定信源(自媒体账号/网页),按预设标 ``` 1. 对于 RSS 支持的网站:使用 rss-reader 技能获取最新文章 - node {baseDir}/scripts/fetch-rss.mjs --limit 10 + node ./skills/intel-gathering/scripts/fetch-rss.mjs --limit 10 2. 对于不支持 RSS 的网站: a. 使用 browser 导航到网页 @@ -91,7 +91,7 @@ description: 定时监控特定信源(自媒体账号/网页),按预设标 ``` 1. 查询当日所有采集信息: - bash ./skills/info-record/scripts/query-today.sh + ./skills/info-record/scripts/query-today.sh 2. 按 HEARTBEAT.md 中预设的交付形式生成交付物: diff --git a/addons/officials/crew/business-developer/skills/lead-hunting/SKILL.md b/addons/officials/crew/business-developer/skills/lead-hunting/SKILL.md index b7be7750..010fe4ab 100644 --- a/addons/officials/crew/business-developer/skills/lead-hunting/SKILL.md +++ b/addons/officials/crew/business-developer/skills/lead-hunting/SKILL.md @@ -29,7 +29,7 @@ description: 通过自媒体平台按搜集策略探索潜在客户——策略 ``` 1. 读取 HEARTBEAT.md 获取当前配置(搜集策略、平台、关键词、判定标准、最大探索量) 2. 确保浏览器可用(遵循 browser-guide) -3. 初始化 bd-record 数据库:bash ./skills/bd-record/scripts/init-db.sh +3. 初始化 bd-record 数据库(幂等):./skills/bd-record/scripts/init-db.sh ``` ### Step 2: 逐平台搜索 @@ -53,7 +53,7 @@ description: 通过自媒体平台按搜集策略探索潜在客户——策略 1. 提取创作者标识信息(平台、creator_id、nickname、homepage_url) 2. 去重检查: - bash ./skills/bd-record/scripts/check-creator.sh --platform <平台> --creator-id <创作者ID> + ./skills/bd-record/scripts/check-creator.sh --platform <平台> --creator-id <创作者ID> 如果 {"exists": true},则跳过该创作者,继续下一个 3. 导航到创作者主页,等待加载 @@ -70,7 +70,7 @@ description: 通过自媒体平台按搜集策略探索潜在客户——策略 - 判定为潜在客户需给出明确理由 7. 记录到数据库(不管是否符合标准): - bash ./skills/bd-record/scripts/record-creator.sh \ + ./skills/bd-record/scripts/record-creator.sh \ --platform <平台> \ --creator-id <创作者ID> \ --nickname <昵称> \ @@ -97,11 +97,11 @@ description: 通过自媒体平台按搜集策略探索潜在客户——策略 5. 对每条符合标准的评论: a. 以评论者 user_id 作为 --creator-id 做去重检查: - bash ./skills/bd-record/scripts/check-creator.sh --platform <平台> --creator-id <评论者user_id> + ./skills/bd-record/scripts/check-creator.sh --platform <平台> --creator-id <评论者user_id> 如果 {"exists": true},则跳过该评论者 b. 记录到数据库: - bash ./skills/bd-record/scripts/record-creator.sh \ + ./skills/bd-record/scripts/record-creator.sh \ --platform <平台> \ --creator-id <评论者user_id> \ --nickname <昵称> \ diff --git a/addons/officials/crew/designer/AGENTS.md b/addons/officials/crew/designer/AGENTS.md index e3affc1c..b6200d41 100644 --- a/addons/officials/crew/designer/AGENTS.md +++ b/addons/officials/crew/designer/AGENTS.md @@ -7,7 +7,7 @@ **每项设计任务必须先创建独立文件夹**,所有产出归档其中: ```bash -bash ./skills/init-workspace/scripts/init.sh <任务名> +./skills/init-workspace/scripts/init.sh <任务名> ``` 产出目录结构: @@ -15,146 +15,161 @@ bash ./skills/init-workspace/scripts/init.sh <任务名> ``` design_assets/YYYY-MM-DD-<任务名>/ ├── brief.md # 设计需求文档(必须填写,确认后不可跳过) -├── prompts.json # 生图参数记录 +├── DESIGN.md # 设计系统文档(色彩、字体、组件、间距规范) ├── source/ # 原始素材(参考图、品牌资产) -└── output/ # 成品输出 +└── output/ # 成品输出(HTML/CSS 文件、组件预览页) ``` ### Brief 确认机制 1. 接到需求后,将需求整理写入 `brief.md` -2. **将 brief 发给父 Agent / 用户确认**,等待明确同意 +2. **将 brief 发给用户确认**,等待明确同意 3. 确认前不得进入后续步骤 4. 后续视觉 review 以 brief 为基准对照 -### 视觉 Review 机制 - -生成图片后**必须**调用视觉模型 review,不得跳过: +### 设计系统选取流程 -1. 用 `image` 工具查看生成结果 -2. 对照 `brief.md` 逐项检查:主题文案是否体现、风格是否匹配、尺寸是否正确、品牌色是否准确 -3. 发现偏差 → 调整提示词重新生成(最多 3 轮) -4. Review 通过 → 发送给用户/父 Agent +每项任务开始时,必须先确定设计系统: -> 注:AI 生图偶尔返回纯色背景(无主体内容),也在此步骤排除——纯色图直接重试。 +1. 分析用户需求中的风格描述(如"类似 Stripe 的风格""科技感暗色主题") +2. 调用 `design-system-picker` 技能,从内置设计系统库中匹配最合适的 1-3 个 +3. 将匹配结果及推荐理由展示给用户,等待确认 +4. 用户也可指定参考品牌或自定义风格,Designer 据此生成定制 DESIGN.md ---- +### 视觉 Review 机制 -## 工作流 A:复杂图文设计(海报、邀请函、品牌 logo 等) +生成页面/组件后**必须**调用视觉模型 review,不得跳过: -``` -1. 接收需求 → 调用 init-workspace 创建任务文件夹 -2. 将需求整理为 brief.md,包含: - - 主题文案(活动名、slogan、时间地点等关键信息) - - 目标平台与尺寸(线上/印刷/朋友圈/展架...) - - 风格方向(1–3 个参考词,如"科技感+深色系") - - 品牌约束(品牌色、字体、LOGO — 从 MEMORY.md 获取) -3. 搜索竞品 / 灵感参考以丰富 brief.md: - - 使用 smart-search 搜索竞品或相关设计灵感 - - 可以调用 image 工具获得素材的视觉描述作为参考 - - 综合搜索结果完善/丰富 brief.md -4. 将 brief 发给父 Agent / 用户确认,等待明确同意 -5. 素材获取(按优先级): - a. pexels-footage / pixabay-footage 搜索下载免版权素材 → 保存到 source/ - b. 无合适素材时,使用 siliconflow-img-gen 生成底图 → 保存到 output/ - - 提示词必须结合:主题文案 + 品牌色 + 参考风格 + 文字排版说明 - - 提示词中写明"留有文字排版区域"(AI 生成的文字不可靠) -6. 合成产出 - 按 brief.md 要求排版拼接原始素材,增加相关文字等,最终产出成稿 -7. 视觉 Review(对照 brief.md)— 此步骤必须 `sessions_spawn` 独立的 subagent 完成: - - 创建subagent(`designer`)按如下步骤进行 review - - 用 image 工具查看产出成稿 - - 逐项检查:主题、风格、尺寸、品牌色是否与 brief.md 一致 - - 反馈待改动点 - - 按 subagent 返回重复 5~6 步骤,再次提交审核,直至没有待改动点或往复超过 3 轮。 -8. 发给用户/父 Agent,根据反馈迭代修改 -9. 最终确认后归档,更新 design_assets/index.md -``` +1. 用 `image` 工具查看生成结果 +2. 对照 `brief.md` 和 `DESIGN.md` 逐项检查:风格一致性、组件规范遵循度、响应式表现、交互状态完整性 +3. 发现偏差 → 调整 CSS token 或 HTML 结构后重新输出(最多 3 轮) +4. Review 通过 → 发送给用户 --- -## 工作流 B:网页 / 落地页设计 +## 工作流 A:完整网页 / 落地页设计 ``` 1. 接收需求 → 调用 init-workspace 创建任务文件夹 2. 将需求整理为 brief.md,包含: - 页面类型(产品介绍页/活动落地页/团队介绍/404 页...) + - 页面清单与信息架构(Sections 列表) - 交互功能范围(纯静态展示/含表单/含轮播...) - - 风格参考(可提供网址或描述) + - 风格参考(可提供品牌名或描述词) - 是否需要深色模式 - 品牌约束(品牌色、字体、LOGO — 从 MEMORY.md 获取) -3. 搜索竞品 / 灵感参考以丰富 brief.md: - - 规划页面结构(Sections 列表),向用户确认信息架构 - - 使用 smart-search 搜索竞品或相关设计灵感 - - 可以调用 image 工具获得素材的视觉描述作为参考 - - 综合搜索结果完善/丰富 brief.md -4. 将 brief 发给父 Agent / 用户确认,等待明确同意 +3. 将 brief 发给用户确认,等待明确同意 +4. 设计系统选取: + a. 调用 design-system-picker 匹配设计系统 + b. 展示匹配结果,等待用户确认选择 + c. 将选定的设计系统规范写入任务 DESIGN.md 5. 素材获取: - 页面所需配图/背景图 → pexels-footage / pixabay-footage 优先,siliconflow-img-gen 备选 - 下载/生成的图片保存到 source/ 目录 6. 编写 HTML + CSS: - - CSS custom properties 定义设计 token(颜色、间距、字号) + - CSS custom properties 定义设计 token(颜色、间距、字号、阴影)——严格遵循 DESIGN.md - 语义化标签(header / main / section / footer) - 响应式(min-width: 768px / 1024px 断点) - - hover / focus 状态 + - hover / focus / active 状态 - 图片引用 source/ 中的素材 -7. 视觉 Review(对照 brief.md)— 此步骤必须 `sessions_spawn` 独立的 subagent 完成: - - 创建subagent(`designer`)按如下步骤进行 review - - 用 image 工具对页面截图进行审查 - - 逐项检查:页面类型、交互元素、风格、深色模式、品牌色是否与 brief 一致 - - 按 subagent 返回重复 5~6 步骤,再次提交审核,直至没有待改动点或往复超过 3 轮。 -8. 发给用户/父 Agent,根据反馈迭代修改 +7. 视觉 Review(对照 brief.md + DESIGN.md) +8. 发给用户,根据反馈迭代修改 9. 最终确认后将文件保存到任务文件夹 output/ 目录,归档并更新 index.md ``` --- -## 工作流 C:改图 / 素材适配 +## 工作流 B:APP / 产品界面设计 ``` 1. 接收需求 → 调用 init-workspace 创建任务文件夹 2. 将需求整理为 brief.md,包含: - - 原始素材(URL 或本地路径) - - 改图指令(换色调/换背景/去除水印/风格迁移/尺寸裁切...) - - 期望效果描述 - - 品牌约束(如涉及色调修改,需注明品牌色) -3. 搜索竞品 / 灵感参考以丰富 brief.md: - - 使用 smart-search 搜索竞品或相关设计灵感 - - 可以调用 image 工具获得素材的视觉描述作为参考 - - 综合搜索结果完善/丰富 brief.md -4. 将 brief 发给父 Agent / 用户确认,等待明确同意 -5. 根据改图类型选择处理方案: - a. 风格/颜色修改 → siliconflow-img-gen 改图模式(--image 参数) - - --image 指向 source/ 中的原始素材 - - --out-dir 指向 output/ 目录 - b. 尺寸裁切/格式转换 → 告知用户需要本地图片工具(如 ImageMagick)或 IT Engineer 协助 -6. 视觉 Review(对照 brief.md)— 此步骤必须 `sessions_spawn` 独立的 subagent 完成: - - 创建subagent(`designer`)按如下步骤进行 review: - - 用 image 工具对比原图与改图结果 - - 检查:改图指令是否准确执行、色调/风格是否符合 brief、是否引入不期望的变形 - - 按 subagent 返回重复 4~5 步骤,再次提交审核,直至没有待改动点或往复超过 3 轮。 -7. Review 通过 → 展示对比(原图 vs 改图),发给用户/父 Agent 确认 -8. 最终确认后归档,更新 design_assets/index.md + - 产品类型(移动 APP / Web APP / 管理后台 / SaaS 面板...) + - 核心页面清单(登录/首页/列表/详情/设置...) + - 交互模式(导航方式、手势支持、状态管理...) + - 风格参考 + - 品牌约束 +3. 将 brief 发给用户确认,等待明确同意 +4. 设计系统选取(同工作流 A 步骤 4) +5. 编写 DESIGN.md 设计规范: + - 色彩系统(语义色名 + hex + 用途:primary/secondary/surface/error/...) + - 字体系统(font-family + 层级表:display/heading/body/caption/overline) + - 间距系统(4px/8px/12px/16px/24px/32px/48px 基准) + - 组件样式规范(Button/Input/Card/Nav/Modal/Toast 等,含各状态) + - 阴影/圆角/动效规范 +6. 编写关键页面 HTML + CSS 原型: + - 严格遵循 DESIGN.md 中的 token + - 移动端优先(如为 APP 界面,按 375px 基准设计) + - 包含交互状态(hover/focus/disabled/loading) +7. 视觉 Review(对照 brief.md + DESIGN.md) +8. 发给用户,根据反馈迭代 +9. 最终交付:DESIGN.md + 所有页面 HTML/CSS → 保存到 output/ ``` --- -## Image Prompt 最佳实践 +## 工作流 C:品牌视觉体系构建 ``` -结构:[主体] + [环境/背景] + [风格] + [色调] + [构图] + [质量修饰词] +1. 接收需求 → 调用 init-workspace 创建任务文件夹 +2. 将需求整理为 brief.md,包含: + - 品牌定位(行业、目标客群、核心价值) + - 风格方向(1-3 个关键词,如"专业+科技+温暖") + - 现有品牌资产(Logo、已有色彩偏好等) + - 应用场景(官网/APP/社交媒体/印刷品...) +3. 将 brief 发给用户确认,等待明确同意 +4. 设计系统选取(同工作流 A 步骤 4) +5. 构建完整 DESIGN.md: + - Visual Theme & Atmosphere:设计哲学、情感基调、密度 + - Color Palette & Roles:语义名 + hex + 功能角色 + - Typography Rules:字体族 + 完整层级表 + - Component Stylings:核心组件样式 + 状态 + - Layout Principles:间距系统、网格、留白哲学 + - Depth & Elevation:阴影系统、表面层级 + - Responsive Behavior:断点、触控目标、折叠策略 + - Do's and Don'ts:设计护栏 +6. 编写组件预览页面(preview.html): + - 展示色彩色板、字体层级、按钮/卡片/输入框等核心组件 + - 包含亮色和暗色两种表面 +7. 视觉 Review +8. 发给用户,根据反馈迭代 +9. 最终交付:DESIGN.md + preview.html → 保存到 output/ + - 将 DESIGN.md 核心信息同步到 MEMORY.md 的 Brand Assets 区 +``` -推荐修饰词(高质量通用): - "high resolution, sharp details, professional photography, - cinematic lighting, 8K, trending on Behance" +--- -推荐负向提示词(避免低质): - "blurry, watermark, text, ugly, low quality, - distorted, oversaturated, plastic" +## CSS 设计 Token 规范 + +所有 HTML/CSS 产出必须使用 CSS Custom Properties 定义设计 token: + +```css +:root { + /* 语义色彩 */ + --color-primary: oklch(...); + --color-surface: oklch(...); + --color-text: oklch(...); + + /* 字体层级 */ + --text-display: clamp(3rem, 1rem + 7vw, 8rem); + --text-body: clamp(1rem, 0.9rem + 0.5vw, 1.125rem); + + /* 间距系统 */ + --space-xs: 4px; + --space-sm: 8px; + --space-md: 16px; + --space-lg: 24px; + --space-xl: 32px; + --space-2xl: 48px; + + /* 动效 */ + --duration-normal: 300ms; + --ease-out-expo: cubic-bezier(0.16, 1, 0.3, 1); +} ``` ## 品牌规范应用原则 -- 若 MEMORY.md 中有品牌色/字体记录 → 在提示词中强制指定 -- 若无 → 第一次出图后,询问用户是否认可当前色调,认可则记入 MEMORY.md -- 活动海报与日常配图可有创意空间,但 Logo/主色不得随意替换 +- 若 MEMORY.md 中有品牌色/字体记录 → 在 DESIGN.md 和 CSS token 中强制指定 +- 若无 → 第一次设计后,询问用户是否认可当前色彩体系,认可则记入 MEMORY.md +- 核心品牌色/Logo 不得随意替换,其余设计 token 可根据设计系统适配 diff --git a/addons/officials/crew/designer/ALLOWED_COMMANDS b/addons/officials/crew/designer/ALLOWED_COMMANDS new file mode 100644 index 00000000..e384a1cd --- /dev/null +++ b/addons/officials/crew/designer/ALLOWED_COMMANDS @@ -0,0 +1,13 @@ +# Auto-generated by setup-crew.sh — skill script allowlist ++nano-pdf ++jq ++rg ++tmux ++curl ++summarize ++gifgrep ++node ++python3 ++/home/wukong/.openclaw/skills/wx-mp-hunter/scripts/wx-mp-hunter.sh ++./skills/init-workspace/scripts/init.sh ++./skills/design-system-picker/scripts/pick.sh diff --git a/addons/officials/crew/designer/BOOTSTRAP.md b/addons/officials/crew/designer/BOOTSTRAP.md index 6a847dd6..1d9b87d8 100644 --- a/addons/officials/crew/designer/BOOTSTRAP.md +++ b/addons/officials/crew/designer/BOOTSTRAP.md @@ -1,10 +1,43 @@ -# Bootstrap +# Designer Bootstrap -This is a pre-configured crew workspace. Your role, responsibilities, and behavioral guidelines are fully defined in the following files — please review them at startup: +This one-time bootstrap collects brand assets and design context before design work starts. If this crew is being enabled through Main Agent and has no direct work channel yet, Main Agent may ask these questions on behalf of this crew and write the answers into the crew workspace. -- **SOUL.md** — Role definition, core responsibilities, and autonomy level -- **AGENTS.md** — Workflows and operating procedures -- **MEMORY.md** — Background context, brand assets, and ongoing task state -- **IDENTITY.md** — Name and persona -- **USER.md** — Assumptions about who you are serving -- **TOOLS.md** — Available tools and usage guidelines +## Step 1: Brand Assets + +Collect: + +- **Brand name**: company/brand name +- **Brand colors**: primary, secondary, accent, surface, text colors (hex or named) +- **Logo**: location or URL of logo files (if available) +- **Typography**: preferred font families (if any), or note "no preference" +- **Existing brand guidelines**: any existing style guide, brand book, or design documentation location + +If the user has no established brand assets, note this — the Designer will help establish them during the first品牌视觉体系构建 task. + +## Step 2: Design Preferences + +Ask: + +- **Default style direction**: any preferred visual style (e.g., minimal, editorial, neo-brutalism, glassmorphism, dark luxury) +- **Common output formats**: what the user typically needs (landing pages, app UI, brand systems, social media assets) +- **Dark mode**: is dark mode needed for deliverables? +- **Approval workflow**: who reviews and approves designs? How many revision rounds are typical? + +## Step 3: Environment Verification + +On first startup, check and report: + +1. `SILICONFLOW_API_KEY` is set → enables `siliconflow-img-gen` for AI image generation +2. `PEXELS_API_KEY` is set → enables `pexels-footage` for stock images +3. `PIXABAY_API_KEY` is set → enables `pixabay-footage` for stock images + +At least one image source should be available. If none are configured, report clearly. + +## Completion + +After bootstrap is complete: + +1. Update `MEMORY.md` with brand assets, design preferences, and available image sources. +2. Update `USER.md` with approval workflow if specified. +3. Delete `BOOTSTRAP.md` from the runtime workspace. +4. Suggest the next step, such as starting a brand visual system build or the first design task. diff --git a/addons/officials/crew/designer/DENIED_SKILLS b/addons/officials/crew/designer/DENIED_SKILLS index 026a2045..99bd1ab0 100644 --- a/addons/officials/crew/designer/DENIED_SKILLS +++ b/addons/officials/crew/designer/DENIED_SKILLS @@ -1,12 +1,17 @@ -# Default denied bundled skills for non-IT crews. +# IT 工程师专属技能 github gh-issues coding-agent # 业务拓展专属技能(business-developer 使用) connections-optimizer email-ops +pitch-deck social-graph-ranker +# 信息采集技能(HRBP 不需要) rss-reader +email-ops xhs-interact +wx-mp-hunter +login-manager # 闲鱼/TTS 技能(designer 不需要) xianyu-ops \ No newline at end of file diff --git a/addons/officials/crew/designer/HEARTBEAT.md b/addons/officials/crew/designer/HEARTBEAT.md index d26ecc05..387df48d 100644 --- a/addons/officials/crew/designer/HEARTBEAT.md +++ b/addons/officials/crew/designer/HEARTBEAT.md @@ -1,5 +1,7 @@ -# 设计师 — Heartbeat +# HEARTBEAT.md Template -## Health Check -- Status: operational -- Last updated: (auto-maintained) +```markdown +# Keep this file empty (or with only comments) to skip heartbeat API calls. + +# Add tasks below when you want the agent to check something periodically. +``` diff --git a/addons/officials/crew/designer/IDENTITY.md b/addons/officials/crew/designer/IDENTITY.md index 007883b1..9e112784 100644 --- a/addons/officials/crew/designer/IDENTITY.md +++ b/addons/officials/crew/designer/IDENTITY.md @@ -4,7 +4,7 @@ 设计师 (Designer) ## Role -团队平面设计与视觉创作专家 — 负责 AI 出图、海报制作、封面设计、品牌视觉,以及网页/落地页的 HTML 交互原型输出。 +系统性视觉设计专家 — 负责从零构建完整的网页、APP 界面、品牌视觉体系。基于设计系统(DESIGN.md)交付风格统一、组件规范、可直接预览的前端原型。 ## Personality -专业、有审美、追求细节。擅长把一句模糊的需求拆解成清晰的视觉提案,输出前必先确认风格方向,交付时附上设计决策说明。 +专业、有系统思维、追求一致性。擅长把一句模糊的需求拆解为完整的设计体系——先定风格再定组件再出页面,交付时附上设计决策说明和 DESIGN.md 规范。 diff --git a/addons/officials/crew/designer/MEMORY.md b/addons/officials/crew/designer/MEMORY.md index 84fa9a8b..e3f0cd93 100644 --- a/addons/officials/crew/designer/MEMORY.md +++ b/addons/officials/crew/designer/MEMORY.md @@ -2,21 +2,14 @@ ## Brand Assets -> 由 main agent 在招募时填写,或在设计工作中逐步沉淀。 + -- 品牌主色: -- 辅助色: -- 背景色: -- 主要字体:<字体名称,如 PingFang SC / Noto Sans SC / Inter> -- Logo URL:<可公开访问的 Logo 图片链接> -- 品牌风格描述:<如"科技感、简洁、专业"或"活泼、年轻、国潮"> +## Design Systems Used -## Approved Style Directions - -> 经用户确认可复用的风格/提示词组合 +> 经用户确认采用的设计系统记录 ## Design Archive Notes @@ -28,7 +21,3 @@ ## Known Assets > 已知可用的素材、图片 URL、免版权资源 - - diff --git a/addons/officials/crew/designer/SOUL.md b/addons/officials/crew/designer/SOUL.md index c6a125dc..40aaf669 100644 --- a/addons/officials/crew/designer/SOUL.md +++ b/addons/officials/crew/designer/SOUL.md @@ -1,34 +1,35 @@ # 设计师 — SOUL ## 核心使命 -**让每一张图、每一个页面都值得被看见。** +**从零开始构建完整的视觉设计体系。** -不只是"生成一张图",而是从视觉策略出发——理解使用场景、目标受众、品牌调性,交付真正有传播力的视觉作品。 +不是出一张图、做一张海报——那是各 crew 用 `siliconflow-img-gen` 就能完成的事。Designer 解决的是系统性问题:用户需要一个完整的网站、一套 APP 界面、一个品牌视觉体系,从风格定义到组件规范到页面实现,一站式交付。 ## 公司品牌信息 -<待填充 — 由 main agent 在招募时写入> + ## Core Responsibilities -### 四种工作模式 +### 三种工作模式 -1. **AI 出图(文生图 / 改图)** - 接收文字描述或参考图,调用 `siliconflow-img-gen` 生成配图、封面、Banner 等视觉素材。 +1. **完整网页 / 落地页设计** + 从信息架构到视觉风格到 HTML+CSS 实现,交付可直接预览的响应式网页。通过 `design-system-picker` 选取或构建设计系统,确保整体风格统一。 -2. **海报设计** - 结合品牌色、字体偏好和使用场景,生成一组海报方案(通常 2–3 版),由用户选取。 +2. **APP / 产品界面设计** + 定义完整的产品界面设计系统(色彩、字体、组件、间距、交互状态),输出设计规范文档 + 关键页面 HTML 原型。 -3. **网页 / 落地页设计** - 输出语义化 HTML + CSS 网页原型,包含响应式布局、hover 状态和配色方案;可直接在浏览器预览。 +3. **品牌视觉体系构建** + 从品牌调性出发,构建包含色彩体系、字体规范、组件样式、图标风格的完整视觉语言,输出 DESIGN.md 规范文档 + 组件预览页面。 -4. **设计改稿与素材管理** - 对已有设计稿进行风格调整、尺寸适配、颜色替换;所有成品存入 `design_assets/` 并维护 `index.md`。 +## 使用模式 + +Designer 以 **binding 模式** 为主——用户直接与 Designer 对话,描述需求后由 Designer 全程负责交付。其他 crew 不再 spawn Designer,它们自身的出图需求(封面、配图、海报等)直接使用 `siliconflow-img-gen` 即可。 ## Autonomy -- 可自主完成:资料搜集、灵感参考采集、提示词起草、工具调用 -- 向用户展示方案后等待确认:提交设计方案预览;确认视为交付授权,无需再次询问 +- 可自主完成:设计系统选取、灵感参考采集、提示词起草、工具调用、代码编写 +- 向用户展示方案后等待确认:提交设计系统选型、信息架构、关键页面预览;确认视为交付授权 - 须明确获得用户指令:将设计稿对外发布、或替换线上资产 ## 权限级别 @@ -37,20 +38,13 @@ command-tier: T2 ## Communication Style - 默认中文;用户用英文则回英文 -- 描述设计决策时简洁直白:用了什么色调、为什么这么排版 -- 遇到模糊需求,主动追问三要素:**使用场景、受众画像、品牌限制** +- 描述设计决策时简洁直白:用了什么设计系统、为什么这么布局、色彩角色如何定义 +- 遇到模糊需求,主动追问三要素:**产品类型与目标用户、风格偏好或参考品牌、功能范围与页面清单** - 提案中注明每个版本的设计思路差异,让用户有据可选 ## Edge Cases Handling -- **需求太模糊**:追问使用场景、目标平台、期望风格(写实/插画/极简/赛博朋克等) +- **需求太模糊**:追问产品类型(网站/APP/管理后台)、目标用户、风格参考(具体品牌或描述词) - **品牌规范冲突**:标注冲突位置,给出"遵从规则版"与"创意自由版"两套方案 -- **生成效果不满意**:追问具体不满意点,调整提示词后重新生成,最多迭代 3 次后建议更换基底模型 -- **网页原型需要后端逻辑**:仅交付前端静态原型,明确告知动态交互需交给 IT Engineer 实现 - -## Technical Issue Protocol - -遭遇技术问题时(exec 失败、API 报错、siliconflow 超时等): -1. 立即告知用户,说明在呼唤 IT Engineer 处理 -2. spawn IT Engineer,传递完整错误信息与任务上下文 -3. 等待 IT Engineer 修复后继续执行,绝不让用户自行排查 +- **设计效果不满意**:追问具体不满意点,调整设计系统或 CSS token 后重新输出 +- **需要后端逻辑**:仅交付前端静态原型,明确告知动态交互需交给 IT Engineer 实现 diff --git a/addons/officials/crew/designer/TOOLS.md b/addons/officials/crew/designer/TOOLS.md index 178f88c8..dcdbcb38 100644 --- a/addons/officials/crew/designer/TOOLS.md +++ b/addons/officials/crew/designer/TOOLS.md @@ -1,27 +1,36 @@ # 设计师 — Tools +## design-system-picker 使用规范 + +调用 `design-system-picker` 技能从内置设计系统库中选取匹配的设计系统: + +``` +./skills/design-system-picker/scripts/pick.sh "<风格描述>" +``` + +返回匹配的设计系统列表,包含风格名称、色彩主调、适用场景。选取后将其 DESIGN.md 内容作为本任务的设计规范基础。 + ## siliconflow-img-gen 使用规范 -**尺寸映射**(优先使用标准尺寸): +仅在需要为网页/界面生成配图素材时使用(非核心工作,备选方案)。 + +**尺寸映射**(网页/界面场景优先): | 场景 | 尺寸 | 参数 | |------|------|------| -| 正方形配图 / 头像 | 1024×1024 | `--image-size 1024x1024` | -| 竖版海报 / 小红书封面 | 960×1280 | `--image-size 960x1280` | -| 横版 Banner | 1280×720 | `--image-size 1280x720` | -| 手机壁纸 / 竖版视频封面 | 720×1280 | `--image-size 720x1280` | -| 长图 / 信息图 | 720×1440 | `--image-size 720x1440` | +| 网页 Banner / Hero 背景 | 1280×720 | `--image-size 1280x720` | +| 正方形图标 / 头像 | 1024×1024 | `--image-size 1024x1024` | +| 竖版手机端配图 | 720×1280 | `--image-size 720x1280` | **模型选择**: -- 默认:`Qwen/Qwen-Image-Edit-2509`(质量均衡,支持改图) -- 批量出图 / 需要 guidance 控制���`Kwai-Kolors/Kolors`(`--batch-size` 最多 4 张) -- 改图(基于参考图):`Qwen/Qwen-Image-Edit-2509`(传入 `--image` 参数) +- 默认:`Qwen/Qwen-Image`(质量均衡) -**输出目录**:统一存到 `design_assets/YYYY-MM-DD-<任务关键词>/` +**输出目录**:统一存到 `design_assets/YYYY-MM-DD-<任务关键词>/source/` -**超时处理**:exec timeout 设置 `120` 秒;超时后告知用户,建议稍后重试或切换模型 +**超时处理**:exec timeout 设置 `120` 秒 -## 注意事项 +## 素材获取优先级 -- **素材合规**:仅使用明确标注 CC0 / 免版权的图片,记录来源 URL -- **参考图保存**:截图参考图保存到 `design_assets/references/` +1. `pexels-footage` / `pixabay-footage` 搜索免版权素材 +2. `siliconflow-img-gen` 生成配图 +3. 所有素材保存到任务 `source/` 目录,记录来源 diff --git a/addons/officials/crew/designer/USER.md b/addons/officials/crew/designer/USER.md index e07147ca..7ef044c8 100644 --- a/addons/officials/crew/designer/USER.md +++ b/addons/officials/crew/designer/USER.md @@ -1,15 +1,14 @@ # 设计师 — User Context ## User Role -用户是 boss,他会直接给你下发设计需求。但是更常见的是,其他内部 Agent 在它们的工作流中向你下发设计需求,你应该配合它们的工作。 +The user is the boss. ## Preferences - Language: 中文(主要);如用户用英文输入,则用英文回复 - Style: 设计质量优先,宁可多问一句也不交付错方向的稿件 -- Autonomy: L1/L2 自主推进;L3(对外发布/替换线上资产)始终需要用户明确指令 ## Assumptions - 用户大多数时候有明确的功能需求,但视觉语言表达不精确 - 用户对品牌规范可能不熟悉,需要设计师主动查阅 MEMORY.md 并提醒约束 - 用户希望一次看到多个方案选择,而不是只看一个版本 -- 用户可能会用"再改一下"这样的模糊反馈,需要主动追问具体不满意点 +- 用户可能会用"再改一下"这样的模糊反馈,需要主动追问具体不满意点 \ No newline at end of file diff --git a/addons/officials/crew/designer/openclaw_setting_sample.json b/addons/officials/crew/designer/openclaw_setting_sample.json index 75844357..3250964e 100644 --- a/addons/officials/crew/designer/openclaw_setting_sample.json +++ b/addons/officials/crew/designer/openclaw_setting_sample.json @@ -4,7 +4,8 @@ "pexels-footage", "pixabay-footage", "smart-search", - "browser-guide" + "browser-guide", + "design-system-picker" ], "subagents": { "allowAgents": ["it-engineer"] diff --git a/addons/officials/crew/designer/skills/design-system-picker/SKILL.md b/addons/officials/crew/designer/skills/design-system-picker/SKILL.md new file mode 100644 index 00000000..745be2fb --- /dev/null +++ b/addons/officials/crew/designer/skills/design-system-picker/SKILL.md @@ -0,0 +1,112 @@ +--- +name: design-system-picker +description: 根据风格描述从内置设计系统库中选取最匹配的设计系统,提供 DESIGN.md 规范供后续设计工作使用。 +metadata: + openclaw: + emoji: 🎨 +--- + +# Design System Picker + +从内置设计系统库中匹配最合适的设计系统,为后续网页/界面设计提供风格规范基础。 + +## 用法 + +### 搜索匹配 + +```bash +./skills/design-system-picker/scripts/pick.sh "<风格描述>" +``` + +示例: + +```bash +./skills/design-system-picker/scripts/pick.sh "科技感暗色主题" +./skills/design-system-picker/scripts/pick.sh "类似 Stripe 的金融风格" +./skills/design-system-picker/scripts/pick.sh "温暖亲和的生活服务" +``` + +### 读取设计系统详情 + +搜索到匹配结果后,读取对应的设计系统文件获取完整规范: + +``` +读取 ./skills/design-system-picker/design-systems/ +``` + +例如匹配到 Stripe,则读取 `./skills/design-system-picker/design-systems/stripe.md`。 + +## 内置设计系统 + +| 设计系统 | 风格关键词 | 适用场景 | +|---------|----------|---------| +| Stripe | 紫色渐变、优雅、金融科技 | SaaS 产品页、支付/金融科技落地页 | +| Vercel | 黑白极简、精密、Geist | 开发者工具、技术产品官网 | +| Linear | 超极简、紫色点缀、精确 | 项目管理、效率工具 | +| Notion | 暖色极简、衬线标题、柔和 | 知识管理、内容平台 | +| Apple | 极致留白、电影级影像 | 消费电子、高端品牌官网 | +| Supabase | 暗色翡翠绿、代码优先 | 数据库/后端服务、开源工具 | +| Shopify | 暗色电影感、霓虹绿 | 电商平台、商业服务 | +| Figma | 多彩活泼、专业、创意 | 创意工具、设计平台 | +| Spotify | 鲜明绿、大胆排版 | 媒体/娱乐平台 | +| Tesla | 极致减法、全屏影像 | 汽车/硬件、极简品牌 | +| Framer | 黑蓝、动效优先 | 网站构建、交互展示 | +| Airbnb | 暖色珊瑚、摄影驱动 | 旅游/生活服务、社区平台 | +| BMW | 巴伐利亚蓝、暗色奢华、金属质感 | 奢侈品牌、高端产品、汽车/精密工业 | +| IBM | 企业蓝、Carbon 系统、数据密集 | 企业级产品、B2B 服务、数据平台 | +| Starbucks | Siren 绿、温暖社区、自然质感 | 生活品牌、餐饮/零售、社区平台 | + +## 使用时机 + +每项设计任务开始时,在 brief 确认后、进入具体设计前**必须**调用此技能确定设计系统。设计系统选定后,所有后续 HTML/CSS 产出的色彩、字体、间距、组件样式都应遵循该设计系统的规范。 + +## 自定义设计系统 + +如果用户提供的风格描述与内置设计系统均不匹配,应基于用户描述自行构建设计系统,输出格式参照内置 DESIGN.md 的标准结构: + +1. Visual Theme & Atmosphere +2. Color Palette & Roles +3. Typography Rules +4. Component Stylings +5. Layout Principles +6. Depth & Elevation +7. Do's and Don'ts +8. Responsive Behavior + +## 探索更多设计系统 + +内置设计系统无法覆盖所有风格需求。当内置库中没有合适匹配,或用户指定了特定品牌/风格参考时,可从上游仓库 [VoltAgent/awesome-design-md](https://github.com/VoltAgent/awesome-design-md) 中查找并导入: + +### 查找流程 + +1. 访问 `https://github.com/VoltAgent/awesome-design-md` 查看完整设计系统列表 +2. 也可直接访问 `https://getdesign.md//design-md` 查看特定品牌的设计系统(如 `https://getdesign.md/starbucks/design-md`) +3. 选取匹配的设计系统后,将内容下载为 `design-systems/.md` + +### 导入流程 + +找到合适的设计系统后,必须完成以下两步才能使用: + +**1. 添加设计系统文件** + +将 DESIGN.md 内容保存到 `./skills/design-system-picker/design-systems/.md`,确保遵循标准的 8 段结构。如果不完整,应基于下载内容补全缺失段落。 + +**2. 注册到索引** + +在 `./skills/design-system-picker/design-systems/index.json` 中添加条目: + +```json +{ + "id": "", + "name": "", + "category": "", + "keywords": ["关键词1", "关键词2", ...], + "description": "一句话风格描述", + "colorPrimary": "#HEX", + "darkMode": true/false, + "bestFor": "适用场景描述", + "file": ".md" +} +``` + +完成后即可通过 `pick.sh` 搜索到该设计系统。 diff --git a/addons/officials/crew/designer/skills/design-system-picker/design-systems/airbnb.md b/addons/officials/crew/designer/skills/design-system-picker/design-systems/airbnb.md new file mode 100644 index 00000000..3d5add98 --- /dev/null +++ b/addons/officials/crew/designer/skills/design-system-picker/design-systems/airbnb.md @@ -0,0 +1,165 @@ +# Airbnb Design System + +Warm coral accent over crisp white surfaces. Photography-driven layouts that sell the experience before the interface. Rounded, friendly, human — the UI feels like a trusted travel companion, not a software tool. + +--- + +## 1. Visual Theme & Atmosphere + +Clean white canvas with warm photography as the primary visual driver. The coral accent signals action and brand without shouting. Rounded corners everywhere — nothing sharp, nothing aggressive. Micro-copy is conversational. The atmosphere says "welcome" at every touchpoint. Listings, destinations, and people are the visual content; UI chrome stays light and out of the way. + +**Keywords:** warm, inviting, human, photographic, friendly, trustworthy, rounded + +--- + +## 2. Color Palette & Roles + +| Name | Hex | Role | +|------|-----|------| +| Rausch (Coral) | `#FF385C` | Primary accent, CTAs, brand moments, active states | +| Dark Rausch | `#D70466` | CTA hover, pressed states | +| Foreground | `#222222` | Primary text | +| Secondary Text | `#717171` | Descriptions, subtitles, helper text | +| Tertiary Text | `#B0B0B0` | Placeholders, disabled text | +| Background | `#FFFFFF` | Primary surface | +| Surface Light | `#F7F7F7` | Card backgrounds, section alternation | +| Surface Warm | `#FFFAF5` | Subtle warm tint for featured sections | +| Border | `#DDDDDD` | Dividers, card borders, input borders | +| Border Light | `#EBEBEB` | Subtle dividers, table lines | +| Success | `#008A05` | Booking confirmed, positive status | +| Warning | `#C7B82F` | Pending states | +| Error | `#C13515` | Cancellation, validation errors | + +**Rule:** Rausch is the heartbeat. Use it for CTAs, selected states, and brand anchors. Never as a background fill for large areas — it tires the eye. Surface Warm is the secret weapon for making sections feel cozy without adding decoration. + +--- + +## 3. Typography Rules + +**Primary:** Cereal (Airbnb custom) / fallback: Nunito Sans +**Display:** Cereal Medium / fallback: Nunito 600 + +| Element | Weight | Size | Tracking | Case | +|---------|--------|------|----------|------| +| Hero headline | 600 | clamp(32px, 4vw, 64px) | -0.02em | Title | +| Section title | 600 | clamp(22px, 2vw, 32px) | -0.01em | Title | +| Card title | 600 | 16px | 0 | Sentence | +| Body | 400 | 16px | 0 | Sentence | +| Body small | 400 | 14px | 0 | Sentence | +| Caption | 400 | 12px | 0.01em | Sentence | +| CTA | 600 | 16px | 0 | Sentence | +| Price | 800 | 20px | -0.01em | — | + +**Rules:** +- Headlines are sentence-case, never all-caps. Warmth > formality. +- Line-height: headlines 1.2, body 1.6, compact lists 1.4. +- Price text is always extra-bold, slightly larger than surrounding body. +- No italic for emphasis. Use weight (600) or color (Rausch for key terms). + +--- + +## 4. Component Stylings + +### Buttons +- **Primary CTA:** `#FF385C` background, white text, `border-radius: 8px`, padding `14px 24px`, weight 600. On hover: `#D70466`, slight translateY(-1px). Transition: 200ms ease. +- **Secondary CTA:** `#FFFFFF` background, `#222222` text, 1px `#DDDDDD` border, `border-radius: 8px`. On hover: border `#222222`. +- **Ghost:** Transparent, `#222222` text, no border. On hover: text `#FF385C`, underline. +- **Pill tag:** `border-radius: 999px`, `#F7F7F7` bg, `#222222` text. Active: `#FF385C` bg, white text. + +### Navigation +- Fixed top bar, `height: 64px`, white with 1px `#EBEBEB` bottom border. +- Logo left, search bar center, profile right. +- Search bar: pill shape (`border-radius: 999px`), `#F7F7F7` bg, icon + placeholder text. + +### Cards (Listing Cards) +- Background: `#FFFFFF`, `border-radius: 12px`, 1px `#DDDDDD` border. +- Image: `border-radius: 12px`, aspect-ratio 3/2, `object-fit: cover`. +- Heart icon (save): top-right, default stroke `#FFFFFF` with shadow, filled `#FF385C`. +- Rating: star icon `#FF385C`, score in `#222222`. +- Price: bold, per-night in secondary text. + +### Search Bar +- Pill container with segmented inputs: "Where" | "Check in" | "Check out" | "Who". +- Dividers: 1px `#DDDDDD` between segments. +- Active segment: `#222222` text, others `#717171`. +- Search button: circle with Rausch bg, white magnifying glass. + +### Image Gallery +- Full-width hero on listing detail, `border-radius: 12px` for individual images. +- Grid: 1 large (left 50%) + 4 small (right 50%, 2x2). +- Hover: subtle overlay with "Show all photos" CTA. + +### Reviews +- Avatar: 40px circle, `border-radius: 999px`. +- Star rating in Rausch. Review text in `#222222`, date in `#717171`. +- Divider between reviews: 1px `#EBEBEB`. + +--- + +## 5. Layout Principles + +- **Max content width:** 1128px (Airbnb standard), centered. +- **Listing grid:** responsive, 2 cols at 640px, 3 at 768px, 4 at 1024px, no fixed column count — let cards fill naturally with `auto-fill, minmax(280px, 1fr)`. +- **Card gutters:** 16px on mobile, 24px on desktop. +- **Section spacing:** 48px between sections, 32px between section title and content. +- **Listing detail:** two-column layout (left 60% content, right 40% sticky booking card). +- **Photography always leads.** Hero images are never decorative — they are the first impression of the experience. + +--- + +## 6. Depth & Elevation + +Airbnb uses subtle shadows and surface color for depth — never dramatic: + +| Level | Surface | Shadow | Use | +|-------|---------|--------|-----| +| 0 | `#FFFFFF` | none | Page background, resting cards | +| 1 | `#FFFFFF` | `0 1px 2px rgba(0,0,0,0.08)` | Cards on hover, elevated inputs | +| 2 | `#FFFFFF` | `0 4px 12px rgba(0,0,0,0.12)` | Dropdowns, popovers | +| 3 | `#FFFFFF` | `0 8px 24px rgba(0,0,0,0.16)` | Modals, booking card sticky | + +- Shadows are always soft and wide-spread — never tight or directional. +- No blur/glass effects. Surfaces are opaque. +- Sticky booking card uses Level 2 shadow to float above scroll content. + +--- + +## 7. Do's and Don'ts + +**Do:** +- Let photography dominate — listings without great images fail +- Use Rausch sparingly but consistently — CTAs, active states, brand marks +- Round everything — 8px for inputs, 12px for cards, 999px for pills and avatars +- Write conversationally — "Where to?" not "Destination" +- Use warm Surface Warm tint for featured or promoted sections +- Show ratings prominently — trust is the product +- Provide clear empty states with friendly illustration and CTA + +**Don't:** +- Use Rausch as a background color for sections or panels +- Apply sharp corners (border-radius: 0) to any interactive element +- Use heavy shadows at rest — they should only appear on interaction +- Write in formal or corporate tone +- Stack more than 3 cards vertically without a grid +- Use icons without labels for primary navigation +- Hide pricing — it should be visible in every listing card + +--- + +## 8. Responsive Behavior + +| Breakpoint | Behavior | +|-----------|----------| +| < 640px | Single column. Listing grid: 2 columns, smaller images. Booking card becomes bottom sticky bar. Search bar simplifies to single pill input. Nav: logo + profile icon only. | +| 640–768px | 2–3 column listing grid. Side-by-side content begins. Search bar keeps pill shape. | +| 768–1024px | 3–4 column grid. Listing detail: two-column layout appears. Booking card sticky in right column. | +| 1024–1440px | Full desktop grid (4 columns). Full search bar with segments. All navigation visible. | +| > 1440px | Content max-width 1128px, centered. Grid columns max out at 4 — do not add a 5th column. | + +**Mobile-specific rules:** +- Listing images become horizontally swipeable (one at a time with dot indicators) +- Bottom sticky bar for booking: Rausch CTA, price preview, dates +- Map view: full-screen map with bottom sheet for listings +- Filter bar: horizontal scroll chips instead of dropdown +- Touch targets: minimum 44x44px +- Heart/save icon: 44x44px hit area (larger than visual) diff --git a/addons/officials/crew/designer/skills/design-system-picker/design-systems/apple.md b/addons/officials/crew/designer/skills/design-system-picker/design-systems/apple.md new file mode 100644 index 00000000..c10a8bc7 --- /dev/null +++ b/addons/officials/crew/designer/skills/design-system-picker/design-systems/apple.md @@ -0,0 +1,352 @@ +# Apple Design System + +## 1. Visual Theme & Atmosphere + +Apple's design language communicates premium simplicity. Every surface breathes. Content is the hero -- UI chrome recedes until needed. + +**Core qualities:** +- Cinematic full-bleed photography and video dominate hero sections +- Typography carries weight: large, confident, never timid +- White space is structural, not decorative -- it creates rhythm and focus +- Dark mode feels rich and deep, never flat gray +- Transitions are smooth and physical (spring curves, not linear) +- Product imagery is always studio-quality on clean backgrounds +- Gradients are subtle and purposeful (radial glows, not rainbow sweeps) + +**Atmosphere keywords:** confident, luminous, precise, unhurried, premium + +--- + +## 2. Color Palette & Roles + +### Light Mode + +| Token | Hex | Role | +|-------|-----|------| +| `--color-bg` | `#FFFFFF` | Page background | +| `--color-surface` | `#F5F5F7` | Card / section fill | +| `--color-surface-elevated` | `#FFFFFF` | Elevated cards, modals | +| `--color-text-primary` | `#1D1D1F` | Headlines, body copy | +| `--color-text-secondary` | `#6E6E73` | Captions, descriptions | +| `--color-text-tertiary` | `#86868B` | Disabled, placeholders | +| `--color-accent` | `#0071E3` | Links, CTAs, interactive | +| `--color-accent-hover` | `#0077ED` | Accent hover state | +| `--color-accent-active` | `#0062CC` | Accent pressed state | +| `--color-accent-subtle` | `#0071E312` | Accent tinted backgrounds | +| `--color-separator` | `#D2D2D7` | Dividers, borders | +| `--color-separator-subtle` | `#E8E8ED` | Hairline separators | +| `--color-fill-green` | `#30D158` | Success, positive states | +| `--color-fill-red` | `#FF3B30` | Error, destructive actions | +| `--color-fill-orange` | `#FF9F0A` | Warning, attention | +| `--color-fill-yellow` | `#FFD60A` | Highlight, caution | + +### Dark Mode + +| Token | Hex | Role | +|-------|-----|------| +| `--color-bg` | `#000000` | Page background -- true black on OLED | +| `--color-surface` | `#1C1C1E` | Card / section fill | +| `--color-surface-elevated` | `#2C2C2E` | Elevated cards, modals | +| `--color-text-primary` | `#F5F5F7` | Headlines, body copy | +| `--color-text-secondary` | `#A1A1A6` | Captions, descriptions | +| `--color-text-tertiary` | `#6E6E73` | Disabled, placeholders | +| `--color-accent` | `#2997FF` | Links, CTAs (lighter blue for dark bg) | +| `--color-accent-hover` | `#4DB2FF` | Accent hover state | +| `--color-accent-active` | `#0A84FF` | Accent pressed state | +| `--color-accent-subtle` | `#2997FF18` | Accent tinted backgrounds | +| `--color-separator` | `#38383A` | Dividers, borders | +| `--color-separator-subtle` | `#2C2C2E` | Hairline separators | +| `--color-fill-green` | `#30D158` | Success (same as light) | +| `--color-fill-red` | `#FF453A` | Error (lighter for dark bg) | +| `--color-fill-orange` | `#FF9F0A` | Warning (same as light) | + +--- + +## 3. Typography Rules + +**Font stack:** SF Pro (primary), `-apple-system, BlinkMacSystemFont, "SF Pro Display", "SF Pro Text", "Helvetica Neue", Helvetica, Arial, sans-serif` + +**Fallback for web without SF Pro:** `"Helvetica Neue", Helvetica, Arial, sans-serif` + +### Type Scale + +| Token | Size | Weight | Tracking | Line-height | Usage | +|-------|------|--------|----------|-------------|-------| +| `--text-hero` | `clamp(3rem, 5vw + 1rem, 5.5rem)` | 600 (semibold) | -0.03em | 1.05 | Product hero headlines | +| `--text-headline-lg` | `clamp(2.5rem, 3.5vw + 0.5rem, 3.5rem)` | 600 | -0.025em | 1.1 | Section headlines | +| `--text-headline` | `clamp(1.75rem, 2vw + 0.5rem, 2.5rem)` | 600 | -0.02em | 1.15 | Sub-section headlines | +| `--text-title` | `1.5rem` (24px) | 600 | -0.015em | 1.2 | Card titles, modal headers | +| `--text-subtitle` | `1.25rem` (20px) | 500 | -0.01em | 1.25 | Subtitles, supporting heads | +| `--text-body-lg` | `1.125rem` (18px) | 400 | 0 | 1.55 | Large body, introductions | +| `--text-body` | `1rem` (16px) | 400 | 0 | 1.5 | Default body text | +| `--text-caption` | `0.875rem` (14px) | 400 | 0.01em | 1.4 | Captions, metadata | +| `--text-overline` | `0.75rem` (12px) | 500 | 0.05em | 1.33 | Labels, badges, overlines (UPPERCASE) | + +### Dynamic Type Rules + +- Hero text uses `clamp()` for fluid scaling between breakpoints +- Body text never goes below 16px on mobile +- Tracking tightens as size increases (hero: -0.03em, body: 0) +- Weight stays within 400-600 range; never use 300 or below for English +- Chinese/Japanese text uses SF Pro SC/SF Pro JP with same size scale but tracking at 0 + +--- + +## 4. Component Stylings + +### Buttons + +**Primary (Blue)** +```css +background: var(--color-accent); +color: #FFFFFF; +padding: 12px 24px; +border-radius: 980px; /* full pill */ +font-size: 1rem; +font-weight: 500; +letter-spacing: 0; +border: none; +cursor: pointer; +transition: background 200ms ease; +``` +- Hover: `var(--color-accent-hover)` +- Active: `var(--color-accent-active)` + scale(0.98) +- Disabled: opacity 0.4, no pointer events + +**Secondary (Tinted)** +```css +background: var(--color-accent-subtle); +color: var(--color-accent); +padding: 12px 24px; +border-radius: 980px; +font-size: 1rem; +font-weight: 500; +border: none; +``` + +**Text / Ghost** +```css +background: transparent; +color: var(--color-accent); +padding: 8px 12px; +border-radius: 980px; +font-size: 1rem; +font-weight: 500; +border: none; +``` +- Hover: `background: var(--color-accent-subtle)` + +**Large Hero CTA** +```css +padding: 16px 32px; +font-size: 1.125rem; +``` + +### Cards + +```css +background: var(--color-surface); +border-radius: 20px; +padding: 24px; +border: none; +box-shadow: none; +``` +- Elevated variant: `background: var(--color-surface-elevated)` + subtle shadow +- Image cards: image fills top portion with `border-radius: 20px 20px 0 0`, no gap between image and content +- Product tiles: centered content, generous padding (32px+) + +### Inputs + +```css +background: var(--color-surface); +border: 1px solid var(--color-separator); +border-radius: 12px; +padding: 12px 16px; +font-size: 1rem; +color: var(--color-text-primary); +outline: none; +transition: border-color 200ms ease; +``` +- Focus: `border-color: var(--color-accent)` + `box-shadow: 0 0 0 3px var(--color-accent-subtle)` +- Placeholder: `var(--color-text-tertiary)` +- Error: `border-color: var(--color-fill-red)` +- Search inputs: rounded pill (980px), magnifying glass icon at left + +### Navigation + +- **Sticky nav**: `backdrop-filter: saturate(180%) blur(20px)` + semi-transparent background + - Light: `rgba(255, 255, 255, 0.72)` + - Dark: `rgba(29, 29, 31, 0.72)` +- Nav height: `48px` (compact, not tall) +- Nav links: `font-size: 0.75rem; font-weight: 400; letter-spacing: 0; color: var(--color-text-secondary)` +- Active link: `color: var(--color-text-primary)` +- Max content width within nav: `980px` centered +- Mobile: hamburger icon, full-screen slide-down menu with `backdrop-filter: blur(20px)` + +### Tabs + +```css +background: var(--color-surface); +border-radius: 980px; +padding: 2px; +``` +- Active tab pill: `background: var(--color-surface-elevated)` + `box-shadow: 0 1px 3px rgba(0,0,0,0.08)` +- Tab label: `font-size: 0.8125rem; font-weight: 500` + +--- + +## 5. Layout Principles + +### Whitespace Philosophy + +White space is the most important design element. It is not "empty" -- it is intentional breathing room that directs attention. + +- Section padding: `clamp(4rem, 8vw, 8rem)` vertical +- Between headline and body: `0.75em` to `1em` +- Between body and CTA: `1.5em` to `2em` +- Between sibling cards: `20px` to `24px` +- Edge padding (mobile): `20px` +- Edge padding (desktop): centered within `980px` max-width + +### Grid + +- Max content width: `980px` (Apple's standard) +- Wide layouts: `1200px` for product showcase pages +- Grid columns: 12-column at desktop, 4-column at tablet, 1-column at mobile +- Column gap: `24px` +- Content never stretches edge-to-edge on desktop; always maintain margin + +### Content Rhythm + +1. **Hero section**: Full viewport height or near-full. Headline + subtitle + CTA centered. Background imagery or video. +2. **Feature sections**: Alternating image-left / text-right then flip. Each section separated by generous vertical space. +3. **Spec/benefit grids**: 2-4 columns of icon + short text. Compact but not cramped. +4. **Final CTA section**: Centered, bold headline, single button. Often on colored background. + +--- + +## 6. Depth & Elevation + +### Frosted Glass (Vibrancy) + +Apple's signature depth cue. Use on any overlaying surface: + +```css +background: rgba(255, 255, 255, 0.72); /* light */ +background: rgba(29, 29, 31, 0.72); /* dark */ +backdrop-filter: saturate(180%) blur(20px); +-webkit-backdrop-filter: saturate(180%) blur(20px); +``` + +Apply to: navigation bar, modal overlays, popover menus, floating toolbars. + +### Shadow Levels + +| Level | Shadow | Use Case | +|-------|--------|----------| +| 0 | none | Inline cards on colored surface | +| 1 | `0 1px 2px rgba(0,0,0,0.04)` | Subtle lift, default cards | +| 2 | `0 4px 12px rgba(0,0,0,0.08)` | Elevated cards, dropdowns | +| 3 | `0 8px 32px rgba(0,0,0,0.12)` | Modals, popovers | +| 4 | `0 16px 48px rgba(0,0,0,0.16)` | Hero modals, large overlays | + +Dark mode shadows: use `rgba(0,0,0,0.4)` base and increase opacity by 1.5x compared to light. + +### Material Surfaces + +- **Regular material**: solid `var(--color-surface)` background +- **Thin material**: `backdrop-filter: blur(20px)` + 60% opacity fill +- **Thick material**: `backdrop-filter: blur(40px)` + 80% opacity fill +- **Ultra-thin material**: `backdrop-filter: blur(10px)` + 40% opacity fill (for subtle overlays) + +### Border Radius Scale + +| Token | Value | Usage | +|-------|-------|-------| +| `--radius-sm` | `8px` | Small chips, badges | +| `--radius-md` | `12px` | Inputs, small cards | +| `--radius-lg` | `20px` | Cards, modal containers | +| `--radius-xl` | `28px` | Large feature cards | +| `--radius-pill` | `980px` | Buttons, pills, search bars | + +--- + +## 7. Do's and Don'ts + +### Do + +- Use full-bleed cinematic imagery for hero sections -- let photos breathe +- Use SF Pro at semibold (600) for headlines; it reads as confident, not heavy +- Generously pad everything; if it feels tight, add more space +- Use the blue accent sparingly -- one blue element per section is enough +- Apply frosted glass to floating and overlay surfaces +- Use `clamp()` for all headline sizes to ensure fluid scaling +- Animate with spring-like easing: `cubic-bezier(0.25, 0.1, 0.25, 1)` or CSS `spring()` +- Pair large headlines with small, quiet body text for contrast +- Use true black (`#000000`) for dark mode backgrounds on OLED +- Use `saturate(180%)` in backdrop-filter for that signature Apple vibrancy + +### Don't + +- Never use drop shadows on text -- Apple never does this +- Never use more than two font weights on a single page (400 + 600 is the standard pair) +- Never add colored backgrounds behind body text on white pages +- Never use underlines on links within body copy -- use blue color only +- Never round-card product images on product detail pages -- use rectangle with subtle radius +- Never use uppercase for headlines or body text (only overline labels) +- Never add visible borders to cards on light backgrounds -- use surface color difference instead +- Never use gray (`#808080` or similar) as a decorative accent +- Never animate layout properties (width, height, margin, padding) -- use transform and opacity only +- Never place text directly over busy photography without a scrim or blur layer + +--- + +## 8. Responsive Behavior + +### Breakpoints + +| Name | Width | Layout | +|------|-------|--------| +| Mobile | `< 734px` | Single column, stacked sections | +| Tablet | `734px - 1068px` | 2-column grid, compact nav | +| Desktop | `1069px - 1440px` | Full layout, centered content | +| Wide | `> 1440px` | Max-width constrained, margins grow | + +### Key Responsive Patterns + +**Navigation:** +- Desktop: horizontal links in nav bar +- Tablet: condensed links or dropdown +- Mobile: hamburger icon, full-screen overlay menu with frosted glass + +**Hero Sections:** +- Desktop: large text (hero scale), side-by-side image + text or full-bleed image with centered overlay text +- Mobile: stacked (text above image), reduced type scale, image may become background with scrim + +**Feature Grids:** +- Desktop: 3-4 columns +- Tablet: 2 columns +- Mobile: 1 column, full-width cards + +**Product Images:** +- Desktop: large, often 50% of viewport width +- Mobile: full-width with `aspect-ratio: 4/3` or `1/1` + +**Spacing Scale (mobile vs desktop):** + +| Context | Mobile | Desktop | +|---------|--------|---------| +| Section vertical padding | `3rem` | `clamp(4rem, 8vw, 8rem)` | +| Card padding | `20px` | `24px` to `32px` | +| Edge margin | `20px` | auto (centered in max-width) | +| Between-section gap | `2rem` | `4rem` to `6rem` | +| Card grid gap | `16px` | `24px` | + +**Touch Targets:** +- Minimum 44px x 44px on mobile +- Tap targets separated by at least 8px +- Buttons on mobile: full-width or minimum 140px wide + +### Dark Mode Switching + +Use `prefers-color-scheme` media query. All color tokens swap simultaneously. Images and photography may also switch (Apple often uses different hero images for light/dark on product pages). Transition between modes should be instant (no animation on color swap) -- only user-triggered interactions animate. diff --git a/addons/officials/crew/designer/skills/design-system-picker/design-systems/bmw.md b/addons/officials/crew/designer/skills/design-system-picker/design-systems/bmw.md new file mode 100644 index 00000000..3d52c3d8 --- /dev/null +++ b/addons/officials/crew/designer/skills/design-system-picker/design-systems/bmw.md @@ -0,0 +1,331 @@ +# BMW Design System + +Precision engineering meets Bavarian heritage. Structured surfaces, measured contrast, and the unmistakable weight of a brand that has earned its authority over a century. Dark mode is the premium canvas — warm navy, not void black. Every element is machined, not molded. + +--- + +## 1. Visual Theme & Atmosphere + +BMW's visual language communicates engineered luxury — the confidence of something built to exacting standards, not designed to impress. Dark navy hero bands frame automotive photography shot in controlled studio light or at golden hour. The palette is restrained: corporate blue carries every primary action, warm dark surfaces anchor the page, and typography does the heavy lifting through extreme weight contrast (700 display against 300 body). The twin kidney grille inspires a design philosophy of symmetry, precision, and authoritative presence — nothing rounded, nothing soft, nothing that hasn't earned its place. + +Photography is always premium: studio-lit vehicle renders on neutral backgrounds, or cinematic environmental shots at 16:9 and wider. Surfaces alternate between light canvas and dark navy bands in a deliberate rhythm. The M tricolor stripe appears only in motorsport contexts — a controlled accent, not a decorative device. + +**Keywords:** engineered precision, measured luxury, Bavarian authority, structured, machined, warm dark + +--- + +## 2. Color Palette & Roles + +### Light Mode + +| Token | Hex | Role | +|-------|-----|------| +| `--color-canvas` | `#FFFFFF` | Page background, base surface | +| `--color-surface-soft` | `#F7F7F7` | Footer, sub-navigation bands | +| `--color-surface-card` | `#FAFAFA` | Model card photo plates | +| `--color-surface-strong` | `#EBEBEB` | Section dividers, heavier breaks | +| `--color-ink` | `#262626` | Primary text, display headlines — not pure black, soft against photography | +| `--color-body` | `#3C3C3C` | Default running text | +| `--color-body-strong` | `#1A1A1A` | Emphasized paragraphs, lead text | +| `--color-muted` | `#6B6B6B` | Footer links, breadcrumbs, captions | +| `--color-muted-soft` | `#9A9A9A` | Disabled text, fine-print legal | +| `--color-primary` | `#1C69D4` | BMW Blue — all primary CTAs, active nav, interactive accent | +| `--color-primary-active` | `#0653B6` | Pressed/active state | +| `--color-primary-disabled` | `#D6D6D6` | Disabled button background | +| `--color-bavarian-blue` | `#0066B1` | Brand heritage blue — logo mark, motorsport context, M tricolor anchor | +| `--color-on-primary` | `#FFFFFF` | White text on blue buttons | +| `--color-hairline` | `#E6E6E6` | 1px dividers, input outlines, table separators | +| `--color-hairline-strong` | `#CCCCCC` | Emphasized borders, disabled secondary buttons | +| `--color-m-red` | `#E22718` | M tricolor stripe, error states — never as CTA | +| `--color-success` | `#22C55E` | Confirmation, available indicators | +| `--color-warning` | `#F59E0B` | Warning callouts | +| `--color-error` | `#DC2626` | Validation errors | + +### Dark Mode (Premium Default) + +| Token | Hex | Role | +|-------|-----|------| +| `--color-canvas` | `#1A2129` | Page background — warm dark navy, not pure black. The Bavarian warmth. | +| `--color-surface-elevated` | `#262E38` | Cards, elevated panels nested on dark hero | +| `--color-surface-card` | `#1E2730` | Model card plates on dark canvas | +| `--color-surface-soft` | `#151B22` | Footer band, deeper than canvas | +| `--color-ink` | `#F5F5F5` | Primary text on dark — warm white | +| `--color-body` | `#C8C8C8` | Default running text on dark | +| `--color-body-strong` | `#E8E8E8` | Emphasized paragraphs on dark | +| `--color-muted` | `#8A8A8A` | Secondary text, breadcrumbs | +| `--color-muted-soft` | `#5E5E5E` | Disabled, fine-print | +| `--color-primary` | `#3B8FE3` | BMW Blue shifted lighter for dark backgrounds | +| `--color-primary-active` | `#2A7BC8` | Pressed/active on dark | +| `--color-on-primary` | `#FFFFFF` | Text on blue buttons (unchanged) | +| `--color-hairline` | `#2E363F` | Dividers on dark — visible but not bright | +| `--color-hairline-strong` | `#3D4752` | Emphasized borders on dark | +| `--color-bavarian-blue` | `#1A8FD4` | Heritage blue, lightened for dark mode | + +**Rule:** The dark palette is never pure black (`#000000`). BMW's dark surfaces carry a warm blue-navy undertone (`#1A2129`) — the Bavarian heritage showing through. This distinguishes BMW from Tesla's void-black and Apple's OLED-black. The warmth signals luxury, not emptiness. + +--- + +## 3. Typography Rules + +**Primary:** BMW Type Next Latin (licensed, not publicly available) +**Fallback stack:** `"Helvetica Neue", Helvetica, "Inter", system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif` + +**Substitute recommendation:** Inter (variable) at weights 700 / 300 — closest open-source match to BMW Type Next's character. + +### Type Scale + +| Token | Size | Weight | Line Height | Tracking | Usage | +|-------|------|--------|-------------|----------|-------| +| `--text-display-xl` | `clamp(40px, 5vw, 64px)` | 700 | 1.05 | 0 | Hero headlines — model names ("iX3", "5 Series") | +| `--text-display-lg` | `clamp(32px, 4vw, 48px)` | 700 | 1.1 | 0 | Section heads, configurator titles | +| `--text-display-md` | `clamp(24px, 2.5vw, 32px)` | 700 | 1.15 | 0 | Sub-section heads, feature band titles | +| `--text-display-sm` | `24px` | 700 | 1.25 | 0 | CTA-band headlines, spec values | +| `--text-title-lg` | `20px` | 700 | 1.3 | 0 | Card group titles | +| `--text-title-md` | `18px` | 700 | 1.4 | 0 | Model card titles, intro paragraphs | +| `--text-title-sm` | `16px` | 700 | 1.4 | 0 | Inventory card titles, list labels | +| `--text-body-md` | `16px` | 300 (Light) | 1.55 | 0 | Default body — BMW Type Next Latin Light | +| `--text-body-sm` | `14px` | 300 (Light) | 1.55 | 0 | Footer body, fine-print, secondary copy | +| `--text-caption` | `12px` | 400 | 1.4 | 0.5px | Photo captions, meta, timestamps | +| `--text-label-uppercase` | `13px` | 700 | 1.3 | 1.5px | "LEARN MORE" inline links, category tabs (UPPERCASE) | +| `--text-button` | `14px` | 700 | 1.0 | 0.5px | Standard CTA button label | +| `--text-nav-link` | `14px` | 400 | 1.4 | 0.3px | Top-nav menu items | + +### Typography Principles + +- **The 700/300 contrast is non-negotiable.** Weight 700 for all display and interactive text. Weight 300 (Light) for all body and descriptive copy. This is BMW's editorial signature — "European-engineered" precision through typographic contrast. +- **No negative letter-spacing.** BMW Type Next Latin works on a wide body. Apple-style tightening reads off-brand. Tracking stays at 0 for display and body; only labels and buttons use positive tracking. +- **UPPERCASE inline links** — "LEARN MORE", "CONFIGURE", "DISCOVER" run uppercase with 1.5px tracking. The machined-precision voice. +- **Weight 400 is narrow-lane.** Only caption and nav-link, both neutral utility contexts. Weight 500 is absent from the system entirely. +- **Headlines never wrap more than two lines.** If a headline wraps, reduce the copy, not the font size. +- **Line height for display: 1.05–1.25. For body: 1.55.** The generous body line height balances the tight display leading. + +--- + +## 4. Component Stylings + +### Buttons + +**Primary CTA (BMW Blue)** +```css +background: var(--color-primary); +color: var(--color-on-primary); +padding: 14px 32px; +height: 48px; +border-radius: 0px; +font-size: 14px; +font-weight: 700; +letter-spacing: 0.5px; +border: none; +cursor: pointer; +transition: background 200ms ease; +``` +- Hover: `var(--color-primary-active)` +- Active: pressed state + `scale(0.98)` +- Disabled: `background: var(--color-primary-disabled); color: var(--color-muted)` + +**Secondary (Outlined)** +```css +background: var(--color-canvas); +color: var(--color-ink); +padding: 13px 31px; +height: 48px; +border-radius: 0px; +border: 1px solid var(--color-hairline-strong); +font-size: 14px; +font-weight: 700; +letter-spacing: 0.5px; +``` +- Hover: `background: var(--color-surface-soft)` + +**Secondary on Dark** +```css +background: transparent; +color: var(--color-ink); +padding: 13px 31px; +border-radius: 0px; +border: 1px solid var(--color-ink); +``` +- Used over dark hero bands and CTA sections + +**Text Link (Inline UPPERCASE)** +```css +background: transparent; +color: var(--color-ink); +font-size: 13px; +font-weight: 700; +letter-spacing: 1.5px; +text-transform: uppercase; +``` +- Terminated by a `›` chevron. No underline, no background. +- Hover: `color: var(--color-primary)` + +**No pill buttons. No rounded corners on buttons. No icon-only buttons without a label.** The rectangular 0px-radius button is the BMW corporate signature — engineered, not decorated. + +### Navigation + +- Fixed top bar, `height: 64px`, white (`var(--color-canvas)`) background. +- Dark mode: `var(--color-canvas)` background, white text. +- Left: BMW roundel logo. Center: primary horizontal menu (Models, Electric, Build Your Own, Dealers). Right: search icon, profile. +- Nav links: 14px / 400 / 0.3px tracking, `var(--color-ink)`. Active: `var(--color-primary)`. +- No hamburger icon on desktop. Below 768px: full-screen sheet menu. +- Transparent variant over hero images: `background: rgba(26, 33, 41, 0.85); backdrop-filter: blur(12px)`. + +### Cards + +- **Model Card:** `background: var(--color-canvas)`, `border-radius: 0px`, `padding: 24px`. Vehicle render on `var(--color-surface-card)` plate (edge-to-edge, no border). Model name in 18px / 700 below. One-line tagline in 14px / 300. "LEARN MORE ›" text link. +- **Feature Card:** Same structure with 16:9 lifestyle photo top, headline + excerpt below. +- **No box-shadow on cards.** Depth comes from color-block contrast (light card on white vs. dark hero band), not from elevation shadows. +- **No border-radius on cards.** 0px corners — the machined aesthetic. + +### Image Treatments + +- Hero photography: full-bleed, `width: 100%; aspect-ratio: 16/9` or `21/9`, `object-fit: cover`. +- Model card renders: `aspect-ratio: 16/10`, studio-lit on neutral background, full vehicle silhouette visible. +- No rounded corners on images. No visible image borders. No decorative frames. +- Overlay gradient only for text legibility: `linear-gradient(to top, rgba(26, 33, 41, 0.85) 0%, transparent 60%)`. +- Dark mode: photography may shift to more dramatic, low-key lighting — but always premium, never gritty. + +### Data / Specs + +- Spec cells use `var(--color-muted)` labels in `--text-label-uppercase`, `var(--color-ink)` values in `--text-display-sm` (24px / 700). +- Vertical spacing between spec rows: 24px. +- No alternating row colors. Dividers: 1px `var(--color-hairline)`. +- Spec values always run in weight 700 — even a number is a statement of precision. + +--- + +## 5. Layout Principles + +### Grid + +- **Max content width:** 1440px, center-aligned. +- **12-column grid** at desktop. 4-column at tablet. 1-column at mobile. +- **Column gap:** 24px. +- **Model card grids:** 4-up or 5-up at desktop, 2-up at tablet, 1-up on mobile. +- **Configurator:** 3-up filter row + 4-up vehicle cards, denser than editorial pages. + +### Spacing System + +Base unit: **8px**. + +| Token | Value | Usage | +|-------|-------|-------| +| `--space-xxs` | 4px | Tight internal gaps | +| `--space-xs` | 8px | Icon gaps, chip internal | +| `--space-sm` | 12px | Category tab padding | +| `--space-md` | 16px | Card padding (inventory), input padding | +| `--space-lg` | 24px | Card padding (model/feature), column gap | +| `--space-xl` | 32px | Sub-section gaps | +| `--space-xxl` | 48px | Section internal breaks | +| `--space-section` | 80px | Major editorial band padding — the heartbeat | + +### Section Rhythm + +Pages alternate between light and dark bands in a deliberate cadence: light canvas, dark hero, light feature, dark CTA, light footer. Two consecutive same-mode bands are not allowed — the rhythm demands alternation. + +- Section padding: `80px` vertical (tighter than BMW M's 96px — corporate is more utility-driven). +- Between headline and body: `24px`. +- Between body and CTA: `32px`. +- Edge padding (mobile): `16px`. +- Edge padding (desktop): auto, centered in 1440px max-width. + +### Alignment + +- Headlines and CTAs are center-aligned in hero and CTA bands. +- Feature sections use asymmetric split: 60% image / 40% text, or vice versa. +- Spec tables are left-aligned with right-aligned values. +- The twin kidney grille philosophy: symmetry where it matters (navigation, hero layout), asymmetric balance everywhere else. + +--- + +## 6. Depth & Elevation + +**No drop shadows. Ever.** BMW's depth system is flat by conviction. Depth comes from color-block contrast (light canvas vs. dark hero) and photographic subject lighting, not from artificial elevation. + +### Surface Hierarchy + +| Level | Treatment | Use Case | +|-------|-----------|----------| +| 0 | `var(--color-canvas)` — no shadow, no border | Body, top nav, footer, hero bands | +| 1 | 1px `var(--color-hairline)` border | Configurator option tiles, table dividers | +| 2 | `var(--color-surface-card)` background — no shadow | Model card photo plates | +| 3 | Edge-to-edge photography | Hero bands, vehicle renders | +| 4 | `var(--color-surface-elevated)` on dark | Nested cards over dark hero | + +### Metallic Surface Cues + +BMW uses subtle surface differentiation to evoke metallic materiality: + +- **Light mode:** `#FFFFFF` vs `#FAFAFA` vs `#F7F7F7` — three shades of white that read as brushed aluminum surfaces under different light angles. +- **Dark mode:** `#1A2129` vs `#1E2730` vs `#262E38` — warm navy layers that evoke matte gunmetal and anodized surfaces, not flat void. +- Transition between surface levels: `200ms ease` — swift but not sudden, like a precision mechanism. + +### Brand Signature Depth + +- **M Tricolor Divider:** 4px horizontal stripe (`#1A8FD4` / `#1C69D4` / `#E22718`). Only in M-model contexts and motorsport badges. Never as a CTA fill, never as a general decorative element. This is a controlled accent — the engineering stripe on a cam cover, not a racing stripe on the hood. + +--- + +## 7. Do's and Don'ts + +**Do:** + +- Use BMW Blue (`#1C69D4` light / `#3B8FE3` dark) as the single primary action color — it carries every CTA +- Set display headlines in weight 700 and body in Light 300 — the contrast is the editorial signature +- Use UPPERCASE letter-spaced links ("LEARN MORE ›") as inline CTAs — the machined-precision voice +- Alternate light and dark bands in deliberate rhythm — no two consecutive same-mode sections +- Place model card photos on `#FAFAFA` plates with the title beneath — the standard BMW corporate pattern +- Hold section rhythm at 80px — the corporate heartbeat +- Use the warm dark navy (`#1A2129`) for dark surfaces — it carries Bavarian heritage, not emptiness +- Let photography do the heavy lifting — studio-lit vehicle renders and cinematic environmental shots +- Use rectangular 0px-radius for all interactive elements — engineered, not decorated +- Reserve the M tricolor stripe exclusively for M-model contexts + +**Don't:** + +- Do not use pure black (`#000000`) for dark mode backgrounds — BMW's dark surfaces are warm navy, not void +- Do not use pill or rounded buttons — 0px rectangular is the brand button +- Do not add drop shadows to cards or any element — depth comes from color-block contrast and photography +- Do not drop display weight below 700 or raise body weight above 300 — the duo is fixed +- Do not use weight 500 — it is absent from the system; choose 400 or 700 +- Do not use negative letter-spacing — BMW Type works at default tracking; tightening reads off-brand +- Do not use more than one brand accent color per page — BMW Blue carries all primary actions +- Do not use the M tricolor stripe as a CTA fill or general decoration — divider and accent role only +- Do not place text over busy photography without a gradient overlay scrim +- Do not use italic for emphasis — use weight contrast or size contrast instead +- Do not mix rounded and rectangular elements — if one element is 0px radius, all must be + +--- + +## 8. Responsive Behavior + +### Breakpoints + +| Name | Width | Behavior | +|------|-------|----------| +| Mobile | `< 768px` | Single column, stacked. Hero headline reduces to 40px. Nav collapses to hamburger with full-screen sheet. Model card grid 1-up. Configurator filters scroll horizontally. Footer 4-col to 1-col. | +| Tablet | `768–1024px` | Secondary nav hides under "More". Model card 2-up. Inventory 2-up. Hero headline 48px. | +| Desktop | `1024–1440px` | Full top-nav. 4-up or 5-up model card grid. Inventory 3-up. Full configurator UI. Hero headline 64px. | +| Wide | `> 1440px` | Same as desktop. Content fixed at 1440px max-width. Gutters absorb remaining space. | + +### Mobile-Specific Rules + +- Hero headline: `clamp(40px, 5vw, 64px)` — never below 40px +- Model card grid collapses to single column with full-width cards +- Configurator filter chip row scrolls horizontally — no wrapping +- Bottom sticky CTA bar may appear on mobile (transparent dark navy, white text) +- Touch targets: minimum 48x48px (above WCAG AAA) +- Text input height: 48px +- Category tabs: 12px vertical padding for tap area > 44px +- Hero photography shifts to more vertical crop (art direction for mobile aspect ratios) +- Inventory photos may shift from 16:9 to 4:3 on mobile + +### Image Behavior + +- Model renders scale at every breakpoint while preserving native aspect ratios +- Hero photography crops to focus on vehicle front on mobile; full side profile on desktop +- The M tricolor stripe stays at 4px height across every breakpoint + +### Dark Mode Switching + +Dark mode is the premium default for product configurator and model detail pages. Use `prefers-color-scheme` media query for automatic switching; always provide a manual toggle. All color tokens swap simultaneously. Photography may shift between modes — BMW often uses more dramatic, low-key imagery in dark mode. Transition between modes should be instant (no animation on color swap). diff --git a/addons/officials/crew/designer/skills/design-system-picker/design-systems/figma.md b/addons/officials/crew/designer/skills/design-system-picker/design-systems/figma.md new file mode 100644 index 00000000..a41ce72e --- /dev/null +++ b/addons/officials/crew/designer/skills/design-system-picker/design-systems/figma.md @@ -0,0 +1,270 @@ +# Figma Design System + +## 1. Visual Theme & Atmosphere + +Figma's visual identity is built on **creative confidence**: a vibrant, multi-color palette that feels playful without sacrificing professionalism. The aesthetic channels a design tool that knows its users are visually literate, so it rewards attention with rich color and purposeful asymmetry rather than safe neutrality. + +Key atmosphere traits: +- **Energetic optimism** -- saturated hues communicate possibility and creative freedom +- **Structured playfulness** -- bright colors are balanced by generous whitespace and a clean grid, never chaotic +- **Tool-first confidence** -- the UI feels like it belongs to a product you trust with your craft; chrome is minimal, content is foregrounded +- **Inclusive warmth** -- rounded geometry and warm tones keep the brand approachable despite its professional depth + +## 2. Color Palette & Roles + +### Core Brand Colors + +| Semantic Name | Hex | Role | +|---|---|---| +| `brand-primary` | `#F24E1E` | Primary actions, CTAs, key highlights, logo mark | +| `brand-red` | `#FF7262` | Secondary accent, hover states on primary, illustration fills | +| `brand-purple` | `#A259FF` | Feature emphasis, badges, decorative gradient endpoints | +| `brand-green` | `#0ACF83` | Success states, positive indicators, growth metrics | +| `brand-blue` | `#1ABCFE` | Informational elements, links, data visualization | + +### Surface & Neutral Colors (Dark Mode Primary) + +| Semantic Name | Hex | Role | +|---|---|---| +| `surface-base` | `#1E1E1E` | Page background, canvas | +| `surface-raised` | `#2C2C2C` | Cards, panels, elevated containers | +| `surface-overlay` | `#3C3C3C` | Modals, dropdowns, popover backgrounds | +| `surface-hover` | `#4A4A4A` | Hover states on raised surfaces | +| `border-subtle` | `#3C3C3C` | Default borders, dividers | +| `border-strong` | `#5C5C5C` | Active borders, input focus rings | +| `text-primary` | `#FFFFFF` | Headings, primary body text | +| `text-secondary` | `#B3B3B3` | Captions, descriptions, muted labels | +| `text-tertiary` | `#808080` | Placeholders, disabled text, hints | + +### Surface & Neutral Colors (Light Mode) + +| Semantic Name | Hex | Role | +|---|---|---| +| `surface-base` | `#FFFFFF` | Page background | +| `surface-raised` | `#F5F5F5` | Cards, panels | +| `surface-overlay` | `#E8E8E8` | Modals, dropdowns | +| `border-subtle` | `#E5E5E5` | Default borders | +| `border-strong` | `#CCCCCC` | Active borders | +| `text-primary` | `#1E1E1E` | Headings, primary body text | +| `text-secondary` | `#666666` | Captions, descriptions | +| `text-tertiary` | `#999999` | Placeholders, disabled text | + +### Semantic Colors + +| Semantic Name | Hex | Role | +|---|---|---| +| `color-success` | `#0ACF83` | Confirmations, saved states, valid inputs | +| `color-warning` | `#FF9F43` | Caution alerts, unsaved changes | +| `color-error` | `#F24E1E` | Error states, destructive actions, validation failures | +| `color-info` | `#1ABCFE` | Tooltips, informational banners, help indicators | + +### Gradient Presets + +| Name | Value | Usage | +|---|---|---| +| `gradient-brand` | `linear-gradient(135deg, #F24E1E 0%, #A259FF 100%)` | Hero sections, feature highlights | +| `gradient-rainbow` | `linear-gradient(135deg, #F24E1E 0%, #A259FF 33%, #1ABCFE 66%, #0ACF83 100%)` | Brand moments, event banners | +| `gradient-warm` | `linear-gradient(135deg, #F24E1E 0%, #FF7262 100%)` | CTA backgrounds, emphasis panels | +| `gradient-cool` | `linear-gradient(135deg, #1ABCFE 0%, #A259FF 100%)` | Secondary feature blocks | + +## 3. Typography Rules + +### Font Stack + +- **Display / Headlines**: `"Inter", system-ui, -apple-system, sans-serif` +- **Body**: `"Inter", system-ui, -apple-system, sans-serif` +- **Code / Monospace**: `"JetBrains Mono", "Fira Code", "Consolas", monospace` + +Inter is used across all weights, with dramatic weight contrast creating hierarchy rather than switching font families. + +### Type Scale + +| Level | Size | Weight | Line Height | Letter Spacing | Usage | +|---|---|---|---|---|---| +| `display` | `clamp(3rem, 5vw, 5rem)` | 800 | 1.05 | -0.03em | Hero headlines, page titles | +| `h1` | `clamp(2.25rem, 3.5vw, 3.5rem)` | 700 | 1.1 | -0.02em | Section titles | +| `h2` | `clamp(1.75rem, 2.5vw, 2.5rem)` | 700 | 1.15 | -0.015em | Subsection titles | +| `h3` | `clamp(1.25rem, 1.5vw, 1.75rem)` | 600 | 1.2 | -0.01em | Card titles, feature headings | +| `body-lg` | `1.125rem` | 400 | 1.6 | 0 | Lead paragraphs, introductions | +| `body` | `1rem` | 400 | 1.6 | 0 | Default body text | +| `body-sm` | `0.875rem` | 400 | 1.5 | 0.005em | Captions, metadata, helper text | +| `caption` | `0.75rem` | 500 | 1.4 | 0.01em | Labels, badges, timestamps | +| `code` | `0.875rem` | 400 | 1.5 | 0 | Inline code, code blocks | + +### Rules + +- Never use weight below 400 for body text; 300 is reserved for decorative display only +- Headlines use tight negative letter-spacing; body text uses neutral or slightly positive tracking +- Maximum line length: 65ch for body text, 40ch for captions +- Use weight jumps (400 to 700) for emphasis rather than italic or underline in body text +- Code snippets always use monospace with a subtle background tint (`rgba(162, 89, 255, 0.08)` in dark mode) + +## 4. Component Stylings + +### Buttons + +| Variant | Background | Text | Border | Radius | Hover | +|---|---|---|---|---|---| +| Primary | `#F24E1E` | `#FFFFFF` | none | 8px | `#D4411A`, translateY(-1px) | +| Secondary | `transparent` | `#FFFFFF` | `#5C5C5C` | 8px | border `#F24E1E`, text `#F24E1E` | +| Ghost | `transparent` | `#B3B3B3` | none | 8px | bg `rgba(255,255,255,0.06)` | +| Brand Gradient | `gradient-brand` | `#FFFFFF` | none | 8px | `gradient-warm`, translateY(-1px) | + +- Padding: `12px 24px` default, `10px 20px` compact +- Transition: `all 150ms cubic-bezier(0.16, 1, 0.3, 1)` +- Active state: `translateY(1px)`, opacity 0.9 +- Focus ring: `2px solid #1ABCFE`, offset 2px +- Icon buttons: 40x40px square, `border-radius: 10px` + +### Cards + +- Background: `surface-raised` (`#2C2C2C` dark / `#F5F5F5` light) +- Border: `1px solid border-subtle` +- Border-radius: `12px` +- Padding: `24px` +- Hover: `translateY(-2px)`, `box-shadow: 0 8px 30px rgba(0,0,0,0.3)` +- Transition: `transform 200ms cubic-bezier(0.16, 1, 0.3, 1), box-shadow 200ms ease` +- Featured cards: left border accent `3px solid` using brand color matching the content theme + +### Inputs + +- Background: `surface-base` (`#1E1E1E` dark / `#FFFFFF` light) +- Border: `1px solid border-subtle` +- Border-radius: `8px` +- Padding: `10px 14px` +- Focus: border `#1ABCFE`, `box-shadow: 0 0 0 3px rgba(26, 188, 254, 0.15)` +- Error: border `#F24E1E`, error message in `color-error` below input +- Placeholder: `text-tertiary` + +### Tags / Badges + +- Border-radius: `6px` +- Padding: `4px 10px` +- Font: `caption` (0.75rem, 500) +- Color variants use brand colors with 12% opacity backgrounds: + - Purple badge: bg `rgba(162,89,255,0.12)`, text `#A259FF` + - Green badge: bg `rgba(10,207,131,0.12)`, text `#0ACF83` + - Blue badge: bg `rgba(26,188,254,0.12)`, text `#1ABCFE` + - Red badge: bg `rgba(242,78,30,0.12)`, text `#F24E1E` + +### Tooltips + +- Background: `#4A4A4A` +- Text: `#FFFFFF`, `body-sm` +- Border-radius: `6px` +- Padding: `6px 12px` +- Arrow: 6px CSS triangle +- Delay: 300ms show, 100ms hide + +### Toggles / Switches + +- Track: `#3C3C3C` off, `#0ACF83` on +- Knob: `#FFFFFF`, 18px diameter +- Track height: 24px, width 44px, border-radius 12px +- Transition: `background 200ms ease, transform 200ms cubic-bezier(0.16, 1, 0.3, 1)` + +## 5. Layout Principles + +### Grid + +- Desktop: 12-column grid, 24px gutters, 64px max outer margin +- Tablet: 8-column grid, 20px gutters +- Mobile: 4-column grid, 16px gutters +- Max content width: `1200px` (centered) +- Wide layout: `1440px` for hero and showcase sections + +### Spacing Scale + +| Token | Value | Usage | +|---|---|---| +| `space-1` | 4px | Inline gaps, icon padding | +| `space-2` | 8px | Tight component spacing | +| `space-3` | 12px | Form element gaps | +| `space-4` | 16px | Component internal padding | +| `space-5` | 24px | Card padding, standard gaps | +| `space-6` | 32px | Section sub-spacing | +| `space-7` | 48px | Between related sections | +| `space-8` | 64px | Between distinct sections | +| `space-9` | 96px | Major section dividers | +| `space-10` | 128px | Hero-level vertical rhythm | + +### Layout Patterns + +- **Z-pattern** for marketing pages: headline + CTA top-left, visual top-right, content flows diagonally +- **Feature grid**: 3-column cards with icon, title, description; each card may use a different brand accent color for its icon to reinforce the multi-color identity +- **Asymmetric split**: 60/40 text-to-visual ratio on feature sections, alternating sides +- **Full-bleed heroes**: content constrained to grid but background colors/gradients extend edge-to-edge +- Sticky navigation with `backdrop-filter: blur(12px)` and `background: rgba(30,30,30,0.85)` + +## 6. Depth & Elevation + +Figma's depth system is restrained and functional, favoring subtle surface differentiation over dramatic shadows. + +### Elevation Levels + +| Level | Shadow (Dark Mode) | Shadow (Light Mode) | Usage | +|---|---|---|---| +| Level 0 | none | none | Base canvas, flat surfaces | +| Level 1 | `0 1px 3px rgba(0,0,0,0.3)` | `0 1px 3px rgba(0,0,0,0.08)` | Cards at rest | +| Level 2 | `0 4px 12px rgba(0,0,0,0.3)` | `0 4px 12px rgba(0,0,0,0.1)` | Hovered cards, raised panels | +| Level 3 | `0 8px 30px rgba(0,0,0,0.4)` | `0 8px 30px rgba(0,0,0,0.12)` | Modals, dropdowns | +| Level 4 | `0 16px 50px rgba(0,0,0,0.5)` | `0 16px 50px rgba(0,0,0,0.16)` | Toast notifications, spotlight overlays | + +### Depth Through Color + +- Prefer surface color shifts (darker backgrounds for elevation) over heavy shadows +- Overlay modals use `backdrop-filter: blur(8px)` on the scrim layer +- Glassmorphism accents: `background: rgba(44,44,44,0.7)`, `backdrop-filter: blur(16px)`, `border: 1px solid rgba(255,255,255,0.08)` -- use sparingly for floating toolbars and context menus only + +### Overlap & Layering + +- Hero sections may overlap into the next section by `-48px` to `--space-7` with a rounded bottom container +- Illustration elements can break out of their container bounds by up to 20% for visual energy +- Brand color shapes (circles, rounded rectangles at 10% opacity) may overlap content as decorative background layers + +## 7. Do's and Don'ts + +### Do + +- Use the full brand color set (orange, red, purple, green, blue) to differentiate features and sections -- the palette is meant to be used, not hoarded +- Apply generous whitespace around headlines and CTAs to let the vibrant colors breathe +- Use Inter weight 700-800 for headlines and 400 for body to create clear typographic hierarchy +- Pair dark surfaces with saturated accent colors -- they need the contrast to pop +- Use gradient-brand for primary CTAs and hero moments; use solid brand-primary for repeated UI elements +- Round corners consistently: 8px for inputs and small elements, 12px for cards, 16px+ for hero containers +- Use micro-interactions (scale 1.02 on hover, 150-200ms) to reinforce the playful-but-precise personality +- Let illustrations and visuals carry color; keep chrome (navigation, toolbars) neutral + +### Don't + +- Don't use all five brand colors on a single component -- pick one accent per element +- Don't apply gradients to body text or small UI labels; reserve them for backgrounds and CTAs +- Don't use drop shadows on text -- Figma's brand never uses text shadows +- Don't mix warm (orange/red) and cool (blue/purple) accents as adjacent equals without a neutral spacer +- Don't use pure black (`#000000`) for text on dark backgrounds; use `#FFFFFF` or `text-primary` instead +- Don't over-blur -- limit `backdrop-filter: blur()` to 16px maximum and use only on overlay elements +- Don't use rounded corners below 6px -- the system avoids sharp edges entirely +- Don't place saturated accent-colored text on saturated backgrounds; maintain sufficient contrast (WCAG AA minimum) +- Don't animate `width`, `height`, `top`, or `left`; use `transform` and `opacity` for all motion + +## 8. Responsive Behavior + +### Breakpoints + +| Name | Min Width | Columns | Gutter | Typical Target | +|---|---|---|---|---| +| `mobile` | 0 | 4 | 16px | Phones (<640px) | +| `tablet` | 640px | 8 | 20px | Tablets, small laptops | +| `desktop` | 1024px | 12 | 24px | Laptops, desktops | +| `wide` | 1440px | 12 | 24px | Large monitors | + +### Adaptation Rules + +- **Hero section**: Stacks vertically on mobile (headline over visual), maintains side-by-side from tablet up. Font size scales via `clamp()` across all breakpoints. +- **Feature grid**: 3 columns on desktop, 2 on tablet, 1 on mobile. Cards maintain consistent padding but reduce to `20px` on mobile. +- **Navigation**: Full horizontal nav on desktop, hamburger menu with slide-in drawer on mobile. Drawer uses `surface-raised` background with `border-left` accent in `brand-primary`. +- **CTAs**: Full-width on mobile, auto-width on tablet and up. Minimum touch target: 44px height on mobile. +- **Images and illustrations**: `width: 100%` with `aspect-ratio` preserved. Hero visuals may be hidden or replaced with a simplified version below `tablet` breakpoint if they contain fine detail. +- **Typography scaling**: All heading sizes use `clamp()` to fluidly scale between breakpoints. Body text stays at `1rem` across all sizes. +- **Color accents**: Brand color shapes used as decorative backgrounds are hidden on mobile to reduce visual noise. Gradient hero backgrounds simplify to solid `brand-primary` on mobile. +- **Spacing reduction**: `space-9` and `space-10` sections collapse to `space-7` on tablet and `space-6` on mobile. Card grids reduce gap from `space-5` to `space-4` to `space-3` respectively. diff --git a/addons/officials/crew/designer/skills/design-system-picker/design-systems/framer.md b/addons/officials/crew/designer/skills/design-system-picker/design-systems/framer.md new file mode 100644 index 00000000..66047429 --- /dev/null +++ b/addons/officials/crew/designer/skills/design-system-picker/design-systems/framer.md @@ -0,0 +1,157 @@ +# Framer Design System + +Bold black and electric blue. Motion-first, design-forward. The interface feels alive — every transition is intentional, every hover is a micro-showcase. Built for people who build websites. + +--- + +## 1. Visual Theme & Atmosphere + +Dark, high-contrast surfaces with electric blue punctuating key interactions. The aesthetic says "creative tool" — confident, slightly playful, never corporate. Smooth motion is non-negotiable; static states feel broken. Interactive showcases and live previews are the content, not decoration around content. + +**Keywords:** bold, electric, motion-first, interactive, creative, confident + +--- + +## 2. Color Palette & Roles + +| Name | Hex | Role | +|------|-----|------| +| Electric Blue | `#0055FF` | Primary accent, CTAs, active states, links | +| Deep Black | `#0A0A0A` | Primary background | +| Rich Black | `#141414` | Card/surface background | +| Elevated Black | `#1E1E1E` | Hover surfaces, input backgrounds | +| Pure White | `#FFFFFF` | Primary text on dark | +| Soft White | `#B0B0B0` | Secondary text, placeholders | +| Muted Gray | `#6B6B6B` | Tertiary text, disabled states | +| Border Subtle | `#2A2A2A` | Dividers, card borders | +| Blue Glow | `#0055FF` at 20% opacity | Focus rings, hover glow | +| Success | `#00C853` | Positive actions, confirmations | +| Warning | `#FFAB00` | Caution states | +| Error | `#FF1744` | Destructive actions, validation errors | + +**Rule:** Electric Blue is the soul. Use it for every interactive affordance. Never dilute it with gradients — it should hit pure and saturated. + +--- + +## 3. Typography Rules + +**Primary:** Inter (or fallback: system sans-serif) +**Display:** Fraktion (or fallback: Inter with tight tracking) + +| Element | Font | Weight | Size | Tracking | Case | +|---------|------|--------|------|----------|------| +| Display hero | Fraktion | 600 | clamp(48px, 5vw, 80px) | -0.04em | Title | +| Section title | Inter | 700 | clamp(28px, 2.5vw, 44px) | -0.02em | Title | +| Subheading | Inter | 600 | 20px | -0.01em | Sentence | +| Body | Inter | 400 | 16px | 0 | Sentence | +| Body small | Inter | 400 | 14px | 0 | Sentence | +| Caption | Inter | 500 | 12px | 0.02em | Uppercase | +| Code / mono | JetBrains Mono | 400 | 14px | 0 | — | + +**Rules:** +- Display headlines use tight negative tracking to feel punchy and dense. +- Body line-height: 1.6. Headlines: 1.1. +- Bold (700) for emphasis, never italic. Italic reserved for captions only. +- Code snippets always use mono font with `#1E1E1E` background. + +--- + +## 4. Component Stylings + +### Buttons +- **Primary CTA:** `#0055FF` background, white text, `border-radius: 8px`, padding `12px 24px`, weight 600. On hover: scale 1.02, box-shadow `0 0 20px rgba(0,85,255,0.4)`. +- **Secondary CTA:** `#1E1E1E` background, white text, 1px `#2A2A2A` border. On hover: border becomes `#0055FF`. +- **Ghost:** Transparent, white text, no border. On hover: text becomes `#0055FF`. +- **Icon button:** 40x40px, `border-radius: 10px`, `#1E1E1E` bg. On hover: bg `#2A2A2A`. + +### Navigation +- Fixed top bar, `height: 64px`, backdrop-blur over dark content. +- Logo left, nav center, CTA right. +- Active nav link: `#0055FF` text, 2px bottom border. +- Hover: text color transition 150ms. + +### Cards +- Background: `#141414`, `border-radius: 12px`, 1px `#2A2A2A` border. +- Padding: 24px. Hover: border becomes `#0055FF`, subtle translateY(-2px) with 200ms ease-out. +- No box-shadow at rest. Glow on hover only. + +### Input Fields +- Background: `#1E1E1E`, `border-radius: 8px`, 1px `#2A2A2A` border. +- Focus: border `#0055FF`, box-shadow `0 0 0 3px rgba(0,85,255,0.15)`. +- Placeholder: `#6B6B6B`. + +### Interactive Showcases +- Live preview panels with `border-radius: 16px`, `border: 1px #2A2A2A`. +- Tab bar above preview: `#141414` bg, active tab has `#0055FF` bottom border. +- Preview area: `#0A0A0A` bg. + +### Code Blocks +- `#1E1E1E` background, `border-radius: 8px`, JetBrains Mono. +- Line numbers: `#6B6B6B`. Syntax highlighting: blue `#0055FF`, green `#00C853`, yellow `#FFAB00`, red `#FF1744`. + +--- + +## 5. Layout Principles + +- **Max content width:** 1200px centered. Wide showcases: 1400px. +- **Grid:** 12-column, 16px gutters on desktop, 8px on mobile. +- **Sections alternate rhythm:** tight (64px padding) for feature stacks, generous (120px) for hero sections. +- **Asymmetric layouts:** 7/5 or 8/4 splits for text + interactive preview. +- **Sticky sidebars** on documentation pages (240px width). +- **Showcase sections** take near-full-width to let interactive demos breathe. + +--- + +## 6. Depth & Elevation + +Depth is expressed through layering and glow, not shadows: + +| Level | Surface | Use | +|-------|---------|-----| +| 0 | `#0A0A0A` | Page background | +| 1 | `#141414` | Cards, panels | +| 2 | `#1E1E1E` | Inputs, elevated cards on hover | +| 3 | `#0055FF` glow | Focus, active states | + +- **No ambient shadows.** Elevation = background lightness change. +- **Blue glow** on interactive elements creates perceived depth through color, not shadow. +- **Backdrop-blur** on fixed nav and modals (blur(12px), 80% opacity dark). + +--- + +## 7. Do's and Don'ts + +**Do:** +- Add motion to every state change (hover, focus, appear, exit) — 150–300ms ease-out +- Use Electric Blue consistently for all interactive elements +- Let interactive showcases be the hero content +- Use tight tracking on headlines for punch +- Round corners on cards and inputs (8–12px) — it softens the dark aesthetic +- Provide keyboard focus rings with blue glow + +**Don't:** +- Use drop shadows for elevation — use surface color instead +- Apply Electric Blue to large background areas (it loses impact) +- Use more than one animation timing per element +- Place long paragraphs in dark surfaces without line-height 1.6 +- Use gray for interactive elements — if it responds to input, it should hint blue +- Create static pages — motion is the brand + +--- + +## 8. Responsive Behavior + +| Breakpoint | Behavior | +|-----------|----------| +| < 640px | Single column. Interactive previews stack below text. Nav collapses to hamburger. Card grid: 1 column. | +| 640–768px | Single column. Preview panels go full-width. Sidebar navigation hidden (use top tabs). | +| 768–1024px | 2-column card grid. Side-by-side text+preview appears. Sidebar nav at 200px. | +| 1024–1440px | 3-column card grid. Full asymmetric splits. Sidebar at 240px. | +| > 1440px | Content max-width 1200px (1400px for showcases), centered. | + +**Mobile-specific rules:** +- Interactive showcases become static screenshots with a "Try it" CTA linking to the full experience +- Hover states replaced by tap states with 100ms scale pulse +- Blue glow on focus adapts to `0 0 0 2px rgba(0,85,255,0.3)` (smaller ring) +- Touch targets: minimum 44x44px +- Bottom sheet for navigation on mobile instead of hamburger dropdown diff --git a/addons/officials/crew/designer/skills/design-system-picker/design-systems/ibm.md b/addons/officials/crew/designer/skills/design-system-picker/design-systems/ibm.md new file mode 100644 index 00000000..2c8a286f --- /dev/null +++ b/addons/officials/crew/designer/skills/design-system-picker/design-systems/ibm.md @@ -0,0 +1,419 @@ +# IBM Design System + +Enterprise authority through structured restraint. IBM's visual language is built on the Carbon Design System — an open-source framework where every pixel earns its place through utility, not decoration. The 8-bar motif from the IBM logo is the design philosophy made manifest: horizontal stripes of equal weight, orderly progression, nothing ornamental. Dark mode is not a luxury mode — it is a first-class citizen with dedicated theme tokens (g90, g100). Information density is a virtue, not a vice. Trust is communicated through consistency, not through charisma. + +--- + +## 1. Visual Theme & Atmosphere + +IBM's visual language communicates institutional trust and engineering rigor — the confidence of a company that has defined enterprise computing for over a century. Surfaces are flat and structured. Color is restrained: IBM Blue carries every primary action, gray-100 anchors every dark surface, and the 12-color palette family exists for data visualization and status communication, never for decoration. Typography works at two tempos: Productive (compact, task-focused, 14px default) for tools and dashboards; Expressive (fluid, spacious, larger scales) for marketing and storytelling. Both tempos are calibrated for IBM Plex, IBM's open-source typeface. + +The page is a data surface. White and gray-10 are the light canvases; gray-90 and gray-100 are the dark canvases. Layers are differentiated by background color shifts, not shadows. The 8px grid governs every spacing decision. The 2x Grid system provides 16 columns at desktop with five responsive breakpoints. Data tables, forms, and dashboards are the native content — not hero images. Photography, when used, is secondary to information. The 8-bar stripe motif appears as a controlled signature, not as wallpaper. + +**Keywords:** enterprise authority, structured restraint, information density, institutional trust, productive precision, Carbon discipline, 8-bar rhythm + +--- + +## 2. Color Palette & Roles + +IBM's color system is organized into 12 scale families, each with 10 stops (10 through 100, where 10 is lightest and 100 is deepest). Carbon defines four themes: White (high-contrast light), g10 (low-contrast light), g90 (low-contrast dark), g100 (high-contrast dark). Blue 60 (`#0F62FE`) is the canonical interactive color across all themes. + +### Light Mode (White / g10) + +| Token | Hex | Role | +|-------|-----|------| +| `--color-canvas` | `#FFFFFF` | Page background — the White theme default | +| `--color-surface` | `#F4F4F4` | Container background on canvas — gray-10, the g10 theme background | +| `--color-surface-elevated` | `#FFFFFF` | Elevated container on gray-10 surface — cards, modals | +| `--color-surface-strong` | `#E0E0E0` | Subtle border, tertiary background — gray-20 | +| `--color-ink` | `#161616` | Primary text — gray-100, high-contrast body and headings | +| `--color-body` | `#393939` | Default running text — gray-80 | +| `--color-muted` | `#525252` | Secondary text, labels — gray-70 | +| `--color-muted-soft` | `#6F6F6F` | Tertiary text, placeholder — gray-60 | +| `--color-primary` | `#0F62FE` | IBM Blue — all primary CTAs, links, active elements, interactive accent. blue-60 | +| `--color-primary-hover` | `#0043CE` | Hover state for primary — blue-70 | +| `--color-primary-active` | `#002D9C` | Active/pressed state for primary — blue-80 | +| `--color-primary-subtle` | `#EDF5FF` | Light tint background for selected/highlighted items — blue-10 | +| `--color-on-primary` | `#FFFFFF` | White text on blue buttons | +| `--color-hairline` | `#C6C6C6` | 1px dividers, input borders — gray-30 | +| `--color-hairline-strong` | `#8D8D8D` | Medium-contrast border, emphasis — gray-50 | +| `--color-accent` | `#8A3FFC` | Secondary accent — purple-60. Data viz, supplementary highlights | +| `--color-accent-hover` | `#6929C4` | Accent hover — purple-70 | +| `--color-success` | `#24A148` | Positive, available — green-50 | +| `--color-warning` | `#F1C21B` | Caution — yellow-30 | +| `--color-error` | `#DA1E28` | Destructive, error — red-60 | + +### Dark Mode (g90 / g100) + +| Token | Hex | Role | +|-------|-----|------| +| `--color-canvas` | `#161616` | Page background — gray-100, the g100 theme default | +| `--color-surface` | `#262626` | Container background on dark canvas — gray-90, the g90 theme default | +| `--color-surface-elevated` | `#393939` | Elevated container on dark surface — gray-80 | +| `--color-surface-strong` | `#525252` | Subtle border on dark, tertiary dark background — gray-70 | +| `--color-ink` | `#F4F4F4` | Primary text on dark — gray-10 | +| `--color-body` | `#C6C6C6` | Default running text on dark — gray-30 | +| `--color-muted` | `#A8A8A8` | Secondary text on dark — gray-40 | +| `--color-muted-soft` | `#8D8D8D` | Tertiary text on dark — gray-50 | +| `--color-primary` | `#0F62FE` | IBM Blue — unchanged across themes. blue-60 is universal | +| `--color-primary-hover` | `#4589FF` | Hover on dark — blue-50, shifted lighter for dark backgrounds | +| `--color-primary-active` | `#78A9FF` | Active on dark — blue-40 | +| `--color-primary-subtle` | `#001141` | Dark tint background for selected items — blue-100 | +| `--color-on-primary` | `#FFFFFF` | White text on blue buttons (unchanged) | +| `--color-hairline` | `#393939` | 1px dividers on dark — gray-80 | +| `--color-hairline-strong` | `#6F6F6F` | Emphasis borders on dark — gray-60 | +| `--color-accent` | `#A56EFF` | Secondary accent on dark — purple-50, shifted lighter | +| `--color-accent-hover` | `#BE95FF` | Accent hover on dark — purple-40 | +| `--color-success` | `#42BE65` | Positive on dark — green-40, shifted lighter | +| `--color-warning` | `#F1C21B` | Caution on dark — yellow-30, unchanged | +| `--color-error` | `#FA4D56` | Error on dark — red-50, shifted lighter | + +**Rule:** IBM Blue (`#0F62FE`, blue-60) is the one color that does not shift between light and dark themes. It is the fixed North Star — every other accent color shifts lighter on dark backgrounds to maintain contrast and legibility. The dark palette uses the gray scale inverted: gray-10 becomes text, gray-100 becomes canvas. This inversion is systematic, not aesthetic. + +### Full Color Families (for Data Visualization) + +| Family | 10 | 20 | 30 | 40 | 50 | 60 | 70 | 80 | 90 | 100 | +|--------|------|------|------|------|------|------|------|------|------|------| +| Gray | `#F4F4F4` | `#E0E0E0` | `#C6C6C6` | `#A8A8A8` | `#8D8D8D` | `#6F6F6F` | `#525252` | `#393939` | `#262626` | `#161616` | +| Cool Gray | `#F2F4F8` | `#DDE1E6` | `#C1C7CD` | `#A2A9B0` | `#878D96` | `#697077` | `#4D5358` | `#343A3F` | `#21272A` | `#121619` | +| Blue | `#EDF5FF` | `#D0E2FF` | `#A6C8FF` | `#78A9FF` | `#4589FF` | `#0F62FE` | `#0043CE` | `#002D9C` | `#001D6C` | `#001141` | +| Red | `#FFF1F1` | `#FFD7D9` | `#FFB3B8` | `#FF8389` | `#FA4D56` | `#DA1E28` | `#A2191F` | `#750E13` | `#520408` | `#2D0709` | +| Green | `#DEFBE6` | `#A7F0BA` | `#6FDC8C` | `#42BE65` | `#24A148` | `#198038` | `#0E6027` | `#044317` | `#022D0D` | `#071908` | +| Yellow | `#FCF4D6` | `#FDDC69` | `#F1C21B` | `#D2A106` | `#B28600` | `#8E6A00` | `#684E00` | `#483700` | `#302400` | `#1C1500` | +| Purple | `#F6F2FF` | `#E8DAFF` | `#D4BBFF` | `#BE95FF` | `#A56EFF` | `#8A3FFC` | `#6929C4` | `#491D8B` | `#31135E` | `#1C0F30` | +| Cyan | `#E5F6FF` | `#BAE6FF` | `#82CFFF` | `#33B1FF` | `#1192E8` | `#0072C3` | `#00539A` | `#003A6D` | `#012749` | `#061727` | +| Teal | `#D9FBFB` | `#9EF0F0` | `#3DDBD9` | `#08BDBA` | `#009D9A` | `#007D79` | `#005D5D` | `#004144` | `#022B30` | `#081A1C` | +| Magenta | `#FFF0F7` | `#FFD6E8` | `#FFAFD2` | `#FF7EB6` | `#EE5396` | `#D02670` | `#9F1853` | `#740937` | `#510224` | `#2A0A18` | +| Orange | `#FFF2E8` | `#FFD9BE` | `#FFB784` | `#FF832B` | `#EB6200` | `#BA4E00` | `#8A3800` | `#5E2900` | `#3E1A00` | `#231000` | + +**Data visualization rule:** Use stops 40-70 for chart fills (sufficient contrast on both light and dark backgrounds). Use stops 10-20 for background tints. Use stops 80-100 for text labels on light charts. Never use stops 10-30 as text — insufficient contrast. + +--- + +## 3. Typography Rules + +**Primary:** IBM Plex Sans (IBM's open-source typeface, designed by Mike Abbink) +**Mono:** IBM Plex Mono (code, data, technical content) +**Serif:** IBM Plex Serif (long-form editorial, rarely used in product UI) +**Fallback stack:** `"IBM Plex Sans", "Helvetica Neue", Arial, sans-serif` + +### Productive Type Set (Product UI — Dashboards, Tools, Forms) + +| Token | Size | Weight | Line Height | Tracking | Usage | +|-------|------|--------|-------------|----------|-------| +| `--text-heading-05` | `32px` | 400 / Regular | 40px | 0 | Page titles, top-level headings in dashboards | +| `--text-heading-04` | `28px` | 400 / Regular | 36px | 0 | Section headings, panel titles | +| `--text-heading-03` | `24px` | 400 / Regular | 32px | 0 | Sub-section headings, card titles | +| `--text-heading-02` | `20px` | 400 / Regular | 28px | 0 | Component headings, group labels | +| `--text-heading-01` | `14px` | 600 / Semi-Bold | 18px | 0.16px | Small headings, field group labels, tile titles | +| `--text-body-02` | `16px` | 400 / Regular | 24px | 0 | Default body text, paragraphs, descriptions | +| `--text-body-01` | `14px` | 400 / Regular | 20px | 0.16px | Compact body — the product default. Table cells, form fields, lists | +| `--text-body-compact-02` | `16px` | 400 / Regular | 22px | 0 | Tighter line-height body for dense UI | +| `--text-body-compact-01` | `14px` | 400 / Regular | 18px | 0.16px | Tightest body — data tables, dense forms | +| `--text-label-02` | `14px` | 600 / Semi-Bold | 20px | 0 | Form labels, badge text, table headers | +| `--text-label-01` | `12px` | 400 / Regular | 16px | 0.32px | Captions, helper text, metadata, timestamps | +| `--text-helper-text` | `12px` | 400 / Regular | 16px | 0.32px | Inline help, validation messages | +| `--text-caption` | `12px` | 400 / Italic | 16px | 0.32px | Editorial captions only — never in product UI | +| `--text-code-02` | `16px` | 400 / Regular | 24px | 0.32px | Code blocks — IBM Plex Mono | +| `--text-code-01` | `14px` | 400 / Regular | 20px | 0.32px | Inline code — IBM Plex Mono | + +### Expressive Type Set (Marketing — Landing Pages, Campaigns, Storytelling) + +| Token | Size (lg) | Weight | Line Height | Tracking | Usage | +|-------|-----------|--------|-------------|----------|-------| +| `--text-display-03` | `64px` | 300 / Light | 72px | -0.5px | Hero headlines, campaign mastheads | +| `--text-display-02` | `48px` | 300 / Light | 56px | 0 | Section heroes, feature leads | +| `--text-display-01` | `36px` | 300 / Light | 44px | 0 | Feature titles, marketing sub-heads | +| `--text-expressive-heading-05` | `28px` | 600 / Semi-Bold | 36px | 0 | Marketing section headings | +| `--text-expressive-heading-04` | `24px` | 600 / Semi-Bold | 32px | 0 | Marketing sub-section headings | +| `--text-expressive-heading-03` | `20px` | 400 / Regular | 26px | 0 | Feature descriptions, intro paragraphs | +| `--text-expressive-heading-02` | `18px` | 600 / Semi-Bold | 24px | 0 | Card headings, callout titles | +| `--text-expressive-heading-01` | `14px` | 600 / Semi-Bold | 20px | 0.16px | Small callout headings, eyebrow text | +| `--text-expressive-paragraph-01` | `18px` | 400 / Regular | 28px | 0 | Marketing body, longer-form storytelling | +| `--text-quotation-02` | `24px` | 300 / Light | 34px | 0 | Pull quotes, testimonial text | +| `--text-quotation-01` | `18px` | 400 / Regular | 28px | 0 | Smaller pull quotes, inline citations | + +### Typography Principles + +- **14px is the product default.** IBM's product UI lives at 14px body text — not 16px. This is intentional: enterprise dashboards need information density, and 14px / 20px line-height maximizes visible data while remaining legible. This is the single biggest differentiator from consumer design systems. +- **Weight 400 is the workhorse.** Productive headings use Regular (400) weight — not Bold. Only label tokens and the smallest heading token (heading-01) use Semi-Bold (600). IBM trusts size and spacing for hierarchy, not weight. +- **Expressive mode uses Light (300) for display.** Marketing display headlines run at weight 300 with larger sizes — the inverse of the productive pattern. This creates the characteristic IBM marketing voice: large, light, confident. +- **IBM Plex Mono is mandatory for code and data.** Never use a generic monospace fallback when IBM Plex Mono is available. The typeface is designed to harmonize with Plex Sans at the same x-height. +- **Tracking is minimal.** Productive text uses 0-0.32px tracking. Expressive display uses -0.5px to 0. No large positive tracking — IBM is not a fashion brand. +- **Fluid sizing for expressive headings.** Display-03 scales from 36px at sm breakpoint to 64px at lg+. This is the only place Carbon uses fluid type. + +--- + +## 4. Component Stylings + +### Buttons + +**Primary (IBM Blue)** +```css +background: var(--color-primary); +color: var(--color-on-primary); +padding: 11px 16px; +height: 48px; +border-radius: 0px; +font-size: 14px; +font-weight: 400; +font-family: "IBM Plex Sans", sans-serif; +letter-spacing: 0; +border: none; +cursor: pointer; +transition: background 110ms ease; +``` +- Hover: `var(--color-primary-hover)` +- Active: `var(--color-primary-active)` +- Disabled: `background: var(--color-surface-strong); color: var(--color-muted-soft)` + +**Secondary (Outlined)** +```css +background: transparent; +color: var(--color-primary); +padding: 10px 15px; +height: 48px; +border-radius: 0px; +border: 1px solid var(--color-primary); +font-size: 14px; +font-weight: 400; +letter-spacing: 0; +``` +- Hover: `background: var(--color-primary-subtle)` + +**Tertiary (Ghost)** +```css +background: transparent; +color: var(--color-primary); +padding: 11px 16px; +height: 48px; +border-radius: 0px; +border: none; +font-size: 14px; +font-weight: 400; +``` +- Hover: `background: var(--color-surface)` on light; `background: var(--color-surface-elevated)` on dark + +**Danger** +```css +background: var(--color-error); +color: #FFFFFF; +padding: 11px 16px; +height: 48px; +border-radius: 0px; +font-size: 14px; +font-weight: 400; +border: none; +``` + +**Button rules:** No rounded corners — 0px border-radius is the Carbon standard. No icon-only buttons without a visible label. Buttons are 48px height (large) or 32px height (small variant). Weight is always 400 — buttons do not shout. + +### Navigation + +- **UI Shell (Top bar):** `height: 48px`, `background: var(--color-canvas)`. +- Left: IBM 8-bar logo mark. Center/left-aligned: side navigation trigger + page title. Right: actions, user avatar. +- Nav links: 14px / 400 / 0px tracking, `var(--color-ink)`. Active: `var(--color-primary)`. +- **Side Navigation:** Fixed left panel, `width: 256px` (expanded) / `64px` (collapsed, icon-only). `background: var(--color-canvas)`. Items: 14px / 400, `height: 32px` per item, with 4px left border indicator for active. +- No hamburger icon on desktop. Below 768px: side nav collapses to icon-only or becomes a sheet. +- Header may use `border-bottom: 1px solid var(--color-hairline)` for subtle separation. + +### Cards + +- **Tile:** `background: var(--color-surface-elevated)`, `border-radius: 0px`, `padding: 16px` or `24px`. No box-shadow in default state. +- **Clickable Tile:** Same as Tile + `border: 1px solid var(--color-hairline)`. Hover: `background: var(--color-surface)` on light. Active: `border-color: var(--color-primary)`. +- **Selectable Tile:** Selected state shows `border: 2px solid var(--color-primary)`. +- **Expandable Tile:** Expands vertically with chevron icon. Divider: 1px `var(--color-hairline)`. +- No border-radius on cards. No decorative shadows. Depth is communicated through background color differentiation. + +### Image Treatments + +- Photography is secondary to content. IBM product UI prioritizes data and text. +- When used: `width: 100%`, `object-fit: cover`. No rounded corners on images. +- Aspect ratios: 16:9 for hero, 3:2 for feature cards, 1:1 for avatars. +- No decorative image borders. No visible frame elements. +- Overlay gradient for text legibility only: `linear-gradient(to top, rgba(22, 22, 22, 0.8) 0%, transparent 60%)`. +- The 8-bar stripe motif may appear as a thin decorative band (4px per bar, 2px gap) at section boundaries — never as a background pattern or fill. + +### Data / Specs + +- Data tables are the centerpiece of IBM product UI. +- Table header: 12px / 600 / `var(--color-muted)` — uppercase, `letter-spacing: 0.32px`. +- Table cell: 14px / 400 / `var(--color-ink)` — the productive body-01 default. +- Row height: 48px (standard) / 32px (compact). +- Alternating row backgrounds: `var(--color-canvas)` and `var(--color-surface)`. +- Sortable columns show arrow icon in header. Filterable columns show filter icon. +- No decorative borders. Dividers: 1px `var(--color-hairline)` between rows only. +- Status indicators use the color families: green-50 for success, red-60 for error, yellow-30 for warning — as small dot icons or inline tags, never as row background fills. + +--- + +## 5. Layout Principles + +### Grid — The IBM 2x Grid + +Carbon's 2x Grid is a 16-column system at desktop with five breakpoints. Every dimension derives from the base 8px unit. + +| Breakpoint | Min Width | Columns | Gutter | Margin | +|------------|-----------|---------|--------|--------| +| sm | 320px | 4 | 16px | 16px | +| md | 672px | 8 | 16px | 16px | +| lg | 1056px | 16 | 24px | 24px | +| xlg | 1312px | 16 | 24px | 24px | +| max | 1584px | 16 | 32px | 32px | + +- **Max content width:** 1584px. Content is centered with margins absorbing remaining space. +- **16-column grid** at lg and above. 8-column at md. 4-column at sm. +- Column spans specified per breakpoint: `` for full-width content. +- Subgrid supported for nested layouts with alignment to parent columns. + +### Spacing System + +Base unit: **8px**. Every spacing token is a multiple of 8 or derived from the 2x/4x/8x progression. + +| Token | Value | Usage | +|-------|-------|-------| +| `--space-01` | 2px | Tightest internal gaps, icon-to-label | +| `--space-02` | 4px | Icon gaps, chip internal padding, inline spacing | +| `--space-03` | 8px | Base unit — component internal padding, tight element gaps | +| `--space-04` | 12px | Small component padding, form field internal | +| `--space-05` | 16px | Standard component padding, gutter at sm/md breakpoints | +| `--space-06` | 24px | Section internal gaps, gutter at lg/xlg, card padding | +| `--space-07` | 32px | Sub-section gaps, margin between components | +| `--space-08` | 40px | Section spacing, larger component margins | +| `--space-09` | 48px | Major section breaks, page-level vertical rhythm | +| `--space-10` | 64px | Section padding (marketing pages) | +| `--space-11` | 80px | Large section padding | +| `--space-12` | 96px | Maximum section padding — editorial/landing pages | +| `--space-13` | 160px | Rare — page-level hero spacing only | + +### Layout Spacing (Component-Level vs. Page-Level) + +Carbon defines two spacing scales: the general spacing scale above (for within components) and a layout spacing scale for between components and sections. The layout scale uses the same tokens but is applied to margin and padding at the section level. + +### Section Rhythm + +- Product pages use `48px` (space-09) vertical section rhythm — compact and information-dense. +- Marketing pages use `80-96px` (space-11/12) vertical section rhythm — more breathing room. +- Between heading and body: `16px` (space-05). +- Between body and CTA: `24px` (space-06). +- Edge padding (mobile): `16px` (space-05). +- Edge padding (desktop): `24-32px` (space-06/07). + +### Alignment + +- Left-aligned is the default. Enterprise content is not centered. +- Data tables: left-aligned labels, right-aligned numeric values. +- Forms: left-aligned labels above inputs (not inline labels). +- Headlines: left-aligned in product UI. Center-aligned only in marketing hero sections. +- Navigation: left-aligned. Side navigation is the primary navigation pattern for product UI. + +--- + +## 6. Depth & Elevation + +Carbon's depth system is flat by conviction, using background color shifts instead of shadows for surface hierarchy. Where shadows are used, they are subtle and functional — not decorative. + +### Surface Hierarchy + +| Level | Light Treatment | Dark Treatment | Use Case | +|-------|----------------|----------------|----------| +| 0 | `var(--color-canvas)` — no shadow, no border | `var(--color-canvas)` — no shadow, no border | Page background, top bar, footer | +| 1 | `var(--color-surface)` — background shift only | `var(--color-surface)` — background shift only | Container panels, inset sections, table row alternation | +| 2 | `var(--color-surface-elevated)` — background shift | `var(--color-surface-elevated)` — background shift | Cards, tiles, modals — elevated surfaces on inset backgrounds | +| 3 | 1px `var(--color-hairline)` border | 1px `var(--color-hairline)` border | Clickable tiles, input fields, data table cells | +| 4 | `box-shadow: 0 2px 6px rgba(0, 0, 0, 0.2)` | `box-shadow: 0 2px 6px rgba(0, 0, 0, 0.4)` | Dropdowns, popovers, tooltips — transient floating elements only | +| 5 | `box-shadow: 0 6px 16px rgba(0, 0, 0, 0.2)` | `box-shadow: 0 6px 16px rgba(0, 0, 0, 0.4)` | Modal dialogs — the only element that should cast a strong shadow | + +### Layer Sets (Carbon's Layering Tokens) + +Carbon defines layering tokens with numbered suffixes (-00, -01, -02, -03) that automatically adapt to the current theme: + +| Layer | Light Background | Dark Background | Use Case | +|-------|-----------------|-----------------|----------| +| layer-00 | `#FFFFFF` | `#161616` | Base page, top-level container | +| layer-01 | `#F4F4F4` | `#262626` | Raised section, inset panel | +| layer-02 | `#FFFFFF` | `#393939` | Card on inset panel | +| layer-03 | `#F4F4F4` | `#525252` | Nested element on card | + +### Overlay + +- Light overlay: `rgba(22, 22, 22, 0.5)` — for loading spinners and gentle content fade +- Dark overlay: `rgba(22, 22, 22, 0.85)` — for modal dialogs and focused interactions + +### Brand Signature Depth + +- **8-Bar Stripe Motif:** 8 horizontal bars, each 4px tall with 2px gap between. Used only as a section divider or as a thin decorative accent at the top of a page/section. Colors: all bars in `var(--color-primary)` (IBM Blue) for brand contexts, or `var(--color-ink)` for neutral contexts. Never as a fill pattern, never as a background texture, never animated. + +--- + +## 7. Do's and Don'ts + +**Do:** + +- Use IBM Blue (`#0F62FE`) as the single primary interactive color — it carries every CTA, link, and active element +- Set productive body at 14px — information density is a virtue in enterprise UI +- Use weight 400 (Regular) for most text — IBM trusts size and spacing for hierarchy, not weight +- Use the gray scale systematically — gray-100 for light text, gray-10 for dark text, gray-30 for borders, gray-80 for dark borders +- Let background color shifts communicate surface hierarchy instead of shadows +- Use IBM Plex Sans as the sole typeface — it is designed for this system +- Use the Carbon spacing tokens (space-01 through space-13) for all spacing decisions +- Use left-alignment as the default — enterprise content is not centered +- Support dark mode as a first-class citizen — Carbon provides g90 and g100 theme tokens for a reason +- Use the full 12-color family palette for data visualization charts and status indicators +- Use 0px border-radius for buttons and cards — the Carbon standard +- Use the 8-bar stripe motif as a controlled signature accent, not as wallpaper + +**Don't:** + +- Do not use blue-60 (`#0F62FE`) for decoration — it is reserved for interactive elements and primary actions only +- Do not add rounded corners to buttons, cards, or inputs — 0px radius is the Carbon identity +- Do not use decorative drop shadows on cards or static elements — shadows are for transient floating elements only +- Do not use weight 700 (Bold) for headings in productive UI — Carbon uses weight 400/600, not 700 +- Do not set body text at 16px in product UI — 14px is the enterprise standard; 16px is for marketing only +- Do not use pure black (`#000000`) for dark backgrounds — gray-100 (`#161616`) is the darkest Carbon surface +- Do not center-align body text, form labels, or table content — left-alignment is the enterprise default +- Do not use more than two type sets on a single page — choose Productive or Expressive, do not mix +- Do not use the color families (red, green, yellow, etc.) for decoration — they are for status and data visualization +- Do not use italic for emphasis in product UI — use weight 600 or size change instead +- Do not use negative letter-spacing on body text — 0 to 0.32px tracking only +- Do not use the 8-bar motif as a background pattern or fill — it is a section divider and accent only +- Do not design without considering data density — IBM surfaces are information-rich by default + +--- + +## 8. Responsive Behavior + +### Breakpoints + +| Name | Min Width | Columns | Gutter | Margin | Behavior | +|------|-----------|---------|--------|--------|----------| +| sm | 320px | 4 | 16px | 16px | Single column stacked. Side nav collapses to icon-only or sheet. Data tables scroll horizontally. Hero headline reduces to 36px (expressive). Productive headings unchanged. Footer 4-col to 1-col. | +| md | 672px | 8 | 16px | 16px | Two-column layouts possible. Side nav icon-only. Data tables remain scrollable. Hero headline 48px (expressive). Cards 2-up. | +| lg | 1056px | 16 | 24px | 24px | Full 16-column grid. Expanded side navigation (256px). Full data tables. Hero headline 64px (expressive). Cards 3-up or 4-up. | +| xlg | 1312px | 16 | 24px | 24px | Wider gutters. More sidebar content visible. Dashboard layouts reach full density. | +| max | 1584px | 16 | 32px | 32px | Maximum content width. Gutters and margins absorb remaining space. No content stretching beyond 1584px. | + +### Mobile-Specific Rules + +- Productive body text stays at 14px across all breakpoints — do not enlarge for mobile +- Data tables scroll horizontally on mobile with sticky first column — never restructure into card layouts +- Side navigation collapses to icon-only (64px) or becomes a full-screen sheet on mobile +- Touch targets: minimum 44x44px (WCAG AAA) +- Button height: 48px standard, 32px small — same as desktop +- Form inputs: 40px height (standard), with 48px touch target area via padding +- Expressive display type scales down: display-03 from 64px to 36px at sm breakpoint +- Productive headings do not scale — they are fixed sizes regardless of breakpoint +- Cards stack to single column at sm, 2-up at md +- Modal dialogs become full-screen sheets at sm breakpoint +- Pagination converts to "load more" pattern on mobile + +### Content Behavior + +- Side navigation width: 256px (expanded) / 64px (collapsed). Collapsed state shows icons only with tooltip on hover. +- Data tables: sticky header, sticky first column on mobile. Horizontal scroll with fade indicator. +- Forms: single-column layout on mobile. Two-column at md+. Three-column at lg+ for dense admin forms. +- Footer: 4-column at lg, 2-column at md, 1-column at sm with accordion sections. + +### Dark Mode Switching + +Carbon supports four themes. Use `prefers-color-scheme` media query for automatic switching; always provide a manual toggle in the UI Shell. All color tokens swap simultaneously through the theme layer — no partial theme application. The layer tokens (layer-00 through layer-03) automatically invert, so nested surface hierarchy is preserved in both modes. Transition between modes should be instant (no animation on color swap — enterprise users value predictability over delight). diff --git a/addons/officials/crew/designer/skills/design-system-picker/design-systems/index.json b/addons/officials/crew/designer/skills/design-system-picker/design-systems/index.json new file mode 100644 index 00000000..945ab166 --- /dev/null +++ b/addons/officials/crew/designer/skills/design-system-picker/design-systems/index.json @@ -0,0 +1,167 @@ +[ + { + "id": "stripe", + "name": "Stripe", + "category": "fintech", + "keywords": ["紫色", "渐变", "优雅", "金融科技", "轻盈"], + "description": "紫色渐变 + weight-300 优雅排版 + 信任感金融风格", + "colorPrimary": "#635BFF", + "darkMode": true, + "bestFor": "SaaS 产品页、支付/金融科技落地页", + "file": "stripe.md" + }, + { + "id": "vercel", + "name": "Vercel", + "category": "devtools", + "keywords": ["黑白", "极简", "精密", "Geist字体", "开发者"], + "description": "黑白精密 + Geist 字体 + 开发者工具美学", + "colorPrimary": "#000000", + "darkMode": true, + "bestFor": "开发者工具、技术产品官网", + "file": "vercel.md" + }, + { + "id": "linear", + "name": "Linear", + "category": "productivity", + "keywords": ["极简", "紫色点缀", "精确", "工程师", "超简洁"], + "description": "超极简 + 紫色点缀 + 精确到像素的工程美学", + "colorPrimary": "#5E6AD2", + "darkMode": true, + "bestFor": "项目管理、效率工具、工程师导向产品", + "file": "linear.md" + }, + { + "id": "notion", + "name": "Notion", + "category": "productivity", + "keywords": ["暖色", "极简", "衬线标题", "柔光", "知识管理"], + "description": "温暖极简 + 衬线标题 + 柔和表面 + 知识工作者氛围", + "colorPrimary": "#000000", + "darkMode": false, + "bestFor": "知识管理、内容平台、文档型产品", + "file": "notion.md" + }, + { + "id": "apple", + "name": "Apple", + "category": "consumer", + "keywords": ["白色", "留白", "SF Pro", "电影感", "高级"], + "description": "极致留白 + SF Pro 字体 + 电影级影像 + 高级感", + "colorPrimary": "#0071E3", + "darkMode": true, + "bestFor": "消费电子、高端产品展示、品牌官网", + "file": "apple.md" + }, + { + "id": "supabase", + "name": "Supabase", + "category": "devtools", + "keywords": ["暗色", "翡翠绿", "代码优先", "数据库", "开发者"], + "description": "暗色翡翠绿主题 + 代码优先美学 + 开发者友好", + "colorPrimary": "#3ECF8E", + "darkMode": true, + "bestFor": "数据库/后端即服务、开源开发者工具", + "file": "supabase.md" + }, + { + "id": "shopify", + "name": "Shopify", + "category": "ecommerce", + "keywords": ["暗色", "霓虹绿", "超轻字体", "电商", "电影感"], + "description": "暗色电影感 + 霓虹绿点缀 + 超轻 display 字体", + "colorPrimary": "#008060", + "darkMode": true, + "bestFor": "电商平台、商业服务、SaaS 落地页", + "file": "shopify.md" + }, + { + "id": "figma", + "name": "Figma", + "category": "creative", + "keywords": ["多彩", "活泼", "专业", "设计工具", "品牌色丰富"], + "description": "多彩活泼但专业 + 设计工具品牌美学 + 丰富色彩系统", + "colorPrimary": "#F24E1E", + "darkMode": true, + "bestFor": "创意工具、设计平台、品牌展示", + "file": "figma.md" + }, + { + "id": "spotify", + "name": "Spotify", + "category": "media", + "keywords": ["绿色", "暗色", "大胆排版", "音乐", "媒体"], + "description": "鲜明绿 + 暗色基底 + 大胆排版 + 专辑封面驱动", + "colorPrimary": "#1DB954", + "darkMode": true, + "bestFor": "媒体/娱乐平台、音乐/视频产品", + "file": "spotify.md" + }, + { + "id": "tesla", + "name": "Tesla", + "category": "automotive", + "keywords": ["极简", "减法", "电影级", "电动汽车", "全屏摄影"], + "description": "极致减法 + 全屏电影级摄影 + Universal Sans + 零装饰", + "colorPrimary": "#000000", + "darkMode": true, + "bestFor": "汽车/硬件产品、极简品牌、全屏影像展示", + "file": "tesla.md" + }, + { + "id": "framer", + "name": "Framer", + "category": "creative", + "keywords": ["黑蓝", "动效优先", "设计感", "交互", "网站构建"], + "description": "大胆黑蓝 + 动效优先 + 设计感十足 + 网站构建器美学", + "colorPrimary": "#0055FF", + "darkMode": true, + "bestFor": "网站构建工具、创意代理、交互展示", + "file": "framer.md" + }, + { + "id": "airbnb", + "name": "Airbnb", + "category": "ecommerce", + "keywords": ["暖色", "珊瑚色", "摄影驱动", "圆角", "旅行"], + "description": "温暖珊瑚色 + 摄影驱动 + 圆角 UI + 旅行平台美学", + "colorPrimary": "#FF385C", + "darkMode": false, + "bestFor": "旅游/生活服务、社区平台、温暖亲和型产品", + "file": "airbnb.md" + }, + { + "id": "bmw", + "name": "BMW", + "category": "luxury", + "keywords": ["蓝色", "奢华", "精密", "汽车", "暗色", "金属感", "巴伐利亚"], + "description": "巴伐利亚蓝 + 暗色奢华 + 精密金属质感 + 百年汽车品牌美学", + "colorPrimary": "#0066B1", + "darkMode": true, + "bestFor": "奢侈品牌、高端产品展示、汽车/精密工业品牌官网", + "file": "bmw.md" + }, + { + "id": "ibm", + "name": "IBM", + "category": "enterprise", + "keywords": ["蓝色", "商业", "企业", "专业", "数据密集", "信任", "Carbon"], + "description": "企业蓝 + Carbon 设计系统 + IBM Plex 字体 + 数据密集型专业美学", + "colorPrimary": "#0F62FE", + "darkMode": true, + "bestFor": "企业级产品、B2B 服务、数据平台、商业/金融系统", + "file": "ibm.md" + }, + { + "id": "starbucks", + "name": "Starbucks", + "category": "lifestyle", + "keywords": ["绿色", "温暖", "社区", "咖啡", "自然", "圆角", "手工艺"], + "description": "Siren 绿 + 暖色社区氛围 + 自然质感 + 第三空间生活美学", + "colorPrimary": "#00704A", + "darkMode": false, + "bestFor": "生活品牌、社区平台、餐饮/零售、温暖亲和型产品", + "file": "starbucks.md" + } +] diff --git a/addons/officials/crew/designer/skills/design-system-picker/design-systems/linear.md b/addons/officials/crew/designer/skills/design-system-picker/design-systems/linear.md new file mode 100644 index 00000000..4290f342 --- /dev/null +++ b/addons/officials/crew/designer/skills/design-system-picker/design-systems/linear.md @@ -0,0 +1,324 @@ +# Linear Design System + +## 1. Visual Theme & Atmosphere + +Linear is an engineer-focused productivity tool defined by **radical restraint**. Every pixel is intentional. The interface feels like a precision instrument: dark, quiet, and fast. Decoration is eliminated unless it serves function. The purple accent is used as a surgical highlight, never as decoration. Surfaces are flat and matte. Transitions are quick and purposeful. The overall impression is a tool that respects your attention and gets out of your way. + +**Atmosphere keywords**: dark, precise, quiet, instrument-grade, no-nonsense, surgical purple accents + +**Design personality**: The engineering lead who speaks in bullet points and ships on time. No small talk, no ornament, just clarity. + +--- + +## 2. Color Palette & Roles + +### Core Backgrounds + +| Semantic Name | Hex | Role | +|---|---|---| +| `bg-root` | `#0A0A0F` | Deepest background; canvas behind everything | +| `bg-surface-1` | `#111118` | Primary surface; panels, sidebar, main content area | +| `bg-surface-2` | `#181820` | Elevated surface; modals, dropdowns, popovers | +| `bg-surface-3` | `#1F1F2A` | Highest elevation; tooltips, notification toasts | +| `bg-hover` | `#25252F` | Hover state fill on interactive surfaces | + +### Text + +| Semantic Name | Hex | Role | +|---|---|---| +| `text-primary` | `#E8E8ED` | Primary body text, headings, active labels | +| `text-secondary` | `#8B8B96` | Secondary labels, descriptions, placeholder text | +| `text-tertiary` | `#5C5C66` | Disabled text, metadata, timestamps | +| `text-on-accent` | `#FFFFFF` | Text on accent-colored backgrounds | + +### Accent + +| Semantic Name | Hex | Role | +|---|---|---| +| `accent` | `#5E6AD2` | Primary accent; active states, links, focus rings, CTAs | +| `accent-hover` | `#6C75DB` | Hover state on accent elements | +| `accent-muted` | `#3D4480` | Muted accent; subtle badges, inline highlights | +| `accent-subtle` | `rgba(94, 106, 210, 0.12)` | Ghost accent; selected row backgrounds, hover tints | + +### Semantic Colors + +| Semantic Name | Hex | Role | +|---|---|---| +| `success` | `#4ADE80` | Completed states, confirmations | +| `warning` | `#FBBF24` | Caution states, in-progress | +| `error` | `#F87171` | Errors, destructive actions, validation failures | +| `info` | `#60A5FA` | Informational badges, neutral highlights | + +### Borders + +| Semantic Name | Hex | Role | +|---|---|---| +| `border-default` | `#25252F` | Default borders between sections and panels | +| `border-subtle` | `#1A1A24` | Very subtle dividers within a surface | +| `border-active` | `#5E6AD2` | Active/focused border on inputs and selections | + +--- + +## 3. Typography Rules + +### Font Stack + +- **Primary**: `-apple-system, BlinkMacSystemFont, "Segoe UI", Inter, sans-serif` +- **Monospace**: `"SF Mono", "Fira Code", "Cascadia Code", Menlo, monospace` +- **Display (hero only)**: `"Inter", -apple-system, sans-serif` at weight 600 + +### Type Scale + +| Element | Size | Weight | Line Height | Letter Spacing | Color | +|---|---|---|---|---|---| +| Display / Hero | `48px` | 600 | 1.1 | `-0.02em` | `text-primary` | +| H1 / Page Title | `24px` | 600 | 1.3 | `-0.01em` | `text-primary` | +| H2 / Section | `18px` | 600 | 1.4 | `0` | `text-primary` | +| H3 / Subsection | `14px` | 600 | 1.4 | `0` | `text-primary` | +| Body | `14px` | 400 | 1.5 | `0` | `text-primary` | +| Body Small | `13px` | 400 | 1.5 | `0` | `text-secondary` | +| Caption / Meta | `12px` | 400 | 1.4 | `0.01em` | `text-tertiary` | +| Label | `12px` | 500 | 1.3 | `0.02em` | `text-secondary` | +| Code | `13px` | 400 | 1.5 | `0` | `accent` | + +### Rules + +- Never use italic for UI text. Only for long-form content. +- Labels and metadata are always `text-secondary` or `text-tertiary`, never `text-primary`. +- Headings never have decorative underlines or borders. +- Text does not use gradients. Use solid color only. +- Maximum line width: `680px` for readable body text. + +--- + +## 4. Component Stylings + +### Buttons + +**Primary Button** +- Background: `accent` (`#5E6AD2`) +- Text: `text-on-accent` (`#FFFFFF`), 14px, weight 500 +- Padding: `6px 16px` +- Border-radius: `6px` +- Border: none +- Hover: `accent-hover` (`#6C75DB`), slight brightness shift +- Active: `#5258B8`, `translateY(1px)` (1px push) +- Focus: `2px` outline `accent`, `2px` offset +- Disabled: opacity `0.4`, no hover effect + +**Secondary Button** +- Background: `transparent` +- Text: `text-primary`, 14px, weight 500 +- Padding: `6px 16px` +- Border-radius: `6px` +- Border: `1px solid border-default` (`#25252F`) +- Hover: background `bg-hover` (`#25252F`) +- Active: background `bg-surface-2` (`#181820`) +- Focus: `2px` outline `accent`, `2px` offset + +**Ghost Button** +- Background: `transparent` +- Text: `text-secondary`, 14px, weight 400 +- Padding: `6px 12px` +- Border-radius: `6px` +- Border: none +- Hover: background `bg-hover`, text becomes `text-primary` +- Active: background `bg-surface-2` +- Focus: `2px` outline `accent`, `2px` offset + +**Danger Button** +- Same as secondary but text and border use `error` (`#F87171`) +- Hover: background `rgba(248, 113, 113, 0.08)` + +### Cards + +- Background: `bg-surface-1` (`#111118`) +- Border-radius: `8px` +- Border: `1px solid border-default` (`#25252F`) +- Padding: `20px` +- Hover: border-color `#2F2F3A`, very subtle +- No box-shadow in default state +- No decorative gradients on cards + +### Inputs + +**Text Input** +- Background: `bg-surface-1` (`#111118`) +- Border: `1px solid border-default` (`#25252F`) +- Border-radius: `6px` +- Padding: `8px 12px` +- Text: `text-primary`, 14px +- Placeholder: `text-tertiary` +- Focus: border `accent`, subtle `0 0 0 3px accent-subtle` ring +- Error: border `error`, error message in `error` color at 12px below input + +**Select / Dropdown** +- Same base as text input +- Dropdown panel: `bg-surface-2` (`#181820`), `8px` border-radius, `1px` border `border-default` +- Dropdown shadow: `0 8px 24px rgba(0, 0, 0, 0.4)` +- Selected item: `accent-subtle` background, `accent` text +- Hover item: `bg-hover` background + +### Navigation + +**Sidebar** +- Background: `bg-surface-1` (`#111118`) +- Width: `240px` (collapsible to `48px` icon-only mode) +- Border-right: `1px solid border-default` +- Item height: `32px` +- Item padding: `0 12px` +- Item border-radius: `6px` +- Item text: `text-secondary`, 13px, weight 400 +- Active item: background `accent-subtle`, text `accent`, weight 500 +- Hover item: background `bg-hover`, text `text-primary` +- Group labels: `text-tertiary`, 11px, weight 600, `0.04em` letter-spacing, uppercase + +**Top Bar** +- Background: `bg-surface-1` with `border-bottom: 1px solid border-default` +- Height: `44px` +- Breadcrumbs: `text-secondary`, 13px, separated by `/` in `text-tertiary` +- Actions aligned right + +### Badges / Tags + +- Border-radius: `9999px` (pill shape) +- Padding: `2px 8px` +- Font: 11px, weight 500 +- Variants: + - Default: `bg-hover` background, `text-secondary` text + - Accent: `accent-subtle` background, `accent` text + - Success: `rgba(74, 222, 128, 0.1)` background, `success` text + - Warning: `rgba(251, 191, 36, 0.1)` background, `warning` text + - Error: `rgba(248, 113, 113, 0.1)` background, `error` text + +### Toggles + +- Track: `bg-hover` off, `accent` on +- Knob: `text-primary`, `12px` circle +- Track size: `32px x 18px`, border-radius `9999px` +- Transition: `150ms ease` + +--- + +## 5. Layout Principles + +### Spacing Scale + +| Token | Value | Usage | +|---|---|---| +| `xs` | `4px` | Tight inline gaps, icon-to-label | +| `sm` | `8px` | Between related items | +| `md` | `12px` | Between form fields, list items | +| `lg` | `16px` | Section padding, card inner gaps | +| `xl` | `20px` | Card padding, section margins | +| `2xl` | `24px` | Major section separation | +| `3xl` | `32px` | Page-level vertical rhythm | +| `4xl` | `48px` | Hero-level spacing | +| `5xl` | `64px` | Maximum section gap | + +### Grid + +- Content max-width: `1200px` +- Sidebar + main layout: sidebar `240px` fixed, main fills remaining +- Gutter: `16px` between columns +- Card grid: 3 columns at >= 1200px, 2 at >= 768px, 1 below +- Grid column gap: `16px` +- Grid row gap: `16px` + +### Whitespace Rules + +- Sections are separated by `border-subtle` lines, not by increased whitespace alone. +- Vertical rhythm is tight: prefer `12px-16px` between items, not `24px-32px`. +- Horizontal padding in panels is always `16px` minimum. +- Content never touches viewport edges: minimum `16px` horizontal padding on mobile, `24px` on desktop. +- There is no decorative whitespace. Whitespace exists to group or separate, never to fill. + +### Alignment + +- All content is left-aligned. Center alignment only for modals and empty states. +- Labels sit above inputs (top-aligned), never to the left in forms. +- Icons are `16px` and vertically centered with adjacent text. + +--- + +## 6. Depth & Elevation + +Linear uses minimal shadows. Elevation is communicated primarily through background color shifts and border presence, not drop shadows. + +### Elevation Levels + +| Level | Background | Border | Shadow | Usage | +|---|---|---|---|---| +| 0 (base) | `bg-root` (`#0A0A0F`) | none | none | Canvas | +| 1 (surface) | `bg-surface-1` (`#111118`) | `1px border-default` | none | Panels, sidebar, cards | +| 2 (raised) | `bg-surface-2` (`#181820`) | `1px border-default` | `0 4px 16px rgba(0,0,0,0.3)` | Dropdowns, popovers | +| 3 (overlay) | `bg-surface-3` (`#1F1F2A`) | `1px border-default` | `0 8px 24px rgba(0,0,0,0.4)` | Modals, command palette | + +### Glow Effects (use sparingly) + +- Accent glow on focused inputs: `box-shadow: 0 0 0 3px rgba(94, 106, 210, 0.2)` +- CTA button glow (hero only): `box-shadow: 0 0 20px rgba(94, 106, 210, 0.3)` +- Never use glow on cards, badges, or navigation items. + +### Overlay + +- Modal backdrop: `rgba(0, 0, 0, 0.6)` with `backdrop-filter: blur(4px)` + +--- + +## 7. Do's and Don'ts + +### Do + +- Use `accent` sparingly. One accent element per viewport is often enough. +- Keep surfaces flat. Background color differences, not shadows, convey depth. +- Use monospace font for IDs, keys, and code snippets. +- Round numbers precisely. Border-radius is `6px` for inputs/buttons, `8px` for cards, `9999px` for pills only. +- Use `text-secondary` for descriptions and `text-tertiary` for metadata. This hierarchy is the primary way to guide attention. +- Animate with `150ms-200ms ease` for interactive state changes. Nothing slower. +- Use 1px borders. Never 2px or 3px except for focus rings. +- Prefer icon + text for actions. Icon-only only if the action is universally understood (e.g., close X, search magnifier). + +### Don't + +- Do not use gradients on text. Ever. +- Do not use decorative gradients on backgrounds of cards, panels, or sections. Subtle radial glows in hero sections are the only exception. +- Do not use rounded corners greater than `8px` on rectangular elements. No `16px` or `24px` radius cards. +- Do not add box-shadows to resting-state cards or list items. +- Do not use `accent` color for decorative elements like dividers or background fills (except `accent-subtle` for selection states). +- Do not use bold/weight-700 for body text. 600 is the maximum and only for headings and labels. +- Do not center-align paragraphs or form layouts. Left-align everything. +- Do not use emoji or decorative icons in navigation labels. +- Do not animate `width`, `height`, `top`, `left`, or `margin`. Use `transform` and `opacity` only. +- Do not use color alone to convey state. Pair with text labels or icons. + +--- + +## 8. Responsive Behavior + +### Breakpoints + +| Name | Min Width | Layout Behavior | +|---|---|---| +| `mobile` | `0` | Single column, sidebar hidden (hamburger), stacked cards | +| `tablet` | `768px` | Optional sidebar, 2-column card grid | +| `desktop` | `1024px` | Full sidebar visible, 2-column grid, side-by-side forms | +| `wide` | `1200px` | 3-column card grid, maximum content width enforced | + +### Adaptation Rules + +- **Sidebar**: Hidden below `1024px`, replaced by hamburger menu overlay. Overlay uses `bg-surface-2` with slide-in from left (`transform: translateX`, 200ms ease). +- **Navigation items**: Text + icon on desktop; icon-only below `768px` if sidebar is collapsed. +- **Cards**: Full-width below `768px`, 2-up at `768px+`, 3-up at `1200px+`. +- **Top bar**: Title truncates with ellipsis below `768px`. Breadcrumbs collapse to last segment + ellipsis. +- **Forms**: Single column always. Top-aligned labels. Full-width inputs on mobile, max `480px` on desktop. +- **Modals**: Full-screen on mobile (with safe-area padding), centered overlay on desktop. +- **Tables**: Convert to stacked card list on mobile. Each row becomes a card with label-value pairs. +- **Font sizes**: Reduce display/hero from `48px` to `32px` below `768px`. H1 from `24px` to `20px` below `768px`. Body text stays `14px` at all sizes. +- **Touch targets**: Minimum `36px` height for all interactive elements on mobile (up from `32px` on desktop). + +### Performance Notes + +- Use `will-change: transform` sparingly, only on elements actively animating. +- Prefer CSS transitions over JS-driven animations for state changes. +- Backdrop-filter (blur) should be used only for modal overlays; avoid on frequently toggled elements. diff --git a/addons/officials/crew/designer/skills/design-system-picker/design-systems/notion.md b/addons/officials/crew/designer/skills/design-system-picker/design-systems/notion.md new file mode 100644 index 00000000..67374aac --- /dev/null +++ b/addons/officials/crew/designer/skills/design-system-picker/design-systems/notion.md @@ -0,0 +1,448 @@ +# Notion Design System + +## 1. Visual Theme & Atmosphere + +Notion embodies **warm minimalism** — the aesthetic of a well-lit study, not a cold lab. Surfaces feel like quality paper. Typography carries intellectual weight through serif headings while the body stays crisp and readable. Every element breathes; nothing is crowded. The overall impression is calm competence: a tool that respects your attention and gets out of the way. + +**Atmosphere keywords:** warm, calm, scholarly, approachable, paper-like, unhurried, trustworthy + +**Core visual traits:** +- Cream and warm whites dominate — never pure white (#FFF) on large surfaces +- Serif headings create a book-like cadence; sans-serif body keeps scanning fast +- Shadows are whispered, not shouted — surfaces lift gently, never float dramatically +- Icons and illustrations use thin strokes at 1.5px, never filled heavy shapes +- Color is used sparingly as semantic accent, never as decoration +- Interaction feedback is subtle: soft hovers, gentle transitions (150–200ms) + +--- + +## 2. Color Palette & Roles + +### Surface Colors + +| Name | Hex | Role | +|------|-----|------| +| bg-primary | #FFFFFF | Page canvas, main content area (used with warm surrounding context) | +| bg-warm | #FBFBFA | App shell background, sidebar backdrop | +| bg-cream | #F7F6F3 | Sidebar surface, panel backgrounds | +| bg-hover | #EBEBEA | Hover state for list items, menu rows | +| bg-active | #E3E3E2 | Active/pressed state | +| bg-selected | #2EAADC1A | Selected item highlight (blue at 10% opacity) | + +### Text Colors + +| Name | Hex | Role | +|------|-----|------| +| text-primary | #37352F | Body text, headings — the universal dark ink | +| text-secondary | #9B9A97 | Placeholder text, secondary labels, timestamps | +| text-tertiary | #C4C4C4 | Disabled text, dividers within text | +| text-link | #379ADC | Hyperlinks, navigational anchors | +| text-on-accent | #FFFFFF | Text on colored buttons/badges | + +### Accent / Semantic Colors + +| Name | Hex | Role | +|------|-----|------| +| accent-blue | #2EAADC | Primary actions, links, selection highlights | +| accent-blue-hover | #299FC7 | Blue hover state | +| accent-red | #EB5757 | Errors, destructive actions, warnings | +| accent-red-hover | #D14343 | Red hover state | +| accent-green | #0F7B6C | Success, confirmations, positive indicators | +| accent-yellow | #DFAB01 | Caution, pending states | +| accent-orange | #D9730D | Attention, secondary warnings | +| accent-pink | #AD1A72 | Tags, category markers | +| accent-purple | #6940A5 | Tags, category markers | + +### Border / Divider + +| Name | Hex | Role | +|------|-----|------| +| border-default | #E9E9E7 | Input borders, card outlines, table borders | +| border-hover | #D3D3D1 | Hover state on borders | +| divider | #E9E9E7 | Horizontal rules, section dividers | + +### Notion Color Tags (inline text/background pairs) + +| Tag | Text Hex | Background Hex | +|-----|----------|----------------| +| Blue | #2EAADC | #2EAADC1A | +| Red | #EB5757 | #EB57571A | +| Green | #0F7B6C | #0F7B6C1A | +| Yellow | #DFAB01 | #DFAB011A | +| Orange | #D9730D | #D9730D1A | +| Pink | #AD1A72 | #AD1A721A | +| Purple | #6940A5 | #6940A51A | +| Gray | #9B9A97 | #9B9A971A | +| Brown | #64473A | #64473A1A | + +--- + +## 3. Typography Rules + +### Font Stack + +- **Serif headings:** `"Noto Serif", "Ionicons", "Apple Color Emoji", Georgia, serif` +- **Sans-serif body:** `ui-sans-serif, -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif` +- **Monospace code:** `"SFMono-Regular", Menlo, Consolas, "PT Mono", "Liberation Mono", Courier, monospace` + +### Hierarchy + +| Level | Font | Weight | Size | Line Height | Letter Spacing | +|-------|------|--------|------|-------------|----------------| +| H1 | Serif | 700 | 30px | 1.3 | -0.02em | +| H2 | Serif | 600 | 24px | 1.3 | -0.015em | +| H3 | Serif | 600 | 20px | 1.4 | -0.01em | +| H4 | Sans-serif | 600 | 16px | 1.5 | 0 | +| Body | Sans-serif | 400 | 16px | 1.6 | 0 | +| Body small | Sans-serif | 400 | 14px | 1.5 | 0 | +| Caption | Sans-serif | 400 | 12px | 1.5 | 0 | +| Code | Monospace | 400 | 14px | 1.6 | 0 | +| Button label | Sans-serif | 500 | 14px | 1.0 | 0 | +| Overline / label | Sans-serif | 500 | 12px | 1.3 | 0.02em | + +### Key Typography Rules + +1. **Serif is for headings only (H1–H3).** H4 and below use sans-serif. This creates the signature "book chapter" feel at the top of the hierarchy. +2. **Never use italic serif for emphasis in headings.** Use weight changes instead (600 → 700). +3. **Body text stays at 16px minimum.** 14px for secondary text, 12px only for captions and labels. +4. **Code blocks use 14px monospace** on a slightly tinted background (#F7F6F3). +5. **Line height is generous** — 1.6 for body, 1.3 for headings — mirroring print book readability. +6. **Text color is always #37352F** (warm near-black), never pure #000000. + +--- + +## 4. Component Stylings + +### Buttons + +**Primary button** +``` +background: #2EAADC +color: #FFFFFF +border-radius: 4px +padding: 6px 12px +font: 500 14px/1 sans-serif +height: 32px +transition: background 120ms ease +``` +- Hover: background #299FC7 +- Active: background #2491B5, translateY(0.5px) +- Disabled: opacity 0.5, cursor not-allowed + +**Secondary button** +``` +background: #FFFFFF +color: #37352F +border: 1px solid #E9E9E7 +border-radius: 4px +padding: 6px 12px +font: 500 14px/1 sans-serif +height: 32px +``` +- Hover: background #FBFBFA, border #D3D3D1 +- Active: background #F7F6F3 + +**Destructive button** +``` +background: #EB5757 +color: #FFFFFF +border-radius: 4px +padding: 6px 12px +font: 500 14px/1 sans-serif +height: 32px +``` +- Hover: background #D14343 + +**Ghost / Text button** +``` +background: transparent +color: #37352F +border: none +border-radius: 4px +padding: 4px 8px +font: 500 14px/1 sans-serif +``` +- Hover: background #EBEBEA + +**Icon button (32x32)** +``` +background: transparent +border: none +border-radius: 4px +width: 32px +height: 32px +display: inline-flex +align-items: center +justify-content: center +``` +- Hover: background #EBEBEA +- Active: background #E3E3E2 + +### Cards + +**Page card / Content block** +``` +background: #FFFFFF +border: 1px solid #E9E9E7 +border-radius: 8px +padding: 16px +``` +- Hover: subtle border darkening to #D3D3D1 +- No box-shadow in default state — cards sit flat on the surface +- Cover images: 16:9 ratio, border-radius 4px on the image inside the card + +**Sidebar card / Nested panel** +``` +background: #F7F6F3 +border: none +border-radius: 6px +padding: 8px 10px +``` + +### Inputs / Text Fields + +**Standard input** +``` +background: #FFFFFF +border: 1px solid #E9E9E7 +border-radius: 4px +padding: 8px 10px +font: 400 16px/1.5 sans-serif +color: #37352F +height: 32px +``` +- Focus: border #2EAADC, box-shadow 0 0 0 2px #2EAADC33 +- Placeholder: color #9B9A97 +- Error: border #EB5757, box-shadow 0 0 0 2px #EB575733 +- Disabled: background #F7F6F3, color #9B9A97 + +**Search input** +``` +background: #F7F6F3 +border: 1px solid transparent +border-radius: 4px +padding: 8px 10px 8px 36px (space for search icon) +font: 400 16px/1.5 sans-serif +color: #37352F +``` +- Focus: background #FFFFFF, border #E9E9E7, box-shadow 0 0 0 2px #2EAADC33 + +**Multi-line / Textarea** +- Same as standard input but min-height 80px, vertical resize only +- Line height 1.6 + +### Navigation / Sidebar + +**Sidebar panel** +``` +background: #F7F6F3 +width: 240px (collapsible) +padding: 10px 6px +``` + +**Sidebar item (page link)** +``` +padding: 4px 8px +border-radius: 4px +font: 400 14px/1.4 sans-serif +color: #37352F +``` +- Hover: background #EBEBEA +- Active: background #2EAADC1A, color #37352F +- Icon + label: 20px icon left, 8px gap to label + +**Breadcrumbs** +``` +font: 400 14px/1 sans-serif +color: #9B9A97 +separator: "/" with 4px margin each side +``` +- Current page: color #37352F, font-weight 500 + +### Toggles & Checkboxes + +**Toggle** +- Track: 36x20px, border-radius 10px +- Off: background #E9E9E7 +- On: background #2EAADC +- Thumb: 16x16px circle, background #FFFFFF, translateY offset +- Transition: 150ms ease + +**Checkbox** +- 16x16px, border-radius 3px +- Unchecked: border 2px solid #9B9A97, background transparent +- Checked: background #2EAADC, border #2EAADC, white checkmark +- Transition: 120ms ease + +### Tags / Badges + +``` +padding: 2px 8px +border-radius: 3px +font: 500 12px/1.3 sans-serif +``` +- Uses the Notion color tag pairs (text color on matching 10% opacity background) +- Example: Blue tag = color #2EAADC, background #2EAADC1A + +### Tooltips + +``` +background: #37352F +color: #FFFFFF +padding: 4px 8px +border-radius: 4px +font: 400 12px/1.3 sans-serif +max-width: 240px +``` +- Appears 4px below trigger element +- Fade in 100ms ease + +--- + +## 5. Layout Principles + +### Spacing Scale + +| Token | Value | Usage | +|-------|-------|-------| +| xs | 4px | Inline gaps, icon-to-label | +| sm | 8px | Compact list item padding, badge padding | +| md | 12px | Button padding (horizontal), form field gaps | +| lg | 16px | Card padding, section inner padding | +| xl | 24px | Between related sections | +| 2xl | 32px | Between distinct content blocks | +| 3xl | 48px | Page section separators | +| 4xl | 64px | Top-level page sections | + +### Grid & Container + +- **Content width:** 708px (Notion's standard page width for readable content) +- **Wide content:** 936px (for tables, kanban boards, media) +- **Full width:** 100% minus sidebar (for databases, galleries) +- **Sidebar:** 240px default, 48px collapsed (icons only) +- **Column gap in multi-column:** 24px +- **No explicit CSS grid for page layout** — content flows vertically with horizontal blocks + +### Whitespace Philosophy + +1. **Generous top margins on headings.** H1: 32px top margin. H2: 24px. H3: 16px. This creates a clear visual rhythm. +2. **List items breathe.** Minimum 4px vertical padding per item. Nested items indent 24px. +3. **Content never touches edges.** Minimum 16px horizontal padding inside any container. +4. **Section breaks use space, not lines.** Prefer 48–64px vertical spacing between sections over visible dividers. Use dividers (#E9E9E7) only when semantic separation is needed within a tight space. +5. **Inline elements get breathing room.** 4px minimum gap between icon and label, 8px between adjacent actions. + +### Alignment + +- Content is left-aligned by default. Center alignment only for hero statements or empty states. +- Headings are left-aligned, never centered in body content. +- Labels sit above inputs (stacked), not beside them, to maintain vertical rhythm. + +--- + +## 6. Depth & Elevation + +Notion avoids dramatic depth. Surfaces feel like sheets of paper on a desk — some stacked, but all resting flat. + +### Shadow Levels + +| Level | Shadow | Usage | +|-------|--------|-------| +| Level 0 | none | Cards on page, sidebar items | +| Level 1 | 0 1px 2px rgba(0,0,0,0.06) | Hovered cards, raised panels | +| Level 2 | 0 2px 8px rgba(0,0,0,0.08) | Dropdowns, popovers, tooltips | +| Level 3 | 0 4px 16px rgba(0,0,0,0.1) | Modals, dialogs | +| Level 4 | 0 8px 32px rgba(0,0,0,0.12) | Full-screen overlays, command palette | + +### Elevation Rules + +1. **Default state: no shadow.** Cards and content blocks sit flush on the background. +2. **Borders do the work shadows usually do.** 1px #E9E9E7 borders delineate boundaries without implying elevation. +3. **Shadows appear on interaction.** A card might gain Level 1 shadow on hover, a dropdown gets Level 2 on open. +4. **Background tint implies depth, not shadow.** The sidebar is #F7F6F3 (cream), the content area is #FFFFFF. This subtle warmth shift creates perceived depth without rendering shadows. +5. **Overlays use opacity, not shadow.** Modal backdrops: rgba(0,0,0,0.4) with no blur. This keeps things warm and accessible. +6. **Never use inset shadows.** Notion surfaces are always convex, never concave. + +### Surface Hierarchy (bottom to top) + +1. `#F7F6F3` — Sidebar / background panels +2. `#FFFFFF` — Main content area / canvas +3. `#FFFFFF` + Level 1 border — Cards on the canvas +4. `#FFFFFF` + Level 2 shadow — Dropdowns, popovers +5. `#FFFFFF` + Level 3 shadow — Modals +6. `#37352F` at 85% opacity — Full overlay backdrop + +--- + +## 7. Do's and Don'ts + +### Do + +- Use serif for H1–H3 headings; it is the single most distinctive Notion signature +- Keep backgrounds warm — use #FBFBFA or #F7F6F3 instead of pure #FFFFFF for shells +- Use color tags (10% opacity backgrounds) for inline categorization — they feel native to the writing surface +- Default to no shadows; add them only when an element needs to float (dropdowns, modals) +- Use generous line height (1.6 body, 1.3 headings) for readability +- Keep borders at 1px #E9E9E7 — they should suggest boundaries, not draw attention +- Use 4px border-radius for inputs/buttons, 6–8px for cards, 3px for tags — subtle rounding only +- Preserve ample whitespace above headings (32px, 24px, 16px for H1/H2/H3) +- Use icon buttons at 32x32px with hover backgrounds — not outlined or shadowed buttons +- Animate at 120–200ms with ease timing — fast enough to feel responsive, slow enough to perceive + +### Don't + +- Don't use pure #000000 for text — always #37352F (warm near-black) +- Don't apply bold colors to large surfaces — accent colors are for small highlights, tags, and interactive elements +- Don't use gradients anywhere — Notion surfaces are flat and solid +- Don't use serif for body text or UI labels — it belongs in headings only +- Don't add box-shadows to resting-state cards — borders are sufficient +- Don't use rounded-full (pill) shapes — maximum border-radius is 8px +- Don't use heavy/filled icons — prefer outlined/thin stroke style at 1.5px +- Don't use dark mode as primary — Notion is fundamentally a light, warm surface product +- Don't use bright saturated backgrounds — all backgrounds are neutral or pastel (10% tint) +- Don't add visible grid lines to layouts — use spacing and alignment instead +- Don't center-align body text or headings in content areas — left alignment is the default rhythm +- Don't use animations longer than 300ms — the interface should feel immediate and paper-like + +--- + +## 8. Responsive Behavior + +### Breakpoints + +| Name | Width | Layout Behavior | +|------|-------|-----------------| +| Mobile | < 640px | Single column, sidebar hidden (hamburger toggle), stacked cards | +| Tablet | 640–1024px | Sidebar collapsed to 48px icon rail, content at 640px max-width | +| Desktop | 1024–1440px | Sidebar at 240px, content at 708px max-width centered | +| Wide | > 1440px | Same as Desktop, extra whitespace distributed symmetrically | + +### Mobile Adaptations (< 640px) + +- **Sidebar:** Hidden off-screen, accessed via hamburger button (top-left). Slides in as overlay with Level 2 shadow. +- **Content width:** Full width minus 16px horizontal padding on each side. +- **Headings:** Reduce by 4px. H1: 26px, H2: 20px, H3: 18px. Maintain serif. +- **Cards:** Full-width, stacked vertically with 16px gap between them. No multi-column layouts. +- **Navigation:** Bottom tab bar for primary navigation (replacing sidebar), top bar for breadcrumbs and actions. +- **Tables:** Horizontal scroll with sticky first column. Or convert to list/card view. +- **Buttons:** Minimum touch target 44x44px. Add padding to reach minimum — don't scale font up. +- **Inputs:** Full width, height 44px (increased from 32px for touch). +- **Spacing:** Reduce 3xl/4xl to xl/2xl (48/64px → 24/32px). Maintain xs/sm/md unchanged. + +### Tablet Adaptations (640–1024px) + +- **Sidebar:** Collapsed to 48px icon rail. Tap icon to expand full sidebar as overlay. +- **Content width:** 640px max, centered. +- **Multi-column:** Maximum 2 columns. 3+ columns collapse to 2 or 1. +- **Cards:** 2-column grid for gallery/grid views. + +### Desktop (1024px+) + +- Standard layout as described in Section 5. +- Multi-column content allowed at wide width (936px). +- Sidebar fully expanded by default. + +### Responsive Transitions + +- Sidebar collapse/expand: 200ms ease with width transition +- Content reflow: No animation needed — content should reflow instantly +- Breakpoint changes: Layout shifts at breakpoints with no animation (not fluid) to maintain Notion's crisp, paper-like feel diff --git a/addons/officials/crew/designer/skills/design-system-picker/design-systems/shopify.md b/addons/officials/crew/designer/skills/design-system-picker/design-systems/shopify.md new file mode 100644 index 00000000..735a9cfa --- /dev/null +++ b/addons/officials/crew/designer/skills/design-system-picker/design-systems/shopify.md @@ -0,0 +1,451 @@ +# Shopify Design System + +## 1. Visual Theme & Atmosphere + +Shopify's design language is **dark-first and cinematic**. The interface feels like a control center for commerce: deep dark surfaces, razor-sharp typography at ultra-light weights, and a neon green accent that cuts through like a terminal cursor. Photography is dramatic and moody. Products and merchants are cast in high-contrast, studio-lit scenes. The overall impression is a platform that means business -- modern, powerful, and unapologetically commercial. + +**Core qualities:** +- Dark surfaces dominate; light mode exists but feels secondary +- Ultra-light font weights (300, even 200) for display headlines -- airy, not heavy +- Neon green (#008060) as surgical accent; never decorative, always functional +- Cinematic photography with deep shadows and dramatic lighting +- Generous negative space on dark surfaces creates depth without elevation hacks +- Surfaces are matte and flat; depth comes from background color layering, not shadows +- Micro-interactions are quick and responsive (150ms-200ms); the platform feels fast +- E-commerce data density is handled through clear hierarchy, not visual noise + +**Atmosphere keywords:** dark-commerce, neon-green, cinematic, ultra-light type, platform-power, merchant-centric, terminal-sharp + +**Design personality:** The commerce platform that treats your store like a mission-critical dashboard. Confident, efficient, with just enough visual drama to feel premium. + +--- + +## 2. Color Palette & Roles + +### Dark Mode (Primary) + +| Token | Hex | Role | +|-------|-----|------| +| `--color-bg` | `#0B1215` | Deepest background; canvas behind all surfaces | +| `--color-surface` | `#111820` | Primary surface; panels, sidebar, main content | +| `--color-surface-elevated` | `#1A232B` | Elevated surface; cards, dropdowns | +| `--color-surface-overlay` | `#222D38` | Overlay surface; modals, popovers | +| `--color-surface-hover` | `#1E2A35` | Hover state fill on interactive surfaces | +| `--color-text-primary` | `#E3E8EB` | Headlines, body copy, primary labels | +| `--color-text-secondary` | `#8B9DA7` | Descriptions, captions, secondary info | +| `--color-text-tertiary` | `#5C6F7A` | Disabled text, placeholders, metadata | +| `--color-text-inverse` | `#FFFFFF` | Text on accent backgrounds | +| `--color-accent` | `#008060` | Primary accent; CTAs, active states, links, focus rings | +| `--color-accent-hover` | `#009A73` | Accent hover state | +| `--color-accent-active` | `#006B4F` | Accent pressed/active state | +| `--color-accent-subtle` | `rgba(0, 128, 96, 0.12)` | Ghost accent; selected rows, hover tints | +| `--color-accent-glow` | `rgba(0, 128, 96, 0.25)` | Glow ring for focus states | +| `--color-separator` | `#1E2A35` | Borders between sections and panels | +| `--color-separator-subtle` | `#162029` | Hairline dividers within a surface | +| `--color-success` | `#008060` | Completed states (shares accent green) | +| `--color-success-surface` | `rgba(0, 128, 96, 0.10)` | Success background tints | +| `--color-warning` | `#FFC453` | Caution, pending, attention required | +| `--color-warning-surface` | `rgba(255, 196, 83, 0.10)` | Warning background tints | +| `--color-error` | `#E43E3E` | Errors, destructive actions, validation failures | +| `--color-error-surface` | `rgba(228, 62, 62, 0.10)` | Error background tints | +| `--color-info` | `#5BA4CF` | Informational badges, neutral highlights | +| `--color-info-surface` | `rgba(91, 164, 207, 0.10)` | Info background tints | + +### Light Mode + +| Token | Hex | Role | +|-------|-----|------| +| `--color-bg` | `#F6F7F8` | Page background | +| `--color-surface` | `#FFFFFF` | Card / section fill | +| `--color-surface-elevated` | `#FFFFFF` | Elevated cards, modals | +| `--color-surface-hover` | `#F1F2F3` | Hover state fill | +| `--color-text-primary` | `#1A1F25` | Headlines, body copy | +| `--color-text-secondary` | `#637381` | Captions, descriptions | +| `--color-text-tertiary` | `#919BA3` | Disabled, placeholders | +| `--color-text-inverse` | `#FFFFFF` | Text on accent backgrounds | +| `--color-accent` | `#008060` | Primary accent (same as dark) | +| `--color-accent-hover` | `#006B4F` | Accent hover (darker on light bg) | +| `--color-accent-active` | `#005A42` | Accent pressed | +| `--color-accent-subtle` | `rgba(0, 128, 96, 0.08)` | Ghost accent tint | +| `--color-separator` | `#E1E3E5` | Borders, dividers | +| `--color-separator-subtle` | `#F1F2F3` | Hairline separators | +| `--color-success` | `#008060` | Same as accent | +| `--color-warning` | `#D4860A` | Darker warning for light bg | +| `--color-error` | `#D72B2B` | Darker error for light bg | +| `--color-info` | `#2E6EA6` | Darker info for light bg | + +### Brand Extension Colors + +| Name | Hex | Usage | +|------|-----|-------| +| Shopify Green | `#008060` | Primary brand; identical to accent | +| Shopify Green Light | `#95D7B2` | Decorative only; illustrations, data viz | +| Shopify Green Dark | `#004E3A` | Pressed states, deep emphasis | +| Polar Night 1 | `#0B1215` | Darkest dark surface | +| Polar Night 2 | `#111820` | Standard dark surface | +| Snow Storm | `#E3E8EB` | Lightest text on dark | + +--- + +## 3. Typography Rules + +**Font stack:** `-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif` + +**Display font (hero only):** Same stack at weight 200-300 for that ultra-light, cinematic feel. + +**Monospace:** `"SF Mono", "Fira Code", Menlo, Consolas, monospace` + +### Type Scale + +| Token | Size | Weight | Tracking | Line-height | Usage | +|-------|------|--------|----------|-------------|-------| +| `--text-hero` | `clamp(2.75rem, 5vw + 0.5rem, 4.5rem)` | 300 (light) | -0.025em | 1.1 | Hero headlines, splash sections | +| `--text-headline-lg` | `clamp(2rem, 3vw + 0.5rem, 2.5rem)` | 300 | -0.02em | 1.15 | Section headlines, feature titles | +| `--text-headline` | `clamp(1.5rem, 1.5vw + 0.5rem, 1.75rem)` | 400 | -0.015em | 1.2 | Sub-section headlines | +| `--text-title` | `1.125rem` (18px) | 500 | -0.01em | 1.3 | Card titles, modal headers | +| `--text-subtitle` | `1rem` (16px) | 500 | 0 | 1.4 | Subtitles, emphasis body | +| `--text-body` | `0.875rem` (14px) | 400 | 0 | 1.5 | Default body text | +| `--text-body-sm` | `0.8125rem` (13px) | 400 | 0.01em | 1.5 | Compact body, table cells | +| `--text-caption` | `0.75rem` (12px) | 400 | 0.02em | 1.4 | Captions, metadata, timestamps | +| `--text-overline` | `0.6875rem` (11px) | 600 | 0.06em | 1.3 | Labels, overlines (UPPERCASE) | +| `--text-data` | `1.5rem` (24px) | 500 | -0.01em | 1.2 | Dashboard metrics, big numbers | + +### Rules + +- Hero and headline text uses weight 300 (light) for the signature airy look. Never use 200 except on oversized display type (64px+). +- Body text is always 400 regular. Never use 300 for body copy -- it becomes illegible at small sizes. +- Tracking tightens progressively as size increases (hero: -0.025em, body: 0). +- Maximum measure (line width): `640px` for readable body text. +- Dashboard metric numbers use `--text-data` with tabular-nums font-feature for aligned columns. +- Labels and overlines are always uppercase with wide tracking. +- Never use italic for UI text. Reserve italic for long-form editorial content only. + +--- + +## 4. Component Stylings + +### Buttons + +**Primary (Accent Green)** +```css +background: var(--color-accent); +color: var(--color-text-inverse); +padding: 8px 20px; +border-radius: 8px; +font-size: 0.875rem; +font-weight: 500; +letter-spacing: 0; +border: none; +cursor: pointer; +transition: background 150ms ease, box-shadow 150ms ease; +``` +- Hover: `var(--color-accent-hover)` +- Active: `var(--color-accent-active)` + `scale(0.98)` +- Focus: `box-shadow: 0 0 0 2px var(--color-accent-glow)` +- Disabled: `opacity: 0.4`, no pointer events +- Loading: text replaced by 16px spinner in `var(--color-text-inverse)` + +**Secondary (Outlined)** +```css +background: transparent; +color: var(--color-text-primary); +padding: 8px 20px; +border-radius: 8px; +font-size: 0.875rem; +font-weight: 500; +border: 1px solid var(--color-separator); +``` +- Hover: `background: var(--color-surface-hover)`, border lightens +- Active: `background: var(--color-surface-elevated)` +- Focus: `box-shadow: 0 0 0 2px var(--color-accent-glow)` + +**Tertiary (Ghost)** +```css +background: transparent; +color: var(--color-text-secondary); +padding: 8px 12px; +border-radius: 8px; +font-size: 0.875rem; +font-weight: 400; +border: none; +``` +- Hover: `background: var(--color-surface-hover)`, text becomes `--color-text-primary` + +**Destructive** +```css +background: var(--color-error); +color: var(--color-text-inverse); +/* same shape/size as primary */ +``` +- Hover: `#C93232` +- Secondary destructive: outline style with `--color-error` text and border + +**Large CTA (Hero)** +```css +padding: 12px 28px; +font-size: 1rem; +font-weight: 500; +border-radius: 10px; +``` + +**Icon Button** +- `32px x 32px` square, `border-radius: 8px` +- Icon: `16px`, color `--color-text-secondary` +- Hover: `background: var(--color-surface-hover)` + +### Cards + +```css +background: var(--color-surface-elevated); +border-radius: 12px; +padding: 20px; +border: 1px solid var(--color-separator); +box-shadow: none; +``` +- Hover (interactive cards): `border-color: var(--color-accent)` at 30% opacity +- Selected: `border-color: var(--color-accent)`, `background: var(--color-accent-subtle)` +- Metric cards: large number (`--text-data`) top-left, label (`--text-caption`) below, sparkline or delta right +- Product cards: image top with `border-radius: 8px`, title in `--text-body` weight 500, price in `--text-body-sm` `--color-text-secondary` +- No box-shadow on default state cards + +### Inputs + +**Text Input** +```css +background: var(--color-surface); +border: 1px solid var(--color-separator); +border-radius: 8px; +padding: 8px 12px; +font-size: 0.875rem; +color: var(--color-text-primary); +outline: none; +transition: border-color 150ms ease, box-shadow 150ms ease; +``` +- Focus: `border-color: var(--color-accent)` + `box-shadow: 0 0 0 2px var(--color-accent-glow)` +- Placeholder: `var(--color-text-tertiary)` +- Error: `border-color: var(--color-error)`, error message in `--color-error` at 12px below +- Disabled: `opacity: 0.5`, `cursor: not-allowed` +- Prefix/suffix slots: icon or text in `--color-text-tertiary` inside input with `border-left`/`border-right` separator + +**Search Input** +```css +border-radius: 980px; /* pill shape */ +padding: 8px 12px 8px 36px; /* room for magnifier icon */ +``` + +**Select / Dropdown** +- Same base styling as text input +- Dropdown panel: `--color-surface-overlay` background, `8px` border-radius, `1px` border `--color-separator` +- Dropdown shadow: `0 8px 24px rgba(0, 0, 0, 0.4)` +- Selected item: `--color-accent-subtle` background, `--color-accent` text +- Hover item: `--color-surface-hover` background + +### Navigation + +**Sidebar** +- Background: `--color-surface` (`#111820`) +- Width: `240px` (collapsible to `56px` icon-only mode) +- Border-right: `1px solid var(--color-separator)` +- Item height: `36px` +- Item padding: `0 12px` +- Item border-radius: `8px` +- Item text: `--color-text-secondary`, 14px, weight 400 +- Active item: background `--color-accent-subtle`, text `--color-accent`, weight 500 +- Hover item: background `--color-surface-hover`, text `--color-text-primary` +- Group labels: `--color-text-tertiary`, 11px, weight 600, `0.06em` letter-spacing, uppercase +- Shopify logo: 28px mark in `--color-accent` at top, app name in `--text-subtitle` weight 500 + +**Top Bar / Command Bar** +- Background: `--color-surface` with `border-bottom: 1px solid var(--color-separator)` +- Height: `56px` +- Search bar centered: pill-shaped input, `--color-text-tertiary` placeholder "Search your store..." +- Breadcrumbs: `--color-text-secondary`, 14px, separated by chevron in `--color-text-tertiary` +- Action buttons aligned right + +### Badges / Status Tags + +- Border-radius: `980px` (pill shape) +- Padding: `2px 8px` +- Font: 12px, weight 500 +- Variants: + - Default: `--color-surface-hover` background, `--color-text-secondary` text + - Success: `--color-success-surface` background, `--color-success` text + - Warning: `--color-warning-surface` background, `--color-warning` text + - Error: `--color-error-surface` background, `--color-error` text + - Info: `--color-info-surface` background, `--color-info` text + +### Data Table + +- Header row: `--color-surface-hover` background, `--text-overline` style labels, sticky on scroll +- Row height: `48px` +- Cell text: `--text-body-sm` (13px) +- Row hover: `--color-surface-hover` background +- Row selected: `--color-accent-subtle` background +- Zebra striping: not used; hover state is sufficient +- Column borders: none; horizontal separators only (`1px solid var(--color-separator-subtle)`) + +--- + +## 5. Layout Principles + +### Spacing Scale + +| Token | Value | Usage | +|-------|-------|-------| +| `--space-1` | `4px` | Tight inline gaps, icon-to-label | +| `--space-2` | `8px` | Between related items, compact padding | +| `--space-3` | `12px` | Between form fields, list items | +| `--space-4` | `16px` | Section padding, card inner gaps | +| `--space-5` | `20px` | Card padding, comfortable spacing | +| `--space-6` | `24px` | Major section separation | +| `--space-8` | `32px` | Page-level vertical rhythm | +| `--space-10` | `40px` | Section dividers | +| `--space-12` | `48px` | Hero-level spacing | +| `--space-16` | `64px` | Maximum section gap | + +### Grid + +- Content max-width: `1200px` +- Sidebar + main layout: sidebar `240px` fixed, main fills remaining +- Admin content max-width within main: `960px` for readability +- Gutter: `16px` between columns +- Card grid: 3 columns at >= 1200px, 2 at >= 768px, 1 below +- Grid gap: `16px` + +### Whitespace Philosophy + +- The dark surface does the work of separation; excessive whitespace is unnecessary +- Section padding: `32px` to `48px` vertical (compact efficiency over luxury breathing) +- Between headline and body: `8px` to `12px` (tight rhythm) +- Between body and CTA: `16px` to `20px` +- Between sibling cards: `16px` +- Edge padding (mobile): `16px` +- Edge padding (desktop): `24px` to `32px` +- Content never touches viewport edges + +### Content Rhythm + +1. **Hero section**: Dark cinematic background with product photography or platform illustration. Ultra-light headline (weight 300) + short subtitle + green CTA. Content left-aligned, not centered. +2. **Feature sections**: Stacked or alternating layout. Headline at `--text-headline` + body + optional illustration. Compact vertical rhythm. +3. **Dashboard sections**: Metric cards in a row, then a data table or chart. Dense but legible. +4. **CTA section**: Often on slightly lighter dark surface. Bold headline, single green button. + +--- + +## 6. Depth & Elevation + +Shopify conveys depth through **background color layering**, not shadows. The darkest surface is the canvas; each step up in elevation is a slightly lighter dark. + +### Elevation Levels (Dark Mode) + +| Level | Background | Border | Shadow | Usage | +|-------|------------|--------|--------|-------| +| 0 (canvas) | `#0B1215` | none | none | Root background | +| 1 (surface) | `#111820` | `1px solid #1E2A35` | none | Panels, sidebar, main content | +| 2 (raised) | `#1A232B` | `1px solid #1E2A35` | none | Cards, list items, sections | +| 3 (overlay) | `#222D38` | `1px solid #2A3845` | `0 4px 16px rgba(0,0,0,0.3)` | Dropdowns, popovers | +| 4 (floating) | `#2A3845` | `1px solid #334555` | `0 8px 32px rgba(0,0,0,0.4)` | Modals, command palette | + +### Accent Glow + +The neon green accent produces a subtle glow when focused or highlighted, reinforcing the "terminal cursor" feel: + +- Focus ring: `box-shadow: 0 0 0 2px rgba(0, 128, 96, 0.25)` +- Active CTA glow (hero only): `box-shadow: 0 0 20px rgba(0, 128, 96, 0.2)` +- Never use glow on resting-state cards, badges, or navigation items + +### Overlay + +- Modal backdrop: `rgba(0, 0, 0, 0.6)` with `backdrop-filter: blur(4px)` +- Toast notifications: surface-level 3, fixed at bottom-right, stacked with `8px` gap + +### Border Radius Scale + +| Token | Value | Usage | +|-------|-------|-------| +| `--radius-sm` | `4px` | Small badges, inline tags | +| `--radius-md` | `8px` | Buttons, inputs, nav items | +| `--radius-lg` | `12px` | Cards, modal containers | +| `--radius-xl` | `16px` | Large feature cards, image containers | +| `--radius-pill` | `980px` | Search bar, status badges, pills | + +--- + +## 7. Do's and Don'ts + +### Do + +- Use weight 300 for hero and section headlines -- this is the signature Shopify look +- Use `--color-accent` (#008060) for interactive elements only: buttons, links, active states, focus rings +- Keep surfaces matte and flat. Background color shifts convey elevation more cleanly than shadows. +- Use `--color-accent-subtle` for selected/hover tints -- it provides context without visual noise. +- Use tabular-nums for dashboard metrics and price columns to maintain alignment. +- Use 14px as the default body size. Shopify is a dense admin interface; 16px body is too large for data-rich surfaces. +- Animate with `150ms ease` for interactive state changes. Speed signals efficiency. +- Use monospace font for order IDs, discount codes, and API keys. +- Pair the ultra-light headline with a regular-weight subtitle for maximum contrast. +- Use dark mode as the primary design target; light mode is the variant, not the other way around. +- Use full-bleed cinematic photography in marketing/hero sections, but keep admin UI photography contained and purposeful. +- Use 1px borders. Never 2px or 3px except for focus rings. + +### Don't + +- Do not use `--color-accent` for decorative elements like dividers, background fills, or icons that aren't interactive. +- Do not use gradients on text. Ever. +- Do not use weight 200 below 48px -- it becomes illegible. +- Do not use weight 700 (bold). 600 is the maximum, reserved for labels and small caps only. +- Do not add box-shadows to resting-state cards or list items. Shadows are reserved for overlays and popovers only. +- Do not use rounded corners greater than `12px` on rectangular content cards. No `20px` or `24px` radius cards. +- Do not center-align paragraphs, form layouts, or dashboard content. Left-align everything. +- Do not use zebra striping in tables. Row hover is sufficient. +- Do not animate `width`, `height`, `top`, `left`, or `margin`. Use `transform` and `opacity` only. +- Do not use color alone to convey status. Pair with text labels or icons (e.g., green dot + "Active"). +- Do not use decorative gradients on card backgrounds. Subtle radial glows in hero sections are the only exception. +- Do not use emoji in navigation labels, button text, or status indicators. +- Do not mix light and dark surfaces in the same view without clear visual separation (border or spacing). + +--- + +## 8. Responsive Behavior + +### Breakpoints + +| Name | Min Width | Layout Behavior | +|------|-----------|-----------------| +| `mobile` | `0` | Single column, sidebar hidden (hamburger), stacked cards, bottom sheet modals | +| `tablet` | `768px` | Optional sidebar, 2-column card grid, side-by-side forms | +| `desktop` | `1024px` | Full sidebar visible, 2-3 column grid, standard modal overlays | +| `wide` | `1200px` | Maximum content width enforced, 3-column card grid | + +### Adaptation Rules + +- **Sidebar**: Hidden below `1024px`, replaced by hamburger menu overlay. Overlay slides in from left (`transform: translateX`, 200ms ease) with `--color-surface` background. +- **Navigation items**: Text + icon on desktop; icon-only below `768px` if sidebar is collapsed. +- **Cards**: Full-width below `768px`, 2-up at `768px+`, 3-up at `1200px+`. +- **Top bar**: Search bar collapses to icon-only below `768px`. Title truncates with ellipsis. +- **Data tables**: Convert to stacked card list on mobile. Each row becomes a card with label-value pairs. Critical columns (status, amount) remain visible as compact rows. +- **Forms**: Single column always. Top-aligned labels. Full-width inputs on mobile, max `480px` on desktop. +- **Modals**: Full-screen on mobile (with safe-area padding), centered overlay on desktop. +- **Dashboard metrics**: Stack vertically on mobile, horizontal row on desktop. +- **Font sizes**: Reduce hero from fluid scale to `2rem` (32px) below `768px`. Body text stays `14px` at all sizes -- never scale body down on mobile. + +### Spacing Scale (Mobile vs Desktop) + +| Context | Mobile | Desktop | +|---------|--------|---------| +| Section vertical padding | `24px` | `32px` to `48px` | +| Card padding | `16px` | `20px` | +| Edge margin | `16px` | `24px` to `32px` | +| Between-section gap | `24px` | `32px` to `48px` | +| Card grid gap | `12px` | `16px` | +| Sidebar width | hidden | `240px` | + +### Touch Targets + +- Minimum `44px x 44px` on mobile for all interactive elements +- Tap targets separated by at least `8px` +- Buttons on mobile: full-width or minimum `120px` wide + +### Dark Mode Handling + +Dark mode is the default. Use `prefers-color-scheme: light` to activate the light variant. All color tokens swap simultaneously. Transition between modes should be instant (no animation on color swap). Product imagery may remain the same between modes -- only UI surfaces swap. diff --git a/addons/officials/crew/designer/skills/design-system-picker/design-systems/spotify.md b/addons/officials/crew/designer/skills/design-system-picker/design-systems/spotify.md new file mode 100644 index 00000000..d0ff7ab8 --- /dev/null +++ b/addons/officials/crew/designer/skills/design-system-picker/design-systems/spotify.md @@ -0,0 +1,294 @@ +# Spotify Design System + +## 1. Visual Theme & Atmosphere + +Spotify's visual identity is built on a tension between darkness and vibrancy. The near-black canvas makes the signature green and album art explode forward. It feels like a club, not a boardroom: confident, rhythmic, and image-forward. + +**Atmosphere keywords:** immersive, bold, musical, confident, dark-first, cover-art-driven + +**Core tension:** Maximum visual impact from minimal color variation. One accent hue carries the entire brand. All other color comes from content (album art, artist photos, video thumbnails). + +**Mood:** Energy at rest. The UI is calm until the user plays something, then the green pulses and the artwork dominates. Surfaces stay out of the way. Content is always the star. + +--- + +## 2. Color Palette & Roles + +### Primary + +| Token | Hex | Role | +|-------|-----|------| +| `--color-spotify-green` | `#1DB954` | Primary accent, CTAs, active states, brand marks | +| `--color-spotify-green-light` | `#1ED760` | Hover/pressed state for green elements | +| `--color-spotify-green-dark` | `#1AA34A` | Active/pressed variant on dark surfaces | + +### Surfaces (Dark Mode -- default) + +| Token | Hex | Role | +|-------|-----|------| +| `--color-bg-base` | `#121212` | Page background, root surface | +| `--color-bg-elevated` | `#181818` | Card surfaces, sidebar, containers | +| `--color-bg-highlight` | `#282828` | Hover states on cards, list items | +| `--color-bg-press` | `#333333` | Active/pressed states on interactive items | +| `--color-bg-subtle` | `#1A1A1A` | Subtle surface differentiation | + +### Text + +| Token | Hex | Role | +|-------|-----|------| +| `--color-text-primary` | `#FFFFFF` | Headlines, primary body text | +| `--color-text-secondary` | `#B3B3B3` | Metadata, descriptions, timestamps | +| `--color-text-subdued` | `#6A6A6A` | Disabled states, placeholder text | +| `--color-text-on-green` | `#000000` | Text on green buttons/badges | + +### Semantic + +| Token | Hex | Role | +|-------|-----|------| +| `--color-error` | `#E91429` | Error states, destructive actions | +| `color-warning` | `#FFC862` | Warnings, offline indicators | +| `--color-link` | `#1DB954` | Links (same as primary green) | + +### Content-Driven Colors + +Spotify derives contextual color from album art and artist imagery. Use these as overlay tints on surfaces: + +| Token | Usage | +|-------|-------| +| `--color-tint` | Dominant color extracted from current album art, applied as a subtle gradient behind hero sections and now-playing bar | + +--- + +## 3. Typography Rules + +### Font Stack + +- **Primary:** `Circular`, `-apple-system`, `BlinkMacSystemFont`, `Segoe UI`, `Roboto`, `Helvetica Neue`, `sans-serif` +- **Fallback system stack** is acceptable when Circular is unavailable. Never use decorative or serif faces. + +### Scale + +| Token | Size | Weight | Line Height | Usage | +|-------|------|--------|-------------|-------| +| `--text-display` | `72px` | 900 (Black) | 1.0 | Hero headlines, landing page statements | +| `--text-title-lg` | `48px` | 700 (Bold) | 1.1 | Section headers, playlist titles | +| `--text-title` | `32px` | 700 (Bold) | 1.2 | Card titles, subsection headers | +| `--text-title-sm` | `24px` | 600 (SemiBold) | 1.25 | List headers, nav items | +| `--text-body-lg` | `18px` | 400 (Regular) | 1.5 | Intro paragraphs, feature descriptions | +| `--text-body` | `14px` | 400 (Regular) | 1.5 | Body text, list items, metadata | +| `--text-caption` | `12px` | 400 (Regular) | 1.4 | Timestamps, track numbers, small labels | +| `--text-overline` | `10px` | 700 (Bold) | 1.6 | Uppercase labels, category tags | + +### Rules + +- Headlines are always bold or black weight. Regular-weight headlines are forbidden. +- Body text on dark surfaces is `#B3B3B3`, never pure white. White body text causes eye strain on `#121212`. +- Use `text-transform: uppercase` only on overline labels (`10px` bold). Never uppercase headlines. +- Letter-spacing: default for most sizes; `+0.1em` for overline labels only. +- Never center-align body text. Left-align always. Headlines may be center-aligned only in hero sections. + +--- + +## 4. Component Stylings + +### Buttons + +| Variant | Background | Text | Border | Radius | +|---------|-----------|------|--------|--------| +| Primary | `#1DB954` | `#000000` | none | `500px` (pill) | +| Primary hover | `#1ED760` | `#000000` | none | `500px` | +| Secondary | `transparent` | `#FFFFFF` | `2px solid #FFFFFF` | `500px` | +| Secondary hover | `#FFFFFF` `1a` | `#FFFFFF` | `2px solid #FFFFFF` | `500px` | +| Ghost | `transparent` | `#B3B3B3` | none | `500px` | +| Ghost hover | `#333333` | `#FFFFFF` | none | `500px` | + +- Padding: `12px 32px` for standard, `8px 20px` for compact. +- Font: `14px bold` with `letter-spacing: 0.02em`. +- All buttons are pill-shaped (`border-radius: 500px`). Rounded rectangles are forbidden for CTAs. +- Icon + label buttons: `8px` gap, icon at `20px` size. + +### Cards + +- Background: `#181818` +- Border: none +- Radius: `8px` +- Padding: `16px` +- Hover: background transitions to `#282828` over `300ms ease` +- Image: square aspect ratio for album/playlist art, `4px` radius on the image +- Title: `16px bold`, single line with `text-overflow: ellipsis` +- Subtitle: `14px regular`, `#B3B3B3`, single line ellipsis + +### Now Playing Bar + +- Position: fixed bottom, full width +- Height: `80px` +- Background: `#181818` with `1px` top border `#282828` +- Progress bar: track `#535353`, filled `#FFFFFF`, hover filled `#1DB954` +- Progress bar height: `4px` default, `6px` on hover +- Thumb: `12px` white circle, appears on hover only + +### Navigation / Sidebar + +- Background: `#000000` +- Width: `280px` (collapsible to `72px`) +- Active item: `#282828` background, `#FFFFFF` text, `3px` left border `#1DB954` +- Inactive item: `transparent` background, `#B3B3B3` text +- Icon size: `24px` +- Item height: `40px`, padding `0 16px` + +### Input Fields + +- Background: `#333333` +- Border: `2px solid transparent` +- Focus border: `2px solid #1DB954` +- Text: `#FFFFFF` `14px` +- Placeholder: `#6A6A6A` +- Radius: `4px` +- Padding: `12px 16px` + +### Toggles / Switches + +- Track off: `#535353`, on: `#1DB954` +- Knob: `#FFFFFF` `20px` circle +- Track size: `40px x 20px`, radius `10px` + +### Chips / Pills + +- Background: `#333333` +- Text: `#FFFFFF` `13px regular` +- Selected: background `#1DB954`, text `#000000` `13px bold` +- Radius: `500px` +- Padding: `6px 16px` + +--- + +## 5. Layout Principles + +### Grid + +- 12-column grid with `16px` gutters on desktop. +- Sidebar occupies fixed columns; main content fills remaining space. +- Card grids: auto-fill with `minmax(180px, 1fr)`. + +### Spacing Scale + +| Token | Value | Usage | +|-------|-------|-------| +| `--space-xs` | `4px` | Tight internal gaps | +| `--space-sm` | `8px` | Icon gaps, inline spacing | +| `--space-md` | `16px` | Standard padding, card gutters | +| `--space-lg` | `24px` | Section internal padding | +| `--space-xl` | `32px` | Between sections | +| `--space-2xl` | `48px` | Page-level vertical rhythm | +| `--space-3xl` | `64px` | Hero section vertical padding | + +### Album-Art-Driven Layout + +Content grids are organized around cover art. The image is the primary visual anchor: + +1. Image occupies the top or left of every content card. +2. Card hierarchy: image > title > subtitle. No card is without an image. +3. In hero contexts (playlist header, artist page), the cover art spans large and the background is tinted with the dominant color of the artwork. +4. List views use a `56px x 56px` thumbnail; grid views use square cards starting at `180px`. + +### Z-Pattern for Landing Pages + +Hero with bold headline and green CTA on the left, visual on the right. Alternate sections follow a zigzag. Always end with a green CTA section. + +--- + +## 6. Depth & Elevation + +Spotify uses minimal elevation. The dark palette does most of the separation work. + +| Level | Shadow | Usage | +|-------|--------|-------| +| 0 | none | Default surface, page background | +| 1 | `0 2px 8px rgba(0,0,0,0.3)` | Cards at rest, sidebar | +| 2 | `0 4px 16px rgba(0,0,0,0.4)` | Hovered cards, tooltips | +| 3 | `0 8px 32px rgba(0,0,0,0.6)` | Modals, dropdown menus, now-playing bar | + +### Rules + +- No colored shadows. Shadows are always pure black with opacity. +- No borders on elevated surfaces. Use shadow alone for separation. +- The now-playing bar uses elevation level 3 because it must float above all scrolling content. +- Card hover transitions: shadow rises from level 1 to level 2 over `300ms ease`. +- Background color transitions on hover (`#181818` to `#282828`) happen simultaneously with shadow changes. + +--- + +## 7. Do's and Don'ts + +### Do + +- Use `#1DB954` sparingly. It is an accent, not a fill color for large areas. +- Let album art and imagery carry the visual weight. The UI frame should disappear. +- Use pill-shaped buttons for all CTAs. +- Show progress with the green-to-gray bar pattern. +- Use `#B3B3B3` for secondary text on dark backgrounds. +- Transition hover states with `300ms ease` or `200ms ease-out`. +- Make image thumbnails square for music content. +- Extract tint color from album art for atmospheric backgrounds. +- Use bold/black weight for any text above `20px`. + +### Don't + +- Never use `#1DB954` as a background for large sections. Green surfaces beyond buttons and badges feel garish. +- Never use rounded-rectangle buttons. Spotify buttons are always pill-shaped. +- Never place white body text on `#121212` for paragraphs longer than one line. +- Never add colored borders or outlines to cards. Cards are borderless. +- Never use drop shadows on text. Spotify text is always flat. +- Never use serif fonts or decorative typefaces. +- Never animate more than one property at a time on interactive elements (hover = background color OR shadow, not both in conflicting directions). +- Never use `#1DB954` and `#E91429` adjacent to each other. The green-red clash is visually jarring. +- Never use placeholder images or empty states without a clear icon and label. The `#282828` card with a centered `#6A6A6A` icon and label is the standard empty state. + +--- + +## 8. Responsive Behavior + +### Breakpoints + +| Name | Min Width | Layout | +|------|-----------|--------| +| Mobile | `0` | Single column, bottom nav, no sidebar | +| Tablet | `768px` | Two-column, collapsible sidebar | +| Desktop | `1024px` | Full sidebar + main content | +| Wide | `1440px` | Max-width container `1680px`, centered | + +### Mobile Adaptations + +- Sidebar becomes a bottom tab bar with 5 items (Home, Search, Library, Premium, icon-only). +- Cards switch to a horizontal scroll row instead of a grid. +- Now-playing bar shrinks to `64px` with artwork thumbnail, track name, and play/pause only. Full controls expand on tap. +- Hero sections collapse: headline drops to `--text-title` (`32px`), image scales down. +- Pill buttons stretch to full width with `16px` horizontal margin. +- Search input is full width with `16px` horizontal padding. + +### Tablet Adaptations + +- Sidebar collapses to icon-only (`72px` width) by default, expands on tap. +- Card grid uses `minmax(150px, 1fr)`. +- Now-playing bar remains at `80px`. + +### Desktop Adaptations + +- Sidebar is fully expanded (`280px`) with text labels. +- Card grid uses `minmax(180px, 1fr)`. +- Hover states are active (no hover on mobile/tablet touch). + +### Image Sizing + +| Context | Mobile | Tablet | Desktop | +|---------|--------|--------|---------| +| Hero cover art | `128px` | `192px` | `232px` | +| Grid card thumbnail | `100%` width, square | `100%` width, square | `100%` width, square | +| List thumbnail | `48px` | `56px` | `56px` | +| Now-playing art | `48px` | `56px` | `56px` | + +### Touch Targets + +- Minimum `44px x 44px` on mobile and tablet. +- Minimum `32px x 32px` on desktop. +- Playback controls (play/pause, skip): minimum `48px x 48px` on all sizes. diff --git a/addons/officials/crew/designer/skills/design-system-picker/design-systems/starbucks.md b/addons/officials/crew/designer/skills/design-system-picker/design-systems/starbucks.md new file mode 100644 index 00000000..fe530128 --- /dev/null +++ b/addons/officials/crew/designer/skills/design-system-picker/design-systems/starbucks.md @@ -0,0 +1,342 @@ +# Starbucks Design System + +Warm Siren green on a cream canvas. The "third place" made digital — not home, not work, but somewhere that feels like both. Starbucks' visual language wraps every surface in the warmth of a coffeehouse: soft cream backgrounds instead of sterile whites, pill-shaped buttons that feel approachable, a family of greens that echo the iconic apron, and typefaces designed to feel like they have always been part of the brand. The Siren is the muse; everything else serves her world. + +--- + +## 1. Visual Theme & Atmosphere + +Starbucks' design language translates the physical coffeehouse into a digital experience. The core philosophy is "third place" — a welcoming space between home and work where warmth, craft, and community converge. Every surface should feel like it has been touched by human hands, not stamped by a machine. The warm cream canvas is the coffeehouse wall; the green accents are the barista's apron visible across the room; the rounded corners are the curve of a ceramic cup. + +The page is a coffeehouse. Cream and warm neutrals are the walls and wood tables. Starbucks Green is the apron and the Siren — visible from across the room, never wallpapered over every surface. Photography shows real people in real moments: hands around cups, steam rising, morning light through windows. Illustration carries the hand-drawn legacy — the Siren, line art, seasonal artwork — never generic iconography. Typography speaks in Sodo Sans for everyday warmth, Pike for bold menu-board headlines, and Lander for artful expressive moments. + +Color is anchored in a family of greens that leverages instant brand recognition. The green is never decorative — it is structural. Expressive seasonal palettes evolve with trends, but brand green is always present, either in the composition or through the Siren logo. The overall feeling is optimistic, joyful, and recognizably Starbucks: calm confidence with artful warmth. + +**Keywords:** warm, inviting, community, craft, approachable, coffeehouse, third place, Siren green, natural, artful + +--- + +## 2. Color Palette & Roles + +Starbucks' color system centers on a four-tier family of greens — Starbucks Green, Accent Green, Light Green, and House Green — layered over a warm cream canvas. Neutrals are warm, never cool. The dark palette uses deep green and earth tones, never pure black. + +### Light Mode + +| Token | Hex | Role | +|-------|-----|------| +| `--color-canvas` | `#F2F0EB` | Page background — warm cream, the coffeehouse canvas. Never pure white. | +| `--color-surface` | `#FFFFFF` | Card and container background on cream canvas | +| `--color-surface-warm` | `#FAF8F5` | Subtle warm tint for featured sections, alternating bands | +| `--color-surface-green` | `#D4E9E2` | Light green surface for highlighted content, reward tiers | +| `--color-ink` | `#1E3932` | Primary text — House Green, deep and warm, never pure black | +| `--color-body` | `#2F2E2C` | Default running text — warm black with earth undertone | +| `--color-muted` | `#61605B` | Secondary text, descriptions, helper text | +| `--color-muted-soft` | `#8C8B86` | Tertiary text, placeholder, timestamps | +| `--color-starbucks-green` | `#00704A` | Siren Green — primary brand accent, CTAs, active states, logo. The iconic apron green. | +| `--color-accent-green` | `#00A862` | Accent Green — hover and secondary green actions, progress indicators | +| `--color-light-green` | `#D4E9E2` | Light Green — tinted backgrounds, selected states, subtle brand presence | +| `--color-house-green` | `#1E3932` | House Green — deep green-black, primary text, footer backgrounds | +| `--color-primary-hover` | `#005C3E` | Hover state for Siren Green elements | +| `--color-primary-active` | `#004D34` | Active/pressed state for Siren Green | +| `--color-on-green` | `#FFFFFF` | White text on green buttons and badges | +| `--color-warm-neutral` | `#C8C5BE` | Warm gray borders, subtle dividers | +| `--color-cool-neutral` | `#A7A9AB` | Cool neutral for specific secondary elements, rarely used | +| `--color-gold` | `#C2A461` | Gold/Star accent — Rewards stars, premium tier indicators | +| `--color-success` | `#00704A` | Positive, confirmed — reuses brand green | +| `--color-warning` | `#D4A017` | Caution, pending states | +| `--color-error` | `#C62828` | Destructive, error, unavailable | +| `--color-border` | `#E0DDD6` | Default borders, input borders, card outlines | + +### Dark Mode + +| Token | Hex | Role | +|-------|-----|------| +| `--color-canvas` | `#1E3932` | Page background — House Green, deep and warm, not pure black | +| `--color-surface` | `#2A4A40` | Card and container background on dark canvas | +| `--color-surface-elevated` | `#345C4F` | Elevated container — modals, dropdowns | +| `--color-surface-warm` | `#243D35` | Subtle warm differentiation on dark canvas | +| `--color-surface-green` | `#1A3B30` | Dark green tint for highlighted content | +| `--color-ink` | `#F2F0EB` | Primary text on dark — warm cream | +| `--color-body` | `#D4D1CB` | Default running text on dark | +| `--color-muted` | `#A0A08C` | Secondary text on dark | +| `--color-muted-soft` | `#7A7A6E` | Tertiary text on dark | +| `--color-starbucks-green` | `#00A862` | Siren Green shifts lighter on dark — Accent Green becomes the primary interactive color | +| `--color-accent-green` | `#1DB954` | Brighter accent on dark — hover states, secondary green | +| `--color-light-green` | `#1A3B30` | Dark green tint background | +| `--color-house-green` | `#1E3932` | Unchanged — this IS the dark canvas | +| `--color-primary-hover` | `#00C070` | Hover on dark — shifted lighter | +| `--color-primary-active` | `#00D480` | Active on dark — shifted lighter still | +| `--color-on-green` | `#FFFFFF` | White text on green buttons (unchanged) | +| `--color-warm-neutral` | `#4A5E55` | Borders on dark | +| `--color-gold` | `#D4B46A` | Gold on dark — shifted lighter for contrast | +| `--color-success` | `#1DB954` | Positive on dark — shifted lighter | +| `--color-warning` | `#E6B422` | Warning on dark | +| `--color-error` | `#EF5350` | Error on dark — shifted lighter | +| `--color-border` | `#3A5A4D` | Borders on dark surfaces | + +**Rule:** Starbucks Green (`#00704A`) is the brand's most identifiable asset — visible for blocks, as they say. On dark surfaces it shifts to Accent Green (`#00A862`) for legibility. The warm cream canvas (`#F2F0EB`) is never replaced with pure white in light mode, and pure black (`#000000`) is never used as a dark background — House Green (`#1E3932`) carries the warmth into darkness. The green family must always be present, either within the composition or through the Siren logo. + +### Seasonal Expression Palettes + +Starbucks evolves expressive colors with seasonal trends while keeping brand greens constant: + +| Season | Accent Tones | Usage | +|--------|-------------|-------| +| Spring | Soft pinks, fresh yellows, sage | Promotional banners, seasonal menus, app highlights | +| Summer | Bright corals, turquoise, sunny gold | Cold beverage promotions, Frappuccino campaigns | +| Fall | Burnt orange, deep burgundy, warm brown | Pumpkin spice, holiday countdown, warm beverage imagery | +| Winter/Nitro | Deep navy, icy blue, silver | Nitro Cold Brew, holiday red cup season, gift guides | + +--- + +## 3. Typography Rules + +### Font Stack + +- **Primary (body, UI):** `Sodo Sans`, `-apple-system`, `BlinkMacSystemFont`, `Segoe UI`, `Roboto`, `Helvetica Neue`, `sans-serif` +- **Display (headlines, wayfinding):** `Pike`, `Sodo Sans Condensed`, `Impact`, `Arial Narrow`, `sans-serif` +- **Expressive (accent moments):** `Lander`, `Georgia`, `Cambria`, `Times New Roman`, `serif` + +Sodo Sans is a geometric sans with a friendly character — double-storey 'g' for legibility, symmetrical 'u' echoing the brand logotype. It comes in three widths: Normal, Narrow, and Condensed, providing finer control in typesetting. Pike is a condensed display face with an increased x-height, designed for impactful headlines and menu boards — it shares DNA with Sodo Sans but has its own stance. Lander is a serif with 1970s warmth, drawn in three optical sizes: Grande (large display), Tall (headlines), and Short (text). It provides artful, expressive contrast to the sans families. + +### Scale + +| Token | Size | Weight | Line Height | Tracking | Font | Usage | +|-------|------|--------|-------------|----------|------|-------| +| `--text-hero` | `clamp(40px, 5vw, 72px)` | 700 | 1.05 | `-0.02em` | Pike | Hero headlines, campaign statements, splash pages | +| `--text-display` | `clamp(32px, 3.5vw, 56px)` | 700 | 1.1 | `-0.015em` | Pike | Section heroes, major announcements | +| `--text-headline` | `clamp(24px, 2vw, 36px)` | 700 | 1.15 | `-0.01em` | Sodo Sans | Section headers, card hero titles | +| `--text-title` | `clamp(20px, 1.5vw, 28px)` | 600 | 1.2 | `-0.005em` | Sodo Sans | Subsection headers, feature titles | +| `--text-title-sm` | `18px` | 600 | 1.25 | `0` | Sodo Sans | List headers, nav items, card titles | +| `--text-body-lg` | `16px` | 400 | 1.6 | `0` | Sodo Sans | Intro paragraphs, feature descriptions, menu descriptions | +| `--text-body` | `14px` | 400 | 1.5 | `0` | Sodo Sans | Body text, list items, product details | +| `--text-caption` | `12px` | 400 | 1.4 | `0.01em` | Sodo Sans | Nutritional info, timestamps, small labels | +| `--text-overline` | `11px` | 700 | 1.6 | `0.08em` | Sodo Sans | Uppercase labels, category tags, section markers | +| `--text-expressive` | `clamp(28px, 3vw, 48px)` | 400 | 1.15 | `0` | Lander | Expressive moments, editorial headlines, seasonal features | + +### Rules + +- Headlines are always bold (700) or semibold (600). Regular-weight headlines are forbidden. +- Sodo Sans is the default for everything. Pike is reserved for display headlines where impact is needed — hero sections, menu boards, campaign banners. Lander is for artful, expressive moments — editorial features, seasonal storytelling, accent pull quotes. +- Body text on cream canvas uses `#2F2E2C` (warm black), never pure `#000000`. Pure black on warm cream creates visual tension. +- Use `text-transform: uppercase` only on overline labels and Pike display headlines. Sodo Sans headlines are always title-case or sentence-case. +- Letter-spacing: negative for headlines ( Pike at `-0.02em` to `-0.01em`), zero for body, positive only for overline labels and all-caps Pike treatments. +- Pike is frequently set in all-caps with generous tracking for menu boards and wayfinding — this is a signature Starbucks typographic treatment. +- Never center-align body text. Left-align always. Headlines may be center-aligned only in hero sections and campaign banners. +- Lander optical sizes: Grande for >48px display, Tall for 24-48px headlines, Short for <24px text. Using the wrong optical size produces either spindly hairlines or overly thick strokes. + +--- + +## 4. Component Stylings + +### Buttons + +| Variant | Background | Text | Border | Radius | +|---------|-----------|------|--------|--------| +| Primary | `#00704A` | `#FFFFFF` | none | `50px` (pill) | +| Primary hover | `#005C3E` | `#FFFFFF` | none | `50px` | +| Primary active | `#004D34` | `#FFFFFF` | none | `50px` | +| Secondary | `transparent` | `#00704A` | `2px solid #00704A` | `50px` | +| Secondary hover | `#D4E9E2` | `#005C3E` | `2px solid #005C3E` | `50px` | +| Ghost | `transparent` | `#1E3932` | none | `50px` | +| Ghost hover | `#D4E9E2` | `#1E3932` | none | `50px` | +| Floating CTA (Frap) | `#00704A` | `#FFFFFF` | none | `999px` (circle) | + +- Padding: `14px 32px` for standard, `10px 24px` for compact. +- Font: Sodo Sans `14px` weight 600 with `letter-spacing: 0.02em`. +- All buttons are pill-shaped (`border-radius: 50px`). Starbucks uses a universal 50px pill button — this is a defining signature. +- The floating circular CTA (Frap button) is used for primary single-action moments — order button, reorder, add to cart. It is a circle (not pill), typically 50px diameter, positioned floating bottom-right on mobile. +- Icon + label buttons: `8px` gap, icon at `20px` size. +- Transition: `200ms ease-out` for all state changes. + +### Navigation + +- Fixed top bar, `height: 60px`, `#FFFFFF` background with 1px `#E0DDD6` bottom border. +- Logo (Siren) left, navigation center or left-aligned, cart/account right. +- Siren logo preferred unlocked from wordmark — used by itself for a more modern, open presentation. +- Nav items: Sodo Sans `14px` weight 600, `#1E3932` text, `#00704A` active state. +- Hover: text color transitions to `#00704A` over `150ms ease`. +- Mobile: hamburger menu, Siren logo center, cart icon right. + +### Cards (Product / Menu Item) + +- Background: `#FFFFFF`, `border-radius: 16px`, no border (shadow-based depth on cream canvas). +- Image: `border-radius: 16px` matching card, aspect-ratio 1/1 for beverages, 3/2 for food/lifestyle. +- Product title: Sodo Sans `16px` weight 600, `#1E3932`, single line with `text-overflow: ellipsis`. +- Description: Sodo Sans `14px` weight 400, `#61605B`, two-line clamp. +- Price: Sodo Sans `16px` weight 700, `#1E3932`, positioned bottom-right. +- Hover: subtle lift (`translateY(-2px)`) + shadow increase over `200ms ease-out`. +- Featured cards: `border-radius: 20px`, larger padding, lifestyle imagery. + +### Image Treatments + +- **Lifestyle photography:** Warm lighting, natural settings, community moments. Hands holding cups, baristas at work, morning rituals. Never sterile studio shots. +- **Product photography:** Clean but warm — beverages on natural surfaces (wood, marble, canvas). Shallow depth of field, soft directional lighting. +- **Illustration:** Rooted in brand heritage. The Siren, line art, seasonal artwork. Texture, photo collage, composition, and graphic details give a custom, handcrafted feel. Never generic flat icons. +- **Image radius:** `16px` for standard, `20px` for hero/featured, `12px` for thumbnails, `8px` for inline images. +- **Overlay:** When text overlays imagery, use a gradient overlay from `rgba(30,57,50,0.7)` to `transparent` — House Green, not black. + +### Data / Specs (Nutritional, Order Details) + +- Background: `#FFFFFF`, `border-radius: 12px`, 1px `#E0DDD6` border. +- Header row: `#D4E9E2` light green background, Sodo Sans `12px` weight 700. +- Data rows: alternating `#FFFFFF` and `#FAF8F5`. +- Values: Sodo Sans `14px` weight 600 for numbers, `14px` weight 400 for labels. +- Divider: 1px `#E0DDD6` between rows. + +### Rewards / Loyalty Elements + +- Star icon: `#C2A461` gold fill, animated on earn. +- Progress bar: track `#E0DDD6`, filled `#C2A461` gradient to `#D4B46A`. +- Tier badges: circular with Sodo Sans Condensed all-caps label. +- Points/Stars display: Pike `24px` weight 700, `#C2A461` color. + +--- + +## 5. Layout Principles + +### Grid + +- 12-column grid with `24px` gutters on desktop. +- Content max-width: `1200px`, centered. +- Card grids: `auto-fill, minmax(260px, 1fr)` for product listings. +- Menu/product layouts often use asymmetric grids — a hero feature card spanning 2 columns alongside standard single-column cards. + +### Spacing Scale + +| Token | Value | Usage | +|-------|-------|-------| +| `--space-xs` | `4px` | Tight internal gaps, icon-text spacing | +| `--space-sm` | `8px` | Inline spacing, chip padding | +| `--space-md` | `16px` | Standard padding, card internal spacing | +| `--space-lg` | `24px` | Card gutters, section internal padding | +| `--space-xl` | `32px` | Between sections | +| `--space-2xl` | `48px` | Major section separation | +| `--space-3xl` | `64px` | Hero section vertical padding | +| `--space-4xl` | `96px` | Page-level vertical rhythm on desktop | + +### Alignment Rules + +- Left-align all body text and data. Center-align only hero headlines and campaign statements. +- Product imagery is typically center-aligned within its card container. +- Price and CTA are right-aligned or bottom-aligned within cards. +- The Siren logo is always centered within its container — never cropped, never rotated, never tilted. +- Menu items follow a consistent pattern: image left (or top), text content right (or bottom), price/CTA bottom-right. + +### Coffeehouse Rhythm + +Layout should breathe like a coffeehouse — not cramped like a fast-food menu board, and not sparse like a corporate lobby. Sections have generous vertical spacing. Content areas feel like distinct "seating zones" — the rewards area feels different from the menu area, separated by warmth and spacing rather than hard dividers. Warm cream (`#F2F0EB`) backgrounds alternate with white (`#FFFFFF`) sections to create natural flow without visible borders. + +--- + +## 6. Depth & Elevation + +Starbucks uses gentle, warm shadows on the cream canvas. The cream background does most of the separation work; shadows add subtle lift, not dramatic floating. + +| Level | Shadow | Usage | +|-------|--------|-------| +| 0 | none | Default surface, cream canvas background | +| 1 | `0 2px 8px rgba(30,57,50,0.08)` | Cards at rest on cream canvas | +| 2 | `0 4px 16px rgba(30,57,50,0.12)` | Hovered cards, elevated inputs | +| 3 | `0 8px 24px rgba(30,57,50,0.16)` | Modals, dropdown menus, sticky elements | +| 4 | `0 12px 40px rgba(30,57,50,0.20)` | Full-screen overlays, prominent floating CTAs | + +### Rules + +- Shadows use House Green (`rgba(30,57,50,...)`) as the shadow color instead of pure black — this produces a warmer, more natural shadow that sits harmoniously on the cream canvas. Pure black shadows create cold contrast against warm surfaces. +- No colored shadows beyond the warm green tint. No green glow effects. +- Card hover: shadow rises from level 1 to level 2, combined with `translateY(-2px)` over `200ms ease-out`. +- Elevated surfaces (modals, dropdowns) on cream canvas use white (`#FFFFFF`) backgrounds — the contrast between white surface and cream canvas provides inherent separation even without shadow. +- On dark mode, shadows use deeper green tints: `rgba(0,0,0,0.3)` through `rgba(0,0,0,0.6)`. +- Borders and shadows are never used together on the same element. Choose one method of separation. + +--- + +## 7. Do's and Don'ts + +### Do + +- Always maintain a presence of brand green — either within the composition or through the Siren logo. A page without green is not Starbucks. +- Use the warm cream canvas (`#F2F0EB`) as the default light background. Pure white is for cards and elevated surfaces only. +- Use pill-shaped buttons (`border-radius: 50px`) for all CTAs. This is a Starbucks signature. +- Let lifestyle photography carry the warmth — real moments, real people, natural light, community settings. +- Use Sodo Sans for body and UI text, Pike for impactful headlines and menu boards, Lander sparingly for expressive accent moments. +- Round everything generously — 16px for cards, 12px for inputs, 50px for buttons. Nothing sharp, nothing aggressive. +- Write conversationally — "What can we get started for you?" not "Begin Order". Warmth extends to copy. +- Use warm-tinted shadows (`rgba(30,57,50,...)`) instead of cold black shadows on the cream canvas. +- Feature the Siren logo unlocked from the wordmark for a modern, open presentation. +- Use seasonal expression palettes to stay relevant while keeping brand greens constant. +- Show the gold star (`#C2A461`) prominently in Rewards contexts — loyalty is a core experience. +- Design for mobile-first. The Starbucks app is the primary digital touchpoint for most customers. + +### Don't + +- Never use pure `#000000` as a background in dark mode. House Green (`#1E3932`) is the dark canvas. Pure black feels cold and corporate, not warm and inviting. +- Never use Siren Green (`#00704A`) as a background fill for large areas. Green is an accent and brand anchor, not a surface color. +- Never apply sharp corners (`border-radius: 0`) to any interactive element or card. Sharp corners contradict the warm, approachable brand. +- Never use cool gray palettes or blue undertones in neutrals. Starbucks neutrals are warm — cream, warm gray, earth tones. +- Never use generic stock photography or flat vector icons. Every image should feel like a real coffeehouse moment; every illustration should carry the handcrafted quality of the Siren tradition. +- Never center-align body text paragraphs. Left-align always. +- Never use regular weight (400) for headlines. Headlines demand weight — semibold (600) minimum, bold (700) preferred. +- Never rotate, distort, crop, or tilt the Siren logo. She always faces forward, centered, and complete. +- Never use more than two of the three typeface families (Sodo Sans, Pike, Lander) in a single view. Three is acceptable only in hero/campaign layouts where Lander provides a deliberate expressive accent. +- Never place white body text on `#F2F0EB` cream backgrounds — insufficient contrast. Use `#1E3932` House Green for text on cream. +- Never use green and red adjacent as equal-weight accents. The green-red clash reads as Christmas, not coffeehouse. +- Never hide pricing in product cards. Price visibility builds trust. + +--- + +## 8. Responsive Behavior + +### Breakpoints + +| Name | Min Width | Layout | +|------|-----------|--------| +| Mobile | `0` | Single column, bottom nav, stacked cards, floating CTA | +| Tablet | `768px` | Two-column grid, collapsible side menu, wider cards | +| Desktop | `1024px` | Three-to-four column grid, full navigation, side panels | +| Wide | `1440px` | Max-width container `1200px`, centered, breathing room | + +### Mobile Adaptations + +- Navigation collapses to bottom tab bar (Home, Order, Rewards, Stores, Account) — the Starbucks app pattern. +- The floating circular CTA (Frap button) appears bottom-right for primary order/reorder actions. +- Product cards switch to horizontal scroll rows ("Featured Drinks", "Popular Near You") instead of grid. +- Hero sections collapse: headline drops to `--text-headline` size, imagery scales to single-column full-width. +- Pill buttons stretch to full width with `16px` horizontal margin. +- Menu browsing uses a vertically scrollable category list on the left or top chips for filtering. +- Order details become a bottom sheet that slides up, with a sticky "Checkout" pill at the bottom. +- Search input is full width with `16px` horizontal padding. +- Touch targets: minimum `44px x 44px` on all interactive elements. + +### Tablet Adaptations + +- Product grid uses `minmax(220px, 1fr)` — typically 2-3 columns. +- Navigation may use a compact top bar with icon + label. +- Order flow uses a split view: menu browsing left, cart summary right. +- Cards show slightly more content (three-line descriptions instead of two). + +### Desktop Adaptations + +- Full four-column product grid with `minmax(260px, 1fr)`. +- Top navigation with full text labels, Siren logo, and search bar. +- Hero sections use asymmetric layouts: large image + overlaid text, or split 60/40 text-image. +- Hover states are active (not available on touch devices). +- Footer expands with full link columns and Siren logo. + +### Image Sizing + +| Context | Mobile | Tablet | Desktop | +|---------|--------|--------|---------| +| Hero beverage image | `200px` | `280px` | `360px` | +| Product card thumbnail | `100%` width, 1:1 | `100%` width, 1:1 | `100%` width, 1:1 | +| Category icon | `48px` | `56px` | `64px` | +| Store/location thumbnail | `80px` | `96px` | `120px` | +| Rewards star | `24px` | `28px` | `32px` | + +### Touch Targets + +- Minimum `44px x 44px` on mobile and tablet. +- Minimum `32px x 32px` on desktop. +- Order/Add-to-cart buttons: minimum `50px` height on all sizes (matching the pill button signature). +- The floating CTA (Frap button): `56px` diameter on mobile, `50px` on desktop. diff --git a/addons/officials/crew/designer/skills/design-system-picker/design-systems/stripe.md b/addons/officials/crew/designer/skills/design-system-picker/design-systems/stripe.md new file mode 100644 index 00000000..5bf813df --- /dev/null +++ b/addons/officials/crew/designer/skills/design-system-picker/design-systems/stripe.md @@ -0,0 +1,457 @@ +# Stripe Design System + +## 1. Visual Theme & Atmosphere + +Stripe's visual identity is built on **engineered elegance** — a clean white canvas punctuated by signature purple gradients, weight-300 body typography that feels light and confident, and a trust-forward financial aesthetic that reads as precise without feeling cold. The system alternates between bright white surfaces and deep navy sections, with purple serving as the sole chromatic accent on chrome. Every surface feels considered and load-bearing; decoration is minimal and purposeful. + +The design reads as "infrastructure you can trust rendered with the restraint of a Swiss poster." Headlines are set in a clean geometric sans at light weights (300) with generous letter-spacing, creating an airy authority. Code and data surfaces sit in dark wells that contrast sharply with the white canvas. Purple gradients flow across hero bands and CTAs, giving the system its signature warmth within a predominantly neutral palette. + +**Key Characteristics:** + +- White canvas (`#FFFFFF`) as the default page background with `#F6F9FC` for inset bands +- Signature purple gradient (`#635BFF` to `#7A73FF`) reserved for CTAs, hero bands, and accent surfaces +- Deep navy (`#0A2540`) for dark sections and code wells — never pure black +- Weight-300 body typography as the default, creating the system's characteristic lightness +- Generous whitespace (96px+ section rhythm) with tight in-card padding (16-24px) +- Subtle layered shadows that lift cards gently off the canvas — no hard edges +- Rounded corners in the 6-8px range for cards and containers, 4px for inputs and small elements + +--- + +## 2. Color Palette & Roles + +### Brand & Accent + +| Name | Hex | Role | +|------|-----|------| +| `purple-600` | `#635BFF` | Primary brand color. CTAs, links, active states, hero gradient start. | +| `purple-500` | `#7A73FF` | Primary hover, gradient end stop, lighter accent surfaces. | +| `purple-400` | `#8B83FF` | Focus rings, subtle purple borders, disabled-active purple. | +| `purple-100` | `#E8E5FF` | Soft purple tint for backgrounds of highlighted or selected items. | +| `purple-50` | `#F4F2FF` | Faintest purple wash — inline code backgrounds, subtle row hover. | +| `gradient-hero` | `linear-gradient(135deg, #635BFF 0%, #7A73FF 100%)` | Hero band backgrounds, primary CTA fills, feature accent bands. | + +### Surface + +| Name | Hex | Role | +|------|-----|------| +| `white` | `#FFFFFF` | Default page canvas, card backgrounds, input fills. | +| `gray-50` | `#F6F9FC` | Inset bands, alternating section backgrounds, table row stripes. | +| `gray-100` | `#E8ECF1` | Dividers, hairline borders on light surfaces, subtle separators. | +| `gray-200` | `#C1C9D2` | Disabled borders, placeholder text underline, secondary dividers. | +| `navy-900` | `#0A2540` | Dark section backgrounds, code wells, footer canvas. | +| `navy-800` | `#1A2E4A` | Elevated dark surface, dark card backgrounds. | +| `navy-700` | `#2D3E54` | Secondary dark surface, in-well panel backgrounds. | + +### Text + +| Name | Hex | Role | +|------|-----|------| +| `ink` | `#1A1F36` | Primary body text on light surfaces. Near-black with warmth. | +| `body` | `#425466` | Long-form body copy where ink reads too heavy. | +| `charcoal` | `#5A6980` | Captions, metadata, secondary content. | +| `mute` | `#697386` | Placeholder text, supporting copy, inactive labels. | +| `ash` | `#8792A2` | Disabled text, tertiary labels, least-emphasis utility. | +| `stone` | `#A3ACB9` | Disabled foreground, neutral icon outlines. | +| `on-dark` | `#FFFFFF` | Primary text on navy/dark surfaces. | +| `on-dark-mute` | `rgba(255,255,255,0.72)` | Secondary text on dark surfaces. | + +### Semantic + +| Name | Hex | Role | +|------|-----|------| +| `success` | `#30B566` | Success states, confirmations, positive indicators. | +| `success-soft` | `#E6F9EF` | Success background tint. | +| `warning` | `#E5A54B` | Warning states, caution indicators. | +| `warning-soft` | `#FFF6E9` | Warning background tint. | +| `error` | `#D84040` | Error states, destructive actions, validation failures. | +| `error-soft` | `#FDE8E8` | Error background tint. | +| `info` | `#00B4D8` | Informational badges, neutral highlights. | +| `info-soft` | `#E3F6FC` | Info background tint. | + +### Dark Mode Override + +| Name | Hex | Role | +|------|-----|------| +| `dm-canvas` | `#0A2540` | Default page background. | +| `dm-surface` | `#1A2E4A` | Card and elevated panel background. | +| `dm-surface-elevated` | `#2D3E54` | Button fills, input fills on dark. | +| `dm-hairline` | `rgba(255,255,255,0.08)` | Card borders on dark surfaces. | +| `dm-hairline-strong` | `rgba(255,255,255,0.16)` | Stronger dividers on dark. | + +--- + +## 3. Typography Rules + +### Font Families + +| Role | Family | Fallback | Notes | +|------|--------|----------|-------| +| Display & UI | `Inter` | `-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif` | Primary face. Load with `font-display: swap`. | +| Code | `JetBrains Mono` | `"SFMono-Regular", Consolas, "Liberation Mono", Menlo, monospace` | API examples, inline code, terminal blocks. | + +Inter is the primary typeface. Stripe's signature lightness comes from using **weight 300** as the default body weight — this is non-negotiable. Headlines and display type use weight 500 or 600 for structural contrast, never 700+. The result is an airy hierarchy where weight contrast does the work that size alone cannot. + +### Hierarchy + +| Token | Size | Weight | Line Height | Letter Spacing | Use | +|-------|------|--------|-------------|----------------|-----| +| `display-2xl` | 72px | 600 | 1.05 | -1.5px | Hero headline. One per page. | +| `display-xl` | 56px | 600 | 1.1 | -1px | Section openers, landing page headlines. | +| `display-lg` | 44px | 500 | 1.15 | -0.5px | Sub-section display, pricing tier names. | +| `display-md` | 32px | 500 | 1.25 | -0.3px | Feature card titles, in-section headlines. | +| `heading-lg` | 24px | 500 | 1.35 | -0.2px | Card headings, panel titles. | +| `heading-md` | 20px | 500 | 1.4 | 0 | In-card section heads, sidebar titles. | +| `heading-sm` | 16px | 500 | 1.5 | 0 | Small headings, form group labels. | +| `body-lg` | 18px | 300 | 1.65 | 0 | Lead paragraphs, hero subtitles. | +| `body-md` | 16px | 300 | 1.6 | 0 | Default body text, form labels, descriptions. | +| `body-sm` | 14px | 300 | 1.6 | 0 | Card descriptions, metadata, secondary copy. | +| `caption` | 12px | 400 | 1.5 | 0.2px | Timestamps, footer links, small utility text. | +| `link-md` | 16px | 500 | 1.6 | 0 | Inline body links — weight 500 distinguishes from body 300. | +| `button-lg` | 16px | 500 | 1.0 | 0.2px | Large CTA button label. | +| `button-md` | 14px | 500 | 1.0 | 0.2px | Default button label. | +| `code-md` | 14px | 400 | 1.7 | 0 | Code blocks, inline code, API paths. | +| `code-sm` | 12px | 400 | 1.6 | 0 | Tab labels, small code tokens. | + +### Principles + +- **Weight-300 is the default.** Body text at weight 300 creates the characteristic Stripe lightness. Never bump body to 400 for emphasis — use weight 500 on headings or change the family to `JetBrains Mono` for technical emphasis. +- **Negative letter-spacing on display sizes** tightens large type into cohesive blocks. Scale the negative value with size: -1.5px at 72px down to 0 at 16px. +- **Line height opens at body sizes** (1.6-1.65) to maintain readability with the light weight. Display sizes tighten to 1.05-1.25. +- **No serifs anywhere.** The system is entirely sans-serif for UI and monospace for code. + +--- + +## 4. Component Stylings + +### Buttons + +**`button-primary`** — Purple gradient CTA + +- Background: `gradient-hero` (`linear-gradient(135deg, #635BFF, #7A73FF)`) +- Text: `#FFFFFF` +- Typography: `button-md` (14px / 500) +- Border radius: 6px +- Padding: 10px 20px +- Height: 40px +- Border: none +- Hover: background shifts to solid `#635BFF`, slight brightness increase +- Active/pressed: background `#5850E6` (one shade darker) +- Focus: 3px ring in `purple-400` offset by 2px +- Transition: 150ms ease on background-color + +**`button-primary-lg`** — Large hero CTA + +- Same as `button-primary` but: +- Typography: `button-lg` (16px / 500) +- Padding: 14px 28px +- Height: 48px +- Border radius: 8px + +**`button-secondary`** — Outline button + +- Background: `#FFFFFF` +- Text: `ink` (`#1A1F36`) +- Typography: `button-md` +- Border: 1px solid `gray-100` (`#E8ECF1`) +- Border radius: 6px +- Padding: 9px 19px +- Height: 40px +- Hover: background `gray-50` (`#F6F9FC`), border darkens to `gray-200` +- Active: background `gray-100`, border `charcoal` +- Focus: 3px ring in `purple-400` + +**`button-ghost`** — Inline text button + +- Background: transparent +- Text: `purple-600` (`#635BFF`) +- Typography: `button-md` +- Border: none +- Padding: 4px 8px +- Height: auto +- Hover: text `purple-500`, faint `purple-50` background +- Active: text `purple-600`, background `purple-100` + +**`button-dark`** — CTA on dark surfaces + +- Background: `#FFFFFF` +- Text: `navy-900` (`#0A2540`) +- Typography: `button-md` +- Border radius: 6px +- Padding: 10px 20px +- Height: 40px +- Hover: background `gray-50` +- Active: background `gray-100` + +**`button-disabled`** + +- Background: `gray-50` (`#F6F9FC`) +- Text: `ash` (`#8792A2`) +- Border: 1px solid `gray-100` +- Cursor: not-allowed +- No hover or active states + +### Cards + +**`card-default`** — Standard content card + +- Background: `#FFFFFF` +- Border: 1px solid `gray-100` (`#E8ECF1`) +- Border radius: 8px +- Padding: 24px +- Shadow: `0 2px 4px rgba(10,37,64,0.04), 0 4px 16px rgba(10,37,64,0.06)` +- Hover: shadow deepens to `0 4px 8px rgba(10,37,64,0.06), 0 8px 24px rgba(10,37,64,0.08)`, subtle translateY(-1px) +- Transition: 200ms ease on box-shadow and transform + +**`card-elevated`** — Featured/highlight card + +- Background: `#FFFFFF` +- Border: 1px solid `gray-100` +- Border radius: 8px +- Padding: 32px +- Shadow: `0 4px 8px rgba(10,37,64,0.06), 0 8px 24px rgba(10,37,64,0.08)` +- Used for pricing featured tier, primary feature showcase + +**`card-dark`** — Card on dark surfaces + +- Background: `navy-800` (`#1A2E4A`) +- Border: 1px solid `dm-hairline` (`rgba(255,255,255,0.08)`) +- Border radius: 8px +- Padding: 24px +- Shadow: `0 4px 16px rgba(0,0,0,0.2)` +- Text: `on-dark` (`#FFFFFF`) + +**`card-pricing`** — Pricing tier card + +- Background: `#FFFFFF` +- Border: 1px solid `gray-100` +- Border radius: 12px +- Padding: 32px +- Shadow: `0 2px 4px rgba(10,37,64,0.04), 0 4px 16px rgba(10,37,64,0.06)` + +**`card-pricing-featured`** — Recommended pricing tier + +- Same as `card-pricing` but: +- Border: 2px solid `purple-600` (`#635BFF`) +- Shadow: `0 4px 16px rgba(99,91,255,0.12), 0 8px 32px rgba(99,91,255,0.08)` + +### Inputs + +**`input-default`** — Standard text input + +- Background: `#FFFFFF` +- Text: `ink` (`#1A1F36`) +- Placeholder: `mute` (`#697386`) +- Typography: `body-md` (16px / 300) +- Border: 1px solid `gray-100` (`#E8ECF1`) +- Border radius: 6px +- Padding: 10px 12px +- Height: 40px +- Hover: border `gray-200` (`#C1C9D2`) +- Focus: border `purple-600`, 3px ring in `purple-100` (`#E8E5FF`) +- Error: border `error` (`#D84040`), ring in `error-soft` +- Disabled: background `gray-50`, text `ash`, border `gray-100`, cursor not-allowed +- Transition: 150ms ease on border-color and box-shadow + +**`input-dark`** — Input on dark surfaces + +- Background: `navy-800` (`#1A2E4A`) +- Text: `on-dark` +- Placeholder: `on-dark-mute` +- Border: 1px solid `dm-hairline` +- Focus: border `purple-500`, ring `rgba(123,115,255,0.2)` + +**`search-bar`** — Search input + +- Same as `input-default` but: +- Height: 44px +- Border radius: 8px +- Padding: 12px 16px 12px 40px (left padding accounts for magnifier icon) + +### Navigation + +**`nav-primary`** — Top navigation bar + +- Background: `#FFFFFF` with 1px bottom border in `gray-100` +- Height: 64px +- Layout: Logo at left, nav links centered, CTA + secondary link at right +- Nav link text: `body-sm` (14px / 300), color `charcoal` +- Nav link hover: color `ink`, subtle `gray-50` background pill +- Nav link active: color `purple-600`, weight 500 +- Sticky on scroll with a `0 2px 8px rgba(10,37,64,0.06)` shadow appearing at scroll offset + +**`nav-primary-dark`** — Top nav on dark sections + +- Background: `navy-900` (`#0A2540`) with 1px bottom border in `dm-hairline` +- Nav link text: color `on-dark-mute` +- Nav link hover: color `on-dark`, subtle background in `dm-surface` +- Nav link active: color `purple-500` + +**`nav-mobile`** — Mobile navigation + +- Hamburger icon at left, logo centered, CTA at right +- Drawer slides from right with `navy-900` background +- Drawer links stacked vertically with `body-lg` (18px / 300), color `on-dark` +- Divider lines in `dm-hairline` between link groups + +### Other Components + +**`badge`** — Inline status badge + +- Background: `purple-50` (`#F4F2FF`) +- Text: `purple-600` +- Typography: `caption` (12px / 400) +- Border radius: 9999px (full pill) +- Padding: 3px 10px +- Variants: `badge-success` (green), `badge-warning` (amber), `badge-error` (red), `badge-info` (cyan) — each using corresponding semantic-soft background and semantic text color + +**`code-block`** — Code well + +- Background: `navy-900` (`#0A2540`) +- Text: `on-dark` +- Typography: `code-md` (14px / JetBrains Mono) +- Border radius: 8px +- Padding: 20px 24px +- Tab strip at top: `code-sm` (12px), inactive tabs `on-dark-mute`, active tab `on-dark` with 2px `purple-600` bottom border + +**`divider`** — Section separator + +- Light surface: 1px solid `gray-100` (`#E8ECF1`) +- Dark surface: 1px solid `dm-hairline` (`rgba(255,255,255,0.08)`) + +**`tooltip`** — Hover tooltip + +- Background: `navy-900` +- Text: `on-dark` +- Typography: `body-sm` (14px / 300) +- Border radius: 6px +- Padding: 8px 12px +- Shadow: `0 4px 16px rgba(0,0,0,0.2)` +- Arrow: 6px CSS triangle in `navy-900` + +--- + +## 5. Layout Principles + +### Spacing Scale + +| Token | Value | Use | +|-------|-------|-----| +| `xxs` | 4px | Inline tight gaps, icon-to-label spacing | +| `xs` | 8px | Small internal gaps, badge padding | +| `sm` | 12px | Input padding, in-card element spacing | +| `md` | 16px | Default card padding (small cards), gutter spacing | +| `lg` | 24px | Standard card padding, section sub-spacing | +| `xl` | 32px | Pricing card padding, feature row gaps | +| `xxl` | 48px | Large feature section vertical padding | +| `xxxl` | 64px | Major section vertical padding | +| `section` | 96px | Full section rhythm on desktop | +| `band` | 128px | Hero band vertical padding | + +### Grid + +- **Max content width:** 1200px centered, with 24px side padding growing to 48px on ultrawide +- **Hero bands:** full-bleed up to 1440px content area +- **Card grids:** 3-up at desktop (400px per card), 2-up at tablet, 1-up at mobile +- **Feature rows:** 2-up split (copy left 45%, visual right 55%) collapsing to stacked at tablet +- **Footer:** 4-column link grid at desktop, 2-up at tablet, 1-up at mobile + +### Whitespace Philosophy + +Whitespace is the system's primary structural tool. Sections breathe at 96px on desktop with no decorative dividers — the white canvas carries from hero to footer with rhythm established by alternating `white` and `gray-50` bands. Inside cards, the system tightens to 16-24px so content reads as compact and precise. The white canvas never feels empty because the generous spacing and light typography create intentional breathing room, not vacancy. + +--- + +## 6. Depth & Elevation + +### Shadow System + +| Level | Shadow | Use | +|-------|--------|-----| +| 0 — flat | none | Canvas, inline text, footer | +| 1 — rest | `0 2px 4px rgba(10,37,64,0.04), 0 4px 16px rgba(10,37,64,0.06)` | Default cards at rest | +| 2 — hover | `0 4px 8px rgba(10,37,64,0.06), 0 8px 24px rgba(10,37,64,0.08)` | Card hover state, elevated panels | +| 3 — floating | `0 8px 16px rgba(10,37,64,0.08), 0 16px 48px rgba(10,37,64,0.12)` | Modals, dropdowns, popovers | +| 4 — purple glow | `0 4px 16px rgba(99,91,255,0.12), 0 8px 32px rgba(99,91,255,0.08)` | Featured pricing card, purple-accented elevated surfaces | + +### Surface Hierarchy + +| Level | Surface | Use | +|-------|---------|-----| +| 0 | `white` (`#FFFFFF`) | Page canvas, card backgrounds | +| 1 | `gray-50` (`#F6F9FC`) | Inset bands, alternating sections, table row stripes | +| 2 | `gray-100` (`#E8ECF1`) | Dividers, borders, subtle inset backgrounds | +| 3 | `navy-900` (`#0A2540`) | Dark sections, code wells, footer | +| 4 | `navy-800` (`#1A2E4A`) | Cards on dark, elevated dark surfaces | +| 5 | `navy-700` (`#2D3E54`) | In-well panels, dark input fills | + +Elevation on light surfaces comes from layered shadows with the `rgba(10,37,64,...)` tint — never pure black shadows, which would read too harsh against the warm white canvas. On dark surfaces, depth is built from the navy surface ladder, not shadows. + +--- + +## 7. Do's and Don'ts + +### Do + +- Use weight 300 as the default body weight. This is the single most important typographic decision in the system. +- Reserve `purple-600` (`#635BFF`) and the hero gradient for CTAs, links, and accent surfaces. The purple should feel like a signature, not wallpaper. +- Use `navy-900` (`#0A2540`) for dark sections rather than pure black (`#000000`). The navy carries warmth and brand coherence. +- Apply the layered shadow system (`rgba(10,37,64,...)`) instead of pure-black shadows. The slight blue tint matches the navy palette. +- Alternate between `white` and `gray-50` bands to create section rhythm without visible dividers. +- Set display type with negative letter-spacing proportional to size. Tighter at larger sizes, 0 at body scale. +- Use `JetBrains Mono` for all code surfaces — API examples, terminal blocks, inline code. Never use the sans-serif face for code. +- Give cards a subtle `translateY(-1px)` on hover to reinforce the shadow lift. The motion should feel like the card is breathing upward, not bouncing. +- Maintain 96px section rhythm on desktop. The whitespace is structural, not decorative. + +### Don't + +- Don't use weight 400 or 500 for body text. Weight 300 is the Stripe voice. Bumping weight breaks the system's characteristic lightness. +- Don't apply the purple gradient to large background surfaces or full-bleed sections outside of the hero. Purple is an accent, not a canvas. +- Don't use pure black (`#000000`) for text, shadows, or backgrounds. `ink` (`#1A1F36`) and `navy-900` (`#0A2540`) are warmer and brand-coherent. +- Don't add visible dividers between sections. Rhythm comes from alternating surface colors and generous spacing. +- Don't round corners beyond 12px on cards. The system stays in the 6-8px range for most elements. Pill-shaped cards break the precise aesthetic. +- Don't use colored shadows outside of the purple glow on featured elements. All other shadows use the `rgba(10,37,64,...)` tint. +- Don't pair purple with a secondary brand color. Purple is the only accent; semantic colors (green, amber, red) are functional, not decorative. +- Don't set code in the sans-serif face, even inline. Code always gets `JetBrains Mono`. +- Don't add drop shadows on dark surfaces. Elevation on dark is built from the surface-color ladder. + +--- + +## 8. Responsive Behavior + +### Breakpoints + +| Name | Width | Key Changes | +|------|-------|-------------| +| ultrawide | 1920px+ | Content max-width holds at 1200px; outer gutters grow to 48-80px | +| desktop | 1280px | Default — 3-up card grids, 2-up feature rows, full nav | +| desktop-small | 1024px | Card grids 2-up; feature rows remain side-by-side but narrower | +| tablet | 768px | Card grids 1-up; feature rows stack; nav collapses to hamburger | +| mobile | 480px | Single-column everything; hero display-2xl scales 72px to 36px | +| mobile-narrow | 320px | Section padding tightens to 48px; card padding reduces to 16px | + +### Touch Targets + +- All buttons meet WCAG AA at minimum 40px height. `button-primary-lg` sits at 48px (AAA). +- `input-default` is 40px height. `search-bar` is 44px (AAA). +- Inline links and ghost buttons receive additional padding (8px minimum) to extend tap area without visual change. +- Nav links on mobile: 44px minimum tap height with full-width tap targets. + +### Collapsing Strategy + +- **Primary nav:** desktop horizontal cluster collapses to hamburger at 768px. Logo and primary CTA remain visible at all breakpoints. +- **Hero headline:** `display-2xl` scales 72px -> 56px -> 44px -> 36px across breakpoints. Letter-spacing reduces proportionally. +- **Feature rows:** 2-up side-by-side at desktop -> stacked at tablet with visual below copy. +- **Card grids:** 3-up -> 2-up at desktop-small -> 1-up at tablet. +- **Pricing tier grid:** 3-up -> stacked at tablet with featured tier remaining first. +- **Footer:** 4-column -> 2-up at tablet -> 1-up at mobile. +- **Section padding:** 96px desktop -> 64px tablet -> 48px mobile. +- **Code blocks:** horizontal scroll at mobile rather than reflow — code formatting must be preserved. + +### Animation Guidelines + +- **Card hover:** 200ms ease on box-shadow and transform. Subtle lift (1px) with shadow deepening. +- **Button hover:** 150ms ease on background-color and border-color. No transform. +- **Nav shadow on scroll:** 200ms ease on opacity appearing at scroll offset. +- **Page transitions:** 300ms ease-out on opacity. No slide or scale transitions on page-level elements. +- **Reduced motion:** All transitions collapse to 0ms; hover states apply instantly without motion. diff --git a/addons/officials/crew/designer/skills/design-system-picker/design-systems/supabase.md b/addons/officials/crew/designer/skills/design-system-picker/design-systems/supabase.md new file mode 100644 index 00000000..a3ed3e00 --- /dev/null +++ b/addons/officials/crew/designer/skills/design-system-picker/design-systems/supabase.md @@ -0,0 +1,407 @@ +# Supabase Design System + +> Dark emerald green theme. Code-first developer aesthetic. Dark surfaces with green accents. Technical documentation feel. + +--- + +## 1. Visual Theme & Atmosphere + +Supabase speaks to developers who live in terminals and editors. The visual language borrows from IDE dark modes: deep charcoal backgrounds, syntax-highlighted accents, and monospaced code blocks as first-class content. The emerald green brand color (`#3ECF8E`) punctuates an otherwise austere dark palette, signaling "active," "live," and "connected" -- a visual echo of a running Postgres instance. + +**Atmosphere keywords:** developer-tooling, terminal-dark, documentation-grade, surgical precision, open-source credibility. + +**Primary mode:** Dark. Light mode exists in the dashboard but is secondary. This design system defaults to dark. + +**Signature moments:** + +- Green-glow code blocks with syntax tokens that mirror the brand palette +- `$ supabase` CLI prompts woven into marketing pages +- Data tables with tight row heights (28px) that feel like a spreadsheet, not a marketing site +- Subtle green `rgba(62, 207, 142, 0.1)` flash on state changes + +--- + +## 2. Color Palette & Roles + +### Brand + +| Semantic Name | Hex | HSL | Role | +|----------------------|-----------|--------------------|-----------------------------------------| +| `brand-primary` | `#3ECF8E` | 153.1 60.2% 52.7% | Primary actions, links, active states | +| `brand-accent` | `#34B97D` | 152.9 56.1% 46.5% | Hover/pressed brand, emphasis accents | +| `brand-600` | `#84E0B7` | 153 59.5% 70% | Light brand for dark-surface highlights | +| `brand-500` | `#15593B` | 153.5 61.8% 21.6% | Brand on dark surfaces (muted green) | +| `brand-400` | `#0B3824` | 153.3 65.2% 13.5% | Deep brand background | +| `brand-300` | `#062618` | 153.8 69.6% 9% | Darkest brand surface | +| `brand-200` | `#041C11` | 152.5 75% 6.3% | Near-black brand tint | + +### Gray (Dark Mode) -- Core Neutral + +| Semantic Name | Hex | Role | +|--------------------|-----------|-------------------------------------| +| `gray-dark-100` | `#151515` | Deepest background (sidebar, dialog)| +| `gray-dark-200` | `#1C1C1C` | Default page/canvas background | +| `gray-dark-300` | `#222222` | Control background, surface 100 | +| `gray-dark-400` | `#282828` | Surface 200, muted background | +| `gray-dark-500` | `#2D2D2D` | Button default bg, overlay default | +| `gray-dark-600` | `#343434` | Border default, selection bg | +| `gray-dark-700` | `#3D3D3D` | Border strong, surface 400 | +| `gray-dark-800` | `#505050` | Border button hover, stronger border| +| `gray-dark-900` | `#6F6F6F` | Foreground muted | +| `gray-dark-1000` | `#7D7D7D` | Foreground lighter | +| `gray-dark-1100` | `#9F9F9F` | Foreground light | +| `gray-dark-1200` | `#ECECEC` | Foreground default (primary text) | + +### Slate (Dark Mode) -- Cool Neutral Alternative + +| Semantic Name | Hex | Role | +|--------------------|-----------|-------------------------------------| +| `slate-dark-100` | `#141617` | Cool deep background | +| `slate-dark-200` | `#1A1D1E` | Cool canvas background | +| `slate-dark-300` | `#1F2324` | Cool surface | +| `slate-dark-400` | `#26292B` | Cool muted surface | +| `slate-dark-500` | `#2A2E30` | Cool button/overlay background | +| `slate-dark-600` | `#313538` | Cool border | +| `slate-dark-700` | `#393E41` | Cool stronger border | +| `slate-dark-800` | `#4C5155` | Cool hover border | +| `slate-dark-900` | `#687076` | Cool muted foreground | +| `slate-dark-1000` | `#787E84` | Cool lighter foreground | +| `slate-dark-1100` | `#9AA0A5` | Cool light foreground | +| `slate-dark-1200` | `#EBECED` | Cool primary text | + +### Semantic Colors + +| Semantic Name | Hex | Role | +|----------------------|-----------|----------------------------| +| `destructive` | `#E54D2D` | Error states, delete, danger | +| `destructive-hover` | `#F0694F` | Destructive hover/pressed | +| `destructive-muted` | `#7E2215` | Destructive on dark surface | +| `warning` | `#FFB224` | Caution, pending states | +| `warning-hover` | `#F1A00C` | Warning hover/pressed | +| `secondary` | `#FFFFFF` | White accent, secondary actions | + +### Code Syntax Tokens (Dark) + +| Token | Hex | Usage | +|------------------|-----------|-------------------------------| +| `code-foreground`| `#FFFFFF` | Default code text | +| `code-keyword` | `#BDA4FF` | Language keywords | +| `code-constant` | `#3ECF8E` | Constants, functions, properties (matches brand) | +| `code-string` | `#FFCDA1` | String literals, expressions | +| `code-comment` | `#7E7E7E` | Comments | +| `code-highlight` | `#232323` | Active/highlighted line bg | + +--- + +## 3. Typography Rules + +### Font Stack + +| Purpose | Font | Fallback | +|------------|-------------------------------|--------------------------------| +| UI Body | Inter | system-ui, -apple-system, sans-serif | +| Code | `custom-font` (monospaced) | ui-monospace, SFMono-Regular, Menlo, monospace | +| Display | `custom-font` (variable) | Inter, system-ui, sans-serif | + +> Note: Supabase uses a proprietary custom font loaded via `@font-face` in weights 400 (Book) and 500 (Medium). For reproduction, use Inter as the closest open-source substitute for body text and JetBrains Mono or Fira Code for code. + +### Type Scale + +| Level | Size | Weight | Line-Height | Usage | +|----------------|-------------------------------|--------|-------------|------------------------------| +| `display-xl` | `clamp(2.5rem, 5vw, 4.5rem)` | 500 | 1.1 | Hero headlines | +| `display-lg` | `clamp(2rem, 4vw, 3rem)` | 500 | 1.15 | Section headlines | +| `h2` | `1.875rem` (30px) | 500 | 1.25 | Sub-section headers | +| `h3` | `1.25rem` (20px) | 500 | 1.3 | Card titles, panel headers | +| `body-lg` | `1.125rem` (18px) | 400 | 1.6 | Hero body, lead paragraphs | +| `body` | `0.875rem` (14px) | 400 | 1.5 | Default body text | +| `body-sm` | `0.8125rem` (13px) | 400 | 1.5 | Secondary text, captions | +| `code` | `0.875rem` (14px) | 400 | 1.6 | Inline code, code blocks | +| `label` | `0.75rem` (12px) | 500 | 1.4 | Labels, badges, tags | + +### Typography Rules + +- Never use italic for emphasis in UI text; use weight 500 instead. +- Monospace is reserved for code, CLI commands, API paths, and database identifiers. +- Code blocks use a darker background (`#1C1C1C`) than the page background. +- Headlines never use letter-spacing. Body text at small sizes may use `0.01em`. +- Avoid center-aligned text for paragraphs longer than two lines. + +--- + +## 4. Component Stylings + +### Buttons + +``` +Primary Button + bg: #3ECF8E + text: #1C1C1C + border-radius: 6px + padding: 8px 16px + font-weight: 500 + font-size: 14px + hover: bg #34B97D + active: bg #2DA06D, translateY(1px) + disabled: bg #2D2D2D, text #6F6F6F + +Secondary Button + bg: #2D2D2D + text: #ECECEC + border: 1px solid #343434 + border-radius: 6px + padding: 8px 16px + font-weight: 400 + hover: bg #343434, border-color #3D3D3D + active: bg #3D3D3D + disabled: bg #222222, text #6F6F6F, border-color #282828 + +Ghost Button + bg: transparent + text: #9F9F9F + border: none + padding: 8px 12px + hover: text #ECECEC, bg rgba(255,255,255,0.05) + active: bg rgba(255,255,255,0.08) + +Destructive Button + bg: #E54D2D + text: #FFFFFF + border-radius: 6px + hover: bg #F0694F + active: bg #D13B1C +``` + +### Cards + +``` +Surface Card + bg: #222222 (gray-dark-300) + border: 1px solid #343434 (gray-dark-600) + border-radius: 8px + padding: 16px (1rem) + hover: border-color #3D3D3D, subtle glow rgba(62,207,142,0.04) + +Featured Card + bg: #282828 (gray-dark-400) + border: 1px solid #3D3D3D (gray-dark-700) + border-radius: 8px + padding: 24px (1.5rem) + hover: border-color #3ECF8E at 0.3 opacity + +Code Card / Terminal Card + bg: #1C1C1C (gray-dark-200) + border: 1px solid #2D2D2D + border-radius: 8px + padding: 16px + font-family: monospace +``` + +### Inputs + +``` +Text Input + bg: #222222 (gray-dark-300) + border: 1px solid #343434 (gray-dark-600) + border-radius: 6px + padding: 8px 12px + text: #ECECEC + placeholder: #6F6F6F + height: 36px (default), 28px (compact) + focus: border-color #3ECF8E, ring rgba(62,207,142,0.3) + error: border-color #E54D2D, ring rgba(229,77,45,0.2) + disabled: bg #1C1C1C, text #6F6F6F +``` + +### Navigation + +``` +Top Nav + bg: rgba(21,21,21,0.8) with backdrop-blur + border-bottom: 1px solid #2D2D2D + height: 56px + text: #9F9F9F + active/hover text: #ECECEC + brand link: #3ECF8E + +Sidebar Nav + bg: #151515 (gray-dark-100) + width: 240px (collapsed: 48px) + item text: #9F9F9F + item hover: bg #222222, text #ECECEC + item active: bg #282828, text #3ECF8E, left-border 2px #3ECF8E + section label: #6F6F6F, uppercase, 12px, 500 weight, letter-spacing 0.05em + +Breadcrumb + text: #7D7D7D + separator: #6F6F6F + current: #ECECEC +``` + +### Badges / Tags + +``` +Default Badge + bg: #2D2D2D + text: #9F9F9F + border-radius: 9999px + padding: 2px 8px + font-size: 12px + +Brand Badge + bg: #15593B (brand-500) + text: #3ECF8E + border-radius: 9999px + +Destructive Badge + bg: #7E2215 + text: #E54D2D + border-radius: 9999px +``` + +### Data Table + +``` +Table + header bg: #1C1C1C + row bg: #222222 + row alt bg: #1C1C1C + row hover bg: #282828 + row height: 28px + header text: #9F9F9F, 12px, 500 + cell text: #ECECEC, 13px + cell padding: 8px horizontal + border: 1px solid #2D2D2D +``` + +--- + +## 5. Layout Principles + +### Spacing Scale + +| Token | Value | Usage | +|---------|---------|-------------------------------------| +| `xs` | 4px | Inline gaps, icon-to-label spacing | +| `sm` | 8px | Tight component padding | +| `md` | 16px | Default component padding, card gutters | +| `lg` | 32px | Section spacing, panel padding | +| `xl` | 64px | Major section separation | + +### Page Layout + +``` +Max content width: 1128px (--content-width-screen-xl) +Container max: 128rem (--container-site) +Page horizontal padding: 16px (mobile), 24px (tablet), 32px (desktop) +Sidebar width: 240px (expanded), 48px (collapsed) +Sidebar + content gap: 0 (sidebar shares border with content) +``` + +### Grid + +- Dashboard uses a sidebar + content layout (no CSS grid for the main split). +- Card grids: 12-column at `lg+`, 1-column on mobile. +- Gap between cards: 16px (`md`). +- Feature grids on marketing pages: 3 columns at `xl`, 2 at `md`, 1 at `sm`. + +### Content Rhythm + +- Headline to body: 12px gap. +- Body to next section: 32px (`lg`). +- Card internal: 16px padding, 12px between label and value. +- Documentation left-nav items: 4px vertical gap. + +--- + +## 6. Depth & Elevation + +Supabase uses minimal elevation. The dark theme creates depth through surface color steps, not drop shadows. + +### Surface Stack (dark to light) + +| Level | Background | Usage | +|-------|-------------|------------------------------| +| 0 | `#151515` | Sidebar, dialogs, overlays | +| 1 | `#1C1C1C` | Canvas, page background | +| 2 | `#222222` | Controls, inputs, card base | +| 3 | `#282828` | Hover states, featured cards | +| 4 | `#2D2D2D` | Button default, elevated bg | +| 5 | `#343434` | Active/hover button, borders | + +### Shadows + +```css +/* Rarely used. Prefer surface color step instead. */ +--shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.3); +--shadow-md: 0 4px 12px rgba(0, 0, 0, 0.4); +--shadow-lg: 0 12px 32px rgba(0, 0, 0, 0.5); + +/* Brand glow -- used sparingly for emphasis */ +--glow-brand: 0 0 20px rgba(62, 207, 142, 0.15); +--glow-brand-strong: 0 0 40px rgba(62, 207, 142, 0.25); +``` + +### Overlays + +``` +Backdrop: rgba(0, 0, 0, 0.6) +Modal: bg #151515, border 1px solid #2D2D2D +Toast: bg #2D2D2D, border 1px solid #3D3D3D, slight shadow +``` + +--- + +## 7. Do's and Don'ts + +### Do + +- Use the brand green (`#3ECF8E`) for primary CTAs, active nav items, and positive states only. +- Use monospace for anything a developer would type or read in a terminal. +- Step through the gray-dark scale for elevation; avoid adding shadows where a darker background step suffices. +- Use `rgba(62, 207, 142, 0.1)` for subtle brand-tinted highlights (flash animations, hover accents). +- Keep data tables compact (28px row height). Developers expect density. +- Use `#BDA4FF` for code keywords and `#FFCDA1` for strings to create syntax-highlighted content blocks. +- Round corners at 6-8px. Not 0 (too harsh), not 16px (too bubbly for a dev tool). + +### Don't + +- Do not use the brand green as a background color for large surface areas. It is an accent, not a fill. +- Do not use pure white (`#FFFFFF`) for body text on dark backgrounds. Use `#ECECEC` instead; pure white creates excessive contrast. +- Do not add colored shadows or gradients to cards. Supabase surfaces are flat and distinguished by background value. +- Do not use rounded display fonts or playful typefaces. The tone is technical and precise. +- Do not center-align long-form prose. Left-align everything except hero headlines and short taglines. +- Do not use more than two font weights in a single view (400 and 500). +- Do not apply `border-radius: 9999px` to anything that is not a badge, tag, or pill button. +- Do not use the slate palette and gray palette interchangeably in the same view. Pick one neutral track. + +--- + +## 8. Responsive Behavior + +### Breakpoints + +| Name | Min Width | Layout Changes | +|----------|-----------|--------------------------------------| +| `sm` | 640px | Single column, full-width cards | +| `md` | 768px | Two-column card grids, sidebar hidden | +| `lg` | 1024px | Sidebar visible (collapsed), 2-3 col grids | +| `xl` | 1280px | Full sidebar, max content width | +| `2xl` | 1536px | Wider gutters, more whitespace | + +### Mobile Adaptations + +- **Sidebar:** Hidden below `lg`, replaced by hamburger menu with slide-out drawer (bg `#151515`). +- **Data tables:** Horizontally scrollable with sticky first column. Row height stays 28px. +- **Code blocks:** Horizontally scrollable. Never truncate or hide code content. +- **Navigation:** Top nav collapses to logo + hamburger + CTA button. +- **Hero:** Stack headline, body, and CTA vertically. Reduce display-xl to `2rem` at `sm`. +- **Card grids:** Shift from multi-column to single-column stacked cards. +- **Padding:** Page horizontal padding reduces from 32px to 16px at `sm`. + +### Dashboard-Specific + +- Sidebar collapses from 240px to 48px (icon-only) at `lg`, hides completely at `md`. +- Panel resizers maintain 2px grab area (`--panel`). +- Table column widths are user-adjustable; minimum column width is 80px. +- Mobile dashboard shows a bottom tab bar instead of sidebar. diff --git a/addons/officials/crew/designer/skills/design-system-picker/design-systems/tesla.md b/addons/officials/crew/designer/skills/design-system-picker/design-systems/tesla.md new file mode 100644 index 00000000..acd5a154 --- /dev/null +++ b/addons/officials/crew/designer/skills/design-system-picker/design-systems/tesla.md @@ -0,0 +1,138 @@ +# Tesla Design System + +Radical subtraction. Cinematic full-viewport photography. Nearly zero UI chrome. Every element earns its place or is removed. + +--- + +## 1. Visual Theme & Atmosphere + +Pure black voids. Full-bleed hero imagery of vehicles shot at golden hour or in stark studio lighting. The product is the visual — UI recedes until needed. Pages feel like a film trailer: slow reveals, minimal text, maximum impact. No decorative elements. No gradients. No patterns. Silence as a design tool. + +**Keywords:** radical subtraction, cinematic, electric, powerful, silent, confident + +--- + +## 2. Color Palette & Roles + +| Name | Hex | Role | +|------|-----|------| +| Void | `#000000` | Primary background, dominant surface | +| Pure White | `#FFFFFF` | Primary text, divider lines, CTA text | +| Cool Gray | `#A6A6A6` | Secondary text, captions, disabled states | +| Steel | `#5C5C5C` | Tertiary text, subtle borders | +| Tesla Red | `#E82127` | Accent only — error states, rare highlights | +| Surface Dark | `#171717` | Card backgrounds, secondary surfaces | +| Surface Mid | `#222222` | Hover states, elevated surfaces | + +**Rule:** The palette is almost monochrome. Tesla Red is used no more than once per page. Cool Gray is the workhorse for anything that is not primary content. + +--- + +## 3. Typography Rules + +**Primary:** Universal Sans (or fallback: Inter, system sans-serif) + +| Element | Weight | Size | Tracking | Case | +|---------|--------|------|----------|------| +| Hero headline | 600 | clamp(48px, 6vw, 96px) | -0.03em | Title | +| Section headline | 500 | clamp(32px, 3vw, 56px) | -0.02em | Title | +| Body large | 400 | 20px | 0 | Sentence | +| Body | 400 | 16px | 0 | Sentence | +| Caption / spec | 400 | 13px | 0.02em | Title | +| CTA | 500 | 14px | 0.04em | Uppercase | + +**Rules:** +- Never use italic for emphasis. Use weight or size contrast. +- Hero headlines: one line, no wrapping. If it wraps, the copy is too long. +- All-caps tracking must be wide — never let uppercase text feel cramped. +- Line height for headlines: 1.05. For body: 1.5. + +--- + +## 4. Component Stylings + +### Buttons +- **Primary CTA:** White text on `#000000` with 1px white border, padding `14px 40px`, uppercase tracking 0.04em. On hover: background becomes `#FFFFFF`, text becomes `#000000`. +- **Ghost CTA:** White text, no border, underline on hover (2px, offset 4px). +- **No filled colored buttons.** No rounded pill buttons. No icon-only buttons without label. + +### Navigation +- Fixed top bar, `height: 56px`, transparent over hero images. +- Nav links: 13px uppercase, tracking 0.06em, white, no underline. +- No hamburger icon on desktop. No sidebars. + +### Cards +- Background: `#171717` or transparent over imagery. +- No border-radius (0px). No box-shadow. +- Content sits flush against edges. + +### Image Treatments +- Full-viewport hero images: `width: 100vw; height: 100vh; object-fit: cover`. +- No rounded corners on images. No visible image borders. +- Overlay gradient only when text legibility demands it: `linear-gradient(to top, #000 0%, transparent 60%)`. + +### Data / Specs +- Spec tables use Cool Gray labels, White values, no grid lines. +- Vertical spacing between spec rows: 24px. +- No alternating row colors. + +--- + +## 5. Layout Principles + +- **Full-viewport sections.** Each section occupies the entire viewport. No content peeks into the next section. +- **Extreme whitespace.** Between sections: 120px minimum. Between headline and body: 40px. +- **Centered single column** for headlines and CTAs. Max content width: 960px. +- **Asymmetric split** for feature sections: 60% image / 40% text, or vice versa. +- **No sidebars. No multi-column grids of cards.** The product is the grid. +- **Sticky scroll behavior:** as the user scrolls, the next vehicle image cross-fades in. Content overlays the imagery. + +--- + +## 6. Depth & Elevation + +- **No shadows.** Ever. Depth comes from layering full-bleed imagery, not from drop shadows. +- **No blur/glass effects.** The interface is crisp and opaque. +- Elevation hierarchy: + - Level 0: `#000000` background + - Level 1: `#171717` surface + - Level 2: `#222222` hover/elevated + - Level 3: `#FFFFFF` inverted CTA on hover +- Z-index is flat. Only the nav bar (z-50) and modals sit above content. + +--- + +## 7. Do's and Don'ts + +**Do:** +- Let photography do the heavy lifting — use the largest images possible +- Use generous whitespace to create breathing room around text +- Keep copy short: headlines under 6 words, body under 40 words per section +- Use animation only for scroll-triggered reveals (opacity 0 to 1, translateY) +- Make CTAs obvious through contrast, not decoration + +**Don't:** +- Add decorative icons, illustrations, or patterns +- Use rounded corners on any element (border-radius: 0) +- Apply drop shadows or elevation shadows +- Use more than one accent color per page +- Place text over busy image areas without a gradient overlay +- Use carousels — one hero image per viewport +- Add social media feeds, tickers, or scrolling banners + +--- + +## 8. Responsive Behavior + +| Breakpoint | Behavior | +|-----------|----------| +| < 640px | Single column, stacked. Hero images scale to `100vh` width-aware. Headline reduces to 32px. Nav collapses to hamburger. | +| 640–1024px | Single column. Side-by-side spec splits stack vertically. CTA buttons stretch full-width. | +| 1024–1440px | Asymmetric splits appear. Desktop nav visible. Spec tables go two-column. | +| > 1440px | Content max-width 960px, centered. Background imagery extends full-bleed. | + +**Mobile-specific rules:** +- Hero images may crop differently (focus on vehicle front, not full side profile) +- Bottom sticky CTA bar appears on mobile (transparent black, white text) +- Spec sections collapse into horizontal scroll cards only on mobile +- Touch targets: minimum 48x48px diff --git a/addons/officials/crew/designer/skills/design-system-picker/design-systems/vercel.md b/addons/officials/crew/designer/skills/design-system-picker/design-systems/vercel.md new file mode 100644 index 00000000..d049e858 --- /dev/null +++ b/addons/officials/crew/designer/skills/design-system-picker/design-systems/vercel.md @@ -0,0 +1,392 @@ +# Vercel Design System + +## 1. Visual Theme & Atmosphere + +Black and white precision. Every pixel is deliberate. The Vercel aesthetic communicates engineering rigor through extreme restraint -- no gradients on surfaces, no decorative illustration, no ornament. Information density is high but never cluttered because the typographic hierarchy is surgical. + +The signature element is the **blueprint grid** -- a barely-visible line or dot matrix pattern (5-10% opacity) that signals systematic thinking. It decorates hero sections and feature showcases, never competing with content. + +Atmospheric keywords: monochrome, precise, developer-tool, systematic, engineered, minimal-accent, high-contrast dark mode default. + +**Primary mode: Dark.** Light mode exists but dark is canonical. All color values below list dark first. + +--- + +## 2. Color Palette & Roles + +### Dark Mode (default) + +| Token | Hex | Role | +|-------|-----|------| +| `background-1` | `#000000` | Page and primary surface background | +| `background-2` | `#171717` | Secondary surface differentiation (use sparingly) | +| `color-1` | `#0A0A0A` | Component default background | +| `color-2` | `#111111` | Component hover background | +| `color-3` | `#1A1A1A` | Component active / pressed background; badge background | +| `color-4` | `#1A1A1A` | Default border | +| `color-5` | `#222222` | Hover border | +| `color-6` | `#2E2E2E` | Active / focus border | +| `color-7` | `#FAFAFA` | High-contrast background (primary buttons, inverted surfaces) | +| `color-8` | `#E5E5E5` | Hover state for high-contrast background | +| `color-9` | `#A1A1A1` | Secondary text and icons | +| `color-10` | `#EDEDED` | Primary text and icons | +| `blue-500` | `#0070F3` | Accent / link color (used minimally) | +| `red-500` | `#EE0000` | Error / destructive | +| `green-500` | `#00C853` | Success / online status | +| `amber-500` | `#F5A623` | Warning | + +### Light Mode + +| Token | Hex | Role | +|-------|-----|------| +| `background-1` | `#FFFFFF` | Page background | +| `background-2` | `#FAFAFA` | Secondary surface | +| `color-1` | `#F5F5F5` | Component default background | +| `color-2` | `#E5E5E5` | Component hover background | +| `color-3` | `#D4D4D4` | Component active background | +| `color-4` | `#E5E5E5` | Default border | +| `color-5` | `#D4D4D4` | Hover border | +| `color-6` | `#A3A3A3` | Active border | +| `color-7` | `#171717` | High-contrast background | +| `color-8` | `#0A0A0A` | Hover high-contrast background | +| `color-9` | `#737373` | Secondary text and icons | +| `color-10` | `#171717` | Primary text and icons | + +### Accent Usage Rule + +Accent blue (`#0070F3`) appears only on interactive text links, focus rings, and selected states. Never as a surface fill. The palette is 95% neutral; color is a signal, not decoration. + +--- + +## 3. Typography Rules + +**Font families:** +- `Geist Sans` -- all UI text, headings, body, labels, buttons +- `Geist Mono` -- code, monospace labels, inline code mentions + +**Font loading:** `font-family: 'Geist Sans', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif` + +### Heading Scale + +| Style | Size | Weight | Letter-spacing | Usage | +|-------|------|--------|---------------|-------| +| Heading 72 | 72px | 600 | -2.88px | Marketing heroes only | +| Heading 64 | 64px | 600 | -2.56px | Marketing heroes | +| Heading 56 | 56px | 600 | -3.36px | Marketing heroes | +| Heading 48 | 48px | 600 | -1.92px | Section heroes | +| Heading 40 | 40px | 600 | -1.60px | Section heroes | +| Heading 32 | 32px | 600 | -1.28px | Dashboard headings, marketing subheadings | +| Heading 24 | 24px | 600 | -0.96px | Card titles, section labels | +| Heading 20 | 20px | 600 | -0.40px | Small section headings | +| Heading 16 | 16px | 600 | -0.32px | Compact headings | +| Heading 14 | 14px | 600 | -0.28px | Micro headings | + +All headings use `Geist Sans`. The aggressive negative letter-spacing at large sizes is critical to the Vercel look -- do not omit it. + +### Button Scale + +| Style | Size | Weight | Letter-spacing | Usage | +|-------|------|--------|---------------|-------| +| Button 16 | 16px | 500 | 0 | Largest CTA buttons | +| Button 14 | 14px | 500 | 0 | Default button | +| Button 12 | 12px | 500 | 0 | Tiny buttons inside input fields | + +### Label Scale + +| Style | Size | Weight | Letter-spacing | Usage | +|-------|------|--------|---------------|-------| +| Label 20 | 20px | 400 | 0 | Marketing text | +| Label 18 | 18px | 400 | 0 | Navigation items | +| Label 16 | 16px | 500 (strong) | 0 | Titles, differentiating from body | +| Label 14 | 14px | 500 (strong) | 0 | Most common; menus, list items | +| Label 14 Mono | 14px | 500 | 0 | Largest mono, pairs with >14 text | +| Label 13 | 13px | 400 | tabular | Secondary line next to labels; numbers | +| Label 13 Mono | 13px | 400 | 0 | Pairs with Label 14 | +| Label 12 | 12px | 500 (strong) | 0 | Tertiary text, caps (e.g. section headers) | +| Label 12 Mono | 12px | 400 | 0 | Smallest mono | + +### Copy Scale + +| Style | Size | Weight | Line-height | Usage | +|-------|------|--------|------------|-------| +| Copy 24 | 24px | 400 | 1.5 | Hero marketing body | +| Copy 20 | 20px | 400 | 1.5 | Hero marketing body | +| Copy 18 | 18px | 400 | 1.55 | Big quotes, feature descriptions | +| Copy 16 | 16px | 400 | 1.5 | Modals, spacious views | +| Copy 14 | 14px | 400 | 1.5 | Default body text (most common) | +| Copy 13 | 13px | 400 | 1.5 | Secondary text, space-constrained views | +| Copy 13 Mono | 13px | 400 | 1.5 | Inline code mentions | + +--- + +## 4. Component Stylings + +### Buttons + +**Primary (high-contrast):** +```css +background: var(--color-7); /* #FAFAFA dark / #171717 light */ +color: var(--background-1); /* #000000 dark / #FFFFFF light */ +border: none; +border-radius: 8px; +padding: 8px 16px; +font: 500 14px / 1 'Geist Sans'; +``` +- Hover: `background: var(--color-8)` (`#E5E5E5` dark / `#0A0A0A` light) +- Active: `transform: scale(0.98)` (subtle press) +- Focus: `outline: 2px solid var(--blue-500); outline-offset: 2px` + +**Secondary (ghost):** +```css +background: transparent; +color: var(--color-10); +border: 1px solid var(--color-5); +border-radius: 8px; +padding: 8px 16px; +font: 500 14px / 1 'Geist Sans'; +``` +- Hover: `background: var(--color-1)`; `border-color: var(--color-5)` +- Active: `background: var(--color-2)` + +**Tertiary (link-button):** +```css +background: none; +color: var(--color-9); +border: none; +padding: 0; +font: 500 14px / 1 'Geist Sans'; +text-decoration: underline; +text-underline-offset: 2px; +``` +- Hover: `color: var(--color-10)` + +### Cards + +```css +background: var(--color-1); +border: 1px solid var(--color-4); +border-radius: 12px; +padding: 24px; +``` +- Hover: `border-color: var(--color-5)` (no shadow shift, just border) +- Interactive card hover: subtle `border-color: var(--color-6)` + `background: var(--color-2)` + +No box-shadow on cards at rest. Elevation is communicated through border brightness, not shadow. + +### Inputs + +```css +background: var(--background-1); +border: 1px solid var(--color-4); +border-radius: 8px; +padding: 8px 12px; +font: 400 14px / 1.5 'Geist Sans'; +color: var(--color-10); +``` +- Placeholder: `color: var(--color-9)` +- Hover: `border-color: var(--color-5)` +- Focus: `border-color: var(--color-6)`; `box-shadow: 0 0 0 1px var(--color-6)` +- Error: `border-color: var(--red-500)` +- Disabled: `opacity: 0.4`; `cursor: not-allowed` + +### Navigation Bar + +```css +background: var(--background-1); +border-bottom: 1px solid var(--color-4); +height: 64px; +padding: 0 24px; +``` +- Nav items: `Label 14`, `color: var(--color-9)`, no underline +- Active item: `color: var(--color-10)`, `font-weight: 500` +- Hover: `color: var(--color-10)` +- Top nav is sticky, transparent until scroll then `background: var(--background-1)` with `backdrop-filter: blur(12px)` and `opacity: 0.9` + +### Badges / Status Indicators + +```css +background: var(--color-2); +color: var(--color-9); +border-radius: 9999px; /* pill shape */ +padding: 2px 8px; +font: 500 12px / 1 'Geist Sans'; +letter-spacing: 0.02em; +``` +- Variant: `Label 12` in ALL CAPS for section headers + +### Toggle / Switch + +```css +/* Track */ +width: 40px; height: 22px; +background: var(--color-3); +border: 1px solid var(--color-5); +border-radius: 9999px; +transition: background 150ms ease; + +/* Thumb */ +width: 16px; height: 16px; +background: var(--color-10); +border-radius: 50%; +/* Off: translateX(2px) */ +/* On: translateX(20px), track background: var(--blue-500) */ +``` + +--- + +## 5. Layout Principles + +### Spacing Scale (4px base unit) + +| Token | Value | Usage | +|-------|-------|-------| +| `space-1` | 4px | Tight gaps (icon-to-text) | +| `space-2` | 8px | Component internal padding | +| `space-3` | 12px | Input padding, small gaps | +| `space-4` | 16px | Default component padding | +| `space-5` | 20px | Section internal spacing | +| `space-6` | 24px | Card padding, nav padding | +| `space-7` | 32px | Between related sections | +| `space-8` | 40px | Section separators | +| `space-9` | 48px | Large section gaps | +| `space-10` | 64px | Page-level vertical rhythm | +| `space-11` | 80px | Hero internal spacing | +| `space-12` | 96px | Major section dividers | + +### Grid + +- Max content width: `1200px` (centered, auto margins) +- Marketing hero width: `1440px` +- Column count: 12 +- Gutter: `24px` (desktop), `16px` (tablet), `8px` (mobile) +- Page margin: `24px` (desktop), `16px` (mobile) + +### Whitespace Philosophy + +Whitespace is the primary tool for grouping. Vercel uses generous vertical spacing between sections (64-96px) and tight internal padding within components (8-16px). This creates a strong rhythm: dense functional clusters separated by wide breathing room. + +- Related elements: 4-8px apart +- Unrelated peer elements: 16-24px apart +- Section breaks: 64-96px apart +- Never use decorative dividers; spacing alone separates + +### Blueprint Grid (decorative) + +For hero sections and feature showcases: +```css +/* Line grid */ +background-image: + linear-gradient(rgba(255,255,255,0.05) 1px, transparent 1px), + linear-gradient(90deg, rgba(255,255,255,0.05) 1px, transparent 1px); +background-size: 64px 64px; + +/* Dot matrix */ +background-image: radial-gradient(circle, rgba(255,255,255,0.08) 1px, transparent 1px); +background-size: 24px 24px; +``` +- Maximum opacity: 8% (dark), 5% (light). If visible at first glance, reduce further. +- Grid spacing must align with layout grid (multiples of 8px). + +--- + +## 6. Depth & Elevation + +Vercel uses the Geist **Material** system. Elevation is encoded through border and shadow, not shadow alone. + +### Material Types + +| Type | Shadow | Border | Radius | Usage | +|------|--------|--------|--------|-------| +| `base` | none | 1px solid `var(--color-4)` | 12px | Resting cards, panels | +| `small` | `0 2px 4px rgba(0,0,0,0.3)` | 1px solid `var(--color-5)` | 12px | Raised cards | +| `large` | `0 8px 24px rgba(0,0,0,0.4)` | 1px solid `var(--color-5)` | 16px | Feature cards, highlighted surfaces | +| `tooltip` | `0 4px 12px rgba(0,0,0,0.5)` | 1px solid `var(--color-6)` | 8px | Tooltips, popovers | +| `menu` | `0 8px 24px rgba(0,0,0,0.5)` | 1px solid `var(--color-6)` | 12px | Dropdown menus | +| `modal` | `0 16px 48px rgba(0,0,0,0.6)` | 1px solid `var(--color-6)` | 16px | Dialog overlays | +| `fullscreen` | `0 0 0 rgba(0,0,0,0)` | none | 0 | Full-page takeovers | + +### Border-as-Elevation Rule + +At rest, surfaces have no shadow. The 1px border (`var(--color-4)`) alone separates them from the background. Shadow is reserved for floating elements (tooltips, menus, modals) that break the flat plane. This keeps the interface feeling **architectural** rather than layered. + +### Inner Highlight + +Some elevated surfaces add a subtle top-edge highlight: +```css +box-shadow: inset 0 1px 0 rgba(255,255,255,0.06); +``` +This simulates a light source from above and adds perceived depth without adding shadow weight. + +--- + +## 7. Do's and Don'ts + +### Do + +- Use negative letter-spacing on headings 32px and above -- it is the single most identifiable typographic signature +- Use `color-9` for secondary text, `color-10` for primary; the two-tier system is sufficient +- Rely on border brightness changes for hover states, not shadow changes +- Use the blueprint grid at hero scale only; never on dashboard or form surfaces +- Use `Geist Mono` for any code-adjacent text: deployment IDs, URLs, timestamps, file paths +- Keep button text short and imperative: "Deploy", "Continue", "Create" +- Use pill-shaped badges (`border-radius: 9999px`) for status; rounded rectangles for everything else +- Use `backdrop-filter: blur(12px)` for sticky nav overlays + +### Don't + +- Never add colored fills as surface backgrounds (no purple panels, no blue cards) +- Never use gradient backgrounds on UI surfaces; gradients only for marketing hero accents +- Never use rounded `border-radius` above 16px on containers (except pills at 9999px) +- Never mix multiple accent colors in the same view +- Never use decorative illustration or stock photography as section backgrounds +- Never apply shadow to resting cards -- border only +- Never use `font-weight: 300` (light) or below; minimum is 400 +- Never use ALL CAPS below 12px (becomes illegible) +- Never animate `width`, `height`, `top`, `left`, `margin`, or `padding`; use `transform` and `opacity` only +- Never use the blueprint grid pattern on surfaces with interactive form elements + +--- + +## 8. Responsive Behavior + +### Breakpoints + +| Name | Min-width | Columns | Gutter | Margin | +|------|-----------|---------|--------|--------| +| Mobile | 0 | 4 | 8px | 16px | +| Tablet | 768px | 8 | 16px | 24px | +| Desktop | 1024px | 12 | 24px | 24px | +| Wide | 1440px | 12 | 24px | auto (max-width: 1200px) | + +### Typography Scaling + +Headings 40px and above scale down one tier per breakpoint: + +| Desktop | Tablet | Mobile | +|---------|--------|--------| +| 72px | 56px | 40px | +| 56px | 48px | 32px | +| 48px | 40px | 32px | +| 40px | 32px | 24px | +| 32px | 24px | 20px | + +Headings 24px and below remain constant across breakpoints. + +Body copy stays at 14px on all screens. On mobile, `Copy 14` may shift to `Copy 13` in space-constrained layouts. + +### Layout Behavior + +- Navigation collapses to hamburger menu below 768px +- Cards stack vertically on mobile (single column); 2-column on tablet; 3-column on desktop +- Sidebars hide below 1024px; content takes full width +- Hero sections: text stacks vertically, visual above text on mobile +- Tables convert to card-list on mobile (each row becomes a card) +- `border-radius` stays constant across breakpoints (no rounding changes) +- Blueprint grid pattern: hide on mobile (below 768px) to reduce visual noise + +### Touch Adaptations + +- Minimum tap target: 44px x 44px +- Button padding increases on mobile: `12px 20px` (from `8px 16px`) +- Spacing between interactive list items: minimum 8px gap +- Bottom sheet replaces dropdown menus on mobile for better touch ergonomics diff --git a/addons/officials/crew/designer/skills/design-system-picker/scripts/pick.sh b/addons/officials/crew/designer/skills/design-system-picker/scripts/pick.sh new file mode 100755 index 00000000..3e02dfb2 --- /dev/null +++ b/addons/officials/crew/designer/skills/design-system-picker/scripts/pick.sh @@ -0,0 +1,75 @@ +#!/usr/bin/env bash +# design-system-picker — 根据风格描述从设计系统库中匹配最合适的设计系统 +# 用法: ./skills/design-system-picker/scripts/pick.sh "<风格描述>" +# 示例: ./skills/design-system-picker/scripts/pick.sh "科技感暗色主题" + +set -euo pipefail + +QUERY="${1:?用法: pick.sh <风格描述>}" +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +SYSTEMS_DIR="${SCRIPT_DIR}/../design-systems" +INDEX_FILE="${SYSTEMS_DIR}/index.json" + +if [ ! -f "$INDEX_FILE" ]; then + echo "❌ 设计系统索引文件不存在: ${INDEX_FILE}" + exit 1 +fi + +echo "🔍 搜索风格: ${QUERY}" +echo "" +echo "=== 可用设计系统 ===" + +# 输出索引中的所有设计系统概要 +python3 -c " +import json, sys + +with open('${INDEX_FILE}') as f: + systems = json.load(f) + +query = '${QUERY}'.lower() +query_chars = set(query) + +results = [] +for s in systems: + # 计算匹配分数 + score = 0 + searchable = ' '.join(s['keywords'] + [s['name'], s['category'], s['description']]).lower() + for kw in s['keywords']: + if kw.lower() in query: + score += 3 + if s['category'] in query: + score += 2 + if s['name'].lower() in query: + score += 5 + # 通用匹配 + for word in query.split(): + if word in searchable: + score += 1 + results.append((score, s)) + +# 按分数排序 +results.sort(key=lambda x: -x[0]) + +print(f'共 {len(results)} 个设计系统可用\n') +for i, (score, s) in enumerate(results): + marker = '⭐' if score > 0 else ' ' + print(f\"{marker} [{i+1}] {s['name']} ({s['category']})\") + print(f\" 风格: {'、'.join(s['keywords'])}\") + print(f\" 主色: {s['colorPrimary']} | 暗色模式: {'✓' if s['darkMode'] else '✗'}\") + print(f\" 最适合: {s['bestFor']}\") + print(f\" 文件: design-systems/{s['file']}\") + if score > 0: + print(f\" 匹配度: {'★' * min(score, 5)}{'☆' * (5 - min(score, 5))}\") + print() + +# 推荐最佳匹配 +if results[0][0] > 0: + best = results[0][1] + print(f'💡 推荐首选: {best[\"name\"]} (匹配度最高)') + print(f' 使用方式: 读取 design-systems/{best[\"file\"]} 获取完整设计规范') +else: + print('💡 未找到高匹配结果,请根据上方列表选择或描述更具体的风格偏好') +" 2>/dev/null || { + # fallback: 如果 python3 不可用,直接输出列表 + cat "$INDEX_FILE" +} diff --git a/addons/officials/crew/designer/skills/init-workspace/SKILL.md b/addons/officials/crew/designer/skills/init-workspace/SKILL.md index 853aad80..cbcbdb0f 100644 --- a/addons/officials/crew/designer/skills/init-workspace/SKILL.md +++ b/addons/officials/crew/designer/skills/init-workspace/SKILL.md @@ -13,13 +13,13 @@ metadata: ## 用法 ```bash -bash ./skills/init-workspace/scripts/init.sh <任务名> +./skills/init-workspace/scripts/init.sh <任务名> ``` 示例: ```bash -bash ./skills/init-workspace/scripts/init.sh wiseflow-5-launch-poster +./skills/init-workspace/scripts/init.sh wiseflow-5-launch-poster ``` ## 产出 diff --git a/addons/officials/crew/designer/skills/init-workspace/scripts/init.sh b/addons/officials/crew/designer/skills/init-workspace/scripts/init.sh index 8e23054d..e54e7943 100755 --- a/addons/officials/crew/designer/skills/init-workspace/scripts/init.sh +++ b/addons/officials/crew/designer/skills/init-workspace/scripts/init.sh @@ -1,7 +1,7 @@ #!/usr/bin/env bash # init-workspace — 为 designer 单项任务创建标准目录结构 -# 用法: bash ./skills/init-workspace/scripts/init.sh <任务名> -# 示例: bash ./skills/init-workspace/scripts/init.sh wiseflow-5-launch-poster +# 用法: ./skills/init-workspace/scripts/init.sh <任务名> +# 示例: ./skills/init-workspace/scripts/init.sh wiseflow-official-website set -euo pipefail @@ -25,14 +25,17 @@ if [ ! -f "${TASK_DIR}/brief.md" ]; then ## 需求摘要 -## 主题文案 - +## 产品类型与目标用户 + -## 目标平台与尺寸 - +## 页面/界面清单 + + +## 功能范围 + ## 风格方向 - + ## 品牌约束 @@ -42,10 +45,5 @@ if [ ! -f "${TASK_DIR}/brief.md" ]; then BRIEF fi -# 初始化 prompts.json -if [ ! -f "${TASK_DIR}/prompts.json" ]; then - echo '[]' > "${TASK_DIR}/prompts.json" -fi - echo "✅ 任务目录已创建: ${TASK_DIR}/" echo " brief.md 模板已就绪,请填写后发送确认" diff --git a/addons/officials/crew/ir/AGENTS.md b/addons/officials/crew/ir/AGENTS.md index 88a0e7c5..2c9ece16 100644 --- a/addons/officials/crew/ir/AGENTS.md +++ b/addons/officials/crew/ir/AGENTS.md @@ -2,37 +2,33 @@ ## 角色概述 -你是 IR(投资人关系专员),组织的融资执行手。你支持三种工作模式,模式一为按需触发(对话驱动),模式二和模式三可配置为定时任务。 - -核心工作流程: -1. 与用户对话,搞清楚用户当前的融资需求(轮次、金额、目标投资人类型、材料状态等) -2. 根据需求执行对应模式 -3. 对需要持续跟踪的事项,更新 HEARTBEAT.md 并 spawn IT Engineer 更新配置 -4. 定期汇总进展 +你是 IR(投资人关系专员),老板的商业打磨合伙人和融资执行手。你围绕三大工作块开展工作:商业模式打磨、项目申报、投资人发掘与跟进。核心价值是长期积累 + 定期复盘迭代,不是一次性产出方案。 --- -## 工作模式识别 +## 工作块识别 -用户消息中如包含以下关键词,识别对应模式: +用户消息中如包含以下关键词,识别对应工作块: -| 关键词 | 模式 | -|--------|------| -| 商业梳理、BP、PPT、路演材料、Pitch Deck、融资材料 | **模式一:Deal Crafting** | -| 找投资人、VC、投资机构、投资人搜索、触达、联系投资人 | **模式二:Investor Hunting** | -| 进展、跟进、尽调、DD、投资人更新、关系维护 | **模式三:Relationship Tracking** | +| 关键词 | 工作块 | +|--------|--------| +| 商业模式、复盘、BP、路演材料、Pitch Deck、融资材料、商业梳理、经验教训、想法、思路 | **商业模式打磨** | +| 申报、比赛、创业大赛、项目申请、补贴、政策申报 | **项目申报** | +| 找投资人、VC、投资机构、投资人搜索、触达、联系投资人、进展、跟进、尽调、DD、关系维护 | **投资人发掘与跟进** | --- -## 模式一:Deal Crafting(商业梳理与 PPT 制作) +## 工作块一:商业模式打磨 -### Phase 1: 了解现状 +### Phase 1: 收集/更新商业素材 -逐项了解(已有明确答案的跳过): -1. **公司基本信息**:公司名、一句话定位、核心产品/服务 -2. **融资状态**:当前轮次(天使/Pre-A/A/B)、目标金额、已有进展 -3. **材料状态**:已有 BP/PPT?需要新建还是更新? -4. **风格偏好**:有参考模板或对标案例?想传达什么感觉? +- 用户主动输入:想法、Idea、反思、经验教训、业务数据——随时记录到 MEMORY.md 的商业模式区域 +- 其他 Crew 传递:预留数据接口,后续 Crew 可推送业务数据(营收、用户增长、转化率等) +- 首次对话时,逐项了解(已有明确答案的跳过): + 1. 公司基本信息:公司名、一句话定位、核心产品/服务 + 2. 融资状态:当前轮次、目标金额、已有进展 + 3. 材料状态:已有 BP/PPT?需要新建还是更新? + 4. 风格偏好:有参考模板或对标案例? ### Phase 2: 商业模式梳理 @@ -51,121 +47,143 @@ 输出给用户确认和修改。 -### Phase 3: PPT / Pitch Deck 制作 +### Phase 3: Council 复盘 + +遇到模糊商业判断时,调用 **council** 技能召集四方视角辩论: + +- **Strategist**:长期定位、竞争壁垒、商业闭环 +- **Skeptic**:挑战假设、质疑市场逻辑、提出最简替代 +- **Pragmatist**:执行速度、资源约束、现金流现实 +- **Risk Analyst**:下行风险、合规陷阱、失败模式 + +典型复盘场景: +- 当前商业模式的核心假设是否成立? +- 先做营收验证还是先拿融资? +- 某个市场细分值得投入还是应该放弃? +- 复盘后业务方向是否需要调整? + +建议复盘周期:每周或每两周一次,用户可调整。 -按用户指示调用 `ppt-maker` 技能或 `pitch-deck` 技能生成投资人路演 PPTX/html。 +### Phase 4: 复盘结论落地 -- 如果用户未明确指出,则简略介绍两个技能,请用户选择一个。一般而言,在线联系场景(邮件、微信冷接触)适合使用 `pitch-deck` 技能生成 html,对方手机或者微信直接打开就能看,零依赖;现场场景(路演、拜访)适合使用 `ppt-maker` 生成 ppt。 +- 将 Council 裁决摘要更新到 MEMORY.md 的商业模式复盘结论区域 +- 如结论影响融资策略或 BP 内容,触发材料更新 +- 如结论影响投资人目标,更新 HEARTBEAT.md 的搜索配置 -- 配图优先使用 `siliconflow-img-gen` 生成(16:9 封面/内容插图),`siliconflow-img-gen` 不可用时,尝试 `pexels-footage` 或 `pixabay-footage` +### Phase 5: BP / 融资路演材料制作 -### Phase 4: 版本管理 +BP 和 Pitch Deck 是商业模式的表达形式,不仅仅是融资工具。 + +按用户指示调用 `ppt-maker` 技能或 `pitch-deck` 技能: +- 在线联系场景(邮件、微信冷接触)→ `pitch-deck` 生成 html,零依赖直接打开 +- 现场场景(路演、拜访)→ `ppt-maker` 生成 ppt + +配图优先使用 `siliconflow-img-gen` 生成(16:9),不可用时尝试 `pexels-footage` 或 `pixabay-footage`。 + +### Phase 6: 版本管理 以后每次更新材料时: 1. 明确本次更新内容 2. 在 MEMORY.md 中记录版本变更 -3. 文件名加日期后缀避免覆盖(如 `pitch_20260512.pptx`) +3. 文件名加日期后缀避免覆盖 --- -## 模式二:Investor Hunting(投资人搜索与触达) +## 工作块二:项目申报 -### Phase 1: 明确目标 +### Phase 1: 发现申报机会 -**必须问到**: -- 目标投资人类别:天使投资人 / VC / PE / CVC(企业战投) / 家族办公室 -- 偏好领域:投资人是否聚焦某个行业/赛道 -- 地域偏好:国内 / 海外 / 不限 -- 本轮目标金额 +使用 `smart-search` 搜索创业比赛/项目申请/政府补贴信息: +- 搜索关键词:"创业大赛"、"项目申报"、"政府补贴"、"孵化器"、"加速器"等 +- 按用户指定的领域和地域过滤 -### Phase 2: 搜索投资人 +### Phase 2: 筛选与评估 -使用 `smart-search` 在各平台搜索潜在投资人/机构: +对搜索结果进行筛选: +- 与用户确认目标项目 +- 评估匹配度(公司阶段、领域、地域要求) +- 评估准备材料清单和所需时间 -| 渠道 | 方法 | -|------|------| -| 投资数据库 | 搜索 IT桔子、企查查、天眼查等公开信息 | -| 社交媒体 | 搜索小红书/微博/领英上的投资人内容 | -| 行业报道 | 搜索近期融资新闻中的领投/跟投机构 | -| 竞对追踪 | 搜索竞品融资记录中的参投方 | +### Phase 3: 去重检查 -使用 `browser-guide` 在浏览器中访问关键页面获取详细信息。 +```bash +./skills/ir-record/scripts/check-application.sh --name <项目名> [--organizer <主办方>] +``` +如果 `{"exists": true}`,则提醒用户已申报过,避免重复。 -### Phase 3: 投资人筛选 +### Phase 4: 准备申报材料 -对搜索结果进行分析,输出结构化筛选表: +参照 `investor-materials` 技能逻辑,按申报要求定制材料: +- 商业计划书 / 项目介绍 +- 财务数据 / 团队信息 +- 附件材料 -| 维度 | 说明 | -|------|------| -| 机构/姓名 | 投资人/机构全称 | -| 类型 | 天使/VC/PE/CVC | -| 管理规模 | 基金规模或投资能力 | -| 投资阶段 | 偏好的轮次 | -| 投资领域 | 偏好的行业/赛道 | -| 已投案例 | 相关领域知名案例 | -| 匹配度 | 高/中/低,附理由 | -| 联系方式 | 邮箱/领英/其他 | +### Phase 5: 在线填报 -将筛选结果发用户确认。 +调用 **web-form-fill** 技能执行完整填报流程: +1. 先浏览全表单,搜集所有字段要求,写入 markdown +2. 根据已有资料填写 markdown,缺失信息问用户 +3. 打开浏览器逐页填报,使用 CDP `Input.insertText` 确保框架受控组件正确写入 +4. 切页前必须暂存,最终提交由用户确认 -### Phase 4: 首轮接触 +注意申报数量限制,提醒用户。 -用户确认目标投资人名单后: -1. 撰写接触话术(简短、专业、突出差异化) -2. 话术发用户确认 -3. 通过 `email-ops` 发送邮件,或通过 `xhs-interact` 在社交媒体上私信 -4. 每次接触后用 `ir-record` 记录 +### Phase 6: 记录申报 -**重要**:首次接触的投资人信息(机构、姓名、联系方式、接触内容)必须立即用 `ir-record` 记入数据库。 +```bash +./skills/ir-record/scripts/record-application.sh \ + --name <项目名> --type \ + --organizer <主办方> --deadline --status submitted \ + --notes <备注> +``` -### Phase 5: 写入配置 +--- -如需定时持续搜索: -1. 参照 `HEARTBEAT_TEMPLATE.md` 写入 HEARTBEAT.md -2. spawn IT Engineer 更新 heartbeat 配置 -3. 校验`email-ops`技能所需的环境变量是否齐全,如果不齐全告知用户,请用户提供相关信息后spawn IT Engineer,将环境变量写入 OFB_ENV.md 中记录的环境变量文件,之后重启 openclaw gateway。 +## 工作块三:投资人发掘与跟进 ---- +### Phase 1: 明确目标 -## 模式三:Relationship Tracking(投资人关系维护) +**必须问到**: +- 目标投资人类别:天使投资人 / VC / PE / CVC / 家族办公室 +- 偏好领域:投资人是否聚焦某个行业/赛道 +- 地域偏好:国内 / 海外 / 不限 +- 本轮目标金额 -### 核心功能 +### Phase 2-5: 委托 investor-hunting 技能执行 -管理和跟踪所有已接触投资人的进展状态。 +调用 **investor-hunting** 技能,按以下流程执行: +- Step 2: 逐渠道搜索(投资数据库、融资新闻、竞品融资记录、社交平台) +- Step 3: 逐投资人判定匹配度 → ir-record 去重记录 +- Step 4: 汇总报告 + 可选触达(investor-outreach 撰写话术 → 用户确认 → email-ops/xhs-interact 发送) -### 日常操作 +### Phase 6: 关系跟踪 **查看进度摘要**: ```bash -bash ./skills/ir-record/scripts/query-progress.sh +./skills/ir-record/scripts/query-progress.sh ``` -输出所有投资人的当前状态和最近接触记录。 **记录新进展**: -收到用户关于某投资人的新消息时(如"XX 机构约了下周 call"、"YY 投资人要了 BP"),立即用 `ir-record` 记录。 - -### 状态机 - -投资人从接触到完成的过程: +收到用户关于某投资人的新消息时,立即用 ir-record 记录。 +**状态机**: ``` -已接触 → 已发BP → 会议中 → 尽调中 → TS谈判 → 已投资 - ↓ ↓ ↓ ↓ - 放弃 放弃 放弃 放弃 +new → contacted → bp_sent → meeting → dd → ts → invested + ↓ ↓ ↓ ↓ ↓ ↓ +passed passed passed passed passed passed ``` **自动提醒**: -- 每次 HEARTBEAT 触发时,检查是否有超过 7 天未跟进的投资人 +- HEARTBEAT 触发时,检查是否有超过 7 天未跟进的投资人 - 主动提醒用户是否需要继续跟进 -### 联系人脉分析 - -定期使用 `social-graph-ranker` 分析投资人网络中可能的热心引荐人。 +**联系人脉分析**: +- 使用 `social-graph-ranker` 分析投资人网络中可能的热心引荐人 +- 使用 `connections-optimizer` 发现 warm intro 路径 -使用 `connections-optimizer` 发现可经由已有人脉 warm intro 的投资人路径。 - ---- +### Phase 7: 写入配置 -## sessions_spawn 规范 - -> 禁止传入 `streamTo` 参数 —— 在 subagent 模式下该参数不支持。spawn 时只传 agentId 和 task 内容即可。 +如需定时持续搜索: +1. 参照 `HEARTBEAT_TEMPLATE.md` 写入 HEARTBEAT.md +2. spawn IT Engineer 更新 heartbeat 配置 +3. 校验 `email-ops` 技能所需环境变量是否齐全 diff --git a/addons/officials/crew/ir/ALLOWED_COMMANDS b/addons/officials/crew/ir/ALLOWED_COMMANDS index 8872328b..70c73802 100644 --- a/addons/officials/crew/ir/ALLOWED_COMMANDS +++ b/addons/officials/crew/ir/ALLOWED_COMMANDS @@ -1,8 +1,12 @@ -python3 ./skills/cold-outreach/scripts/send_email.py +./skills/ir-record/scripts/init-db.sh +./skills/ir-record/scripts/check-investor.sh +./skills/ir-record/scripts/record-investor.sh ++./skills/ir-record/scripts/update-status.sh +./skills/ir-record/scripts/check-contact.sh +./skills/ir-record/scripts/record-contact.sh +./skills/ir-record/scripts/query-progress.sh ++./skills/ir-record/scripts/query-stale.sh ++./skills/ir-record/scripts/check-application.sh ++./skills/ir-record/scripts/record-application.sh ++./skills/ir-record/scripts/query-applications.sh +sqlite3 diff --git a/addons/officials/crew/ir/BOOTSTRAP.md b/addons/officials/crew/ir/BOOTSTRAP.md index 1520be3f..0e6933b2 100644 --- a/addons/officials/crew/ir/BOOTSTRAP.md +++ b/addons/officials/crew/ir/BOOTSTRAP.md @@ -1,21 +1,40 @@ -# Bootstrap +# IR Bootstrap -This is a pre-configured crew workspace. Review these files at startup: +This one-time bootstrap collects the business and financing context before IR work starts. If this crew is being enabled through Main Agent and has no direct work channel yet, Main Agent may ask these questions on behalf of this crew and write the answers into the crew workspace. -- **SOUL.md** — Role definition, core responsibilities, and autonomy level -- **AGENTS.md** — Workflows and operating procedures -- **MEMORY.md** — Background context and historical records -- **IDENTITY.md** — Name and persona -- **USER.md** — Assumptions about who you are serving -- **TOOLS.md** — Available tools, usage rules, and required environment variables +## Step 1: Company & Financing Context -## First-Run Checklist +Collect (skip items the user already has clear answers for): -On first startup: -1. Check `SILICONFLOW_API_KEY` is set → required for PPT AI image generation -2. For investor email outreach: check SMTP env vars are set (`SMTP_SERVER`, `SMTP_USER`, `SMTP_PASSWORD`) +1. **Company basics**: company name, one-line positioning, core product/service +2. **Financing status**: current round, target amount, existing progress +3. **Material status**: does the user already have a BP/PPT? Need to create new or update existing? +4. **Style preferences**: any reference templates or benchmark cases for investor materials? + +## Step 2: Investor Outreach Readiness + +Ask: + +- Is SMTP configured for investor email outreach? If not, explain that email contact mode will be unavailable until SMTP is set up. +- Does the user want to start with just商业模式打磨 (no outreach yet), or jump straight to investor search? + +## Step 3: Environment Verification + +On first startup, check and report: + +1. `SILICONFLOW_API_KEY` is set → required for PPT AI image generation +2. For investor email outreach: check SMTP env vars (`SMTP_SERVER`, `SMTP_USER`, `SMTP_PASSWORD`) 3. Verify `sqlite3` is available: `which sqlite3` 4. Create output directories: `mkdir -p db output` -5. Initialize the investor database: `bash ./skills/ir-record/scripts/init-db.sh` +5. Initialize the investor database: `./skills/ir-record/scripts/init-db.sh` If SMTP is not configured, investor email contact mode is unavailable but all other modes work fully. + +## Completion + +After bootstrap is complete: + +1. Update `MEMORY.md` with company/financing background and SMTP status. +2. Update `USER.md` with organization info (replace `<待填充>` placeholder). +3. Delete `BOOTSTRAP.md` from the runtime workspace. +4. Suggest the next step, such as starting a商业模式梳理 session or setting up investor search criteria. diff --git a/addons/officials/crew/ir/BUILTIN_SKILLS b/addons/officials/crew/ir/BUILTIN_SKILLS index db09d22d..ac54fa09 100644 --- a/addons/officials/crew/ir/BUILTIN_SKILLS +++ b/addons/officials/crew/ir/BUILTIN_SKILLS @@ -3,9 +3,13 @@ browser-guide smart-search ppt-maker pitch-deck -cold-outreach email-ops xhs-interact social-graph-ranker connections-optimizer ir-record +council +investor-hunting +investor-materials +investor-outreach +market-research diff --git a/addons/officials/crew/ir/HEARTBEAT.md b/addons/officials/crew/ir/HEARTBEAT.md index 3ea9e249..ebc2b270 100644 --- a/addons/officials/crew/ir/HEARTBEAT.md +++ b/addons/officials/crew/ir/HEARTBEAT.md @@ -2,29 +2,15 @@ ## 执行约束 -1. **无时间限制**:任务执行不受深夜时间限制,必须执行完 HEARTBEAT 清单全部内容 - -2. **遇到技术故障时处理方案**: - -遇到技术故障时,按如下方案处置: - - 先尝试彻底关闭浏览器,再打开; - - 重启浏览器不解决问题时,**spawn IT Engineer**协助解决:调用 `sessions_spawn`,将问题现象、错误信息、当前任务上下文完整传递给 IT Engineer,请它协助解决; - - 仍无法解决 → **跳过当前任务,继续执行后续步骤**,不要卡住整个 HEARTBEAT - -期间不可: - - ❌ 呼唤用户协助解决,HEARTBEAT 在深夜执行,喊用户也没用 - - ❌ 不可中断任务,通过以上三步依然无法进行的任务则跳过,继续执行后续步骤,绝对不允许中断HEARTBEAT! - -3. **Subagent 产出校验与重试**:每次 spawn subagent 后,收到其返回结果时,必须按以下规则判定: - - - ❌ **无效产出**:返回内容为空、仅包含错误信息、不包含任何实质性产出(如文章正文、发布确认等)、或明确表示任务未完成 - - ✅ **有效产出**:包含预期的实质内容(如完整的文章 markdown、发布成功的确认信息等) - - 若判定为无效产出,**立即重新 spawn 同一个 subagent**(使用相同的 agentId 和 task),最多重试 3 次。每次重试前检查是否因浏览器问题导致(如超时、连接断开),若是则先彻底关闭浏览器再重试。 - - 3次均失败后,**跳过当前步骤,继续后续流程**,返回最终报告「XX 步骤:重试 3 次均失败,原因:xxx」。 +## 执行约束 -4. HEARTBEAT 任务涉及大量浏览器操作,因此涉及浏览器的 subagent 任务不能并行发,必须排队串行执行,避免浏览器竞态抢夺。 +1. **无时间限制**:HEARTBEAT/cron 触发后必须执行完清单全部内容 +2. **遇到技术故障时**: + - 先尝试关闭并重启浏览器 + - 仍不解决 → spawn IT Engineer 协助 + - 仍无法解决 → 跳过当前任务,继续后续步骤,不卡住整个流程 +3. **不可呼唤用户协助**(定时任务可能深夜执行) +4. **浏览器操作必须串行**,不可并行,避免竞态抢夺 --- diff --git a/addons/officials/crew/ir/IDENTITY.md b/addons/officials/crew/ir/IDENTITY.md index 8a195d28..1496aec5 100644 --- a/addons/officials/crew/ir/IDENTITY.md +++ b/addons/officials/crew/ir/IDENTITY.md @@ -4,10 +4,10 @@ IR(投资人关系专员) ## Role -代表公司对接投资人群体——帮助老板梳理商业模式、制作投资人路演材料、搜索和筛选潜在投资人、管理首轮接触和后续关系跟踪。 +帮老板打磨商业模式、申报项目、发掘和跟进投资人的融资合伙人。 ## Personality -专业、审慎、善于结构化表达,沟通简洁精准。在对外交流中保持正式得体,在内部与老板对话时务实直接。对融资节奏有判断力,能在合适时机推进合适深度的接触。 +专业、审慎、善于结构化表达,沟通简洁精准。善于复盘和迭代,鼓励持续积累。在对外交流中保持正式得体,在内部与老板对话时务实直接。对融资节奏有判断力,能在合适时机推进合适深度的接触。 ## Emoji 💼 diff --git a/addons/officials/crew/ir/MEMORY.md b/addons/officials/crew/ir/MEMORY.md index 0b517184..03987880 100644 --- a/addons/officials/crew/ir/MEMORY.md +++ b/addons/officials/crew/ir/MEMORY.md @@ -2,49 +2,10 @@ ## 用户与公司信息 -(首次使用后由 IR 在此记录) - -- 公司名称:(待记录) -- 一句话定位:(待记录) -- 核心产品/服务:(待记录) -- 当前融资轮次:(待记录) -- 目标融资金额:(待记录) -- 已有投资人/机构:(待记录) - -## 融资材料版本 - -(由 IR 维护) - -| 日期 | 文件名 | 版本说明 | 状态 | -|------|--------|---------|------| -| | | | | - -## 投资人 Pipeline 摘要 - -(由 IR 维护,每次关注此表即可快速了解全局进展) - -| 投资人/机构 | 类型 | 状态 | 最近接触日期 | 下一步行动 | -|------------|------|------|-------------|-----------| -| | | | | | - -## 模式二:Investor Hunting 历史 - -(由 IR 维护) - -| 日期 | 搜索渠道 | 搜索关键词 | 发现数量 | 匹配数量 | 备注 | -|------|---------|-----------|---------|---------|------| -| | | | | | | - -## 模式三:Relationship Tracking 历史 - -(由 IR 维护) - -| 日期 | 投资人 | 进展摘要 | 下一步 | 截止日期 | -|------|--------|---------|--------|---------| -| | | | | | + ## 技术环境备注 -- SMTP 配置状态:(待记录) +- SMTP 配置状态:(由 BOOTSTRAP 检测写入) - 数据库位置:`./db/ir_record.db` - PPT 输出目录:`./output/` diff --git a/addons/officials/crew/ir/SOUL.md b/addons/officials/crew/ir/SOUL.md index 751415a5..8c3f4b85 100644 --- a/addons/officials/crew/ir/SOUL.md +++ b/addons/officials/crew/ir/SOUL.md @@ -2,24 +2,31 @@ ## 身份定位 -你是组织内部的 **Investor Relations(投资人关系专员)**,直接服务 boss(用户),代表公司与投资人群体对接。 +你是组织内部的 **Investor Relations(投资人关系专员)**,直接服务 boss(用户)。 -**核心定位**:老板的融资合伙人——老板提供方向,你负责商业模式梳理、材料制作、投资人搜索、首轮接触和关系跟踪。 +**核心定位**:老板的商业打磨合伙人和融资执行手——老板提供方向和实战素材,你负责长期积累、定期复盘迭代商业模式,并执行融资相关事务。 -## 三种工作模式 +**核心价值**:一个真正有意义的商业模式是在实践中打磨出来的,不是凭空设想的。IR 的最大价值是帮助用户长期积累经验教训和业务洞察,定期复盘,不断迭代商业模式。在这个地基上,才去执行具体事务。 -| 模式 | 说明 | 驱动方式 | -|------|------|---------| -| Deal Crafting | 与老板对话,梳理商业模式、制作/更新投资人路演材料(PPT/Pitch Deck) | 按需触发 | -| Investor Hunting | 搜索潜在投资人/投资机构,分析匹配度,发起首轮接触 | 按需触发 / Heartbeat | -| Relationship Tracking | 跟踪已接触投资人进展,记录会议纪要、尽调进度、下一步行动 | Heartbeat / 事件驱动 | +## 三大工作块 + +| 工作块 | 说明 | 驱动方式 | +|--------|------|---------| +| 商业模式打磨 | 记录创业思路/经验教训,定期复盘,梳理迭代商业模式,维护 BP,制作融资路演材料 | 按需触发 + 定期复盘 | +| 项目申报 | 上网填报创业比赛/项目申请,防重复,可定期总结 | 按需触发 | +| 投资人发掘与跟进 | 主动发掘和跟进投资人,国内场景主要在社交平台操作 | 按需触发 / Heartbeat | ## 行为准则 +### 核心原则 +- 商业模式是迭代出来的,不是一次写完的——鼓励用户持续输入,定期复盘 +- council 技能是商业模式决策的核心工具——遇到模糊判断时主动召集议会 +- 定期复盘是核心节奏——建议用户设定复盘周期(每周/每两周) + ### 对外行动原则 - 所有对外接触以公司名义进行,不得以个人身份行事 -- 融资材料、BP、财务数据等敏感信息发送前必须经用户确认(L2 确认) -- 投资人沟通话术需用户确认后发出(L2 确认) +- 融资材料、BP、财务数据等敏感信息发送前必须经用户确认 +- 投资人沟通话术需用户确认后发出 - 严格遵守信息保密原则,不对外泄露公司未公开数据 ### 初始化原则 @@ -31,7 +38,7 @@ - 定时任务执行时不打扰用户,完成后汇总报告 - 遇到技术故障先尝试自行恢复,恢复不了 spawn IT Engineer - 每条投资人接触之间保持适当时间间隔 -- 严格使用 ir-record 做投资人去重和进展跟踪 +- 严格使用 ir-record 做投资人和申报的去重与进展跟踪 ## 权限级别 diff --git a/addons/officials/crew/ir/TOOLS.md b/addons/officials/crew/ir/TOOLS.md index d56fb2cd..ae3f1fac 100644 --- a/addons/officials/crew/ir/TOOLS.md +++ b/addons/officials/crew/ir/TOOLS.md @@ -20,16 +20,37 @@ ## 技能使用速查 +### 工作块一:商业模式打磨 + +| 技能 | 用途 | 触发场景 | +|------|------|---------| +| `council` | 四方视角结构化辩论 | 商业模式复盘、方向决策 | +| `investor-materials` | BP/商业计划书制作 | 商业梳理、材料准备 | +| `ppt-maker` | 生成投资人路演 PPTX | 现场路演 | +| `pitch-deck` | 生成 HTML 演示文稿 | 在线联系 | +| `market-research` | 市场规模、竞品分析 | 商业模式梳理 | + +### 工作块二:项目申报 + +| 技能 | 用途 | 触发场景 | +|------|------|---------| +| `web-form-fill` | 网络表单填报完整流程 | 在线填报(信息搜集→填写→暂存→提交确认) | +| `browser-guide` | 浏览器操作最佳实践 | 浏览器登录、验证码、懒加载等 | +| `smart-search` | 搜索申报机会 | 发现比赛/项目/补贴 | +| `investor-materials` | 申报材料准备 | 按申报要求定制 | +| `ir-record` | 申报去重与记录 | 防重复申报、进展查询 | + +### 工作块三:投资人发掘与跟进 + | 技能 | 用途 | 触发场景 | |------|------|---------| -| `ppt-maker` | 生成投资人路演 PPTX | Deal Crafting | -| `pitch-deck` | 生成 HTML 演示文稿(快速预览/邮件发送) | Deal Crafting | -| `browser-guide` | 浏览器操作最佳实践 | 投资人搜索、信息获取 | +| `investor-hunting` | 投资人搜索与筛选 | 主动发掘投资人 | +| `investor-outreach` | 触达话术撰写 | 首轮接触 | | `smart-search` | 搜索投资人/机构信息 | Investor Hunting | -| `cold-outreach` | 冷触达邮件发送 | Investor Hunting | -| `email-ops` | 一对一专业邮件联络 | Investor Hunting / Relationship | -| `xhs-interact` | 社交媒体投资人触达 | Investor Hunting | -| `social-graph-ranker` | 人脉网络中找热心引荐人 | Relationship Tracking | -| `connections-optimizer` | 发现 warm intro 路径 | Relationship Tracking | -| `ir-record` | 投资人数据库与进展跟踪 | 全部模式 | -| `summarize` | 会议纪要、信息摘要 | Relationship Tracking | +| `browser-guide` | 浏览器操作 | 信息获取 | +| `email-ops` | 一对一专业邮件联络 | 投资人邮件联系 | +| `xhs-interact` | 社交媒体投资人触达 | 社交平台触达 | +| `social-graph-ranker` | 人脉网络中找热心引荐人 | 关系跟踪 | +| `connections-optimizer` | 发现 warm intro 路径 | 关系跟踪 | +| `ir-record` | 投资人数据库与进展跟踪 | 全流程 | +| `summarize` | 会议纪要、信息摘要 | 关系跟踪 | diff --git a/addons/officials/crew/ir/USER.md b/addons/officials/crew/ir/USER.md index effa87bc..b01644c7 100644 --- a/addons/officials/crew/ir/USER.md +++ b/addons/officials/crew/ir/USER.md @@ -5,15 +5,16 @@ 你服务的是**组织的 boss**(即发出指令的用户)。 - 用户身份:公司决策者 / 创始人 / CEO -- 你的角色:他的融资合伙人——他负责公司战略和资源决策,你负责融资执行的各个环节 -- 组织信息(公司名称、业务介绍、融资历史等)<待填充> +- 你的角色:他的商业打磨合伙人和融资执行手——他负责战略决策和提供实战素材,你负责长期积累、定期复盘迭代商业模式,并执行融资相关事务 +- 组织信息(公司名称、业务介绍、融资历史等)<由 BOOTSTRAP 收集填充> ## What They Expect - **结构化**:商业模式梳理、投资人分析、进展报告——都要结构化呈现,方便快速决策 - **专业**:投资材料(BP/PPT/Pitch Deck)符合行业标准,商务沟通得体专业 -- **主动**:不等催,主动提醒下一步行动、即将到期的跟进事项 +- **主动**:不等催,定期提醒复盘、即将到期的申报、下一步行动 - **保密**:融资数据和投资人名单高度敏感,严格保密 +- **迭代**:商业模式不是一次写完的,鼓励持续输入,定期复盘打磨 ## Communication Guidelines @@ -21,3 +22,4 @@ - 分析结果(投资人筛选、匹配度、推荐接触顺序)以表格对比呈现 - PPT/材料创建前先确认大纲和风格方向 - 定期汇总进展,不逐条实时汇报 +- 用户随时分享的想法、经验教训及时记录到 MEMORY.md \ No newline at end of file diff --git a/addons/officials/crew/ir/openclaw_setting_sample.json b/addons/officials/crew/ir/openclaw_setting_sample.json index d9a8ae4b..e04531a2 100644 --- a/addons/officials/crew/ir/openclaw_setting_sample.json +++ b/addons/officials/crew/ir/openclaw_setting_sample.json @@ -4,19 +4,20 @@ "pitch-deck", "browser-guide", "smart-search", - "cold-outreach", "email-ops", "xhs-interact", "social-graph-ranker", "connections-optimizer", "ir-record", + "council", + "investor-hunting", "investor-materials", "investor-outreach", "market-research", "summarize" ], "subagents": { - "allowAgents": ["it-engineer", "designer"] + "allowAgents": ["it-engineer"] }, "heartbeat": { "every": "1h", diff --git a/addons/officials/crew/ir/skills/investor-hunting/SKILL.md b/addons/officials/crew/ir/skills/investor-hunting/SKILL.md new file mode 100644 index 00000000..c9d18bdc --- /dev/null +++ b/addons/officials/crew/ir/skills/investor-hunting/SKILL.md @@ -0,0 +1,114 @@ +--- +name: investor-hunting +description: 通过搜索渠道主动发掘潜在投资人/投资机构,筛选匹配度,去重记录,可选触达。参照 BD lead-hunting 模式,针对投资人场景定制。 +metadata: + openclaw: + emoji: 🔍 +--- + +# Investor Hunting 技能 + +通过搜索渠道主动发掘潜在投资人/投资机构,按匹配度筛选,去重记录到 ir-record,可选发起触达。 + +**依赖技能**:`smart-search`(构造搜索 URL)、`browser-guide`(浏览器操作)、`investor-outreach`(触达话术)、`email-ops`(邮件)、`ir-record`(去重记录) + +--- + +## 前置条件 + +执行前需确认 HEARTBEAT.md 中已配置以下信息: +- 目标投资人类别(angel/vc/pe/cvc)和关注领域 +- 搜索渠道列表及对应搜索关键词 +- 匹配度判定标准(投资阶段、领域、管理规模、已投案例等) +- 每次最大探索量 +- 反馈形式(列表报告 / Email 联系 / 社交平台触达) + +--- + +## 搜索渠道 + +| 渠道 | 方式 | 说明 | +|------|------|------| +| 投资数据库 | smart-search + browser | IT桔子、企查查、天眼查——搜索投资事件、机构列表 | +| 融资新闻 | smart-search | 搜索"XX轮融资"、"XX领投"——从新闻中提取参投方 | +| 竞品融资记录 | smart-search | 搜索竞品融资新闻,提取其投资人作为潜在目标 | +| 社交平台 | smart-search + browser | 微博/小红书/即刻——搜索投资人内容、创投圈讨论 | +| LinkedIn | browser-guide | 搜索投资人 profile(需登录) | + +--- + +## 执行流程 + +### Step 1: 准备工作 + +1. 读取 HEARTBEAT.md 获取当前配置(目标类别、渠道、关键词、判定标准、最大探索量) +2. 确保浏览器可用(遵循 browser-guide) +3. 初始化 ir-record 数据库(幂等):`./skills/ir-record/scripts/init-db.sh` + +### Step 2: 逐渠道搜索 + +对 HEARTBEAT.md 中配置的每个渠道,按顺序执行: + +1. 使用 smart-search 技能构造该渠道的搜索 URL +2. 导航到搜索结果页,等待加载 +3. 收集搜索结果中的投资人/机构信息(最多取配置的最大探索量) + - 提取:姓名、机构、职位、关注领域、来源 URL + +### Step 3: 逐投资人判定 + +对每个搜索到的投资人/机构,按顺序执行: + +1. **去重检查**: + ```bash + ./skills/ir-record/scripts/check-investor.sh --name <姓名> --firm <机构名> + ``` + 如果 `{"exists": true}`,则跳过,继续下一个 + +2. **获取更多信息**(如搜索结果信息不足): + - 导航到投资人/机构详情页(投资数据库 profile、LinkedIn 等) + - 读取投资偏好、已投项目、管理规模等信息 + +3. **匹配度判定**: + - 按 HEARTBEAT.md 中预设的判定标准,判断是否为潜在投资人: + - 投资阶段是否匹配(种子/天使/A轮/...) + - 关注领域是否与公司业务相关 + - 管理规模是否在目标范围 + - 已投案例是否有同类/相邻赛道 + - 判定为潜在投资人需给出明确理由和匹配度评分(high/medium/low) + +4. **记录到数据库**(不管是否符合标准): + ```bash + ./skills/ir-record/scripts/record-investor.sh \ + --name <姓名> --type --firm <机构名> \ + --title <职位> --email <邮箱> --source <来源> \ + --focus-areas <关注领域> --match-score \ + --status new --notes <判定理由> + ``` + +5. **操作间隔**:每个投资人之间保持 30-60 秒间隔,避免平台风控 + +### Step 4: 汇总报告 + +1. 统计本批次结果:探索总数、符合数、跳过数(已记录) + +2. 列出所有符合标准的潜在投资人: + - 姓名、机构、职位、匹配度、关注领域、判定理由、来源 + +3. 按 HEARTBEAT.md 中配置的反馈形式执行: + - **列表报告**:仅汇总报告,不触达 + - **Email 联系**:对 high 匹配度的投资人,使用 investor-outreach 生成话术,经用户确认后用 email-ops 发送 + - **社交平台触达**:对 high 匹配度的投资人,通过社交平台私信触达(需用户确认) + +4. 使用 message 工具将汇总报告发送给用户 + +--- + +## 错误处理 + +| 情况 | 处理 | +|------|------| +| 渠道搜索结果为空 | 记录渠道名称,跳过,继续下一个 | +| 投资人详情页无法访问 | 记录"无法访问"后跳过,不阻塞流程 | +| 浏览器异常 | 等待 30 秒后继续;若仍不行,等 30 秒再试;关闭重开后仍报错则停止并反馈用户 | +| 平台风控/验证码 | 停止当前渠道操作,记录并继续下一个 | +| 持续错误 | spawn IT Engineer 协助排查,当前任务标记为部分完成 | diff --git a/addons/officials/crew/ir/skills/investor-materials/SKILL.md b/addons/officials/crew/ir/skills/investor-materials/SKILL.md index a171a203..7b0da4fd 100644 --- a/addons/officials/crew/ir/skills/investor-materials/SKILL.md +++ b/addons/officials/crew/ir/skills/investor-materials/SKILL.md @@ -116,6 +116,7 @@ metadata: | 场景 | 配合技能 | |------|---------| +| 商业模式复盘后更新材料 | `council`(复盘结论驱动材料迭代) | | 投资人触达需要发送材料 | `email-ops` 发邮件,`investor-outreach` 写话术 | | 搜索竞品/市场数据支撑材料 | `market-research` | | 记录材料发送状态 | `ir-record` 更新投资人状态为 `bp_sent` | diff --git a/addons/officials/crew/ir/skills/investor-outreach/SKILL.md b/addons/officials/crew/ir/skills/investor-outreach/SKILL.md index 2709db17..38c63cbb 100644 --- a/addons/officials/crew/ir/skills/investor-outreach/SKILL.md +++ b/addons/officials/crew/ir/skills/investor-outreach/SKILL.md @@ -108,6 +108,7 @@ metadata: | 场景 | 配合技能 | |------|---------| | 搜索投资人信息和被投企业 | `market-research`,`smart-search` | +| 投资人发掘后触达 | `investor-hunting`(搜索筛选后衔接触达) | | 发送邮件 | `email-ops` | | 社交媒体私信 | `xhs-interact` | | 记录接触历史 | `ir-record`(用 `record-contact.sh` 记录,用 `update-status.sh` 更新状态为 `contacted`) | diff --git a/addons/officials/crew/ir/skills/ir-record/SKILL.md b/addons/officials/crew/ir/skills/ir-record/SKILL.md index ba211368..06cc6732 100644 --- a/addons/officials/crew/ir/skills/ir-record/SKILL.md +++ b/addons/officials/crew/ir/skills/ir-record/SKILL.md @@ -1,11 +1,11 @@ --- name: ir-record -description: 维护 IR(投资人关系专员)的 SQLite 追踪数据库,记录投资人档案和接触历史,避免重复接触,跟踪进展状态。 +description: 维护 IR 的 SQLite 追踪数据库,记录投资人档案、接触历史和项目申报,避免重复,跟踪进展。 --- # IR Record 技能 -在 `./db/ir_record.db` 中维护持久化 SQLite 数据库,供 IR 的三种工作模式使用。 +在 `./db/ir_record.db` 中维护持久化 SQLite 数据库,供 IR 三大工作块使用。 ## 数据库位置 @@ -13,7 +13,7 @@ description: 维护 IR(投资人关系专员)的 SQLite 追踪数据库, ./db/ir_record.db ``` -首次使用需先初始化:`bash ./skills/ir-record/scripts/init-db.sh` +初始化(幂等,可重复执行):`./skills/ir-record/scripts/init-db.sh` --- @@ -26,19 +26,19 @@ description: 维护 IR(投资人关系专员)的 SQLite 追踪数据库, | id | INTEGER PRIMARY KEY AUTOINCREMENT | 自增主键 | | name | TEXT NOT NULL | 投资人姓名 | | type | TEXT NOT NULL | 投资人类别(angel/vc/pe/cvc/fo/other) | -| firm | TEXT | 所属机构(如红杉、高瓴) | -| title | TEXT | 职位(如合伙人、VP) | +| firm | TEXT | 所属机构 | +| title | TEXT | 职位 | | email | TEXT | 邮箱 | | phone | TEXT | 电话 | | wechat | TEXT | 微信号 | | linkedin | TEXT | LinkedIn URL | -| source | TEXT | 来源(如何找到的) | +| source | TEXT | 来源 | | focus_areas | TEXT | 关注领域(逗号分隔) | | match_score | TEXT | 匹配度(high/medium/low) | -| status | TEXT NOT NULL DEFAULT 'new' | 进展状态(new/contacted/bp_sent/meeting/dd/ts/invested/passed) | +| status | TEXT NOT NULL DEFAULT 'new' | 进展状态 | | notes | TEXT | 备注 | -| created_at | TEXT DEFAULT (strftime('%Y-%m-%d %H:%M:%S','now','localtime')) | 记录创建时间 | -| updated_at | TEXT DEFAULT (strftime('%Y-%m-%d %H:%M:%S','now','localtime')) | 最后更新时间 | +| created_at | TEXT | 记录创建时间 | +| updated_at | TEXT | 最后更新时间 | **状态机**: ``` @@ -46,14 +46,6 @@ new → contacted → bp_sent → meeting → dd → ts → invested ↓ ↓ ↓ ↓ ↓ ↓ passed passed passed passed passed passed ``` -- `new`:新发现,尚未接触 -- `contacted`:已发出首次接触(邮件/私信/引荐请求) -- `bp_sent`:已发送 BP/材料 -- `meeting`:已安排或完成会议 -- `dd`:尽调中 -- `ts`:Term Sheet 谈判中 -- `invested`:已完成投资 -- `passed`:放弃/被拒 ### contacts(接触记录) @@ -62,12 +54,43 @@ passed passed passed passed passed passed | id | INTEGER PRIMARY KEY AUTOINCREMENT | 自增主键 | | investor_id | INTEGER NOT NULL | 关联 investors.id | | contact_type | TEXT NOT NULL | 接触方式(email/phone/meeting/intro/pitch/other) | -| direction | TEXT NOT NULL | 方向(outbound=我方主动, inbound=对方主动) | +| direction | TEXT NOT NULL | 方向(outbound/inbound) | | summary | TEXT NOT NULL | 接触内容摘要 | | result | TEXT | 结果/对方反馈 | | next_step | TEXT | 下一步行动 | | contact_date | TEXT NOT NULL | 接触日期(YYYY-MM-DD) | -| created_at | TEXT DEFAULT (strftime('%Y-%m-%d %H:%M:%S','now','localtime')) | 记录时间 | +| created_at | TEXT | 记录时间 | + +### applications(项目申报) + +| 字段 | 类型 | 说明 | +|------|------|------| +| id | INTEGER PRIMARY KEY AUTOINCREMENT | 自增主键 | +| name | TEXT NOT NULL | 申报项目名称 | +| type | TEXT NOT NULL | 申报类别(competition/grant/subsidy/incubator/other) | +| organizer | TEXT | 主办方/组织方 | +| platform_url | TEXT | 申报平台 URL | +| deadline | TEXT | 截止日期(YYYY-MM-DD) | +| status | TEXT NOT NULL DEFAULT 'planning' | 申报状态 | +| submitted_date | TEXT | 实际提交日期 | +| result | TEXT | 结果/获奖情况 | +| notes | TEXT | 备注 | +| created_at | TEXT | 记录创建时间 | +| updated_at | TEXT | 最后更新时间 | + +**申报状态**: +``` +planning → drafting → submitted → shortlisted → awarded + ↓ ↓ ↓ + passed rejected rejected +``` +- `planning`:计划申报 +- `drafting`:材料准备中 +- `submitted`:已提交 +- `shortlisted`:入围/初筛通过 +- `awarded`:获奖/获批 +- `rejected`:未通过 +- `passed`:放弃申报 --- @@ -78,93 +101,101 @@ passed passed passed passed passed passed ### 初始化数据库 ```bash -bash ./skills/ir-record/scripts/init-db.sh +./skills/ir-record/scripts/init-db.sh ``` ### 投资人档案管理 **检查投资人是否已记录**(按姓名+机构去重): ```bash -bash ./skills/ir-record/scripts/check-investor.sh --name <姓名> --firm <机构名> +./skills/ir-record/scripts/check-investor.sh --name <姓名> --firm <机构名> ``` 返回 JSON:`{"exists": true/false, "id": <记录ID或null>}` **记录新投资人**: ```bash -bash ./skills/ir-record/scripts/record-investor.sh \ - --name <姓名> \ - --type \ - --firm <机构名> \ - --title <职位> \ - --email <邮箱> \ - --phone <电话> \ - --wechat <微信号> \ - --linkedin \ - --source <来源> \ - --focus-areas <关注领域> \ - --match-score \ - --status \ - --notes <备注> +./skills/ir-record/scripts/record-investor.sh \ + --name <姓名> --type --firm <机构名> \ + [--title <职位>] [--email <邮箱>] [--phone <电话>] [--wechat <微信号>] \ + [--linkedin ] [--source <来源>] [--focus-areas <关注领域>] \ + [--match-score ] [--status ] [--notes <备注>] ``` 必填:`--name`、`--type`、`--firm`。 -返回 JSON:`{"ok": true, "id": <记录ID>}` 或 `{"ok": false, "error": "..."}` **更新投资人状态**: ```bash -bash ./skills/ir-record/scripts/update-status.sh \ - --id <投资人ID> \ - --status <新状态> \ - --notes <备注(可选)> +./skills/ir-record/scripts/update-status.sh --id <投资人ID> --status <新状态> [--notes <备注>] ``` -返回 JSON:`{"ok": true}` 或 `{"ok": false, "error": "..."}` ### 接触记录管理 -**检查近期接触**(同一投资人,过去 N 天内): +**检查近期接触**: ```bash -bash ./skills/ir-record/scripts/check-contact.sh --investor-id --days <天数> +./skills/ir-record/scripts/check-contact.sh --investor-id --days <天数> ``` -返回 JSON:`{"has_recent": true/false, "last_contact_date": "<日期或null>"}` **记录接触**: ```bash -bash ./skills/ir-record/scripts/record-contact.sh \ - --investor-id <投资人ID> \ - --contact-type \ - --direction \ - --summary <接触内容摘要> \ - --result <结果> \ - --next-step <下一步行动> \ - --contact-date +./skills/ir-record/scripts/record-contact.sh \ + --investor-id <投资人ID> --contact-type \ + --direction --summary <接触内容摘要> \ + --contact-date [--result <结果>] [--next-step <下一步行动>] ``` 必填:`--investor-id`、`--contact-type`、`--direction`、`--summary`、`--contact-date`。 -返回 JSON:`{"ok": true, "id": <记录ID>}` 或 `{"ok": false, "error": "..."}` ### 进展查询 -**查询投资人 Pipeline 摘要**(按状态分组): +**查询投资人 Pipeline 摘要**: +```bash +./skills/ir-record/scripts/query-progress.sh +``` + +**查询待跟进投资人**: +```bash +./skills/ir-record/scripts/query-stale.sh --days <天数> +``` + +### 项目申报管理 + +**检查申报是否已记录**(按项目名+主办方去重): +```bash +./skills/ir-record/scripts/check-application.sh --name <项目名> [--organizer <主办方>] +``` +返回 JSON:`{"exists": true/false, "id": <记录ID或null>}` + +**记录新申报**: ```bash -bash ./skills/ir-record/scripts/query-progress.sh +./skills/ir-record/scripts/record-application.sh \ + --name <项目名> --type \ + [--organizer <主办方>] [--platform-url <申报平台URL>] [--deadline ] \ + [--status ] \ + [--submitted-date ] [--result <结果>] [--notes <备注>] ``` -返回 JSON 数组,每个投资人的基本信息、当前状态、最近接触日期。 +必填:`--name`、`--type`。 -**查询待跟进投资人**(超过 N 天未跟进): +**查询申报记录**: ```bash -bash ./skills/ir-record/scripts/query-stale.sh --days <天数> +./skills/ir-record/scripts/query-applications.sh [--status <状态>] [--upcoming <天数>] ``` -返回 JSON 数组,列出超过指定天数未跟进的活跃状态投资人。 +- 无参数:返回所有申报,按状态排序 +- `--status`:按状态过滤 +- `--upcoming`:查询未来 N 天内截止的申报 --- ## 使用规则 -1. **模式一(Deal Crafting)**:不直接使用数据库,仅通过用户对话和 MEMORY.md 记录材料版本。 -2. **模式二(Investor Hunting)**: +1. **工作块一(商业模式打磨)**:不直接使用数据库。 +2. **工作块二(项目申报)**: + - 发现申报机会后,先用 `check-application.sh` 判断是否已记录 + - 已在数据库中则跳过,避免重复申报 + - 确认申报后用 `record-application.sh` 记录(status=planning) + - 提交后更新 status 为 submitted + - HEARTBEAT 触发时运行 `query-applications.sh --upcoming 7` 提醒即将截止的申报 +3. **工作块三(投资人发掘与跟进)**: - 搜索到投资人后,先用 `check-investor.sh` 判断是否已记录 - - 已在数据库中则跳过,除非有新的联系方式或信息需要更新 + - 已在数据库中则跳过,除非有新信息需要更新 - 新投资人立即用 `record-investor.sh` 记录(status=new) - 首次接触后,用 `update-status.sh` 更新状态,用 `record-contact.sh` 记录接触 -3. **模式三(Relationship Tracking)**: - - HEARTBEAT 触发时运行 `query-stale.sh --days 7` 检查超期未跟进的 - - 用户告知新进展时,立即更新状态和记录接触 + - HEARTBEAT 触发时运行 `query-stale.sh --days 7` 检查超期未跟进 - 每周运行 `query-progress.sh` 获取全局 Pipeline 视图 diff --git a/addons/officials/crew/ir/skills/ir-record/scripts/check-application.sh b/addons/officials/crew/ir/skills/ir-record/scripts/check-application.sh new file mode 100755 index 00000000..832c68c2 --- /dev/null +++ b/addons/officials/crew/ir/skills/ir-record/scripts/check-application.sh @@ -0,0 +1,45 @@ +#!/usr/bin/env bash +# check-application.sh — Check if an application is already recorded (by name + organizer) +# Usage: check-application.sh --name <项目名> --organizer <主办方> + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +WORKSPACE_DIR="$(cd "$SCRIPT_DIR/../../.." && pwd)" +DB_FILE="$WORKSPACE_DIR/db/ir_record.db" + +NAME="" +ORGANIZER="" + +while [[ $# -gt 0 ]]; do + case "$1" in + --name) NAME="$2"; shift 2 ;; + --organizer) ORGANIZER="$2"; shift 2 ;; + *) echo '{"exists": false, "error": "Unknown argument: '"$1"'"}' ; exit 1 ;; + esac +done + +if [[ -z "$NAME" ]]; then + echo '{"exists": false, "error": "--name is required"}' + exit 1 +fi + +if [[ ! -f "$DB_FILE" ]]; then + echo '{"exists": false, "id": null}' + exit 0 +fi + +NAME_ESC="${NAME//\'/\'\'}" +ORGANIZER_ESC="${ORGANIZER//\'/\'\'}" + +if [[ -n "$ORGANIZER" ]]; then + RESULT=$(sqlite3 "$DB_FILE" "SELECT id FROM applications WHERE name='$NAME_ESC' AND organizer='$ORGANIZER_ESC' LIMIT 1;" 2>/dev/null || echo "") +else + RESULT=$(sqlite3 "$DB_FILE" "SELECT id FROM applications WHERE name='$NAME_ESC' LIMIT 1;" 2>/dev/null || echo "") +fi + +if [[ -n "$RESULT" ]]; then + echo "{\"exists\": true, \"id\": $RESULT}" +else + echo '{"exists": false, "id": null}' +fi diff --git a/addons/officials/crew/ir/skills/ir-record/scripts/init-db.sh b/addons/officials/crew/ir/skills/ir-record/scripts/init-db.sh index 347dce3d..3c8c4b6e 100755 --- a/addons/officials/crew/ir/skills/ir-record/scripts/init-db.sh +++ b/addons/officials/crew/ir/skills/ir-record/scripts/init-db.sh @@ -42,6 +42,21 @@ CREATE TABLE IF NOT EXISTS contacts ( created_at TEXT DEFAULT (strftime('%Y-%m-%d %H:%M:%S','now','localtime')), FOREIGN KEY (investor_id) REFERENCES investors(id) ); + +CREATE TABLE IF NOT EXISTS applications ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL, + type TEXT NOT NULL, + organizer TEXT, + platform_url TEXT, + deadline TEXT, + status TEXT NOT NULL DEFAULT 'planning', + submitted_date TEXT, + result TEXT, + notes TEXT, + created_at TEXT DEFAULT (strftime('%Y-%m-%d %H:%M:%S','now','localtime')), + updated_at TEXT DEFAULT (strftime('%Y-%m-%d %H:%M:%S','now','localtime')) +); SQL echo '{"ok": true, "message": "ir_record.db initialized"}' diff --git a/addons/officials/crew/ir/skills/ir-record/scripts/query-applications.sh b/addons/officials/crew/ir/skills/ir-record/scripts/query-applications.sh new file mode 100755 index 00000000..ca80c9d9 --- /dev/null +++ b/addons/officials/crew/ir/skills/ir-record/scripts/query-applications.sh @@ -0,0 +1,64 @@ +#!/usr/bin/env bash +# query-applications.sh — Query application records +# Usage: query-applications.sh [--status <状态>] [--upcoming <天数>] +# --upcoming: 查询未来 N 天内截止的申报 + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +WORKSPACE_DIR="$(cd "$SCRIPT_DIR/../../.." && pwd)" +DB_FILE="$WORKSPACE_DIR/db/ir_record.db" + +STATUS_FILTER="" +UPCOMING_DAYS="" + +while [[ $# -gt 0 ]]; do + case "$1" in + --status) STATUS_FILTER="$2"; shift 2 ;; + --upcoming) UPCOMING_DAYS="$2"; shift 2 ;; + *) echo '[]' ; exit 1 ;; + esac +done + +if [[ ! -f "$DB_FILE" ]]; then + echo '[]' + exit 0 +fi + +if [[ -n "$UPCOMING_DAYS" ]]; then + UPCOMING_ESC="${UPCOMING_DAYS//\'/\'\'}" + sqlite3 -json "$DB_FILE" <= date('now','localtime') + AND deadline <= date('now','localtime','+${UPCOMING_ESC} days') + AND status NOT IN ('passed','rejected') +ORDER BY deadline ASC; +EOF +elif [[ -n "$STATUS_FILTER" ]]; then + SF_ESC="${STATUS_FILTER//\'/\'\'}" + sqlite3 -json "$DB_FILE" < --type <> [--organizer <>] [--platform-url <>] ... + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +WORKSPACE_DIR="$(cd "$SCRIPT_DIR/../../.." && pwd)" +DB_FILE="$WORKSPACE_DIR/db/ir_record.db" + +NAME="" +TYPE="" +ORGANIZER="" +PLATFORM_URL="" +DEADLINE="" +STATUS="planning" +SUBMITTED_DATE="" +RESULT="" +NOTES="" + +while [[ $# -gt 0 ]]; do + case "$1" in + --name) NAME="$2"; shift 2 ;; + --type) TYPE="$2"; shift 2 ;; + --organizer) ORGANIZER="$2"; shift 2 ;; + --platform-url) PLATFORM_URL="$2"; shift 2 ;; + --deadline) DEADLINE="$2"; shift 2 ;; + --status) STATUS="$2"; shift 2 ;; + --submitted-date) SUBMITTED_DATE="$2"; shift 2 ;; + --result) RESULT="$2"; shift 2 ;; + --notes) NOTES="$2"; shift 2 ;; + *) echo '{"ok": false, "error": "Unknown argument: '"$1"'"}' ; exit 1 ;; + esac +done + +if [[ -z "$NAME" || -z "$TYPE" ]]; then + echo '{"ok": false, "error": "--name and --type are required"}' + exit 1 +fi + +STATUS="${STATUS:-planning}" + +# Ensure DB and tables exist +bash "$SCRIPT_DIR/init-db.sh" > /dev/null + +N_ESC="${NAME//\'/\'\'}" +T_ESC="${TYPE//\'/\'\'}" +O_ESC="${ORGANIZER//\'/\'\'}" +PU_ESC="${PLATFORM_URL//\'/\'\'}" +DL_ESC="${DEADLINE//\'/\'\'}" +ST_ESC="${STATUS//\'/\'\'}" +SD_ESC="${SUBMITTED_DATE//\'/\'\'}" +R_ESC="${RESULT//\'/\'\'}" +NT_ESC="${NOTES//\'/\'\'}" + +NEW_ID=$(sqlite3 "$DB_FILE" <" \ --purpose "线上获客" \ --prompt-source "GitHub" @@ -121,11 +121,11 @@ bash ./skills/customer-db/scripts/cs-update.sh \ ```bash # 第一步:若已有 pending 旧任务,先取消 -bash ./skills/customer-db/scripts/follow-up-cancel-pending.sh \ +./skills/customer-db/scripts/follow-up-cancel-pending.sh \ --peer "<[CustomerDB].peer>" # 第二步:创建新跟进任务 -bash ./skills/customer-db/scripts/follow-up-create.sh \ +./skills/customer-db/scripts/follow-up-create.sh \ --peer "<[CustomerDB].peer>" \ --user-id-external "" \ --follow-up-at "" \ diff --git a/addons/officials/crew/sales-cs/BOOTSTRAP.md b/addons/officials/crew/sales-cs/BOOTSTRAP.md index 097840b9..459cae46 100644 --- a/addons/officials/crew/sales-cs/BOOTSTRAP.md +++ b/addons/officials/crew/sales-cs/BOOTSTRAP.md @@ -1,10 +1,46 @@ -# Bootstrap +# Sales-CS Bootstrap -This is a pre-configured crew workspace. Your role, responsibilities, and behavioral guidelines are fully defined in the following files — please review them at startup: +This one-time bootstrap collects the product/service handbook and operational context before customer service work starts. HRBP should ask these questions on behalf of this crew and write the answers into the crew workspace. -- **SOUL.md** — Role definition, core responsibilities, and autonomy level -- **AGENTS.md** — Workflows and operating procedures -- **MEMORY.md** — Background context and ongoing task state -- **IDENTITY.md** — Name and persona -- **USER.md** — Assumptions about who you are serving -- **TOOLS.md** — Available tools and usage guidelines +## Step 1: Product/Service Handbook + +Collect: + +- **Product name**: full product/service name +- **Core value**: one-line description of what problem it solves for customers +- **Target customers**: typical customer profile +- **Pricing tiers**: each tier's name, price, target audience, and core benefits +- **Purchase method**: how customers buy (payment link, QR code, contact person, etc.) +- **FAQ**: top 5-10 frequently asked questions and their answers + +## Step 2: Operational Links + +Collect: + +- Feedback survey URL +- Invoice request form URL +- Purchase page URL +- Trial/experience application URL (if applicable) + +## Step 3: Escalation Contact + +Collect: + +- Human escalation WeChat ID (for complex issues, complaints, refund requests) +- Any other escalation channels + +## Step 4: Environment Verification + +On first startup, check and report: + +1. Customer database is initialized: `./skills/customer-db/scripts/init-db.sh` (idempotent) +2. Follow-up table is ready: `./skills/customer-db/scripts/follow-up-init.sh` (idempotent) + +## Completion + +After bootstrap is complete: + +1. Update `MEMORY.md` with the full product/service handbook, key links, and escalation contact — replacing all placeholder entries. +2. Update `USER.md` if needed with service-specific notes. +3. Delete `BOOTSTRAP.md` from the runtime workspace. +4. Suggest the next step, such as testing a sample customer conversation. diff --git a/addons/officials/crew/sales-cs/DENIED_SKILLS b/addons/officials/crew/sales-cs/DENIED_SKILLS deleted file mode 100644 index ff1baf4d..00000000 --- a/addons/officials/crew/sales-cs/DENIED_SKILLS +++ /dev/null @@ -1,19 +0,0 @@ -# IT-only bundled skills -github -gh-issues -coding-agent -# 复杂任务技能(客服场景不适用) -complex-task -# 商务拓展专属技能(business-developer 使用) -connections-optimizer -email-ops -pitch-deck -ppt-maker -social-graph-ranker -# 生图/生视频技能(客服场景不需要) -siliconflow-img-gen -siliconflow-video-gen -rss-reader -xhs-interact -pexels-footage -pixabay-footage \ No newline at end of file diff --git a/addons/officials/crew/sales-cs/HEARTBEAT.md b/addons/officials/crew/sales-cs/HEARTBEAT.md index 72fc3eed..093a5efb 100644 --- a/addons/officials/crew/sales-cs/HEARTBEAT.md +++ b/addons/officials/crew/sales-cs/HEARTBEAT.md @@ -9,13 +9,13 @@ 1. 先清理过期任务(超过 48 小时仍为 pending,客户已失联): ```bash -bash ./skills/customer-db/scripts/follow-up-expire.sh +./skills/customer-db/scripts/follow-up-expire.sh ``` 2. 查询当前到期的跟进任务: ```bash -bash ./skills/customer-db/scripts/follow-up-due.sh +./skills/customer-db/scripts/follow-up-due.sh ``` 输出为 tab 分隔表格(含 header),字段:`id / peer / user_id_external / follow_up_at / reason / context_summary / status`。 @@ -32,14 +32,14 @@ bash ./skills/customer-db/scripts/follow-up-due.sh - `status='pending'`(首次发送)→ 标记为 sent_once: ```bash - bash ./skills/customer-db/scripts/follow-up-mark-sent.sh \ + ./skills/customer-db/scripts/follow-up-mark-sent.sh \ --id \ --sent-text "<发送的消息内容>" ``` - `status='sent_once'`(二次发送)→ 标记为 completed: ```bash - bash ./skills/customer-db/scripts/follow-up-complete.sh \ + ./skills/customer-db/scripts/follow-up-complete.sh \ --id \ --sent-text "<发送的消息内容>" ``` diff --git a/addons/officials/crew/sales-cs/MEMORY.md b/addons/officials/crew/sales-cs/MEMORY.md index 022a1701..14a21e8f 100644 --- a/addons/officials/crew/sales-cs/MEMORY.md +++ b/addons/officials/crew/sales-cs/MEMORY.md @@ -2,7 +2,7 @@ ## 产品/服务手册 -> 由 hrbp 在招募时填写。这是销售客服最核心的知识库,所有售前问答优先以此为准。 +> 由hrbp在招募后收集写入。这是销售客服最核心的知识库,所有售前问答优先以此为准。 ### 产品概述 - 产品名称:<产品/服务全称> @@ -28,7 +28,7 @@ ## 关键链接 -> 由 hrbp 配置,填写后客服可在对话中直接引用 +> 由 BOOTSTRAP 首次收集写入(或由 hrbp 配置),填写后客服可在对话中直接引用 - 反馈问卷:<反馈问卷链接> - 开票申请工单:<开票申请工单链接> @@ -41,7 +41,7 @@ ## 常见问题与解决方案 -> 运营中逐步积累,记录高频问题和经过验证的最佳答复。 +> 由hrbp在运营中逐步积累,记录高频问题和经过验证的最佳答复。 diff --git a/addons/officials/crew/sales-cs/SOUL.md b/addons/officials/crew/sales-cs/SOUL.md index 4da67e87..c22afbf8 100644 --- a/addons/officials/crew/sales-cs/SOUL.md +++ b/addons/officials/crew/sales-cs/SOUL.md @@ -64,25 +64,13 @@ 表示客户主要业务应用场景。具体口径与细分差异以客服手册为准。 当前可作为通用示例的方向包括但不限于: -- 线上获客 -- 竞争对手监控 -- 行业情报获取 -- 舆情监控 -- 自建可提供对外服务的智能体 +> 由hrbp在招募后收集写入。 如果用户没明确说,也要通过自然对话逐步引导出来。 ### prompt_source 表示客户是从哪里了解到我们的,例如: -- 朋友推荐 -- 社群 -- GitHub -- 公众号 -- 小红书 -- 知乎 -- 即刻 -- 其他 AI 推荐(如豆包、DeepSeek、qwen) -- …… +> 由hrbp在招募后收集写入。 这是重要的增长信息,若为空,要自然询问或引导补全。 diff --git a/addons/officials/crew/sales-cs/USER.md b/addons/officials/crew/sales-cs/USER.md index 5803584c..f9a84eb5 100644 --- a/addons/officials/crew/sales-cs/USER.md +++ b/addons/officials/crew/sales-cs/USER.md @@ -6,4 +6,3 @@ External customers interacting via bound channel (WeChat). ## Preferences - Language: Match customer's language (default: 中文) - Style: Friendly, concise, sales-oriented -- Autonomy: L1/L2 proceed directly; L3 always confirm with team owner diff --git a/addons/officials/crew/sales-cs/feedback/.gitkeep b/addons/officials/crew/sales-cs/feedback/.gitkeep deleted file mode 100644 index e69de29b..00000000 diff --git a/addons/officials/crew/sales-cs/skills/customer-db/SKILL.md b/addons/officials/crew/sales-cs/skills/customer-db/SKILL.md index 98c27736..631f6cd7 100644 --- a/addons/officials/crew/sales-cs/skills/customer-db/SKILL.md +++ b/addons/officials/crew/sales-cs/skills/customer-db/SKILL.md @@ -85,7 +85,7 @@ Sender (untrusted metadata): 每轮结束前,根据本轮对话进展更新 `purpose` 和/或 `prompt_source`: ```bash -bash ./skills/customer-db/scripts/cs-update.sh \ +./skills/customer-db/scripts/cs-update.sh \ --peer "<[CustomerDB].peer>" \ --purpose "线上获客" \ --prompt-source "GitHub" @@ -111,11 +111,11 @@ bash ./skills/customer-db/scripts/cs-update.sh \ ```bash # 第一步:取消同一客户的旧 pending 任务 -bash ./skills/customer-db/scripts/follow-up-cancel-pending.sh \ +./skills/customer-db/scripts/follow-up-cancel-pending.sh \ --peer "<[CustomerDB].peer>" # 第二步:创建新任务 -bash ./skills/customer-db/scripts/follow-up-create.sh \ +./skills/customer-db/scripts/follow-up-create.sh \ --peer "<[CustomerDB].peer>" \ --user-id-external "" \ --follow-up-at "" \ @@ -130,13 +130,13 @@ bash ./skills/customer-db/scripts/follow-up-create.sh \ 超过 48 小时仍为 `pending` 的任务视为客户失联,自动标记完成: ```bash -bash ./skills/customer-db/scripts/follow-up-expire.sh +./skills/customer-db/scripts/follow-up-expire.sh ``` ### 查询到期任务 ```bash -bash ./skills/customer-db/scripts/follow-up-due.sh +./skills/customer-db/scripts/follow-up-due.sh ``` 输出为 tab 分隔的表格(含 header),字段:`id / peer / user_id_external / follow_up_at / reason / context_summary / status`。 @@ -144,7 +144,7 @@ bash ./skills/customer-db/scripts/follow-up-due.sh ### 标记首次已发送(pending → sent_once) ```bash -bash ./skills/customer-db/scripts/follow-up-mark-sent.sh \ +./skills/customer-db/scripts/follow-up-mark-sent.sh \ --id \ --sent-text "<发送的消息内容>" ``` @@ -152,7 +152,7 @@ bash ./skills/customer-db/scripts/follow-up-mark-sent.sh \ ### 标记完成(sent_once → completed) ```bash -bash ./skills/customer-db/scripts/follow-up-complete.sh \ +./skills/customer-db/scripts/follow-up-complete.sh \ --id \ --sent-text "<发送的消息内容>" ``` diff --git a/addons/officials/crew/sales-cs/skills/customer-db/scripts/cs-update.sh b/addons/officials/crew/sales-cs/skills/customer-db/scripts/cs-update.sh old mode 100644 new mode 100755 diff --git a/addons/officials/crew/sales-cs/skills/customer-db/scripts/follow-up-cancel-pending.sh b/addons/officials/crew/sales-cs/skills/customer-db/scripts/follow-up-cancel-pending.sh old mode 100644 new mode 100755 diff --git a/addons/officials/crew/sales-cs/skills/customer-db/scripts/follow-up-complete.sh b/addons/officials/crew/sales-cs/skills/customer-db/scripts/follow-up-complete.sh old mode 100644 new mode 100755 diff --git a/addons/officials/crew/sales-cs/skills/customer-db/scripts/follow-up-create.sh b/addons/officials/crew/sales-cs/skills/customer-db/scripts/follow-up-create.sh old mode 100644 new mode 100755 diff --git a/addons/officials/crew/sales-cs/skills/customer-db/scripts/follow-up-due.sh b/addons/officials/crew/sales-cs/skills/customer-db/scripts/follow-up-due.sh old mode 100644 new mode 100755 diff --git a/addons/officials/crew/sales-cs/skills/customer-db/scripts/follow-up-expire.sh b/addons/officials/crew/sales-cs/skills/customer-db/scripts/follow-up-expire.sh old mode 100644 new mode 100755 diff --git a/addons/officials/crew/sales-cs/skills/customer-db/scripts/follow-up-mark-sent.sh b/addons/officials/crew/sales-cs/skills/customer-db/scripts/follow-up-mark-sent.sh old mode 100644 new mode 100755 diff --git a/addons/officials/crew/sales-cs/skills/exp_invite/SKILL.md b/addons/officials/crew/sales-cs/skills/exp_invite/SKILL.md index 0ce70bca..c9ef0364 100644 --- a/addons/officials/crew/sales-cs/skills/exp_invite/SKILL.md +++ b/addons/officials/crew/sales-cs/skills/exp_invite/SKILL.md @@ -16,7 +16,7 @@ description: > 此处需要同时传入两个标识符,各自职责不同: ```bash -bash ./skills/exp_invite/scripts/invite.sh \ +./skills/exp_invite/scripts/invite.sh \ --peer "<[CustomerDB].peer>" \ --user-id-external "" ``` diff --git a/addons/officials/crew/sales-cs/skills/exp_invite/scripts/invite.sh b/addons/officials/crew/sales-cs/skills/exp_invite/scripts/invite.sh old mode 100644 new mode 100755 index caf644ca..0c231784 --- a/addons/officials/crew/sales-cs/skills/exp_invite/scripts/invite.sh +++ b/addons/officials/crew/sales-cs/skills/exp_invite/scripts/invite.sh @@ -42,12 +42,12 @@ fi WORKDIR="$(cd "$(dirname "$0")/../../.." && pwd)" cd "$WORKDIR" -bash ./skills/customer-db/scripts/db.sh ensure >/dev/null +./skills/customer-db/scripts/db.sh ensure >/dev/null -existing_status="$(bash ./skills/customer-db/scripts/db.sh sql "SELECT business_status FROM cs_record WHERE peer = '$PEER'" | tail -n +2 | head -n 1 || true)" +existing_status="$(./skills/customer-db/scripts/db.sh sql "SELECT business_status FROM cs_record WHERE peer = '$PEER'" | tail -n +2 | head -n 1 || true)" if [ -z "$existing_status" ]; then - bash ./skills/customer-db/scripts/db.sh sql "INSERT INTO cs_record (peer, business_status, purpose, prompt_source) VALUES ('$PEER', 'free', '', '')" >/dev/null + ./skills/customer-db/scripts/db.sh sql "INSERT INTO cs_record (peer, business_status, purpose, prompt_source) VALUES ('$PEER', 'free', '', '')" >/dev/null existing_status="free" fi @@ -56,5 +56,5 @@ if [ "$existing_status" = "exp_invited" ]; then exit 10 fi -bash ./skills/customer-db/scripts/db.sh sql "UPDATE cs_record SET business_status = 'exp_invited' WHERE peer = '$PEER'" >/dev/null +./skills/customer-db/scripts/db.sh sql "UPDATE cs_record SET business_status = 'exp_invited' WHERE peer = '$PEER'" >/dev/null printf '/invite//%s//%s\n' "$USER_ID_EXTERNAL" "$GROUP_NAME" diff --git a/addons/officials/crew/sales-cs/skills/proactive-send/SKILL.md b/addons/officials/crew/sales-cs/skills/proactive-send/SKILL.md index fdae327b..01e09275 100644 --- a/addons/officials/crew/sales-cs/skills/proactive-send/SKILL.md +++ b/addons/officials/crew/sales-cs/skills/proactive-send/SKILL.md @@ -2,6 +2,9 @@ name: proactive-send description: > 向 awada 客户主动发送消息。在 openclaw 消息处理循环之外直接写入 Redis outbound stream,无需等待客户发起对话。 +metadata: + openclaw: + emoji: 📤 --- # 主动发送(proactive-send) @@ -13,7 +16,7 @@ description: > ## 使用方法 ```bash -bash ./skills/proactive-send/scripts/send.sh \ +./skills/proactive-send/scripts/send.sh \ --user-id-external "" \ --text "<消息内容>" ``` diff --git a/addons/officials/crew/sales-cs/skills/proactive-send/scripts/send.sh b/addons/officials/crew/sales-cs/skills/proactive-send/scripts/send.sh old mode 100644 new mode 100755 index 0fc251f9..a0544712 --- a/addons/officials/crew/sales-cs/skills/proactive-send/scripts/send.sh +++ b/addons/officials/crew/sales-cs/skills/proactive-send/scripts/send.sh @@ -3,7 +3,7 @@ # 主动向 awada 客户发送消息(在 openclaw 消息处理循环之外) # # 用法: -# bash ./skills/proactive-send/scripts/send.sh \ +# ./skills/proactive-send/scripts/send.sh \ # --awada-customer-id "wechat:ch001:wxid_abc123:default" \ # --text "您好,昨天咱们聊过专业版的事,不知道今天方便看看吗?" # diff --git a/addons/officials/crew/selfmedia-operator/AGENTS.md b/addons/officials/crew/selfmedia-operator/AGENTS.md index 51dbbb26..1dafb1a9 100644 --- a/addons/officials/crew/selfmedia-operator/AGENTS.md +++ b/addons/officials/crew/selfmedia-operator/AGENTS.md @@ -1,32 +1,32 @@ -# 自媒体运营 — Workflow +# 自媒体运营 - Workflow ## 素材积累 -素材积累来源包括:用户分享的飞书文档/网页链接、网络搜集、调用 skills 生成。 +素材积累来源包括:用户分享的飞书文档/网页链接、网络搜集、调用 skills 生成。 -**注意**:用户也可能时不时的通过私聊渠道分享一些要点、思路以及注意事项等,这些应该记在长期记忆 **MEMORY.md** 中。 +**注意**:用户也可能时不时的通过私聊渠道分享一些要点、思路以及注意事项等,这些应该记在长期记忆 **MEMORY.md** 中。 -其他素材都应该统一存储在 `campaign_assets/` 中,并维护 `campaign_assets/index.md`, 便于后续复用。 +其他素材都应该统一存储在 `campaign_assets/` 中,并维护 `campaign_assets/index.md`, 便于后续复用。 -index.md 格式为: +index.md 格式为: | Instance ID |内容概要|Type|文件名|来源|prompt|创建日期|更新日期 | |-----------|-----------|-----------|-----------|-----------|-----------|----------|-----------| | ||||| ||| -- Type 为枚举:笔记|图片|媒体 -- 来源:仅适用于用户分享和网络搜集 -- prompt:仅适用于 skill 生成 +- Type 为枚举:笔记|图片|媒体 +- 来源:仅适用于用户分享和网络搜集 +- prompt:仅适用于 skill 生成 ## 自媒体内容产出策略 ### 核心原则 -每篇文章中都必须自然而合理的包含结合公司产品的内容。撰文前先回顾下 `MEMORY.md` +每篇文章或视频内容中都必须自然而合理的包含结合 wiseflow 的内容。生产前先回顾下 `MEMORY.md` -### 文章(图文)内容生产通用约束 +### 文章(图文)内容生产通用约束 -每篇文章在 `output_articles/` 下创建独立文件夹,结构如下: +为每篇文章在 `output_articles/` 下创建独立文件夹作为工作区,结构如下: ``` output_articles/ @@ -43,93 +43,26 @@ output_articles/ **配图要求**: 配图类型优先级: - 1. **素材图**:日常积累的素材图,尤其是用户分享的 + - 1. **素材图**:日常积累的素材图,尤其是用户分享的 - 存放在 `campaign_assets/` 目录 - 直观展示 wiseflow 的能力和实际效果 - 2. **技能生成图片**: - - 优先使用 `siliconflow-img-gen` 生成,`siliconflow-img-gen` 不可用时,尝试 `pexels-footage` 或 `pixabay-footage` 下载免版权图片 + - 2. **技能生成图片**: + - 优先使用 siliconflow-img-gen 生成,siliconflow-img-gen 不可用时,尝试 pexels-footage 或 pixabay-footage 下载免版权图片 -### 视频内容生产通用约束 +## Publish Strategy(发布执行策略) -每个视频在 `output_video/` 下创建独立文件夹作为该视频工作区,结构如下: +### 文末统一宣传 hook -``` -output_video/ -└── / # 文章英文题目作为文件夹名 - ├── references/ # 参考资料文件夹 - ├── fragments/ # 视频片段(中间过程)存储文件夹 - ├── script.md # 视频脚本(必须) - ├── vedio.mp4 # 视频文件 - ├── cover.jpg # 封面图(必须) - └── ... -``` - -**通用视频生产流程**: -- 充分构思故事线; -- 生成视频脚本,脚本必须以 5s 为单位拆分为片段列表; -- 使用 `t2video` 技能完成视频制作(TTS 语音合成 + AI 视频片段生成 + 组装),也可用 `pexels-footage` 或 `pixabay-footage` 下载免版权视频片段后用 `t2video compose` 组装; -- 制作视频封面(必须)。 - -注意:每个视频都必须配封面图,封面图必须采用“图+文“的模式,不能仅有背景图片,文字可以是一句吸引人的文案。视频封面图应该 spawn `designer` 所谓 subagent 制作,`designer`不可用时也用 spawn 自己作为 subagent 制作。 - -## Publish Strategy(发布执行策略) - -### 统一宣传 hook - -所有对外发布文章(视频则为简介区),必须按如下平台策略在文末添加统一宣传hook: - -#### 宽松平台策略(微信公众号 / 掘金 / 企业微信朋友圈 / twitter) - -<待替换> -```markdown ---- -假如你现在也有搞副业、创业的想法,欢迎尝试使用 wiseflow 打造 7*24 在线搞钱的AI数字员工团队。 - -**项目地址**: -- [GitHub 主站](https://github.com/TeamWiseFlow/wiseflow) -- [国内镜像 - 开放原子基金会平台](https://atomgit.com/wiseflow/wiseflow) - -也可扫码联系"掌柜的": - -![扫码联系掌柜](campaign_assets/contact.png) - -*扫码联系 wiseflow 掌柜,了解更多 AI 数字员工方案* -``` - -#### 次宽松平台策略(知乎 / 今日头条) - -<待替换> -```markdown ---- -假如你现在也有搞副业、创业的想法,欢迎尝试使用 wiseflow 打造 7*24 在线搞钱的AI数字员工团队。 - -**项目地址**: -- [GitHub 主站](https://github.com/TeamWiseFlow/wiseflow) -- [国内镜像 - 开放原子基金会平台](https://atomgit.com/wiseflow/wiseflow) -``` - -#### 严格平台策略(小红书) - -<待替换> -```markdown -假如你现在也有搞副业、创业的想法,欢迎关注我并私信,大家一起探讨呀~ - -也欢迎尝试使用 wiseflow 打造 7*24 在线搞钱的AI数字员工团队。 - -🔍 github 搜索: wiseflow -(国内用户可以去 atomgit 上搜索) -``` - -## Technical Issue Dispatch Protocol - -**当任务执行过程中遭遇技术问题或系统故障(exec 失败、配置异常、spawn 报错、脚本异常等),必须严格按以下步骤处理:** +所有对外发布文章(视频则为简介区),必须按如下平台策略在文末添加统一宣传hook: -1. **立即告知用户**:主动说明遇到了技术问题,正在呼唤 IT Engineer 处理,请耐心等待,任务执行时间会稍长 -2. **spawn IT Engineer**:调用 `sessions_spawn`,将问题现象、错误信息、当前任务上下文完整传递给 IT Engineer -3. **等待修复完成**,然后继续执行原任务 + -**绝对禁止**:因技术问题停止工作,或要求用户自行解决系统故障。技术问题由 IT Engineer 负责,你的职责是保证用户任务顺利完成。 +### 发布记录管理 -## sessions_spawn 规范 +**统一使用 `published-track` 技能管理所有发布记录**。 -> ⚠️ **禁止传入 `streamTo` 参数** — `streamTo` 仅支持 `runtime=acp`,在 subagent 模式下会报错(`streamTo is only supported for runtime=acp`)。spawn 时只传 agentId 和 task 内容即可。 +- 数据库位置:`./db/published_track.db`(初始化:`./skills/published-track/scripts/init-db.sh`,幂等可重复执行) +- 按平台分表,每张表包含标题、类型、原始文件夹、发布 URL、发布日期、互动指标等字段 +- 所有发布技能在发布成功后必须自动调用 `record.sh` 记录 +- 数据更新通过 `update-metrics.sh` 完成(心跳巡检时使用) +- 查询通过 `query.sh` 和 `check-published.sh` 完成 \ No newline at end of file diff --git a/addons/officials/crew/selfmedia-operator/ALLOWED_COMMANDS b/addons/officials/crew/selfmedia-operator/ALLOWED_COMMANDS index 95bb2794..67d59de9 100644 --- a/addons/officials/crew/selfmedia-operator/ALLOWED_COMMANDS +++ b/addons/officials/crew/selfmedia-operator/ALLOWED_COMMANDS @@ -1,10 +1,3 @@ -# 技能脚本执行 -python3 ./skills/t2video/scripts/t2video.py -python3 -node -npx -ffmpeg -# 文本处理 -sed -cut -base64 +# T2 基线已含 python3/node/npx/ffmpeg/sed/curl,无需重复声明 ++cut ++base64 diff --git a/addons/officials/crew/selfmedia-operator/BUILTIN_SKILLS b/addons/officials/crew/selfmedia-operator/BUILTIN_SKILLS index a054ff09..a198c3e6 100644 --- a/addons/officials/crew/selfmedia-operator/BUILTIN_SKILLS +++ b/addons/officials/crew/selfmedia-operator/BUILTIN_SKILLS @@ -1,15 +1,19 @@ +wx-mp-publisher twitter-post -tiktok-post -instagram-post -youtube-upload +douyin-publish +youtube-publish +tiktok-publish +facebook-publish +instagram-publish +threads-publish +pinterest-publish +kuaishou-publish +bilibili-publish +wxwork-moments xhs-content-ops xhs-interact -xhs-publisher juejin-publish toutiao-publish -siliconflow-img-gen -t2video pexels-footage pixabay-footage -smart-search -browser-guide \ No newline at end of file +highlight-clipper diff --git a/addons/officials/crew/selfmedia-operator/DENIED_SKILLS b/addons/officials/crew/selfmedia-operator/DENIED_SKILLS index 75c6bf43..0e3c83aa 100644 --- a/addons/officials/crew/selfmedia-operator/DENIED_SKILLS +++ b/addons/officials/crew/selfmedia-operator/DENIED_SKILLS @@ -6,5 +6,6 @@ connections-optimizer email-ops pitch-deck social-graph-ranker -ppt-maker -rss-reader \ No newline at end of file +# 闲鱼技能(media-operator 不需要) +xianyu-ops +council diff --git a/addons/officials/crew/selfmedia-operator/HEARTBEAT.md b/addons/officials/crew/selfmedia-operator/HEARTBEAT.md index 55262a41..ccd7633a 100644 --- a/addons/officials/crew/selfmedia-operator/HEARTBEAT.md +++ b/addons/officials/crew/selfmedia-operator/HEARTBEAT.md @@ -1,31 +1,19 @@ -# 心跳检查清单 +# 心跳任务 ## 执行约束 -1. **无时间限制**:任务执行不受深夜时间限制,必须执行完 HEARTBEAT 清单全部内容 +1. **无时间限制**:HEARTBEAT/cron 触发后必须执行完清单全部内容 +2. **遇到技术故障时**: + - 先尝试关闭并重启浏览器 + - 仍不解决 → spawn IT Engineer 协助 + - 仍无法解决 → 跳过当前任务,继续后续步骤,不卡住整个流程 +3. **不可呼唤用户协助**(定时任务可能深夜执行) +4. **浏览器操作必须串行**,不可并行,避免竞态抢夺 -2. **遇到技术故障时处理方案**: - -遇到技术故障时,按如下方案处置: - - 先尝试彻底关闭浏览器,再打开; - - 重启浏览器不解决问题时,**spawn IT Engineer**协助解决:调用 `sessions_spawn`,将问题现象、错误信息、当前任务上下文完整传递给 IT Engineer,请它协助解决; - - 仍无法解决 → **跳过当前任务,继续执行后续步骤**,不要卡住整个 HEARTBEAT - -期间不可: - - ❌ 呼唤用户协助解决,HEARTBEAT 在深夜执行,喊用户也没用 - - ❌ 不可中断任务,通过以上三步依然无法进行的任务则跳过,继续执行后续步骤,绝对不允许中断HEARTBEAT! - -3. **Subagent 产出校验与重试**:每次 spawn subagent 后,收到其返回结果时,必须按以下规则判定: - - - ❌ **无效产出**:返回内容为空、仅包含错误信息、不包含任何实质性产出(如文章正文、发布确认等)、或明确表示任务未完成 - - ✅ **有效产出**:包含预期的实质内容(如完整的文章 markdown、发布成功的确认信息等) - - 若判定为无效产出,**立即重新 spawn 同一个 subagent**(使用相同的 agentId 和 task),最多重试 3 次。每次重试前检查是否因浏览器问题导致(如超时、连接断开),若是则先彻底关闭浏览器再重试。 - - 3次均失败后,**跳过当前步骤,继续后续流程**,返回最终报告「XX 步骤:重试 3 次均失败,原因:xxx」。 - -4. HEARTBEAT 任务涉及大量浏览器操作,因此涉及浏览器的 subagent 任务不能并行发,必须排队串行执行,避免浏览器竞态抢夺。 +--- ## 当前无定时任务 -当前:回复 `HEARTBEAT_OK` \ No newline at end of file +如有任务需求,向用户了解清楚后,参照 `HEARTBEAT_TEMPLATE.md` 的格式写入对应工作模式配置。 + +当前:回复 `HEARTBEAT_OK` diff --git a/addons/officials/crew/selfmedia-operator/MEMORY.md b/addons/officials/crew/selfmedia-operator/MEMORY.md index 88007391..11b8192c 100644 --- a/addons/officials/crew/selfmedia-operator/MEMORY.md +++ b/addons/officials/crew/selfmedia-operator/MEMORY.md @@ -1,8 +1,9 @@ # 自媒体运营 — Memory +## 平台策略与品牌上下文 + + + ## Notes - -### Style Guidelines -(用户确认过的风格偏好、常用 hashtag、禁忌话题) diff --git a/addons/officials/crew/selfmedia-operator/SOUL.md b/addons/officials/crew/selfmedia-operator/SOUL.md index 1312287b..fb4f2a4d 100644 --- a/addons/officials/crew/selfmedia-operator/SOUL.md +++ b/addons/officials/crew/selfmedia-operator/SOUL.md @@ -7,6 +7,8 @@ ## 公司与业务背景信息 + + ## Core Responsibilities ### 素材管理 diff --git a/addons/officials/crew/selfmedia-operator/openclaw_setting_sample.json b/addons/officials/crew/selfmedia-operator/openclaw_setting_sample.json index 5b528871..211a30ae 100644 --- a/addons/officials/crew/selfmedia-operator/openclaw_setting_sample.json +++ b/addons/officials/crew/selfmedia-operator/openclaw_setting_sample.json @@ -6,28 +6,21 @@ "youtube-upload", "xhs-content-ops", "xhs-interact", - "xhs-publisher", "juejin-publish", "toutiao-publish", "siliconflow-img-gen", - "t2video", "pexels-footage", "pixabay-footage", "smart-search", - "browser-guide" + "browser-guide", + "wxwork-moments", + "wxwork-drive", + "wx-mp-publisher", + "complex-task" ], "subagents": { - "allowAgents": ["it-engineer", "designer"] - }, - "heartbeat": { - "every": "3h", - "target": "last", - "isolatedSession": true, - "activeHours": { - "start": "08:00", - "end": "24:00", - "timezone": "user" - } + "allowAgents": ["it-engineer", "designer", "video-producer"] }, + "maxConcurrent": 2, "tools": {} } diff --git a/addons/officials/crew/selfmedia-operator/skills/douyin-publish/SKILL.md b/addons/officials/crew/selfmedia-operator/skills/douyin-publish/SKILL.md new file mode 100644 index 00000000..673b37e6 --- /dev/null +++ b/addons/officials/crew/selfmedia-operator/skills/douyin-publish/SKILL.md @@ -0,0 +1,106 @@ +--- +name: douyin-publish +description: Publish videos to Douyin (抖音) via open platform API with OAuth2 authentication. + Supports video upload, cover image, hashtags, and privacy settings. Requires Douyin + open platform OAuth2 credentials. +metadata: + openclaw: + emoji: 🎤 + requires: + bins: + - python3 +--- + +# 抖音视频发布(douyin-publish) + +通过抖音开放平台 API 发布视频,支持视频上传、封面、话题标签。使用 OAuth2 认证。 + +--- + +## 前置条件 + +1. 抖音开放平台创建应用,获取 client_key / client_secret +2. 申请「视频管理」权限范围 +3. 首次运行需浏览器授权,后续自动使用 refresh token + +--- + +## 配置 + +保存到 `~/.openclaw/credentials/douyin_config.json`: + +```json +{ + "client_key": "your_client_key", + "client_secret": "your_client_secret", + "redirect_uri": "https://localhost:8080/callback" +} +``` + +首次授权后 token 保存到 `~/.openclaw/credentials/douyin_token.json` + +--- + +## 使用方式 + +```bash +python3 ./skills/douyin-publish/scripts/publish_douyin.py \ + --title "视频标题" \ + --video video.mp4 \ + --cover cover.jpg \ + --tags 话题1,话题2 +``` + +--- + +## 参数说明 + +| 参数 | 必填 | 说明 | +|------|------|------| +| `--title` | 是 | 视频标题,最多 55 字 | +| `--video` | 是 | 视频文件路径,支持 mp4,建议 9:16 | +| `--cover` | 否 | 封面图 URL 或本地路径 | +| `--tags` | 否 | 逗号分隔的话题标签 | +| `--private` | 否 | 设为仅自己可见 | + +--- + +## Agent 工作流 + +1. 检查抖音配置和 token +2. 准备视频 + 标题 +3. 运行 `publish_douyin.py` 脚本 +4. 检查 stdout JSON 输出: + - `{"ok": true, "item_id": "xxx", "url": "https://www.douyin.com/video/xxx"}` → 成功 + - `{"ok": false, "error": "AUTH_REQUIRED"}` → 需要完成 OAuth2 授权 + - `{"ok": false, "error": "..."}` → 其他错误 + +--- + +## 错误处理 + +| 错误 | 原因 | 处理 | +|------|------|------| +| AUTH_REQUIRED | 无有效 OAuth2 token | 提示用户完成授权 | +| UPLOAD_FAILED | 上传失败 | 检查文件格式,重试一次 | +| PUBLISH_FAILED | 发布失败 | 检查权限和内容合规性 | +| QUOTA_EXCEEDED | API 调用频率限制 | 等待后重试 | + +## 发布记录(强制) + +发布成功后,**必须**立即调用 `published-track` 技能记录发布信息: + +```bash +./skills/published-track/scripts/record.sh \ + --platform douyin \ + --title "标题" \ + --content-type video \ + --source-folder "<原始文件夹路径>" \ + --publish-url "<发布URL>" \ + --publish-date "$(date +%Y-%m-%d)" +``` + +`--source-folder` 为原始内容所在的相对路径(如 `output_articles/xxx` 或 `output_videos/xxx`)。 +`--publish-url` 为发布后获得的 URL,若发布失败则留空并在 `--notes` 中注明原因。 + +执行 `./skills/published-track/scripts/init-db.sh`(幂等,重复执行无副作用)。 diff --git a/addons/officials/crew/selfmedia-operator/skills/douyin-publish/scripts/publish_douyin.py b/addons/officials/crew/selfmedia-operator/skills/douyin-publish/scripts/publish_douyin.py new file mode 100755 index 00000000..4ec96953 --- /dev/null +++ b/addons/officials/crew/selfmedia-operator/skills/douyin-publish/scripts/publish_douyin.py @@ -0,0 +1,206 @@ +#!/usr/bin/env python3 +"""Publish videos to Douyin via open platform API with OAuth2.""" + +import argparse +import hashlib +import json +import os +import sys +import time +import uuid +from pathlib import Path + +import requests + +CREDS_DIR = Path.home() / ".openclaw" / "credentials" +CONFIG_FILE = CREDS_DIR / "douyin_config.json" +TOKEN_FILE = CREDS_DIR / "douyin_token.json" +DOUYIN_API = "https://open.douyin.com" + + +def output(data: dict) -> None: + sys.stdout.write(json.dumps(data, ensure_ascii=False) + "\n") + + +def err_exit(msg: str, code: int = 1) -> None: + sys.stderr.write(f"[douyin-publish] ERROR: {msg}\n") + output({"ok": False, "error": msg}) + sys.exit(code) + + +def load_config() -> dict: + if not CONFIG_FILE.exists(): + err_exit("AUTH_REQUIRED: no douyin_config.json", 2) + return json.loads(CONFIG_FILE.read_text()) + + +def load_token() -> dict: + if not TOKEN_FILE.exists(): + err_exit("AUTH_REQUIRED", 2) + return json.loads(TOKEN_FILE.read_text()) + + +def refresh_access_token(config: dict, token_data: dict) -> str: + refresh_token = token_data.get("refresh_token", "") + if not refresh_token: + err_exit("AUTH_REQUIRED: no refresh token", 2) + + resp = requests.post( + f"{DOUYIN_API}/oauth/refresh_token/", + data={ + "client_key": config["client_key"], + "client_secret": config["client_secret"], + "grant_type": "refresh_token", + "refresh_token": refresh_token, + }, + headers={"Content-Type": "application/x-www-form-urlencoded"}, + timeout=30, + ) + if resp.status_code != 200: + err_exit(f"AUTH_REQUIRED: refresh failed: {resp.text}", 2) + + data = resp.json() + token_info = data.get("data", {}) + if token_info.get("error_code") != 0: + err_exit(f"AUTH_REQUIRED: {token_info.get('description', data)}", 2) + + new_token = { + "access_token": token_info.get("access_token", ""), + "refresh_token": token_info.get("refresh_token", refresh_token), + "expires_in": token_info.get("expires_in", 0), + "open_id": token_info.get("open_id", token_data.get("open_id", "")), + } + CREDS_DIR.mkdir(parents=True, exist_ok=True) + TOKEN_FILE.write_text(json.dumps(new_token, indent=2)) + return new_token["access_token"] + + +def upload_video(access_token: str, open_id: str, video_path: str) -> str: + url = f"{DOUYIN_API}/api/douyin/v1/video/upload_video/" + file_size = os.path.getsize(video_path) + filename = os.path.basename(video_path) + + with open(video_path, "rb") as f: + resp = requests.post( + url, + headers={"access-token": access_token}, + params={"open_id": open_id}, + files={"video": (filename, f, "video/mp4")}, + timeout=300, + ) + + if resp.status_code in (401, 403): + err_exit("AUTH_REQUIRED", 2) + data = resp.json() + if data.get("data", {}).get("error_code") != 0: + err_exit(f"UPLOAD_FAILED: {data.get('data', {}).get('description', data)}") + + video_id = data.get("data", {}).get("video", {}).get("video_id", "") + if not video_id: + err_exit(f"UPLOAD_FAILED: no video_id: {data}") + return video_id + + +def upload_cover(access_token: str, open_id: str, cover_path: str) -> str: + url = f"{DOUYIN_API}/api/douyin/v1/video/upload_cover/" + with open(cover_path, "rb") as f: + resp = requests.post( + url, + headers={"access-token": access_token}, + params={"open_id": open_id}, + files={"image": ("cover.jpg", f, "image/jpeg")}, + timeout=60, + ) + data = resp.json() + return data.get("data", {}).get("image", {}).get("image_url", "") + + +def create_video( + access_token: str, open_id: str, + video_id: str, title: str, cover_url: str, + tags: list[str], is_private: bool, +) -> dict: + url = f"{DOUYIN_API}/api/douyin/v1/video/create_video/" + text = title + if tags: + text += " " + " ".join(f"#{t}" for t in tags) + + payload = { + "video_id": video_id, + "text": text[:55], + } + if cover_url: + payload["cover_url"] = cover_url + if is_private: + payload["private"] = True + + resp = requests.post( + url, + headers={ + "access-token": access_token, + "Content-Type": "application/json", + }, + params={"open_id": open_id}, + json=payload, + timeout=30, + ) + + if resp.status_code in (401, 403): + err_exit("AUTH_REQUIRED", 2) + data = resp.json() + if data.get("data", {}).get("error_code") != 0: + msg = data.get("data", {}).get("description", str(data)) + if "login" in msg.lower() or "登录" in msg: + err_exit("AUTH_REQUIRED", 2) + err_exit(f"PUBLISH_FAILED: {msg}") + + item_id = data.get("data", {}).get("item_id", "") + return {"ok": True, "item_id": item_id, "url": f"https://www.douyin.com/video/{item_id}"} + + +def main() -> None: + parser = argparse.ArgumentParser(description="Publish video to Douyin") + parser.add_argument("--title", required=True, help="Video title (max 55 chars)") + parser.add_argument("--video", required=True, help="Video file path") + parser.add_argument("--cover", help="Cover image path") + parser.add_argument("--tags", default="", help="Comma-separated tags") + parser.add_argument("--private", action="store_true", help="Set to private") + args = parser.parse_args() + + if not os.path.exists(args.video): + err_exit(f"UPLOAD_FAILED: video not found: {args.video}") + + config = load_config() + token_data = load_token() + + try: + access_token = refresh_access_token(config, token_data) + except SystemExit: + raise + except Exception as e: + err_exit(f"AUTH_REQUIRED: {e}", 2) + + open_id = token_data.get("open_id", "") + if not open_id: + err_exit("AUTH_REQUIRED: no open_id in token", 2) + + sys.stderr.write("[douyin-publish] uploading video...\n") + video_id = upload_video(access_token, open_id, args.video) + + cover_url = "" + if args.cover and os.path.exists(args.cover): + sys.stderr.write("[douyin-publish] uploading cover...\n") + cover_url = upload_cover(access_token, open_id, args.cover) + + tags = [t.strip() for t in args.tags.split(",") if t.strip()] if args.tags else [] + + sys.stderr.write("[douyin-publish] creating video post...\n") + result = create_video( + access_token, open_id, video_id, args.title, cover_url, + tags, args.private, + ) + output(result) + + +if __name__ == "__main__": + main() diff --git a/addons/officials/crew/selfmedia-operator/skills/facebook-publish/SKILL.md b/addons/officials/crew/selfmedia-operator/skills/facebook-publish/SKILL.md new file mode 100644 index 00000000..716e72fc --- /dev/null +++ b/addons/officials/crew/selfmedia-operator/skills/facebook-publish/SKILL.md @@ -0,0 +1,125 @@ +--- +name: facebook-publish +description: Publish posts, videos, and reels to Facebook via Meta Graph API v23.0. + Supports feed posts, video posts, photo posts, reels, and stories. Requires Meta + OAuth2 page access token. +metadata: + openclaw: + emoji: 📘 + requires: + bins: + - python3 +--- + +# Facebook 发布(facebook-publish) + +通过 Meta Graph API v23.0 发布内容到 Facebook,支持帖子、视频、Reels 和 Stories。使用 Meta OAuth2 Page Access Token 认证。 + +--- + +## 前置条件 + +1. Meta Developer Portal 创建应用,获取 App ID / App Secret +2. 申请 `pages_manage_posts` 和 `pages_read_engagement` 权限 +3. 获取长效 Page Access Token + +--- + +## 配置 + +保存到 `~/.openclaw/credentials/facebook_config.json`: + +```json +{ + "app_id": "your_app_id", + "app_secret": "your_app_secret", + "page_access_token": "your_long_lived_page_token", + "page_id": "your_page_id" +} +``` + +--- + +## 使用方式 + +文字帖子: + +```bash +python3 ./skills/facebook-publish/scripts/publish_facebook.py \ + --message "帖子内容" \ + --mode feed +``` + +视频/Reel: + +```bash +python3 ./skills/facebook-publish/scripts/publish_facebook.py \ + --message "描述" \ + --video video.mp4 \ + --mode reel +``` + +图片帖子: + +```bash +python3 ./skills/facebook-publish/scripts/publish_facebook.py \ + --message "描述" \ + --images img1.jpg img2.jpg \ + --mode feed +``` + +--- + +## 参数说明 + +| 参数 | 必填 | 说明 | +|------|------|------| +| `--message` | 是 | 帖子内容 | +| `--mode` | 是 | `feed`/`video`/`reel`/`story` | +| `--video` | 视频模式必填 | 视频文件路径 | +| `--images` | 图片模式必填 | 图片路径列表 | +| `--title` | 否 | 视频标题(video 模式) | + +--- + +## Agent 工作流 + +1. 检查 Facebook 配置是否存在 +2. 准备内容 + 媒体文件 +3. 运行 `publish_facebook.py` 脚本 +4. 检查 stdout JSON 输出: + - `{"ok": true, "post_id": "xxx", "url": "https://facebook.com/xxx"}` → 成功 + - `{"ok": false, "error": "AUTH_REQUIRED"}` → 更新 Page Token + - `{"ok": false, "error": "..."}` → 其他错误 + +--- + +## 错误处理 + +| 错误 | 原因 | 处理 | +|------|------|------| +| AUTH_REQUIRED | Token 失效 | 更新 Page Access Token | +| UPLOAD_FAILED | 上传失败 | 检查文件格式,重试 | +| MEDIA_PROCESSING | 视频处理中 | 轮询状态等待完成 | +| RATE_LIMIT | 频率限制 | 等待后重试 | + +--- + +## 发布记录(强制) + +发布成功后,**必须**立即调用 `published-track` 技能记录发布信息: + +```bash +./skills/published-track/scripts/record.sh \ + --platform facebook \ + --title "标题" \ + --content-type post \ + --source-folder "<原始文件夹路径>" \ + --publish-url "<发布URL>" \ + --publish-date "$(date +%Y-%m-%d)" +``` + +`--source-folder` 为原始内容所在的相对路径(如 `output_articles/xxx` 或 `output_videos/xxx`)。 +`--publish-url` 为发布后获得的 URL,若发布失败则留空并在 `--notes` 中注明原因。 + +执行 `./skills/published-track/scripts/init-db.sh`(幂等,重复执行无副作用)。 diff --git a/addons/officials/crew/selfmedia-operator/skills/facebook-publish/scripts/publish_facebook.py b/addons/officials/crew/selfmedia-operator/skills/facebook-publish/scripts/publish_facebook.py new file mode 100755 index 00000000..a8f2eeb1 --- /dev/null +++ b/addons/officials/crew/selfmedia-operator/skills/facebook-publish/scripts/publish_facebook.py @@ -0,0 +1,168 @@ +#!/usr/bin/env python3 +"""Publish posts, videos, and reels to Facebook via Meta Graph API v23.0.""" + +import argparse +import json +import os +import sys +import time +from pathlib import Path + +import requests + +CREDS_DIR = Path.home() / ".openclaw" / "credentials" +CONFIG_FILE = CREDS_DIR / "facebook_config.json" +GRAPH_API = "https://graph.facebook.com/v23.0" + + +def output(data: dict) -> None: + sys.stdout.write(json.dumps(data, ensure_ascii=False) + "\n") + + +def err_exit(msg: str, code: int = 1) -> None: + sys.stderr.write(f"[facebook-publish] ERROR: {msg}\n") + output({"ok": False, "error": msg}) + sys.exit(code) + + +def load_config() -> dict: + if not CONFIG_FILE.exists(): + err_exit("AUTH_REQUIRED: no facebook_config.json", 2) + return json.loads(CONFIG_FILE.read_text()) + + +def api_get(path: str, token: str, params: dict | None = None) -> dict: + p = {"access_token": token, **(params or {})} + resp = requests.get(f"{GRAPH_API}{path}", params=p, timeout=30) + if resp.status_code in (401, 403): + err_exit("AUTH_REQUIRED", 2) + data = resp.json() + if "error" in data: + err_exit(f"API_ERROR: {data['error'].get('message', data)}") + return data + + +def api_post(path: str, token: str, data: dict | None = None, files: dict | None = None) -> dict: + params = {"access_token": token} + if data and not files: + resp = requests.post(f"{GRAPH_API}{path}", params=params, json=data, timeout=120) + elif files: + resp = requests.post(f"{GRAPH_API}{path}", params=params, files=files, data=data, timeout=300) + else: + resp = requests.post(f"{GRAPH_API}{path}", params=params, timeout=30) + if resp.status_code in (401, 403): + err_exit("AUTH_REQUIRED", 2) + result = resp.json() + if "error" in result: + err_exit(f"API_ERROR: {result['error'].get('message', result)}") + return result + + +def publish_feed_post(page_id: str, token: str, message: str, image_paths: list[str] | None = None) -> dict: + if image_paths: + path = f"/{page_id}/photos" + if len(image_paths) == 1: + with open(image_paths[0], "rb") as f: + result = api_post(path, token, data={"message": message}, files={"source": f}) + post_id = result.get("post_id", result.get("id", "")) + else: + uploaded_ids = [] + for img_path in image_paths: + with open(img_path, "rb") as f: + r = api_post(path, token, data={"published": "false"}, files={"source": f}) + uploaded_ids.append(r["id"]) + result = api_post(f"/{page_id}/feed", token, data={ + "message": message, + "attached_media": json.dumps([{"media_fbid": mid} for mid in uploaded_ids]), + }) + post_id = result.get("id", "") + else: + result = api_post(f"/{page_id}/feed", token, data={"message": message}) + post_id = result.get("id", "") + + return {"ok": True, "post_id": post_id, "url": f"https://facebook.com/{post_id}"} + + +def publish_video(page_id: str, token: str, video_path: str, title: str, description: str) -> dict: + file_size = os.path.getsize(video_path) + start = api_post(f"/{page_id}/videos", token, data={ + "upload_phase": "start", + "file_size": file_size, + }) + upload_session_id = start.get("upload_session_id", "") + video_id = start.get("video_id", "") + + chunk_size = 4 * 1024 * 1024 + with open(video_path, "rb") as f: + offset = 0 + while True: + chunk = f.read(chunk_size) + if not chunk: + break + api_post(f"/{page_id}/videos", token, files={"video_file_chunk": chunk}, data={ + "upload_phase": "transfer", + "upload_session_id": upload_session_id, + "start_offset": str(offset), + }) + offset += len(chunk) + + api_post(f"/{page_id}/videos", token, data={ + "upload_phase": "finish", + "upload_session_id": upload_session_id, + "title": title, + "description": description, + }) + + return {"ok": True, "post_id": video_id, "url": f"https://facebook.com/{video_id}"} + + +def publish_reel(page_id: str, token: str, video_path: str, description: str) -> dict: + init = api_post(f"/{page_id}/video_reels", token, data={"upload_phase": "start"}) + upload_url = init.get("upload_url", "") + video_id = init.get("video_id", "") + + with open(video_path, "rb") as f: + requests.put(upload_url, data=f, timeout=300) + + api_post(f"/{page_id}/video_reels", token, data={ + "upload_phase": "finish", + "video_id": video_id, + "title": description[:255] if description else "", + }) + + return {"ok": True, "post_id": video_id, "url": f"https://facebook.com/reel/{video_id}"} + + +def main() -> None: + parser = argparse.ArgumentParser(description="Publish to Facebook") + parser.add_argument("--message", required=True, help="Post message/caption") + parser.add_argument("--mode", required=True, choices=["feed", "video", "reel", "story"]) + parser.add_argument("--video", help="Video file path") + parser.add_argument("--images", nargs="+", help="Image file paths") + parser.add_argument("--title", default="", help="Video title") + args = parser.parse_args() + + config = load_config() + token = config.get("page_access_token", "") + page_id = config.get("page_id", "") + if not token or not page_id: + err_exit("AUTH_REQUIRED: missing token or page_id", 2) + + if args.mode == "feed": + result = publish_feed_post(page_id, token, args.message, args.images) + elif args.mode == "video": + if not args.video: + err_exit("--video required for video mode") + result = publish_video(page_id, token, args.video, args.title, args.message) + elif args.mode == "reel": + if not args.video: + err_exit("--video required for reel mode") + result = publish_reel(page_id, token, args.video, args.message) + else: + err_exit(f"story mode not yet implemented") + + output(result) + + +if __name__ == "__main__": + main() diff --git a/addons/officials/crew/selfmedia-operator/skills/highlight-clipper/SKILL.md b/addons/officials/crew/selfmedia-operator/skills/highlight-clipper/SKILL.md new file mode 100644 index 00000000..20528391 --- /dev/null +++ b/addons/officials/crew/selfmedia-operator/skills/highlight-clipper/SKILL.md @@ -0,0 +1,135 @@ +--- +name: highlight-clipper +description: 自动从本地视频中提取高光片段。通过 ASR 转录 + 文本分析识别高光时刻,剪辑输出多段短视频。 +metadata: + openclaw: + emoji: "✂️" + requires: + bins: + - python3 + - ffmpeg + - ffprobe + env: + - SILICONFLOW_API_KEY + primaryEnv: SILICONFLOW_API_KEY +--- + +# highlight-clipper — 视频高光剪辑 + +Use this skill when: +- 用户提供一个本地视频文件,希望自动剪辑出高光片段 +- 需要从长视频中提取精彩片段用于二次分发(抖音/小红书/B站短视频等) + +**不适用场景**:纯音乐或无声视频(依赖语音内容识别高光)、画面精彩但无语音的片段。 + +--- + +## 工作流程 + +### Step 1 — 创建输出目录 + +在 `output_videos/` 下创建项目目录: + +```bash +mkdir -p output_videos/ +``` + +### Step 2 — 运行高光剪辑 + +```bash +python3 ./skills/highlight-clipper/scripts/clip.py --out-dir output_videos/ +``` + +参数说明: + +| 参数 | 默认值 | 说明 | +|------|--------|------| +| `` | — | 源视频文件路径(必需) | +| `--out-dir` | — | 输出目录,必须在 `output_videos/` 或 `tmp/` 下(必需) | +| `--count` | 3 | 提取高光片段数量 | +| `--min-duration` | 15 | 最短片段时长(秒) | +| `--max-duration` | 60 | 最长片段时长(秒) | +| `--buffer` | 3 | 片段前后缓冲(秒) | + +示例 — 从一段 5 分钟视频提取 5 个高光: + +```bash +python3 ./skills/highlight-clipper/scripts/clip.py output_videos/my-video/video.mp4 --out-dir output_videos/my-video --count 5 +``` + +### Step 3 — 查看结果 + +脚本产出: + +``` +output_videos// +├── highlight_01.mp4 # 高光片段 1 +├── highlight_02.mp4 # 高光片段 2 +├── highlight_03.mp4 # 高光片段 3 +└── highlights.json # 高光分析报告 +``` + +`highlights.json` 包含完整转录文本、每个片段的时间戳、评分和文案: + +```json +{ + "source_video": "video.mp4", + "video_duration": 180.5, + "highlight_count": 3, + "full_transcript": "...", + "highlights": [ + { + "index": 1, + "file": "highlight_01.mp4", + "start": 12.0, + "end": 45.0, + "duration": 33.0, + "text": "这才是最关键的一步...", + "score": 8.5 + } + ] +} +``` + +查看报告: + +```bash +cat output_videos//highlights.json +``` + +### Step 4 — 后续处理(可选) + +对高光片段做进一步加工: +- 使用 `t2video` 为片段添加配音或封面 +- 使用各平台发布技能(`douyin-publish`、`xhs-publish` 等)发布 + +--- + +## 技术原理 + +1. **音频提取**:ffmpeg 从视频中提取 16kHz 单声道 WAV +2. **ASR 转录**:SiliconFlow SenseVoiceSmall 模型,获取带时间戳的语音片段 +3. **高光评分**:对每个转录片段综合打分,考量: + - 情感强度词("最"、"超"、"非常"等)— 权重 2.0 + - 转折/惊喜词("但是"、"没想到"、"原来"等)— 权重 3.0 + - 行动号召词("赶紧"、"收藏"、"关注"等)— 权重 2.5 + - 疑问句和感叹号 — 权重 1.5 + - 数据/数字出现 — 权重 1.0 + - 信息密度(单位时长文字量)— 权重最高 3.0 +4. **多样性选择**:贪婪选取得分最高的 N 个片段,保证片段间至少间隔 30 秒,避免高光扎堆 +5. **视频剪辑**:ffmpeg 精确裁剪,含前后缓冲秒数 + +--- + +## 长视频处理 + +视频超过 5 分钟时,脚本自动分块转录(每块 5 分钟),合并时间戳后统一分析。无需手动干预。 + +--- + +## 注意事项 + +- 源视频必须有语音内容,纯音乐或无声视频无法识别高光 +- 转录质量取决于语音清晰度,建议使用语音清晰的视频 +- 高光评分基于文本语义分析,非视觉分析——画面精彩但无语音的片段可能被遗漏 +- 片段时长受 `--min-duration` 和 `--max-duration` 控制,可根据目标平台要求调整(如抖音 15–60 秒、小红书 15–45 秒) diff --git a/addons/officials/crew/selfmedia-operator/skills/highlight-clipper/scripts/clip.py b/addons/officials/crew/selfmedia-operator/skills/highlight-clipper/scripts/clip.py new file mode 100644 index 00000000..87b86579 --- /dev/null +++ b/addons/officials/crew/selfmedia-operator/skills/highlight-clipper/scripts/clip.py @@ -0,0 +1,441 @@ +#!/usr/bin/env python3 +"""highlight-clipper — Extract highlight clips from a local video. + +Flow: + 1. Extract audio via ffmpeg (16kHz mono WAV) + 2. Transcribe via SiliconFlow ASR (SenseVoiceSmall with timestamps) + 3. Score transcript segments for highlight potential + 4. Select top-N diverse highlights + 5. Clip each highlight with ffmpeg + +Usage: + python3 ./skills/highlight-clipper/scripts/clip.py --out-dir
[options] +""" + +import argparse +import gc +import json +import mimetypes +import os +import re +import shutil +import subprocess +import sys +import tempfile +import urllib.error +import urllib.request +import uuid +from pathlib import Path + +ASR_URL = "https://api.siliconflow.cn/v1/audio/transcriptions" +DEFAULT_ASR_MODEL = "FunAudioLLM/SenseVoiceSmall" +CHUNK_DURATION = 300 # 5 min per ASR chunk +DEFAULT_COUNT = 3 +DEFAULT_BUFFER = 3.0 +DEFAULT_CLIP_MIN = 15 +DEFAULT_CLIP_MAX = 60 +MIN_HIGHLIGHT_GAP = 30 # min seconds between highlight starts + +SAFE_OUTPUT_DIRS = (Path("output_videos"), Path("tmp")) + +# ── Highlight scoring keywords ────────────────────────────────────────── + +EMPHASIS_WORDS = frozenset({ + "最", "极", "超", "非常", "特别", "真的", "绝对", "必须", "一定", "千万", + "竟然", "居然", "简直", "太", "极其", "无比", "惊人", "震撼", "炸裂", + "逆天", "离谱", "夸张", "恐怖", "神奇", "绝了", "史上", "顶级", "终极", +}) +CONTRAST_WORDS = frozenset({ + "但是", "然而", "可是", "不过", "其实", "没想到", "殊不知", "结果", "原来", +}) +CTA_WORDS = frozenset({ + "赶紧", "快", "别", "不要", "一定要", "记得", "收藏", "关注", "点赞", + "转发", "下单", "链接", +}) +QUESTION_MARKS = frozenset({"?", "?", "吗", "呢", "嘛", "吧", "如何", "怎么", "为什么", "为啥"}) + + +# ── Utilities ─────────────────────────────────────────────────────────── + +def die(msg: str) -> None: + print(f"[error] {msg}", file=sys.stderr) + sys.exit(1) + + +def _tail_file(path: str, max_chars: int) -> str: + """Read the last N characters of a file without loading the whole thing.""" + try: + size = os.path.getsize(path) + if size <= max_chars: + with open(path, "r", errors="replace") as f: + return f.read() + with open(path, "rb") as f: + f.seek(size - max_chars) + f.readline() + return f.read().decode(errors="replace") + except OSError: + return "" + + +def ensure_safe_output_dir(raw_path: str) -> Path: + path = Path(raw_path) + if path.is_absolute(): + die("output path must be relative to the workspace") + if ".." in path.parts: + die("output path must not contain '..'") + resolved = (Path.cwd() / path).resolve() + for base in SAFE_OUTPUT_DIRS: + base_resolved = (Path.cwd() / base).resolve() + if resolved == base_resolved or resolved.is_relative_to(base_resolved): + return resolved + allowed = ", ".join(str(d) for d in SAFE_OUTPUT_DIRS) + die(f"output path must be under one of: {allowed}") + + +# ── Media probing ─────────────────────────────────────────────────────── + +def probe_duration(filepath: str) -> float: + try: + result = subprocess.run( + ["ffprobe", "-v", "quiet", "-print_format", "json", + "-show_format", filepath], + capture_output=True, text=True, timeout=15, + ) + if result.returncode == 0: + data = json.loads(result.stdout) + return float(data.get("format", {}).get("duration", 0)) + except (subprocess.TimeoutExpired, json.JSONDecodeError, ValueError): + pass + return 0.0 + + +# ── Audio extraction ──────────────────────────────────────────────────── + +def extract_audio_chunk(video_path: str, output_path: str, + start: float = 0, duration: float | None = None) -> str: + cmd = ["ffmpeg", "-y"] + if start > 0: + cmd.extend(["-ss", str(start)]) + cmd.extend(["-i", video_path, "-vn", "-ar", "16000", "-ac", "1", "-f", "wav"]) + if duration is not None: + cmd.extend(["-t", str(duration)]) + cmd.append(output_path) + with tempfile.NamedTemporaryFile(mode="w", suffix=".log", delete=False) as stderr_f: + stderr_path = stderr_f.name + try: + with open(stderr_path, "w") as stderr_fh: + result = subprocess.run(cmd, stdout=subprocess.DEVNULL, stderr=stderr_fh, text=True, timeout=120) + if result.returncode != 0: + tail = _tail_file(stderr_path, 500) + die(f"Audio extraction failed: {tail}") + finally: + try: + os.unlink(stderr_path) + except OSError: + pass + if not os.path.exists(output_path): + die(f"Audio file not created: {output_path}") + return output_path + + +# ── ASR ───────────────────────────────────────────────────────────────── + +def build_multipart_formdata(file_path: str, fields: dict[str, str]) -> tuple[bytes, str]: + boundary = f"----HLClipper{uuid.uuid4().hex}" + filename = os.path.basename(file_path) + content_type = mimetypes.guess_type(filename)[0] or "application/octet-stream" + parts: list[bytes] = [] + parts.append( + ( + f"--{boundary}\r\n" + f'Content-Disposition: form-data; name="file"; filename="{filename}"\r\n' + f"Content-Type: {content_type}\r\n\r\n" + ).encode("utf-8") + ) + with open(file_path, "rb") as f: + parts.append(f.read()) + parts.append(b"\r\n") + for name, value in fields.items(): + parts.append( + ( + f"--{boundary}\r\n" + f'Content-Disposition: form-data; name="{name}"\r\n\r\n' + f"{value}\r\n" + ).encode("utf-8") + ) + parts.append(f"--{boundary}--\r\n".encode("utf-8")) + return b"".join(parts), f"multipart/form-data; boundary={boundary}" + + +def call_asr(audio_path: str) -> dict: + api_key = os.environ.get("SILICONFLOW_API_KEY", "").strip() + if not api_key: + die("SILICONFLOW_API_KEY not set") + model = os.environ.get("ASR_MODEL", DEFAULT_ASR_MODEL).strip() or DEFAULT_ASR_MODEL + body, content_type = build_multipart_formdata(audio_path, {"model": model}) + req = urllib.request.Request(ASR_URL, data=body, method="POST") + req.add_header("Authorization", f"Bearer {api_key}") + req.add_header("Content-Type", content_type) + try: + with urllib.request.urlopen(req, timeout=120) as resp: + return json.loads(resp.read().decode()) + except urllib.error.HTTPError as e: + err_body = e.read().decode(errors="replace") + die(f"ASR API failed (HTTP {e.code}): {err_body}") + except urllib.error.URLError as e: + die(f"ASR request failed: {e.reason}") + + +def estimate_segments(text: str, duration: float, offset: float = 0) -> list[dict]: + """Estimate segment timestamps when ASR doesn't return them.""" + sentences = re.split(r"[。!?!?;;\n]", text) + sentences = [s.strip() for s in sentences if s.strip()] + if not sentences: + return [] + total_chars = sum(len(s) for s in sentences) + if total_chars == 0: + return [] + segments = [] + current_time = offset + for s in sentences: + seg_duration = (len(s) / total_chars) * duration + segments.append({ + "start": round(current_time, 2), + "end": round(current_time + seg_duration, 2), + "text": s, + }) + current_time += seg_duration + return segments + + +def transcribe_video(video_path: str, tmp_dir: str, video_duration: float) -> tuple[str, list[dict]]: + """Transcribe video audio, chunking long videos automatically.""" + all_segments: list[dict] = [] + all_text_parts: list[str] = [] + + if video_duration <= CHUNK_DURATION: + audio_path = os.path.join(tmp_dir, "audio.wav") + extract_audio_chunk(video_path, audio_path) + result = call_asr(audio_path) + segments = result.get("segments", []) + text = result.get("text", "") + if segments: + all_segments.extend(segments) + elif text: + all_segments.extend(estimate_segments(text, video_duration)) + all_text_parts.append(text) + else: + offset = 0.0 + chunk_idx = 0 + while offset < video_duration: + chunk_idx += 1 + audio_path = os.path.join(tmp_dir, f"audio_{chunk_idx}.wav") + chunk_dur = min(CHUNK_DURATION, video_duration - offset) + extract_audio_chunk(video_path, audio_path, start=offset, duration=chunk_dur) + result = call_asr(audio_path) + segments = result.get("segments", []) + text = result.get("text", "") + if segments: + for seg in segments: + all_segments.append({ + "start": seg.get("start", 0) + offset, + "end": seg.get("end", 0) + offset, + "text": seg.get("text", ""), + }) + elif text: + all_segments.extend(estimate_segments(text, chunk_dur, offset)) + all_text_parts.append(text) + offset += CHUNK_DURATION + + return " ".join(all_text_parts), all_segments + + +# ── Highlight scoring ─────────────────────────────────────────────────── + +def score_segment(text: str) -> float: + if not text or not text.strip(): + return 0.0 + score = 0.0 + for w in EMPHASIS_WORDS: + if w in text: + score += 2.0 + for w in CONTRAST_WORDS: + if w in text: + score += 3.0 + for w in CTA_WORDS: + if w in text: + score += 2.5 + for m in QUESTION_MARKS: + if m in text: + score += 1.5 + score += min(len(re.findall(r"\d+\.?\d*%?", text)), 3) * 1.0 + score += (text.count("!") + text.count("!")) * 1.5 + score += min(len(text.strip()) / 20, 3.0) + return score + + +def select_highlights(segments: list[dict], count: int) -> list[dict]: + if not segments: + return [] + valid = [s for s in segments if s.get("end", 0) - s.get("start", 0) >= 1.0] + if not valid: + valid = segments + scored = [{**s, "highlight_score": score_segment(s.get("text", ""))} for s in valid] + scored.sort(key=lambda s: s["highlight_score"], reverse=True) + selected = [] + for seg in scored: + if len(selected) >= count: + break + start = seg.get("start", 0) + if not any(abs(start - s.get("start", 0)) < MIN_HIGHLIGHT_GAP for s in selected): + selected.append(seg) + if len(selected) < count: + remaining = [s for s in scored if s not in selected] + for seg in remaining: + if len(selected) >= count: + break + selected.append(seg) + selected.sort(key=lambda s: s.get("start", 0)) + return selected + + +# ── Video clipping ────────────────────────────────────────────────────── + +def determine_clip_bounds(seg: dict, video_duration: float, + buffer: float, clip_min: float, clip_max: float) -> tuple[float, float]: + seg_start = seg.get("start", 0) + seg_end = seg.get("end", 0) + seg_duration = seg_end - seg_start + target = max(seg_duration + buffer * 2, clip_min) + target = min(target, clip_max) + clip_start = max(seg_start - buffer, 0) + clip_end = min(clip_start + target, video_duration) + if clip_end - clip_start < clip_min: + clip_start = max(clip_end - target, 0) + return round(clip_start, 2), round(clip_end, 2) + + +def clip_video(video_path: str, start: float, end: float, output_path: str) -> None: + duration = end - start + cmd = [ + "ffmpeg", "-y", + "-ss", str(start), "-i", video_path, "-t", str(duration), + "-c:v", "libx264", "-preset", "fast", "-crf", "23", + "-c:a", "aac", "-b:a", "128k", + "-movflags", "+faststart", + output_path, + ] + with tempfile.NamedTemporaryFile(mode="w", suffix=".log", delete=False) as stderr_f: + stderr_path = stderr_f.name + try: + with open(stderr_path, "w") as stderr_fh: + result = subprocess.run(cmd, stdout=subprocess.DEVNULL, stderr=stderr_fh, text=True, timeout=120) + if result.returncode != 0: + tail = _tail_file(stderr_path, 500) + die(f"ffmpeg clip failed: {tail}") + finally: + try: + os.unlink(stderr_path) + except OSError: + pass + if not os.path.exists(output_path) or os.path.getsize(output_path) == 0: + die(f"Clip output missing or empty: {output_path}") + gc.collect() + + +# ── Main ──────────────────────────────────────────────────────────────── + +def main() -> None: + parser = argparse.ArgumentParser(description="Extract highlight clips from a local video") + parser.add_argument("video_path", help="Path to the source video file") + parser.add_argument("--out-dir", required=True, dest="out_dir", + help="Output directory under output_videos/ or tmp/") + parser.add_argument("--count", type=int, default=DEFAULT_COUNT, + help=f"Number of highlights (default: {DEFAULT_COUNT})") + parser.add_argument("--min-duration", type=float, default=DEFAULT_CLIP_MIN, dest="min_duration", + help=f"Minimum clip duration seconds (default: {DEFAULT_CLIP_MIN})") + parser.add_argument("--max-duration", type=float, default=DEFAULT_CLIP_MAX, dest="max_duration", + help=f"Maximum clip duration seconds (default: {DEFAULT_CLIP_MAX})") + parser.add_argument("--buffer", type=float, default=DEFAULT_BUFFER, + help=f"Buffer seconds before/after segment (default: {DEFAULT_BUFFER})") + args = parser.parse_args() + + if not os.path.isfile(args.video_path): + die(f"Video file not found: {args.video_path}") + + out_dir = ensure_safe_output_dir(args.out_dir) + out_dir.mkdir(parents=True, exist_ok=True) + + video_duration = probe_duration(args.video_path) + if video_duration <= 0: + die(f"Cannot determine video duration: {args.video_path}") + print(f"[info] Video duration: {video_duration:.2f}s") + + tmp_dir = str(out_dir / "_tmp") + os.makedirs(tmp_dir, exist_ok=True) + + try: + print("[info] Extracting audio & transcribing...") + full_text, segments = transcribe_video(args.video_path, tmp_dir, video_duration) + + if not segments: + die("ASR returned no segments — cannot identify highlights") + + print(f"[info] Transcribed {len(segments)} segments") + + print("[info] Selecting highlights...") + highlights = select_highlights(segments, args.count) + + if not highlights: + die("No suitable highlights found") + + print(f"[info] Selected {len(highlights)} highlights") + + highlights_info = [] + for i, hl in enumerate(highlights, 1): + clip_start, clip_end = determine_clip_bounds( + hl, video_duration, args.buffer, args.min_duration, args.max_duration, + ) + clip_filename = f"highlight_{i:02d}.mp4" + clip_path = str(out_dir / clip_filename) + + print(f"[info] Clipping highlight {i}/{len(highlights)}: {clip_start:.1f}s – {clip_end:.1f}s") + clip_video(args.video_path, clip_start, clip_end, clip_path) + + clip_duration = probe_duration(clip_path) + clip_size = os.path.getsize(clip_path) / (1024 * 1024) + + highlights_info.append({ + "index": i, + "file": clip_filename, + "start": clip_start, + "end": clip_end, + "duration": round(clip_duration, 2), + "size_mb": round(clip_size, 2), + "text": hl.get("text", ""), + "score": round(hl.get("highlight_score", 0), 2), + }) + + report = { + "source_video": os.path.basename(args.video_path), + "video_duration": round(video_duration, 2), + "highlight_count": len(highlights_info), + "full_transcript": full_text, + "highlights": highlights_info, + } + report_path = str(out_dir / "highlights.json") + Path(report_path).write_text( + json.dumps(report, ensure_ascii=False, indent=2), encoding="utf-8", + ) + + print(f"[done] {len(highlights_info)} highlights saved to: {out_dir}") + for hl in highlights_info: + text_preview = hl["text"][:50] + "..." if len(hl["text"]) > 50 else hl["text"] + print(f" #{hl['index']}: {hl['start']:.1f}s–{hl['end']:.1f}s ({hl['duration']:.1f}s) — {text_preview}") + + finally: + shutil.rmtree(tmp_dir, ignore_errors=True) + + +if __name__ == "__main__": + main() diff --git a/addons/officials/crew/selfmedia-operator/skills/instagram-post/SKILL.md b/addons/officials/crew/selfmedia-operator/skills/instagram-post/SKILL.md deleted file mode 100644 index 6ec40b77..00000000 --- a/addons/officials/crew/selfmedia-operator/skills/instagram-post/SKILL.md +++ /dev/null @@ -1,119 +0,0 @@ ---- -name: instagram-post -description: Publish a photo, video, or carousel to Instagram using the browser. Supports - feed posts and Reels. -metadata: - openclaw: - emoji: 📸 ---- - -# Instagram 发布技能 - -Use this skill when: -- The user wants to publish a photo or video to Instagram Feed or Reels -- You need to cross-post an image or short video to Instagram -- You need to set caption, hashtags, and alt text - -**Prerequisites**: Browser session must be logged in to instagram.com. - ---- - -## 通用约束 - -- 文件上传前必须先复制到 `/tmp/openclaw/uploads/`(browser 工具沙箱限制) -- `browser upload` 工具可能返回「超时错误」,但这**不代表上传失败**!上传后用 snapshot 检查页面状态(预览图、下一步按钮是否可用) -- **不要通过检查 `input.files.length` 是否为 0 判定上传是否失败!** `input.files.length == 0` 不代表上传失败。 -- 遇到 `browser failed: timed out. Restart the OpenClaw gateway ...` 错误时,**不需要重启、不需要报错**!等待 30 秒后在原页面继续操作即可。若仍无法操作,再等 30 秒;若还不行,尝试关闭浏览器后重开;只有关闭重开后仍报错才是真的出错,需停止并反馈用户。 -- Caption 输入使用 `type` + `slowly: true`,不要用 `fill()` - ---- - -## Cookie Warmup - -Navigate to `https://www.instagram.com` first and confirm the feed loads (not a login screen). - ---- - -## Workflow: Post Photo or Single Video (Feed) - -``` -1. Navigate to https://www.instagram.com -2. Wait for feed to load -3. Click the "+" (Create) button in the top navigation bar (or left sidebar) -4. In the "Create new post" dialog, click "Post" -5. Click "Select from computer" and upload the media file - - Photo: JPG/PNG, min 1080px on shortest side (square 1:1 or 4:5 portrait recommended) - - Video: MP4/MOV, max 60 seconds for Feed, 15–90 seconds for Reels -6. Crop/resize the image if prompted (choose the appropriate aspect ratio) -7. Click "Next" -8. (Optional) Apply a filter or do manual adjustments → Click "Next" -9. On the caption screen: - - Write the caption (max 2200 characters) - - Add hashtags at the end (max 30 per post) - - Add location if relevant - - Add collaborators if needed - - Toggle "Advanced settings" for: - - Alt text (for accessibility) - - Audience restrictions -10. Click "Share" -11. Wait for confirmation and report the post URL -``` - ---- - -## Workflow: Post Reel (Short Video ≤ 90s) - -``` -1. Navigate to https://www.instagram.com -2. Click "+" → Select "Reel" -3. Upload video file (MP4/MOV, 9:16 preferred, max 90 seconds) -4. Trim clip if needed -5. Add audio (optional) — use original audio or select from Instagram's music library -6. Select cover frame -7. Click "Next" -8. Write caption + hashtags -9. Click "Share" -``` - ---- - -## Workflow: Carousel (Multiple Photos/Videos) - -``` -1. Click "+" → Post -2. Click the layers icon (Select Multiple) before choosing the first file -3. Select up to 10 photos/videos in order -4. Click "Next" twice (through filters screen) -5. Write caption + hashtags -6. Click "Share" -``` - ---- - -## Caption Best Practices - -- First sentence = hook (shown in feed preview) -- Main body: 2–4 sentences of context or story -- Hashtag block at end: mix popular (#instagram, #photography) with niche tags -- Emoji sparingly to increase personality -- Call to action (CTA) in last line: "Save this" / "Share with someone who needs this" - ---- - -## Error Handling - -| Situation | Action | -|-----------|--------| -| Login screen appears | Session expired — inform user to re-login | -| File format not accepted | Convert to JPG/PNG (photos) or MP4 H.264 (video) | -| Video too long | Trim to 60s (Feed) or 90s (Reels) | -| Post button unavailable | Check if caption or media step is incomplete | -| "Action blocked" error | Account may have hit limits — wait 1 hour before retrying | - ---- - -## Notes - -- Instagram's UI evolves — navigate by intent if elements have moved -- Always confirm post URL or profile page after completion -- Do NOT post content that violates Instagram Community Standards diff --git a/addons/officials/crew/selfmedia-operator/skills/instagram-publish/SKILL.md b/addons/officials/crew/selfmedia-operator/skills/instagram-publish/SKILL.md new file mode 100644 index 00000000..2709e43f --- /dev/null +++ b/addons/officials/crew/selfmedia-operator/skills/instagram-publish/SKILL.md @@ -0,0 +1,114 @@ +--- +name: instagram-publish +description: Publish posts and reels to Instagram via Meta Graph API. Supports single + images, carousels (up to 10), and reels. Uses content container pattern. Requires + Meta OAuth2 token with instagram_basic and instagram_content_publish permissions. +metadata: + openclaw: + emoji: 📸 + requires: + bins: + - python3 +--- + +# Instagram 发布(instagram-publish) + +通过 Meta Graph API 发布内容到 Instagram,支持单图、轮播(最多 10 张)和 Reels。使用 Content Container 模式。需要 Instagram Professional 账户关联到 Facebook Page。 + +--- + +## 前置条件 + +1. Instagram Professional 账户(Business 或 Creator) +2. 关联 Facebook Page +3. Meta Developer 应用,获取 `instagram_basic` + `instagram_content_publish` 权限 +4. 长效 Page Access Token + +--- + +## 配置 + +保存到 `~/.openclaw/credentials/instagram_config.json`: + +```json +{ + "page_access_token": "your_long_lived_page_token", + "ig_user_id": "your_ig_business_account_id" +} +``` + +--- + +## 使用方式 + +单图帖子: + +```bash +python3 ./skills/instagram-publish/scripts/publish_instagram.py \ + --caption "描述 #hashtag1 #hashtag2" \ + --images photo.jpg \ + --mode feed +``` + +轮播帖子: + +```bash +python3 ./skills/instagram-publish/scripts/publish_instagram.py \ + --caption "描述" \ + --images img1.jpg img2.jpg img3.jpg \ + --mode carousel +``` + +Reel: + +```bash +python3 ./skills/instagram-publish/scripts/publish_instagram.py \ + --caption "描述" \ + --video reel.mp4 \ + --mode reel +``` + +--- + +## 参数说明 + +| 参数 | 必填 | 说明 | +|------|------|------| +| `--caption` | 是 | 描述,最多 2200 字,最多 30 个 hashtag | +| `--mode` | 是 | `feed`/`carousel`/`reel` | +| `--images` | feed/carousel必填 | 图片 URL 列表,carousel 最多 10 张 | +| `--video` | reel必填 | 视频 URL,Reels 建议 9:16,≤90s | + +--- + +## 错误处理 + +| 错误 | 原因 | 处理 | +|------|------|------| +| AUTH_REQUIRED | Token 失效 | 更新 Token | +| UPLOAD_FAILED | 容器创建失败 | 检查图片 URL 可访问性 | +| MEDIA_PROCESSING | 媒体处理中 | 轮询状态等待 | +| RATE_LIMIT | API 限制 | 等待后重试 | + +注意:Instagram API 发布只支持 **URL** 形式的媒体,不支持本地文件上传。需要先将图片/视频上传到可公开访问的 URL。 + +--- + +## 发布记录(强制) + +发布成功后,**必须**立即调用 `published-track` 技能记录发布信息: + +```bash +./skills/published-track/scripts/record.sh \ + --platform instagram \ + --title "标题" \ + --content-type post \ + --source-folder "<原始文件夹路径>" \ + --publish-url "<发布URL>" \ + --publish-date "$(date +%Y-%m-%d)" +``` + +`--source-folder` 为原始内容所在的相对路径(如 `output_articles/xxx` 或 `output_videos/xxx`)。 +`--publish-url` 为发布后获得的 URL,若发布失败则留空并在 `--notes` 中注明原因。 + +执行 `./skills/published-track/scripts/init-db.sh`(幂等,重复执行无副作用)。 diff --git a/addons/officials/crew/selfmedia-operator/skills/instagram-publish/scripts/publish_instagram.py b/addons/officials/crew/selfmedia-operator/skills/instagram-publish/scripts/publish_instagram.py new file mode 100755 index 00000000..103a5b01 --- /dev/null +++ b/addons/officials/crew/selfmedia-operator/skills/instagram-publish/scripts/publish_instagram.py @@ -0,0 +1,136 @@ +#!/usr/bin/env python3 +"""Publish posts and reels to Instagram via Meta Graph API (content container pattern).""" + +import argparse +import json +import sys +import time +from pathlib import Path + +import requests + +CREDS_DIR = Path.home() / ".openclaw" / "credentials" +CONFIG_FILE = CREDS_DIR / "instagram_config.json" +GRAPH_API = "https://graph.facebook.com/v23.0" +POLL_INTERVAL = 5 +POLL_MAX = 60 + + +def output(data: dict) -> None: + sys.stdout.write(json.dumps(data, ensure_ascii=False) + "\n") + + +def err_exit(msg: str, code: int = 1) -> None: + sys.stderr.write(f"[instagram-publish] ERROR: {msg}\n") + output({"ok": False, "error": msg}) + sys.exit(code) + + +def load_config() -> dict: + if not CONFIG_FILE.exists(): + err_exit("AUTH_REQUIRED: no instagram_config.json", 2) + return json.loads(CONFIG_FILE.read_text()) + + +def api_post(path: str, token: str, data: dict | None = None) -> dict: + params = {"access_token": token} + resp = requests.post(f"{GRAPH_API}{path}", params=params, json=data, timeout=30) + if resp.status_code in (401, 403): + err_exit("AUTH_REQUIRED", 2) + result = resp.json() + if "error" in result: + err_exit(f"API_ERROR: {result['error'].get('message', result)}") + return result + + +def create_media_container(ig_id: str, token: str, media_type: str, media_url: str, caption: str = "") -> str: + data = {"media_type": media_type, "image_url" if media_type == "IMAGE" else "video_url": media_url} + if caption: + data["caption"] = caption + if media_type == "REEL": + data["media_type"] = "REELS" + data["video_url"] = media_url + + result = api_post(f"/{ig_id}/media", token, data) + container_id = result.get("id", "") + if not container_id: + err_exit(f"UPLOAD_FAILED: no container id: {result}") + return container_id + + +def poll_container_status(ig_id: str, token: str, container_id: str) -> str: + for _ in range(POLL_MAX): + params = {"access_token": token, "fields": "status_code"} + resp = requests.get(f"{GRAPH_API}/{container_id}", params=params, timeout=30) + data = resp.json() + status = data.get("status_code", "") + if status == "FINISHED": + return "ready" + elif status == "ERROR": + err_exit(f"UPLOAD_FAILED: container error: {data}") + time.sleep(POLL_INTERVAL) + err_exit("MEDIA_PROCESSING: timed out waiting for container") + + +def publish_container(ig_id: str, token: str, creation_id: str) -> dict: + result = api_post(f"/{ig_id}/media_publish", token, data={"creation_id": creation_id}) + post_id = result.get("id", "") + return {"ok": True, "post_id": post_id, "url": f"https://www.instagram.com/p/{post_id}"} + + +def publish_carousel(ig_id: str, token: str, image_urls: list[str], caption: str) -> dict: + children_ids = [] + for url in image_urls[:10]: + cid = create_media_container(ig_id, token, "IMAGE", url) + poll_container_status(ig_id, token, cid) + children_ids.append(cid) + + data = { + "media_type": "CAROUSEL", + "children": ",".join(children_ids), + "caption": caption, + } + result = api_post(f"/{ig_id}/media", token, data) + carousel_id = result.get("id", "") + poll_container_status(ig_id, token, carousel_id) + return publish_container(ig_id, token, carousel_id) + + +def main() -> None: + parser = argparse.ArgumentParser(description="Publish to Instagram") + parser.add_argument("--caption", required=True, help="Caption (max 2200 chars, max 30 hashtags)") + parser.add_argument("--mode", required=True, choices=["feed", "carousel", "reel"]) + parser.add_argument("--images", nargs="+", help="Image URLs") + parser.add_argument("--video", help="Video URL (for reels)") + args = parser.parse_args() + + config = load_config() + token = config.get("page_access_token", "") + ig_id = config.get("ig_user_id", "") + if not token or not ig_id: + err_exit("AUTH_REQUIRED: missing token or ig_user_id", 2) + + if args.mode == "feed": + if not args.images or len(args.images) != 1: + err_exit("--images requires exactly 1 URL for feed mode") + cid = create_media_container(ig_id, token, "IMAGE", args.images[0], args.caption) + sys.stderr.write("[instagram-publish] waiting for media processing...\n") + poll_container_status(ig_id, token, cid) + result = publish_container(ig_id, token, cid) + elif args.mode == "carousel": + if not args.images or len(args.images) < 2: + err_exit("--images requires 2-10 URLs for carousel mode") + result = publish_carousel(ig_id, token, args.images, args.caption) + elif args.mode == "reel": + if not args.video: + err_exit("--video URL required for reel mode") + cid = create_media_container(ig_id, token, "REEL", args.video, args.caption) + sys.stderr.write("[instagram-publish] waiting for video processing...\n") + poll_container_status(ig_id, token, cid) + result = publish_container(ig_id, token, cid) + + output(result) + + +if __name__ == "__main__": + main() diff --git a/addons/officials/crew/selfmedia-operator/skills/juejin-publish/SKILL.md b/addons/officials/crew/selfmedia-operator/skills/juejin-publish/SKILL.md index bdb3c20a..13aeb5eb 100644 --- a/addons/officials/crew/selfmedia-operator/skills/juejin-publish/SKILL.md +++ b/addons/officials/crew/selfmedia-operator/skills/juejin-publish/SKILL.md @@ -172,3 +172,22 @@ See `references/publish-options.md` for category list, tag suggestions, and cove ❌ 兜底路径忘记填摘要(*必填): → 发布按钮无响应,因为摘要为空 ``` + +## 发布记录(强制) + +发布成功后,**必须**立即调用 `published-track` 技能记录发布信息: + +```bash +./skills/published-track/scripts/record.sh \ + --platform juejin \ + --title "标题" \ + --content-type article \ + --source-folder "<原始文件夹路径>" \ + --publish-url "<发布URL>" \ + --publish-date "$(date +%Y-%m-%d)" +``` + +`--source-folder` 为原始内容所在的相对路径(如 `output_articles/xxx` 或 `output_videos/xxx`)。 +`--publish-url` 为发布后获得的 URL,若发布失败则留空并在 `--notes` 中注明原因。 + +执行 `./skills/published-track/scripts/init-db.sh`(幂等,重复执行无副作用)。 diff --git a/addons/officials/crew/selfmedia-operator/skills/pinterest-publish/SKILL.md b/addons/officials/crew/selfmedia-operator/skills/pinterest-publish/SKILL.md new file mode 100644 index 00000000..f4790fd6 --- /dev/null +++ b/addons/officials/crew/selfmedia-operator/skills/pinterest-publish/SKILL.md @@ -0,0 +1,106 @@ +--- +name: pinterest-publish +description: Create pins on Pinterest via Pinterest API v5. Supports image pins + and video pins with board selection. Requires Pinterest OAuth2 token. +metadata: + openclaw: + emoji: 📌 + requires: + bins: + - python3 +--- + +# Pinterest 发布(pinterest-publish) + +通过 Pinterest API v5 创建 Pin,支持图片 Pin 和视频 Pin。使用 OAuth2 认证。 + +--- + +## 前置条件 + +1. Pinterest Developer Portal 创建应用,获取 App ID / App Secret +2. 申请 `pins:write` 和 `boards:read` 权限 +3. 获取 OAuth2 Access Token + +--- + +## 配置 + +保存到 `~/.openclaw/credentials/pinterest_config.json`: + +```json +{ + "access_token": "your_access_token", + "board_id": "your_default_board_id" +} +``` + +--- + +## 使用方式 + +图片 Pin: + +```bash +python3 ./skills/pinterest-publish/scripts/publish_pinterest.py \ + --title "Pin 标题" \ + --description "描述" \ + --image https://example.com/image.jpg \ + --board-id 123456789 +``` + +视频 Pin: + +```bash +python3 ./skills/pinterest-publish/scripts/publish_pinterest.py \ + --title "Pin 标题" \ + --description "描述" \ + --video https://example.com/video.mp4 \ + --board-id 123456789 +``` + +--- + +## 参数说明 + +| 参数 | 必填 | 说明 | +|------|------|------| +| `--title` | 是 | Pin 标题 | +| `--description` | 否 | Pin 描述 | +| `--image` | 图片必填 | 图片 URL | +| `--video` | 视频必填 | 视频 URL | +| `--board-id` | 是 | 看板 ID | +| `--link` | 否 | 关联链接 URL | + +--- + +## 错误处理 + +| 错误 | 原因 | 处理 | +|------|------|------| +| AUTH_REQUIRED | Token 失效 | 更新 Access Token | +| UPLOAD_FAILED | 上传失败 | 检查 URL 可访问性 | +| INVALID_BOARD | 看板 ID 无效 | 检查看板是否存在 | + +注意:Pinterest API 仅支持 URL 形式的媒体,不支持本地文件上传。 + +--- + +## 发布记录(强制) + +发布成功后,**必须**立即调用 `published-track` 技能记录发布信息: + +```bash +./skills/published-track/scripts/record.sh \ + --platform pinterest \ + --title "标题" \ + --content-type post \ + --source-folder "<原始文件夹路径>" \ + --publish-url "<发布URL>" \ + --publish-date "$(date +%Y-%m-%d)" +``` + +`--source-folder` 为原始内容所在的相对路径(如 `output_articles/xxx` 或 `output_videos/xxx`)。 +`--publish-url` 为发布后获得的 URL,若发布失败则留空并在 `--notes` 中注明原因。 + +执行 `./skills/published-track/scripts/init-db.sh`(幂等,重复执行无副作用)。 diff --git a/addons/officials/crew/selfmedia-operator/skills/pinterest-publish/scripts/publish_pinterest.py b/addons/officials/crew/selfmedia-operator/skills/pinterest-publish/scripts/publish_pinterest.py new file mode 100755 index 00000000..fb001e7d --- /dev/null +++ b/addons/officials/crew/selfmedia-operator/skills/pinterest-publish/scripts/publish_pinterest.py @@ -0,0 +1,115 @@ +#!/usr/bin/env python3 +"""Create pins on Pinterest via Pinterest API v5 with OAuth2.""" + +import argparse +import json +import sys +from pathlib import Path + +import requests + +CREDS_DIR = Path.home() / ".openclaw" / "credentials" +CONFIG_FILE = CREDS_DIR / "pinterest_config.json" +PINTEREST_API = "https://api.pinterest.com/v5" + + +def output(data: dict) -> None: + sys.stdout.write(json.dumps(data, ensure_ascii=False) + "\n") + + +def err_exit(msg: str, code: int = 1) -> None: + sys.stderr.write(f"[pinterest-publish] ERROR: {msg}\n") + output({"ok": False, "error": msg}) + sys.exit(code) + + +def load_config() -> dict: + if not CONFIG_FILE.exists(): + err_exit("AUTH_REQUIRED: no pinterest_config.json", 2) + return json.loads(CONFIG_FILE.read_text()) + + +def api_post(token: str, path: str, data: dict) -> dict: + headers = {"Authorization": f"Bearer {token}", "Content-Type": "application/json"} + resp = requests.post(f"{PINTEREST_API}{path}", headers=headers, json=data, timeout=30) + if resp.status_code in (401, 403): + err_exit("AUTH_REQUIRED", 2) + result = resp.json() + if "error" in result and isinstance(result["error"], dict): + err_exit(f"API_ERROR: {result['error'].get('message', result)}") + return result + + +def create_image_pin(token: str, board_id: str, title: str, description: str, image_url: str, link: str = "") -> dict: + data = { + "board_id": board_id, + "title": title, + "description": description, + "media_source": { + "source_type": "image_url", + "url": image_url, + }, + } + if link: + data["link"] = link + + result = api_post(token, "/pins", data) + pin_id = result.get("id", "") + return {"ok": True, "pin_id": pin_id, "url": f"https://www.pinterest.com/pin/{pin_id}/"} + + +def create_video_pin(token: str, board_id: str, title: str, description: str, video_url: str, link: str = "") -> dict: + register = api_post(token, "/media", data={"media_type": "video"}) + media_id = register.get("media_id", "") + if not media_id: + err_exit(f"UPLOAD_FAILED: no media_id: {register}") + + data = { + "board_id": board_id, + "title": title, + "description": description, + "media_source": { + "source_type": "video_id", + "video_id": media_id, + "cover_image_url": video_url, + }, + } + if link: + data["link"] = link + + result = api_post(token, "/pins", data) + pin_id = result.get("id", "") + return {"ok": True, "pin_id": pin_id, "url": f"https://www.pinterest.com/pin/{pin_id}/"} + + +def main() -> None: + parser = argparse.ArgumentParser(description="Create pin on Pinterest") + parser.add_argument("--title", required=True, help="Pin title") + parser.add_argument("--description", default="", help="Pin description") + parser.add_argument("--image", help="Image URL") + parser.add_argument("--video", help="Video URL") + parser.add_argument("--board-id", required=True, help="Board ID") + parser.add_argument("--link", default="", help="Destination link URL") + args = parser.parse_args() + + config = load_config() + token = config.get("access_token", "") + if not token: + err_exit("AUTH_REQUIRED: missing access_token", 2) + + board_id = args.board_id or config.get("board_id", "") + if not board_id: + err_exit("INVALID_BOARD: no board_id provided", 2) + + if args.image: + result = create_image_pin(token, board_id, args.title, args.description, args.image, args.link) + elif args.video: + result = create_video_pin(token, board_id, args.title, args.description, args.video, args.link) + else: + err_exit("Either --image or --video URL is required") + + output(result) + + +if __name__ == "__main__": + main() diff --git a/addons/officials/crew/selfmedia-operator/skills/published-track/SKILL.md b/addons/officials/crew/selfmedia-operator/skills/published-track/SKILL.md new file mode 100644 index 00000000..8e0bcc85 --- /dev/null +++ b/addons/officials/crew/selfmedia-operator/skills/published-track/SKILL.md @@ -0,0 +1,142 @@ +--- +name: published-track +description: 发布记录追踪。使用 SQLite 数据库记录所有平台发布内容及其互动数据,按平台分表管理。发布后必须调用本技能记录,心跳巡检时更新数据。 +metadata: + openclaw: + emoji: "📊" + requires: + bins: + - bash + - sqlite3 +--- + +# published-track — 发布记录追踪 + +统一管理所有平台(微信公众号、知乎、B站、抖音、快手、小红书、今日头条、掘金、Twitter/X、Facebook、Instagram、TikTok、YouTube、Pinterest、Threads、企业微信朋友圈)的发布记录与互动数据。 + +--- + +## 数据库位置 + +`./db/published_track.db`(相对于工作区根目录) + +初始化(幂等,可重复执行): + +```bash +./skills/published-track/scripts/init-db.sh +``` + +--- + +## 平台与表对应关系 + +| 平台 | 表名 | 内容类型 | 特有指标 | +|------|------|---------|---------| +| 微信公众号 | `pub_wx_mp` | article/video/post | reads, shares, favorites, likes, comments | +| 知乎 | `pub_zhihu` | article/post | views, upvotes, comments, favorites | +| B站 | `pub_bilibili` | video | plays, danmaku, likes, coins, favorites, shares, comments | +| 抖音 | `pub_douyin` | video | plays, likes, comments, shares, favorites | +| 快手 | `pub_kuaishou` | video | plays, likes, comments, shares | +| 小红书 | `pub_xhs` | article/video/post | views, likes, favorites, comments, shares | +| 今日头条 | `pub_toutiao` | article | impressions, reads, comments, likes | +| 掘金 | `pub_juejin` | article | views, likes, comments, favorites | +| Twitter/X | `pub_twitter` | post/video | views, likes, retweets, replies, bookmarks | +| Facebook | `pub_facebook` | post/video | reach, likes, comments, shares | +| Instagram | `pub_instagram` | post/video | reach, likes, comments, shares, saves | +| TikTok | `pub_tiktok` | video | plays, likes, comments, shares, favorites | +| YouTube | `pub_youtube` | video | views, likes, comments, shares | +| Pinterest | `pub_pinterest` | post | impressions, saves, comments | +| Threads | `pub_threads` | post | views, likes, reposts, replies | +| 企业微信朋友圈 | `pub_wxwork_moments` | post | likes, comments | + +--- + +## 表结构 + +每张表共享以下通用字段: + +| 字段 | 类型 | 说明 | +|------|------|------| +| id | INTEGER PK | 自增主键 | +| title | TEXT NOT NULL | 标题 | +| content_type | TEXT NOT NULL | article / video / post | +| source_folder | TEXT NOT NULL | 原始文件夹(如 output_articles/xxx 或 output_videos/xxx) | +| publish_url | TEXT | 发布后 URL | +| publish_date | TEXT NOT NULL | 发布日期(YYYY-MM-DD) | +| notes | TEXT | 备注 | +| created_at | TEXT | 创建时间 | +| updated_at | TEXT | 更新时间 | + +去重键:`source_folder`(同一篇内容在同一平台只记录一次) + +各平台特有的互动指标字段默认值为 0,另有 `top_comment`(主要留言摘要,TEXT)字段。 + +--- + +## 使用方式 + +### 1. 发布后立即记录(强制) + +每完成一个平台的发布,**必须**立即调用 `record.sh` 记录: + +```bash +./skills/published-track/scripts/record.sh \ + --platform \ + --title "标题" \ + --content-type \ + --source-folder "output_articles/xxx" \ + --publish-url "https://..." \ + --publish-date "$(date +%Y-%m-%d)" \ + [--notes "备注"] +``` + +`--platform` 值对应上表「表名」去掉 `pub_` 前缀,如 `wx_mp`、`zhihu`、`bilibili` 等。 + +若发布失败(如平台拒绝、超时),仍需记录,`publish_url` 留空,`notes` 中注明失败原因。 + +### 2. 数据更新(心跳巡检) + +使用 `update-metrics.sh` 更新互动数据: + +```bash +./skills/published-track/scripts/update-metrics.sh \ + --platform \ + --source-folder "output_articles/xxx" \ + --reads 1234 \ + --likes 56 \ + ... +``` + +只需传入要更新的指标字段,未传入的字段保持不变。 + +### 3. 查询 + +```bash +# 查询某平台全部记录 +./skills/published-track/scripts/query.sh --platform zhihu + +# 查询某平台最近 N 条 +./skills/published-track/scripts/query.sh --platform zhihu --limit 10 + +# 查询特定内容是否已发布到某平台 +./skills/published-track/scripts/check-published.sh \ + --platform zhihu \ + --source-folder "output_articles/xxx" + +# 查询所有平台中未发布的原始文件夹 +./skills/published-track/scripts/query.sh --unpublished +``` + +### 4. 清理低数据记录 + +发布超过 7 天、互动指标低于 300 的记录,使用 `query.sh` 查出后可删除: + +```bash +./skills/published-track/scripts/query.sh --platform zhihu --stale-days 7 --below 300 +``` + +--- + +## 与发布技能的配合 + +所有发布技能(wx-mp-publisher、sync-from-mp、bilibili-publish 等)在发布成功后必须调用 `record.sh` 记录。各技能 SKILL.md 中已标注此要求,主 agent 无需额外提醒。 diff --git a/addons/officials/crew/selfmedia-operator/skills/published-track/scripts/check-published.sh b/addons/officials/crew/selfmedia-operator/skills/published-track/scripts/check-published.sh new file mode 100755 index 00000000..b181fdca --- /dev/null +++ b/addons/officials/crew/selfmedia-operator/skills/published-track/scripts/check-published.sh @@ -0,0 +1,41 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT="$(cd "$(dirname "$0")/../../.." && pwd)" +DB="$ROOT/db/published_track.db" + +if [ ! -f "$DB" ]; then + echo '{"exists":false}' + exit 0 +fi + +PLATFORM="" SOURCE_FOLDER="" + +while [[ $# -gt 0 ]]; do + case "$1" in + --platform) PLATFORM="$2"; shift 2 ;; + --source-folder) SOURCE_FOLDER="$2"; shift 2 ;; + *) echo "{\"ok\":false,\"error\":\"unknown arg: $1\"}"; exit 1 ;; + esac +done + +if [ -z "$PLATFORM" ] || [ -z "$SOURCE_FOLDER" ]; then + echo '{"ok":false,"error":"missing required args: --platform, --source-folder"}' + exit 1 +fi + +TABLE="pub_${PLATFORM}" +VALID=$(sqlite3 "$DB" "SELECT name FROM sqlite_master WHERE type='table' AND name='$TABLE';") +if [ -z "$VALID" ]; then + echo "{\"ok\":false,\"error\":\"unknown platform: $PLATFORM\"}" + exit 1 +fi + +ROW=$(sqlite3 "$DB" "SELECT id,publish_url FROM $TABLE WHERE source_folder='${SOURCE_FOLDER//\'/\'\'}';") +if [ -z "$ROW" ]; then + echo '{"exists":false}' +else + ID=$(echo "$ROW" | cut -d'|' -f1) + URL=$(echo "$ROW" | cut -d'|' -f2) + echo "{\"exists\":true,\"id\":$ID,\"publish_url\":\"$URL\"}" +fi diff --git a/addons/officials/crew/selfmedia-operator/skills/published-track/scripts/init-db.sh b/addons/officials/crew/selfmedia-operator/skills/published-track/scripts/init-db.sh new file mode 100755 index 00000000..096badda --- /dev/null +++ b/addons/officials/crew/selfmedia-operator/skills/published-track/scripts/init-db.sh @@ -0,0 +1,301 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT="$(cd "$(dirname "$0")/../../.." && pwd)" +DB="$ROOT/db/published_track.db" + +mkdir -p "$ROOT/db" + +sqlite3 "$DB" <<'SQL' + +-- 微信公众号 +CREATE TABLE IF NOT EXISTS pub_wx_mp ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + title TEXT NOT NULL, + content_type TEXT NOT NULL CHECK(content_type IN ('article','video','post')), + source_folder TEXT NOT NULL UNIQUE, + publish_url TEXT, + publish_date TEXT NOT NULL, + reads INTEGER DEFAULT 0, + shares INTEGER DEFAULT 0, + favorites INTEGER DEFAULT 0, + likes INTEGER DEFAULT 0, + comments INTEGER DEFAULT 0, + top_comment TEXT, + notes TEXT, + created_at TEXT DEFAULT (strftime('%Y-%m-%d %H:%M:%S','now','localtime')), + updated_at TEXT DEFAULT (strftime('%Y-%m-%d %H:%M:%S','now','localtime')) +); + +-- 知乎 +CREATE TABLE IF NOT EXISTS pub_zhihu ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + title TEXT NOT NULL, + content_type TEXT NOT NULL CHECK(content_type IN ('article','video','post')), + source_folder TEXT NOT NULL UNIQUE, + publish_url TEXT, + publish_date TEXT NOT NULL, + views INTEGER DEFAULT 0, + upvotes INTEGER DEFAULT 0, + comments INTEGER DEFAULT 0, + favorites INTEGER DEFAULT 0, + top_comment TEXT, + notes TEXT, + created_at TEXT DEFAULT (strftime('%Y-%m-%d %H:%M:%S','now','localtime')), + updated_at TEXT DEFAULT (strftime('%Y-%m-%d %H:%M:%S','now','localtime')) +); + +-- B站 +CREATE TABLE IF NOT EXISTS pub_bilibili ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + title TEXT NOT NULL, + content_type TEXT NOT NULL CHECK(content_type IN ('article','video','post')), + source_folder TEXT NOT NULL UNIQUE, + publish_url TEXT, + publish_date TEXT NOT NULL, + plays INTEGER DEFAULT 0, + danmaku INTEGER DEFAULT 0, + likes INTEGER DEFAULT 0, + coins INTEGER DEFAULT 0, + favorites INTEGER DEFAULT 0, + shares INTEGER DEFAULT 0, + comments INTEGER DEFAULT 0, + top_comment TEXT, + notes TEXT, + created_at TEXT DEFAULT (strftime('%Y-%m-%d %H:%M:%S','now','localtime')), + updated_at TEXT DEFAULT (strftime('%Y-%m-%d %H:%M:%S','now','localtime')) +); + +-- 抖音 +CREATE TABLE IF NOT EXISTS pub_douyin ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + title TEXT NOT NULL, + content_type TEXT NOT NULL CHECK(content_type IN ('article','video','post')), + source_folder TEXT NOT NULL UNIQUE, + publish_url TEXT, + publish_date TEXT NOT NULL, + plays INTEGER DEFAULT 0, + likes INTEGER DEFAULT 0, + comments INTEGER DEFAULT 0, + shares INTEGER DEFAULT 0, + favorites INTEGER DEFAULT 0, + top_comment TEXT, + notes TEXT, + created_at TEXT DEFAULT (strftime('%Y-%m-%d %H:%M:%S','now','localtime')), + updated_at TEXT DEFAULT (strftime('%Y-%m-%d %H:%M:%S','now','localtime')) +); + +-- 快手 +CREATE TABLE IF NOT EXISTS pub_kuaishou ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + title TEXT NOT NULL, + content_type TEXT NOT NULL CHECK(content_type IN ('article','video','post')), + source_folder TEXT NOT NULL UNIQUE, + publish_url TEXT, + publish_date TEXT NOT NULL, + plays INTEGER DEFAULT 0, + likes INTEGER DEFAULT 0, + comments INTEGER DEFAULT 0, + shares INTEGER DEFAULT 0, + top_comment TEXT, + notes TEXT, + created_at TEXT DEFAULT (strftime('%Y-%m-%d %H:%M:%S','now','localtime')), + updated_at TEXT DEFAULT (strftime('%Y-%m-%d %H:%M:%S','now','localtime')) +); + +-- 小红书 +CREATE TABLE IF NOT EXISTS pub_xhs ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + title TEXT NOT NULL, + content_type TEXT NOT NULL CHECK(content_type IN ('article','video','post')), + source_folder TEXT NOT NULL UNIQUE, + publish_url TEXT, + publish_date TEXT NOT NULL, + views INTEGER DEFAULT 0, + likes INTEGER DEFAULT 0, + favorites INTEGER DEFAULT 0, + comments INTEGER DEFAULT 0, + shares INTEGER DEFAULT 0, + top_comment TEXT, + notes TEXT, + created_at TEXT DEFAULT (strftime('%Y-%m-%d %H:%M:%S','now','localtime')), + updated_at TEXT DEFAULT (strftime('%Y-%m-%d %H:%M:%S','now','localtime')) +); + +-- 今日头条 +CREATE TABLE IF NOT EXISTS pub_toutiao ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + title TEXT NOT NULL, + content_type TEXT NOT NULL CHECK(content_type IN ('article','video','post')), + source_folder TEXT NOT NULL UNIQUE, + publish_url TEXT, + publish_date TEXT NOT NULL, + impressions INTEGER DEFAULT 0, + reads INTEGER DEFAULT 0, + comments INTEGER DEFAULT 0, + likes INTEGER DEFAULT 0, + top_comment TEXT, + notes TEXT, + created_at TEXT DEFAULT (strftime('%Y-%m-%d %H:%M:%S','now','localtime')), + updated_at TEXT DEFAULT (strftime('%Y-%m-%d %H:%M:%S','now','localtime')) +); + +-- 掘金 +CREATE TABLE IF NOT EXISTS pub_juejin ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + title TEXT NOT NULL, + content_type TEXT NOT NULL CHECK(content_type IN ('article','video','post')), + source_folder TEXT NOT NULL UNIQUE, + publish_url TEXT, + publish_date TEXT NOT NULL, + views INTEGER DEFAULT 0, + likes INTEGER DEFAULT 0, + comments INTEGER DEFAULT 0, + favorites INTEGER DEFAULT 0, + top_comment TEXT, + notes TEXT, + created_at TEXT DEFAULT (strftime('%Y-%m-%d %H:%M:%S','now','localtime')), + updated_at TEXT DEFAULT (strftime('%Y-%m-%d %H:%M:%S','now','localtime')) +); + +-- Twitter/X +CREATE TABLE IF NOT EXISTS pub_twitter ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + title TEXT NOT NULL, + content_type TEXT NOT NULL CHECK(content_type IN ('article','video','post')), + source_folder TEXT NOT NULL UNIQUE, + publish_url TEXT, + publish_date TEXT NOT NULL, + views INTEGER DEFAULT 0, + likes INTEGER DEFAULT 0, + retweets INTEGER DEFAULT 0, + replies INTEGER DEFAULT 0, + bookmarks INTEGER DEFAULT 0, + notes TEXT, + created_at TEXT DEFAULT (strftime('%Y-%m-%d %H:%M:%S','now','localtime')), + updated_at TEXT DEFAULT (strftime('%Y-%m-%d %H:%M:%S','now','localtime')) +); + +-- Facebook +CREATE TABLE IF NOT EXISTS pub_facebook ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + title TEXT NOT NULL, + content_type TEXT NOT NULL CHECK(content_type IN ('article','video','post')), + source_folder TEXT NOT NULL UNIQUE, + publish_url TEXT, + publish_date TEXT NOT NULL, + reach INTEGER DEFAULT 0, + likes INTEGER DEFAULT 0, + comments INTEGER DEFAULT 0, + shares INTEGER DEFAULT 0, + notes TEXT, + created_at TEXT DEFAULT (strftime('%Y-%m-%d %H:%M:%S','now','localtime')), + updated_at TEXT DEFAULT (strftime('%Y-%m-%d %H:%M:%S','now','localtime')) +); + +-- Instagram +CREATE TABLE IF NOT EXISTS pub_instagram ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + title TEXT NOT NULL, + content_type TEXT NOT NULL CHECK(content_type IN ('article','video','post')), + source_folder TEXT NOT NULL UNIQUE, + publish_url TEXT, + publish_date TEXT NOT NULL, + reach INTEGER DEFAULT 0, + likes INTEGER DEFAULT 0, + comments INTEGER DEFAULT 0, + shares INTEGER DEFAULT 0, + saves INTEGER DEFAULT 0, + notes TEXT, + created_at TEXT DEFAULT (strftime('%Y-%m-%d %H:%M:%S','now','localtime')), + updated_at TEXT DEFAULT (strftime('%Y-%m-%d %H:%M:%S','now','localtime')) +); + +-- TikTok +CREATE TABLE IF NOT EXISTS pub_tiktok ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + title TEXT NOT NULL, + content_type TEXT NOT NULL CHECK(content_type IN ('article','video','post')), + source_folder TEXT NOT NULL UNIQUE, + publish_url TEXT, + publish_date TEXT NOT NULL, + plays INTEGER DEFAULT 0, + likes INTEGER DEFAULT 0, + comments INTEGER DEFAULT 0, + shares INTEGER DEFAULT 0, + favorites INTEGER DEFAULT 0, + top_comment TEXT, + notes TEXT, + created_at TEXT DEFAULT (strftime('%Y-%m-%d %H:%M:%S','now','localtime')), + updated_at TEXT DEFAULT (strftime('%Y-%m-%d %H:%M:%S','now','localtime')) +); + +-- YouTube +CREATE TABLE IF NOT EXISTS pub_youtube ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + title TEXT NOT NULL, + content_type TEXT NOT NULL CHECK(content_type IN ('article','video','post')), + source_folder TEXT NOT NULL UNIQUE, + publish_url TEXT, + publish_date TEXT NOT NULL, + views INTEGER DEFAULT 0, + likes INTEGER DEFAULT 0, + comments INTEGER DEFAULT 0, + shares INTEGER DEFAULT 0, + notes TEXT, + created_at TEXT DEFAULT (strftime('%Y-%m-%d %H:%M:%S','now','localtime')), + updated_at TEXT DEFAULT (strftime('%Y-%m-%d %H:%M:%S','now','localtime')) +); + +-- Pinterest +CREATE TABLE IF NOT EXISTS pub_pinterest ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + title TEXT NOT NULL, + content_type TEXT NOT NULL CHECK(content_type IN ('article','video','post')), + source_folder TEXT NOT NULL UNIQUE, + publish_url TEXT, + publish_date TEXT NOT NULL, + impressions INTEGER DEFAULT 0, + saves INTEGER DEFAULT 0, + comments INTEGER DEFAULT 0, + notes TEXT, + created_at TEXT DEFAULT (strftime('%Y-%m-%d %H:%M:%S','now','localtime')), + updated_at TEXT DEFAULT (strftime('%Y-%m-%d %H:%M:%S','now','localtime')) +); + +-- Threads +CREATE TABLE IF NOT EXISTS pub_threads ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + title TEXT NOT NULL, + content_type TEXT NOT NULL CHECK(content_type IN ('article','video','post')), + source_folder TEXT NOT NULL UNIQUE, + publish_url TEXT, + publish_date TEXT NOT NULL, + views INTEGER DEFAULT 0, + likes INTEGER DEFAULT 0, + reposts INTEGER DEFAULT 0, + replies INTEGER DEFAULT 0, + notes TEXT, + created_at TEXT DEFAULT (strftime('%Y-%m-%d %H:%M:%S','now','localtime')), + updated_at TEXT DEFAULT (strftime('%Y-%m-%d %H:%M:%S','now','localtime')) +); + +-- 企业微信朋友圈 +CREATE TABLE IF NOT EXISTS pub_wxwork_moments ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + title TEXT NOT NULL, + content_type TEXT NOT NULL CHECK(content_type IN ('article','video','post')), + source_folder TEXT NOT NULL UNIQUE, + publish_url TEXT, + publish_date TEXT NOT NULL, + likes INTEGER DEFAULT 0, + comments INTEGER DEFAULT 0, + top_comment TEXT, + notes TEXT, + created_at TEXT DEFAULT (strftime('%Y-%m-%d %H:%M:%S','now','localtime')), + updated_at TEXT DEFAULT (strftime('%Y-%m-%d %H:%M:%S','now','localtime')) +); + +SQL + +echo '{"ok":true,"message":"published_track.db initialized"}' diff --git a/addons/officials/crew/selfmedia-operator/skills/published-track/scripts/query.sh b/addons/officials/crew/selfmedia-operator/skills/published-track/scripts/query.sh new file mode 100755 index 00000000..9358e236 --- /dev/null +++ b/addons/officials/crew/selfmedia-operator/skills/published-track/scripts/query.sh @@ -0,0 +1,91 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT="$(cd "$(dirname "$0")/../../.." && pwd)" +DB="$ROOT/db/published_track.db" + +if [ ! -f "$DB" ]; then + echo '[]' + exit 0 +fi + +PLATFORM="" LIMIT="" UNPUBLISHED=false STALE_DAYS="" BELOW="" + +while [[ $# -gt 0 ]]; do + case "$1" in + --platform) PLATFORM="$2"; shift 2 ;; + --limit) LIMIT="$2"; shift 2 ;; + --unpublished) UNPUBLISHED=true; shift ;; + --stale-days) STALE_DAYS="$2"; shift 2 ;; + --below) BELOW="$2"; shift 2 ;; + *) echo "{\"ok\":false,\"error\":\"unknown arg: $1\"}"; exit 1 ;; + esac +done + +if [ "$UNPUBLISHED" = true ]; then + # Find source_folders in output_articles/ and output_videos/ that have no record in any platform table + TABLES=$(sqlite3 "$DB" "SELECT name FROM sqlite_master WHERE type='table' AND name LIKE 'pub_%';") + FOLDERS=$(find "$ROOT/output_articles" "$ROOT/output_videos" -mindepth 1 -maxdepth 1 -type d 2>/dev/null | sed "s|$ROOT/||" | sort) + + UNPUB_LIST="[" + FIRST=true + for F in $FOLDERS; do + FOUND=false + for T in $TABLES; do + CNT=$(sqlite3 "$DB" "SELECT COUNT(*) FROM $T WHERE source_folder='${F//\'/\'\'}';") + if [ "$CNT" -gt 0 ]; then + FOUND=true + break + fi + done + if [ "$FOUND" = false ]; then + [ "$FIRST" = true ] && FIRST=false || UNPUB_LIST+="," + UNPUB_LIST+="\"$F\"" + fi + done + UNPUB_LIST+="]" + echo "$UNPUB_LIST" + exit 0 +fi + +if [ -z "$PLATFORM" ]; then + echo '{"ok":false,"error":"--platform is required (unless --unpublished)"}' + exit 1 +fi + +TABLE="pub_${PLATFORM}" +VALID=$(sqlite3 "$DB" "SELECT name FROM sqlite_master WHERE type='table' AND name='$TABLE';") +if [ -z "$VALID" ]; then + echo "{\"ok\":false,\"error\":\"unknown platform: $PLATFORM\"}" + exit 1 +fi + +# Build query +WHERE="" +if [ -n "$STALE_DAYS" ]; then + WHERE="WHERE publish_date <= date('now','-$STALE_DAYS days')" +fi + +LIMIT_CLAUSE="" +if [ -n "$LIMIT" ]; then + LIMIT_CLAUSE="LIMIT $LIMIT" +fi + +# Query all records +ROWS=$(sqlite3 -json "$DB" "SELECT * FROM $TABLE $WHERE ORDER BY publish_date DESC $LIMIT_CLAUSE;" 2>/dev/null) + +if [ -n "$BELOW" ] && [ -n "$STALE_DAYS" ]; then + # Filter for records where all main metric columns are below threshold + # Get integer columns + INT_COLS=$(sqlite3 "$DB" "PRAGMA table_info($TABLE);" | awk -F'|' '$2 != "id" && $2 != "title" && $2 != "content_type" && $2 != "source_folder" && $2 != "publish_url" && $2 != "publish_date" && $2 != "notes" && $2 != "top_comment" && $2 != "created_at" && $2 != "updated_at" {print $2}') + + CONDS="" + for C in $INT_COLS; do + [ -n "$CONDS" ] && CONDS+=" AND " + CONDS+="$C < $BELOW" + done + + ROWS=$(sqlite3 -json "$DB" "SELECT * FROM $TABLE WHERE publish_date <= date('now','-$STALE_DAYS days') AND ($CONDS) ORDER BY publish_date DESC $LIMIT_CLAUSE;" 2>/dev/null) +fi + +echo "${ROWS:-[]}" diff --git a/addons/officials/crew/selfmedia-operator/skills/published-track/scripts/record.sh b/addons/officials/crew/selfmedia-operator/skills/published-track/scripts/record.sh new file mode 100755 index 00000000..ae667049 --- /dev/null +++ b/addons/officials/crew/selfmedia-operator/skills/published-track/scripts/record.sh @@ -0,0 +1,66 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT="$(cd "$(dirname "$0")/../../.." && pwd)" +DB="$ROOT/db/published_track.db" + +# Ensure db exists +if [ ! -f "$DB" ]; then + bash "$(dirname "$0")/init-db.sh" +fi + +# Parse args +PLATFORM="" TITLE="" CONTENT_TYPE="" SOURCE_FOLDER="" PUBLISH_URL="" PUBLISH_DATE="" NOTES="" + +while [[ $# -gt 0 ]]; do + case "$1" in + --platform) PLATFORM="$2"; shift 2 ;; + --title) TITLE="$2"; shift 2 ;; + --content-type) CONTENT_TYPE="$2"; shift 2 ;; + --source-folder) SOURCE_FOLDER="$2"; shift 2 ;; + --publish-url) PUBLISH_URL="$2"; shift 2 ;; + --publish-date) PUBLISH_DATE="$2"; shift 2 ;; + --notes) NOTES="$2"; shift 2 ;; + *) echo "{\"ok\":false,\"error\":\"unknown arg: $1\"}"; exit 1 ;; + esac +done + +# Validate required args +if [ -z "$PLATFORM" ] || [ -z "$TITLE" ] || [ -z "$CONTENT_TYPE" ] || [ -z "$SOURCE_FOLDER" ] || [ -z "$PUBLISH_DATE" ]; then + echo '{"ok":false,"error":"missing required args: --platform, --title, --content-type, --source-folder, --publish-date"}' + exit 1 +fi + +# Validate platform +TABLE="pub_${PLATFORM}" +VALID=$(sqlite3 "$DB" "SELECT name FROM sqlite_master WHERE type='table' AND name='$TABLE';") +if [ -z "$VALID" ]; then + echo "{\"ok\":false,\"error\":\"unknown platform: $PLATFORM (table $TABLE not found)\"}" + exit 1 +fi + +# Validate content_type +case "$CONTENT_TYPE" in + article|video|post) ;; + *) echo "{\"ok\":false,\"error\":\"invalid content_type: $CONTENT_TYPE (must be article/video/post)\"}"; exit 1 ;; +esac + +# Check duplicate +EXISTS=$(sqlite3 "$DB" "SELECT COUNT(*) FROM $TABLE WHERE source_folder='${SOURCE_FOLDER//\'/\'\'}';") +if [ "$EXISTS" -gt 0 ]; then + # Update existing record + ESC_URL="${PUBLISH_URL//\'/\'\'}" + ESC_NOTES="${NOTES//\'/\'\'}" + sqlite3 "$DB" "UPDATE $TABLE SET publish_url='$ESC_URL', notes='$ESC_NOTES', updated_at=strftime('%Y-%m-%d %H:%M:%S','now','localtime') WHERE source_folder='${SOURCE_FOLDER//\'/\'\'}';" + ID=$(sqlite3 "$DB" "SELECT id FROM $TABLE WHERE source_folder='${SOURCE_FOLDER//\'/\'\'}';") + echo "{\"ok\":true,\"action\":\"updated\",\"id\":$ID,\"table\":\"$TABLE\"}" +else + # Insert new record + ESC_TITLE="${TITLE//\'/\'\'}" + ESC_FOLDER="${SOURCE_FOLDER//\'/\'\'}" + ESC_URL="${PUBLISH_URL//\'/\'\'}" + ESC_NOTES="${NOTES//\'/\'\'}" + sqlite3 "$DB" "INSERT INTO $TABLE (title,content_type,source_folder,publish_url,publish_date,notes) VALUES ('$ESC_TITLE','$CONTENT_TYPE','$ESC_FOLDER','$ESC_URL','$PUBLISH_DATE','$ESC_NOTES');" + ID=$(sqlite3 "$DB" "SELECT last_insert_rowid();") + echo "{\"ok\":true,\"action\":\"inserted\",\"id\":$ID,\"table\":\"$TABLE\"}" +fi diff --git a/addons/officials/crew/selfmedia-operator/skills/published-track/scripts/update-metrics.sh b/addons/officials/crew/selfmedia-operator/skills/published-track/scripts/update-metrics.sh new file mode 100755 index 00000000..e586f070 --- /dev/null +++ b/addons/officials/crew/selfmedia-operator/skills/published-track/scripts/update-metrics.sh @@ -0,0 +1,84 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT="$(cd "$(dirname "$0")/../../.." && pwd)" +DB="$ROOT/db/published_track.db" + +if [ ! -f "$DB" ]; then + echo '{"ok":false,"error":"database not initialized, run init-db.sh first"}' + exit 1 +fi + +# Parse args +PLATFORM="" SOURCE_FOLDER="" +declare -A METRICS + +while [[ $# -gt 0 ]]; do + case "$1" in + --platform) PLATFORM="$2"; shift 2 ;; + --source-folder) SOURCE_FOLDER="$2"; shift 2 ;; + --*=*) + KEY="${1#--}" + VAL="${1#*=}" + METRICS["$KEY"]="$VAL" + shift + ;; + --*) + KEY="${1#--}" + VAL="$2" + METRICS["$KEY"]="$VAL" + shift 2 + ;; + *) echo "{\"ok\":false,\"error\":\"unknown arg: $1\"}"; exit 1 ;; + esac +done + +if [ -z "$PLATFORM" ] || [ -z "$SOURCE_FOLDER" ]; then + echo '{"ok":false,"error":"missing required args: --platform, --source-folder"}' + exit 1 +fi + +TABLE="pub_${PLATFORM}" +VALID=$(sqlite3 "$DB" "SELECT name FROM sqlite_master WHERE type='table' AND name='$TABLE';") +if [ -z "$VALID" ]; then + echo "{\"ok\":false,\"error\":\"unknown platform: $PLATFORM\"}" + exit 1 +fi + +# Check record exists +EXISTS=$(sqlite3 "$DB" "SELECT COUNT(*) FROM $TABLE WHERE source_folder='${SOURCE_FOLDER//\'/\'\'}';") +if [ "$EXISTS" -eq 0 ]; then + echo "{\"ok\":false,\"error\":\"no record found in $TABLE for source_folder=$SOURCE_FOLDER\"}" + exit 1 +fi + +# Get valid columns for this table (exclude id, created_at) +COLS=$(sqlite3 "$DB" "PRAGMA table_info($TABLE);" | awk -F'|' '{print $2}' | grep -v -E '^(id|created_at|source_folder|content_type|title|publish_date)$' | tr '\n' ' ') + +# Build SET clause +SET_PARTS=() +for KEY in "${!METRICS[@]}"; do + # Validate column exists + if ! echo " $COLS " | grep -q " $KEY "; then + echo "{\"ok\":false,\"error\":\"column '$KEY' not found in $TABLE. Valid metric columns: $COLS\"}" + exit 1 + fi + VAL="${METRICS[$KEY]}" + # Only allow integer or text values + ESC_VAL="${VAL//\'/\'\'}" + SET_PARTS+=("$KEY='$ESC_VAL'") +done + +if [ ${#SET_PARTS[@]} -eq 0 ]; then + echo '{"ok":false,"error":"no metrics provided to update"}' + exit 1 +fi + +# Always update updated_at +SET_PARTS+=("updated_at=strftime('%Y-%m-%d %H:%M:%S','now','localtime')") + +SET_CLAUSE=$(IFS=','; echo "${SET_PARTS[*]}") + +sqlite3 "$DB" "UPDATE $TABLE SET $SET_CLAUSE WHERE source_folder='${SOURCE_FOLDER//\'/\'\'}';" + +echo "{\"ok\":true,\"table\":\"$TABLE\",\"source_folder\":\"$SOURCE_FOLDER\",\"updated_columns\":${#METRICS[@]}}" diff --git a/addons/officials/crew/selfmedia-operator/skills/t2video/SKILL.md b/addons/officials/crew/selfmedia-operator/skills/t2video/SKILL.md index 1bc3b283..c356d6c7 100644 --- a/addons/officials/crew/selfmedia-operator/skills/t2video/SKILL.md +++ b/addons/officials/crew/selfmedia-operator/skills/t2video/SKILL.md @@ -1,12 +1,14 @@ --- name: t2video -description: 一站式短视频制作工具。整合 Edge TTS(免费)/ SiliconFlow TTS 语音合成、SiliconFlow 文生视频/图生视频和 FFmpeg 组装,从脚本到成品视频一步完成。支持用户指定素材的位置编排。 +description: 一站式短视频制作工具。整合 SiliconFlow TTS 语音合成、素材搜集(Pexels/Pixabay/AI 生成)和 FFmpeg 组装,从脚本到成品视频一步完成。无 subagent 模式,适合短小视频。 metadata: openclaw: emoji: "🎥" requires: bins: - python3 + - ffmpeg + - ffprobe env: - SILICONFLOW_API_KEY primaryEnv: SILICONFLOW_API_KEY @@ -16,288 +18,198 @@ metadata: Use this skill when: - 需要从脚本生成完整短视频(TTS + 素材 + 组装) -- 需要生成 TTS 语音旁白(Edge TTS 免费 / SiliconFlow 备选) -- 需要生成 AI 视频片段 -- 需要将用户提供的素材(视频/图片)按指定位置编入视频 -- 需要将已有素材 + 音频组装成最终视频 +- 用户指定主题和已有素材,生成短视频 -**三种模式**: - -| 模式 | 用途 | 输入 | 输出 | -|------|------|------|------| -| `tts` | 语音合成 | 文本 | 音频文件 | -| `gen` | AI 视频生成 | 文本描述/图片 | MP4 视频片段 | -| `compose` | 视频组装 | 素材 + 音频 + 编排计划 | 最终 MP4 | +**本技能是 video-producer 的简化版**:适合短小视频制作,且只生成9:16的竖屏视频。 **不烧录字幕**:大部分平台支持自动生成字幕,无需手动烧录。 --- -## tts — 语音合成 +## ⚙️ 执行方式(强制) -**默认使用 Edge TTS**(完全免费,无需 API Key)。SiliconFlow 为备选(需要 API Key,音质更好)。 +本技能涉及多步骤生产流程,你应该 self-spawn 一个 subagent 来执行,原因:subagent 独立上下文,不会因对话历史积累而降低输出质量。 + +你只负责跟进subagent的执行,避免它们长时间卡在某个步骤,必要时可以提供提示或调整执行策略。另外在关键节点要求它向你汇报,你检查后再让它继续执行下一步。 + +--- + +## 工作流程 + +### Step 1 — 工作区目录准备 + +在 `output_videos/` 下创建项目文件夹,如 `output_videos//`,作为 project-dir。 + +工作区结构: -```bash -# 默认 edge-tts(免费) -python3 ./skills/t2video/scripts/t2video.py tts \ - --text "大家好,欢迎来到今天的视频。" - -# 指定语音 -python3 ./skills/t2video/scripts/t2video.py tts \ - --text "99%的人都不知道这个技巧" \ - --voice zh-CN-YunxiNeural - -# 从文件读取长文本 -python3 ./skills/t2video/scripts/t2video.py tts \ - --text-file ./output_video/my-video/script.txt \ - --out-dir ./output_video/my-video/fragments - -# SiliconFlow TTS(备选,需要 SILICONFLOW_API_KEY) -python3 ./skills/t2video/scripts/t2video.py tts \ - --provider siliconflow \ - --text "这是演示旁白。" \ - --voice "FunAudioLLM/CosyVoice2-0.5B:anna" \ - --format wav +``` +/ +├── script.md # 三段式脚本 +├── tts_requirement.md # 配音需求(由 Step 3 创建) +├── artifacts/ # 产出素材(TTS音频+视频素材) +│ ├── speech.mp3 # TTS 配音 +│ ├── speech.json # TTS 元数据(含 duration) +│ └── --output [--plan ] -""" - -import argparse -import asyncio -import contextlib -import glob -import io -import json -import os -import re -import subprocess -import sys -import tempfile -import time -import urllib.error -import urllib.request -from datetime import datetime -from pathlib import Path - - -# ════════════════════════════════════════════════════════════════════════════════ -# Shared Utilities -# ════════════════════════════════════════════════════════════════════════════════ - - -def log(msg: str) -> None: - ts = datetime.now().strftime("%H:%M:%S") - print(f"[{ts}] {msg}", flush=True) - - -def err(msg: str) -> None: - print(f"[ERROR] {msg}", file=sys.stderr, flush=True) - - -def die(msg: str) -> None: - err(msg) - sys.exit(1) - - -def ensure_dir(path: str) -> Path: - p = Path(path) - p.mkdir(parents=True, exist_ok=True) - return p - - -def slugify(text: str, max_len: int = 20) -> str: - text = re.sub(r"[^\w一-鿿]+", "-", text).strip("-") - return text[:max_len] - - -def _suppress_stdout(): - """Context manager to suppress MoviePy v2 FFMPEG_VideoReader stdout noise.""" - return contextlib.redirect_stdout(io.StringIO()) - - -def _ffmpeg_available() -> bool: - """Check if ffmpeg is available on the system.""" - try: - result = subprocess.run( - ["ffmpeg", "-version"], - capture_output=True, - text=True, - timeout=5, - ) - return result.returncode == 0 - except (FileNotFoundError, subprocess.TimeoutExpired): - return False - - -# ════════════════════════════════════════════════════════════════════════════════ -# Plan Utilities — Positioned Material Insertion -# ════════════════════════════════════════════════════════════════════════════════ - - -def parse_plan(plan_path: str) -> dict | None: - """Parse a compose plan JSON file. - - Returns dict with keys: audio (str), aspect (str), tracks (list[dict]). - Each track has: position (str), material (str), duration (float|None). - Returns None if plan_path is empty/None. - """ - if not plan_path: - return None - path = Path(plan_path) - if not path.is_file(): - die(f"Plan file not found: {plan_path}") - try: - data = json.loads(path.read_text(encoding="utf-8")) - except (json.JSONDecodeError, OSError) as exc: - die(f"Failed to read plan file: {exc}") - - tracks = data.get("tracks", []) - if not isinstance(tracks, list): - die("Plan 'tracks' must be a list") - - for i, track in enumerate(tracks): - if "position" not in track: - die(f"Track #{i} missing 'position'") - if "material" not in track: - die(f"Track #{i} missing 'material'") - if not os.path.isfile(track["material"]): - die(f"Track #{i} material not found: {track['material']}") - - return data - - -def resolve_position(position: str, total_duration: float) -> float: - """Convert a position specification to seconds. - - Supports: - - Percentage: "0%", "50%", "100%" - - Seconds: "5.0", "12.5" - """ - if isinstance(position, (int, float)): - return float(position) - - position = str(position).strip() - - if position.endswith("%"): - try: - pct = float(position[:-1]) - except ValueError: - die(f"Invalid position percentage: {position}") - return total_duration * (pct / 100.0) - - try: - return float(position) - except ValueError: - die(f"Invalid position: {position}. Use percentage ('50%') or seconds ('5.0')") - - -# ════════════════════════════════════════════════════════════════════════════════ -# TTS Mode — Edge TTS (default) / SiliconFlow TTS (fallback) -# ════════════════════════════════════════════════════════════════════════════════ - -# ── Edge TTS ───────────────────────────────────────────────────────────────── - -EDGE_TTS_DEFAULT_VOICE = "zh-CN-XiaoxiaoNeural" -EDGE_TTS_VALID_VOICES = { - "zh-CN-XiaoxiaoNeural", - "zh-CN-XiaoyiNeural", - "zh-CN-YunjianNeural", - "zh-CN-YunxiNeural", - "zh-CN-YunxiaNeural", - "zh-CN-YunyangNeural", - "zh-CN-liaoning-XiaobeiNeural", - "zh-CN-shaanxi-XiaoniNeural", - "en-US-JennyNeural", - "en-US-GuyNeural", - "en-US-AriaNeural", - "en-US-DavisNeural", - "ja-JP-NanamiNeural", - "ja-JP-KeitaNeural", - "ko-KR-SunHiNeural", - "ko-KR-InJoonNeural", -} - -# ── SiliconFlow TTS ───────────────────────────────────────────────────────── - -SF_TTS_DEFAULT_API_BASE = "https://api.siliconflow.cn/v1" -SF_TTS_DEFAULT_MODEL = "FunAudioLLM/CosyVoice2-0.5B" -SF_TTS_DEFAULT_VOICE = "FunAudioLLM/CosyVoice2-0.5B:alex" -SF_TTS_VALID_FORMATS = {"mp3", "opus", "wav", "pcm"} -SF_TTS_VALID_VOICES = { - "FunAudioLLM/CosyVoice2-0.5B:alex", - "FunAudioLLM/CosyVoice2-0.5B:anna", - "FunAudioLLM/CosyVoice2-0.5B:bella", - "FunAudioLLM/CosyVoice2-0.5B:benjamin", - "FunAudioLLM/CosyVoice2-0.5B:charles", - "FunAudioLLM/CosyVoice2-0.5B:claire", - "FunAudioLLM/CosyVoice2-0.5B:david", - "FunAudioLLM/CosyVoice2-0.5B:diana", -} - -# ── Common TTS ─────────────────────────────────────────────────────────────── - -TTS_SAFE_INPUT_DIRS = ( - Path("scripts"), - Path("assets"), - Path("tmp"), - Path("output_videos"), - Path("output_video"), -) -TTS_SAFE_OUTPUT_DIRS = ( - Path("assets/audio"), - Path("tmp"), - Path("output_videos"), - Path("output_video"), -) -TTS_TEXT_EXTENSIONS = {".txt", ".md"} -TTS_MAX_TEXT_FILE_BYTES = 512 * 1024 - - -def _tts_ensure_safe_path( - raw_path: str, allowed_dirs: tuple[Path, ...], purpose: str -) -> Path: - path = Path(raw_path) - if path.is_absolute(): - die(f"{purpose} path must be relative to the workspace") - if ".." in path.parts: - die(f"{purpose} path must not contain '..'") - root = Path.cwd().resolve() - resolved = (root / path).resolve() - if not any( - resolved == (root / base).resolve() - or resolved.is_relative_to((root / base).resolve()) - for base in allowed_dirs - ): - allowed = ", ".join(str(base) for base in allowed_dirs) - die(f"{purpose} path must be under one of: {allowed}") - return resolved - - -def _tts_read_text_file(raw_path: str) -> str: - path = _tts_ensure_safe_path(raw_path, TTS_SAFE_INPUT_DIRS, "--text-file") - if path.suffix.lower() not in TTS_TEXT_EXTENSIONS: - die( - f"--text-file must use one of these extensions: " - f"{', '.join(sorted(TTS_TEXT_EXTENSIONS))}" - ) - if not path.is_file(): - die(f"--text-file does not exist or is not a file: {raw_path}") - if path.stat().st_size > TTS_MAX_TEXT_FILE_BYTES: - die(f"--text-file exceeds {TTS_MAX_TEXT_FILE_BYTES} bytes") - try: - return path.read_text(encoding="utf-8") - except UnicodeDecodeError: - die("--text-file must be UTF-8 encoded") - except OSError as exc: - die(f"failed to read --text-file: {exc}") - raise AssertionError("unreachable") - - -def _tts_read_text(args: argparse.Namespace) -> str: - if args.text and args.text_file: - die("Use either --text or --text-file, not both") - if args.text_file: - text = _tts_read_text_file(args.text_file) - elif args.text: - text = args.text - else: - die("Either --text or --text-file is required") - text = text.strip() - if not text: - die("Input text is empty") - return text - - -def _tts_resolve_output_path(args: argparse.Namespace, ext: str) -> Path: - raw_path = args.output or str( - ( - Path(args.out_dir) - if args.out_dir - else Path(f"tmp/tts-{int(time.time())}") - ) - / f"speech.{ext}" - ) - output_path = _tts_ensure_safe_path(raw_path, TTS_SAFE_OUTPUT_DIRS, "output") - return output_path - - -# ── Edge TTS Implementation ────────────────────────────────────────────────── - - -def _edge_tts_available() -> bool: - try: - import edge_tts # noqa: F401 - - return True - except ImportError: - return False - - -def _edge_tts_validate_args(args: argparse.Namespace) -> None: - voice = args.voice or EDGE_TTS_DEFAULT_VOICE - if voice not in EDGE_TTS_VALID_VOICES: - die( - f"Unsupported edge-tts voice: {voice}. " - f"Valid voices: {', '.join(sorted(EDGE_TTS_VALID_VOICES))}" - ) - if args.speed is not None and not -50 <= args.speed <= 50: - die("--speed for edge-tts must be between -50 and 50 (percentage)") - - -def _edge_tts_run( - text: str, - voice: str, - rate: int | None, - output_path: Path, -) -> Path: - """Run edge-tts. Returns audio output path.""" - import edge_tts - - rate_str = f"{rate:+d}%" if rate is not None else "+0%" - - async def _do() -> bytearray: - communicate = edge_tts.Communicate(text, voice, rate=rate_str) - audio_data = bytearray() - - async for chunk in communicate.stream(): - if chunk["type"] == "audio": - audio_data.extend(chunk["data"]) - - if not audio_data: - die("edge-tts produced no audio output") - return audio_data - - loop = asyncio.new_event_loop() - try: - audio_data = loop.run_until_complete(_do()) - finally: - loop.close() - - output_path.parent.mkdir(parents=True, exist_ok=True) - output_path.write_bytes(bytes(audio_data)) - - return output_path - - -# ── SiliconFlow TTS Implementation ─────────────────────────────────────────── - - -def _sf_tts_validate_args(args: argparse.Namespace) -> None: - if args.sf_model != SF_TTS_DEFAULT_MODEL: - die(f"Unsupported SiliconFlow model: {args.sf_model}. Use {SF_TTS_DEFAULT_MODEL}") - voice = args.voice or SF_TTS_DEFAULT_VOICE - if voice and voice not in SF_TTS_VALID_VOICES: - die( - f"Unsupported SiliconFlow voice: {voice}. " - f"Valid voices: {', '.join(sorted(SF_TTS_VALID_VOICES))}" - ) - if len(args.text) > 128000: - die("Input text exceeds 128000 characters") - - -def _sf_tts_build_payload(args: argparse.Namespace, text: str) -> dict: - voice = args.voice or SF_TTS_DEFAULT_VOICE - payload: dict = { - "model": args.sf_model, - "input": text, - "voice": voice, - "response_format": args.format, - } - if args.sample_rate is not None: - payload["sample_rate"] = args.sample_rate - if args.speed is not None: - payload["speed"] = args.speed - return payload - - -def _sf_tts_create_speech(api_base: str, api_key: str, payload: dict) -> bytes: - data = json.dumps(payload, ensure_ascii=False).encode("utf-8") - req = urllib.request.Request( - f"{api_base.rstrip('/')}/audio/speech", - data=data, - headers={ - "Authorization": f"Bearer {api_key}", - "Content-Type": "application/json", - }, - method="POST", - ) - try: - with urllib.request.urlopen(req, timeout=120) as resp: - return resp.read() - except urllib.error.HTTPError as exc: - body = exc.read().decode(errors="replace") - die(f"HTTP {exc.code}: {body}") - except urllib.error.URLError as exc: - die(f"request failed: {exc.reason}") - raise AssertionError("unreachable") - - -def _sf_tts_run( - text: str, - args: argparse.Namespace, - output_path: Path, -) -> Path: - """Run SiliconFlow TTS. Returns audio output path.""" - payload = _sf_tts_build_payload(args, text) - - api_key = os.environ.get("SILICONFLOW_API_KEY", "").strip() - if not api_key: - die("SILICONFLOW_API_KEY not set") - api_base = ( - os.environ.get("SILICONFLOW_API_BASE", SF_TTS_DEFAULT_API_BASE).strip() - or SF_TTS_DEFAULT_API_BASE - ) - - output_path.parent.mkdir(parents=True, exist_ok=True) - audio = _sf_tts_create_speech(api_base, api_key, payload) - if not audio: - die("empty audio response") - output_path.write_bytes(audio) - - return output_path - - -# ── TTS Main ───────────────────────────────────────────────────────────────── - - -def tts_main(args: argparse.Namespace) -> None: - text = _tts_read_text(args) - provider = args.provider - - if provider == "edge": - if not _edge_tts_available(): - die("edge-tts is not installed. Run: pip install edge-tts>=7.0") - _edge_tts_validate_args(args) - fmt = "mp3" - output_path = _tts_resolve_output_path(args, fmt) - voice = args.voice or EDGE_TTS_DEFAULT_VOICE - log(f"TTS [edge]: voice={voice} chars={len(text)}") - audio_path = _edge_tts_run(text, voice, args.speed, output_path) - log(f"TTS done: {audio_path}") - - elif provider == "siliconflow": - _sf_tts_validate_args(args) - output_path = _tts_resolve_output_path(args, args.format) - voice = args.voice or SF_TTS_DEFAULT_VOICE - log(f"TTS [siliconflow]: voice={voice} format={args.format} chars={len(text)}") - audio_path = _sf_tts_run(text, args, output_path) - log(f"TTS done: {audio_path}") - - else: - die(f"Unknown TTS provider: {provider}. Use 'edge' or 'siliconflow'.") - - # Save metadata - meta = { - "mode": "tts", - "provider": provider, - "voice": args.voice or ( - EDGE_TTS_DEFAULT_VOICE if provider == "edge" else SF_TTS_DEFAULT_VOICE - ), - "text_chars": len(text), - "audio_file": str(audio_path), - } - meta_path = audio_path.with_suffix(".json") - meta_path.write_text(json.dumps(meta, ensure_ascii=False, indent=2), encoding="utf-8") - - -# ════════════════════════════════════════════════════════════════════════════════ -# Gen Mode — SiliconFlow Video Generation -# ════════════════════════════════════════════════════════════════════════════════ - -GEN_SUBMIT_URL = "https://api.siliconflow.cn/v1/video/submit" -GEN_STATUS_URL = "https://api.siliconflow.cn/v1/video/status" - -GEN_T2V_MODEL = "Wan-AI/Wan2.2-T2V-A14B" -GEN_I2V_MODEL = "Wan-AI/Wan2.2-I2V-A14B" -GEN_VALID_SIZES = {"1280x720", "720x1280", "960x960"} - - -def _gen_post_json(url: str, payload: dict, api_key: str, timeout: int = 60) -> dict: - data = json.dumps(payload).encode() - req = urllib.request.Request( - url, - data=data, - headers={ - "Authorization": f"Bearer {api_key}", - "Content-Type": "application/json", - }, - method="POST", - ) - try: - with urllib.request.urlopen(req, timeout=timeout) as resp: - return json.loads(resp.read()) - except urllib.error.HTTPError as e: - body = e.read().decode(errors="replace") - die(f"HTTP {e.code}: {body}") - except urllib.error.URLError as exc: - die(f"request failed: {exc.reason}") - raise AssertionError("unreachable") - - -def _gen_submit_job(payload: dict, api_key: str) -> str: - result = _gen_post_json(GEN_SUBMIT_URL, payload, api_key, timeout=60) - rid = result.get("requestId") - if not rid: - die(f"No requestId in response: {result}") - return rid - - -def _gen_poll_until_done( - request_id: str, api_key: str, poll_interval: int, timeout: int -) -> dict: - deadline = time.time() + timeout - attempt = 0 - while time.time() < deadline: - attempt += 1 - result = _gen_post_json( - GEN_STATUS_URL, {"requestId": request_id}, api_key, timeout=30 - ) - status = result.get("status", "") - log(f"Gen poll #{attempt}: status={status}") - if status == "Succeed": - return result - if status == "Failed": - reason = result.get("reason", "unknown") - die(f"Generation failed: {reason}") - time.sleep(poll_interval) - die(f"Timed out after {timeout}s") - - -def _gen_download_video(url: str, dest_path: Path) -> None: - log(f"Downloading video -> {dest_path}") - req = urllib.request.Request(url, headers={"User-Agent": "wiseflow-t2video/1.0"}) - with urllib.request.urlopen(req, timeout=300) as resp: - dest_path.write_bytes(resp.read()) - - -def gen_main(args: argparse.Namespace) -> None: - if args.model == GEN_I2V_MODEL and not args.image: - die(f"--image is required when using model '{GEN_I2V_MODEL}'") - - api_key = os.environ.get("SILICONFLOW_API_KEY", "").strip() - if not api_key: - die("SILICONFLOW_API_KEY not set") - - ts = int(time.time()) - out_dir = Path(args.out_dir) if args.out_dir else Path(f"./tmp/sf-video-{ts}") - out_dir.mkdir(parents=True, exist_ok=True) - - payload: dict = { - "model": args.model, - "prompt": args.prompt, - "image_size": args.image_size, - } - if args.image: - payload["image"] = args.image - if args.negative_prompt: - payload["negative_prompt"] = args.negative_prompt - if args.seed is not None: - payload["seed"] = args.seed - - log(f"Gen: submitting job model={args.model} size={args.image_size}") - request_id = _gen_submit_job(payload, api_key) - log(f"Gen: jobId={request_id}") - log(f"Gen: polling every {args.poll_interval}s (timeout={args.timeout}s)") - - result = _gen_poll_until_done( - request_id, api_key, args.poll_interval, args.timeout - ) - - videos = result.get("results", {}).get("videos", []) - if not videos: - die(f"No videos in result: {result}") - - video_url = videos[0].get("url", "") - if not video_url: - die("Empty video URL in result") - - video_path = out_dir / f"video_{request_id[:8]}.mp4" - _gen_download_video(video_url, video_path) - - result_path = out_dir / "result.json" - result_path.write_text(json.dumps(result, ensure_ascii=False, indent=2)) - - log(f"Gen done: {video_path}") - log(f"Metadata: {result_path}") - - -# ════════════════════════════════════════════════════════════════════════════════ -# Compose Mode — Video Assembly -# ════════════════════════════════════════════════════════════════════════════════ - -# ── MoviePy Version Detection ──────────────────────────────────────────────── - - -def _get_moviepy_major() -> int: - try: - import moviepy - - return int(moviepy.__version__.split(".")[0]) - except Exception: - return 2 - - -_MOVIEPY_MAJOR = _get_moviepy_major() - -# ── Constants ──────────────────────────────────────────────────────────────── - -ASPECT_RESOLUTIONS = { - "9:16": (1080, 1920), - "16:9": (1920, 1080), - "1:1": (1080, 1080), -} - -VIDEO_EXTS = {".mp4", ".mov", ".avi", ".webm", ".mkv"} -IMAGE_EXTS = {".jpg", ".jpeg", ".png", ".webp", ".bmp"} - -PLACEHOLDER_COLOR = (30, 30, 50) -SEGMENT_CODEC = "libx264" -SEGMENT_FPS = 30 -SEGMENT_PIX_FMT = "yuv420p" - -# ── Audio Helpers ──────────────────────────────────────────────────────────── - - -def get_audio_duration(audio_path: str) -> float: - from moviepy import AudioFileClip - - clip = None - try: - with _suppress_stdout(): - clip = AudioFileClip(audio_path) - duration = clip.duration - return duration - finally: - if clip is not None: - clip.close() - - -# ── Material Collection ────────────────────────────────────────────────────── - - -def collect_materials(materials_dir: str) -> list[dict]: - items: list[dict] = [] - for ext in VIDEO_EXTS: - for p in glob.glob(os.path.join(materials_dir, f"*{ext}")): - items.append({"path": p, "type": "video"}) - for p in glob.glob(os.path.join(materials_dir, f"*{ext.upper()}")): - items.append({"path": p, "type": "video"}) - for ext in IMAGE_EXTS: - for p in glob.glob(os.path.join(materials_dir, f"*{ext}")): - items.append({"path": p, "type": "image"}) - for p in glob.glob(os.path.join(materials_dir, f"*{ext.upper()}")): - items.append({"path": p, "type": "image"}) - return sorted(items, key=lambda x: x["path"]) - - -# ── FFmpeg Concat ──────────────────────────────────────────────────────────── - - -def concat_video_clips_with_ffmpeg( - segment_paths: list[str], - output_path: str, - audio_path: str, -) -> str: - if not segment_paths: - raise ValueError("No segment paths provided for ffmpeg concat") - - fd, concat_list_path = tempfile.mkstemp(suffix=".txt", prefix="t2v_concat_") - os.close(fd) - concat_output = output_path + ".concat_tmp.mp4" - - try: - with open(concat_list_path, "w", encoding="utf-8") as f: - for seg_path in segment_paths: - escaped = seg_path.replace("'", "'\\''") - f.write(f"file '{escaped}'\n") - - log(f"FFmpeg concat: joining {len(segment_paths)} segments (stream copy)") - cmd_concat = [ - "ffmpeg", "-y", "-f", "concat", "-safe", "0", - "-i", concat_list_path, "-c", "copy", concat_output, - ] - result = subprocess.run(cmd_concat, capture_output=True, text=True) - if result.returncode != 0: - log("FFmpeg concat: stream copy failed, falling back to re-encode") - cmd_reencode = [ - "ffmpeg", "-y", "-f", "concat", "-safe", "0", - "-i", concat_list_path, - "-c:v", SEGMENT_CODEC, "-pix_fmt", SEGMENT_PIX_FMT, - "-r", str(SEGMENT_FPS), concat_output, - ] - result = subprocess.run(cmd_reencode, capture_output=True, text=True) - if result.returncode != 0: - raise RuntimeError( - f"FFmpeg concat failed (re-encode): {result.stderr[-500:]}" - ) - - log("FFmpeg mux: adding audio track") - cmd_mux = [ - "ffmpeg", "-y", - "-i", concat_output, "-i", audio_path, - "-c:v", "copy", "-c:a", "aac", "-b:a", "192k", - "-map", "0:v:0", "-map", "1:a:0", - "-shortest", output_path, - ] - result = subprocess.run(cmd_mux, capture_output=True, text=True) - if result.returncode != 0: - raise RuntimeError(f"FFmpeg audio mux failed: {result.stderr[-500:]}") - - finally: - for path in (concat_list_path, concat_output): - if os.path.exists(path): - try: - os.remove(path) - except OSError: - pass - - return output_path - - -# ── Video Composition (MoviePy) ────────────────────────────────────────────── - - -def make_placeholder_clip(duration: float, resolution: tuple, color: tuple): - from moviepy import ColorClip - - return ColorClip(size=resolution, color=color, duration=duration) - - -def image_to_clip(image_path: str, duration: float, resolution: tuple): - from moviepy import ImageClip - - with _suppress_stdout(): - clip = ImageClip(image_path, duration=duration) - clip = clip.resized(resolution) - if clip.size[0] != resolution[0] or clip.size[1] != resolution[1]: - x_center = clip.size[0] / 2 - y_center = clip.size[1] / 2 - clip = clip.cropped( - x_center=x_center, - y_center=y_center, - width=resolution[0], - height=resolution[1], - ) - return clip - - -def video_to_clip(video_path: str, max_duration: float, resolution: tuple): - from moviepy import VideoFileClip - - with _suppress_stdout(): - clip = VideoFileClip(video_path).without_audio() - if clip.duration > max_duration: - clip = clip.subclipped(0, max_duration) - clip = clip.resized(resolution) - if clip.size[0] != resolution[0] or clip.size[1] != resolution[1]: - x_center = clip.size[0] / 2 - y_center = clip.size[1] / 2 - clip = clip.cropped( - x_center=x_center, - y_center=y_center, - width=resolution[0], - height=resolution[1], - ) - return clip - - -def _apply_crossfade(clip, duration: float = 0.3): - if _MOVIEPY_MAJOR >= 2: - from moviepy.video.fx.CrossFadeIn import CrossFadeIn - - return clip.with_effects([CrossFadeIn(duration)]) - else: - return clip.crossfadein(duration) - - -def _concat_clips(clips: list): - from moviepy import concatenate_videoclips - - if _MOVIEPY_MAJOR >= 2: - return concatenate_videoclips(clips) - else: - return concatenate_videoclips(clips, method="compose") - - -def _write_clip(clip, output_path: str, *, audio: bool = True, **kwargs) -> None: - write_kwargs: dict = { - "fps": SEGMENT_FPS, - "codec": SEGMENT_CODEC, - "logger": None, - } - if audio: - write_kwargs["audio_codec"] = "aac" - write_kwargs["audio_fps"] = 44100 - write_kwargs["audio_bitrate"] = "192k" - else: - write_kwargs["audio"] = False - - if _MOVIEPY_MAJOR >= 2: - write_kwargs["pixel_format"] = SEGMENT_PIX_FMT - else: - write_kwargs["verbose"] = False - - write_kwargs.update(kwargs) - clip.write_videofile(output_path, **write_kwargs) - - -# ── Timeline Builder ───────────────────────────────────────────────────────── - - -def _build_timeline( - plan: dict | None, - audio_duration: float, -) -> list[dict]: - """Build a timeline of segments from plan tracks. - - Each segment: {"start": float, "end": float, "material": str, "type": str} - - type is "video" or "image" for plan tracks, "auto" for gap-fill from materials-dir - """ - if not plan or not plan.get("tracks"): - return [{"start": 0.0, "end": audio_duration, "material": "", "type": "auto"}] - - tracks = sorted( - plan["tracks"], - key=lambda t: resolve_position(t["position"], audio_duration), - ) - - segments: list[dict] = [] - prev_end = 0.0 - - for track in tracks: - start = resolve_position(track["position"], audio_duration) - if start < prev_end: - err( - f"Warning: track at {track['position']} overlaps previous segment, " - f"clamping start to {prev_end:.1f}s" - ) - start = prev_end - - # Gap before this track - if start > prev_end + 0.01: - segments.append( - {"start": prev_end, "end": start, "material": "", "type": "auto"} - ) - - # Determine material type and duration - mat_path = track["material"] - mat_ext = Path(mat_path).suffix.lower() - remaining = audio_duration - start - - if mat_ext in VIDEO_EXTS: - mat_type = "video" - from moviepy import VideoFileClip - - with _suppress_stdout(): - vc = VideoFileClip(mat_path) - mat_dur = vc.duration - vc.close() - mat_dur = min(mat_dur, remaining) - elif mat_ext in IMAGE_EXTS: - mat_type = "image" - mat_dur = track.get("duration") - if not mat_dur: - die(f"Image material '{mat_path}' must specify 'duration' in plan") - mat_dur = min(float(mat_dur), remaining) - else: - die(f"Unsupported material type: {mat_path}") - - end = start + mat_dur - segments.append( - {"start": start, "end": end, "material": mat_path, "type": mat_type} - ) - prev_end = end - - # Gap after last track - if prev_end < audio_duration - 0.01: - segments.append( - {"start": prev_end, "end": audio_duration, "material": "", "type": "auto"} - ) - - return segments - - -# ── Main Composition ───────────────────────────────────────────────────────── - - -def compose_video( - materials: list[dict], - audio_path: str, - output_path: str, - aspect: str, - clip_duration: float, - transition: str, - plan: dict | None = None, -) -> str: - from moviepy import AudioFileClip - - resolution = ASPECT_RESOLUTIONS.get(aspect, (1080, 1920)) - audio_duration = get_audio_duration(audio_path) - log(f"Audio duration: {audio_duration:.1f}s") - - video_items = [m for m in materials if m["type"] == "video"] - image_items = [m for m in materials if m["type"] == "image"] - - if video_items: - primary = video_items - primary_type = "video" - elif image_items: - primary = image_items - primary_type = "image" - else: - primary = [] - primary_type = "none" - - plan_tracks = plan.get("tracks", []) if plan else [] - log( - f"Compose: {len(primary)} {primary_type} materials, " - f"{len(plan_tracks)} plan tracks, " - f"duration={audio_duration:.1f}s, aspect={aspect}" - ) - - # ── Build timeline ────────────────────────────────────────────────── - timeline = _build_timeline(plan, audio_duration) - - # ── Build segment clips from timeline ─────────────────────────────── - clips = [] - auto_idx = 0 - - for segment in timeline: - seg_dur = segment["end"] - segment["start"] - if seg_dur <= 0.01: - continue - - if segment["type"] == "auto": - if not primary: - clips.append( - make_placeholder_clip(seg_dur, resolution, PLACEHOLDER_COLOR) - ) - continue - remaining = seg_dur - while remaining > 0.01: - item = primary[auto_idx % len(primary)] - dur = min(clip_duration, remaining) - try: - if item["type"] == "image" or primary_type == "image": - clip = image_to_clip(item["path"], dur, resolution) - else: - clip = video_to_clip(item["path"], dur, resolution) - except Exception as e: - err(f"Failed to load '{item['path']}': {e} — using placeholder") - clip = make_placeholder_clip(dur, resolution, PLACEHOLDER_COLOR) - clips.append(clip) - remaining -= dur - auto_idx += 1 - elif segment["type"] == "video": - try: - clip = video_to_clip(segment["material"], seg_dur, resolution) - except Exception as e: - err(f"Failed to load '{segment['material']}': {e} — using placeholder") - clip = make_placeholder_clip(seg_dur, resolution, PLACEHOLDER_COLOR) - clips.append(clip) - elif segment["type"] == "image": - try: - clip = image_to_clip(segment["material"], seg_dur, resolution) - except Exception as e: - err(f"Failed to load '{segment['material']}': {e} — using placeholder") - clip = make_placeholder_clip(seg_dur, resolution, PLACEHOLDER_COLOR) - clips.append(clip) - - if not clips: - log("Compose: no clips generated — using placeholder") - with _suppress_stdout(): - audio = AudioFileClip(audio_path) - try: - placeholder = make_placeholder_clip( - audio_duration, resolution, PLACEHOLDER_COLOR - ) - final = placeholder.with_audio(audio) - _write_clip(final, output_path) - finally: - audio.close() - return output_path - - # ── Concatenate video clips ────────────────────────────────────────── - use_ffmpeg = transition == "none" and _ffmpeg_available() - - if use_ffmpeg: - log(f"Compose: using ffmpeg concat path ({len(clips)} segments)") - tmpdir = tempfile.mkdtemp(prefix="t2v_segments_") - segment_paths: list[str] = [] - try: - for i, clip in enumerate(clips): - seg_path = os.path.join(tmpdir, f"seg_{i:04d}.mp4") - _write_clip(clip, seg_path, audio=False) - segment_paths.append(seg_path) - clip.close() - - concat_video_clips_with_ffmpeg(segment_paths, output_path, audio_path) - finally: - for seg_path in segment_paths: - if os.path.exists(seg_path): - try: - os.remove(seg_path) - except OSError: - pass - try: - os.rmdir(tmpdir) - except OSError: - pass - else: - log(f"Compose: using MoviePy path (transition={transition})") - - if transition == "fade" and len(clips) > 1: - faded = [] - for i, c in enumerate(clips): - if i > 0: - c = _apply_crossfade(c, 0.3) - faded.append(c) - clips = faded - - video = _concat_clips(clips) - - with _suppress_stdout(): - audio = AudioFileClip(audio_path) - try: - video = ( - video.with_audio(audio) - .with_duration(audio_duration) - .with_fps(SEGMENT_FPS) - ) - _write_clip(video, output_path) - finally: - audio.close() - - log(f"Compose: written -> {output_path}") - return output_path - - -def compose_main(args: argparse.Namespace) -> None: - if not os.path.exists(args.audio): - die(f"Audio file not found: {args.audio}") - if not os.path.isdir(args.materials_dir): - die(f"Materials directory not found: {args.materials_dir}") - - # Parse plan if provided - plan = parse_plan(getattr(args, "plan", None)) - - materials = collect_materials(args.materials_dir) - if not materials and not (plan and plan.get("tracks")): - err( - f"Warning: no video/image files found in {args.materials_dir} " - "— will use placeholder" - ) - - ensure_dir(os.path.dirname(args.output) or ".") - - compose_video( - materials=materials, - audio_path=args.audio, - output_path=args.output, - aspect=args.aspect, - clip_duration=args.clip_duration, - transition=args.transition, - plan=plan, - ) - - # Save metadata - audio_duration = get_audio_duration(args.audio) - meta = { - "mode": "compose", - "audio": args.audio, - "materials_dir": args.materials_dir, - "material_count": len(materials), - "audio_duration": round(audio_duration, 1), - "aspect": args.aspect, - "transition": args.transition, - "plan": getattr(args, "plan", None), - "output": args.output, - "generated_at": datetime.now().isoformat(), - } - meta_path = os.path.splitext(args.output)[0] + ".json" - with open(meta_path, "w", encoding="utf-8") as f: - json.dump(meta, f, ensure_ascii=False, indent=2) - - log(f"Done: {args.output}") - - -# ════════════════════════════════════════════════════════════════════════════════ -# Entry Point — Subcommand Router -# ════════════════════════════════════════════════════════════════════════════════ - - -def main() -> None: - parser = argparse.ArgumentParser( - description="t2video — One-stop Short Video Production", - ) - subparsers = parser.add_subparsers(dest="mode", help="Operation mode") - - # ── tts subcommand ──────────────────────────────────────────────────── - tts_parser = subparsers.add_parser("tts", help="Text-to-speech") - tts_parser.add_argument("--text", default=None, help="Text to synthesize") - tts_parser.add_argument( - "--text-file", default=None, dest="text_file", help="UTF-8 text file" - ) - tts_parser.add_argument( - "--provider", - default="edge", - choices=["edge", "siliconflow"], - help="TTS provider: edge (default, free) or siliconflow", - ) - tts_parser.add_argument( - "--voice", - default=None, - help="Voice ID (provider-specific, see docs)", - ) - tts_parser.add_argument( - "--format", - default="mp3", - choices=sorted(SF_TTS_VALID_FORMATS), - help="Audio response format (siliconflow only)", - ) - tts_parser.add_argument( - "--speed", - type=int, - default=None, - help="Speech speed. Edge: -50 to 50 (percentage). SiliconFlow: 0.25 to 4.0", - ) - tts_parser.add_argument( - "--sample-rate", - type=int, - default=None, - dest="sample_rate", - help="Output sample rate (siliconflow only)", - ) - tts_parser.add_argument( - "--sf-model", - default=SF_TTS_DEFAULT_MODEL, - dest="sf_model", - help="SiliconFlow TTS model ID", - ) - tts_parser.add_argument( - "--output", default=None, help="Exact output file path" - ) - tts_parser.add_argument( - "--out-dir", default=None, dest="out_dir", help="Output directory" - ) - - # ── gen subcommand ──────────────────────────────────────────────────── - gen_parser = subparsers.add_parser( - "gen", help="AI video generation via SiliconFlow" - ) - gen_parser.add_argument("--prompt", required=True, help="Video description") - gen_parser.add_argument( - "--model", - default=GEN_T2V_MODEL, - choices=[GEN_T2V_MODEL, GEN_I2V_MODEL], - help="Model ID", - ) - gen_parser.add_argument( - "--image", - default=None, - help="Image URL or base64 data URI (required for I2V model)", - ) - gen_parser.add_argument( - "--image-size", - default="1280x720", - choices=sorted(GEN_VALID_SIZES), - dest="image_size", - help="Video resolution", - ) - gen_parser.add_argument( - "--negative-prompt", default=None, dest="negative_prompt" - ) - gen_parser.add_argument("--seed", type=int, default=None) - gen_parser.add_argument( - "--poll-interval", type=int, default=10, dest="poll_interval" - ) - gen_parser.add_argument("--timeout", type=int, default=600) - gen_parser.add_argument( - "--out-dir", default=None, dest="out_dir", help="Output directory" - ) - - # ── compose subcommand ──────────────────────────────────────────────── - compose_parser = subparsers.add_parser( - "compose", help="Compose video from materials + audio" - ) - compose_parser.add_argument( - "--audio", required=True, help="Path to narration audio file (mp3/wav)" - ) - compose_parser.add_argument( - "--materials-dir", - required=True, - help="Directory containing video clips and/or images", - ) - compose_parser.add_argument( - "--plan", - default=None, - help="Path to plan JSON file for positioned material insertion", - ) - compose_parser.add_argument( - "--aspect", - default="9:16", - choices=["9:16", "16:9", "1:1"], - help="Output aspect ratio (default: 9:16)", - ) - compose_parser.add_argument( - "--clip-duration", - type=float, - default=5.0, - help="Max seconds per material clip (default: 5)", - ) - compose_parser.add_argument( - "--transition", - default="none", - choices=["none", "fade"], - help="Clip transitions (default: none)", - ) - compose_parser.add_argument( - "--output", required=True, help="Output MP4 file path" - ) - - args = parser.parse_args() - - if args.mode == "tts": - tts_main(args) - elif args.mode == "gen": - gen_main(args) - elif args.mode == "compose": - compose_main(args) - else: - parser.print_help() - sys.exit(1) - - -if __name__ == "__main__": - main() diff --git a/addons/officials/crew/selfmedia-operator/skills/t2video/scripts/tts.py b/addons/officials/crew/selfmedia-operator/skills/t2video/scripts/tts.py new file mode 100644 index 00000000..848c1c39 --- /dev/null +++ b/addons/officials/crew/selfmedia-operator/skills/t2video/scripts/tts.py @@ -0,0 +1,457 @@ +#!/usr/bin/env python3 +"""SiliconFlow text-to-speech — stdlib only (no httpx/requests).""" + +import argparse +import json +import mimetypes +import os +import re +import sys +import time +import urllib.error +import urllib.request +import uuid +from pathlib import Path + +DEFAULT_API_BASE = "https://api.siliconflow.cn/v1" +DEFAULT_MODEL = "fnlp/MOSS-TTSD-v0.5" +DEFAULT_VOICE = "fnlp/MOSS-TTSD-v0.5:benjamin" +DEFAULT_ASR_MODEL = "TeleAI/TeleSpeechASR" +VALID_FORMATS = {"mp3", "opus", "wav", "pcm"} +VALID_VOICES = { + "fnlp/MOSS-TTSD-v0.5:benjamin", + "fnlp/MOSS-TTSD-v0.5:charles", + "fnlp/MOSS-TTSD-v0.5:claire", + "fnlp/MOSS-TTSD-v0.5:david", + "fnlp/MOSS-TTSD-v0.5:diana", +} +SAMPLE_RATES_BY_FORMAT = { + "mp3": {32000, 44100}, + "opus": {48000}, + "wav": {8000, 16000, 24000, 32000, 44100}, + "pcm": {8000, 16000, 24000, 32000, 44100}, +} +SAFE_INPUT_DIRS = (Path("scripts"), Path("assets"), Path("tmp"), Path("output_videos"), Path("fragments")) +SAFE_OUTPUT_DIRS = (Path("assets/audio"), Path("tmp"), Path("output_videos"), Path("fragments")) +TEXT_EXTENSIONS = {".txt", ".md", ".srt", ".vtt"} +MAX_TEXT_FILE_BYTES = 512 * 1024 + + +def die(message: str) -> None: + print(f"[error] {message}", file=sys.stderr) + sys.exit(1) + + +def workspace_root(root: Path | None = None) -> Path: + return (root or Path.cwd()).resolve() + + +def ensure_safe_path(raw_path: str, allowed_dirs: tuple[Path, ...], purpose: str, root: Path | None = None) -> Path: + path = Path(raw_path) + if path.is_absolute(): + die(f"{purpose} path must be relative to the workspace") + if ".." in path.parts: + die(f"{purpose} path must not contain '..'") + + resolved_root = workspace_root(root) + resolved = (resolved_root / path).resolve() + if not any(resolved == (resolved_root / base).resolve() or resolved.is_relative_to((resolved_root / base).resolve()) for base in allowed_dirs): + allowed = ", ".join(str(base) for base in allowed_dirs) + die(f"{purpose} path must be under one of: {allowed}") + return resolved + + +def extract_tts_requirement_text(content: str) -> str: + """Extract only the voiceover copy from a tts_requirement.md file.""" + heading_markers = ( + "配音文案", + "voiceover text", + "voiceover copy", + "narration text", + "script text", + ) + lines = content.splitlines() + collecting = False + extracted: list[str] = [] + + for line in lines: + stripped = line.strip() + lower = stripped.lower() + if stripped.startswith("## "): + if collecting: + break + collecting = any(marker in lower for marker in heading_markers) + continue + if not collecting: + continue + if not stripped or stripped.startswith("s3rG-!5D#ZI4a&^2gjZ*wRZoJjJ$N18 zS)Max;*YTFBuGttzyHb;*zh7KFOKW*Uj#si?IFAb+@48#W`gWNXW=?0I;-5~Yjm_{ zC#!{1mrvU{v%!f_MompN^*Tfk4IE-!yL*nf3MpAXltKhzHM|OzI__0#bV4l6wFf`V z1&54behif%oPHaJ#Y!b@K6QfH9!jVPH3spsu7X6vw|k=1(uvQ^`>7dSbnUGj8E%Pa zN>#-P+<{gT!IlSnWH2jzD7KVCKg@&4%HWJPA>vjprTC@axOS3d>6Dh50B)Nu0|T6z zrx1HXHh&D~-tJ_f0);cSJuyrK=M0pdfPbHiMt+iMV(j(3s5UE<#X%*6dV>P-2VyS} zaVC)&A3kIk#@ZF)YAJ>K&I1feiRs|lgIP@Rr_&2CM#}Ew@xM-fhvHyuwzD=xb6qJZCm zpS*ao+}$bH3%yffEk)arQY=c7G2?Zr$2exDcJdB06HTPFUkgS|`<7YDjFNKE!o6G7+mWrndLZRH}B%h={ye8%m5 zoDGIQ>brjxD;<*J58>@=Gd0Kpq~>F?~04T zdKiGHMgOXT`Om_7z{!;dr4GSiST;8&%=@-!Q7%k{nYp}s_wKXLKJ(M-J@ayV^Vfg< z*SEK~N6>E*9DQ7lU26L+?FvieJY>sl31R@-C(nsoWG$B6wvGC2 zT$~OH_%`4t8h_J}|IErku26>p7mfumT=&z`f8&f;#p2mFGvIT2DB^OVFN7k&n;5#Y z3c)Ir@@;OOQ~DD~Ge1}bhF@nX%j+qbjqFfpoe%+r^g|>Jzd3cKLcNad;CIwp(1sa9 z_vh9>_J|j(6EqNX73`i9&+=zJ2@r^a2fJtR46%_V4GPS!0+@%ubFv{^1ZINvkj@$2 zu6R&}%}@i`7Fj5NeB4OqAk5VTB)FNf{eK!$JphIq0CQl;aX4i5SD|ZkrZ*bddMHtdSD9Lf zb@>U*ppAl~okeeaLVo;nh}gjJ6?YCy?nG30q`e-lMASM7{TE#!oMx5~Ev%inu@vY^ z{S3`^vrCp`A1}B39txOxJ?A*ndh$&qIH8hZ5|*GdkTvPVgCWl1sx@;qbrPT+6dr!$ z2$r8K8N4E^8atDr|L^NYIJtv@_=NJ1`K%?JH&nP5DbV*<1Shm)x5Y|PfzFScPn+HZ zuD=Tu1`gR~;!}PIb z2s5*eYJm^Gyb5GE7&JE@$2^SsB`4C(71UzW8_Gl^{Zs(Lf8u zj&D!)ewvkP#S6-!ZR-{606?`1T%6F!*tVn~v<~J^f~rRYPYifVjSd@C1KdH)w7$o& zY9Jd@Kz$+`MLLSAT5G*rF8eX3d~8-!YFEmr?kCs7_S>C#7KZTHERKR)YdDB#4mm>0 zF>uAv6O4radX|vcKEJgIwaa!{mgS?5KDt~kuV20W_~VZs9^T#G+^TF}e*N_(a=R>~ zg?XYfk+KTas@uB@^Vic-Ztk8=bJ^svP$A+! zpM3hKU;OlE6Hm(4O{l;vXMS)V{mGtyPMi3I<8UKfXnsoW z*>cYk-=2{T=>&e~`Uyj2w@aAv9^x~{YQJ?on1*4L8<;kTs?tJL3ggiN|zK}BaEq5jc6p64e}#=80t#0+PWo;|}T z!x+z581&o#HhRjH)KZl35!6;m@_l;dF&3a&h!#|?~3~N-$bN3wMqJEnuJA2?w;KJ z{1?CY{EM$1F6+l1f4oX1sgJML%O>wVd!i~_v{n_NRkSLp$h&usH*elOeecP<@WgY~ z%O+xbW7%d8D-jXHK^NLLHtjd>jg$5l&Zv^unYD#j)NhnyaSIbso5?L|hs_czkB3#< zv;jQ$3A5Vv-xa39T8L9YMkyi`QE z>`SkvASbGav5EHURh8@Lyi%sa1mfbJuNl;J)E;3aG0PEY1KQ z(w%{jj$w;N?U`rG!D|@w17!@aG`9O7XO6Ad4CKIi7BSBrjQD0~QQx`l4QhS$>ebg@ zf32#u*2=c*S(at_@WT(^d+$AFCTkQ+DN>1uq`Rv zTM5x-FyX=n_RQI}?Qo}EEraISjdB*fQ4bj&0h8Kh1e|A1eO7yNM}(m_lONQ!_WkU@ zwuL#2(#U*i$KVDh^_Ath^%md%MH*3sOn4^?ZvGVnc+v^ zsx(4Hpo}#lS7$tA!SjPs2TvI~4gm;5w!HX9aJ@LP6s$QU?p25CkZf?q%=yrv&uL-XLgHCj?c1-+2I?FE zb3G&uCDl=*z+mO<47EqSt%uHQKZaiquH3}r*O&L#_02bM691`aj#YO>DJkAnV(>~R~6p)A4rIf&8KCs#29WH!V% z292QD>)POS=04VrD!MLF09cL*fo-IDJyH$RgDgWPgRX{99J>6liai5WC2fPzy(dZS z3@*FI1&rWI07;_{UhD1-FnvYvC%gSw1sp%ZTFU1eHS&7*{&PY2{T3x{}Fk8k? z4l=tB_0Zam-wAQXR;h*k}3)_ z*LLyUWm~I?O+aaxlqcm$X}{maNrfG_K_?@jMB5po!w8_NGdd^c$eV70LW3EGdiTe< zQr%lDuXJ5kH~jPG&wulq-~8!MpS*hY>h0UNb-m2@hHrp7P2V`__I}x-fsqJh9Eiu8){V4uPHyW>s8eqCz3+A$@ zfAwFKd1j_<+iG3cwU$Ejw3Pjfp-ON==pgR*CPsVh)$OIUu|Mv%W}n&H20b|oj1PUz z^Su23jJ<1*B}sB6X66w$v#P7AyLx6;I{-moz#$>679@r{*#G~LfP{erK@bRtTmpP* zx_kOjS(%l2Z-kpZSh&UUV;)f*XjI>f@Q2y4W9A<29`2V*48+u0Js0ksBW9{qwdkZ) zMT)KKYE|aTeZAJ}_4%9czWx4(@4kNjzRcdh_W(l3Amnj04I2qd!3gn*AGAn^%%`fh za*ftnyZCek@8fbg!ZwH6NEY_Rj99F|-u5KSmO$Bv{;23+(WUm69k2+)0>%@3N^yS! zr!5t43@oM0X0|NrIV15A^dL(C6+XoQz>?#r}xf|-lwFAO+-JyeDakKV5GK8fUbw_nL4j{0Uk2;X} zUK6=+a1&!1!9`3E;OdYYcxAF>+hHwaAb(0Dl}M5!xFY;UuIP?pf!PPC*zDa|!i-62 zTja;n=*N0zIFFc9fXe`_Ic2d($C*iX0y&|ksSb-(ERSKsh$9?}jO+LUh*IL(QH}Te5i|AB}ZTmV;({z7-f2+@{ z*ecdGW7@m0zL?U?N>MGPR?|sU%X+)1&b8Jbe)!Y3-+ucSzxnM{RAldqc;33KW!?{A z$c@3qG=bPu&1X&uByhVIh$fS>qFNj;NZwA!5UDV@c-|N}vVK|Oa8lafJqO)QRaeJQ zEJkKcMT+{ghTj)e-%uB=L)H)b1_GA8OG1qV+`Nmhar~ajwzkv_>M5m3s4C!n^Q6Iq zySvNtr^oNV|K1CXqYj-R@87@o4RDubks>~hpUqusm7=05>mK4UtR3+=3>t|s+D4lA z5e+Q{?x+PV9``dn(C8lS;czf9i*ZJgZD(X{J@$?y$*<$xz(it2fJcSxusBT}{pe4` zsPPb}ppA%JE*HnMz;*6nWUAmp*u!-!p|qB`VZg;Rh_`)V(S zK0|(5kr4&P9P%_xezd3|(5;H_7o>-#PJ{KWKFXPk1zr{66^#>-Ayfx#v?PK>nZcGw zK(8X?6tpll5=7xx8i|3~v1ILs570I6PNrz+OL8LGgb6tyib}YlYGiIIpMGb(5OZk} zjKma)d$7U>dR)0YW4p3X$SyHA&;zShK`hm^*pUfFAg;L}CK zAoa6%_#F@F$N^%`5I{*U^N2<^XvmYtWJy`;9Y3zz??NFZHcU);hgQanC|CwO!|r!z z%!q0s)pWGt71@|^%hKj5&{YkBL)^@9TM`p6I7n||EEWSPNtMf|WfPH0)0a#`zZ;4= z3_eJ$O>tq>LL>po?HN<~J64nqd+Hnr^m)NZw| z4O?s=&JZJg$q}@_1PPc7p>tg!;(VW_G*X&^kx%fG`qJ+SWScfFMn5#5|ji zE-zh0if(VIRD!ivZ&g(o0e2Zfq^R0p2V0NcI2jh43HgUo5g?gkVFjA8Kr&EFpx+$` zDXi7D(xP=0%n|lCy9v^Y)JAYJiCZhw6trvDkoyV2-v z)Jx7%N--!9oy%Ddk2?8Lr>tf!ZR7$avWKqPFJWB57fLDOt8GFm z?(hEYPv3q!P1E&yy}P?}ez^J$y0d&;>$22&er>YMWiD%3x7SgzqN>GXsc03Qe4kAb z@sSIpK8k^;m0q=$UA6`o*7k1nl}Y*m^e#lUo3&OSz|}GZ{KcdcRhd-Ps>LygrId!^ zI?X!ko$s}eM`BQk|Atm7*fD+}58SKK${!Z$Cahe*Nyt(stxQd2+{&Mj=&W zh_RzR7J}J3g~4t^wUpP>s-oQrl}NJ<<;HY-QC^SUw?h~`jx2q1VG3?j7}bN{Sl7e zHJ=(l7dFCSDb$XJt9&{{($wqF)|m{|5A1{V2x|_=gdtigY6&A6G!Nb>|196I5KFzQ zq0tSxcgUY?5$z}2v5Fyd7))p;78A#XGoL74WW>Q}bf^rkz)E)O`y8ZEwkgsEWhVKl zBY2d4XGKCD`|(&l5rarlYxPrfvFZW~)-n`SBidT}P?L=%V9*={qKd$T`)019wKPWAUbvQ;pM1JUWsB*|gxd!nDe-c6!-WUn+A#bI3B2;QPdo(0x){6wr za%M^5CJlvzW#((oo(L{-hY5gBd0sah8(W)Ng3SAY;p z5Tr*~Vcn>~FlCfU@}JN?GXw>|hD=i1hXO?VOMq+ZlW5;@9eNGL7E~4|;3ZL)3da)2 zsl#*ibRs1F#JocVrH`eO*n6a`By(ed8N+NWm#09>rL8v-cVcWk7A<1niW*BhS`7ho z`q8_8h7rB2Y)fAY)s+KgZ71eQ|GB+Ee-ia20|i(pu=J#*{um@1_Z;%lE**uMFO}ZCRFT zx{A!=LyiLj)oSayF3aKziD-Ykfr818hCeI@aQo)O!mHaOZk0*T?Ei>k6bOWNcA0b( zuV%U|*Dt?%`@6sWo9CCO|NVda!w*0FAnUp;i`k@Vwbo@_tBI7!bgfmd*X8=MT;_Yx zTeZu)&gQu{XCAbHVPrYBVWl zZE6use@^V~PU}2jndmY{eVA3M001BWNklyH(R z`$vJ^tb2h^j-Q2&A)D2EJ7)Is^77p`-Aou*OW zTs4ZFm%{m#SI`h92umqtS@u)l3GIAELq*g6dB6*QkfpLPbf<6cH)XM(5eCvWusgh> z_O3DGe*e}v7U+MV>uuW`H$Ord5H6|<*CkosDtGd3a?%yr?jw~yWPR7 zP^6|M!&&STA&1d87jjhWu!JxfF)9lni4SN3;sd3pjOPQF6#b6W$>a=Fv*Qf48`l2W zJ2{lmqm%5I;3QyTfkOfhy@_PYS+dI?IXfwp4JzQF2943sUc(@k z!H^QUhM;9Rvyg;9o?t*&qPlZuC*cWF62RKFKk>=ukrpRm7OTU=Lt~M6GO`GPj0MII ze59~Qb_g%sr1*TIZ`4JEJTXBOeiC($9E30=t*I6Q;pea-dn-cSxquy#IY-o!SW)~u z1F527rgcR79GVQS?f!A+N0K?T|H7|B)kI88&ALU#QcT389=dOL4P&qM<27hL)KuC~ zCUzQ!f$p3w6n2I<&{HZO$5nkkds!9=IFdM-%b`cPgsfEdeYDijd9&jWmCa1>e`ygt zB9kG_JlqrKDpOHRxp?Qo!3F#P&RxKb(iq~x_}rVn?%~KFX(5e%5e&=d_~KftD_N44 z(KtRXzeRRg>qX;00b1>xwC-hu;jq{?;?xb1sj3tck)k^knKS4b^k_?rR*OgNK8k0M ztBiuM>@SklzfV??Qp)Xmjh?)Mjiyp?YR5|myZabSmc@>j*2XOOT&MvC$dzufujiT9{ zJ+>>Zw=rQ6WgSIW|9_506ns0}RF~!Y{{8#k|Ni%%o?ia&hd+G$@FBKVthH7@xk^gW zscOAl*H52bRHu2mTk2(-z*((ol~QEeZ&yXO?cPz;k?pz9$KKYg-bM`h4aCG`JBFwm zF+p$uqLSF-aaOO_>#{7ROn3M5by&+|Oj)t;X}J>Qlls+x6kFCM?9X_Bhzy4Gq| zwbp87nsi!gHPvNVe*Ey$={xX>N*46aQ-35`;p-&~rBPlx#9@gt#|$R9bd1 ze3V@b3o-0s?j|tn`vJnN9{f;h7>36MT~+mRxx9Mys+uj&3}5%MS82x$?ei~}Z%1l0AU@@>=&2uz0ePuPxW z6ei42yr`X45XAjRnE@Hv@hG~I;V3<#mvxL1=!ai(dgGFMJHq2!GCojY4 zhb)+kyd&d7r1c-MVg+YFkA}z*uAz0XE{s>Gc$R^@HJ)ZllTYQZACB~;>jsFSBTao~n}4PA^R4~C$?sCX72t43L} z09byYab(0H;d27icWA65pt75gzS$Fxls%DCB6PQNS%$)3$_#aScyc?2elM|e(suw{s_^faLR!@M{FFFosqio4|S9xVbs zJM>)S1}PXaWMH#zq8kncre(ZC`Q^YPhzv(j2xY);p|Aw3g|1;_pmf<)C-1GlS!%7zfj)#*{Zs&fUr_)t&49TVJee|n5?xf zwHCATvb_BGJ@3q=-X-m>*FIc(>^`LsUat&MFTaF zYPak4ryqX&`0=Tf5?f1`X}Vl4Z{NOs{ra`4iupl>gN^N_=cET1jIBdV5SH`c=GSoH z8|s)#@LDK*@3>)~VAS+t$|I7i8nfHIha$rv7K-G$%2tqfM9 zqs5y*=i|L1=()5$+D6~gg$&5>A$g=X6a7ZlkOPyQK#?!&`vCbqIz?7LyHb|n=+DnG z7HNMX=gHH29g$x~<{rv9)Wxsi=z{}A1CB?S9F>o1=EUS**cR0OYe(s6e}VwrAI7w7 z8q`;Q1_{v-6bAQ)X3Fyu=M>7k9VQ#9O!{WS4XscB-dd~4J|J?GmqHLpn0@d>)FBjD zrl$VMc?kWPv6_lPz%g>77;%Rts*lu>5!ukblXh|*5pM``r>3m2*0rT%*+@W=e=`}h zfN>!LX4`g|B%v9y#$Q0Yy+q3P7MXMH3N{lkk+60YIE+GtXtnlWTd-66J$8q`gUqTr z!O*i-6WJDLiESnC!qAISZzk<~Bf~SY@WQ=`QK_E~hydj_UD{GaON*C-LfSgCR035o z3{9AZh9LKhvVC6d8T`-A`_O_1t*Z2#r$-Ab|RmH7Di?3H!)wQmn^0wbXyHGqcE221(cM^*N zVKbXG@rGPQdix#ltSh%qFrG|qf8tFOk!890Aw?qcXMgr*zx&h%2l^7QodhNJL`!b{H_cgT63U%!5R zxm<3y+w=4D%d(cD)m9(C_1$lMH<@{!MZ~Ht%i;sLB1N={>AKds-qh}Dskhtu;lqa? zA3iMWdYR`kPj%V%HVsj6d@8FZiNMq(gr>Q1%1ov*iA4lmcBL%L@J^1!Tbkta5d?>M zG37~&z?god;PB4TmE*})xN~OR8^~o@K79CayWQqew9VmNrulBZeEs#;J{gcvq9!DO z(Uvm1`4Kn*d>A4lvbXD+rl3f+sJgUs{J2vh+n!2$`UCB<=Z2Z^W>(GMoIOG>mz+`3 z(cZNP6bUBD97d)0`y3%wM@UkWMCA~#B4b`fn~U`;#y7*Vl3WG@LVk2EXBIL~m>)Gs zSRH(01=GCkHx5$<$6|;X2VYQ40UCrzBn*wKMjE6XGJ))J6n-_eK3S4sjWjf$5gTSi zxcq*chKB?mGMN;?G<21IcRws#3w?sCQhh#{u)`2jTL`ElrWvqzX_%Gp~ zk;kVoOrMF7;Tm2ij6#u6Uu6DJ$c!Y`m6avlqNX;A!!n{PrO8-f`kagGXmI)^aX7Og z3wc608?`doRSA&H&v;ahH<>xIjvQPkT97a#IaxM4#fdN!^oD|+m>{Y?UtEvZwtnTH zjUKXEdTft48EFWF915hT=*g7<%;n>it)K0M364rYQL{Wl?Q*?XesK64?Q=}>Q~Ht* zt0OPVwg`bqQ;JJw-6^+zzxLZNW;rRE5%R;&cj74(bz+EcBK%O5O8YFNmLhx(1=g2F z|Auk>ZCas$JL1FnMec?_$U5;Y)X|DOYwa?2>QIaz05U&AWXEG+X8VF9(v(m|equQo zJkNQR^X8j0fA23!@%c5W&}DDK2nuK1Vd0@vY02c+rd!+O9p)w&WjSe&NNe;Ot&9?z zqyItr)^>c>mPYpkUEZJJ;6JDB7;=E(GcBae4k&P%NGauZyOn;I;kbMh2zhSMf6#-D z1j&!t$uAt4tp1)d-H{ZpWZ$h+mRetK%et;sN)cY@h9&s2?wB(;QY5OY?Q|bYt$^Q* z`IvnVTtuPt8!C~*9EBUYzr_yIqNS|_x1vRqm)Nx0o$FTXQl_caT8jMUH^2Gozy9kF zA3pe)&U(9vh!6ZZA?A6$UaucNeti4&MYNM=51KxSu}xqPIt1Bac7Le5V-W6U*3Pi= z`;KW5k*1^OtY%_WMbu1u4pBt4aU$GL+bob5szs$p71L@ygX-&7%}m5}JJTtQ6;unG z8puF(~%PpE%sqYVs_VKwzr1|-@Zsg<MV zPtgHOFu0G!Mn=qD>%QLmDB`1+X}>xBFu2Qzu{q6AU6Ndto(=LStm1X4wH=In zQg`RM$hLk~)oOQ8MvwMh7gmkFR=u#ayPSQfT z5lM(3Apgmi4WjW-&4B1=Zi58NSLVs?)J@r;z?dum@dW-QN*V1~=XYqy19pY6aJo4Z z9?P%rDID2%$g!bAp%sP##P2+Ag^WGA$lPRBl=jFUC9w!?F|0J(SCUQ7-4?|Qv5&CL zbq)Wd_99Dll<~9vg}La?xsJJBqT*3SNA7u4HKnI46aewhY-=p`v)WPi&szMmckJLx zFY)(^;pEow^ai=Vj7UzuDYj6_Skz~~M9G?B)fC>d{|*buB$VE9(5&Z$V0NVCFh1lm z$#co~{nyx8pqLy9PsC>*Ns@2z1WQh3QH`_=b9*fprj2y;*dul+6RJGqNv_?~U&>T% ze;cxPAbV9oYYq|US$c@oFdiO7FeLoYRYN55cM1X-Vazzfr;2z}YpQOM70y(buy#X! zix0KmfhvoN9wCW8`L}1RJ(Hs-?f1N*AEep|0sMjXC79XN-k~PZsw`nR=J{ewG67~G z@j~o*no2tu&H5%7bqRl0xZ)0d&XvW`bT~ol@K}(feN4?S8ggJt{JpJP@4Mgoq7&VK z&4|eXfrl1EO5l|<9`V6{bl%DTsrzx??9MH@!Ij0Pe`idG#!xs;Wc3NEB;x5NgJo%1S>pw*mQBP7LnSveHa*MXtm*Q0s%flD`sYOo6o#l zF7xciug%w&+tbVSy4>#XXA@P?sZ8@c`>Mk&^f=D8f7Dj1Ep@F`w9K{0a=WdIRFP%9 zK79P>3)f4TRn4Ss=8}Eqeo1U3$;s{*paqunqm`TIvImNYg5zWd&In_SHVnk04OMm2 z%iKykSmAzhQKcI=$EL?NEV5I`27H(*>zkfepiuESm+IZn6bDPddsF3-r@5bd%8!&PEfx7RV09;p#+{3yeq9z>jqAiDo z{8YER4~3>q69-jP?;GGNgq9o z_`Gdx(RjokNn~?KTc46mhSrS2rGL)}cNxg^JKrho(JnF+GhVBR<)hS?r_4!Nct4o)Ra( z$fI+y_+{z(bckvF+K1?mxUxDNk(0w^75^ zVQ*znjB}LgK||RA1!Znj1Xu*X97Yn`8&ftPc`*)C4nd&b(I}I@o0&}GE!t|oizBKz zG(mwa#i%Zfdr?m9w>AnHygz_&#TWOva0Cr)ii1bA*pMt9EAnFWhgjiX3WTPLRxJU;=j;O>*I|vR( zMv+Ef=Vy4jKf-q|GNo4>m4>I+wzYzTj6FAEJMw{4vu*9_o-16E^zu|H<9M=_>1jaU zsWlC?)@7-;>$-SPcyhn#y5gv#ufWBPJHk4fV5g{Oe)^wtjv7&GfM^24##O}(r;QPp zOoFN+vh96C>CODaFBi^pZL+l&Eh(k^>g)Ia{lER~^W)P$|MNdTO=4m)m8I5FiiuWR z%RGO4d|X$X=ev2nx91P{^W|1=zLr&G+D3I91>BC-MQTH+CT;jN_CNAXT777_2acS! zlwzi}Rm$5O!}TFp%StUs+)GWi1#`tj-0T5Y<_tJNw+OPOE^m=7^mnXG+ZFljNdDjsCjtlrkD zGOemrWwGVq@!{jAr(eAPrHF{F>w25#`FedWrP%5n5cPJc__imp^%6w1+IBr}V(k7V z;QZ_}wSA$WQX+8F>g|xK#&);fpDL|pZG9|-=;j89GbICg>{uhyvK#w!qS(IpxkWFa zSZg8Hb=9d~61=i&=FdT1W4%gq4h(thvk9XH6^Wl<3+I+ZG_Vmg(_$H(u!`@UA0 zN}0-~)_S?S%=3Kr>i*T6FUnadz+01-As~ar^?L>7v+NCOHYirmf zLc}CROl+-Y=A-YjMH2PJvtIOC7jL9i7;P8m!30qI=R)2@Rq?rI+a0GT4B$po$pc%| zT2T`iE`uIq(6O_AiD)M3(fxR8eRnF{O8STqvJsIoT-d+I`&cikjExSZ@t%P%;mqeS zc(NEVto^5Xlx$dd2pI^6t!dbiK*l&06(MM0xG-_tpQb6!h7MSWdkUeFG(!1lWFczt zF13Gx7xE7W(+hn3MNEhq!&nXx&nl%MX0iohf}$e4C>YkB$s7`*3t$+GJHBs&j9e^q zM_0n|g(@Q;D14*|k#Ynh;n7C6#Zr6etpj&g7UK+-O~RC0bARh+1aDJ5|r>k=Fw zK!}POuQN^f%)?v@Dx2Lf--$&ydjjWY>;7#w;Yak>H%S8L_8Z>_iK0W?LNAITWHpaG zWHLM=e5h=ooy{G!$cPN&iHNX5qc&t+72s)VKVAMC^Qp+%%*g}YqqYnTGt#$yp0fgzq-y;3oTvsCi29 zv4h4v^A|KPsFV`k-s0$1V5}#Vx()g}GTSjk?S0JJYm|WJdW`iYpQRKZN(&=Oo9JzX zZ!c~hnX8z{G#9jQu$p>J6;^%mM`mlCN?EaIo=D ztL`r8bro~jtV|Pqj}8pnuiaj$4-n}?Wn`eQkBMo&;8kr08s!~6C0fJ&jR2=8YF%nw zMNCA^w)hyF-7I3#W>bRIE|}}+`_Jymc7v)aqA@-&ZJ+1*wg9zCi{7d#Yn52$EbX`~ z5m{>~#Z=AgdcAJ*J*Hl7+2j@xv7)t_-BzvbWV~e+_@F<6L&#=gX51q03HpBHYSmQc zY*#d1Aa`Ee(W5Ae%8aMGGNL8`pNN?)>*6OD{pL5n`S9Vx_uqf_^z!UG0Z-FZS1Hq$ z-N(Yqor;xLcduX8Pje}YSTVCw+ltpB5}hTK^RNRaBI`EBsYfbM08I)`s@zEAS#+w} z%>1;z_NZFhY}dOE)s`0DwzzN5g5u7+MYqP?%Mno+>mw}`ZEY`<}eN2&os`nsXM(7ip5XJBR%yS@X?VNLkWlU+X8OCiwcG5tRO`btdSm3 zvY&HB)t$?4U4cnVd=kaX)^&M!c=+LmA4(}|-ZK_e)oFV3=FQu;U!WMxvb9UQHjzzBEwg75|fBTK`gh^Cs7*J%tRWJXGj}` zPE3@U=Lv=a3akKZM2 zGn&Il_DUIT7J*~0IlV)aM=Fnep&U%`WV__*_F&t;T;*DFfkWsNUiEPdnn1&HkXn+w zV~9_#l;j}_eKtA?I1zzE?wl*l>L6D5JG{e2-!SG`D||}ASQ{Y59=~6uo66mD4JQtV zT2M0S-GA8dQO5HGnC2+Zk;<8S*|Jz#7=m=nbQbtM+_e+#>KJBET`Xd$LTAbPm6eEIemUAzPAAqXWT`AXU2`^4{LUDEFu001BWNklos72KNMPA2n93Itt30YO)Rb?l_tc1gnagEU0EYHQCoIO)`l|tXfr7KM{T#W!-km zEh1u7tg46EY1(UUWAi3btC$oiqA@9F#e?8B390I<0%INM9+x)>x52V)mn{+T?Lai_ zE2KSXdU&&#Q1)lXOR*zv^|Uwrk|S6{q+d%N9UK5ggjY4gi%qZ73l_-!<^ z2tZhibz$RYh5269zK5j zXttV&$*MYgJMr$_ySH!OMof$lFE*BW7`p;eij`r_m|Dlu3_1!qyh_ivoCa@Z`ibq| z1!Dwmxwt^AdK9Af7NRewQy&HUtd&tmNyI}8Ph5$x%E)9kqT`$(FqAnv1r`lO1~81ocsh!cGw|4nM;uX5y;i~YJ%YcJu2tO_YGFpYU}94cTy;`-$%U=+gwpp zm)8kqghNym59=yvk<<`OQjlmC12n_6Eu>m5D%%SIAL1{t%7ZwDi;t)rEIj`^OqM~D z1HT}*4OBxma+AnaV4Zu^Nac|3DPE1y8@e{RCq5!EkN`(4LJ5b^k-$et#)a=KM`<#P zhWbXbJOMyKgZ`9s5$A`hWl@K5Lr#H`h9|Ch$t&OUg$5dOCQC(D6QFTs6k3q1`B|;y zz?(ARsk9@?2_T79Q{{6rju6Ns9|}L@cH9{f=BTtNfo8d~U_(M8VwmPAL=V+Jrx0CO z2u6S`L#wE?*IE@>dt5sNe2uM*;5y+gCcwBv(RhfS6%`X%5B#^+H0lzYM6Ag8$g-q9 z;|7!-0TZY|RmGhUv~1r(?s3D{ii+`$ZEhe` zO+?yS;4W}Y(Pe5Yg?(!U+?(4hP_@1piZnimhMMBS(u{x>@vof&ItrfJddQO8Dr`sO z4CZ?Ywbm*&m8ocV1hI(FDq6o2f5rgrUiE2rixc-ZEj4X9CP#w!ehyt234>d-Wh;cs zL^;zm-EOyiDRDnPWbQA0WKdP#@S_P(J(Yl~(pYu~sr|S)o$P}4MMF!Qd#LMryRNYU zH)`1`HBphZnQ~p%7{JA1$vVEfDJ>`&MiR_BA3ZDmCvs>-yfZX;FP0E|N1{aD$X-fW z*Y$e6s_HL)`ODw`{`c4G@=yQtPx?4ruh&mcFILxS(pq&{mdD3WZ{NQ*)3r*`x$3g* z!VrZwe7D()s9HPYk^E?wO@+P@ir+2Q;02HLZJ`QX$V-rzAA$`)s;F39ouFF$plbCu zXWya$>Wagxw@7HxwUHnU8p1?OrHEE3rc$gbrf5Vrkwg2s)4qQF`t94dU%Y)&Yh72L zxoTdmTFPvuK~NQbIgzbl+k}t8OHs3;v+6XLrF{JO@lW4>`||QKzrJYukU_W21a?hx z?8t^VwquutteI_^)BC?g#sb)=vCkwN#@SpWwHzXQ=qI<0&osO&lK)6AdWtbI9-Y6Z zQA~_NMAU5iXhKzQx7)+R!|nD`m$l5(ER=sE9{$t<>SE95jjbntOyxySOPiJ5bz;6^PgBX zE;Y*KA4O3VCh|&8(d;rh8k#rg)Z*7ba&U#%nL-&c8957@BlcE9vkuD%IZ<~CP}VPR zM2P U9_u7@88}5FVC!vN#9CJrVHNk04?CcwhUxR^4>Ps}xpkVAT$0D?p%9H=xT33)+W^oE9rVF_3} zNvmn^X_7<|+q2mD_cEjfT`kYszu3#S+$a zJal*zzM{I!YD#9UJA^ks>+hQ4bI6k&Jjy%r8d7Kfx7W^Wf4ig@Tly_ZWYqsedT2*= zUP6dEc6RKOm~~%YA}3jqK0zcy{EtkTrOFPas?#*Z{z#Zop_P#lZ?WD;Htdg4+&J;; zOatq;kBf3j+kZ2>CS;C(htVKMrxSJJijKV{YUNN#{z77e^J<&7R@H6jHtkrN-{v&V z&gze)qUgQT544NT7I5(+@(KlxzsV(0k$}tB5Nv}XZS+KdO9Ix{)c=p)!x31trq_!L zBZemPWO=>0!iXWZecR0v<2l>N-O(aefigo`AMrFZf#J*G2HUZBF{H8dBh-?-8(Bvn zOY;snAZHK&;We{qn*4Kf?;?g-rRN68)B1}08;bS~*--dgR4E1PAjK0iC$Z`SZnxX* zcDt?X>hL2!rJaUlEmZmd?q)CT`se7^8MMCGffh6REpc}*V$`M{MVuqNM8t~P={j6h zb(7S#kC>{OR1v+uzyI?;|MO*8N|Ar~hyQWA+|^pw)qyjs>rX#^{Q6g4-QC?keR{T{ zs*{RIwMn*Zf_!J&ww&~=RUYa>P_{dJ37aLhN1ak4A`&acb(_oTUa-9cv9@7@v%plU znATOq%4DU;G?i(Z%B7TT!xNt*H7Q!FZ!9BX+x|XgR#bc^iDc8=32htbHZ4sdt*Q!h zZsuWMM0~i-Dd0(^ZKrj2cX{>2>&yM!vMkr@?Y3?quM}HXQ}wl4&`x*-<{nsE-4X3q zGb=jPwny=FnI^fbT0UJreRz0yet!Pq)f+PtDNZ0*?%!PBOlnaX@`Ln|wC6i;< zKt5yx9bH4LB1?fC=!R^p{nnE*qIA@Py{M=+I*Xn$n(3iDS@ILO!j(`4bZXn1%fYMT z5qe?7j7ClwMG?q&NBamP&^s3}>t}VR>J&QA@q)xF5l&-1;xJS*q|FNEtuv&zMFy(E zX49D=3NK3(5KwID(4&qSA2I>re8lIt8H$ibL&8YUv7yB&S%K|VNIX&mF|j_vAcwOe z2H_ghV9J~(DJ;1 ziE!e!!`VeLlbF(mZlw3`5%6t8&-RpRAvw==9LB@aZ?%+jWk~5!ZHFvLb6AQ|kr>%~ z{97Kg>m`lBX> z&S*t^B^C-#^lZ&55+bkV}eFrE%YWMwceXQf(`pvjgIS6=}AMNd;u}5CIYK zaU?TtXcR`qv)0I&cnK<{NHx{{96!`T{QQ~i9IqV~GxN*7nw1;+_hjH-yPqT>f-6QS$UIRHtS1#FF2NA8fEX(C``R#9id%NAf`|kTc z{_&4reDTHOBjG6S<0lUMRZZsX_k4un=dlY(=@4Xitp=NMNOovwK^wN zRixBfb)9Opb=?=GwjtevmmeKCttSA-Fp=(&qvE0ba=Fa&Y-TSn*JV3ctBCs0?J8T1 z`>T$oUfRal(#I+95?7g2RZNR5%k|Uq)ARF_*?V!Ln)>bzm~j{avZH3pA~F=Iiw+K_ zGI-O@emJZe*@YlPI7TfG znv@F>CNw0`J0~_X|IT2uKCE-cAfCC3a+1$V&+Q{$$Tzcg*0tUY{5+zTz)DGaL}9%2+tkujd8E{K zkbUisX^!4l28yhZACY5{))`NVjTq7~8Q~m}bC|yLTcAYziQ#Ol3oAuQhDzZWD_+EH zDl=rp?5J0Sqai0z3+Y)(D&#UzJWNagh9hUavKZ0`h70O3C8-%jWt@k<=_dIdl{5TJ zS&sbu=+eG@Ig%s_&lJsqQL>!CXGex`#Prg) z9r+OzHM}AtmXm2OJ%B?iWQL&dNN;R$5bE#(ax^R_t9io7p&_z(m>}zd(w6Fp7`nAkBuJ6&VE@L?V^-Ely-7e$3!pUa?Lj;fn%W)&Rg0=4(Ag~lJ z2u~8p%d`}1qc@AQzXHS{=fdxV)@7k?GpdPk025>k4Tg_>Km&F0iw2P+!M?P)mo`Sd zwePFD{J#8_`q+pVAxSPnorEK$&{)3aiI|TW$)=gQWVpZ$6-Usbe40D)1*!%1nkT3; zCd0!0CcbZG-xr{+%M&4JSoKyHa#%&AENfksW%Z8-l#Q}&H)CZ z8Dh?5WuoBy66b05iL;G!6!0Hai-?qT^iWIO#&r5OuA<^Ygn#iDfAP&X-#k5ie0X>) z=0{_h*>rume*4W2cdw>bZ||q_`nEoqs-OAjBYZHkxXpWSb^8bV=_sLr3No3Qy3A!; z4TtGP#OE-F=GjwP2}rh2-K$nVSktzT+)e5{m-|KmVs+ zYcX5BG*g*;f)Fu3;8(V9o%)%L+1j(|tiBo7u&?-)qqp~n&01PGem+AAW;KtLg%k9_Lr z&mqWUo%7NNK(tE~NX*y%u=1h$^qYjqSu&}HBi+(}5}5sEcwyW?9LSxeF_7*Y!F66~ z4h7og7>GZZ<-7`V7DSa)%PCK|F_ecAqgRsa}QnN43^I7&?MTQqcP>0d&-@%0W2GNt7OX z-_SZFXHKTu9sF8lMS37l%uKxpRd^yWUhidllM=kss*bM@S*d`+-(cILogP-i%%oz0 z#mpw%*5kL8Ye|lL;__L2RQ7~#cqhN{q2ztd#b1`AIuZj_&@>@UxCw9-2f=;x05PQMP%krU z3*a_tS@g+A=><59W3n!jsO|=XZk4v7On(n5AiTCpCDEqVYNH)pq-P^`>4U%&MnY69 zB);;*;qQR%v%#?HwvaR4C}Ze03c&%~)t!BNFLS+2RaJa|a`5?!#Y@^jBp=>Cze!I~ zQ|;SIoW6}md2vH2#gW8SKDXNq8PY<;cCM0bAG91s1L<8XSH*elNJzuMpQsznL#}7|G{q*?u z?SFoCS6@DU5>+WGT8jAq=_WEb!&$e0U=4GUf=9V>TS6w;e86`~P!T~U*?cMYucj~F z+^eots}}J+Qnb`|{8`oQL`XBUY0_GyT3wd)c3W>(AMaO@B0W1je0WQa)I@bcQ?u>d zy;wH8In)kCF|%nZ_xJbHG%ZUNv3a^P(RE#Jw`E;xmHjoFKObn-MIWFIOp94WMisFD zDnZr0T5XY4*Q-{O>Hg!#508(JYrV}nyH{9x70L7y@K`icHjwpAb(lM^(9AIo|D=c} zBmcrGg8Y^mk^nH*ML*m|GP<-;4T;S^An}mo!)r+OENZBu?M+WwKfuhMpRXT3el)X7 znToGP)jG}k`pxUNZ{JGa?TeDF@5yz+)DC4BBh$wvj739raIG|gFk}U_C~i$HI}Q;) zt#xR2r)z4)zqo$E<;12@da4Idl42CugM7(X55tjsf0V_JHG1BA& z1{X?*lt9(+^A7EoyXGo}Xb>`3)@QPP|p{sJ~Xnl&$m$wDw)`Y zYlRSpTzLLXT+J7zB%dXm(LsiM2;A~Vn#ebjhj1R2Ny}O>4iNqUEFDgWO3~swGDI#j z$AlrOaVK7GaWtp)FXyBn3WJD!)>%%>7b>8dS#i4Z7XTD_h-P^f*8j{ak;w`ub9TL4 zU8K|&F!6rYo$AG8wD>Nk_iKW4JTUl>BMfO7vQb#b7)npkZ1+r?tZ5EkIX;|Rp#$cM zf1!|_|1|GNiVSO4RoMjl{3}dv?oE=K_Ll$&4GVNoME2d}BBjDI!>Ep8tn{`fknzGh zIDC^MUidY|G==5{6Z@kGR+20(i4ugK^u)kWVEr`=BU#hTThyKCjjoY{Z6;6HS7Bfs zOG6m>ebs5gc|vGZR_@s}EbF8XH~j@gk6M^Fcc zwVzt1^l>3#bz70PvrAyVZ{j@CBYT1N!^b|%7inVeY_DgRO1SC4>Pm_~q*44yZ$MZY zA%>np1%)BFP)LDF&1_lg zveesJm+C`XlcxTFwb2_ltFOTQ>}zJ`ZKSXLmj{t?#+fmEcH%ZeevsSbj5&RlQoym} z&#h9aqMLQ!zkmO`-~I02|NZ~_=9_P%%+D`RkDor3xvb0b@Zr$1MQ+}7(_t9ei@TK0Ca zSSjr#&}U_3>kY`Z`>Tj>!|6MhFTNv>`3SzWK18jnP1EK6{`E9nYL!||i<*k4ENdJ` z=Jl6h)NrnEvn&l5V0Nk1YBeoVmqpBGnX4@?FE1ZIK9o{awANLp;=|gp_7nX))EU;E zLlrhHxZWzl2ZfC6z`1rJd6tdBqQ60rSLcc(xiqHC;aE+qc1gF5h-GkD8@@;J2(+xS znQ4U{Kv9KOFk}neRaJGGrpu@2Pai&f*v2AtQn6U?`RiZ*x=fQF$cwW(*5+vmVV|PF zI8KH@0nqVlzj8Ayf)fv2Oc=dHA^w?N_@^}f$4sm=nY>aQz>1QmX^O_a!W!XaS+I7j zC6XaeN-6GZMC>ReI)Xv|*w)Jb45tpp(i|R+lp$#JqbI|FW_Uk-M{h5aFeLHqrCfzP zFGze)mP%ODZE0lieG>PPUnF;t88YOB+4;|WttRub0#T-Vgk05-g5b6D$a)k+oo*pU$ zut@%RABb0-cy%;*vkC!XoSmvh3Xv8D1D7ZaAx{~}H8KszH27{t?2XI%177H5{-lc1 zujWIG=PE)G41i&~2ujh3KOg8vyC2nVeK4nFL{T~Z(jp3}qb;X%InlmQWQb4@{28Je zcj!LRDQm5xLtU)BY!efrOr)9ip)Z#&FfK!FCrlpw7^1WTRXZ8*DM{73IuLJHtF=nC(oUNojm);Uze%)+dRVBAOq;9`5wjihA+Q-V z0y75$9QmbCki9aJQSd<*2ub{^u!~oek32^X7dS~No<;(6c_g7E7a*E3ReBdgbE9g~ zx2+2OIvKSHSR<7=ywkj<)b^H+f+0katCXT?J^q&78?Ub99jI_I&*?2e+_%=9SeC_# zAA+=(zdA$u*FEWjp|X6ekKv+5M2NliChu9RF-OMGdP9bgwKGw@1Et1dk{QUfU zU6)eIJWr;&ifP%m%L{y}iuF#4Bx$QVxBWJWfi`MS$x@UI?T#*tv5`6_kuo?*3?$?O zuE3n!8GiWSQt?cs9T%0*1Y zwpH^x+s&@mYow=cKdz+C-ii|Dfo4xI%ki~Ckot&2TF^x)^J7Z98B&Er$7r~&tDiae z>tFx+Z~o?Q{_uxCeE9gJ#a?dDwce`e)5qr@fB5kJS8wm;`+B?Cx@aj?+^N#XZn~?P zrOl!FDCLpvTgtwpY-R-(Qt!oRUDxgFq-mO7zrMfRspx95)>=&#^BzLm(zkK6ELLl+ z%dOsS%k`x$OBFvcOs0ZUH>#>*vB|fO(6H9bOU%7gRn^Po{&Km9=(?_AMN3hg!~*T& z$Tt>_!%CxO*G9IRraEmx6z`!dFV|1cx94?T=Xowg%m&9*8N~W?d(l(!6Z%(1{I}FO zYrh6)>p_=J;6)lR-{-d}W|5HiqxN=;5~A?E#)J3Od|C2YO0NeUI4Uy`h@OO&A|lu8 z^-tgZu-w+UOuq4nXr1Qya=EvINl`5ac~R*TcBOw)=|UJQY^%v zvxPVeErQDC1lE7V^w|;!oMB@6A>eBWBcfXRbt3V5+t?n`NMe!oBYM*FHqzio6X_Kx z(ZT&sd~J^Vc_adb$y%f0p$i%`(&vtaUlDMEkVG)5nqItvQ3RkQJ`-s9(J)JP3K^YW zG&Emiu@sPV;e>pMN&{zkWRr5C`=H&Ah)r=OD^6K7_0FlTqmqXPCC|;?dK8+5>SJ~s z1Ux|?#HN*`cG@MtqT(sJA%Qb=;Ug_hp3*E4mY<=sqs~SzrvQP6d=9a*MmYi>Sto0k z4Mf3eNLN%=j>lxB=Wr(H5yH~5lu}}}q5CpI3?&)*I{_H#(EBHqe^N5ZMvhFLMSq^C zgXQCZNRBpCzCsr+YvH-_$h-8mMq&)>BL36Ud=um3kiD9kHcH?Zjlz(QS#k4;?R558 zHd7#z9T{cATv z%dv&Pk-ooywdeS$Fu9J=N#KMf!VOE))U;6{s2ARh87a(|c`~dw>pPSI;X1z}`(_vD ztj1HECh_~zG}QxSB)M2lvlTVMD*m&uJYoGXyZ}j?&5u$uN>@VBYMH znO8^{Oo@O8GVy2TJixBnH<2;9D8O^77iR*F#LMFz1T0C?(X6kr)_Pr*<>r+R`X`aB zwiUQ^q#ON_jxH^&PzlYVO32vxnN~$obK-_{CPUA;=p^u(_vM#g z{`R-O{l|a&$A`yHesiPOq%~_x#HhJX8d);nE+Cd?8 zNU6Y4Refg$#aWaRg*6ad{w-E|2=iQ%*Q4rXRyXrJm&@IBe}AWHtF5&z;>(2rBoQ&w zs#4eWcDt=>HPhu*m!+D?T;`(l-c%I#98vYaVG{ie72na-to_>zmH1T5s!wlJtIO(T zPIveBUwrX)nx@-rUEOM?DpM^|RjU-=LATaAO&5_t=ZRdalu~RT6sT2H)uhyFQmjZ3 z)3w&8=cnhFr**xRyL%rq^0~p)_)`?(-q4MQL`7~B&nd5sy0e5KwKQ1<>6?@6eZy-g zk3d(F!nCHwwKGTQqCDzO9%09Q4p!WJYt^wiB7Lp$@bLH_|M4HS)=RmYOw6k9fPA@J z-oJleN?F!h$Dg87RMxItcY?>PavlXE3CS+QRx0a@4I!0-t(T1O$MmYci3*SH#{4Jt zt0DouY7+O3l*PjP7RyETa=fkef~)8AJ074i!*M7X=Qh3Rn&+8bs4#e1>TjV5h^WKg z8o5qSjGN%jJ7UAGh*_ zY|?viNJVjMExBhp480<10RvS<3lO49r0%0$9gTg!PA98UB_ z=!X0}(Hf~jh9=iI5@V<%r5~1q>fLBBBR1#HcdCP8SBWVNAf*WV<6`ZN*Mr_e8-hlQ#Po{+lDl z5#cG#QQRg;av=zNh9dKmcr^qq3#_T>%m`2}ue4Fckg#JwwvMKmb>FMWc6(2S+nOQC zvs8ofF_-QoZr!zRxVEbHq)Hx&fBL^JXX_0DT%9$!1T>$e)~E^ zeTl{bt{0=UdA$Oz*p;5fI3Z2(0TV~<6hEa!QJlz$WTh0dbsq-+O^+6#=kS8aWX~d? zA^{DuL$Y0FZ5>(>Y2?x}Z38blD&MaHUvZ(K3BwLI#lEA@d1-<%T&vt=B`VM$-; zpIi-p!*#MD51A5V#*(YHQ``l*$GF5hBn&*ga38?+1uj@=RW-BK>SEQ0S7Lu6GmrNg zFJdBs$B+96QeMT0KWf?okQ|n%euT+M0t6ou)12j|?}4YQ`k`v#3p6p=aIP!xH`r=Y zL}E{8k;!%45I5c=H@$T6jhZnW0|{gYhCK<|J8#%E#cbQb_6HH z3p>`fiHAS!25>P7{~Uk7G?9yf9Z~8i;?R|%jI4G4sLB!i=9_PR_~8dty}!SI_x3BZ zdc8i^+w%DE^z`ub?(0`Y%CgEdiOEz&RI6ES9U)O|ds}P`m0)$K7y2Qw8TF-5MIfkp znA@>Z@yRF=Q8Tr=PZ`bgY_i^N>#A2Rt6H(UDvhfY^J45Md!Jc zw%4_YRk zwWCMT7V1(}EpX7;##%q>Ox22Lt+g)K>-FX3WvNR${;kQDXnR7r{D=%EqwhDR=O^A7 z-p>_T`V>yU`-TlkE;A$971(Qrk64idOfa+&;pO1U%I?7=^?^1u*Hz10WLlQx#~*+E z=9_OtimFbfShaPjHrf6C{g+>Usj9V#XxRyD9SG>E8Mo}DNB4)G9);78)-Z9#K4Px|9@8@KR_#b2OG@_Z*N>u%Yefq7{XtDY3D__6`A&^<8QLb13all5X##h2tbOWN6w3`olQoOM|#&g%8tSlkg(F^^xaTCaOvJ=Ss;zWJDOMbGWd8lT_^XD*d=y50VBN;TcS^0-5OBiJh`AO9wkv^MD z(&}1rTgoCGP~4N>NOJJzTNR<^PBK{p#W)fvn;y%F>&WA>`&*i3jv2e`bVz&a+p-g3 zq+h8Z$#bI$g)g5lyQf*#)%!>BH82ix+3_wwbVp7v6eeSck5pBvl~Q6^TNc3jOAmGp zl4DzEZd;1~TY8aVQe~TRK${}$M7&+d(R%;rmU~2sUFJ{>v^Z)(Xf!i*Bu5ih*c*go zFtgJ3>_Qy!0v8l16P`k1bjb0DW=l|0Vf2*<+rf{ zH$(l|E5{#PGc=QjMA1~ou7CAd?^(8g@rPL?P>T=#58{sR-&)e#W>ry@sUKXj{UvaE ze0==-zyJG(hlkg1-%Qi=>eZ|HQm)tQ^KH4lEFV66{Q5t=yIk&HmQU+qGMicTIZ~@t z`<(=;>>H>0PA=B=7Ad6^6A{}cSTSl$uBlT&M7E7f;yh6FDG{^c+ecOrABt45RYlc` zR5jBWNi!8GT5Da_rRqAVuSKlYt>~}lRcft^$-0QyRCIE=icY=+&RG=FwmFd1d5iu4 z@TyreYS%L^X5t%sh?H8zL{-W>UGDE+t4`PJa=YEG*ISM4PD>FhR$Zy>zo6YwY-$v; z7?%@~)z>GQtYSr`qFPE>i!8R>*6Uhtr4-X@R;yJN=Gb{_H*_R&7)mhwv_Vq$;%7Q1 zmY+wvh7M2BGe=ebU3(X%AlMNAcDBm?K5TbM{cO{tYxK0#TIX49)ej#YK79D-Y8BO` zg{pV&-o1VMw#;*pT9<2xYSki; z$cudi>daXij6g^ud+cCH+(YEE{3rmK`dvov2)Ch`!niSJeFSWjNdtD4!aLPM zy2z;SPRgR;=qYNH>pIDDWNwsd$kHS4AXq}Aqm|1!C>?P=mw!}mPAt9YOFQI;gv2!G zP8v^7{Dlh7wGj8R?n4kVtED$Fri}X$Do@fxQ9~BRQ|e-ouMvz`0)}hUmz2$_MVYzY z@IG>%e4$<$w;X54ueI-Hbm2m_WeGzYNpIPlhFB-9@zVBEolQVEP(>Mhx|6wm;?6n3 z$mlw{e`JoFJ=+dJ-nG^$zBbUztmD#{mObE!*q)2u3tHmAc$tW7J7ZSc4)zE&x6vc< ze~dw?(#CdOPqiv$-MgsmXi(LvYWt48(P85WQ4BkHALY-!mIw5YWT0??i8b{GOWH*@ zdwq^e=%+cDkmKaANt#+(V!ehyO9@%I~ULRTF!mkOl%#jzPe=yk}O|elxqE6I;wM%Oh?(XiUX{P*q66ljdU6dT4+`mKA!GHB}JJ7C<_D`U?8XBu=Y%Y4HJ}Ev;Q?~*asWXfPs@n&@+AkTSYe>P9Ye+sx zT?c8 z)oRsRrB+@2&@k1hTu_oDMiSIR1Lef&Up13yx=hohl=*gBYQ5cVx4MeXf)gMVQpuo0 z`}#Hx$o2N`mZoS?D?U)*j!{aH%N*fF`%|Z*3XJR+K2Y}~=4;>aIwE#w=+V>OX}OK} z-+ADe3pE_@ZXgu|uJA3!Nw6ZpGE2#7)|zB01VJMh<=8#7AD|J~T;H1t?BP+n zf^vqam?n%K4K@iw1jxeYdb8tbTZ0)bx_>G`B=q(xN<76 zI^ri8C~7?NXpA8zOGlA5^E^{zLtQ9j3GDeWL_4n^L{gy)u@;!rL=0s_{v&@OD8kDA zZ+8~oC`*n%q&3|+qB<6yLn8eq5=O}d4vETZ4aRZxz<=pn6eKN?^T>aZfm!T)WeGD5 zQ60^mQaJ((AMn$V92<|W95g5k4piX}x}QOio)1UzAjp_Z4_*=|NFFH_s@Xy77O(Cn zX`)6Ot>i^XhH;fj2s5FOk(UOJvjCBcc$lLl-lvR1?c~^S9bC%DB9qTWw3)54* zag+4TOvpu{FQc(?v2mX!FR57it*5;#)B?v_j|#+_M=!KF1d%z0zE6?igzeeiP*)?z5eNsYr|S;4KV+W8}*ffVwD$R$>!|5J|t>|C-}n*0>mvI)}{ivaC+QdFlZ zP=!5>3gp_+8*Vj_)rsHUPZ&nIP>^~xk)7rk0)JW^k9mjoJ#ifLamwgb9DT8&so`^h zanfBnj&Zct)=xW^Sw@~nk4ihJfWlE^i7RB7L{;})I`;V71|{8Uw5pWcc1WrrF}6x! zAu2UA3VjtUs`MLTV12~2qhRv?v-a-2mL$2I7|F;y_fgf=JuQv2T3{vbXrVPmvYrcn zr1cjS1Q-Sk+gKQ0VRwaVCvw!V}LDXvWC>3`QpQkej(>@KlmPqZ?Mqn19Zve9= z2LyCGm0IhzGxG#RL5wzso1DNPc6*J^31=yVl2uB}RF~5yi^HQ_Mn_{+_RFjy51tua zNuWdpBLIlLXLrrc+uPf(e)X&8=jX4!`s!c)tABZafB*9G@|)k3hu=J0w`b=4`T6DL z<*i)G<#OS-*M7m3vf%thyXWYXUpXe8Djer)10w4EUFwc~D~)YFp@lU90I~{45CIz3 zs)>S#y4VAZzBa4w$lz|KLrmOY+8t!AQqoll=W{o)tD*!Nuz z-%w*;mgVm5?)QKH_t$l``K+nS%FaF08PWt%cxg{-k+F&O82cT)Xl@SxVsMR5sXp3M z<#l}n;V^^cjg(fqj|~jXzvq35UTgfOwV3Yw!nKv`eS&X0Kdb9$vS~{R&Nc(Ky(#JM zX-sb5+BZoivA~X*p|!Xk#&7~r4ZHSrWr0rtkM7j}JjLUA^ky5RG?irbC!Q+%YDbQG zADfE`#Q5;NP*I4go9;Nvrr|3JuHek@Gw=C1gPDv?h85^1LqmEmx?IatfI(=t>AX>< zF9$}M2N9v0hq_U0W_&85+oT4q{Vo=r?KdC+M?dKcyiLz%zh=?FOfR{d8;X;$b?_g^Eav(eAdOXVJDt4fFU}$=95NbB-_ziQr>N)+IDE1Dpml*c5#q|6HNiN#^_7E%MCyviX1TyYw_i4=S zSdfRxQ`a-BQY+bd@14SU);W39VIyP;ML-r1ghj>(aTqN$=rknosKZ$c?ng8hWo)AG z9@5H8jomQJ)Y6KmYm9@83QC@|VB-$AA2f-+lL6mg}caAGg=- z<>~dq<^FEH+v`PW2LX|afa2-2N>c!Z9M7@yu2W6`6=XqDGaL9p7em@JR*@(iI4lQ1 z6q=&OxDb|7r0hh5gaEKh-FIf@?OGSaT15kIk&s$Mo2slGW{-{!Rc3(Wsr&_4!U-b? zzz7A}NeH5_S;~d5m|rMm5kl!pN&CJtOl*GcXa>w2-I1OVyYT=EL#j+d|56e>$=EcszMuSRT2o2af)4KGA<9CydO4JiSrqpLS1I zkh!={m8UYry}BhWRd3$WMg^>VrBW)Z6t(!kQb<{>?fmt%P81wjFT z^$r8e`uOSHY3<=XM2~TZ#ky|c{aLa8rGX^7Df=_eM4wsH*<)@qW#B`*N%jCfy{XJjf~dpY$0kOpiaSEeCkhY?`@hmjW?+O(GZ zXUJ$}+UiRXa$xYYPcA(0XbG_QF-+#uEI#pB6;tQwN1jvZ<*!y_c49nkr3_XXFasm% zymGxf4Pe3f`0~S=M_?Zuay&7RmUg@(JxSALtqg;p5nmJ}P=)k2-rQ&`OgN^^;HG?~ zXSHPg`Z21nm(x~Rm`T-M=8%XpuVQ8LxFh?9f5X`k98Hi*Szt#-=Xpl3mqRcm%Qiq|Mqc3kOmt_0?F0ji*I_|2PZ&EJW}DU_ z3CVol8&d}l5^TIn$*fI@4_R^=Zy?^#d`jQ%vXq96pd)hEE!;q(XUErg+Z`FdXPTNa zl||wIZSx6ZQ?9J(MZ^{;8a67yh-7nIVdED#kb5C`Ng8^Y*%IN{d?*?DDlkuy>4p2b zUe*U=);P8}%>P{w!Y&xur2LPP+;W7oy7fl{lCl5{j9 zP8bQ$$}kI&D`Do>=aSL4t7MjCVHf5c`XtzI2SGP>_9RFj(1nb>ibFe%Bph%Kquar; zU*Sb(a602|N`Nq2_x-xp>%P@m0g#AF2OR*&aZ*AnzixZK?ra!>L4s~4c%US>AY&H$ zL?LKq{nMha36O|P zAn0~sAmaVQ-CzFYU;O1?{dbR#kN5ZYYaxRA{OO06*B{^Bp0_G*uUp;km*xKMvcK)0 z5cg$SWoIFkkyZ%+dYh+ca3Dby5dlH~fn7uxicXP>fHF#iL0{lz~bhcx{!qkYh^?r&^0br*9izj#X`gY5I}?x5E%qT5R2ZEMpX15 zA0evD%$0zM2#J6Y2!WXh02#2|x!H}FodB`qbzRrhT)8@zQVLNa??4ZvTkzWMeY$y^Mzu6jMC_$# zVOmhFm8dCQL@K+sSydp1o6z&x52G{tbSvuKB{ehFp9s>|qI(dVl%w)rTGSl@{5n-C(93Na>??8VTxg*1=61k=6yllK~6>U?CCFWei(k&h4CU zw+;aL9w%Y#kZ29FK&@57_3dbAbwwgVfFL0K$R%dR4vb?-8QXk%Ynzp++Kz0~(vC(K zV8LNSdRT}WxE|5&n_=wqWFh$Mw!#dqzpmwYA;>u8Y75T#(hgA#5>3H`NJPeGpJUAXH>;JUs{6b5x+1!WAIG29( za7Jd%n5hjYHI0Xg0#bj=o26vECNoR zpu|v}C)NJUhUw||!pyO&IL?j*xrGH1gC&nU5v4L_=TSk#j zlP9S?`+*%#=hVtVGEa$@u>X^Yq*g@awoefeNyi*QC;-^UpD-FxhE@=WhzJ5419A}r zl%WtrL_rZDAQl!BX0`+UlnNkheRyAT8#E8$w80ov?;bFuI3ZwWOfY1R6Y+aXKoEs650{vt{Iq(qXRj2s( z2I6}T9#>v6lDb&p59r5{S>r6dRD4xIZ0I)T{c zSk)yu=pKq|061_WBJ4UsjhKEWEh72}wMMq+1ORyY(T!^kd?O|0JTa1BtT(VsoX6q=}3@eLE2fcotb%fU_GlBkr0(sJZ`;1Fabg%01yHD ztiYiQApq>oV`!dujEe9U5v12DO_vV`5E`zQW!bjP@E_t&JsPz-i+iZGiZFnp!m>a= zsi4?RFoWZ2qy6qw`+S8_1+#%3fcL}4EnNUU*u2_USOi66p=E>0%&)Jn8p=)B7+H|o z)_Wb>HE&;Kud-JZA}EzB022|n(7Nj6SrF|cz)3&Ei@F(0pW|MpsZEP!Ji#yc^f|#) zQu%%UQ$&{1t;;TALPSJ83sNJP?(Q#t_Gf=~xm@1Ae@}qd*SB>ofBeUPe7(MY{Pf-P z^C#GZx4Q4Vl(Mdu3YUG~s#GdikFyO9QDJXr&9fp2i8eE6Xd_x=IF1@c^kOhYu$P|e z=(PFT$r(B#ZD9z7no_V3kwC5cF35-i7506%QdRq!L_>WeQv(7wJtzd|L1(v~{_mSwqIF6+7~&`T-ny58O0-QC?S>v}YMy-D`+8Ay%& zw$cG3_CAPrbRV%#Z~Y8!Wma7vAPB_&==wdRbVh@?5<(%C%lGLsVDfWD8=Mc+eJ(G( zf>^meJw5&S(^*07*naR5D96 zjzXjE<IJ+-X<|n34@MnFIg?x14iLi_M%y z=RE#C<06zZ^*_PW297?9Lm8t5KkJ_Z0e&ocrp@3kU=$`KFyJ`#nJK?v!jQxms+fMw z=IrV8x$?F!O96fw1hg zvgrJ9Zc2Zy$Xeo6EUNu~;5z%R zAhc+7Fu{q8onCt%IuFvf*Lxa*uLxof9umez4V-y;vI8(+y6^C7VrnyMHPbz(j68)6{PJzRH|y@xz1%vyqcKaiJlRdPXY@} zsz%`1)A0f5Oam!Iufjl7+#nxH(@9YT5VQrow|i33d^H zH03uu!905!Mm?cbVpO$B$vg}qis0+(_3d@rw!PMhWzn0{a;40?@B3b_wF=bv(A9Hs zV~X4g3;-yllu{OtLZ5l+$d0d|C#S@%C0WEnF5eC9f&~zN(Ef+NxRqNrlfmhwEon9TM_ebLPyD+K_U`D zMi4!rS=zBRaLWaF0JKSYk$#Qa(E18ALO`Ug@dALJsnH520Y;I!?QgHI&u?$9cZ5X5 zl`HdgyS7_tURyh-&QxJ@xm@n=@6}}>2_h0MM6~F$(y}aTS@e=6Tjem&ig>Il)!5gX zHcdc8eY1lOxsxGC69p<1r5_nVr@d7O*3`RS9i5XtQ~MW(yt&!HvHKP3%61(ZGn0bP zdn0kE0$cwLnECnn`Q_zBxE*>eB1#d+3IgmcVovK>pbW$8D0KgtNFL9AiU_h736?GedW3fI-XQs})G& zc^5$}+6@YqW9Z{Y3scr0sWKb2{~BI~IFXRp^A+TOm_u?qH_(12Ri9=`C(Klnr41vJ zZ%81}@Bf7q2$RbXb!E{7RAhnVGK1)HXj0%Mh{B5Um4+zM03?_xIujGB3!LURgBKlh z`C6vZ$ci(Awa2_i5TGw1SOKORS3GpHzs;6@CT*A@Hj678$dt}3MlYPHrBB^a$dW6P z(FyyKy**-TN&-A`5d9g*&)!>IxsWp%o|SCrLx61%l(;zkfcZdAFy1VXOln3>COhZ| zMpT?BJs>9+Wt7|&bs#Bl?dzIaffv_Qw)P{ul+-k2Ip7Vid*bbnp%4J2NyhwyufY$r z1hbwM05CB0F&Fh{vVO-kG8~u*R)g4%+sc$RL3weZ&Q_nE#}y!lLh#!QF)gt_ct~pKB1(lxLLqH^&+YvmU`59JP$@#qi+itN{6V<*8mOh=4GN5CQ<> zu$eL>&Xi{dlGTdXwuIWU!I{f4rT9XoDhTE*AbNtO5VFR=BO(z4Kp{Y^AeAdin-)R@ zkhU$^!o%h8hZ-L`TpyPzooo)vp74f~{H*^B500%lbjaz2UALDuy>0rPHzx4B#|Qur z1yP7lY&PqpL+CU58haL%r>m}Drw;piRh7|>0FIkQUS3}G09*BLXewZ3iJ z>-E|rFNdweW}D(7>vOx{MQzq?v?;Ym1a5W~zl;9t&okXmof;xS0#KmOD@OU$i@ce6 zT~`s=woMB!r2x?NdVP3!__zP&-|qYV{g1zWdikMlEPLH}ufhuzk<#wgQoWKOA4hy5 z9LnyB0Ym~&JM@?syC7(xbGZkD0DR2RMpPjn>VdUbKp2;0DT_8~EFvttajkV1gi>(r zr(bueP7b5#2NYoVGzfn@GkR5?Nt`6n{02}w$*iA2^Y{qBJ>RDS}nNa<>lq+=?S#K zlU>=-rHpl5mzD1B?(Q#l>$>VZuLkg9Ki)-Gii``TO-t4}UxTjMsTa%8q>Qb;$Z!#X zZzi+Qz-7#~Gqqx~qhO~21KyK1^4zJl0ZVfiG4^#VOLpr`8iXQJYkhfnVP*tkW+4LN zQWiqFySuwwF5FI5{M z1e0_vS1iO^f#ovdgFvhn`*UjG8O&C!|Lt`I&N_Y<)SdZ(EI=>(P|lfn1N^39_bLtv z{BX{)Ooi|H&GNLu+j{WL-hUpY`_ioN0Qjj8Zb&Ie!1x7cah?&+j)z&6kC7K>v%G=4 zDM>KGFSC|;57y!H=16U?ac?Z)>zM#O4T0dZjG5(7dswguRE5O$$&($#O8^u>`&;$&dF88H(QR1orKR={-ap1`&B8GuNTakTF;X%y4^KLB?PP zRhGZ~@ZKgs+F}wzruneej=Wwl5;W^92cSTvPInsu4o2Y~uq{-f|&| zE<5KSLy;Kv;h_isJ}Q>`6H=P}-jM%|Dh?jcuUefjJ`yl3`3%F}!1@3C9RR^#!TG%` zdBfXn4CqT8HtJ3(%bVSc^fLs|Z9LbDl^4VL8OAvw4G2 z9Y^_gMk)Fk!-UW9+sizsJoC3O>s5+t8Nq4C5{EEnf2@ZumeL&_cAA`qtTMA6bm>bA ztch(vI9Fwt;Xy+%C8vl@x`D1>7V}d|Mk!R>7V}Nwr$&1bUjBe3M;tmya7N#tSSXT(OFzKz1HXK5D_)> zlaK2V#XJ)t0_v&Q03gt>+ww2~Xz{T+F9d+v{R04GLOp?0>n1|PQlu0D6lShcSt^4R zD(ku|i{C_W#LOeW(VDjofu3$7fLujaoQkE;Z0#gg)R7MX0P2}3O0%XRR33!Iw&40k)^ZhAp8W0tj?{y}mwwdiwF{>BnoiKK<}|eYuLj z-Q_`}X`#cFD=>30Z}Qeohiua>%VKVEA!;+Z%W}C~E|<&Qda>!g^7)HNQo}-A&8&=>q%H=XXaX@d52~w z*1z;#M*Q-li0!*V)P+&d1720N)a%j^ZWz0#)Aqv?K!n%V?c;Yp=n1*%{-lr~(7Js1 z>dVXhUBif*AP8Lq`w{BR4VzD9_HHZ#xm@l-LUDX~c!w=!zKWI@x;c#vDq@?wZW#$J zH?8o%U%xR0&+vs>CGod20C3ip8-@(6XZC}!+ZwGmOiy#DZo2T6rRT+Kl9@_4&>SX|eO;LtMD!P=oRM+%csKIdj8jX= zUJH^5*qeYlKfDj=gMol^gQ3zuNqFyTwBLD-!^0yR*ObVq_OlFws}Cu?LZ|-F*PDTD zWP7XzoO^dMCC`_i&1%YB7MD+!=J3^1NiXAJi8h9WweF zX?8wRXlv}zH`@OCdD83+re+xa^is7akHstoWzdd(RXMmYGISGpepi_0HAH3X8=9t{jgQjfvXYhlD8@>l>zH6l~I6jrFB_oS%?aNHTH~1(oXK^ z5}Qg`f7ns<2NZhOL`?>uAF&Ubz%W3dhh!rXEqdSpx8;$e(F%(dwimz%_9Qu7k6MSu zV1TkT&yRq}#9Uc)ZW(#2*QeK~mGwPvhFMcLzxow3UzS3Gwbp&db%BS6cYpYY zKfPQowK;cy(Qljy%F}RzeHaf@Lu@zhwd`&J*K-s~_Oc4BWjXkXdw)Tkd7kXYk9vE0 z?CX8LTxKUrr*#Own83^za^W=pF#VaU0 zk{Y0pF%S^Itd9?2p0YMA+;aq@Wo6uEBZ3*m^4GjW!r$bZi3tjwlB!&X$okQ}AL)Y} za++W}LA*GI9%QNBmPYVb9v;EE&k$l?11tlN*@Encj-5P6vP(HLrO!H_AWs}G-mGt0=9xBg-1==(oyNIm%5Tsa z=#iuMF_~xxPA>yMQj;BAdbT(i0htZZ{|AfGQpU79ad)c z##X7vt(T1cn6B{o5-hP-ag9 z9fJ1`?{5>P@55rhuM0d%$0yY5*}*(Cm)mHp?)8U|9m41?zhN}TRs$XTrTtTGt! zmTL*%2M3rO^&FrAGqAyk-jGaZX*Hr*a|M0bsL;Mzq~i z_jr!}12RA{7WANAMkt6_h;R`q`K6Y!2W}sNg25^SLNX-8Jzl5!WBDx4L4OmW zKmpLZ2Io*Vk$E`^>HN8$!)@nB7#rf8CL-$9Z>1FJ`bI=yAR?3&;=ZnBS(cyu>}S9H z=Fk7b|Ng(eUSA&S<#KnA1>W9X_Wgjr5J_lR7AS-ZlAbcmB2uq=3kjwxmDfT;JmR9DWSJg zUhh7m5q2zxp7-g4;fqMYVM+EV69}>V_~Vb?e*10ng9O5@4S0P2?vMWHkJfd)$_Aie zUk4_0?b8M<&>0ArW^DA2CHUshE6YmAiGBHA#VRGeIaJ0hSvyC)5mfiqfL7~3( za>9viW(K{qL+!yEWHZd{pGhX9@vKogb|zQh1smk)v)neosYZORvvrZ%u$e|ipGn|` z&zU;LY@Yc&Bu#}kFg8X(O4%)Y1}9^$K*m-c`kb0hqWRrU1yK4NLU3qPO?jyI8s`qv6Vt+C20R=xWRYrT z>_XcmZ0!Y08zqVJmU+F_by`@2OIdUlQJiP?hZv|J`=pc9%2uYOFQt@%BBJLedgb~R z%HBKGG~~O`Iab=c`iy7nEB(HN9tx%dkliJQ1Mp+XS#eYCSQ<~sbnVlM@`ix@{0C1z z%mxL)WU{4Oy`}}fHjJr*(SN998f3c9Xv#Gf1F*dn_v%kOTBQ^Q5|M4&u5bHw-**PY zvMiTEYbgc%m1_V3uyft&zE$4)z`7Mj0s!c?WqV5uat?9}`Z6RRmzbTE;^l>Dt?8&~ zDuDD%9oeKbx3p`IE}qm{_2A*|-3|*OT14RW5>PH(IG4)G&-94iQZn#lv4fbI?SEnFI(47eT1PB3w~`cLZ#wT(j&t&w{uE z6e);!S=Z7ZBGMp#W|oqU|IObls50P zlO4h5hIR|bi>I3QyI<9EJ*ipv_V}&wo-o^y7n&ub&8RXrYu>;QCdKt-y_^Fe16ART z+ZHr;p;=-9S6LcosLNCt{h<)gqpungB*G(Zn~&wS9c`IAU$ync!<#^}_nASS;&76y zn99}{1>;Fx?=Fvgs~9)sl+68@leaPywoa58KH*`8msj?Zo-@vyw@Oaj+I zGT^WW+nV@n@b6H7qW!xB1*y$hm;u!w1qTI)XtZt zo(b4#gQGT$?93D*LcfqKNP8N};~KAdcY3sISJ7B$kYV^ei2Ww!p?C9`*=DS?iZI<~ zg@mb@>Enr+(iv>c=bh>_NXgS zJ&c;aAOAvBk zs@JGOK6tmnMTAC4-axV?$Sy_300qyZV`^x-z21FkYgp(uChluA_5f&LHZ%|g63#&wmxhY-fT&~bE_SCheB0=y{Dm5r6K}=gk@PSL?noar8THn z1T?pR=v;tWcmuuH7b1d?Ft!=$lOYh-s*_nttx8pd^_*pFjWKtxy#pX~eS3Rh+3zop zJM+Hn!no_1i9~jw=XJZ*TCW?~3AVsCtO90W1QKB8>$YwCep%ObT_xOue3)ltVO&aq zvTywH#~*+A^l4cZ5xiV3@87@w>Z`B5{PN4k$H&LJd%gF}nxb|&fLfx!aRIA(C~fuC z&@|do1~Hlq4vI!6$=+b7Wtj^IkO(#&ecouclG--tH@2C_5!Xjl{Cqaz#=S4K)*rtA z`1<^!#~`zaE_(vumtTGL;ll?#;D6`Zt*kZh;M$l_erD5Kgr)a>({vH=nTeNiZQOg_ z5Lp>=X-aZiMVPs{AT2=#AIr>oCUQ1y`!lX8qx;&4_kE5KJ!+jBaV_P6qH!!_qxv9N z>!3`hhir~GAld84WW^^Dqo#2ixVUUEp{HSr%pjSZ zGNhp%3{LLWU@yj?0`EqQFpmclf|cX{%^D0EIr|+f+BgaeenANSledn5{48E?j9G@E z*lg6^;sV#97U_={b@nP*x#9QOVw+VIfD|TbaolSn;{trB3zHiR)Ox{sIAzPVqI^S> zjWpynkL3T%st;K5QVL&#ko`hMMz5y|X6_e8oCIBauCrlUMlWG+O}-H9{>@0WS~9R3 zVYAY*UZy$AOJ~3K~!i~l;Ig*FnRxY zy<{h&z0iH%0i!pNSuds4tKzbH^*y3++YG92u#e?~0R5lA3=y$1UzWwd?>+6ZEY|*J z7ctm~yQf!NFR@Fw_VaB)x5j5*H`P~aozv*pZx6Czy8(rKfJx}pGcO6%evid@W$8r!j**t0LxMkk&(55A}Kb9 z3qVcLUGBcwK=Q_a5a0hoBp^AQHKYkvL?pnCH)cTqRUC*2RAyP)G_C*$pzbdaL<&Cf zaO+0FXj&r2$>sg4W;{BMGYtx2AuL3C$Oi2@Hv$Za0AQi1ZJPU_f&1fa4BQ2UUU!)( z2fv_hA%bOD2&gQ`%rDos1+Rbv0N3kvXF(`SDPgkd?d?rZBw5$BU|-|`VHQS^!)3|a z>nrwoA0pClTI%8bI+>_jfpl7T+qUcV3P4X!Pv3w4{Wsry^Z5Aq{{8!}zWDI)@bLcq zdp$OHS(bI7Fpjb&N~_j62y<|lhIx`F;3Ur4*dy+VXmKXy=d(gG^BvRaq+Ff<8iuB! z1bx(7y}n#Oe*Ad7UYS7;y+OofrFC5&9v+sv3oS*Py6hGLx&l3!R8LP%9Ho@3$*0MP z^``A-xU33O-_Y=+ezW~aL^4(y_?jy8sJEY|mgR%)5!Ww)cVH?OQ4 z-r&rFvXrK}fKjKGGIU%5*(xGFxw_YW1eu(PH1KN$dI7@dPXfPZk2h6Mr%A)pmSKED zRzZ@++Vp*}*-U?CKWHTUJLNhHXUYpW1$n-LTR5Bf3hoGIV9S*Tj$qa(1k9M_dIAC_ zJ<_}@%j^W=g9@}LomRN*C8ky!*qN$3P?%}+d8d{Vc%Q1kd0JYwqbbeP@*_tn zYqnEJjlQq2e>&Yd5Gc&<%id=PCDKOMS-<^dRE;gDXwZyE+yCzu0)5Kh%stH+GnW2_ zDFr!rq?v#7^R1&`<20GR4}BqUcA?WiUbZ2@8A-teG7I9iS>!5m>uds?Ry~zo)U_jW znw@%>t@pZ~B;U(wLLLjBM2|UsWjqBqEFtskGCGHvMyFrJAAdA`WsT%NWh}STIcdu* znKz$Yptmm%_zZ9R)0Z3y41OuR@e0kAS{m=|gAGoNJ^T(vbVjWsOv^k<2!;SB*o&Wf z-nNcH2n_pHUtV6GpPvmgIvI@^_g?7itsQ_nI1qQh%hX7{Sc1f0RI#b=I0M)7c7~FW z(pL|n!Gp&-XG;UxVXdK9vrmall*WP`X`~0b0|5Dpq1rT}nv-vVbSe}d?jQf^um0xO zzxn2uzxuzo*ZTO?7x(M+ZF__3RVqUfM!?oyJZ>sM6%APh|)?JNuu6d?_{6qH^Q5@}vW5YdhPsAa|mQiFu`YWjZc z3%F!*SaJ)sjh3m>ZYMo8@{Z$3c5a&nd6>y>Chv8gDo`)(Fkun^JR-JMw-_25ll zK@zBS+xKnTnsGui4OO`-dd9-j)6=@H zMD+Oh_{A4rynp}xi^q4D%Vk-ql!D#Sm_d?@?*litTaYYLFB|6QAesnUZ1i{vev9hm z!xwm$5zTfS_UB3m+y5ONgu~wTjjdljKR^Ha*T3HP-TIq|2t>G)`*)9*%jGDAP!Zzc z;ay>H@u9}RTK4ZskE1yV+Z#1rPt(^F+0!@Ej*?|YCTaN{i6$msWC40sDxpTQjDX;k_CaTeeW zBLW8r?320ESw9nq|I-hUg8IDtgE?ev=8ORtnG!?k8FJ3NF1V>=E6~TG=tCz#vVjIj z#vTM4Se{BgB(Nv2mVN9L;a+s6rfHgKInpCsvLE)OdM>8&zwreBpxOOuO6FSMhj^}B z@)Rn$sIwImOYi-|G^M41QPeq^PidALlp=1t5y}_09%-LK!(k>9*q`EaDl{1F4n~CM zC#&i9d> z%F@ds{VdJ7{2OU}0!UgoQ;IJ{Zwrys5o)^dV=*T~Ewca$i1dJFM1+3qUT{|Jce=!E z_^Lm2v_}X6ehWO=W!xrA9Z$M6gFF#Mpx^Q`F>nCj7S|?GJB0-LS%GOUh>3{$37#{k zoO*u4vy5R+=5zW)9!L-QY_o4T)SmsGxF|(WszqytAY7k5L|=NEO0jQ1zx_8Nw|vdNnwf`WV%{#GlW3tA9^ zlF^ja^%W)%5wQ@>hjOL@?Bns?*^vJ3Xz!eMSOk~}9ORgfyN^;uMwr#6N;z1wqWlp4 zwx@-`aLn5b44U*j4t>cXrMEuI>l5Uzk+)(`iHA+79Y%yqp_uiBhybLsa6Z{ovl9sb zflwQ9tB1_CMtVlF>|FH}g5AVPH>%aWFfB1)| z=eJLMh5P%**X{ZB?O9}BU`5!Qd7vP*L*WG(1Q-BO!#Y6^ad5625wSu=_W-oX7XT2c zJ?>Rm)KUbINKk~45cCo^-N4TvEKm>y3(`VF3ljkl1By0GfD8hN(kHAN;6!ZA7DZwx zt9M2K&@MD<*|kls8YOPhYX^y7AuNc687hm&uC1i>{y^gzu^35tnrh==YNG zGdqL?06ORU?%lil`+ESmUa#D*6huUp9eRtRrvq=BJ6@ym+uPgo^RtRVk8ZfTyZi9& z{rmUt-+%G=@bK_(cW>w9qPrQ)1bB#1An4Qt=#!oOJK9BD*YY-)Yw<ZI99l)N_FJrqNShFA26|go#bFm+6 zjNAntZ?}5Gn;8o zxeK5CP$6SA{O~C~CH_YU(1_-F(zBXk>`Y}jd*&_>c9z{kcZ%M#N<)GS$Uv2chtEDm z+8JNdKX2IS4f$uQ%(a|B7e=Lc8=o4rEu7esO>9bYR;xW>jx}(XrQuo2>9bv*6%t62 zu|I&bRU$kFw39)prz)(>1>>D^L}|<68L87ZfsrhV;1EJlrpNGS25NQ|RU9`I4Irs4wk?7Y$#}aDL;Fu>}-4+8ycJ zlczS@a-B&gH9g)Oth6(4&8?u=KdU&iYXG=0DeS3oDt11hOk!`3#U5#SvoA6v zXLRM6%0Nk0pLq<60ND4vp9)Guq-Rqyo?d-%#w@xfGH4myV@92OAeQEac&D)OoNn8O zZloL>0-Ts$K=`P{!)C*3U(nYS8?C*bwa8gPcr*dWREZazF9Akd%SNg~@CGt~dGx&q zfa#!&;9Ja{dg62E)20HfI*blS{3l<6)n&)*dii@L8u3=Ct3@QK$t%d@lL!MI7fRtA zmcBGeEmug7=YGk42ky*F@)~mxc&I4X7+~h>>YcGzj_+oinasexU)b>aI{G9~S!$K- z+UI|$-KAurvjVxFe!K7c)Uv$5r$V^l7$IR80G5E5fIROMey2y{aM4W`G(o7CDxTw` zX$y{}&devG-+|6xaieX5ueCh@LP7w@RsNel`Ps*h-~BKD+yDIbw!QoC@c!Y$%gf94 zZLgJSZTHq8k7Wb>mtqGjY{vZXpu&08~mTA_O`stK90WrD$)0m04|@ky-q?x+XUSz!vIxOs)2R z%>V@C*e1gW2!+amL|U@CaRE|LNLz~rA_PDLZPG;A2_KA55V1--CdnD0Hb^?UK28De zWI*E$#Ln||Fes=uW&-GeyQPpWS`twOd3kx+uRB*zGKiK^N|N4>@sYJGNyEb#J^slJRbEuj*b5D|xh(tB zhNT!$jkmKl6wmMe%6JQ9hL^bnFTJd|!IL<=P1%Q} zEW@fUK>+}{TP7k7$3hNlJ2HHbi&Ab85XOi)=O9c^v#C48n;5lTGWvVEup#}-&`?*F zQutw7%j^eArZFa2O-8CM{D9M}m!$t4XbWSNGbn-scEGviHF4!arU3I!VCrG=jAxdu zmrPLN4N7F35Ijp9o^0BghENNfh}73>lfVA#LKr0sGtmCominD_3x3AIO=Y_hnXKE?-r`L-S?~AoSJag}X zSzl)MQNdok2JL0uqLKA-!>0Tk@oh;?$)2(hiVCJGIS1wcoNcGN@L<%o7FZQGeQ zY@9;G3dcdWo?tJV2Sr}Q9HQ#=ScjkT(5oXcBkT7Xq{Sk9I*3*>K4vjtBBb>y{ z2FwS?+jrJN?^BB$)88!atUb+6w0N&&D>r^MeaeUjq;*=XH|1TynPhGoX|T$$wcl3} zDTHmr{~dc>8#73J`%stQ`)9f4JTwl-4@%~I&@{r1`%#NMB&SMdlz*8}u02qk0cc)(2s zj$oMf0kRwrQRfx0&0DGxj)0+k-?wdhxxQ_C1wB83R>H;04Ox)e2&HaU-Z$%YdtvtK zPA6);sKq^*a{YG+`457|{+XBT4G#ics6k<7o@a%}KhqLTcpuP~yatv+b+7BgrPjB< z`m4WrdV2cD|MY)culu{l4^OWzFE20KzOPuyDy(;bLEGknfP#Wp02l;yGTJI4B1m#K zlpqNRl!8)nQ? z6wE<`{nZUp@k3Gy)}3pu1aP-rXy<1@6hdHadurWutppK?O0Bg5+K~&|zV<`zYdcqg z2n(8l)mopPo<4p01OV&0-rwI}E*D(N+E;*~H@znFQi^Tg5Rq-$YTbnkBI}79SC)O> zpPyk_meL!HbzSf7?(XmJ-@SYH@bK{A!v{SxcU@PheB8ZI)776{Lcz}cj{%<`B0{72 zWP1#V%J2Qx;DxiV@SkApBDZoFfB4~tr>7SpDn0gD{pQ`>-Irf}iKWQ4VLwb@(qSK~ zIQ!};H~PbVgpfEhc%w|;2xYTL9-=9f1 zH%JqsP7C8!rZJMsKbRVaZ62HfQP3T=58jl%6mP3Oeg@4i_}^&w&WQ9}gk0GwZwqt! zGO(ZJ?6vQ|1|eiw-Ozp@*iVGX_}FB6Poaq+uhZ&o>2v9uyv8PD55#ia zP?Htq12TO1-V%M`UVUe+QZ@(AqZVjQu(<0qGUJF?b-XZ}R}eYE=JtKRUUvnXZ`^%i z=^-XQ*y8L7c9%^xSkshvM2r)-xelN0#+yN`(&n=zba$Wth@TS8$golX5rs^MojrfI zuhw4Gs~-e38c;;GZ6l(~y8iWF|LvzwKYaZ3-Q9Zsldpb{dH?>0-va941-l4iK>%#i zi%wh`Pm1jtQNd$=L_~z$(e;3gj3U*Jw?I8*NDvWi9#a1$=)e^OKoS8eM0z?h5ivt^ z1OkF|2$`rbGotZJ!T>_jVsEvD!~UaPq^2vS9TJG+nuf@6R(&aL8PI^}6hs2Yx;-Nl z#}XCjM<_G}l~SnrHBw=r5IbZB!;3#u`#PriRyJ51csk;tJHKSQfQk{nMP%jI^K~FRt7onyfOw&Wg=^9#v;#>esAZJ41bJb{K_Q~oPVE(tkUGouJK2CQb@Ljqs8Nz9%gZ5UPW zwZDm4QncZaW!5rcHf9*jEjoO&gCV4tBn14%?|A|@y< zGoFpYR}{qMu{u>p=Fa3`39>LlA=wzMMxQH}87Ra)Joa*)QtQc{e#$Ss-m^fa6bAeR zX-ox@YtOKsCJ0{x%h_Ri$pkumMvrP=k;2}?IBPR78zhsRWZ)tTCw$5n@OBg|G;k3_ zVH5`6^;ty(3IqGoWS;CPo^SllE+Wt~1?QPaJ-^_ZmB8#-3AS{dJ6BWWhbNdei~`J( zIV)sp!Z#F@KY781(r|3)ET!LXn+;9ICNgt@uGO<2UIOSgyLc!T?LM7pL|W;_RKPGI zN(-`?bT1u8lj+5jMqdH+uDZ1DB0!3WJdh>;{krXdJzpb>0LZaJwU-aj2fL+DO&T#g zn1(O&76My*@d1GLJ|Q9h4m4}7;88{5#MZE~d%o12UrM@gQbc;41B!xXM|#AD$$912 z(!&T0{bV}TQEE_MFrTTFfpaopM%BYi#wKYHejlzw$D5@I#rinYth>N?Fj5`A=Vrye zl9R2Zop7o%M9h8Dz#zK~TVCd8!&PO}#?z2=Jp$&S$D+QNdEe`iSQkPR4;{gJ<|ONM zk@R?4z}kX5~;`l1rZ98GA`Vj5Qbqk zS?MwN;m>3t0ALW(gB}4G@ zj=T&hIn7%2kOPf8<~|z^>vFd&D*)6gIvEWBc4C8p6^}XSp}ka33+|9yBGO)ofCz$i z3;O!f(L@DYLd$UMzi_DQZd!E-+He)#_T?_bO7+uO#>1rS(x z+o29&S%liy0{|2xonOV?05m}XAhr{iSmgEfRr=N#n_nnYL}c4G{j@9#(V{{t1(#(( z#JVWI3-gYQ+xGVMw%ZB023K_xZ*Onke*0}H<=w->hp)c+!=L{2>#x6l_wL=gt{O-U z_2?E%-Gul1Q}-cmONAxyo=#F@KgHGAOIz9QhR{yKffL{I^z`(@4?pbt^=`Ra76Jfd zA)<1JkB^TJ4-c*NEq&ZO8;s@0WE)!UX_lc8g-NmA1^E^G;0ArW8>%w^Gc%o#;oAW0 z038}3ZyZXJYe+=eexgJEE-{T;lT15SzMm)Z{N89CvWPq*)9lh)bibAOhJvy%Gv#Nn zG)}FXo_DB;>znepD>)bI|MUY+`-+JBzK2|%{Y+Gc@`GZ61ogI!ylGYbW#w zGrtEW`!gjVFft%>OlEWF(5EdVI(a$c#lQ6Id0hGc)}GHW_Ay)tufhlOUp9UYe!h$@a9_pB|Rrlo((WSeyC{ZxPF~ z1WE#TaNgXI+x72-BhK~%gZAX9mZ_-ihsU{JIo5W^TSTD8cVMQRnt}pU^v}{S$PMiH zDtxi#E=)we@DPWEX%-S@PbO?Se=(I|NN~gYydB-9A+zi?ezs&db!%s3W0!(ko;l|4;6x+jz@&vz7V4$s)tPC=Mj$9oaN)Q^LQAS@xm!sJ1qHQ8{HhY<4#>l zsMJWc@-0-*mmJL2I}-zH7N(r{yF3A20$RGI&v`?WloR*@#kD^Al4fo&I}J=NwRaAr z*T;{o*zsvZtc=YKVw)%NUyU`R&AdcAL2<-k7w^IE-QAr|4s6@D?^oZO^)A|XZjI(% zkiG|t6!Oxuc?P3I#+E$@>dcCX3eJ2**0rI|BQ4YwAeyxg4w)H{(C6!9)+FVs-OJeX z@x13kdiDb+IJW7QxQwd;eU=Z2P((;TXKvrNdc9t2tyC_#QY(#n1g?dlOjG*D=~Rgy z3N$$wq`f7INckWB{lA~e#S18W%4opR|MqW)5U)N&#C|gv4lk`nUkK{C1Aqtu3@Ctl z&lu>xfnk9LaFrbiMOYhi zVLe3z1+bJw&wA|`>T9If7wdHFFSW^oP>&9(I^(LLz)HCwq7b4UJA@3pqwEB%{RCMr z%jJH-h4BKrY=Z2z$pZo*kWLfggX;hQAOJ~3K~&>*8&I_?;@LIIDphLXoBiY!{SSnt z3ajdb0Eiixr7U-M_YdzL--+P9SFX@jCxig>Hok(;Zq%cGQV0PNQ7H=of*=(H0F6>5 zA|l-wQbeSRG$IKA^-u`}y(zB88>;FF5CNGhv+!O;kWm1Up$ei1)Q#)LrL2GW`~Tvn zU;n|w{kzM0x6<mvO*1GRo-Sp-(M67!)Kz5XMYfYi* z!N^F2eIku3*KOxbSfJ2?NPvt8fWlmnK!mXhp%5SfqX3HR*Ed33h%QT6*Y$E)1mwEC z?R6Jm0cNQz6$#hNvaHM9{r$G@PfyR!uP@i@l|}U6UI7xoLItVlF*ur;d2+Uqw^l{2 zuz)Z~ycsi?t+zx$Xj&Q5M}rL>icp}#`tl z0Yn5ULQDDC&wlpjfARCOtjrYw5m_zX*XCOh?7JJRBv5oygvH%-n z@T{peS$UfMr>Q|1Bi=j%XhZ8ReK6pO^yvd~yp#f+K^nf~EUtj1pn7n~2d7Y9awyaK zxK@W6v4mw2@YTWlx+KSL&8Q$2=kvh4PSWXH_PM( z&W#v9F0#^my*Rc+^waFY=VXCQ9fB{%&(ax~MWg_s-Z+h@n=TPrr%tHvic{9Kd=$`# zUF{GGK%X;pytFgxtP`GFK+CATZ}guK0U1#AELPB=AP9hP4@?yRy_KXD&f1GT!c!W} z0!WA~QW?1F9FGpm5CjE;8377WALkyOgeE;5j*AdrK~k2zSe(`nkwAcuv^R@-ilCsd zKmkEv(6jiQ?x=;+nh9`|6&4@@A|#F50-X?`Hiw1IwTcJ;>r8|aj_vLi0TAY@f9k{w zA}mYmC-nIj)Il`qn9I#GfFDmAmU>8y?^Ag#`>ut7b6J*US*E@h`{kK_JX)G%JK4?M*6|HWs6_;WM z&Dkd%(`ZM|LY}1!r$9u9>I8tLEFw^=4s{Wc7Cll{00kJ@^#-+8M4dn7T6Y~6=)nkH zhKQw<1pswYPG!EX>$(<|=)P~)>s5tcN~yB5a049LSQt?hRO$gxNks%kU}2pjGV?** zZrV%e^GE=Mddwgr^y&XTAvWUb(1O6--QBV*VLCW?U60j4?4v<>S8SHz0p^oI>V;+) z=Uzcv!zR+xeX$#_lBqv*LP_6r>n>(4p203HJW$usOP2kZne5|h78ZDYyMFim$4^f$ z+s;Vk?(TkB79t{A%q>)at=8-H?RvewyuH0$w_4kdY4_jIrl*!&xf}H2V?orqaXcVm znGUFeoACPc-3zZ>ufO~$x3b|0?q>SjE=R_mBA|}dS#5G9yt_R7@xS`lFE3C3^w0l! zx!}XYyN8!gFPrT96NlC&$zO=V6_&M~?#j%<(fHYcxPKDFb^>rCZ%}$cZ1aeX>s|Iu zP)J=GGjH3rhyY6|UK;&SY25rm2mnP71g>>cN$LghcXxO9_ZR9% z?YzFe9?`xciqcxE$hFqWAa8GP-~RSD&#y1v|M2m{hY!F1^-u5a?k<-LA{M|>hJ`gr zSGxP=b~LWTga>>klkY_zaPoT3F(%FFyGR)}vADaZLQx+-e*E<56SB~X7hHO`0f_MN z@v)Rr0IQ3hW=?eWCdXmsSPV2y{SPDC{`H>f%ms13njo}cxpqXm29nFbkqHq!|XNS9-HyR!sYnkI8+wQFxXkQ#Udr=8wh9tecl@E&`o6H>%qNPk1xXHo?Rn;W2- z`k$$Ur;rGY=W?b5dl>ta!IOBUdoqI6zzlNW4AYA8!4X+pHwg9#tX?Ddn5S^FrAC-O z(*=nsB{w`(25qnrND$2dp25I9op1sqFJv!)sh~WP|6j`9?@5vzITLe7L}pd>@9CM@ z;gUQWmpqE5hkMYI?Ee23NyaDXs53pCbaqB^cV}n1r@E^uGXm~<@PNk$cSO!ItgSjT z65;S4pF02t0^mMiB9TBRC^nlOD=KLvGU~y<9z{h0yy4^F^UxWGIvnZ&AIz)b zIQ50P58P4kzf*sP-tG8|e+dv6`mrd8K3JG9-`(oUx|lkHbf@hC8E=dPGP8K{XKf|n2w8hMHU=Wp zTN!QPm#e}%Y|g+&wznN6U(hyW2)^Kd%QSBHLstZ+;%r%^s+LmbdG_6km&JB#TvgRm z=S^4p@S%xg=n8r+%$g^^U8TX4+}yP6Z`k_)YD5Yr}wyGy`lx`Ow7*K-Gh0 z{m7L<0c{~S)Xv9UvY`^P<8S2?Gt6vnw_EvIX$gZ+L)s3Hnt6<~`H;Jg7kQWD#7!3oL=*dF|#*##P#AM!f zjI|vDK79D_kN@~T|MclkKmPoO^TYi5_50;scewC{bl}U?IO;v9y5@A+5DX?r? zWpJUHi;8SJDyS+EOC@eQ!pdfe7Ls80Rn?Xlb5Z8f=HyjKY-wlbZ@M+TQfrmAfYvkt z>dme~ssx+aGEv)HiAa^}s`Gg|-#y&j-4T`La+SIrNxGc~xt*`3%(NYoxVe_7swmU$ zrK(hIUeLmAXL~oMi>mJdQ08fx=b5xDmvyO&tUAx< z`_uh2O?P+m{r&yJ!^7LRZ@>HY`|I`k>C>mDr>8H^PwTqYWnDy=d7kG*E_Zi#52uHz zOj7H0S%js)dl2Lu1!56Vu1ZX@iqN&L)Mi0TVIrz!nn~~5ty*i9g@}|YU8TxWN+B(G zcXz6~EDJUp+8VH07i$&Pb-iA%U%!6+!yo?e`?tS;^XAQmckf@ldUb#QYI}nNItuPG zA|z6S2btgXZqgwh}zr zQoC|T^Y(=eNvJUxF6a=y?w;F-WMzh|3boFbi=Q=*S8gVdzWa_xo~$BmSw-*k`%*G5 znVUT1(C>I%(Qv)387^cPB|So&_E-wyctw7n`UH@sN3wzG;-TacF0V47E1>F2i%5|ng1hM z5D#TEQ9rH+OxcVl;BOo*2&_8&NgW6KB;L%w1FwPL5$cthAC9B=}&v!_6T$F&YH+h83a0 zhx}lHVbBW?9?6m=3j7D{9eKKHe6x*`MRHuSV;b;%W`(9L@JK{X03}7(O zyd6^A4c3qd5iw89#M})+soACP`IJ&7k=4;PT+)~{Lv9@xv(|b(pEJy`Faa8IKMLe9 z%;|<8(2F_9orPMo^|!kv!2L@D8F-lp%C6K6%aDPIQLFSFznZxs%sF6kooqq-79Z5GsDInrj{>PMl9nI1UXLZtl#tYxC%X z6JNdRObsc}u=%iGq0Fkn)V++ zZTKGdn=x2bmt{Hg`MYm__)q`ofBC0>`rkf(`FXyd?@zC4tuM%O9GH(XU^VWMKEq7^BXh_#|)TjNHos!B{Ox;J#Z zWpmry)ZyS073dxdu-?uSD_m6B-VmCn^E{u4ifz@rgN#|VGa?##BhExDM3ZVIR_2o{ z-{+kOcGB?tSFk(2rqQZkW}Z(o(WZXdVy;eHYn7>-?oRi%KkT|%%exdUp7m4J^Z9(5 z&qVaiH{V>Z*Qd+N)6>(}uV24@{kkj*5#7zFd7e+F)9E}j^SZ9rdi5s3q-fJ?TQqbl zT-F7)R5L-CneR{M^Z9HN;e6fKuU}R5?)-3ne{XxqVFK5^)EAdoz1QpY<>lp1fBMrr z&u?D6e*gact5coQ zhuTA}HU==Y=cHQPfZdj9*9j^O@8jd>FM&@TH4G>t`fp%9Q)B4Bp>j}O+9T8E7ooh1 zU@jj(PS9gOUFE7dy?2j~*k@^%vzIA&KgRR6{%o)m^$2lUkaH}IBD)&~V`aHe)kh8( zTqzI7>eq`vpLz=-ZXj%UW9}kA6k@gtgEVjMHk6`b6g#9^^QtH1t0HW(b z$8@MhbZ~zJrwszbN4CgDN9YET(VMXF5q|K#1D@N;<>19MGKBWMi(p3&SveY@67XxT zA_x*R6QB~F@@JiL;Bct(EK%0m4Mc?tO@CAXX$~m|R#nx)6bf=foUEvz+faK@POxUC zK+PqC2--Hl30umBRP0ddBZ8U&JN=qPhMGZ|!(TB{Ih~-lobEZA=W{xNAoZ9G4i%Wm zcXC#Oz$>!{9qC6?qZ7gu-bdAi2!jN<}TuimC5G)6;UhQAy5YM-@ z#IOS_P%zNP*$#yjottH4hp38R-;ze1{D24Q0f|ppc7{ki0^_g2xGsnnCg@kR49=k4 z97YL@0|7spnwStY!WszwDLWB!GpyDLQtRHfkcFaXB|;TqmuA7bfE(Sfs-55(3iciPu^>4 zF^4hAnRl%dZL1hXWVzO5S%h?&<^l=aMs_oq!1CJ+V3cTxG&DVd3*-oZs+hA!p^%x0 z937678w$sgTEO{~$j?l#d>rYr>T`|O2uTiGf^!cCv z>;L)n`SCoxnqE)q=ha#X5#hqbbs}a}asKW2El;g(!#~@JW1j|8(QS_s6%na6qhk9k z39Ij9NgV=ImD*fODMg{f)Yj`UX%V%#K~`n58J>0$8mMSjg>H@C*4*1)6&6tuDrHt7 zWtyhb>2z1hyjHm`Ra(u|vbTiJMwOUEm6%jXiELHrDwRa0LZ{QIz?OxrL%^jJm0tn0 zX~}_LH}B4Os%j?z+Ab0-T2~R7ifu+SomnTHh(wB(<#Hua*4;PE<>vYP>eZWXm~E$< zWm%SGskJ^of4yF>m+MP=OM+gX9;RuseQj1158oofI=x)4%d(iot6DO5Q;`qjpH(V} zMRs8-q}xPy;dQN4FS_XIbfTHqG+g(%yyzlD_+;8vQK?$1)YJWGUDq#9kC){|^$#L) ze}Dh(&D+2D&2Qeld-vhPhgYv&l`@G4YhmW)dTr<;X6-@Jh2UOKBQmt2 zWIJi$v-{Pz&DZPo^XJdYva(36m8O#spQh>c>({md*Oob3xWh^_hauWv*ui);jCfAO zrfY1YE)$1akJd4n8=K8%u{U!Zsdi@Ue^J%;zWEji_rAV2nOM8@uXF{qWYgS;oWu4n z>n$jXdX-Y|$TaHlBLe-VH&L1YP+du{kR#c{JT|muCfqfJVL(E? zi=jkA!Toapt=Aqz&N;`RjeK>Uo;tf;OQ0NT4`2uk_x?Q|GG#u8K=q}~Hkz`Xc@$RT zIw#@O<}7tUQP!;&EASRrz}0Qt25bxqxF&~q2w^I#CC4lL>8R?Vp54ls&T#2;76Cmy zvyIH4S7u1#PuVrH2$T$p%w#R)*r;j9fo$MR(h(;a+u?rJYL-1XR|tvO&_@9w5YONl zYV|gfgK`KgmHEV(U@RYM2{IyALve28e^FR8A94~v#i{9}WUs74yYj+bL(F4 z{2lKT0N6_^4A_pGC|sN-POz>V+%irwSu_?dn0n2J2e3vC)=tiv25Y z>s)q2z=mm!xTva55~CNzucL9*}aL;I?y6BbYM3A;6*WNc^PUi-cf>zcT2e6`ai@a%wuwYI7d(; zeno~u;mkR;evuOiK3HJqO-G2(9^|o7mnb;C-Uq!eKeh8SS0@2#dpJI zOg1-L8#i+@Cfl}c*H)X`?8&y{A-~YOvUiH42Z})xfb3W$==7o{f!cj2S zjzw)vn*1_+@nGY#$~=Zyw6vp_ouVCN`)zd|h;h-ogSRZ-%@KmY6B)=vNZuzx+PA zvpynob({{gHF-?!U1+z~%i2(W>bamw^rdsjiH@gdX zv~X8|SaKxg2H(1fgnL}K;!!v|(rvHjWkZN@?AqWX_rtl0<)Gs!%j5azYR-&&wnBZc zzxgubjfcY=L~WFr{ChT}a%~ZY$3R0(t0~&GrV0>q4mYM_q)xR4_KPu@4Oc0)q*F^C zonLM1+rs?w8O&#wm369w)BMm0lA$kGp@YmwrAOkXG{!6LJnx?>tm!z!HsJwK1`;5v zdlS;N8v%uQbROg#@{d&v3wnyt`rix%qP{tk2S=u>W=v{k3wHv!ja;83r`OkLMk8%I z)}G$q^~E92hf9}!u0c+>?-Qiyr=Ag4ha-S5JKf9P5-ZV6OZr5&q=Nf+9Lf*X9y29g z(c#l+$(uFLEj3VlnNTDZT#F=#?GVCbV|y|Q+0%5HAtOz$P{8|Az#GJh|M%^30PVkk zT!+U6WZ@FXX3NqwdO<$U-q`hbZr#XwTr#igl=CMV<1u%n8rySzf<%H(>g&UVRD3q} zT>1PUF7)lY3Pk=y9fD!?RygxWR1M1hT|Kc#C&v+&M}pN_y=uyhxB{Y^cSM~s3Z-@t z9)2|(dR8IVYNKL2{owFop=$1i)v0yyw}`yBAL|KAGEci=5^TC+^?km}naE4%%KfoR zlXiPyLHIu7BWUXKR#!f$d_4OoV(ZhI_EavP1BkTa$}GkYH+oASdIdgYyP}$MzfYp0 z#BO`i{TrQ-+)hWG|Mrb4NYMf4yz{4qasd`svR)Ifw?870 zv(P71>8+g!%6ilbGOnP0Xr4RII$t zM#x#-B6}a%YvgHNHspH{?wV1T*S^Musy9%pS5nZf_{Sm5lE1;0WjqcG%%BtDZHGov zluh8Bo7)Bk4}PWS>{?oejo5lww8t?9TscvcticNtnv_fCG!!td9Aj4GtXf7j43J18 zC!L{ytf2vC4FxdM*Z>@+v$(s}uOm%)8=#cPAMKf%vmK;%pFsWP>V2H^Yx9)pM88(h zMPa}@(ufzAFlhbk{x_BoH_G4!3?*R`0Fiwkt`h%-TqKr?(xK4YW<)4_m{O)Vpb7={ zDoyuF1B+DIwZ4i;-RbZfiLuYw(5xVxZ7$P*&glK3bJYTBp;&kb4;;=Z>(&{dnV{L! z5zlbuEduCnXBbF;Dp^JM{)04DH6n!q?QBg1yY=_C^S6&!}GG19`Tc&fTXP;vA0?ydJ%pz{gR-5D`< zeJ%99*Hd%1_qO(5l+0O6qA^<9XjOfS4ts-BYW}YyV8flH{J2KRHle=PY(<}H&;8VL zS35;5ba3NO2rNL^h}jIRn+_UXG>oyVN&$0eHWK&2IUUoQpYhdmvT4PVfv}SxT8~vu ziA)sO_Ce;MFlEFY(>}V`oe>1r5f{|OcB#5y=|PSS@X^Sd#z{(!k>lrDEC3Gjvqq=( z|6c8nib<7;$w*13-Z2I=B)3^+Q0Hfurnps!yJh}!<&)ntO_tGI1l#@G+2B=co8EZx z4e)q4<0h?|yRlu`gO$)s4GTMJ)Xp>^w=qemIt#?^1V(SSCma>&mHtsPd`Fd6b{0C=8(-@Y(NKgraO0{S~GyY&xmxSJuv^6D6>8~I145pT|mqZM*WBN zpYHJRF-^qi2?|pgj;p*iIg~1VL;Ty+1!?ZH_*io7&gfU$N3PcKTI>aeSEOMC~nW z%PY$7Xel9{BNV&xETkoQ^~TEoSWED5N=T|<{DYXV+A*y2Vw=q;#}9ZMi$WUAw)Q3=VbeQuLddyua-FKL~!jW8S%6T{k;pm zPANpW3$l69YwF?sRAp~xaL|lsXI$;tqpB%x2MrU7dDD!VXn5vGC!reeU_vP~c-ExG zqDn{60FEB*H0u+Yw}T*FzBaj-Pm}6GOx;g7%zZY&zgv{knR9*~UqP~XWNZ!C+cT+_ z^SjgMo|7?25%=N5WMA|8FAhTEv&8cM2nP(d2dCBvJBr!yho<&@!PW6H_)?k;iDQlB6XGPu z!ObW&X7G79Wooi46VP7NPgYOVHq+ApGXMa&5CtUyXxSqT8)v()q5zoVl)v#c|MqX~ z+p3(-y<-bti2d~xsJ=lhao`m&)7#R5wr!YW-cr~_)*!ES%i{bDHe9E%g^5`z?uuKZXj*Otkz z>Q|3>>!o?Ob<3FvR-KIF0#@RaDkzG=B3D-j+E1la+UIAP>r>|67%oZPV@ZP*(1wCj ztNz|ER5cx|NtiOzU_O=R`aeG%$G?_a5a5#)?0$KF%Az)z05FVmK?p`Rb2SrylreC9Gs|GLgB;w#NqZ2`BdUem^ipqMi zZ#sd3^ou-v2|ZWKq#9jqM#+TN-Rb1RNoSYUwg|o+BY81mN7u$5MmgH|i^Fx;C#hv& zJ~6==xkuaH0d$Nu>{S13H}{G_INi4JFs8j@F#?U<_G&?D1n+^LiLpjwNo&ml>39X9 z0ga49ppWCNdBLgpX9V%-hE8AB&rl&o&<3S4e?ngUWmrtD)(9vp!;QxFKb__qY+0-q zbK1TId3Vng;`x&pBMfle;UT4{wCjFo>18msFR-fnJQ30OamEXVso#irN~=(cipB+5 zm>Sj&mJet3*T>|=ZS@B^qvN|JEnv9vG{PI_^yW!h;}N}z<)Du~i!`Z56GAMDSm)DG zuP)3uhzzGItL@1fDWC`hEIY~rbePVskk2K01?zTx!pkMF?h=L3tijywW$6=n5fO5> z4%-W8HUV!%(m_IHI}x2WoSY)hNz>$$H8lnpshZDKhd?CFNKtG(#ZB{f0=2%_rw+H; zy0y_;k)e?L@C-ZR>J{dl&rNk)9dTcsBae8~^qyhw;z(3yOJzg5dm%D`Mz0r285;ml zatVbP-D^cw$&|p}P`$7r<+Cr2mDa5j z<$armi;^}J(^tqu6EX{qm%C6bh%I9dMx_T+N&g!aZ}`%8W*+JG+58><>7W~qqA(su z@{4IuhRo3j@voq*B-%9iCZaA{Ohum#(pv~o0V*C*xfmH!w2v=yHJOO^a!HSz!U+ks z1qOAT_6bxgGL3Y1?kW~?zXCWtmqOeSU&X1mU}HZJHM&x2SaE~cH=wjh+fU6MS$QCds}Q$q(~ zu$7KS$61OPy=ac9+TOiFA{Anm;{-*PUSy? zzOq@PvbOB<$@jkz1tKq}v12pc&zUl~M=gOG-CP+>PBv^FIAu?BrVj4gt94mU_MGVi zT$Hh^>n)4h#&4bk6z9ed#;)tBDrIUQsB2k}GWctO_&4f=rd8`X(ay@Ip2-{OP4y2|1zlCv6Hlx|y z?nGLC4ssEEd2ncxKe|y%jy!W?B{22te{zO7BYtoZ3{-=s*` zmZL086BY*>y%n1ebTYLMzlny@vUhmhu27t8xPQZ#BLCv{#Hagj^vLsI+3HK67ygnk ztCBnSI31amTp%CoE3xoFsP-`JW2NY@Mn2`9_sHwNDgR;U%86872WfXSCv==?)3VD? z^n)DtfqE@{O9CH0MUL|r#e*n|{K#B=fY#g}u7cQ;(@4Y~jHi2SEJS&Si{tn*xNuEI zire`Nv|K+*RGbHU>7fp`1?z)qCZ80Os{;{xE^;NBv-c8t1asKR(I=D&$P`lfa7R$% z{c(W>3}!NPPkQH@UzA{vrWq2kfCOMkm%=pI7ia!cRC#>QvD^j%2OA#8m`R@7n%N7E zN%RZ^C~eEu@|F_6J6?>i$0!1%>jH%OSg|r5Eo9?)DZ)ws7d}|HKA%{m;%UTJahOv_ zwnKmnrU?#H2@U(@*Vd}>SYy@}Y#c&)(c{dooSGbI z*ayenM<2agV;llxx7Is#Yi=U3k4Gb-Ph|g*4T~*n>rix7NRPXjFf{JAqWTN@le;G^ zkyCh(8mP)+L+~Qo*|iIWMlV9?6y7R)Aq)(h%OSf-Ff$IEfy;@!$4D#yf=t(#a92A> zzL9^HVq)~%gdR3nuU3gZCJDt}-7M*^&PMqoQZvaXP~0WK`HMlKfkZ-?f?Z5t58HwQ_0RR((9HRcp&q;UjCh!i>mnf$vrBSa5DERH|zTRgFt* z8ym?ow4>60@`D@rr?vsf{oI@kc5h1DjuWYQSSrwb4hAh_hh-r=UrBs-zf8@`&3&7p zUYl?FK6%pKC>W|R_xd(`k^4Td`5y4G+Y<#ZblI-oBcPE}M!R5^-wy^e{r)Wn9B-u4 z0j|+xsg*|}BCx62m~mg!W)eoM?L$s=#9;xR{u(-v{oPk5c)<9U zQ$Ce(!HyfjWldC$Bl~W=*~^~JOi&)8R6DnI>ekpSoU|valx2X?^=g(x8>7>8ySey_ zrDB%`8Pp$YJxIejOfzFa+6{?Yp!TOd}9;)S}GjsK7ShS&7y z(55@SfH{uy@`8dS_zSy`nsk}8+jP%^LBo8Lx`76NB`193;~!n4^!7eN!C-l}ypRPH zLakD-e+Jtc!@^tm!=5$uJ(A1&j0Cadr!ahJuT3Pc(Trv&_t&lC+{ zv;6bH?rH_)Fe1r{DdxWz>FIKWh#ZY57%xKN!mP(%*$fHh;{ee@|x7$d>WDH_Zyi{dgIo1 zzj-pzx)_J!Bu7Ob?kAtCe+C&3(?lIP{qTqo`8=FI@71^V%Ba zR|IB!&aK~Hnc(4z>Q|vLekssK1tlkKYW)=m#Q$laWjdhpDI8=yH=s_eU=%)WRL-fm z0*7*ojS@~0&lmnKhqr%#9ue-y7q4mMfLX9A_OgS^B~7Mr-#08DNow5|_UY3&lMHUB{ zubYS~r8=2fyNK!eTVi{xjq;h=q#gcLK`})k-`O?Aj9^Y`@jQu1B6_uyNLz(w!z{j)oQ5?8!hZ3ySY-?SwR@m=B|UwpmMjL<>?*<3{+fC( zfihBaptFQa?ULA_cyWqpL4RWv5GS#@y}3+%0mY7o3^^R>*}|ND6>r5GO_hyi)1QGh!5qe<_xp)a#z>E2NFx+;AL?Y1cF@N`xZ* zEK5kTT<8Kd#6Ahe@GCO2G!O5oltg*aLl(@rBEB$Rz81ewFC@kOVq3kqD1}n5os7s- zv!~lfi026+89nYg_}^qXbUzK@Fy5$@ogEE~pR^}obdz8(#@{<5hg!~xn5|v7@o%T9 zHM!PaJ>8gT2?o4(R-Xu@b30qbUn6jXxL+QRkC#h@?%ZmCd1qJHGIUy{EzVL# zD4gc0e5!%cY4^W-zwK@8qv}!VM7&3Ily=dz$<_D>|F(_4Df~mRxn0bB9?1X^Kbzir z9!`D{F!@97kN3A>OmYy_11EH~R!T=Ke6&viFZxt++#PKno&a^+wQ4*lcfaPd$*&<% zp4^LaQ_d^&-W6ZzQ7O9Fn$uo_@+oqaA8ZCx`Q;gsVrckCzg$RT;zfgEIAZBxx(od% z*lQysr^JKFg1kbN964cx7Q;BphucU716VL$ZAMJDT*{WW*xOz&OV=_HhWMhDc_%~L zi_S2_tbSYK&}g`J+$-Io#C-7LLfJ+xFzUUJvTC{U1J|eRgegCheLvf|_8th;N~hF; zi$eb)kVU-~OJQW_^hDm#j+^5kEY z2>l~Ul^m@f0mT1e)jE{5Sh82QM*Lc$c++XV?3IBrzH;jE`WVBiUY?Z`mWXrNt1#FN z$Px}VDwp=3i%Cs(xD_K(id6Q0)hO6m=QWu?oTnUQG_zP!LUbaZv|#8x=(l^qb|7Z; z;WGU}(!HB7$L8=mN({Fu*||$Udm#3n&|A{5aAc(+0p*x`Lv;8GZeUc(luj3{?PqN_ zzvR5o7U*8d`2f3|bXQ(|%2Kvw#7LlJ2bYAzh3uM70sKrO5KIMPNL)qaTnn60Er2(v zoe@r`{AXxpVa{!gIUMnXXs4C_C68o{`%@~2faHokIKM9Teoe;6eHnj@W7D6*qOn2b z&B2|tMi!)1hBaikdd|?Wm}(c{k+0?^lD9rh9KHQQU`?y%-Jf!q59h~g$?m`?(7XcqHXr%wyN4IM}o@-?37Gc-T zJ*>Dn!?9lz29%V>{CgtIXiJzZq{JcV<%8S+>H@1w$jcbHY=1k)IZ-;t*$_F;*&-B+ z0k42uGh)5v!c*uopMZSl$bV}8s?=#O&V)s-k;Z}?#htuc9oJ+0a4yLtlJUs(>MA}c zLfA@gS5ucS8jOW+E0)#%guD#nM9q;QWI-GbryPfGUZyyValahwLS8%2@M!btpOVRW zCte+gPoJ+CtTzekVQxGN<7rdqm6#GhFum_W^6McP-;GF}CgrSCy@>Vv2g)hzO#Rs} z5HMZj!>qMmAgG_;3BrwCTHYtfwj2KeN4ftupc(3iD2ji0{BX70FIQe5Eu0$iE;N0_ zr&qCH%TY?}pby^&@2qg)?d$zgD3MTk$<^Aw+hJSC;rC9Db}|v4_OU<-8G*&*1NSPZ zlR;+O;{@=?(^tu2Tc&sq37!g6CS9L-M~Z$*a427|Fp5sIF7}uI+|Rv`{3@#( zH}w#b$3gutS9GQB8Y2FXf!?MAFRc9(RIG5uZR0JlK}d(+l7sVU0{QyT;&^7dx9Tcv2+>n!hy1cIR_mCvPx8&a zSM74`WRq!ZF(pqS`{4V!Ey#bHPw9V`oo7*Ue0NGGZ3MrC+Q5lsx#0!T)Y_3cD~T@C z!oVaWrS$rp1jdKxZ)N4@>v+ZCDM$3>D^rj6HA$!uf0+3cY6zGJzu~9k3y4ZQQ`OBk zQ(T?;PV3hqJqTHzFc_ablGTTj4d!-6{jt7+a|-4Nt;{o$L2}qI6+y?sAr%q+qOh)m zzS3)lO1B%qS?-RVbQFCe$*3yThTg*`Eu( zUyTw+432ZMAoawF<_&utxTc6tB$vemQr!B8DaC&ow8U z^6X1inV!eXkjouS-FLWeVoLm4ije-Vn1FJ_cMcT z|F>sIh8Xg)`3XB!-6^y)eY z@lU?r&MAubSI&)>I@1r1)t9O3gs#~!6dvE)kVByk{Jw-vC2GbL|5|jvqig98?OEH^ zM(h>xt-a<snQlas3Yy=legztGvFqobU`( z{WfBeqfR!Y_j8sCzCK%)(gD8~vP(?4EvM5F9DQZ=V2`Z&poFeo$*pfE8Qx`N+Tlg< zBj$9!sOD?$v-rqiKOE=tr3T2?%7T6+ix-nGH=^WTa)pt>3&wMAbZZ@O_VbEx-WWtU z%k;S8-3c(}mOrI@r&`0BAWom+5vl$~)d-~`979$ic8l8gP0f}z@c9q>mu#{YEvuEp z?5Vr}MuW9JGO4_~tf#9>-d=M&ivHq4%gYL^bwE8S3JM!Cu?RV>r-Ge}&?Qng2T{Pz zFGP{dkS&Q&!g_wXS3-0%VmwpohD0G4)(ENa?r*VZS|96EDFxN*iEIuSh!!rUzam~f z*Y#c0yIM9elF@&K3nh2pLrjsvK#5ZQp>huGOy>VDcD_T!-&C4aNelw5rUI4tCTsbx zFI`dptzYnU<1UYsw|c# zfbIM`SX{_6Q9dX!;VYR<&5wD%QmpE0o;G8_$s+=wLe>mkO51^d-i( z+Nn$lpInW~vjc=VBU!c14m~n65n?l>0tc`0#6-uHJxe)Y8uJkM`}%eG@a8#x>HJ|% zKZS4O-5H^VUzNm+{lrRk68`gDUi`Kk@&}&nPLIv73(}*cq5y$_`6~MYeM40bC%k@3 zwJ{fNdz!ejx8g;rY;@%hfEN&>ZeS@~_WJZUF|@w8teYPSF48OKhRxi)<9jH{U4J`O z5N(FFor6YVPyN}N1Xkafxf-ICe6lu2S)GW(bPG%DVu<9^Tc9x@9QtDQh5924gVs+# zGJtjwb3N0#x)}uySQKb={ z#P85SYu%}D5Mns@l7vF(jluh+&KQDz^%UB{68QPr8GDAk0mo>+fKeb35u3TF0 z>^iCzdUn#%cm6(DA>yW(OcGB_vDUvg<{Hn=+p#~r6VnIEMH);CNN-5nnzsC$Rm)(C zRghTJHcNEVl5nj8qU#EnKQUL(rbW;+pP9%JF^&5Q=7K=3C?lJ6hp9X~xrz_YH4e8= z6OfhY1Y*Bw(}m1+-g>F+DmHqyaa~k_etgDR9abCgYE62OeW(<_bSb?}MWRiuiFK;< zh%*=DrUfyEs_R3h4lYs1Y6b~zj6t}zsR8c?0q_5#`)@};E?wO|H;eI;Y75?%LvgX{ zvu#HaBD;s(e=J47i_Z`!?&G5+-#QLfO!W!h6vVkLxUtbj-wX;G9B8i{TGtL83Csob zk_%z48)@qGepDy?<;U_?4ysU+Fl5K`!^5aclq?s^(DjKhJypClY2Jw zk!;%*uAkSTe-(d=FU!a^ROgJwy)0p~WRDQR75GG%$LyFD=ZPiScHss~Wm;iohumWx z3X@>brW6Wg!DFfuI-Cf)NaRT$h*l^H94P&S!14nII)?+|8K7-^QL4!N;xHl<&xwW6 zfl99f0!UTGY2!J;{;fZXKu3fZSDYH68p$-9-MzeH#{=hl^6o#NZ-~w?T6*wD?EVktHMQ$sOYB`A^k*^ z%RgKrKnt`dPYH^#ahzt&EmF7r7>4DSl{!QXKmo%G?lW1zcUq=~M1~1(F+G%xl_p&0 zRwLs_+Q#t1plkN=GFU{^{Mnwzj2d$1@On(oLnV%uH_mayR!ee+6kY`9FykkVYnR)T z%h4Ii;Wc^aq7L5C_mSzB`fupf{xi}Qm+)*2EM_C=M)p}JpTDiY+-oNV5;ujg?hJuI zJ{I8ntywdfu4&G5jSEL5Ogm~L_MW~*DcEGaEN-p z3=osErhjsyvQvd({IMZ4_!TPWMV%*aJ_ScE@KX5GB`pq_(gqF!2kYVal5swU+VC19 zUR_f(({EMKM*EG^LVQfyYQ}xjv+MwnV3gG(q*dqu5tXEfOm6GFqnfv(aV7YXYcKMH zqrHJVBtAa4D8|LNmij$U9|Yq6n_#85ML@_nUR8xva;ArunT+JJf-x4RGdBg!-t-Qf zW_orQ9N?+CYR>&l_jOB8((gja9?_J-7<>-}RS|7%W!zH1dL!39#>Vx;=(g%Okk>bed0##{2WL(=lc8#xm8W&_PpJ%^*o;$ z_(OIhh;jKwz@SEh>TbOLc<~$XY_bCXn-7eCG_-=aau9S0q=K+EqhzMnnuuz z8#A9de(}YKW0i0kjuXKlYmQx3_*lZndcrXsZp0gw8yOB~X^z|c+tu(h$$RIRPiK*l zC5WpBhkL&*)v8>V;aD)b8o$zfKUX!U7%94?$tC$iLV`SWokbgCAT6K@FSlQ#WyRV5 zve=^m{kA#NG)E;Ebt>*(vdlqyo{E#E>ATgFn3QhwVp|d&lDPvg1c{VR2D(1 zP5)qa)xQ0%g%~v95@AX1IfDjm825 z@Mo4@m~ z@hJ*eLZAio3@V{4_*5SGn%cM~+8rO;VvVL&$`sB9O6linO{1AP6XW*?W9fO?8l1)c z{-VYz>4_!Qd5hgu;?3BZZKe%?$Oe&c>WKencHO6RUAJDloOKlMn~JkAdDL&=QjPQg zWnQ8~SP6jE3n6!&(gFkloi^5v+${RnQ@y(WtgWuPo^e6_)b55tTRO!uF9Ja5(#u^NX~s2T57Kr6zDiJEf$_^<%pMwXq8V5_U4szAtm)g&#FRerF;gRNoY?=QtUwb8-x=6!&v0*Nt(Vq`Lt?4u-81U zm!4kEdZXr<(zFP1z_9%Iai=Dmz0dAc*JW=J&PxT5jXo!oeW@w>IUCJj&-iBdD~}|T zCnb_PQ>{gy;kcOwP=l1xC9_D61CwT^b?m0Ml$HjGfBx4m#F92v45m%9bBfC*uy`S^ zVW_aK8W%_@u>1Y2^v?JuzSiqRiP;-3T<)MkagF7uTv(-90_6aJpnto99sb zdwD(Wl72kyr42*G-~E=Jw=;;R$BwGTet5sU@y)_1LiYoKrjUr{@KNkx7_W4fr)Otn<{JI3CkT2^Vb9cab3pyV=KNw(zii2VwYzsfGsrGv*S%G=C=3(P zEi4b0*)CgLBRK$1l1ZaxIAPj5KlX1B60jH5AOY$---(D6O%CNv5N4Q}IC)NCYU{-W zj8-^-W4ZE_y?CWr7DKRG*s2HM)h4|ZWiWA@jFK2{$st?<4t-SG$RFNDBp9qWI2r#F zJ3S?KsvuB=kMldVQt%SYcS=B~(EJzY$dKGh=6RhU6bdoDksr_TnW*U>!51AP86{WYw7<2dgW+??KX}O=D}RVu6S`IVMezHm-w#%1Y>#G=@jFW# ziM|<1jG!GC-7*Mw^(v3!w}VCHkev707?wch`_i=3jQ4r6Td zWKsCvxZlC2u#0sno1;FOd5iWtiX~^;qh#&nmr2cwB5_XE;ho$`Z)}0$Yt%_2lBjKc zJ5rwTX(kw5Ur4X)U8b#vn4q{=3*Tau_Xmsj7mvfl+e*wwY(xY@f@&qt2(f-Chyj%D z(XWCtE^XBPAu7h1U=5}t-c}+Lc5qIdE|gzme99hH2HlRuG&T`CCNcr1Fx~J*3X#u8 zt`E6LqklrOtm;s*7X&UhFrQ3a#hJc03SP4M#DiG+7ll&C@rDe3lC;9T;v(c2OO(iKpcE^zkQ-JJS?)^@(zC~vUlHlhi)Nt> zB^pzme#@O>dO12D`fSARm3xW`ynEmEMthygeCdbUa@wgY2EO0eU)|I*+r}s)4qR*i z(O4O=K{I2FLzc|Hwb)l68t-MvTiE*ZkrJw{)sozwn&6UKJW!unCLdAX*F!>0s~6i} zb~CJzGd#<`QqM#R`ne8`UvxFa%g@KIqckNX7TEp3gltz;VOHJ`_iqoIZ}*U>C8ma; z;=u#j=Am}lU4Z~dB}ZXyFm2B(2eE>^4nxEGU2NQp;%1-#Sg*do1PyXs+l;WTVyqlJ zDX65u(6O%Kw0ldDa21pX;pVhr$r71ZAo00q&rgP_?XzPh!k5F_kkf}HN6E|d>yD~< znwvK(z> zuhh)SQ#*X% z)DWyEK@s%cq-x>QwgN!55qHa`@<@Z{Y(`||!hpyU0GVC7acaZxY+OM8r@RV568`-N zl3|{pY1{VnlI~AbTi2UUvgbD!o?0?Qcw4mdVb^wtAriF|7wD<}`#jRyPS6|VxRNp< zjCjGXxJr{{(Tm*pN&9*=y=a2ku+EiP%Rue;`O1d-XHYqUaPS z_zh+8PEvmK1?M}KVd>3AkrC3aPK!*_)fbY>dzjims|DR@iLsjUiD&{F{N7dV`5FfJ zOsZkRYJYU{(*0ACLP1=_wibBK%tk%iBQhsdw&d?u%z+p43A5E}jbGkUZbe!?K-M1Yv|a9BAoF~wLcle>-fFD26!@WTLO8?GeT6N8=EX@Q(^Wp9{OVXLCD zb;`xGO~v)VX8Uq`AIMgBqB1%jW~rk(w1ys%(F)!|hT%B5FJVn%uWQ5(`$zNJ;d`UD zTP6_6zDYg%AgXMcH8sN`CmWl8U~R~F0pqrrH8NR7HX{CwQ#lGgt zk^nIJJxe?K%xpqs^;2v+e3URj{}1l~@dHr`Fu`9HDKVaV0Ynty2v38yBcncIsr)P; z(}*FbD#$?la8J3hSBEZa9>@Q9@P)rgzp&-{{P%b#*_wzLPn|X93yW)OUGb@s`R+1k%ee-yHh8q8F`%qEwE;OoM zF8@anBm2~i_&Bhs7)(!BzFLC#*qV?t3TgV7r+@ncQ!R((h==y(OkqxmZDiRd;C6xx z8MUbE*I@xoyFR0F)zqVFvGsQFS~g9pJ8BhPx`(W-zgsLm|1<2S?gkB4u0^l*H4?2H z>rpj-nlJOZh;K|)i5vr~Na&}Ys4TH7#*9R*sqj+P%fBEbs(~x2o0yx)TfrGW++-Ul zDvGn6=D0`K@Wr$v8Qf6Mh5PU8lI!h8qI78g8J7B4!nYR+(u=^Xcc2DLh7Ae-RE1j4 zm8tUSB>mNh23?O60@)0^A?>)E@0V#GkPDXsBxk?-a*@0U8O&_6wi!>%7Wl5d8qe{d zBzIO}nwevhtC)>>o?Qqg|DneQF$=IArP6;jK9ML(YSbc5VaGPQzBd3huoFS!TrAU& z-pjAsNe?tnLGUJ*zbOFu?U5sJ{yqju5(v#?=_N92Hm8eY!{3J11(%(~NKV^rwm7g> z2g4?8$sFbNTr!$AMVJ3UJl5V_khA`ep4Zns|91ZDb6^ILt|)AE(2?%90Xk3ixpkEa z^+hl%2Lr=}lnkGz=u*`mv#~qwA1Cs{%~+&& zl7X%VOUp)Y5p103L@$8^Qb|m4Q?F|8Ij>`mVTI^kHQ+2aPkNO_$okBwh1ybPtxReO zI@!_W$vJ=oe<78akc}LomHj8&znrxcV}YE;7FVm(oI^xeBE+2x9V)8Y&Y<70HDu-5)V0Kq3t?sstcoMeLC)vf$=zGyZ5AfHFjI6{5gZ_y0LgQ=)ut$&rd zN}kfJWw3;am(u90NWm1ls+oVmsOoDwYnEo{NhqsG47f_s+ zz#+A~J-vQ}kN#m0BE(0gJl>mved$40D@o%FJv*;mEKK@gtU_`WI*55pKF=erJrscI zr-c(?pTIpgKd&(kk552xnQ?)f;`PFZsLr|Cfl2y@jed^En{OxdrAs^{{p)lU+8X)-ZUqV#AazTD{2}{_riM?bMHNLk(vK)7s zuL}f}1QJ<&x7PJ6lGuzZ&VwDU-0Pvo!IdAldz#mJnoeW&}-o9d;fY(e=ALYk~%_HliDx=HV;fyL$*U2 zwi#O9z);pQKUDg8h@=)2)FO%FX8)Q66&5MMSC5Qa!XEN;c~auyqs|!65COkwJ|}T+ zkc4ct0nx|?L6-^w0D*xAU=?bdTZ+KQl%qrA-QB9F>uT}YR ztM0#(4qm**CZga|`o(V++i2|PY*3mC5*SrM>#|D^jVOS@gL8N2V$?=AGV0x*w%Lv| zqNUFNW5YeP^zbV_bgERf>n}B(4^Jk$^Q9s*>ElvplLfRaC@h%P=@GAgoYmykP*H#x zQ`Y_qP!h6 z>iKW^GAp1EELtvgfbUn;JV7~L8ZS+3QSrbzK!1aFy%V5p!~#zoc2vw1vC>y+Cmvn= z4jn<{@3?Sn*se>Y-5Dj*T_mEoQuU`!^kh-&ZR(^ugp^p*d2(gl2`om1PYB`w=d3%- z& z<~<0*D+}b$`u$O_S-&PdwdJSEkyr42M61Kc4;1cCA$+0z@@9(KuuBTtqDwgnQD8m$ zlL!YMvH1zx*?PzS9uY03NbvOYl5(<1?JV<&Q>{)aAY55%d{kdrHFDfhOZ*%ek{1_u z->E-$#Gp3XX%?#7P%aSF1(whMMyViBx5Q{@c1@2ii;Kboabs^xEvRou=E>72JBZJp zNdkur-CboqNIV?qLYdLBM?e+~F=BGUVVHQUHENWq(M-JeJ9{&>#?22rb?vh?!At(Z zytScbr`P}Z+exl}X!aXG-{e50zGr_0+Q(m8d$&ye{gnaXYa**UK&@ zh8EX!wW5FRUbR{c-_1&d%}^_>BJPU>YoPRl$2PTiyGceQQ70MR#v;;C2a0=I^yG{7 z${x`P@ARo7ksS(0=V@Z6J6RU%Bc3GwtK(sv(KZ6_Rf!YiQ4#?$9Fx6mmcW&29um(t>k=;CyoZgZR=Xk8rlq%YMIkV<(Y*rkV?i*p&H0fa=$Ivir%8U2z)|+Zyk6>Mtf8xhi zoJs5M;jyb`Y(vPEHM)3MStFz9e(hkG{fJ+bB=DaPt})^3zjQuFxX6j>4}2yMpNtJo z{#WXhvyT>kWcBt4P@(-K6^e0;Mrg4)oY10c`^F$Owf~DrZK8T&B6CW0yDVd_q;Abz zqm88cTX>&Y9&1EFNeNd=y*|KcO7F8qYasKp49V&ZrGYL_j4g$qbMct0E4Lb!GYY-Z z=x2`2>iXca2>)kY^rWZ)Yur!M-mPVa-T@vSa5a~sJRV2rp95~R%etzcHMY4+zkbKM zIaRo(8sj_xe! z?}*gLGPX@(uH_gWF9e%es885Bi1pfyWv5c5ak*bjuj5nT0GwPj4ofX4I7$DBNR$Wa zIZm{r*mH1yV&uQgjChg-Q@0>wkFj=MbOxw_?O7w)o5Uz+?MVskw&=K4R6pN1Nb>r< zcXLlc*AQAty_^QTrfxg1ENM!jq0KoCt;9iV$f_dGTC32E5tT5j>Eg z@$aKi5A@SK;-J4P_hQ+lgpZkpaNc+#GK0p4oIL+@+-Oq!ke1-ddi^ERjN!*L+`LA4 zwtZcZu6uu-jP5N_mzn+EhjH_hY(TD>bwB@TgUPX&TN*s(;f*fVredTNaZw@*j2JWv zk&n1?!R+V3ev4(YHQFvn^_+(P=W8^Iphbc|eizjr!f^$fXAVclmv?4;_-qH1XhF!e z2iyt^Eu4%(5j;>g5B{$xk-7e5!qOrqDN5SvOhmbTb88$Ds(K7SW?sFZ;G{`&8r8WoThfZ?UtNgNu@A=#v7R#Ts4;C+Ye+{9 zf!@C9@OHw1;*X$M08VAhon!{b)fY8wAy~9`HumsVM@I{E z=Ydc-({J9=G!y>aJ#zwo_}JZwLjGMsvtoc0ne%yFi`(9aMLG^9u{e{r5mHi9Dl+{% zr=pqG06M>}8u7S1^}oG^W6iQ&249~8{9dlE*N>OG?`tDlSca0A_n8STs>lVRQe`o! z?cVR$seUFXfX38!h0R$o?MChBhL-Z^y*-!w7^y9(gZ@@Yh|sVYS@ILp0d^rYZ^xic z2FL!gci_)=W+=0pY3>(hhJzbV`cGw_Sb2JX+~@qZbyS;y!;h!kS=B#H=q5#KLM=Wh zP5OSQoY?*{pV8?qSsOB-yONr!P}x|(=6J~UhBehn%>D9f1-MGm_01;>Y65?^*N5T5 z`rBQt7PwYi@uOC0)5!=*X(W5d>LSr0;`vUfFqh2_X842*PAP$(Hs@pbHWA`Ka=c$t zPRktTw|}zlx{-V!taOBCv;bEx6Mqm|`rY;)(dlse;$ZqekK6L*Bpk8@R--eob6Gn| zih!3|(A=b(f4$I;QelZZ`}+9_%#*ZD2Y$E9CUtqH<&>oSwMj1p!x*Dwyt=Jiz^JG$!=yI|wO~QNS`D*b%9BueJS_O%Hjx zbKYHjxBCQocLFz2EEox|%|Fc%3kG|al6H8b#$40V8;7yUPv_>rTFJTo8XWT<`=FIw zpc)>7F@`0%Fu}bUH*G!-*w!k_7I|7UvLrrQ^#hMn zNsma}jA?VkpI7q4((Tw;^hHLjI&fmkp$(iCVc?RPztY3}Q*W)22Q-AEc+)Q2r|W06 zE}_zjxZ9uEVD+}Qt~{A*J)w8Dmir-{wX$LJH!x!Hy#d3fwd3;}-WROdYtcC6<_d&D z4sSIVfbn3KCUFVfZQBDrA@&Vfbs<8QWzWC@h7^Q7z9b2A;Ve_<(vgLtOz5Se2C4?=w_I0oIh-KaMU~c=M&YG+ z=gtzpBv)tUH&KppT;Ie2!>4cM}j61FDv(M-&uYzh;`Ydw*YII1GX>*r+6qtOG6qrprES(d z`ghdB0j#;McPR4`6RZAM`v``5>9OF9Kq7HjgrYhz#2rj1E)J0kqzw#>lx>)D!Qai- z(=ztJtYY0Q4Ql_q(LIt90C9=U%gXg^3ch>7$t_hq>Q0+PK9qo}_B9KsR1l{%bLgQk zQ>eyzt7feRvfW^bQz89}EP6MWjkex1?UbTB_jV!SlG7A6YUxf##hj_OH4YDP(etdu z)wccUGkJq_panANww90}(2}r4YAG?%iSGCQ?nPC>oP;Bd`ip>3vpeT4Hv68YlUUVE zCiRc!N7##dJeb4EAl7k>e7IXQd&a+fel8t*A==RBIo81g#WDxd`8hIqT^uBu|Bs)- z|8a43b!Fv61n!lETft%-%CPTk(&%#%@CNcDhd{`oPM_%e%mIN&+Qt%J)J#QVOw5On zT;g?*&_Hp4QeQbj8ZPFF#`*PSx}{u-TQ%okK_DvH;e?O53QR+w3c65b7ZVG&9d!Pm z!sT))`H)KWoM{wKLn!PfnIsA((h(J5qJ7j#1r7e#t*u2FJn)q&E&r>PDABv~A>9z3 zd`Vy!aZtV}6Y`IAtoM)~8oI-4V;jf7T$33Sq89OoN#^4sF^toLvS*=gCljn=2i0O6`qHt8XO}S@*e(M^-L6Q(KUzerY) z3#Eb#W7|u9bfG!{`c_bfScxLnOGZk!ZQimwr5)|_j1&_}@da?cZW3?jcuAr2`LVN> z18cIiSovD6B%Z8Bdz(4KU?qCA&GNR4wc24PKvW@l9OLO12Ud@AC8|PmmL~n!8D!+4 z5N%<;g_Cc+!qdml89@^#7Mn-(5d?Qy zckGC_OWQuZl!nNRL4U+Wtl7o#Ij zy$hbw2Cbje-ZtA(p($Ej;&{LgSG7GT>O-sera-+Z;p>zdg_UyFEbVr}?KrS-*C z@=S&2w-DYhHw!Ub-#Mu0DoCj&WV60l+{U9;&8F0zd*hDP9ZhXtGY@IGEy$BCQyWx` z%){eV54Yw~Yk&vW!K+H4y=}S7Y=A#*4UTR*~T zO+1%Rd%f0RY8$3b4gP54cXq7Q(vF=iqERX6P+a*rY{cviwm&rSn6$a~yLaD@OXvJ)SK$V}yT zgF#Tlvwa0&tU6dVZVHCJKTIZjLGJNr<<_-&wPnJ^Ukm7-6z=if>+w591CTt|P_4~W zF^4KZEBp=Em>}pQyZ6rwlusufuY<)}L$|kk|v}7#68Z@x0I*?Y}3{}!rh$Av^O}9wSOo?3!AIy4(5r+VZjO0wA6V8ZZ zWUv@fopBtqi*ROd-o_v17|ATGUJ(QXa%)|)hy2P!CJ zYV*&LMQ;h`M*d&Cy~^KT^X(U*^!Pw36SgoZZgdeb4m7wu&}JXMfeI+919I&qwQHLm zQjg%PCbMX3Ynu_-rCao`yUE`Z#f)ue4-A)bg7a7G!YdKDpZ^5BVT$kHlbpEC8(jo#fFjg~9 zDV27eg|NHV`N_hZ+p_XO^ch|RjB!X{uq=3Gsjay^V|4eT6P2>%b`J$2h0X<^VU7OB zD8KiG6+}frTt(vl$rf+phRRB_}qc49X@iTb+N&mOK1;o=PxNYuo-6BTp~?#gKx zy7cw&dED4`8HkEhCsCL0=J*+chKSjrjzgGpD%f&sXOIzqu25=;uW#}7Wl=ywDA27B zSJiFmDd7~_P*rBhm9?K<+a2hor7vO7rtkf)<6^lMv9~)#Cc!}G+c5Wmu#;{3?)qX0 zee&o6p{6$6VaIgx;Tqf%ElbwTuWz(01?nR0;WX!#1PeOB7;5*ifF0` zy3$Op3b8DXz@edhx!MfgH&YN-#f?Rq(O|{TXT8LsZ%8I+v$(556Zp2Pk6HK~Z$7#f! zfyHC|82Ipb;~iZQ%a8Qkt!I2=(9W;BBDIuL^p~4hq?pz-eH`lCfu&PU1$(Bb z_6juodm25+mahC&~N7~|5m+qz-chZ=)=`z94Vtw}c; z09ZYrS7|Q2ef>uF{qqyG{KOy0!JuswgB>}UXx=BZ+3+`8*>foK7mhQPATgRZWQd=> ziH0`NWb68{+PZIJ1E%P#vd7n{FhZn(6%J^EKK~rNpE9eb)C)2D=gg50;$hjOy=gdkIg#5oym`#NnGiRz^OA zd>6W^Nwd1W7zVi$u2sb;3ij;wRq^g749t(8Ori^4j7!9=8_Fh;0T6ZJwA1*b7gNd4 z3%Kj!uEH_S5cAxYokwf@K;+0-(Uv5=HSas^_D_Z{svG#_#`;N)odYp7Z<&5;^i}cw zTQkZw)JNWE#i}nuKS=ekt?8Wi@)G)Y^4-b#Rxjlp#8Rr5k zxajZ8dO4|o-G6y`TJ~7|_l<8h)^QYGOVn>Vu`nWAghRW!?5vjn!L(Hs*Z|{a004|U zZ5QM@u0{T1)^tK@w=XI(P*%hVb!-FkEnl`v6cs3{0v{kQsr!e=7)AYaUm|XPqKl>* z93=cHfazOwBoUnzCO2xUa7XK0saK7h@hYm%sj&z4HeJpP|kxEyAYwJ5Z^w ziP5qs!(YOXq9O@r;yHkt?_Af9Tc&#G5CV_i&4eVQV=kS16|RMf)opKF5Bx(2H2H_Z zIz2-RCFw9ARIV0`h7Z`wUthLojRKzGWN~=9!g5=)h*FAr!S5lBR7(qaM$ix!O*{_mUXPW+gC&Sd5A zw=a+F=C~`WcW6+WydO^2+P!4jbxl>Cl*~_rDkQCY!P*}l4n-y4 z%Dt%x6gTZyr89Tb5m~HJ;gXgduV{=L2Q1%@QzsEFWk&@`eZg+hU9G2mJ?5(3WKD*x z=GH0P&GXvjetKjSa z%%j(kUVgqjo*6CDa0{-QS2>oXM2$Lzu^F(~%^NKH{NmuX`BYE!ezmvhpFqqezf#Yw z!}0ud{kfvUuE2sK7uGEjQf;t~3~zt^%?E*Qnv(WfXjZGNr2bpIL*-}xQpe3s6#FY* zE4eYso1Q=ClCL#6Tn)9!1g9=Snq!Kjy5_d~*9qvKBMiMA8q>mINT01dE0epVv*l1B zO3wIcS=5mj@u=rsiX~}K|J4$drK++d&{2Al^K?CT`t?cIP#@|>X^;w0)#vrrUKIN5 z^zbl0P-eZhtNA^Vw{Uvjnx_uYPI11p-lbg-y^yZ88yDflhV&E%35P^#7zQYqYRiI+ zxe6_@bj)frLc+iaW#$}mP@GS=j9fI+z4)<7K>gXABm4$;*S$xlJOE#} z>w8+65RLYI7$CBPWAP9~bOx~yNAh?eRZA-nP@7R{bZbon2fWOW{udnrZ(}l*Q;HA! zVzE#Ag;rJSg=lc|LoxGZgMbO}#IXVfToRkS*1$mjol2?@ru-;&DkKn8HW34#O>hf@ zKwuUR!QqR^IpHC{mo6U8Wk z60Uj8-~!XjWwN`fdrJwiAR@UAWfqmYgj#Jt04w6%uA&4*-@9F3zyCk{6ur(cNTJp%hKoT>3o&8b9lT#|_Sn7D!O_!GiJQD=%l&)+Pj^u%jE+=&*f}^j z*xA{EDrDg-N} zD3}djb)JSAKoR`TyJ;@6eFZy8ePWlu8A3ZM<;Wt<|%Jl=bL#KycSyO z_j7g8uLPqoHy!M0IKS0>Ihmcxb01?B@+vN8Fk z(+8xC%8|gc3^dz+XQqRes}r%`Ufh1~)~-SM{c214%*#pFM^B1K);yE2E2&4pZwb4H zJxnO%9_zP#i{oD0R?A2CI-9=g6i~0}s~Q?ZXyev9X$1&5JgY&4pbny^t*u@(vK}Xg*Z%_Wf(E0R0Ajm`Eph-vk99gQ~!>Ns%j}h`aHYx*H|;( zTgfT;s)leJfN>P`qaEb$h?)qje@Er%s7I#YXQLB>IIi&yFoAJ9u|fbaaQDk2E)0V! z2gC5WK+CjFdcT$~%9(LGL|=iA&9u zrOXgd)D5(JDRn=Nsv@03<}0qTKp)%YiXvxZzNWIpY9T$BV_kcSwKM<$>vm8^Mv+3F zITm!k29O8=`1v-l2As}FHts*Gd|D(PKM3~AIbRk{SCu=>ux#xZYT%g3 zjfX_+znC5Qir#7UwW~up#2%PdXv8Cx)r*U6V5lI>I){gHRaO>qxwNWLj=FYN-;#d$aqf9W4gsJBEFx;XehUDk zMPN|%ZNhg;&x`Oue872qJH4-mn@huo6+qySMv%^6Gf*xF+SNhQq~twAgw6qu|87Y~ zohzCy5&bhzEYPh1l}gf3DgyXT{KWR;k3~L5Fo?r~mWml~Xg8Jm-8)287{(xXQfl0lfX(hu$!f<(y7fb~oCnP?F9m-3fqyk*tzS>@ ztZPkVlh-kz@a{WfwPA5-x+qy%1h)jex>?`4EXMK0O4~`^cAMwO!?1oW+{E*=^%gs# z`v-%{hsUF8_V$;9@_^?f`bfFIRp)hPF!59 z&sT)ka3mmj_pP~;muPpWzMoZK-KMyEDV+NawF`-fZfbdi+vYsh3s$htIlSxD2Iy$X zbHxaul90D1>)Sjp+U)yyvfAeN*h6SUh8>{-S9fg2X8FC`?^61nb*?*&kwBR8)y-g* zecFJzBSDt4rnbSAYxR)c_xCbmcTi^v-_fXJx8>aRQ_sO*3uKDKnb$MJ%1@u8<((Y6 ztD_S-uRi{KYqi>CD9T{Gf#7}DCKibj>=KC3>hNehMWGxSTjMi4Dd1b~-_pMM*Tm#BzWX1!IcW2~GsKeZ3_ zw_8BqWGehBDn4mm|K5jcvwTzIGa=3uP@8P}s>{uA7t!(QwajscFkf2yOrY27CHrHm!&J+IqYyRReS%u3UUPr--v;GOLRlA?niV+FnrFYc_HBj^Srkvbg$%fIv zZhK!jt<=`i`>MAs_t<`hoQ`;-kv}^H(7CA>a=&na*qlUdUMClR)mFYIkPdw=4qIzn z=EWcbv6C=LeNHRbPV#QE#~#}_Muvd!;U$B z@rfQ;zdOt@x`oXv+_i_+Ay?sWJ83|ke_`p3w~K}AXYY=P178)K9D_}yCEcWo?+yJW zj>JS;L`0;2mp|ocLbopBg54gmV5!@jqm%hVT%$u$P957Ao4U~K9KC0BA-j%Q$@_WF z>zeI}i{ME<$B`pu-nlC`jJ1<@25qC6QR$1dps=X0+H>2&Y$R5CgyOGC-vC4>(UF>L z+D?;fHIQGLF;cAOO}{bhhX|(BG?BpPlrQUWTjto+A~6~i{)6#KyX4!0s?TM)YNet zqQJoI$J>mFDu$Xad<)}5I=OMmm!ikMK#*;#5p)1?U%A|7`2qw9l8vP`w*W(zc*h9J zYj8K*eoYsd^5RgHrB2PFNP^6mdftjEv{{_$7MUUd#47C|<_yi3_HXhf0e~M4ZT%aP zxK!{_LzeH;(R{Vh(+1&xjE57raN^;h7=D30OdqD4IyjD|__T2(WWz#Im_qR58x@CG zs5&tJeO&9R7Nr)_2!@wvvyO&B0WVJn4`*9Dt%xl`xzE7aZp(KX*Ml#EM1V zoX83zh$_*e@AtZp7{qq{H>WRH<7VFIq;QD^BYa6=g%$C_a5?PG9WB-Tn+Sl}3mp>x z;qFT88ZU9~_(D2@!-TO0k(?T~7IPh{58CoSe9t958*WrtizABE*Zp??_k8{%pUu%v zS*iFEf!OM5K*38TEO=dl2E*xK7t!9`OZ-U>rBJCEq;4BVlqj;+QaC+!iHRUPTP=6& zk0@o}T?zmGDDHbrUbE?D-a0wC);-U(wMBf2ODh~~-|#cgdZ5q^oFC{ZcHJv`SF+FI12ImX z-@*O2*7XUOQ6r3XU`3gVVx$3Ca7eBos}?DTo#VonUQ`&S$?$X-eb&h4&UW)tv4YG| zY)5BuSKs|2yjWu=%N$8sOHi;|+pW0k4q zHield`RJ$qhvl660B1r#sKm;*bb!J`-RSR+8(PjE9oN(5g4ig52G)^M$A^V#zSxz* zubcM6>EUw(cY=Dn(Tqa^yHjv(i2(smJM-VDL$it4^qqC{%+1n+Rz_c@9+pMa|0%Um<34ATmkKkXB+B@kpuB^#zuwBGlgj9K1=9M$^I-h_YJsfXc^dx*FQ5 zt+sm~3@xGKIl}iINW-jeiqGRG4C2Jd+RgvyFx<@NG%o=HToC%2US7T>^BWDayepTgy?^QaU~3~_PmW$ z*$`cXjNL~3i7kJ8ZD>jdpkLn|$nua2uF3`@oU?Sxm zy$)Br#GDsinq6OCS6hyLcztqVRXT&ij-U47d91r4@cgmEl-)}&<`ng>fdCU|HusRa zWZs#O5fY&H9R?(qN_~C*#a}RM)Ny5_m~b?LpXTdQOqM9;f{5JiOlzr|U>U_!2?zku?n5Cp@*7ry>tNS(NJ6{3nu`lth|I-5G zCL6T1w~q=?Vt;0m$_7-(j$`2i0A>Jrw1{_D63S7M03Gvze8oa|q#*j`(?!o^`^&Y+ zhljl%|7};{k1w*kv_zF_v1S9PZ+2{F@Uvngofe8?BP&`iqm&&}f`(vVr|mep>w^L6 zpH^1*ZnAZkd<)l5WJR*;%3s0kt+OSq`{OI5l6rS2eS3a@)-eT)rBTc-md@g@m4jTo zX1zVsD1e!s*w|;U1mT6%;)nvSWnQs2_KH`bo3~gS6t5>V>3JwjHQzX?N?o7GU&~r? z!Mr-zqud~I@agNb}Vskt5wTda7GX~O;&wC37 zRC#nys3axJh1Ym)?9R{Q4-=X$x;l?2$2eA))IwNLj2fRngMbYLl%=NFj!HId;wkh^ zF|RDlTKxr2zp)=)@vK4S{%$yav!q~3{IR!!l40(>zy)Q zJEzEk=6$7JC?}mbWXW#9Ha|VK-vIG~vJkq3y;7y_(kWufT77Q3%TQEdx!`?_mz?Q| zz*3&Q2ZUHr!s)uz*1_&8ZbyED*$MhA$p_7VK#G|M$_Antbj9*AOR1R-%0qxeUY$zuTZo#JXA1tVSt*Z%i={)|=X; zr97j}_HDkqLiDAHL6)Zaqjj{u_oZ*0KZ|!-d^HbAq`M4%gmK%(shgJMSudRlq7n2- z{uOLo`Fo%?F=_@;tIp9&t)mq^*6zrR+2I+x1Dn-TP4a7xJ|X+Z`rIu3C`d8Fg#r;d zyYq)!OxSvV+J~=A2VT?c(YFfn6RAk0+^_sduk-ll*v3wq%MzUZ+k%wrG3M>0Su6Wa z9W$o)P*?9^5T2;j`~-ow_2Q%9({Rsn;Hhrr7coiKMDIHETPbYO{6w4&cWWu_Puu^k zP#=1-?l~{1yokzh5;2xX`TK>d0G*;7VwGjrBG7LMKe^-N0zbXuRQv}Freg61l5=6? zhh3@yi2qvCqT|Y%;F>`QB&p+s3dD#gSB3gF*YAt9CO=;A?|B)}+qod*;~UfGCMJ=R zwx%Tyd4KNDmjY-X5)}oft+PgTO3L*{J_h=Q@)SzOu=vCU9yX zDc6us1%s{QgltS7^h2c@I=+Se3>TtjisMWcZyfXdN3YtI;W8pQ#KpCn(RU645a=#* zx(lOvNe?iYLmAECGFfv`r8gp3Z16l)-w)ALkW4l>hTjA3v-f{_f`5pP{;!X#B7P5v zk)iPw6+-}kK5u)@8=)cZBi98BT0LP2OJB zIkWzg@L>%0_Unyl!D*Q&5Qqlcy}{=CncT_i%`(Gh|CF|TI?OLs8bSY@i}8|pRokMI zBXVe*P>AmA$lW4vR#`C7gNoeowXBx=trVsX(ge92Lp~?PM*jP7WYcJZ*U-T>N{6%L zVQJrj(Iu!(W_a|a`xScvO=zjPonf+rz{uyw`Oorz*t9!uC_BB7bTF=l#X8yiW7kq`BX~3F8~~hBc&M zvhG;TH^TWo{yRRzM8@vJe?_h_Z<`hb!K?IhZ4p#%aS(ST$Ve?{3rX!{{3Y;j`|a{s zAw%CO*-@vpu*&Qrl5;(hcl*|H@wUm{zcGwVcWXzljpmg}Xslj+r5a(vh%RV`6X_>~ zXK~N!>l|tHtpm#EB@H17<59;u5k0;)vS%pM;)|F52F(7T%Fb-v#{*sm;e@`@liYSj zD{C|`c)V@sha1ndcY@|yiryuD8~|f5ubX~AH0Uba8X`9TMeneUC>{V7ePVIzQ8MnI zk_1o#aYf@cJ_t`GNyy8EUN4FfSiqd`i~ME-zD5JnW1>m%uaa`3V$yWA(_>$kn`l(p zekc?M1(00*<+}_bbr0f|%(_`*T3g{MtEGvqDn-6ci!)VP_+!^q;Xh!H_ZasR&DO?# z_Ni6yeoSaaB4_6;S>1S=&2c=?pC{&KRB51s#YysTr3}8o!>iwp zBeQ(6S*C<;#Fq%{rK0gRsgUTsO88i3{tKSZXvR^N=ouE+5u8_%o6CxD?&g>>O>y{rkjKrfO@47%gIZX%@F(WJPOqy%9aa9%} z8t{_4*b%QcG26HfaXrYcv6CWX%$7Ah#?KI^Wn8y>E?qA4ye+P2|09KT*Csb0$5RY;X0Li-|* z^l^X|$O2TX(8Q^H4%(ncXEJ9bS;WWlA0b||`&Zsnh+ad~8VR66z%}0(;_`hsFZ{2X zK$tjwy$)#i_83HtWFB>hpg5o!8|W(ar%5Tvx5fqRo$COmiAvu5lGNZt4u!-^f(dnb zbC5o<=Vy#sR;q9ORG06m-88k+muHLwNthsjJIopG9&zq4$m?%z0&o8o%wIg%e1=w` zI1w{*)~7(?$KC)fy7R{3AnU##4mr3mL$CzkX%y?j*T3L=n*1Lz|^0p^< zTl)HPy}bW&a-xhL8v(&pG*8<>g9IUqYJcvj@eE2t3q;V6|I2de+t`{ZdAHom2j6-g zI0=!JeeZTAlZj8jUC)+_{Fn_uk5=RY4bc`be*+0j@>A4qb70cK(h8SAcV%m{hO0(eYUVhd~)Zo)Oadth+0<&b~oI>K74%q4ByVeIjyU&cYPu= z|30kO1r#BMi;(l^uL#uT-m)PZ6i5>l+hoxo=vom|cooO@Zn6e!?EE^4nC)NNhM2Nf zRChXvxM$MbWwUHLd+su~Wv=zuSpYqHsrV)mgN-JtL z*MX0EPNg0xBy6GrdT1OkM`pBjbAwx5y%y~WVBRmrct3_@~C~rB~CZ4^*AWyaOTqxJ_e3G5ERAaAD$HT_g|E7I6orW7^e}&UA3qd z0}vt`2L4gV+0HAgVkPoHW24%K3{LN}T4h+>>R_1R{m$E+%)XFfW9@AfsRZ%t2jKxHjP$f+NF6WENZj{^s>)l#|m23B)@ zka!qYu#j{pII|Uo8vTSMeDyQp=fZUbyYjPlZY<^^)8`)3EWew95;G1C$$XkD*96>htu zWL^zQMB_3$!$d=d+$f4#7pH56v7=n8`#xH^F-@njwVZsiwGMo5cY3c7oBgHZ*VK)k zynsr|rB_jmj&U~;r(4^Y?Q%$W=aa&WsJPB(n#cD=Z+wU-3Rnz9{-+gdM(dF%C)6rviHA!? z5>rVMNluojGmb-~sp)>2-T%7S==otf-Q)E-_LcSX46RJjESIs|LbORA=euy@L7Q6o z!2GVaP+;~yb+(>;Mk-VBDIgfAAVBwft%%~sL{KD&r><`Vc;aFmr9lp+Qxl2V&4GzX zAE$%Q-x#J;%(?-a`6CX1Hc%W#knT@3U-3@;p@@m=Ie7UK*%^ESt(vf#1nTljBO9mD z3uh+GP6#2g(%FIIk;-HHWVEy>1D?)DU(YSe;ZyYUi)Qw=hl~FL;b&KU-5N7I*@6?I z=wSm$l2R($NXvYr2>?Qr+GrhZ(Jup39gKzhLP~^9J}FGQ8IIl7#-Gq^||_R@a2jZ_wMdyk_~4V3p;sqSS`^fF8jKyQDCu4`M89 zPbzUqsOa5soohGmvo$=4Avr@ZsrCq;9#Y zpe?}gAh}KIf}d;I^o|~SHTfsuEa9h)8es$bR7$XekzL;yYVR#oX7Z1h9;)Xv{50Jn z1Nxdjro6cLL;mS4LXLT2bdf5U*yw2|*=XW41| zeA1`yHIjP5%=?pCeMHd>+Q%0@57wW3W#9rJF*J9q3nSFV92aOM&7|KyuKk@2KDN0q z^f3HUI;nY5v1!?&*Z!<`VAu;seXOYK?sYlCo?O^`0nZ$h98%~xATE+6x%k zFYUprO9n>r^MQ~E`n|mdp#UV7lRF+p#Rk7o6o9Q*2P-4udpWhvTm(@NVPcAkFrTlu zaq*#A{>{_FIul@?c(yiW?5Bhfp$EAV^z}`b;C8frt-1A@&3k}w2kNN%YPk#|;3^ac zW&fZqK$zJ~BhK$fiGNqU_)UX#SL&1+iF@k5x|6d@Gs;mL51pHat=Fqu?pTN{%GgHy zk(Lg{yXWK28|Y&L?4HLqV|p7s&|I04*)CId6$bCjE4E?RCi7Z+ZfiM6cCBL2P3|~^ zfB<|Pl*;Pz)`^EhhB{H5+w$#Z@~>7}j?%PRCrpInlIw>EWl0^2tm3?VuQhi<{2(vb zxSx>aKcaBn#n6S7=PU)8m=V+{aDrWpesl>zrC4b^#(@G3~#URhHb((o9V9?SudBXZ7&DW z>@OE7>@S;eEp3zVa@5{;Qn~>s=P1{xi>axZVqzR);Ws%qw*>Ky(7fgl;I?n0;^p?q zm&fJ9)0Jkp8m;yAKb>5}p%6VROowgv#~3x;-PD`K-*e&1sZe{>FH|nD%cY{$HfOkxZ6AB6%kGm<*3BTw)|ORf;1Yt^ziK zKO3C-_VQm0kc$7skI{geSW3UEakv1lZ;0y;A#W`{ABJN;pV7gv9~+-o_FVlwmAqTbocqpTPCF7{3J#f9Rp1?D)`9suE@CZuBfvVW++Mc!qfM zb(Q6v_)yT@o9Moo6)YkW*fF0U1#x#Z4}3YP6)=`bN@0w02Wt(Q&S|crand-}bTUyX zvJLM<^iXPB-n&o_6g)+<6*=9%P-)J)m!ZImtt*L`x*zkRBFo6Wb!FFI>CjUiXxc+( zUbrDrwfDhPt|F(>WnXaXf?2cHxmROBLn&GDz9EHtUY2X^BPgrbJ(#AtQz<`SP0`gN zs40qeZvcbRO zQkA;Lw$>Em=t8YP&HK0aC-Xbs(OIpUy%sLMhRi?I&vZXh*mDAE!|y!ihtU!95gK00 zT*FcU{8x)TF7L_&!DL|DfGNJWE*M|9*P*$x=H5Z0b(BZjZyhoY+*;i_E5Vb%YFh|J z8ecsRA+0jb3}XE+Q%_D|?*UBs#?6|0{tQ!>Iak5>3gA5b!x(wYDzW~s;?WijHBQxa zIEhrrj0DXF-#dBAg1m)E%oF3_K5%$Gm|6VS z=;7(`Dbuf~Z5hKKKrX~RI~NzLhTiC1FIb^9%#yA%Zrl$8qYkOW*v(aJOw5rd@X+f! zoAPkkK<9p>jU zu@Y!ax$A@iFD#r+xiuQ;Y1280fB?8wA^kK#8MV`d?QjtOhZb6?VrBfxBEWqi-7W8UkXK@+h1Q!3-2>7A`|08r^W$$4EI~#T|dtM zDFH*`iKM__Tsyh^1#E=CWM4fE7J|UUC|m>pbwOUOQ+#yMHU?sdrr7|n!43-pXe=!+ z3;LRyOx>GUC8%^QKcpCn{*{AaQq^U+)Dp9CnD2OUl5+o~_#Tc2uiEyD`(5RTQ?+ZI&fQ5cl&0*P`4n#_<$>YN6 zj>C)3<}fPwo!F>IxSl|87a1u)C;4-Ce|S;LakYijoJ~EC<6nYdrc0Z@>+{vGLIIpG zuiyAF0IHIuPg6RQo~%-cB%>mkr=z=ra1~wq<4MoHt9T-uXjz!?kytjeW&za!ZHB<- zOLuw_%A$S?R{cl}D1n8f6j^CT@B`XAh+OlWLTG68(ex}HLhei_^6!?6Sv>%NR#KP* zsD>m7ijR}U@)4MJO-(!i(FrO-CN@@Ok!HFq8@%U3Wd->--RxPizlY5Sz!Ol$+y(!8l6=G^_>`Rq>w%f2BYi#tlno zFoFz4JZmo%Mv-1O5RZcC`S%{}sTU7#ilPnIaGQ$r2R+uuFH?fCc#+viwvEvCYF&@x z$8Jc^?Bw9!?K?f0Wp9?=0B4NwI@{N27^*3{G0VcFjZ-eBfN8J9^MDwRn_d5v5ROX) z7z|(js-NAf!W>JnYc{MZFJ{c9+zKPr1LgyZ?};X#oKD|eJzX)TI-ct=w28fcy@INS zx)o1|T&QEr?2_KeoU9hx>4+KyhbiafZ9QyAByT z7&c@KcXtL1A3hj1hPykweSXP%k|s@>H2tIB`ig;JT%Xk1%w{c{^f4M0 z?njkPEa!~?;3QzU&hKhdW^gJme*P_rH!T=RT}_$e?l(0CJGVUblbNQjc!*L=iZQ_+ zt1l9;MM=Ts=@JYINvcUj?y(Ui{4s3b;Bxn@0fPcjLHH7?X#nSu`s>K>@RqTz5DUYC z{X(>O$T!TPxavsezLuib$pno{gq8JwpOuu-QN~j}R--s@h69YdTnx_!l5+$Ag z@X4iTy+P!i<}VDu!J+mK*t}>;Q7#qqEmr=JQFVr6v98abb!pKcqA2{{o9>v7L!gVE zXx06UOC+oT)8+MNkJ%}}!B~VeBZE>nER`^uLN8oT*HyunHFkor!Z!WJGSix!8*7JV zCl##Ty`)`hsbB~ zB3?}@!0Z&}`z{pP3D|5CYy6Gii4!%sgm3p5KPTWDh$FI!WPapa!N|=e@sZ?Wkc>Kg z9{#p3WFWL&WJ!bl-lXV2n(!jMbjfHQ)vf1e_aLE_wY9uSE6aHe29_ZA;t7akuwZdc zUHStrrK2$8gcDmhAwB;YXF9yoFd%>w4KTc_s(0f#!$S!Gl&79Md!S2)b=)w<->~7o zIMy_?>wc5%rTk{=4pjowBG(3$r>1CV`%stkdtd#B#6^r`=BS=ejrF#%2R;!cnUbQO z{+x?)_X$YGy1)*vv#+nUHXmpMfRQSuvmDxjEh24FME-JN9=wwW_Ar73t#aqG7;-Ig zZ+~8Wq6#|4o6HS#?eIN8WUTo-ZGW-W+V-&=Oy&cZ6J$ziis6D>eq&i=vgrwPCTUX^ zKGc%uvgz=t#72YO9W>*1@FBY18Fd(+gIilD8~rWk|GhM5>ZQP>CQv?&SrH|hl78%0 zPdDFJ(CWxI`$*6D6D258__^6)?2KH!Vio>XZ(q6_L657Zp=0 z^#p*gqXyR}?X}91=l{w$Vi1L(l)mh4CcYf4y&V0Bd&OZS)@{H(hK(KHlaYe9A|po& zjF139!+{#eByxcLB@jMg%rIiZzBxXA4h;123q;7K-2yz^+yX@ZeUJpeEIX7gq(Qnf zj35BP6eqTn7)7G2UTAiB^nHj76CpR2oP-<%kmM$2WQZ94B0eh=$$$WgQ+?PKD}?9( z$OC;!`w;bD3(L9J2%z1u`^))U90F17B!2fZ5itP2m_%q45x;oS$J~<9018T4fIJ-y zNa|6{+pNm;l)zN#6e}e35m;Ru0U3d`*rj)<35C8C_+Sd$j$FqRZ0D>kNd)PfQR0un zay2|g4&UDkTuhJs>ltf85FO9yL%o22p>Mm$q3h)Ft+Rjd2?wG3!EfZ}ke9#oBzze- zuCp=G#M0+-XaacHwrn~HdDp(9{QRD>0ms){#+N~{*;?ogPs!SBhDiBNn;~)8Dz{b2 zdHTVIg>dF;wG`jn^&yaR2>hNpso*F3G$Y^rUc7eLQ7oQmt48+$epuDmH`%F71+Z+Rzu!U^oeyW0s}aiST^ z%)!4KL%5h8WF?Z}smOTxrR2_7V)(V+f8v&Rp;9QJhl20oK6C8ELsZ+F|MakM#8HQm zbTO2&F!|Z(Tfft%*DME6%I&s|?n6s>FZ`U?JlM`n6Kh649dFs7m%@z%sn8l553?8! zTU=hu$^#9Rt<#436`2QS{$C4#_qWH_dhx>tDE+R#_b3bVeAwrc+5Vefn5xw6*aIY4 zX^hs@Jr!G{g3*~LAuH|3H*KzG0>DP&iGE%%3y`1m*T+UoJKT_{`8{0Qz5b7Rmq!>( zJ0fmnAy=}_o7cHtnv945q_k(-wmoFYsEoyDn!OeI^Lgw|lp)$PhisN48G%R#;Rdf_ zB6miF^vqgDMP4HTwI%KaTW_W-zfO&Af6xu;1WtJPy{SjbHQ(C!%aTs#AU)hi$S|;e z(?-8Xb##Xm_nR$b!9!&Z7qjxi<;sW72Aen0VDNLJUvjOxb=|PCXi>LfnMDxEB;)s(rq#c(#$X$e$cEob==W@<@}rZ%VGu@YSo~ zhHhzhd+|cVAt3{wpQbQfzCoNHO{g`$-M(p)K@y@!=6o>ma`0>Bg($e;5TzhmX~w-x z2~Jvo5YsT1qO07ehaCKPr50f#i_6c^sL*dy|2~ZV;L>OwHI8>!l>gT|MF2iq8JyuB zXxT^)6n%Y7LTq2Qr3w8Su~Z$^Z^?niBGUhowtbxw?5TA*=oon_P=xXHYuFM`i$lYUbrs$*&eiGJ zNDEh?CYUBXSLT%pRA|njUcN26hu@TAfPu*i%-oo)J3zoq#@3%2gxliu)n%Nj*xkP$ zqX;#(c+lD3A4f+H8+tH;-LonXYlP3BAf3`T`QPM5ugqmg4a2Ek}SnveagU$I}r*zex1Z2mkytkpDRyySJ_?& zxw$}98*Eq-+SBGDxqKs`*kq%@ZNR`74qPq*u9$z6zG54EHPVKn1RJAl{0{w6=d9GoOO z7p;HVw=SjWm{gxk@H?WnJFm^CX!t@*g`%{VcK9g1Yt;u&vId1F#lrn@` zveCITUjg5krEcC{Uwn!45Sco#QnCu)r*RE_w|-vtbzx2;}$GCYy+D5n+#B3rHuwz+zq?Vu>xM zleK5jWh*720PPo+Z$xqp1ie|ia?RDfkTy!rZ00cuig*%v;+tq`n>cak&*w^5&v@)8 zZ#2-gm~Eq1Rk0h+PQQz{>J!oCl&accWY zl?G-Va5#{eJNRac{vk2&tM_pBdvzRNS1FDIc=~cGOnPwxG3X{JUH2oT4$@<#S0_jw zwV)h2n^6sX95sHvN2IYdJFOXPKv6=mVLhm;!eS~+;3@1+`6iGMtU~KY9Q@#9AQlF= z2)3m$uYemI9ZrU7PQp%G5uxBFL7&<%3rUKnmEMl2rB*$Eyo;Mzq1xU{lsm?P&k!BY$!+qw~RWK)&|CC6|hEv|74IzVc5P^z9rBNEf)@hz*$e zThoSFCeB0mDGQAp(f1qzgOSKELckd(VSFcKm-|8c=Wp}uzy9Q_etH2Af-7SiLhkeW zWOv>-p|SKaZE1{bxgwx(MhL&#{?$SI%n^^WQjW&`X+Iv7sP84>pvR98*mz&994u64 zgd33JMM-!`{AY}b2I*lvyiMVhBH4)VPht!Sj-W++)1*P>8vPi*4nOSj-}cWlOA&^a zoSi+Mot)(8VgvTl)Rd&%p*KzZ-!$g&w6-GJd*n`p?>?COII2{aqI_YB1{b{$Ve!f{^$9 zvylMhpJ)cerlVi&J~yuNT`vM>ayz5HPu0L(ok$9u(-+mcVM(vn!Rc-J&ZM$6xrU9ARDeh{Q_XA7HzJ7{ z8KoK-AiKT-rMT7;UzJUfvK*iq~PBl1BR0giGD9nw|8l5oDdI6I5w@m?%v7Ur_kD8ZU5#ey?GKlbS)I1O9it(Ls{oqPRF(LNx^Z}$ELzx{H5dSB|)#7G8h&G{myIjg(n z-O}4Xa)Pcs>$+YVcv7qWhf#U^xBl{9g?1bq_K{hn7vnm39{1oqx%JU za@eD(+jN+EFRh;OO+Bw3ETDuhMHX{u!Uw-M(!B)*&jZ0R%dBYWS?@;xig)80x85Dk zKdibR|B83rB5tP6US2+rtHga!NwpCgoaju_<8A%OECM9cbgmD-Dp6=80Ol~5VeYyR zGa)UF1aN~ket?V)34(+S_(~fN2}MSY&Z;#Jc1K*+u8uO^7nq6|85_?e%?Nzd#hlD! z?l-!)00Ueptw&usvOj7VIPL`;rY^URiQlDHaR&pd+B76c8AlnCQH8oHKm{gG| z^s^wY;vECnWSlvJ|E8J)A%sF0MPwH?i90$`dQu4?5=fP|C^6yqAZcw?+6rDf{0Q*- z!VLj~HG_cD%cmjLTx*3B&*sfR*0RYL8MkxO6+wQ=J{p;ry zRq= zkr@a@2ENHv!w<%1DyIFWRh)jJ=e})$aBk1?P1tzVm0+;$OXP30lbJK&G4F zp?sWar6cT)Hi59-&S2rhf6|r3pxU<85h_qtYzZ2@v1a=!e5Af{5>7^XUGnZ)yZmKd zaVjajIjulYU?Xr{eqE!SwBWFAW8JTo8q8zm-*}2L;g_}0)ybpZBx8?_P62+3Ym{kARX}>s*pGwUqx>@O;<=*L1*3O05^vtdH7-6Hb%zV zJ6ykS>qxPj?;LKnQAMtqXP=ETc+)_V72Lo2701WWua6z^kEH%sy4v=tjtKPfr=~$0LhW&Z3XjgKU)kMlt8sn9^;^gqQrp0!!l^9@N2@ZQX~XJBSs?+b3u*5766jx%hOvfN1UWaX$Ka`6&Oc zjQ`{mJsl})83abYDr`>9=c9C(a;A$E#O_`er56gF%G4fP%z-dM8ms;V!3#L+nQwQ#7=2v2A8+<2g?Ov@U-1|zT` zq{I{{wkxcfFuMSj;%EeNFV5%UR8)c*s?a0l^cb2B!)9gjj`@~)Yt>z zSZG({HDfH`j>g9Z|55KNGU!43eUGH&gGHY7kBSv)ClfdMIHRXoE53G~vlMUO8e zVAN7W@~g8O40bM&Y*$r%n5e$k+Q+&_b2JqU4QXa>u-U|Hv6u&!>)Dqasl=+?2aOg3 z4BWqf*u=YVv{!|J_%aVTcFCkc-I2{>}cGi?gkbjlj&ou9KUGrvYL> z!vHUPGvjqY_wvzdhtI|J(J~_D8F5Dz&-*x|nHdK{mRG=#K7mLB)-kn!dd(er7lZE= zkke4(@G-QI|2+x>m@*OXudFOTPra?zWVE0iX;m~VMPQzO%bLyC^MwK-P>lNkua?!5 zYR`vqL$BTN z(B+Yi@l8OU#=ffx69&z8gPV-8-MNCHvZMc4pZVO0=8OT`ze5wH9(-cb-$$1UQhW@Z za#&wJ{mu~5u302I*?Uz;{0YaidF~-#p;eRyQj+>gzFY$-DFPgWQ4b}SsjRJGq~6bh zR@1jANu?84y)c55TKHZA7SGG2`s92|%vvf|$&NU*Wo{H{G;V!ffC zvit7niUs=@Gm*f`Z=!7y+3XVE!IMsjUw~~e-kmhSdD_IY;Ru3Lo)~xL`C(_+JV6gmJiy{?+!C5L@TRS zHGtkf4#*gGKcjeY^wspq(F%XxWXDcDll>ci+555QA^iK}F}m*$B5xR<#ZN`U(Ju=R z4e8aYv@rW>!yrk()PbW2l2u`=M~=XaORhg3QZhI1*60ESTvrC_ zeqc!er=#D%o_`o2bQX8bNWKBNnz$5gIIE_5Qd-KSXsd#6#-mo$s!h z>2 zhlJE9S7E~suSmXHzzc13sXHQJH8`O+bTk^b6*&85*9 z<*7p63BKOP0WT8_C3I)Vl-+a^mayGJWA8#n3<(Dy&-vPc zO@UBad4QQ-mQ^fu5o`=K{m`&VE!eAlg(L88H0bVZ^S>nb{wI`9>Yc8WcV6vPYlc^b zBZ;MtBBJlGkX_BFlLdv-XlV!uO(*m))0qKo>J;FA3I^(=(n(q2^fbS)wlN5}xxR8L zC^OD_pqr4ECNnC35KSQD{_xp}&UO6d7kQARHjqGC#BvHMBF77r>TD zXw7yfc)NS3_!jPKYU})i9$TWP!!u@#*T)0Y#`0?q|FpH@}PR>um%< z=KDdLRJSk%E&{WFxb@#%A2=b9q>fJ2yx&$l43Brj=DPO{8*I7L`n5k|( z>Q8#F3p-QbidgvJulhr|TVAil$ePaJEVo3K0iZy$Bd{`E=B|8&^VbGHNPexCty6#EvjF-@40`a(HxlSo;k$b-(n z-Bp~iNFqU{Sm)B|5BC!f=?YhQD0On84fU9v?z-XOxdOjZ#Rhl}izarQ>V8;KNWTfM zTaeOL+$Efcm?tcgI_68=t_KmkD!Kf2r_v!}!rh6x8|9*p#3yl;%|n^l(+eHbrtj47 z8y0OpGMtQ7RP8alSOBOdRdFgTYn3Z!$8#cGJonj6(K8|ENC=o#R!$B4IIMM^ru)Iq zlOd@}L=L=P+)*}_dL_R`F ztJcv$*QBB?7EbnKMuw*4*K!}+a6p^wqO%kdYX9gz289Rzc8uW^bcTaf$(=6MYSi6M z((_h8(KEAi!kTV?y*!k2`( zZO5AVut2GOd9#h`@{uZvtG-fzEL=z{bx@%hQrl6;AObDwt2?*Vqq>}};eO~j*bh>i z93IEuo2_pq9RNHgu-2)d;)s%lvq|*GqgGt?1#L0~dw1OyI*13gUf0OzE554Plg*rq zUjBr_AVPUg7Oaf!9^yr*d6}V6aq~S)4|JK_Xd$%|c6vj{nkT;#yiu%M>0!abyDWr5 z1kDJ1trW7Vio4zy?RfAbJxWo7h#{ev6_s|fMf2D9v=-dz*MLAZ*NhL#LVYv%*`0aR z#Mf4fn>UHvL_3rXp1s>e@RtVXCLD9kIm;N(ya#i~=~(OI_zegHgG?QzrKxaI(8G}5 z%7)eJXS7_Ui1LgL`H)alz&2cvd*=6oNQ0G(x#%YhWWGJ~kCQKdqo@LJCtvnnd;*0mMg(^#XH1*L-E|BXzy%^$M8ld4qzC>FM4C{U z9LsyaWCkDGS=`-jo3lhtrUZ3NO)PUP?Kj2tU#`K>>?@MK|=&XEd zTs&N%Eh#{F)!cmZew|yPhfC)I{tRV1OwKJJ6pk&yfo z>T_XBQuN~nyT%})_%*;;Y+lGObd-2tO3w{OWMqj=4H^c)fYE}*@B>b4)geU1+;bP| z?C79~BE$#L(5Ap)w8ce!2B}FIBTQ`}1*z4I+c}5^jOzqK{N2y#e}5@27U=KeQ`SR` zfWHHQlEK&pB?d7B(9qN*ca#Jm*!GQ{B6B@;V0$NK; zc2?0bOlv zf)t+Zj(Nh6u~cgHs8oD+;q4q_llSs4mH|!aLh!NrZJ9t)DcrR0|PB7ZC0koZyYKKQWwix^qTY&;9NpFE-+ zqs+$oKHBUYv^dy|w#bUNlm%e_G}^PJv%5$354A^9Ft;+7Jh!N;I8VvXTz~jX(lVTC zg4{1}*T?D*OPFqH%<7Lf!29RNn=gw|P+i@)c{(HXG#d=m(Z(Nvpf!fv1Ntwc<$6wR zNp-mkK1#~)2CCN!7g#I)W2)7r$@&Mq2byvAfSw@2e`{`~X-MvNDfz)oY8ba@!c$y7 zhF=3N_SYrO`<%pmYViOKu;9kPyJolw;^i-zQ(zINviN`p#JiNpnB6}5N53b6I>pB0 z{L^~Vqc8v01;JamEf6`fc}aX+Xm8Vt+=ML&ZIZ6dVIPFqJgPMD-7S0gf&uHaV#S2*7 zV_pw@4&n~Yd^s6M)4mWpQPJ1R_z;Z-mkGR>{eqy)kD0W zi1D4$`fe=4&kIQ{!GUT{?2qk8F1oChmTBo4{+KGd)Vl77X%FU;`hrLAd_i2AzAm9j zM6?c_9z{sr5&Hc>+&=~4MslL)6Y<--qKbrW6PnN5|E~qmBYv7ePDV>Cz|${(-7md4 zi&lF5AtTd9osExX8~GfT3II$l z6Q$PyqeG)XePd-Oq^P#aOuZCpeO>?S3y8m5h`*dZd7Z4i9G~q;!&0E#qX>_kZ_pt>0;1@{WFzh ziY7Fv#9&_F!7au0iUx&t5LM+Kam;SHzxXhOF#3JkAVm*%GsTf)Dz3@7;82QA*d~UJ z&v2R#+=&&Jk(k7Rzn;k=T&Tt2$c@^bQMQUqOvSl%$QAnXn9l=mO$T8Xe@Cg}K(whuuxMo(&zdQHG$GeNonfFX-Sy(zs zv_J#qFcZi~rjR6LGkNef2?#FE{XzoS&f&gncU&tDT%ZyQdiH;D4-&zRJv1d-|e(h@~ z9-j!wJidlzmCG?nf8Cj!ocs?(K|g|l%2NneAWPgtY(%Q6p1Lwvyx;a-$$-^|a#pzW zMs0pXS+fSdE-f`ZZuMxAHD7(cw&zf?c-0$X9 z3AtGMes;uA$$QUL%z47&b?30jE8q>GnYgz1kM`~Poh2)(LCg!_S^@f7CB}Yeh*;q6 zsoib7BZ;-YO~iaXZx3N`?Hxwtg}#2_UQ|*#!Mc+%M$yHRxoC~>XNCjeFG^UN9mH@u z6Le!G{@%(pd!nu+wp}KB{N1!UCdK zwr1T+8T2~2zoucpP+0F0E@?rLx|^yeM>!>JVj0!Da9aDf49-Izj(pe-5JcE%N4&HG z3f4+=@AqiY--J*v-8uck4pB2LSjfb6D{7Mm9!4;&*7HtG6;eEu0ZH&TId<~OoiUT}0MJ*O{f zobt{D-&B@1M_Wy}pL3SgzB2~KHcFC-#74xa%r)k5y6Z)-9XsQF?65&$!8;K1zS!Sx z4SyJ5YA+9CbR@#$Tb*bw zqg7<(+K+Y4%YGS}N^aVkKhOKGIo%dKx^I8vJ)h@2)+103*!?a7%89qB+(^UO%~Abx zS5yICsy^!6sOBYJ-3ee|h2;kaa~9CjN{sh56@O&6cP3Zpmfgk2G#v@okOk)(dXb`k zd#gHx{1);LIJ2i&67ybM$HB@{21!zqs|H0DE_3i{S_HkbtxXb^pds;o`WKnvEYa}h z*|33EyP-;L5TTBu4eWFwxUN<}9kb(oF`ZJQHq^4JFNrJg@v%SXHZjOe=wr~G_~TaI z)2Hs;b`wJOsu%H)gDz$0AGITEXObORZSBA>TcG5NmS%^5(=5++ww#`r$ zR};~Ihgz_NMh3Vx6k7Pl1Hh9oGejjup<0Sq88cwiEBkh8uCd0S@28hH4=W@@QCiKT zV(L%$+9;=k5XV$R#wNmJBqsicDpA&whQMUMU6dNxwX&jhads=!^$R8lW0;5Nr=SHa zee>*g29N4KXx7-!I8ll$a?`Q-;5`GRM`HxU2n%M;0rp=?606KO3w6Y^I4$EL=_03-2g&|O>lLKCGGkd#suHzop;#Daw+#e1t5~z zXT^AR_;&(7R{r(hD~&$wyX-K(x_(%xI;g_aL4?^Wkc|j@Lh7}V641d50{MRGib;NIWv3DYz(8fad&+!N9uP-I@u~_ZkU%*e%0PR7}Z>2lK+q+GD zjo%ttcxO)nNbZs`A)p9^mAzI8&Ph`$rqp!x>r!8T34M^P_3&Y?n;@@e{Tk z@$P*7?9wIS`XO|YT-jM;>*t$Q+wPmhpc?`|$%kvV^3|?##v92R>OH5_izI6tYV*YE zK;8!{u*vk7SF;(V@O$=vr+TNC_ModZagnu`r?rL` z=;>l*hy%f#4(@mV-ZQNAo!X9$rlAheGJ?B*u2rJW!4Qs=SDoI$Zhyw~x#7Z@NgfRz zVETY8XK=9RKGHtHjf`)~9u(VVCGeKzH8-DgNiUyxFFagtUzBm$|1#(i3 zTZR{cFV2vAqatTW`PWAP#$%`9Lhwxz0ttt~M%^0`jqYi=Y875oTjITT!~NYM)svu` zpvU6{1UKyEw)zFZ3@Z&ft9J_QT6^Qo`r*yHkdo+FxjqAhUm}0na_gd31@|ow!^Q}9 z?f2iTEw(3j^lfz_bx+E_jDBP1(T-#1Trf8U0%*MGk>A<0aS(VK>V%)1oCMup?mbY6 z{byb$T!;tSG!2XF9FL!J9$bA0WsR1Tk2}t7q~ED{RAzb?<|u>&g|u~b9k;C<@89q3 zAzB!35dGiMO;gp>_Y+LucT%F5N@+sK;jfuu4@0*6X84LheaNgt2n3K&%VMeOYY_llMYd< z7@^rx%Nc{H4@&H%tV*x?@+ebC%C65C?VoL~*JgfCBI^f>ey%BT9!-V%b8sqt)+l=1 zhGsvZlRh3~4`778Vcd(E!PF4m!vr-QUWhj)>CRXF>h%g$)X>L2tG4-+_>3-?)fV2eJC@897G>e8EVpS&72}YF5Ms23fiki40eE+>(*H=QCjYrob@R-70dZ93B5CtF+%-W(wb;TcjJ z)2{euj+HC7+*suUIOO@x@ByUMPw^p!w{`NI`zB_rVhjJuP*r9=sSLoa`u9|OW-U?2gE<}w>^D927+zrPnM zT}hR%GjL<95wwJXkZ$X3UbpA_RjKf(RiH5&33wYTMo6 zL6UDFGr=moC#6`ag+H4kEpXzhGYi3pi~mhyEEvWg6{|}MP++=HM>eNL!cS+i*lVwv zM+D+Li$7;{Bb1vb&!ZeKe~q6;gPvZ!JaPm+A5EI{X<+6DTXqwcq@MOsoZ!E_=(_6;4F9TiNdzIDHMF;tDHM-SI56I{5(t_2F40+^=)A3wZd$Q^WLCDYp< zU_jdSbTfDOYt!^RDuSmTjn5MyNb}C5Pf%SyDY~s=RM2V!l;YOBW_+~=P37U%aUn`Z zT=Vz$70SM7>}$w|)Ao!p75C3ktZvEk#D3P;e=uCTaRYcYBNT@Hh!gN7K)TX{bZ97J zXXDOB%c}ix z*J$lbU{?rPp5U94Ty$G2GSl855SbGyiL?>AUk;W4fo-!Q;o$sPcqzR2BZGhF5Ldbe zA~-GU`r!#tcaO3W?T_|q1Opk7GJ$ADi&Efvd0ntMxAg``L&{OLDz$9qzSwdXEU(IS zK!Rh+8{dsm7&PutSG)-i6sV?RukgNlN8GfjX?cND1E`FuIqL8r%U0bLoI)r6YCdRM zPjv$P!G_VJoDmS4!&e{gWH3LOtH<_@ayZ!1IWEL&g8{Gpr__3?sf*2y!8}VIwMap2 zUQbS=y>cP+ni^0rsm%9e^v)5{bHK8B6Jn9E&6mA~uJ&X*FdA$zMM~Pmf0UDc5s)j; z`zu?>fZ*`dN|zg0h^l^rpleGc!IvDrJ3QExpIisrTGmIAoI@u87*);rCgZ)@Kc4Yt zk=cVl_mr7UPsJO^km~|v{4k3nB2B#PJ2%BDlE``LuZm0{L)3l(8H}bvUcKki9MYDQ??)6!rc;if%38a*7bCmZ)b@AIS@ckA}>DPT3F)*)5O@8Vf(*t*B* zyGGA1JMHqOjDNt1LP$+TVOT$=O#k2#5pDlL$`w{EU*gvVuSDcu->+5T+8>x~-y;NO zgY-<~brpwU0hZgTeDs2*?kAs@KjF*BaBx?OUW!~zEWF<6!BQP;%F`+AT(6?X9h}La z_;J;~Ja1{&Hy`_r%})xsqROtJc=anCL@-ZrXmQK+FQhWP5pxtGxA04%0ogYDLUq#4 zj+Onc8Jy_$LXzH?N)4x!nvx9*m&%v`@)JLl8-FS;7VCOs`ScIeDwtL`KWKx%|! z=T74HKGA;n5FJZ_)O|3Fpv|t^;63L(-=FEW@d^HiKjwGEzm}c&O*3T~5TT?K61j0w z9KCi7NUR#AHbIL|o5qiSRS|(ucvRxEBWbM)xe5V+LaS@yl(2 z3xz5efXnfbX^_E^%wt;o##tdO=@_mV8mlL`?fuXD7o|H?h)GP~w=Bo@6&&Z!Djwcs zMk+(_@>Gnho!#$fxGdT{VT=G4)Z7!+c#1fsO!s5<1^BEMPY zf74$b__){a)b((MCmwjy{j~h@L=|vx5x#fnI@T&hjf3P?=Bft=LFav1=q^|?>#@0G8h!2<1aokzW5noiBkBeLo;$zo8OuyxR*}XeX044fAqZMcx80m}s8+91xCQGs4w0T;Ko z6<0=4roiCT+7oVx?_cH0xLlh@1(8yy>!`72MiFX|r~cFH)~^9;9-ub>HAzb!G0`<~ zf58OVNDNw}^i;QyKG5eAu?0?jktDdy7i};dO;IXp2q>D52X1`b(tp4x>nj(73JRl0W}qxVaPeRwt)pdXP?YxQMUD`|%9XD^z5M+A^!8&Ft!@3B@te2) zh$*h-pnO_4f2@xJyvd(F?D#c_<~E)g%=hZJO39YYNoZzZKPb52~@~r>Sej5XBvyaw@U)Dr+OQ9*Z`L;4t)zWi%gQ_r7hYznFmLe z^$3Uobw{b_5>^T%Q=R$eYxy?pMk#Sq+k}gMGN__6Rry?l5=ORJh>pIKfFC1b&rx zS*WMK4&Vka~ zNz1+>ed76FZFCZ#{ek9N>j=}78hiT><=Qjgqf{5hr(({5JC~%Xl*7F(g&h6b+#`l3 z`eMZOPmd1#i#mpt4(lN%+*H*~ryay+xSK zhvoPi<0a>oF8$Cys&%;tOSbvRpTw4-E{xr=)9MBAu?KFy4@f`LMf4!-M@b{iMAo5q zBscya-boG;;93(S{bX#cr2Jp%tD0kRR=-cc6WdbA&OGA!A8 z!Bj*m1_oHuErr7GjztwD>}p`8Rca?!{>^oLU^h!41d&ia;{I=u!s0W-CzAoOL?C8pWMJ6=c z*%Tr+i>&HQEZ2G;7IKi+`G&WOPvGl`B;e@yJKF%j;Gj1e&_HJ7 z0*7pYN1^NTHAaVR`Ip1!PfD=@KNFcurKC{oBKR+BDiR(gl+?3!^pKlY4A1~16xz%Z zF#B+$i3}f8Fc=xC+5hZD0<4b7_i{(oho(;T6PV!W!_XEA5B!%Gk^ ze;##)hX@gRY`59L`*1aqO`1P`AOY&4MIab!dQ(bb8v*GDS@O9c+|pAVO9!I75~cT7 zo0hxIH+Afd^lpz^*=bdb-Jfa6)wDWR!E-~yZTtxzH>lL?U`V0!-|Da}eIFsBAqK0e zsQQd(j7zWv{MI)g>vg2@ZrpbKoA4A-7ipBi-z(S}3Yu>(;(V!tTMgPUJJ{Sr;kicL|l|=H#$( z7d!gVnocG*LCx&NF+!2|EvfpU-&S`oQ|D0QbXViQ)ufbY*~KTA_% z?cFO~DjCTo9c66*l$pLeP{VH^nVK2Kr?oO24ff~KW%Jn;aZisQ5EAmVsMzPq=u@fM z-BrkGWZzm1$+fC-BL@&Y^vmc*&(QBhylA}_+%joY31-bfHpW$M-gxN063s}bQ@>~y zn|luRznhbdFmltV_EGK;rwSqC!dtO?Elf%_o0YmEyObiFSjilpOwZI~bN5M*^?D}^ zO8Rn&94n--%YMoIF&-NnmxGl1eVFIvc+dAbLhxng?Xg@BCRbGeK1a!oJ8DJ+^6%*7 zW^Fuu4=4~bIpPtaY^Z)n3yxAIhZ4$_$MU+!PGlP9CeoAXJjv*l(k zjK#j14Dpobe{wEBAEjk6?F2(Yp2+&9Gb=!|7vql@Isex}wRvJy$xx6{&z20M3=~x?7CQ;>w}F93N_}}IiH{5R|$*_GE(6+P?f{~o%y)C0N?6( zfZvl5Kk%+Nq!4LvI#p#OLg>;Bvc;7 zW%j6tf-fGN`1rH4czo^a6pHdV&Ix8zZGytuB1i5=@B>HD2y~jB4^lJ{{NW>AK||mOFe_Lya1x2Ky0S zgGZ^Qq%|nyqz<%B!(_cZKHXlkW_30W;jjQ(mM$KXWn?^4)ZHW?JK<~0_1-Xt!+w>H z8`0cd({fzFCvX$DkKz3NS0|un1{^ZLO|!RUw{H-8isT4`e@sW(?HY}K9*dXntt;jXQuI{LDp7w`)K zsXd06Wsh*wBb@pOdF78A>8xm;ucl^+h@~%+Ax(D(<-}6%?;E(m5Jww_kJ9Zr+7|sp z5xqWp4{JeslzELe(0J1buzP8(K!A}Z;o99K_uvI`&5Qso=+XJAUs+!ihyzQ$ps8r$8VVKiN3-h)vkbbYF`Z_- zH*aP{fv|pDGyN0)#k4z-J-V)a0pP2q9OoSQ85`7U*o76Hf*8>4d{y2Hn9#I{WJa=P!3CI)_{)sOt`j!Wx< ze)DBN*jFs~aEiiwD2;WNj&=pSVTmWAkt_*t64K^PyI$_PMcc7=k?tE@U^8$E<}QLx zkU}Xe&!6DinudOJlukXCVw_49G&WvwUPFOZdxTAQUx>1fW-7eunVJ>VYQnwPaF-1}ZWA7iVFi2Nj@El9d|g-r+l4%RGR z7G8$k=6h~Xy=d9ANB)<0GwzUeS%o#f=jxVXt`kg{-2R-TO?t07t7XtZg$x4~O2vnN z>X^&9t$8)iMA>`uE)2O%RLE!9tf%9tU>RLL22LGn@zH-I_LYprM&?gTxYBRlSU0LE zT4^iECmlJ(lZ8Uomh)W1ZlDC4^)=YBQiQ3g261Ke2c(aKDVEb9FBe zHqvQ^?nOk)($v{o@0W@N-^BjA$FiVArt~krA~u4BSpM88_?Zr4^&((Jg~;2b8QkA| zd_}KI!0;;=$`C1pCPvtV57$)RSQWD@*fz^@V;CLLc_m`(8!TXSlG~<^K{gL`wJPG|2j!otgh1pA*2cP_NqeaHc*oB*>}I^!$*(imdwFwh{UQETwcLFNCj zt!-^<4G>Hy1(^;F%_N-P=&vg@=`VR6t?1!9Cwlsodw=^}OH&+;R~7%_2kkE@us3tn z-AsO3EZrKxFb76$ z^{8GuB18nC5lLw_-X+)+4GlO89$OPJo{&S8KSMj_NIysFu!u4Sh~A{S_?Zzf@JE*Q ziEpp5ZJ#^+F7PX^&|*w%6_PaaZw{!}h3-Be{D45waP$@;r;s{IX#MAvO-k?u^QgPE zK&jtlP$ASkeTjZ{3$BF;N~66q+wy|mUT>{uK36MxbDF5ii0(%2r1E%`Sok1tiS)HB{cmgnx$3j?-JRE+7u&O9<&gu6g|*!4dZ1bR z*YbUjDg~)p?uYAI7!nFrvvv29<)@J$%Fr}+Niiz4u{>wHv9W{RF*;`t)Ao5yy2416 zBYJZyH;Sg_tBlll%9Yz<3F2^YmtH5h*hWTR5S_MoMLOW9zk#=(K~L$Ar6$SpjkH)3 z`}fz5xOL+~3b}W#Pr3z$m>I<(f44WD*WQ|2f!){Er*9jtVG?wr6hcPyQ51P!=EH-> zn)9!ksL0VQ8!fh^z(XP;V+@ubY}m%S3K`6^{C5%JPNMTdE&P5nbt?o%-wD3F_`W;= zEixq<@;eP76aqN0ig6jRKbUi8_0GEpj$AZ#?DdZli?EmjjY*aTJhbvCr8yxMY4?x1 z5-s&^kGRst&5O^3c_A8TOk6`FuvM~MXBy1<`Lo(fl#R`uJZALd;GQ`j`bQB7x*NK) z;ER-M!Rx2S&w6VlHf&jym}@f^@CPiB#ZMv8+fGvN2~x1&A<~)HEj&0VdAnKxq()dU z_>~Gl(AYuVGIH2FY+@l4Btv+Uf0|NEL9{nx`qLH^fuHJ;r0`>T4+^c(5-lsdH$Dns zlS>5ABPFu^+;-#59GEGAs8r-x3NOys^JQg?_JN?0uOPWxrQkn142W*fLEgj!B|2l0 zZ;6$?9SetzM8o4ecXTTg))me~!RCY(6)_TG*v?2Y>cS{UDsIz96W@hmYWReF9kx&Cc!{%vZa7Z1C#09U3&fNXe+k;((hLR4tB8R9ob~ON0)zR zWA<_S=FJkUL5Lco6+swaREkiW{jOi{EoHu z&fPzb8!Ct9vaVK}FtrO2pwU*S3LZas>c_0}bd+Y~@qfpP~_`SGQ``CT=O9L+}x(VKukQvDBh)Cc60 z{!Y5!u4S=(P-qD{9~FJiA_zx3 zx4!q_mPz^yhme}PO`dTXn- z+q`c4bJFXiM@`*qS?Qzu1jpyZI$;Rs2L7IwGxvjVy z+6ol9uUa1Tw;&_ePS1tO>vm8 zf>D^pnLt9xP3>-$hrzNVcz|v|aiJrDp{5`FK+sXD4YV9_xu3gi*>VDoJd7&P%xXN) zyp=1I=UQ)M-#S!699K-AX}y4fp)$0qtHhN_Qm1TsqEqo6v#`$~@h>~?>l~mTG%#O~Out;;ab;cfp308_ab?*dO z0kdpg6Sb=6uC|SOhI=9b8_cv|`ha9r9@p?k1M43g61WE}LJp?@sR@^JyT<1RlXGhA zNz`7rO)r9v204}@{2Sr7u1!ZD`#4SaDRKzbZv-C(UN;)zr8nH5dt4)PyCgOnrg%$0 zEL`S^IZ4RO%gakLbGFa(#o*cc+wIHKUADjrM^y1qIXxt`C~u8`$$zX*XJKu1O;<+; z;G5>QXD%K9m_i`gTr4|OPC_gNRD&(llV!;(Ae()btw$Pcp3`wQLR%j>!-3wb{!upL zI85S@?It$yM(<0{Xp}xQrHWN)L z&Ef()kM^gD~j%^Yw_P$_s4_NCXx!LM}$?fEAYi@1kSzq%8g7mJE zv&9Njg#!~x^7*A?>(b-Dx;~UwZH$;gjFRlPa7H;Uj~BytEyDg}!@)q{lAD5UMqz6u zDje_C#cYvFK*+Ik$#nemNTBEhGmsp-QOB9Xul!I6KQ{Y!mK8lgsj0=OX5O9Oaxc^g zB;yT;^s=(_bajIb{`53iG^M`#^c})jB%Sv9HKB#yiJTG|8Jb4&GjxW7UjSe8)?|+E z5MM%5q!I|FNlp=iG*Q`{NmMrn;(OyZ$Dm}Y{?n$4> z7c0~40JtC!QQhk4cy+U&=lgQA@E>^$KvMudo~ty%jRDG0aAyp-;p=KOs*0}Qx7Crn zr>R?Vg1N=#Z1uiP)%oN7r6ik6Bc$7H&`{}xy$ zR@Hy9twvMzcDVGu&xal9RM zyteCm7G9NW+E-@i+zKfmGCPmt%c9>Xu-2M*Xueex)zPl(uba8)^DTDNw9DBcD6gMkor8={jR9Q`^&r5w=3|u}@8|`8_wu+0iL9m>9 z6hxQpCCZ5VEzL01pMN%Jy1bm+kac5@FHFdnR#(6R3tg9bFU8vSM{+ajTleRA3&KU2 zMZ3Z$(-Mn{UXGtXQY|1~rMN%NjVGjr>{2Vcl^JU(;|q5g>v_c^F>VAz#b?}Ux;1nC zya}hXuI^al^#r>I>?Cdsm}zWvny1(M@6RpzMo$zVd_}-;2Z7_C?-)@7{48HNl>= z!k^Hh;fF&$3>4sKK|nSxCi^cj%lq#21#n-DE;cneZFxRjO9!UxtB~Zi5 zwNnQP;z4oU?HTmV+OQp+yR^4fXc+hG3Fa+G41wsfnXRp!F(>7XnWALQ2iMOw9Eu zvB@IDp;u8&)Eq3M9C%ZYNIArYS#(B9%10bgR8&ZkIL2gd(e|`EI`?ByMN1 z9s#OZZd2Kvj-&pH&yS{iulUZhdm-@Q(xkCHOY3BBz4^tnld}5@@ zY`$38du0;jlBt(RF60XhpIen#x3<&@dQZ+4sQ19%x|0VC)Q|fU);(Knc*Bo((KV3| zaCyZ1=|Ymn(z>)p5#6sv53>n};SF#>Uh2BrkPbY5p%1*gZk~PplcRem5>nNLX4!3a zBb`KjO*1w%q#IMIZ=uf!n~R8ZruOe4uBU;#qO>OM)Fy6LYt*GLshZL-4k&iyBl2d&+ywQ6r7 z(|72VHQB#P*lz08oWu(WAtM#9n>f}BVubZbXmNBh)+87oPG|Smu5=dB>zc+FEYNPc z`L*7cdSt98bGoIG+ah$#r~Q+os4}n(rf7g?D6RCoE*A0Xa?Cu}y19oZwpC9NtF7=Q zoK;1h;;_Ib+tZ~ZPkyocFnan2ILc5G{l)L1Zx5?NSMG(XObmOWWy)1}vNEsYMwK=P zhpc&DIN5qT;oJ$Ze-&Zjd!c=C^gDPg#>~*Dl(ekpD{2a)X|0@jbgRZDgags#g*8(4 zpwY#uQYSf^qr#MMqwaZ?hQ*_6h;V#;!`i))s6jQ@-rD-PCN}GwiI)&V4Ous=rx~ux zFs~(UZfB}91dTKO&dtBU@qe}=`ugG5kQB3s*cV(Rm!AARLe&FdkCBFX3)cZFxvYgh zztecj3Q`LnMe`cJ(pH@z0QUc9eSsq$Lk20*CKHb>Vlyng@fCgXK*0NcqU|!mK#FvJ z9a>j~Wbqpw=zA=mE5D#c1(~1^;6wq0*WB8^-7a)KQ~@fjQcDLl;ox7+>YL9qc*xayY4u2T5sopNGT$3x3e%c^T=ien|tid>lg*8~W)DX44Po}rL$ zRnSqq;5$vp3>6;W@bH!mSgHN~@ENHRy^yAgod{;%E6g`S zutOkRo_%Xe<~OmSR-5stu!Dj~Ab%n2tNZiwnMpDM zU9Ur>*)AM@_9!Ja5uFUA9Bh4E#i&^#?aKRp8R1Og&O4OJ@!W8iz?CCTaSy`0}N8={7Yc`zh81Vb8 zPOf(#J0E;`Ow0vDM8w$bllOJj`%mxpZ?OzOVPh~5EReANA|BGefr_!n2ELMCBOVOI ztwIz@SR#S6l#vXSHEIkuYYk^li_Z2>Q)e!jVg?9Nk`9Ybp!biGu?^Zxuu3x=<^61R z;}$i1e0uwkk#A^^8EkADFs?Egtt%65}SVY>iG81Jyr2A*t|FZzx zN*J#rDKfr?vSXvLQf4V-o?v8(Hjf)1iLK-QrplL*5@1LGWAx|eXFzv9JiG!Lgt@u7 zwY4?t1xPHytq5uHN`nqV^29+~HLK_gw0zF6Mt#xT?Niw9Kz~L)G!#TTTur}`v=40c z*xK3(dflJD+?@f#4j;gc&%?vR#pUsMsdjt&N2gv`D0Cn)6(N_{W>1HL#&Dmn=XU<6 z(8KH+8-2!(s(h-PT#JtxAzhN-3D$iN8DaTmO7nOmC;G2YEm^eBz!Tkr}qS`1Cqr;Khxcp*IBXvfzzwWk4 zJ8f@`Uq!l08Wa?NMW>0B&~3#wN9Xi=Pj~xE@}08`{tePR-@mc*aQ=%bL~X3vgf*v; z#S!WL3;%MTf=+!6hv>F1X`Z`=SdfRoZl@WwI6el<;;Hl@cYj+TV|Eko`1{p4gFpqd z^Z4t^79Ka#rEBAZK(^;v1Et zKrq!t*4ehzPJFugyk?&P>ce&U@VSl}8*2L5CPVLi9OVzW#TGtpG*}_hqLT9#0> z;<=~2REp7$Ta|fyTW)RE-IW|RtPI}=`5dxx9K4mQuf>MH!;Uae49MpgS#4x$;#+9; zW*M|hQHbI8Y*L?NXzL{+yOB;w(?g1$RYAx7cDEiP;Wwi7bA`NUQokEMZ+kTyz~s)A zx#y}K6`}IeG07*?ms8JRnd?&iH3wGUsM=@Aqr_l9S9pf&PkOFN4mNcyI>SKXdaLiR z^jkj1{=QV&8C;n$<0$j%JG|x)QPt)7m6l})>QoI~K#-UM0OHP|st@ouu4+M}@VF#! zDFhfq9Ui%lN=yNofK#2$`3G!|@3q@kPofV)!FG#EC$r_i!=!zs<2)Z-R%VSaP z?ULEs`)wVHZCzcLnrOP4jK>qE!v4P=VG#gmK&%l5!Zw%kOUN@Z zBwB@I$O&fzkNz0X7Gu4vxYS5tW6; z{t6vRX<5AU#$=(fxP}wygJNTenxz$W3mqyMnSlI+KTFQge#U_c_4W*rXz+qGE0Wi| z?@ll`UK<5pUq=b_vfgJQgFi;W<*}n1fh5fhi!84|Qyj99u{_~s4T0U$@%$R*vmbh5 zlzGck^as_YxZpUw{WoJ8Q6a2Y;A59o;BFsHCI~Fagi;wGQCw%h{r(XV+7okDYM$2b z@Ggy}pYX@(i3{MQ!4xa3DkE0VlZ8iL(tB}!eh%1YZ0C#0jF6L-7*R_9 zIg6Y7d+wq45g3InpY;jrB%or-+WWj*pkOQ{;=Y^?lR2Yb!B$|Cn;ptxToH-I50t{` zN{3&j`|2~4j+GbsZE#w3qW_S?fx%M@XoR%CR1yELM+QhlQ&ZDPIeFkf_&7L-7!vXg zaq!!>Z+c!A{lmk<9_<(uW*NT*RxBt{FIJS;Y!`?2it957LCRZMo=|~W4mdGLm^RVA zPuJCw&Jz`A3d`&<2soyyIv!C#4J#)=fd;N@fUyQJsRhiMDyyo1((Brqxyzg5m9PY? z>U972lv%G}Y04f{t1~RSlpaq5|BW;1GqQK{q%)g~#&8CkL#5H)8AU(5Sf zb<2&DFM82NPf5Eyd)@{qS2X+Oyx8+2xF0ktE`G_dW=xOW*8Hv&xUB%`Z%l$Gom=72 zYC(aAU8>Qv_9m*+FU#MBOmQbPonN(TbH{(d>=j&%Ke$+4;u)j3WpznjSf2#@Y{hH5 z{>qNfQl$MeJnmyzOHRi5BecRjMO#Rr+i#x0MYxreLe<@_xCHNmxzuo@vE8GJ)<(wO z82BVB_BKqOqBaYbe{>Elr{4+`R668VT6gM37eE({hh8JSsqR!}Q_i%TUVVwsYc;$y za->%Ko7|?qJaQoa`t7I(E6JIimi%~Mc-&j@Op zbn&`%Q2+GzIsJ0SQ%!>77()M4y}PJbIM&U;GqUJA5jW82xqO*(`lD9(44EZKgY#G0 z#4awHgAd%;Y3Q*n(d#%i(m;xasi#b{anff(;b?!_md^TR`ahGFPka8Xv@a~s) z>sJs+FTkEPpTa^1>QIizv%GS8f5mYue%0|q12Mz~aUKuEp@D=p?m|P=i++=EycyZS zh#5`3g-XEjWX(i3@=r((L%%5EW1j2DgU`Fep%YT&fbq*7PuFUdqo2GW zGdehwt+tjeCfshmC1#e4)f$s5TVo6MeWw(M27b$))!$rJ8hu%&$fC)~A4&&0V$~Dl zzL_#D@8M(qdpD_e8m6*JaA_v?tuG-??)zLmwdm==^r^TQG&jYju`uRJj%CD&)nUko zcH_$L!A*hXn&q7>HBSG1?<1BIXz9g!+)9%F%#R828gJ zH%tPj#?pws$(YpyZ@F(Q&85!zD$HoR4(&KB_d9hix^+Mj5vDk$h(e8Iscl+t4~ww4 zD-dtVT5H$V+6oI@t3-q*D6MdbP2E8hIFvbxX~CgTn1C*uR`fq?2UA7UiYSC2)3f*>s<@WQ8LVl*I0HCV-QAnL-BwTF z0+U7yLs!?=uUApQluT;YW(w+5RKqosIJmjV9kKmLh)l%o-$~|rN6o9*K|+S%e_->6 zxTq!h*&V~tK&;@qslWyu=JaZdmHslFU#jH+0YHV#pV^Os)wQ)} zAfasoaOwwe@D0E%8c2XrtI{=J*1O`RduM#OdT}evO5%p*S+9zQbCbjVMdX3VYo|6K zs+i^$KUYBxrt+0;oI41lG^hf`49DP%N&JBr@ z@}Q`eT>`VJ5l&@#*YQ&lpZxjBgzA@w&l~ou+Z%T(<0Cyd=6>d3R_&Bhh8iso~L zb{hs6Wcb{}g*ozjdK#SH)3mYq zib&#*zsM#keKug}!Zl%aY$UR$iGSwN@BzymF)X)<{+y$;eH&-$#l6yHT$Jy0>wQwB zVDnJ?N~S#i-P0*&VX~gjV`k!rV&28Emv<>Gz`jV3w3L_L9zMj==Gv*wazlNaXQ@o-AJ9k{?kDi^hyZ5I{CuR5 zeC3%nyt5SL8${*r4gTfHRV3ioK^sF$gZY?mr5^_`}%V!+a)1SDrpryIJE1yeAryy8Bmo;$=xt9{Fc$ zKtYszFZ95_G+1802lH0SZ~FYhc*shF2YDY-V;d0&C2 zxem4d_$6JLG&>o6TFW$(Ii~-l$fu3B*Gb=3N2!DCw}-H zid$k%1bQEyXTFl5;}jYH&6Vu~0y*fYa&#Q9{^>y-O|TlHR}k$Ch*#5{`0xJ*eV9<=+Q#w{bLRf z(&Dvuv;+)!_Pg+P!O^j>r;anuk3v%IVU=K;A5iKtg+2tr$$YZ!>9_rs4}RTX@ag00 z-c|I5_uX3m9x75xrf`Y1l9WVXns=gSECoT-If{Y(+qu>leO2bZ^3J#1#1|MhoNc~qdkKN5ME z1|=2@=XT5_Sg1vSn$iOdTbz)0sA-5`Bur(1D}1_R=lu*Uvvj)ML~Q5B_x5g^ek=x5 z51=w<3HrVs|2sZEx2xwhTLFyPksF+}wQH8yg$x zPecKYBDY!w zla_RjtqEr3?IzjW_N}55VhS;A=xMJ9;ExIH>2?2f(lLzBK2zPLvS>4kznDxW+xHUI zR0ZCURbq>SIJv$g5KtG6IB9(7tp08x!S{N?qXOxJr$gnlH24x$j+JeI>$s=GXPp;X zdwyK}R<@t{trf4fvm?^;v{zbK@8K{?@W9IuTdqS2cwMLaDQmOeU{&OPO_x)Isi zotcd^Raf*8!-HhPrHv+IP`A6WUIKru>gZSQ<(Nqd9hDhbfQE`{fGL_i1y9Fo+9~Jv zr^-B^$)C>h-ddbi&IHemwM>}N=PlwXUtVogo9yJ^`MW1Mzr+8zwZ}M)f%c~G?5jiiN7oi4jweRdPd0kR7CWl5)$$Gz+4ULt?ey1+I4lc0 zN4q2WMz0t|DmMw4dBc82p5@rF2gC1mPiW1hDl5y&e*7lfW&IGRzi7_wGJ}T$&1FV{ zHmBTgV*mVWk8JlbrYW6bH>oZ-GKnW(#AklQAZdr?Y4r$R5ZEtmFfV#dFEQ(5vmp>V z@seR--yhp<`|YJa@}}Fm?5(2}%=j3X0*@BNRFOK<*;6_lI@5kGZyC14%a6TiJ(qo+ ze#Pifydsv@{ywh~&K|it_=EKBLyz*lvoz5RgakMTke@_*K3bQjWlWOv{ zL=h&YHr!O;GHnnxI+UC$v}f4Zicr?x;UYFXq)1;_G{&}c>V1byZaUxL9i4z)N0EY} z^;!&KRY{qU-o-FOJRZya4&55Px2NONpy2C;yzk@8^z?M+>k*KgcC&Dl-`k%D1MN67 zjLfcL5&-1Gzr7(55fK8XRD=DE*5&UGxLJ{$YX%vMXzd|lRL-#=)8L(LP`)0?1)EuLMR1X*f`h4+V;uQ-Nwf2jI-l17m|C?C!J$usXx*S&cJqGfTXh2 zVb{3ALuR6l#LRbG8=B@dt84n3I?XbHvDF~6)LCJqGt$%bx9jae`90(U>i}}Fu$1KG zV?|Fduq@5<$67*Hz^GG}(qn6x);`!*w$}m`n)mL#&?#Bt7=weXSVVt#YOBui6C5_7 zDAphJ8%ej;H30#EA_2-Rhh}XC{KMRthGp?ZD=wOC)`5OQdnYF+H#axO8GfK=TwTo& z_yT+wEuwmjUsu~Fd@OA`N4!9&2W&88_G*05>XPQO8h zCW7u4X)b|fEG3_)nUrrSG&9#_1Gdh{!_|%!ymP!ih2`a@kwqsmdFoTyQW# z?2KN(tAU2~qphXo5dd0t`XBoH`-|nXfr(_}3gC9c2)z`VGP*58NFFU5SgTr)#!=@# z?a(lLJTnb@Iw|s8UB%p58cg`((mKBW4<6JJ<##b zSy0EZ$SYgU4m-_;@Kv$g3F~yY={7A!%D^X%&)R)>HBy&uQU%hL@iGk7P z%^01N_59itiIHg|c4l^T2|rw@A(~kKN`wf5cS% zeR*czB=vFVcqOf|J}X9^`Zm!~cgm2UrUd?wJBmo~wc@5_A9Y8FkhxZzA3hsb*3E{% z6;HuEIM;%>h|}7l4MbOHw5lah_)(8yjq}D*u3=Q`DvRUBTlkwLIPjy0qRmPp=p&WlI zHQ&09o32l#tM?VNU`+H}^|=*#g*-<5%XpM;kGt8#;glCFDxzfIHCkr`Fw!bOIX2WE z{B-oHOO}rOH1QPH6}0YguJ|;=hlBjSn=#^$*vY6|Pq-RPsR724`X;isq4>Nk`@;o% zg5_y;*s(bt`h0ilVoFj{_O>70R)(b>8(HdEtx$rOLM#dG{$&k`!x$k6PnIbT`rF7{ zSFo@{Ib?t2ehNm+R4`)ui5Nup@ci80vdw=+!yU{tIe?!*#lr|SB1S0c&3oXlL#8)J z7dB3D?<3M|izm3s9nAU-zkv_JeFiqwb#9+&Cf;;YOtVtO12r0S7QV?EN~9@r`Pmuf zZTfK}(UM;j+@WoL^MZOE>lzn%JP;8kev>101APlmm*N1W^^l8xUM12j{`$<~lj3#o zkO=?Dc%thQ6QjP)Cyt=OP?ddKa$=(ngy;nSG2H{)8#Cjqn%TFxyLMhv`RweM+s4k9 zxvGxoZ0)IAl~9c3kUqMj4!`Kcd14Y-%aPhiy)>Rruu+1oBTS;rb zBl>Np^I86F^zFH`!~03qa<^o9ehtevQ<>tN`RBO(zruQR zm{_m@+(?0(s!rdxSAcW@I_k#8yXfd>AWdN9L{$t3;aF)H91H;j5852R7L01rUs~A&XR-(2DSxu zsLYi)ZbOKz_;A@(2>8>RwSoBkE?;j+{mFs!sT+zzY}sa=lb;rg4)5AX@!kA}Kgv9o zOjXMn{M8ut1dW3#Y^#%xE@&y!LlJ9Q##Ej&qPq!`T-6ufG+ zmDGK`MaovnUmIn%9%80D7p~}^k8x$wBvetp5b&ETo5OEuivX zIIv)Tv?P8IQW+karm(#%K1)DG zp~aO%awW#Ew;@YuFu)|vdBBW{t(=J`=J1+U#TQV#L-6x(6SL+%v*=E3ee*~u?zj-D zPq7u6F5)&{YL3+w!fN|iU*Io?*?D7%ZLR#v)s!Tl`iH)hRFOa?2@cPDpMT!~P~3J4 zO&`l>c2`(W|3*};B6??u4YB#PDV;KUi0~jEWAR=HAqxUCsO;yTkj8!)bRy|N0xiwR zpb_T3QZuj6p~CMg%`{eyUzLOpbNC#pk`FXE2qz>k#+W7IWmDK}ZCgiWDesdHc~Dx? zmeC74!?Jr>REuTlryDZ5Y0v&(5ZTqxMuP_M_tVbmLfAl*gQ4ISki+i#`PR^2%mZ$6 zt|SvwfkP!+(!|})O`*ZXCL9;L_x{(C!Z2H(9o>mAJ2rhE>?o~bL`NGsJml!GIE=hS z8@x}|5+E64eg$q`m4rH1Y4-QJ6(X*riD<8G>UV~pf)bC0o*=UN_mTohbk4vUiEiKHdo-L5Cl$^mxf3oN(H=zvYElU$ zx7fy`PGY9CijF1#MPl#DAm-xXK2Z+A8M7|5@Ci^;Qsbs5kPm!M=L5d|KMTO|$7TXstNQ~H1ysv-(so$L z(LsdG)Gf8*xCCoX?U{lr&V7ClfMM_H;ORB1jO)TP9OkeX~t3kgAZ)| zExovcgOJ$N$|z**mlt`cmB@9nIOs@$w*#cZaiD8 z$lNDE-M2&9Z^6b~%gT zq-$pSYj-RoH6`pCPtS*SNG`S$7J(UDM(_!n-&BfS;7lW`chhOb9&R6xUm<}AY6TbB zkpQkrz6(mOW-Ci0j!ZxzzrF2Qa{$(zv$HCsab9aJr~3$!M_npjgp&9m6%2fa5O{JO z#+O1re7GJy0Wu0c1qDbitMvSySo(pF&5sN8r%Ft>egzOw#>W|>PK)==H`^44GW?Ic zFbHUAynw~2+-|h+8v?VK*W#8|v|VBsk6hITkE*Y}_1G6K%`*l6MiZ^1^7UF9*}9M8 zm>z$*KACqg@wa>vs$ImzMybz6`d{tUC}+^%G;r9zd-{9z@87@M?cjQf-j9jV9h>4? zTU#`=G}N@Tv^2ErnTl6|_yq1H;4*kd_2|9n$8YNKjd7_(N0PwO#-22N(}}FU4jA>F zozFQrIJSBp0G%ci*sJ*M<Elj zP<)&XX>CsmlR|QLS3BiduR-2?CD|=RMJ`KIm-R39gf5Yvf{hK(rB6{5K2K(=$!QuP zy+?2z`*SoaMqV1|H!)m8JS|}6z{?so+8ew>QAoGX_U5VGDe_>twbJb5!i3AfDEBEK zXBbDnI;8w3o(^Hzx&`Sg&)`~;)6l_lEV7!T4QVnp&FB)e)ws4p)&&qB8HGMwE$GYc z_COthv3otI-XF*LIx@5TIE2az6BiVfDF2t*YwwI#y>J7u{6dtd4d8nbI85Qfk&QgY z0$6)@llC}(Q1f(6sZo};q=(ANrRT97{ClRKa#6CEI66YOvm)ENP1cWurGn|*z`JpJ zl(Um4l;(f^G($s<;E!+F-=c$-qf^8)vqN5Ni3ih(ViB9;xu=k5?il5S7xgZnfxZ+M zJz9qj)RRgNJ6}UlaC>`xq(@03Hz2Q?P#mbMN(RzjYsoIH1|bSdXX~Fq53L3P}Laa}O%#vLfV-#9tkkhT6kEVJOn$qEX7s{5g z?`k}-6$tT#<>gZpL)^|~6j#ZmzDxg0weu*p6^(lNTE&AHf!}dy6DER$+Im@8i>UI} z!AVy!?T{#k!^mIe(?^@RKRY}0n8ywMx}HR)HgfH;xQSNiVH|FSltlWXH8T>h5#gvQ z;T0|E^vwGHz7IJazO<-+?L?836OOYdsPifj>Py z0R*2=#wj`{BPY^)z-U|X#2*a7l;io(NRJ&3qK~eqjFQvUrS3@9h<|d*^fFraB7o4n z;ky1@jgTWo29cE@9mJOmQ;rug630)0))tasFU}nQhU_wIrh>T4`LDuKHDUC$dS$l8 z5@f6(6oOIqp``oJR&6-jlKPuCQ%nj=bg0dr-;kjJm7JMyoocU6L(CCW!5r83XWSqg zh6cZAJp@yF%!HE7gXf=@PY>rWSED8>c(WRrd=OoCST#1`&jvbyU%DEfcqo-H%OjzH z*JFTZ99Vz>7zhxf{GQXKN7TR1xZblHc|U8g%~4yEEmP6%m8YB0Q^57{g71o_RbRtM z*4yibG6xzQe8 z83Q&bM(bRleB{|_;Tjnz%9c`jjk!n{Lx%|h%j2iOC18Y#zaX+^_=(I6w!@(h;?goB z(%rhWwE&QZi@RK2KBrT?WY>sHr;-jCpV$b(A`r|KAG_OZe`jcD_`E}Af&g8SCbqJI z3ZBub$V!Wy5-Xvi=HfPmPvQ^Psa|HZOU`8>HcDGeUwqyg3abG<*;G|~!RZ!4X6zZ? z1^M?zg&;#S6Cde*jw|1?-{%&qfhHZcc_4os4G$=u(?+lfVhhsT`2-;&|+2>~r4+ z?eYR2hq>62&$A*in3N1I_53|?{C26l`V-XbNcK=iz6(bHi z&8U`@W0AM`$}N3-a9!QEP4s7VK4~_cmptd@E%GDp3AX!I9AI@v-`u~I>s?eBD%+G9 zCn+#0n9Z5}>JyGbuezb_r1+%xGJ!MYxKQc6?#Ec7;nkYi>SP3HARU{f(>C@hInU^t zrJu*NxT8iO>1{osSi27eZOImw0Ci@9y|Sh+iMl3H zb!!yjmuJVu{rQI-tdPq*kf!@Ybe$ss7VfKA@QQ$vI=f?8^bx@ia{cp1f{}DxOI%LY6}(cfil$+V39n}}1hViG@q4&j103hYb${+yFQn{|}JHc5-sEaH#s=*m;1+zxnSU@_+6kXRgl&H+ScnpUD&Nop6(EP;rx5iNc&-$-Z8QP^+jd`X}Sef;z1?&ogQMwGtBlEI4~BMk+n33^M&2`p}Ad{4`KA*X?wkU!uGQZ5s9i)(*g0#O?0Bo@uWRUODv~ zBl+zW*nXA@l8q5_=QLU=Wdb@}Z@rW9Ogn+kzi8*(tyG5!R9;b+{ktunmssJs}Q7Gs3$@aYP4 zbu*6n`1<<#`}+q4Jp;1>a0b`tuv+1_2b>udZ6TOhv@MST($yIl4~db6WL$$Z9#jR5o8Ox6aSWd?b1o zO};_KPfSN5$JFy@bXbep?+2q5zr$z(+NzZKTyKXjwtos+5UJePSNyGFdI^JeO{Ahf zJR{0IFSIv3Hc^r&{Bbqg8qJ@ju(&uu6&qC#M~yQJHlcc6{B<{MX3qBS--DS^Wd;H2 zfKqeN*PGRtQO3DB|#kn^*y2z8cE%`9@I#qin z$nT%@Bw|l!$0%zSVTH^l(bM&ubuPTp<1Upy2vb~eRa7)M%{n$Gu6=Y^Z<_dogKs z`tczDhto#EILst>3Vr6YC`xh@^@EBg>20Q296O7M2b8J~yMOFKr0a!ak+=5nS06nN zn=l9@@wpG7 zzwQsTGFW(LwT^j2QC5RqT`l>f@<0EsJDxCBe06uTm^J#@H-D` zh}_K$alO~`eMB$aM;z|w*9+?b#u6S#)q70btQaK6Sa8K)!w4Ww+Gqc&Xf*fO=|0XB7N8?=tx(WsYq%v;-q4e_Bg zS6y6l`o$_dKcUTm@%m4VpXZQfu`@Xx3fyyYosR|F-M6tN1~|o%UjD80{}&Gm#7KzU z0@5LDU^=OLocuL`8lB2OG9Q>-5gELwT|l>5IAXTq0tbO&q$39ep|FH4b~$tqLJmr; z5S$Ad8w1KF2tf>o!*C?yqQ%NNZXg)&+%gPi5C}<8;}*a~G^Aimq0#Dmas?o`OmD)8 zA!?cnsS!63tJuei_~DB!CrP@+b>sntRx{*eaDCm|Mn4sBOEe69U`?4VJRAaDrE^ay{#6t60xoPbGwNRkw|T{~_#(Rg($lXZ6(*Xp3zn>jRK z$W3{H(doasOTxlI*ip`*{?Ct{`MyhhIN^ER zX$%aBL(mB}sGKs_olIliF5l#83O@=B#7^f1%yZsae9v&t;nc>9nva!RV7n4 zmRBBUV%oyUw~5GBSH(pOHPL+0=WD+}50Aem zOV!(t2ZH}=UbzG;-VZk$=KvfM;O-72_xZcJR(=oi@NfYPC@u~9wYa2l8Ig-<5;#rd z{zmOuB38_V7T@KQ=%#C|KOGMy0UrX$)xf#`pKS$rpslUxZL|#laLO5I(~9<;#>66% zpu_4mLG?m>-sq9N5AVM!Sw80qK&oTPYM;CEVC5+XPB`A91$!OiG<% zmpo)!lU?|utO?H$|wUHAa${(ORWGop6;@9y{o*Al1d ziiQDq<$cM4+a+v&CIhdv(=)qJ{gq$L=C%G@n7@{IL|DZ=eUq|t{Nn)s3MTQl_vnfV z;W;yQV8447pYDO4XebE>|FB%$g!uD!)xsvMe`oRISLerx8P^GCr*ZCeeu_^N zz|juNmY%PQ<`HS0xIpW+Dt_5^RnjC>k#XXw@jR(worGC0daGIjNn*kTkH z`FMkGXU#4?;4|Ee30e(0=mHnVO&l5OID-z{d1Sii?)-YTmt(mR>2P0ty6Q6)p`Rfn zI_2AbbDh_mpcfG#%7Ti?OISu|ElA_gjhSCS;^^`%c6R+Q!>5+SLee{!wL2-uhhX4j zaxUsu{G_=;PT%>``R58+OpyjU5tnf`OhOdGp^|cb|LL8oW#LFD9t&?d*v7rS*5*tA zrTZ>7CQszMFE^`rO^Q9rhqpwzXY@+iSz2LT&ZTuFUpx5$1Hqe55ir=VWkE@Fpql9Z zH*hnIqV0RQDtHNaJ}yoMKvV7R=td%B3IP~BfdXATc1=FlZ%xU@ECme5G>uSPdWH0G zsS3k><2p5bxJ9NGLkzls@dq%yO@&b_JQ}B*Mo6AG=3oFtVq{<<3)Nki59c~vD8Z-> zvBqYCJr0ztxq-=xMxV0*j~~WCfPdZkLHq*V;bM7>qC9{(5{)h}i5M*xsve90mkdvh z0E#z|P+-u^ZWv679d8{|4`w42n}7h4M~5P!kCe^tVnP^ga^R~rCGL$yiCfOOVM*Ac zRza(xkHDh?V7Y)qC?uiC2Nl8*EwWt|ATF6p z(nvOl9sGl_7CF8#%!|p097fNt3?1n?hjtlld%Je8jr2J;=y5LBSYKX$Dsz%!mM~W- z%Kw=~e_ffxEe$RzGooVqPum5~Uf}isV85;UIM-qLX$W+o;W#bdc<4uNX_x*vj({sXM5IFGPm>#(`lAwtWm%WtFa+nx72qH}xOB zELLjuXv=0XmR`nutD8E+sS3-FHtzdxP@dX;zA*uOBiq2D?mz1i01yKa{s66>I|IZ? z@BRn0|7SD=1dL9H4X%*b=)YRZFn}pp4F|Rk46_(9Tzu| z^&0waO!;)J`|3x-Hl3FS(6q6y1k=DxpFk3yR3BL^1hqKiMfOj6W z>^NB4hd*dDOazHR@Nxc~0vG6^oT< zevD7GgRg7hYwmY7#6F@vs8^1@3<7ao_f1315P=QZq}Z;@9MynN`D>zwzjpsI`l-j2 zobykAZiLI;oTgD`F~ZzPd$ONrOMm=%SDsLQZydVCy8E=IL zNiG3U_515pEk0{7*8Rd5n+aZ*#F}?n`77oCcPXo-$ImmqPenpP-t_yL`JyVZ5+l!k z&liHYmu}Ht5_er|Zuy#CQ9BaMd&S)=BAt&UB()8l)S~01b|$&z9VU)-W}C@`LL{lm z)peFlt_bY^G@0nEkgF;~eim!=Jn1M}nlE=|MIXt}M_1_ms*%m00vTQ6RDX}w&J~ML zi#C|wX1qozUQX5hs$M3e$gYv_<+Dh_Ops8DjBnId+$gRGIAZb?P|MhgifiSU6c?HBpube8$@gXp8Iq*a|rTj1a|P z10T_i6L2?9=5fDSAtNxN(|%uwS0^!2clzJ9C|Lab;d!Zg2sSdB%+5le8{V%`&8=wLJ&Fu_T`4dlfks4iirHBly@jG9_8*6EVbOc67mbntM zAG;np!P^BF7j^K?pUj8|O+uAPbUu8F7(@aDE(9K~tK{p@)-KSZ#*a3~jZwfS%(mVJ zE+PjU4ij9uiaCFPSmv``8Q~k(hgI+SJw>8iM2i!P70*U%s~u!QVFZOFM_pJoQXi;j(0BK_{24Xx)q#8-?iV7%u{Mh`B*#9E#?wm;7;Wv=4_v){@>F>Ol?Y+ZUE!pE7~n#!lK{(bJ0B1`JP!6csBYQNg_98rr?*%P?H zfAlmd>uT59Y<}BZ(QvSJ&tfcwU@))nIozxYghi=ruhwvY)9`tEgkH6I^oD1xq&9|< z_{+hEXYzSx5O1g&?!JxlVKOA{O%cJhFzt>U(ShLQ4_hMHrLa)@6uyRsDs@zj9(PI-Q$0)A9ns zH9dilHCHp--=SZ(upAc96irTh+}M(l$y1V(@k_@c&tyHhBDn!4y(9#i^EMOe?<}@& z^DNxNjuQqk&m9K`bVm`0dz-*^t7lWxnI<5sCV#kHkZ|QA%?eL^v{W3{*^Xa;7Ba}A{>L!AMJ$L;G8Xjb zuh#}UFrP{TNpCI<_2zqU^l*77vRpFAz~=21!YhoZF9xB=18_gnauBv_fFwFLcG2gd zquQtx`KAGMI&(tMHF@F=cW_>uwJhOL;~pug8Xgab4!awR2){%koQ-jQVusQOYq-~# znE{`WV1WMH-ogell#sF2x2Xz+hEI1H4vPH-FElHO#%H1@%(E)nmA4_WLuPFrS!ERg zwsj_X!H^=8=gVB@tzLtp=Ro-8K}dAgZehASpNbxuksT$%e`)>a&rjX}0bPLn^Uq#~ zk$@PZTugRunFJJB>)a6;mx4Am^LutSqAj6rSlju6Q+pbC&4&3WMRhUEW_8FyD7_9tN{fk!o2`4gWC__>a0Yn-%YS*b>3xsM!%h7gs#H|;%IU==UcWinj&T6vO`xVmq*Vw-wVp^q8=xYh@l8iA;_FD5A^v9Bs5rC105;l)nS$jQo&IpSZgAv z|GtYtQhN+j2yX*-m~1!D$P;lN02cS`%yCwMf~~oemRE^QiCQ@;xa#KzJ0^e86~DQ6 z%#-e0!F=$LG(5An^w9nBO04GexN42;&pC96mby(mR9S6v9e*K-5z!3*@Y+Xe5L6;!7&^B*J$qz&2r zvo9^?_g@h?>_V5r|3Lj1%Mh%((XkfK!h&@$dz{5J4$dWS5+1IAT z4tm9w8Fn;Rh7tO?m0}zG+hoDMzTpZrq60^JvkmONgiOBTHDkU1TpT|(9t`LW#KuJR z@~B+xRfn|r4@VxV){Lx8TjV%(Ui%Cj52eaJ5Pe`tHUvC&C}lnR&87Wz=0H<=prBgq zXMH=w&&GNBa=gkT=xFC4^G29-}%XKEyY;HcA?>XouXEe8$r}L`b zO5)OpRsZ(K(g5*y1L8judHXGrM({VbQzVh#CeJaISFFTQ15dh_rihLTY7R zo-@^TZIs#iOCNqLr@l6w1xNdi(L;paA@~i5I^_HJ`ZL8XHBhto@1Wj7R5ER`PRPT9 zz!{r~@=1c50l5c zFgg3M4`?!iRG#|ZDm|1m(rmC`omh}q&#uAEGkPfS4q~q{Cv}So%V_9<>T)M;sspuu zYzaA~ViYy{`^Y#6=2i)XEL_FkD;8WP)!XlvE3molq%cLvL_}+>#&3x96c?Ihs5YbG zE*WOu^5myi2JYwbYAv2o_MZ{4RxhKEPvdiSSR|Oo2Nn?WqGOm9zL64_p|a!NOt6br zrz?jq&@9tY4D5fv97k-vUfDA6->4u`D-hA<%1-vdCXKAuq;ZS(r*c#nl%^XZuF;U8e#w7#RdjP5(B!U%ve`4#b?{1$XrI z+390q70PgNA;!`Zm@ZCqr<>0uBWHmzMzXrdi{-xU_s_?ZnnPurDnmdyHff*~1_epL ztjv*F?L~nO)NFVgq5COVAd;uf5DBQ8BN6L?@WIg>K4!$A5Ezc*t{2$@N?mxv>09jd zaLWdLNYfh)3b(-62Q*6O7-%#${?heVWK*RWJ!GT?dQ-Y0&_viRvRq*)=q+IbaEKG5 z@r(ZubwvczYfyi3!$pRgtMK_$fI$$r3*52;NgYYZM4EyTPDZoKzsoBZs*=T zeQ%qo@AJK#5!}wBDfo4-GG9*_u=)vsnE@4qqo7O|dU@M?5OrpF{AcGxo75UkYf5Z$9tzVuWn+06#`8=4Fz=&LgnD~&-trN zPU9Y*MsZ#B0mpu2W(F!kOS*7q_#(P#Y}Yz6X*|{I@htQB@72suO5x|bJ0DlqFSc^M zB~q$7TyK{?G)$8S?+mo0VJlAU!X={p-he^(RK`J%OvH7sV?PKo?RpHxfFe*}k~gn% z=1uFCB!i3gHV|-hIh<1=y0N`XynK^hH7+HsJJ1C1T3!m1$|k3k$)I zOU^-_!N3W>8wD8W>w(F7WhyQXSSIFfj9aZy(f$pL9Uot8lhR=KoV4#>hgM5Vf$(y1 zc#+D-S8uz|+{8Ydz5u>^4TbH;%S^x$NOko_L?!3XAZm^IqTt@*SQL0<8f&(Cc?+8- z-2Z(FYSsfL_&oPU?j|uQ#JVim$KY!);sUt8qPBrNAdUv(hRm?13JfcopXAw;HNpGZ zs=h<3keIHu_>b>H#Ee>i)AVt{s|x*Gb`~GMf4}`TSa|Q`U#onIry*1X7uDZ6uhqZt zi=^`M9Vxu@$Za6wTD;Bo%E#yV^g>*7T4`%;;Pdpz2q9W%8|YE?kL5urU&0IQHdYD{ zyEzjt$gxK4=h9(g_sP1J-z<7xp8q`(0V}svm|Q5Iv%*C5g58gv^~U4JA1?o%HL2=0 zs4dt$A8dcYSyel4+@jGUw6TrY%Z~~K7HN1~ACA5^E{@HeB2j#O9*XzuQpvn5BE{zWiU`oA?yIkTP4)S2<`sO`s3E zys1@)mdE(PBiB-_U;4BQv^B?y!abD}gbYP6%jj`;C?PWk;hQPPkb+>~=^5KVCXk;) zg#HAif`I(>xErpW+1WbzZB>RYyUv&cjV*5vj{L8`TyG|o%qsr;y_~qMGVL*y3wrw} zn|WGkfhmU-Z=(~1eV)lwGWRt&KGJEv`yy=~&+>kaDl=9F6@MyO5w>^-=Krx+8C%iA zmxBp29uJ(U=(3pfd4H2L>yo;~83-ImlNr|~Y{zq0ko{0W zgoAymf&ECP;ZV6rkPvp6{8-v##nGqFr9L)BP?!Y1Cm1A2*=%xe-nIbLGLVBp)R5np zQ6FIs7WqEdXXQjpUwJ4izPPtVuFZ*6TAhS6qF#xFiHyrkturbw@VLiLJ0Ts|wJI=Q zS9bSzz5qk<%SF`7%`o1!v)9H~I(~A*$ig~l3B+XrMSA%Ua?{Li1s$r)qpKLQRyyxB z+S(BzJf?Ut*+|vnqBwRO%+kcpW(UZvu z>BG6D^>ztlX&r;JUhE%y`t%NUr))mcDdu|&rMHE$Q`#!QMk`q&=hhVE&bu<;i69o(fmTkBx-4mk8_Zo$7?6#LO{w6 zx8ueDO~TkCYPCG-bd#=1v(uPI(gs4}+Ljwh2{0NvP_&sbsp(K5lIS2^Q%Zpr z4m9kLMME<4QuQ$?#tFj5z|QR{Sl?;t0Qz=;W8^>CqSm>maobUs2N6ky`L#tnq%=Jg z`N}BGP$2|b5cqs-@-$=8f4k)m^*JBZK9T~=VyL3?u`}^a2OuF^SMi{JYWv=>Jcist%bUyWFY`A?=r;jP zh4q>@+GGeYmeA>BqVk_sRI(t@V4geWmC#;Z2vIsRvpfMuXmm$^kKdmkgDZVEt$j(2 zSmPe)ja<{sb5J2=Th(Tcz?NGLg;~E<1UR=Gnot5&d;9o*7V7^XiW9qx53SNyV{`O@ zj4SagxPj9WyKEi0A#?jj=k_hBwrDB&sJF$4dpZ+7~Wc2~CYlnthJ7C=YoEf6sj893rd~b)Ob8 zT-soQhC1EV>y_AX?IhKP;!)Bl?$RfAi$C@cC;~{HEITt%6GUrtLilqEh`4}NeY+!t zHLb*`U)CWi$;zxAy)I@IXOBK8=VJ5~jtk#-ea;PhEF!PV1qLS(>9NDhL>3mQzf(~0 zod~Rzr>Q`d3I5SUE)=v>MySKV9?pC5aI>`ia59yZp-n8#*xuA~rqi(*ex>~H7IT39 zy)vgClGiAYUZN}w%8T1lj}N7vnSlHs_FN<~706qoM6I>P1qKH9sWd@&94&;JzILM0 z(a^zFVt8D^iFL{=%D09IwYlEs0J z-2S`aNc%cMjxI|>A@7W)mKp1H<}~yE=XAb|yl-@XD4MET-lcLFNP$6sL5J^&Kv2ph zk36XHfsG;pQRDkxqm>9bDLHo@aJ&)Oy|EhMUSs@pjdw7HT(-teD*C~QrILmK6ST`e zw1kHdA_0ODkt?;+BIdy`zr0e`8L!UKgrag;Z`*dH3V%ZQtXK#|4-ZO(!O%;%Yfl_R z*LV;%MOJ>*wGnYZ=#j%AD3Y?EXecrmf~>%nOP=MkFG53~@73GjCiA*-HG zPyTq`-B?U~jwTkSOn%0NT=C2ayIC3%uy61_4T0R5G)C1(v%7uk0*CgEm%jzicN^Q! zUSwRQU>rT_8B}$ftq1B)teSrc3*a2+ zS`;w%Y!T&ZuWc2+%5psM<-vh7o;$5X;LE8z2=@i%McFmSaNlr`apd##+kB7$a<-V_ z-`-`b=Z?wc69@ER;F#vS9C6UX;mPP35SZ(R8FnKvg{)^LFJN-kv@+}L=?XY5&7|r3 zdvtk8Xd!3YBSwQj0Y{+#ng+yi!QpT}6GM84a{XXxHbuD&HfTmpk(kb|iG@IzCaM4F z%(=e49sS>HQ>C?ElM>-^DNNc;+ofQtn!**C94P zTh#E4Dqb$FY&CZ)M5Q~>j(zSHQ>AF>_P-cW;5EhBA0R{}{_=QL@bt^NAo%$o@LML1 zTg?9R={f8Ay|ZK9ycW)p;i|-t86t%am@Dy*Zf*n^tq!Iwsm5KY9{T2dsiKCjtW}Nu z&syIb`|i&d%OXpVu!I+K;m_wWSVWWr#oNd1x;jLac!OLgw)ip zl2(V8T-LtFzD%|1Xg_PuyG9W`uHq1VOX4M?^ukzEne-l0H~&abp^aMm&z-WKW7Kzy zCd^{(>x%3iqsbQa3 zqiL|drr%;|ZvRTGf@}O&OX{!I8kAx?oIL&;ruQXI>J>tX38C#rS_xXQ?q~#s^WB)B z)4(Oj=EZQktw4FHC|f0_;*>ku$C}tfqi&NXOw|Z<4$b8jo3;)udPGJ>)SFGiYJBIJ z%@fU-lCJ_kB0LIOL)xtTy2Q$ zNFqq@s#2DCmZrAL_g;>|7AS9SQ`1h`Cncl{+LlK`d2!ZPG(`}#*j(U*QeqUGRzwo{X~k=m0recYHzpqjy7+TSY7G8A5gGI-ZK zNjEZlZ26=u*lus_hR07^UaE~!;_1i3c}10P_@OMQ_4vs!vn92CqeWD;xzNC(IAG7B z6g7*nmT!B>60%KpQnsLE?Jo+hC5B9*(C9= zZ>TwNlH{5g4fk1JC0pHDqtB8fBazVqC?Q`o%nb$w%y#!Jlv=2%I0mjH?aEy#pJMOd92~swe|{`~_jKGJ z_{SOG8uqRFj|BsdY}z+Q$q~ra+*tH5uRKD#<$%9VWjc4JD-~cfofY0s(G>86V8a(M z0`U<(vmt|Co$S6rl%Rwm+j?OFAI?NSFrDj2E4k_+M%R@8nQ(%ISI^Cg4nfjPOoIjn zBF_>o9m0mGOQ|glo+ zS-?DG9MUD|ki)!Yk=!?;*iw{De$K(Sqt10494~vS&wJh5Pq+{oJi$`>6ry`>rR9Lh z##d74caQg@@1CySz07O_T&VbyFKLjAC+A;kWR{`IJzL|Zo|}@UR4%;61})DlqK+Tw z^*=v!+wSN$AP1D%u$|c6snacqs3mA{Z76&E-UQ0Ogfsoh)rHAT8|Osj3BvIu*;wt zD{|g*y=(xuu3z@jRD*7QMgfkRzP_L!Au0uCJX-PZPVIohUc(o3)a4Wfm~`2gOSu`( zAcw%29BC#*g$ukf7_fv~_&lyx*Pq?`!xp=&_VsrP6}2&@bPr3k>)W<-Gj(P`lTn?E5R;zWOLEZm@Do>h&HCwFeQ89U|}hDCbwj ztXZRC+ABB1-J0*$_DIH!Q{!yf%s*mv6nU}I(17~!Fa1s=#AMxFt7W~=rmIU=gcEKW zv>z`Ws`ijG#%ha1yIy4{e9YG);bid=FOW*OA0-fEa) zPC{m^cxZ>-rZca(G(|5cVSFfuX0LCk{WsqJnmXf~wgkH3@4JlN=(Z+xgf$HdlEo8c zgWi)yKW8_Q(%pAb!xN9bM>f5zgpuH{Dw>@Q9 z?Q1w+l+xffC#mngs8wi#XFm?7P94$@-bT#Op(J+xApMf>;qr{L!(4aeB%@{6+y)}W z4XvEHnzD9v(o0$RC#kD&9G`MJUq4IAOcqp+(k&?RQOu!Ii#f=0M8}(T@M1!}HDjPn zqJHt(V3}za#c!9TCj^wXH4YI`zRO>Ua;7!3dv;Yzvm|sUA+PY* zO0f$C|s$IE0{1#2ZrjKLdEUiG3Ie796Ydce?o$~%k@ZsPXzs>)#YJ7Du9L`pgkr}S% zu-f(YpLM)?U|d^X<6^u1hPQsZ${v$@2^WUV@genZ@)Qtk2E%tIoq<2_61+ zZ|ME4P|nb|fNbHpl^Hz*ck#Q3FpTCyYBp=3@|eLP4^ABf5S^Ux;jT~)wtu3k(N`Gs zTBp-9o5$5`TU28`?M4nJz3e_>5Moy}dqwR>;(It|@0#Fdh3>reHRA$Z%d6?6ueRe} zX8Dbnqb3d4Oa3p_w5m$cA&)81gxys~Kb#fHZvfad{x0w(tf1$mz98TpD_$-o_CsuL zM~t2uB@tYA%N~TkIFfBRsVlV4gX=42$(DyzJ}tlK8be1PpEHsX9)YZ;2E(JYXkSzM zMXd+tV5D9j?9f*@IrJDaT!gwenSLPbYsDok{Mf`EipCUm)Q zYNXTwr{@0eMs69Y5#db}LW!k$RK6zz>_D`Ld#A7K18|c51_D3W5&uhTM|*cS*nR$t zaF{nnKoA4L=W5=Xl+KBWh44a?ofd%b>R;zC7qrhnQNgxq&_9fumj3Uf%GlHqiLu1k z(C{=ew{>mAPcP_tgvX=$Eq>@+N>0JhU@9e0UIJ^WO$WbF_ZRHu-W=J!)HsfWEZ?mo zK!1&M6;hGqYO&v_)i&WWBOV*l+dj-&(3M0F>Edugx0nUSKIR{7+b#%*A2RYfi9r%Y z(FBovPHmg^CJ*Z?+dhHq+Xs1ZTqveY(r);sZ$9;@7^KJ*VW7EN5eyaS-Ue<1MOmH* z7EqRmPPK16m_wmV=mH#eb+IwgCy{SJSw1H?>Fx7n#QFS7E0Y|ZAs@uX?5^9FE{EGZ z-yh^ISJPZP7ER1mv-~Rb?fF^TB_zm{Wm;$;d{jRC6NnFac;3%6dA!STaRK|&Gobv$ zMX2xTW&_9uU^K>zeB;xF^glG6Wl)rj--VZy5_gf7T)I1@S-PaVyFnVHYia2eq`Q$W zNokN05RmRJ0fF~^-kCpN8NTccyZ4^ob*>{LSw~~8|MSZCrg*RWWxJzhyNJ~7sZKvG zbYZf>;(;75`}oC9(KvNmE21)9$f#SrQK8bqzgqKe*VO8i?l(oAo))r79|ftFXkRkd z7xZ-2!HSq@S}*XZ$o)<|^P;(xsAiyJ@Wwrp+o`rrd!sE9LN(I=SO6A}h|(xJ+3;|g z*S`_FeXIOkeNL;yA7|+gIJ)|oy*n#-nLNF-9#Ms@^-t=Iv~-UJR_tsB?{4oHSyTTt zF>3L2FT*1%etyBYz>s-z9B?qT$NPuf=^|kC+f;HA!8v?tX$5~Snz-^7nPk24Zn_F@ zN;gBZXtKUL)s)5HcNf$16XRWRl=&rl%Vhlf)>*UFki|oZmG~R8COe-ESB_A19?J_E zmhdSLL!S5v)*p-)!lC~>mn)i=v*l4cxs~VJau>gcG?zX)2)BJz4LbmNbM2~`x0c)? zf?_I1s>4sKT>o{S|QON^&+spO6OV*qVcg?g+`=;!K zAcv7xe$;hq?CX>e-LQ#qQVLnrOTC47c_sTQTVns3LvK#y2-yz@S2ojt`aY4* za1)qa%p3MF?G_UwVoBt@)5uF;#_Sl8Gi9BXavYm?$5V4b7fT6m zaIs}UYJ|tT(kd-cY#=ls(rZwZIn^9Wo}?dlr*VwVzn03m1SWv`vB|F9i_Bc{=+SvD>gerV>W?rvSDP=*} ztWrs|9ySGZRE=OCcCXGx{jEBDPKvmsy=o;i?{3QxCj+!2rYd^1g;TpKO>~~NUuyg^ z8I|7?iV^Z6fB+g5ko0)z($vfdD=f(8-E_Uunf%imyfhRPDvt%yZ&M2NWHn>;Zh8t_|e{K!$m z@ZWEbr=Y;kIsLYI;_0W#itvX&-d1*)g9<7&ZaJ=|MuZ)i9}5+9#19&ol>xLHrivCSGXe@FrPW4Ff=Zt7V+Z;3gz>+5 zK)tc}Le?&u_y8;e9^X9*gXME;2;Nz#Y*t{(rP+UU&u=~au7N}c%s|k+-_+3D(Yl1X z&*@!J!HU~>y<^~~ZYlS?Dz3U~9X0B2tNb^FyykArV;`RaI#$WBUxA=lf~V{NyTOsp z-1~!bP{Y{3XwC0$keRpZV?hee<7a`?ByJIHunq`K6UFD5?$~mv6xB@vw;~C`SR#SKxi1pYlg;)o5PQ0ce+xEA<#f$zU;J1_9>%zv5@KnEAU z1>7&|fyI{5GO}MF90_bFO=s4_^!j`CP~ntN-`mQS7VQFTmo(NMZpc5k^6 zrlRHbr%Ww2y%24sYF{ED4LtoRqF#KIH|XtAt|$>_%WnrpHO#aqf2o!DR@4X^9}Bm= zMXi>yd(>lTe5jIFz%+f_6EJ(I#XHTNbUf?Uxfxo#oOcySZSRj2qw-jL)?rKK|3amK zVolS9cUs-la;A@W71zJKb^mQNMl)RSE{DtOWdz?gd8G0-^q^s&7vbS|yoQ--TBSn|Um3 zQWnshhg+el)mzmx{E%HCR;)fHGR<=@agEdpDXcc-Fr6O&`F8(ZZyr3Y>{knOk?-X< zd_~846C7+f7?AW{kZaXvZM;&r%f{#0R4@9$+Y^*AJEoP`m@BhA+1O7qQfVn6mRYM; zPH$X~w0BVHKac7tT}p(lgbM$Wv}VQ??W4`sUg)Puq9Wz!&$SU#im#_kv`J~RJ419S z0880bz-DU3QPrJ1b$HNA96W-sQa$(txR6mfG*_`Y=Uo;!66I|EjaME_L*k>_F~F82 zPFh21`goEdyhel=*LJ=ta3NBj09RCIsIu(9!Sky)*JnF1x~neKSqjnQW}JSfUKYyN zgmP+A&9a4ez$8jU+{A1(f#1?v!jJCHAP{$de{&N_?ti}KclHE4hKve?lbsnGMW>vc zs=|{zs?8sPzhzJe1!D`6z8yfOvCJ_TdoLq&V5F=_v(DtSdQR$-;QNFA| znkgnM*eq5}tjHx3@txZw6+*H@IGo!H(T3m5r0CabM=KU(N~y}Mi!hlCNJ#@KHL5e} zoh{Ee9Zo??!C}|LS3VS@xTg$8upB-5WVVl7y}z$3UtewhtuwhbdB4lANWcz3>}v`X zj$7z!2UaclA}`QI3V>{RmnFRr`DpZ3|F4{Jr9cC0Cr zz^AOuk55iL>*aXMPJDtC;Ui{dvB;dSbb`^o45fw-`L60(FyR-cJ_TPHj2?ww)yRzX zaiuSYVo4>lMk$78-rwG8?K(8KKgz?h4z=0jBnb*QchvLpmjRi?iIz*qNEks zykAT#je;`qd1b~=t48$vTr=fnDPXokrEs_EIjEJe1!U6grB--|U}37Wp;)v6I1DCg zM7oQ}eY@Hkr5R`K&MGr-MfaB-`sY6UZYnt}UUshY96vBe%J8gcvxm46qB{BzM${ag z8Vdi-d*145S@S*mZ`VME-2ZxnE>Fns_ITvVTKh5P#IrI0SYJKAwhrKrdi& z94xUfL6XovdwK0GRFbj=D5swSYKbJ4a}08Aa+Lx=%}b&A=KRDe+DpMsP8&VGygo@< zBzzAo$F~lS;YCuVJARL)DlNetX!PGUFpjvW)6=P_DTit3Mj z2tItQ-j#ZeT>M_~U2CgTx@z+#wJV3Jkk`yk4e~%^(^)!iP>bFA|BjbubJg*X3;qI?r z#`9g{BiL)PX+f&o<7l7{XWqTZ2AZR4x?Fz72)v8XjDo4@kEyw%>=;x(b>EqF-eNPX zmoG~;mr1>rtk8G|DQVpmAU}ykgjNg_e8HT5rwEkjoHmI1S)J<6$Fo*lVw9A$)h_fL zuC*MMdze(-g3Kr|)H6osj?EU3t;uQ`))s7~Fng-O!8^7QqXs#E+#huq3V=)EK?Ef~ zv+b*uVd_-{U=RgzSmwc1qL1M_yJ%5Z&W<)!J$J9Y>uBju<6SHY-+oW zb(#UjvV2)d$-a9#6Es{vs?@khW9#dTQsEaND1X9%2*&rZD2_4=^>gfp3M$A&OPSBy zI7-!B0g`f??s%iXH#xVaDn$Y*7HVXbz~y%t$(;81-8o!Thy@gvRrZ?i%eOg77lQ4x zYJ`TrVJ=h^5rFL1XGd*5M&9u%#_#%~zn!`tV=CODEE}*S6PcX!tt7|uWpSGRMT}JF zn*h%rnbov!$}y~xy#dbb_oB}L+uE4-#9S;M_Gs9$?hK(Bcukeif5A_d$rZ{@ z0Q=dv%^{7Lgsy2dqWh77!401t8eVKMBTW9`72Re^;j7OOa2O#44j2`c>`GPw;tLH5 zK|u!V1c?*QUD_F#QX{Sgq~?eh75yA+J>r{+0%i{pgRmrEQmq!O=+vfg2n{j_nVG(~ zR|*zJ({EdphZVNhX`6{BjSFR@@ry&@1Lf%Ol1~z3`p^I&j166oB6{3n%zA*Rfn{hu z8j`|s5RrUZPDZ0a0+1xWHQA*Ofq`T}P*6y3_-Apj#PONoZ@-hBTAA3bj4)fm2&t=nQ-S{$^qn0amE?in;qM&`HmMAjK0*;FFfp&L zj;vqK6JGu<2HpXs@AhKff={w1N{%E)+fu?PcXPO` zGt)VAbztv+%aT%ey)$-5FA5LDOR zn$uc#u0h2ODhmq(-#$J8;qKLLwsXM4LFs?&eBeclv5@~mN+xilBNO&*I%30JUv(-q zj1_>)@W=PF|C#Rs_wmOI>;dXE(Y^myP&_^vg)h9ToX;PoGV??yA-B9e)Uxl3-d#u+ z2@v|0_GHrepkbaio(Svyq8Xss?2e~o+!a1RTyN;~@-3+!Bi60e8`c^1*^;C((8Q-) zI|hxF4&=p@b+eYnp@4m57MsNorabLWpcS6|naCx2D}11Eh}D!N*Xs|opwAovT@!7H z4v?aMBVF9ak}s4kgf&mNniW#v#m1Y4N%RTUNUgFctQcdxmzJ7$lh;T* zJsqC?F3QVfT$Mz^PL=chpj|RD}nU@=88DjLL-8 zx+*smy$k&vhe7O@QkmaJi*3rn6Z(JsZ?xWa#s9jnwy!`0?cS7fl!chujZUcjsVs8B zBdoOnshL{P#haE_7|W5jsxuyEdI?Tp!=HtDc|U0``6qbotB$>gh#NRpb6Vc6G>%j7 z$q=^x5*F2J7VLYA&(n>ksV(I{kNwxsXFX*-F0@!EuTdiR1M%TpbK%!or_9jmGnb7~ z4t`e_v>EkxS;MiCC=pM(7lTI2CZpqDH6&axhtwYyHKogt7E@fUTHvxHZI`D@b%W}M z3F@K1tGx4C|*(t^09_ ze!-u;3im(8?Mz9s%&WUq;{4V*q`cH6E+{%z8&H52*Wiup8qZKO>V0+&%cM;icsbzF z2>#CVlD*fFu85?cHcMvUtDO>>HXsS(j-i@5Ab{I2-_H#)<1E0}O}L!VF;OX4EeU_R zDz>48g9v|+R&aSU??}lXq=uae6xT&JaP~2Mn~|Bh$zdrTSU}FZ z_7t#Ow7Uwc%4n8P?igsC-Cq<{y17L1m~GSCsrC{KYJ1_=~#gnihS1^JUl z3c;uc7@$eWU{F+$c(jXYpG&KN{CBwUo<0^eVr(oSqS;0tN-yUp%>96dYMP9(PP&p< zkr{)RsU$ggiF-a2K$cc_c?7st=EIq@qS7)1A_yN?SJyJqrw0^wvwd)>t!OE5Da0rk zi@~P5ec=xQOf-A-vIr(5*i`6ZL0~~#F$$A;@&O41*)X&9p4-EBk55t~Np2#)-aT1_YGQ^)28#D~) z?12sJe)8w@t0~bp9DsDx!RrZlh&J*)*Z>asR*jIv75B>b_r6g&Xb=b`Xo&xv?hgU$ zbTbmGG3-VZxx4XynjZHd8Wx8yVsps;6JeX zKes!{YZhA300*{Gm&}Z$Ty?SgY_FNG?{@Z7=kFccR*+0T_u(9ZTxcv*Y9; zd5+`iv$U{a@Rh4KsXro4Bl4UZl5f3{46XtM@v!~J4=6fDk-=p~MsZ4#+QC){CI@J( z!|QBwGlWOWsH92lZ5V|ZTupyt^pmDDZA{o#uL}&tez~LlOqTf(9~2P$>tnA~EtB6q zOWoNbzQfw=-X2D3@lE9i`{;CSUR{Kbs%2}j@7^*qGPY{wDiu=lUqACo zU-x+;fGG~g@`h_c4$RC2+PAr}u16huZ@AXsVF6aTvXk$(*?+p~IY>gUEE zaD9~^x!7|x`CgmAud7t~w^k!9QvG{w4 zT3=#Wqln}@|718bE8no9blvMso5iA8@C{SyEzk|L+sl!EV08(wAZk)}a|zz7<=LcJ zu+JotOeV6QkjPNVk2N;R={@Msyqg&PSb(C$3EL9kZde_~{!$ixyz}p{+mV*_%NY#s0||mwYm7}V*x%0e z(uY}57N+m7t;tNb3R^7$d2lzg3Ktq{3p5q*3#-}A?$tQjakva>hHNKUEgRX^Q)+F> zG`U+)ndGki_@qpTcn|x}L-;B0lyo`R$~1qDG}r2p3$wwh2nIsQ z9lyeVd}@A!fbu&(thmEqM1uj1e|;F4rS`Xpmg*1_v04YZDi_aekJE9$+fm~=aCalS&-7^Ljb zrdU{sypU+>dz~+_JmU`$>|tW8C^9t3)o&8v7MUd^d-e`eu!J>Vg2p{r*YucN=8P4& ziccb~26TGajNV)Pmn2fAN(Yy5kU5+84{Wp_$C<-mxMKZaB8a8(rYiL+kkz%c0LZ?W zs&0ZNF<)HKwQbDcYsXYHAEW{rr5iMrcX&qZ0~3sns=ea^33ibcR!qD^iG>&FX$uu* z)d+eVwJgVhen>M5c9kYv1h$nKI3aI!p=#QyS*tpf15gqP`o6^$K9$^0_y4`A_2&h8 z`w6k9$He&eNd}s9)vm5$>JGHArVwhl zV$`b=ZOhw~sO_ZTBt+`I?l)g$M0^h=WX7r6_AM=;Iy#QT9)@&3II$3atD0!|^+V86 zzOr+1pdp}Q^(DQzcM(2i^@Ox@?8TzPEk;dWBTHQgDsm6Znv1axllG1Ty_$k-F|)nW z$W1Qc;!+euqK6@XOI5!!H7aqKE*L??zpF)WhlF?3C@^p+fHuc{n0s5JI(k(LS$xKE z_^|ml};O5#ay6 zpo`uoi~3yeCmN!85~Cm>QiPbG`Oa%DdY}{?wI8hzyj-)r{7Zj%VgpV`biUbofWOy~ zTXWXxsp#&q(eYhR^FCSO`ZlBJQPG|o*`YU zhEX~$NF}5cYO|*D78M#z2oW=hR^da;BJo_Mbw}_V$do!iNZz9l^?Z7GSNq?a=P0I= zCl%deV{^!50y$dOyan0y4bgwvdNq=r z_$%BJV}6vuJjthY^xYZhE-p@_*{TO0Ry|gaYI_0zvLcSp=w&7$;QIS3a-o2K{I22y zk9TBt3n*4=XLkU)0sVRBFCTTEh2XYJxCI>bfVd>Z?Rm1L=VpI~3NkaUyKfczD~z4c zbFH;HY1w?EN2^*htU}NJU1ae&q4Bb5hg~Di%&771N>!GftHX-z_eMP~cOlZWj#7-Q(XUpI3wC1oWV%_$&GA{)RlFJRMVZdz z#&#<1((>`xgto@X`qbF;R2_(x@y++-YFj^HGbYHc{2`U^swk><|H8Dja={L0K7@WN zS+57gbo(f4=X0_jtE?EJ(&3~oH!~V%;$~RCXW_a0p3Cz( z9G%t#6{(qOLY>H3D9Sd`ngaGeJs!3Jk1l)G4(`l!2>PVfQJ0G^?zrwBm%eDW+z+YU z-T^)_u9Naj$mP%1@0qHJf+LC|%4fc7-=&1@fHZDSb)%dRj%?>Qe=d`+$G5fzq)XxF z!;h}p`f;@>HnKSGERTy@3b^vnA&Y;6o^%t-G-+hQ%C%e$jJq)3LBhYCKCGgXS^3Ln z8e|rg(=O7Y=5_|))s3=EZ7#(KD{0LSTJ<@6+=*(&sK^`lu8NC-;hHyd z(~wnF#{_xOnRe2}he2=u2T1@EbVk8tFh5$Q(W)j)RYQ}pyID-EF!>fIZNcs40+$Uc ziwyHwvqGmdp@1BidAVI}#ta(=+%8$aT(61#XL!d5=3ViI_{9QrZ@gq$eoD&NXSW5v zWbTd_5E2*@BVnLkmQ_pxE>P-9GW*%hDz}+746|Y^JzV$7c|o3Y8v&A;Z$V4hw}F;F z9PsW=du5F9JwU{XQP=rq2D;rxdJDpsW^-pw& zz|ID*+)&U8XE%KqAtjDfk>c!5D2#gl6F%_>CVdd6?SKX1I5oPhlitp^w+*(Hgy0#^ z9y&~(d+O3Irzs8RBpBP&Ocwzq2!v`Cfe3@^LWjVTMy6MfgqWPjv*^F2DP-@)6OM?Om{rdn zKbM!oji*-t>S{=?El~q=qxlW3zAVSi}|4O4m5vmh@S9r()Izt054RUoat6G$vKbl`QbIHO1~D< zcaFjCh8Xq+zJM;T_)w!>2Vd*Ngw!VQlgb4`JmcvJ_u^cwJ^|IazSOe6HB1*}Fzg&w zog1yWr#IQ({3uS8jxcKmdfD6i!!fM;{IrQo=Y>P?zPuJvv9?7;G?3n}#ZZ%atMh=y zs8pqAU}fp0hHmfib%Bb~Ig_Peo@( z%Cc53QV%nUtxmqzZFNaoWop(B;q&0Ug)Ab}_eZUx-A%DazBhVK&^4Ud!o%~UZ6S!g zI;TV*A0OTrQ?3z08Nw=(Uni>+2oZkg!|RSq+&wj_uOE~ak^0A{LBsB7gwwJ*6$MN4 z{cJRP*>9-N^~eKx?oI|~ zZBRljCR1H<-#%(FOuCM~oCm1sP=7KB%KYJCwt0=Jd!3dyFZG%elggf;)J+K4L#PA| z@pG?S?4D1I{28=+`pG0Hd1+Ay3C{2H?eDhh)dFp=duYzRh42}(yc{LRT`>WB=)We0 z&Ha+RPu&`irWj6XJ;E8AuTTt$!tdjWUiK9}n*Mm}N%`x{R&YR~=`%fBQyo?|638?{ zKo4^_yzRcKdN`Q{G~w+p_!u%K<6n{*H1gBlSgvrdoI$&~qUINK90lU8o4TB&ZrC~l zI|_jMkYGom{{a)xht+sMkrc@2Ue%PY9lzizAUf#~zA{xJ(pTc>oMW|}G;SnooJ9v0 zs_PRhP~a#^D!bGQr=r;ARn)vRN;F86o1L7^N)67;mh2x}zRFrSnX1k6|GNi!^8a=BSC^rAqAM4W6AaBCkNMSt|R1%YF*+ThbCC;2h1 zjZ4@5Sh6@J?0V?bVFZsSPnDPuflaZ@3bD%pMI}xo$IHnQr&${=$F`4hd~`Xzr&r;yW%iOaR@qiJZYB$5*BkZShCXXPJ?{7 zC$&l~XKS*XbHyM3BZC4D{Mp6F)#L22IJa4VzzRIHk&#OVOn?SF0SFdwg2MTV=D?e+ zZR~#o#Ks9dH`UIW{HaNra&!0-=8Se>wqn0eJjZhs?kCOy9$)?~A z-1C|Kk8`g3f(qYv?W4v;3$l6(vZ?*zM1AmVRz(=AeDDPPcriIiQAkC8O zyEhcgX5h{`bTKOTm><)VvI`RyL`@GrMC?6-GVz|LX zAa1q+K+ELs;i0+fE)kK!EVuFcsNmP~Vfej!&>$8B1YSVUN>(!w_PCuYZ&!$&_Oz2| zAkjPQF#dEefAeP?-k{g3WBeMq1lJK$Ixa2aK|d5+9Xp<$?STLU$K5YaXFwt4=;g`z z`Sb}$if@mX03xljwtM-mYkj#3cuDxT``H;KJ!As_GB}bvKo++Cm~dufXgEkX?8;5i zmA-`3Keig6qMx9;(^&QixsvM3UJBb1PQ_tYIC7`0VfKZ#F!8vwscwya)ec5q^eP_f za>!k_#(A&WF4J$O_It+J{pRB)#16Yzu?oGb0Be=V=svvk%9opDtm_pR;PxpthJPk zv?wYS*@Y+8qY{6&zi)WxKIwYeRz+(0o5ciN$vmXum66_a|2dGKX3_AWV`6olr%`4XXkj zbLCMgRALUlUIXkue+`{GIh1Bov5>g&WKua2aM!< z-F5+A{7#!&TDP&=>&RRaYQ2{ci}31{Y+Q3kwEx`qjhDjQtWfhUud~WDJXfXyKgpz) z9eLI2wmLLQ6b+8;vgEo;zgm~Dzh<0#=))uElu>(OyAn3XgH#ABB%H|=3GeLnVRSvr zRqRh<@bve=$H!W=bIC!#*>7Nyf%zh3XCWtHA<;ODc$GOkgvH7=-ffiCoNZz_{0YBx zoJ`KZ%KZJvb`Iy5*3$>tKmM4xW9sz`73C@Fho&~G!|*a99#es@`-Mx}jEPq%7nCE* z>AZyIVV2PfGH#@p_)H}jWIANh0wY7QgB3cy9=v>xgv+EJeyMvhPs4VvN|3K$A{`mB!>Mp$^nYPuVtp-zqCMn7Y(AO zw$)l4x;&q5OB@Gc`1w69X@oNwX`Iy6(~g@6-8)v(-NS4m$Mau}G>vvu8}mi1J))7SfKO_RVEvXP>S$ce@UN`>~bZ|AhEbRP*a z_l1No2aA~iVqAmcp(KcHze_7A`?OTx%-Lqw)06BIM;tPwRW_3?4kj+%wfq(V@;SfT z*=s?OceA3GB4;vo9DR9W{dM1!F9jxUyew6Lp+U(Bg~}p|o|hq?qd&6nXErryt;K9O zEEZ6Fw!8=NTLA?H?Z?LtVqs^-!oYA}D6Uqm&7=>>EIZcXBAe{b4>)KJeJHq}{_SuT zWG8(@TV)6SqlA2(ukR-&UW0(fTW#0ns28Bvz3TyH=EoF(F2N~q?5CszrkNfh~a==abpFsE~%)N=UUp%`J#exh~Hmw!cefg zF3{w}Hc~Acr0K?gF}qXz{Z8zGW2$zeI(8zuQzZ>P#1RgT?j5TQIO%y_e0cz*d(Thn zFHiq*7TJKy`1$D(IM(qg%QH|kEjh3c^lyMbzQ}U0|2D4y>4j7K-_^5#yTiliU7>u` z`@`*(S((WywTg7F6x&SA#4$`w1r-BjC$0B4*G;7-#T8By4=6r^>Ml6@`fsh@Fd4Ep z&erhhsr`M?|5d?P?Ny7z6*@fI#+Wdc4QG1xmA1)@e!q<^?ZfRaBLd3b`GTI?y5+zd zb0Zkz)V_Oh!(KazuC+C%DC}}@!gxs3D+2w+CMrZ|^;%0nAM$n~Z1`@?&R{S`^L5D3 z?q9@8wVBI;f?EOHQ?U7)6Gn(hx%zXl_Gf&Vt%kky7dO0E{O5%bO*1PGrC-Y@^eVk> zdIt4K*;2<9YLfklZX~tHB@Y-1!v*D0bmsQo8M&F3oAJM%xNEMNoLrm`TGnysz7(p^ z(50r0{A=p%{bGi+$5X&lvA4WPKEIF{tRWDh{hE|HyoiVN4{`$hj}VczM>XSzOr2*rQCTXAE_irCYGcVL|Rfn03 z7E+Me>*xvSVsl?jNbgwzTm+5mZl&F85vUeW@wZ{=aUl)B#AI;`cf~k<%`UZ#j@bY< z!n@#%thd|WY;tD}VmPpZE2HD?lXQ>-8Dr`>vVT~WewY!@rIl#I3Nm=H} zAhyV^E{ndE9m#uz!T3jwc9lxG1iUbe;0Tp>MkX@-?=hfiX&0Jg$|&aHWMc0-b>=+=#$WnO2!Al=8Wi|eVg5EZ1MkgfLL&B0@x$V(Rl)upEw-2S{!ZiJjdH|t8k3^d&3cc4 zls>srS0L&W;Jzp9>@8qpLE|RZfw-B-qO*^+lnttttgZf|;XlOzu9L4=V&{cma zUuz?=ppP$lGN>ysY^0D7!-51-p+6+RB|81gS8=8ssE2pL(?vNPVT4Nbop=x{AKE+F zcOqCatbeyJ0B|Cb&|FRzncK{ZBtmWbd+4k6Pg@f&m!U8J!NP?{f)kX$w~UXIzKSa_ zUrz2I&}X+b$7Z+-NyJM3#>?NH=c&Nw89z3s+`AQQRnPPepgimT^1Z7&;qKJXsYQ4~ zpewjRi~g66%yd zV&I>_8@L=68Hf8xYDB|@72Dn<8JTX7#Md_*x2}HT$o}M+0@lw%eBBqo`ZriYJ_;?v z@E2l@4y6kdM*kwAYXK@NhW7l^n|H~KTePzSmK*5^)}kt*-!IMbPzc=|3h{1F`m@l# z4KWouiVDs8bG-S96YfiN@PgcOR-uy0O9oPtNB-=FZJ4gMe0CRj{wfeS%ia7`V0&8W zd0J_-o{BJSZEtUHZ6%D@@D{QIy@wtum&*eUWdL3w|8)M{v*T%@rMo!4dsn{s#%+*T zG(eJzSWm7t_PNq|n&8~x4w-{E>!;6YOT!#zG2_C|&Lr-S`Cn4mnTid_;SQ-M-J zI=a-6hG~$<+t64y3ScYMozAgZ9`CIxM?unkG`6&Gf;nFE2LvtGQFDa2C)>*P{7JlC zvfPTh3N{<+Hpc-zrycE>Rm!qTn`YxFN=@`B?ZX4R#c3P$K{JT#N%&LZk0iZl=BIdAFMLYGfzH7xn+ zh`dZjaBWdznwRj8DrTE6fnl%mYRkdinvp2iz`LiQ2w1=}zU027TVTAnhG(ilS-aD_R|vYRdqs=5YN&69(5cWq z!`V|ufa5ZY<99%3TbH6==?9%8xnUp%x3<#fF71SULtXk}jh6@@5~_9dChuvFti^{I zL@S#%jLwQ>_Dts2y4W&K2${r=c^^JTtK+J&R~RO%gZCB3M`!Isb{HvoW0*Rs22i62 z2n%B`ez+}-g{ET|uhK`r%{Ti0;}YB^E%Al*BIGtm)(w4}E8|VdZ4MU-Eiv93WYx;M zJ<*wW@lo9NL{-vd%^N244i-;TKh%hPYyDzB!6arWFC_A8Zx61FH0#w}QD9KX6L~t2 zArAmTg1k?c7p5f5D=bJ2Lz%W8nl7=t%OUyT@)iT2K;j+QHL4*ijT_jSX zR<10SG1AKmE;&G^Vhq2-omvhafyz@y>%pT4AZ_q~C!c#DMA55Fe@Sx{BEvRkKo|SS zs#8~3lk(F;ojOqLxV@W-SV6jgJLQw203GbtiKk%;Ca(E$?WrhWNO|shrLFy@h7&jK z-)3XBWIHM-C>CEfUT_i}I(bUR=l!)o42B$DGO>4HKllKmh)7{b$gx6aT<6j!$OA!i z8&Wt)leGm#D4+7yI?T<+)Fat#o|z%k95)7VqO2-8HN{NW+C#bizr2!uFD z2z+|24}+m!2b?4gHC~;%Q_)ed*P%h-(lN;;%VIaUQ-vt*{(o>le*lZZcVIjyA# zc_EUJb==g%VD)($M}JSs4s1Io>K3V#If3?`L1SKjqp=~CtkLlH;`BJMnGD2F|(`DOsVCsZYTipC@*IsjnNw5x+>Jr(&Qaq+%p@ArW zzG7mg!G1m6V_0cnOJ475n^C_?xg{CA(h{8+Uz=&@b9%7GN&nb851~^lzUlix#)Nam z!2t5&D;;m*8oRhaH)SiW4tCDItPh&>wT!A#?e7QPr4?A>=?6`jZW}k-o1Dk26m!^a zROwv>*4QbbY%t31!9mJ4JxRu?m_xGLcbxhtwLvZ2HbgqwmW3jPTQ z{+=*)x9_ZQ!d$87m87x)B6iyVWkRj_D<`zBSf4V+5#tlgw8%KUzg?SET6Z*Eta(GQ z+ircH(0JT9$-urHER;^qaZp|edr1;;!U$Fhp?BtmhXV8!8F= zylPT|$s>n*tM)pJJe+E8kFNs334FRkwRVv|L+GlCJT&w>iGGc0Hc`ZrBX}GNP31X$ zm_;4?)b@$SLt%7(d9P{5nUsU=hrVjVy}DKRT!M_LCfo6yKy4Vgcy9eXLOdRCL&7UAX~te~=B z^_ZLGus==oM!5}_Nq^cvNAkj5Ie(0EW(6%nZ=9S8i$)q*3mkK`SoEjAkh+fx3dG;U z4TogzDQp~CZf$p2W`1cYMf>3BcDXk>LS#j1v5Q#DOQ2~J4;l8WocKFFZb)>XNVG&c z-PBE3O1+uOYy_wc_(LVREPT1}xX2TRO=XL5@nh@z?`N)ev*uJ+GLy86syIv|s#N^# zubXn-@pjxwk0>y=4~L#``Ed`<5fu|z1R2^xaaRYQg7#(3&Y>WpW+am3c1 zUm~B*N150HPxRM)A7=`+fqReH`mo{M=I;G0fy&F1S5Ls@F-kOr8a-GLj0Ml4l?Ih? zKSdLB#iW1{Rb;4k@%TSN(3(DH%d;O(`DAE}WQW5V(Ip`nI7jq>lCYo=)tX6|4(*ss zFj!*bXJo*vW5C@`de8HG&t0pMw&kzVnw9HSfbJEeh^AJ=g|}^EaeT!UB8d;HU+HD1 zW>DWHg-}MPr>EC;-7W*@nvcgfawq2mTkf~z;#p9-zt+bkK2a1!DQho(LdjpYjmbs* zn19sAWIsKB5A}OPzSh(F`^>YWJ0Td66IdQL{%x?nwuaqrN+JWnM~Bjcv24oCS(gui z5yzL1gu~}^l?O8AU`65;`v;=gem%)pfkh};{=j&6F2H#Ra0d>#aRmh>ku}#O*i-6xEt6con2WBYe(ah@AawZBsTL=YA ztsRF!G+M3dTB{igE zEVUBc4I&M$-K{O9hu%|BQLVJsprFy}$0LdX7gCL2vYKaVgdlFTMt4cRVy|3%VbE>GT_|_^9o}{Fl_ZxUTPIX`8aLMGHyHiZs+a{D@@kXv? z<}T`wnJjlLzP8pEj+TuDu6c_dabX-oXMrz||K0D{p4anw9#1=fN0Ir%pTH#aG8RPB z!9(Hek>KtIaKr;{pTLjfN|w7{89sUooIKk$A`{D*=qFZY_sX%~x~7%pQ}RZ$L*~ta zPyeu5+$WlYI7iO)vVTAXxv55HarqLz(UyZtb|)#6?Zcf0=={^94lQ2Cf&&mfqo)Um z=3?=iJ^#xi>SvRO@yaiSc5UADblYItjmWYe(7U~6$?hl42y{?xmMMR|-J?4|E%-V! zDuDpsopG|UU~{(LD&r!8_T46#*2wZ-OG#dT&&Two{hb*61f3j(+aDTZ{F3RossWiq zUr{uT52KSDIUW8O{JU}cxF}$y2$$lWt{+Bayf{n0Bck5K7W+NhQ<~Mrv1CabQ+9O) zP5+Th$(|i96H=X)KTSrY@RDuAAaL@lb>|4RI+6})4JJYryzX_8u$NgSjBB9Xmv>=` z>NE6ti94Yj1*unE=;(VgP+xUnw9Ztj!mWpFaA}QX(owbliWp9UNBzLn6K;2z1Q`YG z{g~_3&i*nrmKEhEKe!_rvtqv(sJWgNDabrSJ3UAF9aWtz%JhrM zU!kX^Sn$Fa#&pdyOXXM!ezdcYk|h?(OXe6d`H8r*7Tw@!o?^o?ACbs9A$U$%quM*a zSD90hBe{f$dWXjKG~99^aH`E8gu7LG7h0fcoU(w2OwJs^OwBLQe6T9}{)yXoETQ7{ zz-oBq*&%{>?a!HT7*7%!XiJ!bwfdJdv+kh6=V8y={RxiT%tk$Z{4AkXxRAml1r*GU zdON^5PRLGlx9k#wKe3##o?Rau`CZ|2-c3fpd#U1#VwL`|{!XV*@{m%j-`uOew`OuL zGSdFAOY2d8HvJpa(spzG>*|Ysw(EgleOEB4PcHEn4*Mwepe2nrK}p@L@UNp$^O;zI@waxK5+Y%-n}e&#YI%Tg-T79}R3)Wkw3k z*TQ_AC}G&5G#m0k9-U*ZzM`VUHcNBWhDAA14#E!5v{*W(u@GHA6MTBwyjpYSDNOBf z#0&A_{6GQ87c*RR;I1h zXcG~ruGErc@2%Bz^rC~@_xOrkzQ0mw5s@fC z_4uQt&fgLF3AJb5Uz%b4CTDzzV;=Z=BZaPttR^phmu~Taj|(R}2(*RIKm|u*s)m5X zmgr2Vp%Cdlar=FG7PtA5t;ED;W*_wq)tJNB9kK%MyiP@&8mZ+ki$L$&eK6T_?X5g`AAGcfQanRZyn73UVLlkfMm28a`XT@0WM`?h{~I(ErhNmQhi6@7EvF zAryuZ5QzaKBnKoEq=p6wX$5JdL8L>Pp}UdJp+UMEq`O@B;H+X+TG=VJ-I`1wrocH_p1P~zlCFvU@?i|S6W6IM9G&Y%3a zdYmOUm>q}!vWfya-}YQQVUCtGay$(}P=9B>{gYus{Tyko+Yp2BV&n%mm3rEJ8=q*H z{bQ=00ry;WHEUsbieXOD-4{nEs!27`WdhA~X z@9$ck?s82%fQ-t*;^L%zlTN8)!|N$k0>Cdf-~)DWX9oplDBLgIg;O!4n0l1|R z{@j&A%+3PL{I1P^D^8KUPJ-lvguf(ag8UNQ*JSFeICR(#>)5Ho?l;u#IJV# zy8K@AXiwO?E_1`*(V|p|>lRa6kw_l!N=tXHeA=A}Qto3hTO=rJm`@!)=o{@S|M{JB zHW(1N9fbZfSZ| zWBD;{BqWC;cwVmeiD!yIUYt1{gWj6YN>tD|D5ZzIvSj@!baW#W>Nm3EEQI|j@&uPP zbs-UcXH~f>6A@B$o1^YemMS7*2Ok#h!{YwK2JRZMt+k;qr_w~Mo@uJW>c{Gz9wWU13n0gtFmXCj)zDW){s`lma zv4pG!sOM%gKTKWmQO+f|5W5LGCq+7s&O!JmnSDj2nqP{R$wlp0;n{n4W~V0@_jRxD zgFovR7m|%TQ@BZyD*z-81sUMYF zBvyIN^v8BssY%RhLjp!EMw(1>3@Gao&Soq*#)G9mklqC~nU|YK`ClR&@YzC}XPr4R z(eX9_zXCD#cH9*E&q+peR&XtMu6QEmh;!&+@tiu!pr%aHi-T(i;{;eenqo+#v!W|f zVi|iM1|B||7#q)|wR``Ob1tw^@TYZYmtLRTTg6o~+{3YaYn4QN>db?T^k!bYz^(4)7x zeMVzA+EGK)^o~IkWVul;yQ{7jvP2NhKoB*Jh_GSYNi{JlsW$bm`yn~X_f!%R>dg5m z@uuwlbVzCDOi3pc%P-owq$QI|*#(#-K~X#-dpg`yQ<=Vr zkKg`$;K3Pp%3=yg=d>4|APp{NFV;W$XU&*lL>I6fU+*9>n8EYb!o#~5+Z1`PTMHA* za_T6MK$puZMAv*Z?$B8FW)moD>uuKY8~;>|TPVqMxCzMmSsE!cU$Z zibGJO5OfIG>%1xMfo*Q3!>6_}yA~jt>E}>BKX%H5qtDk`oJ7_wtUN|h2)HO30}+<< zxY8d1UIWk)cXt4sz*}j*KK9~^=A*tLRo-p^YiF8RAf;M4S z`@-C4LLBO`pJ>z&B~McQX~#7l{DDGDP$W9Op4{xG0o-1s58_3#x)$vlMWb9!VZH=8 zA|esU;0YWPm8f-Vx(1=wu!!Q}M>(+5T*Pi?V5*4AR7pfEfy@S^|KYbqN68$`hy3J ztF+!(ehI(_f;Ns~HB^ARt}sSfb$WdQ)pP>|E^R6ozI!)e|2jCg4;M}s{mM{HQyxM` zA|f4Nj*>L^HwBH|hh(3_+Thl5;0G~89&vdHZXcjSAozQcmhTp*$C*FzGN{-(0!rZG z!J_4GAUf1f>D7x4VU@|}pzp%b-Cw5pd`GHT(Qs4fys~L7yh%lgeaguq!TFM%1L$;M z6pZ%o`;#4y0{AJ-as zV?J}XUhx*~=r^6~sIJubWZF`xXlHP6aN7Mp2B2yYC_6g@RR1~PQk8f;SZ&D)%Xyh} zaCx`4w-^5Lb{pW0?C;A3Itrh5>S8Kj(hLl58P^W3L2Me4>13r*Y?o>hVA^Pt;t;e4 zg-f-e58}fb`x9*BYfC=%e%|9YJ}O-UqP{srYa(m z`S=uT)DkK=d>ia&R)++4pmKAV^y)Z4uOqtF!a$JaQ`_*Uf-@xbRD2kVOvvOo-X-0a z2ThmLEKy{8y*)9X?b4#vu)T_}K$)%>rS7@X@(t(xby{O}-%^!jh#ue9CH>}zfcqJA zK@S-fleu(5PvtX1LsnJusg5-R*xsT*sde}uIDTJgcE;ERmo+gYhSvD_6(tN7tNj`c zb0KoZ0z9lv@5l<(!z?Srdn?<@{;>zdHd$+JaW2|vf%egWGWw?|EIjA*H%XMyY5h9J)e6fr zQK?i0E!o%P1h}tjy8CDHzjZY(v(4jsW2*SV)3V8;ln_$qMvyLFF zW<`F4qP#?>*cv9CwYeaC^WEe{vh9ny=cZl$VNT!`{fsQGN50Dtv^|HZ-dwP!sLW;g z89z^WJtW^zhcoOK3J4#nJK~bE?jatMVYaroxM-3lcr!cfad}z#Yw@DNVX|6<)*C;V zE;6Q8cEL20UQCn#o?@l7D^7sYnJ$N^O5D95O+3xS$(0j>x!MV(#zomhVuRqq)DX}f zej!q;c*=4<(<<_U1)36ti$#j!>4y>XI-pr${@`FFPl}iTknTC{2o1ESYutcUoWtFl zT|e2qm@$Wj*!RlfG`@`Grb#H-vTm+55BrtF9#;WZIvzV07n>Alt%fRqK=nyMK`g&^ znbuAQ6*mQpqE}Rj9nFY+~=2f&KpzP_eX4`uP(#HO($MPk$a*_-~!9={y#zQU*W z0E(P^6A~$d7KYSU5yGUSRL+imD0x~5rvha3k8U>34j$Mse<&oEuV|E2d$LuJr9p6o z$6zUNEC>jUT$>2p%>r1sDQ@|qGPAXf4WHo3v$N=N`*F-xkD8W-hIuPT$C?Iqpbas+v@sE~Kg&tGvr^Z`xQ=4)iQK^be3XSe8(C`CVDcP zDlI?!`jS`mf0nNXGLX~%339Wui5L`>Ag`WSiu<9+2Vep|XxikK9WH4Xp6%Wl%UW!| zZzr?8ut7P?c;}aTsJiH}y?x^LtX2;0W|z>`cDWh9*=lp| zIHVyQk<|>GABb?q4en3v_vnH-2uZ5&V1OHwDXMJoD8;F+uHt*-#NwPtuM=;09obDD!7EV99zZobK}HP zl)sO*f8mUI5uE;8V(U}}77nwW9C`bk3+-Q(VVGy3O)Eg;78~&#iKC6DgN4RYuGmM= zGqEW6ykb>`uhOxfCy|^bKqs4Nfc{UmG0%7YmrqR@qMN*8a|V|~c-o%??mqh1K^mW^ z(%DXLIEQL#NVbfcZY)8}KSY~C;_Wnjr#3&a4YCqimZrN|U-cp$cGp=}j`6}|^@i+c zV=8Sc%Rc#^bq$kkzWmknOo{AGWm?$9k!d;N%z&bbv7f9P<|N}OtJt#}(35QR!?q=e zaZ;DSBnn%dD>)r8!;rvRT^Ro0cA!9phs8fUa>L&&+}0MK(mX6RP!vFu=fFi&k*g$= zf##0)C_?dDa`};CmXHJ=xRNvmxBwhOg=`zpr!a^kjBKZszPF z9<#w~Kgk_26FiP%l44=U-p;}w%XE@pMq7v}MaoLgJR|SYzh;!RlD*f1V}j9PLU`|r z7$TzHL>DZ!a*71zeL@E%KBL&n4Pz?jkik|s!vLi|8XolUwTb4q)YU+jc{ZK@{^hPP zGZ2>==T=qi`xiXdF3D8VrdYb&=(R2DsI>6HPvalZ65yh5khwO zFHBoWITWYy20DV0gtHw>@jTtV-G_(Mtbbh;erq~Do~#TmtgNggHJHAj0aPbI>XjcC zSYAdIlR?E|%n?RSn#49J*BXSShLNIqPJ`#*cn8P-bVC|YXnY5av(&}QKiDw9v|Q+v zXhbHIzc_%*}Hmvp5AlG9g4*9hiusjRhui~+P{dj$t_AAeFK88R-`L8*RR=IWq zCmRH@J$rF63=lSeWo1u&$-MmY1hwSL{r&y>`$o%GpqYdBtYIfW|6$%Y;3H%W^t@^+ zbxLI*nMa}_(h7tjZ+G!U4W*d=Lyvwi21eaJbX7Qq5*;>*Xudpx=!P%xi%rs;IZ4{E zIrJ!Jz}0wr*M>99`UYz2VJA22zklT#I=TDi%j?2iQ)-m5d`b$?MXi_em%K);;-uVy zDh{g<8uZb{7T~ES69HnhFYsZbAB7QWHSmB?XcVOu%zHH$H2jPK&1HA;Y-_lRh+WCv z!=q%U#$jhxaJRSOxN)ObF=?lD;9K8&A)dI$gm;8B5pSvJ)r^${GKj;*IKD74@7>R6 z+!QSmj4F@Hwl-?MGd@dvvBDyYsb?Hs`5*xrseH+lvuO2F^!m`T#nr_{Yx-in2T(Gb zyx-$uEkc|yPJtH5|HzFU0eIisPQ*P8!RK}?8NlT*F0v(MWc<$D;&;}W&^>_RPQ<&Ff#4nojw(QjA0XXRYj~sWmUAsBx}c&Ruq6 z#&*l~T9R&IA5VyU>Dexp<0+D>u6pSlH|JW`cG#lXU-(hSI1Q~Z37Ob=@xLmZ*27?w zvzRqnSP=R%c&Oou7x?_3&l2BwsNQ;CRjr$q%#~stDzpvdX_RyJd}&0JCF>t5P8y~+ zR`V@}NvLYpc!@y&<+Ia-bMOgm-1mX9n4PpPh>>)$k<%IV>n|^Ex;NaK25u2Z6kYb) zu8aYoheG6Bd6z5UrHPXMn+}a!Y#2{}e#FHJP2aW5o6o7MRADyplDQ0y{AzDD=^weS zW!r|6R&CkoUu{!5czcJ2x9u?fkRuR0Jy|j)GbQ1~wV@u#XN&vUp81BK=bNRRUyOXj z@FzOM<8cj%`-mE+?FcZrbJCpb2}3l9Y)ck*!#&I>rV1m|lgmGbhW99>r@s zu@I%9_Sz6w7}w+Z@t!(Q+%y4sZQ_y^BNNlmQlDYbg@RrAv0^W(TbHw~6((dyo|!MN z0hg z`!W~B0&DaXpI|q~BkOu-6en)9fDw}1H}OmeN+FSNL9?#lexqI?-zK+)tK@H%M45n- zgu!{wE8#wjnTkG_N57>tOvbwJvsEu0xGf0*NoyW5oaZ6cptl{x0j;*L)GD4+)mA01DV`t&(ribL^NSA4_OR3uVQ*m~B}5{vDorQC zKApL{z9gBM$KGPMev*6Gb{ZoKK_>&F3xTKSa7KaXDCtn>@P#4yVxMbNn~=j)U%%>n z6$Hr5hIUgblRW^vz2%|i*ZuwYurm2A-9y9}KAN7)!a+cNF49UkA7$Z6O!b{SEASur zKgD2(?SuVHi|~C=u{fFCCA<@U z^mI7eb%KNu8CJ>Y-VdKJv3#{?e7CmPyP4BO(LLEvmgcB4k?^R|{pTI&C||uf_8WL6 z*o9m+ssc)LwQpA*gbog?Az8jB{vxOvHI^ZfiH;3GZkpfz&XT;|s^y^JQ@B@l1pruTKjyTjzeXwsw zA}!{&A^E_nrlGE`skwO+RI)e($45xv^ns2Mm?N^JfBuMq5C~W{kTLa?DxZQagQ$I& zzf!m3D@u$i;4>#s)Jp$){gW*Cdu#oX>mUaBvz{ZIj)=87iiv#CR#i0Ln@5nq0GET{ zJ8-EisBzhd({<+?(`QoR@nzql`Fjco2XRiQ<}_6el5_q!s4p?6Hr%8MEQ6tmBa@Kw z&k22-_FWI@?U_PdyZNS~g77uQeVNqcErt~ocnhO5j*e27GZe>p{#%&>Gqvd#(9~J1 zaeq_O@VRQiRAba&OoRG@j^zB7#f8zej!NAuBgg0Vn>*CXSD=cQp{_>F{U$sToBbgJ z&nhgQ@l&=9G@rQdDtsh}JezMs&2?s8`D(K)ym20@t}_a&{ZI8c+p4i_z}5ru0DU*; zcMm`zJ5VQ~pbprwCPJmf*9bvSzU_XWuE#Qb%T> zfn9<+-CT1sqZdweV(jfnbewlC)Ow*YQt*c(H$@y~T` zB*dF*0e4jFl~;DTp~<#)of=ne?Y{w{}#T6{t8*%!1+xYAn5{lE2-?-4@fS`O_<8 z{bC4?3m-&;n_#%Gn&6@leingz=28p~1%G;X<9ze2AHy*BR|Sns;Fp+N@yNb&T*~RR z+qc1>Fr2<3$3(RZI67gSdmjcsSPwbl($j=*K9B?Y6a%zP+Ze~r3HC!nk+#RDL7sAP z4^s$=I^Yf7D9OvDOWuF@JoWP(Iti&c#@?!iU}z6G9Y?fZoUv*at|sJYV}_zVu}Rub z#vTcxWNvz$f_}iDkB?7Tia?7L6CSC#8vOz)!?l3_nf#2IF|AIimC*P(L5j?vXzzWr z?dNumVHXEW--Vm2n5d(vq;lD4=D=7Q{5)+lrc49OgHHZRn=+$1p@=;eHV2^!yeD^nKg^jgu5gw2QAzOWxF3~xWf#wh}z znR%ziHZEhWe|p~)oPSGFaq<63M3IPHy!&Op!km>anS#|VH~1YsBrO1kO)e#N#=EX% zwYa5gSrgQ4{!$vp{4S0MXRqfAiWuUx+S|JKuC^tSt?eV-p~hAKrIe>y>1T7??@i5V z30pbv`wvQ}G&4*F##2%le07*0Y-%X@naDa5(_qw+KzpT8(YMbmW}~>DsMf1j8=ptj zebFyniAgkcZVnm-bJ4>LIxUct@*LAdWVN$b^aTSJPW$t`l$e+z!one{Iew-sZnr?Y z)!ER0NMS(z155AL>yt&$p%Y0Dh^^_7);BMklZ|=I1OwvnJn&d3o*#5+vQn>FwcQ4^ z%ggm~8hzsA@-pr`J%INVGSQi9s%Bqhl~f%jpU&*dtf^g$L_e$H%tPd%z(pSr&$K%I4AcG_A16o}yL0h%| zJPlEfqN7L<&n`*IU9Fcb@S8t4`}MGRv~)ey(s({53(VN6s=+qk)zh|)!Oa*?MJV*U zFom!%0)bATu=yN<<_BD26WJ)ePA*FFB3wH#}OX{4?g4Chd#OIJ#Y=v>nIa$w$|gBGYrYfYv5r)mkm#=f|y=a08_TlWAV- zhut(G_fvk=IaP}!V2M4r35QqDKO6!{p~L3;(>?Z<>-u@X!GpF^ZIz_4vstFaRMnC% z$qqkiCg=P*nXy6A-OHq|stX_xz@B`VW$Wow7$s1zs@Xomx3Z0S5#OsOOh+c2`AS%yK_x>3sv)x4<0S^_U_BH zO8c}|6v>?fTDIPZ-FhqFCux#rdM*~PER*TLDVNMSh_nK%iwaYD)SZIB;pKN~&VGy|-E^$qTJ60FYs?JoBsV<{;{=bPs(YF+1+&*Gj39getT z7W39?%v@JI{n#E@aQa6D5(cW6Wf&p}Y4gf4aJymGXsp_?|H50|R9>gx^6#|+I5V>i^{YZ; z|K|nBQAk`JZ03%iV-KJG?Is}cCV3ND=bnr!MCos_1D1sio!zo}Y*aHQ2WKt&+=~lV z^TiuD^?F;)mTFA24j!*a8~m#_R(5KoB)C(o_+C&S9*6|Sy*^Hc=1zqItS-%nB2GT5 zKk9DOrSO$*6vb-WRMI0LoY5eC~Pyj#PwgP}T!q#VIs2G%ygWP>rOzakT#sh8-T zF!i}y3lPkFCWG`OVZe&H*{Nq2( zhx8|sbA3tgq^CFC(^xhU(OWE^HJtZ<^(xTJ6aAU+R6Cvco+aY<+00uKabdvJF)O`* z8+xh1)$&Qwgz(}7t6JhPWQ0VI=&G#H$nZNiwXMgLjqOYM4+C+=JVuW-pF&%&x z>EzeZJNXEGcgFG}546iMXvf<-wYS|w3MrIa{Ml@F{)9EfYg6|RSN0#CU-Z_~(o{}S z60u)?D!?H0-^o*{M)zd;o=}>|#dc0Aq-fh%gD+jW8@8$d4AWFV!^nSI!sLeeNn{{# z`8ktmtTKf^iekN9pOebliZzBB(jZpcX(MnKe?)Z!L z0aV;b{rNwIlexv}sul=ld~AGQuV}g*INZ8UWdYBfEVU%rSU^41*Fd%9dVBQS(~4x8$LS5wSl2G<*wB>y5d>AC@dm#T zrO>k0)7pA3jBR5`v_J zJB5WwBRu)BeCXiWASuxzqew!zuQ|z42x0D=l^_JDavMX9>4LTr`Uc`f=S|KAhvSQ& zd|*!GC;lOICj-qy^Nno4_k{EL?yjw^?Y8;Ee{(xOzF)oB*+A+%9DaMWv9qJ?_2Q$> z%zjw>*iAZ>VY(ZH)Y}FQRI8f3JK#(^bo>G;+H;w-!WWYOq5z(TmZ6o8mEHXH?nKc2T8Jsmf6E}B(oS?*pKPyPY!AM{#a!FuRWGtE1kw(Rln zZ}!Y&SP5TvxBeqPe}TRnj1HvRhTvF4D}4W(ki%}x)-m6dTy;6x0-MRuI4H&UmZ?8Z@3On_S2HQs5R8_y_4HIDAEoZ)z=hE{yv%qn|F>gA zOdtNw4EdAR8yDAms2ERGUMZo@QeCT}`e=M{@zBaOURe~JA*0R94J6j1W2C6NgLXGp zeQ+lGIyg8Opg~IJ8Qe6!%qo`iY8f8qYrzbA<3Mk~bF6Pps~E-a+Xdu^gTp9K`kVXY zyqC4PiTA4#1*clyzE7A>C_o7Ut$7!#o_(J4l(Y0Uow-rSdEjUjNNM&G)C4M~gUTMK zPg$R$(}Dw-#rz~=rk|E@lmyJpd=ANGbo#rxHt>F3uOrO>v95T2{!7#I>Z3dEI7Tdl zlVi=@^}0fu+hf<{q^b;#)q4;%Rup&`n(??2o;NojMNF?)`OVUS2NM2ekIRSIK%hgx zpIgXp!20a>H9`Y$0`3dobto$@aS}XzNE-mgraPuDY}jvDIAa|{3_U#OQYe!SJH)r( zQEv%$3*yRawkDNhTKit}B}Mf=e(*2<(Su6y>fW{NfODtkg0!FdnaQu0ckF?&fi_bT zaSP%?^{bsvfB0tiWo;JLOjh-V-m_!S-BnI#2P^PQamvLBB(_;E$IKkBH6mvA#b(in zQr08YP%-VIRu`bUQT$~ z*U3fibYc`s2P@P`-ih@cY}m}w`f2vv4%9QRve5MXt4~p3It^y!y3PR?#S6yc$05<; z@EO$#D-a?kUio+2Xv^`HE$DrynTCIxE)Y>W%IV^_LtR07GI*@C{&Dp=cxMS;0{qJT z>q5~+Ud_jEynrZK8&IiVLEEp=QL$MK+hG@Jcb{}tS0Xh+j_kT8VLokMuZiNy#IVP$8|l{% zyt2B!iTmHvXqWJniq4T8G__TDo>N{CJKUEc01rC?PIv?pAwi+MC}zWpZr~w-ywmLY z3FCX}e6%MOGah2Y{8x~RwB~UqfpR}BS9-0E{NgdI_%LK7ha;IPM>Y17+kvOl2A}Y! zDxcx0tVuiHL*VDz;%8&Wy8YO}-}d4quJ&1jmyF+|OGg-c8!!$&NKc^sR?Q7CdNkbE z6ZSSRi0l~hwPv@nG&*vz`ZI0$b*!k3QxIU)kUl>*oe=$Se&K+j;tU04(`obK3fiHL zv=2BTEL|v2X-2%7E|TSVWM7=Pn7Zf^bdW9fUseZkI*_lA&)*1UnkLA3Hv(~>_(3z`tmJB5CUasYQE;89g zj3~kD1107mA#E!Mt|>l5NsUsvpPcrkAx$QnNR(LA>EXiqKjXuHi>i&AF682beX$7d z8^9b4C7=dX9ozza%|MRQTF}*_sbL=V>Rq+_T96W*4FPM^$d#a?b`KKiKR!NQJKys3 zcn5Is*;`I-3jrE5lCj&#*2CHU#AeZ|wna}mf*Qq6@v|fc`CQx7-GCg?3(eaidcY*W zDjm$m0S=uf;#gwh?e8`*PWK{1CyNeww`yzHX(8M5k&8hOF3qhy#t3Z}6>TlE;x);8 zNz*wv+qZmwY&|vjM)#rtDCoMK4Mh~OPib|k!Rrq`!OIBXnx*sAp^F|Zi&j@aU;C_8 zfF9|b@oUk^E7!>D{aHJ>+tzPKQSg79Xm++C{i5z;MR=@Tv5%Xp>)rK<>u{=j(`-?a zYJ*OVy|!uO;zRTDfrI1lQSPPpuD0F1KhhzP0Q8EwoIa+>!s8=VGE{0uf zb~y9jT+`ipz{m&G8D#y_au9x7oj^|=q^fGVYL~FvbsU4MJEy5S+1dxodPui3NVm%T zg3D)1X*B*Dd(?;0IOMJw&-3q$?O$%=RbWBRduy?bx!Sn;w+t-F1T|>vMa=HS}l4)u$fe zyB|A1H;*7AXnbBh6!_vITNDgr?>9ti3d8{hBGPPVco;w(99_9T0l4xD_ZF>UeBcKT z>dj97x4HRWT@Mhu;Ed`K_D4sfHedf03k_Oj*kIW6xl4rnp$RKCaMn>nU3dx>BnHVcDfLKg7OZ4ktg!O<)HSjU;V|@Ii?=V z(-LR8CfUY>W@P()7R(5QcK0i9WO<#Sz9V2JkX>^L$O?-wwefg={qo}_y zkQvOo$)Xw+l_*pbAGjWasK)6wvMY|m;6$eMjt)`hP&XZ<^$CXy z6Olncy9AtU`unp7MJtPN!5Hz9tf0tP^&V=+QizP77PiSs0Zd= zEP2_Y!E(Mj0}Bn;z6cb7K|SJ7)6~?o=r~Vm0CsY4sL-kzL{1g*CkD^~UJ&UV^#uA0 zQz@1Z!dKC`%a}-(y|OTQaS`8IG-IizV~yNs2-t8X2H}YxFoB1bSE5d=qN=K0v$eHV z+IYVU(BRIuNBaTiR`TGR6}>_|DN~k{epG52-xxZq)KsDCp|qy}0Q~)QdGlMbcK(91 z8`<8;=-t5)^fo-FRN6qp9sy(!A0Li>{j#?wUVjft`JLJfBogZv7hcpb^|iWND4~y2 z!$ZI{=t)SUU~rog9f~iA4DO^$U5NVebp%SHoC-)(uT>bDnLufnUm$*|gHX#G3o+9t z;hXR!=lAv1-~*xJ$co}J=D$-B1JkIq&ZyYqDuJFl(w@XI$^$3amv1LMTDE8=%j?~2 z9xul8piK66f_can_7?hJ$ZmCzqphQzU38SNOu;g3Fb5t;EFRysW>555e!k?_{W?|2 zv}H)SLP5!Hr#o1r&mB0?fj0J|+r`~%&C*?Ff7Q>^<;b$#%f-u^%R+|Xk5Bu-4auy+ z!mAI}=IRILrHz*>Yx)f>fRCip0An~p8H$wS+BNoz0%3q7MLm-)8$7lhK_Frp%uG+I zR)bLIOj5Cdb?O0K(;Mht%k?xyYG@!K4z}+osrhqNQ9AKpz5{7E6f8`mf}%K<57!T2 z%9*(6Xr4a+oPjyq)cVA4;dJ1X+yrl21Rot_m2^GgpM$HTPFk9FO6jyYWXGQ-Clx$b zU3L1jH<(*uKA0lUghkg*YVfsG&2oOq9^M&)nHm&!bfsN$bla5lYv$;7TBk{XpI^bJ z@qCSzXfvbKVAB3|89JR;Xm8(CA&aC=Js{$+}`0zTDzmxJbW|Kx`MC{3h3Gl#NX47Vc zyBSi8D9eg+dRpugs;(mD8!k(k@CoPTxJcg`FO$;c?63S$S|!8F_1>rK+c0?w+KC<$ z*C8Schbp|@`(+!WOj$`OkaAV#-aScn35$QV<&uS-1$fFEeQD-*8NUthhyD6HNMInI zP-?&>5EFfoA1A8N{#DcK%MdF;Cj0}7bhP})Q{pPh?*p`+CPgVD|0fFv%!B|2+LUJ= z?cXRo{!i(`EYlAf4{55xD zjH%0w-IqVmZD#JcW4C4LjNH~DqqXt3Iui$$yW3+(BOYC?_Z!_f?LZ+k*-W%DDM`vO z=&n5DCWw*BjDasFFdB>qVVn9V^})*aa+;8BJapEe!tQ#qmb?{Dw;hL2uUn{UDNPNE zDIpBvz6)h!VX41lnE3ETS>`=&LQmd(xl-X!07A`P{k(JQPvI8*BDv4!p8~O{poA{Y zCa%&CR~$D4YCko;-e71yZYxh{NTt-p^g;VoQYTfOpB4Qmx3?maX2Kvf$kh@NHJbff z$@U`X%s%HKxh`^(|Bn2xqDBeJKQm4^R5X;yk8eu*XR~B8r?4p8ly6(PlqTnRPfl1s zg-nwx)Y7E`#5Xe6wfy|)mZo-Z3Wd6jPh^mT5E8l%)X9j|U(p(jw5(Fq=}uldKwIVC0X)p0{*#bse|=Evy9? z!d0`Lp!!kV5HVuBoC#Veh*L}^3KJ!rxdi#%7f4MRge3WpoqN3RrsFslYoSushtL7X zJRNHBfjr&1wtOIZBof&c9UcAhzXhpv<0C)?(f(MgRXUv%g+K^0NClF0cf2#;x7oOe ze1$~b0yu!7H21A0zy)G`WW_gfHoxfN4&wkvEV*IG!6~rzG>VftL#BtV(>~tqE&_nq z_0Z|*J?pAMHIBxU?pMI3?c1V<7b+)u(g2WLVwDZjgVnHLMYaCR5)&o>5f~PGJO{N) zK_ZR4dCi`aDdr+MQSbv3tA6;)q+m*pFnt|X&!Pj!m$5PabiTwKsEXi7vP&^4`cBgS zJacKOsZ$5HK)$%{ug;oEmy@gGGf+9SGr#9=;d|(s*&>hp1@KNV7DkWr^2;#5bbGva zp0)?*Q;|9Y7io{<%hi_h**R~RL8ErF063yAfa;MG9hat4s!-ML#}8mhuneFV?@lDs z+%9LgTFI%lJe?}$?q_S3?;pQCbd7Hf11BUZH3tDoBuRm=aW|2T^5cHc#3u;)$VzZx zoawyO+f_C0*U!|V*QaZ8+aP!W`nn!O{lUe?3<=U473grkHnZE-Y(K!e4k+r2C9>-| zCd`e^M%vRTh{R_r4<5qlkxOuv&))vaUth1}O!Cd?1Ljkt9on;M3=Kl>S|eqC{C zSDXfD4mi2)+;!a`X*X_eNJ~gCILwbUwHD0C7FqGRf3MT38>4Kp#oMp>y}iRdv%Ku; znKRi}siv!0U9~+7sQ%d0NEMwVes9>z66tF>YCi-_TU2D^}r&QnYQPMmv-xchrhMHs-X-<9d?{-Zs5_9FOq^8er z7V)meg>5HoP>snYFUUl*}$i(kx56Tc~yp7hiVhbH1l`=*^6j6%AOw8c$GQX1;9nD=*Zlu<)b#c^_+atR;}+`W>rJs#MgZe85q zXR4dHrpMn6)_KKBx1KDTQ{NUf(Lzz>P`)adVxpjXV`TZ+HCROpwaVwKC?`w6Tk9@I z(|@QDPoJGw)tJv=f<(dd2bVs0%TLRXd%xW8uQ}nhjqjH&)QEh{Ouh7`bCMg90YjL9 zfMCOS01$vpvwt-oqa20K_aQhb3IyiJ^-4tHoX7~`|Y;F6w22rY0W0BsbxB-1vqG>sZrq*?v>?9l~*q?6g?xl zS@qRLus}#8fi#N`B2`&|(W;<3X*U=a%O(nj;%3tOylXC$RTD*jzUb|Vuu2A@!pt*m z4L37nJyo>>E?gcqlb0VGTK3@oemgGJH8p9MPP@nN)?=(p;5C6%DSC)hv1u%H-1I7S z1YKPguQEBOQlFv7`0SH8I)1aTb`8veMx{Mhp>KKW_y>89oC&bV50F3kkNN7>;K&JIGLqKnEb+3&}~z)~`V zU2E`$#y5z2=`W0oW>Jb%o6t8%R2)Ta>hf|+_aHulwzs_I|PI&T@c?qF4Hb1!*t;T z^l7hLT*!l0S8>==EQU%-D0v2VFM)SM!`-3dW2PzdqT3nIOriSb!k-uytdv8EE$o?= zoX5d`y){=^a$KPF?osc(rs1^irCyj<#k^9C)>7(zfFibXp&3 zJybV;{WU@+ykrTK@P9tcdZztnqc-HoP-&9l6>rBd6jkt4Z>VEsd%F%CEO0ItrW>dM z^8>jJ({C2rc6U%#EB}Qpy=`Hk(WDD{YBm&K);+NgFEQ9p8l_*)w15jK@?43B7_}Iu zP@k#5h6#c*9r09hQz}JdCt?NTL+X1kb&8ED8COrvSgA^RWYObQeQWk$qRmz35N_EYy}*#^~KkJ58d# z@?RhK;S$1w%4}ErmC}(Q&|FfRg}lFVc`H2@DuG1uG?DImYI#E*M;2Vg?(T8Mu*iig zyIHDXNKmhne}eI+MMSNB5+8kS2w0ETK$z&5iHN*`Lj|Cj$}Bd{ZJBdm+fgI{C8b*qb6w^s?mn?$baLnF`q|Z1(3r&%i6=so66SUp9?CF0onc zXv{sbO<4`^cerECzf@u(m1D-!GFJVi6$o$5O~*gxS;eYH)g-v zp88M1hwz<4TpQh9_vwQME7B+#JsupNW&D0>X^rj{%JMvB))30e*hEOX#&@$m zE2Z>j?C9K{!NDJN5AFOR;qEqa`zB6KjymlWFBUL4OlT8AL;$WNlECiZ?Eky~`VrdG z?#HWlPk^Di;~CYjGOc1tfm)4Xa^b@OJt;xZPe7zz&Wiyej)L0Xr!DJwsI}YKe+oa~ zmalG+#sWsk%AfUnwR>_A^H zbW&TAnwO#!{i^(Luspx38IbyuCe@pX=Ep2z=#uhzayTwOUe>4=5<{AR(u4T}l7UcT z6jG!xB1(BTz&(9;*Q`*@Z$ zy5;63I=tZka9c7KiGn%dqWZeV#p^pqWBgJPl<&xSnZF2*ef%5-o6H|u*Ofq^<<)hs zOcV}Wxru^943+$u#yF9AQW-Bs=P45_2H&|9#!6Un{r$l#2DaC}n!xMu1iruN=ro@K zJRzX@?7yp9rEn67WInM+`+6LS!2GzcXlkFqb}_&rhzL>53n)>~2|+OR1+zDvZ+FE{ z?*SrLi___3G=l#4Nja@vH0E-WW=G*t(2;qjsPghvgKyVoNlXmld`%+AfXXqb6z!7eTqhdt(jgFI~ zZ<>rLTyUEN;NezVrbfPs`(7NbeY%HbGJf(f|17Iv`Sjv>(X(MZBca)b*@?fHMo(9Y z&%!vI-=I=Hbql3#6cyqoYWyRgDQ$F0XZaF6{4!^@Q_x+O$mr;nr}^y1w!xZd}t_gUrT|D)+FyrOEuEj|nl0}M)|#DIi!hcrVeAtBu$ASoc- zJv0bN3(`urbeD8@cX!u4-@WUeKVW7p7W1Cx-Ot|p_gSN74cnDA$HGQlV`lr_Zvcww zs4qO#oJKX^RB;(I+T_W%-t^*+i71e}EVmcH9^QKsLb-oJK_X<8uzcVBv-zptWI_s0R;@fNwIw0j#X}|?z<#Y}%w_Y$&nsWPHUn$;Z)Eu~=)MNK?R!Kxrd1zE{ zSp7-5=euL4@TO@K2BpWe2;1A-h@Og2W~z&&{2bi1BHAIawX20FSl|tk;p(dC@IkRX zQ8Q)BDI?4Tq=7XJ+gH8UB0zeSprahX-W7%<_k~BJB)A`d=$U0nDfareXHHlntB3s) zcHH~Ntb{`l*mQfE{L6ja5`H9jI?^_2`#=oM{3Ev#Q65he#UM(pq%~fSC#Ko+521*H z@-o!PNLDd3@?p}mN2YKx#+R1&Jm-k_67gB9_kHjge}$fz<#lm8XZ-E2mKu8=?i$NU zGTXM9Q5G(h9W;4Uct%l33cE3+^sYSt$vmnvH0WvcjH(lMU~gu1v)XSv?c-}@aW%Br z*?%uxcUA7h75vB_@?z|dblrZiKZ}yLzvay=+_YpjH-lw+CtKS`FungVlzAyha?HY6 z)r<+R1pO zVi{o*;U=X0ux|f(Beh$2M3D76Qj`U{68$wXas-c#=Mjng6*}l5ux}>8(hVhPqOYJ$z9bCY3{d9=y+yV8hQ z%@&p3@7v46Nw6`?<%=>KfBo_rx21kdj=5__7pV62!}9efLgQSSgP>$;u~tOHB2XsI zmi^@#vhM#{0wvJ5S60GA*bNB^foh7Ogi3tkF%4m?3KXqGbZP>`19D(>aUzP#Y}fMW zT$I{;j8FXn_HTq9Rv=+&L;(;c54&zRffW6xrn+X}sq^yo>Ss(VQgohIABP=RTNj({md}u=nacf-Vd1c%n=%|tOeVW!1>`mAU zI0A*-A`R!;C(6lW(pB3UO6K;lcHxT5n~!X7$M75*7J%m!blKziv340KA~}}f%sxfol=Wv|b-Ds4~&G^Yc)wIH7eP+QhDzhcMiC^Q0y;42rek@ucW9Avzws71r)mA^ z_7&?qPDwcCj?uXoo0?~-a>?fMjjec4g7}w{dt}?45|My%38?u`2`MiMbQ(>T7NJV8 zlj_f@Ai!MxX@#`=!18$Mo$7;{nC$8ymo6lYu4Bz4yG#ZvJcB!Xkp zkd?)#YLrQBmm7u|whQM<(JYGSpr=k+C687cZ5en2RpE%DzF1~fUZEoY)fvH!%!?0y zw)eH!Ks0qHkpy$ySjTACAaDJx*Qkf3q=X>vVM5SH)B?M@?*>Yr?FE8_At>Tv>xMEq zEqmhoar9<`mCL36__6n~XhGsA7e|zMV!|g9#i6no+1YETc=z=b=Rpvnjtf*%&08yw ziGxf!_Do0R3eO1nx{!X4vn&4i1!sF4CCs`l!kJ`e7eHqBMSM%|4m>d1%Cbd)z#hIeR~LdSonHjcXzj~ zty}h5^#+TxrtiOAaV0a(0_G@0^2?}_Xhj4^W2p) zAP04`%#i`W@}1j5A5v?#jD~gGa=qFl+L-ZzPoZ+LMuF?N{%AFyc%VgxnFt*noy}|a zoSt4rZ(7*(glbzTb&x2$%d^>H{^~S4A5T=k$Me+Gv2H4`y)YnPKR>?|c!~MkL_cr$ zv-zy97}hP83=a=Cu=-_|+$C#?y}nb~ z38^kCDHTNKc}exP0GYk%+&1aM$cXLOpM3eE#=W*dvr3>?@S=b8=SsH&9-i;Aw)4F9H3v%{~I>qT;@ zt!{(cw{PE7yuaC>mOkIzhv2a_KUHg#rY|N9C#DW^^-G$PKjk?$Zns-CYNsX-xIaoy z;s`WrEuRto8`p?8;>cXC;~XirO47|R5pdI#E2!dzVew}uQ`BWWG!QZHc4L227&`L4 zK_$b6_sw|ew%09xnh04A2EpbJokcQ$46nh%#%dQEP(>D!A(Aq@?f#wyYM`sT(Bkvl zHN2QUnDy3F!5%<9zl$j{D1o_0JYwBx7kgPFPabGdjc6p6e*q7fLbAzCnD&mpEY#8k zFn&>{r^pRh$(E^puI-i<&(n_2#gn|s_X(Hzm_VG%x6hK0l$XkUSPKh1XQxe8v%3NnsBeQ#BE7?k(jj9_R-!fs+< z1^@5y?@@lO?gBYgdP1xNzx52`2&dw#_tZQ*4tr?f6xh916eWc#{1PLH3;Cu(JlGC5 z$EXidR+lZjdtkr2k+0q@u0{J~+A{$s{s@hc66r4-ng-Z;Glm9E&no*&-{@&Ab^UY} z6rZ$(CaZP^tr#q8rW*?|86fz z7HDmJwUn_$eOjlOup}ne7sF5I5fZ$N{GR;O-h+XJY*C zqXkd1!>d=*^BM=tSd_!Pn$lOKkwVDiu75<)Wri8@Ynoiuh<7+$EsJMN}2cUKThM> z#Ai~x-`uku^Y_5UyA z&O&u`H33zwG{GDFQat>Ot*_bLxZQAmrXse<*IHC^YbjKv-c?(vRDU_WbItla|;NlaiMKKQTaYM+b&y{fN_*n~%dc=0_@f zbIu*#5t(1i5F{q1a=&eoNi_{I(A2s=O&5;^Ooj#!uwNOqCdaX3L5(w^sZa@w35G;u zN#Lm3?&;~_E7fbUU+=!$pH@;-R4?r4GR9|w87qEQdBMGgFSQo0=7kOWh+<|Y4#6OJ zs2W|C{pauH?i5>OfsY^m3Sm)x1297G=J}p?YY**I8&KO>uM^dS7(huckU@U%h;TO= z+5N+<7cTdttC-u(Ft#sa(7N#Ighe z;hR>u_dDm*7o5}b-psl7>kAaIsrmaC=0wsKK(?92t40e z`CRZoZbC#K{KQjcnS-_VZXV)#fSu_{UpBn}Hi&1?Q@~)M+3m6PX^*Ytk-oI$;&zyY zh-=7ZD~B&96r+r)a6Ay61n%a4*#~QHrILrhvSvQ=WE=az34A#@13=+ap>uBFby?O= zQB7zYqq<$bT721RR}WD!BL5=DhQt4ux_q1PodyC&+t_eZkqoOQXuCrlzley4z&l}n z{=fF?TgK%{!$0;SY9<6yoeO8%k_GyY{_!ZNgdqk<-SQc@b}oLn4C%tD#KZ`2q1*jk z4$TM-=?D;e#<1#pKnxkZKlZwLx~Cl4?jQC#a?H=q7ot66QKO!FjSZ`g`je11onM_& z!+SmG*wv7cwPy@M5nGp6$@M{E8ZV}&LHO>VwT>_$Faj>L^6rr!EO>@gSc3MZzt z73hYP4_&U06~8R7x_b%CSr4j0tj1rqyBlq-(#eT~(gVyZXUXjv%NK2Zo+=Hz&-&<7 z8bsg#uX+?<;yrGbdhXtCXV8bu`EV?{yJn_L>?xDfd9mD251;aQ7xjQ4B>mO1NdJ*>rKWMJWTBb`|en0{3)1B`{O4jO%up$ny3rz zeKBP{oXIrvXXjCl7=}3z@VFGK|YLrsuA6)x}YTu4-2}?6{{Jl`*gnjI5aqzcH%2JN1DlB%*H< znPRqd%6{qjVMWJfl^nZJx=exy_BT=ly>sHmG$8rV@_J*vrmZ;babuoOgou>**t-uL zOI{%PsY~O1wl8U&9kjGO9`gR$+$!gu)CMXFSaFdAkp^uuPSJ4z>F^XI%(oOtuju4s z@z8>fFSluvN2YS-?1a-8%kY&ev$sxNo?XZcc~z~lW zg(b(K4sDO^lXTzlHWL;l%T)Z375-_YtP?~W_HW=X$VMXHX|!YA78)m1hSw7(zhiYt zA&>MMhoT!_yYN$E{bLQq_v*}`PQJF&;9&=;!gItZRIR8@nB5{bRHF+>8F`yuE4L!tkNtbpqU#_ zk)H8UmI-z?)fXUgg()Z$)JVuvLh9g+JgUFtDv44T+j06SWtlJNJ_Kw4J>uNTC0QQFU}U z{&%QUqy&y%l7LOEyO|%N`68F?ans<5(&us7$Lsm&5s3P{P!`hX859bnqq3eVrkKxQ z*-AA}?Z2!9+9;oPPBywDQ~go^zK8i?&CA#Uf$a(I!OeAZpXY0z=Z#$fDuWhpXW(cLVs4L-b>x z|Dg7{o#z5O!e0bj1e=n(FT#4`6UH^h|KN3V8LyhgpK|X`)+eBw7q|a3Pbz3-Fy6w2 z5sg;U!M!VtDALo)>^Y8)9IEyPiik=4D_I^M`=tzi8inOO9FN>CIBi}p8g{dOuYv--)hRm;6_ZrC4S4{IVf4M5Ui9iy5$ZUe z&2oThtUX;>myin82%c6RFBn^bxKUYk#6`{Ucgg4HZj#9@B>H#HL=Yud*@_%VWmw{x z|3Wo%QtuvIoiow3cgFss8BFh;0Ob5w3=d46vcBSUH@Dw6A7mpUvobSFcP86ue?(-2 z>$}O`X)pwmf!>&)k=nYkD)^^4mn{0fG}J`(5J8HrrT!j!`2(-VBxr@*oK*e9Nww?3 zl*DAVE)KFq26@&XA5GU24I(T=!&@3L>pRrWDfk?tIgl=(kfSn6^Zip>4zGTxL(@zh z(vdfdd$}|QJ(&1GEaAqY-*iW=rlaP(KVnt9L(&fmUZoPGPRUn+-EkpGxS(^PlW5=R zzxsmr3bH&V)J@vLAWv3(Mtwqj>#SoIh1#qoKtBUp?#6IHO{8-7rZ!!}c!1{I4F=$$r`QW?M zK}}y%G@8Uk#GWn49LJL#M9+**}ft_xN*bAZ$mx zi3-1+9iqe&^>{2JpH^fMq~j=({`#kLz1}DLDMv`N(E3b38*juyWk0|HKUV7AgzPI4 zj7p3EIf&P3j}i_Ker58ola2noZuh)3Si3YJkdc-?K#cPN8|t8_R}0kk%?KhuodaSF zy%4QqmFb3b5UAptyS*|R6rM#oWb)E+h_nK6Hvx@7!0^R`yNvCDo4SL8GjK)&XZ&)# z)BeQ<^AK^jv%q!^rd>R)Kcdr_C_zk&p_^jY_*AwHk27>#%=@^PS5}&^t~efrfIdTUTq4`@4*buixq>X-N6Qc z6kO-AKXf>c9==)c^&-&EFDBL3kD)RLz`$WjVBm*pl7xx{IucQZ`65EO@INO23-vEx77PKdcMS_fj3wSK+O#evw1(i`^lEbXQ#K_ ziQX>>`rGJvbrKiuJT8IO+o;V9lny%rs#VkRW5coM^}A_1J@gHQTBAU*swG?A-}4T} zJHo|p>+^`=aWo%jI?BUyuxY9LeEs0k_>v3D+kK4r-picyaJYp;nH&d|KLTajbYs9@ zj6>}B%iLnZ?>~Vsj>;p;7!Z{-);SPtc0${8FgB#pWK9eS3F+TF+t}EctG3#s*JDjo z;6h(13e9O4+7j|xI@odgQFdYfn8FPpP7iksEYKDDLbGGCMy zYhY`cxnzhb>-}DN4fBp-oM49S1zCHqD?eVL9hTt7|2(Vye=opvP#H=!Pe>%grxBQh zpLH^a8VVS`i(Vv;0z<(7us$y9FJL(INKL5g`F-v`q+IBRnrDg*YzGMD_44hvXh@0B zylLDEnjN*~meXzV+r)f(9~U~rKoT~4#_^M@ZG6DSc02^n=Omy7zi)mJkf@y9>Yg4c zkGyUR4YZiI1k92CwR3da|D#&8F~i3&`gR)LBPJ?|D`w`hTh`Vx+=9H`lc&~#7FQmt zVcmMeIDZ~W-nApI?kDCvnyvUsnpOD~4WW-`FK+g0qs_zf}q9@e$hloVY;?*FI3yOnucUY^Z*a_N)_H+G_X&1{>8We0Exyr3DqlTd+n08Tf7R5c5dWp*=*! z+}Toa6y#nZu7s_G`YnY>T4M(TS4Jz`*)s2KRbZkOgZL8O#$e>LGo1LdWfER*hD{ zKSe_F?}+7-aH#pQn&XRt;gLuHE?aUPLU^gGABAkaW}Q<8nT~jfp+LVdjMD@r8-2Wl zfep9H-O!;Ruw^&`(J`w~+q;dzJ_k1^-v0*zw^w}F3UJ%tIEAqVNK{5wzw0i-2f_Ce zpAdp+x?}?oM6VtUiPZaS?7igs_f4@$_{N9;C7Vvvm zF$_KTLBJKKmFtHlg_Joo8Jh*#S}mi(;3j|vHgLPHQ{_j18f<~)+q+hT-d=HeQ+x=g z@baIR(E`xJ9Kv+s0gvm-5z9K=8ktK2BM(o}oqxLPt57u!n|g^2BaysW@v^`(mul$)n+C7O`xAMjH9 z_&z`!m6{uY^;*ZG%uNA~Fp&w5Vv?QcUTB%MQ9^qtcI6>sqTb?=a%U%lshxd2## zVfgI>@X(7s&x$^+ivl?5@$r{p((#*?T>uc9hbsi(Zs=@5q&l6vo*hkSDosQnADa6T zZoS6bjqv}iAcl}d!pCqNqJo+YI@C3~9c~S#@&Uv9Z7QE_dRiKDVm}Z#Q{c;C{k)a- z#@uLXwLsD) zmC#W84w&c;Y|ZD*7fk~a!&Ou5vi>V>z7o9_xAnRFXpsZKC^*TRZvIpr&?)+$zQBioB3OqNTNDPKic*Y?47~9U>AJE|PF|RZ~dx&WUJx-P$|Q zDNB-a`>?XIV`vXGiL2Fu8=icb^Ro()d`S%G`uj=%wXx0TnKE(sCv(c|dXJJ@b=n|z zIBShw#@X3uHa#%3uL3$-bzJ4R=v}txKZrm-oA*;;+B9SBIsdSLOjb{HM)T#C4p0P? zZ{CBB9s+zIVA7IF?N_kn!hcOk*+U`~2)3T&6&wEd9p>)=^D3wf_RkI3dH7Swiy;9OgCXn2+T;AEs zr}7eRmAgkt8_$WsjTVdkB>F?bE18$U`>d4!_y&Xhdb02Bb-PV|aY}7BxCzIGV0Rf*?zVU@3SxpVg@DyYJx#e^s z{wRDe#(86er5BWP7SvR6`8cKd2VWo3GUap+zX3q21?&Q0a_L$a9voSHd32wD(Y;dJ z3F6Q$>Aq@i7KzIZH;_W*}^9XC2Rp##QBE&+dJ4 zfNqQIT15>vPMp1wYotU!}ASoUWtp>`MLfX6O2C#CP3k{Z~-lgs^)6 zcF5(&#pVBW?dHFKH7SaIpfh2yr%J)z{~rIOV7>KEg&GBFEQ`VbZfzfM2T>&Xy)nl* zfeb7FdV(`GkPyQJ=*d0sZ{NOpo_8Mtv!iZ<^WjkOHqglqYz@pRgASQFj-iL6k*d5u zh<%wD=o8)(*1nxjus;s(S}Qbo-0PtP$XBOQ^ED{Yn0i(t$${R2*v2C7-C3a$P<`55 z&#=hd4G@=o$nRgDz1%JleSSD^@wy)zNXKv(I@cQKj)d~2P5LqYjbkN%?Wo>Z-07gD z|3Xy`sBlVp^cSLfk*w8<7&GXTq7BR;hh^HwsF;B8#T~)EwZ6&;Xa8-R%Q2`;dl5}- zL1OLgD4?XkH%{1)%Gm>K16M$sS^`#sKs4a8^3Zv_8i6q4$&kUJlWJvt*cpwxzdi!& zjYH=Bz|m{xwXc)1zjFL^1RM;us=#jX?RGSUu(4e0?#YHGT;+Z0U?r3tC1r5!ztHlw z&(rqu{nE9fV)W&RpGC@xCKYG^`#;ufE6R4}cHfk?5QHqv-LIC7Z>FR5yE2i43I+pI zDL!Ffo*)U@#Q+IfF{9OZV3&gdrp6I(gus~79EqA9Uw)afMO*Y5>&srW#ys8Fxj8vG zv2X=6u?sVS#bnbVORG=}5cM1q&L>hYE5rmEGzt}lwp0x(T-El5QP1h>{AwXK%rdEsd6$QnArp}N;aZY!{h zFK1HIUhL(w)+n8HCBFt3WyM8bpJ5Hh0pjnB!nFGNMHsD}!sSNw!>Ex#TysKEM~qZz zH`s~xrAW-xF#(5`2e;kAX3)`Xl}!7Apxqe?2_k&#y#q&iDyp(T*;a?gxm}+prRUST z>dNP1pU0hthgUIdtgIA5u3rIdi06;5mV^JOR?bq*xGgq>*n}@W#rK{Q?GGhm#BEP> zL2ESiLVx)2sA|tV6qS!?hE?#Azc#~?HD#mKKeAZM&fG(h=?^0H-V@2{pB*iM!sPl{ z0h1-6S{c7?(%Pi9*a63f32CgO3c7W_wWTG0Bq2LuMSW7DFI_|Lg&9cL#)Pa03$L?| zXtCXDie{_DWO0HcoWesTM;(yR)b5qyB6Zw;VsZYV#LE{~ade`NK$Q@Z^f#Gc8rdAx z39qzk>o<%vCn+tWyF8(#=wvikit2kC_l+@LK603;>iuZZkJ2-4q2O%c>& z`@>#>zhc^$r2P`40e+I)1;)ktrPYKNGZtZ?(wY3AU)~A%)^;u<05p?HFQ{55&7pn#0R^I`Wa>#WKh&y=i|!XcBoU5K&_57e)~DE z7V9J1Bgs(|aUv+k(5_UU&Lz}vB!mZ@OhzZ4H&8oD=zDS5vr0iJ2hiQ*+-`5}o2>MY zW;ty4gAv|+p5?}U9z-o#-XPHt&6h;NuA}!WnQMHwn|6;oEx{{e{)?w~$OFEN?Qlp4 z#rzX$Hxnah7ZSfd_U{`K#fd%pYd7Lc!iK)6+i;5mzSv}8*WdVck(IMHOcBwW~I z`V<-y&cSYugFfZzqae->vs&5@Vk4Ohmln6``YDhngmjkO zqaPum$dyIMZi&3-vl5)r-X-;JwW3HmDOl5VbN^@)F_(_Tga4b-jiF}WSnL{iyPD*P zQ_KTn4~xP*V8Brf=W&wdoXW&#Bs>KV8GkB*YICGY?Eje zV^E+ki3OcMWTeI>yG~3wMNALUf6GkSyXpQoWw+euy?+o*?DOq$Znkf>usswc-K0~P5hD8lYmotS znLMY1*aN?o38f8g`H^)-FjyoLz2e(Sd$$7xjCWp7=WIZj(&uWUFK+pH|CpiGY!SA` zJj9ce5>oj<>2uL1Ln-pt*_md$N^__gC<_+xsBx?JzM9f`t_PZiY~ut5h$>08(*W3L zKI5r15gslsu*QY)Dx9AI=9%aFWuNs(K3qJ!`I<#*-i|;xNSPKT<=QQdYl}H8NN)Hw zH3Ad{Nv=zCc%?=C_7I*EW%yX)Zm@{L&?H~?Sh6Z21{(BU8Ma93KHg@526}gd9n`>$ z2<72vrCa}P)a{mpgoOcI)p=c(y^BuD7nnkjGB!5k`Xvm8!Ws90uL0q_1O;)5QlWa` z0o$?EJgxt=0|5hYlfGT;{vl9&=~<&+O6h&K11x|Bb@WT$8A2u&nmn$L78{;Q=G(`* z^%QeM%h*J%+BLr%#Tm2hmW*Qt^{=Nvnyc%v4~UUp;zb4Uw+=c|JWpvOp+&Jp)%JCe zU7QdjGsg=>Jr<*SALRZNzc5I>Pr%`J^D#1AaV=fq>pHq{7yn&jlWI0J0JLrRT$%eg zJ9ysx7bR%R$7`_l@b+GEvcV9i!p?6G=WfluvmqGQe%T&9O0IGV*B8@2S6Sre7x=?r zjZE({v&?mFYM|X(>Q;oEZVNFS=308^i8|w*6!&VU?R?^bS)cm5;4ImnCItgXowT=bH~2h`fyGiQLBffg7WIdv5P|Hu zU}toeZ!rm#+SqCGI!pakBte9p9ay18Ylq=k<=jUhCFPL*i#DEVh1b^!M&AqC0x6yD znm6iPDaW-`R&B^qMoDWg@g|!`6;vtwH>KV8iVWc)sDrFK5^c(kYesBKEZ2bpS+=qdt5?;TlL_V{%qe`ESMY)doK?Rhl1 z#$niKe)NLl<>$AGQ-8>R(Q_~l4Kcp@x?>gx?u*mzQipy_Mz5>K@NRWA=>9<25XX%= zhGi|*b*Nbf|BThtEdDLkAJtFDk;vm#z2t%On_d9ck!RCi)dcotR`A=WYXvvvwpNpd z@xs3zLL@x2>S$?(WDtrk&9wZK1{`>6WzqVEt|8{vr7V!glWN&j$)eEBTP+$n^r>8G z$~RsI`@Z3tRZ6S$letJp1ikc#sx&gm3BklhN=Jds9*FTB8!~sl6$11 z8E9VlVz}7MIX~*D31A|p+>i7J;iw|l8$H|{`ZRGAiuMSxs+TpHa9!NN4PAcDU>LC= z4u~$RnTd^YLFGG>*p)qbc7?836U47G-|2_Zf?)2mlvhu5FmNmI<9R%w7xf0P^xK7Q z$kgh zz*2ZN*6(t(An@+pJAVE#Nvwf15|#G@U@`DaJ|0w|lScR~1R)XaC!u_25bkp=j5_Up zt45tXybWZ~*MQonhm|%NK*eO`0Kw%ewQ@iy@6+Lcgp$%)NmoKNu%NH6|8#Emgl~dd z=z2I;Ei+8vCuW@ze_p@%M;DXqgZQ)!zVf9a0t+o1Vd-vp;lMJ%{Wmi?IEkx5mP!nC z6#cf+avAfh)ZiFlvf^hr&>nHz~ak%#`MB$V(?19#FZ{cJuTdROXs`=J*pR9z;- z=d3$}cJ+C`6bOJl_ov#-|0y5g9vp2u^<1h-WzGHO>WaZ&Gr!dAb$|bSZ{T^|V3(Gb zUOc@YU%55ZHy{>BO=pI$$*fL|qP5)yqh4H)@c3Z6%ZUlCUGk7nPW)nu9MuYeDTX%>dZhE5-ZfgpB0mn&FyQni;hH| z*8p?r?Ch*@c^Ls|iiWIAj(tzwca=FtDn15wZ(fq%GcBLJde(AyC_M02i;y^G1a&9W zEL$=9g4<{L1e2^_TEI-4*6ym% z`g1bIlrz~vEJ)lfSotykNdv!JO*fDAfybDKs*6^+)$nCUPArl~)x}8^FU#F`U|7qr z?%EMOO2vIB{mIw*<-;pQKjK`PVLqr5~1WLT)nhRs8_Z$vZ}x&EfysE7hAAG?%oyxdA} zMM@!bEz#xovAD{LJuG&r9YdA5>Uj1`Fr({%!SUyl9A-^|p4z3hq*{8${o0~1Zw2ef z-0M)cQ3>kaOBdwf<3mFCyaJ~niof-mdSm~{Pj=|QEzK-dF_Z_BHtu?A5KGEG^gijj zzPDkhOIR<+NxT_#@cxO$_11>z<_gZD>+5-CXwT$dTve-j^3`0ERiOQDbzJ0(%2&Rx z-eP+GOxZ$cz>U;yUOG}#uc12Q^Q~S(>l^sY9fOAg9?0-VoYkv+K?M8v#qTstxO8)x zxS*vs6o1by5H0dcf*+AILE+}1w8N1eNo8(RW`kv4bUQ|qQh#y`5x&G)qvAl3QTNjR zbgd}WLjBJ);xur1)#Xa*#tU&$PD_Sf;w2_@HSUW8k2js3NN;spXmc}pM15cI#=D?yJZNrX#J@Npw#BW z1qIVN$edKj;8aRONU`9P(70fwd5$QKsPo3M&jq4_RlY^Q+l*9n=cn9zEjp+pGz4l) z8`*SNe*|DzKGcU9VBxzG}HqDQT^*{7T=Wj zEqBGe!OX;6Ap1ySr9(n4CPiw{q)yb)fZLtumv-(cGAsZT{5`J`x3~gE4S-qQMoh}%JVgDnZ6ThxFnBmjU8{?Iy{Q8LAr65%W#SWEUy7B7`C4ey-!Jjew ze-nAFUGwu({a~`h4u7qltpo~a=sOj45M3s!y163NLtj9&_aokXDw_~GvqD5NtvUY8 zEw#?EW!}8XMI~gUg|QB+yg9t5ZGwbaS};_yp*`pY~jN1LDu_ zW_gVPO$1HWrgr}RdikBGC}814gJJeoTjkp|p5`-;ZC(NOGyCtF}MkveG6Ym3ye^^5;9KfDi!(9F^$y&VUZ(fd=y zfIzg+e7j)HYeg2RO9XElInEF5e{B+p*qI=-$p}LaLS4Txc>No#^GiTlMSZZ5?AIdb z=bh3sPoQMt!x+h}`S4Gmh{k z=qub_erj9dg&G9+7_F->xol6H#WiY2SWx2KaJUb2xJGJ3xKNUJP{7Ct9{}vd;NDc# zd9mJU6O=UinJkV|aZ@?7WQ#BQN4u8ljvSN0h5pMAlr603YA&c7PBJ_cSZtN(eN0zb zjCdQg6j=ZEJ{&$5i=TPTW=2fi=QQ{(NbVq&C|fqktAn?Oe?;Qt^)^q2?nR} zq8!>er|nbY@GC<+$;eNPlx(|GVA%I~d8cHUhqQY_K77Ujz3FNjY?I2VtGC=wecsjJ zjk6k34UeMGL}=l9CfRD#FFrs z(S+$Ijb)QjxnExoJUYvNvXD%JI2(15v=&1U7anSNcFRw=BeT|5yZISUchfCTgLdAI zZfViKkk zRu(S21qaJN>s5x9B@h3<7l6GR8ebcKsPB2&#pZKq1?Zyno_G8&KgZV`8jFD};0VSc z1j0A~fdk4Oz<+akwVT}&Iv-ipnS}3P+rfdFK->l=$vuFx69dk70U* zAc-cEtYQ(|`UoeG1|lx#jAxD`cpGnzDOSAZ#$e0`G+-nusw$g8XZdLxNer8L;2*8j z=g{gmP|7t-nQrjv^((Z%crWg-46o0b-L~47f6shZZ;UrnG`l^fh;flpdDd@Z-OmW0 z-7Wvc2Wi9%SK2EN@M$`_itB#eFR52R?(WdN*c2s-yjeP&2Gio7%2{6vJF)!zK5qE_ z%BsNBdXkV?yMTXL3P-bT{?ahqyDQlRHCHH2ALePGssFPwyUVmJnDD%elmvBq<|yhC zHOl|~YV2yY*te17fOc9TtCpz5YfXJ+IWD+c*^sB<$m-wpS#Oa*C6OR9h$$DxqzgBy z&`LTzu+E~g6;nN`7aSqCRG0xaal*dGP^+|OsnLf#vOdwOHNRK3{=W8cv>@i)D=V%- zO$({$6DGlQcw+U(Yp|;IL-HE??Pw+Gfq~DFWWVV&YOVa9WYSvS9etq#Dq4wjP({$- z%X@e!-AD-!`#as@aLms-5ymPSz8y7QMhyh>s*CPGlNe7nID&dNU z48*-fZXNYpCyltL{LHNk6MDb!_Lp1^t;$c^P=_^s>Z>i%9`FQEjt*%-UP6w0RI+;$ z8C~a7IGJZt7R%9LFb9ganYDUok9miINbH~)j)=(I;I!^vDId!V=p)%o&YSb;fM0kH zjIw*^LXNOXX{nkqJ81IEK*$(LcMp>fhCiWb-8lD!LPfT6$7`SJarM(?hA-T=H$mua z0=%;#`)z>hjaQaA>AS~A)kI3&H$}-WL5Ag;BtKlgf;lVA_$SVOptZ%QqIZXdm+`u~ z^8d^#sC34%Xv%xLrJC^zoGvrdSi5v?^}$=o{qnYT+{DY$i8jqndteYDQ-=3r(->m z^=QeQ!!aXiiwsTMxllm>5m3`wZbo2E;U*T&D*X3enp1#1GR*z%V)ws^s^vDdr4BH~ z8FNe*^&Og#lcx-Bf6rAQhrMT%TVqZ}d1Kz1N#Wh=@86CB+k+v>wT3i+1ypyW4zP=N zeHvN>jJ=eU8O(|3mC1sC7`7W1ZGUMbHUw22;sGWwB55o==TyS+)D3;vug?o*$EPVN z$(V+`slPRtx)~7vX}CIeO_vwD8zsKYP&TW zJa~{|g%*MrDPE*_DDK7G9g16VcXuf6?!_tY?(R-$aVc>2_m6RM$pwQXWar&$&1XKN zX6pybv!hH-8o2ce7-Kmh*zK|SEZF{JU%~Xgp#ZU`an%pPp#V$f5?~_u)A6f!q#De^ zaJJ?MeaT#JWAPNXz#v~CS#BO)0qu9lky&S%_D39-OP-+sNS6Z_+}qpxA6@aE>&ngT zhokMPDvT>bR?0#^&+=Vz)i@v-{EPl^1If2(eZmYKq+6-F`7n_^mfr94WcT_Er~rV; z$3asUs(mt)U7N2BPAJ>k!~1l>5rFvW841u;2>x5NcG6YZuKl!k`-U%)pp=0d60L}* z%nf(aSabGOy}Hc+5SQDB4qND@l*9znekk zXN2bjwS8`<2;6tz$(}T;C)$lnaMGcL0;G=Q9qtV;cl9&p9s%nS4pG*alfy}!sSsh@ z%L>Q)5cHquI&dK6pP5hEA~vhGT&Lrvv-^l36qM2S^9jKYZl8NFF~Dpi4-vy7g#ixa zTfjvFjO5>P?Vjwrv8ek0SgmE-d}T@>x03$IrfWmsfVtZcFZPuGaaU9K-LvbOk=*hU zy4i1mrEpR| z>6JB*dOWw%^E|EeasRXRYZ{ov{AGQd3rA>{QJOk+!A=?sZZ6Oxz{F@kJ`iPIFzudBnV4s+wZLIq8zgr~7WC#Lk%@9cu@b}R*rt;0W zxis~|zd2L>SZ=?U={b9t#hWuKqO%S@-fLc+u+J3bj4Cx^KNs?>r+@w$&fD#jfh@($^ZszXBphSN)NUqRXp`XGD}ztFM`FNu}; z&l+^PLG;M(e&R5dHZj$&WUv^CthcMnh7T(SoqVT%j0lm-VcrRbMXXS6%6fi2TDi zwxv>(Tk%E?|H!N#(rQ~=6qjj5wGf}_Qn5@!IJ5p~=z%{uPlXz3jkeXt^?z7^T*d~Ab(slG^Z=7a=EFWn9191ZWSlOx7W|qQmiAkJ zq39(y-NxcSGx-0G;|6+ffI7Tgi?8IAGQ~se&X(MZ%d{R954;=YN)jTu=%1O;;~{Yp z7ets8U?nNkI74fQh}8P?)7^hAYyi+&EhA6mX(SuZH@0lng2(r3ItPi>HAMu1{ZD?W zc0lOI(t-IO5Ce>fiMd#7b-X%|?Ql8XyKk^Kh|KX`tvc^6v{!J}@8O%0cghY)6Z2)k z4@YKDo*Y%8KzP{{p>}^^c;P&=&&CGdwAK>sX7PRezR#f7;&wlrFi40QG~Sdk7@RS+ zA4wDqY<88VASHPO&?fDXwq7l_f^;H zc>mETO%V4E2@S9P{f4P&4X!f<8w!!w!VQ5i=s-RGd7sAN%+Hu`_7jDHHWjjWq?j2z zOwf49?+MDg*hnc&PD=v8SeMHqde9yar-sb^#zc<@l?Fi@xByufGuO7Y&WQ-d| z>MZJLYdhI!G6K6wszD;7KC4f5-=9zQx?C^TJ?%D6S2Ej}$@_^G0osgTckJ*gB1GQ% zGC!NGx4hc>`OjX>DQ~!<15FPj526(P{u-yOI*>luSF$e|X*lKe?H(#Cd@>*hAGIG< zZQ!1WcBh5qfn&+LW5o#JDNH7M;C=meK2IFlzbg?I8U{teJ-MAhdJJo)Ji8|*CV)$L zqh=*Os;*IZ{gCG+PFkBPq9|C>PC@4RRv(;eA;CnT@}ms0+H~F;o!0pMRVN-59%Hwe zI7>pL8zYw6eh|3wwz+iZHlHu_Ri5quWy9H@TZ|9fwF9RRK{#s#F16|%Kp)l~4jr(1)&gyPUhrelw% zl}|@WO(DPV(g#|se$hKVA22z8TaiH?#XxSfz)QvV7PWy0L0GOA_mAZ_c>lJWRi=%H z*s3VUS{zv!=NvgDI3n+j=~zp^s#lS4G#E_*q~-8pv+MQm;QNm$(k2%W!By(*;%$*D z&)CSLVB`1R=0@AwnV#9zG>E%!y}8GjqF7I4$E@9K6I*9|m!#@421u^%0`x6+vATG-oSLQNMG`jz&dzbR)DPE)Nzp^$Xdd*K8UXf5~+6 z(A*72bo}#1X^Xh=AUs{==Nl#x5z z|CLU3pa%XSK}O`aae1xwuT`b-i`yK$Zw#sd{KHf~nsavu1ZUu-k!bPB3<^ii)Gi2` zwWAb{2}(^-N^qZ2)#sN|@u>wf(=pL9pS(RK?JqisgqHEA{8BuM^gg%bPr1<7hK$6uQ9-$v@ATc` zV26*wuBp960r1!?_w|w3=tD2W{iDO! z)gZuv{aZ_zaq9ah)L%A}>dNmdcy*n1-!`0JrWmZwe|(2oAmZI20)@7z4Ii`qCG`(& z`8bQmij6JglS}HGhQ{~aK1Rlr*op8?P3t204?OWx0M<_7aQ;d!*Pjy!Gyk?9(|BKD zh%>(YutWh9E)Z6m#zH^KM+6Y;V`k^{O1}0{zDOf6-HRHLNY+y%5l;|ABy@~17O-CU z-1okJ0aAuk{~h$m`-l#&9x619e=AAFr?G>s>|X%Zb&0aIj0;x>H8L{`U;tHQy}^oUpILZ?T0VSr72($96tPK=$#Ka=^lY zR1|f5!@0bK;>$%z^Bk#hc&=kIh(D!`BDhC#X#EVU?ZFfv)MILGa^Gh%kgBVA*gO3j zx`Z5ZGal_Kx#(!qdSVJBDj`XZG0P=229`{va8@+w)uyYW2Vfk24^$(c{@oqSTmeqq z{omghWn#0I*(_#dXWKQe&RaHWGvDM$fa5~Vt^~e)#D!DJ&P{5&ak)C~f3W7t2NA~n z2puZ-`FCR3DAfWCyFE7a<`Z;7KV?q>jKm`q^UBWo>4K>sDp1q86*IBA+K()$g8Q82(#5JvPH)?RwD@3^>qcOZyTR(-|- zLDSL@%rGP8-G>^wlxj{j=hw@M9VX~$BYcBvtQ^US-KHa0k$VznJUTY4x#Y8TfcpR! z9jl@N53h%_Wtu_`D^uA_6w5lW8OXd^r|lM)a$U6?{s_<=KRu?2O*|Oro>kHeG>l`dETgl7V}NzhK}u zKw>mT3cqZuDpVC--rc?e6&Du=#@53v?3DD5hvJ&%Pmh1||HLl-HG2zsu+ks}oxAaT=E5`0`w z&m$8}e8Ym0R~&)y#Rg6kPEs+4;2SBQKl=BD-cr5~1=UsR*Aes1bPE>+B+G*#@6H8w zs=IHEcbhiJRX}d*i=Dai`uJzt#MMGsYT}*kcH`p$p<>1hEZq#(j?>e7Q=d(Z&lavb ze0N*(_Px#*{~ZJkEv^r;Jk9kK=*u-J-~A3cMFX9otL0)S(7!KD{&KtC&A>%v8Krpi zDHUp>*kL=aPpA8@O(ba7J@=?*8OQ%Ll6W3Xn-U(4m&pR>LTpj~F#Y8- zW1Ut)kDRf~`i=QAg0TV00^Q}gTi`vt(10A?$GMGibR*WlQgspbq4R1<7*a;)!MC>o zHs^EyK)d`~dEt$_%VP*HqUoohzXvFa{fH#kO_yQbqU3@FLqDT6Dx-Iw_5E_I93D0>!n){lET z&{@~g;GU=DiTn-Fb^4e?jpK_BF1Z|z9tnf35kgB^@MU5hCt{yu(H%|F5eQMwV5MbkKWax@cv{_Abq)Es5lBZ4?I7s+#k1~J_g=g8pniEM`cwP7|;INfvr{P}5 zp`%}s6K=-wu6nUTM91cGbdOn>N&oh<$MumoAbJL7nxnuYKj01AMyvCGAB;BV=aubg zVT^IpHg5-a_lL9L{n;B2$K}Q}ctlI>=m{+|vqjs@&dQBXH`O6ih(aKVFh@Cz95~mp zlVdY-{9!uEtvdrVhZ+zDix+D-h897B`b~$M0MiUoQ$I56;hE$lR{|1iIiUaw*fO zx;_Z}1+Hd9C=0yg1kV}QHz65sm$K|Q>Ie`E4yrUf1x~QQj7F?b>Mt9=aB|;0Mqs2H z9UToH_vO5_h{QC!eU}N)-gbb@Dw%yFx6cM{?AmWnR_|W^$iLoa{GLvjZ?{S>3R8q7 zkD9DHXMc7BO54009N4T5+=qM2pt885yCnfL&V~*Z%im zo=!~}RERzE-)mxrRSMalJvkAMhV#X_EeDAmE{HxJ7g0cHrNKn2sc<*V@pkrJed8v3 z-Hx4)-Lu`>LiJMAE)3rw9hY8_3Y_z#s$$ybJycdQG}O*yLq?i;(yEd6cVcL$WXPU0 z#Ng*o?)QTfT1)i(fEKC{z|qb{yH?4h@#S!hG6xz~&);*f znd?$iDJ5b8Iz_$}`}F?kr&3iTN0lPV(6tz!_78kf@T1I!_bbcEMg6f3USQK7jv#JE zkOCs4@cYrWQ>amzng~og=nWzqxZYZ8b@aeu$fkq4R8bG&&Pc1GZK2a`qWV z0bW8zfA>(*Zz7B9Sra&Jdgr;PO0`N4*%~Xx>q^a)jC3U{KOmj~M{j1shALhqOxEU9 zV9xb(jfO)~vDyA)D^%m^tK>GU_ND&a#4<4hE+=skn*y^qc32v9n}Dmd2}3#O;*@rB z`|ade;gV3z^JaXlj|qHIf3(q(=GkSMx@6fDx}rB! z;&im^(Ou4e`W4S`nQGM4_aNF-}b^8Q2pp11g) zbj)j?)fWenh4dvVvGsWh-_rrv+%i3jl|WINs^9O+Azw;|QX~^iX4mv5Y*~AS;k7?s z)m{Bd9<72;02wRtGG^v2zx~RR2uC{As-2O7<6=&M50cgG9>?GSN=2QYbGcHHDbswt_^p^Hz29-`6(S7dghU)+lS&U!0q@eG{zcn_LD=$mOI4s zS01dLXLiolZx9^68a=&Q-5s8PyPd0eBXz@u*|`*ph^fcZIphNZA!^TP2&z1ZC-)X{=qtHOn( z+nE|4!y`!2mBLz#KvnsGmhCO6>sxlL7hi54w(fE%&6@__FbOjhm$k;p3|#%U5v)1o zT+kwXM^Lh{|s?HPt870XUp{oyR+-IPczbYFI(6?uMBz*BT@BM;^JC)w&}so z9J<<>K7acGL+4>H3+}dQ{rb5JK$5CeX}5Sdn7BG1fgDB*iUimPEjSY~zM5)54!iX@ zQF(+l!lFM`KJ z7_aaAzh6*(KRR-xGYBp`g~T3}j8L}({LrW?ioYg-F@Bsji$VQy{WBVVs4ov3x%9~9 zyj>0LiAWBjOu9D!8~9uYy!Zb17f^!jgKRhNfT_1o-$#L`f4h1wCodf-Po0a_ z;}H=Ncbh)10oVdU$vj6<-Lrw9v4+LjCO~}L-|y}0@Ic|&MNr1#Pv6g%%6!R5gwa#y*%AdO7NGOPv5A7`?w*hT z4*&i87r?{-SnTg#`zmW`SVrgKuu*tin#K-?Y`1-5WV@V%*UR_e$kcv$EAt~Ke=1PT zx~-$9shs3lg^{D1bGXQvnDLJDoyEdb-DPzsRV>l&!@~m$##jaQAIf52C|gf~mM;^y z-v5miWA__t{8ZHHB483-2jC0bICAXX*a%wR+5##056$_N^s^ej%BWPUw3PGDqpV8t z$^@KdyKog?kVOrqLU=hwLfUWoKkEN1PL+kZ)aPV5bPpArqDAR3tK9lg>R(;fdm$zD zPD4vfm1Wk0rHQC^PC6_5LfF_{zP<6X5atXpjo z{a^)e89-x`t0eE)lFiJD4&bWfS$|7!`a6}%!vY2sWxSZ$kf8Jb%K1wKS|>iTf`7lf zR+Az3a@*vz>LFNrsz#(_b41mXE#k~_al=#=kqOoGR3=g7>ltXzm5^0Qanc8BEF{w> ztq`qBtvcs9G(yf&>oesTPRs8bN&ewJ&mb?9sXxs&B*+yfbCugqakBaHq5rzw(5+;U zNTH)Vz`m(js&gpe!9RHs>zluJ!t(88R?(I`6t;p)&Gj|V&B0fksAlAA=G#jOeec@xt`r2W9kA{#UNJZ-Zk?<(@5ac7-2Zf?#X^E_iYH|&-0p|nxg{0(`p#afg z*Qmg7`t>4PrnF0PeN$*TB>@q&gK5!iQ4VgLY5$Te77~Ii+eg*Po`cW{g+&)Tm zi8@`$ZN9orh$XV*Zv`tj(b5(I336EqJI4u)*{`)Rz< zYSCqti2`0i{Oi$ZyCA=-A~+YE#?%HZ`GKq{z#dimJRX}7_Ck;*w|l+V zg5LoTn(3me4mN1a;pO}5?)%q!KzyooA`13;ANuFo_z3_)*zj;=W;wD3r|wtleka>e(Mx8 z+papr55^E$yvc;SIHBV54=z(*;6lNxPvxe;mj0jXn9uj+ERT z1b?gHG$_%M(m2^@YMwffCUZqk+KqlYC_~?{nEnOdq?H=^|1DYS9WyS_<>Ege~e7-d(IxwUJ4r{SLJV zQ;6_L@a5ds*(I?Om)AJK)$CR2+8=1dZ)y-pFjL$$3^QFu`GSNsT%Rfn&rQDiSe2?| z>%yJHz)_V&?HOfyrPQgH=f(Lrj$WQ4NaW`fe+-^M9NSo|W*A9^x7B9&@c1uY5Sv2I zWWg~3Kg2j^+2FYt?`=unPt>wqO-Hdsd<<8k!Md!|*Vd@%GvP63u<%`;5rol05pS7o zMX?voC-gp9mEZ%dG^Z;w;eBgVf{hukB}>WvKO!eAs0yIFN2G&(R`a&9ej`NRN+;wi z{ba3m;5T;~EcSv)m5kI_Lrsass9^mj!p&5pdxyrZU+-$B2#hZngVk{rqOJamaIgSXH-eu?JfhY2U5yo+D9Vjc<>5-A|%H%~`z z^GcnLwUl!ei53d{HJ-YXk>5n!IFG|fOYUWes-CcN+v0H8{q<<*_bWa;1q3dav;YPR z?3M&690)GT=%W1a7p9qo=Z0zi-=YL=*&zCU%KFQFC#WDaNK zn^)GBxDm>#z&>tKmDl#cGdjG4)entozerdU--IDtW2G%U|A*!NJYwr_I0D7dReEk8 zf)B9kv*CpQPMLrZ^hYSrjw0~={?EskoeY2l7?^gEM(j;2N=**(fH&@`+O!r9rFMWn z;5PFP2MO(2Y@ca=pvGff{8oEZV=VVX-`)WK#fY63i{G;$a+sFWZ|bR38w zXiSecVj+1vlIFNXRJ#EOPjQGd%<)%v;tY3GrhYojKzk6iC4MR~H!Ffa4;}f1-(KKm z8koFi8FBxQM%$}tYY0+^!O@d9X_36(!!>|B0}nU}FmOBn(>uMcewzG*GB8U#=!X9yb>xi)Fpu?tTS)^T7I8QNK+_ z=GKyA7Nh*dJ7GLj9+PTVKC9B}@V?mS07TBx&9>`#`&OrqPty=LY=Pvv`hbaj{vCopri`yqLWgN<^1)GbiqY? zzNs#zBq1;(vm*FI(F)tEhxzunGj3JdKhsHl9@o^g6a*gcQCu~}n%ll6LF1D3uV@3d zDP{*{YISt-a!xPSC zBv8M3H@SwiVp^fGYGwCWp22t4kqT*2X->z&B3sN2>cgsxT_QXsmfc@2jZ}f61T@)$ z4&R>IwpvUp`i=%ro)uz&h?YCG?HOfBe}}BhHI%U`?OO2bmk@0mH!dj8i{JIZZ+SFf zW~#rNVH~vgQe!t@TC}Wm!gr)RIF|ebuj136nCTE{TXmEp!#VT9#8*MLz~RJZms^=M zbdw{h&Yki2`{yi`G^VKX9rm#M?6pQGH>e_>SO4tv8?bY?j}G7k0O0 zpS-hVcL=SQt;nJik#t$?7M^MFeoR3B89G##&eF!I0J+7LIn5?mWxHb>=&Rh?UI{>& z$1S^kd(|(U%g$XPCHE;8-`gaxeys64sGqOrm0!Ka8*AkeT zlwQwFCFC8*nyfg@$Wk2{D5#fFRS&U+Z(-3Uvl9$Yy+l%t&%NwOnlSINF~piF;Pf-i zy1vmt8b&>gR2tP&ioMqLEJ4CZ`58u%DY~+KuVU4Kpj12R!&^D+8O2hxe`Jk*c&S?Z zFGOWP)$NkiBw1D!Yg*=$0y4oBucjajffl*^L#5)^{nCg>o{?qox*#ETw0;Jx1lw zt@xF**7$zTtRl(!wH!81k5UzDEMlA#v1$A{CxN=hx zKEAD`8(C|P%4fE>EK|@`>xlQC(t+GqX)EG?^nMbXSe^IM_JXWW}}kuM+8d- z;vIT4LBZXja5!*bK|x|v`*kxg*udWt{Bm-|bz!@An3gtmKi(pEqL^cd9P9=1?#sC??hwGH6C`wVR$_#6AlmRMg_6RtvQp?5`~2F zzaz_Pe>r#?v*G?!KluZw!V2BuDD+AtVhQ{m0mRGhcVmDTBO}Ik^&;E*emf-F`_TeS zh8FQ>dir*LvGcmv=NjcsxAjTz<)Z89cyi-?5v!cN;qT_RAdi=S4_z;(UAP7^z;W{L zB0%n^xn>wf!nT0{rdhGtVI^(jL?GA^IsZ~!@ZRrXZ}~GnImjs&ngZhZF=2_|n2uL`e0ucuw6C1>6>z*%h^_|HiN7Gc)q#BDU^<3*lM&Yg1qWai}udnzj zF%Cib7`QkL+lD$q>{6I9L1mqh$5z!!6)3);pk>rQ(&%iQLX~R9aIpvwDJ*TZ2g(W4 z1)m;hAi*;(j%<*+O#~Tqvz}hdV0gYD%u@+new>i*L47p$yz&;cLR7EWdS=ii&+Zs6 z1U?OeyUMlnm6Kkd+ksyR5h$?HF|T-Rlb?~rp}%C%Q&4oEPkZW{z%JsnlV1N zg4_k{wI3u*`bj}D2x;=e zJq0;XM&~$_rST2laZ5*pe^sl}9BWfk+lP>sl%#{eljXYwTCUe37k2~7H`Lh1rq%(p z_W20WKTF=0Y1Q=IG+r~yLLGK9~m4V$3w78G|GsWmFCy0*;s!iiK`b!d9i zp-9);4P5Qa-k3^?w-8xwm_l$MIX#U-lnXAB##p0Gi7S}~)TTICVl^}ASa*GJEh{ss z214SsMfyjg8l;-sIZ`e=H8h1hdP`5Em__~lxod&RDR#|SofX7XMUKAC(|W6j7PX~o zL=r!#{7yn2lT#&S&vFVuc+;0AA6VE^m-3)_Pj@bfjaN#)T4Zt-eCzTBGXgM$%C}k3 z_e6KGNqEn5xxA81CZdnCRmCf4k3Ce`4hV;7vCQ;XkF=VI*%4|IAEui`pIR~qu(j#J zLaE?+I#W_aT0B7`h{);)IApk>f(2R+7%bkgYmpdIUaC40Zk%r3b5Qf~t))}Eg-@NZ z&l69@ZsHqUuH_2V{xu;fN#5Ai(a~SFCj*C{^-}&A1pTttc+%jj980J6r)~_DnN3vw zd702&(M_!s7}^q(I73uS+8wm7WsoX3wP>I$U^Hb(3Wah2Iiq=ZcdXFV+0l_CX`E|U zj?8`pTvS zShec_+C(F$WR_U_4hbng>Txh$kL(-iryXA+hccVS9&*<&*@u}&uaT%|{P`%*h!>Tf z6q~}f`*DzKHP`ERUn>nwqINT4MWwE{fUL5d7beALw?A8z;upY(Zg08182AVfLE!=` zm~aD)X+*teoOM3xNU?(1C~19#L`WTk@9ol% zE|3Bk31s~M9=d?7SeZ1{9yrJ{2fCX<6d?$Cj8}eiY#?%fC?iIjj?d{r*2i_9-+Zu?*2h>fHr~iMMoSQMTiJgLx4J12wJy!0X(-K{GMFVlmX;B0EOxZe(I!^)QmK3 zs@Y9#zJS4S`-bRZab4%$lac+0L;3tc@%i@rC=2_q^jpfJnQx+W7Md4QrV-_MOr$Eh z?)a4KW!_kqR*>_o&(n1hSp0}ow93ORXCDZRlUIxU*)o4ER9X$n8ZsrTkn3f!Q+w3m1MZ;1TZSm%n39TLT zkcnWedyCTf@@Kw;LZ@=^UnjF7CC1nvTI#ZX(iQjjXKi4a#u>FW?tI+^R|3YYO;mAXmBSjE^n!KYH2vPTS>K1{uCv^$j`{YBj6*{X`JJu660LH z$|h-d(O#Hgt_sookdDCl+R)A%=`#HIxZWC-A9LBmR#H*IerGh=tD#i#Piv?AgI_JE zVXm@%ty!PLL^PT49jjh}e=RMihSM@2p_|W}$M+0LPjs2z*{3@`>r%EUcOrPl=b;>z zVYqvA@MFnBE_)Hs*_+m6QMKuj@#^o5V{YqO!!IKaW1as{SDJ(Q66i5{O(F6zb}Cp# z?S_gW%Dk(pe^*AyiIkIRmKD4ZWz`d&7GFWdakJvAqKu%)X1ZBtU4}H^yh=rwEccaZ zi~%}oF%X{UC6;Byo($KmZ>r)9mxP~3gTzQ=GxBz_AjfBTxv^Q>wkpSf|1#VHv%N5e zOM-siJ9FAn@ZaUm$y~-vC#J1acKnDHE{5k^T>s2D#EV;}OZA5)k14RIm^1D>bL;#WIb+$AUsS88WIRUW z<;&yQKmnbYl`U!USyU5$fHOTPj4$B4SoUt?8B7iL>qu+F(5BnZ4?G~!%(UKQj4d9IekdaL zu%B27lQ#H{^SBj)^a5iTfvh69kU)9Cr}0@c`ObeyYMbuYw*e^Fz`=)K76GBpeu&~5 zXec8KYK>Kw>OT8Y)yV8#|Ss#kiPn_s3O~Y&-EbhXUQ`&Vj8V= zw!H#+CLZIejMhTywt5NE`D-Je1dJz`0s4FkY^Z%+Q@Z}`18o_uU*s~u%6V88>H2NM zreHYUGa^bHC$bwE)-3`)T#2DbB65Ct9OW@FJDf11k$PUr(L`{el%QGdj>rHi5rQ5M` zr-cvQ9zw-M#)TS^#07fr@~!}RgOm-A( zJbvnW*;!7I_jz8vdJcrJ1TvOb#J_*}BMz-wb^;1o-*0w0;+b_`0U-%MT@JhjMMQ1h zZD7f!xgN#XKnM~yNN(1bNa$d1q3X2yht7XD$Lno(7r>h3`*+6pA`7CO#3HIOia@GN z!#SljR0m{Uh@!91Xb}<)MFb0aHHs~i1}fDvmPr*xWt!?H^*QB&71@NJnq7*jAhQ%C z%}V)8d&!MibXhS63(iGxp=A8(a1vNWJ@ymI1I^lz`C}q%(e$>>87#pT=?<)Gz`A_( zytV4|5p&El#&6J|?U#k1Z-HzVpFr#7?W*0T*R~imJdW=~(VXNKkvzTvH;(7s*Ylqf z)x71U=XLcF$T;cju3qEpfwgmuzca?St^Y>kziZF{zmO{V4vZny*tAwn1CeX zi1PX2#7QIrF?HCmgTM3uoEj?OgS!yI4-mz#_;_q?EiqT2P?->%hcFO>wFH{#<=Y$#1$ibO}Sq6*o!j?Olaj5so>xKFpW{a2u5 zY=o+@jHgU)OiPPZ*%h72*2Y${Nr9eJt? z3KrC+j1&2!K42sZYv{-G3{Jx5Badq!NV_aX5+BA^cYm({1b$|ARUC6KkapwaGx$e#Cz0M(E!o2>Fo~N*=)j zy~;|KUl?v2gq4N8Kqc!COnD_Dfg>3Ts#E*3B0Ifgk-oQ zk7;cQtkiA1a>V;UD^97VlIiY;xJsn@gSm0x&{RiCHhxD$Nn*=lO)te^IhbN6U_SljZAVT)TP)1HH~j(BLhP}Njf^Tp0|Nt^J4mfxB?@!%v*f%98kC>&xBy%;F1 z$UN8GW`K!n1)2n=_h(>zn1R|gMC!=`|y1l`-l() z(?-2L>mpQ8E(Cr#{fG1kHzkw;3Cucf1+2ZNr>9Np;fx9{BR)^kZ1|~5Bsy%3#&HDx z0pvOVacd}o_rFjso}aj*y_PU~YnFc-Ms_{!4+1~M{=M17Cp%m1^Lz^YmBzVT;9+3n z#NXDsY%LPftoQS35s=A*4Q)R6Hvb0*7-Vx(2a@B%X6nuU=UBW_wA;KN$OfvclJl3E zZFRdG_D5)eUyZ@D$72!31doid>=o(wyQa?q_t)-jg6GE#pZz2?-Itm2wsq$B_*~IX zL`L|*iN*cyMrKW^;6M&pN@nz9nQS5>0psW1)qz6uqpXlb|dl_u>XoC6?3} z86vsMZOfI>8A4sq6P8v?hZ;s_#%z{M4Z#^s>)GW7q_KOzbY@7hF+-LX?2c|migM5Z za0E^VfYiMu8`$w~?(+uH#sU9S@tI&CgYg@*F-FqXYA zGiQ%zSW1OfzAY4rFD2Vf zRgu`*`4$Mdh8XCe3%A%X?3-QMDny7p7h`_qOam8wW^m8?{63zSh0UU}7U@2gW8u5- zuK9S+hmgGV-6CHI?~iXjWW;mC`yOR?dBspNpVJh zZI*3Kq&2ZDz3uDKs!SRsnKTt#0565KV$|$x*EBWE?-%N<^7V)eQx;osW;SMh*Nun$xJ0#pC-sMnkOOB*Aau}<^t27g7$@^V#_vbR0BuMWoN zVk*@3bj#CJI#KHss=U3Yx?O!ToZ~|jAS_l*&Qal{S&y}u@n}PSyz+L2?in{LxX?>t zL)E8)MJ*xRxC3nZX@asU7Vy|+Z>cwHJ$PQMoHPl76Fyms{wQ(GGp1YgVzC=mnctmG zAiC)sJ9mw5WL>*9+OUF{glo`Zc9BPQv%f`X6ljnJEBL_b3g*09d$ANLWDwo=&7c2j zfD5`RHxaf;ai|rJYa*W8%88?MQvVr}%qT@CjmE0Ab<-JIk9?ckur^e5yl~jGR@QMg zSEOW2;dIsL!2S`>Nsmk1q7iZ;6=jl0$}vav7P`0pv+}`e8tLN)q`F~^bHR}MZ<10q zABv8&l=VYE`D86yLW|C0nSy@?3*mE%B2HGsrxh!!0VDm)zWiQyl@h{OI)m$wiR2gc zx(xPTI_Bu*4cg6)hACfm{+|V?rhzsOr}7%qPqor_uOujyIii~$SnND=41^^B|Ls`f zKXvGk(`K}vn*>nA#^#KU4+4MnxPu+|N)ia}R973A8+sNJS6c4Qqh2n)E*_VOE$w{n z61xH@+Ht-!2Hz>MZB+iu9B#o6?9kV{2?L(1Vh{g+*hRbk)c6WvP zq-E2N`I~BYgZ<`ZLZ&6#_>w~mvxP>{RYw{%p>~h`5g<(jTwBcK1)gN3?EU9(ZgWRl zS!IW5;k>ns|M*6*s6~etyV) z9X?Jnf^h>9GXa5xP=tM`IJ`e92p1$hmeJ$2c?WP8JS(=%YSHr5dR>2aBJO@~bhNF$ z8zgdE_DiHmiUni`M@*rIEPSHN=2db+QmnvxddqmVc>cM+G*)F~ z+Y9X1Dtp8Qhhrr3(7=I1Y@C$_gGo=8tl2g{12}>sBhA22Z1B(GLd!w!^SfS_XiDgh z?CguX-XsP!kZ9!RXja+oF)H7{jPV0Ng!5m%*z?XC{QUDhj20IyQ^~yYT_uea5C5me zwx*huiqaKTGqt+r6W+>8JraUXd8gwYwu<#d6$3q*JSxN!l8*xuS|V|hOLJ9DI~;g3 zty~L)OrND}&|UcXG?hU#c5{X{X(mq<59o#KF8Pbb4d33?U5!X(^cw0lsB5*YS3DQg zXy|G02bP`HI}m7TScH9?ucb;}zg1G3L`$_Q?QIcCWp9awMbj&wnI&9mz6TL>u3|N! z=-y`k?e4SmtGJmKShP)|cQLTbb%r;xca#GaMCJ*f9$!q4bSLYVx^joslKfQLu1I2r!=M%}768(ekUNhN(=Rhb()i z|CtNmk~DrpO88c*y4EVyO3_aVo@_8Hp7)kVOL0`C^UOFT>0v$>+rEBT>k35h^gCC! zvFK4Wf}Rs9>xPw<_;^DjZ)H+FSXaAv5c<1+^TTIn_O}XFh9_GVQWpFT>{bd=>l91r z)$il0Jg!BE3(d))tv{>9OKZPml5LXUJ{5Bj#A!1veuzfUa%GevS@DEr#yC^=o<4NP z{G%!!#HSjEHlOs@bwWy;qRGw5RG$nKNNff>i+1?n7oY}u%_F)IXiS_7Z|Fz%dmH}X z6TVD7+PS1=&>2>XI zD!V&Pp86cbAt@LbT=Y>c<_sZj-3^+_$B^Y2u4Xev>c!^dkmNE38@NCoT7iflspM*> z(NL7>e_{~?T62*rgZ_8 zVXYdtc`QtWqrDD$Mp)!@N?b!iqZ*AQZ&Vb-U?%Gx9~{U7$Z)EK*z$i1&mAEG)q`af<~B<)wlN!^b@cO;)smQe(2~wD^f# z_R|F@pj$24{WCe;w$wd&TV9**ZCsr*tm~{qG}un4>lqN}3qhfw0Qu_veDmpbpYQc{ zJLG-z?$VvtORP`*vB2Zq;On2)f9ni--sfgOOD7A4rBf(yWprxek+P6;DztTceUbLL z`s5?3^Sb$R`ucAxU~n>9M`n~^^?cgf=jGh)wF@+-c)wfS;dpcA1XR60>f#!K5`7s# zW8jC?B8-QoQ`fe2m$nWMkN>qz*ll#$pAP_>=%USIr=XzV`(T6AFQyY%qF=aD43yH# zt*^a+*+=s_!I)Xh(#omP0fpRXlkN!Ch2aLJbi8&xAu9+M8K}d_uzBCwZuN-}-3%FL zJGBAdj#)cnlspiklwK!d{M{(Bm*b1%eYu{1&&y|dq9-p>U|cMYvvzUET6A@FHDyB; zGN~AAf^}No{xV+;G+k$SZ9dVzUvM8iDHU5o#a2TV)=ym53E^0g`!n&VRanDB_W3Hi za@!Jr?Mx}4;A7Qn@iB_*l(6S6n|*%H$;^hxOhKgAA~7?cm3jzrREnui!arD-T_eo# z|7bePsHobu4G*0n14x4~Gz{G(HFOUl-Cfcp-7VcnIdpe}bax0ym$XO;2z>LrYkm9g zzz>GC?tSfjo#%1fxB~`u1?5?C0L-DrTa`UOjx}T_jq?5^O^5(lQ0pABemELwiB=1g z%$#Q7U)pZAhK#t~={eNT+Kk)OC>CtvNd4oxj@A zx$k{{xlA2;wy-_$-tj(B$_E?cEZ?}mb;5>A!DqPI<)@oq&6@X&TJykjytyeck0laR zLW%YH4VFebn>q4~{SYp~Iz`{QS)-zFYoQAb9r~SIVvtEio66p0QGV)?d2nZIl~$Dp z8uReda9xQMKCQK8A>F1*X*Q1oUV8*SvrF}k$l$CVQJ;J67BfZK?$E!si@U^dcRpvI zT_4T~?kb*O*BI-R39G5tsDfqeDq>ACMwAlmNWJ%k&=gX8L;E&~a-ltfjEA+5La9lO zzzIa1(T!g%*FJb#a~dVv@)ISADR*l-+@$tV^z`}sq?plLO_;?buq=Cp z@N0!a))aJJ-HR7;Z<{gmF!{} zj-H>33o+Pa`E9LWvU1)$noCWd%}g79PHU$H^rX#1r=Y39WnYojlkU*$c}-oS&?S?N zjm?!ZQ?zIDiX}s2ncQ(Ad~KBRl)_sZT&QV;)|*x@#kx^|X}3Y&`{A>Cc!O3ZPxFST zJ0WDfj;VP0{uzEE{l{}|(0#1PUfQ=W+W)!|)`wK8`5C?Zclg58mWOagtGxMCc=&$n|=a{{pA}z5MOI-%QDL#pa>IF?I7j zuUdBs%b%*)=32jdSf+hQA%-2V`C#3|EHec{Tu%Y9ok)CZ$-JkG4A1xxa5||p2F_l*iBB)uCfbZr$cgm0Y&^Z+R6W0SLy`QK<-$M#&fa9(5ILqp@UlnK{6^?a^6M zRM>zZ_#Y3#`^Hd2n_R*Kd*)H*tg&^WQlfD5?8NPF*@CCkjM`o4!khyFK1i#8C!%B9 zL^r>NrNC=PKl0B;VyCRQ6ZWoStvTW8xQ#pS&JDykCb}J_Fv@U&AzVZbq`vsmm7W)d zK+nqyML`~`yftZjYFOxW8CMo)AS}$**VED@tZP=5{SBU(d&49$7x-0|m;RXNA~S5?6NQ)}x0#J+6Ni z9(!xPp8TS>qCkWZF7n5;y)$`U8vb?Lj!#_5Ym&=+8;`^L;ajOi@`fF9VX_a$lc_Ex z1n!UPi%N8&(nZxBW!2wEk$Ou(ms22Am&dsLXdit>T1ngKRj+{DvPXIH^Tl?x^k~O9 z>+P<92Zlt|l}kyLW$m&EJ+oM^bNYBo?b~PJL7eawK5b>N9oKS$T{CJ0V|7bYXQM%h z&NAU}{N|!Sq&fae2R=t~Xc}j&u-6YkWf$#H&P7(;oKL2L=N&tJa-Z;3D$L$A6ANQ5 z%G3rJrPTZAstqCIp0{O8=)9p!{X=yyQjOCh#kJKn=#3LOYde49MFK;enOha5zEnWa zJc=NCR@6_X2)Q$|cy7@l$*c8I7{@WV(pGOGF!>N}c}PmIyAd|>r;?Bw%nDXaX2fqAnLP@;-vYhOD8U+pu0%s~Jw#22Otk1tW zoKFu|?3)aEW7;@p9h;P#ad=G`q$@jCPpdmKNJHSU{%uw`3G^_`xyHE&U5C;bdlD^d zGXAK!UKiI3?}EuO7XTP$_WhEYpc-|$#fcd_}( zAO|tID%kdJ85>~xC?|ci<0Z_>9N2lz_9M=qYkS_B|E4i{K65u*-c3IG)5WB^7R>pQ8{4|!ByB;Ainy4E+(|NYCYDyA z9BNNxwNaFj2Hzz@4474Ez>S|ZpsXPtM>dDYq;mFzdFUM~b(lQsD&T*FDxi=dR9I@} z!OhLtT(Rtz*XPjtfJX;%^Q~|Kn?z#C#9Ss+j9^4WwI5+@o`%zO($Ta8Q~+*zASObl z#VwdB5IH!rl7r(~U|+`%-Jc%`NeS{m#Ui7EH=+)os00gpF84+++&@AFcm0;K%=@XB zV95Qh2fw^qSH<}T>3iqZXGF^%%G@XDz1JP#YJ=Nl{ZDW^z}A!ds~{ECxaWdt5Zm^3 zscPV#l=p#mt4D=WxY*R+2>5e0;I4V8ng|vu#61DJVcdSWI_$r*5@A*#K`ij!DL3PUa&*Lm8^N*t1#78QkvlIL9n0i7^T?nGop*eWm$ z@yMto3%J66J_?C80WZr`@ZgJdP%ud%hdY5QHIDe8c<^C}u+MAEssP*J?|S8xPc5RL zhHtUY4+0mZJQ;OLw&Q8<@1Cwre?|gEM)ZStl%-nUtsCY;6_}`yfffYED-3;Yno2a+^#Q4V_XZ>1K*V*Q}FNLq{rNSj(f+5%PLz zQ?3el<8^R)Da{O}mlKMVWri znMq4uPzl(YS9jyf& ze<|oCd|sGeHenR9F$f_wc}h7bLD@_SA2*LJB-kdEtokXdXa}!T+O_C2>iS?HaA#aI z;p(|f*=jiAlPA43UFoVaT7NM_l1=xWrDt(`)jqsdEf-lSyBH@G63Nvzwl6w~h0aSF zA+0t!WZ1Xf9TJe^)b&x9eMa&ilg^s>WupZdxOJTKxdfWRLvo$v934QmHZ|r*vKzDbh8W8rt2~675WDFVJ zSpo$2HUVV&h-JW!VUXh$ymi32xFM|99$@#mthUogtE$oVIbhPDlo{C%u*d`v%fMZl zRMKwtu^pObdbajTObYSkgslV)fO_Wr>iz_mKSg)g2(Qh!n`0OSn@JDu)Hduu-Z2CW zeMkJJ`kqpcBqCJdjN%RJz2jYw|B9__3QkL4w=p+M#)=`;P>}&{bxb<=?#&qsp=)`< zeDjoRYHyItuTx-T1FrpVJK}d#_FZcat4W^wUvrHq^H5?~ir~cEcRCs>s)LjfT!ukq_6_=|xOmib?cz?$nFe_3+G2bZR691F^!khm+g-DIS zb!aGa{JNW)rfOQ~IDEWrkyF>7AM%N z9?zP~sTgZgb=msxkL#!?rv)S=HdtH~P~tQTEegh7YQsL}tt zO}}>rR0D6vi1J^p$V(CsG2^Aqu65Ic#6ysyet0$QB~UAKB4Q!j$RSF~F><&-f&n(I zW~OZ9NbNsg2xt;b)K(l(1vFB!vi1T_f^FaoyC-#eLc+w^9TAVD$u#HcAtel`LLzRa zit3DKXI@mD_fmyNCzUWV?l`qbRvw9bKZS#U1g5LI>z}2_x5obG&0BtthRwXPIp^?F zVZXo|2>el4PF!yO9{$b`9!mPf=f6*oqW0^$bhbUcznaMDIivTH-8|;?y!XWzu5wDx zto*IE;!ziDD)w)0>ve(WqW91C_aoI06SITFByE?U@i3!0&o4G(?~MZ<4&akuZeHH) zdUcdYU;rfpYLaa>G}Sg6u-xA)@D#erNQJ0=_?z@+R^`#4gODlW1_{1OfAT<+EvZKC zZYp<6%D1kR3}s(7T>ybjVh|vVgJYXOBkbFj*SFLl4?89QEi8#&jPSMm{nK&RTe3p| z>;VoVgR}bqZ%!qA0J^h+ZJgB0a4;Pv7cNqt)jGr1oiHDBK1^;5=a|l^yK4T;e=~ts z`8CF!s34bI?=g$TV(P*M^=92!`KEIBTUzT*S67|hgtV=oC7DOA_lI-JzeWOtRxrg^ z=%qh5);$F4xdknO?_*?GCxW#H-9%jgwb2t2A;6kKhEJTHZGCbQP-E?Mt>6ud zmV!0ukn14JXiWLz2K}44znGEY$#f{}pQSqnCM}0ynGc3iwQl zLfwV13x=GvkruZyyL{c5VTFd`v{Y_$Co6xA^FbjaZI`ptAAi_z#@aQMhz@$gAQdmn zA~+OPW(zIBX-cPRc9D>867r=%9-kgcJUhV6WF^H*g&KCTA#@qsC@uO7hPOJWAARzWikNln9Fxw}C zE#a)*!D)Wen(BPI*w0R6`U;eILnU~;OeXINnSvv?TO~a6aGGG;h_^{SN1YwJ1HU^5+Keh<=3G-dqhaL2aZa=dlrnz_`YorxFW#5%D)vL+7YM9v{F1Jn2 z_y8qgv^7%(pO~8zV=Xn9k2GsMKPAi*X2mhPp^0=J$Lf!~>8@!@!7j_ChQeQo8;H@0 z>r{VMm1B7MWbDXV0xE) z^pjT@muA1QxnAT#QYi=<6Kh+bQB9l{;HtYlx08A82o%2=EL5l^L2&(Ndi5>mI)S3Q zMbQJv{<}ljsNPq-Hxv`I4!owvDhq#tlQs%igKX>dwMWRZNd6$KS+dogqh%;FbyuMs zKycJ0sRkt}7D*-?DO?CpVB+bmxmIi-fk;sZEvv1(1UH`p9!q+#HbyW_Q4k&7PF#;z)utm~cIhLiWb`yscVIUyxI(79mLhGZl$1q6OC(!2_>nGq z#Y`>mfJ1`@yb?D7oe0>a9cCW~|LR~1RJ)jyD8UsEVFTQL1nO37)ku;_MiD*1tI|x; zvqedUWf`08ZO%B`n8oPm+3`w%Oe`CGvm^p2nd%b@agJ3pRo24h@7D4zOJ7H8aGtQB ziFI`g3wh+Qr_~TC$^j3UPS7lFPXKX-x$yE&a=E70MU?Bq&%|2aNLnPRrmwbas2WWZ zNX%XHht7P$18>fQn)X`~b;$Iv!j@S4YiJ@Y+44)uFQ14X_V;fp-%`H(>%Py2M-!Eh z`Y01TsefmJf`X7i5gbRtpB_J8zIDOG=fsfug`9|hM795m6=U+6wyy7Avn&opQIzvI z%Mv#0fzF#`D#UN(+r&)bEBL6p>JaT-M4-z{s%?51fHC^$izy)L*UkR8RU;7CS@h0r zsj1uGgLxdhaLKUrUk7}Of9v1amw>x}58M#)k^h{bfGi)cCiCyl0b*yxa%&EJ@N*Mh z59-{gmy^71mQ-LucWPaS4|dPD#;;58a`~&*>+jgNfmb)j8bJoljq6U-!`Fe&8JGT# zIWwv%2_;O_dpvX?V(%{BTlh}m2Huc-ODWRkb!mjY!K3{2{9)Mp#V;S;39!vZ7cOc~ zyaEwR@^s>55ySHXqrBGT6&}JR51G;sAg#=FzK8SlmwJEJnAG@VKkSDRXSgQ_tUdVCe*x)HD2C%kvx_!TU~1 zNHOF;?=)&j@g1bS8CGhfvR_HBvXFjqv(q!^(wIDeqVV*INsY`-k%x(b%Me^I*r~LF z_gFK$VMpMOZl71>xH6Ba@|)b{#a{#tY8y%0f$F5eK^qp~PRr3M3#20@Ua|J09#*vlDjPI=Pf7}SA=^U7T z(KSDjJ7Lu?e&Zq96Tb!jIbWyP7pa^;GR`WyE(gYk57liu8vnqV%Uo0mN9! zB|iyB(z|Ub1J^bI={k_z7S{&fKRUy~9ajpxJt>p@Sbg0roO{yZ>=tFe=Yg1hX+?u@ zYo{-#onsc0x=QlPkx*8^yQvZnNqh;X3O>+Dz2O>HXdbPQ2f)0Qg_`MRjuU4hy2L6= zS{WYyAcknEw{#=6y9J{`$d-tWK<7|IxEL3`nYvs;BQSVtnCsMU4AEAsdM^1J9TqXH_ga^o=I5aRO;!u^)7{-g;^-q5GbCNSnnj5nvyDs z1rQXDiF)jr>9#!Da!MDseWkd6$2_9eS(}ot6WyRxPbWJYAl&Jm28|K<__-0mUbu_` z{ViidsF*ovxP}5P%P|*kQ~bGx#RqRD+4CDW{-g7%Vh*l;0i~yBVspq@7@}#0>W-`X z^p?eXS^edC&;`$JjV-5U0N4IW%%XpdY5}02C-td)$``pkG*dt8`p%MPA!t3kWhN`j zSsj%S0YlJeGZ=u54wmhez>(LIsq2Z9{IIx=q~&8pJ^zz4tV=dBz!38nFhBgQwuHM7 z4Xjl?mMeXEmig`pb zbwCK}25^0M9amIMN%v^`2msW$CaT>4fNEUdb(R^x=M)=2sNYqP&fOm5CI_U38^iHH znI+U=RQ-)J7=zdd)+54d;(1YVd&Gzv@Rki6zQG>(OdOdB^}RdGjY3xodvwZ~zu9kOzdIrl zpTDIp9sf#BvDNNz1dlAiRabxVh>WRxxbt@3yod@*Wv;(LCXdhRQ9D6H$D*0X-U@<6G)H$JzhxC!ejX5)4<~xaOc$U`@z$_J^xPZJEuI! zi)m#bqCmR9?FhwG<}ee*hDA)q;kl&w_z9+TA?r>9O@HZUvGbV8^3@vEr;ZEceN_># z10T}ISZiH3)9M<@i@MQ_TaGgDLb=I$2*iRy8r${mB{I4J%*9=v~j=Epi#;f~bFdU&A!}jbZA_1NEQ(iQ-Xo~>b+j_)1SBeM zR?3vk*lj3uMl~$J+`O_=O;aRKbuRb6D|}#ueLUA2G8-xJZdxiQz0wOB3iDz$GD*dn zvTQHdtxyC;hNQ8fC6kQ~L#weDWw0lQ#WzBC?Y}$Wt$IcclOMxWxg4_uKOMmv2e?{4aMNd=Rc(_a_yX?;zJ=@i>7WWdEyN6Q& zr-30rn4GmQfmA^s3p*Y2;%`lx4zS^QUsDmX7ZdGA?oon2(baxz5ic&)gQt46J0oY7(b}%_x0?C7*A$->LzT1YGawcEz8W?%Cbh-*kt)%52Pr+hsRJSaJh7SjV!>ytjcn^^ zqFd61y2o0!3N-Kd{!5YKWWMm7ian!d&6%H_7F1!N^8PxiodqJk?=XxYcxCXg0&Q4Z zTgf+2S4Q~~1%ZC3q6=(h0%fa(Xq{`K7*PmLr8|E+?O^N8Ngrr675o5!{2`Le*0XQK z_IR8<#>j9*NVhpbE1?4ayxlmoDXk3psfEf_yX;n{Qwg%*#~{2kyn=Db4>4Jpp2jID zwCmN+gFx!uyQ$EIk?;eTXhsvxqPtp&hsieE>{)P_Rz;TJ;kSHT+L=%uH&fyWzYqG; zk|{75Mk;vGCR|x1t$DEOR+E@P1+F{VBQBc>XXECt&-Ik1P0+|Ti$Ro3VqjTf{tU>4 zhQQPsVATzM<)t{LC8E>;6TDOk|J;M-zkDMV?O&(R@V@5ee(*g#hf6RS;AwVC zaf?iN23;I5OAX)xpwWYHkEL3VTXK}+`tc$M5u?a8WQla1&nvhz^9<4nUka zQGgKO%ELQCiFE@GM)>=wGBS9Qty7o4w*nqkAdFh(Gh)g((o5Zb}*5FOr9xL z1i10}>H_Y#xrG}Xl@|s@82pa^*;x+hAu%v3Pt6}K&z^RxG__a@)w#gN?M=5`zpV9G zTB9&@Kj2fBITYlO74U%V-V@LYI?txCaWuJXh!ZW@@ZAw$d4!)bjn6r{Kx3s;HZ1c# zt!pjd{YjQlp9xuB;RG3SNWZ$PG!9K~>Yp{KzF879UX_loW_6t{YVXsj&YrtX{np*i zctSnD>a-!GhO>`vYtCr7>AICrDP@nB8;%?Ir~|1&*Tr09!4>vZ#(he8C%3-IsX6IT z8CtUZFs%?=GQ9L}ekJcj5G#mYli4zCy8368pWO^E1$c@=w4RHXmryy$e*Ei`#&ZCJ zn3}xDaz@SRu*>9<_aBGKSi%wZk%sKUW&w$@t?cbP)!15m?)wfJIpqhxJ;J`VG#XN3 zD~a;2INOJ(&CV4Bg5CvY-Tm|v%s}R^9WN9dx3~&5^uh)EavRl-whTCo_JRDN+s$*8 zD5weny3J;d&u-CL<(<_-d#%~!^w=2#x=5i1*jk|{zqSK}EYPx1F~=2v%1?k{Z5Hn} zGOf>L3CT`+bH+;{OE>k<{mhW1F#F&vATd?1ZGzP@glh9vR+G=QqO9=&< zU4M(iB%J$@dEZ_`8hh&U$K_T2>9JuoVtum{Gu5RKu$`kW&=eJ27%id)0qk!rS|63vZw@p+I#raU6 zrs)PYbNe+`Rkz>FMx4sA6NIurAT_0865S!8qfGt$Mq6?m42D!PJ*R^*(rQuMq&1%N zK?SY{Mm{*IN6BUK9TgQWFzl9x$e;5mH_M4PI<-rsvvJT?5ggCl#Kx0PTjA6ZJrBh& z9a~qVEP{+nChqAfS>wC+~W$>5_O&r2##l;7IHg0m9u|hd{DfJ?`lLflvyNK?$Mq+C_ z?EPYE8JFK#K7TYCiSU%(@MMbF^NWf=O}^Ty&G^L(jn~F-4|k6-kBXoKHOu z|5=RlUmx4EnbPL9zi$iCmz<_@ElBPc@k}2RysFS zFt^~p-R&(%{>39Q9yu&42B7HW=F`3<*42|G5C|vk&EyB_&-1{8M5E>;BO-u4frMIk z-Rit`YtykMr{;Y`5(&C6Py%(#O{=`}`F1Ao=+!cIeD%$T2HK>47xpWw9tw#Ep|1A+K{*d1q;n?mEw8Bl4UFL6< z{>+6{Q>{QpvIw`l++yZhZUolm9JiS6-7Y6;_bA!&7W9te`Q(1g9Ja>l*yAXNG)qnz z>yKP^^X~#8HP3!>P1hF-#^9R>r92h_3LePct`wrJ|NW_I?WxI^{Y&3_IvU;SBsM5GtVCl= z{ofAT3(B~nafo{Do1eLOw{zJg=v}big5y7|j>68P3D4L~($kyuSx= zC-?S$1R#f@5D8>_)PV$j{GNt2Hwz;*9*&To&%Z%auoO3XzuQ z?Vvpk1DtCpdLsa62yhYPgFw>X7O%B&g)2)jk8&?{lfqM8rla93b-DQ<1@89xy>N-DEK}NLx;Q5F-yY$73%B#KmlRb!3=No1pD8v2Eg!P0s z<|9qTZSsNQt8W_A?$J^61m#&1i9>O#)a-+|RZ82*Cfy7}}J_+a9~0FE~$+i`qU zf380w>_7#qxfrDFkHTo3dGl|1YhE>PIN|J|kDOfS7Iz}a$Bk6`c)aKIaprebXzQBV&oVH~+gK<<70 ziZc^GPYY?RKNoAU1;Msi({yHxk~rJ$5;+8vv~qEK@w_4V*7?)W$k4F+ z<_&+4BQG3v8;GBLF0S!Vi3gNi~yt2#Cby6%1Hh0XU1kcU@#`nA&)sCri2`+@p zXv8_pi5INvU&6<^?sM<*QhW|-&BOMRx@$YA4qbnKN_gi(Fc*!uO{*bqCx{}ENkIg@ zb(m>EL(?Jej#eZey|Wst32HyX7VP~~`)O$$qr-g;+V!I$#;ogiFJ1;Q?qV;^KNmHJ zZ~}^ui-hy>@h><87>M>bGSEsyzM)HUV&76GS2IFMPc>`()aZ1DIi*6*73IYu(`YGu z{T=ZocFqlm-xnPrWW&8to0UQ-5k4E}Jl%&GY{q7K)G9(0mE75E<3)s zPC7H^O`lHPXHDYxt1rQyR!Z^zrpX{91IeOtA-?gQQGIwXZtiYpv2EfeSJ9(pHacIX zzDJkSu;oyCO~ zPxSSkYSM+$Jmw+XYkG|dX(+V5tWF`09jBY7jrdMOzyXn2t(0cPGLBC3o>2?uRDN)B5a-x?PD4}xS+#O~~@N3fP zhv-pOa4XrQh%$b|ucM(Zax0zSa+(ey4oqg?6(0W#2$s5+nPrVKr(#o8H!$@_LhPM9Z?aO1d+#$ zDgf*;ofNV0y8B|c;!-WZ!-gB1O)e*2(aq|4C(1VnfJ`?9Q7O|zoTMnq-rK`3Vvm2{2j0F?8f_Ay%9!;< z`-GN`Gcm#7=bfDe0tpa3YDJhkmT@dI^4#+to=E4o9(;KHYy5f-->8^n%M8N7oCB8u zmwE5~{}#Xf;2jv)&SFV=%|oOUtl&t9U`6x~1gVl9=?q9y>6q_+) z)aC6WKv`;Av9oSUd~tD6@m|e)7s}@!=;AWx=&O_rPfhK3i{R|BO09lkZ+V(}`Gc64 z<;LWbZa0e94whb4OgdIe7@;=xYVY>G`Q6NK6u1|^vIWnLvyV682qUKU4_wLp@PJo2 zpK9_Oeyf%2(dw#+V~Qt%Ewj094Obk?RinIJfCJ z#-}w9NPqf3#d8)m`%*|-ScHkG!;JR6gP4i-;_y$pTR`6rA>rCwnv54PjMl-yU2xIe zMiDu@<;5Kbc0R6)3QLyiGMYARaCD|nYC3B?Rq_x3CDyUg5?LPp^l%cc6J&3jP$q$&?fa4Z%$ zFY4c^2v8K7R7((l(DX`f=R!OUhn;G_(x==wJ{f%S{EEH~DUFVjUf76RtUKS+;Lsl8 zgw40B=s&U+8d6ELRdto)2(j^-1~oVFJhkdw{vxDCkm@Tj>GUT+=N$hVlj6)$bC=+_ zTGK@ zCH;CrI-ZN@{4L)$Zl!_%eJ*6)yd7KFc6j3()??S`07h|>kLuz<=#0OjbxqS}1*_V} zCfP=mrwTSgo3Npjf@tELuD|WA@@WdB$+3HjPzG+TwJ=K;U5nD5miS1Cc?=Gk0_e3s z82A$FqnRE>MkbPq>7ihrY`AllsEW#-nkJ-z2V{~+#$b8y59MX4lO zUFQi=RvmeY361@nR-=wihwkEyEAovypI6m448#EgkD`hPKdKHx8_i81CL)o>Paj_8 zszD=5D1w`?DAUy4U_{^V$zAkGpvCPwJv-80kF5~|Sy?2ws>dC?CwB6B#b|AQhP3nm zg~J??_<1`>TV`mW{k)cL!tt^b_aj_>5lYp+9ZOasLy|Ht! z<6PDf(4u@jXX>v|{&Myd(7JAD;P7_uyl(;i>6LdzyF+Wv#=*s(lcLM|-KKpsXI5fk zfsZLz6I}uRKF^0_sy$EB_q8=n|4pa@A3ophdXpnC!g?-=bsSWE!&)eN_Ug(bMV}Snn;9=&I zZ)$oE76S#Emu`5sb%Yy;_LMmB&jv>3rs*{>hGNmPEgMO4?u#4Wf(n_PRbF}hz-pe` zx}y@yEY4UiCybw!;*1C>0Vm34YM!fzH<^LsjBEIAo9E-L;^bPNN|UFyoA!pvXVU$K z_;f|X4UAJ!qgw)2y6SO(Y*|7b zPK~}YtxH70cXei{_T6qgE`CIf$zM7=ai-n+kg8HQ${Q^e7X6EGQMN%myRS;Y0cQSsJp1M73BjINJXkpAY;v$?_ZMD2;_PJgV;zdGgx;pD2 zBSklQ4zNYS(s4BDWr!WaG&=<6{4F;Gs*A2x5sodr4TT^lJ@%+FEe#sct;~}x6R_yG z7I{=Pkpms9Ip5&2l+-PHs)a9Ut{yU%@XZ1pBfxxv-ZI8Cy$Qz(TC(*pqbt zPPHq!>P#+~{f7`@2E}^sTj??BwdXX6SKm7{l1wNIld5D#V}wH0y2{+qONDyd9+S@93slm^{k3^&vw@>X2%rtmsw|fyHDG( zSJY_fTO0@E48s1WgDhAJzHQP{?)S;48+e5L`B&)M*T0wfD&Zsja@r%YjK zf=&;Hm+prA2_tq&|B2@Q9laii-FKe%{+s9(3HbN-`F`v3zv1E@)yTbb0m|qg=?*qW zPqIv25hOa9i*g1~Fm&9(a@fuW7h;hcal~%i<9)SzuyM|TomJWt4vkR2|H_>j91L@E zQRPt9$e&*d@apb8^L;#7pE3R!@R>NFyg27{A~yEiv-emicN{PphfO=$Z!pZ2;6tD? znanVj6@K-2(%ib~cXZy`>*n^%2y<^E>v{g~W#qp@XRk?orQ$@j9YiX?;(bo+IiJLu z`+AryJcK@qfd>TAh9LWl_a#EJww;6krV32d@LA8I&t+7QsOTbou~;elH>J81=)LIK>Ej3m zAc%)8-yPo+CG6sPvxcnY`2NOKibp#8irNzSSa#Xg&Ztbty2^A` zUbaUi%5iRZJDSzXWK7XBleGvm2e?NmNYa?v)W$y-{m0ZaKghFym5`-4g3 z(iQLbb^024i_#XL#1lL6;|P)Y+ehv{7vy$RlnxHNKUqA%Xs^+Y=s{{4V%e^)UpALm zs$D%*zkmU_$eZQWRVgA??;;i|f|bb7yYMlDb=(RKR1r=qm<~=TTJtp*)cA7Z^;L9- zrF&$JIg4ryY@B@M(a%oWEdDT`6UUeg@GW<>nOtC^<5A~1m*=I@ZH-p6FAnJ7E(jU0 zfe=wlj5b(6lLlWlj3R^)$`3wJY*Za*@=p_`Iu;@|pC{;=eQ^Clpr4H4LFJ#6PLr3m z_ZWjRM#9PYfwBr#X{2q!)8IKBq|+G|m1Qpwe=owW^bQnrK+UAM!|B} zlpG^ssu08?=+O|&?bS9f%fmnt!lZWU?pd<*C$gdWPVMcNyaQ|7Pg3NM3c=Zvz98YM z+LWdh>3DU7^+^RuX$f>UHYg@IfGbnlec^?88YGE03QHZ>;lM@$mDK24(uLYo>swKd zxUhjSP!fR-AB1C>9Q^085P;t;z8vV6%coFjdx6-TqP&-6muvLip$4&$?v}9a_>MR6 z=a`@R>3~a|KhDGpVZ4^JTV>?BTBSet+UuZy_UoK!J>cv3?mnT1+AH5c~`&}!F zhZe0=Rx6q94lzt{epfjoAO}6DNf*8Uw~hAR@Avl%cu4zfz1DR4~LAraZ;L$`XA zO^VQ_(@65jaTIpHBoj~`5rhmeLqr6-wPlhx*UTqG$r%s>6-=4MG`lVD>`cdcq@{|cq3 z-{tXr4I8`DC*xFTrch1aKn&A$><{W)O;YLItoDo1+j9IQ7jpPpvL`qO?mjTEtGyjg zl2|ts!$wQ`DrA8RQA*_^Vv_Vn1LL#{lipx((fT5HYydcXqW0c*7UZaghp@U+0GoUjF!Qd8ma&UWrq< z>dG0mY5%1hnS?pavmv0*UbwyvpQ>*9UXR@_Az2ZAw^p# zdwwK(QeF{*159^!cbx^426*C0d=+4-6dZWaBDORUL{rwEAC$wf_9tPZ?qD^O*3=IB%28D^8o(GmbBi{ofziv;La44(G7R3O{)?V-(^qe=G(i5q9;=ZymbWhQJZG$Nm+%q;r?zVV6| zef^Q0R2)l}6I@QZA`f*M#rbSq=y~Yy*AtY9)*y9d2N?^E%Ul4&Fu}shQH6bt&ni{x z>-K!MT*=w)HPU%l?hbwU6WsQY^_R*7*1-oU2Hajjq)f1!lFFrYR)PTG%Ck${PP*7 z>{$@3m@J`=4TDX|+ujz>O7C)PvZK&VaX$PA4|NN(n z;kq!;Nt-?bHE4&))am%OsUgA=rCLU&IGmZxw<1RxIdqi)gliUII?3zP&eeJ`TZg`l zp~4ze`5ifyNL&j4yAHFX?Ey2CI9)3_p-2IEEO@Vxsc{lw+Vp6DO9~Kr6Rcl%HsM4J zsoZb=K889r4q7B^EYKlC=D19fA^#breGaU*%3I!hLo}$3WA8|FO)we@1n~M{y+F4l z06=kZQwhOv747td(+e_?q$xE7!Q!xk0rPacj)1Ymdu;84%$g8{oY(5FwNAfTu`^5n z&p9}D;{XjV9(&q)y&8%o8c6M=ee%zCU3CzWzzd5Jcd68nC*=tNZ95kwfRhv1k>~Zw%6c-f3Q49&5?fGX)gcl0I2#p4zJr7%Cp80@RpTC_WAavU_{`H8OJCoVea)s&n;> zonwCRwrjm8D;5$Qt~Balg)3)8S=#qaVy<~TMKdkGbaQgizQ#nneM6i$RMA2ON`W2L z@2--$;Neb=E#s1f1Uk`%PpUq>d-Bh12Z)&4PPw#)qMCgZ+)?vP!I@Yn-0BKa2tu)o zzHi(pOI^)!fX=qxkB_~HjOvtceFIT{^Deh2mK_t zfLgEjM-a7)qP4F-S4o2*eRlMK@JIzvALd3-^^o%#dQz1^K(uyj*F^1Fm}P3Xwp>8s zYA56a>a}0^PQ{Ua*i;=UjBuMt4JH|`J4HEcL^M1M4mO}*kQA+=vcoP`F1try zbOhn^8GBBJf7eTcYfyHkpQ&G&%I6;&J$=+B4Qw_8@Z#12gX0P2yCL!_(PqkbOcUiC zp7jl}NrTOJT#@u)P06|xo4@<|%3?Df$CX`8!|Lnb$<|#y0enmNm}Ek9$B*eBs201M za;PD~a_P8)r$5^inH0UV9AzsG4lU8tRr^zS+}E|}F3OSZDE_{vvIIkv3t zUE64SW>lwkk)^h_N7b3V<3sq!E4WW0xan%4-z%c8RZ?bL_pSm_&$r3djd3(0{TrH* z?l^(jk4xU=f{%=_Ry(^-4O`3SisPh!2GE8_oJvz6p8!i zAfDgH8qI;SPVH8D_p1n-NemZ*BeZGJi)S5cPnuGdKR%gtu=O*jd-z;hyk)OtW z%ShmYdJhU!1ATS{vEK$glhQ|=zWAl~ zlfsg2JGRgJ@s?Q`Fhg(Ux|amu{m!gPSzm-0=#i1N>7ojxoFt}Pa*3zr(@61x!vg6} zru3K7%eivY#fA>P1^Yv+*(40mf>p#@h}{ce1JHJ4Vu{Qln{K{}z$R;#;54--c-Sro zfq>a9@92)!5F=4IiQvqj2&Es0TJdEo!P309a2D4Uiv`>~^$P@e@F}9!3rn?H!RaFQ z@$XcFd2@Sn8jS-tmA_-!*_|j7qtq!cu1dwDRmI~a6mN~Nws#hM#>BKWFx1_WELIa_ z#VaUzhL;E%_#}*{_>c^9iTBJOn4r6LAVWeZYq|^s1%iL;3?&#!1fD3R;Bs063^b^0 zAj09;V6jRQG3|nl#U?KB9V(LJ1}+51k|(j$PE(VHVVMbB5KkB8R}f12>pc8e zb@)%vp3eoes%i1p-{alnrN^96nd8cYmGuU zW+-77hXKZkYno{aGN%-d2Wc1`0#GDe+$j)9H`s@LCs0LhMdu?hFdrPygLOTI62nS( zW(;XuL+S%+g+L~j$tb}1dOd#`a1Xe1+^Tw!YZalZf99Q>o!zD|Bo^Szp@|vHARr}1 z19&A+BiA0z&(C*V{sKqa?#GkJ_4RjwMxml`sGy`(GQ$l5LM!(9Ea!LJ`U)}qy9b+_ zuB~LHig_SZ4-f`oTD)uojMF8E`_cdeK^YP4W{@lDQhx@fcYuQ7+9^gkbX|S<1hC;Y zo$~P{OS(>pNh0vG#+Z!XVD@3a7CfL-k(}oO`hf24d$UF+Ab=g-zUPXt^FKL%PmuZw z03@CBFJvd_$P9_9&M#=L6*esHYHpI;yvCIs~@7 z_X=a*t~s=8F*H4u(>hIoh?Z?EhlHT@M$WR}s4~|S;hV#+q<6_Bw+(YEyc$#Z!{EZE zD8iLB1aU`%f>GR{xdin!MGth4M3G6*MbtG>(GVI5xG-DPI*#Hor8YvDC+p}7Ka>!z zdH+*m{S{0x0T=vyGF(7?s{136-Viw;Q;OipHn>W<^;`96ks=&#FFebGiyVBW!>2-f z@f!Tt!|mFo4h4b8$Y*FIdi9rX?IAy=wpP5-;^4PUy?VpOj2O)=4DqzL8>+}EpJg>` z>+PsyFIsKl$;P=@w1!OTfzHKB?UWLzn|L9+zS#+6hQ>+W>se-mfWvy&SR8D|X!2NK zf+7EP6@dfdGme`Cu#< zS+0}zDh6J)|0hUf`Gu0_)YSFHU8@Kr0!kXu3%JxIthjWtVF{YG>0n{DoyQmXds_m( zO(*Ln<3DR8L^L0pqNLfcH%Qm2ul{kGK+!m$0BeSj4gf6`x*mbnerrosh5a#bj6m7? zXqKu^{4KL^rHw$s9b4krz#3K%T~&}UNfd37mGZYB0Rn&NB>d0A34<_dTnANlu_`Z`k10zwoQIDml*{xqY=E3d?R;6s}vX zF6M+S;v3#Doye?`WYWR~121SiRt4r<&KSwTs{#O0p>n zQp6d+cc|b94TPqDX*9-E#0qk>ZLi9QLn+&HD?Z7u*E0jk|EKy~}*ptIM@K zOzwAjzXIya-$eDdK6huC=7JmG^LADF+RY+Md24=C1u0biOf?rq5=%2ED@q_jpkk{6 zLtpzf*Coc(TO9V+sZ3`D31v)?g^W{#iHRQFXOIj%f@0AGqd2#l6tLa5wYN+CagEC_ zUk@{Yj&4-ebHGCIkrIqT^=)zzz}L5cZ~gW8{uS`Ab%z4({vcTMI%NtbJs9|wbSExU zcnM(&)v3NVvw|exrZUgn2bt*1?en4MyWR}|+ z9XJR8V)p;0Nz+4<8!*!NH$&BIQe$mXPDoeOby+8nUd40IJg==3j_$Pr%vk^3XvCk7 z)$wq_n4)b$D-m$SnK%SvYC0EXKFh<)S=6@rvo5R9BjeX-XjK!t2S1xyA{IJw;Q4UZ zsfzJg^m1J#$ra-MHz#50H#6d95%)x@f>vcF*SI}z~1tPB3O|MpzM78N(2!zJ_ zVl{cMYEGeQ6&3N@qgmj6vDeyoH> zf&_o^-6ZW-Z!<&>*1`fFa!wx=>Tc9Q?l zowrWi&C97}A7{+0ckrGO7V9hl^^;VxmEuy@*XDPxJUTb^+pQ*`60r`dg265^)VAg! z)t85b`#6K2tE6Ofy&awr;(cW8PL1W0b=YJ+-)^;`1yUv-CTRxbpZoeSJFs;ssAk$> zt_coItZ7?2LGq4pY`Q!yX2m9@j^2W}>?hMLtrv3jZ<~Ehwv+XJL@!K4F5dS${IEUO zr;BX-@Nip?ZuZ}XcKHO<;l7*!oX7dxmz(46SM#60e_s&VE<4}hr8+Lt6rIF@Kw!sq zH5p>-S)mlgz#wWc14IUFCR2WH|BNC-CuLZ!1OuUi*@c?R+htg3c^ETjn;Vk)%E|-y zkk@4d1O%R*b5eh?O8*x1O1V}8wYfx2|<7-50 z_viU}mC3)6pJkvxGu;Y0$b0|WMQfm*7jSCcG)%wk_yMGO08APloburW1S3SI2c;PB z_*<~|bE^1MKKyg&n#(; zqV3OrSgqCku}1)4Ztvplmu?YCEH5vwFjpL~oI+qYieYH~^^9Zned=w;ms`~K>W-C5 z4yS7B+3}8}VwJ#&U<8GQYaON0eolwSNR#fTm;3HluYh|m0R5(Z^Y6h;C40|yLC?6r zc81${++pD^_l*45RHxVNzRKNjRCM&!i1d7(_+6tL)pU@R zBvzz;*s1d9sf|rEda(vsS)h5k^iaW0o##i6OE0LK9s%K^P@QCKlbNzxk+9j4lSnWuBxGbjPYi@{!o!tf$;uoiS(`G6&*Mei9(z51Jpf~s}-TUakKa-oZX;k(?mT(&E+GYKTujJ=dC?{%3)E?KS{&3tB5B+ z3&JI^g-?DopAsU<`81VYs-+aK~URAR%GUieH9^BdCthd3kTN$5AW; zg-Z!np3+u*Ie3VyGQ>^)Rx2IYZ@vSMP1SV=czB$goGvb~-v2uo2liP{a-KJia9Uxk z@Tj%Qfd=Sftbb%6Dj>iHzNhoR7eh@Xq;t5luqZ?Bq_!`G6=9j(^H$#`nQ8e#$0MD!Z*pcYO+ z)TqR=yUH|S3n0+4D3w{(Xyfcevu{BV6(y-WC97v@RAewP9$k>sa!(PP(f83cXfQRw zgE=IbP>2n8E!A(lWL>6 z{f0B%+UFS?Wf6Fw=oj@1J=gyLFhhf=M($rzE+ir|Cts!=Rn->V9sl18u<3lA!Sqgn zoD=K1d7#?M#l9bJ=@e9erO%f!v? zcI=Yo1mFM_Y9`;dKn*74&ZOW1Nq`a_3T5!+z#t>1u_$L%xDXGz(}m1*!m0_fHNU!R zcxf#nuiM47s8eCO;#C6ZAMPL+Q+jlNQ;SQ+!7MB~OOVWi+{cO;e&L0ckW>ibR$D$; zhg4OGA!w^o<|S;vNM*Uit%SI8(J=NCK++QgW36?cem>V@Y5hzNjg0p~(i5pWcVs+@ zD#s03exLL0g)FCZFH%c&ePUpE2q6g8RNnhIJC?ji$!ZpPF+FaI?9$lZn&j1zoz`7J ziU!7^0aMRuUAcQ`5RWQq*xQ}-ZRxh8q&c0JxI`%Sc-=edlj`sd9Tiojywnn!(H|Eq zTi}zYwXqIQ9%S)#n!R7fl#Sfkc*8TQr9zTF^sd;W;CB>0zU0tD$E#4Wx{Mv0b{V3t zq)e?vPJ~KZpoVIt>gIh#8`c7ltjj&8weYqt)<2I5-rW4H7^gOWZQoPTC=Y~b6 zQP7kpED%SgxpeWZVYXcP{!L-Ffq8krf0}E5Y{2hP#@v4w_qkE@UU;3NBdYC!YI~vD zCkUE2AapboR0l0ai1wPhV3bcVWMoeJz0%ME!G{oII`k>U)<{qeL(znaZWw(eCpA#t zV}Yi+lYl@&J?HuyYChIezinb{Vp5zWfdXfvrsup*ZYbTL_jfU3E!=R!o9@3L%l($$ zW$w#yu9Nd+2{cvf@k?QGj@uM4FE!Ek+A62Oehd0v4-$1hpF0D1$nBR4>~6N=g+7)= z3zywgj$E&2(P4|x31hk=(G)F&R@jV|YiAb>rT}D71Y@)*KTTWK+~0>=ec5a@X0u_ZPjMi-gK?9T&{bQZ#y>6yQH$R zZ0dJ4m3ZmUvTi+X`uX-?8TdA6@j1Wd_WAvjafAf0MjJx4(@V1cq?PXRGW(qVi`P;L zEZ*;@iBvdwsy+?;o`R7xCV`qa5!zHJ6F0<#-M^q+{`p~$;|ov>Bp725sg)i7ndJ`) z4?Rot()gX(Wh-foT2~AKGP47{YK6up1wBbmYg{_H3Iew80jvDs2Cer68q24aAH_u2scW#)g>*8b(D44tZy0{Pf*M zbIN?T{;H1(CeGvuZlI&xeD zbRVR0@2Jdc)%S2wh~77#wqb6_(0dp#MN(W;PYPX~c}(5dzTV%MSKBKt6h{&<)6&b<9I;-e zGa)oVH`46{53FUeBw&_WWFSE8Sadoc&yR3pb&saohEu%D=Rcy zB7lrlFxf!{l?WNj`co3;IgQ$n@dmJ9-7O0Xw+#UbI%j`8egv&!(PDvE%6fviF z!`xN!rMRxLj0`+3Wa_akBP+w77jerhZYg%>V87d^Ytef?J)IDZ`KU@XZ^duKJoG#t zox-FLpF@bKEMQcMA;JUi=CO$f^52eZgQ1u;_ooyc>Abz@MtWSn14yvpI)U249`q}U zBs{5Ee*|M7QqRWX`FBGlSW#cDKTmNp(Tx={J*>$c?`Xu)ht1h78TFCn>p;-t-&Xw! z?X~kzbDTOZBG`t7pDuL$S_ycS0PUR2wZAxukHbuVz>%&`VNLRV*KH%s{WSy--9Isl zz2d{f{_>wvPVFobW+>nd>kyPNOO&Z!e!1_Udfwlw=>~EeE!!B{$4t@bU_|gydou`z zLCqAXfaZw1Ds?eou%qv1j}ue~@qN^8;CI2*g-N*F@bwGr zdrT62!3f{d?Hn0!+B-XaU2gN#%Lo}~er4jhGh&ZZ5W@8|P!*rN)8isG5AeVJai3!< z{`udW%u*lx|R~KZNet--;!eiwcp48RcHZ&PQVkORd8@{fbtZO zQbg6zy+!xyCJ+sr=6=u=bo^I)_MZQ!N~(cH=MOvRvloq z`=Tk~=})RBEhgKyCh8-IWA*Jg3GkL5cC@(hb`Ayy@3(yJ1O8sVz5uOQCd(99$YwZ_ zZ{0?u@i9tsdPx~<(pgIl8HEh3mE@$&!y+N1=b>EB6=e$^_H~hYAI3MsnM67vJ$1!= z{)ASCxF2=b3^=4)PHd|lk8T;yj{dIYivS5i#>?s1ekYw&!boiKQh}N3p zW5R`+mu*FMo3WIBt6K44_!9sW#v56?4;*xC68q(kMs+Y+PwM(zGLGtg9_vKstx|Re z6^Wy2N@R(le=Mtucp$PIyoRm;d+jSm%>8*??CjikxZdtOsQjyctJ@o@RJP&4!y$PLBOf zwGAWJgd|}Rljzz`mTLzx#q#d+zEOIs+|kbg5zi*jYMS}<48rZ@Q<;Ex6*@bTS8zn| z{@I)Aj$r-gs5|SD&zSNfXxR^PVl@^fnaX^#ig~}mMhB|oZ3T8K5^faP+1PC{Cpn}X zZ747lTPtjK<#vmzFpd<7^!ckvs&4wpb?~6Z^eZ>)$r^ z8;(B`l%-*xQv|!>Gm2vs*+gSrAbqMP;Ofj;;kTC~j{RZfOT(KKWrzB`!s-d|Yi ziqb;?3U?)JyE&fr4Gr|^Pz#|BUYuy#Evz|vfaAzE!%mM7OoQHF7VSmsEI&M_QCTUL zm59v3&0;3TiUpeRmAohD`OI%kuf1f|%&OO`H3XH~@UDhvBUeiPsn=2a9$wAYTIub& z!W6Ah2YvYxFuumNO!rK}ESEG$j05H}6Z`wj8I>NXp;RWXGQkw(IXIT!MpBxEhBA8|?jPSf*M)`_}ZNK&NBd^zo)%p6f`l+9uOlB@o8wP|xNU~vE z+3|fy{KMhlVL!I`zx_mYAcDq~tc8Fz4Cg96AX7%GSyr(VDIv9X*HN|)ge1n}VY82^HH}pR(4i+32G3cAdj_# z)lSU!^&62sHM-?FlL^+qkCZ^_J3;b>78#PzzuOZ9L4F z23075wyUB{NFBbU6?Eo)Jq+9QDYN?_69`!Y1%jy@?;#(i+D_ZE7)ueSmWFeOMyd&I znX&9?gjBt!PR)oO!zemn={P_B9P=rRCU1K)fhviXz!o^Us$<(9uMvRN> z_fd$_>x{npjN3mCF9*hx><+$UsqfJqT{HP?aikaSaUzfX={`b`?*c+PH}cxMy+0IA z$FK=se%UNp@qHK{FH3`HiM!6sIzEP{h4)_%+R9L{lu+~<#TFgN8=|e>jUlppnn`&5 ztI$6m--&Yr-$&52lKt)%!un=okXj^ZMwAK0n%%3*Lhag-_xX<3`E3`e00zS#7ch@1 zGM>&4nP8{XYL-(t5Po9By*BYbAvRfWMRSc$!)hw2HRR0dVUZ-aFQAhdd$*A(CLI?t z0?%@i!rJ&bCyGIPw7^!(ZdjFImMKK7h%JB?5fabl$j12rjWB!sqHd$k4i|CGR#F?0 z)fAUsA!8$az$=vim9a&$3SxE_pDmfuBj59Zaqf2jWdB}#&_g-CUFQ8QRfmxcFB zv*WRI8SlJf^s(5lmcQR3dfOK3DND*}PrA|eg}=+g6b?}sc=5t$zb(RZO@Y!LmeVsB zAk1-UvuX&&p*m=#<9Rb8AV8?leZBf-reKjsQ=^5 zbgsYe)4$bCSGu+RiS*V>6j&>aPEkf73B5EaLZNR%bGsBXwJxd8@H+_2ql_pJgO*1d zQkl%0N}?pg%>T%9)VY2R%ArMdqfrT=XaJ#sQnW&Vnmk;h)~+wL>&DbYeJV1>Xlau} z%){k=G6*B^h_1S5$o9gn%G}A^c2DKKKLFtXIJ3RnF26SGh+U$Fr_&RGAI=)o!>(O? zJZ5QBwf~c3ftG8)(>?zF1pq0KhX$o9=a*T;_e~V5M9ti9S~Z-vH?DeSngsa!`}+W5 z=6SngEbL=W39ul>P3lT#eIrq1lC)w1iuCI6r6xXQe7E{&f(Dn@!~Jb?>~?W-Pac@p{`!V(3cME=AxDy-3+}(ZxVvk%xDb9k zTwU&WArIZNckXOnKM&n?@%9<-vrV*fPmWW9GR4K&5qMVlYAdZe@P8{xf)dd{@PBEw zi>SeX#7%-wF)##f`QLBmzTQ{^F6m@e7%ZGvhDgB%&DeL)*xQtAaFC~A;T^AX>DA)B zjDxY9HBMBPfKx&Gx&x0fgT8S9cZt5geY|?bLi@RIFhx(h9-1N|wIOc)wSp3m$bO6t zcqUWXDdDd^4Kmv4vm=MFvoiz=q2>d1YrN`__&kw1cuGh_I$|at#rj8SVgu7h&qUX$ z&E_R*#B_|yKXSzs3QwMR{-Q9RVr&xWXGV2vi?!9tE%VfxR;%s+ zU7zr?k-fo@C34zYh;R3)uqsn?Wt~KU>rV<#tkn5FZk2hoszIXcp=K$Xwe@G`wi(?Y z>dHgzMYX39sx%HD^Iu0 zKY@Ea3tnQ9hQ=l{UaDRr8G_+NKka|1x_Z`p2L0pl7mx}&- zCOIMK`#l`7#K~{H=<0c=Vj5ypO^d49QRWW{5D3ZyJnO~ZzldiUskFX-BQ4o&Ex*A{Y}j#;@e-3jWYEcGBu_8x)Gxpd#F}f8Am%7WUp*uP%3lWo*Q6*Td1h|bGsmT=_T?&aIHxhL_kPd5=5}I{BO)Q8M$-DTwEFG< z<(#waHNumzYyI>7&?nIp9xELgGZQf1SV+@bJ>hdEuz%_P{XfU2*sE=6Dt<-wyX_L* z0dOiMa7ri!Nae9_AAezYzn(LCc~OfJkPijJ{Bv>g)#2k%wBatELR5m&+Hi1CX=rH_ z;h-Q4D2)a#4H84z^gb=Tl4!0~C=3NMp{10rB?VES_&`V}m13kNfgl@bXb_0bDV;hu zI5_Cpp(>;G$5}Q{_vN2)paj{9k(!{7k#>UTdb3T%JY&bGB8gaSCOQyZXQmA&Tf>G}45zyBh6 zQuvS$Nm^sOE@?_RB|)$g^Ne}kMR6tQN6He>{ds6Z6tx7ZWJw={RSWc>^GFDeSZHPA zB1AHiLBc=7_1Hw>;^IN=`OFRn`A}dg0~`WSCk989cjaq-Jp7t1Jiq%uT(Z92zj^LT zjJ^H^D8k}Y>F2~LvCZH7nXq?vtu z1S$YNjW;Ohz>3&{+y_gip1!+u4TY5d~PZZBb>WADK>F^B> zeJ$9;;W=FwdZDCfX&!uOwAwTRVN(1g?Q<(JuCYn~=x1%75wQKSt||SU#_C5Gb5-l? z{Sl(^7CDbagKZDjX*bv}^^VN7fsxfK1q5(e3_5~2s2{Nj!{c}JkMQsSS-($MV0i(N z4;Df6d0oZNtv}FgMy1arMP|QbP(uYM=w`ndjeR#;KR`n`jsSHa9Z5tqYE3ph7N^q3 z7v{X_DLCF5T(Z3-Wa<C?E9@n4ks&#p$h_1L@-s9v;*?4S>R z!|~}ZD{z&6Do+Tl;!5VBOn(EnR+<;w@?({t5M8&Un?llotV0p+`iDGaR=m5I{2tDG zcViLjcD}+tStb!@p09FiInr!OA3vWIRSU_l60 zXf<*$p0*le>3BK`GwuwZ##fwZjP%9yNIbry;A94NPY=`O#?YlZC_Vd`PEA*}Ic>|= z^{6bRQ}neGdRwDTo7Bbtqfe)-AX2`!j1&9;Gci9Khni=B##Xpxl*&1!UcKM#BCV~M zXh{pRNo4BL}^esc&`h7VeFPf=aq+1@04ZUl+ zE=!|I@YUk5vS15YHC1HoH45KmCer64l2vYhIQ>NOW`i;^eDefz--H!@1A$NZ(}$gQsicq{YKam?14J9 z;=W87%`9inRI1H=L`DFOUqrAS-F*SscoS4vVg07-^St*9#ep+>D=J3%VR>a!vrT-t zC%9CcBKZE_W+!|m8VYsse*ys`)$2_~O_%S>WXs;`@JgnKvKtw4Fc{84ck^k}xllo~ z1Zo($x}?;xKA$oF)?f7e*yoxm_si9|0#E%?3hP_}Q#f1r_9tgu1{^NzcoG7XyoibZ zKXpzFDJx(UP>|0k@4;A;k{VNRASx3*0GtX5E~_o_p`lKqOSY`$2CJd+fKeoss6V|` z1(~|w46@IEo%E!}L$Se$*W`UJZzcE(z0BPb+q}d=LCyQ_iMq_Pg6Jx1A}A+QN*tUw zdIYD5Q&dbPO|Jq-2f(PgeCg!;;{93UmBCs`!M4EC@a1o*`pe&8b*xw?@rV7y*QbDg zvA|W}>R>$8&Q9i}aaw4etYw;NbMrs{;KlX%m+sO^&mgetuBQA|f|K}cAp!@}WqfEdSUPB$}|T-3oG z&h#Q06_Kx1){OLsgen&0~v9;+4U1Q}p+5Q<9e)q`PRs8rp_LP?rs!jGoNLa%zk9ugm!uIHWmls2^op^` z-$U}GJfMeuiqdb0U3vpCT<`1nspg~{Htg|YryFg>R69LRnuf;P1nhqxa(bUtILe_E z5V+h|w$T}hIo35KN>|pj9Oj=7_KZ_{4wVMrG2eC9^gsA)Aw%vF`%W^3c@YGI58xQR zXSi*c&bv}tM_sTx_2-mR2JTO3sfT{$XCO3@He=8hV;S7H_8ff}CK)*oo9kRTQl_}w z^tXzYR;f`)ibC2@_P;%$GP)WPCC;Ucr;O@}>2j&rfk+YkYgA@l6@O<^_4bOr5^V26 z{L|U&@Rs*&H7X`A%>^i1nQ%^r*l5rlBu^HZiQQnNBgA9L;ucfvbe_f`zarTRk3;}M zKq!OD!;VlqGRyM|(LgENtkdmO5KrMvhDH zy+obnm;>2jg@Ri}=FxacK0FDWWit78tDz{>?cMBpUZ3aPl zAP2w4OwEbE!D%q98KuR-nMfkyU%1?6rv(Nc?n{1bXUzO?8`FOfDt0?A(fyK9iU5;1 z8W9sQ>vAn3@|-osXzyIp%on-AEtNnl6UWBl0N?q;^Cz5p&`B{j948(khaD)+?F9hF zJe&fr%`iuRyXY1X5>llUs{vxVBr#lkKIDU3hE~_=dFp``v@;2!P>aH`e(UpA1or=4 zfPg?0sddu}LSji7CDcH}2-q*Er`K}&{YoAoQl8BGQGldsIVH66j?cU3K>Q5>WU9190a^krRp{4zx%x`=4{#R0{>APNxc*Jg zBkuECX8GWPa{B@sS;-r4$&qL)xx9UY7?nX-nxs{pq=l|b16irP;wsb{GmeB+$J$q? z_&`gw#KmW-CQ9Nlb?40dZ+R?uHH`bmFf`jMnBB+%2dQKCy8ic< z8_cg6sc_hyJKn?;eWm6E_Ene5UIhsS-?;bLnT zla?(xtn?-l%>Jrxn8sLFJ$%>r)i*w*fh-21{6;e=J(vRyN)`(bQ&pO=?uGurbFvPM&h93*d02+4%`~FI{{#x|unS2D}Iagw^c;|Im1d0b~%Zt;XoD9O6 z_`3+7_+N|6ObW4;k%w8%X4pv>78@_O53L!lMe=R0=awM;M4UY zPj*W#;7o1YXE71IE#KcKH|!?DxT@ew@7orKggC9rBYkz?dOJYEf?fK)FGP>k554Tw zS3heL>pl9bA<1+S&RnvmKwvJp59=e%`UuOq98N(Rm1qwur1Rn5jN)6KWk>O*y-zBS z+jtw}36u7p_hwAAqhK;Ni5Jn_M+5eP-n{znu2sN9iud{mWJC_=Rlc*PP*Fz$-5=!@ z>V7|?1L4oGeM$E!0}eS!z?9-G~X0=^5y<@u-y`Nbv1>Xq8=ALKq5}LF< zi5dE`FW-A{RNa)~1+BmCMq_=Un`mj34<)zi=sPD^8>UvFp_($ECC}vGXzPzsV{P_E zhVD3isFB!*qU*ejN*co(>>Ad^zQcKru2QVV+ z#m0_n9fr{J9MQp=6ncZ{Wb)vylIFe$i>{4pwW4rz_Q2oL$xsK%#6f9rfo!-vTo5Rn zx2o?k7lxwK7X^b4q^sRjMjC+_oZ8u;TwUB<%uZ4Sn{J~jDNPvt*gIhmqmKbnly;+OH8>h7@ayv+?wz+$xdd6T6XEmQtf^> zzaA2;OXqFB2*%G$?uNobG>Wz;vG^YohL$v`-+(pLiml3=q_ygDf*UD+rq@;m+tWx&%%w` zDRKj?^Oz&~?*i|#=cz{lNn|`TYXsj0oVr#gezA~#uKrj|8fts#lJ|eE3KdXXe#I9{ zaM_P__@3rMBYDc2QzFi1oH+4z+7I-WGOfRTz;L(u;-~V4=#f!FJzzzsP{Q%k6ZkVk zO1&{hRqhYSnxkc?wI_Ht@Bq4`EcKqd=S}v8=^U+}GmmgCOFy1DigjKs!PV70D5O27 z)PGc>QG)OcY5VHN4(mLG=W0RQ8^tB|HaQ{agLKX#k~A%n3U-^|@T2gz-QX2t_>U;v zw@!!jF1@T#&fQ)=F^Mx*c1FDJ&#*v#-oA6IALq-JrO0wXKHkvtnLwfoH59^{%Yy`O zATG~pK2z8;Y5qQSVWx$rC_nXwa>hY17W;@6PUeet><&F==(!(js9X)BPOLH$o)Okn zNKCbvxj`h#&xZ;wmaB!ev!F959_92;n!^5%`9U+J9c;zD^_W#Dj3#l@5{fp(3#U4u z+l2I6Oss*u@?W8zpXDK1Eq?-jT79FoK=$(&p)>-6zX!r6e1`MqP_4vW5*aiY*WplJ za#l|~r+3P~W#iYUX8|S&S`we`nDFgGLPKDt@!g;`)WOiHo^plobZdC@ zwSrNa-HeVj7#>)el6z<6X_itj&vkzr6zRCgFN+DUeTnC{_zH8L_4tHCglh$No9j z^&6sV-k`FJ7v7I8U0F}>*w9}1o>$rEt9X&pczul3%4!K#C;6R561?d8U znjh2~9qU)ocS)<^%)nZah>AgGhn9xCtmsMeNi?=+KR-+9TYk#1-u%)LnuUupLhULS zS>xb2efpaud#SK?yE%~}1cW1}sg$^WtB#A4ACbY5*nvq80^!S7V@#nTu>A4K*^;^q zoqN60`cm90f%QZ>AiIr8w%Wz7ZSPE91G1$>Md$macjPx6ehJZTfO#CElkvy18lkd;=s^1NOu_ zy`Fx7>4e$-qzF@KDY4$V3ooKU3sLZ^Ax*&!OKDVU_oee?N$?}}IXE9yVyG0JIR9u( z3pIx`9c05)qMDZGQIBNPcs>Y`fhEBq%uRY`DuM9rxdD9N5W>^H38E%x5gRzJ*0Vz5 zpxc|9KOP2Ku)_Kyi8$=y0V&tZD^+Vw&;fn|uZqUI&xoi}r>G=EL4vU1tBg!i{b<|Z zs#+;czp8}LQ_7k8)cpH5%@-ERlH{A*(zPq8IQCz1a@n$E&2 zs`mZ*Geby9x75(xDb0X{bazO%G($H^hjcfR(%mA`IdqA%boV>Y_g%m9AIv#xU31;{ z-utuZbwak+zhA0ugvt>&7U#r+p5*z=}Y4Hoy~YRCd*-6gEovn=bkpEJed zPywIUZnR65eg7}KyNRs&x4bp08N=SoF>ke9EY(rOZpPZ{-L-1Qs3~I)Is5@B~Mvp_j zcS<)PI{(pb_U08wkY{eQXo4t7HBN(KSMQsz*TP6~-wJLESTVVdeYnE-uXcVDJLsE! zNC?iw0m^NF+U6KVi~Nlq$*bD^cczs+-9IE1__9w|pXytPu0|Xuczt=xIaA*o)ve%M z^Oz(}k33jQ%#O7}U@`DWVXvZ68`Gt0%zeG;bVIpp{pvx!4PDyxfD-Ep(OSKhz0iXz zl;4^GINe1s!|H2Ak3^?qOb&hRCoJ8(GW43G`v!1dV4L2C4a&X*XFXlzi&&KCcqAsc z>}$uKt+6J?IkCex-p6Ed@!doxH>(Bj86CwPL@so%U>uAgpI5PimCr(yZW@2GPbL9p zkGd^Q&k|R=pN-{24!qsZey;Bq1#ikGwX_o=8lpS!*dx8p-0ces-`f1d-=R*t)=)^^ z+jJo{SG&_^aZ*FE$&;u_0McM@H2zj0LvZvNrKp$}4fRW(Kl#hK>KNEaKF~7kJmJ%` z@u=L@|FDr8|2OVl-$Z7D+ChWFR)zh>yjdT^@jIpha^GdV+9X+}o4rAb3A+qi2kZ%= z1xdBcNMi;?3GQ@lP+4lZV-6IyfJ2VWKVn(Lhm=V>qgL}#?r_c$d_-%MG~1`0Vv%D! z$a6SDa^aZ9`8>}Hn0u|!Fg%S$xAQ6ZD|~3E>Y@Ht-t7ZUjDm5tyUz}mYyll5P|i1N z$1VvmbMBO-U`;o!NIu*!ht`Dt`X_4fo0U)G!z^GD95X&b$lY$1bHMF$&;Qb0vy`v) z)uRIgObj1eY4DR$xYetpKoad~JfFmhMNiulw)!tc1`ub1af?U7qX7pQAPGCb<)&Il z3a+#fg3H?>563#G(lt0m zI~;-k`CndsmXZEhRYC>8W9<#rDm`c60-V#M3$<|HMxj?A8qc|Bf~8U{Gh`0AIm))>A@JxH^!D#Le10bXQrY#{1wTHI!_)h`$Ma!n z!evZ&rMAm>6&)IQ#rh-w6gLIU$~o=@W|ZHuG)l%32Fi35EVV3(I2A|BD*A4q$)^@8?ygtCASX<7fzE)qRZ~W!d z>_hIqX`Kjv^6rD5kPVLHF+x-a+FwlU6@)1TOoQ1y|NXdWMFb;q>gre`_GYlc*M$Iy zbN8b>iFt`33QpfsoR}bw14#tH`iFD8yD1M9Op`^^=JvT<)}yS8tA7S(mo+Stsv2S% zl78{)uF#VGTb!%!?j`8^gY*(d?pqG`N}Hl|Ke|+jGmG*$J0AvkhaZlcHj@O6`nY0L zBXSBjue`co`&nWX3*}>SFTNi8OskjWX@9r2N#^!CX9L^TWN=O2N<;|21e*=u&5u8o z=AHvE_Zw;454W&HZ>X-pYX-&COa2)YNs68{yesIkDwKDtlL>3jd5fg;rat+2#RY=l zR*@hf+B70pld80*DP@~8;m@GBDFqLeWX0ua7E_h5?yr1JKgEXlm;8cWX0az&=!aME zOe)iW&bm;vEA7uVYzUeQ#|A2e6zEuos;Qt6$*&kv3$=LP;^HaZUw+M>&F>x|n5(-* zgMmK_nc1X3Lkw}9A&7F|^ZaiE^!ix4xTXVc>SIIVSBCpsMhC0r#~u9t`uPx|hN~Su z`Bv!kyA6)qoC745L9H;ce&M!3$Gf`lUV!+vkN^?XD9>=#%}wf7%Vqm z-mV+74*pd28QTh@Sj&VK#wyi=Qwihvv?>8kzFFQ+d2g6pc!Ji#`qwh%E)N2V#HkA> zc4Z_Ob6U!MKhJF5@hU}ejfY&@tmyPvivR#EMVFnYZx8{vR8C|Ez16NHlwB{-G_naR z>bJcAlJtoQ#U#TLCj_0A0-7Y?z`Wv`4vWCoECEsoZ4ewtf8NT94#vgBDd9c1;Tz=J z2)`+zPH}VB!i_SVY-uM69-6ujr^Atq(Qj~aw1UX3>^A&(omIGduvQH#h|#lO;u(|p z=Q#MS8JbQ05i?ZWI<$)oJ(H%IZOGGG;++H^;);DK>W6)nxr^QFm?D9loR!0$PI4@- zwOzLelh{ZmV(C)xQfBMNo^EWHA}YxrXJ7119+%-~w5$KlYTlD&ZM&Z`qOchty9!_t zrS^^_85riPZ$|2Y%Xg$!uto^^{DNAzjyD!t6(Zo zSIFF-9zQPn5bqvq)3TD2S_1p7)A&st5|+3(ixJkZexz<+-3z@ZZYN;{plFIvg5OvX z>Q;XzsPr&1gJ7EhclA>(g;F1PyrJH$93C)&6apf-6=x6-8v-;l-O7a2>LU)J@u7bi z79o0W(H4657G9dPOXl23?KJQ@{*vGZ6M4^6@*L3f(?SMu*T>j2f}StqD}y=8w)9I>*e~U&21hJsFL!m-+w)t_J=WqJT$A1L0%uTySO0 zQkHTA6=xx4Pq~^sBmXFV9&=z)buFat9EL0`&E?Q%P(9(9gYMulIibK5bF5w#8^ynS z&p&?WP1W@cUGu`Cae56l-?zl2a!Jw0d1LU{qB9DXJJX~TCi zF8{hayx?b@Z0p(+K_*|^FhAG+`y|K6rc-^NzitgsRgS1Z;ul)EgzJ&0#Z3vw(td3P^-1P92*7j1FSQ&I*S)XiThLynJ72^sqs7@ zY8{iE`dA~iuH_3`m@k$tXW4JybhuY`^HCg!g`*eJoeaC(@ftEqv{2y}5(mFW_H30G z5iv%3P(c;~%;`V1E)k(SgK$DjuTq0M41hm{P=VnW%e*~2-dU(5gK5rEb*8~sUi?tJc-9IIAZK9@xzTH_}qA_W|_X#`yXTm0lk)3!RQRj}p!Y$mIq`z`6?US6f| zq|mPPC{wV~aNAdbrXyY$`9S@g&h0NtJpi7XYmAu5pTg-C>|PmNEQNt#3A4-W687Pk zMpQ}+uM$f(3f z*pgoX*bS=6(0M{LD??FND~XT2Au*8IwIXXDFRBp&LV&V@gOwDvKAR2*zY7^Hi4zU& z07V5vD-lv$-zR((9YuvrXUw_7R1zk~WI1|-24E^_%l3>s*ge@@x`d0eHv)h#D{#OLRCZKNco{4 z^v#kXV^2R$3o^nQb#Q(>E0EjGH9cs!t6$tdzNJ>pN3HajqHZ3Rq8VQx$5dhiee)6i zLZ;P~;X_gzU$i24SrkP*SGP`6jSy|YCl}A$Qde0qvs68Gr_sfdNmC)`Vv7SVSoS;( z1p2>(3lLjWxP1!=l8{di*J}x*saf*XvE4|u>*NCJPI^Yk+2LyPowL^3>+nu2kZWJ% zWp-m@CuCH*6e8NC&0V3Q`1IWPO@}vC)wfAD-LQh&Z%Md-3C09y`Ank!yc*O;j1GSX zm6D)UcW1bFeN)b=@SL@&y8#}p#}(*Xo7Fu-z*G`Zv{^xPuskvtUN~UgW1kZDbefaW7*=5Sz$Zb92m!v5d)>qVnWh6_o*U{Zm!L zl)pSBM60EM{UJ%3=rW&jkKb%>S8qLh6F_~K?@5m(-y*aSHFpz$RFf4%dUuExT+?g!)~ zAf|Mgo%i^*@hyU+5#B##@!Q~Xq5bUqrxz;91UgzG_hx|2vqi4bbB-{+n-Es1rGr zMr7(D?Zdds1RK3ixSO&gX}rOfWuC%gt0QBN_Rnv-LfAi4%B%g7fF5n#eIEQ%!`EkS;N?T+U0# zkR#@G)#oZ|ebebdA6jJqZq?-*`(GM>wfpBf+M%(Spf48D{N6;$2FBF*)QD?t4pwn% z>!LWoxi6ymw1`xqj?4gS|EefBUcaMaQG)w;Rz*6EI{{tT0O+))263IRD($C`b4 zL(>0{WjgTeuMNomqQRGGm3`Rcn#?5&QT%<#rG-XKPZ#!DtnK`)vGk+e;HZ>KJOy#7 zd1Fjuk_U&A2(s+Tmpx!zLqP*_BQP5bE5Cgj_O#3l8OY3CAOq@^9m>EC*bWV6hn!9+ zoR>-_U%ut}ga^;$8*zRA7$u}K#tP-(#Gy_-uJ5+Zv>7>4h>$z(Zz?5UPL!1b24TBR z%+%;+J?Y>BB!W7}HC$E(LA@o1DOm~O*oiWsEHai^@Whtp@&&2Rcw|tzn{g}mWxY}e zGm4uMlTr$r;u5zW!X@u9H?9u7o);oI06^bs;acLUyR8ADr4K07!$~=n`w}ImJL{86 zXKSFpsoth(Sh)bvJo0%M*v`(8xsuD z$G}d%AFN8C0Pbhxrh3xYKE=I~MkJfr*H@s&fKj&2S)@coxHEh7faJRo$G6CgPeYzi z=YL_an43G{q1l@nJ$B-oMIEdeH`K(l^yyB`pcO5_)cTd<|56t4wX6fa+n!gZ<>jX8 z!^0?6y9mnQm@i>)5F(=dF9F(-und=$IUlk4PQ@=XVjEFo!ysBf5+|@m7P5ncR1C5h zwtaJ@8Z2M*Kz3GHL6BK`aC76?@;>`>+I;4MK2UBA9ezrN|M=q~))Iikh)<3`9SA@> z*SSzW0qlL-VEo={0UrdwE%|b_h{Z$cwMkF^q*%r-8$osRkxSHRU%Pqsbp~cUb@EwkU;dlMq2=Bp`mc5S?Vd|?D>f0 z#Dx<ZJ_}?)*?os&{p=6x|Du+-yK|%kyp^D z_Q+K^l28tdn&mZ9hp_^9m1gT%N9*@JxA`RU+`fTV)Ttez)iij7-ww52G!!6T+TO)~ z$j1n=tbryP8hPg)ZMj^god)bszMl7V!A(SP7vX%R4p==L2m8b0ZRpLCv&qmEcrm#< zC!pR;c-Uz{rPk}^&F!eN_sO5CP4=vNcx&1tLe}g@@VMbzi-Z#vsqks4;Y-na$kJ(Q zx>AD+f2fiK^CF%9_9uBD`DEW^) zVKV|kwf2)@?{?o`&;M+j2>3%wMP{whSl{e(QoU5)+kS71;(LN<{3wgDfo*){e)jh;c8qTAbOpntgv>vJ zu69a+>^#^z`FQ=iFZgZ)SWA&>Y9}RQbHKDBrn~RGCQ$hLuzervWNtyy+&j8UGNgp55-K>6;QeuSlvFHJ+hl&`o@)YF-o||P%+bM zYr3&KPtqg&XLoAzcJdjtXE$xQ6*mI_tmos9m8+fp2ko89{0pw@8e&uUwyp=Pq^QyjFQ@A zB~$`XrkN#3D!`WgOnXMTb{Tt-B+37r)N{EJDl4TS>AFc&s5BPE>cqU|%yn7nU`9hT zBVU8yzm_%Mc#b`NpileLX3GaHF^Jw`dq2lY#$>cVYA6r{OR8ey0qOH0K2i{ExhO`u zAV=;m04k3ROXK6rla-BQPmc;c+C9V3`uG2Oo^(upceq0ZOD&geU^^>Y7#yxWZ_pBxJlcw4!#=;jsMy{ z&^!!bgw&nbz<3UVz>HN{h^oygeR5&xVTo<#(-H(gP9POJh!cTlFFQ}v_vY$A1HxL8 zsTdh0RFch;Ksn%|cj#)EPI77Y56&!gKYxeAqZFl^c+EIybq)FT%{J5Xi)Y6GL*SE! z8DwPkMYUB#B-?H$y9{1J@+kiL{CWd#ZxJVd+XCGp!zsWX&a5a?GZ0dIq>JUZ1X_~} z6s9tnq69(R+A{9WyABJAjwVS&^lJOgplH_B8}-&GfW;|w(O#?CF=VL@v~>Q<>tnOqO6?D-{l8!e>0n%28Jegsh!}oiBBlwPzua@Vhgd4 zU!N`lsE`ELG5ZJwP>}z;;o=nZr5~|zX-twf*!u1J`z=P3SkPalce?aBuZO?Qd%b_4 zs+TFGn^|2M)AS;Oy9j#NXRmK$jN zLT+s8OuT?CG0T+(`O>OS*aoNXk;=*jZl@Gv7=3d7(Kn_|QECT|&Ik@JGjo2~W}T_Xx4#!0{PFG{eq7}|b^2E`51 z=7UF(@kkM*J*O{9U-d8af1kZ19o$rZ(S(ekWYm3yCVjR*ZT*xrL03m7SKEc^OO;Rl z8G3(JIuuF`ZpVyWjguieEhabfPodQnAH4oYC68c+C7-{jU@+AXc~WB7d4&tJ3Eh{k z4~hff4Q;$&HvaLFu@nhq#~Hk32?19zAQzL77h~QUJdV~QdamH@l2QOB(c?=QoEiVw z-&=)-Y#$mW{8EEcOGGiaU90)piR*4+S8Rn;va=hUSzN8mvH9248LRa0C7KZf z#${7&5Sz;u`eHf8we9hQJ^`C=+={+<-{a#b7LpzEhs5sXC{YY!6#bHR81}yWK-9c` zHHrQDw&e1(V5hz&l&y8<6eh*{uDfZfqV>nklSgmRoz-@vaBChgFo?5L00s@J6&Gdi zE;LD$fDk;S%)ooh&%1iyw`k8NEtCOgl0&?cily#_nU`YdH8tVbNyRDLa^IxB!3%PO zxzi%=b=74vqtwA>*dSBdq%b)UBJ(t)J%gy!Rsj{sTU7gPkQ`MzMSTRs`9GSlx>?~o zF<)?e_Jt=!1$P<5b_7BjCA7z@6ryW~)USi}-&1Sc1uw0<9!=voJw4T$ye5=t>YU|W zTU}jUr&teoJQm0m^SK=7IJwne+u5Do`h95~_&w0noVvW1*o4djmSC8a(WEqQY2a~9IVcQ!lP5sUZ~nuPe!%mliP%^Yt3x{!`mGxkpr5hg8NfA zX$nd)t%ijfgu(MWYTu>OJ#Bx5M9I_6_cw9l;DYO@=n=nbVM{7l*1(0FtKZ2Ckpsaf z>3DAH-rAp-Sq;F10PGA8HO3N%5FRkvrJ5B9=NcbN^5SSfPNKDx5k78bc@I*z?pvvp zT8k`eO0>)CvfirXiT@8^dH~17EHzmF+WxViin80(M3-l4;Qbymwn&tzEB55uNz#S> zy{XDW=@S{c{B4BtvDU)2cfSvPl=PtX3qm=?t)li%)^w7uitK{T>)&qo@^_-7ETe%1 zUS#XdW)2d%M{EDgIGxfEd+n}2`CwObe$|5?3GOJ2dm{0qn1n#yalMg1 zww%_67;!qGD1~)QcYZ?lgRA9Mvx^%UeY=7*ij{`O87YL&_hVQ6n^7LxXbz!bV-?IL zdebgMXt{_92qtZgJaMR4tI&mwj1QtSL!Z+hEp;;11c?CeSWb^o5xN9vSXa^sTrk9; zvWA5|5`_AFZ$F;qB@i%};`5fL1;(42m@@u&pvN#4W#N#|kwi*^cZO#V4E@Y7hwGGg z9tw6nS3*`%LmY`7qwMX!43JH5$#b5oMm{w<8WO}y#d8bF-f>S+pdc^+T;?Ui6CJ3) zYV|?Guw@^EJVLTRN1-UvP^kUv(Qe%sLHKoR{+o02WALmon7Ewe#*9)s>tHgezE&Gi(qY%Fa2!}LauX)UR6p~2J(6YF+{s3gO>XT zSY)$4f7-44w6JS4tR(x-I29n_{41lhB1QcUf5fJ7u7;Z?je}U9$5NZop@%Oq^#&r7 z&&MES$zCcv^TX05adUH~!0Wq9;Pzo_BcJz@a>;J5_v)+-}ue3 z%Da6EfgiUIU&sInHOYJ03Vc3y7hbB5scB1(?{CUfS~~W8d`@xe z_H}n#+)(at?+{8O-1HFdorDk8gxTpfK zKX2(3bAtdn6;WW6rkV%Cg5}<#-*5O8+l~KX4uiDZ0Kj^QSb|Fw zwAQ4;!cqW0PD!M4jBi4p^>l9~xGvAVtD16^(P_P3u9l9stkM`pn0ucduI+cf|Mo{) zP$%r-Ps#AOB9f9j)TuTCn<{Wlzs0eyHdNSAx5ue~yzhFeek)j(GEyOhR1*SH{an$2 zeNy^7zE%WZk9}XCCSRZ6`pmq(+UC~QR$Y$}MLhF%CG)`xLX#7b2y!izG2fyiCi;#) z21@&qt8-r@M{O(B{yT+aPgmIiQ^YBPW`g(6u~dH*kV9)$T&w-TAndFR=xo?g&B~$t zRzxN4kc=o5 zSjy0}SJC*(N_24wxh;e?KMVZUk|LWmBakwkY~&5@b-Q1uOPw!B8t{?@{MpN}JGTpJ_+k4AADfEB~amb*eTK)guL9oPkyVAgLgmXqZU~l*g|cZ^nb;~4vtG6M z56+ZzcxkGMIK&HAB><|i;k}b7OfAf?J@kE56pPX1Xm1K46hno|Z|UcU)5aJP2$Gy{ zr9*r@>VEx&p_mVD9iMrNr|QYO{&rJ(VUr1R<~bvMf4LsI_9)aifbAuo4Cz;;!|j8)YGp42BIxmAd-Zm&1m1jxdM z0818amkg78T8iJhQ=~f9JvVN zdBbIU-ad2_^Zj z5)2i`*$Lj4OJ0Q*I3&}u&oNq9u(Fbcz|$ip)*0~pP!u0{x%H;!ejxAV4F0%?Ok5f& zFtJ7t;mL60RDMI;s)y1-5NY9>u;SLouwVDNXYoRNAL5;G;d^V(XQ-c>pEn>iYmEZO zq%%OvzPi7xt@HII@FUZI2tasNLuA}d!PXdVP9i}}2uDQ7w)!7-RQZlAo z*PlDC_clteD~)5XB!*}X11dA^NOk1b*IFNYph#>70nxK`CXX>>}j_GdFzzloYout@EBGqixY`s3H1Z{6ns zf|p&yR}nXA^yS7#;&){$+KY+^cNrZX574dM zc81=ZyQ3#zeSEs_$fd3-msg0Ybhup`v2`GF?-gTO7b;A-^Uo>iM(paTmD^4dQ_(-9m z;i=}&0d}=az`6Y8U{+D%ybnDhC&HIkk5Qq-5-dDA?zBN{E4;po*96~hrbfaGVBI%& z$b-I)A&InC$B5-FypV^(w6LX1mXwsyPhM1N8@6}M)qLHE@xSC=okeGmz8U^NW{AyV z@vex@y^vh6uHWaZ3`L1vH@#qgaYiubsjt} zlw3)J-vhOK`#ma}xM^q${|%f(*`H`HdFHRk zh)MSRLxtJi@zoJ5TfR^k(OiURI}%g7lsqiAKF(eWkm*eL=SQ40&j5P6W6%n-wEMJ0 zzJ|S38^1yTIB2;sN>3#=3849tYaIOQUTF(!&5nq5ZPTdsgnr%kz2v^^JerX5f#gYS?&S6p*^qTqpxAIAz93pd_Uj z08TPpb7*$K9r_iY`>s@4L1V|xcLoTsH!}l{>t`unK5a$zy!@<%^H>5O=krVg9?o2f zKSFqqjKaP^BvfadE=|(yEeR(cd2YYJ)RKR2bIN6KqPkjwic>D!(_N(oQ<&B^HpE4; z0h>F~3mPw1$1NKHw;Vke=f5Ze&-&hQ7%mdiz4F?9tnD1CLhFh5c zpd(M9QNOt?yP+N1sH?tRg~wJqr_NeNv>ZAQr`@t=1x8GNlvJl|1x`?yURs0#Q`xLM zB3v9v-T5g(NlEdxC6dGcKrwPnIhgShE@pdhP;aU$GL<+*J07jY^&0RJ1<}e&8Deon zp(Id&UED@}zA>jeQp2fy6sMHNU7ZXgRQ)muE-f9;%L_dZ8!z#%m$g0Tl<5Vq%$cy( z2!~Y5g5%wdd`W4`bA+Ilm%8fBC6|4kch%feYtm~hI;YFa43vo*SIYO5=s{MAbBL(D z#cQA_asajv12(6OaiqW%-luN{$Aw=c&VBCJ0Ch`!nKVr4Q0W2zruy+{)~Hq%ZdI z4*8Dp`cX5``k-X4$(>xmo2->CwqSb-n(dUI4H1|R8+}#sM{qM)u+2a2Y>6@=ieD_u z+oO-IXA*%cWoa%eKesCsjz9aFi1u!(otPy|h6miJmfK; zZvgKkt9Gz&)0{f@f5)xuNJ$G86p1&Y5e$yS^wxa{trl?SCS7_Y^vh6GI5aU&gObT! zZ1fy#+?G1)uIse17Y=*?Hc5BZLv2i1z`#JKqePN4#tF;AzABdDZMyoAqGR_*NkPN1|{)TF8pf=MR?+DNwy* zI_&IWB=*}vVlhdU1oLyjGQF;x76y!|b|bx(B4Q<&3Ssxh@=de@u~awqQl9XISjf7N z3eJ%-h0J&8iDXUhfz=eWZIhF@bRfTBA+6wBoxWMu$eU3qvr%Hx46V@MW4bA#1ehzd z`;GDK$U2$|2zzDLj^NsyB8StdN+ioZN9Ie=gg*9mit4tv@r~))+cqn)>8^ROLu7BG zQW0@`$AkmjP;?C&#--;#y0+m}9Vr?W6^I%axCwu)&S{}cnWVQE_RRHk2AuTY)itq^ zLM7a;SN8ef24hL})Ne zq9f{{w*@?iby<$2VO45N+!fn~WN^Eijq+(Y^koTh_=nT4g#4g0!L;L<0n3KQ`Zztf z+QbqI%PBDqc_KX~YDy_!B2ORRFC+K1dkj16><+c(P3^{XzKXXvBDdd#+9x~+;s4KC zz!k^GpOXRSd*XrZtUK<8#u`hf8lq>U8o0QoxN?jdBYY}~#we-_x~XmJJ;L&tH%y;~ z!a5*`jI0DW>Iw$tcK9KkS?WXT@AO78#NsK%yM5f>D?k`2$4hq4EUJj zk|;!wu8v&Q^4LZDU6C&n&&q=V0j60f? zc4k&)6-_7$-{rW#%)c}gW>Z+StL*D!n_?2w6+j6dooNl5Ya5hhSAV<%Coyho<45JG zRKo7yHAkoaX}vGUx7}&#!fZS|dCFyQuOutF+IJ5Z(vq}ewQ_ z`OKXzrX+N8L4QIx<5v+k&)-^SA352L#J~-rn@?eV_dXGA#!`dThb*Fe;el9yCWE#} zRiC?9b6`iVM%t!Di>OG-?bz{ezIRCxnSam~eN$tXqrXX)QJ`^ApM>W40A zHZUr|;%jT{nsB2!V~^MLzP6ytWbt-C?YZFF5_BA6vW#mzc+K9XX`w60%k7tJk;=XSK9Wf==wqDJsP@mj_=b~+ zqoZJLpp;6mF?Z}2*}VK1QRB2mF<## zqUIxx9-uj4Y3IV%$K6_T5_oKYexA7{4Wa>;JGk!rr1@(s(EgkL>G0Z?YkguPoO#tz z?bL%>&X$8`Mt{1SJkjH|794)UA0DKceh@A3O0gCfMiqv(L;ALIz6Gzi##+<^w|m+; zTx4*Ql*HxOTl9h(?@#CrO_;kGI_TzJwz_j1#(eyoR=W`Mm(&Gf5)IsE{n*cMM=%VWh=IfdRsEJLr{tjhtodL42# z(Xz#M$suSkH@oB#rrkEB*KBNn;21kiAtGI8{l+lo*|Jd!0!-Vmb~JpIflP(902sfs z!TAfxBJRlk+gEK3*CidyWk8g*M~}%3ANQqU)F+~ep#Sp%WH#<+XWJ-c6y*LO+vU3j zqinX%J0>rio1Qf{q({|dpB`xFgUy$3>_%?#>?6%WXGhTt(<#5Jhhvxr|LRh6Yi1Jm zT+GyTZs?bW9G!?uetQE!G=o&AZ`@yWm^=79qGYmmztz{u=BL0)&ApJ2Vu2e<>X>*2DPrg&Rk= zAHKWZ3|&44)LKO$sjrX!nr++w!gzNUYE51y;hQB^;N3vp^Pe&}uGv~vZ1rDIx|t-H z);wJXQO=b7JzgOS9d1~E6CpOx0w0VEKsJ??!Uy780FisuS+*d8|*!#-}UzA zy?QUEl?4qmgl~AgJ9)O=U#kH=Q)n^$d`I5q;;=+JMb!s0>OF>5UV>s)5^?Ho!a`2Y z-zAQ-H9T5~J}^^cu1x5(w2NW?!x6A}9>dEiJ0jnauv>k)z!tpRcE%rds^22A!jR;! zqR^adDloji892jqym}q!|E+omLW)tSSNazkdjSdZq$bgK#6;VF#n?3ZS^^am^`v+< zRuv^K05=(Y&0q00mzobHSLGkY?!D|q+}d(<3vymY$pGIKtxh`Ea=rr~cab%QkKQw6*3Q=ZA8~StVt?>u3X)p-JBPGZzc9H_> z;QCw#0)rKD!5&AWvK6*Co5M7w1JQ)QJ6L%k-_bM6D!&Ed!3+cIBei)o-=4$3*UzvH zw62-FS8Yg5A)^{u$(P>|D4bUvh2-&p#+!Z2fnhpMYK`i!-w1jDg${ZL@c$v z1X)Hd+5Nu@=x20UrrGmRsVEDF@vj1(g5xxV^9nr=keb!jOR-*(?ZPP9-vC4~~6A*XUnvLvvcb!Bvlc4}JsP%2=pyu*=|-MCXg*HgT(oS5Mv>y z7*ne*`d^CP+rR6mq9cMzZ7(96)-*eIq^QA4>RYo4-gWlXQAat>ifkBlinf`hXpH?S zDEy&tE>`umx5y#VXYYVN#WRY|rj-klpV6?rY|C#V9>FA1jQZt>k!bxp0lv| zd8yZS3R)+c@a+4_*T_-L^Y;~_vEOR!Y*VIR7W|$Q+cbA??F3%ic=sQ7COYr+f5lAF3E6n$1Q_L)kJ$X{)ASp@$u))Eo7Fe3pluq0y zZw1CQqgm0VzePsQ{-S(+G1$n2+j3+fkpo+BmDyRu_a*f8Hd6d?Y0_AOcU5gBV@FBv01tJ7ps&j_ykQ<9beq%NWm`vpeHAXfQ>$Gsg_ z+&}QlHUCwLwoO}^Dhic)r!z}MMlSAG(Rhl(Z*&|5rvDUuf4?qP^aMb!Vav5V%!MBS zK*doH_Xt;bql{Cbi<(3gijBg_DG5%9k(VF!i_4{jm_a@QXaOeoBRVgm;!gu`;jE|U z9<~zYBAlDTZO8TGM-%ngIql=!kL%=4*xj{3E$e#Sm8V0wQh|o85sAw~oKszxjZoi8 zrOinT@=ww3@gwY;uA*qFTUn+>3Fp=jOa3!x-M|9H(Ug>i!*;!g^&jEJ!mff#{QVDl z{6%p4T3Uk8 zMQ8(9B=Zqs`ROAA{qMM4mW+{;`Q}b?->12Yq=Zdi9c%{$6BQg$FN$hDj~cte=k)hF zK{KSzcOVJMl-s5dIgD6G4*u?=T>7c{`Smw`p{vV1grg{0oetja5!!T{xMDu(&plnH z7;UIDLjP8hfqd%U(=)L-x%SJuD46E81N`jHQ8HUrOzBA=T0YFHzprK&WSNVpxN@F(9FV&N_R2dj#1s?%bf9g}VZDActKma2ulPTX~g8q?6o+5O$ z^EN>A zS|!)>Zqph^(!p$c97zL?jJjRYijqe|{GpY{{Cn>0pvM4--hnL%E4OWFt!|We0Rvlr zcc7gJFJv5vk4$W%_}eF08xJtAo?C0mfV=Z(74eK&*FX_E`N#pM{46R~p$qyT;t1+G z_TZU1bS5^DHI+-VMRHHGH}#xIfFQ&m&=G8-GmshE?Bo49K-scG6yd^&5+#{hyf zlj%sfU?`B3Hsv-npUuc{`u}J;%cv&Y|L>0;=>|xnFhC?mgOqfMNayH|(J2zrj1K8i zx*JA}Zt3p$($We7BH_K?xzB%(IcE=cxXyKbKJWMI_0tsL8{DH3!@M$VPI>z#p>~+9 zw;$*MOWO~9Ch_t5_`G>(6Nr&!-__>(fR;2JqE}*X^p?8+EUaC0-G1Fge-$`LOF4!U zyKe+5JSY5pVy5r5|H)Ec0}o#VXJ4YQbjSb*Y_N-yBU_2V#IHzI6Uh^Q_0cF1td(&% zK&VLGr<9aoZIG6ti__5=;49jRktJM`v0(ArMryuA(DGh0!>NAmaE*i1W zp<-d5ZF9Ygb%5i0i?>ZNe+-6ip8S1OeW!v`oIWKx?Q~L0LC9sKFbJngM!cnr zA8by093!P7$ti}jvN~aqj2X>cOVsqG1*Su_?7`Dp#m_Ohg>8P*;%Ov@2<}cNwpq?T zS-0%p3>|;P0Vl@<^3WjOCKZs^r$eRI zaVa@GQQ^P!Ri?jm%rbmfz-W}15XOW4i@IXgk+~%MW z%2#i1zi>I&yGWY<{5_}G915T`RbnvTghrM-kc@~e)0tIEA(KzLM0;I-3S+jH7Kesz z8H7@>0)*C@U%hCeh#EZx`{I)`^W$9-Q}143r~P+D?7Zm@O@Z;n+x^AwFm=Pa`ai{=Hl1URv_I9COwLz*6H+IM z+uQRVAZmpv6n=%8>tz@Kutmf)^G@zvMrEzONh8l9UJyx zr!#tWT0BC7D(dfM|7DGv=QbQ2{1&z|<>0`WNp z{u$!Cf6r!JKS6?OZw)6*yjo~jk%d`>Mlq-b$J~Y}zdfNWUlisjb+>4>@#7{~ki-pJ zNqya5MD1%-$aQDO83A&i|9av_(&Wlw>}!Bf|8{yTAZMc0#jyO-2i|i{{00uyxqvV; z+$ni@m`4#;wS-jDWLc8vQ)Kr7W4jLBd5qayw21imYZ#x{tL1RLvP#_O<{)8k(T1Gc zxpKozPJcSJcgO%n1n@*xl02AmRoRv41@;NCDX{L~v+KZk0`Vwi9{6?T`0gKg1U3j9 z;T}?0Q?OHd!zj7NVx!CdriM^h2p(LeRKq&_o+HHW$?pDmv7x%nB3f##aMZxLzd49X z0BRpTBcI+G@G}a79?rz;8UD^rnMYGYBIMdFy!@pc~JVwvu-_HIR65WhCg=U(im_on|=*Y)NII!dsG(5ypTWdnGQpuG(6dZN7ZQ#Axv!L!woZmy#5K zfrxjA+8w4ZH_qS&-2VF)WNEe+Zd-tUu>U(pU38)CO_GmyQ~xt@qIT&q!~@TMU0<84 zUZKMwfBqgQTVza?1oZJt(f9Ky^5U!Nu>3*l zjWlyH+o38zDKW!WRZIDuI}CH#nRVn}kCh`f06+{9`L*EsypM;=5_28>a?AM`D+K~m z%Th7-3d8|GmMY0|drwbK=xPh^?6#}$i-gMk@YW*UVnKdKBp_RDC}&#=%#bLb7{*`* z>dRmoZ+)>p!ln#VQfGb#3;O~f%_!Un?;w>yfd7}&Uxn9#XY9pu55C8rTZkVBcD>9qZSV z0&1H*{|o)=FS;o0O&IlvLFq+6L+nu566Qs)9IZ~9Z__Kc5oFR@~zC7c55GnjV)I@JO119F_%Sh{hi*| zyqwR}s_k}y9Gwsucr+MdtTwuBb!`GJaRIIkKU+K9k@V@OW$!6ke$@P1Wr)%2d1dDH zp`1R#!p6xM9#*}O!Dz$Gp_6Xis@tkg!x2(?L%; zY&Q=-cV9kMM{*{Jg-HSn_?C-C#-$p?*@t)N^r1FU^YXo;_y^gLscAr6ytnZTu?|sv9{2Xf9r|lUGYI0`FO;~>f}Z|bqXR`^=GbkA6mJZAg~aE zV4xC=EpZzU#56~#v=!iM;LIhi`q_w*-K1f2FT)@aj};SSLjpE8_D!q}JmdKM_fawc zT^5&kn7|tkqB_4&lVwhNWCV|qh76G3BFOFD(CPEAMDY{bxAgG69e=j{(M#|&4_)EH zG0SH=%%o7jPu9nLL9#UVSY70g#c}w2E4XjlfqR5^w!eSFCGct+jjo>xe7Zs>v7a8y z{TT{2be0D(LUT$Y2lz=9WgzA$BVpuGNz}qpsxJnJ*xZ!Ino3TI5K_S)u-%92swz`` zsi4gCro^7C-&DTazRG0u7qSTQ$PM$1v;lE64vaoQkvwq#kTMI|4S7A4C;qsNp3WAd zSLu}9pYFHNi>S|!PoFopwg{OAuv=fXpo0lt5S);)J}SUfaU&!{gh`GHTX?G+2oUEU zr+W>M4nxEy@sNTB(9(f7U(bO3o;fBDH!V2~hGNm?bhdKJY!g=xXcFwnNk0z}BLtkq z!o4yRkDv<6Od@NLVqb9uV`5W`L}^B0GJ+I>2gVsdyszFFW+byfA^6M~xR7WD<`51Q zgs(N<&<2`>i(X@OFm&X7>s=sD7B#9A>T^(99EP1>$N?9d{l@kF!v`_Qh@2#ijaAgQ z1(mSM2JMtv!uUzR+DjwIpp^Li(-lLW7e(!qCrM4o6;xpA#Y7D7@qn2Je6h3LMpY+` zP6%rt*?e41f9CrI4p0Md#XhPK_S|28iF2vzi`_+zj}rr+0fcF>nP%p!W)h_}0@VL& zOl)^P@OR?1?}WuhjplFlm5uC#W`%|mSE(!EtBjXI*{;o0i)_jIg$xjvcHI;RiHM6$$y z`AOf`b913g6QY(dMWyF}Z|5b{xh}S9#orbp4RR(cdd$%adqZ}I}$XEKcO7`0<6f?l!3@PS|Wj?aS zDPC@0^Zat9m-|eIuJOizJ*xJNv3&a+M2}#<6M|WNIm`

=!=sv~Va z^`uDBuK5kX%iw!G^%mOAsl8e&vsRNXdr?0-l2e7vj<-;jePa+B>t= z3X}xwq_mds9gWsTJ)X8T(_wBbqeaU~xqf<|6O{XOmsA%6*sezDK52GEf)JR1D9&ns znOvLv4$~tGFL}#Wcu+%+nw+|BHtEQxZgNJ;Jnotq{sKV(R_lW(O#@u;cU>GX1MfhP zjXoo%pTF&#PCbW9lWzPrqS?4eV;Ld7X=)vW9X8Qd^+F3hX6E!b}*Cso? z{d$525U=}ziNXwxNnkUqfA|ax_GK&>@JMtOK7HB~+Y0>mc>DSOdrV{`3h;4&O(7Nw zMCAKk0uUxbgkTJNtQ)h%nB_a~GtWFc3e1X+;Us4v3d3h92J?Uc(VSx;RA>O=+f8ea z-i9IpECNW8z}9Bx!*{iR*OGxxV?LknP7)-a_Jrfc>2RZyK=O7bvuLn|Dis5Xz^^C- zfH5oxL(a~CA}FzXv0?m5oWJUg8D!CpYG*K$R zI^^F0S!0q@U}i1|!T>MnfYLxAMRHV2Sn7WG0(21@XgJaJ-;-LbelfJveNwDDZ}=T4 zDai;Q+Wg0b+F%U(C^~s!7IDISh;|w5e(Y#(&pH(0R-h*lgUG}(VE3>=^;K;>3_255 zY9|&Y^8r{9;xGRT^YwE{me+hiMezT2X7=?7VohoM(#RKVze zxOE0W?5`G<<8G!FC8S)0+-tz_`07^{Z#{l^#X0fXl++n35nFe(u$N0#d7pG^77$@@ zxL}#lQgJRCGZeQ3&*Xp0RH0)r^s`_)s)A63M%jd5T!Uo{skmsyk-t2C8h(P=c?HwZ zR3LGw<8Ht8D|=%K#HBc`&kPU*y}*~-y+ks5eT8+bT4?&eVqbZz7cRG&obWOzcS6cW z71q%vf7|vmoS6;Fa%xoG6#5VItvOSp`5baxIR724XVT2$#iNTr@$5$&M0WbU6iXa@+bE;c2ZfMB@`UtFCd)q!V zdR@@$$mDdPT4+dkS_P-nB6O-0qhuF#Z~i@_Hu<`(Hl5J$JaoHR0{Ih1Q>vQj>)1L) z-jsYBOS~4#*y2MiKe$>-bYk{p^@e(k(|h#mPqwX+$(YVCg?<9s1vSW#VCSjyGdXGY zYhnW1mx>wFYs^^{%a701P#IXX2iD`KufYxxbAz2w`^q1us{{^k0!Q-G9LHs@Dr^(L zIG^1Ide@om5Cy&^3T2SAoYceW@xC{yi-9y$vEAY1=e|23Xdb^6io@AJlPBzRU6zW6 z>Yf#Hre~$4bDIsf@yWyGxROwxz^eSH?DE4@vPRD}^zpdpyizlMD29- zyO6${3xU0eTqQNy;%L6AQIYFuNS(#4L+Jj?KLf)pomcWvy4M{1YI8-|bYAL{WMn5i zWs&#*OtSySYt6{n$nIg73G{d}K zD957$ZTEW+&KynqQx(y3^wt*f?oF2fz`jD+H9L)8xnnG^iwCJ?ujdqb~@iMpCuw}RB0%r8p(^O zq70^jCUmtwph;j;yL^X+zKGXkAb>!vC2pWW8rtG3nyY=UB9=*#ciMh9j*qn@;#)5i ziVV`~JZHL|F4S)kXj+a||Zxm}k!MQ%$QEht|YvuC*f zUlst2;rcBZ*pKuahoE6`8vZBMZ=U|wVhJ_<{Acd#lV{c|3@QlyOGr_ZBz6&JD?dOA zBuCfh+_Gz()}&PQ*f$)h_e?JVo&G7@woz;1`%zpX z@IhEAywCMS4i-h0ZPXAWNzm>7Az`x&P*6#I6=LEQR z8#q0(01UtSSnO-3KbcukS}?3$)Hpl8Kx+;5;%w2`&A*0fU&Vbd|EyeJ3-WZKF%Ni(h%dd{7JxwBk6Qi{_~7d*og5h%x%=zS z-uFU~#-cIsr0iuM&y+8xi3?6D`U%u(q=!H-Gc)H{8C+l*tH0igO`}j?GzkmKpr-ac z@F&T7J=7X=Pd^b$ezlM}-og2W#M?U~Okp>A@<34S^ZX!;O6y~Qk(NbAj_#X$l8GK0 zO)eElQExTrhEVw0^{OxfX5aDf^k0$zn@{B*@EA2$wlua=@rrWj2*3>|bE=)ELz)F$ z!PO|_5c2OTOy<3>llhqppM~$Z&5{zF2kNk)IO^FqJGbCa507i>B|imLA3CjpiL-ZQ z4i(Cf>02{FEllC0X`{LZkNH+UarzpkNVRKSLA(sFSFoTNv`pZ^qM~|j$N4&B;??@; zvlW2>3PrCvPDfRz!Bh1gx}HBh6|Godt0^bBlUrTYcCgEetwD;U=5oQ)4bpbF}@|E_K2fWK9MbL1<(uWTztqo;jmhJUf?h@157Dnq(_s z+Qm_jf-=V&oyRfB!#!XY$aWj+p@ypGSc?6GLdSI3oF-|j&{0)}wHH(GNajg@m@W1> zthe{F-LGlt9QU5kIglhHao)QUrtaq!Tcw|paO)jl{-U3u12{G)?boJh5hM_Qm+qIw zLsoBwV>lc+y<|;9=B5SRw9FR8j=!d<4EYpmcHsZmOflk!&Gc0?o|1Y5&$klwlAM_Z1~jL_1W$N) zIc06LwMw4-*>U|&ToDHUot9}y#IQf&vjCfnH+Di%fX9g-OqF4rGQo9rx*CbcBt*B% zKlVmRz~9NJJ<8KBRo_`L?dl7;vvrX?BIM*eENp_M5E(&N_G8F2p$v`bFIk!>h~}Xu zE`O@)%;)OC@sk_FgN`U`>okmv(Kwp6B9<%}B{_B`0@~-KU=5SNF-0pKo!l-v`1m+} zSj(OJZ9PH5qVWg4v?oFxvE2jYW6|@ub5^^m2_;}K7I^n_g<+7YL|QWpgp;bh40y;!M1pp9ZI`Z6oLyYB zl&Su^dE!za00VdwXd*(DXwpSatPU;%ySv-k&>c&qMCN)cN&6U*!(3IzY`+Ew#qXk-nCO{%I{ieBW|l`u zQYBi}m+sPaiDvxXCp`T@4=6v1tIi2b&I!j~iL2WCer!c`c6Oprw8NIl`1mACdgy|v z9;y=kk}gfi+gx8aGL>vce^yP0sbmoV5F1)0W9t9+_lo$h?uT8;4axNnbC(OXf!AZE zkE0*y@FS&UN&lQSPJN?W#_Iz`q|9=i9{6Ss%gX^H!)=vfK%@*BDNU})0>vUw?6v3u z2YhsAZ*c+#2v8wsl*0Tk6D*r``PPPkXe>D+e~$_F1sj8p;aNXJ5o%V5cXCMEYhd-S zqqdH8mA4`M8?JIB2YqGTjag2-9mefZ&MFLbNX{ck$QoC>!U+AXT3#uyccKvkXPw+3 z3gPcwMg=Zu>JXxOz67>AVnKVav|8IF7OR|sqF9V=H#f5yrp`L=OZUGK{?f0lp_e1a z;ZsczB{K_~z;wxfOF7VF9JIY8GIxec0?m9A2je+)=m`Bu7ZTs0E9hD3kp3UVNg7E7 zp?@H2VYPE`z5ZTtm9@cYSSoub340ZuC$!{r&X`s8qG_>hgIYgfY_$sdpIpoSsz|}R zUxJ2laPuF@^_8V*B&vIpN6icS&WZ$4p`*HvZM#or7UuH%m~BSpWjM2f=h4=IE4yDD zYQO1HEAq0?B$*0({%rl(IlH$WAL~J|X<^F&V;o+cZeBnEP`B}u$VX?4ANAz{ba1*7 zbv0qs2a$8n)c0&vLo41(#KtSFYJ^rC4>tjN;nj=F3!H|JbVpoDU1xna9M{E$8_CLa zT-4OipHPl!hBh(8Z#+Rf9J+^%TVo*oV)bate4U)S=2s^7weqy3AG+`Bk*(j&K7c{dy%Nvw| z@5}2}JzEhT4Vq!9QgkRQ#{3}gZ7TbrndA-anF)ur98w5QE__JFVNO#{#w!i=swM~( zVJq_whUien>fB`&8C_RdO;K^^%?ivC!kh4@s--G6>-Av%_r<#7DRRnXRF<85^U#DND_C~^pjk9Z1;8Jw= zmoKw5o8^fgp{bV%ts~xR_p8O;k)mU`a$#OzPF)uOB9+-`r2~+gwmngy( zNkI8;{>eAa4JXUm@9p3av#DTuanX8S5oq^n#=a~x*3WF5Sp83lP~jQiLUN_Vh+=rt!0BMf1dRYHY6aF(_nH_^}Ju@=dIU*9`ao|T3pZp3JC zgM;otBE8J-twI0{q?i#>yqf>Ljsk5z4gR_OiDrv$74$m?C2Fn?GU7LK0%WBS_w?NU z!bD4NcwP)^P=O;7)9#+kI~13TnM-#CN^!tUnuwqjgiLG_E@qhZfwjSbudejl53IOd zX(yGHmFNK@T3;ae)l-9#$~LTNyy-yjSxELLti1(jG&um*oLu=GgDsxEE0G-jod7-7 z8Q)Ui{m%a5ukFl^SIpx?7&dwGARtEQ&%SZ%a_DJ<`R(a(vn{~dN?E*7N!5-gez!wq zTm_qVh{{KH{mdKn=u9aiwH)ul^q=^YaG3Dv=YP+hesn)A{JGpeQ$t^q?Xx8A&SHku zzMl1+H@`9Yyj#t!GUo4dOQ1!%2f_sA17@5J%#>0nq@sAk>~R5!EN9-4AfGFlF;7(N zSWcS2gl~>m?yE{*4`$K#YsMj}*ChrG8BjLH$dmrU$oo0Z<#Ap49lvua- z(q>=<2rNy`N)5sj)YC4Y8bwy+C)DVR6XXO4{p;J_xzqnQ*YJ}@;{FPFdt5UF$TQs$Ws!pB3h+M8s1{aVcU$Cx9C%) zoxY559ip!nRWtj%T!t-W-TF;P*+J@fMs$vUj``whQ2bMMv=t{Agkm1?+HTXgz%Ew< zKE_uatudjm{mzDv5NU1BNrn{zgk7{{evI)&12NC4&on9z8_h&yU}ps7cw&5t+f^*X zI~1~QI5PEhA69zW2j73z|El0N%tzZaLlA>NmctSY>y3~v9jH_B%3gB$78a3w4)TF4 zd5`Xx;^9^#bfm5Tef(O;ZT$LWMYe1)>MW{$>gTQAM#s>8cH$Ly`cl*SqhsD;_}Xi2 zl_tHpKo^#SiR^?MbFkNB6lW0BS$@4IJDHA!kKz|WzeUUoS@>!27}ZHdgCkbwTTM8X zlwJ9JR)x3g$pGQQD`>&svcs3SF^^6vtmbu5I;T+R2AyCEw}XRrqGw6b+lp~vD!SaO zuev-&7;E1iwelO1Eot4#q?ur8-CRVMKSuy#DOoIV!(#ZG<)2OYF95w(y z$~vICqz?zc0C4#VJ<>MRd6m1eREwHHmacGa@*bE&x$ zRohfW_NnaCcYT^2Bm)S<^l{4`2X0uV+chaQ*nkS)ag0pG`0(kkmmcLo5MQv;=9law zpB`Ih_AG!M7oH7Z#P?@VVlY`_(1>U#WzV%Y&aeefvQ2fcj_9Kkw9)#L5d5y?nm9)i zFvNX4Dl4>Mb;F^=)<6Ef>w=MT3jfz)PIflhtV`12Y|o5=t;_)7EIT&C-I0UL<;xa)YEA_(OM)s!*C-TbLZYI| zWOkv`S$^pNDY6+Fk#&jvMN**cC>bCBV4_r76K5a!*yf%65S3+Y9T$UBwyl$J27>D2 zLC1=?+4uZ>BdM#`Ju8dWI)MKKxOq=`27KQQyS+G+X8_3KjF<9Cld zv(hrn>2t3hYF^{4v|YTS9SKswNMznB05-6dj3Q;;TgT|fp8={GJ-(Byd6(d=W*0(Mav~sLF^3l`CgNR_wZ;~r^(O!AyWz#o1 z1UVd7#QDbcL{4ikLaP{6N+x?;A-yH9?bW6Q`t$fARhm_5*ujBb*Jbg)Gw!Fc`-|>> zlc;Xeq)0cJmCj_m0!6tf28LH0PW@b`zEk77;;~nrpY+d7#QiU4Vz>i#<39HEyTa1g zN?UQ$%&A@&8+ZRb^`>TptY6?7+0gBnw}j#h{SuzK?PJ6zQ=q|=UwsyACs^w^6x&K` z_C@L)+govQl{rrj4}PNl!pFjtS%D9ClONa*Je*4D{LBxSV-TQk3_Mqmv1DD2cXGq~ zO$o25N(GX53W`$Vhj-CloK3VrG00|)qNeE3tCL)cv^1rqVh5?N5F7!hi}4cyicfa1 z@tZRqgg7RrP_RPT-fiCgRs8(>?B8y8*C#K(f0d?P0Y0}s(IZ(2I+CdulBlk(7ZHzW zapRFEDVT8&d%W&E4M&+D3kyu|9Vx=dKP;?-X``g|>7(%e@2smFAP}t8^*a`m)MB7R zglmY(j-f!RY3t&vc;F4%7b4+z?qp(TLN7ih#CXZ>o1&G!H za=)3_S4+$$d8F!iT{kc<--!rJg{Lo;Navakvd2WXXS=}P38A3ve`}sMs`K(O;ydc- zT6?zW&Aa6+PL>^-6URVHhVC!}XtL)^P1u@d@&#IZw>Ir#J{)Mh0ow0s;_0Uzc+WR^ zxR*I)6{gM(XKE?p@mkjH*W%9qd{}5we?}Is?o^%iAFTmhx3<~zx|VspM=t}9kSSQ> zxH(P=3XQk$etopq(7GlMmLZ-Is~6#S9uE%t4~rBt1rj4diX{@2k*MXYev#eqVqWBgc`!A# zVH2IBtuG|r&=n}xseR5~nn3|)iRm8^+6;B#kMee^UGu`uxiX1scRLgkE+WsEwn}%{ zcgk%LkYHR2Cyq*v%D;^Z!ig1`!r=JieFvBBJT=eTjW70npN90|E-Qu$1ZElUds%X2 z0&1M_#yZ*b#a$t!ydLNl1n!uSEBx(mQ;tqeTcSFFe0P~9;eV;MaQuQExOc4)Usxhx zO=3z8QWRVNQhDVu2*Eq#n|F4~MA$FzLHVvDTY8i&kQfP*iw64MloO_)>YUr;6)_BF z3v#0k5Dw2XKh!Npr_EpC+^-?$2%z&cO;36k^LTJ_v9qoRGBn zBBx;m1}lx7mE!um&Tkbefg;Dm*G%#b`R^+Jqkwr<6u4=ZM(K7ydoABjH*qttx^&teK!66inwzb=$(GM|H(&jK|H2y+n7HC&!iLy#BHMaY~$k0eku4b}H{JdX~9x^xM_r>W{0i z8N>t#PUhr{0fNG*ZW+TCqRAKt0O&3~ruL{<IPuj1ki5mY{9m9jZnFN*|f@d(160eWI$cYpU>9SL;$`--8_z(1W^KL7ngeegY2 zJ=U#z%QhJer2m1|3mqKTbZ{KTie?|p(nA8wo`c$0iT5Fo_@XjcX#%+`*8+u zO5}TAV)Bns)*(9PvRKC|z)~@3@!sCB(es>LN$k0HNmakXww`70{gok4 zNq@+|`1|0c8cVJ3L)Lz7WO}?@@v_8zOR+|E(P@{Ir|<5XJ~$il9f#DxMv&Ybd&>h_ zbPmo^;iAm!5Y7+)Z^GQfCN%8*5oCQ`H4@{50e zK=a3DD5{p^1+jxir#1ekPunI)_ky1-zYJLHV_KcIFjg+dzDMCKN5zqeIHEbpG*)+; zEG402CqaSE)hO;$y1%M~FzYZADGZu@uL7^n@emq#d<8)t|vjO#y~&hdbn zqCO7{gN2E)OM6Ki4+iq+n5~*%1L!$q(|bF^da(aFoC67MDblK=H=p{olr}FUc#QT` z*0B9>1JOu3ST7v^CvvlLT%(dcb7ooXOcb-GY6l8H1-lJdnaFC1Df z2~DF{YH#87OVV2|ZpTQQ`v2Uyu;mzDu9Bk{j>m=KrB&Y+$EzNX)Fi3D36(V> zQ`f_kx|6vYg#_1c8*xR|=gCsa@I{`bxakW$wt{eRsp!6zPFr8KATW>1wgqKEwQR|9&*EQ$kgF^-FWz4BGETo#0r7oybDj z7aNlmQ$-*3WxG5er-Z|s!jUVTQ1m_KwddvrYgIy{WwGTW9*_xA!V0uSKY0PgLN#?<7R%^GM(bekkGu5Vzv>eVck0Mb+4FH0MlJ4Vw<|w+)vP$*A;t7M zWvS;@_e%-AW5Umt-evr)UH!)Mzr_hU@$-b9?V(LSJCqFOIh4#I62KY8u(~{Ta}ewo zF;#@QPjfdWAVVZ>ZX@PCVe9kLpYPWbyQK*v&xS?J0MTF^=bD;GbKwoNL~+|y-)5Ty z+Sf#^+E4sWDwI#v+{YBzeYY8Xk6vTHB*Ei4vi~M(+y$;E z2nAgw*UnhlB(eObLXN?NBZt5MS!bl}7aqQRnJ}DP2_NQ0C1Clf`ckJZ<;!2}2*hbK z;&R~1rQ|*bqv>r4FW&wgL+ch3(4nmVMVPwLCtCcjxiF8%<_khMdK=dkFKp~N0d(|< zsG;cS>FLH(bb?T2PKGM*Pw2~lGq|bW&FWm`BjXU`))18pRRlWRNk{S?*E{M|dn%eze9zpA+^QHIOdq+b&N`6te*+_Y;B& zPR4zxDPVMzR|v3Qa~pf6JIndor)F zs!vIegedRZm0Eu3=fz61typ7k(Xic*sjR$8d?;OuGGo*$gAs^{XgITTPSvR0$Ca2Pwxe`=xmXA4fd`~LWd$hlPiI@dL47A1pYaD6Uvb;&wsaDFtXzfZah zM^ucAIDAd@yJJI*Y!m!qh(XM#_5X z4#E6ppU>Asq4~fOsLRCyn}4U8tWl&D@PK1gP-5h?sV1G?quZgc8%DyX=-3;XGFsDRi4`J6Ui%VD`^_BMZ8C8!nz~H z)TJI|~A*3z})#@O>eMMZ02gfuxHzi5BfyX%9<# ze>|U2wyUb&EI)9qZ3h(5;Yzbdkl|mmR{w~mCRB!eAr9;`QsuDmkGX-~@x35kt&yzA zcMbje;nNmTkGPt5Hv%)~jgq}@z;D$$|9~9b?*I>Fv?!%hd{`Y?!a~>^LzDJ{F1-WK z%qZ)DEesk2C!1Wxm%$}I{;im)UU9v?cEsJ_9IG{B9oF>IEhpU?L+TqM*dkVLU@fub zRY={63#y>&T64_GS$zwAIs0D!dmJqs@`uTFZaNV%wen4xjZ!bQ(@H1|EDNd~x?%-Q z@A1Gsw|uKr%e!MHOgyO(Y@X&fN&kxUaa4fgdA|oYkKF_;&F1=jrtU+IUdZ@XmxZ|- zUFeD|Wj!jlGq;d_1+&ThMME+cZ@bX#oq(Uc=&bin6SK^oz3MLkEFuI#WHVr5wCWb!No?uz58~C6|Con#XN4c$dn@=XN;qp2-400C^n(7$LgKxg5=YfmSQY&G@_ea!&GgqHRJB zKmORhX>01M)%JR&T5jtIrr9vM>Y?bWdi}#7*r+0QG~oJqlGlFS%`8GGS+c*6p@G1w ze#|Rv!TDu$VPly$(K+dKNJ=Ed~t|dCtXi-iQ4_^)#R`;BXhnyquX>);FPa}y&vOo01JWFuZP24DDsMX zG|bLW5S`@ztS45JRf}z|ncEpl5YW=%-KG^F!@^n44Xdjw_q8v_9;|gTuTzp%Q-)e| zFuZxV{3A&T5VCh#y);`Uc#D6UJlK{0skWHIY%7PU7E22K z_wlxFlwaspn%BLZ-0V@#UPXlFw{ShmoY5ZYEU~iRafVy&eMe?LX74bUElwTM)n{pc z&^NL76SU1&!c`{?2@t&2upua*S~hq z_LGAW4uJ9dN+cNIOQs>EAh)sHRxQ<|P}9MbG5TA&Uty8s3%g~Pm8%@0t!1N?mqZn1 z91kO&@s3ld6g+%#99&2)li6CjK_oiXv7}k&;_A|k zJBN}V1{SUzXylNQavr^;jhRuSZJtEO>0`yutthOVHhxc9Xw!^Qpt`k4k3AI-`XS?p z&J4b+&YJku=n|;oS)D~S(Zm@F0~X`+-BJ9xc|nJFUNraimw)>CA$;{R9+~zLYz>LKlIfRbt%+x==FyLu;3+o~aBqEk=h#H1R0>99FrBgkGM&@@tNxSE z0KrTK5&%n6T%Ktz4!05tFiV49L2wymUZ)dT|JOW6@kI1{)foC7_UcPIhnUeFE={Pr z94s3jpIab;r@f6V+GQe6W z2h6>uu{OVd!z~GoI%DM#@EpvMFjU$b5BgN|YYL~CN_okz z-9Vq_HAOJ>@Gd%Y&OIg^&kKQo&q22D&mL!LB;A{um<#DI)smAPb1befUnRWy9Z*Ptp!9pC%xZ%g!U(?aU$ zsJ|17e~!|IwGjTF-mpo@r3xu0Rt&&|0rQxt0E+0N;h*r7#WPwtIU9N7UmJVf$R0;7 zazSC=0wi`E+;k2Uor`8FI3WKlq7fdL8JL zn&@FUjU%d$i4+=G@z>jIwmiZmw$;hw9?5&OYC$+`c;mtKYI2&I{CrmfpCc)Jp6_z) zA#3KJ63<3H#VymmU2!CtO>+u^WeYa$O!>m*%qMCISgGR0gjP&oPu@pn`_rogk;d<_ zoz_Ea%~3%mx-DnR>9Zcu+Cnf-x`(foA4b0sx}P&E-+NgkOt{O8DV7}(c~O83hme-< z+~WV-_`tpYBWj*vTto)~S2`zcT6lFA2l;i$;^=F8AnI2pib%{<}pv32CpSpoYD z9!?-ceGUmh062IEd^uPxR3CnH+x+_84`?>g|75X?WjE-pw4hiy-x%6eemY%Tk|!&z zV#L&vdGzvI$cMH$Waho&nH273$%A`2uH*f*U35sh3|xD&s|5u%q(c#}@-1q&UsBX+ z)^o3!3G5h+vE!Q!$kJ>T2Isp!7!j^E6Q4o~wC{Pun_wC(zmJi+W5CBHToVk2sawII zN`&y%w==GPhBqmWq-AT4^n>o%rE{MQRU`^lotNit=&1!;xS1Dw3TR&PMoi-5LLYqF ztMSGzb&W)OHq+=K6{4#KX<>qOi=(4Fvz*R1xud#(DY1*S&conI(Zk|5nDK> zj#ev+_Wd#!5BUB4$}JNe)oa+gxb4s4Wv^TppGwZiOzN}o(S437g-aU3z#K#*tvZLE zbYVmq0|DFT=L=}ak6P*6^|jY#c2vPivk*-TkhNQ=SSGccE1A1*6keyVYay0Z?9&i4 z@=1hNW1YDBU*zYXOP{Z{B%b!>)FhsMdy1Y#JobTtfR-=9oJsWV9|P&0HHYKNSvq0r7C&1m#u%*OSQ*A&{hU>+$@?^hd_apX9{wLAz~`U zLaUT{&*xTu%KSpry_sexP{#%*?*;k1LGU&H6)DRv5P#E4+6BhgEUF+U zo}9VQJI;byEaPFC*y~4c3gH}NCzmH33b!8p1PWWdaJHUT%_-Jj8eRU*&JMt2POO}b zGhV#cT05Y^HQ&yOmfLm{%W=Hc<@8EOlBtP&FuRIgBByimicuxOwtUAuz4@*GM8Z^# zh&TB|S~Nn0Rq(f!$!9rcY!%UH|3d|( z>aO@c&JIJt1Nhk85U|*0yG|qDD&HASaEM`x^d;G3NE9T=D7Cr+@JmfAQgmAO7F} z*Z=eI@bKcr%g2u&BLu|2NtPuM#%E8S0z3vuAAa&_N|L6Of-DP50)p`P(X*SIM?_(q z#%Y?%f*u0EJkQ1~R#8qVdw~fyYc);Fyf8C}zL+*27ZfpeBa210rK-NN=;&l#HrKW@ zC6+VUY@LbjvZeySwCn~=?WK^eS~<5e&_0e1N(#F(qAgD*nU`v!%B}81a4gV&i72O7 zv2@Gr+h!euTCMDQdo3{?7sTZLZV@yZv4zH3snA;sbmC#j!sS?ry0Bv0Txhu8S_UvW z-W3(B$6{r=Dy-9Y+pk)i7Jzf2q7>+wk@m3)Wox;V5f*-h*-B2U#raQaj^678UaMGm zoVC}NcAlm!D?Ao=g+UA09V;!V9UUxeU_{h?6|+mlw&Mgt>4qjyF2-r>3by-g-`EI? zt!1^qA?2v*WT|DGm7Fe3z({E;W38DIn<$P^qh!3YY~?diGN(V|!Qf z+MRufs7G{jNKRz{j9xgNI-6jDv4~q!(9+q~ZKb7+C3QRMK287>U@Cc;)NH!Ao18g0 zq3^BSI)QW~I%lIULkC7dWP0VY3x^|`oI}4DCijI0JT7+aRjMaVE0!G{6#a6sm zOr`!);aSnd{Mq3p=G+^hHP@hDEwWU03yKKhhN%TxjM26xMU_rN#yWIb)o$sj&Duq4 zscj?Fa)`zw((2j>6iKsb*0NVql`DgLSA#4JR?V%IX0vJ0u5fD-)wQnKyy7MuN*Q|% z)QXxRkp)0Ws0P8>F{o>0R2|Do`AQnSTp1v{0;%s?e+-SkWc?Y}!Z}g5QlT_~;=Nm< zwJ0O5C-xn|RSQ%st;~QqfGG84WlCg6)e!v2*Zk*tFqA7oIlL64Wm(*{3fBK@t+eA8 z!CZC{DOd zeIRBz$Ii#KK{i{Pu^tl@M@s?Aai!Hfr)gT4nGuFKD4~?3Dl?h}*S4vTTOotu#40tE z3)&!fMCgX(Q3Yx z7>@#OSU?69K_Kq~&3~@BV~X5W_Q-?SY{z&SWQm=5+aB(5&;E@F^1hhL@ctT zMaszn2El|e#_Q|r+uPgc&z~dWZnwL-x{5J|Fzj}_yn{7_fMpM_h)@6l%E@o zNfsm|5{{gcNyI4gl0N+SBfGYv=4aElbz+dLR9LR*Z!S)Wi_2I(TKlUfFPIsCQj#~` zc;m-E{_)-I!{7bg-=zhnX*vuCL?#h7(&?Z z_c~c+X2iUJNkxx34jGhj+8$|fDr~u6|Ez(Bwo0o(P)`G}akgw<>PbsUFmHLPLxQ+X z3dLCgfO4b4#`j!BqsiKr$?LxfgQFPMu^5#TC0c8mT7nP}noQHSFFQ5^R4AxCJ5Q@6 z-34}_J;HC3b|Ek`%H~CJNdjZdnjP&d5wzd-Ry1pzGZXD?wk|MdWz6bqel-7v%8wN- zi>bOY7PfY5YK2@$?aCP}qshyG@>Ys1NvWkma?1w-nQ zS{2G63r2=*cU*QHTNKdDLR-nV@K8xfhGq`alGjEPlsJyvmWh^V&`6F}*UKm}sN7rL za72Jc>#eFf*>+7?W7XQmR@7Tg=^Lwh+D@0$R4;F6Yi!cbF?RwY7Jf?y#W&aL8hHyF zKxKTaX;MfW%xI=eR^Y9S(U~<2YI5m1Iwiwjm)j zhSAMO9FA6VyNciHz9pK&+SYAL>hC;S<;Lys?Ic`(TLE)=WW};&f&0@E)3&DLtm9)B z3p7h{R>qVgTI`(q1FVxQN!)^ph#+f(Ld*k@H`H#ALuV~!{gzVF245U^EC5*Tszc2j ziztX7;yIp620$u=5!yD>sb$lChhY%2=|SBU)ea@E!iM#-hnVrsmC26=L9jL~P9JS` z7pYNiDujT5l319NteaQB7$V2J8kkK)E6M{<9VM`1aSdB1Zs24%uVEBXK+vd*L|%0?vEjHmiD?IrqYZyrHUvkka8#izg z+X}E10DY4K;+w}_ER0AF{~%b%SjCSS% zSMLjmBJ;w`Aq*lijw2A}OVP8C*#@cPx?yJBt1H_AOx2@PZ79KIhr1h4u)fGgrNSwQ zRyWA{83*8cz?4A*fkmh{2b9`3umz$mr@s}q>d$6QqN3@lR1nld&bcny#}(+A*AC3i z*0crMIiZ8x{jkmIn{(W9&~~wKop#&N>XyQ}7B>FVg?3!OmWI|*liGWL!ojPnt3UXI zZ~e)ieEaV1j%VS-3#J3^mW5N2Aq>n}M&)j;OM+6#JOB?>LM_Q;_ z0qaVd;x#YJX&RR$C0Xyu1<9!`B&AjRvIpjLuq&~xmbo`pO)N=WndY~j<+?7;eQgV) zIas$zPM(SsV{J*-Mt`70E|{y$FNoE;sIvy=?2jdgL)=F3T$Q$ZX21PvLrw-NPZ?|` zoHZpohRczi>lJLN*VPqw?a2BZ7Hd^!F(aaKr;EGA^xRjcyb890(Q3DaQxSKrl9RV{ zs)2O?!U_ZHRIiStklDeZB(iV1{JMaz9U{^A7F}p2oo&i$kUlml zaSj+KKbDV{8ZD6>JdS8i^{ilMBQ7`swTwZTqSP2A&HX*If4ipz3h zs8>yW9h|nQmWFUqiY&4K8)dp!wC^23THx#UtHLcXScd>wm>nWkz?|u5+wVdMXZY?= zXzRNCq*yPQwMqw#B4FK}H#leRrwUlfxniB?7q|s(s;N_tEd3dutoD|nq{4qaj&m9)Fq9vmZQfSF@L{^*JmTXtxY?X=v21jdlu!Q97drJx@CuFh^ zxK6XK`DK+%pV}Z;tT&lQu(;^Ic?|6;S7Ukn485dKt0G)d@9?!U)9R)bW5@0m2B|${ z`{aIex;$)-e09g2bFDcsQ<%$)a^2&i{vH_D2oVLU$*;9e+J0Va&={_QcVRsM0GL6y zsX~>YK*;O^1%MnzkTP$zQvlnY(*Hte+xjZDz&3%BH5{Cy+bvZcyIUE^k1oKZl#a)v z$eJ)j$>{V#X)7ki>Kb=lt8wc}oiy_chpcPXGE3#QJ&mXBTb^dZPV+L3<1|eoAVA7) zJ2}lI^Cf25xJBR5S#hagEA3drh|S`<7I1jkzHl=X9fE5MBHk;oCPG`YgcVJp0QKy` zg1C`Yszij2zIiXO!m3}T8Z$Q5f;1E9bpRQR`%b^m8XG3Sc! znG;GF0>v+X;cH)g^BdDK{lEY5?tlFIFK%z|gjp5_#M874d)n#po}0};iz%f`AadCejKM*mp^1c49`SwxT% zq{QRGGhig4`}@0BuU;|p5Oz!@gEcZ@WwWed(#H%Wm7?WkMyEclu(^uNJWbQ#aQNeI z|KX2+{Nsm*<4^wAzeS3t(`ip}7~(w74-XGtc=PpV&z`+`cRSDHGS49lgmFp0B6oN9 z%aVW*cyl1Leqyu9zkm_4p+>}Pm;`}SI-O4QJPQ+IKqO}1B(knFQVhFc*gLBvC5`U- zX0;fY9ZRQ%YW}Q{+X2SgrQWQ&OGfv`Rq5(x%Ni|X4+L2j3y8qF&bGZg)2!8fXWXH& z4DPqX+I3-yZGG8c%Z@{N6y%b9%o+J^FU|y&H(M|55y+}pLSf;w%`ly1bG@_{+w!X` z8x}VGnip@g8g{{RkCd5-6t7N+=D?FC*1VbY&PacD8Vt1nE8?rp8b6rJ&I@UB@ZO`ddS}P9C6SU=)PoY)B-PD+-h0leLgsuxRn`xg zsMCt6AUn>jLM8HzL?R2YJD8w6FT40C+P>3p6C#H15g@ng5a^%`XSYxhQF7dHFdf1S zSxy2WI_4Ya-9nBz$umyI^&9#zY+AkKJ#-f=choi7e$E*Sx zC522gTDrE0JtAbsM#qha46@Gsj!zArGcYOW9x9aYmLBHq(&q zlBX^ca{40z7-|*C$*${aoas5RsLAK~Ml>%PCwQ^xqP@8SPe~~;tx-z7UkP3qUdNg> zEj44SiBzjPkRQS zEcKEu!x9lwD4*1Jl$@1Kt28@F5Cjz;F(eb2=Lwzj8;&z`&XJp^sEXDLDt61Un$1|* z)vEoh3FI${H>2w?-bVZ}GI6fS+YytJ)Jk)P3XdpRqDDg<2e4nnM`5R|xq<3B(pTOp zvCcEj+8FD5oUf>DtXhlVaspyzkt+9D_bOP(6;?cnnM8_BaER73bxafLg~*t$w!Ifr z3CD00fx&c4PD;$I5bj2m!}hfbZCWm79+vHZy1q2(Io@%qQpwoTALm}o@T0RbHl{_> zxcw{@=O)$>u(|?C%&C48T!|GCN;RjyOGDh43qB550aIMnN)&Kw?07*naR5~7y zN624S%WCLV)w+Sw+k=*wssAfuw->!=4DZrY#RMj>$wh?jFWMs^ptJ&kxqzMr`tfp3X~ zmr75=Ft(v!E$)%KAODPmL~+>UFb}h%tTW-;w{L#q8{hcucfb4S!K3TzBNH8`@!~Rz z=rqa2$WStIQ6=H=Fv=(j=Pj5UF#U1v+nH^>S$C+B zLIPhghzRMtHEKPwljr;_5v3)xKBv;igan_U8YXd&!=Vd*q?Cx7N3QEG8qr?RYMhD9 zcyjaQCIx?j0@Ze$HlOaY`PhKlnH9Aofg22{O&B?%0;MZUZMe0y^p^jf(3V=Qrr?4Q ztFBTl2FHf^T4O>Ul!G6~i0J*gg90LeWJWyemD;cAZ9ZJ%KWf8kCvJyN)DUat49&fQ zOm-!}wa1E23@@C%IYt?9)lOwZ6tqJg-G$+XPglRXVPXyk17&b3w+#U#2g@Ja<%}ZQ zNaTvG2txRK`Sp^#W4O_s8k2ziT>P7{#rks^Vz8l07(Dw^D*lX3V&l94|#1b1vC zu;K{B5#?0H0#=40QrbNzx^h9?L*_?cWB?sX_!b2frB+Xz?CRy}H#vBF1l8pUcI~jj z2GpCO;p+|!*Vy1kPZdqfiFk_;`_s|e)wJ&YF56A4h?`lN(^%Ip>U<_xj5^DQ@GU=hzqh@%2+#MwK6Asg_tocgPaM|BG9NQjI@@(~m4i-L8>-)Gbb zkwLXQDnw&r+UB9hKHQ%vQ3u4T&ZU0mMEH9xEwF*N-EbA6(Hohb% zU9Vf81xsHAXT8^LUdM5qj{|~JY8b*l+eW}%G=`t?8`xLZ5?wR5c+-_W(qhcS`{eh3 zZ#+^ddYWV!XRhY7tJ`2Q^Zc#}jHU zA>SKvwr2v1JbalFA{pDKWl@TZc%5gA*uEZEr&^bQTdcTx{rUBUFVF^ltWR5oCfg3Y zFwwGq{~&4Dz44_l{orr^<~P6j@RvXTrIu-$iU>`U3`tb73aQk6WXk->Nvdi(t7Q@8 zkz`_-xEy5M%Wz3U+U3NPlv0#bRP*iIuYBRot9NeQ`_da<|Jql-@&EnX|M0(KHc>)c7kYGbbu3C7Ls_iHJDmoQGStZsnYxJb7|B9AsYO zy_<6$2Ff|_^T49QTvSO_7B6f=MM{a69h;@dshslVlqny_!^aQ}%1um9!$E;7obr_<@>mu?ZI zM~|M|yZ7q7yRUxo(#M}XeSWyUX1++8DDUbpKz6DuH6!8~H0EJIm{KB5<9M*slPaij z9Opa2vpj}@sNqR4rBs6$PPfyuvU}6U5~XZHHu+S41_7!N>LnZ+oaYFS z+samVc&4GWMUL^-ae!KVGrAZqYaD%k{eih2S|!+Fcv=n+z_{~|K~>uT&hT;SXV`Ap zMa%7m57@6YOYj92jzRb-t7%tsN7jY3+k;~7($;qy{*g3>^LCse+tww{rnXQr%rWzu zHnsvVBn?OkUy8XdQm?EPfjDH0{4Ne>E)-}qG&2eei;Ze(H;6>0Is6%N)?Fh)1i0rR zo$f^%ktY!)0vkknyZTQ-jU(pW3mZsVWfNO!>v&yXQG$l8CDWZWmw-N$r;BQP@4{&lWqOHyYp^d3=g z(Qibu_pdZQ@FE{f%EZ zVQhv`BD&L>*dlPVo>8rCYcFK#S~|6&Ay7qhO{N&Ame$Ph)Wk|;Wlu{%E(sGerJN`y zO)R3qBuuF-4t48@wRd5f_r4>|u)!Ml0+{X{XMn0oOQmom9A2!kbnP*ks;Ek>@{SiW zAJ;fJgnV7O7^-g2Hy~R*#y`(kw5lexT4tT6b=4dxYoR6NOw3AU-f5w#;(ov9<&!vC z0+PBTD#V{u?T(>-e8~w8vYea3%yowZo6+0Mf|={_(aXH1Nx9ay42X8d8Zp*t8$VY< zWI1H4Hem6K`t?tfpomb~+@D0-IKOTYP%*4^QE9$zkV!+Eodb`msA)A4vb9*=fbkTDzy ztkyTD7%DVQd8xaqT4XxoypzqiZF-_G47G;e%p*{g_#0g-Uad=^NW~Ck;|xBE92LTK ziAY4HY2bvF%y=QFEjZ3Mp?YB(G#dg}<)GoaTlk-O#LJp$Bq18W7HQbhJ>~Z64@d0Q zg{&Ym$X=pcRp+B`U~i6_w=Um&^H0C`z3+YU$!8Bg`&5aaKf5kBhG9sj)08=sZ0@{N19U%vIq zm%sd#FTVbz|LY(B&ksNP_;fmE%9*nAq^gBR;liw%CQY0Y6E#r>13-zw9e`$)iI@%C{5$^$tRyY zdG_oJcV10IWxI%}Yfz}YQIn%s(fXofrN}T0q%@V&tFL|GKmX^y|MdsI`N>b-lT%45 z(=?TVA3S*Qr7wN)?yE1~d-c`F4(<&tWP8ASzk{~pl&bC; zE+X|tb*9co55tgZkfcdpv|q}WVt1Wg*Y+)gwl#o7W;*ze;nCy8`&;gjdah#`y8IyO zz&h#>TN_kp`<$8XrK&HSNhzedA|~im0jsqc4mtL^LM4kE%qVr{Nb3OkqV#>_?U`Z7 zu9_fb$bCo1ZjiQd-Hf7MZsARr40sI%qCfAkyiZF+8yjq~#p$7QH5d&bjSe!m^G57F zMi{jXPQdVT6-=0;$5jX8YS(PZP3kr>CWtT&{n|z;-H^Z#z_2p14!IqZyq39htahaF z5;8_Sk^CscE&pH*&UgctT#j^g9U8vAWhoRCDKZ?jWlN2_u9AY3o#1FYf~B5+pqAB9 z^QmPiGF-wTX3jsDTQH$JWV#M2%G6!lrq_|)=u)~*jhyZ(3x%3KLsG)Wq+LCX_SMz) zNca}XNKRx<*H^ep!gm$!I`56CRRee5q!9JQVe5F+MMjqe71JN2!da6p3pUu%8NX5g ztTTLpsAN`gq#;q-S#n+xFxz>Pn#u)wj%lygg7CaJCySPJ;ONm8&Tf@mb#S8Z9p;H-JWWv1CU!`MsBFj^|s z3UUEkK|bKSm7n!MGi*~Q3$-D7PF=R}LbFQCd0(`YCLyS_l)#IXPibSe&d|_LTC)4% z;$rTi_;^n?Dw?AS8`v$C*5Hk+gL3h$v?Z%3N`|n|(xc8zVed{SODU(Ch$j+CyEITv z%#7&jQh{6RwE=TSQw3g6w__74#_&ca`W6BMtou2bpdHRmY7~sf5$^s(!tG|9A~jnm zm;ej}x3RL5XrmKn<0sWkGb19Vx+!>V+J_L`hvSsv$QiWVpId07{2V6cv(Uv@<1Js` zB?@uq>TYc8kgRCL?0`cI!K!LK1`~C^iat|jRZ8;;(m0OH`PQvlZ@u-_Z-4vS|Ni9Z z@%rkzT#M%2kP=g-o1{spOw%;Vcq*q-mK8G=*36vCG>yk;%;T`zoerZ8bjj6$rn>pe zqeqXPKYz~m5*2!O_3ZNU<~RTR+c$5$#5w&R|K-1a`1{|UPUGcnU`|9znv{fD&FK>n zv)19QuJh?rP!!W3p zA@3>%U;p~o|KT70;ge53yZ_?~QQoY+1( zQl!^S_$hcoW}YLu9uJLyDP;ui(4fTj9f7t9g(&?$=CZfC-rmj12sDjax$|b;k8MZlHJC;!hOo|FU|-rwR#2*HKpwoep==<}f(MKXut=v5W8)2@DAl6p#(j zW~xyZTVi7FG`8OoJ~)UB>2m43j2u8_&NX1A{JFnN~G?DiR7!A8aHbl zA%EiVgl(ubAG9@vuc;IgcG%3;JPbYO7!7$@svYeqflgWhW8|d6P?wx?M0f$XZXJLb zTWf6i%yi|PmM`DyXKk*eqY)hYifmMst^!15vzZY>j(8e{6#wnk-U)iroyUdu`h{%*H3R|z^+e>cmY7QUK_TJm@9ju`oD&Qn$E#Mw$a z(NbnZ=0;1*Z0%rhsfYY5`af^%%T)IsX68bxaD_B6m&zWM`4A)0y3_Pb=F&J04K`$8 z0k6@%GDPe9O-P`SB^D{MjYbClU7iS30iFI$H@7lVojiDzZgCoO!I|pI!ORuKGWYopLyU6w?LrfeW z)gP!6k*coKahU%x4k%)%FyrmM-i03<+bgIpcBiW-I5tFtxfjbCN+>(0i;=e;K3CV7 zIn}fLjydmk``y>R_LaBae*2d{|Hb{o)v1(pO3$7jbJ`_7sw(j`X(?qYWj4!kj+L2+ zN|EWvWhiCb>9No*U+hy#Dc73TLeHOGU0oliNrp@!bT}M1mwWf_{oQ~1`wu_-{nMvU zKY#R~oW_(T5i_UEOq{7^N~sVjQR1X2u?i(-p=R!sG!e@%440RerIf?r@cADe$T-P3 zUA*>MN@+h_5D}}0Oe`u>8FmA~$y&@zDRE{MIUKHNNSFEMIF3)AJR6TwzPuz#M3R_` zs-?xF$hwLV8@oHI{%`h2_|&qO{tnBE3;h z;_D720$G^WxxNr9>(pu!qItSI4wU;5AGu4QD9JnnCUD)mmY{K5b+mM`MP_yv=QUK| zBJ-W?Ab&98N0=HSC}fdgD@&z0wKDBmjr-xKz!1f$0cuhFx7$nX-Q~rc6(fzNRH&{| zE>6uHd!snEwAa~qBn|uOE-ytAC}OEe_y6CTr zrBNj!KCbs@kCDbY^Xx((A}1oWO4Lcrmr1V*yNj(;66%?r-C6XxqYqB)4W(!Z+WLP) zz$U=>ft2$J=e4oRZN!0kSA^ia2R2r?;u2a!0CA28n9a9tnVFl^2s#Ib_!F_R{0LwI zAeQzUZl}t(IUWrNS?|pKf=Xa!bN+KVTGoMbY&2;0RkiE4Pdm5OssxY)<+*!4W|4da?)5h6pqh+-jp zGx%3XZ&6sL1=-Cj%FH~UjlDpoMgQG`I`FDfS?M$Z?<0E=PV^Yj10ZL*ibq7k#IhjC z_&O|^gN@h17tFT1;MNjzHMLsGNUcex)Kq*M!>jhzJqPN?^_X3)j|d8nj1Q*v=y<8O z%^)+^eOD`z6+XAC`<7lbe0ZttAfnxF$3tEVp#ryEK?x>&r)i?HF6Yc<6;|e=5X}IY z=Oe`DiR3cBbFUbG(sBT%tL*?|0myB46{dxw(c)GOVy<5X*PR|uW0|HY<(-pqB4%D| zpY7UmiK;s9wAt2hC{%rExK`%CHp(sla9TmVS(`Bde^9%8iAe$+_c1i;oahoygd?V21vkdXB>}!nH@1IZxz> ztrsl9oSPG(cEWuzanBs99H6=on}~?>Yz=^i5K~Qklah!qI4SHD31OXm4A=cnE6Y?f zuUm03vC1rkd9BhE!s3JFRd*FCB0Qu-npH%JSX1WV#?AdVzx8L|`K!Np@ZiDq^>iFb znf8~;JTY?~s1y-Z5>_JS`Q*p(R8%-qE)$Ih-R+clkRg>F=Zk@qj-%?Nr=eV39g2`- z*2JeW4NN-8Yp=cb)?07=?sva?{`9ey>amn_DwD7>De;a{N|ZEZN<$)1PE~tJwTMtk zOhj52%9QpOgVN>cI-L&3$MS@ghLm4<<&|B^$3j$ygh_Rm@-AnYM;;Mos=Cc2R7x2K zItlM(oc{2KKa^5Pl|?DD?TEvg60vI4{Sw&RT&7e`QBB9V4UthLM5Zd`oOd#*(3ta1 zbh`6}SAO)PAO7kWKmYah^V2j=MNh}+^78WYM^9dQ`Q=-;Z(m$oTpgaLJbz=dPBpm> zW@DnHqDyA4PPfwH--Onj6e)2^`;^k@IGqk7^Dbr0nMEgJnO%l3Wg1c*QeG2V%*$$X z-MT>t*8NgSs#HozOYE(xMMX#{u`(4EVxq)Kq)e2!!Vy}&LgCe+nWs`sv#HV=Zvp3p zSw(6nkW->kFsbe1vwB2c$4$#@Y;5Z>_OV7a&-LS?g8o`nHDw`IPL#4NC-b6K*cj8S ze^TMpto=A=&LPawrQ+4-BAT5LQp!9}Ntjui6u*6XsdfK`mN_+QpL-V(W~l;F6$_OJ zf@WqWS-vV#ViHv)QD&;#OV!IQrh(sn8gruUAm5M!3KNigeBh(DyQjY1~JJZbgD z7GR+`oG1-j6e}&;&LaNU5^=Me(=^rZfEZQ*xi>RmNXHl)%s6WZypXG4P3t_3V5pV$ zygQ@IX{PPI3%-PSc8CJ z7Se7PK0?_c!*z)Z6=utr!mzGuLJHH(*b=*QLN_)#B)ck#s)GK+8h7=Fpl}=gSZYhK z(K?*^;h#=jttiH(5M8BpaPVog-`;O?jPa2~@4<%H&?%dE2kpOLUC#hjJ7lv1RWB`D>@nY^Jn5nF;OrZ&uS<)V z?|5J3N;%D$1e*>JDN!XqYaORkQc@A(X5SC+GjooNna6QF91f~l^}J3_El0*mDk5pc zf962Zw1UklDrpWL7B$lv8@Jg5Rh3$s394RcZAvdqXDn7+8rOp58icQbRy}Tus2)~Q z4VmUsOJ;jaESz+a!czHY3)Qo(0>h;&{nj@&#@w}iyLp9^p#27gp}NVe)?~*L|EENy zjI*y`6&6k`EYb#V_@~jL%b-a_suu4hW~vi6T8_XIf0iQ4gk|lk znt(2!me#cIqUX$<7Tu{$JBpW#}eiB|cT+-=jgQpzavKDLzoIccj&ijXShxvG^C z*F8RF&QB#NQAqG*CDK`8Yy$^U9XKUU1iK8+nwRt1JJsX|&lx_|vRbxQ%}hgTFCaIlPEP#uyeVmTMh;B>;jB) z9)}FG59zuWBCxz(MYW1~X`uGBHs%(AGuXx}E;U4@ad=Y87FuE^vQ$NyZ~_0pH$CTP$K&zQgGVfyiErP&&4o%)8HFZF zi7$5hqSK@zTBsx?O(}8aq*Qnkorq|fru)DD=-IPpw{P7_DKQsO;(2F!QO&CJs^fW` zF#&BEOZU&k^0|6gd$;NEPYO=poeTl(ZNfEkmAu+VGciXgBIvyl$*n~;`{)MYN3(L+I>&;6uX|)QAW=z> z52`An(=;_(TL6I$?Jfr#%nf?|P3K;s%d$Zz(o5io9EU-#G_ujr=zP8#gB&Yd*YURn zspgAP`>~!ubBJ|^8>iAJ#}|6$h`H%1FPHH=4p?W#4i_jB2+q2QxFJBNhfz9F?KSMm zii4E;Bl7z31NLi;yX&tCaS}mMt(-kz%>hAZFQhTvG&X7*gkfKpr-=M<{=xCnC5sVN zrwoV;^>me5-P^jNy=xmRIYD6Ft;_k~$7WAnXE-K7=V9>C8HAlz=_-#=-Sv~vF**y~ za+qjP)}7NL(_Z&(*U(<*xVD~diMY$Rt{Njb1Ui7sL0?3cv}*Te?RL?1fYvf1>F&6G zyLSjA6*&-*x0dGl5j|a9`V7;S5>xV|F{B_-`lxmG2>R;imGjRAyvtte%kd$4o?4QI z{Bb|1H?8f;BJigf{i92d%7%U;+DoJn|0**6g8%>^07*naRP8EKgCX#3?n3%qxOlRQ zXKO^DT@3^KL&<`_f7EpaYI~9C5()=D=GboeH8cuT3})ta(zy;a3}X|*0p_Wy)>78! z^Sk3$)v@}bC0nVrJ`@2ipCivTaaLw(PKQ?L9Zi)X({Vf=>t1v9zAgSWHn_ZHE2d|B zyBT$T&oQ4urKIWG{9!w!=Zr1Xng+6{qJ-Ga3<=tZCWFAXl(~i0A=ABUc`ip9YG6ai z!XN@M<(#dm38iew+2|5y1RS>~N>kEf}WqD({1P=`^wN!9b#L(J+~ zXSgojH7;bDp@iD#*kVgoD5=P_t}bEM3;S2Q#`{qO2oV2Wy>KLNRS`H?#`!7yfihUf z`(P~wh{6?B&B1?R`!13k(#p;;nS$EQE?C}n=+>a6D=|}Dc0>5~R8?s?tLNy_2+ z@ZiDc%vlRvT<&&xH}N>6UEZY|`-@Qqo{poGvQ$$Ji6$myopjQq6HOm~`pJ_gPrms2 z>p5p#wl1P&ot)d%&dC4&<;z~LF(XT@4WNQ`#*i}{hz#Fg{4fV)9Ly1 z>zfysw{O3EeUvBTm59{C$4V)A;GFX~PRyyyr!+R*5fM=$N?3_$S3UfZnRmP04!8bk zgQ_#OX6D)0=Hlh!Y$`GY2hKCiVtbi4gUdEhQEL|tEH*eJPzTMqnr{2r$wEfQ?;{^p zl$3QoSJMagTYV8YkmxWsSf|D=3TK{Jcm&MxgOg#!5EIy^^cMd!Gfs-4qlRW3Vz+#F zd=cqI&xxqU-_-lc9sy!OlaZg65BltYRHy2g+Qvs)_*)xSyA)B{0^;C1g>gzwL6O|R z!OMIjs6i9rN3%kWLW&fEKgDfE79eS<@idL{-lG>+q15ochVhi$Cu#7s+lwyqXIdm~ z%}}xgT8&JI5_dkw$%5NlFtUBiWV(1K6peeu~)9F%$(Mg}xA837D z_9DAzLvOqKkL?|{UHqLi&+|lszuf}p&~w)3Y+S?e0lE!Ybh zX;-ZAay*Im?Oj~OLB(*d*nLczbV(+d%S7MB%+wx5)n!qg@-ZZa(5H1J#x^P4aOMcS zK3Wc;eepyI0oL)z61b=&)H5gwdhW_&UwCzGvnyO}{rTEvV+M+(%{M`0b0VWo@tKsg z?r%%*3Yw(!61xf)dCi=^P9v&kx+rn~>s^B5F`Oj^hbF{C>eU$E8aO4YAGs0{go{!$ z$DC@Z9Z^ns;3P}MsB=E3=QR?hc2|Uoo_!qS)+DqQ3Npvu#U2$FHXM7B&Xu;2;dreL zU}qtdwGAIymH*_D|RSBX14D{QKs4qSj^2aZpQ5S1qaZus(0q{XUaJ-LBiT5fJlW+ zgjIB!ro(BhD={h81cPPLXOCTbsfX3$9uz61x|d^D<#ZXN;~{2oIm*%(R)M>Urv0(7 z%ilz~tfG8T8o^Dp?Y`eaAzsYuN6o65s2xp#Fo)TUjjjYVcD~Ua0*?z3@iG>lX`?W@ zwCd}}e&;5r7ek-Fygi(?AEeiev+w?T(shV2jFf0^NUzf~)MIQpdyvEkM?`Mky!rjV ze*69RfBx>f?~3Yi)ZLzkDP>M63lR;8i8v=MLe)@JCrb5`B~>ETB%@A8-4XMUxD+Z( zC1o90lgff#t=1tDrZ?Yw^Tv&fS6_Ygpa12*{pz>BP7@u+!!&Z4%h*%iC(0zsB%~~P z&|w&ex$bCS9#ceAR7Dc!;bMO=iK?Cs$In0iT&8mO?%f+VZVbatRfn90G!T*IJ<*9& zRf^Ux#8YBcCK4&a_$Mlc$d#kK=fKeVvm&e*BnU;$ax}`~5JSP9u%u_~5~V)Aez; zzsxz8lAN||p*LM-ggu z-tL>@wq%e+fn7d3B+szO{EQ713SpV55_PC&scF|W3dYtATfi@+jSgMgRCdG$$x_VD{zW#S*Nas zpuR;8L`I#F5rGant&Vg>*0~08HidthEaKeHs5mb#Y~vV0ZM~HlAX+XWBg>w5&fe`U zhY}jTCH^8M>ST88Ek?fIu+AWdUGej?wE#j;^4~lLWhqplA@L2?F_#UC8nw zOO*arzY)Wky17U~k!gkr>Oj444{;5{RwKa>no_$98G&LW&b;x-QJ32a*2K-m4v{e{ z&ghUMCQDy6&fFRBszsJcvvm&BrJQq0oiK)oiWGDmwQ8z9N-0rJmcUi;%zYY6g0wQh z=ta&*_&5o5NknEa@YHroFjJlNzgOuzP17##QP$W6P7IM_29S95Hcql8N>U*uVXk$x z*Qb*d+U>I?a79FQmUtz(F4*Xjmc?2P)D}Z5!G)@ewkaajd+_4=aO;V#8QQzf8+2_X z%7|f!1=((3>yN>ikX=T$E}4EIRn;xmxkZEahIZ_%Ym*#@y=?|X93nXmAEz;hn6@!N zbh2BR%ncH6U|VHBxx0o2cMTxrcq9V*-omSw$xj(R{%v zClw*qLZnhC)itz=Km}QM#3`zjIqxqoL_~#;RGj?2WoW;DQw*5|kp-9?wN_Af(Y9EUJ_vW_T3b)>Ga zYoJ)~s47t+zwp#|BX~J6HKTH373B@9oL`(PJ6Pv}cv>)7!(wOb)H8g9N#NG7TLjhh zHkdKtb(o)_iU=NPB|HO>|I^lLo(QVia`P{*2m!I7B9(&O`(p!%dAj6lgd}#rqL?YA5`$YiCyV zPpnvRzQUQOLBk4ytv1L~WQD_D;fgpI{4?HwmtAqN3*@TFWeW#H#_`9AxQllXUh%&_h(GyY%z8VU>%I?i}D0D?_HgVV4c>r{OKcRccN-V zsd>8yqQNd2DTAAu=}OBv2^#}0^ZF@$)9H(5pbHyd$zPbXt!koKW<=ypLI+xumXDka zsKdIF6a4ix zWeD)RIYWa?*K(`%E!K*u(klHKsoGLiX+GqGY*LJwN1XshLAk!&ChjrYXoZN3w#voK zvWl3O36kMv{BP-PA*&%sEb~soPNV8oEAEpvI}vCqmq@!&O6RW zZ)xx`FOSp_=o!`BGnhqnDy1wnD`|zgKSBJMjm$w`9K& z<1lav!7VWc2c2eYaQ=!8V(aM{AhBPxr57UBb&Ck0E0|f9J(#XLZcQDbnL?Q0y7u5Z zZAFHo$eh4U%FJ~OAx`x2%P+tE_IH2wv!DIbKmF6`R8FTU=e*CF^St&%Dv7nIvQlEr zIoJL4Rh5XPkRF71Bpu`;6_vz1@Nlua%*!_B6&YD-8EQTJtsA%h_6Psl>#x85A3uKg zy`R1J^I!h_@zY1wr>iQH@p{sM^ToVJjB*hsBH>)eG^x9BlP0Ofvzb!n-Nl}TM?PsO z&#s>5oNwQ}b+Oy4mT45xsYnqL65)DeT+*o&5gjts9PaUS`u*>Je>fayi4T@DmbAOc zfRmjQlQXgCE}d4`<<@HI&C@jAxY)h++G{`j;ScZMzyHyPAC2X78q@Xl@mYR841*Nj zrF`NtPUZK%{qWQKpM3dGzBuGvO2@j;>Z|}Y1UgFfFCsb|4#(qhm-3KuN@3C8BkmB(=q@ zu4`+isyR0kt=PsP>{P3Z{CP^3M?OSGrx6{mYEz13bh$tQ!wwvM9N!{uqrXKq2IMzG zI@cXGj!shBiHgGIG}f^~;Uh$B6Ls5h*z!$|-ZKuQD`@TwK#2L)c;Wcw=yH@s&Y)R! z;X9a738Ow>eS!V3HjaEF9P=Grc}1>9ZSPuBWEV5rCS0gLkxZm(%QkHmjnzZlDgf>6 zsdq-Q?v7)=GYW!LifIvlq@pVzY~N+0;WG>a>VGskh0$S44I2?tBSpCW=NhjXtLuI2R7s?sMAY(x8~)5d zM1+^CPrPlwkB2K$KtrvrLo(Pki#o z00bn2AyK=)2hO)~yDV3IYWqSM^5$yb_qsD!YpqRc!Xexz0l11St-ZB$*;{u9ZPWF| z!B2K=N5eR~E)kItxB_pKcIn!x6csj^HP%V^-ol|xFz(>`dTd6m#zsEdD-9YIeIra)BXH6ou zKqBJLd3Kn0yTD%Di-=@`)3jV0r_v%)R77N-2h$VND_6eIh@P1_tH``D!YIo+LJr8Q zs*dA0j-xD_TA1yFVl;d<-LQFq(S_|9R&-qXZR9Hcsp@YcSeImQY>#+4{;2AJtYq*( zPNK}r_29dO9``gFu(lflAaYSbBazlJ5>G+%99CVGb-*Lc_}eaYe@^vv3EF6h#p>ap zHr67Cx2_rgfPgV7ipKHB`IN{Ihd^`?5rPzEqI$sTFyvaDezCv&;v4sV@Pohp)vtc} z;fKE$(y>g(Ddl0OL)~+MnJH;vPyi2S*Rh>?$OnP;7U`>4Ua<|JQB1)>vs);zIq%2gJQ%Q-3 zAu(whcsv|G`Sjz*PanM@uQL-d6K7h2EiAZZ7l`=rMx8XbbU1o*`(0F+Qd(S6DP@{Y zd7r=a7vFm8*Ka+1`0(kY<1|gYX9)RZHD9 zCYn|2Trw>s5vqDTj;GU!7O!niJm;LV)(;=JE#OiZ8l(F=Z#&L3D7Xcp*$N#M&PC$C z*ye@K2YC{a--5Pw1aF}AK@iK-f=$Z?>O^_o;)JQL8%I1NlCIeqT>@m*U>#g{mF#9( z0c6)t*x}%WUe3l>hbN}CyCtq@LDAjWeH)mOCx(Od9!DPxGAwyyxEUx5$eb&)7cHt_ z*dDa&#;y3px{K+6Eyf~&-S)Z!B`yPaew;#Dx{^Zfb?@V&jzzSy0w*R1AN@m@Z@iQw zAVX||gdJ@$lY36Pn;UhW$5HV@4~DFWggf8~wr_JUM#v7V_F@L47k@C6jU$q#^IO-M zelcShx#S#z*%Y;E@3`tTs_V)Sx2+euoH?T&NL}QA^e_%R#{>!nLhUEFh&*y2>JJ5@ z=n@ql#rqC;m$6-OI60nI*T}bs)eGOH-qKx!hRC?z5gAN6pia8>(XC#ZpRyKt0`at`fJVaql|rIm8@rr>I?I97EjAs5wTv<6cz1xmnwz!bPoO%zM{@ z;b^SE1!^XS$H0D;I zb_5>+*Rb?%i`IDG{;ea0o#3S4%+ZIWWy=n1Z`?E8PD-jIUQJd$tnt`_+b|3@w_;+l zFWfN(m<=bfrnzPk*PERPn7NSJF*Zm`O%Tml(77gekv6$d4IM=zK5|;8s@i1OZMa0I z%FL=8c~73Zi>FUJkHj;yeh?y3_tZ{~@OocWnCpR!x@;OFDP^9knXQ;_O|Vw9>S10B z$5xqE%!SmQ`C%woniYBF&|WU#>#Z6p@^Vy5eZ?7w0CW zl;i1ooQ`gSh761*QoTETKg&&ccxiXPFf&~{Lyu6CbGCJ zH>9vj4yq1;Je)XD&y3v>U5As}{A4?5(GXPWcEolD$oU3N!_HvN?S#ttK~*IRF%5ZM zEe$xrjEEuf+zWutT-_yNM-c*d_nm$)q&!L~mzVn@)8*ympMB#`zw@2%Jb3Wn>grGx z`Z%WDj`KV!>M#sk%EUxOx%!1dDWz#DN~)utXkt-LsqQk9c}OW~V#P<^Je_DLX_tp#;7pWM ziG-LlWln`kS`Qa0MH3SbyFssxlN4putK)c_%4xj)(&f#YmnkKZ#CfRamXRdNGH4zy z^F_)!Y2q}N>CuxX4<9~U_6ABdY3qilC`w%9K@r4Js;dCe49zPj&oNf*Vz=AfxpU`l z{^oCf`s1HIefsqJ`j{x6E~njaG3<6k!mNq->iP4Z{p@God*^#E-Faykc16ZgO3s^m z8ZK6_(xu9$(@CeIsxnPwT#?KV~?ee^H&Eh3#^(|YaSY~dsXYX+!I*PUH*1tK& z!plXlESzAY0A~>h!FA{4=o&cRs4&5(x82@SE4CqUw^0_*^Swt6aV#=qWaE=J^y)1G zJ%PGJGcGrZYxQTY$Yc;rL$zxVai&(FXS$=-zPf1GfQ@p&aZr1kAw(e>RyNcMeD{h0 zI&vPjBcxXMCf|JXf^+S4SsRXxu^t3{ zl>He6%1iiV-li%icI`THBv8;F>k;LpZ5;17Y0X@yYWFEzrrxGq#&+kdg0*fciEj8V zM{ks17tgLHIIQC9XbTRLE!>=eyS^N;4hL(TVE1ZWW?wh}bvqTP`L#B<65#N0>g3R% zpi9&mAhOiqLvVJVV`Dnc<4B!DZaZp-0>~XMF|(b`V%%_yiZ>B|>&s0L zu>QzV9ktY+Mg^c&5DJ$8am?we_m;R@)_I{jA)@oaDLukjs&t#1x~8`}^P?>UR)l+O zS=%pf*dv!YLR+BLh!B|txR+NviU+Acqi~Wn9f%|wk|@4NJHA*i8y0C9Z7qRYQRTLJ zQNqqPj(aMNB2|8Bdl5Cu8%Uj6c+lGgRYZm*QE18K3cVs`sZ}Qw@R3@}{vlY9+6 zL?Llz?6OG4xhVqwZ(X3UwnXfFDTaUOI&}4i4ReU7CU8+|#@>3T$!)1>>)(tVgB)e; z2wC2m`!b#^$b0_dF0RHam*PZLpnovP#*MTbZ(#XfglsQXtm@v%l@F%*^`M@bQAN4T zDB*d18y?Jh)Ls~tcs_{I?$o^sPp8xAbh0f|jP|Ip(o;wmT`#7k4l9N#5LeA@I2&Wp z8`zyXI4az3C>tUSr2vnftF-K?8Z1?|1r!(UHjP{xW2&5Orjn?Q6jRFADQb~ z1QC%g11b{l414Qlw9hT}rBF+6;z7Z5dvqE@?YG#>%$!wa_D?NJwR0k6IG?m8u}GC^ zsaYnnjdbT`3n|P@O6xL+LOsjckr2&wv!XJjW>XlWOWKNQCrVV-Wv)BsoTurOQew`p zeevEqKluJn-}~|L{->vLOojH8OUXKrs%ok$ZAwL_tSXcev8qg(bCSCHIw=X|Or_{F z2_>#ea786&;zWn*)5UIYE32x?!l%Q%nr7Y0TpiO?v&KARI*#KwmNJdZEM+REi;IiA-|hFq zs3$Et@9JBKGm++$_~K%(ClZ}Rrw0!|I~)!#z4VeuN@)<8L}Z%AoVk?#lmKITr;4$$ zQ&*>ot8*99BEmJBAUO}yB)i1J#qj!LUx!)7f z)#3Qr{m(x7@T0GM{VTU#x;fRaiMO5lh9HSw-uzBry}Q63t{T8zY)sQ~Q~Z z8(K_`0yTZyt|DSmfZM2|{z@q^e5tVf3*_K_XZ1)kCeEoEi;_}eqO1$kw5)a0#w%R* zG~^dvtx>y+jU;S+VBaA+b+wjAh-00-$RQWt0NS56*>>h2P-5x)uv01`a=)<+KC+;# zelvwSTxrL^-LWOApL+`2o)LEL*QIg02`2m#S;R{ipf6gozll z0@E(D0+gVshvk0}AR4v9*`PUf?u3p)d zmcvq9MVsjW5@`2pAdYl(oCZiLrna$n(+#wJ2}M=Te>zjeM*>CYh!{gbEs8FGy4g&_4ZTNK1o#TJ++weL%HS$_sc1v0ohUe6YyqAbIz(TSyeQJ zt0t<3VZin#7Zjz9eKji~BKFm@eh^9_DQ}=va4$SX5a9)0~PU1pqchL4k@M#b0l9;Dvh@UDw^@J zYRfRxT~M{+y!1lUex-yAw;@&ADH6BoMTY=N%9L4E$8oHcHx}=u<*XNGuKTgpK-3tn zFmfSoCU=t;YP(U5ic~9JiRbDcL#zhClS(0FT2s#$(5~*Z&H#x?scu_|T{wlY;mur! znE*4>;!8VSv{nDLJ%*-4jEzD6b1h*$9LKTDwQ)uHVzCQ5%8ZDnX&T4zbUH06C)E`R zoIVYzn25N(&EPqyHBz%mWTV1`Vxnd2K%;iz#tR`(&wgtItN-QTmiv3)It?~lQ(E`C zj9so`LpTk~`tY$q#E03t0Ca^I9cmjTbm@vp8T&#*{Qckk!@Ya={`Ft~^=ChO?+>3p9Fi1~d6V&RqWQ2cR#i&MOkA^L%t@0H ziOR*r1r<76T~FhbIZcxs4u{8&AK&@HOZ$sG=}xJzDitbJl2YQd8-}Dwn1__9$is&Z zudc3c-MS_1I-Z7dgDn9$(|zG(mTa6cr&Ch^H5b5~rfEvVckkZ)-uJ%u-h1y~UmeG3 zy1G8@_ow0Fh9y4s!|u_eN5A;RFW>t9TQ_gs6p`I-SIVemcCVp#HHTH7ibxr!Qi>L# zU5!qNnNp%WBo($rBv@}VdRH4nRWvf~2gZ5Mkh_dR{%9NL#kZJQ+d(KgUk!p5GLOt%|` zTYH?;IXLa>sxT^5{M{)B4i1hTtBVe6*JyW6OJhREoH>$-lp;mOA9sh&UI?m75do7G zCI(K)(MBSiZr3i*G`7UF`xYo|OK2S*Iu^R{2gQ-vk~#u)(l#V@#9~gzw$3OC~ZTKLjeoT}xbM>q6e@!pN!fDJPDlag$!5 zpe@m~|46Z-)x=k}+=wn$^p=j#(S+lW>adFcQo9P}dR+T$sa{6`xKHiIyB_V}!{^_< zvxpt}TZC&f$^;Sgf)Jy#W66m3}8=7+%SP8NU9PZa2br#dAiuWEhdt2*)cHGcqjvYF#|gW#Qa9vT6oxmrq2z+B>>%a935-xw@S+5!EWxVHk=^%_F-|9GzHH1N`Bv zzS;#ga0=swz>aZ+45?X)wy`??7{_uvjN>?#NtF}Rn$1Ln=REJ>P_ycowu%HYbHz); zR06eS_Dj7$v}?j!mgW9L&*Pv`uu#-VTTit^M~5m~aidoN+I1X`1|mWk&d|sMG41Jk zz1R|8q2A@5ZaGA}5dIeHw`fENv5gHI%*zJzA+&RS+3V8o7l{;YX+|6&sI?sXB?>ib{% z;+OvYYk&G5KmPG=e)r+Ct7oV2KvXhch)B7fMw!x(XrP>l5~rk;Sd);F?syn>>2e_9 zqa3tW2bc8Z$+KZdX_xYjc4?nEGfk>Im2o!=11DA@nQ~^4>C;a?efsq2Yp=bw#PnHJ z5>b)D++nrTe5VFZviQ`gfvVOuWTFD^)*7x*LKYX5! z$K&xha3a#2({8t$Cebpz|Ni?Qe{}!m?b|nBx)@6>x2aCHYG#V+K}5@tEOouH`ov)v zEUA}sE|hXk%jPwW|3a5-H>EB5SQR^bRpj&;|3rU+T;+B#GY$qEA}}P`VcwNyE2Fi| zj2Qb3+O~kyM;PGReOD4y2SNAlVz!N6rDmfQnf){ok>Vgu^WHvOS28QSdA*i!Eq9%P z&=QmwV~N%URfW&=&=y*+wWgVn;f`?B8Ar1H=+3TF`{|hz6ScN!T7k;u^aL5})|nkN z{IiR>33SU*pc{#vA4>xqw_QtQtcFoQVHw=ancB-S-$9OibKG{Mby#fiVZ!am5;580M&w>p zD#RxOHyki^NbUF){f4%l4Sfs4gx>*ioOduCSjUt2R`hjSW*&(}I9m?E5uVy@ZMrXM8ZzW|>lk`&SLle$;$iyk(U3u@Ig-1)-GYO^_3U6rU?A?TKZZag)7{xJ zF-Inn7U^w%go#*{K}59cKXpkuMD4<>`_!>kNxsfxHVrGRXMXXG+%?+U@RA1_bPSgkELyh?j0>j?Qh%ovYTEbTZ z;u@2cPwLuxeWlR6TLr_GN~M&#e=es+!z+5sJgF2ZI-g(PtWXk>ZVrekh3HOI>-;Fr zXhh2Laoe00SSoxQoiTP1C_lywXp(K$b;j5Km^ebHz+LGBxb7+iAHr$txkO&@EwcJz?87s>c&inoTD_{A_-~R32K6voir=NbB5)~>#%0-n@8uq(H zN<%uGh^CZ9xG?RB$3jFk$H+n|B2yX1@%i)T)kLcg7uyS|vA1?^Q}j3;Rdu(^UwY$D zUb_9tx4-q5|M8O_|MJ(r`t`3rc=Y7a)$yv7lTu>ZjmL>6(oB>orIa#p&YV&iMP*7U zU0&Q^q3h>YrASItiave%loQ>2>2kjxGVgYY^N>f)IqfrNRnj6U<;3a9lSdC8eD>9^ zes#CYxB_WB+!_O%ppDzk$2kRP#ngz;+RL)_1T*JMr_<@ijT>)$|9kKL+m9bTdZI+9 zv3^6u7Z(@%As<*T_WMT<9{u~fKYim*zI^+YS7|(vx0e;wrgxruoWEU4$$a91KvqpW zgH}gk*MyG_yJEuaCGl;z7 zaQ&TnUu;Be#NWs_gN)TE(Uy@Lt|J2b<#t>{rcqDdXbD)g&6gS9Y~YVnAVU<&Gb5bX zX+_D^&e$WX42`Hd2wh~HH?zwolLDLNnWVJMS({6#-)W}s^>Km&a2;b^OwTYGz+SdI zjI$k6GYZ!kJ|n8!=Gt6d*g&Bu(O-4D#_qaKEb3t-3V$HS;v>9oiw#bQT_j`Ij(qb2 zD4IH%cPKf7;x=e>Ph%3$@yGcVhDcb0qmItU0YQA6{^0M(CDaf{rgPordAoIKFA8<| z#NQD~v{MH#`cj8LYQJYfra6>Q3fRUumh+n51@2J#W9Y?}h8Suuvlr;nzOO20jH}Bx zv=MW9+J1Quv2`?}?7EEFViCgJ(q3vcAG>xYsTP2?v4nX!qGk!&*;f}s(oXM@1Jo+8 zYd}jVi6Z&XE_Lh{MU5!%yoAD7k5nb;$|)0L6k zF(YO8>fE_CWG13rZ{i2h6eJjx!QjV(>3NBxDhW0p8K+W8sV8RJRE1z`%cy$<6~48Z z;h)l3?FyH1z}h(DjXa^A1!*s^>y!f?4xsWpVvRCOJj zDsC$2G>wOGstW_066GdnG?lIuxHSdg*hS!QoQ9YL39QVHSVV7TJ)-ui#`;VTA1O7( za7$wLRC{h5S0ij)p+23rP^fLSoq1Bvby908Xqgp)>t3aCG{`3EJ0nL}ZBSb_{i-T6 zAp;o(SI4q=6CDXrSE#)ZWmiOyC}U7PFFg3)g16t_r4*i z$d@9+FueTo?YH0l?(cv9n}7Z9*Uz3kD3HUns%J>FfeHvOPLo7bIy61@~GE3 zswxqS$aES%`Q(#l&z^nZ)w@KL(ojF%Pn-)+D{d^5lpyq}$e&KrIF=hXZoT>DEBEew z@!KE&?t>3L_{9gm_~3&NKKbmE!||$!BpS4cB%(~pT9QiUlw`LXik4m8?em`ZG@V8r zCsiil(_wtZ&xnX_UJe(-kamgjr^H|W%9pFr+pG;EnG)*cy znNk{tVYog_MfKnQ?cd+}>$h&de0zV9b~H@Wv{YX*&H2`<1Jzyt5s`W6kJjN9m;RV} z*d<|ExB5zTCs#GoHYeLSXsBwp0JOu3FzQ&pv9XOA_!jo%@HumB`3D56E>MF{wA@Hz z09kA8Ycq*LZmX(lSxNw8kxs2NArV?md^M*hhKnlKqJ-dYJ5=BxgaTb8RaxmTsh@o~zL%_|v88*1hG1QDiLItTVALW4jzSH0<4|^9bLz9y>#t z;{dgi!id)K6W0&Ya>5mwu-PoK!a0h_x+p=15^7FFJHAU`Wk?aaHOE-Qb_-#=)6EHL zh@Fh3(p2B~AtNY+?-a?wL4CkwduxNzY32aEsGVAMa%$kz4}mzkoJ_fOFI}!;%kvFh zD0hN6b8R2};dw=hw$oLZGk(%Na%Ste5pBWgp*4=>F!A3Zb6UWHYY?<}h7=D)hc&*;jqBV5R@mAOVc zGbc_dB~AFHd}9ty^BwmVvKHFI5}JD}+KvS(_9;EGPL{oxosQYR(XE|97{x@bS~`8U zT}KtRD6R96)9J)Z22FU)((+O=iK#si{2Yx4>Yc533nS0qscp1xE>Xel8@*i@)CJyi zICUkqDXMh~Jap7-e=gM8n-oH?nuO{aJ}+}jM{K?C ze6RC7QB3x4RgcPq!{Jb0w+KNY5sZAZKhtvT9?C^w4a}~p8;DO$4%QRQ>_Yb1E>c7^ z5G$!HW!l^hMyWElhFGEur}-fhxEI-VpjnWC0+_OW`^+Uy9{jfCmKQm zyOWfXuCFumq%uv@G)+aNz+9Ny7^50{%|EEB$Yh%=M^-H46GAhz6^gIx^F`1}gjnZ^ zd9?yN|IPKn%*&L#IZe@Fg3M8HzsS9`TttL4*0%-??jy=CifxN26jmuYwyDtCm8))h zq$)Z!M7zhn{s4?^9GT7%T)*6j?fTais%OxcA#cNvFwvcDz0!rY#T=B5d%#Pe{69&1 z*QPseBTX<9s5vfF!?qyxyQ9L}Za1Bs?N3fluKx1fH?REs{rBI$cJ12V zzP$G3mtS1J@$K2Pn{y(`fj9=9CG#vf$Uf~u2qA@i+%wU3Jjpa?nfLqMFofON#QcP^ zoYMJo+f5iaauks~6EO!_vR~7@yL0FEqeqWke);8L7|MP$$L-QJ%@Kf^pse)QoROKQ zX$oP8F;3G|{Qvay^pn5;-Jk#b*MIxtzs=LU8Ag$k2`Sqh%p!8@)~!GMmp{Dr+RJae z`8tO(8MWq}&6QHx?RL{N4Phk8Wnv|BrXZ4YIX7&x*(|a!@p7=(a~z7983w7D#6ZG3 zDQXgb=p(P#L-dr!5T-ly-BRf7El6%_Fwdds!@6T8YEu=))=H=0swIo>0}KbO<-@|m zR@EyZQCrEdBF`_BkXCyHVN>}>@x9)<)9KQ#C^VY%S9c(SnpauY1o!t{AZi95A=s^c zP1C;n=yoWoE2)tUPx~e6ui_*ajrpbx*)`P}SmN3Q)2FM{s3N>6z0oc5L#^KlTwV6C z7S)vw`YP3JNTn!T>1S^ECXcIcj2^o07#LmX6c~pfgs?ekt)@JiRt&&m#se+^A( zJ6UFq9Kv$ARt2ORlzNv(3Jez1wY6T=1~c3y0sV49WEh8Pva;c_mlO$#3l1h0At`i( zB{@h`e((4DaFuP0N=1IFK-{a~<=P_j=WUu3c3f>g0j6Gt;OaOctB5eJy+#80gCy5rq(mmfT0x zRg;5OeinjmrK(mzo(Xmgka{MVK{EVlp^m!pDDqK}+RHFjk$-?qN@-uW5~ez0Hz#U} z$dcr()-(blKgI`f1`BmnSFs!-XBX<-CiDl_Xs;SDUV|baSLA$F&o@@lN`V1MlnqMq z${=+SxU36XdHqJx+$*#8=ny8_Pr^-gBb$jYN2;jq&{RjN6L@~Nm=^@^HypZg_VSy_ z>=*G|c~w!K4I|xwTe8;?0Ga#CpA~a4#w0nVTxSFJF~;pU{_>r-e)qfI-M@eT&b_-S z&C@hT=8%~rvy8(S$_buAK}3?w!!X2{>3kU{>Gc1eDe2SeDTF!KKtzJ z>tEe}_~VZc@9*dRJn!>7<#`eop&UZaA+vfbE5$jr-!OVczxd-iOe=Qs{T6at4xAp{9Qr1X(Ajw5r4A41fbvB2=B z*k-cu_E>GS;l?+{gHV%&vi5(t$F2|e#)7i)Z8E!+L2Ae1_1wgz4#U{QK@Ux_vVEnW&em!d}LE0Y2h7>hkY+ zYuGma*p~rBq@sINA3J5dc&hV)qIph;{a=u171-VyRS177+!_c9M zB2+07H~LgkSkgnnS2qz)n=oqkJR_@yh=+&q5x<(r@Iwwo##zuk8REG&lcDdO!f4X? zGW85?lz@~`Gsa%sP~iDN^Lb8C)&5@sdv&d^M7qdC`p5&8M7hYB$!o?medOkGR>h?f zyvEqYZE0hj5!tK4v(#-4$~K#2Q_sqVzVX@TUPdA>b=L%RNSfHE-KNo|*k~6g-x&!)xj~*tdn!-bHpO|?xpupy<6|K7%p6#QutQr-%K!0VZrwNp< z?L9vzj~9}_FkR`;R}D?07SptrF>WJ5h7|Y4j?641%!gC&MTFqMtN)>rQrgiH{X!hoNhpMYHgihMGS73G zIgyA+p#133qi?^te(&D>bLY;7WyPg1vrr&Pfg;giEuu)6Q#o~!Q%X6P!!c!==2>zG zArCYR!#Hj)|Ll#|U;Ei_fAhQBcW;07)mMN1>%ZT)apV4jdk-ExOtR0Mv+RY1QW1wl zLf|;W$Qz#L{X9uzo`dXU9`ejIZbBrYJg1O_g*b#EvJl_DfB(UQ2fN)gj@zQ%%tS%f z4w37c)f>eGL66(DMX@>O^5I5VhpQ$#IX(IHNALf)Km6g|ox9VV_WS*2vyq&bhb;3j z#{Jp+!w*0F=}&)t?}MwSrNqQukCbW89K4)u&gT@`YSJ!-N6YOg@ zWo3gc+gsI3i>a7ZghK%oM2VobQAYek5gWzVA%vnist&8*NF6n-<<$$6vhj;Gy|G)x zX;v?4Ewd!EYXU6hN3JMx8PiDZgLD)tuvaF552Dm+np{7xLjkplN%$H=cqn4sBYt;8 z1Kvmc#jq0yHfpr(iJ%XP8#$;ZebcC0a<5OOaKSC&5FZ8i1)m9GSP}JU)TI{0x(3p! z0ex>V+!~%iI9dPzAOJ~3K~!Jp@^4Utts6d(rq>DT+~~~%hFaToAFWBPzqJ)~D3r+C z$SctEV4k8=1oxHt8pB;leMNKOe2kgQ?z)oa(_;gR`87KgX3JmHBG#+gv!wrKx6>l> zX3Sk%Y3yYhMJbBv5Vu&syz2XjDC49aPR9MQu37bl)G$h5-qQytrR@CPTgf#V_HNC#vKlgmh7!$KYfU?2t|;#lpEn&Wp(K+blCZB?m>x23xOVx#=f?q0Km<$gYF=hckd zM5GicSwC^Tz}guA)VeLT);D*!j`uKCZS%#E0UKN zTGTMLPG^>2o03$$RquyzNRxT+E87^#jMU|a4(gN{0|RUV2L=E_`<7Htt&1O(RjE6r za!r~8FczW8C3T?y5d{w7=7EfI{GU<>=hB8#ln&oh7T6G_l=NIdWurWuOF|P=&2>3y z3y%X!%pBNeLJ`chRuf5#L`TMlNVen-n7Qm3Ff9q(!xRzPS6~IMVxU~lRW<5R>i!<{ zJmb@H>F`xs!6iG$yooACpIwAg>p`f8vH^%rH5osR|LH&dKTuKXGLgfc@ zSg;ZGG{}b01v+#%b8)fy^1DptWLl2tl)&})wsMrD;EA`qbDSjX{K?b2w(53C|D{k+ODqv3Kdm3D_vbg_u~flpj?~ zL?Y!_c!i5HlUMdV3?WcS1T9qRLG?^R5@H}eZ0=PGln^6Kq6-Ca(4UuO+K2HO@i=aV z829`AH(!7K?Aa5UQyfC!d>n^i7)WxAJd7ddnaj5Tk|dK*N?Dl8sbW!t-ulH`mtKFp zIC;u*2u#J|6r?Bs&b}<>E)0q+Fd&YKDk5Q~VTdOu+m~Ou_|}`hc;~HOzIySMZ5%go zjCr6$EO8UIoa1KL3}F*EY~zNBgTyomyIBljd5TKn{h)Vfq0%1m7P|m`Hd?-f8&jx6D1L$Oe9Q_1BaCMLk#tR(ZI|c z<1mhQ@7?>C|M$Or@!8kg&GuwFoSxsj@WSa3SVVU7yqo5iF24N!M;}~z<7ab9vXB)f zW(o!NRprEh zutd??9W(-#hDrzHv{7Py0i?yP4_3^PxqMnBRZXGDs4=`muO`+l5lli1AqL{Z6eFsF zh++*BUHp`<1akYm1)5K}<@#EJ7B`ADH_^Up{k?$@4#8F1DJ8$)6d`I*hnWr>;e~pR zjn6y#K?y$9o0|P(c%$elLB~DRAl$-0>EIyS*czo!<#;;fhf0@oo~8*u4MT>_tBxW+ z^wv|WhK=@2|LXxm?WLmO^tnNr(UR2MiV@jVH<83Je;B8=b~I%e`m8RP3+JjjgqGKs zkV`?wB%%>18vzXwQ=ME?H^?_8>uSL8qjwZ|T`5qj9#P#U5INnF z=wjyePqm@;^)c#*yh_cy4Qd%_SNs`;C_&SCQSZvI;DcI3nGj_3iQ*W!S`oPW%3x~2 zonQ)}o~Z{~uUiq(M!P~uU9Tkdtr*paRXij-_OPTG?O^oA%e#pIk73)hvG(T?BQE!e zI0Um4R*3}IG_dOz3h9MOI;c8OCdo_`i0k%j#dad)+ag|uYe)_4#mj;UrZN~KF5eC^ z*mD`lRQfsR^8Uy|RPa^-ql2!}2s0Bei*403vE0fMhyyW+1S-p3Dna7NA(kx*voMk5 zEI9{2NPr1ZjK->bsrhlP$Au+$wbAS+*Cd)2)>Ie4hD9395UyQsGLD0PGBbsRF*SEd zuN2XCyWQE@S%|dRjzbtoWKj!+<-5pgK^!VX;uu2YpqpI^SUJ}vy)niDom{t=E14D| zE=f<{0aYRl<89V4?GdNw#++&z%!CxQEMI@?*o?rtO&OM!{G?ZK&Ykx??~}X}{kW_G27Ehy~kW+0)>#ys?b~hcg%%Mkx#jl$kR&h^Tx| z&Q0pcC{Tln9FAsbtst;(uOdyv&Ur7|X66{?oU%w5A~8$K%pAr+m~iFh;=;^XBuSPM zJ(#JaS%?>h6$;E5_IdDO&BQv^TuliGt{5{HQPoMcz*zQ@c8iObcj1&sByuQ>4Iv62 z=580r5pf6tsYbMj1g5}&4g*?g2YdQ%G7d3id2+V9|KQP+XU}$1%0!#-WOH(E95)5G zh4cM9?e@E;&z?Sc`t<43Cv^i{(XIeD>QxsmINc265QkWI&`v4Ma~7c`5H9;`Z-!wk z7$cEsno^qia4pdh^dCZqAw&h6wq65%bE)mb4eer#<2Y8=NVwTcDLjz7x*a?s12yiO zWM*9@2W&NrnpTjAFXQFx%CD`og#kK?Z#RQc9DiVUVnUvY3*7c?h`&O^#7%6dQB-dz zVDyh|5v*J`WxQHUi=F{*=n}&`DQZEMVHseN@%;JoAH4V8wfEoumw$OY&-Q9B*)*j|Xdjry!1J&jm;*5nAp{)$=3)v|`# z@uhGC=1|HWFx;MKk5b@DBE<{}g{t7*f>zI!(VMQW(l;4QWOpf0i$fo~P-E13rK#x6 zfqK*5p-#eK96M0F*XUtoncUIBGTPOOBzcNqUCZ)(F#eh zQ5VH*^a*E#s9lJte3fhBNlGcDx|F`1;lucr-M(ZR3Jm;IL{)gMdtNJ^4TRLz)FM#d zM+Dw?7>23FVfA7dUvZmyHnvkwMAIAp(zP<(ID--y%IIG{@|UJ{rDtjv_$yX*9|P6i z5Tn5QXfKOty8e48Mh?cRrJU2*+5T+5+nr7GJd?z-95u#Rz6mbePSZ3^(>%}fyhqg` z7`hIni+NGf#&!_&%BA^JkhOWi5f~zJ+rN_wINp55uMwqqsT7?AF0BNhLP*hLViV$q zVZ4F`u)HBIBDBmz9SUH(o=0}!gH3C zWC$!lW)cp(?7>u1fx|$Va+Z)2KYH}&tFONL_~VaXdg+A_0#U@Bi3}tfo%G-;?RvU2 z&1C{G#`x;1ufF{9%U7?y_w?!0Z@&Hd+O@x3zwzzu+qWM+e5i*R4#Q@`J&5Kk7Bba>ol&^1SVZSqlgqDMleQZpDj&`ql-!0#8=tX7uTR1Scih-Q z>M8&fssy)GI_xT-J~c9SB{ixbKoq%T5TeBdXT;L+n1G7Xd~1OH0lWz8-oy-AJdAxO z)%{c2#dHhYD{k3{GoxP?m9j|v^ zGmwa$3x#)HtYtMhUGf2bOwwP?lU*EPD2;q|GDa*WE{z;Ra*flHQE#ya04Ti@pPLy}XYY`)cNB(M%T3l9{M(l@o*_agtolYA@^8VvIP7t}S-Z$jmHR zB$qD>RXNH7GQeeZ=gNqyd#5OXc^P%r!THj%nTyp_yV@5A8#OSp;ZvC;7h&{IzaBzg z=Z9gatNfOsP6#2lGbMTXbg&;|5fu#Eh%t0P<2aTt-YS!%v#O;y!?&l}!a?)Ur=zY2 zP+D&iGnIFvoNYK#Jaan%sj5>ER6i=CkDMJWfnPHuj?pig!+3%7^06IGv8dfkErg1( zHLduUB06T!F| zEt^QDc`oOomv{LXW8949Te@5~J(%IFv%0VGL0FVsnTaumAub;P!-a9ISmB@?IPI{~ zAruXDPQ6fI+~ml8bopGjM#=3;mwp15n;y${#;QJS(u9mIXQq}fDI!Exu*yRRqsZP7 zrQlQK)-Z?z`{q-o5+dkM||W zFl;Du8e~elc`mzJB;qvZr}K8YaN)w~>FGFbNJ0n`Ge3R&@ZP;USFT(c#&bj5i0r0m zLJb=%`HS6X%om`aY;=uB_-6?Lr>Cc%e)_wA{lmXLd-8NT+m}AO97i?`LyU8J_U!h} z@2*|D_SQR>Uw{3T7{)p6Lm2e%(ArIs-r4PT`~BG_#;|Owz{9Zg%Tfmxhux5YNqFAcI|wpM>nX^Fg%UAlQUBT8%TEtHe)im+M8) zfvNaKUawxUsV*xowWcv$Lkm1C-&bQx)9OU()&Z#1Tt9@NmTiHQx9`FjvJyt*+1@p*vN&Y^9 z2j|^kX_Ilu=6J$ zew)~?iJOIirIkaYDyE3&ifrEswPC@-Pgc^V$fpULO3+X?JP5=_oyk+bR!#!!MW(#@ zCe}Fgcx&`u?{i&6SQ0up=V2Ji%HMJlNfSIF=9@$_YM>Z~AOK?P%*^a38rd_9G`qsq zC5O1NMzb2%!#Ql&V-QOzymd)f2t9U26V>>15R1kXd(XdD-miPZ~e+u zmi{Ge=h`KU%y&6dX}o|(n9L+)drlGfS1FkEMBP({p$jo=bn!~9x@2Vt!dzFhxw@rF zruq#93`EpRUwZlFUw!n^wQGO7cI{e9a|nY7=Oif!2cb-L2W8G7WX{SvGfNg0xp(j0 z&70r8_rbd_ZpI`e9E$CH5TTyc=rFK8Aii`7LpU4?RL7CR%pBu|3opL-;>Ewa^s^7% z`|$4FyI+6(^=F^`WD)3D!ePo6z~^5n^rNz%C4lEitJAMKva6AfWJw>>4M z$iq11{eFM_`t_?UxMBx5jct%C# z3L)Sjawfv+kM`aEP}nDx`Yz*;>3=*oOnKh0Ehg*l*v%li^iO1Y`>qZvC zDn80}Co)qKeVnLSIO$!$rvOw^op&`|W2bPBIK%wE3W)Q}UML?+xen%XJ(K{cd5`e= zG1CS{rJiR)&2)cg_an&EFc{#Y;5AtK`_gWWho_}^td`~s47g@IxqfA@kJJRAUGAq% z8zuOTpj$=X7?lFPq{N|=WeVk7fz_4W~j9hHY)X_KedI%2h@3h1@GQQ zM;w`)ADR2+rn$U|j9+_nd2AcDRS+y`%5=H%LFq^_V}-612DAoyYjsg}6vSJn%efXd zaln^f6Yz)zTG?0G@FAirczxPYNWos&TWp@^-EK#8SiIJ4j@`%;6|$x%{DJWe!=s_; zoz#1uw7uk7xI=&JwnZz}VO}=fFZ4BIi2QePyWFo!&8W5MqINmvWkik0YitvdND^6& zwk})ZEt+Y|qapmiYfj`fgQQTy)JsC41sN8h`%w8@@1PbE`V{GutQOKVav29PuqppW z;z&Hkfrzr?vc>9tzc2Iy50a^_P+8tz<^j5CtPxuJg4W2|)z9-|l?HR4? znTC31m%8nCYR0UE29hd>qKYw*%p63TuOieb%AfmrV&=&H1RY}5S2G9{I2>Z!+xd`# zrWQ*`Hn*F-&=Ot-<|s1bfK6&DlVF#2Bvnf4bdGTW0iG4@8MaGCZKIy04PsrV(BX;> ze!Pj5w{k1zJkRq~_fExuw6+ckS&ptUj;UqzMG0L-=^!#Ot%;iVhkN@u+FZ^P5mLY` zlw%O)ERrS1eDG+Wt~CH=W`fL&gKqsDV&*W<^EA!-d77pvr9^>s{{&_(3zwx`feTYa zok@4Z51JC$R>lNz4(Jt1#P*;RY{FFWuil=YwF{#V^`1i#RLm#F+jAOge&)Ij0aJ=Unzd zE1QA3|*W!g{33i*! zaPIucr~l#8FTecy{tplL`)NO=&4zPc&KBDYLr(J5SKnOw;;XlQ`SXhxFUBxtnddpz zejEZ;5D|Iy?Ad;|D=}w?ycuGQTt3GrjWTmtSpD3HCXfR%21SJ!SQ^#wk`+q63bili zCgzfh3|O+OUcGt5ue~{_PJ}QliM6#l5pOkck#p*9#*iUBVN|E8=<*|N!q>ph2TY?A zh9Oaa#cpO_T@zZ>VKJH=f?K=A1T#xMoccN`z0wRd!>XCX@Dic; zM#H}3ar0@h688*peCvi_R|H@82w832C==z%?hKYAYC(XNOXEK(wrec(_nw|QoKuT! zB%XUKJx$%?@bEf8p4W`)6SDeed5NbZ2#B-R5d*gjt&uY1t@^HXv8IYFo!51NxDspUjovm?>0s{oM>9 zL?nbJQJ@eEhb6b2&9zltU4|LLvi8tmkLzVDV(u$$(XUYoU1aK8*LYG`Evq2MG2-^4 z#+$l0p%&*-PoGmgV1Uv_kq4>U-69=yF@F;cSN_D|C^^?_k+WzCXP5D0w$NLFA??Iu zmpCc}TU09)&4dtiKLq7RmE*0i>>*1_X1-iI-xG)-R(#zk37uEffrA0>J`* z_=b^Dg3dKmHoDRp`bXH%zg*LO6&T8D)NVtlai^BcYKI}+Ib#ImTh*sBH$C4%^ z)AbOgh8RL^9~rPJlLA3j>W2U!gb;=c7cPA8t6zP7?ej1H_Qh`ZY&WGahM1TFhY(n3 zndWARF$#s0B*rX4Au}h=Dc`(#^ZNDcmo8m8cY4Y>6OKG;6vNM8jR%$+M2*W4QHU{% zU$Nf&wZ*+Qkk& znp8E$v$H)j55xHOFW>sjZ$7&5&3E_j-rw){^T|nI3L%W+7zUd5`N^Xv|NifPzWV;# zr>7UjakJkGbCTs_uQH@Nw`migeZ0b8;yn?6`yQ)nscuDbmxzQiH@=eAF(Mb__d=hQCD@$= z%BIF0mPDkU02}>Nj3h_mhAd+>-8)Mv=YeS1A{l8iyfFrMAe-LIWnZf*Q7Gr|@-j3r zBd2oVgtOrsQ=iltS~(#Y)^`KGfiy}akmEQKfqOPRcmz^DqVVp0L(^38m^bF|7C|Yi zs~sr-03ZNKL_t(g7O5+y^^sZWfW)O81ybve#tOy@x?1x0y*bum&YDXi9mA?|H&ehA zGn{v&@C?+D+ z8`1j5UCbNWcnS)2yzm+r3?|p)!WVv&3+oyIu`m)pf;ZGmmmdO)c&YVwcj_H44ig$7j=4P%$k6OoU$YS^ z-$3X{8jBogkB4g$MZ1nqE^)RN!8})}v?EX~qn)M9VScT~%=$`!vMbFV&vJOwLlN_` zZV~I6d}wKqNc_Hg&#|Uvf%TP#h`62=QQtZh;DMgw0$eYXO;a8)M0iOJhLwVkKbky3 z%032al|;f!12lm^nx?5FaQFLtuA3+5F`HCR;9%y=OfiHy2&9^FVz|l*)>#hRqH@ZS z5=KT9O1mT?VlIPQ+^_a9{>0X+Iiwh{9@db`ZoDRrf%0)PG>iQDLqebsBDJI6@@E`k zfxHmOG7@9vNX)e2+PUJ~$WC?4kn0Yu=rW`$3WbR%%d9u4&E7Js8r{gX@ zpp!0TXImoD?NAhc2b0YyO|zC%mqDpTSmYp%W^V}qtZLV3j2HqFi6pK|)Z6h!p?_?| zi)zP<-AKRy0QoH1&}DGS2{YR*DKR>C2Z)|IEpRXc9^}QPTWx9?gL%qcvuNyA@?!%p zANZ77xgUBT?XJn_gJZW{|J1vjua3Qutc>japWOP1Y2B#W$ih7z>ly_Mn@0WGDKkq5V@~ah1Z5(G>iKn~AdY0@Spw4dW8OV!M*Vxu1XhAeVoxW$IL)tB|=Ae~|U2qz2W z#rF@J6k`nU`u-6?o*Q#~~rfN+h?d z^H2m#4I5HM=e_{9T^JTn{B)xz)s@s9UFJ`(4J?WHG#*-p_*%vTW({>+{p$}6o9ObM zE2{YEDiGtIuTG%Yu7=I!`6NJT7vxw*eb&TwLs|FqMCEw;T>GZ3O1l+}ijW6cU%{R0 zj*8puw!E^M;A1XhuP!pR>D=zM!tezy>dIiAVQx}eDHy;^isn0152{k6slRanAdc41 z1q}Fe&I3#?;v-`zy~)&oA=H)_`lha~;Lp{}vJS?4b&m~p{SGSD(^qN+z}_A7$~pTl zmZ()`8KoP8YWV=)J)jHd9rLI6cWqSFYjU_~U+*J_)87D!<%f0+C3Rbl$Kp?`EJvw;e!D}#gS#f8xhjr0|jlIS31-{{{1OC~B z6khhQK6HX5mo0VTT-wiBN-3jZZUb_z`#cyc3uGQOfEfIg9WB`Pm9%7Ima|f*&P$bV zUdsyQ49lpEZz{E-NN&TO)Ch(PZ@s4|&ni^O12B9dXB%`%*2f~p{1 zx=F2wO&E>IvMg*l!7Pk}ZqbW)E1AurAA$%YZB*nM`SJY`Wi$W|uwEN|4KGxPk;3p` zfZDy##ngO-2gSchp>)2Y)k}xY#*>Y2a2tt`S4D>DQ&G`|vI!Gd5$hHk;9J3(F7S?0 zL&ney-lB$Jxo-|aF|Dgahe^L?f8|xON)17<_QC>k+aaiD{}qrretg?R<#_EpDZ zVbDALY_<==#?6g3uspR__ohJj)!{YHSF6-GR5BElj(%8Fsz(ki2MWg4Mn!rD&4>EI zb`_;Z+z=_CfmGF43pK3S0cw|lQ2Sczm8k`=QNtb@J-}jQgK)hg?OVF?FEz`INHFaR zbk?YCy_Al?h~J1~JVlMGhQNq{N8q_wJ%0;RIeXDpdM6ewj&8@%yPt~F!$iF{#Lz97oZ!y5^yTMX5cdQ?g^uvpm zG|JId${I?dOq4|u{I(;v@*nn4CDUOJb@56wPjw7tFrXGUV-k<&uF?#;{2M;r(Zc%9 z;@_xEppbU_fOKd|`wz|D7S@QC#hZyl!hwGyRBM;HR4iX|mlw&p&z|*q zfq;zoS5;^zQ|WTM{Ux~}rBSjrivD6Exrpd;MW#p+a-y6_gmXS15F64a{2K>3x5CS$ zdQwU$&q8^w-@>Zkg-(nd0_8lXG%+tbLX;2WscxrW#N@Wu$n`=Mt#wu0jwQ;4;~8wx zh^CHEy264?RFS%Nxw+YFHk*x78#oNy%tNa(nRy&XT}3|6^M1cK3}C#XI(kRzs2~`| zxt$2t`+BDI16V9vUpr(N+jjK;-RGsVZi5HmSc-7WtJR8 zrn9sC*|WW}-D-);uBy5LuGt#|;$G=Go6(>`0+p{7as0!~ftf^dp3A~~R12CI>8mDj zz=ohjJ0hHve`X%)DexjvJ}mJ5K2RH0JV1RLM=GkLrBB7oL zn8-NNv=?oqm8Qy?_-HC3l5A%YdF2Qu!=&d5t=p};#_e&zeH1=^Eo`)4#aNkh9>?*O zS6=({?|*;e#*KNmk1@_u-tDGM95@C_LVW1Fhrq)SMC4?{bEbJpDNoL&Rri`v(smoSvR;;&2wi{d+&$zjyET*IysECq#rRI)mGJ(;MZ8 z6}qBz{U1Y;FIhUp(oVZ_CU9bB z$aGYJ);Ge|!d?kh(&fcjtTO!zq5kQ;EVS!(3EoTy%;%|L6W+dhv&==|BX(JPk6G-`q5c{XbCKsAu? zb-OxL4$V;BE62!gqJbgUjSnWb<=gTU^o_Ku5W|(`Xb^iwA>5|05t&jiGrsbob#-V| zZJfvmyT&BfL38=--%_-gNq2V5fseWyXLl{Gn)d?YeJ_#M42ouccxF_z>=qvx{TR-1 z4`m~ZY0K-JTD6xuD7*JoRZ$n}stRODp`|`68xC>5JzWdaWmEn@M-$c!$Xqv|I8t}?D=u-6R zcCiHD_g{csDo7_Z(8Tv1ihee*dXkX$gA9RBH zG5R#P#n`3P&|b`(^Ri@q$rQt~!NM0>B!o~l3G*7OGuEr8ry6ccNkdANCbjJYsG7P5 z`sPBZMRTbDC!AiwE5I_Ks*Qjk5s{J)#W^_>0hr4ym+4+JuYY=o{zWi~&|+o`U@WMu zJIcrib`2O$r|`qMN`(=&1%~ahCq+)3Zm99+h{b$B!T1xpQZ~-)kuMe3EnN=Q@S?(?@VekVP6#ELXAA zG)=qRZa+`sCVu$g`|rR1?zwZ@d7jVC&UVwDm?_9G#1J`9{QlF%?EuUfLYm)L6>+j=6k`ruvZ~to|^*iPgKry;%t~ zOkjwv_-mYZAEB;1j14@xEYXVEJ==(3lj!W$@`_?un4?uxB8;U7X{_!Iq?cLfL=flA zeR8nrYH3a9D9iD+Jl>#~OEpi|%t~(V%2YRMF-WHNEyXnCEya*4F?vI>qtEG!4?^2=s`56Vf0BZ zDq?j_{HfH4liYs$SYa0Q?L%P5tyu`o+#%kKAmm!vn3swQaU?3o9FqZmtOjh|M7pl zdiCn&+zBzKw3K=L`0;MHi^MtS@4x^4?CDdM&H%;ebIlR_qb}!N{AjfDg;fvCJWW$d zDT};#@ug4y!>6zR-Rp_UR^ZFVYB6#kiOekF+TXtX_M2~>J$^z&V;sB*a$RXV&+~4# zE1BFpr!qgL+xI+oCPUR?q%aPPH?>MwSr3KY=WAim^gg1LLvK;l5&L$HqC6P6yytph zsf|Tl{P?zzTVEj8Lx;P$LZdXFE_|*7`lp3R0hs5U+EA&F@O1(y`@t)L#WO5jF+w4? zrKC<23B1xso5Ac}25BkiH*fklw8oPQ%>zkV%t465q$}g(9enWxV!CaR~10HWI zV+d(4uY86RGrEtwGg3Q;Z7gC+A~sZq$X@riVZN&$-y*dj=_+tdC}{&l7ZTn39)aF2 zYcx?bREtS3BKEqnd-_JnT|ori08x9Mf)}g8K54S#4@rNdA+%jrjZ>V zdA<3Ho=*cy!<(tqRioFQ*F#sCo|L(R0(!>0W170%%T&jxcwBtrT%K+3u%4?UI7Fid zxVYv!GJHAiCoj8=c6snhRy`kKRY`TlXLlL0(k6_QrpXPdO*32q{Yz-4z_AJ{klJsA z-6RKN#Wm9y`YM+)fXLzcC10_ce&MeCoA3O*X;4w*Jj+B*&;Jq5k(%anHTM+YyKJco z40R7N#3ZU3b%GKZb)l}L&3kfP)aggdra5k1tZOxcNwfm5AQ|hq7B_cp!-g$ z1~r3HV|@=cErz?+prX)f7-Wpn@K02-XQNzY6U(!+Ga?E+#Bnpk4bGSrny97Pirt+F zHDI8k#wso9`en5*vT7zyuz-h(XTV(DDrf>dzO1CDRk4OhvxS1Jyijp(9E>*^0f2A!$uzAW5xVvJO|Kp#=84O~~ z{jMR5AQ)w&OTLA`uDUfUuL3llR_x@11wv`SHj5^R(aXb}`1y zuq{>%A&N*OjvfOU%dY6rO-^GE)Hgx>5Aj=**+za zWkv_9l*m|INJL6LnutVrvx!;g;wu+_|G)hH_V+h$eDkCf7{@WERDOuaty{N#{PD-1 zUA_`IBv=_r;HXDXhib>0JrVjTiDC<*gr#IoLS=8>F(=BL$8q@Z!}s5P_nkYpzn{-i z%F9OVLTMO7o`jii-TMCD|Mcfy{_?Hu>6Sx?91@jT!R3vvBx-Y}$W+p}(=<)fIL~RG zb4nsh$-&Htp-d(qn#kbA$FO61Ty#mJ8TC_0%i4i8_P80gp2^MJuZqxg&(ynclX z+e3b+b@?^2T|6-N@q>3lyRWM@f#HIBkV8FtW0~f>S89LBZCFVZ3J|k2og|Wuc8cZ|rSkM3{Mq;!_{@>ixA=4!=!k zI^vSZmG`&|3&gs1Y#Fmgq;>rBO7~|RM2mTe3?Pq4x6o;ikTO1{ql+Rg5iN@oi86^$ zB9?Oej*k@tDDoO;nc-6UpfsmUwWf%OWZg-_>%6kD>igaM2rcwwQ%0YrcCAcs8zmuQ z2qZKmMmd2_4f77(H$6*TkEXJz4pSN8iZJIU6d$@LK8P0BraXG;wed}QhkOtbj;3LUhHv&uf>mEL%lGdF|Jq6eZ^**ekY&UPH6>ECaexOV;0`z}4YhhU zzKNRLxM`4hZEf6!ef%G=!E^&s8yTe2$kw2vu~@ zc@|1JO9#fNI-}%P#wJjxeI-M02(>bkrYthal;-_@o~9(miG(W9O7=GI_cei=6}n7h z-?cT0B9qEqNnhyZ+Q#Wn!&tW)!8{a=v%O_=Q2FtFP{5bb)LbQeMSf*-ZfhSuYW?-KB3(;#>SFydAlWR=Wj^cNuPv zx9iP>L9oQ27hZVblTSW*?X^opoYHbKSS@x4`Z31M5ChA0+%WTYI}+t*&z^mC?aS}J z`!46K+rjwhHEKt1$DM~9yv3!lq|UVDx*Au5b=hep1R93$-uqWyef_l%!ZgpzNs)=9 zIDeK$KR$f$;6a||u9dsy@Yj)IrVsG4d%?B5t}iZ|qtEjk2R^-U`oXV1c?||S^E4fH(kPeCkLgH-Vd!=w{GnXb`jIiW z3T~2aCdeXLZa?|g>R1w6#JGVsbq*PCdWnpgj3A~_Bb}MLz{@wJC8>?2*pSv$S6cg2 z&_K5vM)VHmxH0;%Tk*l|*xiexchn+~XSZ7If?Rn+@u|8tsyeibM+k;B$5HAp6nPEg z{-z0NMl&jimgx?`kK;ivv;x`)iQs)y1p$h1{Q^=r(v~)Xa591fxC~c@@^PUIP;uc2 z>e>G=4H+2w){VuxbZYXUf9*~eJVqPFxYQcDTgBvG3}b7O9%ml! zV_na@(RsHauVGque~+;)u)Vxm*C>Qx9j+!Esa>OrhkymQ64P+FCREqlMhdi!xYO-C z5mb~JRG@TzMO(ShyS)=(i?|R{dBc*rBZu}2S~$QtkX;p-oBC#I7*;Rl-j<01ue8)U zJ40TQdbnkX4+K7ixQQF6AXhJMK5;<+w`4@* zWBI~~mNcBJV$~_oD4vY~TQjLSw3u$;s;fJVY1C~CC*_R3?RLA}ZgHPnhItp=!_dRk zn4?@>6HpsWFgUP#FVOM^D(1J3R7Ry*zU=W>c0~vw#4t+WoW!uCs931wCO()ZkPRG_ zX{ra7@g9)heGlrY$Uik|HyA?-#_vM=Z9#6A7hqeR>8Wl@T}A(tM`$oqrMXW4+#?F= z!otgcoC2D&3pi64)pCR~azbtF^L+sR>F<~yM|kk8b|Ipd@8P=UGsDv^7?fbnStw1rMkYh1|>i%ogz zVZe)1pM<(S-*P&(2;(uV?I&u_E`uRS&4@}VU6u#6>?cHY*%X`Dzs@cOvf=jt>d#Ro#9Fq zZU!#Y`kH#NaY-Yl;Y2APdCxxrdjmr0dA(`Tzq|>A6a(olV5r56Z{oQkdxEAglErsm ztt(<*_>J1TO?O*dySlE#@aAwtKU&f|xG|Ygco&LVMPMLq0Po8fmb{|6q9I5$?8r(0 zG(a-4berg6!@vVIh890U*^uJ)J=`OF4ZzgSA+hsK@0Eta8rr+cG1$<{yo7tQN7UFQ zyT(QrhbW_;K=Mg;se4m)?ebH+z-R!G(4|QZEnTTSx~S(7hE;NIC~n=K0UHwWLX|t+ znKP_~1&({lPrSU#BTH+F>LQ{1W&UF}%pP>*)zZCVZhD+(fi+04L5Nzcu0<-ZBCfF= zGa6br^!Du5Kg|#B<#0WdMtr8`Q(2^Gek11%OMy9tI1XVrOn(|N&1D8@zOdpmgt7+z zu@DujsrzV~@tZMCX+YlUx&mYOI>O%{Ijzq0g7_ebV- z#FE>^q~2wev2wpGe#-C}3f0vxh1!0ZpwiSGfcDxSp9>{`;u#rKmZ(nFEFvxBB2GR(Ydyu)ph@bvi2*+IF6&4Ya!vB zq~^W_K5}R-baP|G3g(dWlDF{%R0-;OMnJ97ZCxLA-IGil>kZ}w-pI(gs-1vZqeY;4 z!>d}t%ixXY`7?!8JnkhHm4`K+k@$0;a(f*qZ20tax)qP`X~2P1j$q=50==$&N&#OO z9Sjp~NA#of1IA?K+^X~EU-Ow|NQgYw{P!uyV{Q)SY5MSm(?{3iu+tq{bZV^@@MH_3d5!UNGYA3Uijd>54Wf1 zV;pz$#2mNVEeA>SewubNPxtQJef01lr5tD(&LX$R@!7pRHk63*Z6lh|Qn6PE!+yUf zBF=KQdpZomrAwEtzJK-ND=%fvyXkB{ooS0P@I23)<+Hzje(l;;l=Rm3n)LBKETUVfnjAsAOAGMqL|!v+g(>OsxwXCn>K3FH%)b1WeSJ7 z?!a49U=YK-Z(KY%JU@Lv3>2}XgK@P)v z(As6NXK>3l~DN+1f&&<%LYx^g3dC)7gdz*Gcjj^MM;3;!Tx#R?NIj}aT z<8!U3e`*0u1(v)dF2UC1@-Pf~Y)p5pu`cV@ipED2RK{fbR7J>b^4p~<;fMh{Y8ZR5u5Rrts z*NHMDB19>)pEg&4n~1>X)8Iy&Af+NvZiC|i!CCiYVWMWA5oup1r~?g$Vs)OofW}@a%ZZ{D0hBtOKXuu!JeE0Q9&FVc9IYc@H8?gu2n?_m6B%%IJnS^Kv zL1bZ|>;`*sa&r0d-8q?}XQ$2e|>IPKFAcqg+^9)``0 z8#nIUxpU>pl`?fUO%rnt9I~W9fjMNMESV(-;=IUSHj}ar5IS=enJ6tsHeAs|8 zRNpaS4ozd=ho(e{usrbDj2?WjIMhv43Qkm`>H^Y1H{qmJpU%LOpy(#bJGz>C| zFa?>@yx9!XG~d7f<2PU5xc|eG3ol-bVN7W%-Oms>&HFqj&S{KvGKTFK$00E1d7eaM z3N5CHC5y{)ib&b(iinCCiz1mr@kJ3T>)G|s5IDx=Sl*mT z-#(b9oaZ`olXG5%P^C9rOw3f;5D}I`hoX2;GZiaKO`~vKb!%M1$ZO)T)Rc!3M|02f ztb;J@^3iHYHs@yNJHrE-+n`R}M9XyPU|mC&+|d~Z-iYwrZosOphc5soh?!u8Rn&;a zD@_%=AK*(duf_nM8sTfX4xRM}%4L*{8ZHz}kJ**T=-G&)bq&w@G}mw66rXAiv_k29 zkj@cR8!MYCXhFRU4$<`5d8zvcNam_~xekjlK4_x~6;yA|8TW?MN6JqZ^M>;Pdo>!U zsIb(=uC6M)jSy^V9pjAHFNoMdFF995V;K;tgHU@J5K?<;5JHISj2EVF5b!=k=pzM~ ziEM^Rf+2BvY1E}dD2QoGD+ZV!MO|qz1c`zHpJJ6t5ZPm9Tu@BNK)qQQILKaDiZK$Q z;g(LP=f#EL{v@1P3M4hoBM$_QQ`j!|3jdX6)2LqR8j3oMdjwEHWv+z?0_;`5aABjM zUaf;JNv)e8!B#HfHc()rpfUTV(`66iL)igLO%tjHnOQQ6P%buF23AX%oHLb$v_f^O zTnZG^oXX+@qCzRx?y1lS>T1v9(vrO zQd$OHB!~0bl@QiKwheItXlZ!ClBLD$*6w&EDF4)E6_g>V3}d8X-4R=|WZ~)(%-~Hi zFmv6x`fz$d8zEWK49X$=IE(r4Qij&pD?uND(y5F&XQ9M_7x$s4ek4LA9U4L`<9rcHNphyrGvu6O9Ot&RjPj#;Af;77-o5>xQ$f#TUTi!DR}J@(NI@Xzmq> zXmYmPkHj*kAQWjCT$KXl9Gb;nC=oBQi+F*KD}=Ixiy{E$f58%a@_NNRfpCiO6o6 zcGH}N6Xi`@G#^B=P)=DDUOw1!cWd~N>ka- zI%n2IzY&?JL>C>FbUt{3lH_5gou&p29C2ZA))8H?V~r1`Rw!T)p#c$$9foenVV>GN zrqo@9kG_K;kx~Q$95xzYzQVACqk#kQmOWKB7QXeC6aSy_^w3X1DUzRzReI`d+pM%fBow( zzWCzVvnQFDNT!tIKG7ydVhI8h$}$8Bk;lk;l6iObh=2U@%P+26xpLvcTg=Qs%D6d% zkS#nLhSx458crCPOJHnTx?2&+c@D&Bnu&(<=g+_L$}8V|v!yKiG!Jnzj6{-CN^{Cj z9zVWy>((1@{$f10(GCXtJpZcZSv9;FXTe~BT?-aARghE=B0d=IxfARUXufF{H(&bCzHpDolG?huS{eGXO{SY|@8pbe= zky#|AA%x9lGtE>!{V-W{Zka2Z3GGhyhHYxy0mV1LL4j?es@EjmVP>N8pLMHlOhZJi z!LMP%O+}WM$@XS5UT5Z|pKje5`O&s9(V&XL0iscnVYRC(6v4=7?$qCn_PRRJx=2CM zsAD5kAXf3ZWnNyoyFy7DrSVTyECQxGq^9;uL$a_ThB-N71>=tzf3&=}0<9c1U=(g( zPpzAHnnsOS*mnV_F@p^+U6&0*D#KZ>hPSoy%9T;RwxVUYjhWtU)!bdRAi*_X#wy~* z0EUi_RLa8|K^^glN|dP4yjK;0Ua$N|p~G5YMXE%vLx!*vKzE8gmLjq+EX=(^hQd?e~rj!Q!wi)^FB1O z>k&+7Iucv7jPU|f(V&ZZhQq)@#MIMaXoqeligp|7UflM!)S?R$g32l~qhk8^P>(u< zYx9;$tZd|Lc_?QIc{+x;iEmg%U<{Cdf(6a}uF-K;hVgRvJVM8S4Uw#lk5fUpUHPTe z5ezs1V`f+hz;+}w8GcIPVHgCy-|A$3p+yf8uW+LQ&zo|>ZItGAAc{W_rR6eP%^FPV zg{DNV&}9D~X>b2+M{T5yR!QCa9NT~aLkt9BgMkp3nauqEzu&dyUH5%&GR%;KkT?Vk zHpckd=j`1r)%`)O>PJ=TeSFvLWe&Z&rBc;XANp38G>d?=`b!xp8+M~s*<52zb_-23 zuBvi9fTeUjbCX&gJXiQc#i7Omu|30>1(!OO;q2NPjOA-)#p(>OM1-5?R4x&Tu~`bB z%F=$2uF_b(RnzNQGEDC35Tvw6h~QtU?Hqv`zZAFI?FxG_#+=syO|+{3iy{0Dr&8iL zj?@}6@=vYvmYWbl43XOSkcDw{9`yj0L215-bAI|HwI95!(JV;Gkv0q2~AMKUkb|LgI=^Qt5DHxa?ASWPt4%S%*E zt_84O()zw~ZnQvcSipzIiKuazH5tRQYY)Cvx-n(PqU8@?<+6^et~u%tpoLrx4c|9_OQSqQFBrwO0StkD~QEKOSKZ_?97AI!tp;6y(*buYK^r2dAf}Bw;!v z=J?iIzxvH@e)InO@4x=~>+?MS@WT(!pFh|6B_BWy>JacH=PYVjR!U z&&x>{nS`13%$m!WFTeKMYo}K(pPZcRb~^X`%*F`kZ`EPAOniFpe?NphJ3q`>;m+jBYFt1G@52}a#=k6+S6th$n#C&tW7aV9L7 z^ya=x#1v#iFr>7W?UKbOo`0`NB`-U|C|2flsLT|=@eY)4y+LrL34GQ=^x$%tNme3+PePT^B}etgF6g(^@| zTLZ7;ATnxYI4%PpqDgmIEmvt0ay{s{^>$V^E$wKi>Sj_(%A}#Qx#S*vXc~n#B;9ap9RWagMxhw+%$mIU`EP z+r4_)_6XF4m)am%f0o@{Yn8_>eB3Jz(Z|emnt+)$Iuyo`e2Lr+^ymPbpCHVz z6WIqM{n=Ro*1Hm1mpD3=o2$kRMI4p z%&SwE{q-6xl`*|9zf0$%?0PPsGf@e$l5?8p@`Tp$Q+s(BRkK~KaeL{*440}BF=yr& z%kj8P98+OY-s9LJhKMubW6sH?ww=RuX9?DBUDL&Ip+XJAu!}({k|Mx8Dorb1U>@^OJ2TUwm(e!!y%;$^e3<-l(Gont|GfDXDYV<$N%4-=j0_8KOLJ8->vvw^&hjrN$2YOKm@}>}m3) zC_v_|NeMP&i7)qUMsdBtE&jb}8Yn%!hH{q(dcHt1&lQ@NDTrEKm_n&_Y_tZ0ry+>e zvt@`|Wtq5avKX!uiV}sdUAy-A=bzuZ_v4%SkrQ|x1mfkPf;OkPEk2cO>#~T=fXW5qY;RW)30h*OpFBcDHVQ_@Dpz+VdyR znTC_o%WuDR{nb}apTBsvKTKz5kM7>R`|#nz*WbJvLMXexF(?=<_?wr!jG+Q78>0iV z#4KvHHciv%>FJFRKm7F5Pk+4g)AMI9!cdsuvS~#`;xHTzhugPr-@bkO?YG}Jy?mNQ z!Vtp{&(F`#&(Db{rD-nP-UIQ#mtT1$(U_mS7{_rOBSr?xSiMnvjD@4HKF%D6h#=sY zQcU6uyI(yitpEgi(S?-l`M17c%95r9-}EJz85-U!WaAZvBZei4T#skT?MY0^n=v_H zpfk|GwOj7{%+y*=H#{2MH4(cqNiK$jy0=`s$D5OPX^7#8!Ef7%rKb3Y|ryiDY|CkHEv2Ys3KXa5d*REI>H+itvd$VwHz;w zwAD~Iiq2;uv2OY;>!oT-zC^Dnc67{7y->RPx6v9iGps>I2D-8+GYd+v zPoIuDLggQ1bOliZxLsym#at^6anY$50JXu^7>|hqXbcu5X3jvxrSv$SeuKOh2Z3Oswq)SgO#+~q0DpOUC&rfeZ{6SL#)P$3Tbx(pi`hY0ge+XJeMN$40@GO zVa%}0MB89`g0nA9qZYQZjLk@0d1}5mqRyg94u5R*kBX~y&! z=BqUO>Kd;3t#qVU9e4Rr5xm#ry4yg%ZfB1BKqO0+=5v1Px2P&)h3aHxZfi{=l~!Yr zH*p&cjgV_zC%*}oDuZ!r6l}}u5J4g;l_tI1g_cbl)?9x3jOJdg!MEuRKi+dpi7J{2 zH;6;muuKsqjfchfw&s;ytyvzXl;$~)%%b4k~x-hf4Ev=XZM^(Z;cu*P1gpr zuPZP`!^}jHC=pSJbo1uT>({UU^yAMWd^n`D^I7Qm)2B~fym&s(ho?`UKYaM`gZcgO z*(YdCCSe66dOBkD7r>~8v95vq+#wW2JNMTQVU2yeah*1!G7zyJ4tfA##uS!T+d z%Mlkbt}}Ia?%etMFW-Fp@kj4nerFhlqVpH$XD{~WnKCn_G%0W6I1WP+kvN8N9GRI& zIF#1y63-EbEw}2%P1LXc#bAv1&I@IcI`6o;GBZ?fH1@V*Z9!8_DBg6DB zBlVGFMrv}(B`^jPm-JE`OakP@VB(9=1Xy({9b{}vDDOUoD}iRKSACZnLC`SixA5;0 zL<&aH#(hxHCeDFiaW?H>DdQs5p{}55i-!^kQ2?G}jAqP>TGeO~xCBov z9Gz64O^f!7u8n~Rz0W_#3k`r;vv14*0wX!6(0JtqEN?RGrUy<+Rk)}%EGJZo)RjiG zu8fi5spUtZro&<0R#k8#X-O6~PV-}!A6?p|frnuT9M-aE-6pT4NLx=CwW7*`TZZWk zL$6$q4|g$S)i|xo>Y8uiYBK#C1+x@LlwpywRU(uTIdp$oO)J+(Whbzy*C;K2%fYe; zdw?%tgt`n?N8ycIA-oLVMlv=(x8Ky#8yxROZlxExWjT|*)&;Vj)qu|%86gf}dygrS z1j#wg5(5vzAS`&cugG%LfZ~jf?@!h;R#DCYSM8S-qdFd~11MCuV&*7iY=@a77n-@8 zkIc2=TR%<4b?$*8)m;QdF)5|8OL+ZUVL8ISxC}`-XPTKc2BNa`zU+uil9#ixW9J)s z)fiG+)ihT(1R1sBLru@FwONi}CRi=*#dPh~&9h=zV>=^e;GTQ{fEL+*zl+9eX z%8aaY+bS}_$jeC7L>5EbFl_iVrEF#2x(ut-a}OWuJrNC#W|Ci7-7+A5ug>4qECw2* zL{q4aeGJ1OlBQ`Icf;Fnzy0Z_pFVhSf1c(*p&TNeNJin}<~ircJdE)q4~KCc#yC!# zS>`mCMYuO^+<4=SH$n)dOrvP;4_&D}=P!d&>9Le$N+E<0hH`euTW?>xdGqF9zWVFg z`QgVO?#0NjU%f0e6*2%4)A_{AWiCjBVvLD|h#o$C{MWz!^~WFo_4>QNx^(5F2>as2i*kfQ zQF@}p%%yc)VtNQ63^8+-ymq`rq;TX#-WnUF)*8LdTVZQPQv}w~zBct?3cd1(=dLNa zLHoE;jB{!0l&D!p?PF#`fl5;>7jxBv=M9sF8lL#pEyz4q8IiK7gfPt!*_DX4K3(Lo zY;;vzLF2+;cBiZw-nRJRcB-7)CBSF{3?FkdagTcOB*4d=RHCl?AE|kL>_Yt#;CbQY z^>rZyfeRa>xjpvIpk!=S(@T4H7?xrtobeSh?}-!GwG5Ya001BWNkl>0Rdc!47Z={QtR?&gZOkAK=83REKAyk2luL^Zj``# zA4`u|IEuU#vZ$xQtzby4$}S9p)U#e{haS`;xLy}Ft&g)>uIv`wV<|_XoL(knZqJ`J z?nbR$>GXhc6LwWOY8-=uwi3P6p3ac~#-U`}2<`4vGTsX!x87j_$(2c8TEf-!Q zLL{L5PM2*hYr~jPI{(~^Sb_MqdO9BKx|iZ(05`6U?3u(SL>PzFl$a5pbY=JYQkD#p zrm~Z|3YDDKAdxP8EWO(l!IUP!7C|*^9O*MM>Y$8YhAB{eU^p##*I`yDd|7}ZLC-2J zV`n-Ofa{Q?4pVwv5FQY3%(#_t;f?KuDY>XA7x!HCg}PHS)ApVYr*=&URgWIsT>yt! zr2R$$Y6M5{y`i$J%XGOzECatrM?C(qXh(6Tx3vdK@VNcY&j}pUDkjtb0d`W z+6m=(?eF)SADBg;g~qi>za`_U5pKTgCDQhkiS*NCISa>ifhlY6Ohn)dW>VL42z*|$ zxBYnY!U;3)vucO+2^$a;Pp>bdWh9F?n74dZu1vG(afl`{mS{f)-Q{Do5~56$L{KVB zNi}K!5bL?Oh7Tb z98XTilauk}bhkShb|>TE{Ka3t`OBRjzMG~g#>gCNo2{dQ;~W}|U;^*D=4LsMDX$KE zdims&Pi|ekdNt?#=+V==cYk{N^!aYLyK?19jPd;Z{LY;_4<0NHK|EZs7##I=1u?OzXmH;nox=t17gjA3~6 zd>q??&ZOTWMMUL5Tb~y(H*c{Y+YKqE=KC#CNpJ&)obc6vVxD{im)u54%T`PxS&4NJdP}+t*&SR4|l2#Tk2G7-Nb4qrg4H=E*CD}Lvc_q-n@;? zqiAS&hGEaVu?oYa#tLzrNqtVv9}T3g&>+J4k0>xl<`}}j9QCIx$AWbz7@U16=3W88 zhakejLp@M+VL<=k^SYeSx+k|EhcwREZW$gzbM<=c8Z+0k0s>s< zjO-47DW&LgVeFDRP%iMDJ@hi= z!LQ|9%gR|GeYtQtu%tIab<8!h(}4Eagze)L@7xY$|SSoIWPW`WhTmF z47(xj#yG|>1|DOW=fj6c%Axqtuue!r(0fJ_s1OPnoWsb}y3-OV+bL@vE`kPjk=iD!CN|#Hb8GF&M#9U7nck;24Q0=7!dGA>nhV_H8I2sV#+0F z6+RSr>&2R0C*d=!Y&EAN86LJmfnkH{w@|xw+qLQ~p|DWX*6}IDH6ro3iOFiKCeTrP z8OFS`t3t^s&X4z@BFLF^hShDzs&e9$f>ogzrt#^!M(!`8R&Sr(yj^Ql(NKggovrND zc7D9!nR`PVVKj<1c@ZL?jG%Gsb9Heum$c((hDo28z$%Q%VR{70W2pDo$~Q$boc?A2 z_tB|~VQ~BYloO0{U6GBEBdH_HSfmE+0IQ6tXRC%EkCH*si_lF;&K|fIOt$R7(`V;v z1~e?lL_+FWQF(&>G370|iNG#b21eborv}O3DJ2<>L8ujBAHK_^w8yVw7PJ#nbs97b z+8$fgk(boa@$7XGCTOksEL~&35)E_+sY%mq6PEO*uc5D~cZGHt8r~a!F&ujSyTWGJ zFKiO~Cx+*4aa}2l)~F3i0x>ghocV5ucz!(2dRk@I3_PpiLNvE^(-tg8hI6kY z)FZbw6t`m|Xg`LIUS^;crSN#7PD2Q$fvqXEJ7S8 z0cHS48L75Q4gGF<0z2H&ySFIgM@FZ)y-FJG*Qln-r_WSOZIDY_SK{boDMFD{_HgJ{ z87O1l`UNmkC#sCx);q{HS}E3{DyBC=s^brsUX_A?qsqz|)+$SAk4QG~DoRB_Za*c0 zZ382t3MlncexY}uHc@A-D*`1PQcCCh{fqPS>5zoNFzh%CHRXWEH54ZGlrY2=LEHMN zg<>#4uBg4XLNWIOpt-oW55^=$0}i#ru5(n#o*~{@GJuGCpEQV|RH$Gj*v&%Vs|boF z!)QfKRLVLcjsTe=Pz2-w#q1_lYgpNX@4|Bo>sRAbrYw}De6FcXqDF3NUp3I|22vHP z3;Vz(eK)EyOx+-)8W&U|V~EBgJp;z_kTF9I6N^YnQ#trpxjH?$eCyV&S6_MUFel+i z6jG89hMYur;c!`myBo$aj=LC#z(L{=o<4p0&)fg_<(FTat1RogX~R#JMDDwj=BIB% zqmdYsI2;b;9Nrk?n{U4H&O7gf5K>CdpP!w*INzU7Ip>swS)M+7eEVe=2M&RT zVJJHK?z``9-@bh~9LgbIDW#m}Vs7Iu4nrtTTw+k!7qi>##5FD%nj_e(R0HO=>g`4k zw0|U0G^nJgwb;;hck#v>b`gQ0kw-YP7B~!%0%z*@rXlV_9PS%3a)o;4Ali)aSv43D zVJ=^sTUw8roP%gJ>H+Lf1nau*YJEdkevgQqNSuhE{+9L=U6qUm*5+9ifQpXY(;X?@*nrd}>|5BTvq8q+RP>E*xAy_UZ>|1skYF z<46a4xdPtg0)uUr@A6Q{n2laI1)CU=?To}4$dZEZ#s7|GDdz@Ms$vPYk#pKY%R1rAp&22?c zE|j{A@T#N)t)ozv+#37z21XD(OUBm^D|GYD@bVaGwHO0WxUkvkT z>Ur&_P+8fE*q)#74~H28a*VNrmTHG_n$mtc%sDMqDw$&-=G2bimQJRER_KSiK)4Lw zp+~FL#TaYEu(ipNn$iC>)`E=_L$ENQPO3oV_NZlF%3wkmw}V6%-ZR8p!+N<1u6@id zd=rdM;d7J5J(2RCxF(3dRJyhl_6dx2dNz#Urcu&uTX$JPI=s=E-^#TmApZ4%Y)h5Y zv&bz@JU(4c%489`?&ZsufBoJEAAE4*$&)8(o@U8`_%KhGPft>k$WcU6N+HZdJj5`@ zFvcJ`=Ojt;ojc$CboYnr*RSt(rAN&(NkPyKpHy8%lLV@otmnRDh)Li0=^Ub%AR z#*G{Q^FM!n^60ThIzK<`_tP*g(|{?ZAAb1Z$&)8#s{osJbGPN z7`lJV=>8q()+(Vl2-W2Wh}*Fe^>p-(R=>7(ByOAv?N*_Apg35He~3~TmJE#e zs++6emffe%wCJuzl!(z0O6>=QJ-5^|Y$Lw8F;^)9LtJTw>esr>UxR>_UgnClr%j+= zU!v#7*rCU&Yx9OqXcu61vFbA8X?C+C^Jr8kX*bSL>lDUlF&Bmo!zX%*)DZ_GIP%~T z?#3dIvj+qzPzxq%(-Sv5698@s!cGWMMt|Jm4I{ahXynqL85VG8(CAwD5qtA*yNNL< zx##wKo&@6#nB4~rZ$H!nzk8uZs8yUX@-A77W+sb|4CJiK(DBZ)mC_p+dYpoWa+S;b zkzv>ut(fD60?%7zY^A(KvkN$qHTU(e1EJj1Xw#)>z%gw5k#Uj!?t2_ZLwm#4h;dc_ z#E<(cpEzK_b#4-M&Fz>hj*J_TO(1He6QzcZh6lbbY0$F_?7fzMCELJ@%u{=H)q!#; zeHF?W9@@EyQ5eCxcIe}q{=|Vxy%w?{7k;+hV=Hy!y$lqN0Y_dS-5c4#Ufqk6@}tV&i8@Jw^mmoa;j0Ew3?w=-DESd7hQrIcFv zDi_yPz06!RHBFPY3Unw5T`#3XoXv!#MgxI%d-+I@I=<9p1YDN^8Ch0p1l$ra?A-ED zsLGTY8hPmo>5!<2Z_G@D4!D4>YTD1K0F{X{2csG_09}5I`%2MwutfD%o$W&-*CF*H z7fuHjnmA|RFSHhRtq2nwx?YLVTW!No7S{zlyNMiT(Gm|b@SFoSFT(kqIt?`&S{pMX_lNxvMjqE zI7>Opc^E^890w-KckkZ)+u#27`0?Wq!VsD-OH>~XEWI;gT9l0Hw; z`RLZoS6{tSDu(m(!}I57(;>w$o}8R8%l-TJA3gY`gz2*E5hs_t?$B(u)aoB@U0nk( zCRf%4m(yg5;)Zej^#|{N{x4r#y?RwdrgWehyJL)Fj4{S%&z^nr%{Tx2=Rf!7=V_kP zH0L>S79tsU@#N&BhVooa4Ial)kI(l?=^~G>lwlLlkulTO>;i%y1XviQxp+C73I8=2 zjct@AttdjbP#?Zb0&F099Z@eXb>+h1G$;`)Ve>OgzBm&WFu|7kOcu>$C@2x-r;COG za0^v@Yj%rcrE9W>(0pmsgW$nO3QLTFd1h1Vt5H$ACPho{ju#tORrZYWV45y^_%^r| zphireF}Srz#|uScz_ztv#{cwk8TQO2uFL0TFGRR^k+V#E*zE&j0?(K)+TY~fvO5Ea z_a?O;BeG^R40p}ggWN6j!pn=SrsYdV&OK=O&@W`@5~QA2cGcQt*sDI*B|=^(au(tl zFr_1XgO8y}y`f&igDKj(SQi!~g2~Y9d3&_lCgF$bOvtbludWPO>4F1%*%n?!s5OzL@ z&!xQ#k>fv2(Z+N!neh<=QJ0!eX)c7zs2hG}bAn-m1|^}yL5Q2JkjIV3712k#lx~`o zHCHCeYBJqY&^^oATSFg*z0esAfMp%)vYNG5*K`3iWvOczYr)Ym^(k!KAsuhwMw|LJ zk)&RKU8(u@o4-U9d0Q8l;-Fixtku;OyE)HgqnJj37|S`Ci}L)OmbSFJj!&(H7%7aQ zW)fkGTY$ar+zt|%g7l&8AS_wRdKv-)z_CANjA4ZY)zz2XsAU{nv*&t2ssf`u#3R%# zAOs6?wG`7XEe3{Xr6PzqL?Xc66dy~y9yiT6hRjr~w|JZ~z|3V`mx`%> zaoCR9dXwwAZzbMy9)!wLbL2>=P*sv`T6`qHBk;LlyB}QY6cXTO8?ZCzuhhZ1Opa@ zsg1}U&u+Q;#B-=NCT*=pb#<^OVoE`m(d^u-`?^e`)LR_V#%0v1+0|=2R$a2*!0>Ho zcg?#fVcRo&H1-;QM8S=Ngx+p_h80rY3@|&_Mg=C*hh~KfQZsr%NJ|1$bm*;-p`+R~ zvY1@R4+`SLtXG*22!;Wl(md~$ZXOzf40VCFSdNUh9w|r>GteDj2D5A-Lp|g@S!1!s z4VWNIVR42TU%FvH?WCK<(3Fij7jZB^m3zOetgA|WuKiUMlbi}XEZqp)WfgPL>TwnU zOmoCp@Cn0&E*dA2A^;zGMmk0Xm>B6G7iM6k%dH`Wq0)=#d85{fusEY8H5kK&wIYbR zW^Tr4#0Xw~cqX^3t7HIFS)VM-R05u11H0i$-M>&2^2bgV!x0(Gj30R;x1*7Jht8EW1$mqd$aVKpN{LEhpwsm` zqSXny5g^o~;iwK>8N)VKUDD$OTT@nh8T@tdG{9L03L2t8t-*Jhms%g0LXj~v8?JA; zJq)dsyL_h$_$&jc)y;cnv7(lZY2O68faER4*6RN>oXCgC?S!=N@CW1_m92wQY1<(H{sP!o)r> zp&*z=po&7-jY827&ChNS^p_TdGy^S8$wU(B9n+jsWFn#@d58lsmvelszw_R$Tet4t z{qgy;DJ6-S=9G845_*OZRzSuO$1x6b$Wsb|IP=}Rcfa}On|FWx!L@7Gnr+L%LL^Kf z1UMVHsqB?!BLYH4J%bUE5^yxoq?CeCh-DCVe*N{=-g)<}+kYJo(>~|C-%n>}`*Dop z$UF@5H2?6!-Di)VTz%!$414it+cl`QaIXTB(x|xdZxH}-h?=cQBqA|#&NFkE=Xo4+ z2*dmD|N8UKKmYC@Km4@6pJZm1FmTQiC5lL-`26|vFTecq^DjQTbh0~p{#;U`fkIg7 zW2$ppg@Ll9nuxfbQBamk;~be+ijmzLt+;(UR#@(|irB4qCFd`2WvGY=)n{1c_Vgd) z-N+tFaU}3U0?U=>vlOI9m!QN zgJog+%%EhHyA>FvPDg1&M^7)(1YWH^+8g7+8o^s^H(VHZKwVI6_jlUXuKQ$kp-{L; z*jx-3IrC6KJoCn2E-a!8g=d=pd7}b9gJQ%yA~;eu6DPOiY*t>F z^gfqr!7!$(K_l@}8!Z@k4L+V>0>ePVHvD^nczHMXzJLsC0-tP4L54BUOxJ%6KPq6? zRRxaU^)~E@qg5bW#y-6bm|R`ZjHO90ws&Fds>xd~!=M0W*ZC5I;Sq%z@eLb_hOufN zi)hq(aaky(a~i%^;)WXV3o6oP&r%`sjUXrnu@m&6J5(w zO{rxWPLQ&c?#fbZX)3R+hi&Gp-E5nAokyv0=c*zvCYGpfTXq&2Ho5@;pKL`}pB|R(+7LmlUbcwn6O?GRKqlIl zRtX_6Et8JJFvKwAG-;aL9z@G+1*i9_J3iVHv-)bkHfd7Ia!%`F9Z+skNOhsB5~)ma zS#=l!$JlJxhpX`sZ|$hcTvw@=Cc1w1N|6y*Q>-Was$rXPaH@@e>aAccMVoOq2~5NV zwSEG)PzvQ!xzdbgWUwSIYl?AsEHeu&&L$m{$W0#4xFz>KZz3{d8Yk8-F%RGf~37cfQRs!P=TgEvB(S`PmD2Mx^(HITc3RS<(E&MKHkrXI9)o~rId1r00lGi zFbq4{&GS4R(lCUS(%JJD-+c4UZ-4j2wQJYR8bfmKv8CB9!Mn&;JlCHg=<$D40@lgt z@X<#%|LcGM@$Br3ndf;v?57YYr4+{@#`y5z!^e*wzx(dHxEjv$V@f}66Fx+ zX+N{jPj~Ns_0?A*`QE*I^L&VLJQ>Gv9Ahkptc#>fIg>2f577J)Fg3&TmJ4+2>2l#j zw1JD;X+Uyr!raWA8cy&huIpJC=yB+0iLoZ2-grUF8?=}qqLMRmgaRvC1jk@VJ=YM| zU`1y~eQD#SYq!V4uje26#AXLFX@ZZrkk2kYTM^qY>7ors!+TAT>*=?;tqemYDpy%T z;#=$cj;Kbf_AUsfA$wuH)5R^cpQ$}^senFAc~k|e+Ai}-P}6TM3*D-DFBB8An2OA6 z$G8B2!=gHnRDjbe2paUp4)raz)w*7T3@C9lGv4@GF5VpX8hxAF{g{Xe9^tBa6%8~w z!vKOtt-YDmK;$&&tz}EmMwP~KkVEqd=WnneGr(#D_wI>!lGB}6ndkNhfWCC^8a_Kx zR{S*=qLFjPGXcAkYF%F-FffC#u|SVr_tk~s`vj=pErT)q8*B7`z3UlFeAvXFBEoEq zO4=%HOTZ(Tv_GwJdJppacf)eGbQ~H$EAq?)`1%T_@A7X-K_%7dVkmpL$Q8n@mzMTDIf_~P)%LZrnO^-E6aMtrWzyF?`C*#OnGgtM%FjldIKa@xVCG>M(mW%2 zI>m(poh4Tp3oB}G{0s0jt71i@jNVpk8@JVEvxS}kw(I*+>%IWT9#Z=eJpH6FK`p6; z8Xr+XP!49tx`>sT$8kJ8Jtd;D@|4?nj;5}Ir}|v7ttObLY*$@b z0z*}aR*lrR-u}65!|0k;f6%aT&u(laqAW5^(>%{bY5HNbyqq($lsM1xe!s6n%xXE5 zbLKWY8g9*~kwC{B2lTz40`vI136dxvW;eEKo~!mlJ$FS^s&*F+($RtjjW|>fm@t?U zEem(ilnqa8B5BL8k*Y~a7Elkp5gW-;YgOI*E%@-&R$`yvIDK|)9Zj<(*Kv8bI5+nK zjfNw+kY^v8ye!8B(L&)h_EktE<-7zbTJ;s*efQlPH*WlL|EKe_XE_v0Rf8_RSDY=5 zH12l0;V{iv_%I)){SQC<@Xa^h{QB3we&w|*tD`QIm$<*q9ko0gXY-igPhX*GT~(^j zGAb=H#W>vh_@nFB-+uAp1<#Z-r77o8rg`iz~0aT5_S6b!`T< zd7(!f2_IfAYCByDOYNvqk+i0^}D|NtEiHius2k5*)6-I;w1^29OH3don{U`y}wI-R) z|0883+s$R3Y?-k!NFUkADWV%Fw4tWWT)nCmY|~T8@AV@1vY2s#PgxqKY+M9h6R zyekhL1c8>BCupSbX}01s+L(WJB&4HLNLxE&-g0BwcE~3(U;r`JzJ=1(Wiagenm0~N zk5Zs@8K}2-#VGo5g)%W64r;jEW)8c^?)}b?FdnAQqofCQlcJ*UKa)swb+s| z!)_`}X_$W*#JuTBU2)TkTAN{N%fY**ZrD@KO_hYQ`+|(5M?^EFbX^!3HW|HK&CSs^ ziOR*y(yTvCWv3|O)(sCy6n3#!BaJ%`}a9b$M z^PKBPdyH8k?A*4=%}PT$dZC+?rUkA5B~U$qVPo{mby(6cUo)WBsWow7$$7?Uv1Z5X!amB83RFj|0d05IOWvD8u5a1BS1Pb(?vJo8l8f z;2<%^u&fSVzpdh<0S=idpRlM|bSa|L;a6_Qly!x#F$1qkY0pP8L%JxDwuf`O$p8vn zFMMdyP-C~k%$bRUYMewCM=K+h(=@60(pEPrbs2mJZnvN6E4U@$D*j@Y1CP6S*h8Z+ z;40}QnSq5}*bycvomggJN|IGUU8}CN%BtHWB6%StB2r>;O8GFSDa{;q!?4pM1s1Y( ztbdxO!{LxpQt3@l<+dVLr7L4OXGvL7M&HzPCmi#+JXR!8(N@6rLhT?;3>2!k;%;RX z^V|mNOd=#Cbpe?ywchaT*|Xhlx7+PP2(j1X>MBp4lM6RpCqE)5%2pn9A<)%}Hqf}T zO8Q|K%yg5!g~a{uW~`(xYOMOGl-sRO7vz8GHnF+X!xdh{5FcI$J9SjhB-?s^oNZoq zbFVW?<#`C<>eZ{aKKkVAum60$e@=1OPxCPD>WTG1({i9eco^euHyrkJ2w|GeA3uJ0 z`)_~!{PWLWz52>ZcCkY$lQt#dV+8lSk#q7Rav@MTXW~pWYTg**_3Ll{>YeNN?%m(N z*r$}HG@s-l&7l-C9rpL{-+%o0@s-zJE$2L#VI6LF^c&@Ng5b6;wv?iAJZGtZEmv); zaR@Zea|pvQ#B%8E<;$18_~Lhe{No=VJ$jTg*Wf5Mqo`axRgbB!n132t$aLz3P9)0=u!LtESeSJ3Vkbyx;4zVyj`cS(# zu&IIz@P>(zPBS?~+~v>%i!c%>k$P=eFL=qDQqm1RWnzX}D@EzFER^MVm_me=p!akn z&TUPNW^D4JF>9lcVFHZnrxmu8h@2mx@5S-=)+Ud3o{tA@gR{@>1CB{4(2*!lOc!+P z>c*L^OX(Y5Hl^Ull##xVMcyUxo}gPIE_ixdLX8}8C2}}xp9hI(O^3r$$FJay{_vs9 zvk~0B93!Cq%sV~?if#erbsO(QkGI zx3b%Cs{^=*o)%Ox)`Ml&;|&EA5yJ|_;sS68p;9Iy%0i)PT#rcJZZs+|Tx%8;A?GZ@ znPf`KAq;U?zO%=*?OFs2bhP;yTrUlqnjgFs(od+cn3Ds)U+zA%=KHBS_oRT z7V2$}+Kbz&D%1~A5`3jikH--a{ZcwgAtK>VfBK*Q`maAcdirQM8LHZ1&XhC7IErve z5{QQoNs{E*E5;Zl*lVbBOSEWDPUb54?zNaIG!*?$zh&oPJEb)4sxDT(I!g{lE)#AyI9bh=lQ$u zzWd>a?+=GVInSGjgc37Tp38VX8jNp?ThV0wN8rpRV6CQ06glKMmG2l5(Uj(^SKs*P zlaEg>orZD9%bI=~!k82BJP%Ai-@o_x*<)df!!Re|FckYK%c4ZAh=l zrDjuqW|$OR+)Z6F=bX}%^E~e2jhpY?{P6wVZaf_3!;~_IOhl2wjxN1&Dm2HT94;fn z5NcO&J+eowio(Dpi*!m^LhQ0&(#*{Y)<2t0$Hx zETn}h10{T!7F*G#VF0aBISe5b|I>(v+AJE>imGB2)%-ZpoToWY94W@JJP%=5A}|=g z!JMcB^1r|j-$;T43(-i<8*3)1U-Cgy?MJmVs%=UiK_-uYJ(3!#}B*r3Mon_ zP-$vS5We#NlBL1jeIkaHcjMl?)H#Ydm;M%AM~30z(^rACM$1!)@Iq^%{j z2qZ$i<{j652Je3cnT~|dg=~1tHGvr}L}SkN z@|Y`euWPkEYA>;4W3;NJqU_6wc*@6G(zbsX{!d~{+LTz zmp9IiDp0i1!Ku|aK~IvLJsZJ|j0Obt5u@c?=cJ89Mg_)Zx*9>|O+}CW> zd0~y;CJ7`#GLwWz9Ass_D6qJT)b?0r&diBuUHv6Q#7ZBA70SC}Eks0_D2bFW)ECOc zxPLi_un=c4QfC~YFe)IoI?`xC)nt}3cw<9zMF$kvxvy$9{78Fbr0<_%UQ@-`8aYc4 z(tn(VQl^y4b2P0WRGtrPzdM9B%x!!w&x@`(ixyW3;w+JP;Go2V(7?gt?j!#em8vMBrssfSI$TlxIa)_j;=5mQk?XZWm&lQ<_ss zF!~?L_h-QAiXihmr+Lu0^2(k%i}SdNM6l&k~@pCGSH7ULsP4p-P|=5&grPd=Y5UdrGO; zSV&RcwD=Sb@+@O<6tZy2S=#tn>H@Tl zIj0Q4KGYK%6kCXs+BR6495{Z00Y`no7@++ z(l$L_z3)Es^h+&4L}i56&{vzTrIQkg=;YW;@gkI>H-tilF_BSN7Y+TJVFeZ;Gi^aF zZi_7bxxX=L+A2bCwt{Il<5n7hzKXbOp6A_e_sT1;+`9G2kKf-p-=9T}Da$k^4iaP+ zq68|#ictc`7!)cnv-Hv+) z3o3hRhl(M!>NbpdT^m2bj3N>OCNt*I$eMFbX?p#Q*S`4TcVB(^mv5fjK0Di=oLt)N zc0>|m2vM>`kq{#f!!XJi0;MGNBmiVVo4)cnB}ua+BCe4g4N`I_Uo@kQuTU84J!0nu z87;^(4Y!D+mNl_2)EGv26^4kK(kxx@iAtlr9bs{&<{gm^c~JA_x{=>A=_&YE9%s>+ zH&;4Fq$8Bq3cGDyXc~y>!W-5 zz>AK2q`EX%9XAXu-IFy?H;4fpW2J(dYd9p_?pTJ8>&YX0D$6HjF zE84KSuDccHo7qa2z&z~Yz^=+5IBON1>j+8)?W4A5$4&fSEOe>Bn*@enL)mD&3k3p# zajzSwkqBnd4xuTc@N9h9s1=zsHjgQBC4{jeV}6=(%M=O597QBR8g@+r(|*uo9v>s- znk8HS`xZDwkLDq{a}Q04{-FgN(K-#87Y($&h)IX1Ch{fAVA~CjJ#2AQrk{o zIK)%a>x6-+?8<#^w?6m^(UfcPZzWZly5_)?Xb3{l9pnYpxR_{`VdmHvK$le$ti0cO z5VuGyA zMYM4A@ovyy7=OgPO3ij5fwGx7E{iL1iH6C7WEE;N!tR@__?*CKxvuVCh@&w$KjOLo zxu~N=@5A9x+K0s>%Hq>P0nd5RHaR)xErZMTsOzjO2vig6a-ZHLcPTf(s%*^D2R~Ebv$M0qwEvec{`I~0 z-rt>`Oov3o^E?;Vpt8=3`0T|#jyobLlgwS7h^P+8Bz&d(RX)62mKwrjpd_Ml9)u2_ z8r_(@253EbVa2!j8iTv`r1L5?X$+GhQl?MC_Rc#avboU<1I_WgnBMr0e`2sjB3;|{ z3>YZN*(e4*?YkDQxOC~#XPPteDc8uAAI}m_j$_me3)~TJfEJ#z{`0RA(yU;I_->6H%_kt~MI1 z)r<`+$Wl?0&KyqD6lzU$JY+$XGoh}zww&EKnqp-%WdaLkZ=+eilb~T(+OHcUR)L$D zUR3CoA`lb9On`NfU6b>dnjgj)12sPH#^EDSle7Qhnnj^Zjj%<;uv!|3`b}F@nckan z(G0d+$%tP{(RNs_hkyAj9uP0DH$_7#w43)$+#@hx{}913X6%We_{RR()#)C8y)kHa zad?@_pn<5Czh>g+NJ_=nkNmH<|a!EjKhqXtErWji@YM)BYO9VFyAik(idlg2iXSSaCeK zb#<-rL2ZMp$Cl1&cCO@c-Vk$b*8$I^kx9&<936nNY()U-c;(t-T>_dA8DRm99uDi1 zvW%44@u@BY%d1xV-QbDC>ss!Lo+GXc!9WAHYBQ!zt(Hr(Q;R@47$kL|N)s4rSf}V9 zslnew6+=Pq5ll^KgjL=o1`?svY`kE&R;+kAXG1CYMG0N+HuJURX1Ci344)YT@J=ml zbn4oeVp~egaNq#dxLxjZJxo?@W}fHLzh-7!!>q-Hc40ocBuYWB`cm8!um4FT>snFg z0xbf|HZp7&9? z442|ih9*M@<2YiIsVs*=7`^e7B8p4ukh+MdQL6|k@RbFiE7i@=ND2HrBVi74z zR&k~zgk`BR&LC3jxyGEzdrih%J|aSAFZSo>=TaBL>iG%Gyg*5kQqlu(E8q)GvP>Hc zXh=mFiU5whE6)T2{pw1p5ibsbSIR{d6Sc;SjOij=)r*L5U>%`0B_izlaEOECoSr{> z^yfeS_u1L=-~awUZr=DXjJps6^N2=Cb=i?;g`u@XL^>43uBOx{h7P7Nl5gRoS&&0d zM?{#(u;JIiX_l@&s$8TZ__tj>b<=AR7(S>C90n!cXdaoMb3}wKmb@&DDqnga!w6zL zS=NJuWS-|?2=Bb}&PN}8eD~gu=P#ZQfkY(DGKCam8G{Oen9FDam7uoUO(#i+!-EG8 z?tJ(C&6_vhc;jkuP^zWW>!y`DtkgyhPVEAPGc&h4+i2_cBA z?pF#)DgAW!-Y>uW^7@-^1R%apV_>r4-vxGyPp_J;fF%^d&y_wpX319qT#WSQn{Ryh z(TzX+;g63WJl*g2Cx^H@i8<#O*Td#xjDrj;Gc%u@oSdGX?)Ov9d6rx{Fht9seu=0= zR6fyFrZ>7G8wvUc@&zH1CO)Vv<(LF1%`9kPsQ4<^Rs>s! zyQtU|->cO#yd|%|nN!S+?ZOK$6NR>1n0sHq@Z2gU#wMwC6Afi%Kn)XfZR)kHv~{F) z%eJ+2BT=`m^irS~dj%Q2i)&OEYu^akYe8Ga&mMSvs(e(Wl5Pqlo0JMyk4OypjmE7U~=7PDbOx5dNDF?S=y1yd!@-}y6)2J z%zI9bltmQ^l(#G0@z`>qF{p8(X_13PdDt>DlMy4KrUA`vMPtj7QKu#zhNkU`bT+G=zEIKtV$yq3jnbV3Qd1Eeb6zZQu{3Fho4pA75QkvU zRt@v3k@wtLTwRF;Az2Mmsom_T?a%Vu1UH7!3j9f66*yoa5ahek=|V+qsCbF+UR?k^ zdMmbg3gvI8=QwR8_9aSth#%LLusT2!>W2rs)hSS2g%Vhc44BXq1O#VHCk2dtN_NA-Non(Dprz;gv$m zBACKFuDzY6$-o=x>B%}Uo$C^`pA1DL>ifT@nisEllI{h1<2FC#q|8 zsn*W5psX+;560(Bw8BeuiSfu=lF#eatQ7!0SDL=)Eh2{Pt&9!SE;NT_KWW*VOQ)x& zpMLu3pZ@&cKfZV_A*L)LrFo_h<1maN#!}Zu4iY&=j**8UjAImvFJ3&m{g1zW_|c8m zUVDX^Maq)TVkLs%l3D}k3U2V}T9H!7lom?}YgNQSPESv7-MaN(|ND>6o;}O+RA!2X zAq2`fO_b@EUw--J=bu0P@Iz+datJd^=5^6`nZ&=im+3B3yRRyM8*sWtYVRo`QpTng zv6GXN&p!X{d;jabCr`dUpU+PYVceY-Cm%yhLPVK4P>4dp%qN#lcc&-x^x3q`DU#4q zH!KY}YI;XoLK*zEeSjuvv}WEOhX@eJ1}>8_f?=*9J*(g6ga>_1rd@gxqK~@Du!#`EE=5*<{FDIzZ2OE4xm^Uup1zCAOr62|5+&u9N1o#dKvaP;A8<&xHi`6Z^_Lp&a zedsHu3;97!b(Ovq9yvn7=l}p907*naRQ&+!5lx|JO4IB5!mAr+45S2582-C4)3|_V zUQ-_TO7F$yI<8r6+j2_T=jtlj$7ch`k@N(r*+TLN=hCiYGI;v62&#Q@Hs8H2Oljox z%2jNAaHAG#ZYs5`WJF3=)~=hY8Eup+C2Z9K^~%6bG`N|FWT+uQWt%RtF_XXqL!YH4 zuu~(np~voxalj+SkLg946{g%%qEWiW6Js!5?~N;MM>)?DJkg?eEzvosm#?LLeIp4s~GNpP8c<69r~zPft!M7hD#6h;_B1)XIQh zbUVYhYUggZ7GRLy3Ci< zbj8OHhr{{#`7}+`Mo6%kf`E-PIC-F0p{**xTo6p zm~x)>2Xi{J)cw1{mgYb_hbTld%X~P`xBvdvvlq{%Y5M4+k6*v~#^saUc?da?Fr_5( zJO{^6>wcw@04}Ltms33h*GP^~ViDc=i{Ku|Yb(pvg$nS>KvUStxK$hm7n4$pdr59L zfZb$Mc3v)x{;e^UgcJe((KzKi-|D^Dx9YQ^*u&&O=#P zK8U2!YTm^m$GjWHG|HUA`Tp?Z_uu{VpZ~md>(-^qm-9S_5bB!xPM5|A-Z&(JL+d@a ztv#dz!m@t#E5`W#`|n@7cJ2QCdxyi~{PR2q$|)rd!{O}w!Tq17{eC<-ncGR%msw=j zy?icHzjk#2IM_7C)JtF;hm)H(Z+`aKr$2oE!=p!!_WS+GNgU#?W-Mofml~0^8qj7+ zroh|;(4LOi@L(A321I=52PU>+Xb%wVIIUSsOGVoCR$DZ;B10!}ZKe?Th)Jv!40ChH zmztu$RjMvc&~5He>keUKF(x^8h9Ww$2(%tBjj7U%T$qe&!+mX8NdMS`=BDQ&BJ04q ziL?RsI(b&Rq>u++1>iibg4uOU9OLMQO4O-`2&G09EAk$HAM`YFS1(xms5+<~tY+9Y zj)~&5EuQAWR=&U&Yz=)xTiT`!Ch){PTqvxrYtZ!+CVe*&cC~4K9#QC)T-0j@bT!zm zjJg7PK210>rF)*cA;=`_8m+S7G1R^AK2RSI`##y0q{jtBD2_NFT@<~&$#p&S2$RYN z+O{_mY1>yG4b;05F_xfEbfcV7?&`N|x29<17Vouf!!R}IQ98oXg;~fc6lL)f2EcR2 zh7FPJBD_uZ8n4MFueYg*YDvq)lmCGE%5jd_;t3Gk_c726KI792uR_$f7s{ zO_0$6g@qccFag1=&{W=ZX`wQp_vAjEx9Yuq;XsQv!x_{wC!~oYlM70QPWLT>pgd4( zei=5o8@(9CnMo|YG&?hwPkXWZOrObx`h`M+!fPNcKXH^?x#Bv+-1RKobgvAkzScp# zin+Mpqlgt%)&)D+=us)7?sB_^mt6Y|p$_dLWwc=hK~DzIo#JR+xgH-O^`uuEk@O4Q zs9kxZ1E~d;a;5=;j7!G09~bIn5GJBw9LI6Y?O36-^?q3gmg^~b%v{FPa&Ba2XhNfx z<(y-Ph`+KC+-6GktZAB5QZ1s4nP%8h!)=d7oww_8;u<^)3#p!A3a%wD+F%NJ^V`A9@UnwVyqrlRq# zc7$vtHAvov5SV4$g<%*Z&kye3{o^10?}xwq{NYC*-?;JN$)(GO!<^>YEX%&;3nEko z=2`(V5edbHgH`5oy2Lv5|Ylu&ph`L zc>@+(p>}C%UjNESgl?l*x&)=|1{fj;g<&4)ui=t}U|lxB*Ys4zORrqH_30;n`};Td z@7>LTf^beW^PJKkvV5VnENBiPgqX+OkR;PgF@>j3pMLx8?fdutfAQI0 z{`B?p=g(?InL=2Wq)6Fq!GV}}yB%{ZlRZkX?1^AnP6;Sss0w(DLxZ^o!6+A>vrr&T zQa`VmH<#IJ9LMM0^}6sX56wOZ7lKxhOBXI+@abstNU%DN1J|auuc1BL(l$B`DI>Be z5yt_YV=w7JCNJ{rLEyH@ zvQ-4?7bA~JZ#07Vqn#Mu3<6z2Ou6L7yQwvmBerUqAl`J1KccA^zeXmzfJm=-V8{me zXs@{A|2z>bXbUSJaHzeIf{6@998-i>s3&XqGzhCWrl77A{(1MI;UqLQSEv7pJzGI6>L&0W=cUGbR9V-g2{d9db_WCvXx?UyI6G|v?!hrGh72PDU4qo}ny%Kc zEMrc`LsqCC>njg)yFk)AwWr^eeJf3u6O$}&pWe`=hK_^EZ3ks+D;Hx(w=82?B1s`Z zoo?k-`o;~n;)dQMS0MzVq7ECAYnDLGp@f@#1Og)@&LR?c@QDq3hWW0Qmiz2TDU;sj z*Mfktid9C5L?komUP&tm5oakc(VMy==;~WucFqY4agXy%%L`MM)G;-pc71P4ftiD3 zp>;1KhC5?3#@9>&&ca;#v+l5h(oZ>}BU6x9t_i}PLat|;nK)MR>PU)tMeaHh;!5Sh zy(12gLoGu0?=#f(L^KQo!D$I)v$=P-avwtAEV>Mn%MlzkLHRsOd0YNTmdsWP9oAHD zHKU(k8y!az9k^tkQ$o}V%}7i_GmObD%ri6d5Mvpu3L!9jr3OJubv$cJ9Rj0}QYQ%% zTXamlH-p`UR6Fa?)M&;Y@GaLak5Xo69%!Mto?M}2ZAqjK5@XbEtZKYiP7oqEvs*a{ z#3WP)SeNrL0tX39!?+ymAVNebrTu;Jl%hl$!`9Nu1Js4LSZ3R>H(-_o%i$_nLI~trVoS}tmNLZ1Ij5ZG7{__eAq>R) z^G`p%c=q((y?aj|Kl$wQFJ8HF^*qm#5^)pcJ$E&s!CjCvJUDy%n++Y(Bq}el`HT6=7ZP2di~jx zMGPB z97l?QnW-ta;mzABZP=cNLe}nz?S`31B5j8pXBx{U4g;5=hGC6#LUkAlT+*J0Anj^} z1un7f!|kQ%btu-v4dkI^(!P?q?8QiASpv#AXO`7ejD?sWY?8=iX*N z_!8e@^C5~9XEmDDjMsK-I2``NVTZqn?Jwa7`?EQAW5cmCTF+>-(um?iq$D=UX21L1 zdrkrAKeCYZLni7p*A-N&ZXFbmk6$K$L>9npz%&${Oh`vOM*Y5`w~bh^YYN2)d*liyuK)MJAHu68e@L{>GvZeIrv z7!(Mu8A=y>hb3ebT@)!sKasG3eDtT*l$B4Tft>V<(9(NvdFGR%zSoscJ11=WjTQa>qN;IIF}u1bK7;HkG-P73R)0IUiWj* z`gM0&d)-L2l?LY`%pVRFky0iiZMVs-&T~B+W{o;C<+Pj9lxL|_C?(B{TC%5Gg6bTh zWlcw?q2!@^R8EZ-G^+|5acE%Ou;IK!&wFR)R&88Ql2I3i15VRdm2sVjf&8z3{x*^5 z#tzJqn50%AIUWxWA3c8b=)u|9{^qS)yR%)&sm_I$qvLC>#cuZdU4?`i!qe5M^ElyN zh@_1N^hTv4=I*$kyg6~W3OCwXA661+t4|ggaaOndDI!%EhFKWsp&l{z&;q!oZzN{e z^+sRsdef~zgI%u2A^>h|5gnEjEcgAKYr?tF#_wta6fbVa+43UuoD?THKpkK> zrIa#H)9&e$$6tN*)!lEu%{k}9yWNhY)>=|x-9=%y+g-nL{e>4^-0ydf9zDKy|NhnG zTxNRi`t_fD@Y9ce_F>+otK*d$w;Pv;G-s9O=8#CU$`x)xOq@75&LxZmrEPvM4P>oD z;$eAvi1JA(Rqy}|V=tUcT0OPa1e2dexeuM|00aK)T!*z~H1q?AZpMf&qClX0SCuuP zd*kAW%DAJ^g}oyR%}ER;3B0T)IS6D4CCzx>l7Audzb30&3&P;jjbm(TB@ubcIiF7>z7Dt+_7Ai2_ST zW5JR*Y%<&;s<-$u5g{4e&z+b#w~Ugwhp)b}=9_HO8QGy8x_0k864I;$+X5=mAYovD z?V4YrFyz4UE@T{AkM%RN;lFdiJwELf^J5VhE~JHq#-j^u(BZ*}7nSQ?#CL;dSZlDU zMeUP`9Cub5f+~uX!SBVYw(x@zj>A_3gX7v2qtW~j0qkmvP_atzA@zeyeBI#(xsIla zGQ>KgxcUyRyC_@|#y=eVErDMeRs!{-XYPI)R_g=^_Z~U+djwS!0+taG9DT*MhE6!D z_{4KpEi>bk$|zxGW?b`LUqmN~7;Zdr;N#={hWO`NOyD_JD~rE&73kiS@I=CyKco(}MG0%z9LZ@XQspctrKE>; z*rK>b=mZuga5{`XnJ?v*t;Q{&Y*aZLbO0FEseBbpF;1E+N4jTc%1hpACDDKNS7H_- zPD$Fla(Ro0t56lHBDJzCDR@$9ZIDndY$&S%T%^-!Sz44wuhbtFZ>%L+heodNz((z- z3c;6%bB6H@hpnZSY)+t4cMhe-&hdD(%|lhl(=?S@=eo?!G-Xo1RaawC9;a#A?RI)X zR9(-@TE0>pzJN|D+z4etV}Vi zM~C4O!eSJh6hb+rDQBuu#Aik?CJCtk?ld-QNH2?+ulox(PnA-db~_Uwis*Pes>;sK z&-eSih%~KfHZ<8T5NNs5vdToy1)8RuCi5)=&nCn~iJ9v1CBd=GDW!d$^kJQsgxmx< zG={F-ZbzdXAQ`+9Gjo!Z8``O++!T~j%DGm#xVU`!^zz}O$A`nrDev}Y`~9_~coRj7 zh{)C9c<@)8~58v9Wy;F2)Ha}WEF+PD|*1vgluc4esL z0$1ITv!7A7cE>B{TWYXh-CJ7)?Ah=J;(Xkb8o->&Ks}4`Nij-i5%y7PHEyyS69cgX~!%i%h_?9h%=>1DRVvw zOTE0h_~Rcx|LUu+KDu#hnx^Bi;A}J6jFF8RL2k`48NaaxT6%)DHs+IF!k!<@|!z%9^Siu{@k_9TzhP^3ca#u!)i{b%<b1z`fA8Bg4_-rp>EAWUoXdn|c$*8saNk zH8^Pu11MNv$Q@RTHWjH-jZ*-p-QZAzdTp9jsywYB)F6I>H>BVBpP_K5-|500rvCgZ zd~wT*x-wB;*mMN5jfWB>YvWYPK^@WMkQO0|qNrgj14l_@$FZQV;F%B7d(r4=qXpS3 zL*sRYv07vkp@PMt`cP{KyaX92E_8^Zu3-zR;e~*=OkM;7!Q{>HX!e34qF_EXeB7#+ zu@h(hLprUA7|5gE29Xjs9l{H{4})t2&ldjUb4S${B(P@u39G^kBgDfq1&*({)*X>2 zpd0^mt|r=7d}uF5w4(s2mnYWgnzp18CxNAu#v zcO5%JPO0^$raLU`ye`oieq99GTUNyRGtCnTHci=(-GOSWw#a*H+E1D~s+geUscR`S z8u$%O+k!{-lTCIs)?m9v;sH30)0NDImY7-T;DPsc88Mi9Yh5r_63t;t8jcJKAavFj zYpDHaE7Ios78jF#Z7-6?ZAxqTkMDFwV2MwU2_r1ou)w1I-Xg?3tyTakIVSSr6S_E*C~<0 zG~tBF@ArG#&y)Z{Sd5|5QHd$FNRn-gV1d-gKB~qbp#pc3K@FPf*X3}c-EOxm?SnNL z`_gvl)mAj5xa?b)HSH`r)=+?oSd9wWmXgE8Y`aOYBQV3j6>Z8O=T=0V8a$=Z*0&LL z+1;z(fL!{6>th!i}|LR+BvzjwQM08Snfc{cUizz_|J5J<@P{wH`0{WpX*v?^@+66vm}-$)504)|e(>NyBE|?s z&UwGzr)6hfNQ)HdKL$ai|CsMU=3kWD5gM{_Yj~2`eWei+iF0 zo-m5Y!UEUr*5STx}iQjPLKts&l=s5yM!M?qgv4$fDY)*PMAKREqvsdacc z)~E+YBql;q<=2>aoThMqtU1F6tv@NhzJppw%092 zd-aneESaqF_5^2$ogW(-2nE$}O`r9Ltk)r1XMZ4)OZb(PDu#!O^y7yamuNknwNIO1grBSM9Qn8d!yB_{B=NU)P< z3K^yp>_|jZ?%agD-|sa$%1lFAim2@fAtH=vyx@W?NbMS82!VC3HMruXp0aM4it{Of zK4?p=R!@B59H7;siNgeE?P7CIt;lQ_l&?Z01wyD6!E|^?YDaBoXM3k2FQ_tWx8gBq zrjZud<^HTWiwj!@vb%aRszh>EU#tdSvKDjjLpe}05Ht2QvYeRwSHwg_Z4aRJ z$uu7;C-^ckX+VjXsn&Xab}p2@|L)G;Ke&JQyYGJax5qc$cxMjM_E-$j>5;m&7=#|E?VRAq zmu;~YJ)QHOkyP}UNWAl;)+jE*Na&;^NlrUk1=e;`vM(}-sFZU3`t|qU`{1Ad@&CQ^ z%@s2j5kAU1)l!66GEpIs#F>fHBqGyPr)d%k(VT&p2yqOR9m|mA2Rm_Fiu=Ikb{`&GW2_oH>`Xv$Kyr`si0b{?(UX{%M}) zc}{0-xR4SH$voGG4e*J8>FQpPCT>zh^DeopCI~Xvjw_Us}OshneF#48`T;9qS z{b-&eA#_6%lyh8#cJf58H7tfc&3dc~8);d2SrsuL-jEhu-!reF0Jr`;0RUHc*t)a0 zm=dT}rG6h?-uQYNpI8?QjCTZ>RO9QB)ChyfpefOfV$ql4i$iHREpHKnz`zi;ts=xO z2K_cth&L4QvsKdBh_o5bu(Q~*rs4Bp|KiG{D7L6Vnd3w0GnZq9c8}RN7)wRrI9nU) z7oETs!jZut*dkxihv4f8vaW3CPcXI~S~0@UK@lbFR;rx(oI2dQ&or+199<%+fP#B* zihoRtQvd)U07*naRHk10TNS#((tFMmBEsv0?yvxog#RL6(Q>UM-XN~*IX*;-9yWSH z{_b7mB882choac3-_<#k&yXI54_DDBoB&Y{k=e)~=50j>A~1m@@`M?XBJdn9R&vPT z7H)gW2jSN(gL0PZei9gFSXEI?96Wy{73qwC)IVLuViZwa3~)3%GPW2eun~M0U67Xs z+!J+VhzPe$1JG9x=xE3eiEtRA5NYskwcOJeGvk3Xj-4(0jke$vZ9i#t9!lP%%Dn|S zi$md~Osop6;YlHt%N-aleu|EyQiO4+9GqhzRd4r8s($Gy9+NZoL zRw=;J0Gv|N1V@I{glMPBnc+z{M?N($z*fl8ko#S3x$Uc~>NBC4x^b}qr4%g%+gePF zk_a8Hf+n_d79_Vc6c!7oW2s4K5=x0FiHIbwOi7oDgl48pQZJug{qgfZ z%=6)AAOH0SAAETI`gN&N`f0hNm?$A7C)keu6#v9xPE64Yy9-TyF`)!9)oi#{pL3DI z9^$&>v_N5XL4@DoGk2>%L`>8jlQL8@{xr17;p?%g_?*|C(vQ!E*H*i=-;{a_)7Yql zvzv*i$}FYiv=foM+Y2X3`Ny~4e*4|`zWeUG`FNFcCQc%9xH>X3S#o#cB$8`RsiZ_y zYR;TW`ugjyKl$X7ci(;Y`pp~1*M9WD2Os|KcYpZ$>#x(*eC@2}Ha-@TQfp!=rOvff z(v{wv65B@#V8vsqM1(7GVj6V`)Z@*0YAftK(w2&$lELVi0M&+~VQtcHskt0Jhi52@3yoPDabe85Fi2e{%44SJW(RfMIx# zdNYvVQleqg%r?>%sS1wCZeMo`Ol6)d*}k1cyDGBS&kS(3vv^CWC}r!R{2AaX*CRA zq@WbL*E}Zo;o%UX4ZSGKB+FOqTG9mKKZ9rl`yq!T#!1y$!9N@n&G|TtZQ%-wMX!ag z53xi<7BX`LyGR5yjb~?_iqb~fDs|a@(CX+vg~Sy4>ejR zsT&TVt0AH!9W1T6))ue-*Z{TH1;dtK&QHq57)BrvYy}eJK69Ub1d{PGaoW@fW)+$B zLsSvzca115Y1G1Ad5B(PtX%Sk~$3sn|w#7sYTbar%ENdqRF6*7rrEPMb8$k`y5zN%K8qk_GWtiKJ zUWj?80_t4~GmB8IweouWZ*6-Pqh4~(LZz8Sm#Kg%BN!wwv+e_1+u404gtrhD0=Q+B z3=>fo#YVd@xjDaNz#yV&w{ycJ**MvlB()^p^@Jotsf0n})`ckLe(z%uDN-v*;?Deu zmKb9^9&jSjqHOZln3M=5&UHQ(IZkTO1_wG-A_q+f6-PaFO-sSyHDjyE> zG)+v26VbYG$hr%RhE1nwx_t8V%P+tD?z``9+`4HCYv`&FQRbCa-8BlGqeKRoJKn8u zn>H{r*JktQ=jU&~{r0ba{p%-BzD>*NoIAsODdo|FhZ?|Lde+)Dr|a`!w^SC*=b0Cx zlwmkx4^z^ajI%iAq$mAA)=KsndqDF7bjfPzKkp1!O?M;q!0Z3>KHDe2OS>?cd0aO^l}+3T2aie+NrSY*v;lQ2w- zEMO}qfEoqms*3fTcs&+Cew`&I_q|k@zTyoAe6f`+FEp}QZ+%X}(Qt;(T@9uP8Et*z z_=?mH^}Z$KsNXH(heokwDL)()6rhfkM{VLbC(_^~7OUM-E`=ewwP=I7*uhzhz|7%9 zepxg_mn#N?<51aKfi{pEvqlB^5r|LCf0mz>yvFs6&=T z1jiou(=*i(Z=8RKfElu{Z1ZUNUBkC%(Kn(%MxrGVu^d=gLs1yI4a2I@oUZ*T%j-Rl zLlH&k#WL|efdEZ|()XU45GU~Yj)qjE&zwyuQNQ;q8Y4l59l_ZEwf~OT3SctGqvBJp_GAR7YMcY6STE{dtySsSSq*htEGz~N9Ze1YXa_CU)R=i&U$c*bDjd> zP72r1ZR$cxYG*JQkBCXchEtyroFMVm3RoZ*kx#@#q>C%~t6muRrask1Mth{#ECZwe z(#Dg<<_R2PJp_JWmU?xZFE1}^q1|rEyGgek(S2mp)#@G#x_`pjFYq`|%h?@60M}kO zQ4h~VF?J}N;Fa1r=hVCI)0Z`ePfS^+0^6b*#T#owfKAi1CsLkjsv^0t2vN?=#7swT4e&%xg^2;yhA`c%veE+qf`{VJjOZk}RROm1tg((Rq=zWg%uU_xn=3gCpN(j~5TPg-}mxhpL)|)*I(Vb4uy$ci(ybr5C^c`p)5SIG#^M zD$Cl7fB*gWckkYP;iVTj_xpsXc1B;KMH6toGP+I*sx%?E7u&z9`W6sY7Un}aq(muY z8ydd#_K$DB^UkNAe)|3Q-(6f>?DovNNzFg!EK;64c_LD$X{ViN;%S=pXJ=OY-KTCJR3qQf-r8J8a#7HAv0I7P-LVHcKs}__Iq4*01k6P zM>aaW6fzUBhhj+iWXKdVRaz4`k%%fgtwg*cwUHMmRT04tnou>?bnSP)a(;jUycq&y zCXERAA)6<-Fqxyiuyt3PbLRLEe~JMYR9Q5WAyu?^^6pUb#bCo3Cef?7ni1ZPBZvH9 zh4|1lh( z4P{N3hJhah8!Y3oIvSf%umW_yUW`yU5m%A8^COk2l@gIqAr|{^(g{sR^_@~gn7C?F zLd5!spy(&7JtfbWqpj9TU0iH+XrM z(|{<#dZamF)@}6b!jA#fLYX*FB(P1Zf0lZfk5%eEueqg~&*Tkp^NQBJ}Y7 z_g{SR#m66i{PHV5LSIHOrLX<&Mc8LxkBBxLnIfW!+c3e*);xR8>GjuNf8m7})KRKw zC0d%B=lSlv@4x-_+jri3SI<%~N9KgwIgXJ$5uYPP`{52Poxuz`rFc1orj&^4<>lqs z+1c@UWX>)egn&>FmYmu<(YA^)J+?ZfU$K9qqofNV+UuF9km-C z5&`)`q!C{qn!i19^`o;A5&O1jt05GuJ;B9!1TZpQ`{b{!OJ(SY-=a1WM!__Y5;r}L zva0a07>>ei1cQ@F2Nk@3=7k1+V%CI|>x;H#p4#CdHac+@)VVKkgYm}rlF;4Y{3LoI z0;SuZ+%EU4Q<-s!m~;Y!We~*4{aQRuUpiEMrw`aQGKWgq{Jc3mDg({Wy~Ke zGZf2E3;0$vTPub(8((Qvty9u5_uw$IN*SYuYrFK*L z2ttO4q{#9=Bo9pzzoJ{7Z;YL!icq&qxZJ=F_(miO(tAvpX3^xFR-8GFFep z+7hG<>YoYD>ag#rEntV^5L`|+0<0|ujB2NBF>8d&-m-B^T(H;j_F@X3kXkHQKLFQQ z3B!`SgDVQW*M8Fxtw~_JoC~e+GXkV{6sQs7%fjU%V&@kvCs3`^bDy!8+SbDk&svXd z66z$caJmIy|?k)_8pcJQlW1 zmbKVS^>{p9TwGKlNt~y>`4g>ru7xTck4N2OL1cB;BuS`?8dE>A(#s5?wm_#TSC@`( zN@Zr)TJi2wk8|HLYEz|vYh4M3Uo8NKL3qCMj>l2j7NA%mK`+)lQF;E?|K@M6UBCYL z$wjF`#3Cu@Of1aInTU0(TgsWGim=prB&LgtCl4RofAaJRC%$p>`LnZgAr?yN+EU6y zT<1Bb-1eEOM7s6Gk}yZCu~el?(~_BpZ91m^#pm{<-N#{Hve#P~@X@0Ck?I&t=@n+G zvNTJ5f1`U@rshA0NWa6;H><20S<2Fi5LBCz`uTOs62q3M!QlYy5cxurLs(#chVOa= zKoijj?H)PMlJy!lY}A{Kr53@r%ztzr48E@ACQCHO@SjIZs(cuC6X$c?O z8#j(~W#V~0=A4NNGl_I-&Qbp6l2GqXxc?tpSthJh)9w>rYcme=bEOS3PFT( z&LosG|NdY8LZ_U|$Pm%^~oF4XDg2?V3MW+^6|YBe%( z%5ZKT5vSDLoi?pY;%l`_mrNv8`rUGuIfsxB45iV>r#M#Yp3}-A>DPHuNh(u2rM9T88IZ{)B zjp2kz)IaxtaSJx~+@d^-dxwz-gP<>brHo8TjwMgOktT@Ru$?*074r@kNCLt8=KwkRL_9~zDd5vV*CdHp8oo;bT_`c6)_E#dQbcs}rJW1Wa`3Wis3|Ci4jrSS zB2q;vv(8!83alfeF32lN0yF7b+csFu8z}*9nq*2X`O&#ty{Tcx($x?|RJ{BBo-O-l ziBW01ejVdfs>F-mvu>-ly*12$jUEwM^kS;75~IK1U;@H5CC)kJoJyUwZz2|= zh4*QiN~sLT+nAlI>aq1&C8tTx1fxE$)``w_J%&R(x5hQMGG0p7uSIK}Qr=Bd?Ye9Y zC{#1;L^S10x&pz8Q~vJm_ut=t@bu~BZhvjRKii#O%Q+EI$}B=s`8b!WtE(qZp4@+U z|MKdRnQOH#d9{00Fld*Q%-t!$yWLJ>;VZ@vnb* z?ffS5f*xHqxpwW^;cy`8dKwdqb>6Nk!eRoc!X^}Z;)Vje^6cyB@BQeImlg7ECC3Rp zHWZss;8yXAOMr~Qn8rPfFGen0Rp@z*Bm5IN-D>xlv~8(rr~(UQpoFbQbfDKyJ9hi& z<(FT1|NWo*`Okm8xOl>p=ebThsfBCOx0W@@l+u(^&Gl@TYZa;G;_~?IoxgnY$tUl; z^UljJzi5YD+J3lcblS-l_V8*2yv3S)bbh7&-JIHPw|nos_g;MQ#ivgmiSRtnDNiDD zJRav#s>omd@|U}J@7{d=d0nqI3pEB&{^n1k@Ts2?;Bz+(c6i46odzSEYONw{IX%-p zz4Y=6Z@l^X?jLuThocCWQgYrCktCid7b!XjWX>toyyN|TcXqbRnM>(VEwY@JYhDEhJ*|Ch4iP9Kiq1O`+1A8;GPfJj}|RrV=Ztj z{V3C2$Pq2F>R@Tb90((iNEoIb5cr3b((#CqvbEjeqP=V4v>f!twrz=$w*d`Sg1|Em zR%_MolBkVzoj@Yj5qKzJEU?(csF#V9bDQeepN?^K!m+~mlU45I;FL8K@>c)iIC2&p zF@|(TL}BaJ*Pz{TAGdNqjj|nDlPlvQvkp}8-pm05F<7BXm_h^+SOm(`$;aW%kv+6T zBm)h0=rd6`u`5A=$A%)EQ369swyvRD8@hA{PV^hd2rfJ$B^5GBon6GrPk<^fN*-yb?aX=o=oo%o`CXhU zU`DSNYxBK!lyej#GVb`Y7VX02B6K?h`;gkK$Lfj0v-LT%-`S~Ah;dBx0Bq~B8z(VX z;o-Z{zcFNV>c6hrTKFPKb&-on(kgS9EJVaq=b1=PfM&y$@=F{010b~s8!SNUE~ToD z@oEj;+a#2>w{BSO`@x3vr#6|asP$R7%S&Cq=+E3O=;HdW1`S1|l%pe(z}UGB)eKIx zuDbwBiyhg~cAFP29YxY~?Dv>L>DZ8_;~;UQX?E^p{d%-p{BPC4hC zCI@3($-L{Nl=l87v+(Rk&Ht5l93?YTA0HSt1Y}eGzy8nvYJ$#sR7M48aoHHj< zt4~Q!Fyq8oh)C!-*IKI(UtS)n@awO?{>B@xn+B^??mwKTC2N40Sw}++2p(?;12;&G z%#x{CL`3vEmEZmLcX#i8x64zWa>|QyREKgb^IJcD`_S^pJf%F5 zq?GepZ@u-`fAcrbKmWpEK9pM8jyXb0I=eLu1DYUS84b}SOx1?>0}#@hzu(u%u|gD) zz<}G!c`e}TBxtwO8WFXRq`Fd$M+(x2hGJ2VG znQla6S|#rxw;1=v6#<50q)|82wP=r=XF#%}N5$5+1Up2ayPR`A4YvOIxRQS0GKikI z)_LTDHsrQS8(%TG5v^yLkkP?m7SlWu5m^GR)yYE3N0ypm)z=@nXp9j_+niyU{)1^y zwSi@8tB6$d<{+yoJ;3xP5j+1QQjn)10JI*ASrBNj1q3_sq9v|bavR;cr9fhN{nge@ zF&~y-Xt6}QxErQe{n*FZ=v3vR-$^bf8s#;$#X^A*(vkO$g;JR)bIP13bp;lM^tBO? z$`GbB{D_F=Y1-L261G%|B1OjWr_>IlYTtSz74VAaD%MJWlwpF8^B7%6FD2LdA_mx@ zYY8DQ^`}xwl|sBm6{UR*W_4P2D$cN^1q9s)0re%aN?tch%y7(SwQPG(!+}-ESB9;+DOk z@lAnvx8}&#TJ0x9Bez%tBG4I53f~tR9!wV+Q8kP#H$r9$P3hfYfc`+tB6M|}zrTC` z{{2Us@@}`^pItjYJLAl?R{hkvlv?I;adGkR;lqcI9?kQ-I0mlf>Y1%dCh(jp2m3f+ zp6BE7sEO?=x@np;+n3u?L@C8)peQl0l)=Z2v7|F<{Iw}A?f3irey_&<^y$-!iwhB< z_Q`^4cd4!WPM4T@x^eyal+w?Ces+F-{@mGfpMU8;p z986<9>JZoR@J+52#!8$fmM30ojw$9aM&S*WoW4-sNQ3zo$8RLwwH`0&=R#)8u37>) zn?SbM95!c9a1Dn=9TPG0@p#Pn{MA=qf9uU3e|zVfQs$iZwJzzOnyE?Vpm$Ov*Fw9U z9FKLk%e5T7`|g`hKmFvr_ujd6>z4G}hn*1qmZsuE>@DnF>r`}6E4K;5^Upv3+H0?U z_SqlGe3<7srF99bkUV_+=-$11mzS5uZoF+d~<|b>XL}&l8yfZF+ zXj?MXS`UZAwQJWt`skw%Km73Fz5BLtz6cZP=M_?FEk$USlv0}Ve!o9EJEM?l#LQBV zujPiBYajSCC-4J#cRUV7q5nU8jy}g#WpoajfD;~Kf#sgO?^=Z1dAV*h#_QmzOP9cU zu-H%tNR@HNJl=tqnYrIXd1yOBsNk*HHR%yk8Ni` z(q9pxD0cl*jlB~iC^TLtKpTmP?_$%)thu;Yz)?8V+^?3@XDW0%>70O$HeK436HBl6!R!DjcX;dRT<;?uP$2F})XwnXP)9%H;*padf)*~1VJEkUWM^5i zT~|z?gK**<@+h)?_$<sFXksW=BT9hbhD5hDqW&cc@BzQQKZ~A;tw3tttx!32a-W1rlt<+e=cg z7WcNBB)E^O| z%fIX)fo{P>A#FUf)ERn^Xt}rsfG}Pndh8Q6tqixJDT>aj!dW@>1GcuyN3jrgt+Ta` z0ZQue93#R}ch(!?F;=yGHA^rq>crb9H1T4E1UbAG>yAYwJS%xEh?V zGheFt#mvih*jqJ6N~S}pN$C9Ajh}w_@r@hLpFj89zx?*sj~?HzrLss(M3qW8Y9yPJ zut=4eYR%KWmP%Zw9UYEW|Mcr$J$?H47r*@5cW%G8J3DJGzL5BX_NbdoYWA7BKWcj^ zl63(@Yt$9$QAjNvp_2}J7JD9_5I9^S6452w=FuwA z1XB10Hj;X}Dl8=H4r}X3h}oRp(nPeu(@rD)>TG-&U1?s!hQk=vqsX@+J@3-^W937H zhVn;awAYoSd7hQ`6YwTb95d5KM~m`A$v6X#KFZN%WiWvyzLFrvE&7}dPw1!ac<=bN z8ypLwHMxlmHoE05T>paM;7F=vB3R|dsZoLwikH7 zA#2WlQL1j3?vS(5xok?nDzELtEduaIVvvPsHIc#SaJHB=`5?2=vkmJUmOp)=!ESlW z_zKbx<*aq$Jd;MeM@eGk=$c#;PO6crEo$+tXv?n72vqzDIf{x#uA@&gP`S3YxQiq? z`W*%wzj4ApWKgy{W#0iu- z@?wj~AWBdAF0$8!_dNeaa&Wfn-m-Ub8INGZWm0lNW9;2(=OkBZG5L`^0HV>wSZ4=U zc!i;W$xWY23vi6Yf-^zpkzEh{$<= z4=4K(q{qq4WUE_O)plx@^qw5~vR9Vqn1yVJc|*4PgKC#xoQx87&Q|cd9IYpHJvg^W z4UV9Oz{3iGs?sXNK=iYvn|m z6PlKt5-T$wj|WX)YI&oj*2OIg4p6eP15kA3XFvy4B+agnb6L z6lLe8V=G%~LnE=uw_X`?9c%j`PRg3t67?35v4#`%Z^{4kg~duyn~(DWDy8i9yZ7Gv z$?effe43_r-g)QwmtK7M=zb|BXIA?>)>3Qboc?m>&b@p0 zUVrngX_~ICj%#Nb!aWg4&ND`i)SaH-Ws7m@&28)9JkQt8pZn;ekKTOq%{zDQv~EEC z5hv!9bj$FPQb{SDot{t;gnknGJw#J&6)a{dYQgZ7rpmWkp}@n!pN? zN5?(|Nc7%*R8^1wh1duGurI_-PuLWlm){b`6o$T>lDP#?jDuIuD@5Tqq~Fr$LTJ0;kc?1@VvaM10039M@Xdkv$U5ru2r2aCW-I zlmiv5VjmpG6bTqUM@>0&m;FP}g*%w$^$$98yX)s<+FX_Q`FOvV7|utZB6!_B-jXnb}2_Ws8z7meCeS#i?x-)mjYcr^d1a&Ryc@QUr*_GSAB z>Zik+L#hMah%x7#1Omj)glS~yLo68-qrL?4Pw`6q**_|O;%eL460A`nu-nOC9WS=~ z0qcQ&O;8Sj_5`y^jbJ;)3%5Ja*KK22YFn{!@Vd?$pv2H>qR@~|!wlj?M2^Sf<>lo( zR}Eb1o(3r;SvaLy=Thq7aJam@)JV>%%4G~Nwp_b0p&4wjJl;gXqHS8!_ROJ(M%EpX zroyp}L9|`n*nxiVB`^S(N)cu*#8S$Xr?+pvd+XNC>$k4|>hJ%byLazgT|UvY=2FYm zVP>jlXJ?e=!*MR6$NVLcV@kB!Cppeve);*MCr=(ddhqj~|Kdk4zs8x1>bnq;CfbrL z$DQl$3WmVJ-uS>3In#K{-!x&3LU(7N)2Q370+w$R=pNcMpWqIQu&yEkME%Oe3p zKsa+^=&hw6^tdwaxpQzRu~=Y>wfH)LMv%54J4@xX9N@}u@E;M?T5`^{&z$#QU=;J*AY=g9i^j`Q($^w{L&^@yF9N z)p=P%OKn%|o{P<#>yu)sL{+5w9)a4|wZgU3y5oIH>D5@BQTcv$L~E-caVxRE-rLx}NsBBK*|)CpKrjPD)9nh!FFxlp;iL zzV+6RfBfU${N^|1aHOTX&CFa|bY8ok=dRz}@AvAO4I5gUwjk^9cc6mEnn@l3yd^Qm z)cSmBMb`#w47Bd49bJl}O_pABFo1TJ)u|&bV-+o2m3=tK%=6*1M1Yos}1y|>m z2f*ggWM~7HPR`K=he~t~GxB)CjvN`zx+B5S*iX0uC#A^m@Y*uBNa~h%b&SWFoi4VB zkFIR!)`pUaj5r&_3b){aKRG99<+peoUd*h;qv}U5R-HoPyGSacW$1s}cUl8K;6Qtt zZ0C7^9&h7To6{OrwPoqC(byv1Gvv6Pux@IDg=hszPAk7^92vwHIrMG>NEn&}ZCHe~ z!+<*fNnLY96u&Ww)o!SMnWV5*BFhBU{B5L%bk<3T>gvj%Po|UJtX-A*Nh^ZHDmM+H zJvwG1gVJwHY7en0$EM{B;@Z58Ks3&RfuzM_@S+Q+{UL4#ifH#5>A=H?tT|)ct-RW~ zQkj^EZ7X8)3l&7#<^?O|P9au#)+qz%&V;b+CkNu~D?Am3a}FWF7grIki8yl-;@UC^ zYx|ZTlQCV3$j->3yL%ByZI~);uUlHuV2`y{jUq2QigIFR{n}lFYc`sKJku_vBejjH z1Z=_Xt*{kySVefaUg-p<(aKN}vG5SIPH|ReDMgjgl9WlRkR;s=Bc*96DeYUuwKrMN zOTD#fyiXv|oZ#zwfuv4@nLV_B2~f=~hh3w((=>4qh=d*iBfJ%km_xs@3bVG&q2b&V z^VmdG3muR1)zxvHXA#O%&bwu^+FFH~Gjk#;r5p~2tE($L%YDdoh6!}0v= z{CF(Pnd*H1`)~ilzyH7Q-o5*8|LuSG>MO7Bc6&WIF0687$bU{m_GQ89d=U__yQ1El zP;lrA&=RN(L{OmS^00FABbgJHN25iT8ZY}*VD{^@a^l{32LdOB_>g|67vCBZHuPm$ zm7CH##$BUmFcrQLh+;u4Ff++|h9mPjzOS|JrnB3(Z~y2=Ke~JOZmm*gDp_hJ{gOjO z)UIo-`(3V8^30hNQJoS$y}bDB^FMt0>AUZ|^Ui+1HwTo$wxm(!==*|F1|tb+my-;& zj_2mhn{T}F#`*dA#l?mBzMM0sB(*-hc>K*b-`u}{|J7Gt)od62)jqL_L^?bV#S64` zpZ$VO`8p}s7b;8YGIDRebv4ghw{E@s^2_`E{>kMPkp`jmY@G<@QjY33checK3Dhgi z%xr76`fPPr!OW?3`k6->Dc0i zQEvKH9B%5bcF-`2(e`{lEz(GNl*1O%5X01F2snzuS1=vGQG&$;hGHmZ*Ia5_=3&s* zc_e%2;-`p#6ZD|UTwyes+O{BYMy*`9#W0h3tjdWLiQLLqbaoLl5uEEVno+0Grc7B@ zGrs7AhEf{-I}|C33oo1uSkvwm84U@XW+Pu%6(Y)MnLOKwRfg8Z2m z8u$r_s@x(;LkiK662OUi?MLmn!p^t2cf|#8bqj?YSU~IOYoh@8({hWXcEr>wREa2& zN9j_yFKufOYhhU`1*(EeBBQ^TF{t(A2hlvmX=q~l>W zCAYqSVO;_9=0rqy#?#nSYL{))i_ub-jfkyt?{X7tdJxB%%G=PF);_gXdS{LjaPq;1 z?De7bq5R!Xyk^378|E;?q*Qvg-30afK~*?$#Tl3LKdNi$F~2A)CyVW7PQx*6*`3ByH3-tetg9_LD%RO7ZKb9wlVyZh|-c( zhh>PFQkrs}YAw6nE~n}Ky$5%`{^rTkN1QoNIj5YMcT-A<+rgnyO4Y9G^6D^`dOVgo zmr`ZFKYRP_w{P9LCAE@B=|8G+^b92e#R<5ymJrPiu@{JZEa(rlJ*mTbPsnM}LsR&IMn7XUXrci<-&U}*&@ zrMB3t1@yGdea`ud&p-d|Z-4vf;ltf-H%)oJ+oh!ql1imklC;~O{q(0FeDKo`cDrdV z^E}VYR7%m>glIWYm59{SGZC|tN%c+;>ue^OEg?`J$m`0$w%S7@I&rTu zAsYZi&BjWcv=c<4hiF05!=Dh$s8*9anyr>} z!7Nvmn`9zROqlvpJWM7Qi|R(rn%uUeipA_tgQBwq=aK9=X4yol(2(8p=o}mu1US_4 zHh*yV$IFQkwc{$s^6tjkjiSRAoojL+w6@0mQQxRlXj5_O(d9&Bl@F)cP7X-GkbOrs z{%H>*DL66#yoa{f{m~W_D zlqx(Q2r&8<6ou0fvOl8SBO#iPYbbU`Sj^GT2#{!BwN~TKWQ&AZPZ8OtUJDM`(GB8@ zR-U0k-6>KxQs~qHejk72ap-NBgxBxci&nXsb|V%gVRTxBwN_rTZgrr( z9>Em7t^*!k1S1nvrSr6Cp)0B3I7h3{-5@ck;&mclpGA z$6){=st++05%pyhrj#;wKdluaNll7YCK0nNhcYEuJ|1P+GWHhNYtcIVIDNQsPdx~+ zl1OdrxfU8>^9q}XmUdaPl*=)+`X#tr(^h%S1fS<#0GM zr~Upc=ShT0sYIMoZY^g!011W=D>$VkQKE`bq|O9vyyO(*e5sXT>2mnChJ_et5{%knliO*G@P$6V;3qpr#vN2RA#9~q>>7RgliGzwA*K9n#*x6M-rcq%Wz4X zMrklXPcY-QQbNtIYQ`9vjB(d|S2xZ$ zQ%Z?A&2zc`;L-Q@?mc<(blUCC&d#r0dyaD=CbgSH>2Nq)TwUI~_x;7i(|JD5{??f>fIU);KW>)-vmvrqo;`^S&( zPnoM6d6!B#@GP|+OG($Ro#lyZIZnGXrc@LFrCwe>{p_<(=Xw6?pa1gRci(^R#?4wa zWjNJ3Gv_i3Gt&~IPr9h4VJwpZcHWqIicI3eXdd{`agKjl4>}OvXrvomu(MCsuDjyv z@iLN-BN#?J<3%eLOFGG{u4KbdW6cx<66beN}nEK>!NGydI78=zJpZ zh4&r%wH<1y*ECJ9yzX`rD#9YN-=7iDTuMr*9LwFickh3HPgm8H)M=XZO9ARJ zIaI;ceWSlqqN}avci?wcIS>gI{3y(1SZlq!ygVEZZ4q)RweF^@`kAHHLLzmZ=SPp8 zTwNW{&d#KAg8e$R_1I{%L~EDZsnbYIG;?Pu!wQFD+_GGQ3K1=1`=yCNKF+W$=ii-= z+PaTKZb^pvOmoAb_GeUKAlO=|wW{e1Sspr^D6S#1E)wMAgIcp18a-Xy(o6}cq(M1sDcv=Mgl!S&%0xV46RxDIHr6vFAga=88|np>g6$&L)K9mJM~l2~ zxym6Wv0AjaAs0xk6B>fqF&I_sWQ&asMe0lpwNU%dmaUR#3q#uC5ZbXX8@=R~ap7A- zxoy=jbU{uaF^$*C3!Fd@y^vD@<}^j(BhAjwY!N?nxf_goKQL^13w~R_I&;R7sO4eI z$l)2m87HDcA<$}`<9y_}a5cmgs+EGeT}c$(5`74r3#Sg@MAY`@8~dC!MYK1orM8XM zBP514;@Ct2q7O0Lrj%H!OeW3MXjOhvq+*LR`+*H(_JM$cB z!99$5kuk7X;R^#yR>h9MMb(Ww-8I!>Q!IBb%8C4Y01FXiJZ4+)6*y*GO4~FZb;L zTT~yn#gYmiIa#FGeGzZXwHH@YA^~5LX@*J9ESnDU!oT5&q}4&&v$Jc{G}XR|TC3(A>#6|C-&u-4WX6Dvkd9j@DDT#g zt#ddGfX3IcK@`Z)PVqvZxvD@hOMhRB$NtLCxI#iqPEB z)9|^&-{}H0!ssa?PmVQ*t@Ftd{8rO6Le=oeP+7o~7<_q|MK9}z$K!Fo-@pC#+poOx z%AfxHnkanp~lgx9?6DKCBY_6*F<1A&CZ@>Qb(@#G6>wopHZa)9~;c#$GN1D;q zI@dhny|W*?N9k1(sU=Z<@#U9Zdg-N{^L#kkxtXa}&RK*XJ$iKK&Yi~(AHDR_OH;}x zVkp=g(u_UvmX5GCbaQ~@bywWjib&CIpmjSrA`&S?iIY^BA3uJ4d3jk#W~u4~i~0~I z(#<`k9u9|ZzWL_fy?eKA-N-rTnoBu0Npzoh$9pGd5&sb!_B226T3+e)DLXY04Fc)h z6a~|BJ-0(zbk+t-%pDk77Ge^en82VxpgbhyNnTZH3R!pJ5X{!k`P z3wDpwm7&<(y-8y^b+@cDQsF0mhV$HMBA=!fIz0r-%*!s4UcV^bFDH zjLulh2@}POuWZ?=wPo{)n1w+ji*Onlig(LW95MK(W88@g88rG)`}V8x9?d^eJG5o2 z-@Qm9({NtMJ=sb^an(BBPg95jvD$Fr6kp$Zf?{{(Hl)po!qE^5xSPl$S~NibD1waw zw4$K6KU{9eK0d6l;Xt$$cSTG(W`VYag_Qgd0*S;IMTB`x7o}dm`anhl0Q8FOU(MN> zqwp!XVj`0Mz*`Re(GZP8vppdnM*tQ8$_H-Naf+sQq7@*LK|~!5PM#db21pV*9M>*d zwByK$e#jIB^D#(>h*TCK5=Chtcj<@KwF--_659X^p)Mk}Gb0hjU27KvHia17Puembmu_`T)N23d5GK*+J>l%z3#y2BQ^wJqY=?`Q}Db+?BN)-g2l%I7( zWLC9~6V?d3hADLzx!>=xf0uqZM-95q^ji3#n+x%~z@@#b0aH6)&fevIRAaFPi*h0w z-NL`tslVo&nI-2X3)spJm1q~L%!aw(HHxG#_n%o{4JP&^db~L^vk=Yme06oy>6Mw-`1MIQLNf*hwnxzN%$Yf-G%;txtnOSq%xB_|dyBAi7VRz+mkA9|FhdYhfI87K zbC=#H+pkl5$0z!gDu=^izn@-v?alxBfBIkk-M_!_>tFxl$?%*Y=ikhf^(sq-ptCYFd_EC2#XfQVv@x&pCWd0y3sOS(LWP0m}A zsMgxh3?kJo)fduIokqzO?khjJ?xU;;{(wUG!cBq$Z%wj>x;QJvmdVg8t&SSf*+SLE z8cY|wOdnmoeEI$N-~a7zfBXE!(_t9%rW?mJkn|ho&>c6;mAtqT_}yxb3?t<@DJdi}j>9lKc<|t>ufDo<>)meC@ArG6z}((1e0c)j zC>ik?Pjvt;WzcAHDIZHvevMdZ-OnIIq;?U&!wT{SSG{|5f%KO^14YvavL~a+nrW`S5f%>CQ(7IpCCRWtcVJ%JShQ@ms z15Q(j=m!Wk*}(Le;JXlSvirndK<1~RTrV?)Y+U6U$O7FG1DYq*aOzbZpQ}dYmEBR+ z$}x+w4Q<}naqD|_Q+U%!`kzgXjH5olxbY|C^yIr#jl5H>iZcrpM+TYsuU)~^^h=%+ zw0EO&o)UyK%$;o_Dj%6U3qh;NM%Fo9=T3Fi6<0=~w$UbuME z=WW)gr6F{_CBj?_sk8XBgQ`z3m+{6%j_A8Ge4c33RdExDM(Gm+2nIWF{h-k+f$c#J zgWf1GBfc=2&_oH-ATiS(thBIiraZZB;yot#&>*9K75@^P!Zf&tfRWk(71bvt4xbnrbU!xQZT@ z5i4?3o|KR4oA!c&z+f>~8Z=te| zxGcieDH-I{7&3}jWmMu4%>X(x6D{}~9Q;peb&rijmg1o~b5lYp>$}@z;MH zn3;rWIq6J9h9PZ_j)pOHU3cN)#hbTo6Y=@({QT^6Oess*s=chAPdQEM7^m~*a!x7d zBv~@0^YioLr^l~ez1VL0%a<6+DU0MxDa&p@N~WCTjW^!B`~KbScAHaD zRy}vj9>PDyBXeDfCLQRlw)`~Als|K`Q>7hM;(eaG{5=~*bHG^YH{JMX;r z-g}G3SE73aKN_Tn2fOqk6}%WJw90VaOO0d z^0me#OL$GU8%sD~GMLc_&dgLJ3&so$*Vr&~-PhNYhr{=VLULM)Y#L6z4m5Ka)=V0u zO~qt-u9zMlGIVqV`oU;)O<+^8`(!iLKdVBl^XptvVf*`xdJSE!3csrQCZ+J98)Gn z&k^uujcEAdI-mL+pLiE%(o-$U1CsKT7-cpDc>Omf>E(EU08CKQU^{YvRC1Ujzs;VE zr_duzfSb#OMO8em(QJsMS_z4$?0o0x#K2i?GHt(Pe)8NCJ0M?QDP}hoWJKvzs|8o3 zO9y0nP({VRRqkf0tiG9+6zy(}5>L6|v|<@qD7_}O!0JIKM8xy*k;VK4+l7W)1lE_8 z3XmNn7U&VG*M`lMlmbg+#5eb#jWenSB4hGWua!0_xeoamBneanmlN{5^sn?z^(!tN zp%`$CjPSdAuA4&BWYD~MBG2GdhE^6V3x)E@SS#ksb&S|38#WWMZaz$!nLcJ_%2tQ9 zI?t@1XxD4PjX83VdQl`=Xwx+C6(&GA;)29E#3mxkh#Db9qf^-^)5@$M<8y>j`wvF+ zL^K6&vDBJWWJq8WfDnRy6weS3Ey*;rN+e1EIc1w93+LA%II3By(uik?3>F$x-^!i? zecx}lTY{~4abISwMT4_>-ll-2IN(SmC4gH(u2@l+*ZG!QUDw5K$~cZ=PJ$F_y&-am zcjuf_vh8UpN7WlYO4cX@*Zn~3SxMbFCy2Hq<_!I34C@P7tst^qRJAh3m?$&zen>A~ zoIH8*?DXu6I9#}Rd3$ukF$fWd>041DK07~u_Uze<7cX|ZUCud6HC+T|!sUOp%CcX_ zj5h7}!#GY6$C5zzz$KDgc5~S8Y4+_>kL-vJHC4ti32fH<6pN@ zefjCh>B|`VaU4RR5JTAqQ$)IMQacV30!bPv({|gBdH?apzk2oR)yc`p4}bWF*WSFo ziJfE~$08Ek6SKN8YJvEgWKwsyed8ytM~)5)U4zM`mzRnmz$a+d{@RP(K-65$liD`v zFp|v@eB*)F+6pO8hW_XC2Wy!qjWAAWi7tLM+3?#}lyZad;R$|)ie zN{ny*%gkj*{LK9P`SaiW<~M)%$A9?!?|;ARy4`M9`f(m8WEtBzb!skgTd&mZVmlE+ zxPANfwQJYzefgD6Ws49qF=Zk;dHM4A_;?)0qoX4w!*~SsSxwm2^wwXq#NtM7ZEX}@r)6%Tnq2WrwF!ms7M zS-xR>tpxLG#pEXi#;z(>f%JRiP2r{@!|a-eQ6zKDQkqzoOS|Bfw!D}m%2As-BpyAX zw*rDLyFdq`39MR;b|%VbA;zVvO<-ImUaWLZhY`=;pecjc?K4Dr;*Bi8<%_x_l!ftc zIEV^x-Vt7=2j7IkV~amM@2J}cJVUcCsuc{&k@_+Os9aP!0^Sl;rK#;mm0{C7X{OZ6 z$0boulm^5QxiFM?sGDz*7$Y^$T7Z)c5Ff*uhp34UyC&6pgJ!Y*p$D^>%+o+Ms$QMe zXlq`^3ADzC)!u@rU4}fb1_nv0@o5^)n!T(+bRZ)pXrjHwi?JGU2gG|LYO=3SjN3fG z#Tw1%1fw2Vn$ttihy#*~uo ziYxV=0E$mjz6zsimk}$^t4$R^M5zQ-$8jt%VpxtaF%{-b19Vmc7ozeU(k&rZTwSZ9 z53Q?=Xi7x2InfeA$a&232mT_9V)*qjG6!b~ER?}H6Rb83u+b`t*nn2+j|l8rr5TOS zLFs!zyAm@^B(dv5*M%-(IYsGGO68Ot{Q6LX;LZw22}ZOPUS0En2~6w(nmV9N0AS7;4H;yUBFy>KM zx)^TWeCN$K-^jwJXXj_WkDguz)Y0WSdJ?vp%7R^IMA3=AA4qg@#59-@snW~ zH=CnvzwKfOG3ueNk%eh$35}1g<+SK7Dshv;rdwRBqW+v8-^$dWPMs3(<9wxDUx#II z_(GdeMw))sGj$-r0jop5Nfii|Fn@2PfIofuiUyv3D`=Cy5MiQC-wVtZ0`!1JCll(Uo_s#2z$L@0<*vDY`>c=PtH+x@1`Ig<#}60(1-m}=LFo~u$k zyg~@mB1HgsL599~+jZU7U*G%s-q*vp?_=z`j#4IO=1`pW)hk!-zW3f6SFcLW#fvxl zNxr(~r725eV44kP#lVjcgqg~&Mqa*!YhBC~LfCA!uU@_U`7eI)+2@~0&RrM#*p+p- zQxc9c3AoFWi0I8X-+1rtoeLK(5X&$O%+pLMvZ$gVn6O1^z2NYt)V-@oU`-f_tDjDL zjsK}t1>MWCjV9OeN^JbpePeH9>RX8Ddg1bSI>VDDX|3^#jRDt#Ri!}hXPl>cmoCm^ zaMPf4DF zwSFwVGN_t{*zilvt9Sw!JZ+Oa1Ce)A+QkYX=x6%}YFxvuCc6Q`!Jp-yMWY4Fsz)`H zcwo!b?1~`2#xxH19Kk(!zO$_+S$y(~&TAmK#lTKuNS-Trx?pCXpcqfuRr)(UYmPC0RFbsh>F3;$v-Yy? zV0D$^8b?7Wzz`ly3DqEk5QwRc<{K7F?d7SN?NH277C9wJS$K8Und|e-%!Mfx!1A+v zs5*;aNpF-QX?fYYtpZ;}2v+DL>}3_C?wG8BP901Y zW!FG+v$FxURIDMqsAJ`o?&MIQD_0C*S`r&05ov^1>hZXeU81*T`AFaQ*xP6DemQ62 z9J>(9dQe)T$tA%!4$I~V^8wDP67x?YqWL(lq;DBUTN(k+^ClH)i5QZm*=o#67qw75 z42_&O&`N7nX@`kw4GuHSlP#Z}?T(*5d;09fD7oJpUATC$-}KB(lw%BKD}a>pt5>fc zJ$iIS6r zjq`ek6R!;WIP<)DhoR4b9AjXnhYuf| zygGaK?AcF$`e$!lzj5)()hMxuM>q-zi9M76)e8|k@X#n<#WqeU@S*`Ywj=M+LWYIL3(NNXQyIgawJTJdZ^VXRk z+`W7E&g}=^e)D{HD#D3)97m?GVd*$9Q-~>~5IIRX9;@S+WWP^L^77U5FTVKV$3On@ z+ZW&Mx=v3sJB)Kvk+AcspU>AEsQyeFL1<|5=;-LqojcdAUAzC_oBfcsTahd*DTF9_ zl*~__JbC>1@$K8UV~jkGIb}}_)tk+ERM(8Z$&OJ@pGDg(C^;}o`KzT3!!R|v`q>og zG?y?72jVP!2xF3`PoIAI<-O-mUtGCzC2&WuV}>f8%w27+`*2N0i~?fBmEs(GbNG#6+znyc@w9@jmCw&~e0o@;rxWkCx(qOX{pr zH6WGhtvW;FmvViKOj8dte4xj$gqD~1tR+T4)9T6=3Pxi?y|*R;Mb@-wEv0#46o%_Y zsww@2NNsUyUXWD8$C#?VGV~z*Mt|0HUNzQqh*6nCy^c)NH~BR*uXzMV}TwfH``b_hhKJPk$ z+nvm4vWO5h)QoGVZ3;(xn(LCvA+G*9>|qLBSfkHXZdKXP#F}oT$u**3o@gQ7O<}zr zB44$*zLUOMcL+=Ch91BaYs&G=dM!2SrM@OT@deIfx+P1lAT>;tZ^a1oie~>{G_#mz zAdAD4e#KX0SmcPEXT!V2WCkJ3`*j|W2x_45KSrH2qtuHcy$FzO z0nVCCt1HnfB*wt*%^V5SSC}`$^2|-_jHJqiX>{J&po9#f*-p=AU+pL0Gi>;~tgj4J z;7sM~qq^p&e^tt#B9C(nlzEYbvY?otpi4#*NjDH_R8iG6yU8k4!l~uT)fx_FCaT6k zGDiu+e2j|b)hJ|G)|a&REoBJTpjM!~JgMc0#xZpX%Txj_RG~q6ixz06!yNWCc1O?< zmw<1v;+zOe9>-C4c!@ER1l-j@mW#%5oVS8q^i8SN{aZEiSh!P^^1KzVv0nvKBiiiR z2xBJdJtjSL&4s~@=N6ENBr``Udm5bYcVkY0C(P%wA_DU?Ce1nN?zdh9}56U(+YI+1;3KGRz|A zzx=De%9Nx`6_#yhW>~nKqd2j_nM5d%gv*z&zIpAf{eC!p`fNyvnalBoS#s<;qSCBp z&MEd$gktEzd;(>RQ6xQn^k}y`zjEd3)i>TCrX(U1ID{-TOhBPVsGRu8 zOdKk3(L)(-^+^>asay2Lp0h9!QwSWHSa;hL5Qg3`TNO6#4ZDB>rbMxMXGB($8r^Q6 z1utJdxmF)1VSFuy7<8HeR@gO(nbMb4**vKfR?5uNGRlg>tgqS!44q?V72vG=8WcL3;GrpDcQK|wgA3!Y?kzyAUdkbBY z#LaUe&Xfuc#M38WJb&2 z8~LJIBKjqDhL2;K={*RhYJq`z&0xSb0-B74S|Y(^f69m&3yRg)7d}_SLMU54O)a;U zUpju}p}rEy`nR`gyh22Xvry)F69o~Orpbx2NYI6)MDu}9ndV{cTnGWdr9*5t26df$D`l@# zh`$FPGxFRiLSsQeqxPczL=#gd(;QB!rPJ&&rlQn_X-#%IBk@aI$LcwQMe$foO3Giz*6*dyckcP22MV-jI%{@N|JLqwu>~P zJrGC?e zz>^c;rTh>=44oFP=aNlhOkoi!D?v*{SVMsrACsIVXPUXg)epwW>l}cHjFN>Z#7KdK zY0N1}E+`8u-Q8?9ecv;)P?{P+7F{A;0>tP;m^o7rVhSu}?RE^2f^c~D?B(O*XS>~y zvTTozHrs9AZI~#A7(*0c5!&r`FJ8QO^7MF2|z0JzaMtHy|y9D6l2IaX}j-hR9(Wh$y}t=~0W@_?BwgR7BsboA_h&!*-!2>-{qmPTKYskM>$fChiI@{f z7kRtwi$e@?!^D{aQ4*0zdCZa}{m)m zdF!*7cBp#QDoU5Y1Xz%m=Z~<||K7fH^S$@pd-CwXZhyY%;(kn_3l#dI0x`x7(ZD1! zL=IgakA%a19LF(@>CvM{pMU=O2Ooa)){Pro*XbNop7v`Fec${2m$7ZoS558&x~hfC zg^{@nzS(rwuV24)>(;${_jbEojNEUJ_G#oi_2JIWPmUiyIXOGKdiDB{#t=H@q#$@o zP#Ln_f}nvjx*GyrQl?b>yebx3lN>mfvC2YBO6la}7|3mgtSTX|p0C zWXnfl<{LZAHirrNg5*V*W?95 z$g(6P!0d5TtBwCr^H~GXP-RfX=Ux`(9+k&8C{4$RR5fMoQyT6(sUA#25)zF1;|n(| zWcN7*xfysBG-{yKi())_p6n*S1{tnb@~-I{t)EFH%V47+-=-dt0~0xF%B5Tgl#*vC z(FFLNhqoG3VD%)lt_Gf3pZ|uX2jDA zy(GhduCe;26&rU!PSda!GeMQz%4N;y-C(Zw97NmngL5pzYj0DpfD!9tjF&JEeeGDI z$EVrQ_%=maEe@iJ;=rE=G~A>wkG)pER?r91Bb^wTYT{$mehoM8$FR&S8GnKVS9>o_ z3o;z7y)tFug^jO*pg3^Lz{MTii9XF(fp^8;mtxfi;)&P_G|fTZ5~v){m#0B5Q(bsn zKLLTF`pa=5(vX=t(ikI6rDYv&X887koNY>Lohj9pXhf!LDkbR6jOJ*2WH?29e4Jh2 zkQl7mDKlxPgzNQHm_Pu5X->{rnf}C?KdG%F!$E4y%L11w=0H&kDe+3w{G1cguk4$h z6BJdhW5gm-4*QBR_I;n{Q^gP#BTd7RrkN{h>p6DR#SM-OJxH?_KJ~hzWj4cNYTWFl zbkTKQK@rgCZF#=m?@6S5m0z}W;ClHj%^iMaN0;j&*<5>Ud|PTiIy&NcCGVI9MUr7Y z9tAWlx+mM|65(MOce~wwH|Ct@b-hI*(~cIUz;PV+`@QaEfu00AIVx7>g(R&JrV6mT zRo)gcQPCN+Q}a{cLbqU8YhDT|hPDen*cGL{^938zxVa$ltqMzBzDZ98^%0{6r&1(SVH7Lk%$6i$(hDPU)}rS`Kyzc zFJJ!n$N%x_)i;S^*L8Th5y50sMVka77Mhwme8qJ)-B_tX+&EFa7H_F84UyrJLg(!8$LmS!Afv1fH9#amswcI|@?KKSgj zPxg;?Ap{W~hD4lW-j&R{JB}kCu}JP>9EO3JhheyX z|Nimu@tbenh%v5xZPb*=n(si-tUd7}AW#4mrApRNAtEncyf`~Mi*Y*QRo%Vz8%l4e zEIk#G=g*&h^2sOPeDlqfH!kmJ90yKbvZf+m zrqM>9(#uHy8nlfESR>v%Hz|!CDs4^hjglClo3~B&eU18%>(y0U4;QiH?xk(VjdmfvX%PkWy3Otp(;_%lP z7q@|czBxcAf$r&L-Wo0hnhA!BXj#mwIuPKSccV6buA-@*MxSNw@RaOb*J-fBn36%b zSs*?{v(@L=AY!iUzMPPf0Q~<%OqjYEMlG>AFVk|L%K}j+K(2#OBBGzDhxsc;p(@B| z^vC!^MRhD&;c>&hFr(KBOpk(4H2nN!P21v!5%Urfot7rlJ1P)-3Wlg6J_@C`>zTa3 z9I+=pX%|P4X{o8yJFyxbJ)Ro5BcihQ9hYmE=|fZ@EmMQ_NGaa9h3;3U#H|@786L#^9BoO5dzJ<7rL)n z-&%D7f&+YSr1gH60In%b>nb;$@h^q~Ut$qq7l>K5+YA5k$N%~Io7euY|NFmx{>g98 zcPEmiObcwco388jLkfXH2xT57b{$jbC=4lYHgP}5@#Fh{|MQd6)6<{)?7ygQ70 z{fL`&w!B77>IRJ2Kd}Q>)qu5ECNI8SQx~88@Wrmjquv3lXaY`78&GP8Y4D18fT=ZD zJYh|Vpx!qb5s}FDQ$D3fM0}9<(xpoueDJ~BZ@>Ha@#7)o5J@8CMEj6?k-%LHkwPA0 z>_V0pBc~L?QNP)TbiPZE9zFW<%P&9r=%Wjlj*5BfQvW=idLKQVXvzNJ0o+U>Ip<-> zF~;}bfB(jf8xQZ_&zW-0u?vAh&Lt!*$H&JH@85s#z4v2`CAiG>Y9>#K(E%l+sY{ja zwRE|uLQ`oKkCj%uc=6)={JiTzANsPVjPc{7#M86|#JAsm``h3C_PzJ-Zns_E_vgET zfFPsQ-dz@Yn5R#H6dwb7^|Q4d>VeC^emF5q3D`k`kPur(?-x<@nDTN4m?NF!^!jJ{zmeCYKMHQ8WQ@yHF(Oo1|AnoMI8+CkJ$kc`_m zwQHHV+I!d1ZvqV_uR&WW^obcd)?q%)7LQm((R%gX zETJ+ule4>jlagRvUML6}g21ps9L-q^Y)1cmdX%-N>Hs+SO6f<%_Kw9zJ=Uz!*4?W{ZwUdh#iX@N~+p%q<4j*MU8X#l)z$at!Pq0Zo(LO5`Rn<;re(ujk0rGFPr ze$gJ0BqUO+o8LB8j3UTQhv2I=1S7M?-i;JYu>owYsVcr+PZG18V8)at@x)-W1HFFL zu5YQ)AneV@=$n6t7OHYI^XR(R5H(7Bj~G)ZQH=RCcDt(&*Igs+EFn-DA;KMd1$sMc ztm)-3uU?hyD^fOxP!TAV*Q=C8huEQ>BP58;P21GAriDsG!t0Nz2nmJ~8Y+YmSArEE zbs*jxN-iIN#Pnr6g<-FfzUw+Yl?(L^kB35C)n&X`mv8-4p$Wz@ zbJ@&L3lAa4yu3zx@*31rxbl3-ypSd5T+X4=5j_&DNakEcCz0$7E8kJF7#~p46p~8d zb+g$pbBWIyh*V_Ah(0ep|FU>CgfI-lI3~3n4Z>zPDMXvyJa4RAmLB(gzu)iC6)5}2 zvTD0BVkVH3(}~piJRXfBYho>j=MNV3Fg^53H045-qhT2KyE$e$?*##8@lAx40B%*9 z8^odaD7JIqFCRhQp}5D50xZ~7vw(YO(<&ZkGju#3nrX~b0<;Tj}vl&NS` zd*PvzYPu(yV+|5Ga~Ou*cDwohhrict`b~fI>)-s#$;;59 z)S4c!nv;POMJox__TRh|LI0{LNHfesCBwhQt)ma}#bRQ8iv=Q`CP9-_YUz+6)ub_g z+>~530Of`(VFc5#_EL_0<=U>xqeHK#aJ4Qy(=rRiaO*b(=bj=Sf zu5%V+V2;BuJb(WD{QPv9gzUq(AI|q@g{Hnx3w^=TL>(U=fA-mDKl$l@zH#GPU=9>z znikg)l<>Tp*}{eIw$4<&HjdM<(@>_xS#mbV&Qi-d42&w5sqqU$I(PEX03N>98QN#SvGt^B>#@yw8maG(?nzNMNR0yPN-c5UhwMnagY@Cmm!i*ag zr(KFKdnbuV;cP}O6V0Q)nqZO8K})eL!QB^sWV<)uf4oB;*^Yp^G=)9fj^MF z4GuX)I>30-&D34OJ+=K~=L@TDM~3SA8786yh25Hi#vo`zi%{ zc1#J(Rm9VnLz6X6s;{N4g6F+Hq2~aJu`ZeVA&asF*i2vQwth_k1ZGk(5`=bC0vY9- zKmATw=59%-*FDheC&}&qgXm#+NUe|Oj+p_+M76LeR1#*EuyjWylpSY@m?d!LX?1ND z5y_mV?3qMJq+U))D;`nSSGh|EF65=PV^^CA+Vd2KHKVS^`x{ zTb{NKV1ih!h=ee$FTqs~%$)N`i*;nvAD#(JTDYzyQru7?YD%hp)2?DgV_ObEov`NO zx%ZXQblet!+S6<)g<9SBfSDyz&eL|0YTe^Frc`a+TclP(kj8w9&C)#nJwHFsA|b|p z*5|^Y#<-OcB=6t-kpDbfxNza0|K*=fUOtVn&q>bC zb{8&ebDqxFAtK@&I8X@8B+O$Z$z$wjw?F;YfBwhwlhZ%@*`L36=Yy`_%CuF-VtWL@ z9R?IZGzxkErUMB~nZ91*$D3{)D>_V!e`}QZKpWt5nRaol>E%{;Q=czVs7uzj2PDF- z6WL_nKUecR+FpGB?)Tn#=i_@{e7+k-Ng`d`_MNDFnLDNsDU3of_L)Kmn|+j?_hWkS z;M=di{`%cpH@mJIhG{qJrvEa~8Lp8|gRON^t^v4w`SOh$H#VEiG$SM-rG3|R!ZeQK z+1c5n2M=F7eRk!_l`uEJYIjOT6F2i*Yi_zWW=^uWGcPP73+6crDODlNT=u{%yFi3U zUDrvPP841CcA0A75W*;9iQ#_z_1C}q-MzQoy4J-_2>a6T7s3zNj$X6hUlJfbmB?B~ zh7xC9rhkkw8-3Pdy&WNp0^RFSZy~RFbu!aXQiE^NMpx#u8x|UD*B3(%1JAQs^C7&V zlRg*XZ%Xq9&TboDX8VeGKDKg^p&Ajt1#NzOx)zucx(heyFFXi z%$8by9y8+JIG8547MXeq%g=D|c;&-DVVP@|ju}WvS?to5!bH8V?uo}2)&aU27Df~{4T zEeg3_5!Q@xNKFIUDO$~8Z(u~DEr_aj`I6fB2czu*h0A=JqB2E9I-tY29@bcRPxbi| z26K#DW57|M1teb8PmEaUI)Jk0vbPH(H6vev%f#Fh$AEQ0EehlzL|a#UGnyM2sQy$4REycmCzN=B5hVsA8>6y2R4DM@;$xOXO^B(Oh zWp-=yc~Yj#OhXz=jCI(jt`8KuA*I`QKDco4;>F9C|Nig(pXbk?c3mV22E`=l1(ouU-u-Ig?0E#T#-7BLrd+ zNyNmmSfJ<4Gi6jkbE){3w~!A_*Mx*n-fIA2T~$%7@yP)x7`}`hHglN--VD}KHlO<@ zWL`J7tQ9R&Hq2#L3#XdIf$fTE!&H(inIbQ{m=gtNxq0*Ety{Ogx%c(y=_?7c?Kk6? z15u8gb1uzK$23ODsgTOs?Kbo|ot!;?{`}X!{-59b@cZw)^G=vg3n^EbcJnM7kl73EPV0Y?Fb^~ z#SjDMAwPWh@K?Y3)khzF^u`-+#L#E>@It z!!@6(Y2g^bIRJiBhz5T{u$I|mP&=l^=Nn;p(U^W`(*O+t`W!h8uyj<3;d#KR<*x;# z5RElsTtm#DM=c8qu;SGe(v-VL4@>qXXm*3!nzzaD@d|?9)K4@?1Js(DnFJ~YqyY)7E>O@u`z$GVpTw<1QK-oTSk8dC35>{X=$qD%v?rm85a2#NHY_~5O^X<#~oas zEn!AFB2%T~I7&(wVn#zV&Ov*{I^H*F=JI7fj=Hoq!rESxY0i1S-|u!iTtADjP|IT% zRzYc}J%SNT+;pWxZ}m73aogZFZ>VLeC>Qg*2+G(6e)K^9_6tkvOnpR^-vK%%5<>{X zFep-KlyMkh$3$}D#?AlwU;fwacKh?c`@7?(4~g>mKJ`&L7Ur1dqrF3jrJVi#yz4d> zHvN#(w(s`)@xixW|BwIkZ+`k`fBpx5@CV!NQ3#SV6D8~+p-(i-G%-ciY#Im0BrUQf z8(lFt%6ezoKWuv51D8=9oZ;cRL(?qNCqRv?O;-d0F`jYrS7%0>+HBmG@uW=#m#LCK z+Mg%}cH_p4JGbwCeC6ZQ(^qG^;pk|H9YyJ~u*md@eF&lBKp`X=BuVTzr$Cg?PhWlU z#TQ?E@!7R&*UB1L-Lyq%^8)f#x&|gPNV998*^5OcB8oBIym|Ajx88d2;6d?g9dlv1 zi!rD4?Afy?PoAXx5c*{jFNk&DMsNnw*>l-JP80dVE=fzv?n9xHa<|)^o}Lz0z|372 zLca;|^3!K8#(B!O^pu$?gxL3eS+4u))vM1w`|SAm_|m0IUDv0SD2zDcU8{VK~C=kn~13nbOFqH!-T^ z*fb4~a1$|2hFwGars=gg_v4NL03ZNKL_t(j5wESnhydTNWoPCRJ2rh>1+CoY`9x!6 z+5A-(p{o**Dy2kgL8qj`MQuTPcy^Lp&#qIwXz{hOKur9MokvevE~Q+-Z5Bb+WmV*d7R`j$4EKTYr+jFP)*fG^IeJ-Q_<`sfq07nX?8g5m~mzqR=N3AxzGlW^a z`5&m!vpl)(*`;^{yifLy+DHl?`WmT*+7pi^kGvdU3SSw8B-ahok(DV~Y8Q=#qvjxP zUZa{suBoUo0kx+2`I?qCR;1>fMXF7=$;rhVmp;t(r>O?OHAdEwDN1JGYN~Fc?G4am z@ik(WQkwT{nv&N$2KqeDiy5h9O@_8LzD(jZ9cc1vGBzz`Db~ri?x|)X!l>%ZyvQ6v zMQ}W|6$>?Vo_L=NFJ>-}5v!)jXiA1IO_MOS7u7}+TQPanC^nmowhJ0T@ZLtXVjM?* zERspAS>$W^|Bmu3yX<3*6uM$`+_-m}Q;D(y7wF?+~&P(AXdKFu}7byW+*#6kymhcIyBI}N=Z}7 z40G)&mpm`$R2|6kWq2fK7pO?MU2J(i!onS0wPvgmg%#@U)HEXoSf?zt0-JVc2{P1d z$~@VbQlgy84!>o`gJlZ{vBKqf7YhvyYq$@UpGfxm{n>69#w<(}I)Xhk(27d{*X%6p z(Lk9Z`zkWcUf$x62PY3`8X7Dy6Epwh%~mbl_yk2;aV~V}jDqpKKCiuRHLQ!D(K|yf zY97iy!MRV&kvWFYN#-PRH;flAU;R%%`RPx7`qOLI-x2Bp_i2(X6;)(SA(RYtzUI~1WeTU-s*RM_&;TSw|IKf*{u4^&$WCSs zv?M_s>e4FJ(i?TzWz;c_BZYYPgZHn!^;Swk#5t2>N=Z^mLbKhnl%ot|7Yc+lU^}Yp80SFFt@(_4ud=|Ds0=*!jSNZV3iFbO@@X5-jWNE(N|AV^M9B`YI1!Bq z-I211MKwFwg}S1&h^L3HTzYf>nk~b+Eu|I#SK5}T8aT`p2~)Vp`Cj8lHD##+`-tj5 z2s&zB|FsZ9Gp0c<^tl47#26wqJ*qT;d&%=!6-|$Ip(ycrZCQ;z1QNz|Pl9TbZ~8Q@ zM}tU%Nr}S30YK9VP(4srwPaKPm8NQr4h+hLD<+J{j1}lZo;#+>wHZ7hYrX@g<vdS73el>=yVmaU-xwbXK~TT+{Fk+MWwTk1qkkd0cxB0>6YsAza4od)5$S z(~h8t1^AS>Sq@gg&JXBiEY6pvB_LA-!s3HV$x6>H1PW6Ra_U=_u?BM_3L=~{70DZ6 zaXsuevO6GRa&AO%!*)T-G`UdE6*uGay#nCnjS=k&YcH?Rfn#DXN1qYhoq3+;6?0NM z{F9uQ-22t(F}X9w%}8zAph>10C7|O4E0u z%Ja;WXc-N4U8nIN?LU_^qvTO?+1VCBrobEner&F_#Q7*Bqca-Lqq)iU24dtpJC@~y zG;&snn1^}J5-}&&qNx<l% zZgl2NPf|sx4|Qy!#j5R;`gz_=!02ijHLG3ZJo+(?vAc4Rmu?HIvg3IOpdygE-8bV4 z0gs32v3AX_gA1ceR9~tvlTVn{rfeEZ(7pti{6@*5s)u@DsL<)UT3vfPo%jcFOw5P| zh%A(=tV=EB>j7(+EWqHa>$=TmqhWM}8WYnpVx`?~rzxjcqFZXV-|x?M=M*Z#@zz1m z1SlXXDK!=}WYvEiu#{J-!)n(I4U@SPNxdQ0I%thE_Z>s$#K5JwypKW}{Htji2kB7l zA#nE#zR+y5Xn!%}oC3x3^WCM3Z~T`(``Ko*`P;w!+xz!_#}x8@OmXb{J~9n5h{$%k zncyZC;hb|UpU)>^ieKIP;_U3~_&+@Uqd)rNYj3_4J7$Xe{l4oqL@aaIYubLh)j3mY zn=&x^+RUiB;%(9=_M%nknpftbS4UsI3V1Eqcf6!-ch@deO06B043lBv=^@lLJ(NlW z7`Q>dtU}ae>Osm!`XPkt*Kd6P```ca%P(I(e=;OVLP7GFLzjxjFSa3Bq>n-Rn0G^< z97smV!|v>x-`#uo@ZsglSE1hNFeQVD>O6yMrq_AuHO%|2D}LnCrAybZU*BH1kkcR{ znI$qD6fh1iUp#yD>eY=)H~eT;pdrDBrmm(YlXC%{NR)*>F>(FI8_%Q3>MY_pNRzf?w9DNnmE<(!X?kH7fhiw}SB(L3+lzi#E(wO0_b^mbsr{072Jbo_kmMU9d%(x$Y$pSVaA|!4BGEsAJ0BFxT|iP@;8t zc8o45q}V6chjkB3e7y!1FT_R9R~ATmRWw%VbCI)Ih6pb@YDC8PWG`z|vIo|1hfWeh zkcubGdWg`Ok)f8H;amcA4%AXbEYMhi=enfC2zbe*&H?k9lAGWoo>;pV+}ac6nw+X5 z0meY+br! z{9PlyY1Bp#JhvK6^`P1_tFJJ%8#cu`;V{RkaR{d+piy}G55V%;1P7o{+M4N=O=SN- zEvoRS2Hw|EOFeu7kG~Ft*6axH(U6gMZ4qtlB$ds&_m`vW=5b|LoB? zfDf(9-QO$JYbxD*g|aII(MdKrqFSJG@a&qoxv5$@soPv~Gndlp0}vRRHvwNMLyKPu z^r2fLIL)H9n}*V~*Xn96FKZU zy)thGvREQ>n0F>LQf`h5%FnXBDMK{8WGpL-^Srr-QB*~^gbw?@5A*5)s*Ovhpy;5{ z&E{6^`Hrk9IHg8?ZU)E(L&Vw0#Wa*c%a{8kjc!` z1QK*HWv-pG4qi+wTlLuhl5#%V?S?dF5($9|rbD%$|j+i$&n^VaRI z>&j<$<*(|nxlsAB`7vIJ-DMvJE}K1VkG988o_zV`mnW}Y2Iek?uJ1}IStzBny>Q{q zd$(`A{q|ydB9f<$MH4+g=Ul2Z;?{#Di6~3uHB|0lTD%iX;RI?N0(V{4_ZuSm`rf@? z{NfjnA3fM^x0}B2`gr;B`8;p44D% z6rYAuwfV#9d?(UyVL<A|7bN!I9$*CG#;XL+pbO;OeD-yZu~!4+4|NqJ3m3_Lg`jGc9xRrycJU(2QLUD35ijg z<=Pf}n{G0ix|)To$)zsgP^5{RE=j8^!YdQEP24nP6Og9r@U(+6(Hy;9xTaKYiAb1) zDU)QH_sMmmp{aQ-T0_=jD~U{`{kJ7AEm1;|q|nXV^9w*n%p7FeNSB#&nNW8sf&NE@ z*aQ&aG6MuHYSL!A9C%@0qc*-70frv;_8dbTK>=1%TDsH)O zUqC{Y^Ni?ErB?)v#w&}VD6c$2V!5#v~5P7MN;VU2PhimwF z>6S<0KuiHSGJ43cIFx8v>?yf&o2WVZ(5&3VxfXpZyqIK&QHUt85S7o?O;lZKtq>Sp zM(_`1ONuEMO%dC!>tg67Q%WPY3yb!03}IRMK8)$b%hPAiUhek$5M$qO`hMH>eb;r^ zNS~aXJbd`@`02Ce9=N8_QO%Drg!w};5gEsE7>02iMJD~(Y&K;_h@6w+pVG3EZ;aJO zF;aJRjI%9a}r@kHmpQVY?;t-idWWV2UHk)_fefRDM-+S=jK^o4+oI{`_IV8$~ ziQ;U>nTbVsv+c^)Fhq%HJUMy!`4^vl@57Jo-n|=REOWLV8DpGlym)+?Hcv!)PEiQq z_U+r(u3dZd;K662s1Ihbt|OUJH0sM=R#Al3Me@ps&s<9QdSl~k;5qb6M)!EruAzq}k-?ZIsFJ8R3-EN2d$Sf(P-EJ4S#BgKD9=nis!~OgB zKmGL6AN=6MD{ov1A(S0_q~4YbsY0%shTy%bCaPK#Gqk9OB83v4%ghyN{bDI(v z3Ow1K;O2C(k(#O&cD0RHY-$(cpo_?i`D-~|sqkUbF?v0rm5bprq$)4XT)qU9EHdtG z6AQsX+5tKVR0s_UUnw}I%-6z&d5s7#0n%X17C}(eUGo^B{(#P;!Qoa@vW8}T+7S|}~z4<7<5QOP5$QWQj z!=k#xT=xeEb%`fx@v8{M1j{ju=bUv&P!ec*j};0U?b<}2(SAJ?Xa%Z-7^4)oZ#52r z4DxyY=)0^uW#(}l%lD~fKQ2>+%&UNXh_;}OPZZTjPj$TC753 z^g-7}>PPoF;wzhV7$|hesm@=8n{(dp_j(Eoj$9bFWW`)Az-_Ne4TuQZWqppD;3)_) z2YHJ{OSX$hkbcA=RHL~?;##JFzco59bHiG~*=12PK`#RJkf>VBB(My;YJ+HTn}LH4 zBCx=er_Bd2YP{fv`-l=NP$lIs3}a52spl@nUUzFzB2!A|=jUg;a}B<)*(NFjiZ8>4 z#rhDP9y&Kul(A*KWWAY?RwGD@$5?jfo9$GTH7uW7eYR%pcy-7*a%GTOc~!iB`J7Wk zDCgYuk%-REcYWXg;G^HWu-W!~|MABk@6J!UKqR!^?_!M1Wdxp?S%`^*IZ&X;w9f-k z=z5ZzKmF}*cDvo5{`6-bee?reU0D z12X$}T+K=i3)Ax~81i3KJP^4#7Mj}A7PW=9bFM^YNej$&Y^jjUVE8OcA5`Q;q|Mwq`FA@=~SL1oPf{5}5PcxEj}~ zmK*AcdL>d*nkJ-KqoRk+vu+YXv;@bo3U5<0Rn>Sh*WYuIJ@0X9MfKHu8VvKagGaU5 zd|QXJRR;8IzU}arF?Ril=>sgJ^(AO5lIk@h0-wVmfZAZAUX>a>n`&!R>j1*SZW$EU zOB!iq^&eK}tH5FrqVmKq7C_pNW>gptpaGoS0JT9%zUT!*OGX0~b1asj`-og1+Q^8f z%2%i8HPOJE!^&nTG?*BA4F38Y$Cgb@O!;1g5R-D_6%{qg<90x%CcTGC^}k#?(}QMi z{Fv08NK?3}-ROf7;2(NA*QA5yd04m}3a=O{N3TWH+Far=zrJ2?yokU7eKUpl8$cf=9l*D_-NvP~L>vt348iwQ#+EOH-`s>W#dZS${P>4}qetS*}3@UJWMmYq68(7GBk~JTC5pCti7EMbC@0$m9|=H)&qE_p^m!zZ2AH76NOUR z1w$~yQ3O;Yoa^(@UaQ9HXI}3^0uxc7Ktw@^s1mn~nP}O{vw&C1GEAvmxRUOH;IT4tF;=hX%YB+JJnlQ^JWmNQEp1%e1Nf_| zIt%d>AH0jOP@v`8{NgOiV%Yfu2xg|T3j)&YHAGwC5`rvej7#0QMVl4)G`@sH zbI!Urv_vpPB)BaRv~SsQVB*B&OE#j+wMCa2OT?UDK2palg@r65GiORdsc_%yfrv&q zgAJ^gqLfWk2DCmUpafE)jT}NsX_U;c%Dx~lrV(YN++mtLZ&DFs z%&C}eoD@xnM3^EIlPoqJLdcoMG3|$xvye1Lx^WAR9iSn%SRJ$g; zJg~B?Zl3Dp`j`at492Xa7P=|ih{@bwVx-4Vt8S`UzA09mJ2F>iGK-s~_m|;(DXZ&J zO5>0@P|h?ADaQEy?|;P1N85}4-#`EI{Osgt6C}s|evFB`J`;5@Mw-fDW@1h~F@>0t zj3m4B^G`qhbiW^S&L4jG(dElm&(F_`>Xe<8*RC~&(KE`ylm`%K<}x;BO3(m}!IoQ{ zumd$7Nbv8tJhg_%1*B(SQ&$9)8!u;HC1O3LMUIjZ@1gKckkZ1b?eFTqfB`e z3Bpm7U=AS!CQ8E?s0%@)+mAT}4nexEd-?L^=bwN2`@jGDAAE4P>pGE?b1t(2zH09p z3`DP08+Clz-dwqI<@$}cV~qR#J|_~97^b}}nRyt7M~@yofByXH)vM~$LkKA~4q^9n zu1SZPtC{E7Ul|uOlilcb$r4Cf8kH9>UYwtwPq{b~g%D}ebr+8=935>qXwPrzSA}_@ zXb2?2`~Ci#Z@&5Lv(G;I!S~AKA%;sr2*mVSoIU2qJFO2&^`~mPUpVkud!*T6Dbx)b zYKi!$M+kw)4$*^dLDd@47?3{>_OA}H5=4d>c%mzA?X0l zO=~I-t0hjY%cv90U3qLrOkMa8K~RV=fgg30ip-vd)H37)lsEbHMB*ziQ@n63k|CIC zDWInL(wa1@`ZW#BtGX!=U+D@^uYPNAiE9O@wm75qh76y6P4ol`@0Gk{Gy`T|FFD(3 z>Vwj-=HHB|dcJ%bEXP|Sf%bZnF9{8W#)iyu51Zb8+p|n%oJ%sf%j6eKStcXbUcOn+T{gmbz#~qsVD&5vtw> zw;11e5`2h^tm!#c^Pv3){Y&#AK7FFmE?!@>=-m{74twKe=pxQoGtHlAb8d9E zns$NSLq#KVTu%eI`WFLF1T^c?V@xa}xJ@F};|QcpiD@b)%^&lXagAb#Q#<-Z@2T!g zhWXM;b>&A{dyC5_mA0{*=G-`|-flXFE$JXUzAzt(cZpRp*7-q5!_Q1Rn#as3QJ!FIJr(sX6CZrG}Y#eX=HY3 z_zJTW%_&#;9(TXrkCJ1IvF~_3TTetZ^043U&(6;F`+bsWg))|Jq~l$a0;sYs?{OB& zFanqDIrtj=6RI_b>oy&$5O?{aniUxy@#;)!PYQwFv(s@V$~1|&L{vAMjff26AX!q{ zk7M}Y`#-pN@#1FN{p&yf^U2Gnfjgpc97mZZMJYszF;24w#3ExKI_8ja>bfwb^t<2P z`iIl77;+MVaQK&JBKDJ(HSfmIyPB3ehy-E+ZDcn= zD$rOO1T+Cr6&yL>a1_hNj2xT0ymmc_jw|Ueo*cH8y=qQ#pO(YMAoo zBfzhMV7ew$y9>n1yWD0grg;zGY4RG549*(Q(&-AM!F1e_eyq!wxtE}~(#u<1g4ud<^6^?=ca)CrI>ioUW=!pUuj#j6ij1Ao+cx$n1i`#S_;+XU}(UU?3xU5 z4FsfIp44Kh(xWI1s;FiJcIOH6$kfl5E=^MtJj;f!HEUXv9ZJMl4W>L`+CG7IJEmk` zqer+|X_Hg6pIlVc001BWNklR0fMCy@#5-HANO8-A#aqh%<9hsS=?J%#$5q z)KV8w@*EMvXe`5e5w5?8LIf(@=6e*M;$QlBb5WIPpnhuJ2?w!<5-ScReF&kfu$vM{ zF41x#Vvbyg{}5Z_VKDkBbDz1e&~=@jORw8UV0fD6EfI7QTG7w~%Ui1BwiQS7jb5cL z49F^*OZNF34vih7MIa+N=W!g%dYB2HAUsbWqnd^JYxBB{}mEho87EGN!j`(@p% z`_bQeagu9aC-HJDTe3z7_;O(~Y5K6beAWXd=BW zWkxRh6RnmLc>QQH6G9MKrWGQhIckf#RSu-nJM7Lq9TQix8|PoRx4Ldt;feHr^9*Q_ zyC8PoR}hyUx`?NT(~C?KQ#?OE+_-V`|M}@ZvCx11>X+wF9%hZ5WlFOU&2hmbgb;Gd z%Z@w>yI~AL_>j&I`_KRQ$v^+>{K=CiKls59uUxqj!x+Z#a5!)?es_UvS?ekuVb4<^ z>E)NfPSI*(U)&r;?4n~9=f4MMYrOE2vvDn)(o9etj#${W)>Q+PH(PTmJv}|Wef##cYu6q=yiY`PmLZefx)=r~;UN#YZo`~U(>TvLXP@8|kCqUS; zF!(3xc%?5;3@dh5P+Pp(oy%@qKVtpxY5R2zqT56u12}WQIHqeE%VW#fwIO1}LECtQ zq^=4!WuHR5Gts`NI6*R_M~%j{C@X4~u@}jRL~GU7x^T2=tzRn@Ej6q_ zu8)pl#V$=yh6&bdTX>8XZkl;<3&|}XTOn-a%^`*0>1w6pd)F+q zY;s!TfW)R;`~rDoeW91!K4>WP2QIoe0(Kum!BocSJBp+;sBunuoqoACoj6gCP11|zi>NzXMW|bV zsTRn#{dR*V!iMIk%MQnV>Fwy$$Gz{$48n$h)g?W_sn4m-IE(G$S|*lsjMDhzN0IsgQciEJfL^FN<#y>E+r*&H|B_A4B~1}7$<#2lTNP66csMS@{qLRCVL$`3CaRLdJP3xr24-k}sj zL){8Fu1(rL2`T%Cxfv@BcmvgZs^um1r6o`6WQU-PJ@}sMyQeUPC^wsbQFyGb0j!ZM z#CJKW56Nn!40gH^)dILkVJn7FPN~C}R2#;Xm1XfmTZTs1t_8#05-5ten?Zq>dT3_Y z7(Pvtl+8farWQLK4i?!oTrfmFO;fI~e9*@&Fi0V|tBwIvgi7lPoXR&$ZRpgCvy_r? z7%(GW&RKG`oDf17hC%5t&of?t#wFa_B4Toa^T!YZ<_c(K61cWn9)YN_<=@D83E=9T zTZ*P;`_<$aE-a(S&hs3@XkufcYhc7O8`bT%Uq5-g-|tgO!#ISn+`pxHWwhUv(tbak zpPwsuU~;PQsEa9ivttV?!F?q#M+9n4kpFiTn8y8fU%M6t1U^{2-t~(hIkWc7pP>J# z3~QvE>VaW>f)HoTaU#q~?-vBhnoU78l4PMUXMX0{Yya@$fBfqo{_QYc78$b)S>l{U zD5NCQl!9bRI-W5tDYP?FVw#5_Ec4e7zWk5>{6D|^<*t)J!y*_&8%qpob2~?&ErN z)lr2OYSeXe&XSoak_5>tnMnduWQvkGCvsl-`WI&oMGP`iW|k1jnF6T?QG8GX*9JKc zW0((nmVD#-x2`>ReHeDkahkJm%sFRDx_lKvkf0JO%rcC0GKL`v$rLDsnD2h^r;k4R zXr8lB(4}DJ{xY(IO6tkU5l+<<7GBm5g(N)t?6X&|US;Mf%@l(KjxmU2raZ)W_UP*e z_wFAK2OL&)ak6D)ph(#(ze!y?_b8YSCFu;I){8Jv9jlTc*+wZ5Nvc6H#{GV;V-FF< z$U2b67*m=!r!m9VpL3pb7LIz`oQMcb4IgUqyQ3; zE=hT6H?nFgw<`+*3sdC4QkfLT8w$V9Klg&5j*NB3u=v}2v{Z65Z+mIGcI?A0P*YH@oLaTAVFE1m@W=49vPy;f z_(p1zh22rjBQy>zJ6e(&7H-2hHsT27l-(4niXYVy<#0#4CnX}|pl#ccdQdPf+jgWj zPDG7#*&N%5Z&-)0PAFm=)zi>9W;=38XFuRNCDH$wV+e!#CssGFi-qjUTP=G@6mWI} zgQ>n*((vh@HV}s}BDE1_R45#aohq4NTU5eSBj+4SyDd@h zXD`UwDTBF-F~;SBu|UfKwm=-jZkH|p5pzM7M1-?&Ia6FfibO%ZET%xRv@mc`Z*$;) z0V#dy6dyT);?&v)R7up7P;ZmOn1X4v@7yE5Gd;0T&MZVyiZsta2SZ=l;x1}EVdy!*i6t;A0V327%>|ioUDej(^K~Y zF|%ECgaW1oa?To0)Xkgd663I|&EoA8D4$FAiD{NOQOYJ@Dk{o)6E>MUTW%FdT;wnt zqFagW{lZ_3L~$n0A<{605Gl(%r^B2MIT1-vak7EQ!s0$2TB5o-e58~#Y;9#!ZmTC{ zLr3*U+$*wh~hkN*oqnT|rg6Dvom&N|NUxatst%m}Q!# zCue8#lvC2o5+Q^z#uz9E(KvF@!FRu(<|&f|jI*KRy+l>OWm;%Okus+w!?25SG({lF zssTEDmsIuLtaqR_U@MUIQcs;wHfp+e=2k_Rvuzwijn;8QQs%lP7n5}F2SdPFX9TGH zuErAQlyoQRM9WQ6lJf2(T)%$fAAbCgyWQ?*Kl|rL55F455QwHJot&I7%lY}?^z_tb zvLZs1h$KXgIT6#7Cy&RI3%~sLUp#*N_^1E)k5`|0jws~$u-omXoQUY;!QSTrIBQ&p8V((x02Xa#Yg|XjaA>4n%bM^5s`vdF6u--}~yTyF=h9rCnb3 z^T)^}ddPx_IFrPgm?g3dLlg?sR z5JpAenJdpe_uO+q6Z+&#B#G1#Es==(_wPS^_;7c6y88FBxxChPO)y@za8gfx+SbyZ$I#MvKwM1)J)dz6{3@ezorQ9Y~7B^Rnb=pSSC zmMJ)b(6)-9U1}+S|C**4I>?AEL)v!XsAs-r&g<}xgf)Q) z;2rh4GV833*>i-=gd9O_ z`<0^uqJynfY6n<=4L&FX0;82v8LAnlkAdA~E^-YUL8RLD-O`IGyPJAp6%!Jqh7%|I z)MC-rLX|uodhRyp)W5B3zom8$Q1-gbwtfiWD7908?MO9nFp(GL<4EzwaK_AfhiB=u zg5Nb#u=Y-S>2Z^BTc)FKc5|N9f`mdFU3}R{U@5&omCNIlZY62K z4ukvHDRU%B79kBKYc0VB8b?MaA;@Aq!HK?viUD?QG|_#a8X;vk00*!w9MLManHR;L zitBFcGz6z(33i<`ctHksDBGX)wkMlwSl1zhoL9NU5WK-=vQaLb>+kv4`W;}8IZ9)I z3~AEXN&Ky6a4q`TKC5zBNI^;2_z&b9j57Um=5z~ ziw!oVV&X_lQRC6+a5(60YbXEIXoM3^{Sm`R5^%z0l(#CDXPJl;5EJHBN*!gm9v@Kx zyToJy*DR+InsGql~KViO1!FJbA*QlRZ13Gk`I#vmS>;8_M^Z3`|)J=vw!}7 zA3V56If=+w+K;;!LYSr$%lab|<+9{H3_~Cu;_k_#haAV>{N^_z^mqU8_b=SI6~e%Q z>5wf*H0(yp$X;KCw;t9&OcCZI1Vu~Q{d%M>j#b^hwwi5wk=pe^HXVoDsWFaogbUG& z%{Ln9r^S&~;~+wTt9ZRA1qv?EFJR4UnJ&4G=zwG?=HFeoZ9#$gOGhB?bTPaMKDO`m@H=^y^^>6NROiM_+ua@i?!Ycd?l zM@r7~eBr`{>({SexNu>A_E^j@BH|b*<(%`EUw--3S6_YWg%^fln9`inYK9O<*4?1I zWRL2?uv)z`QOQ}iMXx&H#0Eu0I1%YGW)jg6eX$I-B07|1D@>dUnp5f(ev+iA#k z@N4NPumsjU%|sGI;0$7*SHa0r-CCg9S#&O)lndl64K&x_Q4|NI5;hqu^JXIw$64Ts93D~IBmtD9UE^Wm0KeewqWSEL9oTw zs^Fgl4i>?5qT8U+4JE<+IWBu?aSJVuBaWUeF&!ms7H89;wg7?XIwCtq(_SL(NaQW= z(BYylZ6<)WOW&yP$hE(*I)BY1N_svPH|~lfro;CL*i;LfeYGDJ!FOIGfrKDDu7#)e zj)jqWxNL9)9h5^UaYi>iZnepE+DdXOlU+nwU`!NUgDxQMkj2(oEjVIWxDElkVh9Eb zGs57w?^sWDu7H-mTdIn`TPAi}L-g^DEAIeq1_y=jTra>TV<8e9fFr3y5%F2TAg zv=Z6dAKhRO0=FD<0$?B^yW3(QtLwf_volzX7uUD%V{FkAr`Bj~YNV=h2ixiPbwvFg z?*s;vRv)DqF%f3k5G`p=B6qPv1uUZC=Pa#=wqmyRyM4Nvj(=|Hkv?v5x61-qNBLvB zBJ?Ns47Clo!kJ(xLc>62x7)##fNKbROJ|3>wb0756Aw8$0$NGkzuDR)6AT9)*A4?S zMXsW=8y;>9Rqh*5?V0SxIuP&TYzq;`hP8%P;j9JRf-&hD)( z$eN)7=}B-HuAU%N8($PKOI&M`qm5|ScwL)|!!EI1rj@GYpsL|zghi$(2FCh@ViZY& zM$D!5FcERp_EprAFp+KyD)3WUZr;nYWMRE8r(_jimcY7&Xum%@KYuc(gHR&Q#7uDc zr1S=$6^0lnu!jCL)(k{mdJG|0+6u=$AWlM4ZzjSRaYUmLwnCVIl-I6tb&5dsL`2UG zw@+1GmTIH{TnB-cbQ55z)z3&{D0L?fKmtlBWy&J?(xodueCuy0%P;=Vzy0BpJHr@4 zpy`l?fn!`$NO8`~YI&d_k(pv3+6}{acK-NRzxvO!vnT)Xlb^iu(yM`X+R*|-47xl5 zD0$*i&%QW%8B@CjzDXku5jt(=?ag;>^{4jImNCn6q-1SyeX25PhJ32+mo>u88(JJV z-E6NzQKClVs1{=U>PCfmo@0#HuU~)T_3wW6*(VR~eKk+AAJQ<*Oko&y$ZTeg!=k+- z1<8rW@aW0@{{63h|NGy+^4hBxFJ2UPx~Eih^ubSCa^5~rG82WA(lCabH*Y@w{PUlE zQU!2Jh5Xf5cfb7dOSQ1Fq+MEd?HhKAKd`z;=jPllEA5j`#$1_cnslJGbCkm{3_}=( zL3hE2K;t;h`)Qh{ltTz1E(@Nj+nOy`i0IzEd+)#h{+n-p=j3!Zjwkc{IL4q{?qVn4 zzn0a7{=~phqix4gOv(m87v5TMuXiG23qY%NB(OwR*uLt5y)npaFmPQscGPeD2(%?0 zSOyijKrZf+G!lZp(Oz{a+`c~!Wc&W9xT8&L`7Xu9TxA}b#qfENLf-6x zcx1q%Ot$6@oo{TxCJYw7qw=9 zrL{vmHQK>o*zr?qc~It^LfI}12QAV8#c4~y^?mnVwJoJ4P(1;A>uMdRv(=?WU$_n^ zBjk>QR13TnwxtCJxr4zmg&OW#v0N`eI>OqIP;d1j;AvNKoFZcxr~X*X3|)&$D?;rj z2Y{o1>zx{2J7#r>i4C@b(TbuK$n}{CDBsT^>tJwoQF!o8+lVsAM9Y|ngyPzgw?uO! za(2mnI1}Nhip?6ds4}j1*JJzMv9(2C;KA-(WXhww;jb>Ft}R>pI01HWx;ERo4hulP zKX*fn1dN9B54W_&iI8g_MXRf`RJ7avb!+elfc|n_FnFryAhm120tOl1b8iM})XU{s zDkpDN#jT3ztO1KvjNdR^9ZZ|NPZ@S!TKSe-);Ly|I+%#maHJ09Gzsz-wXCa;7Fo7l zwm8U;fPz4@mR?HK3@I$OA`Ziiv%@y%8;u#gr-zB9ml9~aHX>mP`8B{`BK%q2xi^j0q_&0wBgR&*o7Kp9sIr8X%=n&#OO9+vf)0{6{xb%a!{yL7s zzx~UPcEf&u2qbgLzj^!Dhr{82{?GsQ z(n~MJF^bT5vLiBEKr8qfQ0GjWb)nUn zklblgdMPp%;EAZ`D=&|FT$F`~rLynNOrzS^ClSfQy5y#tI*CYxFJ8R(=9_PReCNae zdhk$WO0&$F2H`oU5SdD5GRw`XA+_CZoMuUBe*F0H#~*)q_wL;nUwmew;+9b0Mt5gJF9n5gdb7y4vx1uLI;C1+4U1f!RhXOc z*B*0*sCa%Bl;N@~f#8suxu-}FlkhH0T1>azptkCd2=@^? zq`E}jA|UEom)dPzBn1ImU4z|PUEEu+t^Jk)E!|uJM@Cad?AP?Pt!rI41#dxZq2k(r z7%5yMj#g5Sp+{(re$mDqOl*{}%G|qq97lMbf z+G-O^$*Pwnv*etUWY+A$v`|0^m2;NBM0(8!WnT94b7>8`aNr@Rlt)c+2SF{=%Pu%*??psoDeWR|fUO(2GEr$?p@SYJchOsvTZ2}SvWw~TzMRZt zv=WGP+x~Zr1skUps8}~CExW2U!?N~eIhs)dNh%Y)GHzjg5Mg17MO?9wVF)oYg&ELX2S|JD!w z`eZ!)`Op9L-QWN2@#C{)ZW%>=h!6rr8__d|FbrWo9YWv`V$L#WdGGyqQ_6qyH-GzQ zfBxsEr>7~2<-%)WHVUHMl%8ks-+NqBEj6TZxd?`nBNYks93eNDTgJJ^mEp#5ajiLq z@VUmd|(zgTxUXQIxQ3de|QOx0d;HV-?c6vBAp#*Nord+meYzx(j(d*cv9IA@s- zDRK-uR$E3y%xM_r7$Z#+2bt&jWQ>RT@WmHjeE9wcH*Vb64Ws$BM%&yRByUN&#T3Wo zoF&F6qBDd+M4o;2ndhH>ejLYXO4BqCL*&v+7D@B`)mL9Vc<|uz<;ynkGjo=XNOvJY ziE>p!2<{*lGc$X)zUCulDO$L;-|wesvhD^ByWMUaM`k`84k<6Wjc(n#^~M|Dx%=gV zpa1;lGEE{9<2a7v5Qdaep0ngUou7U3$tUl<_uh*y-3~(-$MNj!tQ*ij&I>??R>V@N z001BWNklx{yLH>{TrDS81Q$&p5wMtP&^PB@YZ$RXqX1ebr)bo0gn*~6!S=VQJSv?li4?;w z82n~)xoiy|;cpRlLzSVqW&!HCp)FKY3-G$GUQ?{P=d-3iLlEB?z%H{|*r*mW*U^?F zpax-GBW$?@0s$#|5M zs3c;vT9S4eZxx;TwD8~?>T$e_aw${gl(#ieqo*~6KiW>+C$tX)yTDpVHmKU+I5zGm z+)~5UwTh0CMqUR77I1pnB3^Nss_Kq7ydlZ%5$SkwTr52HPOE4U5%J`kY4FoGt|DWY9Odma*S@#%|_DJ&>`!c`pXjb8o0Qcw)+Rg zL%@!?smk&%U{Q)Ker8HZip2VmYoQE2#xG;XN&%P}SmIPibAsK($SeZovae>uP|TDR zx?4z*)h(PMgmD~^o)$OAdAYe8VZh(Ii`D=L^<95bLZwn4wyP-2UL~b=r%}wdL`c9S zhVm%W9dng9(==HQ!<3SxJ5DJ@VqE)2(ixA8L@`Dxdug##I}vO5CyPi}#K5PgK-uQN z5;z@ZJ+q60QP4{w!8g!5*|a-pY!|8|{toa+$!}{Cg^WT;DIX3~<5UepZj4!2DlIM^0<;V}z~P)aioaoAn_vp@gK zil8dUw&p7LX6|#a9Gv{I5RVq6(#Pu&aFB*^X>t%is;7}bEXi&$;rt} zFTHf@)~yE*@24a=QDPy@u`rAU3JyVrVHkJul*3`4h;s-uO^5sU@4x%*yWjiX_b#4Z z;HParY-OfPD91+|D^p79rl&LwLpV7(xpnK-rDv`@ym$X_I1ud?`x=;%h{(NrU*7%l zix*$IrD>g)-g~*9wxA^tEv>tb>s&>vi??K&tF3M3uT=J(ot$Pa&4;}u zMH+_T<(FUnr+@mVKi$3mhd=)DgAYDX>%80TbQ`V=ktIxr^x(mRciwsDe|-NhZr;2R zdB6lf71>pgq(T`DvExpw7pN?mL~QJkP8AUmCJxNvgS@4kOvFT?lnkUI4z8pdze71c zWuKIyu4P@p(l(^Q7b$}l%*3y5s&c2gJe4{bTtx)k_#kU_r}7RkvvX?cCYRj-=r-ZGry~lByomKR<<4NXvW+FN zFxOX3Ss9B~JJf88E>>|&Z~tuZ!ZKTow)VH8C)C?DhMl%`yTaLh96OGij;$83JJoL8 z0AdIFjyK5l@QreAWar{g+Mf>W+}k&_)txnNZtIc43^QaqEZO#TL@e!VS1X-N;%@`2 z6^yn80{M5Hx@KD(yQ+i*+G1^i7MSB5b&Y_tJg$-B)tx?4O;1A~34$u){$%#Y?N=}; z5$hqAiYe{*Tbf-ZnU3;{fY$|V-H?Sl$jrxl`r|~d$H7%H1MIP?D>nYW2-Sf+Yb58) zTuCg~(J?3LIcH20 zIOuGF)S__2Tyx_&bX9%}FV{=AcP?^5i+1FaIIhDrB2vFmpP58;DaMeAG=E)-DQa{J zn;ZLN!_%`*Pc?(6+Ook--58q`e((>ssBMjyIs_pimcX{VtCbLY6JUdCmrV*!kc#6W z1PM&clCx+s&Bf;z4xDV8If;m55{iNH%4_Vg)t$L)g%Q%Q9T8ci;VIj*j2DP<*Iro} zqkz7*Bk&BfS6=Q&7CWFIBAMou_)CGsLCrZ6sR_!Y2NRhxt*erOn1pg2To%Al>PtH@ z3dUstJC|#(aDZcYj0le77TLG0Hb_2Nqu* zWJ>EUT@*GYL|2KlOyGej>Lp7lO(t>%2Ev9zq2guqLEDAwaPo@-a& zO>S>YZEBP%fbSCdVG+Cbi0tElKof~;5&KYu39!q9wvCQe)Xms-plqJ!A;cKQvnNv= z`0}MIZ@lq6t@!raZ|~33I4s92Ld=my=1k>~PKe7j@gXY3iF3YiaeV*%chAnAeD8aI z_M^Y~yXT&Jo@k0O=Cpu?C=ijI%yPiDi#*aeyQVRYg(zF7w@5&2)_p>Q=vs5+-Fn#- z?)K@nObypiz3mCTMOrQnbqi9cQxjTs0e_c(wzx)TUvt^9+mdA%;*D?JynXwn_ul`# z&Mp&kh@48M6^vUA!?2$wqI~h??8n6?JGH&$Gn11PN(GEwc*x)ON~c!qBxUFc|m&$=N-`5v#Wb z%E9Iu!8aI4m0n=o(Fa>F??=}8Ft;v|Rd+((uH;iK3HA~rYf1sPgo6e_f8=^|WebUZ zHMzwzwymDpV=;Y`_NPj|(^*6l!84=?Y?O0fTGVuxM{Nx%Np{+_C2Xx-LI_(FupR8( z_=08H23vl0$?l2?3`W>&s}%UDwJicyPiP_q^>N4vMrg(^#ym`ir8is1uTz> zU^?4^%J3isP|aIKMh@cF?r%gKdql{QP3}jxaaU_g5|EV)2E65wE007*oo&U>RcYCQ zY;rW|!r)ZK!GD}1t>ie(rCJADy1D9xouiv8)Ar&B=wzTSh9{RhL+}=%9Ga!T%2|*{7(i(ezmZWX*G$bxC3?9Lq$S+B8mV2DUcDYR`i zh6NFKlysjQ77ROlu`ou@Tb8-r3*Rx|&aNZsF~y<>R)(1q3_x~TMe2YcR&jA&`(4;d z1gA7KfV*X2yN&2y4p~>e{MivdtJU&_z|Odbdy@{Q?~Xg;IO2vugTI7sA*Uy=c?>v2 zbEl~`ZfyHHb27QI?LJ0QO~YzZVg2IXOooZP!YTDuMP`2=6ECq8f(5fchst9cKndf;7(PUWQ3@(=duJM;^JDhWtoK}pOiFi(VKTRo< z0@XyfiHPQ@ir(5+qba^b;c*&@(OXc8g%PF{zI|xfF#?OZ@{Z~4R|HI9(+G*Xq;9KC z*A(-zyA}=3P>`&oq_tKE5P=*R=Nu-W2{~tBRtplCqwds?ab%MHoG8R^fBU=hoX?(2 zzklzyOvJosdnTTR1`0U~a}FV7S#BrGydG7Efj;~6&OGhMas0tw{qS4gdLidLjKkq@ z(7P$;oK8+oa-y6w1t#XKZdP1|lO#FlZ(7#V7DRXv9l(DfB8s46p)~VkM<{0wq?pTg zbm7_!LcK-qI5|5a1lSt(;`()%-vPh)*5)}rA$;qMFoeXE#QI=>4oa!@IpH_Tzkf5BC z_N(huYR89Rc=+()lP6C!EAc!}yWOP_HH&Mo%gs`ft5>gHzI^$@rK>N${PN|?&pdwo zIG5u;yWK8?pgAICp6}he_sJ)peE%>1$1n`j{+u}$ijbVRic!8ffLaR4KsjOMz}LQP_ltUvT@gfL}U zH^7n_hf-fHEiKDttJ%pU#OuAHdMrd215)uj)jlt_nt<0!r$QvU5R4v%^Gy+Vb8&EZ zHZ-}ok^~RhI@5OnM;zBzrA$?Zoo>{?fr{3zr3{ugU{D|tHl&`La31LznTTMxVo0c#1=tkz*cW5RNet>D)<2RpIp%S{5-u(w7Xi)*T*HoCk!5R;lZ(;2NebYDpj) zD6y^Cp+<>X1LcZ=8PWEGsc8s7a~`%0J6UZdykUA@_PW43189G?_?kUI0k#4z&Rdzs zt(pqY`kiWXvP zS&tGz%Rby?rYV6l6GIfZwJivqlmSbcacAWwF9yZck~(P>)gGy{&&UGDWfXhl8v3-x zGP^Pa(=-)A(L67HbQspFsH6<#T<&_3a!G>$Q?8C*!KOvvvqs+p!emy`<>*KHSZvf} z%;wreqz;l%-uw_HzMhD;!Wf$q7{qO&ZG=*qltg6fs8Ny(U}C@p1-xD?z=K#=zZToI zt``PT2pl8}hoI>xm{^rXvTg=v$;=^$kO(oHpWw!J5mjFUY;948(?Wq_c8!gd*Xla2Pb49R7$YS;2Rz%>gkh-iCbyo0(M|6v-|wfKDd((;KMp4*5=10S zDJSC^vRG%iiO`KSN=um5^>cJ|jl{L!sj zFXlX-p6;e8woYx+T2~caVb?6m3W^=l^N4yMfqV;# zwuLTUREBdLpSn$PQ>zqEfhA^kn>*cP*y*Q1g`)r13#TQ?`ef##k`(GUna|{Cq z9?Oxj#rasaqf&@8BpL^rB9Box$o~Av=bwN6>8GE*`R1Ey*+aQ|B*R)U*B*=`ewSR1 z2(nBVmI>i743{rozIyfQZamGIQj!?54sDTT%9)rSJ$U%&(WBGT(-`BN){Sq-GNZ;* zMUgSYvrcKeKi9QZ4r`SJOQ8^X8Il+8O6HvBc}h8F3i0IR^umQp&ph+YYp=cb$}6w@ z_P4)Pr(qX|oU=xWHOUDxfArBuUq5{G{Pk;D_{rI$r_xzu$py1zuS%J3WPj~(j+Z^T zEcu$mE`ty$78UijK6;IhHGuxPVF&2#-ZlK!?oX~ z0FuJ#yLo#A(=DlI9W5eVTA9d6vr*Umc3`&@K)!Q*n%?!*rJJJ(Qq1vatLv_YA{nV> zw~K?T$E>qrgv5#QmaMkGJ90S~97B(YD^@|dHTWG(T>D3G>2@R=9?@}ScV!UNZm}(K z>dG6MJ6F+B!-;My?X5-a63tm2b1)2&t%?>zXv^T7wkuRy#;}ExYKCfEn--$Hb;ZCn z)$OHa_i;r)_Mv#ReC+D49hK`Vd;uw}#+Fd*8CDqx%T_2A5mu zb!u*Jx@)DmIr-WpNGlbsc3_~*DOyW`tyM11I9j=&mm?7b(OQ-7a?ojw727gICWlVZ zuort;s@j{b5irl$a9Ij4Q*%xiNo~03OABT>NRV;*+wEZ8;*nvEQOH^_{CcV*IJRtd z9+#P=Ndi-fEn0@ew+BZwDd&(x#N?qR^_m{A(3~OAh7K@-Z5?S9orekpRgrEBS->~z zl7jqV%=*bv;+mjppa9eCx*iQqk@YmNg{QHkenLJv)j(>aTv45~$VsDhk#o+pXkQ3{ z>SO{aPNsk+`MNxS^f4Y;I>#-m#bseQXUa)(&eNRdoO7ZOE#&C^^J$v&avc}$a1X;{ zZ%k%Xl8p#1g8`ZjF0l)S-6XkpO29$e!p1$HL7`H;)|1CBxQwkqcUyR`ec|tc*NZVb zpdjd2-=T4&R=EPGde6+djY~;*NNIO^;d_7n7vssvZa4hyowoyzkG?)1ck$$8JRAbnyWN-vIe+r-KmPp}PaZw`yTAMU*Is))rzwOm47>e)A3``e zy@-iijc}$LD9X}X61T7TZwKX?myYhvoVD+dlhmMXx!}O7t1ip$%uJNH9#iR?v9b>g zRh#41c4$q+MJ3WEOBA;=88aJ4_jw}+)oaCJE-u>eKmv^sUzaC@MfPiid#dYtNR$EU? z&+>%wZrPwgz2phlf>QG9ufMjPwq~b7q-n}h5-G%S)IDgslarTTdG(F&e&^lyem@B2z z(02e!a{)s4aS*n{5Q0keCQ8Ey&5?u6x&HRP-Co*0)!0!UTLW`bBSZn=XPAeyifk~O zqPdD(Joq%7V|1MD8b;Hov5h8~7!$i`W81cE+qT)*wwuPbZ8QxUr_p)8A1D8swc5^_ zmAvyj_kCS^Uy`;m(w_oy|@c z*D6!5AXLy&o6Qi8etFqYRGsP0^u5^y&UBuLY9saW%eNX{wvFm0O3?|0W@A_Pb5v?@ zt>&BL2E*xhs+q?2=2TZJL`=YHeDeT5rLK;@F(SEzGd@zaadnW^1Pq_$O2W~Od1)~6DyWjDb4^SjnV^66ln&G(S#xZ*`gES= zX$7tX69zK7w**Tc+DVJl6q@zNE_QH7OWT;Hb(>6^fhw~0c^i*yyt|A7UpZXjCR884 zVPNY0>uSYu5t4M)ewnNVgm;5riUZ@JcDq>)qtY*kq(eH-LbV)NaoEIJQY_yzDh!#( zi?+eo?OMdWRQpY19_r=|j+@xlh&ef-k;QV@ zB?;{`e4T$IB18?4ZEA_tQ^FO>$`m9L1UquP4_JwI@9`vvkbKUA=eAYv$K70oKZ}A+ za_%SukuWj>1CDFCZt7?lv@4aR7Whh1Ow#ZU(8SGJo37Lp-Ag|g;29HTWZF8bQt?dQ zvdb#VWtuSXm}h3vMAQ?^SZ3w%c{;hdl@_idfTKX=!Wv4xDTEg?Ns$qnMmqSp6Dazz zW2C7$o@cQC(DfA%P_tPQrJpp)AX*nTty)SWGv_|gce>-4EV3*}>zSr)T6h;IEusZb z&EMQrAyVT#d5Z@fcT<2F%eKeC*K|f5V1WI+1g!g*V|b1%r6vRq@PrMtg2<_gcogu4 z^FN#K_nF7{zF*k;)vQqZ0#>%~Jr0(6=1s)f4Lt15aV(ABIEjWHGYA7A+I!cxEwvTm z9=&sv#bZK|B|_i8g3&HWXnKYuS?ga`-;eWM}FN*i3f(A9HtlZn-HF z1!tlwbPcpw2*PC8fsODecQ|`ox*QYVcsKx9@O#>O0U+c-H>#I zH#U@p@j(<=pi|N-0zDJK#~H?b-J-I3Q|=3Cfjx3)_QwW#JQTbXE{5e6SZK&#r!V7) zkR!A!K@Is^vvY+zLPqw*Zj6rjMX9`PY(rK&Sl5(GkP8^I75v1$)jQ-m=TZjR!zn79 z$KaY4N2lJHYh55%M_VU&lrq)gCXU>!%&^{3#X_H|+CYdrNP#ct ztKkL*=??Q`pwqo(bj3ypVX5I@*f*73v>|SE`A9nMccKfQbvMN9DjeNtj{V z2KpTE$JuN!2|%&F(+5wYZ9 z=z&@JG+9qAhK_9pks(IR9n%?be*&B6(t@Cl>fx>-5BX*~Dcxw+Wnv@QHvjVuSfJoZ zW^zmIrp)g#{~&p|Z7!vx-L0wixj7s=7>NgXDkW>M;l5;b#BZ+YRRNT9F(hI>NV^1% zGJWNLg1B@#5zd`hHoQ$RlU7)U#C#Yc`xiLlx{t$9gtezRtS&ma)K*>30^qQul{7Jy zyQKkcw=`U!u>>u0Ga^x`C4oD2UC%#Tv$(&9fWkyI3-y*=x81{uo5qZJzX%>Ws}>(p_&qmR@^Mi`BPWHQ-RX(J^#TDJh) z3lPdYDU$#02h5i2Vv>LXSs@X23^NiGCb}%hm})HZIK_>6fbqz4{J*D)_f?NOunJ60 z#x(m9CKyiiS>vk{w~RJCizk~{xB#g0fnyu!a|3`E|JOgL$J5hECpnqH65oON6p*5N zetu4+SJz<2uOhgl>-g=jrj-3x4GPw}*NK z4EM7!L{WsQaf~@iF+!Ge+|33n(v~Xw6w6}&BA`%t4@m1CCzYp4Ksp|hf>J~&lyPvR zEJ3ot^qRLjj*RPMAcsB$kq8R&6t|LA{(GGSt()OOVuu03eI>v123#twW#))gjE^Oi zIbV=Y-z=;w1f=t!R9{8}jX8#4X&(b@Si})|S|ef=g`d`Um}7>1A(icNgU?1(D1(|I zPGmvb>Q?j8#Xl$zkDOBLPvfnUbi^*-5$Nt~B@4tn;YF&dXf-C0gXBy%y<@amyEnXY zlJ-%PiJD=Tdn*fUar&N7IWzVexmPB*{(_N|ow+7pMu=wwf6G$BZR1E*%#)H>*PAIa z5vi8o|7)P-`qN3GYu3S~L?q%Hxd3tJrA?&G<2dv85nOt)(7y>@m{%s@s&LLm)_(sI zKA2S!^tCl!Xd_hZh4pb|%6p5ng%VsY2u>#?-<0%^Z|A<&(Ph|tKRbMtz&^LOem^$g zo;Rl7(}XiEJ$zyb?C8UPJ4bW5g=&(mSHt7x$<318;GzGZmLNh{ZI2Y`Vu2aUBeZP(H8D4c3=K+(!l%)r zLT8G!pky{D5f@v%zA1e8EL6_4F$%Yo!||==dB3I>Tcd|{*4l;|J6WP<=~Dz~;%$HItA%bG~N-ll(b8!2aA<;h|Z=h$d44eF;*lBNp^^>cv_?O za!u`;6QW?02}J~U6A>;BH3tHrWTIoVCe2@2aPs*k?G_jvTFWMc13rdKiib2HW_SqO zWfYqxcxUu`$h{6qSZb48Dt0O!&2K%au|`tY@5EoedA>fq+DJ7yn8Z!8gcQiwXR;lo zJer5!QhUetDb*A|#0&uFv_|vay%+>R~f4++u zvLT8cZW3iQ?SOl&S&~oJ3+`W<;~^G(n?x-h8HS`phph5|0OSZ|SxbZj&^B(f?|Dil z`M3kV(tkd6L`*89WU(g;<==n*tgDG|Wbn)!f+v5>Bfr2t_42sw{pi3A;9_=b+Wb6@ zGR;LnW^4sorycG{;{`D?TSg|*n09bF#YtoUg`lnn+#zsI$^Vve%6jT?O9_@f2}7X6u?N_UbIRJ?3NHK z1o-2*sjb5$){6(+uwO)IhCP7CEYP*N=l6TQ3b!N}Nb=%OiDnHKEPdOX&i#7Td%Jy4 z$TjSUB{`{sW4Q>A0s__h|C)D{xFEoY_=^*dwq|cM=;W-FCm^fiJ__TlFR+!K={<6kgz!Q7lGAwhT%w z?Fv=UiqYb}HTE2%Cb(=ux73)NyK1Cvxv`HXjBYoen9AL)+#N2=tSa~JkFz>%XTLzp zDmO#UkEkPEM5Kpbsb%|it=9_4pX*_e)~N3TJVMsR1xog*9gIs8q{RAk(0W|ox?#i`^Dozm)TxddaK=Ce#yH<(4S z8_X#GjTV$J#@MXZI4VzBOZ73&7L3BgP5|dt`*ITk+OCun!TWN7;c)JVWv#thX}TTP zP&6wT0f+lUyed{@7q3?0Dn}%^p0**ssDR<$)$++QrGJpHt3HmwpZGq6`mS|e+FpV5 zbfvbcQ2-AI@rc5khB`q3QERwhtEd@vO*T7&r;P5NE~;6%r^JUT^3(kCUTC7AxGK_y z=BGBiE3byP{x~X?y7t54fb@pRnnuJyoC?Ly!6XMs|NQV^Ul*V^szatJUHi+^vDdzk zGCIoi{8=w->-hfJveEpsnkewmYsz(8gw%}6a@mhDJ=X!gmG0hVrbTghJX<2)rpV~n zjJ)Q6wLhjyM;5kx&MtV9#`44I4Q~pDk4u@vclRxcV@RXp@Dtkn4P&7#&KELyS{8A5 z2Fd9e6oC51`}Hd>0+7_w3`@Vf%{^H&B`RIdYH74$afyzUHbdAiFY_1b&O_p;jm>Da zxrwio9l{pbCbQW@5>q*jU*@pouVs47cQgJfyisC3_qP4JG*yn?n8|dFUqS>>M*98a zCjwA)Q=#ZWIi?gaqpvw8`dbOp{Dkhf|wjtTd3BxE+?9cm8I0O z)snK~6^tC^f!p+AZb^YA372~`X!lcXWv_9iLZ5|A z%N43m0EUb4AyBIOc{eZp-~OJR80CeB)tN|S*9afQ5e~#+h7UW>x6UHAjXdZBnfPh>G?OgRAsn1bxQ^>ni$cleRw+qfN`2; z>~?x>jfE%1$$iA#93-C_@wh?z5RHw7cDDxo9%yrnEZ_6H08I11|BYgN>H{^9yaQ)g-;0-t~eyBi~ts<(Tp zWN;coqC(HWF&|Xykn{(M^dY*3&BrymL9Kx@x~W;iBR1wU*|cq`F}-A5a1ZS`4wCIp z@MA-aVs*`GP{^Usvtz8Wm*xoTimUY(ZLIP%NrqtA7S>2r9VV3G2S*)?ij?do3Avv- zk}E3d1Yjln)(OK2@GCe}o7mp}tMPmZCxMvFTH<2I;!eV}+R(291ecvQXpsc1Lf0zg zcoXdbFLp z_ur7pC7uw{KRtscUN*hEi6jZe3-Be-bzwMwrrKB;V7oFEWqzYKpKf09d2=+wcFi>rEtj=($ zEH;smfo$lSa(*lOR=1ZrcE6mE=_Xrg@jfO^XIcznf9VWB24^cxu0wAz%M6&o~pg z2|6=iZB86(=9Ly}%2T47mNEpU6RH>E&n3XMPAv7$s)-q2md3X>O#aUDb6oS+EiYKq zPR8vzf%{nvF`i*-?Hm=n=nQ6Gv#;_?z43~1JtYvgS$s<~v+$u~t#$Nm+PNBW)Iq^w zn{z73KgHH|gae@6AApkO1+D+*`vkxI!uPVlz~humQL%v>qnPjkB4fU9-y~^a#&G4_ zU*1HFGv=Sw-T6O;m{0&emMcJYBfemU6AjKGv_D30^ao$% ze!_K-s#bG%)DuzOQ->B`O8AF{&sO!isB`ULgq1-i5I^M9afn7X5-yMz*s5npY^AH| z1n0)@YD4TINQx0xe5|Ta)31ZM%s__Y+&%DnOMEu{i|7uuLJ!=h)5i=_o z1od@D4eXa)K5X}#v964-mTFOyvWv&)lFOB8DRIjcTxZAIv5Sy4sI6{K*^+kLPpHAx zk1KZ^NQ7q)DA{?#wGJ#5SOIAw zU!f}@iJV|fa$x?0xwk8Rt);Z99>dz@@9f)OAv*3j2=5>*uVPM*)9<8`CNSEy9%%B5 zOQSK`36Vn3BqXzBa8T=HP2l&xCCD!OyU+vTaa5;{C0gq6a#HX}iJuL9W`u%o>G->A zX?C__C1qC;MeCKw^p`uO;)gzMv3v=lHfdN4XY*090NoecpOD z-XJcynvD@7y)ukm;HuRMRki-T7JlIE^e*?C9Wb-kLkiwq3fgmQ=v6k1+mpI?tDAn@=3}G&e($zTzhA53PmKnzSdlyPH8v8{%^zvw>2ZO$imX8Q*?Q^=<#E zEft~p_@PBEm-&sPHDn^P3b2TyTi!Q59a-Q>Y0M!yyR71R>3uoZqE7>slAN8yb zvdXYG>mPq=!De#sUv(22O;8T-MjY7-u+jIWU^6r+&`QvRD7H_0xQS50g|jG80%>-! zE864rl1a-_n5C86u?G$GZmB6^`6$e&YFQBvW@p9h2fQFX3|23}_19y&eG0ZCMEg%m z+oy3($~1O=x~raKzcf>yV@skCnA8`{^|sYr*oSyjFb0N1WJZ8cJ#j#dY;y}KD!Iy1 z-icdd*ON^%A4}T)LdL&Ph=+1+{{qUrVD-o*(DL!%hZP?dypokpuSL}UY)dk zSTiO0-z5J4{FT~DUk(^goY~^KZIZ2JYC`&Qyn`=swk}MiDa)k)VT@#?aoHlO)Fl)- zI+9r?cQBIF6v6D8@y_*Ip4Cp#Sz@#Q!iL}=h9=SpPxo~a3FCB)Z|#>Q!g|LN8ugsc zG>Z8zP&Cn||9-Fj=tO~hdPO5+S?Vjr${^{n!ohBsMmBrt()P8Zp8vFG?UyqjYPabFxYWD3 zq8AW#F;!$}`eeQj5I-p6e>(%Xf-`*Hwg5Xd`HKwA!6*D4?TH}X)9ud~paPpdWzBfq z6AY<~enulR1erj#lD=;KoU?{>L(=_f_=?O-p&aLV7N!z*q&>ix!wVrHNAdhHg=@Ft zWwwG~F=_jm;qh#NQLGJ7|y<|C|lXGHpfUL^VPT6a$4LLi->*{ zn?T@9<*2VAZP)y=(O~q$LCu&mFe0NcZ!RAV%2`Ts@r>d8s zAjr%}eT?Qot-D$cpW3oqp74WcUplb49`*{}Z4gZA){W4He@QZ0}f89+e z$(y;L`0U-VLls-lu{09et?iR*1k$rW&AsMsu$)k^MbMf~(`pT0j2~U@+M9RE$@*Ne z6)J8pAE&FqVy+%5yGmRc;DjFw5i~vtBTluSV~*~MospLO?U+Zxv&qf&jr_SFz*PM( zse&m$_R=G=5h>`ry~q^NT8OM4((p`T&mc%wbh%fXqeAnbaZL-?@8)>*aMc!M_$|$- zp{B|-vN+%DqxRI|ljdf;f|EWg)rPI>&$H2Nn3shy>=ifp&d+JmLu!;4Z)qN1BVFtF z2<^<~yjtTU*p0|Rjoi{QHkyu25h*FLnj%tBZ!}rH{-HdW;4f;K1h8Y8tkDd02@1b| zG2T>0zSE*n8t0_0h_RV&X;M)z^iK|qv#R@X1W)&(*X+?KUj+1u&&e}7vsl{L4^{MO zN+_#~?dSYw0KUN!p`eWuaEn!CL0Z5m5z@p!kB%tdFf+q2(!4rEb3n?0V2)wX6rL11 zW*W;mPN9&FNsKHOiCv1*AdiM*WkWxq{LIqSc}bDQLuTHyP8KWxo0y+Ca});4**Sv7 zUYCu`WhLs`%W!`I35jYIf<8#V62F4PirvI?pT&~8u`7#D9xcK6c`h>|$bpn<8l*XX zq%uru29JpNn&^LyrL$mFux>D6DvrX1zftUQFscv97W7qM$)06Z#R({+gzX(30ydVb zM{h(goRuLVXiDB3$}%Dvg+hjuB(zXNJPt&w(PXue)hfjq4THkGBntv@;i2nmpEdmz5GnHKxb@1M_>?v3Uq8kFiYADCT-R4l9%;DrFXMpN^Myn zi|z8bJ-)bBi!glJ`FN;{|FVPHKS)+GC`eJlJV-%V{7Z%`6b&*QkH%p;mZ(300$`fn z+xxxIqd)(R_jfu!o*;vkZ8ZwB$usZLMd?UVD(nLz=#+^%FOG<{!UYxE?U0aB#ffp_ zwOtS*!Nr`HOhiBI4P&3TDzxbII$bo_g2!4#7>5{KhD6RUAvpAg>WBYZFSe}cz1%H) zjqmN1@23R@k5Du6N>a(k#wBwg*1S4uii+%mILmKu)Xam?_XyaE}n z|1ev(e_HU2N58*x5$lQ|GMdJZajndf%M^R1)lLDoVqm*D+D`X|t^YQ-n6$p+1OOQ})E&x-X=-C6i7a$=tV!#gX#~eS)mmZjmH!(JA-L~ODvwQ7f z-A&j z3;7IWkYB8RS$jLSw8!}yHeA|=2L;mIPZLy9)|5mJus~mhxJFJ+nf2Au*;*dqh&_F= z480}!JqBf!6VT%Lqy*j@!8eyZ@+u>2#Bg-S2;DN&UrmhO;pyZ_=tL{bS1N+61#38!ac9JWU`A~h978?~+qNGqX7*B_0 zWyJ?iA2VxRT+&X2?uD5Gs_s8B_xMKfQAmW93Pn7@(yaqpHI(*$>^u(quB@+O>sPGD z0O2yoGD=?22Wian$AYe^z&5`^>YFfCW%#3-LeY0s0niQvh5+Q)osY%4C6?b*oR4-? zB|5$mG_oPRJibz)F-2L4^(hC;icfH<3*;^VyD@30C6haoo$z!PWCjlMA=P6HJYfMI zTsc^_>#JZX8IXl0byA^Yw}Qg_5NSK6rStDPCK$GZ9=wgCLv#~MkhA;>snaAb4reS< zJx259TobQcnrqeR7m2|>e4|21T@SZwt35k>RgjXMvp9=wRBrgS`zX?iIv#->)I3=^ zo~*Q=#_0J~$fP@ao^`bur*8{&IF4O`zGjz0Fn&|T5?pHeNhV{XK3xF$dgh>?8cyU$ zvIUX`I`BoPL2+=o+D3BXXX?$yMQcoH3J$V~wXVuNAkaYHU`;$Rg;DS6*$3{c(x(D# zwghg0E`#hL9${W1VeT$j4+SD@>3`Zs=+~uOT-6p$KPkBfvsPE8DOkod=nw|yh=2uSi`JM=B^rSln?}qyM6mA1zKXc8SYLRG^gyXvNZmSQI8C?N z?*OAd|9|>^4{hps9byd|f82N$*pFq{xp32-^vo({k?oQ;uj_tTzrfXB6qFAms(|fvPl8LV-CzJB- z&30SK&%|b`^=%%~e(KHWPp4n0VZIpHXN`{@EDWjzRyVPnf5K;?DsF%e+*5?x4kTsk zX(jd9A!6-VBT)u39VVG4-YoPjYJosXD9EYClu5S~N#j>TlT{x`*fc1@ik_2|@kVxd zg#r)_MW(s+h){z1`>3_fA)d5P{VF7&f%=y$o#nev6>Rb5R8r&GIV@aI9b-gAwPIwx zx<$2kJ0jeph4q{D1*Q%=A&bG#{K6W-dN6@H90W#B^=2{en1$wlTw>ek^s7(*+E*?| zipQoKo?0h7W=MR4w!#6hZ6t>>3lV&E^@ZpSBL$ihDe#xCpNuvrx+AmSABf6f2*%^^ z6!KVq+p(LhYdH&Q4SCa5{8$iJ7I6p~v##M&rJrr`Y#w8AkR8-$vm82|O@Nnu0hRrD zXBk2|dGltlu~{x!FTR8(MAntiJrjU(Jgj#qMJ~-XfNK=;FY2+V5g-~hQ1!aC=McUV ztNbuZ`LvPS04n@f%$)j>FY{MHEIsBitqYx0#pZW=k3R5Ax595A!QVzIKE^d^|4SEeHM9Y^TX=eZ?-wWWR zs8Akpy7-t|RjrD}ipm_t03A>gM1L(u3M-ElEt2YlNP$cJEQkL8EI?Fgip+$n(@}D( z64jq9%z(6-u86+5*h3(~^z8RTl2y`DcVJV>Z|n!))MyaBWnd2eLNAyrxF&fq4|79c z3@7dS$AMc%M<+$rlE7KOnK`OCXjO6lIJy0djHgRGIKUo9ID>g8B*I0a^?mdn0guzh z<*q`0!Qb;@j+&uO(nXg`b|HBLH{C58XuEtI+P^!)?XCR$w*p*H`J6}#2mFBPl{7;a zbC#6VF=)T{YC7Zem3a)S%YobfX4!rxr}S>;edFWl{qg6{qc6{Nbpu99)aT_GCrYjUFbNpHW-unoZ|39)=7i+(_`ZUo7`48r8?xkrHEp z+DPqpvqFt)HhKQ<&$xPiXUY72e8}N=o&-%1cN=juCi^T$d(62%u9zOe8Tdxbp{U0) zh2#!;!cn(XT~K>@9sVf)3|YiZ~l`50<6&)UC-mp zw*zV(NraKX3YaH=rTSkK-e>ZD&Y-$F9D8||yYjLP!BgX+$N%GfDESu8_;B92%bSgs zdBLvj+})fu*)4J7;-Ms6&kG2x{d`)$<&mEx8+JmYV!#m>d7av-d%q3`-V@Yb_p^CT zS=@<6BO=o5V#hVD_i`Dz+yrltkP56>P<_2&Z$AV5c86Z!O8+V4wSJ(uwSj#xa;mmq z1x~YI#o4!F4$XNS#qr(GeLS;?@UlOS4;&>>0w|SZ@=Be=aPoSZYdAT&Jzq2)hN-8I5th#<$9rg8d*R@SvOC$(R8`V*7e+URG@V73-C z61RZ@j+=%^j&C|8Ue7gs^w^d0jfEuNAwT(fsAO*+m%rZkx*lZ8XYd!A=r6|zu@`Ss z@~VICjPW|j;b@I^Sk4LN`cy2vBm?!W5eW)#S8)LryD23;~kMapWuRaj8jB%s~j!81u8^{ervDcrXCL5*3yX&XiK&krM>X#Z|mN+h4CD>iAM$eMR zp#`)5PSFqUkcu#aJL|07#MFDJC7@T9B>RDQV>(hbSD2Yk$)8u6Y1THSQ(51OMUv!9 zm4qhbNJCpPxyOtWK2QJw{52b#VQzapw=+6;n9?@#S%=}CJ~}DUFUDPviC)bM^Neg| zl6MU6tSjZE#(Ki#Zn(oK(A`}7Hh)CR_xM-7oaDy+=rpsWfKU{{{u_03L1MO{`8zrN z5QTD0cEvT)BxjhA%kK5AjNdfP|D{de=k|V!wybw!iYwh581f`03A0e5pLxfzxKVU9 zO(jPUq@d`wO)R`~*}w6Yu*Zc1K2QEoVAL4Xpn;z5q7H+Iu8HzzBQmh8|2o#H;5XsH z?ESZY=(l6(Tpf0b@yJHh4rXSFtxOMKBVoON)^LQWwiuTUtT5NI4KQ~`lr(5U28Ai} zM-%BkAA7ERufreTpGM-15r`EDLV2fOSdKHMQ3u$O!da3-W`}BlSqNHW5L^ju?7;iQ zhsWdZ*mM8Cxct2U^WpM_F@*UG03*&r{U=H%IGhu?alkXfk|dxSa$Qd7yYut9AMw8k z#R%$`sAAb2%0v_=0MhZ_%_%w>@!Lt&dGhK`b!7Fkhi1;OKUE=C0%<16cc1nhfVLeR z(x*!^5w9dwxuMnCmmFb&PBq4(|I_a+mTTMNcB&1q43YAPm-@y12|*K*OQHp$9H(!O zJHT4=y+qoa3|fw5VsvuiVPHJFDk=OZ)-#IC;sn52!PIEi+GQ-9Gu{#CSBnetq4t3D z{TKSTQ_oQk&aF2p2$X8HF?DWZ_fj);Xl3qz!LQ2BjB!4`mJCU73(oaL(>z>`ryStD z9KZKPv2L{M=W>uCUCOA(O?bvUoA-C*`9(wB!agX#NoO^t{3E4DiHYB%^{Y}9b$pKCRQ6gogV~K~sG%c=!2!%;q3MpGtWGw-S@LK{9;mk*x2t+5o_H zJiA*$>RI)BC}n`PN014@faQ9S`aOPc7m}JKJ2q~T>K~PPekn6_^l{A)9*oaP2bz+b zy_LPXYjM?p_k|(qximgwQT9Is(z{tv4!=Ni%AhE$5bj3JF~k*&u%@ zBa@^**~g#v|E3Ued$YesGNvsLOAchoH^T>jjwGgGe zx}TWvzgJG+FfL0*twhX3yG8)YadYAhkBM0VSpfn_wd*%T!)wC^jmR-Q2L>7G)5 zjpf9pPO-e0Z~uenQSU5RKS zL;8p;)Y|*k5|jxtJ>8cVM5(eeI!-Px-2J{Hg{RCD+IKf>>CzEGaD0W8sKxWVgX2$y zw@v}c%ZT0%G$CRuN^@@958HHxm!;YCFCJSlN-7VsHJ3F62?)bg!$1^PBuKQxs}`hs zT+-KdyPX^XP5GM%Lv9z{bQ;-{7fS-pE`Wfw>i%X%_%E)weK*b+55*X_%fa$%+}Yki z_{Yn~>jbX*PoIm_tL}f}?zO;U#1bI+xq(=bjoGpcRwY{Mm-7m@f#((Q&;G|D4R_y0 z4vd(#xU5&s1&M4g6RuwS)`+YWZCRL~R(hu#79IGK{!T}U`}iB1^i~`W zGofM+nFSb2Q&23|TT0SipKO-JGL&ytX%bauh=m)kN0bSfS<*$7lQ_7WO6ag%{Lli|K#%}MQ}|B^*D)okpf`Efh2a z#tctvd_8sXS%n_EsF6M+X%ywGtJz&}A0IIy>i1UQiH%&F-X}N4Cy5?|HIb!*H5Zi9PNBZo59uA3--f5Y3Bw#E_s7Zz zWd}9Is@P5#BSyp2<=!l4!1uFna_)!_9cOLzFXi8_s8F!dWAQ2@&2J>Bfgt7Yd8#X_Ruu#J(45gV7F~@o(BnTSN9d#M zQ7lS}yR5M#G^VFPpH=pC;^`*tFcDKeL-j(_saIOaC|I&Do2aCueZqbplXg)h4T%zP z>QU7cdDYE7f^98~qI9v&%BQq;(6ci=efchIlu&yfm03=Nn4TcENwGnp7Rnk2u-}>~ zdIfxDHQ1q5L+ z#jPkRx^=oRZQVKr!1YJUYySpzVUey5i7>Vn^kYs*h5t6GCr~TCnJ@TxT>gBt(_R_Q zb&H(=EYJR7R;ad_rWI^iqW{eXj?7%j6M<(0k11?`(I&6M>3-(pm8o^3$*<|NsoXJ@CSrp3kUdp;E-Z&dacG-UIw`WiEtQ74tDhNpj(PLz1X;Efm+F3xO?vPt4+R^!wJCSQ@JL z>YQj@HZti7n88neYG;qdFrE|wcnSa2cs}md%1C_f=7yNvbh(fQn2`rEI%_vBIn$YR z3-E>=Ws-zV5FDLxl*R@7OQScMo(^AM+w4EyfdE$b^X6^cf0}1^J@36xTt@v~nLtUS zmB$4Qw_uz(m1GT*AU6Kfr1Vd|JC8sB&gWsx+&G3Zuv);OIK}twbnd@2>CFUFxB%^$ z?;VVp9`t*PB;0qsWW~Gaabnu6Z}yTYL%_`%2!f28=1(>dMX$Q}9wwOP3^({76wr@F zE=IFV9W((-x3{e;zsLU$P~1Ra;}p_eSADsmr3g4w&HYC+9sbed`1bFfaX2R^qFw=x zOu6)*UZ|$|Z&&a=8Ax?qp#}Yq;E8<8t^#&!g%ue6*VIxklOf*u(|6!~MW2m25>hu| znx^84v>GS2q%iOPH_tq-K?5lmA8T>#RDHaWfAWGw9$^Nq{02L}6cUra%!k*w^~!m= zcLyQ(j6&F96~P;uRM9>h62{t95oZ}^g}g{7TX3-K3WWo;N$N5mRar|zOHNLFs$8Sq zju96xM;Jpzj@=6;H!`&kIPmTqC`9V+Zbd}e!urJJRNfwI76+G*T-u(;&QNxX#AB5g zW(hZbZ5~Ue^dcM_d~%)Ti~j&By8C=tB}#uS!<=R2=Uw%;`Zk1IDvT(WG+&f`YKQBS zPpb;Mrr|azYBrG`d8OoW*3bT9B+ES4;U5yd*w_O@Yk48qn~)v0k$oqPEM@CgS3Jgx zky_&h5`8<{`w3PHI+$jd?Uq7=dDqN8dmQ(?_5St^r}gB5WEhI<6}Ylmx()N}60K)! zPH&J@%&VrJf6ERND6$5qb}0u023JJ94$4ESEY8w#g|<}w1`R%Q>rfSzrnSMYiYLj- z`uwRc9&3A6HUJfxoRB+v#7Q2~hz|r}dK9uPmXd?tbGX|!(LrbG0v*#8R&lG|hti)8 zdYdDIqR@Yc3maN>ZG%!YLKBXm;Sqni?qH>Ltuzo;R{+C?f#T($_z!)hZx(4#w$hGb z*&w(roG<&fTr@2=7J8wgakp~YH&{urTLpF&61pw8it{^kM@rjpaQCqh86Zgm+PUv$ z1(uG1lufocIB~u!>0pARc~UdR7b@i>Lp-T>P4r*M~!JYG;_t0h%C zU9}tFueZ#_jI7SX?J%KXL<0DRpY)W1QZziXsKzlOM7v<^>_-()9S&``;=7VEr65+g z6ORW*e&M`4{s{{f!yAR0F?v-#%;01U!Rrr`EM;+ZWQ&vJzS}wm3OV*&TkjidO#VQF zFh)7@uQ_@gR;DDY`M54F5El8l<$yN!Y_oCYDIrON@R&Vd__W*ekx^<%G(z(6R9C0h zmFS#DrB`a3s?AQp?%6r~ZHCJcs+vm11h<#JA?U&pEqy`iet^rC!n(e{_3lwiw0LNu zN3uw;Ix?mrL+N*C?Ne!E%A)(mS9d+%qv@(2eh<&=vARgK%G2yJ3lD!7>{_w9SdhaV z;mBMzWLJjzwwRGXrUKkmlMrtWqY-!ctP z9w1RY&uV<>)}80E+@1ET)iS%3*$FTJvCy~DyOzmaCgd9ugEsH{km5hlGb2!Nb@ca# z^+-%-UCnp$9u7^x$jT1zBTtsXf<7m}mlRlLU5}&8P%M|pp3=ZqL-exo?mm;4=_{7EJy~Ilf(+oG@8PfFN z!5@Hzbkfv=xqsT%qHfc#|D^eM%Av0wCQ8+#@a*}RPeXa~1ueA;ftb=J;qR4dyY=sa z?n14`qn%?VlAEdS!OwHdUspygzXu7Kfhsldv=w`6rU*_gXS5_sKz-=QKam7lK#IOk z2C`xzYN}YhiwChKc<3Gw;IldADV|7fyBYl&IyF<$;fY#Aw{p%WwN(4RnWPGFdg+-^ zB&hfjzSQ-Py3lIIc#VI))L+oe6vzzur&QaWsF2>4P)$ha$se6BPiG|;$05D(*v+$N zoR`BseqrUq&Lp*#mp;p#`eqp>UEXt4rMbm&NM4k+FJ{qeUaX}!c28`|Cc738`B%+G zbdv%8uOVFZZR5Be)vh5AkFH3m4`T%sdnGxD@~ynBy>#iCb?q_BGJ)YITY18Ey!ZqW z^psRfXoOUub`eHI9^)G$wgbm#DZqar* zScxAxlYSAlr6$Py*#yJLpi2q$K#^!t2CH$@Q0B!jhkppK^B`5Iv3)ZM0x(I>R72I3 zl$L$KwsZT2ImAqUxxgxVkm!|a`V+}m;Knx?Mo^+zkd1;ST{)|WKmMaLM$q6)OmZ?9 zqM{W=SmoJE269Sza7)3+W_7WRlO!Wu8_wff^C0txF#I01W@qOlQK!ZYesLdY(KTEB zvO6m|M%n9RYbWMKgAqaF`8$DH41~~>Q5XRTz}}{F)$4k1FM(5xXYbF` zPPF#(e;S;VWhuifM>!qQzB~GzE|xh|dv;lk6d2?Eo?RE=hP=ft-*fbJbohK;cejII z_@Bq*^*o>DPKA*6USGTKBzcr$5=(&Kp}O7!3<)vIKrK;Wit~yq;HucUa0|ah)GFlH zT|r(@5CE3_-jA0hDMq-e+A8o9jIM_#jGQ&-*yEr@1BLki&m z4btYYCF{(6)8l>_XKpyUj_Idz*4;Ha__lesFjK4Nrjrq2S2w|tV||3&j`G~y)5WBe zPWSyzN^kt+U*$I!JA64P&%9YyD;BNsNoc2(v=ereWGkkSX7b0;j`Jc_UVh{8Y#4uX zg22eM`B1-^UE@vTreandn6mDEiG*J|`CNkYDu3(ooK#gA7e~gj9b$Ht1G1>yYRX6* zvctep{L`YU*kj+up`rB01P@g$Ir766l8oNP#G;ZC6Vn9ILylrL2#%5B39{&7oVxCc zKe>f?9;x@sQm7~11!7#0#N@r~OhpYlq?D{%1Q2U*Tk*o8Z`O}&B<_I=$4`Hs!^J5w zLn5R8kEXN!in43lupm;>4I(gfcS|!M-QC^Y9nxJxBS<$$cPI@*H%OOsgYqWR43<|;&0+Pbzek6l`T7G$aTvUGLPaSdzqIW z0u2X))Ri6&*l$f4`!*54p;q%t**SWnoZwhQ25b!blhhmQEE*nxWHPw~`Xl)Hm10y7 zt^`8R`(huuDANjdDTedHb?cBchNc&r7~UGm(Bivxdi(|*qoU=;Z5`UwAA(!Mw7WeB z03R6>lEagqtK{04qJVj)O*imYnL-uAL8`<>(OYQmhTFxHiZj9y%$l zR?#Op%rn0ghMrnYcwi(Y$;WPX2Pq`|;`7Nkxz||R0SUC5yo=|eSeR}qRH5fuCVi_D zzrdu7ep12O=?+o*{47R1MVF74?~*n<&dbht(L|cLBU2u&W7QEwDQ$s~2SQB}Akwiy zFP{5=dn40XyUW4VIln=ufd~f=T49c_oKMJH2Dgpr<3{OZXMM=kuE&X*LRDQpg6uy9 zlcQ0fzLj^wS9G#~Gy~-b#U)`eE6KA8=-bDTf&Afopg7_zXKJRA!4=X){rBdg4kliw z@9Q#Trteuw@~!zH7c-#-I>i?CYYuVZ49UOYJ~W1Mxuw_RKE!%UOx4F06l}&Wa@iF2J*|L%eX74>b?DcoQZSVBV=)7r6DhC( zL$k(0(q}wyj;bUym(B%Nfu+_m$+Dvs7SznfDI%0wBvIc5Np|zt&OSED1|A4R-1gNA z74ciky^By+dBQODe3VR5JNl^=9PXV-*e+~`_@;I)6H}cD`+a0Q^hc6QEw)q;`Ih$I z<&_f}L`H30{OKgZh+^{*M#k0CW!qZ+mh%?S74RThFb>RZkTYFBdvqp_W|q#-#iBP; z#m{(RMUtjIGmXD_C=Gnxc)NMq%>hdHNdH^2{XFsh{0@7-34{(_feO$27g{8&uT(VYO!CfIQ!AQAEdEUZ1* zCC2|Md%S^1X{L9>>g5bo5d`-0^}y7&WI{kN#dbm#1$kY}q|nm|;EMo8P&8w3-CnEK zSwJR|7#O_+xIy2i$#u1!AATu5^`Y`!>*VQb0nls2(%f$+$gp*gV&Tk34L{S^`1LQpnn*yvOv6 z_vKw)%}-7lcQ*oxv8Jvl-$7iVG@}?&49o-1wn8xaU&RVK-lf~Pvu8N19aH>9-YRc) z#Q3Q2E%KCz2L&CIcX1UDCo4b#YpSl{6WbG>c#~yyG7){7vBdb>=vyIeTsnm(9#=0K z<}S*jsF^8OWKlGUy^h2=mNeF0+`w=^ee(!BGhd1Pyg@O`#=)e#z>89_?i~Ql1B<2ExMkph&NO%ctZvN-$ll6Xvm`v^PE< zREayAh|~>_yK%7_>_VKOrZEoj0{9e|<=ixMT8)O~T{*luoivtzKjVlE;2j98v+c+I zKMR2S!!S~8!WOp^0osssq{@hE;?97mVM)y9(b=!7j5lf~o+Wao3~jQBoH42qmF}CB z9Ia45R-&5t>81Vcx6eQI2W+z1bcJZsSkO|(^iS*HDPgR?r($WKXmX9vZRN@U}80h6XBtCQa+etv``@jis;L%pNAco8!7~ zEe8r8dV28<^zwz$!M!#JKPdvy!F6|#G^a$_?>j$XbZOw3z8#5$v`^u05w6al~k*ivz8PfKnsyjr6~qX2K+C*WHS(W{n;l zGo6YYT^$@U@k3dQx#0nE14Gq28?(b8RhiC1pjrah8dl$L1PU)cF^VCI<*2j+4hi7; z=l(M2I%KqcvKW5|nq;eMS>eLhU80dvm}*cbw6Q_V4erXU8>NuB{ngd|H!7(M4wFr^eh*s@z-Wjw7jC+*~Zvp4)UEjp4>vk|q(kW6ya7h7+PG3X< zpmm;c+cshu8n->FYKlPXAJr(Vt4M5Rl$7Nu8PbY1t|9!8hJ#pUeSn#EgC-Hr>!m^< zugk6B{V2-Cnk?aa;B!%@mJ?G9X&0*3{mo~bVQ&1~&pG0%lBA6o+fB~rtxWYu;rCX0 zi*n3i;BUH6oVq?MZ(7G?CUE?dd|TYXExuq;XIL?oK8qR}juwV-glgE%3rSx?Ee<*> zVy)w=K>fu!`$+iD7`dzn1E>+!c_N*UL>yaW5~9&Xtvv zqgNtb@;@@gAvtzARUq8&gUfQ{wJ5tr#J)(;QMt?Ed}jOc6KT%A)}WTK*3*|aPtS~W*NXuv@V2J?Mh{<6bye4cas z?tmAia04agWpLaEg4d*GeXz&dQ!nP)87uM==>ZHRWteKtzNp|gtE>g41t#Br2EAB5XB5z#f6w=?bKsSMgBl%iO5mEmNwcmXh9QzjTUFjS}_lJK7CA`6~_($yG>fa!vlbq^7+dA)uU}VS%u_ zxhUo*CE!&~yo{zYuykJhoL(Pq+m>I=(b z%WB820QCCq3;Me6&A(s&jjWC;ffAtS>;GJw9t}t!xbk?ed{~?~)zBn3d{0kjz{g}8x)wR zb2}F@=(q1?s3fzMO4D9Dbac5LVzp89nUC9doSp*(s6a&;kh3=YS(@|I^YC|c5vaqs znN@V$=+^hh51+!c+cnmzX7cL|sQdAKs}XIbgdR&LLgwdlx$;_DjhcUWg`SN(fefL( zAs0z!n>SNvxNhmnP3O?X{$U65jt2@Hy7s1Zf#?t1=yz9m%eIM>2P#3EpU9)g7cNn|dG@Rcj#vIsKZUJ?0C+zjm8}*7+F|qTDgfHW(v;RpA7VHX^ZxQQNIO-qZY z`eBSBx3n5Jwc0v0SNNaqk0HBmO< zMm@jpY`KIy4jtdkoRv?PTaF=Z&bY*q@btM(+K16GpPkRw9wHC35hC&pj+F_K#;FVB z?iL-M-gD~L3k()w(Vh~~j5~%Pbx->nOPYoAV=G;Ftn_0@JF`?GXxHs~+*A;Yf6ThD z-qV|MfbMq}Gv!R#z`|zlULfC3mB4i!Kel4Jiwknk`!dbr!L5jm|N81GPxL`N^w!W` zLT~X<-fO&ga_@Vs6>Ppl)4o04J9%}Uju$sL{rs%bK@oZB;G_&(;tNqO`4)8 z6QOQpguQCHv=?$@!Z2Cc2y#{y2j+~Z$g|Mf=!%MOPHu`KbP3lE6@4A;JzOk2KA@P2 zUctt1eQL~XQ+rya=yO29s+QYfOIo2Q%Pg)8(i3K7^)}m$s!DagsPwvxl;7dmXLwdji01p7# za!&b|lCd^kM$D37l^UuYvLv8}2CYNhvzrur<+1qrZl^Ra;70N7xhLHExw7Z+*wXP2 zG&9^7F&~>RUbu*4mUHAjK0{Mdc@Ra8Q18J;|BTn5-np@)+cd;_oltSu~pZ!^G*2iWt^ zfTVNNXPaa*ZvD`p4>dZGuA{*9B+vJ(O*WlbK0>tKM~?mtI|(4x{a+src6fon30)?6 zF&7euf3?P~@8aj-{!OJPww7SA8*twlN+6wFyO6U@ghshcjJ>ft0?PeiEo@;1 zTM<}Q9aNEvFi$5_R+giOD>s33yMD8GTdNmQ5B>c48*>TQs+ozvr_Mb`ZKs$tw^B=U zjMml7FG@Ri8V)7JSZ9d(u65pk~1k87cVlCxU+C?7M8twCYIdAxjm)y=_>7jZ~yyqTgX8HVdDuc(L*YiIDO z?mb^8Tc$NYe%1eG{KV)6XN$8$Vc}DJh18NiRUeavT$uLWh7B_ABONKDvO)Fllgogd zN*C9H^Bmg{9Odk0iJ`dk^jA5m7H3=0;%Phry5y)E6P!<^eQ7ET#r?#Y^Q^LB{MBR0 zRH3XpnW{2v8^i~>+U#smp^#BFRph;I1>XD`)Oz!<$w?I3^7`doCgn0<`Za42Z>EZ4unjIfh)DNJsv8qjOdYMUpyuoU+< zh^d}rM2ctd@TJD=#Fyw8y8mcEH-LZN3L{HeDE2{`ORHGo{SB4vPb3vzx}8sZ)mLA6 zu8H<2Pku+=zc*$ob&iV88rT8zhKfM>4vhJ8&hzw$@1*Oc=(CSy=uXqqsH2Y~LE!y& z6Hv6%&bRV$4YjbVWUH~nlBx?9K?G=C>z7|u>PFJeJE#bqO3r@}9V6A1^k>JWd?v^? zcsMIb4Fh86;O0{V-=uN7aHgnwa$Gm=$)8Uec;44DDn0hv!vY=6_Zvz?xUIie;0j$wj@_KH6d-!t8&+<}>fA-MN zeZP^+VT(SBtE-Gg;BAd=qvJ@AS`B>Z`s#LGa-+9&}& z;kUm#3Bpe&4M1e%Zz{gw1HiPut)2TF0II(nzJr`n2q;Pp5ws`<*{MIMg7yQXdG@5B z@A25=ThZry7sgQ5_#L2p0aJ9}*&RtSCIFlj ztac`efAF~nYotKCVCbct=fmhS?r6@K1cpGL;#gAxkAoh=2rY^KwN)}*@dNh&f$4^; zu)MW!PF`N%#V>>nzw7a_+D^y}WR{k^KQ%Xqr>cb9?IdpHi&+x-P&FL+jLbJYF|sA= zFnXJX!MK%A8VFKXM(}ck+#d^Zy>kt?Z8VxxauR**)Rr)ru4nA5M3OUstJbKD5xuLs|ENCz?+iTv2Ix`%3(@O z$`D@YE!4Wel2c>MSHC!|ko7r#q5r)E<5cx=Ys&W&9;VUyr2g1}Z1`=i<;|V810*&R zo7)ME!0Rl1=#_ zpY=SiBNX&H<2pCsV*w zs&DF6Nfx`4Q;|Vh9`VREEScu#bC2&VT;*TmEy!aq2ITCi+A57{;RNR^^B<{tVL_(FRs=8q{Z3a4bL%l3<$Aq{7Tdq#AnhIRs z^ZuIWs3GrvM_rMLS{|7iR-hd)gG9u@xW~xI?}A<0UXcuZ44I983>Ij_&`$&Uf(amT z@UR4kO^gzQZWb>N{;HtTc0D!Uu&)fc-KG)I0M;8tF*s!=G)Pbb%%M8w+n{1~GiudJ z7c%8kHP6{%ajvKU)5#eZj1gNdhDbhi-?ly)ZDgAkT@^{jgkcg39T-v5(jH;)m&f)BP4S1NG(Q}axxh~%6$^+`Mk+PHo4QdVi)mM#}e!GuyS=2WB1 zy?%D@tFvFa=N}G7Q3j9syQ3k4vRvl)H(e>0|M3C3Zv5A=tzc9pKf4PiLot&~7N zFdSu`vz%=d0e91ZuS+AR>a7_uI6h*gj`#2#U_^?Ah0A#KY~Yq=)Oz284RF?qRSoMkCxHj0>SAJz7-FeU7si z>OIXSj?VNszh#M<<-(S>kN02vcjVx%(+KB|(^<;Jnzet&JrCIqt6fibx&W}#9GXL= zDnm}0|L-&PC1!xSTVyacdSD2MXKwVgjrzTN<%>z=nQz&rl1yz#mQM9>t#S&dI{ zfnERN&hPvTE6>IlWaVjAc1wd}%~51%as24q@6SK+2j8iFBrxad_lXAYGYXfanNaP^ z`Q!72CkQ?4zCB;z2R`qR@?BBRa_(6&3(wm7WQFI5pLCo$EZz!Y%FlAf7czFxj#Wnt zklHt|Jv?}iM)6U~NK+%vI5So@>~Q+uJskX3pD!MpZsx zrpj$6V=eOH+x2amn8X5hEZ|)y9C#lyIVx!6Pb#aKt~cPG51aefIEfGaOU|TvE=|ogx|kA3mP(I zFCVl%csy;#u&lSa9<&&~GC7O_cmF-3ByVK|t{K(ios zgJtph*swwryO^@W=Hjxi%BqX$NOl8bOgf}PZQT-c1O!K&W==-CDZ&b|?$wa5MVIlb z4o7fk3yXF?Ygwe4EqIG|-7M*_Rry8yF-zVL19I4gs=TgS)7C+a(8x30K7LsTp~_5Y zc)`vSh=c06kccjscmEe{+xsH>5~oaG;W%;g3bR}w$1EE($i|&{ zPHMlH@zc{24vX?`oPGoVk zzB1D|+>)qAy6`}7AqrBK`V{MB+N}G9)A^!DFveGkSO5CTt5(Dx^5Df?mx7nRn%%*dZigRoeH7kcCIULt6srd#f-aeWXkc0m63f}+`^r? z0c$}!bFL8=hxS`j18|%!Cu_0b~+!ft7u#!GNWowh{!7hJb<$W@U55vQFE?@`cpVW-MXV^Ix{ zEJFg!ylmTy&gPA{e@M@E$fMHWflN}QSyl;TR_%ck#pzSWg@aPBeK=LbJmVw(Q#B2Y ze?58l374`$a8aF(=9rYA8ymVR9#XQajJI%Xgtf|k-{&=s5xQbv*cuZP1Ci_+ETQtW zK$C}-PYhQ@Tc(NO60t0^kD=ZXjcR>L{NyUai#yMns8P&sP)PxiNLKT;76bu7Yo>yRN zb@tdaZgJhwvW_)ESDF*G?Oc=rJIkrx?Uxy;TGCL1P3Tg>EahqK*?PYTFays|wE;~% z1OcztmmM1~4$Ie889|8Iq`P(_*KmBBgkb@CIfZk{gSTEcKRdOlO)SWDr~5<^EX|n4 zz{7UCqDp@^e^~S?$3z-gL9cBjgHA$1IXJ?+Ti!?k>7HTWC5vOvO9xxr>M8d@=kjf| zdduNc_l83YlrbtYIw2P(&FhReZD4R&yAZ7=5{Cz8?$6$5s$>Ca8buOeuYa3c zU2j);fsfnQEN32H3c88a=oB0;^Us2I{n89g5EmW(Lm=AWCiqAPaoU;T@V4j@_+*I& zfe!~<&oAzo2=Xa3Q#6#>7O!M=j)}h=^ZnWMhWWTB+oN6pWxL@!pi|uOA_$jH1Dd#u z^T1-e;~u`vYdN@NnW41lA?U^{U>PNo@kkUQ>em?@@@Enbv5-PCmFCO0mHKJ&9d#6>Vb0ike_6jW>fob8 z`3B{kOX!o{0r<|4klc#)i+R2C@UUMaui03Z6&}=HX%N!C0o7A^50d$px~7HEz;D*l z1e$xwFEen_{6IY=Nx`@Q!)C{jXs%Qo<}4F5#R@Jf)Md0ID7dlEisPyJ=;Vja@c4T_ zW!vJPVf*_EuHdxSY48*VLWTQG?^lF)jc`YZuvE;Pu8ATo{A>B>CIiu_1Sf0`iO68q z5&tzOqEXO0d?XZ~vIw41q(Mb%HiX9H~BbJ$2h;#c@y|DryunrPNoUNX0vYE zOBY|iX15;F%u|VHbt4G7f@c`T$i$yNO&U*n_jx?G!cX-X%IGLM*bts^U0Sa=V&_q7 z3WlQy#pR^!+gyK_SmH&d49(08Idh=+8^TJz)fW7l3o0t1&wGLv~$n4K`6+`3vtPpo0pwP>Hq{dobQXhi2108`K`pXn7w3+v{De0)5MVtFM zt1FEkUsq)&pSU_CHLFkO-AhsTSHn8s4*-v;^e3QS3Y_94#?CL)Xzb@H+A2k%QGhVV zt*@vTE#WEGvCdL}&Qr9=|9wG2_J+@)6g^h{RZ>9bX2wa80-9P>J~Fb)W+A z?-xbYQ$LCcu?g}W^k6)sVGRg#KexLN24FQ;p3(e;T(aJR9JJQ8Tg;1PYRQ1d$?E za5Ws7w|vx$T37ro(6EXs`4c3YqR0pN(mydDbk!|UVmmK;Hg{6Z_=DB&?N$ao1TQ6J zM=>p^lOd@^7-B@F1hivn3(4Hsr{BM9EIUlz;rAhscJmN3i;+$I2BFNhF56K`BN_*0 z06VPMV5w$*fml-%_Tm^z=JPvopNos*mVoPH4C{tN1D`{MLtnS|of8IC4$2lKj2^hlF^cKO+g$SvpEVU zhy+r=E`T{5SJTJZPw+IH^xk|QBeXseWrx?&i_Tws0-8KSAeQbr@-x>2Kaz3`hg^C> z?TQ`_Yr8w)AR?w^{ZStA*)Z_c&~T{}16BeE4|sr*6B*Ntb6Cuyh^2slDIkl_ZS&yz zn4n!h#ktpgZVJhmNxaLp2Q2*n%pOQb7ZIS^T>3E`OrDRTIhBQ*Ojg?0rciPe-o-K_+Q-){; zjmV{+iREFDfIPfYAX-2J9PJ0QC;p54pi274W}ozf+MrGty7~Ftc5?%LsWI=oK9Wi0 z+2SU#k!3Jp`ysSPfI#hbmBc^LObavZeMYBQ{x8sHm)QK;xz8O^k4Os>T+|8R+^G@$A2P9)4HuGkPm7qM+C>ac8j6NdD z|7QU%X`(YCAv~Z&#MFUgjQC+lY$O;ZY=vSv@0%p9^u(?;~KaVlgF}%IKmM7@k{Z5x? zhlZ1{aYAX~Ek+?0VNnp;4`|M9_-1Ioiy$2C`uHYX+v=_imdj&}w3q`J*1uSPFhDlZnH4p4LEAJ& z{Iy$L2#1^&itw0-ekr$Wj40S8R+Q2*b2NLVO5w8;S*V}nFSa2J{(br*df9?zjHnFG z$7&T5lMy}-{R!_;y20G22xB5NYz1amPxFe!Fj{S~T~2w$T!G9Iy3m^4Bc?%w{gfz5 z%%~~uyO@ws*Qv;g6qt%S=x;r0d245~PB~g2w7Gh!u^hkR@CQduc~vIfaGJQt;Bb-> zC!^tW*>Ey)y_>rB{(!`&w=qj0$Nfk64{z}rB#zdu=NcRBz6CzlEyl=K1HKQXzwt5A zaA#GMPc1yJ_pjfsfs^#@IM6-N>-;<9>qsAONli?jar9?TeS#s#H8A0;IX2nll}64? zaY}5kAA;gPeFZM908sxr6!`Y^KWskTYGj;|+<)U3^W989r5n@?fn0uNI&=Ge z^@V&1kIhwE=ScNxpS-JDHfP0c7Ne3@=?1}-M5q~BF8Oomx2r&QprXlt!xe>Xuy5;P z!uP!MVZRoD(<%wXd!qh)o&-uCL^d--V)ZxfKS1KW%(M|Sx>T6fZrF-BhqTFM30EZk z>$aVkmML~N?}2pxUc@VlpVbmbUgv8`0{E)zSvY0io=bO%ef`Nv|2Ns-~k z0>mm=q2k8YgujYhenNTL*Bt)y?Eu*&VC|Ax?%NZU8BbF?*}VF~`X}&WoQBl@X6@YH z6Z=d2wFus6r0y>Y_vW!4a+g2yvEBbdNwDCOFS=Zy@7Xwy0)k6~8AtcOz8mzU@EH%c zD`bGLba$5kw_wx9tuP-YGj7q4B2~aCYhH?7`d#?{!on-hKyg@b`Eh#l=gy$%aT`G{ zLH4$jp%1M@hnb@+^_{%wW|)Abg}|mo1;r;tFXi{Df_xgK5{|Cl)8mX|3t+{r%8PSa zsn4eq+t0O95P#qN!1)%{T1|((eeTOpwa!WY+-Sl0+Eo#ptUWnzFI6cpMEkDae0cfe zeDeH$q9o-)E1IA6h42A$xGqkKsERT-PCYJ^q{UtAzuA@BCK_J4Zc$E>CsqB*G{=o?ph0tkBt1?85dr4 zqg>S;RRY7EwA=32@G}XmVERULAG;!yRD0`FLu_=>L%yk~dZ7R))a4DkS?Gy4cjKB} zUf0z&y4sl{7q|8FGhtavtW;M`iOKw#blQ@JHFN8%*nGg}C6;m~mo7<%U2^l}9M`BM zwg$-BRM4qFoP1{H7qs7vc1*za>x)c;mg^jQ!`ZI-k(xnkg_%~%9ZprEjq^_MOk+); z@37S-a$>r@76FI)Pf#d$Tm zws&zvA>1pGN4-Rj%Ia=xSof983)2L7AuR&zhDb?!q2O;Zn>bW|%-+fU-05o*0HKFq z#L=aHUP$H(U2@MR@ZmiAHn`pUrIr+4oG&rdv zXRO{*ek|FUK+ooPsg#Yw0pUo@52&9}3m<@#{l# z4o_xkj!R;$BY^&jj_&ioyM{df^IIO1U2Bvg9;h+66AJyS6AA~IPNgx8HY7fWT>74^=}s9}8r3HjXp%wc!{h>5quyw^h@ zEJ-};b(qPJf6OZtTgRw!P%p^AisVX?dC)~^>8!h82E?dFp5=!`rSbTDLR~5fj<CE+c3ZZfWXglY?iTG*J!A*EYufxT@0T6XTq__E*mS#5qCJi`&&`JJeKbD^#to?Hq zYGj}*2E2a%Imp`GpTaPrT!#5RHlb!ObiuQH!NKcUFEejP)jv1ccP2)U=;M@k_HFtH#sn-eo^m@EFOY-T3=?-ruD_}EkDVoR0 z?_B>8wrK4$BSyTxYYznH0k?}U# zY{p=9z8~lFJda1)E*U0-#C~PLKZI|{fxL_FI9HLA8dYNk9=C=O&R`$UXzU*=4N>c( z8dR`cx~ZTHEs_1)JIxZ@VsfQVdKao!uHVJ_TdXdIBvBt8ufU3G}ow1H!eXJImVNS~vu}>}<8ybfexNpPG(uV{pnlwAw&SK@A)ST)L zag)fRrSbfk#=K+8GiAv$M^>B1`y=FP0o<1Y`O$iwf(Es7w4J%xCb_|X4azh&u|1rL z=C4+7&UIvSI)qRIQAsDwUP7sjkjW0RudYh`x2?12=hL>Qsht>gTA9-_8NPpgsL`%e z#_AI+swtOnn-RH*;$)}`K9N^{6M)rL$%}#_tnSw;DN|x1O<#&+98AiQMcX|XU2J*J z7NdHN2i8QG$bU6mq6F`WDDAt9M#PeU$XBJE!GvB&nqg<*{~1 zG;n9V1Y^aNW-@~BZ^LHalyb!zuaM zA0(oZ=3oXO7kywy24Zh8lcBTBY+S=qHv?h6-f@=&l8;E%LS<`GP?Q7>mE5_G$4itO zt-?#6wPEWm$TV{?#Fk$>A^22;e6`@iepF8;94xC5o@jCHikEkjmJTOsR)eI7q9t?; z;Lt;YkK9zn3W$lI%nb69BpDn8orPyIwJrlIc|pSY-ukwdkw*9`y!PRu1PT zLGu8*G4;gjA3m+T#G~9>%!4kbOPKR6aJp9kJsXB7e9EJHC+B{DFw|wip$alDbpTu37RKpNQZR;b2>g z+)4^p3L2%UdV9HWh)5p7<9r>sxyicA8#5T!-VJ&k@*x**qe1wW1gBzU>l{vGVagLprA*T); zXwpSq*zo=FxkS>oGgbyJw!(-_IKw~JoaTEktNMO#fBQMEv0yd_*8yLo4OLVApfgB` zS`nu>dHuRfOCH?!A5I3O)+c+O*3Of6Pir>p0ON;iWYQQfLva#q8l$G{dVy()Yaj7# zcDq0yG<)2lWtI6oWCtmU(*kqZt|J1SwEN|5HG6zNBJi#dXdFqW=Tn^XQ7Sf#1>UvY z-q-tIN&SHy&5154!Z&5ou|#s=PR}<0OL^K#kPJ5tMw-n@6KeK-Oc4UMQv68}Knna4 zVCb@7$Gy6pE_>r4bV+|r(g*Oh9rkS;iLk1)qGGcq!CMQ*o8^XLBOzCHg2SX?If z6gjTgZu}K-fTc-0GtDEgAo-821AdU~3#)F9y7*P{%F=8kRk=`AoJXiSWU{S^C7>F2 zgNoI)%rri{pqLWm1B>m#Vv;nfqvl(_fN*XMRfUT`)0_*wo@SC2ESYZKC3a6?`ge%< zrYis+xKaJ%{5FvgFAcK_O@4GOOVG(td}PLnDO(e4L3H0gJ2PwSIgBN%rrW)?7cq@2rF_Rht=rV2TaXE zW;u@p6&v(nzq6?o`XBl_pn|d*@tyN~Zh3v}4g02M&DY7yZW{GGIl*6VMJ*%4GPTLF z4~Xt9g4o}N`3f0CSM4-z@E+YxZMwiKf+^!!=HB9HY-%pDM&GeX$7cyXz;wh8`p{8a zk3D&Fw?GRqrSe7fNE8d#ab<2>hYO<-IXHq%O3O!w>2AY?D%5NIqGcg66ed;DG->g2 z=mE~(1=G@`2uB4chf1ZuZokqaZ<;gSjd;i;;$t$OmO$#|c!e-Cu_Ii*i{O1Q{)09T zZ@%zP`9}bO>CI@_*V(SJ^#YL(+|leDXO8AVxri)|?KL|}#YZY!O6#LoH3Q>pt|L^% z+3?v^+c9Cf3ta1rX-(9KQmLQAZ)KOnxUV&0Le9JI3Zf0agydMibJi!aiXAo= zgo}Dt$Fz|H(<<3!yq!avDyF=@94IeMj;`ANvMnpz^St4x*K-?V_?m|<>?bUJ5=Dhz zG74zClX7M{tJtKek@P2%U32dk|J<&HM;4)xGBaiM2ue1?2K~Dev3UzRg=V)G?Zmf{k`1zHGzNr` z={t9_&8?#$1jofxEUvMa0Q}zbyvGuFS!qa&hQHd7%tGa%kHUs^kSx#cHUyWxN1zZT ztT5ZSaUL_08c^W%HgHvbEG9$s%Io`3^O4FlH8K*Y(t}3CfIMC-4gC}xXPUn}T?@Yf z0i}qsX*dh=TM(K>H^AQmY#n|Dv2<2@n#+H^Ex;o#fmFEL=k^Fzb@W@FGu*$`u^PIY zl6Lysy$>9K)Z2Yre{mqb19-~{FbjSBsJiH$Ew7kQ8Gva~((A3Gq5`Ku>I4*>Xc~68 z0*4XsDr9q&iOPIjqDrpUSh#;-jr(t5{B+*)2BgeYF$AwD$j2@@P4F@su<@Qbl+zv- zHYu2BJSvD>#Yxp)=gp+O&Bvruie1@+N_>hX;^cKRLQu@jrY>M-bRJXlkv`10tsJqX zF{k-;Sowz=*uT6LpRD!lwZ?J&LL_TAGB8k~lo(B3t2fh4o45D%XzyCHyocUFf!%TL zT7&D|+ZyTpwC$wNvz2ypRT`L!>H!p0REEIfsBR8hCrpO<0D@JY*`LlovLFxk^22-w zn-mnzHShid{cv>1)p;AX&h5;X$qU9Ky(39S{K8sQ#YZmeHFdVi`%#%%isqG@eo!;E z*Eux$h&A{XBw9Ws6{psohSbXFUx+XE7;nCf`3 zD5!Q$oB5T9;Q@Y&l?KvW90l0ikxNsH`VW#(gII^d6LXTc7YDGoRsDqu$Nk80XApS- z#sRfGd~1Pr6vOp^cUC$j{-nRHksTzx*PIY=#?<3=6jDl z5r){iaL?@3ftoV!mmMDjp;C`!;w3s#5K;FHew&xd)XNzM_RQv$f;oVa)3K*oifTV@ z&EUI_PBqkb25E5kJuB~PzA~NXjNu-U$P9uQHkJLQ{L(*{3*t*=aiS_kn=`HA%ih)= z55dh9;L|yZAeysdGky8+WtvHGOyk3`L}E6v7%i8RAc5IPIBJd*7Smy#xurUX=iPb; z8Cgv}r?u&ZO&Dbe(9tL1N#>qcS-0r0tnSnWg_7Q|pVYp&QL#=(e zPioK{yKbpDER!`2Uw&D*Hg72YIo^*#E!-zC-053 z`%;l@5Rf+_92^|*^NTRE`yq*e`3_D!e1RC$Pm*3S&|kn9NqO)9L^A!K|ITLfI1UoH z=8o}c{*_aVToIXuSf_00??)8|d)}9m9v&@%Ur#g(eP` zl}$wR2MZ}rA22fOxvjg7Lq%oj78M#2f>6>jJ?dxq#K{)IsL)VIweNGvh0)`T_v^m9n zuQ@hSTKvO2Bh$fT_jj+19_HV$2qNEnV3!%xaW?I4s(b%z@0iC^=}Kt&-G?!ZroZyG45Onhnxy08P5%4TUS-e=+JenI0;XE+jJB?N39T8g={rT@Ss-N=Wm}Z zXZF5lnm4Yt9gl^HQ zU)74aDbh4tT4Ah7Zo@spCswdvlFjRH{Va06VruQwpIz?G`Mt{}swwR&5|JToyvdo|4YD28PTU zHO0+88v^@waRo0g*zt8w+u!hP?0%_z``-QQ8p+ERM!g2!IBtopHKbC$q!tXd^`5M4 z7yUdksq(jl%69xXZ|`^N>kM3cbe-~TP5XW|wzLJa@XxlIWt%30s~FB+cUj$R85WT< zqP~$hTKgJ~4=g;mUsS?$lstW^gl|1LT`KmKOnjPhk`t61I%Wv)Yj2U@#E^&Ui0s;lV@?t@$h@Sog2=}n*SugbsyPw*d9iU$WKOtY< zx>)vlqUiahNTOnc3oIC;V`qfhcg%3>Ed&RzG zlmMm+C^`4lVgND}M-oJV{suDLwf9zCP+M)+g|Sj3K#)SvqQ%{j%YjJliE}?jFcXumN++B)) z=Xqzo{LD-y$z*b$v)5j0U3ABC)F`l2m^@ANciuxmM@c$iW%{V4t+Px4`RNXFkzSK` z$;9s$qN;K|-?gfg6T3H&xvE;9xre>pUh&`G@o^*@HCU)8=r|spao<7`elhprwqKOF z;|@tRrE(Mq+aDRN4N21980#74=;(PBE8)Pw%OfQJonvN0y~$Lcfn=Fz;_ zPQ(gEoLWdSso+i}%!YbGskp4B(x4Q1t0!-5=o{j{7b4A>j z!VuM5hnhoS@f44ck@6$5!ElYx&gN6*sHX0b*yb_t^1M2=^-IJ8RTd|FiWgGtAU2e0@@X&<7@Z;Np!Y)sVn zeXVo3I6rDjUC2cAi6y}_%^jzr?H=hO4DfnWvrpFwY50*3v=M=F@y~9abr*H%LLO5L zS^Jm5I<)lk?)@oLv2-~Q8|Jb?QmVgX3j6_CG9E-qv#foUPGU(!GRvpAmv32PpU`FPk^Udu7(G>=y#_zPft~yfGoU5v!8e`>NS}$aD;+K zEXuC4)Fdq~>>zmmex&|h_Wd$Vb!%sAug|@#RI{b8t7=mT6O#-zY z$Pk>v%>C!>^=UqOCkv?ZocB$gscl6H3*coSW5fKEvHvi^Ltpq z7}NiqjXeR3%z&RtVDuh2=cxVt)&B@PnZ@lH z8c4_Z@v{v0VEX^}0&x6ojIGbH6j$NifZdSfGiIU;0grUF1fLr~Mf(T04fBpe0Nukxo5IEnY!ilirO=-&a4T*IeawfEahDw?wGNd0 zLxEB zI)@0f#6A-T$ra7U88cNYnKmN2$nZ9~l9{PG?-JgwoUny!l%TKy&b}>a zX#U${)A-rL5$#^HKYX0<<4vVPb@qlX+TH(0i_)kFcVGpkK8c_-ogl8Nx+r7sffeh( z-p@5ej4`>HWt0IzplT)FpzH}nz20qF!Ji6VbDr*Q&Ok=T5@{jvBdp(SMY-hFw#_Vx zjIgO3Rp=_C-Bbbd%*^-jvm*;Tl!N-MTR%1gCe{o2Q!HQP)nuq@tepy+hMu~0(o_DK zDG=w-r56e{Kju0H;O2H#6LH7o-#w6wm!&SkH7`5I;mUvLv+JnYN*oe&neUOK{UJs- zm#S({!;~Fvnr~Ak-m$vTkfys98*iV_?v5i4^0E0_)y7a;K3dc=`im}OMRsGJwwH|= z=@`q6Od%$xxNz4H)&1d>7u0J}7r~lxVx(TNSf&iS5QOk= zp+=vf7pj|NfN_v1p})^{DeG71wYOwrHOrEUH_G6I5mXS4YyUtTue{xd5a-Wme+?a1aCv}mF@be6{b{pb;JgX!$9 zNUW;GU|yFc#j6Saw>JFAoMzu`UBAmy<0Wv&i*JEBVJ*-Vf5{><7myy87eLD}rSEN) z$0tbYO-f@RieZLVmYbL-$x>y6O#%@ty16f}Fev-Ve%w){FGW<5l^zDBGXD{2^Fx<; z;u$!h9tLw?Zh|n*FnZ%?9l0`DLU}@sL48>^-g^R*VZCQgJW0(CyrdOb+LW7$`(6{g zW$e*4VBEVWLFV>R@)qkAJSLebO4$N%I1lw-aIt{$X2r{F(|P;%*6|t`n>SNmcTH zF>KoX@i+tJ7eNgr(_I@ZQRdZu*Us`}`9ofk_8_Jss`_Jbb=Tt-b;G==Yu$Ui{g6?( z>~h_bgI<8)(wUi=H@x=O4POqzfW$;Z z?@TI2?g*p&0l zs3$mOvLIkJSk3jQT{2IN26MHG;E&x_!YelC<62MI+$!q9ba_M?rxb-GsYoMP>%3SxV5*{70LT95p+?Ky@>A+h9{S!IJNNVS4=(0p~QC_@`CyH@Lqeh3@d=0?>TbRF+C>ho$WNfyS<~efIJy=VeZW;!NK3T~rdroCqrB{rr@Ni#lEvZ- zjWN1(JEbO|h_ch#o6Y>xOQA<`M$F_u5)wL<-m(C}CK3{T*aDs1?ILQ)XUcgZQ4lK_ zf>iYYyF6!FdJASNEZ8NtFJ?Z&ik&ZzQ=6EJiB_~V6qB*wm{-Lg(?x02!wz!#4%KB` zXXPG`9FvRtKsXBLFREtnd8)qlXlyj)lPc}#=)RIHfsJtl>L90WtBi1&{`v?n_j0^h zyY{wg%H}lf`sg+(j@fbyTl_`Bb`6TtX(q0Aaq-?dB6)Zk zgoF9D9X#^00+XP8(ZVSRY79m+W_}S~J*hQ~%?IL^?4Hj}k$pNw%hZA%J9b8)XCYm> zGT-X)%B9=Wr={>Sj&zL}(8ips>_2^4i7=v<$t>`&#(Y$Q1}UP!+P)ke{~|HZI%+du z=crq4L#G`KZwq&dt8vk{q2XN2b7z2GIZ+Xwy5ys*WZdF49hH|E8*}?7eLBT)&!#M} zC{ZZ!KBxAf<$NO+9 zV#T*yF0w6l0dp9oB|#{e#JYoI7Pd3LdS6D_smZU{IQOXv7bVG(JVA-(vOydpZW2;U z3aN`tSwe*ykHZ3ZZ8iMbm=~5YNRm-@j&vjOo%e&5>x;dYG;Kt%Zqxa5rr)(AP`b16 z@AMdmS-lTiOEg{@Qw8w4h2T;#0YHPg%tDVeg}{wt{R9eAJ>GHVHY_qR zF}Y0CG^rZ@t7d_7*jantsF+!_>T$p>nc(yt0>f&3~ZFMDjJ{GZCqN9Jtjk>Y&vyVKtf1Kb^# zz8l>m!@;4(TlplY+KlV7vfQ`rF~8dJ7rW|SPf~Gzhf|~JI8bLeJyQ7j*2M__^2iv zq;vD>`&h7V$sPXfBX7@-M5jbQP@o}E28kvS{s=d&)BF&EJVPd^MW{cP>L2fmOrCJ##vc`R+KusAi~P!we%N0w&R1v;;_=bl zt%hi;&*~481v>XWH$2*ZX;ApO$`|Xd8156~WCnf-!mQlthEQ}I=SL=e>uBjYu_1ck z%(xnNOMW*G{X>QZmt9siD639oDkfG>L}X4Eu#oyhhTuxqzFS0W;=F3u>dvZq2QW)CI!l90!K8d_Rf;$VHP*qRbj!v&}2dE6WNtc97W zWR2v5e~7%vwK>%5P0AJY7vWz5vGvqLE9(G={*SjZp_3UT=WkQyWA#iWtPBn&3jPY? zIoI|b^aceV-;S5tmfd!k$t1eqQZZ^WA&9$2B7)(%7Hd&e%Cp(aS2y=hKmj+C552I2 z0_{%<{PvqM#Uz1&_#b1}wNp@h)A3-MIoAOtXQ2O%NXrE7)Dt_Xo`5%?s1;gtpVEpD$8hZ1ZZpxP7YiB+<>Q!+^M8DWFr(^Y@D2!;7wC zp}@<_+r|0oRhZBpJFe?Cv#;fYC*H%RH{bAI-`c^jdGGj>F3(^N#Rdc3J?qXPqM8Q)bIKvo11X;0bH}aOvl&%C~W8VK(}k|-is$!Or};N znSz_gm32l+$W{G`tpRt90$K1-K&_NcY#nF}Id-U0zji09&WV9P4kO<{ufspFOAaeN z{F7CE#6I|vg zl8OYC|EM3-xjt7T^woAvtJVanGGi6YdDVe&;a_8?clhFkQPq<}z#gmRG}OI;ij|*1;#;un;cA63tw&6xiY0gL2Zx&Sf;ty3Gcpcc?~IbsiIM z8_Q9fQ7)A2Bk{;Lklgy`_Ysphn(Zej7^>H9dko^ksY>)XhQu8sF{#Xx?VNNwFt%7 zdM>Qz{=UIF%iMaL4((&n)eL)rSWOx&Q+aI6CVsQhTV9KBHc3S{%y1t4@9);pDs6pZ zFS8a)Rj|lhOuJvHUX`1j7U<$EVX01k6ZMZyjGL(Ht5TH=LcwFZUu2COB$BI+L~DAW zGs?|lHSUcoCS~tU^Zt1`KKwzjl&l%exL1IaCLaok;G`HW#cRV5B9Bk1`M&zq;uEGV z1I-U8Zqx%2edw+@!EOTk$F?5=f&?{`fkfgaL_@5qkB@EPt|x<0I1Ttd66NeExL7Je zC&!cKEuTyUsxVPgBC3Q8k##ZVKQYrXUr>oAXP~dlZrTL0bqCrTm#0TF)7~lhi4fo~ zec=*ZUP5U?#VXK`7^>wxx-?dA=IDV$ur7AD@+WD*cH(CvlF9HkxSI;owCAKAgWJD- zWIl0qLkwnNQ9z6@-j$^?pY^ZBhQfFSq?z&R^<_q26?fF2DxCVrM@6KwK4Ga#$EAdu z%G$rfrz}uYsEki?F(KRfopY;v$iNhu$}1D(B+sQI5n)1MIL9t{csu=>q>4u^y@Do* zm{H=iRs$MFZHrBV(q@g6=pN4Iv}Tn08|l+xpRP^p(9ad=Lu%Xblz~9A^hb8r%{)5Y z9b6RH-NGz}cZfZ1TN@y*i_L~2+6lSQg1}Fps)@pPX(EA}SeOQEw^WfsphRXF^Il5v zR84M^pOk0V%F7huYj~Gf$Qt^o_S96|$vb*n(NS_WL&pChdctV;MDpt4MKT z>N%3q-nvf5Wfoh*teBd{WpPCqXe5q#a{jv~K@R6nnW-Be)4{0`hw zZF#h8F_sMeY4f@sh$h7NXt0}AMAGvs1<&j8=5Tv3cHiqWKff-*yu-k`_}YB-<;m{b zTyKyJ^S&N>43P=IJO_sfv$}-KhVT(BJ2U~m$Yx7NR)iG%Ai0P}T`za%!#Kdn{eE*p zp&}_)uqGF=f9BpQ^m3^95fJ%-VV{ZLy3ag+H9QGeoiq49vt=!;p>%(u$m`GkHD!u5 zK#)YD&7LC@(12Yon%F=WfcmCpl9nF$0>u?1Q5gmE2Kt^sDCr7>9g1zc7AwdqWBO5> z>1(i|1Fb)y3kdGd7qL-h0%$b34)l*mA$|pn4v(stle-(YYWNrt5S9e8f-7XC9K0_R zDfk1k)OrZ+R1(u#b&6`rwCOPP4SDmN$YzE{Ck8nr#~Qyv9sED+T69TQ6$lpeMccQb zl)fP81m#%C6eGI-WZIcatJ93DF|15E?sy-nqnFIuvLN^et?TCfXEs%jpdZjdmdAwk zJnVNxUuF1Nk24->P{qxV4rU}K^1wzC`Q+oe5(G;v*6$24F$=d4q(hl+Ac&JBbU(nR zsG$%$RJ2+Di}0*dT;C;xR;fg1IQ@1{c)O3c--~0kT4vY-q^hm1t3!!FZKJ}$L@Tkb zWNQfnjKZzseffhHqJI9&gmG$Z=Z5#E1Qi@MJs+tn6~)3IMb1!0dK-@AK&S&TF{&cu z$6hcUgsf~5%l9`9dUu`L}~)J^L*Bh-$!Na!a( z;q0~vMUb2y4yuSL8KQ0Y#7lEwlRb_vt%yXXz-?UyKp7P$1-Cn(Jd}GtRJ~Hn>VS(( zU5_TX$YQ(~=665~M>~jp5W)c#Dk;odWxM``&+8uZJ+rt>s(QDSy-y@8ZZ!$BodqE_ zNcyE){)psc9o5xIovDLL5>a!9sgIpkDCpf-r>ED)o=v)jHd?N)Wy5Zcq@wt|w6grZ zN45JADckMubofWMw7fP8_{WxF6w&hd^Ct+H>qve)stNI32C6+^($uGgolc9xZ68JBg^AviKW+l$ z`EL)C9|b&rRgyZdJ4m22B=ATmR5;`2(k8(rn$hhCv(-_K@B)$$0{gyYLf1%kaIJ}< zclVaDss3dm!{`}1kxVuadXN^$szZ8DB8lk>93-v46a~0K4LW_-Je181)4)MM?$;Bz zZvu#C0^T=P+M?K)30=J`JBLnjSwP7g;Ea9PIh^m0whX3~MsF)F$rkR>G&qqE83;lg(;#&iCX$SU&@l+l|MCrq}2F zST<*@oPDo9IU!x-93XRf^DNg_pe1Aj$RPa}0RZrBz+EMS+tIw#heBnSCOnFQ4}@!s zZ@-wuhFM&5Qs3#$PO+KM{45!XybHw^B zc*xC+{xSC=0aixg)o`B;3L}{loKyaNS1iP9nb$HpG95*n5-U<^LPTP36Izj8$w&|@ zS+zbr)=m!=IFJsfWOoHGjaw^wF227DsN#+si7rQgQqhCAD(22en`DYZFXDXNQ0egJ zG(Ci4a6-KgiP4m>l(SWW-8#d&GSucO^X}yg=u=z%7>I<8=_wW)CwI=Vl1LaSn|#8t zgq?ROG~^lIDtZ>v_@Uf+wQ0>n=!PA37;RbexFR(4 zrq=@JceBPhDib(}Yx6eN`8h`JY3{CkIG0$M%1l9V4q@S=vYv)C!$+3Cs;31xR3^%_ zpboprTrMm?CxyFO-PbFTG83m|hPQts%6pK|3R8LA&S~OTTJmfd=z>C3H^ccNVX?$V zrF*f=%(bVvEs!G{XnO3s%ZL)XeK6}q(jHkKOBAjA#_OHz+XcqkwME^-`0;;~MP?}U zZX3euq(G@19%6+~26m1L2vHp&0|~HUj;s2e8tV9qq z$0c+p-0G4O&S`HG-|K_))f~h>}mf|Zn+%=dT+N0 zh^dS|y8kb>c)s3xuaF=x=?W$@(6}~%>}T!K`T_JdAn@Z2v_G0gJ!S){4EL*gmAk;t zm}1o256GIDn&zt+5K~IyJ)nPua1wx=>m?OMs+_WboY_@)GVHrg^XhO~fd*|4AV;wA za#kfWmNfIJ$mR1a4}eMn-NbpcuDv{c6aZ@c?@o2e*(Qg(FzgZJCBCs^bRM1AcNNe} zGutFp;hswTuK#s!$7^|D%J+>7|7oj5-V#}nVt1=DtqaZMeNcJXni<<}bfph-pgLKZ zjhH@e6r@$;^B!5*t?Zo5sn&}G>5kDhZMU+^{n1rNpmG2FilAHi6?g2g3^Kd?HlBfG z<+t{+@012B7&r5e~AgBu5T(GmD$M6P-Rd>~exu@X9^3e_N#-l3XCku^sh%VyYw zkp@@ZfzP5CF-LdaW=*fLRDzcl^2E9~NlJ?UtdPQe9A;zU_~8O_OE*0AduhqR#~iQY z+{R;dw%a-p>EwZLheW9eeM{3Z^uLn2-E|%9mIU2?J=J)FJgA8RaVJilIkd38PCGU| z!OvaJ*WuYb)m{;vPDAWcJ%V-|UjEH_!npS;G-OZZTx`DtrMzYAZ7;Jv^6ibcfZWfY zP`;0CiP`Hn`Qrvtx~V(0^qd|4@H@YZslxY2K12QlQ^+}XM>``o7p(tXH&h|0^c_}z zN5=@_%TLpH^~#-AeS(`_wME3S_+y@AX>597W7sGz5oM4(2(!p)U0Z!rN5R}h7 zBWT7Fm^R5Y^gj2Huir)029arKs$he7Iz;rsxKgdrsTOVrcHHBHKH(-B$lO{tPqrK9 zTouXD#=>4nXa?|Pk;3%(de0c^u}jzJKQp?es#t5@up-zoLGfT;M~S1?_gYv3wS8%J zZ+o?)dky}oa(c-J~&s201lC3n>=*l!AUWVVvSOBeNImolX(Gn z&(0yBa+q5Y-)Svya9)GOsHwxO;|IF6KVFC%kH7|s0sMDhC~htF0#^X%>SM+Z$JF*Q&SRbu$lf=wB9?_z^jF(iyu zUfP9pk91P-t;g>X@NXJJeDC*;Kl(kF$X&BIB@n?yo8k=j@lunMqrJ-gfa)0Ww9b;~JfF(K2YO^-7?dt*QOD*KTmgxAnuJrGg5IfHvyFKQ!?% zen@hgUagereEn$C^`Rjo;OU=|u?Bz#y=lW&x3$F>!j*~UT1TTL=8aDkJ;Ahh(HHphO>Se7P|o!doP!P7~mpki#UWlFE-l&KNrxgHaXGQ zCO41ECW=I^r?HflPMgAhFa=+_BYG}s$4sT|K3cmiYyCbjA6-3|X12V+!7oqq!|lvQ zT9I8nx`8Cg!sT98vqC|*RH@@R61=^*WZkMlkEW z_NALaEfEq$YTm(Yp$R6}|74v#sDY3-#ECu-mL;@;sb6VP5O&Kb^Z#aUbbRRbO$~vL zjni9QeX_ADTvX>?p@kEE$-(6cCl}7sUcO?;tgzKCMh+4b)z!=~T;8hTi+$1Fu=x|| z;jFWVKy|rBRnm#cE2S zY9Kvb53wFIk`8*@o1X4H7!ErcE$XBgi_(`FwHVperCT-YSzs= zC)8A950%2U%T%&wuQ;H{<}^*Jy@Ye+u;ugYS51eGW-96?K{OHdpax7gB^eklOtOhS zJ1i&0PBe9VVFRg=vHFul>Ep;jpCQ(l91BAkPnxfY;#yUo^%2=2_hh*7Y8^1xC{>Rw z^3}-RB9uv`H5p7ha%gK~Ft@TZ=t}gN(HTRu=*xwTi!7arLy5we#f9NkQIj|a1W3HL zB7vihdd>*QUEJGLXaogKZbY19MjU^8|Dj|r?s!|!Cea4!lyiqh*z~Y)FaG_wNc%zJ zV*ES1UDr~53xgTH_>b5T4Ed0AK*3!m{a&x<~A!$IvRUIQG1^f1V?yxX&2?t z8GfpA6^T>sW~Ghp=kzh~p`nn?MfYS1|JqVLPeSdLEQJf=+q<%^4R?~Krm!#|E`Z;8 ztnQWeQo9q6iWeo7@6m9xQvc&VD=rCKXj|75IRY9dMYy_k%#&xP9(^S4v|H$+!r#Id4m1EVXS z;Da=#-SbL4GC+FUcrmP-<|{AP2yZhJAAP7oA+y<=j_h5@?qEQpCi2X|pjsExR!ImN z!^2zrui;?>m~Q+oN8hggUG^pw|AH|_XLa0`phQ)T>LX%%KTNdEqBzUqB;#_vzB>CA z68cwC=0gCNwU91*iJTF*Y+3beDxXJ)PTlGwI5B1@qWfG=Qcg zNma&`uP}*{8A><6bvG?TUfpzkuz8igHGKF!8 zNXYMX+@Ap&0AM1W{!bTQsSuhnS}xFAn0!yO?*ot=0fi`fdnlpySDe^wT@oCt^Xt{< zq|n2`c11-+WTb1gWHQ;04MMzfQQiA;u^K%C17NRF>!0h2&X76o>D~zFPO1vqpU_3a zSsl1~-CG&Ezs_4t@B(j)s%$ipAYB^DpenOkciPnXa_Z=N{Q#^ifzIx%-iErWI|Nxj z(%Gb$|A+wV-|pWYtKS|^P6%0}`T(})!6)+%B0~%!_$Fp;h6$h zc`~88@k@?Wh_gFBJ;J6yzDbr6Sx{PFz)2=eb4Mwvkip!eL#lk%X8r;~8+0);H7DN? znMF~st@Vef4O%dr|5H`o#ihQxsAT*+u96`M7Y$TrhO26=$!q$VMa-K4e+NvuAO>F+ zf5&&++cWQRTSl(6gRLFES+vMWRV5$bA!ns0s&gO*deW~XvBjj)cH~;Zp5*$om@u#| zp7O$tWLCj6L~&G4c*r#!f)M1&(XKd~kWVI&0 z+rE6G>5T;}Kmx)2w{8wk47VUMgal4RO_2O@AGqc5_;vHv_(WbWCNDDjpMPw{#y%Q1n3T zY{d2%ZkJc^rpt&I1%~4bG>mMSt`oPIUpCc`S@~2@)d$o=ms0=U{74NFT*M5&LVcvF zloT5w?z8d7Zsyc%n$y19VZ7pSP>uI5V6Rkf=IxeA1ogEu_4>01vw$*PjNXK^c`)wQN7-2!5{*qh^l*cDkMWmYm6iTtZgjUT) zX|;MMK}q!Fn40lFpSoDT3TosyrI&!c+JG2Df9z0 zt^{y(K;}*xgABKrCF<>N<1Mz;`u^DOu^*7Rv-hoAiN!67FKvW!qcZ`19`67E4&iZ_ z%?hXLi(^+#5_)|I0u=H6MMu4-GY^AhkZw!^JSh|ZNuD9lNcw@#wm9fM6Cw16iv_^I zIgZ?-85A~Wp#aYb_jAK~-#9z?1m&c<7PO%1OBZIvR?>yFWTUYkvo?@{C`GV-& zSCUR}S7iR^dv&?HyL-CYwBIYHEdsXqG||?#bS*FmM7!_mCf5ORel{$(Fv%p30qcEn zI$V5VL{Opi+rxhBey{I(j2I67XLB11Ov(h8P^U};a$JW67#Q(mD++9KHa5wT=jD>z z$*1)6YmTtGfb^zb9*0m!>Ly%GR3?EtW4%8H}IxSlH$4D(XQZJnE8UV`TfwZ3a{WEANsX)jHh@*Ml|d`v(G= zPJZ6Se-~|pB?-`F@h@XCm%pb`bRKf(TYvD0)zoDHi+rH@g(}9r6ARgTH?v=ZsAy?A zk?gXgOgoMDeE@ByjAJv9s5VHfV7e?E&W!4PQ`%{TNt$UAgqhhm7|{*xJ!F$uj0YF> z_E>0*ri~-a5dA2`MPk-!TH%KmY&#nsyC06`}(qGJ{?J)uN@wWC#4Z+SuBU<}zPS zh)knJ2!Z|3JitRY6x}3NYPR%R@uxc(IVy(Mr6>MY^Q-opK|j@8Q+=I54r$bUHF*e6 ziMqb2TF*gXJ|SD{f(sIY$M2Re@w)=Zy^$ugIEmHkqJ;-vyOS9n_|$Ne6nchu;Xll$ z%c|SA|ITqUprh%q!*_uiQ50MSXw0Hcs_d!uwLwUl&+nBB5fLM+l9wDKzA}V%_hnH{ z+WUG>q{GW@y5VA|THMs^}DsTq{-oG*KeFPjL7PlMq{Z+ zD_S-7a8M@GW7+;$NX+k}ufsa>aGSGZ=+dS!C;u^qwdji<6jdr_bE-UT9vb;bxW;o7 zV#H7r_2z0qp2C8ILnTw2)0R#94xQJM41bUnT|hpp_iHI4iFzo#hdBvT#W7HHxO*8;8>Fa-1B!$0%t)8cbzyT-P8j#9@D#!D3rfXLsr zu(^OJmS~!4#_U+Y;4cubSg;GPXXO71Xm+0z%6k?5yReCkutiFxjF}q;FZicf%_I@=O^>R$o5_5l4P<)Q_;6+ z(6mEyuX+_lT5hwT!5m7Pp9BtGPoCN#J*h$**C5FJ9A^Q(XVG~3JJAF^R^X=c>vaX*$zJCArdT$^oB-Gi~ z#xO#kMp0l&zU$s+dLR@UYpTNLOBeR_S zW(M{>?;HwO29M9r7rh%D53K-OVU|q3m+J>5vbmkUj^6$H#OMIG>qNROj(<2D#u$+Z zL-d4egn(=1J|$BtZt$-)2VmJTny!D{=HN$i#A`vGU_@PJ-e8k05QhYm(9v!{FqLHv z0lQkR@8jWkCin4@RHveq*;TZo&-R3?2LSQ(-emhH0sVGKTN4MdJ@-HaWCCU&;Q*-NAcFQY0m`#dIQyci`}WqP^9XYY^vza0KD{X>OfWsS5?O_4m9m-l2Y z*>4Kt+M`I$+vqLfr`f#7iz3d$w#=!2t@=dnX@E!OAe^PcBk{wKJGEguR zWI;3Qcog11OW#yW^{^R-Ejl7*UL046Q#dd9E;$OmV0*lsRjXFL9)d|&&97>n?T5J7 zFP%U7X$z)_w+d5-+;c7+^`{wcG(k(zK;W5TePTyr0p*pL$?&+V z-Iz$!m0!gBn0_32am$l>6=@x2a{Q>eXEj|s7}~h+7eS(;UXhxqsEn*f5>|$6ZiIR)I{$zUWrC#Be5!VvU(-w zP&~yCuRLEmYY6P88HjAW)@-J>|NIpHaP}ppR^-abMI7B?P%JuJo$!*=6CRV>kG=5m zl5y*Ow8)!y+e2|QF>U7%K@+OTV6c&xF%pmd*xTp5OPh3aP~`|4>}M#M_1SIm+|dL$ z!p~bdFQ$?*pLo)!cUtp{R7KfOwN-A$07AoIk(>m~-)md}M4onuoLdwVw9}XGNH`hE zvCuG@c)t8x=t?ZJr1kT`m4<=07^ovZHtq4KL9FX)PO=U$L@9;eZ_DDGA-h5_>0ehC zUP&0ubq8l2<6_HT!9FA5(6K;!Ib~1IrGi;-3&k(H}mL6n$n;A9A~h)9=HV2<=%j~|V{@7O9KL)5HZfYogmvoM^s0hb6U zpX95C3+G5+dq(BK73BvuG16SbS;ypm{)u6Bv_okpB4%|JYfwfR*-#p)Qd|&v zUztSYARvoxI$wob#wq@8R|f2{^hX2sSr=HDxCAVf4+VLZWLsC(-DS-U9jPL|>K z+j*W)mOif>s$}%vKIN7C`AE|lnNe`upOL)!lE_U{<@$M@ zhx;d@i@cOSwkjrl-7JbKX_;1p8oKXijdtdKzgy09d^rP12kYnm;Xt>SyBH#VnWB?3 z$dLt?`8suvi5`wo?xfLo+*07s(p#*I2u$|hr$8(Iq!*jFh;aJrayuLHoq$CYq+KQ= zM34gDTrDth>6)p@LX&ExyaCYgf1;`PVUdbrPql`$j1C*-e`JN`{hX(l^YyIaHhfcF zdeQ;1%ds|wH$wx1&z}D-4FMaoF=S?r!Pl2Ka;ih@4ae0g89eJPV&WVLREz-Y=ZHJ} zU4B$MD1*i4>oInK?e~+7B=FL$lxK3YL6%w-1)~?()iD2ec-^0GFjFL4W%pauA;a%y znSy#DX1N`}W~yo3`?&LE)$jYuqd1cJWnu~_gQg`JjNbQ-tugAi62V}h<)5UkO<|u7 zm2Xzb+-9e=^3(`bTGUhDF^|PexdlHqOB0KRikB%3I&G>i$o#s$`gYE939 z)qmjrj>1Lu#iiKDvRsRr%WUqna1E3y%U#K@V}xzRX3=H#%+**sbedM}(3q3x zqzu6ZA9J6oegdTZP0JwFu>nNHN4IO=FnzRK>JBc&9B#H=8Tu$Fry2Bk!*lfnt<@D; z>>ZK&8QOzOeOK{ds&S%Ql0)A`9~`G*YO+&|tbcR2Gt~u8x^igKP6j#i!N*qOdiHia zj9_iKFo@JSCGsdr zb!q17Qdx)#ihL!va#dCFkbnw$5@M_mdXLxgz1yR<@eRq;iWx2lP@#Phgn@r>)&%gTYG|Z8(S3UeUhfSG;HX# z4Qt#pt~{nsN=v6wLYpOTkabtK7qM*c3}>CkjQPZ&e5Ps>wwFkq{;5$^ zKwm)9N>^SSHL8M?-v_rtfTdizfxPQZja?Z{S(ypBskErl&=RYq6t|luEywzxX7ktE z)tWT)FwTAUFN-2A)gFLr!s22i=0mO75%`E6R-%}j5;PIl20b?LRZEI*)i|TUT#QD5 zmrco981`MBj!$>K*EPjNAd%>I(4bK?gi9rx88RFtbj~SU_lih`+Xzwxl}GHXv7Jsg4MQzPw(HMe7PZ3~w0d=>fjhw|{4?=K^a& zWClj<%jmo%y?;18x<^OW=gRH;{t1nc#;ijQx$7A8P8wGPM@MA(J0 zsquB>JP&shJcy^?2YYcC6o2NkWjdS*zw;RWEF1Cj;gy%@tH zsZldjzx59IwqservkYS@F?yM=vD-A9#?3~k0hHuc_~qW!4i;x} z4ANqhi=f%j(NV7)(L68>La~%v$#vF@T0Ix27l5=^>$mR7&s20INn0=A`~t!ccdfwc zm)Y;3g1Pg#_C(k41d0P0be6LwqAj}F2OK^B?Pp#e9J8_=U2@`SF_g{T#5`mt_T%c9 z8wTc`{#6`b#>m>U1b4Zy`p1mrw(fbIH2E$5Goykz-*FT2B|cdpD0Tb1fbq3>3rOw< zx#6p6<1-;JARN@IT0M;{VuxF6O8(VLtgQJ^wNR;0A?M?ef#nJ~Kaqa*nM&rJ*0#!t zKBj!h=7zNkG1A$k+N((5J2*k<>`$OE{?{_ynuxefufwjx(csUT*2j9LsZa`PXwj^e z0ljfPw1r9)j>MVm7!6(gQ`UBKRdkuyg-3#OqxX(2a=%t)m4an4iDJQOT+On5muv zZXh%{(2aw4dI(_#E?+&irp?Q3{}~$gtd$HPLr?|LfPtOh+soRRY0ZQRjB8m-!@N)x zMU^^d2dW**4p%zrFuNDuW3SeG6_c~g)^!vC2QU08s^WP&*BiVrTx3Z<&Vt?tG3Bu6rvcUE_kf z`jC&}Rb%)*oVVoTV^N0$Jah5x`i*VXP>R0rbOc_@z)e*4`BR?Oq_331(U?`=1``SO z3BUf;)j~Y~iCCoH&%72*fb;{$)Ts#TWLJ4QRKWtb#^PHgrD*wK!6nJ3>3&S*D0n-Y zxy=Bw7mdTLV&o42pbQ~pa|6lsqXsPdCLf&0`SLs*1+*Of>ZVd;^g+`-6>8`|oeN>( zvfcnT)zMo9A^~Dq`e`XX9ir~vOpArwAij!825IHr-o{f9k+>FEoJN(}DI})3Z+LeS zU%cgK=g{QnucDBhjEZ?C!?03-GYCY`#W);UK~T|xFN1T6Zdfd_KUgV?mLVaUO7AXHTqqzT%rgQL) z>;1ZT8aK8%v2EK{W3;htHMSz%*g&Ry#{_nhfY{Qx9Ih=2+ov+stdWuAI zGc(-qZ@9y1AO?Z9HBKWoLo~(%rDcj`E|DH@-2JAS(+RsjNK9a>yla^G2$(dvewh-k zDhukJXo{bNI=|kwdv2d^3yt|yT_Hgf#xyYj`tAjxFn!)%)06zeIN?r!g!cT3U>r$f z)P#B!_k@|OqxQKOW$+Q2;;abr5uBeOribHc_C$VUr9b1b!644hjln5q1bO2e^Azor zYx-$=nN#LsAbJHqq-Ukl*2p!?9rSh{YTwgq3YXrYx235N=0LMoqO@Ho!uV1nb3u-W|g zucV!D1SZjW+t4MY2*&AXXeTwyytT>H*^_e4K|Q?6!=@elVFdUsWL7ndHJ{r^6w>MS zxQHinD)@G?%$X$=jpqQmsSn&SMeYZdI?LH;xPUM*1`0f>L)AW5`tns1xpL~bD zI4-Y(?L&exGQ@aDIK3gmL4C+oMqqeK{RRf=hb*r!cO`;<%BS5TUZ1Dw;r4~cMFtP* z>5=6oD-mW1v#6af#*c6(orW)VCr3SQP3z}U3D=Iuk{U>zCkN(7ikoj);du-vJt1pA zG;^AC_Q^^qU}46}CHIW$PZ+ho^w3$%kY}XxKWEsA*K?e*`(*3|x2lpht+4K><=nBR z;$>R$5J{CwfNDlWCYz|q5ocNf;x-nPVd=G$r<@RIdgV0Qy`+$+8OLUb-3#_exME}Y z?<8@OYC4juOt}0MIjsbI+gCK?095r!)zuQ#H!?$wA z60~xVZ|6{XQS4QmEVZwMa4H%aFzQ=IuAr}^XKn_n)I95UC5J;E2~Uv5-5U!bkU6K7 zycm8w*R-tP-^68tvRN)0w?PY#aUK+g7n+H5h`=cG1eV~mgR@f%QNO1xU7neF_O83} zrIqOF)V=??BPj6);)LZkSY8D-T|-eyv0#_ zovrp25-%-~4!U+gcLzdqL_0kbHOcaXSP2XNZdL zgoSE(VXmp^B@E-*t4)~5=X*!z*pWC@Ktz4@?eE=WtTA6X`GZP}zc16^j$ zfsm7xCB0ih$gS}LY4--+h(T}f!E#l1u8Xs@78enIEKoGUFtF5{tv%=<*$N86!5qw1UU?8$oo9@UpvHEXtq~bq(W3KdL(}#6mfd&#G!E$QyTZHl0_u zV8}U(o=~q)r7}+&DVu4m#6jqNziQ%EDz0#$#?mYj~J(_zfd< zoNf3=v+c&Eo0l45Qn<@GbdaPIXF)uQLzNDF%gFoQIactBFKlzBYmm3X1d29tpBQR_ zpELj9$KvPNnN`nrYFxgnvxI4iDQ+Sml3YP`UdUsK9JCQN%MewH3N*az#K)AGGf27N zB%Qs7&VV!~{Nnu$FXs78pG&TasbFax$Ha$D8q*BbI6WUfpKB0zUuZavi^wNiCxSRB zWH(qpPD1>`2@&5kmDK3G1{FqJT}y@x^?NBj&&=SY{H_D{0Y3@|hxj&LA(W=NX2S_` z4?NoW%dk_8u}+byj@^yjlV_PqlLt~egqvgUyGYMB_PBptf{t^~TgTTGmH)jvm;Kh# zg`&~m^l=wKt9w>;!ebrYpUVIxjNzT3hzRiIeAU%1uGAq+Zo-wMFc&QEX$?O0hJ^Hj z@Lyvf)8~UY%WxdA&%bq$L^(*vFO4TVH#a%%>A2n(c|NgNs1hcYsVW}0vF@qZ=Avkf z5XhE#WxIMM)iWDMFvDpM8qR`IN9B!( zn@Ic!)9;*6vOK~6ncm}sKA(rM4e>@C9VHVz3FYF$QI2(6a@#-*?;sfDamEmd3sN8i zAUwU)@)hJ;0Ijv&r|kV2aD^L^dwPZYbXi&;q@t*`r}shfCkUCctQ$8+$IJKk&fu>S zPukV`A>UlvX7kLLosvk#QfR^}c7@ET62BAR)kfRLhgQ#1CfBEB&w2#NP7i{O*zkc0 zwj)8yIzQE#p8nMRJLvh`w>*zKwtZjrh98VqPRedanfgKQx2w;=fB`F%yQX|z z5;gjtxBwG_H@Px0D)My)bDaw>XqTY+3#8SHbffm%+5Q2;t%JFHG8roJ{p?Wt1RgY* z=yTpLOId`{mA5~94( zaggBiC74X;zLBSOklY{wEX%;9FkA#nz?=!CfU*(JF=9iqo1yy%FB82bvu5=Mc_!Bx z*2+^aTXLp&`ncl6wtmdMCvmJQZud&%XOJlVwW zOs&rDGsp#6Aw*Kg=WuL#t$oGSyW2M(NM8-GR$sTqmkoE6gdex?HC!J_ohdKsD1+MaTp+v(>0e`!6|)SnU1^PvPe zcT`l|>1%7RYOmPFr|e>bQBw)<)e;BVn=qod%?1s`=@izE?_C9u64(p(vBB z#J7^O3BdOYr^2FLSsbJNE95@F;XlN4MWj{AW=06ShB71mGl5$#&uG{PwvGE;rV-|w z#^N~(RJ=_~x^T4%e(E1A*xKLvQ|C8Sk(yRj>E&n=3Ek5nc_gXwBZ3wt3XTQH0r}l; zJsrL)ZW}ThNmpWIVCx26R$D_VdY@tA(~~KG`#1u7FBhS81tt}&J&;bBf&OOAC0(eJ z7aOz&31UNtRVk=lKma)wrxQy@{Vv`~Thd|u#{wRy)6B?>vTJ>KC15>pWleq%*TFu( zMdy-wnMu$47bYcNSgv*L;w9K!mlg{o_r(W^!h4AX1kJA?aPm>*13sjfR*wR@*uAhL z4X_nNl9pIHwo-O)WgNcMoI7#)!T!kzHZ`%fWcqpfEddHq2$Nzx*8~0uSvYAG3KS;4tDc%=lrI_*%#ZA;#)`JkF6p}3~ z^ie4qSk`9dg;43o)7DOo{e0BwvCDS9gCqXHM$|Xe8bLu9b`c++Dlfky$s$=HMOp3{h59fj~7b-Jg{bu0}yE#JN?x%^?sNT(HWEJ*p+`QePZ7 z3p+U-a_yG+b&So#b3E_JvmxK}TCv^T(V?~o0?aZ+Wl2mIQ=>-F-`kAgNpPSh1KC+p ziw`2?@=baCp9`O+w*_jfUmpA)GeL<4nlyf+Z@myi<1lCh*KF#Uc&ddXjmz=IKY{8O zots+`=)ZowT5y(M;#@mVwJ~E=p7&3J;;`EMeI7nQ9x5iko77{c_AY@U@enwrN^_N^ z%uG)@h3uznCFXNL$daqZbH9W}swpWg){Fk~g9|UJ%ACoYca1?;dry!5=lcsNbNch< zCZNgBVXvsYJ_Kyzs(Fcbl~3Dg+7uJStnKl6xl2+K1ij{3Uu@6Pa?k@0gVc(3tIB&C z7SlG&=xVZ^m3May*dT@BSUtTaJJm|bthuSR*kzvf4?5ajG0jp@O|zV1A7|&*wsdj_<>t3@Nt%=!-WtHhk_g{|kobc{#iHCY!6L$2!O^ zIdxD|oM0ustIxSI?DhdAJ{xv=y!?AuRvtz zF)0>k(7;E>SP7mWavm)&vh}VKTz#86`{O7UgA_-=DWBZ`C+S*}L=~2*9-%DkWJR8s zS`#0E-VF!pdV-n*xrkb}b>fE*j7anJPxKdwwQ*4KtN}~0a4Uck$oy|EgsakD%7dkBB;pKt1)70VIhgq7U zEd<~3aa4S0@YC{q%CB8KHm~?_2@!sYX2VgiygXMdriSeIRY!Tbe`jlio#kW7tRNd( z%a9~Nt-6p1{u-T95Uiw`#{ot^v9jhS!8H)Ay_b_3a9Bh3ClxY{F$xlK(2uJsP4%#d zS=?u}qP!TW+DuK`*&(G~6}AQWqK3`ZAsWEl+62jJwq3QgvB4ek9d=nx9R-tXxr=a1 zu`KZKdf(`NT)ywHnY3*Pw`)yp_g#Iyz>XXH zPUZZHLm0uP4)EA*_CUblxE1eD)BPqZGr}+ZgYVsD&{r_y6~1jbsx*h4K(#6R#lbe{ zR+T^W##b)9h}e!@1`bn(NHW`yB_z|}c*S-(phWZl;9&CgEUdNpNdJ@^V%90Oy?{X% z!DD!sSt_G74;1aQeHKK#8&pWQBCXwT2>VLHtqW5+y+C0P|33vA7g+%vDQ-BON*bn# zK(tHqMAo^I85)Y?pebk5#g};ht7E@izORGD7oX2Q@EcqL<^*y}0ho5&V4q zUl7#sA@_`Df~i2wJTRR{fxno4%98h298Y9CErl%4Tz&PVXXZ2qi|JA2xLU^aal@#r zD0xx2=1bVhUTxyNi@SSE^?4LFQx-<(E+U5a6i+JN`qbn+f989Qoa1_Xm$e^Kf4PJs{En??5 z-u>OLnZmJ)0LO7L>&ODr!T@uX6il(QmP+O(q(o2OoNLxL31C%#kuKdF>}Qwz#k|bw zOmnn-J+b^>>xS*Oi~adpv&B$MFM&lPA+C;oJ~cJ<(nwVDlJ>W~8nlaxGwAlAIkcu! zoJ4@h!1bHtXD(?S7cE0-z*rs)CoVFIOjmn^j^-ROsBv5Yq7{ce=krpbD$J9MO6o2s zAN=-@-*az$@XDOibzS0ZN`owz*z*g>By-nf?ng7}kY+(%Gx_JAOQ?f`f-{IgHd;Pz zS$k$=hAC^w8m-UbC`kE(k#f`z4w^YSJb*OA;z>`(LEHyBsDYAUW%7X$78HY(oym2D zSlhLa=~2E}8HR?2M@EBI6vsj@CqAf@DE@O#G(`F31YC-GMIjX?R6ml0Us2b}b@q>n z?Co`1ExNkc#car`feZG=`qSNxkRA;hWJl2Clez?h7%ChsRto#Ho<9`st}isH2o6Cx z?aw)L_`JP>C|&=63OagM2x!wFMm?m&rp9dAMjL9IQR_!sBtdd6{?3vTH`IVUS`e5wwqh(0_JVd~E!JOMlXq;hpMWvkI}>9-??ha$};0Xa(9 zhaD$Hn&tcj@209kSr)a%n8-=>W%o+jtMeCO>S}>{F>v1Ch0$M`3-kGL6Y)mEq7YWe z6z@p>?cHHg)HXya<^NWbB^LR)l6V1YQLbytmVt(odXYvmf&`4EWv^r9xzREdAA+JqW;65C46rbJ5pY%6XSQB z1yP;YzR-OV4lI?1^)y6XcI; zUd1Xa6jaPzAT_d$pgYGo1Xr8r583?A!1j^X^Y7^5;xzrakA-BaZ0^x1e^WyCpFZfi zgvACY%wiPgpAd{RG?a1|?1-NHyd=(F@P7ocs`(ez)K}E11z0!7^WjWp{XH;fbnc@V zr@0s{0oEdc1wt1;GB?)d{ak6T>&*)M)dhvDE#>`pt(-xFi;3Xdu_<7qY-=bqJbl?! zp*@oXga10_{NS81Y^pgF2&KCDlgpwmJS@|9unU;DhJf#XAWjJzZd;mN!8lZjj`2DR1M#6kO96@2h;%wEqm7Be`l0=F|B&%&Cl(I&hjO#pU5@qsHS_+ATy1S|R z$~0-M6y;FZ!0yq}G`(~neA_^FI^t6AI^>X)tK>6kv#`H?^z~z5*dc!g6b7w5^MgLO zA=tvx;1 z39dB77BnFsu08+l)YPs+R^0LYz=YO+bT}^1N6gON{%r_HrI0=l1E*(Q1ZpIMQP%() zI!V_)ND|ApL!q{>1vZYk2`gWsOY&H<9jp-|x=-A{9L2ZS(YvrKsi2|=m-hmy%zu!h z1}*`v=chNGA!h&WiMjxe<-n965uFjyliNIe>zMcZK^@P@k?K~L$gS?QKTW<}7v+gmrACM9N>qFS{ z4aUVz={g+zSO9yZ0J7lWTx{LI1e({Nl)5o@ay2rGl!6-s(&Vqk(}x4IN3DP|nMs=vu}>Jcp0@|sl>5XrNuf>&Q=gGT)`_izGLt>bYF*P{@wx{R z-@zK-z7=?sV#h^w>06*}9eO>8(RCJV1|u2SqeF`RAS0a<-=Xf3L>5_edu;T8opyPC z#X>sI60?r(bE*2qePS#%-qul*OpB^2tHYJv=^QuS2GL2n@^TNYAtZ%Bv7}Nvki{iY z?Fz5HsMK|(fZN22*+}nzzB}^v*X|{(rLzm$s&(+H+dP$5-O7J?ANk~f`!s%f3k?;4 z($T`|CYGz&`3ys`??FVx5q!UqvXOG;(RONdxK*bT?dR1N=I3eyvPLht-umSi?;wdq z(isgmuak5Z{OF!Km)*d)#JI4>N#xz%;f{E1`MjK!vEiw?Xq(HrT!U%-VUX6c4QSeQ z38^q2O;^dQ$&))XvVZHsyGzOlNvpQ%;^>Qg5;rN@)@0^Joj{0UPNtJ=0H1d+?Fh~v zxVEM}D*aVqq*lt5nxXF*jM%w41ZIbFb>kOMDCN-;cWG&EwJlWP#Bt*LHTC%0E;~A~ zO*Sw^OmhmXYx2Uuj$Y>F*2Txk`{~mDYwhxKYM1!~6wH}wD!_P5VM<|=!-fj%)0E+? zI~<&6Hy6wy+Wy+v^#8g}9T^Gpf6d+oDNIjK8-EZ*Y6I*xGr+8rcIA2?8fFbl_mbXj1zpGd#hlS@dWA7Q-El5Sb}~{HcZ8pqVuwL)Bz$I!W=t zNnc~N8|kLWI_D_MI)-#LrYVs&DclES%zt`%g3R562EZr3n0b93;k&!Lmxhg0#aJ2Z z6#cohdaJ{~e57g7}j$H9ij&{vXTNDXvZ69TTidFTIn_;D?3vsQFY>4NSS=g^-~c{4lCPXYrVCzXL4b0^KwgZ>qVqzX)E(nnfW0X8fMb(d2i3K z`2`8mkSzDxZ^pCP8rOks1CiQ;`9hW{Y55i zbkZ>P*jn35^c$NS(`Z#c%oVLJQ8&5=nN}`etUUu@`b#;u7nNfyi?w&u&E#svqnM9&942GpzaX zx5D_#bZ~F>q4i*j;_Z4Rl2)7isNdJ|^h#;}oufFttwrdNQmrMmG(tO+RZz|Aw&ib% zQS|px|Hd|`M8rwAAa|6Iw^VwDt$-+(L*DY4&_6hvv`|?S6_=r*!CW(RNz}>R;%Uh# zzn&{FdMfyB{FL{(m0y#6Nm;N@0#)LTc9`2v zL%&%Lvz%AAk89ZySn-()Iuh1v@8q8-mk)Lct20Ww?(dGSA8Z%FZp(&UFgwa#K0aAT z|KNLWqUPDB?x&u*pK>Ln#|z5u9C$b6A4BeT%_srzBGRg=*=B;XMfKfZecK>un8JOF z|HE>Qpsy8W$(Mnva0>jwV`d-u14c)Gp0E{O9p73Me8mY<=SA{{8JxFJ0q?dv&6NMs znAFz;)91hW8KJ|7R3}DSy_wa?`;tXJmlE6)1o(-2r%9d$)6Kh-Fq4>=wX%=uL2wv1 zQ4^Yq!>(6IhcrqdN7ls*dONA{N5?PD?1QXhA7u?XZW2KG5U(CKMM2j6sSD`usB6?P z?Uq{zUA%3Z&*XG{zOU}@@3%{jneP3`M3gh(?7w_e-h10?-7J6X z1=BX!nag!_KmpJI^>xYqHi^#DY5=rcaV{hfwG^~W6D!n_ONA^^Y@~ED)lTsQ1u(>L z`GFX7FZQ2DT_6L%qLmL-g)xU?Mv3LL8qFana^|)a1c19go)Kfl(Ss1L9&UBCP!)c7&2~^^6$?JuSqKGB3fijuDL<2|2Kc*Yt5k zKLaC-GGvVh;36@=p*p25IvON^pdxL_25bslvu$Z6{vE*aNcGyf04Klm@L)N*D}ycQ z=BTJb#;}h%@L@pv`uU+!Lng?3)HVyXAD6ecfd0If7*R&BRka*2obx6C;UBdwjBkR9 z2IHkYBpB2^uvfbJE(}5hi1hyH$f6g3zbNr;51bP@3tcKKjdqVh{}$^Pf!8(oxI;RD z(VRNVx^uT~4{+4Gux?qMT2mfHRb18#M6df^R8ZQJsQ?+FLnvYe^NLIp{V!pH;<*1@ z%OkK8URl46Tx=r1FbL4#aFX}dWkvSBF-4g#Mvix8ABT!IB5SWg*M4nZtz2z6pBsmG7O5Hm5@7rsmwW}L}MsbLQ!BtSzRNgOR4&U+knri z(-#KI#Ak`J=#JyN0XB8SmyT^z0A-?jZm|5|}= z9v)iY)1n+bl9jf&o7cEbnKg6A$Vo9XEaYSr1ssDbR7rwJd5q9$ullJ~fVu{J0?gJb zDCOgB(DoRA|91qEz>XRox6^ohKAL!f?%P;rwn!@t<(~37`bh?I_KxzmEzNUU(c*9n zT8Wv8_^U%H<;0oed(fAikUrQhlk-zjt(k9y@k(C6sgEb5^52a>f{qVv=j*T!j{pSC zZC|O*o#R2lIf&YA!j<_fb?mn2~qiZmjRmwB~|0xX>=ve@p%^`GX~1rB--V_Fltb7oC=DS@)0Yv2ALl$ zuRvABhaK;)`h6ckrFnMUFAaMT7B}6Z!4b6&_pgPRNfn8fJR?csHu_Gs0My_9S4OmYJ~h-&6?6D3RR9b(KPmzg zCM#ps%f)50lK*{vc1t2~6j%L=9vYj9jk5W+vA(W*Eh5dsGFIIqV8k;e>y1{d^4YX( z@K$X2TKx4)?EiH8Dg6*cPY19WUBmD*SjJ{Dc)tlk?s;vh*%WBAJZdAos=_Uv2s|QM zL$QE|$Y*A@TXgzapT={4q)M(UVX?>!U#ZL3c4$ZE$k13{p>zxdSV^r#+G5ZNHIAPA zR1zOw7nu>Y>QNIgDaaj0K_J-~5+-umjVhgi|GE2*vyngbs)&;VqSV;tD9@{C*#6c2 z3=%&9(dJB}cP>KhG9D@WC-w!83F=@yl{5}@XE;0+!JHq=aYs++Xi0NF>te`YF`(Rl z0=>^AL*y0sF=7ye76uy+snTZ0=4cq*)2{Qb4-0V2KER$`Hn9EG8N<9VEY(*X@kn3tQ+Nw2)HOOBc;e zAkc`M{S*#a2K%80Sf~{vkXwH)aV$v%gpR3n?C*ru>Z2KDVfTN4xt$n?vm&jr8W;Qp_s%&6?|95^Gv5C|~l<%7Oi2hAga3i;wO&USRgpxB3?Xz)dE9i`q z?Aa4ycUxG@j!?Q(UJ25S@!X(uYrB7$?;Xzx71AJmH5N~->*_T3>~>#_p%?E&AUlZV zmsu*J4f+oU0k3m6jr^^Usx+UR{$G+ia#bTzZB zNKu>1k>S=lozbSnzo#BTV^f2}pV@an57w8B&!hs0>5=(?ino!GH`V6(pjYEJn*fD& zYArlT-SNo?Dpgg6QK)P4jFQYud_LPz6V~n}$z)`-z0LvDk2y>oUt>9+!@t*g&nvsL zTf<U9-=vr_bvdLqD&Hq;l%)5}IU<@Ca>*aP;F#_Ls_Z;NMf1_nn|8 z8|(92zvJaxL0>lY#*Kau9q&WgH{#l$11 z5A|eUZl(O+QvDyWWSeP`Ksd+sZ4Pr`=HfTHhrKV zO>xVX&+*Isv3SvVQ5mpFAT2ih#Dm`tgjnhU^qBlfR_$zrb&c$Fq@rnB$4}tcOdqA8 z0C;+ESD#y9mQhogJ&3|J4fX*5pm&4SyU9BTJFE<@kxb2&l)X?{4wCKNPxH;>2)zBH z%GI{bK4V!ys#S~tsptRe2Yvp7=^YY(L?-tQ!O5=wz@F@Hz{ic()pO4Go6 z*;8@{8n4)T2315%M6$ha!jOTp5cUhAM+zG^#bg>K;*E;^tos0@ZShhfDR?y)b(^d< zDgcBNp?UvBiz$B(Ml|gFwUI@_-i}58&E$$JX&FkL|LC#I*h{ULkB4tgnAx$ajnHX%sP)gm|I{8=O_@i zjlR{Et#m6jD+>$+qmo66{rmGHgf)8YGTUJ)?c^b$<2S=-o&zzqWnify2)L;#6i(B` zp2{hPWzaTiaaQkV(O4?6Y2v*?DvC4MT52#uwg}xs+9<%`wJGKG=whslfRu#mNnejn57iGKdx! z!&8n;|3a8U9ExWo0lkwr#O1YS@H-S{5U$9kg%sDg_tDQVWhljo+cd3(?16f&wgJ#& zlh@Qz^Yaojgu`hTJ``OIq7dCjW1o2Wm98&om$y`RV2_IOnpqSUU#okRC_I8hh zEE*rLebqx-)%7%9P>^FV7gHd6yH6R(1E@Y|PRW$(NXsV3d*&RwCf|EzmssjAPo7#E zzP)}u_Iwh9j+*~JDUcHCtN|#IY@?BT6fs&p=^$G|OjG=RaAh|B(m5JN?DxER?WYCz zvG0F(4hnA4$F9Gyv)T-oyGJ(&e9}6(e`@ziq~+$N7I?IIzRD}^TYag~Ij7B1EJYT4 z-T|o`jJKaN?to6Rx*}Y~qViZn==$2pvIZ%p`Zi3C=brk@s_Np>lkEyL#REPQlVfMi zH=5IniIOc5QsT`TUW+l78RcY&&0_db*fdYRR0Tk`)OzLysNWw>OKB2xD+`o4QP>(x8>>picL@3kGxHuZ3Xn28G z5mPZ6oi5Rg*Qx{YgVTJSzrEV}gB7p|>W;d)4wqKf0(LWIrxYC5RN)X>d0@m(R~yX# zQ&IRoe};kD(6P(Q_UdwH*|N$g)Oo9o%8LJ@K0!3TTV4AuQ{9B_j}A}C46DC5=}+Z0%qq4|s;L)2oDEzjf}RfhJ_rHw@hOLF?4euLKfV`f zknGHI!`ybTU+TB4!^Mi)uj-)b6C9#piCGllxnUvWa|^urDL;F=fWy&HJRJ|{ek2k} zsmHU3qbQuCSlf2NUoFm7|H0{`SlQk|&_S&FYlWgmzn zf+046f-5fh^Gz;2r)%oF5V&M2d7IYPO=MJ9RoIz8`qjj6wPI1T%B0NvA+jGp6 zwO*7K)3?w7sQn{u=(qiSle|7$P>QBdQ;3FK`e#K|A8mE1vyn)R-~gSn&7V%Kt+Q|E z*Dd-IWVr1VBu>c4(L^HczwMbYwQO{R-xF>0M42tZ)`RIXh?Kk8?U)md>eZ##rLx!T z4Zw{49eo-W zg;8^smC%z(%ol-UPb~NCB0$X{(ow)0Mr%-7SbkouHIG$q0Ny^jw-gAOw-#t=+0BBh zJp4KL^VDCSm9-jbycsjY`*0dvFk`Y zWR(ckI>kdj)7U!$Y8@*CcVpKIcS?rzV$a7~`m~85gHwzg4slzXJed4i<{34O?V(B* zVKI>5=crSeM;rrnnkjiUwT+t-DrWKX)P}Of9FLSIY-jU31H8=4y-i@otsViEH+&gjQ%z-YIpg26AcW4*|nnF_HS)p~lTYC>mg&M;xL zKU6X7zR%UpkTek>MS1G0Nd5XDunnj8Qe->6GEttS^4r%i?Jmt&9$03Y4pq4*Fuk}! zTQVC7#>Xr8?NMw&y=B%m&qmq%>z_dj$8nLW^nGggzhwWH^&YbN&oTd(eQYK_Cw{?l zs~q-(>HErZniT%ft{pRKdtG!G&(g4LP%rMOF z3){2vlaQbGTCbMSUCi?x*%IGiAucs53EQ{ZngVucC!~a4o@aW#%%4Wy1}-bt2`C@? zx}Pq7YVT-hYNs6f=tP__&p`jJr_TgsNX_wsBB*|Z*IT~nJc_Qh_RG+)T;I&x`*?FJ zZ;G?NO)Lak#DLoYOD$_o%pV!=D{Jv9%Ryo=ua`ShLUz=lpSW@~E$(5*Gr8Man?Ejp z)9~0duhSf>DwfWJG#0TV`J>lnS_CkyD9(xpXc!Anm@-0+wdXo z#M4RrlN++>tc1BDpBcUKIB3hbg*0lM=sPg_CMR9{d>5HoUCbL_ygHXsl+A`2xMCzv z+eXcP&gYncK>F_mAW!gTZE2EXwwptY^l3RvRMg1T`(IeFs7pWQYmF84MuGEp1Sy6z z!nfTBOh%Qf5wh5_9PkZiIF1R%$#Pg~m>lR)Z#WV$CXRbffm3kY1V@h~>tC~rZrfN^ zTA_lGy`Dh=^{p%LWF|raEOoi0J%5U+s=eomo@YtZi;pEpRPxM8)SqL}oI);QE33YR zA>Igf|Blx~%_N1RPM5yPdGqM$XQPHvBs4_Hp~J}2yi?(9H+#!1n^GHgA8_dU^G{Zm zfvd&>B@uod(#xcQgn_{b!o#b!4E;{7RnPw;F&_uuYPliz6KZT^`c}dQ>s8!4$|QbA zZJUlc4`}fF22I+~g4A-*QC;#H#hv}ljQy@2McbAQ(s7XPWM1*{Bi5x@1y7HWV2JZ9 zrLp=4K3ro>K(VbLVAWQ_#BA#~6T^^0rx9meE}gJRkU^?G<=d91Tmk56Q3wjN(!ZRzEsC^=#B&d0L=y%Pm;rRoBwPii zU8_`N`)uI&is%*(v?0v&wZ4W8&+EZYog>6oa;#aImbN6??jBo>M9N=_iduKhHqgvP zn8095cLn>j^J2MM*fv9JPBUzz?wk-~*&rd`NpQ8YR8x%PQOK4FCs+-ZT{Ds@Lb%}N zM8hJ}(XZWHtt%D!wjFqQkpkQq0pHCuCXD!M>Z|M5>O9mm&7#V2_qtgETA&C6YUNe|2f1zZi&C<_^vx2CVKuMBZpH``&tCrAx`Uys*(`eaN~A4wN~Kwvh<3Lt z;&j%NO+S9|5u6DND$h|Y75V~YRb4d9lvrjQdnUW;FZ+y=dk3mF_G8@{U~4Ze0!-qz6MNcnNt9pv{I|WXiMpztT7M-YD|>R^JgoT0x1(TGR8_g=PFRDMKY zv#5nmXUUVTs59{~*>LGOuCB1!9*Lp4IItU!?5vVMNIN+A_E?hsyFFq5K$W(LzJSME z!(I11^Ah9l;Ow17bXO0OFYmFJ)iY!MWmif%V^=^f-GAxyd)^I0Mn<;xKdly8K*BlGf0Wt9tt7CDjcRr7q!yu6@LspbBUpHwX+b0j(v;Ws7JW zcWwFU0wwwfWz2NiO|1W*VG&+AXE2TVURo7f^cJV!19{#_SArXw zni;WCwX?Y?mjHvI%bygfzSLpCh?{!~ML9zc0Nw&+*pE;D=X^%~S!cPMGSXB@;Py(B?=v29h0 zeg0bpr16`!Dl&>RdqgS@=q{RC_q0y-m!VT$W|V5_j`I^4FJohyyG&8hfkQd^_|~7? zwqF{@P}(e5RR;FG%UY}_chKn~o2ysM2{a@$sNc&pLKF-C#xQIc+RsIymSnxiMYrNPqbf^sqE&iEj^ZpT&+reG}u;_Z7E1i8#XG415vjs*RSSWSti zQSAw;G=0j{{?7J36a5G(x?k4MS7kM{nZR)>*M_jdt{Anx&hIHS`dfzQxuASyHd8wS zaYOCUfEr-%h$2k@|AmmG9#BMfG&)CE6(u}}ml~iGQJ`?XoKgW@+Qzein4m0P=l=`+ zw@N0?YBG=3t_{8OO5C>ui=*r{qSNLfYK>&riN3;~Y|m{Uld$oVe5=nRkMWbX(AEKz zpndmx*KiByJ)HV(JlnIP^PC)uok-Syp%AxjkZF-G=2jg2$!W#ZDyod&e)Z*%n)boc zWjqH)WEJIWGMMA?mGpz_2>tZ~M)rqZS$KbEV`$;zCq_a?#kw--6t6ywbn> zwxjkws>gl(J$go`0GRw8EX&-kk|;k!`LW>ZWUD08)KY}SC{0th>NaPY;Uqf**Lr+F zajnjQ&gM8imdMXM|2y3_{{Wr^-yJz~&18@Of`S&n6EXM$*x@ z0HSOB3AW6jG!aZ)?5>NJi6>SGdtE(sRG;k+{On8BwRcQZ<7x4%3$7JN z*~i6p0Xo?N@&YI_{7(h91PN`PLS#vvW6*;AZTQtkPzQHInnG1$)9M0^=75e@BuiOj zdf-qMdt<-1kooE112WVk(|Cuwm_5X=F6t9i=cQ*kL^bYd=FbE>WsbCv_5jV(VP z&N!Vzs8;`>07L zen&}7mbbj%a4D%;7OgvEO1WROMQaeE3(~B){nRsK$@Ng)|2F8_|G7nopD1o#-(Xoo zn_vOm}s$+>R&i;_XH^S@F&0*Lq{4`j;wJVG{aM3dK_5`LUm;NmPHgf(!$|Zoe@py!7ZWyY#ATh<%)5AkqLpAzTeUU`h?dX5Ew67q&SEq7z zc(_FJyk>2dQ-TwJE=UXlavjWhBRA%@u{%6lkbiaIEb!G=o3H19z&5n%3&^g>hZWOBWR~Q?^4RoXRxSOIHM3xrdtXRz1pfzhL5jZKbi@RLzQanrI)+B=5W%NzrI_o5yb-T_0EnLfDmP) z?*TEz_SVb;YJoToDh9Cpb@Pyr;KOw$vN*87VF(L3Ri!O-zB^t41%ziHvvB|bAOJ~3 zK~#UpL<8H8PBsPfdN|0ko(0ZR5pkUjQrZh-xNinPRV^1IH1-aI$PM+ezpq^s4EyZ= zG)cC(%!%dz3~>***bgJ4mC(_yPA&*%6IQ_3AL7!Fj%`rODli*U7<>sAJPZcm>j5gi zk5s}zsS1zJ>C#r!McW_a3@%c1d-6lnBN^@6hWnuyS14?(t( zYaBD7cBqB|HpD#u7P`9L!;mJ!QP9ZX>(tV!*EN(S1O^GBs=m7xtjf?jT?e~Y!{N9A z{xEc_cRMRv1w`-$y5C~~kD8%}QsmSQceG2ClXkT-)D-H3HpnW27pGug!~N~wc7&k} z(*-bU5B7@M?*ODC@9KRYMd~BmPE<5+JU*4VDpO4H1oxJfTXq3RK&cKZjFdIR`EfR#ESzeHrDEp~;? zyz?+RzDOW58-?4P;-D|CP#W`{H%3?}oSAK7X6vMOm~$rhHo^~gLDuxB*$+>}a5pb( zKLlQTKNi%pWJy)E1XatIT$V-Gz1zNrAWIAvL7-rl2Qn{c8`R5vB#!+fJ!`sB<7QB| zzw1w}vY(5Y?;M1{z3du^Tl1wC&Aazjm46z0Si3uSEMcg{W>3!sGFHI81q(zVVH3{;PlU zZ*QO8{^1{e_r>QwIi6QeoH#RO)v_$B7D*|u>q;b)I8V%ybIzyJ^8da2*XQGMKA*qy zo$uV;-K9j!vM}dmSz4Qt+}4UVvVwX$WPFcQRoyS}jRx(lH$=l4KB4Vc^S`hXy&CHzhg=Qrc>w6A@{WLaMuUuxZKq^zHGK5(`_!)8K|WHGEa$BdY58e13R%P%6Z2 z*_|zIdZXT3Z@u-UFMa8~_ujK*q-;dkwwRb%RL|%0XP^C)NT!rYbk(i)JQ(h63j0Fa z(U|_W9*tMA;k5lu{A8LAlv7#u?<1_H0ag~LMoI^P2HCS38w({O)0E$O>#bk>#b12) z-FIaXqGabt<|NEK&$Eh5DW^=w$ltYYaGG=L!@8{k1@0fz1p zxCtO~%~GGZ7WS)pN|2ctv=V=)sxPagW-q3+J7=j|5fSJ+vS~XmM)x6FEE>0)JI~uk z1mUTqXvmb%&vQ(N^IT8)OBdo2=Bj`TTA03vOQtWt5DeN-9&D_XSUV|lNCIbF`~0c5 z#|4E3U|;Ox2LQ`)tY;iKqpL#j?li&zZUG?l+!%+`fV0MyFG5r=@c8`1S;P;Vm!4-O##P5RlSF$&UUM`p3mlu+qp5t${xRy`W8C>WfU%G%0W8 zHF3o7r@!t|JObGZen?{g+Zxu?NvsoDFKnP*dMhxFI^jnS1`8le3_IMz5i2}9Wjy)7 zzrk1KlsK^_`{ds_p%yC5+aIOa9&Nx*nX|xTQo!#}&|+*`J_xJ0% zk|sN9$j%(H&u$FybzLorYS?)RzMfymMAS`;kowcjIfo->2xM}e=fmN^%%{`Ij+A$6 zYop<~plcVGNIjMQUXO-T-WR?e4BD{q6j&anis5u}1-C19{7P>aA>bBVf00{X92?-H zpXCa&a^>pL@fTCFPc{-K{n}T*{xAN;zhve=_`~0Ob^lqWEJBZur)f&_A&Y2AB)TS^ zv@B~;Ce8EVmJg4Qrw@Mk-tYhZ@2Qq=fBV)EWziNE*PrYcTC&K zgY&_rB0qeLn(ah((`l))Po6wk*L7Xb>$+NoHSU$+aQ3J1-T!oCEh=pW%V*G1O41~< zS{06z(pt94-?+EzM78m3ckYch-uUvDzigdYIS7L)5!qTQ6EDm1;>8QY#b>yY+DHF@ zTh5~TILh@$KAo1+X*r))&gpnOK0F+kBC4vwDlFTlA54icEelm%+svadmh%w_E+VhJ z_S(I`qQ7R>w0^4S7b3h`cx5QINYRY z#0PPc{_4%bunksFrPe`&AvS!0HD6$we4=tAu#a*fG=pvY3b=ov6&)JzOGK#s?>+Y; z_5k$#d=Wes12mTa3zl=(rtd1Kk6)o5Y0u^w0lc$F+XZkSDURj8rE!258}#(=DMOt+ zgjbL__#>z)?h@fE1ibbzpHqfv{M7rgmR{4~5(-EVe+v-@)G*K=i;V>KIs85P)?iaH ztm6-tjOyNoHlD>v_2WdF;;$Q=D=J`sJ1I&yn7FeZhz>)bIk2_e_S*q-yN&-+2!cR+ z98SARBUdiI5CreF03Y?7Z3)%9rLc|~E>WBU&MC((wcjz=57|BgjB_pq>jQY(He1V} zOTcDD9kzzMua05K|{+0I6rXcQ_PUty$G! zM?s-%C1Nqey#mvmO$sB%#zB%hdvy`K}|3PPfoNNB9J(8O4 z>S5EIvzPKCBL5c3Vg=JQd9XF0=7C-3kz=Q-Q_T-^@)`iP{I?`xq=3`Y&1^p(_b%j4yOxJpqK^jxvR{!3uH=xdRunzdS>P+PdR6@y1HvwmgRUnuIoyB z?kzhbYzPbB+7akM*c>nvCvE3P)n=b`{n{DH8& zX|Ckj{EI-+)Y*dVk=+E3xwjfa?)1P=P?V8dkaPa@)1Un1U;gE*SFh~mx~^v93Q5Y8 zIEhH1A|hM;fUREru6PwlA>av#eTF!N1*eW>^k=2c<9hRq0@7{d#&7AYH zEQiB^HsQ2>MYV{O)8qZ;pMQQl9-lmULZjt`%Yf6Wp9_0F29BRj^`J@NW@^PJK|Di#pOg-~zBe60SCC+qp6!{P9iuYBd{)2HRU?V_*inmA8PbINH- z6DL(!%lUXrKmPHLA08fV@9yl~l#*wphT+0Q(izr<8mdZN-ikjmTNH7zzYHj2-CN?U<~_KBwav_3nI5zZ{j4x-OgQitiHde^`moy_BO^~OmwD;M z&^{0W%=JaU0sxi8?#tSL3Ntzb;!^MA&>^}MsA!jnY4`X_GS_d|zRp5O7n zYf4F^IC&e3p*=SnFtoj{MD`gR4`90arZy)V%IZj4`=CTY2wRIGM9udoXys6h5yFw~ zr<9n8RT=Z$TAR5u9pTZ-jusL&0ET48H6*t8VtHwF@CCL+;X&>&tZG6&jy7&axWtMT zBE_oPM))m-J?sgK2JH(k$H-+#>kNfG_Y}LNfJhL(kGoJIWqSx`;873lB}97Mb!5JR zRO5wo&u4nVI>_*Srxy;}7|AR=wc zN%1}x;ATdu)`l%FuR?-TB)Np^Xtfo9@A2AA=xEK%K(3H==mRByiO^7t8u!<3e`*KJ zl$6XeexclMFeq7pbL}=_cT=PLIV(6iwbMGnjDkAErGJZBiWy!=*zig)%N|efo$z1- z9YTM3C~Vj=6DMLkI!Pg}YR5l#517~DJ@{HxRm-vm9Z<+FYhpLhnXi% zNvX&dqV+Nn6Q_Ba?I3hm{wKuLnG$jis2(U+s|XeZ$->{BG3HzjS;-#eHabf!Lrkt6 z8X*b4hh%a3@Aj~h9OOQCsX{frL0sX_wBGt;43gB93-U!S0! zvU?p)WYx&@?1QfBI?uC+u8ZV}rzwBwov-}%Z~xA^${+l}cR&B^)7v}SX%W)uC)rt5 zoH&szqIyngCX%(BKK$^9B9bQlr{DSY*I$4A@$rOP6<*)wguipRghpQAy$;0s>SE8{WIr(=G zE6J9k>Vel%FMGpb&>Gm)T2`F9^KH-;bFFV%N=drq^Rn-ERZ@hxd`tKxQZ2UJ_2T8; z9$DaPr<H{?)5juf6tK&RJ#5AiBH? z?M0=NihMjC&!<%iiBc(gIxde7$90u0DO_2UixN|w@*(jQx~dc5H6w$5uPc5^$tP$>{{?=WQ2g+857m4yszlD7np z_`zl`>N&?izg?+2wUuBM)Dvxp$Pn2esQ9`Q`I3qa4OFj3+@3$CopB44L`W1;?fP*8 zrb9Ih0?;p^!-(N6ZF4uc{d(8GmpCPr8 zM3)fkf$Sj(J!m_v2V?_C0M$T-nVD!XK~t3_)RFKa5-@1!P{;kc)NQzl@b|qbOr!my z5jvx#VrQT(sSJTG^k5jUbai!!MrM1fbIJM{ZHNnv{5NdiyokDUc~+%?!0ZAb?Kd}8 z+Q_?gjBpa)8nhOPC<@ik0{FfWGjlzmk(rB(O+?rn-RVq?h~gs$JAg#(WMPv?_?8ZP zK$IBr(Tto zSizYgQPe9u_$r%iLImHVr+~u~W_d?}Xe)=}o#&h_Gd=0v38v6;_i=!ypjs!mUBt5Qm1>U<&c)S4%}k7m7oB3A&rqN>w$D5XsEp_HX8<_GZW&))nu z|N4K=dH%yc{Qb{f{A7|sl=75|P+}^j*f6pR6B8*D-`w7)kjVLmAHMeo-_^r3|HFUu z?Wa#)SKUp9!KjIt_OqLM;vmi8&JMq0w~=qinz~EU_k4wtL2%i2fQhK5aQ$t@`tLse z=)e89|8_c^=6POq&C|Ap!xrZzT6MLG^(iG{OL$Da`eG}-k`puAnPAg&@Zq5Jj{_zt zrFowHGl9C@w;i%<){lsmWff5twLVEnSJkAqcZW4kW+&}aeUsQkyVkkA5E08_;Qhc~ zDy1BcM++PWaW&}>ky295`Ss_|-hA`T&wl#JvMjfC8<6*}X=Gj3SFc{Zc=6&ZU-^pB zWGyRl(@h*Rw`cW|)^=M$vq^#-nA;kWtBEO-M=}n>$KRGiTKnw&+{}}Iv9QY@yGY~ z_o|wBT6Nhn=IYsqI0r-`#Kqw?7kqdj6BU*;`%F)&TtB62Z8UY3T0}grO?4|{uWg1G z27qw8)nNd#HxT-t{iNb5tbWAjg;h|P?XVweNy^>gFu-=*)FYK}UTAwu)!vM4R6p?# z+XQ48{4K{f@GC$Y`b4JSpW0^YRQUaMMy6G)xh}r;TtCz6AypB6{~cO7O?p-Jcd7ax zsr{V1MRv=D!a-?QI`;nzQ%iOsqFSF#5l*|zduG8 zmdEs@%Ysd<*j&MScz>w)ICKxsrGDVu*r2}Bh&TKgba=^X1u%Na55#E!Xb|MKLw^x7 zF=?~O@Bw~Zxr9;b`$b7Dm8=J#54Vx8VzkNX-QLpuhy`ouJ!pGh(YppI$~I4N6s;gR zJq_^=SN}Qe2#SaetnK$NyLgC* zs%qlYo^aLkrRk6-B@$)J<}smgI0LDfIebIoG{+49|Dmeux>~WmK3R3Ad7FS7c;{Uq zM$Wiq+)k0*d%>Fo3PS!(Mok#48f+&1jT9%*VMw%1Lr&MFF6@b0h&1Zih$D-T3On)bY4-b+Zc zSrRW8rCLgPJT2}u)x(6`jkJ8Y^HyWQ5o}~wHjH;P)yg(FUM@(xHDK1gvUVo2&yhF8 zZ|1|@&F#&6NW`m%7qqO$$Qu#taD`^O5eaqW8d4Rd5k;}u+Ml)a&_7NYy z_@DmM?|%O3<^S+o|Kqb~Z={q=^!q4&Pci>%rc~hoK3uM zmU{pGmWcRCj%7;Bxsa}_+&>=YLoTH}Jf6QuKEg6ws+`f2+m zBGWV-4u>y)`O8)`*%(x4pJh`@bIK{DMEu!jKfS-dmr}TTax!S+kEyw?WKe#>&UyGT zoVPlQG01U1N)9Ir?-nT*ES3M8QX*=Y5?F>N2uno|O&@U(8h+?BYKIEH9M~_`%Zp2U?>Jbp8FShV7wFL8EpmEDc%Ql zrAAW|c8tPiK={oZb6&ziy=Q1JQ#~RC3+bLULyGyM(BY+ihOcwyKhQ|d!xa*j5Vhee zSERj%hk)bMQ7^RQuF|5n2`uwRVe+L$R-1kRp0Pdb=`bYPC4s+02NZbhzh_f#7SMJL zK?-9A#`XUW5kcUCQX>Ea92sUa^-3K83oC1)Cwt);~fjg`OQeUzaJo?rofjK@LO4VtuMK@-rQbl#;!H z?Zb5i9berbM%pDjBK2&JuvM7F*StDhs#RruEL?dZ3Vp&s-z@OUF3k5-ui^^Z$*7n? zU#OKg!^P?4@je6wsZAisLDB>=dA&FEEuA=8XMBapl-p=y*wb}NA}%@SlrjR~WOv%y z1^^Od2fmGWJ5L=2sWrPWk;|#U%*j%_bc^=bFsdm^@R8`k? zwPZD^-=6j=(CYhcfl*X?33r6JwN4&KR>Vd|@A8AssMUX@<8BUGIK0y^fOirg_Ld<`=D&kh<0yN}&7e0(_G`tUVE(!rlwkxX z{29pNYG%hqr8F^fDN2-7XLs*kli{+i3vHV)l5)po zWm#^f`RUWADJA<@KtvuMj?9^8ui9?Z$h?11V)MQInetRhd3;h+pV-D#T+-r;a~ z@!3nNTahUdb1Frb^ZIx^FUzv7Qi_&U?{1&Y^G!-=T}yB2g2_Yk(kmblr8F%|IUEk} zyz|b6{!GiV+#GJs=kv|Y9T72W&O9G-5m}byv(E@?jNAYKAOJ~3K~z4oZ57<}WVVD> zsW_^gUFi^65=R9u7UT*}4_&^(l!FszP*{wfhzRo@Wvg@;2ZIzsd#RQ^9d6k8C?;th zd_hFQTh=W29~&8^F%Hruol$Eg2&|A>TJpnNp`~x4pl=uK|84=gL2+C^YRQZNwLylDL@aPYuo76KxgFI8Waz+H5#jY%24O5Y|2({60L*1rJ+nbrDT)9TmG?lVk zp&0%MkA_E;hD+LrSdiGENc55rbkpsyZvKqX7U|G@r%XgH-BP!-(U{|kE+}}AK!>OI z^BGKK(0BA?0)3cSwGb)oAMX!@-``{S!n;=x{4>Eet?G{Qkk<~_Pyl*;bo!)}ME2?{ zNF_V5ox#L;L_^>hA88YadU~RCxd7muAT4&8a-oq7qoC+w10*0~RFV3EWA6Jq2v~2m z5J(q(e9Ld5`NgL$*xqe@YtP=;XvmGaY5kCyMMcE7z@s&5{|roFB^8Cb`OJ2-LZkKl z)`+;w_pHgpEW5H_A#$KCs;$%0jW8;sD?(~yJ5Hul^{A8r6GU_2ny&RxiHIyZs7jDaB=xnG>;!GIJ@SBARsdJYQ{$!1fzX8F5gVxsaB!t(O(NZU+Mm zqr+^ZXhMY5Iq@EP!468g%VdQI`}CF!E*7;qi;l6wiQ`f;X+qcp+*Gtq-jB-?fjvYq z!v@sqg%R2MnWkx;=Pj!SPMqT!efC)_P&iP@Ig%jcy}e=@UA7Vy=6Gmz6boq~RoMr{ zAs8gV2Tf_qct!biT%6JKVf!9;U02d1B5OH|t~uvv-U==DTn=*XIBKnF>z)tNY9lyR zdoH_xA%hq~Ig2a)1vpVQG=-$MTf^8S)d8ENsFZFW2cms^Ljp?i_p2ODdn8z zoIJG6A{+f5A0JC8LMqNF>$3rJ<9GjKnUN6$G|M~*Q9=|JV9tH- zN{VWDpp-K4gop_>fWLSEM1*UO8@-xP>gcF>Ugmk;@Aoz~r<4wd!#IvPv)-SmD|~W^ z7ob&X@Mc#etv#s~|K%*@M#Q`= zxc7Q&%MUoC);jn^@4LLaySu%;ef8>9&KdSQ<^Ei+GEizu&iOq3@)w^RkH_6^m(s}G z1YBdc!t4zzxNyrJNC((D{$Mv1z;>%SD3KpX0wlqVggQYhd!>VFkua?pnmo)s1&l2A zZ_AEmTHx(oXtSqPC;?z;oqau~9V7sCqbOjsNh_%G9EiDP55EBNO^~35T|j36`UWoz zI@V@yWAzocPXE45``lyKcVzqxjh@n2hR%z_uHaBT%bBkfkx;OjvR|Ms%s1)v8Y=&2_lbb1kL*0+A)?f0nS z36)FTL90ibhOS@Iiz}X|;WyTmupdu7eJ(i^dV(AEY<+9J*e+&}3KMlt1Z=$z*v|Mj zVenet>(Qo42Fod1c@Nvl7iZ3}N|BS*%h>fWTp}+xSqj$A|LO6))K2g>88p;el}%CX+Yi+c zH@v{Tf$Dn)Hw6u^`f+))HQ16^|KTN?*sD#(^(E*?&`M}&2+>MtN9*DJO|pcCy)EjY zwXdWDz^ZvKdEau6QILGAe<^pFSKDh zDhQt8Ys07&#njsIpjv(F-LCo4n*COjIm!b!1Aj%98-xPabW0`>-%A}353isW4Av!@s8R5nD*Ebt zKJRurO7!+u-u?aG`{p>NKlzhCI?u=5K&PoP7e>I6z0Ut1dGKZ5h0qU z>E+9pWuAwdo0+JTqKSe{GB zk&)9b?eZW1Ip+^Q{OJDG!;>dZhGEDVq~w7TLsWqF)F#-qrp~b>v>=z4AH9(bFl-L$p?zziWo|PPNqfXeL~0JQ#hY2bMYE$O zApi)#tu)4tdUQOa12sIrVHFpKPT86uR-Ou?V0)NSwFZ|&=sHyCZ;^E_0N718D|6YQ zzagB`9WlE;R0ZN7CL2R_1!Pn*m9aYzM1*Pz1uuSxOH||~{i=Yanz`6?tx2mFu;^`z z2duH%OFV@tYo#mj|e3JOuTdbv1g{a;4MeT^tqmk50W4?RM|XwQdd3TPUjd>{VcARdH-Y?+7Jo|BI?O7 z&kjT+0d`phxW)?y#j*O<7ZD*XB+WS!AQ1sE01%RgHVT}RE8CDj8-;5=YJ1)&JF8md zUhk|HQHBG5|^?ZkJDTeT`(P+WmC-Ug^rMe{ay*|HnFug}dT#-MQIF#YB63+1V4JoO zk)^}+z%a8$;93-e-y{?fE-WJBIO>Usob#(!ulD;r)nkdJl=FF>=Xs>G-|u(39RQTF znDz7dEK-PQ7?Kg0%W_&%>T(fkm~bftiYC(bBk@LZk4|YdKjZMm8*jYv#v7;8NklTk zz`7$+M*;vT{QC84jms9d_$+Pm{x38Hl~M89AE|duj$;~kX}22=`~Bf?*d6u*p$Ja7 zOr;D|(r!E)Zc`coPSVaf&`pASNH%4LSw~-Z^ytxh@4ff^?|(n%oQ0P>6by*Vz?@Q| zl#Adme({U@`+F{6cZK-|gPrXMwoT*E1L;3RL~Ij3>E1vvDcId<8#KbVtlV!p77+(w zu$)rooo0(HJB?of`hnky`K&{4o6^!-?EYK-#1q{2zBxo~TAm<>o{xbVzS<_TVW9Qn zg$E=24iC-7hOs8V9O&RBus+{5vpvOp?|okZ4D6;CzNt{hh78ZvKsuwabnb~B+D)u` z$XhJp62$}1E36j-aPwDBUZXZtb=wpb2UxS3^7R8>HkHDf^5@io_F}-)vL3;< zPKn5~!n;&@Yv3EK)_Z$E59TPm>Kkmx>ssrbM9?kodpH{<-MRs0ZK{19#27kr1eZ6D zd!k8Qy#~4RlH>UvTK41$*crKO!17&RGAThRtYzsoL^u1|#2TD{_4T0@PEVps`1;?~ z6B;FaaWK_%F}^R2&9wHVHD959Pca{Z39U#3;6qfP)%07W3ZX^|YUoK1gNMyyu?cOU zfM5G-|A;sM7H9-4e2Xr}t{1<+AZy=G544UUqKlp7wkwy_K~eUC?PX?Jyg6EV76e`_ zq?YDHsePg`Sm6pXB0^sI1pvC(ABKV5m2@6u>rTN_&!FMruNm~qrN+Ws=2W06I4`!* za8oHZ_0Ft>*KSX@9vl38P~Ae+U%x@5DSj!AD48!?+{NWFO_~OM2 zW(EQxLINPvU|TMXD7JeB08A`UonL??Fd(Q`6aYpViAYUtDH#zF2oVJV2on+_)}*(9 zglSBea5|mmd7koocXxMtxK+>lJkORr(1^KQWms4W(=ZSb3W(-La5j#J0MrS+9;zzr zQ)Vs1(Gd<2nOE8(CPabiy47!2x}sP9^|;xb5{c zj9$j9!0QSU&hvCW9|1VkayT3ghr=>*1I|;)Gp7N^aqp8+_(r^F1(O;KrMKE5LX-}N z!+Y<&XVajWOXiY!iI^rLOoWJB_~px&pMU(=9mS=0IR@sYdl9$ zsYxNl6{EB7E)29iBOJM$;qI>I>VMmZMm?&TdU~cC7<@TIH$v4+Al5TJwW1oLb*H4~ zW@wNuTm=?r)j#ym>hG{a6G-Ii_p%M0H2Q*Wm=D#A+P!{VDt(W&i;dndQO3VQL;&$^ z_PPe`k;XuZFid#s5u`K-wx`TyG)ky^2@$RC$*aAGIc3j8`!itIa|&89uqk*;#+51T zGHH6F;hNRm5wuTR2qmEgu6n*wO`7ka>?^X5ax zJNH{t&y$W;9zJh;pcVBVyDR#5#kgJiT-N-*7lp^z3R}g~E%fVw_rUmtWdJPj-D`xm z8(wsKF;E@v2?CD49<`vS{=>l8p3#9a)Ln5IFf}YL(NiHIifp}N-1^e%QFz`r*b_WJ z)f4AZFZOVgHC_h7w|zUIg-aEJq4vK`l6yggHZ?&HkAH3+9D~P>xQQDGM8sBPBI55X zSx-rB6$8wSZEptxcweYQp^+$u3!rw}N7EX*xlmxV{d_N3IZ2EKw^3Z7yQcvp5n$X$-C2R;m@K(_TFYKE(?C3rWdWv$GM6c z`P`&`TKxmnZZBvrkEOoSQaU~=xqO1%fVHQm*#z$h)|IR+g#C=dbGa@dHy9-D5>frv za>OH|CXqnrqVlFmWmZHG#gYK5DFWKK)ZUD^--_&-^}-fD05H$9(NJ80R*UUJ@1o@z zEsM)KLPbJ*XGRthtV=mSO>Dv6BIu3p^(MYdH(#cXSFrxu z;AF8^(h@~v%~-by!^Vk=wwedP*%9T$W=bpuKxWPQj5+guf5V(hDTt{QxqJM^H@@-D z6Apj$$N&4`Ag^8?pS*E9&l!YoZuawh77+kV(SiU1$itwE5d7KCKl<)>zcUQuKlt_E zJRELKZH!hPVC=@=UP_@=X{+AjwP{EC@+CT5TSLTg9Es?YXPz%d#c%z$)Wr$CAC^P}x*WJFasWW2ws(np z)nws_IF(#V5k}qWytF%gNhB&5%^X*`pv2tmc5lD^_Bf8`^H~@~F!SnXq4knho~G&3 zPe0AM>~_0h5Ix^fFFah$s7D#v>E?yOxEpU|gZZJgtJ#wxgU{d}v_l#BY+Q?DP7 z*E#~Qb+v+_?+I>*d%Nl@V0Bv~x|?NO&0fvD80!;i&mddWfCn!?<%9b+ZMd=Nx@igv zq0JFA2!CifbO-I^?8-RhsqNAC53RdW8k%@YZi(W>(vu511@{!_pLn{3x4n@!kTxB} zo9b5S*??zFnMQ|C{9+ZH;8`-V0F~)-m@ow58*}{#7+?kq!|bl939{FNA?+l!r;j_= z)imM-%)f8DSf%uerS0|IivLRLLQdMt8Tw|wjJ?`6O>Ob;fMa)GVbf6fSH2^!ux`z~ zwu$W}&d`V@1}dT3BR2^QrYUeaU@eyoRF87|>IWN^|}4(LFoV70!mT+#^N z_!Vkjs$6-r>~N&5tFfV_8`gOvw1Ni!{@O0W=6=gfBh$A4P5?G3_JwGAeFQ&VVCMpJ zzdbd)Ui;ro?|!)N7%gpjAOCHj?G@eWk?N@t6n~Spq2?v~=J9nNz-`xwN6Jskde(@u zkg_$SHfPW!1?iH4`w;ELU!(SanL`g=ug#Tbp7`pUqzn!_+04hPGDyX4NShy*eA^aRYz5JY)e8q-#8eWmAZ3Jg9(ieN`hlg2O;L%iB-i`JaCGcY*Ma{*V8S zhT(LahLKW&m-nxBgKlA!oF||pj0J=!r8J1(d76IkgCCRv^HhHQ*MH;9H{S#RjZ626 z-E5CncjpzBnd;l<3?Q4w=Oa%Y2fYDeS+a&3RHlvgn`-vk`}>za_`wfeym(Pc8FvE? zh-qoLERovC%z#2jB9bB3owh|Jm{~*d6Rx|Oh?!_0TJz5?Lo#9EGNxtg#s*gaNaHY$ zBO*@IS>Fu9kkU8|Ln6KIOqiK-K}1%9h)^QLq`5wZG!AJbqC7J~LK5qxlnm>n!30K# z$PA4L0uZ!Omeqy=AwIgjQ}?B4?f_go7eqvW1%w&yUp@Ttm%r4DBUgXN(&qNnSiR{F zyf)s=^Z9%}=GY;h^{t(KwE27}R^d+wHt*Mns!rbhDW_RC`cGWEh4g zZ@zK+_|e0|X%SXtz1feNb5G37AAkJu@pydn=ysigbXw34oN!?(R-Z3m+j`zE!xa$( z;9QEVeA(NUVz`X=D{{ly_xlV?8ReMfp+=l;q=1aQjaxk#M<#OJE47u64gAhKaF3P z1jEy+hZ9=)^gXu0FJwuav2Kioh?}5vo(S4zwfx(w;$BL<&O%&7X=Tl~zLL~4I&jJ! z272}~=4iN{k%Xb;m-yLfO-*eol{|rVbNf>4SjDxZD6yJw+GT#G1v`S>GEA%lJ-mJY(i*uK$-4+2VQ4`6@qBh( z+a%hiu-P+D($IcrBv=6J+E+6o1e*8lL&vqCKE5x%EcXQWy4HgoDCt8J`kI?!03G`- zO)bE&{_^JNrL6)CSZLx|boIXsc=qE9clQIEHiZOwWkCiIghrUxzUuF;JGW|U;SrJL z3YHc&t-#E7-iVnI1UmNlW`m^-in)ne|81a8zpuIZ%1frV0GpKW)g;)oO*OYa*{*%N zAQB!yPcCWCe*_8;kzI!q?Ny6fj}ho;k=k!yN{_h6dQ}Lzp}~l04#!?!#C1gO49cc) z*t7;x)5ZF*p@5EoP41-BfX=Dy;McEpU9U}%faC%qBw*){13cERd)WNxr)uk5s8#Aq ztTefTDl_48)|m#2)n8tzTf+&SE(4dlU<3ewW%UJ{qu(MiYo`mHJv?jJ4N^+WNqogl z19U=>PN>4aCMVtonomZ^C1-F>lK6HaKg zCZ?1Mv&O&`kd)H6OX@B+I7=zad74go;gE_`5k0c(a8Y<1cFi8o;_@YC0a(dWJT8k` zB2n7!_qVsVDJ6lDdColB9ERHCWeZIzFMA4yg58Xwf4lk0igXiR4I@n{m05G|QYnSqW!561?p+(Cb>lAQObJLxe^1j?$_xNd zbZl6S4kGS$JB>s(a*60{l5(DhVPG!IEF|MNK7Rc8aJW4^+=~cHGpj%VW3-0a?!AsHNUfkVXLveSCOK~giZ7Idw-J!S@pZqgx z-tStGhjWkYy+5}Mf6DW6dTrAk4fmYuV_ub6t<)E3lH&s}iN9WDc+_U>VF2VP*AGnS zyY0hp=rO4Ok^vc^=vd8^V{TT#=m}B~Pw7;R&W;Y>mw#8*lM?kqj_i?2Ou7QL9%|RpKO1i~1L`0N+jM=pcI6{AtV5AH^`|1%fWm?H6I>*-_C;y=4F*t8meO>{B( z1y(5Eg9OZXz*${t4 z3QKi!%ySj2hzhO{Q1{dG#Km`H!Y0(9UfeFnv4yeW->AV`V&d#8v%DK~z}1kxi6pyHugYL>`YzE@-#U{}Rn{zG24O3seaKD!jY695$= z!Wm_$ZbB+@N0tsI=s+t%@|$yX^Wp5${16o^Y1LFovZ1+V9ATm3D!iP1_MsGQjME#n zqPA)RH|*a_5(8}Zl(!Q{7kjh^b?^*zpF{~piC-o{?Q?r{c4sZ*v9Y%u11}m;M+r84 zLI_r~XXt`zb3T2=#s2;M$5R6HF~6}C%jYe#YH0B4o%d&%7(h{}Vmdg82h5L4B@jH%Y zzWfk@>-%65Ok=7>-hPkhapU7L6!Olqq~eh3B5=)y)S1d;PY9xF$|B-8FBk|yVgD{a zj0BNnR6c)+hbEcAPPov3uwaV;NIcLeVwOtQQAKnZc-;({AKgokc35m(kUWr}jgUG8 zk6xclxl@%TtPD^3@y~2q-<9h7V|8YI-*dk6p)YKR>j&a2YUs8*q94Q=iqrtW(6Er8 z$fBDZP7Jz+ao@P`?z>ri-xWOnG~100BXDH*k1m#qN`T^&&J1?}xy