From f146c20d84f557a36808ab3dac9a0a4a1fcab006 Mon Sep 17 00:00:00 2001 From: Saurabh <41580629+saurabhsharma2u@users.noreply.github.com> Date: Fri, 6 Mar 2026 04:28:37 -0800 Subject: [PATCH] chore: remove binary preview assets to avoid PR binary unsupported errors --- .gitignore | 3 + apps/server-node/src/index.ts | 55 +++-- apps/web/src/app/api/og/route.ts | 46 +++- .../app/api/templates/[id]/preview/route.ts | 46 ++-- apps/web/src/app/globals.css | 159 +++++++------ apps/web/src/app/layout.tsx | 66 +++++- apps/web/src/app/page.tsx | 176 +++++++------- apps/web/src/app/preview/page.tsx | 2 +- apps/worker-cf/src/index.ts | 140 ++++++----- {packages => internal}/sandbox/package.json | 5 +- {packages => internal}/sandbox/src/index.ts | 0 {packages => internal}/sandbox/tsconfig.json | 0 packages/core/README.md | 47 ++++ packages/core/package.json | 2 +- packages/core/src/cache-key.ts | 13 ++ packages/core/src/fonts.ts | 219 ++++++++++++++++-- packages/core/src/handler.ts | 48 +++- packages/core/src/index.ts | 21 +- packages/core/src/params.ts | 13 ++ packages/core/src/render.ts | 36 ++- packages/core/src/wasm-edge.ts | 22 +- packages/core/src/wasm-node.ts | 20 +- packages/core/src/wasm.ts | 33 ++- packages/types/README.md | 48 ++++ packages/types/src/index.ts | 144 ++++++++++++ pnpm-lock.yaml | 16 +- pnpm-workspace.yaml | 3 +- templates/_base/package.json | 3 +- templates/dark/index.tsx | 170 +++++++------- templates/dark/metadata.json | 8 +- templates/dark/package.json | 3 +- templates/dark/preview.png | Bin 40519 -> 0 bytes templates/minimal/index.tsx | 128 +++++----- templates/minimal/metadata.json | 8 +- templates/minimal/package.json | 3 +- templates/minimal/preview.png | Bin 19222 -> 0 bytes templates/sunset/index.tsx | 161 ++++++++----- templates/sunset/metadata.json | 8 +- templates/sunset/package.json | 3 +- templates/sunset/preview.png | Bin 110277 -> 0 bytes turbo.json | 12 +- 41 files changed, 1322 insertions(+), 568 deletions(-) rename {packages => internal}/sandbox/package.json (84%) rename {packages => internal}/sandbox/src/index.ts (100%) rename {packages => internal}/sandbox/tsconfig.json (100%) create mode 100644 packages/core/README.md create mode 100644 packages/types/README.md delete mode 100644 templates/dark/preview.png delete mode 100644 templates/minimal/preview.png delete mode 100644 templates/sunset/preview.png diff --git a/.gitignore b/.gitignore index 9cbb0b5..4fd7f31 100644 --- a/.gitignore +++ b/.gitignore @@ -10,3 +10,6 @@ coverage !.env.example .og-cache .temp + +# generated previews (binary, excluded from PR tooling) +templates/*/preview.png diff --git a/apps/server-node/src/index.ts b/apps/server-node/src/index.ts index 3384718..5929c5e 100644 --- a/apps/server-node/src/index.ts +++ b/apps/server-node/src/index.ts @@ -1,7 +1,7 @@ -import express, { type Request, type Response } from 'express'; import { nodeAdapter } from '@og-engine/adapter-node'; import { createHandler } from '@og-engine/core'; -import type { OGRequest, MetaRequest } from '@og-engine/types'; +import type { MetaRequest, OGRequest, PlatformSize } from '@og-engine/types'; +import express, { type Request, type Response } from 'express'; const app = express(); const port = process.env.PORT ?? 3000; @@ -12,6 +12,33 @@ const adapter = nodeAdapter({ }); const handler = createHandler({ platform: adapter }); +const PLATFORM_SIZES: ReadonlyArray = [ + 'twitter-og', + 'facebook-og', + 'linkedin-og', + 'ig-post', + 'ig-story', + 'discord', + 'whatsapp', + 'github', + 'og' +]; + +function parseSize(value: unknown): PlatformSize { + if (typeof value === 'string' && PLATFORM_SIZES.includes(value as PlatformSize)) { + return value as PlatformSize; + } + + return 'og'; +} + +function parseFormat(value: unknown): OGRequest['format'] { + if (value === 'jpeg' || value === 'png') { + return value; + } + + return 'png'; +} app.get('/health', (_request: Request, response: Response) => { response.json({ ok: true }); @@ -19,16 +46,16 @@ app.get('/health', (_request: Request, response: Response) => { app.get('/api/og', async (req: Request, res: Response) => { try { - const { template, size = 'og', format = 'png', ...params } = req.query; + const { template, size, format, ...params } = req.query; - if (!template) { + if (!template || typeof template !== 'string') { return res.status(400).send('Missing template parameter'); } const ogReq: OGRequest = { - template: template as string, - size: size as any, - format: format as any, + template, + size: parseSize(size), + format: parseFormat(format), params: params as Record }; @@ -40,22 +67,21 @@ app.get('/api/og', async (req: Request, res: Response) => { }); res.send(buffer); } catch (error) { - console.error('Render error:', error); res.status(500).send(error instanceof Error ? error.message : 'Internal Server Error'); } }); app.get('/api/meta', async (req: Request, res: Response) => { try { - const { template, size = 'og', ...params } = req.query; + const { template, size, ...params } = req.query; - if (!template) { + if (!template || typeof template !== 'string') { return res.status(400).send('Missing template parameter'); } const metaReq: MetaRequest = { - template: template as string, - size: size as any, + template, + size: parseSize(size), params: params as Record, baseUrl: `${req.protocol}://${req.get('host')}` }; @@ -63,11 +89,8 @@ app.get('/api/meta', async (req: Request, res: Response) => { const meta = await handler.handleMetaRequest(metaReq); res.json(meta); } catch (error) { - console.error('Meta error:', error); res.status(500).send(error instanceof Error ? error.message : 'Internal Server Error'); } }); -app.listen(port, () => { - console.log(`Server listening on port ${port}`); -}); +app.listen(port); diff --git a/apps/web/src/app/api/og/route.ts b/apps/web/src/app/api/og/route.ts index e867e32..3e7fb0c 100644 --- a/apps/web/src/app/api/og/route.ts +++ b/apps/web/src/app/api/og/route.ts @@ -1,13 +1,42 @@ import { NextRequest } from 'next/server'; import { nodeAdapter } from '@og-engine/adapter-node'; import { createHandler } from '@og-engine/core'; -import type { OGRequest } from '@og-engine/types'; +import type { OGRequest, PlatformSize } from '@og-engine/types'; + + +const PLATFORM_SIZES: ReadonlyArray = [ + 'twitter-og', + 'facebook-og', + 'linkedin-og', + 'ig-post', + 'ig-story', + 'discord', + 'whatsapp', + 'github', + 'og' +]; + +function parseSize(value: string | null): PlatformSize { + if (value && PLATFORM_SIZES.includes(value as PlatformSize)) { + return value as PlatformSize; + } + + return 'og'; +} + +function parseFormat(value: string | null): OGRequest['format'] { + if (value === 'jpeg' || value === 'png') { + return value; + } + + return 'png'; +} export async function GET(req: NextRequest) { const { searchParams } = new URL(req.url); const template = searchParams.get('template'); - const size = searchParams.get('size') || 'og'; - const format = searchParams.get('format') || 'png'; + const size = searchParams.get('size'); + const format = searchParams.get('format'); if (!template) { return new Response('Missing template', { status: 400 }); @@ -30,23 +59,22 @@ export async function GET(req: NextRequest) { const ogReq: OGRequest = { template, - size: size as any, - format: format as any, + size: parseSize(size), + format: parseFormat(format), params }; try { const { buffer, contentType, headers } = await handler.handleImageRequest(ogReq); - return new Response(buffer as any, { + return new Response(buffer as unknown as BodyInit, { status: 200, headers: { 'Content-Type': contentType, ...headers } }); - } catch (err) { - console.error('API Error details:', err); - return new Response(err instanceof Error ? err.message : 'Internal error', { status: 500 }); + } catch (error) { + return new Response(error instanceof Error ? error.message : 'Internal error', { status: 500 }); } } diff --git a/apps/web/src/app/api/templates/[id]/preview/route.ts b/apps/web/src/app/api/templates/[id]/preview/route.ts index c50a7d3..d57376a 100644 --- a/apps/web/src/app/api/templates/[id]/preview/route.ts +++ b/apps/web/src/app/api/templates/[id]/preview/route.ts @@ -1,32 +1,32 @@ -import { NextRequest, NextResponse } from 'next/server'; import fs from 'node:fs'; import path from 'node:path'; +import { NextRequest, NextResponse } from 'next/server'; export async function GET( - request: NextRequest, - { params }: { params: { id: string } } + _request: NextRequest, + { params }: { params: Promise<{ id: string }> } ) { - const { id } = params; - const previewPath = path.join(process.cwd(), '../../templates', id, 'preview.png'); - const fallbackPath = path.join(process.cwd(), '../../templates', id, 'preview.svg'); - - try { - if (fs.existsSync(previewPath)) { - const buffer = fs.readFileSync(previewPath); - return new NextResponse(buffer, { - headers: { 'Content-Type': 'image/png' }, - }); - } + const { id } = await params; + const previewPath = path.join(process.cwd(), '../../templates', id, 'preview.png'); + const fallbackPath = path.join(process.cwd(), '../../templates', id, 'preview.svg'); - if (fs.existsSync(fallbackPath)) { - const buffer = fs.readFileSync(fallbackPath); - return new NextResponse(buffer, { - headers: { 'Content-Type': 'image/svg+xml' }, - }); - } + try { + if (fs.existsSync(previewPath)) { + const buffer = fs.readFileSync(previewPath); + return new NextResponse(buffer, { + headers: { 'Content-Type': 'image/png' } + }); + } - return new NextResponse('Not Found', { status: 404 }); - } catch (err) { - return new NextResponse('Internal Error', { status: 500 }); + if (fs.existsSync(fallbackPath)) { + const buffer = fs.readFileSync(fallbackPath); + return new NextResponse(buffer, { + headers: { 'Content-Type': 'image/svg+xml' } + }); } + + return new NextResponse('Not Found', { status: 404 }); + } catch { + return new NextResponse('Internal Error', { status: 500 }); + } } diff --git a/apps/web/src/app/globals.css b/apps/web/src/app/globals.css index d516af3..9326246 100644 --- a/apps/web/src/app/globals.css +++ b/apps/web/src/app/globals.css @@ -1,29 +1,38 @@ :root { - --background: #000000; - --foreground: #ffffff; - --muted: #888888; - --muted-foreground: #a1a1aa; - --accent: #3b82f6; - --accent-foreground: #ffffff; - --border: #27272a; - --input: #27272a; - --ring: #3b82f6; - --radius: 0.75rem; + --bg-base: #0c0c0e; + --bg-surface: #141416; + --bg-elevated: #1c1c1f; + --bg-overlay: #242428; + --border-subtle: #242428; + --border-default: #2e2e33; + --border-strong: #3d3d44; + --text-primary: #f0eff2; + --text-secondary: #8b8a94; + --text-tertiary: #55545e; + --accent: #e8a020; + --accent-dim: rgba(232, 160, 32, 0.12); + --accent-border: rgba(232, 160, 32, 0.25); + --success: #22c55e; + --error: #ef4444; + --radius-sm: 4px; + --radius-md: 8px; + --radius-lg: 12px; + --shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.4); + --shadow-md: 0 4px 16px rgba(0, 0, 0, 0.5); + --shadow-lg: 0 8px 32px rgba(0, 0, 0, 0.6); } * { box-sizing: border-box; - padding: 0; margin: 0; + padding: 0; } html, body { - max-width: 100vw; - overflow-x: hidden; - background-color: var(--background); - color: var(--foreground); - font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif; + background: var(--bg-base); + color: var(--text-primary); + font-family: var(--font-body), sans-serif; } a { @@ -31,78 +40,88 @@ a { text-decoration: none; } -@media (prefers-color-scheme: dark) { - :root { - --background: #000000; - --foreground: #ffffff; - } +.top-nav { + position: sticky; + top: 0; + z-index: 30; + height: 48px; + display: flex; + align-items: center; + justify-content: space-between; + padding: 0 20px; + border-bottom: 1px solid var(--border-subtle); + background: rgba(12, 12, 14, 0.8); + backdrop-filter: blur(12px); } -.glass { - background: rgba(255, 255, 255, 0.03); - backdrop-filter: blur(10px); - -webkit-backdrop-filter: blur(10px); - border: 1px solid rgba(255, 255, 255, 0.1); - border-radius: var(--radius); +.nav-left, +.nav-center, +.nav-right { + display: flex; + align-items: center; + gap: 14px; } -.gradient-text { - background: linear-gradient(135deg, #60a5fa 0%, #a855f7 100%); - -webkit-background-clip: text; - background-clip: text; - -webkit-text-fill-color: transparent; -} - -.accent { +.logo-mark { + width: 24px; + height: 24px; + border-radius: 6px; + display: flex; + align-items: center; + justify-content: center; + font-size: 10px; + font-weight: 700; + background: var(--accent-dim); + border: 1px solid var(--accent-border); color: var(--accent); + font-family: var(--font-mono), monospace; } -.docs-content pre { - color: var(--muted-foreground); +.wordmark { + font-family: var(--font-display), sans-serif; + font-weight: 700; + font-size: 14px; } -.docs-content h1 { - font-size: 2.5rem; - font-weight: 800; - margin-bottom: 2rem; - color: white; +.nav-center a { + color: var(--text-secondary); + font-size: 13px; } -.docs-content h2 { - font-size: 1.75rem; - font-weight: 700; - margin: 2.5rem 0 1rem; - color: white; +.nav-center a:hover { + color: var(--text-primary); } -.docs-content h3 { - font-size: 1.25rem; - font-weight: 600; - margin: 2rem 0 1rem; - color: #60a5fa; +.ghost-btn, +.cta-btn { + border-radius: var(--radius-md); + font-size: 12px; + padding: 6px 10px; + border: 1px solid var(--border-default); } -.docs-content code { - background: rgba(255, 255, 255, 0.1); - padding: 0.2rem 0.4rem; - border-radius: 0.25rem; - font-size: 0.9em; +.ghost-btn { + color: var(--text-secondary); + background: transparent; } -.docs-content table { - width: 100%; - border-collapse: collapse; - margin: 2rem 0; +.cta-btn { + color: var(--accent); + border-color: var(--accent-border); + background: var(--accent-dim); } -.docs-content th { - text-align: left; - padding: 0.75rem; - border-bottom: 2px solid var(--border); - opacity: 0.8; +.site-footer { + margin-top: 28px; + border-top: 1px solid var(--border-subtle); + padding: 16px 24px; + color: var(--text-secondary); + display: flex; + justify-content: space-between; + font-size: 12px; } -.docs-content td { - padding: 0.75rem; - border-bottom: 1px solid var(--border); -} \ No newline at end of file +.site-footer div { + display: flex; + gap: 12px; +} diff --git a/apps/web/src/app/layout.tsx b/apps/web/src/app/layout.tsx index 1b5a305..df786d1 100644 --- a/apps/web/src/app/layout.tsx +++ b/apps/web/src/app/layout.tsx @@ -1,15 +1,67 @@ import type { Metadata } from 'next'; +import { Bricolage_Grotesque, Geist, Geist_Mono } from 'next/font/google'; +import Link from 'next/link'; import './globals.css'; +const fontDisplay = Bricolage_Grotesque({ + subsets: ['latin'], + variable: '--font-display', + weight: ['400', '500', '600', '700', '800'] +}); + +const fontBody = Geist({ + subsets: ['latin'], + variable: '--font-body' +}); + +const fontMono = Geist_Mono({ + subsets: ['latin'], + variable: '--font-mono' +}); + export const metadata: Metadata = { - title: 'og-engine | Social Image Generation API', - description: 'Open-source, platform-agnostic social image generation engine.' + title: 'og-engine | Social Image Generation API', + description: 'Open-source, platform-agnostic social image generation engine.' }; export default function RootLayout({ children }: { children: React.ReactNode }) { - return ( - - {children} - - ); + return ( + + +
+
+
OG
+ og-engine +
+ + +
+ {children} + + + + ); } diff --git a/apps/web/src/app/page.tsx b/apps/web/src/app/page.tsx index 4431ddb..893e414 100644 --- a/apps/web/src/app/page.tsx +++ b/apps/web/src/app/page.tsx @@ -1,85 +1,103 @@ import Link from 'next/link'; -import { Zap, Shield, Share2, Github } from 'lucide-react'; export default function Home() { - return ( -
- {/* Hero Section */} -
-

- og-engine -

-

- Open-source, platform-agnostic social image generation engine. - Zero lock-in, fully extensible, and secure by default. -

-
- - - - - GitHub - -
-
- - {/* Features */} -
-
- } - title="Fast" - description="Ultra-fast rendering using Satori and Resvg WASM. Perfect for Edge Runtimes." - /> - } - title="Secure" - description="Sandboxed template execution ensures no malicious code can access your infrastructure." - /> - } - title="Agnostic" - description="Deploy anywhere: Cloudflare Workers, Vercel, Node.js, or Docker." - /> -
-
- - {/* Footer */} -
-

© 2024 og-engine. Built with Satori & Resvg.

