From 70085618c7f2e9cb2bf16deaaee6f168911f33b9 Mon Sep 17 00:00:00 2001 From: Nathan Flurry Date: Sun, 30 Aug 2026 22:42:57 -0700 Subject: [PATCH] Launch Dynamic Apps - Add /dynamic-apps landing page diagrams (routing, repair loop, V8 isolation) and annotated code - Add Introducing Dynamic Apps blog post - Add "Set up with your agent" copy-prompt button to all four product landing pages - Add launch graphics generators under .claude/skills --- .claude/skills/create-launch-post/SKILL.md | 266 +++++++++ .../create-launch-post/agents/openai.yaml | 4 + .../assets/logos/sqlite.svg | 1 + .../scripts/render-launch-images.ts | 550 +++++++++++++++++ .../scripts/render-technical-image.ts | 117 ++++ .../scripts/render-tile-images.ts | 274 +++++++++ .claude/skills/launch-graphics/SKILL.md | 29 + .../skills/launch-graphics/graphics/README.md | 47 ++ .../graphics/assets/NimbusSans-Bold.otf | Bin 0 -> 83264 bytes .../graphics/assets/actors.svg | 31 + .../graphics/assets/sqlite.svg | 1 + .../launch-graphics/graphics/assets/v8.svg | 1 + .../graphics/render-dynamic-apps-launch.mjs | 324 ++++++++++ .../render-dynamic-apps-technical.mjs | 124 ++++ .../graphics/render-launch.mjs | 150 +++++ .../graphics/render-technical.mjs | 129 ++++ .../graphics/snippets-dynamic-apps.json | 17 + .../launch-graphics/graphics/snippets.json | 17 + package.json | 6 +- public/images/registry/v8.svg | 2 + .../marketing/SetupWithAgentButton.tsx | 68 +++ .../marketing/agentos/AgentOSPage.tsx | 36 +- .../diagrams/dynamicAppsDiagrams.tsx | 551 ++++++++++++++---- .../dynamic-apps/AppsAnnotatedCode.tsx | 69 +++ .../dynamic-apps/AppsVisualization.tsx | 294 +++++----- .../marketing/workflows/WorkflowCodeTabs.tsx | 2 +- .../page.mdx | 215 +++++++ src/data/agentSetupPrompt.ts | 57 ++ src/pages/actors.astro | 6 +- src/pages/dynamic-apps.astro | 269 +++++---- src/pages/workflows.astro | 18 +- src/sitemap/product-metadata.ts | 2 +- src/styles/main.css | 42 ++ 33 files changed, 3320 insertions(+), 399 deletions(-) create mode 100644 .claude/skills/create-launch-post/SKILL.md create mode 100644 .claude/skills/create-launch-post/agents/openai.yaml create mode 100644 .claude/skills/create-launch-post/assets/logos/sqlite.svg create mode 100644 .claude/skills/create-launch-post/scripts/render-launch-images.ts create mode 100644 .claude/skills/create-launch-post/scripts/render-technical-image.ts create mode 100644 .claude/skills/create-launch-post/scripts/render-tile-images.ts create mode 100644 .claude/skills/launch-graphics/SKILL.md create mode 100644 .claude/skills/launch-graphics/graphics/README.md create mode 100644 .claude/skills/launch-graphics/graphics/assets/NimbusSans-Bold.otf create mode 100644 .claude/skills/launch-graphics/graphics/assets/actors.svg create mode 100644 .claude/skills/launch-graphics/graphics/assets/sqlite.svg create mode 100644 .claude/skills/launch-graphics/graphics/assets/v8.svg create mode 100644 .claude/skills/launch-graphics/graphics/render-dynamic-apps-launch.mjs create mode 100644 .claude/skills/launch-graphics/graphics/render-dynamic-apps-technical.mjs create mode 100644 .claude/skills/launch-graphics/graphics/render-launch.mjs create mode 100644 .claude/skills/launch-graphics/graphics/render-technical.mjs create mode 100644 .claude/skills/launch-graphics/graphics/snippets-dynamic-apps.json create mode 100644 .claude/skills/launch-graphics/graphics/snippets.json create mode 100644 public/images/registry/v8.svg create mode 100644 src/components/marketing/SetupWithAgentButton.tsx create mode 100644 src/components/marketing/dynamic-apps/AppsAnnotatedCode.tsx create mode 100644 src/content/posts/2026-08-31-introducing-dynamic-apps/page.mdx create mode 100644 src/data/agentSetupPrompt.ts diff --git a/.claude/skills/create-launch-post/SKILL.md b/.claude/skills/create-launch-post/SKILL.md new file mode 100644 index 00000000..1ecff2f2 --- /dev/null +++ b/.claude/skills/create-launch-post/SKILL.md @@ -0,0 +1,266 @@ +--- +name: create-launch-post +description: Create and publish concise Rivet launch or changelog posts through an approval-gated workflow, including release research, a required user-selected hero variation, local HTML-rendered social and technical images, an R2 hero upload, MDX authoring, a reviewed draft GitHub PR for manual merge, and a scheduled and verified Buffer thread for @rivet_dev. Use when asked to launch, announce, ship, or prepare and schedule a Rivet feature or release announcement. +--- + +# Create Launch Post + +Prepare a complete, deliberately short launch. Keep the launch date, hero variation, social copy, Buffer schedule, and final GitHub merge as user-owned checkpoints. Opening the draft PR is not completion. + +## Workflow contract + +Run these phases in order: + +1. Accept the user's launch context, research the release, confirm the launch date, and ask the user which hero variation to use. Never silently choose the variation or the painting. +2. Render the 2:1 blog hero and social image. Upload only the blog hero; keep social and technical assets local. +3. Write and validate the blog post, upload the hero, and push an initial draft PR. +4. Give the user one review packet containing the draft PR, social image, first technical-image draft, four Buffer main-post options, and the proposed reply thread. If the feature has meaningful Dashboard, Inspector, or other product UI, recommend adding a clean screenshot and ask the user to provide it or approve capturing it. Iterate on the post, PR, technical image, optional UI screenshot, and social copy until the user approves them. Push every approved blog revision to the same PR. +5. After the user approves the copy, replies, launch date, and exact time, schedule the Buffer thread and reopen it to verify the account, content, image, order, date, time, and timezone. Leave the GitHub PR for the user to merge manually. + +Do not declare the launch ready until all three acceptance gates pass: + +- **Blog post:** The user has reviewed it, the latest approved revision is pushed to the draft PR, the hero resolves, and relevant checks pass or any external blocker is explicit. +- **Technical image:** The user has reviewed and approved the current local `technical.png`. +- **Buffer:** The approved thread is scheduled for `rivet_dev` and the scheduled item has been verified. If Buffer references a tunneled media URL, a reliable tunnel process and cleanup plan must remain active through publication. + +## Parallelize with subagents + +Use subagents in bounded waves when concurrency is available. Keep the main agent as the only user-facing orchestrator and the sole owner of tracked-file edits, asset uploads, `jj` operations, GitHub writes, Buffer mutations, and tunnel lifecycle. Give subagents read-only repository work or a unique output directory outside the repository; never let multiple agents edit the same worktree or perform the same external action. + +Start wave 1 while the main agent asks for the hero variation and confirms the date: + +1. **Release researcher:** Inspect the source PR, implementation, tests, examples, and docs. Return verified public API syntax, constraints, shipped terminology, and candidate documentation and GitHub links with file or URL evidence. +2. **Post-format researcher:** Review at least three recent concise Rivet changelog posts. Return the shared structure, tone, frontmatter conventions, slug rules, and useful examples without editing files. + +After the hero variation and release facts are available, start wave 2 in parallel: + +1. **Visual worker:** Run the bundled renderer in a unique external output directory. Produce the hero, social image, and first technical-image draft; report paths, dimensions, checksums, and visual concerns. Never upload or commit them. +2. **Blog-draft worker:** Return a concise MDX draft grounded in the verified API and generic launch structure. Do not write the tracked post file. +3. **Distribution worker:** Draft four main Buffer options, the four link replies, and scripted character counts using the required Unicode-code and arrow formatting. Do not schedule anything. + +The main agent must reconcile those outputs, verify facts against source, write the MDX, choose what to show the user, and own the approval loop. After integration, use one read-only **QA subagent** to inspect the final diff, post code, URLs, frontmatter/date, rendered images, Buffer payload, and `jj status`. Require it to report concrete defects only; the main agent applies any fixes and reruns focused checks. + +Do not delegate user checkpoints or final side effects. The main agent must personally confirm the hero variation, date, copy, thread, exact schedule, PR state, account/channel, media, and final verification. + +## 1. Work locally and research the release + +1. Read the repository `AGENTS.md` files that govern the files in scope. +2. Work from the local checkout at `~/rivet`; never author repository files through GitHub's contents or object APIs. Reuse a user-provided isolated workspace, or create one before editing: + +```bash +jj -R ~/rivet git fetch +jj -R ~/rivet workspace add \ + --name "launch-" \ + --revision "main@origin" \ + --message "docs(website): launch " \ + "$WORKSPACE_PATH" +cd "$WORKSPACE_PATH" +``` + +3. Run `jj status` and keep the launch revision isolated from unrelated work. Put generated and downloaded files outside the repository workspace. +4. Read the source PR, changed docs, examples, tests, and public API. Treat implemented code and merged docs as authoritative; do not infer syntax from the PR title. +5. Read at least three recent concise posts in `src/content/posts/`, preferring `category: changelog` posts for similar features. +6. Resolve the canonical post URL, public docs route, GitHub repository, and Discord URL before drafting. The route preserves the complete dated post folder name. If the file is `src/content/posts/YYYY-MM-DD-/page.mdx`, its post ID is `YYYY-MM-DD-` and its URL is: + +```text +category: changelog β†’ https://rivet.dev/changelog/YYYY-MM-DD-/ +all other categories β†’ https://rivet.dev/blog/YYYY-MM-DD-/ +``` + +Do not remove the date prefix. The Astro routes derive the slug with `entry.id.replace(/\/page$/, "")`, which removes only `/page` and preserves the full folder name. + +7. Propose the target launch date and get explicit user confirmation. Use that date consistently for the folder name, `published` frontmatter, asset key, canonical URL, and Buffer schedule. + +## 2. Render the launch images + +Ask the user which hero variation to use before rendering anything. This is a required creative checkpoint; never choose the variation silently. Both variations write `image.png` at `2048x1024` for the blog hero and `social.png` at `2048x1238` for distribution, so every later phase is identical regardless of which one is chosen. Use the repository renderers; do not use Figma. + +### Variation A β€” painting + +The default editorial treatment: a full-bleed painting with the launch title beneath it. Best when the launch is a product or capability announcement with no natural logo lockup. + +1. Ask the user to choose and provide a painting. Ask for the source or confirm that it is safe to publish. +2. Pass a short feature name such as `Cron Jobs` as the social title: + +```bash +pnpm render-launch-images -- \ + --painting "$PAINTING_PATH" \ + --title "$SOCIAL_TITLE" \ + --output-dir "$OUTPUT_DIR" +``` + +3. If the source is already 2:1, preserve its composition and only normalize it to `2048x1024`. Otherwise, visually inspect both PNGs and rerun with `--focal-x <0..1>` and/or `--focal-y <0..1>` if the crop misses the subject. Do not hand-edit the output. + +### Variation B β€” logo tiles + +The Rivet wordmark above the launch title, over two to four product logos in rotated rounded "app tiles" with the middle tile dominant. Best when the launch is an integration or a combination of named technologies, where the logo lockup states the story faster than prose. The tile treatment matches the agentOS launch graphics and the landing page's floating-agent tiles. + +1. Ask the user which logos belong in the lockup and in what order. Order is left to right, and with three tiles the middle one is the largest, so put the subject of the launch in the middle. +2. Collect an SVG per logo. Reuse repository marks where they exist (`src/images/products/`, `public/images/registry/`), fall back to the vendored marks in `.claude/skills/create-launch-post/assets/logos/`, and vendor any new mark there. Marks authored white for dark UIs are recolored to the site ink automatically, so a white-on-transparent SVG is fine. +3. Pass the full post title rather than a short feature name; this layout has room for a sentence-length title: + +```bash +pnpm render-tile-images -- \ + --title "$POST_TITLE" \ + --logos "$LOGO_A,$LOGO_B,$LOGO_C" \ + --output-dir "$OUTPUT_DIR" +``` + +4. Inspect both PNGs. If a title wraps to a third line or a tile crowds the title, shorten the title rather than hand-editing the output. Tile geometry lives in `TILE_LAYOUTS` in the renderer; change it there if a lockup genuinely needs different placement. + +When short code demonstrations would help launch distribution, create a temporary JSON file outside the repository with one to four sections: + +```json +[ + { + "title": "Crontab with timezone", + "language": "ts", + "code": "await c.cron.set({ /* ... */ });" + } +] +``` + +Render the technical image directly without generating hero or social assets: + +```bash +pnpm render-technical-image -- \ + --title "Cron Jobs" \ + --technical-snippets "$TECHNICAL_SNIPPETS_PATH" \ + --output "$OUTPUT_DIR/technical.png" +``` + +Use `--layout vertical` to stack every snippet at full width; the default `grid` layout uses the launch-image column rules. Pass `--browser /path/to/chromium` only when neither bundled nor common system Chromium installations are available automatically. Normally create the first technical draft immediately after opening the initial blog PR, once the code example is stable enough to summarize. The renderer writes the PNG and adjacent HTML at a fixed `2048px` width with content-driven height, so short examples produce a shorter image. The white technical image places the Perfectly Nineties launch title at top left and the Rivet logo at top right, with matching 132px side and bottom gutters. Section headings use Manrope; code uses four-space tabs plus the website's JetBrains Mono font, Shiki configuration, customized Ayu Dark theme, and code-block geometry. With three grid sections, the first section fills the left column while the other two stack on the right. Every dark code panel stretches to the full available section height so column bottoms align. Keep snippets brief enough to fit, show it to the user, and rerender until approved. + +When the feature has useful Dashboard, Inspector, or other visible product UI, proactively recommend a supporting screenshot. Ask the user to provide the screenshot or approve the exact authenticated view and state to capture. Use a clean launch-ready environment, crop away irrelevant browser chrome, and inspect the result for credentials, private names, IDs, tokens, customer data, or other sensitive information. Keep the screenshot local and show it to the user for approval. Confirm whether it should be attached to the first Buffer post or used in a dedicated reply; do not add it automatically. + +Do not add `image: true` to the post until the uploaded asset resolves successfully. + +## 3. Upload the hero + +Use the complete dated post folder name as `` and upload the blog hero: + +```bash +AWS_ACCESS_KEY_ID='op://Engineering/rivet-assets R2 Upload/username' \ +AWS_SECRET_ACCESS_KEY='op://Engineering/rivet-assets R2 Upload/password' \ +op run -- aws s3 cp "$IMAGE_PATH" \ + "s3://rivet-assets/website/blog//image.png" \ + --endpoint-url https://2a94c6a0ced8d35ea63cddc86c2681e7.r2.cloudflarestorage.com +``` + +Verify `https://assets.rivet.dev/website/blog//image.png`, then add `image: true` to frontmatter. Never commit the exported hero to Git and never print resolved credentials. + +Return `social.png`, optional `technical.png`, and any approved UI screenshot to the user as local launch-distribution assets. Never upload or commit distribution images, their HTML source, the technical-snippet JSON, the UI screenshot, or the painting source. + +## 4. Bootstrap the MDX + +Run the repository generator: + +```bash +pnpm new-post +``` + +For a future launch date, bootstrap normally, then rename the generated folder and update `published` to `YYYY-MM-DD`. The final path is: + +```text +src/content/posts/YYYY-MM-DD-/page.mdx +``` + +Use `author: nathan-flurry` unless the user specifies another author, `category: changelog`, and targeted lowercase keywords. + +## 5. Write the generic launch format + +Keep the body brief and use this order: + +1. One short paragraph explaining what shipped and why it matters. +2. Short capability-focused H2 sections with one small example each, copied from or checked against the release. Give distinct public APIs separate sections when that makes the launch easier to scan; for example: crontab with timezone, fixed intervals, run history, and one-off jobs. +3. Use `## Show Me The Code` only when one cohesive example explains the feature better than separate API sections. +4. `## More Links` with docs, the relevant GitHub repository, and `https://rivet.dev/discord`. + +Aim for under 200 prose words excluding code. Include all TypeScript imports and referenced definitions. Avoid roadmap claims, implementation detail, a long feature inventory, or repeated calls to action. + +## 6. Validate + +1. Confirm the folder date, `published`, title, description, keywords, and image slug agree. +2. Check the code against the source PR and current documentation. +3. Run the narrowest available website formatting/type checks, then `pnpm build` when dependencies and checkout scope allow it. +4. Review `jj diff` and `jj status`; keep unrelated changes out of the revision. +5. Confirm the blog image URL returns successfully before leaving `image: true` enabled. +6. Derive the canonical URL from the post file instead of rewriting the slug by hand: + +```bash +POST_FILE="src/content/posts/YYYY-MM-DD-/page.mdx" +POST_ID="$(basename "$(dirname "$POST_FILE")")" +POST_CATEGORY="$(awk '/^category:/ { print $2; exit }' "$POST_FILE")" +POST_SECTION="blog" +if [ "$POST_CATEGORY" = "changelog" ]; then + POST_SECTION="changelog" +fi +CANONICAL_URL="https://rivet.dev/$POST_SECTION/$POST_ID/" +printf '%s\n' "$CANONICAL_URL" +``` + +Cross-check the result against `src/pages/changelog/[...slug].astro` or `src/pages/blog/[...slug].astro`, and use the exact same URL in the Buffer reply. If the post is deployed, require the exact URL including its trailing slash to return HTTP 200. Also probe any shorter hand-written alternative; a 404 is evidence that it must not be used. For an unmerged future post, verify the source-derived route before scheduling and repeat the HTTP check after deployment but before publication. + +## 7. Open a draft PR and continue the review loop + +Only push when the user has requested publishing to GitHub. Create an `agent/` bookmark for the revision, push that bookmark, and open a draft PR against `main`. Use a conventional PR title and a short bullet-list body covering the post, artwork, skill changes, and validation. + +Return the PR URL and explicitly tell the user that they must review and merge it manually. Continue by drafting the technical image and Buffer options; do not treat PR creation as the end of the task. Apply the user's blog feedback locally, rerun relevant checks, and push the updated revision to the same PR. Never merge the launch PR on the user's behalf. + +## 8. Prepare the @rivet_dev Buffer thread + +Draft the Buffer options after the initial blog PR and technical image exist. Do not schedule anything until the user approves the blog post, technical image, main copy, reply thread, date, and exact time. + +1. Draft four distinct options for the main `@rivet_dev` post. Match the concise launch style: a direct opening line, the core value, and a few specific emoji-led points when useful. Do not put a URL in the main post. +2. Prefer a code-first structure when the API surface is central to the launch. Surface the API bullets before supporting prose or secondary benefits so readers see the shipped interface immediately. +3. Render short code identifiers with Unicode Mathematical Monospace characters instead of backticks. For example, render `c.cron.set` as `𝚌.πšŒπš›πš˜πš—.𝚜𝚎𝚝` and `c.schedule.after` as `𝚌.πšœπšŒπš‘πšŽπšπšžπš•πšŽ.πšŠπšπšπšŽπš›`. Apply the treatment only to code fragments, preserve punctuation such as periods, and keep normal prose unstyled. Verify that Buffer renders every identifier legibly; fall back to ASCII if it does not. +4. Format API bullets as ` β†’ `. Use the right arrow, not an em dash, and keep the benefit lowercase unless it contains a proper noun. +5. Keep every option strictly below X's 280-character limit. Target at most 250 Unicode code points and show the count beside each option. Before presenting any option, run this sanity check with the complete candidate text, including newlines and emoji; do not present or schedule text when the script exits nonzero: + +```bash +LAUNCH_POST_TEXT="$CANDIDATE_TEXT" node - <<'NODE' +const text = process.env.LAUNCH_POST_TEXT ?? ""; +const count = Array.from(text).length; +console.log(`${count} Unicode code points`); +if (count >= 280) process.exit(1); +NODE +``` + +Treat Buffer's composer counter as authoritative before scheduling because platform weighting can differ from the sanity check. Shorten any option Buffer reports as 280 or more. + +6. Draft these as four separate follow-up replies, using resolved URLs rather than placeholders: + +```text +Read more: +GitHub: +Documentation: +Discord: https://rivet.dev/discord +``` + +7. Ask the user to choose one main-post option or provide their own, edit or approve the four replies, and confirm the launch date. Once the user selects or supplies the copy, treat that version as canonical: stop proposing alternatives and change only the parts they request. +8. Select a random whole-minute time from 7:30 through 8:30 AM in `America/Los_Angeles`, show the exact date, time, and timezone, and get explicit confirmation. The date must match the post's `published` date. + +After approval, prefer the Executor `buffer_mcp` integration and fall back to the authenticated Buffer web app only when the MCP lacks a required capability: + +1. Load the Executor `execute` guidance, search the `buffer_mcp` catalog, and use the exact connected tool paths returned by search. Do not guess paths or call Buffer's raw API. +2. Call `get_account` first. If it returns multiple organizations, list them and get the user's choice; otherwise use the sole organization. Call `list_channels`, select the exact `rivet_dev` Twitter channel, then call `get_channel` and verify it is connected, unlocked, and configured for `America/Los_Angeles`. +3. Call `list_posts` for that channel and launch date with `scheduled` and `sending` statuses. Stop if the same launch is already present instead of creating a duplicate. +4. Call `create_post` with `schedulingType: "automatic"`, `mode: "customScheduled"`, and an ISO-8601 `dueAt` carrying the Pacific UTC offset. Put the approved main copy in both the outer `text` field and the first `metadata.twitter.thread` item; include all replies in that thread array. Put the approved `social.png` asset in both the outer `assets` array and the first `metadata.twitter.thread[0].assets` array, with no link in its text. Buffer may drop threaded media if it is supplied in only one location. Do not use `technical.png` instead or attach an optional dashboard screenshot unless the user explicitly approves that media and placement. +5. Add the approved Read more, GitHub, Documentation, and Discord replies in that order. Do not collapse them into the main post. +6. Call `get_post` and verify the post ID, `status: scheduled`, exact `rivet_dev` channel ID, complete text and reply order, due time, image source, dimensions and alt text, and `error: null`. Confirm the returned UTC time converts to the approved Pacific time. +7. When changing a scheduled post, call `get_post` first and require `updatePost` in `allowedActions`. Buffer's `edit_post` is a whole-post replacement, not a patch: carry forward the scheduling mode, due time, all replies, metadata, and approved assets, then change only what the user requested. For a Twitter thread, include the existing image in both outer `assets` and `metadata.twitter.thread[0].assets`; immediately call `get_post` and repair the post before continuing if its returned `assets` array is empty. Run the character sanity check again and call `get_post` after editing. +8. Return the Buffer post ID or URL and the verified schedule. Treat schedule-and-verify as the required Buffer publication gate. + +Prefer a direct media upload when the Buffer integration provides one. If the Executor Buffer MCP only accepts an image URL and has no upload tool, use this temporary single-file handoff instead of uploading the distribution image to Git or Rivet assets: + +1. Create a temporary directory outside the repository containing only the approved image, and serve it from a loopback-only HTTP server on an unused high port. Never expose the repository or the full output directory. +2. Open a temporary public tunnel to that loopback server. Before scheduling, download the tunneled URL and confirm its SHA-256 checksum and dimensions match the local image. +3. Pass the temporary image URL to Buffer with dimensions and descriptive alt text. Buffer retains this source URL; a successful `HEAD` or `GET` during creation does not prove that Buffer copied the image. +4. Reopen the scheduled post through Buffer and verify `status: scheduled`, the channel, complete thread, due time, image URL, type and dimensions, and alt text. Load the image URL independently to confirm it still returns the approved file. +5. Keep the loopback server and tunnel reachable until the post has published. After Buffer reports `status: sent` and the published post renders the image, stop both processes and delete the temporary directory. If reliable tunnel uptime through publication is not possible, do not use a quick tunnel; use Buffer's authenticated direct-upload UI or a user-approved stable host instead. + +Never commit the distribution image or tunnel URL, and never upload it to Rivet assets without explicit user approval. + +If Buffer access, the verified company account, or threaded scheduling is unavailable, return the approved copy and exact schedule as a handoff. Never claim that the thread was scheduled. + +If the hero variation, painting, logo marks, asset credentials, or release API is missing, complete the safe independent work and report the exact remaining checkpoint instead of fabricating it. diff --git a/.claude/skills/create-launch-post/agents/openai.yaml b/.claude/skills/create-launch-post/agents/openai.yaml new file mode 100644 index 00000000..f17ce711 --- /dev/null +++ b/.claude/skills/create-launch-post/agents/openai.yaml @@ -0,0 +1,4 @@ +interface: + display_name: "Create Launch Post" + short_description: "Prepare and verify complete Rivet launches" + default_prompt: "Use $create-launch-post to prepare a concise Rivet launch, iterate with me on the blog and technical image, update a draft PR for my manual merge, and schedule the approved Buffer thread." diff --git a/.claude/skills/create-launch-post/assets/logos/sqlite.svg b/.claude/skills/create-launch-post/assets/logos/sqlite.svg new file mode 100644 index 00000000..e5e0d013 --- /dev/null +++ b/.claude/skills/create-launch-post/assets/logos/sqlite.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/.claude/skills/create-launch-post/scripts/render-launch-images.ts b/.claude/skills/create-launch-post/scripts/render-launch-images.ts new file mode 100644 index 00000000..d367d3a6 --- /dev/null +++ b/.claude/skills/create-launch-post/scripts/render-launch-images.ts @@ -0,0 +1,550 @@ +import { mkdir, readFile, writeFile } from "node:fs/promises"; +import path from "node:path"; +import { fileURLToPath, pathToFileURL } from "node:url"; +import { chromium } from "playwright"; +import sharp from "sharp"; +import { getSingletonHighlighter, type BundledLanguage } from "shiki"; +import theme from "../../../../src/lib/textmate-code-theme"; + +const BLOG_WIDTH = 2048; +const BLOG_HEIGHT = 1024; +const SOCIAL_WIDTH = 2048; +const SOCIAL_HEIGHT = 1238; +const SCRIPT_DIR = path.dirname(fileURLToPath(import.meta.url)); +const REPO_ROOT = path.resolve(SCRIPT_DIR, "../../../.."); +const WEBSITE_DIR = REPO_ROOT; + +interface Options { + painting: string; + title: string; + outputDir: string; + focalX: number; + focalY: number; + technicalSnippets?: string; +} + +export type TechnicalLayout = "grid" | "vertical"; + +export interface TechnicalHtmlOptions { + title: string; + technicalSnippets?: string; + technicalLayout?: TechnicalLayout; +} + +interface CodeSection { + title: string; + language: BundledLanguage; + code: string; +} + +function parseArgs(argv: string[]): Options { + const args = argv[0] === "--" ? argv.slice(1) : argv; + const values = new Map(); + for (let index = 0; index < args.length; index += 2) { + const key = args[index]; + const value = args[index + 1]; + if (!key?.startsWith("--") || value === undefined) { + throw new Error(`Invalid argument near ${key ?? "end of command"}`); + } + values.set(key.slice(2), value); + } + + const painting = values.get("painting"); + const title = values.get("title"); + const outputDir = values.get("output-dir"); + if (!painting || !title || !outputDir) { + throw new Error( + "Usage: pnpm render-launch-images -- --painting --title --output-dir [--focal-x 0.5] [--focal-y 0.5] [--technical-snippets ]", + ); + } + + const focalX = Number(values.get("focal-x") ?? "0.5"); + const focalY = Number(values.get("focal-y") ?? "0.5"); + if (![focalX, focalY].every((value) => value >= 0 && value <= 1)) { + throw new Error("--focal-x and --focal-y must be between 0 and 1"); + } + + return { + painting: path.resolve(painting), + title, + outputDir: path.resolve(outputDir), + focalX, + focalY, + technicalSnippets: values.get("technical-snippets") + ? path.resolve(values.get("technical-snippets")!) + : undefined, + }; +} + +function escapeHtml(value: string): string { + return value + .replaceAll("&", "&") + .replaceAll("<", "<") + .replaceAll(">", ">") + .replaceAll('"', """) + .replaceAll("'", "'"); +} + +function dataUrl(mime: string, bytes: Buffer): string { + return `data:${mime};base64,${bytes.toString("base64")}`; +} + +async function cropPainting( + options: Options, + outputPath: string, +): Promise { + const metadata = await sharp(options.painting).metadata(); + if (!metadata.width || !metadata.height) { + throw new Error( + `Could not read painting dimensions: ${options.painting}`, + ); + } + + const targetRatio = BLOG_WIDTH / BLOG_HEIGHT; + const sourceRatio = metadata.width / metadata.height; + let width = metadata.width; + let height = metadata.height; + let left = 0; + let top = 0; + + if (sourceRatio > targetRatio) { + width = Math.round(metadata.height * targetRatio); + left = Math.round( + Math.min( + metadata.width - width, + Math.max(0, metadata.width * options.focalX - width / 2), + ), + ); + } else { + height = Math.round(metadata.width / targetRatio); + top = Math.round( + Math.min( + metadata.height - height, + Math.max(0, metadata.height * options.focalY - height / 2), + ), + ); + } + + await sharp(options.painting) + .extract({ left, top, width, height }) + .resize(BLOG_WIDTH, BLOG_HEIGHT) + .png() + .toFile(outputPath); +} + +async function buildSocialHtml( + options: Options, + blogImage: Buffer, +): Promise { + const [font, logo] = await Promise.all([ + readFile( + path.join( + WEBSITE_DIR, + "public/fonts/perfectly-nineties/PerfectlyNineties-Semibold.otf", + ), + ), + readFile( + path.join( + WEBSITE_DIR, + "src/images/rivet-logos/icon-text-black.svg", + ), + "utf8", + ), + ]); + + return ` + + + + + + + +
+ +

