diff --git a/.changes/56.added.md b/.changes/56.added.md new file mode 100644 index 0000000..f659371 --- /dev/null +++ b/.changes/56.added.md @@ -0,0 +1 @@ +Registry Web release pages can return to a Web app with an exact Extension version for installation confirmation. diff --git a/.github/workflows/registry-preview-cleanup.yml b/.github/workflows/registry-preview-cleanup.yml index c5a78f6..f905b57 100644 --- a/.github/workflows/registry-preview-cleanup.yml +++ b/.github/workflows/registry-preview-cleanup.yml @@ -36,6 +36,7 @@ jobs: timeout-minutes: 15 environment: name: preview + deployment: false steps: - name: Checkout the trusted cleanup controller uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 @@ -111,3 +112,21 @@ jobs: NEON_PROJECT_ID: ${{ vars.NEON_PROJECT_ID }} PULL_NUMBER: ${{ steps.identity.outputs.pull_number }} run: pdm run python scripts/registry_preview.py retire --pull-number "$PULL_NUMBER" + - name: Mark the retired PR deployments inactive + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + PULL_NUMBER: ${{ steps.identity.outputs.pull_number }} + with: + script: | + const deployments = await github.paginate(github.rest.repos.listDeployments, { + ...context.repo, environment: 'preview', per_page: 100, + }) + for (const deployment of deployments) { + if (deployment.payload?.pull_number !== Number(process.env.PULL_NUMBER)) continue + await github.rest.repos.createDeploymentStatus({ + ...context.repo, + deployment_id: deployment.id, + state: 'inactive', + auto_inactive: false, + }) + } diff --git a/.github/workflows/registry-preview.yml b/.github/workflows/registry-preview.yml index 8d19c64..5c5732d 100644 --- a/.github/workflows/registry-preview.yml +++ b/.github/workflows/registry-preview.yml @@ -120,12 +120,62 @@ jobs: contents: read pull-requests: read deployments: write + statuses: write runs-on: ubuntu-latest timeout-minutes: 20 environment: name: preview - url: ${{ steps.deploy.outputs.url }} + deployment: false steps: + - name: Verify PR identity after build and concurrency queue + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + PULL_NUMBER: ${{ needs.identity.outputs.pull_number }} + SOURCE_SHA: ${{ needs.identity.outputs.head_sha }} + with: + script: | + const { data: pull } = await github.rest.pulls.get({ + ...context.repo, pull_number: Number(process.env.PULL_NUMBER), + }) + if (pull.state !== 'open' || pull.base.ref !== 'main' || + pull.head.repo?.full_name !== process.env.GITHUB_REPOSITORY || + pull.head.sha !== process.env.SOURCE_SHA) { + throw new Error('PR closed or moved; no preview mutation is allowed.') + } + - name: Create the exact pull-request deployment + id: deployment + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + PULL_NUMBER: ${{ needs.identity.outputs.pull_number }} + SOURCE_SHA: ${{ needs.identity.outputs.head_sha }} + with: + script: | + const { data: deployment } = await github.rest.repos.createDeployment({ + ...context.repo, + ref: process.env.SOURCE_SHA, + environment: 'preview', + auto_merge: false, + required_contexts: [], + transient_environment: true, + production_environment: false, + payload: { pull_number: Number(process.env.PULL_NUMBER) }, + description: 'Registry pull-request preview', + }) + core.setOutput('deployment_id', String(deployment.id)) + await github.rest.repos.createDeploymentStatus({ + ...context.repo, + deployment_id: deployment.id, + state: 'in_progress', + log_url: `${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`, + }) + await github.rest.repos.createCommitStatus({ + ...context.repo, + sha: process.env.SOURCE_SHA, + context: 'ext-reg preview', + state: 'pending', + description: 'Registry preview is deploying', + target_url: `${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`, + }) - name: Checkout only the trusted controller uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: @@ -157,7 +207,7 @@ jobs: with: name: registry-image path: ${{ runner.temp }}/registry-image - - name: Recheck PR identity after build and concurrency queue + - name: Recheck PR identity before preview mutation uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: PULL_NUMBER: ${{ needs.identity.outputs.pull_number }} @@ -201,3 +251,34 @@ jobs: echo '- 目录、详情、Publisher 与原生分发运行同一 CPython 服务。' echo '- 每 PR 独立 Neon 分支与 R2 桶;不生成示例扩展。' } >> "$GITHUB_STEP_SUMMARY" + - name: Report the exact pull-request preview + if: ${{ always() && steps.deployment.outputs.deployment_id != '' }} + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + DEPLOYMENT_ID: ${{ steps.deployment.outputs.deployment_id }} + SOURCE_SHA: ${{ needs.identity.outputs.head_sha }} + PREVIEW_RESULT: ${{ job.status }} + PREVIEW_URL: ${{ steps.deploy.outputs.url }} + with: + script: | + const succeeded = process.env.PREVIEW_RESULT === 'success' && Boolean(process.env.PREVIEW_URL) + const runUrl = `${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}` + await github.rest.repos.createDeploymentStatus({ + ...context.repo, + deployment_id: Number(process.env.DEPLOYMENT_ID), + state: succeeded ? 'success' : 'failure', + log_url: runUrl, + auto_inactive: false, + ...(succeeded ? { environment_url: process.env.PREVIEW_URL } : {}), + }) + await github.rest.repos.createCommitStatus({ + ...context.repo, + sha: process.env.SOURCE_SHA, + context: 'ext-reg preview', + state: succeeded ? 'success' : 'error', + description: succeeded ? 'Registry preview is ready' : 'Registry preview failed', + target_url: succeeded ? process.env.PREVIEW_URL : runUrl, + }) + if (!succeeded && process.env.PREVIEW_RESULT === 'success') { + core.setFailed('Preview deployment did not report a URL.') + } diff --git a/docs/30-unit-tdd/registry-web.md b/docs/30-unit-tdd/registry-web.md index eb48cc7..83ed44d 100644 --- a/docs/30-unit-tdd/registry-web.md +++ b/docs/30-unit-tdd/registry-web.md @@ -21,9 +21,20 @@ version. Missing or withdrawn versions return a human-readable 404. Existing `/v1`, `/simple`, `/packages`, and Module Federation asset paths retain their machine-facing semantics. +The detail page links to client-web's `/extensions?install=&version=`. +The Registry neither reads deployment state nor installs the Release. When a +client-web page opens the Registry, it passes only its Web origin as +`client_origin`; the catalog preserves it through search, filters, and version +links. The Registry accepts HTTPS origins (and local HTTP for development), +then constructs a fixed client-web path and displays the destination host. A +direct Registry visit defaults to `https://app.inkcre.dev`. The return query +contains only the Extension name and exact version; client-web reads the +Release anew from its own configured Registry and requires confirmation. + The UI does not invent descriptions, popularity, trust badges, compatibility verdicts, installation state, or download counts that Registry does not own. -Package inspection and copying an ID do not install or activate an Extension. +Package inspection, copying an ID, and opening the install link do not install +or activate an Extension. ## Publisher workspace diff --git a/docs/40-deployment/pull-request-previews.md b/docs/40-deployment/pull-request-previews.md index 7d415cf..b740f9f 100644 --- a/docs/40-deployment/pull-request-previews.md +++ b/docs/40-deployment/pull-request-previews.md @@ -21,7 +21,7 @@ GitHub `preview` 环境需要: | `CLOUDFLARE_PREVIEW_API_TOKEN` secret | 创建和清理 R2 桶、创建和撤销桶范围的账户 API token | | `REGISTRY_PREVIEW_PUBLISHER_TOKEN` secret | 随机审阅凭据,只登记 `reviewer` namespace | -Cloudflare 控制 token 的账户级权限只属于可信 runner。应用拿到的 S3 key 来自单个桶的 `Workers R2 Storage Bucket Item Write` token;不要直接把账户管理 token 放入应用。部署只登记审阅者凭据,不生成示例扩展、版本或文件。发布验收应在一次性环境完成,共享预览仅保留明确用于人工审阅的数据。 +Cloudflare 控制 token 的账户级权限只属于可信 runner。应用拿到的 S3 key 来自单个桶的 `Workers R2 Storage Bucket Item Write` token;不要直接把账户管理 token 放入应用。部署保留 `reviewer` namespace 的审阅凭据,并通过正式 Publisher API 在每个 PR 的隔离库和桶内发布一个固定的 `inkcre/rss` 0.2.1 Release。wheel 从公开生产 Registry 下载,按固定 SHA-256 核验后上传;关联元数据与生产发行的源码提交、构建号一致,不复制生产数据库或凭据。控制器为播种临时授予 `inkcre` namespace 凭据,发布后立即撤销。后续部署核对现有 Release 和 wheel,不覆盖人工审阅数据;固定产物不可用或预览中存在冲突时交付失败,不将空目录或错误产物报告为可用。此固定 Release 用于浏览目录、版本页和经预览 Registry 实际安装的验收,不代表生产目录快照。 关闭 PR 由可信 cleanup 工作流核验同仓库与 closed 状态,先停止 app,再清理桶对象、桶和对象 token,删除数据库分支与 app。清理失败保留可定位的资源信息,重新运行同一 PR 的清理命令;不得通过范围搜索删除其他 PR、基础分支或生产资源。 diff --git a/scripts/check_registry.py b/scripts/check_registry.py index 9c863ab..4f9a9c6 100644 --- a/scripts/check_registry.py +++ b/scripts/check_registry.py @@ -76,6 +76,8 @@ async def request(method: str, path: str, expected: int = 200, *, private=False, } assert (await request("GET", "/v1/extensions")).json() == [] await request("GET", "/") + await request("GET", "/?client_origin=https://example.invalid/path", 400) + await request("GET", "/?client_origin=https://user@example.invalid", 400) await request("GET", "/v1/publisher", 401) await request( "POST", "/v1/extensions/other/extension/releases", 403, private=True, json=association @@ -156,7 +158,18 @@ async def request(method: str, path: str, expected: int = 200, *, private=False, await request("POST", version_path + "/publish", private=True) await request("POST", version_path + "/publish", private=True) assert (await request("GET", "/v1/extensions")).json()[0]["name"] == "check/extension" - await request("GET", "/explore/check/extension?version=1.0.0") + catalog = await request("GET", "/?client_origin=https://preview.example") + assert 'name="client_origin" value="https://preview.example"' in catalog.text + assert "/explore/check/extension?client_origin=https%3A" in catalog.text + detail = await request( + "GET", "/explore/check/extension?version=1.0.0&client_origin=https://preview.example" + ) + assert ( + 'href="https://preview.example/extensions?install=check%2Fextension&version=1.0.0"' + in detail.text + ) + assert "version=1.0.0&client_origin=https%3A" in detail.text + assert "Install in preview.example" in detail.text await request("GET", "/v1/extensions/check/extension") assert (await request("GET", version_path)).json()["state"] == "published" await request("GET", "/simple/") diff --git a/scripts/registry_preview.py b/scripts/registry_preview.py index 221b64c..e221a63 100644 --- a/scripts/registry_preview.py +++ b/scripts/registry_preview.py @@ -22,6 +22,7 @@ import httpx from botocore.config import Config from registry_database import configure_runtime_login +from registry_preview_seed import seed def request(client: httpx.Client, method: str, path: str, *, missing=False, **kwargs): @@ -245,6 +246,44 @@ def deploy(heroku, neon, cloudflare, name: str, branch_name: str, image: str, re run("heroku", "container:release", "web", "--app", name) run("heroku", "ps:scale", "web=1:eco", "--app", name) smoke(origin, revision) + seed_token = secrets.token_urlsafe(32) + run( + "docker", + "run", + "--rm", + "-i", + "--env", + "DATABASE_URL", + image, + "python", + "-m", + "inkcre_extension_registry.admin", + "grant", + "inkcre", + "--label", + "preview fixture", + env=database_env, + input=seed_token + "\n", + ) + try: + seed(origin, seed_token) + finally: + run( + "docker", + "run", + "--rm", + "--env", + "DATABASE_URL", + image, + "python", + "-m", + "inkcre_extension_registry.admin", + "revoke", + "inkcre", + "--label", + "preview fixture", + env=database_env, + ) print(f"Preview: {origin}\nSource: {revision}\nNeon branch: {branch['id']}") if os.environ.get("GITHUB_OUTPUT"): with Path(os.environ["GITHUB_OUTPUT"]).open("a") as output: diff --git a/scripts/registry_preview_seed.py b/scripts/registry_preview_seed.py new file mode 100644 index 0000000..745c95d --- /dev/null +++ b/scripts/registry_preview_seed.py @@ -0,0 +1,107 @@ +"""Publish one pinned, installable Release into an isolated Registry preview.""" + +from __future__ import annotations + +import argparse +import hashlib +import os + +import httpx + +VERSION = "0.2.1" +WHEEL_NAME = "inkcre_ext_rss-0.2.1-py3-none-any.whl" +WHEEL_SHA256 = "190ff7d15b11a341c2367b87b2348832e7fe7d8042da512f2996926645a6d275" +WHEEL_URL = f"https://registry.inkcre.dev/packages/inkcre-ext-rss/{VERSION}/{WHEEL_NAME}" +RELEASE_PATH = f"/v1/extensions/inkcre/rss/releases/{VERSION}" +PACKAGE_PATH = f"/packages/inkcre-ext-rss/{VERSION}/{WHEEL_NAME}" + + +def seed(origin: str, token: str) -> None: + with httpx.Client( + base_url=origin, headers={"Authorization": f"Bearer {token}"}, timeout=60 + ) as registry: + current = registry.get(RELEASE_PATH) + if current.status_code == 404: + wheel_response = httpx.get(WHEEL_URL, timeout=60) + wheel_response.raise_for_status() + wheel = wheel_response.content + if hashlib.sha256(wheel).hexdigest() != WHEEL_SHA256: + raise RuntimeError("Pinned RSS preview wheel digest changed") + + prepared = registry.post( + "/v1/extensions/inkcre/rss/releases", + json={ + "nickname": "RSS/Atom Feeds", + "version": VERSION, + "python": { + "project": "inkcre-ext-rss", + "host_sdk": "core-py", + "host_sdk_version": ">=0.2.0 <0.4.0", + "entry_point": { + "group": "inkcre.core.extensions", + "name": "rss", + "object": "extensions.rss:Extension", + }, + "source_repository": "https://github.com/InKCre/core-py", + "source_revision": "8f7a17343c13f6c4af19e5ea9b58ceb6b13654c2", + "build_id": "35591185410", + }, + }, + ) + prepared.raise_for_status() + uploaded = registry.post( + "/legacy/", + data={ + ":action": "file_upload", + "protocol_version": "1", + "name": "inkcre-ext-rss", + "version": VERSION, + "filetype": "bdist_wheel", + "metadata_version": "2.4", + "sha256_digest": WHEEL_SHA256, + }, + files={"content": (WHEEL_NAME, wheel, "application/octet-stream")}, + ) + uploaded.raise_for_status() + published = registry.post(RELEASE_PATH + "/publish") + published.raise_for_status() + else: + current.raise_for_status() + + release = registry.get(RELEASE_PATH) + release.raise_for_status() + record = release.json() + if ( + record["name"] != "inkcre/rss" + or record["version"] != VERSION + or record["state"] != "published" + or record["nickname"] != "RSS/Atom Feeds" + or record["python"]["project"] != "inkcre-ext-rss" + or record["python"]["host_sdk"] != "core-py" + or record["python"]["host_sdk_version"] != ">=0.2.0 <0.4.0" + or record["python"]["entry_point"] + != { + "group": "inkcre.core.extensions", + "name": "rss", + "object": "extensions.rss:Extension", + } + or record["module_federation"] is not None + ): + raise RuntimeError("Preview RSS Release conflicts with the pinned fixture") + package = registry.head(PACKAGE_PATH) + package.raise_for_status() + if package.headers.get("etag") != f'"{WHEEL_SHA256}"': + raise RuntimeError("Preview RSS wheel does not match the pinned fixture") + catalog = registry.get("/v1/extensions") + catalog.raise_for_status() + if not any(item["name"] == "inkcre/rss" for item in catalog.json()): + raise RuntimeError("Preview RSS Release is missing from the catalog") + registry.get(f"/explore/inkcre/rss?version={VERSION}").raise_for_status() + print(f"Preview fixture ready: inkcre/rss {VERSION}") + + +if __name__ == "__main__": + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--origin", required=True) + args = parser.parse_args() + seed(args.origin, os.environ["INKCRE_PREVIEW_SEED_TOKEN"]) diff --git a/src/inkcre_extension_registry/service/app.py b/src/inkcre_extension_registry/service/app.py index 0fe5140..30cdc21 100644 --- a/src/inkcre_extension_registry/service/app.py +++ b/src/inkcre_extension_registry/service/app.py @@ -76,6 +76,7 @@ ) from .storage import ArtifactStore from .ui import ( + DEFAULT_CLIENT_WEB_ORIGIN, SCRIPT, extension_catalog_html, extension_detail_html, @@ -92,6 +93,38 @@ r"^/legacy/$|^/v1/extensions/[^/]+/[^/]+/releases/[^/]+/(module-federation|documentation/[^/]+)$" ) + +def _client_web_origin(value: str | None) -> str: + if value is None: + return DEFAULT_CLIENT_WEB_ORIGIN + if ( + not value + or any(character.isspace() for character in value) + or any(character in value for character in ("\\", "%")) + ): + raise HTTPException(400, "client_origin must be a Web origin") + try: + parsed = urlparse(value) + hostname = parsed.hostname + if parsed.port == 0: + raise ValueError("invalid port") + except ValueError as error: + raise HTTPException(400, "client_origin must be a Web origin") from error + local_http = parsed.scheme == "http" and hostname in {"localhost", "127.0.0.1", "::1"} + if ( + not (parsed.scheme == "https" or local_http) + or not hostname + or parsed.username is not None + or parsed.password is not None + or parsed.path not in {"", "/"} + or parsed.params + or parsed.query + or parsed.fragment + ): + raise HTTPException(400, "client_origin must be a Web origin") + return f"{parsed.scheme}://{parsed.netloc}" + + DOCUMENTATION_ERRORS = { code: {"model": RegistryError, "description": description} for code, description in { @@ -569,10 +602,17 @@ async def extension_catalog( request: Request, q: Annotated[str, Query(max_length=200)] = "", publisher: Annotated[str, Query(max_length=64)] = "", + client_origin: Annotated[str | None, Query(max_length=512)] = None, ) -> HTMLResponse: + web_origin = _client_web_origin(client_origin) extensions = await _repository(request).list_extensions() return html_response( - extension_catalog_html(extensions, query=q.strip(), namespace=publisher) + extension_catalog_html( + extensions, + query=q.strip(), + namespace=publisher, + client_origin=web_origin, + ) ) @app.get("/assets/registry.js", include_in_schema=False) @@ -592,7 +632,9 @@ async def extension_detail( name: RegistrySegment, request: Request, version: Annotated[str | None, Query(max_length=128)] = None, + client_origin: Annotated[str | None, Query(max_length=512)] = None, ) -> HTMLResponse: + web_origin = _client_web_origin(client_origin) extension_name = _validate_identity(namespace, name) extension = await _repository(request).get_extension(extension_name) selected = select_release(extension, version) if extension else None @@ -602,7 +644,11 @@ async def extension_detail( hosted = await _documentation(request).discover( extension_name, selected.version, request.app.state.settings ) - document = extension_detail_html(extension, version, hosted) if extension else None + document = ( + extension_detail_html(extension, version, hosted, client_origin=web_origin) + if extension + else None + ) if document is None: return html_response( page( diff --git a/src/inkcre_extension_registry/service/static/registry.css b/src/inkcre_extension_registry/service/static/registry.css index 42c3c19..c87a523 100644 --- a/src/inkcre_extension_registry/service/static/registry.css +++ b/src/inkcre_extension_registry/service/static/registry.css @@ -1 +1 @@ -:root{--ref-elevation-raised-low-shadow-type:dropShadow;--ref-elevation-raised-low-radius:2;--ref-elevation-raised-low-color:rgba(12, 21, 48, 0.0784313725);--ref-elevation-raised-low-offset-x:0;--ref-elevation-raised-low-offset-y:1;--ref-elevation-raised-low-spread:0;--ref-elevation-raised-md-shadow-type:dropShadow;--ref-elevation-raised-md-radius:20;--ref-elevation-raised-md-color:rgba(12, 21, 48, 0.1215686275);--ref-elevation-raised-md-offset-x:0;--ref-elevation-raised-md-offset-y:6;--ref-elevation-raised-md-spread:0;--ref-elevation-raised-high-shadow-type:dropShadow;--ref-elevation-raised-high-radius:32;--ref-elevation-raised-high-color:rgba(12, 21, 84, 0.1607843137);--ref-elevation-raised-high-offset-x:0;--ref-elevation-raised-high-offset-y:12;--ref-elevation-raised-high-spread:0;--ref-color-neutral-2:#070707;--ref-color-neutral-6:#131313;--ref-color-neutral-12:#1f1f1f;--ref-color-neutral-20:#303030;--ref-color-neutral-30:#474747;--ref-color-neutral-40:#5e5e5e;--ref-color-neutral-50:#777777;--ref-color-neutral-60:#919191;--ref-color-neutral-70:#ababab;--ref-color-neutral-80:#c6c6c6;--ref-color-neutral-90:#e2e2e2;--ref-color-neutral-94:#eeeeee;--ref-color-neutral-98:#f9f9f9;--ref-color-neutral-100:#ffffff;--ref-color-tertiary-40:#1f9aa5;--ref-color-tertiary-90:#d0f4f4;--ref-color-danger-10:#410008;--ref-color-danger-30:#92001e;--ref-color-danger-35:#a80324;--ref-color-danger-40:#b9192f;--ref-color-danger-60:#ff525c;--ref-color-danger-65:#ff7073;--ref-color-danger-70:#ff8889;--ref-color-danger-80:#ffb3b1;--ref-color-danger-90:#ffdad8;--ref-color-danger-98:#fff8f7;--ref-color-brand-2:#0b1220;--ref-color-brand-6:#121928;--ref-color-brand-12:#1f2635;--ref-color-brand-20:#303747;--ref-color-brand-30:#474e5f;--ref-color-brand-40:#5f6677;--ref-color-brand-50:#787e91;--ref-color-brand-60:#9198ac;--ref-color-brand-70:#acb3c7;--ref-color-brand-80:#b2bccf;--ref-color-brand-90:#c8cee3;--ref-color-brand-94:#f7f7fb;--ref-color-brand-98:#fbfcff;--ref-radius-xs:4px;--ref-radius-sm:8px;--ref-radius-md:16px;--ref-radius-lg:32px;--ref-radius-none:0px;--ref-radius-full:999px;--ref-space-md:16px;--ref-space-lg:32px;--ref-space-xl:56px;--ref-space-xs:4px;--ref-space-sm:8px;--ref-typo-family-sans:system-ui, -apple-system, sans-serif;--ref-typo-family-mono: SF Mono , Monaco, Roboto Mono , monospace;--ref-breakpoint-mobile:768px;--ref-breakpoint-tablet:1024px;--ref-breakpoint-desktop:1280px;--ref-opacity-muted:0.72;--ref-opacity-medium:0.84;--ref-opacity-strong:0.92;--ref-size-lg:48px;--ref-size-md:36px;--ref-size-sm:24px;--ref-size-icon-sm:20px;--ref-size-icon-md:24px;--ref-size-icon-lg:36px;--ref-size-iconfont-sm:8px;--ref-size-iconfont-md:12px;--ref-size-iconfont-lg:18px;--ref-size-xl:58px;--ref-size-xs:18px;--ref-font-label-lg-font-size:14px;--ref-font-label-lg-text-decoration:none;--ref-font-label-lg-font-family:Inter;--ref-font-label-lg-font-weight:400;--ref-font-label-lg-font-style:normal;--ref-font-label-lg-font-stretch:normal;--ref-font-label-lg-letter-spacing:0px;--ref-font-label-lg-line-height:16px;--ref-font-label-lg-paragraph-indent:0px;--ref-font-label-lg-paragraph-spacing:0px;--ref-font-label-lg-text-case:none;--ref-font-label-md-font-size:12px;--ref-font-label-md-text-decoration:none;--ref-font-label-md-font-family:Inter;--ref-font-label-md-font-weight:400;--ref-font-label-md-font-style:normal;--ref-font-label-md-font-stretch:normal;--ref-font-label-md-letter-spacing:0px;--ref-font-label-md-line-height:16px;--ref-font-label-md-paragraph-indent:0px;--ref-font-label-md-paragraph-spacing:0px;--ref-font-label-md-text-case:none;--ref-font-label-sm-font-size:10px;--ref-font-label-sm-text-decoration:none;--ref-font-label-sm-font-family:Roboto;--ref-font-label-sm-font-weight:700;--ref-font-label-sm-font-style:normal;--ref-font-label-sm-font-stretch:normal;--ref-font-label-sm-letter-spacing:0.7px;--ref-font-label-sm-line-height:14px;--ref-font-label-sm-paragraph-indent:0px;--ref-font-label-sm-paragraph-spacing:0px;--ref-font-label-sm-text-case:none;--ref-font-label-lg-underlined-font-size:14px;--ref-font-label-lg-underlined-text-decoration:underline;--ref-font-label-lg-underlined-font-family:Inter;--ref-font-label-lg-underlined-font-weight:400;--ref-font-label-lg-underlined-font-style:normal;--ref-font-label-lg-underlined-font-stretch:normal;--ref-font-label-lg-underlined-letter-spacing:0px;--ref-font-label-lg-underlined-line-height:16px;--ref-font-label-lg-underlined-paragraph-indent:0px;--ref-font-label-lg-underlined-paragraph-spacing:0px;--ref-font-label-lg-underlined-text-case:none;--ref-font-label-lg-mono-font-size:14px;--ref-font-label-lg-mono-text-decoration:none;--ref-font-label-lg-mono-font-family:JetBrains Mono;--ref-font-label-lg-mono-font-weight:400;--ref-font-label-lg-mono-font-style:normal;--ref-font-label-lg-mono-font-stretch:normal;--ref-font-label-lg-mono-letter-spacing:0px;--ref-font-label-lg-mono-line-height:16px;--ref-font-label-lg-mono-paragraph-indent:0px;--ref-font-label-lg-mono-paragraph-spacing:0px;--ref-font-label-lg-mono-text-case:none;--ref-font-label-sm-mono-font-size:10px;--ref-font-label-sm-mono-text-decoration:none;--ref-font-label-sm-mono-font-family:JetBrains Mono;--ref-font-label-sm-mono-font-weight:700;--ref-font-label-sm-mono-font-style:normal;--ref-font-label-sm-mono-font-stretch:normal;--ref-font-label-sm-mono-letter-spacing:0.4px;--ref-font-label-sm-mono-line-height:14px;--ref-font-label-sm-mono-paragraph-indent:0px;--ref-font-label-sm-mono-paragraph-spacing:0px;--ref-font-label-sm-mono-text-case:none;--ref-font-title-sm-font-size:22px;--ref-font-title-sm-text-decoration:none;--ref-font-title-sm-font-family:Inter;--ref-font-title-sm-font-weight:400;--ref-font-title-sm-font-style:normal;--ref-font-title-sm-font-stretch:normal;--ref-font-title-sm-letter-spacing:0px;--ref-font-title-sm-line-height:28px;--ref-font-title-sm-paragraph-indent:0px;--ref-font-title-sm-paragraph-spacing:0px;--ref-font-title-sm-text-case:none;--ref-font-title-sm-mono-font-size:22px;--ref-font-title-sm-mono-text-decoration:none;--ref-font-title-sm-mono-font-family:IBM Plex Mono;--ref-font-title-sm-mono-font-weight:400;--ref-font-title-sm-mono-font-style:normal;--ref-font-title-sm-mono-font-stretch:normal;--ref-font-title-sm-mono-letter-spacing:0.66px;--ref-font-title-sm-mono-line-height:28px;--ref-font-title-sm-mono-paragraph-indent:0px;--ref-font-title-sm-mono-paragraph-spacing:0px;--ref-font-title-sm-mono-text-case:none;--ref-font-body-lg-font-size:18px;--ref-font-body-lg-text-decoration:none;--ref-font-body-lg-font-family:Inter;--ref-font-body-lg-font-weight:400;--ref-font-body-lg-font-style:normal;--ref-font-body-lg-font-stretch:normal;--ref-font-body-lg-letter-spacing:0px;--ref-font-body-lg-line-height:24px;--ref-font-body-lg-paragraph-indent:0px;--ref-font-body-lg-paragraph-spacing:0px;--ref-font-body-lg-text-case:none;--ref-font-body-lg-mono-font-size:18px;--ref-font-body-lg-mono-text-decoration:none;--ref-font-body-lg-mono-font-family:JetBrains Mono;--ref-font-body-lg-mono-font-weight:400;--ref-font-body-lg-mono-font-style:normal;--ref-font-body-lg-mono-font-stretch:normal;--ref-font-body-lg-mono-letter-spacing:0px;--ref-font-body-lg-mono-line-height:24px;--ref-font-body-lg-mono-paragraph-indent:0px;--ref-font-body-lg-mono-paragraph-spacing:0px;--ref-font-body-lg-mono-text-case:none;--ref-font-headline-lg-font-size:36px;--ref-font-headline-lg-text-decoration:none;--ref-font-headline-lg-font-family:Inter;--ref-font-headline-lg-font-weight:400;--ref-font-headline-lg-font-style:normal;--ref-font-headline-lg-font-stretch:normal;--ref-font-headline-lg-letter-spacing:0px;--ref-font-headline-lg-line-height:48px;--ref-font-headline-lg-paragraph-indent:0px;--ref-font-headline-lg-paragraph-spacing:0px;--ref-font-headline-lg-text-case:none;--sys-elevation-raised-low-shadow-type:dropShadow;--sys-elevation-raised-low-radius:2;--sys-elevation-raised-low-color:rgba(12, 21, 48, 0.0784313725);--sys-elevation-raised-low-offset-x:0;--sys-elevation-raised-low-offset-y:1;--sys-elevation-raised-low-spread:0;--sys-elevation-raised-md-shadow-type:dropShadow;--sys-elevation-raised-md-radius:20;--sys-elevation-raised-md-color:rgba(12, 21, 48, 0.1215686275);--sys-elevation-raised-md-offset-x:0;--sys-elevation-raised-md-offset-y:6;--sys-elevation-raised-md-spread:0;--sys-elevation-raised-high-shadow-type:dropShadow;--sys-elevation-raised-high-radius:32;--sys-elevation-raised-high-color:rgba(12, 21, 84, 0.1607843137);--sys-elevation-raised-high-offset-x:0;--sys-elevation-raised-high-offset-y:12;--sys-elevation-raised-high-spread:0;--sys-radius-xs:4px;--sys-radius-sm:8px;--sys-radius-md:16px;--sys-radius-lg:32px;--sys-radius-none:0px;--sys-radius-full:999px;--sys-space-md:16px;--sys-space-lg:32px;--sys-space-xl:56px;--sys-space-xs:4px;--sys-space-sm:8px;--sys-typo-family-sans:system-ui, -apple-system, sans-serif;--sys-typo-family-mono: SF Mono , Monaco, Roboto Mono , monospace;--sys-breakpoint-mobile:768px;--sys-breakpoint-tablet:1024px;--sys-breakpoint-desktop:1280px;--sys-opacity-muted:0.72;--sys-opacity-medium:0.84;--sys-opacity-strong:0.92;--sys-size-lg:48px;--sys-size-md:36px;--sys-size-sm:24px;--sys-size-icon-sm:20px;--sys-size-icon-md:24px;--sys-size-icon-lg:36px;--sys-size-iconfont-sm:8px;--sys-size-iconfont-md:12px;--sys-size-iconfont-lg:18px;--sys-size-xl:58px;--sys-size-xs:18px;--sys-font-label-lg-font-size:14px;--sys-font-label-lg-text-decoration:none;--sys-font-label-lg-font-family:Inter;--sys-font-label-lg-font-weight:400;--sys-font-label-lg-font-style:normal;--sys-font-label-lg-font-stretch:normal;--sys-font-label-lg-letter-spacing:0px;--sys-font-label-lg-line-height:16px;--sys-font-label-lg-paragraph-indent:0px;--sys-font-label-lg-paragraph-spacing:0px;--sys-font-label-lg-text-case:none;--sys-font-label-md-font-size:12px;--sys-font-label-md-text-decoration:none;--sys-font-label-md-font-family:Inter;--sys-font-label-md-font-weight:400;--sys-font-label-md-font-style:normal;--sys-font-label-md-font-stretch:normal;--sys-font-label-md-letter-spacing:0px;--sys-font-label-md-line-height:16px;--sys-font-label-md-paragraph-indent:0px;--sys-font-label-md-paragraph-spacing:0px;--sys-font-label-md-text-case:none;--sys-font-label-sm-font-size:10px;--sys-font-label-sm-text-decoration:none;--sys-font-label-sm-font-family:Roboto;--sys-font-label-sm-font-weight:700;--sys-font-label-sm-font-style:normal;--sys-font-label-sm-font-stretch:normal;--sys-font-label-sm-letter-spacing:0.7px;--sys-font-label-sm-line-height:14px;--sys-font-label-sm-paragraph-indent:0px;--sys-font-label-sm-paragraph-spacing:0px;--sys-font-label-sm-text-case:none;--sys-font-label-lg-underlined-font-size:14px;--sys-font-label-lg-underlined-text-decoration:underline;--sys-font-label-lg-underlined-font-family:Inter;--sys-font-label-lg-underlined-font-weight:400;--sys-font-label-lg-underlined-font-style:normal;--sys-font-label-lg-underlined-font-stretch:normal;--sys-font-label-lg-underlined-letter-spacing:0px;--sys-font-label-lg-underlined-line-height:16px;--sys-font-label-lg-underlined-paragraph-indent:0px;--sys-font-label-lg-underlined-paragraph-spacing:0px;--sys-font-label-lg-underlined-text-case:none;--sys-font-label-lg-mono-font-size:14px;--sys-font-label-lg-mono-text-decoration:none;--sys-font-label-lg-mono-font-family:JetBrains Mono;--sys-font-label-lg-mono-font-weight:400;--sys-font-label-lg-mono-font-style:normal;--sys-font-label-lg-mono-font-stretch:normal;--sys-font-label-lg-mono-letter-spacing:0px;--sys-font-label-lg-mono-line-height:16px;--sys-font-label-lg-mono-paragraph-indent:0px;--sys-font-label-lg-mono-paragraph-spacing:0px;--sys-font-label-lg-mono-text-case:none;--sys-font-label-sm-mono-font-size:10px;--sys-font-label-sm-mono-text-decoration:none;--sys-font-label-sm-mono-font-family:JetBrains Mono;--sys-font-label-sm-mono-font-weight:700;--sys-font-label-sm-mono-font-style:normal;--sys-font-label-sm-mono-font-stretch:normal;--sys-font-label-sm-mono-letter-spacing:0.4px;--sys-font-label-sm-mono-line-height:14px;--sys-font-label-sm-mono-paragraph-indent:0px;--sys-font-label-sm-mono-paragraph-spacing:0px;--sys-font-label-sm-mono-text-case:none;--sys-font-title-sm-font-size:22px;--sys-font-title-sm-text-decoration:none;--sys-font-title-sm-font-family:Inter;--sys-font-title-sm-font-weight:400;--sys-font-title-sm-font-style:normal;--sys-font-title-sm-font-stretch:normal;--sys-font-title-sm-letter-spacing:0px;--sys-font-title-sm-line-height:28px;--sys-font-title-sm-paragraph-indent:0px;--sys-font-title-sm-paragraph-spacing:0px;--sys-font-title-sm-text-case:none;--sys-font-title-sm-mono-font-size:22px;--sys-font-title-sm-mono-text-decoration:none;--sys-font-title-sm-mono-font-family:IBM Plex Mono;--sys-font-title-sm-mono-font-weight:400;--sys-font-title-sm-mono-font-style:normal;--sys-font-title-sm-mono-font-stretch:normal;--sys-font-title-sm-mono-letter-spacing:0.66px;--sys-font-title-sm-mono-line-height:28px;--sys-font-title-sm-mono-paragraph-indent:0px;--sys-font-title-sm-mono-paragraph-spacing:0px;--sys-font-title-sm-mono-text-case:none;--sys-font-body-lg-font-size:18px;--sys-font-body-lg-text-decoration:none;--sys-font-body-lg-font-family:Inter;--sys-font-body-lg-font-weight:400;--sys-font-body-lg-font-style:normal;--sys-font-body-lg-font-stretch:normal;--sys-font-body-lg-letter-spacing:0px;--sys-font-body-lg-line-height:24px;--sys-font-body-lg-paragraph-indent:0px;--sys-font-body-lg-paragraph-spacing:0px;--sys-font-body-lg-text-case:none;--sys-font-body-lg-mono-font-size:18px;--sys-font-body-lg-mono-text-decoration:none;--sys-font-body-lg-mono-font-family:JetBrains Mono;--sys-font-body-lg-mono-font-weight:400;--sys-font-body-lg-mono-font-style:normal;--sys-font-body-lg-mono-font-stretch:normal;--sys-font-body-lg-mono-letter-spacing:0px;--sys-font-body-lg-mono-line-height:24px;--sys-font-body-lg-mono-paragraph-indent:0px;--sys-font-body-lg-mono-paragraph-spacing:0px;--sys-font-body-lg-mono-text-case:none;--sys-font-headline-lg-font-size:36px;--sys-font-headline-lg-text-decoration:none;--sys-font-headline-lg-font-family:Inter;--sys-font-headline-lg-font-weight:400;--sys-font-headline-lg-font-style:normal;--sys-font-headline-lg-font-stretch:normal;--sys-font-headline-lg-letter-spacing:0px;--sys-font-headline-lg-line-height:48px;--sys-font-headline-lg-paragraph-indent:0px;--sys-font-headline-lg-paragraph-spacing:0px;--sys-font-headline-lg-text-case:none}:root,html[data-theme=light]{color-scheme:light;--sys-color-text-base:#070707;--sys-color-text-muted:#919191;--sys-color-text-danger-on:#fff8f7;--sys-color-text-primary:#fbfcff;--sys-color-text-subtle:#5e5e5e;--sys-color-surface-base:#ffffff;--sys-color-surface-muted:#e2e2e2;--sys-color-surface-subtle:#f9f9f9;--sys-color-surface-primary:#121928;--sys-color-surface-primary-hover:#474e5f;--sys-color-surface-subtle-hover:#eeeeee;--sys-color-surface-danger:#a80324;--sys-color-surface-danger-hover:#ff7073;--sys-color-surface-base-hover:#eeeeee;--sys-color-border-base:#303030;--sys-color-border-strong:#070707;--sys-color-border-subtle:#c6c6c6;--sys-color-border-muted:#919191;--sys-color-feedback-error:#b9192f;--comp-switch-track-bg:#070707;--comp-switch-handle-bg:#f9f9f9;--comp-switch-label-color:#070707}@media(prefers-color-scheme: dark){:root:not([data-theme]),html[data-theme=dark]{color-scheme:dark;--sys-color-text-base:#f9f9f9;--sys-color-text-muted:#f9f9f9;--sys-color-text-danger-on:#fff8f7;--sys-color-text-primary:#0b1220;--sys-color-text-subtle:#c6c6c6;--sys-color-surface-base:#474747;--sys-color-surface-muted:#131313;--sys-color-surface-subtle:#1f1f1f;--sys-color-surface-primary:#c8cee3;--sys-color-surface-primary-hover:#9198ac;--sys-color-surface-subtle-hover:#5e5e5e;--sys-color-surface-danger:#b9192f;--sys-color-surface-danger-hover:#b9192f;--sys-color-surface-base-hover:#474747;--sys-color-border-base:#5e5e5e;--sys-color-border-strong:#c6c6c6;--sys-color-border-subtle:#5e5e5e;--sys-color-border-muted:#5e5e5e;--sys-color-feedback-error:#ff525c;--comp-switch-track-bg:#f9f9f9;--comp-switch-handle-bg:#070707;--comp-switch-label-color:#f9f9f9}}html[data-theme=dark]{color-scheme:dark;--sys-color-text-base:#f9f9f9;--sys-color-text-muted:#f9f9f9;--sys-color-text-danger-on:#fff8f7;--sys-color-text-primary:#0b1220;--sys-color-text-subtle:#c6c6c6;--sys-color-surface-base:#474747;--sys-color-surface-muted:#131313;--sys-color-surface-subtle:#1f1f1f;--sys-color-surface-primary:#c8cee3;--sys-color-surface-primary-hover:#9198ac;--sys-color-surface-subtle-hover:#5e5e5e;--sys-color-surface-danger:#b9192f;--sys-color-surface-danger-hover:#b9192f;--sys-color-surface-base-hover:#474747;--sys-color-border-base:#5e5e5e;--sys-color-border-strong:#c6c6c6;--sys-color-border-subtle:#5e5e5e;--sys-color-border-muted:#5e5e5e;--sys-color-feedback-error:#ff525c}*{box-sizing:border-box}body{margin:0;font-family:var(--ref-typo-family-sans);font-size:14px;line-height:1.5;color:var(--sys-color-text-base);background:var(--sys-color-surface-base)}[hidden]{display:none !important}a{color:inherit;text-underline-offset:3px}button,input,select,textarea{font:inherit}button:focus-visible,a:focus-visible,input:focus-visible,select:focus-visible,textarea:focus-visible{outline:2px solid var(--sys-color-border-strong);outline-offset:3px}h1,h2,h3,p{margin:0}h1{font-size:var(--sys-font-title-sm-font-size);font-weight:var(--sys-font-title-sm-font-weight);line-height:var(--sys-font-title-sm-line-height);letter-spacing:var(--sys-font-title-sm-letter-spacing)}h2,h3{font-size:var(--sys-font-body-lg-font-size);font-weight:var(--sys-font-body-lg-font-weight);line-height:var(--sys-font-body-lg-line-height);letter-spacing:var(--sys-font-body-lg-letter-spacing)}code,.extension-name{font-family:var(--ref-typo-family-mono);overflow-wrap:anywhere}.sr-only,.skip-link:not(:focus){position:absolute;width:1px;height:1px;padding:0;overflow:hidden;clip-path:inset(50%);white-space:nowrap}.skip-link:focus{display:block;padding:8px}.site-header{display:flex;align-items:center;justify-content:space-between;gap:var(--sys-space-md);padding:var(--sys-space-md) var(--sys-space-lg);background:var(--sys-color-surface-subtle);border-bottom:1px solid var(--sys-color-border-base)}.site-header nav{display:flex;align-items:center;gap:var(--sys-space-md)}.site-header nav a{text-decoration:none}.site-header [aria-current=page]{text-decoration:underline}.brand{display:flex;align-items:center;gap:var(--sys-space-sm);text-decoration:none;font-size:var(--sys-font-title-sm-font-size);font-weight:var(--sys-font-title-sm-font-weight);line-height:var(--sys-font-title-sm-line-height);letter-spacing:var(--sys-font-title-sm-letter-spacing);font-family:var(--ref-typo-family-mono)}.brand svg,.brand>span:first-child{display:block;width:32px;height:32px}.count,.extension-name,.field-help{color:var(--sys-color-text-subtle);font-size:12px}main{padding:var(--sys-space-lg) var(--sys-space-xl)}.section-heading,.detail-heading,.workspace-heading,.workspace-toolbar,.dialog-heading{display:flex;align-items:center;justify-content:space-between;gap:var(--sys-space-md);margin-bottom:var(--sys-space-md)}.count{margin-left:var(--sys-space-sm);font-weight:400}.button,.icon-button{display:inline-flex;align-items:center;justify-content:center;gap:var(--sys-space-sm);padding:var(--sys-space-sm) var(--sys-space-md);border:1px solid var(--sys-color-border-base);border-radius:0;background:var(--sys-color-surface-subtle);color:var(--sys-color-text-base);text-decoration:none;cursor:pointer;font-size:var(--sys-font-label-lg-font-size);font-weight:var(--sys-font-label-lg-font-weight);line-height:var(--sys-font-label-lg-line-height);letter-spacing:var(--sys-font-label-lg-letter-spacing)}.button:hover,.icon-button:hover{background:var(--sys-color-surface-subtle-hover)}.button:disabled,.icon-button:disabled{opacity:.5;cursor:not-allowed}.icon-button{width:32px;height:32px;padding:0;border-color:rgba(0,0,0,0)}.button-primary{background:var(--sys-color-surface-primary);color:var(--sys-color-text-primary)}.button-primary:hover{background:var(--sys-color-surface-primary-hover)}input,select,textarea{width:100%;min-width:0;padding:var(--sys-space-sm);border:1px solid var(--sys-color-border-subtle);border-radius:0;background:var(--sys-color-surface-base);color:var(--sys-color-text-base)}input,select{min-height:36px}.search-form{display:flex;gap:var(--sys-space-sm);margin-bottom:var(--sys-space-md)}.search-form input{flex:1}.search-form select{width:auto;max-width:240px}.search-summary{margin-bottom:var(--sys-space-md)}.catalog-list,.version-list{list-style:none;margin:0;padding:0}.catalog-list{display:grid;gap:var(--sys-space-md)}.extension-card{display:flex;align-items:center;justify-content:space-between;gap:var(--sys-space-md);border:1px solid var(--sys-color-border-base);padding:var(--sys-space-md);text-decoration:none}.extension-card h2{font-size:var(--sys-font-label-lg-font-size);font-weight:var(--sys-font-label-lg-font-weight);line-height:var(--sys-font-label-lg-line-height);letter-spacing:var(--sys-font-label-lg-letter-spacing);margin-bottom:var(--sys-space-sm)}.extension-card:hover{background:var(--sys-color-surface-subtle)}.card-action{flex-shrink:0;font-size:12px}.back-link{display:inline-block;margin-bottom:var(--sys-space-lg)}.detail-heading{margin-bottom:var(--sys-space-lg)}.detail-heading h1{margin-bottom:var(--sys-space-sm)}.detail-layout{display:grid;grid-template-columns:minmax(0, 1fr) 280px;gap:var(--sys-space-lg)}.distribution-panel,.sidebar-panel,.release-row,.connect-card{border:1px solid var(--sys-color-border-base);padding:var(--sys-space-md);margin-bottom:var(--sys-space-md)}.metadata{margin:var(--sys-space-md) 0}.metadata>div{display:grid;grid-template-columns:120px minmax(0, 1fr);gap:var(--sys-space-md);margin-bottom:var(--sys-space-sm)}.metadata dt{color:var(--sys-color-text-subtle)}.metadata dd{margin:0;overflow-wrap:anywhere}.version-list{margin-top:var(--sys-space-md)}.version-list a{display:flex;justify-content:space-between;gap:var(--sys-space-sm);padding:var(--sys-space-sm);text-decoration:none}.version-list [aria-current],.version-list a:hover{background:var(--sys-color-surface-subtle-hover)}.version-list span{font-size:12px}.publisher-meta>div{grid-template-columns:auto 1fr}.connect-layout{max-width:480px}label{display:block;margin-bottom:var(--sys-space-sm)}form>.button{margin-top:var(--sys-space-md)}.search-form>.button{margin-top:0}.workspace-toolbar>div,.release-actions,.release-pagination{display:flex;flex-wrap:wrap;align-items:center;gap:var(--sys-space-sm)}.release-row{display:flex;align-items:center;justify-content:space-between;gap:var(--sys-space-md)}.release-row h3,.release-row p{margin-bottom:var(--sys-space-sm);overflow-wrap:anywhere}.release-row p{color:var(--sys-color-text-subtle);font-size:12px}.badge{display:inline-block;margin-bottom:var(--sys-space-sm);padding:0 var(--sys-space-xs);border:1px solid var(--sys-color-border-subtle);font-size:12px}.release-pagination{margin-bottom:var(--sys-space-md)}.form-status:not(:empty),.empty-state,noscript p{margin:var(--sys-space-md) 0}.error{color:var(--sys-color-feedback-error)}dialog{width:min(560px,100% - 32px);max-height:calc(100% - 32px);padding:var(--sys-space-lg);border:1px solid var(--sys-color-border-base);background:var(--sys-color-surface-base);color:var(--sys-color-text-base)}dialog::backdrop{background:rgba(0,0,0,.3)}.form-grid{display:grid;grid-template-columns:1fr 1fr;gap:var(--sys-space-md);margin-top:var(--sys-space-md)}.full-width{grid-column:1/-1}.field-help{margin-top:var(--sys-space-xs)}.toast:not(:empty){position:fixed;bottom:var(--sys-space-md);right:var(--sys-space-md);max-width:calc(100% - 32px);padding:var(--sys-space-sm) var(--sys-space-md);border:1px solid var(--sys-color-border-base);background:var(--sys-color-surface-subtle)}@media(max-width: 768px){.site-header,main{padding:var(--sys-space-md)}.site-header nav{gap:var(--sys-space-sm)}.brand{font-size:18px}.search-form{flex-wrap:wrap}.search-form input{flex-basis:100%}.search-form select{flex:1;max-width:none}.detail-layout,.form-grid{grid-template-columns:minmax(0, 1fr)}.detail-heading,.workspace-toolbar,.release-row{align-items:flex-start;flex-wrap:wrap}dialog{padding:var(--sys-space-md)}.metadata>div{grid-template-columns:100px minmax(0, 1fr);gap:var(--sys-space-sm)}} +:root{--ref-elevation-raised-low-shadow-type:dropShadow;--ref-elevation-raised-low-radius:2;--ref-elevation-raised-low-color:rgba(12, 21, 48, 0.0784313725);--ref-elevation-raised-low-offset-x:0;--ref-elevation-raised-low-offset-y:1;--ref-elevation-raised-low-spread:0;--ref-elevation-raised-md-shadow-type:dropShadow;--ref-elevation-raised-md-radius:20;--ref-elevation-raised-md-color:rgba(12, 21, 48, 0.1215686275);--ref-elevation-raised-md-offset-x:0;--ref-elevation-raised-md-offset-y:6;--ref-elevation-raised-md-spread:0;--ref-elevation-raised-high-shadow-type:dropShadow;--ref-elevation-raised-high-radius:32;--ref-elevation-raised-high-color:rgba(12, 21, 84, 0.1607843137);--ref-elevation-raised-high-offset-x:0;--ref-elevation-raised-high-offset-y:12;--ref-elevation-raised-high-spread:0;--ref-color-neutral-2:#070707;--ref-color-neutral-6:#131313;--ref-color-neutral-12:#1f1f1f;--ref-color-neutral-20:#303030;--ref-color-neutral-30:#474747;--ref-color-neutral-40:#5e5e5e;--ref-color-neutral-50:#777777;--ref-color-neutral-60:#919191;--ref-color-neutral-70:#ababab;--ref-color-neutral-80:#c6c6c6;--ref-color-neutral-90:#e2e2e2;--ref-color-neutral-94:#eeeeee;--ref-color-neutral-98:#f9f9f9;--ref-color-neutral-100:#ffffff;--ref-color-tertiary-40:#1f9aa5;--ref-color-tertiary-90:#d0f4f4;--ref-color-danger-10:#410008;--ref-color-danger-30:#92001e;--ref-color-danger-35:#a80324;--ref-color-danger-40:#b9192f;--ref-color-danger-60:#ff525c;--ref-color-danger-65:#ff7073;--ref-color-danger-70:#ff8889;--ref-color-danger-80:#ffb3b1;--ref-color-danger-90:#ffdad8;--ref-color-danger-98:#fff8f7;--ref-color-brand-2:#0b1220;--ref-color-brand-6:#121928;--ref-color-brand-12:#1f2635;--ref-color-brand-20:#303747;--ref-color-brand-30:#474e5f;--ref-color-brand-40:#5f6677;--ref-color-brand-50:#787e91;--ref-color-brand-60:#9198ac;--ref-color-brand-70:#acb3c7;--ref-color-brand-80:#b2bccf;--ref-color-brand-90:#c8cee3;--ref-color-brand-94:#f7f7fb;--ref-color-brand-98:#fbfcff;--ref-radius-xs:4px;--ref-radius-sm:8px;--ref-radius-md:16px;--ref-radius-lg:32px;--ref-radius-none:0px;--ref-radius-full:999px;--ref-space-md:16px;--ref-space-lg:32px;--ref-space-xl:56px;--ref-space-xs:4px;--ref-space-sm:8px;--ref-typo-family-sans:system-ui, -apple-system, sans-serif;--ref-typo-family-mono: SF Mono , Monaco, Roboto Mono , monospace;--ref-breakpoint-mobile:768px;--ref-breakpoint-tablet:1024px;--ref-breakpoint-desktop:1280px;--ref-opacity-muted:0.72;--ref-opacity-medium:0.84;--ref-opacity-strong:0.92;--ref-size-lg:48px;--ref-size-md:36px;--ref-size-sm:24px;--ref-size-icon-sm:20px;--ref-size-icon-md:24px;--ref-size-icon-lg:36px;--ref-size-iconfont-sm:8px;--ref-size-iconfont-md:12px;--ref-size-iconfont-lg:18px;--ref-size-xl:58px;--ref-size-xs:18px;--ref-font-label-lg-font-size:14px;--ref-font-label-lg-text-decoration:none;--ref-font-label-lg-font-family:Inter;--ref-font-label-lg-font-weight:400;--ref-font-label-lg-font-style:normal;--ref-font-label-lg-font-stretch:normal;--ref-font-label-lg-letter-spacing:0px;--ref-font-label-lg-line-height:16px;--ref-font-label-lg-paragraph-indent:0px;--ref-font-label-lg-paragraph-spacing:0px;--ref-font-label-lg-text-case:none;--ref-font-label-md-font-size:12px;--ref-font-label-md-text-decoration:none;--ref-font-label-md-font-family:Inter;--ref-font-label-md-font-weight:400;--ref-font-label-md-font-style:normal;--ref-font-label-md-font-stretch:normal;--ref-font-label-md-letter-spacing:0px;--ref-font-label-md-line-height:16px;--ref-font-label-md-paragraph-indent:0px;--ref-font-label-md-paragraph-spacing:0px;--ref-font-label-md-text-case:none;--ref-font-label-sm-font-size:10px;--ref-font-label-sm-text-decoration:none;--ref-font-label-sm-font-family:Roboto;--ref-font-label-sm-font-weight:700;--ref-font-label-sm-font-style:normal;--ref-font-label-sm-font-stretch:normal;--ref-font-label-sm-letter-spacing:0.7px;--ref-font-label-sm-line-height:14px;--ref-font-label-sm-paragraph-indent:0px;--ref-font-label-sm-paragraph-spacing:0px;--ref-font-label-sm-text-case:none;--ref-font-label-lg-underlined-font-size:14px;--ref-font-label-lg-underlined-text-decoration:underline;--ref-font-label-lg-underlined-font-family:Inter;--ref-font-label-lg-underlined-font-weight:400;--ref-font-label-lg-underlined-font-style:normal;--ref-font-label-lg-underlined-font-stretch:normal;--ref-font-label-lg-underlined-letter-spacing:0px;--ref-font-label-lg-underlined-line-height:16px;--ref-font-label-lg-underlined-paragraph-indent:0px;--ref-font-label-lg-underlined-paragraph-spacing:0px;--ref-font-label-lg-underlined-text-case:none;--ref-font-label-lg-mono-font-size:14px;--ref-font-label-lg-mono-text-decoration:none;--ref-font-label-lg-mono-font-family:JetBrains Mono;--ref-font-label-lg-mono-font-weight:400;--ref-font-label-lg-mono-font-style:normal;--ref-font-label-lg-mono-font-stretch:normal;--ref-font-label-lg-mono-letter-spacing:0px;--ref-font-label-lg-mono-line-height:16px;--ref-font-label-lg-mono-paragraph-indent:0px;--ref-font-label-lg-mono-paragraph-spacing:0px;--ref-font-label-lg-mono-text-case:none;--ref-font-label-sm-mono-font-size:10px;--ref-font-label-sm-mono-text-decoration:none;--ref-font-label-sm-mono-font-family:JetBrains Mono;--ref-font-label-sm-mono-font-weight:700;--ref-font-label-sm-mono-font-style:normal;--ref-font-label-sm-mono-font-stretch:normal;--ref-font-label-sm-mono-letter-spacing:0.4px;--ref-font-label-sm-mono-line-height:14px;--ref-font-label-sm-mono-paragraph-indent:0px;--ref-font-label-sm-mono-paragraph-spacing:0px;--ref-font-label-sm-mono-text-case:none;--ref-font-title-sm-font-size:22px;--ref-font-title-sm-text-decoration:none;--ref-font-title-sm-font-family:Inter;--ref-font-title-sm-font-weight:400;--ref-font-title-sm-font-style:normal;--ref-font-title-sm-font-stretch:normal;--ref-font-title-sm-letter-spacing:0px;--ref-font-title-sm-line-height:28px;--ref-font-title-sm-paragraph-indent:0px;--ref-font-title-sm-paragraph-spacing:0px;--ref-font-title-sm-text-case:none;--ref-font-title-sm-mono-font-size:22px;--ref-font-title-sm-mono-text-decoration:none;--ref-font-title-sm-mono-font-family:IBM Plex Mono;--ref-font-title-sm-mono-font-weight:400;--ref-font-title-sm-mono-font-style:normal;--ref-font-title-sm-mono-font-stretch:normal;--ref-font-title-sm-mono-letter-spacing:0.66px;--ref-font-title-sm-mono-line-height:28px;--ref-font-title-sm-mono-paragraph-indent:0px;--ref-font-title-sm-mono-paragraph-spacing:0px;--ref-font-title-sm-mono-text-case:none;--ref-font-body-lg-font-size:18px;--ref-font-body-lg-text-decoration:none;--ref-font-body-lg-font-family:Inter;--ref-font-body-lg-font-weight:400;--ref-font-body-lg-font-style:normal;--ref-font-body-lg-font-stretch:normal;--ref-font-body-lg-letter-spacing:0px;--ref-font-body-lg-line-height:24px;--ref-font-body-lg-paragraph-indent:0px;--ref-font-body-lg-paragraph-spacing:0px;--ref-font-body-lg-text-case:none;--ref-font-body-lg-mono-font-size:18px;--ref-font-body-lg-mono-text-decoration:none;--ref-font-body-lg-mono-font-family:JetBrains Mono;--ref-font-body-lg-mono-font-weight:400;--ref-font-body-lg-mono-font-style:normal;--ref-font-body-lg-mono-font-stretch:normal;--ref-font-body-lg-mono-letter-spacing:0px;--ref-font-body-lg-mono-line-height:24px;--ref-font-body-lg-mono-paragraph-indent:0px;--ref-font-body-lg-mono-paragraph-spacing:0px;--ref-font-body-lg-mono-text-case:none;--ref-font-headline-lg-font-size:36px;--ref-font-headline-lg-text-decoration:none;--ref-font-headline-lg-font-family:Inter;--ref-font-headline-lg-font-weight:400;--ref-font-headline-lg-font-style:normal;--ref-font-headline-lg-font-stretch:normal;--ref-font-headline-lg-letter-spacing:0px;--ref-font-headline-lg-line-height:48px;--ref-font-headline-lg-paragraph-indent:0px;--ref-font-headline-lg-paragraph-spacing:0px;--ref-font-headline-lg-text-case:none;--sys-elevation-raised-low-shadow-type:dropShadow;--sys-elevation-raised-low-radius:2;--sys-elevation-raised-low-color:rgba(12, 21, 48, 0.0784313725);--sys-elevation-raised-low-offset-x:0;--sys-elevation-raised-low-offset-y:1;--sys-elevation-raised-low-spread:0;--sys-elevation-raised-md-shadow-type:dropShadow;--sys-elevation-raised-md-radius:20;--sys-elevation-raised-md-color:rgba(12, 21, 48, 0.1215686275);--sys-elevation-raised-md-offset-x:0;--sys-elevation-raised-md-offset-y:6;--sys-elevation-raised-md-spread:0;--sys-elevation-raised-high-shadow-type:dropShadow;--sys-elevation-raised-high-radius:32;--sys-elevation-raised-high-color:rgba(12, 21, 84, 0.1607843137);--sys-elevation-raised-high-offset-x:0;--sys-elevation-raised-high-offset-y:12;--sys-elevation-raised-high-spread:0;--sys-radius-xs:4px;--sys-radius-sm:8px;--sys-radius-md:16px;--sys-radius-lg:32px;--sys-radius-none:0px;--sys-radius-full:999px;--sys-space-md:16px;--sys-space-lg:32px;--sys-space-xl:56px;--sys-space-xs:4px;--sys-space-sm:8px;--sys-typo-family-sans:system-ui, -apple-system, sans-serif;--sys-typo-family-mono: SF Mono , Monaco, Roboto Mono , monospace;--sys-breakpoint-mobile:768px;--sys-breakpoint-tablet:1024px;--sys-breakpoint-desktop:1280px;--sys-opacity-muted:0.72;--sys-opacity-medium:0.84;--sys-opacity-strong:0.92;--sys-size-lg:48px;--sys-size-md:36px;--sys-size-sm:24px;--sys-size-icon-sm:20px;--sys-size-icon-md:24px;--sys-size-icon-lg:36px;--sys-size-iconfont-sm:8px;--sys-size-iconfont-md:12px;--sys-size-iconfont-lg:18px;--sys-size-xl:58px;--sys-size-xs:18px;--sys-font-label-lg-font-size:14px;--sys-font-label-lg-text-decoration:none;--sys-font-label-lg-font-family:Inter;--sys-font-label-lg-font-weight:400;--sys-font-label-lg-font-style:normal;--sys-font-label-lg-font-stretch:normal;--sys-font-label-lg-letter-spacing:0px;--sys-font-label-lg-line-height:16px;--sys-font-label-lg-paragraph-indent:0px;--sys-font-label-lg-paragraph-spacing:0px;--sys-font-label-lg-text-case:none;--sys-font-label-md-font-size:12px;--sys-font-label-md-text-decoration:none;--sys-font-label-md-font-family:Inter;--sys-font-label-md-font-weight:400;--sys-font-label-md-font-style:normal;--sys-font-label-md-font-stretch:normal;--sys-font-label-md-letter-spacing:0px;--sys-font-label-md-line-height:16px;--sys-font-label-md-paragraph-indent:0px;--sys-font-label-md-paragraph-spacing:0px;--sys-font-label-md-text-case:none;--sys-font-label-sm-font-size:10px;--sys-font-label-sm-text-decoration:none;--sys-font-label-sm-font-family:Roboto;--sys-font-label-sm-font-weight:700;--sys-font-label-sm-font-style:normal;--sys-font-label-sm-font-stretch:normal;--sys-font-label-sm-letter-spacing:0.7px;--sys-font-label-sm-line-height:14px;--sys-font-label-sm-paragraph-indent:0px;--sys-font-label-sm-paragraph-spacing:0px;--sys-font-label-sm-text-case:none;--sys-font-label-lg-underlined-font-size:14px;--sys-font-label-lg-underlined-text-decoration:underline;--sys-font-label-lg-underlined-font-family:Inter;--sys-font-label-lg-underlined-font-weight:400;--sys-font-label-lg-underlined-font-style:normal;--sys-font-label-lg-underlined-font-stretch:normal;--sys-font-label-lg-underlined-letter-spacing:0px;--sys-font-label-lg-underlined-line-height:16px;--sys-font-label-lg-underlined-paragraph-indent:0px;--sys-font-label-lg-underlined-paragraph-spacing:0px;--sys-font-label-lg-underlined-text-case:none;--sys-font-label-lg-mono-font-size:14px;--sys-font-label-lg-mono-text-decoration:none;--sys-font-label-lg-mono-font-family:JetBrains Mono;--sys-font-label-lg-mono-font-weight:400;--sys-font-label-lg-mono-font-style:normal;--sys-font-label-lg-mono-font-stretch:normal;--sys-font-label-lg-mono-letter-spacing:0px;--sys-font-label-lg-mono-line-height:16px;--sys-font-label-lg-mono-paragraph-indent:0px;--sys-font-label-lg-mono-paragraph-spacing:0px;--sys-font-label-lg-mono-text-case:none;--sys-font-label-sm-mono-font-size:10px;--sys-font-label-sm-mono-text-decoration:none;--sys-font-label-sm-mono-font-family:JetBrains Mono;--sys-font-label-sm-mono-font-weight:700;--sys-font-label-sm-mono-font-style:normal;--sys-font-label-sm-mono-font-stretch:normal;--sys-font-label-sm-mono-letter-spacing:0.4px;--sys-font-label-sm-mono-line-height:14px;--sys-font-label-sm-mono-paragraph-indent:0px;--sys-font-label-sm-mono-paragraph-spacing:0px;--sys-font-label-sm-mono-text-case:none;--sys-font-title-sm-font-size:22px;--sys-font-title-sm-text-decoration:none;--sys-font-title-sm-font-family:Inter;--sys-font-title-sm-font-weight:400;--sys-font-title-sm-font-style:normal;--sys-font-title-sm-font-stretch:normal;--sys-font-title-sm-letter-spacing:0px;--sys-font-title-sm-line-height:28px;--sys-font-title-sm-paragraph-indent:0px;--sys-font-title-sm-paragraph-spacing:0px;--sys-font-title-sm-text-case:none;--sys-font-title-sm-mono-font-size:22px;--sys-font-title-sm-mono-text-decoration:none;--sys-font-title-sm-mono-font-family:IBM Plex Mono;--sys-font-title-sm-mono-font-weight:400;--sys-font-title-sm-mono-font-style:normal;--sys-font-title-sm-mono-font-stretch:normal;--sys-font-title-sm-mono-letter-spacing:0.66px;--sys-font-title-sm-mono-line-height:28px;--sys-font-title-sm-mono-paragraph-indent:0px;--sys-font-title-sm-mono-paragraph-spacing:0px;--sys-font-title-sm-mono-text-case:none;--sys-font-body-lg-font-size:18px;--sys-font-body-lg-text-decoration:none;--sys-font-body-lg-font-family:Inter;--sys-font-body-lg-font-weight:400;--sys-font-body-lg-font-style:normal;--sys-font-body-lg-font-stretch:normal;--sys-font-body-lg-letter-spacing:0px;--sys-font-body-lg-line-height:24px;--sys-font-body-lg-paragraph-indent:0px;--sys-font-body-lg-paragraph-spacing:0px;--sys-font-body-lg-text-case:none;--sys-font-body-lg-mono-font-size:18px;--sys-font-body-lg-mono-text-decoration:none;--sys-font-body-lg-mono-font-family:JetBrains Mono;--sys-font-body-lg-mono-font-weight:400;--sys-font-body-lg-mono-font-style:normal;--sys-font-body-lg-mono-font-stretch:normal;--sys-font-body-lg-mono-letter-spacing:0px;--sys-font-body-lg-mono-line-height:24px;--sys-font-body-lg-mono-paragraph-indent:0px;--sys-font-body-lg-mono-paragraph-spacing:0px;--sys-font-body-lg-mono-text-case:none;--sys-font-headline-lg-font-size:36px;--sys-font-headline-lg-text-decoration:none;--sys-font-headline-lg-font-family:Inter;--sys-font-headline-lg-font-weight:400;--sys-font-headline-lg-font-style:normal;--sys-font-headline-lg-font-stretch:normal;--sys-font-headline-lg-letter-spacing:0px;--sys-font-headline-lg-line-height:48px;--sys-font-headline-lg-paragraph-indent:0px;--sys-font-headline-lg-paragraph-spacing:0px;--sys-font-headline-lg-text-case:none}:root,html[data-theme=light]{color-scheme:light;--sys-color-text-base:#070707;--sys-color-text-muted:#919191;--sys-color-text-danger-on:#fff8f7;--sys-color-text-primary:#fbfcff;--sys-color-text-subtle:#5e5e5e;--sys-color-surface-base:#ffffff;--sys-color-surface-muted:#e2e2e2;--sys-color-surface-subtle:#f9f9f9;--sys-color-surface-primary:#121928;--sys-color-surface-primary-hover:#474e5f;--sys-color-surface-subtle-hover:#eeeeee;--sys-color-surface-danger:#a80324;--sys-color-surface-danger-hover:#ff7073;--sys-color-surface-base-hover:#eeeeee;--sys-color-border-base:#303030;--sys-color-border-strong:#070707;--sys-color-border-subtle:#c6c6c6;--sys-color-border-muted:#919191;--sys-color-feedback-error:#b9192f;--comp-switch-track-bg:#070707;--comp-switch-handle-bg:#f9f9f9;--comp-switch-label-color:#070707}@media(prefers-color-scheme: dark){:root:not([data-theme]),html[data-theme=dark]{color-scheme:dark;--sys-color-text-base:#f9f9f9;--sys-color-text-muted:#f9f9f9;--sys-color-text-danger-on:#fff8f7;--sys-color-text-primary:#0b1220;--sys-color-text-subtle:#c6c6c6;--sys-color-surface-base:#474747;--sys-color-surface-muted:#131313;--sys-color-surface-subtle:#1f1f1f;--sys-color-surface-primary:#c8cee3;--sys-color-surface-primary-hover:#9198ac;--sys-color-surface-subtle-hover:#5e5e5e;--sys-color-surface-danger:#b9192f;--sys-color-surface-danger-hover:#b9192f;--sys-color-surface-base-hover:#474747;--sys-color-border-base:#5e5e5e;--sys-color-border-strong:#c6c6c6;--sys-color-border-subtle:#5e5e5e;--sys-color-border-muted:#5e5e5e;--sys-color-feedback-error:#ff525c;--comp-switch-track-bg:#f9f9f9;--comp-switch-handle-bg:#070707;--comp-switch-label-color:#f9f9f9}}html[data-theme=dark]{color-scheme:dark;--sys-color-text-base:#f9f9f9;--sys-color-text-muted:#f9f9f9;--sys-color-text-danger-on:#fff8f7;--sys-color-text-primary:#0b1220;--sys-color-text-subtle:#c6c6c6;--sys-color-surface-base:#474747;--sys-color-surface-muted:#131313;--sys-color-surface-subtle:#1f1f1f;--sys-color-surface-primary:#c8cee3;--sys-color-surface-primary-hover:#9198ac;--sys-color-surface-subtle-hover:#5e5e5e;--sys-color-surface-danger:#b9192f;--sys-color-surface-danger-hover:#b9192f;--sys-color-surface-base-hover:#474747;--sys-color-border-base:#5e5e5e;--sys-color-border-strong:#c6c6c6;--sys-color-border-subtle:#5e5e5e;--sys-color-border-muted:#5e5e5e;--sys-color-feedback-error:#ff525c}*{box-sizing:border-box}body{margin:0;font-family:var(--ref-typo-family-sans);font-size:14px;line-height:1.5;color:var(--sys-color-text-base);background:var(--sys-color-surface-base)}[hidden]{display:none !important}a{color:inherit;text-underline-offset:3px}button,input,select,textarea{font:inherit}button:focus-visible,a:focus-visible,input:focus-visible,select:focus-visible,textarea:focus-visible{outline:2px solid var(--sys-color-border-strong);outline-offset:3px}h1,h2,h3,p{margin:0}h1{font-size:var(--sys-font-title-sm-font-size);font-weight:var(--sys-font-title-sm-font-weight);line-height:var(--sys-font-title-sm-line-height);letter-spacing:var(--sys-font-title-sm-letter-spacing)}h2,h3{font-size:var(--sys-font-body-lg-font-size);font-weight:var(--sys-font-body-lg-font-weight);line-height:var(--sys-font-body-lg-line-height);letter-spacing:var(--sys-font-body-lg-letter-spacing)}code,.extension-name{font-family:var(--ref-typo-family-mono);overflow-wrap:anywhere}.sr-only,.skip-link:not(:focus){position:absolute;width:1px;height:1px;padding:0;overflow:hidden;clip-path:inset(50%);white-space:nowrap}.skip-link:focus{display:block;padding:8px}.site-header{display:flex;align-items:center;justify-content:space-between;gap:var(--sys-space-md);padding:var(--sys-space-md) var(--sys-space-lg);background:var(--sys-color-surface-subtle);border-bottom:1px solid var(--sys-color-border-base)}.site-header nav{display:flex;align-items:center;gap:var(--sys-space-md)}.site-header nav a{text-decoration:none}.site-header [aria-current=page]{text-decoration:underline}.brand{display:flex;align-items:center;gap:var(--sys-space-sm);text-decoration:none;font-size:var(--sys-font-title-sm-font-size);font-weight:var(--sys-font-title-sm-font-weight);line-height:var(--sys-font-title-sm-line-height);letter-spacing:var(--sys-font-title-sm-letter-spacing);font-family:var(--ref-typo-family-mono)}.brand svg,.brand>span:first-child{display:block;width:32px;height:32px}.count,.extension-name,.field-help{color:var(--sys-color-text-subtle);font-size:12px}main{padding:var(--sys-space-lg) var(--sys-space-xl)}.section-heading,.detail-heading,.workspace-heading,.workspace-toolbar,.dialog-heading{display:flex;align-items:center;justify-content:space-between;gap:var(--sys-space-md);margin-bottom:var(--sys-space-md)}.count{margin-left:var(--sys-space-sm);font-weight:400}.button,.icon-button{display:inline-flex;align-items:center;justify-content:center;gap:var(--sys-space-sm);padding:var(--sys-space-sm) var(--sys-space-md);border:1px solid var(--sys-color-border-base);border-radius:0;background:var(--sys-color-surface-subtle);color:var(--sys-color-text-base);text-decoration:none;cursor:pointer;font-size:var(--sys-font-label-lg-font-size);font-weight:var(--sys-font-label-lg-font-weight);line-height:var(--sys-font-label-lg-line-height);letter-spacing:var(--sys-font-label-lg-letter-spacing)}.button:hover,.icon-button:hover{background:var(--sys-color-surface-subtle-hover)}.button:disabled,.icon-button:disabled{opacity:.5;cursor:not-allowed}.icon-button{width:32px;height:32px;padding:0;border-color:rgba(0,0,0,0)}.button-primary{background:var(--sys-color-surface-primary);color:var(--sys-color-text-primary)}.button-primary:hover{background:var(--sys-color-surface-primary-hover)}input,select,textarea{width:100%;min-width:0;padding:var(--sys-space-sm);border:1px solid var(--sys-color-border-subtle);border-radius:0;background:var(--sys-color-surface-base);color:var(--sys-color-text-base)}input,select{min-height:36px}.search-form{display:flex;gap:var(--sys-space-sm);margin-bottom:var(--sys-space-md)}.search-form input{flex:1}.search-form select{width:auto;max-width:240px}.search-summary{margin-bottom:var(--sys-space-md)}.catalog-list,.version-list{list-style:none;margin:0;padding:0}.catalog-list{display:grid;gap:var(--sys-space-md)}.extension-card{display:flex;align-items:center;justify-content:space-between;gap:var(--sys-space-md);border:1px solid var(--sys-color-border-base);padding:var(--sys-space-md);text-decoration:none}.extension-card h2{font-size:var(--sys-font-label-lg-font-size);font-weight:var(--sys-font-label-lg-font-weight);line-height:var(--sys-font-label-lg-line-height);letter-spacing:var(--sys-font-label-lg-letter-spacing);margin-bottom:var(--sys-space-sm)}.extension-card:hover{background:var(--sys-color-surface-subtle)}.card-action{flex-shrink:0;font-size:12px}.back-link{display:inline-block;margin-bottom:var(--sys-space-lg)}.detail-heading{margin-bottom:var(--sys-space-lg)}.detail-heading h1{margin-bottom:var(--sys-space-sm)}.detail-actions{display:flex;flex-wrap:wrap;align-items:center;gap:var(--sys-space-sm)}.detail-layout{display:grid;grid-template-columns:minmax(0, 1fr) 280px;gap:var(--sys-space-lg)}.distribution-panel,.sidebar-panel,.release-row,.connect-card{border:1px solid var(--sys-color-border-base);padding:var(--sys-space-md);margin-bottom:var(--sys-space-md)}.metadata{margin:var(--sys-space-md) 0}.metadata>div{display:grid;grid-template-columns:120px minmax(0, 1fr);gap:var(--sys-space-md);margin-bottom:var(--sys-space-sm)}.metadata dt{color:var(--sys-color-text-subtle)}.metadata dd{margin:0;overflow-wrap:anywhere}.version-list{margin-top:var(--sys-space-md)}.version-list a{display:flex;justify-content:space-between;gap:var(--sys-space-sm);padding:var(--sys-space-sm);text-decoration:none}.version-list [aria-current],.version-list a:hover{background:var(--sys-color-surface-subtle-hover)}.version-list span{font-size:12px}.publisher-meta>div{grid-template-columns:auto 1fr}.connect-layout{max-width:480px}label{display:block;margin-bottom:var(--sys-space-sm)}form>.button{margin-top:var(--sys-space-md)}.search-form>.button{margin-top:0}.workspace-toolbar>div,.release-actions,.release-pagination{display:flex;flex-wrap:wrap;align-items:center;gap:var(--sys-space-sm)}.release-row{display:flex;align-items:center;justify-content:space-between;gap:var(--sys-space-md)}.release-row h3,.release-row p{margin-bottom:var(--sys-space-sm);overflow-wrap:anywhere}.release-row p{color:var(--sys-color-text-subtle);font-size:12px}.badge{display:inline-block;margin-bottom:var(--sys-space-sm);padding:0 var(--sys-space-xs);border:1px solid var(--sys-color-border-subtle);font-size:12px}.release-pagination{margin-bottom:var(--sys-space-md)}.form-status:not(:empty),.empty-state,noscript p{margin:var(--sys-space-md) 0}.error{color:var(--sys-color-feedback-error)}dialog{width:min(560px,100% - 32px);max-height:calc(100% - 32px);padding:var(--sys-space-lg);border:1px solid var(--sys-color-border-base);background:var(--sys-color-surface-base);color:var(--sys-color-text-base)}dialog::backdrop{background:rgba(0,0,0,.3)}.form-grid{display:grid;grid-template-columns:1fr 1fr;gap:var(--sys-space-md);margin-top:var(--sys-space-md)}.full-width{grid-column:1/-1}.field-help{margin-top:var(--sys-space-xs)}.toast:not(:empty){position:fixed;bottom:var(--sys-space-md);right:var(--sys-space-md);max-width:calc(100% - 32px);padding:var(--sys-space-sm) var(--sys-space-md);border:1px solid var(--sys-color-border-base);background:var(--sys-color-surface-subtle)}@media(max-width: 768px){.site-header,main{padding:var(--sys-space-md)}.site-header nav{gap:var(--sys-space-sm)}.brand{font-size:18px}.search-form{flex-wrap:wrap}.search-form input{flex-basis:100%}.search-form select{flex:1;max-width:none}.detail-layout,.form-grid{grid-template-columns:minmax(0, 1fr)}.detail-heading,.workspace-toolbar,.release-row{align-items:flex-start;flex-wrap:wrap}dialog{padding:var(--sys-space-md)}.metadata>div{grid-template-columns:100px minmax(0, 1fr);gap:var(--sys-space-sm)}} diff --git a/src/inkcre_extension_registry/service/templates/base.html b/src/inkcre_extension_registry/service/templates/base.html index 95d2f09..9444f9c 100644 --- a/src/inkcre_extension_registry/service/templates/base.html +++ b/src/inkcre_extension_registry/service/templates/base.html @@ -14,12 +14,12 @@