-
-
- ); -} + return ( +
+
+
+
+ Open source · MIT · Self-hostable +
+

+ Social images that look handcrafted. +

+

+ Build, preview, and deploy OG image templates with a fast typed engine and adapters for Node, Cloudflare, + and Vercel. +

+
+ + Open Editor + + + Browse Templates + +
+
-function FeatureCard({ icon, title, description }: { icon: React.ReactNode, title: string, description: string }) { - return ( -
-
{icon}
-

{title}

-

{description}

+
+
+ Live OG preview +
+
+ Sunset template preview +
+ GET /api/og?template=sunset&title=Launch%20Post +
+
- ); +
+
+ ); } diff --git a/apps/web/src/app/preview/page.tsx b/apps/web/src/app/preview/page.tsx index a220bc4..1a77cab 100644 --- a/apps/web/src/app/preview/page.tsx +++ b/apps/web/src/app/preview/page.tsx @@ -1,7 +1,7 @@ 'use client'; import { useState, useEffect, useMemo } from 'react'; -import { Settings, Image as ImageIcon, Copy, ExternalLink, RefreshCw } from 'lucide-react'; +import { Settings, Copy, ExternalLink, RefreshCw } from 'lucide-react'; const TEMPLATES = [ { id: 'sunset', name: 'Sunset', params: ['title', 'subtitle', 'author', 'date'] }, diff --git a/apps/worker-cf/src/index.ts b/apps/worker-cf/src/index.ts index b0923be..a589541 100644 --- a/apps/worker-cf/src/index.ts +++ b/apps/worker-cf/src/index.ts @@ -1,76 +1,100 @@ -import { Hono } from 'hono'; import { cloudflareAdapter } from '@og-engine/adapter-cloudflare'; import { createHandler } from '@og-engine/core'; -import type { OGRequest, MetaRequest } from '@og-engine/types'; +import type { MetaRequest, OGRequest, PlatformSize } from '@og-engine/types'; +import { Hono } from 'hono'; type Bindings = { - KV: KVNamespace; - BUCKET: R2Bucket; - PUBLIC_BASE_URL: string; + KV: KVNamespace; + BUCKET: R2Bucket; + PUBLIC_BASE_URL: string; }; const app = new Hono<{ Bindings: Bindings }>(); +const PLATFORM_SIZES: ReadonlyArray = [ + 'twitter-og', + 'facebook-og', + 'linkedin-og', + 'ig-post', + 'ig-story', + 'discord', + 'whatsapp', + 'github', + 'og' +]; + +function parseSize(value: string | undefined): PlatformSize { + if (value && PLATFORM_SIZES.includes(value as PlatformSize)) { + return value as PlatformSize; + } + + return 'og'; +} + +function parseFormat(value: string | undefined): OGRequest['format'] { + if (value === 'jpeg' || value === 'png') { + return value; + } + + return 'png'; +} app.get('/health', (context) => context.json({ ok: true })); app.get('/api/og', async (context) => { - const adapter = cloudflareAdapter({ - KV: context.env.KV, - BUCKET: context.env.BUCKET, - PUBLIC_BASE_URL: context.env.PUBLIC_BASE_URL - }); - - const handler = createHandler({ platform: adapter }); - - const { template, size = 'og', format = 'png', ...params } = context.req.query(); - - if (!template) { - return context.text('Missing template parameter', 400); + const adapter = cloudflareAdapter({ + KV: context.env.KV, + BUCKET: context.env.BUCKET, + PUBLIC_BASE_URL: context.env.PUBLIC_BASE_URL + }); + + const handler = createHandler({ platform: adapter }); + const { template, size, format, ...params } = context.req.query(); + + if (!template) { + return context.text('Missing template parameter', 400); + } + + const ogReq: OGRequest = { + template, + size: parseSize(size), + format: parseFormat(format), + params: params as Record + }; + + const { buffer, contentType, headers } = await handler.handleImageRequest(ogReq); + + return new Response(buffer, { + status: 200, + headers: { + 'Content-Type': contentType, + ...headers } - - const ogReq: OGRequest = { - template, - size: size as any, - format: format as any, - params: params as Record - }; - - const { buffer, contentType, headers } = await handler.handleImageRequest(ogReq); - - return context.body(buffer as any, { - status: 200, - headers: { - 'Content-Type': contentType, - ...headers - } - }); + }); }); app.get('/api/meta', async (context) => { - const adapter = cloudflareAdapter({ - KV: context.env.KV, - BUCKET: context.env.BUCKET, - PUBLIC_BASE_URL: context.env.PUBLIC_BASE_URL - }); - - const handler = createHandler({ platform: adapter }); - - const { template, size = 'og', ...params } = context.req.query(); - - if (!template) { - return context.text('Missing template parameter', 400); - } - - const metaReq: MetaRequest = { - template, - size: size as any, - params: params as Record, - baseUrl: new URL(context.req.url).origin - }; - - const meta = await handler.handleMetaRequest(metaReq); - - return context.json(meta); + const adapter = cloudflareAdapter({ + KV: context.env.KV, + BUCKET: context.env.BUCKET, + PUBLIC_BASE_URL: context.env.PUBLIC_BASE_URL + }); + + const handler = createHandler({ platform: adapter }); + const { template, size, ...params } = context.req.query(); + + if (!template) { + return context.text('Missing template parameter', 400); + } + + const metaReq: MetaRequest = { + template, + size: parseSize(size), + params: params as Record, + baseUrl: new URL(context.req.url).origin + }; + + const meta = await handler.handleMetaRequest(metaReq); + return context.json(meta); }); export default app; diff --git a/packages/sandbox/package.json b/internal/sandbox/package.json similarity index 84% rename from packages/sandbox/package.json rename to internal/sandbox/package.json index 559b7fa..29a03e2 100644 --- a/packages/sandbox/package.json +++ b/internal/sandbox/package.json @@ -1,5 +1,5 @@ { - "name": "@og-engine/sandbox", + "name": "@og-engine/internal-sandbox", "version": "0.1.0", "type": "module", "main": "dist/index.js", @@ -12,5 +12,6 @@ }, "dependencies": { "@og-engine/types": "workspace:*" - } + }, + "private": true } diff --git a/packages/sandbox/src/index.ts b/internal/sandbox/src/index.ts similarity index 100% rename from packages/sandbox/src/index.ts rename to internal/sandbox/src/index.ts diff --git a/packages/sandbox/tsconfig.json b/internal/sandbox/tsconfig.json similarity index 100% rename from packages/sandbox/tsconfig.json rename to internal/sandbox/tsconfig.json diff --git a/packages/core/README.md b/packages/core/README.md new file mode 100644 index 0000000..65ad1b7 --- /dev/null +++ b/packages/core/README.md @@ -0,0 +1,47 @@ +# @og-engine/core + +Core rendering and request handling primitives for `og-engine`. This package converts typed template inputs into rendered OG images, generates metadata payloads, and coordinates storage/cache behavior via adapter interfaces. + +## Installation + +```bash +pnpm add @og-engine/core +``` + +## Basic usage + +```ts +import { createHandler } from '@og-engine/core'; +import type { PlatformAdapter } from '@og-engine/types'; + +const platform: PlatformAdapter = { + storage: /* your storage adapter */, + cache: /* your cache adapter */, + registry: /* your template registry */ +}; + +const handler = createHandler({ platform }); +const image = await handler.handleImageRequest({ + template: 'sunset', + size: 'og', + params: { title: 'Hello' } +}); +``` + +## API reference + +- `createHandler(deps)`: creates image/meta handlers. +- `render(req, template, fonts)`: runs the render pipeline. +- `coerceParams(raw, schema)`: validates/coerces raw params. +- `buildCacheKey(req, templateVersion)`: deterministic cache key generator. +- `getDefaultFonts()`, `loadFont(name, url)`: font loading helpers. +- `initWasm()`, `getResvg()`: runtime WASM/bootstrap helpers. + +## Used by + +- `@og-engine/adapter-node` +- `@og-engine/adapter-cloudflare` +- `@og-engine/adapter-vercel` +- `apps/web` +- `apps/worker-cf` +- `apps/server-node` diff --git a/packages/core/package.json b/packages/core/package.json index 8c2c37e..07eae51 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -11,7 +11,7 @@ "test": "node -e \"console.log('no tests')\"" }, "dependencies": { - "@og-engine/sandbox": "workspace:*", + "@og-engine/internal-sandbox": "workspace:*", "@og-engine/types": "workspace:*", "@resvg/resvg-js": "^2.6.2", "@resvg/resvg-wasm": "^2.6.2", diff --git a/packages/core/src/cache-key.ts b/packages/core/src/cache-key.ts index 52a8cb6..7924237 100644 --- a/packages/core/src/cache-key.ts +++ b/packages/core/src/cache-key.ts @@ -1,5 +1,18 @@ +/** + * @file cache-key.ts + * @description Deterministic cache key generation for OG image requests. + * @module @og-engine/core + */ + import type { OGRequest } from '@og-engine/types'; +/** + * Builds a deterministic SHA-256 cache key from normalized request fields. + * + * @param req - OG image request payload. + * @param templateVersion - Template version used for automatic cache busting. + * @returns Lowercase 64-character SHA-256 hex digest. + */ export async function buildCacheKey(req: OGRequest, templateVersion: string): Promise { const sorted = Object.entries({ ...req.params, diff --git a/packages/core/src/fonts.ts b/packages/core/src/fonts.ts index a54f8e4..af8b06c 100644 --- a/packages/core/src/fonts.ts +++ b/packages/core/src/fonts.ts @@ -1,36 +1,221 @@ +/** + * @file fonts.ts + * @description Runtime font loading utilities for Satori rendering. + * @module @og-engine/core + */ + +/** + * Font definition passed into Satori. + */ export interface FontConfig { + /** Font family name. */ name: string; + + /** Raw font binary data. */ data: ArrayBuffer; + + /** Optional font weight. */ weight?: 100 | 200 | 300 | 400 | 500 | 600 | 700 | 800 | 900; + + /** Optional font style. */ style?: 'normal' | 'italic'; } -const fontCache = new Map(); +const fontBinaryCache = new Map(); +const cssCache = new Map(); -export async function loadFont(name: string, url: string): Promise { - if (!fontCache.has(url)) { - const response = await fetch(url); - if (!response.ok) { - throw new Error(`Unable to load font from ${url}`); + +const fallbackFontUrls: Record = { + 'Playfair Display|400|normal': + 'https://raw.githubusercontent.com/google/fonts/main/ofl/playfairdisplay/PlayfairDisplay-Regular.ttf', + 'Playfair Display|700|normal': + 'https://raw.githubusercontent.com/google/fonts/main/ofl/playfairdisplay/PlayfairDisplay-Bold.ttf', + 'Playfair Display|900|normal': + 'https://raw.githubusercontent.com/google/fonts/main/ofl/playfairdisplay/PlayfairDisplay-Black.ttf', + 'Instrument Serif|400|italic': + 'https://raw.githubusercontent.com/google/fonts/main/ofl/instrumentserif/InstrumentSerif-Italic.ttf', + 'JetBrains Mono|400|normal': + 'https://raw.githubusercontent.com/JetBrains/JetBrainsMono/master/fonts/ttf/JetBrainsMono-Regular.ttf', + 'JetBrains Mono|700|normal': + 'https://raw.githubusercontent.com/JetBrains/JetBrainsMono/master/fonts/ttf/JetBrainsMono-Bold.ttf', + 'DM Sans|400|normal': + 'https://raw.githubusercontent.com/google/fonts/main/ofl/dmsans/DMSans-Regular.ttf', + 'DM Sans|500|normal': + 'https://raw.githubusercontent.com/google/fonts/main/ofl/dmsans/DMSans-Medium.ttf', + 'Noto Emoji|400|normal': + 'https://raw.githubusercontent.com/googlefonts/noto-emoji/main/fonts/NotoEmoji-VariableFont_wght.ttf' +}; + +function getFallbackKey(variant: GoogleFontVariant): string { + return `${variant.family}|${variant.weight}|${variant.style ?? 'normal'}`; +} + + + +const localFontFallbacks: Record = { + 'Playfair Display': '/usr/share/fonts/truetype/dejavu/DejaVuSerif.ttf', + 'Instrument Serif': '/usr/share/fonts/truetype/dejavu/DejaVuSerif.ttf', + 'JetBrains Mono': '/usr/share/fonts/truetype/dejavu/DejaVuSansMono.ttf', + 'DM Sans': '/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf', + 'Noto Emoji': '/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf' +}; + +async function loadLocalFallbackFont(family: string): Promise { + const path = localFontFallbacks[family]; + if (!path) { + return null; + } + + const isNodeRuntime = + typeof process !== 'undefined' && + typeof process.versions !== 'undefined' && + typeof process.versions.node === 'string'; + + if (!isNodeRuntime) { + return null; + } + + const { readFile } = await import('node:fs/promises'); + const file = await readFile(path); + const bytes = file.buffer.slice(file.byteOffset, file.byteOffset + file.byteLength); + return bytes; +} + +interface GoogleFontVariant { + family: string; + weight: 100 | 200 | 300 | 400 | 500 | 600 | 700 | 800 | 900; + style?: 'normal' | 'italic'; +} + +function buildGoogleCssUrl(variant: GoogleFontVariant): string { + const family = variant.family.replace(/\s+/g, '+'); + const style = variant.style ?? 'normal'; + const ital = style === 'italic' ? 1 : 0; + return `https://fonts.googleapis.com/css2?family=${family}:ital,wght@${ital},${variant.weight}&display=swap`; +} + +async function loadCss(url: string): Promise { + const cached = cssCache.get(url); + if (cached) { + return cached; + } + + const response = await fetch(url, { + headers: { + 'User-Agent': + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0 Safari/537.36' } - fontCache.set(url, await response.arrayBuffer()); + }); + + if (!response.ok) { + throw new Error(`Unable to load font CSS from ${url}`); + } + + const css = await response.text(); + cssCache.set(url, css); + return css; +} + +function extractFontUrl(css: string): string | null { + const match = css.match(/src:\s*url\((https:\/\/fonts\.gstatic\.com\/[^)]+)\)/); + return match ? match[1] : null; +} + +async function loadFontBinary(url: string): Promise { + const cached = fontBinaryCache.get(url); + if (cached) { + return cached; } - const data = fontCache.get(url); - if (!data) { - throw new Error(`Font cache missing loaded value for ${url}`); + const response = await fetch(url); + if (!response.ok) { + throw new Error(`Unable to load font binary from ${url}`); } - return { name, data, weight: 400 }; + const data = await response.arrayBuffer(); + fontBinaryCache.set(url, data); + return data; } -export async function getDefaultFonts(): Promise { - const baseUrl = 'https://github.com/googlefonts/noto-fonts/raw/master/unhinted/ttf/NotoSans'; - const emojiUrl = 'https://github.com/googlefonts/noto-emoji/raw/main/fonts/NotoColorEmoji.ttf'; +async function loadGoogleFont(variant: GoogleFontVariant): Promise { + const cssUrl = buildGoogleCssUrl(variant); + + try { + const css = await loadCss(cssUrl); + const fontUrl = extractFontUrl(css); + + if (!fontUrl) { + throw new Error(`Google Fonts CSS does not contain a usable font URL: ${cssUrl}`); + } + const data = await loadFontBinary(fontUrl); + return { + name: variant.family, + data, + weight: variant.weight, + style: variant.style ?? 'normal' + }; + } catch { + const fallbackUrl = fallbackFontUrls[getFallbackKey(variant)]; + + if (fallbackUrl) { + try { + const data = await loadFontBinary(fallbackUrl); + return { + name: variant.family, + data, + weight: variant.weight, + style: variant.style ?? 'normal' + }; + } catch { + // continue to local fallback + } + } + + const localData = await loadLocalFallbackFont(variant.family); + if (!localData) { + throw new Error(`No fallback font source configured for ${getFallbackKey(variant)}`); + } + + return { + name: variant.family, + data: localData, + weight: variant.weight, + style: variant.style ?? 'normal' + }; + } +} + +/** + * Loads the default font stack used by bundled templates. + * + * The first cold load usually adds around 80-100ms due to Google Fonts CSS + * discovery and font binary download; subsequent loads are served from memory. + * + * @returns Default fonts for editorial, minimal, terminal, labels, and emoji. + */ +export async function getDefaultFonts(): Promise { return Promise.all([ - loadFont('Noto Sans', `${baseUrl}/NotoSans-Regular.ttf`), - loadFont('Noto Sans', `${baseUrl}/NotoSans-Bold.ttf`).then(f => ({ ...f, weight: 700 as const })), - loadFont('Noto Sans', `${baseUrl}/NotoSans-Black.ttf`).then(f => ({ ...f, weight: 900 as const })) + loadGoogleFont({ family: 'Playfair Display', weight: 400 }), + loadGoogleFont({ family: 'Playfair Display', weight: 700 }), + loadGoogleFont({ family: 'Playfair Display', weight: 900 }), + loadGoogleFont({ family: 'Instrument Serif', weight: 400, style: 'italic' }), + loadGoogleFont({ family: 'JetBrains Mono', weight: 400 }), + loadGoogleFont({ family: 'JetBrains Mono', weight: 700 }), + loadGoogleFont({ family: 'DM Sans', weight: 400 }), + loadGoogleFont({ family: 'DM Sans', weight: 500 }), + loadGoogleFont({ family: 'Noto Emoji', weight: 400 }) ]); } + +/** + * Backward-compatible font loader for direct URL usage. + * + * @param name - Font family name. + * @param url - Absolute font URL. + * @returns A Satori-compatible font config object. + */ +export async function loadFont(name: string, url: string): Promise { + const data = await loadFontBinary(url); + return { name, data, weight: 400, style: 'normal' }; +} diff --git a/packages/core/src/handler.ts b/packages/core/src/handler.ts index 46c62cc..e86f254 100644 --- a/packages/core/src/handler.ts +++ b/packages/core/src/handler.ts @@ -1,3 +1,9 @@ +/** + * @file handler.ts + * @description Request handlers for OG image rendering and metadata generation. + * @module @og-engine/core + */ + import { PLATFORM_SIZES, type MetaRequest, @@ -5,15 +11,34 @@ import { type OGRequest, type PlatformAdapter } from '@og-engine/types'; -import { getDefaultFonts, type FontConfig } from './fonts.js'; import { buildCacheKey } from './cache-key.js'; +import { getDefaultFonts, type FontConfig } from './fonts.js'; import { render } from './render.js'; +/** + * Dependency bundle for handler creation. + */ export interface HandlerDeps { + /** Platform adapter that provides registry, storage, and cache backends. */ platform: PlatformAdapter; + + /** Optional preloaded fonts. Defaults are loaded when omitted. */ fonts?: FontConfig[]; } +interface ImageHandlerResponse { + buffer: Buffer; + contentType: 'image/png' | 'image/jpeg'; + headers: Record; + fromCache: boolean; +} + +interface OGHandler { + handleImageRequest(req: OGRequest): Promise; + handleMetaRequest(req: MetaRequest): Promise; + preGenerate(req: OGRequest): Promise; +} + function buildOGImageUrl(req: MetaRequest): string { const params = new URLSearchParams({ template: req.template, @@ -31,9 +56,15 @@ function getCacheHeaders(): Record { }; } -export function createHandler(deps: HandlerDeps) { - return { - async handleImageRequest(req: OGRequest) { +/** + * Creates image and metadata handlers for a given platform adapter. + * + * @param deps - Platform dependencies. + * @returns Handler object with image, metadata, and pre-generation methods. + */ +export function createHandler(deps: HandlerDeps): OGHandler { + const handler: OGHandler = { + async handleImageRequest(req: OGRequest): Promise { const template = await deps.platform.registry.get(req.template); const cacheKey = await buildCacheKey(req, template.version); const cachedUrl = await deps.platform.cache.get(cacheKey); @@ -71,17 +102,18 @@ export function createHandler(deps: HandlerDeps) { 'twitter:image': imageUrl }; - void this.preGenerate({ ...req, format: 'png' }); - + void handler.preGenerate({ ...req, format: 'png' }); return meta; }, async preGenerate(req: OGRequest): Promise { try { - await this.handleImageRequest(req); + await handler.handleImageRequest(req); } catch { - // no-op + // best-effort background pre-generation } } }; + + return handler; } diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index fc71a0c..7109b1f 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -1,6 +1,15 @@ -export * from './cache-key.js'; -export * from './fonts.js'; -export * from './handler.js'; -export * from './params.js'; -export * from './render.js'; -export * from './wasm.js'; +/** + * @file index.ts + * @description Public API surface for @og-engine/core. + * @module @og-engine/core + */ + +export { buildCacheKey } from './cache-key.js'; +export { getDefaultFonts, loadFont } from './fonts.js'; +export type { FontConfig } from './fonts.js'; +export { createHandler } from './handler.js'; +export type { HandlerDeps } from './handler.js'; +export { coerceParams } from './params.js'; +export { render } from './render.js'; +export type { RenderResult } from './render.js'; +export { getResvg, initWasm } from './wasm.js'; diff --git a/packages/core/src/params.ts b/packages/core/src/params.ts index 78f3b28..9a868ad 100644 --- a/packages/core/src/params.ts +++ b/packages/core/src/params.ts @@ -1,5 +1,18 @@ +/** + * @file params.ts + * @description Request parameter coercion and schema validation utilities. + * @module @og-engine/core + */ + import type { TemplateSchema } from '@og-engine/types'; +/** + * Coerces raw query parameters into typed values based on a template schema. + * + * @param raw - Raw request params map. + * @param schema - Template schema used for coercion and validation. + * @returns Coerced parameter object suitable for template rendering. + */ export function coerceParams( raw: Record, schema: TemplateSchema diff --git a/packages/core/src/render.ts b/packages/core/src/render.ts index 5821ef7..dd9f1f8 100644 --- a/packages/core/src/render.ts +++ b/packages/core/src/render.ts @@ -1,4 +1,10 @@ -import { sandboxedRender } from '@og-engine/sandbox'; +/** + * @file render.ts + * @description Core OG image render pipeline (template -> SVG -> PNG). + * @module @og-engine/core + */ + +import { sandboxedRender } from '@og-engine/internal-sandbox'; import { PLATFORM_SIZES, type OGRequest, type OGTemplate } from '@og-engine/types'; import satori from 'satori'; import { buildCacheKey } from './cache-key.js'; @@ -6,14 +12,34 @@ import type { FontConfig } from './fonts.js'; import { coerceParams } from './params.js'; import { getResvg, initWasm } from './wasm.js'; +/** + * Successful render output payload. + */ export interface RenderResult { + /** Encoded image buffer. */ buffer: Buffer; + + /** Output MIME type. */ contentType: 'image/png' | 'image/jpeg'; + + /** Output width in pixels. */ width: number; + + /** Output height in pixels. */ height: number; + + /** Deterministic request cache key. */ cacheKey: string; } +/** + * Renders an OG image from request data and a template. + * + * @param req - Request payload. + * @param template - Template definition. + * @param fonts - Satori font definitions. + * @returns Rendered image data and metadata. + */ export async function render( req: OGRequest, template: OGTemplate, @@ -22,11 +48,8 @@ export async function render( await initWasm(); const { width, height } = PLATFORM_SIZES[req.size]; - - // 1. Coerce raw string params into typed params const typedParams = coerceParams(req.params, template.schema); - // 2. Render JSX via template.render() inside sandbox const element = await sandboxedRender(template, { ...typedParams, width, @@ -34,14 +57,13 @@ export async function render( size: req.size }); - // 3. Satori: JSX -> SVG - const svg = await satori(element as any, { + const satoriInput = element as unknown as Parameters[0]; + const svg = await satori(satoriInput, { width, height, fonts }); - // 4. resvg: SVG -> PNG const ResvgClass = await getResvg(); const resvg = new ResvgClass(svg, { fitTo: { mode: 'width', value: width } diff --git a/packages/core/src/wasm-edge.ts b/packages/core/src/wasm-edge.ts index 238a08e..26e9b2f 100644 --- a/packages/core/src/wasm-edge.ts +++ b/packages/core/src/wasm-edge.ts @@ -1,17 +1,29 @@ +/** + * @file wasm-edge.ts + * @description Edge-runtime WASM initialization for Yoga and Resvg. + * @module @og-engine/core + */ + let initialized = false; +function isInitFunction(value: unknown): value is () => Promise { + return typeof value === 'function'; +} + +/** + * Initializes WASM dependencies required for edge runtimes. + */ export async function initWasm(): Promise { if (initialized) { return; } - // yoga (flexbox engine used by satori) - const { default: initYoga } = await import('yoga-wasm-web/auto'); - await (initYoga as any)(); + const { default: initYogaMaybe } = await import('yoga-wasm-web/auto'); + if (isInitFunction(initYogaMaybe)) { + await initYogaMaybe(); + } - // resvg const { initWasm: initResvg } = await import('@resvg/resvg-wasm'); - // fetch from CDN await initResvg(fetch('https://unpkg.com/@resvg/resvg-wasm/index_bg.wasm')); initialized = true; diff --git a/packages/core/src/wasm-node.ts b/packages/core/src/wasm-node.ts index d0eb2a9..01a5f22 100644 --- a/packages/core/src/wasm-node.ts +++ b/packages/core/src/wasm-node.ts @@ -1,15 +1,27 @@ +/** + * @file wasm-node.ts + * @description Node.js-specific WASM initialization for Yoga. + * @module @og-engine/core + */ + let initialized = false; +function isInitFunction(value: unknown): value is () => Promise { + return typeof value === 'function'; +} + +/** + * Initializes WASM dependencies required for Node.js rendering. + */ export async function initWasm(): Promise { if (initialized) { return; } - // yoga (flexbox engine used by satori) const yoga = await import('yoga-wasm-web/auto'); - const init = yoga.default || yoga; - if (typeof init === 'function') { - await (init as any)(); + const initMaybe: unknown = yoga.default ?? yoga; + if (isInitFunction(initMaybe)) { + await initMaybe(); } initialized = true; diff --git a/packages/core/src/wasm.ts b/packages/core/src/wasm.ts index 22c884a..4d2985d 100644 --- a/packages/core/src/wasm.ts +++ b/packages/core/src/wasm.ts @@ -1,8 +1,17 @@ +/** + * @file wasm.ts + * @description Runtime-aware WASM bootstrap helpers for render dependencies. + * @module @og-engine/core + */ + const isNodeRuntime = typeof process !== 'undefined' && typeof process.versions !== 'undefined' && typeof process.versions.node === 'string'; +/** + * Initializes runtime-specific WASM dependencies. + */ export async function initWasm(): Promise { if (isNodeRuntime) { const mod = await import('./wasm-node.js'); @@ -14,11 +23,29 @@ export async function initWasm(): Promise { await mod.initWasm(); } -export async function getResvg() { +interface ResvgLike { + render(): { asPng(): Uint8Array }; +} + +/** + * Runtime-agnostic Resvg constructor type used by the render pipeline. + */ +type ResvgConstructor = new ( + svg: string | Uint8Array, + options?: { fitTo?: { mode: 'width'; value: number } } +) => ResvgLike; + +/** + * Resolves runtime-appropriate Resvg constructor. + * + * @returns Resvg class implementation for current runtime. + */ +export async function getResvg(): Promise { if (isNodeRuntime) { const { Resvg } = await import('@resvg/resvg-js'); - return Resvg; + return Resvg as unknown as ResvgConstructor; } + const { Resvg } = await import('@resvg/resvg-wasm'); - return Resvg; + return Resvg as unknown as ResvgConstructor; } diff --git a/packages/types/README.md b/packages/types/README.md new file mode 100644 index 0000000..661ad57 --- /dev/null +++ b/packages/types/README.md @@ -0,0 +1,48 @@ +# @og-engine/types + +Shared TypeScript contracts for `og-engine` packages. This package contains strongly-typed interfaces and utility types for template definitions, render requests, platform adapters, and metadata responses used by core and adapter packages. + +## Installation + +```bash +pnpm add @og-engine/types +``` + +## Basic usage + +```ts +import type { OGTemplate, PlatformSize, TemplateSchema } from '@og-engine/types'; + +const schema = { + title: { type: 'string', required: true, maxLength: 120 } +} satisfies TemplateSchema; + +const template: OGTemplate = { + id: 'example', + name: 'Example', + description: 'Example template', + author: 'og-engine', + version: '1.0.0', + supportedSizes: ['og' satisfies PlatformSize], + schema, + render: ({ title }) =>
{title}
+}; +``` + +## API reference + +- `PLATFORM_SIZES`: canonical size map for supported social platforms. +- `PlatformSize`: union of supported size keys. +- `TemplateSchema`, `SchemaFieldType`, `InferParams`: schema/typing helpers for templates. +- `OGTemplate`, `TemplateMetadata`, `RenderContext`: template contracts. +- `StorageAdapter`, `CacheAdapter`, `TemplateRegistryAdapter`, `PlatformAdapter`: integration interfaces. +- `OGRequest`, `MetaRequest`, `MetaResponse`: request/response payload types. + +## Used by + +- `@og-engine/core` +- `@og-engine/adapter-node` +- `@og-engine/adapter-cloudflare` +- `@og-engine/adapter-vercel` +- template packages under `templates/*` +- apps under `apps/*` diff --git a/packages/types/src/index.ts b/packages/types/src/index.ts index eedb5d2..082b857 100644 --- a/packages/types/src/index.ts +++ b/packages/types/src/index.ts @@ -1,3 +1,16 @@ +/** + * @file index.ts + * @description Public type contracts shared across og-engine packages. + * + * This module defines template schemas, rendering interfaces, platform adapter + * contracts, and request/response payload types used throughout the monorepo. + * + * @module @og-engine/types + */ + +/** + * Canonical output size presets for supported social platforms. + */ export const PLATFORM_SIZES = { 'twitter-og': { width: 1200, height: 628, label: 'Twitter / X' }, 'facebook-og': { width: 1200, height: 630, label: 'Facebook OG' }, @@ -10,8 +23,14 @@ export const PLATFORM_SIZES = { og: { width: 1200, height: 630, label: 'Generic OG (default)' } } as const; +/** + * Supported social output size identifier. + */ export type PlatformSize = keyof typeof PLATFORM_SIZES; +/** + * Allowed schema field definitions for template parameters. + */ export type SchemaFieldType = | { type: 'string'; required?: boolean; default?: string; maxLength?: number } | { type: 'text'; required?: boolean; default?: string } @@ -20,8 +39,14 @@ export type SchemaFieldType = | { type: 'boolean'; required?: boolean; default?: boolean } | { type: 'enum'; required?: boolean; values: string[]; default?: string }; +/** + * Template parameter schema keyed by parameter name. + */ export type TemplateSchema = Record; +/** + * Derives strongly-typed render params from a template schema. + */ export type InferParams = { [K in keyof S]: S[K] extends { type: 'string' } ? string @@ -38,83 +63,202 @@ export type InferParams = { : never; }; +/** + * JSX element type used by template render functions. + */ export type JSXElement = JSX.Element; +/** + * Size and platform context passed into template render functions. + */ export interface RenderContext { + /** Output image width in pixels. */ width: number; + + /** Output image height in pixels. */ height: number; + + /** Selected platform size key. */ size: PlatformSize; } +/** + * Fully-typed OG template definition. + */ export interface OGTemplate { + /** Stable template identifier used in requests. */ id: string; + + /** Human-readable template name. */ name: string; + + /** Short description shown in template listings. */ description: string; + + /** Template author or maintainer name. */ author: string; + + /** Template version string (semver recommended). */ version: string; + + /** Optional tag list used for filtering and discovery. */ tags?: string[]; + + /** Platforms/sizes this template supports. */ supportedSizes: PlatformSize[]; + + /** Parameter schema used for coercion and validation. */ schema: S; + + /** Render function that returns a JSX tree for Satori. */ render: (params: InferParams & RenderContext) => JSXElement; + + /** Optional preview image URL/path. */ preview?: string; } +/** + * Metadata shape used for listing templates without full render functions. + */ export interface TemplateMetadata { + /** Stable template identifier used in requests. */ id: string; + + /** Human-readable template name. */ name: string; + + /** Short description shown in template listings. */ description: string; + + /** Template author or maintainer name. */ author: string; + + /** Template version string (semver recommended). */ version: string; + + /** Optional tags used for filtering and discovery. */ tags: string[]; + + /** Platforms/sizes this template supports. */ supportedSizes: PlatformSize[]; } +/** + * Binary storage adapter contract for rendered image assets. + */ export interface StorageAdapter { + /** Reads a binary object by storage key. */ get(key: string): Promise; + + /** Writes a binary object by storage key. */ put(key: string, value: Buffer, options?: { contentType?: string }): Promise; + + /** Removes a binary object by storage key. */ delete(key: string): Promise; + + /** Builds a public URL for a storage key. */ url(key: string): string; } +/** + * Cache adapter contract for key-value cache backends. + */ export interface CacheAdapter { + /** Returns a cached value for a key or null when absent. */ get(key: string): Promise; + + /** Stores a cache value with optional TTL in seconds. */ set(key: string, value: string, ttl?: number): Promise; + + /** Invalidates a cache value by key. */ delete(key: string): Promise; } +/** + * Registry adapter contract used to discover and load templates. + */ export interface TemplateRegistryAdapter { + /** Lists all available template metadata entries. */ list(): Promise; + + /** Loads a concrete template by identifier. */ get(id: string): Promise; + + /** Checks whether a template exists by identifier. */ exists(id: string): Promise; } +/** + * Platform-specific adapter grouping required dependencies. + */ export interface PlatformAdapter { + /** Binary storage backend implementation. */ storage: StorageAdapter; + + /** Cache backend implementation. */ cache: CacheAdapter; + + /** Template registry implementation. */ registry: TemplateRegistryAdapter; } +/** + * Request payload for generating an OG image. + */ export interface OGRequest { + /** Template identifier to render. */ template: string; + + /** Output size preset key. */ size: PlatformSize; + + /** Raw string parameters from a request boundary. */ params: Record; + + /** Optional output format (defaults to PNG in core). */ format?: 'png' | 'jpeg'; + + /** Optional quality hint for lossy output formats. */ quality?: number; + + /** Optional cache TTL in seconds. */ ttl?: number; } +/** + * Request payload for generating metadata output instead of binary images. + */ export type MetaRequest = Omit & { + /** Metadata response format. */ format?: 'json' | 'html'; + + /** Absolute base URL used to build OG image links. */ baseUrl: string; }; +/** + * Open Graph and Twitter card metadata response fields. + */ export interface MetaResponse { + /** Absolute URL for the OG image. */ 'og:image': string; + + /** Image width in pixels as a string. */ 'og:image:width': string; + + /** Image height in pixels as a string. */ 'og:image:height': string; + + /** MIME type for the OG image. */ 'og:image:type': string; + + /** Twitter card style selector. */ 'twitter:card': 'summary_large_image' | 'summary'; + + /** Absolute URL for Twitter image fallback. */ 'twitter:image': string; + + /** Additional metadata fields. */ [key: string]: string; } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index be521a6..f518cea 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -134,6 +134,12 @@ importers: specifier: ^4.20250214.0 version: 4.20260305.1 + internal/sandbox: + dependencies: + '@og-engine/types': + specifier: workspace:* + version: link:../../packages/types + packages/adapter-cloudflare: dependencies: '@cloudflare/workers-types': @@ -178,9 +184,9 @@ importers: packages/core: dependencies: - '@og-engine/sandbox': + '@og-engine/internal-sandbox': specifier: workspace:* - version: link:../sandbox + version: link:../../internal/sandbox '@og-engine/types': specifier: workspace:* version: link:../types @@ -197,12 +203,6 @@ importers: specifier: ^0.3.3 version: 0.3.3 - packages/sandbox: - dependencies: - '@og-engine/types': - specifier: workspace:* - version: link:../types - packages/types: {} templates/_base: {} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 618bc36..8700e9b 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -1,4 +1,5 @@ packages: - 'packages/*' - - 'apps/*' + - 'internal/*' - 'templates/*' + - 'apps/*' diff --git a/templates/_base/package.json b/templates/_base/package.json index bc3e5e2..10e365b 100644 --- a/templates/_base/package.json +++ b/templates/_base/package.json @@ -9,5 +9,6 @@ "typecheck": "tsc -p tsconfig.json --noEmit", "lint": "eslint . --ext .ts,.tsx", "test": "node -e \"console.log('no tests')\"" - } + }, + "private": true } diff --git a/templates/dark/index.tsx b/templates/dark/index.tsx index a9a2fee..cfafd7d 100644 --- a/templates/dark/index.tsx +++ b/templates/dark/index.tsx @@ -2,11 +2,11 @@ import type { OGTemplate } from '@og-engine/types'; const template: OGTemplate = { id: 'dark', - name: 'Dark', - description: 'Dark neon template', + name: 'Dark Terminal', + description: 'Terminal-inspired black canvas with mono typography and sharp accents', author: 'og-engine', - version: '1.0.0', - tags: ['dark', 'neon', 'saas'], + version: '2.0.0', + tags: ['terminal', 'hacker', 'mono', 'dark'], supportedSizes: [ 'twitter-og', 'facebook-og', @@ -19,108 +19,96 @@ const template: OGTemplate = { 'ig-story' ], schema: { - title: { type: 'string', required: true, maxLength: 120 }, - subtitle: { type: 'text' }, - accent: { type: 'color', default: '#00D2FF' }, - author: { type: 'string' } + title: { type: 'string', required: true, maxLength: 100 }, + subtitle: { type: 'string', required: false, maxLength: 200 }, + author: { type: 'string', required: false }, + context: { type: 'string', required: false }, + tag: { type: 'string', required: false }, + accent: { type: 'color', required: false, default: '#00ff88' } }, - render: (params) => ( -
- {/* Ambient Background Glow */} -
+ render: (params) => { + const accent = params.accent || '#00ff88'; + const isCompact = params.size === 'whatsapp'; + return (
-

- {params.title} -

- {params.subtitle && ( -

- {params.subtitle} -

- )} -
+ /> + +
+
{`$/${params.context || 'workspace'}`}
+ +
+
{'>'}
+
+ {params.title} +
+
+ + {params.subtitle ? ( +
+ {params.subtitle} +
+ ) : null} +
-
- {params.author && ( - - {params.author} - - )} + > +
{params.author || 'operator'}
+
+ {params.tag || 'stable'} + v1.0.0 +
+
-
- ), + ); + }, preview: './preview.svg' }; diff --git a/templates/dark/metadata.json b/templates/dark/metadata.json index 721a8df..d869b9c 100644 --- a/templates/dark/metadata.json +++ b/templates/dark/metadata.json @@ -1,10 +1,10 @@ { "id": "dark", - "name": "Dark", - "description": "Dark neon template", + "name": "Dark Terminal", + "description": "Terminal-inspired black canvas with mono typography and sharp accents", "author": "og-engine", - "version": "1.0.0", - "tags": ["dark", "neon", "saas"], + "version": "2.0.0", + "tags": ["terminal", "hacker", "mono", "dark"], "supportedSizes": [ "twitter-og", "facebook-og", diff --git a/templates/dark/package.json b/templates/dark/package.json index 4cd0516..2a03441 100644 --- a/templates/dark/package.json +++ b/templates/dark/package.json @@ -12,5 +12,6 @@ }, "dependencies": { "@og-engine/types": "workspace:*" - } + }, + "private": true } diff --git a/templates/dark/preview.png b/templates/dark/preview.png deleted file mode 100644 index 5949d693e6d0d6ba7bf43e96bcff5407dc746645..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 40519 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 X`IY6FEc$WSN&EL4-ktOF>EHh!CjzRw diff --git a/templates/minimal/index.tsx b/templates/minimal/index.tsx index 9f80115..d0383eb 100644 --- a/templates/minimal/index.tsx +++ b/templates/minimal/index.tsx @@ -2,11 +2,11 @@ import type { OGTemplate } from '@og-engine/types'; const template: OGTemplate = { id: 'minimal', - name: 'Minimal', - description: 'Simple and clean template', + name: 'Minimal Swiss', + description: 'Refined typographic composition with generous whitespace', author: 'og-engine', - version: '1.0.0', - tags: ['minimal', 'docs'], + version: '2.0.0', + tags: ['minimal', 'swiss', 'typography'], supportedSizes: [ 'twitter-og', 'facebook-og', @@ -19,95 +19,79 @@ const template: OGTemplate = { 'ig-story' ], schema: { - title: { type: 'string', required: true, maxLength: 120 }, - description: { type: 'text' }, - logo: { type: 'image' }, - tag: { type: 'string' }, + title: { type: 'string', required: true, maxLength: 80 }, + category: { type: 'string', required: false }, + author: { type: 'string', required: false }, + site: { type: 'string', required: false }, + logo: { type: 'image', required: false }, + accent: { type: 'color', required: false, default: '#2563eb' }, theme: { type: 'enum', values: ['light', 'dark'], default: 'light' } }, render: (params) => { - const isDark = params.theme === 'dark'; - const bg = isDark ? '#000000' : '#ffffff'; - const fg = isDark ? '#ffffff' : '#000000'; - const muted = isDark ? 'rgba(255,255,255,0.6)' : 'rgba(0,0,0,0.6)'; + const dark = params.theme === 'dark'; + const bg = dark ? '#111111' : '#ffffff'; + const text = dark ? '#f4f4f4' : '#111111'; + const secondary = dark ? '#999999' : '#666666'; + const isCompact = params.size === 'whatsapp'; return (
-
- {params.tag && ( -
+ {params.logo ? ( + logo - {params.tag} -
- )} -

+ ) : null} +

+ +
+
+ {params.category || 'Category'} +
+ +
{params.title} - - {params.description && ( -

- {params.description} -

- )} +
-
- {params.logo && ( - Logo - )} +
+
{params.author || 'Unknown author'}
+
{params.site || 'og-engine'}
); diff --git a/templates/minimal/metadata.json b/templates/minimal/metadata.json index 877a6df..0c44e96 100644 --- a/templates/minimal/metadata.json +++ b/templates/minimal/metadata.json @@ -1,10 +1,10 @@ { "id": "minimal", - "name": "Minimal", - "description": "Simple clean template", + "name": "Minimal Swiss", + "description": "Refined typographic composition with generous whitespace", "author": "og-engine", - "version": "1.0.0", - "tags": ["minimal", "docs"], + "version": "2.0.0", + "tags": ["minimal", "swiss", "typography"], "supportedSizes": [ "twitter-og", "facebook-og", diff --git a/templates/minimal/package.json b/templates/minimal/package.json index fc80e6f..e6f449c 100644 --- a/templates/minimal/package.json +++ b/templates/minimal/package.json @@ -12,5 +12,6 @@ }, "dependencies": { "@og-engine/types": "workspace:*" - } + }, + "private": true } diff --git a/templates/minimal/preview.png b/templates/minimal/preview.png deleted file mode 100644 index 3714b5eee9c59cd42647e2cefc31ee2a105032e3..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 19222 zcmeHvXH=7Ew{}2884(;D6_6sMs0>OSdQ))}LCTPk9)i+FI)uV1mdd#fp~+C^8;7RZ-hI5K*m$|Zr}VRB$qa2l5%YvBF!0O#YpmI z6|E0LoX$oHt|lfLx`w-+zxC^Et2$0v)Y9 zW5IW!?#PA>{*E4AMD+L#-{k`X%IN&(()5uG+pG*k^U~I{QgW!T^@7$Ov6re%30Gk~ zIDl630-{9U5dh=@fqu;fo#6gi{Li2NSmS@`3@cKIWfQ0!V&gfq72%>WFArGsQ#`44 zQLW6njryPwx)s7GgF-kNtP6NMn;!PwKvX2-H^qoqbv)N4rQ-Q)DiJhK>&oz{6`$Da z$cuP8qn_LuDy)Fzuy-DKVm+me!>Byxa@{DpKDxJo+3T(kMgrHP3E$6-oS$BgC)?UO zpg=3XTTTo_?$kE&oaMfx>3Z49%|-UpU)p>&)-P)!nL`CZ1zg!ej&`%Fr?-nW1gbH& zqv1#8(Cgf|b+nDMAzj30+xEz{0&+!LUzQn#AFpyl=G3<=odF(b4OqyxX=>8h`qruM zYWoA;YrwUc4B89dh&L27&5Jyv_bs~&_RYwp0IyKuLMeK(@jVtZSAjsVwQA;oM$m0o~6`3soC zC00=8AgV_MxQ@4OWsX#0+s>UGI$m|z*e?U*`Oe@=tbq-?EQ(QSWw7IH5Y!?8Jzzj_ z%q04kq>CnOJ`wy83ml7s2@zA zJ1EW|)Q;VuxPQx*X?L&{#Tgh0r?94Lw$DUu-;Mf(v;X@XI(B^c(rK{YIZb;9XBDT} z28Nnmp}9vL0IEh2DS*f!<&LZg5@z~Kmj+T+K^C9=d8mN1O??jCe`MS2AQ>nDMUEad zV&LE^oh2<1>*3g?a39j2zv&WImWN}&yg7WD0uPd6Gvh1{W~BFH$@t3yE4=8rDzqb1 zvTg+Ne9ra`n76z+PmT)bd@$et@DX&4b>X4or8r~AY5hMog5cCFfAmAQKNuATEB#Ea zJgtsf*`#uA%w>F)C;8w@GO=K&l}+=F(z;-rl78dyQ*B-28!tbVo-mD^ITq=}NIuvw zP%wd3M4Gxu!f*Ea&y4haW8#!Heh2aiCZRsEq9cT)(zJ!1N?Ar{WZ5@;r({VQmYa3V z&2B@kE3ht_=^f(+agqU}g!9W@nu$QwOu2dQS;_%+c0-6cuIel)j)TltX1Kgu<(a8H!0qUd!=VF|*bykPZDK zD>iNYbDR;*D1~XSNFlsC!JOV{Xq@5?7dU@fd$gu+*}NqDB%pnz18@0bz;F7#b&{1Z zHX7`+`khZophn~y#>a&xP1{P;s(1N};d8lw>2KvkZ76d7plz+d15v_vMpKo_4s~65 zQpBgz6o)y1XpXK>m&H@lytU5U$g`~68eUmZ`eC<>IY!X8LSh5V%QdlyO5{AYSkwL{ zDT+-Z4x&gI=O?6BT1w7wo%uP)Q=Dh?HjK|E^_0A7b+g6WG%@|X#<>eWv=8k@9qgit zD)aR+W5(pXY|eR1o#EQOs8Tw(qSjneo$;|D@nj4Yo*#yQ z3@|P6%$kn4L`H9|Hr`m1kp@7a>vGvaThM+Ou&|Ug&*+c5<A(=yu!{l4|nJ*7;Mab3eq`ozwlgI8w|+ zz^$g=`Ktu3NNcJ3I=IAU9=KAq-l#3f6={D9YjoYR^0QmkPi|1xvsPDFMP-lKZL9X& ztbmtK`{qlxcDGA-Hi3)7R2b9EJ!* zNTuGmUP~}*^2|d}s~%(UC64aa&+VR+@!1&8>)SAPM#;gg!mlx82QmO%H*$dA1|a`G zabm0N2vYmPvckss*VxtT)S0&@yqk&Np0--3X)o7-!uFhoANKl>{Zi?B{}ha*%^I#3 z9qNZr;X(DKaJbm_yYehW02z#woj_|>(L2FyOA~mI?<%1&AZFgRzV~um)7TQs1 zCYc*pXiB;NW^J?my}Z{ulE&qy|I+lnPB5A@KJpsLeo5qcLubLKYJ_VPwa23v3s2Cl z)btc3g$izh79RLJL>?9dRB*u$CaAK`TzxqHXQ+NAOBs%v#UTCUWvs8X8}lMdW7D+L z_qYj$GHp;c1ko3TA0%}G7Ka-Qph=;|Q&xbFi46q-=7u~k?L<2Wh3?ybr{fz=Ci~D6 z&Io*SS~Q4C`6UEp!W80KV`EVR7s{YoS`+uc0uAM%S1_QwjLpOSFaWd+vI zJ2uD6;J<>Q3aBinsfE~|6HMWy-!Yh7e98=Mb2Rx~XR*HX#M=O&D-qtdkA!&3vt;qcDMx`j zttxG6@Jjo(*^|CkO?rQdR;x|lI!rVsP^RVA%FTEa*be5-;LSx@eDjm^U!HQ`tGp&q z$;|~#^pP{it1y>b&phJc#%Am~wHc_@(g%Set^jV%t*=5pOm0z`d^RVHqBAa-pn=C` zHoCpa_^czzGYo;MvoBSmI9ml!#2TzXK8jm5N!1*dSE`jFkNgq5<-Zk83e>2`6WP_k zy_wl=1V)8y>{qYLGpc7C&GbBR zq0?mz4UMvMjv)^j$D-DcE%=s=aPwPbei#)!64vGjxCT%y^5FS!`$Sx87-w(En#JZU z%usEfl+%J*!WOgKttiY2iX8`VK~cl#qw6oHd>Wa%2OJ8jh`8SpwM(~ZsS$G2nr5#B zSMJEZ7k9RQQl1lOD*wrI(X8bw~Hh6S}L8EMi5Ok)0<{W)L{B<48^xXw?yQ#v(NuS2D zgjFR&$p}FnkkfIlD?7ojN^^u2^%kofE3I?-+!#|7_Eckx6|bUGCPbis&qlCXo)y|t zEs{JNqLp3%;r1@b3;5<^d)^OPRDNx0YO;j@+Uid4^#^}bJCg{}QY?uRNtQ_gULPL- zCuM{P5{NgWkAm>GfLPQ{7iNmBOZ1S6WLu3>7}(u5`c8o1j3>=FGX=J7`M1{!_jQ4N z0afwwjNaPhIk3?G+&H5wm^~vNw9uLO_prS~MK0F9P7xuwBTRmq-Vs>bh5r|~ zTPOX2Hybo^`lzQ1zTDm;`|TyiIXrm}EDw7>pe?~?0|H$Gx=mT>{h4Fd1Ntm4TMvT$ z^d@qoudk1Gaw?{>Uj9fp&uC|r;(=Dm!6i|u9qV%Y&(rkXA_K+N^;>(Wp+iq^*gtm5 z?S3Cazg3Yo?f&0dJ;@OfL692}WE#F$c0hNl*8|G0a{~tv8#_@Y72iroYLWLh1f_+UNADQm$7qn?L7rt<4ux0f^ZX&N4hv6@Zbz1usT? z3>^wR3{yVPD_I~lE?jPtW`Dj*9CQ^?m+Xmt#kqj!Hx-*)T5RWnQM=dmhhx|Xy7=a- z!NU>LWkaNI4=HC;)DsY1T{d?touEV6>nFE=$PIwjU?*~4>d&hMf1J6((ykwHehMG& zm$*27#d*hn$s*~6gG&0&AV+IuaJqQ-M{l>B#g`L_Xm^*KM%nZ>qN!Gp$OYOliOTU} z-5Ll?QW4l1a~3LFM?w=h=MD9MT_v=quqn2cxCn{)Op~_J1-kG^+7)vvjRj3HPe?;3 zM9PreJFtI$Z_5W9nIu}Y4mh?q*R6#gkGH5kX5 zYJ{Sc=I_pxIWpdDE-l2@`ClVbECH=@y|3e&<>+ZzzB>zgVHoWN#N~EGq=Ks$D0)Ev za7DQz9o0uzcAUm=@ThWvV2H4*U#gPlJ0GPKxWZ%Wj%dNUl8N-}q$x$9B{$etE9@>D z-h$A>NM1K@t*(uA<*1w@Ix#FJ@`%O5-;>o{Pc_J`h}sW)J^>bXB`=mf-g)q(T#j5X z&w8B!lCQm^;*@WPYAGkJjKBu;g6&(73ysaiXHCR6-}ohH6txF(Kl^}o%>gR-3b#gYDf_VE>F4Q21 zcx`x7U{#eIyv+qS|FV%0);2L9QMWygJQx`eeEr@nWaOS{En-i5bg?6>xZ)f%qWW5& z>N>4f4neFAcm@#zQQV{JKSUbHE?FhvBD`<9{W+i8l5F=OMf!yapN+{eRYE^wL#Z<~ z0`meEkh@9@-$smOAkwy^K+!9}V9pE7KY$Wk^(6n${oYsX#|qNQz1p6|moe}s&*CmP zdn-&THc>tnx2o6PKvrF6>3h4~$ee(Zi@oeUv`33u*ItI*NursCvH>`5gm|BCEJ}Lp zUJemUoVJdJU%h!Fsn&DETD-jqZlIcqvFb07zcY&oLVy$LkAZc<2!}U%-Lg=!u z@u&6qk7+f-@q!qQi?Pp4Lpen;MCV4i`g^8?v2;?M;{cy>)8l>7mH@1&R+OSZm580I z`h=<0fL-bh)%Hb1mbhTwL%>_m?AEduor3M3B-W_T5^kTDn$(%|SnsCR8re|XM;6fB zk!0}MXnLy>{L$-cwxv62yPyENlt|nGLEnBNd43Pg~H0o`Lx$x!LXgchb~f*3Ot^TIi+I`dPb+6sfYBLbY# zU07CFa$8Bnez2FXU)so;C?2THHkN_5%VLuHIG_GZvx1aa== zR(%jOb^{GqH-7)u+=+pf3C;Am?+FA%OIfmiW?{k7L@CWZdto0K=q~I;e7ty|m3|P> zqov9~07;c%zkUL=J|<9wxqwsq0cYwZkRx)*;YjV^kL5!ax7UxcGxN@tQ zodw&;tzH^;_@CRfz0fz;X0MIy#LHR;`KgFr76l` zlm~=w1uC=}+mL9S9L;r|E2iv$VVff`u}C%7V+?!P^AC_WZBX=+88I8_Jhu|H0yT;h zU~9>}HWS#oK7puBgWXVv6~{XQKZxvm_XL%j62z^@)5@I3*qjNP+HWvhHB-gd@> zEsFLyDS9vYgBz~bPomacJwA!8Fv8Xop91wZ@|&uLg)dnuU{%5gQ}lx*T+3TzP7RK_ z6_m)Avow)O#+1Y-1hkrKEe7=a7)!=aFt%sM00hXX@!egJ&Z^$20zZYI`FNUEm+X%8$6aNqlp5>U_NzPorPO)Dr5919 zz7PuLN7N4o+w*H6c8b)&ZmruNdm9YV))aly(oBRhp<(K--hel`xK8!%4UHdj$r&*; zOPp67(=SiEShZ+v#Mu;SiEi#+)gH(~DQF&*{mgvguN@W-X! zh2rkL(z!hy1z`G52Ax}uX}c025Vzfby)V34JD_imL}n;mkRht^=j~ZYKX!fx7n8W3 z^$;cw(0d zED?Z5G~|6T=mLA0kOgY&>gA3+lauMrZ0|1C^)npW{Rh;*D^3tja zUJ1TP8e~X_CrY@9UJ#b7qxZlRB&)hU1*wGl_be0}lF{Bo|GKRarJT<%iDhJ&B*rM&4gFhM>kYoR3!_PgE64;8Me*B#ohjo+uj! z6`22;_7RYUeiTYLX~{rKWgHu4BXCS%?}F}wTTkMSl8^^7^ae#@lNUNjQ(Sj4{nJ9R zu7a9dbd_8zTQ272rQ?&xPu@=<%cS(1Zu5(a8W*XsXMqB@#JO7nfY7V-^1FNPhNpvl7v)jhD@9J?eG8Bp8N5q2J>t zGW5;I*Y+h^=f4bi2COBj1?TaAt`^{k#|T>m5?Ozk0>z^11%20#5p0QP@s+1Ixa2rDwnIh2!MI6`_BrLJ)mF98?r#a?(rV1W6TDZpJ>tG`y7wz)!vBdwEL1U>8Evvc;2Z zT24+usE!U>xUOnR{dTc(`s}>76CB!MU43>D3{H(DCMK4%NZ-Yn; zIW~Uhy=41d%VE~a|F@3-R4rL+*fPrSc&(|#yzV0U$?y+WkfD|9|k$S8b zTx3qd%wi&eDe3rBXryySSxBf*9lGKBf+uNv8Wp3JpdDdN&g7O&vLoJ1ZWJ{w8{ z#{n(!x<)hGei)w!C#cX@_G)%#JNkON{VwRn`(oOewEuB4gZ2;V@Z1e5P z7JrwhM{u@oC@ccT1flgG!l&H+3O73;15BQP!+N(s7_7EgF1atm;F516`5sCnKRX7V z`S8Lf_b|7y_C0`9H-{~cRGW@p0rML;PyF5~STAw7(Ne~G&fTUXI$FiWy@FnH_}|g( zgv<%Ovn`e+ZrTCw&i$8{7Uo);w3589a3*uYTX;Y=`5@0b2`K$;_xRfijBsEI+Jo!& z4KEVjOh7RW{>bcn>~YoKLx<^gnO}jsVn=-Ns5igBLc#4d*V1cG-cRT~oN)U|@|KH9 z@<*l&BrKTTwLd}|>ozTL{cTFF|G&J=Zax|5G=d)RAkfmK|ZI+YX z0oDafIUM@0&|?2MZ}Xlw28LNPHCzwA8Z(G1!2Q>`CkTt*RBqZU`|?Y90s(F^tVB*H zeqYiZifj*o(iGreYC0vLS&_$nhgiJ2^e;6;o1+3&7pkXxtb;?J@>mI081`?(MUr)> zem3iEKK4vy*TqL(MD2jR&D_BfgqC}aWP&Ad7RBwh-Wcx9XfMR>7mHuR06mYX*!?^F zmSXGuv%@DRouT^C=6PoVNX(1m4PZX(3BOvO?eY1-HT9wfu|$Xm;1*ZO8Vfd0t4kI! z+rB;cpk8&K%cEO1ZdZU@lg^MbrU@-5ll%*8lUV^?!T>tpemPm&2VUbjCs3_&LVgK? zRP+Z=yALK$UR<@HPT$jPkYl|$!>SQ3!5aKg>@|d0eG|8b9zRSgaCms@LaO%s`||jp$UKl!8HKB&a3UDYk^T$|$ zK=%Q9(VvA8etFEBoQX}`GJv%sOG@B_U{{Mk-By@{+2;INV$ zYTTb%msJ@m(WAcPppAqBkAXn%g$|=Qc4b(2lN7kt3D?hD8pJla+%e@Jwe_VWu+d#{ zM}cWq!M`xHADsN#*L^{+yQ)^_n}`S4db(RTM4`pI7JrB5hH>i8I!&eHX}}=a^|9E} zwBnpTZ@J;}(C{u&xqoEV0;^GP;7qNe45*&2H19{{YG^?NrW4_Rw`6vJd5%LvASxod zARuj0_JzO+b)Z<3y($WXsGH{5uKa8O$cs8q0AMJ%_AbPJ^@2I2PXB=G6-RBV=UGE3 zN`C=MXHKk%E>ZoiQgXrslPe5w75859>T9y&0dZ+qcM>++;nZV$o?_Z5=J<^8a9uv< zjwhyKPt64!y0~Rg(*IPpyA1)1UwjOc3%P*#?xDMNzClr+=VN8O-6s37k*Knok$8Ag zFi=+%M))r8Huok?q&vgVO0ZH`Juk=!AZY-I`PJ4blz@<{i1R8b6jaKJ9m>7e`@RiO zm*>gF5HlP8vY_fQY(&4p5TA^5gFn~QqP2Q?kWh}LT6w+Zo40>}JaIFJT4H(25r%Nh zbozC+iJpH>5MJhCT`9YtQI%Eev zqw&T*;U4>yc|X1~y0TG={U2WaH>C^0*P6;sVprxo$|2*NB9E~vGNZ-A4ZmgCRPftW zUuW%3(9I1yp0Wh1zL%lZ(|?0wgc3H}iYO~dv>q*uh1NXc-~CIgm5!jduqk_T7_F54 z-ZF#$N#zT5l}I7($e6|8EZ0bHV2j^;F3XIH!UFRoT|x4`m5K?cxsDWx_z= z<+{wNlW>A;vC*oR65rE7^6>5`%fyoh1Wju$a1GMo2%#eBy{v!&RndWwZ$QxiUrALb zUAU5^qJ-O7jn)^4KRlxYTIUum7r*qF?OgK9@Y6-fVu6zOm4v=S$&5|Ef|a3`5K?NG z29&4YK-EvL?EUpzaeA6~mC(_^X9g40E{V7P8i51cV@Ge|&VBTj?52Lrb=q1_kc)Kv z4|H>ZYnTTGG7mo>j@hJ|R5iLrLF9XN{w-`5yhjubj&xki_AOxM~uQeELs)YDVbtgwiJwDO~TlH!NbrE z7yWN(Z3otxg@aZTxqn$CWhn2s9;_1QEXg$p@}}q` za8gSXZ+U;A7ahB8u+)=s^Gj~~19#{I1w|-oD2gO+zDMSW>=fyT-bY7WdNr1gs0v-? zo(&2=m{zq+(-m6K6c5-R-qg?7&DZT}E$}g(D%MQL_Af6M&COYihTro`)Ak~6w8)Xw z>V$ST95wDFy)d+mqP`@IIln;`w4s4h5BF7odyWs}Ee{X4DqVU}nzO~j%7u$LV2Br` zGZXl3lF~U+#s@>Y`YVG2@KCRwgb1k;)A2?yZ#ouKmNr+W(*Q`ZGXH*H1LqjE_vNoL zPdK>adCsP$E5xG#Bb7WpTQs-0K1kA^s!p#=9+@qo8MJQK4;1GnOf@l#Q(>2v%p}%I zO}&f}kG2K@y31NJ=!B>@m>)*^hD(N@$vphCqXb}3sjZy>=mGdSSv>C@GH{X)0@eHvn<|mo)R|ZtOKWXN@{1 zeGp=6S24{TZNyA-b{bJd3A2RARYFgJa3z=g`ssU~(KGBtUyj=zte!~&D9$4qh#`%P zq7mu3NBkJ04U8BR$O49_W4G_Yyq!$brd-Fbse>EqeT35y5PO!soi^- z89e(#<_2xdF5msGO~vhRo%Hk}kiWulyWu(794?D`duQ@RZUIoh0p51wJOH*mt1qRQ zh0@w$4wB)(`DqJt6krqA+y+>ab3-Gtz(85$$R?LtMZ>;-&UL_75Mb5ehbAsnxqeh{ z-*EdqGK$?#-|we6F;P)_C?dtGOgM5qcEQ{X?U@~>|Mo)Z2@fedT;+8cUj>=orVR~F z7v<_f07Ci;{+JaY7_N~ieJ~f5j>|)iryUZ?kyd3mR`9{v_p{=FSv!FSb`yHA5{1~y zw>EV1z0}=P6qkD$|L9$uzXHhcG*@!t!kFUbIR3z+DFuu!XvEr7f} zGb`fMH!mLZliTBknobAK@SbYa;=rUMGQ?-c$}xw~^nbHo=@wTs(C!*%CtnzwaUR;YT#lu$^69)cJU$kM=F zd$mG>)I`-Jt-cvn1pY0-vh!)l5jgv(iGhZ+byj3pj z32JGBxcgX6U6Hx#bF~0};mBv{FEP^Z5#9xLIjkcKO^D{8!_6qe?e21>^Js38Sp$5A-g?rjdG4VR4*Ukp(yko9er#r4 zn(r2TGrX@b&hgd~V!GJZB*on>nHSyTek%Zc3NWS4sg2qh@QFnN=&x^dutK9do<^v? z4k&*sHMPPV9lvKfy_LT^*#I%AN=Kwm9CI$vYQs-n*4(!cI6?m-cc0a{Ec_T58J0*v;OFr-Dfb zeYiah*JV#Q``)RPyZl|L;}W3|{*RFJsW<)C&io?0KjbDw80ce)C~Tw2eV#2AZ#^~0 zDR-~Xc78jRTJVeQRCw`d9O6JJxub6i(e~+4t2EV|5M=F=43Au?H(+9-?(MUNt;4%DKD>_ zq6M)YkW*tKM@RG&X;K4g&+4-LWK~|7A0&?qd<+|`Y?;s&`X#<6Ok{n*bNi?^+Pwi+ z_Osw}FmK9si_pekKXx_8!R#y=o)2bmzoe*$WR^rVU(3@M8Z418-*ap;xaj>>IMi&L zRwm(#hH$PY+nu{p?DT@$)^s*1N?Y`v!ReG2XcNghKe-)|N>C(FN!v$_;RSzT;A>^4RS|E~T z$<^2$4>%d%>?Xa5`*|;HZLB{L?{jtxJy*0wZeq~NylFaI7Os>Iz=fc`dXskZF@LpK zV7_=_^2kvh0VyW$%P_wPpV6NiG4EGimVD07p=oJcrJvyO$f{2p|2b7$*5+N_i7Jul zOCO%W$ToD832(Gd%bcc>w$-t$B_`sBkmcdRS$|xMi7B{cTMkMvk{(38m%pu3g!$Nn zm_kE`&Rrdmv4qk*vLul=&}rflbQCjW)w9y7ajH>|p*+0HcBV%iu%JSw0}R@vzeT|b z!wKLhA;b$fSxaemb8`xmxpN(Li=Btt3dXuA=~(G~p`FhpQ4G$R1>Q8ebh}yp_%iQP z@N7qB(3|{+fvu8gXN_O!o|Qu1sf1sx*DDet_WyR2AhLOB^_hd)uI~X2cip69q!0|n zt-PAoOxLy|&1n8OX`};_OhgSZN2_o44Tk(MzU-kYd@zTe+}8hXNSk zR>7LupTQBsGJ_i%pGqPlDOj}U6v>e`+1!jRX=s9uXu)5wJboVjmfDCLQ9R#OY^d4e{uw5724I~t#ou0bq_ z_ff-(H@7V%QIrwSt7$;K(7g;0y=8v|XzgZtetifZl^N4dXT)w_2{AhEUw*##PFqrc zX8T6wQV=a){5EMtJOKKZxkrfkBNH?;d97BC%5rxtkT`ap(6!A9$tmfnPCF&f^FS}0 z9)f%@MZjKwKmE5=%12}dQznK7=wuCe1#6-#&z{tTl`c!pnPU<%u|JVGnPFUU&H|l| z1?9&!srPGT<3bg^((gUSn18tC!7D8WOi*| zZ=QI8)&SZVXBmLv3vf+Wz40kTUV|^~!pNw-8s_Cc;bVT+fG-j#MrRSR6y?}CDq<@0`j^3p9N-;YzSeh5{EMax}SH#?ac$zZf#?*|%PQ2&E-8xQsc-|Cd` z-P7)BaxOIA&DNZz{G6a~=m9QJMQGfrOeTC4D`p7$ZXso++NTABDB*CLus>P1C^NX& zaSb36mvxrR#r&$*ITxaCSt0f7UCK;w?@UN9Y~bYU6}aquqf$TPHc7<8Kbk-*nsFbC zvPy2@(9~D+4k14q>^k5ssu@>c3Nahpj}t4Iv^a*Zq3${L9j)MN+d$$)5bUNXt3$cR zN``OFpq;9xyRfIw0HB?78bR^plNXt~=9HS`TD^Lv!*bal)tI2ALr6;m@=7;ey?qF% z-`Q%h!*;w`!ckv38+q#LU)njvPB=eeU(8zZtmRWvOv z5IeYQ2BGY;!hxzOjmLa#DyTK8UlRDLJgx!zr-V(`;G!e+R+-k%!D$2f=(I!voTGh=MZ-fS4Pg~pGbu74> z)HFvbVRyrlalyu1_sTCFh#8h)+>08ltUJAI-f*4$e4mBhf%U^^9;f zxIn?E8?RA|cHzM+vTLjD^!@AYFD#%{>ybA z`~D&@`7FzFPFtzM^)iIFw3MuW#_6Y9C&l3J4khAZ(opDvl zzUhq}715Uc^bxN*`#@uEF1=j7R+AbN?natth<&%j5W5}BV?}XNFTB0W#L`J!x*D-- zMpp;V=`OIQcHAxUQ!cmP;Ehx0?rE53G?o9L>S=~ ( -
-
-