${escapeHtml(options.title)}

+ +
+ +`; +} + +async function readCodeSections(inputPath: string): Promise { + const input = JSON.parse(await readFile(inputPath, "utf8")) as unknown; + if (!Array.isArray(input) || input.length === 0 || input.length > 4) { + throw new Error( + "--technical-snippets must contain a JSON array with one to four sections", + ); + } + + return input.map((section, index) => { + if ( + typeof section !== "object" || + section === null || + !("title" in section) || + typeof section.title !== "string" || + !("code" in section) || + typeof section.code !== "string" || + ("language" in section && typeof section.language !== "string") + ) { + throw new Error(`Invalid code section at index ${index}`); + } + + return { + title: section.title, + language: ("language" in section + ? section.language + : "ts") as BundledLanguage, + code: section.code, + }; + }); +} + +export async function buildTechnicalHtml( + options: TechnicalHtmlOptions, +): Promise { + if (!options.technicalSnippets) return undefined; + + const sections = await readCodeSections(options.technicalSnippets); + const [monoFont, sansFont, displayFont, logo] = await Promise.all([ + readFile( + path.join( + WEBSITE_DIR, + "public/fonts/jetbrains-mono/JetBrainsMono-Variable-latin.woff2", + ), + ), + readFile( + path.join( + WEBSITE_DIR, + "public/fonts/manrope/Manrope-Variable-latin.woff2", + ), + ), + readFile( + path.join( + WEBSITE_DIR, + "public/fonts/perfectly-nineties/PerfectlyNineties-Semibold.otf", + ), + ), + readFile( + path.join( + WEBSITE_DIR, + "src/images/rivet-logos/icon-text-black.svg", + ), + "utf8", + ), + ]); + const highlighter = await getSingletonHighlighter({ + langs: [...new Set(sections.map(({ language }) => language))], + themes: [theme], + }); + const renderedSections = sections + .map(({ title, language, code }) => { + const highlighted = highlighter.codeToHtml(code, { + lang: language, + theme: theme.name, + }); + return `
+

${escapeHtml(title)}

+
${highlighted}
+
`; + }) + .join("\n"); + + return ` + + + + + + + +
+
+

${escapeHtml(options.title)}

