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)