- {params.title} -

- {params.subtitle && ( -

- {params.subtitle} -

- )} -
+ render: (params) => { + const isStory = params.size === 'ig-story'; + const isCompact = params.size === 'whatsapp'; + return (
-
- {params.author && ( - {params.author} - )} +
+ +
+
+ {params.tag || 'Feature'} +
+
{params.date || 'Today'}
+
+ +
+
+ +
+ {params.title} +
+ +
+ + {params.subtitle ? ( +
+ {params.subtitle} +
+ ) : null} +
+ +
+
+ {params.author || 'Staff Writer'} +
+
og-engine
- {params.date && {params.date}}
-
- ), + ); + }, preview: './preview.svg' }; diff --git a/templates/sunset/metadata.json b/templates/sunset/metadata.json index ad80361..11fd860 100644 --- a/templates/sunset/metadata.json +++ b/templates/sunset/metadata.json @@ -1,10 +1,10 @@ { "id": "sunset", - "name": "Sunset", - "description": "Warm gradient template for articles and blog posts", + "name": "Sunset Editorial", + "description": "Editorial newspaper-inspired composition with serif hierarchy", "author": "og-engine", - "version": "1.0.0", - "tags": ["gradient", "warm", "blog", "article"], + "version": "2.0.0", + "tags": ["editorial", "serif", "magazine", "newsroom"], "supportedSizes": [ "twitter-og", "facebook-og", diff --git a/templates/sunset/package.json b/templates/sunset/package.json index 582100c..291464b 100644 --- a/templates/sunset/package.json +++ b/templates/sunset/package.json @@ -12,5 +12,6 @@ }, "dependencies": { "@og-engine/types": "workspace:*" - } + }, + "private": true } diff --git a/templates/sunset/preview.png b/templates/sunset/preview.png deleted file mode 100644 index 6a928a7791cbdc6aa3dd1946479106e27fe1bf4e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 110277 zcmce9c|4Ts|9*Cw7?L6T7A^M5zLX`csFblqWGRZWFJs?jX(v0C7BLdZGWMwKWJydY zCi_0jSbon8BWs;+pZfiI&g*r~?Rn<8-}m*t-q&^Aff{P6)LWUiZrQSh`q)wBGh4Qh z!nbT8wx=Wq{*C$}7riY+sW*=)AJ%k?9>}MNmpgW@*I+o`;NeMH<=dAu!Vg1uRZoj1 zUOG*8+H2@}08xoSGo1IaK=)VPsn=AMu)Z200L& zP}b!ALEJep0H1laE9h+c4p1wimH&PN+!k^SaqNzXMtu0mMk(n8p%1ovbhb1!eTULN z>!f5{?J2sc>C!4xNj{0-DXJS1(CB5Q3t~MVxV4uw(C66|^PXsJ$??}uH0?}xzY9;? z+l3$|BR#Xdglu0dnE>7CTaQ3K4H+dpAMfv0D?1~wV+Ilvt)BF%m3M~k)P1GEcb5_o zbyRkzpX?8ex3Sis2>R4y6mxYy*!0ookMK(ir{gkep?u_QVC323s zDtbT2EVk+H^XhG7OlLGz8?`y#9y-qbTH_qTqfF>yto6>7kEw!C;qPsDYAKF;%@8UL(n~|xM=ruK4b(nfV!Q+w+`fs8Y{ED#q+Xu_lHXk}Kd+ zY~s@pT-a54`w}zo1>G8q#(zPkcLt(di50f)xE+8w!r!QxP=CY=QFK^tce8E+dQb)v z8FGdHvu|sBhXf}R)0vRt$N+tgp$xnnmk-DHT`(=f%yA*3*GZ)c6vH;Th;4io!X zKB>J1WhiFa91>dboTH~`k3~H9GDVR2g4C`mo-=r2bn^0ZuAz^>bGCNSuE?i0 zwu<_9e<-i|Ma_;pKM!de&=_P52W@+%8e^lp`v#QliYvYH_9xHdpIMoXRyI65{prCo zrn3*P1Yo!BpgnrbhcWI+MvliW?!C8dJ*}PXp{~DM^Q2LklgS~*4Ik9GQA*W^+y7i* zvD9I1+=F6FAvLlucP>9@+94v(yQO1^XXS%8*rAuOrg6|CNv~9RrT9yJi1#j2MIQ z@w3#|4Lpg7_fWlQDJ^g6a36-ip>#RUVZfudxVCODdj<{iwL}ikf4mHp4(3HZdM~zH z<0xMrYm5e?iS!*xX$lIvc<5EZrdIKB!z0i0Eg4O{osqv-mas_~mX`HY;RB5m->?1T zva%H6A5^WRUi<%6imD|xD7dGd^;K%z1Zdc@1b_~8Bz*GLFF$MOtivYCI? z1HseLL64!k#{{LN;*OYP92-0JP{4nC$a`fNP~?*|F7q5levPNu@#1CKVv)~$Fj#D< zG0Z}{ZQHXWkFAvV0$mF64D9sTknZ)pP*hW^Ul49q^L-9Y3ao} zPgJyGzZ6U0J|pIbQf829*_V6H#q10cee^Umdp~Z!#CnxIk|gS_BuhEOr?#>fX?)YQ zy&)g+!Sv3zr~}$GyO{3;Zd8nNA_Jc!-P!O_cWqCWNRG5x{irhg84cAWc#XL(GqXg~ zGs49$bsdt-rxd6bIQHap`VN-XYT!t#18V(y#C!gcq(fYRt7^UXjeG`YBqAxU^{IX0 z`dS}i<#n`Jli|R);BGzZEA+tEi?8+9&3cF{fB5kVa6T3-lDbG_p_rvddN#OdZQ)%$ zx{s~;RoUuPRlz7)u6P53Ou$;fmjOu9EfW5a5HR`kh%Pqb);-JIdK0|c19 z-|5V~NVJbfcWd#F){CPin^fLQiPcNTH$(3|BK_qf=GOCjW#TfLG8Es4mHJMvLJWJ& z`=YdE$6uRjPMYj}clQbN5@N7`1bx^uLM>p(a4E`b<@bv(4d$9z0ld9yN(1!zg`Ks& zH}W}`N&~*~4$E$59jqp{tENk+xOnfWwOaVmi06K$)%c_I$np`hPva2hE>RuMF7F}- zxJOy|ReIcmj+>m=k!-XF@(Q*|wO_8hUQ3ukN82((DS!N^^=lc>>dH&lGHWWY@*9y- zaFQ?t&d$|y`QqJ|#H$dX6!i(STevN=3IQJO-KaRo{Nh0gn1Oo;|Dar1oV;K6z46$^ zxo<0By-l{LN@O*!S9Q^0w%zA-gYB}aWFu}}5pG+F7pTv$&^%LFUFr!ncL^9#Mf!4b zcJ<#L6E=LCH}3iWqyCKuZ3!`SxS&??#v?P@fgLWH!5evGQ^>gvEnG8A{DHcZE5g!< zB_}0OdVcx`%Pj!r`fo)l2bTV*y4(}B66Q*GRacTlG&ngz+?oo9Sk<5i4~FU&ED*E% zybVvGK_QmCz;R{{)Z9zRZ}iirP*rVQUF)2B{R9g9s{as6E8oOh7d<=mvxA5P zi6pb!e$%p_$iDY7RRbgZ$@foW1fct$@gL8*eZg1l|8Hw%Row9i7@v5ATeG*3Wb-xY z?LyU_28+e)#vP&?m)prDgrYg3dU#6%tyde+a|^CxfMY1E%luQwtm3>wO6#R0Mnv z`j0A5dQL!zPk^vN{dY0}M4p}Ds!E%D@rS@|R{&QhC>x8NMlRw_x1m^QzE%&|uupy*M_uc2!9w~RR2I%w^ z6eljgi|sjD79sAx#9{1pWz6v%mrG|@}0F%Gi5|%{Kb$;{D-OIWfry7gN!?xB~Dkm!TEy z{Yh>V>qaEeFX%TyH+XJ|ek+CO;ot1;lLl5qFEnh478$>Pioeo*A-E3+6jA?C*ibZ3 zj1SNF{maT%M+U)HAd%z;`h`Zf7CVODb-aX4(q5_DQ1^Z%R8-!D5%a*n0Z7pMLBO6!=cD`XAbiaOkDYsG46ko(%t|9&kkP$xTBCQ}yhd*3RG> zVo0#SVs-zQ;Oh(`;1xNY*{w%3MU7k^0K#U|2mO0I*f-%=Wm=#f^wFNo56mMIfM~Bq z)4%8d7rXkM&+y&YBFV_Ux&!ojf`}{w$ka6*;4Or?7#)76_%v8?UnL4Yb$5}fshFx- zO){=)bwNu^HJ)Ugto{W#zP8nEU_7xMU-%A5&h2<2XqX=O|ACPoRk?is?AH8zv{qts z)VJJ|Z8UlypuQKu+&AGe8T@)jBwM*7PCk)dKelW zJ-8RfbLLh_pGIfYhQo(1k#D!yNY@yNDlgTdChp@k$NFO*x{!GnO=Z9bV*5_(|VSun8ENm{ueJ z9OJ*Nf$hY8+XkxfQ^^{&1b}RGQ=I58P)@M4`^~C^x*)N?&enFracH7)*F?{O@v2qh z&_#+|HW9?Qw5l+%w@nP00C`>CB^fjPK&h1%%NJJ|w)wOUPfPtA`EEu_;pZ%i0FZ?{ z%e-i*4spXJb9rBZpsDZj{_}kB4~Q`CEa-|Ah52u37wV*ng4az4H-Uev5X$nn_m^W( z+If{Ank6J=1ef%`Ga_W@Yk}fxlfssYG2av9elK4F)c6ak3=+bQP z=MZ$m*&r#htCSMAO#ti%tZ1cHoZ*rA)=zESLlU6hGAX{n-1WbWBb8?oOPQC@NexZ# z@N3@hAZSA$3J*>;ddh0v^U+%5wh4N|0gD_pe&En36Gp0g3mZOeHa~%DvcDe?h*4Rk z7x=~a6OT;#TvzgogpPNuEaK`Y7ZIW8mGnf#nPZ60yM>7F*ieeG%!_X-#UM0hD+MRZ zjoH%t^NFDJrQ)TH3FuAk^9KP$WAoD!{}h4sl=&&P#I({2@kGb6i5p}ovq0d}R~i#4 z-k`p@GT*D>TZM1eN)&AO)1Ol1Up42z>moF#`Hu6dS*h(u;u(y`&-bs?MI3cZ@FZGn zL6aFh%OZEZ-~Ivwojv&N{gOuEU-Y1toIE}0RxWxhyo~t19<|iE4f+?W&>J|XM?l6; zQH13si(vV`Cxc-#|Q zBVzm|^Omxtk|O9^)-!KQDk(9X=tJ33)B{hv75`AdOTno9&P+n!uT0iV>i=TROIO+Q z6L)2n56zbtnfYaom`=2ZX=UV2&RLHzjJ29NmnaFKkQ^I4YvjP(CH2Ul1Dy8z~kM=!{VXrJzZ6*SZeJiD11!QmIAKLG-%ERI;c zdX?O^{DeR_E$ZVl0KjZbTp@s6R)+qz_>6~apmkd#fCzjAMzgH0KAHk5--Hbr0yk2? z{vFsgCf}&;v$RJDT9#*AfNoPIaic1`N&-7k4O88`>07DlTYl1|rtdOG5BW3zEqX#YO(K90cw!1Fe6Z3K<~FG_K1oA=Jrc9=wHZS zkIs^~@~y6WngR!XfysUAt3~12^2llv!N)2!*5=yVr74*SUVDoH5BUa22J_lcK7n}S zFy?{1_m6YsEkEhMAitn2DW}X$(hVs;_YY{*|6-rzu0-3!| zB5M+`tRD#y)cQRS0x`oR+|pi_JDkDwCc+Y z-?H|C2uTHkdS+ay{Wp623W^c2RXreX!g0}i*pDoQ+U!{Uri&7Up__|fPjg>)ws|;X zx{(O>AV;3lY6J!-#vy#u|W#Jd1& zQvtxa=kwjQve3T)!%|nnF#M!pKrQ8s$M?Rf;I#|hA?Q&5cnZlim7ufCL`}cK3l}Xf z$q2{oF7U|Ivts`TTKsfy!*Z!5$TKUZsN;6$dN8Cud+-lr`03!f4OVP-O{@fTj1MtK zS4rY=zNd{XyI3B7w3;CTcV zYUQG*6y91A?aRNQyYZCuf8zT6zPW3Jt^;6z!K)`O`i;jugEZ=YAi;m+ekhi5UCqNA zpVU7SpLfc~5O&HY)6P};%;m}Mx>k2dpZNfo?B;MaC@}GJFU=mety1`E0dTWSD~+8X z3jKQmu)t}GF8s9qXDhfg?d;MK7d9MizQezU_Ftxj7M*1q`j80QWfLD#$^xfe0AW(K zO7kyz+>z)+YAh?^L6?n!1oi75>#>X`Nkr&3C<|5c*TsO@{n+I6u_cq&U({a&gAtXh z2{3{V;t~V|oxCy@{`VF5o$@1FmpkocZw}7ekkR@X%h68Uq<1t&b%jCkw{7Uef8xNf z@%z>xKb#UZBeI70eEr<9zimSba4R%f(+mdr;-#-YKJ*zB_5md0-}VFb(#;uu{5b|( zsr#>an$kg*a?3pWudDD&~exbEp zBE!$oq4AzBT5R~*fw{KYlZsksblS9#7$pgQ(zRwRt$Hj|tE zbVjeVbFGn^aiJ4-EGfU182@E2Hs#%W+J(orZ|1uEbL`c*ARphSnfb%G`#7MAPdarYx z|FZh5b2{O(62|1M)o}FmGbNEsrnSCOfm?{ptHGa3@9X8wf2so3xxcvp|I_VDt4-(2 ziiGU;6l|7?9!aE-WRP3tM(9)3xAUG-bi-w$X4F!NaA3b40| zivQCz3Mw?w8OkMcW@Q_==s7>z`q{ex>@N;$Uqc3;>pK z$+EmnZ#x9OLof2L>b}HAm5im=^6+H@J$%%uUYUzTFQQtv|3>=%e;YvZj$IVpfcqlo zu#CRu0oHJl7Yv&RZ>)qS%Lda;1rRucDYf=ES32veAtc~w1*oA#O`Qs9GzmbbYG(ot zQ$kPnzXPx%95gey4Fl-#12v@=%*#t{0zKXySTVl^tlHtap|Ngokk zZ<3|N?Z9SS5u;cBM7QMk{0j~wtjO+-Q4vn|O4^$Rf>I@330PFt{e?-S;(${8@>6bL zfo%U~4@em2UGa?V@0{4uNSv2{t;cGu2m(98zwr|;rfqzG5BQ+&_xJO!)CT-d6qw?K z62}rpvtjU$AAhrM;nwEn6RRxIxU``^j-g<$1G}u2T>v;?h4Mbv>D4SiGGZ_rGU2}< z-F^{5p*-NtzK)7ov8K$`S6_&mUSpg$T=S1ocmgUZWYtdx)WJAUuFAy z0Td%{a2C!D*)QP8UX)-#5M(J6p9fn}d839bgjglQL?v#OwVnY#d{1kPP|Yz0s^Mo zuR1NKd;HJsXT+4JAj!J1=c)`IHKWgfiV?Q+FD8P`9~Puvx9BS2Yqb%O4CD&Cuqr|7 zf6~4-xgRp#q#-02o*3ya6h7CA^WQL==FdiupTMRB)6-8r3be}asSO2IH7K~b;i`X> zh$A3>SCrrbTGn8!tVe+0-%Gdf%2}DXUoHB7Bf(s+c0O0yDDwv|y=VncE&w=t0PDTV zM%ib70Kw?6w0yrVfMSO(%7dWc?I-_IAXorwdi$pMYcXk!d@F_#PX6j*;;M+iKW{-x zCt|VGw`Fi5>hfb5cG8G-^!pc;z-@Nz%L7JLj#!y8R3bEDt^N|4x(WmAs@+CJ0LjTeGlMYr}FML=P ze@K$yH9*{Sr1h^QPk>M2cF3;+7sI_OiO*|_FGhtDFwh@&eqIaSXO{hOtEP@CG9KZ? z0F%0Q$v=i&Uc)5yc!SfbY|_<%I|;XmfdScHTQYIhyY8pKX+xx^?e}4;$!|+f=r0UT zOB~emrCiBx?wmoJux-mpiwDkB=oSWj0CGZq*KHn0{)gPJzzMJ-{gSnP)9S-~<xJJ4;>qW@7J!xVw{2mzA z>)h230TL3HK+rFu776ss;f+@faDDpzr%Ljt^lORVBgk3y0$#RNEB<-t4s`tDlVub1 zaxnAi;)ffl#Ml@DF8f5XzY)P2Sm)0q{~H*D3p~6TO#!&j ztV+Acc*h8WD(sp3kjD?*~54R@8` za-v^{&VoPQG`EIw;s1^!$TADzdvN8EgG|f~)y%!K*vyk7^4d-8EhpDU=7_-B{0Lk< zL9D-|&sb5nfQZ=UQ?=HV7Jv4 zn~=pM>qdvsi;|oH2_wZtd((YM(M8Y*=7YBbarxcetq!9YUB3AoP%x>1KhyIuOt(=; ze~!!dK;(Ya5q9k_j!pR=q>^`?gRRO~d;&8w{Uy`3Y-R7Hx(b083prC^Qfc0G@Tzn5 zQ(IG}%4V)ipnhCEA+?h>XScz@Pt^K+b>SgSq zZ283zCxpau8Qtkz*dqE2EJ~UmaExyYJ80rnKH^3@6f3~Q&cUJUpUW{}yKUHCCaZG9 z@hkk=>LBaD%Sg|kv(ru5<)sYkTAo~wgY8OSp59t@-J6k^9*4aN62)8tQO3^I)T@Kj z1TPdhOv4mpv#zL%1AGW%~wa%$U7*wvQMEZ%gCmiTdZ%6bXP@axI3y-IvB9d z&UK@Gvv0OdzDg0^wla||z^&~&_ivi=r3ZuKx_}&2mm}3tkkofpuKuM+QvaO@BKG}m zN|rmfU6s~iXllSGb}z>A=Fz@JwzUt9)NGNVv1cC(ey7*_1a&XnVP47<{Y5Lm|AxMt z(R6R2we0Z^U-m9O?!bV7F*{I2y05u^v@{rom||tzDkJu%`puDsvWvScOeornbuR?5 zefTC+QBXaLC=b&zF*@b0E<4mAIh!66H%I^a`#?VSpeZ9Vt#l-|1?wBa6#C-HbN-c& zj6yKXtVgUHrrtq8Ko^2gl{r#9L_K6qyA4U|^5u(2!VK2{OZ!2ej_GT@3>P-=SEqF{Km+ai!HO%1PRz85{$e>1+B zxwAayD#Jfc)_o^yGM|T1AVjirExn; z+dYZGqyj$?`62BGX;e*RD^u9d6^*`mwGfL&cE47in*)E>FXWy@zg<8~VSPAsFSa|L z6~{S6?pH@5hR#BGci-Npj>X86n|BLpLf2~>&`*h=?$BjOAF1s z5@qcEA!XkZJv*CR6){gO+%*y+QpZ8kZCpCyNN)*SeRD*6?TE`4_hGE)XEZF8OaQa* z7(a6K!#p+uQSLv-<@QBv;KkypJ&j3o5ISLyLA9;F^OGM0q_tzq;92)x%y$LY_twX8 z#W`OMT-2SbsIf}3)6x}!k%z0H^P(lr+564u^b6V9_2VpvEYSMULg(HWF6uc7CG)fj zsb0^8R$hpah`oR3zn~6WFw~_nM)?B#3DmpVeyofG z#w`QpKYTbXI^W$TS!y0rpm@-jZLU8AIUU_o^;_}Q z9@mNbOJ~x&ScZ=G`gfc`c9#QT`JO9c{CX3H_ly3%eZwc^b45*%2}m*S^g>*#El8OB zDk!AmuOQBzp`dSfC!?xqGTqQ_E{esb=hJm%zkE=KMO;sDSmYf>7!i+%Lti#=^C++t zfCDD5zWK+RyEhVtox&C}3d8E;C)2rxB1yn=CQW1lUUyNz*Yal1c2oixrQ1m`E@sWH zkkR0C=2r#}akYhmAovf9?5Bf}G)jdd*v4|xDP%vFY5#x=GtHE|gQwJdy;(f>^D`tB z?kyZ$q&MqAnWEKij)QkLG4D?q?5}OP!J%$TGd~-bs1<4e>{xDy4hz*pt@JFE z{&^Gr8W%#+7x13w*1e~79p_@Ybp$Y|8WYMRg)w?Nf^$npPNka30@s7+S54^`7}(Zb z5uCWA%^}-70GrJ>Mel1jg0Pp4grsxu_;qtn%;1jqm2o_>^Dc2r^G=6gL8N3Djmozk zcC&*E9>WIc_M097| z&;d^=JIz=HDKRkEWlq%Jf|O%ZtA-HKft88nUN?B|`Mia`-sg%_7*{sgiRp(2)L@LG z%Kk$?63RvANSksEEJEXY-F3ESq2kNf6G1KgK+QRqbVY9EB_*^Nc6!1dTePLGBn4OB zP-0oi2=9A4LOY?)Uh2}aYveve5ct2^uoV>8cw33JA+Z7CAT6Q+brTp$)@Cee!E2vQ za}SaU3k%~gdtB2B+@i8l&-QLwfh3?a4upTq-Rzh23Z;*Lpr|%*nRq=t4EKOE3Qc}T zM}eNzt9_RqIP27uN)GDvT%aR4{LtoYX+&JXg%Kfs< z>_=rMN(zc!drUG580HsW`3x@I@n-Z9#o!kF2d~eq!sGu*bgb-w4V7zVowPU`DE)`U zLD`-{O24L0Bfznyk1hQ=FSFp$I-fOMbV14`4&xy;1zwUXtHeRk2KmVx`nr^3rJTYkHR;1@aNU9xYi|| zGk1!RVdm&=W8m7CLPe_!{8(j2PIASdGCO-`LNLpK>-gT%}OXPA$Gs@j%ZL~Ew;hgH2&@wthQy>kxQwZ^s=@1<;dg2ICmh z{%hdEYFjnLK&1I~>1uS!3zu|e1;SoVYHn|6GI@^&iG?Iq3EnAdG z{9s;FD&Zekd{*9~^R?|L`SPr{Ybkw^a3|7Fi!|<5EoE&1!_3JF+}n(&7}kDJ*rvVo zxhVvf?So@K7l`pA(e~IUEd|G($akWt6z==VHLt+3xGN^q$`+h}4~^n=0OjHqtmiIH z@Sl6tiWsVbYx(IHI?F)dC?W@J0#i3-fTL}l5_U{E2&h-QUvCQcZXbHlKT4!z zk3+N_2c|mgLZzHKUT8Ei*c(W+#oJwH%i?AQwmKj}L{=tGyY^RX!Wl15<(3)%4d0jf$+jS<4Y%X4 zm$%%+lTY4n;EU)4WxOA2`pVG+Kubz*0 zIp~{`^R_YN+{wk{C0wS%DU)gG@;F-DSw74ii<$)hc3GJy);510aAD%^=;Zt|S0hC{nWSRuO(#V2$)m@QGX zZ#ddHr*-~W@g9obbjxA3q|1fDcq%r* zyXgo}fdhm_L@HF8cX*|-D{=pX=Fsu6y+296Rnqu%*Rky#*D2iEk^A-Q_7VvPM{E$J zj%=XQQ(hc<#HBY~y}rPaY)alXR%+^lLOF{jx`~O1jyfLy$wJ7+Q3jW9UwZX%hosG} zLurw=(%Ph9U>DLv*oxs zS}IA*%^tA$gE5TJ&>ze0u})QG;J#Mk;+eKvhav!ZiAPY1tu*SAH-05$f?L_9I$ z!ekQAifQMhrk&D&eNH}_-HbFEsC}~lZ;Y61(T~MQq2^!QU4*1*$1BmvSQtjvheo zeK#PdMDa3Y>5nv*5v$?YUu9hXs7eI5r08kJp&Ooeeca!tFXvmXvR*CUIJJB&l2A;w zECepyYU1U67$f24(NVblz!3n1_|730?wIzUcTiMU;E!8Ss0^%e-)g1Uu4_yR8pMNi zCKvF4Xq%=<%-OpYPE!vXYk@dxwx}WBiAhom|95=cgZ-4iU3TJhc|)O{2fwOsU91}% zhIK$Er#}!4oKSXSvo@7LEiTS!1(!{%c=?uFYeQ&{L8N4P%TC;+qUm<=O53ZW?DJh7 zaw&B(#1xxQ;CF%fwTM$!p4Ozat<`LEIEEeL3?7{fl(W4Y$K83sJy`U0&sS!ZwN|GW7s-bV%<@8p-MR4$25s(zS zbOS^81NMEt66USE8_F#HkqeOpXAY-i_WjPNL(bjDaX^*h__gV(X^k<*iHQufFR1F4 z!h&F4=a)X@0eVzo7zMW{`)3mrlfDgjiVemeP+%bCBdZv$dzaege(DRcPHe+_?vwcx zyL{LnC+*_hM}_>I(F)dV?X}bFC-Y5>rZFrApzo8$s8$44=u+hVkVH$QBih2jrN_jqmB^GP(w_2n45>oOjt8XC!|8FA3D}ceEyJamEqmasKX0>Vejxo zn$@wk=Hbhz4ssWa)XeAJDB}eTB(LtvD+d!g0BYfWqk|Lvm{2kYvX`<^U-A~VC#2K3 z&Lmpqq@EHo7XPkY`@O%I@5~i&WDlL$v-$WFqy;oY$A=ZtzokPY%=SEBIc}u$(&Xur z0<{TCq!6=VoSUbN3gEHhffl~v@EaWgQB6$7v#a1CuQ+*-#r3Arhj-*|l^FY7;A(pN z>Q;oiW7w?RsPSucm}P*ADe|(}!x+_Bq4WM~qR(oHo6;Tf>Ms;5?2kIcNPX4ZqaExq z24PB2Q7-fgptJqbPY0ez=Q?wO?$j28@?79nqe(v+aap?D>&1gyXX4HLYr;Nw%g44z z9diFPCM{8RL0UETJ0l#`s-=p|LdlpTy>H9G)3N5Gs3EYlIS?5in&*wsdLJ;vHWROU zYpJ?gQaZg9iBPTHE|BM{$M-Gg{@u4aw2L<{22}3bwr>2kTG+LmTm%!}{R22Hg2*ZO zMP1LLkX!0oZXhFKUcmBOohs?;qA1Ga<|J*p#!ig%)aTFXDa-!wJR0wBudLAMU=2nI zr6{8P>4y*Up{Ou3^5oFo?4gI*+Nnt3vLLoOJ2J}l;Fe4QnNsa%VRIV#H6Lb)`mdBn zH&9Y;^J|j2Nu^)@qGYNkj;rw$4j(z&tfSX_S+J48w=Yj)jnJ7PXOy*s>i zmRoF$I;{=K0&JsT;LWwV=MIasIk<36)u$J5&JCt{@UdGzR(MnQm>PCqiz?8TMo@eg zgewLIcu>>vpyJt7WS^1b)OOq4A+e*Q9c4OlfHDC1)sm>yjUwn7u&}L~4#;z($WQ(Y zG*e^QU6EnwnNL&geqcx^sR|Ux#c>Bo*jI!>U4o-@w*l8l{lcL7Q@(+i%n$`Z8cK@&D1t_t)&}lqQRYL|m$5tHcVcRSoGbz!Z77?yDhiTszNKafY16 zL5)`T@?o(yw42D(L&%kl^c53+2=*z9e5e^?Gf7NZ$j88H0H_0YM7WCD%8DtJVbaT-^E(>yj zhb1rd(F$d`A=3GWKQVKSE?p;WxXs= z0S8T#dgJhk+|$teiQcXwf zErl)KL51G>a8(55PcQ1qdpQ^Vw%hLlv3q1H-rm~N5^nQV?1RGS_NZ2G@Vrz8K-;93 zmk*f<7mgdF5*B=Gm~AHDt%#1U62ZwgX%1&6>Lo`D;;x0l#+0UnS|vUNO!P#?AL7{| zcc8p8v0xtH3go)GON@6-Ftfpx`=L_e>=6~UumL$)h!H9y@RKZb+aZ~IqmNI$l)3bY z$oPfQVGKr}wP`ravvYdKJ2m6D(NWsqhw?Nizozdyn_0y2gp5v8%&9bs$@YvMd`3R0 zUrNswl*tGWxadw_UFqH2XXr4)G}wDsNa@{$^zKr}YTg^wKbSfwijg2@gQ4y~8u5&U zo`tdj*Wo15+vV>a5buQSYbP5f6EkncD~#s|VayASE)kssj&4JA9>-_-?Wtk>cEmR= z>O0OTdsoR*4J7FEX@a6!9X{_gMTYgxojXxuQo}S)+Qiv)%n%GP6rdc~7+fBq4pzok zxxFM4FlqtUvw5-UQ0#cX#Ln}sJX!G?@TZlObQ`nF_{Z8PE?sLi{+Ry1PJP(LT**_{VAMDWb51c zszIYBdrP{(+rgFJ-6u{=QS*xDX&*6lP7E_lkZVg$empGTd1CmwLy;&ef0nvjyQTiP zbGCw3$#|b>e}7%F!PYP7c9e4#dW`u*z;QgFKWk!JShw$Hkzz-OyIWKAEZV)qqeR-i zkV&vh{2`H>>^td&PDxQ&aZ}*eR-uXZxQeW@vY0{)Nt^bkq-CJ}TXOh@DB5}SWaf%t z!TJ0KVgMo7E$>Q75~|VOX&08}36*5Zu=!-cq*UTS$6kMT3^kI%6^CDav2qw<4H;D!yd|5_Yw6SojbS23K90+CppEtaKkvvUlLlNf=PzlILzxhr<5r}XZ(@YHy!oZ47IrMmX27**1TU~T zjW$$urgf3*XF`ldjrk4V%}6!1zh=*pVbjn?%9x>s7~R!_nn}yhX5RfMPdVsqKDoog!(L@!FL`XK z+Z^VIGVGFvm4rj`!7*lP(c`icO&Jt%ux5m3Yobb$ZbzM4tvWgAmJ5fwFhsLjf`4Hc zm}GQL($D9}%4p?2Y0VjJed?a!wfU}#|eM7O%AH_PQ6afg+pi`v3$^YLHaDG;=} zG{dPg2(mEHPg(OATo75Scz=d>NH~_iU`*v^VSEFNGe*=%Qf*rpSs+`SM}b>0l5r7U zqp;BMw=;u5ZkW9fqhafN6yP)1fBgNN2|Hc|%d3p=oGtXPX>N>7k9M|H zOCYI&&rJ`HQP;e4w^1uPO7uEgC0ILdA-ffek{L{tPw%3_h$45=GSz!$N8ztd{%TSh z{A>Tj&-3hIqGfovA6 z{Cap=!pTj+CvN|@+DNT~GRPbJ88*3A4hs$ptP3RuBIQ{3u`eci%Fc;JvD8G?@8yR2 zMoqDe`v(*~ij3~*!#`+;zwbmbKo8CE8+VNJ<#}G1JwQ89#|?m`a{p-y&e_bmmTLXs z2bmr!YW#6nmXYgLs0XcLGRTn-(_Vx^Y=QZXLLt;#H>ff&7~vsTK7AZL!fTI8?nF#p zFvV6Q!{qqj5n0#-hlo4Fjtur1x%-BO-yqS`*K=x=?hh^K!#yJ2dZ=)uhR%(4*HUHJ zytFzXgIs_byK}=m@#kr;+r5Rm%r-!E+FtF50shMhX7 z9Gr%9wyGl|*9rK7rG%I(j9ayMpYCNDLmS6-rpMMo$>-%A{m}lH9aLKBysPk=Wgo2A}!V24#H6< zSz_5`#0?!!l6A@BZ@F*(7{Ia2IsWG2Mv=Y>VM8(#Z|{_83C|J<-$l~!hLEBD=zswn znED~V)kWoAbU$hM)L&kfb_Y%SwC;ex30-W1R-1Wk5`||cgY{cE+wX17m?If>x&oo? zeQHdXDIp>D2547cTrgZzT?}i;yxH2H8j>1ZaiNenH_hEsYO3Rn{U<5`@_0FGG&XXu zGWzrgmk}^ZJog)3_F|&N*!W+EgPf+6xCmVjLo5cD?7;@6u=gxel3pmFq&oSPKja$X#9a^U`5=7#KpB5A)UsV*If#U= z_7>o3{6J2j5)VL=kq|CCki&d+`ugsTk#3#@74!@pWWwPpr1abL<> z9C8iO--)793J!Ef3kZn2<`neKTO68B=dvxJ%2`0DD|nuJ)uI}FSrw=m6A;$CHt@qi z88>0)*#V;>RH@8`%V~`U-6J`2*@~}s#kOC|`UafAVopX~8VVgyCnWu(Xn4-P*}+y=m;>o5OU2XE|89Vq4wK z^8AK5$DL1jLf~ouojh+Jp5(8W9yurR6_`y9xSP?I$3ODz8_t!#g6u7SC^X>|N&|t* zYWN7Vd+*NoP|=9Ts@f*_vr@LitJoF|eEFse3>?{Ohb|bMc1NFbzpCcemEWT6*!=-O zz#jRj4V~ptoI1LkZx0<$bfFqu&kJsRO{b8T^eoWWwSJA?Jx|>^Le0JW^YQ^R_=;}P zGu44MHKGslI(JP~MZ0_VVnXe#O#P?FDHYN{Ee@)0P9G4w? z^FrW)lQ~^~#ft@2xpL38FYN=3(7Z82y8dqlv+foJ6+&hUO*SnkwE{DQ?DAbm8 zeY!eiYL7TO#Xc~PtL@;tq=GX!*NhwoU36{j0w`N(uYX6?#_>=lf}nF~dFE!DTFDP+ zNqS{cOH*Xki08D7_3+;L#!u3ydNi>oQ z*^iu(YiTIDWwvl2y6%TZBALO(HdPBAG%+MU^m6A`7H7wz?~3=w5#x3-Gzu4}>zwhozdFuT5Uy$g?x5Jvl%kO+8i{wN z`P`d^z`YWS4qqR+05|(_id)XIl!C+-l|J0pEQaM6=e;>%J<9<6zWED!dQ$A!u9o*} zS&BsGv&)C;-;$CwJM6S%#Qn7Q3r%!TI#ie#t+s{1ov0r^Zv2g-j0!JvxgAc;V-%H!*4bo&FXf9Mar|VwL!DQD3^~e}<_p%%4Je$hSH+f%JF-;$Wwb%uC*7y!|$y733c16q!rJ4h~O?-rU`F0{Y19N5*-^ zbn}usa4gBJBbBYX>@bkO5V#)UXmZ+qihiOP>XeTni zEX0I~=t>ljOidhee>^fRsRM>wgqE{9&^D2_O&lYV@6z!CTX0_02SIKzyrBPM z{=wYji~JJ^OSQatGE)x7$gF}9Ckl?tX-H0 zp#y^f!u?F42-!W{r(ne9(z(ESuK&b?Z)yH`^$S#J3b8F7`@Ot2V=kuO{Z1=lp$0fA z0T6T(0dD?6a-*Cff<$84w23UmIBs49LF}0F%w;4w?y}BY;?yZtw<+WI%=u)t?kIUW z)XrOE0$-layD)UW4y*FQ&A#}ZIcyIIU2sA%=5e@}j9(Yjbu`)%-;4$b1G{bIt+y(# zUc{wSA4QLK`v1-qMabbetG&maXy0yT))bl!`l)S5k$MB7S9I7RZ~l%-BYzj7;Fh_{ z^2+xTb1_o0k)V+$G*+Y57PVbKVQTLnekf5oOv9KO;Km9r9u6z|u5JAoC5YJ z@WMiE!^fc$u@}``F+A-ILaB8I83laB28=MdJ?y*n3o*?0qP-}NdDfxag6?cIV*i#m zUlfbl7X)nz=Sk1E5})5U)q&-+8Hl@Z(?VZQ!c$+5LpmPRlB}R$UBy%gI4Ux)qMvbi z_0jM6!RTzEcXOxWf@0@r^TNG+KCe`e))gLr<0f;HrNCeuM?L{mFA)l)vG9ETXmSXe6#SXZD zH==_q2KJ^@aXjCBhl_J7tRWaE-1#KU8QFXj_c2Pdk}O|l&KoZN0`&cvxT z2A8LGuJ4(8NC|LIIqZ4mb7r|7iBy<;jdCPf{0n-ddCQy6UBvPNMuk9wxe(rpNQy-U z-}&&qVD^+vSK>lxrckO2rM|HKKJkfB_~|P|7A)-9x#qM2D4|1f!0&)cvmczTNHb>8 zLaLYFY=!ko^^`iLC7wW~1*YC;K~JZ%&dzb+ev54X=6!PT;HrVMxgL``0)}-KT&^V6 zWK+!ML>3%(76KnQfS|kIrltbhWMAwgv|r9|h;Ha^uwxT2D9@=?%A~@Kd%MldP0hK{ zxL{gg-#J+q!B>fwa;%~jZOq`AuUUNzU4ICb;u^kDFhD*FCFfJO?BhP zfM5zo72Aw-)Dp?%nW#|Ec>**7^-k#tzS{s8dW%e|%o-V%eg)F*MSNym7;`tQd9ID3 zPB5!0YbyeNhtO-3#;M)tX3viqUyonqVBy9DV6CppWQ==mOLs9w^3*ta<)cf|io{;iejUmUvoR=4bSE2aLCzC7XXg~!VS#f> zo;_fq_x&?4mZ66%rO4mt0HqwX`IVhqmf zSEsQq=TV3e1PYA1q#Gc3nnqJJj}v&o!2yQ=iC57UHs}4vKv4f{Ggm)BY^LPrFJxea ztqzpSj~*WARI8Vfv-EK%F)#P9U~8v+r&m4}tSY@=mTkiNYW7#bQc+t$Qho5`kA7#A1#tCf^z0pj1N=e-r5*0YnLhn>a{L2x z<+VbH1ez)UJC7*%s!UsO7^N{XhBX~=ZXXpi%qIsyh0EF4U3Whrx4lSp*S`cn$m0t} zSq13-N7;9VHMwnDOAsSO2t@$}5&;#JCMq2gs;H=71*OYYkRn}b2t}HR*ik{M2-pDW zH9?w6mtI3B)C36x5|Vr??7h$V&U5d%_uSuNd)HdCj5)@b9es)LHJg4-D$?Dm^umJl z{&Dv7rf01265e&ze6#BwuK+^nOHU*3kI8f0u6yy*+csnh8Q~d zHzkNjjl5%p*Z6%;?(yjHNGivg@iz~D;e+JE?S!Im7c@G^=y1J>J=d(KhYB9Hh?-6#cs(hu?I5L~)!qbiN(+XH(NYOIt_( zBrxo6(~GI!tgY5ibftp8CH3j`EL4}xjwitawy6L^U^^En<^YDGJt&>&x|Ool8$P*#bRC*SDL1{RhIQ@cz@_Go03?3_Uu`1 zU^QL)95J2*vx4gO>S2)Z5eD=2OskCre%zKCB=@!2by;XX)7f^moLM~>-C9$@O6@P3 zs3f7Qdg8TMzV0!8-o3NAZ@)ZC4UsK=!ju!Zj{i2Ga--|iyW97D`?2#CZx-of#>emq zo6RKk<>G|b#%P-L!e`Hh#U)q1p^4CE%!TNDdU3*=3D)M0K#X?;plj_1^jn|xe6_nb zeFNpz&w`N_pvzI}QX&dN>M26!lG-8Hh%$LZh5uF$LL6g!NbBD`9G=#lGeMT@}rFBM;K+o`Sa_l(ZMNXdxWI` zyBLpx0-V1CS*K4}l{PP}Pz{iNB%!osb7^xuuq7s*@loCLhnYKHgHfp*>_SD)YI9AL z)=0aFvwI#rZKXNijAe+8U{?DPHFi4XRK(7t1u`G^3v6U2X@Ur$vz7Z(3v{Ys1t)26bp1}$(xh}(bDP>M#u+VNk)MP@l zaHli5PSeI&A$Wq2Rz!(s3VFf{tm*Su{ zN7!%L$lYYuLPZa6^V%@NmMT(eIoMmOz~uDUS#_M4Wxq*X2zbf6hWdTMYk|{EKi*|D zJkEU${I(fD&Ap3-_C&R3c>49dDb6=-(T6h)DUqcU6bF6=1rDu9VW|ypWLGr*-iE=7 z2gd|T#7R1YRh#5rwZZG1vG8Txad`C>NtdWh1^~5sqcvS*`WHMUe&lv*>1_Wr9)soa zf!isj3{!Fa;@ElPh1Cv5LnLb7u?3+*+{^Qg4+Plj(D253|0GEM3s`xf_Sf|T8*G_$ zOv4|4+3ytam4xhh&dWDv8@8|NI%QPe85f_tIH>xyF#a^M{0`}PpmrX^WNDPMb;&u! z?Gg4taLm&9;{I}B^f=&4PNz9by5A8V$U>H?%bk6cO%#d8MNP*vdwT(U2<)U z<&G@Hv6=Su*A9e!@lt|4dup;E;oouDqiX5s5x%r}eD>!h-212{$$}6=n3>GT<|VIL zT$M;*Q<7)*$3%Vj1{c-N?D2$~e@@w?Ywjtx7bosw-MmIC5RVW@ijc1nbNbHhLDn9X z`(wQ!OXy)Qv5uNU1tAfJ752G7kIEM80oU`xY(83mViklAHAt$fLai%B(F%c5PLtNi?b(qF(l$KsIMPLq=oAe3aDHqmOYi6qh~1V|x~9^I3) zk!N%3`T_0bFTJmGtCvZi&{i_K@_@16Vh|n@Eu+oPy$`8pecyM|8n1B{E%5NE#qGfc z$?8M-74x*<1xf{hus|;+H9YaGrqIz5!h9e%b_~M{ji01OYj34(nXt{B12PWmJg;Zg zqsK>g^ARcVA|dOo)n!Il!d}Z_%zfI>ZZSYrxhF6WWr_uCEMR8!yeRc1Uwfl@JFFD{ z)(5otrR)ivgdB+(>y423w^ow{XZ~fE16Xn(VG(Fj59twnuqf0;Y)OwADP+^?0NcJX z*i@Hr%KJj#dhYARdBQGf*M3F<_ITh-awqGq=r`P6iW@%zHP*P(lKTeKN@(%&fy1TF z^+K5KbRpxqB>-?n;mGQm7uxfcmOgRSsxw97tQNCUz8Aa-=cm6*1XZ01@Y=fC_ z>_C*|a+lFwx>>(F!Q-P@>fB^V%i2NVyhqiDj9}h4Ye9>nL41?BzW`m7UznD$)dB3@Sz&#=>2Ygq3x) z7-iHFU%r06Fk~tzCsV;HG0qWOxw`N?g0J60NyTqv=YMy+898Z{&S;huS)U z~4-o+qr`y>rDquy_s*Xadmj(bWar4kT zL`TI4o;cjAO-Nc-r5YDf@fVPivJF@w5Nkte(bj9 zrUw`s$hHW&#rFYWbaarY{uez+o*xag&5 z`*z)|d`%>MMzkj@0O6@NkAfekFX6ox*B|@Ur=U1`&ntI4#P7*C1n5CeEb&s=Bwcmb zuI98T=GXr6*7Nyh4L<-+HfVLvQg{{XhEOb`n=_oFeChHRPi3z;d)BtVjRp4Dy>D>V zM%o|%aBuv6vB`e&>Ab};5N+uD&MK)*Q8vUbtlXXjy|;jc@30SondgF6Xgo&xvK=s} zA8cKg+PjpxT7A0dyrk-YQ4{D3aGdk{#fa`KwfjzjR}yGzG4;CTZfi1muY~ieH%|gy z?m%ET-;D)HDO19!fUUoD@pzsAyR};4`czBui(nw-`b;HwQ>z=heUg6nIgD1-A0fW* z*pyIIO-N$xqnN@>;KQK3tyAbU`qgZXIJpSj`?0N{; zzQJ_H^Zlm~pb=O(yQ3Q5@z49PX4gf(zd!EBid2RBdDDsgfHD0(afB#OTi+I3mH3K* z-i-Cj$gvfGEwI%#o84{mZuR8zQmd-Ez2P7A-~t?C>0-0e+8r|45^Ig#Q2~}aKs_|0 z;V?WSd7JRoPgoFmPb>|S*WctwsoNK@|Lf0?!C7VNq+vyQMDLoZYnav-S|6*1dD%RQ zHKX{Z)~3qQ6$8nX&$6B`pKdo{A_BB5FTl*BuOy=OOUmiqnk~=oM^Pa!S+gHKh9xPN z{p!*RzBpZAyw1vKtleW0xcf%=U9Dx&FxjQt*I!b+IOfR~HX~RygV>7(BBn529iKt; zY65dt;1s9;nwk%i)S%iQrXG|P+vkOU6)wL!{sFxjXviAh%RqdR#?lLy-YnON{zdQR zrUa?(nD%)fJe2>ZY$iH_S1aExg%;gcbGy~APpD5gPr-ks>GTiU#8FF-lunzR3<(o6 z1Y?(^oqheNfe&mfcwS0-S!brOg2g`w=`kz}i5Tzu4v_}+Psx*HiVn~S#S+6JwtoJG z)3S!k4b@43H4|qC7o^TemFS zSYWQqH|MWd#{^`@U_Qk10^%6=KI!uP+q`a0d+Tpv9R6b%lD&0@!=?&m&e4CRoa!nZ zz^Fhq+3vXSDw3kw$l3G#p>L^sQ{f;P) zp~d!Bq(R)zwNV?L+@z+Ihg@=&2LM4-N#*|0r-~|rGazd-f1*z$U`DklrpS1HKx!8l zYj(r(4~|QC{1zge5MhxV#s+$OWxj{b37Ikavj%Oln`_>4eflCkS3b}OCMRnOW#5CQ z;;}z^3}&yED}6X`sy1H}e?n8WS~*}U$#1rxIe)2EOQ|xzGXHZd??u3#0V*5Fl~b|G zYhok67KTzB#>T%Bw76gqNd5C>O+~3BIrnY>kKwwjOH|QusfyuA7I>(bHbI0koRi zTcnNPK6bs0`+y8BO^^rjA=U~=6{>M&NicJp4bIK7@O@Vj$QZHNGw}+7U2t9NyxNRd z;%Pt>-JJy2mdJK=MwB-(npHY+JJzSNd|~r`f)tv06n3~&nB}^@Oi-|JVN#%gJjvj8 zn$Q%ym7}5p{`_7gfM`G^Cc>5X27+)ATp<0wnDqg4=>OY~QLc*tB$e2RB}^ihn`DQS zlpK|j=%&?Sq*KQVZDfU^{d%spFr!ex#j)mF*p4m`%z*^C)6JTcTFi_ETQ<*y^aLA43bA=w0r$+Dw_%{ zp=9GfV3H9&;Ky%j9_l2NcPy*JrV}s4BhqNY((~%aRSI-ljhSM?#6h~5)^`+`aiRR& z3&6B5nE{V7>20MUhFA>wS8p}HPd;Z5rYY5Um7B+5XezL!x&pl|a4IRN7S_^1t=@Vz9p?+eE57NQwII(+A_5iCQQhB zgCJx&qvuD=3*|SbhRw7X9cyp$$6}(Dw%08xq|c@@_J?{9S4Y2q{#bNv7|RJ#P5N-7 z5{R4kaaCyn$fSNcL7TH^Sx27kQ}b=l4RJ#Q zG7yk8*`4|NQ0MG7g2O#Fj`d#_E#3Uvf%Q5YG;#4B@yp}od7&Z*)SF!eAw>)IJ4Q-Y z*Gt_0tYaNSXXDhstIx*{SS^RHD-hgHZoc?dMXl@*$JWNu2Myi$^xcHhMkc<5-K5sl z*d1uE7mB^tTb18m-r&=+)@xt$V0`r_HvQf6Wr*UmWwwWVClYNu2=vjQ0G;LMjUr9fl*=2r|4S-vqm}XqmXZJ>z|plVCN9VSVI5 z)-W%2?YO)xW_6Y_!-vR@?Qww$pE)dOg|7y1X}WHq@IaA`ML=pBE`&=YAoWlv;;LR- z_aQ!Qe%6VHW4_%*k)Rl_r0iV^><&IpeuvUB#$-;CeZmQSSrY`!8cZ;8=C#w7wxm7MB6Y8up5q;7H9ZlOj5ge!F9>bjDhCkTUm=E_6PUW^lky5X&u5F+{D7rgwSqbyD!?5nz zmM}rUN(b7;y-qjWMf#J@;zH~MZ#>=PNgziPbI%#SLH0?Ldl8Vc!lbjol*gcsMW(}w z6?A=azzg;q1I*7DcXtX)Q~3%8kHHD^S`vEvNILrK1-{Bu>w{Hb2Jt@F8U$r z!*pRv@FAvsAkV4kmXQR7lTkLC2J}^Ynz4@t9*;e8c{3FRgkdQ zjsh+`-Z(99!e8bA#N1s2(0jz9hqf0o0-N(f3}Yn{aI2)W3%yWm!05jA^3%NT^64aR zokqP->xn)Mqcko+Nezc1qV-LC^onxawE0b{2H{^7Y&X!<#b*d(0h2jz+u?#J%bkHK zE`Zfay6(&o>m-ovbb;!9n;$itb=m(2M+Ayuchm=sBHDJE_A<-%&a^MHmrSBS&p zq%^TSuOEK+6U-d2ro8I8NF=8~Xknk$mBiGSwB2nq<950(SO#w$!RKh{*DgtGSloZ)UM+GsyR{4G?%yY4FLejMKYtYvD@=HZjt~ z5rIGRPn30SU!DnO^STLos)1~VbIhOMVfkdrAntL5(7LucRovyDiICCgYnL$sug(nc ztSBL{8)A*Aavwl=ra9jSHClq!M8}oAxe*bZ0}A73By2ggjm#DQ>MWNM0FteXfuFs2 zRm=oC)GM6VbK=gWeE^ycI&$m(08O9S{x&>Z7jRar5i4MiongS;0w_N@6yTkufNyAKxYw&zQGLceC8mzPSO!~@nVHe#8Fu9)DnD!}g4 z%O~@qq<5Q?{V1_KB9a#*9tM8$^GB1a6Nn8>HUe#!yXywNeB3P<_i_GchO_Pw7~`Gv zstLm`x&GaAu1}r8vt>jp^}bR7Dehux&oo|!;oP8n|2GHo#I57U#(x1_{@(bkDg{>RE**eWg^GujJO}KGmvqXzD4{G>+`U~b=_IL8 z1)87!!-!9B2S*YYZAKEm{g`1!sxMEm3aNe7p>=(t#OvQT@Kigc(U-p8Mk*bFZO8I6 zM2spiJMQTf5y_orGf-86mRis5aKtis2B+L^SmV=Ar#kDZheXI0UP&BJ6C=!>%@6V6 zmbXnqT%|nG=?(J~=9wIum_`5oC>MK%Vx~3>5q28aHANm2@5+L%WeVr*oD0krK04q> zQ?&UZl9#8WtBqxB&2DxRll|SHbPQXNfa`P2C8hi<4e?objm@23Z`-pycF=-Lr3WgO zPSR`x9ZO$;=|I>oq9&9L5Hc|7QE`61<~bz_`@m0u4?(mp0)3oct;HJ*nq1IV;raM$ zh~auZmn`Sh&UXjO6*G4Ir!rSINC8~zYR-IDu$>Bw86}tsQr9@L zWXYSSE#i+VNWI}Wd?!suVG<8Me|_RCb=q{;6jKuZRG6p25xD4a=4!#r*YxGqxV;l} z&cL`lC83}iFz(GH`G=wQr(Cn+fF;~cEAPTxQnVd=17iAL3UID+2gH!>pR-LH;~=F) zPEE=UKy3{Wss7@Ijaa>x>!8)1)vF4TO7`<}ObH3&It4!5cbM2I;_BTYNDLnGa&m#@L zyf&#dJmG=(f8-V@_SW?8WxmfIv#qTJIY`o5E)TmUFaSW6DvG8Uq=4I;d^bf3Ann74 zQOzT}VAp+X^n`1n13o~9V?X^pHu-^SRBXwEE~;M%!HB%utBPDJej;8(q}naJF)Q(c8G5vmi|-&Dr$QGnZy>s%tiG@{xqoJL%)O!pMWpz|5KYU zqe0PVxD9xbF9Wq}k?~_bf_LE^5eP>-FRweMbn%k+mwTa;XF(y^e*%>BHTYqf!A5rF zl)=%3gz&;XxujGl@9cpm#*M?w!xh+{R(1^X5`(eDt}CZ)F$o%6YM}sf^LzdMKl48P zr(^js*a(*#N>PA}^5musEA!D}WVeTul!jM_kAz&>`o;;=VR48RWm!jyG3^Pv4!RUR zG^xbSjl9ES{a#1t@7t_y+{nUa7m%qc%8TSFV0fHBsuY=xxs5MHLFka4-%zeOUc7?3 zZ9!2E_SSWLIfZ&r?1@u(vh%djq6l4oz4v?@f0A!;kLoJV8Y%c|- zsH3@U1d16L5k&zPI0G7#_gUxc;kalc5%_R~KL?E9s}Bcsq+?;gB1n5u?=hSF5o1dd zQV}x*LK7WR*KvGz?yWTz(JfIHb_YNs8w${*vQwqGFSq@d%l!8SD~o@n3jMtt(U2@> zyBrwPUX`$bVJM8nj{z?#1CerVgI)LO6mR-&Zl|32Qvw1Pa2u}8p~wKakspX}JCPvS z%WbcU5%(g8qMd=aph^HlT=V8V)=I%`(o@C3EC(tq<%ETURm%cyhBM#nQ6!ff{glL8 zJJ-3icM;-w{TNR$WY7Sx7C6)oMpHjHI3%<A?w)flZtBpFGr4h1o8CAa(ZM9*5q_v=(_dW(g0o^E#$?qxoJ&HzIHOQlK zC&q!8-SseaV0*V#c+UGBjvP(FR_;H5L=6d?o@|r~qlbf)Ed3#+xlgzKhlWI}7qbhW zq`J93j`#}PzZCD0nI5C_8h)CwJ$CWF>mKMHt_8aE7ZX$C;VFzNeCwjH<%sI!@c3H{ zynmV1(6U)!GqbbpB%L{UqGQ6I1muY^UN){5EXmeh>mNnz{@_sP&|BUm*C!EltP}E} z>QtrDDI_R4w`SGEa0qzkn2bj#NU|f~Asvn#SU;N)&MSa?Tq>N`S_|p1a8%+%LQS7b z?P)B&pBvk<1|K56oCeGmpG`$ybFekeSgo zwp@mlCO>;r4xKu7ysrl!sAO7Lq?pA z56fdg4A{O3f9rGAYt96{S9X^cI>u+S3}?sYztdD>tn&82N2Zt98mC7H_|_o#$Dnl_ zVs2dzA6)wFRJ)H5MaaPnn6KSGXewN^eDxH_5vcu)qa?^nA3Up_uUjU_%B2{ac^w}B zu+|Qf;C4`60@eAB(GRY!F4619u)|`B3^-jGPD?;y9$vA=4={!rmn*kb@tW4~ zAJ}K_u7HsjSs3FPW-b}Eo{&+XK^|48|2?+>CJmX#O?7>=^5TnuEmJtJ&1C~EAYm?( z$TkP}+%Zwk_KBh{H5!Q6CUxK-Ujq^30RXz{r{0uA&OMV2^D>C4HUpJ;%FTC4&sgl& z$_+gBs`ly?aertj0^TB>$YX&vG_3%HnLT%xMa^9Qp>N|u%s1sh!7QS`cS}?3gT7h; zgF&NCU3&lXqzVpS<$u~o{)f;BzqBjI$7B(tdofH-ntV<8b4H-s?d#}jJno2(ywsWq zCZL?b#uh;LT8nVIch^(p(Q~g&E)&4Kqn@F-=VUZ#P3fW=*LtBQl{0bEPra-|M)LSH z`n0?;+pVZCZgI>Xmjgi6hv<;3dARQNt_Cw<376>7;8rd;LOa~~y6(Zs&k!|0jGPO6VgI&-yhG6LexT-jU->ZR zA=}@PN?`G3#4a!U*IKdOKTl02WEIa#mAimP26}xQqpBs86s&ftschj)yg}^z&Prpg z#bvCETJRHP#+Q$=$ux8CY`%R%X2v3s?W-NiDIT$duQ=g1Z|JdG+> ztZtr^{4#Mhbq!FMyPV=A&~*u!xQGN+bk!hnWC#Bo-Yb%a0+uM7bLbKf$@KU?#PNEx zCDGoZor>MOOloVE6SOPn+{d~a>~}4x-LO;*n}i6ja$6$q@y~`bkNB46vPFN#%v%$Z ze=)A>wS7qibWYR8U#+)~`2&;y5mnM1s59K?!e{89SWI zE({U)JmxbuSU|jqg+7uCzhm4kb}62;q2!cfOy3#KKHj#! z4p>&~0<9kAm3Mv*Kn+$+dknqvx^3{^__+dE;8=-BXz7AUiq5R7l}N`foil+gle){4 zMpU=UTYn$QPtg5cL}to;k&=S++RRu~gha@6OJ*yq!bgbLZnGFzg!GhnP1JZ3dPRw1 z%ENWBHS8|@rC|GoKim;xE9*lbE^k{Mmf}k8lW8|(7XWN0#Fw1Is_>yC#_MpdPhz8V##t~Ezx5WrCcw>e z_m4fEp@QzQn`>>i0`@<3e#Z5wVyJCl=+Vqb&@3Dg0Vb2EF=yYJ@y1KbSw0-}C56Fg zKE~crHX2ZnTG9nr>2c>?KjRqo{WnC-e=G&!JGk#z4DFw5Icl8li{uSxOxeA4?F^^J z8JCtkiB>SD0qFqh7%6zv(jGh&21d@F*h1vT%{1mLmPdaXml}_0e2w0O$zdbhsRhUr z2{`f*#?{WmGhuTA09azP8R)&x60%*GXtlb8UhgT>jT5*THW!5yQRN9n{T%~)4Dath z97h|59;{UD))~U7;I(NDnq5e18?Z9~XSJqg#l+;=2bdD|=pzP{3 z!2qTH0GVPcf2D94zt`b4XZ!}|!VUt)A&&bZT^mX!2UDudb@r_h?e8-vwr2;K>x)5zypN9ayqF?&!(edh4@isui{%gdI&041jjR8A2loHyp0BEr zhT0ky@$y}B@aHg&y1cK-i!fV5Z$MCm1v|K4af^X{xpIoOyc^?yCVK*T0=OymCatqA zhINll`UHM4G>B^8-_h}^^Db7gAZ1qO**!E9<+Wj)95*u8jjU8ceMe6&6JOW^S=|bR ze#(1T>~guvH>(^a2kklEmNn(vC~Nh7B6-;+l%m-fwr$4)#0MkOdDgy(7k7Yju%28O zvxVfl3gD$@00bE3q&e$lX^-P3e0;4QKG#S4`)$NxtbjofQTGh&>U$Juz--g(cXmse z?xBid-vfwQ!DAt5=1mjI5A`8kMVZ(U?oNko_NF@&YBnI(vMyTBW%xYV2PPFt4-ise zB%wl^FdTL}WBW6L(f@av&H1&{ztA-Qd9-Ee*C)w-{9SMk3GsKQuk99jRH)_&YE}*A zz|5NrB*UT0z$#$Bh77dKRxDpML@@aeLje=5Pck%l0H7r=pI9d;H=;+wdVf7h*aRXk zw4y;(Jth^^5s}<+_c;olTt}u_mPO0>Y~y-3@F#8lnz^d=&S8hYeSrU0@2kJNuoiPU z+u7d_I_Ya~50b4txVH`z45FD#WPe_W^5j#+#HzO7W(CJZ%yPl>`V87ti_-AiUH)co zn#bHgX#~8qq$;bQHH>~FfFOzl*(|?LNGk~tQ3G)?eAA3IcV?|68|!tJAz5kw`_h%< zL4v-)oP(XNo8*$yn&td?gt`v)B=$kBm1G~UyJ`UXpA>%}oWhV2o2OXoQWmc*BU9fg ztE~ni@7%lVFGKO`!w?c%_FNH~+El(w(D&gk51uPfu&r90Di-d}1SB7T&C{c*7prd* zhLIrxJIFQbe zDSj#a4D!bGccYOw?Qj0=Hey4*@z7VFoynV%FJ|gKvYLDLS)W+dF1-S?LbHbr0$1Qy zUA5MI2)1kJZJOwkH^;2l#625xe0!M97C_+jYMGHR^GO}*eEVW`)56dyQ*_FX z%5YPWamPKf304v(>-Ym!F7Y*{7Z<@6|1Wl020T$`QrqeT)N0sQ* zE}<`LQv-;w@8q#9;7tBbP2rOFh}94NuPB)*@5*)j9Lb(=9X2AXFgBvWW}D=PtmfIM zSoC@!v#0BTE7h?CyNfwk>6omjFR`{3cnA;%h@g?H;&zOWZcNkZPadc6oY}>71@}FF- zW)9t^pK_RpsgDqt08J>Y9KegU)lm&k_a=o~^h_)v0E~8IAn-wS&zi}`TGZ=B3qq&+ zSL(q{c)YQ~*A5%xuq-_up5&ptAm3LHY2}tf_@(yHH?nVXz)WZ{Z!y$~Wnzu<6XB~= z6qZBFlJPmWLSbpoQfK!t#JFt+efLq-!d{{p_$|ElAg6`RbujeuV)dcH3%>1k>(2Kj z0?i9GmzR3q$d0gjs)I%3CihH?0{E>s-?3;ljv#3H35b;Kwo0)+r2rmX>gDbKe!2SJ z%a_0c72(tKC++04ra zl*^8mtbJpG=2~hSG1S*pF!WsnV#FhPxFFK zRmdQS3#>@7Hw<_%E4Rywq`^h1AxRBG9PPqPZcB!n{&tC0jK9k|Y_9Y@5b zfC}+XNg<`TxNh%HaP%)&T+ryLVZ>93Mp%HJ{Z4)+LW6o}L3F@^-PG>L4bu z%5aqUU;(#%!&M%O$GtIGjK17Ai?6LLqBFVVDv!Kp^3yZ~cN01FfRf~c$&+Bne4Emx zRuFgx+&8b?3PJy=24^AA7q_io!c1pI;7*3r54bj+l%INu2QnhVb6C@|gA03fx1>DR}|)wlNH z0FBi#(AR!2=}7~c$qtnTUNG|_n%g=6Pf0war>zOh%IcGAuL9~eI!nMHn8$b@E~F>| zrBr=wPUs#|Y^<>~HBbgpkfncM7BWUz$oscT#{LH0{zM2AdWf@@_^)4!e?Uk=fkPIj zw(f_F#%_44J4bA=KYhwA=*Y%3-4BTujDvqmAC#c^m**5o=O>VqCvD~AqYv3?& z?g1L~fm^2CJ%+LLyUmh0Dc=P3TsWM+iI;B;Sl4pc1J-uuy@~zURp-|XI7{MTu^n|d zSGln*@L<4X#18x?(ldu{u1^Z>+EQFwh5u9`k|95V^}XZ@j46o5DI8{$F@1cqMkh-B z*~H#+u`a~a7Bn4{Ycdb`oIFm(?SrFwM{yY)&!)@=A zcmKokNcR4;f89(j6;Ljm_vw@C13bMJQMm;(uqWQqtw5-VvP1%xhu8MuxCVW zvu<5cp!mM<6XL$vqXJ}u6Cqy`QbStA)485I!OR`Eas5s`jl6ULtCz?nH`jTy0o=#m z>dTQhl_&yTIB0md=$En(G0jS+H?AqO=M5-%f)QGnb(nw%yKd3WGuttOp*601{*RyY(A+HWe*Ic($3fziU-~R1_01FM_Psq# z^;uLfg}VSeA?8zuH?_dJz>R-+&=4$_1 zs32;Q$@R&#Fx$wm2Ek{FOvJciz`@Kgdn1LTRT-2$s~#~gmwk+ZKvO*Y=`p-L|BnfZ z(PlBdO6BJnEBtS|>?bk8w!4?%OG(2^Ui-xOkUt`i(&@Iaz11Ye1MDRuaP=(wIESbK5Tf zz#tHt8?Zj@r{*5wZ+B3*mzjTgC8IX}ZcAfn->>7|JLy<{PaE~;LXQ3xBv3z7RbvN zMR7a*Y#cq@g#Na&S4&{41T+Yi%m*{0acO~xj?z9lB65_9S39v7XlvyIb#k_hihuYy zbc-kDVB5(k1XIj#H7fS>b}PDQ`8Aida&<5n#U2%QNjwLJPoJYFIBw`o6kz58}8+c+0yl-}aA zo;fv*=OnKxu2(-h1Xz8I5F*Ooy%$)-m=X*tU*-$CAp6pKS99PSQ-^Q|QiSt{q`2>C zq}qqRT`W+vWdxRhW`x<7-`*RYs-w&uz$;jK^^Y(N7>zDfG+ziOEE|Avkju3+v8_XCR}Xxe;fT; zJ!8G?%(?0Ki!<@Bv!69@pYc4EbfVEBy~9!A!I`e@q?_p;+Y~t-*=|4iBvxQMqTQkG zMz>uF@QD1W>~Qf;FBw6pN(_cxCU zJqU^wp6NX7s&dq(`wz6v$qUl72SRh4+1{srsj5forriKLLgh4lEmKptIVsXo5;>WX zbbDh>pPPgFU|zQDG!~Hjn_~4sEiq5~!zB-W&GpDxK>%Q za}IRzHrjg$%3jEZr~Z;WLzvvpwmWvs!UrI&gB0leLh*2VZ~X za5)!pd3nwkyI3TNUiqH8saZQE;E#QRI%=zw6uQzbOuNhViJY+8Y2y#w?-CZ^!~nG{ zp2s}t>B=Rc0cA;FmmK@+c-pX=xfUnwwC;6%Yo=S%hY%kE73Z=Z62I9u#$kdv^mZd( z&U$$`0#|rB>QUOQp0=cv6SYRw4$M5Ta)m0?Ig^Ago#IqN7^$h&D&&9H8~))SjwMT@ z{_q=hsB$cwIb)I~Aa_9|&(VH;NH9PvbotIjV_s_G%X=F4-=e;q$P7Nb8^bgvI%(hJ zh>d9ru@e27F72-!$_!Km;|`W)>RZjauI4-__U%P|`Nd6Ld2}vRdlSwJ-(10FIB^si za?Dq!&}1Y56n6Tr zzJgZPt5?DfwI0%Ky{PnJZ+~k)tjY1Yida!@K&bXMo}&gf9(tHiX<25b5Jl}g&nu__!hfM(~u*>S9} zD=lsmMe4G`Qya)t9m1{pDFg6;4#Kew*qUUT->PqD?L=>xr{Jo8(IQ+oevXWLsV!a=$ zE@7T3JyES#-I(|iUgtL%c#wEMMcvi=0g%w3Bq~V{qCx93Rdf0 zI%2DiS$wXw(B+TEQdWa#-|+qsjTbp$Eu0)PY7InB+)pvSpZqJRm6v+wKw`eIGS)CT}qhm;|AUqPwB!`@0J(xgT zQ=R?+B~GhL0*lda`dI$3$LVTjNoklI;3x10r{aN6`Br+#Ua{_>KNwhzI6K z`knVS@qu-aYje#0h)?HW<~S1&r8M)_!##hUTI@3s5GU&J78R~Qi;`l+z1BKWZfhcnadMT2Ot^~w)OYsGZd%-=cu6#h|5rF6T5$F9Z5 zn1VBpy{??Q#im7lJH9VdUT^Btn+490FrE&xBvYhLrvB6FAG+e=t1d8h48H?mZ4=## zJ@D@3zNL8i#V&5w^;Dz*DGRr~9UiyoB&YDl@JbZYBx7~d#;-NvBLO?CxQohY)%+&3 z3w@yBrG|GBp`n57_rTBX9I^AvdQ+V@vpwFwhYX@t#P#}j2Ppaq6}WjBn+r1Br=Jy| z7lhClztNNc0bZgxc$D1kDPwfI2I(U2)?7STa_CWU-^**HEb|edEmR{})#{2R&3={N z+)QH1X4SxN7-<1B8T39=O%;yTM)wO#ohn+B>Xd}Q@EI&C(FR?~uwZG<#c3@_Cm+`$ z7FdIi_zIp;5VV9{nU5r7&Cr&5aRCauertP>-e+sxnQJM-rl!;OBv zT%;Vy9DT`G7^jIfZ8D92B<<166D<6sFV3Dash9F7jhBGV$(-o>#T;$addYLbMr&xr z4rLfgCy+mkE%D|efDk9X^E!>$AMW2Sgc<<5=pP*SZcP!3O!S!K(T?j6#Wj=~x9}(T zcFin9mt|v825auhFhi0)CV zjbuL03*0n}x5@XW9-=i-6qywAl1-r1dRlz3&bp*hG0@bWY>=Znk83w?1uI(Y8Yg

