From 569f6b034564889d3a16f3ad46fbeb532924ee65 Mon Sep 17 00:00:00 2001 From: WizardLoop <67387949+WizardLoop@users.noreply.github.com> Date: Tue, 27 Oct 2026 08:00:39 +0200 Subject: [PATCH 1/5] Build illustrated WizardLoop portfolio and guarded integrations --- .editorconfig | 12 + .env.example | 8 + .github/PULL_REQUEST_TEMPLATE.md | 11 + .github/workflows/ci.yml | 37 + .github/workflows/deploy.yml | 46 + .gitignore | 19 + .prettierrc.json | 12 + CHANGELOG.md | 30 + CODE_OF_CONDUCT.md | 13 + CONTRIBUTING.md | 36 + LICENSE | 21 + README.md | 150 +- SECURITY.md | 29 + THIRD_PARTY_NOTICES.md | 43 + astro.config.mjs | 14 + concepts/assets/base.css | 7 + concepts/assets/common.js | 80 + concepts/assets/concept-a.css | 6 + concepts/assets/concept-b.css | 35 + concepts/assets/concept-c.css | 352 + concepts/assets/fonts/CORMORANT-LICENSE.txt | 93 + concepts/assets/fonts/MANROPE-LICENSE.txt | 93 + .../assets/fonts/SPACE-GROTESK-LICENSE.txt | 93 + .../assets/fonts/cormorant-italic-latin.woff2 | Bin 0 -> 38140 bytes concepts/assets/fonts/cormorant-latin.woff2 | Bin 0 -> 35340 bytes concepts/assets/fonts/manrope-latin.woff2 | Bin 0 -> 24836 bytes .../assets/fonts/spacegrotesk-latin.woff2 | Bin 0 -> 22288 bytes concepts/assets/profile-fallback.svg | 1 + concepts/assets/profile-telegram.jpg | Bin 0 -> 32157 bytes concepts/concept-a/index.html | 62 + concepts/concept-b/index.html | 92 + concepts/concept-c/index.html | 121 + concepts/index.html | 4 + concepts/robots.txt | 2 + docs/DEPLOYMENT.md | 88 + docs/DESIGN-ASSET-INVENTORY.md | 120 + docs/FEEDBACK-SETUP.md | 133 + docs/INTEGRATION-FEASIBILITY.md | 90 + docs/POSTS-SYNC.md | 177 + docs/PROFILE-SYNC.md | 127 + docs/QA-INTEGRATIONS.md | 43 + docs/QA.md | 99 + docs/REPOSITORY-AUDIT.md | 88 + docs/previews/home-dark-desktop.png | Bin 0 -> 359305 bytes docs/previews/home-dark-mobile.png | Bin 0 -> 134250 bytes docs/previews/home-light-desktop.png | Bin 0 -> 357956 bytes docs/previews/home-light-mobile.png | Bin 0 -> 133406 bytes docs/qa/browser-summary.json | 190 + docs/qa/lighthouse-summary.json | 1159 ++++ package-lock.json | 6150 +++++++++++++++++ package.json | 48 + playwright.config.ts | 90 + public/CNAME | 1 + public/apple-touch-icon.png | Bin 0 -> 67524 bytes public/assets/css/style.css | 7 + public/favicon-16.png | Bin 0 -> 936 bytes public/favicon-32.png | Bin 0 -> 2954 bytes public/favicon.svg | 1 + public/fonts/CORMORANT-LICENSE.txt | 93 + public/fonts/MANROPE-LICENSE.txt | 93 + public/fonts/SPACE-GROTESK-LICENSE.txt | 93 + public/fonts/cormorant-italic-latin.woff2 | Bin 0 -> 38140 bytes public/fonts/cormorant-latin.woff2 | Bin 0 -> 35340 bytes public/fonts/manrope-latin.woff2 | Bin 0 -> 24836 bytes public/fonts/spacegrotesk-latin.woff2 | Bin 0 -> 22288 bytes public/js/preferences.js | 17 + public/media/animations/arcane-aura.tgs | Bin 0 -> 17957 bytes public/media/animations/arcane-coder.tgs | Bin 0 -> 17414 bytes .../media/animations/aurora-ribbon-wings.tgs | Bin 0 -> 14212 bytes public/media/animations/bubble-brew.tgs | Bin 0 -> 17349 bytes .../media/animations/carnival-hat-burst.tgs | Bin 0 -> 19753 bytes .../animations/chromatic-liquid-wave.tgs | Bin 0 -> 20434 bytes public/media/animations/clockwork-spring.tgs | Bin 0 -> 21721 bytes public/media/animations/cosmic-fishing.tgs | Bin 0 -> 16795 bytes public/media/animations/crystal-forge.tgs | Bin 0 -> 16822 bytes public/media/animations/crystal-thaw.tgs | Bin 0 -> 19050 bytes public/media/animations/flag-br.tgs | Bin 0 -> 43791 bytes public/media/animations/flag-fr.tgs | Bin 0 -> 20474 bytes public/media/animations/flag-gb.tgs | Bin 0 -> 28821 bytes public/media/animations/flag-il.tgs | Bin 0 -> 25281 bytes public/media/animations/flag-jp.tgs | Bin 0 -> 19015 bytes public/media/animations/flag-us.tgs | Bin 0 -> 38473 bytes public/media/animations/hourglass-twist.tgs | Bin 0 -> 16771 bytes .../animations/infinity-constellation.tgs | Bin 0 -> 18014 bytes public/media/animations/living-spellbook.tgs | Bin 0 -> 16831 bytes public/media/animations/magnetic-stars.tgs | Bin 0 -> 16427 bytes public/media/animations/midnight-dj.tgs | Bin 0 -> 17383 bytes public/media/animations/molten-ember.tgs | Bin 0 -> 19899 bytes public/media/animations/moon-hammock.tgs | Bin 0 -> 16252 bytes public/media/animations/neon-wave-surfer.tgs | Bin 0 -> 16860 bytes .../media/animations/origami-enchantment.tgs | Bin 0 -> 25100 bytes public/media/animations/overwhelmed-hat.tgs | Bin 0 -> 37443 bytes public/media/animations/paints-the-world.tgs | Bin 0 -> 45551 bytes public/media/animations/portal-alchemy.tgs | Bin 0 -> 40359 bytes public/media/animations/psychedelic-heart.tgs | Bin 0 -> 25016 bytes .../media/animations/psychedelic-vortex.tgs | Bin 0 -> 43621 bytes public/media/animations/rainbow-rain.tgs | Bin 0 -> 16701 bytes public/media/animations/reality-zipper.tgs | Bin 0 -> 62399 bytes public/media/animations/signature-spirit.tgs | Bin 0 -> 15468 bytes .../media/animations/silver-lunar-eclipse.tgs | Bin 0 -> 19156 bytes public/media/animations/spaceship-flight.tgs | Bin 0 -> 28458 bytes public/media/animations/thunder-catcher.tgs | Bin 0 -> 16515 bytes public/media/hero/manifest.json | 34 + public/media/hero/portal-dark.mp4 | Bin 0 -> 635146 bytes public/media/hero/portal-light.mp4 | Bin 0 -> 645033 bytes public/media/licenses/FEATHER-LICENSE.txt | 21 + public/media/licenses/FLAGS-NOTICES.txt | 312 + public/media/licenses/LOTTIE-LICENSE.txt | 21 + public/media/licenses/PAPERMOD-LICENSE.txt | 22 + public/media/posters/arcane-aura.webp | Bin 0 -> 151134 bytes public/media/posters/arcane-coder.webp | Bin 0 -> 63498 bytes public/media/posters/aurora-ribbon-wings.webp | Bin 0 -> 75342 bytes public/media/posters/bubble-brew.webp | Bin 0 -> 86746 bytes public/media/posters/carnival-hat-burst.webp | Bin 0 -> 73474 bytes .../media/posters/chromatic-liquid-wave.webp | Bin 0 -> 85518 bytes public/media/posters/clockwork-spring.webp | Bin 0 -> 61052 bytes public/media/posters/cosmic-fishing.webp | Bin 0 -> 72376 bytes public/media/posters/crystal-forge.webp | Bin 0 -> 81136 bytes public/media/posters/crystal-thaw.webp | Bin 0 -> 77658 bytes public/media/posters/flag-br.webp | Bin 0 -> 57852 bytes public/media/posters/flag-fr.webp | Bin 0 -> 37640 bytes public/media/posters/flag-gb.webp | Bin 0 -> 46908 bytes public/media/posters/flag-il.webp | Bin 0 -> 43502 bytes public/media/posters/flag-jp.webp | Bin 0 -> 32922 bytes public/media/posters/flag-us.webp | Bin 0 -> 49826 bytes public/media/posters/hourglass-twist.webp | Bin 0 -> 65978 bytes .../media/posters/infinity-constellation.webp | Bin 0 -> 92886 bytes public/media/posters/living-spellbook.webp | Bin 0 -> 101618 bytes public/media/posters/magnetic-stars.webp | Bin 0 -> 86634 bytes public/media/posters/midnight-dj.webp | Bin 0 -> 97660 bytes public/media/posters/molten-ember.webp | Bin 0 -> 86672 bytes public/media/posters/moon-hammock.webp | Bin 0 -> 81692 bytes public/media/posters/neon-wave-surfer.webp | Bin 0 -> 76446 bytes public/media/posters/origami-enchantment.webp | Bin 0 -> 69250 bytes public/media/posters/overwhelmed-hat.webp | Bin 0 -> 37774 bytes public/media/posters/paints-the-world.webp | Bin 0 -> 199692 bytes public/media/posters/portal-alchemy.webp | Bin 0 -> 196314 bytes public/media/posters/psychedelic-heart.webp | Bin 0 -> 80536 bytes public/media/posters/psychedelic-vortex.webp | Bin 0 -> 208366 bytes public/media/posters/rainbow-rain.webp | Bin 0 -> 82328 bytes public/media/posters/reality-zipper.webp | Bin 0 -> 118188 bytes public/media/posters/signature-spirit.webp | Bin 0 -> 97244 bytes .../media/posters/silver-lunar-eclipse.webp | Bin 0 -> 67394 bytes public/media/posters/spaceship-flight.webp | Bin 0 -> 98258 bytes public/media/posters/thunder-catcher.webp | Bin 0 -> 75764 bytes public/media/previews/arcane-aura.webp | Bin 0 -> 67608 bytes public/media/previews/arcane-coder.webp | Bin 0 -> 28380 bytes .../media/previews/aurora-ribbon-wings.webp | Bin 0 -> 27988 bytes public/media/previews/bubble-brew.webp | Bin 0 -> 36298 bytes public/media/previews/carnival-hat-burst.webp | Bin 0 -> 33638 bytes .../media/previews/chromatic-liquid-wave.webp | Bin 0 -> 37898 bytes public/media/previews/clockwork-spring.webp | Bin 0 -> 28780 bytes public/media/previews/cosmic-fishing.webp | Bin 0 -> 35520 bytes public/media/previews/crystal-forge.webp | Bin 0 -> 33422 bytes public/media/previews/crystal-thaw.webp | Bin 0 -> 31868 bytes public/media/previews/flag-br.webp | Bin 0 -> 20248 bytes public/media/previews/flag-fr.webp | Bin 0 -> 12814 bytes public/media/previews/flag-gb.webp | Bin 0 -> 22854 bytes public/media/previews/flag-il.webp | Bin 0 -> 17784 bytes public/media/previews/flag-jp.webp | Bin 0 -> 11492 bytes public/media/previews/flag-us.webp | Bin 0 -> 27050 bytes public/media/previews/hourglass-twist.webp | Bin 0 -> 32418 bytes .../previews/infinity-constellation.webp | Bin 0 -> 53458 bytes public/media/previews/living-spellbook.webp | Bin 0 -> 39322 bytes public/media/previews/magnetic-stars.webp | Bin 0 -> 39646 bytes public/media/previews/midnight-dj.webp | Bin 0 -> 37800 bytes public/media/previews/molten-ember.webp | Bin 0 -> 32756 bytes public/media/previews/moon-hammock.webp | Bin 0 -> 33270 bytes public/media/previews/neon-wave-surfer.webp | Bin 0 -> 31856 bytes .../media/previews/origami-enchantment.webp | Bin 0 -> 29152 bytes public/media/previews/overwhelmed-hat.webp | Bin 0 -> 17432 bytes public/media/previews/paints-the-world.webp | Bin 0 -> 70842 bytes public/media/previews/portal-alchemy.webp | Bin 0 -> 79018 bytes public/media/previews/psychedelic-heart.webp | Bin 0 -> 29316 bytes public/media/previews/psychedelic-vortex.webp | Bin 0 -> 84882 bytes public/media/previews/rainbow-rain.webp | Bin 0 -> 38188 bytes public/media/previews/reality-zipper.webp | Bin 0 -> 52276 bytes public/media/previews/signature-spirit.webp | Bin 0 -> 33288 bytes .../media/previews/silver-lunar-eclipse.webp | Bin 0 -> 25088 bytes public/media/previews/spaceship-flight.webp | Bin 0 -> 42122 bytes public/media/previews/thunder-catcher.webp | Bin 0 -> 33330 bytes public/media/profile/avatar.jpg | Bin 0 -> 32157 bytes public/media/profile/fallback.svg | 1 + public/media/profile/github.jpg | Bin 0 -> 200812 bytes public/media/profile/poster.jpg | Bin 0 -> 32157 bytes public/media/profile/profile.json | 15 + public/robots.txt | 7 + public/social-preview.png | Bin 0 -> 370909 bytes scripts/check-build.mjs | 40 + scripts/check.mjs | 40 + scripts/export-hero-video.mjs | 513 ++ scripts/generate-brand.mjs | 96 + scripts/generate-previews.mjs | 23 + scripts/interactions.mjs | 218 + scripts/lighthouse.mjs | 23 + scripts/preview.mjs | 29 + scripts/qa.mjs | 107 + services/feedback/.env.example | 10 + services/feedback/.gitignore | 8 + services/feedback/package-lock.json | 14 + services/feedback/package.json | 14 + services/feedback/scripts/mock-server.mjs | 33 + services/feedback/scripts/verify-chat.mjs | 74 + services/feedback/test/mock-server.test.mjs | 32 + services/feedback/test/worker.test.mjs | 194 + services/feedback/worker.mjs | 260 + services/feedback/wrangler.jsonc | 24 + services/posts-sync/.gitignore | 7 + services/posts-sync/deploy/run_service.py | 213 + .../deploy/wizardloop-build.service | 26 + .../posts-sync/deploy/wizardloop-build.timer | 11 + .../deploy/wizardloop-posts.service | 23 + .../posts-sync/deploy/wizardloop-posts.timer | 11 + .../deploy/wizardloop-profile.service | 25 + services/posts-sync/fixtures/updates.json | 10 + services/posts-sync/posts_sync.py | 449 ++ services/posts-sync/test_deploy.py | 129 + services/posts-sync/test_posts_sync.py | 330 + services/profile-sync/.gitignore | 7 + .../profile-sync/fixtures/active-still.json | 10 + services/profile-sync/profile_sync.py | 582 ++ services/profile-sync/requirements.txt | 6 + services/profile-sync/test_profile_sync.py | 288 + src/components/HeroArt.astro | 37 + src/components/Icon.astro | 44 + src/components/MotionArt.astro | 29 + src/config/site.ts | 25 + src/content/artworks.json | 272 + src/content/flags.json | 56 + src/content/posts.json | 12 + src/content/projects.json | 32 + src/env.d.ts | 1 + src/layouts/Base.astro | 334 + src/pages/404.astro | 29 + src/pages/about.astro | 122 + src/pages/accessibility.astro | 128 + src/pages/community.astro | 87 + src/pages/cookies.astro | 104 + src/pages/feedback.astro | 172 + src/pages/index.astro | 306 + src/pages/posts.astro | 116 + src/pages/privacy.astro | 158 + src/pages/projects.astro | 59 + src/pages/security.astro | 93 + src/pages/work.astro | 141 + src/scripts/feedback.ts | 244 + src/scripts/hero.ts | 105 + src/scripts/motion.ts | 232 + src/scripts/posts.ts | 125 + src/scripts/site.ts | 205 + src/styles/global.css | 3063 ++++++++ src/types/lottie-canvas.d.ts | 5 + tests/site.spec.ts | 1449 ++++ tsconfig.json | 5 + 254 files changed, 22530 insertions(+), 1 deletion(-) create mode 100644 .editorconfig create mode 100644 .env.example create mode 100644 .github/PULL_REQUEST_TEMPLATE.md create mode 100644 .github/workflows/ci.yml create mode 100644 .github/workflows/deploy.yml create mode 100644 .gitignore create mode 100644 .prettierrc.json create mode 100644 CHANGELOG.md create mode 100644 CODE_OF_CONDUCT.md create mode 100644 CONTRIBUTING.md create mode 100644 LICENSE create mode 100644 SECURITY.md create mode 100644 THIRD_PARTY_NOTICES.md create mode 100644 astro.config.mjs create mode 100644 concepts/assets/base.css create mode 100644 concepts/assets/common.js create mode 100644 concepts/assets/concept-a.css create mode 100644 concepts/assets/concept-b.css create mode 100644 concepts/assets/concept-c.css create mode 100644 concepts/assets/fonts/CORMORANT-LICENSE.txt create mode 100644 concepts/assets/fonts/MANROPE-LICENSE.txt create mode 100644 concepts/assets/fonts/SPACE-GROTESK-LICENSE.txt create mode 100644 concepts/assets/fonts/cormorant-italic-latin.woff2 create mode 100644 concepts/assets/fonts/cormorant-latin.woff2 create mode 100644 concepts/assets/fonts/manrope-latin.woff2 create mode 100644 concepts/assets/fonts/spacegrotesk-latin.woff2 create mode 100644 concepts/assets/profile-fallback.svg create mode 100644 concepts/assets/profile-telegram.jpg create mode 100644 concepts/concept-a/index.html create mode 100644 concepts/concept-b/index.html create mode 100644 concepts/concept-c/index.html create mode 100644 concepts/index.html create mode 100644 concepts/robots.txt create mode 100644 docs/DEPLOYMENT.md create mode 100644 docs/DESIGN-ASSET-INVENTORY.md create mode 100644 docs/FEEDBACK-SETUP.md create mode 100644 docs/INTEGRATION-FEASIBILITY.md create mode 100644 docs/POSTS-SYNC.md create mode 100644 docs/PROFILE-SYNC.md create mode 100644 docs/QA-INTEGRATIONS.md create mode 100644 docs/QA.md create mode 100644 docs/REPOSITORY-AUDIT.md create mode 100644 docs/previews/home-dark-desktop.png create mode 100644 docs/previews/home-dark-mobile.png create mode 100644 docs/previews/home-light-desktop.png create mode 100644 docs/previews/home-light-mobile.png create mode 100644 docs/qa/browser-summary.json create mode 100644 docs/qa/lighthouse-summary.json create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 playwright.config.ts create mode 100644 public/CNAME create mode 100644 public/apple-touch-icon.png create mode 100644 public/assets/css/style.css create mode 100644 public/favicon-16.png create mode 100644 public/favicon-32.png create mode 100644 public/favicon.svg create mode 100644 public/fonts/CORMORANT-LICENSE.txt create mode 100644 public/fonts/MANROPE-LICENSE.txt create mode 100644 public/fonts/SPACE-GROTESK-LICENSE.txt create mode 100644 public/fonts/cormorant-italic-latin.woff2 create mode 100644 public/fonts/cormorant-latin.woff2 create mode 100644 public/fonts/manrope-latin.woff2 create mode 100644 public/fonts/spacegrotesk-latin.woff2 create mode 100644 public/js/preferences.js create mode 100644 public/media/animations/arcane-aura.tgs create mode 100644 public/media/animations/arcane-coder.tgs create mode 100644 public/media/animations/aurora-ribbon-wings.tgs create mode 100644 public/media/animations/bubble-brew.tgs create mode 100644 public/media/animations/carnival-hat-burst.tgs create mode 100644 public/media/animations/chromatic-liquid-wave.tgs create mode 100644 public/media/animations/clockwork-spring.tgs create mode 100644 public/media/animations/cosmic-fishing.tgs create mode 100644 public/media/animations/crystal-forge.tgs create mode 100644 public/media/animations/crystal-thaw.tgs create mode 100644 public/media/animations/flag-br.tgs create mode 100644 public/media/animations/flag-fr.tgs create mode 100644 public/media/animations/flag-gb.tgs create mode 100644 public/media/animations/flag-il.tgs create mode 100644 public/media/animations/flag-jp.tgs create mode 100644 public/media/animations/flag-us.tgs create mode 100644 public/media/animations/hourglass-twist.tgs create mode 100644 public/media/animations/infinity-constellation.tgs create mode 100644 public/media/animations/living-spellbook.tgs create mode 100644 public/media/animations/magnetic-stars.tgs create mode 100644 public/media/animations/midnight-dj.tgs create mode 100644 public/media/animations/molten-ember.tgs create mode 100644 public/media/animations/moon-hammock.tgs create mode 100644 public/media/animations/neon-wave-surfer.tgs create mode 100644 public/media/animations/origami-enchantment.tgs create mode 100644 public/media/animations/overwhelmed-hat.tgs create mode 100644 public/media/animations/paints-the-world.tgs create mode 100644 public/media/animations/portal-alchemy.tgs create mode 100644 public/media/animations/psychedelic-heart.tgs create mode 100644 public/media/animations/psychedelic-vortex.tgs create mode 100644 public/media/animations/rainbow-rain.tgs create mode 100644 public/media/animations/reality-zipper.tgs create mode 100644 public/media/animations/signature-spirit.tgs create mode 100644 public/media/animations/silver-lunar-eclipse.tgs create mode 100644 public/media/animations/spaceship-flight.tgs create mode 100644 public/media/animations/thunder-catcher.tgs create mode 100644 public/media/hero/manifest.json create mode 100644 public/media/hero/portal-dark.mp4 create mode 100644 public/media/hero/portal-light.mp4 create mode 100644 public/media/licenses/FEATHER-LICENSE.txt create mode 100644 public/media/licenses/FLAGS-NOTICES.txt create mode 100644 public/media/licenses/LOTTIE-LICENSE.txt create mode 100644 public/media/licenses/PAPERMOD-LICENSE.txt create mode 100644 public/media/posters/arcane-aura.webp create mode 100644 public/media/posters/arcane-coder.webp create mode 100644 public/media/posters/aurora-ribbon-wings.webp create mode 100644 public/media/posters/bubble-brew.webp create mode 100644 public/media/posters/carnival-hat-burst.webp create mode 100644 public/media/posters/chromatic-liquid-wave.webp create mode 100644 public/media/posters/clockwork-spring.webp create mode 100644 public/media/posters/cosmic-fishing.webp create mode 100644 public/media/posters/crystal-forge.webp create mode 100644 public/media/posters/crystal-thaw.webp create mode 100644 public/media/posters/flag-br.webp create mode 100644 public/media/posters/flag-fr.webp create mode 100644 public/media/posters/flag-gb.webp create mode 100644 public/media/posters/flag-il.webp create mode 100644 public/media/posters/flag-jp.webp create mode 100644 public/media/posters/flag-us.webp create mode 100644 public/media/posters/hourglass-twist.webp create mode 100644 public/media/posters/infinity-constellation.webp create mode 100644 public/media/posters/living-spellbook.webp create mode 100644 public/media/posters/magnetic-stars.webp create mode 100644 public/media/posters/midnight-dj.webp create mode 100644 public/media/posters/molten-ember.webp create mode 100644 public/media/posters/moon-hammock.webp create mode 100644 public/media/posters/neon-wave-surfer.webp create mode 100644 public/media/posters/origami-enchantment.webp create mode 100644 public/media/posters/overwhelmed-hat.webp create mode 100644 public/media/posters/paints-the-world.webp create mode 100644 public/media/posters/portal-alchemy.webp create mode 100644 public/media/posters/psychedelic-heart.webp create mode 100644 public/media/posters/psychedelic-vortex.webp create mode 100644 public/media/posters/rainbow-rain.webp create mode 100644 public/media/posters/reality-zipper.webp create mode 100644 public/media/posters/signature-spirit.webp create mode 100644 public/media/posters/silver-lunar-eclipse.webp create mode 100644 public/media/posters/spaceship-flight.webp create mode 100644 public/media/posters/thunder-catcher.webp create mode 100644 public/media/previews/arcane-aura.webp create mode 100644 public/media/previews/arcane-coder.webp create mode 100644 public/media/previews/aurora-ribbon-wings.webp create mode 100644 public/media/previews/bubble-brew.webp create mode 100644 public/media/previews/carnival-hat-burst.webp create mode 100644 public/media/previews/chromatic-liquid-wave.webp create mode 100644 public/media/previews/clockwork-spring.webp create mode 100644 public/media/previews/cosmic-fishing.webp create mode 100644 public/media/previews/crystal-forge.webp create mode 100644 public/media/previews/crystal-thaw.webp create mode 100644 public/media/previews/flag-br.webp create mode 100644 public/media/previews/flag-fr.webp create mode 100644 public/media/previews/flag-gb.webp create mode 100644 public/media/previews/flag-il.webp create mode 100644 public/media/previews/flag-jp.webp create mode 100644 public/media/previews/flag-us.webp create mode 100644 public/media/previews/hourglass-twist.webp create mode 100644 public/media/previews/infinity-constellation.webp create mode 100644 public/media/previews/living-spellbook.webp create mode 100644 public/media/previews/magnetic-stars.webp create mode 100644 public/media/previews/midnight-dj.webp create mode 100644 public/media/previews/molten-ember.webp create mode 100644 public/media/previews/moon-hammock.webp create mode 100644 public/media/previews/neon-wave-surfer.webp create mode 100644 public/media/previews/origami-enchantment.webp create mode 100644 public/media/previews/overwhelmed-hat.webp create mode 100644 public/media/previews/paints-the-world.webp create mode 100644 public/media/previews/portal-alchemy.webp create mode 100644 public/media/previews/psychedelic-heart.webp create mode 100644 public/media/previews/psychedelic-vortex.webp create mode 100644 public/media/previews/rainbow-rain.webp create mode 100644 public/media/previews/reality-zipper.webp create mode 100644 public/media/previews/signature-spirit.webp create mode 100644 public/media/previews/silver-lunar-eclipse.webp create mode 100644 public/media/previews/spaceship-flight.webp create mode 100644 public/media/previews/thunder-catcher.webp create mode 100644 public/media/profile/avatar.jpg create mode 100644 public/media/profile/fallback.svg create mode 100644 public/media/profile/github.jpg create mode 100644 public/media/profile/poster.jpg create mode 100644 public/media/profile/profile.json create mode 100644 public/robots.txt create mode 100644 public/social-preview.png create mode 100644 scripts/check-build.mjs create mode 100644 scripts/check.mjs create mode 100644 scripts/export-hero-video.mjs create mode 100644 scripts/generate-brand.mjs create mode 100644 scripts/generate-previews.mjs create mode 100644 scripts/interactions.mjs create mode 100644 scripts/lighthouse.mjs create mode 100644 scripts/preview.mjs create mode 100644 scripts/qa.mjs create mode 100644 services/feedback/.env.example create mode 100644 services/feedback/.gitignore create mode 100644 services/feedback/package-lock.json create mode 100644 services/feedback/package.json create mode 100644 services/feedback/scripts/mock-server.mjs create mode 100644 services/feedback/scripts/verify-chat.mjs create mode 100644 services/feedback/test/mock-server.test.mjs create mode 100644 services/feedback/test/worker.test.mjs create mode 100644 services/feedback/worker.mjs create mode 100644 services/feedback/wrangler.jsonc create mode 100644 services/posts-sync/.gitignore create mode 100644 services/posts-sync/deploy/run_service.py create mode 100644 services/posts-sync/deploy/wizardloop-build.service create mode 100644 services/posts-sync/deploy/wizardloop-build.timer create mode 100644 services/posts-sync/deploy/wizardloop-posts.service create mode 100644 services/posts-sync/deploy/wizardloop-posts.timer create mode 100644 services/posts-sync/deploy/wizardloop-profile.service create mode 100644 services/posts-sync/fixtures/updates.json create mode 100644 services/posts-sync/posts_sync.py create mode 100644 services/posts-sync/test_deploy.py create mode 100644 services/posts-sync/test_posts_sync.py create mode 100644 services/profile-sync/.gitignore create mode 100644 services/profile-sync/fixtures/active-still.json create mode 100644 services/profile-sync/profile_sync.py create mode 100644 services/profile-sync/requirements.txt create mode 100644 services/profile-sync/test_profile_sync.py create mode 100644 src/components/HeroArt.astro create mode 100644 src/components/Icon.astro create mode 100644 src/components/MotionArt.astro create mode 100644 src/config/site.ts create mode 100644 src/content/artworks.json create mode 100644 src/content/flags.json create mode 100644 src/content/posts.json create mode 100644 src/content/projects.json create mode 100644 src/env.d.ts create mode 100644 src/layouts/Base.astro create mode 100644 src/pages/404.astro create mode 100644 src/pages/about.astro create mode 100644 src/pages/accessibility.astro create mode 100644 src/pages/community.astro create mode 100644 src/pages/cookies.astro create mode 100644 src/pages/feedback.astro create mode 100644 src/pages/index.astro create mode 100644 src/pages/posts.astro create mode 100644 src/pages/privacy.astro create mode 100644 src/pages/projects.astro create mode 100644 src/pages/security.astro create mode 100644 src/pages/work.astro create mode 100644 src/scripts/feedback.ts create mode 100644 src/scripts/hero.ts create mode 100644 src/scripts/motion.ts create mode 100644 src/scripts/posts.ts create mode 100644 src/scripts/site.ts create mode 100644 src/styles/global.css create mode 100644 src/types/lottie-canvas.d.ts create mode 100644 tests/site.spec.ts create mode 100644 tsconfig.json diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..de91110 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,12 @@ +root = true + +[*] +charset = utf-8 +end_of_line = lf +insert_final_newline = true +indent_style = space +indent_size = 2 +trim_trailing_whitespace = true + +[*.py] +indent_size = 4 diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..6271a7a --- /dev/null +++ b/.env.example @@ -0,0 +1,8 @@ +# Public configuration only. Never prefix secrets with PUBLIC_. +# Leave blank until the separate feedback Worker is configured and approved. +PUBLIC_FEEDBACK_API_URL= +PUBLIC_TURNSTILE_SITE_KEY= +# Local development automatically uses a clearly labeled loopback mock. +# Set true only for a non-indexable build used for review. +PUBLIC_PREVIEW=false +# Telegram/Cloudflare credentials belong to their separate services, never here. diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..40b9dc4 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,11 @@ +## What changes for visitors + +Describe the visible behavior and the reason for it. Link representative desktop/mobile screenshots when the interface changes. + +## Validation + +List commands actually run, measured results, and remaining limitations. Distinguish fixtures from authenticated service tests. + +## Deployment and privacy + +State whether credentials, content synchronization, third-party requests, public URLs or hosting settings are affected. Keep secrets and personal feedback out of this description. Merging or deploying requires the owner's explicit approval. diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..12b19a0 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,37 @@ +name: Quality checks +on: + pull_request: + workflow_dispatch: +permissions: + contents: read +env: + ASTRO_TELEMETRY_DISABLED: 1 +jobs: + check: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 24 + cache: npm + - run: npm ci + - run: npm run check + - run: npx playwright install --with-deps chromium firefox webkit + - run: npm run test:browsers + - uses: actions/upload-artifact@v4 + if: always() + with: + name: browser-quality-results + path: artifacts/final/ + retention-days: 7 + profile-sync-tests: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: '3.13' + - run: python -m pip install -r services/profile-sync/requirements.txt + - run: python -m unittest discover -s services/profile-sync -p "test_*.py" -v + - run: python -m unittest discover -s services/posts-sync -p "test_*.py" -v diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml new file mode 100644 index 0000000..f8612e8 --- /dev/null +++ b/.github/workflows/deploy.yml @@ -0,0 +1,46 @@ +name: Approved Pages deployment +on: + workflow_dispatch: + inputs: + approval: + description: 'Type DEPLOY_WIZARDLOOP only after explicit owner approval' + required: true + type: string +permissions: + contents: read +concurrency: + group: pages-production + cancel-in-progress: false +env: + ASTRO_TELEMETRY_DISABLED: 1 +jobs: + build: + if: github.ref == 'refs/heads/main' && inputs.approval == 'DEPLOY_WIZARDLOOP' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 24 + cache: npm + - run: npm ci + - run: npm run check + env: + PUBLIC_FEEDBACK_API_URL: ${{ vars.PUBLIC_FEEDBACK_API_URL }} + PUBLIC_TURNSTILE_SITE_KEY: ${{ vars.PUBLIC_TURNSTILE_SITE_KEY }} + - uses: actions/upload-pages-artifact@v3 + with: + path: dist + deploy: + needs: build + runs-on: ubuntu-latest + permissions: + pages: write + id-token: write + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + steps: + - name: Deploy approved static artifact + id: deployment + uses: actions/deploy-pages@v4 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..caca028 --- /dev/null +++ b/.gitignore @@ -0,0 +1,19 @@ +node_modules/ +.env +.env.* +!.env.example +*.session +*.session-journal +artifacts/ +.preview.log +.preview.err +dist/ +.astro/ +test-results/ +playwright-report/ +.wrangler/ +.dev.vars +__pycache__/ +.venv/ +*.pyc +*.log diff --git a/.prettierrc.json b/.prettierrc.json new file mode 100644 index 0000000..5a044a0 --- /dev/null +++ b/.prettierrc.json @@ -0,0 +1,12 @@ +{ + "singleQuote": true, + "plugins": ["prettier-plugin-astro"], + "overrides": [ + { + "files": "*.astro", + "options": { + "parser": "astro" + } + } + ] +} diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..107e97f --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,30 @@ +# Changelog + +This file records repository changes. A heading does not mean a Git tag, GitHub Release or production deployment exists. + +## 1.0.0-rc.1 - Unreleased + +### Website + +- Rebuild the personal portfolio with Astro static output and the selected Spectrum Circuit direction, developed with comic framing and the owner's wizard illustrations. +- Add Home, About, Selected Work, Projects, Posts, Community, Feedback, Privacy, Cookies, Security, Accessibility and a custom 404 page. +- Add responsive navigation, light and dark themes, persistent preferences, keyboard controls, motion pause and reduced-motion behavior. +- Return the hero to a still preview when a native decoder stops unexpectedly, while preserving intentional user and visibility pauses. +- Include 30 original wizard animations and six flag samples, unchanged source TGS files, transparent posters and provenance manifests. +- Add page-specific metadata, structured data, static social artwork, favicons, sitemap output and domain-preserving builds. + +### Services and tooling + +- Add an isolated profile synchronization service with active-photo identity matching, video/still selection, cache, fallback and offline fixtures. +- Add consent-gated Telegram posts and a separately configured supported-update ingestion path. Original Telegram links remain available when embedded media is incomplete. +- Add a feedback Worker and local mock, server validation, Turnstile verification, private-chat setup and provider-mocked tests. +- Add source/build checks, browser coverage, accessibility checks, quality CI and an approval-gated manual Pages workflow. +- Document setup, content editing, service limitations, asset rights, contribution, security reporting and release/rollback procedures. + +### Review status + +This is an unreleased review candidate. Measured browser, accessibility, performance and offline-service results are documented in [the quality review](docs/QA.md). Authenticated Telegram/profile media tests, live feedback delivery, native Safari and human screen-reader review, production validation, merge approval and release approval remain separate steps. Complete legal/accessibility conformance is not asserted. + +## Earlier baseline + +The original four-file website remains in Git history at `658ed667c173f861cd16e6048126de12ec3648b9` from 2025-07-20. The initial audit found no Git tags or GitHub Releases. Do not assign a historical semantic version retrospectively. Phase 1 tooling used `0.1.0-preview.1` while three local concepts were evaluated; it was not a production release. diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..6c7ddd2 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,13 @@ +# Code of Conduct + +This is a personal open-source project. Everyone participating in its issues, pull requests and discussions should be able to contribute without harassment. + +Be respectful and specific. Discuss the work and its effects, explain disagreements, welcome questions and credit contributions. Respect privacy, accessibility needs and different backgrounds or levels of experience. + +Harassment, discriminatory abuse, threats, sexualized comments, impersonation, deliberate disruption and publishing another person's private information are not acceptable. Do not use project spaces for spam, unwanted solicitation or attempts to obtain credentials. + +Report a concern privately to WizardLoop through [Telegram](https://t.me/wizardloop) or the published email, [wizard@wizardloop.com](mailto:wizard@wizardloop.com). Share only what is needed to understand the incident. Do not publish private evidence in an issue or circulate it unnecessarily. + +The maintainer may request changes, remove inappropriate material or restrict participation to protect the project. The response should consider the conduct, its impact and whether it continued after a request to stop. No fixed response time or confidential reporting platform is promised by this document. + +These expectations apply to this repository and project spaces managed by its maintainer, and when someone explicitly represents the project elsewhere. This document does not claim authority over unrelated communities. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..886060d --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,36 @@ +# Contributing to WizardLoop + +This repository is the source of one creator's personal website. Contributions should improve that website while preserving the owner's identity, verified content and control over publication. + +For a small correction, open a focused pull request. Discuss substantial visual changes, new services or dependencies in an issue first. Never disclose credentials, personal submissions or a vulnerability in a public issue or pull request; use [SECURITY.md](SECURITY.md). + +## Working locally + +1. Read the [README](README.md), relevant service guide and [THIRD_PARTY_NOTICES.md](THIRD_PARTY_NOTICES.md). +2. Use a dedicated branch or fork. Do not work directly on `main` or alter another repository as part of this project. +3. Install locked dependencies with `npm ci` using Node 22.23.4 or newer. +4. Run `npm run dev`. The frontend and optional `npm run dev:feedback` mock require no live credentials. +5. Make the smallest coherent change. Keep content, styles, browser interactions and private integrations in their existing locations. + +Do not change `wizardloop.com`, either CNAME file, production settings or deployment behavior as incidental cleanup. Such changes need a specific proposal and owner approval. + +## Content, design and accessibility + +- Use English by default and preserve correct direction handling for Hebrew content. +- Verify project links and factual claims. Do not invent clients, achievements, personal details or social profiles. +- Use a regular hyphen instead of the em dash character U+2014 in controlled content, metadata and documentation. +- Keep keyboard access, visible focus, semantic headings, readable contrast and small-screen layouts. Honor reduced motion and the global motion control. +- Keep useful static content when JavaScript or a third party fails. Avoid heavy dependencies for effects that local CSS or existing components can provide. +- Preserve media attribution. MIT code contributions do not relicense the owner's artwork or third-party assets. Record the source and license of any new font, icon, media or dependency. + +## Validation and pull requests + +Run `npm run check`. For layout or interaction changes, build the site and run relevant Playwright checks, including `npm run test:browsers` when browser behavior changes. Follow service-specific tests for profile, posts or feedback changes. Do not write redundant tests for a simple text correction; existing content/build checks still apply. + +A useful pull request describes the concrete problem, resulting behavior, validation and remaining limits. Include desktop/mobile screenshots for visual changes and both themes where relevant. Identify mocks and fixtures explicitly. Do not report an external integration as verified unless it was exercised with authorized credentials. + +Keep lockfile changes with dependency changes. Do not commit `node_modules`, build output, test artifacts, `.env`, `.dev.vars`, sessions, private chat IDs, submitted messages or tokens. Check the diff for accidentally included local files before submitting. + +Owner review governs design and content acceptance. A pull request does not authorize merging, deploying, enabling services, creating releases or changing DNS. Follow [DEPLOYMENT.md](docs/DEPLOYMENT.md) for those separate operations. + +By submitting new source code, you offer it under the repository's MIT license. Only submit material you have the right to contribute, and identify any different asset license. Participation is subject to the [Code of Conduct](CODE_OF_CONDUCT.md). diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..afe447e --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 WizardLoop + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md index 5a7864f..4eaa444 100644 --- a/README.md +++ b/README.md @@ -1 +1,149 @@ -https://wizardloop.com +# WizardLoop + +The source of [wizardloop.com](https://wizardloop.com), the personal website of WizardLoop: independent design, illustration, animation, software, Telegram bots and open source. + +The rebuild develops the selected Spectrum Circuit concept into a comic-inspired portfolio, with original wizard illustrations, expressive vector loops and a light paper palette alongside a complete dark theme. Astro generates the static site; small browser scripts handle interaction, and separate optional services handle private integrations. + +**Status: `1.0.0-rc.1`, unreleased.** This branch is for implementation and review. Fixtures and mocked tests do not establish that production integrations work. Authenticated Telegram profile/video retrieval, live feedback delivery and production deployment need separate configuration, verification and owner approval. No release or deployment is implied by the package version. + +## Run locally + +Use Node.js **22.23.4 or newer**, with npm. Repository workflows use Node 24. Python 3.11 or newer is needed only for the optional synchronization services and their tests. + +```sh +git clone --branch design/three-local-concepts https://github.com/WizardLoop/wizardloop.github.io.git +cd wizardloop.github.io +npm ci +npm run dev +``` + +Open [http://127.0.0.1:4322](http://127.0.0.1:4322). No credentials are needed to view the website. This unreleased review lives on `design/three-local-concepts`; the default branch still contains the earlier site. For an existing checkout of the review branch, skip cloning. Work on a dedicated development branch rather than `main`. + +To exercise feedback locally, run this in a second terminal: + +```sh +npm run dev:feedback +``` + +The loopback mock listens on port 8787. The development form identifies it as a local test and sends nothing to Telegram. See [feedback setup](docs/FEEDBACK-SETUP.md) for its validation rules and limitations. + +Build and inspect the static result: + +```sh +npm run build +npm run preview -- --host 127.0.0.1 --port 4322 +``` + +Stop the development server before starting the preview on the same port. `dist/` contains the publishable HTML, browser assets, domain file and social metadata. Neither command deploys anything. + +## Configuration and content + +`.env.example` contains public frontend options only. Copy it to a local `.env` if configuration is needed. Keep the feedback endpoint and Turnstile sitekey empty until the separate service is approved and verified. A `PUBLIC_` prefix means the value can be included in browser files; it must never contain a secret. + +| Edit | Location | +| --- | --- | +| Name, verified social URLs, email and navigation | `src/config/site.ts` | +| Page copy and route-specific metadata | `src/pages/*.astro` | +| Verified software projects | `src/content/projects.json` | +| Wizard artwork titles, descriptions and provenance | `src/content/artworks.json` | +| Curated flag animations and attribution | `src/content/flags.json` | +| Public channel post index | `src/content/posts.json` | +| Public profile snapshot and media | `public/media/profile/` | +| Colors, typography and responsive layouts | `src/styles/global.css` | +| Common header, footer and metadata | `src/layouts/Base.astro` | +| Reusable icons, art and content components | `src/components/` | +| Browser preferences, embeds and motion | `src/scripts/` | + +Keep claims factual. Link to verified projects and describe the actual role. The wizard scenes are portfolio artwork, not software screenshots or evidence of client relationships. Record provenance and rights for new assets, write meaningful alternative text and preserve image dimensions. + +Controlled content must not contain the em dash character U+2014. Use a regular hyphen instead; source checks enforce this rule. Do not introduce unverified X, LinkedIn or organization identities into metadata. + +## Architecture + +- Astro generates complete HTML for Home, About, Work, Projects, Posts, Community, Feedback, Privacy, Cookies, Security, Accessibility and a custom 404. Directory output supports direct requests and refreshes on GitHub Pages. +- Local CSS and fonts provide the visual system. No application framework, CMS, database or general-purpose backend is required for the portfolio. +- The Home hero uses a native 768 x 768 H.264 MP4 loop derived from the supplied `portal-alchemy` artwork, with separate light and dark versions and the original 2.4-second duration. Other original TGS artwork loads through one shared light Canvas Lottie player. At most three visible Canvas scenes play, drawn at up to 24 frames per second while elapsed-time sampling preserves each original animation's duration. Motion controls, reduced-motion preferences and viewport/document visibility limit both playback paths. +- Static WebP delivery copies in `public/media/previews/` provide the initial and no-animation view. The build creates these quality-94 copies from unchanged lossless masters in `public/media/posters/`, preserving dimensions and the alpha channel. Original TGS sources also remain unchanged. +- Official Telegram embeds load only after visitor consent. Original post links remain available without embedding. Telegram controls embedded media support; custom emoji, stickers and animation fidelity are not guaranteed by the website. +- Profile data is read at build time from a public snapshot. Private MTProto work happens in the separate sync service, never in the browser or Pages output. +- Feedback uses an optional Cloudflare Worker with server-side validation and Turnstile verification. GitHub Pages serves only the frontend. + +The build copies `public/CNAME` to `dist/CNAME`, preserving `wizardloop.com`. The original root `CNAME`, `index.html` and PaperMod-attributed stylesheet remain as historical source. Astro builds from `src/`; the legacy HTML is not its homepage input. No repository-name base path is added to this custom-domain site. + +## Integration status and setup + +| Integration | Current review boundary | Instructions | +| --- | --- | --- | +| Telegram profile | Public snapshot and local fallbacks available; authenticated active-primary/video access unverified | [Profile sync](docs/PROFILE-SYNC.md) | +| Telegram posts | Verified post index and consent-gated embeds; supported-update ingestion requires separate configuration, with no live schedule enabled | [Posts sync](docs/POSTS-SYNC.md) | +| Feedback | Local mock and provider-mocked tests; no public Worker or real private-message delivery claimed | [Feedback setup](docs/FEEDBACK-SETUP.md) | +| Brand derivatives | Local profile and original artwork generate static icons and a 1200 x 630 composition | `scripts/generate-brand.mjs` | + +Run `node scripts/generate-brand.mjs` after an approved profile change; `npm run build` also runs it. The script updates the avatar badge and favicons while preserving illustration sources. Its native social-card typography uses installed Arial or a sans fallback, can vary by operating system, and does not redistribute system font files. The build also runs `scripts/generate-previews.mjs` to regenerate static delivery copies from the retained poster masters. + +Read each service's status fields and setup guide. A usable cached output is not proof of a successful Telegram connection. No credentials, private chat IDs or sessions belong in the public snapshot. Research and external limitations are documented in [integration feasibility](docs/INTEGRATION-FEASIBILITY.md). + +## Quality checks + +Read the [measured website quality review](docs/QA.md) and [integration verification](docs/QA-INTEGRATIONS.md) for observed results, preview images and outstanding acceptance limits. + +```sh +npm run check +npx playwright install chromium firefox webkit +npm run test:browsers +``` + +On Linux CI, install system dependencies with `npx playwright install --with-deps chromium firefox webkit`. Stop any development server on port 4322 before acceptance testing so Playwright starts the built preview. Browser tests require a successful build; `npm run check` includes one. + +| Command | Scope | +| --- | --- | +| `npm run format` | Format the final site's Astro, CSS and TypeScript source plus its preference/config scripts | +| `npm run format:check` | Check that the same scoped source follows the pinned Prettier configuration | +| `npm run lint` | Formatting, source punctuation, basic secret patterns, JavaScript syntax and project invariants | +| `npm run typecheck` | Astro and TypeScript diagnostics | +| `npm run test:unit` | Feedback service and local mock tests | +| `npm run build` | Static preview and brand derivatives, static generation, HTML/metadata/assets/link checks | +| `npm run check` | Lint, typecheck, feedback unit tests and build | +| `npm test` | Chromium browser suite | +| `npm run test:browsers` | Chromium suite plus selected Firefox and WebKit smoke/interaction coverage | +| `npm run audit:performance` | Lighthouse on Home, Work, Posts and Feedback at the running built preview on port 4322 | + +Browser coverage includes 320, 360, 390, 768, 1024 and 1440px layouts, both themes, navigation, motion, profile fallback, consent and feedback. Reports, screenshots and traces are written under ignored `artifacts/final/`. Test coverage is not a claim that every browser, assistive technology or legal accessibility requirement has been verified. + +The profile service uses pinned Python dependencies in an isolated environment. After following [PROFILE-SYNC.md](docs/PROFILE-SYNC.md), run: + +```sh +python -m unittest discover -s services/profile-sync -p 'test_*.py' -v +``` + +The posts updater uses Python's standard library and needs no package installation: + +```sh +python -m unittest discover -s services/posts-sync -p 'test_*.py' +``` + +Its offline fixture and live configuration are documented in [POSTS-SYNC.md](docs/POSTS-SYNC.md). Identify mocks and fixtures in reports. For performance auditing, keep the built preview running on port 4322, then run `npm run audit:performance` in another terminal. Reports are written to `artifacts/lighthouse/`; set `CHROME_PATH` when Chrome cannot be discovered. Review measured results and outstanding issues before approval. This README does not assert a passing final score or complete QA. + +## Local design studies + +The three original concepts remain available for comparison: + +```sh +npm run dev:concepts +``` + +Open `http://127.0.0.1:4321/concept-a/`, `/concept-b/` or `/concept-c/`. These non-indexable local studies are separate from the Astro build. `npm run test:concepts` and `npm run test:concept-interactions` exercise that earlier review material. + +## Review, deployment and releases + +Read [DEPLOYMENT.md](docs/DEPLOYMENT.md) before any operational change. Quality CI cannot deploy. The separate Pages workflow is manually triggered, restricted to `main`, and requires a confirmation input. These controls do not replace owner approval. + +Do not merge, alter Pages settings, change DNS, deploy, create a tag or publish a release until the owner approves the corresponding action. A release follows approved deployment and live verification. Repository commands do not automatically activate sync schedules, provision a Worker or configure secrets. + +## License and contact + +New website source code and tooling are [MIT licensed](LICENSE). WizardLoop's name, branding, illustrations, avatar, animation media and personal content are outside that grant. Fonts and third-party assets retain their own licenses. See [THIRD_PARTY_NOTICES.md](THIRD_PARTY_NOTICES.md) and the [asset inventory](docs/DESIGN-ASSET-INVENTORY.md). + +Contributions follow [CONTRIBUTING.md](CONTRIBUTING.md) and the [Code of Conduct](CODE_OF_CONDUCT.md). Report vulnerabilities privately using [SECURITY.md](SECURITY.md). + +[Telegram](https://t.me/wizardloop) · [Channel](https://t.me/wizard_loop) · [Community](https://t.me/wizardloopchat) · [GitHub](https://github.com/WizardLoop) diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..506031c --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,29 @@ +# Security + +Report suspected vulnerabilities privately to [wizard@wizardloop.com](mailto:wizard@wizardloop.com) or [WizardLoop on Telegram](https://t.me/wizardloop). These are the contact details published by the owner. Do not create a public issue containing exploit details, credentials, personal feedback or private account identifiers. + +Include the affected page/component, commit/version if known, concise reproduction, expected impact and a safe way to contact you. Redact tokens, cookies, sessions, chat IDs and personal messages. Do not send real credentials as proof. + +## Scope and safe reporting + +Relevant code includes the static site, build pipeline and optional profile, posts and feedback services. Bugs in Telegram, Cloudflare, GitHub or a dependency should also be reported through that provider's process where appropriate. + +Use a local checkout, fixtures and the mock to investigate. This document does not authorize production testing, unsolicited messages, account access, accessing another person's data, sustained traffic or denial-of-service testing. Stop if a test exposes private information and report the minimum necessary evidence privately. + +There is no bug bounty or guaranteed response time. The rebuild is an unreleased candidate, and no support promise is made for a historical version. Fixes should be reviewed on the current development line; deployed-version handling depends on the actual production state. + +## Credential and data boundaries + +- Pages output is public. Never put tokens, API hashes, private chat IDs, sessions or secret keys in `public/`, content, `dist/`, issue attachments or `PUBLIC_` environment variables. +- Keep service credentials in the deployment host's secret store. Personal Telegram sessions are not supported by the profile service. +- Run real service tests only with owner approval. Local feedback uses an explicit mock and sends no Telegram message. +- Keep provider responses and visitor submissions out of CI logs and public artifacts. Use non-sensitive fixtures and generic failure messages. +- Review dependency upgrades and lockfile changes. Pattern checks are guardrails, not a complete secret scan or security certification. + +If a credential is published accidentally, notify the owner privately. The owner should revoke or rotate it at the provider, assess use and remove it from accessible artifacts as appropriate. Deleting a file or commit alone does not invalidate a leaked credential. Production credential or service changes still follow the owner's operational authorization. + +## Hosting limitations + +The site includes a document-level CSP where applicable. GitHub Pages does not provide a general repository-defined response-header configuration. Do not claim that Permissions-Policy, HSTS or header-only CSP directives are set by this source alone. Audit real responses after approved deployment. A separate CDN/proxy requires an approved infrastructure decision. + +See [integration feasibility](docs/INTEGRATION-FEASIBILITY.md), [feedback setup](docs/FEEDBACK-SETUP.md), [profile sync](docs/PROFILE-SYNC.md) and [deployment](docs/DEPLOYMENT.md) for implementation boundaries and remaining live checks. diff --git a/THIRD_PARTY_NOTICES.md b/THIRD_PARTY_NOTICES.md new file mode 100644 index 0000000..1958a0c --- /dev/null +++ b/THIRD_PARTY_NOTICES.md @@ -0,0 +1,43 @@ +# Licenses and media provenance + +The root MIT license applies to newly authored website source code and tooling. It does not grant rights to WizardLoop's name, branding, profile artwork, personal content or third-party assets. Those retain their respective rights. No permission to reuse the profile artwork is implied. + +## Existing code + +The original `assets/css/style.css` remains unchanged and includes its PaperMod attribution: PaperMod v8+, MIT, Copyright (c) 2020 nanxiaobei and adityatelange; Copyright (c) 2021-2025 adityatelange. Its license reference is https://github.com/adityatelange/hugo-PaperMod/blob/master/LICENSE. The concepts do not import or copy this stylesheet. Existing copyright notices must be preserved if that code is reused later. + +## Self-hosted fonts + +The legacy stylesheet is also copied unchanged to `public/assets/css/style.css` to preserve its existing public asset URL after the static migration. The current upstream full MIT permission notice is retained in `public/media/licenses/PAPERMOD-LICENSE.txt`; the older copyright dates in the original stylesheet header remain untouched. The new portfolio does not import the legacy stylesheet. + +Font files are unmodified Latin subsets from these pinned Fontsource variable packages, all under SIL Open Font License 1.1. They are hosted locally and make no font-provider requests in the browser. Original license/copyright notices are included alongside them. + +| Font | Package version | Local license | +| --- | --- | --- | +| Manrope | `@fontsource-variable/manrope@5.3.0` | `concepts/assets/fonts/MANROPE-LICENSE.txt` | +| Cormorant | `@fontsource-variable/cormorant@5.3.0` | `concepts/assets/fonts/CORMORANT-LICENSE.txt` | +| Space Grotesk | `@fontsource-variable/space-grotesk@5.3.0` | `concepts/assets/fonts/SPACE-GROTESK-LICENSE.txt` | + +## Profile and artwork + +`concepts/assets/profile-telegram.jpg` is the public still exposed by https://t.me/wizardloop, downloaded on 2026-10-27 for the owner's requested website. It is 320 x 320 pixels. It is not an authenticated original-resolution primary profile download and is not a claimed video. Provenance and checksum are documented in `docs/INTEGRATION-FEASIBILITY.md`. Keep this image outside the source-code MIT grant. + +The orbital diagrams, journal drawings, circuit sculpture, icons, fallback monogram and project illustrations were authored for these concepts. Project illustrations are clearly labeled concept artwork, not real product screenshots or invented client work. SVG/CSS implementation code is MIT; any resulting WizardLoop brand identity and commissioned visual assets retain the owner's brand/media rights. + +The owner supplied the local design collection for use in this website. The 30 original TGS animations from `WizardLoop_Infinity_Atelier_30_TGS.zip` are copied unchanged into `public/media/animations/`. Their titles, original archive members, byte sizes and SHA-256 hashes are recorded in `src/content/artworks.json`. Corresponding transparent WebP posters in `public/media/posters/` are lossless raster renders of those vector assets at 35% of the animation duration. These WizardLoop illustrations and derived posters are reserved media, outside the website source-code MIT license. No additional license grant was found inside the source archive. + +Six flag animations from `WizardLoop_Country_Flags_001_050.zip` are included: Israel, United States, United Kingdom, France, Japan and Brazil. `src/content/flags.json` records their exact source members and checksums. The supplied flag-source notice is preserved in full in `public/media/licenses/FLAGS-NOTICES.txt`; it lists the larger original collection as well as the six used here. The source collection identifies the underlying flag designs as public domain, with country-flags/Wikimedia attribution. The animation treatment and posters are not covered by the code MIT grant. National-symbol usage restrictions can apply independently of copyright. No third-party app logos or Belize flag artwork are included in this selection. + +The Home hero uses two native 768 x 768 H.264 MP4 derivatives of `portal-alchemy`, one for each theme, retaining the original 2.4-second loop duration. Other original TGS media use the locally bundled light Canvas build of `lottie-web`, version 5.13.0, under MIT, Copyright (c) 2015 Bodymovin. At most three visible Canvas scenes play, with drawing capped at 24 frames per second while preserving the source loop duration. The player's original license is preserved at `public/media/licenses/LOTTIE-LICENSE.txt` and remains applicable to bundled player code. These animations contain no expression scripts or references to external images. + +WebP assets are static previews. The quality-94 delivery copies in `public/media/previews/` retain the masters' dimensions and alpha channel; the lossless masters in `public/media/posters/` and original TGS sources are unchanged. The MP4 and WebP derivatives inherit the underlying WizardLoop media rights and are outside the source-code MIT grant. Full provenance, rendering checks and source-license links are documented in `docs/DESIGN-ASSET-INVENTORY.md`. + +## Interface icons and service dependencies + +The local interface icon set adapts familiar Feather geometry for GitHub, mail and common controls, with custom WizardLoop symbols and Telegram/community treatments. Feather is MIT licensed, Copyright (c) 2013-2023 Cole Bemis. Its full notice is preserved in `public/media/licenses/FEATHER-LICENSE.txt`; source: https://github.com/feathericons/feather/blob/main/LICENSE. No icon CDN is loaded. Social marks identify their respective services and do not imply endorsement. + +The final site's self-hosted font notices are also present in `public/fonts/`. The separate profile synchronizer uses Telethon and pyaes (MIT), Pillow (MIT-CMU), rsa (Apache-2.0) and pyasn1 (BSD-2-Clause). Their pinned distributions include their original license files. These Python dependencies run separately and are not distributed in the static website build. + +## Development tooling + +Playwright, axe-core, Lighthouse, Prettier and the Prettier Astro plugin are development-only dependencies. Their upstream licenses remain in the installed packages. They are not served to website visitors or included as production scripts. `package-lock.json` records exact dependency versions. Lottie is a runtime dependency because its Canvas player is bundled into the site's lazy-loaded animation module. diff --git a/astro.config.mjs b/astro.config.mjs new file mode 100644 index 0000000..ce0ffa4 --- /dev/null +++ b/astro.config.mjs @@ -0,0 +1,14 @@ +import { defineConfig } from 'astro/config'; +import sitemap from '@astrojs/sitemap'; + +export default defineConfig({ + site: 'https://wizardloop.com', + output: 'static', + trailingSlash: 'always', + integrations: [ + sitemap({ filter: (page) => !/\/404(?:\.html|\/)?$/.test(page) }), + ], + server: { host: '127.0.0.1', port: 4322 }, + build: { format: 'directory' }, + vite: { build: { sourcemap: false } }, +}); diff --git a/concepts/assets/base.css b/concepts/assets/base.css new file mode 100644 index 0000000..34d4d6e --- /dev/null +++ b/concepts/assets/base.css @@ -0,0 +1,7 @@ +@font-face{font-family:Studio;src:url('/assets/fonts/manrope-latin.woff2') format('woff2');font-weight:200 800;font-style:normal;font-display:swap} +@font-face{font-family:Journal;src:url('/assets/fonts/cormorant-latin.woff2') format('woff2');font-weight:300 700;font-style:normal;font-display:swap} +@font-face{font-family:Journal;src:url('/assets/fonts/cormorant-italic-latin.woff2') format('woff2');font-weight:300 700;font-style:italic;font-display:swap} +@font-face{font-family:Circuit;src:url('/assets/fonts/spacegrotesk-latin.woff2') format('woff2');font-weight:300 700;font-style:normal;font-display:swap} +*{box-sizing:border-box}html{scroll-behavior:smooth;scroll-padding-top:110px}body{margin:0;font-family:Studio,Arial,sans-serif;line-height:1.6;-webkit-font-smoothing:antialiased}button,input,textarea,select{font:inherit}button,a{-webkit-tap-highlight-color:transparent}button{cursor:pointer}a{color:inherit}img,svg{max-width:100%}img{display:block}button,a{touch-action:manipulation}button:disabled{cursor:default}a:focus-visible,button:focus-visible,input:focus-visible,summary:focus-visible,[tabindex]:focus-visible{outline:3px solid var(--focus,var(--accent,#7c66d7));outline-offset:5px}::selection{background:#afa0e8;color:#171428}[hidden]{display:none!important}.sr-only{position:absolute;width:1px;height:1px;overflow:hidden;clip-path:inset(50%);white-space:nowrap}.skip-link{position:fixed;z-index:2000;left:20px;top:-100px;padding:12px 22px;background:#fff;color:#19152c;border:2px solid currentColor}.skip-link:focus{top:16px}.menu-toggle{display:none}#mobile-nav{position:fixed;z-index:1000;inset:12px;background:var(--bg,#13101f);color:var(--ink,var(--text,#f4f0fb));padding:28px;overflow:auto;border:1px solid var(--line,var(--border,#645e77));border-radius:10px;box-shadow:0 0 0 25px #1118}#mobile-nav a{display:block;font-size:clamp(24px,7vw,38px);padding:8px 0;text-decoration:none}#mobile-nav button[data-menu-close]{min-width:48px;min-height:48px;float:right;color:inherit;border:1px solid currentColor;background:none;padding:8px 12px}#mobile-nav .menu-caption{clear:both;padding-top:32px;font-size:12px;text-transform:uppercase;letter-spacing:.15em}html[data-motion=paused] *,html[data-motion=paused] *::before,html[data-motion=paused] *::after{animation-play-state:paused!important;scroll-behavior:auto!important}html[data-motion=paused] .pointer-art{transform:none!important}.profile-source{font-size:11px;line-height:1.5}footer{padding-bottom:max(28px,env(safe-area-inset-bottom))}.preview-crosslinks{display:flex;flex-wrap:wrap;gap:20px}.preview-crosslinks a{font-size:12px;min-height:44px;display:inline-flex;align-items:center}.mobile-socials{display:flex;gap:16px;flex-wrap:wrap}#mobile-nav .mobile-socials a{font-size:15px}section{scroll-margin-top:110px} +@media(max-width:800px){.desktop-nav{display:none!important}.menu-toggle{display:inline-flex;align-items:center;justify-content:center;min-width:44px;min-height:44px}.site-header{max-width:calc(100% - 24px)}} +@media(prefers-reduced-motion:reduce){*,*::before,*::after{animation:none!important;transition:none!important;scroll-behavior:auto!important}.pointer-art{transform:none!important}} diff --git a/concepts/assets/common.js b/concepts/assets/common.js new file mode 100644 index 0000000..a5d4888 --- /dev/null +++ b/concepts/assets/common.js @@ -0,0 +1,80 @@ +(() => { + const root = document.documentElement; + const key = `wizardloop-concept-${root.dataset.concept || 'index'}`; + const media = matchMedia('(prefers-reduced-motion: reduce)'); + const storage = { get(k) { try { return localStorage.getItem(k); } catch { return null; } }, set(k, v) { try { localStorage.setItem(k, v); } catch { /* Preferences remain usable without storage. */ } } }; + const saved = storage.get(`${key}-theme`); + const requested = new URLSearchParams(location.search).get('theme'); + root.dataset.theme = ['dark', 'light'].includes(requested) ? requested : ['dark', 'light'].includes(saved) ? saved : root.dataset.theme || 'dark'; + root.dataset.motion = media.matches || storage.get('wizardloop-motion') === 'paused' ? 'paused' : 'running'; + const theme = document.querySelector('[data-theme-toggle]'); + const motion = document.querySelector('[data-motion-toggle]'); + function sync() { + const nextTheme = root.dataset.theme === 'dark' ? 'light' : 'dark'; + const paused = root.dataset.motion === 'paused'; + theme?.setAttribute('aria-label', `Switch to ${nextTheme} mode`); + theme?.setAttribute('title', `Switch to ${nextTheme} mode`); + theme?.querySelector('[data-theme-label]')?.replaceChildren(nextTheme === 'light' ? 'Light mode' : 'Dark mode'); + motion?.setAttribute('aria-label', paused ? 'Resume motion' : 'Pause motion'); + motion?.setAttribute('aria-pressed', String(paused)); + motion?.querySelector('[data-motion-label]')?.replaceChildren(paused ? 'Resume motion' : 'Pause motion'); + document.querySelectorAll('video').forEach(video => { if (paused) video.pause(); else video.play().catch(() => {}); }); + } + theme?.addEventListener('click', () => { root.dataset.theme = root.dataset.theme === 'dark' ? 'light' : 'dark'; storage.set(`${key}-theme`, root.dataset.theme); sync(); }); + motion?.addEventListener('click', () => { root.dataset.motion = root.dataset.motion === 'paused' ? 'running' : 'paused'; storage.set('wizardloop-motion', root.dataset.motion); sync(); }); + media.addEventListener('change', () => { if (media.matches) { root.dataset.motion = 'paused'; sync(); } }); + sync(); + + const trigger = document.querySelector('[data-menu-toggle]'); + const menu = document.getElementById('mobile-nav'); + let priorFocus, priorOverflow; + const inerted = []; + function close() { + if (!menu || menu.hidden) return; + menu.hidden = true; + trigger?.setAttribute('aria-expanded', 'false'); + document.body.style.overflow = priorOverflow || ''; + inerted.splice(0).forEach(el => { el.inert = false; }); + priorFocus?.focus(); + } + function open() { + if (!menu) return; + priorFocus = trigger || document.activeElement; + priorOverflow = document.body.style.overflow; + menu.hidden = false; + menu.setAttribute('role', 'dialog'); menu.setAttribute('aria-modal', 'true'); + if (!menu.hasAttribute('aria-label')) menu.setAttribute('aria-label', 'Site navigation'); + trigger?.setAttribute('aria-expanded', 'true'); + document.body.style.overflow = 'hidden'; + document.querySelectorAll('main,footer').forEach(el => { if (!el.contains(menu) && !el.inert) { el.inert = true; inerted.push(el); } }); + menu.querySelector('button,a')?.focus(); + } + trigger?.addEventListener('click', () => menu?.hidden ? open() : close()); + menu?.querySelectorAll('a,[data-menu-close]').forEach(el => el.addEventListener('click', close)); + document.addEventListener('keydown', event => { + if (!menu || menu.hidden) return; + if (event.key === 'Escape') { event.preventDefault(); close(); } + if (event.key === 'Tab') { + const items = [...menu.querySelectorAll('a,button,input,[tabindex="0"]')].filter(el => !el.disabled && el.getClientRects().length); + event.preventDefault(); + const index = items.indexOf(document.activeElement); + const next = event.shiftKey ? (index <= 0 ? items.length - 1 : index - 1) : (index + 1) % items.length; + items[next]?.focus(); + } + }); + matchMedia('(min-width:801px)').addEventListener('change', event => { if (event.matches) close(); }); + document.querySelectorAll('img[data-profile]').forEach(img => { + const fallback = () => { if (!img.src.endsWith('profile-fallback.svg')) { img.src = '/assets/profile-fallback.svg'; const label = document.querySelector('[data-profile-source]'); if (label) label.textContent = 'Local WizardLoop fallback'; } }; + img.addEventListener('error', fallback); + if (img.complete && img.naturalWidth === 0) fallback(); + }); + document.querySelectorAll('[data-pointer]').forEach(zone => { + zone.addEventListener('pointermove', event => { + if (root.dataset.motion === 'paused' || media.matches || event.pointerType === 'touch') return; + const rect = zone.getBoundingClientRect(); + zone.style.setProperty('--px', `${((event.clientX - rect.left) / rect.width - .5) * 14}px`); + zone.style.setProperty('--py', `${((event.clientY - rect.top) / rect.height - .5) * 12}px`); + }); + zone.addEventListener('pointerleave', () => { zone.style.setProperty('--px', '0px'); zone.style.setProperty('--py', '0px'); }); + }); +})(); diff --git a/concepts/assets/concept-a.css b/concepts/assets/concept-a.css new file mode 100644 index 0000000..7d10222 --- /dev/null +++ b/concepts/assets/concept-a.css @@ -0,0 +1,6 @@ +:root{--bg:#0e0c18;--surface:#151220;--text:#eeeaf5;--muted:#a49db6;--accent:#b9a4f3;--mint:#a4d8cc;--line:#302a41;--focus:#c9b7ff;--hero-glow:#42256355;--art-bg:#221a35;--header-bg:#15121ee8;--ink:var(--text)}html[data-theme=light]{--bg:#f4f1f8;--surface:#ebe5f2;--text:#242033;--muted:#62596f;--accent:#675099;--mint:#386f69;--line:#d5ccdf;--focus:#654a9d;--hero-glow:#c8b6e84d;--art-bg:#e5dbf0;--header-bg:#f4f1f8ed}body{background:var(--bg);color:var(--text)}a{text-decoration:none}.site-header{position:fixed;z-index:100;top:24px;left:50%;transform:translateX(-50%);width:1240px;max-width:calc(100% - 80px);height:70px;border:1px solid var(--line);border-radius:100px;display:flex;align-items:center;justify-content:space-between;padding:0 18px 0 28px;background:var(--header-bg);backdrop-filter:blur(16px);gap:24px}.brand{display:inline-flex;align-items:center;gap:10px;font-weight:700;font-size:21px;letter-spacing:-.8px;white-space:nowrap}.brand svg{stroke:var(--accent);stroke-width:1.25}.brand-dot{color:var(--accent)}.desktop-nav{display:flex;gap:27px;font-size:12px;color:var(--muted)}.desktop-nav a{padding:14px 0;position:relative}.desktop-nav a::after{content:'';position:absolute;bottom:6px;left:0;width:0;height:1px;background:var(--accent);transition:width .25s}.desktop-nav a:hover::after{width:100%}.header-controls{display:flex;align-items:center;gap:12px}.icon-button{width:44px;height:44px;display:inline-flex;align-items:center;justify-content:center;background:none;border:0;border-radius:50%;color:var(--text)}.icon-button svg{stroke:currentColor;stroke-width:1.4}.icon-button:hover{background:var(--line)}.header-contact{font-size:12px;padding:12px 19px;border:1px solid var(--line);border-radius:30px;white-space:nowrap}.header-contact span{margin-left:18px;color:var(--accent)}.menu-toggle{display:none}.orbit-hero{position:relative;overflow:hidden;text-align:center;padding:165px 40px 0;min-height:930px;background:radial-gradient(ellipse at 50% 60%,var(--hero-glow),transparent 55%)}.hero-stardust{position:absolute;inset:0;pointer-events:none;opacity:.5;background-image:radial-gradient(circle at 14% 26%,var(--accent) 0 1px,transparent 1.8px),radial-gradient(circle at 83% 42%,var(--accent) 0 1px,transparent 1.8px),radial-gradient(circle at 29% 66%,var(--accent) 0 1px,transparent 1.8px),radial-gradient(circle at 92% 75%,var(--accent) 0 1px,transparent 1.8px),radial-gradient(circle at 8% 69%,var(--accent) 0 1px,transparent 1.8px)}.hero-kicker{font-size:10px;letter-spacing:2.7px;color:var(--muted);display:flex;gap:12px;justify-content:center;align-items:center}.status-star{font-size:17px;color:var(--mint)}h1{font-size:clamp(58px,7.65vw,110px);font-weight:400;line-height:1.05;letter-spacing:-7px;margin:24px 0 24px}h1 span{font-family:Journal,Georgia,serif;font-style:italic;font-size:1.15em;color:var(--accent);letter-spacing:-5px}.hero-description{font-size:13px;line-height:1.85;color:var(--muted);margin:0}.orbit-system{position:relative;width:900px;max-width:100%;height:390px;margin:-2px auto 0;transform:translate(var(--px,0px),var(--py,0px));transition:transform .6s ease}.orbital-field{width:100%;height:100%;position:absolute;inset:0;color:var(--accent)}.profile-orb{position:absolute;left:50%;top:50%;width:216px;height:216px;transform:translate(-50%,-50%);border-radius:50%;padding:8px;border:1px solid #aa90d86b;box-shadow:0 0 0 12px #8f6ab008,0 0 80px #8e62d029}.profile-orb img{width:100%;height:100%;border-radius:50%;object-fit:cover}.orb-aura{position:absolute;inset:-19px;border-radius:50%;border:1px dashed #9073b868;animation:orbit 60s linear infinite}.orb-dot{position:absolute;top:24px;left:1px;width:7px;height:7px;border-radius:50%;background:var(--mint);box-shadow:0 0 15px #a4d8cc}.orbit-satellite{animation:pulse 4s ease-in-out infinite}.orbit-label{position:absolute;font-size:9px;letter-spacing:1.4px;color:var(--muted);background:var(--bg);padding:4px 9px}.label-design{top:43%;left:4%}.label-code{top:55%;right:1%}.orb-coordinate{position:absolute;bottom:0;left:0;right:0;color:var(--muted);font-size:8px;letter-spacing:2px}.hero-bottom{max-width:1240px;margin:35px auto 0;display:flex;justify-content:space-between;align-items:center;color:var(--muted);font-size:10px;min-height:60px}.explore-link{display:flex;align-items:center;gap:22px}.explore-link span{font-size:18px;color:var(--accent)}.motion-control{color:var(--muted);border:0;background:none;font-size:10px;min-height:44px;padding:8px 0}.pause-symbol{border:1px solid var(--line);padding:5px 9px;border-radius:50%;margin-right:8px}.practice-strip{border-block:1px solid var(--line);display:flex;justify-content:center;gap:50px;align-items:center;padding:28px 24px;font-size:12px;color:var(--muted)}.practice-strip i{color:var(--accent);font-style:normal;font-size:11px}.section-shell{max-width:1240px;width:calc(100% - 120px);margin-inline:auto}.section-eyebrow{font-size:9px;letter-spacing:1.7px;color:var(--muted);display:flex;justify-content:space-between;gap:25px;margin:0 0 48px}.about-section{padding-block:115px}.about-grid{display:grid;grid-template-columns:1.1fr 1fr;gap:90px}h2{font-size:clamp(36px,4.3vw,60px);line-height:1.15;letter-spacing:-2.5px;font-weight:400;margin:0}h2 span{color:var(--accent)}.about-copy{font-size:13px;color:var(--muted);max-width:440px;padding-top:3px}.about-copy p{margin:0 0 24px}.about-copy .lead{font-size:19px;color:var(--text);line-height:1.55}.text-link{display:inline-flex;gap:23px;align-items:center;font-size:12px;min-height:44px;color:var(--text);border-bottom:1px solid var(--line)}.text-link span{color:var(--accent);transition:transform .2s}.text-link:hover span{transform:translate(3px,-3px)}.secondary-link{margin-left:18px}.work-section{padding-block:45px 90px}.section-title-row{display:flex;justify-content:space-between;align-items:end;margin-bottom:40px}.work-grid{display:grid;grid-template-columns:1fr 1fr;gap:28px}.work-art{height:330px;overflow:hidden;position:relative;border:1px solid var(--line);border-radius:3px;background:var(--art-bg)}.lottie-art{display:flex;align-items:center;justify-content:center;background:radial-gradient(ellipse at 50% 45%,#7953ba38,transparent 60%),var(--art-bg)}.ribbon-loop{width:150px;height:150px;border-radius:45% 55% 50% 50%;border:22px solid #b498ea;box-shadow:inset 4px 8px 10px #e1cfff80,8px 10px 25px #0a071c66;transform:rotate(-30deg);animation:ribbon 12s ease-in-out infinite}.loop-one{margin-right:-42px;z-index:1}.loop-two{transform:rotate(30deg);border-color:#7b6aaf;animation-delay:-6s}.art-caption{position:absolute;bottom:22px;left:25px;font:8px monospace;letter-spacing:2px;color:var(--accent)}.art-number{position:absolute;right:24px;top:18px;font:11px monospace;color:var(--accent)}.shabbat-art{background:radial-gradient(ellipse at center,#40655b30,transparent 65%),var(--surface);color:var(--mint)}.shabbat-art svg{width:100%;height:100%;stroke:currentColor;stroke-width:1}.work-description{display:flex;justify-content:space-between;align-items:center;padding-top:24px}.work-category{font-size:8px;letter-spacing:1.7px;color:var(--muted)}.work-card h3{font-size:24px;font-weight:400;margin:8px 0;letter-spacing:-.5px}.work-arrow{border:1px solid var(--line);border-radius:50%;width:40px;height:40px;display:grid;place-items:center;transition:background .2s}.work-card:hover .work-arrow{background:var(--accent);color:var(--bg)}.work-card p{font-size:12px;color:var(--muted);margin-top:4px}.asset-note{font-size:10px;color:var(--muted);margin-top:25px}.posts-section{padding-block:55px 110px;border-top:1px solid var(--line)}.posts-grid{display:grid;grid-template-columns:1fr 1fr;gap:90px;align-items:center}.posts-grid h2{margin-bottom:32px}.post-preview{padding:30px;border:1px solid var(--line);border-radius:8px;background:var(--surface)}.post-top{display:flex;align-items:center;gap:10px;font-size:11px;line-height:1.6}.mini-avatar img{border-radius:50%}.post-top small{color:var(--muted);font-size:9px}.sample-tag{margin-left:auto;font-size:8px;letter-spacing:1px;color:var(--muted)}.post-visual{background:var(--bg);border-radius:3px;margin-top:25px;color:var(--accent)}.post-visual svg{display:block;width:100%;height:160px;stroke:currentColor;stroke-width:1}.post-visual g{transform-origin:center;animation:orbit 50s linear infinite}.post-preview h3{font-size:18px;font-weight:400;letter-spacing:-.3px;margin:24px 0 8px}.post-preview p{font-size:12px;color:var(--muted);margin:0 0 15px}.post-preview .text-link{font-size:10px}.community-section{padding-block:100px 115px;text-align:center;border-top:1px solid var(--line)}.community-star{display:block;color:var(--mint);font-size:36px;margin-bottom:20px}.community-section .section-eyebrow{justify-content:center;margin-bottom:28px}.community-section h2{font-size:clamp(42px,5.5vw,76px)}.community-section h2 em{font-family:Journal,Georgia,serif;color:var(--accent);font-weight:400}.community-section p:not(.section-eyebrow){font-size:12px;color:var(--muted);margin:30px 0}.community-actions{display:flex;gap:30px;justify-content:center;align-items:center}.primary-link{display:inline-flex;gap:50px;padding:14px 24px;border-radius:30px;font-size:12px;background:var(--accent);color:var(--bg)}.site-footer{border-top:1px solid var(--line);padding-top:45px;color:var(--muted)}.footer-main{display:flex;justify-content:space-between;gap:30px;align-items:start}.footer-main .brand{color:var(--text)}.footer-main p{font-size:11px}.footer-main nav{display:flex;gap:24px;font-size:11px;flex-wrap:wrap}.footer-main nav a{min-height:44px;display:flex;align-items:center}.footer-bottom{display:flex;justify-content:space-between;gap:20px;border-top:1px solid var(--line);padding-top:24px;margin-top:35px;font-size:9px}.rights-note{font-size:9px;margin-top:18px}.preview-crosslinks{margin-top:20px}.preview-crosslinks a{font-size:10px}@keyframes orbit{to{transform:rotate(360deg)}}@keyframes pulse{50%{opacity:.25}}@keyframes ribbon{50%{border-radius:55% 45% 58% 42%;transform:rotate(20deg) translateY(-8px)}} +@media(min-width:1700px){.orbit-hero{padding-top:190px;min-height:1000px}.orbit-system{margin-top:20px}} +@media(max-width:1050px){.desktop-nav{gap:18px}.site-header{max-width:calc(100% - 40px);padding-left:20px;gap:14px}.brand{font-size:18px}.header-contact{padding-inline:15px}.orbit-hero{min-height:900px}.section-shell{width:calc(100% - 70px)}.about-grid,.posts-grid{gap:45px}.practice-strip{gap:30px}.work-art{height:280px}} +@media(max-width:800px){.site-header{top:14px;max-width:calc(100% - 28px);height:62px;padding-inline:18px 8px}.menu-toggle{display:inline-flex}.header-contact{display:none}.header-controls{gap:0}.brand{font-size:18px}.brand svg{width:29px}.orbit-hero{padding:140px 20px 0;min-height:800px}.hero-kicker{font-size:8px;letter-spacing:1.8px}h1{font-size:76px;letter-spacing:-4px}h1 span{letter-spacing:-3px}.hero-description{font-size:12px}.orbit-system{height:320px;width:740px;max-width:calc(100% + 70px);left:-35px;margin-top:15px}.profile-orb{width:178px;height:178px}.orbit-label{font-size:7px;letter-spacing:.7px}.label-design{left:0;top:25%}.label-code{right:0;top:75%}.orb-coordinate{bottom:-5px;font-size:7px}.hero-bottom{margin-top:27px;gap:12px}.profile-source{font-size:9px}.practice-strip{gap:22px;font-size:10px;padding-block:25px}.section-shell{width:calc(100% - 44px)}.section-eyebrow{font-size:8px;line-height:1.8;margin-bottom:30px}.section-eyebrow span:last-child:not(:first-child){max-width:40%;text-align:right}.about-section{padding-block:75px}.about-grid{grid-template-columns:1fr;gap:32px}.about-copy{max-width:100%}.about-copy .lead{font-size:18px}h2{font-size:46px}.work-section{padding-block:20px 70px}.work-art{height:220px}.work-grid{gap:20px}.ribbon-loop{width:110px;height:110px;border-width:18px}.work-category{font-size:7px;letter-spacing:.7px}.work-card h3{font-size:21px}.work-card p{font-size:11px}.art-caption{font-size:6px;left:14px}.posts-grid{gap:36px}.posts-section{padding-block:50px 75px}.post-preview{padding:20px}.post-top{font-size:10px}.sample-tag{font-size:7px}.community-section{padding-block:70px 80px}.community-section h2{font-size:58px}.footer-main{flex-direction:column}.footer-main nav{gap:22px}} +@media(max-width:540px){.hero-kicker{font-size:7px;letter-spacing:1.4px}.orbit-hero{padding-top:130px;min-height:785px}h1{font-size:60px;line-height:1.07;margin-top:24px;letter-spacing:-3px}h1 span{letter-spacing:-2px}.hero-description{font-size:10px;line-height:1.9}.orbit-system{height:330px;margin-top:4px}.orbital-field{width:620px;max-width:none;left:50%;transform:translateX(-50%)}.profile-orb{width:180px;height:180px}.orbit-label{font-size:6px;letter-spacing:1px;top:15%}.label-design{left:calc(50% - 135px)}.label-code{right:calc(50% - 142px);top:80%}.orb-coordinate{font-size:6px;letter-spacing:1.4px;bottom:6px}.hero-bottom{margin-top:17px;flex-wrap:wrap;gap:4px 20px}.hero-bottom .profile-source{order:3;width:100%;font-size:8px;margin-top:0}.motion-control{font-size:9px}.explore-link{font-size:9px}.practice-strip{justify-content:space-around;gap:8px;font-size:8px;padding:20px 16px}.practice-strip i{font-size:7px}.section-title-row{gap:20px;align-items:end}.section-title-row .text-link{font-size:10px;gap:10px}.section-title-row h2{font-size:39px}.work-grid{grid-template-columns:1fr;gap:32px}.work-art{height:280px}.ribbon-loop{width:145px;height:145px;border-width:22px}.work-category{font-size:8px;letter-spacing:1.3px}.art-caption{font-size:7px;left:20px}.about-copy{font-size:12px}.text-link{font-size:11px}.secondary-link{margin-left:14px}.posts-grid{grid-template-columns:1fr;gap:30px}.posts-grid h2{font-size:47px}.post-preview{padding:25px}.post-top{font-size:12px}.sample-tag{font-size:8px}.community-section h2{font-size:45px;letter-spacing:-1.7px}.community-actions{gap:17px;flex-wrap:wrap}.community-actions .text-link{font-size:10px}.primary-link{gap:28px}.footer-bottom{flex-direction:column;gap:10px}.footer-main nav{font-size:10px;gap:20px}.preview-crosslinks{gap:4px 15px}.preview-crosslinks a{font-size:9px}h2{font-size:43px}} +@media(max-width:359px){h1{font-size:52px}.hero-kicker{font-size:6px}.brand{font-size:16px}.hero-description{font-size:9px}.site-header{padding-left:14px}.section-shell{width:calc(100% - 36px)}.practice-strip{font-size:7px}.section-title-row h2{font-size:34px}.about-copy .lead{font-size:16px}.secondary-link{margin-left:0;display:flex;max-width:max-content}.community-section h2{font-size:39px}.section-eyebrow{letter-spacing:1px}.post-preview{padding:18px}.post-preview .text-link{font-size:9px}} diff --git a/concepts/assets/concept-b.css b/concepts/assets/concept-b.css new file mode 100644 index 0000000..64c3683 --- /dev/null +++ b/concepts/assets/concept-b.css @@ -0,0 +1,35 @@ +@font-face{font-family:Journal;src:url('/assets/fonts/cormorant-latin.woff2') format('woff2');font-style:normal;font-weight:300 700;font-display:swap} +:root{--paper:#f3efe5;--paper-alt:#e9e2d4;--ink:#292822;--muted:#655f53;--line:#c7c0af;--accent:#a13d25;--violet:#4e405a;--nav-bg:rgba(243,239,229,.94);--display:Journal,Georgia,serif;--body:Studio,Arial,sans-serif} +html{background:var(--paper);color:var(--ink);scroll-padding-top:112px;scroll-behavior:smooth} +html[data-theme=dark]{--paper:#211e29;--paper-alt:#2c2637;--ink:#f0e8d9;--muted:#c5b8c8;--line:#625469;--accent:#f2b58e;--violet:#d6b7df;--nav-bg:rgba(33,30,41,.95);color-scheme:dark} +body{margin:0;font-family:var(--body);background-image:url("data:image/svg+xml,%3Csvg viewBox='0 0 180 180' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.76' numOctaves='3' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' opacity='.035' filter='url(%23n)'/%3E%3C/svg%3E");line-height:1.6;transition:color .3s,background-color .3s;background-color:var(--paper)} +html[data-theme=dark] body{background-image:radial-gradient(ellipse at 78% 22%,#51405936,transparent 45%),radial-gradient(#c2a3ce18 .7px,transparent .7px);background-size:auto,27px 27px} +*,*::before,*::after{box-sizing:border-box}a{color:inherit;text-decoration:none}button{font:inherit}button,a{-webkit-tap-highlight-color:transparent}button{color:inherit}button:not(:disabled),a{touch-action:manipulation}button{cursor:pointer}button:focus-visible,a:focus-visible{outline:2px solid var(--accent);outline-offset:6px}svg{display:block}button{border:0}h1,h2,h3,p{margin:0}em{font-weight:400}.skip-link{position:fixed;top:-100px;left:20px;z-index:100;background:var(--ink);color:var(--paper);padding:12px 20px}.skip-link:focus{top:12px} +.site-header{position:fixed;z-index:40;inset:20px 4.45% auto;max-width:1440px;margin:0 auto;height:75px;display:flex;align-items:center;justify-content:space-between;border-top:1px solid var(--ink);border-bottom:1px solid var(--line);padding:0 20px;background:var(--nav-bg);backdrop-filter:blur(12px)} +.brand{font-family:var(--display);font-size:29px;letter-spacing:-1px;display:inline-flex;align-items:center;font-weight:600;line-height:1.1}.brand svg{width:31px;height:31px;fill:none;stroke:var(--ink);stroke-width:1.3;margin-right:10px}.brand-dot{color:var(--accent)}.desktop-nav{display:flex;gap:28px;font-size:12px;font-weight:600}.desktop-nav a{position:relative;padding:12px 0}.desktop-nav a::after{content:'';position:absolute;left:0;right:0;bottom:6px;height:1px;background:var(--accent);transform:scaleX(0);transform-origin:left;transition:transform .22s}.desktop-nav a:hover::after{transform:scaleX(1)}.header-controls{display:flex;gap:14px;align-items:center}.theme-button{background:none;display:flex;align-items:center;gap:8px;font-size:10px;text-transform:uppercase;letter-spacing:1px;min-height:44px}.theme-button svg{width:18px;height:18px;stroke:currentColor;fill:none;stroke-width:1.4}.menu-toggle{display:none;width:44px;height:44px;background:none;padding:11px}.menu-toggle span{display:block;height:1px;background:var(--ink);margin:7px 0}.site-header #mobile-nav{position:absolute;top:calc(100% + 1px);left:0;right:0;bottom:auto;background:var(--paper);padding:22px;border-radius:0;max-height:calc(100dvh - 115px);overflow:auto;border-bottom:1px solid var(--ink)}#mobile-nav[hidden]{display:none}#mobile-nav a{display:block;padding:11px 0;font-family:var(--display);font-size:27px;border-bottom:1px solid var(--line)} +main,.site-footer{max-width:1568px;margin:auto}.hero{padding:139px 6% 0}.edition{display:flex;justify-content:space-between;align-items:center;font-size:9px;letter-spacing:1.7px}.edition-label{font-weight:700}.edition-right{color:var(--muted);letter-spacing:1px}.edition-right span{margin-left:14px;color:var(--accent);font-size:17px}.hero-composition{display:grid;grid-template-columns:1.46fr 1fr;gap:10px;margin-top:53px;position:relative}.hero-copy{padding-top:32px;position:relative;z-index:1}.eyebrow{font-size:10px;letter-spacing:1.8px;display:flex;align-items:center;gap:12px;font-weight:600}.tiny-star{color:var(--accent);font-size:23px;line-height:1}.hero h1{font-family:var(--display);font-weight:400;font-size:clamp(70px,8.8vw,142px);line-height:.91;letter-spacing:-6px;margin:27px -70px 31px 0;white-space:nowrap}.hero h1 em{color:var(--accent)}.hero-summary{display:grid;grid-template-columns:37px 1fr;max-width:422px;margin-top:39px}.margin-mark{font-family:var(--display);font-size:35px;line-height:1.2;color:var(--accent)}.hero-summary p{font-size:13px;line-height:1.9;max-width:355px}.text-link{display:inline-flex;align-items:center;gap:36px;font-size:11px;font-weight:700;border-bottom:1px solid var(--ink);min-height:44px;padding:7px 0;transition:gap .25s,color .25s}.text-link:hover{gap:46px;color:var(--accent)}.text-link span{font-size:20px;font-weight:400}.hero-summary .text-link{margin-top:19px}.hero-art{position:relative;align-self:start;padding-top:12px;min-width:0}.art-note{position:absolute;left:18%;top:4px;font-size:8px;letter-spacing:1px;color:var(--muted);text-transform:uppercase}.ink-drawing{position:relative;margin:13px -10px -15px;width:calc(100% + 20px);color:var(--violet)}.alchemist-emblem{width:100%;height:auto;overflow:visible;stroke:currentColor;stroke-width:1.3}.ink-guides{stroke-width:.65;opacity:.62}.ink-guides>path:first-child{stroke-dasharray:2 6}.turning-ink{stroke-width:.8;opacity:.7;transform-origin:250px 254px;animation:ink-turn 55s linear infinite}.ink-flower{stroke-width:1.4;animation:ink-breathe 8s ease-in-out infinite;transform-origin:250px 254px}.ink-core{stroke-width:1}.orbit-dots{stroke-width:1}.ink-stars{stroke:var(--accent);stroke-width:1}.hand-loop{stroke-width:1.3;stroke:var(--accent)}.profile-stamp{position:absolute;right:1%;top:9%;transform:rotate(9deg);width:90px;height:90px;border-radius:50%;padding:5px;background:var(--paper);border:1px solid var(--ink);box-shadow:0 0 0 5px var(--paper),0 0 0 6px var(--line)}.profile-stamp img{width:100%;height:100%;object-fit:cover;border-radius:50%;filter:saturate(.65)}.stamp-star{position:absolute;right:-13px;bottom:0;color:var(--accent);font-size:33px}.art-bottom{display:flex;align-items:start;justify-content:space-between;padding:0 5% 0 16%}.redraw-button{background:none;border-bottom:1px solid var(--line);font-size:9px;letter-spacing:1px;min-height:44px;padding:5px 0;display:flex;align-items:center;gap:8px}.redraw-button span{font-size:19px}.redraw-button:hover{color:var(--accent)}.handwritten{font-family:var(--display);font-style:italic;font-size:22px;line-height:1;transform:rotate(-7deg);color:var(--accent)}.profile-caption{text-align:center;font-size:8px;color:var(--muted);margin:27px 0 25px;letter-spacing:.5px}.disciplines{display:flex;justify-content:space-between;align-items:center;gap:22px;margin-top:42px;border-top:1px solid var(--ink);border-bottom:1px solid var(--ink);padding:19px 2%;font-family:var(--display);font-size:27px;letter-spacing:-.6px}.disciplines>span:nth-child(even){color:var(--accent);font-size:16px}.hero-footnote{font-size:8px;letter-spacing:1.4px;margin-top:13px;display:flex;justify-content:space-between;color:var(--muted)}.hero-footnote a{font-weight:600;min-height:30px}.hero-footnote a:hover{color:var(--accent)} +.section-shell{margin:115px 6% 0;display:grid;grid-template-columns:23% 1fr;gap:28px}.section-margin{padding-top:12px}.chapter{font-size:9px;font-weight:600;letter-spacing:1.4px}.margin-flower{stroke:var(--accent);stroke-width:1.2;width:78px;height:120px;margin:35px 0 0 17px;transform:rotate(-12deg)}.margin-scribble{display:block;font-family:var(--display);font-style:italic;color:var(--accent);font-size:22px;line-height:1;transform:rotate(-8deg);margin-left:12px}.about-body h2,.work-body h2,.posts-body h2{font-family:var(--display);font-size:clamp(36px,4.4vw,65px);font-weight:400;letter-spacing:-1.6px;line-height:1.03}.about-body h2 em,.work-body h2 em,.posts-body h2 em{color:var(--accent)}.about-columns{display:grid;grid-template-columns:1fr 1fr;gap:42px;margin:30px 0 18px;max-width:755px;font-size:12px;line-height:1.9;color:var(--muted)}.margin-description{font-family:var(--display);font-size:24px;line-height:1.1;font-style:italic;margin-top:30px;color:var(--muted)}.work-section{padding-top:60px;border-top:1px solid var(--line)}.section-title-row{display:flex;justify-content:space-between;align-items:end;margin-bottom:40px;gap:18px}.work-note{font-size:8px;line-height:1.7;text-align:right;letter-spacing:.8px;color:var(--muted);padding-bottom:5px}.project-row{display:grid;grid-template-columns:37px 124px 1fr 36px;align-items:center;gap:25px;min-height:147px;border-top:1px solid var(--line);transition:padding .25s,background .25s;padding:16px 0}.project-row:last-of-type{border-bottom:1px solid var(--line)}.project-row:hover{padding-left:8px;background:color-mix(in srgb,var(--paper-alt),transparent 55%)}.project-number{font-family:var(--display);font-size:20px;color:var(--muted)}.project-art{width:124px;height:98px;display:grid;place-items:center;background:var(--paper-alt);color:var(--violet)}.project-art svg{width:124px;fill:none;stroke:currentColor;stroke-width:1.3}.art-shabbat{background:color-mix(in srgb,var(--accent) 8%,var(--paper));color:var(--accent)}.art-date{background:color-mix(in srgb,var(--violet) 9%,var(--paper))}.project-title h3{font-family:var(--display);font-size:37px;font-weight:400;letter-spacing:-.7px;line-height:1.1}.project-title p{font-size:9px;text-transform:uppercase;letter-spacing:1px;color:var(--muted);margin-top:10px}.project-arrow{font-size:29px;transition:transform .25s}.project-row:hover .project-arrow{transform:translate(4px,-4px);color:var(--accent)}.work-footnote{display:flex;justify-content:space-between;align-items:center;margin-top:13px;gap:20px}.work-footnote>span{font-size:8px;color:var(--muted);max-width:235px}.work-footnote .text-link{font-size:9px;gap:18px}.posts-section{padding-top:60px;border-top:1px solid var(--line)}.posts-body h2{margin-bottom:35px}.journal-preview{display:grid;grid-template-columns:.85fr 1.2fr;gap:35px;background:var(--violet);color:#fff5e7;padding:42px 36px 40px}.journal-illustration{align-self:center;position:relative}.journal-illustration svg{width:100%;stroke:currentColor;stroke-width:1.15}.journal-illustration .handwritten{display:block;text-align:center;color:#efd5bf;font-size:24px}.journal-preview-copy h3{font-family:var(--display);font-size:45px;font-weight:400;letter-spacing:-.7px;line-height:1.04;margin:15px 0 18px}.preview-badge{font-size:8px;letter-spacing:1.5px;border:1px solid #ffffff60;padding:5px 8px}.journal-preview-copy p{font-size:11px;line-height:1.8}.journal-preview-copy .preview-disclosure{font-size:9px;margin:15px 0 7px;color:#e1d3e5}.journal-preview-copy .text-link{border-color:#fff5e7;gap:20px;font-size:10px}.journal-preview-copy .text-link:hover{color:#fff5e7;gap:28px}html[data-theme=dark] .journal-preview{background:#b997c4;color:#251d2b}html[data-theme=dark] .journal-illustration .handwritten,html[data-theme=dark] .journal-preview-copy .preview-disclosure{color:#3e2d47}html[data-theme=dark] .journal-preview-copy .text-link{border-color:#251d2b}html[data-theme=dark] .journal-preview-copy .text-link:hover{color:#251d2b}html[data-theme=dark] .preview-badge{border-color:#251d2b60} +.community-section{margin:115px 6% 0;padding:40px 0 0;border-top:1px solid var(--ink)}.community-top{display:flex;justify-content:space-between;align-items:center}.community-star{font-size:55px;line-height:1;color:var(--accent);animation:ink-turn 40s linear infinite}.community-main{display:grid;grid-template-columns:1.45fr 1fr;gap:90px;margin:25px 0 48px;align-items:end}.community-main h2{font-family:var(--display);font-size:clamp(50px,6.5vw,95px);line-height:.95;font-weight:400;letter-spacing:-3px}.community-main h2 em{color:var(--accent)}.community-copy p{font-size:12px;line-height:1.9;max-width:280px}.contact-link{display:flex;align-items:center;justify-content:space-between;border-bottom:1px solid var(--ink);padding:15px 0;font-size:12px;font-weight:700;max-width:300px;margin-top:20px}.contact-link span{font-size:26px}.contact-link:hover{color:var(--accent)}.social-links{display:grid;grid-template-columns:1fr 1fr 1fr;border-top:1px solid var(--line);border-bottom:1px solid var(--line)}.social-links a{display:flex;align-items:center;gap:15px;min-height:76px;font-size:11px;font-weight:600;padding:12px 25px;border-right:1px solid var(--line)}.social-links a:first-child{padding-left:0}.social-links a:last-child{border:0}.social-links a span:first-child{font-family:var(--display);font-size:17px;color:var(--muted)}.social-links a span:last-child{font-size:22px;margin-left:auto;transition:transform .25s}.social-links a:hover{color:var(--accent)}.social-links a:hover span:last-child{transform:translate(3px,-3px)} +.site-footer{padding:50px 6% 25px}.footer-top{display:flex;justify-content:space-between;align-items:center;gap:20px;margin-bottom:35px}.footer-top .brand{font-size:30px}.footer-top>span{font-family:var(--display);font-style:italic;font-size:22px;color:var(--muted)}.back-top{font-size:9px;display:flex;align-items:center;gap:18px;min-height:44px}.back-top span{font-size:21px}.footer-bottom{display:flex;gap:28px;align-items:center;font-size:8px;color:var(--muted);border-top:1px solid var(--line);padding-top:17px}.footer-bottom>span{margin-right:auto}.footer-bottom button{font-size:8px;color:var(--muted);background:none;min-height:44px;padding:0 8px;text-decoration:underline;text-underline-offset:4px}.license-note{font-size:8px;color:var(--muted);margin:4px 0 24px}.concept-nav{display:flex;gap:23px;font-size:8px;letter-spacing:.2px;border-top:1px dashed var(--line);padding-top:16px;align-items:center;flex-wrap:wrap}.concept-nav a{min-height:32px;display:flex;align-items:center}.concept-nav [aria-current]{color:var(--accent);font-weight:600}.concept-nav a:hover{text-decoration:underline} +.is-drawing .ink-flower path,.is-drawing .turning-ink ellipse,.is-drawing .hand-loop{stroke-dasharray:1000;stroke-dashoffset:1000;animation:draw-ink 2.5s cubic-bezier(.3,.2,.2,1) forwards}.is-drawing .ink-flower{animation:none}.is-drawing .turning-ink{animation:none}.is-drawing .hand-loop{animation-delay:.2s}@keyframes draw-ink{to{stroke-dashoffset:0}}@keyframes ink-turn{to{transform:rotate(360deg)}}@keyframes ink-breathe{50%{transform:scale(.96) rotate(4deg)}}html[data-motion=paused] *,html[data-motion=paused] *::before,html[data-motion=paused] *::after{animation-play-state:paused!important}html[data-motion=paused] .is-drawing path,html[data-motion=paused] .is-drawing ellipse{stroke-dasharray:none!important;stroke-dashoffset:0!important} +@media(min-width:1600px){.hero h1{font-size:142px}.community-main h2{font-size:95px}} +@media(max-width:1100px){.desktop-nav{gap:18px;font-size:10px}.hero{padding-left:5%;padding-right:5%}.hero h1{letter-spacing:-4px}.hero-summary p{font-size:12px}.hero-composition{grid-template-columns:1.38fr 1fr}.hero-copy{padding-top:23px}.ink-drawing{margin-top:30px}.eyebrow{font-size:8px;letter-spacing:1.2px}.art-note{top:17px;font-size:7px}.hero-summary{margin-top:28px;max-width:345px}.profile-stamp{width:73px;height:73px}.profile-caption{font-size:7px}.handwritten{font-size:20px}.disciplines{font-size:24px;margin-top:40px}.section-shell{grid-template-columns:20% 1fr;gap:25px;margin-left:5%;margin-right:5%}.work-note{font-size:7px}.project-row{grid-template-columns:25px 105px 1fr 30px;gap:17px}.project-art{width:105px;height:89px}.project-art svg{width:105px}.journal-preview{padding:30px 25px;gap:22px}.journal-preview-copy h3{font-size:37px}.community-section{margin-left:5%;margin-right:5%}.community-main{gap:55px}.site-footer{padding-left:5%;padding-right:5%}.social-links a{font-size:10px;padding-right:16px;padding-left:16px}.social-links a span:first-child{font-size:14px}} +@media(max-width:800px){.site-header{height:67px;inset:14px 5% auto;padding:0 10px}.brand{font-size:26px}.brand svg{width:26px;height:26px;margin-right:7px}.desktop-nav{display:none}.menu-toggle{display:block}.theme-button{font-size:8px}.hero{padding-top:112px}.edition{font-size:8px}.edition-right{font-size:7px;max-width:180px;text-align:right;line-height:1.5}.edition-right span{display:none}.hero-composition{grid-template-columns:1.35fr 1fr;margin-top:25px}.hero h1{font-size:9.2vw;letter-spacing:-3px;margin-top:23px}.hero-summary{grid-template-columns:27px 1fr;margin-top:24px;max-width:305px}.hero-summary p{font-size:10px}.hero-summary .text-link{font-size:10px;margin-top:12px}.margin-mark{font-size:27px}.hero-art{padding-top:22px}.art-note{font-size:6px;left:11%;top:15px}.ink-drawing{margin-top:20px}.profile-stamp{width:61px;height:61px}.art-bottom{padding-left:5%;padding-right:0;gap:10px}.redraw-button{font-size:7px;letter-spacing:0}.handwritten{font-size:17px}.profile-caption{font-size:6px;margin-top:17px}.disciplines{font-size:20px;gap:13px;padding:16px 0}.disciplines>span:nth-child(even){font-size:11px}.section-shell{margin-top:80px;grid-template-columns:17% 1fr;gap:25px}.chapter{font-size:7px;letter-spacing:.8px}.about-body h2,.work-body h2,.posts-body h2{font-size:39px}.margin-description{font-size:20px}.about-columns{font-size:10px;gap:24px}.margin-flower{width:55px;height:100px;margin-left:5px}.margin-scribble{font-size:19px}.project-row{min-height:123px;grid-template-columns:22px 85px 1fr 26px;gap:13px}.project-art,.project-art svg{width:85px}.project-art{height:77px}.project-title h3{font-size:28px}.project-title p{font-size:7px}.project-number{font-size:17px}.work-footnote>span{max-width:165px;font-size:7px}.work-footnote .text-link{font-size:8px;gap:9px}.journal-preview{gap:15px;padding:25px 20px}.journal-preview-copy h3{font-size:32px}.journal-preview-copy p{font-size:10px}.journal-preview-copy .preview-disclosure{font-size:8px}.journal-preview-copy .text-link{font-size:8px;gap:12px}.journal-illustration .handwritten{font-size:19px}.community-section{margin-top:80px}.community-main{gap:35px}.community-main h2{font-size:60px;letter-spacing:-2px}.community-copy p{font-size:10px}.contact-link{font-size:10px}.social-links a{font-size:8px;gap:10px;padding-left:13px;padding-right:13px}.footer-top>span{font-size:18px}.back-top{font-size:8px;gap:10px}.footer-bottom{gap:17px;font-size:7px}.footer-bottom button,.license-note{font-size:7px}} +@media(max-width:600px){html{scroll-padding-top:103px}.site-header{inset:12px 5% auto;height:62px}.brand{font-size:25px}.header-controls{gap:8px}.theme-word{display:none}.theme-button{width:40px;justify-content:center}.theme-button svg{width:19px;height:19px}.hero{padding-top:103px}.edition{font-size:7px;letter-spacing:1.25px}.edition-right{max-width:112px;font-size:6px;letter-spacing:.5px}.hero-composition{display:flex;flex-direction:column;margin-top:28px;gap:0}.hero-copy{padding-top:0}.eyebrow{font-size:7px;letter-spacing:1.25px;gap:9px}.tiny-star{font-size:20px}.hero h1{font-size:clamp(55px,14.4vw,85px);letter-spacing:-3px;margin:23px -10px 26px 0;line-height:.94}.hero-summary{margin-top:0;max-width:350px;grid-template-columns:29px 1fr}.hero-summary p{font-size:11px;line-height:1.85;max-width:285px}.hero-summary .text-link{margin-top:12px;font-size:10px}.hero-art{width:86%;max-width:350px;margin:30px auto 0;padding-top:16px;align-self:center}.art-note{font-size:7px;top:5px;left:16%}.ink-drawing{margin-top:7px;margin-bottom:-20px}.profile-stamp{width:70px;height:70px;right:0;top:8%}.stamp-star{font-size:28px}.art-bottom{padding-left:12%;padding-right:1%}.redraw-button{font-size:8px}.handwritten{font-size:21px}.profile-caption{font-size:7px;margin-top:20px;margin-bottom:10px}.disciplines{margin-top:27px;flex-wrap:wrap;justify-content:center;gap:8px 19px;font-size:22px;padding:16px 0;line-height:1.2}.disciplines>span:nth-child(6){display:none}.disciplines>span:nth-child(5)::after{content:'';display:block}.hero-footnote{font-size:6px;letter-spacing:.8px;gap:15px}.hero-footnote>span{max-width:220px;line-height:1.7}.hero-footnote a{white-space:nowrap}.section-shell{display:block;margin-top:63px}.section-margin{padding-top:0;margin-bottom:23px}.chapter{font-size:8px;letter-spacing:1.1px}.margin-flower,.margin-scribble,.margin-description{display:none}.about-body h2,.work-body h2,.posts-body h2{font-size:clamp(35px,8.9vw,51px);letter-spacing:-1px}.about-columns{font-size:11px;gap:18px;margin-top:24px;grid-template-columns:1fr}.about-columns p{max-width:390px}.about-body .text-link{font-size:10px;margin-top:2px}.work-section,.posts-section{padding-top:33px}.section-title-row{margin-bottom:27px}.work-note{max-width:81px;font-size:6px;line-height:1.9}.project-row{grid-template-columns:18px 75px 1fr 23px;gap:12px;min-height:115px;padding:14px 0}.project-art,.project-art svg{width:75px}.project-art{height:75px}.project-number{font-size:16px}.project-title h3{font-size:29px;letter-spacing:-.6px}.project-title p{font-size:6px;letter-spacing:.6px;line-height:1.5;max-width:120px;margin-top:8px}.project-arrow{font-size:25px}.work-footnote{display:block;margin-top:15px}.work-footnote>span{display:block;font-size:7px;max-width:none;margin-bottom:8px}.work-footnote .text-link{font-size:9px;gap:23px}.posts-body h2{margin-bottom:26px}.journal-preview{padding:28px 23px;grid-template-columns:1fr;gap:15px}.journal-illustration{display:flex;align-items:center;justify-content:center;gap:15px;margin:-9px -5px 3px}.journal-illustration svg{width:123px;flex:0 0 123px}.journal-illustration .handwritten{font-size:24px;text-align:left}.journal-preview-copy{padding-top:11px;border-top:1px solid #ffffff40}.journal-preview-copy h3{font-size:44px;margin-top:17px}.journal-preview-copy p{font-size:11px}.journal-preview-copy .preview-disclosure{font-size:9px}.journal-preview-copy .text-link{font-size:10px;margin-top:4px;gap:30px}.community-section{margin-top:64px;padding-top:30px}.community-top .chapter{font-size:7px}.community-star{font-size:37px}.community-main{display:block;margin:19px 0 28px}.community-main h2{font-size:clamp(48px,12.4vw,70px);letter-spacing:-2px}.community-copy{margin-top:27px;max-width:330px}.community-copy p{font-size:11px;max-width:300px}.contact-link{font-size:11px;margin-top:12px;max-width:100%}.social-links{display:block}.social-links a,.social-links a:first-child{border-right:0;border-bottom:1px solid var(--line);padding:11px 0;font-size:11px;min-height:61px;gap:20px}.social-links a span:first-child{font-size:18px}.social-links a span:last-child{font-size:24px}.site-footer{padding-top:36px}.footer-top{flex-wrap:wrap;justify-content:start;column-gap:20px;row-gap:5px;margin-bottom:20px}.footer-top .brand{font-size:28px}.footer-top>span{font-size:18px}.back-top{width:100%;justify-content:space-between;border-bottom:1px solid var(--line);padding:10px 0;font-size:9px;margin-top:10px;min-height:48px}.footer-bottom{flex-wrap:wrap;padding-top:0;border-top:0;gap:5px 19px;font-size:7px}.footer-bottom>span{width:100%;margin-bottom:5px}.footer-bottom button{font-size:8px;margin-left:auto}.license-note{font-size:7px;max-width:290px;margin-top:8px;line-height:1.8}.concept-nav{gap:6px 17px;font-size:7px}.concept-nav a:first-child{width:100%}.concept-nav [aria-current]{padding:8px 0}} +@media(max-width:359px){.hero h1{font-size:52px;letter-spacing:-2.8px}.hero-summary p{font-size:10px}.hero-art{width:90%}.disciplines{gap:8px 14px;font-size:20px}.project-row{grid-template-columns:14px 61px 1fr 19px;gap:9px}.project-art,.project-art svg{width:61px}.project-art{height:65px}.project-title h3{font-size:26px}.journal-illustration svg{width:100px;flex-basis:100px}.journal-illustration .handwritten{font-size:21px}.journal-preview{padding:25px 19px}.footer-top>span{font-size:17px}} +@media(prefers-reduced-motion:reduce){html{scroll-behavior:auto}*,*::before,*::after{animation:none!important;transition:none!important}.is-drawing path,.is-drawing ellipse{stroke-dasharray:none!important;stroke-dashoffset:0!important}} + +/* Keep the reading copy comfortable even when the editorial labels are small. */ +.hero-summary p{font-size:14px} +.about-columns{font-size:13px} +.journal-preview-copy p{font-size:13px} +.journal-preview-copy .preview-disclosure{font-size:11px} +.community-copy p{font-size:13px} +.footer-bottom,.footer-bottom button,.license-note,.concept-nav{font-size:10px} +.work-footnote>span{font-size:10px} +@media(max-width:800px){.hero-summary p,.about-columns,.community-copy p{font-size:12px}.journal-preview-copy p{font-size:12px}.journal-preview-copy .preview-disclosure{font-size:10px}.work-footnote>span{font-size:9px}.footer-bottom,.footer-bottom button,.license-note,.concept-nav{font-size:9px}} +@media(max-width:600px){.hero-summary p,.about-columns,.community-copy p,.journal-preview-copy p{font-size:13px}.hero-summary{max-width:360px}.hero-summary p{max-width:310px}.project-title p{font-size:8px}.journal-preview-copy .preview-disclosure{font-size:11px}.license-note{max-width:330px}.work-footnote>span{font-size:9px}.footer-bottom,.footer-bottom button,.license-note,.concept-nav{font-size:9px}.hero-summary .text-link,.about-body .text-link,.work-footnote .text-link,.journal-preview-copy .text-link{font-size:11px}} + +/* Motion control stays beside the first animated illustration. */ +.hero-footnote{align-items:center;gap:18px}.hero-footnote-controls{display:flex;align-items:center;gap:28px;flex-shrink:0}.hero-footnote-controls a{display:flex;align-items:center;min-height:44px}.hero-footnote [data-motion-toggle]{display:inline-flex;align-items:center;justify-content:center;min-height:44px;padding:7px 0;background:none;color:var(--ink);font-size:11px;letter-spacing:.2px;text-decoration:underline;text-underline-offset:4px}.hero-footnote [data-motion-toggle]:hover{color:var(--accent)} +@media(max-width:600px){.hero-footnote{align-items:flex-start;flex-wrap:wrap;gap:0 15px}.hero-footnote>span{padding-top:7px;max-width:none;width:100%}.hero-footnote-controls{width:100%;justify-content:space-between;gap:20px}.hero-footnote-controls a{font-size:9px}.hero-footnote [data-motion-toggle]{font-size:11px}} diff --git a/concepts/assets/concept-c.css b/concepts/assets/concept-c.css new file mode 100644 index 0000000..b457927 --- /dev/null +++ b/concepts/assets/concept-c.css @@ -0,0 +1,352 @@ +:root { + --paper: #f4f4ee; + --ink: #191b19; + --muted: #5b6059; + --line: #bfc3b8; + --lime: #cef948; + --blue: #4131da; + --pink: #d893c5; + --panel: #e9e9df; + --posts: #e7e5f5; + --display: Circuit, "Arial Black", sans-serif; + --body: Studio, Arial, sans-serif; + --mono: "Courier New", monospace; + color-scheme: light; +} +:root[data-theme="dark"] { + --paper: #171a18; + --ink: #f1f2e9; + --muted: #b3baac; + --line: #444c41; + --blue: #cfef69; + --panel: #242c22; + --posts: #222524; + color-scheme: dark; +} +* { box-sizing: border-box; } +html { scroll-behavior: smooth; scroll-padding-top: 110px; } +body { margin: 0; background: var(--paper); color: var(--ink); font-family: var(--body); font-size: 16px; line-height: 1.65; } +button, input, a { -webkit-tap-highlight-color: transparent; } +button, input { font: inherit; } +button, a { touch-action: manipulation; } +a { color: inherit; text-decoration: none; } +button { color: inherit; } +svg { display: block; } +button { cursor: pointer; } +h1, h2, h3, p { margin: 0; } +button:focus-visible, input:focus-visible, a:focus-visible { outline: 3px solid var(--blue); outline-offset: 6px; } +::selection { color: #191b19; background: var(--lime); } +.section-shell { padding-left: max(5vw, calc((100vw - 1370px)/2)); padding-right: max(5vw, calc((100vw - 1370px)/2)); } +.skip-link { position: fixed; top: 10px; left: 12px; z-index: 200; background: var(--lime); color: #191b19; padding: 12px 20px; transform: translateY(-180%); } +.skip-link:focus { transform: translateY(0); } +.site-header { position: fixed; z-index: 100; top: 0; left: 0; width: 100%; height: 86px; display: flex; align-items: center; justify-content: space-between; padding: 0 5vw; background: var(--paper); border-bottom: 1px solid var(--line); } +.wordmark { display: inline-flex; align-items: center; gap: 11px; font-family: var(--display); font-size: 25px; font-weight: 700; letter-spacing: -1.1px; line-height: 1; } +.brand-mark { width: 31px; height: 34px; } +.wordmark-dot { font-family: Arial, sans-serif; font-size: 11px; font-weight: 400; margin-left: 3px; vertical-align: top; letter-spacing: 0; } +.desktop-nav { display: flex; align-items: center; gap: 30px; } +.desktop-nav a { position: relative; font-size: 13px; font-weight: 600; padding: 13px 0; } +.desktop-nav a::after { content: ""; position: absolute; bottom: 6px; left: 0; width: 0; height: 2px; background: currentColor; transition: width 180ms; } +.desktop-nav a:hover::after { width: 100%; } +.header-controls { display: flex; align-items: center; gap: 20px; } +.header-contact { font-size: 13px; font-weight: 700; display: flex; gap: 24px; align-items: center; } +.header-contact span { font-size: 24px; font-weight: 400; } +.control-button { width: 44px; height: 44px; border: 1px solid var(--line); background: transparent; display: flex; align-items: center; justify-content: center; border-radius: 50%; transition: background 160ms, transform 160ms; } +.control-button:hover { background: var(--lime); color: #191b19; transform: rotate(12deg); } +.control-button svg { width: 22px; height: 22px; } +.menu-toggle { display: none; } +.mobile-nav[hidden] { display: none !important; } +#mobile-nav { background: var(--paper); color: var(--ink); } +.hero { padding-top: 125px; } +.hero-topline { display: flex; justify-content: space-between; align-items: center; gap: 18px; margin-bottom: 30px; } +.system-label, .preview-link, .section-label { font-family: var(--mono); text-transform: uppercase; font-size: 10px; line-height: 1.5; letter-spacing: .08em; } +.system-label { display: flex; align-items: center; gap: 10px; } +.status-dot { width: 7px; height: 7px; background: var(--blue); border-radius: 50%; } +.preview-link { text-align: right; text-decoration: underline; text-underline-offset: 4px; } +.hero-grid { display: grid; grid-template-columns: 49% 51%; align-items: start; } +.hero-copy { position: relative; padding-top: 20px; z-index: 2; } +h1 { font-family: var(--display); font-weight: 700; letter-spacing: -.082em; font-size: clamp(90px, 10.9vw, 162px); line-height: .82; } +.move-word { color: var(--blue); } +.tiny-spark { display: inline-block; color: var(--pink); font-weight: 400; font-size: .78em; vertical-align: .04em; padding-left: 14px; animation: spark-turn 16s linear infinite; transform-origin: 56% 55%; } +.hero-description { margin-top: 34px; font-size: clamp(13px, 1.12vw, 17px); line-height: 1.75; max-width: 430px; } +.solid-link { display: inline-flex; align-items: center; justify-content: space-between; gap: 36px; min-height: 52px; margin-top: 27px; background: var(--ink); color: var(--paper); padding: 12px 20px; font-size: 12px; font-weight: 600; transition: gap 180ms, background 180ms; } +.solid-link svg { width: 24px; height: 24px; } +.solid-link:hover { gap: 48px; } +.art-station { position: relative; margin-top: 15px; min-width: 0; } +.art-caption { display: flex; align-items: center; justify-content: space-between; font-family: var(--mono); font-size: 9px; letter-spacing: .08em; padding: 0 8px 16px; border-bottom: 1px solid var(--line); } +.vector-stage { position: relative; margin-top: 10px; height: clamp(350px, 34vw, 520px); isolation: isolate; } +.stage-grid { position: absolute; inset: 12px 7px 22px; background-image: linear-gradient(var(--line) 1px, transparent 1px), linear-gradient(90deg, var(--line) 1px, transparent 1px); background-size: 35px 35px; opacity: .25; mask-image: radial-gradient(ellipse at center, #000 22%, transparent 69%); } +.loop-sculpture { position: absolute; top: 50%; left: 50%; width: 112%; max-width: none; height: auto; transform: translate(-50%, -50%); overflow: visible; pointer-events: none; } +.loop-body { transform-origin: 300px 290px; transition: transform 120ms ease-out; } +.sculpture-orbit { transform-origin: 300px 290px; animation: float-loop 10s ease-in-out infinite; } +.orbit-guide { opacity: .55; } +.orbit-satellite { transform-origin: 300px 290px; animation: satellite 30s linear infinite; } +.axis { position: absolute; font-family: var(--mono); font-size: 8px; color: var(--muted); } +.axis-top { top: 6px; left: 50%; } +.axis-bottom { right: 3px; top: 49%; } +.svg-label { font-family: var(--mono); font-size: 7.4px; letter-spacing: .1em; fill: currentColor; } +.art-side-label { position: absolute; bottom: 26px; right: 20px; writing-mode: vertical-rl; font-family: var(--mono); font-size: 8px; letter-spacing: .1em; } +.art-controls { position: relative; z-index: 2; display: flex; align-items: center; justify-content: space-between; gap: 20px; border-top: 1px solid var(--line); padding: 17px 6px; } +.phase-control { display: flex; flex-wrap: wrap; align-items: center; column-gap: 12px; width: 66%; font-family: var(--mono); font-size: 9px; line-height: 1.6; } +.phase-control label { width: 100%; display: flex; justify-content: space-between; padding-right: 38px; font-size: 8px; letter-spacing: .08em; margin-bottom: 4px; } +.phase-control input { accent-color: var(--ink); height: 27px; cursor: ew-resize; width: calc(100% - 44px); min-width: 50px; margin: 0; } +.phase-control output { min-width: 28px; font-variant-numeric: tabular-nums; text-align: right; } +.motion-control { border: 0; background: none; display: flex; align-items: center; gap: 7px; min-height: 44px; font-family: var(--mono); font-size: 9px; padding: 4px 0; } +.pause-glyph { width: 12px; height: 14px; } +.play-symbol { display: none; } +html[data-motion="paused"] .pause-symbol { display: none; } +html[data-motion="paused"] .play-symbol { display: block; } +.discipline-strip { display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 32px 0; margin-top: 48px; border-top: 1px solid var(--line); border-bottom: 1px solid var(--line); font-family: var(--display); font-size: clamp(13px, 1.6vw, 24px); font-weight: 500; letter-spacing: -.03em; white-space: nowrap; } +.strip-star { font-size: 25px; color: var(--blue); } +.strip-arrow { font-size: 34px; line-height: 1; } +.about-section { padding-top: 90px; padding-bottom: 100px; } +.section-label { display: flex; justify-content: space-between; gap: 15px; padding-bottom: 22px; border-bottom: 1px solid var(--line); } +.about-grid { display: grid; grid-template-columns: 250px 1fr 135px; gap: 55px; align-items: start; margin-top: 53px; } +.profile-composition { padding: 7px 7px 0 0; } +.profile-wrap { position: relative; background: var(--lime); box-shadow: 8px -8px 0 var(--lime); } +.profile-wrap img { display: block; width: 100%; height: auto; aspect-ratio: 1; object-fit: cover; } +.profile-corner { position: absolute; bottom: 0; right: 0; background: var(--lime); color: #191b19; padding: 9px 11px; font-family: var(--display); font-size: 18px; font-weight: 700; } +.asset-caption { font-family: var(--mono); font-size: 8px; line-height: 1.5; margin-top: 16px; color: var(--muted); } +.about-copy h2, .work-heading h2, .posts-intro h2 { font-family: var(--display); font-weight: 500; font-size: clamp(37px, 4.2vw, 61px); line-height: 1.08; letter-spacing: -.06em; } +.about-copy p { margin-top: 25px; max-width: 530px; font-size: 14px; line-height: 1.8; } +.about-copy p + p { color: var(--muted); margin-top: 17px; } +.about-links { display: flex; gap: 30px; flex-wrap: wrap; margin-top: 30px; } +.underlined-link { display: inline-flex; align-items: center; justify-content: space-between; gap: 14px; min-height: 44px; border-bottom: 1px solid currentColor; font-size: 11px; font-weight: 600; } +.underlined-link span { font-size: 20px; } +.about-tag { padding-top: 10px; } +.about-tag > span { display: block; font-size: 93px; line-height: 1; color: var(--blue); } +.about-tag p { font-family: var(--mono); font-size: 9px; line-height: 1.7; margin-top: 20px; } +.work-section { background: var(--panel); padding-top: 64px; padding-bottom: 64px; } +.work-heading { display: flex; align-items: flex-end; justify-content: space-between; gap: 25px; margin-top: 43px; margin-bottom: 43px; } +.work-heading > p { font-size: 13px; color: var(--muted); line-height: 1.8; padding-bottom: 4px; } +.project-list { border-top: 1px solid var(--ink); } +.project-row { display: grid; grid-template-columns: 55px 162px 1fr 160px 35px; gap: 24px; align-items: center; padding: 24px 0; border-bottom: 1px solid var(--line); transition: padding 200ms; } +.project-row:hover { padding-left: 12px; } +.project-number { align-self: start; font-family: var(--mono); font-size: 9px; padding-top: 5px; } +.project-visual { height: 106px; width: 162px; display: flex; justify-content: center; align-items: center; color: #191b19; overflow: hidden; } +.project-visual svg { width: 140px; height: 100px; transition: transform 300ms ease; } +.project-row:hover .project-visual svg { transform: rotate(10deg) scale(1.1); } +.visual-lottie { background: #cfef69; } +.visual-shabbat { background: #cbc3f1; } +.visual-date { background: #f0b3c5; } +.project-name h3 { font-family: var(--display); font-size: clamp(24px, 2.8vw, 41px); font-weight: 500; line-height: 1.2; letter-spacing: -.05em; } +.project-name p { font-size: 11px; color: var(--muted); margin-top: 8px; } +.project-kind { font-family: var(--mono); font-size: 9px; line-height: 1.8; color: var(--muted); } +.project-arrow { font-size: 36px; font-family: Arial, sans-serif; line-height: 1; } +.work-footnote { display: flex; gap: 20px; align-items: center; justify-content: space-between; margin-top: 23px; } +.work-footnote p { font-family: var(--mono); font-size: 8px; color: var(--muted); } +.posts-section { padding-top: 80px; padding-bottom: 90px; background: var(--posts); } +.posts-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 9%; margin-top: 49px; } +.posts-intro { padding-top: 30px; } +.posts-intro p { font-size: 14px; color: var(--muted); max-width: 290px; margin-top: 24px; line-height: 1.8; } +.post-prototype { border: 1px solid var(--line); padding: 16px; background: var(--paper); } +.post-topline { display: flex; align-items: center; justify-content: space-between; font-size: 22px; margin-bottom: 13px; } +.sample-pill { font-family: var(--mono); font-size: 8px; letter-spacing: .08em; } +.post-graphic { position: relative; min-height: 295px; background: #4230d4; overflow: hidden; display: flex; align-items: center; padding: 25px; isolation: isolate; } +.post-graphic span { position: relative; z-index: 1; font-family: var(--display); font-size: 77px; line-height: .85; font-weight: 700; letter-spacing: -.07em; color: #f1f1e9; } +.post-graphic i { position: absolute; display: block; width: 210px; height: 210px; border: 1px solid #cef948; border-radius: 50%; transform: rotate(-35deg) scaleX(.45); right: 5px; top: 50px; } +.post-graphic i:nth-child(2) { right: 17px; } +.post-graphic i:nth-child(3) { right: 29px; } +.post-graphic i:nth-child(4) { right: 41px; } +.post-graphic i:nth-child(5) { right: 53px; } +.post-graphic i:nth-child(6) { right: 65px; } +.post-caption { padding: 18px 0 3px; } +.post-caption h3 { font-family: var(--display); font-size: 19px; line-height: 1.3; letter-spacing: -.03em; font-weight: 500; } +.post-caption p { font-size: 10px; color: var(--muted); line-height: 1.8; margin-top: 11px; max-width: 430px; } +.community-section { background: #4131da; color: #f4f4ee; padding-top: 48px; padding-bottom: 45px; --line: #8274ee; } +.community-heading { display: flex; justify-content: space-between; align-items: center; gap: 35px; padding: 55px 0; } +.community-heading h2 { font-family: var(--display); font-size: clamp(54px, 7.5vw, 109px); line-height: .93; letter-spacing: -.075em; font-weight: 700; } +.community-heading h2 > span { color: #cef948; } +.round-contact { width: 165px; height: 165px; flex-shrink: 0; border: 1px solid #ac9cff; border-radius: 50%; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 10px; font-family: var(--mono); font-size: 10px; transition: background 220ms, color 220ms, transform 220ms; } +.round-contact:hover { transform: rotate(8deg); background: #cef948; color: #191b19; } +.big-arrow { font-family: Arial, sans-serif; font-size: 62px; line-height: 1; } +.community-links { display: grid; grid-template-columns: repeat(3, 1fr); gap: 35px; } +.community-links a { display: flex; justify-content: space-between; align-items: center; min-height: 58px; border-top: 1px solid var(--line); font-size: 11px; } +.community-links a span { font-size: 24px; } +.community-section :focus-visible { outline-color: #cef948; } +.site-footer { padding-top: 43px; padding-bottom: 22px; } +.footer-top { display: flex; justify-content: space-between; align-items: center; gap: 24px; padding-bottom: 36px; border-bottom: 1px solid var(--line); } +.footer-top .wordmark { font-size: 23px; } +.footer-top .wordmark span { color: var(--blue); font-size: 27px; } +.footer-top p, .footer-top > a:last-child { font-size: 11px; } +.footer-top > a:last-child { text-decoration: underline; text-underline-offset: 5px; min-height: 44px; display: flex; align-items: center; } +.footer-bottom { display: flex; justify-content: space-between; align-items: center; gap: 25px; padding-top: 24px; font-size: 8px; color: var(--muted); } +.footer-bottom a { color: var(--ink); min-height: 44px; display: flex; align-items: center; } +.local-note { font-family: var(--mono); font-size: 8px; color: var(--muted); letter-spacing: .03em; text-align: center; margin-top: 25px; } +@keyframes float-loop { 0%, 100% { transform: translateY(2px) rotate(-3deg); } 50% { transform: translateY(-13px) rotate(3deg); } } +@keyframes spark-turn { to { transform: rotate(360deg); } } +@keyframes satellite { to { transform: rotate(360deg); } } +html[data-motion="paused"] *, html[data-motion="paused"] *::before, html[data-motion="paused"] *::after { animation-play-state: paused !important; } +@media (prefers-reduced-motion: reduce) { html { scroll-behavior: auto; } *, *::before, *::after { animation: none !important; transition: none !important; } } +@media (min-width: 1600px) { .site-header { padding-left: calc((100vw - 1370px)/2); padding-right: calc((100vw - 1370px)/2); } } +@media (max-width: 1100px) { + .desktop-nav { gap: 20px; } + .header-controls { gap: 16px; } + .wordmark { font-size: 22px; } + .about-grid { grid-template-columns: 210px 1fr; gap: 40px; } + .about-tag { display: none; } + .project-row { grid-template-columns: 30px 140px 1fr 110px 30px; gap: 20px; } + .project-visual { width: 140px; } + .hero-topline { margin-bottom: 22px; } + .hero-copy { padding-top: 24px; } + .hero-description { font-size: 12px; } + .hero-grid { grid-template-columns: 48% 52%; } +} +@media (max-width: 800px) { + html { scroll-padding-top: 90px; } + .site-header { height: 72px; padding: 0 6vw; max-width: none; } + .section-shell { padding-left: 6vw; padding-right: 6vw; } + .desktop-nav, .header-contact { display: none; } + .header-controls { gap: 10px; } + .wordmark { font-size: 22px; gap: 7px; } + .brand-mark { width: 25px; height: 29px; } + .menu-toggle { display: flex; flex-direction: column; gap: 5px; border-radius: 0; border: 0; } + .menu-toggle span { display: block; width: 20px; height: 1px; background: currentColor; } + #mobile-nav { position: fixed; inset: 72px 0 0; z-index: 99; padding: 32px 8vw; background: var(--paper); display: flex; flex-direction: column; gap: 3px; overflow-y: auto; border: 0; border-radius: 0; box-shadow: none; } + #mobile-nav a { font-family: var(--display); font-size: clamp(27px, 6vw, 45px); line-height: 1.3; letter-spacing: -.04em; padding: 15px 0; border-bottom: 1px solid var(--line); } + #mobile-nav .menu-close { align-self: flex-end; font-family: var(--mono); font-size: 12px; } + .hero { padding-top: 102px; } + .system-label, .preview-link { font-size: 8px; } + .hero-grid { grid-template-columns: 47% 53%; } + h1 { font-size: clamp(80px, 12vw, 100px); } + .tiny-spark { padding-left: 5px; } + .hero-description { margin-top: 25px; font-size: 11px; } + .hero-description br { display: none; } + .solid-link { min-height: 48px; font-size: 11px; gap: 22px; padding: 10px 14px; } + .vector-stage { height: 300px; } + .art-station { margin-top: 10px; } + .art-caption { font-size: 7px; padding-bottom: 13px; } + .art-controls { gap: 10px; } + .phase-control { width: 63%; column-gap: 8px; } + .phase-control label, .motion-control { font-size: 7px; } + .art-side-label { right: 6px; bottom: 20px; font-size: 6px; } + .discipline-strip { margin-top: 35px; padding: 25px 0; font-size: 13px; gap: 8px; } + .strip-star { font-size: 18px; } + .strip-arrow { display: none; } + .about-section { padding-top: 60px; padding-bottom: 65px; } + .section-label { font-size: 8px; padding-bottom: 18px; } + .about-grid { grid-template-columns: 155px 1fr; gap: 30px; margin-top: 35px; } + .about-copy h2, .work-heading h2, .posts-intro h2 { font-size: 37px; } + .about-copy p { font-size: 12px; margin-top: 18px; } + .about-links { gap: 17px; margin-top: 17px; } + .underlined-link { font-size: 10px; gap: 9px; } + .work-section { padding-top: 45px; padding-bottom: 45px; } + .work-heading { margin-top: 30px; margin-bottom: 30px; } + .work-heading > p { font-size: 11px; } + .project-row { grid-template-columns: 22px 110px 1fr 24px; gap: 15px; padding: 20px 0; } + .project-visual { width: 110px; height: 88px; } + .project-visual svg { width: 110px; height: 88px; } + .project-kind { display: none; } + .project-name h3 { font-size: 28px; } + .project-name p { font-size: 10px; } + .project-arrow { font-size: 29px; } + .posts-section { padding-top: 55px; padding-bottom: 60px; } + .posts-grid { gap: 6%; margin-top: 35px; } + .posts-intro { padding-top: 15px; } + .posts-intro p { font-size: 12px; } + .post-prototype { padding: 12px; } + .post-graphic { min-height: 235px; padding: 18px; } + .post-graphic span { font-size: 61px; } + .post-graphic i { right: -65px; top: 20px; } + .post-graphic i:nth-child(2) { right: -53px; } + .post-graphic i:nth-child(3) { right: -41px; } + .post-graphic i:nth-child(4) { right: -29px; } + .post-graphic i:nth-child(5) { right: -17px; } + .post-graphic i:nth-child(6) { right: -5px; } + .post-caption h3 { font-size: 16px; } + .post-caption p { font-size: 9px; } + .community-heading { padding: 45px 0; } + .community-heading h2 { font-size: 7.5vw; } + .round-contact { width: 115px; height: 115px; font-size: 8px; gap: 6px; } + .big-arrow { font-size: 43px; } + .community-links { gap: 20px; } + .community-links a { font-size: 10px; } + .footer-top p { display: none; } + .footer-bottom { flex-wrap: wrap; gap: 12px 25px; font-size: 8px; } + .footer-bottom span:nth-child(2) { order: 3; width: 100%; } + .footer-bottom a { margin-left: auto; } +} +@media (max-width: 580px) { + .hero-topline { align-items: start; gap: 12px; margin-bottom: 27px; } + .system-label { max-width: 170px; gap: 7px; font-size: 7px; } + .status-dot { width: 5px; height: 5px; flex-shrink: 0; } + .preview-link { font-size: 7px; max-width: 120px; line-height: 1.7; } + .hero-grid { grid-template-columns: 1fr; } + .hero-copy { padding-top: 0; } + h1 { font-size: clamp(91px, 23.8vw, 135px); letter-spacing: -.08em; line-height: .84; } + .hero-description { font-size: 12px; max-width: 280px; line-height: 1.75; margin-top: 26px; } + .hero-description br { display: initial; } + .hero-copy .solid-link { margin-top: 20px; } + .art-station { margin-top: 40px; } + .art-caption { font-size: 8px; } + .vector-stage { height: 350px; margin-top: 6px; } + .loop-sculpture { width: 110%; } + .art-controls { padding-top: 12px; padding-bottom: 10px; gap: 20px; } + .phase-control { width: 65%; } + .phase-control label { font-size: 8px; } + .motion-control { font-size: 8px; } + .art-side-label { font-size: 7px; right: 11px; } + .discipline-strip { gap: 6px; font-size: clamp(8px, 2.65vw, 14px); padding: 21px 0; margin-top: 17px; } + .strip-star { font-size: 14px; } + .about-section { padding-top: 48px; padding-bottom: 55px; } + .section-label { font-size: 7px; line-height: 1.7; } + .section-label > span:last-child { text-align: right; } + .about-grid { grid-template-columns: 1fr; gap: 30px; margin-top: 35px; } + .profile-composition { width: 63%; max-width: 260px; } + .profile-wrap::after { content: "✳"; position: absolute; font-family: var(--display); color: var(--blue); font-size: 74px; line-height: 1; top: 35px; right: -100px; } + .about-copy h2, .work-heading h2, .posts-intro h2 { font-size: 42px; } + .about-copy p { font-size: 13px; line-height: 1.8; } + .about-links { gap: 25px; } + .about-links .underlined-link { font-size: 10px; } + .work-heading { display: block; margin-top: 28px; } + .work-heading > p { font-size: 12px; margin-top: 18px; } + .project-row { grid-template-columns: 83px 1fr 24px; gap: 14px; position: relative; padding: 20px 0; } + .project-number { display: none; } + .project-visual { width: 83px; height: 85px; } + .project-visual svg { width: 110px; max-width: none; height: 85px; } + .project-name h3 { font-size: clamp(19px, 6vw, 29px); } + .project-name p { font-size: 9px; line-height: 1.5; margin-top: 5px; } + .project-arrow { font-size: 27px; } + .project-row:hover { padding-left: 0; } + .work-footnote { flex-direction: column; align-items: flex-start; gap: 8px; } + .work-footnote p { font-size: 7px; } + .posts-grid { grid-template-columns: 1fr; gap: 35px; margin-top: 22px; } + .posts-intro p { max-width: 300px; font-size: 13px; margin-top: 19px; } + .post-graphic { min-height: 275px; padding: 24px; } + .post-graphic span { font-size: 77px; } + .post-graphic i { top: 45px; right: 5px; } + .post-graphic i:nth-child(2) { right: 17px; } + .post-graphic i:nth-child(3) { right: 29px; } + .post-graphic i:nth-child(4) { right: 41px; } + .post-graphic i:nth-child(5) { right: 53px; } + .post-graphic i:nth-child(6) { right: 65px; } + .post-caption h3 { font-size: 18px; } + .post-caption p { font-size: 10px; } + .community-section { padding-top: 35px; padding-bottom: 30px; } + .community-heading { align-items: flex-end; gap: 10px; padding-top: 33px; padding-bottom: 31px; } + .community-heading h2 { font-size: 10vw; } + .round-contact { width: 76px; height: 76px; font-size: 6px; gap: 4px; } + .big-arrow { font-size: 29px; } + .community-links { grid-template-columns: 1fr; gap: 0; } + .community-links a { font-size: 11px; min-height: 54px; } + .site-footer { padding-top: 30px; } + .footer-top { gap: 14px; padding-bottom: 22px; } + .footer-top .wordmark { font-size: 20px; } + .footer-top .wordmark span { font-size: 23px; } + .footer-top > a:last-child { font-size: 9px; } + .footer-bottom { padding-top: 14px; font-size: 7px; gap: 3px 10px; } + .local-note { font-size: 6px; line-height: 1.8; margin-top: 24px; } +} +@media (max-width: 360px) { + .site-header { padding-left: 5vw; padding-right: 5vw; } + .wordmark { font-size: 20px; gap: 6px; } + .brand-mark { width: 23px; height: 26px; } + .header-controls { gap: 4px; } + .hero-description { font-size: 11px; } + .vector-stage { height: 300px; } + .project-row { grid-template-columns: 70px 1fr 20px; gap: 11px; } + .project-visual { width: 70px; height: 76px; } + .project-name p { font-size: 8px; } + .profile-wrap::after { right: -85px; } + .community-heading h2 { font-size: 10.2vw; } + .round-contact { width: 65px; height: 65px; } +} diff --git a/concepts/assets/fonts/CORMORANT-LICENSE.txt b/concepts/assets/fonts/CORMORANT-LICENSE.txt new file mode 100644 index 0000000..8918461 --- /dev/null +++ b/concepts/assets/fonts/CORMORANT-LICENSE.txt @@ -0,0 +1,93 @@ +Copyright 2015 The Cormorant Project Authors (github.com/CatharsisFonts/Cormorant) Cormorant-Italic[wght].ttf: Copyright 2015 The Cormorant Project Authors (github.com/CatharsisFonts/Cormorant) + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +http://scripts.sil.org/OFL + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/concepts/assets/fonts/MANROPE-LICENSE.txt b/concepts/assets/fonts/MANROPE-LICENSE.txt new file mode 100644 index 0000000..462280f --- /dev/null +++ b/concepts/assets/fonts/MANROPE-LICENSE.txt @@ -0,0 +1,93 @@ +Copyright 2019 The Manrope Project Authors (https://github.com/sharanda/manrope) + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +http://scripts.sil.org/OFL + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/concepts/assets/fonts/SPACE-GROTESK-LICENSE.txt b/concepts/assets/fonts/SPACE-GROTESK-LICENSE.txt new file mode 100644 index 0000000..d5666d7 --- /dev/null +++ b/concepts/assets/fonts/SPACE-GROTESK-LICENSE.txt @@ -0,0 +1,93 @@ +Copyright 2020 The Space Grotesk Project Authors (https://github.com/floriankarsten/space-grotesk) + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +http://scripts.sil.org/OFL + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/concepts/assets/fonts/cormorant-italic-latin.woff2 b/concepts/assets/fonts/cormorant-italic-latin.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..7048a05f297b36b4513310ccbf0d488b56f23015 GIT binary patch literal 38140 zcmV(|K+(T@ZTPI0T!#u1xT-_xE!M^sNgo5ej9=Ti9MK~ayGlRj#0fUKt zzS;kOT2dK9)3PTmfP&L)dY@8&AQTD`N~*Fkku5sLmX&)&WNMDt(1=CbkIDW%NjoH) zMl^Z3>2J2uOh0Iw#QCAnFx9TUrx|8qD6`>fBYqHsv1n+B&9Eev^TsXrpgUR2l*5Oz z2QNH2?%mW`QB8a^vDt_{aR&BY3-Z%L)sXF;?S=AfU(tas(oi_j8 zg7~W6Fb|^qbQgh*%NWVZ$@}yC-2UAAV)Yh`!NwRcLYPR74yzH1Fmgoxp`Zu`A}S(n z(PA`46;^_NE3l~t6|FsQRnOl0!0Z561GxZ!kWpbLqAV#Qo1ur^zkm8(``wWli4mlT zil!zK5CM$7GJx+4&~$dWyHs))xr^z=n$)5#ZPS*BH%*gN#HcE?SiuXW=nx$`Yzi}e zWB4}4oQVT7WShW=UUSTgC|t>a0@OAcJHP*f9qOaN+Xpx82U>5vuaL;)?3K zBivcAP-oq)$d2S3Q~3Z`e^E9>6<~Y(gb7>H&d=Mle-GVrc}S8aTlV6^Pf*dmpKuxO zQ}jvw?nt&DK?o|YewP|MK;C7qESOPf7iYhklxomv$#)+3*rfiu^UD;nL{A5d63FH}vQu=GJRhK1Z&R0tL9$)x|kWgxm0~=~lVuMai(w4HY zi?E{Ky+!Wy4zSAt)B)6?IkU4r`yi(b8TFfooJL& z3$BQTz}5iD1i*BF^Yf-^wf_O4HK6QWKy-k-7YKWK3)&Zmi>(A}L%y7gD$V@CaAy7h zB4$8Zz<`oKP^$w#haqUnAhCfE1c3xa1vsSzTGfCuk@D3+D-z@&o>dmvvaRZIg_3IATT4#U`uEGVQFN zzOAEgvRM-s>}_O7q#7F{VDC+2c`TOZbo@Zh+lREzR0d2HkK%sZ6>}^mK?^6f>KNZG z=kL91=<7$i^Yr;O*!aG?e%{{(wI8!wpwGRq=VDR&NP`=ZIapPG4YPs-K!S{gWNvVki}-k!+eMph^)flrm5& z!*p`2C5*M2@z!#x^_*@KXW0_!9fc^{#awP7SGsCNsimKVU;wrN7>K~h5l4q7oXn@D zN6>5wQ3FiFs!}C}j{Ge&C4?T3(;xi&+KfPHtF7}PA+Z!B7NfT5wzl8aPJAa;6Ov|m zQBqcmX1S4tUklxWr$-3zIhlai>uP@vvQx4`3bJ$Kk%0^e09aJt+>_3EDr8{ul_3JE z^XB#?Kn4J#ah3)WwJQJw?&jZZzI*e%%YC}wB$a%T24({M7i8p+Oh_RH*92n>63m4y z6)8e(|1aCR*fa>w<{|)tcMw)Qp~26~@&Ur#u^v7E0MRu7p{*Y)+~YCUvR`80Wqzm0V5{!IiHG)S=ZIZ!i6P_JJELKw z4tc#-1?8Q4Dan9v{L(WP8)G7!mznPt#|frkPKJ+YCLs9&%tvf9Z|S9@i3g%hk8CV& zu{s{fpZ1+%3L?aq41jE+MlF|blUH&vfWc*8kEkS_Z&=?byxBv5l2D`K z?&=_ET5+2iNdNw^A=N zDDsf!ddSxjT)wdlv04TPR0Q}&g`EMBy*%4P=BbwQZLq0Xu}Iy`q~ug0#$986JMN`l zx#M9WKp?Y}|6e~zzfJmMmKVq}MoXF3-E2WCd zh5Qgw>`3gffOm`N4kVLk;-iPyX*4S!V0amJp0upqF&M_J|p_5LAe!e93(=-=p>9_r3+Y-tO((>Axst!b$KI-BA3 zH=qU)pctkD-6mi1I?wYkH*yX%N3u25??Y4g4$A0KEO{(fokt% zXikQuSd_F|fAbqZb*nF(j8C}5J1>6J)h-ry4O1<3wtL;;Di=6tuN;zX&BqixEZ)kR z3oOf0EF1~?kGaEl@Oi^~x&h~WM{IbvngG^A2^6^7?aPA)k6xW>P?Hcl4;tYGC%4Msl^0vg!WDQmjyX=eEX+?mEv{o97(dvp$UH zh3)_-r#wA&rs&IuWyopkrjm>vWt(CEdbTmjl)3U9To@^HPOU&XZUV9LJcz2?l;+?9 zOr_uck;^M#{zp@n@8H3CB=4$}<*u~66r?L};xG|LIa5sB4%oDE6#@@#A@YzC-I&~l zQz@^fHOj)d@kM#t=57-J=>Pd8y`0Xi8>LQ9M-ne|ca4k;H2WAr`>DFRG!jQns@Dp$ zFL9o_s^8$y^v8*O{c83Jtv){d6}>bFH?}OrTx_z9;UJ3}U2g5|zYQ9ID7}irBpYRl z@;lqdU{f@pFW9)gyxE-F)B@8l{5;gWO-BHJTK~rk*)>geZ@nMeblgfZbRWfKrE?l$ zE#CC$9VJn1Z7{8o11Y`ZJ^nVmHPn96I2CBN4FS>f?g+BEod)GgD><#tIr@Y~f0CA^~W%58WCfCnVj*?^P`ncOlb9#Ld zS|+9Bt+`D5wK@&R4&bqElCcZKK0S;t|F1Qy51`x>AS46w9i&aTvmzds?*d}k7y`by zo0{qN?}mHjjG|nQEzg?9)j=ZCe{)GFRDge5<-?qgW z+3!uRlYf5QaIRnWmoOde-UaoI3iDOU@;bC~TNnt#DdQ9=aqcc@(}PB4EHdfc#q?U} zy_!2AusOW7jMm90LSNZ@YV8jryTmQ0rozRh8l3`Htk+Rab;wt(bbhxsIX!$~q+=Ra zN^jgYaa)Z7J4meixU3Q~VbjN?jG9d}0DiWY9(?xSwx?9^M~+(L+kZ%fMY#}EQ$odJ z@Jwb1;I`T_Wvxw({Ep+|54n>d6dKsxLpatCKtRts*Wz0dt(l$Rkarz$5fx~rAool3 zx>Lf<)DIbqj8x7r$r_(XZn{Q)qPMBYCw0JT$2DmOl8#d9d@-0OOk_t&?k@fSwB)9TBSpAxg|w5- z$G6Mvb3))hhB8O)#SAj#wj`IPs?3X%DxLd+jEpSz0(}afTt(|)B+(u^ila`)OWZ=8 z2DVE-sE?Ltb{>Zkr6tZe^KQ7=LP7e_FN%O=$@)*=?~WbqhN zt{P)5KTQRab5D!TbB#4hY>LICCVkEjrT`0gw$QdCJWmLM7zw)QVSpjVIE05J=2&2f zH8$AdfD>-Ga$$Qve)_7kkA-w$(`TomNj}qq8uGe`?-F1W^RXGN6cV1#L&N#lP|4hEsCm88 z;;8a(a)f$zvW4MyIJ{w3E@|f9)ByJy&V3GfUuXOdHYC}C1h+R1#ORp~1 zCgj)%#p?E%dIt{+L(f02j8dsSMVfdK6 zCfZ(ri<1v?MP!!y$*i)=$$ss?X=X33y;k1&#N{{NSW3JUvh(U-Uv3I5Yv26u2_Mso z&5<$Ag(-8@@>RRIJItk?SXXxaPxbk^f~tl`QqiZmT(3Qp9Uc=4#!K0QP5<_5Jlb=STFautp~_! zU&S3aMh@pfy4?N{4)0DnCMNdvn`^Z{|mhQ`@w_ z)ckyDbbh?m9CLv1B)nNfjK{bS1uR%&IR4uPw7f|9@TU-D2$kVRus6yW4x&uuY`R1N zGGypaFUtT6R2ZaMt!e5t(%EX8Bs=VsVvqgO91`jsVV2`IZ*;4g?=ra1HRig{MP4#b z`=|0VlE>mSYAMJ;G758k1uJsZL{T1+u{y7e;(R{In*7YBqzFTlH%dN9UE?jIzR6b6 zSgK@fuCn!Pt_IZr^c_f0;J^YB*h2s@81U8n(^wRQkHiqdxW7UX`7tn$ zEf>B_ZZ|641mt4r8{wi6QEmVqMu&|`>r3R4QBgm0>;-6sQQ6If5xO7t=!f56gPUr& zA>@LB3ViS(y3B`7sw-eMzo;f>*@qHVtQm4NTw{u-P^(y=S`vF z8-+qH5RK}O18LDl$^-}kP!JenKtKQnCPT)#G|ZyHmDgX2Tn?Z>fC8a{x)db@SVIQI zt)z7}LC^u1nY8#oU@F4Sp2(ii#9sSQT4-WFX@^KTLfUasPSe<#Tg{wxj#N`T6PQ2^ zwOU=^!byQSEXC51NvBt> zunGPXEN`sVz~Ig+R$Z}t#nKgvS1epHf5lwTiu%V)%P{y<>i&4F6IyE?kF;A1p0O5lvWZQ1C!K6hpTk&xK@fS>ERVtmA;^&U4|U0$KkfXbkk$*?xwcY-A`gsFOrPn14um$Yv19 zh6vvgEt=I?uUxT0i|LVL&jI@!tkKz^!5Q^VpAuNalVJ=3`0JZ@UV7-3OHRS(Zp~^` zDpnv*woJ+9m?lQFQHF>V>^Q8{C1`d+l~b}EGv85qCnY%|(KUOVx7lSndneu@#8H>J z`iYCmc2LcCKQ9}Fon2VPv zNt$$-{-ZZvwj8B)*`1xKF4U?`yAGXt^y<^!EVf(j(n!8;9t+QZujXjJR=9juk!TZ4 zG|6N!V#S%J%+^N08k?<3ep4${J6d24h-}fHgwr6(2U{#)uo0W*tELzN1CSs%P$B4Y zCILf(01n%k2tdCo+!eqcM*JX?H*PW;XuPPTguAdu+e~Fb=a!C%%QV9}%V=#eS~NL7 zzEHMAaYhN}+3Yk0kWRA$gmxf6TVSvcP-p`*S_22IfQy#ELpAWx0x+5b5t((5j0+92 z$0NZd#o>siMe#=pMhH#ZGZr!s7=s8z@-nUec|VMFQ#(K#_ZyKF-@vFp z>Saty!zjPh6Hh(!+zT(YX?N2tx3#+Cu6yo#;9(l$LrF+`U-=E<)RCRjbX&C{rs0zf zHrv+V!c7lpL`Coi8W5=Xuu13R&z5o@LS``{Wgl#4Uc<-pbf(kdoy;6_1V!l@nS5Hj zozj<41;49XTHSTO<_2Z7yE&S_OiXeQPsmA^`;x5TTtU^{(dXwBv(fwbFk8ev%t|&2 zfF^;uWk;~e8fBG&)#$biYFp}T7a@%_O7(R3tDCTpMDMk#$GnB1hS?F%8RWKauN0K` z|5D17R;USPNmFEf^YT&PK-na1y7)1}XbpQgCPQmbkq_DdcrbE^XA+dg(gsj%a$LxB zss896SQIE)3t@!iD7BUfAO8cVW{euia{kI1@nkCg$AXRV9XmQs#Fi1 zHs8fAacO)2AI~8}*ss@#3x6f!kRwlOVPe%T1kpv0E)h#z5+uJEWsf+u98u`-(1Ggs z*QvD1Vsnh(#0ElI2bbNu*us5n#aPIpppeD#kD3QTMucmL6Y@tdhDERta)}yJX!TS5 znf~}*52Gyw(1QVzN9!N~w4>u84phF{7x)-LmfoQre-ARKK)|U)lP=vRO}q3nRoGI( z2ZA0Tg(surPPXnNlfs{mwy9e70RZ@N^;Jdjh59nn&%6eY?mu}6ZejOopMr3w8KpVM z8h7&7baxHes?}MJUrA>H+ywgIkk9lkVe}xqiLbD|r2+|| zbxtpnRNo<*cKZeY92HwbM`MTOeC06UZbo zxlAoH$ueY4_4ak-#Oyko*Tw%WH~|2J)Li(gaUfSCxj=CZAm#|}kt<|8ndqA}%F@## zWCE~BZe#hR^uG1@V@Ki>7ohjpwq0#&w5{H@F#tf@(rpX2?P**7y99-PBeo%WmMbc)BDVj5JPgtH*-?1ya& zyR20fY!#~T(dC~lb?P-0V!3vERhewH_14&6g#|8gEY}-spBWap(rA0+$aSfsH8`UY zHyWZqzC!hIkSIJfErN8!zrH!c&A$VZsgKtJ35%C|j&arQ=LB z*DSNm@!xseLpQ^8ORZL9r8QQSK(S5M+hC&xr5brFsmeK^7&pGiD1CdueQ=w&B=%ay}3fjsCU1X(_qLR1g&5D)Q?sr*+lG=f8Q`#Q+*_LoSCe1L@M1km6D z-=UZZ%-5qr>E*1ElLl1t7Ezk)0>Z4HMB+B&XmS8=*CPvURxC`8kF{&uG9B@WQ&1@~ z?bNJmgglxRZTRkb=W@24;D^4}t1r(1NKaMePrXY%1-YtGJX)m5ICzpt>lkS)=Os?|<{NF%B!j zfjtBnG@9NvFnNSo(Mf|8G9c20G64HFUSe|MdjTs$6!1YL2jzf@(88{eP|2z+O@)XA z)N7@5lIG>ak2qb8Ee@M*XGk)V0SD!+)(MJVH`(y}U1mG3qPmev9)fOsvfD(ll< zpb2#6!diuG6Fjf6Y^ufeSw{BPcdc5XW9zO{2s`9fLmE2Z7$=;^IW2lfx2deqgNT4E zZ?r&E7=M}Kpovs$3s4_5D{?nLm(gB^f<+cEG~VnVu-hwS-{Gha=7te4S~(2P%o;)z zQ;Z`H(8a+q7pFtUTzUYOZUHC4lRIqOs9&we5o)u=k`fl0`QVK}TPP^0dk%6qWO2OFC3jGI>$dKU_ z%axO7dctx7pN-vLly{8Q*L8pHA_CycMYm&fqLvuGe4=HTM5T0l|!Hqmix zrOPsaP7$Vv%Va;6?KF>~%Lu8)`W2PA@Upc4OJ`H{vEsob;p?^huGWmgaC1xx(ku)3 zXP>Qx6Oic)*^SOZ;Qxi!uXomycJ_D)ZD9#fUHWB2c+F?g+O*|ZU?Gy<5?(vW0w)(X zuxOLQFcPR{LC-MHXW!%_dDx{$_w*z5F~yPJ6}4Q}dF`9L&Nbu)+44~Cv`S2LTR419 zGegA}*UWhdEaCVPk2kG(<{ckn)=yXM`i;|z36ZnR(oBU%rA@~oyCV${ zmi38=Xhgw)pZduKrxO~Z`IlS!QL)*X-(%eW5RG&WCliM=d!D4fmHBoQWLLXePG)Dv z4pwyj2P_<~Zc+;J%*-i4#nN0I)dG!NO<2zU369M=wL;~Wlg#T{)_@wX9!;CKC)&YS z)v%3_z&S~jGmA7~(lhbP(!aA=YuyH!wqU%@n&>DgrO#Q|15!IX>@E^`A&f;)2(9E$DTN;_c8EkvVbB0D$~`Y)!NpjCMUD&_i%7YjUw-Rv zqQBC6)IG_m$q89fpWIZCXd{7AZ8|%uxkxs-e|vRSJw}$@W4~}DvrcoQid=0A)Gywg z)06PYYdWiq*(KS*jYS4grKdbD&QW8h@Jz3r>fN0SUX~uMmOH^`1gsUd)De**t}(_j zuIXFOWSD5pw!>`IA)i`8b*`#9_XX!Rn~eBGO*Uy82E{#<+I+5X3ZZzCp`FPuJV(0? ztJ! z+Ho*4v}MMj;gQjM&Os9L+7z8pmeY9T8Y+8NDqxRc&~Sk)q4_F`=bf0Jkp@+>;Y?hQ zEHZ7=hv-k#1@geSoGwb|hMm6RE|wlNjQ2u&zD;ZqFMb-5T(6CR4x|CwRNVPXZ(P5( z4Z1zs9gfW%><-S?DT;Tky4@jB^EC98YjSn9zf5&jY@J4saxO!$C1P$O%r6;^u1nKq z9eg34uQC1_=w@+tY}>7e{b4MkHqe*IXU*XF zt1LUp4vCYD%A#pA#Av$%llIrj@I{hqoGHgUt@u@S;z&`Eb&bPanO=1+3wSkSEWdc? z{nEH~y{isKAmf^N-ZI6kNC_%k?SLphkyskX&o8*m8Wo!k z5tCC(RCIj2#!C9!01=rgP4O7Uuh-Yd_%fN|`aemI^=T&W4O50%Ye-}?rJ!$e9FqLh z$&j;_Fz=i$jKnt6mRX8{Aga7LLcMHW(-Ts+kuXTeQLqkLSm8q$g55wykoJGfDhUOHRD z{V5Ae5EoB}fsGDyX1$DLlpa}+3;*WKe6~MHx&5qGSfrE+E25vB8jI3bz@{?^DxS&X z`k^5Fbctyfiu3$arADw~$`9|HfthBX=K5>uNFBr=QwYcy`d-M)zY5$g=qDgyt; zq_doaCTr5-Wt|5+XJ6*q(hT!pYTmdZeb+cy2#Jeqhpo3kaP_ZN>7RcB*CYjNi$t|d zgY3y6-{qf8$N}Aqr@S4l%WHZHhJvw=L1kR6Gru`9M=_k;U3oACR-%0K-qJf=-*CR3 ziP=`&;Lw?6@HzAW6e+UjytX-ZEhRe($K^H7I~`*tKZ-oNJ|Y@_ zn+=$mjAjQ|RM#;shM7A}Cu_sW*pY0S-BOmCaa_9Qrzo;WS8<4p``)pA&Q$my-b*yZ z&hRxNWhL7HnR19%M;e2MjuJCE##R5#xLoJ^_QX(8b3lwDwVUvfI8X>mR@<9Gje##a zHX5v@B)q0bO7`jpleo1zM*0Vy1OP(!E7?-MZ6>!i&`m0>F5@mg^;+H@hj5YMh{iFT zJjS*@nGLx>QLn(^{vuItEeUN*EM%dZcxM)T7C)~MMc=YVmsH35<4cUvWHk{SrrbE6 zczV;9XmxkBjRHL~6vso5iSx_v`0A|o{CGHT6QC>apouVcBa%PqJeG4<;~MNk1%xan z45flvsyk;at=r^(eXKNV))bC663ba7#UZlL5~;Uzgf$MeVG&m=Z<*ukNC3>-4zWMs zPTpIVIzMu{)+z*LER|*@%uB(Y%%_O6 zq`3mcy%3__tSmmQ(9G?^6DuQOn6qyOPyMbBdY zJ$yXljLWuNKRonzb{B=busVB;mR=mw+typbnq1DFB$>CSO*uf)QFro1&Ek+_3KB%f zL@5p_&xj@*35EMPzLP#nP=lNG%ot}yBU_)L5EzYrlco_8>bnW1nsd+ry-=Dpv%)bx zL<}_^V(Qw9>ZN>JzGyWWLA8oQ<<-$~r7oz>lKEC$$4{c)swVqVpW^NGOG#=HvRl0Z zVq>DJ{Jn}$CKpZFx%%wuRu!9_<+T_t+WFn#H%Z3VifD!k15i}HdM5a&{XlRcF}**S zH~VSofl3>E<@Z^s*QRx1>mmaNQZ)E8i1WASMBgVr>Tz`-^LMFqkz#hA;<~o4BP42v zQ4Z_zHS}FG`C(`*XFGR*gl^+%no5b7$xS}2s(W1rDa*#?kQ!V7J#@^H`IAe&Q>9F3 z^U<=*-~f8KBw=ya0h27CgfamQ_0-vjHHWmWfz%Gn8q_*-af_uaVh5d|>7p&k0b@rF z*9KtjQVayNSS}SYO&?!G`oYIj%6m_rhEKGMteDB=h#k+qn##7YkEX#cxb>|5AJOm3 zK>83kJkGb;1MW9^!`%tXDRnF#o4mK|>$Qtm*C{{o>`mIs{ki7qyL#Br?2ic(wvptD z?vl^~nyx)l-t^6TXnQTS)I)zG+1x_L{eih!G`j-Mtt~=SrSq~o7AR$wWMwPh(?Gsyf;190X!k))NW10U8LCnL1gAM=! z65?^`mkO}(wU`xReJ{WyGSsU>Ju!!WhkL4T)968}I=9CGqr|)O6j${_tI3f?NKja7 zb#B78TN8@02d;bX>?)qqOXsaNkG%eA5-s7nst)gXMxTO;3+|F@!ES{q12!DBZ?B`m zUtMKf(R57_Sk^~G{Hq9@8w+-gK#Eu~S;l|!`AfN6 zV1&Gt`$&);(M<|rs+Xb(B8fUNUZLIJHaRoR1Zk=!=u{W8dQrliMfqsW6nY0}=qdgj z`Px4TrYl%(LrJW*aW&&_$+i!|dG53?H@{)GkKCfS4&G*OHoYX{Pn;aE@+MRP`h;9l zEOGTDNBN1|`iA%CA}GD07L`ar32Wj2iE2#e%WHy;!rQd7{+DYspu%7d)_RmiI4 zea5TzQ)z;`v>SKESLMJmj%IMYLa{|cB^G>rpBf&Pkbo6;cC z6t5JFGIaep^i)zfGU7xdT8cmL|7}ArY&;uvL+a@7L|rrw%>Np(+_&KzU-=92I{%P^Pt_6;wjoUU&{qXNfHjM+)o|9u-c@~CuF;y3`b!caJsb%6#`5(oBI<#m zqQbE7+mJ(Tt7obgiQ$5bFQavmlIKaNq+9mquKPi&pzf?H;tMB@y1@neCn@6tcFhf4 zVGJ{AuYIF~(R!(2c~cz=tlyKwl^d0cYjXa=9n>gDy%%U+f#H2pKQL@s_zoynkO`bDJ|jx!mEC> zG^U0t{6|CUv-q1X33Hj7Lhv>+$BK=1qY`a6T-EJN$8Waz+&IDkKNktU2*A?1nZ`0J zBKNQ3O(6`hAMCPuOJ#`mU*Bw9hx{v>UUoh;AVrw>#0YfnrgJV-?n)Mi=fVr!$&99X zX7@ZTZ*j$jsx@1f-DO)P9?9ZzYM#Wsg;6I)tV60Y#FZJ^60+oHnx`X@x}WB4nTWH@ zKVO|$n3XqIo$ks66|qI>3Wa$S_gr{R;Sd{#?FP z0;6PRzCi4_ANl+O4y3^-%>SPwFZk>w-CJrHh5KT8L`QiNxEJiJ)+HpqFUm~2nh{}MqvCi5?5NUvf$6kFNH|6;P}48#qyaun9(Cj$Oq2ikb0i z*1&6pz6JkqF4Oagm$$xb1w)bW_RwqiNLg+{QLGioah2r)8-H55I+4`)5vdTcpapFi zskKP|cGT_eYs?=^%GBoJwGCWKbPGg*;ZVgwucOoMAEt&-4`aeq6-i0W=3(k zU0pBbe-{W{Z7tC)B{DsatbT-1hI$`54UP9{56O?JC56i%*fNQw_yMXJE_`YrbZex{ z(0?*UgLSlvVGLxk-&N#Y&F#Ey+P^D8CrK;B8b5;gU9PkMJ_A|0h?FwGhtw}F&~K!d zq8{&Xd(l#zSL?zfMKRIQEf4~Op0L-OH)78lp@*@Cq;1^=;e^^>dN(9iCX?x~sSx;? zq>`;wZ_ZhDG%nc@zEDL6kO0L&k=V}s8YoNnz9M|1&1X2Blw}Z$uxX2N65nEph$D1k zG{+EO%0)si6Z%Ty{|Y{~9ejgo~>XwUS_+}*hwaWj1nyaq4XlOJ*T}4cYQ@2?h$7I*3?>^RleRuPD zwbK~Tf1_wHp~+N9{u@Y3niHRv%j}q@i9^E2MUMbRW>t*h*)&^+Q?rhMUIs@n8F;H0 z5mRO6{I%6x$=nNdv#TF_k1DgHzDYqzWaL~|+t2fQ^F0PROyRtYrITv!l{()jD-BPV zSd85~{EM6eF8P=SqM)*+ zx7Zyo_;ET-d6yXk`o2qfq3D3-_s)ap`mX4T{}b~n{coix>!ECBv*(C@_>I;wB2suvRczboWsErCCXQo`bik@s_SQ%sD+ z3H`0QzbBac(&*Ef-Z6XO6upvGOsuQOzx63Y&aw($&omQi(o&|jGKp1u7mWU=(KC#oXUm zo^1QkZd*a6F(o?G9sffh>km1MLL5RW>bb;N5c1B&H!WT;5!&QhsD@EkrGJ=jcVr?C zYADreXtFn=_sHL;}x$ z@p3HE3Zt<2pKDIq{I3Ed*V?~?cT22`C(;6yNkA#W@uNg+G*|_1go(yI!uT}T#;#az zl0DkNVQ%>HZZ#*_64L1_5JzfSAU9S4q9FGTo)}<5D5Uqb8Ra9%Wx09ndPhb*dv1LJmFy5E>C3u!(dH$8XI(#oEc^k3Tk#w11P%gUH zg>QtiPCvC?2=0ZJj$TO;#vf}A-3V0PCu&j!Q|#pRI2eUloA{hfjKIwF)Ov%M8uWCu zrrGR88L~FiyC8dX#&}EO*hMlsGx{^lp$Ck7)^KPzlHb?QXaFFfN7VGX;E}*0nB?}? zMzTls?LYp&KuL+ZYt5KOG4D2KO61q3|Bt?;Opuy67-+x%1xky6t?Bh-+%X%YKO!um#}rF)X=K=`uKa>_uWcAXA`bV zb9xB&l*ux){qmyM6mrk%gv9V1u+?+HZOj{veSC(uuPiNC?Y`<*onOA{#IV%sF5l^S0j! zM%9dvJd}ZHdEB_j&}@9*6HoqIeke3Qu;6VD*qh8T z)+JgBX211{YZpb!Biebq=_0>Hy-eX*BZN>$owZ#`<+P@#na;z-I#HZ9(eks+aasqS z#&@&3f@HRMK&kxFnnZ9flt{7h^~EHeEf-XX?saW=thZSGo}QU_tvRw-ZX2sZc}p4g zB(XHF4&U1Z?gclsJo+^Wmpu5+A1RE|(r9Wg1f5E+rEy~~@@AAwMH`*T!6$QL1?lsB zF8xFB3^;u&=}SOIq`QHzJ$^> zy`R$>cfNv*^v{qS{B-miYhH&wAU?Zvz zwzR}mfA+P(q93sshDbe&3TR1$U2m>HYy-i!r3tyVi~(BEF_Y9}%MB#P=@KGTD{9TC z3)u1qU?&>i^aPaC_lcvZI4PbtSt_NxOhah7@!N$;jl?!IkJbkCs_ZZ^B%ccqKr_l) zo!Gn8RO`T?LbR2qmE)HZ11Ug$L1UPmRUzF z#D9(5-&b{#iewZjR+?C8z;@JdnN_w7^|~udl2r12pYWGZedu=mnm`BD_)URvVW`8n zn!XM7qdAVoQlm|mQ-QbA^3@fd1xSzhVl1EP1RkD${K|4`xMq)lMM zBNjIO&GVnz8l2cBp)lyQTlu?DRE(FRrN){x_hB(Cgj8Ro(aMOyMCXkJc``EzL8xc9 z{mjrxuN=|aoT|9EZ|Js_YmGLCa#reC z7moFr30d)I9pVpne--tngDoP{Y!yVC6#~BD*5GDM*JocFa`dCVfs6r;sOiWwujk0S4OWMD2?GgH{Zao zJ_Cm7XYTYE|MKy#FVlhJ4rZKMecdrHK*ZFJ<~w4=GEaWCWh1k(xK6KGYm)lV^Mv;S zVI2hRO3{9~OBfV|yxhsCNuKoTp)%zHG7_@VMXpN)!SjT5%qg)JZqb6VGR31H=Sy7Z~FuljDJp8<^ve>vS(dR*I zb#N}v`K=Cv;Z-9e*#}yIBdS}$QUgAjLRZe5gc!!e@q~dy^zP|m1U}FY!;iq?JG#K1j}Ej`aV1n zoo7p7E>>gky{hm;{)PpLRf|L zrE%o7>DxS!!sNt*hBAVW+ZIp5#390BeU}>_$V_iOwf!sO!{&Z!hal}*F3>{WI4W+A zYf3Eqfn)~B)b=)hPhs?h2NA@QMHM2SIv_@au%1l74a4;|i+ms`IZNWqC2 zT75&*KMkm*c|Xufvd2oSW=(jVF;e=u!JoGPVr%UdVXIp45J!KcQctgVi1WxM;{yCA zPPiVQuL4M%xqr|l>*@v z$%5TJu1Sp#81a6FpS%b8#C{r;@1WUi6G^r4m1T%oF#q*1^8o8X@n7PSSjB?Dgpp87 zP$79n?|bvjmUM?nM#I)pxN^X2U6WXmY5i&Ix0qbdgSdx)%&rjF{#F#!IWI66pr$6O zkZ(O8;IT0b(SYO%5#&8#@o4`Z|Pi`eH6_xbY^>PazGS= zy?w~}@VQqIG{yD_R4knGob#uHdngt5zvpD9keDA}{Hqkok;h|^iI+no!~Wr<^<^7h zY0Vo?UROqAJ|JS>kEJ|*B$l%KG=TFMHoQ>GKD_wPs&wzds`LhJX-64j>v%N!pHA*_ zRFcNjLDx{QGgRm|K5>HlVfIfJ>Whb%`ythGp)%DeAENGpOMoawF_7L=cGJgZudy&N z-cjg0>w#;;N|bYI8DpOk#aLQX`AsjIZ>3zvG4^)SKIr|_7S?Gy;y~YEjNOb4`7NLO zqLbNT8+h9qSHY+MB=&3^l^|KpwYPzW}#q%O!t4w20``H8H094EH(3E~wAAI;Lb>R7Vb*20 zhvSV2XiJm9+sSPH5BV~uI$Ti0c=(1bZYg@H=Tge;C#GDvf#Y+fEA1@n*`5HZerfE^ z8V&2Uw1;=5{=S+Q4_U$-zH|s;aQs`D2BGXP%Nw=(&8ev|a-Wl~c30(hu&ZfhGGE=s zvq?CCc2S5!BCYk`Ej`VYzpl7I`$?-l6?=2h8CAyNESr-rH(XvI{uZHM-sR?|m#k#Y zC@XwxsNJQ^>Wa0;%7LIXzlOY=gCuNQl;(Tf=i~rcPV#qSdgFo8o?813O?Xi;W3+<2 zh)-0{QC$^tUql`B{4v`=`AxF-Y~8ra6&CwI4!;uh>}%YcRMjj?(AT%0s-i&cj?9X( zbo=ngA%n##d88rVp33}5ssE^3Yh1iQnbhzV8O@A?O9NJ`E8ABCKIK~2Z)putOxn+t z@!zM}dusNENqXE^_=f6_j&4b7lXcYf9#+`G2ncw+#@Bw));`l}zdUN6(zSxbgY0;2 z1ej1aoME%_3H{}`qz^0FTt`ayesRX~cC1id4ovJU0(@9r1ys*M5@r_3F9#>g4V0X{ zS5$HTno8bt^Iaw2o_Ta#l`h~ja1GC7l|Nlo`H4eY(7ZmW$P#$PO6XGVtFZBxUiW0D ze)i0f!(L4Na%|ZrYr6Wc7Rg`@4Xkskc&aH_o9vK%T|4Bl*fq_aKaefeISzZ;2e9a# zxb?3CXS}gs$IqFr1NKB^LD`g}9f48%Uq1#uE=rSO`>w&<8 zrc4tHWwK^dZG)XH{uqDs7hoxm5tgS&*=q_v`tPcRNtHx-V~DY7n1cRDXWiu_D8@3^ z1!h8B0f$8|^&MblulU^J-cO(g;jzStbQi&1#)8fSAyidHF(onuDIG)4D!#@Yb3Av?;iF#lz%E+K^Qvy2!UQmR9pF=_sw5x%! zuKUjpmtiBM?*rzWq+9+YPQZdO^2(h_c_2Df7sE?^{l zrXxuK;zFyw_!UEjew~ZR$5K-Q9i?&dZ;sp^nRFje`B8abp)Dh`Nv~Ah*a@A3(l+5! zP+n^k4U{Inw%&WGrO^G6K@NHH!S5E$F!w{EI?x4MH`rkxAY|$j7)AV+LQ{21?u;{z zQh!O7CCg=c{5v;XPt-i(Tk(USWfq@fcFjc5*iyxkJ5~8wht)76)Om4h!-ThSaS3q! z=p3c=&rDq9j&-+_szE)(Du8}&1Q!FvaGa@R2tNbYsswca2iN-yCGo8Vrh+zVxTvC@ zVzHdz7`ub9#pujkAHEsCS#cgY1|e1Y&7R_Vog#mJo2z272zUW4%+o zphzE2)F#ZXDcMI2++fy!YtptY-9dzn{NMk?o4{;mIPu;mk+xJt5m3h*2$bVWi-b9+Q&4O16d@-=XZNe?7ReXWz(dHI$_-g#f))-itWLw&N( zzH5WP7JR$t&HQG_#+PW71;rf>H$TN!AFKc;C9ku2_v={86 z0hbuFGs4-#sAg38X6qs;DfWDJ;?L`v@R-Kx*RPHk! z>Tm{&N6LY@ z@BG+4W~${C3TF7=j(irz# zToIGMUc=p5P7IRlE<%xFMk9;nG6O|gzy_@aiyH!UZf0ju8=x2Q5& zJiSEONCwWMDXt_*e9CUpcvk)Sf!)rWx|8jk+`S3%{oBKn0$1u8(+e#d=e-;HHs3Oz^#~k`rfk8AM8JW<6uk{;a{F8K6Nr5b$Y;AA#ThcJ z7cexA<83XI58^hbTw!(!b;$CF(ikk(Y`hJ1Py?Ck%X^31KpBXFrqSIw{$nGon9aHx zQ@i1RYg>o9OS{0g@PF@nNb&K065yZJB~~G>wQ;`HQrQ^Qzpf+BJKCmDg?7_I+&r0F zQ+N`!|7%-V`(b)M1SyJvTwb)@ZVaS6cR`}MW`Rya z{8fEyez>sR!~;=~^}ZrG#mZsZ4nuRKo8Kyl2YIHYUO>DKMqwhu6!(BSamEy-_-WHd zP?Q83!=RnSVx0LD3qyL2agBtGlF=TFX#g~{8WXqzAKie zPydpB5@G?Wl`vV8ATL&vW}!nGq>vOHEo=m=v*PT-kqzeZ+^pMhjBMrms?cZ-bFqN` zR89+1gs$p)9asY7uI2L&Z!wM8%mAZ3vpn2 zY_b`TQqBrC{SNrgYHIS*#b?;*-~IYyc)Of$P4n3k5(_dcKUC5sP$1{|-9@kPr4?K; ztoWG~(`ykaePtdPfM&rspzn8?d$b&{5io!6t}P&tlKNAm1|TVsBm2A6P+r95#w8tJ zpB&D!07WFyfi0wPE4KeI4p=F;6UQ7l(}cwV(PxdV23?%pnKs;a7%3*K+- zviN}R7vOzRjEyx%-%PT>%~V=3#I~vCWOGL3{4Lfm`Va@N!h2!2Z8LrDpkmsGJh26s zd7<2G&L3lK9Im_;C6G;q;-;=6)U!e&I3-R02M#WT@JCrH z3OP7nT(Zo`mH{vJn=KRQL3#zF(tV(1@)x3*$oDkeKFoMaQ!Or7H9M$=Rf3aS1-P0!;t zJ^z~ZvwPKOBNCjxtbkBp;?6l1EBt*lCEA4#8)ZM4$%$pAM9IlA7ZXNd<(N1ycwlsA zDj6lSJis4!DV%$#LF!^HNVeEu6b@Y1x7i!=2u=QGcFmVB8kgdGPyKG1j(-+xZ0N z)2C$i8ehXp{O*L%V#fi%fLJPYSrJb2;isJWWx7QEi_inQk}U+aOP?3EWak#f>umK4 zGbf?y-=bUnYlfV$ug_!^J4!u%Uy@hBoV&Xdq5gof&J*#*!bpB?UXABr6y+`^_*Nhk z3{JQ?vIh)@4JhEh`#8@(PNza+VmED2I*C+#;e_D)s+ z^|Jbn38or>FH_n;^>c>z4-ZPeiW3+)WRx7IZHnCGEGZlW1}^Y@?;017V=8b82;AA3 z&*kqMj&#k#>+(8^*e^^`T!M%j=vS(AA)8kQaMWI&z$*?$S_CLUwLPLp->%7q%|Sb}r0K zR(MpYrkq^0hP&oGmGUEkFx-#z@eFaE46VRri%(0AD9piZL!JNkG}Prz8}Ry5+6$c3 zGOhaUFCq*Vwz``K>{?}RRu=e7i@a7-dugyYA-i}Y$1&!&>w5|cI&~E}Jz3ekc|qUg zq%Rcea;EjzJ;t_(r&h6|gCry=g8}usen3eQlIS%!4l)uZ!{ImmJfo@A$=%R$TPhfj zDDMPla!?4;Wn65@LyEeT@0$pS{3IdB1w|^rRZK!jiVm{uvjYcXz$noF>F`M^h=MT$ zIp$3`=2*S`jqbUJ3tl5bBns`PSP%udx3v6os+&R(1<{8XoAE9QCCPGCvS(9163U-C zNNGT-2fWehAo9ix!=19j0Pl+;EhL{hW#nUsHIAPUlCY^LlA2_}RQTcosqEoYFxn~r#v1y!a!B01`6vweCRo0=YyG*Y#>fY5 zxlZ__-Oh_5SCKc8)hB+IGB?JAYdbeBUVJW8(D>FL1RJUBb4EoKLQ`bclQlS7`6;mU zf-NvIP$2ewL^zE_M2`97N(Tu*l2xl7b;vI|6sWd~KAwimV+W=fhfU+rRmB z18H`Z3tKqYRI1A-ony+IUsULd%vlxJ0SqHnUb8u)fmg$@ow3^MB!m*H3+JWxYQjr+~I7Un+SMRwmXEd;VaAQ}`f{)yB#PXbPVRw31w zRUV4hvWM0k6tEUS2%LCxb)2S$$Z75-=aGZm>;EYRqRuJD`_7W~ks3bqCDTBnmR^=W zhxvRM&?;^p{L;Dn2lF@U1)KG0@55RROd)Hu2}V^G^U*R z>?}mc^ehAcefH)9sw~)p7Z~^NV;|ncArf}-TkQTl?AC|a5$d5QnF5rBiEk0-7pCae z^w2abZ&))jKg!@31b7WHr9nAPQchqRxiNRMU2Fq-YVCx={>c~Fl!UDp17a}1oc64c z?;e&wP%_zEQ?~<+q!aWbJT56Ll0AAJYr?e01Y&}aO6CCKOFzP!sa4xvRKM_~08|eW z%SewsVqviB5x5i#p99VVn+HfIq}bdbwpyao=WL0T)=72Afw?ViUz_4q=gB;oTxOfx z%y1vn=yd-1Gt?s_l^D!Qx#w?qI6<+zRaZK=~I;Q@#T|1ZRQ4 z_rL>K?T>g=0i6kad5`)C+zSd06&?XMlNukf7=ZmVMgu;2^cM_ozrbf%N3bSrKL_Kk z-6>$nfSUKA#02OsSSW&lBcEWWK_##zM`v1wNB^&uQLK;UQKcIc=7$w~0QA#a<6KxS z#qnpMEq4nv0QyMpV4v-z@(9MA_v$tr=C%|Ij8 zQf@|+o^q@Si(~&t-l!v?>e6Vfdw+-+B^c6zf4aG%cX{lJN{a;sD@WyK|r z$$7mtuhgVqa)dhe)v=sxH*njKp}$*@O3hD!Q&7w()O0|e3QT|MYwW30R1#3fg>u@{ ze%HZ6jl{AezgrR3;%WB%Gh4K=&3!EY{{YoRD1Xz`%9Ibzu>SyW2m-)q1;@}TedCpE z`NqY(`Nlc%dh^mJS$IAQqDCbSqz-vLo1m5-3Iq)Sv`5#U}P* zFe@v*a6-{eZCotJoovAkEM(zbYo$D0V}tS5Mml%7O1_?3s0%Qk!wnH@j832fHNZs; zHknAkR_AB~-RPK=^1+p~@J(~ry=gJuH!b3BF_+gArg*h3oeP$N2aAOo5wZ_(rk1O0%*fCek{tfgWFJ;dWRjEx|~1v3#I(Ws1`ML1Puy4dKNlG#r~^r3uefwFJ@ zn`K$<(`9+@lr4GDF4e;nS@0~(=*vEavi#Sxxhk1=9g_PNZO8S<{=O`KW7+&aY&q{h zA@lx*tiDADnatlU7Q70lEFKwcI!{RDP%84a(`7vmW0F?(3vvMmUX3U-YaF< zhs$!v=IevG**prnR25gl-YK~TMFA^MDpyT!df5Q~VJCKBH}+sJuoZ+9DsEE4pwrn=x7)?z6VN?d%@nCrS0TR>Up=wMbZacN znE+LOH?~2A-;0mE!iCw|HMcbMiN6YHW7NVf{-9`jM~qn79?y8K-&=(!&f4h|z{g0k|<%(y;w5iv&1rXI~C3= zVF19#aEu%$WZ;aSp-kzFO=IIwPe{*rW? zNRR&_tI;aFD#ildC+a#B#O}oaw72W<1?UQQVLgBGKX*b*e5Z2*CG%IS+=|8|u$@@u(ekDXKeJ6P0Jhh^(R#H=`}7|!G=rKRToh@rR|d@|AH>YGj&FDA@58fOq%idd%c4-OP( zG8Bl^;ccd4d_V-4ocKO$>tv1^FrCHAxW#|KE%a-~Lby*Q+xkS{>kwoap_Q1KMmbfi zY(n2p!Z4;NlMsGtE%%u^ht?WksyUS)JNJYW1ab85s~OpHV=`)?)io-;C7HqvTo8eZ z40?P*Uen)<1>8>{0BG7l9PsP@$wByk{=t?LPk|y06D7tB2lZqy-6nVY%8&l=uRvIU z2WD}e%k4bM%e>3CJSs-*pT%t3(Oy^7>`6)|n_UBm#b#EWHy&u}q z&fyS37pk^uEjzYfjRTP)L8J(&Lbf1}A%~FfP%k=(E<+zjPod}0=g^nX%jgaCHu?b- zq*ADP)DG$v>OJa1>LVIJOQ+drK3bSoN}HsupslCP({|Gi)83+eNc)1$p!4WW^xgCi z7)(ZhG0nJx#b9>Kk43O8*kjls>{C1wUyi?szlOh#|Hov^5#|!+409uMD{~L?2=gTK zN#>ud3f5oj=Q#D8Ke$ius`(JVi+@#M7d$1D2z!OEi3UY~ia{|&yj~KN{3@-Kek$uB zKw>FziFhEV$rI&`^8NCc<)16)iUdW2Vovd_;v*%l%u#kL_bTrw|9^XP@9QhrU=z)^ z)=r5|I_OLD{E}!YSLa|F+1uCb-A{Y%`jw5FvucOu)SGb7^kE#!XMb~>!+Bi5%h*GNK7Bd;(r{Xl&Zm!)OlJ)7Ek^`9*E}!0*ue)y)Mh zFyu_UKu>Vix-;PQ0If@NaVnO*`3Ae{X<74l{;7N{4|jRv-sF!6O@}nV3B93r=flrs zciP=Aca3YM%-2vg_;;VMUjHqNP)=Pz3$Sw${kwzo;tNtqDNyj6qp}uaz!$l75G~xD z-yV;LRGl~j${f4kyuZeOpq}@+LbbVyAplv{28x;S(jqx)FgF@7;;U*>ipldk;NM!r zMs{YI^+G_+1jdIHEJ06*4&+yLsvCFp^2Os`rjs0}Uaiv{W>5ZUZG-i>DU!K4eD?aY zt*yFoyKUR9&3vk39W2{8kLsIEJ#MzP1y=>Sogic4H-(6Z-;UO?JWoj*0jws<1ioJB zu542baWywO`T?=ZK~#fcA_m1Se~bo>fUOfJGHg zM#PLM1n;piM%?z>C^o^C3%uO&-iD$)?DvVM0g_RS2}5O;e0= z(F^rQ>{Du>es!i!$|YwcZ1or1fUe-FaYSR)^6x3xcl|K*?c6}%G#VzR|D_d&Nbw_- zQ7#ZnGZDMG%~79FrCl(iXUZFZ_W|#4ci<_#A2b+Ei~wc*`=)ypAspa#nOR^l~aaxfSteG(nf?vvUSdP?7f_hdfh=gw8tT={Mos`Q(`)4QV> zr|})9*jE;u<0qg%G?~8RL?7{gy$*w%HiXtqQgXOK!NeFI2r=$7ua2%B9v&aJ^M8w( z@|0QH(qeJ5%qt(z;Mp&0YjT?{n7sK(Xy^c;Ttfe2yg2_woaRrs!2S-k0dPI#`L`i9 zaoU|JV~s|ca7(vpl9cz8QraqUq!5?3gh&*AJ}`)nmC>SUF@E=%H~=gP@cVoRjW1y{ z6%5sztnd7&-@%saJ`M{&WtH7soj(B)ntGqSb^O#$Qn5=hDjxVU|8g{nn`U{qxZe8+ zs8S3HjAQAI66hxBrGU;zy%3Ow*A3ikcqwyQ$O?BS-Tg6#oM>Uf%lm#T6k{1{)lgnu zXqdPYt2D~@H9GhXjk>3iym3Afmw`~Ml6hD2d|F%&EoGe1#Qbikt}roz9u3omYo22) zE`#GnHMPs&vPUUf)sdc}z&L9Qe|A=<(hBoOE-4q}tgOxk{8i=FsoKK~#BJxqB0L-x z3^g^$1TN|;kEtk8jy}8+A6{7<*Y!y=j40kIt(Cx8RDkA?kp4D90GG9clOXhB>~Nur z+=xUcN+BphWk1q3taXYkvZ!CYx=L_kwElFWSyk^IRW&LGcVvy$ez*Wx`PJY933eEv zSy;3+haJY3G!n4-xPk*hg}YlMz{9s)RhL2#oOWTigd$xeOz|$WRDbyPv>&KJz<=_| z&=opE@F7WSv$>h7f4I9DL{92)vl(k6b5#UE`=6|;;}>^rWsR|m|9>1Ajv&10n~gX( z6zH(CXp2Zzsk&tAtS28i7DmE~Z|r*Lhc6(rcrABv61Zr{4f*Up6u^R7X-v zh67?e2NXTYpE}(b&E)Of{r!uJdhH0xmz?Sl$3=rayL-I7z3Pp%Mp-K78omAGX8caNKnotV_9(%1H8cEmUM{=w405|9rt1{{mKAjhg;f+o|KI6Lb0g{n4qw~JH(gC2*Frhri&x_5!Q$(|I)Xpkr+p{=CJWq_vBZ&F;rG0V{9oU+_JKd_W)_ZvDKES8?q)ZxX$nER zbHIl;yUg?AYd0<1rfIvjZ$2E%e{(jIXPelPi-%+Qy>STK@-bid>Cfah$+z_>1uW?$ z^~zF>T{kIrObQ{JZs->Ot6VAfP}$tPlFak@_EFXf8O2ai#vD%9b@gf21P)K>cce&! zw3-r>qeREVEFv&Y51IdE82KmxUN2~wgHO;bKZoBaU)RIk?g7jHivsl##=x|5&Iak4 zkYY?Xwc_W+O1b?R(nW*&?W44%IA^)=Luuc`mCbC=2#4=_pB^^58K|s*dcb=i9q@bV zZ%#IQ9$y!1hKAq6ba4%1lTK%Jvbi0!6@7zQjnR7e;+GU zS#C-B1}URlgkwY3@8IzEe}yBc{toFt0fYM8bjdhh^HTR-8P#l?5L@jXB_5TfFH^q# z%};*wZ|I0ed?c>}%R4sObL1nvTmKl9=UnhVG_h}*z<$$lK)>l2sK2RznO_1WJ~@Z4 zB{-VpU=3NLoZ98r$Ru&JnvBqPZG~BFJ`Qc#?EFavaHP(d*CO#hVAubZH7Q(`9&8akpUga!A&Cx5ooc)sIDd7~v*(TGY_NxIDev)9Y8>Mxa(<9){5E?iRbY3YkNbip=7j%C=o;a7bphDqQsjuO6q?Tk@( z#!3>$QIxUqL^!xA`&Nh$ym!(;)T0z}@Gnr$MVIob-mF#h=>Pf>V7|3;wtfP2?k(be zIZCsYT)pQvelQbt^@dw`o`PH=&{lWJYp-oR)?Oc21` zg)TEB?q@vs8;0F^j8rjH?6z9I*)<+w0K@DD+e@JX5*4Dc_4ZJZPsnns=lTC+`< ztAn>^QAAK*R*y(uIw>ZEbH2vtYB+&pfozTfogQn3Q#q6k1*ict)hv5y2j(HTo44|SgjfsRQ~ z*gA@eWGg_LH3`#{`H{aVaQHtO)dE#9WzB zw-~3+?636Y|5>+c%DSuRQs-vZy5YSu5)%S5yv5Phj9lxp zJMk2?bM=f1p%{2q8)Z9Y=vl2p9i6LEJ)u*yuDTSgW|>vTpd9yOg5GEe7_{3nvbFle z+d+S$8H1`do{iPD0YH&1$br@-%yURV>wCV~JjJP6UGYxXQ1&F(dR`{S?ZkM_DQv{( z4#BSz9O43^YeI1nnI&NqcxQsy*k?alf@)zXL6{KZy!$4#wo%H}*O8oZSrOG13hfP1 zNc+0(+5>Og0e#-8Z;McAGumqK>zSN?7-mZkbmhB*fI&(M8ZcZtSXY7QE&yP@hr4eV z{|vNs@4`&%4l}@HO>W$|Ll82j%IqT{No`YuQ65{_{L@D8FjpHWxpCJH!N}CZ$!@pbq0sS`J<%$UxC_efZy0f}nvEq>cU#E~ z<7Z6Vm7IFS>KPp(SgW3N-%CE)lhpZ0a+-%`_unYNqHDVCW^8SCAJQaEbE+Ly#x-d2 zK)GAES*6f!tE$ana54}To;k#hg%B|HLIm9z1LWWG9rZ?AtA_eSfCh9{TdAR|d*+S! zf(&$nHC8m0W}`D@Fjo}%5J23YvW>$f3WN13h@&jca@K+IQqdZM-fl%i)MsMd=mK^s zs;e$|5+_FJadU`UI$Qn@*g+(nRqs*oBohUxJF>2yk@=jbQC_Y=Ll;WNE1M=H1_BUx zMl;f&t6K}*R*>bc^wOYI+cmvnP9oY*_15zsuR@@li{^UaeNat@nqCt>5?Z%CVhJ&fcC+`@< zB=cyN+_OG=6_KgxALXGEHgW2kwI`p1F&Mr&ZCXu@(Rnl^Rzqn}Mp5TYAza3$E=?`1 zvdDDlnfD=LHld$>`mYJBP$Z|=s9NH$Q_TD7l-Y3 zcf7i}xxU;rrGP2T+<19$IP8tqO0ojyNz_d6C${#M@A|a}*EB^d1h_ed2<@@y>*i!- z^HeaqE_IPFhQ4vWb+n2+Dr+8HxJ_M_wmy~?K9`%6Ye4@GF@p~Ze~e$|N%N*avxl$F z`l_N?i!jPzgDFMGFu1}`6M$7IFHt%H&yr(G)u1%{hS3{Ka3^#k1q!V}}S59;B&p^xTZr zx}1SW?p{VIm1+zzHe4`MI-Z&>ik}UO(AuOK6^cPa#K5k1rs)!)2x^cBDLuJ*xos?m z-sEv1QuDv#3Cg&vd$V%k*CK{Z+L__k5gb5f8y3H}upqcsrp$w!R1nZ%er3PoR_11<&#Ff(!>KRSD(7V|UJoNvI0-Q{@89I$^{;U31e1J#8c74}eX6`R0 ziw(w+o+cOB&2|1(CgIHr0SU{i40ns3Ph2bB3|~>PQ(Smw@`RBlaWYSF@0IVAEmi7W z5JUpe##QHq;0f={B#OcyC|+dk;pZqVs#ou1DC<@YM~P_I_Y>YwPDC+uyr<2t#Lktj zK}BShDhHykE|Tq8>T9wKBHb%sk}zhlJ1~=yD2vX>e^cGYJUUnIYeo{3)gUZo;^s$K z>4&On=$a4a9Bjb#lloSKECBd-Wu<;OsRe%MgGn*#sErwt;-DZtln5kwK|ix!2q8Jbv5Liv98o0 zgdfk0B9O=V|DG8gS4hGP1Wfks_?tVeN3xW1ZbF;A*|WC5wEf6QSd~x6figny*|slo zBaDaxXLB2xk$8g@QopxRE-?c#E7q0>^Zu z?omc^h)82E-@P;?%Z-b7ht`SRWFJHfekFloyu zKta(hUm33yr-V|%1Oqqe>*9yfLAI)uKU0s4>H=e*W1!45)%zK(Do%pP^do7v%>SW9 z;@rzl}kGUUVkAzj3=QrVa50vHY*f*`9 zc^s!gaDH4qFZq1?I(knYZ7J66IajilzrK6h`|Y+rzN8e`Q<<@VYRmU5UN!%Eh|p}F zp$2ZNPY#_&y~N>%JtNn*e4E||L%r{Lm}caP_zr?I8mez{cX=gCp@VdYvUt69l1 z-||e~^}@1g*!nfXm~nz{opi$1m*z{=$I{X)s)Af-(DeNlA3tI)MTB}&g;{j@?S~1k zim7LevUXm-my$7Xn5U0*wKm}q>Fs}>)g-VD$HDFXU`!0d?QsH#pHkZnw-~&!Qu>GR z<}J?j;0E30*>0Y;GhxO>pwjGT`hEq||w8!jM&za=~a~OQ?`FbNFt?`;z%vla2GAgc7H^XH`dxlKA%nZUK8?ku*eV$N9YQbi0 z-{&z=6GUFRF-Zx!;z~gw!;l~v%bv1e%5rM$Jj-lq8#q)JU%-C=QR=)uraI-InwKTb zGPf@8q^2c-oKV$k*gBwxgv>8z4l{e~W~?DCC0~7Sym!$XsbTEvS{Aq{C;@_lh5JbH zaIjZe@CP)f9B5A`HArZAQ(BQEK@NEqcBA~>w<`@8^YsIY-#n3aJ(Vk`$fEgy!Is^uU?FNa;6l_ zJU=J2ZVBDK!D9qL^vt}slL@=xR{*`$otF7j+)_y)`&~@}_2Y6+sWR^Ue3|m4cbEV@ zwtLYK^rnf&kual?VnPU$G}fmb&=sbziMw^tWl!><3-WW*WfMnKM*yzKQ#RC?Y;NQ^NlSY2~|_S0OF&g7-+x=0Tt{;~U`29nUe zZ`SqviV$4+&{JAB`G@|p|1FaJ;%NxJzG5mkj@$gWe?A)VZdbC~EY+gD`yK3MDKQ#>-J6TUhL^p62qV4Orxat7)8X}% zLrBsc4wc=Ecqv|Nu+ht3cjkX|piFIE5#hx5`V<#olBVNV)ONn23_@H&20(rwn^tP_ zFZdCNF25a{U)ous)goH-M)FGxANNskA=*Y7swe*H@mAoFyObUGqoY}x zktkWJ=nNyj2dH29wT3}@tc)*(B64o%H=b~F0pX|Oo}P~k}*#2Q^qMp z&(Ix*%cvh71Sx%O6@LKULMi(l*<%!iyHYuJohW04qZ9eeo0O}xoi2Ul>|(2OiZL9x zH*!)8s&d3JnXR8QjYdofKm57D(5a2(yQb|qQR^ia;v>Hd)O?V#TaVkEa5>?e#32D` zMyZn=vy4Bg|&6|B#hY2jTv&nTv%02b=tH; zT`V^zibbW4M@IQFwXL9{2f7pij6=BqD%H`E1EipXR&ycvTM-{zj);(|8@FH*$Se(Q zEtHwsue5fNszY3<>FJg$vxQgN_YEI?qbay`RU_R=IP;YZ;n#f}#aLL!hfF|ed`MYJ z*E$2{U6YTsvx??%aI#(lE~2gmMFCC)FUsDh-&dKT5d#rg(TN?#{F0XhIk5aW?aWBC z2v>E!Q8gt=@P3*m3G_@caCTG~6Y8qYUVu~9Go=FTw5nJEvH>l2S1CmGGE%+SE?~kn zc$@~e9^sUD)iOMbB31bI|0EO_by)V})+Z>6Oy@aGL!rV+2^1Ae8HyNr%A95W&ruYw zm%e{G-a8nFVH`flxsI!u;3&pmuf6{Na6KiW-}pUoUZz#|-pfEU@dR z$y>jB@$s{pd%49IuU|htzIgojT0=gpCp>naCFoh6lQcknq_%`K{|})cD?DY+Ol`1f zf>DfHgbCw=SLG(-2%+V9cIb&tnAm8IgMT*dU70>J?vgWF8{NG{bEZm27!4v<(BEwgQu?Bf5$u*w~TEb%GZ)kV~q;wZvgSL<5pDmxAKo2XXRo%VP8MEK9;foUIq$sNF2uUmb-b9cCPMiwd%9 z3XE2J55Bx^P#|Pmx`7X!euk=vW89r6g*qahjG$h%f!;d!9t+47;N1Wj1k26;(Vu62 zy>&1dZ4!h+^cp4|D1BcRnc+CO$?Ur(`~mO6%wKI*t1X|VdD)s+^4lfh0iOUD{@p!y zf}7_WM&P6x9Yh9uC83=PrQYr)&FiXK+EUah@5M%V0_KW0c>=!KfemhCy_(FR=$*Ct z0L!K*Uu2z}!pS^+{VXUSz?L_{zoX96>BO25$(eZmYTc;~E& z2vD^z{nW-NUxO>9R(FTunIipMw8*IXHltBTwWC;9=|!sT!>f3~%MegQd%2ft`&p<&KYh5a=IR+`+z&R7@ zp^hm{Q+^mFFRQ`{F!oG)V|w7iEJE*r>a|XQLCPBejCm}ufq@<#00?!;Ot>r-hJhWs zMv@D`=w;N7wkZyNSev;YOcc~h5878Zfd8O5v?i9cOY4@j zFp)`x{FiP$|Gsa*Aonamo;0(T$KEbB*~%v_M8qR2V8Fe5-mp~=mJ8T(!<>?{huza| zIN!|#2KY;S1OXDgX_f|2kja?c!>NUa(@Wiq3T5TF>pXx4ZdJ8#h#72}8%{D{N^}R- z7bj}iO7G`=HDa~Qi%)kzozq(gKB7R0=gNG6jFqgSDP};3UrKRF>3_}ch80tsuJg}= zOaiFEm`Ux7@dLrH*Nx3`yl&=oVs-~AJ82H~Z|n0nz0@DXH~8qEz8{1kS|?@R&$V?l z+-mDdxJoHEIn)t|)p9mF*xNj{3(t$_EI{zb#zuy$Pt&wuq+b}oLcrq2c~^6q*{;*l zS?M7uayP6R*q1{XA0>BA4#(quw>w;pGJ{|9D66VboaRXtfE_3oO))ql9?Z(!%=VJ5 zVP|$*(Z-H5%Oq zL@#y*uAJ}`0Aopj7hkr?2#*$v>rtB~-V z4cNQ$#DDu18O!4-(D2WQu4C#x^;kp*R_NhoHC>Rb~gcvUgym(u69cl-EFv-#8vF z>uFM8N)LF04Tz?+E2-5@5+A z)MbNG{m`jTtuqwryl%X8bZHC_<(!t#uLIG)p8Qh|(}x5RV`GF#Yf@K{ z@IYO6B$(<}ZgG@9h$g5@DCL|!>zgaW*Xy@{bD*~Xm)QHwe-lv$fNhh~uweipZ`;$y zV9PJSNui}SbZCt~M!Ow_BqmrRcVHd>z3ue>y}#O-!O}FFeS`sm6n*Pum?N+6bB0w4 zCQJaB_YlK!i8fJ6ugh+vCJmLADpxJXsiWsQV1cvpd6t$;iGkR#vj0~SE$=XD$19(5 zqyl{H>}CMw1;Ooux2#6K{aOVl4mvU9eG47!m#ei2Y+0lT+_k39!4#N@>%G_ zr=9#DzEE-WKeN0IsLIcuA|R~vj{}O1En;H%hXHjgIsil<9hs)Z>~p3r5GKyBH8jGK zAH;h+&`q;vdoTuLqc}WJ=TDqPD7qSh?#t96hboh>@Zcxswq6dH z7Gqq@dJio(6G{n%!*B#cqf;MzlGWqDn8W>`mPwt+Xk z?!GEP+j%?g22V0&VXV5N;$gv-puJ|aSz0Bkh2ih|gu(6im#JznLarD7*r+{q%th%6 zN4Z@@P++mRVpxa}&z(vCU1d0Nr>+&rJT3PWK}Vn`g2q{mO|`@ndFreW^4}l}UuS{T zBIXoQgGFPJhW%pI#@gKKX{Vl`{{ZKT?7zdwWV#43P(qW%2$k8Wv+6@=4JGM%D#VH+ z-UfO<1I)~-?vR_%dFQ4W6QyXL!+{Wpk}!W27v<7fLXX)s^}3P;USj_u1Xx(|f=&*v z=!$|AX=*5w!*;jZqCp!3SSiQpDwsJ)O7EQ*tcl|8pyICRu6LbArl_wa_pwG2MaEUr z#Pr7vj@9P~<`r|YDkckEROhfousV85yW478+5o1iBK=Xn) z^=&0qJ14L{cjUfVT*GYW^29ptM1Uwwq(ZnaB%WG2Qt?}-Kz&MJC&~n@Utx9p%2x7L zQVNyH_Es6ST^Ea!>00-5Ju1^t90jzclN?&}^ghmAaI#j1kW9466QvlN5-_iL34j#m z4ng?)pN^S^Y=tv6b5_UR zIZ~=;TvqNTYXK|G5Y6O30(0d8UhW{u zdocM+oTYL88UH%>{u&!_nfntH0lVr@fPlC>7BRy%UC%dMCl zwQtBgFH(3okg};O#KVv}FS#6~_dyrET}ZhM(H@J)r|42t+2yu{7ZbDixQ_eIs&M8Y_^%6}kY%hCf#$jYAhS;t{2ymsuHpVnc zEFRZ$W9RCHSR~D6zs$3fHZP>45Co;J$&4wk7~G-EQQZlp7wo&N*gg|gg6^ueN-=B* zg+wesp=*ty)w*f>aev@!`?Ab`DOf&H1|K@ggut<-!dqN z9SX!WGq{2hN?lhesfv(TFMCqVk+53NI5gHeyH{1Qd4Dne9N?wSg|@xC)G5MfpTMHO zjC(dyruQk3tfMzv)dIOZv&Mu}Gg}Whu#}um=pv@{)OzQ+je)omFVZBm$xU_a{V2Au zR+9Dyk({e4;(&}d-J`X0=-sL$LQIpiNj>frttKpd08(O$65;$QhfWz$)!5}z8%O5O z1Na3d4j5Bmue+Qh7PDC+yp=R6te-O;A)>kaZr`_+b`~U0Ea5DVxDX2?W+9aPIsT~S zKZTg8ZKBK3`U>5**Tb#_Md3PBhj$R=B}+vA!_K9=>Hq4==pN~Dpl&kiIFh3GF;0XE zv(R2mFjZvw`xIoBZ5rz)M18p>&Pu$aFooV)BXJZJ**mZ_hQFp<5X0kI5JI{ zHXNe0!nb@k0ly$BM7TbS2`N_|t6b&2Hr{W~nzrpbuqgGgjZT6g#W>ZYWnNOh1Y>dz z1p-dAv6U#Wp0u@wdr9B3$X_Tj`?U^`7{Y+;*u+SQZ{l_=IJ(%t!)F19S-4vX;j|kz zLqCjaIeyn|M4u9dGw}m5?Pml`Sy@p6e*S`x-gqDSo=lk`E7vH+XI`< z1&BFEy?as0T`^;4^&zTei?m@x+|<^jxR{53YIAd54=1ZR2Omx%P^n`5cl~??%IdS2 z0Sq1rk(avWuu8IT)?5Hw|6jp%rnlj)FT#rT?MaM?X^<>Hv|?tq80ziFyr>$4YUyx4 zU5~76k4-6;=010;lS;wc?}{+Xh^ru{IyaK{B=sb0(}~VeUkC3w)dfIffM*lOp@;|0Y~J?wWtOF6KELc`&qGaz;TvO!{IU z@2fk(Kk&3t`0C}=_)CK~<{POtbN&Rh(kXMwJxF$ZHs5KxTk!LmnR|GU1mCy`+R@LG zI@xz?c<~c77i$Pl_fhH*Xn<|1lU>KCc1hUXiI@=i~ znPsp(nLqGANjRQ@k-T|cLi>E(f`gKC$RSWhBHYfjqjGK1Is8$rs)S<$^fmNK!0bos z?Lz$FaV!v-q$n&XaL~iG);$MJgy;!O8DF?+1AO=|v z)XhmR@@PTne8%F%FY?gMx{Lj&&n1ddo9Z@IJk*JkG|=Es-+?)Sjy_^Tmx`F44`~Xv zK!(4Em8dn&Xd6iCz-1?O;7u!Ci)0vhe$<(ogq&?kF%1?RhLy*)!VzVR#)DmMEuml7 zhmIvGt|wecaXwk>J>g>2q^1pxQKO;ZaYdRPsn%fq$Vf+LQgXOGZaW__t*Odd0;x|a z*a-}IMX++xX^Z)TG;j8EOwRBf7IOqsGRwCza9fbpj8wc!?CI3@Gz|sD{=-X5r6R`R zZ})C52V_))u}3Du2Vg6 zP86U1fHdS_APzccMiK2eJiu5Fkf@&Oh$n8HAyXqv#3@3UqCj4auG3C7Ex|EECz;;9 z(KU>Mx1y%8Zkn?-W67$ah_KlRei6wYxMi`4mR&r-BwcETMQ2L0OZi;d<6N;^pjrwL z01}8!{@Ig>GymH_#5(}s@xPxI0suU}+h0cGelu8x4~&KZ03h)19m~{;8DwGJ*mAf0 zDMEpl~R-(WBrQM9#-R^qLF1F}L$iig|bHfoDU zw-EIjw<8-Z-=G7HJ=1TMqWet8HN=1~SOWnPpGJ-30~$ox6`#=XY8ueNct+ba=kK)V@dT66QXY$; z!1GBPUQI}=KzS%MMNAP5!^NYEV&RcJOkfh@-P1&x1zt}`@rE6z}jaiJ)iE}($;F8u=R9$7)NCBO}M~M$`IB|#B~Nm zhF;<*q`FDXEW{K}0|K+TY~9)$Y{WSZVdB)WBse&+6aFw~LgL`f4TFU!$4Cc7IYxE} zw~#U>9$Pr3S%|Vw(74HveM`h5gg1@VxWlmhgMQbUSOUQcxrKt8%0ukw=X}x;(Ko0Y zKvfr(gAD4H_1%*=bggXnG|2D~W;_6BSasQi zn2+%$j2X#Igy89$NDc#Tq9}XNM6>UutBZKH`YRv|ObeO_P}DSxq$Y^M7hp{^n3vqd z0poa0T(I}0i3d(jHSxhgX_Ek4Olx95!@o%geA*@vh}f(V1A{e5kl>02^92KhN}@B( zXwxN_EyGZlCT1LnqZV;y=&chk)es#x(=6%2B(0@{hfuu4hd3~Z@tzZBo63J2gk2@zWvn=)-rS^8HMcmIY!-rc@1V0Zl z%3NGxO(18$zx;W^v30rf0PmM}V%5oMXDFYKV;3vU)L1do*U6J&I?T!pV`I zB!ROq@gsA*ID3#H473SE1YCCU0Xv@hb}H6~t!eDM zrwNAXF1g~WzU=km5E-ty?uMHjivlOkMT1K*h==iE)kgc-EakQhnCl9d26r8NmK z=ydZrWgBvM3VX*)VuCm;J+^%K^5gHLfLOBS$dxBwfkH)!l_(`V8MDo$Ge@EXH6kLA z-rolUB1n$Q&Qc6C$Y4V}$Vx>DB8sFW!_ZW!8wHK06Go&_Ydj-MFwrFRMwP5iy#}(( zmu;R}IrbRsi!sKEvfMb)8Cf9LA`6XAIYjACj+mTDvni&gc|lD#BS{`iH8MNZ5i4D~ z(;ZQgQ%*P7fVl=!A92aTM$EUsLW|g7BSSJq|APFxPRW$2VoNNw%yKKNw90C0thLU1 z8q|N)Cz@)!tf?hbb%^3^wo zs9CbGR=frwxb1Scd)(_jTiovf4|<4Io(i+lL(e?&*z*p1#G@YbxFc_T33 zwTk*+P95q?51|np!H9Xa;?OkLtcm(6=|;JsE_4mO@}4MhxjJ5$IdwA+Oy5^k``vkTMtJ= zCgf}LX-^FGrI~C|y=?Ft4F)BHd?Cn6vV0-PgS;e%!dQ5i>abfkVcg>Ip>fX%1B2Nn zjS>k6If`z?w1=};f|TGrzCHn;3|*R{yQ!6XGRKv#^eEs4{J;f1bNWlr=OI%9deVf? z@WSut(uRpNT=GsHhd&WBL69UKkF|+&h3~?OPDmJ@wqAyqQHI@8Kmh_&=w^fH#Yp_J zSK6riz?t({$Dhnf2~?+1Bwkq+DjMWlHxiLweFa%6H=PVdx*1`F8LB+yaR?C+^EiY< z2uO%!O!BBBp`S<)tRl3CWWgB;Z9+nW1e zG3XMT5X=*Sz#yT)8G%618G{6aU_hAxU3e3Mc_I)PBs4f95C}SBkYErDD6`@Zce7e9 zFf)^t`KdOo`yb*%u-bb0X6>dnz56QU$#$*TXf}(*Oe#gytfXXDX4|T2+8)g( zs4vSqFkbaub=Cj7`VPBJkMTqLBjbmQ02!;2&SLeYE~C5Tb5DwvpBio~UytcZqSwZz zO4*oyGNjicZ;K!QbI|LnazN+(+$rcQ#d$yTn8uvbvd(yT5Sh{<)aX`KMuW zNwcl>HaRj&%U&gya) stX_f03^Gq1a68)_+pi_@w#u@obiaEXcFpF{)U4KudsY9-o=30}0FZ*xeE~`2Rwfx2uuw#$7 z9gq}-e(A~o|0gB*Bw5@?so0-vt*5L5ObN65)y=jEg$>A)NiF5 za;hC`;`Qd&TYia0S^r&G1%Z}Y3E(!p|I7{vy93$E($9{gbd4SHrGEtdAdM1S7dqlc5vwOKDm&+xDB(wk_&?R)KK@dwr9?L7D zVxvD*5gVWcUI9x5Yz6cmdq-c<$Bwl=md0B;$LqBJA3Q9y9S1hKfK<0RmXqxA!xBD1 z^=qIC2ruv^Ym&r2@ZV+sYoF?*XKKJC7f*n`V0=MJ4bgO7{*$InK53+F#QyV9Z_+er zOxjdZ5uu7{i)jl($FLbwOxA6{l)+SiIn01LlcCzQnLD2irz4lbIyDdno|9|Gsb)>XWKa@}nnKh9 zGD&xUOBf!Y%y4-B_aE2v1&*jdDIkBLCVj3rKjF*@Apvr2m?5FwnuL{R>E->eHN6c0 z`b1Tskg+MYUM&G|K{Eff0pLHk9p4)zStCpsZU$_Y@(Q65BnLxwXj{eB zz4}r$^v_>{NveAl>IiURGce$*0<6jiJpWDYr5at&&Ysb4T77>xy#a2Y7^V{%K)hpl zwch9Xt?Bpf^dhGkYEJ;{Ft9gNQGyx_$_Zkfg+?D`)-Pz`G2LW>TdoIrW$ZH_j8j#mnkHQtcpsqq?(M*jv#pg~Fqqy_{h zAaFbYC=Nkz0zrr)CV41I6Ese8ma`m^?A>Yr^bSGI>@aAYQ#ftNr@d0U`R421eB9GD z#_P?;qYu~h!~IJ8CUcPg=a$?RAuSu*s}w(3N-kMMRRm9V*Hzmdt+o7uS%`#4h@zp8 zIx}|+HVM&?L_<28T|})BJ@hc!9@1ArrBrf(1ZIM&ma`uz55k$JXNQoYa7YSa&Nv|5 z2)%YFj32y6ksz7Ye80@qIz0Cp@G=NR==zjK+bF$4aTMXaXY=QQg?g61n(jc*e?>t= z^M<+Yb%(Lk2GR8S*yAiQ)KDcz5QP9LqH(?VF`Nd>**s13xvFzaDX>FWYtO+y`^h>= zs@q>}2MAADQ#Xa%xGH?zipJpY2oS9afC`Z30rF9Pv^`u+(<`hwU#ckZ#WO7$HCV02 zbVbG*EkV3!1Tk0wfF1rpAO_713qbQ20K~v_sjWIefox)F!%8H$9I}+ktKdptA^I>7at4pE-9A=f ziUFv_ivmSXJi}`1IQq(@LqaLD7)vHp(Ewvjq_Vse;RFq%D1%^AZcPla9k%A=Q<|U) z(>!z;x(VHZ9>35t{}r#j_Rf2seZGD3eQpONC?!J~0R`Ua`DGSUSZu}ON{O$+z$(Y& zI?+<=kq z;X~B=>sN2zdxhjfOh70jV#S~9oS*e(vbM3acQ81+xEs^oiyS@Ce~JLdM*@0e3fHp$B)tzPgk~`+jWkHC7@>T)u>H9Z-^tBq*6~RO;PNt{W*)iA&13@sHD&jo zp%Va5Tl*ilJOWTBhAVJ+2%yI8lQZO?aoRpP%N&4=x)~3wJM@t?R{Fsf)-*C;af$`o za2RH?GAjQDzk;8@kKwzM{P+tdRoWa_9UW+ePZAGCpUVsoC?ECmf`}qJ^YE*HM-DgR z1VZ%b26mWs+4DrV&wi|f8c8Up7p7TL;=~v)jiO=cyH++ZsdtW;C6NI!bD$cBj)`E? zL0uELIDqjnK(8x6{yV74eBhDHp9l{Qxw=bQ_`Ji#%mc+eAK`mVGevfnzO4XXbj(i#&G}844;OY zqc-@TXu-T0SQF6k9T?7(N#B(&>r6Tc7FLGET3su%cu!X`J=NsG{q$6mA57Q7ee6;< z(-jEUiZkNR!kL%gC(1!^S*|O$MwXqb%PpTXoMf6@O}++PWnSY`WA6YJi0sIxEdYKV zth=vU7xsvghdm&0=;|DZ_O?9Nvb_CZ>wKF*UYnQt*=}(kIggY?zwR;o&&RUHZ)s-1BvIH znX*0orjBNvrB*M&E@-G-eJRX?=}=TV_6%r;T1eR%3wvLtAAZd;op^F9mN3XTbdp`_ zEHw{XbqAWR$e3QWQbG!r`Jd^~_a2U*tk=KK+t<(L(DkQ#v(|6lIO^dOOJkWL>&Eqt z^qQHFLscHiO(YH?5}*R2c7Le z+gji9<%qV`E~o-Z)-5QrkqxSkEv8WL0T+vH%2=a6NE^n|Z1tWkz*#s3du+9Ow&C%! z-sLW^!V(M2bfklfHAa)(8njkme&J!*y11;2Yc$iyA@;C|wM*t=7EnQnO;|hDgQYOf zo(>7%8`3T9$H-O8~45x}8 znjN!g)}teUzGhm=TrXIyovNm3)8uK^)H&7nQ@JUAil8|G-DFwEishfehrC)T`a|ct z-rdpQX~Hx!1Dd~l)OEm&hv=VnAy~VVlgu3R&mr5ULw4uQ6%h$s-Ap6OU zb(Sn&nTVj9dlXlo8CA2r06aFfvX0=umYsh=tyD9Fqm}v?8SR;QSE>Y8Dq9Tzi>kxu zKQFuqXoVV0#!u1kF{;$b(aCH&eoOGl?^iDWq<6N*Hma&V1CKNWuy44~#OBg1Iy=AP zNyA?kBDPNn4>gsn(H(D)*+Ji&-b&XsZO)l+6DKs8v*voj9P3A+ z3jQ)x@#M`y&=6u=#wwmN8l3uT0O?~)Xy$w@+M_4r1>u_JCw#h;d0M-R8eZLC^YL&E zvA$b)V)JeAP<)B4od*;=8&J~Yso?E;x4VVhtJJTR)gJ^Gn%H@=!rTSmJK6a%tNs68 zwsZE!%fof&)Xocy_8ZTWOBdq~Zm&n}O7c3E_U9mQpPc@U%CqkR{JVPMh|Y8bAtX(3Hp(D={t zS>TUCHM<$Y7#~Jf^9W9h_9i$j+i^54T@7t@tT)Z$(KefRwx6d>GR^1E#GSMc4qyNY0jbPP4#@b zkM+ynvX<{n5rx9Vieq;CR}EV`$Es-ThZ9h@4P;BxFj1lA@v5g2V_O(Tif}6$fZ0cC zIlj6#BZGuFM>xuH8acr~oaSGeId{rMJb>Qz6;?Up zTsM8!RA?lCL;wQ;!~y6DAOO{)iQ$Q}awt&2fkKXZ6vqoQOD}rB_XDIu#?$dSiDS8y_3-j@yo+s)@J^47umiJ~nz!X|dQ^MolDpPy~E z$BUmY_bO+_iIIMmDwrt-r7Juj{jq#uIdBUc8U9o8He7*pZOQ6* zeW=qx*gt>TUF??@1*!&H2T)pD)7DaX!DsQgTA0!bOTkU36C}){Avu^qD_q zi$l)t{@5oc2<#Cp-ujxPAbSmIlGk+D?vFMY-8pj;xR=(G_ta^vHr-b!sN=0O*4aio z`<9if$>OQf=C_V=)_%2j!GJ!dW+RpynY54 z=y!u97-E#s#z>YXU4~45$dV&hp(0aFQ_8H&3>D^EXpw3)YAv?R8f(?5w_bzocHHMu zcfW%UJL;IxxkI*gU-V^JZ@0)Pa7=rQ+Op4rF|vlT9=@U>$xw6t?# z7zN*g{>lmQxF%E0Ehvmy&|MmNiAU-@CLsUDCSn4FA*il50BZx^(Ao3hW z4^? zRC!bRJ!WiT3(-fe6%H zZ@^Jo>RagI0O-uzidkHTXCY?YfxLfQO-v?IZ}Nky9Cft)v6751TB?~r9Xe(#eTs082U^Q+W8NJ#s0DP(_ zyX90XC_3S)lVut$_NtUO9m#fztYz=e-3A@5d0f0k>C9^=f`r{!#LSZ=mw5zXIJjiK z-z!JhMl7~_$dwws-%Y5ZoGL2_EdyS;?0~h(Wr|^F#)_EEQ}(Bd)LLf6L7~Sn2Bmv* z)(}Ib$xvaoqNy5mL!n&dag;ErO&SN~E=;TQ5{mZCPb2A{uy@2xfgZ!}rzd zxffn~<+V57y5p{U?t9>&M;?3PX)ei+J5Jk@Cb!=dx5}lC6NlzFRMv{S!SPSJ?1~mw zU31+HH{Ei({-vv|3PCiv1n^pwi=mbe69P8wvb8xrad*m^ zk|fDx>)E87l1$`Po$;@8E_CsHHIJT_0E@f&9;nQ#{7zif8OZ@Yxq)L!(qimk4&lah z_DC^XgNw`uP{%QpHKsa|WMC83fJTz13K;zj7*1W89X1>}&WyK=eMul|DAtp zu+b))D}`hjDs}?I7%D@7*;bThODu-vyn(dokhrVSxVZ!ZquiA0OGB8~Ln-X{mH3Ya zw&XT*=yYEnrEqQ;@;u9`m9|UtS2b zxnYk@)>>@N$aGUtTw1zgIZ_HU^$sg>X2$xnN}Q=OPF3Xw$*`J0VtSJD);}>bjZ#;F zIb+NPW~S*i^-tv2f(bK*RwG)}ts0cbjl%E^Q^S&de13DGG1xk&(=)5x*QV79%XZ^I znHl~BI++yq-aXq|v79*|PY13mjt(n)|$I}jp6JV81i?w+JT z>1e&=paE4pJ@KFuIw5@WbE*OZg)J00u#Dr3qait{-0{)~9QxMlttFocC=9!TLryiC z@8wk9JQuzoa^-WZAjS}rCduc+6r-=I+3d(WyG2M;W`dV>c?VsIWH9|pxIY~Y9dRW?b2ypCZxz)k@G zcrZuVpos+lKsop;rvcqp&I3Mo5<({csZLQ-z%0v*#V^h=qKOc`!j#yhJ^#|~| z8h0k743$y`a1=tbP%4^(@=-A=MHQ$OEkHx)6POg6#I{OVeEzR?xd7<-3Afsw{Hqw} z>3k*7iAt_@OmD5%eMe!ZO+b1kQCzi9w|UG;|WaDKS=;T8bEga2oL z&5u9q!KWUqdtkW#jr$kxcOwia20)T$2>|%2R)eeHOMI{JIW0OM9*JLJSgG17%H#Gw z`O&J&OHpppIB{#NzJ`Q(AsL=PNy~~g8;+b9w3iY#!A>LZqydO#pG@JIYfaOSGGsy> zQk3k8bWh$O#G&mxi>A*#OYh?A)Y-7evGVHJ94kS^l~q!C@%#-IaH`N+QWk7h;hG{L zi+TAHH#;JnF03q+AcywE7$QU8zNpyU16nF>Q!4W)WW9sXUPo=r4-Ac>I5s? z@SIc=;d&F%;ZLCl^Z*W|xH{Z}OylPO+>0XlZ%)q00`&2YeJCzh2KNKPMN~9QPXBg9s3lmC!I2lWGRIr4wcb1o6A=(ar0JFmPe2Q3v zgc8W7AnwN_uu~9*Z4t`gq?dw>8~DhRP>uK{0IFJp23yCQQVL`f%5{~i`(32M85J?3NQa3Bb z{4AiJ0y@f%7D-3?UWj|?t`J77{)KEFv`WHh6eiPYJl%a{O4bmZY#R_Y;Eb%v&1$+@ z^(#Vla}2=_JpJTHjyt=h9U+vm*GgRQLia{xv*Q~+MV580+(h;C zJUxdg@?~dhLhG{hl}A~GGL)9n#&}~@(OiGRrN(VGs-xs)dzoIO&}`v(x`jDD2Vi=h zqnlJ!So}p5>2rW6vaH^wit18oogQfq2zxwWU+H-*LNG(6HB)s3FVm>v5lsDgP+dK$ z#?imNfza-(!dZ(m=xC=obG@G41GFhWWKQZZhytrgOzk?7;# z^KYAOK3<%o?0~NYVBAb&FpF+zugxAsiv^)<8a-v~$R{?6sCZ%$Q&>@|p3l=X28#?f zhGpsDlwrrOD>R;gSG@H zv=5b#Cq0pj%5R`GrkY&2hAW8k2Wh;%*PI0FeNImy;|`SUuWq2s5WK}~n!_d3p|!MA zSS&4CHabLY2F25(Kv?74OsUbP5Cks=lvCf%)=t1+`(CP=2V5lBNWqz)OhoAX5<_40|FUn{lGJIVcWPNTR@6v{_F;)LveG%#g~{wB5>!*S9r0 zyFCI0+y}&R8IhL&g6s>q#D^d`fS;noD-(p8yh>9LijRD0{|dr+mq5Nn9#SI4r3GrJ zH^4NeR1e9RW6z6r)wY=Mo5m)~!Nfa2c-~#n=7a%=c9jMgkJA?qxX9RyLfk(be3nmf zOe*M!+y+V}jzmSrLae&{gpx`W62D;k=^=Iq6F8?hbTn+Zx?jHA3OGixIEW2qCPFN6gz1`{+ zT_81cboa}qIPR>ynu|eaz$IBtZ+5kUmagjbY6B7EOjL0i306oxO7(7lyMoTfeZS6} zP3CDhHN|6RHj-(5@C>!QcesvhOcQbm$F%HVg`sYfc|7ZKOGUn z3(q^u&Yo2)urCI>HpE&i=%(ANENqyLv98Jf?P0d&nDD;a&C|Eb{qyj{^>6|0X)d~4 zCWT}tG|V2J|I{rI2l5-fL5xS%N0l}Pf#c!yoe5wV<*0CfSx*#SMqJ`*1uuL8*59i- zCMAXnwPA@i%{CDoi1DjuC}-0oihpL1@8m|cFX=QKDSrdKXK^el} zvE&a;T(Cq%mJbaxY9)9|@UD(g4(4bnpY^cdjGz*XkgzMsjs$pgj+{TyeBN&Cs}R5` ztj$j8RjC6S01)Lt)5)(d9trv`krL$&LxAa}geWSf%7fb_5Y8TnRfy5khnCcQ2IArr zk|Ch`z&tLV%=?qs$NdW01+yzACnZaGEbuY|K=lbx=?zC)74>-H;W=y#3tkSlhB3zN z;S5gdf>N#KcLzLnbkPy59EM~wUd!}Br;*6dlU?7}hf)+rS-Td&-l)5kV~Bl4k} zNL827wjqs;{#TpMrJRn_{3SQp!4VFX^oTL)g4^L~y*0GNW{hgdQsZoD2A6uTs5yz_ zp{9#k-izy+p#4fX4IkaxfoU0h_KhIOXd90JJ^9V${; zEM^!c<+shfq&d8D7uk(P<3%|u8@&OH+~f%`UUs zp}k;Y@NdZH7`ico-R#@?Xl8Ka%jldRB#eTd*>zVA%{K+v;gjAx+)H}!%%0eu(*6CO z;huEs)_F46Dy)yMUrtRiHEQ3os+lcQKTBIzVAy6Qu1m=DlKjPwxdZ+U3g>E3?$ZSt zF#2AfzZa17PbKgU^g3J}Divor}c%Famb<5c`+wchafzrS^Ik^cYvxEP|?)7*C!{nn+~K>B}MymrJc zA=zKmtNy7iidVbsXVseOzx~x_7RRcyjs5@^E=`l*9YGW&R|@+|eTyeu=_IGO({_AX zAy%zX=z$1kCl_3lZbMPIQuhUJ$!LJUePijAa6C!KT$uY2)sg2)p=dM{&-}_zGVHP2 zm%ife8?JR)#x#)++~wXr>$_2iJ>qd==}(|zw=0^yE}i})q8_E|yrcfd21`r1LU@KK zZe!pFG~eG73F)JbjZ5!V>myx;=ePDR*vkOtRl8OSw#x`Saou4nir%G%OvEjZBFwbw zEnzsQlg!BmZ`1%9=PNlV4qWC40zjsGz*$S)?pxYYAg}Nem=`QGcSa> zCrV!Fc2l2QHk$+@itCcqNn))=1dF>9$?B>nzHw0PO^g#6#wM?qs{0DpV%?!&!4_oNBRw2qW_%>qdrENP5s zcf6$guaK6;kN1>1=^)|zhe|t6AOz_3A(8=(K1q(T==I1v%`3?;L8Hm^hMbT%xF{^K zV6{U0GN(z6??Vy^8PP}7Y6nRN%y_9r)7wx?_?Vve;T3BhlrzOZ;FCX?oa3NDJL%6M zF2Dp;2D$GGc%O4XLKs5b%^F=&B^@PtbkU>r_t8R+L_5;n(Lj$RZ4QridNk1GqllyR z{}#v~9d4a(V0PvM@l-}y2w~Hge_2e5?ifRD{C?aw5Mr2DHXJYfqtTtc75d82-POsV z$vYBM3VSi&2>7VCpJugYYNt$?S?KZ(=!YoVTney+2oU2EBAHF+A@a+VQ{+ZsH%!SE zL=o33nxM|jR@D{{uQLQ79GudNN~jW=><24ZoAWvj2?3SjT#URyC3rFY_^?goH#)tO z=E*Y`<~pW_#Wj3;OH0j_BRorAc^l7UI&aaoIm77Uu9?wDS-H!PQ`PKR_Hb?6>&DR# zHh<{`jhHBfn#heau9z^T%QOymQ9(N+OjjEiIbRF+G`hs z{PPShrcb0Xr!3>uN>d_k??0C6uiue2s1WzLV&$pl%#YX>DJ9R`D(`(+E^m%MU%)^w zZY@=ELSGVhAWqBOSABgfj7(KyA~zIZT8s+mjoC-B>`l1pzr7!rCm~PEC3;O`v9~P^}`In|PEJlX&%IpEcM< z7`}_;BF%6N(q*puRZW;UD#1Ni@zUG*w`IyCy*OH2hXEoag@d3KAM;#;9#+Gl6~(Gp%Ao{hGY9rlHb>$S%J**H{(Jp~>=AGL~ zBCekzva&FT^=CO*JqdIStK@v&Ev%pmL}k72Hw3pf)amvz)DirYUgyFur`Z)T?Vhvl z2ZbKK;$ML#^)gbzaPW}UVMQ*?Q=#v3>K#T+Nx^gVmIuRs*^>#+FbqIcp%Cu~4}^Kj zv-^jcs+o?Z+MSMPg7lQ;_`VAc^2Q$qj_@e7lig3CU!A*yATY-Zr5N2celfg#a=UBX z4e{4^;^NjY8^h{81mR{w5{Z>CI_d4W5)`VVPfMbdfdOG!oY+$Q8^GaRj8>}nNo{)U z{KivT930#F|4`D$)^apyzl8GHuTRHN7B_>Rxg?Hk5P7ar%G7kQJe_LrY8l0_FknT2 z=DlSIfC3)}OvyEX%BXi1-tT$;W&ilnI@p|gcktnHY-qo;`bnjMa3>nVG2GuAkOicj zkpTAa%0;WB4|kx!qf)Na)K!L!K0Mm#7XiQNDy^lMR@O$shRGP#6*8cvGPqK4#)iod2_cEu3jC0mh!6!c@uD|>XJtX(-fnYPfrvqf zq7q-X<*d5c3n9=lX%=A~rNY?wc~OBZc3E&)_*z_6E_nLGFnol%h%v`(#w3J5in^9$bIAW_D{?7Y11b5q`|9z5J;5G%H#7O1gHl;>Z;Njk|u|_s@mkh zN=Z0+luPT&hQ7~4Z-}ncD_{iX97OpcQl2{^yC-v%mGu0_egCTLg%56Tfzw@51=6(> znf0-eR5;ntQVNdrJMOtHAwJj2*1&v!WDAFPWn3vPHh?(%Aw3yz-b~aYa+V zy)Zf>$t#Bs5?a4qWcjbnOjFD1g34Cqtb(X}W_Nl*4?L)_oU5vYhIMfjnM~~^2PrE$9IsxvWolV_#r92A z4b}_Ct)?rq{0s)^oh~pYqLY^8l-sc)LuQ7xO)!uR{Pv>h2BosQr06lu(;!9v710}_ zt9=#_J&H;XeDS&}>!wAI-(+88e{@rTA$*E6CDOnf|8(EusBuwm2G7O+PE6w10WN)q z0%$+t5H8dUt=G0=^eBj(`C;*>P7HBUSsbQhg9-|IsL)XI>FBW|8&&B z!-_1k=ZELP`>C2FR%KrqI{HfK!Q%B-!Q8a0+x;?shEx{G$-6G(9?eNW8r9RPnbse` zB<9=~3CpW|<_|BB8YrE8H%}6YLI{-2q|!%O`tA2m-t79#nxJKp%ryRO7KA|N53^Wr zB3J9c?6f=OzgW2%7=hX2JnE%%-~C<$ZBbs*a*O4<5T=<WsGL&QolCm#U?#?q3nl>1R<=Hn=U={eRWs zxl;{AT1#JqOEt5sB2XYl15-(nHfve}J{0 zHS2s@TPSGI`Ah~~ah*Pt_t5M22UHpKab`dDEF6DbE!iH_n>9YaAtdwbHyLf#Pz9#v zulr%Fmz3GZP~luUStZlrRp78pcuSk+!}Mn60Mq=*k4cQ);v#hTY-I-r8hG3rVk9V7 zQK?bU7;=fG>y$78^ZkJDG{S`u*rC?qYOFq5I?G&-^v1#qWGK@s{{Gbg`gTY8k;!al3a(8uL6j-~?dboJ*iWMAI&(aAQi>hr zWQ%!&Vbuv6i`+khkdn^nz7INv%I$=sh_MG0KDC_WWN(Qvo}jgrTH}Kt9VrlPK6Q}d z@F3LpCdS}mz=@=;E6bSm44$+<^{435eg&vC2PLTGH$W`swcr@=+mzHfmj-qhKCZ0e zb{s&}jLy}Xnco(E+;i0Gl-zrWy&|z)*VwhVd|TA7`Ht2Vyk8=;V;!496k`gyC9ji; zh#23$MYU>h-FB<}1U_^4-+_{w+qF7kZ32{E2guo5X;JU^RD3lj=NUc0_Vz7WPbu-A zV0O0v17lKR%nte?=WAZ z`NUg%-dn6@f7fbBRAiU$H@6(o-Q3}!I5f9Kk+NJ){#a$xJ1%=KYWryX-wNOqnTd!? zb<&LAUM3`qYRsPe))r@>eT9KqkZJbcP`f?pxJy8yN}JXD)Wk$Qz~vI}pJ~~f?5yt3ri6K+3nf57N){WR^$XwrvgRyB0Z#4O97ckXqiCwCTB~N= zItBr@g7PHIEB_b08H#2o|9$C^jZ2R`JT3mJgxvqsV-SxxV=}FgdlUPM-Lw-Jez1w6=9EY*8q+o%C1pk(-4cf>)&fy&(l>w7u?l&p9c(Dz*@!S50=L54MWq!^5W0hT(f6n_N+HNteC-~QhyV_-Fb=u^~ zYjKXOu*$7(viV#M&E^ZvV4}vOZb{@@Tt%sbTh$dW1o9&JSKatV=87um)lGxbnh{5- zOSh7RC-;Xqm?IvZg*DcIslCE6ZgN$YwUm$fEd?dXLPck=FwQ6}Gbt-9h4$`dPoYU> z$V1HjDctRAO4Qc2$2s&cb*3#s%16X(fC{bghq7OCDW}Bf+?jNp?ym61%c=dz&A*NF zZeJBC4ry0h4C`I?SW~*LxY20{Tj4ogO%9Vfq*0f<3)NKtM~n}Iu-(mpcjxdKv#OaUa)eihTUr$bK4s0X$vjyseqMZ)6r9y~ zd{vFLk#=WrRMn(RJ=aHCMV-X#G&kOdeXm`sHj7V1Z}Ei~s1jTy*I10)*$m3hbc9yO z%u2W{v@2Q6lavegnF3lm3m$xQV9%!?py+ScjNDI0nRX%}O81isn&Sf8$3TwP_OHME zDyA5D8Dlxf%f;DDvoA%s&Jo-KX^vfN9`poAll}P6{b~+hh3C>f`@a%1$lh*#U&VQ^ zM^PBuuy{~AU)&ko{Tx0cr!Ie@?b3prKl`>c8nF_#u!)>eZzHz_eyLtEA(GtM4i3q< zPfdw`;#F6#l|PWaN8xO^&TEZIu$3Y-K>?nET7>i+yx%LI}a5r65b)%PjJR+3Yp? z!d=FbpcClWZP2A8sYk{IGv=UgYn^Qme?Xc$3HAUM#jV+h(|medI52yK07+2}={?!3 zTQFuNxAQ7t+1pEn3uk0}HcW%rfNl0hhyHl@-q{%s#@h-kRFx&4OXDfj5lT#?XPrrn zzO!hrd>mxg|J?IyVl&znv8U`ei6tk&rqWK2w&1%(#dwQauGACGUV3W1*w%^N`iJ6g%i!Su?T)NeBIV16pkpYRPdI`_bt1ewLaed; zvDJ~47zjV#eT+bX2yIM+G4}LXh|!$&q5mD;0`AJ021N~v`URc#Mtp02CNupqaUkKj zQ0n{8Pl#I$SMDxW=j)=J&>tK8DwW*w=No)m46#q~F_or#%M0(QfbTkJw*XUN@K~x- z5zj3!X`@m_QBnxT`xUMYo8RRL8DhDEPd-?z(+A7v&f?*t>3V{DcAge zWMd+4=}o9x;heaTxk>PSs5`nJHNRZApSffyIbOiJw+D}wgmNc%=g`Dy^ zEfc!rl-o~{G4LQMO}$x^r7_{;HMzrOqo@$*NA{k6s-pTC{b?pt1s&ifTSYlVlms zMCG;b7cHJfzYL7;YAwbDr}En`0&=zbA~vVnR!GsRCB2kL91`G5wfCO{l5aD1l#k)M zLwEQ*WtZ-=$I3={eZztk8`3l``$TlSe{FOhEX*z`Cq1tfG^n&G!jX3Zy`VbVfKqsv zBK|kK6|jhQ$ocl6@0N$YwJv`{`=F?{iIhgHcyqYm*8N|EAw_;H;M>wT9_{wlFZM## zKG8m#r7+t%ig)i zm$YBszJIc^o&VR(DVHw)NQ|q2!;f%R-EYF9+F6fGk#AL!S$$WDy}uzlU8gjO+eBYV zDDmfxS&xht+gZ-iYZt!Y4XqqoR=n^y!a@Ob9To^-OUc$O$z18@y|lcIp^QO3>pV+c z^ZP~y=elCHbUoGVWL`jX<}jECsCD|RO8wVoDAfPcF8RNpJmA>}aALZ~T!p1Z)d8cN@BbG3jJO-@)+SG(wZ75ncGuc0jfI7l#yZ=* zfVZnGs;&=eRlaafX@R^kYzN^4fi!1-6g{Ik^2od9glSXQTm@TK@%;I3r!whnkc>9h zrkYm^wWUhc;|Sp>kxxO#EQ$`3v!%FohmP0IE00WDti;rCh+3RL*iUF}OoWN9N)l_p ztuiG{L3WMH63PjRB?*!i8NY(MO%*wwnA<#U(Z0;^5snhsB0bWJX`5({cU9OJm3=H$Smu}~Gv0Pu67 zeU8zd&Ud*Q7gTo`Om*fuO-H4&Qd=?FID`l!M>8xVRaq_knPxS#pB5FKK%SvAF>fny zw=pl@Au-xhwfg*Y+gN{7#j(Ac2IVFf)}dXwA#k9?gLj17W@}Nf&}NC|3&|g|wzCez zrC{Z4*UrudKnhZu+%l_j2tG0W+Y0Cam8@XyCz!Zcwl`HOA=O#}W&bRa)U->F=gHBQ zcUdcvMpM9Iai*GeE_b!w5Z732m1UmAHIMol-8ywi++>zdtJF4AC~26C@od}o(_(`uRIapmY6?t!k)UB!-JFSr3LY9aCqyHSiDB97d>Nx$9=obD zPrIzHWB@Bb)W3mfa+`EJO9j3m5%m?;sn!$#10jQX2 zr_*h-s^;2fP=PHyNcwB5WCk z)xb-TlQnTBqvv)_W`lut{QQV+%dTVrsVx!qoQ{r13-Q(_X8C9hT zqs5&F`pt%5%-kLMcZNpyPfQ}$-f>Fve2i?`q*3;Y_-^FnI2bmW zcR$<-ksy#35f!Z}BA8Q0?lP-+1LslInT7CCTB?!pixtWzQHToVp1DZpUf^*^{quWC zvr4{0k{c=j2Rzv4qy&UO+i#gy#vmXBnL6jzkG)4BSn0pVre=%Cy^M>A@aUVUGX=YNYi z4;^e~xJ=yV0IR4aNB5{c3>^>J}tWd@iDwB?Xv+fL>XwlI(d z3WaAGI>aR`qVug&_iG8Qz=|V~ndB8pilrbI!UDnaxC$~79KOPrIcPj);9{|+OjKHZkj7RtQML~-xDE`U{YE&gB$}bqf{V}PYc0ewGiz5BgzHh$?<9e zrX0feImJOstin6piS@N!>n$>zrEMh)Z4a^Frl)*HEep%3Q3kn50 zM47M@>{eZ@!QCi3NiEqWeDYV0p6WKL)S$ zGD5#AvkNMXva>QZnj!+}KRBW>LI=osc~~Q=rvE>M>W=G%Kk3-J($7jBw*P;4tshfq z)gHF1j&n-N{Fhx+djixfGFNFyzCJ@u^UUt!`}|CHhe%SiL#WgglV8D!%miFa4ouJKm%0^KNCGD{CuHmy_9U7 zg`0tcof%|2gJV6W;d9}GqR)N}DUj{>yKJ{KnBCxm-lw)%>9(Z^)J4XZjPD_I&EIJX(nHd&5VebjP&(ni z={T~;cO|iZ`C&7iFb=C`FdIJY_SE>zV(vS!T2)y=;16+*gLiQ>Bw_;e_@cO5BJ7qY z?LJ@cpf{2lO1c+BOT)7l`OA$_*7wXBX8#P1vUFKda&BeBHnYBR%-~53dJ4O}PGeij zZIlkrXXITwg$7#VozL?|v}H?!hd@-7Q5y{xl-R=gTA$hgJU!mD+cdq&?vB>^pUrab z@bTdsD~I-6XVo{V)|OQFve&=ns+FOr+Q>;I)Y_**wxlWK(1Z+HO(d=|nA18>0&6t$ z(?*y|d=(IRqD=RJe@aqnED07h$lcyjjXVq@$MRmzre8xCSEp#~oSBqV)NIov-`dTu zEVOne=Wm)fUm79lgcCdnfwUhf!JYBs93bW9JyVEuqT-VgCT=(!5a6Q2e?CYhHxUG4 zR2IF%7mll?%<7X(V!hysXWm!B!(>YjDWj1hFr(7@4qtdcjicS~4I1>q*F2&tSFF=F zzFUpGD4h%NL3XmSQ(6r)i~44GLq#P9Syb+@CQ}Mk-ue?X z$`@1wL{`k?nU&E3wIJ-*I1S~vE#;NZD@6HF#5p^bun$y*T2Qt8y}K0*J9FJI7vxsj zdSsPx%Z!-MI#8Dy)p~0dCqoMp0n=z@<&g2Y6%Yc+45AMOs0>0N`iKSl21EO~RO*Xx z{W)v>9FsaA;TPiXi40Pbe|VYmn3EfOk|Q4PSZOM@WJ$cNd?@4`4MZ*d711_)a}UK{ zr-+yQqfWF6pe#tJ0I4a`?l^=%943njA7L_CS?o7?B9y@l^0FgR38y(Kfn=n^6*0T^ zho`mndV&x^zh$Wx2t4XfsXd_mdHZYZxxt=2tqVa27kz*hzmLG9c3rYV5W>GZqEQM2 z9-S|#VXuE`yaZxZ_r2D1mnS?CqeyaTB{}>2w2RT1e`0$@6gfFc2~VsZl||aQ@!I;2 zg6>Tw@^S7BwKtw{IO6es>AgvGNgYNzuD8j;K;lm~^cc-pPiPs$ESXJ`vPhMWCGB-% zFs71c?wOg)yn%~ThCqMd=OR`~e}_J`BULm8Qs%mIqm z{IZp+d3c1p4Gm;DF+fr!A||H$+=3Oxq1{~r5fngtP~7|q zcl%)`6SDIV0y{U6Y>;5?V!(E2XC{b0U`qJkzblW)*|3M7 zgYIHRj!nGICyElHc(&=AwW7DR=xiCwI#~ zrO)jz)PgI@=Op}#`*WpqjC*XdS7E_J0^F4Q;d#}+W_-!lyeSEbato$$o}WddQIRJ~ ze_+Vf{8-kvoT1iPyVin_rxm^OK(?=`KZ30)H)NO!fL*<*0L?2iti-DN&-en29yxH> zV5wtnKOB~(=YHtl@@0A6yZ%tEb6@c|S$V469Z4E^T9XW7($0Pu{s$c|^j$us>Hy!??$$<|{vN!QxMAQ5@`y{K2R z3{9~&l8(Iq*l~v#W5?f@v?${=y&}QVoTB`jWRT0a4B?H*A4|Cmu0h5Z(!9${^8;zw zuc4eHWpgokEa!r-X%`t3ckgLd70lG*ss0DoG%bgNTm)q|OjVYqIW@~tx=m=uU4x1! z^^UzP{-R2c*RXzJ1l<;-1LAXgbrax;Lgxi!Ag$a7FWjo$y<$DL zi80vZEA}9p_RkPo^BxJpz*!@Z>O%#p1ynv0Az~*_|?S~blgKw0+0X(K>IErADm=P z%e7KlJdrM?gWpdM{=2Wo*s+Kgz@Mm$v{I{A=!iE8ILPO4!3bwYl21l}9dLn@t^Jqp zhfmpH;E+8epT|k`Y!d@j*;(T!o(mt(oAc@1@T66$DJ98w8Yj&TiSLn+R zC~g7pEaN>KAU|*UsHt7WKOzrO3N^E$Gx_AC4b|?sh4GC!r;(-V77Vuyw_1#@-*T04 z+rZOTy~r^*mRjoRW0fxH!&N#;82YsKvBYTwwjh43xxC%t&yHdd0N*-_#Dc!;rs_XF zCh49$IOd6zj2Gz%A~=VCzy&!BI04@BbJ?^E;FFszvGO}wChh+pt!gQc<=gO#Bd2Gw zx&~cJ2Wyz9-_$C``sYR`q4G|*w|IxYn1CakjH3#xmiH zQduI?@&k1sBBM!X23VugYtatO1)os0PN)fET3s_18dVqftv^5>OP|T7MgokH2O5^| z$h)Qlxv(sO0PtTQJ%f}X*)xIv&*m+NNmsE38YWqmlZi33qS{*5`L1=ZXT9muFpie7 zcB~x@L)0;Hj$DK;Lq9>k(?B>)8|hBEpFT>D&>Qeg_zwNX5F($55miJZ(LwYOLrjvX zVCplTA}-5_I-2B4pXApcEo)sH+u6ZR^!x|?alig={x|-IKkkDPUShd+Ygo_q+X1_9 zKiyYXd#i54ZN0sB^iCgidV`{-v~wG|`?!y&5LHjLQ$5tMtVC8N+a&vy_eWevE8okH z@bma({2G1>zmwn3UqD`^Wzn=W8*ROOt9+0Ako*SiChd;=n?UF|T}!vp-SnW46w*S2 z&>?IT?h{_gCT05+a}-Z0eo+=M1{qH?PBWfkyv%rm@d4vsrj%4LaIGFAucGTXs^BWEx-BX>g{EAJ_emUEF?#y!QO@MiHo;@kLt3sQm| zLWVFdY!WUIZWmq_5k$BsU(_aAD0)Wpt(Yd(i<`w;#ZQXglVnJgl3vMf$$rU2$t#lA zCGSc;mRys3Bl$scNAjoSKa_}4PzIWZ3Q;MlLJepPZ9_Mqm(Z^=O2= zG)wB2c1WkBk4az03AhT6H;zPwX#V^W~@}Tl%l~~oUdQUA-Yt?RbP@Pm)sXNsJ>K*E7O@^jOGgos) z3v2z_McQfYE7~`;Kj>&Wo=%~&>U_FpU7v1Dw_kTs_nPh--Jg0$kLYvtHhoavr616* z((lwC)IXs=rN65GL4VJH8|;RVp~o<4*kU+pxMXC(mZ`-5KVt+m67uevVySFY|`OR8F=qtP_L#JPF`}rO#iYH&*Ue^zB z!5D|~oWLHIVKI*?!>)5SM@#r=n|<#qdAhJob5!KP!f^@37co~ttaVVJeo^LXMqP(&13)#wnL<+4krW7r~Pb*PcmUYFkg`4_~D%EfrtdlW#AHSn_CPVKc!?Vhki9VfjIA;*2w1!PkPa!v zj|ru4giGX;SWlZ~mG2daN5bQFMAguVI-t5eaeh`7X#hxSn2?B2gcyktdC!QLHk$#7 z#bb%gU$^9K?>+jiWUfckprJ~H|lW zb>8>5R8u9E-`;psG_7@wp8p9yA(bS8uQE=rrv840L8ydf62a2(`v+}=VsbczB}k*2 zlU>Nuc9GYQ*aR%MIgV!Vb}zrJl;-<{mWfuQi##t-D)7bLIo}u%4@Lb>KzaP02Pey~ zr^R`Yq)(uPgmg?aT>x@{5P3K&NUIxF1wXsW;*hCO^&xHt{opkZ!#4b{;1?&>7#?N> zTogK1<^R*Fr^gpKez7AS(ROuWP8<|t3c)Zjw+Q?c`^kP?2tV@6QucA0_7gwgp?u=L zzbqw8CVm`!vDl#=syj&AAJQcne>SgYZ)7GG&d!3S^%X}s3OBT)PajtO)Z}}jW)1M$ z90#3f%_Ik1H(~}f@}@Ygh30tj#q7qmM!8tdf|cV%vl|iSxUYAn3Ql+X=Ek2Yv?7Ci zFa1^X5RMQjFypHx&ylD+c!mRu0s(Jmhbk_1w14>eLxtv5QyNNJYdBosj?E^tIz=Lu zanILL+boR&n}5ZO-kexn5??pL8T>yef2V(7rwYW$k}FP#9Erf00zsU_@1-YKMx)=X@M{=LgpckPaN-V-GycOdxj6--r2+2|A zc1CL>`Pf}u`VjsnC0Lxv-P8|5KlpfrU23Mrl}DQNbFyx`^=4HETSr(s;u`rLtb89| z_vgxRO#!@xm+Dlk>@3)g7zc@w#nLw{QgOyO0m-bDluAu&h|ph<_S7QWsQL2`9z1>m zMV2{zo0N;y>_aCj7-VckWaT+Iaz)SP_D*g1?=UbGz@6hba7+1%V!ytIWnV_Sf_} zlfmloo?{T)RIR+q%8=A#<<8HZF#Q9j-pRNOFxTG&K;t_#+JdZ=86rN8nmpKJU=^$U z5^TuEyf0Hz5Y8DDfK$)K17kMDswi+lxG80q?Ku*cpYL6?e<-6KX7g_^rNm=o06pJn z#$zR!qb!Z~I;xH;UaU6#xHYAn1HsLt+Mh@j3g<(=1M1SfzUzJITsi;$*V>2TzIL|1 z*bbfAsG7Ftu|rj(_p7Qv0ytYHmua4I`Y|PEAWkW&sQZOUL{W zCs@T-8sDMir1p9n#8z;^=ef}osb=`c_L3MA2kcUr5ZWKJnno$ek~PRqrPT(WBFjij z$S9^xjq%C)}qOM0~f%2RLrYA!5k=Br6?*Um3qsn&(CX{Ove^iJawv?={ zo-MWc8I=-y4GapMUM1mY8U(bdf)E>7`0fo?mA4#eCRG!z#QODSJ|!??0+X>Pe-C47 zTufMb^gl7d)H|+5qst1IZH|K&-{P6;)UEu%x}A@86TR$*Cm(+(+|(C}2s8gNiNQfL z=ZTi6J;&AoHpV2`Tkud(|6JEf!{AfI@$G|w-iO|xW%%AWycz`MHgU^2d^zsVu{ zd-6L@|0XjSnjmuL&@bo+WeEpQ;=w)fvvY47$~^dRk6rQA)kE@!HmGr`&&!}VSf#Z( zJoqO7qLeBB5B%L+TwR*{-VnKf6Dj4lixW)Buw;AME?44L5_W z5BXJH!BiWa*b%yA{tR;6nAZ-W@?FZr{?r5ph=>t>SOlf+!8iXu#xWA2iPeOZ#T)kC zFa6BVF4O$xzxXcetG=lL%US|Hf2<}EeB%g4PUEAO82ys7v|$ZQZ#?slUZEIxzCq=U z`+7gRFhKfY*YQWjzQ})-#^O=GYB5v7OW>`X&a>e_aXhc z*@-%!xBK!DL7qF!=jzmD+|MQzuTT#Pe6Rqj5X}~aXxZvZ<&T``_Bo8IBPn2-5jFkV zEp2h&n{|oNI%hR4fUk$QC*72fpK)F7Jfav)YPZ@~^)RkH%mS2E3yR@zBU{a8?5nXI!v>m+#dmBCiLZU@oPo`x2z-fFYJIzTtx!$NIm( z$LnE9eba95iP?DBwExNL|CI#Y$*;11K_0q?x4BUa>L1H3eCa)#i}9!dAR7ZCe!dSh z(G`;}_M~bGJOewCqe8P&6#`ZG(m-=;*jUF2{uBZYn)@O%{=~8@tgcC0)%nzDO%m%< ziK)i-EXl!Y6eh+%4>R0q1xe1Wwa>~0Vjiu!T+G8<&}a#=kTFEB8dpmD?b7PklE+17 zZ2g02CrnLRG&$GS0Q8`*EDP60vb+`svOq6+`dhe1A=@XD}MjdpeuqD0c9ZeER1&Es#nOUTLkBSfnw#pnJ3# zbO^erY!leEElX!(m2Rm!Y$u`f)v=g0`V*dWKx(6o7W^ahN=aiW!dx6Qhp#=&sVudP zE~oDs!mL2CwM#&PZ1fkG?JZBNWF&ztIle#KFO>C?HRS{Z6zMsTqAm8RVNKe1rJBPA|6{=^(gV_U7 z?&aaMJ%W~I?6zwf&S>M_vbxQ$ZQX^Ta%FJNBCHOj8m#8YJWJ8Xn1hp$#=wR9gyDCM z=I#z@Y9Bift@V6_lBQiIqmxBHfQjL{Zrxj!b77Votre%9MA~`|c=)8^D|3Y8;U+Y# zb4S z)FAfKTPu1^L?$_2p-n^t-%VH9Ugqu<2lBjhA5(Dsdr7{Z9*#~_@ z#VNBr-52HAOx#kPaDue>bsEf4wB>__HMh$Hg>7-JQ`%MX@rji0wb*4QR7L&9acq}C zYCPhjHW_#^0d<3W%Iyao(TprXxTnKH1wC+~Lk#p_krT}uM?r*O<`jEynesI1PRQNE|s>QyQw1T^rj*=_1lobY% z+{1$15diXKP8gRNMjZi$Yok8)@5vcjUrauoy8;7~x1Y+&@qG z#Auky{YW@aG$mcq1fCZO7F)W8Q&CfIsAL&?i1zI6*UFN5GH&)&~slJTv_8XI3~Np^2^! zK-!Q@b(gRrT||f@g>nLT!as)MP1#C|(Z8M>?+@0{WX}GApRhLcEyfwJ=HshBqQ?0q zrZvo>?c|dqX~|b6pBRX8WyndYd!ckXY&GeF*jfgc;`AXtdoT{wb2M00Q8PF$Rg@Hf z=JTrJ{VA%X?j8K>F`m`+#!-oR4b|q%P0aa>0{UQ$RJmY!d_ten{KJF_M?_Nuuh{wf zYzy^zvP&z7x-yr{%M@cYdjxir(+mVh@CsvM=edrAw!@g$r{D0tzxQCIgk{lY7`bKl z5#jKH(N#ixB$H%OAHbqZRW!1Vp;ZIxj0T%#?jZMmO?Yz(1uW}ZqzjT2C!sa8!xNRg&kB-KWXW2oXIHfqu+PrQ!NsJR8U<@m?~FV@~z z{IGlSq22|C;8u#dMktMd1ndd{$Rag*=$*9*zaCupMKjY(kbNVREv+V5bS!rm?wTE_ zQcy2rg+C_7<*_q}Q-G=lEuCHI62_G+^32h5&Uyw++F9p`G7_FJ3dCY66J>mw6kIqV zfytp;qWnb2gQbyqtZDj<^?L2*Yn)b3757#WP}b-jqlINj+E7La_QutHFqf03Vwef` zN+s?$Pk6kKN$Y53iY?A#X-omOQ;JBsBY4xut_O4590y(KOjLt;?1GPBW4n2Vp$B9$ zqU+gg{k#X&9xRc8%}YDoT@En;-c^BjU)>c73Zs)mLor|>@ucp@VHo$6eq@% zkf~bA`)nAdj8>@d3zprs!{X7ookd`0JUHMp^>7;e6~E*8?teHv!pylS;19ZPRa@Qx z7*%6LZ01w1Bf%I+1AtM6pDH-G$WRyC@DPN_xe1o_o$bF54LuqlUG+^hqzBq%g~X|R+lv&q{GgH1)2eKTn|sXM z4e8EhOm|X$hm#Y~`0!m@8EJ~J$HhB7bFI`i)g0b_8G{vqXETeobEV9q;!%RK{uU)I z*85di3K*w)VrVxWbZd(?Hj5XbrJ+AVaQFCiQ&%y75wC<}Vpp0nr|CsLOrS&TF_XE# z(HqKH&kVd%Hr{(STp>lcAI6a|K-+sox`w{-%Gu7~C=-kpW>RXpVS>e`SZ3`(5E{Ii z)Q%+JdqG^aA5}_8o5&_}iJ_c%*xK;BfMm&W179WM-RP(3H6=;=OTl%esxHR)PBYe+1(DEBCa*Z4|}*_B?z~d5-<0`r2z93Q47wrfiI+cm@;m z;<#^x_H)qEQRg>k+MXBEp!muk)=xNP<9vM}f!=KpUZY^m7aX$bdB;jg)yz3Z7w}of z$<(FmI35#o8EI@9qD~<@spI1vRrrXQ+@39gSUidINP`P+%XV+ybkB{>vl<`x<+0@`2CN%HR|s6uP#KjI*U_%)nZ`ys`dn2uMCh(;;51Fq z0Jo_rqch!q1z+k-RqatI8KRP-poi<5Mk^^*HyNdUZ1^Nm6<^;mBuNd`ge_sY1HR`V z?N&CEIDt0yZub(W)7r64{_4FG-J7zVPmAI|2uY*6lY9xYrB`nsOlA<>?JW&tZG6JD zG<4UsO?1|cT*BzHW{`}@U)Lv;Mout>uGVyC`;^_-c;C!ubDT{IR&4uX2Cvje>v-?1 zFcB{Q7Aehx6-C*2VjE}c!ysRP*s6-)^Zm@$MVD*a>3m7-N9t@FV^V{iuPS4y!NiH_iyy(_GYiA9QCaQ@oLS;i<9NZnygn3K zH~r?;*E6uI6aR$#G~=?+X*&j@gX6{d4hx1$x}WAb18ancOyrnl0nW9-$>Fi3r45fQ z)s)z)ChNNGhxPXC61KbA`nIWq&g__9h}eFgan%;*itHOEgxN81#8yf0Wsv8?dJI?CBK5lu*on_cQ{b_!Vljis8seh;i@XjPXpy? zUQhWe?(rS);vaykqsyefg^KpVxxvH;&nMUhy4D!&^D@(6xMU)f6jDUEI!#W@ZK6z% zCCDsX(y^h%jXoY=DNe27M_{Jd)4m)*p}JZZ(vK6?XKc&5E-fdyFGn!K=}xBsiVQ7_ zs_m!CVx>b7e35lkSm$#(xJJ5U_nH$GM1wXFV;7@f@$ij3VjAGbL#*(sDrX#AK1*Dc z=D=)>>fpm%GR7!`p`pav5`G@Zqz06WV5OLn)LUyz#`sJuB7Q`b>Zqy?qNt5$_|K+# z1|_&HZ;%pwI0m|UP-KK)c6Di!k7IYlgr08?r~~QT)Q>T3O-87tiI0lr+(VEVu z(7(W<*l6-VdTcLjzC$e7q4ZBGB}u8!f$vf3dd+!6?#WD{D{9LtSw1z?r@Q3r?43ZL zhS-*KQ#jwqF2Jm59r=dib*>uJP4Aaa;Y#&Bl#s8(oV|i%BRph;iNPy7IdqNl*4t8*Do;3$ zMsFl%OM+TH#*xv0UCtriAz_q!D4RM#`EBbKd*Nk>mPTNDf36&tM<6@Fl{B#w5nUc9 zt1da766|cumJCWs@Fbw8A_AiJ+n>-O5u*9f!GDy{p7|67?#ArWJ%&G@%<+}Ha-X^w zokQTfB@R^Nyh#^p$wRtcdyH{m!6tJG2W=N~4myK-WLMHgT^5gE&0?lb8U(3dbHHA! z&#Vo`Vag;44x;@_v76Pl%-y@&5|sI~ppGul!tQIhl^m{pb5Y#>bJIg_Qlc4G1yUH2 zy?m9dTj>AaqyByhRA-S6h)R?XZFc=I$YsAQo0`JeeT>jHIW83IP5z%d?QN&p7W}G- zd*b&zd40&&pvN~iPp+?@e5(=|r2)Cz^ZUF=4zkYF=+Te2PO|`KV>6W$s%X;`E|A{m zvEr7#J3C4nPAUT#1$|g^KjZF_=eSuV;;&~AoC|3|;vLW-p^TA*P=$r+xEejsAxKV? zLW<_LTEIbp$v?mcfAPz5&?Ed&U{Cmp8IndHJcDn07;nf--K9HGqjo+ypH18~Pi|g= z>O>gK4VS(KKq764&_NNh;8a1uoU&6T<@>kuqgzUZlHPAtPt0W%|(x}K5f;^_k2PB3jceW!ZjZeE= zW^^mv$SzsW;VctZAM7823_f*f9>?$-ZP7T8u0^zq3C6qZ!Z6z+@>XWWW!Y!FAqe%o z1IU!u%Yf4EHa?meXszZ$2DQe#V3mwFOGF3m{SvNlJYVf;TOjcs5hWm~bXmpE!6 zm$?NdcmekUP!)c4gm?<6wKN9dqgj z0Nmb{fEUSEP3V{#);OfZ%!65(HFlGPQI14%#sWAOAhn^6lSbl}%%Ffo8u*~dMF}@| zqe5fiL!ewNgT*Q0S{fhx3QB3Db&e`G^Bg4P^a@j-^km2ie(B<{cec(Mnyy5`-8M7u zYtWNaNiY@RV80{8(JwEeNL*!!2yLb|$z`ae-iKO8rlP}I8v0zbITUlmDUgf)d|(ff zdrga(0_2&1D8iP3f%;{su{{s>t0c?bdhJFdF+^h-F>X7?IxB!|7{|-^dDE6xHhoEg zQY&rq5c!PVtlVs?ANyFD+W;p+=HS5KAZFgJzq3&K;@WDg6(eNX2Qp*bbIhS`)Wd{= znde_S7D;F3&hhSc^d8$r%{9&dNe&4z5E(ai*3z0cPUDS{TCi0sdPUa!piW z+d@F+eW7BLS@SAA$>#ruWppUtv%W9F#<9&OS9-;8mByyAW%AQ2D3nVg&<--D2$#Yp zfx9PqUZis!a2IQv3E*2BS7=6S_!hJCJT9onFw3E5lgkzXO)GUW7vjB#e|0HYMdCGL zo>Y88$Wn(stT<94>J9bgU=p+s5l=}w(J=v^i9_nRM7uz&l1NRk;Q~4DqTH7^b}YC> zHx8fCU8P0^9zaYU@ZxqRy{~Wbi@fFQUxRE z&n1qbG8tAPb;h7*F`brcY5^bas93q{+T6MSj5OE!qdPQe8-Bb&E}EyiKW@Xks}r3@9Nd3Y4X#%n94 zF;GFT{lg&R~{g$ke3gz+$^YD0d-o^xz18Ygqi+S|9=;nK5;V2J2)fh7x=>(7+i$QQ@ z7s-sCgi;36lI&cti(#>%$P%Xr&iUBwU9j<-WT+I^qv%i>r_1FZ6_vnO{x}MET1jw) z+4sIyfy*y;4La4g1qgXXGhSP|&7&6_j+PBtw{IW!>)lr~6R2wt{6O-Fwxn_S{^@wU zY;PfEnsGhB(Yx?Lgo?Uj5xa*sPd;eFYw!8pMT!M;?!3#YQCu9XFLvyHf0ldx`7T_=N{yAlfy&_`3O}c3}Jz2V8e05)P>)E`BWzo7RO}i=2;9YjG zcFf7lgtv(;c0Jbp62LmZARmlfUS4xq7{x_?E$-LDXuTTScHHeR4;Fgg>o?bH9&WjG zsZv@-qN{g zut!tGizt>7j6I=ov5DwXa7GAz0rYa1n?hh1M+q16vSqezJcf%XpxEA#MVQ`6FaH78BuY8p$&rnduY; zVi9JTK6GtsJGwXH;4A52TQR>u-|4H6zLDK!0MtBuLd6yJBi)E3n?a``L;+Ilb)xVn z=ou=f=*HFj65JFf_YcC)tE!9%Wb-7g5$9D&)O&F0*z&}Am(q+GcG`!b;n>*Qa18M) zcrigqr&#)?UuiBF=^aj|+N05gu>doM*che_%&p`tlA_^=FqE@?p>3IsAEj9ae#jTc z4m7Wm(_dm=Ht+=dma1g4SD7zJTmXmX7&E1KN%0Bi8B?yhx_I=Gtc{6qeWx~KO5m!V z&{+|b%@2y&a4`X?_5I9kfa|aYBNQ3qo2iw_5+3f))24j2gH73ye9=`qaF0htwiv5p z_LU-W3ct|OLjdY{f(kE9q&Q<3;x=E5MD=RP6i`3E%J`;b#mGZ0&JG5X91cWidgc$# z`eEp;b!E=^L#l{;8_eb#AxTwp6LPU=X>ph`vCeI>bjfkFMTeM~HLR)@VCT8uxgk1yZA;^gHnActZd%77fP}0)B;)Lj|pPoV2U!>zObSzWZ=s zXmURnqBEPLWT}*6E+=RB6(KaFDR|S?HaJOvbneu0qr znctiTyyp|%^Gj-4tdzbBN{PgL77XP7fj@j*P@@j7x1aGz|M?8PCy8bb@PW>#H`%7j zBMIWujXVSnQX*ERnyC=`yBrIeDIgajCnc!{Ipe1~S+yO$HFIBPk znRaT{m2`K7p-20N9&4!w%jEFxk)nwmm0AES*c0gRFPqtVT`c@x7IR{YHt6_~5T-`t zm~=vGiOknG3bx3*;y)jnikUe0!-8BA%7P2ra!y%Q!B_5Jrub4trYsG#;i7R;v1-s9l;)8_LuhzOg`$?cQYHu@ zvQ<-i-&GQuuhq5;$;3>5cK*nXuJ*%in{&0L2#oItXS6c3QW~QZV;g-s^hH=Di{6CZ zK2QI(amzanCMjO+my#@kM&oBN*1^ z7BTQS1T{a-y0};@L0g`?FZxsjg9Knw)E|Cy@ha1?M9W67nGJG4AL@y^-}w}-M#u+15$mSRhYdQ;jsg#kjK zQV9hiwAbC@j2pI>65~~EXV-agRUH(a19pb0L92roo&M1jLA0n^o3^%TS6Hj2YLwQ3 z5)8!#)|aU5xF&HdE|#hD&bf%bClM*O=1`$24sQM(L-b~ZPwu0nom=8xWv3oETzQSX{FmAz4BuidFQ&hJ z<5idZ-c^DTaAHqb;?vOBz zQ6iv!NUBW)GYH1sKQi}Snwf1+Q1lMXlG6e-FV><9Pu6Zm*U>FB0E_QB{=^*t{l0dW zD>b0Pc#>`ZEwP)6>8sOhy}0`G?-q;FJloC@vTTQA*$uUqyxq&yU^OokwN+R<)`TT7 zPpnNS6)R;sSr;>Em{qNBT3n%0ZiX~XU8Nvzg&3HTUo)xfI}G;JQjE|PGm?YKVV{dl z$%Pg3Z;GK6oVv)OT|#?H%TGG;_f>w&gWndiT2vP}bewbV;yc7QS|ho5V{h?>!4Ltl z;0E0x5_+QZgb|cMBBX%_q(c`lfw6LEYW8J--ZwhmY4>tgzH%*xtF~J1CN3#?Q68^| zQP=JTL6H62k-*W1km?{aQ8%lHH{s?|HVFM6giT7 zI@14hEUsH(c3$^>XB-!(S-6E42=nE4VRvv)9AokGd|My&YlIDN=r}E19e@FR3}Fxs zo)Bp=6fjyKU-d|fuMA>#(Jgb0)v>LdWfh<{XdpZg6YIppw%>t9*Ik$z>_fwqK;*6# zs{D@9D2Kf5IWgF1+`@~!)p++ZH~;I47Fb& z2u|7u3GivDfdUsliYehkQ5v9u1*LZlVvuNUkh}~S`rqJS>TiRKF|WbHT&uxH-?xSU zK35L6N^m_Oq>0R7FCU-`Lm(Bdm>!MX`acF>d(1U>WW`N_X22Ru`9Y3=4F}oja-g;+ z<+CevvkR3Bt1Ped(2UJjR){xy=f-Xtvi2G`q^Oi}$nq1Uf^3O+IxsO=ab55o}p$W-bqHl_nU2$S6@n`UF)9*yo zCz+`Ksi>D?G07A~CR?M}b_2XQp*%ZCWwC^;N=#E~>Ud)(8futV4j6ut{{wj|%9p=D z!9s;k_8*QFKiz-1;z~+YT3O`}6?&Q_HO(WK-q@BM-RYADRb5TB)jhK5QW|Tjxt3aM ztG$jo>pHWqyZ5q>W;EE)NrBfitJ$}vdCfnU@XJx9nJcTjKb!854?gME+Ns+r9~ zo86q|vY=qk!A6XA4Awef204~WR9l))*_0i2ifJnuUXqbIiFVRIcG-=x_Y}upjvsN9 z2*;UJh3RIhGD}&Dsy0GvHPtG2n7GCMRMV1{wyfm@Z@sgRo)bq4jZ&vGNh?~}s#dqA zwXLhJ`qsC>9Cvgv+YNW!bj!W|>hBub*rqnOrLApidpp|Mu6DPlz5Sc-%kdG-)3d8~ezQ>tk}}sLbN+rt(XD0V}m_0N8O9$b5~8u5#~ ztoUxVWcGAN4tscus}kHvImsuNw}18SRH9We$_k^OOu)gP+-fv!GIZ+aDU;~wuw-T5 zyHKnW&-}Wed1y1Nsx*&mMpc#O&>U=nzQXGvbtUf|D-E^C)PtM+GWXL+nn}m8ifm9D zFwTPoR?up%jY$k{uxo8aw?FA^5X5p2^487VX0(~t%-07xX)kmkjcWDyO)_rg&TG#Q ziP&wFH`B#l zADN5&I--LFWQ7&Bu*MoIY+(yas0HNo)-0K@={#0!9q zks+`G{{cV@z(~9R=olFSYs?}@o9&=Z)`dfnhbi@0xgs;?S2x1M)3G?b_w;>D#*+zo zE#z`g+kUQWTAytO{Qc#KRv`e$p`!r<7+Wjo9?UpI5X#0uHP2; zF{NMnTe@=rd`*X5Z_xz&ykKnS(qMu3TJ(!1;vmW3P!{GACC%rbPW|Gs03KO@W18N7 znULNWpw({0r^>4#pfR0)6r_h>>D`lJDj8%gM_Nk1ao$J0K>O`rNPZ3e)jm>Y192eD As{jB1 literal 0 HcmV?d00001 diff --git a/concepts/assets/fonts/manrope-latin.woff2 b/concepts/assets/fonts/manrope-latin.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..71eb731d558046199aa7f985adbf812890a093a1 GIT binary patch literal 24836 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 literal 0 HcmV?d00001 diff --git a/concepts/assets/fonts/spacegrotesk-latin.woff2 b/concepts/assets/fonts/spacegrotesk-latin.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..0f3474ee8af2f8253225f3b41bf79be986a1ae13 GIT binary patch literal 22288 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 S8#S~?xaXqu;tAo2S~9su2vSq{fi^hG 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 literal 0 HcmV?d00001 diff --git a/concepts/assets/profile-fallback.svg b/concepts/assets/profile-fallback.svg new file mode 100644 index 0000000..f4d83bc --- /dev/null +++ b/concepts/assets/profile-fallback.svg @@ -0,0 +1 @@ +WL diff --git a/concepts/assets/profile-telegram.jpg b/concepts/assets/profile-telegram.jpg new file mode 100644 index 0000000000000000000000000000000000000000..d0df4dd6c59971fb8fa62cfe0b45ada14cf32136 GIT binary patch literal 32157 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 literal 0 HcmV?d00001 diff --git a/concepts/concept-a/index.html b/concepts/concept-a/index.html new file mode 100644 index 0000000..fd9337b --- /dev/null +++ b/concepts/concept-a/index.html @@ -0,0 +1,62 @@ + + + + + + WizardLoop | Arcane Orbit - Local Concept + + + + + + + + + +
+
+ +
INDEPENDENT CREATIVE / WIZARDLOOP
+

I make things
come alive.

+

A little design. A little code. A different kind of magic.
Exploring the space between imagination and interaction.

+
+ +
WizardLoop's purple wizard character in a magical studio
+ 01 / DESIGN & MOTION02 / CODE & CURIOSITY + A SMALL WORLD. ALWAYS IN MOTION. +
+
Enter the orbit Telegram public profile still
+
+
DesignMotionAnimationSoftwareOpen source
+
+
01 / THE HUMAN BEHIND THE MAGIC
+

Curiosity is
my source code.

I'm WizardLoop, an independent designer, illustrator, animator and developer.

I bring playful visuals and useful software into the same orbit. From expressive characters and unexpected loops to Telegram bots and open-source tools, I like making ideas feel alive.

Meet me on Telegram Explore my GitHub
+
+
+
02 / SELECTED OPEN SOURCESMALL TOOLS. REAL POSSIBILITIES.
+

From thought
to thing.

All repositories
+

Original concept illustrations, not product screenshots.

+
+
+
03 / SIGNALS FROM THE STUDIOTHE TELEGRAM CHANNEL
+

A thought.
A loop.
A little discovery.

Follow the channel
WizardLoop
@wizard_loop
LAYOUT STUDY

A window into the process.

This is a posts layout preview. Original channel posts and their media will appear here after the integration is approved.

Read the real posts on Telegram
+
+

04 / GOOD THINGS HAPPEN IN LOOPS

Let's make
something unexpected.

Have a thought, a question, or a little spark of an idea?

+
+ + diff --git a/concepts/concept-b/index.html b/concepts/concept-b/index.html new file mode 100644 index 0000000..171bcb6 --- /dev/null +++ b/concepts/concept-b/index.html @@ -0,0 +1,92 @@ + + + + + + + + + The Alchemist's Journal | WizardLoop concept B + + + + + + + + + +
+
+
THE ALCHEMIST'S JOURNALA study in making things feel alive
+
+
+
INDEPENDENT CREATIVE & DEVELOPER
+

I make things
come alive.

+

I'm WizardLoop. I bring together design, motion and code to turn curious ideas into things you can see, feel and use.

Turn the page
+
+
+ Fig. 01 / an idea taking shape +
+ +
WizardLoop's public Telegram profile still
+
+
a little curiosity
goes a long way.
+

Telegram public profile still · local snapshot

+
+
+
DesignMotionAnimationSoftwareOpen source
+
AN OPEN NOTEBOOK OF IDEAS & EXPERIMENTS
Concept B
+
+ +
+
01 / THE MAKERAlways in
the making.
+

Somewhere between
art & a working prototype.

I’m an independent creator, designer and developer. I work with animation, visual design, Telegram bots and software tools, following ideas wherever they lead.

I like the space where a carefully drawn detail meets a clever piece of code. Open source is part of how I build, share and keep learning.

A look inside my GitHub
+
+ +
+
02 / SELECTED BUILDS

A few things
from the workbench.

+

From idea
to something real.

PUBLIC REPOSITORIES
BY WIZARDLOOP
+ I.

MagicLottie

Motion, animation & code

+ II.

Shabbat

An open-source build

+ III.

CreationDate

A software experiment

+
Original concept illustrations, not project screenshots.Explore the workbench
+
+
+ +
+
03 / NOTES IN THE MARGIN

Thoughts, process
& the in-between.

+

Notes from the loop.

LAYOUT PREVIEW

A little closer
to the process.

The journal will bring posts from my Telegram channel into this space, with a direct path to the original.

This is a design sample. Live posts and Telegram media are not loaded in this local concept.

Read the real channel
+
+ +
04 / KEEP THE CONVERSATION GOING

Good things start
with a hello.

Have a curious idea, a question, or something you've been making? I'd love to hear about it.

Say hello on Telegram
+
+ + + + diff --git a/concepts/concept-c/index.html b/concepts/concept-c/index.html new file mode 100644 index 0000000..4978f0c --- /dev/null +++ b/concepts/concept-c/index.html @@ -0,0 +1,121 @@ + + + + + + + + Spectrum Circuit | WizardLoop Design Preview + + + + + + + + + +
+
+
Independent creative / WizardLoopC / Spectrum Circuit
+
+
+

MAKE
IT
MOVE.

+

I make things come alive.
Design, motion and code. A little curiosity.
A little magic. Always in the loop.

+ Explore my work +
+
+
EXPERIMENT / 001LOOP FORM
+
+ + + + An acid green sculptural loopAn original ribbed geometric torus. Use the phase control below to turn the composition. + + + + + + + + + FORM FOLLOWS CURIOSITY + + +
+
+
0°
+ +
+
+
+
DESIGNMOTIONSOFTWAREOPEN SOURCE
+
+ +
+ +
+
+
WizardLoop Telegram public profile still
+

Telegram public profile still
Local snapshot, not a live sync.

+
+

A creative mind.
A builder's instinct.

I'm WizardLoop, an independent creator working where visual design, animation and software meet.

I turn ideas into things you can see, feel and use. That might be a moving illustration, a Telegram bot, a small tool or a piece of open-source software.

+

Code, Design
& a Little Magic.

+
+
+ +
+ +

From idea
to interaction.

A few open-source builds.
Explore the code behind the work.

+ +

Original concept illustrations, not project screenshots.

All repositories
+
+ +
+ +

Ideas don't
stand still.

Notes, experiments and updates live on my Telegram channel.

Open the channel
+
LAYOUT STUDY

A space for the next experiment.

Sample post layout. No channel content is reproduced here. Original posts will use verified Telegram embeds after approval.

+
+
+ +
+ +

GOOD THINGS
START WITH
A HELLO.

LET'S TALK
+ +
+
+ + + + + diff --git a/concepts/index.html b/concepts/index.html new file mode 100644 index 0000000..eb18052 --- /dev/null +++ b/concepts/index.html @@ -0,0 +1,4 @@ + +WizardLoop | Three Directions
LOCAL DESIGN REVIEW / 01

One creator.
Three possible worlds.

Three original directions for WizardLoop. Explore the layouts, switch the lights, and set the ideas in motion. This is the beginning of a conversation.

Preview scope. These are local design studies. The profile still is from the public Telegram profile. Project illustrations are original concept art. Post areas are labeled layout examples. Live synchronization, embedded posts, feedback delivery and production pages await approval.

diff --git a/concepts/robots.txt b/concepts/robots.txt new file mode 100644 index 0000000..1f53798 --- /dev/null +++ b/concepts/robots.txt @@ -0,0 +1,2 @@ +User-agent: * +Disallow: / diff --git a/docs/DEPLOYMENT.md b/docs/DEPLOYMENT.md new file mode 100644 index 0000000..4b0d95f --- /dev/null +++ b/docs/DEPLOYMENT.md @@ -0,0 +1,88 @@ +# Deployment, release and rollback + +Scope is `WizardLoop/wizardloop.github.io` and the existing domain `wizardloop.com`. This is a procedure for a later approved operation. It does not authorize a merge, deployment, DNS change, credential update, external message, tag or release. + +The package is `1.0.0-rc.1`, unreleased. Keep development on a dedicated branch. The original baseline is commit `658ed667c173f861cd16e6048126de12ec3648b9`; retain it and the original root `CNAME`, `index.html` and `assets/css/style.css` so the earlier site remains recoverable. Preserve the PaperMod attribution. + +## Build and review + +Astro uses `site: 'https://wizardloop.com'`, static output, directory-format pages and trailing slashes. Build from the repository root with Node 22.23.4 or newer; workflows use Node 24. + +```sh +npm ci +npm run check +npx playwright install --with-deps chromium firefox webkit +npm run test:browsers +``` + +`npm run check` includes brand generation, static generation and validation of `dist/`. Stop any development server at port 4322 before browser tests so the runner does not reuse it instead of the built preview. Python service tests and authorized live-provider acceptance are separate checks in their guides. + +The deployable artifact is **only `dist/`**. It contains static HTML, media, metadata, icons, sitemap output, the custom 404 and CNAME. It must not contain services, credentials, sessions, local artifacts, source maps or experimental concepts. `public/CNAME` and `dist/CNAME` must contain exactly `wizardloop.com`; preserve the root CNAME too. Do not add `/wizardloop.github.io` as an Astro base path. + +Set `PUBLIC_PREVIEW=true` before building a review version to mark pages non-indexable. Production must have preview mode disabled. This variable does not create a protected hosting environment; local review remains the default. + +Before publication: + +1. Review the final diff, screenshots, measured accessibility/performance results and limitations. Separate mocked/fixture results from real provider checks. +2. Verify content and rights, direct page loads, links, theme/motion behavior, profile fallback and JavaScript-free metadata. Inspect social artwork directly and check the artifact for sensitive material. +3. Read current GitHub Pages source, domain and HTTPS settings through an authorized account. The initial audit confirmed Pages exists, but its detailed settings were unavailable through the read-only interface. +4. Determine whether Pages publishes directly from a branch. A merge into that branch can trigger production even when the new workflow is manual. Resolve the source and timing with the owner before approving or executing a merge. +5. Identify the exact commit/artifact, configuration and rollback target. Obtain explicit approval for merge and deployment. A selected design, passing tests or typed workflow token is not that approval. + +Any needed Pages-source, domain, HTTPS, environment-protection or DNS change is a separate concrete infrastructure change for approval. This rebuild does not require a DNS change. + +## Manual Pages workflow + +`.github/workflows/ci.yml` runs quality checks on pull requests or manual dispatch with read-only repository permissions. It cannot publish Pages. Its actual run result is evidence; the presence of the file does not mean CI passed. + +`.github/workflows/deploy.yml`, named **Approved Pages deployment**, runs only by manual `workflow_dispatch`. Its build requires `main` and confirmation input `DEPLOY_WIZARDLOOP`. It installs locked dependencies, runs `npm run check` and uploads `dist/`. The deployment job alone receives `pages: write` and `id-token: write`, targeting the `github-pages` environment. New production jobs do not cancel an already-running deployment. + +After owner approval, merge the reviewed commit through the approved process and manually dispatch this workflow from `main` with that confirmation input. The token is a procedural guard, not proof of authorization. Inspect environment protection/reviewers separately; their presence is not assumed. Configuring them is an infrastructure change needing approval. + +Only these public frontend values belong in the repository/environment variables used by this workflow: + +| Variable | Meaning | +| --- | --- | +| `PUBLIC_FEEDBACK_API_URL` | Approved HTTPS Worker endpoint, empty while unavailable | +| `PUBLIC_TURNSTILE_SITE_KEY` | Public Managed Turnstile sitekey, empty while unavailable | + +Do not supply Telegram sessions, bot tokens, API hashes, private Chat IDs or the Turnstile secret to the Pages build. No profile/posts credentials or ingestion schedule are activated by deployment. + +## Separate integrations + +These operations need their own configuration, verification and approval: + +- [Profile sync](PROFILE-SYNC.md): verify the bot-visible primary and public biography, produce a private staged snapshot, regenerate brand assets and validate a complete static build. A fixture is not a successful live sync. +- [Posts sync](POSTS-SYNC.md): use the supported update-ingestion procedure and private state in that guide. Verify access and edit behavior before scheduling. A bot cannot be assumed to read arbitrary historical channel messages. +- [Feedback](FEEDBACK-SETUP.md): review the Worker, verify its numeric private Chat ID, deploy only with approval and perform a separately authorized delivery test. Leave the public endpoint unavailable until verified. + +No repository command automatically provisions a Worker or recurring sync. Never put secrets into CLI arguments, public URLs, logs or tracked configuration. Some secret-management commands publish service versions; follow the relevant guide first. + +## Verify production + +Record the deployed commit, workflow run, time, artifact identity and observed results. Verify the real site after the approved deployment: + +1. Confirm valid HTTPS, the existing domain, no unexpected redirects and the actual Pages/certificate settings. +2. Request every page directly, including nested routes, icons, social image, sitemap and 404 behavior. Check navigation and narrow mobile layout. +3. Inspect HTML without JavaScript for titles, descriptions, canonical URLs, social metadata and structured data. Confirm production does not have preview `noindex`. +4. Exercise theme/motion preferences and profile media/fallbacks. Check third-party media only after consent. A loaded iframe does not establish full Telegram fidelity. +5. If feedback was activated separately, confirm the approved non-sensitive test reaches the intended chat. Test failures without storing submission text in reports. +6. Inspect actual response headers. Document headers unavailable on Pages instead of claiming source files enforce them. + +If a material failure occurs, stop further rollout and use the approved rollback path. Do not release while verification is incomplete. + +## Rollback + +Before the first production change, retain the previous deployed artifact or a verified copy, the baseline commit, current Pages settings and domain value. Protect the recovery material from routine artifact expiry. Git history does not record every hosting setting. + +For a later Astro deployment, restore the last known-good reviewed commit through a revert/recovery pull request, retain `wizardloop.com`, rebuild with its lockfile and rerun relevant checks. Obtain rollback authorization, then publish the reviewed artifact through the manual workflow. Do not force-push `main`, overwrite a tag or delete history. + +The pre-rebuild baseline has no npm build. Running the new workflow against that old commit is not a valid recovery procedure. Package its original `index.html`, `assets/css/style.css` and `CNAME` from `658ed667c173f861cd16e6048126de12ec3648b9`, or use the preserved legacy artifact, and select an explicitly reviewed Pages publication method appropriate to the active source setting. Preserve relative paths, notices and the existing domain. Do not change DNS to perform recovery. + +Worker versions and private sync processes are independent. Stopping a schedule or restoring a Worker needs the relevant operational authorization. Reverting static files does not undo a message already sent or invalidate a leaked credential. + +## Tags and releases + +After approved deployment and successful live verification, obtain approval for the final semantic version, tag and GitHub Release. Recheck remote tags/releases immediately before selecting the version; never overwrite a tag. The initial audit found none, but that is historical evidence. + +Update the package and lockfile consistently, complete `CHANGELOG.md`, and tag the exact reviewed/deployed commit. Release notes should cover visible changes, validation, remaining integration limits, licensing and rollback. A release-candidate version does not mean `1.0.0` has been released. diff --git a/docs/DESIGN-ASSET-INVENTORY.md b/docs/DESIGN-ASSET-INVENTORY.md new file mode 100644 index 0000000..5513307 --- /dev/null +++ b/docs/DESIGN-ASSET-INVENTORY.md @@ -0,0 +1,120 @@ +# WizardLoop supplied design assets + +Inspected on 2026-10-27. Source directory: `C:/Users/ADMIN/Desktop/design`. This directory was read only. No source files were edited, renamed or deleted. Archive members were listed without execution. The initial review copied selected TGS JSON and contact sheets into workspace `artifacts/`. After the owner selected Concept C and authorized use of this collection, 30 wizard scenes and six flag samples were imported for the website as documented below. + +## Inventory summary + +The source contains 423 files totaling 86,253,828 bytes: 407 loose TGS animations and 16 ZIP archives. The loose animation folders are `flags` (200 TGS), `TGS coins` (200 TGS), and `לאף` (7 TGS). The directory `logo - profile video` is empty. There are no loose PNG, SVG, WebP, JPG, MP4 or WebM files. Some archives contain raster contact sheets and animated WebP previews. + +This is a user-supplied design collection. WizardLoop names and embedded credits support its use as supplied portfolio material, but filenames alone do not prove copyright ownership of every depicted symbol, third-party logo or font. There are no client names or client relationships verified by this inventory. + +The full file and ZIP-member index, including exact names and byte sizes, is in [inventory.json](../artifacts/design-inventory/inventory.json). Parsed selected animation metadata is in [candidates.json](../artifacts/design-inventory/candidates.json). These are review artifacts, not intended public website content. + +| Archive relative to source directory | Bytes | Contents observed | +| --- | ---: | --- | +| `WizardLoop_Infinity_Atelier_30_TGS.zip` | 697,996 | 30 named wizard/magic scenes. Best initial source for original hero and portfolio artwork. | +| `WizardLoop_4TGS.zip` | 200,804 | 4 tiles of a Wizard Loop Magic Ribbon composition. | +| `WizardLoop-MadeBy-5-TGS.zip` | 58,464 | 5 tiles, embedded name describes a KINETIC PRESS credit composition. | +| `WizardLoop_Order_from_me_4_TGS.zip` | 77,727 | 4 order-message animation tiles. | +| `WizardLoop_Order_your_pack_4_TGS.zip` | 222,729 | 4 order-pack animation tiles. | +| `WizardLoop_Order_your_pack_5_TGS.zip` | 282,079 | 5 order-pack animation tiles. | +| `WizardLoop_App_Coins_200.zip` | 14,873,171 | 200 TGS, 4 JPG contact sheets, 10 WebP samples, manifests and notes. | +| `WizardLoop_Country_Flags_001_050.zip` | 17,775,525 | 50 TGS, 50 WebP previews, PNG contact sheet, manifests and licenses. | +| `WizardLoop_Country_Flags_051_200.zip` | 13,327,139 | 150 TGS, 150 WebP previews, 4 PNG sheets, manifests and licenses. | +| `Israelis_20_Stickers_Updated_Compact.zip` | 6,886,877 | 20 TGS, 42 PNG, 24 JSON, 6 TTF, 12 Python files and text/Markdown notes. Listed only; embedded code was not executed. | +| `shabbat-12-tgs.zip` | 24,437 | 12 TGS. | +| `tgs signs heb.zip` | 671,744 | 30 TGS. | +| `flags/flags.zip` | See full index | 200 TGS. Likely an alternate packaging of loose files; no identity assumption made. | +| `TGS coins/TGS coins.zip` | 8,916,163 | 200 TGS. | +| `TGS coins/101-120.zip` | 921,378 | 20 TGS. | +| `לאף/lefaroll.zip` | 239,092 | 7 TGS corresponding to separately named LEFAROLL work. Do not present as WizardLoop branding. | + +## Infinity Atelier candidates + +All 30 parsed scenes declare 512 x 512, 60 fps, 144 frames and a 2.4-second duration. They identify Lottie version 5.7.4. No raster/external file assets were listed in the selected metadata. Compressed TGS bytes and uncompressed JSON bytes differ materially; budget browser loading against the actual served representation. + +The following initial choices are based on names, parsed layer descriptions and composition previews. The website should use selected scenes, with one main animation at a time, rather than load the whole collection into the first viewport. + +| Scene in `WizardLoop_Infinity_Atelier_30_TGS.zip` | TGS bytes | JSON bytes | Top-level layers | Potential role | +| --- | ---: | ---: | ---: | --- | +| `WL_001_Signature-Spirit.tgs` | 15,468 | 106,842 | 23 | Small character identity or compact hero. | +| `WL_002_Arcane-Aura.tgs` | 17,957 | 128,295 | 27 | Alternative character spotlight. | +| `WL_006_Portal-Alchemy.tgs` | 40,359 | 398,214 | 72 | Main magic scene; rich energy/portal treatment. | +| `WL_007_Paints-the-World.tgs` | 45,551 | 529,572 | 79 | Design/illustration portfolio feature. | +| `WL_008_Psychedelic-Vortex.tgs` | 43,621 | 411,015 | 80 | Expressive motion showcase, not a background on every page. | +| `WL_009_Reality-Zipper.tgs` | 62,399 | 597,685 | 56 | High-impact art feature with the largest selected JSON payload. | +| `WL_010_Living-Spellbook.tgs` | 16,831 | 125,216 | 30 | About/story or journal entry artwork. | +| `WL_017_Arcane-Coder.tgs` | 17,414 | 135,061 | 32 | Software and creative-code section. | +| `WL_024_Origami-Enchantment.tgs` | 25,100 | 210,407 | 23 | Playful secondary illustration. | +| `WL_025_Aurora-Ribbon-Wings.tgs` | 14,212 | 115,251 | 27 | Lighter decorative character variant. | +| `WL_030_Infinity-Constellation.tgs` | 18,014 | 180,084 | 45 | Brand-loop or closing section illustration. | + +Other available scene names: Spaceship-Flight, Overwhelmed-Hat, Psychedelic-Heart, Bubble-Brew, Crystal-Forge, Moon-Hammock, Thunder-Catcher, Hourglass-Twist, Midnight-DJ, Magnetic-Stars, Rainbow-Rain, Cosmic-Fishing, Crystal-Thaw, Molten-Ember, Neon-Wave-Surfer, Carnival-Hat-Burst, Clockwork-Spring, Silver-Lunar-Eclipse and Chromatic-Liquid-Wave. Exact original names and sizes are retained in the full index. + +No profile video was found in this source directory. A character scene can be presented as portfolio illustration, but should not be described as the current Telegram profile video. The previously verified public profile still remains a separate identity source. + +## Branding tiles + +`WizardLoop_4Tiles_01.tgs` through `WizardLoop_4Tiles_04.tgs` are 512 x 512, 60 fps and 3 seconds each. Their embedded names identify `Wizard Loop Magic Ribbon` and tile numbers. Their respective compressed sizes are 53,606, 47,702, 49,754 and 51,624 bytes. Each contains a precomposition, so a top-level layer count of one does not mean the artwork is cheap to render. + +The five `MadeBy` animations are also 512 x 512, 60 fps and 3 seconds. Their embedded names identify a five-part `KINETIC PRESS | made by @WizardLoop` composition. They should be viewed together before deciding whether to use a complete strip or a selected standalone fragment. Do not treat these fragments as five unrelated logos. + +The order packs have a clear promotional function from their names. They are better suited to a restrained commissioning section or portfolio example than to the permanent site identity. No assumption of an active storefront, pricing or purchase process follows from these filenames. + +## Flags and app coins + +Two original contact sheets were extracted and visually inspected: + +- [Country flags 001-050](../artifacts/design-preview/contact_sheet.png): consistent silver masts, cloth folds and full flag silhouettes. Suitable for a dedicated motion-system project card or small curated sample strip. +- [App coins 001-050](../artifacts/design-preview/App_Coins_001_050.jpg): circular dimensional coins with app identities and visible credit. Suitable as a motion-design collection, with third-party brand rights disclosed. They are not evidence that these organizations are customers. + +The flags' bundled notes say the full TGS versions are 512 x 512 at 60 fps, while WebP previews are reduced review copies. Keep the full vector animation for the chosen samples instead of upscaling tiny WebP previews. Parsed loose samples include Israel and United States; their individual TGS sizes are 25,281 and 38,473 bytes. Parsed app-coin samples include `036_GitHub.tgs` (46,142 bytes) and `034_LottieFiles.tgs` (42,642 bytes). + +The supplied flag packs include `manifest/LICENSES.txt`, `NOTES.txt` and machine-readable source manifests. Entries cite the country-flags collection and Wikimedia sources with per-artwork attribution. The country-flags README at the pinned source revision and the Brazil source page were checked directly for the selected six designs; the rest of the collection was not independently re-audited. The second pack explicitly says the Belize source copyright license is unasserted; that item is excluded from the public selection. + +The app-coins README explicitly reserves third-party logos, mascots and names to their respective owners and disclaims affiliation. Its claims about previous rendering tests are historical package notes, not tests performed for this website. Do not relabel the artwork MIT or claim endorsement. The same separation applies to all source-code licensing versus branding/media licensing. + +## Preview and implementation boundaries + +The local poster review renderer used the locally installed `lottie-web` SVG player and Playwright Chromium. It did not contact external media services. Screenshots use a fixed frame for comparison; they do not certify all animation frames or Telegram-app compatibility. The original flag/app-coins sheets are package-provided previews, distinct from newly rendered browser screenshots. These SVG poster-rendering checks are distinct from the final site's Canvas playback. + +The final site uses a static preview for the initial HTML and reduced-motion view, with fixed dimensions to prevent layout shift. The Home hero has a separate native-video path: two 768 x 768 H.264 MP4 derivatives of `portal-alchemy`, matched to light and dark themes, retain the original 2.4-second loop duration. Other scenes lazy-load one shared light Canvas player for the original TGS files when animation is allowed. At most three visible Canvas scenes play, with drawing capped at 24 frames per second. Frames are sampled by elapsed time at the source frame rate so the original loop duration is preserved. The pause control and document/viewport visibility stop both playback paths. Check full loop playback, clipping, contrast on both themes and keyboard access in the actual final layout. + +The source archives contain no added grant of MIT rights for the Infinity/branding animation assets. Treat them as user-supplied WizardLoop portfolio media, with source provenance and reserved asset rights. If a derived poster or recolored copy is produced, retain its original source-file association and document the transformation. + +## Authorized website selection + +The 30 original Infinity Atelier TGS files are imported unchanged, totaling 695,731 bytes. The six selected flags total 175,855 bytes. They are served under `public/media/animations/.tgs`. The curated manifests `src/content/artworks.json` and `src/content/flags.json` each retain title, alt text, original ZIP/member path, byte size and SHA-256 hash. The manifests do not publish an absolute local source-directory path. The original archive/member paths are resolved relative to the source directory at the start of this report. + +The hero candidate is `portal-alchemy`, with `signature-spirit` for a compact character identity, `paints-the-world` for visual work, `living-spellbook` for the personal introduction and `arcane-coder` for software. The 30 scenes are supplied artwork, not invented clients or claims about commissioned projects. The public Telegram profile still is separate from these portfolio illustrations. + +| Public slug | Original member inside `WizardLoop_Country_Flags_001_050.zip` | TGS bytes | +| --- | --- | ---: | +| `flag-il` | `WizardLoop_Country_Flags_001_050/tgs/001_IL_Israel.tgs` | 25,281 | +| `flag-us` | `WizardLoop_Country_Flags_001_050/tgs/002_US_United_States.tgs` | 38,473 | +| `flag-gb` | `WizardLoop_Country_Flags_001_050/tgs/005_GB_United_Kingdom.tgs` | 28,821 | +| `flag-fr` | `WizardLoop_Country_Flags_001_050/tgs/007_FR_France.tgs` | 20,474 | +| `flag-jp` | `WizardLoop_Country_Flags_001_050/tgs/017_JP_Japan.tgs` | 19,015 | +| `flag-br` | `WizardLoop_Country_Flags_001_050/tgs/012_BR_Brazil.tgs` | 43,791 | + +The original full flag-source notice is preserved in `public/media/licenses/FLAGS-NOTICES.txt`, preceded by a scope explanation. It includes unused entries from the original 50-item collection; only the six listed above are imported. The pinned [country-flags source README](https://github.com/hampusborgos/country-flags/blob/c09927e63705529bbf59ca6684cd9b23225dddad/README.md) states that its flag designs are public domain and notes possible other usage restrictions. The [Brazil vector source](https://commons.wikimedia.org/wiki/File:Flag_of_Brazil_as_Paths.svg) names FireDragonValo and identifies the work as public domain in Brazil, with a separate national-symbol notice. These source statements do not grant MIT rights to WizardLoop's animation treatment. App logos and Belize artwork were not imported. + +All 36 imported animations have been recursively inspected: no expression strings and no external image/file references were found. The installed player is `lottie-web` 5.13.0. The gallery's browser module imports `lottie-web/build/player/lottie_light_canvas`; its light Canvas build omits expression support, which these selected assets do not require. The earlier poster comparison used the distinct light SVG build `lottie_light.min.js`. The hero uses native MP4 derivatives instead of loading a Canvas player for that illustration. No animated WebP file is used for playback, and the original hero TGS remains in the retained source collection. + +Each selected scene has a transparent 512 x 512 WebP poster at `public/media/posters/.webp`. Posters are native vector renders at frame `floor(totalFrames * 0.35)`, captured by Playwright Chromium using the original full SVG player, then encoded losslessly. The 30 artwork posters total 2,848,628 bytes; the six flag posters total 268,650 bytes. These are individual lazy-loadable files, not a single first-page payload. The portal hero poster retains the complete supplied vector composition and no contact-sheet background. + +The site serves static delivery copies from `public/media/previews/.webp`. `scripts/generate-previews.mjs` encodes them at WebP quality 94 with alpha quality 100, retaining the original file when re-encoding would be larger. It checks each copy's dimensions and alpha-channel presence. The measured 36-copy set is 1,309,102 bytes, down from 3,117,278 bytes of lossless masters. This optimization changes color pixels, so the delivery copies are not claimed to be pixel-identical to the masters. The masters and original TGS files remain unchanged. `npm run build` regenerates these copies; hidden gallery items do not need to load before the visitor reveals them. + +All 36 posters were also rendered with the light SVG player at the same frame. The full and light results were pixel-identical for every asset. All outputs have alpha values ranging from transparent to opaque, are visibly nonempty, and round-trip through lossless WebP without changed pixels. The renderer recorded zero browser page errors. This comparison verifies the selected poster frame, not every point in every loop or the performance of the eventual site layout. Animated playback, visibility pausing and reduced-motion behavior must still be checked in the final site. + +Review evidence is saved in `artifacts/design-preview/selected-asset-audit.json`, `poster-audit.json` and `posters/render-log.json`. The source TGS remains the authoritative original; derived poster hashes and dimensions are recorded separately. The asset-import and rendering utilities are local review artifacts and do not ship to visitors. + +## Native hero delivery + +`public/media/hero/manifest.json` records the original TGS member and SHA-256, renderer, encoder, frame timing, dimensions, theme colors and each MP4 checksum. The 768 x 768 exports contain 72 frames at 30 frames per second for an exact 2.4-second loop. They use H.264 Baseline (`avc1.42001F`) with a target bitrate of 2,000,000 bits per second, one video track and no audio. Their MP4 metadata precedes the media data for progressive loading. + +H.264 has no alpha channel here. Each export includes the matching hero background: `#e4dcf7` for light and `#34244e` for dark. The light file is 645,033 bytes and the dark file is 635,146 bytes. Only the active theme's source is selected when playback is allowed. The original transparent vector animation and lossless poster remain available as source assets. + +Regenerate these files manually with `node scripts/export-hero-video.mjs` after an intentional source or theme-color change. This export uses Playwright Chromium with native AVC WebCodecs support; `CHROME_PATH` can select an installed Chromium. It does not run in the normal site build. The exporter renders locally and records provenance without external media requests. + +The exported files passed native Chromium metadata, duration, seek and playback checks; the playback observation reported zero dropped frames. Container checks confirmed `ftyp`, `moov`, then `mdat` ordering. Local evidence is in `artifacts/hero-video-export/report.json` and `container-check.json`. These checks validate the files and one browser's native playback, not every device or the final page's performance. diff --git a/docs/FEEDBACK-SETUP.md b/docs/FEEDBACK-SETUP.md new file mode 100644 index 0000000..4c7a769 --- /dev/null +++ b/docs/FEEDBACK-SETUP.md @@ -0,0 +1,133 @@ +# Feedback service setup and review + +The static website remains on GitHub Pages. `services/feedback/worker.mjs` is a separate, dependency-free Cloudflare Worker with JSDoc types. It receives feedback, verifies Cloudflare Turnstile on the server, and sends a private message through the Telegram Bot API. + +**Status:** implemented and tested against mocked providers. No Worker has been deployed, no live credentials have been used, and no Telegram delivery has been tested. Repository automation must not deploy this service. Merging, deployment and a real delivery test require the owner's explicit approval. + +## Local tests and mock + +From the repository root, with Node 22 or later: + +```sh +npm --prefix services/feedback test +npm --prefix services/feedback run dev:mock +``` + +The mock listens only on `http://127.0.0.1:8787/feedback`. It accepts frontend origins `http://127.0.0.1:4321`, `http://localhost:4321` and the same hostnames on port 4322. The local server explicitly sets `MODE=mock`; production requires `MODE=production`. + +A mock request must use `turnstileToken: "local-mock-token"`. A successful mock result contains `mock: true` and the message `Local test complete. No message was sent to Telegram.` The frontend must display this distinction. This is not a working CAPTCHA or proof of real delivery. The mock makes no outbound requests, retains no submitted messages, and permits five requests per origin per minute in process memory. + +Mock mode requires both a loopback request target and a loopback Origin. Uploading the same mock configuration onto a public Worker cannot enable this local bypass. Production refuses missing credentials, unverified Chat IDs, test Turnstile keys, missing rate-limit bindings and non-HTTPS requests. The package has no dependencies; the lockfile records that fact. + +## Frontend contract + +Set only the deployed endpoint URL and public Turnstile sitekey in public frontend configuration. Do not put a secret key, bot token, numeric Chat ID, or rate-limit salt in any variable prefixed `PUBLIC_` or in the Pages build. + +`POST /feedback` with `Content-Type: application/json`: + +```json +{ + "name": "A visitor", + "message": "A message for WizardLoop.", + "email": "", + "website": "", + "turnstileToken": "a-fresh-widget-token", + "sourcePath": "/feedback/" +} +``` + +| Field | Rule | +| --- | --- | +| `name` | Required, trimmed, 1 to 80 UTF-16 code units, no line breaks | +| `message` | Required, trimmed, 1 to 2500 UTF-16 code units; line breaks permitted | +| `email` | Optional, maximum 254 code units; reasonable email syntax, no whitespace or header injection | +| `website` | Optional honeypot, must be exactly empty when supplied | +| `turnstileToken` | Required, maximum 2048 code units; each real submission requires fresh verification | +| `sourcePath` | Optional, defaults to `/feedback/`; restricted local path, no query, fragment or external URL | + +Extra fields are rejected. The stream is limited to 16 KiB even without a Content-Length header; body reading has a five-second deadline. Unsupported content types, invalid UTF-8, malformed JSON and control characters are rejected. + +Use Turnstile Managed mode with `action: "feedback"`. Reset the widget after every submission attempt, including network failures, validation errors and delivery errors, because a token may have been consumed. Send requests with `credentials: "omit"`. Never put feedback fields in query strings, analytics events, error reports or client logs. Use the accessible status region for the returned generic message and keep the user's text in the form on failure. + +| HTTP status | Meaning | Frontend behavior | +| --- | --- | --- | +| 200, `ok: true`, `mock: false` | Telegram returned success | Show success and clear the form | +| 200, `ok: true`, `mock: true` | Local demonstration only | State explicitly that nothing was sent | +| 400 / 408 / 413 / 415 / 422 | Invalid request, timeout, size, content type or CAPTCHA | Keep entered values, display error, reset verification | +| 403 | Rejected origin or preflight | Display a general unavailable message | +| 429 | Local submission rate exceeded | Honor `Retry-After`, keep content, reset verification | +| 503 | Configuration, verification or delivery unavailable | Keep content, offer manual retry with new verification | +| 503 with `mayHaveBeenDelivered: true` | Telegram request outcome is uncertain | Say delivery could not be confirmed and ask the visitor to wait before retrying | + +The worker retries a transient Siteverify failure once using the same idempotency key. It rejects unsuccessful or replayed verification and requires the returned hostname to equal the request Origin's hostname and the returned action to equal `feedback`. This follows [Cloudflare's server-side validation contract](https://developers.cloudflare.com/turnstile/get-started/server-side-validation/). There is no client-only verification shortcut. + +## Create and verify the private Telegram destination + +Use a dedicated bot created through [Telegram's BotFather](https://t.me/BotFather). Keep its token in a password manager. Do not paste the token into a browser URL, command argument, screenshot, public issue or chat with this assistant. + +Run the local helper in a private interactive terminal: + +```sh +npm --prefix services/feedback run setup:chat +``` + +It prompts for the token without echoing it or accepting command-line arguments. It calls `getMe` and `getWebhookInfo`, refuses a bot with an active webhook, then prints a random one-time phrase. Open the verified bot from the `@wizardloop` account, press Start and send the phrase manually. After Enter, the helper reads at most 100 updates and requires exactly one private message containing that phrase from `@wizardloop`, with matching sender and chat IDs. It confirms the private chat using `getChat`. + +The numeric Chat ID is printed only in the local terminal so the owner can copy it into the secret manager. The token is never printed. No files are written, no update offsets are acknowledged, no webhook is changed and no message is sent by the helper. Clear the terminal if it is shared. Use a fresh dedicated bot to avoid existing webhook integrations and update backlogs. If the private username changes, review and update the expected username in this helper intentionally. + +Set `TELEGRAM_CHAT_ID_VERIFIED=true` only after this verification. The flag records an operator check, not a cryptographic proof. Starting a conversation permits a bot to reply, but blocked bots and revoked tokens can still fail later. Actual send capability requires one separately approved live test. The API takes a numeric private Chat ID, not the owner's personal username; see the official [Telegram sendMessage and getUpdates documentation](https://core.telegram.org/bots/api). + +## Configuration and secrets + +Review `services/feedback/wrangler.jsonc`. All routes, workers.dev serving and preview URLs are disabled initially. No account ID, DNS change or public endpoint is included. + +| Name | Storage | Value | +| --- | --- | --- | +| `MODE` | Worker variable | `production` | +| `ALLOWED_ORIGINS` | Worker variable | Exact comma-separated HTTPS origins, initially `https://wizardloop.com` | +| `TURNSTILE_HOSTNAMES` | Worker variable | Exact comma-separated verified hostnames, initially `wizardloop.com` | +| `TELEGRAM_CHAT_ID_VERIFIED` | Worker variable | `true` only after private-chat verification | +| `TURNSTILE_SECRET_KEY` | Worker secret | Secret from the Managed Turnstile widget | +| `TELEGRAM_BOT_TOKEN` | Worker secret | Dedicated bot's token | +| `TELEGRAM_CHAT_ID` | Worker secret | Verified positive numeric private Chat ID | +| `RATE_LIMIT_SALT` | Worker secret | Independently generated high-entropy random value of at least 32 bytes | +| `RATE_LIMITER` | Worker binding | Cloudflare Rate Limiting binding | + +Generate the salt with a trusted password manager. Never commit `.dev.vars`, `.env` files, tokens, session files or personal Chat IDs. The directory's `.gitignore` excludes them. `.env.example` contains empty placeholders only. Local real-credential testing is deliberately not enabled by the mock server. + +The Turnstile widget must allow the production hostname. Do not add localhost to a production widget. Public test keys belong only in a dedicated test setup; this production worker intentionally rejects the documented dummy secret patterns. See [Cloudflare testing guidance](https://developers.cloudflare.com/turnstile/troubleshooting/testing/). + +For the rate-limit binding, select an unused positive `namespace_id` within the owner's Cloudflare account; `1001` is a configuration example, not a provisioned account resource. The configured threshold is five attempts per minute. Missing or failed rate-limit infrastructure fails closed. The key is a daily HMAC of the Cloudflare-provided IP with a secret salt; neither the raw IP nor the key is sent to Telegram or application logs. No tracking cookie is created. + +Cloudflare's rate-limit binding is approximate and local to each Cloudflare location. It is not a strict worldwide quota. This anonymous form has no account identifier, so a transient IP-derived key is used with the known tradeoff that people sharing a connection can share a limit. A strict global limit would require additional state such as a Durable Object. See [Workers Rate Limiting](https://developers.cloudflare.com/workers/runtime-apis/bindings/rate-limit/). + +## Approved deployment procedure + +Do not execute this section until the owner separately approves production changes. The main site's CNAME and GitHub Pages deployment remain separate. + +1. Review the tests and configuration, choose the endpoint without changing `wizardloop.com` DNS, and obtain approval for the exact external Worker endpoint and cost settings. +2. Use a current, pinned Wrangler 4 release supporting `ratelimits` (at least 4.36.0). Install it as local development tooling in this service and retain its lockfile update for review. It is unnecessary for the dependency-free local mock and unit tests. +3. Configure the secrets through Cloudflare's secret manager. **`wrangler secret put` immediately deploys a Worker version.** On an existing Worker, `wrangler versions secret put NAME` creates a version without deploying it. Do not treat uploading secrets as a harmless read-only setup action. Cloudflare's [secret management documentation](https://developers.cloudflare.com/workers/configuration/secrets/) explains the difference. +4. Choose the approved route or explicitly enable the approved workers.dev endpoint. Keep `MODE=production`, exact origin and hostname lists, and the configured rate-limit binding. Do not enable wildcard CORS or allow localhost in the public production configuration. +5. Review an uploaded version and deploy only through the approved release process. The Worker code does not require a bundling step or Node compatibility flags. +6. Set the static website's public feedback endpoint and public sitekey. Update its CSP `connect-src` with that exact endpoint and permit only the required Turnstile hosts in the appropriate directives. +7. With explicit permission, submit one clearly labeled non-sensitive test from the real production website and confirm receipt in the intended private chat. Test invalid CAPTCHA, bad Origin and a rate-limit response as well. Record the date and observed results without visitor messages, bot tokens or Chat IDs. +8. Keep the form unavailable or clearly local-only until delivery is verified. Do not claim that Telegram sending works based only on these mocks. + +For rollback, restore the previous reviewed Worker version through Cloudflare and disable the public form endpoint in the static configuration if needed. Rotating a bot token or secret is an external change requiring the same explicit operational approval. No deployment workflow is supplied here. + +## Data flow and known limits + +The submitted name, message and optional email pass through Cloudflare to the owner's private Telegram conversation. The message contains the verified site origin plus a restricted source path and a UTC receipt time. HTML metacharacters are escaped, URL previews are disabled, and `protect_content` is requested. These settings do not guarantee that a recipient or provider cannot retain content. + +There is no database, queue, mail provider or application log containing submissions. The worker does not send IP addresses to Telegram and omits optional `remoteip` from Siteverify. Cloudflare still handles connection metadata and performs security processing, and Telegram stores the resulting message under its own service rules. The owner must choose an actual message-retention and deletion practice before publishing the privacy policy. This implementation does not enforce deletion from the Telegram conversation. + +No automatic retry of `sendMessage` occurs: a timeout may happen after delivery, and the API call has no application idempotency key here. The user can retry with a new CAPTCHA after a clear warning. A queue and durable delivery ledger would be needed for stronger delivery guarantees. Message limits keep the resulting formatted text below Telegram's 4096-character limit after parsing. + +CORS and Origin checks limit browser callers but are not authentication for arbitrary clients; Turnstile and rate limiting remain required. The honeypot is supplemental. All API responses carry `no-store`, no-referrer, `nosniff`, restrictive CSP and frame protection. These are this Worker's response headers, not a claim that GitHub Pages serves those same headers. Cloudflare observability is disabled in this initial configuration; platform-level security records may still exist. + +## Automated evidence + +The offline test suite covers successful escaped delivery, required fields, injection attempts, body byte limits, malformed JSON, exact Origin checks, preflight restrictions, missing configuration, disabled and failed rate-limit bindings, daily key rotation, invalid and replayed CAPTCHA, hostname/action mismatch, bounded verification retries, provider timeout, manual retry after Telegram failure, ambiguous delivery, security headers, local-only mock restrictions and mock rate limits. + +Tests use fake credentials and mocked network transports. They are not evidence of a deployed Cloudflare binding, real Turnstile operation, legal compliance or a successful Telegram message. Preserve the final test output with the repository's QA report. diff --git a/docs/INTEGRATION-FEASIBILITY.md b/docs/INTEGRATION-FEASIBILITY.md new file mode 100644 index 0000000..27ffbf0 --- /dev/null +++ b/docs/INTEGRATION-FEASIBILITY.md @@ -0,0 +1,90 @@ +# Integration feasibility - Phase 1 + +Reviewed on 2026-10-27 (Asia/Jerusalem). This is research for local design review. No remote settings, deployments, credentials, webhooks, sessions, releases or production content were changed. Integration proposals below are not implemented or end-to-end tested. + +## Findings that affect design approval + +- The complete Telegram channel page cannot be embedded on WizardLoop: the observed response for `https://t.me/s/wizard_loop` contains `X-Frame-Options: SAMEORIGIN`. Use a native Posts page containing individual official Post Widgets and permanent Telegram links. +- Telegram documents a bot-authenticated MTProto path for profile photo, video and bio retrieval. It must be tested with an authorized bot and the owner's privacy settings before promising automatic primary-profile video sync. A personal user session is not the default. +- Official post responses expose real animated media, but do not guarantee complete Telegram-client fidelity. The observed responses contain both supported-media markup and unsupported-media notices. Custom emoji animation and TGS/WebM/video playback require browser verification after the design is chosen. +- Telegram embeds execute third-party code and the observed embed HTML writes a `stel_dt` cookie. They must not be described as cookieless. Proposed default: show local post information and an explicit control to load Telegram content, with an always-available source link. +- The local profile asset is a verified public Telegram still preview. It is not proof of the account's current primary video, original-resolution image, or an operating sync service. + +## Public observations + +Read-only HTTP requests were made from the development environment. Initial sandbox requests failed because socket access was restricted; approved public reads subsequently succeeded. These are observations of those responses, not permanent service guarantees. + +| URL | Result | Relevant observation | +| --- | --- | --- | +| [Telegram profile](https://t.me/wizardloop) | 200 | Public bio and JPEG Open Graph image; no video element. CSP `frame-ancestors https://web.telegram.org`; legacy X-Frame-Options `ALLOW-FROM https://web.telegram.org`; no Access-Control-Allow-Origin. | +| [Channel preview](https://t.me/s/wizard_loop) | 200 | Real message IDs and custom emoji markup. X-Frame-Options `SAMEORIGIN`; no Access-Control-Allow-Origin. | +| [Post 51 embed](https://t.me/Wizard_Loop/51?embed=1) | 200 | Custom emoji tags with static emoji fallback and a notice to open Telegram. | +| [Post 52 embed](https://t.me/Wizard_Loop/52?embed=1) | 200 | `application/x-tgsticker` source and Telegram's sticker player script; unsupported-media fallback also present. | +| [Post 53 embed](https://t.me/Wizard_Loop/53?embed=1) | 200 | MP4 video markup, custom emoji tags, Telegram player scripts and unsupported-media fallback. | +| [Official widget script](https://telegram.org/js/telegram-widget.js?22) | 200 | Creates a per-message `https://t.me/{channel}/{id}?embed=1` iframe; script response has Access-Control-Allow-Origin `*`. | +| [Production home](https://wizardloop.com/) | 200 | Server `GitHub.com`, Cache-Control `max-age=600`. No CSP, HSTS, Referrer-Policy, Permissions-Policy or X-Frame-Options header in the observed response. | +| [Public Pages API](https://api.github.com/repos/WizardLoop/wizardloop.github.io/pages) | 404 without authentication | Publishing settings could not be inspected through this request. This does not prove Pages is disabled. | + +The individual embed responses had no X-Frame-Options or CSP header in this observation. Missing CORS headers do not prevent normal iframe embedding; they do prevent treating the response as freely readable cross-origin data. Do not fetch the channel HTML from client JavaScript or strip Telegram's framing restrictions. + +### Verified content for local concepts + +Public profile bio, observed in both page content and metadata: + +> Independent designer, illustrator & animator. Expressive characters. Playful visuals. Unexpected loops. + +The image was downloaded from the `og:image` URL published by the profile page into `concepts/assets/profile-telegram.jpg`. It is a 320 x 320 JPEG, 32,157 bytes. SHA-256: `2FB36A4AD9412D9212EA029B81A0F8054AF440E3F5208F06128074D4A580690A`. It was visually checked: a purple wizard character beside a glowing portal. Its permanent provenance is the public profile URL above; the CDN URL is an implementation detail, not a stable publishing contract. Treat this as owner branding/media, outside the new code's MIT license. + +Genuine sample links: [Amanimo post](https://t.me/Wizard_Loop/51), [associated sticker](https://t.me/Wizard_Loop/52), [Psychedelic Adaptive post](https://t.me/Wizard_Loop/53), and [Adaptive Star of David post](https://t.me/Wizard_Loop/54). Post 51 reports `2026-10-22T20:55:52+00:00`; post 52 reports `2026-10-22T20:56:09+00:00`. These are source examples, not invented project credits. No reusable scraping infrastructure was created. + +## Profile and bio sync proposal + +Telegram's supported MTProto methods [contacts.resolveUsername](https://core.telegram.org/method/contacts.resolveUsername), [users.getFullUser](https://core.telegram.org/method/users.getFullUser), [photos.getUserPhotos](https://core.telegram.org/method/photos.getUserPhotos) and [upload.getFile](https://core.telegram.org/method/upload.getFile) explicitly permit bots. [auth.importBotAuthorization](https://core.telegram.org/method/auth.importBotAuthorization) accepts bot authorization with an API ID/hash. This supports testing a dedicated bot before considering a more powerful personal user session. + +Proposed selection algorithm: + +1. Resolve `wizardloop`, read current user and full-user information. [UserProfilePhoto](https://core.telegram.org/constructor/userProfilePhoto) provides `photo_id`, `has_video` and the `personal` flag. [userFull](https://core.telegram.org/constructor/userFull) provides `profile_photo`, optional `about`, and separately identified personal/fallback photos. +2. Select the current `profile_photo`, cross-checking the current photo ID. If a complete Photo is needed, obtain the matching ID through `photos.getUserPhotos`. Never select the largest ID or newest upload date as a substitute for the active profile. Do not publish a contact-specific personal photo or privately accessible profile data as public branding. +3. Inspect [Photo.video_sizes](https://core.telegram.org/constructor/photo). Telegram documents MPEG4 variants. Prefer the highest appropriate full animation, not a small preview: the [file documentation](https://core.telegram.org/api/files#videosize-type-values) identifies `u` as the animated profile and `p` as its small preview. Download using the documented file location and refresh expiring file references as needed. This promises the best available Telegram rendition, not an unavailable pre-upload original. +4. Use [videoSize.video_start_ts](https://core.telegram.org/constructor/videoSize) for the intended static preview frame. Generate fixed-size local poster, favicon and social-image derivatives. Keep a stable public URL for crawler assets and an atomic, validated asset manifest for the site. +5. Read supported `userFull.about` when available. Otherwise use verified GitHub bio, then editable local copy. The public-page bio above is evidence for Phase 1, not a proposed long-term HTML scraper. + +Proposed operation: a separate small sync process with encrypted credentials, timeout, bounded retries and Telegram flood-wait handling. Check on a conservative schedule, for example every 6 hours, plus a manual trigger. Publish only changed, validated assets and safe metadata. Do not serialize API responses, access hashes, sessions or download credentials into the site. Preserve the last valid asset on transient failure; if none exists, fall back to GitHub, then a local mark. An explicit owner removal/privacy change should be handled separately from a network outage. + +Playback in the chosen design should use a muted, looping, `playsinline` video with dimensions, poster and a pause control. Reduced motion should default to the poster. No MTProto login or video download was attempted because credentials were not provided. Bot visibility, identity matching and video quality remain acceptance tests. + +The HTTP [Bot API](https://core.telegram.org/bots/api#getuserprofilephotos) returns profile photos as PhotoSize arrays; that method does not document video sizes. It is not evidence of profile-video support. + +## Posts: supported approach and limitations + +Telegram's [official Post Widget](https://core.telegram.org/widgets/post) supports individual public group/channel messages, including width and dark-theme configuration. It is the preferred renderer. A native `/posts` page can keep the WizardLoop navigation and own a cached list of verified message IDs, dates, short accessible descriptions and source links. Load a limited batch of widgets on request or near the viewport; reserve space to reduce layout shift and provide failure/retry states. + +For automatic discovery, a channel-member bot can receive new posts and edits. [Telegram's FAQ](https://core.telegram.org/bots/faq#what-messages-will-my-bot-get) documents channel message delivery. The [Update schema](https://core.telegram.org/bots/api#update) exposes `channel_post` and `edited_channel_post`; undelivered updates are retained for at most 24 hours. Persist updates promptly, deduplicate IDs and handle downtime. The reviewed schema has no general channel-post deletion event. Design reconciliation and owner removal controls explicitly. + +Historical backfill differs: [messages.getHistory](https://core.telegram.org/method/messages.getHistory) is users-only. Start with owner-approved known IDs or an export. If an authorized user session is eventually needed for complete backfill, keep it isolated and explain the scope first. [channels.getMessages](https://core.telegram.org/method/channels.getMessages) permits bots for known IDs, subject to access; test this for edit/deletion reconciliation rather than assuming bot history access. + +Do not cache expiring CDN media links as permanent URLs. Do not substitute static emoji or poster images and label them full animation. The cross-origin widget cannot be restyled or paused by arbitrary parent-page DOM access. Reduced-motion behavior and the ability to pause each animated type need real browser testing. If the widget lacks sufficient controls, keep it unloaded until requested and clearly offer the Telegram original. The site's motion toggle should not claim to pause third-party media unless verified. + +Before final implementation approval, run a Chromium/Firefox/WebKit matrix on real channel examples for: TGS, WebM, custom emoji, video, albums, long text, keyboard use, RTL, narrow viewports, blocked requests, consent refusal and reduced motion. HTTP source inspection is not playback verification. No claim of complete fidelity is made. + +## Feedback and hosting notes for Phase 2 + +Keep the site static. A separate Worker can validate method, exact allowed Origin, content type, body size, required name/message and optional email. Limit lengths, reject unexpected fields, use a honeypot and abuse controls, and send escaped plain text or safely escaped Telegram HTML. Do not treat CORS as authentication. Never send visitor IPs to Telegram or log form bodies. + +[Turnstile Siteverify](https://developers.cloudflare.com/turnstile/get-started/server-side-validation/) is mandatory on the server. Tokens are single-use and expire after five minutes. Validate success, hostname and action; reject missing, expired and replayed tokens. `remoteip` is optional. Keep the secret server-side and show accessible timeout/retry messages. The normal design can omit passing the IP to Siteverify, though Cloudflare still receives network/security signals. + +Cloudflare's [Worker rate-limit binding](https://developers.cloudflare.com/workers/runtime-apis/bindings/rate-limit/) is approximate and local to each Cloudflare location. It is not a strict global quota. Use a Durable Object if strict global protection for the Telegram destination is required, with documented retention for any abuse-control keys. + +Configure the numeric private chat ID through a private setup step after the owner starts the bot, then verify with an explicitly approved test. The username alone is not proof that a bot can message the private account. Until secrets and a real delivery test exist, use a clearly labeled local mock, not a false success message. + +The production response currently lacks the headers listed above. A static HTML CSP can cover some resource rules, but [frame-ancestors cannot be supplied in a meta element](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Content-Security-Policy/frame-ancestors). Do not claim server headers are configured by writing an unsupported `_headers` file into GitHub Pages. A proxy/CDN may supply them only after the owner separately approves infrastructure changes. + +## Privacy and accessibility review boundaries + +- Feedback will process name, message and optional email through the chosen backend and Telegram. Publish purpose, recipients, voluntary/required fields, retention, contact and deletion/correction procedure based on actual operation. Retention is an owner decision, not an invented policy. Israel's [Privacy Protection Authority guidance](https://www.gov.il/BlobFolder/legalinfo/duty_to_notify/he/notify13.pdf) and [Amendment 13 information](https://www.gov.il/he/pages/tikun13_qa) are relevant to the notice and rights assessment. +- [GitHub documents security logging of visitor IPs](https://docs.github.com/en/pages/getting-started-with-github-pages/what-is-github-pages#data-collection). [Cloudflare's Turnstile addendum](https://www.cloudflare.com/turnstile-privacy-policy/) describes security signals including IP, TLS fingerprint, user agent and origin. Therefore, do not publish an unconditional claim that the website collects no information. +- [GDPR territorial scope](https://commission.europa.eu/law/law-topic/data-protection/information-business-and-organisations/application-gdpr_en) depends on establishment, offering services and monitoring; public accessibility from Europe alone is not enough to conclude applicability. [ePrivacy Article 5(3)](https://eur-lex.europa.eu/legal-content/EN/TXT/?uri=CELEX%3A02002L0058-20091219) distinguishes consent from strictly necessary storage/access. Assess the actual embeds and tracking before choosing consent behavior. No analytics are needed for the concepts. +- Use [WCAG 2.2 AA](https://www.w3.org/TR/WCAG22/) as the engineering target, including reflow, focus, target sizes and motion controls. Israel's [internet accessibility guide](https://www.gov.il/he/pages/website_accessibility) and [SI 5568-1 (2023)](https://www.gov.il/BlobFolder/legalinfo/israeli_accessibility_standards_pdf/he/sitedocs_si-5568-1-september-2023.pdf) require a separate applicability and national-adjustments review. No exemption or legal compliance is assumed from the site's personal branding. +- An accessibility statement must describe actual tests and known third-party limits. Automated axe/Lighthouse results alone do not establish full conformance. Screen-reader testing and legal applicability remain separate review items before publication. + +These are implementation research notes, not a legal certification. Final policy text and compliance claims must follow the services actually enabled and the owner's verified circumstances. diff --git a/docs/POSTS-SYNC.md b/docs/POSTS-SYNC.md new file mode 100644 index 0000000..e454f15 --- /dev/null +++ b/docs/POSTS-SYNC.md @@ -0,0 +1,177 @@ +# Telegram posts synchronization + +This optional Python 3.11+ service maintains the native site's post index using supported Telegram Bot API events. It has no Python package dependencies. It does not scrape `t.me/s`, download Telegram media, run inside the browser, publish a site, or send messages. + +The implementation and offline tests are available. No bot credentials, authenticated requests, live channel updates, scheduler, or deployment have been configured or verified. The current site's manually verified snapshot remains the live input until an operator explicitly runs the service and reviews its output. + +## Supported contract and limits + +- A dedicated bot receives `channel_post` and `edited_channel_post` updates. This service requires it to be an administrator of the verified public `@wizard_loop` channel. Grant only the minimum rights Telegram permits for membership, not publishing, deleting, inviting, or editing rights unless separately needed. +- The existing verified IDs in `src/content/posts.json` form a deliberately incomplete manual history. New and edited events update that index. The service cannot backfill arbitrary historical posts through Bot API, and Bot MTProto `messages.getHistory` is not available to bots. +- Telegram retains undelivered updates for at most 24 hours. Poll regularly and alert on failures. An outage beyond the retention window can create a permanent gap. The index always declares `historyComplete: false`; `lastSyncedAt` means the last successfully persisted poll, not proof of complete history. +- The reviewed API has no general channel-post deletion event. Maintain an owner-reviewed exclusion file for removed posts. This service does not infer deletion from a failed widget or a temporary network error. +- Album members share `media_group_id` and accumulate across polls in the private normalized cache. The index emits one item with `messageIds`, uses the smallest message ID as its official embed reference, and preserves individual IDs for source links. It cannot know whether an album is complete until its events arrive; a first poll may briefly show a single member. Telegram controls the widget's album presentation. +- TGS, WebM stickers, animations, video, custom emoji and newer rich message types are identified, not flattened into still images. The official widget remains the media renderer. Some content requires Telegram's app; no full playback or formatting fidelity is claimed. The native excerpt is plain text only, while the official embed retains Telegram's own formatting when supported. +- A verified `getChat` response with `has_protected_content: true` produces an empty public index with `status: protected`. No updates are polled or acknowledged while this authoritative flag is present. A message-level protection flag also excludes that message. Protection changes unavailable because of a network failure cannot be detected until a successful check. + +Authoritative references: [receiving updates and retention](https://core.telegram.org/bots/api#getting-updates), [getUpdates cursor semantics](https://core.telegram.org/bots/api#getupdates), [channel messages and media groups](https://core.telegram.org/bots/api#message), [bot channel delivery](https://core.telegram.org/bots/faq#what-messages-will-my-bot-get), [individual official Post Widgets](https://core.telegram.org/widgets/post), and [users-only history method](https://core.telegram.org/method/messages.getHistory). Reviewed 2026-10-27. + +## Run the offline checks + +From the repository root, with Python 3.11 or newer: + +```sh +python -m unittest discover -s services/posts-sync -p 'test_*.py' +python services/posts-sync/posts_sync.py --fixture services/posts-sync/fixtures/updates.json --output services/posts-sync/test-output/posts.json --state-dir services/posts-sync/test-output/private +``` + +The fixture contains synthetic channel events clearly labeled as fixtures. It uses the real manual snapshot only as its seed. Its output is marked `status: fixture`, and the CLI refuses the real `src/content/posts.json` as a fixture output. It never creates a Bot API client or uses credentials. The generated `test-output` directory is ignored by Git and must never be published. + +## Configure a separate operator service + +1. Create a dedicated bot using Telegram's official BotFather flow. Add it as a channel administrator with minimum permissions. Do not reuse a feedback bot or a bot with another updates consumer. +2. Set `POSTS_TELEGRAM_BOT_TOKEN` in the host's protected environment or secret manager. Do not paste tokens into shell history, source code, public artifacts, CI command arguments, or logs. +3. Set `POSTS_SYNC_STATE_DIR` to a persistent private directory outside this repository and outside any web document root, accessible only to the service account. On Windows, configure its ACL explicitly; Python's POSIX mode flag does not replace Windows ACLs. The service refuses live state anywhere under this repository. +4. Back up the private directory. It contains the update cursor, bot/channel identity binding, blocked IDs, normalized public-message metadata and, after a protection change, `last-public-index.json` for recovery. That backup is the previous public snapshot, now retained privately. The directory contains no bot token, Telegram file IDs, raw API response, private-chat text, author identity, or session credentials. Avoid sharing it because it contains operational metadata and recovery content no longer included in the public index. +5. Run manually with a separately staged index, inspect it, then copy the reviewed result into the site's content input before the site's normal build/review process. A live run consumes this bot's events. Only one consumer may own it. + +Example after securely setting the two environment variables: + +```sh +python services/posts-sync/posts_sync.py --live --seed src/content/posts.json --output services/posts-sync/test-output/posts.json +``` + +`--state-dir /absolute/private/directory` can be used instead of `POSTS_SYNC_STATE_DIR`. The path is not a secret but must be private. After live verification and explicit integration approval, omit `--output` to update `src/content/posts.json` directly before a build. Keep the normalized private state and use the last generated index as the next `--seed` when running in a separate checkout or service directory. + +The service verifies the bot, resolves the exact public channel, and checks administrator membership. An existing webhook is a hard error. It never calls `deleteWebhook`, `setWebhook`, `sendMessage`, or any destructive method. Inspect and consciously change any competing integration outside this service before retrying. + +## Scheduling, persistence, and failure behavior + +A persistent host or small scheduled job every 1-5 minutes is appropriate after live verification. This repository does not create that schedule or deploy the service. Keep one concurrent instance, durable private storage, a secret-backed environment, generic failure alerts, and a timeout long enough for bounded network retries. Ephemeral CI without durable state is not a suitable default. + +Each poll requests at most 100 updates with a 20-second long-poll timeout. Requests have a 35-second socket timeout and at most three attempts. HTTP 429 respects a short server delay; a delay over 30 seconds stops the run so the scheduler can retry later. Each run drains at most 20 batches. Another run continues any backlog. Unwanted update types and other chats are skipped, without persisting their contents. + +The public index is replaced atomically first, then the private cursor is replaced atomically. Only a subsequent request acknowledges the new offset to Telegram. If writing the index fails, the cursor does not advance. If writing the cursor fails after a valid index write, replay is safe and deterministic. A process crash between the writes can leave the valid index ahead of its cursor; rerun from the intact state and index. This is idempotent recovery, not a multi-file filesystem transaction. If the state is lost, restore its backup: simply resetting the cursor does not recover expired updates or previous album membership. + +Ordinary network, configuration, and parsing failures leave the last valid index on disk. If a later batch fails after earlier batches succeeded, those earlier batches remain safely saved. There is no rollback to older, less complete content. A crashed process can leave a `.lock`; first verify no sync process is running, then remove that specific stale lock manually. Never run concurrent writers or share one bot across consumers. + +An authoritative channel-protection change is handled differently from an outage. The service retains the previous index privately, keeps the normalized cache/cursor, and writes `posts: []` with `status: protected` and a `checkedAt` timestamp. It does not present this as a successful content poll or advance `lastSyncedAt`. Repeated protected checks preserve the private recovery copy. After a later verified unprotected response, that seed and the cached messages can be restored through normal polling, with owner exclusions still applied. This does not recover updates that expired while protection was active. A failed write stops preparation without acknowledging updates; an already deployed static snapshot is only removed after a separately approved build/deployment. + +The private cache is bounded to 10,000 messages and each JSON file/response to 4 MiB. Exceeding a bound stops processing before acknowledging that batch; plan an archive strategy before growing beyond it. No messages are silently dropped to fit a limit. + +## Manual removal and public JSON contract + +For confirmed deleted or intentionally excluded post IDs, create a private JSON array, for example `[123, 124]`, and pass `--exclude-ids /private/removed-posts.json` on every run. IDs are examples, not claims about existing channel posts. Apply the same exclusion list consistently; removing a cached ID from the list allows it to appear again. Excluding one member currently removes an overlapping preserved seed album, and a cached album is rebuilt from its remaining unexcluded members. Review the result. + +The output retains the existing fields `channel`, `source`, `verifiedAt`, and `posts` with `id`, `title`, `kind`, `url`, and `note`. Additions: + +| Field | Meaning | +| --- | --- | +| `status` | `incremental` for a saved live poll, `protected` with no public posts for a verified protected channel, or `fixture` for synthetic output | +| `lastSyncedAt` | UTC time of the last saved batch | +| `checkedAt` | UTC time of an authoritative protected-channel check, when applicable | +| `historyComplete` | Always `false` | +| `posts[].origin` | `manual-verified`, `telegram-updates`, or `fixture` | +| `posts[].date` / `editedAt` | Telegram's original publication / edit timestamp when known | +| `posts[].messageIds` | Known member IDs, including the representative ID | +| `posts[].media` | Detected media kinds, not a playback capability guarantee | +| `posts[].excerpt` | Plain text preview, limited to 280 characters | + +Titles use the first real message line or a factual media label when no text exists. Public URLs are constructed only for the known channel. Telegram file IDs, expiring CDN URLs, chat IDs, bot IDs and credentials never enter the public JSON. Public text normalizes the prohibited punctuation to an ASCII hyphen. Render all strings as escaped text; never inject `excerpt` or `title` through raw HTML. Keep official widgets consent-gated and unloaded until requested, with original source links always accessible. + +## Required live acceptance before claiming connection + +Verify one new channel post, an edit, an album delivered across updates, an animated sticker/custom emoji post, a temporary failure and successful recovery, and an owner-excluded deleted post. Check the generated index, source links, official embeds, consent behavior, mobile layout and motion controls. Confirm no token or operational identity appears in logs or built files. These authenticated and real-media checks remain unperformed because credentials were not supplied. Only then update the site's connection wording and approve any automated build or deployment integration separately. + +## Prepared persistent-host scheduling + +The `services/posts-sync/deploy/` directory contains inactive Linux/systemd templates. They are files for operator review, not an installed service. No timer has been enabled and no host provisioned. A persistent host is recommended instead of GitHub Actions cache: the cursor, album membership and public history must survive every run, and a cache miss cannot reconstruct expired events. A hosted workflow must never start a fresh `getUpdates` consumer with a missing cursor. + +The prepared schedule has two independent paths: + +| Schedule | Account and behavior | +| --- | --- | +| Five minutes after the previous posts run ends | `wizardloop-sync` polls supported events into durable public JSON and a separate private cursor. | +| Six hours, at 00:15, 06:15, 12:15 and 18:15 UTC with a short jitter | A profile sync runs first as `wizardloop-sync`; then `wizardloop-build` creates a validated local static archive. | + +The builder has no credential environment or credential mount. Its service denies access to the private cursor and root-only secret directory. It receives only an explicit file allowlist: posts JSON plus profile JSON, JPEGs, and MP4 when the manifest declares video. A shared filesystem lock gives the builder a consistent copy while preventing concurrent snapshot writes. The builder releases the lock before dependency installation or static generation, so builds do not delay ordinary polling. A long sync can delay another sync; monitor duration and failures. + +The builder archives an operator-approved local Git commit, copies the public snapshot into that temporary source tree, runs `npm ci --ignore-scripts` and `npm run check`, then writes `static.zip` and `provenance.json` to `/var/lib/wizardloop-build/artifacts/-/`. The existing build regenerates profile-derived brand assets and checks the static output. Provenance records the source commit, snapshot times, profile source/status and SHA-256 of the archive. It explicitly declares `published: false`. Browser/media acceptance remains a separate release check. No GitHub dispatch, upload, push, merge or deployment happens. + +### Install on a separately approved Linux host + +These are future operator instructions. Do not execute the activation section until live integration and scheduling are explicitly approved. Use a maintained Linux distribution with systemd supporting `LoadCredential`, Python 3.11+, Node satisfying `package.json`, npm, Git, FFmpeg, and fonts needed by the brand generator. Ensure `/usr/bin/npm` points to the intended npm installation, or adapt both the script and reviewed unit paths. Test Linux font rendering because it can differ from the locally measured Windows output. + +1. Create system accounts and private directories. These names are deployment examples used by the templates, not existing infrastructure: + +```sh +sudo groupadd --system wizardloop-content +sudo useradd --system --no-create-home --home-dir /nonexistent --shell /usr/sbin/nologin --gid wizardloop-content wizardloop-sync +sudo useradd --system --no-create-home --home-dir /var/lib/wizardloop-build --shell /usr/sbin/nologin --gid wizardloop-content wizardloop-build +sudo install -d -o root -g root -m 0755 /opt/wizardloop +sudo install -d -o wizardloop-sync -g wizardloop-content -m 0750 /var/lib/wizardloop-content +sudo install -d -o wizardloop-sync -g wizardloop-content -m 2750 /var/lib/wizardloop-content/public /var/lib/wizardloop-content/public/profile +sudo install -d -o wizardloop-sync -g wizardloop-content -m 0700 /var/lib/wizardloop-content/private +sudo install -o wizardloop-sync -g wizardloop-content -m 0640 /dev/null /var/lib/wizardloop-content/snapshot.lock +sudo install -d -o wizardloop-build -g wizardloop-content -m 0750 /var/lib/wizardloop-build +sudo install -d -o root -g root -m 0755 /etc/wizardloop +sudo install -d -o root -g root -m 0700 /etc/wizardloop/secrets +``` + +2. Install the explicitly approved repository revision at `/opt/wizardloop/repository`, owned by root and read-only to both service accounts. Use only `WizardLoop/wizardloop.github.io`; pin the reviewed commit from the approved main branch. This is an operator-controlled checkout, not a service that automatically pulls new code. The builder records its exact commit and uses a narrowly scoped Git `safe.directory` setting for that root-owned path. + +```sh +sudo git clone https://github.com/WizardLoop/wizardloop.github.io.git /opt/wizardloop/repository +sudo git -C /opt/wizardloop/repository checkout --detach APPROVED_MAIN_COMMIT +sudo python3 -m venv /opt/wizardloop/profile-venv +sudo /opt/wizardloop/profile-venv/bin/python -m pip install -r /opt/wizardloop/repository/services/profile-sync/requirements.txt +``` + +Replace `APPROVED_MAIN_COMMIT` with the explicitly reviewed full commit hash. Do not use the literal placeholder. Both checkout and environment must remain non-writable by service accounts; dependency updates are separate operator-reviewed changes. + +3. Provision four root-owned, mode `0600` secret files from a secret manager: `/etc/wizardloop/secrets/posts-token`, `profile-api-id`, `profile-api-hash`, and `profile-token`. Use separate bots for posts and profile visibility. Do not print or place values in command history. `LoadCredential` provides each sync unit only its required files. The adapter passes them only to that sync subprocess. See the profile setup document for obtaining the API credentials; no personal Telegram session is used. + +4. Create `/etc/wizardloop/removed-posts.json` as a root-owned, group-readable JSON array, initially `[]`, with group `wizardloop-content` and mode `0640`. This file is always supplied to the scheduled poller. Keep confirmed removals there across every run. + +5. Copy the reviewed service/timer files and validate them on the target system. Copying these files does not enable timers: + +```sh +sudo install -o root -g root -m 0644 /opt/wizardloop/repository/services/posts-sync/deploy/wizardloop-posts.service /etc/systemd/system/ +sudo install -o root -g root -m 0644 /opt/wizardloop/repository/services/posts-sync/deploy/wizardloop-posts.timer /etc/systemd/system/ +sudo install -o root -g root -m 0644 /opt/wizardloop/repository/services/posts-sync/deploy/wizardloop-profile.service /etc/systemd/system/ +sudo install -o root -g root -m 0644 /opt/wizardloop/repository/services/posts-sync/deploy/wizardloop-build.service /etc/systemd/system/ +sudo install -o root -g root -m 0644 /opt/wizardloop/repository/services/posts-sync/deploy/wizardloop-build.timer /etc/systemd/system/ +sudo systemd-analyze verify /etc/systemd/system/wizardloop-posts.service /etc/systemd/system/wizardloop-posts.timer /etc/systemd/system/wizardloop-profile.service /etc/systemd/system/wizardloop-build.service /etc/systemd/system/wizardloop-build.timer +sudo systemctl daemon-reload +``` + +The global `/etc/wizardloop/ENABLE_CONTENT_SYNC` gate is intentionally absent after installation. All services refuse to run without it. Unit parsing, OS permissions, lock behavior, Node dependencies and actual execution must be checked on the intended Linux host; this Windows development environment has not executed systemd. + +### Separate approval: live acceptance and scheduling + +Only after explicit approval, create the gate and run the first poll manually. It must succeed before building, because the builder rejects a fixture or an unconnected manual snapshot as a live input. It accepts an authoritative `protected` status only when the public posts array is empty, allowing a reviewed build to remove stale embeds: + +```sh +sudo install -o root -g root -m 0644 /dev/null /etc/wizardloop/ENABLE_CONTENT_SYNC +sudo systemctl start wizardloop-posts.service +sudo systemctl start wizardloop-build.service +``` + +The build service starts its profile prerequisite. Inspect generic journal events, manifests, source links, media, the generated archive and provenance; perform the acceptance list above. Exit success can still mean profile fallback or retention, so inspect `profileSource` and `profileStatus`. If approved, enable the timers: + +```sh +sudo systemctl enable --now wizardloop-posts.timer wizardloop-build.timer +sudo systemctl list-timers wizardloop-posts.timer wizardloop-build.timer +``` + +Set host monitoring to alert on failed units, stale successful-poll timestamps, growing disk use and stale locks well before 24 hours. The templates do not install an alert destination. Keep encrypted private-state backups and a bounded artifact-retention policy. Never copy the entire `/var/lib/wizardloop-content` directory into a public artifact. Rotate bot secrets by replacing only the corresponding root-owned file; stop both timers while moving between hosts to avoid competing consumers. + +To stop future scheduling, disable both timers and remove the single gate file. Allow a currently running job to finish before making state changes. A forced process termination may leave the service's inner `.lock` or `.sync.lock`, which needs the manual recovery described earlier. Do not delete a lock while another process owns it. + +### Review and later delivery to GitHub Pages + +Prepared archives remain private on the host. An authorized operator can copy only `static.zip` and `provenance.json` to a review location and independently verify the SHA-256. The archive contains only the validated `dist` tree, including the existing CNAME, with no cursor, source checkout, dependencies or credentials. Local archive preparation is not a Pages deployment. + +A future approved delivery path can use a GitHub App with the minimum repository Actions permission to dispatch a separate artifact-import/build workflow from the approved main branch. That workflow must verify an immutable bundle hash/provenance and upload only the static artifact; a distinct protected Pages job must require the owner's deployment approval. It should consume a public-content snapshot from this service, never poll `getUpdates` or restore cursor state from an optional Actions cache. No such GitHub App, workflow dispatch credential, remote upload endpoint or automated delivery is implemented or active here because those require choosing and approving the host and delivery trust boundary. The fully prepared local bundle remains usable through the normal approved review/deployment process without those integrations. + +Primary systemd references: [timer behavior](https://www.freedesktop.org/software/systemd/man/latest/systemd.timer.html) and [credentials and execution isolation](https://www.freedesktop.org/software/systemd/man/latest/systemd.exec.html). Offline adapter tests verify secret-free build environments, exact public-file copying, fixture/path rejection, stale-video removal, safe archive extraction and inactive unit gates. They do not claim Linux host acceptance or live scheduling passed. diff --git a/docs/PROFILE-SYNC.md b/docs/PROFILE-SYNC.md new file mode 100644 index 0000000..731e6c7 --- /dev/null +++ b/docs/PROFILE-SYNC.md @@ -0,0 +1,127 @@ +# Profile synchronization + +This service is implemented for local review. No Telegram credentials have been supplied, no bot login has been attempted, and authenticated profile or video downloads remain untested. Offline fixtures exercise selection, fallback, output and safety behavior without claiming that a Telegram account has been synchronized. + +## Public output contract + +Run the service from the repository root. Its default output directory is the repository's `public/media/profile`, anchored to the script location rather than the current working directory. The site may read its JSON during a static build; the browser never receives Telegram credentials or talks to MTProto. + +| File | Purpose | +| --- | --- | +| `avatar.jpg` | Validated current profile still, GitHub fallback or local mark. | +| `profile.mp4` | Best accessible full Telegram profile animation. Absent when unavailable. | +| `poster.jpg` | Frame selected with Telegram's timestamp when FFmpeg succeeds; otherwise the profile still. | +| `profile.json` | Allowlisted public metadata and stable site-relative asset URLs. | + +The manifest fields are `source`, `avatar`, `video`, `poster`, `bio`, `bioSource`, `syncedAt`, `checkedAt`, `status`, `posterSource`, `width`, `height` and `mediaNote`. `video` is either `/media/profile/profile.mp4` or `null`. `source` is `telegram`, `github` or `local`; `status` is `current`, `fallback`, `retained` or `fixture`. `syncedAt` remains unchanged when retaining a previous snapshot after a source outage. `checkedAt` tracks the latest attempt. A fixture status and note must never be presented as a successful live sync. + +The service does not create favicons, Apple icons or social compositions. After a changed snapshot, run the site's asset-generation and static-build pipeline so those assets and Schema.org image references are derived from the same snapshot. Keep their public URLs stable. Publish a complete validated build only through the separately approved deployment process. + +## Selection and privacy rules + +The bot resolves the known `wizardloop` username and reads `users.getFullUser`. It uses `User.photo.photo_id` as the active identity. A matching `full_user.profile_photo` is preferred; otherwise up to five pages of `photos.getUserPhotos` are searched for that exact ID. Upload dates, list order and the highest photo ID never determine the primary. An unresolved identity causes retention or fallback, not a guess. + +Contact-specific `personal` photos and the separately identified personal/fallback profile fields are rejected. A confirmed primary removal or privacy change prevents retaining the old Telegram image. The bot should be dedicated to this purpose and should not be added as a contact to expand profile visibility. The owner must verify the bot-visible biography and image are intended for public publication before enabling a live schedule. + +Only `video_sizes` type `u` is accepted as full profile MPEG4. Type `p` is a small preview and is never silently substituted. When more than one full rendition is exposed, the largest pixel area and then file size wins. This is the best rendition Telegram makes available, not a claim to retrieve an unavailable pre-upload original. Original MP4 bytes are preserved; the service does not transcode or reduce their quality. Oversized or invalid media is rejected instead of choosing an inferior rendition. + +The current photo identity is checked again after download. File reference failures trigger a fresh lookup before a retry. If the full animation is inaccessible but the current still is valid, the manifest explicitly describes the still-only result. `video_start_ts` controls the optional poster extraction. Missing FFmpeg or failed extraction leaves a valid still poster and an honest `posterSource`. + +Telegram's supported `about` field is the first biography source. If absent, the public GitHub biography is read independently of GitHub image downloading. The editable local biography is last. Biography text is limited to 500 characters, control characters are removed and U+2014 is converted to a normal hyphen. Render this field as escaped text, never as HTML. + +## Isolated setup + +Use Python 3.11 or newer. Create a dedicated environment rather than installing dependencies into a system Python: + +```sh +python -m venv services/profile-sync/.venv +``` + +Linux/macOS: + +```sh +services/profile-sync/.venv/bin/python -m pip install -r services/profile-sync/requirements.txt +services/profile-sync/.venv/bin/python -m unittest discover -s services/profile-sync -p 'test_*.py' -v +``` + +Windows PowerShell: + +```powershell +services/profile-sync/.venv/Scripts/python.exe -m pip install -r services/profile-sync/requirements.txt +services/profile-sync/.venv/Scripts/python.exe -m unittest discover -s services/profile-sync -p 'test_*.py' -v +``` + +Telethon 1.45.0 and Pillow 13.0.0 were verified against their official PyPI releases on 2026-10-27. Resolved transitive dependencies are pinned in the same requirements file. Recheck releases and advisories when updating; do not let an unreviewed upgrade silently change Telegram schema handling. + +## Credentials + +Supply these only through the host's secret store or a private process environment: + +| Variable | Meaning | +| --- | --- | +| `TELEGRAM_API_ID` | API application ID issued by Telegram. | +| `TELEGRAM_API_HASH` | Its private API application hash. | +| `TELEGRAM_BOT_TOKEN` | Dedicated bot token issued by BotFather. | +| `PROFILE_LOCAL_BIO` | Optional editable public fallback biography. | +| `FFMPEG_BINARY` | Optional path to an installed, maintained FFmpeg binary. Otherwise `PATH` is searched. | + +The service uses bot authorization with a fresh `MemorySession`. It never prompts for a phone number, exports sessions or writes a `.session` file. Personal sessions are deliberately unsupported. Setting `TELEGRAM_SESSION`, `TELEGRAM_STRING_SESSION` or `TELEGRAM_PHONE` produces a configuration error. Do not pass secrets as command-line arguments, commit a private environment file or store credentials inside the public output directory. No API responses, access hashes, user IDs or session data are serialized into the manifest. + +The application ID/hash can be obtained through [Telegram API development tools](https://my.telegram.org). Create the dedicated bot through the official [BotFather setup](https://core.telegram.org/bots/features#botfather), then provision its token privately. This service does not need a destination chat ID and never sends a Telegram message. + +## Offline fixture and manual runs + +The fixture command makes no network requests and requires a separate output directory. The real repository profile directory is rejected even when the command is invoked from another working directory. It generates a synthetic local mark, labels its result `fixture`, and never invents an animated profile: + +```sh +python services/profile-sync/profile_sync.py --fixture services/profile-sync/fixtures/active-still.json --output services/profile-sync/test-output +``` + +Use the dedicated environment's Python executable in place of `python` when it is not activated. With private credentials provisioned, the manual sync command is: + +```sh +python services/profile-sync/profile_sync.py --force +``` + +An optional `--local-avatar path/to/approved-avatar.jpg` overrides the generated local mark. Without `--force`, a valid recent manifest avoids repeat requests for six hours. Missing Telegram credentials allow the public GitHub fallback or a valid existing snapshot; that is not a successful Telegram integration. Safe log events report only source, status, whether video exists and poster provenance. Raw exceptions, biographies and credentials are not logged. + +Exit `0` means a usable snapshot or cache was produced, including fallback or retention. Exit `1` means the operation failed. Exit `2` means setup or an existing output lock needs attention. Inspect `status` rather than treating an exit code alone as proof that Telegram succeeded. + +## Separate host and proposed schedule + +Nothing is deployed or scheduled by these files. A future authorized setup can run as a short-lived process on a small server or private CI runner: + +1. Install Python, the pinned environment and optionally FFmpeg on the separate host. +2. Provision bot secrets outside the checkout and public document root. Use a dedicated operating-system account with only the necessary output access. +3. Run the command against a private staging copy of the site's `public/media/profile` directory. Keep the previous valid snapshot between runs. +4. Validate the JSON and local assets, run the site's derivative-asset pipeline, then build and test the full static site. +5. Submit the generated public asset changes for review. Publishing requires the user's separate merge/deployment approval. + +A conservative future schedule is once every six hours, with a manual trigger. Use a scheduler concurrency limit of one, a finite overall job timeout, and notifications for sustained failure. Do not place secrets in a scheduler command string. No remote workflow, cron job, credential or production setting has been created here. + +## Failure and output behavior + +- Ordinary Telegram network/API failure preserves a valid previous Telegram snapshot. A first run falls back to GitHub and then the local mark. Existing GitHub fallback data can refresh on later runs. +- A confirmed missing/personal primary prevents stale Telegram retention. If visibility changes are uncertain because the service cannot connect, the last known valid image remains until a successful check or manual owner removal. +- Requests have bounded retries, timeouts and backoff. Telegram flood waits over 30 seconds stop the attempt. Image bytes are limited to 12 MiB and video bytes to 64 MiB; decoded images are limited to 16 million pixels. Downloads are held in bounded memory and validated before publication. +- GitHub requests use fixed public identity and allowlisted HTTPS hosts. Redirects are revalidated. No GitHub token or browser scraping is needed. +- JPEG normalization removes source metadata without reducing pixel dimensions. MP4 container checks reject truncated or non-MP4 payloads; they are not a substitute for real cross-browser playback testing. +- A lock prevents concurrent writers. Each file replacement is atomic, the manifest is committed last, and ordinary write failures restore prior files. This is not a multi-file filesystem transaction or a live-hosting atomic deployment. Build from the output only after the process exits and publish the complete static build as a unit. +- A killed process can leave `.sync.lock`. Confirm no sync process is running before removing that one lock file. Never publish lock or `.pending` files. No automatic stale-lock deletion can accidentally release a live worker's lock. + +## Verification and remaining acceptance tests + +Offline tests cover active ID selection against newer history, personal/fallback rejection, missing and full video variants, original rendition ranking, still-only output, independent biography fallback, cache behavior, outage retention, GitHub/local fallback, privacy removal, safe download hosts, write rollback, timestamped poster command arguments and non-secret logs. Telethon constructor signatures are checked when the pinned package is installed. + +Measured local result on 2026-10-27: 31 tests passed with zero failures, errors or skips under Telethon 1.45.0 and Pillow 13.0.0. The tests include independent biography fallback when an avatar download fails and rejection of fixture writes to the real profile directory from another working directory. The separate fixture command completed with `status: fixture`, `video: null` and valid 640 x 640 JPEG assets. Python compilation and the U+2014 source-content check also passed. The ignored `services/profile-sync/test-output/test-results.json` records the local test count and dependency versions. + +Authenticated acceptance still requires an authorized bot and owner review: confirm bot visibility of the active primary; compare an older photo deliberately selected as primary against a newer history entry; verify a real `u` video download and intended poster frame; test an actual file-reference refresh; confirm profile removal/privacy behavior; and play the resulting MP4 in Chrome, Firefox and WebKit. The UI must use `muted`, `loop`, `playsinline`, stable dimensions, a pause control and a still under reduced motion. No report in this repository claims these authenticated checks passed. + +## Primary references + +- [Telegram UserProfilePhoto](https://core.telegram.org/constructor/userProfilePhoto), [userFull](https://core.telegram.org/constructor/userFull) and [photos.getUserPhotos](https://core.telegram.org/method/photos.getUserPhotos). +- [Telegram video size types](https://core.telegram.org/api/files#videosize-type-values), [videoSize](https://core.telegram.org/constructor/videoSize) and [InputPhotoFileLocation](https://core.telegram.org/constructor/inputPhotoFileLocation). +- [Bot authorization](https://core.telegram.org/method/auth.importBotAuthorization) and [users.getFullUser](https://core.telegram.org/method/users.getFullUser). +- [Official Telethon client documentation](https://docs.telethon.dev/en/stable/modules/client.html), [Telethon on PyPI](https://pypi.org/project/Telethon/) and [Pillow on PyPI](https://pypi.org/project/Pillow/). + +New service code is MIT under the repository license. Installed package metadata identifies Telethon and pyaes as MIT, Pillow as MIT-CMU, rsa as Apache-2.0, and pyasn1 as BSD-2-Clause. Third-party libraries retain those licenses. Profile media and owner branding are not relicensed by this service. diff --git a/docs/QA-INTEGRATIONS.md b/docs/QA-INTEGRATIONS.md new file mode 100644 index 0000000..9cf4226 --- /dev/null +++ b/docs/QA-INTEGRATIONS.md @@ -0,0 +1,43 @@ +# Integration verification + +Reviewed locally on **2026-10-27** for the unreleased `1.0.0-rc.1` build. These results do not establish production readiness, authenticated Telegram synchronization or real feedback delivery. See [QA.md](QA.md) for the final website/browser and performance review. + +## Offline services + +| Component | Observed result | What the result covers | +| --- | --- | --- | +| Profile synchronization | **31 passed, 0 failures, 0 errors, 0 skips** | Active-primary identity matching, video/still selection logic, independent bio fallback, caching, privacy removal, safe output paths, bounded failures and last-good preservation. Telethon constructor checks ran with Telethon 1.45.0 and Pillow 13.0.0. | +| Posts synchronization | **45 passed** | 33 ingestion/state tests plus 12 deployment-adapter tests. Includes albums across batches, edits, cursor ordering, exclusions, protected-channel removal, private recovery, build isolation and inactive deployment gates. | +| Feedback service | **22 passed** | Provider-mocked validation, Turnstile verification, restrictive origins, rate limiting, safe Telegram formatting, timeouts, uncertain delivery and the loopback-only HTTP mock. No message was sent to Telegram. | + +Profile evidence is recorded in the ignored `services/profile-sync/test-output/test-results.json`. The posts and feedback counts are the observed local test-run results; their reproducible commands are: + +```sh +python -m unittest discover -s services/profile-sync -p 'test_*.py' -v +python -m unittest discover -s services/posts-sync -p 'test_*.py' +npm run test:unit +``` + +Install the profile service's pinned requirements in an isolated environment first. Independent regression evidence in `artifacts/sync-service-review.json` confirms that an accessible GitHub bio survives avatar failure, a fixture cannot overwrite the real profile directory from another working directory, and an authoritative protected-channel response produces an empty public index. Private cursor/history remain available for recovery; removing an already deployed snapshot still requires an approved deployment. + +## Real public Telegram embeds + +The local Posts page was tested in Chromium against the real public provider, without bot credentials or send actions. The initial observation is timestamped **2026-10-27T03:18:49.901Z** in `artifacts/final/telegram-live-results.json`. + +- No Telegram requests occurred before consent. Official post frames for [51](https://t.me/wizard_loop/51), [52](https://t.me/wizard_loop/52), [53](https://t.me/wizard_loop/53) and [54](https://t.me/wizard_loop/54) loaded after consent. +- The inspected images loaded. Video metadata was available for posts 53 and 54. The motion follow-up made post 54 visible and explicitly invoked muted playback: its video advanced from 0 to **1.326075 seconds**, with `paused: false` and `readyState: 4`. This verifies that video's playback, not automatic playback of every embed. +- Pausing site motion removed all embedded frames. Revoking consent stored `no`. Telegram set its own cookies after consent; this supports the external-content disclosure. +- The provider returned **HTTP 500** for one `cdn4.telesco.pe/file/sticker.tgs` request. Two MP4 requests were recorded as aborted. The initial run recorded no console errors, but those external network failures remain part of the result. +- Custom-emoji/sticker canvas markup was present. In the visible follow-up, post 52's sampled canvas hashes did not change over the observation interval. **Animated TGS and custom-emoji fidelity are not proven.** Original Telegram links remain necessary; a loaded frame or canvas is not proof of complete media playback. + +Evidence: `artifacts/final/telegram-live-motion-results.json`, `telegram-live-visible-52.png`, `telegram-live-visible-54.png` and the two `telegram-official-post-*.png` screenshots. These ignored local artifacts describe that run, not a permanent provider guarantee. + +## Local feedback UI + +`artifacts/final/feedback-ui-results.json` records required-field focus, rejection before verification, keyboard submission to the actual loopback HTTP mock, uncertain-delivery messaging, retry with new local verification, and simulated rate-limit recovery. It explicitly records `realTelegramDelivery: false`, `realCAPTCHA: false` and `localHttpMockVerified: true`. + +## Remaining acceptance + +Authenticated primary-profile/video access, real MP4/poster selection and cross-browser playback need configured MTProto credentials and owner verification. Live posts ingestion, edits and scheduling have not been activated. Real feedback requires an approved Worker endpoint, Turnstile credentials, verified numeric private Chat ID and a separately authorized delivery test. Read [Profile sync](PROFILE-SYNC.md), [Posts sync](POSTS-SYNC.md) and [Feedback setup](FEEDBACK-SETUP.md) before these steps. + +No complete human screen-reader audit or legal/accessibility certification has been performed. No production deployment, merge, tag or release is represented by this report. diff --git a/docs/QA.md b/docs/QA.md new file mode 100644 index 0000000..8135efa --- /dev/null +++ b/docs/QA.md @@ -0,0 +1,99 @@ +# Website quality review + +Local review on **2026-10-27**, Windows, for unreleased `1.0.0-rc.1`. The selected Spectrum Circuit direction now uses WizardLoop's supplied wizard artwork, comic framing, local social icons and complete light/dark layouts. This report separates observed results from integrations that still need credentials or operational approval. + +## Review locally + +```sh +npm ci +npm run dev +``` + +Open [the website](http://127.0.0.1:4322), [the artwork collection](http://127.0.0.1:4322/work/), [posts](http://127.0.0.1:4322/posts/) or [feedback](http://127.0.0.1:4322/feedback/). Use the header controls to change theme and pause motion. The original three design studies run separately with `npm run dev:concepts` on port 4321; they are excluded from the final static build. + +For a production-like local review, stop the development server, run `npm run build`, then `npm run preview -- --host 127.0.0.1 --port 4322`. Neither path publishes the site. + +### Actual browser captures + +| Theme | Desktop, 1440 x 1000 | Mobile, 390 x 844 | +| --- | --- | --- | +| Light | [View desktop](previews/home-light-desktop.png) | [View mobile](previews/home-light-mobile.png) | +| Dark | [View desktop](previews/home-dark-desktop.png) | [View mobile](previews/home-dark-mobile.png) | + +These PNGs were captured from the built site with Playwright, with motion paused for reproducibility. Full-page route captures remain under `artifacts/final/`. `artifacts/final/wizardloop-home-animation.webm` records actual native hero animation; `hero-live-animation.png` captures a playing frame. The running local preview is the interactive example. + +## Browser acceptance + +The complete serial run recorded **129 passed, 6 failed and 1 skipped** across 136 cases. Follow-up checks corrected no-JavaScript/HTML-attribute test assumptions, exposed the Windows native looping failure described below, and verified its graceful fallback after the application fix. The final affected-behavior run recorded **15 passed, 0 failed and 1 explicitly unsupported check** across 16 cases. + +The latest result for each case is **138 passed, 0 unresolved failures and 2 skipped** across 140 unique engine/test combinations: Chromium 69 passed, Firefox 35 passed, and Windows WebKit 34 passed with 2 explicit limitations. This combines the complete run and focused rechecks; it is **not** a claim that a fresh complete local run was green. [The machine-readable summary](qa/browser-summary.json) preserves all six run histories and their failures. Full reports remain in `artifacts/final/`. + +The two skipped checks are uninterrupted native looping in this Windows WebKit installation and its unsupported `canvas.captureStream()` profile-video fixture. A separate real-media check passed for the Windows native-loop fallback. Chromium and Firefox passed sustained hero playback across a full loop boundary, theme changes and intentional pause/resume. + +Coverage includes all 12 routes, direct loads, local assets and browser errors; Chromium layouts at 320, 360, 390, 768, 1024 and 1440px in both themes; and selected Firefox/WebKit interaction coverage. The complete six-width matrix is not claimed for every engine. + +Interaction checks cover keyboard navigation, visible focus, skip links, modal focus containment/return, Escape, fixed header, saved preferences, manual motion pause, reduced motion, tooltip dismissal, real native hero playback, Canvas motion, gallery reveal/focus, profile fallbacks, JavaScript-free content and metadata, consent-gated posts, pagination and feedback failures. Provider-mocked tests are identified as such. + +The final accessibility scan set uses axe-core on Home, About, Work, Posts and Feedback at desktop/mobile sizes and in both themes. Automated checks do not replace a human screen-reader audit or establish legal conformance. + +### Windows WebKit media behavior + +The installed Windows WebKit uses its native Media Foundation backend. Its reported `videoWidth`/`videoHeight` can reflect destination dimensions, and native media requests can bypass Playwright route interception. This was checked against [the exact pinned WebKit source](https://github.com/WebKit/WebKit/blob/56453fdfe0b0ca6258c23e6453b34f70b885d13f/Source/WebCore/platform/graphics/win/MediaPlayerPrivateMediaFoundation.cpp#L1769): `BeginCreateObjectFromURL` loads the URL directly, destination dimensions configure output, and the resulting frame size becomes `naturalSize`. + +The MP4 container and Chromium/Firefox decoding verify 768 x 768 source dimensions. Windows WebKit reports its native display size instead; failed-media tests use a genuinely missing server URL because interception is bypassed. + +An uninterrupted probe also reproduced a native looping failure: both the real site and a bare video element advanced through the first 2.4-second clip, then became paused near the loop boundary without a JavaScript `pause()` call. Identical later video-frame captures confirmed that playback had stopped. This occurred before any screenshot, so an earlier screenshot-interference hypothesis was rejected. The native source sets a paused flag when a session ends, consistent with the observation, but the exact upstream cause is not established. Evidence is retained in `artifacts/final/webkit-capture-side-effect-probe.json`. + +The site now returns to its still preview after an unexpected native pause while playback is allowed, instead of repeatedly forcing a restart. User pauses, theme transitions, offscreen/hidden content and stale queued pause events remain separate cases. A dedicated Windows WebKit check verifies the real fallback; successful native looping on this installation is explicitly unsupported, not reported as passing. Actual Safari on Apple hardware remains unverified. + +## Measured performance + +Lighthouse **13.5.0**, mobile simulation, built local preview, three sequential runs with no concurrent acceptance browsers. [All sample summaries](qa/lighthouse-summary.json) are committed. Full HTML/JSON reports remain in `artifacts/lighthouse-review/`, `artifacts/lighthouse-review-2/` and `artifacts/lighthouse-review-3/`. + +| Page | Performance median | Observed range | Accessibility | Best Practices | SEO | Median LCP | Median TBT | Median CLS | +| --- | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | +| Home | 92 | 59-97 | 100 | 100 | 100 | 2.58 s | 133 ms | 0.00005 | +| Work | 95 | 94-96 | 100 | 100 | 100 | 1.49 s | 241 ms | 0 | +| Posts | 100 | 66-100 | 100 | 100 | 100 | 1.52 s | 49 ms | 0 | +| Feedback | 99 | 98-100 | 100 | 100 | 100 | 1.67 s | 23 ms | 0.03183 | + +Accessibility, Best Practices and SEO scored 100 in every one of these samples. Performance medians meet the 90+ target, but **not every run does**. Home and Posts had substantially slower initial-render outliers in the first run. Local machine and rendering variability remain material; these are lab results, not field Core Web Vitals or a guarantee of production performance. Median metrics are calculated individually rather than taken from one selected run. + +These samples include the final performance changes. The first precedes the paused-menu CSS fix; later tooltip hover/Escape fixes do not alter the initial, unfocused rendering path. The subsequent native-pause fallback is event-driven and adds no timer or initial media request. No audit-specific user-agent detection, animation suppression or benchmark delay is used. + +### Improvements supported by measurements + +- The original in-page SVG animation implementation measured Home performance 43 and about 16.4 seconds of total blocking time. The native hero now plays a 768 x 768 H.264 loop from the original animation, with a still poster, no audio and no video request under initial reduced motion. Separate theme files are approximately 645 KB and 635 KB. +- The 36 quality-94 WebP delivery images total 1,309,102 bytes, compared with 3,117,278 bytes for the preserved lossless masters, a reduction of about 58%. Source TGS files, dimensions and alpha channels are preserved. +- Work renders nine initial cards and reveals the remainder on request. Hidden cards do not request their previews during normal initial loading, while a no-JavaScript visitor can access all 30. +- Canvas playback is limited to three visible scenes and 24 drawn frames per second, while elapsed-time sampling preserves the original animation duration. Offscreen/hidden-document playback pauses. + +## Source, build and service checks + +Formatting, Astro/TypeScript checking and static generation passed. The last typecheck reported **0 errors, 0 warnings and 0 hints**. Static checks cover unique titles, HTML metadata without JavaScript, local links/assets, social images, domain preservation, exclusion of experiments/private services and basic secret patterns. Source checks enforce the ban on U+2014. The npm dependency audit reported **0 vulnerabilities** at review time; this is not a security certification. + +Offline service tests passed: **31 profile tests, 45 posts tests and 22 feedback tests**. Read [integration verification](QA-INTEGRATIONS.md) for scope and provider observations. A read-only security review found no additional concrete issue in the feedback validation, private-chat setup, public snapshot boundary, isolated synchronization adapters or workflow permissions. + +Quality CI is defined with read-only repository permissions and cannot publish Pages. Its actual remote result must be inspected separately. A manual deployment workflow exists but has not been run. + +## External limitations and remaining approval + +- Authenticated Telegram primary-profile/video retrieval is unverified. The public still snapshot is not claimed to be an authenticated original or the active primary video. Last-good assets and GitHub/local fallbacks remain available. +- Real public Telegram widgets loaded only after consent. A video in post 54 played in the observed run. A provider sticker request returned HTTP 500, and animated TGS/custom-emoji fidelity was not established. Original post links remain available and these limits are disclosed. +- Feedback validation and UI states were exercised with a loopback mock and mocked providers. There is no approved public Worker, configured Turnstile service or verified real private-message delivery. The public form remains unavailable until configured. +- Sync schedules and Linux/systemd deployment adapters are prepared but inactive. Their intended hosting environment and real provider credentials still need verification. +- A complete human screen-reader audit and native browser-chrome zoom inspection have not been performed. Responsive reflow and automated accessibility results do not establish complete WCAG 2.2 AA, Israeli Standard 5568 or legal compliance. +- GitHub Pages response-header controls and detailed hosting settings must be checked before an approved production operation. Source configuration alone does not establish active CSP, Permissions Policy, HTTPS settings or protected environments. + +No merge, production deployment, DNS change, Git tag or release is authorized by these results. Review the concrete changes and [deployment procedure](DEPLOYMENT.md) before approving any of those actions. + +## Reproduce + +```sh +npm run check +npx playwright install chromium firefox webkit +npm run test:browsers +npm run audit:performance +``` + +Browser tests use one worker locally and two in CI. Stop a development server before acceptance testing so the runner uses the static build. Run Lighthouse separately from browser tests while the built preview is listening on port 4322; set `LIGHTHOUSE_OUTPUT` to preserve separate samples. Optional service test commands and their pinned dependencies are in [the README](../README.md) and [integration verification](QA-INTEGRATIONS.md). diff --git a/docs/REPOSITORY-AUDIT.md b/docs/REPOSITORY-AUDIT.md new file mode 100644 index 0000000..58fd875 --- /dev/null +++ b/docs/REPOSITORY-AUDIT.md @@ -0,0 +1,88 @@ +# Repository audit - Phase 1 + +Reviewed on 2026-10-27 (Asia/Jerusalem). Scope: `WizardLoop/wizardloop.github.io` only. All original tracked files were read. Remote inspection used read-only GitHub connector calls and public HTTP observations. No repository settings, DNS, Pages source, production deployment, tags or releases were changed. + +## Baseline and repository identity + +- Repository: [WizardLoop/wizardloop.github.io](https://github.com/WizardLoop/wizardloop.github.io). +- Local origin: `https://github.com/WizardLoop/wizardloop.github.io.git` for fetch and push. +- Local working branch: `design/three-local-concepts`. +- Baseline commit: `658ed667c173f861cd16e6048126de12ec3648b9`, dated `2025-07-20T17:57:47+03:00`, subject `Create style.css`. +- Authenticated repository metadata: public, not archived, owner type `User`, default branch `main`, `has_pages: true`, repository license detection `null`. +- GitHub repository description: `A carefully crafted web potion.` Its homepage metadata currently uses `http://wizardloop.com`; the site's canonical URL and live HTTPS address use `https://wizardloop.com/`. + +At audit time, a diff of the four original tracked files against the baseline was empty. Local concepts and review tooling are additions. Existing production files and the domain declaration remain intact. + +## Complete original file inventory + +Sizes below are Git blob sizes, independent of checkout line-ending conversion. + +| File | Bytes | Findings | +| --- | ---: | --- | +| `CNAME` | 14 | Contains `wizardloop.com`. Preserve its value and the domain. | +| `README.md` | 23 | Only the production URL. No setup, architecture, testing or license explanation. | +| `index.html` | 9,879 | One static profile page, inline JavaScript, theme toggle, GitHub avatar links, metadata and social links. | +| `assets/css/style.css` | 17,634 | Minified PaperMod-derived stylesheet with explicit MIT attribution; many unused blog/search/code-highlighting selectors. | + +There were no original dependency manifests, lockfiles, build configuration, `.github/workflows`, tests, local avatar media, content collections, `robots.txt`, `sitemap.xml`, custom 404 page, web app manifest, policy pages, accessibility statement, contributor guidance or repository-level `LICENSE`. There are no original application routes beyond the root HTML page and its static CSS asset. + +## Existing content and behavior + +The page identifies WizardLoop and presents a short coding tagline. It has no About narrative, work/projects content, Posts section, community navigation, feedback form or footer. The `