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..5a5bf8c --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,40 @@ +name: Quality checks +on: + push: + branches-ignore: + - main + 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..5ff31f8 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,32 @@ +# 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. +- Detect a frozen native-video clock even when the decoder still reports playback, and avoid false failures after delayed timer callbacks. +- 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. +- Correct the canvas media fixture, measure native loop capability independently from application code, and record actual encoded AVC metadata. +- 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 0000000..7048a05 Binary files /dev/null and b/concepts/assets/fonts/cormorant-italic-latin.woff2 differ diff --git a/concepts/assets/fonts/cormorant-latin.woff2 b/concepts/assets/fonts/cormorant-latin.woff2 new file mode 100644 index 0000000..023d95b Binary files /dev/null and b/concepts/assets/fonts/cormorant-latin.woff2 differ diff --git a/concepts/assets/fonts/manrope-latin.woff2 b/concepts/assets/fonts/manrope-latin.woff2 new file mode 100644 index 0000000..71eb731 Binary files /dev/null and b/concepts/assets/fonts/manrope-latin.woff2 differ diff --git a/concepts/assets/fonts/spacegrotesk-latin.woff2 b/concepts/assets/fonts/spacegrotesk-latin.woff2 new file mode 100644 index 0000000..0f3474e Binary files /dev/null and b/concepts/assets/fonts/spacegrotesk-latin.woff2 differ 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 0000000..d0df4dd Binary files /dev/null and b/concepts/assets/profile-telegram.jpg differ 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..0c1db5e --- /dev/null +++ b/docs/DEPLOYMENT.md @@ -0,0 +1,107 @@ +# 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. + +## What the owner should prepare in GitHub + +Prepare these values and review the current settings before the final approval. Do not change the production source or merge a publishing branch during preparation. + +| Location | Required preparation | +| --- | --- | +| Settings > Pages | Record the current publishing source. The prepared Astro workflow expects **GitHub Actions** at publication time. If Pages currently publishes a branch, resolve that transition before merging. Preserve the existing `wizardloop.com` custom domain and verify HTTPS. | +| Settings > Environments > github-pages | Review branch restrictions and any required reviewer. Limit production deployment to the reviewed `main` branch; the manual confirmation field is not an access-control boundary. | +| Settings > Actions > General | Allow the repository's checked-in GitHub Actions. The workflow declares its permissions; a personal access token is not required for Pages deployment. | +| Settings > Secrets and variables > Actions > Variables | If feedback will be enabled, prepare `PUBLIC_FEEDBACK_API_URL` including `/feedback`, and `PUBLIC_TURNSTILE_SITE_KEY`. Both are public values. Leave them empty until the separate Worker is configured and verified. | +| Pull request checks | Require the current `check` and `profile-sync-tests` jobs to succeed on the commit being reviewed. Check the attached media capability evidence as well as the pass/fail totals. | +| Recovery record | Retain the previously deployed artifact, its commit and current Pages/domain settings before the approved change. | + +The Pages source and environment behavior follow [GitHub's publishing-source guidance](https://docs.github.com/en/pages/getting-started-with-github-pages/configuring-a-publishing-source-for-your-github-pages-site) and [custom-workflow documentation](https://docs.github.com/en/pages/getting-started-with-github-pages/using-custom-workflows-with-github-pages). These are preparation instructions, not a claim that the account settings have been inspected or changed. + +For feedback, prepare a Cloudflare Worker endpoint, a Managed Turnstile widget for `wizardloop.com`, a dedicated Telegram bot and a verified private numeric Chat ID using [the setup guide](FEEDBACK-SETUP.md). Keep `TELEGRAM_BOT_TOKEN`, `TELEGRAM_CHAT_ID`, `TURNSTILE_SECRET_KEY` and `RATE_LIMIT_SALT` in Worker secrets, outside GitHub Pages. Never send those values in a PR or commit them. + +Automatic profile/posts synchronization additionally needs a separate private host, its service credentials and a reviewed method for delivering validated snapshots into the site build. The prepared schedules do not publish new snapshots to Pages. With no integrations activated, the static site retains its existing snapshots, Telegram links and an unavailable feedback form; approving publication does not make those integrations live. + +## Manual Pages workflow + +`.github/workflows/ci.yml` runs quality checks on development-branch pushes, pull requests or manual dispatch with read-only repository permissions. Pushes to `main` do not trigger this workflow. 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..f8966a1 --- /dev/null +++ b/docs/QA.md @@ -0,0 +1,114 @@ +# 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 + +### PR CI follow-up, 2026-10-28 + +The [first PR quality run](https://github.com/WizardLoop/wizardloop.github.io/actions/runs/40461052552) on Ubuntu 24.04 recorded **137 browser tests passed and 2 failed**. Formatting, source checks, typechecking, build, feedback unit tests and both Python test suites passed. The failures were confined to native-media tests in Linux WebKit: + +- The hero advanced through its first clip, then its clock stopped while `paused` remained `false`. Listening only for `pause` and `error` missed that state. A lightweight progress monitor now returns to the labeled still preview after four seconds of observed nonprogress. It stops while motion is disabled, the hero is offscreen, the document is hidden, or the page is suspended. A long timer scheduling gap resets observation rather than misclassifying a busy main thread. +- The canvas-stream fixture painted before capture started and never produced another frame. Its unbounded `play()` wait timed out. The fixture now paints after capture starts, keeps its source and player visible, uses an explicit start gesture, and asserts playback and clock progress with bounded waits. The later Linux run verified actual canvas-stream playback in WebKit as well as Chromium and Firefox. + +The native-media test now first measures the same MP4 in a bare video element without site code, attaching its timeline to the report. If that independent probe sustains playback, the full loop, theme and pause/resume assertions remain required. If it demonstrably stops or stalls, the test explicitly verifies the real site's still fallback and records the unsupported native loop in a platform annotation. It does not report fallback as successful uninterrupted playback. A separate frozen-clock fixture verifies the new monitor and preserves an intentional pause longer than its deadline. + +The frozen-clock regression uses controlled timers to verify both an intentional five-second pause and a delayed-callback gap before requiring sustained nonprogress to trigger fallback. The [subsequent Linux run](https://github.com/WizardLoop/wizardloop.github.io/actions/runs/40651474776) passed all media checks, with **141 passed and one unrelated Firefox failure**: the long-feed fixture timed out waiting for `networkidle`. That fixture now waits for its actual six-card UI state; the checks for 6/12/13 items, focus, consent and retained embed identity remain intact. + +The AVC manifest also now records the encoded codec `avc1.424029`, independently of the encoder's requested `avc1.42001F`; the original MP4 bytes are unchanged. Review the latest PR check run for the remote result of these corrections. The local results below describe the earlier review and remain historical evidence. + +### Earlier local browser review, 2026-10-27 + +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 original 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 was event-driven. The PR follow-up adds a 500ms progress sample only during permitted native playback; these Lighthouse measurements predate that monitor and are not a fresh benchmark of it. 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 `