+ +
+
${renderedSections}
+
+ +`; +} + +export async function captureHtml( + browser: Awaited>, + html: string, + outputPath: string, + contentSelector?: string, + dynamicHeight = false, +): Promise { + const page = await browser.newPage({ + viewport: { width: SOCIAL_WIDTH, height: SOCIAL_HEIGHT }, + deviceScaleFactor: 1, + }); + try { + await page.setContent(html, { waitUntil: "load" }); + await page.evaluate(() => document.fonts.ready); + if (dynamicHeight) { + const height = await page.$eval(".card", (card) => + Math.ceil(card.getBoundingClientRect().height), + ); + await page.setViewportSize({ width: SOCIAL_WIDTH, height }); + } + if (contentSelector) { + const overflowingBlock = await page.$$eval( + ".code-block", + (blocks) => + blocks + .map((block, index) => ({ + index, + overflow: block.scrollWidth - block.clientWidth, + })) + .find(({ overflow }) => overflow > 1), + ); + if (overflowingBlock) { + throw new Error( + `Code section ${overflowingBlock.index + 1} overflows horizontally by ${Math.ceil(overflowingBlock.overflow)}px; shorten or wrap the snippet`, + ); + } + } + await page.screenshot({ path: outputPath, fullPage: false }); + } finally { + await page.close(); + } +} + +async function main(): Promise { + const options = parseArgs(process.argv.slice(2)); + await mkdir(options.outputDir, { recursive: true }); + + const blogPath = path.join(options.outputDir, "image.png"); + const socialPath = path.join(options.outputDir, "social.png"); + const htmlPath = path.join(options.outputDir, "social.html"); + const technicalPath = path.join(options.outputDir, "technical.png"); + const technicalHtmlPath = path.join(options.outputDir, "technical.html"); + + await cropPainting(options, blogPath); + const blogImage = await readFile(blogPath); + const html = await buildSocialHtml(options, blogImage); + await writeFile(htmlPath, html); + const technicalHtml = await buildTechnicalHtml(options); + if (technicalHtml) await writeFile(technicalHtmlPath, technicalHtml); + + const browser = await chromium.launch({ headless: true }); + try { + await captureHtml(browser, html, socialPath); + if (technicalHtml) { + await captureHtml( + browser, + technicalHtml, + technicalPath, + ".sections", + true, + ); + } + } finally { + await browser.close(); + } + + process.stdout.write( + `${JSON.stringify( + { + blogPath, + socialPath, + htmlPath, + ...(technicalHtml ? { technicalPath, technicalHtmlPath } : {}), + }, + null, + 2, + )}\n`, + ); +} + +if ( + process.argv[1] && + import.meta.url === pathToFileURL(process.argv[1]).href +) { + main().catch((error: unknown) => { + console.error(error); + process.exitCode = 1; + }); +} diff --git a/.claude/skills/create-launch-post/scripts/render-technical-image.ts b/.claude/skills/create-launch-post/scripts/render-technical-image.ts new file mode 100644 index 00000000..a5fb074a --- /dev/null +++ b/.claude/skills/create-launch-post/scripts/render-technical-image.ts @@ -0,0 +1,117 @@ +import { access, mkdir, writeFile } from "node:fs/promises"; +import path from "node:path"; +import { chromium } from "playwright"; +import { + buildTechnicalHtml, + captureHtml, + type TechnicalLayout, +} from "./render-launch-images"; + +interface Options { + title: string; + technicalSnippets: string; + output: string; + layout: TechnicalLayout; + browser?: string; +} + +function parseArgs(argv: string[]): Options { + const args = argv[0] === "--" ? argv.slice(1) : argv; + const values = new Map(); + for (let index = 0; index < args.length; index += 2) { + const key = args[index]; + const value = args[index + 1]; + if (!key?.startsWith("--") || value === undefined) { + throw new Error(`Invalid argument near ${key ?? "end of command"}`); + } + values.set(key.slice(2), value); + } + + const title = values.get("title"); + const technicalSnippets = values.get("technical-snippets"); + const output = values.get("output"); + if (!title || !technicalSnippets || !output) { + throw new Error( + "Usage: pnpm render-technical-image -- --title --technical-snippets --output [--layout grid|vertical] [--browser ]", + ); + } + + const layout = values.get("layout") ?? "grid"; + if (layout !== "grid" && layout !== "vertical") { + throw new Error("--layout must be either grid or vertical"); + } + if (path.extname(output).toLowerCase() !== ".png") { + throw new Error("--output must end in .png"); + } + + return { + title, + technicalSnippets: path.resolve(technicalSnippets), + output: path.resolve(output), + layout, + browser: values.get("browser") + ? path.resolve(values.get("browser")!) + : undefined, + }; +} + +async function launchBrowser(browserPath?: string) { + if (browserPath) { + return chromium.launch({ headless: true, executablePath: browserPath }); + } + + try { + return await chromium.launch({ headless: true }); + } catch (error) { + for (const candidate of [ + "/usr/bin/chromium", + "/usr/bin/chromium-browser", + "/usr/bin/google-chrome", + ]) { + try { + await access(candidate); + process.stderr.write( + `Bundled Chromium unavailable; using ${candidate}\n`, + ); + return chromium.launch({ + headless: true, + executablePath: candidate, + }); + } catch { + // Try the next system browser. + } + } + throw error; + } +} + +async function main(): Promise { + const options = parseArgs(process.argv.slice(2)); + await mkdir(path.dirname(options.output), { recursive: true }); + + const html = await buildTechnicalHtml({ + title: options.title, + technicalSnippets: options.technicalSnippets, + technicalLayout: options.layout, + }); + if (!html) throw new Error("Technical snippets are required"); + + const htmlPath = options.output.replace(/\.png$/i, ".html"); + await writeFile(htmlPath, html); + + const browser = await launchBrowser(options.browser); + try { + await captureHtml(browser, html, options.output, ".sections", true); + } finally { + await browser.close(); + } + + process.stdout.write( + `${JSON.stringify({ imagePath: options.output, htmlPath }, null, 2)}\n`, + ); +} + +main().catch((error: unknown) => { + console.error(error); + process.exitCode = 1; +}); diff --git a/.claude/skills/create-launch-post/scripts/render-tile-images.ts b/.claude/skills/create-launch-post/scripts/render-tile-images.ts new file mode 100644 index 00000000..fb82ce78 --- /dev/null +++ b/.claude/skills/create-launch-post/scripts/render-tile-images.ts @@ -0,0 +1,274 @@ +import { mkdir, readFile, writeFile } from "node:fs/promises"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; +import { chromium } from "playwright"; + +// Logo-tile launch hero. The alternative to the painting hero in +// `render-launch-images.ts`: the launch title over two to four product logos, +// each in a rounded "app tile" with its own rotation and the middle tile +// dominant. The tile treatment comes from the agentOS launch graphics, which in +// turn copies the landing page's floating-agent tiles. +const BLOG_WIDTH = 2048; +const BLOG_HEIGHT = 1024; +const SOCIAL_WIDTH = 2048; +const SOCIAL_HEIGHT = 1238; +const SCRIPT_DIR = path.dirname(fileURLToPath(import.meta.url)); +const SKILL_DIR = path.resolve(SCRIPT_DIR, ".."); +const REPO_ROOT = path.resolve(SKILL_DIR, "../../.."); +const WEBSITE_DIR = REPO_ROOT; + +// Site tokens (website/tailwind.config.mjs). +const INK = "#1B1916"; +const PAPER = "#EFEFEF"; + +interface Options { + title: string; + logos: string[]; + outputDir: string; +} + +function parseArgs(argv: string[]): Options { + const args = argv[0] === "--" ? argv.slice(1) : argv; + const values = new Map(); + for (let index = 0; index < args.length; index += 2) { + const key = args[index]; + const value = args[index + 1]; + if (!key?.startsWith("--") || value === undefined) { + throw new Error(`Invalid argument near ${key ?? "end of command"}`); + } + values.set(key.slice(2), value); + } + + const title = values.get("title"); + const logos = values.get("logos"); + const outputDir = values.get("output-dir"); + if (!title || !logos || !outputDir) { + throw new Error( + "Usage: pnpm render-tile-images -- --title --logos --output-dir ", + ); + } + + const logoPaths = logos + .split(",") + .map((entry) => entry.trim()) + .filter(Boolean) + .map((entry) => path.resolve(entry)); + if (logoPaths.length < 2 || logoPaths.length > 4) { + throw new Error("--logos must list two to four SVG paths"); + } + + return { title, logos: logoPaths, outputDir: path.resolve(outputDir) }; +} + +function escapeHtml(value: string): string { + return value + .replace(/&/g, "&") + .replace(//g, ">") + .replace(/"/g, """); +} + +function dataUrl(mime: string, bytes: Buffer): string { + return `data:${mime};base64,${bytes.toString("base64")}`; +} + +function stripDoctype(svg: string): string { + return svg.replace(/<\?xml[^>]*\?>/i, "").replace(/]*>/i, ""); +} + +// Prefix ids and their references so inlining several SVGs cannot collide on a +// shared id such as `_clip1`. +function namespaceIds(svg: string, prefix: string): string { + return svg + .replace(/id="([^"]+)"/g, `id="${prefix}$1"`) + .replace(/url\(#([^)]+)\)/g, `url(#${prefix}$1)`) + .replace(/(xlink:href|href)="#([^"]+)"/g, `$1="#${prefix}$2"`); +} + +interface TilePlacement { + left: number; + top: number; + size: number; + rot: number; + inner: number; +} + +// Middle-dominant arrangements. Percentages are relative to the scatter box. +const TILE_LAYOUTS: Record = { + 2: [ + { left: 34, top: 48, size: 300, rot: -6, inner: 0.6 }, + { left: 66, top: 52, size: 300, rot: 5, inner: 0.6 }, + ], + 3: [ + { left: 23, top: 52, size: 268, rot: -6, inner: 0.58 }, + { left: 50, top: 38, size: 330, rot: 5, inner: 0.6 }, + { left: 77, top: 53, size: 268, rot: -5, inner: 0.62 }, + ], + 4: [ + { left: 18, top: 54, size: 248, rot: -6, inner: 0.58 }, + { left: 39, top: 40, size: 292, rot: 4, inner: 0.6 }, + { left: 61, top: 40, size: 292, rot: -4, inner: 0.6 }, + { left: 82, top: 54, size: 248, rot: 6, inner: 0.58 }, + ], +}; + +async function buildHtml( + options: Options, + height: number, +): Promise { + const [titleFont, rivetLogoRaw, ...logoSources] = await Promise.all([ + readFile( + path.join( + WEBSITE_DIR, + "public/fonts/perfectly-nineties/PerfectlyNineties-Semibold.otf", + ), + ), + readFile( + path.join(WEBSITE_DIR, "src/images/rivet-logos/icon-text-black.svg"), + "utf8", + ), + ...options.logos.map((logoPath) => readFile(logoPath, "utf8")), + ]); + + const rivetLogo = namespaceIds(stripDoctype(rivetLogoRaw), "rv-"); + // Marks authored white for dark UIs are recolored to the site ink so they + // read on the light tile. + const art = logoSources.map((source, index) => + namespaceIds( + stripDoctype(source) + .replace(/white/g, INK) + .replace(/#fff(fff)?\b/gi, INK), + `lg${index}-`, + ), + ); + + const placements = TILE_LAYOUTS[options.logos.length]; + const tiles = placements + .map((tile, index) => { + const inner = Math.round(tile.size * tile.inner); + const radius = Math.round(tile.size * 0.2); + return `
+
`; + }) + .join(""); + + // The composition is authored against the 2048x1024 blog crop and shifted + // down by half the surplus so the taller social canvas stays centered. + const offset = Math.round((height - BLOG_HEIGHT) / 2); + + return ` + + + + + + +
+ +

${escapeHtml(options.title)}

+
${tiles}
+
+ +`; +} + +async function main(): Promise { + const options = parseArgs(process.argv.slice(2)); + await mkdir(options.outputDir, { recursive: true }); + + const browser = await chromium.launch(); + try { + const targets = [ + { name: "image", width: BLOG_WIDTH, height: BLOG_HEIGHT }, + { name: "social", width: SOCIAL_WIDTH, height: SOCIAL_HEIGHT }, + ]; + for (const target of targets) { + const html = await buildHtml(options, target.height); + await writeFile(path.join(options.outputDir, `${target.name}.html`), html); + const page = await browser.newPage({ + viewport: { width: target.width, height: target.height }, + deviceScaleFactor: 1, + }); + await page.setContent(html, { waitUntil: "load" }); + await page.evaluate(() => document.fonts.ready); + await page.screenshot({ + path: path.join(options.outputDir, `${target.name}.png`), + fullPage: false, + }); + await page.close(); + console.log(`wrote ${path.join(options.outputDir, `${target.name}.png`)}`); + } + } finally { + await browser.close(); + } +} + +main().catch((error) => { + console.error(error); + process.exitCode = 1; +}); diff --git a/.claude/skills/launch-graphics/SKILL.md b/.claude/skills/launch-graphics/SKILL.md new file mode 100644 index 00000000..0d87a4e9 --- /dev/null +++ b/.claude/skills/launch-graphics/SKILL.md @@ -0,0 +1,29 @@ +--- +name: launch-graphics +description: Generate agentOS marketing/launch graphics (launch & social heroes, code-snippet cards) from the committed generators in .claude/skills/launch-graphics/graphics. Use when asked to make, update, or render an agentOS launch image, social image, "agentOS Apps" graphic, or technical code-snippet image. +--- + +# agentOS launch graphics + +Source generators live in **`.claude/skills/launch-graphics/graphics/`** (committed). Rendered +PNGs go to **`~/tmp/agentos-graphics/`** — never commit output images. + +## Generate + +```bash +node .claude/skills/launch-graphics/graphics/render-launch.mjs # launch/social hero → ~/tmp/agentos-graphics/launch.png +node .claude/skills/launch-graphics/graphics/render-technical.mjs # code-snippet card → ~/tmp/agentos-graphics/technical.png +``` + +- Pass an output dir as arg 1 to override (default `~/tmp/agentos-graphics/`). +- `render-technical.mjs` needs `shiki` (dev-only, not a repo dep): set + `SHIKI_DIR=/path/to/node_modules/shiki` or `pnpm add -D shiki`. +- After rendering, open the PNG to review before sharing. + +## Editing + +- Read `.claude/skills/launch-graphics/graphics/README.md` first — it records the brand + decisions (palette, fonts, "Apps" treatment, tile styling) to stay on. +- Change layout/typography/snippets in the `render-*.mjs` files or + `snippets.json`; keep new external logos vendored in `.claude/skills/launch-graphics/graphics/assets/`. +- Iterate by re-running and viewing the PNG; tune sizes/spacing until it reads right. diff --git a/.claude/skills/launch-graphics/graphics/README.md b/.claude/skills/launch-graphics/graphics/README.md new file mode 100644 index 00000000..791682ef --- /dev/null +++ b/.claude/skills/launch-graphics/graphics/README.md @@ -0,0 +1,47 @@ +# Launch / marketing graphics + +Source for internally-generated agentOS marketing graphics (launch heroes, +social images, code-snippet cards). **Only the source lives here** — rendered +PNGs are written to `~/tmp/agentos-graphics/` and are intentionally *not* +committed. + +## Generators + +| Script | Output | Notes | +| --- | --- | --- | +| `render-launch.mjs` | `launch.png` (2048×1024) | "agentOS Apps" launch/social hero: wordmark lockup + V8 / SQLite / Rivet-Actors app tiles + PREVIEW pill. | +| `render-technical.mjs` | `technical.png` (2048×auto) | Titled header + syntax-highlighted steps from `snippets.json`. | + +```bash +# writes to ~/tmp/agentos-graphics/ by default (pass a dir to override) +node .claude/skills/launch-graphics/graphics/render-launch.mjs +node .claude/skills/launch-graphics/graphics/render-technical.mjs +``` + +## How it stays reproducible + +- **Fonts:** Manrope + JetBrains Mono are read live from `website/public/fonts/`. +- **`assets/`** (vendored, committed): + - `v8.svg` — official V8 logo (`v8.dev/_img/v8.svg`). + - `sqlite.svg` — SQLite logo, color (vectorlogo.zone icon). + - `actors.svg` — Rivet Actors mark (from the `rivet` repo), recolored to ink at render time. + - `NimbusSans-Bold.otf` — URW Helvetica clone. The agentOS wordmark's "OS" + is a live `` in Helvetica; the site renders it in the viewer's system + Helvetica, so we embed this clone to render it faithfully off a Mac. +- **playwright** is resolved from the repo's `node_modules` and drives system + Chromium (`/usr/bin/chromium`). +- **shiki** (`render-technical.mjs` only) is a dev-only highlighter, **not** a + repo dependency. Resolve order: repo `node_modules` → `SHIKI_DIR=/path/to/node_modules/shiki`. + If absent: `pnpm add -D shiki`. + +## Design decisions (so future edits stay on-brand) + +- Background is the site's paper surface `#EFEFEF`; text is ink `#1B1916` + (`tailwind.config` / `global.css`). +- "Apps" is **Manrope Semibold**, tracking `-0.015em` — the landing page's + `HERO_H1_CLASS` heading treatment — sized as a supporting label, not competing + with the wordmark. It's optically centered on the "OS" badge at render time. +- Logo tiles copy the landing page `FLOATING_AGENTS` treatment: rounded-square, + white→`#f1f1f3` gradient, `ring-ink/10`, layered shadow, individual rotation, + V8 dominant in the middle. +- Twitter shows 2:1 uncropped, so the canvas is 2048×1024. diff --git a/.claude/skills/launch-graphics/graphics/assets/NimbusSans-Bold.otf b/.claude/skills/launch-graphics/graphics/assets/NimbusSans-Bold.otf new file mode 100644 index 0000000000000000000000000000000000000000..5b1d298267bfd4ce7bfd9355e9bdd32b6dcea73e GIT binary patch literal 83264 zcmbTecU)6T^Dv%}@-vgUh|oJ0~IP_xpL@-ygq+$84XOot>SXot<+-!ox#2W}F!u9%rC$ zK!D9(9^+*k4(Bq5^I&{nXh@iELQy@3^U#jN@%a!K7BPrtvgZkht8B;NhzEv*^>)_n zKC_&|?LHdDM2Gqg4IedUK^&A1K-oPuS(Vlv37Vk%3HtX;RIB35&BI=BxNaTT_C%-< z$_P6sheNqbVzMrG19@Q+lyf;8^YO{5+%#TfD2KxxbPL*v6jicX$i46g$`3gl6Th_7 zOx=GAIQ|^2zYVn8a5;Pv4wuWd9Ii=@$;u2On-bAQY*jBr264l=(cA=XCU+XQfV-Sq!Y$=iaJO)`b8EQ=xJS9CxmUTj zxc9kFxo^3jxyXcTA~Z2Kk(#tO>1NW?#Mz{u$zYQZlW-H2NrFkbNtVeJlldmgOo~l5 znd~&#YjVuwjLBt_yC%;}KAK>j2~Wan!&C4&@$7hxyuQ4FyZ~M>FN8OWH-Q(&OXg+s zX7dVo%X#Z~TX;Kob-ew&BfRsxtGrvhCf;M-Ti$oR317sw=2QGGd|Q4Gz60Nx-=9C2 zAHpBSAJ32BC-PJHI(|NX27f+(3BQ=XnP0`<&9CDh;5YD3@~`l(@$d1U@L%)4@Ug%| zU@DLbItaQ7dJ5bG{RP2-5rWZzD1laxC73Q)AXqFY5tIw61T}(s!3n`7!A-$q!3)8A z0V24B86hJ&5)On5F@Ojl!iaH10+B}K5i^K|L=mxp*iP&tjuEGc8^mMc9l;1qgc6}b zXe)FTdJBDpLxm%SqlGa-jW9)+C7ddpD_ksGCEOsa6z&w(3y%oT2(Jil3m*wz2;U3; zBm74s5Sfc)B1+U%)JxL>CS4HHF(CWzFcWKouAs%Wlgv1pZOgQ!xpQ&cZHBsw8F zFKQIs5j_87(x=bA1sU23}0w8V6S=_b=^)19U@ruC)=O^=$MHoate#kA4% zj%kzW1JkFb&rIK%zBm13`qh*%{VypXO-TzPL`9~ z$bIBd@*H`Sd_=w>e~2aGKg2fT?qX-Lx7bfSOgu_FQJg4F7w3y-iwni8#HHe`;u`Tm z@k#L|@h$Nq@k{YXF=NIxBh92{9n89#Ihc8v4Ky2UHqva2S+tqPEW>QF*&MUQW-HCs znQbzwHmfl^Xm;G}oY^(AduD%`{cZNm>>qQ!x!By=yuG=NxvjZ_xtn=kb6@jd^AYBw z%_o}2n;e`?3NsmoRD0U+?3pxJd?bWe3ksL;8~CsRu=6n zI$GFUxLEYH@V6Ll5n(ajBGzJ(MY=_<#dM2#7K<%bTCB6!WKnHVW3k`jsKptJ%N92+ z?pyq6@!H~}#dnK;EcupVOKZ#amNu5QmJXI~mVGUKErTsbSdO-wXc=#rY^k%HY&pwv zq2)5mV#`v?O3NLVb(V)LPgtI}Y_z;%`N;CQ<=>WHESs%3RzfR@mCUMxRX3|%R<2gw zR)eetTZLLhT18o@t+ZB|R{2<rl1Y3GB ztIt-9)qkxCYjf*1)(Y!R*4?cgtv#&!Tl-rNwGOi$Ypt?Qv`)3oww`J|$9j=%1P8A!gPSN@4%YLyL zsVbenKJWjt&EGIEP!A>WXZ7HpxuJTHL-oUk>+{3)`H;k{lmt~qR;8l2t%oN>+--(b>tzH&&Aoo0S}|Rp&Gt`-y%rU>}f?`w|{)VxEzt#TA4mLCnHq0~lSHqCl)Z}E9fkQ)o%Mbr8A7U6h z^0%t6Ul0O+tBx@Aj4+__*Ryl5{#P?eo0^i4snQozhW1$f>|^!3iT%lgIDI)zzfNj> zUj4I8Z5WuKhm!EKTB`@7)pt(O=Tr3g)ZclXp>N1E{Qe?8^EdK!`cYZzGG+ZfG+#d` z|0ljgV7Fn72Ic&Mlw({Nm4OR+znWryWz~kpgkLop1A`3plYZ4F{YIJgw|vTP`BcNG zjNhuV^x(1#4Os?;W#}iC*(!|*zxDRd%1BL9XR387ttLYipP@>LWoPE%=>dtevy&HF za`f?2#cLpScJyR3?oQ6Gp6OYsIuOKcbw;K-&d}M7?d8K6UG!Uc( zUZ(0RQZu!x%tU=>jZUMDQ)lUaheC%CeTT5$s15%uulp?@q3`MQ6K%*Zpg#V3*#0RA z;jn7zlms0lNeLNhb&6J%5~qnxQL!c$a3N70p+~J!>qiXM7a|ifRN3lCeNB!&pQF!@ z(dYB@zwG3JHwI2QW+rR2>UdSGT9cCfGY>{#hAvK{N={9Qi`8buWTvU&KoX#%{x>-* zQxmJwrT|AX0ZdAEOqN!w*0IdfvTzKUcu*zkxD0iy&X9{q&1F%$`8eq^G^&KGG*w1M zYEFhGAyKE#0dzwqCRLZHuW9WPmztB(T8{gbiOE!JQ}cmCYCX8PlprThHy0NW9Tqw) zwJs|oMU|4ERqFw>89gqbffWLk7N!GC%++M-K;3Fo+QA@BNib++YYNPQEdrWYt<|aY z_~T+g5L#)0d{!FEf%Gf@l%A!@h9o6c%?<+IXi{`)0Pr)_r5S5tbgHzpjMQBHK&?76 zQ=fqqRO9Z&_sTpc$jmwJFWoCg2)%P*%4Nosv36=qIn)rCb(j{kUb(%D7 zUXB`+H%OZ*GZUto34#DSkL4tb2wJk(AGOX{Vw(XK900}^V~K5HnPs5e$jqNB4$S-| z2T))oAS)#ffX4#eEUQ_Wb@5jxr|I$llsY$7t4fA?cFkQ~*k3m%n0O+)T~%7G4z_5V zTK^N5q));EoSYote_;WRUWU}EHHE#PuXk>3ch*;g1vtC3*1NW*Zmp?%YwFROdKyy4 z*8ZS%;D3OpwcW9`zhi4ZJsP&X6`o6LyGv_-m)3qRt@P>9Lx0y+`dnM#ySCzUZKcn( z6`pG=Jl9ruuC4G~TjA-u0X(->cy6um+*;wewZd~Wruu#{+SJ&j44ppF#_N-~6n&DF zrcaVJDPYBbjDpHYOwGzvvrFdW?5=|Cm7&Q@0+CD1%!*Bftqt9D8Yrvd^gB00r%$x; zzo`{iS|4p%qG}MxTc8T$6`(n~c)9wgWoqDL9t+j}I#qZgq=Ug?8UkBNHB^-x6Q>%M zH9TvyCL~$IS|*{Ik%_5cnuKIkxGF2cFg7$%#PUo1Wiu*S|=8=@hEN`XDsScQ&KbkM?udNeRV4*40#}u?aH!3pJ|c+0_WSwcc3NGeqC1l@Eq|D`=KneMRfMTk}R7tuxTq{ysB(#s6)3>;VlD=Hy|_ z=#Ol+#;di)+0mGBGG?5O8CPS*&6sgFW{hy0jc{Cya9oUVT#RsBTv|uD7y-H%0lF9g zx)>>RF+z4RLUuJmb~QqFH9~eZLUuL6aWz7AHR5tLLUuJmb~QqFGeUMVLUuC(bTbZj z)7vqwzK*O=y7~sRPQ}d#)y)Xi%?Q=K6{?e~VW_{SeyEqbKJoF_Cw_*?K|U-zcPAG& zy~W__Wk?1Y33E2mMS@G)&Ra zPv5f@n1`XKhpPdKVStaJr;h=q@1RzmxEp!z{*yE#)$T?vxf{9UVVt6eaY7!(xq29R z?_q@FVT9vhgyUg^<7tHBX=H<^5um3Lpr;X_r;$QWBV zjTtY)DtNKRxudI3t7Lk*8dh`AAj66oMh`Mh%HwC|Cu*a(I=0Tv$+Iyj3b3=Y z7u(a>(TBzE?C8e=17kv8a&ls&6V5>v5hSd1LSpFRZ0O>`vcuWQMSq2H(r-ez$h4LW z1N1~VJNdFQ4vArapB^nBWGi93K5=eM3~kPOD9#47&dvsMT?}moe9rn^1%(nzKO`*ukgzoR2l%n{Lt?<^XTaxY zz~^Vcr(X)_V!-ETz^7NBP-(#DXTaxYz~{#b5VRTa`5Ex}8SwcV@cA3?>2;-lfL_8O zG2rtz%+KF|&)p)$Zw8PHm3AR@p(L_mO(gQKHe#K_3* zJ$m%?O-;+wTjn;-j!v#NF?lwvEjE7O(@99N=?0l>xQL~(camSd|No6QycyWAj|MjE zqrq=w&UN-7fZ&YgEan{MT;n=&hk)~b4tFE>85f(lnKbhvcw>0edGmP1yjpOc-{!sL zoAT57+5DIMe+3EP;7$|d2<8gv1P=)w(H-2_{lR^mMC>8H5&sBA=ePZ2Hz z*YpW+NZ%KJ5!vb8&xzo4&IYIREK!~4sOXyB#r#RsENU@r0}f_Ka4Qct9RtqfL{lxe zk;_agz*&6D^q%Q6QbhJ7J-`_}kxV9aWIj2K+(1@?Blr?|o%|p+1xK*G*h%az?k^r9 z9wF9>v&1vRbHoe9MdCH$GI53Yu=u+8iTFD>e&uGiW=>|_W}#+d%~H%}nawj>Vz$w2 zuh{{!V`it#u9!VB`)Kynj4}Jy+yY#@y}-R2Y#t8I-5B!(^Az(8^IY?3=8MhOn{PKi zWPZ;4viW`U*XG~BsVkOPNZLs{O1etyB_0w#$q31KNvtGMk}AoTOqI-$ER?JQ$L>bS zR!N=Yh~$jqg5r4y)5_*;UU*cr z__gfu%3XCA6qjnFy^Club5Mr8x?wK%#{r)(2lYW-pAiQQLPPAn<9>7@7RZ{81a#~c z=|6D5=xblU9=&!K1{H==Ewh9!QL0|OqUxV1$BI@Sjyv9z{!H<#sq*k~x?%su zGmqrVETLG2O|S%+5NJ4(81oX;g~xnlmda3%f^LuTPzCnw=7l?9!Rc^hmr8%m5q!+8 zNez_`4~uu~I&$B=c}m=kz>X`wyQAR>6pDC%q3%dH;v7~~(wNvNuqoG;AD5pzyzAcE zE73=6(_$vXW|xDI z=vG7J`8uVT$CRR%GSqIS>wVlof!!TC;-NU;b!X&?{&@Ki$x3J}C;anM)4~*fs*C?Q zw(8LO?R53JZR_e3r?;f}t)s<#Vkh{GPpPP@-&l60Sb2D5P0{kj>sGWEGfJjLhWRZN z7BiFpI}m6>%UMBKqB?TMti?<7m7RnO%8LuuD|Xivt*W5+tXscv(`A&=ENv%7b2E_u z^S`4`uaPMd*nGF%f^4MP=JBYfEOz?D)NzWq#0}eW=qa1#Z9XVR{}LctMaw|6j!oOG z{1ESU2J;oeN99k7p`|N#s6*HHQ{e9XhhocxH2Q-$QB+ZOQE_@}e!}Eglc&t4b7wAE z6fh%fW;@Ku8y((V-tF>`FApzlyL~|^^$V=Op72b8B=17 z#qlEL;iBqg#Y@+(YL7ZzmOWfvQ-4WuxklB870`LjIkL_!2?P7ne*Q5%Y~@%~>->0t z((lgm#7_#OMtxC#l=v1q++!Kf6k+>Ijz#-lH zZNCzz47!@|_=Q}&$cqxoM#sjD9#a>4q+xGe!?CzMW9V`GO&jtub-B4&nYpE#HkWSL zOpDPKrqY%YgA@!PyzIT+3HlLVKt3dd!^{z*928g-_y)Nkw;O*W@~;M!a~UnZw7>q^ zx#~$nl=x3%gZ>pFv*4>(G-z!4@K_~?)<4O}UVNBJDYL#fo0J}SOi7zuDXHf@p_sV? zo9h6!VoZq^)0RvDi}c&0w&)@8@3mZ>mpw9#%@3n+LbnUyvWoI2N;8Sra{a9jx9x0@-6L8S)^m?@o;fPe}Eru?K;z z_$ZK&e!EqhG<29cX%LMA2}xPAf0>wB3!-=5v8E?Bk@rj_auCsl6qsI+@)+t{aB@H_YvI*#g&E~ zjXUXpU5AsdC{7&RT)&&%+fa7tPdO4Lz4Cnle6e~UTs$T{C2Xc>g7Ddjqo?NoB|==G z`1T@Xg(f{f!;lrX2_}CL)JGF~CBv^!tq>8{D$X6IPd4uOia2t_ne-+6n)2+}pb9Kh z;97jKzc+z$+9LZ0*d72Vh|9H;)s^(NiED@Vk$X=WuAQhiFiE3kr)ETUjpkUdX) zjAE9dHey+e_dl6}zX-;=8F-IQFqN`?s4KR?T>~7h;irWA0oAMRo0hDoRK6E3oI0mq zh9Z8}>cccT1Ph5uWGI9{@;~1qJJikd0q#lTJ9|h*#z(2hM);s6(Pv; zGA0u#gh+6zFLuMOVSTZH2H{4oW!$SE{G)=XtKrmr3N;BY9N2$x+ol;=>y#yVOQvY# z__2O-T+KTizfD=GjanO}2pE$V5=Wylp|}i5krgwzOuVQJT>=4EUM3DtNsb(ux%H~@ zI=1%}4%je$U#wDHe=6si0(F1dggVg7ZuY=A))yTkBxfkb6$!-5CmGVf4tj{KPCub|>q8=v&&*A9?D-t zG4t8egb+6;%f#sv<0|vpvjWRBNa+IGu1KC#yPwm`!`c$oE%&3_75y~iRb&A-uY0flIJ0(;Q z1?mNNW2afhWo-X1XU9xG-4w?q?Xf`ks-&=JD}B7QY~$wZh9jf5E`$>ELBOq$4I$Q{ zHq56oG5P>(oO^%PYvugir`xpgln)ZN5??%n$M!KV?M(6}=i zFK!{iNb!wDtlr54O`Cj$2`hU+iTy|kvho%#JW^S8YL)1q&}Gi(kxN}g7_@@*nc1ZH z1)eQHoe9iCCtjf00^Eu4$DsnT*VVy~A2wcj@MQ26589hww+$YYlCM-nS_+_i`b18Fa{V0@b4* zf|js}q@xhm;}!uEiS0pY*I*m5EIv6UA-*c5W_MLp&90Q{M7m{a+t#|>HO4v+PYYy; zEQFFg5_yXgC-j?7UnI6K6E~;G@HpHDd*VnOi`=j$nt)Wu1G%G#C<=QZ4;%%2^ExIQ zbLMu&OU0K*I~p(1ml|uoewV*aeG_$F`TRrV0ngq7&|NSFWS!MtI10PNo}7S3BX@u? z2L3z&=4dn;N_}tyK2?v3owV`5^-B(1G>uw7ZWEECsYI~u&g__ z#2$k-`s|LR#fw}>@s4x*Q?`ipWR2I8<&GkOESMmqqp>^k0wBYg8UeN=Ft>%qrVP#I z(TJ8Bfdowg{U%(Im%k)W5j-+txO$6j|Dl7M>$cM6RV%6+!s;T%_4V6tTu_y01~3T@aF$gzs7Q>IJQND(mAEZYhh>6O zu$yI|Q^{1>jYEN*yN?U>_Z#2%@$;d^8?=PLW6*On6h#ZL3&GMfzB*@ZUb!gtVAl1= za@6(S8)PQNjVKl=xacT)E5j97fhq{e4)QMX_~XN$R%t0{$2qXvsc<&JhBfU~#yBEV zaZA_1OahZIm=F^ok%Eyzv=&qo{DbVdNC60jO(01$C+{Wi^}<~Te`~Q?C3w1+*sI6Z+sjLe!@%oX7C!i!{|#V!2iF=gD34|pgjobndm#%V|HqA{N+?tVDk7hOOO=TYwqT=8(2k<5pP zz$*o(332?Q8Gz24osU@diW1*wPA-F0ZVNh4j4iYM13D=>`&~np*|d0l@T8(lMY?WQ zeil7th8Wx8u6SIZjREzel^fHIP5aQY8dPTUc{^>GWf&@-w;>_%o zYo{q^teLmIV5bOdn=&pctVf;ddFUUpOq-lFGft5{ZOOV3v>3)`p4T=c?GP~xA-3N7 zhD2borjp_({r9F#Zb<-X6<_%=1>}{Brt6I`RL^d!iQ@Ge zw}|SGZoKwTE*`8y0%lOzLnHygSV6M#$SeSXl)NS-Oc5*;>&ke>6+loW$uZ#$C>buLG|;uLsYI7s|WBd&l>I@V+mC zV8MC84`MFyAL744QwXDQZoxncf9Y#Y6B_@W{5{tB54yz)!J~{EPe>g48H+SFyi1NE|6n6R!l1 zM2q;pW~OH05M)+qw#%#md=F>LE}Pvk`_t@=d60RC`7Q}pVk?a5jys}^gebx-R75H6*%o^Jg{IzSpA9WEUs&6CcRu9NPS zo|ZOAnKqm@&Taa&8PH}>n-gu`$+)sq**@7p*=gC^wt}|xf4KZH@Q=tpj<(~pGim45 zF1cNOySweV?Je66Za=O4j`odmPx%n}4FyM`P^2hw6*Cn@iuH zw8PjA+79zOtnP5P!^;kcvY?by1QkbRQ;Vpr)IsVv^@;jd*+J>8j8)E3-co*})98G9 zE?rFTq#Ni)`VHM|BeUsZ<7P9$CfcUbX0Od*n^QJ_+kEZF>nQKoy<=?02tuIl)> zQwjb@db{*^-?E2V6*u~h*uv=-j$!?$BiS8!d+jQ^Jy;t}C z-F4lUcHh{2Z}*GcA9jD&{U3YE-r0VG{eJt4_8)r4dkpL`yT`H~8+sh*@vi5fo~Cm z_IBw#u6K6tRlRF^Kk41#sBrA%nB=&{@uZWLliX>K(`VjnA=9TV{Q-JzPYz|AL1VGp5wmA{gj7;hp)$Yj}(t=k7u4z&qUAd zp7*`DUfsQ%ye4^Nd(HG(>{a4b?p5Pe=e4g--#(>%-gvk1p6Pw7Z)V?9{jB@#==Z39 z&;Et|*Y>aKf2se20X+uf4>TF57^oe%c;NAYUwn8zE@CWp=qT^m{zdL;CA=$jGT5#)$&BL<8ZKO%R;iV@pJ939a# z;;#`uMz$T?#Z3UdqV7Zwl}5jHQZIovKh zDg1Hxhwy(Rlo6dHx<+_KL`K9!Xd<)`iy~GRfdsE(r?Mn#T_9Th+7 z#i&=2!I5(!zmJ|hx_0!pF_B}=j(IdTVr=x-8Dlq&eKYpsxbkt^#@!j;X?*th)#EEC z$R-S(uw}xrD3_=U6N4wdjOIqSi|!IVC^{rMHhOLJ&gg5Z&MIG3lxj%~KPEY*Af`N~ zKBg(=Q>-l3D>gB}$q&TERrOZvKNnuiDsa;Zgrp`?3lBP?$l7`Yf z(`Tf=%ovohIb%!4*UT=Nk(ui=D>DyeKGj+4Ty*_(V{}V(cXeNMOxC=tWmyebU$dpz zUfE&UCD}i6!g7*w&gWX@PRU)AdmxXW*E268Z)M)Ie6xJ_{Dl1C{BM(cPaZS5bn=ZU zqA8YBdQM50QaWY(ly_4tr}|FKnYwuD%W2d!muUm16-}q6$4x&s{q>AdGnUWf&di%N zWLD+uA#;4@9G|;t-u!u=<_FB*J^#l1FALf&=(nJ7!Gnb%3o8~rTKKuZvY=PNkVQ6& z)QjdWx>Cp~bShj_*tl4+IAigG#pR0|7XQ7Z>yoG?YnR+z@}H$`mbxwtU#eNUV(FEo z-*U=68JxET!gxD5x+l$){dqmcn~uQ6?2au<@+@h7@%!s|I6Hz^hN_e zpk5!is9Tb(4$R9hL@bULd?VOA5`VBIwxM1E#Qos(nC$YH#A3S-L^AFru(xZ{cSK(3 zl&l)Me9>1+dV_hg2U$F9e1h8Xm^I8i*+OL1?>@FJ5}gQ)5MtT%frBt#j(OMokV31( z8e!Nf?cN2~Mfi_d#GvZXQ!&cgw3=z>=88HseiNQtUcY8Xx#+~M?U(M#kx9r|?6^;f zCJ2wtsZuQ*F2ddXWMLaJw(OT5tE@V?TG@#2%TDEQ)r^ymNu3lnRVhJpbIPW@M}0p` zd5^q5Sa1A*`hWPw{z%JapqSd#Ax_Wupc6;RY`P1V^@4x;WIxZ?sJz>1M z_Ta(V+C#MDwhoCL%9xjkYkd;6AA#B-@a#!Jq}2MqSBW5 zj$?LU9=4NXTMZU;#O%Og79VoB$wPlIdu2hr&O|r9x!UW@zMwaVvn%U5<|GJ^$QO%* zn7cbG0Q~DS%K#17e^U^+)2q>b84gClSd4?=PYk(W1ivVl-52bpxYVDzw2+i+pnew7 zq^T4%P6f1a%~3KO^N$vMDj*nIfuy$%ZvCr~fLjGtWn2)2Dw`8!1A>!6qv@FNTqmrQ zW9xM;J0p~V_1EGbE1E8DJG-B*J6ZM;wU?u|lb>l$DjQV+HJ%DA;P0-=iC<5zNnX*v zyWD$DzZoe?)#PMdtRgOO z@)!r9N7kTdEKxWJp&9PldGIO79;84Mf+%JsTnO)5i>E*p! z4_uZb(&Y}Oy7Z0euT@Ud%-4kiPl&?WC8b3hMB7TXm+n=Z+O7%pjY;&RrvqxZ8Bj>X zMdx8IrKnQo!vtY30vERg2r!9iuoJ9-(`_N1ghVnti~zsvDZ>h(&4~96gqAUfkyD$; z$XYCjp)IrkSrfa~ZZF-ZxUp}vn`iWBzu}r~rx&n~T-b8)8xN#Zpq^j9!sAyrrw{m# z@rl#5G4v*6CqM@cj6lU8)?Hxrw<9IeqV_yyDRV$J=isbOd6lB0@xj&o6)wJUe$lil zAipn`$+2YZfZZdNgZJK!d!j%c|Ne%0qYfT#uw+DB{)CBi;o8MlmXxpB(*9WK&dLVG zfz5eQ8)!QmF1u1crtiQB;lqYZJa)Hf|A|X9%M0c_JEIDAHuJ%;_8%g8f;*O_4jbDg z!yy;<5TTAl3DVs{1K}}aqDa4RXzE*dFB4dAgr$S^M+wLrQXA$Gr|HrSGsD zWyvn=Q-?=Pj2k{Qy7ttGeYF>9>6e9gr|jCk2|j@nqXGk?4_v>w?_eVkpNY&s$B9rW z2nA1 z2i?Hv5c!&_Mjd&K08NlxDO!E5UitdUmihsbzq>p=dA%RPoCO(XpeG5#)_F`Q$_g|+h*-hoI6*20^TVu zZn^eY{yp)@fZNJP?wGp|OX2AOlVf}Krk#67;5OJ?j)kYO;Ej(m_|dy$Bvrh-3e-?Lpr15qTpg zO;Appq)JUxBWxS^xdgW{v<)Xc=(6vb%u!Si35KHjG# z_sLGA#FK(^&f4HGu_YygDM>QPy$>cxUN=e-|B$*3SsnYtdJ~-DHycSpGMVDmBm1tT zqz}nzXV!~5fRyz9i`oqS1b68E9A33{3x`pX+fL+dWbX%X7LgB-eHbM{!<@(~;16(s zZdzXoIyAzdgZ(LYQc^?JJ*ODC4J9d~?n3P<=#xE&s-gBhrTY|Lk-3p#X0{OJxK4 zGz}6=&rF(=q`+q0pFnvy-2aHc9dh3m_YRp6ku?#{`$dyURM(A^l@f!?*-1Faf(SHCf0X?ig4)8E z4eWV5?}r#h+Ic9WQGsOdK}~tw%DSC!j6NPec;{e6;P{LP4ULM0h{Q)^*Bbn7?ZyRw z3-s#O4~HACgK`0H&UDKDC)g?gJB8^!hmscFAf;MNGUj^6DcCr7$ZH=y9lmy5DH%#~ zJF=X6MDmAtZRhq*t%*-gE9yRa&$Xw?I>q@XGGGUuDKx z_-)7q%n29|AMz@Xrbko-DWs0WVspYK&{u^LcS>@Q+BFsgt`jABK%D``96)2QlK&I{ zs9i+J&cms|@wR`1uzKFjx|?u>mX1D}5VSK`5jZX-L_f+6tgN z89U&Vw@})2cG8&YAO&vQ152>ifQ?7-dzy#l+HA#L$A=^@XQJ zC|9!0TWTp@Nq}hTua@ zw@+m68f5cmKM$Q^0%eEhRHT*+6CpV<8(F$M#TJW2PbazwahECmx?vwV?(?`4qGu_) z3ilTXJ`0g}>GP+nk(mfwv{Qy>rcRh6nz%$ZYHiAy*YXcl=Z}{t*&S--fCvsr)Njil0CM$8Chm?q|~nOBlq`-Iw7d5%QwhH&P88km?9w1<39p;z=x;U zBG`^a0-z2}#89UUMO{bXuJDT?um=U9PG@nab zor#5l$vZ0MZdV*RxS_g=-np~S-4%a3jZO*?F(-NA{vy$vnE8G*6km^ zgO~He`CP?dxG38`={3_YSt~QI7HtWc&)MjT|wEdl{@#yYv*px z-mAQGC*Ya@LfAUW&Kw>)B4pgS(9m&*&z?Vg=o~F+abWaLk2S2)FrHXyBwNlQIdcx;E+oCH!643L+hWt<ErThpU>N~q3xOT%75Wt@ks*eE zZPL)ZsTBIt@K2J}gy;vTAN$K)eIIqaU(EhoXMcFe6=Jfyf(CnODR@=iwx@R6wmmd@ zeqYw&`OhN36@u|#kJ&HPl zDTQW3WKZy%=^-j*ufb^(;TqH7b1!7~=hf=7`;biH|@9h-`p7s@cljsic3$#zOwj&@BUr32Y; zk6cCgF!Bb3dz8;Ek73)}+~=ak=GftoXFfL_1l@D2d4?>Em@q3V$BmV!W-gL|Qx2K% zAZ%jt=Mh7AoQYSTQq3p@O;!%q%#o8X6s`Zlk2WK9=%jzs+dX6#=)khLJ;ojwD3$ptLmX#nHx&eAZbNJD9fT+3MsD>J9=eTQ%WzFUR70#l zcCsmJ=B->WM}fjCQ9E6nqtreNmG_AqOV_ThQ*2*9BWK3E*)!+SGv+Q@q?$8nZoA+a z+DW1E&^>YIcJEkMU8S77cC(bg%i8Ph z`&T66EkkFqeT!7+9VT!VJ~$=7tC%$qj>AK0AvhZaf@eZ_?wwv{y$h}YcF10OARmI^ z29pwacK8>|y7aC}3d?+&w27C}omtgzjve7AtSrG~wKi1ywb>q|b6T7lTR{`^O|76yt zC#Waz>o1mHC;Fii1n$8k$ZSJ`Z91M0{_x@a<*&#j^inrEkbkf`W=M!u6*VEb{=$Xo zy$5MYFV^5RSet!Guoq8(IXRkvZZ9B1qVfj z2+sc*9L!S;7@j^RnwEwi9UENHM}hlv!yMq0A=>xBrHUg*A-Z$y{tM~16c9Ol?LXV- zfB2|IAN5O1b-xGxD$z!4DqFIBS?S6RB5BVZ>vxscDt7G1Pc5ZOa+l{UjT1@Y^UD^L zF|WaK!3KT}LpJt6Zw0DFpJgS)pGPP3^B*@lU?H7DES$HfU@ndM!udPaFWjg=fBf|c z^}H~(Hhw3azBzAIVv$IC;Pl4oLs#YBd)>i`fS_D$oHAOIH@2T#n%lA#LUeZuk8H0x zdA>#)84<4;UZ9-C`pS{=Ee|P~>l$zeprcJ(CZUN}$B1S9nE^Pl3DvXA{%DKDOy4r= zGw`@AH&7`JN8vb4hNcVCwpP!mR~)QfQ&&ez_r`ACJoS)bYw5D$P4r*F1vBRt%vEI0 zU3-RR+Z`dP8e_@+{3Wbtp`X2Zo-=g4`Xzfd|&YEc% zYJabV+YeDKlD7miy6t21S%5#kWX5Ak=z+hup)UeVwTyp>J`+#zXMvI#Fqc*RvDxXU3*_as{HFb(z&;@eHWblr}|Ma77 z{vvBhg3#e4f(hqiKZyDhV?X#4L(UI+LJc8dgGkH_K_nl`xIZu2$VMN+sCfDsb-$f+ zO0}2X6FYjnmjVw&W^Q10ZTR1zj0Tc#-C-$ahW;e?h@RZeE&IdCAv2K0IV3)ZEVw8i zS+;q~Yy(Fx_;606lg7v>2pIY|o7zXUw0{75kMGT<5?shgFCzbo+{5fI4~=ggB=ZOi z^z>{Dc=WXK>eI)8jh^&S{^7c~v14N6#*B}xJKRuL*8t~>ZZ*kON(ww3p{?tE!NM{Zf8%(s2sqStSwKM=(v`IFLrd`Tt= z1;ZMU?4&^Nx4X?PHmuNfhI@_Lw61#NRI+vxtjd(h-3MPFpJ&}f>mDM`!)G_SQ}3ZR zeH_9l%LCqsVn9cmC6^OHVcbJZx$Wi zT)yL|{BG2N0ZKfxCFE}c74Z@0v#?>^w3wL5)5g--=<^VA2wX+MGqxAF+ENJXUPu@e z%&Tu`WEpoC>WG%HZZW^+v9i%ST_a~NnY*N*uzm9SDJ$|X35p(XuPgXO?WY(GOYhYG7opSSGQsRo_L@(>>rjr zc>-~iGWJh@4sJFuKLMBzzH2Kz=b!qL65!>%$LQ$e`z$Lzykgw%@t6k2SGHj9?9I8A zq63=n3b@r;^>fCZapxvyh_>V3y7X-s-J1xTr%4{E@Q*q1=FPrSH;z|jMy#eC(B&Z{ z>-Iuj84EjoE|Rg(k^RUU>>;mhfTlQx+l6BL;4X}X12omK$2!`|p6i{+LZ$;8?ET1_ z5bb0w{REKUf>cLIMa4<8sZEMimr1GRDRMuB#{9(t$TAeX6p6m9KwhIsEUWEu%bz|R zwI}cO=!D@#``nMO^XzHr`D`ZuQ=TH1Wf4nA}-I^ixgACuYGQOPWc*7uQpQaUMR zSnr(l`@QDcF3@1?ncaNMf&A%_Yx>dG?pFz0{^+YWiC*a8%t9t=WS@Xcq_6!c={L;( z;n0PBTQNBEaORJ`4&?3Llzx_a4;`Gy@7zgL&h#~g(dI5$q7TrLb|{`SpAGfNCs(ap zIDa+G2GjOrgK2*{^dLI+t{wTaI2zs6lQMuh3tX>5_Q%u7`W?o9pe zzk}%TSUd86)VxNqdYW!eq*y+KTAYg9vWzzP1aT?~W-0rB`ruA61$N~B zvlYvkf>LS%brZ;iK-Y=sWH9*gs=<#pcm$>Q;z`%^pros@IWwip{~HO;WC|R}3DjPY zZx+rPNap~cEi(X8zzid((iuG{FQ!;#{Dgv*uH*cFeMTXfK;6;@1YV@1n*ow9_!f)x zRP~^?GqXlga3zOGy`j@7um^Z(KkO8%g13LJMpmqkg8g_VLZMAAE}KkeR!u#2T7H!M zh(`JQM>OF2_+Ovkc;4-flzDWtyI^JM>LsO$17v#9rZKhTg>7>WC>|f2M~#~@ZQ;CW zw861h#{}DvdKV+MXM#(qp(JpUi`q4i9VtvPeY-#n_#bfr6D&hgefV4e`)SZGo9{7u z+xS()Wb3U%O+nO}HEb~rvPouz0nBo$BYR2st#dMaWnsSxd9dv4p2KZ~Q$ZM~!pXXyn?+9o@l%m&lz3+q9YzEh)fU4D&)9dz4) z98TWaPVFk8pw|bs7kJ;U=(ll>V)oB%+_p6uK9?#kF(@*J*=1}z<2i_Dgurg2|1TcR zWu?}htYN2bPvTz93w0zIw&EV4Bs=jl$Yy*xIRTJqf=Ow0cS<^Dh)_BT+prvf{n(b@Mk)-=M6{jV+z1n3z0OlTOzOB^$s45D`QJE|X?*YAUlPO-k3rtJ62_*ipH$mM*}3WzZp(l9ZF{nl?0|1h6e~S=(YTfLjpF zykWszX~_XNQDK zuKJQF^JD_>b!T%p)sp^Y4qu2scnZ`iev_96xq zoQZs_cy;08tG^=7^-ay5lbcDGJQuJ=rJh+R`HnSH0JB@cVUDpRy)w+s$fe1gn&V_E zXQlvW@Cg>@v4JRo=#2$&z4LH4(NnmwD)xVYw;2&@<8K19;5K*pTVx3arbly(Y^*9b zA&pL(G(EygK45iZ`6OlR*8RHUiiW+VySLI?b`&3fAiqEJXx0v8ecHs05egsv!F6gZ zz}(646DCZqflmPV>uTZh2G@0SFkOQI>|JIwYt|t!K_u|OP3+B+53EtnTWJtF4bQlE zArS-L9sEg14**wIR{~d3#*R-@1)ihye>PjC!GXfA!+4XCuyeq)wTir=?4p z1ZbH4k^&R3y?O68cf}hdgUdl9T-xVV&s{ftooJgbdc#-+=8fosd-|^$zbTQ9tvn1L zPQAQv>Gk7?Q+<-sXXRwjg}WB7UA{(CRa~~FO0l(MR%$UF0v~GbsYs2APSdK@>DBcI zDyzY@0(PnC11@^Nl6>16r4aU581In_Ed85h>4A^mk-3qje_Z@>+j{X^?_PkuqLZ61zwlOrsZhI%Dpa!z5Dy}sYeaD)nm58%W`id zlhsyaCnu+8C8*QOw^dh`*TBPa0q!HKuSk!HOV5ajO|Ph{tE{Zk`zkgy-MoYBVIf~4 z>vz0jrkyOH@a))!3dBA05aP3E9?IT9A54kb9IkL2I^46D}b;S2-+A)yhXJFHcsg0`)LX@77UBu zDli|N-q2dZT08rJN)a-#g=j2h=ucVEEJbKfQ3#Q+B)(Hic z9~#hH`*Uomeo`)A1p?^NAY*y|w+~{I2)DSn=rDQ6iNv>COBd_CN(=N}rO}z|cORo@!%hB;0YT)D=Rlj5#>!n6FUWHgBM8?Xy5+B@h2GuyYeD@ zbO>$IPheHP&iUc+v)T9p+{hZY@N93z%s_lDJPN~352HRZpSqC0KL(>+j2|%ax94Ga zsoS;TmkoIx-R(~rUu+*Ur8g;Kff4b6kw)vUfbjwQOmAz189xLwZ*a^v@%9hj=?yFOpRlrfXMr&6 z&^k3%2Gq`G=wHo3keSvJXP6rQrlq#5Q%?NLeYp(i(JEtgnG#&{*&LN0#n@hQ^B zHzD|lK0I5Zv`v;~^0!F$9s=N=_91$*S3*=<_LTNnik|;eV4Znm!QRluu<1;g4Vu3+ zDy|N>j=fn*#|m2PP$~Vc%cwwNgOIj&>z7|MbA?b!BwQjUd z`)WA(thyW$bXSb@DomofTcnPG`GcG7LLHFw&b>>NA5aGBFaDY;+`%O2ul#G_&0`!o z4SF}=ka<4~SuzOo3e$QbGMV5$-4vm`%S^ShvskNG+G=|{2NOCU9@MZM9^t;MtxHyM zeQDFtTBtMsjaQ&D7y-D#m1HRzyF!LMA$qfwPtZZ=K7XYkt<{3GR@24kGtEJ;jcyaY zM*@D~wz{J~Y?T`~b-G{-c*uQAgvNGA9}>Om1Es^&j{T({1e2QbGc{{2f@`{X##%2!L7`iDXG4CZt(*HGy{|Q^26TPR2n`+{`(L`aD?bPV43d+sfS-=(v z&S$|g#RB?aXd*i6yH*7F#vkLGY0_hVjsEAO5s0`G<7H9^gU(&(Q)2i;-G7#LI+!U* z{6cN?UCksaPvUcPeg39c$j(pxke$E9DRL`2Qj?uOb@+9)p%_-g5%}a`Llg+s2yBRq zE*qka4-lO6LGFT+euN+Zqk&}j>9K%iFvhWY+Pu%}%v5L7HE?Gi+W4$66U*KssV58g za%I!LhKty;7IZcD5ui}bj~8q%En2~pWM@c6tPxBsAxrK!`Fx1djQ`=|6nmbfJV(0q zC`nQt#EkOghXT(K4LpP6CwPX|k9Y>mkQRtima^$=V`dKl0a4HYz{By}L;P>~b9CJA zz=X=rHRc|w;AFn5-~fHves<{@YA~PnF%+lJ30+W$0-@=b2QGYoN|=z^3|KT8GiECm zgzuH+FxR^%8PG`(nbCieeox6af9jBU?0~La@`CNoEwyrgX8HLm+_4kM>3drxK--yb zw-G2M?9+-pIo~V|rNk$^0j<7OT1tw~KcS>BAU_tSM<-TtvyI(0n<S&)z}t5fV7j(n}Gr0$lHIS zG0l^N!+*u$y@bQl1rwb{0-N^?TMnGn7%W|4-Ra;VJ=p2cUph_zSHlFs z>knsz)%c^Sb)H?N4fS}+7P;Mp?ib#O=glchI8GoPK5SSw2U zNohaz)c#VM>@Hv?KxffS2YJxK9n4U=@=C~q)^KVO%x_pu$#_kIVPfYG+o{kW#*?}K zLh&F?IBAF!MkAs{TAOGVxr4{TdyUPs4&yx?wir48alj8K>wl^Gn3jzP9wR?W4OT3k zd_w&QQ&XFvU98Q}_KflQUoy1Uv-6CcmLWr1pUnFOp~`cbClel|2Oa`WRN1Q;!X7eH z9o%0^^nt2boT)QHN_%zsxNH8_+?r1_a3Qq2VEwIEKDmj>T!sm{2$MOjYxbSxH!Oie z-~(KQo(2R7>+pwZNXPQyg})a`pWK8+-pcpKhW7tN1I?EHOm1jaR~zL~A*iRp^874q z@AMQ_4Q*)8M)~q)k9d+yP0-&@$%db$vRB7D;RZ_nnIvnr6%BgmALtPMrV~5_pW()m zrOUJkB4SW12-E+CBBF!Tzx9U`PS`7iFbS0leWsN?^qahEYmqu8D{F~f4erAXZU9HP z0WxlgYlU-{2+#b%5io>QB2Jks?Bxo=;UrCa>yVFEBbE)mU0ZuQC2?0+hKlqRM+SvO zgs=-&S39et<46?fE%5Wh26_^}!tCVF+6)#xw&jhu8d6z^)u zE^bUf_WGRgRSTo1NcxM(g!%uuIzh5~k-q27okLgY>zp$LeD$K-ZJy{7vu9!E+LHAW z!-hNNH`(g)`23q3`9W+s#VNp=U24+$#DydCnY?o%>cE62M#Lp5_C_W}rK(6UlZy{y zm|t@n4xC^ctF{}R{9*>WZfE^?CD z%#v6A2f4;{M%M41K&&FxaBO6Xj~8y7KEr9_G7Z#F05ZKlT>25o1gPPcSQrmePP{WQ zFmrpwU1f0ra7?|Jon1I4+o>8H(+>L&I3|mD_u5&im|&3rWs32O^^FRa3|d8Wm|e?V zH%(Y^cwlc&sh4MToHGKa0-R|(>*x{eu$Vi4u%aQM0T$k2`Z*Jp6c!h<7btJ2$8`V? zDrQQ7@*V3MkyqJv*vxj}l6-m|aM zZ@9>z@`fVwc?%rO&CMP5pE+B+e?KRiHDnho z@yR2VRb=X5=>jDcb$RR;Nd*-V`$8EM>W?a6mrUPp4o6D*$@MetMG_ z{6@ajB~F3@@f$zIL%BqI2Om~xIXV5$CWA#&6Y$s3g!>JZfy(95d|p@Z8^iI7QHS@% z`e$jAw^Y{i-|#8J1bj-@l6za@NyoBljb%3%YvkI6;!K>OAu#!fXz8Un1Ew{ZqrFqu zD`Y77cBK-(4|*u435I=_ZE}^IT>-OLFgzTU&xqc=0ZO`9eG2=77O^7OiaNXwJoEpf zpN)b2gjD4oBfwZRmThslQo|yF1yKui8T3InuzG-PZkm^TXIkp6RJJ4+v^kuXHph>( z4N7X_$blPAKc&|ZuS#*Ka#yVx0EA3pR0h`D2C2vH+*SLeX9_-g@3o;a7(}p0{xOKQ z5aSge!*qF&9)#(_g)m()nIc4;G+>jd%I(VeQhUDpV5y;$qzSh3RShx&8D1C2;LVvm z082-Jb|@0ORJ2AOT3;1j%(mC3mFIBj1#xBP75q!_^KD1gHK?krR%I+^e_UWUaUCZ! zeLq&ouMszOUC=_8p8Z3E-34gv zvHXuuM`7?X!{gw0&=4JEN5>avh>n6lbY!O?I_h~5aAhoII)~orf&pu)yzMafAhrtm zI&>@8i7bed28V7!-9^N`uR89XSg&1!oW72|GKJ_RAk0xg26lm_q2x@xA*Q)RYvXD{ zjhaCfVtjCa%poR-tA6)STy@wH>5oe2f(Y9@*2Z7qZ4%;_wSSF~Zw}LN`yiSTq=^Og zPPPw4P;%9Wprio#8hrGFS0hx2Z)!ap<$`sf#V%7TAu^$TpCbe};NdV7j{HY8RsTpF zfHWJ7bxe={BBJ^E9kTKz8Q+W)%x3b_OCo(qernd`dBBw*_KtO#5PzytQbg9M__iM0+~2iCTQ1x@ft|Xv;E*L} z*W}$0{8VD0ELQ5R*5vjzJIOO2x4(h&a)1P2^wP686?f*G8*gInVPmh_>Kd@scfDU_ zP`Y2bq{V(+(JFS_{CT4cv&`;Nh9g6%S70Gj+i|BNG6qRt zlb_Iu#K->J%KkI@(t&i5L@)j)DSu5rQ`|q(aqs#Arfu5~?s}1B8}FO!BUxX4V#`7H z$;B(r9xu2w$uT(GaT!-wn^aPzcsTRo$lukKSbVvzHm$f+m64v5l2)~wP98Ii zj@~8Fdrd|iAX!ahS*FgL7Lo;_=*b7lGdH-!ZDI$H8#id^y@@Y>zkB~xqgUO=3J#0G zkXYAD;}nNJ+md2K64|)e-EpzIV#1TS;L`ByBE`Pw;*?6H%%eT$Zaz``G5*fr zA0~_$IOg2cC%&ozz-G@MTQ+Cb^5wHxiCB+hZ--I!V>^6YE5T5;)^1ow<0t19^4k_nUc( zW}O`2w=Ox{*;|g)9+WJ|ni~PEVoy*P4hQ>L4*{bH4`dH;$6@V|z3GobWc~Z&&>jQc z!UF&1-5b(fxITdDyai!0buRH1+6K`Cjs^E28URz@aJ=gdf8q!zg$P}fAsqkN-@WCX z4u9jHnAhQ9#dh!84~Zc9ulwP`@;9$>w5N`BXktoyzc_O_Hle^Y{n;m*<_8XXW%De~cbfA44isdArU% zQZDZt!nirwO!ra*#s??HM@OfnsA`(_mtU@xWG2P!j#I?%j17ua4Lt6gy{=u75VSKY zm<mP@<>Fhvx>aUq%hs>@dUH`SO*wx>jg#j}a=aS7=*SKR;wC1ve8o$E=N z+}_M_rKchQHy0NZl@PCLK38ck*u0oXI{RcQIh7fGdf6fb6!=` z^Bg$!G^U`e@=}H(9ybsa7Pj46Wo>F{Kf_Y8%|AFKSP>Kx8Xu~9wyMN!|2m0BdgQJI z%?<4KD-BBJ{ImSiJqocf4kotk0Js~-+tE`w+fye>uk6b$#+I%9BB#?vz7)^zp0*=# zTaqNv({67N8yFlE5FE8LA&6@?%5Xb2MBlUI#`%nlAZjp4@_@)r;v2gx~TU zW~`OE`+NDhXimx9o-$E-bzg1~+8hqx&?*Q{UZ=NwF+*3WB?LdB6G+zJB_irdPs{08 za+(M~W9Vs!#dETj|4mLHBLuP{f@txnI`w?2E@|ge<)B&mf--<~4Meg!b{uS&68!6r z4OM~+GY$h+c}l0VeLnDu*XQC zoXn(8MLXgXgOl0#*qFGu@c6KJj`kwAVT<1uMaM_QLkvS>g8YI)yaEJei4@{2I+HvV zr3D7Y`LRKvJA*=^f}#V#iArxH5BIhxEI2#}Lmay^J~@7OTB1sC6{cqcl7j~E-^uAF zW(HX-`jH_%>MNo{#;ois^0E(b^i*t1aL?SR%D0G!@K<>G25eP%ExFA+%uYF86PU5;3b(1z zub^0w5tE!zp(1TaT1m``P0v(h1*N-|sx~x>Y?}hI$`t9*dy`95``?SQne4;>*BCCL zBqqN|(Yig;Ts4oe@kuJ>QpJv2eC#(Uoa0^h`lJe*H-pQ?XkC9vN)GMk%ttArUs=;8ay`Tu4iX1STffw zZc8$(^S|ncMnpuqNNl4vZZT&!IHm31%$c2xhzO1KPIpgpPm=6$-V*1)1~R)gR`{g^ zq&xLFIx-?6-$`OuQSDjFHl4^RZ{X_6vdStIxx1om?x|$&+*^ZlvJ^$x*+n%u&I?jO zRiPh}>2;T1e|FJ{9+#TPil=1MTiFsKNtUN#tw~}N6T(8{xGhZ)5d~~@ZBcU>SF}In z*m=cKab(=?h-fx@PgrmwwYUf-tz&zf-?b|*%4gpe@j;gA72>t+q7?$;CTQ(Q3f`_=D&kjIs)Jx+ zDETY$ixWlapgMkqxNRv2-n3+PGzD+l+C}OWbv%w6{2`N^eA6c1u;90q=g8M0z}3H` zj*RB*5c1`CrlRAABMaykB2e&sNi3Op^lL!1>6eV&f_ZSVAQ4@YjQ$UlSx86cU*MPC zVBYPo0dS!FT|`&&26XVubo*zGBt#uThW#}g{t;>48~K@?$FA!T@nw()@|p4~OC|Lb zo5w6p>|dcw2=I^fVO>!5W=o-4#r~3#V|%&OQ?ZxH427O~=XIP`hST0tM|r2Gc%P;6 zZ)fbs)DfST;`35m`fo3>?e3owhF2&P1K_Y?ojtZXIiRij4{efaL?Sy3yoq1|!h z^h3%(e>c0ZojB4TNBV1zEGbOR!I8;PQROufy+hJYWjEyUFA&kMa;1l)pu(8A6OiQ# z*L7O1>++?br?{^6q8-=C4zb?Fh+m{UkAf%@7Szb8>Dob7;1h{t#V%t6B<60hfC_%<{E1gV{2<;%ax@^S8y`V+4?J2tlVtL8Uq%80I>MFBU@G+<@DZn zkf9wq#OSzg=TtITZXD^5w&s}RU}IHwJ-fTZk6F6hZoUzBZuRo=W$cudD<(}oxavmR z!MuhVE-1awVWu;@09khj4G~`}q={+5?@ptmC-*w4GI#ND$ z9Eo9e-4jjkEG7T?yL1i{IZi}^@6GHieRc2em~n5RY`&tspAW;fJ(Bngd)`B4(P`i5 zD_8qXK${5H(KwI=`}`=D#fDRrh*mR5rvDdRLT-vk2_rLz!G}+ZDE?JM!x5fT&xWjP=| zt{^ivPvtja*N)AKP4X<1&+>*Tn?{hJ<(rsm!+e`gpOvp>gSX8AO(P zol8OhC8D+=O}sEm57LaY*V$@ zrd+PvG;8_{g;Tg&;8NAWxz5R}S>p{mHf`e8n>raT0LQ|eaerRxsJ&jV+IOyy=)YAE zQ>u%6+aJwL1H7yStz(?rXH>AgkA-B4^Lq2u>`MFA2Sjq_#V_@hTdndrJ-2rLMY|69 z{uuxJFVo5apq%Q>TJ-ZIg-e`Ms-hr#p){S=0o)d&(?T1 zS>01rH&<65X_Op1QI%22mS;PduXdZd$%sQWMmKwQi&Iw4p18QZkz96EN=k+z)h~Iw zquUmn^_81jTaK0;MxsPh_L0Mi^VW^ae>OA))$dQLz>Ki;Y{k{;L(dMY@WDNK?esKr zd&L$H6nu3qsjrSpZMmk>yDTL>7j*e4QS$KY-4W&Yr8^knz%!!9C)O~92Q2d}W{T+4 z<5K!<>)6MRDktMw7ZLI0gP?BG{}Ij3*mUfbg2>yBkgrvG5p_D9z4)04UEF7+YxXlFDXA);o?hc#XAjPfQq zAisT4#5dQeBba<8A5o`nX7G7uFMKg^wn*J<$w%ZU)e)Aw=5w+EfscG8IhmuR)Kbn5 zV{XqD4jaZyyeQHeN~_MF2j|9Rx*41s#JU@ZHxT7`jE`*p8Y0ke_#WdQ9Md6VPw_Bb zo;S*COvqO+MYGO6+VFx!0fQ0na;&+!^@3G9M|7WBAE4cai!M%|S^OhQSJDw6ep)j- zeaXJX+<{uTQ;T2IVFj3!Wnk?YaqYP4iVjt^iM+%pb>&h86ml8uO-KAZv+Vd96|5_y z+`fR5N#I@CwN6Qn3jA_SFnkhpnn{9gGwCIVQzL;UV6no-ZRE!`o#r+&tnI^BiTH|G z;ECxlI4Xu=ZM++CMyKVBZj1h3-Ol`dxU*rtuwG6OSaq9~IwfTCbx>aSyjODf6lk@{ z{SUuEBkU_xhtvD=k1sgw4w=lmgZd|wOx6wOe*xz($gwBkJp6RMPI6VEZT#D&B$Ku& zKM-~^H~=5ID&e^Pe*>Ir?p27 zm@npw)pls$cO`EJBm;P*&)}06^Z5Y+zqDQ@njk~v>H%l?7wTfPfwr|R3;ekJ3mPp) zOaJl~qaMHudA3shqT4Ph?oD8}7H*96{+BmeCqvJiiI;*qtx~Qo7Mgm23lE^ZL1vAc z)HGErP%7gJFZdVp8QNPt0Idl(Ylm|*mlc?!U+~4af&Y54V&NRUQ@B?=KW@Jmx7gJq z!X<^%i}~U+A3qe@#B+c(TCL%t7ETqek3RgDx6o0-qyKvY|MET~@K&P@P{8XDhr)mB zbIn(pw*I5*H6wb4ACM>&E;T?bJkbl`N?`CFfcrwTcvyXH9|{Ae=?h^bgeOH`oDr)B zklt}f1<-b(raOwysL_RIkW<1Bh*N$zjSMXm8ppxv7y9Tt?HS~CH{n6i3^m@Z{BQ;B z;25h1;9z`#My^SP^UvV9J{7^`1s&V|5fu3|@e|I>j3(D!yq;vleaYaXfu zZ3?3E@zTZASR+DsVxfaIeHg9OyhDEoL!^-`45PLI=A-DM|IiC~UWlb#R~HI03AyX) z)nZ1F-C|+j(ZNE;K)@g(TBiPJ<;OevbV4}DpWuss#$~>Gz!T76Xonn6!b4m%vq+Gt zzq|!;fmCTEfYD4n8jYv@%Uj@h*385(+z=o3m$y3X+IRK(@&XXD&r0$P?eh!W8v7A3iO+`HZeoYb^NnN8ZN$l(+R~zk8l2^b6n~T)C zyOgl#KhTt!76@EqS5TP&@#ud6tB$?uVuUnWzjHl9_XrsM)u0bV0g%z~j*n4Wh(5`w z{J_Em#?3)Zi!xLnxPR0sI7Ak!4+%K{ z@K)e@g1h_w!DqHJM5zP0yN_GF&s31*ay}k!;~59=>hV|c_A2uU=h>A4Wmg1R_l))# zYf->tXr}{C{&zw&6EKS9>+>yW0hP9Vp=@m@{vw0}yQ@pU14ewQKMkN!qEB(=dSD_W zGkh{TEfMEJ5ONSu0s~23UGmxoiFc2fbGvJk_UB6OHMaapbivkY^y|1rm3gjJ(!AZ0 zN3{DW@q&;I{+3%Mqs>+gp`XDHI6UN`nW}DUwg2f53HhSiu(Y)u&lN9PkH1J!k?#5p zAvu9Lo{|d3OykvxVXGI6*scN_8X5bO^e~i=p18aWipYI^ywPW_N1w=Oth#(6^U^M| zOhOzH9&r@Y?{{wA3cb!{({?MCcLI>kNo?zoT;R%WDGx0r!xUY|?V>YM79LsHWOTv# zNf=~pIOcHchdKN@4T++8O3m~fFRm}he4&KBG6?0A%&FO7W#;yJX9;|knyF}y`dOng z`LU!Mn7*=!Le`s)A_emIBtwLHWY)BgM~z>)M^+zMQH{;I95^e#{s|Nm6X}G9^lS3< zosL%0?I0&JY@r~q{E?9r!H-)YcbO3A>g_H;mE>(M>}s1!Z{Aj&KhjohL`4;xo{jr! z{=|zOXTbEY7!DxF(=%}H00tDn*w)v19|O(Wi$6%OPmDo80!FJ})|ZdhjRW#{D0c?E0_vkjL_ zj1{Lurbbo-X8HG#J+1f1HD95ayl&ZS8@uG(&EWds)3BUw1o1CyW86k>;>i$WzCc8W zFqBcVl;K(7J+`d^k)9<`Ehtr`t5a#L{IfGaK157L(68o%Z1lC+)MvGk;|x&U&VG%Y z@rn19nQ<3ZB*F$5OZNBTs%@0M(i z_sT9-)D-77AMAj;pQSw~E+1)>y`j+#ETzl0I$17K^m}MXzI${$yY+%{iPsjlW$dbr zh5L)sbBp4*+}+WUS0WKqS;nhD1DXqYUqcw(3%puR)-lu8Zn8I9Bw04W>E|(u(dDy` zI;x6%_WK_UtqSXXE@W?@tzwg}mGvkUkuo*!5~|u2w>FjzNl@9utcrDy^-AjFoa$Y5 zKykVF+Og}Zqu2IVyl9dTk-EEBuU?AkE#UJwqXjrz50wf<`_wW`0m$#ZFu3lK; zzGt3-5KqsJIub{a)(Tu2r%4~$2S|>h(_ohS8+>uKqz_4DWUI`x3q9-Dwvy~qfUVcg zE7Kr4H_qQ+y=eZ%stYReSS*mky$@4Tb8-!RVhHMxy~JQ7P9H%eNSPiB!Q1yT`M^O% zm0zhxhRQSDJ>F>d-1y$B;{2k$_DDQZe2YsI`}3=7QdCLBMKP#sNJNVnYlqlGH%@kb z%bu+zWs2hT!ot0(u5#Z*5>uuQVVX|B? zzi4Try{go`+W$;&J zNEr}QLuKb8p%)n1?XnuQfc$h~s(ro8=sB)Y>YN4<#cA!du4EQh)VrX16$oBXO@%BR zNTAD(46gkw0W`QY~Zl5XlLNLuU1=yg@#&}j*h>i#IzJIm*1>Xf{Y zf;_6>f!sRQ4QLXHL#20fyW(1I`~Gs(12L6to;{Ru3i?^~q-&>;Tm$y6>42-sb)pw2L@JeK9sI}5gE=)gQamBrzA=i3BxE1HZ4eJn z*i>b#YF>WE`6&<`#K;|y_vwhcqxHdvn-3-iJts_p;@OjjEbGMM!fh=Hr+f^MZRFF-^LZy zDa!oww`N21Jrfq>tjy?bo?@B3EnVVJ;B}x)@v!Ci<&*nt4R@(Y8NOF4i<{ASQBhx3w!g93$t+E^XJdkixkB&t0m#Qa03p!j7<1;X6lMDUu)R6& zfXckYa?hMt34lMn=(plIp_abpTO{*LY<~uiIK<_usj6NpE?^Kh667L4kPG`1)0K*q z-lkf)2(W@Q_0qSBbx+)yCvhnEZaShklG0RMrpnIGNvw?##>gLKFhv;rd+&PO$1c_r zgVK}ql6iwDc%FQRN03i2oU69}E7vG~Dx7=7ROL`!zoTAAAFcuzz1gO!#;chn;)j3Y zJY9?1qYrcY7Eek?rOCR!;K0#;)D*m}nmf6BzCOc$Abb6o|+y$nqZdev5b=@mtRc;0ayOxg$_2^9+#Q zlpcQ#*dyrGrjL>mOK6^LuiKFk_SrxswdG&ov!q4J9u2SCp?(46NC7vJJ?K>gfW}zkW_ZTU)?Q}p(bC7c@R7?bBGWxYg7c0kU zey)L1HWEetBsgTn11Y{uz_-JA12Rl@n(qTu>?^2ZXE13eKo!e^DrTawkj$z1VP%#^ z6`KH6>(WcFtTNX#KnU}HB9Lr01TA=B)##PAH>5&cD!nIFMoG=Yx)6`El8yB#eWO>mJ$6CBRS%DObc05>0Z$wqf4PbYSn z)#W#@YA>`OtTv`hB{z>xMo$}Q^&wD#3HngNYiLA`(1^?;eIq?~OSWh=qQcy!HfTgD zmiCyue1u7+bv-nqr>nO*TP{|7e}6vt?p}LN%LV09uPv_2*|nRX5vAo8YBeH|sBY&^ zkCN6)AH7b7m^F9lDYu}f{0!2Gl_Y8uP{}>c;|9KdExRXZ4zlx}g6v`cBL>%AUlZ0l zVH<2E?=i?-%VCeM<&ZM3f!0ED#w4Yz1I6CZ+Q%J)7_`1_F;xC2B|q}#gsUJiND$?~ z-#j%}i}+3UNOQdx^lObsvjNiIl)@3hw9v@AFf*R|V`}{RSu39nsqgifAog$>YO}b7 zePE}cvdsFwGqo>bULdWL9MIU2Zy93n_8ANzJ^I=2^g9l`ofG+QU`6FKnnIV@QZ|bl zDy&wmvbQk>Jv6GZ3MtkftFhkHC%2iqhyS}8>*dA*p&F~3rW&iIQR-Bd-bR!EKUQO% zgzVriG2u`dPq1{Oa6{OPPi>I)@FhbEa07-cCB(4k}qfb+r*7DM$#}woaK`JG9f-a+3H_&>Mgm@+YHKx+nxCXiV;JT8G28}iU z9Tbzt!cIF98|#lGAC_2t|7@5i+-Mz{P_^;$gC<$Kk_p5-&(! zS7ohQ*L20<7Z!jW-8ZDqeaq7`^ElZnBjkvsf|C<`dGX3)QyW}H5PyHj0&FGpPmz%_5VUDgys~_u2VhFDtR#)Z^JQ8_;C>g zvrWIV>8kRQRCA{h1=IVnBbK&-b%&z{ zW+FPGiG2MN8OOf7S#hDAJAS_W)t`#r9iOc`r@FOlY%Tqar3U>L((a3p-MCQnpyuGd zTWouavuO#J?ijx{$yh?0;Y`uvWf1zl+M={SY4*H>=%8;gqK?Fz2PL~^zkDx_or?lh zt0l5}%5?mWS~2X)Tjgilxsw+vQA87}f!*sx7gT4LPA(q^Mm_pff4Tr^m3PQba`MHM zL5S-NnKlT3H_7yu0D}&{`jUt_L5M-(+8yr9?maF;TzaA(B5~n&U4ulQF^i11+gfra zmd;BSD~wZ(ifmP>9;yCiKo0!2GTh!Wa-M{KE#A4*&($$NLR3t}u8hKH@LhA_%DsE{ zMX6v7lkVM6)>igzxlK(+drkcj4#jNs=-2`!GQN3T{7wch2Jp~Gj?zCB))Eu9kUL$I7#LTjGs?e6D zr=Mjxpf3H91La+H>Gdq6!awi~2#dB*7vWd1l9Y;KX=JJUxAV{RS24 z8he7}eaO8VgyZ;9F_QASr2iex-GLv>nN%!{C&icmSQ(iQD`V$p`oU6ubO8!uU|e=* z_Go*Cy4J#cl2s71dpLt3s#niRFHUAgdJlR=ITam4is8wTwQ5(+hf;naeBH9M!5}_@ z$UXZowh?G261N?p9oxjT-3bW^lB}%yJ#B1TPKftjF3Bfu`*sCwz=1c!RX#Otg{rzu zrfCz{AFX}XE*6H>2*X;4VO=CZe59!7eN6Me=LD3S-hdk!0+nGy*Rq0@2+rBnc#IYB z&aQ!ip`;93P zPdTS*SvDho400*I$Fi|dvur%9J5+U_eS2yVmF04%t$t9ccOG5%H@eVN$h_99sl;K5 zlDuOw&Yws;z}`7vzl@`G&y-@q^}j`DEp^)I`XiDs?INCEF%y*LivXDUlds{rKt*7Y zER&zEPrgPEeXR}#&jpXl)8Ar7ehsci-b5WK=O;W@PhezDD14-l(>@cC?=gqYy@h^I zkUn>iUon#`ocyPt-H5uKsGa-$K+8Ex7h9cw`?mIc2jC!6ZYi%~h_@mY5hS}BU4zye z(e5o&xcReCf#BEzV<^-l-X0FZZ1nhe5+pAbqv$vmS&bfjh5kUUi0EVnO1YV6GYq`$ z0LThE9Rn*64P=FA`0=26xoF4>-39W(y8?OP#SzM;J8nu|LM-IQAH`L z9(Oj&je}iW%~*5iyyg&Y6z(@hbH4>`PM6qon)^-3i%pNj{o*07+{8mJBtI?@8UdXL z-~iw+(bO(ikN&$|4z;29Cn@^87?j%)*jF618%r@NQxJnps7iuL=0Xg9X7qlE$ITBR z`4|cxea#%&H&m<2dC$3#ZDiNcHLu+vd;+sT*6Xe8QUJ{)VdnQdn zqo9wy6I=Oh47LuHkU>AwVd5FFR;9ox)Zton;$2knasruzv=kO>$mg4+J!;U}$$&l#sZz`3XdqhLsMYOFfDh9bcC_z_o(U+xMnSuk zRq+Pk3ZOcZHXAM!D$IaMS@C_r_ zz)m-|8t=+cef1o0n$z(@85+o}P=4JcS_|4E;b*_W4(G1hOzlkHsGqenE$0^!6)>BUn=VZIzR$Nkr#O9Qg?> zsoQ|a9Y|IMaTXIk@;JKh&P6fl(}n+$2@1uvhoHDxkRj&S@E-92!bbM=ds>ALM?>;JyhUj=N0Jf9mshGL`2N>Um4Kbz-#TQA&McV7yQwF zD6g#r-dQdEj1URhL#G0IfQmW}f)Do(qN7u1tmMDyd?*i}?&Y+`dziy|N6Y1sHM3oZ zARaI(VNr&q>Zh8AYl$vKD_)`tV@T3q$VO4W186}~c z)FX1F_Copdub%$__wWPE*152%5lWK1d!SEFF~6QU`S8rT&E#<%AcRv%T|SlB*6 zMNeU3tEb5k)b0&x($YL<`%SJcn>pH>Y?l8ae8v}KY;$z*+~&;bMN9R3qm(`3h(QS2 zA3zLH)=l;(j&>6wR^7;L7&8WBDh(s)-g{CIt8jc;npGMggL zOR&!XvjGN#&k6Sw*Zy3`N@}^?#@&du-ke&zjq}Y6%?WRjL_L+?N~oxYkAI)#lsHbW z!%<51p$f^}HLbr2}K3_$bi~Fm8_<{dHM3Bo{n%gUOa&Vq*h1vHI`NE;?Uk3de1{@isaC!~^HPf>#LNT(8A zoduBYe^9K>d)glwTdya#hRH2m2f(w3@_Qpmd z9AgIC7IAZg8%^Pd*e1l4IxRAFn(zek@iPyX?WT`U4Txiw_{^<>>Q=7fK=sXQic5Ca zS07LvSUn?uGV4LsVVu`_Fu+mh!)zJ~-kUz~noZ`ih)BLT#xcRZK(euTD>6pvGHc6= zRAq(5iKpOvHqbx0vna64NmA~RXpHj|-;Ku4DD4T4>h(LnM`;2BP?X7d z?Fpkar;Qd)yOl3}AOr+0o=MR`7Wx~M^b)#e9;PHc>nu1C@} zdp?y5a)@;EZ7;X@tT5#qH~MVxTPY!XUT6vp3|KpN z`m}ZD-@a+RfHc$vkn{PFbNmAkQ9)K<{J#*SrHZyOKNv0?How^TkUMAP9JD=be%QKbFv=)@6Afj%FR62R&l$B@Z6mh7rs7KcqN*|(UZwo=Pka<$f zixot44@5NvPn$AH%xf~~>0PJ~eKA1#IIb@)N?e$I#Tm&(Y2dyblkBW+xrZFfnA z4M(2o?~&Jk3Hw7r=8K;!>lkBU@4LxFg#>=ZQ>u6iPr~=Z-$S2~!hh%?e~wyK=aZTn zJJ{xOw{;*z`LuVSOuqH1v)N%f8+AYKW3bxpL28)!lG+Y-t*&<$XDsZI2a=? z%5t-}_i%HBev_n?L^hrOy3fc)CE{fS< zwjs#I68dlJ(S)>P=!eDH^#m{-1HfZ6KRqY3>vx)I?ro4}3Bhm7&PZ&D1V>mxQi8B8 zp4M!OE!tIT6f>7BJ1iCU!#R7U7li$=udv!`qN=Eg%VUnosjgZ{zv7i5I-G&IT_w`4 zykewnR*Cdx3VZZdPr*O^Ip|9c%6Iuk1coTq#jn|8r~0At!7{iK(&%qd~<1R?f&VRq9y z)X8dCE%iWq9%RgOvG$3FJ;yWmARDl9RgqWvnrEZ^nNOcxwoLQ%2E+bjWU(OMLn*vv zV97&T2O8;O)a%nMwUUAp&rPGKlt59*f}+wYC@OanDry8p#bR_ZghG5n7lnA28bVee*9a?LFEUC*doeVEZV7>6azd*a;pzU-jOIWyS}Z6l z0>%e-WvZkLKi-sy=BDN`k&e+j61GdMGY&c(W)B}MtjXXK(xNqYg=aZ>bG(%QihKu_ zHni~z?AJw|OS|k>+sO#fLyW%c5~KC%;{mhXy+=m(feY~0J&|#R+`){@?A!||pCs$8 zH|hq+Aq`54b>ZcBC*Le+Y}iGDN(UGJQNV0gI=@5KxrL)}46%lkju0$dY`T5LbI7tT zRZn;zqYo#xFoG_q2P{LE!UQg0h?8~`=Akvg-aUfkV*^s4-G@IeD0*{mNcja2pttjs zSG7AeMFBd~l~N(;8J?RkJC#Rw>nR*o<}Z<33KhtAIF_q zO_12@YMgA%(iJb?yz5U`qzJHM5Z1P**X1~Uskk=F)5g}r(+U)|2&2Ea>TwP2cI$yi zWIGQS#S&H!(KeFVq`qG!zLu$kM;A8 zOY`7lS-B27;@nwJ-<@9TITA5eKGSEt*&^1~F1^@;+nyVo8C)+(F5VlR!e;Fa_Q>aC z(>-(c2B)&Adt%bT)Ey25d^FK@5$s-M5tqsnHhD+6vokDh#%$%5Q7^&l{ffL2uu9lv zszfN*hw9%dn7w6*aq$^!Vnl>D_+FtTY6=cmXiS3h4~)8uf(IlK#;zph_N5mJjU@n6 z*HxMn?zfkdJ?OFo9G$F>3))u{Wlql`A|H!!y@fhn0!vXzIq=;fnNkxUa3H}#D2F`F zg-z&< zMwYjm*#vLECiJKcn-FpZiLO^>cqVbg`n;Yn2nh=mnc!m5lx3&BsND%S$iZQxL_p%hW(cn;4n*-y?8(AekoK9UBpe1#&s=1I_jn zUn$>=Qp%fWaC%{x@=ia(OG@ zpYSC>ChnyJBs7;H_#_YsbYBCL+1Fn}io`2v5oiSVVhlt+iWt#6UQOS)Cza4BW<;E>dkUTIaYbOhR^0ANg6PoU9P}lq82`u!Q*o z%*``@+t4!A)Owq(1GmZ2YxP$8rKAyTK^0_`=zeN>^M1BE%X$ViSVKRb=dvv^+Y2N2 zD(Wcz+fiiL2`^pUX-3D=pQsTvC8MyKn!qbBbf*zngTKbpm7G7ia|-zm2}6?r{q6%d znUHmZLBvEKkKaJsNG=H9bq4wgTjy^{Yv{n|e$?<_c zz7Y{VR@@@GQ50Yw>gJ``lHrzD-dcV3h{|JcK)|kDzCL|CyaIy#*x=y!_~@wks5mYr zF)A)Hs)Jbf&gh6r%u>|3mfEkKwr0{om7OUd=OICXeNL0jWbCbsLv+%ZW-^ve^$2tl zwd@Uc57-s9V==d5ITf$iZbZZ=EKeLG3Y?3c6!Z6sdYml0Qo|qJC-d!$Lc;4n6C>Nv zthrr6@pSGP(MiavB#S^Parou?-xOC?9hx@HaPgqcDzD|imVs0{C34*!<1)#*a>tfK zirVz@$`VyoNmkHH{ucngA&fSxp-Y5ZQR-!y6>TqO=D*9e*Nm`TS zP-ac}ezvaE#%R6kN^5h@!QR1h(gumFf^MO|%N@s2^~APbr@Wr@s^4Ev=GW`y^FHz&;xz|s&Yi0*KicG(GBI7Xe2;g0x*{(x zy?kGqk8Ql_|Kskxo4b7pqtl;8Qa^ER^TZP-z5Eb5`XO87$KY4%ARGn`h! z^$XSTy~?4QhED2|_R12ycOSLOE~Q4S()O-$^-!)kI~2=PWk_n<)ZA~C`t0V~op^A~ zLFvk_I!6CGY-g^7R7Hy3@yo50 z+~v|rr{%GX-8yQrKB~>^`NOPz2TmT;)t>v#fmY*|^|!WY(Yr}Md#`$H>ugR8|N>jEtU8lYSnVTw7GlX%=Q~MZLE{X zZ&`P0_4Z9Mri5L)wnbXkubwz&cyO}urEPq8A#ncm#k04W#s$xs800f@>B!ZSY*SXOnjK|j zod06WFjd!icF^Qb^zg})s#6wkbbCOFLnx61@y|AYH-FZ$ZUt7aS+w?$PxPGa(=OPi zpPsuV&S!ntPYc%D=5v7RQlHqdtJ^NIbyzZf$>ea;#NZz$kM;@vaqNnTwpC+S{IF-X z>BtYO$9M6mIkW!MK%4jFw60Su8TTD`m7Y~L?Bny4x!QH$6!!!1>nbaOjt3d3dhg3W zOtI|l?Re-&r5oEnj$@b(wEIOpqUKuPQ|Wb=_Z+kILz-kE^&;8RT@OO6y9p-n%m>_MXxsa6JyMgC{z2R7|ch`_!IQ(0H$vo`;nlhxw+qBkY7P zONMnDem=X^6lrX^tyfINho-)Uh=$>PWBTDZQ5RY=9ER9kn|5zb;(3Mg-wkRx)HKX^ zE@;IxC7WtGi%;%zacNFRm`EM7WAk1ub-A*AhG?nJj@U4s#g4kOzL|5?9#ol)qfVBd zi_oeJjH`TKt8zoX*uKntDGSd$~6 z&$a)yNh{U6!m6?P*Dg7@@s|iwMqXo+m?5jjES)qizv5l~eA|SkCzGxxE4mbu8t<@J z!q@(|XwC99Qy2fhE|ZLY#t~!Z%weCyve^qbD#lwaIq#C<`MctIiIMxwyh;JXhXYF^ z_u9|wjZMDhb9q=|hj`nL?%kKGMypz&p<*#Sj-5Gnn|(*nhGB~bnN&O(iecu^iKB)D z4GJ1KvuLPEwJ_R#&4(7Vv2iFHgC9OC&IwtOEZ)fIt!6VidaJ*4&vBaHaBk5z{Ac5? zF3IZ}y{~8+)oxyJbJ*E=*W}Zr3+!7`7M*-~(oI>E?qMmY&O2GqeT?yC-p_a93@&!d zEwiv`GdN=z+rs!Va2agLGTgde=2e0n%h(#G@=)a^w~9y#nChI!^heurDcg2Vf1}iR zH+g67R7Y*jR4O;DeWPkVC9l*`bQ5fA)soAqjrVCgq{{$X?%$@*S-5n;ym|AN+3ATF z{^+wcIDFX9;L$_+!~`8ab};IfO}33kSH4`JR5m?`JaPH9&(nri{npy}jPKOj>wL~` zKOSq#eP-#x*&I1NXU;VH+!>*>XZZwe92*t2A)Gamk)1mB9NuY^ZFtLRP3EdqOf2>D zQJ-6GOn)#zakJeVccjfXKHv6f-*kkn=yXffHSN#*?sIAzXZ<6Fe||R5uFTEg><8D=xL8-pfxppVVY^zhIQK^K^Qdh1Grf zUdFV-YP|k;BcHsO=7?6N>sz?9BcXxAU#-tmb>d59;ww#rYm=p}(Gjgq=Kzsu`mf2T z#OwDOGYczaEnhR+UR<46g2xVjrM}4E+Q9EfYi`-SIp~o7VZ@QUN)Dfg!!NXnw?*~o zzqEo?tz+o*+jpb3cGGuk@sJY)IILo#!L?s7`p0?mH`vwAj(QfHsfyRpcB-Y=pf(Lc z1eQjfRyLkeHl9Xy9HD8=Cvj$G21^UdO=Og3Qduot9xY{3%1yL~ow6upS2NzsCS1R) zj^cf})uN27Qy%QDQyxOaDGxn3<)OWu=Iyrxo)~lXlCUj(7@PzoCyog`Y1`M!65nn^ zi~2rnsj^~#1~mS5Lws9~R(d_^3}1=%Qf6{$gzcmTj z%a-WvBeo2-FI0|A@}1Gjf&^PJW@}ScK~Wawtd+|6@Fx}eD%I@XH$9Z5?7MB6H%Xc6 zICUZ8lro1Mw>Le~CMLP19d;bEG*#zhoND4YrOsvBvhtcS=$;EYM{lIKcisz*v&xJM z8GD;>>NfAxqDd)ul7~7{EU;dR{-Qxy;7HMbV^I5NOjY|hrt0fw4zV~+BymO=$(5i) zcA1}bOP`>`SUMXp%#DK@)wUNaGptgW+S%0k>=EPqZDC5q=v&XO7C00CbfaQfU^?<= z&i;vRbII8Jh*2@keKht|f0H8B-(5ki;WK}zzH{OcW0=x$=!0TUP06)YL$q4K>Z;Pg z@oWw)Z;<7PK`E~oqm!@OUq9V;KUC>xI?}weaqiGiwc?eWixa+->?JEjlx|-+5c*S7l6%t}Dht3Kd_DJEe0x z#mhL|F$BU7(L1uAvZ%2Jd=z*WFDtpvx+%H0SsZ*(E~CFPQ-3zY+oD7p`F?U?UOU>K zV!^v(p+zC9LJ|6m?5!*jb!_A$nc{_JFOlu&IJb(dmOb}8fs(X&|I zuB>5s(of1>b)9lRJe&%p+Oj``Z6VK?rhcxTQ=aM7HR>}`D)AyUKlXDlTglrem7h=Q z5=1PPWc0BVu2QwAD&492@m*@_`}b8-{q13GEWe&=U9W!YRt>&xbL#g?XHNafvOTrj z@wA)AbxP3DTSqx2S}kW$Vvd(eyZP+68e=&6DYqr<=B-Tj8~>Rw`RL7~e2$8Q>CM!b z_RlkKmTYfSCLFf}q}{wpXTgU^11;+ZDLE3A(itI+$7#p(5kavb#*8(YT~#kfLA7TQ zz4u`cWqyiVnA+ar7?OUv)~AqW=^?~wn8Evqcy*qaY4H{%?9+sl^aO}iXT&KxZ&pO>6&=gZ`-MCv?C+UVp?>6IFNs+2KJyL2*$ zy2DbESh=EAKQu)R)9P+YtkwScWk(BDCusmEVK*{{QhZwNDNnV!b-KlBahX8UGXjb5sowQiH} zoxSbf?H+sSjZd0iBPfzKY~3Y~^pHjd$Ei;tmSk}UpN45I6TqLXh3nwcS=XKS<(dgM z7-{FIX>klqpfxw#9YYQ3_l~}LwMa%O3v)|x^mH1lx4nGhvMx+%Yf0;KIkOKbT~`WE zQeIjdc{8H)>*FHU85Yjt3%$1aGgeGlZBu{8(0J|`q^ENm190Qg5-e#$Kh4yy-L-X< z;m3K4Lg(8;!zQl}j?8S7-a3CspAmu*8VT|mSaJFL{Ix5W|8h{-o4@|5;JR(pGDRAx zwdWU5XVa#UYH7=sK5Gq*{EnCmf}Y$#2IXnSV3nYUWi)2#jpeaOG1NY$R{Ob4{$E%1 zQ$nVl(x3f)+_ECpjBa0)lQt}UrhaSCuJOjqVL%8R!K&GEq@F&lNlK=#A+36P75$bG zyF$b=z;$VxZs^9O9kHYh`1HMg&8{tmFdDPSXZgH&0}k6VbEmuM)2uhZ(v(jo%Ze3K z3{%~gEuUsg>m9273Y%(KwrrXa##Qp~RQ%s4g?Ebcd`9!#lhF;T@U`M^H?`W^)3$9$ z)WpM6O={E|CCa#O*_M?N;ie;dw_T*;k?Y@6<#3x4UeuUb!@c+Nkfo#9pF{GfHs&vWPeY(G9l zKj&w>Yt};>R&3omYE}29RVIz=Y#;pn*oncz$B!CVeQKbvc+w2#*pHi}R8CTUN^*Of z#-c2pdL>D@qUg@4d=BA0tC*6M$4l6t`B;C-@Q3nPzgT^opx_|ch&~&PhI)#_=#WlMoj1?zO^SxN=SN}q1bvo~)AC1DN>Y+7 z`Qn|JXVxdz2RBQyCpV6%TH2@VkXlWf+L|=2+`odgWbLS=#`Y#j1MWQZQHH6_X>ZBf zhI4$1^{U}(rMmve`t{q6`CRY5qofVzY?4~r^7esoS9r*f{-Y+f5CALnZzy4L%FY{Z z;f|G-&4%blkr8JXnbc@w_sLVfoox$JrfxOX{qct%=if9b3yl}&Y#Oz_q-nEZrczq3 zelj=;XB4(5HMbg?FPpP){zH>8)X2KV*LvKI*pGU3y)m@QtXZvRnSzv_TaE3144of( zmA$~mXU4ES%a$AuGi_CC8B3|FODL=LYPBH4{^is9+tn$?UQ?!an5mgIJCuqMO3Jz) zwkr8|;fzF#$XZGkH*qPHFQIUg8R!KU^MG93D0;q0)ECbfexwEgTKx^gCf z6(joOil2AwT|cVp;HhKBPP0uPKP|K!R)O+cirf9?mc^4Jeo}g>hxEUuU#Rt|t$~dX zqlHzFL?81e{S&91AmQiP{#KL+O3THm59V?_Kw9^rneB{WYMpLhtDfUc-g|sm;xJqJ zdS`-S-)uUJ8X;3OG*s}SwZR6p+5tmn*wi?w=GXu&lR{NVo-+CphUoWin z>nht<=+VVP(Vze3>d6ahy)c+neiJ4@kregH=ib-#Q0se@D>pKVh%Rm2NqVDo^hPJq zvsv0ctxvL{<iG8j^RTIs>&m6gdw1E^hcDl- zl0zl2#M8Pl$(7dK%`rQzo8_y3K@IEMn>6cJv8<1OOtmWwY)z7H4SsILxm}U}uFnUi zHcGZ9H>?)(l`w=ESU+WL&FgtP(>!;v_+w|9*G{*m9y>EDThct!yf6^Fl^4A9+NDcN z*yZM!lh)m$c49K3qS~R5DBdoLe`bAtCnh=Be(6%oofkeYL>0D64Xa0UdP=gYSd!M9 zial(5aQ4}e_g1BW!VUzMWY;71SL;@&A91#Yy~UY9*KYZEE4ilbcf8r3>)3sz_{VV{ z6%RfR^_*`f##oN8*}i;N_|}aD;-WSjJmRAm3*BNNVu;#GZLW?{tGwo%aMmQ>f28O) z&0RAi&Zg#@WC?Gvyyq~V&iw~;91*l)eTc2+{*gED`6y+?S8WTPWt88e*|@H~e0neI zx~`M$wW70(IXic8tWV63n^y0$g{@l}w%odV+mzsNd*qNco!a?S8Q9-n92d<7su+ho zM)z~fgIJ|V?1LDrytMJ`050s$QR6-@inGbKz-tbwQ1TUdr{=ZKA81kJyA_3^su4i! zUgZuhDgC_%iktZ0!39gK5^UJVCn6FIdl7hbbgVM3iLsyBNk7ZbpX2=+8@z+V9dS#Q zCM$-lGAKDb6ubG%;zbJ4&~hxdbV-(U zQafq2bVgTI*HRZLd&ytR&E)y=M)`pJ%B`$hO}D{rquj>2&2S5KTi~|TZJXN#cga1# zeSrIT_bKi(-PgEpa!+u7>z<}BqOYm%ryr^xso!sKH+UJ^8v+dj3_}bf3}Xy?4Oa~* zhBRY-V;kdKV}voz7;ijgylA{;yk&e~d~PzBs+yXZTAMnUdYFcprkEC)Hkjf~7frvK zZkz6z-kKcQ3T3O7t#P*Q+4^Lgoo#!z9ogcuoyvASo9ZEZ7(6^Z3VM|BsOV9}qlQOE zkC7hJJl1$@^@#G=<+0D>yvHSv-#o5)-1m6w@yz2rUy#+A-OUBfh0N8>EzP~nW6hJz zv&}!7W6j6SSIxK0kIXMUb)I@pvu7^P0-mKjYkM~EY~|U(vzzBo&mhn5Jr{Vc^$hpi z?zz|Vfaej<1kXgz>z+?I;K<;W+bgeE5wD6~b-h}8b@dAL8sIg;E7)s-*KDt)UK_k( zy!Ln<@;c#_=yk#Cir00oTV8*7eemM&eDB=eR`0^zrM$~|SM#prUC+C*cMI>f-aWhr zdXM%V>pkB4d++I%qSXd9X<={Huzyii$2l@_MOaeZIPsQPQmyT|;9$%TZ}}c;|F_MO zZOPx(j4EUeQ}bAq?ANtzQUS$PPu07YwoNL9Z^zorV#)64@ENPKmh9F&!EU|(V9Boe z&+h??ouvQfo4@A2t>n2t)xutaz$Bf4_G72!v?mcbPn31NQ1KN#j%a;BsA6Nrj z;rFLXo}>FF4c~5GKYsaymH2>}B`-Bz_5MR`U{(7wlrE=^Q%7!aC+urV z=^q-*k3Ez<7NxG?{MM1J?8A(qA@jyh^=Y(l?3#f#rOFmV@XG0{qI_m6LuM~4yUTPd zYWJxJJ}1W=8yIVg9^7Mf3u}gTr-!4WJ45ZJ#=Sq!p0w7!X42A8gM0=~4;nwoHhDtG z5525r4x;^XyJdwxw_6tH1fwZ@(m-stZ1#D(WwA=WAR?EcA@<)TgLh)zUJ-4r-8zo$ z+uz=yT~KWf=MA{erW4D_n6;-4+3Fda^$2X*W$1?8Q*A8v3Hxr<@VH^7fqPF)_}!|M z|AXU~?B=YmEAd^W5WcGv&v%tNe`_?4^mXlW`TH(&bK3B{N}%i1CsoH*H4n!=y;_3P ziZk}AddFUI8kEvoth%={mUXbZBqK?0R@z?hP;VNltnL^)z}9Ei`Ehrx4=;xA-)Y|# zyCR-h@|*MuSB>pw1&<$R3mG3Yb*Odd@a1bJ*hALM+K}L5wy@4b2(p~*;2H);jf+a z?4GFSVXp0KF!!|fsOe#LESc{iHW%#?n~Tb7+x@}}W}UVWk7anQ%DQY|LCs>ldVRZZ z156Wjv0mX@Lq@Z@0>7B+ON6=OTwft<{ja}5SpI|2JZ;@n)3mj*lb2YRE?%;1@!W;e zm)Kh!4%yh|xoOR`IrFDm`C`JjsrKIW2KDUT$24KmoH;XmrY)Yabd6c;ViCJoKc8Ok zfR0jpWSCXzV#g}WJN`Poz|0p6XW83LGl-8DUQI~2YBOK(XzXDyUzzvqXC^8RUfJMO z+{IGjm$xplj&DO>v!f>~<1(tVa$vqeU8i1P9fzYGtIM_+aS>gBsD%v|!VX78o>t=0 za&X3`K`ooXc2kF*NF4RQHl)ww0dwb$o?U=>12a1zYA89)(d*aneVWT{_gCuFYr>FG zwvgbNqvmumJuoo;yLPuVYW>*3{YH-KVxML(tD#KsCmpI+{HyLw8n*0p;l-Q1mlN$~ z%a%2B=PtExow+7>xX-|mqXv98c76O@TRB7duoIo0Tc0QXa_7L9=$;$w=6)WIoa=OE zR+jsj=P5b2C^_TmdFUMV*sD`dDa{F$855E%&vD?$(dQlsIGQ>;rtlG zAHEL7$C;b*0Y&A8{(`}Ls!795rLF!o1~+bQZ`!DT5tR2=TdE%a##W{Dksf--c*l0j zPkonk?dGGFuT@;l-l^M~$f36HPFO9uE z)^;tiTK1#jFFtB(p5U=`+1%O7k?f9;F>aQJS*+bR@9ErTT|KK>OIv$ecXg}iuJ*ch z_V&>`+#DX3C8HOP8sSsCW5aI-5C3WFXxp%(mg(U?Y~AMb`sk&rF`=yfkP_CB3her&4$pldY8jn_T{GGoD@*ig|7T>s7vdDhw^@gpd51W=QnZIzc_8E{_gQv`xXfvBs^eLSD=Vtf6X+8b8FW1hQ!NcyzxOEe7`KU^lbz@2c4NXIAvRe9f91mNZZC zI9JDauCKSX(~xyh_CMFe_1`d3TN8KIYIba4m2}whdCM009GP+K`&ie9gxPCfwlrJd zo2uVDEyQLo@0ZCRwz18g(zP9|O1|Px`Sb^CRw4ykJK#{WQfiv7nQx^%e81G!oMC{0 zvTMiToKqf;^^J`57_~UOvpuAfe(ag;Gvlpr+@7b7+TR+xOzttNwzZ{^GZw^_aB-3F+4QQx__|^-7F=dCa*AvTy?N~{Uo#62 zgIGOR3yB-&Yoq{!QqUObTGu?xZdT;q71?}v^Ok+T_}s5~iWN%Ps#O*zFmB(p zVdt)mV+QmY8Pd@RB6}T1@NqUpG9}KkeG^6uxiE;vLp48)i>AWY0Zw z%As&R_kC#BitQ)t?48z+>oa8fV5?c2t7{JNUA}Uo)x3^lFT}pwoUFMnX=XT_^}V%m z3l_{2-&-@AZXDmR?X=I`4hKsXZ`ZlpoOv_DX0S3pbaCk7`9AR*Ht#S8aUO7@hxwrg z+~g>$n9U`X^_HR){_2=mSmyoQ!15$-*PZ#PFAA)?hqtA%hnX+hC7+HQ{H-m6rQMEe zM#Zz)1=XZ|`40JXOjere6)$7PWSniPo6(U=&6kl&EdZwWomICRst=qws=vKiuR&&o z0~LzhHO_Aw)VI}CQ#<3w)dv!0E3!#}bmH_<*D1dv=rl7zSkf9|Fz4;*VOA%y$>z@u zHri5SgSowLOACS+M!nG1$n;i@(8!N%g)&;Yk#H2JfTR7R&=sMZ-{NC-qz4S5vuARLs z1kQ&F+3Ri=qCLheOcv;L$8aa~%CA_)j&91tjMwHsarQO`Uvq{@w(H!T^H%R43; z%n!vLHGfRk)_S&$nP;qDGkdl5)V4jTbROM&y%WDUSVd>nxV4}C+g^kC;)Hqn_;t4R z;uw=R6A|9)$S79Z%%*-CFY$s$U|sr?Eq`@(z&x&*!kq4 z+0spPglGmcR)}_Jsz8>xl-P<`@F`0np;Fe|PCRb5WOK*3q`^s6*SF`*ESTn)5c9mV z4#QcUnOz(ls%`$NTjPceyJ2L21M#G$xgEE)34C3`d`Wr2+M#D=;cHRGTu?aG+q}&8 zA;@NNV?DjUVUSu+Z@#!FcG=#|rmF{b-ZYCzx;^S0Gv~ga<=}ZuwTCJ0<|;@eA7-5s z5;7OfUQTo`)c~EXz7x!R2y_yv)lX?n^64{v)KIC|fKmO1NEL^E7d%|5Be|1C zayqrNf8Fc8crHt2CF3_;8~RCA>$h$1C)H`tu!En}s70&hep1V}ty=g=9ox07=O+cW zZ~LvE)IX~!N{0=M-IDbW_m-MUNFtDVkcWWU=bSMirY{Y(cS`#hw;>U0g11D&DMk z=i>c~FDkyacwF%dC8QFD5~WMTl-T{1$5%PNn()>7ueOwwN_v+pQ8KV(LdmzKYL#kP zYJI8WrLL8FS-N!Tv85-KUR!!o>C2@bl&M&zb(vFTrLtAa_ANWM?D?`+%jGDSx7@&T zq2bzrb%ov)!YXX3u)V^O3JDbwDtq0V$5&+dE#>?Y@J z-kPN#sh~8@c|$5hjTw~bE0vIJQYn6y=US29m8D!#6=#A}&3RX<4pcRqDN=y*uvF9e zMyjVlpaX(@QhUi$>HvhDD5ogWrpW$2Xd@OCEhDb`XOF5)GQemJiDiwzU#fVEtUjcPV z^i65*%kW0rmE&0@?yJy_s-&+bS-@mLFqs2P<^q%Xz@il_=A#W3u$T`l<^zlQz@il_ z=A%vdq^@9m7_|&1$QtbjYBVr8k4hfU*9&;PB{yi~E%_4)L%AYAU&47EDyB>2owuZl z{H_cxsyL&is$jnwIIQkG?}Ej-6<9o_dhkenbY25++tB%1YUF$`H6c%Pp0$Rb+VZSIX`t<&*+RJJ5MT8thD#hB$wPvWKJ{ygN+&zfk9K!U;kG;Usll zpq^C9x#y&>c1G()JELT)^Oo#SDCUfoi#xZIz0G7n(uT-3%HLW-*!SY zA(jwFI7t3Ogu{d*&NsRk=M%XqW%z@289eL2Zhr8rgF3mOWG-rdK<#N#QED&2^()?$ zf;y$aS$S%yOuj1cTs7pfy7Lv#{0=l9q(+q3nzZeOTyj4U-VFuki+Hw_{85w}O^78N zq@2Tq6yD_*^rhD8QZZ^M#gi&v-PP7-NXHFoNTi0xNQaJE%+w-N@}sPhO_Y3{c3-FT z>rxCMmJmlsBz+2LOCSaBXt5jZdM*_QTjib4!0Jb8Nrzwb)Zl<$bXpDdk?;oaMMKdh z+TVmWHbs`25xxbB&3WH~`_`1%mTL#v*vXj+-@Je#ci|g7JmY{@UeV5HwDXzdfVNiW zGx*>c^uGb^Z^`AHZ=wGkxej$z1YZU;-yU#bK%?vdAHM+YODOOPK6nJguYp)a@^T?@ zxsWywAohU61y?(O&;f)FAXI?P0d#NCm=EFV$3XQAYIsWHz(6PxvVfK^qTD64W+`c- zsB1eRnh-;XCBzX9Q}!7`BH=vYB5AKtZVKTx<)#uoknW@NxGuo?R#%tbg~0WD_{vC2 zeBdi1e3c)*%1?_Fjh8Az7Pq7-$D6xFz| z&b0>oAX3+)UXj{BsyjS+4~qVu#=iD#AJn<`MKs^ENTt} zXq3)?(vP69&|P<+>>uFh3XmN}PrU|Ds>XAnk{=ZQ6$)oS-G}htStxlHDrP{%3@CU< z>mO5{f9Og%9q{B?crs27fG59pK9=h{ugV>qkK~TdOt}lsxlw00<(`6jJR(R4=< zq__lJUlu7TPtT^?2Ij~=@(9VMKgu9MV}NZBcrl}flrc@F0T zo!Pk_?Ymy1a|ZpE8T`uNRR*eb`XeJc3Tco`a(Q}qe>m0yZuNlP>GW%QbcRfiouwI| zeLdbaBE`2nYfbKsz||G1hSI)8l)i)zMc!yaEMW)f4iXL%&X6~eaGr3HGOh`D38~bc z$C(ZUIl;O^?nv7UqXph0J4Mj~?~$DX$d2d{uEASE9}5lX4z`61J2NDF{8igKa}vCeDs1dplxnp^-{+qf_0d`kaxTK>wJLjR=D zOFYq}#v6#jfhHU%4#MTnfg~L+e+8Gngv(z8l?s=?lS(2vrJ!|bo|QoxmZb&dcwQc^ zt3djSt=(NUAr72}JBYcbWwBRcAS_htY zrg!cNo(3Wvb`ULbkQN`I#YfO(X-GjDQjqpHUG{-AADu_Q z#zCYZLoV%1LmFN~4;cw~Eq6s?Owg_%^%cYZal`h}A(Q@afj9PNPAKUOw+I{71F7{! zdi{Z=FnB3~RuVmFF=r;Ya)PT5nieccTA>j{uUZEDlqG#RN-YmADv-V+=_>JDXs4>+ zq8gMHy=@Kh1t5oYfTlJ0>`4BuJPQN|qQ@Pl@uW901r2iu_^ATBbAS*YKetKQP zZQiF6?n&;*$|qzcQ^+-Myy1U;jmnuCWlPa6LBYz@QbmI#6-b@|#cwWZ@w@>wH=^xL zX+<+a3qot~)E4>cL@k}U9|shnwfUNIr3&8{RvJ z&ba{p9h9tS*MbCpX9T$Y2yUN(+ca?N0JrHt`xrjV1h0|cH66Sv@ZcwSFq0NM0k3J` z^)Yx=XbpA-y;4=4Rnz*^{NUCbjhh3Vl>_|dg%9^&3G9Ox<9yb)-54 zt$!Q6m`b<@)<00jM`xtYz$mDcbC0gHbDyq~GoHTsjIO5hE~BHv^x5}y-w?Vucf>;y<()_63iJp8&RDsYb30b+dU}g^dWu8z6iLWQEON3< z>n-v@wR9+!4yCZs;e{$-tQuIa&b0=u4}iOc&G;=?Y03LG&|Hjj{?b_=z}6Ema|6uW z05dnh$_=n`gK>n=)LyjTn-tl(TERy_fbp)z0jAsZv#Er8!196oAEBecc^#Tw)|GQ6>nb>}K-VkK^#XLg1zj&d*Hq}5s_RS} zJ0ZcMZxhmV39PAL?Gjk~2-ZG=wUbDb@(K8kA4rqX z<_@qaM%6-+(*KmCze~|QFnb@&J_NI=U{+WG&MYZ%*)r{r%8t|*2rmo-&cVPm1l!1^ z*)M_J7hv}-*u4*S?`d{T6lt~-q6smCSVA1(Ag~=G93~tg93`9~kC2*+!0;!=-KStTU9))8z%C=b`=M-lXFdF-T$(6tJ!sZIz$Ki8qBaP@7E;U{6iJ%N{QXuLEKeWYnf^!fs9t3{N2 zm@+Pc8R5-HMJ7uVgq2o@Uc0{YN|u$DA*?hc$b@WpP=l8<7D&!R<5*~X85(1sq7jSm z3~7d*+koyI&?Q2<`#_i%y5#`Myl6mAw4XPS<^j?}&}~2CphM8^D74!P-7=(>@J=hn z!>xJOhO*o8z8$~YbM45r6Zq{6KD*KSKsa_FdVR2SE%e+#3)7I6J<#(4t^AEv9)_OB zkd#Ph83`>Tp=BhrjD(hv(DEF#JO?JuLCbT{@*K201T9m+%u#506p`=UMc4h=ynXc?T2p3(Ce~}ve9ypjK$8O<<6ny@}lAP%Qew*U63p>D)!*t6AQx( z7H=UpW~?wbsN$x*6@5*4`r@kCIMucO!r-!T8ST^)y&%#d>=j|(c0xA``RmGkAokus z(vRaB3U(LJmoMUeDd`VVULtw1VX+0x(D);?5SC~u5VPmMg7q)sf`ieJ{`i{Qh z9sKwKetZQ#z6Hx4sKEqJz68^+;K8@>-+L@W544>d_J*0hMA0mxZj><$dt*2ul+o%U z@-8JrQGYZcmXOHr6w2{Nirztc;dyeSbkWymQ1T~6`I*r31@v@CT}aUti=`X21cAk6 zyf;f`QhL#z>;x+*3KE2+&cj?@I%rCZ1o9^ zdESIlni52doAay%_Zp16>jafL)7CC%;;!5a3<&rbW5GoxTIKsPROjr$aW%NowBD<#S3i%fL96E2nMHB7wqpd>+E;YaT=}_T=)&shHw4%5B1ZRF? zyz&W|k%81@yBmP$6_iOQxA5*f(WrlqtEa|O%-itqh9n8>32bF(bm>HW-GC<${`kx< z*|Ko&PRhKL<~zW%w35+{kKk9C9>z%zCQrH z2-&7RL1;MMbX;wSlLIM=L0h9?)P=SI96wGu?^LO}hO zC7iDL&4o4Gk~FP&*P3fvYH7!Pd#KrgJe}aU&S;P>$Zc1k5HdIr3J1|@(fbJ>-V(6B zl3mTwOX0Z3*@N8cn8w$B$aA zZJ}p-YVQD?9l=p2VD3!6FFYNt-o{%SI}5xCoN4$1>c4c@Cvf8i_X}H4$g7uthT!5q z(Hp7ooeI}v{3!*6fnDg{b1vAqTFF_EP!FsLX>J4-nxFyxJ|g3vvf*+JT)>{%nuel;YK5n=67);7D{fF67}2ob;X=UQp^|bd5_w1t(#DVy%0_0ol1)DX}0y^oGLU@1n?S z+Wc74DJ^*3inOi4TYIg4^@IwqY1cE_^_q6MeE8?M3O{}lLFmk6!ezoW@}&@N;3p`< zsHPTW z$1I-m0>8pf?V9Cj&wC-2Vl3Sm7&IB;Ui8_+;LqU%mk$!}7FbzI`Y2$CCd3ki-%;>R zZaEM0vo_}o{H>euw{Az%KS9fXkel?RcvJ$J7aE8UHi$GMoT+j!zS&9i2s=r; zpLwGL_-vDr7cq-*jyA2p4&Q}$!{-c-EFY?fBiEcO8(Njy&rGy*r~_yTHd? zfh3XqDWp&3T~2}w{$GIqC)lx`(BKbf@B~<6fi)IbPXX&X#tWX{`6YZSbZr8<_IGHV zq{%7V1D%UdehEq{MXBXUS&`n^HBQ)-)#^kjDQLfu(sEK7V*&JUC+1!{GZOF0*sz=6 z8rpl(xlv~ZQ(}H-FMQwzA9#^dj7m>{v%TPKFF4z)VfHl8F3E!S7gGI5s$WTUnpCGr zbsFxgLK&_xkQ<}3Y(Su=L@)ZC?1b{bT7~gwH7tPY)DYlY30%p*bq`op(qCTyo=Dwj z(&&KcHd0VU<4yymnJLZ4lXkrOJoe}cCb0Uc#WhnVY$;(4bSBu)MMtUouetNjbLOAt z%l~n%Jb*dte>F$`d4Bw#&5eIC9}fJ{U|rNIX6yeaHGZCD7c=Hx%#_2Y|9kku{|o%C zF`;X2|G&ciJHCj&=JL(|E4Y3C1$@HW<64jK&++U0VhzH3w2Pu~{6EJpeFJvce-5MI zqI*cDm__=>8Kf^~i~`_ZFJ!c|mFOv2m{nfvkJrXlSU(ULS85S|KauI*zHMGB&MG-S758xoz zEJ_rk$^+QdhZ%F+Wz2DxF~?uCC|^6%v{{sT&KJ7+1pGa^VC)c=-^KzQN5Ih$EVX&V0w>D^`0@1H@=DY zl8&03SWjNSotoL@)DMnnIlS8dBzg~^y00Zvq`m{ zR9moA_WM!mOV}Z*pr^BjX*( z2#l^(8v)QPYvqRA1zytO!_h(CthRu~f4|1Uof6%lMur@KpQj`A7V~tj6&GS0F2*@x zWdti9DDNb79|Z0UaFht_8O&kmf&Cuy7;paR$^|1We?sn0-Haqgyt|NRV&7dcKL1?R!ZF5r3(OsBxb6~vd5IRjg)aA@-COFs zuSr}AsqcV;6mXF8-_E58IXMqhA1V7hsXt=L%bHHf11II7fAhgs_0d*1gs?uOtmGq? zlic8u>~aODQAx7MH6#z{sgqe%2qxaibtIcy7y343ey0o1gf~aV+xG!@Un1X(-GS+q zTu#UYvQd>e^%`(s0KaQ70(7m)dWoEg`MyWA>MCUjj;oGF$^$F~;5|R!C;%MAfuj&` z_{&v+qXxeNqR~(lqoG2 zP&k0#6{+%4yMx+a!4*RH3a>zGJv)6PH8WI?FOZ>-bY*U_Jq=I z0G~TO9{mJ3a0i|||hNA9tbS zGw5gnVlm%*9;j2nk)Yu<@}Jkd)mO=Ti&o#H)t6}XbILd`xRRclvcFj4To;}Z+u&UMr~i9J0}xkH96kIUBgQz|CeLvBJqY;AA5?(||}th6*x^)Eb^H zEfpiRos`8%SpvQnDSc^GQFz@BpVNy#{ZDX=pw*Xj5;PK?_kZ3N-eh5jHqpvwe_~2_ z!aR{(J!N}JpXaPX=v({2#{-e3A>d^=bPi&DS)1Ctd$&_;o+szx#&;z+Iv<-4Tlt;j8!PGVeNu3RSmc!dI zXvF>WjR(N|QF;q}e&oD@W(p)_U+{|Gk(`go`9dCpoXv)s>;@obCOJQn^F2A=lJh0? zirnw<>UiN-@@73zcCJ=NV+GOVz3GVt(z+q6Vjqqy1%bs8Ks(a;3#Dh$%3Ja*p3kPF za6E|a%A7s%1hmDCtd>lev!SU)>hKeW?1Q{Kh%-OdeQV~M=i`Ln!_74z}V zr9ieH$PR*)#quF+ki*V3K)G8!!K(TM(w=s%2I3!pI2wqbK_k+I!;PfEaIE|bngRxskXC82OU+zr{`jV<2I0=Gg z0{f$&OEAAD!S}N$Z8pCJ6?W3%{qQJzFo5VrXTbHGlI~E_ZQ!~ITl9XnGAQqytx)1{dqjf$eD)Y`5UG zC&2a!*xmu#C16Vhwim$i3eTMv7N|EmBsf+yOrx zppCoX$35`lA!xD(nj}J#Bxte)3ZH}@_ri~-p!6e*Y}2+{on*ayCHDGaH!1OB+wTI`YUoUeeNXaHU(U1bcA~&&?AvZ)^=*e z7pX~oEG>;gbMFARJE?gW8elh++v7YA2j!W&TX2@pPma#x{RB3$o;yDk9PeV}>-${BB+-Y^kDfZa9szUTflP#c&-A^Y3jdA-DhcG5PCxFWC|w5c;{T`Hk&$x zMm(jxUB%l6yuHWU1-zZl+XuWohrJU>o*?Kw0$vTK7aET=PJ%CIX?pUimhv29&K#ut zoz^*ModUcsX<<4!pOEu0t-L|`pUC}~+(HW8Q_n?mi&j3Pm3X(weUV&mY3Vas`U1-N zKsnzm%H<&EJ6isR+%M$WXr<-!MG@$zZSd`OXE@l{4)xB1k&RGqC)7JZ>C2(scBr=p zENz8)aZv9d*opvKqIbBM#dl|*-VR!Q1`7TH1tY*bCR-vWu zXz6-dxQ-SolzEe$^#98mUg*%@zi8hA?qZ^_^- z8N8j%;Lqgsh^qeu=?c~jVQXJsTA^NohT5^uElEJzt zLs$suP)~t+4qEY+vR(tvd&;^8)f|*1Yh~$p>!iH5U$pZHJo|z2-c!~eSZ06s$ZD&q zfWnPj!a`Dk;xSPC0Te25W+T@#M*qvv6A`pAh8DyE>rUS8C)WY+cMy0Fkt>0AoFgPN zh7rEKv*7b4_&kelclnxxB_BcV?QmbTpooUGJG3W__CP;u+5^~s2T7Lzj!#qDIidRm z9C!f43Jt71F}~s&0)>R;6jb`F-xbt)PH9*ga42gCf$JSu5;(X=N%->V-CmLFMOOdt zp4N!Hp|5GpZCY{-j6EaQ8*-#Vg>-Om8yZJw6nF-XpHs&p>Ucu=zf(3mgsp4T=o3#( z`{^kU(5D>58cZhjEj+a1t!U*(-p2FxByXd5`+~PZv+v~Xe%gBg$d7`Xb6_|b{M^Ft z3IA$17Zl1M#S@@>N-77bK9b@&DYj$B>;w~gXsxK_pvFm=K&O)Gh9)?1Kg^i}t#tiWD5h&gRg+s&tX`bC; z+~b)=Eun=zl1f-ff?97#3CI48T4@@kbkZX+l4QjBc}9vAcFcad_T+xvk6V&C(9+D{ zmckP@Vm_)`;E$8#Q^YO?ez`<$*bBYZ=;#hmax z*6n=TV-F_Cfj8{ zxsdEH7nY02Mdf00ak+&2m0VITEtiqY%H`zpas{~xrvOx!Ysdj|O}Um_Tdu+DQH$`H7KO{I!$~dS5Z7}(y4=d@iVARB19+)x)^h_I zs~{_hE5d^f;jnI5&yDCZu>-ymoY@Gz>z?)8M1Nvqm2qYGwK3e-BkOrKJREk`BvgTm zo4})iS!pKy}tWyXs}#Qi<@b2TP^OY#l5w7uojQf;t5(jU5i7t zcyZt1LBr%#TD)G1w`lQpE#9fc@mhRDixadsQHztc_-g+_!@rYnYVkcSexk)MwfMaj zr;FH4(qe-adunkmEzaM6)OUT{Y+77di<@b2FD)Lc#bH_;uEp_Md_jvJXmR?$L8F7+ zb7*m1EiS0V{#sl@i_2xO_qJNxU5oo_@lY)utHo2bI8=+5 z4j(gYl>5(G9InL?S{$RryS4a$79Z2%Q(Ampi!W>OwV+Xbhr8d_;s;v%REuA0@kcGr z5V2m?Vv`noYjGYe_8C29#Av;r78ldv(pp?mi>qsK9W8F4#m%(1wH9~OVmJ|v>pGR> z|Lv>m#FqbCoJ)i4KaaEFl``Pp6Mc$U^C@OpCGP+K`&5Sx67>AfW1$VRqu~T+{QdgZ zoB#gqb3PrKRlxFpiF0cu{9j@hU;n4Eh2BW$=LYmH?a|zQu?&KxNzzPgf~C^WoF1}W z+Kmm6z)D@6fYq(JtG#YRUB9KZS=XIe*Ep^B(qTb}o=+#;%(}jGU3CSsuFbNpAz9Zg zS=ZlOS6R-wme0C2$h!7&)gx!sFAqinB<#t7^yy*p`mDFNvA0u^h!@gw+7KzdlwL`% zr8m-B>7DctOYpW>ZzVHJt;I6Z;|D(&;0*)q%K?9hYagy(K7En(l+{`CHfje8M1 zTdt>Bb&2a=b-A9tm0V9nT`T{V>seMVas4Zo>*-s`^;F~<{I^`svvP^+U%6aQ-%75h zB3G?HY2qzCCT(NvtYMVYjCX$8J2z;S11n6dg=vf}BrNjA(r{wd?C8WX-(sfJU0%TZ zcIXG4^j^9mVrMCyh+jILr^eS3R*ZOZk0(6f&hxzZNk6jYceE#!?@<1gls^S~c@u^;n8aUbk89sfB9 zf+yie{teo%T*!iWng@9h*SyGtxaLPLM5@MGTuzHc%{m~^!Cn8X7Li_@)S|-<6mfP! zPEDWwv()vp5lMTkJaHC{sI?wB-Q=b6A$lu!IaJ0fmA-Xdb%9btT}^&NjL&~~WWM;- z;oD9vB|pheb(gPydLx~2FRQE6OOn=!)UvmHfs3C;9q~ZY<&_IcQZLdoDG^i%$Q~#*00$&wJ9YXhwuPq@B_M z#)Bsq1D<0~N1~L(IPd}^!OQUJB}Ri+rQeX3-x(F&(8h%~rQ6b7iJh&|2x)`#oz$E2 z>Vl=wj59Y&+oYeQ$jM~OD_}0*mMDXm5`4?jAy@rP1fncrTa)qA6++T2tC>`Bz&Ya zN*nP`phw%qD0jECN7^gxlj5cQ+9+3Ilq<%#3DS?!_tF$;sx(cSPCqzH`azm4&5_)* z<%iV0Ai5Dq#Au!I2`Sf08+B83i*-&$v!~^=@-4R?+-CC`fH1d3ZX4Vp-D2J1+;{O$$Zzj80E6eEjQbKk7qHPi z%6*snLHA?s=iM*5Uw6On{)hW3+br8VKiSXXXY=#-E9O_;uaaMYUoF2nehvIa_>K3Q z;EIu}#A1{m- z$LGfHi2s;(-~A%~bT8X0tyj-p{d_iF{j>ZuzrEkl@9OvVv;4vSaDSd(>aX%w^B%mr z{Pq5i{D=Hs`M>j@;*EIE_&fX;{g?gM{0jeV|GkXi86z|DGG=AW&iHP|-0UtNfB13z z=N=={pz->h)miAwRy615Nca?M1}`DuJ#IA;UW0`1b{pK!__2lU?zc$z1@|fv-pvm$ zH22Q((vWaxub-EVgmb;|-UP3}yTY646?$dfdT*1r)qBi)-1|Keehmrl@%DRl-r?@6 zx*tKp?P47;4l3cC*aZy|E=R&Y`;vs)d`-dwdJgG%by`Np5-#xPHRxzn7lK&m~h$G?ceFY7<2l`GwJ*9t-U>rB`!@& ztlg8ysePb!Q=&)Wro{3D@3KrhmH5s6y#41SuB|;_OzqOd_Qd+yw`zCS+?Y66t9ISg z+)a9M&B>ZiYA>wKCH{z}CcMP48tSPjs5u_QYZD1l@wkb~nw2#RxpG-zTX3bcrc>go znwI+>+qZGwP1T31tEx{_zg_*0>JO^ls{T{;^VNT-e!`f&k5=DVy}J6Q>J`;XswY%o z^Q-Fi7W4n$Ue+=8;;pK7L#e-NYSkrG1FQO1^)aThS-YZkGpHry8?-_`G7Wdq)TOy} zCn?&fpq3aY5K@y=_1#Q;J=PaU+F4Sq?;7TO=9`cn(i`{HQ8ZO zDsy?VyGJ}ZUwX(iX(AWM&9Y2ZNK7u3MKVvCO0N1-X~w#3Pq|F^j-|9<1vf4iNlUrX zJa6{0id)Y5?g&{fEAf2`rMFxzi)Ff;B_riqGD>`zA?;+GoFfy=OVXOP-?L@3dD$G0 zOj#l&vQ$>djWS=#RRqf`<~r{cX$UM{8xNL6)Us{?Oyw)eaP0?ZT1j9)$=|p zzB}y??LU}h?y^6(AJ_+Mt^Ki0*k9NW?Pj~*K5Rd->ue?OoBgN#iT#)TseRXOu?P5} zpWXJC_At+vf4BG9efCaUVehv!a*M2%Tje%sFXMIoA%#*I%s&=OQ7{)-!mMYJDVJ;I z8u?#)RCe0qHqD;k2ZT=AQ}Pq}sce=9Ws7W;zsYuaOMWA}9M4(unCzCv<=678RLG0w zVdt24JR#4^4tZN%#f~(SXXI&lQvTvv$ZzF$u9dtlugNQ}wY({B*iYQq@-nuj4L{d? zfO(0Lf6IQUm3Jj6r{##$$w7HfK9Nu5GpUh;{EOe){z8t)NjW7)t+gU2|ErrgOb7ypy2)i5C!# zalJL+NrXGP?d;xd=3X{a$86J``tu~c{wTC9^k;*DF1+LE599Iz{W+h$m=uL`0$Hp#+2qM3P3v!FYuVjX z9Z}iln<=C<4bnm#Q9b!8vM6&(W6-7%$nzs`9DGKeKvz^>8iPEsKvz^>Qczuq^hIU+ z4TzJjdfk9f4vp$OqaUgxXQ1OH)EU*A5aTQI+jwHOxQG*5pw9KP+8Fq)vE${#wb)qP>qLUs`0o|IWd+3XkFoW z%hItHjW-?7qIGHcS{`*+pfYP1!_=Hc#cQ@iXjsp1yW0Uboya{p)#x3p9v8eTG--P`g zmhC9NIqmv@^uQi`sg6{iLaAdYJlfxx(5Buvb2U`QY&vC(41g^S#@OPZFA6AkEp1Sn zflUhTU3ueD6WB!6qgL=#LAC7#fefSmA)iIAD#OmC>o^r#iH!+u#t6MP${osSiL;e2 z9KZEdslL;D4(?m^Kh(#-Ke*H89#4sT_VBPN0nZ}8^V~-4_DrtL zWSq~e|1)*3QA)hO=_t155p$lcFym=k zzJ1q>wMEpu%d89F<4eW^pPk~i+}BL$NI6dukAbcM)ZbK?8x^$S5O~GDVmd*0x;jyh z`m{S>=7A675Z{wJtbJmBz-b3ad&3x&CeHpCyH6kiu1( z9LnwC4%85Yp(>T9|HBSnua=)n@y&mvtz9?@hPjD@&M~*gb2P^xz z=-(pltBYe5mb}GWKg62i5c;hS{24t^{~h}AEAd$s%q2oU-ktPdtfR*x2v4+|>pxHV zaJ!l^Lwl14|0K!`Z3;T*hTv!M-!9V(b8-A+3FEvl@R^*cG`*z?fA%^yWj1Nu2&bbP z=aNT#Zjl)UZ6nVH-JdQFK!#5fkWssf^-&o@P zDF0h_0XF|~e2dO;-obY7Fg@Kzw2QYiz*~gw&E-ijjZ-@9JibFpT8hgnYF>v<+{V(caZ&Jo?Z{)9jX@uSe;9NPB#3 zA#)enMcz>XeHc5uhy2_FdDB5A(0SVw<#EM~L-or8>H9L~WQFzWgR=Bq=)W%PKlMYV zGvp!WGb;k0(9Q0wKOvuDlS|Av>_acoeS5(0OoRVJ-p~n9)q~#heqc-6az6u!wmBeOHxba(0JUa=Yswt$2zxcU{h}*XhW(hw8=k#}64xg>{ol&y8<6#*d$FUU zurnPw{S&5p>@!^xg`2PsIzI~Yc6;`=*fH_=l^HY7tck+Hp3~7N%t<(D8HJt6#y5ui z9?wDg_Bzem1Y0#L3O6NvY2WG7W@Ps7pEY`6>9VppCD)b5a + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/.claude/skills/launch-graphics/graphics/assets/sqlite.svg b/.claude/skills/launch-graphics/graphics/assets/sqlite.svg new file mode 100644 index 00000000..e5e0d013 --- /dev/null +++ b/.claude/skills/launch-graphics/graphics/assets/sqlite.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/.claude/skills/launch-graphics/graphics/assets/v8.svg b/.claude/skills/launch-graphics/graphics/assets/v8.svg new file mode 100644 index 00000000..5241ce63 --- /dev/null +++ b/.claude/skills/launch-graphics/graphics/assets/v8.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/.claude/skills/launch-graphics/graphics/render-dynamic-apps-launch.mjs b/.claude/skills/launch-graphics/graphics/render-dynamic-apps-launch.mjs new file mode 100644 index 00000000..e2636c1b --- /dev/null +++ b/.claude/skills/launch-graphics/graphics/render-dynamic-apps-launch.mjs @@ -0,0 +1,324 @@ +// Dynamic Apps — launch/social hero (2048×1024, site paper background). +// +// ‒ "Dynamic Apps" lockup: ink product tile (white mark) + wordmark in Manrope +// ‒ the landing-page hero explainer, wordless: generated-file stack → deploy +// code chip → GET 200 + mini browser, joined by arrows +// +// Fonts come from public/, the product mark from src/images/products/, +// playwright from the repo's node_modules; drives system Chromium. +// +// node .claude/skills/launch-graphics/graphics/render-dynamic-apps-launch.mjs [outDir] +import { createRequire } from "node:module"; +import { mkdir, readFile, writeFile, readdir } from "node:fs/promises"; +import os from "node:os"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; + +const require = createRequire(import.meta.url); +const SCRIPT_DIR = path.dirname(fileURLToPath(import.meta.url)); +const REPO = path.resolve(SCRIPT_DIR, "../../../.."); +const CHROMIUM = "/usr/bin/chromium"; +const OUT_DIR = process.argv[2] || path.join(os.homedir(), "tmp/dynamic-apps-graphics"); +// VARIANT=chips → generateText() → deployApp() → GET /apps/pied-piper 200, no pictures. +// Variants: pictures (default) | pictures-split | chips | chips-icons | chips-ink | chips-bar | chips-split | plain +// plain → just "Introducing / Dynamic Apps" centered, no explainer (blog hero, 2026-08-31). +// Default (locked in 2026-08-31): pictures-split → launch.png. Other variants write launch-.png. +const VARIANT = process.env.VARIANT || "pictures-split"; +const OUT_NAME = VARIANT === "pictures-split" ? "launch" : `launch-${VARIANT}`; +const IS_CHIPS = VARIANT.startsWith("chips"); +const IS_SPLIT = VARIANT.endsWith("-split"); +const IS_PLAIN = VARIANT === "plain"; + +async function loadChromium() { + const pnpm = path.join(REPO, "node_modules/.pnpm"); + const dir = (await readdir(pnpm)).find((d) => /^playwright@/.test(d)); + if (!dir) throw new Error("playwright not found under node_modules/.pnpm — run `pnpm install`"); + return require(path.join(pnpm, dir, "node_modules/playwright")).chromium; +} + +const WIDTH = 2048; +const HEIGHT = 1024; +const dataUrl = (mime, bytes) => `data:${mime};base64,${bytes.toString("base64")}`; + +const [sansFont, monoFont, markSvg, nodeSvg] = await Promise.all([ + readFile(path.join(REPO, "public/fonts/manrope/Manrope-Variable-latin.woff2")), + readFile(path.join(REPO, "public/fonts/jetbrains-mono/JetBrainsMono-Variable-latin.woff2")), + readFile(path.join(REPO, "src/images/products/dynamic-apps-logo.svg"), "utf8"), + readFile(path.join(REPO, "public/images/registry/nodejs.svg"), "utf8"), +]); + +// Site palette +const INK = "#1B1916"; +const INK_SOFT = "#56524A"; +const INK_FAINT = "#8A8478"; +const PAPER = "#EFEFEF"; +const PINE = "#2E4034"; +const PRODUCT = "#2F6B4B"; // Dynamic Apps accent (src/sitemap/product-metadata.ts) +// Code palette (src/lib/highlight-code.ts) +const KW = "#8250df"; +const STR = "#b45309"; +const FN = "#0550ae"; + +// Everything in the explainer is drawn at K× the landing-page pixel size. +const K = VARIANT === "pictures-split" ? 2.15 : 2.3; +const px = (n) => `${Math.round(n * K)}px`; + +const skel = (w, strong) => ``; + +const fileCard = ({ cls, icon, name, lines, caret }) => ` +
+
${icon}${name ? `${name}` : ""}
+
${lines.map((w, i) => `
${skel(w, i === 0)}${caret && i === lines.length - 1 ? '' : ""}
`).join("")}
+
`; + +const reactMark = ``; +const icons = { + react: `${reactMark}`, + ts: `TS`, + node: `${nodeSvg.replace(/`, +}; + +const ICO = { + sparkle: ``, + upload: ``, + globe: ``, +}; +const downArrow = ``; +const arrow = ``; + + +// Backdrop: the product page's Game of Life motif at generation 0 (ProductMotif +// DynamicAppsMotif). Cells alive at gen 0: "on", "a", and q-cells at phase 0. +// Tiled every 13 columns like the hero, faded out toward the bottom and cut +// away from the center so the lockup and explainer sit on clean paper. +const LIFE = { pitch: 32, cell: 24, inset: 4 }; +const boardCells = [ + [3,2,"soft"],[0,3,"soft"],[1,2,"soft"],[2,2,"soft"],[1,3,"soft"],[2,3,"soft"], // toad (gen 0) + [6,0,"soft"],[7,0,"soft"],[6,1,"soft"],[9,2,"soft"],[8,3,"soft"],[9,3,"soft"],[7,1,"bright"],[8,2,"bright"], // beacon + [0,7,"still"],[1,7,"still"],[0,8,"still"],[1,8,"still"], // block + [6,7,"bright"],[7,8,"bright"],[5,9,"bright"],[6,9,"bright"],[7,9,"bright"], // glider at origin (5,7), gen 0 +]; +const TONE = { bright: "rgba(27,25,22,0.075)", soft: "rgba(27,25,22,0.05)" }; +const cellRect = (c, r, tone, shift) => { + const x = (c + shift) * LIFE.pitch + LIFE.inset, y = r * LIFE.pitch + LIFE.inset; + return tone === "still" + ? `` + : ``; +}; +const backdropSvg = ` + + + + + + + + ${[-4, 9, 22, 35].map((shift) => boardCells.map(([c, r, t]) => cellRect(c, r, t, shift)).join("")).join("")} + +`; + +const chip = (inner, icon) => `
${icon ? ICO[icon] : ""}${inner}
`; +const GEN = `generateText()`; +const DEP = `deployApp()`; +const GET = `GET /apps/pied-piper200`; +const STACK = `
+ ${fileCard({ cls: "back", icon: icons.node, lines: ["80%", "60%"] })} + ${fileCard({ cls: "mid", icon: icons.ts, lines: ["75%", "50%"] })} + ${fileCard({ cls: "front", icon: icons.react, name: "App.tsx", lines: ["83%", "66%", "75%"], caret: true })} +
`; +const CODE = `
await deployApp({ + appId: "pied-piper", + files, +});
`; +const ROUTE = `
+
GET /apps/pied-piper200
+
+
acme.dev/apps/pied-piper
+
+
+
+
${skel("50%", true)}${skel("100%")}${skel("75%")}
+
${skel("50%", true)}${skel("100%")}${skel("75%")}
+
+ ${skel("83%")} +
+
+
`; +function chipsMarkup() { + switch (VARIANT) { + case "chips-icons": + return `
${chip(GEN, "sparkle")}${arrow}${chip(DEP, "upload")}${arrow}${chip(GET, "globe")}
`; + case "chips-ink": + return `
${chip(GEN)}${arrow}${chip(DEP)}${arrow}${chip(GET)}
`; + case "chips-bar": + return `
${GEN}
${DEP}
${GET}
`; + case "chips-split": + return ""; // handled in the head block + default: + return `
${chip(GEN)}${arrow}${chip(DEP)}${arrow}${chip(GET)}
`; + } +} +const splitColumn = VARIANT === "pictures-split" + ? `
${STACK}${downArrow}${CODE}${downArrow}
${GET}
` + : `
${chip(GEN, "sparkle")}${downArrow}${chip(DEP, "upload")}${downArrow}${chip(GET, "globe")}
`; +const headMarkup = IS_SPLIT + ? `
+
+
Introducing
+
${markSvg.replace(/<\?xml[^>]*\?>/i, "")}
Dynamic Apps
+
+ ${splitColumn} +
` + : `
+
Introducing
+
+
${markSvg.replace(/<\?xml[^>]*\?>/i, "")}
+
Dynamic Apps
+
+
`; + +const html = ` +
+ ${backdropSvg} + ${headMarkup} + ${IS_SPLIT || IS_PLAIN ? "" : IS_CHIPS ? chipsMarkup() : ` +
+
+ ${fileCard({ cls: "back", icon: icons.node, lines: ["80%", "60%"] })} + ${fileCard({ cls: "mid", icon: icons.ts, lines: ["75%", "50%"] })} + ${fileCard({ cls: "front", icon: icons.react, name: "App.tsx", lines: ["83%", "66%", "75%"], caret: true })} +
+ ${arrow} +
await deployApp({ + appId: "pied-piper", + files, +});
+ ${arrow} +
+
GET /apps/pied-piper200
+
+
acme.dev/apps/pied-piper
+
+
+
+
${skel("50%", true)}${skel("100%")}${skel("75%")}
+
${skel("50%", true)}${skel("100%")}${skel("75%")}
+
+ ${skel("83%")} +
+
+
+
`} +
+`; + +await mkdir(OUT_DIR, { recursive: true }); +await writeFile(path.join(OUT_DIR, `${OUT_NAME}.html`), html); +const chromium = await loadChromium(); +const browser = await chromium.launch({ headless: true, executablePath: CHROMIUM }); +try { + const page = await browser.newPage({ viewport: { width: WIDTH, height: HEIGHT }, deviceScaleFactor: 1 }); + await page.setContent(html, { waitUntil: "load" }); + await page.evaluate(() => document.fonts.ready); + await page.screenshot({ path: path.join(OUT_DIR, `${OUT_NAME}.png`), fullPage: false }); + await page.close(); +} finally { + await browser.close(); +} +console.log(`wrote ${path.join(OUT_DIR, `${OUT_NAME}.png`)}`); diff --git a/.claude/skills/launch-graphics/graphics/render-dynamic-apps-technical.mjs b/.claude/skills/launch-graphics/graphics/render-dynamic-apps-technical.mjs new file mode 100644 index 00000000..5a7c8600 --- /dev/null +++ b/.claude/skills/launch-graphics/graphics/render-dynamic-apps-technical.mjs @@ -0,0 +1,124 @@ +// Dynamic Apps β€” technical code-snippet image (white, 2048px wide, auto height). +// A "Dynamic Apps" lockup header (product-green tile + wordmark) + N syntax-highlighted steps, styled to match +// the Rivet launch technical-image treatment (JetBrains Mono, Ayu Dark @ #0a0a0a, +// 132px gutters). Steps come from ./snippets.json. +// +// node .claude/skills/launch-graphics/graphics/render-dynamic-apps-technical.mjs +// +// Requires `shiki` (a dev-only highlighter, NOT a repo dependency). Resolved +// from the repo's node_modules, else set SHIKI_DIR=/path/to/shiki, else +// `pnpm add -D shiki`. +import { createRequire } from "node:module"; +import { mkdir, readFile, writeFile, readdir } from "node:fs/promises"; +import os from "node:os"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; + +const require = createRequire(import.meta.url); +const SCRIPT_DIR = path.dirname(fileURLToPath(import.meta.url)); +const REPO = path.resolve(SCRIPT_DIR, "../../../.."); +const CHROMIUM = "/usr/bin/chromium"; +const OUT_DIR = process.argv[2] || path.join(os.homedir(), "tmp/dynamic-apps-graphics"); +const PRODUCT = "#2F6B4B"; // Dynamic Apps accent (src/sitemap/product-metadata.ts) +const WIDTH = 2048; + +async function findPkg(name) { + const stores = [path.join(REPO, "node_modules/.pnpm"), process.env.SHIKI_DIR && path.dirname(path.dirname(process.env.SHIKI_DIR))].filter(Boolean); + for (const pnpm of stores) { + try { + const dir = (await readdir(pnpm)).find((d) => new RegExp(`^${name}@`).test(d)); + if (dir) return path.join(pnpm, dir, "node_modules", name); + } catch {} + } + if (name === "shiki" && process.env.SHIKI_DIR) return process.env.SHIKI_DIR; + return null; +} + +const pwDir = await findPkg("playwright"); +if (!pwDir) throw new Error("playwright not found under node_modules/.pnpm β€” run `pnpm install`"); +const { chromium } = require(pwDir); + +const shikiDir = await findPkg("shiki"); +if (!shikiDir) throw new Error("shiki not found β€” `pnpm add -D shiki` or set SHIKI_DIR=/path/to/node_modules/shiki"); +const { getSingletonHighlighter } = await import(path.join(shikiDir, "dist/index.mjs")); +const { default: ayuDark } = await import(path.join(shikiDir, "dist/themes/ayu-dark.mjs")); + +// Ayu Dark with the panel background remapped to #0a0a0a (matches the website theme). +const theme = structuredClone(ayuDark); +theme.name = "ayu-dark-dynamic-apps"; +for (const k in theme.colors ?? {}) if (theme.colors[k] === "#0b0e14") theme.colors[k] = "#0a0a0a"; +for (const tc of theme.tokenColors ?? []) for (const k in tc.settings ?? {}) if (tc.settings[k] === "#0b0e14") tc.settings[k] = "#0a0a0a"; + +const dataUrl = (mime, bytes) => `data:${mime};base64,${bytes.toString("base64")}`; +const escapeHtml = (v) => v.replace(/&/g, "&").replace(//g, ">").replace(/"/g, """); + +const [monoFont, sansFont, markSvg, sectionsRaw] = await Promise.all([ + readFile(path.join(REPO, "public/fonts/jetbrains-mono/JetBrainsMono-Variable-latin.woff2")), + readFile(path.join(REPO, "public/fonts/manrope/Manrope-Variable-latin.woff2")), + readFile(path.join(REPO, "src/images/products/dynamic-apps-logo.svg"), "utf8"), + readFile(path.join(SCRIPT_DIR, "snippets-dynamic-apps.json"), "utf8"), +]); +const sections = JSON.parse(sectionsRaw); + +const mark = markSvg.replace(/<\?xml[^>]*\?>/i, ""); + +const highlighter = await getSingletonHighlighter({ + langs: [...new Set(sections.map((s) => s.language))], + themes: [theme], +}); +const renderedSections = sections + .map(({ title, language, code }) => { + const highlighted = highlighter.codeToHtml(code, { lang: language, theme: theme.name }); + return `

${escapeHtml(title)}

+
${highlighted}
`; + }) + .join("\n"); + +const html = ` + +
+
+
${mark}
+
Dynamic Apps
+
+
${renderedSections}
+
+`; + +await mkdir(OUT_DIR, { recursive: true }); +await writeFile(path.join(OUT_DIR, "technical.html"), html); + +const browser = await chromium.launch({ headless: true, executablePath: CHROMIUM }); +try { + const page = await browser.newPage({ viewport: { width: WIDTH, height: 1238 }, deviceScaleFactor: 1 }); + await page.setContent(html, { waitUntil: "load" }); + await page.evaluate(() => document.fonts.ready); + const height = await page.$eval(".card", (c) => Math.ceil(c.getBoundingClientRect().height)); + await page.setViewportSize({ width: WIDTH, height }); + const overflow = await page.$$eval(".code-block", (blocks) => + blocks.map((b, i) => ({ i, o: b.scrollWidth - b.clientWidth })).find(({ o }) => o > 1), + ); + if (overflow) throw new Error(`Code section ${overflow.i + 1} overflows by ${Math.ceil(overflow.o)}px β€” shorten it`); + await page.screenshot({ path: path.join(OUT_DIR, "technical.png"), fullPage: false }); + await page.close(); +} finally { + await browser.close(); +} +console.log(`wrote ${path.join(OUT_DIR, "technical.png")}`); diff --git a/.claude/skills/launch-graphics/graphics/render-launch.mjs b/.claude/skills/launch-graphics/graphics/render-launch.mjs new file mode 100644 index 00000000..91cb5c03 --- /dev/null +++ b/.claude/skills/launch-graphics/graphics/render-launch.mjs @@ -0,0 +1,150 @@ +// agentOS Apps β€” launch/social hero (2048Γ—1024, site paper background). +// +// β€’ "agentOS Apps" lockup: agentOS wordmark + "Apps" (Manrope Semibold, ink) +// β€’ V8 + SQLite + Rivet Actors, each in its own rounded "app tile" (the +// landing page FLOATING_AGENTS treatment), scattered with V8 dominant +// β€’ subtle letter-spaced "PREVIEW" label, top center +// +// Reproducible from a fresh checkout: fonts come from website/, logos + the +// Helvetica-clone OS font are vendored in ./assets, playwright is resolved from +// the repo's node_modules, and it drives system Chromium. +// +// node docs-internal/launch-graphics/render-launch.mjs +import { createRequire } from "node:module"; +import { mkdir, readFile, writeFile, readdir } from "node:fs/promises"; +import os from "node:os"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; + +const require = createRequire(import.meta.url); +const SCRIPT_DIR = path.dirname(fileURLToPath(import.meta.url)); +const REPO = path.resolve(SCRIPT_DIR, "../../../.."); +const CHROMIUM = "/usr/bin/chromium"; +// Rendered images are NOT committed β€” they land in ~/tmp (override with arg 1). +const OUT_DIR = process.argv[2] || path.join(os.homedir(), "tmp/agentos-graphics"); + +// Resolve playwright from the repo's pnpm store (version-agnostic). +async function loadChromium() { + const pnpm = path.join(REPO, "node_modules/.pnpm"); + const dir = (await readdir(pnpm)).find((d) => /^playwright@/.test(d)); + if (!dir) throw new Error("playwright not found under node_modules/.pnpm β€” run `pnpm install`"); + return require(path.join(pnpm, dir, "node_modules/playwright")).chromium; +} + +const WIDTH = 2048; +const HEIGHT = 1024; + +const dataUrl = (mime, bytes) => `data:${mime};base64,${bytes.toString("base64")}`; +const clean = (s) => s.replace(/<\?xml[^>]*\?>/i, "").replace(/]*>/i, ""); +// Prefix ids/refs so multiple inline SVGs don't collide (e.g. `_clip1`). +const nsIds = (svg, p) => + svg + .replace(/id="([^"]+)"/g, `id="${p}$1"`) + .replace(/url\(#([^)]+)\)/g, `url(#${p}$1)`) + .replace(/(xlink:href|href)="#([^"]+)"/g, `$1="#${p}$2"`); + +const A = (...p) => path.join(SCRIPT_DIR, "assets", ...p); +const [sansFont, osFont, rawWordmark, v8Raw, sqliteRaw, actorsRaw] = await Promise.all([ + readFile(path.join(REPO, "public/fonts/manrope/Manrope-Variable-latin.woff2")), + readFile(A("NimbusSans-Bold.otf")), // URW Helvetica clone β†’ the wordmark's live "OS" text + readFile(path.join(REPO, "public/images/agent-os/agentos-hero-logo.svg"), "utf8"), + readFile(A("v8.svg"), "utf8"), + readFile(A("sqlite.svg"), "utf8"), + readFile(A("actors.svg"), "utf8"), +]); + +const wordmark = nsIds( + clean(rawWordmark).replace(/font-family:'Helvetica-Bold', 'Helvetica'/g, "font-family:'AgentOS OS'"), + "wm-", +); +const ART = { + v8: nsIds(clean(v8Raw), "v8-"), + sqlite: nsIds(clean(sqliteRaw), "sq-"), + // Actors ships white-stroked for dark UIs; recolor to the site ink. + actors: nsIds(clean(actorsRaw).replace(/white/g, "#1B1916").replace(/#fff(fff)?\b/gi, "#1B1916"), "ac-"), +}; + +// Site palette (tailwind.config / global.css) +const INK = "#1B1916"; +const INK_SOFT = "#56524A"; +const PAPER = "#EFEFEF"; + +// F5 tile arrangement β€” V8 dominant in the middle, gently scattered + rotated. +// Content scaled to ~0.95 to leave headroom for the PREVIEW pill. +const S = 0.95; +const TILES = [ + { k: "sqlite", left: 24, top: 58, size: 222, rot: 7, inner: 0.56 }, + { k: "v8", left: 52, top: 42, size: 286, rot: -6, inner: 0.60 }, + { k: "actors", left: 79, top: 56, size: 240, rot: 5, inner: 0.60 }, +]; + +const tileHtml = (t) => { + const size = Math.round(t.size * S); + const inner = Math.round(size * t.inner); + const radius = Math.round(size * 0.2); // ~rounded-2xl proportion + return `
+
`; +}; + +const html = ` +
+
PREVIEW
+
+
${wordmark}
+
Apps
+
+
${TILES.map(tileHtml).join("")}
+
+`; + +await mkdir(OUT_DIR, { recursive: true }); +await writeFile(path.join(OUT_DIR, "launch.html"), html); + +const chromium = await loadChromium(); +const browser = await chromium.launch({ headless: true, executablePath: CHROMIUM }); +try { + const page = await browser.newPage({ viewport: { width: WIDTH, height: HEIGHT }, deviceScaleFactor: 1 }); + await page.setContent(html, { waitUntil: "load" }); + await page.evaluate(() => document.fonts.ready); + // Optically center "Apps" on the wordmark's OS square badge. + await page.evaluate(() => { + const apps = document.querySelector(".product"); + const frame = document.querySelector(".wordmark #wm-frame-layer").getBoundingClientRect(); + const ar = apps.getBoundingClientRect(); + const fs = parseFloat(getComputedStyle(apps).fontSize); + apps.style.transform = `translateY(${(frame.top + frame.bottom) / 2 - (ar.top + ar.bottom) / 2 - fs * 0.06}px)`; + }); + await page.screenshot({ path: path.join(OUT_DIR, "launch.png"), fullPage: false }); + await page.close(); +} finally { + await browser.close(); +} +console.log(`wrote ${path.join(OUT_DIR, "launch.png")}`); diff --git a/.claude/skills/launch-graphics/graphics/render-technical.mjs b/.claude/skills/launch-graphics/graphics/render-technical.mjs new file mode 100644 index 00000000..a6d39f27 --- /dev/null +++ b/.claude/skills/launch-graphics/graphics/render-technical.mjs @@ -0,0 +1,129 @@ +// agentOS Apps β€” technical code-snippet image (white, 2048px wide, auto height). +// A titled "agentOS Apps" header + N syntax-highlighted steps, styled to match +// the Rivet launch technical-image treatment (JetBrains Mono, Ayu Dark @ #0a0a0a, +// 132px gutters). Steps come from ./snippets.json. +// +// node docs-internal/graphics/render-technical.mjs +// +// Requires `shiki` (a dev-only highlighter, NOT a repo dependency). Resolved +// from the repo's node_modules, else set SHIKI_DIR=/path/to/shiki, else +// `pnpm add -D shiki`. +import { createRequire } from "node:module"; +import { mkdir, readFile, writeFile, readdir } from "node:fs/promises"; +import os from "node:os"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; + +const require = createRequire(import.meta.url); +const SCRIPT_DIR = path.dirname(fileURLToPath(import.meta.url)); +const REPO = path.resolve(SCRIPT_DIR, "../../../.."); +const CHROMIUM = "/usr/bin/chromium"; +const OUT_DIR = process.argv[2] || path.join(os.homedir(), "tmp/agentos-graphics"); +const WIDTH = 2048; + +async function findPkg(name) { + const stores = [path.join(REPO, "node_modules/.pnpm"), process.env.SHIKI_DIR && path.dirname(path.dirname(process.env.SHIKI_DIR))].filter(Boolean); + for (const pnpm of stores) { + try { + const dir = (await readdir(pnpm)).find((d) => new RegExp(`^${name}@`).test(d)); + if (dir) return path.join(pnpm, dir, "node_modules", name); + } catch {} + } + if (name === "shiki" && process.env.SHIKI_DIR) return process.env.SHIKI_DIR; + return null; +} + +const pwDir = await findPkg("playwright"); +if (!pwDir) throw new Error("playwright not found under node_modules/.pnpm β€” run `pnpm install`"); +const { chromium } = require(pwDir); + +const shikiDir = await findPkg("shiki"); +if (!shikiDir) throw new Error("shiki not found β€” `pnpm add -D shiki` or set SHIKI_DIR=/path/to/node_modules/shiki"); +const { getSingletonHighlighter } = await import(path.join(shikiDir, "dist/index.mjs")); +const { default: ayuDark } = await import(path.join(shikiDir, "dist/themes/ayu-dark.mjs")); + +// Ayu Dark with the panel background remapped to #0a0a0a (matches the website theme). +const theme = structuredClone(ayuDark); +theme.name = "ayu-dark-agentos"; +for (const k in theme.colors ?? {}) if (theme.colors[k] === "#0b0e14") theme.colors[k] = "#0a0a0a"; +for (const tc of theme.tokenColors ?? []) for (const k in tc.settings ?? {}) if (tc.settings[k] === "#0b0e14") tc.settings[k] = "#0a0a0a"; + +const dataUrl = (mime, bytes) => `data:${mime};base64,${bytes.toString("base64")}`; +const escapeHtml = (v) => v.replace(/&/g, "&").replace(//g, ">").replace(/"/g, """); + +const A = (...p) => path.join(SCRIPT_DIR, "assets", ...p); +const [monoFont, sansFont, osFont, rawLogo, sectionsRaw] = await Promise.all([ + readFile(path.join(REPO, "public/fonts/jetbrains-mono/JetBrainsMono-Variable-latin.woff2")), + readFile(path.join(REPO, "public/fonts/manrope/Manrope-Variable-latin.woff2")), + readFile(A("NimbusSans-Bold.otf")), + readFile(path.join(REPO, "public/images/agent-os/agentos-hero-logo.svg"), "utf8"), + readFile(path.join(SCRIPT_DIR, "snippets.json"), "utf8"), +]); +const sections = JSON.parse(sectionsRaw); + +const logo = rawLogo + .replace(/<\?xml[^>]*\?>/i, "") + .replace(/]*>/i, "") + .replace(/font-family:'Helvetica-Bold', 'Helvetica'/g, "font-family:'AgentOS OS'"); + +const highlighter = await getSingletonHighlighter({ + langs: [...new Set(sections.map((s) => s.language))], + themes: [theme], +}); +const renderedSections = sections + .map(({ title, language, code }) => { + const highlighted = highlighter.codeToHtml(code, { lang: language, theme: theme.name }); + return `

${escapeHtml(title)}

+
${highlighted}
`; + }) + .join("\n"); + +const html = ` + +
+
+ +
Apps
+
+
${renderedSections}
+
+`; + +await mkdir(OUT_DIR, { recursive: true }); +await writeFile(path.join(OUT_DIR, "technical.html"), html); + +const browser = await chromium.launch({ headless: true, executablePath: CHROMIUM }); +try { + const page = await browser.newPage({ viewport: { width: WIDTH, height: 1238 }, deviceScaleFactor: 1 }); + await page.setContent(html, { waitUntil: "load" }); + await page.evaluate(() => document.fonts.ready); + const height = await page.$eval(".card", (c) => Math.ceil(c.getBoundingClientRect().height)); + await page.setViewportSize({ width: WIDTH, height }); + const overflow = await page.$$eval(".code-block", (blocks) => + blocks.map((b, i) => ({ i, o: b.scrollWidth - b.clientWidth })).find(({ o }) => o > 1), + ); + if (overflow) throw new Error(`Code section ${overflow.i + 1} overflows by ${Math.ceil(overflow.o)}px β€” shorten it`); + await page.screenshot({ path: path.join(OUT_DIR, "technical.png"), fullPage: false }); + await page.close(); +} finally { + await browser.close(); +} +console.log(`wrote ${path.join(OUT_DIR, "technical.png")}`); diff --git a/.claude/skills/launch-graphics/graphics/snippets-dynamic-apps.json b/.claude/skills/launch-graphics/graphics/snippets-dynamic-apps.json new file mode 100644 index 00000000..86a98007 --- /dev/null +++ b/.claude/skills/launch-graphics/graphics/snippets-dynamic-apps.json @@ -0,0 +1,17 @@ +[ + { + "title": "1. Generate", + "language": "ts", + "code": "import { anthropic } from \"@ai-sdk/anthropic\";\nimport { generateText, Output } from \"ai\";\nimport { z } from \"zod\";\n\nconst { output } = await generateText({\n model: anthropic(\"claude-sonnet-4-5\"),\n output: Output.object({\n schema: z.object({ files: z.record(z.string(), z.string()) }),\n }),\n prompt: \"Build a team board as a complete web app.\",\n});" + }, + { + "title": "2. Deploy", + "language": "ts", + "code": "import { deployApp } from \"@rivet-dev/dynamic-apps\";\n\nawait deployApp({\n appId: \"pied-piper\",\n files: output.files,\n});" + }, + { + "title": "3. Route", + "language": "ts", + "code": "import { serve } from \"@hono/node-server\";\nimport { appsRouter } from \"@rivet-dev/dynamic-apps\";\nimport { Hono } from \"hono\";\n\nconst server = new Hono();\nserver.route(\"/apps\", appsRouter);\n\nserve({ fetch: server.fetch, port: 3000 });" + } +] \ No newline at end of file diff --git a/.claude/skills/launch-graphics/graphics/snippets.json b/.claude/skills/launch-graphics/graphics/snippets.json new file mode 100644 index 00000000..cb71985d --- /dev/null +++ b/.claude/skills/launch-graphics/graphics/snippets.json @@ -0,0 +1,17 @@ +[ + { + "title": "1. Set up the Hono router", + "language": "ts", + "code": "import { serve } from \"@hono/node-server\";\nimport { appsRouter } from \"@rivet-dev/agentos-apps\";\nimport { Hono } from \"hono\";\n\nconst server = new Hono();\nserver.route(\"/apps\", appsRouter);\n\nserve({ fetch: server.fetch, port: 3000 });" + }, + { + "title": "2. Deploy the app", + "language": "ts", + "code": "import { deployApp } from \"@rivet-dev/agentos-apps\";\n\nawait deployApp({\n appId: \"hello-world\",\n source: new URL(\"./app/\", import.meta.url),\n});" + }, + { + "title": "3. Fetch your app", + "language": "ts", + "code": "const res = await fetch(\"https://foobar.com/apps/hello-world\");\nconst html = await res.text();" + } +] diff --git a/package.json b/package.json index 5cece1d2..c17e7dc6 100644 --- a/package.json +++ b/package.json @@ -12,9 +12,9 @@ "format": "prettier --write .", "astro": "astro", "new-post": "tsx ./scripts/new-post.ts", - "render-launch-images": "tsx ../.claude/skills/create-launch-post/scripts/render-launch-images.ts", - "render-tile-images": "tsx ../.claude/skills/create-launch-post/scripts/render-tile-images.ts", - "render-technical-image": "tsx ../.claude/skills/create-launch-post/scripts/render-technical-image.ts", + "render-launch-images": "tsx ./.claude/skills/create-launch-post/scripts/render-launch-images.ts", + "render-tile-images": "tsx ./.claude/skills/create-launch-post/scripts/render-tile-images.ts", + "render-technical-image": "tsx ./.claude/skills/create-launch-post/scripts/render-technical-image.ts", "gen:markdown": "tsx ./scripts/generate-markdown.ts", "gen:skills": "tsx ./scripts/generate-skills.ts", "check:sitemap": "tsx ./scripts/check-sitemap.ts", diff --git a/public/images/registry/v8.svg b/public/images/registry/v8.svg new file mode 100644 index 00000000..6e088f3e --- /dev/null +++ b/public/images/registry/v8.svg @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/src/components/marketing/SetupWithAgentButton.tsx b/src/components/marketing/SetupWithAgentButton.tsx new file mode 100644 index 00000000..29ed793f --- /dev/null +++ b/src/components/marketing/SetupWithAgentButton.tsx @@ -0,0 +1,68 @@ +'use client'; + +import { Check, Copy } from 'lucide-react'; +import { useState } from 'react'; +import { INK_PANEL_LIGHT_BUTTON_CLASS, PRODUCT_HERO_PRIMARY_BUTTON_CLASS } from '@/components/marketing/typography'; + +// Firefox (and Chrome) only expose navigator.clipboard in secure contexts, so +// a page served over plain http on a LAN address has no clipboard API. Fall +// back to a hidden textarea + execCommand('copy'), which still works there. +async function copyText(text: string): Promise { + try { + if (navigator.clipboard?.writeText) { + await navigator.clipboard.writeText(text); + return true; + } + } catch { + // fall through to the legacy path + } + const textarea = document.createElement('textarea'); + textarea.value = text; + textarea.setAttribute('readonly', ''); + textarea.style.position = 'fixed'; + textarea.style.top = '0'; + textarea.style.left = '0'; + textarea.style.opacity = '0'; + document.body.appendChild(textarea); + textarea.select(); + let ok = false; + try { + ok = document.execCommand('copy'); + } catch { + ok = false; + } + document.body.removeChild(textarea); + return ok; +} + +// "Set up with your agent": copies a ready-to-paste prompt for a coding agent. +// The accent variant is the hero's single ember; `light` is for ink panels. +// Shared by all four product pages so the affordance is identical everywhere. +export const SetupWithAgentButton = ({ prompt, variant = 'accent' }: { prompt: string; variant?: 'accent' | 'light' }) => { + const [copied, setCopied] = useState(false); + + const handleCopy = async () => { + const ok = await copyText(prompt); + if (!ok) return; + setCopied(true); + setTimeout(() => setCopied(false), 2000); + }; + + return ( + + ); +}; diff --git a/src/components/marketing/agentos/AgentOSPage.tsx b/src/components/marketing/agentos/AgentOSPage.tsx index 00d26da6..d60c26a7 100644 --- a/src/components/marketing/agentos/AgentOSPage.tsx +++ b/src/components/marketing/agentos/AgentOSPage.tsx @@ -11,7 +11,6 @@ import { Workflow, ChevronLeft, ChevronRight, - Copy, Check, ShieldCheck, Package, @@ -33,7 +32,8 @@ import { wordmarkMaskStyle } from '@/lib/product-accent'; import { canonicalizeInternalHref } from '@/lib/internalHref'; import { registry } from '@/data/registry'; import { REGISTRY_ICONS } from '@/data/registry-icons'; -import { AGENT_PROMPT } from '@/data/agentPrompt'; +import { AGENT_SETUP_PROMPTS } from '@/data/agentSetupPrompt'; +import { SetupWithAgentButton } from '@/components/marketing/SetupWithAgentButton'; import { benchColdStart, benchWorkloads, @@ -55,7 +55,6 @@ import { PRIMARY_INK_BUTTON_CLASS, PRODUCT_HERO_H1_CLASS, PRODUCT_HERO_INNER_CLASS, - PRODUCT_HERO_PRIMARY_BUTTON_CLASS, PRODUCT_HERO_SECTION_CLASS, PRODUCT_HERO_SUBTITLE_CLASS, SECTION_H2_CLASS, @@ -293,33 +292,10 @@ export const AnimatedAgentOSLogo = ({ className, displayedAgent, drawDurationSec ); }; -// --- Set up with your agent (copies the agent prompt) --- -const SetupWithAgent = ({ variant = 'accent' }: { variant?: 'accent' | 'light' }) => { - const [copied, setCopied] = useState(false); - - const handleCopy = async () => { - await navigator.clipboard.writeText(AGENT_PROMPT); - setCopied(true); - setTimeout(() => setCopied(false), 2000); - }; - - return ( - - ); -}; +// --- Set up with your agent: shared button + shared prompt (src/data/agentSetupPrompt.ts) --- +const SetupWithAgent = ({ variant = 'accent' }: { variant?: 'accent' | 'light' }) => ( + +); // --- Hero Tabs (scrollable with fade + arrows) --- interface HeroTabEntry { diff --git a/src/components/marketing/diagrams/dynamicAppsDiagrams.tsx b/src/components/marketing/diagrams/dynamicAppsDiagrams.tsx index d606f350..2a1dcbd3 100644 --- a/src/components/marketing/diagrams/dynamicAppsDiagrams.tsx +++ b/src/components/marketing/diagrams/dynamicAppsDiagrams.tsx @@ -1,26 +1,37 @@ 'use client'; import { motion, useInView, useReducedMotion } from 'framer-motion'; -import { useRef } from 'react'; +import { Bot, Braces, Globe, Lock, Route } from 'lucide-react'; +import type { ReactNode } from 'react'; +import { useEffect, useRef, useState } from 'react'; +import { wordmarkMaskStyle } from '@/lib/product-accent'; +import { productLogos } from '@/sitemap/productLogos'; // --------------------------------------------------------------------------- -// Two abstract diagrams for the /dynamic-apps chapters. Porcelain card, grey -// shapes, one ember moment each. No axes, clocks, or code strings β€” each -// picture makes one point. Conventions match workflowDiagrams.tsx. +// Three diagrams for the /dynamic-apps chapters. Same vocabulary as the hero +// explainer: white nodes with a hairline ring, one pine highlight for the +// Rivet-owned piece, ink arrows. No shadows, no orange β€” the page's single +// ember is the hero CTA. // --------------------------------------------------------------------------- -const CARD_CLASS = - 'rounded-2xl bg-white/55 p-6 ring-1 ring-ink/[0.08] shadow-[inset_0_1px_0_rgba(255,255,255,0.9)] md:p-8'; +const CARD_CLASS = 'rounded-xl border border-ink/10 bg-white/55 px-6 py-10 md:px-10 md:py-12'; +const NODE_CLASS = 'rounded-lg bg-white px-4 py-2.5 text-sm font-medium text-ink ring-1 ring-ink/10'; +const CHIP_CLASS = 'rounded-md bg-paper px-2.5 py-1.5 text-xs text-ink-soft ring-1 ring-ink/[0.08]'; +const CHIP_PINE_CLASS = 'rounded-md bg-pine px-2.5 py-1.5 text-xs font-medium text-white'; +const SANDBOX_CLASS = 'rounded-lg border border-dashed border-ink/20 p-3'; -const INK = '#1B1916'; -const INK_SOFT = '#56524A'; const INK_FAINT = '#8A8478'; -const CREAM = '#F4F1E7'; -const EMBER = '#CB5A33'; -const EMBER_DEEP = '#AB451F'; +const INK_SOFT = '#56524A'; +const INK = '#1B1916'; +const PINE = '#2E4034'; +// Build verdict colors (repair diagram only). +const FAIL = '#B42318'; +const FAIL_FILL = '#FDECEA'; +const PASS = '#2E4034'; +const PASS_FILL = '#E7ECE7'; // Staged fade-in gated on scroll-into-view; settled instantly under reduced -// motion. `show(n)` fades element n in at ~0.18 s intervals. +// motion. `show(n)` fades element n in at ~0.16 s intervals. const useDiagram = () => { const ref = useRef(null); const inView = useInView(ref, { once: true, margin: '-15% 0px' }); @@ -31,133 +42,469 @@ const useDiagram = () => { : { initial: { opacity: 0 }, animate: inView ? { opacity: 1 } : { opacity: 0 }, - transition: { duration: 0.35, delay: 0.15 + order * 0.18, ease: 'easeOut' as const }, + transition: { duration: 0.3, delay: 0.1 + order * 0.16, ease: 'easeOut' as const }, }; - return { ref, show }; + return { ref, inView, reduced, show }; }; -// A request crosses your server before Rivet ever sees it. The ember dot is -// the prewarmed VM answering. +// Horizontal on md+, vertical below. +const Arrow = ({ className = '' }: { className?: string }) => ( + +); + +// Always vertical; sage so it reads inside ink panels. +const DownArrow = ({ className = '' }: { className?: string }) => ( + +); + +// The agentOS product mark inside its ink tile (see Product Marks in CLAUDE.md). +const AgentOSTile = () => ( +