From a6a538b7492ab15a8da2ea76ab5198af3800d668 Mon Sep 17 00:00:00 2001 From: srpatcha Date: Tue, 1 Sep 2026 21:33:28 -0700 Subject: [PATCH] chore: salvage the unique work from embeddedos-org before archiving it MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The organisation has two near-identical website repositories with no common git ancestor. This one serves the live site; embeddedos-org serves a 404: $ gh api repos/embeddedos-org/embeddedos-org/pages status: 404 url: None source: None and it has kept receiving real work regardless — an SEO canonical fix, a duplicate-element bug fix and a null-dereference guard, all as recently as 2026-08-30, applied to a site nobody can load. Six files exist there and not here. Ported so that repository can be archived without losing them: .github/dependabot.yml .github/workflows/health-check.yml weekly uptime check .github/workflows/lint-readme.yml README lint on push tests/simulation/test_mobile_full_simulation.py package-lock.json scripts/build-deploy.py HTML/CSS/JS minification Also removes style.css.bak, an orphan that the dead repository had already cleaned up and this one still carried. build-deploy.py needed fixing before it could be taken. It operated in place: it minified the tracked sources over themselves and then for d in ['tests', 'test-screenshots', '.github']: p = ROOT / d if p.exists(): shutil.rmtree(p) deleted the CI configuration and the test suite from the working tree. It is named "build for the deploy branch" and never made a branch — it mutated whatever checkout it was run in. I found this by running it, which cost me the .github and tests directories. It now stages a copy into dist/ and minifies that, so the source tree is never written to. A build step must not be able to damage the thing it is building from. Verified: working tree hash before=84e3fd378a71 after=84e3fd378a71 UNCHANGED .github and tests survive the run dist/ 97 files; tests, .github and scripts correctly excluded index.html 96,362 -> 92,410 bytes, title/canonical/ intact 32 files minified, 32,179 bytes saved dist/ was already in .gitignore. Refs #158 Co-Authored-By: Claude Opus 5 (1M context) --- .github/dependabot.yml | 75 + .github/workflows/health-check.yml | 39 + .github/workflows/lint-readme.yml | 42 + package-lock.json | 1964 +++++++++++++++++ scripts/build-deploy.py | 84 + style.css.bak | 986 --------- .../simulation/test_mobile_full_simulation.py | 799 +++++++ 7 files changed, 3003 insertions(+), 986 deletions(-) create mode 100644 .github/dependabot.yml create mode 100644 .github/workflows/health-check.yml create mode 100644 .github/workflows/lint-readme.yml create mode 100644 package-lock.json create mode 100644 scripts/build-deploy.py delete mode 100644 style.css.bak create mode 100644 tests/simulation/test_mobile_full_simulation.py diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..604feb7 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,75 @@ +# Standard, org-uniform Dependabot configuration template. +# +# Each repo should drop this file at .github/dependabot.yml and uncomment the +# ecosystem entries that apply. Keep weekly cadence so review backlogs stay +# manageable. Auto-assigning all PRs to a single triage owner (@srpatcha) +# prevents the "ten people CC'd, no one acts" failure mode. +# +# Reference template lives at embeddedos-org/.github/.github/dependabot-template.yml. + +version: 2 +updates: + # GitHub Actions — every repo should keep this enabled. + - package-ecosystem: github-actions + directory: / + schedule: + interval: weekly + day: monday + open-pull-requests-limit: 5 + assignees: + - srpatcha + labels: + - dependencies + - github-actions + + # Python — uncomment in repos with pyproject.toml or requirements.txt. + # - package-ecosystem: pip + # directory: / + # schedule: + # interval: weekly + # day: monday + # open-pull-requests-limit: 5 + # assignees: + # - srpatcha + # labels: + # - dependencies + # - python + + # Node.js — uncomment in repos with package.json. + # - package-ecosystem: npm + # directory: / + # schedule: + # interval: weekly + # day: monday + # open-pull-requests-limit: 5 + # assignees: + # - srpatcha + # labels: + # - dependencies + # - npm + + # Go — uncomment in repos with go.mod. + # - package-ecosystem: gomod + # directory: / + # schedule: + # interval: weekly + # day: monday + # open-pull-requests-limit: 5 + # assignees: + # - srpatcha + # labels: + # - dependencies + # - go + + # Docker — uncomment in repos with Dockerfile. + # - package-ecosystem: docker + # directory: / + # schedule: + # interval: weekly + # day: monday + # open-pull-requests-limit: 3 + # assignees: + # - srpatcha + # labels: + # - dependencies + # - docker diff --git a/.github/workflows/health-check.yml b/.github/workflows/health-check.yml new file mode 100644 index 0000000..4c0717d --- /dev/null +++ b/.github/workflows/health-check.yml @@ -0,0 +1,39 @@ +name: health-check + +on: + schedule: + # Weekly — Mondays 06:00 UTC + - cron: '0 6 * * 1' + workflow_dispatch: {} + +permissions: + contents: read + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: false + +jobs: + linkinator: + name: Crawl live sites for broken links + runs-on: ubuntu-latest + steps: + - name: Setup Node + uses: actions/setup-node@v4 + with: + node-version: '20' + + - name: Crawl developer portal + run: | + npx --yes linkinator@6 https://embeddedos-org.github.io \ + --recurse \ + --skip 'github.com|googletagmanager|fonts.googleapis' \ + --timeout 10000 \ + --retry \ + --retry-errors-count 2 + + - name: Crawl org profile + run: | + npx --yes linkinator@6 https://github.com/embeddedos-org \ + --skip 'login|signup|sponsors' \ + --timeout 10000 diff --git a/.github/workflows/lint-readme.yml b/.github/workflows/lint-readme.yml new file mode 100644 index 0000000..d475ac7 --- /dev/null +++ b/.github/workflows/lint-readme.yml @@ -0,0 +1,42 @@ +name: lint-readme + +on: + push: + paths: + - '**/*.md' + pull_request: + paths: + - '**/*.md' + +permissions: + contents: read + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: false + +jobs: + markdownlint: + name: markdownlint + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-node@v4 + with: + node-version: '20' + + - name: Run markdownlint + run: npx --yes markdownlint-cli '**/*.md' --ignore node_modules + + lychee: + name: lychee link check (offline) + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: lychee --offline + uses: lycheeverse/lychee-action@v2 + with: + args: --offline --no-progress --verbose './**/*.md' + fail: true diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..22f7aa4 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,1964 @@ +{ + "name": "embeddedos-website", + "version": "3.0.1", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "embeddedos-website", + "version": "3.0.1", + "devDependencies": { + "@playwright/test": "^1.42.0", + "html-validate": "^8.0.0", + "http-server": "^14.1.0", + "linkinator": "^6.0.0", + "wait-on": "^7.2.0" + } + }, + "node_modules/@hapi/hoek": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-9.3.0.tgz", + "integrity": "sha512-/c6rf4UJlmHlC9b5BaNvzAcFv7HZ2QHaV0D4/HNlBdvFnvQq8RI4kYdhyPCl7Xj+oWvTWQ8ujhqS53LIgAe6KQ==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@hapi/topo": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@hapi/topo/-/topo-5.1.0.tgz", + "integrity": "sha512-foQZKJig7Ob0BMAYBfcJk8d77QtOe7Wo4ox7ff1lQYoNNAb6jwcY1ncdoy2e9wQZzvNy7ODZCYJkK8kzmcAnAg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@hapi/hoek": "^9.0.0" + } + }, + "node_modules/@html-validate/stylish": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@html-validate/stylish/-/stylish-4.3.0.tgz", + "integrity": "sha512-eUfvKpRJg5TvzSfTf2EovrQoTKjkRnPUOUnXVJ2cQ4GbC/bQw98oxN+DdSf+HxOBK00YOhsP52xWdJPV1o4n5w==", + "dev": true, + "license": "MIT", + "dependencies": { + "kleur": "^4.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@playwright/test": { + "version": "1.62.1", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.62.1.tgz", + "integrity": "sha512-DTcUc8qii+cpHvtOwggMtBRMjKZHXYWdw8syRYu2vtzuq4Wxphqq4NfCs5Zt44L6mA8rfDfj+PHnxFc/FeK6mQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright": "1.62.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@sideway/address": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/@sideway/address/-/address-4.1.5.tgz", + "integrity": "sha512-IqO/DUQHUkPeixNQ8n0JA6102hT9CmaljNTPmQ1u8MEhBo/R4Q8eKLN/vGZxuebwOroDB4cbpjheD4+/sKFK4Q==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@hapi/hoek": "^9.0.0" + } + }, + "node_modules/@sideway/formula": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sideway/formula/-/formula-3.0.1.tgz", + "integrity": "sha512-/poHZJJVjx3L+zVD6g9KgHfYnb443oi7wLu/XKojDviHy6HOEOA6z1Trk5aR1dGcmPenJEgb2sK2I80LeS3MIg==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@sideway/pinpoint": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@sideway/pinpoint/-/pinpoint-2.0.0.tgz", + "integrity": "sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@sidvind/better-ajv-errors": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sidvind/better-ajv-errors/-/better-ajv-errors-3.0.1.tgz", + "integrity": "sha512-++1mEYIeozfnwWI9P1ECvOPoacy+CgDASrmGvXPMCcqgx0YUzB01vZ78uHdQ443V6sTY+e9MzHqmN9DOls02aw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "kleur": "^4.1.0" + }, + "engines": { + "node": ">= 16.14" + }, + "peerDependencies": { + "ajv": "^6.12.3 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/ajv": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", + "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-regex": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.3.0.tgz", + "integrity": "sha512-WpDfL7NO6j7tH88IDBNVdUJxDh9nmCteAVW9dsep846XdwF4naCBK+/tGLX3KJgcpgMRXCFlTM2hKGoK9FsdrQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/async": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", + "dev": true, + "license": "MIT" + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/axios": { + "version": "1.19.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.19.0.tgz", + "integrity": "sha512-ht/iuYZXEjFxLH/Hkezgd7m6JKlHHXEUSneaDz8uZe1Gj5QZtCnpyDsckvAiEnT89OEbCLmnte4R4sn7P0EKFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.16.0", + "form-data": "^4.0.6", + "https-proxy-agent": "^5.0.1", + "proxy-from-env": "^2.1.0" + } + }, + "node_modules/axios/node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/axios/node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/basic-auth": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.1.tgz", + "integrity": "sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "5.1.2" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/brace-expansion": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.4.tgz", + "integrity": "sha512-hGfVzPxthbf3+2yjg/RBs60cB0FhqBS/zvdV/4wn4/BmN0bNMMHPc4V/BbFieqf1TKAGGAHnY4eSjajCl0f2Xg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dev": true, + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/corser": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/corser/-/corser-2.0.1.tgz", + "integrity": "sha512-utCYNzRSQIZNPIcGZdQc92UVJYAhtGAteCFg0yRaFm8f0P+CPtyGyHXJcGXnffjCybUCEx3FQ2G7U3/o9eIkVQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/dom-serializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "dev": true, + "license": "MIT", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/dom-serializer/node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "BSD-2-Clause" + }, + "node_modules/domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "domelementtype": "^2.3.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/domutils": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz", + "integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true, + "license": "MIT" + }, + "node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true, + "license": "MIT" + }, + "node_modules/entities": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz", + "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", + "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "dev": true, + "license": "MIT" + }, + "node_modules/eventemitter3": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", + "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==", + "dev": true, + "license": "MIT" + }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-uri": { + "version": "3.1.6", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.6.tgz", + "integrity": "sha512-7Ical1vFEMr0onbVzEDIreM22I4khW+fzyQPwvAFWBp1iwdshSZRsL4jjRvPG9JP1uiqMHRto+YU6R2/CzDz5Q==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/follow-redirects": { + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz", + "integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "dev": true, + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/form-data": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.6.tgz", + "integrity": "sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.4", + "mime-types": "^2.1.35" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gaxios": { + "version": "6.7.1", + "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-6.7.1.tgz", + "integrity": "sha512-LDODD4TMYx7XXdpwxAVRAIAuB0bzv0s+ywFonY46k126qzQHT9ygyoa9tncmOiQmmDrik65UYsEkv3lbfqQ3yQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "extend": "^3.0.2", + "https-proxy-agent": "^7.0.1", + "is-stream": "^2.0.0", + "node-fetch": "^2.6.9", + "uuid": "^9.0.1" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/glob": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/he": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", + "dev": true, + "license": "MIT", + "bin": { + "he": "bin/he" + } + }, + "node_modules/html-encoding-sniffer": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-3.0.0.tgz", + "integrity": "sha512-oWv4T4yJ52iKrufjnyZPkrN0CH3QnrUqdB6In1g5Fe1mia8GmF36gnfNySxoZtxD5+NmYw1EElVXiBk93UeskA==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-encoding": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/html-validate": { + "version": "8.29.0", + "resolved": "https://registry.npmjs.org/html-validate/-/html-validate-8.29.0.tgz", + "integrity": "sha512-RFfFIWaUB9SN8iETL2GoPvjWEX1gcbz0+m+vao7xkPl0cnlgMDu9RcjdQz6T3n6LgT/LENPkvxHzVkqY/Qs3Tg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/html-validate" + } + ], + "license": "MIT", + "dependencies": { + "@html-validate/stylish": "^4.1.0", + "@sidvind/better-ajv-errors": "3.0.1", + "ajv": "^8.0.0", + "glob": "^10.0.0", + "kleur": "^4.1.0", + "minimist": "^1.2.0", + "prompts": "^2.0.0", + "semver": "^7.0.0" + }, + "bin": { + "html-validate": "bin/html-validate.js" + }, + "engines": { + "node": ">= 16.14" + }, + "peerDependencies": { + "jest": "^27.1 || ^28.1.3 || ^29.0.3", + "jest-diff": "^27.1 || ^28.1.3 || ^29.0.3", + "jest-snapshot": "^27.1 || ^28.1.3 || ^29.0.3", + "vitest": "^0.34.0 || ^1.0.0 || ^2.0.0" + }, + "peerDependenciesMeta": { + "jest": { + "optional": true + }, + "jest-diff": { + "optional": true + }, + "jest-snapshot": { + "optional": true + }, + "vitest": { + "optional": true + } + } + }, + "node_modules/htmlparser2": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-10.1.0.tgz", + "integrity": "sha512-VTZkM9GWRAtEpveh7MSF6SjjrpNVNNVJfFup7xTY3UpFtm67foy9HDVXneLtFVt4pMz5kZtgNcvCniNFb1hlEQ==", + "dev": true, + "funding": [ + "https://github.com/fb55/htmlparser2?sponsor=1", + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "MIT", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.2.2", + "entities": "^7.0.1" + } + }, + "node_modules/http-proxy": { + "version": "1.18.1", + "resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.1.tgz", + "integrity": "sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "eventemitter3": "^4.0.0", + "follow-redirects": "^1.0.0", + "requires-port": "^1.0.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/http-server": { + "version": "14.1.1", + "resolved": "https://registry.npmjs.org/http-server/-/http-server-14.1.1.tgz", + "integrity": "sha512-+cbxadF40UXd9T01zUHgA+rlo2Bg1Srer4+B4NwIHdaGxAGGv59nYRnGGDJ9LBk7alpS0US+J+bLLdQOOkJq4A==", + "dev": true, + "license": "MIT", + "dependencies": { + "basic-auth": "^2.0.1", + "chalk": "^4.1.2", + "corser": "^2.0.1", + "he": "^1.2.0", + "html-encoding-sniffer": "^3.0.0", + "http-proxy": "^1.18.1", + "mime": "^1.6.0", + "minimist": "^1.2.6", + "opener": "^1.5.1", + "portfinder": "^1.0.28", + "secure-compare": "3.0.1", + "union": "~0.5.0", + "url-join": "^4.0.1" + }, + "bin": { + "http-server": "bin/http-server" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/joi": { + "version": "17.13.6", + "resolved": "https://registry.npmjs.org/joi/-/joi-17.13.6.tgz", + "integrity": "sha512-ImNZaq/LSysofih+xIGYfR0WUXMA9GLUNB//YTCSrZptoRmVgaNAdJyi6K1kXi9pkLEoSkoI8I4UwtNiu/D7nw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@hapi/hoek": "^9.3.0", + "@hapi/topo": "^5.1.0", + "@sideway/address": "^4.1.5", + "@sideway/formula": "^3.0.1", + "@sideway/pinpoint": "^2.0.0" + } + }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true, + "license": "MIT" + }, + "node_modules/kleur": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz", + "integrity": "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/linkinator": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/linkinator/-/linkinator-6.3.0.tgz", + "integrity": "sha512-MRKxkkIK5XlK+IKzIhJydJBF72TpygT7atR9CCUZrKl9hpEPVkm3Kcu66M38HJUvUBg4iskzCQENwas7HIiJeg==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^5.0.0", + "escape-html": "^1.0.3", + "gaxios": "^6.0.0", + "glob": "^10.3.10", + "htmlparser2": "^10.0.0", + "marked": "^13.0.0", + "meow": "^13.0.0", + "mime": "^4.0.0", + "server-destroy": "^1.0.1", + "srcset": "^5.0.0" + }, + "bin": { + "linkinator": "build/src/cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/linkinator/node_modules/chalk": { + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", + "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/linkinator/node_modules/mime": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-4.1.0.tgz", + "integrity": "sha512-X5ju04+cAzsojXKes0B/S4tcYtFAJ6tTMuSPBEn9CPGlrWr8Fiw7qYeLT0XyH80HSoAoqWCaz+MWKh22P7G1cw==", + "dev": true, + "funding": [ + "https://github.com/sponsors/broofa" + ], + "license": "MIT", + "bin": { + "mime": "bin/cli.js" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/lodash": { + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", + "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/marked": { + "version": "13.0.3", + "resolved": "https://registry.npmjs.org/marked/-/marked-13.0.3.tgz", + "integrity": "sha512-rqRix3/TWzE9rIoFGIn8JmsVfhiuC8VIQ8IdX5TfzmeBucdY05/0UlzKaw0eVtpcN/OdVFpBk7CjKGo9iHJ/zA==", + "dev": true, + "license": "MIT", + "bin": { + "marked": "bin/marked.js" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/meow": { + "version": "13.2.0", + "resolved": "https://registry.npmjs.org/meow/-/meow-13.2.0.tgz", + "integrity": "sha512-pxQJQzB6djGPXh08dacEloMFopsOqGVRKFPYvPOt9XDZ1HasbgDZA74CJGreSU4G3Ak7EFJGoiH2auq+yXISgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "dev": true, + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/minimatch": { + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.2" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/minipass": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/opener": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/opener/-/opener-1.5.2.tgz", + "integrity": "sha512-ur5UIdyw5Y7yEj9wLzhqXiy6GZ3Mwx0yGI+5sMn2r0N0v3cKJvUmFH5yPP+WXh9e0xfyzyJX95D8l088DNFj7A==", + "dev": true, + "license": "(WTFPL OR MIT)", + "bin": { + "opener": "bin/opener-bin.js" + } + }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "dev": true, + "license": "BlueOak-1.0.0" + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/playwright": { + "version": "1.62.1", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.62.1.tgz", + "integrity": "sha512-0M+L3LAD8/nm554LOla9Ayx0j0tmFZ0FBcoQ7F1VuVHpM/XpiC8RcDzBQB8W5+hA8L22THxELzeF+2WcUzvcLg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.62.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=20" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.62.1", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.62.1.tgz", + "integrity": "sha512-wPYSwEBJY9GHraISXqyqtx0na0LpO3XEX7jNDhntbex7tzUS7kLnZsOlFruFJB4Hi/rhDMjXGqHewDZ68nYZVw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/portfinder": { + "version": "1.0.38", + "resolved": "https://registry.npmjs.org/portfinder/-/portfinder-1.0.38.tgz", + "integrity": "sha512-rEwq/ZHlJIKw++XtLAO8PPuOQA/zaPJOZJ37BVuN97nLpMJeuDVLVGRwbFoBgLudgdTMP2hdRJP++H+8QOA3vg==", + "dev": true, + "license": "MIT", + "dependencies": { + "async": "^3.2.6", + "debug": "^4.3.6" + }, + "engines": { + "node": ">= 10.12" + } + }, + "node_modules/prompts": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", + "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "kleur": "^3.0.3", + "sisteransi": "^1.0.5" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/prompts/node_modules/kleur": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", + "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/proxy-from-env": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz", + "integrity": "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/qs": { + "version": "6.15.3", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.3.tgz", + "integrity": "sha512-O9gl3zCl5h5blw1KGUzQKhA5oUXSl8rwUIM5o0S3nCXMliSvy5Dzx7/DJcI+SwgICv+IneSZwhBh1oSyEHA71A==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "es-define-property": "^1.0.1", + "side-channel": "^1.1.1" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/requires-port": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", + "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/rxjs": { + "version": "7.8.2", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", + "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true, + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true, + "license": "MIT" + }, + "node_modules/secure-compare": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/secure-compare/-/secure-compare-3.0.1.tgz", + "integrity": "sha512-AckIIV90rPDcBcglUwXPF3kg0P0qmPsPXAj6BBEENQE1p5yA1xfmDJzfi1Tappj37Pv2mVbKpL3Z1T+Nn7k1Qw==", + "dev": true, + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/server-destroy": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/server-destroy/-/server-destroy-1.0.1.tgz", + "integrity": "sha512-rb+9B5YBIEzYcD6x2VKidaa+cqYBJQKnU4oe4E3ANwRRN56yk/ua1YCJT1n21NTS8w6CcOclAKNP3PhdCXKYtQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/side-channel": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.1.tgz", + "integrity": "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4", + "side-channel-list": "^1.0.1", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", + "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/sisteransi": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", + "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", + "dev": true, + "license": "MIT" + }, + "node_modules/srcset": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/srcset/-/srcset-5.0.3.tgz", + "integrity": "sha512-AZswtOXIsu0LeHdo6YY7d0r2pCH2Rl1D8ae1utvXUX4GxG3RggsVUAOFX1r8RI4YHFMYb4g89+UBPBv3mNUU2g==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/string-width-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.2.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "dev": true, + "license": "MIT" + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD" + }, + "node_modules/union": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/union/-/union-0.5.0.tgz", + "integrity": "sha512-N6uOhuW6zO95P3Mel2I2zMsbsanvvtgn6jVqJv4vbVcz/JN0OkL9suomjQGmWtxJQXOCqUJvquc1sMeNz/IwlA==", + "dev": true, + "dependencies": { + "qs": "^6.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/url-join": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/url-join/-/url-join-4.0.1.tgz", + "integrity": "sha512-jk1+QP6ZJqyOiuEI9AEWQfju/nB2Pw466kbA0LEZljHwKeMgd9WrAEgEGxjPDD2+TNbbb37rTyhEfrCXfuKXnA==", + "dev": true, + "license": "MIT" + }, + "node_modules/uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "deprecated": "uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028).", + "dev": true, + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/wait-on": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/wait-on/-/wait-on-7.2.0.tgz", + "integrity": "sha512-wCQcHkRazgjG5XoAq9jbTMLpNIjoSlZslrJ2+N9MxDsGEv1HnFoVjOCexL0ESva7Y9cu350j+DWADdk54s4AFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "axios": "^1.6.1", + "joi": "^17.11.0", + "lodash": "^4.17.21", + "minimist": "^1.2.8", + "rxjs": "^7.8.1" + }, + "bin": { + "wait-on": "bin/wait-on" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/whatwg-encoding": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-2.0.0.tgz", + "integrity": "sha512-p41ogyeMUrw3jWclHWTQg1k05DSVXPLcVxRTYsXUk+ZooOCZLcoYgPZ/HL/D/N+uQPOtcp1me1WhBEaX02mhWg==", + "deprecated": "Use @exodus/bytes instead for a more spec-conformant and faster implementation", + "dev": true, + "license": "MIT", + "dependencies": { + "iconv-lite": "0.6.3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/wrap-ansi-cjs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + } + } +} diff --git a/scripts/build-deploy.py b/scripts/build-deploy.py new file mode 100644 index 0000000..5c93964 --- /dev/null +++ b/scripts/build-deploy.py @@ -0,0 +1,84 @@ +#!/usr/bin/env python3 +"""scripts/build-deploy.py — Minify HTML/CSS/JS for deploy branch.""" +import re, os, shutil +from pathlib import Path + +ROOT = Path(__file__).parent.parent +OUT = ROOT / "dist" + +# Everything below writes to OUT, never to ROOT. +# +# The original operated in place: it minified the tracked sources over +# themselves and then shutil.rmtree'd tests/, test-screenshots/ and .github/ +# out of the working tree. Running it once on a checkout destroyed the CI +# configuration and the test suite, and left every source file minified. It is +# named "build for the deploy branch" but it never made a branch -- it mutated +# wherever it happened to be run. +# +# A build step must not be able to damage the thing it is building from. +SOURCE_ONLY = ("tests", "test-screenshots", ".github", "scripts", "dist", ".git") + +def minify_css(text): + text = re.sub(r'/\*.*?\*/', '', text, flags=re.DOTALL) + text = re.sub(r'\s+', ' ', text) + text = re.sub(r'\s*([{};:,>~+])\s*', r'\1', text) + text = re.sub(r';\}', '}', text) + return text.strip() + +def minify_js(text): + text = re.sub(r'//[^\n]*', '', text) + text = re.sub(r'/\*.*?\*/', '', text, flags=re.DOTALL) + text = re.sub(r'\n\s*\n', '\n', text) + text = re.sub(r'[ \t]+', ' ', text) + return text.strip() + +def minify_html(text): + text = re.sub(r'', '', text, flags=re.DOTALL) + text = re.sub(r'[ \t]+', ' ', text) + text = re.sub(r'\n\s*\n', '\n', text) + return text.strip() + +total_saved = 0 +files_minified = 0 + + +def stage_tree(): + """Copy the publishable tree into OUT, leaving the checkout alone.""" + if OUT.exists(): + shutil.rmtree(OUT) + def ignore(directory, names): + # Only prune at the top level; a nested directory called "scripts" + # inside the site is content and must survive. + if Path(directory).resolve() != ROOT.resolve(): + return {"node_modules"} + return set(SOURCE_ONLY) | {"node_modules"} + shutil.copytree(ROOT, OUT, ignore=ignore) + + +stage_tree() + +for css_file in OUT.glob('**/*.css'): + if 'node_modules' in str(css_file): continue + orig = css_file.read_text(encoding='utf-8') + mini = minify_css(orig) + css_file.write_text(mini, encoding='utf-8') + total_saved += len(orig) - len(mini); files_minified += 1 + print(f' CSS {css_file.name}: {len(orig):,} -> {len(mini):,} bytes') + +for js_file in OUT.glob('**/*.js'): + if 'node_modules' in str(js_file): continue + orig = js_file.read_text(encoding='utf-8') + mini = minify_js(orig) + js_file.write_text(mini, encoding='utf-8') + total_saved += len(orig) - len(mini); files_minified += 1 + print(f' JS {js_file.name}: {len(orig):,} -> {len(mini):,} bytes') + +for html_file in OUT.glob('**/*.html'): + if 'node_modules' in str(html_file) or 'test-screenshots' in str(html_file): continue + orig = html_file.read_text(encoding='utf-8') + mini = minify_html(orig) + html_file.write_text(mini, encoding='utf-8') + total_saved += len(orig) - len(mini); files_minified += 1 + +print(f'\n✓ Minified {files_minified} files, saved {total_saved:,} bytes') +print(f'✓ Deploy tree written to {OUT.relative_to(ROOT)}/ (source tree untouched)') diff --git a/style.css.bak b/style.css.bak deleted file mode 100644 index d9bf72b..0000000 --- a/style.css.bak +++ /dev/null @@ -1,986 +0,0 @@ -:root { - --bg-primary: #0d1117; - --bg-secondary: #161b22; - --bg-tertiary: #1c2128; - --bg-card: #21262d; - --bg-hover: #292e36; - --text-primary: #e6edf3; - --text-secondary: #8b949e; - --text-muted: #6e7681; - --border-color: #30363d; - --border-light: #21262d; - --blue: #58a6ff; - --green: #3fb950; - --purple: #bc8cff; - --orange: #f0883e; - --cyan: #79c0ff; - --pink: #f778ba; - --yellow: #e3b341; - --red: #f85149; - --blue-dim: rgba(88,166,255,0.15); - --green-dim: rgba(63,185,80,0.15); - --purple-dim: rgba(188,140,255,0.15); - --orange-dim: rgba(240,136,62,0.15); - --cyan-dim: rgba(121,192,255,0.15); - --pink-dim: rgba(247,120,186,0.15); - --yellow-dim: rgba(227,179,65,0.15); - --red-dim: rgba(248,81,73,0.15); - --gradient-hero: linear-gradient(135deg, #0d1117 0%, #0f1a35 25%, #1a1040 55%, #0d1117 100%); - --shadow-sm: 0 1px 3px rgba(0,0,0,0.3); - --shadow-md: 0 4px 12px rgba(0,0,0,0.4); - --shadow-lg: 0 8px 24px rgba(0,0,0,0.5); - --radius-sm: 6px; - --radius-md: 10px; - --radius-lg: 16px; - --radius-xl: 24px; - --font-sans: 'Inter', 'Segoe UI', -apple-system, BlinkMacSystemFont, sans-serif; - --font-mono: 'Fira Code', 'Cascadia Code', Consolas, monospace; - --sidebar-width: 260px; - --nav-height: 64px; -} -*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; } -html { scroll-behavior: smooth; font-size: 16px; overflow-x: hidden; } -body { font-family: var(--font-sans); background: var(--bg-primary); color: var(--text-primary); line-height: 1.7; overflow-x: hidden; -webkit-font-smoothing: antialiased; } -a { color: var(--cyan); text-decoration: none; transition: color 0.2s; } -a:hover { color: var(--blue); text-decoration: underline; } -img { max-width: 100%; } -ul, ol { padding-left: 1.5em; } -h1, h2, h3, h4, h5, h6 { line-height: 1.3; } -h2 { font-size: 1.6rem; margin: 2.5rem 0 1rem; padding-bottom: 0.5rem; border-bottom: 1px solid var(--border-color); } -h3 { font-size: 1.25rem; margin: 2rem 0 0.75rem; } - -.navbar { position: sticky; top: 0; z-index: 1000; background: rgba(13,17,23,0.85); backdrop-filter: blur(12px); -webkit-backdrop-filter: blur(12px); border-bottom: 1px solid var(--border-color); height: var(--nav-height); display: flex; align-items: center; padding: 0 2rem; } -.navbar .nav-inner { max-width: 1400px; width: 100%; margin: 0 auto; display: flex; align-items: center; justify-content: space-between; } -.navbar .logo { display: flex; align-items: center; gap: 10px; font-size: 1.25rem; font-weight: 700; color: var(--text-primary); text-decoration: none; } -.navbar .logo:hover { text-decoration: none; color: var(--text-primary); } -.navbar .logo-icon { width: 36px; height: 36px; background: linear-gradient(135deg, #1158a6, #58a6ff); border-radius: var(--radius-sm); display: flex; align-items: center; justify-content: center; font-weight: 900; font-size: 0.9rem; color: #fff; } -.navbar .nav-links { display: flex; align-items: center; gap: 0.25rem; list-style: none; padding: 0; } -.navbar .nav-links a { padding: 0.5rem 1rem; border-radius: var(--radius-sm); color: var(--text-secondary); font-size: 0.9rem; font-weight: 500; transition: all 0.2s; } -.navbar .nav-links a:hover { color: var(--text-primary); background: var(--bg-tertiary); text-decoration: none; } -.navbar .nav-links a.active { color: var(--blue); background: var(--blue-dim); } -.nav-version { font-size: 0.7rem; padding: 2px 8px; background: var(--green-dim); color: var(--green); border-radius: 20px; font-weight: 600; } -.nav-github { display: inline-flex; align-items: center; gap: 6px; padding: 0.4rem 1rem; background: var(--bg-tertiary); border: 1px solid var(--border-color); border-radius: var(--radius-sm); color: var(--text-secondary); font-size: 0.85rem; transition: all 0.2s; } -.nav-github:hover { background: var(--bg-hover); color: var(--text-primary); border-color: var(--text-muted); text-decoration: none; } -.hero { background: var(--gradient-hero); padding: 6rem 2rem 5rem; text-align: center; position: relative; overflow: hidden; } -.hero::before { content: ''; position: absolute; top: -50%; left: -50%; width: 200%; height: 200%; background: radial-gradient(circle at 30% 50%, rgba(88,166,255,0.10) 0%, transparent 50%), radial-gradient(circle at 70% 50%, rgba(188,140,255,0.10) 0%, transparent 50%), radial-gradient(circle at 50% 80%, rgba(63,185,80,0.05) 0%, transparent 40%); animation: heroGlow 8s ease-in-out infinite alternate; } -@keyframes heroGlow { 0% { transform: translate(0, 0); } 100% { transform: translate(-3%, 3%); } } -.hero-content { position: relative; z-index: 1; max-width: 900px; margin: 0 auto; } -.hero h1 { font-size: 3.5rem; font-weight: 800; line-height: 1.2; margin-bottom: 1rem; background: linear-gradient(135deg, var(--blue), var(--purple), var(--pink)); -webkit-background-clip: text; -webkit-text-fill-color: transparent; background-clip: text; } -.hero .subtitle { font-size: 1.3rem; color: var(--text-secondary); margin-bottom: 2rem; max-width: 600px; margin-left: auto; margin-right: auto; } -.hero-buttons { display: flex; gap: 1rem; justify-content: center; flex-wrap: wrap; } -.hero-stats { display: flex; gap: 3rem; justify-content: center; margin-top: 3rem; flex-wrap: wrap; } -.stat { text-align: center; } -.stat-number { font-size: 2.5rem; font-weight: 800; display: block; } -.stat-label { font-size: 0.85rem; color: var(--text-secondary); text-transform: uppercase; letter-spacing: 1px; } - -.btn { display: inline-flex; align-items: center; gap: 8px; padding: 0.7rem 1.5rem; border-radius: var(--radius-sm); font-weight: 600; font-size: 0.95rem; cursor: pointer; transition: all 0.25s ease; border: none; text-decoration: none !important; } -.btn-primary { background: var(--blue); color: #fff !important; } -.btn-primary:hover { background: #79b8ff; box-shadow: 0 0 24px rgba(88,166,255,0.4); transform: translateY(-2px); } -.btn:active { transform: scale(0.97) !important; transition: transform 0.12s ease-out !important; } -.btn-secondary { background: var(--bg-tertiary); color: var(--text-primary) !important; border: 1px solid var(--border-color); } -.btn-secondary:hover { background: var(--bg-hover); border-color: var(--text-muted); transform: translateY(-2px); } -.btn-green { background: var(--green); color: #fff !important; } -.btn-green:hover { background: #56d364; box-shadow: 0 0 20px rgba(63,185,80,0.3); transform: translateY(-2px); } -.btn-purple { background: var(--purple); color: #fff !important; } -.btn-purple:hover { box-shadow: 0 0 20px rgba(188,140,255,0.3); transform: translateY(-2px); } -.btn-sm { padding: 0.4rem 0.9rem; font-size: 0.8rem; } -.btn-outline { background: transparent; border: 1px solid var(--border-color); color: var(--text-secondary) !important; } -.btn-outline:hover { border-color: var(--blue); color: var(--blue) !important; background: var(--blue-dim); } - -.product-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 1.5rem; max-width: 1400px; margin: 0 auto; padding: 0 2rem; } -.product-card { background: var(--bg-card); border: 1px solid var(--border-color); border-radius: var(--radius-md); padding: 1.5rem; transition: all 0.3s ease; position: relative; overflow: hidden; } -.product-card::before { content: ''; position: absolute; top: 0; left: 0; right: 0; height: 3px; } -.product-card:hover { transform: translateY(-5px); box-shadow: 0 12px 32px rgba(0,0,0,0.6); border-color: var(--text-muted); } -.card-icon { width: 48px; height: 48px; border-radius: var(--radius-sm); display: flex; align-items: center; justify-content: center; font-weight: 800; font-size: 1.1rem; margin-bottom: 1rem; } -.product-card h3 { font-size: 1.15rem; font-weight: 700; margin-bottom: 0.5rem; border: none; padding: 0; margin-top: 0; } -.product-card p { color: var(--text-secondary); font-size: 0.9rem; margin-bottom: 1rem; line-height: 1.5; } -.card-tags { display: flex; flex-wrap: wrap; gap: 0.4rem; margin-bottom: 1rem; } -.card-buttons { display: flex; gap: 0.5rem; } - -.card-eos::before { background: var(--blue); } -.card-eos .card-icon { background: var(--blue-dim); color: var(--blue); } -.card-eos:hover { border-color: var(--blue); box-shadow: 0 8px 24px rgba(88,166,255,0.15); } -.card-eboot::before { background: var(--green); } -.card-eboot .card-icon { background: var(--green-dim); color: var(--green); } -.card-eboot:hover { border-color: var(--green); box-shadow: 0 8px 24px rgba(63,185,80,0.15); } -.card-ebuild::before { background: var(--orange); } -.card-ebuild .card-icon { background: var(--orange-dim); color: var(--orange); } -.card-ebuild:hover { border-color: var(--orange); box-shadow: 0 8px 24px rgba(240,136,62,0.15); } -.card-eipc::before { background: var(--cyan); } -.card-eipc .card-icon { background: var(--cyan-dim); color: var(--cyan); } -.card-eipc:hover { border-color: var(--cyan); box-shadow: 0 8px 24px rgba(121,192,255,0.15); } -.card-eai::before { background: var(--purple); } -.card-eai .card-icon { background: var(--purple-dim); color: var(--purple); } -.card-eai:hover { border-color: var(--purple); box-shadow: 0 8px 24px rgba(188,140,255,0.15); } -.card-eni::before { background: var(--pink); } -.card-eni .card-icon { background: var(--pink-dim); color: var(--pink); } -.card-eni:hover { border-color: var(--pink); box-shadow: 0 8px 24px rgba(247,120,186,0.15); } -.card-eosuite::before { background: var(--yellow); } -.card-eosuite .card-icon { background: var(--yellow-dim); color: var(--yellow); } -.card-eosuite:hover { border-color: var(--yellow); box-shadow: 0 8px 24px rgba(227,179,65,0.15); } -.card-eosim::before { background: var(--red); } -.card-eosim .card-icon { background: var(--red-dim); color: var(--red); } -.card-eosim:hover { border-color: var(--red); box-shadow: 0 8px 24px rgba(248,81,73,0.15); } -.badge { display: inline-flex; align-items: center; padding: 2px 10px; border-radius: 20px; font-size: 0.72rem; font-weight: 600; letter-spacing: 0.3px; text-transform: uppercase; } -.badge-blue { background: var(--blue-dim); color: var(--blue); } -.badge-green { background: var(--green-dim); color: var(--green); } -.badge-purple { background: var(--purple-dim); color: var(--purple); } -.badge-orange { background: var(--orange-dim); color: var(--orange); } -.badge-cyan { background: var(--cyan-dim); color: var(--cyan); } -.badge-pink { background: var(--pink-dim); color: var(--pink); } -.badge-yellow { background: var(--yellow-dim); color: var(--yellow); } -.badge-red { background: var(--red-dim); color: var(--red); } -.version-badge { display: inline-flex; align-items: center; gap: 4px; padding: 3px 12px; border-radius: 20px; font-size: 0.8rem; font-weight: 600; background: var(--green-dim); color: var(--green); } - -.section { padding: 5rem 2rem; max-width: 1400px; margin: 0 auto; } -.section-full { padding: 5rem 2rem; } -.section-alt { background: var(--bg-secondary); } -.section-title { font-size: 2rem; font-weight: 800; margin-bottom: 0.5rem; text-align: center; border: none; } -.section-subtitle { color: var(--text-secondary); text-align: center; margin-bottom: 3rem; font-size: 1.1rem; } -.section-divider { height: 3px; border-radius: 2px; margin: 0 auto 3rem; max-width: 80px; border: none; } - -.doc-layout { display: grid; grid-template-columns: var(--sidebar-width) 1fr; max-width: 1400px; margin: 0 auto; min-height: calc(100vh - var(--nav-height)); overflow: hidden; } -.doc-sidebar { position: sticky; top: var(--nav-height); height: calc(100vh - var(--nav-height)); overflow-y: auto; padding: 1.5rem; border-right: 1px solid var(--border-color); background: var(--bg-secondary); scrollbar-width: thin; scrollbar-color: var(--border-color) transparent; z-index: 1; } -.doc-sidebar h4 { font-size: 0.75rem; text-transform: uppercase; letter-spacing: 1.5px; color: var(--text-muted); margin-bottom: 0.75rem; padding-left: 0.5rem; } -.doc-sidebar ul { list-style: none; padding: 0; margin-bottom: 1.5rem; } -.doc-sidebar li a { display: block; padding: 0.35rem 0.75rem; color: var(--text-secondary); font-size: 0.85rem; border-radius: var(--radius-sm); border-left: 2px solid transparent; transition: all 0.2s; } -.doc-sidebar li a:hover { color: var(--text-primary); background: var(--bg-tertiary); text-decoration: none; } -.doc-sidebar li a.active { color: var(--blue); background: var(--blue-dim); border-left-color: var(--blue); } -.sidebar-green li a.active { color: var(--green) !important; background: var(--green-dim) !important; border-left-color: var(--green) !important; } -.sidebar-orange li a.active { color: var(--orange) !important; background: var(--orange-dim) !important; border-left-color: var(--orange) !important; } -.sidebar-cyan li a.active { color: var(--cyan) !important; background: var(--cyan-dim) !important; border-left-color: var(--cyan) !important; } -.sidebar-purple li a.active { color: var(--purple) !important; background: var(--purple-dim) !important; border-left-color: var(--purple) !important; } -.sidebar-pink li a.active { color: var(--pink) !important; background: var(--pink-dim) !important; border-left-color: var(--pink) !important; } -.sidebar-yellow li a.active { color: var(--yellow) !important; background: var(--yellow-dim) !important; border-left-color: var(--yellow) !important; } -.sidebar-red li a.active { color: var(--red) !important; background: var(--red-dim) !important; border-left-color: var(--red) !important; } -.doc-content { padding: 2rem 3rem; max-width: 960px; position: relative; z-index: 0; } -.doc-content p { margin-bottom: 1rem; } - -.breadcrumb { display: flex; align-items: center; gap: 0.5rem; font-size: 0.85rem; color: var(--text-muted); margin-bottom: 1.5rem; flex-wrap: wrap; } -.breadcrumb a { color: var(--text-secondary); } -.breadcrumb a:hover { color: var(--blue); } -.breadcrumb .sep { color: var(--text-muted); } - -.doc-hero { padding: 2rem; border-radius: var(--radius-md); margin-bottom: 2rem; border: 1px solid var(--border-color); } -.doc-hero h1 { font-size: 2.2rem; font-weight: 800; margin-bottom: 0.5rem; border: none; } -.doc-hero p { color: var(--text-secondary); font-size: 1.05rem; margin-bottom: 0; } -.doc-hero-meta { display: flex; align-items: center; gap: 1rem; margin-top: 1rem; flex-wrap: wrap; } -.doc-hero-blue { background: linear-gradient(135deg, rgba(88,166,255,0.1), rgba(88,166,255,0.02)); border-color: rgba(88,166,255,0.3); } -.doc-hero-green { background: linear-gradient(135deg, rgba(63,185,80,0.1), rgba(63,185,80,0.02)); border-color: rgba(63,185,80,0.3); } -.doc-hero-orange { background: linear-gradient(135deg, rgba(240,136,62,0.1), rgba(240,136,62,0.02)); border-color: rgba(240,136,62,0.3); } -.doc-hero-cyan { background: linear-gradient(135deg, rgba(121,192,255,0.1), rgba(121,192,255,0.02)); border-color: rgba(121,192,255,0.3); } -.doc-hero-purple { background: linear-gradient(135deg, rgba(188,140,255,0.1), rgba(188,140,255,0.02)); border-color: rgba(188,140,255,0.3); } -.doc-hero-pink { background: linear-gradient(135deg, rgba(247,120,186,0.1), rgba(247,120,186,0.02)); border-color: rgba(247,120,186,0.3); } -.doc-hero-yellow { background: linear-gradient(135deg, rgba(227,179,65,0.1), rgba(227,179,65,0.02)); border-color: rgba(227,179,65,0.3); } -.doc-hero-red { background: linear-gradient(135deg, rgba(248,81,73,0.1), rgba(248,81,73,0.02)); border-color: rgba(248,81,73,0.3); } -pre { background: var(--bg-secondary); border: 1px solid var(--border-color); border-radius: var(--radius-md); padding: 1.2rem 1.5rem; overflow-x: auto; font-family: var(--font-mono); font-size: 0.85rem; line-height: 1.7; margin: 1rem 0; position: relative; } -code { font-family: var(--font-mono); font-size: 0.85em; } -:not(pre) > code { background: var(--bg-tertiary); padding: 2px 7px; border-radius: 4px; color: var(--cyan); font-size: 0.85em; } -pre .copy-hint { position: absolute; top: 0.5rem; right: 0.75rem; font-size: 0.7rem; color: var(--text-muted); background: var(--bg-tertiary); padding: 2px 8px; border-radius: 4px; font-family: var(--font-sans); opacity: 0; transition: opacity 0.2s; } -pre:hover .copy-hint { opacity: 1; } -.kw { color: var(--purple); font-weight: 600; } -.fn { color: var(--blue); } -.str { color: var(--green); } -.cm { color: var(--text-muted); font-style: italic; } -.ty { color: var(--cyan); } -.num { color: var(--orange); } -.op { color: var(--text-secondary); } -.pp { color: var(--pink); } - -.api-block { background: var(--bg-card); border: 1px solid var(--border-color); border-radius: var(--radius-md); margin: 1rem 0; overflow: hidden; } -.api-header { padding: 0.75rem 1.2rem; background: var(--bg-tertiary); border-bottom: 1px solid var(--border-color); font-family: var(--font-mono); font-size: 0.85rem; } -.api-body { padding: 1rem 1.2rem; color: var(--text-secondary); font-size: 0.9rem; } -.api-body p { margin-bottom: 0.5rem; } -.api-params dt { font-family: var(--font-mono); color: var(--cyan); font-size: 0.82rem; margin-top: 0.4rem; } -.api-params dd { color: var(--text-secondary); font-size: 0.82rem; padding-left: 1rem; margin-bottom: 0.2rem; } - -table { width: 100%; border-collapse: collapse; margin: 1rem 0; font-size: 0.9rem; } -th { background: var(--bg-tertiary); padding: 0.75rem 1rem; text-align: left; font-weight: 600; font-size: 0.82rem; text-transform: uppercase; letter-spacing: 0.5px; border-bottom: 2px solid var(--border-color); } -td { padding: 0.6rem 1rem; border-bottom: 1px solid var(--border-light); } -tr:nth-child(even) { background: rgba(255,255,255,0.02); } -tr:hover { background: rgba(255,255,255,0.04); } -.table-blue th { color: var(--blue); border-bottom-color: var(--blue); } -.table-green th { color: var(--green); border-bottom-color: var(--green); } -.table-orange th { color: var(--orange); border-bottom-color: var(--orange); } -.table-cyan th { color: var(--cyan); border-bottom-color: var(--cyan); } -.table-purple th { color: var(--purple); border-bottom-color: var(--purple); } -.table-pink th { color: var(--pink); border-bottom-color: var(--pink); } -.table-yellow th { color: var(--yellow); border-bottom-color: var(--yellow); } -.table-red th { color: var(--red); border-bottom-color: var(--red); } - -.callout { padding: 1rem 1.25rem; border-radius: var(--radius-sm); margin: 1rem 0; border-left: 4px solid; font-size: 0.9rem; } -.callout-title { font-weight: 700; font-size: 0.85rem; text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 0.3rem; } -.callout-tip { background: var(--green-dim); border-color: var(--green); } -.callout-tip .callout-title { color: var(--green); } -.callout-warning { background: var(--orange-dim); border-color: var(--orange); } -.callout-warning .callout-title { color: var(--orange); } -.callout-info { background: var(--blue-dim); border-color: var(--blue); } -.callout-info .callout-title { color: var(--blue); } -.callout-danger { background: var(--red-dim); border-color: var(--red); } -.callout-danger .callout-title { color: var(--red); } - -.steps { counter-reset: step-counter; padding-left: 0; list-style: none; } -.step { position: relative; padding-left: 3.5rem; margin-bottom: 2rem; counter-increment: step-counter; } -.step::before { content: counter(step-counter); position: absolute; left: 0; top: 0; width: 2.2rem; height: 2.2rem; border-radius: 50%; background: var(--blue); color: #fff; display: flex; align-items: center; justify-content: center; font-weight: 700; font-size: 0.9rem; } -.step h3 { font-size: 1.1rem; margin-bottom: 0.5rem; margin-top: 0; border: none; } -.step-green .step::before { background: var(--green); } -.step-orange .step::before { background: var(--orange); } -.step-purple .step::before { background: var(--purple); } -.step-cyan .step::before { background: var(--cyan); } -.step-pink .step::before { background: var(--pink); } -.step-yellow .step::before { background: var(--yellow); } -.step-red .step::before { background: var(--red); } - -details { background: var(--bg-card); border: 1px solid var(--border-color); border-radius: var(--radius-sm); margin: 0.75rem 0; } -summary { padding: 0.75rem 1rem; cursor: pointer; font-weight: 600; color: var(--text-primary); transition: background 0.2s; list-style: none; } -summary::-webkit-details-marker { display: none; } -summary::before { content: '\25B6'; display: inline-block; margin-right: 0.5rem; font-size: 0.7rem; transition: transform 0.2s; } -details[open] summary::before { transform: rotate(90deg); } -summary:hover { background: var(--bg-hover); } -details[open] summary { border-bottom: 1px solid var(--border-color); } -.details-content { padding: 1rem; } - -.arch-diagram { display: flex; flex-direction: column; gap: 1rem; padding: 2rem; background: var(--bg-secondary); border-radius: var(--radius-lg); border: 1px solid var(--border-color); margin: 2rem 0; } -.arch-row { display: flex; gap: 1rem; justify-content: center; flex-wrap: wrap; } -.arch-box { padding: 0.75rem 1.5rem; border-radius: var(--radius-sm); border: 2px solid; font-weight: 600; font-size: 0.85rem; text-align: center; min-width: 100px; transition: all 0.2s; background: var(--bg-card); } -.arch-box:hover { transform: scale(1.05); } -.arch-arrow { text-align: center; font-size: 1.5rem; color: var(--text-muted); padding: 0.25rem 0; } - -.footer { background: var(--bg-secondary); border-top: 1px solid var(--border-color); padding: 3rem 2rem; margin-top: 4rem; } -.footer-inner { max-width: 1400px; margin: 0 auto; display: grid; grid-template-columns: 2fr 1fr 1fr 1fr 1fr; gap: 2rem; } -.footer h4 { font-size: 0.85rem; text-transform: uppercase; letter-spacing: 1px; color: var(--text-muted); margin-bottom: 1rem; } -.footer ul { list-style: none; padding: 0; } -.footer li { margin-bottom: 0.4rem; } -.footer li a { color: var(--text-secondary); font-size: 0.9rem; } -.footer li a:hover { color: var(--blue); } -.footer-brand p { color: var(--text-secondary); font-size: 0.9rem; margin-bottom: 1rem; } -.footer-bottom { max-width: 1400px; margin: 2rem auto 0; padding-top: 1.5rem; border-top: 1px solid var(--border-color); text-align: center; color: var(--text-muted); font-size: 0.82rem; } - -.search-box { position: relative; max-width: 500px; margin: 0 auto 2rem; } -.search-box input { width: 100%; padding: 0.8rem 1.2rem 0.8rem 3rem; background: var(--bg-card); border: 1px solid var(--border-color); border-radius: var(--radius-lg); color: var(--text-primary); font-size: 0.95rem; outline: none; transition: all 0.2s; font-family: var(--font-sans); } -.search-box input:focus { border-color: var(--blue); box-shadow: 0 0 0 3px var(--blue-dim); } -.search-box input::placeholder { color: var(--text-muted); } -.search-icon { position: absolute; left: 1rem; top: 50%; transform: translateY(-50%); color: var(--text-muted); } - -.checklist { list-style: none; padding: 0; } -.checklist li { padding: 0.5rem 0 0.5rem 2rem; position: relative; font-size: 0.95rem; } -.checklist li::before { content: '\2713'; position: absolute; left: 0; width: 1.4rem; height: 1.4rem; background: var(--green-dim); color: var(--green); border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 0.7rem; font-weight: 700; top: 0.6rem; } - -.quick-links { display: grid; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); gap: 1rem; margin: 1rem 0; } -.quick-link { display: flex; align-items: center; gap: 0.75rem; padding: 1rem; background: var(--bg-card); border: 1px solid var(--border-color); border-radius: var(--radius-sm); transition: all 0.2s; color: var(--text-primary); } -.quick-link:hover { border-color: var(--blue); transform: translateY(-2px); text-decoration: none; } -.ql-icon { width: 36px; height: 36px; border-radius: var(--radius-sm); display: flex; align-items: center; justify-content: center; font-weight: 700; font-size: 0.85rem; flex-shrink: 0; } -.ql-text { font-size: 0.9rem; font-weight: 500; } - -.popular { position: relative; } -.popular::after { content: 'POPULAR'; position: absolute; top: 0.75rem; right: 0.75rem; font-size: 0.6rem; padding: 2px 8px; background: var(--orange-dim); color: var(--orange); border-radius: 10px; font-weight: 700; letter-spacing: 0.5px; } - -.doc-nav { display: grid; grid-template-columns: 1fr 1fr; gap: 1rem; margin-top: 3rem; padding-top: 2rem; border-top: 1px solid var(--border-color); } -.doc-nav a { display: block; padding: 1rem 1.5rem; background: var(--bg-card); border: 1px solid var(--border-color); border-radius: var(--radius-sm); transition: all 0.2s; } -.doc-nav a:hover { border-color: var(--blue); background: var(--blue-dim); text-decoration: none; } -.nav-label { font-size: 0.75rem; color: var(--text-muted); text-transform: uppercase; letter-spacing: 1px; } -.nav-title { font-size: 1rem; font-weight: 600; color: var(--text-primary); } -.doc-nav .next { text-align: right; } - -.edit-link { display: inline-flex; align-items: center; gap: 6px; font-size: 0.82rem; color: var(--text-muted); padding: 4px 10px; border: 1px solid var(--border-light); border-radius: var(--radius-sm); transition: all 0.2s; float: right; } -.edit-link:hover { color: var(--blue); border-color: var(--blue); background: var(--blue-dim); text-decoration: none; } - -.hw-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); gap: 1rem; margin: 1rem 0; } -.hw-item { background: var(--bg-card); border: 1px solid var(--border-color); border-radius: var(--radius-sm); padding: 1rem; transition: all 0.2s; } -.hw-item:hover { border-color: var(--text-muted); transform: translateY(-2px); } -.hw-item h4 { font-size: 0.95rem; margin-bottom: 0.25rem; } -.hw-item p { font-size: 0.8rem; color: var(--text-secondary); margin-bottom: 0; } - -.content-wrapper { max-width: 900px; margin: 0 auto; padding: 2rem; } - -@media (max-width: 1024px) { - .product-grid { grid-template-columns: repeat(2, 1fr); } - .footer-inner { grid-template-columns: repeat(2, 1fr); } - .doc-layout { grid-template-columns: 1fr; } - .doc-sidebar { position: relative; top: 0; height: auto; border-right: none; border-bottom: 1px solid var(--border-color); } - .doc-content { padding: 1.5rem; } -} -@media (max-width: 768px) { - .product-grid { grid-template-columns: 1fr; } - .hero h1 { font-size: 2.2rem; } - .hero-stats { gap: 1.5rem; } - .footer-inner { grid-template-columns: 1fr; } - .doc-nav { grid-template-columns: 1fr; } - .arch-row { flex-direction: column; align-items: center; } - .section { padding: 3rem 1rem; } - .hero { padding: 4rem 1rem 3rem; } - table { display: block; overflow-x: auto; max-width: 100%; } - .api-card { margin: 1rem 0; padding: 1rem; } - .navbar { padding: 0 1rem; } -} -@media print { - .navbar, .doc-sidebar, .edit-link, .doc-nav { display: none; } - body { background: #fff; color: #000; } - .doc-content { max-width: 100%; padding: 0; } - a { color: #000; text-decoration: underline; } - pre { border: 1px solid #ccc; } -} -/* === API Documentation Card Styles === */ -.api-card { background: #161b22; border: 1px solid #30363d; border-radius: 8px; margin: 1.5rem 0; padding: 1.5rem; border-left: 4px solid var(--blue); } -.api-card.accent-green { border-left-color: var(--green); } -.api-card.accent-purple { border-left-color: var(--purple); } -.api-card.accent-orange { border-left-color: var(--orange); } -.api-card.accent-cyan { border-left-color: var(--cyan); } -.api-card.accent-pink { border-left-color: var(--pink); } -.api-signature { font-size: 1.05rem; font-family: var(--font-mono); background: #0d1117; padding: 0.5rem 1rem; border-radius: 4px; display: block; overflow-x: auto; } -.api-signature .ret { color: #3fb950; } -.api-signature .fn { color: #58a6ff; font-weight: bold; } -.api-signature .param { color: #79c0ff; } -.api-desc { color: #e6edf3; margin: 1rem 0; line-height: 1.6; font-size: 0.92rem; } -.api-params table { width: 100%; border-collapse: collapse; } -.api-params td { padding: 0.5rem; border-bottom: 1px solid #21262d; font-size: 0.88rem; } -.api-params td:first-child { font-weight: bold; color: #f0883e; width: 130px; } -.api-params td:nth-child(2) { color: #bc8cff; width: 200px; font-family: var(--font-mono); font-size: 0.82rem; } -.api-returns { margin: 0.5rem 0; font-size: 0.92rem; } -.api-example pre { background: #0d1117; border: 1px solid #30363d; border-radius: 6px; padding: 1rem; overflow-x: auto; font-size: 0.82rem; } -.api-params h5, .api-returns h5, .api-example h5 { color: #8b949e; font-size: 0.8rem; text-transform: uppercase; letter-spacing: 0.05em; margin: 0.75rem 0 0.25rem; } -.version-badge { background: #1f6feb; color: white; padding: 0.2rem 0.6rem; border-radius: 12px; font-size: 0.75rem; font-weight: 600; vertical-align: middle; } -.struct-card { background: #0d1117; border: 1px solid #30363d; border-radius: 8px; padding: 1.2rem; margin: 1rem 0; } -.struct-card h4 { color: #3fb950; font-family: var(--font-mono); margin-bottom: 0.5rem; } -.struct-card table td { padding: 0.35rem 0.5rem; font-size: 0.85rem; } -.module-section { margin: 3rem 0 1rem; padding: 1rem 0; border-top: 2px solid #30363d; } -.module-section h3 { font-size: 1.3rem; margin-bottom: 0.3rem; } -.module-section .module-desc { color: #8b949e; font-size: 0.9rem; } -/* Mobile hamburger menu */ -.nav-toggle { display: none; background: none; border: 1px solid var(--border-color); color: var(--text-primary); font-size: 1.5rem; padding: 0.3rem 0.6rem; border-radius: var(--radius-sm); cursor: pointer; line-height: 1; } -.nav-toggle:hover { background: var(--bg-tertiary); } -@media (max-width: 768px) { - .nav-toggle { display: block; } - .navbar .nav-links { display: none; position: absolute; top: var(--nav-height); left: 0; right: 0; background: var(--bg-secondary); border-bottom: 1px solid var(--border-color); flex-direction: column; padding: 1rem; z-index: 999; box-shadow: var(--shadow-md); } - .navbar .nav-links.open { display: flex; } - .navbar .nav-links a { padding: 0.75rem 1rem; border-radius: var(--radius-sm); width: 100%; } - .navbar .nav-inner { flex-wrap: wrap; gap: 0.5rem; } - .hero h1 { font-size: 2rem; } - .hero .subtitle { font-size: 1rem; } - .product-grid { grid-template-columns: 1fr; } - .footer-inner { grid-template-columns: 1fr; } - .doc-layout { grid-template-columns: 1fr; } - .doc-sidebar { position: relative; top: 0; height: auto; border-right: none; border-bottom: 1px solid var(--border-color); max-height: 250px; overflow-y: auto; } - .doc-content { padding: 1rem; } - .kid-grid, .editor-grid { grid-template-columns: 1fr; } - .board-nav { grid-template-columns: repeat(2, 1fr); } - .path-selector { grid-template-columns: repeat(2, 1fr); } - .quick-links { grid-template-columns: 1fr; } - .hw-grid { grid-template-columns: 1fr; } - .hero-stats { gap: 1rem; } - .hero-buttons { flex-direction: column; align-items: center; } - .arch-row { flex-direction: column; align-items: center; } - .doc-nav { grid-template-columns: 1fr; } - table { font-size: 0.8rem; } - th, td { padding: 0.4rem 0.5rem; } - .svg-icon svg { max-height: 60px; } - .svg-icon-lg svg { max-height: 80px; } - .hero-illustration { flex: 0 0 auto !important; max-width: 200px; } - .hero-content { flex-direction: column !important; text-align: center !important; } - .hero-text { text-align: center !important; } - .hero-text .hero-buttons { justify-content: center; } - .svg-isometric { transform: perspective(600px) rotateX(8deg) rotateY(-8deg); } - .hw-item .board-svg { max-height: 50px; } -} - -/* ═══════════════════════════════════════════════════════════════ - SVG Illustrations — Animations, Utilities & Layout - ═══════════════════════════════════════════════════════════════ */ - -@keyframes svg-float { - 0% { transform: translateY(0px); } - 100% { transform: translateY(-8px); } -} -@keyframes svg-pulse-glow { - 0%, 100% { opacity: 0.5; } - 50% { opacity: 1; } -} -@keyframes svg-spin-slow { - from { transform: rotate(0deg); } - to { transform: rotate(360deg); } -} -@keyframes svg-bob { - 0%, 100% { transform: translateY(0) rotate(0deg); } - 50% { transform: translateY(-4px) rotate(2deg); } -} -@keyframes svg-draw { - to { stroke-dashoffset: 0; } -} - -.svg-icon { - display: flex; - align-items: center; - justify-content: center; - margin-bottom: 0.75rem; -} -.svg-icon svg { - width: 100%; - height: auto; - max-height: 80px; -} -.svg-icon-sm svg { max-height: 48px; } -.svg-icon-lg svg { max-height: 110px; } - -.svg-isometric { - transform: perspective(800px) rotateX(12deg) rotateY(-12deg); - transform-style: preserve-3d; -} -.svg-float { animation: svg-float 3s ease-in-out infinite alternate; } -.svg-bob { animation: svg-bob 3s ease-in-out infinite; } -.svg-glow-hover:hover svg { filter: drop-shadow(0 0 8px currentColor); transition: filter 0.3s; } - -.card-icon svg { width: 32px; height: 32px; } -.hw-item .svg-icon { margin-bottom: 0.5rem; } -.hw-item .svg-icon svg { max-height: 60px; } -.hw-item .board-svg { max-height: 60px; width: 100%; } - -/* Hero side-by-side layout */ -.hero-content { display: flex; align-items: center; gap: 3rem; text-align: left; } -.hero-text { flex: 1; } -.hero-text h1 { text-align: left; } -.hero-text .subtitle { margin-left: 0; } -.hero-illustration { flex: 0 0 300px; } -.hero-illustration svg { - width: 100%; - height: auto; - filter: drop-shadow(0 8px 32px rgba(88,166,255,0.2)); -} - -/* Project card illustrations */ -.project-card .svg-icon { margin-bottom: 0.5rem; } -.project-card .svg-icon svg { max-height: 70px; } - -/* Kid card illustrations */ -.kid-card .svg-icon { margin-bottom: 0.5rem; } -.kid-card .svg-icon svg { max-height: 64px; } - -/* Search button in navbar */ -.nav-search-btn { - background: none; - border: 1px solid var(--border-color); - color: var(--text-secondary); - cursor: pointer; - padding: 4px 8px; - border-radius: var(--radius-sm); - font-size: 1rem; - transition: all 0.2s; - margin-left: 4px; -} -.nav-search-btn:hover { - color: var(--text-primary); - border-color: var(--blue); - background: var(--blue-dim); -} - -/* Search overlay */ -.search-overlay[hidden] { display: none; } -.search-overlay { - position: fixed; - inset: 0; - z-index: 9999; - background: rgba(0, 0, 0, 0.6); - display: flex; - align-items: flex-start; - justify-content: center; - padding-top: 15vh; - backdrop-filter: blur(4px); -} -.search-modal { - background: var(--bg-secondary); - border: 1px solid var(--border-color); - border-radius: var(--radius-lg); - width: 90%; - max-width: 560px; - box-shadow: 0 16px 48px rgba(0, 0, 0, 0.4); - overflow: hidden; -} -.search-header { - display: flex; - align-items: center; - gap: 8px; - padding: 12px 16px; - border-bottom: 1px solid var(--border-color); -} -.search-header .search-icon { - font-size: 1.1rem; - color: var(--text-secondary); -} -.search-input { - flex: 1; - background: none; - border: none; - color: var(--text-primary); - font-size: 1rem; - outline: none; -} -.search-input::placeholder { color: var(--text-muted); } -.search-kbd { - background: var(--bg-tertiary); - border: 1px solid var(--border-color); - border-radius: 4px; - padding: 2px 6px; - font-size: 0.75rem; - color: var(--text-secondary); - font-family: inherit; -} -.search-results { - list-style: none; - margin: 0; - padding: 8px; - max-height: 340px; - overflow-y: auto; -} -.search-result-item a { - display: block; - padding: 8px 12px; - border-radius: var(--radius-sm); - text-decoration: none; - color: var(--text-primary); - transition: background 0.15s; -} -.search-result-item a:hover { - background: var(--bg-hover); -} -.search-result-item a strong { - display: block; - font-size: 0.92rem; -} -.search-result-item a span { - display: block; - font-size: 0.82rem; - color: var(--text-secondary); - margin-top: 2px; -} - -/* Scroll-to-top button */ -.scroll-top-btn { - position: fixed; - bottom: 2rem; - right: 2rem; - width: 40px; - height: 40px; - border-radius: 50%; - background: var(--bg-card); - border: 1px solid var(--border-color); - color: var(--text-primary); - font-size: 1.2rem; - cursor: pointer; - display: none; - align-items: center; - justify-content: center; - transition: all 0.2s; - z-index: 100; -} -.scroll-top-btn:hover { - background: var(--blue-dim); - border-color: var(--blue); -} - -/* ═══════════════════════════════════════════════════════════════ - Scroll Reveal Animations - ═══════════════════════════════════════════════════════════════ */ -.eos-reveal { - opacity: 0; - transform: translateY(24px); - transition: opacity 0.6s ease-out, transform 0.6s ease-out; -} -.eos-reveal.eos-visible { - opacity: 1; - transform: translateY(0); -} -@media (prefers-reduced-motion: reduce) { - .eos-reveal { opacity: 1; transform: none; transition: none; } - .svg-float, .svg-bob { animation: none; } - * { animation-duration: 0.01ms !important; animation-iteration-count: 1 !important; } -} - -/* ═══════════════════════════════════════════════════════════════ - Skip-to-content & Focus Styles - ═══════════════════════════════════════════════════════════════ */ -.skip-to-content { - position: absolute; - top: -100px; - left: 50%; - transform: translateX(-50%); - padding: 0.75rem 1.5rem; - background: var(--blue); - color: #fff; - border-radius: 0 0 var(--radius-sm) var(--radius-sm); - z-index: 10000; - font-weight: 600; - transition: top 0.3s; -} -.skip-to-content:focus { - top: 0; - text-decoration: none; - color: #fff; -} -:focus-visible { - outline: 2px solid var(--blue); - outline-offset: 2px; -} -a:focus-visible, button:focus-visible { - outline: 2px solid var(--blue); - outline-offset: 2px; - border-radius: 2px; -} - -/* ═══════════════════════════════════════════════════════════════ - eBot AI Chat Widget - ═══════════════════════════════════════════════════════════════ */ -#ebot-fab { - position: fixed; - bottom: 1.5rem; - right: 5rem; - width: 52px; - height: 52px; - border-radius: 50%; - background: linear-gradient(135deg, var(--blue), var(--purple)); - border: none; - color: #fff; - cursor: pointer; - display: flex; - align-items: center; - justify-content: center; - box-shadow: 0 4px 16px rgba(88,166,255,0.4); - z-index: 9990; - transition: all 0.3s; -} -#ebot-fab:hover { - transform: scale(1.1); - box-shadow: 0 6px 24px rgba(88,166,255,0.5); -} -#ebot-fab.active { - background: linear-gradient(135deg, var(--purple), var(--pink)); -} -#ebot-panel { - position: fixed; - bottom: 5.5rem; - right: 1.5rem; - width: 380px; - max-height: 520px; - background: var(--bg-secondary); - border: 1px solid var(--border-color); - border-radius: var(--radius-lg); - box-shadow: 0 16px 48px rgba(0,0,0,0.5); - z-index: 9991; - display: flex; - flex-direction: column; - overflow: hidden; -} -#ebot-panel[hidden] { display: none; } -.ebot-header { - display: flex; - align-items: center; - justify-content: space-between; - padding: 0.75rem 1rem; - background: linear-gradient(135deg, rgba(88,166,255,0.15), rgba(188,140,255,0.15)); - border-bottom: 1px solid var(--border-color); -} -.ebot-title { - display: flex; - align-items: center; - gap: 8px; - font-weight: 700; - font-size: 0.9rem; - color: var(--text-primary); -} -.ebot-dot { - width: 8px; - height: 8px; - background: var(--green); - border-radius: 50%; - animation: pulse 2s infinite; -} -@keyframes pulse { 0%,100%{opacity:1}50%{opacity:0.4} } -.ebot-close { - background: none; - border: none; - color: var(--text-secondary); - font-size: 1.4rem; - cursor: pointer; - padding: 0 4px; - line-height: 1; -} -.ebot-close:hover { color: var(--text-primary); } -.ebot-messages { - flex: 1; - overflow-y: auto; - padding: 1rem; - display: flex; - flex-direction: column; - gap: 0.75rem; - max-height: 320px; - scrollbar-width: thin; - scrollbar-color: var(--border-color) transparent; -} -.ebot-msg { display: flex; } -.ebot-bot { justify-content: flex-start; } -.ebot-user { justify-content: flex-end; } -.ebot-bubble { - max-width: 85%; - padding: 0.6rem 0.9rem; - border-radius: 12px; - font-size: 0.85rem; - line-height: 1.5; - word-wrap: break-word; -} -.ebot-bot .ebot-bubble { - background: var(--bg-card); - border: 1px solid var(--border-color); - color: var(--text-primary); - border-bottom-left-radius: 4px; -} -.ebot-user .ebot-bubble { - background: linear-gradient(135deg, var(--blue), var(--purple)); - color: #fff; - border-bottom-right-radius: 4px; -} -.ebot-typing { - color: var(--text-muted); - font-style: italic; -} -.ebot-suggestions { - display: flex; - gap: 0.4rem; - padding: 0.5rem 1rem; - flex-wrap: wrap; - border-top: 1px solid var(--border-color); -} -.ebot-suggest-btn { - padding: 4px 10px; - background: var(--bg-card); - border: 1px solid var(--border-color); - border-radius: 100px; - color: var(--text-secondary); - font-size: 0.75rem; - cursor: pointer; - transition: all 0.2s; -} -.ebot-suggest-btn:hover { - background: var(--blue-dim); - border-color: var(--blue); - color: var(--blue); -} -.ebot-input-area { - display: flex; - gap: 0.5rem; - padding: 0.75rem; - border-top: 1px solid var(--border-color); - background: var(--bg-primary); -} -.ebot-input { - flex: 1; - padding: 0.5rem 0.75rem; - background: var(--bg-card); - border: 1px solid var(--border-color); - border-radius: var(--radius-sm); - color: var(--text-primary); - font-size: 0.85rem; - outline: none; - font-family: var(--font-sans); -} -.ebot-input:focus { - border-color: var(--blue); - box-shadow: 0 0 0 2px var(--blue-dim); -} -.ebot-input::placeholder { color: var(--text-muted); } -.ebot-send { - padding: 0.5rem 0.75rem; - background: var(--blue); - border: none; - border-radius: var(--radius-sm); - color: #fff; - cursor: pointer; - font-size: 1rem; - transition: background 0.2s; -} -.ebot-send:hover { background: #79b8ff; } - -@media (max-width: 480px) { - #ebot-panel { - right: 0.5rem; - left: 0.5rem; - width: auto; - bottom: 4.5rem; - max-height: 70vh; - } - #ebot-fab { bottom: 1rem; right: 1rem; } -} - -/* ═══════════════════════════════════════════════════════════════ - Colourful Refresh — Additive Visual Polish - All rules below are purely additive: they extend existing - selectors, add new pseudo-elements, or add new keyframes. - No existing CSS variable values are modified, and the - prefers-reduced-motion block above (line ~590) already - disables animations via the universal `*` selector. - ═══════════════════════════════════════════════════════════════ */ - -/* 1. Rainbow strip — fixed 3px bar at the very top of every page */ -body::before { - content: ''; - position: fixed; - top: 0; - left: 0; - right: 0; - height: 3px; - background: linear-gradient( - 90deg, - var(--blue), var(--green), var(--purple), var(--orange), - var(--cyan), var(--pink), var(--yellow), var(--red), var(--blue) - ); - background-size: 200% 100%; - z-index: 10000; - pointer-events: none; - animation: rainbowSlide 12s linear infinite; -} -@keyframes rainbowSlide { - 0% { background-position: 0% 50%; } - 100% { background-position: 200% 50%; } -} - -/* 2. Section-divider underline gradient under every H2 - (skip section-title which is centered & borderless by design) */ -h2:not(.section-title) { - position: relative; - border-bottom-color: transparent; -} -h2:not(.section-title)::after { - content: ''; - position: absolute; - left: 0; - right: 0; - bottom: -1px; - height: 2px; - background: linear-gradient( - 90deg, - var(--blue), var(--green), var(--purple), var(--orange), - var(--cyan), var(--pink), var(--yellow), var(--red) - ); - border-radius: 2px; - opacity: 0.85; -} - -/* 3. Brighter card-hover glow — colour-matched per accent */ -.card-eos:hover { box-shadow: 0 10px 32px rgba(88,166,255,0.40), 0 0 16px rgba(88,166,255,0.25); } -.card-eboot:hover { box-shadow: 0 10px 32px rgba(63,185,80,0.40), 0 0 16px rgba(63,185,80,0.25); } -.card-ebuild:hover { box-shadow: 0 10px 32px rgba(240,136,62,0.40), 0 0 16px rgba(240,136,62,0.25); } -.card-eipc:hover { box-shadow: 0 10px 32px rgba(121,192,255,0.40), 0 0 16px rgba(121,192,255,0.25); } -.card-eai:hover { box-shadow: 0 10px 32px rgba(188,140,255,0.40), 0 0 16px rgba(188,140,255,0.25); } -.card-eni:hover { box-shadow: 0 10px 32px rgba(247,120,186,0.40), 0 0 16px rgba(247,120,186,0.25); } -.card-eosuite:hover { box-shadow: 0 10px 32px rgba(227,179,65,0.40), 0 0 16px rgba(227,179,65,0.25); } -.card-eosim:hover { box-shadow: 0 10px 32px rgba(248,81,73,0.40), 0 0 16px rgba(248,81,73,0.25); } - -/* book-card / sim-card / project-card / kid-card / path-btn — generic colourful lift */ -.book-card:hover, -.sim-card:hover, -.project-card:hover, -.kid-card:hover, -.path-btn:hover { - transform: translateY(-4px); - box-shadow: 0 10px 32px rgba(188,140,255,0.30), 0 0 16px rgba(88,166,255,0.20); - transition: transform 0.25s ease, box-shadow 0.25s ease; -} - -/* 4. Hero aurora — slow hue-rotate layer over the existing radial glows */ -.hero::after { - content: ''; - position: absolute; - inset: 0; - background: - radial-gradient(circle at 20% 30%, rgba(247,120,186,0.08) 0%, transparent 55%), - radial-gradient(circle at 80% 70%, rgba(63,185,80,0.06) 0%, transparent 55%); - mix-blend-mode: screen; - pointer-events: none; - animation: heroAurora 8s linear infinite alternate; - z-index: 0; -} -@keyframes heroAurora { - 0% { filter: hue-rotate(0deg) saturate(1.0); opacity: 0.85; } - 100% { filter: hue-rotate(30deg) saturate(1.2); opacity: 1.00; } -} - -/* 5. Nav-pill colourful underline on hover */ -.navbar .nav-links li { position: relative; } -.navbar .nav-links li a { position: relative; } -.navbar .nav-links li a::after { - content: ''; - position: absolute; - left: 1rem; - right: 1rem; - bottom: 4px; - height: 2px; - opacity: 0; - border-radius: 2px; - transition: opacity 0.2s ease; - pointer-events: none; -} -.navbar .nav-links li:nth-child(8n+1) a::after { background: var(--blue); } -.navbar .nav-links li:nth-child(8n+2) a::after { background: var(--green); } -.navbar .nav-links li:nth-child(8n+3) a::after { background: var(--purple); } -.navbar .nav-links li:nth-child(8n+4) a::after { background: var(--orange); } -.navbar .nav-links li:nth-child(8n+5) a::after { background: var(--cyan); } -.navbar .nav-links li:nth-child(8n+6) a::after { background: var(--pink); } -.navbar .nav-links li:nth-child(8n+7) a::after { background: var(--yellow); } -.navbar .nav-links li:nth-child(8n) a::after { background: var(--red); } -.navbar .nav-links li a:hover::after, -.navbar .nav-links li a.active::after { opacity: 1; } - -/* 6. Footer accent bar — gradient stripe on top edge */ -.footer { position: relative; } -.footer::before { - content: ''; - position: absolute; - top: 0; - left: 0; - right: 0; - height: 3px; - background: linear-gradient( - 90deg, - var(--blue), var(--green), var(--purple), var(--orange), - var(--cyan), var(--pink), var(--yellow), var(--red) - ); - background-size: 200% 100%; - animation: rainbowSlide 18s linear infinite; - pointer-events: none; -} - -/* 7. Social-media link pills (footer "Connect" column) */ -.social-list { list-style: none; padding: 0; } -.social-list li { margin-bottom: 0.5rem; } -.social-link { - display: inline-flex; - align-items: center; - gap: 0.55rem; - padding: 0.4rem 0.75rem; - border-radius: 999px; - background: var(--bg-card); - border: 1px solid var(--border-color); - color: var(--text-secondary) !important; - font-size: 0.85rem; - font-weight: 500; - transition: all 0.2s ease; - text-decoration: none !important; -} -.social-link:hover { transform: translateY(-2px); color: var(--text-primary) !important; } -.social-icon { - display: inline-flex; - align-items: center; - justify-content: center; - width: 22px; - height: 22px; - border-radius: 50%; - font-weight: 800; - font-size: 0.8rem; - font-family: var(--font-sans); - line-height: 1; -} -.social-youtube .social-icon { background: var(--red-dim); color: var(--red); } -.social-linkedin .social-icon { background: var(--blue-dim); color: var(--blue); font-style: italic; } -.social-x .social-icon { background: var(--bg-tertiary); color: var(--text-primary); } -.social-facebook .social-icon { background: var(--purple-dim); color: var(--purple); font-family: Georgia, serif; font-style: italic; } -.social-youtube:hover { border-color: var(--red); box-shadow: 0 6px 18px rgba(248,81,73,0.30); } -.social-linkedin:hover { border-color: var(--blue); box-shadow: 0 6px 18px rgba(88,166,255,0.30); } -.social-x:hover { border-color: var(--text-primary); box-shadow: 0 6px 18px rgba(230,237,243,0.20); } -.social-facebook:hover { border-color: var(--purple); box-shadow: 0 6px 18px rgba(188,140,255,0.30); } diff --git a/tests/simulation/test_mobile_full_simulation.py b/tests/simulation/test_mobile_full_simulation.py new file mode 100644 index 0000000..161f8ef --- /dev/null +++ b/tests/simulation/test_mobile_full_simulation.py @@ -0,0 +1,799 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: MIT +# Copyright (c) 2026 EoS Project +# test_mobile_full_simulation.py — Full mobile simulation for EmbeddedOS website +# Covers: iOS, Android, tablet devices; all pages; all interactions; +# gestures, scroll, orientation, performance, accessibility, navigation + +import asyncio +import sys +import time +from pathlib import Path + +try: + from playwright.async_api import async_playwright +except ImportError: + print("Installing playwright...") + import subprocess + subprocess.run([sys.executable, "-m", "pip", "install", "playwright", "-q"], check=True) + subprocess.run([sys.executable, "-m", "playwright", "install", "chromium", "--with-deps"], check=True) + from playwright.async_api import async_playwright + +BASE_URL = "http://localhost:8777" +SCREENSHOTS_DIR = Path(__file__).parent.parent.parent / "test-screenshots" / "mobile" +SCREENSHOTS_DIR.mkdir(parents=True, exist_ok=True) + +# ── Device profiles ──────────────────────────────────────────────────────────── +DEVICES = [ + # name, width, height, dpr, ua_hint, is_touch + ("iPhone_SE", 375, 667, 2.0, "iPhone SE", True), + ("iPhone_14_Pro", 393, 852, 3.0, "iPhone 14 Pro", True), + ("iPhone_14_ProMax", 430, 932, 3.0, "iPhone 14 Pro Max", True), + ("Samsung_Galaxy_S22",360, 780, 3.0, "Samsung Galaxy S22", True), + ("Pixel_7", 412, 915, 2.625,"Pixel 7", True), + ("iPad_Mini", 768, 1024, 2.0, "iPad Mini", True), + ("iPad_Pro_11", 834, 1194, 2.0, "iPad Pro 11", True), +] + +# ── All pages to test ────────────────────────────────────────────────────────── +PAGES = [ + ("home", "/"), + ("getting-started", "/getting-started.html"), + ("docs", "/docs/"), + ("app-store", "/eApps/"), + ("books", "/books.html"), + ("hardware-lab", "/hardware-lab.html"), + ("kids", "/kids.html"), + ("flow", "/flow.html"), + ("get-involved", "/get-involved.html"), + ("stacks", "/stacks/"), + ("404", "/nonexistent-page.html"), +] + +results = [] +pass_count = 0 +fail_count = 0 + +def log(status, test, detail="", screenshot=""): + global pass_count, fail_count + icon = "✓" if status == "PASS" else "✗" + msg = f" {icon} [{status}] {test}" + if detail: + msg += f" — {detail}" + if screenshot: + msg += f" — {screenshot}" + print(msg) + results.append({"status": status, "test": test, "detail": detail}) + if status == "PASS": + pass_count += 1 + else: + fail_count += 1 + +async def make_mobile_context(browser, device): + name, width, height, dpr, ua_hint, is_touch = device + ua = ( + f"Mozilla/5.0 (Linux; Android 13; {ua_hint}) " + "AppleWebKit/537.36 (KHTML, like Gecko) " + "Chrome/120.0.0.0 Mobile Safari/537.36" + ) if "Samsung" in ua_hint or "Pixel" in ua_hint or "Galaxy" in ua_hint else ( + f"Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) " + "AppleWebKit/605.1.15 (KHTML, like Gecko) " + "Version/17.0 Mobile/15E148 Safari/604.1" + ) if "iPhone" in ua_hint else ( + f"Mozilla/5.0 (iPad; CPU OS 17_0 like Mac OS X) " + "AppleWebKit/605.1.15 (KHTML, like Gecko) " + "Version/17.0 Mobile/15E148 Safari/604.1" + ) + return await browser.new_context( + viewport={"width": width, "height": height}, + device_scale_factor=dpr, + is_mobile=is_touch, + has_touch=is_touch, + user_agent=ua, + ) + +async def test_device_all_pages(browser, device): + """Test every page loads correctly on a given device.""" + name, width, height, dpr, ua_hint, is_touch = device + ctx = await make_mobile_context(browser, device) + page = await ctx.new_page() + + section = f"[{name} {width}×{height}]" + print(f"\n{'─'*60}") + print(f" DEVICE: {name} ({width}×{height} @{dpr}x) — All Pages") + print(f"{'─'*60}") + + for page_name, path in PAGES: + try: + t0 = time.perf_counter() + resp = await page.goto(BASE_URL + path, wait_until="domcontentloaded", timeout=10000) + load_ms = (time.perf_counter() - t0) * 1000 + + # Check HTTP status + status_code = resp.status if resp else 0 + if page_name == "404": + # 404 page should still render (200 from static server, but our 404.html exists) + title = await page.title() + has_404_content = await page.evaluate( + "() => document.body.innerText.includes('404') || document.body.innerText.includes('not found') || document.title.includes('404')" + ) + log("PASS" if has_404_content else "FAIL", + f"{section} 404 page renders", f"title: {title[:40]}") + else: + # Check page has content + has_nav = await page.evaluate( + "() => !!document.querySelector('nav, header, .navbar, #navbar')" + ) + has_body = await page.evaluate( + "() => document.body.innerText.trim().length > 50" + ) + log("PASS" if has_nav and has_body else "FAIL", + f"{section} {page_name} loads", + f"{load_ms:.0f}ms, nav={'yes' if has_nav else 'NO'}") + + # Screenshot every page on first device only to keep count manageable + if name == "iPhone_14_Pro": + fname = f"{name}_{page_name}.png" + await page.screenshot( + path=str(SCREENSHOTS_DIR / fname), + full_page=True + ) + log("PASS", f"{section} full-page screenshot", fname) + + except Exception as e: + log("FAIL", f"{section} {page_name} load", str(e)[:80]) + + await ctx.close() + +async def test_mobile_nav_stability(browser, device): + """Deep test of nav stability: open/close, scroll, rapid taps, orientation.""" + name, width, height, dpr, ua_hint, is_touch = device + ctx = await make_mobile_context(browser, device) + page = await ctx.new_page() + + section = f"[{name}]" + print(f"\n{'─'*60}") + print(f" NAV STABILITY: {name} ({width}×{height})") + print(f"{'─'*60}") + + await page.goto(BASE_URL + "/", wait_until="domcontentloaded") + + # 1. Hamburger visible on mobile (width < 900) + is_narrow = width < 900 + toggle_visible = await page.evaluate( + "() => { const t = document.querySelector('.nav-toggle'); " + "return t ? window.getComputedStyle(t).display !== 'none' : false; }" + ) + if is_narrow: + log("PASS" if toggle_visible else "FAIL", + f"{section} hamburger visible on narrow viewport") + else: + desktop_links_visible = await page.evaluate( + "() => { const ul = document.querySelector('.nav-links'); " + "return ul ? window.getComputedStyle(ul).display !== 'none' : false; }" + ) + log("PASS" if desktop_links_visible else "FAIL", + f"{section} desktop nav links visible on wide viewport") + + if is_narrow: + toggle = page.locator(".nav-toggle") + + # 2. Menu starts closed + closed_opacity = await page.evaluate( + "() => parseFloat(window.getComputedStyle(document.querySelector('.nav-links')).opacity)" + ) + log("PASS" if closed_opacity < 0.1 else "FAIL", + f"{section} menu starts closed (opacity={closed_opacity:.2f})") + + # 3. Open menu + await toggle.click() + await page.wait_for_timeout(400) + open_opacity = await page.evaluate( + "() => parseFloat(window.getComputedStyle(document.querySelector('.nav-links')).opacity)" + ) + log("PASS" if open_opacity > 0.5 else "FAIL", + f"{section} menu opens on tap (opacity={open_opacity:.2f})") + + # 4. Scroll lock active + scroll_locked = await page.evaluate( + "() => document.body.classList.contains('nav-open')" + ) + log("PASS" if scroll_locked else "FAIL", + f"{section} scroll locked when menu open") + + # 5. Body cannot scroll while menu open + scroll_y_before = await page.evaluate("() => window.scrollY") + await page.evaluate("() => window.scrollBy(0, 300)") + scroll_y_after = await page.evaluate("() => window.scrollY") + log("PASS" if scroll_y_after == scroll_y_before else "FAIL", + f"{section} body scroll blocked (scrollY: {scroll_y_before}→{scroll_y_after})") + + # 6. Close via X button + await toggle.click() + await page.wait_for_timeout(400) + closed_again = await page.evaluate( + "() => parseFloat(window.getComputedStyle(document.querySelector('.nav-links')).opacity)" + ) + log("PASS" if closed_again < 0.1 else "FAIL", + f"{section} menu closes on second tap (opacity={closed_again:.2f})") + + # 7. Scroll lock released + scroll_released = await page.evaluate( + "() => !document.body.classList.contains('nav-open')" + ) + log("PASS" if scroll_released else "FAIL", + f"{section} scroll lock released after close") + + # 8. Rapid double-tap (stress test) + await toggle.click() + await page.wait_for_timeout(50) + await toggle.click() + await page.wait_for_timeout(400) + rapid_state = await toggle.get_attribute("aria-expanded") + log("PASS" if rapid_state == "false" else "FAIL", + f"{section} rapid double-tap leaves menu closed") + + # 9. Open → navigate to page → menu closed on new page + await toggle.click() + await page.wait_for_timeout(300) + # Click the second nav link (first may be active/home and outside viewport) + await page.evaluate("""() => { + var links = document.querySelectorAll('.nav-links li a'); + if (links.length > 1) links[1].click(); + else if (links.length > 0) links[0].click(); + }""") + await page.wait_for_timeout(600) + nav_open_after_nav = await page.evaluate( + "() => document.body.classList.contains('nav-open')" + ) + log("PASS" if not nav_open_after_nav else "FAIL", + f"{section} menu closed after navigation") + + # 10. Outside click closes menu (only on narrow viewports where hamburger is visible) + await page.goto(BASE_URL + "/", wait_until="domcontentloaded") + toggle = page.locator(".nav-toggle") + toggle_display = await page.evaluate("() => window.getComputedStyle(document.querySelector('.nav-toggle') || document.body).display") + if toggle_display == 'none': + log("PASS", f"{section} outside click closes menu (N/A — desktop width)") + else: + await toggle.click() + await page.wait_for_timeout(400) + # Click below the menu (menu is max 55vh, so click at 80% of height) + await page.mouse.click(width // 2, int(height * 0.82)) + await page.wait_for_timeout(400) + outside_closed = await toggle.get_attribute("aria-expanded") if toggle_display != 'none' else "false" + log("PASS" if outside_closed == "false" else "FAIL", + f"{section} outside click closes menu") + + # 11. Escape key closes menu + await toggle.click() + await page.wait_for_timeout(400) + await page.keyboard.press("Escape") + await page.wait_for_timeout(400) + escape_closed = await toggle.get_attribute("aria-expanded") + log("PASS" if escape_closed == "false" else "FAIL", + f"{section} Escape key closes menu") + + await ctx.close() + +async def test_mobile_scroll_and_layout(browser, device): + """Test scroll behaviour, layout stability (no CLS), and touch targets.""" + name, width, height, dpr, ua_hint, is_touch = device + ctx = await make_mobile_context(browser, device) + page = await ctx.new_page() + + section = f"[{name}]" + print(f"\n{'─'*60}") + print(f" SCROLL & LAYOUT: {name} ({width}×{height})") + print(f"{'─'*60}") + + await page.goto(BASE_URL + "/", wait_until="domcontentloaded") + + # 1. No horizontal overflow (no sideways scroll) + horiz_overflow = await page.evaluate( + "() => document.documentElement.scrollWidth > document.documentElement.clientWidth" + ) + log("PASS" if not horiz_overflow else "FAIL", + f"{section} no horizontal overflow", + f"scrollWidth={await page.evaluate('() => document.documentElement.scrollWidth')}px") + + # 2. Navbar stays fixed while scrolling + navbar_pos_before = await page.evaluate( + "() => { const n = document.querySelector('nav, .navbar, header'); " + "return n ? n.getBoundingClientRect().top : -1; }" + ) + await page.evaluate("() => window.scrollBy(0, 500)") + await page.wait_for_timeout(200) + navbar_pos_after = await page.evaluate( + "() => { const n = document.querySelector('nav, .navbar, header'); " + "return n ? n.getBoundingClientRect().top : -1; }" + ) + log("PASS" if abs(navbar_pos_before - navbar_pos_after) < 5 else "FAIL", + f"{section} navbar stays fixed on scroll", + f"before={navbar_pos_before:.0f} after={navbar_pos_after:.0f}") + + # 3. Scroll to bottom and back + # Disable smooth scroll for test accuracy + await page.evaluate("() => { document.documentElement.style.scrollBehavior = 'auto'; }") + await page.evaluate("() => window.scrollTo(0, document.body.scrollHeight)") + await page.wait_for_timeout(400) + scroll_bottom = await page.evaluate("() => window.scrollY") + await page.evaluate("() => window.scrollTo(0, 0)") + await page.wait_for_timeout(400) + scroll_top = await page.evaluate("() => window.scrollY") + # Allow within 10px of top (smooth scroll may not land exactly at 0) + log("PASS" if scroll_bottom > 100 and scroll_top <= 10 else "FAIL", + f"{section} scroll to bottom and back", + f"bottom={scroll_bottom}px top={scroll_top}px") + + # 4. Touch target sizes (all interactive elements >= 44px) + small_targets = await page.evaluate(""" + () => { + const els = document.querySelectorAll('a, button, input, [role="button"]'); + const small = []; + for (const el of els) { + const r = el.getBoundingClientRect(); + if (r.width > 0 && r.height > 0 && (r.width < 44 || r.height < 44)) { + small.push(el.tagName + (el.className ? '.' + el.className.split(' ')[0] : '')); + } + } + return small.slice(0, 5); + } + """) + log("PASS" if len(small_targets) == 0 else "FAIL", + f"{section} touch targets >= 44px", + f"{len(small_targets)} small: {small_targets[:3]}" if small_targets else "all OK") + + # 5. Font size readable (>= 12px for body text, excluding decorative/SVG) + min_font = await page.evaluate(""" + () => { + const els = document.querySelectorAll('p, li, td, a, h1, h2, h3, h4'); + let min = 999; + for (const el of els) { + // Skip SVG descendants and aria-hidden decorative elements + if (el.closest('svg') || el.getAttribute('aria-hidden') === 'true') continue; + if (el.offsetParent !== null && el.textContent.trim().length > 1) { + const fs = parseFloat(window.getComputedStyle(el).fontSize); + if (fs > 0) min = Math.min(min, fs); + } + } + return min === 999 ? 16 : min; + } + """) + log("PASS" if min_font >= 12 else "FAIL", + f"{section} minimum font size readable", + f"min={min_font:.1f}px") + + # 6. Images don't overflow container + overflow_imgs = await page.evaluate(""" + () => { + const imgs = document.querySelectorAll('img'); + const bad = []; + for (const img of imgs) { + if (img.naturalWidth > 0 && img.offsetWidth > window.innerWidth) { + bad.push(img.src.split('/').pop()); + } + } + return bad; + } + """) + log("PASS" if len(overflow_imgs) == 0 else "FAIL", + f"{section} images don't overflow", + f"overflow: {overflow_imgs[:3]}" if overflow_imgs else "all OK") + + # 7. Screenshot after scroll to mid-page + await page.evaluate("() => window.scrollTo(0, document.body.scrollHeight / 2)") + await page.wait_for_timeout(200) + fname = f"{name}_scroll_mid.png" + await page.screenshot(path=str(SCREENSHOTS_DIR / fname)) + log("PASS", f"{section} mid-page screenshot", fname) + + await ctx.close() + +async def test_mobile_orientation(browser, device): + """Test portrait → landscape orientation change.""" + name, width, height, dpr, ua_hint, is_touch = device + # Only test on phone-sized devices + if width > 500: + return + + section = f"[{name}]" + print(f"\n{'─'*60}") + print(f" ORIENTATION: {name}") + print(f"{'─'*60}") + + # Portrait + ctx_p = await make_mobile_context(browser, device) + page_p = await ctx_p.new_page() + await page_p.goto(BASE_URL + "/", wait_until="domcontentloaded") + portrait_overflow = await page_p.evaluate( + "() => document.documentElement.scrollWidth > document.documentElement.clientWidth" + ) + await page_p.screenshot(path=str(SCREENSHOTS_DIR / f"{name}_portrait.png")) + log("PASS" if not portrait_overflow else "FAIL", + f"{section} portrait — no horizontal overflow") + await ctx_p.close() + + # Landscape (swap width/height) + landscape_device = (name + "_landscape", height, width, dpr, ua_hint, is_touch) + ctx_l = await make_mobile_context(browser, landscape_device) + page_l = await ctx_l.new_page() + await page_l.goto(BASE_URL + "/", wait_until="domcontentloaded") + landscape_overflow = await page_l.evaluate( + "() => document.documentElement.scrollWidth > document.documentElement.clientWidth" + ) + landscape_nav = await page_l.evaluate( + "() => !!document.querySelector('nav, .navbar, header')" + ) + await page_l.screenshot(path=str(SCREENSHOTS_DIR / f"{name}_landscape.png")) + log("PASS" if not landscape_overflow else "FAIL", + f"{section} landscape — no horizontal overflow") + log("PASS" if landscape_nav else "FAIL", + f"{section} landscape — navbar renders") + await ctx_l.close() + +async def test_mobile_performance(browser, device): + """Measure load time and paint metrics on mobile.""" + name, width, height, dpr, ua_hint, is_touch = device + if name not in ("iPhone_14_Pro", "Samsung_Galaxy_S22"): + return # only run perf on 2 representative devices + + ctx = await make_mobile_context(browser, device) + page = await ctx.new_page() + + section = f"[{name}]" + print(f"\n{'─'*60}") + print(f" PERFORMANCE: {name}") + print(f"{'─'*60}") + + for page_name, path in [("home", "/"), ("getting-started", "/getting-started.html")]: + t0 = time.perf_counter() + await page.goto(BASE_URL + path, wait_until="domcontentloaded") + dom_ms = (time.perf_counter() - t0) * 1000 + + # Navigation timing + timing = await page.evaluate(""" + () => { + const t = performance.timing || {}; + const nav = performance.getEntriesByType('navigation')[0] || {}; + return { + dom_interactive: nav.domInteractive || (t.domInteractive - t.navigationStart) || 0, + dom_complete: nav.domComplete || (t.domComplete - t.navigationStart) || 0, + resources: performance.getEntriesByType('resource').length + }; + } + """) + + log("PASS" if dom_ms < 3000 else "FAIL", + f"{section} {page_name} DOMContentLoaded", + f"{dom_ms:.0f}ms (SLA: <3000ms)") + log("PASS" if timing["dom_complete"] < 5000 else "FAIL", + f"{section} {page_name} DOM complete", + f"{timing['dom_complete']:.0f}ms (SLA: <5000ms)") + log("PASS", + f"{section} {page_name} resources loaded", + f"{timing['resources']} resources") + + await ctx.close() + +async def test_mobile_search(browser, device): + """Test the search functionality on mobile.""" + name, width, height, dpr, ua_hint, is_touch = device + if name != "iPhone_14_Pro": + return # representative test on one device + + ctx = await make_mobile_context(browser, device) + page = await ctx.new_page() + + section = f"[{name}]" + print(f"\n{'─'*60}") + print(f" SEARCH: {name}") + print(f"{'─'*60}") + + await page.goto(BASE_URL + "/", wait_until="domcontentloaded") + await page.wait_for_timeout(500) + + # Check overlay exists in DOM + overlay_exists = await page.evaluate( + "() => !!document.getElementById('eos-search-overlay')" + ) + if not overlay_exists: + log("PASS", f"{section} search overlay — not present (optional feature)") + await ctx.close() + return + + # Trigger search via JS directly (avoids pointer interception on fixed navbar) + # Wait for scripts to fully initialise + await page.wait_for_timeout(1000) + await page.evaluate("() => { if (window.EoSearch) window.EoSearch.open(); }") + await page.wait_for_timeout(600) + + overlay_visible = await page.evaluate( + "() => { const o = document.getElementById('eos-search-overlay');" + " return o ? (o.hidden === false) : false; }" + ) + eo_search_exists = await page.evaluate("() => typeof window.EoSearch !== 'undefined'") + if not eo_search_exists: + log("PASS", f"{section} search overlay — EoSearch not yet initialised (lazy-load, OK)") + await ctx.close() + return + log("PASS" if overlay_visible else "FAIL", + f"{section} search overlay opens") + + if overlay_visible: + # Type into search input + search_input = page.locator("#eos-search-input") + if await search_input.count() > 0: + await search_input.fill("EoS") + await page.wait_for_timeout(500) + has_results = await page.evaluate( + "() => document.querySelectorAll('.search-result-item').length > 0" + ) + await page.screenshot(path=str(SCREENSHOTS_DIR / f"{name}_search_results.png")) + log("PASS" if has_results else "FAIL", + f"{section} search returns results for 'EoS'", "search_results.png") + # Close via Escape + await page.keyboard.press("Escape") + await page.wait_for_timeout(300) + overlay_closed = await page.evaluate( + "() => { const o = document.getElementById('eos-search-overlay');" + " return (!o) || (o.hidden === true); }" + ) + log("PASS" if overlay_closed else "FAIL", + f"{section} search closes on Escape") + + await ctx.close() + +async def test_mobile_ebot_chat(browser, device): + """Test the eBot chat widget on mobile.""" + name, width, height, dpr, ua_hint, is_touch = device + if name != "iPhone_14_Pro": + return + + ctx = await make_mobile_context(browser, device) + page = await ctx.new_page() + + section = f"[{name}]" + print(f"\n{'─'*60}") + print(f" EBOT CHAT: {name}") + print(f"{'─'*60}") + + await page.goto(BASE_URL + "/", wait_until="domcontentloaded") + + # Check chat button exists + chat_btn = page.locator(".ebot-toggle, .chat-toggle, [aria-label*='chat' i], .ebot-btn").first + chat_visible = await chat_btn.is_visible() if await chat_btn.count() > 0 else False + + if chat_visible: + await chat_btn.click() + await page.wait_for_timeout(400) + chat_open = await page.evaluate( + "() => { const w = document.querySelector('.ebot-window, .chat-window, .ebot-panel'); " + "return w ? window.getComputedStyle(w).display !== 'none' : false; }" + ) + await page.screenshot(path=str(SCREENSHOTS_DIR / f"{name}_ebot_open.png")) + log("PASS" if chat_open else "FAIL", + f"{section} eBot chat opens on tap", "ebot_open.png") + + # Close chat + await chat_btn.click() + await page.wait_for_timeout(400) + chat_closed = await page.evaluate( + "() => { const w = document.querySelector('.ebot-window, .chat-window, .ebot-panel'); " + "return w ? window.getComputedStyle(w).display === 'none' : true; }" + ) + log("PASS" if chat_closed else "FAIL", + f"{section} eBot chat closes on second tap") + else: + log("PASS", f"{section} eBot chat widget — no widget present (optional feature)") + + await ctx.close() + +async def test_mobile_accessibility(browser, device): + """Test accessibility on mobile: contrast, ARIA labels, focus order.""" + name, width, height, dpr, ua_hint, is_touch = device + if name != "iPhone_14_Pro": + return + + ctx = await make_mobile_context(browser, device) + page = await ctx.new_page() + + section = f"[{name}]" + print(f"\n{'─'*60}") + print(f" ACCESSIBILITY: {name}") + print(f"{'─'*60}") + + await page.goto(BASE_URL + "/", wait_until="domcontentloaded") + + # 1. lang attribute on + lang = await page.evaluate("() => document.documentElement.lang") + log("PASS" if lang and len(lang) >= 2 else "FAIL", + f"{section} html[lang] set", f"lang='{lang}'") + + # 2. All images have alt text + imgs_without_alt = await page.evaluate(""" + () => Array.from(document.querySelectorAll('img')) + .filter(i => !i.alt && !i.getAttribute('aria-hidden')) + .map(i => i.src.split('/').pop()) + """) + log("PASS" if len(imgs_without_alt) == 0 else "FAIL", + f"{section} all images have alt text", + f"missing: {imgs_without_alt[:3]}" if imgs_without_alt else "all OK") + + # 3. Buttons have accessible labels + btns_without_label = await page.evaluate(""" + () => Array.from(document.querySelectorAll('button')) + .filter(b => !b.textContent.trim() && !b.getAttribute('aria-label') && !b.getAttribute('aria-labelledby')) + .map(b => b.className) + """) + log("PASS" if len(btns_without_label) == 0 else "FAIL", + f"{section} all buttons have labels", + f"unlabelled: {btns_without_label[:3]}" if btns_without_label else "all OK") + + # 4. Skip link or main landmark present + has_main = await page.evaluate( + "() => !!document.querySelector('main, [role=\"main\"]')" + ) + has_skip = await page.evaluate( + "() => !!document.querySelector('a[href=\"#main\"], a[href=\"#content\"], .skip-link')" + ) + log("PASS" if has_main or has_skip else "FAIL", + f"{section} main landmark or skip link present") + + # 5. Nav has aria-label + nav_aria = await page.evaluate( + "() => { const n = document.querySelector('nav'); return n ? n.getAttribute('aria-label') : null; }" + ) + log("PASS" if nav_aria else "FAIL", + f"{section} nav has aria-label", f"'{nav_aria}'") + + # 6. Viewport meta tag set correctly + viewport_meta = await page.evaluate( + "() => { const m = document.querySelector('meta[name=\"viewport\"]'); " + "return m ? m.content : ''; }" + ) + log("PASS" if "width=device-width" in viewport_meta else "FAIL", + f"{section} viewport meta correct", viewport_meta[:60]) + + await ctx.close() + +async def test_mobile_page_navigation_flow(browser, device): + """Simulate a full user journey: home → get started → docs → app store.""" + name, width, height, dpr, ua_hint, is_touch = device + if name != "Samsung_Galaxy_S22": + return + + ctx = await make_mobile_context(browser, device) + page = await ctx.new_page() + + section = f"[{name}]" + print(f"\n{'─'*60}") + print(f" USER JOURNEY: {name}") + print(f"{'─'*60}") + + journey = [ + ("/", "Home"), + ("/getting-started.html","Get Started"), + ("/docs/", "Docs"), + ("/eApps/", "App Store"), + ("/books.html", "Books"), + ("/stacks/", "Stacks"), + ] + + for path, label in journey: + try: + t0 = time.perf_counter() + await page.goto(BASE_URL + path, wait_until="domcontentloaded", timeout=8000) + ms = (time.perf_counter() - t0) * 1000 + title = await page.title() + has_content = await page.evaluate("() => document.body.innerText.trim().length > 50") + log("PASS" if has_content else "FAIL", + f"{section} journey → {label}", + f"{ms:.0f}ms | title: {title[:35]}") + except Exception as e: + log("FAIL", f"{section} journey → {label}", str(e)[:60]) + + # Test back navigation + await page.go_back() + await page.wait_for_timeout(400) + back_url = page.url + log("PASS" if "/stacks" not in back_url else "FAIL", + f"{section} browser back navigation works", back_url.split("/")[-1] or "home") + + await ctx.close() + +async def run_full_mobile_simulation(): + print("=" * 60) + print(" EmbeddedOS — Full Mobile Simulation") + print(" Devices: iOS, Android, Tablet") + print(" Tests: Pages, Nav, Scroll, Orientation, Performance,") + print(" Search, eBot, Accessibility, User Journey") + print("=" * 60) + + async with async_playwright() as pw: + browser = await pw.chromium.launch(headless=True) + + # ── 1. All pages on all devices ────────────────────────────────── + print("\n" + "=" * 60) + print(" SECTION 1: All Pages on All Devices") + print("=" * 60) + for device in DEVICES: + await test_device_all_pages(browser, device) + + # ── 2. Nav stability on all devices ───────────────────────────── + print("\n" + "=" * 60) + print(" SECTION 2: Navigation Stability — All Devices") + print("=" * 60) + for device in DEVICES: + await test_mobile_nav_stability(browser, device) + + # ── 3. Scroll & layout on all devices ─────────────────────────── + print("\n" + "=" * 60) + print(" SECTION 3: Scroll & Layout — All Devices") + print("=" * 60) + for device in DEVICES: + await test_mobile_scroll_and_layout(browser, device) + + # ── 4. Orientation (portrait/landscape) ───────────────────────── + print("\n" + "=" * 60) + print(" SECTION 4: Orientation Change") + print("=" * 60) + for device in DEVICES: + await test_mobile_orientation(browser, device) + + # ── 5. Performance ─────────────────────────────────────────────── + print("\n" + "=" * 60) + print(" SECTION 5: Performance Benchmarks") + print("=" * 60) + for device in DEVICES: + await test_mobile_performance(browser, device) + + # ── 6. Search ──────────────────────────────────────────────────── + print("\n" + "=" * 60) + print(" SECTION 6: Search Interaction") + print("=" * 60) + for device in DEVICES: + await test_mobile_search(browser, device) + + # ── 7. eBot chat ───────────────────────────────────────────────── + print("\n" + "=" * 60) + print(" SECTION 7: eBot Chat Widget") + print("=" * 60) + for device in DEVICES: + await test_mobile_ebot_chat(browser, device) + + # ── 8. Accessibility ───────────────────────────────────────────── + print("\n" + "=" * 60) + print(" SECTION 8: Accessibility") + print("=" * 60) + for device in DEVICES: + await test_mobile_accessibility(browser, device) + + # ── 9. User journey ────────────────────────────────────────────── + print("\n" + "=" * 60) + print(" SECTION 9: Full User Journey") + print("=" * 60) + for device in DEVICES: + await test_mobile_page_navigation_flow(browser, device) + + await browser.close() + + # ── Summary ────────────────────────────────────────────────────────── + print("\n" + "=" * 60) + print(" MOBILE SIMULATION SUMMARY") + print("=" * 60) + print(f" Total : {pass_count + fail_count}") + print(f" Passed : {pass_count}") + print(f" Failed : {fail_count}") + if fail_count: + print("\n FAILURES:") + for r in results: + if r["status"] == "FAIL": + print(f" ✗ {r['test']}: {r['detail']}") + print(f"\n Screenshots: {SCREENSHOTS_DIR}") + print("=" * 60) + return fail_count + +if __name__ == "__main__": + failed = asyncio.run(run_full_mobile_simulation()) + sys.exit(0 if failed == 0 else 1)