s`m^FM2DLO}wcF+p+!#8Tj^FXP;`u+9B=3am-Mfb_&p$eoNRPOrK z_!ZnRj^LUJ!D^Gp#P!NqQR`5jiyE2HAXtc;8oBdOF59GqFM|_2IeDymRH`+x6GRV( zY}FwCJq0BH@e_JuEt`tpuk=kj1hW#YHHR2;9~(;U{L*fJJ5)>&33k#;87sJj7HpIo zBS;93ftAiHW6bkSn%KH~N+eG7QrX0b$u`W76rnNyDwWFPj|tWr)g_lDBT=vXNXtoM zL%RaCUaK^q&)&E-e6z_ad;Pe%~jmOEr!9&jq?|mWP zV`P`@#f!P%CH+y$!9(+Ri^T4JI5Tf{aHzPlk=b{=O9DYN%1&L4LI;(CB@(q$aP0>V zj>V_Tde5ZcSmA0SIg#`ceeF3G&3P7DPi^nZg;QBkHpmfr%7|{pZu@|sq5DX_te20v zekm<<%?LQXUOw%*a4zrvv3BM0PVho81FIJ(OdK!b0_gj2|4Jg z2eE^+OxA+%+|9&#h0!QEAI~yX#dC#nGsJzRwPT{L({jC)%5?s|1t(^lu98=wLD4lf za8ubu4&*LLim}2R4^ycf^3yxLtP7E-V{DUs-DwY!Wb%h9#w-=UpF~-!e-H=%OMK{_ zYjM!NhUhP6Q|K>Lwm5#FOI9}ayX1O*g!l(#<#eH{Vq_QvTHYMZ39k-jK+hUgRL3Dp zrl~!g9#qJAmwMLw!ob{}dCQsCr%OVL9(3(6OV_hMn@$rvuOVmK6;EZEJwq8k!`{V` z0!@juter=rNW7iF8PFIak)LI)>~Z8LDHv|&DWQseRc{dUpi->dpAl) zINz1&RPZI&wu+0=ipsAVXk@)d>4(z|YkGV>&05-LJXQ0Sp=}#A47(Q6X<b&Pa*6< z+TZ9x!B&MfNXfB&_>uyicC}UPl3j*)LAV%LyO=(&id{?;oDhFfZy9Zn4Exr9ZGPZB zDEEUVYUyfC0gLHO{gA3R%jEZgp~k3mRMDRMx-R`bb50lDEa>0wLP-}73?W;+d_$6V z_PswM%$3oTG?%ZFqM!r{XUrbJ-bde5z1YR8dDFBS1QE!0ViOV+e0pLx$v&50%Z6VZ zpT>itrP$+hy$@A6y-wta1*G=T-eBbF&fQ>-#cMXz$T&`8IlXl^I_@36r! zG4z9*GR5Mst1mH6tM2-3*IY=f>=sERZ25%n8KNT62N??;Y;?ITO>IoIoJEr-qF=%^ zkJ}}TdG0*Y72k%6MFti$hGZFLZ z#BLX>?eyGGnaI&w)W7GM`5 zpbqItPsN%aC~)XD?6tl}{j9lxsWUOZQQBKWSbR#=lUiKDUrMX8^{RJ5vwR@Z1%hxKg0C&g!OW;;p;Er!f0++`qHC`N3kdQPuIB zIxXWJGu7a|PkvV$`cx-0@y@&7InLNd&M`DqDf!cdv73r^eh2JlUkeZY$brX4pR$({ zl_2cbO;G@)aCK+*r}S8N8yTvqbLreqzO#m=w~b0TCN!jPlhFhzVyf`LS4H)<(F5^> zu{ky-0j4h0PLT;&MmrJ!bO4;*O${|Gn~gBgfj5D+Hm^+hwtk{CXxM$|1=`EO_H#rh zuThcM^`;D{-SpwRPyNhN+4jdF@QTsOJ3x5jxZ;U$P5~h+tyWxBv8q}fUAaS?Gvh(Z zmGsnxaW{LS0&pf-LGND~fb#;> zO3YrVHUcgzwhz^*P(Uf!y;Pn?EyfwDa_7_O-cFGeYz3<4K^&4UTvk-b1>!1GC)7r+ z9qc8MVLfG84pN1h0wb}e(XG^4#(vPoBd8yAt#q8WHQhH^#&_-Ju=v=3s;v8j^32zf zn~Oj7n24z~48^fDV3x8^rzT@tQsfmVZ(~wa+f^|O#r#lP7)MdiNDS*pf;$M{*lp2% zw{+)`UXw)I^Kl)-c0&2CJ8H!3;lVQb+gjKyQI|AjlfWO!&<-ewOSp1+wWVJU_Ha2om*@gzeYXfEfOLXAlpqyb@8j3=$}#!fk1)aD6dI3=)K~>3Vex_=eG#oW35X-HNNHwNPwZ};D4FKScWp;{J#%_!`kP$r&!VWysjliq zwdNR=br@%GOQG7*sj>gLht?07=U_=>s$tSe!?|^}Uq4^<=x<`W)81xl;lY{=jfPA< ze9&7HbOog@MFI|}Q%U5-^li9(E51#6qz5J9;NO4(s7uD_B=M#OF^Z@MO?TD18!z=Y zk#T^{H%!~suxIj0QG(1CGtnBJnyqnn3|2#lx!!rqu}yp3nASjv_-#f^@N5E-xr2?` z`{1Kmu0cm1^ZPV5gHfY2!EI5(%3leh#JXMhBF|n?B-ocD8QB=++eX_KY1usP)09qp zD6db79v=Tdx0r(S-qlkIy0x;CXDOU3QFWd-LiuM!(b4Bd*au8%C&!X!+KXz1!REZ0 z6nfM*^3AmP)U2pMGnZCP&RO9oaj^rHuRPy&xAnv#yPK&Z4EU+N4@>bm5JuK}JF+_3 z-45**Q`qJu#{e%vDuA=M^(ikyxI#R?l~njx5ptkypI)AN#M_HfBn6bRCPh=yG!6`L z3Tac^DaRn0wl|rx<2}$%N|lN-l&?{7WN_SOb6j{TqO6KHdrx zT?w4D$!0SbR)QrB1vz%DWvdZco(B`mAlVgs0a+%1jOw{EEf#JNe{84mKG! z*_H|8u2$YpZb#+9E6bB4+h;-WY;lRZMUWyquijOd45f%GU`-|y4!49UOlMS{#vmH@ zto@DAU_%-2G7TU6%2qJe`n*&t!pEV{QU(hOv%tgKdhbLvLM>_t zqks)V@-n)z2iPpOztvE+hwsYIhtw!=kPeGCI?3tECW(HI5MpQg$~t8O>dUdj?Ilq; z9LRGZQZE#3M}Q0bB3~rq8M^MR6Mh9!A(HUA7NKhz)%v>_W|@Q$={l|F^-CVT@$s`u zJhB>UA>JVYs)5 zlRpapRA!%Pk?=%TO)%()wG*Z^F_|yjUUc0jLCC39u0UuHc0y(J#rA+#@%*0N5oO*s za=L7?WENqQf=`uFKDy+oM!c-^_>xZSBJnaPnGXYsKPNz)o=V6#x({`S8XTka*i^?( znF~8@!p72r5q)o6m6qSj$1&7I#x^)F`>vXFUhfD8Pe`l|;Xpy@a8`p$o%~sCS@#;t zTW8-=K1l<&2_^^!2?lo`uWU|FQs9A=1dm8oOtaM&fy%;J+2En-&?i1nG@ue+H0vrZ zN2uK$ycSQb9b1X68?#%+c5lBczODG;y(A=bcw#{e2L6ysb^y97vadn-)`-U`rW6%+ z)I^aM$FT0UPHf>aW!P>|+#ywiYFSi+Ii9mXjHwB`_2%RprLWZ4C8MiOp>1TV$(l)Hb3O{;b#N({i7_>-cN zJWKp5s;}K}XiWE(K?Xn7260T9uXj7NY2yB_9!b-5_wz(9oX{a*+Hp})Ro~#3wO)6G z%b<1?MIg3OPJZ{?8ELDNdm!{5^)LwUwE7U9i%pXKG-xN{gSf|4QWGQfTps)a0IWSl z@HUN}b@a`??K4P0VxJb?9eqlsbgJ*5iRwe&p{+BgisRINgmdc|K>MG8t)AO!7p7NgUHL=`= zuqYV=F)Y?u!3O-1Ecm00doFi`X(x^!-+>4h>P8f$A;w#;v-n$HUAl|NgOh#)mAug8ab$78>j zbd2~mc&t!2yx3*%l0t=JU30@sGk2Gv)y1JJ&-8v=?S_fgjdP}pOu`X!a`f=})LXEi z*rBgpn9ZQ*HY4Shq3GT$v*tepA~S*wMuLs$%snZC50;m8f<$+|isacCP!4=luijR~{Hs z7S~m4P$fmlJAQ=5OFg$@P4OL*-v+-6Pc}=0SLN)i9pJw^a$SnybQUUKYj>OTx1q!g z`|UNHWeYd_aRxL?w9KX-a7)U9UIfRLvGaV9 zPd?WnlR6FSJnw^2aTfE7=t@d=y4Lz*z6H`YujfE-DveXVYQ-nQ#>=4@-Jd4W$9~+t zbucjn(kIMuZyqU6KQN)j9w#mDQftvSF1>fmsA}xpY%|#JHPmz{&Ya>7&n!JgT9jm@x(qi&vju?qYj^v|jbua(0{n;<;3=+h@F3(Z5|;Y+(|=7+ zk$iK!z0XQc#F?$0ET=m$V8+3%|>?%`2RH`)$oBy`kJ6FDE%er&&n4 z&;z5*30Jz)8xhRwKBRbV6387IK^xNEY?8=}fIh1zd zvXf$Q>0)NCS5T@|Ffqk#@ObdR@fLFX-X!j4vb<1fCYxWxknMv7vA|^k>VQ{&oa@V11t-z*HKV&sBvsOJ4*#nZ;QmJ@8eTf>#T%|J-gxFH`>|PZuad?fBNVx5 zpS5@fOw0gO>7Qc1jLD}+`G`N4O7PMvtbU*;1b)Md;!-J=k)EmdU4e$C+ZyZSuc|lD z!5bXorEYJLR;|{zv68hF6}qR>%A=2X^hSYT&qG@8YO+Z}eHp&K+w4_@CZZRL=7Tf* zK7@bA;D<5~vAcOQ5Q;yupsz$#3qk@W_ZPxE+vMx#7gsswalGjPhnMV%dTUD?yYsP zxj3|^e!TU9+&dPiWJ_~ZJbTmu`?G;E&&UmPtP8o27bP3`9VCU)t61vVXXs#AbT1|o zhGtxfZ?}cVsKoIIX=>(9vV3Nj1b0*FKiB82%10j@z$T-d%{ zz})+Dd%dhDgF@RRjbGh(NZI7%y>t8c)W2{JaLvUkGH{+Y&P^^r6?$;hy6iGBuGYvw z)DB8><{(O$yKgxSYdiJp+!h@Xdzgs z`ggiIp%)>6#rY}-(CI%MTNEe6(E^QaiJzQ2O+Oo#xez<;;wqe*46Q|0d*7)2Sl{*C zNH|LvUY}dsF?+Gf#y(1|$3%uhthd|SD#5AvxwSNE*Yt%%1)f@I?DwavZL53l;_VP6 zjoR-!ln>v>oL5&+azN?cW2E$S*t5z@?J)1Kf|6wQAK}Q?@sYe^r>rhsH8Rt>U=z6= zSHEJNRh?nAzcZ%$q_j{k9?sFTp@_ke! z-8aCsRSz0JSuDZtVKd|_9%b<~Yb1JrvFOe19ULKfA|gUBqHmPm5L6LZIzCKH&4CEo z^R^!IQrX29d4^x_+luOhwm#&TPljZAeNV*{p`DoiDLa{Ran>D107>-<8=9HrCaV|3 z2ae3Yo#fg^o6watv6nxqxmd!h$cdv5J+(hOIp$knCF`^})KR*)P^))eZ=Lw5dLuK% z%6EBph8v-sMY@sCniMSW>MJ1fc1egH?9NC$S3j9JIVo;e+|_nG*+nYb{kmFD*u|#} zuP@nE>$E>}ihmhakQ`Glus6p&{bg#m@PIVi{Q;xGEw8%R+u6m<@(%6;z0hkzx7YSU z*T1@>0Z=JF=IkS?&!(>Mqxg4Fp<3Qx4c$`l4G%pHLX0MI?Nv(UV8u zIT&!i|GmGY%ph%VW&^@ZsUYE?h)^ASRma5j#Mt`_N2b`LK5k){O^`QRSQs*u+6Dc5 z>lNzyrARMmeYOaT>Qr*etsC_UeetSA*HcnHd}?19DH?hgr}lzYbLfIj?%U1YS%%hJ*fLQrwnD(Q!BZCx%BX^0IIs|3kWmVh-FK1DGY4Uua$VhJT%U-*o?Jqur z{^0T9WTV#;PWouV(Zt8mA~X*7Je6MTr9u7HN+jmWbaGcSRL%k^B>y;XQ1)w0-1tsK z$&4Jx(|Y@$Z(Ou#iEgd+5|O)8J{&zzf1Yt5^Ijl>^%7@a>*CHAYA>Zl4rQ&b;;iOv zM1|7N^b29CILz%qmsCyWM%W~+@?Ki{3SoAuNVzoo0dvOe_mRWreVp5nQzWmCy4&w( zU+^pZHX5R`b3&e6%xT1>)@T|=KgO)=Rc}?Is5*5))rRDZsfezRjr%9(?3vE@*Jqy5 zK}V&hi|ka`Qi4iU2?ZIlW-iaA`Z1nr1)!c!nCEE=rvW$jOwBU_Z) zx(^zsIPsMNZY9!mp;sDFuN?q@_^b=A7r|y|{zRBoANKmvNmg|Plqwl!Dy^L;UVpVw z?qzdbUj4*qd*oB=Zu%c#P)}$b>>be?(Ck$L7DZjK?GN4^>3Mu>kmO3uSquRL(+G)n z)ugr{$b7EuOdWE1FS#K+@w z@2hK$-s+e7xo1sLYL`S@5T(ApFPS6Xn4AooVUN<58d1zLp7Pe*8n4!?ewF`|M_x~j z_h&PmVeze{Zw%gQX6m+F@9A1N+VWwU+9GXJV&iWG`i!Zs3+!AJuD-XqH-6&y6!GEi z{dcpZQWgA;h46XDTxKD7JVc*ZdFVb6M<-h9Jyn#`%+<3DnjyB}S3(ag^ysJDmsrHiS<3)Il#$CN_ zjp!Y35*N!G!dhpW`L=9RZa6qcu3OO~F^~ln(G` zC`Ign)JIu5`|j=i;5;p%Y0mCQ<~=`I-u8JdQLXN^JHpiP$h~}(hl$H*GxusnAxKz) zKYI*6%rGc>>v5jo&HIlW4G6NLD%$;&r+J)2-6M?i)_}dT*v|aE1)W(6)^v5 zX;?;HEx35ci_F`;Pt5=T?^C>TCnI^TSO{!+2cw{87IIKJVRp~z$QjVQKbKf0)kZ(} zeBg3;-apJ>ohk=qJ#xoH&2P${4755sKy8N#G_fG%l(IdfR+#A@u1hDN(+HsXLdEBR zy7pYVDp$z-)x49h(H=FI=>BA{8PJ}JkhjkW^$xDD)!w455p*ZR$!5IH`rX6+b2sYt zXh^hqbI?l#T75TuN+VJta!8e3`Q@2kQi^cbs46bM#{RmnF+uas(kK1a|GGqq4-bg+ z)9wOv-5L809hytoIMMuI6x-NMgZw5IOvm>bKv4h;@na8@K$=oRv!wXN-dr2~Nr_`N z3~AekOQ^}%mG=F3@{xqu>>npbHXTKl!QMc7&`5Xv!4uieB!hs*O(K3!roh132hJ`6 z6)KNBHD5KPJ1nXUgArvpo+<@uB%pXoQUuVoDm+@sowJ&#TA$^XwCyg0$;XICB}d*w zROnZJZQ=G;Rtqkzf6>`Bur95Vm{lER-1{!hX*45atXXllvB)5m-j69UJEl{-p#X!U1_-RPA@BzRSZg6h@Si z`{^YH#U&_`DsPPXCj}3$gjL z(;zJ>)CCS=9{a7XC*5-Nn?6puEBKE81S5E9ADzz#C|L_&P&W*<5!;z4JPQtIfmc;m z)7*)Zm`mQ>p1Hd&w|=w;6pg%L8GH9b5gLg_nfxb`sIIdHK8$>vr|cYZt*&vpj_3D3 zZ-r7y5s%=%j(Bs3((i*iczxPIXrhzys>HiKXY_6L>daUBn{mEW%8|2qI<%@f{r*}j_6g?WbwNhr+|2w-|JW4-T< z=5fig`CUIRj8j!S49YwC!8d;Akm8|$`P4F@|$xK_@dYE(uS zV;1-3txtMumWfZqur=^e$?^yO`dP}~rEn)n2UG1{j{|EFygvXx1Wyaj zOeM)42NJh4Qpu|V^70mJyKYTqFQUtR%^k-QK!Px$r5E9)rO+W9zY58aDgl6L zP$qAo9b@P#sVd0hPqv_4%vVype1@8S=0YgC9r`Y$?e(`6tr6iLUrO?w5G6XAo zb$ew}=;UZWW9Nc5GU=bBoVI2Yw5WpwlrXvW>(W7>oV!DE%-Ew$HfxJP`e_1`5Gh$4w~=%@ zxvnbYanCc}8ln%cUr4#J4yShCoSbBfe46^cP8xHv)?bULl5Xq2yMY==?zbKy3>Qkk zM$ns`{rMD~i4bn&K0Xq}01sn~fc}GCjvNtg432d;@2F*pA{xq(Km=gsZP>B}GDct-#EYjrygyO|_$ z1jq(jy)Eq?+W7eYSB;~f_K=7vdspqc3xrZNZN!vY@WS&u5+y;`ruX0O`Y=~ag1i3L zT@Sr&9?v!YBKU#}tbv5&5!eQAE?};IN$z--fUWNhuF;EBp6L{1%(0rdYM}79P$pdvIP?Fo*ZV_y@F!I}DQJV)Ui+7w) zC>`nwcyJu#hS*o#mUML+ONjB^#VL0t++FyyM!~Z?ZSGB-5X~kzcqnL%00h8OAOpi7 z{Cb;b z-EBTn7slNFgs|d*2@h6SgL|)9srC-~R(GQy{Ed{QCG zTr7`e81YARb%dftkdtjp+Nzh6@)~5w9lE~^NS*iXP0DTwrKhoVdT$E|<+kgDVMF5P z3*xfGn$eIH-PJW0&F{ghKkxj0#nMg^-#%VRKIsH(KUjgQb8XiVmwe6;JWSqW9=+4y zE8$S$HFe5zVr&k9^cy>IWe+H#H-u7b5nVyEf3|Su#UJz^x0F$p;SK~V4K!?lKT4=8 zzsZ8!{>~@WFbi&8waY5LzR`^@kTqb-4e||7xRM-LbQiOh5t-l?)IZLU{rWn{AU#aU zo@%}%z-F|{M99olCu2Q=aZ<7D6B57R3h4R&ROAgLWsky_gXt(ju>8YW<}zstr3!1c z9f_+C2J6}+x$ej1aCr=5vJq5X-IBzE%GEU&#r+_Krq?mqPlLYzWAZ?&RfIqAcfQvx_Fq?<;F8uJ-9b~_S-GxD@CH6KuZt#mt%qQ33eT4w)p3(EQX z+T<8WPMll_SIH{S7n?85XRLAHomk{`1(-uu3p|&FeQmM{LoLR zG?=)?fctJd`62O+fGUXkZS9rfX*)<63#x`f=zf{Kt!Q&L&EB*mAA#z7(Emgkz15fN zAlTrLj9&*EY~JQF&t3JG!OivlcB`|p#777R?w@C)zYxmpX)mRSRt`qCYu%?F145?C zbH0%>UZmi7N=3rB=I@`GY1pkBGV@N7veF4H;8tAN;6q>CoIrE>Ypv6z_S!Itp?9w} zl8Z9epI(k3kw3>n9Dc_Ujg?R?7nJ4cydQR`-wu8b5qgj*Yhq{4Iazqd^NW*qh#?bO z1FT-VDIukUq9j6Qi(&!S%&z`SI)I&BQ$uhqCFJ{Vb-Us_&zrxr*DX`}n&(G~U}SU@ zN3g+S+1}!$lBYCjVWZwK_Mtk_(N_(_$3vaO65fFO@!kr|?WWdAd);L|^=Ab-bL$sW zh;?xXi*k21McZF{ARKZ4lBr?DJaa-h_^2+)V2JYn={GtY*i5oGaZ=IRYc0 zay=Ms3o|UE$Nc(4H+NxkGJ3AD#>!DvGYyV_aD$~8whTRj4pm^t;OymNwFyL8GZaF# zc`^owHTWxT{RJR;6f~g}zN<;Ret%NwE78f+Olhsz`)53-P{YhmKcX87s4Cffqh-qS zVKr}vVt4SF1Row4)G534iTCVMXU?9(k71`Cmf&x0)CP$X2L~?G)U&rF1=E7mSi+p<>=)Ojrv<9?s^(}A*hD7 z5O30+2W?)ZdHobitx~8ZbXMOtf)bi~qLZ#qHP^&3Ts1sw=TW{XD$%oFBPr;ufy8GT z+N(_%64QoCydk<@NLGSrhAEp;<=w1_K)&2CZ-(;d7h z*nzuTbl$jG6V}v0s(jl2`~0V<`$QkZGn9ec`U*nTaF&TNo1Px+URpm--cMUgbkd@q zU!hlvx>DTiAje&ILcx}z_|0nSqI+Md{sQW?K^0v-zWWw$%wV%8FqFH%@azmX3)p#a zOCw9W`dP}d-ggzSj|-mS8My?fT~tx-`(RugYo=2<(JdN=GTU2UT6rbQWl8wk+ZMBF zsH+R4o(#&rtf*H1>Gup#;AG8(cP zN(b8%^2>Yt_bANrJ>Gn$*zbhSNi8K2>goWxi6;HMp6|4@cgHaM^i1B|k0xgp**k`S zJeeWi9ZaFvF|XUtcSXU(xl5iYy6{f zjiUQLi!5}{?KC0icCpFwtFE6lN>S$sm>)}+Ksg*2dLcF_!k8_^#0;16_@=0BvdBKs z(!^!dHZahq;M?W(u)8{FN`rhu-}nvY5|DcNhuUbZHXaA5r9|pq7IJv@M$}y5n!J{h zqkPp;?vBz6{NqqXIZ8z>FKUw?$S$hVu+VTm^%>E@l*yg?QmH(Ic?Lq^kW5l$r$T}% z$URbKGlfZtj3kTd0D}4jq!ULyFMIgJ1u_5GR42Azvw4;NOrQuRh0o=Tq)iFC?kk6g z`J8m&M9n_UN=J|{#&K9i2T^bjmIhl^Nt?)7I3ZZim`z1OlFZO=xQpr4EBBEw2h7~G z9^j#2_D>#d1iqj29q*Zqm=lF44CzEP#~#-aF}IS56pod$ysq~{8t6Wu2$H*LSRQ9g7{hC<32gG1Er}t8*;q_}#pZf|*(4GNlsNI(Q1<>7{loi{JG!l+iIx#U(>x zx0i2|b$}vN^(DpMmfV=smkLtk2jRPI1KL3H5N6ZlMSe?vn|Ex!E<$%kALku2WO`^i zCHXzuFDWjFPXz6pdM>Jd>1_^6)u`y5j$#Y@L%@APF7oBBglVODYTvQP!L!q%3tYZ2 z)D%+G%x3ZWgy)QIoMMjsIEo7Jjbf~HA_msDMzQCL%&X2xIO!NOB^Dk8W<*^9)Jr^Q z-rp6Zk;7H1Ei7Lsz=y0ID7);Junp(Ovk zOc#%;Vyi1(wA2AS7g2Jx=YNQhbRkT-}RibG%2cE zJjd^ut0vQRcSFQNFaJl*TR^$^b@-t+yhKisN!D~RSIjZ6o1HN`_S`Jv3AD*{r!C_W zq+DN!%x8d)Ie-V&9SRddBUr(D1J7DTo4=_!*K(F|-BVl|(jXXC{7}%2{VOQ_@U( z+!FbYN^k-)+z7*`ch(ZcZjG#*=U@A@iwg6k2?x5v|`C8ZK)> zV4>R*XV)7B$t*pVldQ(UbE51;Ly6Vw+g&~76-#)n=-`Bc%@_A&F9#Hv(3x-o{U+-R4_j#m5KV{?pvAieb$_I*CqV zUd5x)imdwlZSdt5Kql0q4^tgyNt^{~Z0hw)i*- ziPSs!H+YmXF^CJH0I;1>W=ZA>=RjxCUP$Gs&;-fsK$n3CbL%r_?g8l^McoFrVv6Gq z$}?XL=>YpOymW56x`6pYmljoJ{ri@;biOyx3f-W7bN0W&5$l|3c=(YH*eje!+`BOQ z(Lq^BoNikV$?Rzfxn*s!a~a&i@SJe{(1LG~?#%xRPHUoy0axX@ea6>W9=(l&P0k1i zF4oQzz~1ex%&O}$uoLT;b!+J`em2!(U=KXp9*Re=c_~GHlfe#$Ldh`uBcLML8fRhU6Q%A#o4r-H4;95UCWn_ zu`~G?N5I!vMk{;WP%YgxG!AT+0fP$sW&5=0YCoTh#OWO8tpeu4nLMGX{mx^3L87VD zwxH*;JfrT1X{KH~T)lOdn|G+qQ~k28jxok? zW~X~yZxt``ySaCIb#{?es@&uL_r7j)D#Dg|(|c zRCd;}HM)Pww411IPJuXvRFS5gH-qTo?uJRAQ7iPT4$5ReN6ehih#pxC9un_@iE%QA z&OmeE2}DfH;}(sf6c$TJgN!_4R8JDpvse^a@g-fGF!IdE2=`Kx25DVePl22{EqXR$0ohSA||ye4R* zG$=)5TX;+ej7#Rijxr|gSIN+WriaWtSd!jItbz*(~6smePkK13pJJLA%{)5Vb9$&59JvcRpPWxAMxa%zGVV1YGB4eHW*} zh1wBaU0qwI6gU#s(a)ShmM3IholzUGt+HA$vS_MX3f(UV%sDHoQ=3r%Sy0p7j+p5v zn5>(-lwR4XxiR+k>CTJA;&LD|!D!H7Pia!?2`ha}tc*Jb=oewH79=xb4#8?({Y0Fx zcYz_^sDzU)QMjUE z77CG-PVc_OE~%7^iq7+2DXk0}x!hq2pRSl@USd=#VFn%vORByeXm;UHF&=cRs!uTb zVsAaKmkV74GC;qW3z$1TJYC^p(HN3Kq3DEwi(M3ke^c64`b4Y-Inc2%Y56iZ1v!4= z6k0&cCzx`nHla9p2@|Hjxd;hihayoI%d27B_igA&`Kx+ADs z%xnp2Nm*j%{!O|#uS3=@!ff^dQpro}Cy4^v`SZwsxx$nAhkdv{3=I2`V|OAOfcb15 z>v_ZmrHkJWGiaR6E3p!i8!Y#|h*F|vww(E(T9@|yu(M7GYH!AX`gQ2NhA_jjE}ku3 znMgwuZ6pt8>2~{BfyWFcuAk?>o*3iapA(ZyT^bZ8v1-> zr!V(4ihcE$2$N`QJGdFoFpJDND~KE$GoFg<%B6w#xYLLv%R}E51&1HY(XNKvy+`yh z$UV4Y3huO|K34y*kb&Y9j0=04GMFTG8N^f@Sa6JSeLvT8o=|XZ>L)E5ONscm6~z-- z2E3~J8MabK9Zj)n`QCApovLW7iXzb=sacV zu~6`jU^6F9r zO=flPm8Miw^qFj-L%XY%cr1nxbqVJ3jLaOLle@MXer0UBu%Vp|#{tG=@)2LSx;C%n z{-nvl<#jK(D+_c91<%9+1{J~pP#)_uDGY^+8rs=06qsN3GeZhCFP_fE%TTPQPi)rD z1li<^)UuW$zngnkdSOlnLHT0m6se>n#u#jaSj%W|b0-}J9UHXrm-1y@!awC~EaTr_ z%{vDGE*ZZQaIt?d-VdxjdWtDqBwtdMJ9PsC9M2SUnW>x$`-{XmZ=wy~+k8>p&{DYi zS!vE<60~;q2Z0wZL2u34V7fjl%s3pcHrQ9Y<6rhvUzC@(JY2o5mQJ3ZTPg?9@A|5j z74J0tEr;WUPSRn({9*S7({Rc1@Zt|6m8+Q0Ldx(PA_w`RxkAmq>2(fJ+oI~Qcik46 z#5~Z@LR4(}tT-(RHfY6+k&H`YXh*-dTYQ%~fvi4&7p>#zBUlVzAo3XE?dnxUy4tna zgF}2t-L9l8osnTd_QAIQhDmmB=CWbP$~pQ+gj{^Kr0;$b^pR{_z+DBYrS}n&VqlH7~ettzfLGS-&cVMC+VV%dyikQIU zd~il!)mPMeB*VrhPO8IUU>nj2;NLzPLoR%|wxZVkeff&heKe48WFT?@U-(Xme%YMp zb49buZ-$yAexF)^Qdxzq;V#hmH5nWitYY8T7dA zdX29Yu0hqYDm?TdqI>W#77P+ZLN>K znCPSbdA3a!#NW|)p6n3K(ajgc+ok=Iw~xz52u5Cl)9fq#b+aa_| z{ciI2;~(-jR9SEk12+Vk&4u20zMKp0@Sm4yWKXbKp*vGek{bryRUp*`ys8b<)n)14 zvg}N~ZINLA-^r$CT<^(*uCR}#XPC1i4m;3cE{)$ux59;TRM#NAYn50&+xl?^e~56) z7cmJ|Sgar5L|AM1oE<1a&9xb443J7V*J1hJTD*He#1$hghu(z zrQh`OVkADlXUk6cHXL8!b#62JmDkF;WMi3ty!sifLLCTejZQ{waw6ABsy;0c7F7JJ z^k!KJ&>wW1@bUG*i%bF%zudG>k{7awq8z*KxY)bXmR_8W}Z0<~{3 z9KdxHUA)W`S7K@`a@i66%Zv)E(0!s}O87W>Pz));|1T;%+>6DgaabkNqjyC_On%>)uo*yLmadZY zE9rNzHPWwTT93)$#mkQgHjq*NFWUm@14(v~8kkHDtNhXyjHEC;c-LUdpIQ3zzIKco zD$5lUsPoMw{gA1RCQ!?i^1dT(^eeZ>sTdJ+!mzMSV;x=xi3QM~`x>&IS#~ex-5d!E)+ID6b39AHi@olR($HK#@hWK1iQeRXMT{~f`87fy16v5jPlOqLq{4N-q33uquM0r2T{sv-G5 zCRdjKJe}Jlxw<3WXPn`;^Ajm3I({b&Fz%Wnxrz&_f7@xr`x89Kf)c7{W8Uco_2Fur zC0H?H!;1K^KEHKQDNkqnaMX20P`azB1^u^k*(x|0|+C-jC##l36!VtT``8 z%6}zAz&3_h7X2@TV@j*vcZQ^06^cJ?#gC!ESH{aI4o>uB*?C^Sj9%l9q2{up+WY?^ zvDy-q3Oz%pzL!qq4>Bb3MFQ!X{~)hwg-|0BAVTGbH=O--IMth(%iZjoCpmxCAz?^S zE8Zy1t|ytYw`}`Ucsnvi5)9~0zjNFE|H?=ALl9dq+N`X%lU`_Tfs7BRIGJVdH!g-t zD>nw-Mt<`NbnEOwR$wEX%$(6%iy?ap8&Cw&mTaaZ#jSW`hWIY!UvR>Kf69+Ts*myp zS0shj)k0QAfB$uiFn_A9p``j*a!+@)Qk|JIIz(wxxr0qM*0FxvR?rTeUT^{mVwCc?m7ZK!N@q2Mf z%R|K9Uz(K>-H_jc3v!#ARopL~Wy5q_e_yZS$Iu+cj@rUMnX?^^>$f6Y2a4y6YHKi-S1F%k(+6B` z)Lj{ng#JZ@Tcvtg?aFhrFv6(p*p-i}zeTGgFZ?GpBi5X)x!gJv;GK4-*R|)?f>kB{ z_?P7Th~r>+Y(HA8Iqn;%8H8!Px9KAtX*^;$!CUKMSCrNcODKVvqfRqg6nv))_B zu_J!-So6<-DxAHK1VB0|ttVD5xL()o*l}(11%Ejv2j|wdF}Q*LhBQejQS9YR&foDM zCa%U~gcxhushi_B7#|}wncXC7l-55BxPxSY_D-jHwn6+mu>jR3ifJ&?>}clZ6yOv1Xum1cwFZN>Rub4U4= z=KN}eeA&d)`rJ5$;Z2okoz2}Hu!Y>4c^faH4WSk9(DVOjyw{pddm85G~0NKj!~?=L(i#1LGwee zG2QB4;Z(D??RTwIR#h#(pK7_nIly`?EepfeT6CO)nv?}xJFzOqSs!=iJ2WW%9m#=k zq!={j+zGK%5=WI6eh}`>>|{>hY(JlO9KO0C3}9Im(o}l3sTe=qpL09c_%N<2tbST% z^n7e%%HCnM+BE|Y$>Svrleg2wR^BOopw1dA=9Pgu7_0L&4i;F&^@{7Glo{ycL{u2A zd#Cu&iU;H!g9K~-mG~HsjV+rvMi;idxbwkNW6Iw1 za%<0z-u)BrR_oX!@m;wRom=Y-Y{)ica;%z4i=7YD-nC~Z*-a~b=)#-L~8VjewoZ(!+V=q zXJGNFLT_bPa7q5d2?(pyieR++HjIhIvcDetZdek9;ciMSpKQ|@7x6_>X$4yO8{ywt z)5U9vuI>WnNp*;yW!3tSL)}53x&d0jPU!oqT!&jnhK)I!;WOi}h+M3dPOyTNYV{u( z_G5A#plw{|3Us{>Ac4A81{*B1@G*|E1o>d|p!!!a&RUH;3D5+?1~;+HT6VY9D3^R; zx#6Au6;xF)VjN|)Sx&!|Bv&;)n7``2R+xPBl z?`;epZ-vR_VT++0@q{^@zl3LUt@zjk{+3|Th0Ah2x!#rDU_EOsl?Y>m2nG4CanN4v zoO3;P^E!wIt2g}AF2 zBoO-*H}@bE_ioDl*{k{s`|uWVi-{{>n_kLhv1 z1fxaFzasiB|6)ZSy?uC6p9kOot8^EOa}l?-$L|pOiu0>RQ7I39+C~m|6j$p9kBYNCc`Bz0_KwyMe`T#;9XBw z`+T^}Z?*a*=inc(9PpvMY$lnxFWt{=F*$;No!K)ks~Y<+BM!!gP(yDF_&(Zsldm0w zfHNVb!k+(sORmECN@KZWC-q!_2B-0b)vo#tPW=Eqy1H^S{)(x-Z0xvPH7+xbWc9=- z3}tf{H@CfFi&$Cj-(lk`2Bc>MP7D!+j2v_R2jgGEq4utCb=TuiOhN`rHyV5Hsv-)xS? z;m6wGO##=ksc>UUSgA?PFdCgO8Hf;Zqa6MiuFj_D3!JuZK*sQ$xc`@<{&V^QuJL!G zqQK93&!cAjN^D)!MJGcBEH2T&2F1%K7&7>6^ zo9e@MZ^Fk3=QoQvu1jNrf564?bEf_PkC)Ao|KzEf0$U!5n6&^7ZX!z2(s%LyhEatb z{e*XQZhsBQw8>p7O2pbMi(Ei+d2!<+fK%k}Nfk6WmNUjnwhjTDLeP` z=Y-mQ*EZOhu7$?&affujfVs88gN;)#+7^}n!xkW(+!*kt-~!>fw~uyi!Wdoh+q^&a z_elLJ!}0&Un8(!_T~%z zziQ~sA{=8dv28lh$3r!|`{FV@>$o0V=9Wkl?t2&K;^t1}7*^@~kXIzCY|Su}NAg!4 zpM%l%mnN()L79M!yszKkak^cryML_-gEu*!c|n7JU^Iq5NZ6x2ol6&NWcbuHf>42Z zE`$SzYZx{f`LHaKyr}*Uk$4P$6Vpq%<9tBCoPVFln+;mzE9JiN^Y~0|rT_o0U~wRC z+zF-~#UPCh%ig|b;omq{2(qNZiHG7=CLXPD#{a5*!%F}5jK2F{ZD$@1<@(0)&_u?* z4J9IF-%?q!FC|%{P?$kdsqBq)EZIj_{+_l|H>I_LaO zo&M?>w%`<;U#t-#7o(XE=R)P+8um z!(aR;H{`?bYwY?BL+Wd&zl{asQ3>96A0GbU_&?Y*&ifi6J`tkTraGJ_*9LHbp{H?2hItSf#w_NaTo!6DVjWq5zRK?f4@=IE!iBI_8 zM<2Ceg@dbgKQnupKY#sKOJe`FMzu;Fp}HFd0cE@m{qgu~66{+yfB@h4^Z%b3z%?A* zdQ43n)dXb_1jffd-caUMMq`nd$8&Gzf*rBnk<+HU6d(Q& zRez=Pw*lu!X5@rq{$XR6wgIPH#qwkkIJLQc-&ECldIK~z<+P(jV0%y{-(7K9@#L0% z#3IfwY8=a@I?{!H4Oe^}}}m!1}aZser{LHd3l&^gu)yC}VnB{5#+$Kzqt5 zmM+|=r2b!43g1i_{3-pl_Ur~2XUd=qv-emi$$BS+h|8?}qCmCL*ZlVq=dH*bnzc*( zu-jMUsEB;Y4R0AQx{dI?<&eR@xi5aTcY)MSfQ`V$UaRY^O*H`78^sUnF)gwP*QEfd z6p=6ePlp=Rv1DmCPG)ZQ!gxWI>wPzTw(P6Lz?z;KPY(P4 zM6sD;Q?+m~+7qsV&H!o!vg7*|H;8|&sh&;XQ4(F< ztysRDU%l(T9oyJ&7y$4%lW(4U0ow3tSH-?9PgzL*RM)(YE7+RI^HtYu_)`E-hRLVr z-cOi>%hs5NYgi6s+i$N;eMKWn+;0|zS%vhQZzlY+b@Xx*5*>zRX1z+VjCRT=vczQ`Ub8Von3>z{oxF*4i^2=mF=q?O)Cc) zBxKoY`E8Jayx6+Xzut4B-A1`Fmbw|T{%6bG^rv_u26>PWZ|kAWOxj93627%$`~Q{s zkqr=)p=(*``oMiQP(+EcvK3$LMXjlsuR~w%Trb%COTU&4zT+wzhXT53D7C>tzxXN7 zQHxC?IAo;P3yyyg+O|^qeYMK^&XGpM3Y76Q>l+g6A~cosKT4riOMQ`rq-RbWZwjYv z3@ebq6YKAFV|rqJu;8CifHrY6e`F2`s~HwQ6}bG3yCoj1AN>2qHNS%#<#d~nq(4nv zgR!rADBQ;EmG}o&w!d(C|F+NnEy3;bJNr!wGBe1Ib(p)1Mx;qhIx47>lW?6ES%wDr zU-aO_SDWX}8#g{&xY^Qxml@9Zu>Bn|>-jAi)-=y#x%D4DE`XV>dElj|f}A+>9{gup zj(LX53uAYHV*NwjAztyLB`BXBeN#dUTC1j@PPc~CIx1r^K}IK#@}CHku+(Vs@I+G=~5!NEqy zm8k8$zmNW_5?_DdY301tCTAjSfNv^8DeDfMN#RnqC|kgBg&1ubm)em^(tDu&=#*+rJA>jHDt>W9 ziS*t@kq~`2mT+?@cZW#77+hh#KAH3$;~j}z-*}V#s|m9{A^FadvOXPL-+PDno?A$R z7avP34YuXW_U{WUF!c25NY9jwYGOYL7)#$uS-Nws_>*I@Z*N}&=6q;XfwX%t=-~aF zPKSAnHYh`YRhI1rT^Xl)vzW&xh`zNi_%z`bT72T%jzwl;+&$%Mgr?lEo+n09tqsP>$!V~2a;u=f$!=O0cg{C z1;sX}MN&nhP^CZXo`C?2jrc?oUfuQ7WfC!x z+sEAN#XPDEZL!*w7)B)cEPu?z%w*F!lU|F3X90yVNYmQGA07@=Wp3(%B-7w&EMK$m1Ia*j#%KD__1f{Z+E9eiA(YI42^&Wad4I%uIL^71i^MLpR<;BUB? z-&&!6OB{wwms?Xq1fr-gi?wC#qAOz5d|if@B(a!ka4Km@IzU36!k@8=lUAsF-X9sSgJ#X zLwip*Ux)F>`#lv#quR0LfV(*dE+fCsxiBX7-LBX3wGATY~p4l4X}i2dPe~!YgQzT&Vmf%84i|x>p9L5K$uQXbtE;LPC+}dCUdLb@tVG2 zVB#w6%;M2KjZ)(l5d#X)u29)4u=`$8EdpoFe$lz$Ra*Sz*5 z+D}&Wy2iM7yLsASD$0c!rSg=vBcf!y6f34sB(%C$;)95>#ObhG@Ez*m%^i3b5nUo(kMXlm?ZNw!xh$Tzg$`ozb2*-3%*OXptetsQBSN?66 zZ@q7AN&_}cYFx)laHVK)u}o}*d+Fgkj^^3&?a$rsdI4Zug0h~A|E_$jA|8%HNXzBs z{t@h35`zPJe_-1DSx-si+1ANkIx!^A*Ox0Ix+n5DY(V zCyRLUhi|*603v{}*g^%lMw@3@-zS(_EuQP0!;ZDg^;WBwUL8D7 z_EEen0(gN4j{AURyZ^`IvMUGRIixIJrc9O$fGNKI!+&Gi@D)(~xD52j8oz(|eLca1 zwY&QRTk1!81iaaEoP8`c^-y7tR@sEEz-4bhIjH^~XVsa27aIahWmlmaC7D4T~V^8x&ne2C}#2u9H!dm@H*mk1}$ z#lel+G$}`c;8`{i%QxWLPRgS2IE>&CGTCYFGP3&^q`P!}evrQdgV&yN7o62r^iKyf zCp;*ST!GHKt?uEXHWQJ%tz6cpp0mtv!SJ< zr6{{@OIK>U)%FFuMew-<&uc6Y;vPVvoo!H2_5u0FAFeZSPrjS#k_~Y^XCSBl21`kFVVQ zJn)Se3Qwk^xPk8@8h{{8rhoVt=2`*w%O$ky8Tvs@E8pWfYcwY<%tvmG$3kzia8|$pb8UB;FlFBJ!xcJ)#x9!evhP-Q7eAm>rzd`Qy{5V$a{L<` zN21hVO{XSkV)QUXrOXy{Nr=wgzK;UH7m(R=SylA)K&2)G%`Dfb?U#ju)6(4DxAcVF;>Sb$deNpg^HezU&If zk{(HU2u`?%zxE1-@@5V(>@DQy6YpiCR0wuCF=%WWv$DT2UKXbzCZZ}#F+Us6sV4WD zW7>3(N7?_`V{7_FUAF_c%Yv`3&ohc9`Uu_+JGU^64v3a_by$+b=DS>od@yAyKP37- zDv%bAA_SXvb~m$Kyktm$J_Zuvsx=NRjq>%fZJdJSAI}S~sE9ShSkM_S;nN_rj`4%f zY^DkwFR4qL7ivfTUl|NCOZOv-k+~`^x zR%kL}31bz|^e1IvesoQ$XSd~tPmj5one{D5M#fUm`+M`uU89kP4T3Qj@;XaVOWdUx z);yR9Y^4&_)_-2)<@6^b#j{111&oVBroF~J%i6=d7^yRYT7nBWz1ZFrgt?KwpQZqz z=FyIN9_NZdi35cXWSGK`T7|JbEuJ+mugx|qzVMzsFJK~%x2!TW^Eb3Ly$DroDLn`F zS{4Q}4i8Rh_RO=C^(U~Uq|I-H7>=1YPJAxRvzr;DO^jf+b!`{JW#2aiLr#8#3n=@s zQsovN$Isi)pVu#TtH=#g%b-B@a2xo6FPeA<%MO8du~=RS-yLytS=iK zDYFlbyH`92Y@!fM80BggU&Q)1Hh$bEu)TX}wlWS}>r-mQS*R}+?~p z%sn3JSlyEgEoCZ0*|{yHP(M~? zfxN)KIhiI=Lb%+~5PNc3;i2|+01!3;@F&!Eizut%7bP1YGwG?>ohUT00mp%#&izbP zVI~(-y5CJ$v&c(>G8o&0TgFN%@>1k%^Cl!9dHkpB?XA099j{toywzxZD77X1)3QMs zjiQ%RMkGNQws;L023q%CyG0~aEBbaptWL%g=lc+$Zc(IxM;O5W47H(_yXBzi8f{X)5PF>x*Q>x9*Iby#qjp0 zK#KD5j4fx>wX0G>QhN4bSPNE!A?y5ND@qrVnOxe!2Jm^`W8@E)UatgvAhPJgW2;e9j9bAN+-`kssx$ z_|Ec*JMh9wLXE01OAlw@ktwGFJAE4yydC2cb`oeZ#Lz+pA6$ZRIiQ)suy*;peBB(w zVv8#O+B^9{2Wk*BPJVH+Y^W$!#gOJb6NAlsK=iZJ)$HQCOP56SWQVrfbJ9__ndcPR@LiEC6ST#+*v8A+iuY9Na*eW&GKt`1>EK4Q3BY#oC^(p>;JURc`6#I zJ3{)w7hoC<-*3QLrcoRcM`?(tBzwpKf;=o)z{YZ(=~d!u5^emXj8k zmE+Z&M5#4}zy?$p86ef!k75~_ z10G~x?Q{Aap$AQX;0zK7fJPr)A$98Smmx-LI8Go7>driSAZD(xs~^f{0*OiObkcuN z!w=vYtj#8v3qToNq8_t$U51b7;W+1MPpo1QD0WG$tdD=@CcOKCFgDQ&AcU}3e!W*` zd1gcItsD0J(+Wovis-z`)AtsIckPGnLY`JQvT|^m7;C+9CM3^blH?drDDY_mO>tqE zu*U#56XbH9Vk9gkTfpI650RWWg3wtB^?@2Y&o^K-$gJKHYC5&)LmqnZGGoccsiFhz zEllGm*-bFQuoatK`F&F_2WZO@W2M@PhE6w#+UsPxRJOwgJ#>o?R$I`O`V)LZAUM%t zdwL{T%>=l<(L!wUTyk8HpG|_6^hG&J1s`(1$gGfEaaC~vHQ&j*H7Nv}lmJAA0H_*B zqP!-hFv8=P>WiFvmT}8aiMMuoLG#rH#jg{i$48R+%IlHP!xi;{r4_qGZWT?QwnXof zUp^VQfTB7G$1Xd!9PV{>ohyyV_#~W1jtX)3S?@a>7TruB}DU zbv8pXFRcf^TmmVx?>*_CI#K%;qL!i?>N=`l<@IEOdS*UsKj%be*HjguTTFI_Uebot zK(kd8c9TI8^mP05tksL)AhpzXUwzz@Q5=eL3PY+6%2zjWXN(x%hfnuT!=8KMAJfv8 zD8H|_Xqjy)aW24$OMofrqV>W1M`? zQ<)e9+f9bd2frJCvE&FnY?7Cm+c^eoBKRP{+C2szWyjvjBbO_COsee>K#Sn(`zR0m zn?+#&Gjq7i|1+asX~PtoxYwfXm*=#EJ9}09(}4el#5NNdpX`Kb!V(?SKc{Ms0%jcd zX;S(aP(6iw(h>vn=!@U|n|ZozMpy5*x{kpbzS5k(Ys>D1iX3aV@9NG)+zn9}Pg;Qw zk}84jPMj&>l}Bn1j#J`}$i$SDCp5dK8kM6igxGLQ1$9l$4A92AsDa|N;zL6CKa;YY z5PASMNF!y@(qefWB*n3}zMzFr-h>1nc8b;Hqp|P0s$^1dOB!=CoT5SoUono$U`1nS z5p80!IPu3WlHra#ERhSs_q(8YBq9vaZRdJWPR9+QY1bPaFWV*TSKf}oRp*2s>xt)+ zTolg*32A7Gi-vja=yM&go3}-e45MLA3Co?*IDw4ZBA;Wt98-1eq} zYw>k66DX}IjZAo!^6wMNu#8@Ff+`;_kYOkXp@pF#x}xzB!TCUv;}2FP@=|A+$8Q7d zTBN=Sm4@roVG)?Hxnu|L5s(miydE)`=`jKgDI;Z(^W9d(-Quhix)T%))VgvjfB=Sc zt;r6kb;Jb8oEyudoBI^IN2ETP&>4K51PNA+brGf$78itx12x7oO4k_upeFUG!VuI_ zNG9|eiJ*KB_t#|UZ;Fa*0??mqsnLR&JlPdGOcpG4@-|TKkl~|z?pCm?0XmX z%A8YDdtfK(q;s-AC;b75K%;4@?#o)+@wVDtcQR&>DCg|M`1WG8XSw*{Vh}$%FyFi4 zJ!=^G$LJXQ&=R$06QYc#=&t~IIYYw$Z{brxd9XvDGaR*8@}hoXV5&!6G<4j4-iEN$ zOzvOt%uNZDAsS0*1TvsI><{MGajT^3V5nXhI<4Au`ZNx;P&8*fsIZjVP)r%KJfpK4 zsa6;;;MmoRE%fl%PmNI?(QA<0Ei$OOFn?KZ(kj47gd*k!ic7;1g5LR>@@NK6uR z?JPdVA}^Ulb}KjE;MAC(I`W8+J)?#fc$Dc|ICe315r6*=E)s=wUfF28id&{1zD z|K}k*R)t`Wxyp*)Q=!d_5_yU0vR-&OaC^MSGB)<}=Z-@xy?OPh_ZGGndXz~PjrHPr z+gi#vPS{{&@ghQK1$5;I8$w?8RsX&ijxp)CeIA9F$)+CBh?EI)Tm)1-dgra_*QeRh zb<^B95Z*~g_42RW|l7%eBrVO^1MUG|2cf6na=E{>hvPB zPS;&i6R{B8=e0$#8q<7u2M?tMtATP8rnAzv&vo9a@o}V(*7pU^!1oArZr7=S&!2M= z^-$P3XHoZFF2KwFc8}bsI8)eI1O-8ctLn*ugdmoBR0Z^6qAgFSo=?^q;S5oTCXguXiLe*%5IhB8nPCv1n=o-$>IM%PEIL6TxlzJeCZbVYOF-N%0}{4Jwu-79h0cP z8&vzpCD5(dA}9SygkbwsuiC430&u?N-ruj-q&u|9-e-?ri2!U*(5KGuVlJMZxfB1* zobSUo688@i=a{n?&BZ8rn|wzcD%PYYinqBQ1%9=o-A{qZZ-s= zyZv5!4@GegMX{F?FC?ApG@ctMqsIUXUTMpbxYhN}&?O*9(iP8Ca^(So%(L<9hNAyX zfZ6dzNZCLT##YT?8C5I=4Bndmi}n7}^kNH7SXE%0G9c?xC?jhi;CmnUF8Uo=M>l)N z#$?(rdR`DEn&0^dy{JTa_w98@u0|LS4zZsr!~eZvY7;CCV9>n?^aNzv*UML9=quXm zztOyKJ^R}J&I7#3@nSAh5GIX$&0|OL=A-QURRQyHc9jD2?knGRiIKX}=?9j##l`i% zU!1Vmz3nc|7D(i;45M|G_K!gu7%gTEo+7IY5nXm#AT zdCj0TYCp!iPh^-F)*|M&>1<$zzft!b-1TJRS7M`dc_m7c%*cCW|IWHDqLJV#(_`~! z+PZ!L5WZ}dpr4%q3BmOOx6^R3wcpVH@7K!*^xFBzRfTPoJ8ac_vIsaA8l2m%biKN!FnRp7T=Soi4>CzOrfBMdWi64ca{|7wiTjlxp49FXRIk) zY53DU@VD7rP=0T9&Es*C(kQt4iIm@Ys%uC7^bh(?em;LXhpt;)_Y~)5dOOoeLx9*_ zqQ&tyLdpNH3$$5G^rw;ET}GgZo56TH2YAMV*T#N=5Ls8B|5N?aswwOD(R-a%Bt4WN z1d9%RTEfMXn8&{kg7{N@x^;BhY(BAu^R;eWMWk-FpIP;u_yNGJCnHE{R8!3W7#8x{ z9-#e2Q`Ry<`>p)_1-CL34~=<`d%L}Cm(O~QqhkT_gsaX_20k+dJ+vW&bQsbx-Aaw0 zIh_<~Bg5xG%@Cls>r+&8MP1Grjc5`p?;wY$y4|9gQExEYs)`bE`w*RV6U91rdi1@e zRFvoVGJNT#sD+m=J$1I6I!ryFl^Fv@j2Xl@j3amfQ$0dR`s#s z@0TfkOX&|sqC+Abw8;HYdZkhw+K*A$AM=|v(j?-j==8MTFZP4Zh%N56Ydfld_mQ%* zcIo7OGs&+Q#l_{$bf(1aXaU6XP}6JiijKE?-zEdQYA9IT-*^tmM$E*L|U_rzRx?#A7T{O>qSyk zG`;`Uz6$C6Gbb#ZcF|d@S9AEE7Hg{;t6jZenmj2kcVq6am1fF9*153&lMCKe4(C9Ajx2aD0RqM5~Sywrnm zW&EMM6_S6s*xNQ4tl6C6w=WX0BaI^~tX?WL8YJBn#35s?rk;l;vQwpv&gM=s(O2%e5-t{c z>izJD5d3+L-Y{5ztRnLEDALpCE)S9s$4z`HH3wPt2ckmiDVdbMpIeGp6Vs0Py=grZ zMdstn)?~aV)nC&ZiJb2920O^zE?Zo9RI;~8Wp0vhRWk?;-`2l zO*9oSp*;3_+*)ZA)NIW{g$Y* z*)qD|hjlCo*)G*2FFxte4D{ISxM6UAbWvi58&*M=T2Os}AI%>xdZ(wW*uz{&#{V>(GCJN$K`ZB)COWxG zR{hW`J>a7!?iO8ig$hkj6JqVbUZ3B5#3WBi8ou0Wt-d4~Y~W`zSi5z8E5vRaJb)Z= zhn<$Z9}ND3|F6%pgJQ;9#M+MScgKhN{C)H|6+K@%ZwC8f_3#6dtT?*MCzhuwWt)9~ z->9Auh^u$^*|`RTk;-11U3E89Eq6B=?nw>Q9b)-lDBJ5}GG01mO4~;D!NZ{@M!_;} zo_V|fzRP@`%=~?iXegnHe)fgYN1)a(9G~4iWv$4zHzCStV8wSCVzexM0daqa&(Sh6 zg%h`QNV=(ZwkBp$27GQMeu~XETU!df0~JgBwg^;+%G8*IfqK?KPmoeq&@K(An#LCA zOLs>QA8&iB**1#1{Zg9O?x1 zDj5I^dEKf>->wnCq z^fYj^M}p?EwUV|}LSUUS{i)|eag&FGSdZ~voOQl;wB-}uEaN`De17Bh{c=Lw+EOr!uYvFPe66&+zo*tD-Uxcc69-nzHEvQ^`ct2C! zj-6$Epl9ehhk>)ZpP=Io>T0qLp%+LOz(N;49$g@;DE)YtH|^KPVTKIUl0bUhxC5sr z58qgOGPi-B9_Fi&OKWfO08`ZMOo&l9U9djW$sgDHt%2hJlZRRW6y|?#e1V}NoMDMu znuF#oge6Je2vqWA#4NPI=u7G50|ta&@CRerUT>o>ylW43a_-Nc%YpQt1t}nK@cI#g zoG`@>!e6FTnrUSxBL0kse&MzkTXpoG?IuP{O|rptzryG$?i10KX}Jyzd_sy8b-p)G!qvnQCOja1`Bh)kjd+ z!+>XeS=T#>fuqc4vDDXzGJUU~he|Et(dG6B9$ICt;TNwYETGuUefc5?Av7r+Oa)AEb zd^8!abk}l#F|Y_lWw2